@tanstack/pacer 0.9.0 → 0.10.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 +1 -12
- package/dist/cjs/async-batcher.cjs.map +1 -1
- package/dist/cjs/async-batcher.d.cts +4 -16
- package/dist/cjs/async-debouncer.cjs +25 -8
- package/dist/cjs/async-debouncer.cjs.map +1 -1
- package/dist/cjs/async-debouncer.d.cts +1 -1
- package/dist/cjs/async-queuer.cjs +20 -9
- package/dist/cjs/async-queuer.cjs.map +1 -1
- package/dist/cjs/async-queuer.d.cts +10 -5
- package/dist/cjs/async-rate-limiter.cjs +53 -12
- package/dist/cjs/async-rate-limiter.cjs.map +1 -1
- package/dist/cjs/async-rate-limiter.d.cts +8 -0
- package/dist/cjs/async-throttler.cjs +17 -6
- package/dist/cjs/async-throttler.cjs.map +1 -1
- package/dist/cjs/async-throttler.d.cts +1 -1
- package/dist/cjs/batcher.cjs +1 -12
- package/dist/cjs/batcher.cjs.map +1 -1
- package/dist/cjs/batcher.d.cts +4 -16
- package/dist/cjs/queuer.cjs +16 -4
- package/dist/cjs/queuer.cjs.map +1 -1
- package/dist/cjs/queuer.d.cts +13 -8
- package/dist/cjs/rate-limiter.cjs +53 -12
- package/dist/cjs/rate-limiter.cjs.map +1 -1
- package/dist/cjs/rate-limiter.d.cts +8 -0
- package/dist/cjs/throttler.cjs.map +1 -1
- package/dist/cjs/throttler.d.cts +4 -4
- package/dist/esm/async-batcher.d.ts +4 -16
- package/dist/esm/async-batcher.js +1 -12
- package/dist/esm/async-batcher.js.map +1 -1
- package/dist/esm/async-debouncer.d.ts +1 -1
- package/dist/esm/async-debouncer.js +25 -8
- package/dist/esm/async-debouncer.js.map +1 -1
- package/dist/esm/async-queuer.d.ts +10 -5
- package/dist/esm/async-queuer.js +20 -9
- package/dist/esm/async-queuer.js.map +1 -1
- package/dist/esm/async-rate-limiter.d.ts +8 -0
- package/dist/esm/async-rate-limiter.js +53 -12
- package/dist/esm/async-rate-limiter.js.map +1 -1
- package/dist/esm/async-throttler.d.ts +1 -1
- package/dist/esm/async-throttler.js +17 -6
- package/dist/esm/async-throttler.js.map +1 -1
- package/dist/esm/batcher.d.ts +4 -16
- package/dist/esm/batcher.js +1 -12
- package/dist/esm/batcher.js.map +1 -1
- package/dist/esm/queuer.d.ts +13 -8
- package/dist/esm/queuer.js +16 -4
- package/dist/esm/queuer.js.map +1 -1
- package/dist/esm/rate-limiter.d.ts +8 -0
- package/dist/esm/rate-limiter.js +53 -12
- package/dist/esm/rate-limiter.js.map +1 -1
- package/dist/esm/throttler.d.ts +4 -4
- package/dist/esm/throttler.js.map +1 -1
- package/package.json +1 -1
- package/src/async-batcher.ts +5 -28
- package/src/async-debouncer.ts +27 -8
- package/src/async-queuer.ts +32 -14
- package/src/async-rate-limiter.ts +72 -12
- package/src/async-throttler.ts +19 -6
- package/src/batcher.ts +5 -28
- package/src/queuer.ts +29 -12
- package/src/rate-limiter.ts +71 -11
- package/src/throttler.ts +4 -4
package/src/async-debouncer.ts
CHANGED
|
@@ -176,6 +176,7 @@ export class AsyncDebouncer<TFn extends AnyAsyncFunction> {
|
|
|
176
176
|
#resolvePreviousPromise:
|
|
177
177
|
| ((value?: ReturnType<TFn> | undefined) => void)
|
|
178
178
|
| null = null
|
|
179
|
+
#rejectPreviousPromise: ((reason?: unknown) => void) | null = null
|
|
179
180
|
|
|
180
181
|
constructor(
|
|
181
182
|
private fn: TFn,
|
|
@@ -270,8 +271,9 @@ export class AsyncDebouncer<TFn extends AnyAsyncFunction> {
|
|
|
270
271
|
this.#setState({ isPending: true })
|
|
271
272
|
}
|
|
272
273
|
|
|
273
|
-
return new Promise((resolve) => {
|
|
274
|
+
return new Promise((resolve, reject) => {
|
|
274
275
|
this.#resolvePreviousPromise = resolve
|
|
276
|
+
this.#rejectPreviousPromise = reject
|
|
275
277
|
this.#timeoutId = setTimeout(async () => {
|
|
276
278
|
// Execute trailing if enabled
|
|
277
279
|
if (this.options.trailing && this.store.state.lastArgs) {
|
|
@@ -305,7 +307,7 @@ export class AsyncDebouncer<TFn extends AnyAsyncFunction> {
|
|
|
305
307
|
})
|
|
306
308
|
this.options.onError?.(error, this)
|
|
307
309
|
if (this.options.throwOnError) {
|
|
308
|
-
|
|
310
|
+
this.#rejectPreviousPromiseInternal(error)
|
|
309
311
|
}
|
|
310
312
|
} finally {
|
|
311
313
|
this.#setState({
|
|
@@ -322,11 +324,31 @@ export class AsyncDebouncer<TFn extends AnyAsyncFunction> {
|
|
|
322
324
|
/**
|
|
323
325
|
* Processes the current pending execution immediately
|
|
324
326
|
*/
|
|
325
|
-
flush = ():
|
|
327
|
+
flush = async (): Promise<ReturnType<TFn> | undefined> => {
|
|
326
328
|
if (this.store.state.isPending && this.store.state.lastArgs) {
|
|
327
329
|
this.#abortExecution() // abort any current execution
|
|
328
330
|
this.#clearTimeout() // clear any existing timeout
|
|
329
|
-
this.#execute(...this.store.state.lastArgs)
|
|
331
|
+
const result = await this.#execute(...this.store.state.lastArgs)
|
|
332
|
+
|
|
333
|
+
// Resolve any pending promise from maybeExecute
|
|
334
|
+
this.#resolvePreviousPromiseInternal()
|
|
335
|
+
|
|
336
|
+
return result
|
|
337
|
+
}
|
|
338
|
+
return undefined
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
#resolvePreviousPromiseInternal = (): void => {
|
|
342
|
+
if (this.#resolvePreviousPromise) {
|
|
343
|
+
this.#resolvePreviousPromise(this.store.state.lastResult)
|
|
344
|
+
this.#resolvePreviousPromise = null
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
#rejectPreviousPromiseInternal = (error: unknown): void => {
|
|
349
|
+
if (this.#rejectPreviousPromise) {
|
|
350
|
+
this.#rejectPreviousPromise(error)
|
|
351
|
+
this.#rejectPreviousPromise = null
|
|
330
352
|
}
|
|
331
353
|
}
|
|
332
354
|
|
|
@@ -339,10 +361,7 @@ export class AsyncDebouncer<TFn extends AnyAsyncFunction> {
|
|
|
339
361
|
|
|
340
362
|
#cancelPendingExecution = (): void => {
|
|
341
363
|
this.#clearTimeout()
|
|
342
|
-
|
|
343
|
-
this.#resolvePreviousPromise(this.store.state.lastResult)
|
|
344
|
-
this.#resolvePreviousPromise = null
|
|
345
|
-
}
|
|
364
|
+
this.#resolvePreviousPromiseInternal()
|
|
346
365
|
this.#setState({
|
|
347
366
|
isPending: false,
|
|
348
367
|
isExecuting: false,
|
package/src/async-queuer.ts
CHANGED
|
@@ -32,14 +32,14 @@ export interface AsyncQueuerState<TValue> {
|
|
|
32
32
|
* Whether the queuer is active and will process items automatically
|
|
33
33
|
*/
|
|
34
34
|
isRunning: boolean
|
|
35
|
-
/**
|
|
36
|
-
* Timestamps when items were added to the queue for expiration tracking
|
|
37
|
-
*/
|
|
38
|
-
itemTimestamps: Array<number>
|
|
39
35
|
/**
|
|
40
36
|
* Array of items currently waiting to be processed
|
|
41
37
|
*/
|
|
42
38
|
items: Array<TValue>
|
|
39
|
+
/**
|
|
40
|
+
* Timestamps when items were added to the queue for expiration tracking
|
|
41
|
+
*/
|
|
42
|
+
itemTimestamps: Array<number>
|
|
43
43
|
/**
|
|
44
44
|
* The result from the most recent task execution
|
|
45
45
|
*/
|
|
@@ -359,7 +359,7 @@ export class AsyncQueuer<TValue> {
|
|
|
359
359
|
const activeItems = this.store.state.activeItems
|
|
360
360
|
while (
|
|
361
361
|
activeItems.length < this.#getConcurrency() &&
|
|
362
|
-
|
|
362
|
+
this.store.state.items.length > 0
|
|
363
363
|
) {
|
|
364
364
|
const nextItem = this.peekNextItem()
|
|
365
365
|
if (!nextItem) {
|
|
@@ -402,7 +402,7 @@ export class AsyncQueuer<TValue> {
|
|
|
402
402
|
position: QueuePosition = this.options.addItemsTo ?? 'back',
|
|
403
403
|
runOnItemsChange: boolean = true,
|
|
404
404
|
): boolean => {
|
|
405
|
-
if (this.store.state.
|
|
405
|
+
if (this.store.state.items.length >= (this.options.maxSize ?? Infinity)) {
|
|
406
406
|
this.#setState({
|
|
407
407
|
rejectionCount: this.store.state.rejectionCount + 1,
|
|
408
408
|
})
|
|
@@ -507,6 +507,12 @@ export class AsyncQueuer<TValue> {
|
|
|
507
507
|
return item
|
|
508
508
|
}
|
|
509
509
|
|
|
510
|
+
#getAllItems = (): Array<TValue> => {
|
|
511
|
+
const items = this.peekAllItems()
|
|
512
|
+
this.clear()
|
|
513
|
+
return items
|
|
514
|
+
}
|
|
515
|
+
|
|
510
516
|
/**
|
|
511
517
|
* Removes and returns the next item from the queue and executes the task function with it.
|
|
512
518
|
*
|
|
@@ -552,14 +558,26 @@ export class AsyncQueuer<TValue> {
|
|
|
552
558
|
* Processes a specified number of items to execute immediately with no wait time
|
|
553
559
|
* If no numberOfItems is provided, all items will be processed
|
|
554
560
|
*/
|
|
555
|
-
flush = (
|
|
561
|
+
flush = async (
|
|
556
562
|
numberOfItems: number = this.store.state.items.length,
|
|
557
563
|
position?: QueuePosition,
|
|
558
|
-
): void => {
|
|
564
|
+
): Promise<void> => {
|
|
559
565
|
this.#clearTimeouts() // clear any pending timeouts
|
|
560
|
-
|
|
561
|
-
this.execute(position)
|
|
562
|
-
|
|
566
|
+
await Promise.all(
|
|
567
|
+
Array.from({ length: numberOfItems }, () => this.execute(position)),
|
|
568
|
+
)
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
/**
|
|
572
|
+
* Processes all items in the queue as a batch using the provided function
|
|
573
|
+
* The queue is cleared after processing
|
|
574
|
+
*/
|
|
575
|
+
flushAsBatch = async (
|
|
576
|
+
batchFunction: (items: Array<TValue>) => Promise<any>,
|
|
577
|
+
): Promise<void> => {
|
|
578
|
+
this.#clearTimeouts() // clear any pending timeouts
|
|
579
|
+
const items = this.#getAllItems()
|
|
580
|
+
await batchFunction(items)
|
|
563
581
|
}
|
|
564
582
|
|
|
565
583
|
/**
|
|
@@ -578,7 +596,7 @@ export class AsyncQueuer<TValue> {
|
|
|
578
596
|
const expiredIndices: Array<number> = []
|
|
579
597
|
|
|
580
598
|
// Find indices of expired items
|
|
581
|
-
for (let i = 0; i < this.store.state.
|
|
599
|
+
for (let i = 0; i < this.store.state.items.length; i++) {
|
|
582
600
|
const timestamp = this.store.state.itemTimestamps[i]
|
|
583
601
|
if (timestamp === undefined) continue
|
|
584
602
|
|
|
@@ -633,7 +651,7 @@ export class AsyncQueuer<TValue> {
|
|
|
633
651
|
if (position === 'front') {
|
|
634
652
|
return this.store.state.items[0]
|
|
635
653
|
}
|
|
636
|
-
return this.store.state.items[this.store.state.
|
|
654
|
+
return this.store.state.items[this.store.state.items.length - 1]
|
|
637
655
|
}
|
|
638
656
|
|
|
639
657
|
/**
|
|
@@ -662,7 +680,7 @@ export class AsyncQueuer<TValue> {
|
|
|
662
680
|
*/
|
|
663
681
|
start = (): void => {
|
|
664
682
|
this.#setState({ isRunning: true })
|
|
665
|
-
if (!this.store.state.pendingTick &&
|
|
683
|
+
if (!this.store.state.pendingTick && this.store.state.items.length > 0) {
|
|
666
684
|
this.#tick()
|
|
667
685
|
}
|
|
668
686
|
}
|
|
@@ -11,6 +11,10 @@ export interface AsyncRateLimiterState<TFn extends AnyAsyncFunction> {
|
|
|
11
11
|
* Array of timestamps when executions occurred for rate limiting calculations
|
|
12
12
|
*/
|
|
13
13
|
executionTimes: Array<number>
|
|
14
|
+
/**
|
|
15
|
+
* Whether the rate limiter has exceeded the limit
|
|
16
|
+
*/
|
|
17
|
+
isExceeded: boolean
|
|
14
18
|
/**
|
|
15
19
|
* Whether the rate-limited function is currently executing asynchronously
|
|
16
20
|
*/
|
|
@@ -27,6 +31,10 @@ export interface AsyncRateLimiterState<TFn extends AnyAsyncFunction> {
|
|
|
27
31
|
* Number of function executions that have completed (either successfully or with errors)
|
|
28
32
|
*/
|
|
29
33
|
settleCount: number
|
|
34
|
+
/**
|
|
35
|
+
* Current execution status - 'disabled' when not active, 'executing' when executing, 'idle' when not executing, 'exceeded' when rate limit is exceeded
|
|
36
|
+
*/
|
|
37
|
+
status: 'disabled' | 'executing' | 'exceeded' | 'idle'
|
|
30
38
|
/**
|
|
31
39
|
* Number of function executions that have completed successfully
|
|
32
40
|
*/
|
|
@@ -39,11 +47,13 @@ function getDefaultAsyncRateLimiterState<
|
|
|
39
47
|
return {
|
|
40
48
|
errorCount: 0,
|
|
41
49
|
executionTimes: [],
|
|
50
|
+
isExceeded: false,
|
|
42
51
|
isExecuting: false,
|
|
43
52
|
lastResult: undefined,
|
|
44
53
|
rejectionCount: 0,
|
|
45
54
|
settleCount: 0,
|
|
46
55
|
successCount: 0,
|
|
56
|
+
status: 'idle',
|
|
47
57
|
}
|
|
48
58
|
}
|
|
49
59
|
|
|
@@ -189,6 +199,7 @@ export class AsyncRateLimiter<TFn extends AnyAsyncFunction> {
|
|
|
189
199
|
AsyncRateLimiterState<TFn>
|
|
190
200
|
>(getDefaultAsyncRateLimiterState<TFn>())
|
|
191
201
|
options: AsyncRateLimiterOptions<TFn>
|
|
202
|
+
#timeoutIds: Set<NodeJS.Timeout> = new Set()
|
|
192
203
|
|
|
193
204
|
constructor(
|
|
194
205
|
private fn: TFn,
|
|
@@ -200,6 +211,9 @@ export class AsyncRateLimiter<TFn extends AnyAsyncFunction> {
|
|
|
200
211
|
throwOnError: initialOptions.throwOnError ?? !initialOptions.onError,
|
|
201
212
|
}
|
|
202
213
|
this.#setState(this.options.initialState ?? {})
|
|
214
|
+
for (const executionTime of this.#getExecutionTimesInWindow()) {
|
|
215
|
+
this.#setCleanupTimeout(executionTime)
|
|
216
|
+
}
|
|
203
217
|
}
|
|
204
218
|
|
|
205
219
|
/**
|
|
@@ -215,7 +229,19 @@ export class AsyncRateLimiter<TFn extends AnyAsyncFunction> {
|
|
|
215
229
|
...state,
|
|
216
230
|
...newState,
|
|
217
231
|
}
|
|
218
|
-
|
|
232
|
+
const isExceeded = combinedState.executionTimes.length >= this.#getLimit()
|
|
233
|
+
const status = !this.#getEnabled()
|
|
234
|
+
? 'disabled'
|
|
235
|
+
: combinedState.isExecuting
|
|
236
|
+
? 'executing'
|
|
237
|
+
: isExceeded
|
|
238
|
+
? 'exceeded'
|
|
239
|
+
: 'idle'
|
|
240
|
+
return {
|
|
241
|
+
...combinedState,
|
|
242
|
+
isExceeded,
|
|
243
|
+
status,
|
|
244
|
+
}
|
|
219
245
|
})
|
|
220
246
|
}
|
|
221
247
|
|
|
@@ -270,7 +296,7 @@ export class AsyncRateLimiter<TFn extends AnyAsyncFunction> {
|
|
|
270
296
|
): Promise<ReturnType<TFn> | undefined> => {
|
|
271
297
|
this.#cleanupOldExecutions()
|
|
272
298
|
|
|
273
|
-
const relevantExecutionTimes = this.#
|
|
299
|
+
const relevantExecutionTimes = this.#getExecutionTimesInWindow()
|
|
274
300
|
|
|
275
301
|
if (relevantExecutionTimes.length < this.#getLimit()) {
|
|
276
302
|
await this.#execute(...args)
|
|
@@ -297,7 +323,8 @@ export class AsyncRateLimiter<TFn extends AnyAsyncFunction> {
|
|
|
297
323
|
})
|
|
298
324
|
|
|
299
325
|
try {
|
|
300
|
-
const result = await this.fn(...args)
|
|
326
|
+
const result = await this.fn(...args) // EXECUTE!
|
|
327
|
+
this.#setCleanupTimeout(now)
|
|
301
328
|
this.#setState({
|
|
302
329
|
successCount: this.store.state.successCount + 1,
|
|
303
330
|
lastResult: result,
|
|
@@ -322,7 +349,7 @@ export class AsyncRateLimiter<TFn extends AnyAsyncFunction> {
|
|
|
322
349
|
return this.store.state.lastResult
|
|
323
350
|
}
|
|
324
351
|
|
|
325
|
-
#
|
|
352
|
+
#getExecutionTimesInWindow = (): Array<number> => {
|
|
326
353
|
if (this.options.windowType === 'sliding') {
|
|
327
354
|
// For sliding window, return all executions within the current window
|
|
328
355
|
return this.store.state.executionTimes.filter(
|
|
@@ -331,22 +358,54 @@ export class AsyncRateLimiter<TFn extends AnyAsyncFunction> {
|
|
|
331
358
|
} else {
|
|
332
359
|
// For fixed window, return all executions in the current window
|
|
333
360
|
// The window starts from the oldest execution time
|
|
361
|
+
if (this.store.state.executionTimes.length === 0) {
|
|
362
|
+
return []
|
|
363
|
+
}
|
|
334
364
|
const oldestExecution = Math.min(...this.store.state.executionTimes)
|
|
335
365
|
const windowStart = oldestExecution
|
|
366
|
+
const windowEnd = windowStart + this.#getWindow()
|
|
367
|
+
const now = Date.now()
|
|
368
|
+
|
|
369
|
+
// If the window has expired, return empty array
|
|
370
|
+
if (now > windowEnd) {
|
|
371
|
+
return []
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// Otherwise, return all executions in the current window
|
|
336
375
|
return this.store.state.executionTimes.filter(
|
|
337
|
-
(time) =>
|
|
338
|
-
time >= windowStart && time <= windowStart + this.#getWindow(),
|
|
376
|
+
(time) => time >= windowStart && time <= windowEnd,
|
|
339
377
|
)
|
|
340
378
|
}
|
|
341
379
|
}
|
|
342
380
|
|
|
381
|
+
#setCleanupTimeout = (executionTime: number): void => {
|
|
382
|
+
if (
|
|
383
|
+
this.options.windowType === 'sliding' ||
|
|
384
|
+
this.#timeoutIds.size === 0 // new fixed window
|
|
385
|
+
) {
|
|
386
|
+
const now = Date.now()
|
|
387
|
+
const timeUntilExpiration = executionTime - now + this.#getWindow() + 1
|
|
388
|
+
const timeoutId = setTimeout(() => {
|
|
389
|
+
this.#cleanupOldExecutions()
|
|
390
|
+
this.#clearTimeout(timeoutId)
|
|
391
|
+
}, timeUntilExpiration)
|
|
392
|
+
this.#timeoutIds.add(timeoutId)
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
#clearTimeout = (timeoutId: NodeJS.Timeout): void => {
|
|
397
|
+
clearTimeout(timeoutId)
|
|
398
|
+
this.#timeoutIds.delete(timeoutId)
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
#clearTimeouts = (): void => {
|
|
402
|
+
this.#timeoutIds.forEach((timeoutId) => clearTimeout(timeoutId))
|
|
403
|
+
this.#timeoutIds.clear()
|
|
404
|
+
}
|
|
405
|
+
|
|
343
406
|
#cleanupOldExecutions = (): void => {
|
|
344
|
-
const now = Date.now()
|
|
345
|
-
const windowStart = now - this.#getWindow()
|
|
346
407
|
this.#setState({
|
|
347
|
-
executionTimes: this
|
|
348
|
-
(time) => time > windowStart,
|
|
349
|
-
),
|
|
408
|
+
executionTimes: this.#getExecutionTimesInWindow(),
|
|
350
409
|
})
|
|
351
410
|
}
|
|
352
411
|
|
|
@@ -354,7 +413,7 @@ export class AsyncRateLimiter<TFn extends AnyAsyncFunction> {
|
|
|
354
413
|
* Returns the number of remaining executions allowed in the current window
|
|
355
414
|
*/
|
|
356
415
|
getRemainingInWindow = (): number => {
|
|
357
|
-
const relevantExecutionTimes = this.#
|
|
416
|
+
const relevantExecutionTimes = this.#getExecutionTimesInWindow()
|
|
358
417
|
return Math.max(0, this.#getLimit() - relevantExecutionTimes.length)
|
|
359
418
|
}
|
|
360
419
|
|
|
@@ -376,6 +435,7 @@ export class AsyncRateLimiter<TFn extends AnyAsyncFunction> {
|
|
|
376
435
|
*/
|
|
377
436
|
reset = (): void => {
|
|
378
437
|
this.#setState(getDefaultAsyncRateLimiterState())
|
|
438
|
+
this.#clearTimeouts()
|
|
379
439
|
}
|
|
380
440
|
}
|
|
381
441
|
|
package/src/async-throttler.ts
CHANGED
|
@@ -187,6 +187,7 @@ export class AsyncThrottler<TFn extends AnyAsyncFunction> {
|
|
|
187
187
|
#resolvePreviousPromise:
|
|
188
188
|
| ((value?: ReturnType<TFn> | undefined) => void)
|
|
189
189
|
| null = null
|
|
190
|
+
#rejectPreviousPromise: ((reason?: unknown) => void) | null = null
|
|
190
191
|
|
|
191
192
|
constructor(
|
|
192
193
|
private fn: TFn,
|
|
@@ -287,8 +288,9 @@ export class AsyncThrottler<TFn extends AnyAsyncFunction> {
|
|
|
287
288
|
await this.#execute(...args)
|
|
288
289
|
return this.store.state.lastResult
|
|
289
290
|
} else {
|
|
290
|
-
return new Promise((resolve) => {
|
|
291
|
+
return new Promise((resolve, reject) => {
|
|
291
292
|
this.#resolvePreviousPromise = resolve
|
|
293
|
+
this.#rejectPreviousPromise = reject
|
|
292
294
|
// Clear any existing timeout to ensure we use the latest arguments
|
|
293
295
|
this.#clearTimeout()
|
|
294
296
|
|
|
@@ -330,9 +332,7 @@ export class AsyncThrottler<TFn extends AnyAsyncFunction> {
|
|
|
330
332
|
})
|
|
331
333
|
this.options.onError?.(error, this)
|
|
332
334
|
if (this.options.throwOnError) {
|
|
333
|
-
|
|
334
|
-
} else {
|
|
335
|
-
console.error(error)
|
|
335
|
+
this.#rejectPreviousPromiseInternal(error)
|
|
336
336
|
}
|
|
337
337
|
} finally {
|
|
338
338
|
const lastExecutionTime = Date.now()
|
|
@@ -353,12 +353,18 @@ export class AsyncThrottler<TFn extends AnyAsyncFunction> {
|
|
|
353
353
|
/**
|
|
354
354
|
* Processes the current pending execution immediately
|
|
355
355
|
*/
|
|
356
|
-
flush = ():
|
|
356
|
+
flush = async (): Promise<ReturnType<TFn> | undefined> => {
|
|
357
357
|
if (this.store.state.isPending && this.store.state.lastArgs) {
|
|
358
358
|
this.#abortExecution() // abort any current execution
|
|
359
359
|
this.#clearTimeout() // clear any existing timeout
|
|
360
|
-
this.#execute(...this.store.state.lastArgs)
|
|
360
|
+
const result = await this.#execute(...this.store.state.lastArgs)
|
|
361
|
+
|
|
362
|
+
// Resolve any pending promise from maybeExecute
|
|
363
|
+
this.#resolvePreviousPromiseInternal()
|
|
364
|
+
|
|
365
|
+
return result
|
|
361
366
|
}
|
|
367
|
+
return undefined
|
|
362
368
|
}
|
|
363
369
|
|
|
364
370
|
#resolvePreviousPromiseInternal = (): void => {
|
|
@@ -368,6 +374,13 @@ export class AsyncThrottler<TFn extends AnyAsyncFunction> {
|
|
|
368
374
|
}
|
|
369
375
|
}
|
|
370
376
|
|
|
377
|
+
#rejectPreviousPromiseInternal = (error: unknown): void => {
|
|
378
|
+
if (this.#rejectPreviousPromise) {
|
|
379
|
+
this.#rejectPreviousPromise(error)
|
|
380
|
+
this.#rejectPreviousPromise = null
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
371
384
|
#clearTimeout = (): void => {
|
|
372
385
|
if (this.#timeoutId) {
|
|
373
386
|
clearTimeout(this.#timeoutId)
|
package/src/batcher.ts
CHANGED
|
@@ -15,14 +15,6 @@ export interface BatcherState<TValue> {
|
|
|
15
15
|
* Whether the batcher is waiting for the timeout to trigger batch processing
|
|
16
16
|
*/
|
|
17
17
|
isPending: boolean
|
|
18
|
-
/**
|
|
19
|
-
* Whether the batcher is active and will process items automatically
|
|
20
|
-
*/
|
|
21
|
-
isRunning: boolean
|
|
22
|
-
/**
|
|
23
|
-
* Total number of items that have been processed across all batches
|
|
24
|
-
*/
|
|
25
|
-
totalItemsProcessed: number
|
|
26
18
|
/**
|
|
27
19
|
* Array of items currently queued for batch processing
|
|
28
20
|
*/
|
|
@@ -35,6 +27,10 @@ export interface BatcherState<TValue> {
|
|
|
35
27
|
* Current processing status - 'idle' when not processing, 'pending' when waiting for timeout
|
|
36
28
|
*/
|
|
37
29
|
status: 'idle' | 'pending'
|
|
30
|
+
/**
|
|
31
|
+
* Total number of items that have been processed across all batches
|
|
32
|
+
*/
|
|
33
|
+
totalItemsProcessed: number
|
|
38
34
|
}
|
|
39
35
|
|
|
40
36
|
function getDefaultBatcherState<TValue>(): BatcherState<TValue> {
|
|
@@ -42,7 +38,6 @@ function getDefaultBatcherState<TValue>(): BatcherState<TValue> {
|
|
|
42
38
|
executionCount: 0,
|
|
43
39
|
isEmpty: true,
|
|
44
40
|
isPending: false,
|
|
45
|
-
isRunning: true,
|
|
46
41
|
totalItemsProcessed: 0,
|
|
47
42
|
items: [],
|
|
48
43
|
size: 0,
|
|
@@ -204,7 +199,7 @@ export class Batcher<TValue> {
|
|
|
204
199
|
|
|
205
200
|
if (shouldProcess) {
|
|
206
201
|
this.#execute()
|
|
207
|
-
} else if (this.
|
|
202
|
+
} else if (this.options.wait !== Infinity) {
|
|
208
203
|
this.#clearTimeout() // clear any pending timeout to replace it with a new one
|
|
209
204
|
this.#timeoutId = setTimeout(() => this.#execute(), this.#getWait())
|
|
210
205
|
}
|
|
@@ -244,24 +239,6 @@ export class Batcher<TValue> {
|
|
|
244
239
|
this.#execute() // execute immediately
|
|
245
240
|
}
|
|
246
241
|
|
|
247
|
-
/**
|
|
248
|
-
* Stops the batcher from processing batches
|
|
249
|
-
*/
|
|
250
|
-
stop = (): void => {
|
|
251
|
-
this.#setState({ isRunning: false })
|
|
252
|
-
this.#clearTimeout()
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
/**
|
|
256
|
-
* Starts the batcher and processes any pending items
|
|
257
|
-
*/
|
|
258
|
-
start = (): void => {
|
|
259
|
-
this.#setState({ isRunning: true })
|
|
260
|
-
if (this.store.state.items.length > 0) {
|
|
261
|
-
this.#execute()
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
|
|
265
242
|
/**
|
|
266
243
|
* Returns a copy of all items in the batcher
|
|
267
244
|
*/
|
package/src/queuer.ts
CHANGED
|
@@ -26,14 +26,14 @@ export interface QueuerState<TValue> {
|
|
|
26
26
|
* Whether the queuer is active and will process items automatically
|
|
27
27
|
*/
|
|
28
28
|
isRunning: boolean
|
|
29
|
-
/**
|
|
30
|
-
* Timestamps when items were added to the queue for expiration tracking
|
|
31
|
-
*/
|
|
32
|
-
itemTimestamps: Array<number>
|
|
33
29
|
/**
|
|
34
30
|
* Array of items currently waiting to be processed
|
|
35
31
|
*/
|
|
36
32
|
items: Array<TValue>
|
|
33
|
+
/**
|
|
34
|
+
* Timestamps when items were added to the queue for expiration tracking
|
|
35
|
+
*/
|
|
36
|
+
itemTimestamps: Array<number>
|
|
37
37
|
/**
|
|
38
38
|
* Whether the queuer has a pending timeout for processing the next item
|
|
39
39
|
*/
|
|
@@ -112,14 +112,14 @@ export interface QueuerOptions<TValue> {
|
|
|
112
112
|
* Maximum number of items allowed in the queuer
|
|
113
113
|
*/
|
|
114
114
|
maxSize?: number
|
|
115
|
-
/**
|
|
116
|
-
* Callback fired whenever an item expires in the queuer
|
|
117
|
-
*/
|
|
118
|
-
onExpire?: (item: TValue, queuer: Queuer<TValue>) => void
|
|
119
115
|
/**
|
|
120
116
|
* Callback fired whenever an item is removed from the queuer
|
|
121
117
|
*/
|
|
122
118
|
onExecute?: (item: TValue, queuer: Queuer<TValue>) => void
|
|
119
|
+
/**
|
|
120
|
+
* Callback fired whenever an item expires in the queuer
|
|
121
|
+
*/
|
|
122
|
+
onExpire?: (item: TValue, queuer: Queuer<TValue>) => void
|
|
123
123
|
/**
|
|
124
124
|
* Callback fired whenever an item is added or removed from the queuer
|
|
125
125
|
*/
|
|
@@ -331,7 +331,7 @@ export class Queuer<TValue> {
|
|
|
331
331
|
// Check for expired items
|
|
332
332
|
this.#checkExpiredItems()
|
|
333
333
|
|
|
334
|
-
while (
|
|
334
|
+
while (this.store.state.items.length > 0) {
|
|
335
335
|
const nextItem = this.execute(this.options.getItemsFrom ?? 'front')
|
|
336
336
|
if (nextItem === undefined) {
|
|
337
337
|
break
|
|
@@ -366,7 +366,7 @@ export class Queuer<TValue> {
|
|
|
366
366
|
position: QueuePosition = this.options.addItemsTo ?? 'back',
|
|
367
367
|
runOnItemsChange: boolean = true,
|
|
368
368
|
): boolean => {
|
|
369
|
-
if (this.store.state.
|
|
369
|
+
if (this.store.state.items.length >= (this.options.maxSize ?? Infinity)) {
|
|
370
370
|
this.#setState({
|
|
371
371
|
rejectionCount: this.store.state.rejectionCount + 1,
|
|
372
372
|
})
|
|
@@ -472,6 +472,12 @@ export class Queuer<TValue> {
|
|
|
472
472
|
return item
|
|
473
473
|
}
|
|
474
474
|
|
|
475
|
+
#getAllItems = (): Array<TValue> => {
|
|
476
|
+
const items = this.peekAllItems()
|
|
477
|
+
this.clear()
|
|
478
|
+
return items
|
|
479
|
+
}
|
|
480
|
+
|
|
475
481
|
/**
|
|
476
482
|
* Removes and returns the next item from the queue and processes it using the provided function.
|
|
477
483
|
*
|
|
@@ -506,6 +512,17 @@ export class Queuer<TValue> {
|
|
|
506
512
|
for (let i = 0; i < numberOfItems; i++) {
|
|
507
513
|
this.execute(position)
|
|
508
514
|
}
|
|
515
|
+
this.#tick()
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
/**
|
|
519
|
+
* Processes all items in the queue as a batch using the provided function
|
|
520
|
+
* The queue is cleared after processing
|
|
521
|
+
*/
|
|
522
|
+
flushAsBatch = (batchFunction: (items: Array<TValue>) => void): void => {
|
|
523
|
+
const items = this.#getAllItems()
|
|
524
|
+
this.clear()
|
|
525
|
+
batchFunction(items)
|
|
509
526
|
}
|
|
510
527
|
|
|
511
528
|
/**
|
|
@@ -579,7 +596,7 @@ export class Queuer<TValue> {
|
|
|
579
596
|
if (position === 'front') {
|
|
580
597
|
return this.store.state.items[0]
|
|
581
598
|
}
|
|
582
|
-
return this.store.state.items[this.store.state.
|
|
599
|
+
return this.store.state.items[this.store.state.items.length - 1]
|
|
583
600
|
}
|
|
584
601
|
|
|
585
602
|
/**
|
|
@@ -594,7 +611,7 @@ export class Queuer<TValue> {
|
|
|
594
611
|
*/
|
|
595
612
|
start = () => {
|
|
596
613
|
this.#setState({ isRunning: true })
|
|
597
|
-
if (!this.store.state.pendingTick &&
|
|
614
|
+
if (!this.store.state.pendingTick && this.store.state.items.length > 0) {
|
|
598
615
|
this.#tick()
|
|
599
616
|
}
|
|
600
617
|
}
|