@tanstack/pacer 0.15.3 → 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.
- package/dist/cjs/async-batcher.cjs +66 -4
- package/dist/cjs/async-batcher.cjs.map +1 -1
- package/dist/cjs/async-batcher.d.cts +97 -16
- package/dist/cjs/async-debouncer.cjs +53 -18
- package/dist/cjs/async-debouncer.cjs.map +1 -1
- package/dist/cjs/async-debouncer.d.cts +79 -9
- package/dist/cjs/async-queuer.cjs +58 -1
- package/dist/cjs/async-queuer.cjs.map +1 -1
- package/dist/cjs/async-queuer.d.cts +103 -9
- package/dist/cjs/async-rate-limiter.cjs +51 -1
- package/dist/cjs/async-rate-limiter.cjs.map +1 -1
- package/dist/cjs/async-rate-limiter.d.cts +94 -27
- package/dist/cjs/async-retryer.cjs +286 -0
- package/dist/cjs/async-retryer.cjs.map +1 -0
- package/dist/cjs/async-retryer.d.cts +312 -0
- package/dist/cjs/async-throttler.cjs +101 -52
- package/dist/cjs/async-throttler.cjs.map +1 -1
- package/dist/cjs/async-throttler.d.cts +85 -9
- package/dist/cjs/batcher.cjs +5 -0
- package/dist/cjs/batcher.cjs.map +1 -1
- package/dist/cjs/batcher.d.cts +13 -1
- package/dist/cjs/debouncer.cjs +5 -0
- package/dist/cjs/debouncer.cjs.map +1 -1
- package/dist/cjs/debouncer.d.cts +10 -2
- package/dist/cjs/event-client.cjs.map +1 -1
- package/dist/cjs/event-client.d.cts +3 -0
- package/dist/cjs/index.cjs +13 -0
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/index.d.cts +1 -0
- package/dist/cjs/queuer.cjs +5 -0
- package/dist/cjs/queuer.cjs.map +1 -1
- package/dist/cjs/queuer.d.cts +11 -3
- package/dist/cjs/rate-limiter.cjs +5 -0
- package/dist/cjs/rate-limiter.cjs.map +1 -1
- package/dist/cjs/rate-limiter.d.cts +11 -0
- package/dist/cjs/throttler.cjs +5 -0
- package/dist/cjs/throttler.cjs.map +1 -1
- package/dist/cjs/throttler.d.cts +11 -0
- package/dist/cjs/utils.cjs.map +1 -1
- package/dist/esm/async-batcher.d.ts +97 -16
- package/dist/esm/async-batcher.js +67 -5
- package/dist/esm/async-batcher.js.map +1 -1
- package/dist/esm/async-debouncer.d.ts +79 -9
- package/dist/esm/async-debouncer.js +54 -19
- package/dist/esm/async-debouncer.js.map +1 -1
- package/dist/esm/async-queuer.d.ts +103 -9
- package/dist/esm/async-queuer.js +59 -2
- package/dist/esm/async-queuer.js.map +1 -1
- package/dist/esm/async-rate-limiter.d.ts +94 -27
- package/dist/esm/async-rate-limiter.js +52 -2
- package/dist/esm/async-rate-limiter.js.map +1 -1
- package/dist/esm/async-retryer.d.ts +312 -0
- package/dist/esm/async-retryer.js +286 -0
- package/dist/esm/async-retryer.js.map +1 -0
- package/dist/esm/async-throttler.d.ts +85 -9
- package/dist/esm/async-throttler.js +102 -53
- package/dist/esm/async-throttler.js.map +1 -1
- package/dist/esm/batcher.d.ts +13 -1
- package/dist/esm/batcher.js +5 -0
- package/dist/esm/batcher.js.map +1 -1
- package/dist/esm/debouncer.d.ts +10 -2
- package/dist/esm/debouncer.js +6 -1
- package/dist/esm/debouncer.js.map +1 -1
- package/dist/esm/event-client.d.ts +3 -0
- package/dist/esm/event-client.js.map +1 -1
- package/dist/esm/index.d.ts +1 -0
- package/dist/esm/index.js +23 -10
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/queuer.d.ts +11 -3
- package/dist/esm/queuer.js +6 -1
- package/dist/esm/queuer.js.map +1 -1
- package/dist/esm/rate-limiter.d.ts +11 -0
- package/dist/esm/rate-limiter.js +6 -1
- package/dist/esm/rate-limiter.js.map +1 -1
- package/dist/esm/throttler.d.ts +11 -0
- package/dist/esm/throttler.js +6 -1
- package/dist/esm/throttler.js.map +1 -1
- package/dist/esm/utils.js.map +1 -1
- package/package.json +13 -3
- package/src/async-batcher.ts +146 -19
- package/src/async-debouncer.ts +120 -30
- package/src/async-queuer.ts +149 -11
- package/src/async-rate-limiter.ts +131 -30
- package/src/async-retryer.ts +669 -0
- package/src/async-throttler.ts +198 -72
- package/src/batcher.ts +18 -1
- package/src/debouncer.ts +19 -2
- package/src/event-client.ts +3 -0
- package/src/index.ts +2 -0
- package/src/queuer.ts +21 -3
- package/src/rate-limiter.ts +20 -0
- package/src/throttler.ts +20 -0
package/src/async-debouncer.ts
CHANGED
|
@@ -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:
|
|
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
|
-
|
|
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
|
-
|
|
364
|
+
const currentMaybeExecuteCount = this.store.state.maybeExecuteCount + 1
|
|
365
|
+
|
|
330
366
|
try {
|
|
331
367
|
this.#setState({ isExecuting: true })
|
|
332
|
-
const
|
|
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
|
|
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
|
-
|
|
365
|
-
this.#
|
|
366
|
-
|
|
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
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
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
|
|
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
|
-
*
|
|
430
|
-
*
|
|
431
|
-
*
|
|
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
|
package/src/async-queuer.ts
CHANGED
|
@@ -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:
|
|
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
|
-
*
|
|
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
|
|
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.
|
|
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
|
-
*
|
|
906
|
+
* @example
|
|
773
907
|
* ```ts
|
|
774
908
|
* const enqueue = asyncQueue<string>(async (item) => {
|
|
775
909
|
* return item.toUpperCase();
|
|
776
|
-
* }, {
|
|
910
|
+
* }, {
|
|
911
|
+
* concurrency: 2,
|
|
912
|
+
* wait: 100,
|
|
913
|
+
* onSuccess: (result) => console.log('Processed:', result)
|
|
914
|
+
* });
|
|
777
915
|
*
|
|
778
916
|
* enqueue('hello');
|
|
779
917
|
* ```
|