@tanstack/pacer 0.15.4 → 0.16.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.
Files changed (99) hide show
  1. package/dist/cjs/async-batcher.cjs +66 -5
  2. package/dist/cjs/async-batcher.cjs.map +1 -1
  3. package/dist/cjs/async-batcher.d.cts +94 -17
  4. package/dist/cjs/async-debouncer.cjs +53 -19
  5. package/dist/cjs/async-debouncer.cjs.map +1 -1
  6. package/dist/cjs/async-debouncer.d.cts +76 -10
  7. package/dist/cjs/async-queuer.cjs +58 -2
  8. package/dist/cjs/async-queuer.cjs.map +1 -1
  9. package/dist/cjs/async-queuer.d.cts +100 -10
  10. package/dist/cjs/async-rate-limiter.cjs +51 -2
  11. package/dist/cjs/async-rate-limiter.cjs.map +1 -1
  12. package/dist/cjs/async-rate-limiter.d.cts +91 -28
  13. package/dist/cjs/async-retryer.cjs +285 -0
  14. package/dist/cjs/async-retryer.cjs.map +1 -0
  15. package/dist/cjs/async-retryer.d.cts +308 -0
  16. package/dist/cjs/async-throttler.cjs +101 -53
  17. package/dist/cjs/async-throttler.cjs.map +1 -1
  18. package/dist/cjs/async-throttler.d.cts +82 -10
  19. package/dist/cjs/batcher.cjs +5 -1
  20. package/dist/cjs/batcher.cjs.map +1 -1
  21. package/dist/cjs/batcher.d.cts +10 -2
  22. package/dist/cjs/debouncer.cjs +5 -1
  23. package/dist/cjs/debouncer.cjs.map +1 -1
  24. package/dist/cjs/debouncer.d.cts +7 -3
  25. package/dist/cjs/event-client.cjs +3 -1
  26. package/dist/cjs/event-client.cjs.map +1 -1
  27. package/dist/cjs/event-client.d.cts +3 -0
  28. package/dist/cjs/index.cjs +13 -1
  29. package/dist/cjs/index.cjs.map +1 -1
  30. package/dist/cjs/index.d.cts +1 -0
  31. package/dist/cjs/queuer.cjs +5 -1
  32. package/dist/cjs/queuer.cjs.map +1 -1
  33. package/dist/cjs/queuer.d.cts +8 -4
  34. package/dist/cjs/rate-limiter.cjs +5 -1
  35. package/dist/cjs/rate-limiter.cjs.map +1 -1
  36. package/dist/cjs/rate-limiter.d.cts +8 -1
  37. package/dist/cjs/throttler.cjs +5 -1
  38. package/dist/cjs/throttler.cjs.map +1 -1
  39. package/dist/cjs/throttler.d.cts +7 -0
  40. package/dist/cjs/utils.cjs +0 -10
  41. package/dist/cjs/utils.cjs.map +1 -1
  42. package/dist/cjs/utils.d.cts +0 -1
  43. package/dist/esm/async-batcher.d.ts +94 -17
  44. package/dist/esm/async-batcher.js +68 -7
  45. package/dist/esm/async-batcher.js.map +1 -1
  46. package/dist/esm/async-debouncer.d.ts +76 -10
  47. package/dist/esm/async-debouncer.js +55 -21
  48. package/dist/esm/async-debouncer.js.map +1 -1
  49. package/dist/esm/async-queuer.d.ts +100 -10
  50. package/dist/esm/async-queuer.js +60 -4
  51. package/dist/esm/async-queuer.js.map +1 -1
  52. package/dist/esm/async-rate-limiter.d.ts +91 -28
  53. package/dist/esm/async-rate-limiter.js +53 -4
  54. package/dist/esm/async-rate-limiter.js.map +1 -1
  55. package/dist/esm/async-retryer.d.ts +308 -0
  56. package/dist/esm/async-retryer.js +285 -0
  57. package/dist/esm/async-retryer.js.map +1 -0
  58. package/dist/esm/async-throttler.d.ts +82 -10
  59. package/dist/esm/async-throttler.js +103 -55
  60. package/dist/esm/async-throttler.js.map +1 -1
  61. package/dist/esm/batcher.d.ts +10 -2
  62. package/dist/esm/batcher.js +6 -2
  63. package/dist/esm/batcher.js.map +1 -1
  64. package/dist/esm/debouncer.d.ts +7 -3
  65. package/dist/esm/debouncer.js +7 -3
  66. package/dist/esm/debouncer.js.map +1 -1
  67. package/dist/esm/event-client.d.ts +3 -0
  68. package/dist/esm/event-client.js +3 -1
  69. package/dist/esm/event-client.js.map +1 -1
  70. package/dist/esm/index.d.ts +1 -0
  71. package/dist/esm/index.js +24 -12
  72. package/dist/esm/index.js.map +1 -1
  73. package/dist/esm/queuer.d.ts +8 -4
  74. package/dist/esm/queuer.js +7 -3
  75. package/dist/esm/queuer.js.map +1 -1
  76. package/dist/esm/rate-limiter.d.ts +8 -1
  77. package/dist/esm/rate-limiter.js +7 -3
  78. package/dist/esm/rate-limiter.js.map +1 -1
  79. package/dist/esm/throttler.d.ts +7 -0
  80. package/dist/esm/throttler.js +7 -3
  81. package/dist/esm/throttler.js.map +1 -1
  82. package/dist/esm/utils.d.ts +0 -1
  83. package/dist/esm/utils.js +0 -10
  84. package/dist/esm/utils.js.map +1 -1
  85. package/package.json +13 -3
  86. package/src/async-batcher.ts +144 -22
  87. package/src/async-debouncer.ts +118 -33
  88. package/src/async-queuer.ts +147 -14
  89. package/src/async-rate-limiter.ts +129 -33
  90. package/src/async-retryer.ts +664 -0
  91. package/src/async-throttler.ts +196 -75
  92. package/src/batcher.ts +16 -4
  93. package/src/debouncer.ts +17 -5
  94. package/src/event-client.ts +6 -1
  95. package/src/index.ts +2 -0
  96. package/src/queuer.ts +19 -6
  97. package/src/rate-limiter.ts +18 -3
  98. package/src/throttler.ts +17 -2
  99. package/src/utils.ts +0 -15
