@tanstack/pacer 0.15.4 → 0.16.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 (92) hide show
  1. package/dist/cjs/async-batcher.cjs +66 -4
  2. package/dist/cjs/async-batcher.cjs.map +1 -1
  3. package/dist/cjs/async-batcher.d.cts +97 -16
  4. package/dist/cjs/async-debouncer.cjs +53 -18
  5. package/dist/cjs/async-debouncer.cjs.map +1 -1
  6. package/dist/cjs/async-debouncer.d.cts +79 -9
  7. package/dist/cjs/async-queuer.cjs +58 -1
  8. package/dist/cjs/async-queuer.cjs.map +1 -1
  9. package/dist/cjs/async-queuer.d.cts +103 -9
  10. package/dist/cjs/async-rate-limiter.cjs +51 -1
  11. package/dist/cjs/async-rate-limiter.cjs.map +1 -1
  12. package/dist/cjs/async-rate-limiter.d.cts +94 -27
  13. package/dist/cjs/async-retryer.cjs +286 -0
  14. package/dist/cjs/async-retryer.cjs.map +1 -0
  15. package/dist/cjs/async-retryer.d.cts +312 -0
  16. package/dist/cjs/async-throttler.cjs +101 -52
  17. package/dist/cjs/async-throttler.cjs.map +1 -1
  18. package/dist/cjs/async-throttler.d.cts +85 -9
  19. package/dist/cjs/batcher.cjs +5 -0
  20. package/dist/cjs/batcher.cjs.map +1 -1
  21. package/dist/cjs/batcher.d.cts +13 -1
  22. package/dist/cjs/debouncer.cjs +5 -0
  23. package/dist/cjs/debouncer.cjs.map +1 -1
  24. package/dist/cjs/debouncer.d.cts +10 -2
  25. package/dist/cjs/event-client.cjs.map +1 -1
  26. package/dist/cjs/event-client.d.cts +3 -0
  27. package/dist/cjs/index.cjs +13 -0
  28. package/dist/cjs/index.cjs.map +1 -1
  29. package/dist/cjs/index.d.cts +1 -0
  30. package/dist/cjs/queuer.cjs +5 -0
  31. package/dist/cjs/queuer.cjs.map +1 -1
  32. package/dist/cjs/queuer.d.cts +11 -3
  33. package/dist/cjs/rate-limiter.cjs +5 -0
  34. package/dist/cjs/rate-limiter.cjs.map +1 -1
  35. package/dist/cjs/rate-limiter.d.cts +11 -0
  36. package/dist/cjs/throttler.cjs +5 -0
  37. package/dist/cjs/throttler.cjs.map +1 -1
  38. package/dist/cjs/throttler.d.cts +11 -0
  39. package/dist/cjs/utils.cjs.map +1 -1
  40. package/dist/esm/async-batcher.d.ts +97 -16
  41. package/dist/esm/async-batcher.js +67 -5
  42. package/dist/esm/async-batcher.js.map +1 -1
  43. package/dist/esm/async-debouncer.d.ts +79 -9
  44. package/dist/esm/async-debouncer.js +54 -19
  45. package/dist/esm/async-debouncer.js.map +1 -1
  46. package/dist/esm/async-queuer.d.ts +103 -9
  47. package/dist/esm/async-queuer.js +59 -2
  48. package/dist/esm/async-queuer.js.map +1 -1
  49. package/dist/esm/async-rate-limiter.d.ts +94 -27
  50. package/dist/esm/async-rate-limiter.js +52 -2
  51. package/dist/esm/async-rate-limiter.js.map +1 -1
  52. package/dist/esm/async-retryer.d.ts +312 -0
  53. package/dist/esm/async-retryer.js +286 -0
  54. package/dist/esm/async-retryer.js.map +1 -0
  55. package/dist/esm/async-throttler.d.ts +85 -9
  56. package/dist/esm/async-throttler.js +102 -53
  57. package/dist/esm/async-throttler.js.map +1 -1
  58. package/dist/esm/batcher.d.ts +13 -1
  59. package/dist/esm/batcher.js +5 -0
  60. package/dist/esm/batcher.js.map +1 -1
  61. package/dist/esm/debouncer.d.ts +10 -2
  62. package/dist/esm/debouncer.js +6 -1
  63. package/dist/esm/debouncer.js.map +1 -1
  64. package/dist/esm/event-client.d.ts +3 -0
  65. package/dist/esm/event-client.js.map +1 -1
  66. package/dist/esm/index.d.ts +1 -0
  67. package/dist/esm/index.js +23 -10
  68. package/dist/esm/index.js.map +1 -1
  69. package/dist/esm/queuer.d.ts +11 -3
  70. package/dist/esm/queuer.js +6 -1
  71. package/dist/esm/queuer.js.map +1 -1
  72. package/dist/esm/rate-limiter.d.ts +11 -0
  73. package/dist/esm/rate-limiter.js +6 -1
  74. package/dist/esm/rate-limiter.js.map +1 -1
  75. package/dist/esm/throttler.d.ts +11 -0
  76. package/dist/esm/throttler.js +6 -1
  77. package/dist/esm/throttler.js.map +1 -1
  78. package/dist/esm/utils.js.map +1 -1
  79. package/package.json +12 -2
  80. package/src/async-batcher.ts +146 -19
  81. package/src/async-debouncer.ts +120 -30
  82. package/src/async-queuer.ts +149 -11
  83. package/src/async-rate-limiter.ts +131 -30
  84. package/src/async-retryer.ts +669 -0
  85. package/src/async-throttler.ts +198 -72
  86. package/src/batcher.ts +18 -1
  87. package/src/debouncer.ts +19 -2
  88. package/src/event-client.ts +3 -0
  89. package/src/index.ts +2 -0
  90. package/src/queuer.ts +21 -3
  91. package/src/rate-limiter.ts +20 -0
  92. package/src/throttler.ts +20 -0
