@tanstack/pacer 0.16.2 → 0.16.4

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 (180) hide show
  1. package/README.md +165 -0
  2. package/dist/async-batcher.cjs +355 -0
  3. package/dist/async-batcher.cjs.map +1 -0
  4. package/dist/async-batcher.d.cts +344 -0
  5. package/dist/async-batcher.d.ts +344 -0
  6. package/dist/async-batcher.js +353 -0
  7. package/dist/async-batcher.js.map +1 -0
  8. package/dist/async-debouncer.cjs +340 -0
  9. package/dist/async-debouncer.cjs.map +1 -0
  10. package/dist/async-debouncer.d.cts +300 -0
  11. package/dist/async-debouncer.d.ts +300 -0
  12. package/dist/async-debouncer.js +338 -0
  13. package/dist/async-debouncer.js.map +1 -0
  14. package/dist/async-queuer.cjs +496 -0
  15. package/dist/async-queuer.cjs.map +1 -0
  16. package/dist/async-queuer.d.cts +440 -0
  17. package/dist/async-queuer.d.ts +440 -0
  18. package/dist/async-queuer.js +494 -0
  19. package/dist/async-queuer.js.map +1 -0
  20. package/dist/async-rate-limiter.cjs +381 -0
  21. package/dist/async-rate-limiter.cjs.map +1 -0
  22. package/dist/{cjs/async-rate-limiter.d.cts → async-rate-limiter.d.cts} +190 -185
  23. package/dist/{esm/async-rate-limiter.d.ts → async-rate-limiter.d.ts} +190 -185
  24. package/dist/async-rate-limiter.js +379 -0
  25. package/dist/async-rate-limiter.js.map +1 -0
  26. package/dist/async-retryer.cjs +378 -0
  27. package/dist/async-retryer.cjs.map +1 -0
  28. package/dist/async-retryer.d.cts +312 -0
  29. package/dist/async-retryer.d.ts +312 -0
  30. package/dist/async-retryer.js +376 -0
  31. package/dist/async-retryer.js.map +1 -0
  32. package/dist/async-throttler.cjs +362 -0
  33. package/dist/async-throttler.cjs.map +1 -0
  34. package/dist/async-throttler.d.cts +320 -0
  35. package/dist/async-throttler.d.ts +320 -0
  36. package/dist/async-throttler.js +360 -0
  37. package/dist/async-throttler.js.map +1 -0
  38. package/dist/batcher.cjs +194 -0
  39. package/dist/batcher.cjs.map +1 -0
  40. package/dist/batcher.d.cts +180 -0
  41. package/dist/batcher.d.ts +180 -0
  42. package/dist/batcher.js +193 -0
  43. package/dist/batcher.js.map +1 -0
  44. package/dist/debouncer.cjs +197 -0
  45. package/dist/debouncer.cjs.map +1 -0
  46. package/dist/debouncer.d.cts +167 -0
  47. package/dist/debouncer.d.ts +167 -0
  48. package/dist/debouncer.js +195 -0
  49. package/dist/debouncer.js.map +1 -0
  50. package/dist/event-client.cjs +20 -0
  51. package/dist/event-client.cjs.map +1 -0
  52. package/dist/event-client.d.cts +49 -0
  53. package/dist/event-client.d.ts +49 -0
  54. package/dist/event-client.js +19 -0
  55. package/dist/event-client.js.map +1 -0
  56. package/dist/index.cjs +49 -0
  57. package/dist/index.d.cts +15 -0
  58. package/dist/index.d.ts +15 -0
  59. package/dist/{esm/index.js → index.js} +5 -41
  60. package/dist/queuer.cjs +393 -0
  61. package/dist/queuer.cjs.map +1 -0
  62. package/dist/queuer.d.cts +345 -0
  63. package/dist/queuer.d.ts +345 -0
  64. package/dist/queuer.js +391 -0
  65. package/dist/queuer.js.map +1 -0
  66. package/dist/rate-limiter.cjs +251 -0
  67. package/dist/rate-limiter.cjs.map +1 -0
  68. package/dist/{cjs/rate-limiter.d.cts → rate-limiter.d.cts} +113 -108
  69. package/dist/{esm/rate-limiter.d.ts → rate-limiter.d.ts} +113 -108
  70. package/dist/rate-limiter.js +249 -0
  71. package/dist/rate-limiter.js.map +1 -0
  72. package/dist/throttler.cjs +209 -0
  73. package/dist/throttler.cjs.map +1 -0
  74. package/dist/throttler.d.cts +207 -0
  75. package/dist/throttler.d.ts +207 -0
  76. package/dist/throttler.js +207 -0
  77. package/dist/throttler.js.map +1 -0
  78. package/dist/types.cjs +0 -0
  79. package/dist/types.d.cts +13 -0
  80. package/dist/types.d.ts +13 -0
  81. package/dist/types.js +1 -0
  82. package/dist/utils.cjs +13 -0
  83. package/dist/utils.cjs.map +1 -0
  84. package/dist/utils.d.cts +8 -0
  85. package/dist/utils.d.ts +8 -0
  86. package/dist/utils.js +11 -0
  87. package/dist/utils.js.map +1 -0
  88. package/package.json +38 -122
  89. package/src/async-batcher.ts +7 -5
  90. package/src/async-debouncer.ts +7 -5
  91. package/src/async-queuer.ts +7 -5
  92. package/src/async-rate-limiter.ts +7 -5
  93. package/src/async-retryer.ts +7 -5
  94. package/src/async-throttler.ts +7 -5
  95. package/src/batcher.ts +7 -5
  96. package/src/debouncer.ts +7 -5
  97. package/src/queuer.ts +7 -5
  98. package/src/rate-limiter.ts +7 -5
  99. package/src/throttler.ts +7 -5
  100. package/dist/cjs/async-batcher.cjs +0 -220
  101. package/dist/cjs/async-batcher.cjs.map +0 -1
  102. package/dist/cjs/async-batcher.d.cts +0 -340
  103. package/dist/cjs/async-debouncer.cjs +0 -231
  104. package/dist/cjs/async-debouncer.cjs.map +0 -1
  105. package/dist/cjs/async-debouncer.d.cts +0 -295
  106. package/dist/cjs/async-queuer.cjs +0 -397
  107. package/dist/cjs/async-queuer.cjs.map +0 -1
  108. package/dist/cjs/async-queuer.d.cts +0 -435
  109. package/dist/cjs/async-rate-limiter.cjs +0 -252
  110. package/dist/cjs/async-rate-limiter.cjs.map +0 -1
  111. package/dist/cjs/async-retryer.cjs +0 -285
  112. package/dist/cjs/async-retryer.cjs.map +0 -1
  113. package/dist/cjs/async-retryer.d.cts +0 -308
  114. package/dist/cjs/async-throttler.cjs +0 -261
  115. package/dist/cjs/async-throttler.cjs.map +0 -1
  116. package/dist/cjs/async-throttler.d.cts +0 -315
  117. package/dist/cjs/batcher.cjs +0 -130
  118. package/dist/cjs/batcher.cjs.map +0 -1
  119. package/dist/cjs/batcher.d.cts +0 -176
  120. package/dist/cjs/debouncer.cjs +0 -137
  121. package/dist/cjs/debouncer.cjs.map +0 -1
  122. package/dist/cjs/debouncer.d.cts +0 -162
  123. package/dist/cjs/event-client.cjs +0 -20
  124. package/dist/cjs/event-client.cjs.map +0 -1
  125. package/dist/cjs/event-client.d.cts +0 -45
  126. package/dist/cjs/index.cjs +0 -51
  127. package/dist/cjs/index.cjs.map +0 -1
  128. package/dist/cjs/index.d.cts +0 -15
  129. package/dist/cjs/queuer.cjs +0 -306
  130. package/dist/cjs/queuer.cjs.map +0 -1
  131. package/dist/cjs/queuer.d.cts +0 -340
  132. package/dist/cjs/rate-limiter.cjs +0 -179
  133. package/dist/cjs/rate-limiter.cjs.map +0 -1
  134. package/dist/cjs/throttler.cjs +0 -151
  135. package/dist/cjs/throttler.cjs.map +0 -1
  136. package/dist/cjs/throttler.d.cts +0 -202
  137. package/dist/cjs/types.d.cts +0 -9
  138. package/dist/cjs/utils.cjs +0 -11
  139. package/dist/cjs/utils.cjs.map +0 -1
  140. package/dist/cjs/utils.d.cts +0 -3
  141. package/dist/esm/async-batcher.d.ts +0 -340
  142. package/dist/esm/async-batcher.js +0 -220
  143. package/dist/esm/async-batcher.js.map +0 -1
  144. package/dist/esm/async-debouncer.d.ts +0 -295
  145. package/dist/esm/async-debouncer.js +0 -231
  146. package/dist/esm/async-debouncer.js.map +0 -1
  147. package/dist/esm/async-queuer.d.ts +0 -435
  148. package/dist/esm/async-queuer.js +0 -397
  149. package/dist/esm/async-queuer.js.map +0 -1
  150. package/dist/esm/async-rate-limiter.js +0 -252
  151. package/dist/esm/async-rate-limiter.js.map +0 -1
  152. package/dist/esm/async-retryer.d.ts +0 -308
  153. package/dist/esm/async-retryer.js +0 -285
  154. package/dist/esm/async-retryer.js.map +0 -1
  155. package/dist/esm/async-throttler.d.ts +0 -315
  156. package/dist/esm/async-throttler.js +0 -261
  157. package/dist/esm/async-throttler.js.map +0 -1
  158. package/dist/esm/batcher.d.ts +0 -176
  159. package/dist/esm/batcher.js +0 -130
  160. package/dist/esm/batcher.js.map +0 -1
  161. package/dist/esm/debouncer.d.ts +0 -162
  162. package/dist/esm/debouncer.js +0 -137
  163. package/dist/esm/debouncer.js.map +0 -1
  164. package/dist/esm/event-client.d.ts +0 -45
  165. package/dist/esm/event-client.js +0 -20
  166. package/dist/esm/event-client.js.map +0 -1
  167. package/dist/esm/index.d.ts +0 -15
  168. package/dist/esm/index.js.map +0 -1
  169. package/dist/esm/queuer.d.ts +0 -340
  170. package/dist/esm/queuer.js +0 -306
  171. package/dist/esm/queuer.js.map +0 -1
  172. package/dist/esm/rate-limiter.js +0 -179
  173. package/dist/esm/rate-limiter.js.map +0 -1
  174. package/dist/esm/throttler.d.ts +0 -202
  175. package/dist/esm/throttler.js +0 -151
  176. package/dist/esm/throttler.js.map +0 -1
  177. package/dist/esm/types.d.ts +0 -9
  178. package/dist/esm/utils.d.ts +0 -3
  179. package/dist/esm/utils.js +0 -11
  180. package/dist/esm/utils.js.map +0 -1
