@tanstack/pacer 0.9.1 → 0.11.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 +26 -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 +15 -4
- 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 +23 -7
- package/dist/cjs/async-throttler.cjs.map +1 -1
- package/dist/cjs/async-throttler.d.cts +2 -2
- 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/debouncer.cjs +2 -1
- package/dist/cjs/debouncer.cjs.map +1 -1
- package/dist/cjs/queuer.cjs +11 -0
- 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 +5 -0
- package/dist/cjs/throttler.cjs.map +1 -1
- package/dist/cjs/throttler.d.cts +5 -5
- 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 +26 -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 +15 -4
- 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 +2 -2
- package/dist/esm/async-throttler.js +23 -7
- 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/debouncer.js +2 -1
- package/dist/esm/debouncer.js.map +1 -1
- package/dist/esm/queuer.d.ts +13 -8
- package/dist/esm/queuer.js +11 -0
- 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 +5 -5
- package/dist/esm/throttler.js +5 -0
- 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 +28 -8
- package/src/async-queuer.ts +27 -9
- package/src/async-rate-limiter.ts +72 -12
- package/src/async-throttler.ts +26 -8
- package/src/batcher.ts +5 -28
- package/src/debouncer.ts +2 -1
- package/src/queuer.ts +24 -8
- package/src/rate-limiter.ts +71 -11
- package/src/throttler.ts +10 -5
package/src/async-batcher.ts
CHANGED
|
@@ -23,10 +23,6 @@ export interface AsyncBatcherState<TValue> {
|
|
|
23
23
|
* Whether the batcher is waiting for the timeout to trigger batch processing
|
|
24
24
|
*/
|
|
25
25
|
isPending: boolean
|
|
26
|
-
/**
|
|
27
|
-
* Whether the batcher is active and will process items automatically
|
|
28
|
-
*/
|
|
29
|
-
isRunning: boolean
|
|
30
26
|
/**
|
|
31
27
|
* Array of items currently queued for batch processing
|
|
32
28
|
*/
|
|
@@ -51,14 +47,14 @@ export interface AsyncBatcherState<TValue> {
|
|
|
51
47
|
* Number of batch executions that have completed successfully
|
|
52
48
|
*/
|
|
53
49
|
successCount: number
|
|
54
|
-
/**
|
|
55
|
-
* Total number of items that have been processed across all batches
|
|
56
|
-
*/
|
|
57
|
-
totalItemsProcessed: number
|
|
58
50
|
/**
|
|
59
51
|
* Total number of items that have failed processing across all batches
|
|
60
52
|
*/
|
|
61
53
|
totalItemsFailed: number
|
|
54
|
+
/**
|
|
55
|
+
* Total number of items that have been processed across all batches
|
|
56
|
+
*/
|
|
57
|
+
totalItemsProcessed: number
|
|
62
58
|
}
|
|
63
59
|
|
|
64
60
|
function getDefaultAsyncBatcherState<TValue>(): AsyncBatcherState<TValue> {
|
|
@@ -68,7 +64,6 @@ function getDefaultAsyncBatcherState<TValue>(): AsyncBatcherState<TValue> {
|
|
|
68
64
|
isEmpty: true,
|
|
69
65
|
isExecuting: false,
|
|
70
66
|
isPending: false,
|
|
71
|
-
isRunning: true,
|
|
72
67
|
items: [],
|
|
73
68
|
lastResult: undefined,
|
|
74
69
|
settleCount: 0,
|
|
@@ -297,7 +292,7 @@ export class AsyncBatcher<TValue> {
|
|
|
297
292
|
|
|
298
293
|
if (shouldProcess) {
|
|
299
294
|
this.#execute()
|
|
300
|
-
} else if (this.
|
|
295
|
+
} else if (this.options.wait !== Infinity) {
|
|
301
296
|
this.#clearTimeout() // clear any pending timeout to replace it with a new one
|
|
302
297
|
this.#timeoutId = setTimeout(() => this.#execute(), this.#getWait())
|
|
303
298
|
}
|
|
@@ -365,24 +360,6 @@ export class AsyncBatcher<TValue> {
|
|
|
365
360
|
return await this.#execute()
|
|
366
361
|
}
|
|
367
362
|
|
|
368
|
-
/**
|
|
369
|
-
* Stops the async batcher from processing batches
|
|
370
|
-
*/
|
|
371
|
-
stop = (): void => {
|
|
372
|
-
this.#setState({ isRunning: false })
|
|
373
|
-
this.#clearTimeout()
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
/**
|
|
377
|
-
* Starts the async batcher and processes any pending items
|
|
378
|
-
*/
|
|
379
|
-
start = (): void => {
|
|
380
|
-
this.#setState({ isRunning: true })
|
|
381
|
-
if (this.store.state.items.length > 0 && !this.#timeoutId) {
|
|
382
|
-
this.#timeoutId = setTimeout(() => this.#execute(), this.#getWait())
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
|
|
386
363
|
/**
|
|
387
364
|
* Returns a copy of all items in the async batcher
|
|
388
365
|
*/
|
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,12 +307,13 @@ 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({
|
|
312
314
|
isExecuting: false,
|
|
313
315
|
isPending: false,
|
|
316
|
+
lastArgs: undefined,
|
|
314
317
|
settleCount: this.store.state.settleCount + 1,
|
|
315
318
|
})
|
|
316
319
|
this.#abortController = null
|
|
@@ -322,11 +325,31 @@ export class AsyncDebouncer<TFn extends AnyAsyncFunction> {
|
|
|
322
325
|
/**
|
|
323
326
|
* Processes the current pending execution immediately
|
|
324
327
|
*/
|
|
325
|
-
flush = ():
|
|
328
|
+
flush = async (): Promise<ReturnType<TFn> | undefined> => {
|
|
326
329
|
if (this.store.state.isPending && this.store.state.lastArgs) {
|
|
327
330
|
this.#abortExecution() // abort any current execution
|
|
328
331
|
this.#clearTimeout() // clear any existing timeout
|
|
329
|
-
this.#execute(...this.store.state.lastArgs)
|
|
332
|
+
const result = await this.#execute(...this.store.state.lastArgs)
|
|
333
|
+
|
|
334
|
+
// Resolve any pending promise from maybeExecute
|
|
335
|
+
this.#resolvePreviousPromiseInternal()
|
|
336
|
+
|
|
337
|
+
return result
|
|
338
|
+
}
|
|
339
|
+
return undefined
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
#resolvePreviousPromiseInternal = (): void => {
|
|
343
|
+
if (this.#resolvePreviousPromise) {
|
|
344
|
+
this.#resolvePreviousPromise(this.store.state.lastResult)
|
|
345
|
+
this.#resolvePreviousPromise = null
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
#rejectPreviousPromiseInternal = (error: unknown): void => {
|
|
350
|
+
if (this.#rejectPreviousPromise) {
|
|
351
|
+
this.#rejectPreviousPromise(error)
|
|
352
|
+
this.#rejectPreviousPromise = null
|
|
330
353
|
}
|
|
331
354
|
}
|
|
332
355
|
|
|
@@ -339,10 +362,7 @@ export class AsyncDebouncer<TFn extends AnyAsyncFunction> {
|
|
|
339
362
|
|
|
340
363
|
#cancelPendingExecution = (): void => {
|
|
341
364
|
this.#clearTimeout()
|
|
342
|
-
|
|
343
|
-
this.#resolvePreviousPromise(this.store.state.lastResult)
|
|
344
|
-
this.#resolvePreviousPromise = null
|
|
345
|
-
}
|
|
365
|
+
this.#resolvePreviousPromiseInternal()
|
|
346
366
|
this.#setState({
|
|
347
367
|
isPending: false,
|
|
348
368
|
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
|
*/
|
|
@@ -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
|
/**
|
|
@@ -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
|
@@ -30,7 +30,7 @@ export interface AsyncThrottlerState<TFn extends AnyAsyncFunction> {
|
|
|
30
30
|
/**
|
|
31
31
|
* Timestamp when the next execution can occur in milliseconds
|
|
32
32
|
*/
|
|
33
|
-
nextExecutionTime: number
|
|
33
|
+
nextExecutionTime: number | undefined
|
|
34
34
|
/**
|
|
35
35
|
* Number of function executions that have completed (either successfully or with errors)
|
|
36
36
|
*/
|
|
@@ -55,7 +55,7 @@ function getDefaultAsyncThrottlerState<
|
|
|
55
55
|
lastArgs: undefined,
|
|
56
56
|
lastExecutionTime: 0,
|
|
57
57
|
lastResult: undefined,
|
|
58
|
-
nextExecutionTime:
|
|
58
|
+
nextExecutionTime: undefined,
|
|
59
59
|
settleCount: 0,
|
|
60
60
|
status: 'idle',
|
|
61
61
|
successCount: 0,
|
|
@@ -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()
|
|
@@ -346,6 +346,11 @@ export class AsyncThrottler<TFn extends AnyAsyncFunction> {
|
|
|
346
346
|
})
|
|
347
347
|
this.#abortController = null
|
|
348
348
|
this.options.onSettled?.(this)
|
|
349
|
+
setTimeout(() => {
|
|
350
|
+
if (!this.store.state.isPending) {
|
|
351
|
+
this.#setState({ nextExecutionTime: undefined })
|
|
352
|
+
}
|
|
353
|
+
}, this.#getWait())
|
|
349
354
|
}
|
|
350
355
|
return this.store.state.lastResult
|
|
351
356
|
}
|
|
@@ -353,12 +358,18 @@ export class AsyncThrottler<TFn extends AnyAsyncFunction> {
|
|
|
353
358
|
/**
|
|
354
359
|
* Processes the current pending execution immediately
|
|
355
360
|
*/
|
|
356
|
-
flush = ():
|
|
361
|
+
flush = async (): Promise<ReturnType<TFn> | undefined> => {
|
|
357
362
|
if (this.store.state.isPending && this.store.state.lastArgs) {
|
|
358
363
|
this.#abortExecution() // abort any current execution
|
|
359
364
|
this.#clearTimeout() // clear any existing timeout
|
|
360
|
-
this.#execute(...this.store.state.lastArgs)
|
|
365
|
+
const result = await this.#execute(...this.store.state.lastArgs)
|
|
366
|
+
|
|
367
|
+
// Resolve any pending promise from maybeExecute
|
|
368
|
+
this.#resolvePreviousPromiseInternal()
|
|
369
|
+
|
|
370
|
+
return result
|
|
361
371
|
}
|
|
372
|
+
return undefined
|
|
362
373
|
}
|
|
363
374
|
|
|
364
375
|
#resolvePreviousPromiseInternal = (): void => {
|
|
@@ -368,6 +379,13 @@ export class AsyncThrottler<TFn extends AnyAsyncFunction> {
|
|
|
368
379
|
}
|
|
369
380
|
}
|
|
370
381
|
|
|
382
|
+
#rejectPreviousPromiseInternal = (error: unknown): void => {
|
|
383
|
+
if (this.#rejectPreviousPromise) {
|
|
384
|
+
this.#rejectPreviousPromise(error)
|
|
385
|
+
this.#rejectPreviousPromise = null
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
371
389
|
#clearTimeout = (): void => {
|
|
372
390
|
if (this.#timeoutId) {
|
|
373
391
|
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/debouncer.ts
CHANGED
|
@@ -213,8 +213,9 @@ export class Debouncer<TFn extends AnyFunction> {
|
|
|
213
213
|
if (!this.#getEnabled()) return undefined
|
|
214
214
|
this.fn(...args) // EXECUTE!
|
|
215
215
|
this.#setState({
|
|
216
|
-
isPending: false,
|
|
217
216
|
executionCount: this.store.state.executionCount + 1,
|
|
217
|
+
isPending: false,
|
|
218
|
+
lastArgs: undefined,
|
|
218
219
|
})
|
|
219
220
|
this.options.onExecute?.(this)
|
|
220
221
|
}
|
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
|
*/
|
|
@@ -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
|
*
|
|
@@ -509,6 +515,16 @@ export class Queuer<TValue> {
|
|
|
509
515
|
this.#tick()
|
|
510
516
|
}
|
|
511
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)
|
|
526
|
+
}
|
|
527
|
+
|
|
512
528
|
/**
|
|
513
529
|
* Checks for expired items in the queue and removes them. Calls onExpire for each expired item.
|
|
514
530
|
* Internal use only.
|