@tanstack/pacer 0.15.4 → 0.16.1
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 -5
- package/dist/cjs/async-batcher.cjs.map +1 -1
- package/dist/cjs/async-batcher.d.cts +94 -17
- package/dist/cjs/async-debouncer.cjs +53 -19
- package/dist/cjs/async-debouncer.cjs.map +1 -1
- package/dist/cjs/async-debouncer.d.cts +76 -10
- package/dist/cjs/async-queuer.cjs +58 -2
- package/dist/cjs/async-queuer.cjs.map +1 -1
- package/dist/cjs/async-queuer.d.cts +100 -10
- package/dist/cjs/async-rate-limiter.cjs +51 -2
- package/dist/cjs/async-rate-limiter.cjs.map +1 -1
- package/dist/cjs/async-rate-limiter.d.cts +91 -28
- package/dist/cjs/async-retryer.cjs +285 -0
- package/dist/cjs/async-retryer.cjs.map +1 -0
- package/dist/cjs/async-retryer.d.cts +308 -0
- package/dist/cjs/async-throttler.cjs +101 -53
- package/dist/cjs/async-throttler.cjs.map +1 -1
- package/dist/cjs/async-throttler.d.cts +82 -10
- package/dist/cjs/batcher.cjs +5 -1
- package/dist/cjs/batcher.cjs.map +1 -1
- package/dist/cjs/batcher.d.cts +10 -2
- package/dist/cjs/debouncer.cjs +5 -1
- package/dist/cjs/debouncer.cjs.map +1 -1
- package/dist/cjs/debouncer.d.cts +7 -3
- package/dist/cjs/event-client.cjs +3 -1
- package/dist/cjs/event-client.cjs.map +1 -1
- package/dist/cjs/event-client.d.cts +3 -0
- package/dist/cjs/index.cjs +13 -1
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/index.d.cts +1 -0
- package/dist/cjs/queuer.cjs +5 -1
- package/dist/cjs/queuer.cjs.map +1 -1
- package/dist/cjs/queuer.d.cts +8 -4
- package/dist/cjs/rate-limiter.cjs +5 -1
- package/dist/cjs/rate-limiter.cjs.map +1 -1
- package/dist/cjs/rate-limiter.d.cts +8 -1
- package/dist/cjs/throttler.cjs +5 -1
- package/dist/cjs/throttler.cjs.map +1 -1
- package/dist/cjs/throttler.d.cts +7 -0
- package/dist/cjs/utils.cjs +0 -10
- package/dist/cjs/utils.cjs.map +1 -1
- package/dist/cjs/utils.d.cts +0 -1
- package/dist/esm/async-batcher.d.ts +94 -17
- package/dist/esm/async-batcher.js +68 -7
- package/dist/esm/async-batcher.js.map +1 -1
- package/dist/esm/async-debouncer.d.ts +76 -10
- package/dist/esm/async-debouncer.js +55 -21
- package/dist/esm/async-debouncer.js.map +1 -1
- package/dist/esm/async-queuer.d.ts +100 -10
- package/dist/esm/async-queuer.js +60 -4
- package/dist/esm/async-queuer.js.map +1 -1
- package/dist/esm/async-rate-limiter.d.ts +91 -28
- package/dist/esm/async-rate-limiter.js +53 -4
- package/dist/esm/async-rate-limiter.js.map +1 -1
- package/dist/esm/async-retryer.d.ts +308 -0
- package/dist/esm/async-retryer.js +285 -0
- package/dist/esm/async-retryer.js.map +1 -0
- package/dist/esm/async-throttler.d.ts +82 -10
- package/dist/esm/async-throttler.js +103 -55
- package/dist/esm/async-throttler.js.map +1 -1
- package/dist/esm/batcher.d.ts +10 -2
- package/dist/esm/batcher.js +6 -2
- package/dist/esm/batcher.js.map +1 -1
- package/dist/esm/debouncer.d.ts +7 -3
- package/dist/esm/debouncer.js +7 -3
- package/dist/esm/debouncer.js.map +1 -1
- package/dist/esm/event-client.d.ts +3 -0
- package/dist/esm/event-client.js +3 -1
- package/dist/esm/event-client.js.map +1 -1
- package/dist/esm/index.d.ts +1 -0
- package/dist/esm/index.js +24 -12
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/queuer.d.ts +8 -4
- package/dist/esm/queuer.js +7 -3
- package/dist/esm/queuer.js.map +1 -1
- package/dist/esm/rate-limiter.d.ts +8 -1
- package/dist/esm/rate-limiter.js +7 -3
- package/dist/esm/rate-limiter.js.map +1 -1
- package/dist/esm/throttler.d.ts +7 -0
- package/dist/esm/throttler.js +7 -3
- package/dist/esm/throttler.js.map +1 -1
- package/dist/esm/utils.d.ts +0 -1
- package/dist/esm/utils.js +0 -10
- package/dist/esm/utils.js.map +1 -1
- package/package.json +13 -3
- package/src/async-batcher.ts +144 -22
- package/src/async-debouncer.ts +118 -33
- package/src/async-queuer.ts +147 -14
- package/src/async-rate-limiter.ts +129 -33
- package/src/async-retryer.ts +664 -0
- package/src/async-throttler.ts +196 -75
- package/src/batcher.ts +16 -4
- package/src/debouncer.ts +17 -5
- package/src/event-client.ts +6 -1
- package/src/index.ts +2 -0
- package/src/queuer.ts +19 -6
- package/src/rate-limiter.ts +18 -3
- package/src/throttler.ts +17 -2
- package/src/utils.ts +0 -15
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { Store } from '@tanstack/store'
|
|
2
|
-
import {
|
|
2
|
+
import { AsyncRetryer } from './async-retryer'
|
|
3
|
+
import { 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
|
|
@@ -217,15 +246,16 @@ export class AsyncRateLimiter<TFn extends AnyAsyncFunction> {
|
|
|
217
246
|
readonly store: Store<Readonly<AsyncRateLimiterState<TFn>>> = new Store<
|
|
218
247
|
AsyncRateLimiterState<TFn>
|
|
219
248
|
>(getDefaultAsyncRateLimiterState<TFn>())
|
|
220
|
-
key: string
|
|
249
|
+
key: string | undefined
|
|
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(
|
|
225
255
|
public fn: TFn,
|
|
226
256
|
initialOptions: AsyncRateLimiterOptions<TFn>,
|
|
227
257
|
) {
|
|
228
|
-
this.key =
|
|
258
|
+
this.key = initialOptions.key
|
|
229
259
|
this.options = {
|
|
230
260
|
...defaultOptions,
|
|
231
261
|
...initialOptions,
|
|
@@ -347,6 +377,7 @@ export class AsyncRateLimiter<TFn extends AnyAsyncFunction> {
|
|
|
347
377
|
): Promise<ReturnType<TFn> | undefined> => {
|
|
348
378
|
if (!this.#getEnabled()) return
|
|
349
379
|
|
|
380
|
+
const currentMaybeExecute = this.store.state.maybeExecuteCount
|
|
350
381
|
const now = Date.now()
|
|
351
382
|
const executionTimes = [...this.store.state.executionTimes, now]
|
|
352
383
|
this.#setState({
|
|
@@ -355,22 +386,29 @@ export class AsyncRateLimiter<TFn extends AnyAsyncFunction> {
|
|
|
355
386
|
})
|
|
356
387
|
|
|
357
388
|
try {
|
|
358
|
-
|
|
389
|
+
// Create a new AsyncRetryer for this execution to avoid cancelling concurrent executions
|
|
390
|
+
const currentAsyncRetryer = new AsyncRetryer(this.fn, {
|
|
391
|
+
...this.options.asyncRetryerOptions,
|
|
392
|
+
key: `${this.key}-retryer-${currentMaybeExecute}`,
|
|
393
|
+
})
|
|
394
|
+
this.asyncRetryers.set(currentMaybeExecute, currentAsyncRetryer)
|
|
395
|
+
const result = await currentAsyncRetryer.execute(...args) // EXECUTE!
|
|
359
396
|
this.#setCleanupTimeout(now)
|
|
360
397
|
this.#setState({
|
|
361
398
|
successCount: this.store.state.successCount + 1,
|
|
362
399
|
lastResult: result,
|
|
363
400
|
})
|
|
364
|
-
this.options.onSuccess?.(result
|
|
401
|
+
this.options.onSuccess?.(result as ReturnType<TFn>, args, this)
|
|
365
402
|
} catch (error) {
|
|
366
403
|
this.#setState({
|
|
367
404
|
errorCount: this.store.state.errorCount + 1,
|
|
368
405
|
})
|
|
369
|
-
this.options.onError?.(error, args, this)
|
|
406
|
+
this.options.onError?.(error as Error, args, this)
|
|
370
407
|
if (this.options.throwOnError) {
|
|
371
408
|
throw error
|
|
372
409
|
}
|
|
373
410
|
} finally {
|
|
411
|
+
this.asyncRetryers.delete(currentMaybeExecute) // dispose retryer
|
|
374
412
|
this.#setState({
|
|
375
413
|
isExecuting: false,
|
|
376
414
|
settleCount: this.store.state.settleCount + 1,
|
|
@@ -462,33 +500,102 @@ export class AsyncRateLimiter<TFn extends AnyAsyncFunction> {
|
|
|
462
500
|
return oldestExecution + this.#getWindow() - Date.now()
|
|
463
501
|
}
|
|
464
502
|
|
|
503
|
+
/**
|
|
504
|
+
* Returns the AbortSignal for a specific execution.
|
|
505
|
+
* If no maybeExecuteCount is provided, returns the signal for the most recent execution.
|
|
506
|
+
* Returns null if no execution is found or not currently executing.
|
|
507
|
+
*
|
|
508
|
+
* @param maybeExecuteCount - Optional specific execution to get signal for
|
|
509
|
+
* @example
|
|
510
|
+
* ```typescript
|
|
511
|
+
* const rateLimiter = new AsyncRateLimiter(
|
|
512
|
+
* async (userId: string) => {
|
|
513
|
+
* const signal = rateLimiter.getAbortSignal()
|
|
514
|
+
* if (signal) {
|
|
515
|
+
* const response = await fetch(`/api/users/${userId}`, { signal })
|
|
516
|
+
* return response.json()
|
|
517
|
+
* }
|
|
518
|
+
* },
|
|
519
|
+
* { limit: 5, window: 1000 }
|
|
520
|
+
* )
|
|
521
|
+
* ```
|
|
522
|
+
*/
|
|
523
|
+
getAbortSignal(maybeExecuteCount?: number): AbortSignal | null {
|
|
524
|
+
const count = maybeExecuteCount ?? this.store.state.maybeExecuteCount
|
|
525
|
+
const retryer = this.asyncRetryers.get(count)
|
|
526
|
+
return retryer?.getAbortSignal() ?? null
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
/**
|
|
530
|
+
* Aborts all ongoing executions with the internal abort controllers.
|
|
531
|
+
* Does NOT clear out the execution times or reset the rate limiter.
|
|
532
|
+
*/
|
|
533
|
+
abort = (): void => {
|
|
534
|
+
this.asyncRetryers.forEach((retryer) => retryer.abort())
|
|
535
|
+
this.asyncRetryers.clear()
|
|
536
|
+
this.#setState({
|
|
537
|
+
isExecuting: false,
|
|
538
|
+
})
|
|
539
|
+
}
|
|
540
|
+
|
|
465
541
|
/**
|
|
466
542
|
* Resets the rate limiter state
|
|
467
543
|
*/
|
|
468
544
|
reset = (): void => {
|
|
469
545
|
this.#setState(getDefaultAsyncRateLimiterState())
|
|
470
546
|
this.#clearTimeouts()
|
|
547
|
+
this.asyncRetryers.forEach((retryer) => retryer.reset())
|
|
471
548
|
}
|
|
472
549
|
}
|
|
473
550
|
|
|
474
551
|
/**
|
|
475
552
|
* Creates an async rate-limited function that will execute the provided function up to a maximum number of times within a time window.
|
|
476
553
|
*
|
|
477
|
-
*
|
|
478
|
-
*
|
|
479
|
-
*
|
|
554
|
+
* Async vs Sync Versions:
|
|
555
|
+
* The async version provides advanced features over the sync rate limit function:
|
|
556
|
+
* - Returns promises that can be awaited for rate-limited function results
|
|
557
|
+
* - Built-in retry support via AsyncRetryer integration
|
|
558
|
+
* - Abort support to cancel in-flight executions
|
|
559
|
+
* - Comprehensive error handling with onError callbacks and throwOnError control
|
|
560
|
+
* - Detailed execution tracking (success/error/settle counts, rejection counts)
|
|
561
|
+
* - More sophisticated window management with automatic cleanup
|
|
480
562
|
*
|
|
481
|
-
* The rate
|
|
563
|
+
* The sync rate limit function is lighter weight and simpler when you don't need async features,
|
|
564
|
+
* return values, or execution control.
|
|
565
|
+
*
|
|
566
|
+
* What is Rate Limiting?
|
|
567
|
+
* Rate limiting allows a function to execute up to a limit within a time window,
|
|
568
|
+
* then blocks all subsequent calls until the window passes. This can lead to "bursty" behavior where
|
|
569
|
+
* all executions happen immediately, followed by a complete block.
|
|
570
|
+
*
|
|
571
|
+
* Window Types:
|
|
482
572
|
* - 'fixed': A strict window that resets after the window period. All executions within the window count
|
|
483
573
|
* towards the limit, and the window resets completely after the period.
|
|
484
574
|
* - 'sliding': A rolling window that allows executions as old ones expire. This provides a more
|
|
485
575
|
* consistent rate of execution over time.
|
|
486
576
|
*
|
|
487
|
-
*
|
|
577
|
+
* Configuration Options:
|
|
578
|
+
* - `limit`: Maximum number of executions allowed within the window (required)
|
|
579
|
+
* - `window`: Time window in milliseconds (required)
|
|
580
|
+
* - `windowType`: 'fixed' or 'sliding' (default: 'fixed')
|
|
581
|
+
* - `enabled`: Whether the rate limiter is enabled (default: true)
|
|
582
|
+
* - `asyncRetryerOptions`: Configure retry behavior for executions
|
|
583
|
+
*
|
|
584
|
+
* When to Use Rate Limiting:
|
|
585
|
+
* Rate limiting is best used for hard API limits or resource constraints. For UI updates or
|
|
586
|
+
* smoothing out frequent events, throttling or debouncing usually provide better user experience.
|
|
488
587
|
* - A rate limiter will allow all executions until the limit is reached, then block all subsequent calls until the window resets
|
|
489
588
|
* - A throttler ensures even spacing between executions, which can be better for consistent performance
|
|
490
589
|
* - A debouncer collapses multiple calls into one, which is better for handling bursts of events
|
|
491
590
|
*
|
|
591
|
+
* Error Handling:
|
|
592
|
+
* - If an `onError` handler is provided, it will be called with the error and rate limiter instance
|
|
593
|
+
* - If `throwOnError` is true (default when no onError handler is provided), the error will be thrown
|
|
594
|
+
* - If `throwOnError` is false (default when onError handler is provided), the error will be swallowed
|
|
595
|
+
* - Both onError and throwOnError can be used together - the handler will be called before any error is thrown
|
|
596
|
+
* - The error state can be checked using the underlying AsyncRateLimiter instance
|
|
597
|
+
* - Rate limit rejections (when limit is exceeded) are handled separately from execution errors via the `onReject` handler
|
|
598
|
+
*
|
|
492
599
|
* State Management:
|
|
493
600
|
* - Uses TanStack Store for reactive state management
|
|
494
601
|
* - Use `initialState` to provide initial state values when creating the rate limiter
|
|
@@ -501,17 +608,6 @@ export class AsyncRateLimiter<TFn extends AnyAsyncFunction> {
|
|
|
501
608
|
* - State can be accessed via the underlying AsyncRateLimiter instance's `store.state` property
|
|
502
609
|
* - When using framework adapters (React/Solid), state is accessed from the hook's state property
|
|
503
610
|
*
|
|
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
611
|
* @example
|
|
516
612
|
* ```ts
|
|
517
613
|
* // Rate limit to 5 calls per minute with a sliding window
|