@tanstack/pacer 0.8.0 → 0.9.0

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.
Files changed (91) hide show
  1. package/dist/cjs/async-batcher.cjs +163 -0
  2. package/dist/cjs/async-batcher.cjs.map +1 -0
  3. package/dist/cjs/async-batcher.d.cts +273 -0
  4. package/dist/cjs/async-debouncer.cjs +149 -162
  5. package/dist/cjs/async-debouncer.cjs.map +1 -1
  6. package/dist/cjs/async-debouncer.d.cts +76 -57
  7. package/dist/cjs/async-queuer.cjs +282 -343
  8. package/dist/cjs/async-queuer.cjs.map +1 -1
  9. package/dist/cjs/async-queuer.d.cts +121 -100
  10. package/dist/cjs/async-rate-limiter.cjs +128 -185
  11. package/dist/cjs/async-rate-limiter.cjs.map +1 -1
  12. package/dist/cjs/async-rate-limiter.d.cts +72 -61
  13. package/dist/cjs/async-throttler.cjs +168 -178
  14. package/dist/cjs/async-throttler.cjs.map +1 -1
  15. package/dist/cjs/async-throttler.d.cts +97 -69
  16. package/dist/cjs/batcher.cjs +110 -119
  17. package/dist/cjs/batcher.cjs.map +1 -1
  18. package/dist/cjs/batcher.d.cts +76 -51
  19. package/dist/cjs/debouncer.cjs +97 -85
  20. package/dist/cjs/debouncer.cjs.map +1 -1
  21. package/dist/cjs/debouncer.d.cts +54 -26
  22. package/dist/cjs/index.cjs +3 -6
  23. package/dist/cjs/index.cjs.map +1 -1
  24. package/dist/cjs/index.d.cts +1 -1
  25. package/dist/cjs/queuer.cjs +246 -294
  26. package/dist/cjs/queuer.cjs.map +1 -1
  27. package/dist/cjs/queuer.d.cts +102 -81
  28. package/dist/cjs/rate-limiter.cjs +97 -130
  29. package/dist/cjs/rate-limiter.cjs.map +1 -1
  30. package/dist/cjs/rate-limiter.d.cts +50 -37
  31. package/dist/cjs/throttler.cjs +107 -123
  32. package/dist/cjs/throttler.cjs.map +1 -1
  33. package/dist/cjs/throttler.d.cts +59 -35
  34. package/dist/cjs/utils.cjs +0 -13
  35. package/dist/cjs/utils.cjs.map +1 -1
  36. package/dist/cjs/utils.d.cts +0 -1
  37. package/dist/esm/async-batcher.d.ts +273 -0
  38. package/dist/esm/async-batcher.js +163 -0
  39. package/dist/esm/async-batcher.js.map +1 -0
  40. package/dist/esm/async-debouncer.d.ts +76 -57
  41. package/dist/esm/async-debouncer.js +149 -162
  42. package/dist/esm/async-debouncer.js.map +1 -1
  43. package/dist/esm/async-queuer.d.ts +121 -100
  44. package/dist/esm/async-queuer.js +282 -343
  45. package/dist/esm/async-queuer.js.map +1 -1
  46. package/dist/esm/async-rate-limiter.d.ts +72 -61
  47. package/dist/esm/async-rate-limiter.js +128 -185
  48. package/dist/esm/async-rate-limiter.js.map +1 -1
  49. package/dist/esm/async-throttler.d.ts +97 -69
  50. package/dist/esm/async-throttler.js +168 -178
  51. package/dist/esm/async-throttler.js.map +1 -1
  52. package/dist/esm/batcher.d.ts +76 -51
  53. package/dist/esm/batcher.js +110 -119
  54. package/dist/esm/batcher.js.map +1 -1
  55. package/dist/esm/debouncer.d.ts +54 -26
  56. package/dist/esm/debouncer.js +97 -85
  57. package/dist/esm/debouncer.js.map +1 -1
  58. package/dist/esm/index.d.ts +1 -1
  59. package/dist/esm/index.js +4 -7
  60. package/dist/esm/queuer.d.ts +102 -81
  61. package/dist/esm/queuer.js +246 -294
  62. package/dist/esm/queuer.js.map +1 -1
  63. package/dist/esm/rate-limiter.d.ts +50 -37
  64. package/dist/esm/rate-limiter.js +97 -130
  65. package/dist/esm/rate-limiter.js.map +1 -1
  66. package/dist/esm/throttler.d.ts +59 -35
  67. package/dist/esm/throttler.js +107 -123
  68. package/dist/esm/throttler.js.map +1 -1
  69. package/dist/esm/utils.d.ts +0 -1
  70. package/dist/esm/utils.js +0 -13
  71. package/dist/esm/utils.js.map +1 -1
  72. package/package.json +14 -11
  73. package/src/async-batcher.ts +475 -0
  74. package/src/async-debouncer.ts +201 -121
  75. package/src/async-queuer.ts +337 -216
  76. package/src/async-rate-limiter.ts +176 -136
  77. package/src/async-throttler.ts +233 -139
  78. package/src/batcher.ts +158 -92
  79. package/src/debouncer.ts +135 -52
  80. package/src/index.ts +1 -1
  81. package/src/queuer.ts +348 -226
  82. package/src/rate-limiter.ts +125 -80
  83. package/src/throttler.ts +152 -78
  84. package/src/utils.ts +0 -15
  85. package/dist/cjs/compare.cjs +0 -72
  86. package/dist/cjs/compare.cjs.map +0 -1
  87. package/dist/cjs/compare.d.cts +0 -12
  88. package/dist/esm/compare.d.ts +0 -12
  89. package/dist/esm/compare.js +0 -72
  90. package/dist/esm/compare.js.map +0 -1
  91. package/src/compare.ts +0 -105
