@tanstack/pacer 0.21.0 → 0.22.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/README.md +17 -2
- package/dist/async-batcher.cjs +1 -1
- package/dist/async-batcher.cjs.map +1 -1
- package/dist/async-batcher.d.cts +0 -1
- package/dist/async-batcher.d.ts +0 -1
- package/dist/async-batcher.js +1 -1
- package/dist/async-batcher.js.map +1 -1
- package/dist/async-debouncer.cjs +2 -5
- package/dist/async-debouncer.cjs.map +1 -1
- package/dist/async-debouncer.d.cts +5 -6
- package/dist/async-debouncer.d.ts +5 -6
- package/dist/async-debouncer.js +2 -5
- package/dist/async-debouncer.js.map +1 -1
- package/dist/async-queuer.cjs +49 -17
- package/dist/async-queuer.cjs.map +1 -1
- package/dist/async-queuer.d.cts +1 -1
- package/dist/async-queuer.d.ts +1 -1
- package/dist/async-queuer.js +49 -17
- package/dist/async-queuer.js.map +1 -1
- package/dist/async-rate-limiter.cjs +3 -6
- package/dist/async-rate-limiter.cjs.map +1 -1
- package/dist/async-rate-limiter.d.cts +4 -5
- package/dist/async-rate-limiter.d.ts +4 -5
- package/dist/async-rate-limiter.js +3 -6
- package/dist/async-rate-limiter.js.map +1 -1
- package/dist/async-retryer.cjs +2 -11
- package/dist/async-retryer.cjs.map +1 -1
- package/dist/async-retryer.d.cts +4 -2
- package/dist/async-retryer.d.ts +4 -2
- package/dist/async-retryer.js +2 -11
- package/dist/async-retryer.js.map +1 -1
- package/dist/async-throttler.cjs +2 -5
- package/dist/async-throttler.cjs.map +1 -1
- package/dist/async-throttler.d.cts +5 -6
- package/dist/async-throttler.d.ts +5 -6
- package/dist/async-throttler.js +2 -5
- package/dist/async-throttler.js.map +1 -1
- package/dist/batcher.cjs.map +1 -1
- package/dist/batcher.d.cts +0 -1
- package/dist/batcher.d.ts +0 -1
- package/dist/batcher.js.map +1 -1
- package/dist/debouncer.cjs.map +1 -1
- package/dist/debouncer.d.cts +0 -1
- package/dist/debouncer.d.ts +0 -1
- package/dist/debouncer.js.map +1 -1
- package/dist/event-client.cjs +8 -0
- package/dist/event-client.cjs.map +1 -1
- package/dist/event-client.d.cts +0 -1
- package/dist/event-client.d.ts +0 -1
- package/dist/event-client.js +8 -0
- package/dist/event-client.js.map +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.js +1 -1
- package/dist/queuer.cjs +7 -2
- package/dist/queuer.cjs.map +1 -1
- package/dist/queuer.d.cts +1 -1
- package/dist/queuer.d.ts +1 -1
- package/dist/queuer.js +7 -2
- package/dist/queuer.js.map +1 -1
- package/dist/rate-limiter.cjs.map +1 -1
- package/dist/rate-limiter.d.cts +0 -1
- package/dist/rate-limiter.d.ts +0 -1
- package/dist/rate-limiter.js.map +1 -1
- package/dist/throttler.cjs.map +1 -1
- package/dist/throttler.d.cts +0 -1
- package/dist/throttler.d.ts +0 -1
- package/dist/throttler.js.map +1 -1
- package/dist/utils.cjs +0 -1
- package/dist/utils.cjs.map +1 -1
- package/dist/utils.d.cts +0 -1
- package/dist/utils.d.ts +0 -1
- package/dist/utils.js.map +1 -1
- package/package.json +3 -3
- package/src/async-debouncer.ts +30 -29
- package/src/async-queuer.ts +83 -20
- package/src/async-rate-limiter.ts +11 -10
- package/src/async-retryer.ts +4 -15
- package/src/async-throttler.ts +33 -32
- package/src/queuer.ts +13 -2
package/src/async-queuer.ts
CHANGED
|
@@ -431,25 +431,36 @@ export class AsyncQueuer<TValue> {
|
|
|
431
431
|
this.#checkExpiredItems()
|
|
432
432
|
|
|
433
433
|
// Process items concurrently up to the concurrency limit
|
|
434
|
-
|
|
434
|
+
let scheduledAsyncWork = false
|
|
435
|
+
const activeItems = [...this.store.state.activeItems]
|
|
435
436
|
while (
|
|
436
437
|
activeItems.length < this.#getConcurrency() &&
|
|
437
438
|
this.store.state.items.length > 0
|
|
438
439
|
) {
|
|
439
440
|
const nextItem = this.peekNextItem()
|
|
440
|
-
if (
|
|
441
|
+
if (nextItem === undefined) {
|
|
441
442
|
break
|
|
442
443
|
}
|
|
443
444
|
activeItems.push(nextItem)
|
|
444
445
|
this.#setState({
|
|
445
|
-
activeItems,
|
|
446
|
+
activeItems: [...activeItems],
|
|
446
447
|
})
|
|
448
|
+
scheduledAsyncWork = true
|
|
447
449
|
;(async () => {
|
|
448
|
-
|
|
450
|
+
try {
|
|
451
|
+
await this.execute()
|
|
452
|
+
} catch {
|
|
453
|
+
// errors are already surfaced via onError/errorCount (and rethrown to
|
|
454
|
+
// direct execute/flush callers); swallowing here prevents unhandled
|
|
455
|
+
// rejections and keeps the processing chain alive
|
|
456
|
+
}
|
|
449
457
|
|
|
450
458
|
const wait = this.#getWait()
|
|
451
459
|
if (wait > 0) {
|
|
452
|
-
const timeoutId = setTimeout(() =>
|
|
460
|
+
const timeoutId = setTimeout(() => {
|
|
461
|
+
this.#timeoutIds.delete(timeoutId)
|
|
462
|
+
this.#tick()
|
|
463
|
+
}, wait)
|
|
453
464
|
this.#timeoutIds.add(timeoutId)
|
|
454
465
|
return
|
|
455
466
|
}
|
|
@@ -458,12 +469,17 @@ export class AsyncQueuer<TValue> {
|
|
|
458
469
|
})()
|
|
459
470
|
}
|
|
460
471
|
|
|
461
|
-
|
|
472
|
+
// pendingTick must stay true while executions or wait timers are pending so
|
|
473
|
+
// that addItem does not trigger an extra tick that bypasses the wait period
|
|
474
|
+
if (!scheduledAsyncWork) {
|
|
475
|
+
this.#setState({ pendingTick: false })
|
|
476
|
+
}
|
|
462
477
|
}
|
|
463
478
|
|
|
464
479
|
/**
|
|
465
480
|
* Adds an item to the queue. If the queue is full, the item is rejected and onReject is called.
|
|
466
481
|
* Items can be inserted based on priority or at the front/back depending on configuration.
|
|
482
|
+
* `undefined` cannot be queued (it is the internal "no item" sentinel) and is always rejected.
|
|
467
483
|
*
|
|
468
484
|
* @example
|
|
469
485
|
* ```ts
|
|
@@ -480,6 +496,16 @@ export class AsyncQueuer<TValue> {
|
|
|
480
496
|
addItemCount: this.store.state.addItemCount + 1,
|
|
481
497
|
})
|
|
482
498
|
|
|
499
|
+
// undefined is the internal "no item" sentinel (peekNextItem/getNextItem);
|
|
500
|
+
// queuing it would wedge the processing loop and block items behind it
|
|
501
|
+
if (item === undefined) {
|
|
502
|
+
this.#setState({
|
|
503
|
+
rejectionCount: this.store.state.rejectionCount + 1,
|
|
504
|
+
})
|
|
505
|
+
this.options.onReject?.(item, this)
|
|
506
|
+
return false
|
|
507
|
+
}
|
|
508
|
+
|
|
483
509
|
if (this.store.state.items.length >= (this.options.maxSize ?? Infinity)) {
|
|
484
510
|
this.#setState({
|
|
485
511
|
rejectionCount: this.store.state.rejectionCount + 1,
|
|
@@ -492,7 +518,7 @@ export class AsyncQueuer<TValue> {
|
|
|
492
518
|
const priority =
|
|
493
519
|
this.options.getPriority !== defaultOptions.getPriority
|
|
494
520
|
? this.options.getPriority!(item)
|
|
495
|
-
: (item as any)
|
|
521
|
+
: (item as any)?.priority
|
|
496
522
|
|
|
497
523
|
const items = this.store.state.items
|
|
498
524
|
const itemTimestamps = this.store.state.itemTimestamps
|
|
@@ -503,7 +529,7 @@ export class AsyncQueuer<TValue> {
|
|
|
503
529
|
const existingPriority =
|
|
504
530
|
this.options.getPriority !== defaultOptions.getPriority
|
|
505
531
|
? this.options.getPriority!(existing)
|
|
506
|
-
: (existing as any)
|
|
532
|
+
: (existing as any)?.priority
|
|
507
533
|
return existingPriority < priority
|
|
508
534
|
})
|
|
509
535
|
|
|
@@ -616,10 +642,10 @@ export class AsyncQueuer<TValue> {
|
|
|
616
642
|
isExecuting: true,
|
|
617
643
|
})
|
|
618
644
|
try {
|
|
619
|
-
const currentAsyncRetryer = new AsyncRetryer(
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
645
|
+
const currentAsyncRetryer = new AsyncRetryer(
|
|
646
|
+
this.fn,
|
|
647
|
+
this.options.asyncRetryerOptions,
|
|
648
|
+
)
|
|
623
649
|
this.asyncRetryers.set(currentExecuteCount, currentAsyncRetryer)
|
|
624
650
|
const lastResult = await currentAsyncRetryer.execute(item) // EXECUTE!
|
|
625
651
|
this.#setState({
|
|
@@ -637,11 +663,17 @@ export class AsyncQueuer<TValue> {
|
|
|
637
663
|
}
|
|
638
664
|
} finally {
|
|
639
665
|
this.asyncRetryers.delete(currentExecuteCount) // dispose retryer
|
|
666
|
+
// remove only one occurrence so duplicate item values keep accurate
|
|
667
|
+
// concurrency accounting
|
|
668
|
+
const remainingActiveItems = [...this.store.state.activeItems]
|
|
669
|
+
const activeItemIndex = remainingActiveItems.indexOf(item)
|
|
670
|
+
if (activeItemIndex !== -1) {
|
|
671
|
+
remainingActiveItems.splice(activeItemIndex, 1)
|
|
672
|
+
}
|
|
640
673
|
this.#setState({
|
|
641
|
-
activeItems:
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
isExecuting: false,
|
|
674
|
+
activeItems: remainingActiveItems,
|
|
675
|
+
// other executions may still be in flight (concurrency > 1 or flush)
|
|
676
|
+
isExecuting: this.asyncRetryers.size > 0,
|
|
645
677
|
settledCount: this.store.state.settledCount + 1,
|
|
646
678
|
})
|
|
647
679
|
this.options.onSettled?.(item, this)
|
|
@@ -658,10 +690,33 @@ export class AsyncQueuer<TValue> {
|
|
|
658
690
|
numberOfItems: number = this.store.state.items.length,
|
|
659
691
|
position?: QueuePosition,
|
|
660
692
|
): Promise<void> => {
|
|
661
|
-
this.#clearTimeouts() // clear any pending timeouts
|
|
662
|
-
await Promise.
|
|
693
|
+
this.#clearTimeouts() // clear any pending timeouts (kills the tick chain)
|
|
694
|
+
const results = await Promise.allSettled(
|
|
663
695
|
Array.from({ length: numberOfItems }, () => this.execute(position)),
|
|
664
696
|
)
|
|
697
|
+
// the tick chain was killed above; restart it so remaining and future items
|
|
698
|
+
// process even when a flushed task rejected. Remaining items resume with the
|
|
699
|
+
// normal wait spacing after the flushed executions.
|
|
700
|
+
this.#setState({ pendingTick: false })
|
|
701
|
+
if (this.store.state.isRunning && this.store.state.items.length > 0) {
|
|
702
|
+
const wait = this.#getWait()
|
|
703
|
+
if (wait > 0) {
|
|
704
|
+
this.#setState({ pendingTick: true })
|
|
705
|
+
const timeoutId = setTimeout(() => {
|
|
706
|
+
this.#timeoutIds.delete(timeoutId)
|
|
707
|
+
this.#tick()
|
|
708
|
+
}, wait)
|
|
709
|
+
this.#timeoutIds.add(timeoutId)
|
|
710
|
+
} else {
|
|
711
|
+
this.#tick()
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
const failure = results.find(
|
|
715
|
+
(result): result is PromiseRejectedResult => result.status === 'rejected',
|
|
716
|
+
)
|
|
717
|
+
if (failure) {
|
|
718
|
+
throw failure.reason
|
|
719
|
+
}
|
|
665
720
|
}
|
|
666
721
|
|
|
667
722
|
/**
|
|
@@ -671,9 +726,17 @@ export class AsyncQueuer<TValue> {
|
|
|
671
726
|
flushAsBatch = async (
|
|
672
727
|
batchFunction: (items: Array<TValue>) => Promise<any>,
|
|
673
728
|
): Promise<void> => {
|
|
674
|
-
this.#clearTimeouts() // clear any pending timeouts
|
|
729
|
+
this.#clearTimeouts() // clear any pending timeouts (kills the tick chain)
|
|
675
730
|
const items = this.#getAllItems()
|
|
676
|
-
|
|
731
|
+
try {
|
|
732
|
+
await batchFunction(items)
|
|
733
|
+
} finally {
|
|
734
|
+
// restore the tick chain even when the batch function rejects
|
|
735
|
+
this.#setState({ pendingTick: false })
|
|
736
|
+
if (this.store.state.isRunning && this.store.state.items.length > 0) {
|
|
737
|
+
this.#tick()
|
|
738
|
+
}
|
|
739
|
+
}
|
|
677
740
|
}
|
|
678
741
|
|
|
679
742
|
/**
|
|
@@ -25,7 +25,7 @@ export interface AsyncRateLimiterState<TFn extends AnyAsyncFunction> {
|
|
|
25
25
|
/**
|
|
26
26
|
* The result from the most recent successful function execution
|
|
27
27
|
*/
|
|
28
|
-
lastResult: ReturnType<TFn
|
|
28
|
+
lastResult: Awaited<ReturnType<TFn>> | undefined
|
|
29
29
|
/**
|
|
30
30
|
* Number of function executions that have been rejected due to rate limiting
|
|
31
31
|
*/
|
|
@@ -118,7 +118,7 @@ export interface AsyncRateLimiterOptions<TFn extends AnyAsyncFunction> {
|
|
|
118
118
|
* Optional function to call when the rate-limited function is executed
|
|
119
119
|
*/
|
|
120
120
|
onSuccess?: (
|
|
121
|
-
result: ReturnType<TFn
|
|
121
|
+
result: Awaited<ReturnType<TFn>>,
|
|
122
122
|
args: Parameters<TFn>,
|
|
123
123
|
rateLimiter: AsyncRateLimiter<TFn>,
|
|
124
124
|
) => void
|
|
@@ -357,7 +357,7 @@ export class AsyncRateLimiter<TFn extends AnyAsyncFunction> {
|
|
|
357
357
|
*/
|
|
358
358
|
maybeExecute = async (
|
|
359
359
|
...args: Parameters<TFn>
|
|
360
|
-
): Promise<ReturnType<TFn
|
|
360
|
+
): Promise<Awaited<ReturnType<TFn>> | undefined> => {
|
|
361
361
|
this.#setState({
|
|
362
362
|
maybeExecuteCount: this.store.state.maybeExecuteCount + 1,
|
|
363
363
|
})
|
|
@@ -380,7 +380,7 @@ export class AsyncRateLimiter<TFn extends AnyAsyncFunction> {
|
|
|
380
380
|
|
|
381
381
|
#execute = async (
|
|
382
382
|
...args: Parameters<TFn>
|
|
383
|
-
): Promise<ReturnType<TFn
|
|
383
|
+
): Promise<Awaited<ReturnType<TFn>> | undefined> => {
|
|
384
384
|
if (!this.#getEnabled()) return
|
|
385
385
|
|
|
386
386
|
const currentMaybeExecute = this.store.state.maybeExecuteCount
|
|
@@ -393,10 +393,10 @@ export class AsyncRateLimiter<TFn extends AnyAsyncFunction> {
|
|
|
393
393
|
|
|
394
394
|
try {
|
|
395
395
|
// Create a new AsyncRetryer for this execution to avoid cancelling concurrent executions
|
|
396
|
-
const currentAsyncRetryer = new AsyncRetryer(
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
396
|
+
const currentAsyncRetryer = new AsyncRetryer(
|
|
397
|
+
this.fn,
|
|
398
|
+
this.options.asyncRetryerOptions,
|
|
399
|
+
)
|
|
400
400
|
this.asyncRetryers.set(currentMaybeExecute, currentAsyncRetryer)
|
|
401
401
|
const result = await currentAsyncRetryer.execute(...args) // EXECUTE!
|
|
402
402
|
this.#setCleanupTimeout(now)
|
|
@@ -404,7 +404,7 @@ export class AsyncRateLimiter<TFn extends AnyAsyncFunction> {
|
|
|
404
404
|
successCount: this.store.state.successCount + 1,
|
|
405
405
|
lastResult: result,
|
|
406
406
|
})
|
|
407
|
-
this.options.onSuccess?.(result as ReturnType<TFn
|
|
407
|
+
this.options.onSuccess?.(result as Awaited<ReturnType<TFn>>, args, this)
|
|
408
408
|
} catch (error) {
|
|
409
409
|
this.#setState({
|
|
410
410
|
errorCount: this.store.state.errorCount + 1,
|
|
@@ -416,7 +416,8 @@ export class AsyncRateLimiter<TFn extends AnyAsyncFunction> {
|
|
|
416
416
|
} finally {
|
|
417
417
|
this.asyncRetryers.delete(currentMaybeExecute) // dispose retryer
|
|
418
418
|
this.#setState({
|
|
419
|
-
|
|
419
|
+
// other executions may still be in flight within the window
|
|
420
|
+
isExecuting: this.asyncRetryers.size > 0,
|
|
420
421
|
settleCount: this.store.state.settleCount + 1,
|
|
421
422
|
})
|
|
422
423
|
this.options.onSettled?.(args, this)
|
package/src/async-retryer.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { Store } from '@tanstack/store'
|
|
2
2
|
import { parseFunctionOrValue } from './utils'
|
|
3
|
-
import { emitChange, pacerEventClient } from './event-client'
|
|
4
3
|
import type { AnyAsyncFunction } from './types'
|
|
5
4
|
|
|
6
5
|
export interface AsyncRetryerState<TFn extends AnyAsyncFunction> {
|
|
@@ -87,7 +86,10 @@ export interface AsyncRetryerOptions<TFn extends AnyAsyncFunction> {
|
|
|
87
86
|
jitter?: number
|
|
88
87
|
/**
|
|
89
88
|
* Optional key to identify this async retryer instance.
|
|
90
|
-
*
|
|
89
|
+
* Note: async retryers are not currently surfaced in the devtools, so this key
|
|
90
|
+
* is only a plain identifier. Retryer instances are often created per-execution
|
|
91
|
+
* (including internally by the other async utilities), so they intentionally do
|
|
92
|
+
* not register with the devtools event bus.
|
|
91
93
|
*/
|
|
92
94
|
key?: string
|
|
93
95
|
/**
|
|
@@ -319,18 +321,6 @@ export class AsyncRetryer<TFn extends AnyAsyncFunction> {
|
|
|
319
321
|
(initialOptions.onError ? false : defaultOptions.throwOnError),
|
|
320
322
|
}
|
|
321
323
|
this.#setState(this.options.initialState ?? {})
|
|
322
|
-
|
|
323
|
-
if (this.key) {
|
|
324
|
-
pacerEventClient.on('d-AsyncRetryer', (event) => {
|
|
325
|
-
if (event.payload.key !== this.key) return
|
|
326
|
-
this.#setState(
|
|
327
|
-
event.payload.store.state as Partial<AsyncRetryerState<TFn>>,
|
|
328
|
-
)
|
|
329
|
-
this.setOptions(
|
|
330
|
-
event.payload.options as Partial<AsyncRetryerOptions<TFn>>,
|
|
331
|
-
)
|
|
332
|
-
})
|
|
333
|
-
}
|
|
334
324
|
}
|
|
335
325
|
|
|
336
326
|
/**
|
|
@@ -359,7 +349,6 @@ export class AsyncRetryer<TFn extends AnyAsyncFunction> {
|
|
|
359
349
|
: 'idle',
|
|
360
350
|
}
|
|
361
351
|
})
|
|
362
|
-
emitChange('AsyncRetryer', this)
|
|
363
352
|
}
|
|
364
353
|
|
|
365
354
|
#getEnabled = (): boolean => {
|
package/src/async-throttler.ts
CHANGED
|
@@ -29,7 +29,7 @@ export interface AsyncThrottlerState<TFn extends AnyAsyncFunction> {
|
|
|
29
29
|
/**
|
|
30
30
|
* The result from the most recent successful function execution
|
|
31
31
|
*/
|
|
32
|
-
lastResult: ReturnType<TFn
|
|
32
|
+
lastResult: Awaited<ReturnType<TFn>> | undefined
|
|
33
33
|
/**
|
|
34
34
|
* Number of times maybeExecute has been called (for reduction calculations)
|
|
35
35
|
*/
|
|
@@ -119,7 +119,7 @@ export interface AsyncThrottlerOptions<TFn extends AnyAsyncFunction> {
|
|
|
119
119
|
* Optional function to call when the throttled function is executed
|
|
120
120
|
*/
|
|
121
121
|
onSuccess?: (
|
|
122
|
-
result: ReturnType<TFn
|
|
122
|
+
result: Awaited<ReturnType<TFn>>,
|
|
123
123
|
args: Parameters<TFn>,
|
|
124
124
|
asyncThrottler: AsyncThrottler<TFn>,
|
|
125
125
|
) => void
|
|
@@ -236,8 +236,7 @@ export class AsyncThrottler<TFn extends AnyAsyncFunction> {
|
|
|
236
236
|
asyncRetryers = new Map<number, AsyncRetryer<TFn>>()
|
|
237
237
|
#timeoutId: ReturnType<typeof setTimeout> | null = null
|
|
238
238
|
#resolvePreviousPromise:
|
|
239
|
-
|
|
240
|
-
| null = null
|
|
239
|
+
((value?: Awaited<ReturnType<TFn>> | undefined) => void) | null = null
|
|
241
240
|
|
|
242
241
|
constructor(
|
|
243
242
|
public fn: TFn,
|
|
@@ -337,7 +336,7 @@ export class AsyncThrottler<TFn extends AnyAsyncFunction> {
|
|
|
337
336
|
*/
|
|
338
337
|
maybeExecute = async (
|
|
339
338
|
...args: Parameters<TFn>
|
|
340
|
-
): Promise<ReturnType<TFn
|
|
339
|
+
): Promise<Awaited<ReturnType<TFn>> | undefined> => {
|
|
341
340
|
if (!this.#getEnabled()) return undefined
|
|
342
341
|
|
|
343
342
|
this.#resolvePreviousPromiseInternal()
|
|
@@ -380,51 +379,53 @@ export class AsyncThrottler<TFn extends AnyAsyncFunction> {
|
|
|
380
379
|
})
|
|
381
380
|
|
|
382
381
|
// Set up new trailing execution
|
|
383
|
-
return new Promise
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
this.#
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
382
|
+
return new Promise<Awaited<ReturnType<TFn>> | undefined>(
|
|
383
|
+
(resolve, reject) => {
|
|
384
|
+
this.#resolvePreviousPromise = resolve
|
|
385
|
+
|
|
386
|
+
const newTimeSinceLastExecution = this.store.state.lastExecutionTime
|
|
387
|
+
? now - this.store.state.lastExecutionTime
|
|
388
|
+
: 0
|
|
389
|
+
const timeoutDuration = Math.max(0, wait - newTimeSinceLastExecution)
|
|
390
|
+
|
|
391
|
+
this.#timeoutId = setTimeout(async () => {
|
|
392
|
+
this.#clearTimeout()
|
|
393
|
+
if (this.store.state.lastArgs !== undefined) {
|
|
394
|
+
try {
|
|
395
|
+
await this.#execute(...this.store.state.lastArgs) // Trailing EXECUTE!
|
|
396
|
+
} catch (error) {
|
|
397
|
+
reject(error)
|
|
398
|
+
}
|
|
398
399
|
}
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
},
|
|
403
|
-
|
|
400
|
+
this.#resolvePreviousPromise = null
|
|
401
|
+
resolve(this.store.state.lastResult)
|
|
402
|
+
}, timeoutDuration)
|
|
403
|
+
},
|
|
404
|
+
)
|
|
404
405
|
}
|
|
405
406
|
return this.store.state.lastResult
|
|
406
407
|
}
|
|
407
408
|
|
|
408
409
|
#execute = async (
|
|
409
410
|
...args: Parameters<TFn>
|
|
410
|
-
): Promise<ReturnType<TFn
|
|
411
|
+
): Promise<Awaited<ReturnType<TFn>> | undefined> => {
|
|
411
412
|
if (!this.#getEnabled()) return undefined
|
|
412
413
|
|
|
413
414
|
const currentMaybeExecute = this.store.state.maybeExecuteCount
|
|
414
415
|
|
|
415
416
|
try {
|
|
416
417
|
this.#setState({ isExecuting: true })
|
|
417
|
-
const currentAsyncRetryer = new AsyncRetryer(
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
418
|
+
const currentAsyncRetryer = new AsyncRetryer(
|
|
419
|
+
this.fn,
|
|
420
|
+
this.options.asyncRetryerOptions,
|
|
421
|
+
)
|
|
421
422
|
this.asyncRetryers.set(currentMaybeExecute, currentAsyncRetryer)
|
|
422
423
|
const result = await currentAsyncRetryer.execute(...args) // EXECUTE!
|
|
423
424
|
this.#setState({
|
|
424
425
|
lastResult: result,
|
|
425
426
|
successCount: this.store.state.successCount + 1,
|
|
426
427
|
})
|
|
427
|
-
this.options.onSuccess?.(result as ReturnType<TFn
|
|
428
|
+
this.options.onSuccess?.(result as Awaited<ReturnType<TFn>>, args, this)
|
|
428
429
|
} catch (error) {
|
|
429
430
|
this.#setState({
|
|
430
431
|
errorCount: this.store.state.errorCount + 1,
|
|
@@ -459,7 +460,7 @@ export class AsyncThrottler<TFn extends AnyAsyncFunction> {
|
|
|
459
460
|
/**
|
|
460
461
|
* Processes the current pending execution immediately
|
|
461
462
|
*/
|
|
462
|
-
flush = async (): Promise<ReturnType<TFn
|
|
463
|
+
flush = async (): Promise<Awaited<ReturnType<TFn>> | undefined> => {
|
|
463
464
|
if (this.store.state.isPending && this.store.state.lastArgs) {
|
|
464
465
|
// Store the pending promise resolver before clearing timeout
|
|
465
466
|
const resolvePromise = this.#resolvePreviousPromise
|
package/src/queuer.ts
CHANGED
|
@@ -390,6 +390,7 @@ export class Queuer<TValue> {
|
|
|
390
390
|
|
|
391
391
|
/**
|
|
392
392
|
* Adds an item to the queue. If the queue is full, the item is rejected and onReject is called.
|
|
393
|
+
* `undefined` cannot be queued (it is the internal "no item" sentinel) and is always rejected.
|
|
393
394
|
* Items can be inserted based on priority or at the front/back depending on configuration.
|
|
394
395
|
*
|
|
395
396
|
* Returns true if the item was added, false if the queue is full.
|
|
@@ -409,6 +410,16 @@ export class Queuer<TValue> {
|
|
|
409
410
|
addItemCount: this.store.state.addItemCount + 1,
|
|
410
411
|
})
|
|
411
412
|
|
|
413
|
+
// undefined is the internal "no item" sentinel (peekNextItem/getNextItem);
|
|
414
|
+
// queuing it would break the processing loop and block items behind it
|
|
415
|
+
if (item === undefined) {
|
|
416
|
+
this.#setState({
|
|
417
|
+
rejectionCount: this.store.state.rejectionCount + 1,
|
|
418
|
+
})
|
|
419
|
+
this.options.onReject?.(item, this)
|
|
420
|
+
return false
|
|
421
|
+
}
|
|
422
|
+
|
|
412
423
|
if (this.store.state.items.length >= (this.options.maxSize ?? Infinity)) {
|
|
413
424
|
this.#setState({
|
|
414
425
|
rejectionCount: this.store.state.rejectionCount + 1,
|
|
@@ -421,7 +432,7 @@ export class Queuer<TValue> {
|
|
|
421
432
|
const priority =
|
|
422
433
|
this.options.getPriority !== defaultOptions.getPriority
|
|
423
434
|
? this.options.getPriority!(item)
|
|
424
|
-
: (item as any)
|
|
435
|
+
: (item as any)?.priority
|
|
425
436
|
|
|
426
437
|
const items = this.store.state.items
|
|
427
438
|
const itemTimestamps = this.store.state.itemTimestamps
|
|
@@ -432,7 +443,7 @@ export class Queuer<TValue> {
|
|
|
432
443
|
const existingPriority: number =
|
|
433
444
|
this.options.getPriority !== defaultOptions.getPriority
|
|
434
445
|
? this.options.getPriority!(existing)
|
|
435
|
-
: (existing as any)
|
|
446
|
+
: (existing as any)?.priority
|
|
436
447
|
return existingPriority < priority
|
|
437
448
|
})
|
|
438
449
|
|