Skip to content

Commit 231eebc

Browse files
authored
Merge pull request #509 from lekescrew22/feat/batch-onprogress-callback
feat: expose onProgress callback on batchWithdraw and bulkCreateStreams
2 parents 223e1a8 + 2230e10 commit 231eebc

8 files changed

Lines changed: 476 additions & 334 deletions

File tree

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
"@sorostream/sdk": minor
3+
---
4+
5+
Add an optional `onProgress` callback to the batch operations so callers can render incremental progress:
6+
7+
- `batchWithdraw(streamIds, batchSize?, onProgress?)` — fires after each chunk (transaction) completes, whether it succeeded or failed.
8+
- `bulkCreateStreams(rows, { ..., onProgress })` — fires after each chunk, or after each individual row for mixed-token chunks.
9+
10+
Both callbacks receive `{ completed, total, processedIds }`, where `completed`/`total` count individual streams/rows and `processedIds` lists the stream IDs (withdraw) or recipient addresses (create) handled by the step that just finished. The new `BatchProgress` type is exported from `@sorostream/sdk`, `@sorostream/sdk/core`, and `@sorostream/sdk/batch`.

README.md

Lines changed: 331 additions & 331 deletions
Large diffs are not rendered by default.

src/SoroStreamClient.ts

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@ import type {
121121
BatchCancelResult,
122122
BatchWithdrawResult,
123123
BatchWithdrawPartialResult,
124+
BatchProgress,
124125
BulkCreateOptions,
125126
BulkCreateResult,
126127
CancelStreamParams,
@@ -2473,6 +2474,9 @@ export class SoroStreamClient<TEventData = Record<string, unknown>> {
24732474
* @param streamIds - Stream IDs to withdraw from.
24742475
* @param batchSize - Maximum operations per transaction (default 8). Chunks
24752476
* are still attempted together, but a chunk failure is recorded per-stream.
2477+
* @param onProgress - Optional callback fired after each chunk completes
2478+
* (success or failure), reporting cumulative `{ completed, total,
2479+
* processedIds }` so callers can render incremental progress.
24762480
* @returns `{ successes, failures }` — IDs that were successfully withdrawn
24772481
* and the IDs+errors that were not.
24782482
*
@@ -2483,17 +2487,27 @@ export class SoroStreamClient<TEventData = Record<string, unknown>> {
24832487
*
24842488
* @example
24852489
* ```ts
2486-
* const { successes, failures } = await client.batchWithdraw(["1", "2", "3"]);
2490+
* const { successes, failures } = await client.batchWithdraw(
2491+
* ["1", "2", "3"],
2492+
* 2,
2493+
* ({ completed, total }) => console.log(`Withdrawn ${completed}/${total}`),
2494+
* );
24872495
* if (failures.length) {
24882496
* console.warn("Some withdrawals failed:", failures);
24892497
* }
24902498
* console.log("Withdrawn:", successes);
24912499
* ```
24922500
*/
2493-
async batchWithdraw(streamIds: string[], batchSize = 8): Promise<BatchWithdrawPartialResult> {
2501+
async batchWithdraw(
2502+
streamIds: string[],
2503+
batchSize = 8,
2504+
onProgress?: (progress: BatchProgress) => void,
2505+
): Promise<BatchWithdrawPartialResult> {
24942506
const successes: string[] = [];
24952507
const failures: { id: string; error: Error }[] = [];
24962508
const recipient = await this.requireWalletAdapter().getPublicKey();
2509+
const total = streamIds.length;
2510+
let completed = 0;
24972511

24982512
for (let i = 0; i < streamIds.length; i += batchSize) {
24992513
const chunk = streamIds.slice(i, i + batchSize);
@@ -2522,6 +2536,9 @@ export class SoroStreamClient<TEventData = Record<string, unknown>> {
25222536
failures.push({ id, error: err instanceof Error ? err : new Error(String(err)) });
25232537
}
25242538
}
2539+
2540+
completed += chunk.length;
2541+
onProgress?.({ completed, total, processedIds: chunk });
25252542
}
25262543

25272544
return { successes, failures };
@@ -4478,14 +4495,22 @@ export class SoroStreamClient<TEventData = Record<string, unknown>> {
44784495
* @param options.token - Default SAC token contract address for rows that omit `token`.
44794496
* @param options.autoRenew - Whether created streams auto-renew (default false).
44804497
* @param options.batchSize - Maximum operations per transaction (default 8).
4498+
* @param options.onProgress - Optional callback fired after each chunk (or
4499+
* row, for mixed-token chunks) is submitted, reporting cumulative
4500+
* `{ completed, total, processedIds }` so callers can render incremental
4501+
* progress. Fires on failures too — a failed row still counts as processed.
44814502
* @returns `{ batches }` — one entry per submitted transaction, each with its `txHash` and the resulting `streamIds`.
44824503
* @throws {BulkCreatePartialError} If one or more rows fail; carries `successfulBatches` and `failedSlots`.
44834504
* @throws {TransactionFailedError} If a submitted transaction is rejected (wrapped into `failedSlots` rather than thrown directly).
44844505
*
44854506
* @example
44864507
* ```ts
44874508
* try {
4488-
* const { batches } = await client.bulkCreateStreams(rows, { token: usdc });
4509+
* const { batches } = await client.bulkCreateStreams(rows, {
4510+
* token: usdc,
4511+
* onProgress: ({ completed, total }) =>
4512+
* console.log(`Created ${completed}/${total} streams`),
4513+
* });
44894514
* } catch (err) {
44904515
* if (err instanceof BulkCreatePartialError) {
44914516
* console.error(`${err.failedSlots.length} stream(s) failed:`, err.failedSlots);
@@ -4502,6 +4527,9 @@ export class SoroStreamClient<TEventData = Record<string, unknown>> {
45024527
const defaultToken = options.token;
45034528
const autoRenew = options.autoRenew ?? false;
45044529
const batchSize = options.batchSize ?? 8;
4530+
const onProgress = options.onProgress;
4531+
const total = rows.length;
4532+
let completed = 0;
45054533

45064534
// Validate cliff for all rows before submitting anything
45074535
for (const row of rows) {
@@ -4543,6 +4571,9 @@ export class SoroStreamClient<TEventData = Record<string, unknown>> {
45434571
} catch (error) {
45444572
failedSlots.push({ index: i + j, row, error });
45454573
}
4574+
4575+
completed += 1;
4576+
onProgress?.({ completed, total, processedIds: [row.recipient] });
45464577
}
45474578
} else {
45484579
try {
@@ -4570,6 +4601,9 @@ export class SoroStreamClient<TEventData = Record<string, unknown>> {
45704601
failedSlots.push({ index: i + j, row, error });
45714602
});
45724603
}
4604+
4605+
completed += chunk.length;
4606+
onProgress?.({ completed, total, processedIds: chunk.map((r) => r.recipient) });
45734607
}
45744608
}
45754609

src/batch.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ export type {
2727
BatchCancelResult,
2828
BatchWithdrawResult,
2929
BatchWithdrawPartialResult,
30+
BatchProgress,
3031
} from './types.js';
3132
export { BulkCreatePartialError } from './errors.js';
3233
export type { BulkCreateFailedSlot } from './errors.js';

src/core.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@ export type {
129129
BatchCancelResult,
130130
BatchWithdrawResult,
131131
BatchWithdrawPartialResult,
132+
BatchProgress,
132133
TokenAggregate,
133134
MultisigSigner,
134135
StreamEvent,

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,7 @@ export type {
197197
BatchCancelResult,
198198
BatchWithdrawResult,
199199
BatchWithdrawPartialResult,
200+
BatchProgress,
200201
TokenAggregate,
201202
MultisigSigner,
202203
StreamEvent,

src/types.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -545,6 +545,12 @@ export interface BulkCreateOptions {
545545
autoRenew?: boolean;
546546
/** Max operations per transaction (default 8). */
547547
batchSize?: number;
548+
/**
549+
* Optional callback fired after each chunk (or row, for mixed-token chunks)
550+
* is submitted. Receives cumulative progress so callers can render
551+
* incremental progress while the bulk create is in flight.
552+
*/
553+
onProgress?: (progress: BatchProgress) => void;
548554
}
549555

550556
/** Result of one batch within a bulk create. */
@@ -577,6 +583,24 @@ export interface BatchWithdrawPartialResult {
577583
failures: { id: string; error: Error }[];
578584
}
579585

586+
/**
587+
* Progress reported after each completed step of a batch operation, via the
588+
* `onProgress` callback of {@link batchWithdraw} and {@link bulkCreateStreams}.
589+
* `completed`/`total` count individual items (streams or rows), so callers can
590+
* render incremental progress bars while the operation is in flight.
591+
*/
592+
export interface BatchProgress {
593+
/** Number of items (streams or rows) processed so far, including this step. */
594+
completed: number;
595+
/** Total number of items to process. */
596+
total: number;
597+
/**
598+
* Identifiers of the items handled by the step that just completed:
599+
* stream IDs for `batchWithdraw`, recipient addresses for `bulkCreateStreams`.
600+
*/
601+
processedIds: string[];
602+
}
603+
580604
/** Per-token aggregate of a set of streams. */
581605
export interface TokenAggregate {
582606
token: string;

test/client.test.ts

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import type {
1313
Stream,
1414
WalletAdapter,
1515
BulkStreamRow,
16+
BatchProgress,
1617
PriceFeedAdapter,
1718
FeeBumpOptions,
1819
} from '../src/types.js';
@@ -917,6 +918,32 @@ describe('SoroStreamClient batchWithdraw', () => {
917918
expect(result.failures).toEqual([]);
918919
});
919920

921+
it('fires onProgress after each chunk with cumulative counts', async () => {
922+
const progress: BatchProgress[] = [];
923+
await client.batchWithdraw(['1', '2', '3', '4', '5'], 2, (p) => progress.push(p));
924+
925+
expect(progress).toEqual([
926+
{ completed: 2, total: 5, processedIds: ['1', '2'] },
927+
{ completed: 4, total: 5, processedIds: ['3', '4'] },
928+
{ completed: 5, total: 5, processedIds: ['5'] },
929+
]);
930+
});
931+
932+
it('fires onProgress for failed chunks too', async () => {
933+
vi.spyOn(client, 'executeBatch')
934+
.mockResolvedValueOnce('tx1')
935+
.mockRejectedValueOnce(new Error('chunk failed'));
936+
937+
const progress: BatchProgress[] = [];
938+
const result = await client.batchWithdraw(['1', '2', '3', '4'], 2, (p) => progress.push(p));
939+
940+
expect(progress).toEqual([
941+
{ completed: 2, total: 4, processedIds: ['1', '2'] },
942+
{ completed: 4, total: 4, processedIds: ['3', '4'] },
943+
]);
944+
expect(result.failures).toHaveLength(2);
945+
});
946+
920947
it('records failures when executeBatch rejects for a chunk', async () => {
921948
vi.spyOn(client, 'executeBatch')
922949
.mockResolvedValueOnce('tx1')
@@ -997,6 +1024,50 @@ describe('SoroStreamClient bulkCreateStreams', () => {
9971024
expect(result.batches).toHaveLength(1);
9981025
expect(result.batches[0]!.streamIds).toEqual([]);
9991026
});
1027+
1028+
it('fires onProgress after each chunk with cumulative counts', async () => {
1029+
vi.spyOn(client, 'getStreamsBySender').mockResolvedValue([]);
1030+
1031+
const rows: BulkStreamRow[] = [
1032+
{ recipient: TEST_PK, amount: 100n, durationSeconds: 3600 },
1033+
{ recipient: TEST_PK, amount: 200n, durationSeconds: 7200 },
1034+
{ recipient: TEST_PK, amount: 300n, durationSeconds: 3600 },
1035+
];
1036+
1037+
const progress: BatchProgress[] = [];
1038+
await client.bulkCreateStreams(rows, {
1039+
token: TEST_TOKEN,
1040+
batchSize: 2,
1041+
onProgress: (p) => progress.push(p),
1042+
});
1043+
1044+
expect(progress).toEqual([
1045+
{ completed: 2, total: 3, processedIds: [TEST_PK, TEST_PK] },
1046+
{ completed: 3, total: 3, processedIds: [TEST_PK] },
1047+
]);
1048+
});
1049+
1050+
it('fires onProgress per row when a chunk has mixed tokens', async () => {
1051+
vi.spyOn(client, 'getStreamsBySender').mockResolvedValue([]);
1052+
vi.spyOn(client, 'buildAndSubmit').mockResolvedValue({ txHash: 'tx_mixed', ledger: 0 });
1053+
1054+
const otherToken = VALID_CONTRACT;
1055+
const rows: BulkStreamRow[] = [
1056+
{ recipient: TEST_PK, amount: 100n, durationSeconds: 3600, token: TEST_TOKEN },
1057+
{ recipient: TEST_PK, amount: 200n, durationSeconds: 7200, token: otherToken },
1058+
];
1059+
1060+
const progress: BatchProgress[] = [];
1061+
await client.bulkCreateStreams(rows, {
1062+
token: TEST_TOKEN,
1063+
onProgress: (p) => progress.push(p),
1064+
});
1065+
1066+
expect(progress).toEqual([
1067+
{ completed: 1, total: 2, processedIds: [TEST_PK] },
1068+
{ completed: 2, total: 2, processedIds: [TEST_PK] },
1069+
]);
1070+
});
10001071
});
10011072

10021073
// ── Pre-flight validation tests (Issue 2) ────────────────────────────────────

0 commit comments

Comments
 (0)