@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
@@ -70,12 +70,17 @@ export interface RateLimiterOptions<TFn extends AnyFunction> {
70
70
  */
71
71
  windowType?: 'fixed' | 'sliding';
72
72
  }
73
+ /**
74
+ * Utility function for sharing common `RateLimiterOptions` options between different `RateLimiter` instances.
75
+ */
76
+ export declare function rateLimiterOptions<TFn extends AnyFunction = AnyFunction, TOptions extends Partial<RateLimiterOptions<TFn>> = Partial<RateLimiterOptions<TFn>>>(options: TOptions): TOptions;
73
77
  /**
74
78
  * A class that creates a rate-limited function.
75
79
  *
76
80
  * Rate limiting is a simple approach that allows a function to execute up to a limit within a time window,
77
81
  * then blocks all subsequent calls until the window passes. This can lead to "bursty" behavior where
78
82
  * all executions happen immediately, followed by a complete block.
83
+ * 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.
79
84
  *
80
85
  * The rate limiter supports two types of windows:
81
86
  * - 'fixed': A strict window that resets after the window period. All executions within the window count
@@ -121,6 +126,10 @@ export declare class RateLimiter<TFn extends AnyFunction> {
121
126
  key: string;
122
127
  options: RateLimiterOptions<TFn>;
123
128
  constructor(fn: TFn, initialOptions: RateLimiterOptions<TFn>);
129
+ /**
130
+ * Emits a change event for the rate limiter instance. Mostly useful for devtools.
131
+ */
132
+ _emit: () => void;
124
133
  /**
125
134
  * Updates the rate limiter options
126
135
  */
