@tanstack/pacer 0.20.1 → 0.21.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/async-batcher.cjs +72 -67
- package/dist/async-batcher.cjs.map +1 -1
- package/dist/async-batcher.js +72 -67
- package/dist/async-batcher.js.map +1 -1
- package/dist/async-debouncer.cjs +83 -74
- package/dist/async-debouncer.cjs.map +1 -1
- package/dist/async-debouncer.js +83 -74
- package/dist/async-debouncer.js.map +1 -1
- package/dist/async-queuer.cjs +102 -94
- package/dist/async-queuer.cjs.map +1 -1
- package/dist/async-queuer.js +102 -94
- package/dist/async-queuer.js.map +1 -1
- package/dist/async-rate-limiter.cjs +104 -93
- package/dist/async-rate-limiter.cjs.map +1 -1
- package/dist/async-rate-limiter.js +104 -93
- package/dist/async-rate-limiter.js.map +1 -1
- package/dist/async-retryer.cjs +67 -59
- package/dist/async-retryer.cjs.map +1 -1
- package/dist/async-retryer.js +67 -59
- package/dist/async-retryer.js.map +1 -1
- package/dist/async-throttler.cjs +81 -73
- package/dist/async-throttler.cjs.map +1 -1
- package/dist/async-throttler.js +81 -73
- package/dist/async-throttler.js.map +1 -1
- package/dist/batcher.cjs +45 -40
- package/dist/batcher.cjs.map +1 -1
- package/dist/batcher.js +45 -40
- package/dist/batcher.js.map +1 -1
- package/dist/debouncer.cjs +41 -36
- package/dist/debouncer.cjs.map +1 -1
- package/dist/debouncer.js +41 -36
- package/dist/debouncer.js.map +1 -1
- package/dist/event-client.cjs +8 -0
- package/dist/event-client.cjs.map +1 -1
- package/dist/event-client.js +8 -0
- package/dist/event-client.js.map +1 -1
- package/dist/queuer.cjs +92 -85
- package/dist/queuer.cjs.map +1 -1
- package/dist/queuer.js +92 -85
- package/dist/queuer.js.map +1 -1
- package/dist/rate-limiter.cjs +77 -66
- package/dist/rate-limiter.cjs.map +1 -1
- package/dist/rate-limiter.js +77 -66
- package/dist/rate-limiter.js.map +1 -1
- package/dist/throttler.cjs +49 -44
- package/dist/throttler.cjs.map +1 -1
- package/dist/throttler.js +49 -44
- package/dist/throttler.js.map +1 -1
- package/dist/utils.cjs.map +1 -1
- package/dist/utils.js.map +1 -1
- package/package.json +2 -2
package/dist/async-batcher.cjs
CHANGED
|
@@ -106,17 +106,39 @@ const defaultOptions = {
|
|
|
106
106
|
* ```
|
|
107
107
|
*/
|
|
108
108
|
var AsyncBatcher = class {
|
|
109
|
-
#timeoutId
|
|
109
|
+
#timeoutId;
|
|
110
110
|
constructor(fn, initialOptions) {
|
|
111
111
|
this.fn = fn;
|
|
112
112
|
this.store = new _tanstack_store.Store(getDefaultAsyncBatcherState());
|
|
113
113
|
this.asyncRetryers = /* @__PURE__ */ new Map();
|
|
114
|
+
this.#timeoutId = null;
|
|
114
115
|
this.setOptions = (newOptions) => {
|
|
115
116
|
this.options = {
|
|
116
117
|
...this.options,
|
|
117
118
|
...newOptions
|
|
118
119
|
};
|
|
119
120
|
};
|
|
121
|
+
this.#setState = (newState) => {
|
|
122
|
+
this.store.setState((state) => {
|
|
123
|
+
const combinedState = {
|
|
124
|
+
...state,
|
|
125
|
+
...newState
|
|
126
|
+
};
|
|
127
|
+
const { isExecuting, isPending, items } = combinedState;
|
|
128
|
+
const size = items.length;
|
|
129
|
+
const isEmpty = size === 0;
|
|
130
|
+
return {
|
|
131
|
+
...combinedState,
|
|
132
|
+
isEmpty,
|
|
133
|
+
size,
|
|
134
|
+
status: isExecuting ? "executing" : isPending ? "pending" : isEmpty ? "idle" : "populated"
|
|
135
|
+
};
|
|
136
|
+
});
|
|
137
|
+
require_event_client.emitChange("AsyncBatcher", this);
|
|
138
|
+
};
|
|
139
|
+
this.#getWait = () => {
|
|
140
|
+
return require_utils.parseFunctionOrValue(this.options.wait, this);
|
|
141
|
+
};
|
|
120
142
|
this.addItem = async (item) => {
|
|
121
143
|
this.#setState({
|
|
122
144
|
items: [...this.store.state.items, item],
|
|
@@ -130,6 +152,45 @@ var AsyncBatcher = class {
|
|
|
130
152
|
await new Promise((resolve) => setTimeout(resolve, this.#getWait()));
|
|
131
153
|
}
|
|
132
154
|
};
|
|
155
|
+
this.#execute = async () => {
|
|
156
|
+
if (this.store.state.items.length === 0) return;
|
|
157
|
+
const currentExecuteCount = this.store.state.executeCount + 1;
|
|
158
|
+
const batch = this.peekAllItems();
|
|
159
|
+
this.clear();
|
|
160
|
+
this.options.onItemsChange?.(this);
|
|
161
|
+
this.#setState({
|
|
162
|
+
isExecuting: true,
|
|
163
|
+
executeCount: currentExecuteCount
|
|
164
|
+
});
|
|
165
|
+
try {
|
|
166
|
+
const currentAsyncRetryer = new require_async_retryer.AsyncRetryer(this.fn, this.options.asyncRetryerOptions);
|
|
167
|
+
this.asyncRetryers.set(currentExecuteCount, currentAsyncRetryer);
|
|
168
|
+
const result = await currentAsyncRetryer.execute(batch);
|
|
169
|
+
this.#setState({
|
|
170
|
+
totalItemsProcessed: this.store.state.totalItemsProcessed + batch.length,
|
|
171
|
+
lastResult: result,
|
|
172
|
+
successCount: this.store.state.successCount + 1
|
|
173
|
+
});
|
|
174
|
+
this.options.onSuccess?.(result, batch, this);
|
|
175
|
+
return result;
|
|
176
|
+
} catch (error) {
|
|
177
|
+
this.#setState({
|
|
178
|
+
errorCount: this.store.state.errorCount + 1,
|
|
179
|
+
failedItems: [...this.store.state.failedItems, ...batch],
|
|
180
|
+
totalItemsFailed: this.store.state.totalItemsFailed + batch.length
|
|
181
|
+
});
|
|
182
|
+
this.options.onError?.(error, batch, this);
|
|
183
|
+
if (this.options.throwOnError) throw error;
|
|
184
|
+
return;
|
|
185
|
+
} finally {
|
|
186
|
+
this.asyncRetryers.delete(currentExecuteCount);
|
|
187
|
+
this.#setState({
|
|
188
|
+
isExecuting: false,
|
|
189
|
+
settleCount: this.store.state.settleCount + 1
|
|
190
|
+
});
|
|
191
|
+
this.options.onSettled?.(batch, this);
|
|
192
|
+
}
|
|
193
|
+
};
|
|
133
194
|
this.flush = async () => {
|
|
134
195
|
this.#clearTimeout();
|
|
135
196
|
return await this.#execute();
|
|
@@ -140,6 +201,12 @@ var AsyncBatcher = class {
|
|
|
140
201
|
this.peekFailedItems = () => {
|
|
141
202
|
return [...this.store.state.failedItems];
|
|
142
203
|
};
|
|
204
|
+
this.#clearTimeout = () => {
|
|
205
|
+
if (this.#timeoutId) {
|
|
206
|
+
clearTimeout(this.#timeoutId);
|
|
207
|
+
this.#timeoutId = null;
|
|
208
|
+
}
|
|
209
|
+
};
|
|
143
210
|
this.clear = () => {
|
|
144
211
|
this.#setState({
|
|
145
212
|
items: [],
|
|
@@ -178,27 +245,8 @@ var AsyncBatcher = class {
|
|
|
178
245
|
this.setOptions(event.payload.options);
|
|
179
246
|
});
|
|
180
247
|
}
|
|
181
|
-
#setState
|
|
182
|
-
|
|
183
|
-
const combinedState = {
|
|
184
|
-
...state,
|
|
185
|
-
...newState
|
|
186
|
-
};
|
|
187
|
-
const { isExecuting, isPending, items } = combinedState;
|
|
188
|
-
const size = items.length;
|
|
189
|
-
const isEmpty = size === 0;
|
|
190
|
-
return {
|
|
191
|
-
...combinedState,
|
|
192
|
-
isEmpty,
|
|
193
|
-
size,
|
|
194
|
-
status: isExecuting ? "executing" : isPending ? "pending" : isEmpty ? "idle" : "populated"
|
|
195
|
-
};
|
|
196
|
-
});
|
|
197
|
-
require_event_client.emitChange("AsyncBatcher", this);
|
|
198
|
-
};
|
|
199
|
-
#getWait = () => {
|
|
200
|
-
return require_utils.parseFunctionOrValue(this.options.wait, this);
|
|
201
|
-
};
|
|
248
|
+
#setState;
|
|
249
|
+
#getWait;
|
|
202
250
|
/**
|
|
203
251
|
* Processes the current batch of items asynchronously.
|
|
204
252
|
* This method will automatically be triggered if the batcher is running and any of these conditions are met:
|
|
@@ -211,51 +259,8 @@ var AsyncBatcher = class {
|
|
|
211
259
|
* @returns A promise that resolves with the result of the batch function, or undefined if an error occurred and was handled by onError
|
|
212
260
|
* @throws The error from the batch function if no onError handler is configured or throwOnError is true
|
|
213
261
|
*/
|
|
214
|
-
#execute
|
|
215
|
-
|
|
216
|
-
const currentExecuteCount = this.store.state.executeCount + 1;
|
|
217
|
-
const batch = this.peekAllItems();
|
|
218
|
-
this.clear();
|
|
219
|
-
this.options.onItemsChange?.(this);
|
|
220
|
-
this.#setState({
|
|
221
|
-
isExecuting: true,
|
|
222
|
-
executeCount: currentExecuteCount
|
|
223
|
-
});
|
|
224
|
-
try {
|
|
225
|
-
const currentAsyncRetryer = new require_async_retryer.AsyncRetryer(this.fn, this.options.asyncRetryerOptions);
|
|
226
|
-
this.asyncRetryers.set(currentExecuteCount, currentAsyncRetryer);
|
|
227
|
-
const result = await currentAsyncRetryer.execute(batch);
|
|
228
|
-
this.#setState({
|
|
229
|
-
totalItemsProcessed: this.store.state.totalItemsProcessed + batch.length,
|
|
230
|
-
lastResult: result,
|
|
231
|
-
successCount: this.store.state.successCount + 1
|
|
232
|
-
});
|
|
233
|
-
this.options.onSuccess?.(result, batch, this);
|
|
234
|
-
return result;
|
|
235
|
-
} catch (error) {
|
|
236
|
-
this.#setState({
|
|
237
|
-
errorCount: this.store.state.errorCount + 1,
|
|
238
|
-
failedItems: [...this.store.state.failedItems, ...batch],
|
|
239
|
-
totalItemsFailed: this.store.state.totalItemsFailed + batch.length
|
|
240
|
-
});
|
|
241
|
-
this.options.onError?.(error, batch, this);
|
|
242
|
-
if (this.options.throwOnError) throw error;
|
|
243
|
-
return;
|
|
244
|
-
} finally {
|
|
245
|
-
this.asyncRetryers.delete(currentExecuteCount);
|
|
246
|
-
this.#setState({
|
|
247
|
-
isExecuting: false,
|
|
248
|
-
settleCount: this.store.state.settleCount + 1
|
|
249
|
-
});
|
|
250
|
-
this.options.onSettled?.(batch, this);
|
|
251
|
-
}
|
|
252
|
-
};
|
|
253
|
-
#clearTimeout = () => {
|
|
254
|
-
if (this.#timeoutId) {
|
|
255
|
-
clearTimeout(this.#timeoutId);
|
|
256
|
-
this.#timeoutId = null;
|
|
257
|
-
}
|
|
258
|
-
};
|
|
262
|
+
#execute;
|
|
263
|
+
#clearTimeout;
|
|
259
264
|
};
|
|
260
265
|
/**
|
|
261
266
|
* Creates an async batcher that processes items in batches.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"async-batcher.cjs","names":["Store","#setState","#execute","#clearTimeout","#timeoutId","#getWait","parseFunctionOrValue","AsyncRetryer"],"sources":["../src/async-batcher.ts"],"sourcesContent":["import { Store } from '@tanstack/store'\nimport { AsyncRetryer } from './async-retryer'\nimport { parseFunctionOrValue } from './utils'\nimport { emitChange, pacerEventClient } from './event-client'\nimport type { AsyncRetryerOptions } from './async-retryer'\nimport type { OptionalKeys } from './types'\n\nexport interface AsyncBatcherState<TValue> {\n /**\n * Number of batch executions that have resulted in errors\n */\n errorCount: number\n /**\n * Number of batch executions that have been executed\n */\n executeCount: number\n /**\n * Array of items that failed during batch processing\n */\n failedItems: Array<TValue>\n /**\n * Whether the batcher has no items to process (items array is empty)\n */\n isEmpty: boolean\n /**\n * Whether a batch is currently being processed asynchronously\n */\n isExecuting: boolean\n /**\n * Whether the batcher is waiting for the timeout to trigger batch processing\n */\n isPending: boolean\n /**\n * Array of items currently queued for batch processing\n */\n items: Array<TValue>\n /**\n * The result from the most recent batch execution\n */\n lastResult: any\n /**\n * Number of batch executions that have completed (either successfully or with errors)\n */\n settleCount: number\n /**\n * Number of items currently in the batch queue\n */\n size: number\n /**\n * Current processing status - 'idle' when not processing, 'pending' when waiting for timeout, 'executing' when processing, 'populated' when items are present, but no wait is configured\n */\n status: 'idle' | 'pending' | 'executing' | 'populated'\n /**\n * Number of batch executions that have completed successfully\n */\n successCount: number\n /**\n * Total number of items that have failed processing across all batches\n */\n totalItemsFailed: number\n /**\n * Total number of items that have been processed across all batches\n */\n totalItemsProcessed: number\n}\n\nfunction getDefaultAsyncBatcherState<TValue>(): AsyncBatcherState<TValue> {\n return {\n errorCount: 0,\n executeCount: 0,\n failedItems: [],\n isEmpty: true,\n isExecuting: false,\n isPending: false,\n items: [],\n lastResult: undefined,\n settleCount: 0,\n size: 0,\n status: 'idle',\n successCount: 0,\n totalItemsProcessed: 0,\n totalItemsFailed: 0,\n }\n}\n\n/**\n * Options for configuring an AsyncBatcher instance\n */\nexport interface AsyncBatcherOptions<TValue> {\n /**\n * Options for configuring the underlying async retryer\n */\n asyncRetryerOptions?: AsyncRetryerOptions<\n (items: Array<TValue>) => Promise<any>\n >\n /**\n * Custom function to determine if a batch should be processed\n * Return true to process the batch immediately\n */\n getShouldExecute?: (\n items: Array<TValue>,\n batcher: AsyncBatcher<TValue>,\n ) => boolean\n /**\n * Initial state for the async batcher\n */\n initialState?: Partial<AsyncBatcherState<TValue>>\n /**\n * Optional key to identify this async batcher instance.\n * If provided, the async batcher will be identified by this key in the devtools and PacerProvider if applicable.\n */\n key?: string\n /**\n * Maximum number of items in a batch\n * @default Infinity\n */\n maxSize?: number\n /**\n * Optional error handler for when the batch function throws.\n * If provided, the handler will be called with the error, the batch of items that failed, and batcher instance.\n * This can be used alongside throwOnError - the handler will be called before any error is thrown.\n */\n onError?: (\n error: Error,\n batch: Array<TValue>,\n batcher: AsyncBatcher<TValue>,\n ) => void\n /**\n * Callback fired after items are added to the batcher\n */\n onItemsChange?: (batcher: AsyncBatcher<TValue>) => void\n /**\n * Optional callback to call when a batch is settled (completed or failed)\n */\n onSettled?: (batch: Array<TValue>, batcher: AsyncBatcher<TValue>) => void\n /**\n * Optional callback to call when a batch succeeds\n */\n onSuccess?: (\n result: any,\n batch: Array<TValue>,\n batcher: AsyncBatcher<TValue>,\n ) => void\n /**\n * Whether the batcher should start processing immediately\n * @default true\n */\n started?: boolean\n /**\n * Whether to throw errors when they occur.\n * Defaults to true if no onError handler is provided, false if an onError handler is provided.\n * Can be explicitly set to override these defaults.\n */\n throwOnError?: boolean\n /**\n * Maximum time in milliseconds to wait before processing a batch.\n * If the wait duration has elapsed, the batch will be processed.\n * If not provided, the batch will not be triggered by a timeout.\n * @default Infinity\n */\n wait?: number | ((asyncBatcher: AsyncBatcher<TValue>) => number)\n}\n\n/**\n * Utility function for sharing common `AsyncBatcherOptions` options between different `AsyncBatcher` instances.\n *\n */\nexport function asyncBatcherOptions<\n TValue = any,\n TOptions extends Partial<AsyncBatcherOptions<TValue>> = Partial<\n AsyncBatcherOptions<TValue>\n >,\n>(options: TOptions): TOptions {\n return options\n}\n\ntype AsyncBatcherOptionsWithOptionalCallbacks<TValue> = OptionalKeys<\n Required<AsyncBatcherOptions<TValue>>,\n | 'initialState'\n | 'onError'\n | 'onItemsChange'\n | 'onSettled'\n | 'onSuccess'\n | 'key'\n>\n\nconst defaultOptions: AsyncBatcherOptionsWithOptionalCallbacks<any> = {\n asyncRetryerOptions: {\n maxAttempts: 1,\n },\n getShouldExecute: () => false,\n maxSize: Infinity,\n started: true,\n throwOnError: true,\n wait: Infinity,\n}\n\n/**\n * A class that collects items and processes them in batches asynchronously.\n *\n * Async vs Sync Versions:\n * The async version provides advanced features over the sync Batcher:\n * - Returns promises that can be awaited for batch results\n * - Built-in retry support via AsyncRetryer integration\n * - Abort support to cancel in-flight batch executions\n * - Cancel support to prevent pending batches from starting\n * - Comprehensive error handling with onError callbacks and throwOnError control\n * - Detailed execution tracking (success/error/settle counts)\n *\n * The sync Batcher is lighter weight and simpler when you don't need async features,\n * return values, or execution control.\n *\n * What is Batching?\n * Batching is a technique for grouping multiple operations together to be processed as a single unit.\n *\n * The AsyncBatcher provides a flexible way to implement async batching with configurable:\n * - Maximum batch size (number of items per batch)\n * - Time-based batching (process after X milliseconds)\n * - Custom batch processing logic via getShouldExecute\n * - Event callbacks for monitoring batch operations\n * - Error handling for failed batch operations\n *\n * Error Handling:\n * - If an `onError` handler is provided, it will be called with the error, the batch of items that failed, and batcher instance\n * - If `throwOnError` is true (default when no onError handler is provided), the error will be thrown\n * - If `throwOnError` is false (default when onError handler is provided), the error will be swallowed\n * - Both onError and throwOnError can be used together - the handler will be called before any error is thrown\n * - The error state can be checked using the AsyncBatcher instance\n *\n * State Management:\n * - Uses TanStack Store for reactive state management\n * - Use `initialState` to provide initial state values when creating the async batcher\n * - Use `onSuccess` callback to react to successful batch execution and implement custom logic\n * - Use `onError` callback to react to batch execution errors and implement custom error handling\n * - Use `onSettled` callback to react to batch execution completion (success or error) and implement custom logic\n * - Use `onExecute` callback to react to batch execution and implement custom logic\n * - Use `onItemsChange` callback to react to items being added or removed from the batcher\n * - The state includes total items processed, success/error counts, and execution status\n * - State can be accessed via `asyncBatcher.store.state` when using the class directly\n * - When using framework adapters (React/Solid), state is accessed from `asyncBatcher.state`\n *\n * @example\n * ```ts\n * const batcher = new AsyncBatcher<number>(\n * async (items) => {\n * const result = await processItems(items);\n * console.log('Processing batch:', items);\n * return result;\n * },\n * {\n * maxSize: 5,\n * wait: 2000,\n * onSuccess: (result) => console.log('Batch succeeded:', result),\n * onError: (error) => console.error('Batch failed:', error)\n * }\n * );\n *\n * batcher.addItem(1);\n * batcher.addItem(2);\n * // After 2 seconds or when 5 items are added, whichever comes first,\n * // the batch will be processed and the result will be available\n * // batcher.execute() // manually trigger a batch\n * ```\n */\nexport class AsyncBatcher<TValue> {\n readonly store: Store<Readonly<AsyncBatcherState<TValue>>> = new Store(\n getDefaultAsyncBatcherState<TValue>(),\n )\n key: string | undefined\n options: AsyncBatcherOptionsWithOptionalCallbacks<TValue>\n asyncRetryers = new Map<\n number,\n AsyncRetryer<(items: Array<TValue>) => Promise<any>>\n >()\n #timeoutId: ReturnType<typeof setTimeout> | null = null\n\n constructor(\n public fn: (items: Array<TValue>) => Promise<any>,\n initialOptions: AsyncBatcherOptions<TValue>,\n ) {\n this.key = initialOptions.key\n this.options = {\n ...defaultOptions,\n ...initialOptions,\n throwOnError: initialOptions.throwOnError ?? !initialOptions.onError,\n }\n this.#setState(this.options.initialState ?? {})\n\n if (this.key) {\n pacerEventClient.on('d-AsyncBatcher', (event) => {\n if (event.payload.key !== this.key) return\n this.#setState(\n event.payload.store.state as Partial<AsyncBatcherState<TValue>>,\n )\n this.setOptions(\n event.payload.options as Partial<AsyncBatcherOptions<TValue>>,\n )\n })\n }\n }\n\n /**\n * Updates the async batcher options\n */\n setOptions = (newOptions: Partial<AsyncBatcherOptions<TValue>>): void => {\n this.options = { ...this.options, ...newOptions }\n }\n\n #setState = (newState: Partial<AsyncBatcherState<TValue>>): void => {\n this.store.setState((state) => {\n const combinedState = {\n ...state,\n ...newState,\n }\n const { isExecuting, isPending, items } = combinedState\n const size = items.length\n const isEmpty = size === 0\n return {\n ...combinedState,\n isEmpty,\n size,\n status: isExecuting\n ? 'executing'\n : isPending\n ? 'pending'\n : isEmpty\n ? 'idle'\n : 'populated',\n }\n })\n emitChange('AsyncBatcher', this)\n }\n\n #getWait = (): number => {\n return parseFunctionOrValue(this.options.wait, this)\n }\n\n /**\n * Adds an item to the async batcher\n * If the batch size is reached, timeout occurs, or shouldProcess returns true, the batch will be processed\n *\n * @returns The result from the batch function, or undefined if an error occurred and was handled by onError\n *\n * @throws The error from the batch function if no onError handler is configured or throwOnError is true\n */\n addItem = async (item: TValue): Promise<any> => {\n this.#setState({\n items: [...this.store.state.items, item],\n isPending: this.options.wait !== Infinity,\n })\n this.options.onItemsChange?.(this)\n\n const shouldProcess =\n this.store.state.items.length >= this.options.maxSize ||\n this.options.getShouldExecute(this.store.state.items, this)\n\n if (shouldProcess) {\n return await this.#execute()\n } else if (this.options.wait !== Infinity) {\n this.#clearTimeout() // clear any pending timeout to replace it with a new one\n this.#timeoutId = setTimeout(() => this.#execute(), this.#getWait())\n await new Promise((resolve) => setTimeout(resolve, this.#getWait()))\n }\n }\n\n /**\n * Processes the current batch of items asynchronously.\n * This method will automatically be triggered if the batcher is running and any of these conditions are met:\n * - The number of items reaches maxSize\n * - The wait duration has elapsed\n * - The getShouldExecute function returns true upon adding an item\n *\n * You can also call this method manually to process the current batch at any time.\n *\n * @returns A promise that resolves with the result of the batch function, or undefined if an error occurred and was handled by onError\n * @throws The error from the batch function if no onError handler is configured or throwOnError is true\n */\n #execute = async (): Promise<any> => {\n if (this.store.state.items.length === 0) {\n return undefined\n }\n\n const currentExecuteCount = this.store.state.executeCount + 1\n const batch = this.peekAllItems() // copy of the items to be processed (to prevent race conditions)\n this.clear() // Clear items before processing to prevent race conditions\n this.options.onItemsChange?.(this)\n\n this.#setState({ isExecuting: true, executeCount: currentExecuteCount })\n\n try {\n const currentAsyncRetryer = new AsyncRetryer(\n this.fn,\n this.options.asyncRetryerOptions,\n )\n this.asyncRetryers.set(currentExecuteCount, currentAsyncRetryer)\n const result = await currentAsyncRetryer.execute(batch) // EXECUTE\n this.#setState({\n totalItemsProcessed:\n this.store.state.totalItemsProcessed + batch.length,\n lastResult: result,\n successCount: this.store.state.successCount + 1,\n })\n this.options.onSuccess?.(result, batch, this)\n return result\n } catch (error) {\n this.#setState({\n errorCount: this.store.state.errorCount + 1,\n failedItems: [...this.store.state.failedItems, ...batch],\n totalItemsFailed: this.store.state.totalItemsFailed + batch.length,\n })\n this.options.onError?.(error as Error, batch, this)\n if (this.options.throwOnError) {\n throw error\n }\n return undefined\n } finally {\n this.asyncRetryers.delete(currentExecuteCount) // dispose retryer\n this.#setState({\n isExecuting: false,\n settleCount: this.store.state.settleCount + 1,\n })\n this.options.onSettled?.(batch, this)\n }\n }\n\n /**\n * Processes the current batch of items immediately\n */\n flush = async (): Promise<any> => {\n this.#clearTimeout() // clear any pending timeout\n return await this.#execute()\n }\n\n /**\n * Returns a copy of all items in the async batcher\n */\n peekAllItems = (): Array<TValue> => {\n return [...this.store.state.items]\n }\n\n peekFailedItems = (): Array<TValue> => {\n return [...this.store.state.failedItems]\n }\n\n #clearTimeout = (): void => {\n if (this.#timeoutId) {\n clearTimeout(this.#timeoutId)\n this.#timeoutId = null\n }\n }\n\n /**\n * Removes all items from the async batcher\n */\n clear = (): void => {\n this.#setState({ items: [], failedItems: [], isPending: false })\n }\n\n /**\n * Returns the AbortSignal for a specific execution.\n * If no executeCount is provided, returns the signal for the most recent execution.\n * Returns null if no execution is found or not currently executing.\n *\n * @param executeCount - Optional specific execution to get signal for\n * @example\n * ```typescript\n * const batcher = new AsyncBatcher(\n * async (items: string[]) => {\n * const signal = batcher.getAbortSignal()\n * if (signal) {\n * const response = await fetch('/api/batch', {\n * method: 'POST',\n * body: JSON.stringify(items),\n * signal\n * })\n * return response.json()\n * }\n * },\n * { maxSize: 10, wait: 100 }\n * )\n * ```\n */\n getAbortSignal = (executeCount?: number): AbortSignal | null => {\n const count = executeCount ?? this.store.state.executeCount\n const retryer = this.asyncRetryers.get(count)\n return retryer?.getAbortSignal() ?? null\n }\n\n /**\n * Aborts all ongoing executions with the internal abort controllers.\n * Does NOT cancel any pending execution that have not started yet.\n * Does NOT clear out the items.\n */\n abort = (): void => {\n this.asyncRetryers.forEach((retryer) => retryer.abort())\n this.asyncRetryers.clear()\n this.#setState({\n isExecuting: false,\n })\n }\n\n /**\n * Cancels any pending execution that have not started yet.\n * Does NOT abort any execution already in progress.\n * Does NOT clear out the items.\n */\n cancel = (): void => {\n this.#clearTimeout()\n this.#setState({\n isPending: false,\n })\n }\n\n /**\n * Resets the async batcher state to its default values\n */\n reset = (): void => {\n this.#setState(getDefaultAsyncBatcherState<TValue>())\n this.options.onItemsChange?.(this)\n this.asyncRetryers.forEach((retryer) => retryer.reset())\n }\n}\n\n/**\n * Creates an async batcher that processes items in batches.\n *\n * Async vs Sync Versions:\n * The async version provides advanced features over the sync batch function:\n * - Returns promises that can be awaited for batch results\n * - Built-in retry support via AsyncRetryer integration\n * - Abort support to cancel in-flight batch executions\n * - Cancel support to prevent pending batches from starting\n * - Comprehensive error handling with onError callbacks and throwOnError control\n * - Detailed execution tracking (success/error/settle counts)\n *\n * The sync batch function is lighter weight and simpler when you don't need async features,\n * return values, or execution control.\n *\n * What is Batching?\n * Batching is a technique for grouping multiple operations together to be processed as a single unit.\n *\n * Configuration Options:\n * - `maxSize`: Maximum number of items per batch (default: Infinity)\n * - `wait`: Time to wait before processing batch (default: Infinity)\n * - `getShouldExecute`: Custom logic to trigger batch processing\n * - `asyncRetryerOptions`: Configure retry behavior for batch executions\n * - `started`: Whether to start processing immediately (default: true)\n *\n * Error Handling:\n * - If an `onError` handler is provided, it will be called with the error, the batch of items that failed, and batcher instance\n * - If `throwOnError` is true (default when no onError handler is provided), the error will be thrown\n * - If `throwOnError` is false (default when onError handler is provided), the error will be swallowed\n * - Both onError and throwOnError can be used together - the handler will be called before any error is thrown\n * - The error state can be checked using the underlying AsyncBatcher instance\n *\n * State Management:\n * - Uses TanStack Store for reactive state management\n * - Use `initialState` to provide initial state values when creating the async batcher\n * - Use `onSuccess` callback to react to successful batch execution and implement custom logic\n * - Use `onError` callback to react to batch execution errors and implement custom error handling\n * - Use `onSettled` callback to react to batch execution completion (success or error) and implement custom logic\n * - Use `onItemsChange` callback to react to items being added or removed from the batcher\n * - The state includes total items processed, success/error counts, and execution status\n * - State can be accessed via the underlying AsyncBatcher instance's `store.state` property\n * - When using framework adapters (React/Solid), state is accessed from the hook's state property\n *\n * @example\n * ```ts\n * const batchItems = asyncBatch<number>(\n * async (items) => {\n * const result = await processApiCall(items);\n * console.log('Processing:', items);\n * return result;\n * },\n * {\n * maxSize: 3,\n * wait: 1000,\n * onSuccess: (result) => console.log('Batch succeeded:', result),\n * onError: (error) => console.error('Batch failed:', error)\n * }\n * );\n *\n * batchItems(1);\n * batchItems(2);\n * batchItems(3); // Triggers batch processing\n * ```\n */\nexport function asyncBatch<TValue>(\n fn: (items: Array<TValue>) => Promise<any>,\n options: AsyncBatcherOptions<TValue>,\n) {\n const batcher = new AsyncBatcher<TValue>(fn, options)\n return batcher.addItem\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"async-batcher.cjs","names":["Store","parseFunctionOrValue","#setState","#execute","#clearTimeout","#timeoutId","#getWait","AsyncRetryer"],"sources":["../src/async-batcher.ts"],"sourcesContent":["import { Store } from '@tanstack/store'\nimport { AsyncRetryer } from './async-retryer'\nimport { parseFunctionOrValue } from './utils'\nimport { emitChange, pacerEventClient } from './event-client'\nimport type { AsyncRetryerOptions } from './async-retryer'\nimport type { OptionalKeys } from './types'\n\nexport interface AsyncBatcherState<TValue> {\n /**\n * Number of batch executions that have resulted in errors\n */\n errorCount: number\n /**\n * Number of batch executions that have been executed\n */\n executeCount: number\n /**\n * Array of items that failed during batch processing\n */\n failedItems: Array<TValue>\n /**\n * Whether the batcher has no items to process (items array is empty)\n */\n isEmpty: boolean\n /**\n * Whether a batch is currently being processed asynchronously\n */\n isExecuting: boolean\n /**\n * Whether the batcher is waiting for the timeout to trigger batch processing\n */\n isPending: boolean\n /**\n * Array of items currently queued for batch processing\n */\n items: Array<TValue>\n /**\n * The result from the most recent batch execution\n */\n lastResult: any\n /**\n * Number of batch executions that have completed (either successfully or with errors)\n */\n settleCount: number\n /**\n * Number of items currently in the batch queue\n */\n size: number\n /**\n * Current processing status - 'idle' when not processing, 'pending' when waiting for timeout, 'executing' when processing, 'populated' when items are present, but no wait is configured\n */\n status: 'idle' | 'pending' | 'executing' | 'populated'\n /**\n * Number of batch executions that have completed successfully\n */\n successCount: number\n /**\n * Total number of items that have failed processing across all batches\n */\n totalItemsFailed: number\n /**\n * Total number of items that have been processed across all batches\n */\n totalItemsProcessed: number\n}\n\nfunction getDefaultAsyncBatcherState<TValue>(): AsyncBatcherState<TValue> {\n return {\n errorCount: 0,\n executeCount: 0,\n failedItems: [],\n isEmpty: true,\n isExecuting: false,\n isPending: false,\n items: [],\n lastResult: undefined,\n settleCount: 0,\n size: 0,\n status: 'idle',\n successCount: 0,\n totalItemsProcessed: 0,\n totalItemsFailed: 0,\n }\n}\n\n/**\n * Options for configuring an AsyncBatcher instance\n */\nexport interface AsyncBatcherOptions<TValue> {\n /**\n * Options for configuring the underlying async retryer\n */\n asyncRetryerOptions?: AsyncRetryerOptions<\n (items: Array<TValue>) => Promise<any>\n >\n /**\n * Custom function to determine if a batch should be processed\n * Return true to process the batch immediately\n */\n getShouldExecute?: (\n items: Array<TValue>,\n batcher: AsyncBatcher<TValue>,\n ) => boolean\n /**\n * Initial state for the async batcher\n */\n initialState?: Partial<AsyncBatcherState<TValue>>\n /**\n * Optional key to identify this async batcher instance.\n * If provided, the async batcher will be identified by this key in the devtools and PacerProvider if applicable.\n */\n key?: string\n /**\n * Maximum number of items in a batch\n * @default Infinity\n */\n maxSize?: number\n /**\n * Optional error handler for when the batch function throws.\n * If provided, the handler will be called with the error, the batch of items that failed, and batcher instance.\n * This can be used alongside throwOnError - the handler will be called before any error is thrown.\n */\n onError?: (\n error: Error,\n batch: Array<TValue>,\n batcher: AsyncBatcher<TValue>,\n ) => void\n /**\n * Callback fired after items are added to the batcher\n */\n onItemsChange?: (batcher: AsyncBatcher<TValue>) => void\n /**\n * Optional callback to call when a batch is settled (completed or failed)\n */\n onSettled?: (batch: Array<TValue>, batcher: AsyncBatcher<TValue>) => void\n /**\n * Optional callback to call when a batch succeeds\n */\n onSuccess?: (\n result: any,\n batch: Array<TValue>,\n batcher: AsyncBatcher<TValue>,\n ) => void\n /**\n * Whether the batcher should start processing immediately\n * @default true\n */\n started?: boolean\n /**\n * Whether to throw errors when they occur.\n * Defaults to true if no onError handler is provided, false if an onError handler is provided.\n * Can be explicitly set to override these defaults.\n */\n throwOnError?: boolean\n /**\n * Maximum time in milliseconds to wait before processing a batch.\n * If the wait duration has elapsed, the batch will be processed.\n * If not provided, the batch will not be triggered by a timeout.\n * @default Infinity\n */\n wait?: number | ((asyncBatcher: AsyncBatcher<TValue>) => number)\n}\n\n/**\n * Utility function for sharing common `AsyncBatcherOptions` options between different `AsyncBatcher` instances.\n *\n */\nexport function asyncBatcherOptions<\n TValue = any,\n TOptions extends Partial<AsyncBatcherOptions<TValue>> = Partial<\n AsyncBatcherOptions<TValue>\n >,\n>(options: TOptions): TOptions {\n return options\n}\n\ntype AsyncBatcherOptionsWithOptionalCallbacks<TValue> = OptionalKeys<\n Required<AsyncBatcherOptions<TValue>>,\n | 'initialState'\n | 'onError'\n | 'onItemsChange'\n | 'onSettled'\n | 'onSuccess'\n | 'key'\n>\n\nconst defaultOptions: AsyncBatcherOptionsWithOptionalCallbacks<any> = {\n asyncRetryerOptions: {\n maxAttempts: 1,\n },\n getShouldExecute: () => false,\n maxSize: Infinity,\n started: true,\n throwOnError: true,\n wait: Infinity,\n}\n\n/**\n * A class that collects items and processes them in batches asynchronously.\n *\n * Async vs Sync Versions:\n * The async version provides advanced features over the sync Batcher:\n * - Returns promises that can be awaited for batch results\n * - Built-in retry support via AsyncRetryer integration\n * - Abort support to cancel in-flight batch executions\n * - Cancel support to prevent pending batches from starting\n * - Comprehensive error handling with onError callbacks and throwOnError control\n * - Detailed execution tracking (success/error/settle counts)\n *\n * The sync Batcher is lighter weight and simpler when you don't need async features,\n * return values, or execution control.\n *\n * What is Batching?\n * Batching is a technique for grouping multiple operations together to be processed as a single unit.\n *\n * The AsyncBatcher provides a flexible way to implement async batching with configurable:\n * - Maximum batch size (number of items per batch)\n * - Time-based batching (process after X milliseconds)\n * - Custom batch processing logic via getShouldExecute\n * - Event callbacks for monitoring batch operations\n * - Error handling for failed batch operations\n *\n * Error Handling:\n * - If an `onError` handler is provided, it will be called with the error, the batch of items that failed, and batcher instance\n * - If `throwOnError` is true (default when no onError handler is provided), the error will be thrown\n * - If `throwOnError` is false (default when onError handler is provided), the error will be swallowed\n * - Both onError and throwOnError can be used together - the handler will be called before any error is thrown\n * - The error state can be checked using the AsyncBatcher instance\n *\n * State Management:\n * - Uses TanStack Store for reactive state management\n * - Use `initialState` to provide initial state values when creating the async batcher\n * - Use `onSuccess` callback to react to successful batch execution and implement custom logic\n * - Use `onError` callback to react to batch execution errors and implement custom error handling\n * - Use `onSettled` callback to react to batch execution completion (success or error) and implement custom logic\n * - Use `onExecute` callback to react to batch execution and implement custom logic\n * - Use `onItemsChange` callback to react to items being added or removed from the batcher\n * - The state includes total items processed, success/error counts, and execution status\n * - State can be accessed via `asyncBatcher.store.state` when using the class directly\n * - When using framework adapters (React/Solid), state is accessed from `asyncBatcher.state`\n *\n * @example\n * ```ts\n * const batcher = new AsyncBatcher<number>(\n * async (items) => {\n * const result = await processItems(items);\n * console.log('Processing batch:', items);\n * return result;\n * },\n * {\n * maxSize: 5,\n * wait: 2000,\n * onSuccess: (result) => console.log('Batch succeeded:', result),\n * onError: (error) => console.error('Batch failed:', error)\n * }\n * );\n *\n * batcher.addItem(1);\n * batcher.addItem(2);\n * // After 2 seconds or when 5 items are added, whichever comes first,\n * // the batch will be processed and the result will be available\n * // batcher.execute() // manually trigger a batch\n * ```\n */\nexport class AsyncBatcher<TValue> {\n readonly store: Store<Readonly<AsyncBatcherState<TValue>>> = new Store(\n getDefaultAsyncBatcherState<TValue>(),\n )\n key: string | undefined\n options: AsyncBatcherOptionsWithOptionalCallbacks<TValue>\n asyncRetryers = new Map<\n number,\n AsyncRetryer<(items: Array<TValue>) => Promise<any>>\n >()\n #timeoutId: ReturnType<typeof setTimeout> | null = null\n\n constructor(\n public fn: (items: Array<TValue>) => Promise<any>,\n initialOptions: AsyncBatcherOptions<TValue>,\n ) {\n this.key = initialOptions.key\n this.options = {\n ...defaultOptions,\n ...initialOptions,\n throwOnError: initialOptions.throwOnError ?? !initialOptions.onError,\n }\n this.#setState(this.options.initialState ?? {})\n\n if (this.key) {\n pacerEventClient.on('d-AsyncBatcher', (event) => {\n if (event.payload.key !== this.key) return\n this.#setState(\n event.payload.store.state as Partial<AsyncBatcherState<TValue>>,\n )\n this.setOptions(\n event.payload.options as Partial<AsyncBatcherOptions<TValue>>,\n )\n })\n }\n }\n\n /**\n * Updates the async batcher options\n */\n setOptions = (newOptions: Partial<AsyncBatcherOptions<TValue>>): void => {\n this.options = { ...this.options, ...newOptions }\n }\n\n #setState = (newState: Partial<AsyncBatcherState<TValue>>): void => {\n this.store.setState((state) => {\n const combinedState = {\n ...state,\n ...newState,\n }\n const { isExecuting, isPending, items } = combinedState\n const size = items.length\n const isEmpty = size === 0\n return {\n ...combinedState,\n isEmpty,\n size,\n status: isExecuting\n ? 'executing'\n : isPending\n ? 'pending'\n : isEmpty\n ? 'idle'\n : 'populated',\n }\n })\n emitChange('AsyncBatcher', this)\n }\n\n #getWait = (): number => {\n return parseFunctionOrValue(this.options.wait, this)\n }\n\n /**\n * Adds an item to the async batcher\n * If the batch size is reached, timeout occurs, or shouldProcess returns true, the batch will be processed\n *\n * @returns The result from the batch function, or undefined if an error occurred and was handled by onError\n *\n * @throws The error from the batch function if no onError handler is configured or throwOnError is true\n */\n addItem = async (item: TValue): Promise<any> => {\n this.#setState({\n items: [...this.store.state.items, item],\n isPending: this.options.wait !== Infinity,\n })\n this.options.onItemsChange?.(this)\n\n const shouldProcess =\n this.store.state.items.length >= this.options.maxSize ||\n this.options.getShouldExecute(this.store.state.items, this)\n\n if (shouldProcess) {\n return await this.#execute()\n } else if (this.options.wait !== Infinity) {\n this.#clearTimeout() // clear any pending timeout to replace it with a new one\n this.#timeoutId = setTimeout(() => this.#execute(), this.#getWait())\n await new Promise((resolve) => setTimeout(resolve, this.#getWait()))\n }\n }\n\n /**\n * Processes the current batch of items asynchronously.\n * This method will automatically be triggered if the batcher is running and any of these conditions are met:\n * - The number of items reaches maxSize\n * - The wait duration has elapsed\n * - The getShouldExecute function returns true upon adding an item\n *\n * You can also call this method manually to process the current batch at any time.\n *\n * @returns A promise that resolves with the result of the batch function, or undefined if an error occurred and was handled by onError\n * @throws The error from the batch function if no onError handler is configured or throwOnError is true\n */\n #execute = async (): Promise<any> => {\n if (this.store.state.items.length === 0) {\n return undefined\n }\n\n const currentExecuteCount = this.store.state.executeCount + 1\n const batch = this.peekAllItems() // copy of the items to be processed (to prevent race conditions)\n this.clear() // Clear items before processing to prevent race conditions\n this.options.onItemsChange?.(this)\n\n this.#setState({ isExecuting: true, executeCount: currentExecuteCount })\n\n try {\n const currentAsyncRetryer = new AsyncRetryer(\n this.fn,\n this.options.asyncRetryerOptions,\n )\n this.asyncRetryers.set(currentExecuteCount, currentAsyncRetryer)\n const result = await currentAsyncRetryer.execute(batch) // EXECUTE\n this.#setState({\n totalItemsProcessed:\n this.store.state.totalItemsProcessed + batch.length,\n lastResult: result,\n successCount: this.store.state.successCount + 1,\n })\n this.options.onSuccess?.(result, batch, this)\n return result\n } catch (error) {\n this.#setState({\n errorCount: this.store.state.errorCount + 1,\n failedItems: [...this.store.state.failedItems, ...batch],\n totalItemsFailed: this.store.state.totalItemsFailed + batch.length,\n })\n this.options.onError?.(error as Error, batch, this)\n if (this.options.throwOnError) {\n throw error\n }\n return undefined\n } finally {\n this.asyncRetryers.delete(currentExecuteCount) // dispose retryer\n this.#setState({\n isExecuting: false,\n settleCount: this.store.state.settleCount + 1,\n })\n this.options.onSettled?.(batch, this)\n }\n }\n\n /**\n * Processes the current batch of items immediately\n */\n flush = async (): Promise<any> => {\n this.#clearTimeout() // clear any pending timeout\n return await this.#execute()\n }\n\n /**\n * Returns a copy of all items in the async batcher\n */\n peekAllItems = (): Array<TValue> => {\n return [...this.store.state.items]\n }\n\n peekFailedItems = (): Array<TValue> => {\n return [...this.store.state.failedItems]\n }\n\n #clearTimeout = (): void => {\n if (this.#timeoutId) {\n clearTimeout(this.#timeoutId)\n this.#timeoutId = null\n }\n }\n\n /**\n * Removes all items from the async batcher\n */\n clear = (): void => {\n this.#setState({ items: [], failedItems: [], isPending: false })\n }\n\n /**\n * Returns the AbortSignal for a specific execution.\n * If no executeCount is provided, returns the signal for the most recent execution.\n * Returns null if no execution is found or not currently executing.\n *\n * @param executeCount - Optional specific execution to get signal for\n * @example\n * ```typescript\n * const batcher = new AsyncBatcher(\n * async (items: string[]) => {\n * const signal = batcher.getAbortSignal()\n * if (signal) {\n * const response = await fetch('/api/batch', {\n * method: 'POST',\n * body: JSON.stringify(items),\n * signal\n * })\n * return response.json()\n * }\n * },\n * { maxSize: 10, wait: 100 }\n * )\n * ```\n */\n getAbortSignal = (executeCount?: number): AbortSignal | null => {\n const count = executeCount ?? this.store.state.executeCount\n const retryer = this.asyncRetryers.get(count)\n return retryer?.getAbortSignal() ?? null\n }\n\n /**\n * Aborts all ongoing executions with the internal abort controllers.\n * Does NOT cancel any pending execution that have not started yet.\n * Does NOT clear out the items.\n */\n abort = (): void => {\n this.asyncRetryers.forEach((retryer) => retryer.abort())\n this.asyncRetryers.clear()\n this.#setState({\n isExecuting: false,\n })\n }\n\n /**\n * Cancels any pending execution that have not started yet.\n * Does NOT abort any execution already in progress.\n * Does NOT clear out the items.\n */\n cancel = (): void => {\n this.#clearTimeout()\n this.#setState({\n isPending: false,\n })\n }\n\n /**\n * Resets the async batcher state to its default values\n */\n reset = (): void => {\n this.#setState(getDefaultAsyncBatcherState<TValue>())\n this.options.onItemsChange?.(this)\n this.asyncRetryers.forEach((retryer) => retryer.reset())\n }\n}\n\n/**\n * Creates an async batcher that processes items in batches.\n *\n * Async vs Sync Versions:\n * The async version provides advanced features over the sync batch function:\n * - Returns promises that can be awaited for batch results\n * - Built-in retry support via AsyncRetryer integration\n * - Abort support to cancel in-flight batch executions\n * - Cancel support to prevent pending batches from starting\n * - Comprehensive error handling with onError callbacks and throwOnError control\n * - Detailed execution tracking (success/error/settle counts)\n *\n * The sync batch function is lighter weight and simpler when you don't need async features,\n * return values, or execution control.\n *\n * What is Batching?\n * Batching is a technique for grouping multiple operations together to be processed as a single unit.\n *\n * Configuration Options:\n * - `maxSize`: Maximum number of items per batch (default: Infinity)\n * - `wait`: Time to wait before processing batch (default: Infinity)\n * - `getShouldExecute`: Custom logic to trigger batch processing\n * - `asyncRetryerOptions`: Configure retry behavior for batch executions\n * - `started`: Whether to start processing immediately (default: true)\n *\n * Error Handling:\n * - If an `onError` handler is provided, it will be called with the error, the batch of items that failed, and batcher instance\n * - If `throwOnError` is true (default when no onError handler is provided), the error will be thrown\n * - If `throwOnError` is false (default when onError handler is provided), the error will be swallowed\n * - Both onError and throwOnError can be used together - the handler will be called before any error is thrown\n * - The error state can be checked using the underlying AsyncBatcher instance\n *\n * State Management:\n * - Uses TanStack Store for reactive state management\n * - Use `initialState` to provide initial state values when creating the async batcher\n * - Use `onSuccess` callback to react to successful batch execution and implement custom logic\n * - Use `onError` callback to react to batch execution errors and implement custom error handling\n * - Use `onSettled` callback to react to batch execution completion (success or error) and implement custom logic\n * - Use `onItemsChange` callback to react to items being added or removed from the batcher\n * - The state includes total items processed, success/error counts, and execution status\n * - State can be accessed via the underlying AsyncBatcher instance's `store.state` property\n * - When using framework adapters (React/Solid), state is accessed from the hook's state property\n *\n * @example\n * ```ts\n * const batchItems = asyncBatch<number>(\n * async (items) => {\n * const result = await processApiCall(items);\n * console.log('Processing:', items);\n * return result;\n * },\n * {\n * maxSize: 3,\n * wait: 1000,\n * onSuccess: (result) => console.log('Batch succeeded:', result),\n * onError: (error) => console.error('Batch failed:', error)\n * }\n * );\n *\n * batchItems(1);\n * batchItems(2);\n * batchItems(3); // Triggers batch processing\n * ```\n */\nexport function asyncBatch<TValue>(\n fn: (items: Array<TValue>) => Promise<any>,\n options: AsyncBatcherOptions<TValue>,\n) {\n const batcher = new AsyncBatcher<TValue>(fn, options)\n return batcher.addItem\n}\n"],"mappings":"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"}
|
package/dist/async-batcher.js
CHANGED
|
@@ -105,17 +105,39 @@ const defaultOptions = {
|
|
|
105
105
|
* ```
|
|
106
106
|
*/
|
|
107
107
|
var AsyncBatcher = class {
|
|
108
|
-
#timeoutId
|
|
108
|
+
#timeoutId;
|
|
109
109
|
constructor(fn, initialOptions) {
|
|
110
110
|
this.fn = fn;
|
|
111
111
|
this.store = new Store(getDefaultAsyncBatcherState());
|
|
112
112
|
this.asyncRetryers = /* @__PURE__ */ new Map();
|
|
113
|
+
this.#timeoutId = null;
|
|
113
114
|
this.setOptions = (newOptions) => {
|
|
114
115
|
this.options = {
|
|
115
116
|
...this.options,
|
|
116
117
|
...newOptions
|
|
117
118
|
};
|
|
118
119
|
};
|
|
120
|
+
this.#setState = (newState) => {
|
|
121
|
+
this.store.setState((state) => {
|
|
122
|
+
const combinedState = {
|
|
123
|
+
...state,
|
|
124
|
+
...newState
|
|
125
|
+
};
|
|
126
|
+
const { isExecuting, isPending, items } = combinedState;
|
|
127
|
+
const size = items.length;
|
|
128
|
+
const isEmpty = size === 0;
|
|
129
|
+
return {
|
|
130
|
+
...combinedState,
|
|
131
|
+
isEmpty,
|
|
132
|
+
size,
|
|
133
|
+
status: isExecuting ? "executing" : isPending ? "pending" : isEmpty ? "idle" : "populated"
|
|
134
|
+
};
|
|
135
|
+
});
|
|
136
|
+
emitChange("AsyncBatcher", this);
|
|
137
|
+
};
|
|
138
|
+
this.#getWait = () => {
|
|
139
|
+
return parseFunctionOrValue(this.options.wait, this);
|
|
140
|
+
};
|
|
119
141
|
this.addItem = async (item) => {
|
|
120
142
|
this.#setState({
|
|
121
143
|
items: [...this.store.state.items, item],
|
|
@@ -129,6 +151,45 @@ var AsyncBatcher = class {
|
|
|
129
151
|
await new Promise((resolve) => setTimeout(resolve, this.#getWait()));
|
|
130
152
|
}
|
|
131
153
|
};
|
|
154
|
+
this.#execute = async () => {
|
|
155
|
+
if (this.store.state.items.length === 0) return;
|
|
156
|
+
const currentExecuteCount = this.store.state.executeCount + 1;
|
|
157
|
+
const batch = this.peekAllItems();
|
|
158
|
+
this.clear();
|
|
159
|
+
this.options.onItemsChange?.(this);
|
|
160
|
+
this.#setState({
|
|
161
|
+
isExecuting: true,
|
|
162
|
+
executeCount: currentExecuteCount
|
|
163
|
+
});
|
|
164
|
+
try {
|
|
165
|
+
const currentAsyncRetryer = new AsyncRetryer(this.fn, this.options.asyncRetryerOptions);
|
|
166
|
+
this.asyncRetryers.set(currentExecuteCount, currentAsyncRetryer);
|
|
167
|
+
const result = await currentAsyncRetryer.execute(batch);
|
|
168
|
+
this.#setState({
|
|
169
|
+
totalItemsProcessed: this.store.state.totalItemsProcessed + batch.length,
|
|
170
|
+
lastResult: result,
|
|
171
|
+
successCount: this.store.state.successCount + 1
|
|
172
|
+
});
|
|
173
|
+
this.options.onSuccess?.(result, batch, this);
|
|
174
|
+
return result;
|
|
175
|
+
} catch (error) {
|
|
176
|
+
this.#setState({
|
|
177
|
+
errorCount: this.store.state.errorCount + 1,
|
|
178
|
+
failedItems: [...this.store.state.failedItems, ...batch],
|
|
179
|
+
totalItemsFailed: this.store.state.totalItemsFailed + batch.length
|
|
180
|
+
});
|
|
181
|
+
this.options.onError?.(error, batch, this);
|
|
182
|
+
if (this.options.throwOnError) throw error;
|
|
183
|
+
return;
|
|
184
|
+
} finally {
|
|
185
|
+
this.asyncRetryers.delete(currentExecuteCount);
|
|
186
|
+
this.#setState({
|
|
187
|
+
isExecuting: false,
|
|
188
|
+
settleCount: this.store.state.settleCount + 1
|
|
189
|
+
});
|
|
190
|
+
this.options.onSettled?.(batch, this);
|
|
191
|
+
}
|
|
192
|
+
};
|
|
132
193
|
this.flush = async () => {
|
|
133
194
|
this.#clearTimeout();
|
|
134
195
|
return await this.#execute();
|
|
@@ -139,6 +200,12 @@ var AsyncBatcher = class {
|
|
|
139
200
|
this.peekFailedItems = () => {
|
|
140
201
|
return [...this.store.state.failedItems];
|
|
141
202
|
};
|
|
203
|
+
this.#clearTimeout = () => {
|
|
204
|
+
if (this.#timeoutId) {
|
|
205
|
+
clearTimeout(this.#timeoutId);
|
|
206
|
+
this.#timeoutId = null;
|
|
207
|
+
}
|
|
208
|
+
};
|
|
142
209
|
this.clear = () => {
|
|
143
210
|
this.#setState({
|
|
144
211
|
items: [],
|
|
@@ -177,27 +244,8 @@ var AsyncBatcher = class {
|
|
|
177
244
|
this.setOptions(event.payload.options);
|
|
178
245
|
});
|
|
179
246
|
}
|
|
180
|
-
#setState
|
|
181
|
-
|
|
182
|
-
const combinedState = {
|
|
183
|
-
...state,
|
|
184
|
-
...newState
|
|
185
|
-
};
|
|
186
|
-
const { isExecuting, isPending, items } = combinedState;
|
|
187
|
-
const size = items.length;
|
|
188
|
-
const isEmpty = size === 0;
|
|
189
|
-
return {
|
|
190
|
-
...combinedState,
|
|
191
|
-
isEmpty,
|
|
192
|
-
size,
|
|
193
|
-
status: isExecuting ? "executing" : isPending ? "pending" : isEmpty ? "idle" : "populated"
|
|
194
|
-
};
|
|
195
|
-
});
|
|
196
|
-
emitChange("AsyncBatcher", this);
|
|
197
|
-
};
|
|
198
|
-
#getWait = () => {
|
|
199
|
-
return parseFunctionOrValue(this.options.wait, this);
|
|
200
|
-
};
|
|
247
|
+
#setState;
|
|
248
|
+
#getWait;
|
|
201
249
|
/**
|
|
202
250
|
* Processes the current batch of items asynchronously.
|
|
203
251
|
* This method will automatically be triggered if the batcher is running and any of these conditions are met:
|
|
@@ -210,51 +258,8 @@ var AsyncBatcher = class {
|
|
|
210
258
|
* @returns A promise that resolves with the result of the batch function, or undefined if an error occurred and was handled by onError
|
|
211
259
|
* @throws The error from the batch function if no onError handler is configured or throwOnError is true
|
|
212
260
|
*/
|
|
213
|
-
#execute
|
|
214
|
-
|
|
215
|
-
const currentExecuteCount = this.store.state.executeCount + 1;
|
|
216
|
-
const batch = this.peekAllItems();
|
|
217
|
-
this.clear();
|
|
218
|
-
this.options.onItemsChange?.(this);
|
|
219
|
-
this.#setState({
|
|
220
|
-
isExecuting: true,
|
|
221
|
-
executeCount: currentExecuteCount
|
|
222
|
-
});
|
|
223
|
-
try {
|
|
224
|
-
const currentAsyncRetryer = new AsyncRetryer(this.fn, this.options.asyncRetryerOptions);
|
|
225
|
-
this.asyncRetryers.set(currentExecuteCount, currentAsyncRetryer);
|
|
226
|
-
const result = await currentAsyncRetryer.execute(batch);
|
|
227
|
-
this.#setState({
|
|
228
|
-
totalItemsProcessed: this.store.state.totalItemsProcessed + batch.length,
|
|
229
|
-
lastResult: result,
|
|
230
|
-
successCount: this.store.state.successCount + 1
|
|
231
|
-
});
|
|
232
|
-
this.options.onSuccess?.(result, batch, this);
|
|
233
|
-
return result;
|
|
234
|
-
} catch (error) {
|
|
235
|
-
this.#setState({
|
|
236
|
-
errorCount: this.store.state.errorCount + 1,
|
|
237
|
-
failedItems: [...this.store.state.failedItems, ...batch],
|
|
238
|
-
totalItemsFailed: this.store.state.totalItemsFailed + batch.length
|
|
239
|
-
});
|
|
240
|
-
this.options.onError?.(error, batch, this);
|
|
241
|
-
if (this.options.throwOnError) throw error;
|
|
242
|
-
return;
|
|
243
|
-
} finally {
|
|
244
|
-
this.asyncRetryers.delete(currentExecuteCount);
|
|
245
|
-
this.#setState({
|
|
246
|
-
isExecuting: false,
|
|
247
|
-
settleCount: this.store.state.settleCount + 1
|
|
248
|
-
});
|
|
249
|
-
this.options.onSettled?.(batch, this);
|
|
250
|
-
}
|
|
251
|
-
};
|
|
252
|
-
#clearTimeout = () => {
|
|
253
|
-
if (this.#timeoutId) {
|
|
254
|
-
clearTimeout(this.#timeoutId);
|
|
255
|
-
this.#timeoutId = null;
|
|
256
|
-
}
|
|
257
|
-
};
|
|
261
|
+
#execute;
|
|
262
|
+
#clearTimeout;
|
|
258
263
|
};
|
|
259
264
|
/**
|
|
260
265
|
* Creates an async batcher that processes items in batches.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"async-batcher.js","names":["#setState","#execute","#clearTimeout","#timeoutId","#getWait"],"sources":["../src/async-batcher.ts"],"sourcesContent":["import { Store } from '@tanstack/store'\nimport { AsyncRetryer } from './async-retryer'\nimport { parseFunctionOrValue } from './utils'\nimport { emitChange, pacerEventClient } from './event-client'\nimport type { AsyncRetryerOptions } from './async-retryer'\nimport type { OptionalKeys } from './types'\n\nexport interface AsyncBatcherState<TValue> {\n /**\n * Number of batch executions that have resulted in errors\n */\n errorCount: number\n /**\n * Number of batch executions that have been executed\n */\n executeCount: number\n /**\n * Array of items that failed during batch processing\n */\n failedItems: Array<TValue>\n /**\n * Whether the batcher has no items to process (items array is empty)\n */\n isEmpty: boolean\n /**\n * Whether a batch is currently being processed asynchronously\n */\n isExecuting: boolean\n /**\n * Whether the batcher is waiting for the timeout to trigger batch processing\n */\n isPending: boolean\n /**\n * Array of items currently queued for batch processing\n */\n items: Array<TValue>\n /**\n * The result from the most recent batch execution\n */\n lastResult: any\n /**\n * Number of batch executions that have completed (either successfully or with errors)\n */\n settleCount: number\n /**\n * Number of items currently in the batch queue\n */\n size: number\n /**\n * Current processing status - 'idle' when not processing, 'pending' when waiting for timeout, 'executing' when processing, 'populated' when items are present, but no wait is configured\n */\n status: 'idle' | 'pending' | 'executing' | 'populated'\n /**\n * Number of batch executions that have completed successfully\n */\n successCount: number\n /**\n * Total number of items that have failed processing across all batches\n */\n totalItemsFailed: number\n /**\n * Total number of items that have been processed across all batches\n */\n totalItemsProcessed: number\n}\n\nfunction getDefaultAsyncBatcherState<TValue>(): AsyncBatcherState<TValue> {\n return {\n errorCount: 0,\n executeCount: 0,\n failedItems: [],\n isEmpty: true,\n isExecuting: false,\n isPending: false,\n items: [],\n lastResult: undefined,\n settleCount: 0,\n size: 0,\n status: 'idle',\n successCount: 0,\n totalItemsProcessed: 0,\n totalItemsFailed: 0,\n }\n}\n\n/**\n * Options for configuring an AsyncBatcher instance\n */\nexport interface AsyncBatcherOptions<TValue> {\n /**\n * Options for configuring the underlying async retryer\n */\n asyncRetryerOptions?: AsyncRetryerOptions<\n (items: Array<TValue>) => Promise<any>\n >\n /**\n * Custom function to determine if a batch should be processed\n * Return true to process the batch immediately\n */\n getShouldExecute?: (\n items: Array<TValue>,\n batcher: AsyncBatcher<TValue>,\n ) => boolean\n /**\n * Initial state for the async batcher\n */\n initialState?: Partial<AsyncBatcherState<TValue>>\n /**\n * Optional key to identify this async batcher instance.\n * If provided, the async batcher will be identified by this key in the devtools and PacerProvider if applicable.\n */\n key?: string\n /**\n * Maximum number of items in a batch\n * @default Infinity\n */\n maxSize?: number\n /**\n * Optional error handler for when the batch function throws.\n * If provided, the handler will be called with the error, the batch of items that failed, and batcher instance.\n * This can be used alongside throwOnError - the handler will be called before any error is thrown.\n */\n onError?: (\n error: Error,\n batch: Array<TValue>,\n batcher: AsyncBatcher<TValue>,\n ) => void\n /**\n * Callback fired after items are added to the batcher\n */\n onItemsChange?: (batcher: AsyncBatcher<TValue>) => void\n /**\n * Optional callback to call when a batch is settled (completed or failed)\n */\n onSettled?: (batch: Array<TValue>, batcher: AsyncBatcher<TValue>) => void\n /**\n * Optional callback to call when a batch succeeds\n */\n onSuccess?: (\n result: any,\n batch: Array<TValue>,\n batcher: AsyncBatcher<TValue>,\n ) => void\n /**\n * Whether the batcher should start processing immediately\n * @default true\n */\n started?: boolean\n /**\n * Whether to throw errors when they occur.\n * Defaults to true if no onError handler is provided, false if an onError handler is provided.\n * Can be explicitly set to override these defaults.\n */\n throwOnError?: boolean\n /**\n * Maximum time in milliseconds to wait before processing a batch.\n * If the wait duration has elapsed, the batch will be processed.\n * If not provided, the batch will not be triggered by a timeout.\n * @default Infinity\n */\n wait?: number | ((asyncBatcher: AsyncBatcher<TValue>) => number)\n}\n\n/**\n * Utility function for sharing common `AsyncBatcherOptions` options between different `AsyncBatcher` instances.\n *\n */\nexport function asyncBatcherOptions<\n TValue = any,\n TOptions extends Partial<AsyncBatcherOptions<TValue>> = Partial<\n AsyncBatcherOptions<TValue>\n >,\n>(options: TOptions): TOptions {\n return options\n}\n\ntype AsyncBatcherOptionsWithOptionalCallbacks<TValue> = OptionalKeys<\n Required<AsyncBatcherOptions<TValue>>,\n | 'initialState'\n | 'onError'\n | 'onItemsChange'\n | 'onSettled'\n | 'onSuccess'\n | 'key'\n>\n\nconst defaultOptions: AsyncBatcherOptionsWithOptionalCallbacks<any> = {\n asyncRetryerOptions: {\n maxAttempts: 1,\n },\n getShouldExecute: () => false,\n maxSize: Infinity,\n started: true,\n throwOnError: true,\n wait: Infinity,\n}\n\n/**\n * A class that collects items and processes them in batches asynchronously.\n *\n * Async vs Sync Versions:\n * The async version provides advanced features over the sync Batcher:\n * - Returns promises that can be awaited for batch results\n * - Built-in retry support via AsyncRetryer integration\n * - Abort support to cancel in-flight batch executions\n * - Cancel support to prevent pending batches from starting\n * - Comprehensive error handling with onError callbacks and throwOnError control\n * - Detailed execution tracking (success/error/settle counts)\n *\n * The sync Batcher is lighter weight and simpler when you don't need async features,\n * return values, or execution control.\n *\n * What is Batching?\n * Batching is a technique for grouping multiple operations together to be processed as a single unit.\n *\n * The AsyncBatcher provides a flexible way to implement async batching with configurable:\n * - Maximum batch size (number of items per batch)\n * - Time-based batching (process after X milliseconds)\n * - Custom batch processing logic via getShouldExecute\n * - Event callbacks for monitoring batch operations\n * - Error handling for failed batch operations\n *\n * Error Handling:\n * - If an `onError` handler is provided, it will be called with the error, the batch of items that failed, and batcher instance\n * - If `throwOnError` is true (default when no onError handler is provided), the error will be thrown\n * - If `throwOnError` is false (default when onError handler is provided), the error will be swallowed\n * - Both onError and throwOnError can be used together - the handler will be called before any error is thrown\n * - The error state can be checked using the AsyncBatcher instance\n *\n * State Management:\n * - Uses TanStack Store for reactive state management\n * - Use `initialState` to provide initial state values when creating the async batcher\n * - Use `onSuccess` callback to react to successful batch execution and implement custom logic\n * - Use `onError` callback to react to batch execution errors and implement custom error handling\n * - Use `onSettled` callback to react to batch execution completion (success or error) and implement custom logic\n * - Use `onExecute` callback to react to batch execution and implement custom logic\n * - Use `onItemsChange` callback to react to items being added or removed from the batcher\n * - The state includes total items processed, success/error counts, and execution status\n * - State can be accessed via `asyncBatcher.store.state` when using the class directly\n * - When using framework adapters (React/Solid), state is accessed from `asyncBatcher.state`\n *\n * @example\n * ```ts\n * const batcher = new AsyncBatcher<number>(\n * async (items) => {\n * const result = await processItems(items);\n * console.log('Processing batch:', items);\n * return result;\n * },\n * {\n * maxSize: 5,\n * wait: 2000,\n * onSuccess: (result) => console.log('Batch succeeded:', result),\n * onError: (error) => console.error('Batch failed:', error)\n * }\n * );\n *\n * batcher.addItem(1);\n * batcher.addItem(2);\n * // After 2 seconds or when 5 items are added, whichever comes first,\n * // the batch will be processed and the result will be available\n * // batcher.execute() // manually trigger a batch\n * ```\n */\nexport class AsyncBatcher<TValue> {\n readonly store: Store<Readonly<AsyncBatcherState<TValue>>> = new Store(\n getDefaultAsyncBatcherState<TValue>(),\n )\n key: string | undefined\n options: AsyncBatcherOptionsWithOptionalCallbacks<TValue>\n asyncRetryers = new Map<\n number,\n AsyncRetryer<(items: Array<TValue>) => Promise<any>>\n >()\n #timeoutId: ReturnType<typeof setTimeout> | null = null\n\n constructor(\n public fn: (items: Array<TValue>) => Promise<any>,\n initialOptions: AsyncBatcherOptions<TValue>,\n ) {\n this.key = initialOptions.key\n this.options = {\n ...defaultOptions,\n ...initialOptions,\n throwOnError: initialOptions.throwOnError ?? !initialOptions.onError,\n }\n this.#setState(this.options.initialState ?? {})\n\n if (this.key) {\n pacerEventClient.on('d-AsyncBatcher', (event) => {\n if (event.payload.key !== this.key) return\n this.#setState(\n event.payload.store.state as Partial<AsyncBatcherState<TValue>>,\n )\n this.setOptions(\n event.payload.options as Partial<AsyncBatcherOptions<TValue>>,\n )\n })\n }\n }\n\n /**\n * Updates the async batcher options\n */\n setOptions = (newOptions: Partial<AsyncBatcherOptions<TValue>>): void => {\n this.options = { ...this.options, ...newOptions }\n }\n\n #setState = (newState: Partial<AsyncBatcherState<TValue>>): void => {\n this.store.setState((state) => {\n const combinedState = {\n ...state,\n ...newState,\n }\n const { isExecuting, isPending, items } = combinedState\n const size = items.length\n const isEmpty = size === 0\n return {\n ...combinedState,\n isEmpty,\n size,\n status: isExecuting\n ? 'executing'\n : isPending\n ? 'pending'\n : isEmpty\n ? 'idle'\n : 'populated',\n }\n })\n emitChange('AsyncBatcher', this)\n }\n\n #getWait = (): number => {\n return parseFunctionOrValue(this.options.wait, this)\n }\n\n /**\n * Adds an item to the async batcher\n * If the batch size is reached, timeout occurs, or shouldProcess returns true, the batch will be processed\n *\n * @returns The result from the batch function, or undefined if an error occurred and was handled by onError\n *\n * @throws The error from the batch function if no onError handler is configured or throwOnError is true\n */\n addItem = async (item: TValue): Promise<any> => {\n this.#setState({\n items: [...this.store.state.items, item],\n isPending: this.options.wait !== Infinity,\n })\n this.options.onItemsChange?.(this)\n\n const shouldProcess =\n this.store.state.items.length >= this.options.maxSize ||\n this.options.getShouldExecute(this.store.state.items, this)\n\n if (shouldProcess) {\n return await this.#execute()\n } else if (this.options.wait !== Infinity) {\n this.#clearTimeout() // clear any pending timeout to replace it with a new one\n this.#timeoutId = setTimeout(() => this.#execute(), this.#getWait())\n await new Promise((resolve) => setTimeout(resolve, this.#getWait()))\n }\n }\n\n /**\n * Processes the current batch of items asynchronously.\n * This method will automatically be triggered if the batcher is running and any of these conditions are met:\n * - The number of items reaches maxSize\n * - The wait duration has elapsed\n * - The getShouldExecute function returns true upon adding an item\n *\n * You can also call this method manually to process the current batch at any time.\n *\n * @returns A promise that resolves with the result of the batch function, or undefined if an error occurred and was handled by onError\n * @throws The error from the batch function if no onError handler is configured or throwOnError is true\n */\n #execute = async (): Promise<any> => {\n if (this.store.state.items.length === 0) {\n return undefined\n }\n\n const currentExecuteCount = this.store.state.executeCount + 1\n const batch = this.peekAllItems() // copy of the items to be processed (to prevent race conditions)\n this.clear() // Clear items before processing to prevent race conditions\n this.options.onItemsChange?.(this)\n\n this.#setState({ isExecuting: true, executeCount: currentExecuteCount })\n\n try {\n const currentAsyncRetryer = new AsyncRetryer(\n this.fn,\n this.options.asyncRetryerOptions,\n )\n this.asyncRetryers.set(currentExecuteCount, currentAsyncRetryer)\n const result = await currentAsyncRetryer.execute(batch) // EXECUTE\n this.#setState({\n totalItemsProcessed:\n this.store.state.totalItemsProcessed + batch.length,\n lastResult: result,\n successCount: this.store.state.successCount + 1,\n })\n this.options.onSuccess?.(result, batch, this)\n return result\n } catch (error) {\n this.#setState({\n errorCount: this.store.state.errorCount + 1,\n failedItems: [...this.store.state.failedItems, ...batch],\n totalItemsFailed: this.store.state.totalItemsFailed + batch.length,\n })\n this.options.onError?.(error as Error, batch, this)\n if (this.options.throwOnError) {\n throw error\n }\n return undefined\n } finally {\n this.asyncRetryers.delete(currentExecuteCount) // dispose retryer\n this.#setState({\n isExecuting: false,\n settleCount: this.store.state.settleCount + 1,\n })\n this.options.onSettled?.(batch, this)\n }\n }\n\n /**\n * Processes the current batch of items immediately\n */\n flush = async (): Promise<any> => {\n this.#clearTimeout() // clear any pending timeout\n return await this.#execute()\n }\n\n /**\n * Returns a copy of all items in the async batcher\n */\n peekAllItems = (): Array<TValue> => {\n return [...this.store.state.items]\n }\n\n peekFailedItems = (): Array<TValue> => {\n return [...this.store.state.failedItems]\n }\n\n #clearTimeout = (): void => {\n if (this.#timeoutId) {\n clearTimeout(this.#timeoutId)\n this.#timeoutId = null\n }\n }\n\n /**\n * Removes all items from the async batcher\n */\n clear = (): void => {\n this.#setState({ items: [], failedItems: [], isPending: false })\n }\n\n /**\n * Returns the AbortSignal for a specific execution.\n * If no executeCount is provided, returns the signal for the most recent execution.\n * Returns null if no execution is found or not currently executing.\n *\n * @param executeCount - Optional specific execution to get signal for\n * @example\n * ```typescript\n * const batcher = new AsyncBatcher(\n * async (items: string[]) => {\n * const signal = batcher.getAbortSignal()\n * if (signal) {\n * const response = await fetch('/api/batch', {\n * method: 'POST',\n * body: JSON.stringify(items),\n * signal\n * })\n * return response.json()\n * }\n * },\n * { maxSize: 10, wait: 100 }\n * )\n * ```\n */\n getAbortSignal = (executeCount?: number): AbortSignal | null => {\n const count = executeCount ?? this.store.state.executeCount\n const retryer = this.asyncRetryers.get(count)\n return retryer?.getAbortSignal() ?? null\n }\n\n /**\n * Aborts all ongoing executions with the internal abort controllers.\n * Does NOT cancel any pending execution that have not started yet.\n * Does NOT clear out the items.\n */\n abort = (): void => {\n this.asyncRetryers.forEach((retryer) => retryer.abort())\n this.asyncRetryers.clear()\n this.#setState({\n isExecuting: false,\n })\n }\n\n /**\n * Cancels any pending execution that have not started yet.\n * Does NOT abort any execution already in progress.\n * Does NOT clear out the items.\n */\n cancel = (): void => {\n this.#clearTimeout()\n this.#setState({\n isPending: false,\n })\n }\n\n /**\n * Resets the async batcher state to its default values\n */\n reset = (): void => {\n this.#setState(getDefaultAsyncBatcherState<TValue>())\n this.options.onItemsChange?.(this)\n this.asyncRetryers.forEach((retryer) => retryer.reset())\n }\n}\n\n/**\n * Creates an async batcher that processes items in batches.\n *\n * Async vs Sync Versions:\n * The async version provides advanced features over the sync batch function:\n * - Returns promises that can be awaited for batch results\n * - Built-in retry support via AsyncRetryer integration\n * - Abort support to cancel in-flight batch executions\n * - Cancel support to prevent pending batches from starting\n * - Comprehensive error handling with onError callbacks and throwOnError control\n * - Detailed execution tracking (success/error/settle counts)\n *\n * The sync batch function is lighter weight and simpler when you don't need async features,\n * return values, or execution control.\n *\n * What is Batching?\n * Batching is a technique for grouping multiple operations together to be processed as a single unit.\n *\n * Configuration Options:\n * - `maxSize`: Maximum number of items per batch (default: Infinity)\n * - `wait`: Time to wait before processing batch (default: Infinity)\n * - `getShouldExecute`: Custom logic to trigger batch processing\n * - `asyncRetryerOptions`: Configure retry behavior for batch executions\n * - `started`: Whether to start processing immediately (default: true)\n *\n * Error Handling:\n * - If an `onError` handler is provided, it will be called with the error, the batch of items that failed, and batcher instance\n * - If `throwOnError` is true (default when no onError handler is provided), the error will be thrown\n * - If `throwOnError` is false (default when onError handler is provided), the error will be swallowed\n * - Both onError and throwOnError can be used together - the handler will be called before any error is thrown\n * - The error state can be checked using the underlying AsyncBatcher instance\n *\n * State Management:\n * - Uses TanStack Store for reactive state management\n * - Use `initialState` to provide initial state values when creating the async batcher\n * - Use `onSuccess` callback to react to successful batch execution and implement custom logic\n * - Use `onError` callback to react to batch execution errors and implement custom error handling\n * - Use `onSettled` callback to react to batch execution completion (success or error) and implement custom logic\n * - Use `onItemsChange` callback to react to items being added or removed from the batcher\n * - The state includes total items processed, success/error counts, and execution status\n * - State can be accessed via the underlying AsyncBatcher instance's `store.state` property\n * - When using framework adapters (React/Solid), state is accessed from the hook's state property\n *\n * @example\n * ```ts\n * const batchItems = asyncBatch<number>(\n * async (items) => {\n * const result = await processApiCall(items);\n * console.log('Processing:', items);\n * return result;\n * },\n * {\n * maxSize: 3,\n * wait: 1000,\n * onSuccess: (result) => console.log('Batch succeeded:', result),\n * onError: (error) => console.error('Batch failed:', error)\n * }\n * );\n *\n * batchItems(1);\n * batchItems(2);\n * batchItems(3); // Triggers batch processing\n * ```\n */\nexport function asyncBatch<TValue>(\n fn: (items: Array<TValue>) => Promise<any>,\n options: AsyncBatcherOptions<TValue>,\n) {\n const batcher = new AsyncBatcher<TValue>(fn, options)\n return batcher.addItem\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"async-batcher.js","names":["#setState","#execute","#clearTimeout","#timeoutId","#getWait"],"sources":["../src/async-batcher.ts"],"sourcesContent":["import { Store } from '@tanstack/store'\nimport { AsyncRetryer } from './async-retryer'\nimport { parseFunctionOrValue } from './utils'\nimport { emitChange, pacerEventClient } from './event-client'\nimport type { AsyncRetryerOptions } from './async-retryer'\nimport type { OptionalKeys } from './types'\n\nexport interface AsyncBatcherState<TValue> {\n /**\n * Number of batch executions that have resulted in errors\n */\n errorCount: number\n /**\n * Number of batch executions that have been executed\n */\n executeCount: number\n /**\n * Array of items that failed during batch processing\n */\n failedItems: Array<TValue>\n /**\n * Whether the batcher has no items to process (items array is empty)\n */\n isEmpty: boolean\n /**\n * Whether a batch is currently being processed asynchronously\n */\n isExecuting: boolean\n /**\n * Whether the batcher is waiting for the timeout to trigger batch processing\n */\n isPending: boolean\n /**\n * Array of items currently queued for batch processing\n */\n items: Array<TValue>\n /**\n * The result from the most recent batch execution\n */\n lastResult: any\n /**\n * Number of batch executions that have completed (either successfully or with errors)\n */\n settleCount: number\n /**\n * Number of items currently in the batch queue\n */\n size: number\n /**\n * Current processing status - 'idle' when not processing, 'pending' when waiting for timeout, 'executing' when processing, 'populated' when items are present, but no wait is configured\n */\n status: 'idle' | 'pending' | 'executing' | 'populated'\n /**\n * Number of batch executions that have completed successfully\n */\n successCount: number\n /**\n * Total number of items that have failed processing across all batches\n */\n totalItemsFailed: number\n /**\n * Total number of items that have been processed across all batches\n */\n totalItemsProcessed: number\n}\n\nfunction getDefaultAsyncBatcherState<TValue>(): AsyncBatcherState<TValue> {\n return {\n errorCount: 0,\n executeCount: 0,\n failedItems: [],\n isEmpty: true,\n isExecuting: false,\n isPending: false,\n items: [],\n lastResult: undefined,\n settleCount: 0,\n size: 0,\n status: 'idle',\n successCount: 0,\n totalItemsProcessed: 0,\n totalItemsFailed: 0,\n }\n}\n\n/**\n * Options for configuring an AsyncBatcher instance\n */\nexport interface AsyncBatcherOptions<TValue> {\n /**\n * Options for configuring the underlying async retryer\n */\n asyncRetryerOptions?: AsyncRetryerOptions<\n (items: Array<TValue>) => Promise<any>\n >\n /**\n * Custom function to determine if a batch should be processed\n * Return true to process the batch immediately\n */\n getShouldExecute?: (\n items: Array<TValue>,\n batcher: AsyncBatcher<TValue>,\n ) => boolean\n /**\n * Initial state for the async batcher\n */\n initialState?: Partial<AsyncBatcherState<TValue>>\n /**\n * Optional key to identify this async batcher instance.\n * If provided, the async batcher will be identified by this key in the devtools and PacerProvider if applicable.\n */\n key?: string\n /**\n * Maximum number of items in a batch\n * @default Infinity\n */\n maxSize?: number\n /**\n * Optional error handler for when the batch function throws.\n * If provided, the handler will be called with the error, the batch of items that failed, and batcher instance.\n * This can be used alongside throwOnError - the handler will be called before any error is thrown.\n */\n onError?: (\n error: Error,\n batch: Array<TValue>,\n batcher: AsyncBatcher<TValue>,\n ) => void\n /**\n * Callback fired after items are added to the batcher\n */\n onItemsChange?: (batcher: AsyncBatcher<TValue>) => void\n /**\n * Optional callback to call when a batch is settled (completed or failed)\n */\n onSettled?: (batch: Array<TValue>, batcher: AsyncBatcher<TValue>) => void\n /**\n * Optional callback to call when a batch succeeds\n */\n onSuccess?: (\n result: any,\n batch: Array<TValue>,\n batcher: AsyncBatcher<TValue>,\n ) => void\n /**\n * Whether the batcher should start processing immediately\n * @default true\n */\n started?: boolean\n /**\n * Whether to throw errors when they occur.\n * Defaults to true if no onError handler is provided, false if an onError handler is provided.\n * Can be explicitly set to override these defaults.\n */\n throwOnError?: boolean\n /**\n * Maximum time in milliseconds to wait before processing a batch.\n * If the wait duration has elapsed, the batch will be processed.\n * If not provided, the batch will not be triggered by a timeout.\n * @default Infinity\n */\n wait?: number | ((asyncBatcher: AsyncBatcher<TValue>) => number)\n}\n\n/**\n * Utility function for sharing common `AsyncBatcherOptions` options between different `AsyncBatcher` instances.\n *\n */\nexport function asyncBatcherOptions<\n TValue = any,\n TOptions extends Partial<AsyncBatcherOptions<TValue>> = Partial<\n AsyncBatcherOptions<TValue>\n >,\n>(options: TOptions): TOptions {\n return options\n}\n\ntype AsyncBatcherOptionsWithOptionalCallbacks<TValue> = OptionalKeys<\n Required<AsyncBatcherOptions<TValue>>,\n | 'initialState'\n | 'onError'\n | 'onItemsChange'\n | 'onSettled'\n | 'onSuccess'\n | 'key'\n>\n\nconst defaultOptions: AsyncBatcherOptionsWithOptionalCallbacks<any> = {\n asyncRetryerOptions: {\n maxAttempts: 1,\n },\n getShouldExecute: () => false,\n maxSize: Infinity,\n started: true,\n throwOnError: true,\n wait: Infinity,\n}\n\n/**\n * A class that collects items and processes them in batches asynchronously.\n *\n * Async vs Sync Versions:\n * The async version provides advanced features over the sync Batcher:\n * - Returns promises that can be awaited for batch results\n * - Built-in retry support via AsyncRetryer integration\n * - Abort support to cancel in-flight batch executions\n * - Cancel support to prevent pending batches from starting\n * - Comprehensive error handling with onError callbacks and throwOnError control\n * - Detailed execution tracking (success/error/settle counts)\n *\n * The sync Batcher is lighter weight and simpler when you don't need async features,\n * return values, or execution control.\n *\n * What is Batching?\n * Batching is a technique for grouping multiple operations together to be processed as a single unit.\n *\n * The AsyncBatcher provides a flexible way to implement async batching with configurable:\n * - Maximum batch size (number of items per batch)\n * - Time-based batching (process after X milliseconds)\n * - Custom batch processing logic via getShouldExecute\n * - Event callbacks for monitoring batch operations\n * - Error handling for failed batch operations\n *\n * Error Handling:\n * - If an `onError` handler is provided, it will be called with the error, the batch of items that failed, and batcher instance\n * - If `throwOnError` is true (default when no onError handler is provided), the error will be thrown\n * - If `throwOnError` is false (default when onError handler is provided), the error will be swallowed\n * - Both onError and throwOnError can be used together - the handler will be called before any error is thrown\n * - The error state can be checked using the AsyncBatcher instance\n *\n * State Management:\n * - Uses TanStack Store for reactive state management\n * - Use `initialState` to provide initial state values when creating the async batcher\n * - Use `onSuccess` callback to react to successful batch execution and implement custom logic\n * - Use `onError` callback to react to batch execution errors and implement custom error handling\n * - Use `onSettled` callback to react to batch execution completion (success or error) and implement custom logic\n * - Use `onExecute` callback to react to batch execution and implement custom logic\n * - Use `onItemsChange` callback to react to items being added or removed from the batcher\n * - The state includes total items processed, success/error counts, and execution status\n * - State can be accessed via `asyncBatcher.store.state` when using the class directly\n * - When using framework adapters (React/Solid), state is accessed from `asyncBatcher.state`\n *\n * @example\n * ```ts\n * const batcher = new AsyncBatcher<number>(\n * async (items) => {\n * const result = await processItems(items);\n * console.log('Processing batch:', items);\n * return result;\n * },\n * {\n * maxSize: 5,\n * wait: 2000,\n * onSuccess: (result) => console.log('Batch succeeded:', result),\n * onError: (error) => console.error('Batch failed:', error)\n * }\n * );\n *\n * batcher.addItem(1);\n * batcher.addItem(2);\n * // After 2 seconds or when 5 items are added, whichever comes first,\n * // the batch will be processed and the result will be available\n * // batcher.execute() // manually trigger a batch\n * ```\n */\nexport class AsyncBatcher<TValue> {\n readonly store: Store<Readonly<AsyncBatcherState<TValue>>> = new Store(\n getDefaultAsyncBatcherState<TValue>(),\n )\n key: string | undefined\n options: AsyncBatcherOptionsWithOptionalCallbacks<TValue>\n asyncRetryers = new Map<\n number,\n AsyncRetryer<(items: Array<TValue>) => Promise<any>>\n >()\n #timeoutId: ReturnType<typeof setTimeout> | null = null\n\n constructor(\n public fn: (items: Array<TValue>) => Promise<any>,\n initialOptions: AsyncBatcherOptions<TValue>,\n ) {\n this.key = initialOptions.key\n this.options = {\n ...defaultOptions,\n ...initialOptions,\n throwOnError: initialOptions.throwOnError ?? !initialOptions.onError,\n }\n this.#setState(this.options.initialState ?? {})\n\n if (this.key) {\n pacerEventClient.on('d-AsyncBatcher', (event) => {\n if (event.payload.key !== this.key) return\n this.#setState(\n event.payload.store.state as Partial<AsyncBatcherState<TValue>>,\n )\n this.setOptions(\n event.payload.options as Partial<AsyncBatcherOptions<TValue>>,\n )\n })\n }\n }\n\n /**\n * Updates the async batcher options\n */\n setOptions = (newOptions: Partial<AsyncBatcherOptions<TValue>>): void => {\n this.options = { ...this.options, ...newOptions }\n }\n\n #setState = (newState: Partial<AsyncBatcherState<TValue>>): void => {\n this.store.setState((state) => {\n const combinedState = {\n ...state,\n ...newState,\n }\n const { isExecuting, isPending, items } = combinedState\n const size = items.length\n const isEmpty = size === 0\n return {\n ...combinedState,\n isEmpty,\n size,\n status: isExecuting\n ? 'executing'\n : isPending\n ? 'pending'\n : isEmpty\n ? 'idle'\n : 'populated',\n }\n })\n emitChange('AsyncBatcher', this)\n }\n\n #getWait = (): number => {\n return parseFunctionOrValue(this.options.wait, this)\n }\n\n /**\n * Adds an item to the async batcher\n * If the batch size is reached, timeout occurs, or shouldProcess returns true, the batch will be processed\n *\n * @returns The result from the batch function, or undefined if an error occurred and was handled by onError\n *\n * @throws The error from the batch function if no onError handler is configured or throwOnError is true\n */\n addItem = async (item: TValue): Promise<any> => {\n this.#setState({\n items: [...this.store.state.items, item],\n isPending: this.options.wait !== Infinity,\n })\n this.options.onItemsChange?.(this)\n\n const shouldProcess =\n this.store.state.items.length >= this.options.maxSize ||\n this.options.getShouldExecute(this.store.state.items, this)\n\n if (shouldProcess) {\n return await this.#execute()\n } else if (this.options.wait !== Infinity) {\n this.#clearTimeout() // clear any pending timeout to replace it with a new one\n this.#timeoutId = setTimeout(() => this.#execute(), this.#getWait())\n await new Promise((resolve) => setTimeout(resolve, this.#getWait()))\n }\n }\n\n /**\n * Processes the current batch of items asynchronously.\n * This method will automatically be triggered if the batcher is running and any of these conditions are met:\n * - The number of items reaches maxSize\n * - The wait duration has elapsed\n * - The getShouldExecute function returns true upon adding an item\n *\n * You can also call this method manually to process the current batch at any time.\n *\n * @returns A promise that resolves with the result of the batch function, or undefined if an error occurred and was handled by onError\n * @throws The error from the batch function if no onError handler is configured or throwOnError is true\n */\n #execute = async (): Promise<any> => {\n if (this.store.state.items.length === 0) {\n return undefined\n }\n\n const currentExecuteCount = this.store.state.executeCount + 1\n const batch = this.peekAllItems() // copy of the items to be processed (to prevent race conditions)\n this.clear() // Clear items before processing to prevent race conditions\n this.options.onItemsChange?.(this)\n\n this.#setState({ isExecuting: true, executeCount: currentExecuteCount })\n\n try {\n const currentAsyncRetryer = new AsyncRetryer(\n this.fn,\n this.options.asyncRetryerOptions,\n )\n this.asyncRetryers.set(currentExecuteCount, currentAsyncRetryer)\n const result = await currentAsyncRetryer.execute(batch) // EXECUTE\n this.#setState({\n totalItemsProcessed:\n this.store.state.totalItemsProcessed + batch.length,\n lastResult: result,\n successCount: this.store.state.successCount + 1,\n })\n this.options.onSuccess?.(result, batch, this)\n return result\n } catch (error) {\n this.#setState({\n errorCount: this.store.state.errorCount + 1,\n failedItems: [...this.store.state.failedItems, ...batch],\n totalItemsFailed: this.store.state.totalItemsFailed + batch.length,\n })\n this.options.onError?.(error as Error, batch, this)\n if (this.options.throwOnError) {\n throw error\n }\n return undefined\n } finally {\n this.asyncRetryers.delete(currentExecuteCount) // dispose retryer\n this.#setState({\n isExecuting: false,\n settleCount: this.store.state.settleCount + 1,\n })\n this.options.onSettled?.(batch, this)\n }\n }\n\n /**\n * Processes the current batch of items immediately\n */\n flush = async (): Promise<any> => {\n this.#clearTimeout() // clear any pending timeout\n return await this.#execute()\n }\n\n /**\n * Returns a copy of all items in the async batcher\n */\n peekAllItems = (): Array<TValue> => {\n return [...this.store.state.items]\n }\n\n peekFailedItems = (): Array<TValue> => {\n return [...this.store.state.failedItems]\n }\n\n #clearTimeout = (): void => {\n if (this.#timeoutId) {\n clearTimeout(this.#timeoutId)\n this.#timeoutId = null\n }\n }\n\n /**\n * Removes all items from the async batcher\n */\n clear = (): void => {\n this.#setState({ items: [], failedItems: [], isPending: false })\n }\n\n /**\n * Returns the AbortSignal for a specific execution.\n * If no executeCount is provided, returns the signal for the most recent execution.\n * Returns null if no execution is found or not currently executing.\n *\n * @param executeCount - Optional specific execution to get signal for\n * @example\n * ```typescript\n * const batcher = new AsyncBatcher(\n * async (items: string[]) => {\n * const signal = batcher.getAbortSignal()\n * if (signal) {\n * const response = await fetch('/api/batch', {\n * method: 'POST',\n * body: JSON.stringify(items),\n * signal\n * })\n * return response.json()\n * }\n * },\n * { maxSize: 10, wait: 100 }\n * )\n * ```\n */\n getAbortSignal = (executeCount?: number): AbortSignal | null => {\n const count = executeCount ?? this.store.state.executeCount\n const retryer = this.asyncRetryers.get(count)\n return retryer?.getAbortSignal() ?? null\n }\n\n /**\n * Aborts all ongoing executions with the internal abort controllers.\n * Does NOT cancel any pending execution that have not started yet.\n * Does NOT clear out the items.\n */\n abort = (): void => {\n this.asyncRetryers.forEach((retryer) => retryer.abort())\n this.asyncRetryers.clear()\n this.#setState({\n isExecuting: false,\n })\n }\n\n /**\n * Cancels any pending execution that have not started yet.\n * Does NOT abort any execution already in progress.\n * Does NOT clear out the items.\n */\n cancel = (): void => {\n this.#clearTimeout()\n this.#setState({\n isPending: false,\n })\n }\n\n /**\n * Resets the async batcher state to its default values\n */\n reset = (): void => {\n this.#setState(getDefaultAsyncBatcherState<TValue>())\n this.options.onItemsChange?.(this)\n this.asyncRetryers.forEach((retryer) => retryer.reset())\n }\n}\n\n/**\n * Creates an async batcher that processes items in batches.\n *\n * Async vs Sync Versions:\n * The async version provides advanced features over the sync batch function:\n * - Returns promises that can be awaited for batch results\n * - Built-in retry support via AsyncRetryer integration\n * - Abort support to cancel in-flight batch executions\n * - Cancel support to prevent pending batches from starting\n * - Comprehensive error handling with onError callbacks and throwOnError control\n * - Detailed execution tracking (success/error/settle counts)\n *\n * The sync batch function is lighter weight and simpler when you don't need async features,\n * return values, or execution control.\n *\n * What is Batching?\n * Batching is a technique for grouping multiple operations together to be processed as a single unit.\n *\n * Configuration Options:\n * - `maxSize`: Maximum number of items per batch (default: Infinity)\n * - `wait`: Time to wait before processing batch (default: Infinity)\n * - `getShouldExecute`: Custom logic to trigger batch processing\n * - `asyncRetryerOptions`: Configure retry behavior for batch executions\n * - `started`: Whether to start processing immediately (default: true)\n *\n * Error Handling:\n * - If an `onError` handler is provided, it will be called with the error, the batch of items that failed, and batcher instance\n * - If `throwOnError` is true (default when no onError handler is provided), the error will be thrown\n * - If `throwOnError` is false (default when onError handler is provided), the error will be swallowed\n * - Both onError and throwOnError can be used together - the handler will be called before any error is thrown\n * - The error state can be checked using the underlying AsyncBatcher instance\n *\n * State Management:\n * - Uses TanStack Store for reactive state management\n * - Use `initialState` to provide initial state values when creating the async batcher\n * - Use `onSuccess` callback to react to successful batch execution and implement custom logic\n * - Use `onError` callback to react to batch execution errors and implement custom error handling\n * - Use `onSettled` callback to react to batch execution completion (success or error) and implement custom logic\n * - Use `onItemsChange` callback to react to items being added or removed from the batcher\n * - The state includes total items processed, success/error counts, and execution status\n * - State can be accessed via the underlying AsyncBatcher instance's `store.state` property\n * - When using framework adapters (React/Solid), state is accessed from the hook's state property\n *\n * @example\n * ```ts\n * const batchItems = asyncBatch<number>(\n * async (items) => {\n * const result = await processApiCall(items);\n * console.log('Processing:', items);\n * return result;\n * },\n * {\n * maxSize: 3,\n * wait: 1000,\n * onSuccess: (result) => console.log('Batch succeeded:', result),\n * onError: (error) => console.error('Batch failed:', error)\n * }\n * );\n *\n * batchItems(1);\n * batchItems(2);\n * batchItems(3); // Triggers batch processing\n * ```\n */\nexport function asyncBatch<TValue>(\n fn: (items: Array<TValue>) => Promise<any>,\n options: AsyncBatcherOptions<TValue>,\n) {\n const batcher = new AsyncBatcher<TValue>(fn, options)\n return batcher.addItem\n}\n"],"mappings":"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"}
|