@@ -1,6 +1,8 @@
1
1
  import { Store } from '@tanstack/store'
2
- import { createKey, parseFunctionOrValue } from './utils'
2
+ import { AsyncRetryer } from './async-retryer'
3
+ import { 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
@@ -189,9 +219,9 @@ export class AsyncDebouncer<TFn extends AnyAsyncFunction> {
189
219
  readonly store: Store<Readonly<AsyncDebouncerState<TFn>>> = new Store<
190
220
  AsyncDebouncerState<TFn>
191
221
  >(getDefaultAsyncDebouncerState<TFn>())
192
- key: string
222
+ key: string | undefined
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)
@@ -201,7 +231,7 @@ export class AsyncDebouncer<TFn extends AnyAsyncFunction> {
201
231
  public fn: TFn,
202
232
  initialOptions: AsyncDebouncerOptions<TFn>,
203
233
  ) {
204
- this.key = createKey(initialOptions.key)
234
+ this.key = initialOptions.key
205
235
  this.options = {
206
236
  ...defaultOptions,
207
237
  ...initialOptions,
@@ -326,31 +356,37 @@ export class AsyncDebouncer<TFn extends AnyAsyncFunction> {
326
356
  ...args: Parameters<TFn>
327
357
  ): Promise<ReturnType<TFn> | undefined> => {
328
358
  if (!this.#getEnabled()) return undefined
329
- this.#abortController = new AbortController()
359
+ const currentMaybeExecuteCount = this.store.state.maybeExecuteCount + 1
360
+
330
361
  try {
331
362
  this.#setState({ isExecuting: true })
332
- const result = await this.fn(...args) // EXECUTE!
363
+ const currentAsyncRetryer = new AsyncRetryer(this.fn, {
364
+ ...this.options.asyncRetryerOptions,
365
+ key: `${this.key}-retryer-${currentMaybeExecuteCount}`,
366
+ })
367
+ this.asyncRetryers.set(currentMaybeExecuteCount, currentAsyncRetryer)
368
+ const result = await currentAsyncRetryer.execute(...args) // EXECUTE!
333
369
  this.#setState({
334
370
  lastResult: result,
335
371
  successCount: this.store.state.successCount + 1,
336
372
  })
337
- this.options.onSuccess?.(result, args, this)
373
+ this.options.onSuccess?.(result as ReturnType<TFn>, args, this)
338
374
  } catch (error) {
339
375
  this.#setState({
340
376
  errorCount: this.store.state.errorCount + 1,
341
377
  })
342
- this.options.onError?.(error, args, this)
378
+ this.options.onError?.(error as Error, args, this)
343
379
  if (this.options.throwOnError) {
344
380
  throw error
345
381
  }
346
382
  } finally {
383
+ this.asyncRetryers.delete(currentMaybeExecuteCount) // dispose retryer
347
384
  this.#setState({
348
385
  isExecuting: false,
349
386
  isPending: false,
350
387
  lastArgs: undefined,
351
388
  settleCount: this.store.state.settleCount + 1,
352
389
  })
