@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.
Files changed (99) hide show
  1. package/dist/cjs/async-batcher.cjs +66 -5
  2. package/dist/cjs/async-batcher.cjs.map +1 -1
  3. package/dist/cjs/async-batcher.d.cts +94 -17
  4. package/dist/cjs/async-debouncer.cjs +53 -19
  5. package/dist/cjs/async-debouncer.cjs.map +1 -1
  6. package/dist/cjs/async-debouncer.d.cts +76 -10
  7. package/dist/cjs/async-queuer.cjs +58 -2
  8. package/dist/cjs/async-queuer.cjs.map +1 -1
  9. package/dist/cjs/async-queuer.d.cts +100 -10
  10. package/dist/cjs/async-rate-limiter.cjs +51 -2
  11. package/dist/cjs/async-rate-limiter.cjs.map +1 -1
  12. package/dist/cjs/async-rate-limiter.d.cts +91 -28
  13. package/dist/cjs/async-retryer.cjs +285 -0
  14. package/dist/cjs/async-retryer.cjs.map +1 -0
  15. package/dist/cjs/async-retryer.d.cts +308 -0
  16. package/dist/cjs/async-throttler.cjs +101 -53
  17. package/dist/cjs/async-throttler.cjs.map +1 -1
  18. package/dist/cjs/async-throttler.d.cts +82 -10
  19. package/dist/cjs/batcher.cjs +5 -1
  20. package/dist/cjs/batcher.cjs.map +1 -1
  21. package/dist/cjs/batcher.d.cts +10 -2
  22. package/dist/cjs/debouncer.cjs +5 -1
  23. package/dist/cjs/debouncer.cjs.map +1 -1
  24. package/dist/cjs/debouncer.d.cts +7 -3
  25. package/dist/cjs/event-client.cjs +3 -1
  26. package/dist/cjs/event-client.cjs.map +1 -1
  27. package/dist/cjs/event-client.d.cts +3 -0
  28. package/dist/cjs/index.cjs +13 -1
  29. package/dist/cjs/index.cjs.map +1 -1
  30. package/dist/cjs/index.d.cts +1 -0
  31. package/dist/cjs/queuer.cjs +5 -1
  32. package/dist/cjs/queuer.cjs.map +1 -1
  33. package/dist/cjs/queuer.d.cts +8 -4
  34. package/dist/cjs/rate-limiter.cjs +5 -1
  35. package/dist/cjs/rate-limiter.cjs.map +1 -1
  36. package/dist/cjs/rate-limiter.d.cts +8 -1
  37. package/dist/cjs/throttler.cjs +5 -1
  38. package/dist/cjs/throttler.cjs.map +1 -1
  39. package/dist/cjs/throttler.d.cts +7 -0
  40. package/dist/cjs/utils.cjs +0 -10
  41. package/dist/cjs/utils.cjs.map +1 -1
  42. package/dist/cjs/utils.d.cts +0 -1
  43. package/dist/esm/async-batcher.d.ts +94 -17
  44. package/dist/esm/async-batcher.js +68 -7
  45. package/dist/esm/async-batcher.js.map +1 -1
  46. package/dist/esm/async-debouncer.d.ts +76 -10
  47. package/dist/esm/async-debouncer.js +55 -21
  48. package/dist/esm/async-debouncer.js.map +1 -1
  49. package/dist/esm/async-queuer.d.ts +100 -10
  50. package/dist/esm/async-queuer.js +60 -4
  51. package/dist/esm/async-queuer.js.map +1 -1
  52. package/dist/esm/async-rate-limiter.d.ts +91 -28
  53. package/dist/esm/async-rate-limiter.js +53 -4
  54. package/dist/esm/async-rate-limiter.js.map +1 -1
  55. package/dist/esm/async-retryer.d.ts +308 -0
  56. package/dist/esm/async-retryer.js +285 -0
  57. package/dist/esm/async-retryer.js.map +1 -0
  58. package/dist/esm/async-throttler.d.ts +82 -10
  59. package/dist/esm/async-throttler.js +103 -55
  60. package/dist/esm/async-throttler.js.map +1 -1
  61. package/dist/esm/batcher.d.ts +10 -2
  62. package/dist/esm/batcher.js +6 -2
  63. package/dist/esm/batcher.js.map +1 -1
  64. package/dist/esm/debouncer.d.ts +7 -3
  65. package/dist/esm/debouncer.js +7 -3
  66. package/dist/esm/debouncer.js.map +1 -1
  67. package/dist/esm/event-client.d.ts +3 -0
  68. package/dist/esm/event-client.js +3 -1
  69. package/dist/esm/event-client.js.map +1 -1
  70. package/dist/esm/index.d.ts +1 -0
  71. package/dist/esm/index.js +24 -12
  72. package/dist/esm/index.js.map +1 -1
  73. package/dist/esm/queuer.d.ts +8 -4
  74. package/dist/esm/queuer.js +7 -3
  75. package/dist/esm/queuer.js.map +1 -1
  76. package/dist/esm/rate-limiter.d.ts +8 -1
  77. package/dist/esm/rate-limiter.js +7 -3
  78. package/dist/esm/rate-limiter.js.map +1 -1
  79. package/dist/esm/throttler.d.ts +7 -0
  80. package/dist/esm/throttler.js +7 -3
  81. package/dist/esm/throttler.js.map +1 -1
  82. package/dist/esm/utils.d.ts +0 -1
  83. package/dist/esm/utils.js +0 -10
  84. package/dist/esm/utils.js.map +1 -1
  85. package/package.json +13 -3
  86. package/src/async-batcher.ts +144 -22
  87. package/src/async-debouncer.ts +118 -33
  88. package/src/async-queuer.ts +147 -14
  89. package/src/async-rate-limiter.ts +129 -33
  90. package/src/async-retryer.ts +664 -0
  91. package/src/async-throttler.ts +196 -75
  92. package/src/batcher.ts +16 -4
  93. package/src/debouncer.ts +17 -5
  94. package/src/event-client.ts +6 -1
  95. package/src/index.ts +2 -0
  96. package/src/queuer.ts +19 -6
  97. package/src/rate-limiter.ts +18 -3
  98. package/src/throttler.ts +17 -2
  99. package/src/utils.ts +0 -15