@@ -1 +0,0 @@
1
- {"version":3,"file":"async-retryer.cjs","sources":["../../src/async-retryer.ts"],"sourcesContent":["import { Store } from '@tanstack/store'\nimport { parseFunctionOrValue } from './utils'\nimport { emitChange, pacerEventClient } from './event-client'\nimport type { AnyAsyncFunction } from './types'\n\nexport interface AsyncRetryerState<TFn extends AnyAsyncFunction> {\n /**\n * The current retry attempt number (0 when not executing)\n */\n currentAttempt: number\n /**\n * Total number of completed executions (successful or failed)\n */\n executionCount: number\n /**\n * Whether the retryer is currently executing the function\n */\n isExecuting: boolean\n /**\n * The most recent error encountered during execution\n */\n lastError: Error | undefined\n /**\n * Timestamp of the last execution completion in milliseconds\n */\n lastExecutionTime: number\n /**\n * The result from the most recent successful execution\n */\n lastResult: Awaited<ReturnType<TFn>> | undefined\n /**\n * Current execution status - 'disabled' when not enabled, 'idle' when ready, 'executing' when running\n */\n status: 'disabled' | 'idle' | 'executing' | 'retrying'\n /**\n * Total time spent executing (including retries) in milliseconds\n */\n totalExecutionTime: number\n}\n\n/**\n * Creates the default initial state for an AsyncRetryer instance\n * @returns The default state with all values reset to initial values\n */\nfunction getDefaultAsyncRetryerState<\n TFn extends AnyAsyncFunction,\n>(): AsyncRetryerState<TFn> {\n return structuredClone({\n currentAttempt: 0,\n executionCount: 0,\n isExecuting: false,\n lastError: undefined,\n lastExecutionTime: 0,\n lastResult: undefined,\n status: 'idle',\n totalExecutionTime: 0,\n })\n}\n\nexport interface AsyncRetryerOptions<TFn extends AnyAsyncFunction> {\n /**\n * The backoff strategy for retry delays:\n * - 'exponential': Wait time doubles with each attempt (1s, 2s, 4s, ...)\n * - 'linear': Wait time increases linearly (1s, 2s, 3s, ...)\n * - 'fixed': Same wait time for all attempts\n * @default 'exponential'\n */\n backoff?: 'linear' | 'exponential' | 'fixed'\n /**\n * Base wait time in milliseconds between retries, or a function that returns the wait time\n * @default 1000\n */\n baseWait?: number | ((retryer: AsyncRetryer<TFn>) => number)\n /**\n * Whether the retryer is enabled, or a function that determines if it's enabled\n * @default true\n */\n enabled?: boolean | ((retryer: AsyncRetryer<TFn>) => boolean)\n /**\n * Initial state to merge with the default state\n */\n initialState?: Partial<AsyncRetryerState<TFn>>\n /**\n * Jitter percentage to add to retry delays (0-1). Adds randomness to prevent thundering herd.\n * @default 0\n */\n jitter?: number\n /**\n * Optional key to identify this async retryer instance.\n * If provided, the async retryer will be identified by this key in the devtools and PacerProvider if applicable.\n */\n key?: string\n /**\n * Maximum number of retry attempts, or a function that returns the max attempts\n * @default 3\n */\n maxAttempts?: number | ((retryer: AsyncRetryer<TFn>) => number)\n /**\n * Maximum execution time in milliseconds for a single function call before aborting\n * @default Infinity\n */\n maxExecutionTime?: number\n /**\n * Maximum total execution time in milliseconds for the entire retry operation before aborting\n * @default Infinity\n */\n maxTotalExecutionTime?: number\n /**\n * Callback invoked when the execution is aborted (manually or due to timeouts)\n */\n onAbort?: (\n reason: 'manual' | 'execution-timeout' | 'total-timeout' | 'new-execution',\n retryer: AsyncRetryer<TFn>,\n ) => void\n /**\n * Callback invoked when any error occurs during execution (including retries)\n */\n onError?: (\n error: Error,\n args: Parameters<TFn>,\n retryer: AsyncRetryer<TFn>,\n ) => void\n /**\n * Callback invoked when the final error occurs after all retries are exhausted\n */\n onLastError?: (error: Error, retryer: AsyncRetryer<TFn>) => void\n /**\n * Callback invoked before each retry attempt\n */\n onRetry?: (attempt: number, error: Error, retryer: AsyncRetryer<TFn>) => void\n /**\n * Callback invoked after execution completes (success or failure) of each attempt\n */\n onSettled?: (args: Parameters<TFn>, retryer: AsyncRetryer<TFn>) => void\n /**\n * Callback invoked when execution succeeds\n */\n onSuccess?: (\n result: Awaited<ReturnType<TFn>>,\n args: Parameters<TFn>,\n retryer: AsyncRetryer<TFn>,\n ) => void\n /**\n * Callback invoked when a single execution attempt times out (maxExecutionTime exceeded)\n */\n onExecutionTimeout?: (retryer: AsyncRetryer<TFn>) => void\n /**\n * Callback invoked when the total execution time times out (maxTotalExecutionTime exceeded)\n */\n onTotalExecutionTimeout?: (retryer: AsyncRetryer<TFn>) => void\n /**\n * Controls when errors are thrown:\n * - 'last': Only throw the final error after all retries are exhausted\n * - true: Throw every error immediately (disables retrying)\n * - false: Never throw errors, return undefined instead\n * @default 'last'\n */\n throwOnError?: boolean | 'last'\n}\n\n/**\n * Utility function for sharing common `AsyncRetryerOptions` options between different `AsyncRetryer` instances.\n */\nexport function asyncRetryerOptions<\n TFn extends AnyAsyncFunction = AnyAsyncFunction,\n TOptions extends Partial<AsyncRetryerOptions<TFn>> = Partial<\n AsyncRetryerOptions<TFn>\n >,\n>(options: TOptions): TOptions {\n return options\n}\n\nconst defaultOptions: Omit<\n Required<AsyncRetryerOptions<any>>,\n | 'initialState'\n | 'key'\n | 'onAbort'\n | 'onError'\n | 'onLastError'\n | 'onRetry'\n | 'onSettled'\n | 'onSuccess'\n | 'onExecutionTimeout'\n | 'onTotalExecutionTimeout'\n> = {\n backoff: 'exponential',\n baseWait: 1000,\n enabled: true,\n jitter: 0,\n maxAttempts: 3,\n maxExecutionTime: Infinity,\n maxTotalExecutionTime: Infinity,\n throwOnError: 'last',\n}\n\n/**\n * Provides robust retry functionality for asynchronous functions, supporting configurable backoff strategies,\n * attempt limits, timeout controls, and detailed state management. The AsyncRetryer class is designed to help you reliably\n * execute async operations that may fail intermittently, such as network requests or database operations,\n * by automatically retrying them according to your chosen policy.\n *\n * ## Retrying Concepts\n *\n * - **Retrying**: Automatically re-executes a failed async function up to a specified number of attempts.\n * Useful for handling transient errors (e.g., network flakiness, rate limits, temporary server issues).\n * - **Backoff Strategies**: Controls the delay between retry attempts (default: `'exponential'`):\n * - `'exponential'`: Wait time doubles with each attempt (1s, 2s, 4s, ...) - **DEFAULT**\n * - `'linear'`: Wait time increases linearly (1s, 2s, 3s, ...)\n * - `'fixed'`: Waits a constant amount of time (`baseWait`) between each attempt\n * - **Jitter**: Adds randomness to retry delays to prevent thundering herd problems (default: `0`).\n * Set to a value between 0-1 to apply that percentage of random variation to each delay.\n * - **Timeout Controls**: Set limits on execution time to prevent hanging operations:\n * - `maxExecutionTime`: Maximum time for a single function call (default: `Infinity`)\n * - `maxTotalExecutionTime`: Maximum time for the entire retry operation (default: `Infinity`)\n * - **Abort & Cancellation**: Supports cancellation via an internal `AbortController`. Call `abort()` to stop retries.\n * Use `getAbortSignal()` to make your async function actually cancellable (e.g., with fetch requests).\n *\n * ## State Management\n *\n * Uses TanStack Store for fine-grained reactivity. State can be accessed via the `store.state` property.\n *\n * Available state properties:\n * - `currentAttempt`: The current retry attempt number (0 when not executing)\n * - `executionCount`: Total number of completed executions (successful or failed)\n * - `isExecuting`: Whether the retryer is currently executing the function\n * - `lastError`: The most recent error encountered during execution\n * - `lastExecutionTime`: Timestamp of the last execution completion in milliseconds\n * - `lastResult`: The result from the most recent successful execution\n * - `status`: Current execution status ('disabled' | 'idle' | 'executing' | 'retrying')\n * - `totalExecutionTime`: Total time spent executing (including retries) in milliseconds\n *\n * ## Error Handling\n *\n * The `throwOnError` option controls when errors are thrown (default: `'last'`):\n * - `'last'`: Only throws the final error after all retries are exhausted - **DEFAULT**\n * - `true`: Throws every error immediately (disables retrying)\n * - `false`: Never throws errors, returns `undefined` instead\n *\n * Callbacks for lifecycle management:\n * - `onAbort`: Called when execution is aborted (manually or due to timeouts)\n * - `onError`: Called for every error (including during retries)\n * - `onLastError`: Called only for the final error after all retries fail\n * - `onRetry`: Called before each retry attempt\n * - `onSettled`: Called after execution completes (success or failure) of each attempt\n * - `onSuccess`: Called when execution succeeds\n * - `onExecutionTimeout`: Called when a single execution attempt times out\n * - `onTotalExecutionTimeout`: Called when the total execution time times out\n *\n * ## Usage\n *\n * - Use for async operations that may fail transiently and benefit from retrying.\n * - Configure `maxAttempts`, `backoff`, `baseWait`, and `jitter` to control retry behavior.\n * - Set `maxExecutionTime` and `maxTotalExecutionTime` to prevent hanging operations.\n * - Use `onAbort`, `onError`, `onLastError`, `onRetry`, `onSettled`, `onSuccess`, `onExecutionTimeout`, and `onTotalExecutionTimeout` for custom side effects.\n * - Call `abort()` to cancel ongoing execution and pending retries.\n * - Call `reset()` to reset state and cancel execution.\n * - Use `getAbortSignal()` to make your async function cancellable.\n * - Use dynamic options (functions) for `maxAttempts`, `baseWait`, and `enabled` based on retryer state.\n *\n * **Important:** This class is designed for single-use execution. Calling `execute()` multiple times\n * on the same instance will abort previous executions. For multiple calls, create a new instance\n * each time.\n *\n * @example\n * ```typescript\n * // Retry a fetch operation up to 5 times with exponential backoff, jitter, and timeouts\n * const retryer = new AsyncRetryer(async (url: string) => {\n * const signal = retryer.getAbortSignal()\n * return await fetch(url, { signal })\n * }, {\n * maxAttempts: 5,\n * backoff: 'exponential',\n * baseWait: 1000,\n * jitter: 0.1, // Add 10% random variation to prevent thundering herd\n * maxExecutionTime: 5000, // Abort individual calls after 5 seconds\n * maxTotalExecutionTime: 30000, // Abort entire operation after 30 seconds\n * onRetry: (attempt, error) => console.log(`Retry attempt ${attempt} after error:`, error),\n * onSuccess: (result) => console.log('Success:', result),\n * onError: (error) => console.error('Error:', error),\n * onLastError: (error) => console.error('All retries failed:', error),\n * })\n *\n * const result = await retryer.execute('/api/data')\n * ```\n *\n * @template TFn The async function type to be retried.\n */\nexport class AsyncRetryer<TFn extends AnyAsyncFunction> {\n readonly store: Store<Readonly<AsyncRetryerState<TFn>>> = new Store(\n getDefaultAsyncRetryerState<TFn>(),\n )\n key: string | undefined\n options: AsyncRetryerOptions<TFn> & typeof defaultOptions\n #abortController: AbortController | null = null\n\n /**\n * Creates a new AsyncRetryer instance\n * @param fn The async function to retry\n * @param initialOptions Configuration options for the retryer\n */\n constructor(\n public fn: TFn,\n initialOptions: AsyncRetryerOptions<TFn> = {},\n ) {\n this.key = initialOptions.key\n this.options = {\n ...defaultOptions,\n ...initialOptions,\n throwOnError:\n initialOptions.throwOnError ??\n (initialOptions.onError ? false : defaultOptions.throwOnError),\n }\n this.#setState(this.options.initialState ?? {})\n\n pacerEventClient.on('d-AsyncRetryer', (event) => {\n if (event.payload.key !== this.key) return\n this.#setState(event.payload.store.state as AsyncRetryerState<TFn>)\n this.setOptions(event.payload.options)\n })\n }\n\n /**\n * Updates the retryer options\n * @param newOptions Partial options to merge with existing options\n */\n setOptions = (newOptions: Partial<AsyncRetryerOptions<TFn>>): void => {\n this.options = { ...this.options, ...newOptions }\n }\n\n #setState = (newState: Partial<AsyncRetryerState<TFn>>): void => {\n this.store.setState((state) => {\n const combinedState = {\n ...state,\n ...newState,\n }\n const { isExecuting, currentAttempt } = combinedState\n return {\n ...combinedState,\n status: !this.#getEnabled()\n ? 'disabled'\n : isExecuting && currentAttempt === 1\n ? 'executing'\n : isExecuting && currentAttempt > 1\n ? 'retrying'\n : 'idle',\n }\n })\n emitChange('AsyncRetryer', this)\n }\n\n #getEnabled = (): boolean => {\n return !!parseFunctionOrValue(this.options.enabled, this)\n }\n\n #getMaxAttempts = (): number => {\n return parseFunctionOrValue(this.options.maxAttempts, this)\n }\n\n #getBaseWait = (): number => {\n return parseFunctionOrValue(this.options.baseWait, this)\n }\n\n #calculateJitter = (waitTime: number): number => {\n const jitterAmount = this.options.jitter\n if (jitterAmount <= 0) return 0\n\n try {\n const crypto =\n typeof globalThis !== 'undefined' ? globalThis.crypto : undefined\n if (crypto?.getRandomValues) {\n const array = new Uint32Array(1)\n crypto.getRandomValues(array)\n // Convert to 0-1 range and apply jitter percentage\n const randomFactor = (array[0]! / 0xffffffff) * 2 - 1 // -1 to 1\n return Math.floor(waitTime * jitterAmount * randomFactor)\n }\n } catch {\n // No crypto available\n }\n return 0\n }\n\n #calculateWait = (attempt: number): number => {\n const baseWait = this.#getBaseWait()\n let waitTime: number\n\n switch (this.options.backoff) {\n case 'linear':\n waitTime = baseWait * attempt\n break\n case 'exponential':\n waitTime = baseWait * Math.pow(2, attempt - 1)\n break\n case 'fixed':\n default:\n waitTime = baseWait\n break\n }\n\n const jitter = this.#calculateJitter(waitTime)\n return Math.max(0, waitTime + jitter)\n }\n\n /**\n * Executes the function with retry logic\n * @param args Arguments to pass to the function\n * @returns The function result, or undefined if disabled or all retries failed (when throwOnError is false)\n * @throws The last error if throwOnError is true and all retries fail\n */\n execute = async (\n ...args: Parameters<TFn>\n ): Promise<Awaited<ReturnType<TFn>> | undefined> => {\n if (!this.#getEnabled()) {\n return undefined\n }\n\n // Cancel any existing execution\n this.abort('new-execution')\n\n const startTime = Date.now()\n let lastError: Error | undefined\n let result: Awaited<ReturnType<TFn>> | undefined\n\n this.#abortController = new AbortController()\n const signal = this.#abortController.signal\n\n this.#setState({\n isExecuting: true,\n currentAttempt: 0,\n lastError: undefined,\n })\n\n // Set up total execution timeout\n let totalTimeoutId: NodeJS.Timeout | undefined\n if (this.options.maxTotalExecutionTime !== Infinity) {\n totalTimeoutId = setTimeout(() => {\n this.options.onTotalExecutionTimeout?.(this)\n this.abort('total-timeout')\n }, this.options.maxTotalExecutionTime)\n }\n\n let isLastAttempt = false\n for (let attempt = 1; attempt <= this.#getMaxAttempts(); attempt++) {\n isLastAttempt = attempt === this.#getMaxAttempts()\n this.#setState({ currentAttempt: attempt })\n\n try {\n if (signal.aborted) {\n return undefined\n }\n\n // Check if total execution time has been exceeded\n const currentTotalTime = Date.now() - startTime\n if (\n this.options.maxTotalExecutionTime !== Infinity &&\n currentTotalTime >= this.options.maxTotalExecutionTime\n ) {\n this.options.onTotalExecutionTimeout?.(this)\n this.abort('total-timeout')\n return undefined\n }\n\n // Execute with individual timeout if specified\n if (this.options.maxExecutionTime === Infinity) {\n result = (await this.fn(...args)) as Awaited<ReturnType<TFn>>\n } else {\n result = (await Promise.race([\n this.fn(...args),\n new Promise<never>((_, reject) => {\n const timeout = setTimeout(() => {\n this.options.onExecutionTimeout?.(this)\n this.abort('execution-timeout')\n reject(\n new Error(\n `Execution timeout: ${this.options.maxExecutionTime}ms exceeded`,\n ),\n )\n }, this.options.maxExecutionTime)\n\n signal.addEventListener(\n 'abort',\n () => {\n clearTimeout(timeout)\n reject(new Error('Aborted'))\n },\n { once: true },\n )\n }),\n ])) as Awaited<ReturnType<TFn>>\n }\n\n // Check if cancelled during execution\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n if (signal.aborted) {\n return undefined\n }\n\n const totalTime = Date.now() - startTime\n this.#setState({\n executionCount: this.store.state.executionCount + 1,\n isExecuting: false,\n lastExecutionTime: Date.now(),\n totalExecutionTime: totalTime,\n currentAttempt: 0,\n lastResult: result,\n })\n\n this.options.onSuccess?.(result as Awaited<ReturnType<TFn>>, args, this)\n\n return result\n } catch (error) {\n // Treat abort as a non-error cancellation outcome\n if (\n error &&\n typeof error === 'object' &&\n 'name' in error &&\n (error as Error).name === 'AbortError'\n ) {\n return undefined\n }\n lastError = error instanceof Error ? error : new Error(String(error))\n this.#setState({ lastError })\n\n // Call onError for every error (including during retries)\n this.options.onError?.(lastError, args, this)\n\n if (attempt < this.#getMaxAttempts()) {\n this.options.onRetry?.(attempt, lastError, this)\n\n const wait = this.#calculateWait(attempt)\n if (wait > 0) {\n // Eagerly reflect retrying status during the wait window\n this.#setState({ isExecuting: true, currentAttempt: attempt + 1 })\n await new Promise<void>((resolve) => {\n const timeout = setTimeout(() => {\n signal.removeEventListener('abort', onAbort)\n resolve()\n }, wait)\n const onAbort = () => {\n clearTimeout(timeout)\n signal.removeEventListener('abort', onAbort)\n resolve()\n }\n signal.addEventListener('abort', onAbort)\n })\n if (signal.aborted) {\n return undefined\n }\n }\n }\n } finally {\n this.options.onSettled?.(args, this)\n }\n }\n\n // Clean up total timeout\n if (totalTimeoutId) {\n clearTimeout(totalTimeoutId)\n }\n\n // Exhausted retries - finalize state\n this.#setState({ isExecuting: false })\n this.options.onLastError?.(lastError as Error, this)\n this.options.onSettled?.(args, this)\n\n if (\n (this.options.throwOnError === 'last' && isLastAttempt) ||\n this.options.throwOnError === true\n ) {\n throw lastError\n }\n\n return undefined\n }\n\n /**\n * Returns the current AbortSignal for the executing operation.\n * Use this signal in your async function to make it cancellable.\n * Returns null when not currently executing.\n *\n * @example\n * ```typescript\n * const retryer = new AsyncRetryer(async (userId: string) => {\n * const signal = retryer.getAbortSignal()\n * if (signal) {\n * return fetch(`/api/users/${userId}`, { signal })\n * }\n * return fetch(`/api/users/${userId}`)\n * })\n *\n * // Abort will now actually cancel the fetch\n * retryer.abort()\n * ```\n */\n getAbortSignal(): AbortSignal | null {\n return this.#abortController?.signal ?? null\n }\n\n /**\n * Cancels the current execution and any pending retries\n * @param reason The reason for the abort (defaults to 'manual')\n */\n abort = (\n reason:\n | 'manual'\n | 'execution-timeout'\n | 'total-timeout'\n | 'new-execution' = 'manual',\n ): void => {\n if (this.#abortController) {\n this.#abortController.abort()\n this.#abortController = null\n this.#setState({\n isExecuting: false,\n })\n this.options.onAbort?.(reason, this)\n }\n }\n\n /**\n * Resets the retryer to its initial state\n */\n reset = (): void => {\n this.#setState(getDefaultAsyncRetryerState<TFn>())\n }\n}\n\n/**\n * Creates a retry-enabled version of an async function. This is a convenience wrapper\n * around the AsyncRetryer class that returns the execute method.\n *\n * @param fn The async function to add retry functionality to\n * @param initialOptions Configuration options for the retry behavior\n * @returns A new function that executes the original with retry logic\n *\n * @example\n * ```typescript\n * // Define your async function normally\n * async function fetchData(url: string) {\n * const response = await fetch(url)\n * if (!response.ok) throw new Error('Request failed')\n * return response.json()\n * }\n *\n * // Create retry-enabled function\n * const fetchWithRetry = asyncRetry(fetchData, {\n * maxAttempts: 3,\n * backoff: 'exponential',\n * baseWait: 1000,\n * jitter: 0.1\n * })\n *\n * // Call it multiple times\n * const data1 = await fetchWithRetry('/api/data1')\n * const data2 = await fetchWithRetry('/api/data2')\n * ```\n */\nexport function asyncRetry<TFn extends AnyAsyncFunction>(\n fn: TFn,\n initialOptions: AsyncRetryerOptions<TFn> = {},\n): (...args: Parameters<TFn>) => Promise<Awaited<ReturnType<TFn>> | undefined> {\n const retryer = new AsyncRetryer(fn, initialOptions)\n return retryer.execute\n}\n"],"names":["Store","emitChange","parseFunctionOrValue","pacerEventClient"],"mappings":"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"}
@@ -1,308 +0,0 @@
1
- import { Store } from '@tanstack/store';
2
- import { AnyAsyncFunction } from './types.cjs';
3
- export interface AsyncRetryerState<TFn extends AnyAsyncFunction> {
4
- /**
5
- * The current retry attempt number (0 when not executing)
6
- */
7
- currentAttempt: number;
8
- /**
9
- * Total number of completed executions (successful or failed)
10
- */
11
- executionCount: number;
12
- /**
13
- * Whether the retryer is currently executing the function
14
- */
15
- isExecuting: boolean;
16
- /**
17
- * The most recent error encountered during execution
18
- */
19
- lastError: Error | undefined;
20
- /**
21
- * Timestamp of the last execution completion in milliseconds
22
- */
23
- lastExecutionTime: number;
24
- /**
25
- * The result from the most recent successful execution
26
- */
27
- lastResult: Awaited<ReturnType<TFn>> | undefined;
28
- /**
29
- * Current execution status - 'disabled' when not enabled, 'idle' when ready, 'executing' when running
30
- */
31
- status: 'disabled' | 'idle' | 'executing' | 'retrying';
32
- /**
33
- * Total time spent executing (including retries) in milliseconds
34
- */
35
- totalExecutionTime: number;
36
- }
37
- export interface AsyncRetryerOptions<TFn extends AnyAsyncFunction> {
38
- /**
39
- * The backoff strategy for retry delays:
40
- * - 'exponential': Wait time doubles with each attempt (1s, 2s, 4s, ...)
41
- * - 'linear': Wait time increases linearly (1s, 2s, 3s, ...)
42
- * - 'fixed': Same wait time for all attempts
43
- * @default 'exponential'
44
- */
45
- backoff?: 'linear' | 'exponential' | 'fixed';
46
- /**
47
- * Base wait time in milliseconds between retries, or a function that returns the wait time
48
- * @default 1000
49
- */
50
- baseWait?: number | ((retryer: AsyncRetryer<TFn>) => number);
51
- /**
52
- * Whether the retryer is enabled, or a function that determines if it's enabled
53
- * @default true
54
- */
55
- enabled?: boolean | ((retryer: AsyncRetryer<TFn>) => boolean);
56
- /**
57
- * Initial state to merge with the default state
58
- */
59
- initialState?: Partial<AsyncRetryerState<TFn>>;
60
- /**
61
- * Jitter percentage to add to retry delays (0-1). Adds randomness to prevent thundering herd.
62
- * @default 0
63
- */
64
- jitter?: number;
65
- /**
66
- * Optional key to identify this async retryer instance.
67
- * If provided, the async retryer will be identified by this key in the devtools and PacerProvider if applicable.
68
- */
69
- key?: string;
70
- /**
71
- * Maximum number of retry attempts, or a function that returns the max attempts
72
- * @default 3
73
- */
74
- maxAttempts?: number | ((retryer: AsyncRetryer<TFn>) => number);
75
- /**
76
- * Maximum execution time in milliseconds for a single function call before aborting
77
- * @default Infinity
78
- */
79
- maxExecutionTime?: number;
80
- /**
81
- * Maximum total execution time in milliseconds for the entire retry operation before aborting
82
- * @default Infinity
83
- */
84
- maxTotalExecutionTime?: number;
85
- /**
86
- * Callback invoked when the execution is aborted (manually or due to timeouts)
87
- */
88
- onAbort?: (reason: 'manual' | 'execution-timeout' | 'total-timeout' | 'new-execution', retryer: AsyncRetryer<TFn>) => void;
89
- /**
90
- * Callback invoked when any error occurs during execution (including retries)
91
- */
92
- onError?: (error: Error, args: Parameters<TFn>, retryer: AsyncRetryer<TFn>) => void;
93
- /**
94
- * Callback invoked when the final error occurs after all retries are exhausted
95
- */
96
- onLastError?: (error: Error, retryer: AsyncRetryer<TFn>) => void;
97
- /**
98
- * Callback invoked before each retry attempt
99
- */
100
- onRetry?: (attempt: number, error: Error, retryer: AsyncRetryer<TFn>) => void;
101
- /**
102
- * Callback invoked after execution completes (success or failure) of each attempt
103
- */
104
- onSettled?: (args: Parameters<TFn>, retryer: AsyncRetryer<TFn>) => void;
105
- /**
106
- * Callback invoked when execution succeeds
107
- */
108
- onSuccess?: (result: Awaited<ReturnType<TFn>>, args: Parameters<TFn>, retryer: AsyncRetryer<TFn>) => void;
109
- /**
110
- * Callback invoked when a single execution attempt times out (maxExecutionTime exceeded)
111
- */
112
- onExecutionTimeout?: (retryer: AsyncRetryer<TFn>) => void;
113
- /**
114
- * Callback invoked when the total execution time times out (maxTotalExecutionTime exceeded)
115
- */
116
- onTotalExecutionTimeout?: (retryer: AsyncRetryer<TFn>) => void;
117
- /**
118
- * Controls when errors are thrown:
119
- * - 'last': Only throw the final error after all retries are exhausted
120
- * - true: Throw every error immediately (disables retrying)
121
- * - false: Never throw errors, return undefined instead
122
- * @default 'last'
123
- */
124
- throwOnError?: boolean | 'last';
125
- }
126
- /**
127
- * Utility function for sharing common `AsyncRetryerOptions` options between different `AsyncRetryer` instances.
128
- */
129
- export declare function asyncRetryerOptions<TFn extends AnyAsyncFunction = AnyAsyncFunction, TOptions extends Partial<AsyncRetryerOptions<TFn>> = Partial<AsyncRetryerOptions<TFn>>>(options: TOptions): TOptions;
130
- declare const defaultOptions: Omit<Required<AsyncRetryerOptions<any>>, 'initialState' | 'key' | 'onAbort' | 'onError' | 'onLastError' | 'onRetry' | 'onSettled' | 'onSuccess' | 'onExecutionTimeout' | 'onTotalExecutionTimeout'>;
131
- /**
132
- * Provides robust retry functionality for asynchronous functions, supporting configurable backoff strategies,
133
- * attempt limits, timeout controls, and detailed state management. The AsyncRetryer class is designed to help you reliably
134
- * execute async operations that may fail intermittently, such as network requests or database operations,
135
- * by automatically retrying them according to your chosen policy.
136
- *
137
- * ## Retrying Concepts
138
- *
139
- * - **Retrying**: Automatically re-executes a failed async function up to a specified number of attempts.
140
- * Useful for handling transient errors (e.g., network flakiness, rate limits, temporary server issues).
141
- * - **Backoff Strategies**: Controls the delay between retry attempts (default: `'exponential'`):
142
- * - `'exponential'`: Wait time doubles with each attempt (1s, 2s, 4s, ...) - **DEFAULT**
143
- * - `'linear'`: Wait time increases linearly (1s, 2s, 3s, ...)
144
- * - `'fixed'`: Waits a constant amount of time (`baseWait`) between each attempt
145
- * - **Jitter**: Adds randomness to retry delays to prevent thundering herd problems (default: `0`).
146
- * Set to a value between 0-1 to apply that percentage of random variation to each delay.
147
- * - **Timeout Controls**: Set limits on execution time to prevent hanging operations:
148
- * - `maxExecutionTime`: Maximum time for a single function call (default: `Infinity`)
149
- * - `maxTotalExecutionTime`: Maximum time for the entire retry operation (default: `Infinity`)
150
- * - **Abort & Cancellation**: Supports cancellation via an internal `AbortController`. Call `abort()` to stop retries.
151
- * Use `getAbortSignal()` to make your async function actually cancellable (e.g., with fetch requests).
152
- *
153
- * ## State Management
154
- *
155
- * Uses TanStack Store for fine-grained reactivity. State can be accessed via the `store.state` property.
156
- *
157
- * Available state properties:
158
- * - `currentAttempt`: The current retry attempt number (0 when not executing)
159
- * - `executionCount`: Total number of completed executions (successful or failed)
160
- * - `isExecuting`: Whether the retryer is currently executing the function
161
- * - `lastError`: The most recent error encountered during execution
162
- * - `lastExecutionTime`: Timestamp of the last execution completion in milliseconds
163
- * - `lastResult`: The result from the most recent successful execution
164
- * - `status`: Current execution status ('disabled' | 'idle' | 'executing' | 'retrying')
165
- * - `totalExecutionTime`: Total time spent executing (including retries) in milliseconds
166
- *
167
- * ## Error Handling
168
- *
169
- * The `throwOnError` option controls when errors are thrown (default: `'last'`):
170
- * - `'last'`: Only throws the final error after all retries are exhausted - **DEFAULT**
171
- * - `true`: Throws every error immediately (disables retrying)
172
- * - `false`: Never throws errors, returns `undefined` instead
173
- *
174
- * Callbacks for lifecycle management:
175
- * - `onAbort`: Called when execution is aborted (manually or due to timeouts)
176
- * - `onError`: Called for every error (including during retries)
177
- * - `onLastError`: Called only for the final error after all retries fail
178
- * - `onRetry`: Called before each retry attempt
179
- * - `onSettled`: Called after execution completes (success or failure) of each attempt
180
- * - `onSuccess`: Called when execution succeeds
181
- * - `onExecutionTimeout`: Called when a single execution attempt times out
182
- * - `onTotalExecutionTimeout`: Called when the total execution time times out
183
- *
184
- * ## Usage
185
- *
186
- * - Use for async operations that may fail transiently and benefit from retrying.
187
- * - Configure `maxAttempts`, `backoff`, `baseWait`, and `jitter` to control retry behavior.
188
- * - Set `maxExecutionTime` and `maxTotalExecutionTime` to prevent hanging operations.
189
- * - Use `onAbort`, `onError`, `onLastError`, `onRetry`, `onSettled`, `onSuccess`, `onExecutionTimeout`, and `onTotalExecutionTimeout` for custom side effects.
190
- * - Call `abort()` to cancel ongoing execution and pending retries.
191
- * - Call `reset()` to reset state and cancel execution.
192
- * - Use `getAbortSignal()` to make your async function cancellable.
193
- * - Use dynamic options (functions) for `maxAttempts`, `baseWait`, and `enabled` based on retryer state.
194
- *
195
- * **Important:** This class is designed for single-use execution. Calling `execute()` multiple times
196
- * on the same instance will abort previous executions. For multiple calls, create a new instance
197
- * each time.
198
- *
199
- * @example
200
- * ```typescript
201
- * // Retry a fetch operation up to 5 times with exponential backoff, jitter, and timeouts
202
- * const retryer = new AsyncRetryer(async (url: string) => {
203
- * const signal = retryer.getAbortSignal()
204
- * return await fetch(url, { signal })
205
- * }, {
206
- * maxAttempts: 5,
207
- * backoff: 'exponential',
208
- * baseWait: 1000,
209
- * jitter: 0.1, // Add 10% random variation to prevent thundering herd
210
- * maxExecutionTime: 5000, // Abort individual calls after 5 seconds
211
- * maxTotalExecutionTime: 30000, // Abort entire operation after 30 seconds
212
- * onRetry: (attempt, error) => console.log(`Retry attempt ${attempt} after error:`, error),
213
- * onSuccess: (result) => console.log('Success:', result),
214
- * onError: (error) => console.error('Error:', error),
215
- * onLastError: (error) => console.error('All retries failed:', error),
216
- * })
217
- *
218
- * const result = await retryer.execute('/api/data')
219
- * ```
220
- *
221
- * @template TFn The async function type to be retried.
222
- */
223
- export declare class AsyncRetryer<TFn extends AnyAsyncFunction> {
224
- #private;
225
- fn: TFn;
226
- readonly store: Store<Readonly<AsyncRetryerState<TFn>>>;
227
- key: string | undefined;
228
- options: AsyncRetryerOptions<TFn> & typeof defaultOptions;
229
- /**
230
- * Creates a new AsyncRetryer instance
231
- * @param fn The async function to retry
232
- * @param initialOptions Configuration options for the retryer
233
- */
234
- constructor(fn: TFn, initialOptions?: AsyncRetryerOptions<TFn>);
235
- /**
236
- * Updates the retryer options
237
- * @param newOptions Partial options to merge with existing options
238
- */
239
- setOptions: (newOptions: Partial<AsyncRetryerOptions<TFn>>) => void;
240
- /**
241
- * Executes the function with retry logic
242
- * @param args Arguments to pass to the function
243
- * @returns The function result, or undefined if disabled or all retries failed (when throwOnError is false)
244
- * @throws The last error if throwOnError is true and all retries fail
245
- */
246
- execute: (...args: Parameters<TFn>) => Promise<Awaited<ReturnType<TFn>> | undefined>;
247
- /**
248
- * Returns the current AbortSignal for the executing operation.
249
- * Use this signal in your async function to make it cancellable.
250
- * Returns null when not currently executing.
251
- *
252
- * @example
253
- * ```typescript
254
- * const retryer = new AsyncRetryer(async (userId: string) => {
255
- * const signal = retryer.getAbortSignal()
256
- * if (signal) {
257
- * return fetch(`/api/users/${userId}`, { signal })
258
- * }
259
- * return fetch(`/api/users/${userId}`)
260
- * })
261
- *
262
- * // Abort will now actually cancel the fetch
263
- * retryer.abort()
264
- * ```
265
- */
266
- getAbortSignal(): AbortSignal | null;
267
- /**
268
- * Cancels the current execution and any pending retries
269
- * @param reason The reason for the abort (defaults to 'manual')
270
- */
271
- abort: (reason?: "manual" | "execution-timeout" | "total-timeout" | "new-execution") => void;
272
- /**
273
- * Resets the retryer to its initial state
274
- */
275
- reset: () => void;
276
- }
277
- /**
278
- * Creates a retry-enabled version of an async function. This is a convenience wrapper
279
- * around the AsyncRetryer class that returns the execute method.
280
- *
281
- * @param fn The async function to add retry functionality to
282
- * @param initialOptions Configuration options for the retry behavior
283
- * @returns A new function that executes the original with retry logic
284
- *
285
- * @example
286
- * ```typescript
287
- * // Define your async function normally
288
- * async function fetchData(url: string) {
289
- * const response = await fetch(url)
290
- * if (!response.ok) throw new Error('Request failed')
291
- * return response.json()
292
- * }
293
- *
294
- * // Create retry-enabled function
295
- * const fetchWithRetry = asyncRetry(fetchData, {
296
- * maxAttempts: 3,
297
- * backoff: 'exponential',
298
- * baseWait: 1000,
299
- * jitter: 0.1
300
- * })
301
- *
302
- * // Call it multiple times
303
- * const data1 = await fetchWithRetry('/api/data1')
304
- * const data2 = await fetchWithRetry('/api/data2')
305
- * ```
306
- */
307
- export declare function asyncRetry<TFn extends AnyAsyncFunction>(fn: TFn, initialOptions?: AsyncRetryerOptions<TFn>): (...args: Parameters<TFn>) => Promise<Awaited<ReturnType<TFn>> | undefined>;
308
- export {};
@@ -1,261 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
- const store = require("@tanstack/store");
4
- const asyncRetryer = require("./async-retryer.cjs");
5
- const utils = require("./utils.cjs");
6
- const eventClient = require("./event-client.cjs");
7
- function getDefaultAsyncThrottlerState() {
8
- return {
9
- errorCount: 0,
10
- isExecuting: false,
11
- isPending: false,
12
- lastArgs: void 0,
13
- lastExecutionTime: 0,
14
- lastResult: void 0,
15
- maybeExecuteCount: 0,
16
- nextExecutionTime: void 0,
17
- settleCount: 0,
18
- status: "idle",
19
- successCount: 0
20
- };
21
- }
22
- function asyncThrottlerOptions(options) {
23
- return options;
24
- }
25
- const defaultOptions = {
26
- asyncRetryerOptions: {
27
- maxAttempts: 1
28
- },
29
- enabled: true,
30
- leading: true,
31
- trailing: true,
32
- wait: 0
33
- };
34
- class AsyncThrottler {
35
- constructor(fn, initialOptions) {
36
- this.fn = fn;
37
- this.store = new store.Store(getDefaultAsyncThrottlerState());
38
- this.asyncRetryers = /* @__PURE__ */ new Map();
39
- this.#timeoutId = null;
40
- this.#resolvePreviousPromise = null;
41
- this.setOptions = (newOptions) => {
42
- this.options = { ...this.options, ...newOptions };
43
- if (!this.#getEnabled()) {
44
- this.cancel();
45
- }
46
- };
47
- this.#setState = (newState) => {
48
- this.store.setState((state) => {
49
- const combinedState = {
50
- ...state,
51
- ...newState
52
- };
53
- const { isPending, isExecuting, settleCount } = combinedState;
54
- return {
55
- ...combinedState,
56
- status: !this.#getEnabled() ? "disabled" : isPending ? "pending" : isExecuting ? "executing" : settleCount > 0 ? "settled" : "idle"
57
- };
58
- });
59
- eventClient.emitChange("AsyncThrottler", this);
60
- };
61
- this.#getEnabled = () => {
62
- return !!utils.parseFunctionOrValue(this.options.enabled, this);
63
- };
64
- this.#getWait = () => {
65
- return utils.parseFunctionOrValue(this.options.wait, this);
66
- };
67
- this.maybeExecute = async (...args) => {
68
- if (!this.#getEnabled()) return void 0;
69
- this.#resolvePreviousPromiseInternal();
70
- this.#setState({
71
- maybeExecuteCount: this.store.state.maybeExecuteCount + 1,
72
- lastArgs: args
73
- // store the arguments for potential trailing execution
74
- });
75
- const wait = this.#getWait();
76
- const thisMaybeExecuteNumber = this.store.state.maybeExecuteCount;
77
- for (let maxNumIterations = wait / 10; this.store.state.isExecuting && maxNumIterations > 0; maxNumIterations--) {
78
- await new Promise((resolve) => setTimeout(resolve, 10));
79
- if (this.store.state.maybeExecuteCount !== thisMaybeExecuteNumber) {
80
- return this.store.state.lastResult;
81
- }
82
- }
83
- const now = Date.now();
84
- const timeSinceLastExecution = now - this.store.state.lastExecutionTime;
85
- if (this.options.leading && !this.store.state.isPending && timeSinceLastExecution >= wait) {
86
- await this.#execute(...args);
87
- } else if (this.options.trailing) {
88
- this.cancel();
89
- this.#setState({
90
- isPending: true
91
- });
92
- return new Promise((resolve, reject) => {
93
- this.#resolvePreviousPromise = resolve;
94
- const newTimeSinceLastExecution = this.store.state.lastExecutionTime ? now - this.store.state.lastExecutionTime : 0;
95
- const timeoutDuration = Math.max(0, wait - newTimeSinceLastExecution);
96
- this.#timeoutId = setTimeout(async () => {
97
- this.#clearTimeout();
98
- if (this.store.state.lastArgs !== void 0) {
99
- try {
100
- await this.#execute(...this.store.state.lastArgs);
101
- } catch (error) {
102
- reject(error);
103
- }
104
- }
105
- this.#resolvePreviousPromise = null;
106
- resolve(this.store.state.lastResult);
107
- }, timeoutDuration);
108
- });
109
- }
110
- return this.store.state.lastResult;
111
- };
112
- this.#execute = async (...args) => {
113
- if (!this.#getEnabled()) return void 0;
114
- const currentMaybeExecute = this.store.state.maybeExecuteCount;
115
- try {
116
- this.#setState({ isExecuting: true });
117
- const currentAsyncRetryer = new asyncRetryer.AsyncRetryer(this.fn, {
118
- ...this.options.asyncRetryerOptions,
119
- key: `${this.key}-retryer-${currentMaybeExecute}`
120
- });
121
- this.asyncRetryers.set(currentMaybeExecute, currentAsyncRetryer);
122
- const result = await currentAsyncRetryer.execute(...args);
123
- this.#setState({
124
- lastResult: result,
125
- successCount: this.store.state.successCount + 1
126
- });
127
- this.options.onSuccess?.(result, args, this);
128
- } catch (error) {
129
- this.#setState({
130
- errorCount: this.store.state.errorCount + 1
131
- });
132
- this.options.onError?.(error, args, this);
133
- if (this.options.throwOnError) {
134
- throw error;
135
- }
136
- } finally {
137
- this.asyncRetryers.delete(currentMaybeExecute);
138
- const lastExecutionTime = Date.now();
139
- const wait = this.#getWait();
140
- const nextExecutionTime = lastExecutionTime + wait;
141
- this.#setState({
142
- isExecuting: false,
143
- isPending: !!this.#timeoutId,
144
- settleCount: this.store.state.settleCount + 1,
145
- lastExecutionTime,
146
- nextExecutionTime
147
- });
148
- this.options.onSettled?.(args, this);
149
- setTimeout(() => {
150
- if (!this.store.state.isPending) {
151
- this.#setState({ nextExecutionTime: void 0 });
152
- }
153
- }, wait);
154
- }
155
- return this.store.state.lastResult;
156
- };
157
- this.flush = async () => {
158
- if (this.store.state.isPending && this.store.state.lastArgs) {
159
- const resolvePromise = this.#resolvePreviousPromise;
160
- this.#clearTimeout();
161
- this.#setState({
162
- isPending: false
163
- });
164
- const result = await this.#execute(...this.store.state.lastArgs);
165
- if (resolvePromise) {
166
- resolvePromise(result);
167
- }
168
- return result;
169
- }
170
- return void 0;
171
- };
172
- this.#resolvePreviousPromiseInternal = () => {
173
- if (this.#resolvePreviousPromise) {
174
- this.#resolvePreviousPromise(this.store.state.lastResult);
175
- this.#resolvePreviousPromise = null;
176
- }
177
- };
178
- this.#clearTimeout = () => {
179
- if (this.#timeoutId) {
180
- clearTimeout(this.#timeoutId);
181
- this.#timeoutId = null;
182
- }
183
- };
184
- this.abort = () => {
185
- this.asyncRetryers.forEach((retryer) => retryer.abort());
186
- this.asyncRetryers.clear();
187
- this.#setState({ isExecuting: false });
188
- };
189
- this.cancel = () => {
190
- this.#clearTimeout();
191
- if (this.#resolvePreviousPromise) {
192
- this.#resolvePreviousPromiseInternal();
193
- this.#resolvePreviousPromise = null;
194
- }
195
- this.#setState({
196
- isPending: false
197
- });
198
- };
199
- this.reset = () => {
200
- this.#setState(getDefaultAsyncThrottlerState());
201
- this.asyncRetryers.forEach((retryer) => retryer.reset());
202
- };
203
- this.key = initialOptions.key;
204
- this.options = {
205
- ...defaultOptions,
206
- ...initialOptions,
207
- throwOnError: initialOptions.throwOnError ?? !initialOptions.onError
208
- };
209
- this.#setState(this.options.initialState ?? {});
210
- eventClient.pacerEventClient.on("d-AsyncThrottler", (event) => {
211
- if (event.payload.key !== this.key) return;
212
- this.#setState(event.payload.store.state);
213
- this.setOptions(event.payload.options);
214
- });
215
- }
216
- #timeoutId;
217
- #resolvePreviousPromise;
218
- #setState;
219
- #getEnabled;
220
- #getWait;
221
- #execute;
222
- #resolvePreviousPromiseInternal;
223
- #clearTimeout;
224
- /**
225
- * Returns the AbortSignal for a specific execution.
226
- * If no maybeExecuteCount is provided, returns the signal for the most recent execution.
227
- * Returns null if no execution is found or not currently executing.
228
- *
229
- * @param maybeExecuteCount - Optional specific execution to get signal for
230
- * @example
231
- * ```typescript
232
- * const throttler = new AsyncThrottler(
233
- * async (data: string) => {
234
- * const signal = throttler.getAbortSignal()
235
- * if (signal) {
236
- * const response = await fetch('/api/save', {
237
- * method: 'POST',
238
- * body: data,
239
- * signal
240
- * })
241
- * return response.json()
242
- * }
243
- * },
244
- * { wait: 1000 }
245
- * )
246
- * ```
247
- */
248
- getAbortSignal(maybeExecuteCount) {
249
- const count = maybeExecuteCount ?? this.store.state.maybeExecuteCount;
250
- const retryer = this.asyncRetryers.get(count);
251
- return retryer?.getAbortSignal() ?? null;
252
- }
253
- }
254
- function asyncThrottle(fn, initialOptions) {
255
- const asyncThrottler = new AsyncThrottler(fn, initialOptions);
256
- return asyncThrottler.maybeExecute;
257
- }
258
- exports.AsyncThrottler = AsyncThrottler;
259
- exports.asyncThrottle = asyncThrottle;
260
- exports.asyncThrottlerOptions = asyncThrottlerOptions;
261
- //# sourceMappingURL=async-throttler.cjs.map