353
- this.#abortController = null
354
390
  this.options.onSettled?.(args, this)
355
391
  }
356
392
  return this.store.state.lastResult
@@ -361,14 +397,9 @@ export class AsyncDebouncer<TFn extends AnyAsyncFunction> {
361
397
  */
362
398
  flush = async (): Promise<ReturnType<TFn> | undefined> => {
363
399
  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
400
+ const { lastArgs } = this.store.state
401
+ this.#cancelPendingExecution()
402
+ return await this.#execute(...lastArgs)
372
403
  }
373
404
  return undefined
374
405
  }
@@ -387,29 +418,62 @@ export class AsyncDebouncer<TFn extends AnyAsyncFunction> {
387
418
  }
388
419
  }
389
420
 
421
+ /**
422
+ * Internal cancel without resetting the leading execute state
423
+ */
390
424
  #cancelPendingExecution = (): void => {
391
425
  this.#clearTimeout()
392
426
  this.#resolvePreviousPromiseInternal()
393
427
  this.#setState({
394
428
  isPending: false,
395
- isExecuting: false,
396
429
  lastArgs: undefined,
397
430
  })
398
431
  }
399
432
 
400
- #abortExecution = (): void => {
401
- if (this.#abortController) {
402
- this.#abortController.abort()
403
- this.#abortController = null
404
- }
433
+ /**
434
+ * Returns the AbortSignal for a specific execution.
435
+ * If no maybeExecuteCount is provided, returns the signal for the most recent execution.
436
+ * Returns null if no execution is found or not currently executing.
437
+ *
438
+ * @param maybeExecuteCount - Optional specific execution to get signal for
439
+ * @example
440
+ * ```typescript
441
+ * const debouncer = new AsyncDebouncer(
442
+ * async (searchTerm: string) => {
443
+ * const signal = debouncer.getAbortSignal()
444
+ * if (signal) {
445
+ * const response = await fetch(`/api/search?q=${searchTerm}`, { signal })
446
+ * return response.json()
447
+ * }
448
+ * },
449
+ * { wait: 300 }
450
+ * )
451
+ * ```
452
+ */
453
+ getAbortSignal(maybeExecuteCount?: number): AbortSignal | null {
454
+ const count = maybeExecuteCount ?? this.store.state.maybeExecuteCount
455
+ const retryer = this.asyncRetryers.get(count)
456
+ return retryer?.getAbortSignal() ?? null
457
+ }
458
+
459
+ /**
460
+ * Aborts all ongoing executions with the internal abort controllers.
461
+ * Does NOT cancel any pending execution that have not started yet.
462
+ */
463
+ abort = (): void => {
464
+ this.asyncRetryers.forEach((retryer) => retryer.abort())
465
+ this.asyncRetryers.clear()
466
+ this.#setState({
467
+ isExecuting: false,
468
+ })
405
469
  }
406
470
 
407
471
  /**
408
- * Cancels any pending execution or aborts any execution in progress
472
+ * Cancels any pending execution that have not started yet.
473
+ * Does NOT abort any execution already in progress.
409
474
  */
410
475
  cancel = (): void => {
411
476
  this.#cancelPendingExecution()
412
- this.#abortExecution()
413
477
  this.#setState({ canLeadingExecute: true })
414
478
  }
415
479
 
@@ -418,6 +482,7 @@ export class AsyncDebouncer<TFn extends AnyAsyncFunction> {
418
482
  */
419
483
  reset = (): void => {
420
484
  this.#setState(getDefaultAsyncDebouncerState<TFn>())
485
+ this.asyncRetryers.forEach((retryer) => retryer.reset())
421
486
  }
422
487
  }
423
488
 