@@ -1,6 +1,8 @@
1
1
  import { Store } from '@tanstack/store'
2
- import { createKey, parseFunctionOrValue } from './utils'
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, 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
  *
@@ -200,9 +231,9 @@ export class AsyncThrottler<TFn extends AnyAsyncFunction> {
200
231
  readonly store: Store<Readonly<AsyncThrottlerState<TFn>>> = new Store<
201
232
  AsyncThrottlerState<TFn>
202
233
  >(getDefaultAsyncThrottlerState<TFn>())
203
- key: string
234
+ key: string | undefined
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)
@@ -212,13 +243,14 @@ export class AsyncThrottler<TFn extends AnyAsyncFunction> {
212
243
  public fn: TFn,
213
244
  initialOptions: AsyncThrottlerOptions<TFn>,
214
245
  ) {
215
- this.key = createKey(initialOptions.key)
246
+ this.key = initialOptions.key
216
247
  this.options = {
217
248
  ...defaultOptions,
218
249
  ...initialOptions,
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>)
@@ -301,88 +333,119 @@ export class AsyncThrottler<TFn extends AnyAsyncFunction> {
301
333
  ...args: Parameters<TFn>
302
334
  ): Promise<ReturnType<TFn> | undefined> => {
303
335
  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
336
+
337
+ this.#resolvePreviousPromiseInternal()
338
+
308
339
  this.#setState({
309
- lastArgs: args,
310
340
  maybeExecuteCount: this.store.state.maybeExecuteCount + 1,
341
+ lastArgs: args, // store the arguments for potential trailing execution
311
342
  })
312
343
 
313
- this.#resolvePreviousPromiseInternal()
344
+ const wait = this.#getWait()
345
+ const thisMaybeExecuteNumber = this.store.state.maybeExecuteCount
314
346
 