@@ -1,6 +1,8 @@
1
1
  import { Store } from '@tanstack/store'
2
+ import { AsyncRetryer } from './async-retryer'
2
3
  import { createKey, parseFunctionOrValue } from './utils'
3
4
  import { emitChange, pacerEventClient } from './event-client'
5
+ import type { AsyncRetryerOptions } from './async-retryer'
4
6
  import type { AnyAsyncFunction, OptionalKeys } from './types'
5
7
 
6
8
  export interface AsyncDebouncerState<TFn extends AnyAsyncFunction> {
@@ -67,6 +69,10 @@ function getDefaultAsyncDebouncerState<
67
69
  * Options for configuring an async debounced function
68
70
  */
69
71
  export interface AsyncDebouncerOptions<TFn extends AnyAsyncFunction> {
72
+ /**
73
+ * Options for configuring the underlying async retryer
74
+ */
75
+ asyncRetryerOptions?: AsyncRetryerOptions<TFn>
70
76
  /**
71
77
  * Whether the debouncer is enabled. When disabled, maybeExecute will not trigger any executions.
72
78
  * Can be a boolean or a function that returns a boolean.
@@ -93,7 +99,7 @@ export interface AsyncDebouncerOptions<TFn extends AnyAsyncFunction> {
93
99
  * This can be used alongside throwOnError - the handler will be called before any error is thrown.
94
100
  */
95
101
  onError?: (
96
- error: unknown,
102
+ error: Error,
97
103
  args: Parameters<TFn>,
98
104
  debouncer: AsyncDebouncer<TFn>,
99
105
  ) => void
@@ -128,12 +134,27 @@ export interface AsyncDebouncerOptions<TFn extends AnyAsyncFunction> {
128
134
  wait: number | ((debouncer: AsyncDebouncer<TFn>) => number)
129
135
  }
130
136
 
137
+ /**
138
+ * Utility function for sharing common `AsyncDebouncerOptions` options between different `AsyncDebouncer` instances.
139
+ */
140
+ export function asyncDebouncerOptions<
141
+ TFn extends AnyAsyncFunction = AnyAsyncFunction,
142
+ TOptions extends Partial<AsyncDebouncerOptions<TFn>> = Partial<
143
+ AsyncDebouncerOptions<TFn>
144
+ >,
145
+ >(options: TOptions): TOptions {
146
+ return options
147
+ }
148
+
131
149
  type AsyncDebouncerOptionsWithOptionalCallbacks = OptionalKeys<
132
150
  AsyncDebouncerOptions<any>,
133
151
  'initialState' | 'onError' | 'onSettled' | 'onSuccess' | 'key'
134
152
  >
135
153
 
136
154
  const defaultOptions: AsyncDebouncerOptionsWithOptionalCallbacks = {
155
+ asyncRetryerOptions: {
156
+ maxAttempts: 1,
157
+ },
137
158
  enabled: true,
138
159
  leading: false,
139
160
  trailing: true,
@@ -143,6 +164,19 @@ const defaultOptions: AsyncDebouncerOptionsWithOptionalCallbacks = {
143
164
  /**
144
165
  * A class that creates an async debounced function.
145
166
  *
167
+ * Async vs Sync Versions:
168
+ * The async version provides advanced features over the sync Debouncer:
169
+ * - Returns promises that can be awaited for debounced function results
170
+ * - Built-in retry support via AsyncRetryer integration
171
+ * - Abort support to cancel in-flight executions
172
+ * - Cancel support to prevent pending executions from starting
173
+ * - Comprehensive error handling with onError callbacks and throwOnError control
174
+ * - Detailed execution tracking (success/error/settle counts)
175
+ *
176
+ * The sync Debouncer is lighter weight and simpler when you don't need async features,
177
+ * return values, or execution control.
178
+ *
179
+ * What is Debouncing?
146
180
  * Debouncing ensures that a function is only executed after a specified delay has passed since its last invocation.
147
181
  * Each new invocation resets the delay timer. This is useful for handling frequent events like window resizing
148
182
  * or input changes where you only want to execute the handler after the events have stopped occurring.
@@ -150,10 +184,6 @@ const defaultOptions: AsyncDebouncerOptionsWithOptionalCallbacks = {
150
184
  * Unlike throttling which allows execution at regular intervals, debouncing prevents any execution until
151
185
  * the function stops being called for the specified delay period.
152
186
  *
153
- * Unlike the non-async Debouncer, this async version supports returning values from the debounced function,
154
- * making it ideal for API calls and other async operations where you want the result of the `maybeExecute` call
155
- * instead of setting the result on a state variable from within the debounced function.
156
- *
157
187
  * Error Handling:
158
188
  * - If an `onError` handler is provided, it will be called with the error and debouncer instance
159
189
  * - If `throwOnError` is true (default when no onError handler is provided), the error will be thrown
@@ -191,7 +221,7 @@ export class AsyncDebouncer<TFn extends AnyAsyncFunction> {
191
221
  >(getDefaultAsyncDebouncerState<TFn>())
192
222
  key: string
193
223
  options: AsyncDebouncerOptions<TFn>
194
- #abortController: AbortController | null = null
224
+ asyncRetryers = new Map<number, AsyncRetryer<TFn>>()
195
225
  #timeoutId: NodeJS.Timeout | null = null
196
226
  #resolvePreviousPromise:
197
227
  | ((value?: ReturnType<TFn> | undefined) => void)
@@ -216,6 +246,11 @@ export class AsyncDebouncer<TFn extends AnyAsyncFunction> {
216
246
  })
217
247
  }
218
248
 
249
+ /**
250
+ * Emits a change event for the async debouncer instance. Mostly useful for devtools.
251
+ */
252
+ _emit = () => emitChange('AsyncDebouncer', this)
253
+
219
254
  /**
220
255
  * Updates the async debouncer options
221
256
  */
@@ -326,31 +361,37 @@ export class AsyncDebouncer<TFn extends AnyAsyncFunction> {
326
361
  ...args: Parameters<TFn>
327
362
  ): Promise<ReturnType<TFn> | undefined> => {
328
363
  if (!this.#getEnabled()) return undefined
329
- this.#abortController = new AbortController()
364
+ const currentMaybeExecuteCount = this.store.state.maybeExecuteCount + 1
365
+
330
366
  try {
331
367
  this.#setState({ isExecuting: true })
332
- const result = await this.fn(...args) // EXECUTE!
368
+ const currentAsyncRetryer = new AsyncRetryer(this.fn, {
369
+ ...this.options.asyncRetryerOptions,
370
+ key: `${this.key}-retryer-${currentMaybeExecuteCount}`,
371
+ })
372
+ this.asyncRetryers.set(currentMaybeExecuteCount, currentAsyncRetryer)
373
+ const result = await currentAsyncRetryer.execute(...args) // EXECUTE!
333
374
  this.#setState({
334
375
  lastResult: result,
335
376
  successCount: this.store.state.successCount + 1,
336
377
  })
337
- this.options.onSuccess?.(result, args, this)
378
+ this.options.onSuccess?.(result as ReturnType<TFn>, args, this)
338
379
  } catch (error) {
339
380
  this.#setState({
340
381
  errorCount: this.store.state.errorCount + 1,
341
382
  })
342
- this.options.onError?.(error, args, this)
383
+ this.options.onError?.(error as Error, args, this)
343
384
  if (this.options.throwOnError) {
344
385
  throw error
345
386
  }
346
387
  } finally {
388
+ this.asyncRetryers.delete(currentMaybeExecuteCount) // dispose retryer
347
389
  this.#setState({
348
390
  isExecuting: false,
349
391
  isPending: false,
350
392
  lastArgs: undefined,
351
393
  settleCount: this.store.state.settleCount + 1,
352
394
  })
353
- this.#abortController = null
354
395
  this.options.onSettled?.(args, this)
355
396
  }