@@ -0,0 +1,475 @@
1
+ import { Store } from '@tanstack/store'
2
+ import { parseFunctionOrValue } from './utils'
3
+ import type { OptionalKeys } from './types'
4
+
5
+ export interface AsyncBatcherState<TValue> {
6
+ /**
7
+ * Number of batch executions that have resulted in errors
8
+ */
9
+ errorCount: number
10
+ /**
11
+ * Array of items that failed during batch processing
12
+ */
13
+ failedItems: Array<TValue>
14
+ /**
15
+ * Whether the batcher has no items to process (items array is empty)
16
+ */
17
+ isEmpty: boolean
18
+ /**
19
+ * Whether a batch is currently being processed asynchronously
20
+ */
21
+ isExecuting: boolean
22
+ /**
23
+ * Whether the batcher is waiting for the timeout to trigger batch processing
24
+ */
25
+ isPending: boolean
26
+ /**
27
+ * Whether the batcher is active and will process items automatically
28
+ */
29
+ isRunning: boolean
30
+ /**
31
+ * Array of items currently queued for batch processing
32
+ */
33
+ items: Array<TValue>
34
+ /**
35
+ * The result from the most recent batch execution
36
+ */
37
+ lastResult: any
38
+ /**
39
+ * Number of batch executions that have completed (either successfully or with errors)
40
+ */
41
+ settleCount: number
42
+ /**
43
+ * Number of items currently in the batch queue
44
+ */
45
+ size: number
46
+ /**
47
+ * 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
48
+ */
49
+ status: 'idle' | 'pending' | 'executing' | 'populated'
50
+ /**
51
+ * Number of batch executions that have completed successfully
52
+ */
53
+ successCount: number
54
+ /**
55
+ * Total number of items that have been processed across all batches
56
+ */
57
+ totalItemsProcessed: number
58
+ /**
59
+ * Total number of items that have failed processing across all batches
60
+ */
61
+ totalItemsFailed: number
62
+ }
63
+
64
+ function getDefaultAsyncBatcherState<TValue>(): AsyncBatcherState<TValue> {
65
+ return {
66
+ errorCount: 0,
67
+ failedItems: [],
68
+ isEmpty: true,
69
+ isExecuting: false,
70
+ isPending: false,
71
+ isRunning: true,
72
+ items: [],
73
+ lastResult: undefined,
74
+ settleCount: 0,
75
+ size: 0,
76
+ status: 'idle',
77
+ successCount: 0,
78
+ totalItemsProcessed: 0,
79
+ totalItemsFailed: 0,
80
+ }
81
+ }
82
+
83
+ /**
84
+ * Options for configuring an AsyncBatcher instance
85
+ */
86
+ export interface AsyncBatcherOptions<TValue> {
87
+ /**
88
+ * Custom function to determine if a batch should be processed
89
+ * Return true to process the batch immediately
90
+ */
91
+ getShouldExecute?: (
92
+ items: Array<TValue>,
93
+ batcher: AsyncBatcher<TValue>,
94
+ ) => boolean
95
+ /**
96
+ * Initial state for the async batcher
97
+ */
98
+ initialState?: Partial<AsyncBatcherState<TValue>>
99
+ /**
100
+ * Maximum number of items in a batch
101
+ * @default Infinity
102
+ */
103
+ maxSize?: number
104
+ /**
105
+ * Optional error handler for when the batch function throws.
106
+ * If provided, the handler will be called with the error and batcher instance.
107
+ * This can be used alongside throwOnError - the handler will be called before any error is thrown.
108
+ */
109
+ onError?: (
110
+ error: unknown,
111
+ failedItems: Array<TValue>,
112
+ batcher: AsyncBatcher<TValue>,
113
+ ) => void
114
+ /**
115
+ * Callback fired after a batch is processed
116
+ */
117
+ onExecute?: (batcher: AsyncBatcher<TValue>) => void
118
+ /**
119
+ * Callback fired after items are added to the batcher
120
+ */
121
+ onItemsChange?: (batcher: AsyncBatcher<TValue>) => void
122
+ /**
123
+ * Optional callback to call when a batch is settled (completed or failed)
124
+ */
125
+ onSettled?: (batcher: AsyncBatcher<TValue>) => void
126
+ /**
127
+ * Optional callback to call when a batch succeeds
128
+ */
129
+ onSuccess?: (result: any, batcher: AsyncBatcher<TValue>) => void
130
+ /**
131
+ * Whether the batcher should start processing immediately
132
+ * @default true
133
+ */
134
+ started?: boolean
135
+ /**
136
+ * Whether to throw errors when they occur.
137
+ * Defaults to true if no onError handler is provided, false if an onError handler is provided.
138
+ * Can be explicitly set to override these defaults.
139
+ */
140
+ throwOnError?: boolean
141
+ /**
142
+ * Maximum time in milliseconds to wait before processing a batch.
143
+ * If the wait duration has elapsed, the batch will be processed.
144
+ * If not provided, the batch will not be triggered by a timeout.
145
+ * @default Infinity
146
+ */
147
+ wait?: number | ((asyncBatcher: AsyncBatcher<TValue>) => number)
148
+ }
149
+
150
+ type AsyncBatcherOptionsWithOptionalCallbacks<TValue> = OptionalKeys<
151
+ Required<AsyncBatcherOptions<TValue>>,
152
+ | 'initialState'
153
+ | 'onError'
154
+ | 'onExecute'
155
+ | 'onItemsChange'
156
+ | 'onSettled'
157
+ | 'onSuccess'
158
+ >
159
+
160
+ const defaultOptions: AsyncBatcherOptionsWithOptionalCallbacks<any> = {
161
+ getShouldExecute: () => false,
162
+ maxSize: Infinity,
163
+ started: true,
164
+ throwOnError: true,
165
+ wait: Infinity,
166
+ }
167
+
168
+ /**
169
+ * A class that collects items and processes them in batches asynchronously.
170
+ *
171
+ * This is the async version of the Batcher class. Unlike the sync version, this async batcher:
172
+ * - Handles promises and returns results from batch executions
173
+ * - Provides error handling with configurable error behavior
174
+ * - Tracks success, error, and settle counts separately
175
+ * - Has state tracking for when batches are executing
176
+ * - Returns the result of the batch function execution
177
+ *
178
+ * Batching is a technique for grouping multiple operations together to be processed as a single unit.
179
+ *
180
+ * The AsyncBatcher provides a flexible way to implement async batching with configurable:
181
+ * - Maximum batch size (number of items per batch)
182
+ * - Time-based batching (process after X milliseconds)
183
+ * - Custom batch processing logic via getShouldExecute
184
+ * - Event callbacks for monitoring batch operations
185
+ * - Error handling for failed batch operations
186
+ *
187
+ * Error Handling:
188
+ * - If an `onError` handler is provided, it will be called with the error and batcher instance
189
+ * - If `throwOnError` is true (default when no onError handler is provided), the error will be thrown
190
+ * - If `throwOnError` is false (default when onError handler is provided), the error will be swallowed
191
+ * - Both onError and throwOnError can be used together - the handler will be called before any error is thrown
192
+ * - The error state can be checked using the AsyncBatcher instance
193
+ *
194
+ * State Management:
195
+ * - Uses TanStack Store for reactive state management
196
+ * - Use `initialState` to provide initial state values when creating the async batcher
197
+ * - Use `onSuccess` callback to react to successful batch execution and implement custom logic
198
+ * - Use `onError` callback to react to batch execution errors and implement custom error handling
199
+ * - Use `onSettled` callback to react to batch execution completion (success or error) and implement custom logic
200
+ * - Use `onExecute` callback to react to batch execution and implement custom logic
201
+ * - Use `onItemsChange` callback to react to items being added or removed from the batcher
202
+ * - The state includes total items processed, success/error counts, and execution status
203
+ * - State can be accessed via `asyncBatcher.store.state` when using the class directly
204
+ * - When using framework adapters (React/Solid), state is accessed from `asyncBatcher.state`
205
+ *
206
+ * @example
207
+ * ```ts
208
+ * const batcher = new AsyncBatcher<number>(
209
+ * async (items) => {
210
+ * const result = await processItems(items);
211
+ * console.log('Processing batch:', items);
212
+ * return result;
213
+ * },
214
+ * {
215
+ * maxSize: 5,
216
+ * wait: 2000,
217
+ * onSuccess: (result) => console.log('Batch succeeded:', result),
218
+ * onError: (error) => console.error('Batch failed:', error)
219
+ * }
220
+ * );
221
+ *
222
+ * batcher.addItem(1);
223
+ * batcher.addItem(2);
224
+ * // After 2 seconds or when 5 items are added, whichever comes first,
225
+ * // the batch will be processed and the result will be available
226
+ * // batcher.execute() // manually trigger a batch
227
+ * ```
228
+ */
229
+ export class AsyncBatcher<TValue> {
230
+ readonly store: Store<Readonly<AsyncBatcherState<TValue>>> = new Store(
231
+ getDefaultAsyncBatcherState<TValue>(),
232
+ )
233
+ options: AsyncBatcherOptionsWithOptionalCallbacks<TValue>
234
+ #timeoutId: NodeJS.Timeout | null = null
235
+
236
+ constructor(
237
+ private fn: (items: Array<TValue>) => Promise<any>,
238
+ initialOptions: AsyncBatcherOptions<TValue>,
239
+ ) {
240
+ this.options = {
241
+ ...defaultOptions,
242
+ ...initialOptions,
243
+ throwOnError: initialOptions.throwOnError ?? !initialOptions.onError,
244
+ }
245
+ this.#setState(this.options.initialState ?? {})
246
+ }
247
+
248
+ /**
249
+ * Updates the async batcher options
250
+ */
251
+ setOptions = (newOptions: Partial<AsyncBatcherOptions<TValue>>): void => {
252
+ this.options = { ...this.options, ...newOptions }
253
+ }
254
+
255
+ #setState = (newState: Partial<AsyncBatcherState<TValue>>): void => {
256
+ this.store.setState((state) => {
257
+ const combinedState = {
258
+ ...state,
259
+ ...newState,
260
+ }
261
+ const { isExecuting, isPending, items } = combinedState
262
+ const size = items.length
263
+ const isEmpty = size === 0
264
+ return {
265
+ ...combinedState,
266
+ isEmpty,
267
+ size,
268
+ status: isExecuting
269
+ ? 'executing'
270
+ : isPending
271
+ ? 'pending'
272
+ : isEmpty
273
+ ? 'idle'
274
+ : 'populated',
275
+ }
276
+ })
277
+ }
278
+
279
+ #getWait = (): number => {
280
+ return parseFunctionOrValue(this.options.wait, this)
281
+ }
282
+
283
+ /**
284
+ * Adds an item to the async batcher
285
+ * If the batch size is reached, timeout occurs, or shouldProcess returns true, the batch will be processed
286
+ */
287
+ addItem = (item: TValue): void => {
288
+ this.#setState({
289
+ items: [...this.store.state.items, item],
290
+ isPending: this.options.wait !== Infinity,
291
+ })
292
+ this.options.onItemsChange?.(this)
293
+
294
+ const shouldProcess =
295
+ this.store.state.items.length >= this.options.maxSize ||
296
+ this.options.getShouldExecute(this.store.state.items, this)
297
+
298
+ if (shouldProcess) {
299
+ this.#execute()
300
+ } else if (this.store.state.isRunning && this.options.wait !== Infinity) {
301
+ this.#clearTimeout() // clear any pending timeout to replace it with a new one
302
+ this.#timeoutId = setTimeout(() => this.#execute(), this.#getWait())
303
+ }
304
+ }
305
+
306
+ /**
307
+ * Processes the current batch of items asynchronously.
308
+ * This method will automatically be triggered if the batcher is running and any of these conditions are met:
309
+ * - The number of items reaches maxSize
310
+ * - The wait duration has elapsed
311
+ * - The getShouldExecute function returns true upon adding an item
312
+ *
313
+ * You can also call this method manually to process the current batch at any time.
314
+ *
315
+ * @returns A promise that resolves with the result of the batch function, or undefined if an error occurred and was handled by onError
316
+ * @throws The error from the batch function if no onError handler is configured
317
+ */
318
+ #execute = async (): Promise<any> => {
319
+ if (this.store.state.items.length === 0) {
320
+ return undefined
321
+ }
322
+
323
+ const batch = this.peekAllItems() // copy of the items to be processed (to prevent race conditions)
324
+ this.clear() // Clear items before processing to prevent race conditions
325
+ this.options.onItemsChange?.(this) // Call onItemsChange to notify listeners that the items have changed
326
+
327
+ this.#setState({ isExecuting: true })
328
+
329
+ try {
330
+ const result = await this.fn(batch) // EXECUTE
331
+ this.#setState({
332
+ totalItemsProcessed:
333
+ this.store.state.totalItemsProcessed + batch.length,
334
+ lastResult: result,
335
+ successCount: this.store.state.successCount + 1,
336
+ })
337
+ this.options.onSuccess?.(result, this)
338
+ return result
339
+ } catch (error) {
340
+ this.#setState({
341
+ errorCount: this.store.state.errorCount + 1,
342
+ failedItems: [...this.store.state.failedItems, ...batch],
343
+ totalItemsFailed: this.store.state.totalItemsFailed + batch.length,
344
+ })
345
+ this.options.onError?.(error, batch, this)
346
+ if (this.options.throwOnError) {
347
+ throw error
348
+ }
349
+ return undefined
350
+ } finally {
351
+ this.#setState({
352
+ isExecuting: false,
353
+ settleCount: this.store.state.settleCount + 1,
354
+ })
355
+ this.options.onSettled?.(this)
356
+ this.options.onExecute?.(this)
357
+ }
358
+ }
359
+
360
+ /**
361
+ * Processes the current batch of items immediately
362
+ */
363
+ flush = async (): Promise<any> => {
364
+ this.#clearTimeout() // clear any pending timeout
365
+ return await this.#execute()
366
+ }
367
+
368
+ /**
369
+ * Stops the async batcher from processing batches
370
+ */
371
+ stop = (): void => {
372
+ this.#setState({ isRunning: false })
373
+ this.#clearTimeout()
374
+ }
375
+
376
+ /**
377
+ * Starts the async batcher and processes any pending items
378
+ */
379
+ start = (): void => {
380
+ this.#setState({ isRunning: true })
381
+ if (this.store.state.items.length > 0 && !this.#timeoutId) {
382
+ this.#timeoutId = setTimeout(() => this.#execute(), this.#getWait())
383
+ }
384
+ }
385
+
386
+ /**
387
+ * Returns a copy of all items in the async batcher
388
+ */
389
+ peekAllItems = (): Array<TValue> => {
390
+ return [...this.store.state.items]
391
+ }
392
+
393
+ peekFailedItems = (): Array<TValue> => {
394
+ return [...this.store.state.failedItems]
395
+ }
396
+
397
+ #clearTimeout = (): void => {
398
+ if (this.#timeoutId) {
399
+ clearTimeout(this.#timeoutId)
400
+ this.#timeoutId = null
401
+ }
402
+ }
403
+
404
+ /**
405
+ * Removes all items from the async batcher
406
+ */
407
+ clear = (): void => {
408
+ this.#setState({ items: [], failedItems: [], isPending: false })
409
+ }
410
+
411
+ /**
412
+ * Resets the async batcher state to its default values
413
+ */
414
+ reset = (): void => {
415
+ this.#setState(getDefaultAsyncBatcherState<TValue>())
416
+ this.options.onItemsChange?.(this)
417
+ }
418
+ }
419
+
420
+ /**
421
+ * Creates an async batcher that processes items in batches
422
+ *
423
+ * Unlike the sync batcher, this async version:
424
+ * - Handles promises and returns results from batch executions
425
+ * - Provides error handling with configurable error behavior
426
+ * - Tracks success, error, and settle counts separately
427
+ * - Has state tracking for when batches are executing
428
+ *
429
+ * Error Handling:
430
+ * - If an `onError` handler is provided, it will be called with the error and batcher instance
431
+ * - If `throwOnError` is true (default when no onError handler is provided), the error will be thrown
432
+ * - If `throwOnError` is false (default when onError handler is provided), the error will be swallowed
433
+ * - Both onError and throwOnError can be used together - the handler will be called before any error is thrown
434
+ * - The error state can be checked using the underlying AsyncBatcher instance
435
+ *
436
+ * State Management:
437
+ * - Uses TanStack Store for reactive state management
438
+ * - Use `initialState` to provide initial state values when creating the async batcher
439
+ * - Use `onSuccess` callback to react to successful batch execution and implement custom logic
440
+ * - Use `onError` callback to react to batch execution errors and implement custom error handling
441
+ * - Use `onSettled` callback to react to batch execution completion (success or error) and implement custom logic
442
+ * - Use `onExecute` callback to react to batch execution and implement custom logic
443
+ * - Use `onItemsChange` callback to react to items being added or removed from the batcher
444
+ * - The state includes total items processed, success/error counts, and execution status
445
+ * - State can be accessed via the underlying AsyncBatcher instance's `store.state` property
446
+ * - When using framework adapters (React/Solid), state is accessed from the hook's state property
447
+ *
448
+ * @example
449
+ * ```ts
450
+ * const batchItems = asyncBatch<number>(
451
+ * async (items) => {
452
+ * const result = await processApiCall(items);
453
+ * console.log('Processing:', items);
454
+ * return result;
455
+ * },
456
+ * {
457
+ * maxSize: 3,
458
+ * wait: 1000,
459
+ * onSuccess: (result) => console.log('Batch succeeded:', result),
460
+ * onError: (error) => console.error('Batch failed:', error)
461
+ * }
462
+ * );
463
+ *
464
+ * batchItems(1);
465
+ * batchItems(2);
466
+ * batchItems(3); // Triggers batch processing
467
+ * ```
468
+ */
469
+ export function asyncBatch<TValue>(
470
+ fn: (items: Array<TValue>) => Promise<any>,
471
+ options: AsyncBatcherOptions<TValue>,
472
+ ) {
473
+ const batcher = new AsyncBatcher<TValue>(fn, options)
474
+ return batcher.addItem
475
+ }