315
- // Handle leading execution
316
- if (this.options.leading && timeSinceLastExecution >= wait) {
317
- await this.#execute(...args)
318
- return this.store.state.lastResult
319
- } else {
347
+ // Wait for the wait period for the previous execution to complete if it's still running
348
+ for (
349
+ let maxNumIterations = wait / 10;
350
+ this.store.state.isExecuting && maxNumIterations > 0;
351
+ maxNumIterations--
352
+ ) {
353
+ await new Promise((resolve) => setTimeout(resolve, 10))
354
+ if (this.store.state.maybeExecuteCount !== thisMaybeExecuteNumber) {
355
+ // cancel the current maybeExecute loop because a new maybeExecute call was made
356
+ return this.store.state.lastResult
357
+ }
358
+ }
359
+
360
+ const now = Date.now()
361
+ const timeSinceLastExecution = now - this.store.state.lastExecutionTime
362
+
363
+ if (
364
+ this.options.leading &&
365
+ !this.store.state.isPending &&
366
+ timeSinceLastExecution >= wait
367
+ ) {
368
+ await this.#execute(...args) // Leading EXECUTE!
369
+ } else if (this.options.trailing) {
370
+ // replace old pending execution with a new one
371
+ this.cancel()
372
+ this.#setState({
373
+ isPending: true,
374
+ })
375
+
376
+ // Set up new trailing execution
320
377
  return new Promise((resolve, reject) => {
321
378
  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
- }
379
+
380
+ const newTimeSinceLastExecution = this.store.state.lastExecutionTime
381
+ ? now - this.store.state.lastExecutionTime
382
+ : 0
383
+ const timeoutDuration = Math.max(0, wait - newTimeSinceLastExecution)
384
+
385
+ this.#timeoutId = setTimeout(async () => {
386
+ this.#clearTimeout()
387
+ if (this.store.state.lastArgs !== undefined) {
388
+ try {
389
+ await this.#execute(...this.store.state.lastArgs) // Trailing EXECUTE!
390
+ } catch (error) {
391
+ reject(error)
339
392
  }
340
- this.#resolvePreviousPromise = null
341
- resolve(this.store.state.lastResult)
342
- }, timeoutDuration)
343
- }
393
+ }
394
+ this.#resolvePreviousPromise = null
395
+ resolve(this.store.state.lastResult)
396
+ }, timeoutDuration)
344
397
  })
345
398
  }
399
+ return this.store.state.lastResult
346
400
  }
347
401
 
348
402
  #execute = async (
349
403
  ...args: Parameters<TFn>
350
404
  ): Promise<ReturnType<TFn> | undefined> => {
351
- if (!this.#getEnabled() || this.store.state.isExecuting) return undefined
352
- this.#abortController = new AbortController()
405
+ if (!this.#getEnabled()) return undefined
406
+
407
+ const currentMaybeExecute = this.store.state.maybeExecuteCount
408
+
353
409
  try {
354
410
  this.#setState({ isExecuting: true })
355
- const result = await this.fn(...args) // EXECUTE!
411
+ const currentAsyncRetryer = new AsyncRetryer(this.fn, {
412
+ ...this.options.asyncRetryerOptions,
413
+ key: `${this.key}-retryer-${currentMaybeExecute}`,
414
+ })
415
+ this.asyncRetryers.set(currentMaybeExecute, currentAsyncRetryer)
416
+ const result = await currentAsyncRetryer.execute(...args) // EXECUTE!
356
417
  this.#setState({
357
418
  lastResult: result,
358
419
  successCount: this.store.state.successCount + 1,
359
420
  })
360
- this.options.onSuccess?.(result, args, this)
421
+ this.options.onSuccess?.(result as ReturnType<TFn>, args, this)
361
422
  } catch (error) {
362
423
  this.#setState({
363
424
  errorCount: this.store.state.errorCount + 1,
364
425
  })
365
- this.options.onError?.(error, args, this)
426
+ this.options.onError?.(error as Error, args, this)
366
427
  if (this.options.throwOnError) {
367
428
  throw error
368
429
  }