@@ -426,9 +491,29 @@ export class AsyncDebouncer<TFn extends AnyAsyncFunction> {
426
491
  * The debounced function will only execute once the wait period has elapsed without any new calls.
427
492
  * If called again during the wait period, the timer resets and a new wait period begins.
428
493
  *
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.
494
+ * Async vs Sync Versions:
495
+ * The async version provides advanced features over the sync debounce function:
496
+ * - Returns promises that can be awaited for debounced function results
497
+ * - Built-in retry support via AsyncRetryer integration
498
+ * - Abort support to cancel in-flight executions
499
+ * - Cancel support to prevent pending executions from starting
500
+ * - Comprehensive error handling with onError callbacks and throwOnError control
501
+ * - Detailed execution tracking (success/error/settle counts)
502
+ *
503
+ * The sync debounce function is lighter weight and simpler when you don't need async features,
504
+ * return values, or execution control.
505
+ *
506
+ * What is Debouncing?
507
+ * Debouncing ensures that a function is only executed after a specified delay has passed since its last invocation.
508
+ * Each new invocation resets the delay timer. This is useful for handling frequent events like window resizing
509
+ * or input changes where you only want to execute the handler after the events have stopped occurring.
510
+ *
511
+ * Configuration Options:
512
+ * - `wait`: Delay in milliseconds to wait after the last call (required)
513
+ * - `leading`: Execute on the leading edge of the timeout (default: false)
514
+ * - `trailing`: Execute on the trailing edge of the timeout (default: true)
515
+ * - `enabled`: Whether the debouncer is enabled (default: true)
516
+ * - `asyncRetryerOptions`: Configure retry behavior for executions
432
517
  *
433
518
  * Error Handling:
434
519
  * - 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 { createKey, parseFunctionOrValue } from './utils'
2
+ import { AsyncRetryer } from './async-retryer'
3
+ import { 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
@@ -272,15 +316,19 @@ export class AsyncQueuer<TValue> {
272
316
  readonly store: Store<Readonly<AsyncQueuerState<TValue>>> = new Store<
273
317
  AsyncQueuerState<TValue>
274
318
  >(getDefaultAsyncQueuerState<TValue>())
275
- key: string
319
+ key: string | undefined
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(
280
328
  public fn: (item: TValue) => Promise<any>,
281
329
  initialOptions: AsyncQueuerOptions<TValue> = {},
282
330
  ) {
283
- this.key = createKey(initialOptions.key)
331
+ this.key = initialOptions.key
284
332
  this.options = {
285
333
  ...defaultOptions,
286
334
  ...initialOptions,
@@ -555,9 +603,20 @@ export class AsyncQueuer<TValue> {
555
603
  */
556
604
  execute = async (position?: QueuePosition): Promise<any> => {
557
605
  const item = this.getNextItem(position)
606
+
558
607
  if (item !== undefined) {
608
+ const currentExecuteCount = this.store.state.executeCount + 1
609
+ this.#setState({
610
+ executeCount: currentExecuteCount,
611
+ isExecuting: true,
612
+ })
559
613
  try {
560
- const lastResult = await this.fn(item) // EXECUTE!
614
+ const currentAsyncRetryer = new AsyncRetryer(this.fn, {
615
+ ...this.options.asyncRetryerOptions,
616
+ key: `${this.key}-retryer-${currentExecuteCount}`,
617
+ })
618
+ this.asyncRetryers.set(currentExecuteCount, currentAsyncRetryer)
619
+ const lastResult = await currentAsyncRetryer.execute(item) // EXECUTE!
561
620
  this.#setState({
562
621
  successCount: this.store.state.successCount + 1,
563
622
  lastResult,
@@ -567,15 +626,17 @@ export class AsyncQueuer<TValue> {
567
626
  this.#setState({
568
627
  errorCount: this.store.state.errorCount + 1,
569
628
  })
570
- this.options.onError?.(error, item, this)
629
+ this.options.onError?.(error as Error, item, this)
571
630
  if (this.options.throwOnError) {
572
631
  throw error
573
632
  }
574
633
  } finally {
634
+ this.asyncRetryers.delete(currentExecuteCount) // dispose retryer
575
635
  this.#setState({
576
636
  activeItems: this.store.state.activeItems.filter(
577
637
  (activeItem) => activeItem !== item,
578
638
  ),
639
+ isExecuting: false,
579
640
  settledCount: this.store.state.settledCount + 1,
580
641
  })
581
642
  this.options.onSettled?.(item, this)
@@ -729,19 +790,59 @@ export class AsyncQueuer<TValue> {
729
790
  }
730
791
 
731
792
  /**
732
- * Removes all pending items from the queue. Does not affect active tasks.
793
+ * Removes all pending items from the queue.
794
+ * Does NOT affect active tasks.
733
795
  */
734
796
  clear = (): void => {
735
797
  this.#setState({ items: [], itemTimestamps: [] })
736
798
  this.options.onItemsChange?.(this)
737
799
  }
738
800
 
801
+ /**
802
+ * Returns the AbortSignal for a specific execution.
803
+ * If no executeCount is provided, returns the signal for the most recent execution.
804
+ * Returns null if no execution is found or not currently executing.
805
+ *
806
+ * @param executeCount - Optional specific execution to get signal for
807
+ * @example
808
+ * ```typescript
809
+ * const queuer = new AsyncQueuer(
810
+ * async (item: string) => {
811
+ * const signal = queuer.getAbortSignal()
812
+ * if (signal) {
813
+ * const response = await fetch(`/api/process/${item}`, { signal })
814
+ * return response.json()
815
+ * }
816
+ * },
817
+ * { concurrency: 2 }
818
+ * )
819
+ * ```
820
+ */
821
+ getAbortSignal(executeCount?: number): AbortSignal | null {
822
+ const count = executeCount ?? this.store.state.executeCount
823
+ const retryer = this.asyncRetryers.get(count)
824
+ return retryer?.getAbortSignal() ?? null
825
+ }
826
+
827
+ /**
828
+ * Aborts all ongoing executions with the internal abort controllers.
829
+ * Does NOT clear out the items.
830
+ */
831
+ abort = (): void => {
832
+ this.asyncRetryers.forEach((retryer) => retryer.abort())
833
+ this.asyncRetryers.clear()
834
+ this.#setState({
835
+ isExecuting: false,
836
+ })
837
+ }
838
+
739
839
  /**
740
840
  * Resets the queuer state to its default values
741
841
  */
742
842
  reset = (): void => {
743
843
  this.#setState(getDefaultAsyncQueuerState<TValue>())
744
844
  this.options.onItemsChange?.(this)
845
+ this.asyncRetryers.forEach((retryer) => retryer.reset())
745
846
  }
746
847
  }
747
848
 
@@ -749,6 +850,34 @@ export class AsyncQueuer<TValue> {
749
850
  * Creates a new AsyncQueuer instance and returns a bound addItem function for adding tasks.
750
851
  * The queuer is started automatically and ready to process items.
751
852
  *
853
+ * Async vs Sync Versions:
854
+ * The async version provides advanced features over the sync queue function:
855
+ * - Returns promises that can be awaited for task results
856
+ * - Built-in retry support via AsyncRetryer integration for each queued task
857
+ * - Abort support to cancel in-flight task executions
858
+ * - Comprehensive error handling with onError callbacks and throwOnError control
859
+ * - Detailed execution tracking (success/error/settle counts)
860
+ * - Concurrent execution support (process multiple items simultaneously)
861
+ *
862
+ * The sync queue function is lighter weight and simpler when you don't need async features,
863
+ * return values, or execution control.
864
+ *
865
+ * What is Queuing?
866
+ * Queuing is a technique for managing and processing items sequentially or with controlled concurrency.
867
+ * Tasks are processed up to the configured concurrency limit. When a task completes,
868
+ * the next pending task is processed if the concurrency limit allows.
869
+ *
870
+ * Configuration Options:
871
+ * - `concurrency`: Maximum number of concurrent tasks (default: 1)
872
+ * - `wait`: Time to wait between processing items (default: 0)
873
+ * - `maxSize`: Maximum number of items allowed in the queue (default: Infinity)
874
+ * - `getPriority`: Function to determine item priority
875
+ * - `addItemsTo`: Default position to add items ('back' or 'front', default: 'back')
876
+ * - `getItemsFrom`: Default position to get items ('front' or 'back', default: 'front')
877
+ * - `expirationDuration`: Maximum time items can stay in queue
878
+ * - `started`: Whether to start processing immediately (default: true)
879
+ * - `asyncRetryerOptions`: Configure retry behavior for task executions
880
+ *
752
881
  * Error Handling:
753
882
  * - If an `onError` handler is provided, it will be called with the error and queuer instance
754
883
  * - If `throwOnError` is true (default when no onError handler is provided), the error will be thrown
@@ -769,11 +898,15 @@ export class AsyncQueuer<TValue> {
769
898
  * - State can be accessed via the underlying AsyncQueuer instance's `store.state` property
770
899
  * - When using framework adapters (React/Solid), state is accessed from the hook's state property
771
900
  *
772
- * Example usage:
901
+ * @example
773
902
  * ```ts
774
903
  * const enqueue = asyncQueue<string>(async (item) => {
775
904
  * return item.toUpperCase();
776
- * }, {...options});
905
+ * }, {
906
+ * concurrency: 2,
907
+ * wait: 100,
908
+ * onSuccess: (result) => console.log('Processed:', result)
909
+ * });
777
910
  *
778
911
  * enqueue('hello');
779
912
  * ```