356
397
  return this.store.state.lastResult
@@ -361,14 +402,9 @@ export class AsyncDebouncer<TFn extends AnyAsyncFunction> {
361
402
  */
362
403
  flush = async (): Promise<ReturnType<TFn> | undefined> => {
363
404
  if (this.store.state.isPending && this.store.state.lastArgs) {
364
- this.#abortExecution() // abort any current execution
365
- this.#clearTimeout() // clear any existing timeout
366
- const result = await this.#execute(...this.store.state.lastArgs)
367
-
368
- // Resolve any pending promise from maybeExecute
369
- this.#resolvePreviousPromiseInternal()
370
-
371
- return result
405
+ const { lastArgs } = this.store.state
406
+ this.#cancelPendingExecution()
407
+ return await this.#execute(...lastArgs)
372
408
  }
373
409
  return undefined
374
410
  }
@@ -387,29 +423,62 @@ export class AsyncDebouncer<TFn extends AnyAsyncFunction> {
387
423
  }
388
424
  }
389
425
 
426
+ /**
427
+ * Internal cancel without resetting the leading execute state
428
+ */
390
429
  #cancelPendingExecution = (): void => {
391
430
  this.#clearTimeout()
392
431
  this.#resolvePreviousPromiseInternal()
393
432
  this.#setState({