@@ -157,6 +166,8 @@ export declare class RateLimiter<TFn extends AnyFunction> {
157
166
  /**
158
167
  * Creates a rate-limited function that will execute the provided function up to a maximum number of times within a time window.
159
168
  *
169
+ * 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.
170
+ *
160
171
  * Note that rate limiting is a simpler form of execution control compared to throttling or debouncing:
161
172
  * - A rate limiter will allow all executions until the limit is reached, then block all subsequent calls until the window resets
162
173
  * - A throttler ensures even spacing between executions, which can be better for consistent performance
@@ -14,6 +14,9 @@ function getDefaultThrottlerState() {
14
14
  maybeExecuteCount: 0
15
15
  };
16
16
  }
17
+ function throttlerOptions(options) {
18
+ return options;
19
+ }
17
20
  const defaultOptions = {
18
21
  enabled: true,
19
22
  leading: true,
@@ -26,6 +29,7 @@ class Throttler {
26
29
  this.store = new store.Store(
27
30
  getDefaultThrottlerState()
28
31
  );
32
+ this._emit = () => eventClient.emitChange("Throttler", this);
29
33
  this.setOptions = (newOptions) => {
30
34
  this.options = { ...this.options, ...newOptions };
31
35
  if (!this.#getEnabled()) {
@@ -144,4 +148,5 @@ function throttle(fn, initialOptions) {
144
148
  }
145
149
  exports.Throttler = Throttler;
146
150
  exports.throttle = throttle;
151
+ exports.throttlerOptions = throttlerOptions;
147
152
  //# sourceMappingURL=throttler.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"throttler.cjs","sources":["../../src/throttler.ts"],"sourcesContent":["import { Store } from '@tanstack/store'\nimport { createKey, parseFunctionOrValue } from './utils'\nimport { emitChange, pacerEventClient } from './event-client'\nimport type { AnyFunction } from './types'\n\nexport interface ThrottlerState<TFn extends AnyFunction> {\n /**\n * Number of function executions that have been completed\n */\n executionCount: number\n /**\n * Whether the throttler is waiting for the timeout to trigger execution\n */\n isPending: boolean\n /**\n * The arguments from the most recent call to maybeExecute\n */\n lastArgs: Parameters<TFn> | undefined\n /**\n * Timestamp of the last function execution in milliseconds\n */\n lastExecutionTime: number\n /**\n * Number of times maybeExecute has been called (for reduction calculations)\n */\n maybeExecuteCount: number\n /**\n * Timestamp when the next execution can occur in milliseconds\n */\n nextExecutionTime: number | undefined\n /**\n * Current execution status - 'idle' when not active, 'pending' when waiting for timeout\n */\n status: 'disabled' | 'idle' | 'pending'\n}\n\nfunction getDefaultThrottlerState<\n TFn extends AnyFunction,\n>(): ThrottlerState<TFn> {\n return {\n executionCount: 0,\n isPending: false,\n lastArgs: undefined,\n lastExecutionTime: 0,\n nextExecutionTime: 0,\n status: 'idle',\n maybeExecuteCount: 0,\n }\n}\n\n/**\n * Options for configuring a throttled function\n */\nexport interface ThrottlerOptions<TFn extends AnyFunction> {\n /**\n * Whether the throttler is enabled. When disabled, maybeExecute will not trigger any executions.\n * Can be a boolean or a function that returns a boolean.\n * Defaults to true.\n */\n enabled?: boolean | ((throttler: Throttler<TFn>) => boolean)\n /**\n * Initial state for the throttler\n */\n initialState?: Partial<ThrottlerState<TFn>>\n /**\n * A key to identify the throttler.\n * If provided, the throttler will be identified by this key in the devtools and PacerProvider if applicable.\n */\n key?: string\n /**\n * Whether to execute on the leading edge of the timeout.\n * Defaults to true.\n */\n leading?: boolean\n /**\n * Callback function that is called after the function is executed\n */\n onExecute?: (args: Parameters<TFn>, throttler: Throttler<TFn>) => void\n /**\n * Whether to execute on the trailing edge of the timeout.\n * Defaults to true.\n */\n trailing?: boolean\n /**\n * Time window in milliseconds during which the function can only be executed once.\n * Can be a number or a function that returns a number.\n * Defaults to 0ms\n */\n wait: number | ((throttler: Throttler<TFn>) => number)\n}\n\nconst defaultOptions: Omit<\n Required<ThrottlerOptions<any>>,\n 'initialState' | 'onExecute' | 'key'\n> = {\n enabled: true,\n leading: true,\n trailing: true,\n wait: 0,\n}\n\n/**\n * A class that creates a throttled function.\n *\n * Throttling ensures a function is called at most once within a specified time window.\n * Unlike debouncing which waits for a pause in calls, throttling guarantees consistent\n * execution timing regardless of call frequency.\n *\n * Supports both leading and trailing edge execution:\n * - Leading: Execute immediately on first call (default: true)\n * - Trailing: Execute after wait period if called during throttle (default: true)\n *\n * For collapsing rapid-fire events where you only care about the last call, consider using Debouncer.\n *\n * State Management:\n * - Uses TanStack Store for reactive state management\n * - Use `initialState` to provide initial state values when creating the throttler\n * - Use `onExecute` callback to react to function execution and implement custom logic\n * - The state includes execution count, last execution time, pending status, and more\n * - State can be accessed via `throttler.store.state` when using the class directly\n * - When using framework adapters (React/Solid), state is accessed from `throttler.state`\n *\n * @example\n * ```ts\n * const throttler = new Throttler(\n * (id: string) => api.getData(id),\n * { wait: 1000 } // Execute at most once per second\n * );\n *\n * // First call executes immediately\n * throttler.maybeExecute('123');\n *\n * // Subsequent calls within 1000ms are throttled\n * throttler.maybeExecute('123'); // Throttled\n * ```\n */\nexport class Throttler<TFn extends AnyFunction> {\n readonly store: Store<Readonly<ThrottlerState<TFn>>> = new Store(\n getDefaultThrottlerState(),\n )\n key: string | undefined\n options: ThrottlerOptions<TFn>\n #timeoutId: NodeJS.Timeout | undefined\n\n constructor(\n public fn: TFn,\n initialOptions: ThrottlerOptions<TFn>,\n ) {\n this.key = createKey(initialOptions.key)\n this.options = {\n ...defaultOptions,\n ...initialOptions,\n }\n this.#setState(this.options.initialState ?? {})\n\n pacerEventClient.on('d-Throttler', (event) => {\n if (event.payload.key !== this.key) return\n this.#setState(event.payload.store.state as ThrottlerState<TFn>)\n this.setOptions(event.payload.options)\n })\n }\n\n /**\n * Updates the throttler options\n */\n setOptions = (newOptions: Partial<ThrottlerOptions<TFn>>): void => {\n this.options = { ...this.options, ...newOptions }\n\n // Cancel pending execution if the throttler is disabled\n if (!this.#getEnabled()) {\n this.cancel()\n }\n }\n\n #setState = (newState: Partial<ThrottlerState<TFn>>): void => {\n this.store.setState((state) => {\n const combinedState = {\n ...state,\n ...newState,\n }\n const { isPending } = combinedState\n return {\n ...combinedState,\n status: !this.#getEnabled()\n ? 'disabled'\n : isPending\n ? 'pending'\n : 'idle',\n }\n })\n emitChange('Throttler', this)\n }\n\n #getEnabled = (): boolean => {\n return !!parseFunctionOrValue(this.options.enabled, this)\n }\n\n #getWait = (): number => {\n return parseFunctionOrValue(this.options.wait, this)\n }\n\n /**\n * Attempts to execute the throttled function. The execution behavior depends on the throttler options:\n *\n * - If enough time has passed since the last execution (>= wait period):\n * - With leading=true: Executes immediately\n * - With leading=false: Waits for the next trailing execution\n *\n * - If within the wait period:\n * - With trailing=true: Schedules execution for end of wait period\n * - With trailing=false: Drops the execution\n *\n * @example\n * ```ts\n * const throttled = new Throttler(fn, { wait: 1000 });\n *\n * // First call executes immediately\n * throttled.maybeExecute('a', 'b');\n *\n * // Call during wait period - gets throttled\n * throttled.maybeExecute('c', 'd');\n * ```\n */\n maybeExecute = (...args: Parameters<TFn>): void => {\n this.#setState({\n maybeExecuteCount: this.store.state.maybeExecuteCount + 1,\n })\n\n const now = Date.now()\n const timeSinceLastExecution = now - this.store.state.lastExecutionTime\n const wait = this.#getWait()\n\n // Handle leading execution\n if (this.options.leading && timeSinceLastExecution >= wait) {\n this.#execute(...args)\n } else {\n // Store the most recent arguments for potential trailing execution\n this.#setState({\n lastArgs: args,\n })\n // Set up trailing execution if not already scheduled\n if (!this.#timeoutId && this.options.trailing) {\n // prevent large number if lastExecutionTime is undefined\n const _timeSinceLastExecution = this.store.state.lastExecutionTime\n ? now - this.store.state.lastExecutionTime\n : 0\n const timeoutDuration = wait - _timeSinceLastExecution\n this.#setState({ isPending: true })\n this.#timeoutId = setTimeout(() => {\n const { lastArgs } = this.store.state\n if (lastArgs !== undefined) {\n this.#execute(...lastArgs)\n }\n }, timeoutDuration)\n }\n }\n }\n\n #execute = (...args: Parameters<TFn>): void => {\n if (!this.#getEnabled()) return\n this.fn(...args) // EXECUTE!\n const lastExecutionTime = Date.now()\n const nextExecutionTime = lastExecutionTime + this.#getWait()\n this.#clearTimeout()\n this.#setState({\n executionCount: this.store.state.executionCount + 1,\n lastExecutionTime,\n nextExecutionTime,\n isPending: false,\n lastArgs: undefined,\n })\n this.options.onExecute?.(args, this)\n setTimeout(() => {\n if (!this.store.state.isPending) {\n this.#setState({ nextExecutionTime: undefined })\n }\n }, this.#getWait())\n }\n\n /**\n * Processes the current pending execution immediately\n */\n flush = (): void => {\n if (this.store.state.isPending && this.store.state.lastArgs) {\n this.#execute(...this.store.state.lastArgs)\n }\n }\n\n #clearTimeout = (): void => {\n if (this.#timeoutId) {\n clearTimeout(this.#timeoutId)\n this.#timeoutId = undefined\n }\n }\n\n /**\n * Cancels any pending trailing execution and clears internal state.\n *\n * If a trailing execution is scheduled (due to throttling with trailing=true),\n * this will prevent that execution from occurring. The internal timeout and\n * stored arguments will be cleared.\n *\n * Has no effect if there is no pending execution.\n */\n cancel = (): void => {\n this.#clearTimeout()\n this.#setState({\n lastArgs: undefined,\n isPending: false,\n })\n }\n\n /**\n * Resets the throttler state to its default values\n */\n reset = (): void => {\n this.#setState(getDefaultThrottlerState<TFn>())\n }\n}\n\n/**\n * Creates a throttled function that limits how often the provided function can execute.\n *\n * Throttling ensures a function executes at most once within a specified time window,\n * regardless of how many times it is called. This is useful for rate-limiting\n * expensive operations or UI updates.\n *\n * The throttled function can be configured to execute on the leading and/or trailing\n * edge of the throttle window via options.\n *\n * For handling bursts of events, consider using debounce() instead. For hard execution\n * limits, consider using rateLimit().\n *\n * State Management:\n * - Uses TanStack Store for reactive state management\n * - Use `initialState` to provide initial state values when creating the throttler\n * - Use `onExecute` callback to react to function execution and implement custom logic\n * - The state includes execution count, last execution time, pending status, and more\n * - State can be accessed via the underlying Throttler instance's `store.state` property\n * - When using framework adapters (React/Solid), state is accessed from the hook's state property\n *\n * @example\n * ```ts\n * // Basic throttling - max once per second\n * const throttled = throttle(updateUI, { wait: 1000 });\n *\n * // Configure leading/trailing execution\n * const throttled = throttle(saveData, {\n * wait: 2000,\n * leading: true, // Execute immediately on first call\n * trailing: true // Execute again after delay if called during wait\n * });\n * ```\n */\nexport function throttle<TFn extends AnyFunction>(\n fn: TFn,\n initialOptions: ThrottlerOptions<TFn>,\n) {\n const throttler = new Throttler(fn, initialOptions)\n return throttler.maybeExecute\n}\n"],"names":["Store","emitChange","parseFunctionOrValue","createKey","pacerEventClient"],"mappings":"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"}
1
+ {"version":3,"file":"throttler.cjs","sources":["../../src/throttler.ts"],"sourcesContent":["import { Store } from '@tanstack/store'\nimport { createKey, parseFunctionOrValue } from './utils'\nimport { emitChange, pacerEventClient } from './event-client'\nimport type { AnyFunction } from './types'\n\nexport interface ThrottlerState<TFn extends AnyFunction> {\n /**\n * Number of function executions that have been completed\n */\n executionCount: number\n /**\n * Whether the throttler is waiting for the timeout to trigger execution\n */\n isPending: boolean\n /**\n * The arguments from the most recent call to maybeExecute\n */\n lastArgs: Parameters<TFn> | undefined\n /**\n * Timestamp of the last function execution in milliseconds\n */\n lastExecutionTime: number\n /**\n * Number of times maybeExecute has been called (for reduction calculations)\n */\n maybeExecuteCount: number\n /**\n * Timestamp when the next execution can occur in milliseconds\n */\n nextExecutionTime: number | undefined\n /**\n * Current execution status - 'idle' when not active, 'pending' when waiting for timeout\n */\n status: 'disabled' | 'idle' | 'pending'\n}\n\nfunction getDefaultThrottlerState<\n TFn extends AnyFunction,\n>(): ThrottlerState<TFn> {\n return {\n executionCount: 0,\n isPending: false,\n lastArgs: undefined,\n lastExecutionTime: 0,\n nextExecutionTime: 0,\n status: 'idle',\n maybeExecuteCount: 0,\n }\n}\n\n/**\n * Options for configuring a throttled function\n */\nexport interface ThrottlerOptions<TFn extends AnyFunction> {\n /**\n * Whether the throttler is enabled. When disabled, maybeExecute will not trigger any executions.\n * Can be a boolean or a function that returns a boolean.\n * Defaults to true.\n */\n enabled?: boolean | ((throttler: Throttler<TFn>) => boolean)\n /**\n * Initial state for the throttler\n */\n initialState?: Partial<ThrottlerState<TFn>>\n /**\n * A key to identify the throttler.\n * If provided, the throttler will be identified by this key in the devtools and PacerProvider if applicable.\n */\n key?: string\n /**\n * Whether to execute on the leading edge of the timeout.\n * Defaults to true.\n */\n leading?: boolean\n /**\n * Callback function that is called after the function is executed\n */\n onExecute?: (args: Parameters<TFn>, throttler: Throttler<TFn>) => void\n /**\n * Whether to execute on the trailing edge of the timeout.\n * Defaults to true.\n */\n trailing?: boolean\n /**\n * Time window in milliseconds during which the function can only be executed once.\n * Can be a number or a function that returns a number.\n * Defaults to 0ms\n */\n wait: number | ((throttler: Throttler<TFn>) => number)\n}\n\n/**\n * Utility function for sharing common `ThrottlerOptions` options between different `Throttler` instances.\n */\nexport function throttlerOptions<\n TFn extends AnyFunction = AnyFunction,\n TOptions extends Partial<ThrottlerOptions<TFn>> = Partial<\n ThrottlerOptions<TFn>\n >,\n>(options: TOptions): TOptions {\n return options\n}\n\nconst defaultOptions: Omit<\n Required<ThrottlerOptions<any>>,\n 'initialState' | 'onExecute' | 'key'\n> = {\n enabled: true,\n leading: true,\n trailing: true,\n wait: 0,\n}\n\n/**\n * A class that creates a throttled function.\n *\n * Throttling ensures a function is called at most once within a specified time window.\n * Unlike debouncing which waits for a pause in calls, throttling guarantees consistent\n * execution timing regardless of call frequency.\n * This synchronous version is lighter weight and often all you need - upgrade to AsyncThrottler when you need promises, retry support, abort/cancel capabilities, or advanced error handling.\n *\n * Supports both leading and trailing edge execution:\n * - Leading: Execute immediately on first call (default: true)\n * - Trailing: Execute after wait period if called during throttle (default: true)\n *\n * For collapsing rapid-fire events where you only care about the last call, consider using Debouncer.\n *\n * State Management:\n * - Uses TanStack Store for reactive state management\n * - Use `initialState` to provide initial state values when creating the throttler\n * - Use `onExecute` callback to react to function execution and implement custom logic\n * - The state includes execution count, last execution time, pending status, and more\n * - State can be accessed via `throttler.store.state` when using the class directly\n * - When using framework adapters (React/Solid), state is accessed from `throttler.state`\n *\n * @example\n * ```ts\n * const throttler = new Throttler(\n * (id: string) => api.getData(id),\n * { wait: 1000 } // Execute at most once per second\n * );\n *\n * // First call executes immediately\n * throttler.maybeExecute('123');\n *\n * // Subsequent calls within 1000ms are throttled\n * throttler.maybeExecute('123'); // Throttled\n * ```\n */\nexport class Throttler<TFn extends AnyFunction> {\n readonly store: Store<Readonly<ThrottlerState<TFn>>> = new Store(\n getDefaultThrottlerState(),\n )\n key: string | undefined\n options: ThrottlerOptions<TFn>\n #timeoutId: NodeJS.Timeout | undefined\n\n constructor(\n public fn: TFn,\n initialOptions: ThrottlerOptions<TFn>,\n ) {\n this.key = createKey(initialOptions.key)\n this.options = {\n ...defaultOptions,\n ...initialOptions,\n }\n this.#setState(this.options.initialState ?? {})\n\n pacerEventClient.on('d-Throttler', (event) => {\n if (event.payload.key !== this.key) return\n this.#setState(event.payload.store.state as ThrottlerState<TFn>)\n this.setOptions(event.payload.options)\n })\n }\n\n /**\n * Emits a change event for the throttler instance. Mostly useful for devtools.\n */\n _emit = () => emitChange('Throttler', this)\n\n /**\n * Updates the throttler options\n */\n setOptions = (newOptions: Partial<ThrottlerOptions<TFn>>): void => {\n this.options = { ...this.options, ...newOptions }\n\n // Cancel pending execution if the throttler is disabled\n if (!this.#getEnabled()) {\n this.cancel()\n }\n }\n\n #setState = (newState: Partial<ThrottlerState<TFn>>): void => {\n this.store.setState((state) => {\n const combinedState = {\n ...state,\n ...newState,\n }\n const { isPending } = combinedState\n return {\n ...combinedState,\n status: !this.#getEnabled()\n ? 'disabled'\n : isPending\n ? 'pending'\n : 'idle',\n }\n })\n emitChange('Throttler', this)\n }\n\n #getEnabled = (): boolean => {\n return !!parseFunctionOrValue(this.options.enabled, this)\n }\n\n #getWait = (): number => {\n return parseFunctionOrValue(this.options.wait, this)\n }\n\n /**\n * Attempts to execute the throttled function. The execution behavior depends on the throttler options:\n *\n * - If enough time has passed since the last execution (>= wait period):\n * - With leading=true: Executes immediately\n * - With leading=false: Waits for the next trailing execution\n *\n * - If within the wait period:\n * - With trailing=true: Schedules execution for end of wait period\n * - With trailing=false: Drops the execution\n *\n * @example\n * ```ts\n * const throttled = new Throttler(fn, { wait: 1000 });\n *\n * // First call executes immediately\n * throttled.maybeExecute('a', 'b');\n *\n * // Call during wait period - gets throttled\n * throttled.maybeExecute('c', 'd');\n * ```\n */\n maybeExecute = (...args: Parameters<TFn>): void => {\n this.#setState({\n maybeExecuteCount: this.store.state.maybeExecuteCount + 1,\n })\n\n const now = Date.now()\n const timeSinceLastExecution = now - this.store.state.lastExecutionTime\n const wait = this.#getWait()\n\n // Handle leading execution\n if (this.options.leading && timeSinceLastExecution >= wait) {\n this.#execute(...args)\n } else {\n // Store the most recent arguments for potential trailing execution\n this.#setState({\n lastArgs: args,\n })\n // Set up trailing execution if not already scheduled\n if (!this.#timeoutId && this.options.trailing) {\n // prevent large number if lastExecutionTime is undefined\n const _timeSinceLastExecution = this.store.state.lastExecutionTime\n ? now - this.store.state.lastExecutionTime\n : 0\n const timeoutDuration = wait - _timeSinceLastExecution\n this.#setState({ isPending: true })\n this.#timeoutId = setTimeout(() => {\n const { lastArgs } = this.store.state\n if (lastArgs !== undefined) {\n this.#execute(...lastArgs)\n }\n }, timeoutDuration)\n }\n }\n }\n\n #execute = (...args: Parameters<TFn>): void => {\n if (!this.#getEnabled()) return\n this.fn(...args) // EXECUTE!\n const lastExecutionTime = Date.now()\n const nextExecutionTime = lastExecutionTime + this.#getWait()\n this.#clearTimeout()\n this.#setState({\n executionCount: this.store.state.executionCount + 1,\n lastExecutionTime,\n nextExecutionTime,\n isPending: false,\n lastArgs: undefined,\n })\n this.options.onExecute?.(args, this)\n setTimeout(() => {\n if (!this.store.state.isPending) {\n this.#setState({ nextExecutionTime: undefined })\n }\n }, this.#getWait())\n }\n\n /**\n * Processes the current pending execution immediately\n */\n flush = (): void => {\n if (this.store.state.isPending && this.store.state.lastArgs) {\n this.#execute(...this.store.state.lastArgs)\n }\n }\n\n #clearTimeout = (): void => {\n if (this.#timeoutId) {\n clearTimeout(this.#timeoutId)\n this.#timeoutId = undefined\n }\n }\n\n /**\n * Cancels any pending trailing execution and clears internal state.\n *\n * If a trailing execution is scheduled (due to throttling with trailing=true),\n * this will prevent that execution from occurring. The internal timeout and\n * stored arguments will be cleared.\n *\n * Has no effect if there is no pending execution.\n */\n cancel = (): void => {\n this.#clearTimeout()\n this.#setState({\n lastArgs: undefined,\n isPending: false,\n })\n }\n\n /**\n * Resets the throttler state to its default values\n */\n reset = (): void => {\n this.#setState(getDefaultThrottlerState<TFn>())\n }\n}\n\n/**\n * Creates a throttled function that limits how often the provided function can execute.\n *\n * This synchronous version is lighter weight and often all you need - upgrade to asyncThrottle when you need promises, retry support, abort/cancel capabilities, or advanced error handling.\n *\n * Throttling ensures a function executes at most once within a specified time window,\n * regardless of how many times it is called. This is useful for rate-limiting\n * expensive operations or UI updates.\n *\n * The throttled function can be configured to execute on the leading and/or trailing\n * edge of the throttle window via options.\n *\n * For handling bursts of events, consider using debounce() instead. For hard execution\n * limits, consider using rateLimit().\n *\n * State Management:\n * - Uses TanStack Store for reactive state management\n * - Use `initialState` to provide initial state values when creating the throttler\n * - Use `onExecute` callback to react to function execution and implement custom logic\n * - The state includes execution count, last execution time, pending status, and more\n * - State can be accessed via the underlying Throttler instance's `store.state` property\n * - When using framework adapters (React/Solid), state is accessed from the hook's state property\n *\n * @example\n * ```ts\n * // Basic throttling - max once per second\n * const throttled = throttle(updateUI, { wait: 1000 });\n *\n * // Configure leading/trailing execution\n * const throttled = throttle(saveData, {\n * wait: 2000,\n * leading: true, // Execute immediately on first call\n * trailing: true // Execute again after delay if called during wait\n * });\n * ```\n */\nexport function throttle<TFn extends AnyFunction>(\n fn: TFn,\n initialOptions: ThrottlerOptions<TFn>,\n) {\n const throttler = new Throttler(fn, initialOptions)\n return throttler.maybeExecute\n}\n"],"names":["Store","emitChange","parseFunctionOrValue","createKey","pacerEventClient"],"mappings":";;;;;AAoCA,SAAS,2BAEgB;AACvB,SAAO;AAAA,IACL,gBAAgB;AAAA,IAChB,WAAW;AAAA,IACX,UAAU;AAAA,IACV,mBAAmB;AAAA,IACnB,mBAAmB;AAAA,IACnB,QAAQ;AAAA,IACR,mBAAmB;AAAA,EAAA;AAEvB;AA8CO,SAAS,iBAKd,SAA6B;AAC7B,SAAO;AACT;AAEA,MAAM,iBAGF;AAAA,EACF,SAAS;AAAA,EACT,SAAS;AAAA,EACT,UAAU;AAAA,EACV,MAAM;AACR;AAsCO,MAAM,UAAmC;AAAA,EAQ9C,YACS,IACP,gBACA;AAFO,SAAA,KAAA;AART,SAAS,QAA8C,IAAIA,MAAAA;AAAAA,MACzD,yBAAA;AAAA,IAAyB;AA2B3B,SAAA,QAAQ,MAAMC,uBAAW,aAAa,IAAI;AAK1C,SAAA,aAAa,CAAC,eAAqD;AACjE,WAAK,UAAU,EAAE,GAAG,KAAK,SAAS,GAAG,WAAA;AAGrC,UAAI,CAAC,KAAK,eAAe;AACvB,aAAK,OAAA;AAAA,MACP;AAAA,IACF;AAEA,SAAA,YAAY,CAAC,aAAiD;AAC5D,WAAK,MAAM,SAAS,CAAC,UAAU;AAC7B,cAAM,gBAAgB;AAAA,UACpB,GAAG;AAAA,UACH,GAAG;AAAA,QAAA;AAEL,cAAM,EAAE,cAAc;AACtB,eAAO;AAAA,UACL,GAAG;AAAA,UACH,QAAQ,CAAC,KAAK,gBACV,aACA,YACE,YACA;AAAA,QAAA;AAAA,MAEV,CAAC;AACDA,kBAAAA,WAAW,aAAa,IAAI;AAAA,IAC9B;AAEA,SAAA,cAAc,MAAe;AAC3B,aAAO,CAAC,CAACC,MAAAA,qBAAqB,KAAK,QAAQ,SAAS,IAAI;AAAA,IAC1D;AAEA,SAAA,WAAW,MAAc;AACvB,aAAOA,MAAAA,qBAAqB,KAAK,QAAQ,MAAM,IAAI;AAAA,IACrD;AAwBA,SAAA,eAAe,IAAI,SAAgC;AACjD,WAAK,UAAU;AAAA,QACb,mBAAmB,KAAK,MAAM,MAAM,oBAAoB;AAAA,MAAA,CACzD;AAED,YAAM,MAAM,KAAK,IAAA;AACjB,YAAM,yBAAyB,MAAM,KAAK,MAAM,MAAM;AACtD,YAAM,OAAO,KAAK,SAAA;AAGlB,UAAI,KAAK,QAAQ,WAAW,0BAA0B,MAAM;AAC1D,aAAK,SAAS,GAAG,IAAI;AAAA,MACvB,OAAO;AAEL,aAAK,UAAU;AAAA,UACb,UAAU;AAAA,QAAA,CACX;AAED,YAAI,CAAC,KAAK,cAAc,KAAK,QAAQ,UAAU;AAE7C,gBAAM,0BAA0B,KAAK,MAAM,MAAM,oBAC7C,MAAM,KAAK,MAAM,MAAM,oBACvB;AACJ,gBAAM,kBAAkB,OAAO;AAC/B,eAAK,UAAU,EAAE,WAAW,KAAA,CAAM;AAClC,eAAK,aAAa,WAAW,MAAM;AACjC,kBAAM,EAAE,SAAA,IAAa,KAAK,MAAM;AAChC,gBAAI,aAAa,QAAW;AAC1B,mBAAK,SAAS,GAAG,QAAQ;AAAA,YAC3B;AAAA,UACF,GAAG,eAAe;AAAA,QACpB;AAAA,MACF;AAAA,IACF;AAEA,SAAA,WAAW,IAAI,SAAgC;AAC7C,UAAI,CAAC,KAAK,cAAe;AACzB,WAAK,GAAG,GAAG,IAAI;AACf,YAAM,oBAAoB,KAAK,IAAA;AAC/B,YAAM,oBAAoB,oBAAoB,KAAK,SAAA;AACnD,WAAK,cAAA;AACL,WAAK,UAAU;AAAA,QACb,gBAAgB,KAAK,MAAM,MAAM,iBAAiB;AAAA,QAClD;AAAA,QACA;AAAA,QACA,WAAW;AAAA,QACX,UAAU;AAAA,MAAA,CACX;AACD,WAAK,QAAQ,YAAY,MAAM,IAAI;AACnC,iBAAW,MAAM;AACf,YAAI,CAAC,KAAK,MAAM,MAAM,WAAW;AAC/B,eAAK,UAAU,EAAE,mBAAmB,OAAA,CAAW;AAAA,QACjD;AAAA,MACF,GAAG,KAAK,UAAU;AAAA,IACpB;AAKA,SAAA,QAAQ,MAAY;AAClB,UAAI,KAAK,MAAM,MAAM,aAAa,KAAK,MAAM,MAAM,UAAU;AAC3D,aAAK,SAAS,GAAG,KAAK,MAAM,MAAM,QAAQ;AAAA,MAC5C;AAAA,IACF;AAEA,SAAA,gBAAgB,MAAY;AAC1B,UAAI,KAAK,YAAY;AACnB,qBAAa,KAAK,UAAU;AAC5B,aAAK,aAAa;AAAA,MACpB;AAAA,IACF;AAWA,SAAA,SAAS,MAAY;AACnB,WAAK,cAAA;AACL,WAAK,UAAU;AAAA,QACb,UAAU;AAAA,QACV,WAAW;AAAA,MAAA,CACZ;AAAA,IACH;AAKA,SAAA,QAAQ,MAAY;AAClB,WAAK,UAAU,0BAA+B;AAAA,IAChD;AA9KE,SAAK,MAAMC,gBAAU,eAAe,GAAG;AACvC,SAAK,UAAU;AAAA,MACb,GAAG;AAAA,MACH,GAAG;AAAA,IAAA;AAEL,SAAK,UAAU,KAAK,QAAQ,gBAAgB,CAAA,CAAE;AAE9CC,gBAAAA,iBAAiB,GAAG,eAAe,CAAC,UAAU;AAC5C,UAAI,MAAM,QAAQ,QAAQ,KAAK,IAAK;AACpC,WAAK,UAAU,MAAM,QAAQ,MAAM,KAA4B;AAC/D,WAAK,WAAW,MAAM,QAAQ,OAAO;AAAA,IACvC,CAAC;AAAA,EACH;AAAA,EAlBA;AAAA,EAqCA;AAAA,EAmBA;AAAA,EAIA;AAAA,EA6DA;AAAA,EA8BA;AA8BF;AAsCO,SAAS,SACd,IACA,gBACA;AACA,QAAM,YAAY,IAAI,UAAU,IAAI,cAAc;AAClD,SAAO,UAAU;AACnB;;;;"}
@@ -70,12 +70,17 @@ export interface ThrottlerOptions<TFn extends AnyFunction> {
70
70
  */
71
71
  wait: number | ((throttler: Throttler<TFn>) => number);
72
72
  }
73
+ /**
74
+ * Utility function for sharing common `ThrottlerOptions` options between different `Throttler` instances.
75
+ */
76
+ export declare function throttlerOptions<TFn extends AnyFunction = AnyFunction, TOptions extends Partial<ThrottlerOptions<TFn>> = Partial<ThrottlerOptions<TFn>>>(options: TOptions): TOptions;
73
77
  /**
74
78
  * A class that creates a throttled function.
75
79
  *
76
80
  * Throttling ensures a function is called at most once within a specified time window.
77
81
  * Unlike debouncing which waits for a pause in calls, throttling guarantees consistent
78
82
  * execution timing regardless of call frequency.
83
+ * This synchronous version is lighter weight and often all you need - upgrade to AsyncThrottler when you need promises, retry support, abort/cancel capabilities, or advanced error handling.
79
84
  *
80
85
  * Supports both leading and trailing edge execution:
81
86
  * - Leading: Execute immediately on first call (default: true)
@@ -112,6 +117,10 @@ export declare class Throttler<TFn extends AnyFunction> {
112
117
  key: string | undefined;
113
118
  options: ThrottlerOptions<TFn>;
114
119
  constructor(fn: TFn, initialOptions: ThrottlerOptions<TFn>);
120
+ /**
121
+ * Emits a change event for the throttler instance. Mostly useful for devtools.
122
+ */
123
+ _emit: () => void;
115
124
  /**
116
125
  * Updates the throttler options
117
126
  */
@@ -161,6 +170,8 @@ export declare class Throttler<TFn extends AnyFunction> {
161
170
  /**
162
171
  * Creates a throttled function that limits how often the provided function can execute.
163
172
  *
173
+ * This synchronous version is lighter weight and often all you need - upgrade to asyncThrottle when you need promises, retry support, abort/cancel capabilities, or advanced error handling.
174
+ *
164
175
  * Throttling ensures a function executes at most once within a specified time window,
165
176
  * regardless of how many times it is called. This is useful for rate-limiting
166
177
  * expensive operations or UI updates.
@@ -1 +1 @@
1
- {"version":3,"file":"utils.cjs","sources":["../../src/utils.ts"],"sourcesContent":["import type { AnyFunction } from './types'\n\nexport function isFunction<T extends AnyFunction>(value: any): value is T {\n return typeof value === 'function'\n}\n\nexport function parseFunctionOrValue<T, TArgs extends Array<any>>(\n value: T | ((...args: TArgs) => T),\n ...args: TArgs\n): T {\n return isFunction(value) ? value(...args) : value\n}\n\nexport function createKey(key?: string): string {\n if (key) {\n return key\n }\n\n if (\n typeof crypto !== 'undefined' &&\n typeof crypto.randomUUID === 'function'\n ) {\n return crypto.randomUUID()\n }\n\n return ''\n}\n"],"names":[],"mappings":";;AAEO,SAAS,WAAkC,OAAwB;AACxE,SAAO,OAAO,UAAU;AAC1B;AAEO,SAAS,qBACd,UACG,MACA;AACH,SAAO,WAAW,KAAK,IAAI,MAAM,GAAG,IAAI,IAAI;AAC9C;AAEO,SAAS,UAAU,KAAsB;AAC9C,MAAI,KAAK;AACP,WAAO;AAAA,EAAA;AAGT,MACE,OAAO,WAAW,eAClB,OAAO,OAAO,eAAe,YAC7B;AACA,WAAO,OAAO,WAAA;AAAA,EAAW;AAG3B,SAAO;AACT;;;;"}
1
+ {"version":3,"file":"utils.cjs","sources":["../../src/utils.ts"],"sourcesContent":["import type { AnyFunction } from './types'\n\nexport function isFunction<T extends AnyFunction>(value: any): value is T {\n return typeof value === 'function'\n}\n\nexport function parseFunctionOrValue<T, TArgs extends Array<any>>(\n value: T | ((...args: TArgs) => T),\n ...args: TArgs\n): T {\n return isFunction(value) ? value(...args) : value\n}\n\nexport function createKey(key?: string): string {\n if (key) {\n return key\n }\n\n if (\n typeof crypto !== 'undefined' &&\n typeof crypto.randomUUID === 'function'\n ) {\n return crypto.randomUUID()\n }\n\n return ''\n}\n"],"names":[],"mappings":";;AAEO,SAAS,WAAkC,OAAwB;AACxE,SAAO,OAAO,UAAU;AAC1B;AAEO,SAAS,qBACd,UACG,MACA;AACH,SAAO,WAAW,KAAK,IAAI,MAAM,GAAG,IAAI,IAAI;AAC9C;AAEO,SAAS,UAAU,KAAsB;AAC9C,MAAI,KAAK;AACP,WAAO;AAAA,EACT;AAEA,MACE,OAAO,WAAW,eAClB,OAAO,OAAO,eAAe,YAC7B;AACA,WAAO,OAAO,WAAA;AAAA,EAChB;AAEA,SAAO;AACT;;;;"}
@@ -1,10 +1,15 @@
1
1
  import { Store } from '@tanstack/store';
2
+ import { AsyncRetryer, AsyncRetryerOptions } from './async-retryer.js';
2
3
  import { OptionalKeys } from './types.js';
3
4
  export interface AsyncBatcherState<TValue> {
4
5
  /**
5
6
  * Number of batch executions that have resulted in errors
6
7
  */
7
8
  errorCount: number;
9
+ /**
10
+ * Number of batch executions that have been executed
11
+ */
12
+ executeCount: number;
8
13
  /**
9
14
  * Array of items that failed during batch processing
10
15
  */
@@ -58,6 +63,10 @@ export interface AsyncBatcherState<TValue> {
58
63
  * Options for configuring an AsyncBatcher instance
59
64
  */
60
65
  export interface AsyncBatcherOptions<TValue> {
66
+ /**
67
+ * Options for configuring the underlying async retryer
68
+ */
69
+ asyncRetryerOptions?: AsyncRetryerOptions<(items: Array<TValue>) => Promise<any>>;
61
70
  /**
62
71
  * Custom function to determine if a batch should be processed
63
72
  * Return true to process the batch immediately
@@ -82,7 +91,7 @@ export interface AsyncBatcherOptions<TValue> {
82
91
  * If provided, the handler will be called with the error, the batch of items that failed, and batcher instance.
83
92
  * This can be used alongside throwOnError - the handler will be called before any error is thrown.
84
93
  */
85
- onError?: (error: unknown, batch: Array<TValue>, batcher: AsyncBatcher<TValue>) => void;
94
+ onError?: (error: Error, batch: Array<TValue>, batcher: AsyncBatcher<TValue>) => void;
86
95
  /**
87
96
  * Callback fired after items are added to the batcher
88
97
  */
@@ -114,17 +123,28 @@ export interface AsyncBatcherOptions<TValue> {
114
123
  */
115
124
  wait?: number | ((asyncBatcher: AsyncBatcher<TValue>) => number);
116
125
  }
126
+ /**
127
+ * Utility function for sharing common `AsyncBatcherOptions` options between different `AsyncBatcher` instances.
128
+ *
129
+ */
130
+ export declare function asyncBatcherOptions<TValue = any, TOptions extends Partial<AsyncBatcherOptions<TValue>> = Partial<AsyncBatcherOptions<TValue>>>(options: TOptions): TOptions;
117
131
  type AsyncBatcherOptionsWithOptionalCallbacks<TValue> = OptionalKeys<Required<AsyncBatcherOptions<TValue>>, 'initialState' | 'onError' | 'onItemsChange' | 'onSettled' | 'onSuccess' | 'key'>;
118
132
  /**
119
133
  * A class that collects items and processes them in batches asynchronously.
120
134
  *
121
- * This is the async version of the Batcher class. Unlike the sync version, this async batcher:
122
- * - Handles promises and returns results from batch executions
123
- * - Provides error handling with configurable error behavior
124
- * - Tracks success, error, and settle counts separately
125
- * - Has state tracking for when batches are executing
126
- * - Returns the result of the batch function execution
135
+ * Async vs Sync Versions:
136
+ * The async version provides advanced features over the sync Batcher:
137
+ * - Returns promises that can be awaited for batch results
138
+ * - Built-in retry support via AsyncRetryer integration
139
+ * - Abort support to cancel in-flight batch executions
140
+ * - Cancel support to prevent pending batches from starting
141
+ * - Comprehensive error handling with onError callbacks and throwOnError control
142
+ * - Detailed execution tracking (success/error/settle counts)
143
+ *
144
+ * The sync Batcher is lighter weight and simpler when you don't need async features,
145
+ * return values, or execution control.
127
146
  *
147
+ * What is Batching?
128
148
  * Batching is a technique for grouping multiple operations together to be processed as a single unit.
129
149
  *
130
150
  * The AsyncBatcher provides a flexible way to implement async batching with configurable:
@@ -182,7 +202,12 @@ export declare class AsyncBatcher<TValue> {
182
202
  readonly store: Store<Readonly<AsyncBatcherState<TValue>>>;
183
203
  key: string;
184
204
  options: AsyncBatcherOptionsWithOptionalCallbacks<TValue>;
205
+ asyncRetryers: Map<number, AsyncRetryer<(items: Array<TValue>) => Promise<any>>>;
185
206
  constructor(fn: (items: Array<TValue>) => Promise<any>, initialOptions: AsyncBatcherOptions<TValue>);
207
+ /**
208
+ * Emits a change event for the async batcher instance. Mostly useful for devtools.
209
+ */
210
+ _emit: () => void;
186
211
  /**
187
212
  * Updates the async batcher options
188
213
  */
@@ -190,8 +215,12 @@ export declare class AsyncBatcher<TValue> {
190
215
  /**
191
216
  * Adds an item to the async batcher
192
217
  * If the batch size is reached, timeout occurs, or shouldProcess returns true, the batch will be processed
218
+ *
219
+ * @returns The result from the batch function, or undefined if an error occurred and was handled by onError
220
+ *
221
+ * @throws The error from the batch function if no onError handler is configured or throwOnError is true
193
222
  */
194
- addItem: (item: TValue) => void;
223
+ addItem: (item: TValue) => Promise<any>;
195
224
  /**
196
225
  * Processes the current batch of items immediately
197
226
  */
@@ -205,19 +234,72 @@ export declare class AsyncBatcher<TValue> {
205
234
  * Removes all items from the async batcher
206
235
  */
207
236
  clear: () => void;
237
+ /**
238
+ * Returns the AbortSignal for a specific execution.
239
+ * If no executeCount is provided, returns the signal for the most recent execution.
240
+ * Returns null if no execution is found or not currently executing.
241
+ *
242
+ * @param executeCount - Optional specific execution to get signal for
243
+ * @example
244
+ * ```typescript
245
+ * const batcher = new AsyncBatcher(
246
+ * async (items: string[]) => {
247
+ * const signal = batcher.getAbortSignal()
248
+ * if (signal) {
249
+ * const response = await fetch('/api/batch', {
250
+ * method: 'POST',
251
+ * body: JSON.stringify(items),
252
+ * signal
253
+ * })
254
+ * return response.json()
255
+ * }
256
+ * },
257
+ * { maxSize: 10, wait: 100 }
258
+ * )
259
+ * ```
260
+ */
261
+ getAbortSignal(executeCount?: number): AbortSignal | null;
262
+ /**
263
+ * Aborts all ongoing executions with the internal abort controllers.
264
+ * Does NOT cancel any pending execution that have not started yet.
265
+ * Does NOT clear out the items.
266
+ */
267
+ abort: () => void;
268
+ /**
269
+ * Cancels any pending execution that have not started yet.
270
+ * Does NOT abort any execution already in progress.
271
+ * Does NOT clear out the items.
272
+ */
273
+ cancel: () => void;
208
274
  /**
209
275
  * Resets the async batcher state to its default values
210
276
  */
211
277
  reset: () => void;
212
278
  }
213
279
  /**
214
- * Creates an async batcher that processes items in batches
280
+ * Creates an async batcher that processes items in batches.
215
281
  *
216
- * Unlike the sync batcher, this async version:
217
- * - Handles promises and returns results from batch executions
218
- * - Provides error handling with configurable error behavior
219
- * - Tracks success, error, and settle counts separately
220
- * - Has state tracking for when batches are executing
282
+ * Async vs Sync Versions:
283
+ * The async version provides advanced features over the sync batch function:
284
+ * - Returns promises that can be awaited for batch results
285
+ * - Built-in retry support via AsyncRetryer integration
286
+ * - Abort support to cancel in-flight batch executions
287
+ * - Cancel support to prevent pending batches from starting
288
+ * - Comprehensive error handling with onError callbacks and throwOnError control
289
+ * - Detailed execution tracking (success/error/settle counts)
290
+ *
291
+ * The sync batch function is lighter weight and simpler when you don't need async features,
292
+ * return values, or execution control.
293
+ *
294
+ * What is Batching?
295
+ * Batching is a technique for grouping multiple operations together to be processed as a single unit.
296
+ *
297
+ * Configuration Options:
298
+ * - `maxSize`: Maximum number of items per batch (default: Infinity)
299
+ * - `wait`: Time to wait before processing batch (default: Infinity)
300
+ * - `getShouldExecute`: Custom logic to trigger batch processing
301
+ * - `asyncRetryerOptions`: Configure retry behavior for batch executions
302
+ * - `started`: Whether to start processing immediately (default: true)
221
303
  *
222
304
  * Error Handling:
223
305
  * - If an `onError` handler is provided, it will be called with the error, the batch of items that failed, and batcher instance
@@ -232,7 +314,6 @@ export declare class AsyncBatcher<TValue> {
232
314
  * - Use `onSuccess` callback to react to successful batch execution and implement custom logic
233
315
  * - Use `onError` callback to react to batch execution errors and implement custom error handling
234
316
  * - Use `onSettled` callback to react to batch execution completion (success or error) and implement custom logic
235
- * - Use `onExecute` callback to react to batch execution and implement custom logic
236
317
  * - Use `onItemsChange` callback to react to items being added or removed from the batcher
237
318
  * - The state includes total items processed, success/error counts, and execution status
238
319
  * - State can be accessed via the underlying AsyncBatcher instance's `store.state` property
@@ -259,5 +340,5 @@ export declare class AsyncBatcher<TValue> {
259
340
  * batchItems(3); // Triggers batch processing
260
341
  * ```
261
342
  */
262
- export declare function asyncBatch<TValue>(fn: (items: Array<TValue>) => Promise<any>, options: AsyncBatcherOptions<TValue>): (item: TValue) => void;
343
+ export declare function asyncBatch<TValue>(fn: (items: Array<TValue>) => Promise<any>, options: AsyncBatcherOptions<TValue>): (item: TValue) => Promise<any>;
263
344
  export {};
@@ -1,9 +1,11 @@
1
1
  import { Store } from "@tanstack/store";
2
+ import { AsyncRetryer } from "./async-retryer.js";
2
3
  import { parseFunctionOrValue, createKey } from "./utils.js";
3
4
  import { emitChange, pacerEventClient } from "./event-client.js";
4
5
  function getDefaultAsyncBatcherState() {
5
6
  return {
6
7
  errorCount: 0,
8
+ executeCount: 0,
7
9
  failedItems: [],
8
10
  isEmpty: true,
9
11
  isExecuting: false,
@@ -18,7 +20,13 @@ function getDefaultAsyncBatcherState() {
18
20
  totalItemsFailed: 0
19
21
  };
20
22
  }
23
+ function asyncBatcherOptions(options) {
24
+ return options;
25
+ }
21
26
  const defaultOptions = {
27
+ asyncRetryerOptions: {
28
+ maxAttempts: 1
29
+ },
22
30
  getShouldExecute: () => false,
23
31
  maxSize: Infinity,
24
32
  started: true,
@@ -31,7 +39,9 @@ class AsyncBatcher {
31
39
  this.store = new Store(
32
40
  getDefaultAsyncBatcherState()
33
41
  );
42
+ this.asyncRetryers = /* @__PURE__ */ new Map();
34
43
  this.#timeoutId = null;
44
+ this._emit = () => emitChange("AsyncBatcher", this);
35
45
  this.setOptions = (newOptions) => {
36
46
  this.options = { ...this.options, ...newOptions };
37
47
  };
@@ -56,7 +66,7 @@ class AsyncBatcher {
56
66
  this.#getWait = () => {
57
67
  return parseFunctionOrValue(this.options.wait, this);
58
68
  };
59
- this.addItem = (item) => {
69
+ this.addItem = async (item) => {
60
70
  this.#setState({
61
71
  items: [...this.store.state.items, item],
62
72
  isPending: this.options.wait !== Infinity
@@ -64,22 +74,29 @@ class AsyncBatcher {
64
74
  this.options.onItemsChange?.(this);
65
75
  const shouldProcess = this.store.state.items.length >= this.options.maxSize || this.options.getShouldExecute(this.store.state.items, this);
66
76
  if (shouldProcess) {
67
- this.#execute();
77
+ return await this.#execute();
68
78
  } else if (this.options.wait !== Infinity) {
69
79
  this.#clearTimeout();
70
80
  this.#timeoutId = setTimeout(() => this.#execute(), this.#getWait());
81
+ await new Promise((resolve) => setTimeout(resolve, this.#getWait()));
71
82
  }
72
83
  };
73
84
  this.#execute = async () => {
74
85
  if (this.store.state.items.length === 0) {
75
86
  return void 0;
76
87
  }
88
+ const currentExecuteCount = this.store.state.executeCount + 1;
77
89
  const batch = this.peekAllItems();
78
90
  this.clear();
79
91
  this.options.onItemsChange?.(this);
80
- this.#setState({ isExecuting: true });
92
+ this.#setState({ isExecuting: true, executeCount: currentExecuteCount });
81
93
  try {
82
- const result = await this.fn(batch);
94
+ const currentAsyncRetryer = new AsyncRetryer(
95
+ this.fn,
96
+ this.options.asyncRetryerOptions
97
+ );
98
+ this.asyncRetryers.set(currentExecuteCount, currentAsyncRetryer);
99
+ const result = await currentAsyncRetryer.execute(batch);
83
100
  this.#setState({
84
101
  totalItemsProcessed: this.store.state.totalItemsProcessed + batch.length,
85
102
  lastResult: result,
@@ -99,6 +116,7 @@ class AsyncBatcher {
99
116
  }
100
117
  return void 0;
101
118
  } finally {
119
+ this.asyncRetryers.delete(currentExecuteCount);
102
120
  this.#setState({
103
121
  isExecuting: false,
104
122
  settleCount: this.store.state.settleCount + 1
@@ -125,9 +143,23 @@ class AsyncBatcher {
125
143
  this.clear = () => {
126
144
  this.#setState({ items: [], failedItems: [], isPending: false });
127
145
  };
146
+ this.abort = () => {
147
+ this.asyncRetryers.forEach((retryer) => retryer.abort());
148
+ this.asyncRetryers.clear();
149
+ this.#setState({
150
+ isExecuting: false
151
+ });
152
+ };
153
+ this.cancel = () => {
154
+ this.#clearTimeout();
155
+ this.#setState({
156
+ isPending: false
157
+ });
158
+ };
128
159
  this.reset = () => {
129
160
  this.#setState(getDefaultAsyncBatcherState());
130
161
  this.options.onItemsChange?.(this);
162
+ this.asyncRetryers.forEach((retryer) => retryer.reset());
131
163
  };
132
164
  this.key = createKey(initialOptions.key);
133
165
  this.options = {
@@ -147,6 +179,35 @@ class AsyncBatcher {
147
179
  #getWait;
148
180
  #execute;
149
181
  #clearTimeout;
182
+ /**
183
+ * Returns the AbortSignal for a specific execution.
184
+ * If no executeCount is provided, returns the signal for the most recent execution.
185
+ * Returns null if no execution is found or not currently executing.
186
+ *
187
+ * @param executeCount - Optional specific execution to get signal for
188
+ * @example
189
+ * ```typescript
190
+ * const batcher = new AsyncBatcher(
191
+ * async (items: string[]) => {
192
+ * const signal = batcher.getAbortSignal()
193
+ * if (signal) {
194
+ * const response = await fetch('/api/batch', {
195
+ * method: 'POST',
196
+ * body: JSON.stringify(items),
197
+ * signal
198
+ * })
199
+ * return response.json()
200
+ * }
201
+ * },
202
+ * { maxSize: 10, wait: 100 }
203
+ * )
204
+ * ```
205
+ */
206
+ getAbortSignal(executeCount) {
207
+ const count = executeCount ?? this.store.state.executeCount;
208
+ const retryer = this.asyncRetryers.get(count);
209
+ return retryer?.getAbortSignal() ?? null;
210
+ }
150
211
  }
151
212
  function asyncBatch(fn, options) {
152
213
  const batcher = new AsyncBatcher(fn, options);
@@ -154,6 +215,7 @@ function asyncBatch(fn, options) {
154
215
  }
155
216
  export {
156
217
  AsyncBatcher,
157
- asyncBatch
218
+ asyncBatch,
219
+ asyncBatcherOptions
158
220
  };
159
221
  //# sourceMappingURL=async-batcher.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"async-batcher.js","sources":["../../src/async-batcher.ts"],"sourcesContent":["import { Store } from '@tanstack/store'\nimport { createKey, parseFunctionOrValue } from './utils'\nimport { emitChange, pacerEventClient } from './event-client'\nimport type { OptionalKeys } from './types'\n\nexport interface AsyncBatcherState<TValue> {\n /**\n * Number of batch executions that have resulted in errors\n */\n errorCount: number\n /**\n * Array of items that failed during batch processing\n */\n failedItems: Array<TValue>\n /**\n * Whether the batcher has no items to process (items array is empty)\n */\n isEmpty: boolean\n /**\n * Whether a batch is currently being processed asynchronously\n */\n isExecuting: boolean\n /**\n * Whether the batcher is waiting for the timeout to trigger batch processing\n */\n isPending: boolean\n /**\n * Array of items currently queued for batch processing\n */\n items: Array<TValue>\n /**\n * The result from the most recent batch execution\n */\n lastResult: any\n /**\n * Number of batch executions that have completed (either successfully or with errors)\n */\n settleCount: number\n /**\n * Number of items currently in the batch queue\n */\n size: number\n /**\n * Current processing status - 'idle' when not processing, 'pending' when waiting for timeout, 'executing' when processing, 'populated' when items are present, but no wait is configured\n */\n status: 'idle' | 'pending' | 'executing' | 'populated'\n /**\n * Number of batch executions that have completed successfully\n */\n successCount: number\n /**\n * Total number of items that have failed processing across all batches\n */\n totalItemsFailed: number\n /**\n * Total number of items that have been processed across all batches\n */\n totalItemsProcessed: number\n}\n\nfunction getDefaultAsyncBatcherState<TValue>(): AsyncBatcherState<TValue> {\n return {\n errorCount: 0,\n failedItems: [],\n isEmpty: true,\n isExecuting: false,\n isPending: false,\n items: [],\n lastResult: undefined,\n settleCount: 0,\n size: 0,\n status: 'idle',\n successCount: 0,\n totalItemsProcessed: 0,\n totalItemsFailed: 0,\n }\n}\n\n/**\n * Options for configuring an AsyncBatcher instance\n */\nexport interface AsyncBatcherOptions<TValue> {\n /**\n * Custom function to determine if a batch should be processed\n * Return true to process the batch immediately\n */\n getShouldExecute?: (\n items: Array<TValue>,\n batcher: AsyncBatcher<TValue>,\n ) => boolean\n /**\n * Initial state for the async batcher\n */\n initialState?: Partial<AsyncBatcherState<TValue>>\n /**\n * Optional key to identify this async batcher instance.\n * If provided, the async batcher will be identified by this key in the devtools and PacerProvider if applicable.\n */\n key?: string\n /**\n * Maximum number of items in a batch\n * @default Infinity\n */\n maxSize?: number\n /**\n * Optional error handler for when the batch function throws.\n * If provided, the handler will be called with the error, the batch of items that failed, and batcher instance.\n * This can be used alongside throwOnError - the handler will be called before any error is thrown.\n */\n onError?: (\n error: unknown,\n batch: Array<TValue>,\n batcher: AsyncBatcher<TValue>,\n ) => void\n /**\n * Callback fired after items are added to the batcher\n */\n onItemsChange?: (batcher: AsyncBatcher<TValue>) => void\n /**\n * Optional callback to call when a batch is settled (completed or failed)\n */\n onSettled?: (batch: Array<TValue>, batcher: AsyncBatcher<TValue>) => void\n /**\n * Optional callback to call when a batch succeeds\n */\n onSuccess?: (\n result: any,\n batch: Array<TValue>,\n batcher: AsyncBatcher<TValue>,\n ) => void\n /**\n * Whether the batcher should start processing immediately\n * @default true\n */\n started?: boolean\n /**\n * Whether to throw errors when they occur.\n * Defaults to true if no onError handler is provided, false if an onError handler is provided.\n * Can be explicitly set to override these defaults.\n */\n throwOnError?: boolean\n /**\n * Maximum time in milliseconds to wait before processing a batch.\n * If the wait duration has elapsed, the batch will be processed.\n * If not provided, the batch will not be triggered by a timeout.\n * @default Infinity\n */\n wait?: number | ((asyncBatcher: AsyncBatcher<TValue>) => number)\n}\n\ntype AsyncBatcherOptionsWithOptionalCallbacks<TValue> = OptionalKeys<\n Required<AsyncBatcherOptions<TValue>>,\n | 'initialState'\n | 'onError'\n | 'onItemsChange'\n | 'onSettled'\n | 'onSuccess'\n | 'key'\n>\n\nconst defaultOptions: AsyncBatcherOptionsWithOptionalCallbacks<any> = {\n getShouldExecute: () => false,\n maxSize: Infinity,\n started: true,\n throwOnError: true,\n wait: Infinity,\n}\n\n/**\n * A class that collects items and processes them in batches asynchronously.\n *\n * This is the async version of the Batcher class. Unlike the sync version, this async batcher:\n * - Handles promises and returns results from batch executions\n * - Provides error handling with configurable error behavior\n * - Tracks success, error, and settle counts separately\n * - Has state tracking for when batches are executing\n * - Returns the result of the batch function execution\n *\n * Batching is a technique for grouping multiple operations together to be processed as a single unit.\n *\n * The AsyncBatcher provides a flexible way to implement async batching with configurable:\n * - Maximum batch size (number of items per batch)\n * - Time-based batching (process after X milliseconds)\n * - Custom batch processing logic via getShouldExecute\n * - Event callbacks for monitoring batch operations\n * - Error handling for failed batch operations\n *\n * Error Handling:\n * - If an `onError` handler is provided, it will be called with the error, the batch of items that failed, and batcher instance\n * - If `throwOnError` is true (default when no onError handler is provided), the error will be thrown\n * - If `throwOnError` is false (default when onError handler is provided), the error will be swallowed\n * - Both onError and throwOnError can be used together - the handler will be called before any error is thrown\n * - The error state can be checked using the AsyncBatcher instance\n *\n * State Management:\n * - Uses TanStack Store for reactive state management\n * - Use `initialState` to provide initial state values when creating the async batcher\n * - Use `onSuccess` callback to react to successful batch execution and implement custom logic\n * - Use `onError` callback to react to batch execution errors and implement custom error handling\n * - Use `onSettled` callback to react to batch execution completion (success or error) and implement custom logic\n * - Use `onExecute` callback to react to batch execution and implement custom logic\n * - Use `onItemsChange` callback to react to items being added or removed from the batcher\n * - The state includes total items processed, success/error counts, and execution status\n * - State can be accessed via `asyncBatcher.store.state` when using the class directly\n * - When using framework adapters (React/Solid), state is accessed from `asyncBatcher.state`\n *\n * @example\n * ```ts\n * const batcher = new AsyncBatcher<number>(\n * async (items) => {\n * const result = await processItems(items);\n * console.log('Processing batch:', items);\n * return result;\n * },\n * {\n * maxSize: 5,\n * wait: 2000,\n * onSuccess: (result) => console.log('Batch succeeded:', result),\n * onError: (error) => console.error('Batch failed:', error)\n * }\n * );\n *\n * batcher.addItem(1);\n * batcher.addItem(2);\n * // After 2 seconds or when 5 items are added, whichever comes first,\n * // the batch will be processed and the result will be available\n * // batcher.execute() // manually trigger a batch\n * ```\n */\nexport class AsyncBatcher<TValue> {\n readonly store: Store<Readonly<AsyncBatcherState<TValue>>> = new Store(\n getDefaultAsyncBatcherState<TValue>(),\n )\n key: string\n options: AsyncBatcherOptionsWithOptionalCallbacks<TValue>\n #timeoutId: NodeJS.Timeout | null = null\n\n constructor(\n public fn: (items: Array<TValue>) => Promise<any>,\n initialOptions: AsyncBatcherOptions<TValue>,\n ) {\n this.key = createKey(initialOptions.key)\n this.options = {\n ...defaultOptions,\n ...initialOptions,\n throwOnError: initialOptions.throwOnError ?? !initialOptions.onError,\n }\n this.#setState(this.options.initialState ?? {})\n\n pacerEventClient.on('d-AsyncBatcher', (event) => {\n if (event.payload.key !== this.key) return\n this.#setState(event.payload.store.state)\n this.setOptions(event.payload.options)\n })\n }\n\n /**\n * Updates the async batcher options\n */\n setOptions = (newOptions: Partial<AsyncBatcherOptions<TValue>>): void => {\n this.options = { ...this.options, ...newOptions }\n }\n\n #setState = (newState: Partial<AsyncBatcherState<TValue>>): void => {\n this.store.setState((state) => {\n const combinedState = {\n ...state,\n ...newState,\n }\n const { isExecuting, isPending, items } = combinedState\n const size = items.length\n const isEmpty = size === 0\n return {\n ...combinedState,\n isEmpty,\n size,\n status: isExecuting\n ? 'executing'\n : isPending\n ? 'pending'\n : isEmpty\n ? 'idle'\n : 'populated',\n }\n })\n emitChange('AsyncBatcher', this)\n }\n\n #getWait = (): number => {\n return parseFunctionOrValue(this.options.wait, this)\n }\n\n /**\n * Adds an item to the async batcher\n * If the batch size is reached, timeout occurs, or shouldProcess returns true, the batch will be processed\n */\n addItem = (item: TValue): void => {\n this.#setState({\n items: [...this.store.state.items, item],\n isPending: this.options.wait !== Infinity,\n })\n this.options.onItemsChange?.(this)\n\n const shouldProcess =\n this.store.state.items.length >= this.options.maxSize ||\n this.options.getShouldExecute(this.store.state.items, this)\n\n if (shouldProcess) {\n this.#execute()\n } else if (this.options.wait !== Infinity) {\n this.#clearTimeout() // clear any pending timeout to replace it with a new one\n this.#timeoutId = setTimeout(() => this.#execute(), this.#getWait())\n }\n }\n\n /**\n * Processes the current batch of items asynchronously.\n * This method will automatically be triggered if the batcher is running and any of these conditions are met:\n * - The number of items reaches maxSize\n * - The wait duration has elapsed\n * - The getShouldExecute function returns true upon adding an item\n *\n * You can also call this method manually to process the current batch at any time.\n *\n * @returns A promise that resolves with the result of the batch function, or undefined if an error occurred and was handled by onError\n * @throws The error from the batch function if no onError handler is configured or throwOnError is true\n */\n #execute = async (): Promise<any> => {\n if (this.store.state.items.length === 0) {\n return undefined\n }\n\n const batch = this.peekAllItems() // copy of the items to be processed (to prevent race conditions)\n this.clear() // Clear items before processing to prevent race conditions\n this.options.onItemsChange?.(this)\n\n this.#setState({ isExecuting: true })\n\n try {\n const result = await this.fn(batch) // EXECUTE\n this.#setState({\n totalItemsProcessed:\n this.store.state.totalItemsProcessed + batch.length,\n lastResult: result,\n successCount: this.store.state.successCount + 1,\n })\n this.options.onSuccess?.(result, batch, this)\n return result\n } catch (error) {\n this.#setState({\n errorCount: this.store.state.errorCount + 1,\n failedItems: [...this.store.state.failedItems, ...batch],\n totalItemsFailed: this.store.state.totalItemsFailed + batch.length,\n })\n this.options.onError?.(error, batch, this)\n if (this.options.throwOnError) {\n throw error\n }\n return undefined\n } finally {\n this.#setState({\n isExecuting: false,\n settleCount: this.store.state.settleCount + 1,\n })\n this.options.onSettled?.(batch, this)\n }\n }\n\n /**\n * Processes the current batch of items immediately\n */\n flush = async (): Promise<any> => {\n this.#clearTimeout() // clear any pending timeout\n return await this.#execute()\n }\n\n /**\n * Returns a copy of all items in the async batcher\n */\n peekAllItems = (): Array<TValue> => {\n return [...this.store.state.items]\n }\n\n peekFailedItems = (): Array<TValue> => {\n return [...this.store.state.failedItems]\n }\n\n #clearTimeout = (): void => {\n if (this.#timeoutId) {\n clearTimeout(this.#timeoutId)\n this.#timeoutId = null\n }\n }\n\n /**\n * Removes all items from the async batcher\n */\n clear = (): void => {\n this.#setState({ items: [], failedItems: [], isPending: false })\n }\n\n /**\n * Resets the async batcher state to its default values\n */\n reset = (): void => {\n this.#setState(getDefaultAsyncBatcherState<TValue>())\n this.options.onItemsChange?.(this)\n }\n}\n\n/**\n * Creates an async batcher that processes items in batches\n *\n * Unlike the sync batcher, this async version:\n * - Handles promises and returns results from batch executions\n * - Provides error handling with configurable error behavior\n * - Tracks success, error, and settle counts separately\n * - Has state tracking for when batches are executing\n *\n * Error Handling:\n * - If an `onError` handler is provided, it will be called with the error, the batch of items that failed, and batcher instance\n * - If `throwOnError` is true (default when no onError handler is provided), the error will be thrown\n * - If `throwOnError` is false (default when onError handler is provided), the error will be swallowed\n * - Both onError and throwOnError can be used together - the handler will be called before any error is thrown\n * - The error state can be checked using the underlying AsyncBatcher instance\n *\n * State Management:\n * - Uses TanStack Store for reactive state management\n * - Use `initialState` to provide initial state values when creating the async batcher\n * - Use `onSuccess` callback to react to successful batch execution and implement custom logic\n * - Use `onError` callback to react to batch execution errors and implement custom error handling\n * - Use `onSettled` callback to react to batch execution completion (success or error) and implement custom logic\n * - Use `onExecute` callback to react to batch execution and implement custom logic\n * - Use `onItemsChange` callback to react to items being added or removed from the batcher\n * - The state includes total items processed, success/error counts, and execution status\n * - State can be accessed via the underlying AsyncBatcher instance's `store.state` property\n * - When using framework adapters (React/Solid), state is accessed from the hook's state property\n *\n * @example\n * ```ts\n * const batchItems = asyncBatch<number>(\n * async (items) => {\n * const result = await processApiCall(items);\n * console.log('Processing:', items);\n * return result;\n * },\n * {\n * maxSize: 3,\n * wait: 1000,\n * onSuccess: (result) => console.log('Batch succeeded:', result),\n * onError: (error) => console.error('Batch failed:', error)\n * }\n * );\n *\n * batchItems(1);\n * batchItems(2);\n * batchItems(3); // Triggers batch processing\n * ```\n */\nexport function asyncBatch<TValue>(\n fn: (items: Array<TValue>) => Promise<any>,\n options: AsyncBatcherOptions<TValue>,\n) {\n const batcher = new AsyncBatcher<TValue>(fn, options)\n return batcher.addItem\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"async-batcher.js","sources":["../../src/async-batcher.ts"],"sourcesContent":["import { Store } from '@tanstack/store'\nimport { AsyncRetryer } from './async-retryer'\nimport { createKey, parseFunctionOrValue } from './utils'\nimport { emitChange, pacerEventClient } from './event-client'\nimport type { AsyncRetryerOptions } from './async-retryer'\nimport type { OptionalKeys } from './types'\n\nexport interface AsyncBatcherState<TValue> {\n /**\n * Number of batch executions that have resulted in errors\n */\n errorCount: number\n /**\n * Number of batch executions that have been executed\n */\n executeCount: number\n /**\n * Array of items that failed during batch processing\n */\n failedItems: Array<TValue>\n /**\n * Whether the batcher has no items to process (items array is empty)\n */\n isEmpty: boolean\n /**\n * Whether a batch is currently being processed asynchronously\n */\n isExecuting: boolean\n /**\n * Whether the batcher is waiting for the timeout to trigger batch processing\n */\n isPending: boolean\n /**\n * Array of items currently queued for batch processing\n */\n items: Array<TValue>\n /**\n * The result from the most recent batch execution\n */\n lastResult: any\n /**\n * Number of batch executions that have completed (either successfully or with errors)\n */\n settleCount: number\n /**\n * Number of items currently in the batch queue\n */\n size: number\n /**\n * Current processing status - 'idle' when not processing, 'pending' when waiting for timeout, 'executing' when processing, 'populated' when items are present, but no wait is configured\n */\n status: 'idle' | 'pending' | 'executing' | 'populated'\n /**\n * Number of batch executions that have completed successfully\n */\n successCount: number\n /**\n * Total number of items that have failed processing across all batches\n */\n totalItemsFailed: number\n /**\n * Total number of items that have been processed across all batches\n */\n totalItemsProcessed: number\n}\n\nfunction getDefaultAsyncBatcherState<TValue>(): AsyncBatcherState<TValue> {\n return {\n errorCount: 0,\n executeCount: 0,\n failedItems: [],\n isEmpty: true,\n isExecuting: false,\n isPending: false,\n items: [],\n lastResult: undefined,\n settleCount: 0,\n size: 0,\n status: 'idle',\n successCount: 0,\n totalItemsProcessed: 0,\n totalItemsFailed: 0,\n }\n}\n\n/**\n * Options for configuring an AsyncBatcher instance\n */\nexport interface AsyncBatcherOptions<TValue> {\n /**\n * Options for configuring the underlying async retryer\n */\n asyncRetryerOptions?: AsyncRetryerOptions<\n (items: Array<TValue>) => Promise<any>\n >\n /**\n * Custom function to determine if a batch should be processed\n * Return true to process the batch immediately\n */\n getShouldExecute?: (\n items: Array<TValue>,\n batcher: AsyncBatcher<TValue>,\n ) => boolean\n /**\n * Initial state for the async batcher\n */\n initialState?: Partial<AsyncBatcherState<TValue>>\n /**\n * Optional key to identify this async batcher instance.\n * If provided, the async batcher will be identified by this key in the devtools and PacerProvider if applicable.\n */\n key?: string\n /**\n * Maximum number of items in a batch\n * @default Infinity\n */\n maxSize?: number\n /**\n * Optional error handler for when the batch function throws.\n * If provided, the handler will be called with the error, the batch of items that failed, and batcher instance.\n * This can be used alongside throwOnError - the handler will be called before any error is thrown.\n */\n onError?: (\n error: Error,\n batch: Array<TValue>,\n batcher: AsyncBatcher<TValue>,\n ) => void\n /**\n * Callback fired after items are added to the batcher\n */\n onItemsChange?: (batcher: AsyncBatcher<TValue>) => void\n /**\n * Optional callback to call when a batch is settled (completed or failed)\n */\n onSettled?: (batch: Array<TValue>, batcher: AsyncBatcher<TValue>) => void\n /**\n * Optional callback to call when a batch succeeds\n */\n onSuccess?: (\n result: any,\n batch: Array<TValue>,\n batcher: AsyncBatcher<TValue>,\n ) => void\n /**\n * Whether the batcher should start processing immediately\n * @default true\n */\n started?: boolean\n /**\n * Whether to throw errors when they occur.\n * Defaults to true if no onError handler is provided, false if an onError handler is provided.\n * Can be explicitly set to override these defaults.\n */\n throwOnError?: boolean\n /**\n * Maximum time in milliseconds to wait before processing a batch.\n * If the wait duration has elapsed, the batch will be processed.\n * If not provided, the batch will not be triggered by a timeout.\n * @default Infinity\n */\n wait?: number | ((asyncBatcher: AsyncBatcher<TValue>) => number)\n}\n\n/**\n * Utility function for sharing common `AsyncBatcherOptions` options between different `AsyncBatcher` instances.\n *\n */\nexport function asyncBatcherOptions<\n TValue = any,\n TOptions extends Partial<AsyncBatcherOptions<TValue>> = Partial<\n AsyncBatcherOptions<TValue>\n >,\n>(options: TOptions): TOptions {\n return options\n}\n\ntype AsyncBatcherOptionsWithOptionalCallbacks<TValue> = OptionalKeys<\n Required<AsyncBatcherOptions<TValue>>,\n | 'initialState'\n | 'onError'\n | 'onItemsChange'\n | 'onSettled'\n | 'onSuccess'\n | 'key'\n>\n\nconst defaultOptions: AsyncBatcherOptionsWithOptionalCallbacks<any> = {\n asyncRetryerOptions: {\n maxAttempts: 1,\n },\n getShouldExecute: () => false,\n maxSize: Infinity,\n started: true,\n throwOnError: true,\n wait: Infinity,\n}\n\n/**\n * A class that collects items and processes them in batches asynchronously.\n *\n * Async vs Sync Versions:\n * The async version provides advanced features over the sync Batcher:\n * - Returns promises that can be awaited for batch results\n * - Built-in retry support via AsyncRetryer integration\n * - Abort support to cancel in-flight batch executions\n * - Cancel support to prevent pending batches from starting\n * - Comprehensive error handling with onError callbacks and throwOnError control\n * - Detailed execution tracking (success/error/settle counts)\n *\n * The sync Batcher is lighter weight and simpler when you don't need async features,\n * return values, or execution control.\n *\n * What is Batching?\n * Batching is a technique for grouping multiple operations together to be processed as a single unit.\n *\n * The AsyncBatcher provides a flexible way to implement async batching with configurable:\n * - Maximum batch size (number of items per batch)\n * - Time-based batching (process after X milliseconds)\n * - Custom batch processing logic via getShouldExecute\n * - Event callbacks for monitoring batch operations\n * - Error handling for failed batch operations\n *\n * Error Handling:\n * - If an `onError` handler is provided, it will be called with the error, the batch of items that failed, and batcher instance\n * - If `throwOnError` is true (default when no onError handler is provided), the error will be thrown\n * - If `throwOnError` is false (default when onError handler is provided), the error will be swallowed\n * - Both onError and throwOnError can be used together - the handler will be called before any error is thrown\n * - The error state can be checked using the AsyncBatcher instance\n *\n * State Management:\n * - Uses TanStack Store for reactive state management\n * - Use `initialState` to provide initial state values when creating the async batcher\n * - Use `onSuccess` callback to react to successful batch execution and implement custom logic\n * - Use `onError` callback to react to batch execution errors and implement custom error handling\n * - Use `onSettled` callback to react to batch execution completion (success or error) and implement custom logic\n * - Use `onExecute` callback to react to batch execution and implement custom logic\n * - Use `onItemsChange` callback to react to items being added or removed from the batcher\n * - The state includes total items processed, success/error counts, and execution status\n * - State can be accessed via `asyncBatcher.store.state` when using the class directly\n * - When using framework adapters (React/Solid), state is accessed from `asyncBatcher.state`\n *\n * @example\n * ```ts\n * const batcher = new AsyncBatcher<number>(\n * async (items) => {\n * const result = await processItems(items);\n * console.log('Processing batch:', items);\n * return result;\n * },\n * {\n * maxSize: 5,\n * wait: 2000,\n * onSuccess: (result) => console.log('Batch succeeded:', result),\n * onError: (error) => console.error('Batch failed:', error)\n * }\n * );\n *\n * batcher.addItem(1);\n * batcher.addItem(2);\n * // After 2 seconds or when 5 items are added, whichever comes first,\n * // the batch will be processed and the result will be available\n * // batcher.execute() // manually trigger a batch\n * ```\n */\nexport class AsyncBatcher<TValue> {\n readonly store: Store<Readonly<AsyncBatcherState<TValue>>> = new Store(\n getDefaultAsyncBatcherState<TValue>(),\n )\n key: string\n options: AsyncBatcherOptionsWithOptionalCallbacks<TValue>\n asyncRetryers = new Map<\n number,\n AsyncRetryer<(items: Array<TValue>) => Promise<any>>\n >()\n #timeoutId: NodeJS.Timeout | null = null\n\n constructor(\n public fn: (items: Array<TValue>) => Promise<any>,\n initialOptions: AsyncBatcherOptions<TValue>,\n ) {\n this.key = createKey(initialOptions.key)\n this.options = {\n ...defaultOptions,\n ...initialOptions,\n throwOnError: initialOptions.throwOnError ?? !initialOptions.onError,\n }\n this.#setState(this.options.initialState ?? {})\n\n pacerEventClient.on('d-AsyncBatcher', (event) => {\n if (event.payload.key !== this.key) return\n this.#setState(event.payload.store.state)\n this.setOptions(event.payload.options)\n })\n }\n\n /**\n * Emits a change event for the async batcher instance. Mostly useful for devtools.\n */\n _emit = () => emitChange('AsyncBatcher', this)\n\n /**\n * Updates the async batcher options\n */\n setOptions = (newOptions: Partial<AsyncBatcherOptions<TValue>>): void => {\n this.options = { ...this.options, ...newOptions }\n }\n\n #setState = (newState: Partial<AsyncBatcherState<TValue>>): void => {\n this.store.setState((state) => {\n const combinedState = {\n ...state,\n ...newState,\n }\n const { isExecuting, isPending, items } = combinedState\n const size = items.length\n const isEmpty = size === 0\n return {\n ...combinedState,\n isEmpty,\n size,\n status: isExecuting\n ? 'executing'\n : isPending\n ? 'pending'\n : isEmpty\n ? 'idle'\n : 'populated',\n }\n })\n emitChange('AsyncBatcher', this)\n }\n\n #getWait = (): number => {\n return parseFunctionOrValue(this.options.wait, this)\n }\n\n /**\n * Adds an item to the async batcher\n * If the batch size is reached, timeout occurs, or shouldProcess returns true, the batch will be processed\n *\n * @returns The result from the batch function, or undefined if an error occurred and was handled by onError\n *\n * @throws The error from the batch function if no onError handler is configured or throwOnError is true\n */\n addItem = async (item: TValue): Promise<any> => {\n this.#setState({\n items: [...this.store.state.items, item],\n isPending: this.options.wait !== Infinity,\n })\n this.options.onItemsChange?.(this)\n\n const shouldProcess =\n this.store.state.items.length >= this.options.maxSize ||\n this.options.getShouldExecute(this.store.state.items, this)\n\n if (shouldProcess) {\n return await this.#execute()\n } else if (this.options.wait !== Infinity) {\n this.#clearTimeout() // clear any pending timeout to replace it with a new one\n this.#timeoutId = setTimeout(() => this.#execute(), this.#getWait())\n await new Promise((resolve) => setTimeout(resolve, this.#getWait()))\n }\n }\n\n /**\n * Processes the current batch of items asynchronously.\n * This method will automatically be triggered if the batcher is running and any of these conditions are met:\n * - The number of items reaches maxSize\n * - The wait duration has elapsed\n * - The getShouldExecute function returns true upon adding an item\n *\n * You can also call this method manually to process the current batch at any time.\n *\n * @returns A promise that resolves with the result of the batch function, or undefined if an error occurred and was handled by onError\n * @throws The error from the batch function if no onError handler is configured or throwOnError is true\n */\n #execute = async (): Promise<any> => {\n if (this.store.state.items.length === 0) {\n return undefined\n }\n\n const currentExecuteCount = this.store.state.executeCount + 1\n const batch = this.peekAllItems() // copy of the items to be processed (to prevent race conditions)\n this.clear() // Clear items before processing to prevent race conditions\n this.options.onItemsChange?.(this)\n\n this.#setState({ isExecuting: true, executeCount: currentExecuteCount })\n\n try {\n const currentAsyncRetryer = new AsyncRetryer(\n this.fn,\n this.options.asyncRetryerOptions,\n )\n this.asyncRetryers.set(currentExecuteCount, currentAsyncRetryer)\n const result = await currentAsyncRetryer.execute(batch) // EXECUTE\n this.#setState({\n totalItemsProcessed:\n this.store.state.totalItemsProcessed + batch.length,\n lastResult: result,\n successCount: this.store.state.successCount + 1,\n })\n this.options.onSuccess?.(result, batch, this)\n return result\n } catch (error) {\n this.#setState({\n errorCount: this.store.state.errorCount + 1,\n failedItems: [...this.store.state.failedItems, ...batch],\n totalItemsFailed: this.store.state.totalItemsFailed + batch.length,\n })\n this.options.onError?.(error as Error, batch, this)\n if (this.options.throwOnError) {\n throw error\n }\n return undefined\n } finally {\n this.asyncRetryers.delete(currentExecuteCount) // dispose retryer\n this.#setState({\n isExecuting: false,\n settleCount: this.store.state.settleCount + 1,\n })\n this.options.onSettled?.(batch, this)\n }\n }\n\n /**\n * Processes the current batch of items immediately\n */\n flush = async (): Promise<any> => {\n this.#clearTimeout() // clear any pending timeout\n return await this.#execute()\n }\n\n /**\n * Returns a copy of all items in the async batcher\n */\n peekAllItems = (): Array<TValue> => {\n return [...this.store.state.items]\n }\n\n peekFailedItems = (): Array<TValue> => {\n return [...this.store.state.failedItems]\n }\n\n #clearTimeout = (): void => {\n if (this.#timeoutId) {\n clearTimeout(this.#timeoutId)\n this.#timeoutId = null\n }\n }\n\n /**\n * Removes all items from the async batcher\n */\n clear = (): void => {\n this.#setState({ items: [], failedItems: [], isPending: false })\n }\n\n /**\n * Returns the AbortSignal for a specific execution.\n * If no executeCount is provided, returns the signal for the most recent execution.\n * Returns null if no execution is found or not currently executing.\n *\n * @param executeCount - Optional specific execution to get signal for\n * @example\n * ```typescript\n * const batcher = new AsyncBatcher(\n * async (items: string[]) => {\n * const signal = batcher.getAbortSignal()\n * if (signal) {\n * const response = await fetch('/api/batch', {\n * method: 'POST',\n * body: JSON.stringify(items),\n * signal\n * })\n * return response.json()\n * }\n * },\n * { maxSize: 10, wait: 100 }\n * )\n * ```\n */\n getAbortSignal(executeCount?: number): AbortSignal | null {\n const count = executeCount ?? this.store.state.executeCount\n const retryer = this.asyncRetryers.get(count)\n return retryer?.getAbortSignal() ?? null\n }\n\n /**\n * Aborts all ongoing executions with the internal abort controllers.\n * Does NOT cancel any pending execution that have not started yet.\n * Does NOT clear out the items.\n */\n abort = (): void => {\n this.asyncRetryers.forEach((retryer) => retryer.abort())\n this.asyncRetryers.clear()\n this.#setState({\n isExecuting: false,\n })\n }\n\n /**\n * Cancels any pending execution that have not started yet.\n * Does NOT abort any execution already in progress.\n * Does NOT clear out the items.\n */\n cancel = (): void => {\n this.#clearTimeout()\n this.#setState({\n isPending: false,\n })\n }\n\n /**\n * Resets the async batcher state to its default values\n */\n reset = (): void => {\n this.#setState(getDefaultAsyncBatcherState<TValue>())\n this.options.onItemsChange?.(this)\n this.asyncRetryers.forEach((retryer) => retryer.reset())\n }\n}\n\n/**\n * Creates an async batcher that processes items in batches.\n *\n * Async vs Sync Versions:\n * The async version provides advanced features over the sync batch function:\n * - Returns promises that can be awaited for batch results\n * - Built-in retry support via AsyncRetryer integration\n * - Abort support to cancel in-flight batch executions\n * - Cancel support to prevent pending batches from starting\n * - Comprehensive error handling with onError callbacks and throwOnError control\n * - Detailed execution tracking (success/error/settle counts)\n *\n * The sync batch function is lighter weight and simpler when you don't need async features,\n * return values, or execution control.\n *\n * What is Batching?\n * Batching is a technique for grouping multiple operations together to be processed as a single unit.\n *\n * Configuration Options:\n * - `maxSize`: Maximum number of items per batch (default: Infinity)\n * - `wait`: Time to wait before processing batch (default: Infinity)\n * - `getShouldExecute`: Custom logic to trigger batch processing\n * - `asyncRetryerOptions`: Configure retry behavior for batch executions\n * - `started`: Whether to start processing immediately (default: true)\n *\n * Error Handling:\n * - If an `onError` handler is provided, it will be called with the error, the batch of items that failed, and batcher instance\n * - If `throwOnError` is true (default when no onError handler is provided), the error will be thrown\n * - If `throwOnError` is false (default when onError handler is provided), the error will be swallowed\n * - Both onError and throwOnError can be used together - the handler will be called before any error is thrown\n * - The error state can be checked using the underlying AsyncBatcher instance\n *\n * State Management:\n * - Uses TanStack Store for reactive state management\n * - Use `initialState` to provide initial state values when creating the async batcher\n * - Use `onSuccess` callback to react to successful batch execution and implement custom logic\n * - Use `onError` callback to react to batch execution errors and implement custom error handling\n * - Use `onSettled` callback to react to batch execution completion (success or error) and implement custom logic\n * - Use `onItemsChange` callback to react to items being added or removed from the batcher\n * - The state includes total items processed, success/error counts, and execution status\n * - State can be accessed via the underlying AsyncBatcher instance's `store.state` property\n * - When using framework adapters (React/Solid), state is accessed from the hook's state property\n *\n * @example\n * ```ts\n * const batchItems = asyncBatch<number>(\n * async (items) => {\n * const result = await processApiCall(items);\n * console.log('Processing:', items);\n * return result;\n * },\n * {\n * maxSize: 3,\n * wait: 1000,\n * onSuccess: (result) => console.log('Batch succeeded:', result),\n * onError: (error) => console.error('Batch failed:', error)\n * }\n * );\n *\n * batchItems(1);\n * batchItems(2);\n * batchItems(3); // Triggers batch processing\n * ```\n */\nexport function asyncBatch<TValue>(\n fn: (items: Array<TValue>) => Promise<any>,\n options: AsyncBatcherOptions<TValue>,\n) {\n const batcher = new AsyncBatcher<TValue>(fn, options)\n return batcher.addItem\n}\n"],"names":[],"mappings":"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"}