@tanstack/pacer 0.15.4 → 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 +12 -2
- 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
|
@@ -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 } from './types'
|
|
5
7
|
|
|
6
8
|
export interface AsyncRateLimiterState<TFn extends AnyAsyncFunction> {
|
|
@@ -67,6 +69,10 @@ function getDefaultAsyncRateLimiterState<
|
|
|
67
69
|
* Options for configuring an async rate-limited function
|
|
68
70
|
*/
|
|
69
71
|
export interface AsyncRateLimiterOptions<TFn extends AnyAsyncFunction> {
|
|
72
|
+
/**
|
|
73
|
+
* Options for configuring the underlying async retryer
|
|
74
|
+
*/
|
|
75
|
+
asyncRetryerOptions?: AsyncRetryerOptions<TFn>
|
|
70
76
|
/**
|
|
71
77
|
* Whether the rate limiter is enabled. When disabled, maybeExecute will not trigger any executions.
|
|
72
78
|
* Can be a boolean or a function that returns a boolean.
|
|
@@ -93,7 +99,7 @@ export interface AsyncRateLimiterOptions<TFn extends AnyAsyncFunction> {
|
|
|
93
99
|
* This can be used alongside throwOnError - the handler will be called before any error is thrown.
|
|
94
100
|
*/
|
|
95
101
|
onError?: (
|
|
96
|
-
error:
|
|
102
|
+
error: Error,
|
|
97
103
|
args: Parameters<TFn>,
|
|
98
104
|
rateLimiter: AsyncRateLimiter<TFn>,
|
|
99
105
|
) => void
|
|
@@ -136,10 +142,25 @@ export interface AsyncRateLimiterOptions<TFn extends AnyAsyncFunction> {
|
|
|
136
142
|
windowType?: 'fixed' | 'sliding'
|
|
137
143
|
}
|
|
138
144
|
|
|
145
|
+
/**
|
|
146
|
+
* Utility function for sharing common `AsyncRateLimiterOptions` options between different `AsyncRateLimiter` instances.
|
|
147
|
+
*/
|
|
148
|
+
export function asyncRateLimiterOptions<
|
|
149
|
+
TFn extends AnyAsyncFunction = AnyAsyncFunction,
|
|
150
|
+
TOptions extends Partial<AsyncRateLimiterOptions<TFn>> = Partial<
|
|
151
|
+
AsyncRateLimiterOptions<TFn>
|
|
152
|
+
>,
|
|
153
|
+
>(options: TOptions): TOptions {
|
|
154
|
+
return options
|
|
155
|
+
}
|
|
156
|
+
|
|
139
157
|
const defaultOptions: Omit<
|
|
140
158
|
Required<AsyncRateLimiterOptions<any>>,
|
|
141
159
|
'initialState' | 'onError' | 'onReject' | 'onSettled' | 'onSuccess' | 'key'
|
|
142
160
|
> = {
|
|
161
|
+
asyncRetryerOptions: {
|
|
162
|
+
maxAttempts: 1,
|
|
163
|
+
},
|
|
143
164
|
enabled: true,
|
|
144
165
|
limit: 1,
|
|
145
166
|
window: 0,
|
|
@@ -150,26 +171,34 @@ const defaultOptions: Omit<
|
|
|
150
171
|
/**
|
|
151
172
|
* A class that creates an async rate-limited function.
|
|
152
173
|
*
|
|
153
|
-
*
|
|
174
|
+
* Async vs Sync Versions:
|
|
175
|
+
* The async version provides advanced features over the sync RateLimiter:
|
|
176
|
+
* - Returns promises that can be awaited for rate-limited function results
|
|
177
|
+
* - Built-in retry support via AsyncRetryer integration
|
|
178
|
+
* - Abort support to cancel in-flight executions
|
|
179
|
+
* - Comprehensive error handling with onError callbacks and throwOnError control
|
|
180
|
+
* - Detailed execution tracking (success/error/settle counts, rejection counts)
|
|
181
|
+
* - More sophisticated window management with automatic cleanup
|
|
182
|
+
*
|
|
183
|
+
* The sync RateLimiter is lighter weight and simpler when you don't need async features,
|
|
184
|
+
* return values, or execution control.
|
|
185
|
+
*
|
|
186
|
+
* What is Rate Limiting?
|
|
187
|
+
* Rate limiting allows a function to execute up to a limit within a time window,
|
|
154
188
|
* then blocks all subsequent calls until the window passes. This can lead to "bursty" behavior where
|
|
155
189
|
* all executions happen immediately, followed by a complete block.
|
|
156
190
|
*
|
|
157
|
-
*
|
|
191
|
+
* Window Types:
|
|
158
192
|
* - 'fixed': A strict window that resets after the window period. All executions within the window count
|
|
159
193
|
* towards the limit, and the window resets completely after the period.
|
|
160
194
|
* - 'sliding': A rolling window that allows executions as old ones expire. This provides a more
|
|
161
195
|
* consistent rate of execution over time.
|
|
162
196
|
*
|
|
163
|
-
*
|
|
164
|
-
* making it ideal for API calls and other async operations where you want the result of the `maybeExecute` call
|
|
165
|
-
* instead of setting the result on a state variable from within the rate-limited function.
|
|
166
|
-
*
|
|
167
|
-
* For smoother execution patterns, consider using:
|
|
168
|
-
* - Throttling: Ensures consistent spacing between executions (e.g. max once per 200ms)
|
|
169
|
-
* - Debouncing: Waits for a pause in calls before executing (e.g. after 500ms of no calls)
|
|
170
|
-
*
|
|
197
|
+
* When to Use Rate Limiting:
|
|
171
198
|
* Rate limiting is best used for hard API limits or resource constraints. For UI updates or
|
|
172
199
|
* smoothing out frequent events, throttling or debouncing usually provide better user experience.
|
|
200
|
+
* - Throttling: Ensures consistent spacing between executions (e.g. max once per 200ms)
|
|
201
|
+
* - Debouncing: Waits for a pause in calls before executing (e.g. after 500ms of no calls)
|
|
173
202
|
*
|
|
174
203
|
* State Management:
|
|
175
204
|
* - Uses TanStack Store for reactive state management
|
|
@@ -219,6 +248,7 @@ export class AsyncRateLimiter<TFn extends AnyAsyncFunction> {
|
|
|
219
248
|
>(getDefaultAsyncRateLimiterState<TFn>())
|
|
220
249
|
key: string
|
|
221
250
|
options: AsyncRateLimiterOptions<TFn>
|
|
251
|
+
asyncRetryers = new Map<number, AsyncRetryer<TFn>>()
|
|
222
252
|
#timeoutIds: Set<NodeJS.Timeout> = new Set()
|
|
223
253
|
|
|
224
254
|
constructor(
|
|
@@ -243,6 +273,11 @@ export class AsyncRateLimiter<TFn extends AnyAsyncFunction> {
|
|
|
243
273
|
})
|
|
244
274
|
}
|
|
245
275
|
|
|
276
|
+
/**
|
|
277
|
+
* Emits a change event for the async rate limiter instance. Mostly useful for devtools.
|
|
278
|
+
*/
|
|
279
|
+
_emit = () => emitChange('AsyncRateLimiter', this)
|
|
280
|
+
|
|
246
281
|
/**
|
|
247
282
|
* Updates the async rate limiter options
|
|
248
283
|
*/
|
|
@@ -347,6 +382,7 @@ export class AsyncRateLimiter<TFn extends AnyAsyncFunction> {
|
|
|
347
382
|
): Promise<ReturnType<TFn> | undefined> => {
|
|
348
383
|
if (!this.#getEnabled()) return
|
|
349
384
|
|
|
385
|
+
const currentMaybeExecute = this.store.state.maybeExecuteCount
|
|
350
386
|
const now = Date.now()
|
|
351
387
|
const executionTimes = [...this.store.state.executionTimes, now]
|
|
352
388
|
this.#setState({
|
|
@@ -355,22 +391,29 @@ export class AsyncRateLimiter<TFn extends AnyAsyncFunction> {
|
|
|
355
391
|
})
|
|
356
392
|
|
|
357
393
|
try {
|
|
358
|
-
|
|
394
|
+
// Create a new AsyncRetryer for this execution to avoid cancelling concurrent executions
|
|
395
|
+
const currentAsyncRetryer = new AsyncRetryer(this.fn, {
|
|
396
|
+
...this.options.asyncRetryerOptions,
|
|
397
|
+
key: `${this.key}-retryer-${currentMaybeExecute}`,
|
|
398
|
+
})
|
|
399
|
+
this.asyncRetryers.set(currentMaybeExecute, currentAsyncRetryer)
|
|
400
|
+
const result = await currentAsyncRetryer.execute(...args) // EXECUTE!
|
|
359
401
|
this.#setCleanupTimeout(now)
|
|
360
402
|
this.#setState({
|
|
361
403
|
successCount: this.store.state.successCount + 1,
|
|
362
404
|
lastResult: result,
|
|
363
405
|
})
|
|
364
|
-
this.options.onSuccess?.(result
|
|
406
|
+
this.options.onSuccess?.(result as ReturnType<TFn>, args, this)
|
|
365
407
|
} catch (error) {
|
|
366
408
|
this.#setState({
|
|
367
409
|
errorCount: this.store.state.errorCount + 1,
|
|
368
410
|
})
|
|
369
|
-
this.options.onError?.(error, args, this)
|
|
411
|
+
this.options.onError?.(error as Error, args, this)
|
|
370
412
|
if (this.options.throwOnError) {
|
|
371
413
|
throw error
|
|
372
414
|
}
|
|
373
415
|
} finally {
|
|
416
|
+
this.asyncRetryers.delete(currentMaybeExecute) // dispose retryer
|
|
374
417
|
this.#setState({
|
|
375
418
|
isExecuting: false,
|
|
376
419
|
settleCount: this.store.state.settleCount + 1,
|
|
@@ -462,33 +505,102 @@ export class AsyncRateLimiter<TFn extends AnyAsyncFunction> {
|
|
|
462
505
|
return oldestExecution + this.#getWindow() - Date.now()
|
|
463
506
|
}
|
|
464
507
|
|
|
508
|
+
/**
|
|
509
|
+
* Returns the AbortSignal for a specific execution.
|
|
510
|
+
* If no maybeExecuteCount is provided, returns the signal for the most recent execution.
|
|
511
|
+
* Returns null if no execution is found or not currently executing.
|
|
512
|
+
*
|
|
513
|
+
* @param maybeExecuteCount - Optional specific execution to get signal for
|
|
514
|
+
* @example
|
|
515
|
+
* ```typescript
|
|
516
|
+
* const rateLimiter = new AsyncRateLimiter(
|
|
517
|
+
* async (userId: string) => {
|
|
518
|
+
* const signal = rateLimiter.getAbortSignal()
|
|
519
|
+
* if (signal) {
|
|
520
|
+
* const response = await fetch(`/api/users/${userId}`, { signal })
|
|
521
|
+
* return response.json()
|
|
522
|
+
* }
|
|
523
|
+
* },
|
|
524
|
+
* { limit: 5, window: 1000 }
|
|
525
|
+
* )
|
|
526
|
+
* ```
|
|
527
|
+
*/
|
|
528
|
+
getAbortSignal(maybeExecuteCount?: number): AbortSignal | null {
|
|
529
|
+
const count = maybeExecuteCount ?? this.store.state.maybeExecuteCount
|
|
530
|
+
const retryer = this.asyncRetryers.get(count)
|
|
531
|
+
return retryer?.getAbortSignal() ?? null
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
/**
|
|
535
|
+
* Aborts all ongoing executions with the internal abort controllers.
|
|
536
|
+
* Does NOT clear out the execution times or reset the rate limiter.
|
|
537
|
+
*/
|
|
538
|
+
abort = (): void => {
|
|
539
|
+
this.asyncRetryers.forEach((retryer) => retryer.abort())
|
|
540
|
+
this.asyncRetryers.clear()
|
|
541
|
+
this.#setState({
|
|
542
|
+
isExecuting: false,
|
|
543
|
+
})
|
|
544
|
+
}
|
|
545
|
+
|
|
465
546
|
/**
|
|
466
547
|
* Resets the rate limiter state
|
|
467
548
|
*/
|
|
468
549
|
reset = (): void => {
|
|
469
550
|
this.#setState(getDefaultAsyncRateLimiterState())
|
|
470
551
|
this.#clearTimeouts()
|
|
552
|
+
this.asyncRetryers.forEach((retryer) => retryer.reset())
|
|
471
553
|
}
|
|
472
554
|
}
|
|
473
555
|
|
|
474
556
|
/**
|
|
475
557
|
* Creates an async rate-limited function that will execute the provided function up to a maximum number of times within a time window.
|
|
476
558
|
*
|
|
477
|
-
*
|
|
478
|
-
*
|
|
479
|
-
*
|
|
559
|
+
* Async vs Sync Versions:
|
|
560
|
+
* The async version provides advanced features over the sync rate limit function:
|
|
561
|
+
* - Returns promises that can be awaited for rate-limited function results
|
|
562
|
+
* - Built-in retry support via AsyncRetryer integration
|
|
563
|
+
* - Abort support to cancel in-flight executions
|
|
564
|
+
* - Comprehensive error handling with onError callbacks and throwOnError control
|
|
565
|
+
* - Detailed execution tracking (success/error/settle counts, rejection counts)
|
|
566
|
+
* - More sophisticated window management with automatic cleanup
|
|
567
|
+
*
|
|
568
|
+
* The sync rate limit function is lighter weight and simpler when you don't need async features,
|
|
569
|
+
* return values, or execution control.
|
|
570
|
+
*
|
|
571
|
+
* What is Rate Limiting?
|
|
572
|
+
* Rate limiting allows a function to execute up to a limit within a time window,
|
|
573
|
+
* then blocks all subsequent calls until the window passes. This can lead to "bursty" behavior where
|
|
574
|
+
* all executions happen immediately, followed by a complete block.
|
|
480
575
|
*
|
|
481
|
-
*
|
|
576
|
+
* Window Types:
|
|
482
577
|
* - 'fixed': A strict window that resets after the window period. All executions within the window count
|
|
483
578
|
* towards the limit, and the window resets completely after the period.
|
|
484
579
|
* - 'sliding': A rolling window that allows executions as old ones expire. This provides a more
|
|
485
580
|
* consistent rate of execution over time.
|
|
486
581
|
*
|
|
487
|
-
*
|
|
582
|
+
* Configuration Options:
|
|
583
|
+
* - `limit`: Maximum number of executions allowed within the window (required)
|
|
584
|
+
* - `window`: Time window in milliseconds (required)
|
|
585
|
+
* - `windowType`: 'fixed' or 'sliding' (default: 'fixed')
|
|
586
|
+
* - `enabled`: Whether the rate limiter is enabled (default: true)
|
|
587
|
+
* - `asyncRetryerOptions`: Configure retry behavior for executions
|
|
588
|
+
*
|
|
589
|
+
* When to Use Rate Limiting:
|
|
590
|
+
* Rate limiting is best used for hard API limits or resource constraints. For UI updates or
|
|
591
|
+
* smoothing out frequent events, throttling or debouncing usually provide better user experience.
|
|
488
592
|
* - A rate limiter will allow all executions until the limit is reached, then block all subsequent calls until the window resets
|
|
489
593
|
* - A throttler ensures even spacing between executions, which can be better for consistent performance
|
|
490
594
|
* - A debouncer collapses multiple calls into one, which is better for handling bursts of events
|
|
491
595
|
*
|
|
596
|
+
* Error Handling:
|
|
597
|
+
* - If an `onError` handler is provided, it will be called with the error and rate limiter instance
|
|
598
|
+
* - If `throwOnError` is true (default when no onError handler is provided), the error will be thrown
|
|
599
|
+
* - If `throwOnError` is false (default when onError handler is provided), the error will be swallowed
|
|
600
|
+
* - Both onError and throwOnError can be used together - the handler will be called before any error is thrown
|
|
601
|
+
* - The error state can be checked using the underlying AsyncRateLimiter instance
|
|
602
|
+
* - Rate limit rejections (when limit is exceeded) are handled separately from execution errors via the `onReject` handler
|
|
603
|
+
*
|
|
492
604
|
* State Management:
|
|
493
605
|
* - Uses TanStack Store for reactive state management
|
|
494
606
|
* - Use `initialState` to provide initial state values when creating the rate limiter
|
|
@@ -501,17 +613,6 @@ export class AsyncRateLimiter<TFn extends AnyAsyncFunction> {
|
|
|
501
613
|
* - State can be accessed via the underlying AsyncRateLimiter instance's `store.state` property
|
|
502
614
|
* - When using framework adapters (React/Solid), state is accessed from the hook's state property
|
|
503
615
|
*
|
|
504
|
-
* Consider using throttle() or debounce() if you need more intelligent execution control. Use rate limiting when you specifically
|
|
505
|
-
* need to enforce a hard limit on the number of executions within a time period.
|
|
506
|
-
*
|
|
507
|
-
* Error Handling:
|
|
508
|
-
* - If an `onError` handler is provided, it will be called with the error and rate limiter instance
|
|
509
|
-
* - If `throwOnError` is true (default when no onError handler is provided), the error will be thrown
|
|
510
|
-
* - If `throwOnError` is false (default when onError handler is provided), the error will be swallowed
|
|
511
|
-
* - Both onError and throwOnError can be used together - the handler will be called before any error is thrown
|
|
512
|
-
* - The error state can be checked using the underlying AsyncRateLimiter instance
|
|
513
|
-
* - Rate limit rejections (when limit is exceeded) are handled separately from execution errors via the `onReject` handler
|
|
514
|
-
*
|
|
515
616
|
* @example
|
|
516
617
|
* ```ts
|
|
517
618
|
* // Rate limit to 5 calls per minute with a sliding window
|