394
433
  isPending: false,
395
- isExecuting: false,
396
434
  lastArgs: undefined,
397
435
  })
398
436
  }
399
437
 
400
- #abortExecution = (): void => {
401
- if (this.#abortController) {
402
- this.#abortController.abort()
403
- this.#abortController = null
404
- }
438
+ /**
439
+ * Returns the AbortSignal for a specific execution.
440
+ * If no maybeExecuteCount is provided, returns the signal for the most recent execution.
441
+ * Returns null if no execution is found or not currently executing.
442
+ *
443
+ * @param maybeExecuteCount - Optional specific execution to get signal for
444
+ * @example
445
+ * ```typescript
446
+ * const debouncer = new AsyncDebouncer(
447
+ * async (searchTerm: string) => {
448
+ * const signal = debouncer.getAbortSignal()
449
+ * if (signal) {
450
+ * const response = await fetch(`/api/search?q=${searchTerm}`, { signal })
451
+ * return response.json()
452
+ * }
453
+ * },
454
+ * { wait: 300 }
455
+ * )
456
+ * ```
457
+ */
458
+ getAbortSignal(maybeExecuteCount?: number): AbortSignal | null {
459
+ const count = maybeExecuteCount ?? this.store.state.maybeExecuteCount
460
+ const retryer = this.asyncRetryers.get(count)
461
+ return retryer?.getAbortSignal() ?? null
462
+ }
463
+
464
+ /**
465
+ * Aborts all ongoing executions with the internal abort controllers.
466
+ * Does NOT cancel any pending execution that have not started yet.
467
+ */
468
+ abort = (): void => {
469
+ this.asyncRetryers.forEach((retryer) => retryer.abort())
470
+ this.asyncRetryers.clear()
471
+ this.#setState({
472
+ isExecuting: false,
473
+ })
405
474
  }
406
475
 
