@tanstack/pacer 0.21.1 → 0.22.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 (77) hide show
  1. package/README.md +17 -2
  2. package/dist/async-batcher.cjs +1 -1
  3. package/dist/async-batcher.cjs.map +1 -1
  4. package/dist/async-batcher.d.cts +0 -1
  5. package/dist/async-batcher.d.ts +0 -1
  6. package/dist/async-batcher.js +1 -1
  7. package/dist/async-batcher.js.map +1 -1
  8. package/dist/async-debouncer.cjs +2 -5
  9. package/dist/async-debouncer.cjs.map +1 -1
  10. package/dist/async-debouncer.d.cts +5 -6
  11. package/dist/async-debouncer.d.ts +5 -6
  12. package/dist/async-debouncer.js +2 -5
  13. package/dist/async-debouncer.js.map +1 -1
  14. package/dist/async-queuer.cjs +49 -17
  15. package/dist/async-queuer.cjs.map +1 -1
  16. package/dist/async-queuer.d.cts +1 -1
  17. package/dist/async-queuer.d.ts +1 -1
  18. package/dist/async-queuer.js +49 -17
  19. package/dist/async-queuer.js.map +1 -1
  20. package/dist/async-rate-limiter.cjs +3 -6
  21. package/dist/async-rate-limiter.cjs.map +1 -1
  22. package/dist/async-rate-limiter.d.cts +4 -5
  23. package/dist/async-rate-limiter.d.ts +4 -5
  24. package/dist/async-rate-limiter.js +3 -6
  25. package/dist/async-rate-limiter.js.map +1 -1
  26. package/dist/async-retryer.cjs +2 -11
  27. package/dist/async-retryer.cjs.map +1 -1
  28. package/dist/async-retryer.d.cts +4 -2
  29. package/dist/async-retryer.d.ts +4 -2
  30. package/dist/async-retryer.js +2 -11
  31. package/dist/async-retryer.js.map +1 -1
  32. package/dist/async-throttler.cjs +2 -5
  33. package/dist/async-throttler.cjs.map +1 -1
  34. package/dist/async-throttler.d.cts +5 -6
  35. package/dist/async-throttler.d.ts +5 -6
  36. package/dist/async-throttler.js +2 -5
  37. package/dist/async-throttler.js.map +1 -1
  38. package/dist/batcher.cjs.map +1 -1
  39. package/dist/batcher.d.cts +0 -1
  40. package/dist/batcher.d.ts +0 -1
  41. package/dist/batcher.js.map +1 -1
  42. package/dist/debouncer.cjs.map +1 -1
  43. package/dist/debouncer.d.cts +0 -1
  44. package/dist/debouncer.d.ts +0 -1
  45. package/dist/debouncer.js.map +1 -1
  46. package/dist/event-client.cjs.map +1 -1
  47. package/dist/event-client.d.cts +0 -1
  48. package/dist/event-client.d.ts +0 -1
  49. package/dist/event-client.js.map +1 -1
  50. package/dist/index.cjs +1 -1
  51. package/dist/index.js +1 -1
  52. package/dist/queuer.cjs +7 -2
  53. package/dist/queuer.cjs.map +1 -1
  54. package/dist/queuer.d.cts +1 -1
  55. package/dist/queuer.d.ts +1 -1
  56. package/dist/queuer.js +7 -2
  57. package/dist/queuer.js.map +1 -1
  58. package/dist/rate-limiter.cjs.map +1 -1
  59. package/dist/rate-limiter.d.cts +0 -1
  60. package/dist/rate-limiter.d.ts +0 -1
  61. package/dist/rate-limiter.js.map +1 -1
  62. package/dist/throttler.cjs.map +1 -1
  63. package/dist/throttler.d.cts +0 -1
  64. package/dist/throttler.d.ts +0 -1
  65. package/dist/throttler.js.map +1 -1
  66. package/dist/utils.cjs +0 -1
  67. package/dist/utils.cjs.map +1 -1
  68. package/dist/utils.d.cts +0 -1
  69. package/dist/utils.d.ts +0 -1
  70. package/dist/utils.js.map +1 -1
  71. package/package.json +3 -3
  72. package/src/async-debouncer.ts +30 -29
  73. package/src/async-queuer.ts +83 -20
  74. package/src/async-rate-limiter.ts +11 -10
  75. package/src/async-retryer.ts +4 -15
  76. package/src/async-throttler.ts +33 -32
  77. package/src/queuer.ts +13 -2
