@tanstack/pacer 0.15.3 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/async-batcher.cjs +66 -4
- package/dist/cjs/async-batcher.cjs.map +1 -1
- package/dist/cjs/async-batcher.d.cts +97 -16
- package/dist/cjs/async-debouncer.cjs +53 -18
- package/dist/cjs/async-debouncer.cjs.map +1 -1
- package/dist/cjs/async-debouncer.d.cts +79 -9
- package/dist/cjs/async-queuer.cjs +58 -1
- package/dist/cjs/async-queuer.cjs.map +1 -1
- package/dist/cjs/async-queuer.d.cts +103 -9
- package/dist/cjs/async-rate-limiter.cjs +51 -1
- package/dist/cjs/async-rate-limiter.cjs.map +1 -1
- package/dist/cjs/async-rate-limiter.d.cts +94 -27
- package/dist/cjs/async-retryer.cjs +286 -0
- package/dist/cjs/async-retryer.cjs.map +1 -0
- package/dist/cjs/async-retryer.d.cts +312 -0
- package/dist/cjs/async-throttler.cjs +101 -52
- package/dist/cjs/async-throttler.cjs.map +1 -1
- package/dist/cjs/async-throttler.d.cts +85 -9
- package/dist/cjs/batcher.cjs +5 -0
- package/dist/cjs/batcher.cjs.map +1 -1
- package/dist/cjs/batcher.d.cts +13 -1
- package/dist/cjs/debouncer.cjs +5 -0
- package/dist/cjs/debouncer.cjs.map +1 -1
- package/dist/cjs/debouncer.d.cts +10 -2
- package/dist/cjs/event-client.cjs.map +1 -1
- package/dist/cjs/event-client.d.cts +3 -0
- package/dist/cjs/index.cjs +13 -0
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/index.d.cts +1 -0
- package/dist/cjs/queuer.cjs +5 -0
- package/dist/cjs/queuer.cjs.map +1 -1
- package/dist/cjs/queuer.d.cts +11 -3
- package/dist/cjs/rate-limiter.cjs +5 -0
- package/dist/cjs/rate-limiter.cjs.map +1 -1
- package/dist/cjs/rate-limiter.d.cts +11 -0
- package/dist/cjs/throttler.cjs +5 -0
- package/dist/cjs/throttler.cjs.map +1 -1
- package/dist/cjs/throttler.d.cts +11 -0
- package/dist/cjs/utils.cjs.map +1 -1
- package/dist/esm/async-batcher.d.ts +97 -16
- package/dist/esm/async-batcher.js +67 -5
- package/dist/esm/async-batcher.js.map +1 -1
- package/dist/esm/async-debouncer.d.ts +79 -9
- package/dist/esm/async-debouncer.js +54 -19
- package/dist/esm/async-debouncer.js.map +1 -1
- package/dist/esm/async-queuer.d.ts +103 -9
- package/dist/esm/async-queuer.js +59 -2
- package/dist/esm/async-queuer.js.map +1 -1
- package/dist/esm/async-rate-limiter.d.ts +94 -27
- package/dist/esm/async-rate-limiter.js +52 -2
- package/dist/esm/async-rate-limiter.js.map +1 -1
- package/dist/esm/async-retryer.d.ts +312 -0
- package/dist/esm/async-retryer.js +286 -0
- package/dist/esm/async-retryer.js.map +1 -0
- package/dist/esm/async-throttler.d.ts +85 -9
- package/dist/esm/async-throttler.js +102 -53
- package/dist/esm/async-throttler.js.map +1 -1
- package/dist/esm/batcher.d.ts +13 -1
- package/dist/esm/batcher.js +5 -0
- package/dist/esm/batcher.js.map +1 -1
- package/dist/esm/debouncer.d.ts +10 -2
- package/dist/esm/debouncer.js +6 -1
- package/dist/esm/debouncer.js.map +1 -1
- package/dist/esm/event-client.d.ts +3 -0
- package/dist/esm/event-client.js.map +1 -1
- package/dist/esm/index.d.ts +1 -0
- package/dist/esm/index.js +23 -10
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/queuer.d.ts +11 -3
- package/dist/esm/queuer.js +6 -1
- package/dist/esm/queuer.js.map +1 -1
- package/dist/esm/rate-limiter.d.ts +11 -0
- package/dist/esm/rate-limiter.js +6 -1
- package/dist/esm/rate-limiter.js.map +1 -1
- package/dist/esm/throttler.d.ts +11 -0
- package/dist/esm/throttler.js +6 -1
- package/dist/esm/throttler.js.map +1 -1
- package/dist/esm/utils.js.map +1 -1
- package/package.json +13 -3
- package/src/async-batcher.ts +146 -19
- package/src/async-debouncer.ts +120 -30
- package/src/async-queuer.ts +149 -11
- package/src/async-rate-limiter.ts +131 -30
- package/src/async-retryer.ts +669 -0
- package/src/async-throttler.ts +198 -72
- package/src/batcher.ts +18 -1
- package/src/debouncer.ts +19 -2
- package/src/event-client.ts +3 -0
- package/src/index.ts +2 -0
- package/src/queuer.ts +21 -3
- package/src/rate-limiter.ts +20 -0
- package/src/throttler.ts +20 -0
package/src/async-throttler.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { Store } from '@tanstack/store'
|
|
2
|
+
import { AsyncRetryer } from './async-retryer'
|
|
2
3
|
import { createKey, parseFunctionOrValue } from './utils'
|
|
3
4
|
import { emitChange, pacerEventClient } from './event-client'
|
|
5
|
+
import type { AsyncRetryerOptions } from './async-retryer'
|
|
4
6
|
import type { AnyAsyncFunction, OptionalKeys } from './types'
|
|
5
7
|
|
|
6
8
|
export interface AsyncThrottlerState<TFn extends AnyAsyncFunction> {
|
|
@@ -72,6 +74,10 @@ function getDefaultAsyncThrottlerState<
|
|
|
72
74
|
* Options for configuring an async throttled function
|
|
73
75
|
*/
|
|
74
76
|
export interface AsyncThrottlerOptions<TFn extends AnyAsyncFunction> {
|
|
77
|
+
/**
|
|
78
|
+
* Options for configuring the underlying async retryer
|
|
79
|
+
*/
|
|
80
|
+
asyncRetryerOptions?: AsyncRetryerOptions<TFn>
|
|
75
81
|
/**
|
|
76
82
|
* Whether the throttler is enabled. When disabled, maybeExecute will not trigger any executions.
|
|
77
83
|
* Can be a boolean or a function that returns a boolean.
|
|
@@ -98,7 +104,7 @@ export interface AsyncThrottlerOptions<TFn extends AnyAsyncFunction> {
|
|
|
98
104
|
* This can be used alongside throwOnError - the handler will be called before any error is thrown.
|
|
99
105
|
*/
|
|
100
106
|
onError?: (
|
|
101
|
-
error:
|
|
107
|
+
error: Error,
|
|
102
108
|
args: Parameters<TFn>,
|
|
103
109
|
asyncThrottler: AsyncThrottler<TFn>,
|
|
104
110
|
) => void
|
|
@@ -136,12 +142,27 @@ export interface AsyncThrottlerOptions<TFn extends AnyAsyncFunction> {
|
|
|
136
142
|
wait: number | ((throttler: AsyncThrottler<TFn>) => number)
|
|
137
143
|
}
|
|
138
144
|
|
|
145
|
+
/**
|
|
146
|
+
* Utility function for sharing common `AsyncThrottlerOptions` options between different `AsyncThrottler` instances.
|
|
147
|
+
*/
|
|
148
|
+
export function asyncThrottlerOptions<
|
|
149
|
+
TFn extends AnyAsyncFunction = AnyAsyncFunction,
|
|
150
|
+
TOptions extends Partial<AsyncThrottlerOptions<TFn>> = Partial<
|
|
151
|
+
AsyncThrottlerOptions<TFn>
|
|
152
|
+
>,
|
|
153
|
+
>(options: TOptions): TOptions {
|
|
154
|
+
return options
|
|
155
|
+
}
|
|
156
|
+
|
|
139
157
|
type AsyncThrottlerOptionsWithOptionalCallbacks = OptionalKeys<
|
|
140
158
|
AsyncThrottlerOptions<any>,
|
|
141
159
|
'initialState' | 'onError' | 'onSettled' | 'onSuccess'
|
|
142
160
|
>
|
|
143
161
|
|
|
144
162
|
const defaultOptions: AsyncThrottlerOptionsWithOptionalCallbacks = {
|
|
163
|
+
asyncRetryerOptions: {
|
|
164
|
+
maxAttempts: 1,
|
|
165
|
+
},
|
|
145
166
|
enabled: true,
|
|
146
167
|
leading: true,
|
|
147
168
|
trailing: true,
|
|
@@ -151,14 +172,24 @@ const defaultOptions: AsyncThrottlerOptionsWithOptionalCallbacks = {
|
|
|
151
172
|
/**
|
|
152
173
|
* A class that creates an async throttled function.
|
|
153
174
|
*
|
|
175
|
+
* Async vs Sync Versions:
|
|
176
|
+
* The async version provides advanced features over the sync Throttler:
|
|
177
|
+
* - Returns promises that can be awaited for throttled function results
|
|
178
|
+
* - Built-in retry support via AsyncRetryer integration
|
|
179
|
+
* - Abort support to cancel in-flight executions
|
|
180
|
+
* - Cancel support to prevent pending executions from starting
|
|
181
|
+
* - Comprehensive error handling with onError callbacks and throwOnError control
|
|
182
|
+
* - Detailed execution tracking (success/error/settle counts)
|
|
183
|
+
* - Waits for ongoing executions to complete before scheduling the next one
|
|
184
|
+
*
|
|
185
|
+
* The sync Throttler is lighter weight and simpler when you don't need async features,
|
|
186
|
+
* return values, or execution control.
|
|
187
|
+
*
|
|
188
|
+
* What is Throttling?
|
|
154
189
|
* Throttling limits how often a function can be executed, allowing only one execution within a specified time window.
|
|
155
190
|
* Unlike debouncing which resets the delay timer on each call, throttling ensures the function executes at a
|
|
156
191
|
* regular interval regardless of how often it's called.
|
|
157
192
|
*
|
|
158
|
-
* Unlike the non-async Throttler, this async version supports returning values from the throttled function,
|
|
159
|
-
* making it ideal for API calls and other async operations where you want the result of the `maybeExecute` call
|
|
160
|
-
* instead of setting the result on a state variable from within the throttled function.
|
|
161
|
-
*
|
|
162
193
|
* This is useful for rate-limiting API calls, handling scroll/resize events, or any scenario where you want to
|
|
163
194
|
* ensure a maximum execution frequency.
|
|
164
195
|
*
|
|
@@ -202,7 +233,7 @@ export class AsyncThrottler<TFn extends AnyAsyncFunction> {
|
|
|
202
233
|
>(getDefaultAsyncThrottlerState<TFn>())
|
|
203
234
|
key: string
|
|
204
235
|
options: AsyncThrottlerOptions<TFn>
|
|
205
|
-
|
|
236
|
+
asyncRetryers = new Map<number, AsyncRetryer<TFn>>()
|
|
206
237
|
#timeoutId: NodeJS.Timeout | null = null
|
|
207
238
|
#resolvePreviousPromise:
|
|
208
239
|
| ((value?: ReturnType<TFn> | undefined) => void)
|
|
@@ -219,6 +250,7 @@ export class AsyncThrottler<TFn extends AnyAsyncFunction> {
|
|
|
219
250
|
throwOnError: initialOptions.throwOnError ?? !initialOptions.onError,
|
|
220
251
|
}
|
|
221
252
|
this.#setState(this.options.initialState ?? {})
|
|
253
|
+
|
|
222
254
|
pacerEventClient.on('d-AsyncThrottler', (event) => {
|
|
223
255
|
if (event.payload.key !== this.key) return
|
|
224
256
|
this.#setState(event.payload.store.state as AsyncThrottlerState<TFn>)
|
|
@@ -226,6 +258,11 @@ export class AsyncThrottler<TFn extends AnyAsyncFunction> {
|
|
|
226
258
|
})
|
|
227
259
|
}
|
|
228
260
|
|
|
261
|
+
/**
|
|
262
|
+
* Emits a change event for the async throttler instance. Mostly useful for devtools.
|
|
263
|
+
*/
|
|
264
|
+
_emit = () => emitChange('AsyncThrottler', this)
|
|
265
|
+
|
|
229
266
|
/**
|
|
230
267
|
* Updates the async throttler options
|
|
231
268
|
*/
|
|
@@ -301,88 +338,119 @@ export class AsyncThrottler<TFn extends AnyAsyncFunction> {
|
|
|
301
338
|
...args: Parameters<TFn>
|
|
302
339
|
): Promise<ReturnType<TFn> | undefined> => {
|
|
303
340
|
if (!this.#getEnabled()) return undefined
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
// Store the most recent arguments for potential trailing execution
|
|
341
|
+
|
|
342
|
+
this.#resolvePreviousPromiseInternal()
|
|
343
|
+
|
|
308
344
|
this.#setState({
|
|
309
|
-
lastArgs: args,
|
|
310
345
|
maybeExecuteCount: this.store.state.maybeExecuteCount + 1,
|
|
346
|
+
lastArgs: args, // store the arguments for potential trailing execution
|
|
311
347
|
})
|
|
312
348
|
|
|
313
|
-
this.#
|
|
349
|
+
const wait = this.#getWait()
|
|
350
|
+
const thisMaybeExecuteNumber = this.store.state.maybeExecuteCount
|
|
351
|
+
|
|
352
|
+
// Wait for the wait period for the previous execution to complete if it's still running
|
|
353
|
+
for (
|
|
354
|
+
let maxNumIterations = wait / 10;
|
|
355
|
+
this.store.state.isExecuting && maxNumIterations > 0;
|
|
356
|
+
maxNumIterations--
|
|
357
|
+
) {
|
|
358
|
+
await new Promise((resolve) => setTimeout(resolve, 10))
|
|
359
|
+
if (this.store.state.maybeExecuteCount !== thisMaybeExecuteNumber) {
|
|
360
|
+
// cancel the current maybeExecute loop because a new maybeExecute call was made
|
|
361
|
+
return this.store.state.lastResult
|
|
362
|
+
}
|
|
363
|
+
}
|
|
314
364
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
365
|
+
const now = Date.now()
|
|
366
|
+
const timeSinceLastExecution = now - this.store.state.lastExecutionTime
|
|
367
|
+
|
|
368
|
+
if (
|
|
369
|
+
this.options.leading &&
|
|
370
|
+
!this.store.state.isPending &&
|
|
371
|
+
timeSinceLastExecution >= wait
|
|
372
|
+
) {
|
|
373
|
+
await this.#execute(...args) // Leading EXECUTE!
|
|
374
|
+
} else if (this.options.trailing) {
|
|
375
|
+
// replace old pending execution with a new one
|
|
376
|
+
this.cancel()
|
|
377
|
+
this.#setState({
|
|
378
|
+
isPending: true,
|
|
379
|
+
})
|
|
380
|
+
|
|
381
|
+
// Set up new trailing execution
|
|
320
382
|
return new Promise((resolve, reject) => {
|
|
321
383
|
this.#resolvePreviousPromise = resolve
|
|
322
|
-
|
|
323
|
-
this
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
await this.#execute(...this.store.state.lastArgs) // EXECUTE!
|
|
336
|
-
} catch (error) {
|
|
337
|
-
reject(error)
|
|
338
|
-
}
|
|
384
|
+
|
|
385
|
+
const newTimeSinceLastExecution = this.store.state.lastExecutionTime
|
|
386
|
+
? now - this.store.state.lastExecutionTime
|
|
387
|
+
: 0
|
|
388
|
+
const timeoutDuration = Math.max(0, wait - newTimeSinceLastExecution)
|
|
389
|
+
|
|
390
|
+
this.#timeoutId = setTimeout(async () => {
|
|
391
|
+
this.#clearTimeout()
|
|
392
|
+
if (this.store.state.lastArgs !== undefined) {
|
|
393
|
+
try {
|
|
394
|
+
await this.#execute(...this.store.state.lastArgs) // Trailing EXECUTE!
|
|
395
|
+
} catch (error) {
|
|
396
|
+
reject(error)
|
|
339
397
|
}
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
}
|
|
398
|
+
}
|
|
399
|
+
this.#resolvePreviousPromise = null
|
|
400
|
+
resolve(this.store.state.lastResult)
|
|
401
|
+
}, timeoutDuration)
|
|
344
402
|
})
|
|
345
403
|
}
|
|
404
|
+
return this.store.state.lastResult
|
|
346
405
|
}
|
|
347
406
|
|
|
348
407
|
#execute = async (
|
|
349
408
|
...args: Parameters<TFn>
|
|
350
409
|
): Promise<ReturnType<TFn> | undefined> => {
|
|
351
|
-
if (!this.#getEnabled()
|
|
352
|
-
|
|
410
|
+
if (!this.#getEnabled()) return undefined
|
|
411
|
+
|
|
412
|
+
const currentMaybeExecute = this.store.state.maybeExecuteCount
|
|
413
|
+
|
|
353
414
|
try {
|
|
354
415
|
this.#setState({ isExecuting: true })
|
|
355
|
-
const
|
|
416
|
+
const currentAsyncRetryer = new AsyncRetryer(this.fn, {
|
|
417
|
+
...this.options.asyncRetryerOptions,
|
|
418
|
+
key: `${this.key}-retryer-${currentMaybeExecute}`,
|
|
419
|
+
})
|
|
420
|
+
this.asyncRetryers.set(currentMaybeExecute, currentAsyncRetryer)
|
|
421
|
+
const result = await currentAsyncRetryer.execute(...args) // EXECUTE!
|
|
356
422
|
this.#setState({
|
|
357
423
|
lastResult: result,
|
|
358
424
|
successCount: this.store.state.successCount + 1,
|
|
359
425
|
})
|
|
360
|
-
this.options.onSuccess?.(result
|
|
426
|
+
this.options.onSuccess?.(result as ReturnType<TFn>, args, this)
|
|
361
427
|
} catch (error) {
|
|
362
428
|
this.#setState({
|
|
363
429
|
errorCount: this.store.state.errorCount + 1,
|
|
364
430
|
})
|
|
365
|
-
this.options.onError?.(error, args, this)
|
|
431
|
+
this.options.onError?.(error as Error, args, this)
|
|
366
432
|
if (this.options.throwOnError) {
|
|
367
433
|
throw error
|
|
368
434
|
}
|
|
369
435
|
} finally {
|
|
436
|
+
this.asyncRetryers.delete(currentMaybeExecute) // dispose retryer
|
|
370
437
|
const lastExecutionTime = Date.now()
|
|
371
|
-
const
|
|
438
|
+
const wait = this.#getWait()
|
|
439
|
+
const nextExecutionTime = lastExecutionTime + wait
|
|
372
440
|
this.#setState({
|
|
373
441
|
isExecuting: false,
|
|
374
|
-
isPending:
|
|
442
|
+
isPending: !!this.#timeoutId,
|
|
375
443
|
settleCount: this.store.state.settleCount + 1,
|
|
376
444
|
lastExecutionTime,
|
|
377
445
|
nextExecutionTime,
|
|
378
446
|
})
|
|
379
|
-
this.#abortController = null
|
|
380
447
|
this.options.onSettled?.(args, this)
|
|
381
448
|
setTimeout(() => {
|
|
382
449
|
if (!this.store.state.isPending) {
|
|
450
|
+
// clear nextExecutionTime if there is no pending execution
|
|
383
451
|
this.#setState({ nextExecutionTime: undefined })
|
|
384
452
|
}
|
|
385
|
-
},
|
|
453
|
+
}, wait)
|
|
386
454
|
}
|
|
387
455
|
return this.store.state.lastResult
|
|
388
456
|
}
|
|
@@ -392,12 +460,21 @@ export class AsyncThrottler<TFn extends AnyAsyncFunction> {
|
|
|
392
460
|
*/
|
|
393
461
|
flush = async (): Promise<ReturnType<TFn> | undefined> => {
|
|
394
462
|
if (this.store.state.isPending && this.store.state.lastArgs) {
|
|
395
|
-
|
|
396
|
-
|
|
463
|
+
// Store the pending promise resolver before clearing timeout
|
|
464
|
+
const resolvePromise = this.#resolvePreviousPromise
|
|
465
|
+
|
|
466
|
+
// Clear timeout and state without resolving the promise
|
|
467
|
+
this.#clearTimeout()
|
|
468
|
+
this.#setState({
|
|
469
|
+
isPending: false,
|
|
470
|
+
})
|
|
471
|
+
|
|
397
472
|
const result = await this.#execute(...this.store.state.lastArgs)
|
|
398
473
|
|
|
399
|
-
// Resolve
|
|
400
|
-
|
|
474
|
+
// Resolve the pending promise with the result
|
|
475
|
+
if (resolvePromise) {
|
|
476
|
+
resolvePromise(result)
|
|
477
|
+
}
|
|
401
478
|
|
|
402
479
|
return result
|
|
403
480
|
}
|
|
@@ -418,7 +495,51 @@ export class AsyncThrottler<TFn extends AnyAsyncFunction> {
|
|
|
418
495
|
}
|
|
419
496
|
}
|
|
420
497
|
|
|
421
|
-
|
|
498
|
+
/**
|
|
499
|
+
* Returns the AbortSignal for a specific execution.
|
|
500
|
+
* If no maybeExecuteCount is provided, returns the signal for the most recent execution.
|
|
501
|
+
* Returns null if no execution is found or not currently executing.
|
|
502
|
+
*
|
|
503
|
+
* @param maybeExecuteCount - Optional specific execution to get signal for
|
|
504
|
+
* @example
|
|
505
|
+
* ```typescript
|
|
506
|
+
* const throttler = new AsyncThrottler(
|
|
507
|
+
* async (data: string) => {
|
|
508
|
+
* const signal = throttler.getAbortSignal()
|
|
509
|
+
* if (signal) {
|
|
510
|
+
* const response = await fetch('/api/save', {
|
|
511
|
+
* method: 'POST',
|
|
512
|
+
* body: data,
|
|
513
|
+
* signal
|
|
514
|
+
* })
|
|
515
|
+
* return response.json()
|
|
516
|
+
* }
|
|
517
|
+
* },
|
|
518
|
+
* { wait: 1000 }
|
|
519
|
+
* )
|
|
520
|
+
* ```
|
|
521
|
+
*/
|
|
522
|
+
getAbortSignal(maybeExecuteCount?: number): AbortSignal | null {
|
|
523
|
+
const count = maybeExecuteCount ?? this.store.state.maybeExecuteCount
|
|
524
|
+
const retryer = this.asyncRetryers.get(count)
|
|
525
|
+
return retryer?.getAbortSignal() ?? null
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/**
|
|
529
|
+
* Aborts all ongoing executions with the internal abort controllers.
|
|
530
|
+
* Does NOT cancel any pending execution that have not started yet.
|
|
531
|
+
*/
|
|
532
|
+
abort = (): void => {
|
|
533
|
+
this.asyncRetryers.forEach((retryer) => retryer.abort())
|
|
534
|
+
this.asyncRetryers.clear()
|
|
535
|
+
this.#setState({ isExecuting: false })
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
/**
|
|
539
|
+
* Cancels any pending execution that have not started yet.
|
|
540
|
+
* Does NOT abort any execution already in progress.
|
|
541
|
+
*/
|
|
542
|
+
cancel = (): void => {
|
|
422
543
|
this.#clearTimeout()
|
|
423
544
|
if (this.#resolvePreviousPromise) {
|
|
424
545
|
this.#resolvePreviousPromiseInternal()
|
|
@@ -426,31 +547,15 @@ export class AsyncThrottler<TFn extends AnyAsyncFunction> {
|
|
|
426
547
|
}
|
|
427
548
|
this.#setState({
|
|
428
549
|
isPending: false,
|
|
429
|
-
isExecuting: false,
|
|
430
|
-
lastArgs: undefined,
|
|
431
550
|
})
|
|
432
551
|
}
|
|
433
552
|
|
|
434
|
-
#abortExecution = (): void => {
|
|
435
|
-
if (this.#abortController) {
|
|
436
|
-
this.#abortController.abort()
|
|
437
|
-
this.#abortController = null
|
|
438
|
-
}
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
/**
|
|
442
|
-
* Cancels any pending execution or aborts any execution in progress
|
|
443
|
-
*/
|
|
444
|
-
cancel = (): void => {
|
|
445
|
-
this.#cancelPendingExecution()
|
|
446
|
-
this.#abortExecution()
|
|
447
|
-
}
|
|
448
|
-
|
|
449
553
|
/**
|
|
450
554
|
* Resets the debouncer state to its default values
|
|
451
555
|
*/
|
|
452
556
|
reset = (): void => {
|
|
453
557
|
this.#setState(getDefaultAsyncThrottlerState<TFn>())
|
|
558
|
+
this.asyncRetryers.forEach((retryer) => retryer.reset())
|
|
454
559
|
}
|
|
455
560
|
}
|
|
456
561
|
|
|
@@ -459,9 +564,30 @@ export class AsyncThrottler<TFn extends AnyAsyncFunction> {
|
|
|
459
564
|
* The throttled function will execute at most once per wait period, even if called multiple times.
|
|
460
565
|
* If called while executing, it will wait until execution completes before scheduling the next call.
|
|
461
566
|
*
|
|
462
|
-
*
|
|
463
|
-
*
|
|
464
|
-
*
|
|
567
|
+
* Async vs Sync Versions:
|
|
568
|
+
* The async version provides advanced features over the sync throttle function:
|
|
569
|
+
* - Returns promises that can be awaited for throttled function results
|
|
570
|
+
* - Built-in retry support via AsyncRetryer integration
|
|
571
|
+
* - Abort support to cancel in-flight executions
|
|
572
|
+
* - Cancel support to prevent pending executions from starting
|
|
573
|
+
* - Comprehensive error handling with onError callbacks and throwOnError control
|
|
574
|
+
* - Detailed execution tracking (success/error/settle counts)
|
|
575
|
+
* - Waits for ongoing executions to complete before scheduling the next one
|
|
576
|
+
*
|
|
577
|
+
* The sync throttle function is lighter weight and simpler when you don't need async features,
|
|
578
|
+
* return values, or execution control.
|
|
579
|
+
*
|
|
580
|
+
* What is Throttling?
|
|
581
|
+
* Throttling limits how often a function can be executed, allowing only one execution within a specified time window.
|
|
582
|
+
* Unlike debouncing which resets the delay timer on each call, throttling ensures the function executes at a
|
|
583
|
+
* regular interval regardless of how often it's called.
|
|
584
|
+
*
|
|
585
|
+
* Configuration Options:
|
|
586
|
+
* - `wait`: Time window in milliseconds during which the function can only execute once (required)
|
|
587
|
+
* - `leading`: Execute immediately when called (default: true)
|
|
588
|
+
* - `trailing`: Execute on the trailing edge of the wait period (default: true)
|
|
589
|
+
* - `enabled`: Whether the throttler is enabled (default: true)
|
|
590
|
+
* - `asyncRetryerOptions`: Configure retry behavior for executions
|
|
465
591
|
*
|
|
466
592
|
* Error Handling:
|
|
467
593
|
* - If an `onError` handler is provided, it will be called with the error and throttler instance
|
package/src/batcher.ts
CHANGED
|
@@ -107,6 +107,7 @@ const defaultOptions: BatcherOptionsWithOptionalCallbacks<any> = {
|
|
|
107
107
|
* A class that collects items and processes them in batches.
|
|
108
108
|
*
|
|
109
109
|
* Batching is a technique for grouping multiple operations together to be processed as a single unit.
|
|
110
|
+
* This synchronous version is lighter weight and often all you need - upgrade to AsyncBatcher when you need promises, retry support, abort/cancel capabilities, or advanced error handling.
|
|
110
111
|
*
|
|
111
112
|
* The Batcher provides a flexible way to implement batching with configurable:
|
|
112
113
|
* - Maximum batch size (number of items per batch)
|
|
@@ -167,6 +168,11 @@ export class Batcher<TValue> {
|
|
|
167
168
|
})
|
|
168
169
|
}
|
|
169
170
|
|
|
171
|
+
/**
|
|
172
|
+
* Emits a change event for the batcher instance. Mostly useful for devtools.
|
|
173
|
+
*/
|
|
174
|
+
_emit = () => emitChange('Batcher', this)
|
|
175
|
+
|
|
170
176
|
/**
|
|
171
177
|
* Updates the batcher options
|
|
172
178
|
*/
|
|
@@ -275,6 +281,15 @@ export class Batcher<TValue> {
|
|
|
275
281
|
this.#setState({ items: [], isPending: false })
|
|
276
282
|
}
|
|
277
283
|
|
|
284
|
+
/**
|
|
285
|
+
* Cancels any pending execution that was scheduled.
|
|
286
|
+
* Does NOT clear out the items.
|
|
287
|
+
*/
|
|
288
|
+
cancel = (): void => {
|
|
289
|
+
this.#clearTimeout()
|
|
290
|
+
this.#setState({ isPending: false })
|
|
291
|
+
}
|
|
292
|
+
|
|
278
293
|
/**
|
|
279
294
|
* Resets the batcher state to its default values
|
|
280
295
|
*/
|
|
@@ -285,7 +300,9 @@ export class Batcher<TValue> {
|
|
|
285
300
|
}
|
|
286
301
|
|
|
287
302
|
/**
|
|
288
|
-
* Creates a batcher that processes items in batches
|
|
303
|
+
* Creates a batcher that processes items in batches.
|
|
304
|
+
*
|
|
305
|
+
* This synchronous version is lighter weight and often all you need - upgrade to asyncBatch when you need promises, retry support, abort/cancel capabilities, or advanced error handling.
|
|
289
306
|
*
|
|
290
307
|
* @example
|
|
291
308
|
* ```ts
|
package/src/debouncer.ts
CHANGED
|
@@ -85,6 +85,18 @@ export interface DebouncerOptions<TFn extends AnyFunction> {
|
|
|
85
85
|
wait: number | ((debouncer: Debouncer<TFn>) => number)
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
+
/**
|
|
89
|
+
* Utility function for sharing common `DebouncerOptions` options between different `Debouncer` instances.
|
|
90
|
+
*/
|
|
91
|
+
export function debouncerOptions<
|
|
92
|
+
TFn extends AnyFunction = AnyFunction,
|
|
93
|
+
TOptions extends Partial<DebouncerOptions<TFn>> = Partial<
|
|
94
|
+
DebouncerOptions<TFn>
|
|
95
|
+
>,
|
|
96
|
+
>(options: TOptions): TOptions {
|
|
97
|
+
return options
|
|
98
|
+
}
|
|
99
|
+
|
|
88
100
|
const defaultOptions: Omit<
|
|
89
101
|
Required<DebouncerOptions<any>>,
|
|
90
102
|
'initialState' | 'onExecute' | 'key'
|
|
@@ -101,6 +113,7 @@ const defaultOptions: Omit<
|
|
|
101
113
|
* Debouncing ensures that a function is only executed after a certain amount of time has passed
|
|
102
114
|
* since its last invocation. This is useful for handling frequent events like window resizing,
|
|
103
115
|
* scroll events, or input changes where you want to limit the rate of execution.
|
|
116
|
+
* This synchronous version is lighter weight and often all you need - upgrade to AsyncDebouncer when you need promises, retry support, abort/cancel capabilities, or advanced error handling.
|
|
104
117
|
*
|
|
105
118
|
* The debounced function can be configured to execute either at the start of the delay period
|
|
106
119
|
* (leading edge) or at the end (trailing edge, default). Each new call during the wait period
|
|
@@ -152,6 +165,11 @@ export class Debouncer<TFn extends AnyFunction> {
|
|
|
152
165
|
})
|
|
153
166
|
}
|
|
154
167
|
|
|
168
|
+
/**
|
|
169
|
+
* Emits a change event for the debouncer instance. Mostly useful for devtools.
|
|
170
|
+
*/
|
|
171
|
+
_emit = () => emitChange('Debouncer', this)
|
|
172
|
+
|
|
155
173
|
/**
|
|
156
174
|
* Updates the debouncer options
|
|
157
175
|
*/
|
|
@@ -285,8 +303,7 @@ export class Debouncer<TFn extends AnyFunction> {
|
|
|
285
303
|
* Creates a debounced function that delays invoking the provided function until after a specified wait time.
|
|
286
304
|
* Multiple calls during the wait period will cancel previous pending invocations and reset the timer.
|
|
287
305
|
*
|
|
288
|
-
* This
|
|
289
|
-
* more control over the debouncing behavior, use the Debouncer class directly.
|
|
306
|
+
* This synchronous version is lighter weight and often all you need - upgrade to asyncDebounce when you need promises, retry support, abort/cancel capabilities, or advanced error handling.
|
|
290
307
|
*
|
|
291
308
|
* If leading option is true, the function will execute immediately on the first call, then wait the delay
|
|
292
309
|
* before allowing another execution.
|
package/src/event-client.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type { AsyncBatcher } from './async-batcher'
|
|
|
3
3
|
import type { AsyncDebouncer } from './async-debouncer'
|
|
4
4
|
import type { AsyncQueuer } from './async-queuer'
|
|
5
5
|
import type { AsyncRateLimiter } from './async-rate-limiter'
|
|
6
|
+
import type { AsyncRetryer } from './async-retryer'
|
|
6
7
|
import type { AsyncThrottler } from './async-throttler'
|
|
7
8
|
import type { Debouncer } from './debouncer'
|
|
8
9
|
import type { Batcher } from './batcher'
|
|
@@ -15,6 +16,7 @@ export interface PacerEventMap {
|
|
|
15
16
|
'pacer:d-AsyncDebouncer': AsyncDebouncer<any>
|
|
16
17
|
'pacer:d-AsyncQueuer': AsyncQueuer<any>
|
|
17
18
|
'pacer:d-AsyncRateLimiter': AsyncRateLimiter<any>
|
|
19
|
+
'pacer:d-AsyncRetryer': AsyncRetryer<any>
|
|
18
20
|
'pacer:d-AsyncThrottler': AsyncThrottler<any>
|
|
19
21
|
'pacer:d-Batcher': Batcher<any>
|
|
20
22
|
'pacer:d-Debouncer': Debouncer<any>
|
|
@@ -25,6 +27,7 @@ export interface PacerEventMap {
|
|
|
25
27
|
'pacer:AsyncDebouncer': AsyncDebouncer<any>
|
|
26
28
|
'pacer:AsyncQueuer': AsyncQueuer<any>
|
|
27
29
|
'pacer:AsyncRateLimiter': AsyncRateLimiter<any>
|
|
30
|
+
'pacer:AsyncRetryer': AsyncRetryer<any>
|
|
28
31
|
'pacer:AsyncThrottler': AsyncThrottler<any>
|
|
29
32
|
'pacer:Batcher': Batcher<any>
|
|
30
33
|
'pacer:Debouncer': Debouncer<any>
|
package/src/index.ts
CHANGED
|
@@ -2,6 +2,7 @@ export * from './async-batcher'
|
|
|
2
2
|
export * from './async-debouncer'
|
|
3
3
|
export * from './async-queuer'
|
|
4
4
|
export * from './async-rate-limiter'
|
|
5
|
+
export * from './async-retryer'
|
|
5
6
|
export * from './async-throttler'
|
|
6
7
|
export * from './batcher'
|
|
7
8
|
export * from './debouncer'
|
|
@@ -10,5 +11,6 @@ export * from './rate-limiter'
|
|
|
10
11
|
export * from './throttler'
|
|
11
12
|
export * from './types'
|
|
12
13
|
export * from './utils'
|
|
14
|
+
|
|
13
15
|
export { pacerEventClient } from './event-client'
|
|
14
16
|
export type { PacerEventMap, PacerEventName } from './event-client'
|
package/src/queuer.ts
CHANGED
|
@@ -151,6 +151,18 @@ export interface QueuerOptions<TValue> {
|
|
|
151
151
|
wait?: number | ((queuer: Queuer<TValue>) => number)
|
|
152
152
|
}
|
|
153
153
|
|
|
154
|
+
/**
|
|
155
|
+
* Utility function for sharing common `QueuerOptions` options between different `Queuer` instances.
|
|
156
|
+
*/
|
|
157
|
+
export function queuerOptions<
|
|
158
|
+
TValue = any,
|
|
159
|
+
TOptions extends Partial<QueuerOptions<TValue>> = Partial<
|
|
160
|
+
QueuerOptions<TValue>
|
|
161
|
+
>,
|
|
162
|
+
>(options: TOptions): TOptions {
|
|
163
|
+
return options
|
|
164
|
+
}
|
|
165
|
+
|
|
154
166
|
const defaultOptions: Omit<
|
|
155
167
|
Required<QueuerOptions<any>>,
|
|
156
168
|
| 'initialState'
|
|
@@ -183,6 +195,8 @@ export type QueuePosition = 'front' | 'back'
|
|
|
183
195
|
/**
|
|
184
196
|
* A flexible queue that processes items with configurable wait times, expiration, and priority.
|
|
185
197
|
*
|
|
198
|
+
* This synchronous version is lighter weight and often all you need - upgrade to AsyncQueuer when you need promises, retry support, abort capabilities, concurrent execution, or advanced error handling.
|
|
199
|
+
*
|
|
186
200
|
* Features:
|
|
187
201
|
* - Automatic or manual processing of items
|
|
188
202
|
* - FIFO (First In First Out), LIFO (Last In First Out), or double-ended queue behavior
|
|
@@ -287,6 +301,7 @@ export class Queuer<TValue> {
|
|
|
287
301
|
this.addItem(item, this.options.addItemsTo ?? 'back', isLast)
|
|
288
302
|
}
|
|
289
303
|
}
|
|
304
|
+
|
|
290
305
|
pacerEventClient.on('d-Queuer', (event) => {
|
|
291
306
|
if (event.payload.key !== this.key) return
|
|
292
307
|
this.#setState(event.payload.store.state)
|
|
@@ -294,6 +309,11 @@ export class Queuer<TValue> {
|
|
|
294
309
|
})
|
|
295
310
|
}
|
|
296
311
|
|
|
312
|
+
/**
|
|
313
|
+
* Emits a change event for the queuer instance. Mostly useful for devtools.
|
|
314
|
+
*/
|
|
315
|
+
_emit = () => emitChange('Queuer', this)
|
|
316
|
+
|
|
297
317
|
/**
|
|
298
318
|
* Updates the queuer options. New options are merged with existing options.
|
|
299
319
|
*/
|
|
@@ -681,9 +701,7 @@ export class Queuer<TValue> {
|
|
|
681
701
|
* Creates a queue that processes items immediately upon addition.
|
|
682
702
|
* Items are processed sequentially in FIFO order by default.
|
|
683
703
|
*
|
|
684
|
-
* This is
|
|
685
|
-
* `addItem` method. The queue is always isRunning and will process items as they are added.
|
|
686
|
-
* For more control over queue processing, use the Queuer class directly.
|
|
704
|
+
* This synchronous version is lighter weight and often all you need - upgrade to asyncQueue when you need promises, retry support, abort capabilities, concurrent execution, or advanced error handling.
|
|
687
705
|
*
|
|
688
706
|
* State Management:
|
|
689
707
|
* - Uses TanStack Store for reactive state management
|
package/src/rate-limiter.ts
CHANGED
|
@@ -86,6 +86,18 @@ export interface RateLimiterOptions<TFn extends AnyFunction> {
|
|
|
86
86
|
windowType?: 'fixed' | 'sliding'
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
+
/**
|
|
90
|
+
* Utility function for sharing common `RateLimiterOptions` options between different `RateLimiter` instances.
|
|
91
|
+
*/
|
|
92
|
+
export function rateLimiterOptions<
|
|
93
|
+
TFn extends AnyFunction = AnyFunction,
|
|
94
|
+
TOptions extends Partial<RateLimiterOptions<TFn>> = Partial<
|
|
95
|
+
RateLimiterOptions<TFn>
|
|
96
|
+
>,
|
|
97
|
+
>(options: TOptions): TOptions {
|
|
98
|
+
return options
|
|
99
|
+
}
|
|
100
|
+
|
|
89
101
|
const defaultOptions: Omit<
|
|
90
102
|
Required<RateLimiterOptions<any>>,
|
|
91
103
|
'initialState' | 'onExecute' | 'onReject' | 'key'
|
|
@@ -102,6 +114,7 @@ const defaultOptions: Omit<
|
|
|
102
114
|
* Rate limiting is a simple approach that allows a function to execute up to a limit within a time window,
|
|
103
115
|
* then blocks all subsequent calls until the window passes. This can lead to "bursty" behavior where
|
|
104
116
|
* all executions happen immediately, followed by a complete block.
|
|
117
|
+
* This synchronous version is lighter weight and often all you need - upgrade to AsyncRateLimiter when you need promises, retry support, abort capabilities, or advanced error handling.
|
|
105
118
|
*
|
|
106
119
|
* The rate limiter supports two types of windows:
|
|
107
120
|
* - 'fixed': A strict window that resets after the window period. All executions within the window count
|
|
@@ -168,6 +181,11 @@ export class RateLimiter<TFn extends AnyFunction> {
|
|
|
168
181
|
})
|
|
169
182
|
}
|
|
170
183
|
|
|
184
|
+
/**
|
|
185
|
+
* Emits a change event for the rate limiter instance. Mostly useful for devtools.
|
|
186
|
+
*/
|
|
187
|
+
_emit = () => emitChange('RateLimiter', this)
|
|
188
|
+
|
|
171
189
|
/**
|
|
172
190
|
* Updates the rate limiter options
|
|
173
191
|
*/
|
|
@@ -358,6 +376,8 @@ export class RateLimiter<TFn extends AnyFunction> {
|
|
|
358
376
|
/**
|
|
359
377
|
* Creates a rate-limited function that will execute the provided function up to a maximum number of times within a time window.
|
|
360
378
|
*
|
|
379
|
+
* This synchronous version is lighter weight and often all you need - upgrade to asyncRateLimit when you need promises, retry support, abort capabilities, or advanced error handling.
|
|
380
|
+
*
|
|
361
381
|
* Note that rate limiting is a simpler form of execution control compared to throttling or debouncing:
|
|
362
382
|
* - A rate limiter will allow all executions until the limit is reached, then block all subsequent calls until the window resets
|
|
363
383
|
* - A throttler ensures even spacing between executions, which can be better for consistent performance
|