369
430
  } finally {
431
+ this.asyncRetryers.delete(currentMaybeExecute) // dispose retryer
370
432
  const lastExecutionTime = Date.now()
371
- const nextExecutionTime = lastExecutionTime + this.#getWait()
433
+ const wait = this.#getWait()
434
+ const nextExecutionTime = lastExecutionTime + wait
372
435
  this.#setState({
373
436
  isExecuting: false,
374
- isPending: false,
437
+ isPending: !!this.#timeoutId,
375
438
  settleCount: this.store.state.settleCount + 1,
376
439
  lastExecutionTime,
377
440
  nextExecutionTime,
378
441
  })
379
- this.#abortController = null
380
442
  this.options.onSettled?.(args, this)
381
443
  setTimeout(() => {
382
444
  if (!this.store.state.isPending) {
445
+ // clear nextExecutionTime if there is no pending execution
383
446
  this.#setState({ nextExecutionTime: undefined })
384
447
  }
385
- }, this.#getWait())
448
+ }, wait)
386
449
  }
387
450
  return this.store.state.lastResult
388
451
  }
@@ -392,12 +455,21 @@ export class AsyncThrottler<TFn extends AnyAsyncFunction> {
392
455
  */
393
456
  flush = async (): Promise<ReturnType<TFn> | undefined> => {
394
457
  if (this.store.state.isPending && this.store.state.lastArgs) {
395
- this.#abortExecution() // abort any current execution
396
- this.#clearTimeout() // clear any existing timeout
458
+ // Store the pending promise resolver before clearing timeout
459
+ const resolvePromise = this.#resolvePreviousPromise
460
+
461
+ // Clear timeout and state without resolving the promise
462
+ this.#clearTimeout()
463
+ this.#setState({
464
+ isPending: false,
465
+ })
466
+
397
467
  const result = await this.#execute(...this.store.state.lastArgs)
398
468
 
399
- // Resolve any pending promise from maybeExecute
400
- this.#resolvePreviousPromiseInternal()
469
+ // Resolve the pending promise with the result
470
+ if (resolvePromise) {
471
+ resolvePromise(result)
472
+ }
401
473
 
402
474
  return result
403
475
  }
@@ -418,7 +490,51 @@ export class AsyncThrottler<TFn extends AnyAsyncFunction> {
418
490
  }
419
491
  }
420
492
 
421
- #cancelPendingExecution = (): void => {
493
+ /**
494
+ * Returns the AbortSignal for a specific execution.
495
+ * If no maybeExecuteCount is provided, returns the signal for the most recent execution.
496
+ * Returns null if no execution is found or not currently executing.
497
+ *
498
+ * @param maybeExecuteCount - Optional specific execution to get signal for
499
+ * @example
500
+ * ```typescript
501
+ * const throttler = new AsyncThrottler(
502
+ * async (data: string) => {
503
+ * const signal = throttler.getAbortSignal()
504
+ * if (signal) {
505
+ * const response = await fetch('/api/save', {
506
+ * method: 'POST',
507
+ * body: data,
508
+ * signal
509
+ * })
510
+ * return response.json()
511
+ * }
512
+ * },
513
+ * { wait: 1000 }
514
+ * )
515
+ * ```
516
+ */
517
+ getAbortSignal(maybeExecuteCount?: number): AbortSignal | null {
518
+ const count = maybeExecuteCount ?? this.store.state.maybeExecuteCount
519
+ const retryer = this.asyncRetryers.get(count)
520
+ return retryer?.getAbortSignal() ?? null
521
+ }
522
+
523
+ /**
524
+ * Aborts all ongoing executions with the internal abort controllers.
525
+ * Does NOT cancel any pending execution that have not started yet.
526
+ */
527
+ abort = (): void => {
528
+ this.asyncRetryers.forEach((retryer) => retryer.abort())
529
+ this.asyncRetryers.clear()
530
+ this.#setState({ isExecuting: false })
531
+ }
532
+
533
+ /**
534
+ * Cancels any pending execution that have not started yet.
535
+ * Does NOT abort any execution already in progress.
536
+ */
537
+ cancel = (): void => {
422
538
  this.#clearTimeout()
423
539
  if (this.#resolvePreviousPromise) {
424
540
  this.#resolvePreviousPromiseInternal()
@@ -426,31 +542,15 @@ export class AsyncThrottler<TFn extends AnyAsyncFunction> {
426
542
  }
427
543
  this.#setState({
428
544
  isPending: false,
429
- isExecuting: false,
430
- lastArgs: undefined,
431
545
  })
