@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.
- package/dist/cjs/async-batcher.cjs +66 -5
- package/dist/cjs/async-batcher.cjs.map +1 -1
- package/dist/cjs/async-batcher.d.cts +94 -17
- package/dist/cjs/async-debouncer.cjs +53 -19
- package/dist/cjs/async-debouncer.cjs.map +1 -1
- package/dist/cjs/async-debouncer.d.cts +76 -10
- package/dist/cjs/async-queuer.cjs +58 -2
- package/dist/cjs/async-queuer.cjs.map +1 -1
- package/dist/cjs/async-queuer.d.cts +100 -10
- package/dist/cjs/async-rate-limiter.cjs +51 -2
- package/dist/cjs/async-rate-limiter.cjs.map +1 -1
- package/dist/cjs/async-rate-limiter.d.cts +91 -28
- package/dist/cjs/async-retryer.cjs +285 -0
- package/dist/cjs/async-retryer.cjs.map +1 -0
- package/dist/cjs/async-retryer.d.cts +308 -0
- package/dist/cjs/async-throttler.cjs +101 -53
- package/dist/cjs/async-throttler.cjs.map +1 -1
- package/dist/cjs/async-throttler.d.cts +82 -10
- package/dist/cjs/batcher.cjs +5 -1
- package/dist/cjs/batcher.cjs.map +1 -1
- package/dist/cjs/batcher.d.cts +10 -2
- package/dist/cjs/debouncer.cjs +5 -1
- package/dist/cjs/debouncer.cjs.map +1 -1
- package/dist/cjs/debouncer.d.cts +7 -3
- package/dist/cjs/event-client.cjs +3 -1
- package/dist/cjs/event-client.cjs.map +1 -1
- package/dist/cjs/event-client.d.cts +3 -0
- package/dist/cjs/index.cjs +13 -1
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/index.d.cts +1 -0
- package/dist/cjs/queuer.cjs +5 -1
- package/dist/cjs/queuer.cjs.map +1 -1
- package/dist/cjs/queuer.d.cts +8 -4
- package/dist/cjs/rate-limiter.cjs +5 -1
- package/dist/cjs/rate-limiter.cjs.map +1 -1
- package/dist/cjs/rate-limiter.d.cts +8 -1
- package/dist/cjs/throttler.cjs +5 -1
- package/dist/cjs/throttler.cjs.map +1 -1
- package/dist/cjs/throttler.d.cts +7 -0
- package/dist/cjs/utils.cjs +0 -10
- package/dist/cjs/utils.cjs.map +1 -1
- package/dist/cjs/utils.d.cts +0 -1
- package/dist/esm/async-batcher.d.ts +94 -17
- package/dist/esm/async-batcher.js +68 -7
- package/dist/esm/async-batcher.js.map +1 -1
- package/dist/esm/async-debouncer.d.ts +76 -10
- package/dist/esm/async-debouncer.js +55 -21
- package/dist/esm/async-debouncer.js.map +1 -1
- package/dist/esm/async-queuer.d.ts +100 -10
- package/dist/esm/async-queuer.js +60 -4
- package/dist/esm/async-queuer.js.map +1 -1
- package/dist/esm/async-rate-limiter.d.ts +91 -28
- package/dist/esm/async-rate-limiter.js +53 -4
- package/dist/esm/async-rate-limiter.js.map +1 -1
- package/dist/esm/async-retryer.d.ts +308 -0
- package/dist/esm/async-retryer.js +285 -0
- package/dist/esm/async-retryer.js.map +1 -0
- package/dist/esm/async-throttler.d.ts +82 -10
- package/dist/esm/async-throttler.js +103 -55
- package/dist/esm/async-throttler.js.map +1 -1
- package/dist/esm/batcher.d.ts +10 -2
- package/dist/esm/batcher.js +6 -2
- package/dist/esm/batcher.js.map +1 -1
- package/dist/esm/debouncer.d.ts +7 -3
- package/dist/esm/debouncer.js +7 -3
- package/dist/esm/debouncer.js.map +1 -1
- package/dist/esm/event-client.d.ts +3 -0
- package/dist/esm/event-client.js +3 -1
- package/dist/esm/event-client.js.map +1 -1
- package/dist/esm/index.d.ts +1 -0
- package/dist/esm/index.js +24 -12
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/queuer.d.ts +8 -4
- package/dist/esm/queuer.js +7 -3
- package/dist/esm/queuer.js.map +1 -1
- package/dist/esm/rate-limiter.d.ts +8 -1
- package/dist/esm/rate-limiter.js +7 -3
- package/dist/esm/rate-limiter.js.map +1 -1
- package/dist/esm/throttler.d.ts +7 -0
- package/dist/esm/throttler.js +7 -3
- package/dist/esm/throttler.js.map +1 -1
- package/dist/esm/utils.d.ts +0 -1
- package/dist/esm/utils.js +0 -10
- package/dist/esm/utils.js.map +1 -1
- package/package.json +13 -3
- package/src/async-batcher.ts +144 -22
- package/src/async-debouncer.ts +118 -33
- package/src/async-queuer.ts +147 -14
- package/src/async-rate-limiter.ts +129 -33
- package/src/async-retryer.ts +664 -0
- package/src/async-throttler.ts +196 -75
- package/src/batcher.ts +16 -4
- package/src/debouncer.ts +17 -5
- package/src/event-client.ts +6 -1
- package/src/index.ts +2 -0
- package/src/queuer.ts +19 -6
- package/src/rate-limiter.ts +18 -3
- package/src/throttler.ts +17 -2
- package/src/utils.ts +0 -15
package/src/async-debouncer.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { Store } from '@tanstack/store'
|
|
2
|
-
import {
|
|
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:
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
359
|
+
const currentMaybeExecuteCount = this.store.state.maybeExecuteCount + 1
|
|
360
|
+
|
|
330
361
|
try {
|
|
331
362
|
this.#setState({ isExecuting: true })
|
|
332
|
-
const
|
|
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
|
|
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
|
-
|
|
365
|
-
this.#
|
|
366
|
-
|
|
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
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
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
|
|
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
|
-
*
|
|
430
|
-
*
|
|
431
|
-
*
|
|
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
|
package/src/async-queuer.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { Store } from '@tanstack/store'
|
|
2
|
-
import {
|
|
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:
|
|
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
|
|
@@ -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 =
|
|
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
|
|
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.
|
|
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
|
-
*
|
|
901
|
+
* @example
|
|
773
902
|
* ```ts
|
|
774
903
|
* const enqueue = asyncQueue<string>(async (item) => {
|
|
775
904
|
* return item.toUpperCase();
|
|
776
|
-
* }, {
|
|
905
|
+
* }, {
|
|
906
|
+
* concurrency: 2,
|
|
907
|
+
* wait: 100,
|
|
908
|
+
* onSuccess: (result) => console.log('Processed:', result)
|
|
909
|
+
* });
|
|
777
910
|
*
|
|
778
911
|
* enqueue('hello');
|
|
779
912
|
* ```
|