@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.
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 +13 -3
  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, 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: unknown,
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
- #abortController: AbortController | null = null
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
- const now = Date.now()
305
- const timeSinceLastExecution = now - this.store.state.lastExecutionTime
306
- const wait = this.#getWait()
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.#resolvePreviousPromiseInternal()
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
- // Handle leading execution
316
- if (this.options.leading && timeSinceLastExecution >= wait) {
317
- await this.#execute(...args)
318
- return this.store.state.lastResult
319
- } else {
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
- // Clear any existing timeout to ensure we use the latest arguments
323
- this.#clearTimeout()
324
-
325
- // Set up trailing execution if enabled
326
- if (this.options.trailing) {
327
- const _timeSinceLastExecution = this.store.state.lastExecutionTime
328
- ? now - this.store.state.lastExecutionTime
329
- : 0
330
- const timeoutDuration = wait - _timeSinceLastExecution
331
- this.#setState({ isPending: true })
332
- this.#timeoutId = setTimeout(async () => {
333
- if (this.store.state.lastArgs !== undefined) {
334
- try {
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
- this.#resolvePreviousPromise = null
341
- resolve(this.store.state.lastResult)
342
- }, timeoutDuration)
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() || this.store.state.isExecuting) return undefined
352
- this.#abortController = new AbortController()
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 result = await this.fn(...args) // EXECUTE!
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, args, this)
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 nextExecutionTime = lastExecutionTime + this.#getWait()
438
+ const wait = this.#getWait()
439
+ const nextExecutionTime = lastExecutionTime + wait
372
440
  this.#setState({
373
441
  isExecuting: false,
374
- isPending: false,
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
- }, this.#getWait())
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
- this.#abortExecution() // abort any current execution
396
- this.#clearTimeout() // clear any existing timeout
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 any pending promise from maybeExecute
400
- this.#resolvePreviousPromiseInternal()
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
- #cancelPendingExecution = (): void => {
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
- * Unlike the non-async Throttler, this async version supports returning values from the throttled function,
463
- * making it ideal for API calls and other async operations where you want the result of the `maybeExecute` call
464
- * instead of setting the result on a state variable from within the throttled function.
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 the the simple function wrapper implementation pulled from the Debouncer class. If you need
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.
@@ -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 a simplified wrapper around the Queuer class that only exposes the
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
@@ -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