432
546
  }
433
547
 
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
548
  /**
450
549
  * Resets the debouncer state to its default values
451
550
  */
452
551
  reset = (): void => {
453
552
  this.#setState(getDefaultAsyncThrottlerState<TFn>())
553
+ this.asyncRetryers.forEach((retryer) => retryer.reset())
454
554
  }
455
555
  }
456
556
 
@@ -459,9 +559,30 @@ export class AsyncThrottler<TFn extends AnyAsyncFunction> {
459
559
  * The throttled function will execute at most once per wait period, even if called multiple times.
460
560
  * If called while executing, it will wait until execution completes before scheduling the next call.
461
561
  *
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.
562
+ * Async vs Sync Versions:
563
+ * The async version provides advanced features over the sync throttle function:
564
+ * - Returns promises that can be awaited for throttled function results
565
+ * - Built-in retry support via AsyncRetryer integration
566
+ * - Abort support to cancel in-flight executions
567
+ * - Cancel support to prevent pending executions from starting
568
+ * - Comprehensive error handling with onError callbacks and throwOnError control
569
+ * - Detailed execution tracking (success/error/settle counts)
570
+ * - Waits for ongoing executions to complete before scheduling the next one
571
+ *
572
+ * The sync throttle function is lighter weight and simpler when you don't need async features,
573
+ * return values, or execution control.
574
+ *
575
+ * What is Throttling?
576
+ * Throttling limits how often a function can be executed, allowing only one execution within a specified time window.
577
+ * Unlike debouncing which resets the delay timer on each call, throttling ensures the function executes at a
578
+ * regular interval regardless of how often it's called.
579
+ *
580
+ * Configuration Options:
581
+ * - `wait`: Time window in milliseconds during which the function can only execute once (required)
582
+ * - `leading`: Execute immediately when called (default: true)
583
+ * - `trailing`: Execute on the trailing edge of the wait period (default: true)
584
+ * - `enabled`: Whether the throttler is enabled (default: true)
585
+ * - `asyncRetryerOptions`: Configure retry behavior for executions
465
586
  *
466
587
  * Error Handling:
467
588
  * - If an `onError` handler is provided, it will be called with the error and throttler instance
package/src/batcher.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { Store } from '@tanstack/store'
2
- import { createKey, parseFunctionOrValue } from './utils'
2
+ import { parseFunctionOrValue } from './utils'
3
3
  import { emitChange, pacerEventClient } from './event-client'
4
4
  import type { OptionalKeys } from './types'
5
5
 
@@ -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)
@@ -145,7 +146,7 @@ export class Batcher<TValue> {
145
146
  readonly store: Store<Readonly<BatcherState<TValue>>> = new Store(
146
147
  getDefaultBatcherState<TValue>(),
147
148
  )
148
- key: string
149
+ key: string | undefined
149
150
  options: BatcherOptionsWithOptionalCallbacks<TValue>
150
151
  #timeoutId: NodeJS.Timeout | null = null
151
152
 
@@ -153,7 +154,7 @@ export class Batcher<TValue> {
153
154
  public fn: (items: Array<TValue>) => void,
154
155
  initialOptions: BatcherOptions<TValue>,
155
156
  ) {
156
- this.key = createKey(initialOptions.key)
157
+ this.key = initialOptions.key
157
158
  this.options = {
158
159
  ...defaultOptions,
159
160
  ...initialOptions,
@@ -275,6 +276,15 @@ export class Batcher<TValue> {
275
276
  this.#setState({ items: [], isPending: false })
276
277
  }
277
278
 
279
+ /**
280
+ * Cancels any pending execution that was scheduled.
281
+ * Does NOT clear out the items.
282
+ */
283
+ cancel = (): void => {
284
+ this.#clearTimeout()
285
+ this.#setState({ isPending: false })
286
+ }
287
+
278
288
  /**
279
289
  * Resets the batcher state to its default values
280
290
  */
@@ -285,7 +295,9 @@ export class Batcher<TValue> {
285
295
  }
286
296
 
287
297
  /**
288
- * Creates a batcher that processes items in batches
298
+ * Creates a batcher that processes items in batches.
299
+ *
300
+ * 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
301
  *
290
302
  * @example
291
303
  * ```ts
package/src/debouncer.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { Store } from '@tanstack/store'
2
- import { createKey, parseFunctionOrValue } from './utils'
2
+ import { parseFunctionOrValue } from './utils'
3
3
  import { emitChange, pacerEventClient } from './event-client'
4
4
  import type { AnyFunction } from './types'
5
5
 
@@ -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
@@ -130,7 +143,7 @@ export class Debouncer<TFn extends AnyFunction> {
130
143
  readonly store: Store<Readonly<DebouncerState<TFn>>> = new Store(
131
144
  getDefaultDebouncerState<TFn>(),
132
145
  )
133
- key: string
146
+ key: string | undefined
134
147
  options: DebouncerOptions<TFn>
135
148
  #timeoutId: NodeJS.Timeout | undefined
136
149
 
@@ -138,7 +151,7 @@ export class Debouncer<TFn extends AnyFunction> {
138
151
  public fn: TFn,
139
152
  initialOptions: DebouncerOptions<TFn>,
140
153
  ) {
141
- this.key = createKey(initialOptions.key)
154
+ this.key = initialOptions.key
142
155
  this.options = {
143
156
  ...defaultOptions,
144
157
  ...initialOptions,
@@ -285,8 +298,7 @@ export class Debouncer<TFn extends AnyFunction> {
285
298
  * Creates a debounced function that delays invoking the provided function until after a specified wait time.
286
299
  * Multiple calls during the wait period will cancel previous pending invocations and reset the timer.
287
300
  *
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.
301
+ * 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
302
  *
291
303
  * If leading option is true, the function will execute immediately on the first call, then wait the delay
292
304
  * 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>
@@ -57,7 +60,9 @@ export const emitChange = <
57
60
  event: TSuffix,
58
61
  payload: PacerEventMap[`pacer:${TSuffix}`],
59
62
  ) => {
60
- pacerEventClient.emit(event, payload)
63
+ if (payload.key) {
64
+ pacerEventClient.emit(event, payload)
65
+ }
61
66
  }
62
67
 
63
68
  export const pacerEventClient = new PacerEventClient()
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
@@ -1,5 +1,5 @@
1
1
  import { Store } from '@tanstack/store'
2
- import { createKey, parseFunctionOrValue } from './utils'
2
+ import { parseFunctionOrValue } from './utils'
3
3
  import { emitChange, pacerEventClient } from './event-client'
4
4
 
5
5
  export interface QueuerState<TValue> {
@@ -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
@@ -256,7 +270,7 @@ export class Queuer<TValue> {
256
270
  readonly store: Store<Readonly<QueuerState<TValue>>> = new Store(
257
271
  getDefaultQueuerState<TValue>(),
258
272
  )
259
- key: string
273
+ key: string | undefined
260
274
  options: QueuerOptions<TValue>
261
275
  #timeoutId: NodeJS.Timeout | null = null
262
276
 
@@ -264,7 +278,7 @@ export class Queuer<TValue> {
264
278
  public fn: (item: TValue) => void,
265
279
  initialOptions: QueuerOptions<TValue> = {},
266
280
  ) {
267
- this.key = createKey(initialOptions.key)
281
+ this.key = initialOptions.key
268
282
  this.options = {
269
283
  ...defaultOptions,
270
284
  ...initialOptions,
@@ -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)
@@ -681,9 +696,7 @@ export class Queuer<TValue> {
681
696
  * Creates a queue that processes items immediately upon addition.
682
697
  * Items are processed sequentially in FIFO order by default.
683
698
  *
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.
699
+ * 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
700
  *
688
701
  * State Management:
689
702
  * - Uses TanStack Store for reactive state management