@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.
Files changed (92) hide show
  1. package/dist/cjs/async-batcher.cjs +66 -4
  2. package/dist/cjs/async-batcher.cjs.map +1 -1
  3. package/dist/cjs/async-batcher.d.cts +97 -16
  4. package/dist/cjs/async-debouncer.cjs +53 -18
  5. package/dist/cjs/async-debouncer.cjs.map +1 -1
  6. package/dist/cjs/async-debouncer.d.cts +79 -9
  7. package/dist/cjs/async-queuer.cjs +58 -1
  8. package/dist/cjs/async-queuer.cjs.map +1 -1
  9. package/dist/cjs/async-queuer.d.cts +103 -9
  10. package/dist/cjs/async-rate-limiter.cjs +51 -1
  11. package/dist/cjs/async-rate-limiter.cjs.map +1 -1
  12. package/dist/cjs/async-rate-limiter.d.cts +94 -27
  13. package/dist/cjs/async-retryer.cjs +286 -0
  14. package/dist/cjs/async-retryer.cjs.map +1 -0
  15. package/dist/cjs/async-retryer.d.cts +312 -0
  16. package/dist/cjs/async-throttler.cjs +101 -52
  17. package/dist/cjs/async-throttler.cjs.map +1 -1
  18. package/dist/cjs/async-throttler.d.cts +85 -9
  19. package/dist/cjs/batcher.cjs +5 -0
  20. package/dist/cjs/batcher.cjs.map +1 -1
  21. package/dist/cjs/batcher.d.cts +13 -1
  22. package/dist/cjs/debouncer.cjs +5 -0
  23. package/dist/cjs/debouncer.cjs.map +1 -1
  24. package/dist/cjs/debouncer.d.cts +10 -2
  25. package/dist/cjs/event-client.cjs.map +1 -1
  26. package/dist/cjs/event-client.d.cts +3 -0
  27. package/dist/cjs/index.cjs +13 -0
  28. package/dist/cjs/index.cjs.map +1 -1
  29. package/dist/cjs/index.d.cts +1 -0
  30. package/dist/cjs/queuer.cjs +5 -0
  31. package/dist/cjs/queuer.cjs.map +1 -1
  32. package/dist/cjs/queuer.d.cts +11 -3
  33. package/dist/cjs/rate-limiter.cjs +5 -0
  34. package/dist/cjs/rate-limiter.cjs.map +1 -1
  35. package/dist/cjs/rate-limiter.d.cts +11 -0
  36. package/dist/cjs/throttler.cjs +5 -0
  37. package/dist/cjs/throttler.cjs.map +1 -1
  38. package/dist/cjs/throttler.d.cts +11 -0
  39. package/dist/cjs/utils.cjs.map +1 -1
  40. package/dist/esm/async-batcher.d.ts +97 -16
  41. package/dist/esm/async-batcher.js +67 -5
  42. package/dist/esm/async-batcher.js.map +1 -1
  43. package/dist/esm/async-debouncer.d.ts +79 -9
  44. package/dist/esm/async-debouncer.js +54 -19
  45. package/dist/esm/async-debouncer.js.map +1 -1
  46. package/dist/esm/async-queuer.d.ts +103 -9
  47. package/dist/esm/async-queuer.js +59 -2
  48. package/dist/esm/async-queuer.js.map +1 -1
  49. package/dist/esm/async-rate-limiter.d.ts +94 -27
  50. package/dist/esm/async-rate-limiter.js +52 -2
  51. package/dist/esm/async-rate-limiter.js.map +1 -1
  52. package/dist/esm/async-retryer.d.ts +312 -0
  53. package/dist/esm/async-retryer.js +286 -0
  54. package/dist/esm/async-retryer.js.map +1 -0
  55. package/dist/esm/async-throttler.d.ts +85 -9
  56. package/dist/esm/async-throttler.js +102 -53
  57. package/dist/esm/async-throttler.js.map +1 -1
  58. package/dist/esm/batcher.d.ts +13 -1
  59. package/dist/esm/batcher.js +5 -0
  60. package/dist/esm/batcher.js.map +1 -1
  61. package/dist/esm/debouncer.d.ts +10 -2
  62. package/dist/esm/debouncer.js +6 -1
  63. package/dist/esm/debouncer.js.map +1 -1
  64. package/dist/esm/event-client.d.ts +3 -0
  65. package/dist/esm/event-client.js.map +1 -1
  66. package/dist/esm/index.d.ts +1 -0
  67. package/dist/esm/index.js +23 -10
  68. package/dist/esm/index.js.map +1 -1
  69. package/dist/esm/queuer.d.ts +11 -3
  70. package/dist/esm/queuer.js +6 -1
  71. package/dist/esm/queuer.js.map +1 -1
  72. package/dist/esm/rate-limiter.d.ts +11 -0
  73. package/dist/esm/rate-limiter.js +6 -1
  74. package/dist/esm/rate-limiter.js.map +1 -1
  75. package/dist/esm/throttler.d.ts +11 -0
  76. package/dist/esm/throttler.js +6 -1
  77. package/dist/esm/throttler.js.map +1 -1
  78. package/dist/esm/utils.js.map +1 -1
  79. package/package.json +12 -2
  80. package/src/async-batcher.ts +146 -19
  81. package/src/async-debouncer.ts +120 -30
  82. package/src/async-queuer.ts +149 -11
  83. package/src/async-rate-limiter.ts +131 -30
  84. package/src/async-retryer.ts +669 -0
  85. package/src/async-throttler.ts +198 -72
  86. package/src/batcher.ts +18 -1
  87. package/src/debouncer.ts +19 -2
  88. package/src/event-client.ts +3 -0
  89. package/src/index.ts +2 -0
  90. package/src/queuer.ts +21 -3
  91. package/src/rate-limiter.ts +20 -0
  92. 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: unknown,
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
- * Rate limiting is a simple approach that allows a function to execute up to a limit within a time window,
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
- * The rate limiter supports two types of windows:
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
- * Unlike the non-async RateLimiter, this async version supports returning values from the rate-limited function,
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
- const result = await this.fn(...args) // EXECUTE!
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, args, this)
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
- * Unlike the non-async rate limiter, this async version supports returning values from the rate-limited function,
478
- * making it ideal for API calls and other async operations where you want the result of the `maybeExecute` call
479
- * instead of setting the result on a state variable from within the rate-limited function.
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
- * The rate limiter supports two types of windows:
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
- * Note that rate limiting is a simpler form of execution control compared to throttling or debouncing:
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