@@ -1 +1 @@
1
- {"version":3,"file":"async-throttler.cjs","names":["Store","#getEnabled","parseFunctionOrValue","#resolvePreviousPromiseInternal","#setState","#getWait","#execute","#resolvePreviousPromise","#timeoutId","#clearTimeout","AsyncRetryer"],"sources":["../src/async-throttler.ts"],"sourcesContent":["import { Store } from '@tanstack/store'\nimport { AsyncRetryer } from './async-retryer'\nimport { parseFunctionOrValue } from './utils'\nimport { emitChange, pacerEventClient } from './event-client'\nimport type { AsyncRetryerOptions } from './async-retryer'\nimport type { AnyAsyncFunction, OptionalKeys } from './types'\n\nexport interface AsyncThrottlerState<TFn extends AnyAsyncFunction> {\n /**\n * Number of function executions that have resulted in errors\n */\n errorCount: number\n /**\n * Whether the throttled function is currently executing asynchronously\n */\n isExecuting: boolean\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 * The result from the most recent successful function execution\n */\n lastResult: ReturnType<TFn> | undefined\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 * Number of function executions that have completed (either successfully or with errors)\n */\n settleCount: number\n /**\n * Current execution status - 'idle' when not active, 'pending' when waiting, 'executing' when running, 'settled' when completed\n */\n status: 'disabled' | 'idle' | 'pending' | 'executing' | 'settled'\n /**\n * Number of function executions that have completed successfully\n */\n successCount: number\n}\n\nfunction getDefaultAsyncThrottlerState<\n TFn extends AnyAsyncFunction,\n>(): AsyncThrottlerState<TFn> {\n return {\n errorCount: 0,\n isExecuting: false,\n isPending: false,\n lastArgs: undefined,\n lastExecutionTime: 0,\n lastResult: undefined,\n maybeExecuteCount: 0,\n nextExecutionTime: undefined,\n settleCount: 0,\n status: 'idle',\n successCount: 0,\n }\n}\n\n/**\n * Options for configuring an async throttled function\n */\nexport interface AsyncThrottlerOptions<TFn extends AnyAsyncFunction> {\n /**\n * Options for configuring the underlying async retryer\n */\n asyncRetryerOptions?: AsyncRetryerOptions<TFn>\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: AsyncThrottler<TFn>) => boolean)\n /**\n * Initial state for the async throttler\n */\n initialState?: Partial<AsyncThrottlerState<TFn>>\n /**\n * Optional key to identify this async throttler instance.\n * If provided, the async throttler will be identified by this key in the devtools and PacerProvider if applicable.\n */\n key?: string\n /**\n * Whether to execute the function immediately when called\n * Defaults to true\n */\n leading?: boolean\n /**\n * Optional error handler for when the throttled function throws.\n * If provided, the handler will be called with the error and throttler 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 args: Parameters<TFn>,\n asyncThrottler: AsyncThrottler<TFn>,\n ) => void\n /**\n * Optional function to call when the throttled function is executed\n */\n onSettled?: (\n args: Parameters<TFn>,\n asyncThrottler: AsyncThrottler<TFn>,\n ) => void\n /**\n * Optional function to call when the throttled function is executed\n */\n onSuccess?: (\n result: ReturnType<TFn>,\n args: Parameters<TFn>,\n asyncThrottler: AsyncThrottler<TFn>,\n ) => void\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 * Whether to execute the function on the trailing edge of the wait period\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: AsyncThrottler<TFn>) => number)\n}\n\n/**\n * Utility function for sharing common `AsyncThrottlerOptions` options between different `AsyncThrottler` instances.\n */\nexport function asyncThrottlerOptions<\n TFn extends AnyAsyncFunction = AnyAsyncFunction,\n TOptions extends Partial<AsyncThrottlerOptions<TFn>> = Partial<\n AsyncThrottlerOptions<TFn>\n >,\n>(options: TOptions): TOptions {\n return options\n}\n\ntype AsyncThrottlerOptionsWithOptionalCallbacks = OptionalKeys<\n AsyncThrottlerOptions<any>,\n 'initialState' | 'onError' | 'onSettled' | 'onSuccess'\n>\n\nconst defaultOptions: AsyncThrottlerOptionsWithOptionalCallbacks = {\n asyncRetryerOptions: {\n maxAttempts: 1,\n },\n enabled: true,\n leading: true,\n trailing: true,\n wait: 0,\n}\n\n/**\n * A class that creates an async throttled function.\n *\n * Async vs Sync Versions:\n * The async version provides advanced features over the sync Throttler:\n * - Returns promises that can be awaited for throttled function results\n * - Built-in retry support via AsyncRetryer integration\n * - Abort support to cancel in-flight executions\n * - Cancel support to prevent pending executions from starting\n * - Comprehensive error handling with onError callbacks and throwOnError control\n * - Detailed execution tracking (success/error/settle counts)\n * - Waits for ongoing executions to complete before scheduling the next one\n *\n * The sync Throttler is lighter weight and simpler when you don't need async features,\n * return values, or execution control.\n *\n * What is Throttling?\n * Throttling limits how often a function can be executed, allowing only one execution within a specified time window.\n * Unlike debouncing which resets the delay timer on each call, throttling ensures the function executes at a\n * regular interval regardless of how often it's called.\n *\n * This is useful for rate-limiting API calls, handling scroll/resize events, or any scenario where you want to\n * ensure a maximum execution frequency.\n *\n * Error Handling:\n * - If an `onError` handler is provided, it will be called with the error and throttler 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 AsyncThrottler 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 throttler\n * - Use `onSuccess` callback to react to successful function execution and implement custom logic\n * - Use `onError` callback to react to function execution errors and implement custom error handling\n * - Use `onSettled` callback to react to function execution completion (success or error) and implement custom logic\n * - The state includes error count, execution status, last execution time, and success/settle counts\n * - State can be accessed via `asyncThrottler.store.state` when using the class directly\n * - When using framework adapters (React/Solid), state is accessed from `asyncThrottler.state`\n *\n * @example\n * ```ts\n * const throttler = new AsyncThrottler(async (value: string) => {\n * const result = await saveToAPI(value);\n * return result; // Return value is preserved\n * }, {\n * wait: 1000,\n * onError: (error) => {\n * console.error('API call failed:', error);\n * }\n * });\n *\n * // Will only execute once per second no matter how often called\n * // Returns the API response directly\n * const result = await throttler.maybeExecute(inputElement.value);\n * ```\n */\nexport class AsyncThrottler<TFn extends AnyAsyncFunction> {\n readonly store: Store<Readonly<AsyncThrottlerState<TFn>>> = new Store<\n AsyncThrottlerState<TFn>\n >(getDefaultAsyncThrottlerState<TFn>())\n key: string | undefined\n options: AsyncThrottlerOptions<TFn>\n asyncRetryers = new Map<number, AsyncRetryer<TFn>>()\n #timeoutId: ReturnType<typeof setTimeout> | null = null\n #resolvePreviousPromise:\n | ((value?: ReturnType<TFn> | undefined) => void)\n | null = null\n\n constructor(\n public fn: TFn,\n initialOptions: AsyncThrottlerOptions<TFn>,\n ) {\n this.key = initialOptions.key\n this.options = {\n ...defaultOptions,\n ...initialOptions,\n throwOnError: initialOptions.throwOnError ?? !initialOptions.onError,\n }\n this.#setState(this.options.initialState ?? {})\n\n if (this.key) {\n pacerEventClient.on('d-AsyncThrottler', (event) => {\n if (event.payload.key !== this.key) return\n this.#setState(\n event.payload.store.state as Partial<AsyncThrottlerState<TFn>>,\n )\n this.setOptions(\n event.payload.options as Partial<AsyncThrottlerOptions<TFn>>,\n )\n })\n }\n }\n\n /**\n * Updates the async throttler options\n */\n setOptions = (newOptions: Partial<AsyncThrottlerOptions<TFn>>): void => {\n this.options = { ...this.options, ...newOptions }\n\n // End the pending state if the throttler is disabled\n if (!this.#getEnabled()) {\n this.cancel()\n }\n }\n\n #setState = (newState: Partial<AsyncThrottlerState<TFn>>): void => {\n this.store.setState((state) => {\n const combinedState = {\n ...state,\n ...newState,\n }\n const { isPending, isExecuting, settleCount } = combinedState\n return {\n ...combinedState,\n status: !this.#getEnabled()\n ? 'disabled'\n : isPending\n ? 'pending'\n : isExecuting\n ? 'executing'\n : settleCount > 0\n ? 'settled'\n : 'idle',\n }\n })\n emitChange('AsyncThrottler', this)\n }\n\n /**\n * Returns the current enabled state of the async throttler\n */\n #getEnabled = (): boolean => {\n return !!parseFunctionOrValue(this.options.enabled, this)\n }\n\n /**\n * Returns the current wait time in milliseconds\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 AsyncThrottler(fn, { wait: 1000 });\n *\n * // First call executes immediately\n * await throttled.maybeExecute('a', 'b');\n *\n * // Call during wait period - gets throttled\n * await throttled.maybeExecute('c', 'd');\n * ```\n */\n maybeExecute = async (\n ...args: Parameters<TFn>\n ): Promise<ReturnType<TFn> | undefined> => {\n if (!this.#getEnabled()) return undefined\n\n this.#resolvePreviousPromiseInternal()\n\n this.#setState({\n maybeExecuteCount: this.store.state.maybeExecuteCount + 1,\n lastArgs: args, // store the arguments for potential trailing execution\n })\n\n const wait = this.#getWait()\n const thisMaybeExecuteNumber = this.store.state.maybeExecuteCount\n\n // Wait for the wait period for the previous execution to complete if it's still running\n for (\n let maxNumIterations = wait / 10;\n this.store.state.isExecuting && maxNumIterations > 0;\n maxNumIterations--\n ) {\n await new Promise((resolve) => setTimeout(resolve, 10))\n if (this.store.state.maybeExecuteCount !== thisMaybeExecuteNumber) {\n // cancel the current maybeExecute loop because a new maybeExecute call was made\n return this.store.state.lastResult\n }\n }\n\n const now = Date.now()\n const timeSinceLastExecution = now - this.store.state.lastExecutionTime\n\n if (\n this.options.leading &&\n !this.store.state.isPending &&\n timeSinceLastExecution >= wait\n ) {\n await this.#execute(...args) // Leading EXECUTE!\n } else if (this.options.trailing) {\n // replace old pending execution with a new one\n this.cancel()\n this.#setState({\n isPending: true,\n })\n\n // Set up new trailing execution\n return new Promise((resolve, reject) => {\n this.#resolvePreviousPromise = resolve\n\n const newTimeSinceLastExecution = this.store.state.lastExecutionTime\n ? now - this.store.state.lastExecutionTime\n : 0\n const timeoutDuration = Math.max(0, wait - newTimeSinceLastExecution)\n\n this.#timeoutId = setTimeout(async () => {\n this.#clearTimeout()\n if (this.store.state.lastArgs !== undefined) {\n try {\n await this.#execute(...this.store.state.lastArgs) // Trailing EXECUTE!\n } catch (error) {\n reject(error)\n }\n }\n this.#resolvePreviousPromise = null\n resolve(this.store.state.lastResult)\n }, timeoutDuration)\n })\n }\n return this.store.state.lastResult\n }\n\n #execute = async (\n ...args: Parameters<TFn>\n ): Promise<ReturnType<TFn> | undefined> => {\n if (!this.#getEnabled()) return undefined\n\n const currentMaybeExecute = this.store.state.maybeExecuteCount\n\n try {\n this.#setState({ isExecuting: true })\n const currentAsyncRetryer = new AsyncRetryer(this.fn, {\n ...this.options.asyncRetryerOptions,\n key: `${this.key}-retryer-${currentMaybeExecute}`,\n })\n this.asyncRetryers.set(currentMaybeExecute, currentAsyncRetryer)\n const result = await currentAsyncRetryer.execute(...args) // EXECUTE!\n this.#setState({\n lastResult: result,\n successCount: this.store.state.successCount + 1,\n })\n this.options.onSuccess?.(result as ReturnType<TFn>, args, this)\n } catch (error) {\n this.#setState({\n errorCount: this.store.state.errorCount + 1,\n })\n this.options.onError?.(error as Error, args, this)\n if (this.options.throwOnError) {\n throw error\n }\n } finally {\n this.asyncRetryers.delete(currentMaybeExecute) // dispose retryer\n const lastExecutionTime = Date.now()\n const wait = this.#getWait()\n const nextExecutionTime = lastExecutionTime + wait\n this.#setState({\n isExecuting: false,\n isPending: !!this.#timeoutId,\n settleCount: this.store.state.settleCount + 1,\n lastExecutionTime,\n nextExecutionTime,\n })\n this.options.onSettled?.(args, this)\n setTimeout(() => {\n if (!this.store.state.isPending) {\n // clear nextExecutionTime if there is no pending execution\n this.#setState({ nextExecutionTime: undefined })\n }\n }, wait)\n }\n return this.store.state.lastResult\n }\n\n /**\n * Processes the current pending execution immediately\n */\n flush = async (): Promise<ReturnType<TFn> | undefined> => {\n if (this.store.state.isPending && this.store.state.lastArgs) {\n // Store the pending promise resolver before clearing timeout\n const resolvePromise = this.#resolvePreviousPromise\n\n // Clear timeout and state without resolving the promise\n this.#clearTimeout()\n this.#setState({\n isPending: false,\n })\n\n const result = await this.#execute(...this.store.state.lastArgs)\n\n // Resolve the pending promise with the result\n if (resolvePromise) {\n resolvePromise(result)\n }\n\n return result\n }\n return undefined\n }\n\n #resolvePreviousPromiseInternal = (): void => {\n if (this.#resolvePreviousPromise) {\n this.#resolvePreviousPromise(this.store.state.lastResult)\n this.#resolvePreviousPromise = null\n }\n }\n\n #clearTimeout = (): void => {\n if (this.#timeoutId) {\n clearTimeout(this.#timeoutId)\n this.#timeoutId = null\n }\n }\n\n /**\n * Returns the AbortSignal for a specific execution.\n * If no maybeExecuteCount 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 maybeExecuteCount - Optional specific execution to get signal for\n * @example\n * ```typescript\n * const throttler = new AsyncThrottler(\n * async (data: string) => {\n * const signal = throttler.getAbortSignal()\n * if (signal) {\n * const response = await fetch('/api/save', {\n * method: 'POST',\n * body: data,\n * signal\n * })\n * return response.json()\n * }\n * },\n * { wait: 1000 }\n * )\n * ```\n */\n getAbortSignal = (maybeExecuteCount?: number): AbortSignal | null => {\n const count = maybeExecuteCount ?? this.store.state.maybeExecuteCount\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 */\n abort = (): void => {\n this.asyncRetryers.forEach((retryer) => retryer.abort())\n this.asyncRetryers.clear()\n this.#setState({ isExecuting: false })\n }\n\n /**\n * Cancels any pending execution that have not started yet.\n * Does NOT abort any execution already in progress.\n */\n cancel = (): void => {\n this.#clearTimeout()\n if (this.#resolvePreviousPromise) {\n this.#resolvePreviousPromiseInternal()\n this.#resolvePreviousPromise = null\n }\n this.#setState({\n isPending: false,\n })\n }\n\n /**\n * Resets the debouncer state to its default values\n */\n reset = (): void => {\n this.#setState(getDefaultAsyncThrottlerState<TFn>())\n this.asyncRetryers.forEach((retryer) => retryer.reset())\n }\n}\n\n/**\n * Creates an async throttled function that limits how often the function can execute.\n * The throttled function will execute at most once per wait period, even if called multiple times.\n * If called while executing, it will wait until execution completes before scheduling the next call.\n *\n * Async vs Sync Versions:\n * The async version provides advanced features over the sync throttle function:\n * - Returns promises that can be awaited for throttled function results\n * - Built-in retry support via AsyncRetryer integration\n * - Abort support to cancel in-flight executions\n * - Cancel support to prevent pending executions from starting\n * - Comprehensive error handling with onError callbacks and throwOnError control\n * - Detailed execution tracking (success/error/settle counts)\n * - Waits for ongoing executions to complete before scheduling the next one\n *\n * The sync throttle function is lighter weight and simpler when you don't need async features,\n * return values, or execution control.\n *\n * What is Throttling?\n * Throttling limits how often a function can be executed, allowing only one execution within a specified time window.\n * Unlike debouncing which resets the delay timer on each call, throttling ensures the function executes at a\n * regular interval regardless of how often it's called.\n *\n * Configuration Options:\n * - `wait`: Time window in milliseconds during which the function can only execute once (required)\n * - `leading`: Execute immediately when called (default: true)\n * - `trailing`: Execute on the trailing edge of the wait period (default: true)\n * - `enabled`: Whether the throttler is enabled (default: true)\n * - `asyncRetryerOptions`: Configure retry behavior for executions\n *\n * Error Handling:\n * - If an `onError` handler is provided, it will be called with the error and throttler 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 AsyncThrottler 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 throttler\n * - Use `onSuccess` callback to react to successful function execution and implement custom logic\n * - Use `onError` callback to react to function execution errors and implement custom error handling\n * - Use `onSettled` callback to react to function execution completion (success or error) and implement custom logic\n * - The state includes error count, execution status, last execution time, and success/settle counts\n * - State can be accessed via the underlying AsyncThrottler 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 throttled = asyncThrottle(async (value: string) => {\n * const result = await saveToAPI(value);\n * return result; // Return value is preserved\n * }, {\n * wait: 1000,\n * onError: (error) => {\n * console.error('API call failed:', error);\n * }\n * });\n *\n * // This will execute at most once per second\n * // Returns the API response directly\n * const result = await throttled(inputElement.value);\n * ```\n */\nexport function asyncThrottle<TFn extends AnyAsyncFunction>(\n fn: TFn,\n initialOptions: AsyncThrottlerOptions<TFn>,\n) {\n const asyncThrottler = new AsyncThrottler(fn, initialOptions)\n return asyncThrottler.maybeExecute\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"async-throttler.cjs","names":["Store","#getEnabled","parseFunctionOrValue","#resolvePreviousPromiseInternal","#setState","#getWait","#execute","#resolvePreviousPromise","#timeoutId","#clearTimeout","AsyncRetryer"],"sources":["../src/async-throttler.ts"],"sourcesContent":["import { Store } from '@tanstack/store'\nimport { AsyncRetryer } from './async-retryer'\nimport { parseFunctionOrValue } from './utils'\nimport { emitChange, pacerEventClient } from './event-client'\nimport type { AsyncRetryerOptions } from './async-retryer'\nimport type { AnyAsyncFunction, OptionalKeys } from './types'\n\nexport interface AsyncThrottlerState<TFn extends AnyAsyncFunction> {\n /**\n * Number of function executions that have resulted in errors\n */\n errorCount: number\n /**\n * Whether the throttled function is currently executing asynchronously\n */\n isExecuting: boolean\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 * The result from the most recent successful function execution\n */\n lastResult: Awaited<ReturnType<TFn>> | undefined\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 * Number of function executions that have completed (either successfully or with errors)\n */\n settleCount: number\n /**\n * Current execution status - 'idle' when not active, 'pending' when waiting, 'executing' when running, 'settled' when completed\n */\n status: 'disabled' | 'idle' | 'pending' | 'executing' | 'settled'\n /**\n * Number of function executions that have completed successfully\n */\n successCount: number\n}\n\nfunction getDefaultAsyncThrottlerState<\n TFn extends AnyAsyncFunction,\n>(): AsyncThrottlerState<TFn> {\n return {\n errorCount: 0,\n isExecuting: false,\n isPending: false,\n lastArgs: undefined,\n lastExecutionTime: 0,\n lastResult: undefined,\n maybeExecuteCount: 0,\n nextExecutionTime: undefined,\n settleCount: 0,\n status: 'idle',\n successCount: 0,\n }\n}\n\n/**\n * Options for configuring an async throttled function\n */\nexport interface AsyncThrottlerOptions<TFn extends AnyAsyncFunction> {\n /**\n * Options for configuring the underlying async retryer\n */\n asyncRetryerOptions?: AsyncRetryerOptions<TFn>\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: AsyncThrottler<TFn>) => boolean)\n /**\n * Initial state for the async throttler\n */\n initialState?: Partial<AsyncThrottlerState<TFn>>\n /**\n * Optional key to identify this async throttler instance.\n * If provided, the async throttler will be identified by this key in the devtools and PacerProvider if applicable.\n */\n key?: string\n /**\n * Whether to execute the function immediately when called\n * Defaults to true\n */\n leading?: boolean\n /**\n * Optional error handler for when the throttled function throws.\n * If provided, the handler will be called with the error and throttler 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 args: Parameters<TFn>,\n asyncThrottler: AsyncThrottler<TFn>,\n ) => void\n /**\n * Optional function to call when the throttled function is executed\n */\n onSettled?: (\n args: Parameters<TFn>,\n asyncThrottler: AsyncThrottler<TFn>,\n ) => void\n /**\n * Optional function to call when the throttled function is executed\n */\n onSuccess?: (\n result: Awaited<ReturnType<TFn>>,\n args: Parameters<TFn>,\n asyncThrottler: AsyncThrottler<TFn>,\n ) => void\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 * Whether to execute the function on the trailing edge of the wait period\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: AsyncThrottler<TFn>) => number)\n}\n\n/**\n * Utility function for sharing common `AsyncThrottlerOptions` options between different `AsyncThrottler` instances.\n */\nexport function asyncThrottlerOptions<\n TFn extends AnyAsyncFunction = AnyAsyncFunction,\n TOptions extends Partial<AsyncThrottlerOptions<TFn>> = Partial<\n AsyncThrottlerOptions<TFn>\n >,\n>(options: TOptions): TOptions {\n return options\n}\n\ntype AsyncThrottlerOptionsWithOptionalCallbacks = OptionalKeys<\n AsyncThrottlerOptions<any>,\n 'initialState' | 'onError' | 'onSettled' | 'onSuccess'\n>\n\nconst defaultOptions: AsyncThrottlerOptionsWithOptionalCallbacks = {\n asyncRetryerOptions: {\n maxAttempts: 1,\n },\n enabled: true,\n leading: true,\n trailing: true,\n wait: 0,\n}\n\n/**\n * A class that creates an async throttled function.\n *\n * Async vs Sync Versions:\n * The async version provides advanced features over the sync Throttler:\n * - Returns promises that can be awaited for throttled function results\n * - Built-in retry support via AsyncRetryer integration\n * - Abort support to cancel in-flight executions\n * - Cancel support to prevent pending executions from starting\n * - Comprehensive error handling with onError callbacks and throwOnError control\n * - Detailed execution tracking (success/error/settle counts)\n * - Waits for ongoing executions to complete before scheduling the next one\n *\n * The sync Throttler is lighter weight and simpler when you don't need async features,\n * return values, or execution control.\n *\n * What is Throttling?\n * Throttling limits how often a function can be executed, allowing only one execution within a specified time window.\n * Unlike debouncing which resets the delay timer on each call, throttling ensures the function executes at a\n * regular interval regardless of how often it's called.\n *\n * This is useful for rate-limiting API calls, handling scroll/resize events, or any scenario where you want to\n * ensure a maximum execution frequency.\n *\n * Error Handling:\n * - If an `onError` handler is provided, it will be called with the error and throttler 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 AsyncThrottler 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 throttler\n * - Use `onSuccess` callback to react to successful function execution and implement custom logic\n * - Use `onError` callback to react to function execution errors and implement custom error handling\n * - Use `onSettled` callback to react to function execution completion (success or error) and implement custom logic\n * - The state includes error count, execution status, last execution time, and success/settle counts\n * - State can be accessed via `asyncThrottler.store.state` when using the class directly\n * - When using framework adapters (React/Solid), state is accessed from `asyncThrottler.state`\n *\n * @example\n * ```ts\n * const throttler = new AsyncThrottler(async (value: string) => {\n * const result = await saveToAPI(value);\n * return result; // Return value is preserved\n * }, {\n * wait: 1000,\n * onError: (error) => {\n * console.error('API call failed:', error);\n * }\n * });\n *\n * // Will only execute once per second no matter how often called\n * // Returns the API response directly\n * const result = await throttler.maybeExecute(inputElement.value);\n * ```\n */\nexport class AsyncThrottler<TFn extends AnyAsyncFunction> {\n readonly store: Store<Readonly<AsyncThrottlerState<TFn>>> = new Store<\n AsyncThrottlerState<TFn>\n >(getDefaultAsyncThrottlerState<TFn>())\n key: string | undefined\n options: AsyncThrottlerOptions<TFn>\n asyncRetryers = new Map<number, AsyncRetryer<TFn>>()\n #timeoutId: ReturnType<typeof setTimeout> | null = null\n #resolvePreviousPromise:\n ((value?: Awaited<ReturnType<TFn>> | undefined) => void) | null = null\n\n constructor(\n public fn: TFn,\n initialOptions: AsyncThrottlerOptions<TFn>,\n ) {\n this.key = initialOptions.key\n this.options = {\n ...defaultOptions,\n ...initialOptions,\n throwOnError: initialOptions.throwOnError ?? !initialOptions.onError,\n }\n this.#setState(this.options.initialState ?? {})\n\n if (this.key) {\n pacerEventClient.on('d-AsyncThrottler', (event) => {\n if (event.payload.key !== this.key) return\n this.#setState(\n event.payload.store.state as Partial<AsyncThrottlerState<TFn>>,\n )\n this.setOptions(\n event.payload.options as Partial<AsyncThrottlerOptions<TFn>>,\n )\n })\n }\n }\n\n /**\n * Updates the async throttler options\n */\n setOptions = (newOptions: Partial<AsyncThrottlerOptions<TFn>>): void => {\n this.options = { ...this.options, ...newOptions }\n\n // End the pending state if the throttler is disabled\n if (!this.#getEnabled()) {\n this.cancel()\n }\n }\n\n #setState = (newState: Partial<AsyncThrottlerState<TFn>>): void => {\n this.store.setState((state) => {\n const combinedState = {\n ...state,\n ...newState,\n }\n const { isPending, isExecuting, settleCount } = combinedState\n return {\n ...combinedState,\n status: !this.#getEnabled()\n ? 'disabled'\n : isPending\n ? 'pending'\n : isExecuting\n ? 'executing'\n : settleCount > 0\n ? 'settled'\n : 'idle',\n }\n })\n emitChange('AsyncThrottler', this)\n }\n\n /**\n * Returns the current enabled state of the async throttler\n */\n #getEnabled = (): boolean => {\n return !!parseFunctionOrValue(this.options.enabled, this)\n }\n\n /**\n * Returns the current wait time in milliseconds\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 AsyncThrottler(fn, { wait: 1000 });\n *\n * // First call executes immediately\n * await throttled.maybeExecute('a', 'b');\n *\n * // Call during wait period - gets throttled\n * await throttled.maybeExecute('c', 'd');\n * ```\n */\n maybeExecute = async (\n ...args: Parameters<TFn>\n ): Promise<Awaited<ReturnType<TFn>> | undefined> => {\n if (!this.#getEnabled()) return undefined\n\n this.#resolvePreviousPromiseInternal()\n\n this.#setState({\n maybeExecuteCount: this.store.state.maybeExecuteCount + 1,\n lastArgs: args, // store the arguments for potential trailing execution\n })\n\n const wait = this.#getWait()\n const thisMaybeExecuteNumber = this.store.state.maybeExecuteCount\n\n // Wait for the wait period for the previous execution to complete if it's still running\n for (\n let maxNumIterations = wait / 10;\n this.store.state.isExecuting && maxNumIterations > 0;\n maxNumIterations--\n ) {\n await new Promise((resolve) => setTimeout(resolve, 10))\n if (this.store.state.maybeExecuteCount !== thisMaybeExecuteNumber) {\n // cancel the current maybeExecute loop because a new maybeExecute call was made\n return this.store.state.lastResult\n }\n }\n\n const now = Date.now()\n const timeSinceLastExecution = now - this.store.state.lastExecutionTime\n\n if (\n this.options.leading &&\n !this.store.state.isPending &&\n timeSinceLastExecution >= wait\n ) {\n await this.#execute(...args) // Leading EXECUTE!\n } else if (this.options.trailing) {\n // replace old pending execution with a new one\n this.cancel()\n this.#setState({\n isPending: true,\n })\n\n // Set up new trailing execution\n return new Promise<Awaited<ReturnType<TFn>> | undefined>(\n (resolve, reject) => {\n this.#resolvePreviousPromise = resolve\n\n const newTimeSinceLastExecution = this.store.state.lastExecutionTime\n ? now - this.store.state.lastExecutionTime\n : 0\n const timeoutDuration = Math.max(0, wait - newTimeSinceLastExecution)\n\n this.#timeoutId = setTimeout(async () => {\n this.#clearTimeout()\n if (this.store.state.lastArgs !== undefined) {\n try {\n await this.#execute(...this.store.state.lastArgs) // Trailing EXECUTE!\n } catch (error) {\n reject(error)\n }\n }\n this.#resolvePreviousPromise = null\n resolve(this.store.state.lastResult)\n }, timeoutDuration)\n },\n )\n }\n return this.store.state.lastResult\n }\n\n #execute = async (\n ...args: Parameters<TFn>\n ): Promise<Awaited<ReturnType<TFn>> | undefined> => {\n if (!this.#getEnabled()) return undefined\n\n const currentMaybeExecute = this.store.state.maybeExecuteCount\n\n try {\n this.#setState({ isExecuting: true })\n const currentAsyncRetryer = new AsyncRetryer(\n this.fn,\n this.options.asyncRetryerOptions,\n )\n this.asyncRetryers.set(currentMaybeExecute, currentAsyncRetryer)\n const result = await currentAsyncRetryer.execute(...args) // EXECUTE!\n this.#setState({\n lastResult: result,\n successCount: this.store.state.successCount + 1,\n })\n this.options.onSuccess?.(result as Awaited<ReturnType<TFn>>, args, this)\n } catch (error) {\n this.#setState({\n errorCount: this.store.state.errorCount + 1,\n })\n this.options.onError?.(error as Error, args, this)\n if (this.options.throwOnError) {\n throw error\n }\n } finally {\n this.asyncRetryers.delete(currentMaybeExecute) // dispose retryer\n const lastExecutionTime = Date.now()\n const wait = this.#getWait()\n const nextExecutionTime = lastExecutionTime + wait\n this.#setState({\n isExecuting: false,\n isPending: !!this.#timeoutId,\n settleCount: this.store.state.settleCount + 1,\n lastExecutionTime,\n nextExecutionTime,\n })\n this.options.onSettled?.(args, this)\n setTimeout(() => {\n if (!this.store.state.isPending) {\n // clear nextExecutionTime if there is no pending execution\n this.#setState({ nextExecutionTime: undefined })\n }\n }, wait)\n }\n return this.store.state.lastResult\n }\n\n /**\n * Processes the current pending execution immediately\n */\n flush = async (): Promise<Awaited<ReturnType<TFn>> | undefined> => {\n if (this.store.state.isPending && this.store.state.lastArgs) {\n // Store the pending promise resolver before clearing timeout\n const resolvePromise = this.#resolvePreviousPromise\n\n // Clear timeout and state without resolving the promise\n this.#clearTimeout()\n this.#setState({\n isPending: false,\n })\n\n const result = await this.#execute(...this.store.state.lastArgs)\n\n // Resolve the pending promise with the result\n if (resolvePromise) {\n resolvePromise(result)\n }\n\n return result\n }\n return undefined\n }\n\n #resolvePreviousPromiseInternal = (): void => {\n if (this.#resolvePreviousPromise) {\n this.#resolvePreviousPromise(this.store.state.lastResult)\n this.#resolvePreviousPromise = null\n }\n }\n\n #clearTimeout = (): void => {\n if (this.#timeoutId) {\n clearTimeout(this.#timeoutId)\n this.#timeoutId = null\n }\n }\n\n /**\n * Returns the AbortSignal for a specific execution.\n * If no maybeExecuteCount 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 maybeExecuteCount - Optional specific execution to get signal for\n * @example\n * ```typescript\n * const throttler = new AsyncThrottler(\n * async (data: string) => {\n * const signal = throttler.getAbortSignal()\n * if (signal) {\n * const response = await fetch('/api/save', {\n * method: 'POST',\n * body: data,\n * signal\n * })\n * return response.json()\n * }\n * },\n * { wait: 1000 }\n * )\n * ```\n */\n getAbortSignal = (maybeExecuteCount?: number): AbortSignal | null => {\n const count = maybeExecuteCount ?? this.store.state.maybeExecuteCount\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 */\n abort = (): void => {\n this.asyncRetryers.forEach((retryer) => retryer.abort())\n this.asyncRetryers.clear()\n this.#setState({ isExecuting: false })\n }\n\n /**\n * Cancels any pending execution that have not started yet.\n * Does NOT abort any execution already in progress.\n */\n cancel = (): void => {\n this.#clearTimeout()\n if (this.#resolvePreviousPromise) {\n this.#resolvePreviousPromiseInternal()\n this.#resolvePreviousPromise = null\n }\n this.#setState({\n isPending: false,\n })\n }\n\n /**\n * Resets the debouncer state to its default values\n */\n reset = (): void => {\n this.#setState(getDefaultAsyncThrottlerState<TFn>())\n this.asyncRetryers.forEach((retryer) => retryer.reset())\n }\n}\n\n/**\n * Creates an async throttled function that limits how often the function can execute.\n * The throttled function will execute at most once per wait period, even if called multiple times.\n * If called while executing, it will wait until execution completes before scheduling the next call.\n *\n * Async vs Sync Versions:\n * The async version provides advanced features over the sync throttle function:\n * - Returns promises that can be awaited for throttled function results\n * - Built-in retry support via AsyncRetryer integration\n * - Abort support to cancel in-flight executions\n * - Cancel support to prevent pending executions from starting\n * - Comprehensive error handling with onError callbacks and throwOnError control\n * - Detailed execution tracking (success/error/settle counts)\n * - Waits for ongoing executions to complete before scheduling the next one\n *\n * The sync throttle function is lighter weight and simpler when you don't need async features,\n * return values, or execution control.\n *\n * What is Throttling?\n * Throttling limits how often a function can be executed, allowing only one execution within a specified time window.\n * Unlike debouncing which resets the delay timer on each call, throttling ensures the function executes at a\n * regular interval regardless of how often it's called.\n *\n * Configuration Options:\n * - `wait`: Time window in milliseconds during which the function can only execute once (required)\n * - `leading`: Execute immediately when called (default: true)\n * - `trailing`: Execute on the trailing edge of the wait period (default: true)\n * - `enabled`: Whether the throttler is enabled (default: true)\n * - `asyncRetryerOptions`: Configure retry behavior for executions\n *\n * Error Handling:\n * - If an `onError` handler is provided, it will be called with the error and throttler 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 AsyncThrottler 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 throttler\n * - Use `onSuccess` callback to react to successful function execution and implement custom logic\n * - Use `onError` callback to react to function execution errors and implement custom error handling\n * - Use `onSettled` callback to react to function execution completion (success or error) and implement custom logic\n * - The state includes error count, execution status, last execution time, and success/settle counts\n * - State can be accessed via the underlying AsyncThrottler 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 throttled = asyncThrottle(async (value: string) => {\n * const result = await saveToAPI(value);\n * return result; // Return value is preserved\n * }, {\n * wait: 1000,\n * onError: (error) => {\n * console.error('API call failed:', error);\n * }\n * });\n *\n * // This will execute at most once per second\n * // Returns the API response directly\n * const result = await throttled(inputElement.value);\n * ```\n */\nexport function asyncThrottle<TFn extends AnyAsyncFunction>(\n fn: TFn,\n initialOptions: AsyncThrottlerOptions<TFn>,\n) {\n const asyncThrottler = new AsyncThrottler(fn, initialOptions)\n return asyncThrottler.maybeExecute\n}\n"],"mappings":"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"}
@@ -1,7 +1,6 @@
1
1
  import { AnyAsyncFunction } from "./types.cjs";
2
2
  import { AsyncRetryer, AsyncRetryerOptions } from "./async-retryer.cjs";
3
3
  import { Store } from "@tanstack/store";
4
-
5
4
  //#region src/async-throttler.d.ts
6
5
  interface AsyncThrottlerState<TFn extends AnyAsyncFunction> {
7
6
  /**
@@ -27,7 +26,7 @@ interface AsyncThrottlerState<TFn extends AnyAsyncFunction> {
27
26
  /**
28
27
  * The result from the most recent successful function execution
29
28
  */
30
- lastResult: ReturnType<TFn> | undefined;
29
+ lastResult: Awaited<ReturnType<TFn>> | undefined;
31
30
  /**
32
31
  * Number of times maybeExecute has been called (for reduction calculations)
33
32
  */
@@ -90,7 +89,7 @@ interface AsyncThrottlerOptions<TFn extends AnyAsyncFunction> {
90
89
  /**
91
90
  * Optional function to call when the throttled function is executed
92
91
  */
93
- onSuccess?: (result: ReturnType<TFn>, args: Parameters<TFn>, asyncThrottler: AsyncThrottler<TFn>) => void;
92
+ onSuccess?: (result: Awaited<ReturnType<TFn>>, args: Parameters<TFn>, asyncThrottler: AsyncThrottler<TFn>) => void;
94
93
  /**
95
94
  * Whether to throw errors when they occur.
96
95
  * Defaults to true if no onError handler is provided, false if an onError handler is provided.
@@ -205,11 +204,11 @@ declare class AsyncThrottler<TFn extends AnyAsyncFunction> {
205
204
  * await throttled.maybeExecute('c', 'd');
206
205
  * ```
207
206
  */
208
- maybeExecute: (...args: Parameters<TFn>) => Promise<ReturnType<TFn> | undefined>;
207
+ maybeExecute: (...args: Parameters<TFn>) => Promise<Awaited<ReturnType<TFn>> | undefined>;
209
208
  /**
210
209
  * Processes the current pending execution immediately
211
210
  */
212
- flush: () => Promise<ReturnType<TFn> | undefined>;
211
+ flush: () => Promise<Awaited<ReturnType<TFn>> | undefined>;
213
212
  /**
214
213
  * Returns the AbortSignal for a specific execution.
215
214
  * If no maybeExecuteCount is provided, returns the signal for the most recent execution.
@@ -314,7 +313,7 @@ declare class AsyncThrottler<TFn extends AnyAsyncFunction> {
314
313
  * const result = await throttled(inputElement.value);
315
314
  * ```
316
315
  */
317
- declare function asyncThrottle<TFn extends AnyAsyncFunction>(fn: TFn, initialOptions: AsyncThrottlerOptions<TFn>): (...args: Parameters<TFn>) => Promise<ReturnType<TFn> | undefined>;
316
+ declare function asyncThrottle<TFn extends AnyAsyncFunction>(fn: TFn, initialOptions: AsyncThrottlerOptions<TFn>): (...args: Parameters<TFn>) => Promise<Awaited<ReturnType<TFn>> | undefined>;
318
317
  //#endregion
319
318
  export { AsyncThrottler, AsyncThrottlerOptions, AsyncThrottlerState, asyncThrottle, asyncThrottlerOptions };
320
319
  //# sourceMappingURL=async-throttler.d.cts.map
@@ -1,7 +1,6 @@
1
1
  import { AnyAsyncFunction } from "./types.js";
2
2
  import { AsyncRetryer, AsyncRetryerOptions } from "./async-retryer.js";
3
3
  import { Store } from "@tanstack/store";
4
-
5
4
  //#region src/async-throttler.d.ts
6
5
  interface AsyncThrottlerState<TFn extends AnyAsyncFunction> {
7
6
  /**
@@ -27,7 +26,7 @@ interface AsyncThrottlerState<TFn extends AnyAsyncFunction> {
27
26
  /**
28
27
  * The result from the most recent successful function execution
29
28
  */
30
- lastResult: ReturnType<TFn> | undefined;
29
+ lastResult: Awaited<ReturnType<TFn>> | undefined;
31
30
  /**
32
31
  * Number of times maybeExecute has been called (for reduction calculations)
33
32
  */
@@ -90,7 +89,7 @@ interface AsyncThrottlerOptions<TFn extends AnyAsyncFunction> {
90
89
  /**
91
90
  * Optional function to call when the throttled function is executed
92
91
  */
93
- onSuccess?: (result: ReturnType<TFn>, args: Parameters<TFn>, asyncThrottler: AsyncThrottler<TFn>) => void;
92
+ onSuccess?: (result: Awaited<ReturnType<TFn>>, args: Parameters<TFn>, asyncThrottler: AsyncThrottler<TFn>) => void;
94
93
  /**
95
94
  * Whether to throw errors when they occur.
96
95
  * Defaults to true if no onError handler is provided, false if an onError handler is provided.
@@ -205,11 +204,11 @@ declare class AsyncThrottler<TFn extends AnyAsyncFunction> {
205
204
  * await throttled.maybeExecute('c', 'd');
206
205
  * ```
207
206
  */
208
- maybeExecute: (...args: Parameters<TFn>) => Promise<ReturnType<TFn> | undefined>;
207
+ maybeExecute: (...args: Parameters<TFn>) => Promise<Awaited<ReturnType<TFn>> | undefined>;
209
208
  /**
210
209
  * Processes the current pending execution immediately
211
210
  */
212
- flush: () => Promise<ReturnType<TFn> | undefined>;
211
+ flush: () => Promise<Awaited<ReturnType<TFn>> | undefined>;
213
212
  /**
214
213
  * Returns the AbortSignal for a specific execution.
215
214
  * If no maybeExecuteCount is provided, returns the signal for the most recent execution.
@@ -314,7 +313,7 @@ declare class AsyncThrottler<TFn extends AnyAsyncFunction> {
314
313
  * const result = await throttled(inputElement.value);
315
314
  * ```
316
315
  */
317
- declare function asyncThrottle<TFn extends AnyAsyncFunction>(fn: TFn, initialOptions: AsyncThrottlerOptions<TFn>): (...args: Parameters<TFn>) => Promise<ReturnType<TFn> | undefined>;
316
+ declare function asyncThrottle<TFn extends AnyAsyncFunction>(fn: TFn, initialOptions: AsyncThrottlerOptions<TFn>): (...args: Parameters<TFn>) => Promise<Awaited<ReturnType<TFn>> | undefined>;
318
317
  //#endregion
319
318
  export { AsyncThrottler, AsyncThrottlerOptions, AsyncThrottlerState, asyncThrottle, asyncThrottlerOptions };
320
319
  //# sourceMappingURL=async-throttler.d.ts.map
@@ -1,6 +1,6 @@
1
1
  import { parseFunctionOrValue } from "./utils.js";
2
- import { emitChange, pacerEventClient } from "./event-client.js";
3
2
  import { AsyncRetryer } from "./async-retryer.js";
3
+ import { emitChange, pacerEventClient } from "./event-client.js";
4
4
  import { Store } from "@tanstack/store";
5
5
 
6
6
  //#region src/async-throttler.ts
@@ -168,10 +168,7 @@ var AsyncThrottler = class {
168
168
  const currentMaybeExecute = this.store.state.maybeExecuteCount;
169
169
  try {
170
170
  this.#setState({ isExecuting: true });
171
- const currentAsyncRetryer = new AsyncRetryer(this.fn, {
172
- ...this.options.asyncRetryerOptions,
173
- key: `${this.key}-retryer-${currentMaybeExecute}`
174
- });
171
+ const currentAsyncRetryer = new AsyncRetryer(this.fn, this.options.asyncRetryerOptions);
175
172
  this.asyncRetryers.set(currentMaybeExecute, currentAsyncRetryer);
176
173
  const result = await currentAsyncRetryer.execute(...args);
177
174
  this.#setState({
@@ -1 +1 @@
1
- {"version":3,"file":"async-throttler.js","names":["#getEnabled","#resolvePreviousPromiseInternal","#setState","#getWait","#execute","#resolvePreviousPromise","#timeoutId","#clearTimeout"],"sources":["../src/async-throttler.ts"],"sourcesContent":["import { Store } from '@tanstack/store'\nimport { AsyncRetryer } from './async-retryer'\nimport { parseFunctionOrValue } from './utils'\nimport { emitChange, pacerEventClient } from './event-client'\nimport type { AsyncRetryerOptions } from './async-retryer'\nimport type { AnyAsyncFunction, OptionalKeys } from './types'\n\nexport interface AsyncThrottlerState<TFn extends AnyAsyncFunction> {\n /**\n * Number of function executions that have resulted in errors\n */\n errorCount: number\n /**\n * Whether the throttled function is currently executing asynchronously\n */\n isExecuting: boolean\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 * The result from the most recent successful function execution\n */\n lastResult: ReturnType<TFn> | undefined\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 * Number of function executions that have completed (either successfully or with errors)\n */\n settleCount: number\n /**\n * Current execution status - 'idle' when not active, 'pending' when waiting, 'executing' when running, 'settled' when completed\n */\n status: 'disabled' | 'idle' | 'pending' | 'executing' | 'settled'\n /**\n * Number of function executions that have completed successfully\n */\n successCount: number\n}\n\nfunction getDefaultAsyncThrottlerState<\n TFn extends AnyAsyncFunction,\n>(): AsyncThrottlerState<TFn> {\n return {\n errorCount: 0,\n isExecuting: false,\n isPending: false,\n lastArgs: undefined,\n lastExecutionTime: 0,\n lastResult: undefined,\n maybeExecuteCount: 0,\n nextExecutionTime: undefined,\n settleCount: 0,\n status: 'idle',\n successCount: 0,\n }\n}\n\n/**\n * Options for configuring an async throttled function\n */\nexport interface AsyncThrottlerOptions<TFn extends AnyAsyncFunction> {\n /**\n * Options for configuring the underlying async retryer\n */\n asyncRetryerOptions?: AsyncRetryerOptions<TFn>\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: AsyncThrottler<TFn>) => boolean)\n /**\n * Initial state for the async throttler\n */\n initialState?: Partial<AsyncThrottlerState<TFn>>\n /**\n * Optional key to identify this async throttler instance.\n * If provided, the async throttler will be identified by this key in the devtools and PacerProvider if applicable.\n */\n key?: string\n /**\n * Whether to execute the function immediately when called\n * Defaults to true\n */\n leading?: boolean\n /**\n * Optional error handler for when the throttled function throws.\n * If provided, the handler will be called with the error and throttler 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 args: Parameters<TFn>,\n asyncThrottler: AsyncThrottler<TFn>,\n ) => void\n /**\n * Optional function to call when the throttled function is executed\n */\n onSettled?: (\n args: Parameters<TFn>,\n asyncThrottler: AsyncThrottler<TFn>,\n ) => void\n /**\n * Optional function to call when the throttled function is executed\n */\n onSuccess?: (\n result: ReturnType<TFn>,\n args: Parameters<TFn>,\n asyncThrottler: AsyncThrottler<TFn>,\n ) => void\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 * Whether to execute the function on the trailing edge of the wait period\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: AsyncThrottler<TFn>) => number)\n}\n\n/**\n * Utility function for sharing common `AsyncThrottlerOptions` options between different `AsyncThrottler` instances.\n */\nexport function asyncThrottlerOptions<\n TFn extends AnyAsyncFunction = AnyAsyncFunction,\n TOptions extends Partial<AsyncThrottlerOptions<TFn>> = Partial<\n AsyncThrottlerOptions<TFn>\n >,\n>(options: TOptions): TOptions {\n return options\n}\n\ntype AsyncThrottlerOptionsWithOptionalCallbacks = OptionalKeys<\n AsyncThrottlerOptions<any>,\n 'initialState' | 'onError' | 'onSettled' | 'onSuccess'\n>\n\nconst defaultOptions: AsyncThrottlerOptionsWithOptionalCallbacks = {\n asyncRetryerOptions: {\n maxAttempts: 1,\n },\n enabled: true,\n leading: true,\n trailing: true,\n wait: 0,\n}\n\n/**\n * A class that creates an async throttled function.\n *\n * Async vs Sync Versions:\n * The async version provides advanced features over the sync Throttler:\n * - Returns promises that can be awaited for throttled function results\n * - Built-in retry support via AsyncRetryer integration\n * - Abort support to cancel in-flight executions\n * - Cancel support to prevent pending executions from starting\n * - Comprehensive error handling with onError callbacks and throwOnError control\n * - Detailed execution tracking (success/error/settle counts)\n * - Waits for ongoing executions to complete before scheduling the next one\n *\n * The sync Throttler is lighter weight and simpler when you don't need async features,\n * return values, or execution control.\n *\n * What is Throttling?\n * Throttling limits how often a function can be executed, allowing only one execution within a specified time window.\n * Unlike debouncing which resets the delay timer on each call, throttling ensures the function executes at a\n * regular interval regardless of how often it's called.\n *\n * This is useful for rate-limiting API calls, handling scroll/resize events, or any scenario where you want to\n * ensure a maximum execution frequency.\n *\n * Error Handling:\n * - If an `onError` handler is provided, it will be called with the error and throttler 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 AsyncThrottler 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 throttler\n * - Use `onSuccess` callback to react to successful function execution and implement custom logic\n * - Use `onError` callback to react to function execution errors and implement custom error handling\n * - Use `onSettled` callback to react to function execution completion (success or error) and implement custom logic\n * - The state includes error count, execution status, last execution time, and success/settle counts\n * - State can be accessed via `asyncThrottler.store.state` when using the class directly\n * - When using framework adapters (React/Solid), state is accessed from `asyncThrottler.state`\n *\n * @example\n * ```ts\n * const throttler = new AsyncThrottler(async (value: string) => {\n * const result = await saveToAPI(value);\n * return result; // Return value is preserved\n * }, {\n * wait: 1000,\n * onError: (error) => {\n * console.error('API call failed:', error);\n * }\n * });\n *\n * // Will only execute once per second no matter how often called\n * // Returns the API response directly\n * const result = await throttler.maybeExecute(inputElement.value);\n * ```\n */\nexport class AsyncThrottler<TFn extends AnyAsyncFunction> {\n readonly store: Store<Readonly<AsyncThrottlerState<TFn>>> = new Store<\n AsyncThrottlerState<TFn>\n >(getDefaultAsyncThrottlerState<TFn>())\n key: string | undefined\n options: AsyncThrottlerOptions<TFn>\n asyncRetryers = new Map<number, AsyncRetryer<TFn>>()\n #timeoutId: ReturnType<typeof setTimeout> | null = null\n #resolvePreviousPromise:\n | ((value?: ReturnType<TFn> | undefined) => void)\n | null = null\n\n constructor(\n public fn: TFn,\n initialOptions: AsyncThrottlerOptions<TFn>,\n ) {\n this.key = initialOptions.key\n this.options = {\n ...defaultOptions,\n ...initialOptions,\n throwOnError: initialOptions.throwOnError ?? !initialOptions.onError,\n }\n this.#setState(this.options.initialState ?? {})\n\n if (this.key) {\n pacerEventClient.on('d-AsyncThrottler', (event) => {\n if (event.payload.key !== this.key) return\n this.#setState(\n event.payload.store.state as Partial<AsyncThrottlerState<TFn>>,\n )\n this.setOptions(\n event.payload.options as Partial<AsyncThrottlerOptions<TFn>>,\n )\n })\n }\n }\n\n /**\n * Updates the async throttler options\n */\n setOptions = (newOptions: Partial<AsyncThrottlerOptions<TFn>>): void => {\n this.options = { ...this.options, ...newOptions }\n\n // End the pending state if the throttler is disabled\n if (!this.#getEnabled()) {\n this.cancel()\n }\n }\n\n #setState = (newState: Partial<AsyncThrottlerState<TFn>>): void => {\n this.store.setState((state) => {\n const combinedState = {\n ...state,\n ...newState,\n }\n const { isPending, isExecuting, settleCount } = combinedState\n return {\n ...combinedState,\n status: !this.#getEnabled()\n ? 'disabled'\n : isPending\n ? 'pending'\n : isExecuting\n ? 'executing'\n : settleCount > 0\n ? 'settled'\n : 'idle',\n }\n })\n emitChange('AsyncThrottler', this)\n }\n\n /**\n * Returns the current enabled state of the async throttler\n */\n #getEnabled = (): boolean => {\n return !!parseFunctionOrValue(this.options.enabled, this)\n }\n\n /**\n * Returns the current wait time in milliseconds\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 AsyncThrottler(fn, { wait: 1000 });\n *\n * // First call executes immediately\n * await throttled.maybeExecute('a', 'b');\n *\n * // Call during wait period - gets throttled\n * await throttled.maybeExecute('c', 'd');\n * ```\n */\n maybeExecute = async (\n ...args: Parameters<TFn>\n ): Promise<ReturnType<TFn> | undefined> => {\n if (!this.#getEnabled()) return undefined\n\n this.#resolvePreviousPromiseInternal()\n\n this.#setState({\n maybeExecuteCount: this.store.state.maybeExecuteCount + 1,\n lastArgs: args, // store the arguments for potential trailing execution\n })\n\n const wait = this.#getWait()\n const thisMaybeExecuteNumber = this.store.state.maybeExecuteCount\n\n // Wait for the wait period for the previous execution to complete if it's still running\n for (\n let maxNumIterations = wait / 10;\n this.store.state.isExecuting && maxNumIterations > 0;\n maxNumIterations--\n ) {\n await new Promise((resolve) => setTimeout(resolve, 10))\n if (this.store.state.maybeExecuteCount !== thisMaybeExecuteNumber) {\n // cancel the current maybeExecute loop because a new maybeExecute call was made\n return this.store.state.lastResult\n }\n }\n\n const now = Date.now()\n const timeSinceLastExecution = now - this.store.state.lastExecutionTime\n\n if (\n this.options.leading &&\n !this.store.state.isPending &&\n timeSinceLastExecution >= wait\n ) {\n await this.#execute(...args) // Leading EXECUTE!\n } else if (this.options.trailing) {\n // replace old pending execution with a new one\n this.cancel()\n this.#setState({\n isPending: true,\n })\n\n // Set up new trailing execution\n return new Promise((resolve, reject) => {\n this.#resolvePreviousPromise = resolve\n\n const newTimeSinceLastExecution = this.store.state.lastExecutionTime\n ? now - this.store.state.lastExecutionTime\n : 0\n const timeoutDuration = Math.max(0, wait - newTimeSinceLastExecution)\n\n this.#timeoutId = setTimeout(async () => {\n this.#clearTimeout()\n if (this.store.state.lastArgs !== undefined) {\n try {\n await this.#execute(...this.store.state.lastArgs) // Trailing EXECUTE!\n } catch (error) {\n reject(error)\n }\n }\n this.#resolvePreviousPromise = null\n resolve(this.store.state.lastResult)\n }, timeoutDuration)\n })\n }\n return this.store.state.lastResult\n }\n\n #execute = async (\n ...args: Parameters<TFn>\n ): Promise<ReturnType<TFn> | undefined> => {\n if (!this.#getEnabled()) return undefined\n\n const currentMaybeExecute = this.store.state.maybeExecuteCount\n\n try {\n this.#setState({ isExecuting: true })\n const currentAsyncRetryer = new AsyncRetryer(this.fn, {\n ...this.options.asyncRetryerOptions,\n key: `${this.key}-retryer-${currentMaybeExecute}`,\n })\n this.asyncRetryers.set(currentMaybeExecute, currentAsyncRetryer)\n const result = await currentAsyncRetryer.execute(...args) // EXECUTE!\n this.#setState({\n lastResult: result,\n successCount: this.store.state.successCount + 1,\n })\n this.options.onSuccess?.(result as ReturnType<TFn>, args, this)\n } catch (error) {\n this.#setState({\n errorCount: this.store.state.errorCount + 1,\n })\n this.options.onError?.(error as Error, args, this)\n if (this.options.throwOnError) {\n throw error\n }\n } finally {\n this.asyncRetryers.delete(currentMaybeExecute) // dispose retryer\n const lastExecutionTime = Date.now()\n const wait = this.#getWait()\n const nextExecutionTime = lastExecutionTime + wait\n this.#setState({\n isExecuting: false,\n isPending: !!this.#timeoutId,\n settleCount: this.store.state.settleCount + 1,\n lastExecutionTime,\n nextExecutionTime,\n })\n this.options.onSettled?.(args, this)\n setTimeout(() => {\n if (!this.store.state.isPending) {\n // clear nextExecutionTime if there is no pending execution\n this.#setState({ nextExecutionTime: undefined })\n }\n }, wait)\n }\n return this.store.state.lastResult\n }\n\n /**\n * Processes the current pending execution immediately\n */\n flush = async (): Promise<ReturnType<TFn> | undefined> => {\n if (this.store.state.isPending && this.store.state.lastArgs) {\n // Store the pending promise resolver before clearing timeout\n const resolvePromise = this.#resolvePreviousPromise\n\n // Clear timeout and state without resolving the promise\n this.#clearTimeout()\n this.#setState({\n isPending: false,\n })\n\n const result = await this.#execute(...this.store.state.lastArgs)\n\n // Resolve the pending promise with the result\n if (resolvePromise) {\n resolvePromise(result)\n }\n\n return result\n }\n return undefined\n }\n\n #resolvePreviousPromiseInternal = (): void => {\n if (this.#resolvePreviousPromise) {\n this.#resolvePreviousPromise(this.store.state.lastResult)\n this.#resolvePreviousPromise = null\n }\n }\n\n #clearTimeout = (): void => {\n if (this.#timeoutId) {\n clearTimeout(this.#timeoutId)\n this.#timeoutId = null\n }\n }\n\n /**\n * Returns the AbortSignal for a specific execution.\n * If no maybeExecuteCount 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 maybeExecuteCount - Optional specific execution to get signal for\n * @example\n * ```typescript\n * const throttler = new AsyncThrottler(\n * async (data: string) => {\n * const signal = throttler.getAbortSignal()\n * if (signal) {\n * const response = await fetch('/api/save', {\n * method: 'POST',\n * body: data,\n * signal\n * })\n * return response.json()\n * }\n * },\n * { wait: 1000 }\n * )\n * ```\n */\n getAbortSignal = (maybeExecuteCount?: number): AbortSignal | null => {\n const count = maybeExecuteCount ?? this.store.state.maybeExecuteCount\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 */\n abort = (): void => {\n this.asyncRetryers.forEach((retryer) => retryer.abort())\n this.asyncRetryers.clear()\n this.#setState({ isExecuting: false })\n }\n\n /**\n * Cancels any pending execution that have not started yet.\n * Does NOT abort any execution already in progress.\n */\n cancel = (): void => {\n this.#clearTimeout()\n if (this.#resolvePreviousPromise) {\n this.#resolvePreviousPromiseInternal()\n this.#resolvePreviousPromise = null\n }\n this.#setState({\n isPending: false,\n })\n }\n\n /**\n * Resets the debouncer state to its default values\n */\n reset = (): void => {\n this.#setState(getDefaultAsyncThrottlerState<TFn>())\n this.asyncRetryers.forEach((retryer) => retryer.reset())\n }\n}\n\n/**\n * Creates an async throttled function that limits how often the function can execute.\n * The throttled function will execute at most once per wait period, even if called multiple times.\n * If called while executing, it will wait until execution completes before scheduling the next call.\n *\n * Async vs Sync Versions:\n * The async version provides advanced features over the sync throttle function:\n * - Returns promises that can be awaited for throttled function results\n * - Built-in retry support via AsyncRetryer integration\n * - Abort support to cancel in-flight executions\n * - Cancel support to prevent pending executions from starting\n * - Comprehensive error handling with onError callbacks and throwOnError control\n * - Detailed execution tracking (success/error/settle counts)\n * - Waits for ongoing executions to complete before scheduling the next one\n *\n * The sync throttle function is lighter weight and simpler when you don't need async features,\n * return values, or execution control.\n *\n * What is Throttling?\n * Throttling limits how often a function can be executed, allowing only one execution within a specified time window.\n * Unlike debouncing which resets the delay timer on each call, throttling ensures the function executes at a\n * regular interval regardless of how often it's called.\n *\n * Configuration Options:\n * - `wait`: Time window in milliseconds during which the function can only execute once (required)\n * - `leading`: Execute immediately when called (default: true)\n * - `trailing`: Execute on the trailing edge of the wait period (default: true)\n * - `enabled`: Whether the throttler is enabled (default: true)\n * - `asyncRetryerOptions`: Configure retry behavior for executions\n *\n * Error Handling:\n * - If an `onError` handler is provided, it will be called with the error and throttler 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 AsyncThrottler 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 throttler\n * - Use `onSuccess` callback to react to successful function execution and implement custom logic\n * - Use `onError` callback to react to function execution errors and implement custom error handling\n * - Use `onSettled` callback to react to function execution completion (success or error) and implement custom logic\n * - The state includes error count, execution status, last execution time, and success/settle counts\n * - State can be accessed via the underlying AsyncThrottler 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 throttled = asyncThrottle(async (value: string) => {\n * const result = await saveToAPI(value);\n * return result; // Return value is preserved\n * }, {\n * wait: 1000,\n * onError: (error) => {\n * console.error('API call failed:', error);\n * }\n * });\n *\n * // This will execute at most once per second\n * // Returns the API response directly\n * const result = await throttled(inputElement.value);\n * ```\n */\nexport function asyncThrottle<TFn extends AnyAsyncFunction>(\n fn: TFn,\n initialOptions: AsyncThrottlerOptions<TFn>,\n) {\n const asyncThrottler = new AsyncThrottler(fn, initialOptions)\n return asyncThrottler.maybeExecute\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"async-throttler.js","names":["#getEnabled","#resolvePreviousPromiseInternal","#setState","#getWait","#execute","#resolvePreviousPromise","#timeoutId","#clearTimeout"],"sources":["../src/async-throttler.ts"],"sourcesContent":["import { Store } from '@tanstack/store'\nimport { AsyncRetryer } from './async-retryer'\nimport { parseFunctionOrValue } from './utils'\nimport { emitChange, pacerEventClient } from './event-client'\nimport type { AsyncRetryerOptions } from './async-retryer'\nimport type { AnyAsyncFunction, OptionalKeys } from './types'\n\nexport interface AsyncThrottlerState<TFn extends AnyAsyncFunction> {\n /**\n * Number of function executions that have resulted in errors\n */\n errorCount: number\n /**\n * Whether the throttled function is currently executing asynchronously\n */\n isExecuting: boolean\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 * The result from the most recent successful function execution\n */\n lastResult: Awaited<ReturnType<TFn>> | undefined\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 * Number of function executions that have completed (either successfully or with errors)\n */\n settleCount: number\n /**\n * Current execution status - 'idle' when not active, 'pending' when waiting, 'executing' when running, 'settled' when completed\n */\n status: 'disabled' | 'idle' | 'pending' | 'executing' | 'settled'\n /**\n * Number of function executions that have completed successfully\n */\n successCount: number\n}\n\nfunction getDefaultAsyncThrottlerState<\n TFn extends AnyAsyncFunction,\n>(): AsyncThrottlerState<TFn> {\n return {\n errorCount: 0,\n isExecuting: false,\n isPending: false,\n lastArgs: undefined,\n lastExecutionTime: 0,\n lastResult: undefined,\n maybeExecuteCount: 0,\n nextExecutionTime: undefined,\n settleCount: 0,\n status: 'idle',\n successCount: 0,\n }\n}\n\n/**\n * Options for configuring an async throttled function\n */\nexport interface AsyncThrottlerOptions<TFn extends AnyAsyncFunction> {\n /**\n * Options for configuring the underlying async retryer\n */\n asyncRetryerOptions?: AsyncRetryerOptions<TFn>\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: AsyncThrottler<TFn>) => boolean)\n /**\n * Initial state for the async throttler\n */\n initialState?: Partial<AsyncThrottlerState<TFn>>\n /**\n * Optional key to identify this async throttler instance.\n * If provided, the async throttler will be identified by this key in the devtools and PacerProvider if applicable.\n */\n key?: string\n /**\n * Whether to execute the function immediately when called\n * Defaults to true\n */\n leading?: boolean\n /**\n * Optional error handler for when the throttled function throws.\n * If provided, the handler will be called with the error and throttler 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 args: Parameters<TFn>,\n asyncThrottler: AsyncThrottler<TFn>,\n ) => void\n /**\n * Optional function to call when the throttled function is executed\n */\n onSettled?: (\n args: Parameters<TFn>,\n asyncThrottler: AsyncThrottler<TFn>,\n ) => void\n /**\n * Optional function to call when the throttled function is executed\n */\n onSuccess?: (\n result: Awaited<ReturnType<TFn>>,\n args: Parameters<TFn>,\n asyncThrottler: AsyncThrottler<TFn>,\n ) => void\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 * Whether to execute the function on the trailing edge of the wait period\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: AsyncThrottler<TFn>) => number)\n}\n\n/**\n * Utility function for sharing common `AsyncThrottlerOptions` options between different `AsyncThrottler` instances.\n */\nexport function asyncThrottlerOptions<\n TFn extends AnyAsyncFunction = AnyAsyncFunction,\n TOptions extends Partial<AsyncThrottlerOptions<TFn>> = Partial<\n AsyncThrottlerOptions<TFn>\n >,\n>(options: TOptions): TOptions {\n return options\n}\n\ntype AsyncThrottlerOptionsWithOptionalCallbacks = OptionalKeys<\n AsyncThrottlerOptions<any>,\n 'initialState' | 'onError' | 'onSettled' | 'onSuccess'\n>\n\nconst defaultOptions: AsyncThrottlerOptionsWithOptionalCallbacks = {\n asyncRetryerOptions: {\n maxAttempts: 1,\n },\n enabled: true,\n leading: true,\n trailing: true,\n wait: 0,\n}\n\n/**\n * A class that creates an async throttled function.\n *\n * Async vs Sync Versions:\n * The async version provides advanced features over the sync Throttler:\n * - Returns promises that can be awaited for throttled function results\n * - Built-in retry support via AsyncRetryer integration\n * - Abort support to cancel in-flight executions\n * - Cancel support to prevent pending executions from starting\n * - Comprehensive error handling with onError callbacks and throwOnError control\n * - Detailed execution tracking (success/error/settle counts)\n * - Waits for ongoing executions to complete before scheduling the next one\n *\n * The sync Throttler is lighter weight and simpler when you don't need async features,\n * return values, or execution control.\n *\n * What is Throttling?\n * Throttling limits how often a function can be executed, allowing only one execution within a specified time window.\n * Unlike debouncing which resets the delay timer on each call, throttling ensures the function executes at a\n * regular interval regardless of how often it's called.\n *\n * This is useful for rate-limiting API calls, handling scroll/resize events, or any scenario where you want to\n * ensure a maximum execution frequency.\n *\n * Error Handling:\n * - If an `onError` handler is provided, it will be called with the error and throttler 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 AsyncThrottler 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 throttler\n * - Use `onSuccess` callback to react to successful function execution and implement custom logic\n * - Use `onError` callback to react to function execution errors and implement custom error handling\n * - Use `onSettled` callback to react to function execution completion (success or error) and implement custom logic\n * - The state includes error count, execution status, last execution time, and success/settle counts\n * - State can be accessed via `asyncThrottler.store.state` when using the class directly\n * - When using framework adapters (React/Solid), state is accessed from `asyncThrottler.state`\n *\n * @example\n * ```ts\n * const throttler = new AsyncThrottler(async (value: string) => {\n * const result = await saveToAPI(value);\n * return result; // Return value is preserved\n * }, {\n * wait: 1000,\n * onError: (error) => {\n * console.error('API call failed:', error);\n * }\n * });\n *\n * // Will only execute once per second no matter how often called\n * // Returns the API response directly\n * const result = await throttler.maybeExecute(inputElement.value);\n * ```\n */\nexport class AsyncThrottler<TFn extends AnyAsyncFunction> {\n readonly store: Store<Readonly<AsyncThrottlerState<TFn>>> = new Store<\n AsyncThrottlerState<TFn>\n >(getDefaultAsyncThrottlerState<TFn>())\n key: string | undefined\n options: AsyncThrottlerOptions<TFn>\n asyncRetryers = new Map<number, AsyncRetryer<TFn>>()\n #timeoutId: ReturnType<typeof setTimeout> | null = null\n #resolvePreviousPromise:\n ((value?: Awaited<ReturnType<TFn>> | undefined) => void) | null = null\n\n constructor(\n public fn: TFn,\n initialOptions: AsyncThrottlerOptions<TFn>,\n ) {\n this.key = initialOptions.key\n this.options = {\n ...defaultOptions,\n ...initialOptions,\n throwOnError: initialOptions.throwOnError ?? !initialOptions.onError,\n }\n this.#setState(this.options.initialState ?? {})\n\n if (this.key) {\n pacerEventClient.on('d-AsyncThrottler', (event) => {\n if (event.payload.key !== this.key) return\n this.#setState(\n event.payload.store.state as Partial<AsyncThrottlerState<TFn>>,\n )\n this.setOptions(\n event.payload.options as Partial<AsyncThrottlerOptions<TFn>>,\n )\n })\n }\n }\n\n /**\n * Updates the async throttler options\n */\n setOptions = (newOptions: Partial<AsyncThrottlerOptions<TFn>>): void => {\n this.options = { ...this.options, ...newOptions }\n\n // End the pending state if the throttler is disabled\n if (!this.#getEnabled()) {\n this.cancel()\n }\n }\n\n #setState = (newState: Partial<AsyncThrottlerState<TFn>>): void => {\n this.store.setState((state) => {\n const combinedState = {\n ...state,\n ...newState,\n }\n const { isPending, isExecuting, settleCount } = combinedState\n return {\n ...combinedState,\n status: !this.#getEnabled()\n ? 'disabled'\n : isPending\n ? 'pending'\n : isExecuting\n ? 'executing'\n : settleCount > 0\n ? 'settled'\n : 'idle',\n }\n })\n emitChange('AsyncThrottler', this)\n }\n\n /**\n * Returns the current enabled state of the async throttler\n */\n #getEnabled = (): boolean => {\n return !!parseFunctionOrValue(this.options.enabled, this)\n }\n\n /**\n * Returns the current wait time in milliseconds\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 AsyncThrottler(fn, { wait: 1000 });\n *\n * // First call executes immediately\n * await throttled.maybeExecute('a', 'b');\n *\n * // Call during wait period - gets throttled\n * await throttled.maybeExecute('c', 'd');\n * ```\n */\n maybeExecute = async (\n ...args: Parameters<TFn>\n ): Promise<Awaited<ReturnType<TFn>> | undefined> => {\n if (!this.#getEnabled()) return undefined\n\n this.#resolvePreviousPromiseInternal()\n\n this.#setState({\n maybeExecuteCount: this.store.state.maybeExecuteCount + 1,\n lastArgs: args, // store the arguments for potential trailing execution\n })\n\n const wait = this.#getWait()\n const thisMaybeExecuteNumber = this.store.state.maybeExecuteCount\n\n // Wait for the wait period for the previous execution to complete if it's still running\n for (\n let maxNumIterations = wait / 10;\n this.store.state.isExecuting && maxNumIterations > 0;\n maxNumIterations--\n ) {\n await new Promise((resolve) => setTimeout(resolve, 10))\n if (this.store.state.maybeExecuteCount !== thisMaybeExecuteNumber) {\n // cancel the current maybeExecute loop because a new maybeExecute call was made\n return this.store.state.lastResult\n }\n }\n\n const now = Date.now()\n const timeSinceLastExecution = now - this.store.state.lastExecutionTime\n\n if (\n this.options.leading &&\n !this.store.state.isPending &&\n timeSinceLastExecution >= wait\n ) {\n await this.#execute(...args) // Leading EXECUTE!\n } else if (this.options.trailing) {\n // replace old pending execution with a new one\n this.cancel()\n this.#setState({\n isPending: true,\n })\n\n // Set up new trailing execution\n return new Promise<Awaited<ReturnType<TFn>> | undefined>(\n (resolve, reject) => {\n this.#resolvePreviousPromise = resolve\n\n const newTimeSinceLastExecution = this.store.state.lastExecutionTime\n ? now - this.store.state.lastExecutionTime\n : 0\n const timeoutDuration = Math.max(0, wait - newTimeSinceLastExecution)\n\n this.#timeoutId = setTimeout(async () => {\n this.#clearTimeout()\n if (this.store.state.lastArgs !== undefined) {\n try {\n await this.#execute(...this.store.state.lastArgs) // Trailing EXECUTE!\n } catch (error) {\n reject(error)\n }\n }\n this.#resolvePreviousPromise = null\n resolve(this.store.state.lastResult)\n }, timeoutDuration)\n },\n )\n }\n return this.store.state.lastResult\n }\n\n #execute = async (\n ...args: Parameters<TFn>\n ): Promise<Awaited<ReturnType<TFn>> | undefined> => {\n if (!this.#getEnabled()) return undefined\n\n const currentMaybeExecute = this.store.state.maybeExecuteCount\n\n try {\n this.#setState({ isExecuting: true })\n const currentAsyncRetryer = new AsyncRetryer(\n this.fn,\n this.options.asyncRetryerOptions,\n )\n this.asyncRetryers.set(currentMaybeExecute, currentAsyncRetryer)\n const result = await currentAsyncRetryer.execute(...args) // EXECUTE!\n this.#setState({\n lastResult: result,\n successCount: this.store.state.successCount + 1,\n })\n this.options.onSuccess?.(result as Awaited<ReturnType<TFn>>, args, this)\n } catch (error) {\n this.#setState({\n errorCount: this.store.state.errorCount + 1,\n })\n this.options.onError?.(error as Error, args, this)\n if (this.options.throwOnError) {\n throw error\n }\n } finally {\n this.asyncRetryers.delete(currentMaybeExecute) // dispose retryer\n const lastExecutionTime = Date.now()\n const wait = this.#getWait()\n const nextExecutionTime = lastExecutionTime + wait\n this.#setState({\n isExecuting: false,\n isPending: !!this.#timeoutId,\n settleCount: this.store.state.settleCount + 1,\n lastExecutionTime,\n nextExecutionTime,\n })\n this.options.onSettled?.(args, this)\n setTimeout(() => {\n if (!this.store.state.isPending) {\n // clear nextExecutionTime if there is no pending execution\n this.#setState({ nextExecutionTime: undefined })\n }\n }, wait)\n }\n return this.store.state.lastResult\n }\n\n /**\n * Processes the current pending execution immediately\n */\n flush = async (): Promise<Awaited<ReturnType<TFn>> | undefined> => {\n if (this.store.state.isPending && this.store.state.lastArgs) {\n // Store the pending promise resolver before clearing timeout\n const resolvePromise = this.#resolvePreviousPromise\n\n // Clear timeout and state without resolving the promise\n this.#clearTimeout()\n this.#setState({\n isPending: false,\n })\n\n const result = await this.#execute(...this.store.state.lastArgs)\n\n // Resolve the pending promise with the result\n if (resolvePromise) {\n resolvePromise(result)\n }\n\n return result\n }\n return undefined\n }\n\n #resolvePreviousPromiseInternal = (): void => {\n if (this.#resolvePreviousPromise) {\n this.#resolvePreviousPromise(this.store.state.lastResult)\n this.#resolvePreviousPromise = null\n }\n }\n\n #clearTimeout = (): void => {\n if (this.#timeoutId) {\n clearTimeout(this.#timeoutId)\n this.#timeoutId = null\n }\n }\n\n /**\n * Returns the AbortSignal for a specific execution.\n * If no maybeExecuteCount 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 maybeExecuteCount - Optional specific execution to get signal for\n * @example\n * ```typescript\n * const throttler = new AsyncThrottler(\n * async (data: string) => {\n * const signal = throttler.getAbortSignal()\n * if (signal) {\n * const response = await fetch('/api/save', {\n * method: 'POST',\n * body: data,\n * signal\n * })\n * return response.json()\n * }\n * },\n * { wait: 1000 }\n * )\n * ```\n */\n getAbortSignal = (maybeExecuteCount?: number): AbortSignal | null => {\n const count = maybeExecuteCount ?? this.store.state.maybeExecuteCount\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 */\n abort = (): void => {\n this.asyncRetryers.forEach((retryer) => retryer.abort())\n this.asyncRetryers.clear()\n this.#setState({ isExecuting: false })\n }\n\n /**\n * Cancels any pending execution that have not started yet.\n * Does NOT abort any execution already in progress.\n */\n cancel = (): void => {\n this.#clearTimeout()\n if (this.#resolvePreviousPromise) {\n this.#resolvePreviousPromiseInternal()\n this.#resolvePreviousPromise = null\n }\n this.#setState({\n isPending: false,\n })\n }\n\n /**\n * Resets the debouncer state to its default values\n */\n reset = (): void => {\n this.#setState(getDefaultAsyncThrottlerState<TFn>())\n this.asyncRetryers.forEach((retryer) => retryer.reset())\n }\n}\n\n/**\n * Creates an async throttled function that limits how often the function can execute.\n * The throttled function will execute at most once per wait period, even if called multiple times.\n * If called while executing, it will wait until execution completes before scheduling the next call.\n *\n * Async vs Sync Versions:\n * The async version provides advanced features over the sync throttle function:\n * - Returns promises that can be awaited for throttled function results\n * - Built-in retry support via AsyncRetryer integration\n * - Abort support to cancel in-flight executions\n * - Cancel support to prevent pending executions from starting\n * - Comprehensive error handling with onError callbacks and throwOnError control\n * - Detailed execution tracking (success/error/settle counts)\n * - Waits for ongoing executions to complete before scheduling the next one\n *\n * The sync throttle function is lighter weight and simpler when you don't need async features,\n * return values, or execution control.\n *\n * What is Throttling?\n * Throttling limits how often a function can be executed, allowing only one execution within a specified time window.\n * Unlike debouncing which resets the delay timer on each call, throttling ensures the function executes at a\n * regular interval regardless of how often it's called.\n *\n * Configuration Options:\n * - `wait`: Time window in milliseconds during which the function can only execute once (required)\n * - `leading`: Execute immediately when called (default: true)\n * - `trailing`: Execute on the trailing edge of the wait period (default: true)\n * - `enabled`: Whether the throttler is enabled (default: true)\n * - `asyncRetryerOptions`: Configure retry behavior for executions\n *\n * Error Handling:\n * - If an `onError` handler is provided, it will be called with the error and throttler 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 AsyncThrottler 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 throttler\n * - Use `onSuccess` callback to react to successful function execution and implement custom logic\n * - Use `onError` callback to react to function execution errors and implement custom error handling\n * - Use `onSettled` callback to react to function execution completion (success or error) and implement custom logic\n * - The state includes error count, execution status, last execution time, and success/settle counts\n * - State can be accessed via the underlying AsyncThrottler 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 throttled = asyncThrottle(async (value: string) => {\n * const result = await saveToAPI(value);\n * return result; // Return value is preserved\n * }, {\n * wait: 1000,\n * onError: (error) => {\n * console.error('API call failed:', error);\n * }\n * });\n *\n * // This will execute at most once per second\n * // Returns the API response directly\n * const result = await throttled(inputElement.value);\n * ```\n */\nexport function asyncThrottle<TFn extends AnyAsyncFunction>(\n fn: TFn,\n initialOptions: AsyncThrottlerOptions<TFn>,\n) {\n const asyncThrottler = new AsyncThrottler(fn, initialOptions)\n return asyncThrottler.maybeExecute\n}\n"],"mappings":";;;;;;AAsDA,SAAS,gCAEqB;CAC5B,OAAO;EACL,YAAY;EACZ,aAAa;EACb,WAAW;EACX,UAAU;EACV,mBAAmB;EACnB,YAAY;EACZ,mBAAmB;EACnB,mBAAmB;EACnB,aAAa;EACb,QAAQ;EACR,cAAc;CAChB;AACF;;;;AA6EA,SAAgB,sBAKd,SAA6B;CAC7B,OAAO;AACT;AAOA,MAAM,iBAA6D;CACjE,qBAAqB,EACnB,aAAa,EACf;CACA,SAAS;CACT,SAAS;CACT,UAAU;CACV,MAAM;AACR;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4DA,IAAa,iBAAb,MAA0D;CAOxD;CACA;CAGA,YACE,AAAO,IACP,gBACA;EAFO;eAXmD,IAAI,MAE9D,8BAAmC,CAAC;uCAGtB,IAAI,IAA+B;oBACA;iCAEiB;qBA8BtD,eAA0D;GACtE,KAAK,UAAU;IAAE,GAAG,KAAK;IAAS,GAAG;GAAW;GAGhD,IAAI,CAAC,KAAKA,YAAY,GACpB,KAAK,OAAO;EAEhB;oBAEa,aAAsD;GACjE,KAAK,MAAM,UAAU,UAAU;IAC7B,MAAM,gBAAgB;KACpB,GAAG;KACH,GAAG;IACL;IACA,MAAM,EAAE,WAAW,aAAa,gBAAgB;IAChD,OAAO;KACL,GAAG;KACH,QAAQ,CAAC,KAAKA,YAAY,IACtB,aACA,YACE,YACA,cACE,cACA,cAAc,IACZ,YACA;IACZ;GACF,CAAC;GACD,WAAW,kBAAkB,IAAI;EACnC;2BAK6B;GAC3B,OAAO,CAAC,CAAC,qBAAqB,KAAK,QAAQ,SAAS,IAAI;EAC1D;wBAKyB;GACvB,OAAO,qBAAqB,KAAK,QAAQ,MAAM,IAAI;EACrD;sBAwBe,OACb,GAAG,SAC+C;GAClD,IAAI,CAAC,KAAKA,YAAY,GAAG,OAAO;GAEhC,KAAKC,gCAAgC;GAErC,KAAKC,UAAU;IACb,mBAAmB,KAAK,MAAM,MAAM,oBAAoB;IACxD,UAAU;GACZ,CAAC;GAED,MAAM,OAAO,KAAKC,SAAS;GAC3B,MAAM,yBAAyB,KAAK,MAAM,MAAM;GAGhD,KACE,IAAI,mBAAmB,OAAO,IAC9B,KAAK,MAAM,MAAM,eAAe,mBAAmB,GACnD,oBACA;IACA,MAAM,IAAI,SAAS,YAAY,WAAW,SAAS,EAAE,CAAC;IACtD,IAAI,KAAK,MAAM,MAAM,sBAAsB,wBAEzC,OAAO,KAAK,MAAM,MAAM;GAE5B;GAEA,MAAM,MAAM,KAAK,IAAI;GACrB,MAAM,yBAAyB,MAAM,KAAK,MAAM,MAAM;GAEtD,IACE,KAAK,QAAQ,WACb,CAAC,KAAK,MAAM,MAAM,aAClB,0BAA0B,MAE1B,MAAM,KAAKC,SAAS,GAAG,IAAI;QACtB,IAAI,KAAK,QAAQ,UAAU;IAEhC,KAAK,OAAO;IACZ,KAAKF,UAAU,EACb,WAAW,KACb,CAAC;IAGD,OAAO,IAAI,SACR,SAAS,WAAW;KACnB,KAAKG,0BAA0B;KAE/B,MAAM,4BAA4B,KAAK,MAAM,MAAM,oBAC/C,MAAM,KAAK,MAAM,MAAM,oBACvB;KACJ,MAAM,kBAAkB,KAAK,IAAI,GAAG,OAAO,yBAAyB;KAEpE,KAAKC,aAAa,WAAW,YAAY;MACvC,KAAKC,cAAc;MACnB,IAAI,KAAK,MAAM,MAAM,aAAa,QAChC,IAAI;OACF,MAAM,KAAKH,SAAS,GAAG,KAAK,MAAM,MAAM,QAAQ;MAClD,SAAS,OAAO;OACd,OAAO,KAAK;MACd;MAEF,KAAKC,0BAA0B;MAC/B,QAAQ,KAAK,MAAM,MAAM,UAAU;KACrC,GAAG,eAAe;IACpB,CACF;GACF;GACA,OAAO,KAAK,MAAM,MAAM;EAC1B;kBAEW,OACT,GAAG,SAC+C;GAClD,IAAI,CAAC,KAAKL,YAAY,GAAG,OAAO;GAEhC,MAAM,sBAAsB,KAAK,MAAM,MAAM;GAE7C,IAAI;IACF,KAAKE,UAAU,EAAE,aAAa,KAAK,CAAC;IACpC,MAAM,sBAAsB,IAAI,aAC9B,KAAK,IACL,KAAK,QAAQ,mBACf;IACA,KAAK,cAAc,IAAI,qBAAqB,mBAAmB;IAC/D,MAAM,SAAS,MAAM,oBAAoB,QAAQ,GAAG,IAAI;IACxD,KAAKA,UAAU;KACb,YAAY;KACZ,cAAc,KAAK,MAAM,MAAM,eAAe;IAChD,CAAC;IACD,KAAK,QAAQ,YAAY,QAAoC,MAAM,IAAI;GACzE,SAAS,OAAO;IACd,KAAKA,UAAU,EACb,YAAY,KAAK,MAAM,MAAM,aAAa,EAC5C,CAAC;IACD,KAAK,QAAQ,UAAU,OAAgB,MAAM,IAAI;IACjD,IAAI,KAAK,QAAQ,cACf,MAAM;GAEV,UAAU;IACR,KAAK,cAAc,OAAO,mBAAmB;IAC7C,MAAM,oBAAoB,KAAK,IAAI;IACnC,MAAM,OAAO,KAAKC,SAAS;IAC3B,MAAM,oBAAoB,oBAAoB;IAC9C,KAAKD,UAAU;KACb,aAAa;KACb,WAAW,CAAC,CAAC,KAAKI;KAClB,aAAa,KAAK,MAAM,MAAM,cAAc;KAC5C;KACA;IACF,CAAC;IACD,KAAK,QAAQ,YAAY,MAAM,IAAI;IACnC,iBAAiB;KACf,IAAI,CAAC,KAAK,MAAM,MAAM,WAEpB,KAAKJ,UAAU,EAAE,mBAAmB,OAAU,CAAC;IAEnD,GAAG,IAAI;GACT;GACA,OAAO,KAAK,MAAM,MAAM;EAC1B;eAKQ,YAA2D;GACjE,IAAI,KAAK,MAAM,MAAM,aAAa,KAAK,MAAM,MAAM,UAAU;IAE3D,MAAM,iBAAiB,KAAKG;IAG5B,KAAKE,cAAc;IACnB,KAAKL,UAAU,EACb,WAAW,MACb,CAAC;IAED,MAAM,SAAS,MAAM,KAAKE,SAAS,GAAG,KAAK,MAAM,MAAM,QAAQ;IAG/D,IAAI,gBACF,eAAe,MAAM;IAGvB,OAAO;GACT;EAEF;+CAE8C;GAC5C,IAAI,KAAKC,yBAAyB;IAChC,KAAKA,wBAAwB,KAAK,MAAM,MAAM,UAAU;IACxD,KAAKA,0BAA0B;GACjC;EACF;6BAE4B;GAC1B,IAAI,KAAKC,YAAY;IACnB,aAAa,KAAKA,UAAU;IAC5B,KAAKA,aAAa;GACpB;EACF;yBA0BkB,sBAAmD;GACnE,MAAM,QAAQ,qBAAqB,KAAK,MAAM,MAAM;GAEpD,OADgB,KAAK,cAAc,IAAI,KAC1B,CAAC,EAAE,eAAe,KAAK;EACtC;qBAMoB;GAClB,KAAK,cAAc,SAAS,YAAY,QAAQ,MAAM,CAAC;GACvD,KAAK,cAAc,MAAM;GACzB,KAAKJ,UAAU,EAAE,aAAa,MAAM,CAAC;EACvC;sBAMqB;GACnB,KAAKK,cAAc;GACnB,IAAI,KAAKF,yBAAyB;IAChC,KAAKJ,gCAAgC;IACrC,KAAKI,0BAA0B;GACjC;GACA,KAAKH,UAAU,EACb,WAAW,MACb,CAAC;EACH;qBAKoB;GAClB,KAAKA,UAAU,8BAAmC,CAAC;GACnD,KAAK,cAAc,SAAS,YAAY,QAAQ,MAAM,CAAC;EACzD;EA5TE,KAAK,MAAM,eAAe;EAC1B,KAAK,UAAU;GACb,GAAG;GACH,GAAG;GACH,cAAc,eAAe,gBAAgB,CAAC,eAAe;EAC/D;EACA,KAAKA,UAAU,KAAK,QAAQ,gBAAgB,CAAC,CAAC;EAE9C,IAAI,KAAK,KACP,iBAAiB,GAAG,qBAAqB,UAAU;GACjD,IAAI,MAAM,QAAQ,QAAQ,KAAK,KAAK;GACpC,KAAKA,UACH,MAAM,QAAQ,MAAM,KACtB;GACA,KAAK,WACH,MAAM,QAAQ,OAChB;EACF,CAAC;CAEL;CAcA;;;;CA0BA;;;;CAOA;CAkGA;CA6EA;CAOA;AAqEF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkEA,SAAgB,cACd,IACA,gBACA;CAEA,OAAO,IADoB,eAAe,IAAI,cAC1B,CAAC,CAAC;AACxB"}
@@ -1 +1 @@
1
- {"version":3,"file":"batcher.cjs","names":["Store","parseFunctionOrValue","#setState","#execute","#clearTimeout","#timeoutId","#getWait"],"sources":["../src/batcher.ts"],"sourcesContent":["import { Store } from '@tanstack/store'\nimport { parseFunctionOrValue } from './utils'\nimport { emitChange, pacerEventClient } from './event-client'\nimport type { OptionalKeys } from './types'\n\nexport interface BatcherState<TValue> {\n /**\n * Number of batch executions that have been completed\n */\n executionCount: number\n /**\n * Whether the batcher has no items to process (items array is empty)\n */\n isEmpty: 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 * 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\n */\n status: 'idle' | 'pending'\n /**\n * Total number of items that have been processed across all batches\n */\n totalItemsProcessed: number\n}\n\nfunction getDefaultBatcherState<TValue>(): BatcherState<TValue> {\n return {\n executionCount: 0,\n isEmpty: true,\n isPending: false,\n totalItemsProcessed: 0,\n items: [],\n size: 0,\n status: 'idle',\n }\n}\n\n/**\n * Options for configuring a Batcher instance\n */\nexport interface BatcherOptions<TValue> {\n /**\n * Custom function to determine if a batch should be processed\n * Return true to process the batch immediately\n */\n getShouldExecute?: (items: Array<TValue>, batcher: Batcher<TValue>) => boolean\n /**\n * Initial state for the batcher\n */\n initialState?: Partial<BatcherState<TValue>>\n /**\n * Optional key to identify this batcher instance.\n * If provided, the 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 * Callback fired after a batch is processed\n */\n onExecute?: (batch: Array<TValue>, batcher: Batcher<TValue>) => void\n /**\n * Callback fired after items are added to the batcher\n */\n onItemsChange?: (batcher: Batcher<TValue>) => void\n /**\n * Whether the batcher should start processing immediately\n * @default true\n */\n started?: 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 | ((batcher: Batcher<TValue>) => number)\n}\n\ntype BatcherOptionsWithOptionalCallbacks<TValue> = OptionalKeys<\n Required<BatcherOptions<TValue>>,\n 'initialState' | 'onExecute' | 'onItemsChange' | 'key'\n>\n\nconst defaultOptions: BatcherOptionsWithOptionalCallbacks<any> = {\n getShouldExecute: () => false,\n maxSize: Infinity,\n started: true,\n wait: Infinity,\n}\n\n/**\n * A class that collects items and processes them in batches.\n *\n * Batching is a technique for grouping multiple operations together to be processed as a single unit.\n * This synchronous version is lighter weight and often all you need - upgrade to AsyncBatcher when you need promises, retry support, abort/cancel capabilities, or advanced error handling.\n *\n * The Batcher provides a flexible way to implement 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 *\n * State Management:\n * - Uses TanStack Store for reactive state management\n * - Use `initialState` to provide initial state values when creating the batcher\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 batch execution count, total items processed, items, and running status\n * - State can be accessed via `batcher.store.state` when using the class directly\n * - When using framework adapters (React/Solid), state is accessed from `batcher.state`\n *\n * @example\n * ```ts\n * const batcher = new Batcher<number>(\n * (items) => console.log('Processing batch:', items),\n * {\n * maxSize: 5,\n * wait: 2000,\n * onExecute: (batch, batcher) => console.log('Batch executed:', batch)\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\n * // batcher.flush() // manually trigger a batch\n * ```\n */\nexport class Batcher<TValue> {\n readonly store: Store<Readonly<BatcherState<TValue>>> = new Store(\n getDefaultBatcherState<TValue>(),\n )\n key: string | undefined\n options: BatcherOptionsWithOptionalCallbacks<TValue>\n #timeoutId: ReturnType<typeof setTimeout> | null = null\n\n constructor(\n public fn: (items: Array<TValue>) => void,\n initialOptions: BatcherOptions<TValue>,\n ) {\n this.key = initialOptions.key\n this.options = {\n ...defaultOptions,\n ...initialOptions,\n }\n this.#setState(this.options.initialState ?? {})\n\n if (this.key) {\n pacerEventClient.on('d-Batcher', (event) => {\n if (event.payload.key !== this.key) return\n this.#setState(\n event.payload.store.state as Partial<BatcherState<TValue>>,\n )\n this.setOptions(\n event.payload.options as Partial<BatcherOptions<TValue>>,\n )\n })\n }\n }\n\n /**\n * Updates the batcher options\n */\n setOptions = (newOptions: Partial<BatcherOptions<TValue>>): void => {\n this.options = { ...this.options, ...newOptions }\n }\n\n #setState = (newState: Partial<BatcherState<TValue>>): void => {\n this.store.setState((state) => {\n const combinedState = {\n ...state,\n ...newState,\n }\n const { isPending, items } = combinedState\n const size = items.length\n const isEmpty = size === 0\n return {\n ...combinedState,\n isEmpty,\n size,\n status: isPending ? 'pending' : 'idle',\n }\n })\n emitChange('Batcher', this)\n }\n\n #getWait = (): number => {\n return parseFunctionOrValue(this.options.wait, this)\n }\n\n /**\n * Adds an item to the 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.\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 batchSize\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 #execute = (): void => {\n if (this.store.state.items.length === 0) {\n return\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) // Call onItemsChange to notify listeners that the items have changed\n\n this.fn(batch) // EXECUTE\n this.#setState({\n executionCount: this.store.state.executionCount + 1,\n totalItemsProcessed: this.store.state.totalItemsProcessed + batch.length,\n })\n this.options.onExecute?.(batch, this)\n }\n\n /**\n * Processes the current batch of items immediately\n */\n flush = (): void => {\n this.#clearTimeout() // clear any pending timeout\n this.#execute() // execute immediately\n }\n\n /**\n * Returns a copy of all items in the batcher\n */\n peekAllItems = (): Array<TValue> => {\n return [...this.store.state.items]\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 batcher\n */\n clear = (): void => {\n this.#setState({ items: [], isPending: false })\n }\n\n /**\n * Cancels any pending execution that was scheduled.\n * Does NOT clear out the items.\n */\n cancel = (): void => {\n this.#clearTimeout()\n this.#setState({ isPending: false })\n }\n\n /**\n * Resets the batcher state to its default values\n */\n reset = (): void => {\n this.#setState(getDefaultBatcherState<TValue>())\n this.options.onItemsChange?.(this)\n }\n}\n\n/**\n * Creates a batcher that processes items in batches.\n *\n * This synchronous version is lighter weight and often all you need - upgrade to asyncBatch when you need promises, retry support, abort/cancel capabilities, or advanced error handling.\n *\n * @example\n * ```ts\n * const batchItems = batch<number>(\n * (items) => console.log('Processing:', items),\n * {\n * maxSize: 3,\n * onExecute: (batch, batcher) => console.log('Batch executed:', batch)\n * }\n * );\n *\n * batchItems(1);\n * batchItems(2);\n * batchItems(3); // Triggers batch processing\n * ```\n */\nexport function batch<TValue>(\n fn: (items: Array<TValue>) => void,\n options: BatcherOptions<TValue>,\n) {\n const batcher = new Batcher<TValue>(fn, options)\n return batcher.addItem\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"batcher.cjs","names":["Store","parseFunctionOrValue","#setState","#execute","#clearTimeout","#timeoutId","#getWait"],"sources":["../src/batcher.ts"],"sourcesContent":["import { Store } from '@tanstack/store'\nimport { parseFunctionOrValue } from './utils'\nimport { emitChange, pacerEventClient } from './event-client'\nimport type { OptionalKeys } from './types'\n\nexport interface BatcherState<TValue> {\n /**\n * Number of batch executions that have been completed\n */\n executionCount: number\n /**\n * Whether the batcher has no items to process (items array is empty)\n */\n isEmpty: 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 * 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\n */\n status: 'idle' | 'pending'\n /**\n * Total number of items that have been processed across all batches\n */\n totalItemsProcessed: number\n}\n\nfunction getDefaultBatcherState<TValue>(): BatcherState<TValue> {\n return {\n executionCount: 0,\n isEmpty: true,\n isPending: false,\n totalItemsProcessed: 0,\n items: [],\n size: 0,\n status: 'idle',\n }\n}\n\n/**\n * Options for configuring a Batcher instance\n */\nexport interface BatcherOptions<TValue> {\n /**\n * Custom function to determine if a batch should be processed\n * Return true to process the batch immediately\n */\n getShouldExecute?: (items: Array<TValue>, batcher: Batcher<TValue>) => boolean\n /**\n * Initial state for the batcher\n */\n initialState?: Partial<BatcherState<TValue>>\n /**\n * Optional key to identify this batcher instance.\n * If provided, the 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 * Callback fired after a batch is processed\n */\n onExecute?: (batch: Array<TValue>, batcher: Batcher<TValue>) => void\n /**\n * Callback fired after items are added to the batcher\n */\n onItemsChange?: (batcher: Batcher<TValue>) => void\n /**\n * Whether the batcher should start processing immediately\n * @default true\n */\n started?: 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 | ((batcher: Batcher<TValue>) => number)\n}\n\ntype BatcherOptionsWithOptionalCallbacks<TValue> = OptionalKeys<\n Required<BatcherOptions<TValue>>,\n 'initialState' | 'onExecute' | 'onItemsChange' | 'key'\n>\n\nconst defaultOptions: BatcherOptionsWithOptionalCallbacks<any> = {\n getShouldExecute: () => false,\n maxSize: Infinity,\n started: true,\n wait: Infinity,\n}\n\n/**\n * A class that collects items and processes them in batches.\n *\n * Batching is a technique for grouping multiple operations together to be processed as a single unit.\n * This synchronous version is lighter weight and often all you need - upgrade to AsyncBatcher when you need promises, retry support, abort/cancel capabilities, or advanced error handling.\n *\n * The Batcher provides a flexible way to implement 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 *\n * State Management:\n * - Uses TanStack Store for reactive state management\n * - Use `initialState` to provide initial state values when creating the batcher\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 batch execution count, total items processed, items, and running status\n * - State can be accessed via `batcher.store.state` when using the class directly\n * - When using framework adapters (React/Solid), state is accessed from `batcher.state`\n *\n * @example\n * ```ts\n * const batcher = new Batcher<number>(\n * (items) => console.log('Processing batch:', items),\n * {\n * maxSize: 5,\n * wait: 2000,\n * onExecute: (batch, batcher) => console.log('Batch executed:', batch)\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\n * // batcher.flush() // manually trigger a batch\n * ```\n */\nexport class Batcher<TValue> {\n readonly store: Store<Readonly<BatcherState<TValue>>> = new Store(\n getDefaultBatcherState<TValue>(),\n )\n key: string | undefined\n options: BatcherOptionsWithOptionalCallbacks<TValue>\n #timeoutId: ReturnType<typeof setTimeout> | null = null\n\n constructor(\n public fn: (items: Array<TValue>) => void,\n initialOptions: BatcherOptions<TValue>,\n ) {\n this.key = initialOptions.key\n this.options = {\n ...defaultOptions,\n ...initialOptions,\n }\n this.#setState(this.options.initialState ?? {})\n\n if (this.key) {\n pacerEventClient.on('d-Batcher', (event) => {\n if (event.payload.key !== this.key) return\n this.#setState(\n event.payload.store.state as Partial<BatcherState<TValue>>,\n )\n this.setOptions(\n event.payload.options as Partial<BatcherOptions<TValue>>,\n )\n })\n }\n }\n\n /**\n * Updates the batcher options\n */\n setOptions = (newOptions: Partial<BatcherOptions<TValue>>): void => {\n this.options = { ...this.options, ...newOptions }\n }\n\n #setState = (newState: Partial<BatcherState<TValue>>): void => {\n this.store.setState((state) => {\n const combinedState = {\n ...state,\n ...newState,\n }\n const { isPending, items } = combinedState\n const size = items.length\n const isEmpty = size === 0\n return {\n ...combinedState,\n isEmpty,\n size,\n status: isPending ? 'pending' : 'idle',\n }\n })\n emitChange('Batcher', this)\n }\n\n #getWait = (): number => {\n return parseFunctionOrValue(this.options.wait, this)\n }\n\n /**\n * Adds an item to the 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.\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 batchSize\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 #execute = (): void => {\n if (this.store.state.items.length === 0) {\n return\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) // Call onItemsChange to notify listeners that the items have changed\n\n this.fn(batch) // EXECUTE\n this.#setState({\n executionCount: this.store.state.executionCount + 1,\n totalItemsProcessed: this.store.state.totalItemsProcessed + batch.length,\n })\n this.options.onExecute?.(batch, this)\n }\n\n /**\n * Processes the current batch of items immediately\n */\n flush = (): void => {\n this.#clearTimeout() // clear any pending timeout\n this.#execute() // execute immediately\n }\n\n /**\n * Returns a copy of all items in the batcher\n */\n peekAllItems = (): Array<TValue> => {\n return [...this.store.state.items]\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 batcher\n */\n clear = (): void => {\n this.#setState({ items: [], isPending: false })\n }\n\n /**\n * Cancels any pending execution that was scheduled.\n * Does NOT clear out the items.\n */\n cancel = (): void => {\n this.#clearTimeout()\n this.#setState({ isPending: false })\n }\n\n /**\n * Resets the batcher state to its default values\n */\n reset = (): void => {\n this.#setState(getDefaultBatcherState<TValue>())\n this.options.onItemsChange?.(this)\n }\n}\n\n/**\n * Creates a batcher that processes items in batches.\n *\n * This synchronous version is lighter weight and often all you need - upgrade to asyncBatch when you need promises, retry support, abort/cancel capabilities, or advanced error handling.\n *\n * @example\n * ```ts\n * const batchItems = batch<number>(\n * (items) => console.log('Processing:', items),\n * {\n * maxSize: 3,\n * onExecute: (batch, batcher) => console.log('Batch executed:', batch)\n * }\n * );\n *\n * batchItems(1);\n * batchItems(2);\n * batchItems(3); // Triggers batch processing\n * ```\n */\nexport function batch<TValue>(\n fn: (items: Array<TValue>) => void,\n options: BatcherOptions<TValue>,\n) {\n const batcher = new Batcher<TValue>(fn, options)\n return batcher.addItem\n}\n"],"mappings":"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"}
@@ -1,6 +1,5 @@
1
1
  import { OptionalKeys } from "./types.cjs";
2
2
  import { Store } from "@tanstack/store";
3
-
4
3
  //#region src/batcher.d.ts
5
4
  interface BatcherState<TValue> {
6
5
  /**
package/dist/batcher.d.ts CHANGED
@@ -1,6 +1,5 @@
1
1
  import { OptionalKeys } from "./types.js";
2
2
  import { Store } from "@tanstack/store";
3
-
4
3
  //#region src/batcher.d.ts
5
4
  interface BatcherState<TValue> {
6
5
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"batcher.js","names":["#setState","#execute","#clearTimeout","#timeoutId","#getWait"],"sources":["../src/batcher.ts"],"sourcesContent":["import { Store } from '@tanstack/store'\nimport { parseFunctionOrValue } from './utils'\nimport { emitChange, pacerEventClient } from './event-client'\nimport type { OptionalKeys } from './types'\n\nexport interface BatcherState<TValue> {\n /**\n * Number of batch executions that have been completed\n */\n executionCount: number\n /**\n * Whether the batcher has no items to process (items array is empty)\n */\n isEmpty: 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 * 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\n */\n status: 'idle' | 'pending'\n /**\n * Total number of items that have been processed across all batches\n */\n totalItemsProcessed: number\n}\n\nfunction getDefaultBatcherState<TValue>(): BatcherState<TValue> {\n return {\n executionCount: 0,\n isEmpty: true,\n isPending: false,\n totalItemsProcessed: 0,\n items: [],\n size: 0,\n status: 'idle',\n }\n}\n\n/**\n * Options for configuring a Batcher instance\n */\nexport interface BatcherOptions<TValue> {\n /**\n * Custom function to determine if a batch should be processed\n * Return true to process the batch immediately\n */\n getShouldExecute?: (items: Array<TValue>, batcher: Batcher<TValue>) => boolean\n /**\n * Initial state for the batcher\n */\n initialState?: Partial<BatcherState<TValue>>\n /**\n * Optional key to identify this batcher instance.\n * If provided, the 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 * Callback fired after a batch is processed\n */\n onExecute?: (batch: Array<TValue>, batcher: Batcher<TValue>) => void\n /**\n * Callback fired after items are added to the batcher\n */\n onItemsChange?: (batcher: Batcher<TValue>) => void\n /**\n * Whether the batcher should start processing immediately\n * @default true\n */\n started?: 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 | ((batcher: Batcher<TValue>) => number)\n}\n\ntype BatcherOptionsWithOptionalCallbacks<TValue> = OptionalKeys<\n Required<BatcherOptions<TValue>>,\n 'initialState' | 'onExecute' | 'onItemsChange' | 'key'\n>\n\nconst defaultOptions: BatcherOptionsWithOptionalCallbacks<any> = {\n getShouldExecute: () => false,\n maxSize: Infinity,\n started: true,\n wait: Infinity,\n}\n\n/**\n * A class that collects items and processes them in batches.\n *\n * Batching is a technique for grouping multiple operations together to be processed as a single unit.\n * This synchronous version is lighter weight and often all you need - upgrade to AsyncBatcher when you need promises, retry support, abort/cancel capabilities, or advanced error handling.\n *\n * The Batcher provides a flexible way to implement 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 *\n * State Management:\n * - Uses TanStack Store for reactive state management\n * - Use `initialState` to provide initial state values when creating the batcher\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 batch execution count, total items processed, items, and running status\n * - State can be accessed via `batcher.store.state` when using the class directly\n * - When using framework adapters (React/Solid), state is accessed from `batcher.state`\n *\n * @example\n * ```ts\n * const batcher = new Batcher<number>(\n * (items) => console.log('Processing batch:', items),\n * {\n * maxSize: 5,\n * wait: 2000,\n * onExecute: (batch, batcher) => console.log('Batch executed:', batch)\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\n * // batcher.flush() // manually trigger a batch\n * ```\n */\nexport class Batcher<TValue> {\n readonly store: Store<Readonly<BatcherState<TValue>>> = new Store(\n getDefaultBatcherState<TValue>(),\n )\n key: string | undefined\n options: BatcherOptionsWithOptionalCallbacks<TValue>\n #timeoutId: ReturnType<typeof setTimeout> | null = null\n\n constructor(\n public fn: (items: Array<TValue>) => void,\n initialOptions: BatcherOptions<TValue>,\n ) {\n this.key = initialOptions.key\n this.options = {\n ...defaultOptions,\n ...initialOptions,\n }\n this.#setState(this.options.initialState ?? {})\n\n if (this.key) {\n pacerEventClient.on('d-Batcher', (event) => {\n if (event.payload.key !== this.key) return\n this.#setState(\n event.payload.store.state as Partial<BatcherState<TValue>>,\n )\n this.setOptions(\n event.payload.options as Partial<BatcherOptions<TValue>>,\n )\n })\n }\n }\n\n /**\n * Updates the batcher options\n */\n setOptions = (newOptions: Partial<BatcherOptions<TValue>>): void => {\n this.options = { ...this.options, ...newOptions }\n }\n\n #setState = (newState: Partial<BatcherState<TValue>>): void => {\n this.store.setState((state) => {\n const combinedState = {\n ...state,\n ...newState,\n }\n const { isPending, items } = combinedState\n const size = items.length\n const isEmpty = size === 0\n return {\n ...combinedState,\n isEmpty,\n size,\n status: isPending ? 'pending' : 'idle',\n }\n })\n emitChange('Batcher', this)\n }\n\n #getWait = (): number => {\n return parseFunctionOrValue(this.options.wait, this)\n }\n\n /**\n * Adds an item to the 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.\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 batchSize\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 #execute = (): void => {\n if (this.store.state.items.length === 0) {\n return\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) // Call onItemsChange to notify listeners that the items have changed\n\n this.fn(batch) // EXECUTE\n this.#setState({\n executionCount: this.store.state.executionCount + 1,\n totalItemsProcessed: this.store.state.totalItemsProcessed + batch.length,\n })\n this.options.onExecute?.(batch, this)\n }\n\n /**\n * Processes the current batch of items immediately\n */\n flush = (): void => {\n this.#clearTimeout() // clear any pending timeout\n this.#execute() // execute immediately\n }\n\n /**\n * Returns a copy of all items in the batcher\n */\n peekAllItems = (): Array<TValue> => {\n return [...this.store.state.items]\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 batcher\n */\n clear = (): void => {\n this.#setState({ items: [], isPending: false })\n }\n\n /**\n * Cancels any pending execution that was scheduled.\n * Does NOT clear out the items.\n */\n cancel = (): void => {\n this.#clearTimeout()\n this.#setState({ isPending: false })\n }\n\n /**\n * Resets the batcher state to its default values\n */\n reset = (): void => {\n this.#setState(getDefaultBatcherState<TValue>())\n this.options.onItemsChange?.(this)\n }\n}\n\n/**\n * Creates a batcher that processes items in batches.\n *\n * This synchronous version is lighter weight and often all you need - upgrade to asyncBatch when you need promises, retry support, abort/cancel capabilities, or advanced error handling.\n *\n * @example\n * ```ts\n * const batchItems = batch<number>(\n * (items) => console.log('Processing:', items),\n * {\n * maxSize: 3,\n * onExecute: (batch, batcher) => console.log('Batch executed:', batch)\n * }\n * );\n *\n * batchItems(1);\n * batchItems(2);\n * batchItems(3); // Triggers batch processing\n * ```\n */\nexport function batch<TValue>(\n fn: (items: Array<TValue>) => void,\n options: BatcherOptions<TValue>,\n) {\n const batcher = new Batcher<TValue>(fn, options)\n return batcher.addItem\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"batcher.js","names":["#setState","#execute","#clearTimeout","#timeoutId","#getWait"],"sources":["../src/batcher.ts"],"sourcesContent":["import { Store } from '@tanstack/store'\nimport { parseFunctionOrValue } from './utils'\nimport { emitChange, pacerEventClient } from './event-client'\nimport type { OptionalKeys } from './types'\n\nexport interface BatcherState<TValue> {\n /**\n * Number of batch executions that have been completed\n */\n executionCount: number\n /**\n * Whether the batcher has no items to process (items array is empty)\n */\n isEmpty: 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 * 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\n */\n status: 'idle' | 'pending'\n /**\n * Total number of items that have been processed across all batches\n */\n totalItemsProcessed: number\n}\n\nfunction getDefaultBatcherState<TValue>(): BatcherState<TValue> {\n return {\n executionCount: 0,\n isEmpty: true,\n isPending: false,\n totalItemsProcessed: 0,\n items: [],\n size: 0,\n status: 'idle',\n }\n}\n\n/**\n * Options for configuring a Batcher instance\n */\nexport interface BatcherOptions<TValue> {\n /**\n * Custom function to determine if a batch should be processed\n * Return true to process the batch immediately\n */\n getShouldExecute?: (items: Array<TValue>, batcher: Batcher<TValue>) => boolean\n /**\n * Initial state for the batcher\n */\n initialState?: Partial<BatcherState<TValue>>\n /**\n * Optional key to identify this batcher instance.\n * If provided, the 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 * Callback fired after a batch is processed\n */\n onExecute?: (batch: Array<TValue>, batcher: Batcher<TValue>) => void\n /**\n * Callback fired after items are added to the batcher\n */\n onItemsChange?: (batcher: Batcher<TValue>) => void\n /**\n * Whether the batcher should start processing immediately\n * @default true\n */\n started?: 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 | ((batcher: Batcher<TValue>) => number)\n}\n\ntype BatcherOptionsWithOptionalCallbacks<TValue> = OptionalKeys<\n Required<BatcherOptions<TValue>>,\n 'initialState' | 'onExecute' | 'onItemsChange' | 'key'\n>\n\nconst defaultOptions: BatcherOptionsWithOptionalCallbacks<any> = {\n getShouldExecute: () => false,\n maxSize: Infinity,\n started: true,\n wait: Infinity,\n}\n\n/**\n * A class that collects items and processes them in batches.\n *\n * Batching is a technique for grouping multiple operations together to be processed as a single unit.\n * This synchronous version is lighter weight and often all you need - upgrade to AsyncBatcher when you need promises, retry support, abort/cancel capabilities, or advanced error handling.\n *\n * The Batcher provides a flexible way to implement 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 *\n * State Management:\n * - Uses TanStack Store for reactive state management\n * - Use `initialState` to provide initial state values when creating the batcher\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 batch execution count, total items processed, items, and running status\n * - State can be accessed via `batcher.store.state` when using the class directly\n * - When using framework adapters (React/Solid), state is accessed from `batcher.state`\n *\n * @example\n * ```ts\n * const batcher = new Batcher<number>(\n * (items) => console.log('Processing batch:', items),\n * {\n * maxSize: 5,\n * wait: 2000,\n * onExecute: (batch, batcher) => console.log('Batch executed:', batch)\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\n * // batcher.flush() // manually trigger a batch\n * ```\n */\nexport class Batcher<TValue> {\n readonly store: Store<Readonly<BatcherState<TValue>>> = new Store(\n getDefaultBatcherState<TValue>(),\n )\n key: string | undefined\n options: BatcherOptionsWithOptionalCallbacks<TValue>\n #timeoutId: ReturnType<typeof setTimeout> | null = null\n\n constructor(\n public fn: (items: Array<TValue>) => void,\n initialOptions: BatcherOptions<TValue>,\n ) {\n this.key = initialOptions.key\n this.options = {\n ...defaultOptions,\n ...initialOptions,\n }\n this.#setState(this.options.initialState ?? {})\n\n if (this.key) {\n pacerEventClient.on('d-Batcher', (event) => {\n if (event.payload.key !== this.key) return\n this.#setState(\n event.payload.store.state as Partial<BatcherState<TValue>>,\n )\n this.setOptions(\n event.payload.options as Partial<BatcherOptions<TValue>>,\n )\n })\n }\n }\n\n /**\n * Updates the batcher options\n */\n setOptions = (newOptions: Partial<BatcherOptions<TValue>>): void => {\n this.options = { ...this.options, ...newOptions }\n }\n\n #setState = (newState: Partial<BatcherState<TValue>>): void => {\n this.store.setState((state) => {\n const combinedState = {\n ...state,\n ...newState,\n }\n const { isPending, items } = combinedState\n const size = items.length\n const isEmpty = size === 0\n return {\n ...combinedState,\n isEmpty,\n size,\n status: isPending ? 'pending' : 'idle',\n }\n })\n emitChange('Batcher', this)\n }\n\n #getWait = (): number => {\n return parseFunctionOrValue(this.options.wait, this)\n }\n\n /**\n * Adds an item to the 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.\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 batchSize\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 #execute = (): void => {\n if (this.store.state.items.length === 0) {\n return\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) // Call onItemsChange to notify listeners that the items have changed\n\n this.fn(batch) // EXECUTE\n this.#setState({\n executionCount: this.store.state.executionCount + 1,\n totalItemsProcessed: this.store.state.totalItemsProcessed + batch.length,\n })\n this.options.onExecute?.(batch, this)\n }\n\n /**\n * Processes the current batch of items immediately\n */\n flush = (): void => {\n this.#clearTimeout() // clear any pending timeout\n this.#execute() // execute immediately\n }\n\n /**\n * Returns a copy of all items in the batcher\n */\n peekAllItems = (): Array<TValue> => {\n return [...this.store.state.items]\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 batcher\n */\n clear = (): void => {\n this.#setState({ items: [], isPending: false })\n }\n\n /**\n * Cancels any pending execution that was scheduled.\n * Does NOT clear out the items.\n */\n cancel = (): void => {\n this.#clearTimeout()\n this.#setState({ isPending: false })\n }\n\n /**\n * Resets the batcher state to its default values\n */\n reset = (): void => {\n this.#setState(getDefaultBatcherState<TValue>())\n this.options.onItemsChange?.(this)\n }\n}\n\n/**\n * Creates a batcher that processes items in batches.\n *\n * This synchronous version is lighter weight and often all you need - upgrade to asyncBatch when you need promises, retry support, abort/cancel capabilities, or advanced error handling.\n *\n * @example\n * ```ts\n * const batchItems = batch<number>(\n * (items) => console.log('Processing:', items),\n * {\n * maxSize: 3,\n * onExecute: (batch, batcher) => console.log('Batch executed:', batch)\n * }\n * );\n *\n * batchItems(1);\n * batchItems(2);\n * batchItems(3); // Triggers batch processing\n * ```\n */\nexport function batch<TValue>(\n fn: (items: Array<TValue>) => void,\n options: BatcherOptions<TValue>,\n) {\n const batcher = new Batcher<TValue>(fn, options)\n return batcher.addItem\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"debouncer.cjs","names":["Store","#getEnabled","parseFunctionOrValue","#setState","#execute","#timeoutId","#getWait","#clearTimeout"],"sources":["../src/debouncer.ts"],"sourcesContent":["import { Store } from '@tanstack/store'\nimport { parseFunctionOrValue } from './utils'\nimport { emitChange, pacerEventClient } from './event-client'\nimport type { AnyFunction } from './types'\n\nexport interface DebouncerState<TFn extends AnyFunction> {\n /**\n * Whether the debouncer can execute on the leading edge of the timeout\n */\n canLeadingExecute: boolean\n /**\n * Number of function executions that have been completed\n */\n executionCount: number\n /**\n * Whether the debouncer 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 * Number of times maybeExecute has been called (for reduction calculations)\n */\n maybeExecuteCount: number\n /**\n * Current execution status - 'idle' when not active, 'pending' when waiting for timeout\n */\n status: 'disabled' | 'idle' | 'pending'\n}\n\nfunction getDefaultDebouncerState<\n TFn extends AnyFunction,\n>(): DebouncerState<TFn> {\n return {\n canLeadingExecute: true,\n executionCount: 0,\n isPending: false,\n lastArgs: undefined,\n status: 'idle',\n maybeExecuteCount: 0,\n }\n}\n\n/**\n * Options for configuring a debounced function\n */\nexport interface DebouncerOptions<TFn extends AnyFunction> {\n /**\n * Whether the debouncer 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 | ((debouncer: Debouncer<TFn>) => boolean)\n /**\n * Initial state for the debouncer\n */\n initialState?: Partial<DebouncerState<TFn>>\n /**\n * A key to identify the debouncer.\n * If provided, the debouncer 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 * The first call will execute immediately and the rest will wait the delay.\n * Defaults to false.\n */\n leading?: boolean\n /**\n * Callback function that is called after the function is executed\n */\n onExecute?: (args: Parameters<TFn>, debouncer: Debouncer<TFn>) => void\n /**\n * Whether to execute on the trailing edge of the timeout.\n * Defaults to true.\n */\n trailing?: boolean\n /**\n * Delay in milliseconds before executing the function.\n * Can be a number or a function that returns a number.\n * Defaults to 0ms\n */\n wait: number | ((debouncer: Debouncer<TFn>) => number)\n}\n\n/**\n * Utility function for sharing common `DebouncerOptions` options between different `Debouncer` instances.\n */\nexport function debouncerOptions<\n TFn extends AnyFunction = AnyFunction,\n TOptions extends Partial<DebouncerOptions<TFn>> = Partial<\n DebouncerOptions<TFn>\n >,\n>(options: TOptions): TOptions {\n return options\n}\n\nconst defaultOptions: Omit<\n Required<DebouncerOptions<any>>,\n 'initialState' | 'onExecute' | 'key'\n> = {\n enabled: true,\n leading: false,\n trailing: true,\n wait: 0,\n}\n\n/**\n * A class that creates a debounced function.\n *\n * Debouncing ensures that a function is only executed after a certain amount of time has passed\n * since its last invocation. This is useful for handling frequent events like window resizing,\n * scroll events, or input changes where you want to limit the rate of execution.\n * This synchronous version is lighter weight and often all you need - upgrade to AsyncDebouncer when you need promises, retry support, abort/cancel capabilities, or advanced error handling.\n *\n * The debounced function can be configured to execute either at the start of the delay period\n * (leading edge) or at the end (trailing edge, default). Each new call during the wait period\n * will reset the timer.\n *\n * State Management:\n * - Uses TanStack Store for reactive state management\n * - Use `initialState` to provide initial state values when creating the debouncer\n * - Use `onExecute` callback to react to function execution and implement custom logic\n * - The state includes canLeadingExecute, execution count, and isPending status\n * - State can be accessed via `debouncer.store.state` when using the class directly\n * - When using framework adapters (React/Solid), state is accessed from `debouncer.state`\n *\n * @example\n * ```ts\n * const debouncer = new Debouncer((value: string) => {\n * saveToDatabase(value);\n * }, { wait: 500 });\n *\n * // Will only save after 500ms of no new input\n * inputElement.addEventListener('input', () => {\n * debouncer.maybeExecute(inputElement.value);\n * });\n * ```\n */\nexport class Debouncer<TFn extends AnyFunction> {\n readonly store: Store<Readonly<DebouncerState<TFn>>> = new Store(\n getDefaultDebouncerState<TFn>(),\n )\n key: string | undefined\n options: DebouncerOptions<TFn>\n #timeoutId: ReturnType<typeof setTimeout> | undefined\n\n constructor(\n public fn: TFn,\n initialOptions: DebouncerOptions<TFn>,\n ) {\n this.key = initialOptions.key\n this.options = {\n ...defaultOptions,\n ...initialOptions,\n }\n this.#setState(this.options.initialState ?? {})\n\n if (this.key) {\n pacerEventClient.on('d-Debouncer', (event) => {\n if (event.payload.key !== this.key) return\n this.#setState(\n event.payload.store.state as Partial<DebouncerState<TFn>>,\n )\n this.setOptions(event.payload.options as Partial<DebouncerOptions<TFn>>)\n })\n }\n }\n\n /**\n * Updates the debouncer options\n */\n setOptions = (newOptions: Partial<DebouncerOptions<TFn>>): void => {\n this.options = { ...this.options, ...newOptions }\n\n // Cancel pending execution if the debouncer is disabled\n if (!this.#getEnabled()) {\n this.cancel()\n }\n }\n\n #setState = (newState: Partial<DebouncerState<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('Debouncer', this)\n }\n\n /**\n * Returns the current enabled state of the debouncer\n */\n #getEnabled = (): boolean => {\n return !!parseFunctionOrValue(this.options.enabled, this)\n }\n\n /**\n * Returns the current wait time in milliseconds\n */\n #getWait = (): number => {\n return parseFunctionOrValue(this.options.wait, this)\n }\n\n /**\n * Attempts to execute the debounced function\n * If a call is already in progress, it will be queued\n */\n maybeExecute = (...args: Parameters<TFn>): void => {\n if (!this.#getEnabled()) return undefined\n\n this.#setState({\n maybeExecuteCount: this.store.state.maybeExecuteCount + 1,\n })\n\n let _didLeadingExecute = false\n\n // Handle leading execution\n if (this.options.leading && this.store.state.canLeadingExecute) {\n this.#setState({ canLeadingExecute: false })\n _didLeadingExecute = true\n this.#execute(...args)\n }\n\n // Start pending state to indicate that the debouncer is waiting for the trailing edge\n if (this.options.trailing) {\n this.#setState({ isPending: true, lastArgs: args })\n }\n\n // Clear any existing timeout\n if (this.#timeoutId) clearTimeout(this.#timeoutId)\n\n // Set new timeout that will reset canLeadingExecute and execute trailing only if enabled and did not execute leading\n this.#timeoutId = setTimeout(() => {\n this.#setState({ canLeadingExecute: true })\n if (this.options.trailing && !_didLeadingExecute) {\n this.#execute(...args)\n }\n }, this.#getWait())\n }\n\n #execute = (...args: Parameters<TFn>): void => {\n if (!this.#getEnabled()) return undefined\n this.fn(...args) // EXECUTE!\n this.#setState({\n executionCount: this.store.state.executionCount + 1,\n isPending: false,\n lastArgs: undefined,\n })\n this.options.onExecute?.(args, this)\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.#clearTimeout() // clear any pending timeout\n this.#execute(...this.store.state.lastArgs) // execute immediately\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 execution\n */\n cancel = (): void => {\n this.#clearTimeout()\n this.#setState({\n canLeadingExecute: true,\n isPending: false,\n })\n }\n\n /**\n * Resets the debouncer state to its default values\n */\n reset = (): void => {\n this.#setState(getDefaultDebouncerState<TFn>())\n }\n}\n\n/**\n * Creates a debounced function that delays invoking the provided function until after a specified wait time.\n * Multiple calls during the wait period will cancel previous pending invocations and reset the timer.\n *\n * This synchronous version is lighter weight and often all you need - upgrade to asyncDebounce when you need promises, retry support, abort/cancel capabilities, or advanced error handling.\n *\n * If leading option is true, the function will execute immediately on the first call, then wait the delay\n * before allowing another execution.\n *\n * State Management:\n * - Uses TanStack Store for reactive state management\n * - Use `initialState` to provide initial state values when creating the debouncer\n * - Use `onExecute` callback to react to function execution and implement custom logic\n * - The state includes canLeadingExecute, execution count, and isPending status\n * - State can be accessed via the underlying Debouncer 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 debounced = debounce(() => {\n * saveChanges();\n * }, { wait: 1000 });\n *\n * // Called repeatedly but executes at most once per second\n * inputElement.addEventListener('input', debounced);\n * ```\n */\nexport function debounce<TFn extends AnyFunction>(\n fn: TFn,\n initialOptions: DebouncerOptions<TFn>,\n): (...args: Parameters<TFn>) => void {\n const debouncer = new Debouncer(fn, initialOptions)\n return debouncer.maybeExecute\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"debouncer.cjs","names":["Store","#getEnabled","parseFunctionOrValue","#setState","#execute","#timeoutId","#getWait","#clearTimeout"],"sources":["../src/debouncer.ts"],"sourcesContent":["import { Store } from '@tanstack/store'\nimport { parseFunctionOrValue } from './utils'\nimport { emitChange, pacerEventClient } from './event-client'\nimport type { AnyFunction } from './types'\n\nexport interface DebouncerState<TFn extends AnyFunction> {\n /**\n * Whether the debouncer can execute on the leading edge of the timeout\n */\n canLeadingExecute: boolean\n /**\n * Number of function executions that have been completed\n */\n executionCount: number\n /**\n * Whether the debouncer 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 * Number of times maybeExecute has been called (for reduction calculations)\n */\n maybeExecuteCount: number\n /**\n * Current execution status - 'idle' when not active, 'pending' when waiting for timeout\n */\n status: 'disabled' | 'idle' | 'pending'\n}\n\nfunction getDefaultDebouncerState<\n TFn extends AnyFunction,\n>(): DebouncerState<TFn> {\n return {\n canLeadingExecute: true,\n executionCount: 0,\n isPending: false,\n lastArgs: undefined,\n status: 'idle',\n maybeExecuteCount: 0,\n }\n}\n\n/**\n * Options for configuring a debounced function\n */\nexport interface DebouncerOptions<TFn extends AnyFunction> {\n /**\n * Whether the debouncer 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 | ((debouncer: Debouncer<TFn>) => boolean)\n /**\n * Initial state for the debouncer\n */\n initialState?: Partial<DebouncerState<TFn>>\n /**\n * A key to identify the debouncer.\n * If provided, the debouncer 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 * The first call will execute immediately and the rest will wait the delay.\n * Defaults to false.\n */\n leading?: boolean\n /**\n * Callback function that is called after the function is executed\n */\n onExecute?: (args: Parameters<TFn>, debouncer: Debouncer<TFn>) => void\n /**\n * Whether to execute on the trailing edge of the timeout.\n * Defaults to true.\n */\n trailing?: boolean\n /**\n * Delay in milliseconds before executing the function.\n * Can be a number or a function that returns a number.\n * Defaults to 0ms\n */\n wait: number | ((debouncer: Debouncer<TFn>) => number)\n}\n\n/**\n * Utility function for sharing common `DebouncerOptions` options between different `Debouncer` instances.\n */\nexport function debouncerOptions<\n TFn extends AnyFunction = AnyFunction,\n TOptions extends Partial<DebouncerOptions<TFn>> = Partial<\n DebouncerOptions<TFn>\n >,\n>(options: TOptions): TOptions {\n return options\n}\n\nconst defaultOptions: Omit<\n Required<DebouncerOptions<any>>,\n 'initialState' | 'onExecute' | 'key'\n> = {\n enabled: true,\n leading: false,\n trailing: true,\n wait: 0,\n}\n\n/**\n * A class that creates a debounced function.\n *\n * Debouncing ensures that a function is only executed after a certain amount of time has passed\n * since its last invocation. This is useful for handling frequent events like window resizing,\n * scroll events, or input changes where you want to limit the rate of execution.\n * This synchronous version is lighter weight and often all you need - upgrade to AsyncDebouncer when you need promises, retry support, abort/cancel capabilities, or advanced error handling.\n *\n * The debounced function can be configured to execute either at the start of the delay period\n * (leading edge) or at the end (trailing edge, default). Each new call during the wait period\n * will reset the timer.\n *\n * State Management:\n * - Uses TanStack Store for reactive state management\n * - Use `initialState` to provide initial state values when creating the debouncer\n * - Use `onExecute` callback to react to function execution and implement custom logic\n * - The state includes canLeadingExecute, execution count, and isPending status\n * - State can be accessed via `debouncer.store.state` when using the class directly\n * - When using framework adapters (React/Solid), state is accessed from `debouncer.state`\n *\n * @example\n * ```ts\n * const debouncer = new Debouncer((value: string) => {\n * saveToDatabase(value);\n * }, { wait: 500 });\n *\n * // Will only save after 500ms of no new input\n * inputElement.addEventListener('input', () => {\n * debouncer.maybeExecute(inputElement.value);\n * });\n * ```\n */\nexport class Debouncer<TFn extends AnyFunction> {\n readonly store: Store<Readonly<DebouncerState<TFn>>> = new Store(\n getDefaultDebouncerState<TFn>(),\n )\n key: string | undefined\n options: DebouncerOptions<TFn>\n #timeoutId: ReturnType<typeof setTimeout> | undefined\n\n constructor(\n public fn: TFn,\n initialOptions: DebouncerOptions<TFn>,\n ) {\n this.key = initialOptions.key\n this.options = {\n ...defaultOptions,\n ...initialOptions,\n }\n this.#setState(this.options.initialState ?? {})\n\n if (this.key) {\n pacerEventClient.on('d-Debouncer', (event) => {\n if (event.payload.key !== this.key) return\n this.#setState(\n event.payload.store.state as Partial<DebouncerState<TFn>>,\n )\n this.setOptions(event.payload.options as Partial<DebouncerOptions<TFn>>)\n })\n }\n }\n\n /**\n * Updates the debouncer options\n */\n setOptions = (newOptions: Partial<DebouncerOptions<TFn>>): void => {\n this.options = { ...this.options, ...newOptions }\n\n // Cancel pending execution if the debouncer is disabled\n if (!this.#getEnabled()) {\n this.cancel()\n }\n }\n\n #setState = (newState: Partial<DebouncerState<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('Debouncer', this)\n }\n\n /**\n * Returns the current enabled state of the debouncer\n */\n #getEnabled = (): boolean => {\n return !!parseFunctionOrValue(this.options.enabled, this)\n }\n\n /**\n * Returns the current wait time in milliseconds\n */\n #getWait = (): number => {\n return parseFunctionOrValue(this.options.wait, this)\n }\n\n /**\n * Attempts to execute the debounced function\n * If a call is already in progress, it will be queued\n */\n maybeExecute = (...args: Parameters<TFn>): void => {\n if (!this.#getEnabled()) return undefined\n\n this.#setState({\n maybeExecuteCount: this.store.state.maybeExecuteCount + 1,\n })\n\n let _didLeadingExecute = false\n\n // Handle leading execution\n if (this.options.leading && this.store.state.canLeadingExecute) {\n this.#setState({ canLeadingExecute: false })\n _didLeadingExecute = true\n this.#execute(...args)\n }\n\n // Start pending state to indicate that the debouncer is waiting for the trailing edge\n if (this.options.trailing) {\n this.#setState({ isPending: true, lastArgs: args })\n }\n\n // Clear any existing timeout\n if (this.#timeoutId) clearTimeout(this.#timeoutId)\n\n // Set new timeout that will reset canLeadingExecute and execute trailing only if enabled and did not execute leading\n this.#timeoutId = setTimeout(() => {\n this.#setState({ canLeadingExecute: true })\n if (this.options.trailing && !_didLeadingExecute) {\n this.#execute(...args)\n }\n }, this.#getWait())\n }\n\n #execute = (...args: Parameters<TFn>): void => {\n if (!this.#getEnabled()) return undefined\n this.fn(...args) // EXECUTE!\n this.#setState({\n executionCount: this.store.state.executionCount + 1,\n isPending: false,\n lastArgs: undefined,\n })\n this.options.onExecute?.(args, this)\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.#clearTimeout() // clear any pending timeout\n this.#execute(...this.store.state.lastArgs) // execute immediately\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 execution\n */\n cancel = (): void => {\n this.#clearTimeout()\n this.#setState({\n canLeadingExecute: true,\n isPending: false,\n })\n }\n\n /**\n * Resets the debouncer state to its default values\n */\n reset = (): void => {\n this.#setState(getDefaultDebouncerState<TFn>())\n }\n}\n\n/**\n * Creates a debounced function that delays invoking the provided function until after a specified wait time.\n * Multiple calls during the wait period will cancel previous pending invocations and reset the timer.\n *\n * This synchronous version is lighter weight and often all you need - upgrade to asyncDebounce when you need promises, retry support, abort/cancel capabilities, or advanced error handling.\n *\n * If leading option is true, the function will execute immediately on the first call, then wait the delay\n * before allowing another execution.\n *\n * State Management:\n * - Uses TanStack Store for reactive state management\n * - Use `initialState` to provide initial state values when creating the debouncer\n * - Use `onExecute` callback to react to function execution and implement custom logic\n * - The state includes canLeadingExecute, execution count, and isPending status\n * - State can be accessed via the underlying Debouncer 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 debounced = debounce(() => {\n * saveChanges();\n * }, { wait: 1000 });\n *\n * // Called repeatedly but executes at most once per second\n * inputElement.addEventListener('input', debounced);\n * ```\n */\nexport function debounce<TFn extends AnyFunction>(\n fn: TFn,\n initialOptions: DebouncerOptions<TFn>,\n): (...args: Parameters<TFn>) => void {\n const debouncer = new Debouncer(fn, initialOptions)\n return debouncer.maybeExecute\n}\n"],"mappings":"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"}