407
476
  /**
408
- * Cancels any pending execution or aborts any execution in progress
477
+ * Cancels any pending execution that have not started yet.
478
+ * Does NOT abort any execution already in progress.
409
479
  */
410
480
  cancel = (): void => {
411
481
  this.#cancelPendingExecution()
412
- this.#abortExecution()
413
482
  this.#setState({ canLeadingExecute: true })
414
483
  }
415
484
 
@@ -418,6 +487,7 @@ export class AsyncDebouncer<TFn extends AnyAsyncFunction> {
418
487
  */
419
488
  reset = (): void => {
420
489
  this.#setState(getDefaultAsyncDebouncerState<TFn>())
490
+ this.asyncRetryers.forEach((retryer) => retryer.reset())
421
491
  }
422
492
  }
423
493
 
@@ -426,9 +496,29 @@ export class AsyncDebouncer<TFn extends AnyAsyncFunction> {
426
496
  * The debounced function will only execute once the wait period has elapsed without any new calls.
427
497
  * If called again during the wait period, the timer resets and a new wait period begins.
428
498
  *
429
- * Unlike the non-async Debouncer, this async version supports returning values from the debounced function,
430
- * making it ideal for API calls and other async operations where you want the result of the `maybeExecute` call
431
- * instead of setting the result on a state variable from within the debounced function.
499
+ * Async vs Sync Versions:
500
+ * The async version provides advanced features over the sync debounce function:
501
+ * - Returns promises that can be awaited for debounced function results
502
+ * - Built-in retry support via AsyncRetryer integration
503
+ * - Abort support to cancel in-flight executions
504
+ * - Cancel support to prevent pending executions from starting
505
+ * - Comprehensive error handling with onError callbacks and throwOnError control
506
+ * - Detailed execution tracking (success/error/settle counts)
507
+ *
508
+ * The sync debounce function is lighter weight and simpler when you don't need async features,
509
+ * return values, or execution control.
510
+ *
511
+ * What is Debouncing?
512
+ * Debouncing ensures that a function is only executed after a specified delay has passed since its last invocation.
513
+ * Each new invocation resets the delay timer. This is useful for handling frequent events like window resizing
514
+ * or input changes where you only want to execute the handler after the events have stopped occurring.
515
+ *
516
+ * Configuration Options:
517
+ * - `wait`: Delay in milliseconds to wait after the last call (required)
518
+ * - `leading`: Execute on the leading edge of the timeout (default: false)
519
+ * - `trailing`: Execute on the trailing edge of the timeout (default: true)
520
+ * - `enabled`: Whether the debouncer is enabled (default: true)
521
+ * - `asyncRetryerOptions`: Configure retry behavior for executions
432
522
  *
433
523
  * Error Handling:
434
524
  * - If an `onError` handler is provided, it will be called with the error and debouncer instance
@@ -1,6 +1,8 @@
1
1
  import { Store } from '@tanstack/store'
2
+ import { AsyncRetryer } from './async-retryer'
2
3
  import { createKey, parseFunctionOrValue } from './utils'
3
4
  import { emitChange, pacerEventClient } from './event-client'
5
+ import type { AsyncRetryerOptions } from './async-retryer'
4
6
  import type { OptionalKeys } from './types'
5
7
  import type { QueuePosition } from './queuer'
6
8
 
@@ -17,6 +19,10 @@ export interface AsyncQueuerState<TValue> {
17
19
  * Number of task executions that have resulted in errors
18
20
  */
19
21
  errorCount: number
22
+ /**
23
+ * Number of times execute has been called
24
+ */
25
+ executeCount: number
20
26
  /**
21
27
  * Number of items that have been removed from the queue due to expiration
22
28
  */
@@ -25,6 +31,10 @@ export interface AsyncQueuerState<TValue> {
25
31
  * Whether the queuer has no items to process (items array is empty)
26
32
  */
27
33
  isEmpty: boolean
34
+ /**
35
+ * Whether the queuer is currently executing
36
+ */
37
+ isExecuting: boolean
28
38
  /**
29
39
  * Whether the queuer has reached its maximum capacity
30
40
  */
@@ -80,8 +90,10 @@ function getDefaultAsyncQueuerState<TValue>(): AsyncQueuerState<TValue> {
80
90
  activeItems: [],
81
91
  addItemCount: 0,
82
92
  errorCount: 0,
93
+ executeCount: 0,
83
94
  expirationCount: 0,
84
95
  isEmpty: true,
96
+ isExecuting: false,
85
97
  isFull: false,
86
98
  isIdle: true,
87
99
  isRunning: true,
@@ -98,6 +110,10 @@ function getDefaultAsyncQueuerState<TValue>(): AsyncQueuerState<TValue> {
98
110
  }
99
111
 
100
112
  export interface AsyncQueuerOptions<TValue> {
113
+ /**
114
+ * Options for configuring the underlying async retryer
115
+ */
116
+ asyncRetryerOptions?: AsyncRetryerOptions<(item: TValue) => Promise<any>>
101
117
  /**
102
118
  * Default position to add items to the queuer
103
119
  * @default 'back'
@@ -152,7 +168,7 @@ export interface AsyncQueuerOptions<TValue> {
152
168
  * If provided, the handler will be called with the error and queuer instance.
153
169
  * This can be used alongside throwOnError - the handler will be called before any error is thrown.
154
170
  */
155
- onError?: (error: unknown, item: TValue, queuer: AsyncQueuer<TValue>) => void
171
+ onError?: (error: Error, item: TValue, queuer: AsyncQueuer<TValue>) => void
156
172
  /**
157
173
  * Callback fired whenever an item expires in the queuer
158
174
  */
@@ -191,6 +207,18 @@ export interface AsyncQueuerOptions<TValue> {
191
207
  wait?: number | ((queuer: AsyncQueuer<TValue>) => number)
192
208
  }
193
209
 
210
+ /**
211
+ * Utility function for sharing common `AsyncQueuerOptions` options between different `AsyncQueuer` instances.
212
+ */
213
+ export function asyncQueuerOptions<
214
+ TValue = any,
215
+ TOptions extends Partial<AsyncQueuerOptions<TValue>> = Partial<
216
+ AsyncQueuerOptions<TValue>
217
+ >,
218
+ >(options: TOptions): TOptions {
219
+ return options
220
+ }
221
+
194
222
  type AsyncQueuerOptionsWithOptionalCallbacks = OptionalKeys<
195
223
  Required<AsyncQueuerOptions<any>>,
196
224
  | 'initialState'
@@ -206,6 +234,9 @@ type AsyncQueuerOptionsWithOptionalCallbacks = OptionalKeys<
206
234
 
207
235
  const defaultOptions: AsyncQueuerOptionsWithOptionalCallbacks = {
208
236
  addItemsTo: 'back',
237
+ asyncRetryerOptions: {
238
+ maxAttempts: 1,
239
+ },
209
240
  concurrency: 1,
210
241
  expirationDuration: Infinity,
211
242
  getIsExpired: () => false,
@@ -220,18 +251,31 @@ const defaultOptions: AsyncQueuerOptionsWithOptionalCallbacks = {
220
251
  /**
221
252
  * A flexible asynchronous queue for processing tasks with configurable concurrency, priority, and expiration.
222
253
  *
223
- * Features:
254
+ * Async vs Sync Versions:
255
+ * The async version provides advanced features over the sync Queuer:
256
+ * - Returns promises that can be awaited for task results
257
+ * - Built-in retry support via AsyncRetryer integration for each queued task
258
+ * - Abort support to cancel in-flight task executions
259
+ * - Comprehensive error handling with onError callbacks and throwOnError control
260
+ * - Detailed execution tracking (success/error/settle counts)
261
+ * - Concurrent execution support (process multiple items simultaneously)
262
+ *
263
+ * The sync Queuer is lighter weight and simpler when you don't need async features,
264
+ * return values, or execution control.
265
+ *
266
+ * What is Queuing?
267
+ * Queuing is a technique for managing and processing items sequentially or with controlled concurrency.
268
+ * Tasks are processed up to the configured concurrency limit. When a task completes,
269
+ * the next pending task is processed if the concurrency limit allows.
270
+ *
271
+ * Key Features:
224
272
  * - Priority queue support via the getPriority option
225
273
  * - Configurable concurrency limit
226
274
  * - Callbacks for task success, error, completion, and queue state changes
227
275
  * - FIFO (First In First Out) or LIFO (Last In First Out) queue behavior
228
276
  * - Pause and resume processing
229
- * - Task cancellation
230
277
  * - Item expiration to remove stale items from the queue
231
278
  *
232
- * Tasks are processed concurrently up to the configured concurrency limit. When a task completes,
233
- * the next pending task is processed if the concurrency limit allows.
234
- *
235
279
  * Error Handling:
236
280
  * - If an `onError` handler is provided, it will be called with the error and queuer instance
237
281
  * - If `throwOnError` is true (default when no onError handler is provided), the error will be thrown
@@ -274,6 +318,10 @@ export class AsyncQueuer<TValue> {
274
318
  >(getDefaultAsyncQueuerState<TValue>())
275
319
  key: string
276
320
  options: AsyncQueuerOptions<TValue>
321
+ asyncRetryers = new Map<
322
+ number,
323
+ AsyncRetryer<(item: TValue) => Promise<any>>
324
+ >()
277
325
  #timeoutIds: Set<NodeJS.Timeout> = new Set()
278
326
 
279
327
  constructor(
@@ -312,6 +360,11 @@ export class AsyncQueuer<TValue> {
312
360
  })
313
361
  }
314
362
 
363
+ /**
364
+ * Emits a change event for the async queuer instance. Mostly useful for devtools.
365
+ */
366
+ _emit = () => emitChange('AsyncQueuer', this)
367
+
315
368
  /**
316
369
  * Updates the queuer options. New options are merged with existing options.
317
370
  */
@@ -555,9 +608,20 @@ export class AsyncQueuer<TValue> {
555
608
  */
556
609
  execute = async (position?: QueuePosition): Promise<any> => {
557
610
  const item = this.getNextItem(position)
611
+
558
612
  if (item !== undefined) {
613
+ const currentExecuteCount = this.store.state.executeCount + 1
614
+ this.#setState({
615
+ executeCount: currentExecuteCount,
616
+ isExecuting: true,
617
+ })
559
618
  try {
560
- const lastResult = await this.fn(item) // EXECUTE!
619
+ const currentAsyncRetryer = new AsyncRetryer(this.fn, {
620
+ ...this.options.asyncRetryerOptions,
621
+ key: `${this.key}-retryer-${currentExecuteCount}`,
622
+ })
623
+ this.asyncRetryers.set(currentExecuteCount, currentAsyncRetryer)
624
+ const lastResult = await currentAsyncRetryer.execute(item) // EXECUTE!
561
625
  this.#setState({
562
626
  successCount: this.store.state.successCount + 1,
563
627
  lastResult,
@@ -567,15 +631,17 @@ export class AsyncQueuer<TValue> {
567
631
  this.#setState({
568
632
  errorCount: this.store.state.errorCount + 1,
569
633
  })
570
- this.options.onError?.(error, item, this)
634
+ this.options.onError?.(error as Error, item, this)
571
635
  if (this.options.throwOnError) {
572
636
  throw error
573
637
  }
574
638
  } finally {
639
+ this.asyncRetryers.delete(currentExecuteCount) // dispose retryer
575
640
  this.#setState({
576
641
  activeItems: this.store.state.activeItems.filter(
577
642
  (activeItem) => activeItem !== item,
578
643
  ),
644
+ isExecuting: false,
579
645
  settledCount: this.store.state.settledCount + 1,
580
646
  })
581
647
  this.options.onSettled?.(item, this)
@@ -729,19 +795,59 @@ export class AsyncQueuer<TValue> {
729
795
  }
730
796
 
731
797
  /**
732
- * Removes all pending items from the queue. Does not affect active tasks.
798
+ * Removes all pending items from the queue.
799
+ * Does NOT affect active tasks.
733
800
  */
734
801
  clear = (): void => {
735
802
  this.#setState({ items: [], itemTimestamps: [] })
736
803
  this.options.onItemsChange?.(this)
737
804
  }
738
805
 
806
+ /**
807
+ * Returns the AbortSignal for a specific execution.
808
+ * If no executeCount is provided, returns the signal for the most recent execution.
809
+ * Returns null if no execution is found or not currently executing.
810
+ *
811
+ * @param executeCount - Optional specific execution to get signal for
812
+ * @example
813
+ * ```typescript
814
+ * const queuer = new AsyncQueuer(
815
+ * async (item: string) => {
816
+ * const signal = queuer.getAbortSignal()
817
+ * if (signal) {
818
+ * const response = await fetch(`/api/process/${item}`, { signal })
819
+ * return response.json()
820
+ * }
821
+ * },
822
+ * { concurrency: 2 }
823
+ * )
824
+ * ```
825
+ */
826
+ getAbortSignal(executeCount?: number): AbortSignal | null {
827
+ const count = executeCount ?? this.store.state.executeCount
828
+ const retryer = this.asyncRetryers.get(count)
829
+ return retryer?.getAbortSignal() ?? null
830
+ }
831
+
832
+ /**
833
+ * Aborts all ongoing executions with the internal abort controllers.
834
+ * Does NOT clear out the items.
835
+ */
836
+ abort = (): void => {
837
+ this.asyncRetryers.forEach((retryer) => retryer.abort())
838
+ this.asyncRetryers.clear()
839
+ this.#setState({
840
+ isExecuting: false,
841
+ })
842
+ }
843
+
739
844
  /**
740
845
  * Resets the queuer state to its default values
741
846
  */
742
847
  reset = (): void => {
743
848
  this.#setState(getDefaultAsyncQueuerState<TValue>())
744
849
  this.options.onItemsChange?.(this)
850
+ this.asyncRetryers.forEach((retryer) => retryer.reset())
745
851
  }
746
852
  }
747
853
 
@@ -749,6 +855,34 @@ export class AsyncQueuer<TValue> {
749
855
  * Creates a new AsyncQueuer instance and returns a bound addItem function for adding tasks.
750
856
  * The queuer is started automatically and ready to process items.
751
857
  *
858
+ * Async vs Sync Versions:
859
+ * The async version provides advanced features over the sync queue function:
860
+ * - Returns promises that can be awaited for task results
861
+ * - Built-in retry support via AsyncRetryer integration for each queued task
862
+ * - Abort support to cancel in-flight task executions
863
+ * - Comprehensive error handling with onError callbacks and throwOnError control
864
+ * - Detailed execution tracking (success/error/settle counts)
865
+ * - Concurrent execution support (process multiple items simultaneously)
866
+ *
867
+ * The sync queue function is lighter weight and simpler when you don't need async features,
868
+ * return values, or execution control.
869
+ *
870
+ * What is Queuing?
871
+ * Queuing is a technique for managing and processing items sequentially or with controlled concurrency.
872
+ * Tasks are processed up to the configured concurrency limit. When a task completes,
873
+ * the next pending task is processed if the concurrency limit allows.
874
+ *
875
+ * Configuration Options:
876
+ * - `concurrency`: Maximum number of concurrent tasks (default: 1)
877
+ * - `wait`: Time to wait between processing items (default: 0)
878
+ * - `maxSize`: Maximum number of items allowed in the queue (default: Infinity)
879
+ * - `getPriority`: Function to determine item priority
880
+ * - `addItemsTo`: Default position to add items ('back' or 'front', default: 'back')
881
+ * - `getItemsFrom`: Default position to get items ('front' or 'back', default: 'front')
882
+ * - `expirationDuration`: Maximum time items can stay in queue
883
+ * - `started`: Whether to start processing immediately (default: true)
884
+ * - `asyncRetryerOptions`: Configure retry behavior for task executions
885
+ *
752
886
  * Error Handling:
753
887
  * - If an `onError` handler is provided, it will be called with the error and queuer instance
754
888
  * - If `throwOnError` is true (default when no onError handler is provided), the error will be thrown
@@ -769,11 +903,15 @@ export class AsyncQueuer<TValue> {
769
903
  * - State can be accessed via the underlying AsyncQueuer instance's `store.state` property
770
904
  * - When using framework adapters (React/Solid), state is accessed from the hook's state property
771
905
  *
772
- * Example usage:
906
+ * @example
773
907
  * ```ts
774
908
  * const enqueue = asyncQueue<string>(async (item) => {
775
909
  * return item.toUpperCase();
776
- * }, {...options});
910
+ * }, {
911
+ * concurrency: 2,
912
+ * wait: 100,
913
+ * onSuccess: (result) => console.log('Processed:', result)
914
+ * });
777
915
  *
778
916
  * enqueue('hello');
779
917
  * ```