Sign In

@tanstack/pacer

Package Overview
Dependencies
Maintainers
6
Versions
36
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@tanstack/pacer - npm Package Compare versions

Comparing version
0.20.0
to
0.20.1
+1
-1
dist/async-batcher.cjs.map

@@ -1,1 +0,1 @@

{"version":3,"file":"async-batcher.cjs","names":["Store","#setState","#execute","#clearTimeout","#timeoutId","#getWait","parseFunctionOrValue","AsyncRetryer"],"sources":["../src/async-batcher.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 { OptionalKeys } from './types'\n\nexport interface AsyncBatcherState<TValue> {\n /**\n * Number of batch executions that have resulted in errors\n */\n errorCount: number\n /**\n * Number of batch executions that have been executed\n */\n executeCount: number\n /**\n * Array of items that failed during batch processing\n */\n failedItems: Array<TValue>\n /**\n * Whether the batcher has no items to process (items array is empty)\n */\n isEmpty: boolean\n /**\n * Whether a batch is currently being processed asynchronously\n */\n isExecuting: boolean\n /**\n * Whether the batcher is waiting for the timeout to trigger batch processing\n */\n isPending: boolean\n /**\n * Array of items currently queued for batch processing\n */\n items: Array<TValue>\n /**\n * The result from the most recent batch execution\n */\n lastResult: any\n /**\n * Number of batch executions that have completed (either successfully or with errors)\n */\n settleCount: number\n /**\n * Number of items currently in the batch queue\n */\n size: number\n /**\n * Current processing status - 'idle' when not processing, 'pending' when waiting for timeout, 'executing' when processing, 'populated' when items are present, but no wait is configured\n */\n status: 'idle' | 'pending' | 'executing' | 'populated'\n /**\n * Number of batch executions that have completed successfully\n */\n successCount: number\n /**\n * Total number of items that have failed processing across all batches\n */\n totalItemsFailed: number\n /**\n * Total number of items that have been processed across all batches\n */\n totalItemsProcessed: number\n}\n\nfunction getDefaultAsyncBatcherState<TValue>(): AsyncBatcherState<TValue> {\n return {\n errorCount: 0,\n executeCount: 0,\n failedItems: [],\n isEmpty: true,\n isExecuting: false,\n isPending: false,\n items: [],\n lastResult: undefined,\n settleCount: 0,\n size: 0,\n status: 'idle',\n successCount: 0,\n totalItemsProcessed: 0,\n totalItemsFailed: 0,\n }\n}\n\n/**\n * Options for configuring an AsyncBatcher instance\n */\nexport interface AsyncBatcherOptions<TValue> {\n /**\n * Options for configuring the underlying async retryer\n */\n asyncRetryerOptions?: AsyncRetryerOptions<\n (items: Array<TValue>) => Promise<any>\n >\n /**\n * Custom function to determine if a batch should be processed\n * Return true to process the batch immediately\n */\n getShouldExecute?: (\n items: Array<TValue>,\n batcher: AsyncBatcher<TValue>,\n ) => boolean\n /**\n * Initial state for the async batcher\n */\n initialState?: Partial<AsyncBatcherState<TValue>>\n /**\n * Optional key to identify this async batcher instance.\n * If provided, the async batcher will be identified by this key in the devtools and PacerProvider if applicable.\n */\n key?: string\n /**\n * Maximum number of items in a batch\n * @default Infinity\n */\n maxSize?: number\n /**\n * Optional error handler for when the batch function throws.\n * If provided, the handler will be called with the error, the batch of items that failed, and batcher instance.\n * This can be used alongside throwOnError - the handler will be called before any error is thrown.\n */\n onError?: (\n error: Error,\n batch: Array<TValue>,\n batcher: AsyncBatcher<TValue>,\n ) => void\n /**\n * Callback fired after items are added to the batcher\n */\n onItemsChange?: (batcher: AsyncBatcher<TValue>) => void\n /**\n * Optional callback to call when a batch is settled (completed or failed)\n */\n onSettled?: (batch: Array<TValue>, batcher: AsyncBatcher<TValue>) => void\n /**\n * Optional callback to call when a batch succeeds\n */\n onSuccess?: (\n result: any,\n batch: Array<TValue>,\n batcher: AsyncBatcher<TValue>,\n ) => void\n /**\n * Whether the batcher should start processing immediately\n * @default true\n */\n started?: boolean\n /**\n * Whether to throw errors when they occur.\n * Defaults to true if no onError handler is provided, false if an onError handler is provided.\n * Can be explicitly set to override these defaults.\n */\n throwOnError?: boolean\n /**\n * Maximum time in milliseconds to wait before processing a batch.\n * If the wait duration has elapsed, the batch will be processed.\n * If not provided, the batch will not be triggered by a timeout.\n * @default Infinity\n */\n wait?: number | ((asyncBatcher: AsyncBatcher<TValue>) => number)\n}\n\n/**\n * Utility function for sharing common `AsyncBatcherOptions` options between different `AsyncBatcher` instances.\n *\n */\nexport function asyncBatcherOptions<\n TValue = any,\n TOptions extends Partial<AsyncBatcherOptions<TValue>> = Partial<\n AsyncBatcherOptions<TValue>\n >,\n>(options: TOptions): TOptions {\n return options\n}\n\ntype AsyncBatcherOptionsWithOptionalCallbacks<TValue> = OptionalKeys<\n Required<AsyncBatcherOptions<TValue>>,\n | 'initialState'\n | 'onError'\n | 'onItemsChange'\n | 'onSettled'\n | 'onSuccess'\n | 'key'\n>\n\nconst defaultOptions: AsyncBatcherOptionsWithOptionalCallbacks<any> = {\n asyncRetryerOptions: {\n maxAttempts: 1,\n },\n getShouldExecute: () => false,\n maxSize: Infinity,\n started: true,\n throwOnError: true,\n wait: Infinity,\n}\n\n/**\n * A class that collects items and processes them in batches asynchronously.\n *\n * Async vs Sync Versions:\n * The async version provides advanced features over the sync Batcher:\n * - Returns promises that can be awaited for batch results\n * - Built-in retry support via AsyncRetryer integration\n * - Abort support to cancel in-flight batch executions\n * - Cancel support to prevent pending batches from starting\n * - Comprehensive error handling with onError callbacks and throwOnError control\n * - Detailed execution tracking (success/error/settle counts)\n *\n * The sync Batcher is lighter weight and simpler when you don't need async features,\n * return values, or execution control.\n *\n * What is Batching?\n * Batching is a technique for grouping multiple operations together to be processed as a single unit.\n *\n * The AsyncBatcher provides a flexible way to implement async batching with configurable:\n * - Maximum batch size (number of items per batch)\n * - Time-based batching (process after X milliseconds)\n * - Custom batch processing logic via getShouldExecute\n * - Event callbacks for monitoring batch operations\n * - Error handling for failed batch operations\n *\n * Error Handling:\n * - If an `onError` handler is provided, it will be called with the error, the batch of items that failed, and batcher instance\n * - If `throwOnError` is true (default when no onError handler is provided), the error will be thrown\n * - If `throwOnError` is false (default when onError handler is provided), the error will be swallowed\n * - Both onError and throwOnError can be used together - the handler will be called before any error is thrown\n * - The error state can be checked using the AsyncBatcher instance\n *\n * State Management:\n * - Uses TanStack Store for reactive state management\n * - Use `initialState` to provide initial state values when creating the async batcher\n * - Use `onSuccess` callback to react to successful batch execution and implement custom logic\n * - Use `onError` callback to react to batch execution errors and implement custom error handling\n * - Use `onSettled` callback to react to batch execution completion (success or error) and implement custom logic\n * - Use `onExecute` callback to react to batch execution and implement custom logic\n * - Use `onItemsChange` callback to react to items being added or removed from the batcher\n * - The state includes total items processed, success/error counts, and execution status\n * - State can be accessed via `asyncBatcher.store.state` when using the class directly\n * - When using framework adapters (React/Solid), state is accessed from `asyncBatcher.state`\n *\n * @example\n * ```ts\n * const batcher = new AsyncBatcher<number>(\n * async (items) => {\n * const result = await processItems(items);\n * console.log('Processing batch:', items);\n * return result;\n * },\n * {\n * maxSize: 5,\n * wait: 2000,\n * onSuccess: (result) => console.log('Batch succeeded:', result),\n * onError: (error) => console.error('Batch failed:', error)\n * }\n * );\n *\n * batcher.addItem(1);\n * batcher.addItem(2);\n * // After 2 seconds or when 5 items are added, whichever comes first,\n * // the batch will be processed and the result will be available\n * // batcher.execute() // manually trigger a batch\n * ```\n */\nexport class AsyncBatcher<TValue> {\n readonly store: Store<Readonly<AsyncBatcherState<TValue>>> = new Store(\n getDefaultAsyncBatcherState<TValue>(),\n )\n key: string | undefined\n options: AsyncBatcherOptionsWithOptionalCallbacks<TValue>\n asyncRetryers = new Map<\n number,\n AsyncRetryer<(items: Array<TValue>) => Promise<any>>\n >()\n #timeoutId: NodeJS.Timeout | null = null\n\n constructor(\n public fn: (items: Array<TValue>) => Promise<any>,\n initialOptions: AsyncBatcherOptions<TValue>,\n ) {\n this.key = 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-AsyncBatcher', (event) => {\n if (event.payload.key !== this.key) return\n this.#setState(\n event.payload.store.state as Partial<AsyncBatcherState<TValue>>,\n )\n this.setOptions(\n event.payload.options as Partial<AsyncBatcherOptions<TValue>>,\n )\n })\n }\n }\n\n /**\n * Updates the async batcher options\n */\n setOptions = (newOptions: Partial<AsyncBatcherOptions<TValue>>): void => {\n this.options = { ...this.options, ...newOptions }\n }\n\n #setState = (newState: Partial<AsyncBatcherState<TValue>>): void => {\n this.store.setState((state) => {\n const combinedState = {\n ...state,\n ...newState,\n }\n const { isExecuting, isPending, items } = combinedState\n const size = items.length\n const isEmpty = size === 0\n return {\n ...combinedState,\n isEmpty,\n size,\n status: isExecuting\n ? 'executing'\n : isPending\n ? 'pending'\n : isEmpty\n ? 'idle'\n : 'populated',\n }\n })\n emitChange('AsyncBatcher', this)\n }\n\n #getWait = (): number => {\n return parseFunctionOrValue(this.options.wait, this)\n }\n\n /**\n * Adds an item to the async batcher\n * If the batch size is reached, timeout occurs, or shouldProcess returns true, the batch will be processed\n *\n * @returns The result from the batch function, or undefined if an error occurred and was handled by onError\n *\n * @throws The error from the batch function if no onError handler is configured or throwOnError is true\n */\n addItem = async (item: TValue): Promise<any> => {\n this.#setState({\n items: [...this.store.state.items, item],\n isPending: this.options.wait !== Infinity,\n })\n this.options.onItemsChange?.(this)\n\n const shouldProcess =\n this.store.state.items.length >= this.options.maxSize ||\n this.options.getShouldExecute(this.store.state.items, this)\n\n if (shouldProcess) {\n return await this.#execute()\n } else if (this.options.wait !== Infinity) {\n this.#clearTimeout() // clear any pending timeout to replace it with a new one\n this.#timeoutId = setTimeout(() => this.#execute(), this.#getWait())\n await new Promise((resolve) => setTimeout(resolve, this.#getWait()))\n }\n }\n\n /**\n * Processes the current batch of items asynchronously.\n * This method will automatically be triggered if the batcher is running and any of these conditions are met:\n * - The number of items reaches maxSize\n * - The wait duration has elapsed\n * - The getShouldExecute function returns true upon adding an item\n *\n * You can also call this method manually to process the current batch at any time.\n *\n * @returns A promise that resolves with the result of the batch function, or undefined if an error occurred and was handled by onError\n * @throws The error from the batch function if no onError handler is configured or throwOnError is true\n */\n #execute = async (): Promise<any> => {\n if (this.store.state.items.length === 0) {\n return undefined\n }\n\n const currentExecuteCount = this.store.state.executeCount + 1\n const batch = this.peekAllItems() // copy of the items to be processed (to prevent race conditions)\n this.clear() // Clear items before processing to prevent race conditions\n this.options.onItemsChange?.(this)\n\n this.#setState({ isExecuting: true, executeCount: currentExecuteCount })\n\n try {\n const currentAsyncRetryer = new AsyncRetryer(\n this.fn,\n this.options.asyncRetryerOptions,\n )\n this.asyncRetryers.set(currentExecuteCount, currentAsyncRetryer)\n const result = await currentAsyncRetryer.execute(batch) // EXECUTE\n this.#setState({\n totalItemsProcessed:\n this.store.state.totalItemsProcessed + batch.length,\n lastResult: result,\n successCount: this.store.state.successCount + 1,\n })\n this.options.onSuccess?.(result, batch, this)\n return result\n } catch (error) {\n this.#setState({\n errorCount: this.store.state.errorCount + 1,\n failedItems: [...this.store.state.failedItems, ...batch],\n totalItemsFailed: this.store.state.totalItemsFailed + batch.length,\n })\n this.options.onError?.(error as Error, batch, this)\n if (this.options.throwOnError) {\n throw error\n }\n return undefined\n } finally {\n this.asyncRetryers.delete(currentExecuteCount) // dispose retryer\n this.#setState({\n isExecuting: false,\n settleCount: this.store.state.settleCount + 1,\n })\n this.options.onSettled?.(batch, this)\n }\n }\n\n /**\n * Processes the current batch of items immediately\n */\n flush = async (): Promise<any> => {\n this.#clearTimeout() // clear any pending timeout\n return await this.#execute()\n }\n\n /**\n * Returns a copy of all items in the async batcher\n */\n peekAllItems = (): Array<TValue> => {\n return [...this.store.state.items]\n }\n\n peekFailedItems = (): Array<TValue> => {\n return [...this.store.state.failedItems]\n }\n\n #clearTimeout = (): void => {\n if (this.#timeoutId) {\n clearTimeout(this.#timeoutId)\n this.#timeoutId = null\n }\n }\n\n /**\n * Removes all items from the async batcher\n */\n clear = (): void => {\n this.#setState({ items: [], failedItems: [], isPending: false })\n }\n\n /**\n * Returns the AbortSignal for a specific execution.\n * If no executeCount is provided, returns the signal for the most recent execution.\n * Returns null if no execution is found or not currently executing.\n *\n * @param executeCount - Optional specific execution to get signal for\n * @example\n * ```typescript\n * const batcher = new AsyncBatcher(\n * async (items: string[]) => {\n * const signal = batcher.getAbortSignal()\n * if (signal) {\n * const response = await fetch('/api/batch', {\n * method: 'POST',\n * body: JSON.stringify(items),\n * signal\n * })\n * return response.json()\n * }\n * },\n * { maxSize: 10, wait: 100 }\n * )\n * ```\n */\n getAbortSignal = (executeCount?: number): AbortSignal | null => {\n const count = executeCount ?? this.store.state.executeCount\n const retryer = this.asyncRetryers.get(count)\n return retryer?.getAbortSignal() ?? null\n }\n\n /**\n * Aborts all ongoing executions with the internal abort controllers.\n * Does NOT cancel any pending execution that have not started yet.\n * Does NOT clear out the items.\n */\n abort = (): void => {\n this.asyncRetryers.forEach((retryer) => retryer.abort())\n this.asyncRetryers.clear()\n this.#setState({\n isExecuting: false,\n })\n }\n\n /**\n * Cancels any pending execution that have not started yet.\n * Does NOT abort any execution already in progress.\n * Does NOT clear out the items.\n */\n cancel = (): void => {\n this.#clearTimeout()\n this.#setState({\n isPending: false,\n })\n }\n\n /**\n * Resets the async batcher state to its default values\n */\n reset = (): void => {\n this.#setState(getDefaultAsyncBatcherState<TValue>())\n this.options.onItemsChange?.(this)\n this.asyncRetryers.forEach((retryer) => retryer.reset())\n }\n}\n\n/**\n * Creates an async batcher that processes items in batches.\n *\n * Async vs Sync Versions:\n * The async version provides advanced features over the sync batch function:\n * - Returns promises that can be awaited for batch results\n * - Built-in retry support via AsyncRetryer integration\n * - Abort support to cancel in-flight batch executions\n * - Cancel support to prevent pending batches from starting\n * - Comprehensive error handling with onError callbacks and throwOnError control\n * - Detailed execution tracking (success/error/settle counts)\n *\n * The sync batch function is lighter weight and simpler when you don't need async features,\n * return values, or execution control.\n *\n * What is Batching?\n * Batching is a technique for grouping multiple operations together to be processed as a single unit.\n *\n * Configuration Options:\n * - `maxSize`: Maximum number of items per batch (default: Infinity)\n * - `wait`: Time to wait before processing batch (default: Infinity)\n * - `getShouldExecute`: Custom logic to trigger batch processing\n * - `asyncRetryerOptions`: Configure retry behavior for batch executions\n * - `started`: Whether to start processing immediately (default: true)\n *\n * Error Handling:\n * - If an `onError` handler is provided, it will be called with the error, the batch of items that failed, and batcher instance\n * - If `throwOnError` is true (default when no onError handler is provided), the error will be thrown\n * - If `throwOnError` is false (default when onError handler is provided), the error will be swallowed\n * - Both onError and throwOnError can be used together - the handler will be called before any error is thrown\n * - The error state can be checked using the underlying AsyncBatcher instance\n *\n * State Management:\n * - Uses TanStack Store for reactive state management\n * - Use `initialState` to provide initial state values when creating the async batcher\n * - Use `onSuccess` callback to react to successful batch execution and implement custom logic\n * - Use `onError` callback to react to batch execution errors and implement custom error handling\n * - Use `onSettled` callback to react to batch execution completion (success or error) and implement custom logic\n * - Use `onItemsChange` callback to react to items being added or removed from the batcher\n * - The state includes total items processed, success/error counts, and execution status\n * - State can be accessed via the underlying AsyncBatcher instance's `store.state` property\n * - When using framework adapters (React/Solid), state is accessed from the hook's state property\n *\n * @example\n * ```ts\n * const batchItems = asyncBatch<number>(\n * async (items) => {\n * const result = await processApiCall(items);\n * console.log('Processing:', items);\n * return result;\n * },\n * {\n * maxSize: 3,\n * wait: 1000,\n * onSuccess: (result) => console.log('Batch succeeded:', result),\n * onError: (error) => console.error('Batch failed:', error)\n * }\n * );\n *\n * batchItems(1);\n * batchItems(2);\n * batchItems(3); // Triggers batch processing\n * ```\n */\nexport function asyncBatch<TValue>(\n fn: (items: Array<TValue>) => Promise<any>,\n options: AsyncBatcherOptions<TValue>,\n) {\n const batcher = new AsyncBatcher<TValue>(fn, options)\n return batcher.addItem\n}\n"],"mappings":"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"}
{"version":3,"file":"async-batcher.cjs","names":["Store","#setState","#execute","#clearTimeout","#timeoutId","#getWait","parseFunctionOrValue","AsyncRetryer"],"sources":["../src/async-batcher.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 { OptionalKeys } from './types'\n\nexport interface AsyncBatcherState<TValue> {\n /**\n * Number of batch executions that have resulted in errors\n */\n errorCount: number\n /**\n * Number of batch executions that have been executed\n */\n executeCount: number\n /**\n * Array of items that failed during batch processing\n */\n failedItems: Array<TValue>\n /**\n * Whether the batcher has no items to process (items array is empty)\n */\n isEmpty: boolean\n /**\n * Whether a batch is currently being processed asynchronously\n */\n isExecuting: boolean\n /**\n * Whether the batcher is waiting for the timeout to trigger batch processing\n */\n isPending: boolean\n /**\n * Array of items currently queued for batch processing\n */\n items: Array<TValue>\n /**\n * The result from the most recent batch execution\n */\n lastResult: any\n /**\n * Number of batch executions that have completed (either successfully or with errors)\n */\n settleCount: number\n /**\n * Number of items currently in the batch queue\n */\n size: number\n /**\n * Current processing status - 'idle' when not processing, 'pending' when waiting for timeout, 'executing' when processing, 'populated' when items are present, but no wait is configured\n */\n status: 'idle' | 'pending' | 'executing' | 'populated'\n /**\n * Number of batch executions that have completed successfully\n */\n successCount: number\n /**\n * Total number of items that have failed processing across all batches\n */\n totalItemsFailed: number\n /**\n * Total number of items that have been processed across all batches\n */\n totalItemsProcessed: number\n}\n\nfunction getDefaultAsyncBatcherState<TValue>(): AsyncBatcherState<TValue> {\n return {\n errorCount: 0,\n executeCount: 0,\n failedItems: [],\n isEmpty: true,\n isExecuting: false,\n isPending: false,\n items: [],\n lastResult: undefined,\n settleCount: 0,\n size: 0,\n status: 'idle',\n successCount: 0,\n totalItemsProcessed: 0,\n totalItemsFailed: 0,\n }\n}\n\n/**\n * Options for configuring an AsyncBatcher instance\n */\nexport interface AsyncBatcherOptions<TValue> {\n /**\n * Options for configuring the underlying async retryer\n */\n asyncRetryerOptions?: AsyncRetryerOptions<\n (items: Array<TValue>) => Promise<any>\n >\n /**\n * Custom function to determine if a batch should be processed\n * Return true to process the batch immediately\n */\n getShouldExecute?: (\n items: Array<TValue>,\n batcher: AsyncBatcher<TValue>,\n ) => boolean\n /**\n * Initial state for the async batcher\n */\n initialState?: Partial<AsyncBatcherState<TValue>>\n /**\n * Optional key to identify this async batcher instance.\n * If provided, the async batcher will be identified by this key in the devtools and PacerProvider if applicable.\n */\n key?: string\n /**\n * Maximum number of items in a batch\n * @default Infinity\n */\n maxSize?: number\n /**\n * Optional error handler for when the batch function throws.\n * If provided, the handler will be called with the error, the batch of items that failed, and batcher instance.\n * This can be used alongside throwOnError - the handler will be called before any error is thrown.\n */\n onError?: (\n error: Error,\n batch: Array<TValue>,\n batcher: AsyncBatcher<TValue>,\n ) => void\n /**\n * Callback fired after items are added to the batcher\n */\n onItemsChange?: (batcher: AsyncBatcher<TValue>) => void\n /**\n * Optional callback to call when a batch is settled (completed or failed)\n */\n onSettled?: (batch: Array<TValue>, batcher: AsyncBatcher<TValue>) => void\n /**\n * Optional callback to call when a batch succeeds\n */\n onSuccess?: (\n result: any,\n batch: Array<TValue>,\n batcher: AsyncBatcher<TValue>,\n ) => void\n /**\n * Whether the batcher should start processing immediately\n * @default true\n */\n started?: boolean\n /**\n * Whether to throw errors when they occur.\n * Defaults to true if no onError handler is provided, false if an onError handler is provided.\n * Can be explicitly set to override these defaults.\n */\n throwOnError?: boolean\n /**\n * Maximum time in milliseconds to wait before processing a batch.\n * If the wait duration has elapsed, the batch will be processed.\n * If not provided, the batch will not be triggered by a timeout.\n * @default Infinity\n */\n wait?: number | ((asyncBatcher: AsyncBatcher<TValue>) => number)\n}\n\n/**\n * Utility function for sharing common `AsyncBatcherOptions` options between different `AsyncBatcher` instances.\n *\n */\nexport function asyncBatcherOptions<\n TValue = any,\n TOptions extends Partial<AsyncBatcherOptions<TValue>> = Partial<\n AsyncBatcherOptions<TValue>\n >,\n>(options: TOptions): TOptions {\n return options\n}\n\ntype AsyncBatcherOptionsWithOptionalCallbacks<TValue> = OptionalKeys<\n Required<AsyncBatcherOptions<TValue>>,\n | 'initialState'\n | 'onError'\n | 'onItemsChange'\n | 'onSettled'\n | 'onSuccess'\n | 'key'\n>\n\nconst defaultOptions: AsyncBatcherOptionsWithOptionalCallbacks<any> = {\n asyncRetryerOptions: {\n maxAttempts: 1,\n },\n getShouldExecute: () => false,\n maxSize: Infinity,\n started: true,\n throwOnError: true,\n wait: Infinity,\n}\n\n/**\n * A class that collects items and processes them in batches asynchronously.\n *\n * Async vs Sync Versions:\n * The async version provides advanced features over the sync Batcher:\n * - Returns promises that can be awaited for batch results\n * - Built-in retry support via AsyncRetryer integration\n * - Abort support to cancel in-flight batch executions\n * - Cancel support to prevent pending batches from starting\n * - Comprehensive error handling with onError callbacks and throwOnError control\n * - Detailed execution tracking (success/error/settle counts)\n *\n * The sync Batcher is lighter weight and simpler when you don't need async features,\n * return values, or execution control.\n *\n * What is Batching?\n * Batching is a technique for grouping multiple operations together to be processed as a single unit.\n *\n * The AsyncBatcher provides a flexible way to implement async batching with configurable:\n * - Maximum batch size (number of items per batch)\n * - Time-based batching (process after X milliseconds)\n * - Custom batch processing logic via getShouldExecute\n * - Event callbacks for monitoring batch operations\n * - Error handling for failed batch operations\n *\n * Error Handling:\n * - If an `onError` handler is provided, it will be called with the error, the batch of items that failed, and batcher instance\n * - If `throwOnError` is true (default when no onError handler is provided), the error will be thrown\n * - If `throwOnError` is false (default when onError handler is provided), the error will be swallowed\n * - Both onError and throwOnError can be used together - the handler will be called before any error is thrown\n * - The error state can be checked using the AsyncBatcher instance\n *\n * State Management:\n * - Uses TanStack Store for reactive state management\n * - Use `initialState` to provide initial state values when creating the async batcher\n * - Use `onSuccess` callback to react to successful batch execution and implement custom logic\n * - Use `onError` callback to react to batch execution errors and implement custom error handling\n * - Use `onSettled` callback to react to batch execution completion (success or error) and implement custom logic\n * - Use `onExecute` callback to react to batch execution and implement custom logic\n * - Use `onItemsChange` callback to react to items being added or removed from the batcher\n * - The state includes total items processed, success/error counts, and execution status\n * - State can be accessed via `asyncBatcher.store.state` when using the class directly\n * - When using framework adapters (React/Solid), state is accessed from `asyncBatcher.state`\n *\n * @example\n * ```ts\n * const batcher = new AsyncBatcher<number>(\n * async (items) => {\n * const result = await processItems(items);\n * console.log('Processing batch:', items);\n * return result;\n * },\n * {\n * maxSize: 5,\n * wait: 2000,\n * onSuccess: (result) => console.log('Batch succeeded:', result),\n * onError: (error) => console.error('Batch failed:', error)\n * }\n * );\n *\n * batcher.addItem(1);\n * batcher.addItem(2);\n * // After 2 seconds or when 5 items are added, whichever comes first,\n * // the batch will be processed and the result will be available\n * // batcher.execute() // manually trigger a batch\n * ```\n */\nexport class AsyncBatcher<TValue> {\n readonly store: Store<Readonly<AsyncBatcherState<TValue>>> = new Store(\n getDefaultAsyncBatcherState<TValue>(),\n )\n key: string | undefined\n options: AsyncBatcherOptionsWithOptionalCallbacks<TValue>\n asyncRetryers = new Map<\n number,\n AsyncRetryer<(items: Array<TValue>) => Promise<any>>\n >()\n #timeoutId: ReturnType<typeof setTimeout> | null = null\n\n constructor(\n public fn: (items: Array<TValue>) => Promise<any>,\n initialOptions: AsyncBatcherOptions<TValue>,\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-AsyncBatcher', (event) => {\n if (event.payload.key !== this.key) return\n this.#setState(\n event.payload.store.state as Partial<AsyncBatcherState<TValue>>,\n )\n this.setOptions(\n event.payload.options as Partial<AsyncBatcherOptions<TValue>>,\n )\n })\n }\n }\n\n /**\n * Updates the async batcher options\n */\n setOptions = (newOptions: Partial<AsyncBatcherOptions<TValue>>): void => {\n this.options = { ...this.options, ...newOptions }\n }\n\n #setState = (newState: Partial<AsyncBatcherState<TValue>>): void => {\n this.store.setState((state) => {\n const combinedState = {\n ...state,\n ...newState,\n }\n const { isExecuting, isPending, items } = combinedState\n const size = items.length\n const isEmpty = size === 0\n return {\n ...combinedState,\n isEmpty,\n size,\n status: isExecuting\n ? 'executing'\n : isPending\n ? 'pending'\n : isEmpty\n ? 'idle'\n : 'populated',\n }\n })\n emitChange('AsyncBatcher', this)\n }\n\n #getWait = (): number => {\n return parseFunctionOrValue(this.options.wait, this)\n }\n\n /**\n * Adds an item to the async batcher\n * If the batch size is reached, timeout occurs, or shouldProcess returns true, the batch will be processed\n *\n * @returns The result from the batch function, or undefined if an error occurred and was handled by onError\n *\n * @throws The error from the batch function if no onError handler is configured or throwOnError is true\n */\n addItem = async (item: TValue): Promise<any> => {\n this.#setState({\n items: [...this.store.state.items, item],\n isPending: this.options.wait !== Infinity,\n })\n this.options.onItemsChange?.(this)\n\n const shouldProcess =\n this.store.state.items.length >= this.options.maxSize ||\n this.options.getShouldExecute(this.store.state.items, this)\n\n if (shouldProcess) {\n return await this.#execute()\n } else if (this.options.wait !== Infinity) {\n this.#clearTimeout() // clear any pending timeout to replace it with a new one\n this.#timeoutId = setTimeout(() => this.#execute(), this.#getWait())\n await new Promise((resolve) => setTimeout(resolve, this.#getWait()))\n }\n }\n\n /**\n * Processes the current batch of items asynchronously.\n * This method will automatically be triggered if the batcher is running and any of these conditions are met:\n * - The number of items reaches maxSize\n * - The wait duration has elapsed\n * - The getShouldExecute function returns true upon adding an item\n *\n * You can also call this method manually to process the current batch at any time.\n *\n * @returns A promise that resolves with the result of the batch function, or undefined if an error occurred and was handled by onError\n * @throws The error from the batch function if no onError handler is configured or throwOnError is true\n */\n #execute = async (): Promise<any> => {\n if (this.store.state.items.length === 0) {\n return undefined\n }\n\n const currentExecuteCount = this.store.state.executeCount + 1\n const batch = this.peekAllItems() // copy of the items to be processed (to prevent race conditions)\n this.clear() // Clear items before processing to prevent race conditions\n this.options.onItemsChange?.(this)\n\n this.#setState({ isExecuting: true, executeCount: currentExecuteCount })\n\n try {\n const currentAsyncRetryer = new AsyncRetryer(\n this.fn,\n this.options.asyncRetryerOptions,\n )\n this.asyncRetryers.set(currentExecuteCount, currentAsyncRetryer)\n const result = await currentAsyncRetryer.execute(batch) // EXECUTE\n this.#setState({\n totalItemsProcessed:\n this.store.state.totalItemsProcessed + batch.length,\n lastResult: result,\n successCount: this.store.state.successCount + 1,\n })\n this.options.onSuccess?.(result, batch, this)\n return result\n } catch (error) {\n this.#setState({\n errorCount: this.store.state.errorCount + 1,\n failedItems: [...this.store.state.failedItems, ...batch],\n totalItemsFailed: this.store.state.totalItemsFailed + batch.length,\n })\n this.options.onError?.(error as Error, batch, this)\n if (this.options.throwOnError) {\n throw error\n }\n return undefined\n } finally {\n this.asyncRetryers.delete(currentExecuteCount) // dispose retryer\n this.#setState({\n isExecuting: false,\n settleCount: this.store.state.settleCount + 1,\n })\n this.options.onSettled?.(batch, this)\n }\n }\n\n /**\n * Processes the current batch of items immediately\n */\n flush = async (): Promise<any> => {\n this.#clearTimeout() // clear any pending timeout\n return await this.#execute()\n }\n\n /**\n * Returns a copy of all items in the async batcher\n */\n peekAllItems = (): Array<TValue> => {\n return [...this.store.state.items]\n }\n\n peekFailedItems = (): Array<TValue> => {\n return [...this.store.state.failedItems]\n }\n\n #clearTimeout = (): void => {\n if (this.#timeoutId) {\n clearTimeout(this.#timeoutId)\n this.#timeoutId = null\n }\n }\n\n /**\n * Removes all items from the async batcher\n */\n clear = (): void => {\n this.#setState({ items: [], failedItems: [], isPending: false })\n }\n\n /**\n * Returns the AbortSignal for a specific execution.\n * If no executeCount is provided, returns the signal for the most recent execution.\n * Returns null if no execution is found or not currently executing.\n *\n * @param executeCount - Optional specific execution to get signal for\n * @example\n * ```typescript\n * const batcher = new AsyncBatcher(\n * async (items: string[]) => {\n * const signal = batcher.getAbortSignal()\n * if (signal) {\n * const response = await fetch('/api/batch', {\n * method: 'POST',\n * body: JSON.stringify(items),\n * signal\n * })\n * return response.json()\n * }\n * },\n * { maxSize: 10, wait: 100 }\n * )\n * ```\n */\n getAbortSignal = (executeCount?: number): AbortSignal | null => {\n const count = executeCount ?? this.store.state.executeCount\n const retryer = this.asyncRetryers.get(count)\n return retryer?.getAbortSignal() ?? null\n }\n\n /**\n * Aborts all ongoing executions with the internal abort controllers.\n * Does NOT cancel any pending execution that have not started yet.\n * Does NOT clear out the items.\n */\n abort = (): void => {\n this.asyncRetryers.forEach((retryer) => retryer.abort())\n this.asyncRetryers.clear()\n this.#setState({\n isExecuting: false,\n })\n }\n\n /**\n * Cancels any pending execution that have not started yet.\n * Does NOT abort any execution already in progress.\n * Does NOT clear out the items.\n */\n cancel = (): void => {\n this.#clearTimeout()\n this.#setState({\n isPending: false,\n })\n }\n\n /**\n * Resets the async batcher state to its default values\n */\n reset = (): void => {\n this.#setState(getDefaultAsyncBatcherState<TValue>())\n this.options.onItemsChange?.(this)\n this.asyncRetryers.forEach((retryer) => retryer.reset())\n }\n}\n\n/**\n * Creates an async batcher that processes items in batches.\n *\n * Async vs Sync Versions:\n * The async version provides advanced features over the sync batch function:\n * - Returns promises that can be awaited for batch results\n * - Built-in retry support via AsyncRetryer integration\n * - Abort support to cancel in-flight batch executions\n * - Cancel support to prevent pending batches from starting\n * - Comprehensive error handling with onError callbacks and throwOnError control\n * - Detailed execution tracking (success/error/settle counts)\n *\n * The sync batch function is lighter weight and simpler when you don't need async features,\n * return values, or execution control.\n *\n * What is Batching?\n * Batching is a technique for grouping multiple operations together to be processed as a single unit.\n *\n * Configuration Options:\n * - `maxSize`: Maximum number of items per batch (default: Infinity)\n * - `wait`: Time to wait before processing batch (default: Infinity)\n * - `getShouldExecute`: Custom logic to trigger batch processing\n * - `asyncRetryerOptions`: Configure retry behavior for batch executions\n * - `started`: Whether to start processing immediately (default: true)\n *\n * Error Handling:\n * - If an `onError` handler is provided, it will be called with the error, the batch of items that failed, and batcher instance\n * - If `throwOnError` is true (default when no onError handler is provided), the error will be thrown\n * - If `throwOnError` is false (default when onError handler is provided), the error will be swallowed\n * - Both onError and throwOnError can be used together - the handler will be called before any error is thrown\n * - The error state can be checked using the underlying AsyncBatcher instance\n *\n * State Management:\n * - Uses TanStack Store for reactive state management\n * - Use `initialState` to provide initial state values when creating the async batcher\n * - Use `onSuccess` callback to react to successful batch execution and implement custom logic\n * - Use `onError` callback to react to batch execution errors and implement custom error handling\n * - Use `onSettled` callback to react to batch execution completion (success or error) and implement custom logic\n * - Use `onItemsChange` callback to react to items being added or removed from the batcher\n * - The state includes total items processed, success/error counts, and execution status\n * - State can be accessed via the underlying AsyncBatcher instance's `store.state` property\n * - When using framework adapters (React/Solid), state is accessed from the hook's state property\n *\n * @example\n * ```ts\n * const batchItems = asyncBatch<number>(\n * async (items) => {\n * const result = await processApiCall(items);\n * console.log('Processing:', items);\n * return result;\n * },\n * {\n * maxSize: 3,\n * wait: 1000,\n * onSuccess: (result) => console.log('Batch succeeded:', result),\n * onError: (error) => console.error('Batch failed:', error)\n * }\n * );\n *\n * batchItems(1);\n * batchItems(2);\n * batchItems(3); // Triggers batch processing\n * ```\n */\nexport function asyncBatch<TValue>(\n fn: (items: Array<TValue>) => Promise<any>,\n options: AsyncBatcherOptions<TValue>,\n) {\n const batcher = new AsyncBatcher<TValue>(fn, options)\n return batcher.addItem\n}\n"],"mappings":"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"}

@@ -1,1 +0,1 @@

{"version":3,"file":"async-batcher.js","names":["#setState","#execute","#clearTimeout","#timeoutId","#getWait"],"sources":["../src/async-batcher.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 { OptionalKeys } from './types'\n\nexport interface AsyncBatcherState<TValue> {\n /**\n * Number of batch executions that have resulted in errors\n */\n errorCount: number\n /**\n * Number of batch executions that have been executed\n */\n executeCount: number\n /**\n * Array of items that failed during batch processing\n */\n failedItems: Array<TValue>\n /**\n * Whether the batcher has no items to process (items array is empty)\n */\n isEmpty: boolean\n /**\n * Whether a batch is currently being processed asynchronously\n */\n isExecuting: boolean\n /**\n * Whether the batcher is waiting for the timeout to trigger batch processing\n */\n isPending: boolean\n /**\n * Array of items currently queued for batch processing\n */\n items: Array<TValue>\n /**\n * The result from the most recent batch execution\n */\n lastResult: any\n /**\n * Number of batch executions that have completed (either successfully or with errors)\n */\n settleCount: number\n /**\n * Number of items currently in the batch queue\n */\n size: number\n /**\n * Current processing status - 'idle' when not processing, 'pending' when waiting for timeout, 'executing' when processing, 'populated' when items are present, but no wait is configured\n */\n status: 'idle' | 'pending' | 'executing' | 'populated'\n /**\n * Number of batch executions that have completed successfully\n */\n successCount: number\n /**\n * Total number of items that have failed processing across all batches\n */\n totalItemsFailed: number\n /**\n * Total number of items that have been processed across all batches\n */\n totalItemsProcessed: number\n}\n\nfunction getDefaultAsyncBatcherState<TValue>(): AsyncBatcherState<TValue> {\n return {\n errorCount: 0,\n executeCount: 0,\n failedItems: [],\n isEmpty: true,\n isExecuting: false,\n isPending: false,\n items: [],\n lastResult: undefined,\n settleCount: 0,\n size: 0,\n status: 'idle',\n successCount: 0,\n totalItemsProcessed: 0,\n totalItemsFailed: 0,\n }\n}\n\n/**\n * Options for configuring an AsyncBatcher instance\n */\nexport interface AsyncBatcherOptions<TValue> {\n /**\n * Options for configuring the underlying async retryer\n */\n asyncRetryerOptions?: AsyncRetryerOptions<\n (items: Array<TValue>) => Promise<any>\n >\n /**\n * Custom function to determine if a batch should be processed\n * Return true to process the batch immediately\n */\n getShouldExecute?: (\n items: Array<TValue>,\n batcher: AsyncBatcher<TValue>,\n ) => boolean\n /**\n * Initial state for the async batcher\n */\n initialState?: Partial<AsyncBatcherState<TValue>>\n /**\n * Optional key to identify this async batcher instance.\n * If provided, the async batcher will be identified by this key in the devtools and PacerProvider if applicable.\n */\n key?: string\n /**\n * Maximum number of items in a batch\n * @default Infinity\n */\n maxSize?: number\n /**\n * Optional error handler for when the batch function throws.\n * If provided, the handler will be called with the error, the batch of items that failed, and batcher instance.\n * This can be used alongside throwOnError - the handler will be called before any error is thrown.\n */\n onError?: (\n error: Error,\n batch: Array<TValue>,\n batcher: AsyncBatcher<TValue>,\n ) => void\n /**\n * Callback fired after items are added to the batcher\n */\n onItemsChange?: (batcher: AsyncBatcher<TValue>) => void\n /**\n * Optional callback to call when a batch is settled (completed or failed)\n */\n onSettled?: (batch: Array<TValue>, batcher: AsyncBatcher<TValue>) => void\n /**\n * Optional callback to call when a batch succeeds\n */\n onSuccess?: (\n result: any,\n batch: Array<TValue>,\n batcher: AsyncBatcher<TValue>,\n ) => void\n /**\n * Whether the batcher should start processing immediately\n * @default true\n */\n started?: boolean\n /**\n * Whether to throw errors when they occur.\n * Defaults to true if no onError handler is provided, false if an onError handler is provided.\n * Can be explicitly set to override these defaults.\n */\n throwOnError?: boolean\n /**\n * Maximum time in milliseconds to wait before processing a batch.\n * If the wait duration has elapsed, the batch will be processed.\n * If not provided, the batch will not be triggered by a timeout.\n * @default Infinity\n */\n wait?: number | ((asyncBatcher: AsyncBatcher<TValue>) => number)\n}\n\n/**\n * Utility function for sharing common `AsyncBatcherOptions` options between different `AsyncBatcher` instances.\n *\n */\nexport function asyncBatcherOptions<\n TValue = any,\n TOptions extends Partial<AsyncBatcherOptions<TValue>> = Partial<\n AsyncBatcherOptions<TValue>\n >,\n>(options: TOptions): TOptions {\n return options\n}\n\ntype AsyncBatcherOptionsWithOptionalCallbacks<TValue> = OptionalKeys<\n Required<AsyncBatcherOptions<TValue>>,\n | 'initialState'\n | 'onError'\n | 'onItemsChange'\n | 'onSettled'\n | 'onSuccess'\n | 'key'\n>\n\nconst defaultOptions: AsyncBatcherOptionsWithOptionalCallbacks<any> = {\n asyncRetryerOptions: {\n maxAttempts: 1,\n },\n getShouldExecute: () => false,\n maxSize: Infinity,\n started: true,\n throwOnError: true,\n wait: Infinity,\n}\n\n/**\n * A class that collects items and processes them in batches asynchronously.\n *\n * Async vs Sync Versions:\n * The async version provides advanced features over the sync Batcher:\n * - Returns promises that can be awaited for batch results\n * - Built-in retry support via AsyncRetryer integration\n * - Abort support to cancel in-flight batch executions\n * - Cancel support to prevent pending batches from starting\n * - Comprehensive error handling with onError callbacks and throwOnError control\n * - Detailed execution tracking (success/error/settle counts)\n *\n * The sync Batcher is lighter weight and simpler when you don't need async features,\n * return values, or execution control.\n *\n * What is Batching?\n * Batching is a technique for grouping multiple operations together to be processed as a single unit.\n *\n * The AsyncBatcher provides a flexible way to implement async batching with configurable:\n * - Maximum batch size (number of items per batch)\n * - Time-based batching (process after X milliseconds)\n * - Custom batch processing logic via getShouldExecute\n * - Event callbacks for monitoring batch operations\n * - Error handling for failed batch operations\n *\n * Error Handling:\n * - If an `onError` handler is provided, it will be called with the error, the batch of items that failed, and batcher instance\n * - If `throwOnError` is true (default when no onError handler is provided), the error will be thrown\n * - If `throwOnError` is false (default when onError handler is provided), the error will be swallowed\n * - Both onError and throwOnError can be used together - the handler will be called before any error is thrown\n * - The error state can be checked using the AsyncBatcher instance\n *\n * State Management:\n * - Uses TanStack Store for reactive state management\n * - Use `initialState` to provide initial state values when creating the async batcher\n * - Use `onSuccess` callback to react to successful batch execution and implement custom logic\n * - Use `onError` callback to react to batch execution errors and implement custom error handling\n * - Use `onSettled` callback to react to batch execution completion (success or error) and implement custom logic\n * - Use `onExecute` callback to react to batch execution and implement custom logic\n * - Use `onItemsChange` callback to react to items being added or removed from the batcher\n * - The state includes total items processed, success/error counts, and execution status\n * - State can be accessed via `asyncBatcher.store.state` when using the class directly\n * - When using framework adapters (React/Solid), state is accessed from `asyncBatcher.state`\n *\n * @example\n * ```ts\n * const batcher = new AsyncBatcher<number>(\n * async (items) => {\n * const result = await processItems(items);\n * console.log('Processing batch:', items);\n * return result;\n * },\n * {\n * maxSize: 5,\n * wait: 2000,\n * onSuccess: (result) => console.log('Batch succeeded:', result),\n * onError: (error) => console.error('Batch failed:', error)\n * }\n * );\n *\n * batcher.addItem(1);\n * batcher.addItem(2);\n * // After 2 seconds or when 5 items are added, whichever comes first,\n * // the batch will be processed and the result will be available\n * // batcher.execute() // manually trigger a batch\n * ```\n */\nexport class AsyncBatcher<TValue> {\n readonly store: Store<Readonly<AsyncBatcherState<TValue>>> = new Store(\n getDefaultAsyncBatcherState<TValue>(),\n )\n key: string | undefined\n options: AsyncBatcherOptionsWithOptionalCallbacks<TValue>\n asyncRetryers = new Map<\n number,\n AsyncRetryer<(items: Array<TValue>) => Promise<any>>\n >()\n #timeoutId: NodeJS.Timeout | null = null\n\n constructor(\n public fn: (items: Array<TValue>) => Promise<any>,\n initialOptions: AsyncBatcherOptions<TValue>,\n ) {\n this.key = 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-AsyncBatcher', (event) => {\n if (event.payload.key !== this.key) return\n this.#setState(\n event.payload.store.state as Partial<AsyncBatcherState<TValue>>,\n )\n this.setOptions(\n event.payload.options as Partial<AsyncBatcherOptions<TValue>>,\n )\n })\n }\n }\n\n /**\n * Updates the async batcher options\n */\n setOptions = (newOptions: Partial<AsyncBatcherOptions<TValue>>): void => {\n this.options = { ...this.options, ...newOptions }\n }\n\n #setState = (newState: Partial<AsyncBatcherState<TValue>>): void => {\n this.store.setState((state) => {\n const combinedState = {\n ...state,\n ...newState,\n }\n const { isExecuting, isPending, items } = combinedState\n const size = items.length\n const isEmpty = size === 0\n return {\n ...combinedState,\n isEmpty,\n size,\n status: isExecuting\n ? 'executing'\n : isPending\n ? 'pending'\n : isEmpty\n ? 'idle'\n : 'populated',\n }\n })\n emitChange('AsyncBatcher', this)\n }\n\n #getWait = (): number => {\n return parseFunctionOrValue(this.options.wait, this)\n }\n\n /**\n * Adds an item to the async batcher\n * If the batch size is reached, timeout occurs, or shouldProcess returns true, the batch will be processed\n *\n * @returns The result from the batch function, or undefined if an error occurred and was handled by onError\n *\n * @throws The error from the batch function if no onError handler is configured or throwOnError is true\n */\n addItem = async (item: TValue): Promise<any> => {\n this.#setState({\n items: [...this.store.state.items, item],\n isPending: this.options.wait !== Infinity,\n })\n this.options.onItemsChange?.(this)\n\n const shouldProcess =\n this.store.state.items.length >= this.options.maxSize ||\n this.options.getShouldExecute(this.store.state.items, this)\n\n if (shouldProcess) {\n return await this.#execute()\n } else if (this.options.wait !== Infinity) {\n this.#clearTimeout() // clear any pending timeout to replace it with a new one\n this.#timeoutId = setTimeout(() => this.#execute(), this.#getWait())\n await new Promise((resolve) => setTimeout(resolve, this.#getWait()))\n }\n }\n\n /**\n * Processes the current batch of items asynchronously.\n * This method will automatically be triggered if the batcher is running and any of these conditions are met:\n * - The number of items reaches maxSize\n * - The wait duration has elapsed\n * - The getShouldExecute function returns true upon adding an item\n *\n * You can also call this method manually to process the current batch at any time.\n *\n * @returns A promise that resolves with the result of the batch function, or undefined if an error occurred and was handled by onError\n * @throws The error from the batch function if no onError handler is configured or throwOnError is true\n */\n #execute = async (): Promise<any> => {\n if (this.store.state.items.length === 0) {\n return undefined\n }\n\n const currentExecuteCount = this.store.state.executeCount + 1\n const batch = this.peekAllItems() // copy of the items to be processed (to prevent race conditions)\n this.clear() // Clear items before processing to prevent race conditions\n this.options.onItemsChange?.(this)\n\n this.#setState({ isExecuting: true, executeCount: currentExecuteCount })\n\n try {\n const currentAsyncRetryer = new AsyncRetryer(\n this.fn,\n this.options.asyncRetryerOptions,\n )\n this.asyncRetryers.set(currentExecuteCount, currentAsyncRetryer)\n const result = await currentAsyncRetryer.execute(batch) // EXECUTE\n this.#setState({\n totalItemsProcessed:\n this.store.state.totalItemsProcessed + batch.length,\n lastResult: result,\n successCount: this.store.state.successCount + 1,\n })\n this.options.onSuccess?.(result, batch, this)\n return result\n } catch (error) {\n this.#setState({\n errorCount: this.store.state.errorCount + 1,\n failedItems: [...this.store.state.failedItems, ...batch],\n totalItemsFailed: this.store.state.totalItemsFailed + batch.length,\n })\n this.options.onError?.(error as Error, batch, this)\n if (this.options.throwOnError) {\n throw error\n }\n return undefined\n } finally {\n this.asyncRetryers.delete(currentExecuteCount) // dispose retryer\n this.#setState({\n isExecuting: false,\n settleCount: this.store.state.settleCount + 1,\n })\n this.options.onSettled?.(batch, this)\n }\n }\n\n /**\n * Processes the current batch of items immediately\n */\n flush = async (): Promise<any> => {\n this.#clearTimeout() // clear any pending timeout\n return await this.#execute()\n }\n\n /**\n * Returns a copy of all items in the async batcher\n */\n peekAllItems = (): Array<TValue> => {\n return [...this.store.state.items]\n }\n\n peekFailedItems = (): Array<TValue> => {\n return [...this.store.state.failedItems]\n }\n\n #clearTimeout = (): void => {\n if (this.#timeoutId) {\n clearTimeout(this.#timeoutId)\n this.#timeoutId = null\n }\n }\n\n /**\n * Removes all items from the async batcher\n */\n clear = (): void => {\n this.#setState({ items: [], failedItems: [], isPending: false })\n }\n\n /**\n * Returns the AbortSignal for a specific execution.\n * If no executeCount is provided, returns the signal for the most recent execution.\n * Returns null if no execution is found or not currently executing.\n *\n * @param executeCount - Optional specific execution to get signal for\n * @example\n * ```typescript\n * const batcher = new AsyncBatcher(\n * async (items: string[]) => {\n * const signal = batcher.getAbortSignal()\n * if (signal) {\n * const response = await fetch('/api/batch', {\n * method: 'POST',\n * body: JSON.stringify(items),\n * signal\n * })\n * return response.json()\n * }\n * },\n * { maxSize: 10, wait: 100 }\n * )\n * ```\n */\n getAbortSignal = (executeCount?: number): AbortSignal | null => {\n const count = executeCount ?? this.store.state.executeCount\n const retryer = this.asyncRetryers.get(count)\n return retryer?.getAbortSignal() ?? null\n }\n\n /**\n * Aborts all ongoing executions with the internal abort controllers.\n * Does NOT cancel any pending execution that have not started yet.\n * Does NOT clear out the items.\n */\n abort = (): void => {\n this.asyncRetryers.forEach((retryer) => retryer.abort())\n this.asyncRetryers.clear()\n this.#setState({\n isExecuting: false,\n })\n }\n\n /**\n * Cancels any pending execution that have not started yet.\n * Does NOT abort any execution already in progress.\n * Does NOT clear out the items.\n */\n cancel = (): void => {\n this.#clearTimeout()\n this.#setState({\n isPending: false,\n })\n }\n\n /**\n * Resets the async batcher state to its default values\n */\n reset = (): void => {\n this.#setState(getDefaultAsyncBatcherState<TValue>())\n this.options.onItemsChange?.(this)\n this.asyncRetryers.forEach((retryer) => retryer.reset())\n }\n}\n\n/**\n * Creates an async batcher that processes items in batches.\n *\n * Async vs Sync Versions:\n * The async version provides advanced features over the sync batch function:\n * - Returns promises that can be awaited for batch results\n * - Built-in retry support via AsyncRetryer integration\n * - Abort support to cancel in-flight batch executions\n * - Cancel support to prevent pending batches from starting\n * - Comprehensive error handling with onError callbacks and throwOnError control\n * - Detailed execution tracking (success/error/settle counts)\n *\n * The sync batch function is lighter weight and simpler when you don't need async features,\n * return values, or execution control.\n *\n * What is Batching?\n * Batching is a technique for grouping multiple operations together to be processed as a single unit.\n *\n * Configuration Options:\n * - `maxSize`: Maximum number of items per batch (default: Infinity)\n * - `wait`: Time to wait before processing batch (default: Infinity)\n * - `getShouldExecute`: Custom logic to trigger batch processing\n * - `asyncRetryerOptions`: Configure retry behavior for batch executions\n * - `started`: Whether to start processing immediately (default: true)\n *\n * Error Handling:\n * - If an `onError` handler is provided, it will be called with the error, the batch of items that failed, and batcher instance\n * - If `throwOnError` is true (default when no onError handler is provided), the error will be thrown\n * - If `throwOnError` is false (default when onError handler is provided), the error will be swallowed\n * - Both onError and throwOnError can be used together - the handler will be called before any error is thrown\n * - The error state can be checked using the underlying AsyncBatcher instance\n *\n * State Management:\n * - Uses TanStack Store for reactive state management\n * - Use `initialState` to provide initial state values when creating the async batcher\n * - Use `onSuccess` callback to react to successful batch execution and implement custom logic\n * - Use `onError` callback to react to batch execution errors and implement custom error handling\n * - Use `onSettled` callback to react to batch execution completion (success or error) and implement custom logic\n * - Use `onItemsChange` callback to react to items being added or removed from the batcher\n * - The state includes total items processed, success/error counts, and execution status\n * - State can be accessed via the underlying AsyncBatcher instance's `store.state` property\n * - When using framework adapters (React/Solid), state is accessed from the hook's state property\n *\n * @example\n * ```ts\n * const batchItems = asyncBatch<number>(\n * async (items) => {\n * const result = await processApiCall(items);\n * console.log('Processing:', items);\n * return result;\n * },\n * {\n * maxSize: 3,\n * wait: 1000,\n * onSuccess: (result) => console.log('Batch succeeded:', result),\n * onError: (error) => console.error('Batch failed:', error)\n * }\n * );\n *\n * batchItems(1);\n * batchItems(2);\n * batchItems(3); // Triggers batch processing\n * ```\n */\nexport function asyncBatch<TValue>(\n fn: (items: Array<TValue>) => Promise<any>,\n options: AsyncBatcherOptions<TValue>,\n) {\n const batcher = new AsyncBatcher<TValue>(fn, options)\n return batcher.addItem\n}\n"],"mappings":"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"}
{"version":3,"file":"async-batcher.js","names":["#setState","#execute","#clearTimeout","#timeoutId","#getWait"],"sources":["../src/async-batcher.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 { OptionalKeys } from './types'\n\nexport interface AsyncBatcherState<TValue> {\n /**\n * Number of batch executions that have resulted in errors\n */\n errorCount: number\n /**\n * Number of batch executions that have been executed\n */\n executeCount: number\n /**\n * Array of items that failed during batch processing\n */\n failedItems: Array<TValue>\n /**\n * Whether the batcher has no items to process (items array is empty)\n */\n isEmpty: boolean\n /**\n * Whether a batch is currently being processed asynchronously\n */\n isExecuting: boolean\n /**\n * Whether the batcher is waiting for the timeout to trigger batch processing\n */\n isPending: boolean\n /**\n * Array of items currently queued for batch processing\n */\n items: Array<TValue>\n /**\n * The result from the most recent batch execution\n */\n lastResult: any\n /**\n * Number of batch executions that have completed (either successfully or with errors)\n */\n settleCount: number\n /**\n * Number of items currently in the batch queue\n */\n size: number\n /**\n * Current processing status - 'idle' when not processing, 'pending' when waiting for timeout, 'executing' when processing, 'populated' when items are present, but no wait is configured\n */\n status: 'idle' | 'pending' | 'executing' | 'populated'\n /**\n * Number of batch executions that have completed successfully\n */\n successCount: number\n /**\n * Total number of items that have failed processing across all batches\n */\n totalItemsFailed: number\n /**\n * Total number of items that have been processed across all batches\n */\n totalItemsProcessed: number\n}\n\nfunction getDefaultAsyncBatcherState<TValue>(): AsyncBatcherState<TValue> {\n return {\n errorCount: 0,\n executeCount: 0,\n failedItems: [],\n isEmpty: true,\n isExecuting: false,\n isPending: false,\n items: [],\n lastResult: undefined,\n settleCount: 0,\n size: 0,\n status: 'idle',\n successCount: 0,\n totalItemsProcessed: 0,\n totalItemsFailed: 0,\n }\n}\n\n/**\n * Options for configuring an AsyncBatcher instance\n */\nexport interface AsyncBatcherOptions<TValue> {\n /**\n * Options for configuring the underlying async retryer\n */\n asyncRetryerOptions?: AsyncRetryerOptions<\n (items: Array<TValue>) => Promise<any>\n >\n /**\n * Custom function to determine if a batch should be processed\n * Return true to process the batch immediately\n */\n getShouldExecute?: (\n items: Array<TValue>,\n batcher: AsyncBatcher<TValue>,\n ) => boolean\n /**\n * Initial state for the async batcher\n */\n initialState?: Partial<AsyncBatcherState<TValue>>\n /**\n * Optional key to identify this async batcher instance.\n * If provided, the async batcher will be identified by this key in the devtools and PacerProvider if applicable.\n */\n key?: string\n /**\n * Maximum number of items in a batch\n * @default Infinity\n */\n maxSize?: number\n /**\n * Optional error handler for when the batch function throws.\n * If provided, the handler will be called with the error, the batch of items that failed, and batcher instance.\n * This can be used alongside throwOnError - the handler will be called before any error is thrown.\n */\n onError?: (\n error: Error,\n batch: Array<TValue>,\n batcher: AsyncBatcher<TValue>,\n ) => void\n /**\n * Callback fired after items are added to the batcher\n */\n onItemsChange?: (batcher: AsyncBatcher<TValue>) => void\n /**\n * Optional callback to call when a batch is settled (completed or failed)\n */\n onSettled?: (batch: Array<TValue>, batcher: AsyncBatcher<TValue>) => void\n /**\n * Optional callback to call when a batch succeeds\n */\n onSuccess?: (\n result: any,\n batch: Array<TValue>,\n batcher: AsyncBatcher<TValue>,\n ) => void\n /**\n * Whether the batcher should start processing immediately\n * @default true\n */\n started?: boolean\n /**\n * Whether to throw errors when they occur.\n * Defaults to true if no onError handler is provided, false if an onError handler is provided.\n * Can be explicitly set to override these defaults.\n */\n throwOnError?: boolean\n /**\n * Maximum time in milliseconds to wait before processing a batch.\n * If the wait duration has elapsed, the batch will be processed.\n * If not provided, the batch will not be triggered by a timeout.\n * @default Infinity\n */\n wait?: number | ((asyncBatcher: AsyncBatcher<TValue>) => number)\n}\n\n/**\n * Utility function for sharing common `AsyncBatcherOptions` options between different `AsyncBatcher` instances.\n *\n */\nexport function asyncBatcherOptions<\n TValue = any,\n TOptions extends Partial<AsyncBatcherOptions<TValue>> = Partial<\n AsyncBatcherOptions<TValue>\n >,\n>(options: TOptions): TOptions {\n return options\n}\n\ntype AsyncBatcherOptionsWithOptionalCallbacks<TValue> = OptionalKeys<\n Required<AsyncBatcherOptions<TValue>>,\n | 'initialState'\n | 'onError'\n | 'onItemsChange'\n | 'onSettled'\n | 'onSuccess'\n | 'key'\n>\n\nconst defaultOptions: AsyncBatcherOptionsWithOptionalCallbacks<any> = {\n asyncRetryerOptions: {\n maxAttempts: 1,\n },\n getShouldExecute: () => false,\n maxSize: Infinity,\n started: true,\n throwOnError: true,\n wait: Infinity,\n}\n\n/**\n * A class that collects items and processes them in batches asynchronously.\n *\n * Async vs Sync Versions:\n * The async version provides advanced features over the sync Batcher:\n * - Returns promises that can be awaited for batch results\n * - Built-in retry support via AsyncRetryer integration\n * - Abort support to cancel in-flight batch executions\n * - Cancel support to prevent pending batches from starting\n * - Comprehensive error handling with onError callbacks and throwOnError control\n * - Detailed execution tracking (success/error/settle counts)\n *\n * The sync Batcher is lighter weight and simpler when you don't need async features,\n * return values, or execution control.\n *\n * What is Batching?\n * Batching is a technique for grouping multiple operations together to be processed as a single unit.\n *\n * The AsyncBatcher provides a flexible way to implement async batching with configurable:\n * - Maximum batch size (number of items per batch)\n * - Time-based batching (process after X milliseconds)\n * - Custom batch processing logic via getShouldExecute\n * - Event callbacks for monitoring batch operations\n * - Error handling for failed batch operations\n *\n * Error Handling:\n * - If an `onError` handler is provided, it will be called with the error, the batch of items that failed, and batcher instance\n * - If `throwOnError` is true (default when no onError handler is provided), the error will be thrown\n * - If `throwOnError` is false (default when onError handler is provided), the error will be swallowed\n * - Both onError and throwOnError can be used together - the handler will be called before any error is thrown\n * - The error state can be checked using the AsyncBatcher instance\n *\n * State Management:\n * - Uses TanStack Store for reactive state management\n * - Use `initialState` to provide initial state values when creating the async batcher\n * - Use `onSuccess` callback to react to successful batch execution and implement custom logic\n * - Use `onError` callback to react to batch execution errors and implement custom error handling\n * - Use `onSettled` callback to react to batch execution completion (success or error) and implement custom logic\n * - Use `onExecute` callback to react to batch execution and implement custom logic\n * - Use `onItemsChange` callback to react to items being added or removed from the batcher\n * - The state includes total items processed, success/error counts, and execution status\n * - State can be accessed via `asyncBatcher.store.state` when using the class directly\n * - When using framework adapters (React/Solid), state is accessed from `asyncBatcher.state`\n *\n * @example\n * ```ts\n * const batcher = new AsyncBatcher<number>(\n * async (items) => {\n * const result = await processItems(items);\n * console.log('Processing batch:', items);\n * return result;\n * },\n * {\n * maxSize: 5,\n * wait: 2000,\n * onSuccess: (result) => console.log('Batch succeeded:', result),\n * onError: (error) => console.error('Batch failed:', error)\n * }\n * );\n *\n * batcher.addItem(1);\n * batcher.addItem(2);\n * // After 2 seconds or when 5 items are added, whichever comes first,\n * // the batch will be processed and the result will be available\n * // batcher.execute() // manually trigger a batch\n * ```\n */\nexport class AsyncBatcher<TValue> {\n readonly store: Store<Readonly<AsyncBatcherState<TValue>>> = new Store(\n getDefaultAsyncBatcherState<TValue>(),\n )\n key: string | undefined\n options: AsyncBatcherOptionsWithOptionalCallbacks<TValue>\n asyncRetryers = new Map<\n number,\n AsyncRetryer<(items: Array<TValue>) => Promise<any>>\n >()\n #timeoutId: ReturnType<typeof setTimeout> | null = null\n\n constructor(\n public fn: (items: Array<TValue>) => Promise<any>,\n initialOptions: AsyncBatcherOptions<TValue>,\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-AsyncBatcher', (event) => {\n if (event.payload.key !== this.key) return\n this.#setState(\n event.payload.store.state as Partial<AsyncBatcherState<TValue>>,\n )\n this.setOptions(\n event.payload.options as Partial<AsyncBatcherOptions<TValue>>,\n )\n })\n }\n }\n\n /**\n * Updates the async batcher options\n */\n setOptions = (newOptions: Partial<AsyncBatcherOptions<TValue>>): void => {\n this.options = { ...this.options, ...newOptions }\n }\n\n #setState = (newState: Partial<AsyncBatcherState<TValue>>): void => {\n this.store.setState((state) => {\n const combinedState = {\n ...state,\n ...newState,\n }\n const { isExecuting, isPending, items } = combinedState\n const size = items.length\n const isEmpty = size === 0\n return {\n ...combinedState,\n isEmpty,\n size,\n status: isExecuting\n ? 'executing'\n : isPending\n ? 'pending'\n : isEmpty\n ? 'idle'\n : 'populated',\n }\n })\n emitChange('AsyncBatcher', this)\n }\n\n #getWait = (): number => {\n return parseFunctionOrValue(this.options.wait, this)\n }\n\n /**\n * Adds an item to the async batcher\n * If the batch size is reached, timeout occurs, or shouldProcess returns true, the batch will be processed\n *\n * @returns The result from the batch function, or undefined if an error occurred and was handled by onError\n *\n * @throws The error from the batch function if no onError handler is configured or throwOnError is true\n */\n addItem = async (item: TValue): Promise<any> => {\n this.#setState({\n items: [...this.store.state.items, item],\n isPending: this.options.wait !== Infinity,\n })\n this.options.onItemsChange?.(this)\n\n const shouldProcess =\n this.store.state.items.length >= this.options.maxSize ||\n this.options.getShouldExecute(this.store.state.items, this)\n\n if (shouldProcess) {\n return await this.#execute()\n } else if (this.options.wait !== Infinity) {\n this.#clearTimeout() // clear any pending timeout to replace it with a new one\n this.#timeoutId = setTimeout(() => this.#execute(), this.#getWait())\n await new Promise((resolve) => setTimeout(resolve, this.#getWait()))\n }\n }\n\n /**\n * Processes the current batch of items asynchronously.\n * This method will automatically be triggered if the batcher is running and any of these conditions are met:\n * - The number of items reaches maxSize\n * - The wait duration has elapsed\n * - The getShouldExecute function returns true upon adding an item\n *\n * You can also call this method manually to process the current batch at any time.\n *\n * @returns A promise that resolves with the result of the batch function, or undefined if an error occurred and was handled by onError\n * @throws The error from the batch function if no onError handler is configured or throwOnError is true\n */\n #execute = async (): Promise<any> => {\n if (this.store.state.items.length === 0) {\n return undefined\n }\n\n const currentExecuteCount = this.store.state.executeCount + 1\n const batch = this.peekAllItems() // copy of the items to be processed (to prevent race conditions)\n this.clear() // Clear items before processing to prevent race conditions\n this.options.onItemsChange?.(this)\n\n this.#setState({ isExecuting: true, executeCount: currentExecuteCount })\n\n try {\n const currentAsyncRetryer = new AsyncRetryer(\n this.fn,\n this.options.asyncRetryerOptions,\n )\n this.asyncRetryers.set(currentExecuteCount, currentAsyncRetryer)\n const result = await currentAsyncRetryer.execute(batch) // EXECUTE\n this.#setState({\n totalItemsProcessed:\n this.store.state.totalItemsProcessed + batch.length,\n lastResult: result,\n successCount: this.store.state.successCount + 1,\n })\n this.options.onSuccess?.(result, batch, this)\n return result\n } catch (error) {\n this.#setState({\n errorCount: this.store.state.errorCount + 1,\n failedItems: [...this.store.state.failedItems, ...batch],\n totalItemsFailed: this.store.state.totalItemsFailed + batch.length,\n })\n this.options.onError?.(error as Error, batch, this)\n if (this.options.throwOnError) {\n throw error\n }\n return undefined\n } finally {\n this.asyncRetryers.delete(currentExecuteCount) // dispose retryer\n this.#setState({\n isExecuting: false,\n settleCount: this.store.state.settleCount + 1,\n })\n this.options.onSettled?.(batch, this)\n }\n }\n\n /**\n * Processes the current batch of items immediately\n */\n flush = async (): Promise<any> => {\n this.#clearTimeout() // clear any pending timeout\n return await this.#execute()\n }\n\n /**\n * Returns a copy of all items in the async batcher\n */\n peekAllItems = (): Array<TValue> => {\n return [...this.store.state.items]\n }\n\n peekFailedItems = (): Array<TValue> => {\n return [...this.store.state.failedItems]\n }\n\n #clearTimeout = (): void => {\n if (this.#timeoutId) {\n clearTimeout(this.#timeoutId)\n this.#timeoutId = null\n }\n }\n\n /**\n * Removes all items from the async batcher\n */\n clear = (): void => {\n this.#setState({ items: [], failedItems: [], isPending: false })\n }\n\n /**\n * Returns the AbortSignal for a specific execution.\n * If no executeCount is provided, returns the signal for the most recent execution.\n * Returns null if no execution is found or not currently executing.\n *\n * @param executeCount - Optional specific execution to get signal for\n * @example\n * ```typescript\n * const batcher = new AsyncBatcher(\n * async (items: string[]) => {\n * const signal = batcher.getAbortSignal()\n * if (signal) {\n * const response = await fetch('/api/batch', {\n * method: 'POST',\n * body: JSON.stringify(items),\n * signal\n * })\n * return response.json()\n * }\n * },\n * { maxSize: 10, wait: 100 }\n * )\n * ```\n */\n getAbortSignal = (executeCount?: number): AbortSignal | null => {\n const count = executeCount ?? this.store.state.executeCount\n const retryer = this.asyncRetryers.get(count)\n return retryer?.getAbortSignal() ?? null\n }\n\n /**\n * Aborts all ongoing executions with the internal abort controllers.\n * Does NOT cancel any pending execution that have not started yet.\n * Does NOT clear out the items.\n */\n abort = (): void => {\n this.asyncRetryers.forEach((retryer) => retryer.abort())\n this.asyncRetryers.clear()\n this.#setState({\n isExecuting: false,\n })\n }\n\n /**\n * Cancels any pending execution that have not started yet.\n * Does NOT abort any execution already in progress.\n * Does NOT clear out the items.\n */\n cancel = (): void => {\n this.#clearTimeout()\n this.#setState({\n isPending: false,\n })\n }\n\n /**\n * Resets the async batcher state to its default values\n */\n reset = (): void => {\n this.#setState(getDefaultAsyncBatcherState<TValue>())\n this.options.onItemsChange?.(this)\n this.asyncRetryers.forEach((retryer) => retryer.reset())\n }\n}\n\n/**\n * Creates an async batcher that processes items in batches.\n *\n * Async vs Sync Versions:\n * The async version provides advanced features over the sync batch function:\n * - Returns promises that can be awaited for batch results\n * - Built-in retry support via AsyncRetryer integration\n * - Abort support to cancel in-flight batch executions\n * - Cancel support to prevent pending batches from starting\n * - Comprehensive error handling with onError callbacks and throwOnError control\n * - Detailed execution tracking (success/error/settle counts)\n *\n * The sync batch function is lighter weight and simpler when you don't need async features,\n * return values, or execution control.\n *\n * What is Batching?\n * Batching is a technique for grouping multiple operations together to be processed as a single unit.\n *\n * Configuration Options:\n * - `maxSize`: Maximum number of items per batch (default: Infinity)\n * - `wait`: Time to wait before processing batch (default: Infinity)\n * - `getShouldExecute`: Custom logic to trigger batch processing\n * - `asyncRetryerOptions`: Configure retry behavior for batch executions\n * - `started`: Whether to start processing immediately (default: true)\n *\n * Error Handling:\n * - If an `onError` handler is provided, it will be called with the error, the batch of items that failed, and batcher instance\n * - If `throwOnError` is true (default when no onError handler is provided), the error will be thrown\n * - If `throwOnError` is false (default when onError handler is provided), the error will be swallowed\n * - Both onError and throwOnError can be used together - the handler will be called before any error is thrown\n * - The error state can be checked using the underlying AsyncBatcher instance\n *\n * State Management:\n * - Uses TanStack Store for reactive state management\n * - Use `initialState` to provide initial state values when creating the async batcher\n * - Use `onSuccess` callback to react to successful batch execution and implement custom logic\n * - Use `onError` callback to react to batch execution errors and implement custom error handling\n * - Use `onSettled` callback to react to batch execution completion (success or error) and implement custom logic\n * - Use `onItemsChange` callback to react to items being added or removed from the batcher\n * - The state includes total items processed, success/error counts, and execution status\n * - State can be accessed via the underlying AsyncBatcher instance's `store.state` property\n * - When using framework adapters (React/Solid), state is accessed from the hook's state property\n *\n * @example\n * ```ts\n * const batchItems = asyncBatch<number>(\n * async (items) => {\n * const result = await processApiCall(items);\n * console.log('Processing:', items);\n * return result;\n * },\n * {\n * maxSize: 3,\n * wait: 1000,\n * onSuccess: (result) => console.log('Batch succeeded:', result),\n * onError: (error) => console.error('Batch failed:', error)\n * }\n * );\n *\n * batchItems(1);\n * batchItems(2);\n * batchItems(3); // Triggers batch processing\n * ```\n */\nexport function asyncBatch<TValue>(\n fn: (items: Array<TValue>) => Promise<any>,\n options: AsyncBatcherOptions<TValue>,\n) {\n const batcher = new AsyncBatcher<TValue>(fn, options)\n return batcher.addItem\n}\n"],"mappings":"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"}

@@ -1,1 +0,1 @@

{"version":3,"file":"async-debouncer.cjs","names":["Store","#getEnabled","#cancelPendingExecution","#setState","#execute","#resolvePreviousPromise","#timeoutId","#getWait","parseFunctionOrValue","AsyncRetryer","#clearTimeout","#resolvePreviousPromiseInternal"],"sources":["../src/async-debouncer.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 AsyncDebouncerState<TFn extends AnyAsyncFunction> {\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 resulted in errors\n */\n errorCount: number\n /**\n * Whether the debounced function is currently executing asynchronously\n */\n isExecuting: boolean\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 * 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 * 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 getDefaultAsyncDebouncerState<\n TFn extends AnyAsyncFunction,\n>(): AsyncDebouncerState<TFn> {\n return {\n canLeadingExecute: true,\n errorCount: 0,\n isExecuting: false,\n isPending: false,\n lastArgs: undefined,\n lastResult: undefined,\n maybeExecuteCount: 0,\n settleCount: 0,\n status: 'idle',\n successCount: 0,\n }\n}\n\n/**\n * Options for configuring an async debounced function\n */\nexport interface AsyncDebouncerOptions<TFn extends AnyAsyncFunction> {\n /**\n * Options for configuring the underlying async retryer\n */\n asyncRetryerOptions?: AsyncRetryerOptions<TFn>\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: AsyncDebouncer<TFn>) => boolean)\n /**\n * Initial state for the async debouncer\n */\n initialState?: Partial<AsyncDebouncerState<TFn>>\n /**\n * Optional key to identify this async debouncer instance.\n * If provided, the async 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 * Defaults to false.\n */\n leading?: boolean\n /**\n * Optional error handler for when the debounced function throws.\n * If provided, the handler will be called with the error and debouncer 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 debouncer: AsyncDebouncer<TFn>,\n ) => void\n /**\n * Optional callback to call when the debounced function is executed\n */\n onSettled?: (args: Parameters<TFn>, debouncer: AsyncDebouncer<TFn>) => void\n /**\n * Optional callback to call when the debounced function is executed\n */\n onSuccess?: (\n result: ReturnType<TFn>,\n args: Parameters<TFn>,\n debouncer: AsyncDebouncer<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 on the trailing edge of the timeout.\n * Defaults to true.\n */\n trailing?: boolean\n /**\n * Delay in milliseconds to wait after the last call before executing.\n * Can be a number or a function that returns a number.\n * Defaults to 0ms\n */\n wait: number | ((debouncer: AsyncDebouncer<TFn>) => number)\n}\n\n/**\n * Utility function for sharing common `AsyncDebouncerOptions` options between different `AsyncDebouncer` instances.\n */\nexport function asyncDebouncerOptions<\n TFn extends AnyAsyncFunction = AnyAsyncFunction,\n TOptions extends Partial<AsyncDebouncerOptions<TFn>> = Partial<\n AsyncDebouncerOptions<TFn>\n >,\n>(options: TOptions): TOptions {\n return options\n}\n\ntype AsyncDebouncerOptionsWithOptionalCallbacks = OptionalKeys<\n AsyncDebouncerOptions<any>,\n 'initialState' | 'onError' | 'onSettled' | 'onSuccess' | 'key'\n>\n\nconst defaultOptions: AsyncDebouncerOptionsWithOptionalCallbacks = {\n asyncRetryerOptions: {\n maxAttempts: 1,\n },\n enabled: true,\n leading: false,\n trailing: true,\n wait: 0,\n}\n\n/**\n * A class that creates an async debounced function.\n *\n * Async vs Sync Versions:\n * The async version provides advanced features over the sync Debouncer:\n * - Returns promises that can be awaited for debounced 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 *\n * The sync Debouncer is lighter weight and simpler when you don't need async features,\n * return values, or execution control.\n *\n * What is Debouncing?\n * Debouncing ensures that a function is only executed after a specified delay has passed since its last invocation.\n * Each new invocation resets the delay timer. This is useful for handling frequent events like window resizing\n * or input changes where you only want to execute the handler after the events have stopped occurring.\n *\n * Unlike throttling which allows execution at regular intervals, debouncing prevents any execution until\n * the function stops being called for the specified delay period.\n *\n * Error Handling:\n * - If an `onError` handler is provided, it will be called with the error and debouncer 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 store\n *\n * State Management:\n * - The debouncer uses a reactive store for state management\n * - Use `initialState` to provide initial state values when creating the async debouncer\n * - The state includes canLeadingExecute, error count, execution status, and success/settle counts\n * - State can be accessed via the `store` property and its `state` getter\n * - The store is reactive and will notify subscribers of state changes\n *\n * @example\n * ```ts\n * const asyncDebouncer = new AsyncDebouncer(async (value: string) => {\n * const results = await searchAPI(value);\n * return results; // Return value is preserved\n * }, {\n * wait: 500,\n * onError: (error) => {\n * console.error('Search failed:', error);\n * }\n * });\n *\n * // Called on each keystroke but only executes after 500ms of no typing\n * // Returns the API response directly\n * const results = await asyncDebouncer.maybeExecute(inputElement.value);\n * ```\n */\nexport class AsyncDebouncer<TFn extends AnyAsyncFunction> {\n readonly store: Store<Readonly<AsyncDebouncerState<TFn>>> = new Store<\n AsyncDebouncerState<TFn>\n >(getDefaultAsyncDebouncerState<TFn>())\n key: string | undefined\n options: AsyncDebouncerOptions<TFn>\n asyncRetryers = new Map<number, AsyncRetryer<TFn>>()\n #timeoutId: NodeJS.Timeout | null = null\n #resolvePreviousPromise:\n | ((value?: ReturnType<TFn> | undefined) => void)\n | null = null\n\n constructor(\n public fn: TFn,\n initialOptions: AsyncDebouncerOptions<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-AsyncDebouncer', (event) => {\n if (event.payload.key !== this.key) return\n this.#setState(\n event.payload.store.state as Partial<AsyncDebouncerState<TFn>>,\n )\n this.setOptions(\n event.payload.options as Partial<AsyncDebouncerOptions<TFn>>,\n )\n })\n }\n }\n\n /**\n * Updates the async debouncer options\n */\n setOptions = (newOptions: Partial<AsyncDebouncerOptions<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<AsyncDebouncerState<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('AsyncDebouncer', this)\n }\n\n /**\n * Returns the current debouncer enabled state\n */\n #getEnabled = (): boolean => {\n return !!parseFunctionOrValue(this.options.enabled, this)\n }\n\n /**\n * Returns the current debouncer wait state\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 * Error Handling:\n * - If the debounced function throws and no `onError` handler is configured,\n * the error will be thrown from this method.\n * - If an `onError` handler is configured, errors will be caught and passed to the handler,\n * and this method will return undefined.\n * - The error state can be checked using `getErrorCount()` and `getIsExecuting()`.\n *\n * @returns A promise that resolves with the function's return value, or undefined if an error occurred and was handled by onError\n * @throws The error from the debounced function if no onError handler is configured\n */\n maybeExecute = async (\n ...args: Parameters<TFn>\n ): Promise<ReturnType<TFn> | undefined> => {\n if (!this.#getEnabled()) return undefined\n this.#cancelPendingExecution()\n this.#setState({\n lastArgs: args,\n maybeExecuteCount: this.store.state.maybeExecuteCount + 1,\n })\n\n // Handle leading execution\n if (this.options.leading && this.store.state.canLeadingExecute) {\n this.#setState({ canLeadingExecute: false })\n await this.#execute(...args)\n return this.store.state.lastResult\n }\n\n // Handle trailing execution\n if (this.options.trailing && this.#getEnabled()) {\n this.#setState({ isPending: true })\n }\n\n return new Promise((resolve, reject) => {\n this.#resolvePreviousPromise = resolve\n // this.#rejectPreviousPromise = reject\n this.#timeoutId = setTimeout(async () => {\n // Execute trailing if enabled\n if (this.options.trailing && this.store.state.lastArgs) {\n try {\n await this.#execute(...this.store.state.lastArgs)\n } catch (error) {\n reject(error)\n }\n }\n\n // Reset state and resolve\n this.#setState({ canLeadingExecute: true })\n this.#resolvePreviousPromise = null\n resolve(this.store.state.lastResult)\n }, this.#getWait())\n })\n }\n\n #execute = async (\n ...args: Parameters<TFn>\n ): Promise<ReturnType<TFn> | undefined> => {\n if (!this.#getEnabled()) return undefined\n const currentMaybeExecuteCount = this.store.state.maybeExecuteCount + 1\n\n try {\n this.#setState({ isExecuting: true })\n const currentAsyncRetryer = new AsyncRetryer(this.fn, {\n ...this.options.asyncRetryerOptions,\n key: `${this.key}-retryer-${currentMaybeExecuteCount}`,\n })\n this.asyncRetryers.set(currentMaybeExecuteCount, 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(currentMaybeExecuteCount) // dispose retryer\n this.#setState({\n isExecuting: false,\n isPending: false,\n lastArgs: undefined,\n settleCount: this.store.state.settleCount + 1,\n })\n this.options.onSettled?.(args, this)\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 const { lastArgs } = this.store.state\n this.#cancelPendingExecution()\n return await this.#execute(...lastArgs)\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 * Internal cancel without resetting the leading execute state\n */\n #cancelPendingExecution = (): void => {\n this.#clearTimeout()\n this.#resolvePreviousPromiseInternal()\n this.#setState({\n isPending: false,\n lastArgs: undefined,\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 debouncer = new AsyncDebouncer(\n * async (searchTerm: string) => {\n * const signal = debouncer.getAbortSignal()\n * if (signal) {\n * const response = await fetch(`/api/search?q=${searchTerm}`, { signal })\n * return response.json()\n * }\n * },\n * { wait: 300 }\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({\n isExecuting: false,\n })\n }\n\n /**\n * Cancels any pending execution that have not started yet.\n * Does NOT abort any execution already in progress.\n */\n cancel = (): void => {\n this.#cancelPendingExecution()\n this.#setState({ canLeadingExecute: true })\n }\n\n /**\n * Resets the debouncer state to its default values\n */\n reset = (): void => {\n this.#setState(getDefaultAsyncDebouncerState<TFn>())\n this.asyncRetryers.forEach((retryer) => retryer.reset())\n }\n}\n\n/**\n * Creates an async debounced function that delays execution until after a specified wait time.\n * The debounced function will only execute once the wait period has elapsed without any new calls.\n * If called again during the wait period, the timer resets and a new wait period begins.\n *\n * Async vs Sync Versions:\n * The async version provides advanced features over the sync debounce function:\n * - Returns promises that can be awaited for debounced 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 *\n * The sync debounce function is lighter weight and simpler when you don't need async features,\n * return values, or execution control.\n *\n * What is Debouncing?\n * Debouncing ensures that a function is only executed after a specified delay has passed since its last invocation.\n * Each new invocation resets the delay timer. This is useful for handling frequent events like window resizing\n * or input changes where you only want to execute the handler after the events have stopped occurring.\n *\n * Configuration Options:\n * - `wait`: Delay in milliseconds to wait after the last call (required)\n * - `leading`: Execute on the leading edge of the timeout (default: false)\n * - `trailing`: Execute on the trailing edge of the timeout (default: true)\n * - `enabled`: Whether the debouncer 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 debouncer 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 * - The error state can be checked using the underlying AsyncDebouncer instance\n * - Both onError and throwOnError can be used together - the handler will be called before any error is thrown\n *\n * State Management:\n * - Uses TanStack Store for reactive state management\n * - Use `initialState` to provide initial state values when creating the async debouncer\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 canLeadingExecute, error count, execution status, and success/settle counts\n * - State can be accessed via `asyncDebouncer.store.state` when using the class directly\n * - When using framework adapters (React/Solid), state is accessed from `asyncDebouncer.state`\n *\n * @example\n * ```ts\n * const debounced = asyncDebounce(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 * throwOnError: true // Will both log the error and throw it\n * });\n *\n * // Will only execute once, 1 second after the last call\n * // Returns the API response directly\n * const result = await debounced(\"third\");\n * ```\n */\nexport function asyncDebounce<TFn extends AnyAsyncFunction>(\n fn: TFn,\n initialOptions: AsyncDebouncerOptions<TFn>,\n) {\n const asyncDebouncer = new AsyncDebouncer(fn, initialOptions)\n return asyncDebouncer.maybeExecute\n}\n"],"mappings":"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"}
{"version":3,"file":"async-debouncer.cjs","names":["Store","#getEnabled","#cancelPendingExecution","#setState","#execute","#resolvePreviousPromise","#timeoutId","#getWait","parseFunctionOrValue","AsyncRetryer","#clearTimeout","#resolvePreviousPromiseInternal"],"sources":["../src/async-debouncer.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 AsyncDebouncerState<TFn extends AnyAsyncFunction> {\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 resulted in errors\n */\n errorCount: number\n /**\n * Whether the debounced function is currently executing asynchronously\n */\n isExecuting: boolean\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 * 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 * 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 getDefaultAsyncDebouncerState<\n TFn extends AnyAsyncFunction,\n>(): AsyncDebouncerState<TFn> {\n return {\n canLeadingExecute: true,\n errorCount: 0,\n isExecuting: false,\n isPending: false,\n lastArgs: undefined,\n lastResult: undefined,\n maybeExecuteCount: 0,\n settleCount: 0,\n status: 'idle',\n successCount: 0,\n }\n}\n\n/**\n * Options for configuring an async debounced function\n */\nexport interface AsyncDebouncerOptions<TFn extends AnyAsyncFunction> {\n /**\n * Options for configuring the underlying async retryer\n */\n asyncRetryerOptions?: AsyncRetryerOptions<TFn>\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: AsyncDebouncer<TFn>) => boolean)\n /**\n * Initial state for the async debouncer\n */\n initialState?: Partial<AsyncDebouncerState<TFn>>\n /**\n * Optional key to identify this async debouncer instance.\n * If provided, the async 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 * Defaults to false.\n */\n leading?: boolean\n /**\n * Optional error handler for when the debounced function throws.\n * If provided, the handler will be called with the error and debouncer 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 debouncer: AsyncDebouncer<TFn>,\n ) => void\n /**\n * Optional callback to call when the debounced function is executed\n */\n onSettled?: (args: Parameters<TFn>, debouncer: AsyncDebouncer<TFn>) => void\n /**\n * Optional callback to call when the debounced function is executed\n */\n onSuccess?: (\n result: ReturnType<TFn>,\n args: Parameters<TFn>,\n debouncer: AsyncDebouncer<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 on the trailing edge of the timeout.\n * Defaults to true.\n */\n trailing?: boolean\n /**\n * Delay in milliseconds to wait after the last call before executing.\n * Can be a number or a function that returns a number.\n * Defaults to 0ms\n */\n wait: number | ((debouncer: AsyncDebouncer<TFn>) => number)\n}\n\n/**\n * Utility function for sharing common `AsyncDebouncerOptions` options between different `AsyncDebouncer` instances.\n */\nexport function asyncDebouncerOptions<\n TFn extends AnyAsyncFunction = AnyAsyncFunction,\n TOptions extends Partial<AsyncDebouncerOptions<TFn>> = Partial<\n AsyncDebouncerOptions<TFn>\n >,\n>(options: TOptions): TOptions {\n return options\n}\n\ntype AsyncDebouncerOptionsWithOptionalCallbacks = OptionalKeys<\n AsyncDebouncerOptions<any>,\n 'initialState' | 'onError' | 'onSettled' | 'onSuccess' | 'key'\n>\n\nconst defaultOptions: AsyncDebouncerOptionsWithOptionalCallbacks = {\n asyncRetryerOptions: {\n maxAttempts: 1,\n },\n enabled: true,\n leading: false,\n trailing: true,\n wait: 0,\n}\n\n/**\n * A class that creates an async debounced function.\n *\n * Async vs Sync Versions:\n * The async version provides advanced features over the sync Debouncer:\n * - Returns promises that can be awaited for debounced 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 *\n * The sync Debouncer is lighter weight and simpler when you don't need async features,\n * return values, or execution control.\n *\n * What is Debouncing?\n * Debouncing ensures that a function is only executed after a specified delay has passed since its last invocation.\n * Each new invocation resets the delay timer. This is useful for handling frequent events like window resizing\n * or input changes where you only want to execute the handler after the events have stopped occurring.\n *\n * Unlike throttling which allows execution at regular intervals, debouncing prevents any execution until\n * the function stops being called for the specified delay period.\n *\n * Error Handling:\n * - If an `onError` handler is provided, it will be called with the error and debouncer 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 store\n *\n * State Management:\n * - The debouncer uses a reactive store for state management\n * - Use `initialState` to provide initial state values when creating the async debouncer\n * - The state includes canLeadingExecute, error count, execution status, and success/settle counts\n * - State can be accessed via the `store` property and its `state` getter\n * - The store is reactive and will notify subscribers of state changes\n *\n * @example\n * ```ts\n * const asyncDebouncer = new AsyncDebouncer(async (value: string) => {\n * const results = await searchAPI(value);\n * return results; // Return value is preserved\n * }, {\n * wait: 500,\n * onError: (error) => {\n * console.error('Search failed:', error);\n * }\n * });\n *\n * // Called on each keystroke but only executes after 500ms of no typing\n * // Returns the API response directly\n * const results = await asyncDebouncer.maybeExecute(inputElement.value);\n * ```\n */\nexport class AsyncDebouncer<TFn extends AnyAsyncFunction> {\n readonly store: Store<Readonly<AsyncDebouncerState<TFn>>> = new Store<\n AsyncDebouncerState<TFn>\n >(getDefaultAsyncDebouncerState<TFn>())\n key: string | undefined\n options: AsyncDebouncerOptions<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: AsyncDebouncerOptions<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-AsyncDebouncer', (event) => {\n if (event.payload.key !== this.key) return\n this.#setState(\n event.payload.store.state as Partial<AsyncDebouncerState<TFn>>,\n )\n this.setOptions(\n event.payload.options as Partial<AsyncDebouncerOptions<TFn>>,\n )\n })\n }\n }\n\n /**\n * Updates the async debouncer options\n */\n setOptions = (newOptions: Partial<AsyncDebouncerOptions<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<AsyncDebouncerState<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('AsyncDebouncer', this)\n }\n\n /**\n * Returns the current debouncer enabled state\n */\n #getEnabled = (): boolean => {\n return !!parseFunctionOrValue(this.options.enabled, this)\n }\n\n /**\n * Returns the current debouncer wait state\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 * Error Handling:\n * - If the debounced function throws and no `onError` handler is configured,\n * the error will be thrown from this method.\n * - If an `onError` handler is configured, errors will be caught and passed to the handler,\n * and this method will return undefined.\n * - The error state can be checked using `getErrorCount()` and `getIsExecuting()`.\n *\n * @returns A promise that resolves with the function's return value, or undefined if an error occurred and was handled by onError\n * @throws The error from the debounced function if no onError handler is configured\n */\n maybeExecute = async (\n ...args: Parameters<TFn>\n ): Promise<ReturnType<TFn> | undefined> => {\n if (!this.#getEnabled()) return undefined\n this.#cancelPendingExecution()\n this.#setState({\n lastArgs: args,\n maybeExecuteCount: this.store.state.maybeExecuteCount + 1,\n })\n\n // Handle leading execution\n if (this.options.leading && this.store.state.canLeadingExecute) {\n this.#setState({ canLeadingExecute: false })\n await this.#execute(...args)\n return this.store.state.lastResult\n }\n\n // Handle trailing execution\n if (this.options.trailing && this.#getEnabled()) {\n this.#setState({ isPending: true })\n }\n\n return new Promise((resolve, reject) => {\n this.#resolvePreviousPromise = resolve\n // this.#rejectPreviousPromise = reject\n this.#timeoutId = setTimeout(async () => {\n // Execute trailing if enabled\n if (this.options.trailing && this.store.state.lastArgs) {\n try {\n await this.#execute(...this.store.state.lastArgs)\n } catch (error) {\n reject(error)\n }\n }\n\n // Reset state and resolve\n this.#setState({ canLeadingExecute: true })\n this.#resolvePreviousPromise = null\n resolve(this.store.state.lastResult)\n }, this.#getWait())\n })\n }\n\n #execute = async (\n ...args: Parameters<TFn>\n ): Promise<ReturnType<TFn> | undefined> => {\n if (!this.#getEnabled()) return undefined\n const currentMaybeExecuteCount = this.store.state.maybeExecuteCount + 1\n\n try {\n this.#setState({ isExecuting: true })\n const currentAsyncRetryer = new AsyncRetryer(this.fn, {\n ...this.options.asyncRetryerOptions,\n key: `${this.key}-retryer-${currentMaybeExecuteCount}`,\n })\n this.asyncRetryers.set(currentMaybeExecuteCount, 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(currentMaybeExecuteCount) // dispose retryer\n this.#setState({\n isExecuting: false,\n isPending: false,\n lastArgs: undefined,\n settleCount: this.store.state.settleCount + 1,\n })\n this.options.onSettled?.(args, this)\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 const { lastArgs } = this.store.state\n this.#cancelPendingExecution()\n return await this.#execute(...lastArgs)\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 * Internal cancel without resetting the leading execute state\n */\n #cancelPendingExecution = (): void => {\n this.#clearTimeout()\n this.#resolvePreviousPromiseInternal()\n this.#setState({\n isPending: false,\n lastArgs: undefined,\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 debouncer = new AsyncDebouncer(\n * async (searchTerm: string) => {\n * const signal = debouncer.getAbortSignal()\n * if (signal) {\n * const response = await fetch(`/api/search?q=${searchTerm}`, { signal })\n * return response.json()\n * }\n * },\n * { wait: 300 }\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({\n isExecuting: false,\n })\n }\n\n /**\n * Cancels any pending execution that have not started yet.\n * Does NOT abort any execution already in progress.\n */\n cancel = (): void => {\n this.#cancelPendingExecution()\n this.#setState({ canLeadingExecute: true })\n }\n\n /**\n * Resets the debouncer state to its default values\n */\n reset = (): void => {\n this.#setState(getDefaultAsyncDebouncerState<TFn>())\n this.asyncRetryers.forEach((retryer) => retryer.reset())\n }\n}\n\n/**\n * Creates an async debounced function that delays execution until after a specified wait time.\n * The debounced function will only execute once the wait period has elapsed without any new calls.\n * If called again during the wait period, the timer resets and a new wait period begins.\n *\n * Async vs Sync Versions:\n * The async version provides advanced features over the sync debounce function:\n * - Returns promises that can be awaited for debounced 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 *\n * The sync debounce function is lighter weight and simpler when you don't need async features,\n * return values, or execution control.\n *\n * What is Debouncing?\n * Debouncing ensures that a function is only executed after a specified delay has passed since its last invocation.\n * Each new invocation resets the delay timer. This is useful for handling frequent events like window resizing\n * or input changes where you only want to execute the handler after the events have stopped occurring.\n *\n * Configuration Options:\n * - `wait`: Delay in milliseconds to wait after the last call (required)\n * - `leading`: Execute on the leading edge of the timeout (default: false)\n * - `trailing`: Execute on the trailing edge of the timeout (default: true)\n * - `enabled`: Whether the debouncer 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 debouncer 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 * - The error state can be checked using the underlying AsyncDebouncer instance\n * - Both onError and throwOnError can be used together - the handler will be called before any error is thrown\n *\n * State Management:\n * - Uses TanStack Store for reactive state management\n * - Use `initialState` to provide initial state values when creating the async debouncer\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 canLeadingExecute, error count, execution status, and success/settle counts\n * - State can be accessed via `asyncDebouncer.store.state` when using the class directly\n * - When using framework adapters (React/Solid), state is accessed from `asyncDebouncer.state`\n *\n * @example\n * ```ts\n * const debounced = asyncDebounce(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 * throwOnError: true // Will both log the error and throw it\n * });\n *\n * // Will only execute once, 1 second after the last call\n * // Returns the API response directly\n * const result = await debounced(\"third\");\n * ```\n */\nexport function asyncDebounce<TFn extends AnyAsyncFunction>(\n fn: TFn,\n initialOptions: AsyncDebouncerOptions<TFn>,\n) {\n const asyncDebouncer = new AsyncDebouncer(fn, initialOptions)\n return asyncDebouncer.maybeExecute\n}\n"],"mappings":"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"}

@@ -1,1 +0,1 @@

{"version":3,"file":"async-debouncer.js","names":["#getEnabled","#cancelPendingExecution","#setState","#execute","#resolvePreviousPromise","#timeoutId","#getWait","#clearTimeout","#resolvePreviousPromiseInternal"],"sources":["../src/async-debouncer.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 AsyncDebouncerState<TFn extends AnyAsyncFunction> {\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 resulted in errors\n */\n errorCount: number\n /**\n * Whether the debounced function is currently executing asynchronously\n */\n isExecuting: boolean\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 * 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 * 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 getDefaultAsyncDebouncerState<\n TFn extends AnyAsyncFunction,\n>(): AsyncDebouncerState<TFn> {\n return {\n canLeadingExecute: true,\n errorCount: 0,\n isExecuting: false,\n isPending: false,\n lastArgs: undefined,\n lastResult: undefined,\n maybeExecuteCount: 0,\n settleCount: 0,\n status: 'idle',\n successCount: 0,\n }\n}\n\n/**\n * Options for configuring an async debounced function\n */\nexport interface AsyncDebouncerOptions<TFn extends AnyAsyncFunction> {\n /**\n * Options for configuring the underlying async retryer\n */\n asyncRetryerOptions?: AsyncRetryerOptions<TFn>\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: AsyncDebouncer<TFn>) => boolean)\n /**\n * Initial state for the async debouncer\n */\n initialState?: Partial<AsyncDebouncerState<TFn>>\n /**\n * Optional key to identify this async debouncer instance.\n * If provided, the async 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 * Defaults to false.\n */\n leading?: boolean\n /**\n * Optional error handler for when the debounced function throws.\n * If provided, the handler will be called with the error and debouncer 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 debouncer: AsyncDebouncer<TFn>,\n ) => void\n /**\n * Optional callback to call when the debounced function is executed\n */\n onSettled?: (args: Parameters<TFn>, debouncer: AsyncDebouncer<TFn>) => void\n /**\n * Optional callback to call when the debounced function is executed\n */\n onSuccess?: (\n result: ReturnType<TFn>,\n args: Parameters<TFn>,\n debouncer: AsyncDebouncer<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 on the trailing edge of the timeout.\n * Defaults to true.\n */\n trailing?: boolean\n /**\n * Delay in milliseconds to wait after the last call before executing.\n * Can be a number or a function that returns a number.\n * Defaults to 0ms\n */\n wait: number | ((debouncer: AsyncDebouncer<TFn>) => number)\n}\n\n/**\n * Utility function for sharing common `AsyncDebouncerOptions` options between different `AsyncDebouncer` instances.\n */\nexport function asyncDebouncerOptions<\n TFn extends AnyAsyncFunction = AnyAsyncFunction,\n TOptions extends Partial<AsyncDebouncerOptions<TFn>> = Partial<\n AsyncDebouncerOptions<TFn>\n >,\n>(options: TOptions): TOptions {\n return options\n}\n\ntype AsyncDebouncerOptionsWithOptionalCallbacks = OptionalKeys<\n AsyncDebouncerOptions<any>,\n 'initialState' | 'onError' | 'onSettled' | 'onSuccess' | 'key'\n>\n\nconst defaultOptions: AsyncDebouncerOptionsWithOptionalCallbacks = {\n asyncRetryerOptions: {\n maxAttempts: 1,\n },\n enabled: true,\n leading: false,\n trailing: true,\n wait: 0,\n}\n\n/**\n * A class that creates an async debounced function.\n *\n * Async vs Sync Versions:\n * The async version provides advanced features over the sync Debouncer:\n * - Returns promises that can be awaited for debounced 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 *\n * The sync Debouncer is lighter weight and simpler when you don't need async features,\n * return values, or execution control.\n *\n * What is Debouncing?\n * Debouncing ensures that a function is only executed after a specified delay has passed since its last invocation.\n * Each new invocation resets the delay timer. This is useful for handling frequent events like window resizing\n * or input changes where you only want to execute the handler after the events have stopped occurring.\n *\n * Unlike throttling which allows execution at regular intervals, debouncing prevents any execution until\n * the function stops being called for the specified delay period.\n *\n * Error Handling:\n * - If an `onError` handler is provided, it will be called with the error and debouncer 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 store\n *\n * State Management:\n * - The debouncer uses a reactive store for state management\n * - Use `initialState` to provide initial state values when creating the async debouncer\n * - The state includes canLeadingExecute, error count, execution status, and success/settle counts\n * - State can be accessed via the `store` property and its `state` getter\n * - The store is reactive and will notify subscribers of state changes\n *\n * @example\n * ```ts\n * const asyncDebouncer = new AsyncDebouncer(async (value: string) => {\n * const results = await searchAPI(value);\n * return results; // Return value is preserved\n * }, {\n * wait: 500,\n * onError: (error) => {\n * console.error('Search failed:', error);\n * }\n * });\n *\n * // Called on each keystroke but only executes after 500ms of no typing\n * // Returns the API response directly\n * const results = await asyncDebouncer.maybeExecute(inputElement.value);\n * ```\n */\nexport class AsyncDebouncer<TFn extends AnyAsyncFunction> {\n readonly store: Store<Readonly<AsyncDebouncerState<TFn>>> = new Store<\n AsyncDebouncerState<TFn>\n >(getDefaultAsyncDebouncerState<TFn>())\n key: string | undefined\n options: AsyncDebouncerOptions<TFn>\n asyncRetryers = new Map<number, AsyncRetryer<TFn>>()\n #timeoutId: NodeJS.Timeout | null = null\n #resolvePreviousPromise:\n | ((value?: ReturnType<TFn> | undefined) => void)\n | null = null\n\n constructor(\n public fn: TFn,\n initialOptions: AsyncDebouncerOptions<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-AsyncDebouncer', (event) => {\n if (event.payload.key !== this.key) return\n this.#setState(\n event.payload.store.state as Partial<AsyncDebouncerState<TFn>>,\n )\n this.setOptions(\n event.payload.options as Partial<AsyncDebouncerOptions<TFn>>,\n )\n })\n }\n }\n\n /**\n * Updates the async debouncer options\n */\n setOptions = (newOptions: Partial<AsyncDebouncerOptions<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<AsyncDebouncerState<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('AsyncDebouncer', this)\n }\n\n /**\n * Returns the current debouncer enabled state\n */\n #getEnabled = (): boolean => {\n return !!parseFunctionOrValue(this.options.enabled, this)\n }\n\n /**\n * Returns the current debouncer wait state\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 * Error Handling:\n * - If the debounced function throws and no `onError` handler is configured,\n * the error will be thrown from this method.\n * - If an `onError` handler is configured, errors will be caught and passed to the handler,\n * and this method will return undefined.\n * - The error state can be checked using `getErrorCount()` and `getIsExecuting()`.\n *\n * @returns A promise that resolves with the function's return value, or undefined if an error occurred and was handled by onError\n * @throws The error from the debounced function if no onError handler is configured\n */\n maybeExecute = async (\n ...args: Parameters<TFn>\n ): Promise<ReturnType<TFn> | undefined> => {\n if (!this.#getEnabled()) return undefined\n this.#cancelPendingExecution()\n this.#setState({\n lastArgs: args,\n maybeExecuteCount: this.store.state.maybeExecuteCount + 1,\n })\n\n // Handle leading execution\n if (this.options.leading && this.store.state.canLeadingExecute) {\n this.#setState({ canLeadingExecute: false })\n await this.#execute(...args)\n return this.store.state.lastResult\n }\n\n // Handle trailing execution\n if (this.options.trailing && this.#getEnabled()) {\n this.#setState({ isPending: true })\n }\n\n return new Promise((resolve, reject) => {\n this.#resolvePreviousPromise = resolve\n // this.#rejectPreviousPromise = reject\n this.#timeoutId = setTimeout(async () => {\n // Execute trailing if enabled\n if (this.options.trailing && this.store.state.lastArgs) {\n try {\n await this.#execute(...this.store.state.lastArgs)\n } catch (error) {\n reject(error)\n }\n }\n\n // Reset state and resolve\n this.#setState({ canLeadingExecute: true })\n this.#resolvePreviousPromise = null\n resolve(this.store.state.lastResult)\n }, this.#getWait())\n })\n }\n\n #execute = async (\n ...args: Parameters<TFn>\n ): Promise<ReturnType<TFn> | undefined> => {\n if (!this.#getEnabled()) return undefined\n const currentMaybeExecuteCount = this.store.state.maybeExecuteCount + 1\n\n try {\n this.#setState({ isExecuting: true })\n const currentAsyncRetryer = new AsyncRetryer(this.fn, {\n ...this.options.asyncRetryerOptions,\n key: `${this.key}-retryer-${currentMaybeExecuteCount}`,\n })\n this.asyncRetryers.set(currentMaybeExecuteCount, 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(currentMaybeExecuteCount) // dispose retryer\n this.#setState({\n isExecuting: false,\n isPending: false,\n lastArgs: undefined,\n settleCount: this.store.state.settleCount + 1,\n })\n this.options.onSettled?.(args, this)\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 const { lastArgs } = this.store.state\n this.#cancelPendingExecution()\n return await this.#execute(...lastArgs)\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 * Internal cancel without resetting the leading execute state\n */\n #cancelPendingExecution = (): void => {\n this.#clearTimeout()\n this.#resolvePreviousPromiseInternal()\n this.#setState({\n isPending: false,\n lastArgs: undefined,\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 debouncer = new AsyncDebouncer(\n * async (searchTerm: string) => {\n * const signal = debouncer.getAbortSignal()\n * if (signal) {\n * const response = await fetch(`/api/search?q=${searchTerm}`, { signal })\n * return response.json()\n * }\n * },\n * { wait: 300 }\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({\n isExecuting: false,\n })\n }\n\n /**\n * Cancels any pending execution that have not started yet.\n * Does NOT abort any execution already in progress.\n */\n cancel = (): void => {\n this.#cancelPendingExecution()\n this.#setState({ canLeadingExecute: true })\n }\n\n /**\n * Resets the debouncer state to its default values\n */\n reset = (): void => {\n this.#setState(getDefaultAsyncDebouncerState<TFn>())\n this.asyncRetryers.forEach((retryer) => retryer.reset())\n }\n}\n\n/**\n * Creates an async debounced function that delays execution until after a specified wait time.\n * The debounced function will only execute once the wait period has elapsed without any new calls.\n * If called again during the wait period, the timer resets and a new wait period begins.\n *\n * Async vs Sync Versions:\n * The async version provides advanced features over the sync debounce function:\n * - Returns promises that can be awaited for debounced 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 *\n * The sync debounce function is lighter weight and simpler when you don't need async features,\n * return values, or execution control.\n *\n * What is Debouncing?\n * Debouncing ensures that a function is only executed after a specified delay has passed since its last invocation.\n * Each new invocation resets the delay timer. This is useful for handling frequent events like window resizing\n * or input changes where you only want to execute the handler after the events have stopped occurring.\n *\n * Configuration Options:\n * - `wait`: Delay in milliseconds to wait after the last call (required)\n * - `leading`: Execute on the leading edge of the timeout (default: false)\n * - `trailing`: Execute on the trailing edge of the timeout (default: true)\n * - `enabled`: Whether the debouncer 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 debouncer 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 * - The error state can be checked using the underlying AsyncDebouncer instance\n * - Both onError and throwOnError can be used together - the handler will be called before any error is thrown\n *\n * State Management:\n * - Uses TanStack Store for reactive state management\n * - Use `initialState` to provide initial state values when creating the async debouncer\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 canLeadingExecute, error count, execution status, and success/settle counts\n * - State can be accessed via `asyncDebouncer.store.state` when using the class directly\n * - When using framework adapters (React/Solid), state is accessed from `asyncDebouncer.state`\n *\n * @example\n * ```ts\n * const debounced = asyncDebounce(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 * throwOnError: true // Will both log the error and throw it\n * });\n *\n * // Will only execute once, 1 second after the last call\n * // Returns the API response directly\n * const result = await debounced(\"third\");\n * ```\n */\nexport function asyncDebounce<TFn extends AnyAsyncFunction>(\n fn: TFn,\n initialOptions: AsyncDebouncerOptions<TFn>,\n) {\n const asyncDebouncer = new AsyncDebouncer(fn, initialOptions)\n return asyncDebouncer.maybeExecute\n}\n"],"mappings":"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"}
{"version":3,"file":"async-debouncer.js","names":["#getEnabled","#cancelPendingExecution","#setState","#execute","#resolvePreviousPromise","#timeoutId","#getWait","#clearTimeout","#resolvePreviousPromiseInternal"],"sources":["../src/async-debouncer.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 AsyncDebouncerState<TFn extends AnyAsyncFunction> {\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 resulted in errors\n */\n errorCount: number\n /**\n * Whether the debounced function is currently executing asynchronously\n */\n isExecuting: boolean\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 * 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 * 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 getDefaultAsyncDebouncerState<\n TFn extends AnyAsyncFunction,\n>(): AsyncDebouncerState<TFn> {\n return {\n canLeadingExecute: true,\n errorCount: 0,\n isExecuting: false,\n isPending: false,\n lastArgs: undefined,\n lastResult: undefined,\n maybeExecuteCount: 0,\n settleCount: 0,\n status: 'idle',\n successCount: 0,\n }\n}\n\n/**\n * Options for configuring an async debounced function\n */\nexport interface AsyncDebouncerOptions<TFn extends AnyAsyncFunction> {\n /**\n * Options for configuring the underlying async retryer\n */\n asyncRetryerOptions?: AsyncRetryerOptions<TFn>\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: AsyncDebouncer<TFn>) => boolean)\n /**\n * Initial state for the async debouncer\n */\n initialState?: Partial<AsyncDebouncerState<TFn>>\n /**\n * Optional key to identify this async debouncer instance.\n * If provided, the async 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 * Defaults to false.\n */\n leading?: boolean\n /**\n * Optional error handler for when the debounced function throws.\n * If provided, the handler will be called with the error and debouncer 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 debouncer: AsyncDebouncer<TFn>,\n ) => void\n /**\n * Optional callback to call when the debounced function is executed\n */\n onSettled?: (args: Parameters<TFn>, debouncer: AsyncDebouncer<TFn>) => void\n /**\n * Optional callback to call when the debounced function is executed\n */\n onSuccess?: (\n result: ReturnType<TFn>,\n args: Parameters<TFn>,\n debouncer: AsyncDebouncer<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 on the trailing edge of the timeout.\n * Defaults to true.\n */\n trailing?: boolean\n /**\n * Delay in milliseconds to wait after the last call before executing.\n * Can be a number or a function that returns a number.\n * Defaults to 0ms\n */\n wait: number | ((debouncer: AsyncDebouncer<TFn>) => number)\n}\n\n/**\n * Utility function for sharing common `AsyncDebouncerOptions` options between different `AsyncDebouncer` instances.\n */\nexport function asyncDebouncerOptions<\n TFn extends AnyAsyncFunction = AnyAsyncFunction,\n TOptions extends Partial<AsyncDebouncerOptions<TFn>> = Partial<\n AsyncDebouncerOptions<TFn>\n >,\n>(options: TOptions): TOptions {\n return options\n}\n\ntype AsyncDebouncerOptionsWithOptionalCallbacks = OptionalKeys<\n AsyncDebouncerOptions<any>,\n 'initialState' | 'onError' | 'onSettled' | 'onSuccess' | 'key'\n>\n\nconst defaultOptions: AsyncDebouncerOptionsWithOptionalCallbacks = {\n asyncRetryerOptions: {\n maxAttempts: 1,\n },\n enabled: true,\n leading: false,\n trailing: true,\n wait: 0,\n}\n\n/**\n * A class that creates an async debounced function.\n *\n * Async vs Sync Versions:\n * The async version provides advanced features over the sync Debouncer:\n * - Returns promises that can be awaited for debounced 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 *\n * The sync Debouncer is lighter weight and simpler when you don't need async features,\n * return values, or execution control.\n *\n * What is Debouncing?\n * Debouncing ensures that a function is only executed after a specified delay has passed since its last invocation.\n * Each new invocation resets the delay timer. This is useful for handling frequent events like window resizing\n * or input changes where you only want to execute the handler after the events have stopped occurring.\n *\n * Unlike throttling which allows execution at regular intervals, debouncing prevents any execution until\n * the function stops being called for the specified delay period.\n *\n * Error Handling:\n * - If an `onError` handler is provided, it will be called with the error and debouncer 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 store\n *\n * State Management:\n * - The debouncer uses a reactive store for state management\n * - Use `initialState` to provide initial state values when creating the async debouncer\n * - The state includes canLeadingExecute, error count, execution status, and success/settle counts\n * - State can be accessed via the `store` property and its `state` getter\n * - The store is reactive and will notify subscribers of state changes\n *\n * @example\n * ```ts\n * const asyncDebouncer = new AsyncDebouncer(async (value: string) => {\n * const results = await searchAPI(value);\n * return results; // Return value is preserved\n * }, {\n * wait: 500,\n * onError: (error) => {\n * console.error('Search failed:', error);\n * }\n * });\n *\n * // Called on each keystroke but only executes after 500ms of no typing\n * // Returns the API response directly\n * const results = await asyncDebouncer.maybeExecute(inputElement.value);\n * ```\n */\nexport class AsyncDebouncer<TFn extends AnyAsyncFunction> {\n readonly store: Store<Readonly<AsyncDebouncerState<TFn>>> = new Store<\n AsyncDebouncerState<TFn>\n >(getDefaultAsyncDebouncerState<TFn>())\n key: string | undefined\n options: AsyncDebouncerOptions<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: AsyncDebouncerOptions<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-AsyncDebouncer', (event) => {\n if (event.payload.key !== this.key) return\n this.#setState(\n event.payload.store.state as Partial<AsyncDebouncerState<TFn>>,\n )\n this.setOptions(\n event.payload.options as Partial<AsyncDebouncerOptions<TFn>>,\n )\n })\n }\n }\n\n /**\n * Updates the async debouncer options\n */\n setOptions = (newOptions: Partial<AsyncDebouncerOptions<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<AsyncDebouncerState<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('AsyncDebouncer', this)\n }\n\n /**\n * Returns the current debouncer enabled state\n */\n #getEnabled = (): boolean => {\n return !!parseFunctionOrValue(this.options.enabled, this)\n }\n\n /**\n * Returns the current debouncer wait state\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 * Error Handling:\n * - If the debounced function throws and no `onError` handler is configured,\n * the error will be thrown from this method.\n * - If an `onError` handler is configured, errors will be caught and passed to the handler,\n * and this method will return undefined.\n * - The error state can be checked using `getErrorCount()` and `getIsExecuting()`.\n *\n * @returns A promise that resolves with the function's return value, or undefined if an error occurred and was handled by onError\n * @throws The error from the debounced function if no onError handler is configured\n */\n maybeExecute = async (\n ...args: Parameters<TFn>\n ): Promise<ReturnType<TFn> | undefined> => {\n if (!this.#getEnabled()) return undefined\n this.#cancelPendingExecution()\n this.#setState({\n lastArgs: args,\n maybeExecuteCount: this.store.state.maybeExecuteCount + 1,\n })\n\n // Handle leading execution\n if (this.options.leading && this.store.state.canLeadingExecute) {\n this.#setState({ canLeadingExecute: false })\n await this.#execute(...args)\n return this.store.state.lastResult\n }\n\n // Handle trailing execution\n if (this.options.trailing && this.#getEnabled()) {\n this.#setState({ isPending: true })\n }\n\n return new Promise((resolve, reject) => {\n this.#resolvePreviousPromise = resolve\n // this.#rejectPreviousPromise = reject\n this.#timeoutId = setTimeout(async () => {\n // Execute trailing if enabled\n if (this.options.trailing && this.store.state.lastArgs) {\n try {\n await this.#execute(...this.store.state.lastArgs)\n } catch (error) {\n reject(error)\n }\n }\n\n // Reset state and resolve\n this.#setState({ canLeadingExecute: true })\n this.#resolvePreviousPromise = null\n resolve(this.store.state.lastResult)\n }, this.#getWait())\n })\n }\n\n #execute = async (\n ...args: Parameters<TFn>\n ): Promise<ReturnType<TFn> | undefined> => {\n if (!this.#getEnabled()) return undefined\n const currentMaybeExecuteCount = this.store.state.maybeExecuteCount + 1\n\n try {\n this.#setState({ isExecuting: true })\n const currentAsyncRetryer = new AsyncRetryer(this.fn, {\n ...this.options.asyncRetryerOptions,\n key: `${this.key}-retryer-${currentMaybeExecuteCount}`,\n })\n this.asyncRetryers.set(currentMaybeExecuteCount, 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(currentMaybeExecuteCount) // dispose retryer\n this.#setState({\n isExecuting: false,\n isPending: false,\n lastArgs: undefined,\n settleCount: this.store.state.settleCount + 1,\n })\n this.options.onSettled?.(args, this)\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 const { lastArgs } = this.store.state\n this.#cancelPendingExecution()\n return await this.#execute(...lastArgs)\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 * Internal cancel without resetting the leading execute state\n */\n #cancelPendingExecution = (): void => {\n this.#clearTimeout()\n this.#resolvePreviousPromiseInternal()\n this.#setState({\n isPending: false,\n lastArgs: undefined,\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 debouncer = new AsyncDebouncer(\n * async (searchTerm: string) => {\n * const signal = debouncer.getAbortSignal()\n * if (signal) {\n * const response = await fetch(`/api/search?q=${searchTerm}`, { signal })\n * return response.json()\n * }\n * },\n * { wait: 300 }\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({\n isExecuting: false,\n })\n }\n\n /**\n * Cancels any pending execution that have not started yet.\n * Does NOT abort any execution already in progress.\n */\n cancel = (): void => {\n this.#cancelPendingExecution()\n this.#setState({ canLeadingExecute: true })\n }\n\n /**\n * Resets the debouncer state to its default values\n */\n reset = (): void => {\n this.#setState(getDefaultAsyncDebouncerState<TFn>())\n this.asyncRetryers.forEach((retryer) => retryer.reset())\n }\n}\n\n/**\n * Creates an async debounced function that delays execution until after a specified wait time.\n * The debounced function will only execute once the wait period has elapsed without any new calls.\n * If called again during the wait period, the timer resets and a new wait period begins.\n *\n * Async vs Sync Versions:\n * The async version provides advanced features over the sync debounce function:\n * - Returns promises that can be awaited for debounced 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 *\n * The sync debounce function is lighter weight and simpler when you don't need async features,\n * return values, or execution control.\n *\n * What is Debouncing?\n * Debouncing ensures that a function is only executed after a specified delay has passed since its last invocation.\n * Each new invocation resets the delay timer. This is useful for handling frequent events like window resizing\n * or input changes where you only want to execute the handler after the events have stopped occurring.\n *\n * Configuration Options:\n * - `wait`: Delay in milliseconds to wait after the last call (required)\n * - `leading`: Execute on the leading edge of the timeout (default: false)\n * - `trailing`: Execute on the trailing edge of the timeout (default: true)\n * - `enabled`: Whether the debouncer 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 debouncer 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 * - The error state can be checked using the underlying AsyncDebouncer instance\n * - Both onError and throwOnError can be used together - the handler will be called before any error is thrown\n *\n * State Management:\n * - Uses TanStack Store for reactive state management\n * - Use `initialState` to provide initial state values when creating the async debouncer\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 canLeadingExecute, error count, execution status, and success/settle counts\n * - State can be accessed via `asyncDebouncer.store.state` when using the class directly\n * - When using framework adapters (React/Solid), state is accessed from `asyncDebouncer.state`\n *\n * @example\n * ```ts\n * const debounced = asyncDebounce(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 * throwOnError: true // Will both log the error and throw it\n * });\n *\n * // Will only execute once, 1 second after the last call\n * // Returns the API response directly\n * const result = await debounced(\"third\");\n * ```\n */\nexport function asyncDebounce<TFn extends AnyAsyncFunction>(\n fn: TFn,\n initialOptions: AsyncDebouncerOptions<TFn>,\n) {\n const asyncDebouncer = new AsyncDebouncer(fn, initialOptions)\n return asyncDebouncer.maybeExecute\n}\n"],"mappings":"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"}

@@ -1,1 +0,1 @@

{"version":3,"file":"async-queuer.cjs","names":["Store","#setState","#tick","AsyncRetryer","#clearTimeouts","#getAllItems","parseFunctionOrValue","#checkExpiredItems","#getConcurrency","#getWait","#timeoutIds"],"sources":["../src/async-queuer.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 { OptionalKeys } from './types'\nimport type { QueuePosition } from './queuer'\n\nexport interface AsyncQueuerState<TValue> {\n /**\n * Items currently being processed by the queuer\n */\n activeItems: Array<TValue>\n /**\n * Number of times addItem has been called (for reduction calculations)\n */\n addItemCount: number\n /**\n * Number of task executions that have resulted in errors\n */\n errorCount: number\n /**\n * Number of times execute has been called\n */\n executeCount: number\n /**\n * Number of items that have been removed from the queue due to expiration\n */\n expirationCount: number\n /**\n * Whether the queuer has no items to process (items array is empty)\n */\n isEmpty: boolean\n /**\n * Whether the queuer is currently executing\n */\n isExecuting: boolean\n /**\n * Whether the queuer has reached its maximum capacity\n */\n isFull: boolean\n /**\n * Whether the queuer is not currently processing any items\n */\n isIdle: boolean\n /**\n * Whether the queuer is active and will process items automatically\n */\n isRunning: boolean\n /**\n * Array of items currently waiting to be processed\n */\n items: Array<TValue>\n /**\n * Timestamps when items were added to the queue for expiration tracking\n */\n itemTimestamps: Array<number>\n /**\n * The result from the most recent task execution\n */\n lastResult: any\n /**\n * Whether the queuer has a pending timeout for processing the next item\n */\n pendingTick: boolean\n /**\n * Number of items that have been rejected from being added to the queue\n */\n rejectionCount: number\n /**\n * Number of task executions that have completed (either successfully or with errors)\n */\n settledCount: number\n /**\n * Number of items currently in the queue\n */\n size: number\n /**\n * Current processing status - 'idle' when not processing, 'running' when active, 'stopped' when paused\n */\n status: 'idle' | 'running' | 'stopped'\n /**\n * Number of task executions that have completed successfully\n */\n successCount: number\n}\n\nfunction getDefaultAsyncQueuerState<TValue>(): AsyncQueuerState<TValue> {\n return {\n activeItems: [],\n addItemCount: 0,\n errorCount: 0,\n executeCount: 0,\n expirationCount: 0,\n isEmpty: true,\n isExecuting: false,\n isFull: false,\n isIdle: true,\n isRunning: true,\n itemTimestamps: [],\n items: [],\n lastResult: null,\n pendingTick: false,\n rejectionCount: 0,\n settledCount: 0,\n size: 0,\n status: 'idle',\n successCount: 0,\n }\n}\n\nexport interface AsyncQueuerOptions<TValue> {\n /**\n * Options for configuring the underlying async retryer\n */\n asyncRetryerOptions?: AsyncRetryerOptions<(item: TValue) => Promise<any>>\n /**\n * Default position to add items to the queuer\n * @default 'back'\n */\n addItemsTo?: QueuePosition\n /**\n * Maximum number of concurrent tasks to process.\n * Can be a number or a function that returns a number.\n * @default 1\n */\n concurrency?: number | ((queuer: AsyncQueuer<TValue>) => number)\n /**\n * Maximum time in milliseconds that an item can stay in the queue\n * If not provided, items will never expire\n */\n expirationDuration?: number\n /**\n * Function to determine if an item has expired\n * If provided, this overrides the expirationDuration behavior\n */\n getIsExpired?: (item: TValue, addedAt: number) => boolean\n /**\n * Default position to get items from during processing\n * @default 'front'\n */\n getItemsFrom?: QueuePosition\n /**\n * Function to determine priority of items in the queuer\n * Higher priority items will be processed first\n * If not provided, will use static priority values attached to tasks\n */\n getPriority?: (item: TValue) => number\n /**\n * Initial items to populate the queuer with\n */\n initialItems?: Array<TValue>\n /**\n * Initial state for the async queuer\n */\n initialState?: Partial<AsyncQueuerState<TValue>>\n /**\n * Optional key to identify this async queuer instance.\n * If provided, the async queuer will be identified by this key in the devtools and PacerProvider if applicable.\n */\n key?: string\n /**\n * Maximum number of items allowed in the queuer\n */\n maxSize?: number\n /**\n * Optional error handler for when a task throws.\n * If provided, the handler will be called with the error and queuer instance.\n * This can be used alongside throwOnError - the handler will be called before any error is thrown.\n */\n onError?: (error: Error, item: TValue, queuer: AsyncQueuer<TValue>) => void\n /**\n * Callback fired whenever an item expires in the queuer\n */\n onExpire?: (item: TValue, queuer: AsyncQueuer<TValue>) => void\n /**\n * Callback fired whenever an item is added or removed from the queuer\n */\n onItemsChange?: (queuer: AsyncQueuer<TValue>) => void\n /**\n * Callback fired whenever an item is rejected from being added to the queuer\n */\n onReject?: (item: TValue, queuer: AsyncQueuer<TValue>) => void\n /**\n * Optional callback to call when a task is settled\n */\n onSettled?: (item: TValue, queuer: AsyncQueuer<TValue>) => void\n /**\n * Optional callback to call when a task succeeds\n */\n onSuccess?: (result: any, item: TValue, queuer: AsyncQueuer<TValue>) => void\n /**\n * Whether the queuer should start processing tasks immediately or not.\n */\n started?: boolean\n /**\n * Whether to throw errors when they occur.\n * Defaults to true if no onError handler is provided, false if an onError handler is provided.\n * Can be explicitly set to override these defaults.\n */\n throwOnError?: boolean\n /**\n * Time in milliseconds to wait between processing items.\n * Can be a number or a function that returns a number.\n * @default 0\n */\n wait?: number | ((queuer: AsyncQueuer<TValue>) => number)\n}\n\n/**\n * Utility function for sharing common `AsyncQueuerOptions` options between different `AsyncQueuer` instances.\n */\nexport function asyncQueuerOptions<\n TValue = any,\n TOptions extends Partial<AsyncQueuerOptions<TValue>> = Partial<\n AsyncQueuerOptions<TValue>\n >,\n>(options: TOptions): TOptions {\n return options\n}\n\ntype AsyncQueuerOptionsWithOptionalCallbacks = OptionalKeys<\n Required<AsyncQueuerOptions<any>>,\n | 'initialState'\n | 'throwOnError'\n | 'onSuccess'\n | 'onSettled'\n | 'onReject'\n | 'onItemsChange'\n | 'onExpire'\n | 'onError'\n | 'key'\n>\n\nconst defaultOptions: AsyncQueuerOptionsWithOptionalCallbacks = {\n addItemsTo: 'back',\n asyncRetryerOptions: {\n maxAttempts: 1,\n },\n concurrency: 1,\n expirationDuration: Infinity,\n getIsExpired: () => false,\n getItemsFrom: 'front',\n getPriority: (item: any) => item?.priority ?? 0,\n initialItems: [],\n maxSize: Infinity,\n started: true,\n wait: 0,\n}\n\n/**\n * A flexible asynchronous queue for processing tasks with configurable concurrency, priority, and expiration.\n *\n * Async vs Sync Versions:\n * The async version provides advanced features over the sync Queuer:\n * - Returns promises that can be awaited for task results\n * - Built-in retry support via AsyncRetryer integration for each queued task\n * - Abort support to cancel in-flight task executions\n * - Comprehensive error handling with onError callbacks and throwOnError control\n * - Detailed execution tracking (success/error/settle counts)\n * - Concurrent execution support (process multiple items simultaneously)\n *\n * The sync Queuer is lighter weight and simpler when you don't need async features,\n * return values, or execution control.\n *\n * What is Queuing?\n * Queuing is a technique for managing and processing items sequentially or with controlled concurrency.\n * Tasks are processed up to the configured concurrency limit. When a task completes,\n * the next pending task is processed if the concurrency limit allows.\n *\n * Key Features:\n * - Priority queue support via the getPriority option\n * - Configurable concurrency limit\n * - Callbacks for task success, error, completion, and queue state changes\n * - FIFO (First In First Out) or LIFO (Last In First Out) queue behavior\n * - Pause and resume processing\n * - Item expiration to remove stale items from the queue\n *\n * Error Handling:\n * - If an `onError` handler is provided, it will be called with the error and queuer 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 AsyncQueuer 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 queuer\n * - Use `onSuccess` callback to react to successful task execution and implement custom logic\n * - Use `onError` callback to react to task execution errors and implement custom error handling\n * - Use `onSettled` callback to react to task execution completion (success or error) and implement custom logic\n * - Use `onItemsChange` callback to react to items being added or removed from the queue\n * - Use `onExpire` callback to react to items expiring and implement custom logic\n * - Use `onReject` callback to react to items being rejected when the queue is full\n * - The state includes error count, expiration count, rejection count, running status, and success/settle counts\n * - State can be accessed via `asyncQueuer.store.state` when using the class directly\n * - When using framework adapters (React/Solid), state is accessed from `asyncQueuer.state`\n *\n * Example usage:\n * ```ts\n * const asyncQueuer = new AsyncQueuer<string>(async (item) => {\n * // process item\n * return item.toUpperCase();\n * }, {\n * concurrency: 2,\n * onSuccess: (result) => {\n * console.log(result);\n * }\n * });\n *\n * asyncQueuer.addItem('hello');\n * asyncQueuer.start();\n * ```\n */\nexport class AsyncQueuer<TValue> {\n readonly store: Store<Readonly<AsyncQueuerState<TValue>>> = new Store<\n AsyncQueuerState<TValue>\n >(getDefaultAsyncQueuerState<TValue>())\n key: string | undefined\n options: AsyncQueuerOptions<TValue>\n asyncRetryers = new Map<\n number,\n AsyncRetryer<(item: TValue) => Promise<any>>\n >()\n #timeoutIds: Set<NodeJS.Timeout> = new Set()\n\n constructor(\n public fn: (item: TValue) => Promise<any>,\n initialOptions: AsyncQueuerOptions<TValue> = {},\n ) {\n this.key = initialOptions.key\n this.options = {\n ...defaultOptions,\n ...initialOptions,\n throwOnError: initialOptions.throwOnError ?? !initialOptions.onError,\n }\n const isInitiallyRunning =\n this.options.initialState?.isRunning ?? this.options.started ?? true\n this.#setState({\n ...this.options.initialState,\n isRunning: isInitiallyRunning,\n })\n\n if (this.options.initialState?.items) {\n if (this.store.state.isRunning) {\n this.#tick()\n }\n } else {\n for (let i = 0; i < (this.options.initialItems?.length ?? 0); i++) {\n const item = this.options.initialItems![i]!\n const isLast = i === (this.options.initialItems?.length ?? 0) - 1\n this.addItem(item, this.options.addItemsTo ?? 'back', isLast)\n }\n }\n\n if (this.key) {\n pacerEventClient.on('d-AsyncQueuer', (e) => {\n if (e.payload.key !== this.key) return\n this.#setState(\n e.payload.store.state as Partial<AsyncQueuerState<TValue>>,\n )\n this.setOptions(\n e.payload.options as Partial<AsyncQueuerOptions<TValue>>,\n )\n })\n }\n }\n\n /**\n * Updates the queuer options. New options are merged with existing options.\n */\n setOptions = (newOptions: Partial<AsyncQueuerOptions<TValue>>): void => {\n this.options = { ...this.options, ...newOptions }\n }\n\n #setState = (newState: Partial<AsyncQueuerState<TValue>>): void => {\n this.store.setState((state) => {\n const combinedState = {\n ...state,\n ...newState,\n }\n\n const { activeItems, items, isRunning } = combinedState\n\n const size = items.length\n const isFull = size >= (this.options.maxSize ?? Infinity)\n const isEmpty = size === 0\n const isIdle = isRunning && isEmpty && activeItems.length === 0\n\n const status = isIdle ? 'idle' : isRunning ? 'running' : 'stopped'\n\n return {\n ...combinedState,\n isEmpty,\n isFull,\n isIdle,\n size,\n status,\n }\n })\n emitChange('AsyncQueuer', this)\n }\n\n /**\n * Returns the current wait time (in milliseconds) between processing items.\n * If a function is provided, it is called with the queuer instance.\n */\n #getWait = (): number => {\n return parseFunctionOrValue(this.options.wait ?? 0, this)\n }\n\n /**\n * Returns the current concurrency limit for processing items.\n * If a function is provided, it is called with the queuer instance.\n */\n #getConcurrency = (): number => {\n return parseFunctionOrValue(this.options.concurrency ?? 1, this)\n }\n\n /**\n * Processes items in the queue up to the concurrency limit. Internal use only.\n */\n #tick = () => {\n if (!this.store.state.isRunning) {\n this.#setState({ pendingTick: false })\n return\n }\n this.#setState({ pendingTick: true })\n\n // Check for expired items\n this.#checkExpiredItems()\n\n // Process items concurrently up to the concurrency limit\n const activeItems = this.store.state.activeItems\n while (\n activeItems.length < this.#getConcurrency() &&\n this.store.state.items.length > 0\n ) {\n const nextItem = this.peekNextItem()\n if (!nextItem) {\n break\n }\n activeItems.push(nextItem)\n this.#setState({\n activeItems,\n })\n ;(async () => {\n await this.execute()\n\n const wait = this.#getWait()\n if (wait > 0) {\n const timeoutId = setTimeout(() => this.#tick(), wait)\n this.#timeoutIds.add(timeoutId)\n return\n }\n\n this.#tick()\n })()\n }\n\n this.#setState({ pendingTick: false })\n }\n\n /**\n * Adds an item to the queue. If the queue is full, the item is rejected and onReject is called.\n * Items can be inserted based on priority or at the front/back depending on configuration.\n *\n * @example\n * ```ts\n * queuer.addItem({ value: 'task', priority: 10 });\n * queuer.addItem('task2', 'front');\n * ```\n */\n addItem = (\n item: TValue,\n position: QueuePosition = this.options.addItemsTo ?? 'back',\n runOnItemsChange: boolean = true,\n ): boolean => {\n this.#setState({\n addItemCount: this.store.state.addItemCount + 1,\n })\n\n if (this.store.state.items.length >= (this.options.maxSize ?? Infinity)) {\n this.#setState({\n rejectionCount: this.store.state.rejectionCount + 1,\n })\n this.options.onReject?.(item, this)\n return false\n }\n\n // Get priority either from the function or from getPriority option\n const priority =\n this.options.getPriority !== defaultOptions.getPriority\n ? this.options.getPriority!(item)\n : (item as any).priority\n\n const items = this.store.state.items\n const itemTimestamps = this.store.state.itemTimestamps\n\n if (priority !== undefined) {\n // Insert based on priority - higher priority items go to front\n const insertIndex = items.findIndex((existing) => {\n const existingPriority =\n this.options.getPriority !== defaultOptions.getPriority\n ? this.options.getPriority!(existing)\n : (existing as any).priority\n return existingPriority < priority\n })\n\n if (insertIndex === -1) {\n items.push(item)\n itemTimestamps.push(Date.now())\n } else {\n items.splice(insertIndex, 0, item)\n itemTimestamps.splice(insertIndex, 0, Date.now())\n }\n } else {\n if (position === 'front') {\n // Default FIFO/LIFO behavior\n items.unshift(item)\n itemTimestamps.unshift(Date.now())\n } else {\n // LIFO\n items.push(item)\n itemTimestamps.push(Date.now())\n }\n }\n\n this.#setState({\n items,\n itemTimestamps,\n })\n\n if (runOnItemsChange) {\n this.options.onItemsChange?.(this)\n }\n\n if (this.store.state.isRunning && !this.store.state.pendingTick) {\n this.#tick()\n }\n\n return true\n }\n\n /**\n * Removes and returns the next item from the queue without executing the task function.\n * Use for manual queue management. Normally, use execute() to process items.\n *\n * @example\n * ```ts\n * // FIFO\n * queuer.getNextItem();\n * // LIFO\n * queuer.getNextItem('back');\n * ```\n */\n getNextItem = (\n position: QueuePosition = this.options.getItemsFrom ?? 'front',\n ): TValue | undefined => {\n const { items, itemTimestamps } = this.store.state\n let item: TValue | undefined\n\n // When priority function is provided or position is 'front', always get from front (highest priority)\n // Priority takes precedence over FIFO/LIFO behavior\n if (\n this.options.getPriority !== defaultOptions.getPriority ||\n position === 'front'\n ) {\n item = items[0]\n if (item !== undefined) {\n this.#setState({\n items: items.slice(1),\n itemTimestamps: itemTimestamps.slice(1),\n })\n }\n } else {\n item = items[items.length - 1]\n if (item !== undefined) {\n this.#setState({\n items: items.slice(0, -1),\n itemTimestamps: itemTimestamps.slice(0, -1),\n })\n }\n }\n\n if (item !== undefined) {\n this.options.onItemsChange?.(this)\n }\n\n return item\n }\n\n #getAllItems = (): Array<TValue> => {\n const items = this.peekAllItems()\n this.clear()\n return items\n }\n\n /**\n * Removes and returns the next item from the queue and executes the task function with it.\n *\n * @example\n * ```ts\n * queuer.execute();\n * // LIFO\n * queuer.execute('back');\n * ```\n */\n execute = async (position?: QueuePosition): Promise<any> => {\n const item = this.getNextItem(position)\n\n if (item !== undefined) {\n const currentExecuteCount = this.store.state.executeCount + 1\n this.#setState({\n executeCount: currentExecuteCount,\n isExecuting: true,\n })\n try {\n const currentAsyncRetryer = new AsyncRetryer(this.fn, {\n ...this.options.asyncRetryerOptions,\n key: `${this.key}-retryer-${currentExecuteCount}`,\n })\n this.asyncRetryers.set(currentExecuteCount, currentAsyncRetryer)\n const lastResult = await currentAsyncRetryer.execute(item) // EXECUTE!\n this.#setState({\n successCount: this.store.state.successCount + 1,\n lastResult,\n })\n this.options.onSuccess?.(lastResult, item, this)\n } catch (error) {\n this.#setState({\n errorCount: this.store.state.errorCount + 1,\n })\n this.options.onError?.(error as Error, item, this)\n if (this.options.throwOnError) {\n throw error\n }\n } finally {\n this.asyncRetryers.delete(currentExecuteCount) // dispose retryer\n this.#setState({\n activeItems: this.store.state.activeItems.filter(\n (activeItem) => activeItem !== item,\n ),\n isExecuting: false,\n settledCount: this.store.state.settledCount + 1,\n })\n this.options.onSettled?.(item, this)\n }\n }\n return item\n }\n\n /**\n * Processes a specified number of items to execute immediately with no wait time\n * If no numberOfItems is provided, all items will be processed\n */\n flush = async (\n numberOfItems: number = this.store.state.items.length,\n position?: QueuePosition,\n ): Promise<void> => {\n this.#clearTimeouts() // clear any pending timeouts\n await Promise.all(\n Array.from({ length: numberOfItems }, () => this.execute(position)),\n )\n }\n\n /**\n * Processes all items in the queue as a batch using the provided function as an argument\n * The queue is cleared after processing\n */\n flushAsBatch = async (\n batchFunction: (items: Array<TValue>) => Promise<any>,\n ): Promise<void> => {\n this.#clearTimeouts() // clear any pending timeouts\n const items = this.#getAllItems()\n await batchFunction(items)\n }\n\n /**\n * Checks for expired items in the queue and removes them. Calls onExpire for each expired item.\n * Internal use only.\n */\n #checkExpiredItems = (): void => {\n if (\n (this.options.expirationDuration ?? Infinity) === Infinity &&\n this.options.getIsExpired === defaultOptions.getIsExpired\n ) {\n return\n }\n\n const now = Date.now()\n const expiredIndices: Array<number> = []\n\n // Find indices of expired items\n for (let i = 0; i < this.store.state.items.length; i++) {\n const timestamp = this.store.state.itemTimestamps[i]\n if (timestamp === undefined) continue\n\n const item = this.store.state.items[i]\n if (item === undefined) continue\n\n const isExpired =\n this.options.getIsExpired !== defaultOptions.getIsExpired\n ? this.options.getIsExpired!(item, timestamp)\n : now - timestamp > (this.options.expirationDuration ?? Infinity)\n\n if (isExpired) {\n expiredIndices.push(i)\n }\n }\n\n // Remove expired items from back to front to maintain indices\n for (let i = expiredIndices.length - 1; i >= 0; i--) {\n const index = expiredIndices[i]\n if (index === undefined) continue\n\n const expiredItem = this.store.state.items[index]\n if (expiredItem === undefined) continue\n\n const newItems = [...this.store.state.items]\n const newTimestamps = [...this.store.state.itemTimestamps]\n newItems.splice(index, 1)\n newTimestamps.splice(index, 1)\n this.#setState({\n items: newItems,\n itemTimestamps: newTimestamps,\n expirationCount: this.store.state.expirationCount + 1,\n })\n this.options.onExpire?.(expiredItem, this)\n }\n\n if (expiredIndices.length > 0) {\n this.options.onItemsChange?.(this)\n }\n }\n\n /**\n * Returns the next item in the queue without removing it.\n *\n * @example\n * ```ts\n * queuer.peekNextItem(); // front\n * queuer.peekNextItem('back'); // back\n * ```\n */\n peekNextItem = (position: QueuePosition = 'front'): TValue | undefined => {\n if (position === 'front') {\n return this.store.state.items[0]\n }\n return this.store.state.items[this.store.state.items.length - 1]\n }\n\n /**\n * Returns a copy of all items in the queue, including active and pending items.\n */\n peekAllItems = (): Array<TValue> => {\n return [...this.peekActiveItems(), ...this.peekPendingItems()]\n }\n\n /**\n * Returns the items currently being processed (active tasks).\n */\n peekActiveItems = (): Array<TValue> => {\n return [...this.store.state.activeItems]\n }\n\n /**\n * Returns the items waiting to be processed (pending tasks).\n */\n peekPendingItems = (): Array<TValue> => {\n return [...this.store.state.items]\n }\n\n /**\n * Starts processing items in the queue. If already running, does nothing.\n */\n start = (): void => {\n this.#setState({ isRunning: true })\n if (!this.store.state.pendingTick && this.store.state.items.length > 0) {\n this.#tick()\n }\n }\n\n /**\n * Stops processing items in the queue. Does not clear the queue.\n */\n stop = (): void => {\n this.#clearTimeouts()\n this.#setState({ isRunning: false, pendingTick: false })\n }\n\n #clearTimeouts = (): void => {\n this.#timeoutIds.forEach((timeoutId) => clearTimeout(timeoutId))\n this.#timeoutIds.clear()\n }\n\n /**\n * Removes all pending items from the queue.\n * Does NOT affect active tasks.\n */\n clear = (): void => {\n this.#setState({ items: [], itemTimestamps: [] })\n this.options.onItemsChange?.(this)\n }\n\n /**\n * Returns the AbortSignal for a specific execution.\n * If no executeCount is provided, returns the signal for the most recent execution.\n * Returns null if no execution is found or not currently executing.\n *\n * @param executeCount - Optional specific execution to get signal for\n * @example\n * ```typescript\n * const queuer = new AsyncQueuer(\n * async (item: string) => {\n * const signal = queuer.getAbortSignal()\n * if (signal) {\n * const response = await fetch(`/api/process/${item}`, { signal })\n * return response.json()\n * }\n * },\n * { concurrency: 2 }\n * )\n * ```\n */\n getAbortSignal = (executeCount?: number): AbortSignal | null => {\n const count = executeCount ?? this.store.state.executeCount\n const retryer = this.asyncRetryers.get(count)\n return retryer?.getAbortSignal() ?? null\n }\n\n /**\n * Aborts all ongoing executions with the internal abort controllers.\n * Does NOT clear out the items.\n */\n abort = (): void => {\n this.asyncRetryers.forEach((retryer) => retryer.abort())\n this.asyncRetryers.clear()\n this.#setState({\n isExecuting: false,\n })\n }\n\n /**\n * Resets the queuer state to its default values\n */\n reset = (): void => {\n this.#setState(getDefaultAsyncQueuerState<TValue>())\n this.options.onItemsChange?.(this)\n this.asyncRetryers.forEach((retryer) => retryer.reset())\n }\n}\n\n/**\n * Creates a new AsyncQueuer instance and returns a bound addItem function for adding tasks.\n * The queuer is started automatically and ready to process items.\n *\n * Async vs Sync Versions:\n * The async version provides advanced features over the sync queue function:\n * - Returns promises that can be awaited for task results\n * - Built-in retry support via AsyncRetryer integration for each queued task\n * - Abort support to cancel in-flight task executions\n * - Comprehensive error handling with onError callbacks and throwOnError control\n * - Detailed execution tracking (success/error/settle counts)\n * - Concurrent execution support (process multiple items simultaneously)\n *\n * The sync queue function is lighter weight and simpler when you don't need async features,\n * return values, or execution control.\n *\n * What is Queuing?\n * Queuing is a technique for managing and processing items sequentially or with controlled concurrency.\n * Tasks are processed up to the configured concurrency limit. When a task completes,\n * the next pending task is processed if the concurrency limit allows.\n *\n * Configuration Options:\n * - `concurrency`: Maximum number of concurrent tasks (default: 1)\n * - `wait`: Time to wait between processing items (default: 0)\n * - `maxSize`: Maximum number of items allowed in the queue (default: Infinity)\n * - `getPriority`: Function to determine item priority\n * - `addItemsTo`: Default position to add items ('back' or 'front', default: 'back')\n * - `getItemsFrom`: Default position to get items ('front' or 'back', default: 'front')\n * - `expirationDuration`: Maximum time items can stay in queue\n * - `started`: Whether to start processing immediately (default: true)\n * - `asyncRetryerOptions`: Configure retry behavior for task executions\n *\n * Error Handling:\n * - If an `onError` handler is provided, it will be called with the error and queuer 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 AsyncQueuer 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 queuer\n * - Use `onSuccess` callback to react to successful task execution and implement custom logic\n * - Use `onError` callback to react to task execution errors and implement custom error handling\n * - Use `onSettled` callback to react to task execution completion (success or error) and implement custom logic\n * - Use `onItemsChange` callback to react to items being added or removed from the queue\n * - Use `onExpire` callback to react to items expiring and implement custom logic\n * - Use `onReject` callback to react to items being rejected when the queue is full\n * - The state includes error count, expiration count, rejection count, running status, and success/settle counts\n * - State can be accessed via the underlying AsyncQueuer 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 enqueue = asyncQueue<string>(async (item) => {\n * return item.toUpperCase();\n * }, {\n * concurrency: 2,\n * wait: 100,\n * onSuccess: (result) => console.log('Processed:', result)\n * });\n *\n * enqueue('hello');\n * ```\n */\nexport function asyncQueue<TValue>(\n fn: (value: TValue) => Promise<any>,\n initialOptions: AsyncQueuerOptions<TValue>,\n) {\n const asyncQueuer = new AsyncQueuer<TValue>(fn, initialOptions)\n return asyncQueuer.addItem\n}\n"],"mappings":"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"}
{"version":3,"file":"async-queuer.cjs","names":["Store","#setState","#tick","AsyncRetryer","#clearTimeouts","#getAllItems","parseFunctionOrValue","#checkExpiredItems","#getConcurrency","#getWait","#timeoutIds"],"sources":["../src/async-queuer.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 { OptionalKeys } from './types'\nimport type { QueuePosition } from './queuer'\n\nexport interface AsyncQueuerState<TValue> {\n /**\n * Items currently being processed by the queuer\n */\n activeItems: Array<TValue>\n /**\n * Number of times addItem has been called (for reduction calculations)\n */\n addItemCount: number\n /**\n * Number of task executions that have resulted in errors\n */\n errorCount: number\n /**\n * Number of times execute has been called\n */\n executeCount: number\n /**\n * Number of items that have been removed from the queue due to expiration\n */\n expirationCount: number\n /**\n * Whether the queuer has no items to process (items array is empty)\n */\n isEmpty: boolean\n /**\n * Whether the queuer is currently executing\n */\n isExecuting: boolean\n /**\n * Whether the queuer has reached its maximum capacity\n */\n isFull: boolean\n /**\n * Whether the queuer is not currently processing any items\n */\n isIdle: boolean\n /**\n * Whether the queuer is active and will process items automatically\n */\n isRunning: boolean\n /**\n * Array of items currently waiting to be processed\n */\n items: Array<TValue>\n /**\n * Timestamps when items were added to the queue for expiration tracking\n */\n itemTimestamps: Array<number>\n /**\n * The result from the most recent task execution\n */\n lastResult: any\n /**\n * Whether the queuer has a pending timeout for processing the next item\n */\n pendingTick: boolean\n /**\n * Number of items that have been rejected from being added to the queue\n */\n rejectionCount: number\n /**\n * Number of task executions that have completed (either successfully or with errors)\n */\n settledCount: number\n /**\n * Number of items currently in the queue\n */\n size: number\n /**\n * Current processing status - 'idle' when not processing, 'running' when active, 'stopped' when paused\n */\n status: 'idle' | 'running' | 'stopped'\n /**\n * Number of task executions that have completed successfully\n */\n successCount: number\n}\n\nfunction getDefaultAsyncQueuerState<TValue>(): AsyncQueuerState<TValue> {\n return {\n activeItems: [],\n addItemCount: 0,\n errorCount: 0,\n executeCount: 0,\n expirationCount: 0,\n isEmpty: true,\n isExecuting: false,\n isFull: false,\n isIdle: true,\n isRunning: true,\n itemTimestamps: [],\n items: [],\n lastResult: null,\n pendingTick: false,\n rejectionCount: 0,\n settledCount: 0,\n size: 0,\n status: 'idle',\n successCount: 0,\n }\n}\n\nexport interface AsyncQueuerOptions<TValue> {\n /**\n * Options for configuring the underlying async retryer\n */\n asyncRetryerOptions?: AsyncRetryerOptions<(item: TValue) => Promise<any>>\n /**\n * Default position to add items to the queuer\n * @default 'back'\n */\n addItemsTo?: QueuePosition\n /**\n * Maximum number of concurrent tasks to process.\n * Can be a number or a function that returns a number.\n * @default 1\n */\n concurrency?: number | ((queuer: AsyncQueuer<TValue>) => number)\n /**\n * Maximum time in milliseconds that an item can stay in the queue\n * If not provided, items will never expire\n */\n expirationDuration?: number\n /**\n * Function to determine if an item has expired\n * If provided, this overrides the expirationDuration behavior\n */\n getIsExpired?: (item: TValue, addedAt: number) => boolean\n /**\n * Default position to get items from during processing\n * @default 'front'\n */\n getItemsFrom?: QueuePosition\n /**\n * Function to determine priority of items in the queuer\n * Higher priority items will be processed first\n * If not provided, will use static priority values attached to tasks\n */\n getPriority?: (item: TValue) => number\n /**\n * Initial items to populate the queuer with\n */\n initialItems?: Array<TValue>\n /**\n * Initial state for the async queuer\n */\n initialState?: Partial<AsyncQueuerState<TValue>>\n /**\n * Optional key to identify this async queuer instance.\n * If provided, the async queuer will be identified by this key in the devtools and PacerProvider if applicable.\n */\n key?: string\n /**\n * Maximum number of items allowed in the queuer\n */\n maxSize?: number\n /**\n * Optional error handler for when a task throws.\n * If provided, the handler will be called with the error and queuer instance.\n * This can be used alongside throwOnError - the handler will be called before any error is thrown.\n */\n onError?: (error: Error, item: TValue, queuer: AsyncQueuer<TValue>) => void\n /**\n * Callback fired whenever an item expires in the queuer\n */\n onExpire?: (item: TValue, queuer: AsyncQueuer<TValue>) => void\n /**\n * Callback fired whenever an item is added or removed from the queuer\n */\n onItemsChange?: (queuer: AsyncQueuer<TValue>) => void\n /**\n * Callback fired whenever an item is rejected from being added to the queuer\n */\n onReject?: (item: TValue, queuer: AsyncQueuer<TValue>) => void\n /**\n * Optional callback to call when a task is settled\n */\n onSettled?: (item: TValue, queuer: AsyncQueuer<TValue>) => void\n /**\n * Optional callback to call when a task succeeds\n */\n onSuccess?: (result: any, item: TValue, queuer: AsyncQueuer<TValue>) => void\n /**\n * Whether the queuer should start processing tasks immediately or not.\n */\n started?: boolean\n /**\n * Whether to throw errors when they occur.\n * Defaults to true if no onError handler is provided, false if an onError handler is provided.\n * Can be explicitly set to override these defaults.\n */\n throwOnError?: boolean\n /**\n * Time in milliseconds to wait between processing items.\n * Can be a number or a function that returns a number.\n * @default 0\n */\n wait?: number | ((queuer: AsyncQueuer<TValue>) => number)\n}\n\n/**\n * Utility function for sharing common `AsyncQueuerOptions` options between different `AsyncQueuer` instances.\n */\nexport function asyncQueuerOptions<\n TValue = any,\n TOptions extends Partial<AsyncQueuerOptions<TValue>> = Partial<\n AsyncQueuerOptions<TValue>\n >,\n>(options: TOptions): TOptions {\n return options\n}\n\ntype AsyncQueuerOptionsWithOptionalCallbacks = OptionalKeys<\n Required<AsyncQueuerOptions<any>>,\n | 'initialState'\n | 'throwOnError'\n | 'onSuccess'\n | 'onSettled'\n | 'onReject'\n | 'onItemsChange'\n | 'onExpire'\n | 'onError'\n | 'key'\n>\n\nconst defaultOptions: AsyncQueuerOptionsWithOptionalCallbacks = {\n addItemsTo: 'back',\n asyncRetryerOptions: {\n maxAttempts: 1,\n },\n concurrency: 1,\n expirationDuration: Infinity,\n getIsExpired: () => false,\n getItemsFrom: 'front',\n getPriority: (item: any) => item?.priority ?? 0,\n initialItems: [],\n maxSize: Infinity,\n started: true,\n wait: 0,\n}\n\n/**\n * A flexible asynchronous queue for processing tasks with configurable concurrency, priority, and expiration.\n *\n * Async vs Sync Versions:\n * The async version provides advanced features over the sync Queuer:\n * - Returns promises that can be awaited for task results\n * - Built-in retry support via AsyncRetryer integration for each queued task\n * - Abort support to cancel in-flight task executions\n * - Comprehensive error handling with onError callbacks and throwOnError control\n * - Detailed execution tracking (success/error/settle counts)\n * - Concurrent execution support (process multiple items simultaneously)\n *\n * The sync Queuer is lighter weight and simpler when you don't need async features,\n * return values, or execution control.\n *\n * What is Queuing?\n * Queuing is a technique for managing and processing items sequentially or with controlled concurrency.\n * Tasks are processed up to the configured concurrency limit. When a task completes,\n * the next pending task is processed if the concurrency limit allows.\n *\n * Key Features:\n * - Priority queue support via the getPriority option\n * - Configurable concurrency limit\n * - Callbacks for task success, error, completion, and queue state changes\n * - FIFO (First In First Out) or LIFO (Last In First Out) queue behavior\n * - Pause and resume processing\n * - Item expiration to remove stale items from the queue\n *\n * Error Handling:\n * - If an `onError` handler is provided, it will be called with the error and queuer 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 AsyncQueuer 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 queuer\n * - Use `onSuccess` callback to react to successful task execution and implement custom logic\n * - Use `onError` callback to react to task execution errors and implement custom error handling\n * - Use `onSettled` callback to react to task execution completion (success or error) and implement custom logic\n * - Use `onItemsChange` callback to react to items being added or removed from the queue\n * - Use `onExpire` callback to react to items expiring and implement custom logic\n * - Use `onReject` callback to react to items being rejected when the queue is full\n * - The state includes error count, expiration count, rejection count, running status, and success/settle counts\n * - State can be accessed via `asyncQueuer.store.state` when using the class directly\n * - When using framework adapters (React/Solid), state is accessed from `asyncQueuer.state`\n *\n * Example usage:\n * ```ts\n * const asyncQueuer = new AsyncQueuer<string>(async (item) => {\n * // process item\n * return item.toUpperCase();\n * }, {\n * concurrency: 2,\n * onSuccess: (result) => {\n * console.log(result);\n * }\n * });\n *\n * asyncQueuer.addItem('hello');\n * asyncQueuer.start();\n * ```\n */\nexport class AsyncQueuer<TValue> {\n readonly store: Store<Readonly<AsyncQueuerState<TValue>>> = new Store<\n AsyncQueuerState<TValue>\n >(getDefaultAsyncQueuerState<TValue>())\n key: string | undefined\n options: AsyncQueuerOptions<TValue>\n asyncRetryers = new Map<\n number,\n AsyncRetryer<(item: TValue) => Promise<any>>\n >()\n #timeoutIds: Set<ReturnType<typeof setTimeout>> = new Set()\n\n constructor(\n public fn: (item: TValue) => Promise<any>,\n initialOptions: AsyncQueuerOptions<TValue> = {},\n ) {\n this.key = initialOptions.key\n this.options = {\n ...defaultOptions,\n ...initialOptions,\n throwOnError: initialOptions.throwOnError ?? !initialOptions.onError,\n }\n const isInitiallyRunning =\n this.options.initialState?.isRunning ?? this.options.started ?? true\n this.#setState({\n ...this.options.initialState,\n isRunning: isInitiallyRunning,\n })\n\n if (this.options.initialState?.items) {\n if (this.store.state.isRunning) {\n this.#tick()\n }\n } else {\n for (let i = 0; i < (this.options.initialItems?.length ?? 0); i++) {\n const item = this.options.initialItems![i]!\n const isLast = i === (this.options.initialItems?.length ?? 0) - 1\n this.addItem(item, this.options.addItemsTo ?? 'back', isLast)\n }\n }\n\n if (this.key) {\n pacerEventClient.on('d-AsyncQueuer', (e) => {\n if (e.payload.key !== this.key) return\n this.#setState(\n e.payload.store.state as Partial<AsyncQueuerState<TValue>>,\n )\n this.setOptions(\n e.payload.options as Partial<AsyncQueuerOptions<TValue>>,\n )\n })\n }\n }\n\n /**\n * Updates the queuer options. New options are merged with existing options.\n */\n setOptions = (newOptions: Partial<AsyncQueuerOptions<TValue>>): void => {\n this.options = { ...this.options, ...newOptions }\n }\n\n #setState = (newState: Partial<AsyncQueuerState<TValue>>): void => {\n this.store.setState((state) => {\n const combinedState = {\n ...state,\n ...newState,\n }\n\n const { activeItems, items, isRunning } = combinedState\n\n const size = items.length\n const isFull = size >= (this.options.maxSize ?? Infinity)\n const isEmpty = size === 0\n const isIdle = isRunning && isEmpty && activeItems.length === 0\n\n const status = isIdle ? 'idle' : isRunning ? 'running' : 'stopped'\n\n return {\n ...combinedState,\n isEmpty,\n isFull,\n isIdle,\n size,\n status,\n }\n })\n emitChange('AsyncQueuer', this)\n }\n\n /**\n * Returns the current wait time (in milliseconds) between processing items.\n * If a function is provided, it is called with the queuer instance.\n */\n #getWait = (): number => {\n return parseFunctionOrValue(this.options.wait ?? 0, this)\n }\n\n /**\n * Returns the current concurrency limit for processing items.\n * If a function is provided, it is called with the queuer instance.\n */\n #getConcurrency = (): number => {\n return parseFunctionOrValue(this.options.concurrency ?? 1, this)\n }\n\n /**\n * Processes items in the queue up to the concurrency limit. Internal use only.\n */\n #tick = () => {\n if (!this.store.state.isRunning) {\n this.#setState({ pendingTick: false })\n return\n }\n this.#setState({ pendingTick: true })\n\n // Check for expired items\n this.#checkExpiredItems()\n\n // Process items concurrently up to the concurrency limit\n const activeItems = this.store.state.activeItems\n while (\n activeItems.length < this.#getConcurrency() &&\n this.store.state.items.length > 0\n ) {\n const nextItem = this.peekNextItem()\n if (!nextItem) {\n break\n }\n activeItems.push(nextItem)\n this.#setState({\n activeItems,\n })\n ;(async () => {\n await this.execute()\n\n const wait = this.#getWait()\n if (wait > 0) {\n const timeoutId = setTimeout(() => this.#tick(), wait)\n this.#timeoutIds.add(timeoutId)\n return\n }\n\n this.#tick()\n })()\n }\n\n this.#setState({ pendingTick: false })\n }\n\n /**\n * Adds an item to the queue. If the queue is full, the item is rejected and onReject is called.\n * Items can be inserted based on priority or at the front/back depending on configuration.\n *\n * @example\n * ```ts\n * queuer.addItem({ value: 'task', priority: 10 });\n * queuer.addItem('task2', 'front');\n * ```\n */\n addItem = (\n item: TValue,\n position: QueuePosition = this.options.addItemsTo ?? 'back',\n runOnItemsChange: boolean = true,\n ): boolean => {\n this.#setState({\n addItemCount: this.store.state.addItemCount + 1,\n })\n\n if (this.store.state.items.length >= (this.options.maxSize ?? Infinity)) {\n this.#setState({\n rejectionCount: this.store.state.rejectionCount + 1,\n })\n this.options.onReject?.(item, this)\n return false\n }\n\n // Get priority either from the function or from getPriority option\n const priority =\n this.options.getPriority !== defaultOptions.getPriority\n ? this.options.getPriority!(item)\n : (item as any).priority\n\n const items = this.store.state.items\n const itemTimestamps = this.store.state.itemTimestamps\n\n if (priority !== undefined) {\n // Insert based on priority - higher priority items go to front\n const insertIndex = items.findIndex((existing) => {\n const existingPriority =\n this.options.getPriority !== defaultOptions.getPriority\n ? this.options.getPriority!(existing)\n : (existing as any).priority\n return existingPriority < priority\n })\n\n if (insertIndex === -1) {\n items.push(item)\n itemTimestamps.push(Date.now())\n } else {\n items.splice(insertIndex, 0, item)\n itemTimestamps.splice(insertIndex, 0, Date.now())\n }\n } else {\n if (position === 'front') {\n // Default FIFO/LIFO behavior\n items.unshift(item)\n itemTimestamps.unshift(Date.now())\n } else {\n // LIFO\n items.push(item)\n itemTimestamps.push(Date.now())\n }\n }\n\n this.#setState({\n items,\n itemTimestamps,\n })\n\n if (runOnItemsChange) {\n this.options.onItemsChange?.(this)\n }\n\n if (this.store.state.isRunning && !this.store.state.pendingTick) {\n this.#tick()\n }\n\n return true\n }\n\n /**\n * Removes and returns the next item from the queue without executing the task function.\n * Use for manual queue management. Normally, use execute() to process items.\n *\n * @example\n * ```ts\n * // FIFO\n * queuer.getNextItem();\n * // LIFO\n * queuer.getNextItem('back');\n * ```\n */\n getNextItem = (\n position: QueuePosition = this.options.getItemsFrom ?? 'front',\n ): TValue | undefined => {\n const { items, itemTimestamps } = this.store.state\n let item: TValue | undefined\n\n // When priority function is provided or position is 'front', always get from front (highest priority)\n // Priority takes precedence over FIFO/LIFO behavior\n if (\n this.options.getPriority !== defaultOptions.getPriority ||\n position === 'front'\n ) {\n item = items[0]\n if (item !== undefined) {\n this.#setState({\n items: items.slice(1),\n itemTimestamps: itemTimestamps.slice(1),\n })\n }\n } else {\n item = items[items.length - 1]\n if (item !== undefined) {\n this.#setState({\n items: items.slice(0, -1),\n itemTimestamps: itemTimestamps.slice(0, -1),\n })\n }\n }\n\n if (item !== undefined) {\n this.options.onItemsChange?.(this)\n }\n\n return item\n }\n\n #getAllItems = (): Array<TValue> => {\n const items = this.peekAllItems()\n this.clear()\n return items\n }\n\n /**\n * Removes and returns the next item from the queue and executes the task function with it.\n *\n * @example\n * ```ts\n * queuer.execute();\n * // LIFO\n * queuer.execute('back');\n * ```\n */\n execute = async (position?: QueuePosition): Promise<any> => {\n const item = this.getNextItem(position)\n\n if (item !== undefined) {\n const currentExecuteCount = this.store.state.executeCount + 1\n this.#setState({\n executeCount: currentExecuteCount,\n isExecuting: true,\n })\n try {\n const currentAsyncRetryer = new AsyncRetryer(this.fn, {\n ...this.options.asyncRetryerOptions,\n key: `${this.key}-retryer-${currentExecuteCount}`,\n })\n this.asyncRetryers.set(currentExecuteCount, currentAsyncRetryer)\n const lastResult = await currentAsyncRetryer.execute(item) // EXECUTE!\n this.#setState({\n successCount: this.store.state.successCount + 1,\n lastResult,\n })\n this.options.onSuccess?.(lastResult, item, this)\n } catch (error) {\n this.#setState({\n errorCount: this.store.state.errorCount + 1,\n })\n this.options.onError?.(error as Error, item, this)\n if (this.options.throwOnError) {\n throw error\n }\n } finally {\n this.asyncRetryers.delete(currentExecuteCount) // dispose retryer\n this.#setState({\n activeItems: this.store.state.activeItems.filter(\n (activeItem) => activeItem !== item,\n ),\n isExecuting: false,\n settledCount: this.store.state.settledCount + 1,\n })\n this.options.onSettled?.(item, this)\n }\n }\n return item\n }\n\n /**\n * Processes a specified number of items to execute immediately with no wait time\n * If no numberOfItems is provided, all items will be processed\n */\n flush = async (\n numberOfItems: number = this.store.state.items.length,\n position?: QueuePosition,\n ): Promise<void> => {\n this.#clearTimeouts() // clear any pending timeouts\n await Promise.all(\n Array.from({ length: numberOfItems }, () => this.execute(position)),\n )\n }\n\n /**\n * Processes all items in the queue as a batch using the provided function as an argument\n * The queue is cleared after processing\n */\n flushAsBatch = async (\n batchFunction: (items: Array<TValue>) => Promise<any>,\n ): Promise<void> => {\n this.#clearTimeouts() // clear any pending timeouts\n const items = this.#getAllItems()\n await batchFunction(items)\n }\n\n /**\n * Checks for expired items in the queue and removes them. Calls onExpire for each expired item.\n * Internal use only.\n */\n #checkExpiredItems = (): void => {\n if (\n (this.options.expirationDuration ?? Infinity) === Infinity &&\n this.options.getIsExpired === defaultOptions.getIsExpired\n ) {\n return\n }\n\n const now = Date.now()\n const expiredIndices: Array<number> = []\n\n // Find indices of expired items\n for (let i = 0; i < this.store.state.items.length; i++) {\n const timestamp = this.store.state.itemTimestamps[i]\n if (timestamp === undefined) continue\n\n const item = this.store.state.items[i]\n if (item === undefined) continue\n\n const isExpired =\n this.options.getIsExpired !== defaultOptions.getIsExpired\n ? this.options.getIsExpired!(item, timestamp)\n : now - timestamp > (this.options.expirationDuration ?? Infinity)\n\n if (isExpired) {\n expiredIndices.push(i)\n }\n }\n\n // Remove expired items from back to front to maintain indices\n for (let i = expiredIndices.length - 1; i >= 0; i--) {\n const index = expiredIndices[i]\n if (index === undefined) continue\n\n const expiredItem = this.store.state.items[index]\n if (expiredItem === undefined) continue\n\n const newItems = [...this.store.state.items]\n const newTimestamps = [...this.store.state.itemTimestamps]\n newItems.splice(index, 1)\n newTimestamps.splice(index, 1)\n this.#setState({\n items: newItems,\n itemTimestamps: newTimestamps,\n expirationCount: this.store.state.expirationCount + 1,\n })\n this.options.onExpire?.(expiredItem, this)\n }\n\n if (expiredIndices.length > 0) {\n this.options.onItemsChange?.(this)\n }\n }\n\n /**\n * Returns the next item in the queue without removing it.\n *\n * @example\n * ```ts\n * queuer.peekNextItem(); // front\n * queuer.peekNextItem('back'); // back\n * ```\n */\n peekNextItem = (position: QueuePosition = 'front'): TValue | undefined => {\n if (position === 'front') {\n return this.store.state.items[0]\n }\n return this.store.state.items[this.store.state.items.length - 1]\n }\n\n /**\n * Returns a copy of all items in the queue, including active and pending items.\n */\n peekAllItems = (): Array<TValue> => {\n return [...this.peekActiveItems(), ...this.peekPendingItems()]\n }\n\n /**\n * Returns the items currently being processed (active tasks).\n */\n peekActiveItems = (): Array<TValue> => {\n return [...this.store.state.activeItems]\n }\n\n /**\n * Returns the items waiting to be processed (pending tasks).\n */\n peekPendingItems = (): Array<TValue> => {\n return [...this.store.state.items]\n }\n\n /**\n * Starts processing items in the queue. If already running, does nothing.\n */\n start = (): void => {\n this.#setState({ isRunning: true })\n if (!this.store.state.pendingTick && this.store.state.items.length > 0) {\n this.#tick()\n }\n }\n\n /**\n * Stops processing items in the queue. Does not clear the queue.\n */\n stop = (): void => {\n this.#clearTimeouts()\n this.#setState({ isRunning: false, pendingTick: false })\n }\n\n #clearTimeouts = (): void => {\n this.#timeoutIds.forEach((timeoutId) => clearTimeout(timeoutId))\n this.#timeoutIds.clear()\n }\n\n /**\n * Removes all pending items from the queue.\n * Does NOT affect active tasks.\n */\n clear = (): void => {\n this.#setState({ items: [], itemTimestamps: [] })\n this.options.onItemsChange?.(this)\n }\n\n /**\n * Returns the AbortSignal for a specific execution.\n * If no executeCount is provided, returns the signal for the most recent execution.\n * Returns null if no execution is found or not currently executing.\n *\n * @param executeCount - Optional specific execution to get signal for\n * @example\n * ```typescript\n * const queuer = new AsyncQueuer(\n * async (item: string) => {\n * const signal = queuer.getAbortSignal()\n * if (signal) {\n * const response = await fetch(`/api/process/${item}`, { signal })\n * return response.json()\n * }\n * },\n * { concurrency: 2 }\n * )\n * ```\n */\n getAbortSignal = (executeCount?: number): AbortSignal | null => {\n const count = executeCount ?? this.store.state.executeCount\n const retryer = this.asyncRetryers.get(count)\n return retryer?.getAbortSignal() ?? null\n }\n\n /**\n * Aborts all ongoing executions with the internal abort controllers.\n * Does NOT clear out the items.\n */\n abort = (): void => {\n this.asyncRetryers.forEach((retryer) => retryer.abort())\n this.asyncRetryers.clear()\n this.#setState({\n isExecuting: false,\n })\n }\n\n /**\n * Resets the queuer state to its default values\n */\n reset = (): void => {\n this.#setState(getDefaultAsyncQueuerState<TValue>())\n this.options.onItemsChange?.(this)\n this.asyncRetryers.forEach((retryer) => retryer.reset())\n }\n}\n\n/**\n * Creates a new AsyncQueuer instance and returns a bound addItem function for adding tasks.\n * The queuer is started automatically and ready to process items.\n *\n * Async vs Sync Versions:\n * The async version provides advanced features over the sync queue function:\n * - Returns promises that can be awaited for task results\n * - Built-in retry support via AsyncRetryer integration for each queued task\n * - Abort support to cancel in-flight task executions\n * - Comprehensive error handling with onError callbacks and throwOnError control\n * - Detailed execution tracking (success/error/settle counts)\n * - Concurrent execution support (process multiple items simultaneously)\n *\n * The sync queue function is lighter weight and simpler when you don't need async features,\n * return values, or execution control.\n *\n * What is Queuing?\n * Queuing is a technique for managing and processing items sequentially or with controlled concurrency.\n * Tasks are processed up to the configured concurrency limit. When a task completes,\n * the next pending task is processed if the concurrency limit allows.\n *\n * Configuration Options:\n * - `concurrency`: Maximum number of concurrent tasks (default: 1)\n * - `wait`: Time to wait between processing items (default: 0)\n * - `maxSize`: Maximum number of items allowed in the queue (default: Infinity)\n * - `getPriority`: Function to determine item priority\n * - `addItemsTo`: Default position to add items ('back' or 'front', default: 'back')\n * - `getItemsFrom`: Default position to get items ('front' or 'back', default: 'front')\n * - `expirationDuration`: Maximum time items can stay in queue\n * - `started`: Whether to start processing immediately (default: true)\n * - `asyncRetryerOptions`: Configure retry behavior for task executions\n *\n * Error Handling:\n * - If an `onError` handler is provided, it will be called with the error and queuer 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 AsyncQueuer 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 queuer\n * - Use `onSuccess` callback to react to successful task execution and implement custom logic\n * - Use `onError` callback to react to task execution errors and implement custom error handling\n * - Use `onSettled` callback to react to task execution completion (success or error) and implement custom logic\n * - Use `onItemsChange` callback to react to items being added or removed from the queue\n * - Use `onExpire` callback to react to items expiring and implement custom logic\n * - Use `onReject` callback to react to items being rejected when the queue is full\n * - The state includes error count, expiration count, rejection count, running status, and success/settle counts\n * - State can be accessed via the underlying AsyncQueuer 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 enqueue = asyncQueue<string>(async (item) => {\n * return item.toUpperCase();\n * }, {\n * concurrency: 2,\n * wait: 100,\n * onSuccess: (result) => console.log('Processed:', result)\n * });\n *\n * enqueue('hello');\n * ```\n */\nexport function asyncQueue<TValue>(\n fn: (value: TValue) => Promise<any>,\n initialOptions: AsyncQueuerOptions<TValue>,\n) {\n const asyncQueuer = new AsyncQueuer<TValue>(fn, initialOptions)\n return asyncQueuer.addItem\n}\n"],"mappings":"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"}

@@ -1,1 +0,1 @@

{"version":3,"file":"async-queuer.js","names":["#setState","#tick","#clearTimeouts","#getAllItems","#checkExpiredItems","#getConcurrency","#getWait","#timeoutIds"],"sources":["../src/async-queuer.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 { OptionalKeys } from './types'\nimport type { QueuePosition } from './queuer'\n\nexport interface AsyncQueuerState<TValue> {\n /**\n * Items currently being processed by the queuer\n */\n activeItems: Array<TValue>\n /**\n * Number of times addItem has been called (for reduction calculations)\n */\n addItemCount: number\n /**\n * Number of task executions that have resulted in errors\n */\n errorCount: number\n /**\n * Number of times execute has been called\n */\n executeCount: number\n /**\n * Number of items that have been removed from the queue due to expiration\n */\n expirationCount: number\n /**\n * Whether the queuer has no items to process (items array is empty)\n */\n isEmpty: boolean\n /**\n * Whether the queuer is currently executing\n */\n isExecuting: boolean\n /**\n * Whether the queuer has reached its maximum capacity\n */\n isFull: boolean\n /**\n * Whether the queuer is not currently processing any items\n */\n isIdle: boolean\n /**\n * Whether the queuer is active and will process items automatically\n */\n isRunning: boolean\n /**\n * Array of items currently waiting to be processed\n */\n items: Array<TValue>\n /**\n * Timestamps when items were added to the queue for expiration tracking\n */\n itemTimestamps: Array<number>\n /**\n * The result from the most recent task execution\n */\n lastResult: any\n /**\n * Whether the queuer has a pending timeout for processing the next item\n */\n pendingTick: boolean\n /**\n * Number of items that have been rejected from being added to the queue\n */\n rejectionCount: number\n /**\n * Number of task executions that have completed (either successfully or with errors)\n */\n settledCount: number\n /**\n * Number of items currently in the queue\n */\n size: number\n /**\n * Current processing status - 'idle' when not processing, 'running' when active, 'stopped' when paused\n */\n status: 'idle' | 'running' | 'stopped'\n /**\n * Number of task executions that have completed successfully\n */\n successCount: number\n}\n\nfunction getDefaultAsyncQueuerState<TValue>(): AsyncQueuerState<TValue> {\n return {\n activeItems: [],\n addItemCount: 0,\n errorCount: 0,\n executeCount: 0,\n expirationCount: 0,\n isEmpty: true,\n isExecuting: false,\n isFull: false,\n isIdle: true,\n isRunning: true,\n itemTimestamps: [],\n items: [],\n lastResult: null,\n pendingTick: false,\n rejectionCount: 0,\n settledCount: 0,\n size: 0,\n status: 'idle',\n successCount: 0,\n }\n}\n\nexport interface AsyncQueuerOptions<TValue> {\n /**\n * Options for configuring the underlying async retryer\n */\n asyncRetryerOptions?: AsyncRetryerOptions<(item: TValue) => Promise<any>>\n /**\n * Default position to add items to the queuer\n * @default 'back'\n */\n addItemsTo?: QueuePosition\n /**\n * Maximum number of concurrent tasks to process.\n * Can be a number or a function that returns a number.\n * @default 1\n */\n concurrency?: number | ((queuer: AsyncQueuer<TValue>) => number)\n /**\n * Maximum time in milliseconds that an item can stay in the queue\n * If not provided, items will never expire\n */\n expirationDuration?: number\n /**\n * Function to determine if an item has expired\n * If provided, this overrides the expirationDuration behavior\n */\n getIsExpired?: (item: TValue, addedAt: number) => boolean\n /**\n * Default position to get items from during processing\n * @default 'front'\n */\n getItemsFrom?: QueuePosition\n /**\n * Function to determine priority of items in the queuer\n * Higher priority items will be processed first\n * If not provided, will use static priority values attached to tasks\n */\n getPriority?: (item: TValue) => number\n /**\n * Initial items to populate the queuer with\n */\n initialItems?: Array<TValue>\n /**\n * Initial state for the async queuer\n */\n initialState?: Partial<AsyncQueuerState<TValue>>\n /**\n * Optional key to identify this async queuer instance.\n * If provided, the async queuer will be identified by this key in the devtools and PacerProvider if applicable.\n */\n key?: string\n /**\n * Maximum number of items allowed in the queuer\n */\n maxSize?: number\n /**\n * Optional error handler for when a task throws.\n * If provided, the handler will be called with the error and queuer instance.\n * This can be used alongside throwOnError - the handler will be called before any error is thrown.\n */\n onError?: (error: Error, item: TValue, queuer: AsyncQueuer<TValue>) => void\n /**\n * Callback fired whenever an item expires in the queuer\n */\n onExpire?: (item: TValue, queuer: AsyncQueuer<TValue>) => void\n /**\n * Callback fired whenever an item is added or removed from the queuer\n */\n onItemsChange?: (queuer: AsyncQueuer<TValue>) => void\n /**\n * Callback fired whenever an item is rejected from being added to the queuer\n */\n onReject?: (item: TValue, queuer: AsyncQueuer<TValue>) => void\n /**\n * Optional callback to call when a task is settled\n */\n onSettled?: (item: TValue, queuer: AsyncQueuer<TValue>) => void\n /**\n * Optional callback to call when a task succeeds\n */\n onSuccess?: (result: any, item: TValue, queuer: AsyncQueuer<TValue>) => void\n /**\n * Whether the queuer should start processing tasks immediately or not.\n */\n started?: boolean\n /**\n * Whether to throw errors when they occur.\n * Defaults to true if no onError handler is provided, false if an onError handler is provided.\n * Can be explicitly set to override these defaults.\n */\n throwOnError?: boolean\n /**\n * Time in milliseconds to wait between processing items.\n * Can be a number or a function that returns a number.\n * @default 0\n */\n wait?: number | ((queuer: AsyncQueuer<TValue>) => number)\n}\n\n/**\n * Utility function for sharing common `AsyncQueuerOptions` options between different `AsyncQueuer` instances.\n */\nexport function asyncQueuerOptions<\n TValue = any,\n TOptions extends Partial<AsyncQueuerOptions<TValue>> = Partial<\n AsyncQueuerOptions<TValue>\n >,\n>(options: TOptions): TOptions {\n return options\n}\n\ntype AsyncQueuerOptionsWithOptionalCallbacks = OptionalKeys<\n Required<AsyncQueuerOptions<any>>,\n | 'initialState'\n | 'throwOnError'\n | 'onSuccess'\n | 'onSettled'\n | 'onReject'\n | 'onItemsChange'\n | 'onExpire'\n | 'onError'\n | 'key'\n>\n\nconst defaultOptions: AsyncQueuerOptionsWithOptionalCallbacks = {\n addItemsTo: 'back',\n asyncRetryerOptions: {\n maxAttempts: 1,\n },\n concurrency: 1,\n expirationDuration: Infinity,\n getIsExpired: () => false,\n getItemsFrom: 'front',\n getPriority: (item: any) => item?.priority ?? 0,\n initialItems: [],\n maxSize: Infinity,\n started: true,\n wait: 0,\n}\n\n/**\n * A flexible asynchronous queue for processing tasks with configurable concurrency, priority, and expiration.\n *\n * Async vs Sync Versions:\n * The async version provides advanced features over the sync Queuer:\n * - Returns promises that can be awaited for task results\n * - Built-in retry support via AsyncRetryer integration for each queued task\n * - Abort support to cancel in-flight task executions\n * - Comprehensive error handling with onError callbacks and throwOnError control\n * - Detailed execution tracking (success/error/settle counts)\n * - Concurrent execution support (process multiple items simultaneously)\n *\n * The sync Queuer is lighter weight and simpler when you don't need async features,\n * return values, or execution control.\n *\n * What is Queuing?\n * Queuing is a technique for managing and processing items sequentially or with controlled concurrency.\n * Tasks are processed up to the configured concurrency limit. When a task completes,\n * the next pending task is processed if the concurrency limit allows.\n *\n * Key Features:\n * - Priority queue support via the getPriority option\n * - Configurable concurrency limit\n * - Callbacks for task success, error, completion, and queue state changes\n * - FIFO (First In First Out) or LIFO (Last In First Out) queue behavior\n * - Pause and resume processing\n * - Item expiration to remove stale items from the queue\n *\n * Error Handling:\n * - If an `onError` handler is provided, it will be called with the error and queuer 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 AsyncQueuer 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 queuer\n * - Use `onSuccess` callback to react to successful task execution and implement custom logic\n * - Use `onError` callback to react to task execution errors and implement custom error handling\n * - Use `onSettled` callback to react to task execution completion (success or error) and implement custom logic\n * - Use `onItemsChange` callback to react to items being added or removed from the queue\n * - Use `onExpire` callback to react to items expiring and implement custom logic\n * - Use `onReject` callback to react to items being rejected when the queue is full\n * - The state includes error count, expiration count, rejection count, running status, and success/settle counts\n * - State can be accessed via `asyncQueuer.store.state` when using the class directly\n * - When using framework adapters (React/Solid), state is accessed from `asyncQueuer.state`\n *\n * Example usage:\n * ```ts\n * const asyncQueuer = new AsyncQueuer<string>(async (item) => {\n * // process item\n * return item.toUpperCase();\n * }, {\n * concurrency: 2,\n * onSuccess: (result) => {\n * console.log(result);\n * }\n * });\n *\n * asyncQueuer.addItem('hello');\n * asyncQueuer.start();\n * ```\n */\nexport class AsyncQueuer<TValue> {\n readonly store: Store<Readonly<AsyncQueuerState<TValue>>> = new Store<\n AsyncQueuerState<TValue>\n >(getDefaultAsyncQueuerState<TValue>())\n key: string | undefined\n options: AsyncQueuerOptions<TValue>\n asyncRetryers = new Map<\n number,\n AsyncRetryer<(item: TValue) => Promise<any>>\n >()\n #timeoutIds: Set<NodeJS.Timeout> = new Set()\n\n constructor(\n public fn: (item: TValue) => Promise<any>,\n initialOptions: AsyncQueuerOptions<TValue> = {},\n ) {\n this.key = initialOptions.key\n this.options = {\n ...defaultOptions,\n ...initialOptions,\n throwOnError: initialOptions.throwOnError ?? !initialOptions.onError,\n }\n const isInitiallyRunning =\n this.options.initialState?.isRunning ?? this.options.started ?? true\n this.#setState({\n ...this.options.initialState,\n isRunning: isInitiallyRunning,\n })\n\n if (this.options.initialState?.items) {\n if (this.store.state.isRunning) {\n this.#tick()\n }\n } else {\n for (let i = 0; i < (this.options.initialItems?.length ?? 0); i++) {\n const item = this.options.initialItems![i]!\n const isLast = i === (this.options.initialItems?.length ?? 0) - 1\n this.addItem(item, this.options.addItemsTo ?? 'back', isLast)\n }\n }\n\n if (this.key) {\n pacerEventClient.on('d-AsyncQueuer', (e) => {\n if (e.payload.key !== this.key) return\n this.#setState(\n e.payload.store.state as Partial<AsyncQueuerState<TValue>>,\n )\n this.setOptions(\n e.payload.options as Partial<AsyncQueuerOptions<TValue>>,\n )\n })\n }\n }\n\n /**\n * Updates the queuer options. New options are merged with existing options.\n */\n setOptions = (newOptions: Partial<AsyncQueuerOptions<TValue>>): void => {\n this.options = { ...this.options, ...newOptions }\n }\n\n #setState = (newState: Partial<AsyncQueuerState<TValue>>): void => {\n this.store.setState((state) => {\n const combinedState = {\n ...state,\n ...newState,\n }\n\n const { activeItems, items, isRunning } = combinedState\n\n const size = items.length\n const isFull = size >= (this.options.maxSize ?? Infinity)\n const isEmpty = size === 0\n const isIdle = isRunning && isEmpty && activeItems.length === 0\n\n const status = isIdle ? 'idle' : isRunning ? 'running' : 'stopped'\n\n return {\n ...combinedState,\n isEmpty,\n isFull,\n isIdle,\n size,\n status,\n }\n })\n emitChange('AsyncQueuer', this)\n }\n\n /**\n * Returns the current wait time (in milliseconds) between processing items.\n * If a function is provided, it is called with the queuer instance.\n */\n #getWait = (): number => {\n return parseFunctionOrValue(this.options.wait ?? 0, this)\n }\n\n /**\n * Returns the current concurrency limit for processing items.\n * If a function is provided, it is called with the queuer instance.\n */\n #getConcurrency = (): number => {\n return parseFunctionOrValue(this.options.concurrency ?? 1, this)\n }\n\n /**\n * Processes items in the queue up to the concurrency limit. Internal use only.\n */\n #tick = () => {\n if (!this.store.state.isRunning) {\n this.#setState({ pendingTick: false })\n return\n }\n this.#setState({ pendingTick: true })\n\n // Check for expired items\n this.#checkExpiredItems()\n\n // Process items concurrently up to the concurrency limit\n const activeItems = this.store.state.activeItems\n while (\n activeItems.length < this.#getConcurrency() &&\n this.store.state.items.length > 0\n ) {\n const nextItem = this.peekNextItem()\n if (!nextItem) {\n break\n }\n activeItems.push(nextItem)\n this.#setState({\n activeItems,\n })\n ;(async () => {\n await this.execute()\n\n const wait = this.#getWait()\n if (wait > 0) {\n const timeoutId = setTimeout(() => this.#tick(), wait)\n this.#timeoutIds.add(timeoutId)\n return\n }\n\n this.#tick()\n })()\n }\n\n this.#setState({ pendingTick: false })\n }\n\n /**\n * Adds an item to the queue. If the queue is full, the item is rejected and onReject is called.\n * Items can be inserted based on priority or at the front/back depending on configuration.\n *\n * @example\n * ```ts\n * queuer.addItem({ value: 'task', priority: 10 });\n * queuer.addItem('task2', 'front');\n * ```\n */\n addItem = (\n item: TValue,\n position: QueuePosition = this.options.addItemsTo ?? 'back',\n runOnItemsChange: boolean = true,\n ): boolean => {\n this.#setState({\n addItemCount: this.store.state.addItemCount + 1,\n })\n\n if (this.store.state.items.length >= (this.options.maxSize ?? Infinity)) {\n this.#setState({\n rejectionCount: this.store.state.rejectionCount + 1,\n })\n this.options.onReject?.(item, this)\n return false\n }\n\n // Get priority either from the function or from getPriority option\n const priority =\n this.options.getPriority !== defaultOptions.getPriority\n ? this.options.getPriority!(item)\n : (item as any).priority\n\n const items = this.store.state.items\n const itemTimestamps = this.store.state.itemTimestamps\n\n if (priority !== undefined) {\n // Insert based on priority - higher priority items go to front\n const insertIndex = items.findIndex((existing) => {\n const existingPriority =\n this.options.getPriority !== defaultOptions.getPriority\n ? this.options.getPriority!(existing)\n : (existing as any).priority\n return existingPriority < priority\n })\n\n if (insertIndex === -1) {\n items.push(item)\n itemTimestamps.push(Date.now())\n } else {\n items.splice(insertIndex, 0, item)\n itemTimestamps.splice(insertIndex, 0, Date.now())\n }\n } else {\n if (position === 'front') {\n // Default FIFO/LIFO behavior\n items.unshift(item)\n itemTimestamps.unshift(Date.now())\n } else {\n // LIFO\n items.push(item)\n itemTimestamps.push(Date.now())\n }\n }\n\n this.#setState({\n items,\n itemTimestamps,\n })\n\n if (runOnItemsChange) {\n this.options.onItemsChange?.(this)\n }\n\n if (this.store.state.isRunning && !this.store.state.pendingTick) {\n this.#tick()\n }\n\n return true\n }\n\n /**\n * Removes and returns the next item from the queue without executing the task function.\n * Use for manual queue management. Normally, use execute() to process items.\n *\n * @example\n * ```ts\n * // FIFO\n * queuer.getNextItem();\n * // LIFO\n * queuer.getNextItem('back');\n * ```\n */\n getNextItem = (\n position: QueuePosition = this.options.getItemsFrom ?? 'front',\n ): TValue | undefined => {\n const { items, itemTimestamps } = this.store.state\n let item: TValue | undefined\n\n // When priority function is provided or position is 'front', always get from front (highest priority)\n // Priority takes precedence over FIFO/LIFO behavior\n if (\n this.options.getPriority !== defaultOptions.getPriority ||\n position === 'front'\n ) {\n item = items[0]\n if (item !== undefined) {\n this.#setState({\n items: items.slice(1),\n itemTimestamps: itemTimestamps.slice(1),\n })\n }\n } else {\n item = items[items.length - 1]\n if (item !== undefined) {\n this.#setState({\n items: items.slice(0, -1),\n itemTimestamps: itemTimestamps.slice(0, -1),\n })\n }\n }\n\n if (item !== undefined) {\n this.options.onItemsChange?.(this)\n }\n\n return item\n }\n\n #getAllItems = (): Array<TValue> => {\n const items = this.peekAllItems()\n this.clear()\n return items\n }\n\n /**\n * Removes and returns the next item from the queue and executes the task function with it.\n *\n * @example\n * ```ts\n * queuer.execute();\n * // LIFO\n * queuer.execute('back');\n * ```\n */\n execute = async (position?: QueuePosition): Promise<any> => {\n const item = this.getNextItem(position)\n\n if (item !== undefined) {\n const currentExecuteCount = this.store.state.executeCount + 1\n this.#setState({\n executeCount: currentExecuteCount,\n isExecuting: true,\n })\n try {\n const currentAsyncRetryer = new AsyncRetryer(this.fn, {\n ...this.options.asyncRetryerOptions,\n key: `${this.key}-retryer-${currentExecuteCount}`,\n })\n this.asyncRetryers.set(currentExecuteCount, currentAsyncRetryer)\n const lastResult = await currentAsyncRetryer.execute(item) // EXECUTE!\n this.#setState({\n successCount: this.store.state.successCount + 1,\n lastResult,\n })\n this.options.onSuccess?.(lastResult, item, this)\n } catch (error) {\n this.#setState({\n errorCount: this.store.state.errorCount + 1,\n })\n this.options.onError?.(error as Error, item, this)\n if (this.options.throwOnError) {\n throw error\n }\n } finally {\n this.asyncRetryers.delete(currentExecuteCount) // dispose retryer\n this.#setState({\n activeItems: this.store.state.activeItems.filter(\n (activeItem) => activeItem !== item,\n ),\n isExecuting: false,\n settledCount: this.store.state.settledCount + 1,\n })\n this.options.onSettled?.(item, this)\n }\n }\n return item\n }\n\n /**\n * Processes a specified number of items to execute immediately with no wait time\n * If no numberOfItems is provided, all items will be processed\n */\n flush = async (\n numberOfItems: number = this.store.state.items.length,\n position?: QueuePosition,\n ): Promise<void> => {\n this.#clearTimeouts() // clear any pending timeouts\n await Promise.all(\n Array.from({ length: numberOfItems }, () => this.execute(position)),\n )\n }\n\n /**\n * Processes all items in the queue as a batch using the provided function as an argument\n * The queue is cleared after processing\n */\n flushAsBatch = async (\n batchFunction: (items: Array<TValue>) => Promise<any>,\n ): Promise<void> => {\n this.#clearTimeouts() // clear any pending timeouts\n const items = this.#getAllItems()\n await batchFunction(items)\n }\n\n /**\n * Checks for expired items in the queue and removes them. Calls onExpire for each expired item.\n * Internal use only.\n */\n #checkExpiredItems = (): void => {\n if (\n (this.options.expirationDuration ?? Infinity) === Infinity &&\n this.options.getIsExpired === defaultOptions.getIsExpired\n ) {\n return\n }\n\n const now = Date.now()\n const expiredIndices: Array<number> = []\n\n // Find indices of expired items\n for (let i = 0; i < this.store.state.items.length; i++) {\n const timestamp = this.store.state.itemTimestamps[i]\n if (timestamp === undefined) continue\n\n const item = this.store.state.items[i]\n if (item === undefined) continue\n\n const isExpired =\n this.options.getIsExpired !== defaultOptions.getIsExpired\n ? this.options.getIsExpired!(item, timestamp)\n : now - timestamp > (this.options.expirationDuration ?? Infinity)\n\n if (isExpired) {\n expiredIndices.push(i)\n }\n }\n\n // Remove expired items from back to front to maintain indices\n for (let i = expiredIndices.length - 1; i >= 0; i--) {\n const index = expiredIndices[i]\n if (index === undefined) continue\n\n const expiredItem = this.store.state.items[index]\n if (expiredItem === undefined) continue\n\n const newItems = [...this.store.state.items]\n const newTimestamps = [...this.store.state.itemTimestamps]\n newItems.splice(index, 1)\n newTimestamps.splice(index, 1)\n this.#setState({\n items: newItems,\n itemTimestamps: newTimestamps,\n expirationCount: this.store.state.expirationCount + 1,\n })\n this.options.onExpire?.(expiredItem, this)\n }\n\n if (expiredIndices.length > 0) {\n this.options.onItemsChange?.(this)\n }\n }\n\n /**\n * Returns the next item in the queue without removing it.\n *\n * @example\n * ```ts\n * queuer.peekNextItem(); // front\n * queuer.peekNextItem('back'); // back\n * ```\n */\n peekNextItem = (position: QueuePosition = 'front'): TValue | undefined => {\n if (position === 'front') {\n return this.store.state.items[0]\n }\n return this.store.state.items[this.store.state.items.length - 1]\n }\n\n /**\n * Returns a copy of all items in the queue, including active and pending items.\n */\n peekAllItems = (): Array<TValue> => {\n return [...this.peekActiveItems(), ...this.peekPendingItems()]\n }\n\n /**\n * Returns the items currently being processed (active tasks).\n */\n peekActiveItems = (): Array<TValue> => {\n return [...this.store.state.activeItems]\n }\n\n /**\n * Returns the items waiting to be processed (pending tasks).\n */\n peekPendingItems = (): Array<TValue> => {\n return [...this.store.state.items]\n }\n\n /**\n * Starts processing items in the queue. If already running, does nothing.\n */\n start = (): void => {\n this.#setState({ isRunning: true })\n if (!this.store.state.pendingTick && this.store.state.items.length > 0) {\n this.#tick()\n }\n }\n\n /**\n * Stops processing items in the queue. Does not clear the queue.\n */\n stop = (): void => {\n this.#clearTimeouts()\n this.#setState({ isRunning: false, pendingTick: false })\n }\n\n #clearTimeouts = (): void => {\n this.#timeoutIds.forEach((timeoutId) => clearTimeout(timeoutId))\n this.#timeoutIds.clear()\n }\n\n /**\n * Removes all pending items from the queue.\n * Does NOT affect active tasks.\n */\n clear = (): void => {\n this.#setState({ items: [], itemTimestamps: [] })\n this.options.onItemsChange?.(this)\n }\n\n /**\n * Returns the AbortSignal for a specific execution.\n * If no executeCount is provided, returns the signal for the most recent execution.\n * Returns null if no execution is found or not currently executing.\n *\n * @param executeCount - Optional specific execution to get signal for\n * @example\n * ```typescript\n * const queuer = new AsyncQueuer(\n * async (item: string) => {\n * const signal = queuer.getAbortSignal()\n * if (signal) {\n * const response = await fetch(`/api/process/${item}`, { signal })\n * return response.json()\n * }\n * },\n * { concurrency: 2 }\n * )\n * ```\n */\n getAbortSignal = (executeCount?: number): AbortSignal | null => {\n const count = executeCount ?? this.store.state.executeCount\n const retryer = this.asyncRetryers.get(count)\n return retryer?.getAbortSignal() ?? null\n }\n\n /**\n * Aborts all ongoing executions with the internal abort controllers.\n * Does NOT clear out the items.\n */\n abort = (): void => {\n this.asyncRetryers.forEach((retryer) => retryer.abort())\n this.asyncRetryers.clear()\n this.#setState({\n isExecuting: false,\n })\n }\n\n /**\n * Resets the queuer state to its default values\n */\n reset = (): void => {\n this.#setState(getDefaultAsyncQueuerState<TValue>())\n this.options.onItemsChange?.(this)\n this.asyncRetryers.forEach((retryer) => retryer.reset())\n }\n}\n\n/**\n * Creates a new AsyncQueuer instance and returns a bound addItem function for adding tasks.\n * The queuer is started automatically and ready to process items.\n *\n * Async vs Sync Versions:\n * The async version provides advanced features over the sync queue function:\n * - Returns promises that can be awaited for task results\n * - Built-in retry support via AsyncRetryer integration for each queued task\n * - Abort support to cancel in-flight task executions\n * - Comprehensive error handling with onError callbacks and throwOnError control\n * - Detailed execution tracking (success/error/settle counts)\n * - Concurrent execution support (process multiple items simultaneously)\n *\n * The sync queue function is lighter weight and simpler when you don't need async features,\n * return values, or execution control.\n *\n * What is Queuing?\n * Queuing is a technique for managing and processing items sequentially or with controlled concurrency.\n * Tasks are processed up to the configured concurrency limit. When a task completes,\n * the next pending task is processed if the concurrency limit allows.\n *\n * Configuration Options:\n * - `concurrency`: Maximum number of concurrent tasks (default: 1)\n * - `wait`: Time to wait between processing items (default: 0)\n * - `maxSize`: Maximum number of items allowed in the queue (default: Infinity)\n * - `getPriority`: Function to determine item priority\n * - `addItemsTo`: Default position to add items ('back' or 'front', default: 'back')\n * - `getItemsFrom`: Default position to get items ('front' or 'back', default: 'front')\n * - `expirationDuration`: Maximum time items can stay in queue\n * - `started`: Whether to start processing immediately (default: true)\n * - `asyncRetryerOptions`: Configure retry behavior for task executions\n *\n * Error Handling:\n * - If an `onError` handler is provided, it will be called with the error and queuer 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 AsyncQueuer 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 queuer\n * - Use `onSuccess` callback to react to successful task execution and implement custom logic\n * - Use `onError` callback to react to task execution errors and implement custom error handling\n * - Use `onSettled` callback to react to task execution completion (success or error) and implement custom logic\n * - Use `onItemsChange` callback to react to items being added or removed from the queue\n * - Use `onExpire` callback to react to items expiring and implement custom logic\n * - Use `onReject` callback to react to items being rejected when the queue is full\n * - The state includes error count, expiration count, rejection count, running status, and success/settle counts\n * - State can be accessed via the underlying AsyncQueuer 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 enqueue = asyncQueue<string>(async (item) => {\n * return item.toUpperCase();\n * }, {\n * concurrency: 2,\n * wait: 100,\n * onSuccess: (result) => console.log('Processed:', result)\n * });\n *\n * enqueue('hello');\n * ```\n */\nexport function asyncQueue<TValue>(\n fn: (value: TValue) => Promise<any>,\n initialOptions: AsyncQueuerOptions<TValue>,\n) {\n const asyncQueuer = new AsyncQueuer<TValue>(fn, initialOptions)\n return asyncQueuer.addItem\n}\n"],"mappings":"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"}
{"version":3,"file":"async-queuer.js","names":["#setState","#tick","#clearTimeouts","#getAllItems","#checkExpiredItems","#getConcurrency","#getWait","#timeoutIds"],"sources":["../src/async-queuer.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 { OptionalKeys } from './types'\nimport type { QueuePosition } from './queuer'\n\nexport interface AsyncQueuerState<TValue> {\n /**\n * Items currently being processed by the queuer\n */\n activeItems: Array<TValue>\n /**\n * Number of times addItem has been called (for reduction calculations)\n */\n addItemCount: number\n /**\n * Number of task executions that have resulted in errors\n */\n errorCount: number\n /**\n * Number of times execute has been called\n */\n executeCount: number\n /**\n * Number of items that have been removed from the queue due to expiration\n */\n expirationCount: number\n /**\n * Whether the queuer has no items to process (items array is empty)\n */\n isEmpty: boolean\n /**\n * Whether the queuer is currently executing\n */\n isExecuting: boolean\n /**\n * Whether the queuer has reached its maximum capacity\n */\n isFull: boolean\n /**\n * Whether the queuer is not currently processing any items\n */\n isIdle: boolean\n /**\n * Whether the queuer is active and will process items automatically\n */\n isRunning: boolean\n /**\n * Array of items currently waiting to be processed\n */\n items: Array<TValue>\n /**\n * Timestamps when items were added to the queue for expiration tracking\n */\n itemTimestamps: Array<number>\n /**\n * The result from the most recent task execution\n */\n lastResult: any\n /**\n * Whether the queuer has a pending timeout for processing the next item\n */\n pendingTick: boolean\n /**\n * Number of items that have been rejected from being added to the queue\n */\n rejectionCount: number\n /**\n * Number of task executions that have completed (either successfully or with errors)\n */\n settledCount: number\n /**\n * Number of items currently in the queue\n */\n size: number\n /**\n * Current processing status - 'idle' when not processing, 'running' when active, 'stopped' when paused\n */\n status: 'idle' | 'running' | 'stopped'\n /**\n * Number of task executions that have completed successfully\n */\n successCount: number\n}\n\nfunction getDefaultAsyncQueuerState<TValue>(): AsyncQueuerState<TValue> {\n return {\n activeItems: [],\n addItemCount: 0,\n errorCount: 0,\n executeCount: 0,\n expirationCount: 0,\n isEmpty: true,\n isExecuting: false,\n isFull: false,\n isIdle: true,\n isRunning: true,\n itemTimestamps: [],\n items: [],\n lastResult: null,\n pendingTick: false,\n rejectionCount: 0,\n settledCount: 0,\n size: 0,\n status: 'idle',\n successCount: 0,\n }\n}\n\nexport interface AsyncQueuerOptions<TValue> {\n /**\n * Options for configuring the underlying async retryer\n */\n asyncRetryerOptions?: AsyncRetryerOptions<(item: TValue) => Promise<any>>\n /**\n * Default position to add items to the queuer\n * @default 'back'\n */\n addItemsTo?: QueuePosition\n /**\n * Maximum number of concurrent tasks to process.\n * Can be a number or a function that returns a number.\n * @default 1\n */\n concurrency?: number | ((queuer: AsyncQueuer<TValue>) => number)\n /**\n * Maximum time in milliseconds that an item can stay in the queue\n * If not provided, items will never expire\n */\n expirationDuration?: number\n /**\n * Function to determine if an item has expired\n * If provided, this overrides the expirationDuration behavior\n */\n getIsExpired?: (item: TValue, addedAt: number) => boolean\n /**\n * Default position to get items from during processing\n * @default 'front'\n */\n getItemsFrom?: QueuePosition\n /**\n * Function to determine priority of items in the queuer\n * Higher priority items will be processed first\n * If not provided, will use static priority values attached to tasks\n */\n getPriority?: (item: TValue) => number\n /**\n * Initial items to populate the queuer with\n */\n initialItems?: Array<TValue>\n /**\n * Initial state for the async queuer\n */\n initialState?: Partial<AsyncQueuerState<TValue>>\n /**\n * Optional key to identify this async queuer instance.\n * If provided, the async queuer will be identified by this key in the devtools and PacerProvider if applicable.\n */\n key?: string\n /**\n * Maximum number of items allowed in the queuer\n */\n maxSize?: number\n /**\n * Optional error handler for when a task throws.\n * If provided, the handler will be called with the error and queuer instance.\n * This can be used alongside throwOnError - the handler will be called before any error is thrown.\n */\n onError?: (error: Error, item: TValue, queuer: AsyncQueuer<TValue>) => void\n /**\n * Callback fired whenever an item expires in the queuer\n */\n onExpire?: (item: TValue, queuer: AsyncQueuer<TValue>) => void\n /**\n * Callback fired whenever an item is added or removed from the queuer\n */\n onItemsChange?: (queuer: AsyncQueuer<TValue>) => void\n /**\n * Callback fired whenever an item is rejected from being added to the queuer\n */\n onReject?: (item: TValue, queuer: AsyncQueuer<TValue>) => void\n /**\n * Optional callback to call when a task is settled\n */\n onSettled?: (item: TValue, queuer: AsyncQueuer<TValue>) => void\n /**\n * Optional callback to call when a task succeeds\n */\n onSuccess?: (result: any, item: TValue, queuer: AsyncQueuer<TValue>) => void\n /**\n * Whether the queuer should start processing tasks immediately or not.\n */\n started?: boolean\n /**\n * Whether to throw errors when they occur.\n * Defaults to true if no onError handler is provided, false if an onError handler is provided.\n * Can be explicitly set to override these defaults.\n */\n throwOnError?: boolean\n /**\n * Time in milliseconds to wait between processing items.\n * Can be a number or a function that returns a number.\n * @default 0\n */\n wait?: number | ((queuer: AsyncQueuer<TValue>) => number)\n}\n\n/**\n * Utility function for sharing common `AsyncQueuerOptions` options between different `AsyncQueuer` instances.\n */\nexport function asyncQueuerOptions<\n TValue = any,\n TOptions extends Partial<AsyncQueuerOptions<TValue>> = Partial<\n AsyncQueuerOptions<TValue>\n >,\n>(options: TOptions): TOptions {\n return options\n}\n\ntype AsyncQueuerOptionsWithOptionalCallbacks = OptionalKeys<\n Required<AsyncQueuerOptions<any>>,\n | 'initialState'\n | 'throwOnError'\n | 'onSuccess'\n | 'onSettled'\n | 'onReject'\n | 'onItemsChange'\n | 'onExpire'\n | 'onError'\n | 'key'\n>\n\nconst defaultOptions: AsyncQueuerOptionsWithOptionalCallbacks = {\n addItemsTo: 'back',\n asyncRetryerOptions: {\n maxAttempts: 1,\n },\n concurrency: 1,\n expirationDuration: Infinity,\n getIsExpired: () => false,\n getItemsFrom: 'front',\n getPriority: (item: any) => item?.priority ?? 0,\n initialItems: [],\n maxSize: Infinity,\n started: true,\n wait: 0,\n}\n\n/**\n * A flexible asynchronous queue for processing tasks with configurable concurrency, priority, and expiration.\n *\n * Async vs Sync Versions:\n * The async version provides advanced features over the sync Queuer:\n * - Returns promises that can be awaited for task results\n * - Built-in retry support via AsyncRetryer integration for each queued task\n * - Abort support to cancel in-flight task executions\n * - Comprehensive error handling with onError callbacks and throwOnError control\n * - Detailed execution tracking (success/error/settle counts)\n * - Concurrent execution support (process multiple items simultaneously)\n *\n * The sync Queuer is lighter weight and simpler when you don't need async features,\n * return values, or execution control.\n *\n * What is Queuing?\n * Queuing is a technique for managing and processing items sequentially or with controlled concurrency.\n * Tasks are processed up to the configured concurrency limit. When a task completes,\n * the next pending task is processed if the concurrency limit allows.\n *\n * Key Features:\n * - Priority queue support via the getPriority option\n * - Configurable concurrency limit\n * - Callbacks for task success, error, completion, and queue state changes\n * - FIFO (First In First Out) or LIFO (Last In First Out) queue behavior\n * - Pause and resume processing\n * - Item expiration to remove stale items from the queue\n *\n * Error Handling:\n * - If an `onError` handler is provided, it will be called with the error and queuer 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 AsyncQueuer 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 queuer\n * - Use `onSuccess` callback to react to successful task execution and implement custom logic\n * - Use `onError` callback to react to task execution errors and implement custom error handling\n * - Use `onSettled` callback to react to task execution completion (success or error) and implement custom logic\n * - Use `onItemsChange` callback to react to items being added or removed from the queue\n * - Use `onExpire` callback to react to items expiring and implement custom logic\n * - Use `onReject` callback to react to items being rejected when the queue is full\n * - The state includes error count, expiration count, rejection count, running status, and success/settle counts\n * - State can be accessed via `asyncQueuer.store.state` when using the class directly\n * - When using framework adapters (React/Solid), state is accessed from `asyncQueuer.state`\n *\n * Example usage:\n * ```ts\n * const asyncQueuer = new AsyncQueuer<string>(async (item) => {\n * // process item\n * return item.toUpperCase();\n * }, {\n * concurrency: 2,\n * onSuccess: (result) => {\n * console.log(result);\n * }\n * });\n *\n * asyncQueuer.addItem('hello');\n * asyncQueuer.start();\n * ```\n */\nexport class AsyncQueuer<TValue> {\n readonly store: Store<Readonly<AsyncQueuerState<TValue>>> = new Store<\n AsyncQueuerState<TValue>\n >(getDefaultAsyncQueuerState<TValue>())\n key: string | undefined\n options: AsyncQueuerOptions<TValue>\n asyncRetryers = new Map<\n number,\n AsyncRetryer<(item: TValue) => Promise<any>>\n >()\n #timeoutIds: Set<ReturnType<typeof setTimeout>> = new Set()\n\n constructor(\n public fn: (item: TValue) => Promise<any>,\n initialOptions: AsyncQueuerOptions<TValue> = {},\n ) {\n this.key = initialOptions.key\n this.options = {\n ...defaultOptions,\n ...initialOptions,\n throwOnError: initialOptions.throwOnError ?? !initialOptions.onError,\n }\n const isInitiallyRunning =\n this.options.initialState?.isRunning ?? this.options.started ?? true\n this.#setState({\n ...this.options.initialState,\n isRunning: isInitiallyRunning,\n })\n\n if (this.options.initialState?.items) {\n if (this.store.state.isRunning) {\n this.#tick()\n }\n } else {\n for (let i = 0; i < (this.options.initialItems?.length ?? 0); i++) {\n const item = this.options.initialItems![i]!\n const isLast = i === (this.options.initialItems?.length ?? 0) - 1\n this.addItem(item, this.options.addItemsTo ?? 'back', isLast)\n }\n }\n\n if (this.key) {\n pacerEventClient.on('d-AsyncQueuer', (e) => {\n if (e.payload.key !== this.key) return\n this.#setState(\n e.payload.store.state as Partial<AsyncQueuerState<TValue>>,\n )\n this.setOptions(\n e.payload.options as Partial<AsyncQueuerOptions<TValue>>,\n )\n })\n }\n }\n\n /**\n * Updates the queuer options. New options are merged with existing options.\n */\n setOptions = (newOptions: Partial<AsyncQueuerOptions<TValue>>): void => {\n this.options = { ...this.options, ...newOptions }\n }\n\n #setState = (newState: Partial<AsyncQueuerState<TValue>>): void => {\n this.store.setState((state) => {\n const combinedState = {\n ...state,\n ...newState,\n }\n\n const { activeItems, items, isRunning } = combinedState\n\n const size = items.length\n const isFull = size >= (this.options.maxSize ?? Infinity)\n const isEmpty = size === 0\n const isIdle = isRunning && isEmpty && activeItems.length === 0\n\n const status = isIdle ? 'idle' : isRunning ? 'running' : 'stopped'\n\n return {\n ...combinedState,\n isEmpty,\n isFull,\n isIdle,\n size,\n status,\n }\n })\n emitChange('AsyncQueuer', this)\n }\n\n /**\n * Returns the current wait time (in milliseconds) between processing items.\n * If a function is provided, it is called with the queuer instance.\n */\n #getWait = (): number => {\n return parseFunctionOrValue(this.options.wait ?? 0, this)\n }\n\n /**\n * Returns the current concurrency limit for processing items.\n * If a function is provided, it is called with the queuer instance.\n */\n #getConcurrency = (): number => {\n return parseFunctionOrValue(this.options.concurrency ?? 1, this)\n }\n\n /**\n * Processes items in the queue up to the concurrency limit. Internal use only.\n */\n #tick = () => {\n if (!this.store.state.isRunning) {\n this.#setState({ pendingTick: false })\n return\n }\n this.#setState({ pendingTick: true })\n\n // Check for expired items\n this.#checkExpiredItems()\n\n // Process items concurrently up to the concurrency limit\n const activeItems = this.store.state.activeItems\n while (\n activeItems.length < this.#getConcurrency() &&\n this.store.state.items.length > 0\n ) {\n const nextItem = this.peekNextItem()\n if (!nextItem) {\n break\n }\n activeItems.push(nextItem)\n this.#setState({\n activeItems,\n })\n ;(async () => {\n await this.execute()\n\n const wait = this.#getWait()\n if (wait > 0) {\n const timeoutId = setTimeout(() => this.#tick(), wait)\n this.#timeoutIds.add(timeoutId)\n return\n }\n\n this.#tick()\n })()\n }\n\n this.#setState({ pendingTick: false })\n }\n\n /**\n * Adds an item to the queue. If the queue is full, the item is rejected and onReject is called.\n * Items can be inserted based on priority or at the front/back depending on configuration.\n *\n * @example\n * ```ts\n * queuer.addItem({ value: 'task', priority: 10 });\n * queuer.addItem('task2', 'front');\n * ```\n */\n addItem = (\n item: TValue,\n position: QueuePosition = this.options.addItemsTo ?? 'back',\n runOnItemsChange: boolean = true,\n ): boolean => {\n this.#setState({\n addItemCount: this.store.state.addItemCount + 1,\n })\n\n if (this.store.state.items.length >= (this.options.maxSize ?? Infinity)) {\n this.#setState({\n rejectionCount: this.store.state.rejectionCount + 1,\n })\n this.options.onReject?.(item, this)\n return false\n }\n\n // Get priority either from the function or from getPriority option\n const priority =\n this.options.getPriority !== defaultOptions.getPriority\n ? this.options.getPriority!(item)\n : (item as any).priority\n\n const items = this.store.state.items\n const itemTimestamps = this.store.state.itemTimestamps\n\n if (priority !== undefined) {\n // Insert based on priority - higher priority items go to front\n const insertIndex = items.findIndex((existing) => {\n const existingPriority =\n this.options.getPriority !== defaultOptions.getPriority\n ? this.options.getPriority!(existing)\n : (existing as any).priority\n return existingPriority < priority\n })\n\n if (insertIndex === -1) {\n items.push(item)\n itemTimestamps.push(Date.now())\n } else {\n items.splice(insertIndex, 0, item)\n itemTimestamps.splice(insertIndex, 0, Date.now())\n }\n } else {\n if (position === 'front') {\n // Default FIFO/LIFO behavior\n items.unshift(item)\n itemTimestamps.unshift(Date.now())\n } else {\n // LIFO\n items.push(item)\n itemTimestamps.push(Date.now())\n }\n }\n\n this.#setState({\n items,\n itemTimestamps,\n })\n\n if (runOnItemsChange) {\n this.options.onItemsChange?.(this)\n }\n\n if (this.store.state.isRunning && !this.store.state.pendingTick) {\n this.#tick()\n }\n\n return true\n }\n\n /**\n * Removes and returns the next item from the queue without executing the task function.\n * Use for manual queue management. Normally, use execute() to process items.\n *\n * @example\n * ```ts\n * // FIFO\n * queuer.getNextItem();\n * // LIFO\n * queuer.getNextItem('back');\n * ```\n */\n getNextItem = (\n position: QueuePosition = this.options.getItemsFrom ?? 'front',\n ): TValue | undefined => {\n const { items, itemTimestamps } = this.store.state\n let item: TValue | undefined\n\n // When priority function is provided or position is 'front', always get from front (highest priority)\n // Priority takes precedence over FIFO/LIFO behavior\n if (\n this.options.getPriority !== defaultOptions.getPriority ||\n position === 'front'\n ) {\n item = items[0]\n if (item !== undefined) {\n this.#setState({\n items: items.slice(1),\n itemTimestamps: itemTimestamps.slice(1),\n })\n }\n } else {\n item = items[items.length - 1]\n if (item !== undefined) {\n this.#setState({\n items: items.slice(0, -1),\n itemTimestamps: itemTimestamps.slice(0, -1),\n })\n }\n }\n\n if (item !== undefined) {\n this.options.onItemsChange?.(this)\n }\n\n return item\n }\n\n #getAllItems = (): Array<TValue> => {\n const items = this.peekAllItems()\n this.clear()\n return items\n }\n\n /**\n * Removes and returns the next item from the queue and executes the task function with it.\n *\n * @example\n * ```ts\n * queuer.execute();\n * // LIFO\n * queuer.execute('back');\n * ```\n */\n execute = async (position?: QueuePosition): Promise<any> => {\n const item = this.getNextItem(position)\n\n if (item !== undefined) {\n const currentExecuteCount = this.store.state.executeCount + 1\n this.#setState({\n executeCount: currentExecuteCount,\n isExecuting: true,\n })\n try {\n const currentAsyncRetryer = new AsyncRetryer(this.fn, {\n ...this.options.asyncRetryerOptions,\n key: `${this.key}-retryer-${currentExecuteCount}`,\n })\n this.asyncRetryers.set(currentExecuteCount, currentAsyncRetryer)\n const lastResult = await currentAsyncRetryer.execute(item) // EXECUTE!\n this.#setState({\n successCount: this.store.state.successCount + 1,\n lastResult,\n })\n this.options.onSuccess?.(lastResult, item, this)\n } catch (error) {\n this.#setState({\n errorCount: this.store.state.errorCount + 1,\n })\n this.options.onError?.(error as Error, item, this)\n if (this.options.throwOnError) {\n throw error\n }\n } finally {\n this.asyncRetryers.delete(currentExecuteCount) // dispose retryer\n this.#setState({\n activeItems: this.store.state.activeItems.filter(\n (activeItem) => activeItem !== item,\n ),\n isExecuting: false,\n settledCount: this.store.state.settledCount + 1,\n })\n this.options.onSettled?.(item, this)\n }\n }\n return item\n }\n\n /**\n * Processes a specified number of items to execute immediately with no wait time\n * If no numberOfItems is provided, all items will be processed\n */\n flush = async (\n numberOfItems: number = this.store.state.items.length,\n position?: QueuePosition,\n ): Promise<void> => {\n this.#clearTimeouts() // clear any pending timeouts\n await Promise.all(\n Array.from({ length: numberOfItems }, () => this.execute(position)),\n )\n }\n\n /**\n * Processes all items in the queue as a batch using the provided function as an argument\n * The queue is cleared after processing\n */\n flushAsBatch = async (\n batchFunction: (items: Array<TValue>) => Promise<any>,\n ): Promise<void> => {\n this.#clearTimeouts() // clear any pending timeouts\n const items = this.#getAllItems()\n await batchFunction(items)\n }\n\n /**\n * Checks for expired items in the queue and removes them. Calls onExpire for each expired item.\n * Internal use only.\n */\n #checkExpiredItems = (): void => {\n if (\n (this.options.expirationDuration ?? Infinity) === Infinity &&\n this.options.getIsExpired === defaultOptions.getIsExpired\n ) {\n return\n }\n\n const now = Date.now()\n const expiredIndices: Array<number> = []\n\n // Find indices of expired items\n for (let i = 0; i < this.store.state.items.length; i++) {\n const timestamp = this.store.state.itemTimestamps[i]\n if (timestamp === undefined) continue\n\n const item = this.store.state.items[i]\n if (item === undefined) continue\n\n const isExpired =\n this.options.getIsExpired !== defaultOptions.getIsExpired\n ? this.options.getIsExpired!(item, timestamp)\n : now - timestamp > (this.options.expirationDuration ?? Infinity)\n\n if (isExpired) {\n expiredIndices.push(i)\n }\n }\n\n // Remove expired items from back to front to maintain indices\n for (let i = expiredIndices.length - 1; i >= 0; i--) {\n const index = expiredIndices[i]\n if (index === undefined) continue\n\n const expiredItem = this.store.state.items[index]\n if (expiredItem === undefined) continue\n\n const newItems = [...this.store.state.items]\n const newTimestamps = [...this.store.state.itemTimestamps]\n newItems.splice(index, 1)\n newTimestamps.splice(index, 1)\n this.#setState({\n items: newItems,\n itemTimestamps: newTimestamps,\n expirationCount: this.store.state.expirationCount + 1,\n })\n this.options.onExpire?.(expiredItem, this)\n }\n\n if (expiredIndices.length > 0) {\n this.options.onItemsChange?.(this)\n }\n }\n\n /**\n * Returns the next item in the queue without removing it.\n *\n * @example\n * ```ts\n * queuer.peekNextItem(); // front\n * queuer.peekNextItem('back'); // back\n * ```\n */\n peekNextItem = (position: QueuePosition = 'front'): TValue | undefined => {\n if (position === 'front') {\n return this.store.state.items[0]\n }\n return this.store.state.items[this.store.state.items.length - 1]\n }\n\n /**\n * Returns a copy of all items in the queue, including active and pending items.\n */\n peekAllItems = (): Array<TValue> => {\n return [...this.peekActiveItems(), ...this.peekPendingItems()]\n }\n\n /**\n * Returns the items currently being processed (active tasks).\n */\n peekActiveItems = (): Array<TValue> => {\n return [...this.store.state.activeItems]\n }\n\n /**\n * Returns the items waiting to be processed (pending tasks).\n */\n peekPendingItems = (): Array<TValue> => {\n return [...this.store.state.items]\n }\n\n /**\n * Starts processing items in the queue. If already running, does nothing.\n */\n start = (): void => {\n this.#setState({ isRunning: true })\n if (!this.store.state.pendingTick && this.store.state.items.length > 0) {\n this.#tick()\n }\n }\n\n /**\n * Stops processing items in the queue. Does not clear the queue.\n */\n stop = (): void => {\n this.#clearTimeouts()\n this.#setState({ isRunning: false, pendingTick: false })\n }\n\n #clearTimeouts = (): void => {\n this.#timeoutIds.forEach((timeoutId) => clearTimeout(timeoutId))\n this.#timeoutIds.clear()\n }\n\n /**\n * Removes all pending items from the queue.\n * Does NOT affect active tasks.\n */\n clear = (): void => {\n this.#setState({ items: [], itemTimestamps: [] })\n this.options.onItemsChange?.(this)\n }\n\n /**\n * Returns the AbortSignal for a specific execution.\n * If no executeCount is provided, returns the signal for the most recent execution.\n * Returns null if no execution is found or not currently executing.\n *\n * @param executeCount - Optional specific execution to get signal for\n * @example\n * ```typescript\n * const queuer = new AsyncQueuer(\n * async (item: string) => {\n * const signal = queuer.getAbortSignal()\n * if (signal) {\n * const response = await fetch(`/api/process/${item}`, { signal })\n * return response.json()\n * }\n * },\n * { concurrency: 2 }\n * )\n * ```\n */\n getAbortSignal = (executeCount?: number): AbortSignal | null => {\n const count = executeCount ?? this.store.state.executeCount\n const retryer = this.asyncRetryers.get(count)\n return retryer?.getAbortSignal() ?? null\n }\n\n /**\n * Aborts all ongoing executions with the internal abort controllers.\n * Does NOT clear out the items.\n */\n abort = (): void => {\n this.asyncRetryers.forEach((retryer) => retryer.abort())\n this.asyncRetryers.clear()\n this.#setState({\n isExecuting: false,\n })\n }\n\n /**\n * Resets the queuer state to its default values\n */\n reset = (): void => {\n this.#setState(getDefaultAsyncQueuerState<TValue>())\n this.options.onItemsChange?.(this)\n this.asyncRetryers.forEach((retryer) => retryer.reset())\n }\n}\n\n/**\n * Creates a new AsyncQueuer instance and returns a bound addItem function for adding tasks.\n * The queuer is started automatically and ready to process items.\n *\n * Async vs Sync Versions:\n * The async version provides advanced features over the sync queue function:\n * - Returns promises that can be awaited for task results\n * - Built-in retry support via AsyncRetryer integration for each queued task\n * - Abort support to cancel in-flight task executions\n * - Comprehensive error handling with onError callbacks and throwOnError control\n * - Detailed execution tracking (success/error/settle counts)\n * - Concurrent execution support (process multiple items simultaneously)\n *\n * The sync queue function is lighter weight and simpler when you don't need async features,\n * return values, or execution control.\n *\n * What is Queuing?\n * Queuing is a technique for managing and processing items sequentially or with controlled concurrency.\n * Tasks are processed up to the configured concurrency limit. When a task completes,\n * the next pending task is processed if the concurrency limit allows.\n *\n * Configuration Options:\n * - `concurrency`: Maximum number of concurrent tasks (default: 1)\n * - `wait`: Time to wait between processing items (default: 0)\n * - `maxSize`: Maximum number of items allowed in the queue (default: Infinity)\n * - `getPriority`: Function to determine item priority\n * - `addItemsTo`: Default position to add items ('back' or 'front', default: 'back')\n * - `getItemsFrom`: Default position to get items ('front' or 'back', default: 'front')\n * - `expirationDuration`: Maximum time items can stay in queue\n * - `started`: Whether to start processing immediately (default: true)\n * - `asyncRetryerOptions`: Configure retry behavior for task executions\n *\n * Error Handling:\n * - If an `onError` handler is provided, it will be called with the error and queuer 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 AsyncQueuer 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 queuer\n * - Use `onSuccess` callback to react to successful task execution and implement custom logic\n * - Use `onError` callback to react to task execution errors and implement custom error handling\n * - Use `onSettled` callback to react to task execution completion (success or error) and implement custom logic\n * - Use `onItemsChange` callback to react to items being added or removed from the queue\n * - Use `onExpire` callback to react to items expiring and implement custom logic\n * - Use `onReject` callback to react to items being rejected when the queue is full\n * - The state includes error count, expiration count, rejection count, running status, and success/settle counts\n * - State can be accessed via the underlying AsyncQueuer 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 enqueue = asyncQueue<string>(async (item) => {\n * return item.toUpperCase();\n * }, {\n * concurrency: 2,\n * wait: 100,\n * onSuccess: (result) => console.log('Processed:', result)\n * });\n *\n * enqueue('hello');\n * ```\n */\nexport function asyncQueue<TValue>(\n fn: (value: TValue) => Promise<any>,\n initialOptions: AsyncQueuerOptions<TValue>,\n) {\n const asyncQueuer = new AsyncQueuer<TValue>(fn, initialOptions)\n return asyncQueuer.addItem\n}\n"],"mappings":"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"}

@@ -1,1 +0,1 @@

{"version":3,"file":"async-rate-limiter.cjs","names":["Store","#setState","#cleanupOldExecutions","#getExecutionTimesInWindow","#getLimit","#execute","#getWindow","#clearTimeouts","#setCleanupTimeout","#getEnabled","parseFunctionOrValue","AsyncRetryer","#timeoutIds","#clearTimeout"],"sources":["../src/async-rate-limiter.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 } from './types'\n\nexport interface AsyncRateLimiterState<TFn extends AnyAsyncFunction> {\n /**\n * Number of function executions that have resulted in errors\n */\n errorCount: number\n /**\n * Array of timestamps when executions occurred for rate limiting calculations\n */\n executionTimes: Array<number>\n /**\n * Whether the rate limiter has exceeded the limit\n */\n isExceeded: boolean\n /**\n * Whether the rate-limited function is currently executing asynchronously\n */\n isExecuting: boolean\n /**\n * The result from the most recent successful function execution\n */\n lastResult: ReturnType<TFn> | undefined\n /**\n * Number of function executions that have been rejected due to rate limiting\n */\n rejectionCount: number\n /**\n * Number of function executions that have completed (either successfully or with errors)\n */\n settleCount: number\n /**\n * Current execution status - 'disabled' when not active, 'executing' when executing, 'idle' when not executing, 'exceeded' when rate limit is exceeded\n */\n status: 'disabled' | 'executing' | 'exceeded' | 'idle'\n /**\n * Number of function executions that have completed successfully\n */\n successCount: number\n /**\n * Number of times maybeExecute has been called (for reduction calculations)\n */\n maybeExecuteCount: number\n}\n\nfunction getDefaultAsyncRateLimiterState<\n TFn extends AnyAsyncFunction,\n>(): AsyncRateLimiterState<TFn> {\n return {\n errorCount: 0,\n executionTimes: [],\n isExceeded: false,\n isExecuting: false,\n lastResult: undefined,\n maybeExecuteCount: 0,\n rejectionCount: 0,\n settleCount: 0,\n status: 'idle',\n successCount: 0,\n }\n}\n\n/**\n * Options for configuring an async rate-limited function\n */\nexport interface AsyncRateLimiterOptions<TFn extends AnyAsyncFunction> {\n /**\n * Options for configuring the underlying async retryer\n */\n asyncRetryerOptions?: AsyncRetryerOptions<TFn>\n /**\n * Whether the rate limiter 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 | ((rateLimiter: AsyncRateLimiter<TFn>) => boolean)\n /**\n * Initial state for the rate limiter\n */\n initialState?: Partial<AsyncRateLimiterState<TFn>>\n /**\n * Optional key to identify this async rate limiter instance.\n * If provided, the async rate limiter will be identified by this key in the devtools and PacerProvider if applicable.\n */\n key?: string\n /**\n * Maximum number of executions allowed within the time window.\n * Can be a number or a function that returns a number.\n */\n limit: number | ((rateLimiter: AsyncRateLimiter<TFn>) => number)\n /**\n * Optional error handler for when the rate-limited function throws.\n * If provided, the handler will be called with the error and rate limiter 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 rateLimiter: AsyncRateLimiter<TFn>,\n ) => void\n /**\n * Optional callback function that is called when an execution is rejected due to rate limiting\n */\n onReject?: (args: Parameters<TFn>, rateLimiter: AsyncRateLimiter<TFn>) => void\n /**\n * Optional function to call when the rate-limited function is executed\n */\n onSettled?: (\n args: Parameters<TFn>,\n rateLimiter: AsyncRateLimiter<TFn>,\n ) => void\n /**\n * Optional function to call when the rate-limited function is executed\n */\n onSuccess?: (\n result: ReturnType<TFn>,\n args: Parameters<TFn>,\n rateLimiter: AsyncRateLimiter<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 * Time window in milliseconds within which the limit applies.\n * Can be a number or a function that returns a number.\n */\n window: number | ((rateLimiter: AsyncRateLimiter<TFn>) => number)\n /**\n * Type of window to use for rate limiting\n * - 'fixed': Uses a fixed window that resets after the window period\n * - 'sliding': Uses a sliding window that allows executions as old ones expire\n * Defaults to 'fixed'\n */\n windowType?: 'fixed' | 'sliding'\n}\n\n/**\n * Utility function for sharing common `AsyncRateLimiterOptions` options between different `AsyncRateLimiter` instances.\n */\nexport function asyncRateLimiterOptions<\n TFn extends AnyAsyncFunction = AnyAsyncFunction,\n TOptions extends Partial<AsyncRateLimiterOptions<TFn>> = Partial<\n AsyncRateLimiterOptions<TFn>\n >,\n>(options: TOptions): TOptions {\n return options\n}\n\nconst defaultOptions: Omit<\n Required<AsyncRateLimiterOptions<any>>,\n 'initialState' | 'onError' | 'onReject' | 'onSettled' | 'onSuccess' | 'key'\n> = {\n asyncRetryerOptions: {\n maxAttempts: 1,\n },\n enabled: true,\n limit: 1,\n window: 0,\n windowType: 'fixed',\n throwOnError: true,\n}\n\n/**\n * A class that creates an async rate-limited function.\n *\n * Async vs Sync Versions:\n * The async version provides advanced features over the sync RateLimiter:\n * - Returns promises that can be awaited for rate-limited function results\n * - Built-in retry support via AsyncRetryer integration\n * - Abort support to cancel in-flight executions\n * - Comprehensive error handling with onError callbacks and throwOnError control\n * - Detailed execution tracking (success/error/settle counts, rejection counts)\n * - More sophisticated window management with automatic cleanup\n *\n * The sync RateLimiter is lighter weight and simpler when you don't need async features,\n * return values, or execution control.\n *\n * What is Rate Limiting?\n * Rate limiting allows a function to execute up to a limit within a time window,\n * then blocks all subsequent calls until the window passes. This can lead to \"bursty\" behavior where\n * all executions happen immediately, followed by a complete block.\n *\n * Window Types:\n * - 'fixed': A strict window that resets after the window period. All executions within the window count\n * towards the limit, and the window resets completely after the period.\n * - 'sliding': A rolling window that allows executions as old ones expire. This provides a more\n * consistent rate of execution over time.\n *\n * When to Use Rate Limiting:\n * Rate limiting is best used for hard API limits or resource constraints. For UI updates or\n * smoothing out frequent events, throttling or debouncing usually provide better user experience.\n * - Throttling: Ensures consistent spacing between executions (e.g. max once per 200ms)\n * - Debouncing: Waits for a pause in calls before executing (e.g. after 500ms of no calls)\n *\n * State Management:\n * - Uses TanStack Store for reactive state management\n * - Use `initialState` to provide initial state values when creating the rate limiter\n * - `initialState` can be a partial state object\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 * - Use `onReject` callback to react to executions being rejected when rate limit is exceeded\n * - The state includes execution times, success/error counts, and current execution status\n * - State can be accessed via `asyncRateLimiter.store.state` when using the class directly\n * - When using framework adapters (React/Solid), state is accessed from `asyncRateLimiter.state`\n *\n * Error Handling:\n * - If an `onError` handler is provided, it will be called with the error and rate limiter 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 AsyncRateLimiter instance\n * - Rate limit rejections (when limit is exceeded) are handled separately from execution errors via the `onReject` handler\n *\n * @example\n * ```ts\n * const rateLimiter = new AsyncRateLimiter(\n * async (id: string) => await api.getData(id),\n * {\n * limit: 5,\n * window: 1000,\n * windowType: 'sliding',\n * onError: (error) => {\n * console.error('API call failed:', error);\n * },\n * onReject: (limiter) => {\n * console.log(`Rate limit exceeded. Try again in ${limiter.getMsUntilNextWindow()}ms`);\n * }\n * }\n * );\n *\n * // Will execute immediately until limit reached, then block\n * // Returns the API response directly\n * const data = await rateLimiter.maybeExecute('123');\n * ```\n */\nexport class AsyncRateLimiter<TFn extends AnyAsyncFunction> {\n readonly store: Store<Readonly<AsyncRateLimiterState<TFn>>> = new Store<\n AsyncRateLimiterState<TFn>\n >(getDefaultAsyncRateLimiterState<TFn>())\n key: string | undefined\n options: AsyncRateLimiterOptions<TFn>\n asyncRetryers = new Map<number, AsyncRetryer<TFn>>()\n #timeoutIds: Set<NodeJS.Timeout> = new Set()\n\n constructor(\n public fn: TFn,\n initialOptions: AsyncRateLimiterOptions<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 for (const executionTime of this.#getExecutionTimesInWindow()) {\n this.#setCleanupTimeout(executionTime)\n }\n\n if (this.key) {\n pacerEventClient.on('d-AsyncRateLimiter', (event) => {\n if (event.payload.key !== this.key) return\n this.#setState(\n event.payload.store.state as Partial<AsyncRateLimiterState<TFn>>,\n )\n this.setOptions(\n event.payload.options as Partial<AsyncRateLimiterOptions<TFn>>,\n )\n })\n }\n }\n\n /**\n * Updates the async rate limiter options\n */\n setOptions = (newOptions: Partial<AsyncRateLimiterOptions<TFn>>): void => {\n this.options = { ...this.options, ...newOptions }\n }\n\n #setState = (newState: Partial<AsyncRateLimiterState<TFn>>): void => {\n this.store.setState((state) => {\n const combinedState = {\n ...state,\n ...newState,\n }\n const isExceeded = combinedState.executionTimes.length >= this.#getLimit()\n const status = !this.#getEnabled()\n ? 'disabled'\n : combinedState.isExecuting\n ? 'executing'\n : isExceeded\n ? 'exceeded'\n : 'idle'\n return {\n ...combinedState,\n isExceeded,\n status,\n }\n })\n emitChange('AsyncRateLimiter', this)\n }\n\n /**\n * Returns the current enabled state of the async rate limiter\n */\n #getEnabled = (): boolean => {\n return !!parseFunctionOrValue(this.options.enabled, this)\n }\n\n /**\n * Returns the current limit of executions allowed within the time window\n */\n #getLimit = (): number => {\n return parseFunctionOrValue(this.options.limit, this)\n }\n\n /**\n * Returns the current time window in milliseconds\n */\n #getWindow = (): number => {\n return parseFunctionOrValue(this.options.window, this)\n }\n\n /**\n * Attempts to execute the rate-limited function if within the configured limits.\n * Will reject execution if the number of calls in the current window exceeds the limit.\n *\n * Error Handling:\n * - If the rate-limited function throws and no `onError` handler is configured,\n * the error will be thrown from this method.\n * - If an `onError` handler is configured, errors will be caught and passed to the handler,\n * and this method will return undefined.\n * - The error state can be checked using `getErrorCount()` and `getIsExecuting()`.\n *\n * @returns A promise that resolves with the function's return value, or undefined if an error occurred and was handled by onError\n * @throws The error from the rate-limited function if no onError handler is configured\n *\n * @example\n * ```ts\n * const rateLimiter = new AsyncRateLimiter(fn, { limit: 5, window: 1000 });\n *\n * // First 5 calls will return a promise that resolves with the result\n * const result = await rateLimiter.maybeExecute('arg1', 'arg2');\n *\n * // Additional calls within the window will return undefined\n * const result2 = await rateLimiter.maybeExecute('arg1', 'arg2'); // undefined\n * ```\n */\n maybeExecute = async (\n ...args: Parameters<TFn>\n ): Promise<ReturnType<TFn> | undefined> => {\n this.#setState({\n maybeExecuteCount: this.store.state.maybeExecuteCount + 1,\n })\n\n this.#cleanupOldExecutions()\n\n const relevantExecutionTimes = this.#getExecutionTimesInWindow()\n\n if (relevantExecutionTimes.length < this.#getLimit()) {\n await this.#execute(...args)\n return this.store.state.lastResult\n }\n\n this.#setState({\n rejectionCount: this.store.state.rejectionCount + 1,\n })\n this.options.onReject?.(args, this)\n return undefined\n }\n\n #execute = async (\n ...args: Parameters<TFn>\n ): Promise<ReturnType<TFn> | undefined> => {\n if (!this.#getEnabled()) return\n\n const currentMaybeExecute = this.store.state.maybeExecuteCount\n const now = Date.now()\n const executionTimes = [...this.store.state.executionTimes, now]\n this.#setState({\n isExecuting: true,\n executionTimes,\n })\n\n try {\n // Create a new AsyncRetryer for this execution to avoid cancelling concurrent executions\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.#setCleanupTimeout(now)\n this.#setState({\n successCount: this.store.state.successCount + 1,\n lastResult: result,\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 this.#setState({\n isExecuting: false,\n settleCount: this.store.state.settleCount + 1,\n })\n this.options.onSettled?.(args, this)\n }\n\n return this.store.state.lastResult\n }\n\n #getExecutionTimesInWindow = (): Array<number> => {\n if (this.options.windowType === 'sliding') {\n // For sliding window, return all executions within the current window\n return this.store.state.executionTimes.filter(\n (time) => time > Date.now() - this.#getWindow(),\n )\n } else {\n // For fixed window, return all executions in the current window\n // The window starts from the oldest execution time\n if (this.store.state.executionTimes.length === 0) {\n return []\n }\n const oldestExecution = Math.min(...this.store.state.executionTimes)\n const windowStart = oldestExecution\n const windowEnd = windowStart + this.#getWindow()\n const now = Date.now()\n\n // If the window has expired, return empty array\n if (now > windowEnd) {\n return []\n }\n\n // Otherwise, return all executions in the current window\n return this.store.state.executionTimes.filter(\n (time) => time >= windowStart && time <= windowEnd,\n )\n }\n }\n\n #setCleanupTimeout = (executionTime: number): void => {\n if (\n this.options.windowType === 'sliding' ||\n this.#timeoutIds.size === 0 // new fixed window\n ) {\n const now = Date.now()\n const timeUntilExpiration = executionTime - now + this.#getWindow() + 1\n const timeoutId = setTimeout(() => {\n this.#cleanupOldExecutions()\n this.#clearTimeout(timeoutId)\n }, timeUntilExpiration)\n this.#timeoutIds.add(timeoutId)\n }\n }\n\n #clearTimeout = (timeoutId: NodeJS.Timeout): void => {\n clearTimeout(timeoutId)\n this.#timeoutIds.delete(timeoutId)\n }\n\n #clearTimeouts = (): void => {\n this.#timeoutIds.forEach((timeoutId) => clearTimeout(timeoutId))\n this.#timeoutIds.clear()\n }\n\n #cleanupOldExecutions = (): void => {\n this.#setState({\n executionTimes: this.#getExecutionTimesInWindow(),\n })\n }\n\n /**\n * Returns the number of remaining executions allowed in the current window\n */\n getRemainingInWindow = (): number => {\n const relevantExecutionTimes = this.#getExecutionTimesInWindow()\n return Math.max(0, this.#getLimit() - relevantExecutionTimes.length)\n }\n\n /**\n * Returns the number of milliseconds until the next execution will be possible\n * For fixed windows, this is the time until the current window resets\n * For sliding windows, this is the time until the oldest execution expires\n */\n getMsUntilNextWindow = (): number => {\n if (this.getRemainingInWindow() > 0) {\n return 0\n }\n const oldestExecution = this.store.state.executionTimes[0] ?? Infinity\n return oldestExecution + this.#getWindow() - Date.now()\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 rateLimiter = new AsyncRateLimiter(\n * async (userId: string) => {\n * const signal = rateLimiter.getAbortSignal()\n * if (signal) {\n * const response = await fetch(`/api/users/${userId}`, { signal })\n * return response.json()\n * }\n * },\n * { limit: 5, window: 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 clear out the execution times or reset the rate limiter.\n */\n abort = (): void => {\n this.asyncRetryers.forEach((retryer) => retryer.abort())\n this.asyncRetryers.clear()\n this.#setState({\n isExecuting: false,\n })\n }\n\n /**\n * Resets the rate limiter state\n */\n reset = (): void => {\n this.#setState(getDefaultAsyncRateLimiterState())\n this.#clearTimeouts()\n this.asyncRetryers.forEach((retryer) => retryer.reset())\n }\n}\n\n/**\n * Creates an async rate-limited function that will execute the provided function up to a maximum number of times within a time window.\n *\n * Async vs Sync Versions:\n * The async version provides advanced features over the sync rate limit function:\n * - Returns promises that can be awaited for rate-limited function results\n * - Built-in retry support via AsyncRetryer integration\n * - Abort support to cancel in-flight executions\n * - Comprehensive error handling with onError callbacks and throwOnError control\n * - Detailed execution tracking (success/error/settle counts, rejection counts)\n * - More sophisticated window management with automatic cleanup\n *\n * The sync rate limit function is lighter weight and simpler when you don't need async features,\n * return values, or execution control.\n *\n * What is Rate Limiting?\n * Rate limiting allows a function to execute up to a limit within a time window,\n * then blocks all subsequent calls until the window passes. This can lead to \"bursty\" behavior where\n * all executions happen immediately, followed by a complete block.\n *\n * Window Types:\n * - 'fixed': A strict window that resets after the window period. All executions within the window count\n * towards the limit, and the window resets completely after the period.\n * - 'sliding': A rolling window that allows executions as old ones expire. This provides a more\n * consistent rate of execution over time.\n *\n * Configuration Options:\n * - `limit`: Maximum number of executions allowed within the window (required)\n * - `window`: Time window in milliseconds (required)\n * - `windowType`: 'fixed' or 'sliding' (default: 'fixed')\n * - `enabled`: Whether the rate limiter is enabled (default: true)\n * - `asyncRetryerOptions`: Configure retry behavior for executions\n *\n * When to Use Rate Limiting:\n * Rate limiting is best used for hard API limits or resource constraints. For UI updates or\n * smoothing out frequent events, throttling or debouncing usually provide better user experience.\n * - A rate limiter will allow all executions until the limit is reached, then block all subsequent calls until the window resets\n * - A throttler ensures even spacing between executions, which can be better for consistent performance\n * - A debouncer collapses multiple calls into one, which is better for handling bursts of events\n *\n * Error Handling:\n * - If an `onError` handler is provided, it will be called with the error and rate limiter 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 AsyncRateLimiter instance\n * - Rate limit rejections (when limit is exceeded) are handled separately from execution errors via the `onReject` handler\n *\n * State Management:\n * - Uses TanStack Store for reactive state management\n * - Use `initialState` to provide initial state values when creating the rate limiter\n * - `initialState` can be a partial state object\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 * - Use `onReject` callback to react to executions being rejected when rate limit is exceeded\n * - The state includes execution times, success/error counts, and current execution status\n * - State can be accessed via the underlying AsyncRateLimiter 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 * // Rate limit to 5 calls per minute with a sliding window\n * const rateLimited = asyncRateLimit(makeApiCall, {\n * limit: 5,\n * window: 60000,\n * windowType: 'sliding',\n * onError: (error) => {\n * console.error('API call failed:', error);\n * },\n * onReject: (rateLimiter) => {\n * console.log(`Rate limit exceeded. Try again in ${rateLimiter.getMsUntilNextWindow()}ms`);\n * }\n * });\n *\n * // First 5 calls will execute immediately\n * // Additional calls will be rejected until the minute window resets\n * // Returns the API response directly\n * const result = await rateLimited();\n *\n * // For more even execution, consider using throttle instead:\n * const throttled = throttle(makeApiCall, { wait: 12000 }); // One call every 12 seconds\n * ```\n */\nexport function asyncRateLimit<TFn extends AnyAsyncFunction>(\n fn: TFn,\n initialOptions: AsyncRateLimiterOptions<TFn>,\n) {\n const rateLimiter = new AsyncRateLimiter(fn, initialOptions)\n return rateLimiter.maybeExecute\n}\n"],"mappings":"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"}
{"version":3,"file":"async-rate-limiter.cjs","names":["Store","#setState","#cleanupOldExecutions","#getExecutionTimesInWindow","#getLimit","#execute","#getWindow","#clearTimeouts","#setCleanupTimeout","#getEnabled","parseFunctionOrValue","AsyncRetryer","#timeoutIds","#clearTimeout"],"sources":["../src/async-rate-limiter.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 } from './types'\n\nexport interface AsyncRateLimiterState<TFn extends AnyAsyncFunction> {\n /**\n * Number of function executions that have resulted in errors\n */\n errorCount: number\n /**\n * Array of timestamps when executions occurred for rate limiting calculations\n */\n executionTimes: Array<number>\n /**\n * Whether the rate limiter has exceeded the limit\n */\n isExceeded: boolean\n /**\n * Whether the rate-limited function is currently executing asynchronously\n */\n isExecuting: boolean\n /**\n * The result from the most recent successful function execution\n */\n lastResult: ReturnType<TFn> | undefined\n /**\n * Number of function executions that have been rejected due to rate limiting\n */\n rejectionCount: number\n /**\n * Number of function executions that have completed (either successfully or with errors)\n */\n settleCount: number\n /**\n * Current execution status - 'disabled' when not active, 'executing' when executing, 'idle' when not executing, 'exceeded' when rate limit is exceeded\n */\n status: 'disabled' | 'executing' | 'exceeded' | 'idle'\n /**\n * Number of function executions that have completed successfully\n */\n successCount: number\n /**\n * Number of times maybeExecute has been called (for reduction calculations)\n */\n maybeExecuteCount: number\n}\n\nfunction getDefaultAsyncRateLimiterState<\n TFn extends AnyAsyncFunction,\n>(): AsyncRateLimiterState<TFn> {\n return {\n errorCount: 0,\n executionTimes: [],\n isExceeded: false,\n isExecuting: false,\n lastResult: undefined,\n maybeExecuteCount: 0,\n rejectionCount: 0,\n settleCount: 0,\n status: 'idle',\n successCount: 0,\n }\n}\n\n/**\n * Options for configuring an async rate-limited function\n */\nexport interface AsyncRateLimiterOptions<TFn extends AnyAsyncFunction> {\n /**\n * Options for configuring the underlying async retryer\n */\n asyncRetryerOptions?: AsyncRetryerOptions<TFn>\n /**\n * Whether the rate limiter 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 | ((rateLimiter: AsyncRateLimiter<TFn>) => boolean)\n /**\n * Initial state for the rate limiter\n */\n initialState?: Partial<AsyncRateLimiterState<TFn>>\n /**\n * Optional key to identify this async rate limiter instance.\n * If provided, the async rate limiter will be identified by this key in the devtools and PacerProvider if applicable.\n */\n key?: string\n /**\n * Maximum number of executions allowed within the time window.\n * Can be a number or a function that returns a number.\n */\n limit: number | ((rateLimiter: AsyncRateLimiter<TFn>) => number)\n /**\n * Optional error handler for when the rate-limited function throws.\n * If provided, the handler will be called with the error and rate limiter 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 rateLimiter: AsyncRateLimiter<TFn>,\n ) => void\n /**\n * Optional callback function that is called when an execution is rejected due to rate limiting\n */\n onReject?: (args: Parameters<TFn>, rateLimiter: AsyncRateLimiter<TFn>) => void\n /**\n * Optional function to call when the rate-limited function is executed\n */\n onSettled?: (\n args: Parameters<TFn>,\n rateLimiter: AsyncRateLimiter<TFn>,\n ) => void\n /**\n * Optional function to call when the rate-limited function is executed\n */\n onSuccess?: (\n result: ReturnType<TFn>,\n args: Parameters<TFn>,\n rateLimiter: AsyncRateLimiter<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 * Time window in milliseconds within which the limit applies.\n * Can be a number or a function that returns a number.\n */\n window: number | ((rateLimiter: AsyncRateLimiter<TFn>) => number)\n /**\n * Type of window to use for rate limiting\n * - 'fixed': Uses a fixed window that resets after the window period\n * - 'sliding': Uses a sliding window that allows executions as old ones expire\n * Defaults to 'fixed'\n */\n windowType?: 'fixed' | 'sliding'\n}\n\n/**\n * Utility function for sharing common `AsyncRateLimiterOptions` options between different `AsyncRateLimiter` instances.\n */\nexport function asyncRateLimiterOptions<\n TFn extends AnyAsyncFunction = AnyAsyncFunction,\n TOptions extends Partial<AsyncRateLimiterOptions<TFn>> = Partial<\n AsyncRateLimiterOptions<TFn>\n >,\n>(options: TOptions): TOptions {\n return options\n}\n\nconst defaultOptions: Omit<\n Required<AsyncRateLimiterOptions<any>>,\n 'initialState' | 'onError' | 'onReject' | 'onSettled' | 'onSuccess' | 'key'\n> = {\n asyncRetryerOptions: {\n maxAttempts: 1,\n },\n enabled: true,\n limit: 1,\n window: 0,\n windowType: 'fixed',\n throwOnError: true,\n}\n\n/**\n * A class that creates an async rate-limited function.\n *\n * Async vs Sync Versions:\n * The async version provides advanced features over the sync RateLimiter:\n * - Returns promises that can be awaited for rate-limited function results\n * - Built-in retry support via AsyncRetryer integration\n * - Abort support to cancel in-flight executions\n * - Comprehensive error handling with onError callbacks and throwOnError control\n * - Detailed execution tracking (success/error/settle counts, rejection counts)\n * - More sophisticated window management with automatic cleanup\n *\n * The sync RateLimiter is lighter weight and simpler when you don't need async features,\n * return values, or execution control.\n *\n * What is Rate Limiting?\n * Rate limiting allows a function to execute up to a limit within a time window,\n * then blocks all subsequent calls until the window passes. This can lead to \"bursty\" behavior where\n * all executions happen immediately, followed by a complete block.\n *\n * Window Types:\n * - 'fixed': A strict window that resets after the window period. All executions within the window count\n * towards the limit, and the window resets completely after the period.\n * - 'sliding': A rolling window that allows executions as old ones expire. This provides a more\n * consistent rate of execution over time.\n *\n * When to Use Rate Limiting:\n * Rate limiting is best used for hard API limits or resource constraints. For UI updates or\n * smoothing out frequent events, throttling or debouncing usually provide better user experience.\n * - Throttling: Ensures consistent spacing between executions (e.g. max once per 200ms)\n * - Debouncing: Waits for a pause in calls before executing (e.g. after 500ms of no calls)\n *\n * State Management:\n * - Uses TanStack Store for reactive state management\n * - Use `initialState` to provide initial state values when creating the rate limiter\n * - `initialState` can be a partial state object\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 * - Use `onReject` callback to react to executions being rejected when rate limit is exceeded\n * - The state includes execution times, success/error counts, and current execution status\n * - State can be accessed via `asyncRateLimiter.store.state` when using the class directly\n * - When using framework adapters (React/Solid), state is accessed from `asyncRateLimiter.state`\n *\n * Error Handling:\n * - If an `onError` handler is provided, it will be called with the error and rate limiter 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 AsyncRateLimiter instance\n * - Rate limit rejections (when limit is exceeded) are handled separately from execution errors via the `onReject` handler\n *\n * @example\n * ```ts\n * const rateLimiter = new AsyncRateLimiter(\n * async (id: string) => await api.getData(id),\n * {\n * limit: 5,\n * window: 1000,\n * windowType: 'sliding',\n * onError: (error) => {\n * console.error('API call failed:', error);\n * },\n * onReject: (limiter) => {\n * console.log(`Rate limit exceeded. Try again in ${limiter.getMsUntilNextWindow()}ms`);\n * }\n * }\n * );\n *\n * // Will execute immediately until limit reached, then block\n * // Returns the API response directly\n * const data = await rateLimiter.maybeExecute('123');\n * ```\n */\nexport class AsyncRateLimiter<TFn extends AnyAsyncFunction> {\n readonly store: Store<Readonly<AsyncRateLimiterState<TFn>>> = new Store<\n AsyncRateLimiterState<TFn>\n >(getDefaultAsyncRateLimiterState<TFn>())\n key: string | undefined\n options: AsyncRateLimiterOptions<TFn>\n asyncRetryers = new Map<number, AsyncRetryer<TFn>>()\n #timeoutIds: Set<ReturnType<typeof setTimeout>> = new Set()\n\n constructor(\n public fn: TFn,\n initialOptions: AsyncRateLimiterOptions<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 for (const executionTime of this.#getExecutionTimesInWindow()) {\n this.#setCleanupTimeout(executionTime)\n }\n\n if (this.key) {\n pacerEventClient.on('d-AsyncRateLimiter', (event) => {\n if (event.payload.key !== this.key) return\n this.#setState(\n event.payload.store.state as Partial<AsyncRateLimiterState<TFn>>,\n )\n this.setOptions(\n event.payload.options as Partial<AsyncRateLimiterOptions<TFn>>,\n )\n })\n }\n }\n\n /**\n * Updates the async rate limiter options\n */\n setOptions = (newOptions: Partial<AsyncRateLimiterOptions<TFn>>): void => {\n this.options = { ...this.options, ...newOptions }\n }\n\n #setState = (newState: Partial<AsyncRateLimiterState<TFn>>): void => {\n this.store.setState((state) => {\n const combinedState = {\n ...state,\n ...newState,\n }\n const isExceeded = combinedState.executionTimes.length >= this.#getLimit()\n const status = !this.#getEnabled()\n ? 'disabled'\n : combinedState.isExecuting\n ? 'executing'\n : isExceeded\n ? 'exceeded'\n : 'idle'\n return {\n ...combinedState,\n isExceeded,\n status,\n }\n })\n emitChange('AsyncRateLimiter', this)\n }\n\n /**\n * Returns the current enabled state of the async rate limiter\n */\n #getEnabled = (): boolean => {\n return !!parseFunctionOrValue(this.options.enabled, this)\n }\n\n /**\n * Returns the current limit of executions allowed within the time window\n */\n #getLimit = (): number => {\n return parseFunctionOrValue(this.options.limit, this)\n }\n\n /**\n * Returns the current time window in milliseconds\n */\n #getWindow = (): number => {\n return parseFunctionOrValue(this.options.window, this)\n }\n\n /**\n * Attempts to execute the rate-limited function if within the configured limits.\n * Will reject execution if the number of calls in the current window exceeds the limit.\n *\n * Error Handling:\n * - If the rate-limited function throws and no `onError` handler is configured,\n * the error will be thrown from this method.\n * - If an `onError` handler is configured, errors will be caught and passed to the handler,\n * and this method will return undefined.\n * - The error state can be checked using `getErrorCount()` and `getIsExecuting()`.\n *\n * @returns A promise that resolves with the function's return value, or undefined if an error occurred and was handled by onError\n * @throws The error from the rate-limited function if no onError handler is configured\n *\n * @example\n * ```ts\n * const rateLimiter = new AsyncRateLimiter(fn, { limit: 5, window: 1000 });\n *\n * // First 5 calls will return a promise that resolves with the result\n * const result = await rateLimiter.maybeExecute('arg1', 'arg2');\n *\n * // Additional calls within the window will return undefined\n * const result2 = await rateLimiter.maybeExecute('arg1', 'arg2'); // undefined\n * ```\n */\n maybeExecute = async (\n ...args: Parameters<TFn>\n ): Promise<ReturnType<TFn> | undefined> => {\n this.#setState({\n maybeExecuteCount: this.store.state.maybeExecuteCount + 1,\n })\n\n this.#cleanupOldExecutions()\n\n const relevantExecutionTimes = this.#getExecutionTimesInWindow()\n\n if (relevantExecutionTimes.length < this.#getLimit()) {\n await this.#execute(...args)\n return this.store.state.lastResult\n }\n\n this.#setState({\n rejectionCount: this.store.state.rejectionCount + 1,\n })\n this.options.onReject?.(args, this)\n return undefined\n }\n\n #execute = async (\n ...args: Parameters<TFn>\n ): Promise<ReturnType<TFn> | undefined> => {\n if (!this.#getEnabled()) return\n\n const currentMaybeExecute = this.store.state.maybeExecuteCount\n const now = Date.now()\n const executionTimes = [...this.store.state.executionTimes, now]\n this.#setState({\n isExecuting: true,\n executionTimes,\n })\n\n try {\n // Create a new AsyncRetryer for this execution to avoid cancelling concurrent executions\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.#setCleanupTimeout(now)\n this.#setState({\n successCount: this.store.state.successCount + 1,\n lastResult: result,\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 this.#setState({\n isExecuting: false,\n settleCount: this.store.state.settleCount + 1,\n })\n this.options.onSettled?.(args, this)\n }\n\n return this.store.state.lastResult\n }\n\n #getExecutionTimesInWindow = (): Array<number> => {\n if (this.options.windowType === 'sliding') {\n // For sliding window, return all executions within the current window\n return this.store.state.executionTimes.filter(\n (time) => time > Date.now() - this.#getWindow(),\n )\n } else {\n // For fixed window, return all executions in the current window\n // The window starts from the oldest execution time\n if (this.store.state.executionTimes.length === 0) {\n return []\n }\n const oldestExecution = Math.min(...this.store.state.executionTimes)\n const windowStart = oldestExecution\n const windowEnd = windowStart + this.#getWindow()\n const now = Date.now()\n\n // If the window has expired, return empty array\n if (now > windowEnd) {\n return []\n }\n\n // Otherwise, return all executions in the current window\n return this.store.state.executionTimes.filter(\n (time) => time >= windowStart && time <= windowEnd,\n )\n }\n }\n\n #setCleanupTimeout = (executionTime: number): void => {\n if (\n this.options.windowType === 'sliding' ||\n this.#timeoutIds.size === 0 // new fixed window\n ) {\n const now = Date.now()\n const timeUntilExpiration = executionTime - now + this.#getWindow() + 1\n const timeoutId = setTimeout(() => {\n this.#cleanupOldExecutions()\n this.#clearTimeout(timeoutId)\n }, timeUntilExpiration)\n this.#timeoutIds.add(timeoutId)\n }\n }\n\n #clearTimeout = (timeoutId: ReturnType<typeof setTimeout>): void => {\n clearTimeout(timeoutId)\n this.#timeoutIds.delete(timeoutId)\n }\n\n #clearTimeouts = (): void => {\n this.#timeoutIds.forEach((timeoutId) => clearTimeout(timeoutId))\n this.#timeoutIds.clear()\n }\n\n #cleanupOldExecutions = (): void => {\n this.#setState({\n executionTimes: this.#getExecutionTimesInWindow(),\n })\n }\n\n /**\n * Returns the number of remaining executions allowed in the current window\n */\n getRemainingInWindow = (): number => {\n const relevantExecutionTimes = this.#getExecutionTimesInWindow()\n return Math.max(0, this.#getLimit() - relevantExecutionTimes.length)\n }\n\n /**\n * Returns the number of milliseconds until the next execution will be possible\n * For fixed windows, this is the time until the current window resets\n * For sliding windows, this is the time until the oldest execution expires\n */\n getMsUntilNextWindow = (): number => {\n if (this.getRemainingInWindow() > 0) {\n return 0\n }\n const oldestExecution = this.store.state.executionTimes[0] ?? Infinity\n return oldestExecution + this.#getWindow() - Date.now()\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 rateLimiter = new AsyncRateLimiter(\n * async (userId: string) => {\n * const signal = rateLimiter.getAbortSignal()\n * if (signal) {\n * const response = await fetch(`/api/users/${userId}`, { signal })\n * return response.json()\n * }\n * },\n * { limit: 5, window: 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 clear out the execution times or reset the rate limiter.\n */\n abort = (): void => {\n this.asyncRetryers.forEach((retryer) => retryer.abort())\n this.asyncRetryers.clear()\n this.#setState({\n isExecuting: false,\n })\n }\n\n /**\n * Resets the rate limiter state\n */\n reset = (): void => {\n this.#setState(getDefaultAsyncRateLimiterState())\n this.#clearTimeouts()\n this.asyncRetryers.forEach((retryer) => retryer.reset())\n }\n}\n\n/**\n * Creates an async rate-limited function that will execute the provided function up to a maximum number of times within a time window.\n *\n * Async vs Sync Versions:\n * The async version provides advanced features over the sync rate limit function:\n * - Returns promises that can be awaited for rate-limited function results\n * - Built-in retry support via AsyncRetryer integration\n * - Abort support to cancel in-flight executions\n * - Comprehensive error handling with onError callbacks and throwOnError control\n * - Detailed execution tracking (success/error/settle counts, rejection counts)\n * - More sophisticated window management with automatic cleanup\n *\n * The sync rate limit function is lighter weight and simpler when you don't need async features,\n * return values, or execution control.\n *\n * What is Rate Limiting?\n * Rate limiting allows a function to execute up to a limit within a time window,\n * then blocks all subsequent calls until the window passes. This can lead to \"bursty\" behavior where\n * all executions happen immediately, followed by a complete block.\n *\n * Window Types:\n * - 'fixed': A strict window that resets after the window period. All executions within the window count\n * towards the limit, and the window resets completely after the period.\n * - 'sliding': A rolling window that allows executions as old ones expire. This provides a more\n * consistent rate of execution over time.\n *\n * Configuration Options:\n * - `limit`: Maximum number of executions allowed within the window (required)\n * - `window`: Time window in milliseconds (required)\n * - `windowType`: 'fixed' or 'sliding' (default: 'fixed')\n * - `enabled`: Whether the rate limiter is enabled (default: true)\n * - `asyncRetryerOptions`: Configure retry behavior for executions\n *\n * When to Use Rate Limiting:\n * Rate limiting is best used for hard API limits or resource constraints. For UI updates or\n * smoothing out frequent events, throttling or debouncing usually provide better user experience.\n * - A rate limiter will allow all executions until the limit is reached, then block all subsequent calls until the window resets\n * - A throttler ensures even spacing between executions, which can be better for consistent performance\n * - A debouncer collapses multiple calls into one, which is better for handling bursts of events\n *\n * Error Handling:\n * - If an `onError` handler is provided, it will be called with the error and rate limiter 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 AsyncRateLimiter instance\n * - Rate limit rejections (when limit is exceeded) are handled separately from execution errors via the `onReject` handler\n *\n * State Management:\n * - Uses TanStack Store for reactive state management\n * - Use `initialState` to provide initial state values when creating the rate limiter\n * - `initialState` can be a partial state object\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 * - Use `onReject` callback to react to executions being rejected when rate limit is exceeded\n * - The state includes execution times, success/error counts, and current execution status\n * - State can be accessed via the underlying AsyncRateLimiter 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 * // Rate limit to 5 calls per minute with a sliding window\n * const rateLimited = asyncRateLimit(makeApiCall, {\n * limit: 5,\n * window: 60000,\n * windowType: 'sliding',\n * onError: (error) => {\n * console.error('API call failed:', error);\n * },\n * onReject: (rateLimiter) => {\n * console.log(`Rate limit exceeded. Try again in ${rateLimiter.getMsUntilNextWindow()}ms`);\n * }\n * });\n *\n * // First 5 calls will execute immediately\n * // Additional calls will be rejected until the minute window resets\n * // Returns the API response directly\n * const result = await rateLimited();\n *\n * // For more even execution, consider using throttle instead:\n * const throttled = throttle(makeApiCall, { wait: 12000 }); // One call every 12 seconds\n * ```\n */\nexport function asyncRateLimit<TFn extends AnyAsyncFunction>(\n fn: TFn,\n initialOptions: AsyncRateLimiterOptions<TFn>,\n) {\n const rateLimiter = new AsyncRateLimiter(fn, initialOptions)\n return rateLimiter.maybeExecute\n}\n"],"mappings":"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"}

@@ -1,1 +0,1 @@

{"version":3,"file":"async-rate-limiter.js","names":["#setState","#cleanupOldExecutions","#getExecutionTimesInWindow","#getLimit","#execute","#getWindow","#clearTimeouts","#setCleanupTimeout","#getEnabled","#timeoutIds","#clearTimeout"],"sources":["../src/async-rate-limiter.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 } from './types'\n\nexport interface AsyncRateLimiterState<TFn extends AnyAsyncFunction> {\n /**\n * Number of function executions that have resulted in errors\n */\n errorCount: number\n /**\n * Array of timestamps when executions occurred for rate limiting calculations\n */\n executionTimes: Array<number>\n /**\n * Whether the rate limiter has exceeded the limit\n */\n isExceeded: boolean\n /**\n * Whether the rate-limited function is currently executing asynchronously\n */\n isExecuting: boolean\n /**\n * The result from the most recent successful function execution\n */\n lastResult: ReturnType<TFn> | undefined\n /**\n * Number of function executions that have been rejected due to rate limiting\n */\n rejectionCount: number\n /**\n * Number of function executions that have completed (either successfully or with errors)\n */\n settleCount: number\n /**\n * Current execution status - 'disabled' when not active, 'executing' when executing, 'idle' when not executing, 'exceeded' when rate limit is exceeded\n */\n status: 'disabled' | 'executing' | 'exceeded' | 'idle'\n /**\n * Number of function executions that have completed successfully\n */\n successCount: number\n /**\n * Number of times maybeExecute has been called (for reduction calculations)\n */\n maybeExecuteCount: number\n}\n\nfunction getDefaultAsyncRateLimiterState<\n TFn extends AnyAsyncFunction,\n>(): AsyncRateLimiterState<TFn> {\n return {\n errorCount: 0,\n executionTimes: [],\n isExceeded: false,\n isExecuting: false,\n lastResult: undefined,\n maybeExecuteCount: 0,\n rejectionCount: 0,\n settleCount: 0,\n status: 'idle',\n successCount: 0,\n }\n}\n\n/**\n * Options for configuring an async rate-limited function\n */\nexport interface AsyncRateLimiterOptions<TFn extends AnyAsyncFunction> {\n /**\n * Options for configuring the underlying async retryer\n */\n asyncRetryerOptions?: AsyncRetryerOptions<TFn>\n /**\n * Whether the rate limiter 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 | ((rateLimiter: AsyncRateLimiter<TFn>) => boolean)\n /**\n * Initial state for the rate limiter\n */\n initialState?: Partial<AsyncRateLimiterState<TFn>>\n /**\n * Optional key to identify this async rate limiter instance.\n * If provided, the async rate limiter will be identified by this key in the devtools and PacerProvider if applicable.\n */\n key?: string\n /**\n * Maximum number of executions allowed within the time window.\n * Can be a number or a function that returns a number.\n */\n limit: number | ((rateLimiter: AsyncRateLimiter<TFn>) => number)\n /**\n * Optional error handler for when the rate-limited function throws.\n * If provided, the handler will be called with the error and rate limiter 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 rateLimiter: AsyncRateLimiter<TFn>,\n ) => void\n /**\n * Optional callback function that is called when an execution is rejected due to rate limiting\n */\n onReject?: (args: Parameters<TFn>, rateLimiter: AsyncRateLimiter<TFn>) => void\n /**\n * Optional function to call when the rate-limited function is executed\n */\n onSettled?: (\n args: Parameters<TFn>,\n rateLimiter: AsyncRateLimiter<TFn>,\n ) => void\n /**\n * Optional function to call when the rate-limited function is executed\n */\n onSuccess?: (\n result: ReturnType<TFn>,\n args: Parameters<TFn>,\n rateLimiter: AsyncRateLimiter<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 * Time window in milliseconds within which the limit applies.\n * Can be a number or a function that returns a number.\n */\n window: number | ((rateLimiter: AsyncRateLimiter<TFn>) => number)\n /**\n * Type of window to use for rate limiting\n * - 'fixed': Uses a fixed window that resets after the window period\n * - 'sliding': Uses a sliding window that allows executions as old ones expire\n * Defaults to 'fixed'\n */\n windowType?: 'fixed' | 'sliding'\n}\n\n/**\n * Utility function for sharing common `AsyncRateLimiterOptions` options between different `AsyncRateLimiter` instances.\n */\nexport function asyncRateLimiterOptions<\n TFn extends AnyAsyncFunction = AnyAsyncFunction,\n TOptions extends Partial<AsyncRateLimiterOptions<TFn>> = Partial<\n AsyncRateLimiterOptions<TFn>\n >,\n>(options: TOptions): TOptions {\n return options\n}\n\nconst defaultOptions: Omit<\n Required<AsyncRateLimiterOptions<any>>,\n 'initialState' | 'onError' | 'onReject' | 'onSettled' | 'onSuccess' | 'key'\n> = {\n asyncRetryerOptions: {\n maxAttempts: 1,\n },\n enabled: true,\n limit: 1,\n window: 0,\n windowType: 'fixed',\n throwOnError: true,\n}\n\n/**\n * A class that creates an async rate-limited function.\n *\n * Async vs Sync Versions:\n * The async version provides advanced features over the sync RateLimiter:\n * - Returns promises that can be awaited for rate-limited function results\n * - Built-in retry support via AsyncRetryer integration\n * - Abort support to cancel in-flight executions\n * - Comprehensive error handling with onError callbacks and throwOnError control\n * - Detailed execution tracking (success/error/settle counts, rejection counts)\n * - More sophisticated window management with automatic cleanup\n *\n * The sync RateLimiter is lighter weight and simpler when you don't need async features,\n * return values, or execution control.\n *\n * What is Rate Limiting?\n * Rate limiting allows a function to execute up to a limit within a time window,\n * then blocks all subsequent calls until the window passes. This can lead to \"bursty\" behavior where\n * all executions happen immediately, followed by a complete block.\n *\n * Window Types:\n * - 'fixed': A strict window that resets after the window period. All executions within the window count\n * towards the limit, and the window resets completely after the period.\n * - 'sliding': A rolling window that allows executions as old ones expire. This provides a more\n * consistent rate of execution over time.\n *\n * When to Use Rate Limiting:\n * Rate limiting is best used for hard API limits or resource constraints. For UI updates or\n * smoothing out frequent events, throttling or debouncing usually provide better user experience.\n * - Throttling: Ensures consistent spacing between executions (e.g. max once per 200ms)\n * - Debouncing: Waits for a pause in calls before executing (e.g. after 500ms of no calls)\n *\n * State Management:\n * - Uses TanStack Store for reactive state management\n * - Use `initialState` to provide initial state values when creating the rate limiter\n * - `initialState` can be a partial state object\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 * - Use `onReject` callback to react to executions being rejected when rate limit is exceeded\n * - The state includes execution times, success/error counts, and current execution status\n * - State can be accessed via `asyncRateLimiter.store.state` when using the class directly\n * - When using framework adapters (React/Solid), state is accessed from `asyncRateLimiter.state`\n *\n * Error Handling:\n * - If an `onError` handler is provided, it will be called with the error and rate limiter 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 AsyncRateLimiter instance\n * - Rate limit rejections (when limit is exceeded) are handled separately from execution errors via the `onReject` handler\n *\n * @example\n * ```ts\n * const rateLimiter = new AsyncRateLimiter(\n * async (id: string) => await api.getData(id),\n * {\n * limit: 5,\n * window: 1000,\n * windowType: 'sliding',\n * onError: (error) => {\n * console.error('API call failed:', error);\n * },\n * onReject: (limiter) => {\n * console.log(`Rate limit exceeded. Try again in ${limiter.getMsUntilNextWindow()}ms`);\n * }\n * }\n * );\n *\n * // Will execute immediately until limit reached, then block\n * // Returns the API response directly\n * const data = await rateLimiter.maybeExecute('123');\n * ```\n */\nexport class AsyncRateLimiter<TFn extends AnyAsyncFunction> {\n readonly store: Store<Readonly<AsyncRateLimiterState<TFn>>> = new Store<\n AsyncRateLimiterState<TFn>\n >(getDefaultAsyncRateLimiterState<TFn>())\n key: string | undefined\n options: AsyncRateLimiterOptions<TFn>\n asyncRetryers = new Map<number, AsyncRetryer<TFn>>()\n #timeoutIds: Set<NodeJS.Timeout> = new Set()\n\n constructor(\n public fn: TFn,\n initialOptions: AsyncRateLimiterOptions<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 for (const executionTime of this.#getExecutionTimesInWindow()) {\n this.#setCleanupTimeout(executionTime)\n }\n\n if (this.key) {\n pacerEventClient.on('d-AsyncRateLimiter', (event) => {\n if (event.payload.key !== this.key) return\n this.#setState(\n event.payload.store.state as Partial<AsyncRateLimiterState<TFn>>,\n )\n this.setOptions(\n event.payload.options as Partial<AsyncRateLimiterOptions<TFn>>,\n )\n })\n }\n }\n\n /**\n * Updates the async rate limiter options\n */\n setOptions = (newOptions: Partial<AsyncRateLimiterOptions<TFn>>): void => {\n this.options = { ...this.options, ...newOptions }\n }\n\n #setState = (newState: Partial<AsyncRateLimiterState<TFn>>): void => {\n this.store.setState((state) => {\n const combinedState = {\n ...state,\n ...newState,\n }\n const isExceeded = combinedState.executionTimes.length >= this.#getLimit()\n const status = !this.#getEnabled()\n ? 'disabled'\n : combinedState.isExecuting\n ? 'executing'\n : isExceeded\n ? 'exceeded'\n : 'idle'\n return {\n ...combinedState,\n isExceeded,\n status,\n }\n })\n emitChange('AsyncRateLimiter', this)\n }\n\n /**\n * Returns the current enabled state of the async rate limiter\n */\n #getEnabled = (): boolean => {\n return !!parseFunctionOrValue(this.options.enabled, this)\n }\n\n /**\n * Returns the current limit of executions allowed within the time window\n */\n #getLimit = (): number => {\n return parseFunctionOrValue(this.options.limit, this)\n }\n\n /**\n * Returns the current time window in milliseconds\n */\n #getWindow = (): number => {\n return parseFunctionOrValue(this.options.window, this)\n }\n\n /**\n * Attempts to execute the rate-limited function if within the configured limits.\n * Will reject execution if the number of calls in the current window exceeds the limit.\n *\n * Error Handling:\n * - If the rate-limited function throws and no `onError` handler is configured,\n * the error will be thrown from this method.\n * - If an `onError` handler is configured, errors will be caught and passed to the handler,\n * and this method will return undefined.\n * - The error state can be checked using `getErrorCount()` and `getIsExecuting()`.\n *\n * @returns A promise that resolves with the function's return value, or undefined if an error occurred and was handled by onError\n * @throws The error from the rate-limited function if no onError handler is configured\n *\n * @example\n * ```ts\n * const rateLimiter = new AsyncRateLimiter(fn, { limit: 5, window: 1000 });\n *\n * // First 5 calls will return a promise that resolves with the result\n * const result = await rateLimiter.maybeExecute('arg1', 'arg2');\n *\n * // Additional calls within the window will return undefined\n * const result2 = await rateLimiter.maybeExecute('arg1', 'arg2'); // undefined\n * ```\n */\n maybeExecute = async (\n ...args: Parameters<TFn>\n ): Promise<ReturnType<TFn> | undefined> => {\n this.#setState({\n maybeExecuteCount: this.store.state.maybeExecuteCount + 1,\n })\n\n this.#cleanupOldExecutions()\n\n const relevantExecutionTimes = this.#getExecutionTimesInWindow()\n\n if (relevantExecutionTimes.length < this.#getLimit()) {\n await this.#execute(...args)\n return this.store.state.lastResult\n }\n\n this.#setState({\n rejectionCount: this.store.state.rejectionCount + 1,\n })\n this.options.onReject?.(args, this)\n return undefined\n }\n\n #execute = async (\n ...args: Parameters<TFn>\n ): Promise<ReturnType<TFn> | undefined> => {\n if (!this.#getEnabled()) return\n\n const currentMaybeExecute = this.store.state.maybeExecuteCount\n const now = Date.now()\n const executionTimes = [...this.store.state.executionTimes, now]\n this.#setState({\n isExecuting: true,\n executionTimes,\n })\n\n try {\n // Create a new AsyncRetryer for this execution to avoid cancelling concurrent executions\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.#setCleanupTimeout(now)\n this.#setState({\n successCount: this.store.state.successCount + 1,\n lastResult: result,\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 this.#setState({\n isExecuting: false,\n settleCount: this.store.state.settleCount + 1,\n })\n this.options.onSettled?.(args, this)\n }\n\n return this.store.state.lastResult\n }\n\n #getExecutionTimesInWindow = (): Array<number> => {\n if (this.options.windowType === 'sliding') {\n // For sliding window, return all executions within the current window\n return this.store.state.executionTimes.filter(\n (time) => time > Date.now() - this.#getWindow(),\n )\n } else {\n // For fixed window, return all executions in the current window\n // The window starts from the oldest execution time\n if (this.store.state.executionTimes.length === 0) {\n return []\n }\n const oldestExecution = Math.min(...this.store.state.executionTimes)\n const windowStart = oldestExecution\n const windowEnd = windowStart + this.#getWindow()\n const now = Date.now()\n\n // If the window has expired, return empty array\n if (now > windowEnd) {\n return []\n }\n\n // Otherwise, return all executions in the current window\n return this.store.state.executionTimes.filter(\n (time) => time >= windowStart && time <= windowEnd,\n )\n }\n }\n\n #setCleanupTimeout = (executionTime: number): void => {\n if (\n this.options.windowType === 'sliding' ||\n this.#timeoutIds.size === 0 // new fixed window\n ) {\n const now = Date.now()\n const timeUntilExpiration = executionTime - now + this.#getWindow() + 1\n const timeoutId = setTimeout(() => {\n this.#cleanupOldExecutions()\n this.#clearTimeout(timeoutId)\n }, timeUntilExpiration)\n this.#timeoutIds.add(timeoutId)\n }\n }\n\n #clearTimeout = (timeoutId: NodeJS.Timeout): void => {\n clearTimeout(timeoutId)\n this.#timeoutIds.delete(timeoutId)\n }\n\n #clearTimeouts = (): void => {\n this.#timeoutIds.forEach((timeoutId) => clearTimeout(timeoutId))\n this.#timeoutIds.clear()\n }\n\n #cleanupOldExecutions = (): void => {\n this.#setState({\n executionTimes: this.#getExecutionTimesInWindow(),\n })\n }\n\n /**\n * Returns the number of remaining executions allowed in the current window\n */\n getRemainingInWindow = (): number => {\n const relevantExecutionTimes = this.#getExecutionTimesInWindow()\n return Math.max(0, this.#getLimit() - relevantExecutionTimes.length)\n }\n\n /**\n * Returns the number of milliseconds until the next execution will be possible\n * For fixed windows, this is the time until the current window resets\n * For sliding windows, this is the time until the oldest execution expires\n */\n getMsUntilNextWindow = (): number => {\n if (this.getRemainingInWindow() > 0) {\n return 0\n }\n const oldestExecution = this.store.state.executionTimes[0] ?? Infinity\n return oldestExecution + this.#getWindow() - Date.now()\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 rateLimiter = new AsyncRateLimiter(\n * async (userId: string) => {\n * const signal = rateLimiter.getAbortSignal()\n * if (signal) {\n * const response = await fetch(`/api/users/${userId}`, { signal })\n * return response.json()\n * }\n * },\n * { limit: 5, window: 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 clear out the execution times or reset the rate limiter.\n */\n abort = (): void => {\n this.asyncRetryers.forEach((retryer) => retryer.abort())\n this.asyncRetryers.clear()\n this.#setState({\n isExecuting: false,\n })\n }\n\n /**\n * Resets the rate limiter state\n */\n reset = (): void => {\n this.#setState(getDefaultAsyncRateLimiterState())\n this.#clearTimeouts()\n this.asyncRetryers.forEach((retryer) => retryer.reset())\n }\n}\n\n/**\n * Creates an async rate-limited function that will execute the provided function up to a maximum number of times within a time window.\n *\n * Async vs Sync Versions:\n * The async version provides advanced features over the sync rate limit function:\n * - Returns promises that can be awaited for rate-limited function results\n * - Built-in retry support via AsyncRetryer integration\n * - Abort support to cancel in-flight executions\n * - Comprehensive error handling with onError callbacks and throwOnError control\n * - Detailed execution tracking (success/error/settle counts, rejection counts)\n * - More sophisticated window management with automatic cleanup\n *\n * The sync rate limit function is lighter weight and simpler when you don't need async features,\n * return values, or execution control.\n *\n * What is Rate Limiting?\n * Rate limiting allows a function to execute up to a limit within a time window,\n * then blocks all subsequent calls until the window passes. This can lead to \"bursty\" behavior where\n * all executions happen immediately, followed by a complete block.\n *\n * Window Types:\n * - 'fixed': A strict window that resets after the window period. All executions within the window count\n * towards the limit, and the window resets completely after the period.\n * - 'sliding': A rolling window that allows executions as old ones expire. This provides a more\n * consistent rate of execution over time.\n *\n * Configuration Options:\n * - `limit`: Maximum number of executions allowed within the window (required)\n * - `window`: Time window in milliseconds (required)\n * - `windowType`: 'fixed' or 'sliding' (default: 'fixed')\n * - `enabled`: Whether the rate limiter is enabled (default: true)\n * - `asyncRetryerOptions`: Configure retry behavior for executions\n *\n * When to Use Rate Limiting:\n * Rate limiting is best used for hard API limits or resource constraints. For UI updates or\n * smoothing out frequent events, throttling or debouncing usually provide better user experience.\n * - A rate limiter will allow all executions until the limit is reached, then block all subsequent calls until the window resets\n * - A throttler ensures even spacing between executions, which can be better for consistent performance\n * - A debouncer collapses multiple calls into one, which is better for handling bursts of events\n *\n * Error Handling:\n * - If an `onError` handler is provided, it will be called with the error and rate limiter 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 AsyncRateLimiter instance\n * - Rate limit rejections (when limit is exceeded) are handled separately from execution errors via the `onReject` handler\n *\n * State Management:\n * - Uses TanStack Store for reactive state management\n * - Use `initialState` to provide initial state values when creating the rate limiter\n * - `initialState` can be a partial state object\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 * - Use `onReject` callback to react to executions being rejected when rate limit is exceeded\n * - The state includes execution times, success/error counts, and current execution status\n * - State can be accessed via the underlying AsyncRateLimiter 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 * // Rate limit to 5 calls per minute with a sliding window\n * const rateLimited = asyncRateLimit(makeApiCall, {\n * limit: 5,\n * window: 60000,\n * windowType: 'sliding',\n * onError: (error) => {\n * console.error('API call failed:', error);\n * },\n * onReject: (rateLimiter) => {\n * console.log(`Rate limit exceeded. Try again in ${rateLimiter.getMsUntilNextWindow()}ms`);\n * }\n * });\n *\n * // First 5 calls will execute immediately\n * // Additional calls will be rejected until the minute window resets\n * // Returns the API response directly\n * const result = await rateLimited();\n *\n * // For more even execution, consider using throttle instead:\n * const throttled = throttle(makeApiCall, { wait: 12000 }); // One call every 12 seconds\n * ```\n */\nexport function asyncRateLimit<TFn extends AnyAsyncFunction>(\n fn: TFn,\n initialOptions: AsyncRateLimiterOptions<TFn>,\n) {\n const rateLimiter = new AsyncRateLimiter(fn, initialOptions)\n return rateLimiter.maybeExecute\n}\n"],"mappings":"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"}
{"version":3,"file":"async-rate-limiter.js","names":["#setState","#cleanupOldExecutions","#getExecutionTimesInWindow","#getLimit","#execute","#getWindow","#clearTimeouts","#setCleanupTimeout","#getEnabled","#timeoutIds","#clearTimeout"],"sources":["../src/async-rate-limiter.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 } from './types'\n\nexport interface AsyncRateLimiterState<TFn extends AnyAsyncFunction> {\n /**\n * Number of function executions that have resulted in errors\n */\n errorCount: number\n /**\n * Array of timestamps when executions occurred for rate limiting calculations\n */\n executionTimes: Array<number>\n /**\n * Whether the rate limiter has exceeded the limit\n */\n isExceeded: boolean\n /**\n * Whether the rate-limited function is currently executing asynchronously\n */\n isExecuting: boolean\n /**\n * The result from the most recent successful function execution\n */\n lastResult: ReturnType<TFn> | undefined\n /**\n * Number of function executions that have been rejected due to rate limiting\n */\n rejectionCount: number\n /**\n * Number of function executions that have completed (either successfully or with errors)\n */\n settleCount: number\n /**\n * Current execution status - 'disabled' when not active, 'executing' when executing, 'idle' when not executing, 'exceeded' when rate limit is exceeded\n */\n status: 'disabled' | 'executing' | 'exceeded' | 'idle'\n /**\n * Number of function executions that have completed successfully\n */\n successCount: number\n /**\n * Number of times maybeExecute has been called (for reduction calculations)\n */\n maybeExecuteCount: number\n}\n\nfunction getDefaultAsyncRateLimiterState<\n TFn extends AnyAsyncFunction,\n>(): AsyncRateLimiterState<TFn> {\n return {\n errorCount: 0,\n executionTimes: [],\n isExceeded: false,\n isExecuting: false,\n lastResult: undefined,\n maybeExecuteCount: 0,\n rejectionCount: 0,\n settleCount: 0,\n status: 'idle',\n successCount: 0,\n }\n}\n\n/**\n * Options for configuring an async rate-limited function\n */\nexport interface AsyncRateLimiterOptions<TFn extends AnyAsyncFunction> {\n /**\n * Options for configuring the underlying async retryer\n */\n asyncRetryerOptions?: AsyncRetryerOptions<TFn>\n /**\n * Whether the rate limiter 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 | ((rateLimiter: AsyncRateLimiter<TFn>) => boolean)\n /**\n * Initial state for the rate limiter\n */\n initialState?: Partial<AsyncRateLimiterState<TFn>>\n /**\n * Optional key to identify this async rate limiter instance.\n * If provided, the async rate limiter will be identified by this key in the devtools and PacerProvider if applicable.\n */\n key?: string\n /**\n * Maximum number of executions allowed within the time window.\n * Can be a number or a function that returns a number.\n */\n limit: number | ((rateLimiter: AsyncRateLimiter<TFn>) => number)\n /**\n * Optional error handler for when the rate-limited function throws.\n * If provided, the handler will be called with the error and rate limiter 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 rateLimiter: AsyncRateLimiter<TFn>,\n ) => void\n /**\n * Optional callback function that is called when an execution is rejected due to rate limiting\n */\n onReject?: (args: Parameters<TFn>, rateLimiter: AsyncRateLimiter<TFn>) => void\n /**\n * Optional function to call when the rate-limited function is executed\n */\n onSettled?: (\n args: Parameters<TFn>,\n rateLimiter: AsyncRateLimiter<TFn>,\n ) => void\n /**\n * Optional function to call when the rate-limited function is executed\n */\n onSuccess?: (\n result: ReturnType<TFn>,\n args: Parameters<TFn>,\n rateLimiter: AsyncRateLimiter<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 * Time window in milliseconds within which the limit applies.\n * Can be a number or a function that returns a number.\n */\n window: number | ((rateLimiter: AsyncRateLimiter<TFn>) => number)\n /**\n * Type of window to use for rate limiting\n * - 'fixed': Uses a fixed window that resets after the window period\n * - 'sliding': Uses a sliding window that allows executions as old ones expire\n * Defaults to 'fixed'\n */\n windowType?: 'fixed' | 'sliding'\n}\n\n/**\n * Utility function for sharing common `AsyncRateLimiterOptions` options between different `AsyncRateLimiter` instances.\n */\nexport function asyncRateLimiterOptions<\n TFn extends AnyAsyncFunction = AnyAsyncFunction,\n TOptions extends Partial<AsyncRateLimiterOptions<TFn>> = Partial<\n AsyncRateLimiterOptions<TFn>\n >,\n>(options: TOptions): TOptions {\n return options\n}\n\nconst defaultOptions: Omit<\n Required<AsyncRateLimiterOptions<any>>,\n 'initialState' | 'onError' | 'onReject' | 'onSettled' | 'onSuccess' | 'key'\n> = {\n asyncRetryerOptions: {\n maxAttempts: 1,\n },\n enabled: true,\n limit: 1,\n window: 0,\n windowType: 'fixed',\n throwOnError: true,\n}\n\n/**\n * A class that creates an async rate-limited function.\n *\n * Async vs Sync Versions:\n * The async version provides advanced features over the sync RateLimiter:\n * - Returns promises that can be awaited for rate-limited function results\n * - Built-in retry support via AsyncRetryer integration\n * - Abort support to cancel in-flight executions\n * - Comprehensive error handling with onError callbacks and throwOnError control\n * - Detailed execution tracking (success/error/settle counts, rejection counts)\n * - More sophisticated window management with automatic cleanup\n *\n * The sync RateLimiter is lighter weight and simpler when you don't need async features,\n * return values, or execution control.\n *\n * What is Rate Limiting?\n * Rate limiting allows a function to execute up to a limit within a time window,\n * then blocks all subsequent calls until the window passes. This can lead to \"bursty\" behavior where\n * all executions happen immediately, followed by a complete block.\n *\n * Window Types:\n * - 'fixed': A strict window that resets after the window period. All executions within the window count\n * towards the limit, and the window resets completely after the period.\n * - 'sliding': A rolling window that allows executions as old ones expire. This provides a more\n * consistent rate of execution over time.\n *\n * When to Use Rate Limiting:\n * Rate limiting is best used for hard API limits or resource constraints. For UI updates or\n * smoothing out frequent events, throttling or debouncing usually provide better user experience.\n * - Throttling: Ensures consistent spacing between executions (e.g. max once per 200ms)\n * - Debouncing: Waits for a pause in calls before executing (e.g. after 500ms of no calls)\n *\n * State Management:\n * - Uses TanStack Store for reactive state management\n * - Use `initialState` to provide initial state values when creating the rate limiter\n * - `initialState` can be a partial state object\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 * - Use `onReject` callback to react to executions being rejected when rate limit is exceeded\n * - The state includes execution times, success/error counts, and current execution status\n * - State can be accessed via `asyncRateLimiter.store.state` when using the class directly\n * - When using framework adapters (React/Solid), state is accessed from `asyncRateLimiter.state`\n *\n * Error Handling:\n * - If an `onError` handler is provided, it will be called with the error and rate limiter 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 AsyncRateLimiter instance\n * - Rate limit rejections (when limit is exceeded) are handled separately from execution errors via the `onReject` handler\n *\n * @example\n * ```ts\n * const rateLimiter = new AsyncRateLimiter(\n * async (id: string) => await api.getData(id),\n * {\n * limit: 5,\n * window: 1000,\n * windowType: 'sliding',\n * onError: (error) => {\n * console.error('API call failed:', error);\n * },\n * onReject: (limiter) => {\n * console.log(`Rate limit exceeded. Try again in ${limiter.getMsUntilNextWindow()}ms`);\n * }\n * }\n * );\n *\n * // Will execute immediately until limit reached, then block\n * // Returns the API response directly\n * const data = await rateLimiter.maybeExecute('123');\n * ```\n */\nexport class AsyncRateLimiter<TFn extends AnyAsyncFunction> {\n readonly store: Store<Readonly<AsyncRateLimiterState<TFn>>> = new Store<\n AsyncRateLimiterState<TFn>\n >(getDefaultAsyncRateLimiterState<TFn>())\n key: string | undefined\n options: AsyncRateLimiterOptions<TFn>\n asyncRetryers = new Map<number, AsyncRetryer<TFn>>()\n #timeoutIds: Set<ReturnType<typeof setTimeout>> = new Set()\n\n constructor(\n public fn: TFn,\n initialOptions: AsyncRateLimiterOptions<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 for (const executionTime of this.#getExecutionTimesInWindow()) {\n this.#setCleanupTimeout(executionTime)\n }\n\n if (this.key) {\n pacerEventClient.on('d-AsyncRateLimiter', (event) => {\n if (event.payload.key !== this.key) return\n this.#setState(\n event.payload.store.state as Partial<AsyncRateLimiterState<TFn>>,\n )\n this.setOptions(\n event.payload.options as Partial<AsyncRateLimiterOptions<TFn>>,\n )\n })\n }\n }\n\n /**\n * Updates the async rate limiter options\n */\n setOptions = (newOptions: Partial<AsyncRateLimiterOptions<TFn>>): void => {\n this.options = { ...this.options, ...newOptions }\n }\n\n #setState = (newState: Partial<AsyncRateLimiterState<TFn>>): void => {\n this.store.setState((state) => {\n const combinedState = {\n ...state,\n ...newState,\n }\n const isExceeded = combinedState.executionTimes.length >= this.#getLimit()\n const status = !this.#getEnabled()\n ? 'disabled'\n : combinedState.isExecuting\n ? 'executing'\n : isExceeded\n ? 'exceeded'\n : 'idle'\n return {\n ...combinedState,\n isExceeded,\n status,\n }\n })\n emitChange('AsyncRateLimiter', this)\n }\n\n /**\n * Returns the current enabled state of the async rate limiter\n */\n #getEnabled = (): boolean => {\n return !!parseFunctionOrValue(this.options.enabled, this)\n }\n\n /**\n * Returns the current limit of executions allowed within the time window\n */\n #getLimit = (): number => {\n return parseFunctionOrValue(this.options.limit, this)\n }\n\n /**\n * Returns the current time window in milliseconds\n */\n #getWindow = (): number => {\n return parseFunctionOrValue(this.options.window, this)\n }\n\n /**\n * Attempts to execute the rate-limited function if within the configured limits.\n * Will reject execution if the number of calls in the current window exceeds the limit.\n *\n * Error Handling:\n * - If the rate-limited function throws and no `onError` handler is configured,\n * the error will be thrown from this method.\n * - If an `onError` handler is configured, errors will be caught and passed to the handler,\n * and this method will return undefined.\n * - The error state can be checked using `getErrorCount()` and `getIsExecuting()`.\n *\n * @returns A promise that resolves with the function's return value, or undefined if an error occurred and was handled by onError\n * @throws The error from the rate-limited function if no onError handler is configured\n *\n * @example\n * ```ts\n * const rateLimiter = new AsyncRateLimiter(fn, { limit: 5, window: 1000 });\n *\n * // First 5 calls will return a promise that resolves with the result\n * const result = await rateLimiter.maybeExecute('arg1', 'arg2');\n *\n * // Additional calls within the window will return undefined\n * const result2 = await rateLimiter.maybeExecute('arg1', 'arg2'); // undefined\n * ```\n */\n maybeExecute = async (\n ...args: Parameters<TFn>\n ): Promise<ReturnType<TFn> | undefined> => {\n this.#setState({\n maybeExecuteCount: this.store.state.maybeExecuteCount + 1,\n })\n\n this.#cleanupOldExecutions()\n\n const relevantExecutionTimes = this.#getExecutionTimesInWindow()\n\n if (relevantExecutionTimes.length < this.#getLimit()) {\n await this.#execute(...args)\n return this.store.state.lastResult\n }\n\n this.#setState({\n rejectionCount: this.store.state.rejectionCount + 1,\n })\n this.options.onReject?.(args, this)\n return undefined\n }\n\n #execute = async (\n ...args: Parameters<TFn>\n ): Promise<ReturnType<TFn> | undefined> => {\n if (!this.#getEnabled()) return\n\n const currentMaybeExecute = this.store.state.maybeExecuteCount\n const now = Date.now()\n const executionTimes = [...this.store.state.executionTimes, now]\n this.#setState({\n isExecuting: true,\n executionTimes,\n })\n\n try {\n // Create a new AsyncRetryer for this execution to avoid cancelling concurrent executions\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.#setCleanupTimeout(now)\n this.#setState({\n successCount: this.store.state.successCount + 1,\n lastResult: result,\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 this.#setState({\n isExecuting: false,\n settleCount: this.store.state.settleCount + 1,\n })\n this.options.onSettled?.(args, this)\n }\n\n return this.store.state.lastResult\n }\n\n #getExecutionTimesInWindow = (): Array<number> => {\n if (this.options.windowType === 'sliding') {\n // For sliding window, return all executions within the current window\n return this.store.state.executionTimes.filter(\n (time) => time > Date.now() - this.#getWindow(),\n )\n } else {\n // For fixed window, return all executions in the current window\n // The window starts from the oldest execution time\n if (this.store.state.executionTimes.length === 0) {\n return []\n }\n const oldestExecution = Math.min(...this.store.state.executionTimes)\n const windowStart = oldestExecution\n const windowEnd = windowStart + this.#getWindow()\n const now = Date.now()\n\n // If the window has expired, return empty array\n if (now > windowEnd) {\n return []\n }\n\n // Otherwise, return all executions in the current window\n return this.store.state.executionTimes.filter(\n (time) => time >= windowStart && time <= windowEnd,\n )\n }\n }\n\n #setCleanupTimeout = (executionTime: number): void => {\n if (\n this.options.windowType === 'sliding' ||\n this.#timeoutIds.size === 0 // new fixed window\n ) {\n const now = Date.now()\n const timeUntilExpiration = executionTime - now + this.#getWindow() + 1\n const timeoutId = setTimeout(() => {\n this.#cleanupOldExecutions()\n this.#clearTimeout(timeoutId)\n }, timeUntilExpiration)\n this.#timeoutIds.add(timeoutId)\n }\n }\n\n #clearTimeout = (timeoutId: ReturnType<typeof setTimeout>): void => {\n clearTimeout(timeoutId)\n this.#timeoutIds.delete(timeoutId)\n }\n\n #clearTimeouts = (): void => {\n this.#timeoutIds.forEach((timeoutId) => clearTimeout(timeoutId))\n this.#timeoutIds.clear()\n }\n\n #cleanupOldExecutions = (): void => {\n this.#setState({\n executionTimes: this.#getExecutionTimesInWindow(),\n })\n }\n\n /**\n * Returns the number of remaining executions allowed in the current window\n */\n getRemainingInWindow = (): number => {\n const relevantExecutionTimes = this.#getExecutionTimesInWindow()\n return Math.max(0, this.#getLimit() - relevantExecutionTimes.length)\n }\n\n /**\n * Returns the number of milliseconds until the next execution will be possible\n * For fixed windows, this is the time until the current window resets\n * For sliding windows, this is the time until the oldest execution expires\n */\n getMsUntilNextWindow = (): number => {\n if (this.getRemainingInWindow() > 0) {\n return 0\n }\n const oldestExecution = this.store.state.executionTimes[0] ?? Infinity\n return oldestExecution + this.#getWindow() - Date.now()\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 rateLimiter = new AsyncRateLimiter(\n * async (userId: string) => {\n * const signal = rateLimiter.getAbortSignal()\n * if (signal) {\n * const response = await fetch(`/api/users/${userId}`, { signal })\n * return response.json()\n * }\n * },\n * { limit: 5, window: 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 clear out the execution times or reset the rate limiter.\n */\n abort = (): void => {\n this.asyncRetryers.forEach((retryer) => retryer.abort())\n this.asyncRetryers.clear()\n this.#setState({\n isExecuting: false,\n })\n }\n\n /**\n * Resets the rate limiter state\n */\n reset = (): void => {\n this.#setState(getDefaultAsyncRateLimiterState())\n this.#clearTimeouts()\n this.asyncRetryers.forEach((retryer) => retryer.reset())\n }\n}\n\n/**\n * Creates an async rate-limited function that will execute the provided function up to a maximum number of times within a time window.\n *\n * Async vs Sync Versions:\n * The async version provides advanced features over the sync rate limit function:\n * - Returns promises that can be awaited for rate-limited function results\n * - Built-in retry support via AsyncRetryer integration\n * - Abort support to cancel in-flight executions\n * - Comprehensive error handling with onError callbacks and throwOnError control\n * - Detailed execution tracking (success/error/settle counts, rejection counts)\n * - More sophisticated window management with automatic cleanup\n *\n * The sync rate limit function is lighter weight and simpler when you don't need async features,\n * return values, or execution control.\n *\n * What is Rate Limiting?\n * Rate limiting allows a function to execute up to a limit within a time window,\n * then blocks all subsequent calls until the window passes. This can lead to \"bursty\" behavior where\n * all executions happen immediately, followed by a complete block.\n *\n * Window Types:\n * - 'fixed': A strict window that resets after the window period. All executions within the window count\n * towards the limit, and the window resets completely after the period.\n * - 'sliding': A rolling window that allows executions as old ones expire. This provides a more\n * consistent rate of execution over time.\n *\n * Configuration Options:\n * - `limit`: Maximum number of executions allowed within the window (required)\n * - `window`: Time window in milliseconds (required)\n * - `windowType`: 'fixed' or 'sliding' (default: 'fixed')\n * - `enabled`: Whether the rate limiter is enabled (default: true)\n * - `asyncRetryerOptions`: Configure retry behavior for executions\n *\n * When to Use Rate Limiting:\n * Rate limiting is best used for hard API limits or resource constraints. For UI updates or\n * smoothing out frequent events, throttling or debouncing usually provide better user experience.\n * - A rate limiter will allow all executions until the limit is reached, then block all subsequent calls until the window resets\n * - A throttler ensures even spacing between executions, which can be better for consistent performance\n * - A debouncer collapses multiple calls into one, which is better for handling bursts of events\n *\n * Error Handling:\n * - If an `onError` handler is provided, it will be called with the error and rate limiter 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 AsyncRateLimiter instance\n * - Rate limit rejections (when limit is exceeded) are handled separately from execution errors via the `onReject` handler\n *\n * State Management:\n * - Uses TanStack Store for reactive state management\n * - Use `initialState` to provide initial state values when creating the rate limiter\n * - `initialState` can be a partial state object\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 * - Use `onReject` callback to react to executions being rejected when rate limit is exceeded\n * - The state includes execution times, success/error counts, and current execution status\n * - State can be accessed via the underlying AsyncRateLimiter 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 * // Rate limit to 5 calls per minute with a sliding window\n * const rateLimited = asyncRateLimit(makeApiCall, {\n * limit: 5,\n * window: 60000,\n * windowType: 'sliding',\n * onError: (error) => {\n * console.error('API call failed:', error);\n * },\n * onReject: (rateLimiter) => {\n * console.log(`Rate limit exceeded. Try again in ${rateLimiter.getMsUntilNextWindow()}ms`);\n * }\n * });\n *\n * // First 5 calls will execute immediately\n * // Additional calls will be rejected until the minute window resets\n * // Returns the API response directly\n * const result = await rateLimited();\n *\n * // For more even execution, consider using throttle instead:\n * const throttled = throttle(makeApiCall, { wait: 12000 }); // One call every 12 seconds\n * ```\n */\nexport function asyncRateLimit<TFn extends AnyAsyncFunction>(\n fn: TFn,\n initialOptions: AsyncRateLimiterOptions<TFn>,\n) {\n const rateLimiter = new AsyncRateLimiter(fn, initialOptions)\n return rateLimiter.maybeExecute\n}\n"],"mappings":"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"}

@@ -1,1 +0,1 @@

{"version":3,"file":"async-retryer.cjs","names":["Store","#getEnabled","#abortController","#setState","#getMaxAttempts","#calculateWait","parseFunctionOrValue","#getBaseWait","#getMaxWait","#calculateJitter"],"sources":["../src/async-retryer.ts"],"sourcesContent":["import { Store } from '@tanstack/store'\nimport { parseFunctionOrValue } from './utils'\nimport { emitChange, pacerEventClient } from './event-client'\nimport type { AnyAsyncFunction } from './types'\n\nexport interface AsyncRetryerState<TFn extends AnyAsyncFunction> {\n /**\n * The current retry attempt number (0 when not executing)\n */\n currentAttempt: number\n /**\n * Total number of completed executions (successful or failed)\n */\n executionCount: number\n /**\n * Whether the retryer is currently executing the function\n */\n isExecuting: boolean\n /**\n * The most recent error encountered during execution\n */\n lastError: Error | undefined\n /**\n * Timestamp of the last execution completion in milliseconds\n */\n lastExecutionTime: number\n /**\n * The result from the most recent successful execution\n */\n lastResult: Awaited<ReturnType<TFn>> | undefined\n /**\n * Current execution status - 'disabled' when not enabled, 'idle' when ready, 'executing' when running\n */\n status: 'disabled' | 'idle' | 'executing' | 'retrying'\n /**\n * Total time spent executing (including retries) in milliseconds\n */\n totalExecutionTime: number\n}\n\n/**\n * Creates the default initial state for an AsyncRetryer instance\n * @returns The default state with all values reset to initial values\n */\nfunction getDefaultAsyncRetryerState<\n TFn extends AnyAsyncFunction,\n>(): AsyncRetryerState<TFn> {\n return {\n currentAttempt: 0,\n executionCount: 0,\n isExecuting: false,\n lastError: undefined,\n lastExecutionTime: 0,\n lastResult: undefined,\n status: 'idle',\n totalExecutionTime: 0,\n }\n}\n\nexport interface AsyncRetryerOptions<TFn extends AnyAsyncFunction> {\n /**\n * The backoff strategy for retry delays:\n * - 'exponential': Wait time doubles with each attempt (1s, 2s, 4s, ...)\n * - 'linear': Wait time increases linearly (1s, 2s, 3s, ...)\n * - 'fixed': Same wait time for all attempts\n * @default 'exponential'\n */\n backoff?: 'linear' | 'exponential' | 'fixed'\n /**\n * Base wait time in milliseconds between retries, or a function that returns the wait time\n * @default 1000\n */\n baseWait?: number | ((retryer: AsyncRetryer<TFn>) => number)\n /**\n * Whether the retryer is enabled, or a function that determines if it's enabled\n * @default true\n */\n enabled?: boolean | ((retryer: AsyncRetryer<TFn>) => boolean)\n /**\n * Initial state to merge with the default state\n */\n initialState?: Partial<AsyncRetryerState<TFn>>\n /**\n * Jitter percentage to add to retry delays (0-1). Adds randomness to prevent thundering herd.\n * @default 0\n */\n jitter?: number\n /**\n * Optional key to identify this async retryer instance.\n * If provided, the async retryer will be identified by this key in the devtools and PacerProvider if applicable.\n */\n key?: string\n /**\n * Maximum number of retry attempts, or a function that returns the max attempts\n * @default 3\n */\n maxAttempts?: number | ((retryer: AsyncRetryer<TFn>) => number)\n /**\n * Maximum execution time in milliseconds for a single function call before aborting\n * @default Infinity\n */\n maxExecutionTime?: number\n /**\n * Maximum total execution time in milliseconds for the entire retry operation before aborting\n * @default Infinity\n */\n maxTotalExecutionTime?: number\n /**\n * Maximum wait time in milliseconds to cap retry delays, or a function that returns the max wait time\n * @default Infinity\n */\n maxWait?: number | ((retryer: AsyncRetryer<TFn>) => number)\n /**\n * Callback invoked when the execution is aborted (manually or due to timeouts)\n */\n onAbort?: (\n reason: 'manual' | 'execution-timeout' | 'total-timeout' | 'new-execution',\n retryer: AsyncRetryer<TFn>,\n ) => void\n /**\n * Callback invoked when any error occurs during execution (including retries)\n */\n onError?: (\n error: Error,\n args: Parameters<TFn>,\n retryer: AsyncRetryer<TFn>,\n ) => void\n /**\n * Callback invoked when a single execution attempt times out (maxExecutionTime exceeded)\n */\n onExecutionTimeout?: (retryer: AsyncRetryer<TFn>) => void\n /**\n * Callback invoked when the final error occurs after all retries are exhausted\n */\n onLastError?: (error: Error, retryer: AsyncRetryer<TFn>) => void\n /**\n * Callback invoked before each retry attempt\n */\n onRetry?: (attempt: number, error: Error, retryer: AsyncRetryer<TFn>) => void\n /**\n * Callback invoked after execution completes (success or failure) of each attempt\n */\n onSettled?: (args: Parameters<TFn>, retryer: AsyncRetryer<TFn>) => void\n /**\n * Callback invoked when execution succeeds\n */\n onSuccess?: (\n result: Awaited<ReturnType<TFn>>,\n args: Parameters<TFn>,\n retryer: AsyncRetryer<TFn>,\n ) => void\n /**\n * Callback invoked when the total execution time times out (maxTotalExecutionTime exceeded)\n */\n onTotalExecutionTimeout?: (retryer: AsyncRetryer<TFn>) => void\n /**\n * Controls when errors are thrown:\n * - 'last': Only throw the final error after all retries are exhausted\n * - true: Throw every error immediately (disables retrying)\n * - false: Never throw errors, return undefined instead\n * @default 'last'\n */\n throwOnError?: boolean | 'last'\n}\n\n/**\n * Utility function for sharing common `AsyncRetryerOptions` options between different `AsyncRetryer` instances.\n */\nexport function asyncRetryerOptions<\n TFn extends AnyAsyncFunction = AnyAsyncFunction,\n TOptions extends Partial<AsyncRetryerOptions<TFn>> = Partial<\n AsyncRetryerOptions<TFn>\n >,\n>(options: TOptions): TOptions {\n return options\n}\n\nconst defaultOptions: Omit<\n Required<AsyncRetryerOptions<any>>,\n | 'initialState'\n | 'key'\n | 'onAbort'\n | 'onError'\n | 'onLastError'\n | 'onRetry'\n | 'onSettled'\n | 'onSuccess'\n | 'onExecutionTimeout'\n | 'onTotalExecutionTimeout'\n> = {\n backoff: 'exponential',\n baseWait: 1000,\n maxWait: Infinity,\n enabled: true,\n jitter: 0,\n maxAttempts: 3,\n maxExecutionTime: Infinity,\n maxTotalExecutionTime: Infinity,\n throwOnError: 'last',\n}\n\n/**\n * Provides robust retry functionality for asynchronous functions, supporting configurable backoff strategies,\n * attempt limits, timeout controls, and detailed state management. The AsyncRetryer class is designed to help you reliably\n * execute async operations that may fail intermittently, such as network requests or database operations,\n * by automatically retrying them according to your chosen policy.\n *\n * ## Retrying Concepts\n *\n * - **Retrying**: Automatically re-executes a failed async function up to a specified number of attempts.\n * Useful for handling transient errors (e.g., network flakiness, rate limits, temporary server issues).\n * - **Backoff Strategies**: Controls the delay between retry attempts (default: `'exponential'`):\n * - `'exponential'`: Wait time doubles with each attempt (1s, 2s, 4s, ...) - **DEFAULT**\n * - `'linear'`: Wait time increases linearly (1s, 2s, 3s, ...)\n * - `'fixed'`: Waits a constant amount of time (`baseWait`) between each attempt\n * - **Jitter**: Adds randomness to retry delays to prevent thundering herd problems (default: `0`).\n * Set to a value between 0-1 to apply that percentage of random variation to each delay.\n * - **Max Wait**: Caps the maximum wait time between retries (default: `Infinity`).\n * Useful for preventing exponential backoff from growing too large (e.g., cap at 30s even if exponential would be 64s).\n * - **Timeout Controls**: Set limits on execution time to prevent hanging operations:\n * - `maxExecutionTime`: Maximum time for a single function call (default: `Infinity`)\n * - `maxTotalExecutionTime`: Maximum time for the entire retry operation (default: `Infinity`)\n * - **Abort & Cancellation**: Supports cancellation via an internal `AbortController`. Call `abort()` to stop retries.\n * Use `getAbortSignal()` to make your async function actually cancellable (e.g., with fetch requests).\n *\n * ## State Management\n *\n * Uses TanStack Store for fine-grained reactivity. State can be accessed via the `store.state` property.\n *\n * Available state properties:\n * - `currentAttempt`: The current retry attempt number (0 when not executing)\n * - `executionCount`: Total number of completed executions (successful or failed)\n * - `isExecuting`: Whether the retryer is currently executing the function\n * - `lastError`: The most recent error encountered during execution\n * - `lastExecutionTime`: Timestamp of the last execution completion in milliseconds\n * - `lastResult`: The result from the most recent successful execution\n * - `status`: Current execution status ('disabled' | 'idle' | 'executing' | 'retrying')\n * - `totalExecutionTime`: Total time spent executing (including retries) in milliseconds\n *\n * ## Error Handling\n *\n * The `throwOnError` option controls when errors are thrown (default: `'last'`):\n * - `'last'`: Only throws the final error after all retries are exhausted - **DEFAULT**\n * - `true`: Throws every error immediately (disables retrying)\n * - `false`: Never throws errors, returns `undefined` instead\n *\n * Callbacks for lifecycle management:\n * - `onAbort`: Called when execution is aborted (manually or due to timeouts)\n * - `onError`: Called for every error (including during retries)\n * - `onLastError`: Called only for the final error after all retries fail\n * - `onRetry`: Called before each retry attempt\n * - `onSettled`: Called after execution completes (success or failure) of each attempt\n * - `onSuccess`: Called when execution succeeds\n * - `onExecutionTimeout`: Called when a single execution attempt times out\n * - `onTotalExecutionTimeout`: Called when the total execution time times out\n *\n * ## Usage\n *\n * - Use for async operations that may fail transiently and benefit from retrying.\n * - Configure `maxAttempts`, `backoff`, `baseWait`, `maxWait`, and `jitter` to control retry behavior.\n * - Set `maxExecutionTime` and `maxTotalExecutionTime` to prevent hanging operations.\n * - Use `onAbort`, `onError`, `onLastError`, `onRetry`, `onSettled`, `onSuccess`, `onExecutionTimeout`, and `onTotalExecutionTimeout` for custom side effects.\n * - Call `abort()` to cancel ongoing execution and pending retries.\n * - Call `reset()` to reset state and cancel execution.\n * - Use `getAbortSignal()` to make your async function cancellable.\n * - Use dynamic options (functions) for `maxAttempts`, `baseWait`, and `enabled` based on retryer state.\n *\n * **Important:** This class is designed for single-use execution. Calling `execute()` multiple times\n * on the same instance will abort previous executions. For multiple calls, create a new instance\n * each time.\n *\n * @example\n * ```typescript\n * // Retry a fetch operation up to 5 times with exponential backoff, jitter, and timeouts\n * const retryer = new AsyncRetryer(async (url: string) => {\n * const signal = retryer.getAbortSignal()\n * return await fetch(url, { signal })\n * }, {\n * maxAttempts: 5,\n * backoff: 'exponential',\n * baseWait: 1000,\n * jitter: 0.1, // Add 10% random variation to prevent thundering herd\n * maxExecutionTime: 5000, // Abort individual calls after 5 seconds\n * maxTotalExecutionTime: 30000, // Abort entire operation after 30 seconds\n * onRetry: (attempt, error) => console.log(`Retry attempt ${attempt} after error:`, error),\n * onSuccess: (result) => console.log('Success:', result),\n * onError: (error) => console.error('Error:', error),\n * onLastError: (error) => console.error('All retries failed:', error),\n * })\n *\n * const result = await retryer.execute('/api/data')\n * ```\n *\n * @template TFn The async function type to be retried.\n */\nexport class AsyncRetryer<TFn extends AnyAsyncFunction> {\n readonly store: Store<Readonly<AsyncRetryerState<TFn>>> = new Store(\n getDefaultAsyncRetryerState<TFn>(),\n )\n key: string | undefined\n options: AsyncRetryerOptions<TFn> & typeof defaultOptions\n #abortController: AbortController | null = null\n\n /**\n * Creates a new AsyncRetryer instance\n * @param fn The async function to retry\n * @param initialOptions Configuration options for the retryer\n */\n constructor(\n public fn: TFn,\n initialOptions: AsyncRetryerOptions<TFn> = {},\n ) {\n this.key = initialOptions.key\n this.options = {\n ...defaultOptions,\n ...initialOptions,\n throwOnError:\n initialOptions.throwOnError ??\n (initialOptions.onError ? false : defaultOptions.throwOnError),\n }\n this.#setState(this.options.initialState ?? {})\n\n if (this.key) {\n pacerEventClient.on('d-AsyncRetryer', (event) => {\n if (event.payload.key !== this.key) return\n this.#setState(\n event.payload.store.state as Partial<AsyncRetryerState<TFn>>,\n )\n this.setOptions(\n event.payload.options as Partial<AsyncRetryerOptions<TFn>>,\n )\n })\n }\n }\n\n /**\n * Updates the retryer options\n * @param newOptions Partial options to merge with existing options\n */\n setOptions = (newOptions: Partial<AsyncRetryerOptions<TFn>>): void => {\n this.options = { ...this.options, ...newOptions }\n }\n\n #setState = (newState: Partial<AsyncRetryerState<TFn>>): void => {\n this.store.setState((state) => {\n const combinedState = {\n ...state,\n ...newState,\n }\n const { isExecuting, currentAttempt } = combinedState\n return {\n ...combinedState,\n status: !this.#getEnabled()\n ? 'disabled'\n : isExecuting && currentAttempt === 1\n ? 'executing'\n : isExecuting && currentAttempt > 1\n ? 'retrying'\n : 'idle',\n }\n })\n emitChange('AsyncRetryer', this)\n }\n\n #getEnabled = (): boolean => {\n return !!parseFunctionOrValue(this.options.enabled, this)\n }\n\n #getMaxAttempts = (): number => {\n return parseFunctionOrValue(this.options.maxAttempts, this)\n }\n\n #getBaseWait = (): number => {\n return parseFunctionOrValue(this.options.baseWait, this)\n }\n\n #getMaxWait = (): number => {\n return parseFunctionOrValue(this.options.maxWait, this)\n }\n\n #calculateJitter = (waitTime: number): number => {\n const jitterAmount = this.options.jitter\n if (jitterAmount <= 0) return 0\n\n try {\n const crypto =\n typeof globalThis !== 'undefined' ? globalThis.crypto : undefined\n if (crypto?.getRandomValues) {\n const array = new Uint32Array(1)\n crypto.getRandomValues(array)\n // Convert to 0-1 range and apply jitter percentage\n const randomFactor = (array[0]! / 0xffffffff) * 2 - 1 // -1 to 1\n return Math.floor(waitTime * jitterAmount * randomFactor)\n }\n } catch {\n // No crypto available\n }\n return 0\n }\n\n #calculateWait = (attempt: number): number => {\n const baseWait = this.#getBaseWait()\n let waitTime: number\n\n switch (this.options.backoff) {\n case 'linear':\n waitTime = baseWait * attempt\n break\n case 'exponential':\n waitTime = baseWait * Math.pow(2, attempt - 1)\n break\n case 'fixed':\n default:\n waitTime = baseWait\n break\n }\n\n waitTime = Math.min(waitTime, this.#getMaxWait())\n\n const jitter = this.#calculateJitter(waitTime)\n return Math.max(0, waitTime + jitter)\n }\n\n /**\n * Executes the function with retry logic\n * @param args Arguments to pass to the function\n * @returns The function result, or undefined if disabled or all retries failed (when throwOnError is false)\n * @throws The last error if throwOnError is true and all retries fail\n */\n execute = async (\n ...args: Parameters<TFn>\n ): Promise<Awaited<ReturnType<TFn>> | undefined> => {\n if (!this.#getEnabled()) {\n return undefined\n }\n\n // Cancel any existing execution\n this.abort('new-execution')\n\n const startTime = Date.now()\n let lastError: Error | undefined\n let result: Awaited<ReturnType<TFn>> | undefined\n\n this.#abortController = new AbortController()\n const signal = this.#abortController.signal\n\n this.#setState({\n isExecuting: true,\n currentAttempt: 0,\n lastError: undefined,\n })\n\n // Set up total execution timeout\n let totalTimeoutId: NodeJS.Timeout | undefined\n if (this.options.maxTotalExecutionTime !== Infinity) {\n totalTimeoutId = setTimeout(() => {\n this.options.onTotalExecutionTimeout?.(this)\n this.abort('total-timeout')\n }, this.options.maxTotalExecutionTime)\n }\n\n let isLastAttempt = false\n for (let attempt = 1; attempt <= this.#getMaxAttempts(); attempt++) {\n isLastAttempt = attempt === this.#getMaxAttempts()\n this.#setState({ currentAttempt: attempt })\n\n try {\n if (signal.aborted) {\n return undefined\n }\n\n // Check if total execution time has been exceeded\n const currentTotalTime = Date.now() - startTime\n if (\n this.options.maxTotalExecutionTime !== Infinity &&\n currentTotalTime >= this.options.maxTotalExecutionTime\n ) {\n this.options.onTotalExecutionTimeout?.(this)\n this.abort('total-timeout')\n return undefined\n }\n\n // Execute with individual timeout if specified\n if (this.options.maxExecutionTime === Infinity) {\n result = (await this.fn(...args)) as Awaited<ReturnType<TFn>>\n } else {\n result = (await Promise.race([\n this.fn(...args),\n new Promise<never>((_, reject) => {\n const timeout = setTimeout(() => {\n this.options.onExecutionTimeout?.(this)\n this.abort('execution-timeout')\n reject(\n new Error(\n `Execution timeout: ${this.options.maxExecutionTime}ms exceeded`,\n ),\n )\n }, this.options.maxExecutionTime)\n\n signal.addEventListener(\n 'abort',\n () => {\n clearTimeout(timeout)\n reject(new Error('Aborted'))\n },\n { once: true },\n )\n }),\n ])) as Awaited<ReturnType<TFn>>\n }\n\n // Check if cancelled during execution\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n if (signal.aborted) {\n return undefined\n }\n\n const totalTime = Date.now() - startTime\n this.#setState({\n executionCount: this.store.state.executionCount + 1,\n isExecuting: false,\n lastExecutionTime: Date.now(),\n totalExecutionTime: totalTime,\n currentAttempt: 0,\n lastResult: result,\n })\n\n this.options.onSuccess?.(result as Awaited<ReturnType<TFn>>, args, this)\n\n return result\n } catch (error) {\n // Treat abort as a non-error cancellation outcome\n if (\n error &&\n typeof error === 'object' &&\n 'name' in error &&\n (error as Error).name === 'AbortError'\n ) {\n return undefined\n }\n lastError = error instanceof Error ? error : new Error(String(error))\n this.#setState({ lastError })\n\n // Call onError for every error (including during retries)\n this.options.onError?.(lastError, args, this)\n\n if (attempt < this.#getMaxAttempts()) {\n this.options.onRetry?.(attempt, lastError, this)\n\n const wait = this.#calculateWait(attempt)\n if (wait > 0) {\n // Eagerly reflect retrying status during the wait window\n this.#setState({ isExecuting: true, currentAttempt: attempt + 1 })\n await new Promise<void>((resolve) => {\n const timeout = setTimeout(() => {\n signal.removeEventListener('abort', onAbort)\n resolve()\n }, wait)\n const onAbort = () => {\n clearTimeout(timeout)\n signal.removeEventListener('abort', onAbort)\n resolve()\n }\n signal.addEventListener('abort', onAbort)\n })\n if (signal.aborted) {\n return undefined\n }\n }\n }\n } finally {\n this.options.onSettled?.(args, this)\n }\n }\n\n // Clean up total timeout\n if (totalTimeoutId) {\n clearTimeout(totalTimeoutId)\n }\n\n // Exhausted retries - finalize state\n this.#setState({ isExecuting: false })\n this.options.onLastError?.(lastError as Error, this)\n this.options.onSettled?.(args, this)\n\n if (\n (this.options.throwOnError === 'last' && isLastAttempt) ||\n this.options.throwOnError === true\n ) {\n throw lastError\n }\n\n return undefined\n }\n\n /**\n * Returns the current AbortSignal for the executing operation.\n * Use this signal in your async function to make it cancellable.\n * Returns null when not currently executing.\n *\n * @example\n * ```typescript\n * const retryer = new AsyncRetryer(async (userId: string) => {\n * const signal = retryer.getAbortSignal()\n * if (signal) {\n * return fetch(`/api/users/${userId}`, { signal })\n * }\n * return fetch(`/api/users/${userId}`)\n * })\n *\n * // Abort will now actually cancel the fetch\n * retryer.abort()\n * ```\n */\n getAbortSignal = (): AbortSignal | null => {\n return this.#abortController?.signal ?? null\n }\n\n /**\n * Cancels the current execution and any pending retries\n * @param reason The reason for the abort (defaults to 'manual')\n */\n abort = (\n reason:\n | 'manual'\n | 'execution-timeout'\n | 'total-timeout'\n | 'new-execution' = 'manual',\n ): void => {\n if (this.#abortController) {\n this.#abortController.abort()\n this.#abortController = null\n this.#setState({\n isExecuting: false,\n })\n this.options.onAbort?.(reason, this)\n }\n }\n\n /**\n * Resets the retryer to its initial state\n */\n reset = (): void => {\n this.#setState(getDefaultAsyncRetryerState<TFn>())\n }\n}\n\n/**\n * Creates a retry-enabled version of an async function. This is a convenience wrapper\n * around the AsyncRetryer class that returns the execute method.\n *\n * @param fn The async function to add retry functionality to\n * @param initialOptions Configuration options for the retry behavior\n * @returns A new function that executes the original with retry logic\n *\n * @example\n * ```typescript\n * // Define your async function normally\n * async function fetchData(url: string) {\n * const response = await fetch(url)\n * if (!response.ok) throw new Error('Request failed')\n * return response.json()\n * }\n *\n * // Create retry-enabled function\n * const fetchWithRetry = asyncRetry(fetchData, {\n * maxAttempts: 3,\n * backoff: 'exponential',\n * baseWait: 1000,\n * jitter: 0.1\n * })\n *\n * // Call it multiple times\n * const data1 = await fetchWithRetry('/api/data1')\n * const data2 = await fetchWithRetry('/api/data2')\n * ```\n */\nexport function asyncRetry<TFn extends AnyAsyncFunction>(\n fn: TFn,\n initialOptions: AsyncRetryerOptions<TFn> = {},\n): (...args: Parameters<TFn>) => Promise<Awaited<ReturnType<TFn>> | undefined> {\n const retryer = new AsyncRetryer(fn, initialOptions)\n return retryer.execute\n}\n"],"mappings":"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"}
{"version":3,"file":"async-retryer.cjs","names":["Store","#getEnabled","#abortController","#setState","#getMaxAttempts","#calculateWait","parseFunctionOrValue","#getBaseWait","#getMaxWait","#calculateJitter"],"sources":["../src/async-retryer.ts"],"sourcesContent":["import { Store } from '@tanstack/store'\nimport { parseFunctionOrValue } from './utils'\nimport { emitChange, pacerEventClient } from './event-client'\nimport type { AnyAsyncFunction } from './types'\n\nexport interface AsyncRetryerState<TFn extends AnyAsyncFunction> {\n /**\n * The current retry attempt number (0 when not executing)\n */\n currentAttempt: number\n /**\n * Total number of completed executions (successful or failed)\n */\n executionCount: number\n /**\n * Whether the retryer is currently executing the function\n */\n isExecuting: boolean\n /**\n * The most recent error encountered during execution\n */\n lastError: Error | undefined\n /**\n * Timestamp of the last execution completion in milliseconds\n */\n lastExecutionTime: number\n /**\n * The result from the most recent successful execution\n */\n lastResult: Awaited<ReturnType<TFn>> | undefined\n /**\n * Current execution status - 'disabled' when not enabled, 'idle' when ready, 'executing' when running\n */\n status: 'disabled' | 'idle' | 'executing' | 'retrying'\n /**\n * Total time spent executing (including retries) in milliseconds\n */\n totalExecutionTime: number\n}\n\n/**\n * Creates the default initial state for an AsyncRetryer instance\n * @returns The default state with all values reset to initial values\n */\nfunction getDefaultAsyncRetryerState<\n TFn extends AnyAsyncFunction,\n>(): AsyncRetryerState<TFn> {\n return {\n currentAttempt: 0,\n executionCount: 0,\n isExecuting: false,\n lastError: undefined,\n lastExecutionTime: 0,\n lastResult: undefined,\n status: 'idle',\n totalExecutionTime: 0,\n }\n}\n\nexport interface AsyncRetryerOptions<TFn extends AnyAsyncFunction> {\n /**\n * The backoff strategy for retry delays:\n * - 'exponential': Wait time doubles with each attempt (1s, 2s, 4s, ...)\n * - 'linear': Wait time increases linearly (1s, 2s, 3s, ...)\n * - 'fixed': Same wait time for all attempts\n * @default 'exponential'\n */\n backoff?: 'linear' | 'exponential' | 'fixed'\n /**\n * Base wait time in milliseconds between retries, or a function that returns the wait time\n * @default 1000\n */\n baseWait?: number | ((retryer: AsyncRetryer<TFn>) => number)\n /**\n * Whether the retryer is enabled, or a function that determines if it's enabled\n * @default true\n */\n enabled?: boolean | ((retryer: AsyncRetryer<TFn>) => boolean)\n /**\n * Initial state to merge with the default state\n */\n initialState?: Partial<AsyncRetryerState<TFn>>\n /**\n * Jitter percentage to add to retry delays (0-1). Adds randomness to prevent thundering herd.\n * @default 0\n */\n jitter?: number\n /**\n * Optional key to identify this async retryer instance.\n * If provided, the async retryer will be identified by this key in the devtools and PacerProvider if applicable.\n */\n key?: string\n /**\n * Maximum number of retry attempts, or a function that returns the max attempts\n * @default 3\n */\n maxAttempts?: number | ((retryer: AsyncRetryer<TFn>) => number)\n /**\n * Maximum execution time in milliseconds for a single function call before aborting\n * @default Infinity\n */\n maxExecutionTime?: number\n /**\n * Maximum total execution time in milliseconds for the entire retry operation before aborting\n * @default Infinity\n */\n maxTotalExecutionTime?: number\n /**\n * Maximum wait time in milliseconds to cap retry delays, or a function that returns the max wait time\n * @default Infinity\n */\n maxWait?: number | ((retryer: AsyncRetryer<TFn>) => number)\n /**\n * Callback invoked when the execution is aborted (manually or due to timeouts)\n */\n onAbort?: (\n reason: 'manual' | 'execution-timeout' | 'total-timeout' | 'new-execution',\n retryer: AsyncRetryer<TFn>,\n ) => void\n /**\n * Callback invoked when any error occurs during execution (including retries)\n */\n onError?: (\n error: Error,\n args: Parameters<TFn>,\n retryer: AsyncRetryer<TFn>,\n ) => void\n /**\n * Callback invoked when a single execution attempt times out (maxExecutionTime exceeded)\n */\n onExecutionTimeout?: (retryer: AsyncRetryer<TFn>) => void\n /**\n * Callback invoked when the final error occurs after all retries are exhausted\n */\n onLastError?: (error: Error, retryer: AsyncRetryer<TFn>) => void\n /**\n * Callback invoked before each retry attempt\n */\n onRetry?: (attempt: number, error: Error, retryer: AsyncRetryer<TFn>) => void\n /**\n * Callback invoked after execution completes (success or failure) of each attempt\n */\n onSettled?: (args: Parameters<TFn>, retryer: AsyncRetryer<TFn>) => void\n /**\n * Callback invoked when execution succeeds\n */\n onSuccess?: (\n result: Awaited<ReturnType<TFn>>,\n args: Parameters<TFn>,\n retryer: AsyncRetryer<TFn>,\n ) => void\n /**\n * Callback invoked when the total execution time times out (maxTotalExecutionTime exceeded)\n */\n onTotalExecutionTimeout?: (retryer: AsyncRetryer<TFn>) => void\n /**\n * Controls when errors are thrown:\n * - 'last': Only throw the final error after all retries are exhausted\n * - true: Throw every error immediately (disables retrying)\n * - false: Never throw errors, return undefined instead\n * @default 'last'\n */\n throwOnError?: boolean | 'last'\n}\n\n/**\n * Utility function for sharing common `AsyncRetryerOptions` options between different `AsyncRetryer` instances.\n */\nexport function asyncRetryerOptions<\n TFn extends AnyAsyncFunction = AnyAsyncFunction,\n TOptions extends Partial<AsyncRetryerOptions<TFn>> = Partial<\n AsyncRetryerOptions<TFn>\n >,\n>(options: TOptions): TOptions {\n return options\n}\n\nconst defaultOptions: Omit<\n Required<AsyncRetryerOptions<any>>,\n | 'initialState'\n | 'key'\n | 'onAbort'\n | 'onError'\n | 'onLastError'\n | 'onRetry'\n | 'onSettled'\n | 'onSuccess'\n | 'onExecutionTimeout'\n | 'onTotalExecutionTimeout'\n> = {\n backoff: 'exponential',\n baseWait: 1000,\n maxWait: Infinity,\n enabled: true,\n jitter: 0,\n maxAttempts: 3,\n maxExecutionTime: Infinity,\n maxTotalExecutionTime: Infinity,\n throwOnError: 'last',\n}\n\n/**\n * Provides robust retry functionality for asynchronous functions, supporting configurable backoff strategies,\n * attempt limits, timeout controls, and detailed state management. The AsyncRetryer class is designed to help you reliably\n * execute async operations that may fail intermittently, such as network requests or database operations,\n * by automatically retrying them according to your chosen policy.\n *\n * ## Retrying Concepts\n *\n * - **Retrying**: Automatically re-executes a failed async function up to a specified number of attempts.\n * Useful for handling transient errors (e.g., network flakiness, rate limits, temporary server issues).\n * - **Backoff Strategies**: Controls the delay between retry attempts (default: `'exponential'`):\n * - `'exponential'`: Wait time doubles with each attempt (1s, 2s, 4s, ...) - **DEFAULT**\n * - `'linear'`: Wait time increases linearly (1s, 2s, 3s, ...)\n * - `'fixed'`: Waits a constant amount of time (`baseWait`) between each attempt\n * - **Jitter**: Adds randomness to retry delays to prevent thundering herd problems (default: `0`).\n * Set to a value between 0-1 to apply that percentage of random variation to each delay.\n * - **Max Wait**: Caps the maximum wait time between retries (default: `Infinity`).\n * Useful for preventing exponential backoff from growing too large (e.g., cap at 30s even if exponential would be 64s).\n * - **Timeout Controls**: Set limits on execution time to prevent hanging operations:\n * - `maxExecutionTime`: Maximum time for a single function call (default: `Infinity`)\n * - `maxTotalExecutionTime`: Maximum time for the entire retry operation (default: `Infinity`)\n * - **Abort & Cancellation**: Supports cancellation via an internal `AbortController`. Call `abort()` to stop retries.\n * Use `getAbortSignal()` to make your async function actually cancellable (e.g., with fetch requests).\n *\n * ## State Management\n *\n * Uses TanStack Store for fine-grained reactivity. State can be accessed via the `store.state` property.\n *\n * Available state properties:\n * - `currentAttempt`: The current retry attempt number (0 when not executing)\n * - `executionCount`: Total number of completed executions (successful or failed)\n * - `isExecuting`: Whether the retryer is currently executing the function\n * - `lastError`: The most recent error encountered during execution\n * - `lastExecutionTime`: Timestamp of the last execution completion in milliseconds\n * - `lastResult`: The result from the most recent successful execution\n * - `status`: Current execution status ('disabled' | 'idle' | 'executing' | 'retrying')\n * - `totalExecutionTime`: Total time spent executing (including retries) in milliseconds\n *\n * ## Error Handling\n *\n * The `throwOnError` option controls when errors are thrown (default: `'last'`):\n * - `'last'`: Only throws the final error after all retries are exhausted - **DEFAULT**\n * - `true`: Throws every error immediately (disables retrying)\n * - `false`: Never throws errors, returns `undefined` instead\n *\n * Callbacks for lifecycle management:\n * - `onAbort`: Called when execution is aborted (manually or due to timeouts)\n * - `onError`: Called for every error (including during retries)\n * - `onLastError`: Called only for the final error after all retries fail\n * - `onRetry`: Called before each retry attempt\n * - `onSettled`: Called after execution completes (success or failure) of each attempt\n * - `onSuccess`: Called when execution succeeds\n * - `onExecutionTimeout`: Called when a single execution attempt times out\n * - `onTotalExecutionTimeout`: Called when the total execution time times out\n *\n * ## Usage\n *\n * - Use for async operations that may fail transiently and benefit from retrying.\n * - Configure `maxAttempts`, `backoff`, `baseWait`, `maxWait`, and `jitter` to control retry behavior.\n * - Set `maxExecutionTime` and `maxTotalExecutionTime` to prevent hanging operations.\n * - Use `onAbort`, `onError`, `onLastError`, `onRetry`, `onSettled`, `onSuccess`, `onExecutionTimeout`, and `onTotalExecutionTimeout` for custom side effects.\n * - Call `abort()` to cancel ongoing execution and pending retries.\n * - Call `reset()` to reset state and cancel execution.\n * - Use `getAbortSignal()` to make your async function cancellable.\n * - Use dynamic options (functions) for `maxAttempts`, `baseWait`, and `enabled` based on retryer state.\n *\n * **Important:** This class is designed for single-use execution. Calling `execute()` multiple times\n * on the same instance will abort previous executions. For multiple calls, create a new instance\n * each time.\n *\n * @example\n * ```typescript\n * // Retry a fetch operation up to 5 times with exponential backoff, jitter, and timeouts\n * const retryer = new AsyncRetryer(async (url: string) => {\n * const signal = retryer.getAbortSignal()\n * return await fetch(url, { signal })\n * }, {\n * maxAttempts: 5,\n * backoff: 'exponential',\n * baseWait: 1000,\n * jitter: 0.1, // Add 10% random variation to prevent thundering herd\n * maxExecutionTime: 5000, // Abort individual calls after 5 seconds\n * maxTotalExecutionTime: 30000, // Abort entire operation after 30 seconds\n * onRetry: (attempt, error) => console.log(`Retry attempt ${attempt} after error:`, error),\n * onSuccess: (result) => console.log('Success:', result),\n * onError: (error) => console.error('Error:', error),\n * onLastError: (error) => console.error('All retries failed:', error),\n * })\n *\n * const result = await retryer.execute('/api/data')\n * ```\n *\n * @template TFn The async function type to be retried.\n */\nexport class AsyncRetryer<TFn extends AnyAsyncFunction> {\n readonly store: Store<Readonly<AsyncRetryerState<TFn>>> = new Store(\n getDefaultAsyncRetryerState<TFn>(),\n )\n key: string | undefined\n options: AsyncRetryerOptions<TFn> & typeof defaultOptions\n #abortController: AbortController | null = null\n\n /**\n * Creates a new AsyncRetryer instance\n * @param fn The async function to retry\n * @param initialOptions Configuration options for the retryer\n */\n constructor(\n public fn: TFn,\n initialOptions: AsyncRetryerOptions<TFn> = {},\n ) {\n this.key = initialOptions.key\n this.options = {\n ...defaultOptions,\n ...initialOptions,\n throwOnError:\n initialOptions.throwOnError ??\n (initialOptions.onError ? false : defaultOptions.throwOnError),\n }\n this.#setState(this.options.initialState ?? {})\n\n if (this.key) {\n pacerEventClient.on('d-AsyncRetryer', (event) => {\n if (event.payload.key !== this.key) return\n this.#setState(\n event.payload.store.state as Partial<AsyncRetryerState<TFn>>,\n )\n this.setOptions(\n event.payload.options as Partial<AsyncRetryerOptions<TFn>>,\n )\n })\n }\n }\n\n /**\n * Updates the retryer options\n * @param newOptions Partial options to merge with existing options\n */\n setOptions = (newOptions: Partial<AsyncRetryerOptions<TFn>>): void => {\n this.options = { ...this.options, ...newOptions }\n }\n\n #setState = (newState: Partial<AsyncRetryerState<TFn>>): void => {\n this.store.setState((state) => {\n const combinedState = {\n ...state,\n ...newState,\n }\n const { isExecuting, currentAttempt } = combinedState\n return {\n ...combinedState,\n status: !this.#getEnabled()\n ? 'disabled'\n : isExecuting && currentAttempt === 1\n ? 'executing'\n : isExecuting && currentAttempt > 1\n ? 'retrying'\n : 'idle',\n }\n })\n emitChange('AsyncRetryer', this)\n }\n\n #getEnabled = (): boolean => {\n return !!parseFunctionOrValue(this.options.enabled, this)\n }\n\n #getMaxAttempts = (): number => {\n return parseFunctionOrValue(this.options.maxAttempts, this)\n }\n\n #getBaseWait = (): number => {\n return parseFunctionOrValue(this.options.baseWait, this)\n }\n\n #getMaxWait = (): number => {\n return parseFunctionOrValue(this.options.maxWait, this)\n }\n\n #calculateJitter = (waitTime: number): number => {\n const jitterAmount = this.options.jitter\n if (jitterAmount <= 0) return 0\n\n try {\n const crypto =\n typeof globalThis !== 'undefined' ? globalThis.crypto : undefined\n if (crypto?.getRandomValues) {\n const array = new Uint32Array(1)\n crypto.getRandomValues(array)\n // Convert to 0-1 range and apply jitter percentage\n const randomFactor = (array[0]! / 0xffffffff) * 2 - 1 // -1 to 1\n return Math.floor(waitTime * jitterAmount * randomFactor)\n }\n } catch {\n // No crypto available\n }\n return 0\n }\n\n #calculateWait = (attempt: number): number => {\n const baseWait = this.#getBaseWait()\n let waitTime: number\n\n switch (this.options.backoff) {\n case 'linear':\n waitTime = baseWait * attempt\n break\n case 'exponential':\n waitTime = baseWait * Math.pow(2, attempt - 1)\n break\n case 'fixed':\n default:\n waitTime = baseWait\n break\n }\n\n waitTime = Math.min(waitTime, this.#getMaxWait())\n\n const jitter = this.#calculateJitter(waitTime)\n return Math.max(0, waitTime + jitter)\n }\n\n /**\n * Executes the function with retry logic\n * @param args Arguments to pass to the function\n * @returns The function result, or undefined if disabled or all retries failed (when throwOnError is false)\n * @throws The last error if throwOnError is true and all retries fail\n */\n execute = async (\n ...args: Parameters<TFn>\n ): Promise<Awaited<ReturnType<TFn>> | undefined> => {\n if (!this.#getEnabled()) {\n return undefined\n }\n\n // Cancel any existing execution\n this.abort('new-execution')\n\n const startTime = Date.now()\n let lastError: Error | undefined\n let result: Awaited<ReturnType<TFn>> | undefined\n\n this.#abortController = new AbortController()\n const signal = this.#abortController.signal\n\n this.#setState({\n isExecuting: true,\n currentAttempt: 0,\n lastError: undefined,\n })\n\n // Set up total execution timeout\n let totalTimeoutId: ReturnType<typeof setTimeout> | undefined\n if (this.options.maxTotalExecutionTime !== Infinity) {\n totalTimeoutId = setTimeout(() => {\n this.options.onTotalExecutionTimeout?.(this)\n this.abort('total-timeout')\n }, this.options.maxTotalExecutionTime)\n }\n\n let isLastAttempt = false\n for (let attempt = 1; attempt <= this.#getMaxAttempts(); attempt++) {\n isLastAttempt = attempt === this.#getMaxAttempts()\n this.#setState({ currentAttempt: attempt })\n\n try {\n if (signal.aborted) {\n return undefined\n }\n\n // Check if total execution time has been exceeded\n const currentTotalTime = Date.now() - startTime\n if (\n this.options.maxTotalExecutionTime !== Infinity &&\n currentTotalTime >= this.options.maxTotalExecutionTime\n ) {\n this.options.onTotalExecutionTimeout?.(this)\n this.abort('total-timeout')\n return undefined\n }\n\n // Execute with individual timeout if specified\n if (this.options.maxExecutionTime === Infinity) {\n result = (await this.fn(...args)) as Awaited<ReturnType<TFn>>\n } else {\n result = (await Promise.race([\n this.fn(...args),\n new Promise<never>((_, reject) => {\n const timeout = setTimeout(() => {\n this.options.onExecutionTimeout?.(this)\n this.abort('execution-timeout')\n reject(\n new Error(\n `Execution timeout: ${this.options.maxExecutionTime}ms exceeded`,\n ),\n )\n }, this.options.maxExecutionTime)\n\n signal.addEventListener(\n 'abort',\n () => {\n clearTimeout(timeout)\n reject(new Error('Aborted'))\n },\n { once: true },\n )\n }),\n ])) as Awaited<ReturnType<TFn>>\n }\n\n // Check if cancelled during execution\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n if (signal.aborted) {\n return undefined\n }\n\n const totalTime = Date.now() - startTime\n this.#setState({\n executionCount: this.store.state.executionCount + 1,\n isExecuting: false,\n lastExecutionTime: Date.now(),\n totalExecutionTime: totalTime,\n currentAttempt: 0,\n lastResult: result,\n })\n\n this.options.onSuccess?.(result as Awaited<ReturnType<TFn>>, args, this)\n\n return result\n } catch (error) {\n // Treat abort as a non-error cancellation outcome\n if (\n error &&\n typeof error === 'object' &&\n 'name' in error &&\n (error as Error).name === 'AbortError'\n ) {\n return undefined\n }\n lastError = error instanceof Error ? error : new Error(String(error))\n this.#setState({ lastError })\n\n // Call onError for every error (including during retries)\n this.options.onError?.(lastError, args, this)\n\n if (attempt < this.#getMaxAttempts()) {\n this.options.onRetry?.(attempt, lastError, this)\n\n const wait = this.#calculateWait(attempt)\n if (wait > 0) {\n // Eagerly reflect retrying status during the wait window\n this.#setState({ isExecuting: true, currentAttempt: attempt + 1 })\n await new Promise<void>((resolve) => {\n const timeout = setTimeout(() => {\n signal.removeEventListener('abort', onAbort)\n resolve()\n }, wait)\n const onAbort = () => {\n clearTimeout(timeout)\n signal.removeEventListener('abort', onAbort)\n resolve()\n }\n signal.addEventListener('abort', onAbort)\n })\n if (signal.aborted) {\n return undefined\n }\n }\n }\n } finally {\n this.options.onSettled?.(args, this)\n }\n }\n\n // Clean up total timeout\n if (totalTimeoutId) {\n clearTimeout(totalTimeoutId)\n }\n\n // Exhausted retries - finalize state\n this.#setState({ isExecuting: false })\n this.options.onLastError?.(lastError as Error, this)\n this.options.onSettled?.(args, this)\n\n if (\n (this.options.throwOnError === 'last' && isLastAttempt) ||\n this.options.throwOnError === true\n ) {\n throw lastError\n }\n\n return undefined\n }\n\n /**\n * Returns the current AbortSignal for the executing operation.\n * Use this signal in your async function to make it cancellable.\n * Returns null when not currently executing.\n *\n * @example\n * ```typescript\n * const retryer = new AsyncRetryer(async (userId: string) => {\n * const signal = retryer.getAbortSignal()\n * if (signal) {\n * return fetch(`/api/users/${userId}`, { signal })\n * }\n * return fetch(`/api/users/${userId}`)\n * })\n *\n * // Abort will now actually cancel the fetch\n * retryer.abort()\n * ```\n */\n getAbortSignal = (): AbortSignal | null => {\n return this.#abortController?.signal ?? null\n }\n\n /**\n * Cancels the current execution and any pending retries\n * @param reason The reason for the abort (defaults to 'manual')\n */\n abort = (\n reason:\n | 'manual'\n | 'execution-timeout'\n | 'total-timeout'\n | 'new-execution' = 'manual',\n ): void => {\n if (this.#abortController) {\n this.#abortController.abort()\n this.#abortController = null\n this.#setState({\n isExecuting: false,\n })\n this.options.onAbort?.(reason, this)\n }\n }\n\n /**\n * Resets the retryer to its initial state\n */\n reset = (): void => {\n this.#setState(getDefaultAsyncRetryerState<TFn>())\n }\n}\n\n/**\n * Creates a retry-enabled version of an async function. This is a convenience wrapper\n * around the AsyncRetryer class that returns the execute method.\n *\n * @param fn The async function to add retry functionality to\n * @param initialOptions Configuration options for the retry behavior\n * @returns A new function that executes the original with retry logic\n *\n * @example\n * ```typescript\n * // Define your async function normally\n * async function fetchData(url: string) {\n * const response = await fetch(url)\n * if (!response.ok) throw new Error('Request failed')\n * return response.json()\n * }\n *\n * // Create retry-enabled function\n * const fetchWithRetry = asyncRetry(fetchData, {\n * maxAttempts: 3,\n * backoff: 'exponential',\n * baseWait: 1000,\n * jitter: 0.1\n * })\n *\n * // Call it multiple times\n * const data1 = await fetchWithRetry('/api/data1')\n * const data2 = await fetchWithRetry('/api/data2')\n * ```\n */\nexport function asyncRetry<TFn extends AnyAsyncFunction>(\n fn: TFn,\n initialOptions: AsyncRetryerOptions<TFn> = {},\n): (...args: Parameters<TFn>) => Promise<Awaited<ReturnType<TFn>> | undefined> {\n const retryer = new AsyncRetryer(fn, initialOptions)\n return retryer.execute\n}\n"],"mappings":"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"}

@@ -1,1 +0,1 @@

{"version":3,"file":"async-retryer.js","names":["#getEnabled","#abortController","#setState","#getMaxAttempts","#calculateWait","#getBaseWait","#getMaxWait","#calculateJitter"],"sources":["../src/async-retryer.ts"],"sourcesContent":["import { Store } from '@tanstack/store'\nimport { parseFunctionOrValue } from './utils'\nimport { emitChange, pacerEventClient } from './event-client'\nimport type { AnyAsyncFunction } from './types'\n\nexport interface AsyncRetryerState<TFn extends AnyAsyncFunction> {\n /**\n * The current retry attempt number (0 when not executing)\n */\n currentAttempt: number\n /**\n * Total number of completed executions (successful or failed)\n */\n executionCount: number\n /**\n * Whether the retryer is currently executing the function\n */\n isExecuting: boolean\n /**\n * The most recent error encountered during execution\n */\n lastError: Error | undefined\n /**\n * Timestamp of the last execution completion in milliseconds\n */\n lastExecutionTime: number\n /**\n * The result from the most recent successful execution\n */\n lastResult: Awaited<ReturnType<TFn>> | undefined\n /**\n * Current execution status - 'disabled' when not enabled, 'idle' when ready, 'executing' when running\n */\n status: 'disabled' | 'idle' | 'executing' | 'retrying'\n /**\n * Total time spent executing (including retries) in milliseconds\n */\n totalExecutionTime: number\n}\n\n/**\n * Creates the default initial state for an AsyncRetryer instance\n * @returns The default state with all values reset to initial values\n */\nfunction getDefaultAsyncRetryerState<\n TFn extends AnyAsyncFunction,\n>(): AsyncRetryerState<TFn> {\n return {\n currentAttempt: 0,\n executionCount: 0,\n isExecuting: false,\n lastError: undefined,\n lastExecutionTime: 0,\n lastResult: undefined,\n status: 'idle',\n totalExecutionTime: 0,\n }\n}\n\nexport interface AsyncRetryerOptions<TFn extends AnyAsyncFunction> {\n /**\n * The backoff strategy for retry delays:\n * - 'exponential': Wait time doubles with each attempt (1s, 2s, 4s, ...)\n * - 'linear': Wait time increases linearly (1s, 2s, 3s, ...)\n * - 'fixed': Same wait time for all attempts\n * @default 'exponential'\n */\n backoff?: 'linear' | 'exponential' | 'fixed'\n /**\n * Base wait time in milliseconds between retries, or a function that returns the wait time\n * @default 1000\n */\n baseWait?: number | ((retryer: AsyncRetryer<TFn>) => number)\n /**\n * Whether the retryer is enabled, or a function that determines if it's enabled\n * @default true\n */\n enabled?: boolean | ((retryer: AsyncRetryer<TFn>) => boolean)\n /**\n * Initial state to merge with the default state\n */\n initialState?: Partial<AsyncRetryerState<TFn>>\n /**\n * Jitter percentage to add to retry delays (0-1). Adds randomness to prevent thundering herd.\n * @default 0\n */\n jitter?: number\n /**\n * Optional key to identify this async retryer instance.\n * If provided, the async retryer will be identified by this key in the devtools and PacerProvider if applicable.\n */\n key?: string\n /**\n * Maximum number of retry attempts, or a function that returns the max attempts\n * @default 3\n */\n maxAttempts?: number | ((retryer: AsyncRetryer<TFn>) => number)\n /**\n * Maximum execution time in milliseconds for a single function call before aborting\n * @default Infinity\n */\n maxExecutionTime?: number\n /**\n * Maximum total execution time in milliseconds for the entire retry operation before aborting\n * @default Infinity\n */\n maxTotalExecutionTime?: number\n /**\n * Maximum wait time in milliseconds to cap retry delays, or a function that returns the max wait time\n * @default Infinity\n */\n maxWait?: number | ((retryer: AsyncRetryer<TFn>) => number)\n /**\n * Callback invoked when the execution is aborted (manually or due to timeouts)\n */\n onAbort?: (\n reason: 'manual' | 'execution-timeout' | 'total-timeout' | 'new-execution',\n retryer: AsyncRetryer<TFn>,\n ) => void\n /**\n * Callback invoked when any error occurs during execution (including retries)\n */\n onError?: (\n error: Error,\n args: Parameters<TFn>,\n retryer: AsyncRetryer<TFn>,\n ) => void\n /**\n * Callback invoked when a single execution attempt times out (maxExecutionTime exceeded)\n */\n onExecutionTimeout?: (retryer: AsyncRetryer<TFn>) => void\n /**\n * Callback invoked when the final error occurs after all retries are exhausted\n */\n onLastError?: (error: Error, retryer: AsyncRetryer<TFn>) => void\n /**\n * Callback invoked before each retry attempt\n */\n onRetry?: (attempt: number, error: Error, retryer: AsyncRetryer<TFn>) => void\n /**\n * Callback invoked after execution completes (success or failure) of each attempt\n */\n onSettled?: (args: Parameters<TFn>, retryer: AsyncRetryer<TFn>) => void\n /**\n * Callback invoked when execution succeeds\n */\n onSuccess?: (\n result: Awaited<ReturnType<TFn>>,\n args: Parameters<TFn>,\n retryer: AsyncRetryer<TFn>,\n ) => void\n /**\n * Callback invoked when the total execution time times out (maxTotalExecutionTime exceeded)\n */\n onTotalExecutionTimeout?: (retryer: AsyncRetryer<TFn>) => void\n /**\n * Controls when errors are thrown:\n * - 'last': Only throw the final error after all retries are exhausted\n * - true: Throw every error immediately (disables retrying)\n * - false: Never throw errors, return undefined instead\n * @default 'last'\n */\n throwOnError?: boolean | 'last'\n}\n\n/**\n * Utility function for sharing common `AsyncRetryerOptions` options between different `AsyncRetryer` instances.\n */\nexport function asyncRetryerOptions<\n TFn extends AnyAsyncFunction = AnyAsyncFunction,\n TOptions extends Partial<AsyncRetryerOptions<TFn>> = Partial<\n AsyncRetryerOptions<TFn>\n >,\n>(options: TOptions): TOptions {\n return options\n}\n\nconst defaultOptions: Omit<\n Required<AsyncRetryerOptions<any>>,\n | 'initialState'\n | 'key'\n | 'onAbort'\n | 'onError'\n | 'onLastError'\n | 'onRetry'\n | 'onSettled'\n | 'onSuccess'\n | 'onExecutionTimeout'\n | 'onTotalExecutionTimeout'\n> = {\n backoff: 'exponential',\n baseWait: 1000,\n maxWait: Infinity,\n enabled: true,\n jitter: 0,\n maxAttempts: 3,\n maxExecutionTime: Infinity,\n maxTotalExecutionTime: Infinity,\n throwOnError: 'last',\n}\n\n/**\n * Provides robust retry functionality for asynchronous functions, supporting configurable backoff strategies,\n * attempt limits, timeout controls, and detailed state management. The AsyncRetryer class is designed to help you reliably\n * execute async operations that may fail intermittently, such as network requests or database operations,\n * by automatically retrying them according to your chosen policy.\n *\n * ## Retrying Concepts\n *\n * - **Retrying**: Automatically re-executes a failed async function up to a specified number of attempts.\n * Useful for handling transient errors (e.g., network flakiness, rate limits, temporary server issues).\n * - **Backoff Strategies**: Controls the delay between retry attempts (default: `'exponential'`):\n * - `'exponential'`: Wait time doubles with each attempt (1s, 2s, 4s, ...) - **DEFAULT**\n * - `'linear'`: Wait time increases linearly (1s, 2s, 3s, ...)\n * - `'fixed'`: Waits a constant amount of time (`baseWait`) between each attempt\n * - **Jitter**: Adds randomness to retry delays to prevent thundering herd problems (default: `0`).\n * Set to a value between 0-1 to apply that percentage of random variation to each delay.\n * - **Max Wait**: Caps the maximum wait time between retries (default: `Infinity`).\n * Useful for preventing exponential backoff from growing too large (e.g., cap at 30s even if exponential would be 64s).\n * - **Timeout Controls**: Set limits on execution time to prevent hanging operations:\n * - `maxExecutionTime`: Maximum time for a single function call (default: `Infinity`)\n * - `maxTotalExecutionTime`: Maximum time for the entire retry operation (default: `Infinity`)\n * - **Abort & Cancellation**: Supports cancellation via an internal `AbortController`. Call `abort()` to stop retries.\n * Use `getAbortSignal()` to make your async function actually cancellable (e.g., with fetch requests).\n *\n * ## State Management\n *\n * Uses TanStack Store for fine-grained reactivity. State can be accessed via the `store.state` property.\n *\n * Available state properties:\n * - `currentAttempt`: The current retry attempt number (0 when not executing)\n * - `executionCount`: Total number of completed executions (successful or failed)\n * - `isExecuting`: Whether the retryer is currently executing the function\n * - `lastError`: The most recent error encountered during execution\n * - `lastExecutionTime`: Timestamp of the last execution completion in milliseconds\n * - `lastResult`: The result from the most recent successful execution\n * - `status`: Current execution status ('disabled' | 'idle' | 'executing' | 'retrying')\n * - `totalExecutionTime`: Total time spent executing (including retries) in milliseconds\n *\n * ## Error Handling\n *\n * The `throwOnError` option controls when errors are thrown (default: `'last'`):\n * - `'last'`: Only throws the final error after all retries are exhausted - **DEFAULT**\n * - `true`: Throws every error immediately (disables retrying)\n * - `false`: Never throws errors, returns `undefined` instead\n *\n * Callbacks for lifecycle management:\n * - `onAbort`: Called when execution is aborted (manually or due to timeouts)\n * - `onError`: Called for every error (including during retries)\n * - `onLastError`: Called only for the final error after all retries fail\n * - `onRetry`: Called before each retry attempt\n * - `onSettled`: Called after execution completes (success or failure) of each attempt\n * - `onSuccess`: Called when execution succeeds\n * - `onExecutionTimeout`: Called when a single execution attempt times out\n * - `onTotalExecutionTimeout`: Called when the total execution time times out\n *\n * ## Usage\n *\n * - Use for async operations that may fail transiently and benefit from retrying.\n * - Configure `maxAttempts`, `backoff`, `baseWait`, `maxWait`, and `jitter` to control retry behavior.\n * - Set `maxExecutionTime` and `maxTotalExecutionTime` to prevent hanging operations.\n * - Use `onAbort`, `onError`, `onLastError`, `onRetry`, `onSettled`, `onSuccess`, `onExecutionTimeout`, and `onTotalExecutionTimeout` for custom side effects.\n * - Call `abort()` to cancel ongoing execution and pending retries.\n * - Call `reset()` to reset state and cancel execution.\n * - Use `getAbortSignal()` to make your async function cancellable.\n * - Use dynamic options (functions) for `maxAttempts`, `baseWait`, and `enabled` based on retryer state.\n *\n * **Important:** This class is designed for single-use execution. Calling `execute()` multiple times\n * on the same instance will abort previous executions. For multiple calls, create a new instance\n * each time.\n *\n * @example\n * ```typescript\n * // Retry a fetch operation up to 5 times with exponential backoff, jitter, and timeouts\n * const retryer = new AsyncRetryer(async (url: string) => {\n * const signal = retryer.getAbortSignal()\n * return await fetch(url, { signal })\n * }, {\n * maxAttempts: 5,\n * backoff: 'exponential',\n * baseWait: 1000,\n * jitter: 0.1, // Add 10% random variation to prevent thundering herd\n * maxExecutionTime: 5000, // Abort individual calls after 5 seconds\n * maxTotalExecutionTime: 30000, // Abort entire operation after 30 seconds\n * onRetry: (attempt, error) => console.log(`Retry attempt ${attempt} after error:`, error),\n * onSuccess: (result) => console.log('Success:', result),\n * onError: (error) => console.error('Error:', error),\n * onLastError: (error) => console.error('All retries failed:', error),\n * })\n *\n * const result = await retryer.execute('/api/data')\n * ```\n *\n * @template TFn The async function type to be retried.\n */\nexport class AsyncRetryer<TFn extends AnyAsyncFunction> {\n readonly store: Store<Readonly<AsyncRetryerState<TFn>>> = new Store(\n getDefaultAsyncRetryerState<TFn>(),\n )\n key: string | undefined\n options: AsyncRetryerOptions<TFn> & typeof defaultOptions\n #abortController: AbortController | null = null\n\n /**\n * Creates a new AsyncRetryer instance\n * @param fn The async function to retry\n * @param initialOptions Configuration options for the retryer\n */\n constructor(\n public fn: TFn,\n initialOptions: AsyncRetryerOptions<TFn> = {},\n ) {\n this.key = initialOptions.key\n this.options = {\n ...defaultOptions,\n ...initialOptions,\n throwOnError:\n initialOptions.throwOnError ??\n (initialOptions.onError ? false : defaultOptions.throwOnError),\n }\n this.#setState(this.options.initialState ?? {})\n\n if (this.key) {\n pacerEventClient.on('d-AsyncRetryer', (event) => {\n if (event.payload.key !== this.key) return\n this.#setState(\n event.payload.store.state as Partial<AsyncRetryerState<TFn>>,\n )\n this.setOptions(\n event.payload.options as Partial<AsyncRetryerOptions<TFn>>,\n )\n })\n }\n }\n\n /**\n * Updates the retryer options\n * @param newOptions Partial options to merge with existing options\n */\n setOptions = (newOptions: Partial<AsyncRetryerOptions<TFn>>): void => {\n this.options = { ...this.options, ...newOptions }\n }\n\n #setState = (newState: Partial<AsyncRetryerState<TFn>>): void => {\n this.store.setState((state) => {\n const combinedState = {\n ...state,\n ...newState,\n }\n const { isExecuting, currentAttempt } = combinedState\n return {\n ...combinedState,\n status: !this.#getEnabled()\n ? 'disabled'\n : isExecuting && currentAttempt === 1\n ? 'executing'\n : isExecuting && currentAttempt > 1\n ? 'retrying'\n : 'idle',\n }\n })\n emitChange('AsyncRetryer', this)\n }\n\n #getEnabled = (): boolean => {\n return !!parseFunctionOrValue(this.options.enabled, this)\n }\n\n #getMaxAttempts = (): number => {\n return parseFunctionOrValue(this.options.maxAttempts, this)\n }\n\n #getBaseWait = (): number => {\n return parseFunctionOrValue(this.options.baseWait, this)\n }\n\n #getMaxWait = (): number => {\n return parseFunctionOrValue(this.options.maxWait, this)\n }\n\n #calculateJitter = (waitTime: number): number => {\n const jitterAmount = this.options.jitter\n if (jitterAmount <= 0) return 0\n\n try {\n const crypto =\n typeof globalThis !== 'undefined' ? globalThis.crypto : undefined\n if (crypto?.getRandomValues) {\n const array = new Uint32Array(1)\n crypto.getRandomValues(array)\n // Convert to 0-1 range and apply jitter percentage\n const randomFactor = (array[0]! / 0xffffffff) * 2 - 1 // -1 to 1\n return Math.floor(waitTime * jitterAmount * randomFactor)\n }\n } catch {\n // No crypto available\n }\n return 0\n }\n\n #calculateWait = (attempt: number): number => {\n const baseWait = this.#getBaseWait()\n let waitTime: number\n\n switch (this.options.backoff) {\n case 'linear':\n waitTime = baseWait * attempt\n break\n case 'exponential':\n waitTime = baseWait * Math.pow(2, attempt - 1)\n break\n case 'fixed':\n default:\n waitTime = baseWait\n break\n }\n\n waitTime = Math.min(waitTime, this.#getMaxWait())\n\n const jitter = this.#calculateJitter(waitTime)\n return Math.max(0, waitTime + jitter)\n }\n\n /**\n * Executes the function with retry logic\n * @param args Arguments to pass to the function\n * @returns The function result, or undefined if disabled or all retries failed (when throwOnError is false)\n * @throws The last error if throwOnError is true and all retries fail\n */\n execute = async (\n ...args: Parameters<TFn>\n ): Promise<Awaited<ReturnType<TFn>> | undefined> => {\n if (!this.#getEnabled()) {\n return undefined\n }\n\n // Cancel any existing execution\n this.abort('new-execution')\n\n const startTime = Date.now()\n let lastError: Error | undefined\n let result: Awaited<ReturnType<TFn>> | undefined\n\n this.#abortController = new AbortController()\n const signal = this.#abortController.signal\n\n this.#setState({\n isExecuting: true,\n currentAttempt: 0,\n lastError: undefined,\n })\n\n // Set up total execution timeout\n let totalTimeoutId: NodeJS.Timeout | undefined\n if (this.options.maxTotalExecutionTime !== Infinity) {\n totalTimeoutId = setTimeout(() => {\n this.options.onTotalExecutionTimeout?.(this)\n this.abort('total-timeout')\n }, this.options.maxTotalExecutionTime)\n }\n\n let isLastAttempt = false\n for (let attempt = 1; attempt <= this.#getMaxAttempts(); attempt++) {\n isLastAttempt = attempt === this.#getMaxAttempts()\n this.#setState({ currentAttempt: attempt })\n\n try {\n if (signal.aborted) {\n return undefined\n }\n\n // Check if total execution time has been exceeded\n const currentTotalTime = Date.now() - startTime\n if (\n this.options.maxTotalExecutionTime !== Infinity &&\n currentTotalTime >= this.options.maxTotalExecutionTime\n ) {\n this.options.onTotalExecutionTimeout?.(this)\n this.abort('total-timeout')\n return undefined\n }\n\n // Execute with individual timeout if specified\n if (this.options.maxExecutionTime === Infinity) {\n result = (await this.fn(...args)) as Awaited<ReturnType<TFn>>\n } else {\n result = (await Promise.race([\n this.fn(...args),\n new Promise<never>((_, reject) => {\n const timeout = setTimeout(() => {\n this.options.onExecutionTimeout?.(this)\n this.abort('execution-timeout')\n reject(\n new Error(\n `Execution timeout: ${this.options.maxExecutionTime}ms exceeded`,\n ),\n )\n }, this.options.maxExecutionTime)\n\n signal.addEventListener(\n 'abort',\n () => {\n clearTimeout(timeout)\n reject(new Error('Aborted'))\n },\n { once: true },\n )\n }),\n ])) as Awaited<ReturnType<TFn>>\n }\n\n // Check if cancelled during execution\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n if (signal.aborted) {\n return undefined\n }\n\n const totalTime = Date.now() - startTime\n this.#setState({\n executionCount: this.store.state.executionCount + 1,\n isExecuting: false,\n lastExecutionTime: Date.now(),\n totalExecutionTime: totalTime,\n currentAttempt: 0,\n lastResult: result,\n })\n\n this.options.onSuccess?.(result as Awaited<ReturnType<TFn>>, args, this)\n\n return result\n } catch (error) {\n // Treat abort as a non-error cancellation outcome\n if (\n error &&\n typeof error === 'object' &&\n 'name' in error &&\n (error as Error).name === 'AbortError'\n ) {\n return undefined\n }\n lastError = error instanceof Error ? error : new Error(String(error))\n this.#setState({ lastError })\n\n // Call onError for every error (including during retries)\n this.options.onError?.(lastError, args, this)\n\n if (attempt < this.#getMaxAttempts()) {\n this.options.onRetry?.(attempt, lastError, this)\n\n const wait = this.#calculateWait(attempt)\n if (wait > 0) {\n // Eagerly reflect retrying status during the wait window\n this.#setState({ isExecuting: true, currentAttempt: attempt + 1 })\n await new Promise<void>((resolve) => {\n const timeout = setTimeout(() => {\n signal.removeEventListener('abort', onAbort)\n resolve()\n }, wait)\n const onAbort = () => {\n clearTimeout(timeout)\n signal.removeEventListener('abort', onAbort)\n resolve()\n }\n signal.addEventListener('abort', onAbort)\n })\n if (signal.aborted) {\n return undefined\n }\n }\n }\n } finally {\n this.options.onSettled?.(args, this)\n }\n }\n\n // Clean up total timeout\n if (totalTimeoutId) {\n clearTimeout(totalTimeoutId)\n }\n\n // Exhausted retries - finalize state\n this.#setState({ isExecuting: false })\n this.options.onLastError?.(lastError as Error, this)\n this.options.onSettled?.(args, this)\n\n if (\n (this.options.throwOnError === 'last' && isLastAttempt) ||\n this.options.throwOnError === true\n ) {\n throw lastError\n }\n\n return undefined\n }\n\n /**\n * Returns the current AbortSignal for the executing operation.\n * Use this signal in your async function to make it cancellable.\n * Returns null when not currently executing.\n *\n * @example\n * ```typescript\n * const retryer = new AsyncRetryer(async (userId: string) => {\n * const signal = retryer.getAbortSignal()\n * if (signal) {\n * return fetch(`/api/users/${userId}`, { signal })\n * }\n * return fetch(`/api/users/${userId}`)\n * })\n *\n * // Abort will now actually cancel the fetch\n * retryer.abort()\n * ```\n */\n getAbortSignal = (): AbortSignal | null => {\n return this.#abortController?.signal ?? null\n }\n\n /**\n * Cancels the current execution and any pending retries\n * @param reason The reason for the abort (defaults to 'manual')\n */\n abort = (\n reason:\n | 'manual'\n | 'execution-timeout'\n | 'total-timeout'\n | 'new-execution' = 'manual',\n ): void => {\n if (this.#abortController) {\n this.#abortController.abort()\n this.#abortController = null\n this.#setState({\n isExecuting: false,\n })\n this.options.onAbort?.(reason, this)\n }\n }\n\n /**\n * Resets the retryer to its initial state\n */\n reset = (): void => {\n this.#setState(getDefaultAsyncRetryerState<TFn>())\n }\n}\n\n/**\n * Creates a retry-enabled version of an async function. This is a convenience wrapper\n * around the AsyncRetryer class that returns the execute method.\n *\n * @param fn The async function to add retry functionality to\n * @param initialOptions Configuration options for the retry behavior\n * @returns A new function that executes the original with retry logic\n *\n * @example\n * ```typescript\n * // Define your async function normally\n * async function fetchData(url: string) {\n * const response = await fetch(url)\n * if (!response.ok) throw new Error('Request failed')\n * return response.json()\n * }\n *\n * // Create retry-enabled function\n * const fetchWithRetry = asyncRetry(fetchData, {\n * maxAttempts: 3,\n * backoff: 'exponential',\n * baseWait: 1000,\n * jitter: 0.1\n * })\n *\n * // Call it multiple times\n * const data1 = await fetchWithRetry('/api/data1')\n * const data2 = await fetchWithRetry('/api/data2')\n * ```\n */\nexport function asyncRetry<TFn extends AnyAsyncFunction>(\n fn: TFn,\n initialOptions: AsyncRetryerOptions<TFn> = {},\n): (...args: Parameters<TFn>) => Promise<Awaited<ReturnType<TFn>> | undefined> {\n const retryer = new AsyncRetryer(fn, initialOptions)\n return retryer.execute\n}\n"],"mappings":"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"}
{"version":3,"file":"async-retryer.js","names":["#getEnabled","#abortController","#setState","#getMaxAttempts","#calculateWait","#getBaseWait","#getMaxWait","#calculateJitter"],"sources":["../src/async-retryer.ts"],"sourcesContent":["import { Store } from '@tanstack/store'\nimport { parseFunctionOrValue } from './utils'\nimport { emitChange, pacerEventClient } from './event-client'\nimport type { AnyAsyncFunction } from './types'\n\nexport interface AsyncRetryerState<TFn extends AnyAsyncFunction> {\n /**\n * The current retry attempt number (0 when not executing)\n */\n currentAttempt: number\n /**\n * Total number of completed executions (successful or failed)\n */\n executionCount: number\n /**\n * Whether the retryer is currently executing the function\n */\n isExecuting: boolean\n /**\n * The most recent error encountered during execution\n */\n lastError: Error | undefined\n /**\n * Timestamp of the last execution completion in milliseconds\n */\n lastExecutionTime: number\n /**\n * The result from the most recent successful execution\n */\n lastResult: Awaited<ReturnType<TFn>> | undefined\n /**\n * Current execution status - 'disabled' when not enabled, 'idle' when ready, 'executing' when running\n */\n status: 'disabled' | 'idle' | 'executing' | 'retrying'\n /**\n * Total time spent executing (including retries) in milliseconds\n */\n totalExecutionTime: number\n}\n\n/**\n * Creates the default initial state for an AsyncRetryer instance\n * @returns The default state with all values reset to initial values\n */\nfunction getDefaultAsyncRetryerState<\n TFn extends AnyAsyncFunction,\n>(): AsyncRetryerState<TFn> {\n return {\n currentAttempt: 0,\n executionCount: 0,\n isExecuting: false,\n lastError: undefined,\n lastExecutionTime: 0,\n lastResult: undefined,\n status: 'idle',\n totalExecutionTime: 0,\n }\n}\n\nexport interface AsyncRetryerOptions<TFn extends AnyAsyncFunction> {\n /**\n * The backoff strategy for retry delays:\n * - 'exponential': Wait time doubles with each attempt (1s, 2s, 4s, ...)\n * - 'linear': Wait time increases linearly (1s, 2s, 3s, ...)\n * - 'fixed': Same wait time for all attempts\n * @default 'exponential'\n */\n backoff?: 'linear' | 'exponential' | 'fixed'\n /**\n * Base wait time in milliseconds between retries, or a function that returns the wait time\n * @default 1000\n */\n baseWait?: number | ((retryer: AsyncRetryer<TFn>) => number)\n /**\n * Whether the retryer is enabled, or a function that determines if it's enabled\n * @default true\n */\n enabled?: boolean | ((retryer: AsyncRetryer<TFn>) => boolean)\n /**\n * Initial state to merge with the default state\n */\n initialState?: Partial<AsyncRetryerState<TFn>>\n /**\n * Jitter percentage to add to retry delays (0-1). Adds randomness to prevent thundering herd.\n * @default 0\n */\n jitter?: number\n /**\n * Optional key to identify this async retryer instance.\n * If provided, the async retryer will be identified by this key in the devtools and PacerProvider if applicable.\n */\n key?: string\n /**\n * Maximum number of retry attempts, or a function that returns the max attempts\n * @default 3\n */\n maxAttempts?: number | ((retryer: AsyncRetryer<TFn>) => number)\n /**\n * Maximum execution time in milliseconds for a single function call before aborting\n * @default Infinity\n */\n maxExecutionTime?: number\n /**\n * Maximum total execution time in milliseconds for the entire retry operation before aborting\n * @default Infinity\n */\n maxTotalExecutionTime?: number\n /**\n * Maximum wait time in milliseconds to cap retry delays, or a function that returns the max wait time\n * @default Infinity\n */\n maxWait?: number | ((retryer: AsyncRetryer<TFn>) => number)\n /**\n * Callback invoked when the execution is aborted (manually or due to timeouts)\n */\n onAbort?: (\n reason: 'manual' | 'execution-timeout' | 'total-timeout' | 'new-execution',\n retryer: AsyncRetryer<TFn>,\n ) => void\n /**\n * Callback invoked when any error occurs during execution (including retries)\n */\n onError?: (\n error: Error,\n args: Parameters<TFn>,\n retryer: AsyncRetryer<TFn>,\n ) => void\n /**\n * Callback invoked when a single execution attempt times out (maxExecutionTime exceeded)\n */\n onExecutionTimeout?: (retryer: AsyncRetryer<TFn>) => void\n /**\n * Callback invoked when the final error occurs after all retries are exhausted\n */\n onLastError?: (error: Error, retryer: AsyncRetryer<TFn>) => void\n /**\n * Callback invoked before each retry attempt\n */\n onRetry?: (attempt: number, error: Error, retryer: AsyncRetryer<TFn>) => void\n /**\n * Callback invoked after execution completes (success or failure) of each attempt\n */\n onSettled?: (args: Parameters<TFn>, retryer: AsyncRetryer<TFn>) => void\n /**\n * Callback invoked when execution succeeds\n */\n onSuccess?: (\n result: Awaited<ReturnType<TFn>>,\n args: Parameters<TFn>,\n retryer: AsyncRetryer<TFn>,\n ) => void\n /**\n * Callback invoked when the total execution time times out (maxTotalExecutionTime exceeded)\n */\n onTotalExecutionTimeout?: (retryer: AsyncRetryer<TFn>) => void\n /**\n * Controls when errors are thrown:\n * - 'last': Only throw the final error after all retries are exhausted\n * - true: Throw every error immediately (disables retrying)\n * - false: Never throw errors, return undefined instead\n * @default 'last'\n */\n throwOnError?: boolean | 'last'\n}\n\n/**\n * Utility function for sharing common `AsyncRetryerOptions` options between different `AsyncRetryer` instances.\n */\nexport function asyncRetryerOptions<\n TFn extends AnyAsyncFunction = AnyAsyncFunction,\n TOptions extends Partial<AsyncRetryerOptions<TFn>> = Partial<\n AsyncRetryerOptions<TFn>\n >,\n>(options: TOptions): TOptions {\n return options\n}\n\nconst defaultOptions: Omit<\n Required<AsyncRetryerOptions<any>>,\n | 'initialState'\n | 'key'\n | 'onAbort'\n | 'onError'\n | 'onLastError'\n | 'onRetry'\n | 'onSettled'\n | 'onSuccess'\n | 'onExecutionTimeout'\n | 'onTotalExecutionTimeout'\n> = {\n backoff: 'exponential',\n baseWait: 1000,\n maxWait: Infinity,\n enabled: true,\n jitter: 0,\n maxAttempts: 3,\n maxExecutionTime: Infinity,\n maxTotalExecutionTime: Infinity,\n throwOnError: 'last',\n}\n\n/**\n * Provides robust retry functionality for asynchronous functions, supporting configurable backoff strategies,\n * attempt limits, timeout controls, and detailed state management. The AsyncRetryer class is designed to help you reliably\n * execute async operations that may fail intermittently, such as network requests or database operations,\n * by automatically retrying them according to your chosen policy.\n *\n * ## Retrying Concepts\n *\n * - **Retrying**: Automatically re-executes a failed async function up to a specified number of attempts.\n * Useful for handling transient errors (e.g., network flakiness, rate limits, temporary server issues).\n * - **Backoff Strategies**: Controls the delay between retry attempts (default: `'exponential'`):\n * - `'exponential'`: Wait time doubles with each attempt (1s, 2s, 4s, ...) - **DEFAULT**\n * - `'linear'`: Wait time increases linearly (1s, 2s, 3s, ...)\n * - `'fixed'`: Waits a constant amount of time (`baseWait`) between each attempt\n * - **Jitter**: Adds randomness to retry delays to prevent thundering herd problems (default: `0`).\n * Set to a value between 0-1 to apply that percentage of random variation to each delay.\n * - **Max Wait**: Caps the maximum wait time between retries (default: `Infinity`).\n * Useful for preventing exponential backoff from growing too large (e.g., cap at 30s even if exponential would be 64s).\n * - **Timeout Controls**: Set limits on execution time to prevent hanging operations:\n * - `maxExecutionTime`: Maximum time for a single function call (default: `Infinity`)\n * - `maxTotalExecutionTime`: Maximum time for the entire retry operation (default: `Infinity`)\n * - **Abort & Cancellation**: Supports cancellation via an internal `AbortController`. Call `abort()` to stop retries.\n * Use `getAbortSignal()` to make your async function actually cancellable (e.g., with fetch requests).\n *\n * ## State Management\n *\n * Uses TanStack Store for fine-grained reactivity. State can be accessed via the `store.state` property.\n *\n * Available state properties:\n * - `currentAttempt`: The current retry attempt number (0 when not executing)\n * - `executionCount`: Total number of completed executions (successful or failed)\n * - `isExecuting`: Whether the retryer is currently executing the function\n * - `lastError`: The most recent error encountered during execution\n * - `lastExecutionTime`: Timestamp of the last execution completion in milliseconds\n * - `lastResult`: The result from the most recent successful execution\n * - `status`: Current execution status ('disabled' | 'idle' | 'executing' | 'retrying')\n * - `totalExecutionTime`: Total time spent executing (including retries) in milliseconds\n *\n * ## Error Handling\n *\n * The `throwOnError` option controls when errors are thrown (default: `'last'`):\n * - `'last'`: Only throws the final error after all retries are exhausted - **DEFAULT**\n * - `true`: Throws every error immediately (disables retrying)\n * - `false`: Never throws errors, returns `undefined` instead\n *\n * Callbacks for lifecycle management:\n * - `onAbort`: Called when execution is aborted (manually or due to timeouts)\n * - `onError`: Called for every error (including during retries)\n * - `onLastError`: Called only for the final error after all retries fail\n * - `onRetry`: Called before each retry attempt\n * - `onSettled`: Called after execution completes (success or failure) of each attempt\n * - `onSuccess`: Called when execution succeeds\n * - `onExecutionTimeout`: Called when a single execution attempt times out\n * - `onTotalExecutionTimeout`: Called when the total execution time times out\n *\n * ## Usage\n *\n * - Use for async operations that may fail transiently and benefit from retrying.\n * - Configure `maxAttempts`, `backoff`, `baseWait`, `maxWait`, and `jitter` to control retry behavior.\n * - Set `maxExecutionTime` and `maxTotalExecutionTime` to prevent hanging operations.\n * - Use `onAbort`, `onError`, `onLastError`, `onRetry`, `onSettled`, `onSuccess`, `onExecutionTimeout`, and `onTotalExecutionTimeout` for custom side effects.\n * - Call `abort()` to cancel ongoing execution and pending retries.\n * - Call `reset()` to reset state and cancel execution.\n * - Use `getAbortSignal()` to make your async function cancellable.\n * - Use dynamic options (functions) for `maxAttempts`, `baseWait`, and `enabled` based on retryer state.\n *\n * **Important:** This class is designed for single-use execution. Calling `execute()` multiple times\n * on the same instance will abort previous executions. For multiple calls, create a new instance\n * each time.\n *\n * @example\n * ```typescript\n * // Retry a fetch operation up to 5 times with exponential backoff, jitter, and timeouts\n * const retryer = new AsyncRetryer(async (url: string) => {\n * const signal = retryer.getAbortSignal()\n * return await fetch(url, { signal })\n * }, {\n * maxAttempts: 5,\n * backoff: 'exponential',\n * baseWait: 1000,\n * jitter: 0.1, // Add 10% random variation to prevent thundering herd\n * maxExecutionTime: 5000, // Abort individual calls after 5 seconds\n * maxTotalExecutionTime: 30000, // Abort entire operation after 30 seconds\n * onRetry: (attempt, error) => console.log(`Retry attempt ${attempt} after error:`, error),\n * onSuccess: (result) => console.log('Success:', result),\n * onError: (error) => console.error('Error:', error),\n * onLastError: (error) => console.error('All retries failed:', error),\n * })\n *\n * const result = await retryer.execute('/api/data')\n * ```\n *\n * @template TFn The async function type to be retried.\n */\nexport class AsyncRetryer<TFn extends AnyAsyncFunction> {\n readonly store: Store<Readonly<AsyncRetryerState<TFn>>> = new Store(\n getDefaultAsyncRetryerState<TFn>(),\n )\n key: string | undefined\n options: AsyncRetryerOptions<TFn> & typeof defaultOptions\n #abortController: AbortController | null = null\n\n /**\n * Creates a new AsyncRetryer instance\n * @param fn The async function to retry\n * @param initialOptions Configuration options for the retryer\n */\n constructor(\n public fn: TFn,\n initialOptions: AsyncRetryerOptions<TFn> = {},\n ) {\n this.key = initialOptions.key\n this.options = {\n ...defaultOptions,\n ...initialOptions,\n throwOnError:\n initialOptions.throwOnError ??\n (initialOptions.onError ? false : defaultOptions.throwOnError),\n }\n this.#setState(this.options.initialState ?? {})\n\n if (this.key) {\n pacerEventClient.on('d-AsyncRetryer', (event) => {\n if (event.payload.key !== this.key) return\n this.#setState(\n event.payload.store.state as Partial<AsyncRetryerState<TFn>>,\n )\n this.setOptions(\n event.payload.options as Partial<AsyncRetryerOptions<TFn>>,\n )\n })\n }\n }\n\n /**\n * Updates the retryer options\n * @param newOptions Partial options to merge with existing options\n */\n setOptions = (newOptions: Partial<AsyncRetryerOptions<TFn>>): void => {\n this.options = { ...this.options, ...newOptions }\n }\n\n #setState = (newState: Partial<AsyncRetryerState<TFn>>): void => {\n this.store.setState((state) => {\n const combinedState = {\n ...state,\n ...newState,\n }\n const { isExecuting, currentAttempt } = combinedState\n return {\n ...combinedState,\n status: !this.#getEnabled()\n ? 'disabled'\n : isExecuting && currentAttempt === 1\n ? 'executing'\n : isExecuting && currentAttempt > 1\n ? 'retrying'\n : 'idle',\n }\n })\n emitChange('AsyncRetryer', this)\n }\n\n #getEnabled = (): boolean => {\n return !!parseFunctionOrValue(this.options.enabled, this)\n }\n\n #getMaxAttempts = (): number => {\n return parseFunctionOrValue(this.options.maxAttempts, this)\n }\n\n #getBaseWait = (): number => {\n return parseFunctionOrValue(this.options.baseWait, this)\n }\n\n #getMaxWait = (): number => {\n return parseFunctionOrValue(this.options.maxWait, this)\n }\n\n #calculateJitter = (waitTime: number): number => {\n const jitterAmount = this.options.jitter\n if (jitterAmount <= 0) return 0\n\n try {\n const crypto =\n typeof globalThis !== 'undefined' ? globalThis.crypto : undefined\n if (crypto?.getRandomValues) {\n const array = new Uint32Array(1)\n crypto.getRandomValues(array)\n // Convert to 0-1 range and apply jitter percentage\n const randomFactor = (array[0]! / 0xffffffff) * 2 - 1 // -1 to 1\n return Math.floor(waitTime * jitterAmount * randomFactor)\n }\n } catch {\n // No crypto available\n }\n return 0\n }\n\n #calculateWait = (attempt: number): number => {\n const baseWait = this.#getBaseWait()\n let waitTime: number\n\n switch (this.options.backoff) {\n case 'linear':\n waitTime = baseWait * attempt\n break\n case 'exponential':\n waitTime = baseWait * Math.pow(2, attempt - 1)\n break\n case 'fixed':\n default:\n waitTime = baseWait\n break\n }\n\n waitTime = Math.min(waitTime, this.#getMaxWait())\n\n const jitter = this.#calculateJitter(waitTime)\n return Math.max(0, waitTime + jitter)\n }\n\n /**\n * Executes the function with retry logic\n * @param args Arguments to pass to the function\n * @returns The function result, or undefined if disabled or all retries failed (when throwOnError is false)\n * @throws The last error if throwOnError is true and all retries fail\n */\n execute = async (\n ...args: Parameters<TFn>\n ): Promise<Awaited<ReturnType<TFn>> | undefined> => {\n if (!this.#getEnabled()) {\n return undefined\n }\n\n // Cancel any existing execution\n this.abort('new-execution')\n\n const startTime = Date.now()\n let lastError: Error | undefined\n let result: Awaited<ReturnType<TFn>> | undefined\n\n this.#abortController = new AbortController()\n const signal = this.#abortController.signal\n\n this.#setState({\n isExecuting: true,\n currentAttempt: 0,\n lastError: undefined,\n })\n\n // Set up total execution timeout\n let totalTimeoutId: ReturnType<typeof setTimeout> | undefined\n if (this.options.maxTotalExecutionTime !== Infinity) {\n totalTimeoutId = setTimeout(() => {\n this.options.onTotalExecutionTimeout?.(this)\n this.abort('total-timeout')\n }, this.options.maxTotalExecutionTime)\n }\n\n let isLastAttempt = false\n for (let attempt = 1; attempt <= this.#getMaxAttempts(); attempt++) {\n isLastAttempt = attempt === this.#getMaxAttempts()\n this.#setState({ currentAttempt: attempt })\n\n try {\n if (signal.aborted) {\n return undefined\n }\n\n // Check if total execution time has been exceeded\n const currentTotalTime = Date.now() - startTime\n if (\n this.options.maxTotalExecutionTime !== Infinity &&\n currentTotalTime >= this.options.maxTotalExecutionTime\n ) {\n this.options.onTotalExecutionTimeout?.(this)\n this.abort('total-timeout')\n return undefined\n }\n\n // Execute with individual timeout if specified\n if (this.options.maxExecutionTime === Infinity) {\n result = (await this.fn(...args)) as Awaited<ReturnType<TFn>>\n } else {\n result = (await Promise.race([\n this.fn(...args),\n new Promise<never>((_, reject) => {\n const timeout = setTimeout(() => {\n this.options.onExecutionTimeout?.(this)\n this.abort('execution-timeout')\n reject(\n new Error(\n `Execution timeout: ${this.options.maxExecutionTime}ms exceeded`,\n ),\n )\n }, this.options.maxExecutionTime)\n\n signal.addEventListener(\n 'abort',\n () => {\n clearTimeout(timeout)\n reject(new Error('Aborted'))\n },\n { once: true },\n )\n }),\n ])) as Awaited<ReturnType<TFn>>\n }\n\n // Check if cancelled during execution\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n if (signal.aborted) {\n return undefined\n }\n\n const totalTime = Date.now() - startTime\n this.#setState({\n executionCount: this.store.state.executionCount + 1,\n isExecuting: false,\n lastExecutionTime: Date.now(),\n totalExecutionTime: totalTime,\n currentAttempt: 0,\n lastResult: result,\n })\n\n this.options.onSuccess?.(result as Awaited<ReturnType<TFn>>, args, this)\n\n return result\n } catch (error) {\n // Treat abort as a non-error cancellation outcome\n if (\n error &&\n typeof error === 'object' &&\n 'name' in error &&\n (error as Error).name === 'AbortError'\n ) {\n return undefined\n }\n lastError = error instanceof Error ? error : new Error(String(error))\n this.#setState({ lastError })\n\n // Call onError for every error (including during retries)\n this.options.onError?.(lastError, args, this)\n\n if (attempt < this.#getMaxAttempts()) {\n this.options.onRetry?.(attempt, lastError, this)\n\n const wait = this.#calculateWait(attempt)\n if (wait > 0) {\n // Eagerly reflect retrying status during the wait window\n this.#setState({ isExecuting: true, currentAttempt: attempt + 1 })\n await new Promise<void>((resolve) => {\n const timeout = setTimeout(() => {\n signal.removeEventListener('abort', onAbort)\n resolve()\n }, wait)\n const onAbort = () => {\n clearTimeout(timeout)\n signal.removeEventListener('abort', onAbort)\n resolve()\n }\n signal.addEventListener('abort', onAbort)\n })\n if (signal.aborted) {\n return undefined\n }\n }\n }\n } finally {\n this.options.onSettled?.(args, this)\n }\n }\n\n // Clean up total timeout\n if (totalTimeoutId) {\n clearTimeout(totalTimeoutId)\n }\n\n // Exhausted retries - finalize state\n this.#setState({ isExecuting: false })\n this.options.onLastError?.(lastError as Error, this)\n this.options.onSettled?.(args, this)\n\n if (\n (this.options.throwOnError === 'last' && isLastAttempt) ||\n this.options.throwOnError === true\n ) {\n throw lastError\n }\n\n return undefined\n }\n\n /**\n * Returns the current AbortSignal for the executing operation.\n * Use this signal in your async function to make it cancellable.\n * Returns null when not currently executing.\n *\n * @example\n * ```typescript\n * const retryer = new AsyncRetryer(async (userId: string) => {\n * const signal = retryer.getAbortSignal()\n * if (signal) {\n * return fetch(`/api/users/${userId}`, { signal })\n * }\n * return fetch(`/api/users/${userId}`)\n * })\n *\n * // Abort will now actually cancel the fetch\n * retryer.abort()\n * ```\n */\n getAbortSignal = (): AbortSignal | null => {\n return this.#abortController?.signal ?? null\n }\n\n /**\n * Cancels the current execution and any pending retries\n * @param reason The reason for the abort (defaults to 'manual')\n */\n abort = (\n reason:\n | 'manual'\n | 'execution-timeout'\n | 'total-timeout'\n | 'new-execution' = 'manual',\n ): void => {\n if (this.#abortController) {\n this.#abortController.abort()\n this.#abortController = null\n this.#setState({\n isExecuting: false,\n })\n this.options.onAbort?.(reason, this)\n }\n }\n\n /**\n * Resets the retryer to its initial state\n */\n reset = (): void => {\n this.#setState(getDefaultAsyncRetryerState<TFn>())\n }\n}\n\n/**\n * Creates a retry-enabled version of an async function. This is a convenience wrapper\n * around the AsyncRetryer class that returns the execute method.\n *\n * @param fn The async function to add retry functionality to\n * @param initialOptions Configuration options for the retry behavior\n * @returns A new function that executes the original with retry logic\n *\n * @example\n * ```typescript\n * // Define your async function normally\n * async function fetchData(url: string) {\n * const response = await fetch(url)\n * if (!response.ok) throw new Error('Request failed')\n * return response.json()\n * }\n *\n * // Create retry-enabled function\n * const fetchWithRetry = asyncRetry(fetchData, {\n * maxAttempts: 3,\n * backoff: 'exponential',\n * baseWait: 1000,\n * jitter: 0.1\n * })\n *\n * // Call it multiple times\n * const data1 = await fetchWithRetry('/api/data1')\n * const data2 = await fetchWithRetry('/api/data2')\n * ```\n */\nexport function asyncRetry<TFn extends AnyAsyncFunction>(\n fn: TFn,\n initialOptions: AsyncRetryerOptions<TFn> = {},\n): (...args: Parameters<TFn>) => Promise<Awaited<ReturnType<TFn>> | undefined> {\n const retryer = new AsyncRetryer(fn, initialOptions)\n return retryer.execute\n}\n"],"mappings":"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"}

@@ -1,1 +0,1 @@

{"version":3,"file":"async-throttler.cjs","names":["Store","#getEnabled","#resolvePreviousPromiseInternal","#setState","#getWait","#execute","#resolvePreviousPromise","#timeoutId","#clearTimeout","parseFunctionOrValue","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: NodeJS.Timeout | 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":";;;;;;;AAsDA,SAAS,gCAEqB;AAC5B,QAAO;EACL,YAAY;EACZ,aAAa;EACb,WAAW;EACX,UAAU;EACV,mBAAmB;EACnB,YAAY;EACZ,mBAAmB;EACnB,mBAAmB;EACnB,aAAa;EACb,QAAQ;EACR,cAAc;EACf;;;;;AA8EH,SAAgB,sBAKd,SAA6B;AAC7B,QAAO;;AAQT,MAAM,iBAA6D;CACjE,qBAAqB,EACnB,aAAa,GACd;CACD,SAAS;CACT,SAAS;CACT,UAAU;CACV,MAAM;CACP;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4DD,IAAa,iBAAb,MAA0D;CAOxD,aAAoC;CACpC,0BAEW;CAEX,YACE,AAAO,IACP,gBACA;EAFO;eAZmD,IAAIA,sBAE9D,+BAAoC,CAAC;uCAGvB,IAAI,KAAgC;qBAkCtC,eAA0D;AACtE,QAAK,UAAU;IAAE,GAAG,KAAK;IAAS,GAAG;IAAY;AAGjD,OAAI,CAAC,MAAKC,YAAa,CACrB,MAAK,QAAQ;;sBA+DF,OACb,GAAG,SACsC;AACzC,OAAI,CAAC,MAAKA,YAAa,CAAE,QAAO;AAEhC,SAAKC,gCAAiC;AAEtC,SAAKC,SAAU;IACb,mBAAmB,KAAK,MAAM,MAAM,oBAAoB;IACxD,UAAU;IACX,CAAC;GAEF,MAAM,OAAO,MAAKC,SAAU;GAC5B,MAAM,yBAAyB,KAAK,MAAM,MAAM;AAGhD,QACE,IAAI,mBAAmB,OAAO,IAC9B,KAAK,MAAM,MAAM,eAAe,mBAAmB,GACnD,oBACA;AACA,UAAM,IAAI,SAAS,YAAY,WAAW,SAAS,GAAG,CAAC;AACvD,QAAI,KAAK,MAAM,MAAM,sBAAsB,uBAEzC,QAAO,KAAK,MAAM,MAAM;;GAI5B,MAAM,MAAM,KAAK,KAAK;GACtB,MAAM,yBAAyB,MAAM,KAAK,MAAM,MAAM;AAEtD,OACE,KAAK,QAAQ,WACb,CAAC,KAAK,MAAM,MAAM,aAClB,0BAA0B,KAE1B,OAAM,MAAKC,QAAS,GAAG,KAAK;YACnB,KAAK,QAAQ,UAAU;AAEhC,SAAK,QAAQ;AACb,UAAKF,SAAU,EACb,WAAW,MACZ,CAAC;AAGF,WAAO,IAAI,SAAS,SAAS,WAAW;AACtC,WAAKG,yBAA0B;KAE/B,MAAM,4BAA4B,KAAK,MAAM,MAAM,oBAC/C,MAAM,KAAK,MAAM,MAAM,oBACvB;KACJ,MAAM,kBAAkB,KAAK,IAAI,GAAG,OAAO,0BAA0B;AAErE,WAAKC,YAAa,WAAW,YAAY;AACvC,YAAKC,cAAe;AACpB,UAAI,KAAK,MAAM,MAAM,aAAa,OAChC,KAAI;AACF,aAAM,MAAKH,QAAS,GAAG,KAAK,MAAM,MAAM,SAAS;eAC1C,OAAO;AACd,cAAO,MAAM;;AAGjB,YAAKC,yBAA0B;AAC/B,cAAQ,KAAK,MAAM,MAAM,WAAW;QACnC,gBAAgB;MACnB;;AAEJ,UAAO,KAAK,MAAM,MAAM;;eAyDlB,YAAkD;AACxD,OAAI,KAAK,MAAM,MAAM,aAAa,KAAK,MAAM,MAAM,UAAU;IAE3D,MAAM,iBAAiB,MAAKA;AAG5B,UAAKE,cAAe;AACpB,UAAKL,SAAU,EACb,WAAW,OACZ,CAAC;IAEF,MAAM,SAAS,MAAM,MAAKE,QAAS,GAAG,KAAK,MAAM,MAAM,SAAS;AAGhE,QAAI,eACF,gBAAe,OAAO;AAGxB,WAAO;;;yBA2CO,sBAAmD;GACnE,MAAM,QAAQ,qBAAqB,KAAK,MAAM,MAAM;AAEpD,UADgB,KAAK,cAAc,IAAI,MAAM,EAC7B,gBAAgB,IAAI;;qBAOlB;AAClB,QAAK,cAAc,SAAS,YAAY,QAAQ,OAAO,CAAC;AACxD,QAAK,cAAc,OAAO;AAC1B,SAAKF,SAAU,EAAE,aAAa,OAAO,CAAC;;sBAOnB;AACnB,SAAKK,cAAe;AACpB,OAAI,MAAKF,wBAAyB;AAChC,UAAKJ,gCAAiC;AACtC,UAAKI,yBAA0B;;AAEjC,SAAKH,SAAU,EACb,WAAW,OACZ,CAAC;;qBAMgB;AAClB,SAAKA,SAAU,+BAAoC,CAAC;AACpD,QAAK,cAAc,SAAS,YAAY,QAAQ,OAAO,CAAC;;AAzTxD,OAAK,MAAM,eAAe;AAC1B,OAAK,UAAU;GACb,GAAG;GACH,GAAG;GACH,cAAc,eAAe,gBAAgB,CAAC,eAAe;GAC9D;AACD,QAAKA,SAAU,KAAK,QAAQ,gBAAgB,EAAE,CAAC;AAE/C,MAAI,KAAK,IACP,uCAAiB,GAAG,qBAAqB,UAAU;AACjD,OAAI,MAAM,QAAQ,QAAQ,KAAK,IAAK;AACpC,SAAKA,SACH,MAAM,QAAQ,MAAM,MACrB;AACD,QAAK,WACH,MAAM,QAAQ,QACf;IACD;;CAgBN,aAAa,aAAsD;AACjE,OAAK,MAAM,UAAU,UAAU;GAC7B,MAAM,gBAAgB;IACpB,GAAG;IACH,GAAG;IACJ;GACD,MAAM,EAAE,WAAW,aAAa,gBAAgB;AAChD,UAAO;IACL,GAAG;IACH,QAAQ,CAAC,MAAKF,YAAa,GACvB,aACA,YACE,YACA,cACE,cACA,cAAc,IACZ,YACA;IACX;IACD;AACF,kCAAW,kBAAkB,KAAK;;;;;CAMpC,oBAA6B;AAC3B,SAAO,CAAC,CAACQ,mCAAqB,KAAK,QAAQ,SAAS,KAAK;;;;;CAM3D,iBAAyB;AACvB,SAAOA,mCAAqB,KAAK,QAAQ,MAAM,KAAK;;CA+FtD,WAAW,OACT,GAAG,SACsC;AACzC,MAAI,CAAC,MAAKR,YAAa,CAAE,QAAO;EAEhC,MAAM,sBAAsB,KAAK,MAAM,MAAM;AAE7C,MAAI;AACF,SAAKE,SAAU,EAAE,aAAa,MAAM,CAAC;GACrC,MAAM,sBAAsB,IAAIO,mCAAa,KAAK,IAAI;IACpD,GAAG,KAAK,QAAQ;IAChB,KAAK,GAAG,KAAK,IAAI,WAAW;IAC7B,CAAC;AACF,QAAK,cAAc,IAAI,qBAAqB,oBAAoB;GAChE,MAAM,SAAS,MAAM,oBAAoB,QAAQ,GAAG,KAAK;AACzD,SAAKP,SAAU;IACb,YAAY;IACZ,cAAc,KAAK,MAAM,MAAM,eAAe;IAC/C,CAAC;AACF,QAAK,QAAQ,YAAY,QAA2B,MAAM,KAAK;WACxD,OAAO;AACd,SAAKA,SAAU,EACb,YAAY,KAAK,MAAM,MAAM,aAAa,GAC3C,CAAC;AACF,QAAK,QAAQ,UAAU,OAAgB,MAAM,KAAK;AAClD,OAAI,KAAK,QAAQ,aACf,OAAM;YAEA;AACR,QAAK,cAAc,OAAO,oBAAoB;GAC9C,MAAM,oBAAoB,KAAK,KAAK;GACpC,MAAM,OAAO,MAAKC,SAAU;GAC5B,MAAM,oBAAoB,oBAAoB;AAC9C,SAAKD,SAAU;IACb,aAAa;IACb,WAAW,CAAC,CAAC,MAAKI;IAClB,aAAa,KAAK,MAAM,MAAM,cAAc;IAC5C;IACA;IACD,CAAC;AACF,QAAK,QAAQ,YAAY,MAAM,KAAK;AACpC,oBAAiB;AACf,QAAI,CAAC,KAAK,MAAM,MAAM,UAEpB,OAAKJ,SAAU,EAAE,mBAAmB,QAAW,CAAC;MAEjD,KAAK;;AAEV,SAAO,KAAK,MAAM,MAAM;;CA6B1B,wCAA8C;AAC5C,MAAI,MAAKG,wBAAyB;AAChC,SAAKA,uBAAwB,KAAK,MAAM,MAAM,WAAW;AACzD,SAAKA,yBAA0B;;;CAInC,sBAA4B;AAC1B,MAAI,MAAKC,WAAY;AACnB,gBAAa,MAAKA,UAAW;AAC7B,SAAKA,YAAa;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoIxB,SAAgB,cACd,IACA,gBACA;AAEA,QADuB,IAAI,eAAe,IAAI,eAAe,CACvC"}
{"version":3,"file":"async-throttler.cjs","names":["Store","#getEnabled","#resolvePreviousPromiseInternal","#setState","#getWait","#execute","#resolvePreviousPromise","#timeoutId","#clearTimeout","parseFunctionOrValue","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,1 +0,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: NodeJS.Timeout | 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":";;;;;;AAsDA,SAAS,gCAEqB;AAC5B,QAAO;EACL,YAAY;EACZ,aAAa;EACb,WAAW;EACX,UAAU;EACV,mBAAmB;EACnB,YAAY;EACZ,mBAAmB;EACnB,mBAAmB;EACnB,aAAa;EACb,QAAQ;EACR,cAAc;EACf;;;;;AA8EH,SAAgB,sBAKd,SAA6B;AAC7B,QAAO;;AAQT,MAAM,iBAA6D;CACjE,qBAAqB,EACnB,aAAa,GACd;CACD,SAAS;CACT,SAAS;CACT,UAAU;CACV,MAAM;CACP;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4DD,IAAa,iBAAb,MAA0D;CAOxD,aAAoC;CACpC,0BAEW;CAEX,YACE,AAAO,IACP,gBACA;EAFO;eAZmD,IAAI,MAE9D,+BAAoC,CAAC;uCAGvB,IAAI,KAAgC;qBAkCtC,eAA0D;AACtE,QAAK,UAAU;IAAE,GAAG,KAAK;IAAS,GAAG;IAAY;AAGjD,OAAI,CAAC,MAAKA,YAAa,CACrB,MAAK,QAAQ;;sBA+DF,OACb,GAAG,SACsC;AACzC,OAAI,CAAC,MAAKA,YAAa,CAAE,QAAO;AAEhC,SAAKC,gCAAiC;AAEtC,SAAKC,SAAU;IACb,mBAAmB,KAAK,MAAM,MAAM,oBAAoB;IACxD,UAAU;IACX,CAAC;GAEF,MAAM,OAAO,MAAKC,SAAU;GAC5B,MAAM,yBAAyB,KAAK,MAAM,MAAM;AAGhD,QACE,IAAI,mBAAmB,OAAO,IAC9B,KAAK,MAAM,MAAM,eAAe,mBAAmB,GACnD,oBACA;AACA,UAAM,IAAI,SAAS,YAAY,WAAW,SAAS,GAAG,CAAC;AACvD,QAAI,KAAK,MAAM,MAAM,sBAAsB,uBAEzC,QAAO,KAAK,MAAM,MAAM;;GAI5B,MAAM,MAAM,KAAK,KAAK;GACtB,MAAM,yBAAyB,MAAM,KAAK,MAAM,MAAM;AAEtD,OACE,KAAK,QAAQ,WACb,CAAC,KAAK,MAAM,MAAM,aAClB,0BAA0B,KAE1B,OAAM,MAAKC,QAAS,GAAG,KAAK;YACnB,KAAK,QAAQ,UAAU;AAEhC,SAAK,QAAQ;AACb,UAAKF,SAAU,EACb,WAAW,MACZ,CAAC;AAGF,WAAO,IAAI,SAAS,SAAS,WAAW;AACtC,WAAKG,yBAA0B;KAE/B,MAAM,4BAA4B,KAAK,MAAM,MAAM,oBAC/C,MAAM,KAAK,MAAM,MAAM,oBACvB;KACJ,MAAM,kBAAkB,KAAK,IAAI,GAAG,OAAO,0BAA0B;AAErE,WAAKC,YAAa,WAAW,YAAY;AACvC,YAAKC,cAAe;AACpB,UAAI,KAAK,MAAM,MAAM,aAAa,OAChC,KAAI;AACF,aAAM,MAAKH,QAAS,GAAG,KAAK,MAAM,MAAM,SAAS;eAC1C,OAAO;AACd,cAAO,MAAM;;AAGjB,YAAKC,yBAA0B;AAC/B,cAAQ,KAAK,MAAM,MAAM,WAAW;QACnC,gBAAgB;MACnB;;AAEJ,UAAO,KAAK,MAAM,MAAM;;eAyDlB,YAAkD;AACxD,OAAI,KAAK,MAAM,MAAM,aAAa,KAAK,MAAM,MAAM,UAAU;IAE3D,MAAM,iBAAiB,MAAKA;AAG5B,UAAKE,cAAe;AACpB,UAAKL,SAAU,EACb,WAAW,OACZ,CAAC;IAEF,MAAM,SAAS,MAAM,MAAKE,QAAS,GAAG,KAAK,MAAM,MAAM,SAAS;AAGhE,QAAI,eACF,gBAAe,OAAO;AAGxB,WAAO;;;yBA2CO,sBAAmD;GACnE,MAAM,QAAQ,qBAAqB,KAAK,MAAM,MAAM;AAEpD,UADgB,KAAK,cAAc,IAAI,MAAM,EAC7B,gBAAgB,IAAI;;qBAOlB;AAClB,QAAK,cAAc,SAAS,YAAY,QAAQ,OAAO,CAAC;AACxD,QAAK,cAAc,OAAO;AAC1B,SAAKF,SAAU,EAAE,aAAa,OAAO,CAAC;;sBAOnB;AACnB,SAAKK,cAAe;AACpB,OAAI,MAAKF,wBAAyB;AAChC,UAAKJ,gCAAiC;AACtC,UAAKI,yBAA0B;;AAEjC,SAAKH,SAAU,EACb,WAAW,OACZ,CAAC;;qBAMgB;AAClB,SAAKA,SAAU,+BAAoC,CAAC;AACpD,QAAK,cAAc,SAAS,YAAY,QAAQ,OAAO,CAAC;;AAzTxD,OAAK,MAAM,eAAe;AAC1B,OAAK,UAAU;GACb,GAAG;GACH,GAAG;GACH,cAAc,eAAe,gBAAgB,CAAC,eAAe;GAC9D;AACD,QAAKA,SAAU,KAAK,QAAQ,gBAAgB,EAAE,CAAC;AAE/C,MAAI,KAAK,IACP,kBAAiB,GAAG,qBAAqB,UAAU;AACjD,OAAI,MAAM,QAAQ,QAAQ,KAAK,IAAK;AACpC,SAAKA,SACH,MAAM,QAAQ,MAAM,MACrB;AACD,QAAK,WACH,MAAM,QAAQ,QACf;IACD;;CAgBN,aAAa,aAAsD;AACjE,OAAK,MAAM,UAAU,UAAU;GAC7B,MAAM,gBAAgB;IACpB,GAAG;IACH,GAAG;IACJ;GACD,MAAM,EAAE,WAAW,aAAa,gBAAgB;AAChD,UAAO;IACL,GAAG;IACH,QAAQ,CAAC,MAAKF,YAAa,GACvB,aACA,YACE,YACA,cACE,cACA,cAAc,IACZ,YACA;IACX;IACD;AACF,aAAW,kBAAkB,KAAK;;;;;CAMpC,oBAA6B;AAC3B,SAAO,CAAC,CAAC,qBAAqB,KAAK,QAAQ,SAAS,KAAK;;;;;CAM3D,iBAAyB;AACvB,SAAO,qBAAqB,KAAK,QAAQ,MAAM,KAAK;;CA+FtD,WAAW,OACT,GAAG,SACsC;AACzC,MAAI,CAAC,MAAKA,YAAa,CAAE,QAAO;EAEhC,MAAM,sBAAsB,KAAK,MAAM,MAAM;AAE7C,MAAI;AACF,SAAKE,SAAU,EAAE,aAAa,MAAM,CAAC;GACrC,MAAM,sBAAsB,IAAI,aAAa,KAAK,IAAI;IACpD,GAAG,KAAK,QAAQ;IAChB,KAAK,GAAG,KAAK,IAAI,WAAW;IAC7B,CAAC;AACF,QAAK,cAAc,IAAI,qBAAqB,oBAAoB;GAChE,MAAM,SAAS,MAAM,oBAAoB,QAAQ,GAAG,KAAK;AACzD,SAAKA,SAAU;IACb,YAAY;IACZ,cAAc,KAAK,MAAM,MAAM,eAAe;IAC/C,CAAC;AACF,QAAK,QAAQ,YAAY,QAA2B,MAAM,KAAK;WACxD,OAAO;AACd,SAAKA,SAAU,EACb,YAAY,KAAK,MAAM,MAAM,aAAa,GAC3C,CAAC;AACF,QAAK,QAAQ,UAAU,OAAgB,MAAM,KAAK;AAClD,OAAI,KAAK,QAAQ,aACf,OAAM;YAEA;AACR,QAAK,cAAc,OAAO,oBAAoB;GAC9C,MAAM,oBAAoB,KAAK,KAAK;GACpC,MAAM,OAAO,MAAKC,SAAU;GAC5B,MAAM,oBAAoB,oBAAoB;AAC9C,SAAKD,SAAU;IACb,aAAa;IACb,WAAW,CAAC,CAAC,MAAKI;IAClB,aAAa,KAAK,MAAM,MAAM,cAAc;IAC5C;IACA;IACD,CAAC;AACF,QAAK,QAAQ,YAAY,MAAM,KAAK;AACpC,oBAAiB;AACf,QAAI,CAAC,KAAK,MAAM,MAAM,UAEpB,OAAKJ,SAAU,EAAE,mBAAmB,QAAW,CAAC;MAEjD,KAAK;;AAEV,SAAO,KAAK,MAAM,MAAM;;CA6B1B,wCAA8C;AAC5C,MAAI,MAAKG,wBAAyB;AAChC,SAAKA,uBAAwB,KAAK,MAAM,MAAM,WAAW;AACzD,SAAKA,yBAA0B;;;CAInC,sBAA4B;AAC1B,MAAI,MAAKC,WAAY;AACnB,gBAAa,MAAKA,UAAW;AAC7B,SAAKA,YAAa;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoIxB,SAAgB,cACd,IACA,gBACA;AAEA,QADuB,IAAI,eAAe,IAAI,eAAe,CACvC"}
{"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,1 +0,1 @@

{"version":3,"file":"batcher.cjs","names":["Store","#setState","#execute","#clearTimeout","#timeoutId","#getWait","parseFunctionOrValue"],"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: NodeJS.Timeout | 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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"}
{"version":3,"file":"batcher.cjs","names":["Store","#setState","#execute","#clearTimeout","#timeoutId","#getWait","parseFunctionOrValue"],"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":";;;;;;AAoCA,SAAS,yBAAuD;AAC9D,QAAO;EACL,gBAAgB;EAChB,SAAS;EACT,WAAW;EACX,qBAAqB;EACrB,OAAO,EAAE;EACT,MAAM;EACN,QAAQ;EACT;;AAqDH,MAAM,iBAA2D;CAC/D,wBAAwB;CACxB,SAAS;CACT,SAAS;CACT,MAAM;CACP;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAyCD,IAAa,UAAb,MAA6B;CAM3B,aAAmD;CAEnD,YACE,AAAO,IACP,gBACA;EAFO;eAR+C,IAAIA,sBAC1D,wBAAgC,CACjC;qBAgCa,eAAsD;AAClE,QAAK,UAAU;IAAE,GAAG,KAAK;IAAS,GAAG;IAAY;;kBA8BxC,SAAuB;AAChC,SAAKC,SAAU;IACb,OAAO,CAAC,GAAG,KAAK,MAAM,MAAM,OAAO,KAAK;IACxC,WAAW,KAAK,QAAQ,SAAS;IAClC,CAAC;AACF,QAAK,QAAQ,gBAAgB,KAAK;AAMlC,OAHE,KAAK,MAAM,MAAM,MAAM,UAAU,KAAK,QAAQ,WAC9C,KAAK,QAAQ,iBAAiB,KAAK,MAAM,MAAM,OAAO,KAAK,CAG3D,OAAKC,SAAU;YACN,KAAK,QAAQ,SAAS,UAAU;AACzC,UAAKC,cAAe;AACpB,UAAKC,YAAa,iBAAiB,MAAKF,SAAU,EAAE,MAAKG,SAAU,CAAC;;;qBAiCpD;AAClB,SAAKF,cAAe;AACpB,SAAKD,SAAU;;4BAMmB;AAClC,UAAO,CAAC,GAAG,KAAK,MAAM,MAAM,MAAM;;qBAahB;AAClB,SAAKD,SAAU;IAAE,OAAO,EAAE;IAAE,WAAW;IAAO,CAAC;;sBAO5B;AACnB,SAAKE,cAAe;AACpB,SAAKF,SAAU,EAAE,WAAW,OAAO,CAAC;;qBAMlB;AAClB,SAAKA,SAAU,wBAAgC,CAAC;AAChD,QAAK,QAAQ,gBAAgB,KAAK;;AA9IlC,OAAK,MAAM,eAAe;AAC1B,OAAK,UAAU;GACb,GAAG;GACH,GAAG;GACJ;AACD,QAAKA,SAAU,KAAK,QAAQ,gBAAgB,EAAE,CAAC;AAE/C,MAAI,KAAK,IACP,uCAAiB,GAAG,cAAc,UAAU;AAC1C,OAAI,MAAM,QAAQ,QAAQ,KAAK,IAAK;AACpC,SAAKA,SACH,MAAM,QAAQ,MAAM,MACrB;AACD,QAAK,WACH,MAAM,QAAQ,QACf;IACD;;CAWN,aAAa,aAAkD;AAC7D,OAAK,MAAM,UAAU,UAAU;GAC7B,MAAM,gBAAgB;IACpB,GAAG;IACH,GAAG;IACJ;GACD,MAAM,EAAE,WAAW,UAAU;GAC7B,MAAM,OAAO,MAAM;GACnB,MAAM,UAAU,SAAS;AACzB,UAAO;IACL,GAAG;IACH;IACA;IACA,QAAQ,YAAY,YAAY;IACjC;IACD;AACF,kCAAW,WAAW,KAAK;;CAG7B,iBAAyB;AACvB,SAAOK,mCAAqB,KAAK,QAAQ,MAAM,KAAK;;;;;;;;;;;CAmCtD,iBAAuB;AACrB,MAAI,KAAK,MAAM,MAAM,MAAM,WAAW,EACpC;EAGF,MAAM,QAAQ,KAAK,cAAc;AACjC,OAAK,OAAO;AACZ,OAAK,QAAQ,gBAAgB,KAAK;AAElC,OAAK,GAAG,MAAM;AACd,QAAKL,SAAU;GACb,gBAAgB,KAAK,MAAM,MAAM,iBAAiB;GAClD,qBAAqB,KAAK,MAAM,MAAM,sBAAsB,MAAM;GACnE,CAAC;AACF,OAAK,QAAQ,YAAY,OAAO,KAAK;;CAkBvC,sBAA4B;AAC1B,MAAI,MAAKG,WAAY;AACnB,gBAAa,MAAKA,UAAW;AAC7B,SAAKA,YAAa;;;;;;;;;;;;;;;;;;;;;;;;AAiDxB,SAAgB,MACd,IACA,SACA;AAEA,QADgB,IAAI,QAAgB,IAAI,QAAQ,CACjC"}

@@ -1,1 +0,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: NodeJS.Timeout | 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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"}
{"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 +0,1 @@

{"version":3,"file":"debouncer.cjs","names":["Store","#getEnabled","#setState","#execute","#timeoutId","#getWait","#clearTimeout","parseFunctionOrValue"],"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: NodeJS.Timeout | 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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"}
{"version":3,"file":"debouncer.cjs","names":["Store","#getEnabled","#setState","#execute","#timeoutId","#getWait","#clearTimeout","parseFunctionOrValue"],"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":";;;;;;AAgCA,SAAS,2BAEgB;AACvB,QAAO;EACL,mBAAmB;EACnB,gBAAgB;EAChB,WAAW;EACX,UAAU;EACV,QAAQ;EACR,mBAAmB;EACpB;;;;;AAgDH,SAAgB,iBAKd,SAA6B;AAC7B,QAAO;;AAGT,MAAM,iBAGF;CACF,SAAS;CACT,SAAS;CACT,UAAU;CACV,MAAM;CACP;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkCD,IAAa,YAAb,MAAgD;CAM9C;CAEA,YACE,AAAO,IACP,gBACA;EAFO;eAR8C,IAAIA,sBACzD,0BAA+B,CAChC;qBA8Ba,eAAqD;AACjE,QAAK,UAAU;IAAE,GAAG,KAAK;IAAS,GAAG;IAAY;AAGjD,OAAI,CAAC,MAAKC,YAAa,CACrB,MAAK,QAAQ;;uBAyCD,GAAG,SAAgC;AACjD,OAAI,CAAC,MAAKA,YAAa,CAAE,QAAO;AAEhC,SAAKC,SAAU,EACb,mBAAmB,KAAK,MAAM,MAAM,oBAAoB,GACzD,CAAC;GAEF,IAAI,qBAAqB;AAGzB,OAAI,KAAK,QAAQ,WAAW,KAAK,MAAM,MAAM,mBAAmB;AAC9D,UAAKA,SAAU,EAAE,mBAAmB,OAAO,CAAC;AAC5C,yBAAqB;AACrB,UAAKC,QAAS,GAAG,KAAK;;AAIxB,OAAI,KAAK,QAAQ,SACf,OAAKD,SAAU;IAAE,WAAW;IAAM,UAAU;IAAM,CAAC;AAIrD,OAAI,MAAKE,UAAY,cAAa,MAAKA,UAAW;AAGlD,SAAKA,YAAa,iBAAiB;AACjC,UAAKF,SAAU,EAAE,mBAAmB,MAAM,CAAC;AAC3C,QAAI,KAAK,QAAQ,YAAY,CAAC,mBAC5B,OAAKC,QAAS,GAAG,KAAK;MAEvB,MAAKE,SAAU,CAAC;;qBAiBD;AAClB,OAAI,KAAK,MAAM,MAAM,aAAa,KAAK,MAAM,MAAM,UAAU;AAC3D,UAAKC,cAAe;AACpB,UAAKH,QAAS,GAAG,KAAK,MAAM,MAAM,SAAS;;;sBAc1B;AACnB,SAAKG,cAAe;AACpB,SAAKJ,SAAU;IACb,mBAAmB;IACnB,WAAW;IACZ,CAAC;;qBAMgB;AAClB,SAAKA,SAAU,0BAA+B,CAAC;;AA/I/C,OAAK,MAAM,eAAe;AAC1B,OAAK,UAAU;GACb,GAAG;GACH,GAAG;GACJ;AACD,QAAKA,SAAU,KAAK,QAAQ,gBAAgB,EAAE,CAAC;AAE/C,MAAI,KAAK,IACP,uCAAiB,GAAG,gBAAgB,UAAU;AAC5C,OAAI,MAAM,QAAQ,QAAQ,KAAK,IAAK;AACpC,SAAKA,SACH,MAAM,QAAQ,MAAM,MACrB;AACD,QAAK,WAAW,MAAM,QAAQ,QAA0C;IACxE;;CAgBN,aAAa,aAAiD;AAC5D,OAAK,MAAM,UAAU,UAAU;GAC7B,MAAM,gBAAgB;IACpB,GAAG;IACH,GAAG;IACJ;GACD,MAAM,EAAE,cAAc;AACtB,UAAO;IACL,GAAG;IACH,QAAQ,CAAC,MAAKD,YAAa,GACvB,aACA,YACE,YACA;IACP;IACD;AACF,kCAAW,aAAa,KAAK;;;;;CAM/B,oBAA6B;AAC3B,SAAO,CAAC,CAACM,mCAAqB,KAAK,QAAQ,SAAS,KAAK;;;;;CAM3D,iBAAyB;AACvB,SAAOA,mCAAqB,KAAK,QAAQ,MAAM,KAAK;;CAwCtD,YAAY,GAAG,SAAgC;AAC7C,MAAI,CAAC,MAAKN,YAAa,CAAE,QAAO;AAChC,OAAK,GAAG,GAAG,KAAK;AAChB,QAAKC,SAAU;GACb,gBAAgB,KAAK,MAAM,MAAM,iBAAiB;GAClD,WAAW;GACX,UAAU;GACX,CAAC;AACF,OAAK,QAAQ,YAAY,MAAM,KAAK;;CAatC,sBAA4B;AAC1B,MAAI,MAAKE,WAAY;AACnB,gBAAa,MAAKA,UAAW;AAC7B,SAAKA,YAAa;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkDxB,SAAgB,SACd,IACA,gBACoC;AAEpC,QADkB,IAAI,UAAU,IAAI,eAAe,CAClC"}

@@ -1,1 +0,1 @@

{"version":3,"file":"debouncer.js","names":["#getEnabled","#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: NodeJS.Timeout | 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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"}
{"version":3,"file":"debouncer.js","names":["#getEnabled","#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,1 +0,1 @@

{"version":3,"file":"queuer.cjs","names":["Store","#setState","#tick","#clearTimeout","#getAllItems","parseFunctionOrValue","#checkExpiredItems","#getWait","#timeoutId"],"sources":["../src/queuer.ts"],"sourcesContent":["import { Store } from '@tanstack/store'\nimport { parseFunctionOrValue } from './utils'\nimport { emitChange, pacerEventClient } from './event-client'\n\nexport interface QueuerState<TValue> {\n /**\n * Number of times addItem has been called (for reduction calculations)\n */\n addItemCount: number\n /**\n * Number of items that have been processed by the queuer\n */\n executionCount: number\n /**\n * Number of items that have been removed from the queue due to expiration\n */\n expirationCount: number\n /**\n * Whether the queuer has no items to process (items array is empty)\n */\n isEmpty: boolean\n /**\n * Whether the queuer has reached its maximum capacity\n */\n isFull: boolean\n /**\n * Whether the queuer is not currently processing any items\n */\n isIdle: boolean\n /**\n * Whether the queuer is active and will process items automatically\n */\n isRunning: boolean\n /**\n * Array of items currently waiting to be processed\n */\n items: Array<TValue>\n /**\n * Timestamps when items were added to the queue for expiration tracking\n */\n itemTimestamps: Array<number>\n /**\n * Whether the queuer has a pending timeout for processing the next item\n */\n pendingTick: boolean\n /**\n * Number of items that have been rejected from being added to the queue\n */\n rejectionCount: number\n /**\n * Number of items currently in the queue\n */\n size: number\n /**\n * Current processing status - 'idle' when not processing, 'running' when active, 'stopped' when paused\n */\n status: 'idle' | 'running' | 'stopped'\n}\n\nfunction getDefaultQueuerState<TValue>(): QueuerState<TValue> {\n return {\n executionCount: 0,\n expirationCount: 0,\n isEmpty: true,\n isFull: false,\n isIdle: true,\n isRunning: true,\n itemTimestamps: [],\n items: [],\n pendingTick: false,\n rejectionCount: 0,\n size: 0,\n status: 'idle',\n addItemCount: 0,\n }\n}\n\n/**\n * Options for configuring a Queuer instance.\n *\n * These options control queue behavior, item expiration, callbacks, and more.\n */\nexport interface QueuerOptions<TValue> {\n /**\n * Default position to add items to the queuer\n * @default 'back'\n */\n addItemsTo?: QueuePosition\n /**\n * Maximum time in milliseconds that an item can stay in the queue\n * If not provided, items will never expire\n */\n expirationDuration?: number\n /**\n * Function to determine if an item has expired\n * If provided, this overrides the expirationDuration behavior\n */\n getIsExpired?: (item: TValue, addedAt: number) => boolean\n /**\n * Default position to get items from during processing\n * @default 'front'\n */\n getItemsFrom?: QueuePosition\n /**\n * Function to determine priority of items in the queuer\n * Higher priority items will be processed first\n */\n getPriority?: (item: TValue) => number\n /**\n * Initial items to populate the queuer with\n */\n initialItems?: Array<TValue>\n /**\n * Initial state for the queuer\n */\n initialState?: Partial<QueuerState<TValue>>\n /**\n * Optional key to identify this queuer instance.\n * If provided, the queuer will be identified by this key in the devtools and PacerProvider if applicable.\n */\n key?: string\n /**\n * Maximum number of items allowed in the queuer\n */\n maxSize?: number\n /**\n * Callback fired whenever an item is removed from the queuer\n */\n onExecute?: (item: TValue, queuer: Queuer<TValue>) => void\n /**\n * Callback fired whenever an item expires in the queuer\n */\n onExpire?: (item: TValue, queuer: Queuer<TValue>) => void\n /**\n * Callback fired whenever an item is added or removed from the queuer\n */\n onItemsChange?: (queuer: Queuer<TValue>) => void\n /**\n * Callback fired whenever an item is rejected from being added to the queuer\n */\n onReject?: (item: TValue, queuer: Queuer<TValue>) => void\n /**\n * Whether the queuer should start processing tasks immediately\n */\n started?: boolean\n /**\n * Time in milliseconds to wait between processing items.\n * Can be a number or a function that returns a number.\n * @default 0\n */\n wait?: number | ((queuer: Queuer<TValue>) => number)\n}\n\n/**\n * Utility function for sharing common `QueuerOptions` options between different `Queuer` instances.\n */\nexport function queuerOptions<\n TValue = any,\n TOptions extends Partial<QueuerOptions<TValue>> = Partial<\n QueuerOptions<TValue>\n >,\n>(options: TOptions): TOptions {\n return options\n}\n\nconst defaultOptions: Omit<\n Required<QueuerOptions<any>>,\n | 'initialState'\n | 'onExecute'\n | 'onIsRunningChange'\n | 'onItemsChange'\n | 'onReject'\n | 'onExpire'\n | 'key'\n> = {\n addItemsTo: 'back',\n getItemsFrom: 'front',\n getPriority: (item) => item?.priority ?? 0,\n getIsExpired: () => false,\n expirationDuration: Infinity,\n initialItems: [],\n maxSize: Infinity,\n started: true,\n wait: 0,\n}\n\n/**\n * Position type for addItem and getNextItem operations.\n *\n * - 'front': Operate on the front of the queue (FIFO)\n * - 'back': Operate on the back of the queue (LIFO)\n */\nexport type QueuePosition = 'front' | 'back'\n\n/**\n * A flexible queue that processes items with configurable wait times, expiration, and priority.\n *\n * This synchronous version is lighter weight and often all you need - upgrade to AsyncQueuer when you need promises, retry support, abort capabilities, concurrent execution, or advanced error handling.\n *\n * Features:\n * - Automatic or manual processing of items\n * - FIFO (First In First Out), LIFO (Last In First Out), or double-ended queue behavior\n * - Priority-based ordering when getPriority is provided\n * - Item expiration and removal of stale items\n * - Callbacks for queue state changes, execution, rejection, and expiration\n *\n * Running behavior:\n * - `start()`: Begins automatically processing items in the queue (defaults to isRunning)\n * - `stop()`: Pauses processing but maintains queue state\n * - `wait`: Configurable delay between processing items\n * - `onItemsChange`/`onExecute`: Callbacks for monitoring queue state\n *\n * Manual processing is also supported when automatic processing is disabled:\n * - `execute()`: Processes the next item using the provided function\n * - `getNextItem()`: Removes and returns the next item without processing\n *\n * Queue behavior defaults to FIFO:\n * - `addItem(item)`: Adds to the back of the queue\n * - Items processed from the front of the queue\n *\n * Priority queue:\n * - Provide a `getPriority` function; higher values are processed first\n *\n * Stack (LIFO):\n * - `addItem(item, 'back')`: Adds to the back\n * - `getNextItem('back')`: Removes from the back\n *\n * Double-ended queue:\n * - `addItem(item, position)`: Adds to specified position ('front'/'back')\n * - `getNextItem(position)`: Removes from specified position\n *\n * Item expiration:\n * - `expirationDuration`: Maximum time items can stay in the queue\n * - `getIsExpired`: Function to override default expiration\n * - `onExpire`: Callback for expired items\n *\n * State Management:\n * - Uses TanStack Store for reactive state management\n * - Use `initialState` to provide initial state values when creating the queuer\n * - Use `onExecute` callback to react to item execution and implement custom logic\n * - Use `onItemsChange` callback to react to items being added or removed from the queue\n * - Use `onExpire` callback to react to items expiring and implement custom logic\n * - Use `onReject` callback to react to items being rejected when the queue is full\n * - The state includes execution count, expiration count, rejection count, and isRunning status\n * - State can be accessed via `queuer.store.state` when using the class directly\n * - When using framework adapters (React/Solid), state is accessed from `queuer.state`\n *\n * Example usage:\n * ```ts\n * // Auto-processing queue with wait time\n * const autoQueue = new Queuer<number>((n) => console.log(n), {\n * started: true, // Begin processing immediately\n * wait: 1000, // Wait 1s between items\n * onExecute: (item, queuer) => console.log(`Processed ${item}`)\n * });\n * autoQueue.addItem(1); // Will process after 1s\n * autoQueue.addItem(2); // Will process 1s after first item\n *\n * // Manual processing queue\n * const manualQueue = new Queuer<number>((n) => console.log(n), {\n * started: false\n * });\n * manualQueue.addItem(1); // [1]\n * manualQueue.addItem(2); // [1, 2]\n * manualQueue.execute(); // logs 1, queue is [2]\n * manualQueue.getNextItem(); // returns 2, queue is empty\n * ```\n */\nexport class Queuer<TValue> {\n readonly store: Store<Readonly<QueuerState<TValue>>> = new Store(\n getDefaultQueuerState<TValue>(),\n )\n key: string | undefined\n options: QueuerOptions<TValue>\n #timeoutId: NodeJS.Timeout | null = null\n\n constructor(\n public fn: (item: TValue) => void,\n initialOptions: QueuerOptions<TValue> = {},\n ) {\n this.key = initialOptions.key\n this.options = {\n ...defaultOptions,\n ...initialOptions,\n }\n const isInitiallyRunning =\n this.options.initialState?.isRunning ?? this.options.started ?? true\n this.#setState({\n ...this.options.initialState,\n isRunning: isInitiallyRunning,\n })\n\n if (this.options.initialState?.items) {\n if (this.store.state.isRunning) {\n this.#tick()\n }\n } else {\n for (let i = 0; i < (this.options.initialItems?.length ?? 0); i++) {\n const item = this.options.initialItems![i]!\n const isLast = i === (this.options.initialItems?.length ?? 0) - 1\n this.addItem(item, this.options.addItemsTo ?? 'back', isLast)\n }\n }\n\n if (this.key) {\n pacerEventClient.on('d-Queuer', (event) => {\n if (event.payload.key !== this.key) return\n this.#setState(\n event.payload.store.state as Partial<QueuerState<TValue>>,\n )\n this.setOptions(event.payload.options as Partial<QueuerOptions<TValue>>)\n })\n }\n }\n\n /**\n * Updates the queuer options. New options are merged with existing options.\n */\n setOptions = (newOptions: Partial<QueuerOptions<TValue>>): void => {\n this.options = { ...this.options, ...newOptions }\n }\n\n #setState = (newState: Partial<QueuerState<TValue>>): void => {\n this.store.setState((state) => {\n const combinedState = {\n ...state,\n ...newState,\n }\n\n const { items, isRunning } = combinedState\n\n const size = items.length\n const isFull = size >= (this.options.maxSize ?? Infinity)\n const isEmpty = size === 0\n const isIdle = isRunning && isEmpty\n\n const status = isIdle ? 'idle' : isRunning ? 'running' : 'stopped'\n\n return {\n ...combinedState,\n isEmpty,\n isFull,\n isIdle,\n size,\n status,\n }\n })\n emitChange('Queuer', this)\n }\n\n /**\n * Returns the current wait time (in milliseconds) between processing items.\n * If a function is provided, it is called with the queuer instance.\n */\n #getWait = (): number => {\n return parseFunctionOrValue(this.options.wait ?? 0, this)\n }\n\n /**\n * Processes items in the queue up to the wait interval. Internal use only.\n */\n #tick = () => {\n if (!this.store.state.isRunning) {\n this.#setState({ pendingTick: false })\n return\n }\n\n this.#setState({ pendingTick: true })\n\n // Check for expired items\n this.#checkExpiredItems()\n\n while (this.store.state.items.length > 0) {\n const nextItem = this.execute(this.options.getItemsFrom ?? 'front')\n if (nextItem === undefined) {\n break\n }\n\n const wait = this.#getWait()\n if (wait > 0) {\n // Use setTimeout to wait before processing next item\n this.#timeoutId = setTimeout(() => this.#tick(), wait)\n return\n }\n\n this.#tick()\n }\n this.#setState({ pendingTick: false })\n }\n\n /**\n * Adds an item to the queue. If the queue is full, the item is rejected and onReject is called.\n * Items can be inserted based on priority or at the front/back depending on configuration.\n *\n * Returns true if the item was added, false if the queue is full.\n *\n * Example usage:\n * ```ts\n * queuer.addItem('task');\n * queuer.addItem('task2', 'front');\n * ```\n */\n addItem = (\n item: TValue,\n position: QueuePosition = this.options.addItemsTo ?? 'back',\n runOnItemsChange: boolean = true,\n ): boolean => {\n this.#setState({\n addItemCount: this.store.state.addItemCount + 1,\n })\n\n if (this.store.state.items.length >= (this.options.maxSize ?? Infinity)) {\n this.#setState({\n rejectionCount: this.store.state.rejectionCount + 1,\n })\n this.options.onReject?.(item, this)\n return false\n }\n\n // Get priority either from the function or from getPriority option\n const priority =\n this.options.getPriority !== defaultOptions.getPriority\n ? this.options.getPriority!(item)\n : (item as any).priority\n\n const items = this.store.state.items\n const itemTimestamps = this.store.state.itemTimestamps\n\n if (priority !== undefined) {\n // Insert based on priority - higher priority items go to front\n const insertIndex = items.findIndex((existing) => {\n const existingPriority: number =\n this.options.getPriority !== defaultOptions.getPriority\n ? this.options.getPriority!(existing)\n : (existing as any).priority\n return existingPriority < priority\n })\n\n if (insertIndex === -1) {\n items.push(item)\n itemTimestamps.push(Date.now())\n } else {\n items.splice(insertIndex, 0, item)\n itemTimestamps.splice(insertIndex, 0, Date.now())\n }\n } else {\n if (position === 'front') {\n // Default FIFO/LIFO behavior\n items.unshift(item)\n itemTimestamps.unshift(Date.now())\n } else {\n // LIFO\n items.push(item)\n itemTimestamps.push(Date.now())\n }\n }\n\n this.#setState({\n items,\n itemTimestamps,\n })\n\n if (runOnItemsChange) {\n this.options.onItemsChange?.(this)\n }\n\n if (this.store.state.isRunning && !this.store.state.pendingTick) {\n this.#setState({ pendingTick: true })\n this.#tick()\n }\n\n return true\n }\n\n /**\n * Removes and returns the next item from the queue without executing the function.\n * Use for manual queue management. Normally, use execute() to process items.\n *\n * Example usage:\n * ```ts\n * // FIFO\n * queuer.getNextItem();\n * // LIFO\n * queuer.getNextItem('back');\n * ```\n */\n getNextItem = (\n position: QueuePosition = this.options.getItemsFrom ?? 'front',\n ): TValue | undefined => {\n const { items, itemTimestamps } = this.store.state\n let item: TValue | undefined\n\n // When priority function is provided, always get from front (highest priority)\n // Priority takes precedence over FIFO/LIFO behavior\n if (\n this.options.getPriority !== defaultOptions.getPriority ||\n position === 'front'\n ) {\n item = items[0]\n if (item !== undefined) {\n this.#setState({\n items: items.slice(1),\n itemTimestamps: itemTimestamps.slice(1),\n })\n }\n } else {\n item = items[items.length - 1]\n if (item !== undefined) {\n this.#setState({\n items: items.slice(0, -1),\n itemTimestamps: itemTimestamps.slice(0, -1),\n })\n }\n }\n\n if (item !== undefined) {\n this.options.onItemsChange?.(this)\n }\n\n return item\n }\n\n #getAllItems = (): Array<TValue> => {\n const items = this.peekAllItems()\n this.clear()\n return items\n }\n\n /**\n * Removes and returns the next item from the queue and processes it using the provided function.\n *\n * Example usage:\n * ```ts\n * queuer.execute();\n * // LIFO\n * queuer.execute('back');\n * ```\n */\n execute = (position?: QueuePosition): TValue | undefined => {\n const item = this.getNextItem(position)\n if (item !== undefined) {\n this.fn(item)\n this.#setState({\n executionCount: this.store.state.executionCount + 1,\n })\n this.options.onExecute?.(item, this)\n }\n return item\n }\n\n /**\n * Processes a specified number of items to execute immediately with no wait time\n * If no numberOfItems is provided, all items will be processed\n */\n flush = (\n numberOfItems: number = this.store.state.items.length,\n position?: QueuePosition,\n ): void => {\n this.#clearTimeout() // clear any pending timeout\n for (let i = 0; i < numberOfItems; i++) {\n this.execute(position)\n }\n this.#tick()\n }\n\n /**\n * Processes all items in the queue as a batch using the provided function as an argument\n * The queue is cleared after processing\n */\n flushAsBatch = (batchFunction: (items: Array<TValue>) => void): void => {\n const items = this.#getAllItems()\n this.clear()\n batchFunction(items)\n }\n\n /**\n * Checks for expired items in the queue and removes them. Calls onExpire for each expired item.\n * Internal use only.\n */\n #checkExpiredItems = (): void => {\n if (\n (this.options.expirationDuration ?? Infinity) === Infinity &&\n this.options.getIsExpired === defaultOptions.getIsExpired\n ) {\n return\n }\n\n const now = Date.now()\n const expiredIndices: Array<number> = []\n\n // Find indices of expired items\n for (let i = 0; i < this.store.state.items.length; i++) {\n const timestamp = this.store.state.itemTimestamps[i]\n if (timestamp === undefined) continue\n\n const item = this.store.state.items[i]\n if (item === undefined) continue\n\n const isExpired =\n this.options.getIsExpired !== defaultOptions.getIsExpired\n ? this.options.getIsExpired!(item, timestamp)\n : now - timestamp > (this.options.expirationDuration ?? Infinity)\n\n if (isExpired) {\n expiredIndices.push(i)\n }\n }\n\n // Remove expired items from back to front to maintain indices\n for (let i = expiredIndices.length - 1; i >= 0; i--) {\n const index = expiredIndices[i]\n if (index === undefined) continue\n\n const expiredItem = this.store.state.items[index]\n if (expiredItem === undefined) continue\n\n const newItems = [...this.store.state.items]\n const newTimestamps = [...this.store.state.itemTimestamps]\n newItems.splice(index, 1)\n newTimestamps.splice(index, 1)\n this.#setState({\n items: newItems,\n itemTimestamps: newTimestamps,\n expirationCount: this.store.state.expirationCount + 1,\n })\n this.options.onExpire?.(expiredItem, this)\n }\n\n if (expiredIndices.length > 0) {\n this.options.onItemsChange?.(this)\n }\n }\n\n /**\n * Returns the next item in the queue without removing it.\n *\n * Example usage:\n * ```ts\n * queuer.peekNextItem(); // front\n * queuer.peekNextItem('back'); // back\n * ```\n */\n peekNextItem = (position: QueuePosition = 'front'): TValue | undefined => {\n if (position === 'front') {\n return this.store.state.items[0]\n }\n return this.store.state.items[this.store.state.items.length - 1]\n }\n\n /**\n * Returns a copy of all items in the queue.\n */\n peekAllItems = (): Array<TValue> => {\n return [...this.store.state.items]\n }\n\n /**\n * Starts processing items in the queue. If already isRunning, does nothing.\n */\n start = () => {\n this.#setState({ isRunning: true })\n if (!this.store.state.pendingTick && this.store.state.items.length > 0) {\n this.#tick()\n }\n }\n\n /**\n * Stops processing items in the queue. Does not clear the queue.\n */\n stop = () => {\n this.#clearTimeout()\n this.#setState({ isRunning: false, pendingTick: false })\n }\n\n #clearTimeout = (): void => {\n if (this.#timeoutId) {\n clearTimeout(this.#timeoutId)\n this.#timeoutId = null\n }\n }\n\n /**\n * Removes all pending items from the queue. Does not affect items being processed.\n */\n clear = (): void => {\n this.#setState({ items: [], itemTimestamps: [] })\n this.options.onItemsChange?.(this)\n }\n\n /**\n * Resets the queuer state to its default values\n */\n reset = (): void => {\n this.#setState(getDefaultQueuerState<TValue>())\n this.options.onItemsChange?.(this)\n }\n}\n\n/**\n * Creates a queue that processes items immediately upon addition.\n * Items are processed sequentially in FIFO order by default.\n *\n * This synchronous version is lighter weight and often all you need - upgrade to asyncQueue when you need promises, retry support, abort capabilities, concurrent execution, or advanced error handling.\n *\n * State Management:\n * - Uses TanStack Store for reactive state management\n * - Use `initialState` to provide initial state values when creating the queuer\n * - Use `onExecute` callback to react to item execution and implement custom logic\n * - Use `onItemsChange` callback to react to items being added or removed from the queue\n * - Use `onExpire` callback to react to items expiring and implement custom logic\n * - Use `onReject` callback to react to items being rejected when the queue is full\n * - The state includes execution count, expiration count, rejection count, and isRunning status\n * - State can be accessed via the underlying Queuer instance's `store.state` property\n * - When using framework adapters (React/Solid), state is accessed from the hook's state property\n *\n * Example usage:\n * ```ts\n * // Basic sequential processing\n * const processItems = queue<number>((n) => console.log(n), {\n * wait: 1000,\n * onItemsChange: (queuer) => console.log(queuer.peekAllItems())\n * });\n * processItems(1); // Logs: 1\n * processItems(2); // Logs: 2 after 1 completes\n *\n * // Priority queue\n * const processPriority = queue<number>((n) => console.log(n), {\n * getPriority: n => n // Higher numbers processed first\n * });\n * processPriority(1);\n * processPriority(3); // Processed before 1\n * ```\n */\nexport function queue<TValue>(\n fn: (item: TValue) => void,\n initialOptions: QueuerOptions<TValue>,\n) {\n const queuer = new Queuer<TValue>(fn, initialOptions)\n return queuer.addItem\n}\n"],"mappings":"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"}
{"version":3,"file":"queuer.cjs","names":["Store","#setState","#tick","#clearTimeout","#getAllItems","parseFunctionOrValue","#checkExpiredItems","#getWait","#timeoutId"],"sources":["../src/queuer.ts"],"sourcesContent":["import { Store } from '@tanstack/store'\nimport { parseFunctionOrValue } from './utils'\nimport { emitChange, pacerEventClient } from './event-client'\n\nexport interface QueuerState<TValue> {\n /**\n * Number of times addItem has been called (for reduction calculations)\n */\n addItemCount: number\n /**\n * Number of items that have been processed by the queuer\n */\n executionCount: number\n /**\n * Number of items that have been removed from the queue due to expiration\n */\n expirationCount: number\n /**\n * Whether the queuer has no items to process (items array is empty)\n */\n isEmpty: boolean\n /**\n * Whether the queuer has reached its maximum capacity\n */\n isFull: boolean\n /**\n * Whether the queuer is not currently processing any items\n */\n isIdle: boolean\n /**\n * Whether the queuer is active and will process items automatically\n */\n isRunning: boolean\n /**\n * Array of items currently waiting to be processed\n */\n items: Array<TValue>\n /**\n * Timestamps when items were added to the queue for expiration tracking\n */\n itemTimestamps: Array<number>\n /**\n * Whether the queuer has a pending timeout for processing the next item\n */\n pendingTick: boolean\n /**\n * Number of items that have been rejected from being added to the queue\n */\n rejectionCount: number\n /**\n * Number of items currently in the queue\n */\n size: number\n /**\n * Current processing status - 'idle' when not processing, 'running' when active, 'stopped' when paused\n */\n status: 'idle' | 'running' | 'stopped'\n}\n\nfunction getDefaultQueuerState<TValue>(): QueuerState<TValue> {\n return {\n executionCount: 0,\n expirationCount: 0,\n isEmpty: true,\n isFull: false,\n isIdle: true,\n isRunning: true,\n itemTimestamps: [],\n items: [],\n pendingTick: false,\n rejectionCount: 0,\n size: 0,\n status: 'idle',\n addItemCount: 0,\n }\n}\n\n/**\n * Options for configuring a Queuer instance.\n *\n * These options control queue behavior, item expiration, callbacks, and more.\n */\nexport interface QueuerOptions<TValue> {\n /**\n * Default position to add items to the queuer\n * @default 'back'\n */\n addItemsTo?: QueuePosition\n /**\n * Maximum time in milliseconds that an item can stay in the queue\n * If not provided, items will never expire\n */\n expirationDuration?: number\n /**\n * Function to determine if an item has expired\n * If provided, this overrides the expirationDuration behavior\n */\n getIsExpired?: (item: TValue, addedAt: number) => boolean\n /**\n * Default position to get items from during processing\n * @default 'front'\n */\n getItemsFrom?: QueuePosition\n /**\n * Function to determine priority of items in the queuer\n * Higher priority items will be processed first\n */\n getPriority?: (item: TValue) => number\n /**\n * Initial items to populate the queuer with\n */\n initialItems?: Array<TValue>\n /**\n * Initial state for the queuer\n */\n initialState?: Partial<QueuerState<TValue>>\n /**\n * Optional key to identify this queuer instance.\n * If provided, the queuer will be identified by this key in the devtools and PacerProvider if applicable.\n */\n key?: string\n /**\n * Maximum number of items allowed in the queuer\n */\n maxSize?: number\n /**\n * Callback fired whenever an item is removed from the queuer\n */\n onExecute?: (item: TValue, queuer: Queuer<TValue>) => void\n /**\n * Callback fired whenever an item expires in the queuer\n */\n onExpire?: (item: TValue, queuer: Queuer<TValue>) => void\n /**\n * Callback fired whenever an item is added or removed from the queuer\n */\n onItemsChange?: (queuer: Queuer<TValue>) => void\n /**\n * Callback fired whenever an item is rejected from being added to the queuer\n */\n onReject?: (item: TValue, queuer: Queuer<TValue>) => void\n /**\n * Whether the queuer should start processing tasks immediately\n */\n started?: boolean\n /**\n * Time in milliseconds to wait between processing items.\n * Can be a number or a function that returns a number.\n * @default 0\n */\n wait?: number | ((queuer: Queuer<TValue>) => number)\n}\n\n/**\n * Utility function for sharing common `QueuerOptions` options between different `Queuer` instances.\n */\nexport function queuerOptions<\n TValue = any,\n TOptions extends Partial<QueuerOptions<TValue>> = Partial<\n QueuerOptions<TValue>\n >,\n>(options: TOptions): TOptions {\n return options\n}\n\nconst defaultOptions: Omit<\n Required<QueuerOptions<any>>,\n | 'initialState'\n | 'onExecute'\n | 'onIsRunningChange'\n | 'onItemsChange'\n | 'onReject'\n | 'onExpire'\n | 'key'\n> = {\n addItemsTo: 'back',\n getItemsFrom: 'front',\n getPriority: (item) => item?.priority ?? 0,\n getIsExpired: () => false,\n expirationDuration: Infinity,\n initialItems: [],\n maxSize: Infinity,\n started: true,\n wait: 0,\n}\n\n/**\n * Position type for addItem and getNextItem operations.\n *\n * - 'front': Operate on the front of the queue (FIFO)\n * - 'back': Operate on the back of the queue (LIFO)\n */\nexport type QueuePosition = 'front' | 'back'\n\n/**\n * A flexible queue that processes items with configurable wait times, expiration, and priority.\n *\n * This synchronous version is lighter weight and often all you need - upgrade to AsyncQueuer when you need promises, retry support, abort capabilities, concurrent execution, or advanced error handling.\n *\n * Features:\n * - Automatic or manual processing of items\n * - FIFO (First In First Out), LIFO (Last In First Out), or double-ended queue behavior\n * - Priority-based ordering when getPriority is provided\n * - Item expiration and removal of stale items\n * - Callbacks for queue state changes, execution, rejection, and expiration\n *\n * Running behavior:\n * - `start()`: Begins automatically processing items in the queue (defaults to isRunning)\n * - `stop()`: Pauses processing but maintains queue state\n * - `wait`: Configurable delay between processing items\n * - `onItemsChange`/`onExecute`: Callbacks for monitoring queue state\n *\n * Manual processing is also supported when automatic processing is disabled:\n * - `execute()`: Processes the next item using the provided function\n * - `getNextItem()`: Removes and returns the next item without processing\n *\n * Queue behavior defaults to FIFO:\n * - `addItem(item)`: Adds to the back of the queue\n * - Items processed from the front of the queue\n *\n * Priority queue:\n * - Provide a `getPriority` function; higher values are processed first\n *\n * Stack (LIFO):\n * - `addItem(item, 'back')`: Adds to the back\n * - `getNextItem('back')`: Removes from the back\n *\n * Double-ended queue:\n * - `addItem(item, position)`: Adds to specified position ('front'/'back')\n * - `getNextItem(position)`: Removes from specified position\n *\n * Item expiration:\n * - `expirationDuration`: Maximum time items can stay in the queue\n * - `getIsExpired`: Function to override default expiration\n * - `onExpire`: Callback for expired items\n *\n * State Management:\n * - Uses TanStack Store for reactive state management\n * - Use `initialState` to provide initial state values when creating the queuer\n * - Use `onExecute` callback to react to item execution and implement custom logic\n * - Use `onItemsChange` callback to react to items being added or removed from the queue\n * - Use `onExpire` callback to react to items expiring and implement custom logic\n * - Use `onReject` callback to react to items being rejected when the queue is full\n * - The state includes execution count, expiration count, rejection count, and isRunning status\n * - State can be accessed via `queuer.store.state` when using the class directly\n * - When using framework adapters (React/Solid), state is accessed from `queuer.state`\n *\n * Example usage:\n * ```ts\n * // Auto-processing queue with wait time\n * const autoQueue = new Queuer<number>((n) => console.log(n), {\n * started: true, // Begin processing immediately\n * wait: 1000, // Wait 1s between items\n * onExecute: (item, queuer) => console.log(`Processed ${item}`)\n * });\n * autoQueue.addItem(1); // Will process after 1s\n * autoQueue.addItem(2); // Will process 1s after first item\n *\n * // Manual processing queue\n * const manualQueue = new Queuer<number>((n) => console.log(n), {\n * started: false\n * });\n * manualQueue.addItem(1); // [1]\n * manualQueue.addItem(2); // [1, 2]\n * manualQueue.execute(); // logs 1, queue is [2]\n * manualQueue.getNextItem(); // returns 2, queue is empty\n * ```\n */\nexport class Queuer<TValue> {\n readonly store: Store<Readonly<QueuerState<TValue>>> = new Store(\n getDefaultQueuerState<TValue>(),\n )\n key: string | undefined\n options: QueuerOptions<TValue>\n #timeoutId: ReturnType<typeof setTimeout> | null = null\n\n constructor(\n public fn: (item: TValue) => void,\n initialOptions: QueuerOptions<TValue> = {},\n ) {\n this.key = initialOptions.key\n this.options = {\n ...defaultOptions,\n ...initialOptions,\n }\n const isInitiallyRunning =\n this.options.initialState?.isRunning ?? this.options.started ?? true\n this.#setState({\n ...this.options.initialState,\n isRunning: isInitiallyRunning,\n })\n\n if (this.options.initialState?.items) {\n if (this.store.state.isRunning) {\n this.#tick()\n }\n } else {\n for (let i = 0; i < (this.options.initialItems?.length ?? 0); i++) {\n const item = this.options.initialItems![i]!\n const isLast = i === (this.options.initialItems?.length ?? 0) - 1\n this.addItem(item, this.options.addItemsTo ?? 'back', isLast)\n }\n }\n\n if (this.key) {\n pacerEventClient.on('d-Queuer', (event) => {\n if (event.payload.key !== this.key) return\n this.#setState(\n event.payload.store.state as Partial<QueuerState<TValue>>,\n )\n this.setOptions(event.payload.options as Partial<QueuerOptions<TValue>>)\n })\n }\n }\n\n /**\n * Updates the queuer options. New options are merged with existing options.\n */\n setOptions = (newOptions: Partial<QueuerOptions<TValue>>): void => {\n this.options = { ...this.options, ...newOptions }\n }\n\n #setState = (newState: Partial<QueuerState<TValue>>): void => {\n this.store.setState((state) => {\n const combinedState = {\n ...state,\n ...newState,\n }\n\n const { items, isRunning } = combinedState\n\n const size = items.length\n const isFull = size >= (this.options.maxSize ?? Infinity)\n const isEmpty = size === 0\n const isIdle = isRunning && isEmpty\n\n const status = isIdle ? 'idle' : isRunning ? 'running' : 'stopped'\n\n return {\n ...combinedState,\n isEmpty,\n isFull,\n isIdle,\n size,\n status,\n }\n })\n emitChange('Queuer', this)\n }\n\n /**\n * Returns the current wait time (in milliseconds) between processing items.\n * If a function is provided, it is called with the queuer instance.\n */\n #getWait = (): number => {\n return parseFunctionOrValue(this.options.wait ?? 0, this)\n }\n\n /**\n * Processes items in the queue up to the wait interval. Internal use only.\n */\n #tick = () => {\n if (!this.store.state.isRunning) {\n this.#setState({ pendingTick: false })\n return\n }\n\n this.#setState({ pendingTick: true })\n\n // Check for expired items\n this.#checkExpiredItems()\n\n while (this.store.state.items.length > 0) {\n const nextItem = this.execute(this.options.getItemsFrom ?? 'front')\n if (nextItem === undefined) {\n break\n }\n\n const wait = this.#getWait()\n if (wait > 0) {\n // Use setTimeout to wait before processing next item\n this.#timeoutId = setTimeout(() => this.#tick(), wait)\n return\n }\n\n this.#tick()\n }\n this.#setState({ pendingTick: false })\n }\n\n /**\n * Adds an item to the queue. If the queue is full, the item is rejected and onReject is called.\n * Items can be inserted based on priority or at the front/back depending on configuration.\n *\n * Returns true if the item was added, false if the queue is full.\n *\n * Example usage:\n * ```ts\n * queuer.addItem('task');\n * queuer.addItem('task2', 'front');\n * ```\n */\n addItem = (\n item: TValue,\n position: QueuePosition = this.options.addItemsTo ?? 'back',\n runOnItemsChange: boolean = true,\n ): boolean => {\n this.#setState({\n addItemCount: this.store.state.addItemCount + 1,\n })\n\n if (this.store.state.items.length >= (this.options.maxSize ?? Infinity)) {\n this.#setState({\n rejectionCount: this.store.state.rejectionCount + 1,\n })\n this.options.onReject?.(item, this)\n return false\n }\n\n // Get priority either from the function or from getPriority option\n const priority =\n this.options.getPriority !== defaultOptions.getPriority\n ? this.options.getPriority!(item)\n : (item as any).priority\n\n const items = this.store.state.items\n const itemTimestamps = this.store.state.itemTimestamps\n\n if (priority !== undefined) {\n // Insert based on priority - higher priority items go to front\n const insertIndex = items.findIndex((existing) => {\n const existingPriority: number =\n this.options.getPriority !== defaultOptions.getPriority\n ? this.options.getPriority!(existing)\n : (existing as any).priority\n return existingPriority < priority\n })\n\n if (insertIndex === -1) {\n items.push(item)\n itemTimestamps.push(Date.now())\n } else {\n items.splice(insertIndex, 0, item)\n itemTimestamps.splice(insertIndex, 0, Date.now())\n }\n } else {\n if (position === 'front') {\n // Default FIFO/LIFO behavior\n items.unshift(item)\n itemTimestamps.unshift(Date.now())\n } else {\n // LIFO\n items.push(item)\n itemTimestamps.push(Date.now())\n }\n }\n\n this.#setState({\n items,\n itemTimestamps,\n })\n\n if (runOnItemsChange) {\n this.options.onItemsChange?.(this)\n }\n\n if (this.store.state.isRunning && !this.store.state.pendingTick) {\n this.#setState({ pendingTick: true })\n this.#tick()\n }\n\n return true\n }\n\n /**\n * Removes and returns the next item from the queue without executing the function.\n * Use for manual queue management. Normally, use execute() to process items.\n *\n * Example usage:\n * ```ts\n * // FIFO\n * queuer.getNextItem();\n * // LIFO\n * queuer.getNextItem('back');\n * ```\n */\n getNextItem = (\n position: QueuePosition = this.options.getItemsFrom ?? 'front',\n ): TValue | undefined => {\n const { items, itemTimestamps } = this.store.state\n let item: TValue | undefined\n\n // When priority function is provided, always get from front (highest priority)\n // Priority takes precedence over FIFO/LIFO behavior\n if (\n this.options.getPriority !== defaultOptions.getPriority ||\n position === 'front'\n ) {\n item = items[0]\n if (item !== undefined) {\n this.#setState({\n items: items.slice(1),\n itemTimestamps: itemTimestamps.slice(1),\n })\n }\n } else {\n item = items[items.length - 1]\n if (item !== undefined) {\n this.#setState({\n items: items.slice(0, -1),\n itemTimestamps: itemTimestamps.slice(0, -1),\n })\n }\n }\n\n if (item !== undefined) {\n this.options.onItemsChange?.(this)\n }\n\n return item\n }\n\n #getAllItems = (): Array<TValue> => {\n const items = this.peekAllItems()\n this.clear()\n return items\n }\n\n /**\n * Removes and returns the next item from the queue and processes it using the provided function.\n *\n * Example usage:\n * ```ts\n * queuer.execute();\n * // LIFO\n * queuer.execute('back');\n * ```\n */\n execute = (position?: QueuePosition): TValue | undefined => {\n const item = this.getNextItem(position)\n if (item !== undefined) {\n this.fn(item)\n this.#setState({\n executionCount: this.store.state.executionCount + 1,\n })\n this.options.onExecute?.(item, this)\n }\n return item\n }\n\n /**\n * Processes a specified number of items to execute immediately with no wait time\n * If no numberOfItems is provided, all items will be processed\n */\n flush = (\n numberOfItems: number = this.store.state.items.length,\n position?: QueuePosition,\n ): void => {\n this.#clearTimeout() // clear any pending timeout\n for (let i = 0; i < numberOfItems; i++) {\n this.execute(position)\n }\n this.#tick()\n }\n\n /**\n * Processes all items in the queue as a batch using the provided function as an argument\n * The queue is cleared after processing\n */\n flushAsBatch = (batchFunction: (items: Array<TValue>) => void): void => {\n const items = this.#getAllItems()\n this.clear()\n batchFunction(items)\n }\n\n /**\n * Checks for expired items in the queue and removes them. Calls onExpire for each expired item.\n * Internal use only.\n */\n #checkExpiredItems = (): void => {\n if (\n (this.options.expirationDuration ?? Infinity) === Infinity &&\n this.options.getIsExpired === defaultOptions.getIsExpired\n ) {\n return\n }\n\n const now = Date.now()\n const expiredIndices: Array<number> = []\n\n // Find indices of expired items\n for (let i = 0; i < this.store.state.items.length; i++) {\n const timestamp = this.store.state.itemTimestamps[i]\n if (timestamp === undefined) continue\n\n const item = this.store.state.items[i]\n if (item === undefined) continue\n\n const isExpired =\n this.options.getIsExpired !== defaultOptions.getIsExpired\n ? this.options.getIsExpired!(item, timestamp)\n : now - timestamp > (this.options.expirationDuration ?? Infinity)\n\n if (isExpired) {\n expiredIndices.push(i)\n }\n }\n\n // Remove expired items from back to front to maintain indices\n for (let i = expiredIndices.length - 1; i >= 0; i--) {\n const index = expiredIndices[i]\n if (index === undefined) continue\n\n const expiredItem = this.store.state.items[index]\n if (expiredItem === undefined) continue\n\n const newItems = [...this.store.state.items]\n const newTimestamps = [...this.store.state.itemTimestamps]\n newItems.splice(index, 1)\n newTimestamps.splice(index, 1)\n this.#setState({\n items: newItems,\n itemTimestamps: newTimestamps,\n expirationCount: this.store.state.expirationCount + 1,\n })\n this.options.onExpire?.(expiredItem, this)\n }\n\n if (expiredIndices.length > 0) {\n this.options.onItemsChange?.(this)\n }\n }\n\n /**\n * Returns the next item in the queue without removing it.\n *\n * Example usage:\n * ```ts\n * queuer.peekNextItem(); // front\n * queuer.peekNextItem('back'); // back\n * ```\n */\n peekNextItem = (position: QueuePosition = 'front'): TValue | undefined => {\n if (position === 'front') {\n return this.store.state.items[0]\n }\n return this.store.state.items[this.store.state.items.length - 1]\n }\n\n /**\n * Returns a copy of all items in the queue.\n */\n peekAllItems = (): Array<TValue> => {\n return [...this.store.state.items]\n }\n\n /**\n * Starts processing items in the queue. If already isRunning, does nothing.\n */\n start = () => {\n this.#setState({ isRunning: true })\n if (!this.store.state.pendingTick && this.store.state.items.length > 0) {\n this.#tick()\n }\n }\n\n /**\n * Stops processing items in the queue. Does not clear the queue.\n */\n stop = () => {\n this.#clearTimeout()\n this.#setState({ isRunning: false, pendingTick: false })\n }\n\n #clearTimeout = (): void => {\n if (this.#timeoutId) {\n clearTimeout(this.#timeoutId)\n this.#timeoutId = null\n }\n }\n\n /**\n * Removes all pending items from the queue. Does not affect items being processed.\n */\n clear = (): void => {\n this.#setState({ items: [], itemTimestamps: [] })\n this.options.onItemsChange?.(this)\n }\n\n /**\n * Resets the queuer state to its default values\n */\n reset = (): void => {\n this.#setState(getDefaultQueuerState<TValue>())\n this.options.onItemsChange?.(this)\n }\n}\n\n/**\n * Creates a queue that processes items immediately upon addition.\n * Items are processed sequentially in FIFO order by default.\n *\n * This synchronous version is lighter weight and often all you need - upgrade to asyncQueue when you need promises, retry support, abort capabilities, concurrent execution, or advanced error handling.\n *\n * State Management:\n * - Uses TanStack Store for reactive state management\n * - Use `initialState` to provide initial state values when creating the queuer\n * - Use `onExecute` callback to react to item execution and implement custom logic\n * - Use `onItemsChange` callback to react to items being added or removed from the queue\n * - Use `onExpire` callback to react to items expiring and implement custom logic\n * - Use `onReject` callback to react to items being rejected when the queue is full\n * - The state includes execution count, expiration count, rejection count, and isRunning status\n * - State can be accessed via the underlying Queuer instance's `store.state` property\n * - When using framework adapters (React/Solid), state is accessed from the hook's state property\n *\n * Example usage:\n * ```ts\n * // Basic sequential processing\n * const processItems = queue<number>((n) => console.log(n), {\n * wait: 1000,\n * onItemsChange: (queuer) => console.log(queuer.peekAllItems())\n * });\n * processItems(1); // Logs: 1\n * processItems(2); // Logs: 2 after 1 completes\n *\n * // Priority queue\n * const processPriority = queue<number>((n) => console.log(n), {\n * getPriority: n => n // Higher numbers processed first\n * });\n * processPriority(1);\n * processPriority(3); // Processed before 1\n * ```\n */\nexport function queue<TValue>(\n fn: (item: TValue) => void,\n initialOptions: QueuerOptions<TValue>,\n) {\n const queuer = new Queuer<TValue>(fn, initialOptions)\n return queuer.addItem\n}\n"],"mappings":"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"}

@@ -1,1 +0,1 @@

{"version":3,"file":"queuer.js","names":["#setState","#tick","#clearTimeout","#getAllItems","#checkExpiredItems","#getWait","#timeoutId"],"sources":["../src/queuer.ts"],"sourcesContent":["import { Store } from '@tanstack/store'\nimport { parseFunctionOrValue } from './utils'\nimport { emitChange, pacerEventClient } from './event-client'\n\nexport interface QueuerState<TValue> {\n /**\n * Number of times addItem has been called (for reduction calculations)\n */\n addItemCount: number\n /**\n * Number of items that have been processed by the queuer\n */\n executionCount: number\n /**\n * Number of items that have been removed from the queue due to expiration\n */\n expirationCount: number\n /**\n * Whether the queuer has no items to process (items array is empty)\n */\n isEmpty: boolean\n /**\n * Whether the queuer has reached its maximum capacity\n */\n isFull: boolean\n /**\n * Whether the queuer is not currently processing any items\n */\n isIdle: boolean\n /**\n * Whether the queuer is active and will process items automatically\n */\n isRunning: boolean\n /**\n * Array of items currently waiting to be processed\n */\n items: Array<TValue>\n /**\n * Timestamps when items were added to the queue for expiration tracking\n */\n itemTimestamps: Array<number>\n /**\n * Whether the queuer has a pending timeout for processing the next item\n */\n pendingTick: boolean\n /**\n * Number of items that have been rejected from being added to the queue\n */\n rejectionCount: number\n /**\n * Number of items currently in the queue\n */\n size: number\n /**\n * Current processing status - 'idle' when not processing, 'running' when active, 'stopped' when paused\n */\n status: 'idle' | 'running' | 'stopped'\n}\n\nfunction getDefaultQueuerState<TValue>(): QueuerState<TValue> {\n return {\n executionCount: 0,\n expirationCount: 0,\n isEmpty: true,\n isFull: false,\n isIdle: true,\n isRunning: true,\n itemTimestamps: [],\n items: [],\n pendingTick: false,\n rejectionCount: 0,\n size: 0,\n status: 'idle',\n addItemCount: 0,\n }\n}\n\n/**\n * Options for configuring a Queuer instance.\n *\n * These options control queue behavior, item expiration, callbacks, and more.\n */\nexport interface QueuerOptions<TValue> {\n /**\n * Default position to add items to the queuer\n * @default 'back'\n */\n addItemsTo?: QueuePosition\n /**\n * Maximum time in milliseconds that an item can stay in the queue\n * If not provided, items will never expire\n */\n expirationDuration?: number\n /**\n * Function to determine if an item has expired\n * If provided, this overrides the expirationDuration behavior\n */\n getIsExpired?: (item: TValue, addedAt: number) => boolean\n /**\n * Default position to get items from during processing\n * @default 'front'\n */\n getItemsFrom?: QueuePosition\n /**\n * Function to determine priority of items in the queuer\n * Higher priority items will be processed first\n */\n getPriority?: (item: TValue) => number\n /**\n * Initial items to populate the queuer with\n */\n initialItems?: Array<TValue>\n /**\n * Initial state for the queuer\n */\n initialState?: Partial<QueuerState<TValue>>\n /**\n * Optional key to identify this queuer instance.\n * If provided, the queuer will be identified by this key in the devtools and PacerProvider if applicable.\n */\n key?: string\n /**\n * Maximum number of items allowed in the queuer\n */\n maxSize?: number\n /**\n * Callback fired whenever an item is removed from the queuer\n */\n onExecute?: (item: TValue, queuer: Queuer<TValue>) => void\n /**\n * Callback fired whenever an item expires in the queuer\n */\n onExpire?: (item: TValue, queuer: Queuer<TValue>) => void\n /**\n * Callback fired whenever an item is added or removed from the queuer\n */\n onItemsChange?: (queuer: Queuer<TValue>) => void\n /**\n * Callback fired whenever an item is rejected from being added to the queuer\n */\n onReject?: (item: TValue, queuer: Queuer<TValue>) => void\n /**\n * Whether the queuer should start processing tasks immediately\n */\n started?: boolean\n /**\n * Time in milliseconds to wait between processing items.\n * Can be a number or a function that returns a number.\n * @default 0\n */\n wait?: number | ((queuer: Queuer<TValue>) => number)\n}\n\n/**\n * Utility function for sharing common `QueuerOptions` options between different `Queuer` instances.\n */\nexport function queuerOptions<\n TValue = any,\n TOptions extends Partial<QueuerOptions<TValue>> = Partial<\n QueuerOptions<TValue>\n >,\n>(options: TOptions): TOptions {\n return options\n}\n\nconst defaultOptions: Omit<\n Required<QueuerOptions<any>>,\n | 'initialState'\n | 'onExecute'\n | 'onIsRunningChange'\n | 'onItemsChange'\n | 'onReject'\n | 'onExpire'\n | 'key'\n> = {\n addItemsTo: 'back',\n getItemsFrom: 'front',\n getPriority: (item) => item?.priority ?? 0,\n getIsExpired: () => false,\n expirationDuration: Infinity,\n initialItems: [],\n maxSize: Infinity,\n started: true,\n wait: 0,\n}\n\n/**\n * Position type for addItem and getNextItem operations.\n *\n * - 'front': Operate on the front of the queue (FIFO)\n * - 'back': Operate on the back of the queue (LIFO)\n */\nexport type QueuePosition = 'front' | 'back'\n\n/**\n * A flexible queue that processes items with configurable wait times, expiration, and priority.\n *\n * This synchronous version is lighter weight and often all you need - upgrade to AsyncQueuer when you need promises, retry support, abort capabilities, concurrent execution, or advanced error handling.\n *\n * Features:\n * - Automatic or manual processing of items\n * - FIFO (First In First Out), LIFO (Last In First Out), or double-ended queue behavior\n * - Priority-based ordering when getPriority is provided\n * - Item expiration and removal of stale items\n * - Callbacks for queue state changes, execution, rejection, and expiration\n *\n * Running behavior:\n * - `start()`: Begins automatically processing items in the queue (defaults to isRunning)\n * - `stop()`: Pauses processing but maintains queue state\n * - `wait`: Configurable delay between processing items\n * - `onItemsChange`/`onExecute`: Callbacks for monitoring queue state\n *\n * Manual processing is also supported when automatic processing is disabled:\n * - `execute()`: Processes the next item using the provided function\n * - `getNextItem()`: Removes and returns the next item without processing\n *\n * Queue behavior defaults to FIFO:\n * - `addItem(item)`: Adds to the back of the queue\n * - Items processed from the front of the queue\n *\n * Priority queue:\n * - Provide a `getPriority` function; higher values are processed first\n *\n * Stack (LIFO):\n * - `addItem(item, 'back')`: Adds to the back\n * - `getNextItem('back')`: Removes from the back\n *\n * Double-ended queue:\n * - `addItem(item, position)`: Adds to specified position ('front'/'back')\n * - `getNextItem(position)`: Removes from specified position\n *\n * Item expiration:\n * - `expirationDuration`: Maximum time items can stay in the queue\n * - `getIsExpired`: Function to override default expiration\n * - `onExpire`: Callback for expired items\n *\n * State Management:\n * - Uses TanStack Store for reactive state management\n * - Use `initialState` to provide initial state values when creating the queuer\n * - Use `onExecute` callback to react to item execution and implement custom logic\n * - Use `onItemsChange` callback to react to items being added or removed from the queue\n * - Use `onExpire` callback to react to items expiring and implement custom logic\n * - Use `onReject` callback to react to items being rejected when the queue is full\n * - The state includes execution count, expiration count, rejection count, and isRunning status\n * - State can be accessed via `queuer.store.state` when using the class directly\n * - When using framework adapters (React/Solid), state is accessed from `queuer.state`\n *\n * Example usage:\n * ```ts\n * // Auto-processing queue with wait time\n * const autoQueue = new Queuer<number>((n) => console.log(n), {\n * started: true, // Begin processing immediately\n * wait: 1000, // Wait 1s between items\n * onExecute: (item, queuer) => console.log(`Processed ${item}`)\n * });\n * autoQueue.addItem(1); // Will process after 1s\n * autoQueue.addItem(2); // Will process 1s after first item\n *\n * // Manual processing queue\n * const manualQueue = new Queuer<number>((n) => console.log(n), {\n * started: false\n * });\n * manualQueue.addItem(1); // [1]\n * manualQueue.addItem(2); // [1, 2]\n * manualQueue.execute(); // logs 1, queue is [2]\n * manualQueue.getNextItem(); // returns 2, queue is empty\n * ```\n */\nexport class Queuer<TValue> {\n readonly store: Store<Readonly<QueuerState<TValue>>> = new Store(\n getDefaultQueuerState<TValue>(),\n )\n key: string | undefined\n options: QueuerOptions<TValue>\n #timeoutId: NodeJS.Timeout | null = null\n\n constructor(\n public fn: (item: TValue) => void,\n initialOptions: QueuerOptions<TValue> = {},\n ) {\n this.key = initialOptions.key\n this.options = {\n ...defaultOptions,\n ...initialOptions,\n }\n const isInitiallyRunning =\n this.options.initialState?.isRunning ?? this.options.started ?? true\n this.#setState({\n ...this.options.initialState,\n isRunning: isInitiallyRunning,\n })\n\n if (this.options.initialState?.items) {\n if (this.store.state.isRunning) {\n this.#tick()\n }\n } else {\n for (let i = 0; i < (this.options.initialItems?.length ?? 0); i++) {\n const item = this.options.initialItems![i]!\n const isLast = i === (this.options.initialItems?.length ?? 0) - 1\n this.addItem(item, this.options.addItemsTo ?? 'back', isLast)\n }\n }\n\n if (this.key) {\n pacerEventClient.on('d-Queuer', (event) => {\n if (event.payload.key !== this.key) return\n this.#setState(\n event.payload.store.state as Partial<QueuerState<TValue>>,\n )\n this.setOptions(event.payload.options as Partial<QueuerOptions<TValue>>)\n })\n }\n }\n\n /**\n * Updates the queuer options. New options are merged with existing options.\n */\n setOptions = (newOptions: Partial<QueuerOptions<TValue>>): void => {\n this.options = { ...this.options, ...newOptions }\n }\n\n #setState = (newState: Partial<QueuerState<TValue>>): void => {\n this.store.setState((state) => {\n const combinedState = {\n ...state,\n ...newState,\n }\n\n const { items, isRunning } = combinedState\n\n const size = items.length\n const isFull = size >= (this.options.maxSize ?? Infinity)\n const isEmpty = size === 0\n const isIdle = isRunning && isEmpty\n\n const status = isIdle ? 'idle' : isRunning ? 'running' : 'stopped'\n\n return {\n ...combinedState,\n isEmpty,\n isFull,\n isIdle,\n size,\n status,\n }\n })\n emitChange('Queuer', this)\n }\n\n /**\n * Returns the current wait time (in milliseconds) between processing items.\n * If a function is provided, it is called with the queuer instance.\n */\n #getWait = (): number => {\n return parseFunctionOrValue(this.options.wait ?? 0, this)\n }\n\n /**\n * Processes items in the queue up to the wait interval. Internal use only.\n */\n #tick = () => {\n if (!this.store.state.isRunning) {\n this.#setState({ pendingTick: false })\n return\n }\n\n this.#setState({ pendingTick: true })\n\n // Check for expired items\n this.#checkExpiredItems()\n\n while (this.store.state.items.length > 0) {\n const nextItem = this.execute(this.options.getItemsFrom ?? 'front')\n if (nextItem === undefined) {\n break\n }\n\n const wait = this.#getWait()\n if (wait > 0) {\n // Use setTimeout to wait before processing next item\n this.#timeoutId = setTimeout(() => this.#tick(), wait)\n return\n }\n\n this.#tick()\n }\n this.#setState({ pendingTick: false })\n }\n\n /**\n * Adds an item to the queue. If the queue is full, the item is rejected and onReject is called.\n * Items can be inserted based on priority or at the front/back depending on configuration.\n *\n * Returns true if the item was added, false if the queue is full.\n *\n * Example usage:\n * ```ts\n * queuer.addItem('task');\n * queuer.addItem('task2', 'front');\n * ```\n */\n addItem = (\n item: TValue,\n position: QueuePosition = this.options.addItemsTo ?? 'back',\n runOnItemsChange: boolean = true,\n ): boolean => {\n this.#setState({\n addItemCount: this.store.state.addItemCount + 1,\n })\n\n if (this.store.state.items.length >= (this.options.maxSize ?? Infinity)) {\n this.#setState({\n rejectionCount: this.store.state.rejectionCount + 1,\n })\n this.options.onReject?.(item, this)\n return false\n }\n\n // Get priority either from the function or from getPriority option\n const priority =\n this.options.getPriority !== defaultOptions.getPriority\n ? this.options.getPriority!(item)\n : (item as any).priority\n\n const items = this.store.state.items\n const itemTimestamps = this.store.state.itemTimestamps\n\n if (priority !== undefined) {\n // Insert based on priority - higher priority items go to front\n const insertIndex = items.findIndex((existing) => {\n const existingPriority: number =\n this.options.getPriority !== defaultOptions.getPriority\n ? this.options.getPriority!(existing)\n : (existing as any).priority\n return existingPriority < priority\n })\n\n if (insertIndex === -1) {\n items.push(item)\n itemTimestamps.push(Date.now())\n } else {\n items.splice(insertIndex, 0, item)\n itemTimestamps.splice(insertIndex, 0, Date.now())\n }\n } else {\n if (position === 'front') {\n // Default FIFO/LIFO behavior\n items.unshift(item)\n itemTimestamps.unshift(Date.now())\n } else {\n // LIFO\n items.push(item)\n itemTimestamps.push(Date.now())\n }\n }\n\n this.#setState({\n items,\n itemTimestamps,\n })\n\n if (runOnItemsChange) {\n this.options.onItemsChange?.(this)\n }\n\n if (this.store.state.isRunning && !this.store.state.pendingTick) {\n this.#setState({ pendingTick: true })\n this.#tick()\n }\n\n return true\n }\n\n /**\n * Removes and returns the next item from the queue without executing the function.\n * Use for manual queue management. Normally, use execute() to process items.\n *\n * Example usage:\n * ```ts\n * // FIFO\n * queuer.getNextItem();\n * // LIFO\n * queuer.getNextItem('back');\n * ```\n */\n getNextItem = (\n position: QueuePosition = this.options.getItemsFrom ?? 'front',\n ): TValue | undefined => {\n const { items, itemTimestamps } = this.store.state\n let item: TValue | undefined\n\n // When priority function is provided, always get from front (highest priority)\n // Priority takes precedence over FIFO/LIFO behavior\n if (\n this.options.getPriority !== defaultOptions.getPriority ||\n position === 'front'\n ) {\n item = items[0]\n if (item !== undefined) {\n this.#setState({\n items: items.slice(1),\n itemTimestamps: itemTimestamps.slice(1),\n })\n }\n } else {\n item = items[items.length - 1]\n if (item !== undefined) {\n this.#setState({\n items: items.slice(0, -1),\n itemTimestamps: itemTimestamps.slice(0, -1),\n })\n }\n }\n\n if (item !== undefined) {\n this.options.onItemsChange?.(this)\n }\n\n return item\n }\n\n #getAllItems = (): Array<TValue> => {\n const items = this.peekAllItems()\n this.clear()\n return items\n }\n\n /**\n * Removes and returns the next item from the queue and processes it using the provided function.\n *\n * Example usage:\n * ```ts\n * queuer.execute();\n * // LIFO\n * queuer.execute('back');\n * ```\n */\n execute = (position?: QueuePosition): TValue | undefined => {\n const item = this.getNextItem(position)\n if (item !== undefined) {\n this.fn(item)\n this.#setState({\n executionCount: this.store.state.executionCount + 1,\n })\n this.options.onExecute?.(item, this)\n }\n return item\n }\n\n /**\n * Processes a specified number of items to execute immediately with no wait time\n * If no numberOfItems is provided, all items will be processed\n */\n flush = (\n numberOfItems: number = this.store.state.items.length,\n position?: QueuePosition,\n ): void => {\n this.#clearTimeout() // clear any pending timeout\n for (let i = 0; i < numberOfItems; i++) {\n this.execute(position)\n }\n this.#tick()\n }\n\n /**\n * Processes all items in the queue as a batch using the provided function as an argument\n * The queue is cleared after processing\n */\n flushAsBatch = (batchFunction: (items: Array<TValue>) => void): void => {\n const items = this.#getAllItems()\n this.clear()\n batchFunction(items)\n }\n\n /**\n * Checks for expired items in the queue and removes them. Calls onExpire for each expired item.\n * Internal use only.\n */\n #checkExpiredItems = (): void => {\n if (\n (this.options.expirationDuration ?? Infinity) === Infinity &&\n this.options.getIsExpired === defaultOptions.getIsExpired\n ) {\n return\n }\n\n const now = Date.now()\n const expiredIndices: Array<number> = []\n\n // Find indices of expired items\n for (let i = 0; i < this.store.state.items.length; i++) {\n const timestamp = this.store.state.itemTimestamps[i]\n if (timestamp === undefined) continue\n\n const item = this.store.state.items[i]\n if (item === undefined) continue\n\n const isExpired =\n this.options.getIsExpired !== defaultOptions.getIsExpired\n ? this.options.getIsExpired!(item, timestamp)\n : now - timestamp > (this.options.expirationDuration ?? Infinity)\n\n if (isExpired) {\n expiredIndices.push(i)\n }\n }\n\n // Remove expired items from back to front to maintain indices\n for (let i = expiredIndices.length - 1; i >= 0; i--) {\n const index = expiredIndices[i]\n if (index === undefined) continue\n\n const expiredItem = this.store.state.items[index]\n if (expiredItem === undefined) continue\n\n const newItems = [...this.store.state.items]\n const newTimestamps = [...this.store.state.itemTimestamps]\n newItems.splice(index, 1)\n newTimestamps.splice(index, 1)\n this.#setState({\n items: newItems,\n itemTimestamps: newTimestamps,\n expirationCount: this.store.state.expirationCount + 1,\n })\n this.options.onExpire?.(expiredItem, this)\n }\n\n if (expiredIndices.length > 0) {\n this.options.onItemsChange?.(this)\n }\n }\n\n /**\n * Returns the next item in the queue without removing it.\n *\n * Example usage:\n * ```ts\n * queuer.peekNextItem(); // front\n * queuer.peekNextItem('back'); // back\n * ```\n */\n peekNextItem = (position: QueuePosition = 'front'): TValue | undefined => {\n if (position === 'front') {\n return this.store.state.items[0]\n }\n return this.store.state.items[this.store.state.items.length - 1]\n }\n\n /**\n * Returns a copy of all items in the queue.\n */\n peekAllItems = (): Array<TValue> => {\n return [...this.store.state.items]\n }\n\n /**\n * Starts processing items in the queue. If already isRunning, does nothing.\n */\n start = () => {\n this.#setState({ isRunning: true })\n if (!this.store.state.pendingTick && this.store.state.items.length > 0) {\n this.#tick()\n }\n }\n\n /**\n * Stops processing items in the queue. Does not clear the queue.\n */\n stop = () => {\n this.#clearTimeout()\n this.#setState({ isRunning: false, pendingTick: false })\n }\n\n #clearTimeout = (): void => {\n if (this.#timeoutId) {\n clearTimeout(this.#timeoutId)\n this.#timeoutId = null\n }\n }\n\n /**\n * Removes all pending items from the queue. Does not affect items being processed.\n */\n clear = (): void => {\n this.#setState({ items: [], itemTimestamps: [] })\n this.options.onItemsChange?.(this)\n }\n\n /**\n * Resets the queuer state to its default values\n */\n reset = (): void => {\n this.#setState(getDefaultQueuerState<TValue>())\n this.options.onItemsChange?.(this)\n }\n}\n\n/**\n * Creates a queue that processes items immediately upon addition.\n * Items are processed sequentially in FIFO order by default.\n *\n * This synchronous version is lighter weight and often all you need - upgrade to asyncQueue when you need promises, retry support, abort capabilities, concurrent execution, or advanced error handling.\n *\n * State Management:\n * - Uses TanStack Store for reactive state management\n * - Use `initialState` to provide initial state values when creating the queuer\n * - Use `onExecute` callback to react to item execution and implement custom logic\n * - Use `onItemsChange` callback to react to items being added or removed from the queue\n * - Use `onExpire` callback to react to items expiring and implement custom logic\n * - Use `onReject` callback to react to items being rejected when the queue is full\n * - The state includes execution count, expiration count, rejection count, and isRunning status\n * - State can be accessed via the underlying Queuer instance's `store.state` property\n * - When using framework adapters (React/Solid), state is accessed from the hook's state property\n *\n * Example usage:\n * ```ts\n * // Basic sequential processing\n * const processItems = queue<number>((n) => console.log(n), {\n * wait: 1000,\n * onItemsChange: (queuer) => console.log(queuer.peekAllItems())\n * });\n * processItems(1); // Logs: 1\n * processItems(2); // Logs: 2 after 1 completes\n *\n * // Priority queue\n * const processPriority = queue<number>((n) => console.log(n), {\n * getPriority: n => n // Higher numbers processed first\n * });\n * processPriority(1);\n * processPriority(3); // Processed before 1\n * ```\n */\nexport function queue<TValue>(\n fn: (item: TValue) => void,\n initialOptions: QueuerOptions<TValue>,\n) {\n const queuer = new Queuer<TValue>(fn, initialOptions)\n return queuer.addItem\n}\n"],"mappings":"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"}
{"version":3,"file":"queuer.js","names":["#setState","#tick","#clearTimeout","#getAllItems","#checkExpiredItems","#getWait","#timeoutId"],"sources":["../src/queuer.ts"],"sourcesContent":["import { Store } from '@tanstack/store'\nimport { parseFunctionOrValue } from './utils'\nimport { emitChange, pacerEventClient } from './event-client'\n\nexport interface QueuerState<TValue> {\n /**\n * Number of times addItem has been called (for reduction calculations)\n */\n addItemCount: number\n /**\n * Number of items that have been processed by the queuer\n */\n executionCount: number\n /**\n * Number of items that have been removed from the queue due to expiration\n */\n expirationCount: number\n /**\n * Whether the queuer has no items to process (items array is empty)\n */\n isEmpty: boolean\n /**\n * Whether the queuer has reached its maximum capacity\n */\n isFull: boolean\n /**\n * Whether the queuer is not currently processing any items\n */\n isIdle: boolean\n /**\n * Whether the queuer is active and will process items automatically\n */\n isRunning: boolean\n /**\n * Array of items currently waiting to be processed\n */\n items: Array<TValue>\n /**\n * Timestamps when items were added to the queue for expiration tracking\n */\n itemTimestamps: Array<number>\n /**\n * Whether the queuer has a pending timeout for processing the next item\n */\n pendingTick: boolean\n /**\n * Number of items that have been rejected from being added to the queue\n */\n rejectionCount: number\n /**\n * Number of items currently in the queue\n */\n size: number\n /**\n * Current processing status - 'idle' when not processing, 'running' when active, 'stopped' when paused\n */\n status: 'idle' | 'running' | 'stopped'\n}\n\nfunction getDefaultQueuerState<TValue>(): QueuerState<TValue> {\n return {\n executionCount: 0,\n expirationCount: 0,\n isEmpty: true,\n isFull: false,\n isIdle: true,\n isRunning: true,\n itemTimestamps: [],\n items: [],\n pendingTick: false,\n rejectionCount: 0,\n size: 0,\n status: 'idle',\n addItemCount: 0,\n }\n}\n\n/**\n * Options for configuring a Queuer instance.\n *\n * These options control queue behavior, item expiration, callbacks, and more.\n */\nexport interface QueuerOptions<TValue> {\n /**\n * Default position to add items to the queuer\n * @default 'back'\n */\n addItemsTo?: QueuePosition\n /**\n * Maximum time in milliseconds that an item can stay in the queue\n * If not provided, items will never expire\n */\n expirationDuration?: number\n /**\n * Function to determine if an item has expired\n * If provided, this overrides the expirationDuration behavior\n */\n getIsExpired?: (item: TValue, addedAt: number) => boolean\n /**\n * Default position to get items from during processing\n * @default 'front'\n */\n getItemsFrom?: QueuePosition\n /**\n * Function to determine priority of items in the queuer\n * Higher priority items will be processed first\n */\n getPriority?: (item: TValue) => number\n /**\n * Initial items to populate the queuer with\n */\n initialItems?: Array<TValue>\n /**\n * Initial state for the queuer\n */\n initialState?: Partial<QueuerState<TValue>>\n /**\n * Optional key to identify this queuer instance.\n * If provided, the queuer will be identified by this key in the devtools and PacerProvider if applicable.\n */\n key?: string\n /**\n * Maximum number of items allowed in the queuer\n */\n maxSize?: number\n /**\n * Callback fired whenever an item is removed from the queuer\n */\n onExecute?: (item: TValue, queuer: Queuer<TValue>) => void\n /**\n * Callback fired whenever an item expires in the queuer\n */\n onExpire?: (item: TValue, queuer: Queuer<TValue>) => void\n /**\n * Callback fired whenever an item is added or removed from the queuer\n */\n onItemsChange?: (queuer: Queuer<TValue>) => void\n /**\n * Callback fired whenever an item is rejected from being added to the queuer\n */\n onReject?: (item: TValue, queuer: Queuer<TValue>) => void\n /**\n * Whether the queuer should start processing tasks immediately\n */\n started?: boolean\n /**\n * Time in milliseconds to wait between processing items.\n * Can be a number or a function that returns a number.\n * @default 0\n */\n wait?: number | ((queuer: Queuer<TValue>) => number)\n}\n\n/**\n * Utility function for sharing common `QueuerOptions` options between different `Queuer` instances.\n */\nexport function queuerOptions<\n TValue = any,\n TOptions extends Partial<QueuerOptions<TValue>> = Partial<\n QueuerOptions<TValue>\n >,\n>(options: TOptions): TOptions {\n return options\n}\n\nconst defaultOptions: Omit<\n Required<QueuerOptions<any>>,\n | 'initialState'\n | 'onExecute'\n | 'onIsRunningChange'\n | 'onItemsChange'\n | 'onReject'\n | 'onExpire'\n | 'key'\n> = {\n addItemsTo: 'back',\n getItemsFrom: 'front',\n getPriority: (item) => item?.priority ?? 0,\n getIsExpired: () => false,\n expirationDuration: Infinity,\n initialItems: [],\n maxSize: Infinity,\n started: true,\n wait: 0,\n}\n\n/**\n * Position type for addItem and getNextItem operations.\n *\n * - 'front': Operate on the front of the queue (FIFO)\n * - 'back': Operate on the back of the queue (LIFO)\n */\nexport type QueuePosition = 'front' | 'back'\n\n/**\n * A flexible queue that processes items with configurable wait times, expiration, and priority.\n *\n * This synchronous version is lighter weight and often all you need - upgrade to AsyncQueuer when you need promises, retry support, abort capabilities, concurrent execution, or advanced error handling.\n *\n * Features:\n * - Automatic or manual processing of items\n * - FIFO (First In First Out), LIFO (Last In First Out), or double-ended queue behavior\n * - Priority-based ordering when getPriority is provided\n * - Item expiration and removal of stale items\n * - Callbacks for queue state changes, execution, rejection, and expiration\n *\n * Running behavior:\n * - `start()`: Begins automatically processing items in the queue (defaults to isRunning)\n * - `stop()`: Pauses processing but maintains queue state\n * - `wait`: Configurable delay between processing items\n * - `onItemsChange`/`onExecute`: Callbacks for monitoring queue state\n *\n * Manual processing is also supported when automatic processing is disabled:\n * - `execute()`: Processes the next item using the provided function\n * - `getNextItem()`: Removes and returns the next item without processing\n *\n * Queue behavior defaults to FIFO:\n * - `addItem(item)`: Adds to the back of the queue\n * - Items processed from the front of the queue\n *\n * Priority queue:\n * - Provide a `getPriority` function; higher values are processed first\n *\n * Stack (LIFO):\n * - `addItem(item, 'back')`: Adds to the back\n * - `getNextItem('back')`: Removes from the back\n *\n * Double-ended queue:\n * - `addItem(item, position)`: Adds to specified position ('front'/'back')\n * - `getNextItem(position)`: Removes from specified position\n *\n * Item expiration:\n * - `expirationDuration`: Maximum time items can stay in the queue\n * - `getIsExpired`: Function to override default expiration\n * - `onExpire`: Callback for expired items\n *\n * State Management:\n * - Uses TanStack Store for reactive state management\n * - Use `initialState` to provide initial state values when creating the queuer\n * - Use `onExecute` callback to react to item execution and implement custom logic\n * - Use `onItemsChange` callback to react to items being added or removed from the queue\n * - Use `onExpire` callback to react to items expiring and implement custom logic\n * - Use `onReject` callback to react to items being rejected when the queue is full\n * - The state includes execution count, expiration count, rejection count, and isRunning status\n * - State can be accessed via `queuer.store.state` when using the class directly\n * - When using framework adapters (React/Solid), state is accessed from `queuer.state`\n *\n * Example usage:\n * ```ts\n * // Auto-processing queue with wait time\n * const autoQueue = new Queuer<number>((n) => console.log(n), {\n * started: true, // Begin processing immediately\n * wait: 1000, // Wait 1s between items\n * onExecute: (item, queuer) => console.log(`Processed ${item}`)\n * });\n * autoQueue.addItem(1); // Will process after 1s\n * autoQueue.addItem(2); // Will process 1s after first item\n *\n * // Manual processing queue\n * const manualQueue = new Queuer<number>((n) => console.log(n), {\n * started: false\n * });\n * manualQueue.addItem(1); // [1]\n * manualQueue.addItem(2); // [1, 2]\n * manualQueue.execute(); // logs 1, queue is [2]\n * manualQueue.getNextItem(); // returns 2, queue is empty\n * ```\n */\nexport class Queuer<TValue> {\n readonly store: Store<Readonly<QueuerState<TValue>>> = new Store(\n getDefaultQueuerState<TValue>(),\n )\n key: string | undefined\n options: QueuerOptions<TValue>\n #timeoutId: ReturnType<typeof setTimeout> | null = null\n\n constructor(\n public fn: (item: TValue) => void,\n initialOptions: QueuerOptions<TValue> = {},\n ) {\n this.key = initialOptions.key\n this.options = {\n ...defaultOptions,\n ...initialOptions,\n }\n const isInitiallyRunning =\n this.options.initialState?.isRunning ?? this.options.started ?? true\n this.#setState({\n ...this.options.initialState,\n isRunning: isInitiallyRunning,\n })\n\n if (this.options.initialState?.items) {\n if (this.store.state.isRunning) {\n this.#tick()\n }\n } else {\n for (let i = 0; i < (this.options.initialItems?.length ?? 0); i++) {\n const item = this.options.initialItems![i]!\n const isLast = i === (this.options.initialItems?.length ?? 0) - 1\n this.addItem(item, this.options.addItemsTo ?? 'back', isLast)\n }\n }\n\n if (this.key) {\n pacerEventClient.on('d-Queuer', (event) => {\n if (event.payload.key !== this.key) return\n this.#setState(\n event.payload.store.state as Partial<QueuerState<TValue>>,\n )\n this.setOptions(event.payload.options as Partial<QueuerOptions<TValue>>)\n })\n }\n }\n\n /**\n * Updates the queuer options. New options are merged with existing options.\n */\n setOptions = (newOptions: Partial<QueuerOptions<TValue>>): void => {\n this.options = { ...this.options, ...newOptions }\n }\n\n #setState = (newState: Partial<QueuerState<TValue>>): void => {\n this.store.setState((state) => {\n const combinedState = {\n ...state,\n ...newState,\n }\n\n const { items, isRunning } = combinedState\n\n const size = items.length\n const isFull = size >= (this.options.maxSize ?? Infinity)\n const isEmpty = size === 0\n const isIdle = isRunning && isEmpty\n\n const status = isIdle ? 'idle' : isRunning ? 'running' : 'stopped'\n\n return {\n ...combinedState,\n isEmpty,\n isFull,\n isIdle,\n size,\n status,\n }\n })\n emitChange('Queuer', this)\n }\n\n /**\n * Returns the current wait time (in milliseconds) between processing items.\n * If a function is provided, it is called with the queuer instance.\n */\n #getWait = (): number => {\n return parseFunctionOrValue(this.options.wait ?? 0, this)\n }\n\n /**\n * Processes items in the queue up to the wait interval. Internal use only.\n */\n #tick = () => {\n if (!this.store.state.isRunning) {\n this.#setState({ pendingTick: false })\n return\n }\n\n this.#setState({ pendingTick: true })\n\n // Check for expired items\n this.#checkExpiredItems()\n\n while (this.store.state.items.length > 0) {\n const nextItem = this.execute(this.options.getItemsFrom ?? 'front')\n if (nextItem === undefined) {\n break\n }\n\n const wait = this.#getWait()\n if (wait > 0) {\n // Use setTimeout to wait before processing next item\n this.#timeoutId = setTimeout(() => this.#tick(), wait)\n return\n }\n\n this.#tick()\n }\n this.#setState({ pendingTick: false })\n }\n\n /**\n * Adds an item to the queue. If the queue is full, the item is rejected and onReject is called.\n * Items can be inserted based on priority or at the front/back depending on configuration.\n *\n * Returns true if the item was added, false if the queue is full.\n *\n * Example usage:\n * ```ts\n * queuer.addItem('task');\n * queuer.addItem('task2', 'front');\n * ```\n */\n addItem = (\n item: TValue,\n position: QueuePosition = this.options.addItemsTo ?? 'back',\n runOnItemsChange: boolean = true,\n ): boolean => {\n this.#setState({\n addItemCount: this.store.state.addItemCount + 1,\n })\n\n if (this.store.state.items.length >= (this.options.maxSize ?? Infinity)) {\n this.#setState({\n rejectionCount: this.store.state.rejectionCount + 1,\n })\n this.options.onReject?.(item, this)\n return false\n }\n\n // Get priority either from the function or from getPriority option\n const priority =\n this.options.getPriority !== defaultOptions.getPriority\n ? this.options.getPriority!(item)\n : (item as any).priority\n\n const items = this.store.state.items\n const itemTimestamps = this.store.state.itemTimestamps\n\n if (priority !== undefined) {\n // Insert based on priority - higher priority items go to front\n const insertIndex = items.findIndex((existing) => {\n const existingPriority: number =\n this.options.getPriority !== defaultOptions.getPriority\n ? this.options.getPriority!(existing)\n : (existing as any).priority\n return existingPriority < priority\n })\n\n if (insertIndex === -1) {\n items.push(item)\n itemTimestamps.push(Date.now())\n } else {\n items.splice(insertIndex, 0, item)\n itemTimestamps.splice(insertIndex, 0, Date.now())\n }\n } else {\n if (position === 'front') {\n // Default FIFO/LIFO behavior\n items.unshift(item)\n itemTimestamps.unshift(Date.now())\n } else {\n // LIFO\n items.push(item)\n itemTimestamps.push(Date.now())\n }\n }\n\n this.#setState({\n items,\n itemTimestamps,\n })\n\n if (runOnItemsChange) {\n this.options.onItemsChange?.(this)\n }\n\n if (this.store.state.isRunning && !this.store.state.pendingTick) {\n this.#setState({ pendingTick: true })\n this.#tick()\n }\n\n return true\n }\n\n /**\n * Removes and returns the next item from the queue without executing the function.\n * Use for manual queue management. Normally, use execute() to process items.\n *\n * Example usage:\n * ```ts\n * // FIFO\n * queuer.getNextItem();\n * // LIFO\n * queuer.getNextItem('back');\n * ```\n */\n getNextItem = (\n position: QueuePosition = this.options.getItemsFrom ?? 'front',\n ): TValue | undefined => {\n const { items, itemTimestamps } = this.store.state\n let item: TValue | undefined\n\n // When priority function is provided, always get from front (highest priority)\n // Priority takes precedence over FIFO/LIFO behavior\n if (\n this.options.getPriority !== defaultOptions.getPriority ||\n position === 'front'\n ) {\n item = items[0]\n if (item !== undefined) {\n this.#setState({\n items: items.slice(1),\n itemTimestamps: itemTimestamps.slice(1),\n })\n }\n } else {\n item = items[items.length - 1]\n if (item !== undefined) {\n this.#setState({\n items: items.slice(0, -1),\n itemTimestamps: itemTimestamps.slice(0, -1),\n })\n }\n }\n\n if (item !== undefined) {\n this.options.onItemsChange?.(this)\n }\n\n return item\n }\n\n #getAllItems = (): Array<TValue> => {\n const items = this.peekAllItems()\n this.clear()\n return items\n }\n\n /**\n * Removes and returns the next item from the queue and processes it using the provided function.\n *\n * Example usage:\n * ```ts\n * queuer.execute();\n * // LIFO\n * queuer.execute('back');\n * ```\n */\n execute = (position?: QueuePosition): TValue | undefined => {\n const item = this.getNextItem(position)\n if (item !== undefined) {\n this.fn(item)\n this.#setState({\n executionCount: this.store.state.executionCount + 1,\n })\n this.options.onExecute?.(item, this)\n }\n return item\n }\n\n /**\n * Processes a specified number of items to execute immediately with no wait time\n * If no numberOfItems is provided, all items will be processed\n */\n flush = (\n numberOfItems: number = this.store.state.items.length,\n position?: QueuePosition,\n ): void => {\n this.#clearTimeout() // clear any pending timeout\n for (let i = 0; i < numberOfItems; i++) {\n this.execute(position)\n }\n this.#tick()\n }\n\n /**\n * Processes all items in the queue as a batch using the provided function as an argument\n * The queue is cleared after processing\n */\n flushAsBatch = (batchFunction: (items: Array<TValue>) => void): void => {\n const items = this.#getAllItems()\n this.clear()\n batchFunction(items)\n }\n\n /**\n * Checks for expired items in the queue and removes them. Calls onExpire for each expired item.\n * Internal use only.\n */\n #checkExpiredItems = (): void => {\n if (\n (this.options.expirationDuration ?? Infinity) === Infinity &&\n this.options.getIsExpired === defaultOptions.getIsExpired\n ) {\n return\n }\n\n const now = Date.now()\n const expiredIndices: Array<number> = []\n\n // Find indices of expired items\n for (let i = 0; i < this.store.state.items.length; i++) {\n const timestamp = this.store.state.itemTimestamps[i]\n if (timestamp === undefined) continue\n\n const item = this.store.state.items[i]\n if (item === undefined) continue\n\n const isExpired =\n this.options.getIsExpired !== defaultOptions.getIsExpired\n ? this.options.getIsExpired!(item, timestamp)\n : now - timestamp > (this.options.expirationDuration ?? Infinity)\n\n if (isExpired) {\n expiredIndices.push(i)\n }\n }\n\n // Remove expired items from back to front to maintain indices\n for (let i = expiredIndices.length - 1; i >= 0; i--) {\n const index = expiredIndices[i]\n if (index === undefined) continue\n\n const expiredItem = this.store.state.items[index]\n if (expiredItem === undefined) continue\n\n const newItems = [...this.store.state.items]\n const newTimestamps = [...this.store.state.itemTimestamps]\n newItems.splice(index, 1)\n newTimestamps.splice(index, 1)\n this.#setState({\n items: newItems,\n itemTimestamps: newTimestamps,\n expirationCount: this.store.state.expirationCount + 1,\n })\n this.options.onExpire?.(expiredItem, this)\n }\n\n if (expiredIndices.length > 0) {\n this.options.onItemsChange?.(this)\n }\n }\n\n /**\n * Returns the next item in the queue without removing it.\n *\n * Example usage:\n * ```ts\n * queuer.peekNextItem(); // front\n * queuer.peekNextItem('back'); // back\n * ```\n */\n peekNextItem = (position: QueuePosition = 'front'): TValue | undefined => {\n if (position === 'front') {\n return this.store.state.items[0]\n }\n return this.store.state.items[this.store.state.items.length - 1]\n }\n\n /**\n * Returns a copy of all items in the queue.\n */\n peekAllItems = (): Array<TValue> => {\n return [...this.store.state.items]\n }\n\n /**\n * Starts processing items in the queue. If already isRunning, does nothing.\n */\n start = () => {\n this.#setState({ isRunning: true })\n if (!this.store.state.pendingTick && this.store.state.items.length > 0) {\n this.#tick()\n }\n }\n\n /**\n * Stops processing items in the queue. Does not clear the queue.\n */\n stop = () => {\n this.#clearTimeout()\n this.#setState({ isRunning: false, pendingTick: false })\n }\n\n #clearTimeout = (): void => {\n if (this.#timeoutId) {\n clearTimeout(this.#timeoutId)\n this.#timeoutId = null\n }\n }\n\n /**\n * Removes all pending items from the queue. Does not affect items being processed.\n */\n clear = (): void => {\n this.#setState({ items: [], itemTimestamps: [] })\n this.options.onItemsChange?.(this)\n }\n\n /**\n * Resets the queuer state to its default values\n */\n reset = (): void => {\n this.#setState(getDefaultQueuerState<TValue>())\n this.options.onItemsChange?.(this)\n }\n}\n\n/**\n * Creates a queue that processes items immediately upon addition.\n * Items are processed sequentially in FIFO order by default.\n *\n * This synchronous version is lighter weight and often all you need - upgrade to asyncQueue when you need promises, retry support, abort capabilities, concurrent execution, or advanced error handling.\n *\n * State Management:\n * - Uses TanStack Store for reactive state management\n * - Use `initialState` to provide initial state values when creating the queuer\n * - Use `onExecute` callback to react to item execution and implement custom logic\n * - Use `onItemsChange` callback to react to items being added or removed from the queue\n * - Use `onExpire` callback to react to items expiring and implement custom logic\n * - Use `onReject` callback to react to items being rejected when the queue is full\n * - The state includes execution count, expiration count, rejection count, and isRunning status\n * - State can be accessed via the underlying Queuer instance's `store.state` property\n * - When using framework adapters (React/Solid), state is accessed from the hook's state property\n *\n * Example usage:\n * ```ts\n * // Basic sequential processing\n * const processItems = queue<number>((n) => console.log(n), {\n * wait: 1000,\n * onItemsChange: (queuer) => console.log(queuer.peekAllItems())\n * });\n * processItems(1); // Logs: 1\n * processItems(2); // Logs: 2 after 1 completes\n *\n * // Priority queue\n * const processPriority = queue<number>((n) => console.log(n), {\n * getPriority: n => n // Higher numbers processed first\n * });\n * processPriority(1);\n * processPriority(3); // Processed before 1\n * ```\n */\nexport function queue<TValue>(\n fn: (item: TValue) => void,\n initialOptions: QueuerOptions<TValue>,\n) {\n const queuer = new Queuer<TValue>(fn, initialOptions)\n return queuer.addItem\n}\n"],"mappings":";;;;;AA2DA,SAAS,wBAAqD;AAC5D,QAAO;EACL,gBAAgB;EAChB,iBAAiB;EACjB,SAAS;EACT,QAAQ;EACR,QAAQ;EACR,WAAW;EACX,gBAAgB,EAAE;EAClB,OAAO,EAAE;EACT,aAAa;EACb,gBAAgB;EAChB,MAAM;EACN,QAAQ;EACR,cAAc;EACf;;;;;AAkFH,SAAgB,cAKd,SAA6B;AAC7B,QAAO;;AAGT,MAAM,iBASF;CACF,YAAY;CACZ,cAAc;CACd,cAAc,SAAS,MAAM,YAAY;CACzC,oBAAoB;CACpB,oBAAoB;CACpB,cAAc,EAAE;CAChB,SAAS;CACT,SAAS;CACT,MAAM;CACP;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoFD,IAAa,SAAb,MAA4B;CAM1B,aAAmD;CAEnD,YACE,AAAO,IACP,iBAAwC,EAAE,EAC1C;EAFO;eAR8C,IAAI,MACzD,uBAA+B,CAChC;qBA+Ca,eAAqD;AACjE,QAAK,UAAU;IAAE,GAAG,KAAK;IAAS,GAAG;IAAY;;kBAoFjD,MACA,WAA0B,KAAK,QAAQ,cAAc,QACrD,mBAA4B,SAChB;AACZ,SAAKA,SAAU,EACb,cAAc,KAAK,MAAM,MAAM,eAAe,GAC/C,CAAC;AAEF,OAAI,KAAK,MAAM,MAAM,MAAM,WAAW,KAAK,QAAQ,WAAW,WAAW;AACvE,UAAKA,SAAU,EACb,gBAAgB,KAAK,MAAM,MAAM,iBAAiB,GACnD,CAAC;AACF,SAAK,QAAQ,WAAW,MAAM,KAAK;AACnC,WAAO;;GAIT,MAAM,WACJ,KAAK,QAAQ,gBAAgB,eAAe,cACxC,KAAK,QAAQ,YAAa,KAAK,GAC9B,KAAa;GAEpB,MAAM,QAAQ,KAAK,MAAM,MAAM;GAC/B,MAAM,iBAAiB,KAAK,MAAM,MAAM;AAExC,OAAI,aAAa,QAAW;IAE1B,MAAM,cAAc,MAAM,WAAW,aAAa;AAKhD,aAHE,KAAK,QAAQ,gBAAgB,eAAe,cACxC,KAAK,QAAQ,YAAa,SAAS,GAClC,SAAiB,YACE;MAC1B;AAEF,QAAI,gBAAgB,IAAI;AACtB,WAAM,KAAK,KAAK;AAChB,oBAAe,KAAK,KAAK,KAAK,CAAC;WAC1B;AACL,WAAM,OAAO,aAAa,GAAG,KAAK;AAClC,oBAAe,OAAO,aAAa,GAAG,KAAK,KAAK,CAAC;;cAG/C,aAAa,SAAS;AAExB,UAAM,QAAQ,KAAK;AACnB,mBAAe,QAAQ,KAAK,KAAK,CAAC;UAC7B;AAEL,UAAM,KAAK,KAAK;AAChB,mBAAe,KAAK,KAAK,KAAK,CAAC;;AAInC,SAAKA,SAAU;IACb;IACA;IACD,CAAC;AAEF,OAAI,iBACF,MAAK,QAAQ,gBAAgB,KAAK;AAGpC,OAAI,KAAK,MAAM,MAAM,aAAa,CAAC,KAAK,MAAM,MAAM,aAAa;AAC/D,UAAKA,SAAU,EAAE,aAAa,MAAM,CAAC;AACrC,UAAKC,MAAO;;AAGd,UAAO;;sBAgBP,WAA0B,KAAK,QAAQ,gBAAgB,YAChC;GACvB,MAAM,EAAE,OAAO,mBAAmB,KAAK,MAAM;GAC7C,IAAI;AAIJ,OACE,KAAK,QAAQ,gBAAgB,eAAe,eAC5C,aAAa,SACb;AACA,WAAO,MAAM;AACb,QAAI,SAAS,OACX,OAAKD,SAAU;KACb,OAAO,MAAM,MAAM,EAAE;KACrB,gBAAgB,eAAe,MAAM,EAAE;KACxC,CAAC;UAEC;AACL,WAAO,MAAM,MAAM,SAAS;AAC5B,QAAI,SAAS,OACX,OAAKA,SAAU;KACb,OAAO,MAAM,MAAM,GAAG,GAAG;KACzB,gBAAgB,eAAe,MAAM,GAAG,GAAG;KAC5C,CAAC;;AAIN,OAAI,SAAS,OACX,MAAK,QAAQ,gBAAgB,KAAK;AAGpC,UAAO;;kBAmBE,aAAiD;GAC1D,MAAM,OAAO,KAAK,YAAY,SAAS;AACvC,OAAI,SAAS,QAAW;AACtB,SAAK,GAAG,KAAK;AACb,UAAKA,SAAU,EACb,gBAAgB,KAAK,MAAM,MAAM,iBAAiB,GACnD,CAAC;AACF,SAAK,QAAQ,YAAY,MAAM,KAAK;;AAEtC,UAAO;;gBAQP,gBAAwB,KAAK,MAAM,MAAM,MAAM,QAC/C,aACS;AACT,SAAKE,cAAe;AACpB,QAAK,IAAI,IAAI,GAAG,IAAI,eAAe,IACjC,MAAK,QAAQ,SAAS;AAExB,SAAKD,MAAO;;uBAOE,kBAAwD;GACtE,MAAM,QAAQ,MAAKE,aAAc;AACjC,QAAK,OAAO;AACZ,iBAAc,MAAM;;uBAsEN,WAA0B,YAAgC;AACxE,OAAI,aAAa,QACf,QAAO,KAAK,MAAM,MAAM,MAAM;AAEhC,UAAO,KAAK,MAAM,MAAM,MAAM,KAAK,MAAM,MAAM,MAAM,SAAS;;4BAM5B;AAClC,UAAO,CAAC,GAAG,KAAK,MAAM,MAAM,MAAM;;qBAMtB;AACZ,SAAKH,SAAU,EAAE,WAAW,MAAM,CAAC;AACnC,OAAI,CAAC,KAAK,MAAM,MAAM,eAAe,KAAK,MAAM,MAAM,MAAM,SAAS,EACnE,OAAKC,MAAO;;oBAOH;AACX,SAAKC,cAAe;AACpB,SAAKF,SAAU;IAAE,WAAW;IAAO,aAAa;IAAO,CAAC;;qBAatC;AAClB,SAAKA,SAAU;IAAE,OAAO,EAAE;IAAE,gBAAgB,EAAE;IAAE,CAAC;AACjD,QAAK,QAAQ,gBAAgB,KAAK;;qBAMhB;AAClB,SAAKA,SAAU,uBAA+B,CAAC;AAC/C,QAAK,QAAQ,gBAAgB,KAAK;;AA9ZlC,OAAK,MAAM,eAAe;AAC1B,OAAK,UAAU;GACb,GAAG;GACH,GAAG;GACJ;EACD,MAAM,qBACJ,KAAK,QAAQ,cAAc,aAAa,KAAK,QAAQ,WAAW;AAClE,QAAKA,SAAU;GACb,GAAG,KAAK,QAAQ;GAChB,WAAW;GACZ,CAAC;AAEF,MAAI,KAAK,QAAQ,cAAc,OAC7B;OAAI,KAAK,MAAM,MAAM,UACnB,OAAKC,MAAO;QAGd,MAAK,IAAI,IAAI,GAAG,KAAK,KAAK,QAAQ,cAAc,UAAU,IAAI,KAAK;GACjE,MAAM,OAAO,KAAK,QAAQ,aAAc;GACxC,MAAM,SAAS,OAAO,KAAK,QAAQ,cAAc,UAAU,KAAK;AAChE,QAAK,QAAQ,MAAM,KAAK,QAAQ,cAAc,QAAQ,OAAO;;AAIjE,MAAI,KAAK,IACP,kBAAiB,GAAG,aAAa,UAAU;AACzC,OAAI,MAAM,QAAQ,QAAQ,KAAK,IAAK;AACpC,SAAKD,SACH,MAAM,QAAQ,MAAM,MACrB;AACD,QAAK,WAAW,MAAM,QAAQ,QAA0C;IACxE;;CAWN,aAAa,aAAiD;AAC5D,OAAK,MAAM,UAAU,UAAU;GAC7B,MAAM,gBAAgB;IACpB,GAAG;IACH,GAAG;IACJ;GAED,MAAM,EAAE,OAAO,cAAc;GAE7B,MAAM,OAAO,MAAM;GACnB,MAAM,SAAS,SAAS,KAAK,QAAQ,WAAW;GAChD,MAAM,UAAU,SAAS;GACzB,MAAM,SAAS,aAAa;GAE5B,MAAM,SAAS,SAAS,SAAS,YAAY,YAAY;AAEzD,UAAO;IACL,GAAG;IACH;IACA;IACA;IACA;IACA;IACD;IACD;AACF,aAAW,UAAU,KAAK;;;;;;CAO5B,iBAAyB;AACvB,SAAO,qBAAqB,KAAK,QAAQ,QAAQ,GAAG,KAAK;;;;;CAM3D,cAAc;AACZ,MAAI,CAAC,KAAK,MAAM,MAAM,WAAW;AAC/B,SAAKA,SAAU,EAAE,aAAa,OAAO,CAAC;AACtC;;AAGF,QAAKA,SAAU,EAAE,aAAa,MAAM,CAAC;AAGrC,QAAKI,mBAAoB;AAEzB,SAAO,KAAK,MAAM,MAAM,MAAM,SAAS,GAAG;AAExC,OADiB,KAAK,QAAQ,KAAK,QAAQ,gBAAgB,QAAQ,KAClD,OACf;GAGF,MAAM,OAAO,MAAKC,SAAU;AAC5B,OAAI,OAAO,GAAG;AAEZ,UAAKC,YAAa,iBAAiB,MAAKL,MAAO,EAAE,KAAK;AACtD;;AAGF,SAAKA,MAAO;;AAEd,QAAKD,SAAU,EAAE,aAAa,OAAO,CAAC;;CAuIxC,qBAAoC;EAClC,MAAM,QAAQ,KAAK,cAAc;AACjC,OAAK,OAAO;AACZ,SAAO;;;;;;CAsDT,2BAAiC;AAC/B,OACG,KAAK,QAAQ,sBAAsB,cAAc,YAClD,KAAK,QAAQ,iBAAiB,eAAe,aAE7C;EAGF,MAAM,MAAM,KAAK,KAAK;EACtB,MAAM,iBAAgC,EAAE;AAGxC,OAAK,IAAI,IAAI,GAAG,IAAI,KAAK,MAAM,MAAM,MAAM,QAAQ,KAAK;GACtD,MAAM,YAAY,KAAK,MAAM,MAAM,eAAe;AAClD,OAAI,cAAc,OAAW;GAE7B,MAAM,OAAO,KAAK,MAAM,MAAM,MAAM;AACpC,OAAI,SAAS,OAAW;AAOxB,OAJE,KAAK,QAAQ,iBAAiB,eAAe,eACzC,KAAK,QAAQ,aAAc,MAAM,UAAU,GAC3C,MAAM,aAAa,KAAK,QAAQ,sBAAsB,UAG1D,gBAAe,KAAK,EAAE;;AAK1B,OAAK,IAAI,IAAI,eAAe,SAAS,GAAG,KAAK,GAAG,KAAK;GACnD,MAAM,QAAQ,eAAe;AAC7B,OAAI,UAAU,OAAW;GAEzB,MAAM,cAAc,KAAK,MAAM,MAAM,MAAM;AAC3C,OAAI,gBAAgB,OAAW;GAE/B,MAAM,WAAW,CAAC,GAAG,KAAK,MAAM,MAAM,MAAM;GAC5C,MAAM,gBAAgB,CAAC,GAAG,KAAK,MAAM,MAAM,eAAe;AAC1D,YAAS,OAAO,OAAO,EAAE;AACzB,iBAAc,OAAO,OAAO,EAAE;AAC9B,SAAKA,SAAU;IACb,OAAO;IACP,gBAAgB;IAChB,iBAAiB,KAAK,MAAM,MAAM,kBAAkB;IACrD,CAAC;AACF,QAAK,QAAQ,WAAW,aAAa,KAAK;;AAG5C,MAAI,eAAe,SAAS,EAC1B,MAAK,QAAQ,gBAAgB,KAAK;;CA6CtC,sBAA4B;AAC1B,MAAI,MAAKM,WAAY;AACnB,gBAAa,MAAKA,UAAW;AAC7B,SAAKA,YAAa;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAwDxB,SAAgB,MACd,IACA,gBACA;AAEA,QADe,IAAI,OAAe,IAAI,eAAe,CACvC"}

@@ -1,1 +0,1 @@

{"version":3,"file":"rate-limiter.cjs","names":["Store","#setState","#cleanupOldExecutions","#getExecutionTimesInWindow","#getLimit","#execute","#getWindow","#clearTimeouts","#setCleanupTimeout","#getEnabled","parseFunctionOrValue","#timeoutIds","#clearTimeout"],"sources":["../src/rate-limiter.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 RateLimiterState {\n /**\n * Number of function executions that have been completed\n */\n executionCount: number\n /**\n * Array of timestamps when executions occurred for rate limiting calculations\n */\n executionTimes: Array<number>\n /**\n * Whether the rate limiter has exceeded the limit\n */\n isExceeded: boolean\n /**\n * Number of times maybeExecute has been called (for reduction calculations)\n */\n maybeExecuteCount: number\n /**\n * Number of function executions that have been rejected due to rate limiting\n */\n rejectionCount: number\n /**\n * Current execution status - 'disabled' when not active, 'executing' when executing, 'idle' when not executing, 'exceeded' when rate limit is exceeded\n */\n status: 'disabled' | 'exceeded' | 'idle'\n}\n\nfunction getDefaultRateLimiterState(): RateLimiterState {\n return {\n executionCount: 0,\n executionTimes: [],\n isExceeded: false,\n rejectionCount: 0,\n status: 'idle',\n maybeExecuteCount: 0,\n }\n}\n\n/**\n * Options for configuring a rate-limited function\n */\nexport interface RateLimiterOptions<TFn extends AnyFunction> {\n /**\n * Whether the rate limiter is enabled. When disabled, maybeExecute will not trigger any executions.\n * Defaults to true.\n */\n enabled?: boolean | ((rateLimiter: RateLimiter<TFn>) => boolean)\n /**\n * Initial state for the rate limiter\n */\n initialState?: Partial<RateLimiterState>\n /**\n * Optional key to identify this rate limiter instance.\n * If provided, the rate limiter will be identified by this key in the devtools and PacerProvider if applicable.\n */\n key?: string\n /**\n * Maximum number of executions allowed within the time window.\n * Can be a number or a callback function that receives the rate limiter instance and returns a number.\n */\n limit: number | ((rateLimiter: RateLimiter<TFn>) => number)\n /**\n * Callback function that is called after the function is executed\n */\n onExecute?: (args: Parameters<TFn>, rateLimiter: RateLimiter<TFn>) => void\n /**\n * Optional callback function that is called when an execution is rejected due to rate limiting\n */\n onReject?: (rateLimiter: RateLimiter<TFn>) => void\n /**\n * Time window in milliseconds within which the limit applies.\n * Can be a number or a callback function that receives the rate limiter instance and returns a number.\n */\n window: number | ((rateLimiter: RateLimiter<TFn>) => number)\n /**\n * Type of window to use for rate limiting\n * - 'fixed': Uses a fixed window that resets after the window period\n * - 'sliding': Uses a sliding window that allows executions as old ones expire\n * Defaults to 'fixed'\n */\n windowType?: 'fixed' | 'sliding'\n}\n\n/**\n * Utility function for sharing common `RateLimiterOptions` options between different `RateLimiter` instances.\n */\nexport function rateLimiterOptions<\n TFn extends AnyFunction = AnyFunction,\n TOptions extends Partial<RateLimiterOptions<TFn>> = Partial<\n RateLimiterOptions<TFn>\n >,\n>(options: TOptions): TOptions {\n return options\n}\n\nconst defaultOptions: Omit<\n Required<RateLimiterOptions<any>>,\n 'initialState' | 'onExecute' | 'onReject' | 'key'\n> = {\n enabled: true,\n limit: 1,\n window: 0,\n windowType: 'fixed',\n}\n\n/**\n * A class that creates a rate-limited function.\n *\n * Rate limiting is a simple approach that allows a function to execute up to a limit within a time window,\n * then blocks all subsequent calls until the window passes. This can lead to \"bursty\" behavior where\n * all executions happen immediately, followed by a complete block.\n * This synchronous version is lighter weight and often all you need - upgrade to AsyncRateLimiter when you need promises, retry support, abort capabilities, or advanced error handling.\n *\n * The rate limiter supports two types of windows:\n * - 'fixed': A strict window that resets after the window period. All executions within the window count\n * towards the limit, and the window resets completely after the period.\n * - 'sliding': A rolling window that allows executions as old ones expire. This provides a more\n * consistent rate of execution over time.\n *\n * For smoother execution patterns, consider using:\n * - Throttling: Ensures consistent spacing between executions (e.g. max once per 200ms)\n * - Debouncing: Waits for a pause in calls before executing (e.g. after 500ms of no calls)\n *\n * Rate limiting is best used for hard API limits or resource constraints. For UI updates or\n * smoothing out frequent events, throttling or debouncing usually provide better user experience.\n *\n * State Management:\n * - Uses TanStack Store for reactive state management\n * - Use `initialState` to provide initial state values when creating the rate limiter\n * - Use `onExecute` callback to react to function execution and implement custom logic\n * - Use `onReject` callback to react to executions being rejected when rate limit is exceeded\n * - The state includes execution count, execution times, and rejection count\n * - State can be accessed via `rateLimiter.store.state` when using the class directly\n * - When using framework adapters (React/Solid), state is accessed from `rateLimiter.state`\n *\n * @example\n * ```ts\n * const rateLimiter = new RateLimiter(\n * (id: string) => api.getData(id),\n * {\n * limit: 5,\n * window: 1000,\n * windowType: 'sliding',\n * }\n * );\n *\n * // Will execute immediately until limit reached, then block\n * rateLimiter.maybeExecute('123');\n * ```\n */\nexport class RateLimiter<TFn extends AnyFunction> {\n readonly store: Store<Readonly<RateLimiterState>> =\n new Store<RateLimiterState>(getDefaultRateLimiterState())\n key: string | undefined\n options: RateLimiterOptions<TFn>\n #timeoutIds: Set<NodeJS.Timeout> = new Set()\n\n constructor(\n public fn: TFn,\n initialOptions: RateLimiterOptions<TFn>,\n ) {\n this.key = initialOptions.key\n this.options = {\n ...defaultOptions,\n ...initialOptions,\n }\n this.#setState(this.options.initialState ?? {})\n for (const executionTime of this.#getExecutionTimesInWindow()) {\n this.#setCleanupTimeout(executionTime)\n }\n\n if (this.key) {\n pacerEventClient.on('d-RateLimiter', (event) => {\n if (event.payload.key !== this.key) return\n this.#setState(event.payload.store.state as Partial<RateLimiterState>)\n this.setOptions(\n event.payload.options as Partial<RateLimiterOptions<TFn>>,\n )\n })\n }\n }\n\n /**\n * Updates the rate limiter options\n */\n setOptions = (newOptions: Partial<RateLimiterOptions<TFn>>): void => {\n this.options = { ...this.options, ...newOptions }\n }\n\n #setState = (newState: Partial<RateLimiterState>): void => {\n this.store.setState((state) => {\n const combinedState = {\n ...state,\n ...newState,\n }\n const isExceeded = combinedState.executionTimes.length >= this.#getLimit()\n const status = !this.#getEnabled()\n ? 'disabled'\n : isExceeded\n ? 'exceeded'\n : 'idle'\n return {\n ...combinedState,\n isExceeded,\n status,\n }\n })\n emitChange('RateLimiter', this)\n }\n\n /**\n * Returns the current enabled state of the rate limiter\n */\n #getEnabled = (): boolean => {\n return !!parseFunctionOrValue(this.options.enabled, this)\n }\n\n /**\n * Returns the current limit of executions allowed within the time window\n */\n #getLimit = (): number => {\n return parseFunctionOrValue(this.options.limit, this)\n }\n\n /**\n * Returns the current time window in milliseconds\n */\n #getWindow = (): number => {\n return parseFunctionOrValue(this.options.window, this)\n }\n\n /**\n * Attempts to execute the rate-limited function if within the configured limits.\n * Will reject execution if the number of calls in the current window exceeds the limit.\n *\n * @example\n * ```ts\n * const rateLimiter = new RateLimiter(fn, { limit: 5, window: 1000 });\n *\n * // First 5 calls will return true\n * rateLimiter.maybeExecute('arg1', 'arg2'); // true\n *\n * // Additional calls within the window will return false\n * rateLimiter.maybeExecute('arg1', 'arg2'); // false\n * ```\n */\n maybeExecute = (...args: Parameters<TFn>): boolean => {\n this.#setState({\n maybeExecuteCount: this.store.state.maybeExecuteCount + 1,\n })\n\n this.#cleanupOldExecutions()\n\n const relevantExecutionTimes = this.#getExecutionTimesInWindow()\n\n if (relevantExecutionTimes.length < this.#getLimit()) {\n this.#execute(...args)\n return true\n }\n\n this.#setState({\n rejectionCount: this.store.state.rejectionCount + 1,\n })\n this.options.onReject?.(this)\n return false\n }\n\n #execute = (...args: Parameters<TFn>): void => {\n if (!this.#getEnabled()) return\n const now = Date.now()\n this.fn(...args) // EXECUTE!\n this.store.state.executionTimes.push(now) // mutate state directly for performance\n\n this.#setCleanupTimeout(now)\n\n this.#setState({\n executionCount: this.store.state.executionCount + 1,\n })\n this.options.onExecute?.(args, this)\n }\n\n #getExecutionTimesInWindow = (): Array<number> => {\n if (this.options.windowType === 'sliding') {\n // For sliding window, return all executions within the current window\n return this.store.state.executionTimes.filter(\n (time) => time > Date.now() - this.#getWindow(),\n )\n } else {\n // For fixed window, return all executions in the current window\n // The window starts from the oldest execution time\n if (this.store.state.executionTimes.length === 0) {\n return []\n }\n const oldestExecution = Math.min(...this.store.state.executionTimes)\n const windowStart = oldestExecution\n const windowEnd = windowStart + this.#getWindow()\n const now = Date.now()\n\n // If the window has expired, return empty array\n if (now > windowEnd) {\n return []\n }\n\n // Otherwise, return all executions in the current window\n return this.store.state.executionTimes.filter(\n (time) => time >= windowStart && time <= windowEnd,\n )\n }\n }\n\n #setCleanupTimeout = (executionTime: number): void => {\n if (\n this.options.windowType === 'sliding' ||\n this.#timeoutIds.size === 0 // new fixed window\n ) {\n const now = Date.now()\n const timeUntilExpiration = executionTime - now + this.#getWindow() + 1\n const timeoutId = setTimeout(() => {\n this.#cleanupOldExecutions()\n this.#clearTimeout(timeoutId)\n }, timeUntilExpiration)\n this.#timeoutIds.add(timeoutId)\n }\n }\n\n #clearTimeout = (timeoutId: NodeJS.Timeout): void => {\n clearTimeout(timeoutId)\n this.#timeoutIds.delete(timeoutId)\n }\n\n #clearTimeouts = (): void => {\n this.#timeoutIds.forEach((timeoutId) => clearTimeout(timeoutId))\n this.#timeoutIds.clear()\n }\n\n #cleanupOldExecutions = (): void => {\n this.#setState({\n executionTimes: this.#getExecutionTimesInWindow(),\n })\n }\n\n /**\n * Returns the number of remaining executions allowed in the current window\n */\n getRemainingInWindow = (): number => {\n const relevantExecutionTimes = this.#getExecutionTimesInWindow()\n return Math.max(0, this.#getLimit() - relevantExecutionTimes.length)\n }\n\n /**\n * Returns the number of milliseconds until the next execution will be possible\n */\n getMsUntilNextWindow = (): number => {\n if (this.getRemainingInWindow() > 0) {\n return 0\n }\n const oldestExecution = this.store.state.executionTimes[0] ?? Infinity\n return oldestExecution + this.#getWindow() - Date.now()\n }\n\n /**\n * Resets the rate limiter state\n */\n reset = (): void => {\n this.#setState(getDefaultRateLimiterState())\n this.#clearTimeouts()\n }\n}\n\n/**\n * Creates a rate-limited function that will execute the provided function up to a maximum number of times within a time window.\n *\n * This synchronous version is lighter weight and often all you need - upgrade to asyncRateLimit when you need promises, retry support, abort capabilities, or advanced error handling.\n *\n * Note that rate limiting is a simpler form of execution control compared to throttling or debouncing:\n * - A rate limiter will allow all executions until the limit is reached, then block all subsequent calls until the window resets\n * - A throttler ensures even spacing between executions, which can be better for consistent performance\n * - A debouncer collapses multiple calls into one, which is better for handling bursts of events\n *\n * The rate limiter supports two types of windows:\n * - 'fixed': A strict window that resets after the window period. All executions within the window count\n * towards the limit, and the window resets completely after the period.\n * - 'sliding': A rolling window that allows executions as old ones expire. This provides a more\n * consistent rate of execution over time.\n *\n * State Management:\n * - Uses TanStack Store for reactive state management\n * - Use `initialState` to provide initial state values when creating the rate limiter\n * - Use `onExecute` callback to react to function execution and implement custom logic\n * - Use `onReject` callback to react to executions being rejected when rate limit is exceeded\n * - The state includes execution count, execution times, and rejection count\n * - State can be accessed via the underlying RateLimiter instance's `store.state` property\n * - When using framework adapters (React/Solid), state is accessed from the hook's state property\n *\n * Consider using throttle() or debounce() if you need more intelligent execution control. Use rate limiting when you specifically\n * need to enforce a hard limit on the number of executions within a time period.\n *\n * @example\n * ```ts\n * // Rate limit to 5 calls per minute with a sliding window\n * const rateLimited = rateLimit(makeApiCall, {\n * limit: 5,\n * window: 60000,\n * windowType: 'sliding',\n * onReject: (rateLimiter) => {\n * console.log(`Rate limit exceeded. Try again in ${rateLimiter.getMsUntilNextWindow()}ms`);\n * }\n * });\n *\n * // First 5 calls will execute immediately\n * // Additional calls will be rejected until the minute window resets\n * rateLimited();\n *\n * // For more even execution, consider using throttle instead:\n * const throttled = throttle(makeApiCall, { wait: 12000 }); // One call every 12 seconds\n * ```\n */\nexport function rateLimit<TFn extends AnyFunction>(\n fn: TFn,\n initialOptions: RateLimiterOptions<TFn>,\n) {\n const rateLimiter = new RateLimiter(fn, initialOptions)\n return rateLimiter.maybeExecute\n}\n"],"mappings":"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"}
{"version":3,"file":"rate-limiter.cjs","names":["Store","#setState","#cleanupOldExecutions","#getExecutionTimesInWindow","#getLimit","#execute","#getWindow","#clearTimeouts","#setCleanupTimeout","#getEnabled","parseFunctionOrValue","#timeoutIds","#clearTimeout"],"sources":["../src/rate-limiter.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 RateLimiterState {\n /**\n * Number of function executions that have been completed\n */\n executionCount: number\n /**\n * Array of timestamps when executions occurred for rate limiting calculations\n */\n executionTimes: Array<number>\n /**\n * Whether the rate limiter has exceeded the limit\n */\n isExceeded: boolean\n /**\n * Number of times maybeExecute has been called (for reduction calculations)\n */\n maybeExecuteCount: number\n /**\n * Number of function executions that have been rejected due to rate limiting\n */\n rejectionCount: number\n /**\n * Current execution status - 'disabled' when not active, 'executing' when executing, 'idle' when not executing, 'exceeded' when rate limit is exceeded\n */\n status: 'disabled' | 'exceeded' | 'idle'\n}\n\nfunction getDefaultRateLimiterState(): RateLimiterState {\n return {\n executionCount: 0,\n executionTimes: [],\n isExceeded: false,\n rejectionCount: 0,\n status: 'idle',\n maybeExecuteCount: 0,\n }\n}\n\n/**\n * Options for configuring a rate-limited function\n */\nexport interface RateLimiterOptions<TFn extends AnyFunction> {\n /**\n * Whether the rate limiter is enabled. When disabled, maybeExecute will not trigger any executions.\n * Defaults to true.\n */\n enabled?: boolean | ((rateLimiter: RateLimiter<TFn>) => boolean)\n /**\n * Initial state for the rate limiter\n */\n initialState?: Partial<RateLimiterState>\n /**\n * Optional key to identify this rate limiter instance.\n * If provided, the rate limiter will be identified by this key in the devtools and PacerProvider if applicable.\n */\n key?: string\n /**\n * Maximum number of executions allowed within the time window.\n * Can be a number or a callback function that receives the rate limiter instance and returns a number.\n */\n limit: number | ((rateLimiter: RateLimiter<TFn>) => number)\n /**\n * Callback function that is called after the function is executed\n */\n onExecute?: (args: Parameters<TFn>, rateLimiter: RateLimiter<TFn>) => void\n /**\n * Optional callback function that is called when an execution is rejected due to rate limiting\n */\n onReject?: (rateLimiter: RateLimiter<TFn>) => void\n /**\n * Time window in milliseconds within which the limit applies.\n * Can be a number or a callback function that receives the rate limiter instance and returns a number.\n */\n window: number | ((rateLimiter: RateLimiter<TFn>) => number)\n /**\n * Type of window to use for rate limiting\n * - 'fixed': Uses a fixed window that resets after the window period\n * - 'sliding': Uses a sliding window that allows executions as old ones expire\n * Defaults to 'fixed'\n */\n windowType?: 'fixed' | 'sliding'\n}\n\n/**\n * Utility function for sharing common `RateLimiterOptions` options between different `RateLimiter` instances.\n */\nexport function rateLimiterOptions<\n TFn extends AnyFunction = AnyFunction,\n TOptions extends Partial<RateLimiterOptions<TFn>> = Partial<\n RateLimiterOptions<TFn>\n >,\n>(options: TOptions): TOptions {\n return options\n}\n\nconst defaultOptions: Omit<\n Required<RateLimiterOptions<any>>,\n 'initialState' | 'onExecute' | 'onReject' | 'key'\n> = {\n enabled: true,\n limit: 1,\n window: 0,\n windowType: 'fixed',\n}\n\n/**\n * A class that creates a rate-limited function.\n *\n * Rate limiting is a simple approach that allows a function to execute up to a limit within a time window,\n * then blocks all subsequent calls until the window passes. This can lead to \"bursty\" behavior where\n * all executions happen immediately, followed by a complete block.\n * This synchronous version is lighter weight and often all you need - upgrade to AsyncRateLimiter when you need promises, retry support, abort capabilities, or advanced error handling.\n *\n * The rate limiter supports two types of windows:\n * - 'fixed': A strict window that resets after the window period. All executions within the window count\n * towards the limit, and the window resets completely after the period.\n * - 'sliding': A rolling window that allows executions as old ones expire. This provides a more\n * consistent rate of execution over time.\n *\n * For smoother execution patterns, consider using:\n * - Throttling: Ensures consistent spacing between executions (e.g. max once per 200ms)\n * - Debouncing: Waits for a pause in calls before executing (e.g. after 500ms of no calls)\n *\n * Rate limiting is best used for hard API limits or resource constraints. For UI updates or\n * smoothing out frequent events, throttling or debouncing usually provide better user experience.\n *\n * State Management:\n * - Uses TanStack Store for reactive state management\n * - Use `initialState` to provide initial state values when creating the rate limiter\n * - Use `onExecute` callback to react to function execution and implement custom logic\n * - Use `onReject` callback to react to executions being rejected when rate limit is exceeded\n * - The state includes execution count, execution times, and rejection count\n * - State can be accessed via `rateLimiter.store.state` when using the class directly\n * - When using framework adapters (React/Solid), state is accessed from `rateLimiter.state`\n *\n * @example\n * ```ts\n * const rateLimiter = new RateLimiter(\n * (id: string) => api.getData(id),\n * {\n * limit: 5,\n * window: 1000,\n * windowType: 'sliding',\n * }\n * );\n *\n * // Will execute immediately until limit reached, then block\n * rateLimiter.maybeExecute('123');\n * ```\n */\nexport class RateLimiter<TFn extends AnyFunction> {\n readonly store: Store<Readonly<RateLimiterState>> =\n new Store<RateLimiterState>(getDefaultRateLimiterState())\n key: string | undefined\n options: RateLimiterOptions<TFn>\n #timeoutIds: Set<ReturnType<typeof setTimeout>> = new Set()\n\n constructor(\n public fn: TFn,\n initialOptions: RateLimiterOptions<TFn>,\n ) {\n this.key = initialOptions.key\n this.options = {\n ...defaultOptions,\n ...initialOptions,\n }\n this.#setState(this.options.initialState ?? {})\n for (const executionTime of this.#getExecutionTimesInWindow()) {\n this.#setCleanupTimeout(executionTime)\n }\n\n if (this.key) {\n pacerEventClient.on('d-RateLimiter', (event) => {\n if (event.payload.key !== this.key) return\n this.#setState(event.payload.store.state as Partial<RateLimiterState>)\n this.setOptions(\n event.payload.options as Partial<RateLimiterOptions<TFn>>,\n )\n })\n }\n }\n\n /**\n * Updates the rate limiter options\n */\n setOptions = (newOptions: Partial<RateLimiterOptions<TFn>>): void => {\n this.options = { ...this.options, ...newOptions }\n }\n\n #setState = (newState: Partial<RateLimiterState>): void => {\n this.store.setState((state) => {\n const combinedState = {\n ...state,\n ...newState,\n }\n const isExceeded = combinedState.executionTimes.length >= this.#getLimit()\n const status = !this.#getEnabled()\n ? 'disabled'\n : isExceeded\n ? 'exceeded'\n : 'idle'\n return {\n ...combinedState,\n isExceeded,\n status,\n }\n })\n emitChange('RateLimiter', this)\n }\n\n /**\n * Returns the current enabled state of the rate limiter\n */\n #getEnabled = (): boolean => {\n return !!parseFunctionOrValue(this.options.enabled, this)\n }\n\n /**\n * Returns the current limit of executions allowed within the time window\n */\n #getLimit = (): number => {\n return parseFunctionOrValue(this.options.limit, this)\n }\n\n /**\n * Returns the current time window in milliseconds\n */\n #getWindow = (): number => {\n return parseFunctionOrValue(this.options.window, this)\n }\n\n /**\n * Attempts to execute the rate-limited function if within the configured limits.\n * Will reject execution if the number of calls in the current window exceeds the limit.\n *\n * @example\n * ```ts\n * const rateLimiter = new RateLimiter(fn, { limit: 5, window: 1000 });\n *\n * // First 5 calls will return true\n * rateLimiter.maybeExecute('arg1', 'arg2'); // true\n *\n * // Additional calls within the window will return false\n * rateLimiter.maybeExecute('arg1', 'arg2'); // false\n * ```\n */\n maybeExecute = (...args: Parameters<TFn>): boolean => {\n this.#setState({\n maybeExecuteCount: this.store.state.maybeExecuteCount + 1,\n })\n\n this.#cleanupOldExecutions()\n\n const relevantExecutionTimes = this.#getExecutionTimesInWindow()\n\n if (relevantExecutionTimes.length < this.#getLimit()) {\n this.#execute(...args)\n return true\n }\n\n this.#setState({\n rejectionCount: this.store.state.rejectionCount + 1,\n })\n this.options.onReject?.(this)\n return false\n }\n\n #execute = (...args: Parameters<TFn>): void => {\n if (!this.#getEnabled()) return\n const now = Date.now()\n this.fn(...args) // EXECUTE!\n this.store.state.executionTimes.push(now) // mutate state directly for performance\n\n this.#setCleanupTimeout(now)\n\n this.#setState({\n executionCount: this.store.state.executionCount + 1,\n })\n this.options.onExecute?.(args, this)\n }\n\n #getExecutionTimesInWindow = (): Array<number> => {\n if (this.options.windowType === 'sliding') {\n // For sliding window, return all executions within the current window\n return this.store.state.executionTimes.filter(\n (time) => time > Date.now() - this.#getWindow(),\n )\n } else {\n // For fixed window, return all executions in the current window\n // The window starts from the oldest execution time\n if (this.store.state.executionTimes.length === 0) {\n return []\n }\n const oldestExecution = Math.min(...this.store.state.executionTimes)\n const windowStart = oldestExecution\n const windowEnd = windowStart + this.#getWindow()\n const now = Date.now()\n\n // If the window has expired, return empty array\n if (now > windowEnd) {\n return []\n }\n\n // Otherwise, return all executions in the current window\n return this.store.state.executionTimes.filter(\n (time) => time >= windowStart && time <= windowEnd,\n )\n }\n }\n\n #setCleanupTimeout = (executionTime: number): void => {\n if (\n this.options.windowType === 'sliding' ||\n this.#timeoutIds.size === 0 // new fixed window\n ) {\n const now = Date.now()\n const timeUntilExpiration = executionTime - now + this.#getWindow() + 1\n const timeoutId = setTimeout(() => {\n this.#cleanupOldExecutions()\n this.#clearTimeout(timeoutId)\n }, timeUntilExpiration)\n this.#timeoutIds.add(timeoutId)\n }\n }\n\n #clearTimeout = (timeoutId: ReturnType<typeof setTimeout>): void => {\n clearTimeout(timeoutId)\n this.#timeoutIds.delete(timeoutId)\n }\n\n #clearTimeouts = (): void => {\n this.#timeoutIds.forEach((timeoutId) => clearTimeout(timeoutId))\n this.#timeoutIds.clear()\n }\n\n #cleanupOldExecutions = (): void => {\n this.#setState({\n executionTimes: this.#getExecutionTimesInWindow(),\n })\n }\n\n /**\n * Returns the number of remaining executions allowed in the current window\n */\n getRemainingInWindow = (): number => {\n const relevantExecutionTimes = this.#getExecutionTimesInWindow()\n return Math.max(0, this.#getLimit() - relevantExecutionTimes.length)\n }\n\n /**\n * Returns the number of milliseconds until the next execution will be possible\n */\n getMsUntilNextWindow = (): number => {\n if (this.getRemainingInWindow() > 0) {\n return 0\n }\n const oldestExecution = this.store.state.executionTimes[0] ?? Infinity\n return oldestExecution + this.#getWindow() - Date.now()\n }\n\n /**\n * Resets the rate limiter state\n */\n reset = (): void => {\n this.#setState(getDefaultRateLimiterState())\n this.#clearTimeouts()\n }\n}\n\n/**\n * Creates a rate-limited function that will execute the provided function up to a maximum number of times within a time window.\n *\n * This synchronous version is lighter weight and often all you need - upgrade to asyncRateLimit when you need promises, retry support, abort capabilities, or advanced error handling.\n *\n * Note that rate limiting is a simpler form of execution control compared to throttling or debouncing:\n * - A rate limiter will allow all executions until the limit is reached, then block all subsequent calls until the window resets\n * - A throttler ensures even spacing between executions, which can be better for consistent performance\n * - A debouncer collapses multiple calls into one, which is better for handling bursts of events\n *\n * The rate limiter supports two types of windows:\n * - 'fixed': A strict window that resets after the window period. All executions within the window count\n * towards the limit, and the window resets completely after the period.\n * - 'sliding': A rolling window that allows executions as old ones expire. This provides a more\n * consistent rate of execution over time.\n *\n * State Management:\n * - Uses TanStack Store for reactive state management\n * - Use `initialState` to provide initial state values when creating the rate limiter\n * - Use `onExecute` callback to react to function execution and implement custom logic\n * - Use `onReject` callback to react to executions being rejected when rate limit is exceeded\n * - The state includes execution count, execution times, and rejection count\n * - State can be accessed via the underlying RateLimiter instance's `store.state` property\n * - When using framework adapters (React/Solid), state is accessed from the hook's state property\n *\n * Consider using throttle() or debounce() if you need more intelligent execution control. Use rate limiting when you specifically\n * need to enforce a hard limit on the number of executions within a time period.\n *\n * @example\n * ```ts\n * // Rate limit to 5 calls per minute with a sliding window\n * const rateLimited = rateLimit(makeApiCall, {\n * limit: 5,\n * window: 60000,\n * windowType: 'sliding',\n * onReject: (rateLimiter) => {\n * console.log(`Rate limit exceeded. Try again in ${rateLimiter.getMsUntilNextWindow()}ms`);\n * }\n * });\n *\n * // First 5 calls will execute immediately\n * // Additional calls will be rejected until the minute window resets\n * rateLimited();\n *\n * // For more even execution, consider using throttle instead:\n * const throttled = throttle(makeApiCall, { wait: 12000 }); // One call every 12 seconds\n * ```\n */\nexport function rateLimit<TFn extends AnyFunction>(\n fn: TFn,\n initialOptions: RateLimiterOptions<TFn>,\n) {\n const rateLimiter = new RateLimiter(fn, initialOptions)\n return rateLimiter.maybeExecute\n}\n"],"mappings":"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"}

@@ -1,1 +0,1 @@

{"version":3,"file":"rate-limiter.js","names":["#setState","#cleanupOldExecutions","#getExecutionTimesInWindow","#getLimit","#execute","#getWindow","#clearTimeouts","#setCleanupTimeout","#getEnabled","#timeoutIds","#clearTimeout"],"sources":["../src/rate-limiter.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 RateLimiterState {\n /**\n * Number of function executions that have been completed\n */\n executionCount: number\n /**\n * Array of timestamps when executions occurred for rate limiting calculations\n */\n executionTimes: Array<number>\n /**\n * Whether the rate limiter has exceeded the limit\n */\n isExceeded: boolean\n /**\n * Number of times maybeExecute has been called (for reduction calculations)\n */\n maybeExecuteCount: number\n /**\n * Number of function executions that have been rejected due to rate limiting\n */\n rejectionCount: number\n /**\n * Current execution status - 'disabled' when not active, 'executing' when executing, 'idle' when not executing, 'exceeded' when rate limit is exceeded\n */\n status: 'disabled' | 'exceeded' | 'idle'\n}\n\nfunction getDefaultRateLimiterState(): RateLimiterState {\n return {\n executionCount: 0,\n executionTimes: [],\n isExceeded: false,\n rejectionCount: 0,\n status: 'idle',\n maybeExecuteCount: 0,\n }\n}\n\n/**\n * Options for configuring a rate-limited function\n */\nexport interface RateLimiterOptions<TFn extends AnyFunction> {\n /**\n * Whether the rate limiter is enabled. When disabled, maybeExecute will not trigger any executions.\n * Defaults to true.\n */\n enabled?: boolean | ((rateLimiter: RateLimiter<TFn>) => boolean)\n /**\n * Initial state for the rate limiter\n */\n initialState?: Partial<RateLimiterState>\n /**\n * Optional key to identify this rate limiter instance.\n * If provided, the rate limiter will be identified by this key in the devtools and PacerProvider if applicable.\n */\n key?: string\n /**\n * Maximum number of executions allowed within the time window.\n * Can be a number or a callback function that receives the rate limiter instance and returns a number.\n */\n limit: number | ((rateLimiter: RateLimiter<TFn>) => number)\n /**\n * Callback function that is called after the function is executed\n */\n onExecute?: (args: Parameters<TFn>, rateLimiter: RateLimiter<TFn>) => void\n /**\n * Optional callback function that is called when an execution is rejected due to rate limiting\n */\n onReject?: (rateLimiter: RateLimiter<TFn>) => void\n /**\n * Time window in milliseconds within which the limit applies.\n * Can be a number or a callback function that receives the rate limiter instance and returns a number.\n */\n window: number | ((rateLimiter: RateLimiter<TFn>) => number)\n /**\n * Type of window to use for rate limiting\n * - 'fixed': Uses a fixed window that resets after the window period\n * - 'sliding': Uses a sliding window that allows executions as old ones expire\n * Defaults to 'fixed'\n */\n windowType?: 'fixed' | 'sliding'\n}\n\n/**\n * Utility function for sharing common `RateLimiterOptions` options between different `RateLimiter` instances.\n */\nexport function rateLimiterOptions<\n TFn extends AnyFunction = AnyFunction,\n TOptions extends Partial<RateLimiterOptions<TFn>> = Partial<\n RateLimiterOptions<TFn>\n >,\n>(options: TOptions): TOptions {\n return options\n}\n\nconst defaultOptions: Omit<\n Required<RateLimiterOptions<any>>,\n 'initialState' | 'onExecute' | 'onReject' | 'key'\n> = {\n enabled: true,\n limit: 1,\n window: 0,\n windowType: 'fixed',\n}\n\n/**\n * A class that creates a rate-limited function.\n *\n * Rate limiting is a simple approach that allows a function to execute up to a limit within a time window,\n * then blocks all subsequent calls until the window passes. This can lead to \"bursty\" behavior where\n * all executions happen immediately, followed by a complete block.\n * This synchronous version is lighter weight and often all you need - upgrade to AsyncRateLimiter when you need promises, retry support, abort capabilities, or advanced error handling.\n *\n * The rate limiter supports two types of windows:\n * - 'fixed': A strict window that resets after the window period. All executions within the window count\n * towards the limit, and the window resets completely after the period.\n * - 'sliding': A rolling window that allows executions as old ones expire. This provides a more\n * consistent rate of execution over time.\n *\n * For smoother execution patterns, consider using:\n * - Throttling: Ensures consistent spacing between executions (e.g. max once per 200ms)\n * - Debouncing: Waits for a pause in calls before executing (e.g. after 500ms of no calls)\n *\n * Rate limiting is best used for hard API limits or resource constraints. For UI updates or\n * smoothing out frequent events, throttling or debouncing usually provide better user experience.\n *\n * State Management:\n * - Uses TanStack Store for reactive state management\n * - Use `initialState` to provide initial state values when creating the rate limiter\n * - Use `onExecute` callback to react to function execution and implement custom logic\n * - Use `onReject` callback to react to executions being rejected when rate limit is exceeded\n * - The state includes execution count, execution times, and rejection count\n * - State can be accessed via `rateLimiter.store.state` when using the class directly\n * - When using framework adapters (React/Solid), state is accessed from `rateLimiter.state`\n *\n * @example\n * ```ts\n * const rateLimiter = new RateLimiter(\n * (id: string) => api.getData(id),\n * {\n * limit: 5,\n * window: 1000,\n * windowType: 'sliding',\n * }\n * );\n *\n * // Will execute immediately until limit reached, then block\n * rateLimiter.maybeExecute('123');\n * ```\n */\nexport class RateLimiter<TFn extends AnyFunction> {\n readonly store: Store<Readonly<RateLimiterState>> =\n new Store<RateLimiterState>(getDefaultRateLimiterState())\n key: string | undefined\n options: RateLimiterOptions<TFn>\n #timeoutIds: Set<NodeJS.Timeout> = new Set()\n\n constructor(\n public fn: TFn,\n initialOptions: RateLimiterOptions<TFn>,\n ) {\n this.key = initialOptions.key\n this.options = {\n ...defaultOptions,\n ...initialOptions,\n }\n this.#setState(this.options.initialState ?? {})\n for (const executionTime of this.#getExecutionTimesInWindow()) {\n this.#setCleanupTimeout(executionTime)\n }\n\n if (this.key) {\n pacerEventClient.on('d-RateLimiter', (event) => {\n if (event.payload.key !== this.key) return\n this.#setState(event.payload.store.state as Partial<RateLimiterState>)\n this.setOptions(\n event.payload.options as Partial<RateLimiterOptions<TFn>>,\n )\n })\n }\n }\n\n /**\n * Updates the rate limiter options\n */\n setOptions = (newOptions: Partial<RateLimiterOptions<TFn>>): void => {\n this.options = { ...this.options, ...newOptions }\n }\n\n #setState = (newState: Partial<RateLimiterState>): void => {\n this.store.setState((state) => {\n const combinedState = {\n ...state,\n ...newState,\n }\n const isExceeded = combinedState.executionTimes.length >= this.#getLimit()\n const status = !this.#getEnabled()\n ? 'disabled'\n : isExceeded\n ? 'exceeded'\n : 'idle'\n return {\n ...combinedState,\n isExceeded,\n status,\n }\n })\n emitChange('RateLimiter', this)\n }\n\n /**\n * Returns the current enabled state of the rate limiter\n */\n #getEnabled = (): boolean => {\n return !!parseFunctionOrValue(this.options.enabled, this)\n }\n\n /**\n * Returns the current limit of executions allowed within the time window\n */\n #getLimit = (): number => {\n return parseFunctionOrValue(this.options.limit, this)\n }\n\n /**\n * Returns the current time window in milliseconds\n */\n #getWindow = (): number => {\n return parseFunctionOrValue(this.options.window, this)\n }\n\n /**\n * Attempts to execute the rate-limited function if within the configured limits.\n * Will reject execution if the number of calls in the current window exceeds the limit.\n *\n * @example\n * ```ts\n * const rateLimiter = new RateLimiter(fn, { limit: 5, window: 1000 });\n *\n * // First 5 calls will return true\n * rateLimiter.maybeExecute('arg1', 'arg2'); // true\n *\n * // Additional calls within the window will return false\n * rateLimiter.maybeExecute('arg1', 'arg2'); // false\n * ```\n */\n maybeExecute = (...args: Parameters<TFn>): boolean => {\n this.#setState({\n maybeExecuteCount: this.store.state.maybeExecuteCount + 1,\n })\n\n this.#cleanupOldExecutions()\n\n const relevantExecutionTimes = this.#getExecutionTimesInWindow()\n\n if (relevantExecutionTimes.length < this.#getLimit()) {\n this.#execute(...args)\n return true\n }\n\n this.#setState({\n rejectionCount: this.store.state.rejectionCount + 1,\n })\n this.options.onReject?.(this)\n return false\n }\n\n #execute = (...args: Parameters<TFn>): void => {\n if (!this.#getEnabled()) return\n const now = Date.now()\n this.fn(...args) // EXECUTE!\n this.store.state.executionTimes.push(now) // mutate state directly for performance\n\n this.#setCleanupTimeout(now)\n\n this.#setState({\n executionCount: this.store.state.executionCount + 1,\n })\n this.options.onExecute?.(args, this)\n }\n\n #getExecutionTimesInWindow = (): Array<number> => {\n if (this.options.windowType === 'sliding') {\n // For sliding window, return all executions within the current window\n return this.store.state.executionTimes.filter(\n (time) => time > Date.now() - this.#getWindow(),\n )\n } else {\n // For fixed window, return all executions in the current window\n // The window starts from the oldest execution time\n if (this.store.state.executionTimes.length === 0) {\n return []\n }\n const oldestExecution = Math.min(...this.store.state.executionTimes)\n const windowStart = oldestExecution\n const windowEnd = windowStart + this.#getWindow()\n const now = Date.now()\n\n // If the window has expired, return empty array\n if (now > windowEnd) {\n return []\n }\n\n // Otherwise, return all executions in the current window\n return this.store.state.executionTimes.filter(\n (time) => time >= windowStart && time <= windowEnd,\n )\n }\n }\n\n #setCleanupTimeout = (executionTime: number): void => {\n if (\n this.options.windowType === 'sliding' ||\n this.#timeoutIds.size === 0 // new fixed window\n ) {\n const now = Date.now()\n const timeUntilExpiration = executionTime - now + this.#getWindow() + 1\n const timeoutId = setTimeout(() => {\n this.#cleanupOldExecutions()\n this.#clearTimeout(timeoutId)\n }, timeUntilExpiration)\n this.#timeoutIds.add(timeoutId)\n }\n }\n\n #clearTimeout = (timeoutId: NodeJS.Timeout): void => {\n clearTimeout(timeoutId)\n this.#timeoutIds.delete(timeoutId)\n }\n\n #clearTimeouts = (): void => {\n this.#timeoutIds.forEach((timeoutId) => clearTimeout(timeoutId))\n this.#timeoutIds.clear()\n }\n\n #cleanupOldExecutions = (): void => {\n this.#setState({\n executionTimes: this.#getExecutionTimesInWindow(),\n })\n }\n\n /**\n * Returns the number of remaining executions allowed in the current window\n */\n getRemainingInWindow = (): number => {\n const relevantExecutionTimes = this.#getExecutionTimesInWindow()\n return Math.max(0, this.#getLimit() - relevantExecutionTimes.length)\n }\n\n /**\n * Returns the number of milliseconds until the next execution will be possible\n */\n getMsUntilNextWindow = (): number => {\n if (this.getRemainingInWindow() > 0) {\n return 0\n }\n const oldestExecution = this.store.state.executionTimes[0] ?? Infinity\n return oldestExecution + this.#getWindow() - Date.now()\n }\n\n /**\n * Resets the rate limiter state\n */\n reset = (): void => {\n this.#setState(getDefaultRateLimiterState())\n this.#clearTimeouts()\n }\n}\n\n/**\n * Creates a rate-limited function that will execute the provided function up to a maximum number of times within a time window.\n *\n * This synchronous version is lighter weight and often all you need - upgrade to asyncRateLimit when you need promises, retry support, abort capabilities, or advanced error handling.\n *\n * Note that rate limiting is a simpler form of execution control compared to throttling or debouncing:\n * - A rate limiter will allow all executions until the limit is reached, then block all subsequent calls until the window resets\n * - A throttler ensures even spacing between executions, which can be better for consistent performance\n * - A debouncer collapses multiple calls into one, which is better for handling bursts of events\n *\n * The rate limiter supports two types of windows:\n * - 'fixed': A strict window that resets after the window period. All executions within the window count\n * towards the limit, and the window resets completely after the period.\n * - 'sliding': A rolling window that allows executions as old ones expire. This provides a more\n * consistent rate of execution over time.\n *\n * State Management:\n * - Uses TanStack Store for reactive state management\n * - Use `initialState` to provide initial state values when creating the rate limiter\n * - Use `onExecute` callback to react to function execution and implement custom logic\n * - Use `onReject` callback to react to executions being rejected when rate limit is exceeded\n * - The state includes execution count, execution times, and rejection count\n * - State can be accessed via the underlying RateLimiter instance's `store.state` property\n * - When using framework adapters (React/Solid), state is accessed from the hook's state property\n *\n * Consider using throttle() or debounce() if you need more intelligent execution control. Use rate limiting when you specifically\n * need to enforce a hard limit on the number of executions within a time period.\n *\n * @example\n * ```ts\n * // Rate limit to 5 calls per minute with a sliding window\n * const rateLimited = rateLimit(makeApiCall, {\n * limit: 5,\n * window: 60000,\n * windowType: 'sliding',\n * onReject: (rateLimiter) => {\n * console.log(`Rate limit exceeded. Try again in ${rateLimiter.getMsUntilNextWindow()}ms`);\n * }\n * });\n *\n * // First 5 calls will execute immediately\n * // Additional calls will be rejected until the minute window resets\n * rateLimited();\n *\n * // For more even execution, consider using throttle instead:\n * const throttled = throttle(makeApiCall, { wait: 12000 }); // One call every 12 seconds\n * ```\n */\nexport function rateLimit<TFn extends AnyFunction>(\n fn: TFn,\n initialOptions: RateLimiterOptions<TFn>,\n) {\n const rateLimiter = new RateLimiter(fn, initialOptions)\n return rateLimiter.maybeExecute\n}\n"],"mappings":"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"}
{"version":3,"file":"rate-limiter.js","names":["#setState","#cleanupOldExecutions","#getExecutionTimesInWindow","#getLimit","#execute","#getWindow","#clearTimeouts","#setCleanupTimeout","#getEnabled","#timeoutIds","#clearTimeout"],"sources":["../src/rate-limiter.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 RateLimiterState {\n /**\n * Number of function executions that have been completed\n */\n executionCount: number\n /**\n * Array of timestamps when executions occurred for rate limiting calculations\n */\n executionTimes: Array<number>\n /**\n * Whether the rate limiter has exceeded the limit\n */\n isExceeded: boolean\n /**\n * Number of times maybeExecute has been called (for reduction calculations)\n */\n maybeExecuteCount: number\n /**\n * Number of function executions that have been rejected due to rate limiting\n */\n rejectionCount: number\n /**\n * Current execution status - 'disabled' when not active, 'executing' when executing, 'idle' when not executing, 'exceeded' when rate limit is exceeded\n */\n status: 'disabled' | 'exceeded' | 'idle'\n}\n\nfunction getDefaultRateLimiterState(): RateLimiterState {\n return {\n executionCount: 0,\n executionTimes: [],\n isExceeded: false,\n rejectionCount: 0,\n status: 'idle',\n maybeExecuteCount: 0,\n }\n}\n\n/**\n * Options for configuring a rate-limited function\n */\nexport interface RateLimiterOptions<TFn extends AnyFunction> {\n /**\n * Whether the rate limiter is enabled. When disabled, maybeExecute will not trigger any executions.\n * Defaults to true.\n */\n enabled?: boolean | ((rateLimiter: RateLimiter<TFn>) => boolean)\n /**\n * Initial state for the rate limiter\n */\n initialState?: Partial<RateLimiterState>\n /**\n * Optional key to identify this rate limiter instance.\n * If provided, the rate limiter will be identified by this key in the devtools and PacerProvider if applicable.\n */\n key?: string\n /**\n * Maximum number of executions allowed within the time window.\n * Can be a number or a callback function that receives the rate limiter instance and returns a number.\n */\n limit: number | ((rateLimiter: RateLimiter<TFn>) => number)\n /**\n * Callback function that is called after the function is executed\n */\n onExecute?: (args: Parameters<TFn>, rateLimiter: RateLimiter<TFn>) => void\n /**\n * Optional callback function that is called when an execution is rejected due to rate limiting\n */\n onReject?: (rateLimiter: RateLimiter<TFn>) => void\n /**\n * Time window in milliseconds within which the limit applies.\n * Can be a number or a callback function that receives the rate limiter instance and returns a number.\n */\n window: number | ((rateLimiter: RateLimiter<TFn>) => number)\n /**\n * Type of window to use for rate limiting\n * - 'fixed': Uses a fixed window that resets after the window period\n * - 'sliding': Uses a sliding window that allows executions as old ones expire\n * Defaults to 'fixed'\n */\n windowType?: 'fixed' | 'sliding'\n}\n\n/**\n * Utility function for sharing common `RateLimiterOptions` options between different `RateLimiter` instances.\n */\nexport function rateLimiterOptions<\n TFn extends AnyFunction = AnyFunction,\n TOptions extends Partial<RateLimiterOptions<TFn>> = Partial<\n RateLimiterOptions<TFn>\n >,\n>(options: TOptions): TOptions {\n return options\n}\n\nconst defaultOptions: Omit<\n Required<RateLimiterOptions<any>>,\n 'initialState' | 'onExecute' | 'onReject' | 'key'\n> = {\n enabled: true,\n limit: 1,\n window: 0,\n windowType: 'fixed',\n}\n\n/**\n * A class that creates a rate-limited function.\n *\n * Rate limiting is a simple approach that allows a function to execute up to a limit within a time window,\n * then blocks all subsequent calls until the window passes. This can lead to \"bursty\" behavior where\n * all executions happen immediately, followed by a complete block.\n * This synchronous version is lighter weight and often all you need - upgrade to AsyncRateLimiter when you need promises, retry support, abort capabilities, or advanced error handling.\n *\n * The rate limiter supports two types of windows:\n * - 'fixed': A strict window that resets after the window period. All executions within the window count\n * towards the limit, and the window resets completely after the period.\n * - 'sliding': A rolling window that allows executions as old ones expire. This provides a more\n * consistent rate of execution over time.\n *\n * For smoother execution patterns, consider using:\n * - Throttling: Ensures consistent spacing between executions (e.g. max once per 200ms)\n * - Debouncing: Waits for a pause in calls before executing (e.g. after 500ms of no calls)\n *\n * Rate limiting is best used for hard API limits or resource constraints. For UI updates or\n * smoothing out frequent events, throttling or debouncing usually provide better user experience.\n *\n * State Management:\n * - Uses TanStack Store for reactive state management\n * - Use `initialState` to provide initial state values when creating the rate limiter\n * - Use `onExecute` callback to react to function execution and implement custom logic\n * - Use `onReject` callback to react to executions being rejected when rate limit is exceeded\n * - The state includes execution count, execution times, and rejection count\n * - State can be accessed via `rateLimiter.store.state` when using the class directly\n * - When using framework adapters (React/Solid), state is accessed from `rateLimiter.state`\n *\n * @example\n * ```ts\n * const rateLimiter = new RateLimiter(\n * (id: string) => api.getData(id),\n * {\n * limit: 5,\n * window: 1000,\n * windowType: 'sliding',\n * }\n * );\n *\n * // Will execute immediately until limit reached, then block\n * rateLimiter.maybeExecute('123');\n * ```\n */\nexport class RateLimiter<TFn extends AnyFunction> {\n readonly store: Store<Readonly<RateLimiterState>> =\n new Store<RateLimiterState>(getDefaultRateLimiterState())\n key: string | undefined\n options: RateLimiterOptions<TFn>\n #timeoutIds: Set<ReturnType<typeof setTimeout>> = new Set()\n\n constructor(\n public fn: TFn,\n initialOptions: RateLimiterOptions<TFn>,\n ) {\n this.key = initialOptions.key\n this.options = {\n ...defaultOptions,\n ...initialOptions,\n }\n this.#setState(this.options.initialState ?? {})\n for (const executionTime of this.#getExecutionTimesInWindow()) {\n this.#setCleanupTimeout(executionTime)\n }\n\n if (this.key) {\n pacerEventClient.on('d-RateLimiter', (event) => {\n if (event.payload.key !== this.key) return\n this.#setState(event.payload.store.state as Partial<RateLimiterState>)\n this.setOptions(\n event.payload.options as Partial<RateLimiterOptions<TFn>>,\n )\n })\n }\n }\n\n /**\n * Updates the rate limiter options\n */\n setOptions = (newOptions: Partial<RateLimiterOptions<TFn>>): void => {\n this.options = { ...this.options, ...newOptions }\n }\n\n #setState = (newState: Partial<RateLimiterState>): void => {\n this.store.setState((state) => {\n const combinedState = {\n ...state,\n ...newState,\n }\n const isExceeded = combinedState.executionTimes.length >= this.#getLimit()\n const status = !this.#getEnabled()\n ? 'disabled'\n : isExceeded\n ? 'exceeded'\n : 'idle'\n return {\n ...combinedState,\n isExceeded,\n status,\n }\n })\n emitChange('RateLimiter', this)\n }\n\n /**\n * Returns the current enabled state of the rate limiter\n */\n #getEnabled = (): boolean => {\n return !!parseFunctionOrValue(this.options.enabled, this)\n }\n\n /**\n * Returns the current limit of executions allowed within the time window\n */\n #getLimit = (): number => {\n return parseFunctionOrValue(this.options.limit, this)\n }\n\n /**\n * Returns the current time window in milliseconds\n */\n #getWindow = (): number => {\n return parseFunctionOrValue(this.options.window, this)\n }\n\n /**\n * Attempts to execute the rate-limited function if within the configured limits.\n * Will reject execution if the number of calls in the current window exceeds the limit.\n *\n * @example\n * ```ts\n * const rateLimiter = new RateLimiter(fn, { limit: 5, window: 1000 });\n *\n * // First 5 calls will return true\n * rateLimiter.maybeExecute('arg1', 'arg2'); // true\n *\n * // Additional calls within the window will return false\n * rateLimiter.maybeExecute('arg1', 'arg2'); // false\n * ```\n */\n maybeExecute = (...args: Parameters<TFn>): boolean => {\n this.#setState({\n maybeExecuteCount: this.store.state.maybeExecuteCount + 1,\n })\n\n this.#cleanupOldExecutions()\n\n const relevantExecutionTimes = this.#getExecutionTimesInWindow()\n\n if (relevantExecutionTimes.length < this.#getLimit()) {\n this.#execute(...args)\n return true\n }\n\n this.#setState({\n rejectionCount: this.store.state.rejectionCount + 1,\n })\n this.options.onReject?.(this)\n return false\n }\n\n #execute = (...args: Parameters<TFn>): void => {\n if (!this.#getEnabled()) return\n const now = Date.now()\n this.fn(...args) // EXECUTE!\n this.store.state.executionTimes.push(now) // mutate state directly for performance\n\n this.#setCleanupTimeout(now)\n\n this.#setState({\n executionCount: this.store.state.executionCount + 1,\n })\n this.options.onExecute?.(args, this)\n }\n\n #getExecutionTimesInWindow = (): Array<number> => {\n if (this.options.windowType === 'sliding') {\n // For sliding window, return all executions within the current window\n return this.store.state.executionTimes.filter(\n (time) => time > Date.now() - this.#getWindow(),\n )\n } else {\n // For fixed window, return all executions in the current window\n // The window starts from the oldest execution time\n if (this.store.state.executionTimes.length === 0) {\n return []\n }\n const oldestExecution = Math.min(...this.store.state.executionTimes)\n const windowStart = oldestExecution\n const windowEnd = windowStart + this.#getWindow()\n const now = Date.now()\n\n // If the window has expired, return empty array\n if (now > windowEnd) {\n return []\n }\n\n // Otherwise, return all executions in the current window\n return this.store.state.executionTimes.filter(\n (time) => time >= windowStart && time <= windowEnd,\n )\n }\n }\n\n #setCleanupTimeout = (executionTime: number): void => {\n if (\n this.options.windowType === 'sliding' ||\n this.#timeoutIds.size === 0 // new fixed window\n ) {\n const now = Date.now()\n const timeUntilExpiration = executionTime - now + this.#getWindow() + 1\n const timeoutId = setTimeout(() => {\n this.#cleanupOldExecutions()\n this.#clearTimeout(timeoutId)\n }, timeUntilExpiration)\n this.#timeoutIds.add(timeoutId)\n }\n }\n\n #clearTimeout = (timeoutId: ReturnType<typeof setTimeout>): void => {\n clearTimeout(timeoutId)\n this.#timeoutIds.delete(timeoutId)\n }\n\n #clearTimeouts = (): void => {\n this.#timeoutIds.forEach((timeoutId) => clearTimeout(timeoutId))\n this.#timeoutIds.clear()\n }\n\n #cleanupOldExecutions = (): void => {\n this.#setState({\n executionTimes: this.#getExecutionTimesInWindow(),\n })\n }\n\n /**\n * Returns the number of remaining executions allowed in the current window\n */\n getRemainingInWindow = (): number => {\n const relevantExecutionTimes = this.#getExecutionTimesInWindow()\n return Math.max(0, this.#getLimit() - relevantExecutionTimes.length)\n }\n\n /**\n * Returns the number of milliseconds until the next execution will be possible\n */\n getMsUntilNextWindow = (): number => {\n if (this.getRemainingInWindow() > 0) {\n return 0\n }\n const oldestExecution = this.store.state.executionTimes[0] ?? Infinity\n return oldestExecution + this.#getWindow() - Date.now()\n }\n\n /**\n * Resets the rate limiter state\n */\n reset = (): void => {\n this.#setState(getDefaultRateLimiterState())\n this.#clearTimeouts()\n }\n}\n\n/**\n * Creates a rate-limited function that will execute the provided function up to a maximum number of times within a time window.\n *\n * This synchronous version is lighter weight and often all you need - upgrade to asyncRateLimit when you need promises, retry support, abort capabilities, or advanced error handling.\n *\n * Note that rate limiting is a simpler form of execution control compared to throttling or debouncing:\n * - A rate limiter will allow all executions until the limit is reached, then block all subsequent calls until the window resets\n * - A throttler ensures even spacing between executions, which can be better for consistent performance\n * - A debouncer collapses multiple calls into one, which is better for handling bursts of events\n *\n * The rate limiter supports two types of windows:\n * - 'fixed': A strict window that resets after the window period. All executions within the window count\n * towards the limit, and the window resets completely after the period.\n * - 'sliding': A rolling window that allows executions as old ones expire. This provides a more\n * consistent rate of execution over time.\n *\n * State Management:\n * - Uses TanStack Store for reactive state management\n * - Use `initialState` to provide initial state values when creating the rate limiter\n * - Use `onExecute` callback to react to function execution and implement custom logic\n * - Use `onReject` callback to react to executions being rejected when rate limit is exceeded\n * - The state includes execution count, execution times, and rejection count\n * - State can be accessed via the underlying RateLimiter instance's `store.state` property\n * - When using framework adapters (React/Solid), state is accessed from the hook's state property\n *\n * Consider using throttle() or debounce() if you need more intelligent execution control. Use rate limiting when you specifically\n * need to enforce a hard limit on the number of executions within a time period.\n *\n * @example\n * ```ts\n * // Rate limit to 5 calls per minute with a sliding window\n * const rateLimited = rateLimit(makeApiCall, {\n * limit: 5,\n * window: 60000,\n * windowType: 'sliding',\n * onReject: (rateLimiter) => {\n * console.log(`Rate limit exceeded. Try again in ${rateLimiter.getMsUntilNextWindow()}ms`);\n * }\n * });\n *\n * // First 5 calls will execute immediately\n * // Additional calls will be rejected until the minute window resets\n * rateLimited();\n *\n * // For more even execution, consider using throttle instead:\n * const throttled = throttle(makeApiCall, { wait: 12000 }); // One call every 12 seconds\n * ```\n */\nexport function rateLimit<TFn extends AnyFunction>(\n fn: TFn,\n initialOptions: RateLimiterOptions<TFn>,\n) {\n const rateLimiter = new RateLimiter(fn, initialOptions)\n return rateLimiter.maybeExecute\n}\n"],"mappings":"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"}

@@ -1,1 +0,1 @@

{"version":3,"file":"throttler.cjs","names":["Store","#getEnabled","#setState","#getWait","#execute","#timeoutId","#clearTimeout","parseFunctionOrValue"],"sources":["../src/throttler.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 ThrottlerState<TFn extends AnyFunction> {\n /**\n * Number of function executions that have been completed\n */\n executionCount: number\n /**\n * Whether the throttler is waiting for the timeout to trigger execution\n */\n isPending: boolean\n /**\n * The arguments from the most recent call to maybeExecute\n */\n lastArgs: Parameters<TFn> | undefined\n /**\n * Timestamp of the last function execution in milliseconds\n */\n lastExecutionTime: number\n /**\n * Number of times maybeExecute has been called (for reduction calculations)\n */\n maybeExecuteCount: number\n /**\n * Timestamp when the next execution can occur in milliseconds\n */\n nextExecutionTime: number | undefined\n /**\n * Current execution status - 'idle' when not active, 'pending' when waiting for timeout\n */\n status: 'disabled' | 'idle' | 'pending'\n}\n\nfunction getDefaultThrottlerState<\n TFn extends AnyFunction,\n>(): ThrottlerState<TFn> {\n return {\n executionCount: 0,\n isPending: false,\n lastArgs: undefined,\n lastExecutionTime: 0,\n nextExecutionTime: 0,\n status: 'idle',\n maybeExecuteCount: 0,\n }\n}\n\n/**\n * Options for configuring a throttled function\n */\nexport interface ThrottlerOptions<TFn extends AnyFunction> {\n /**\n * Whether the throttler is enabled. When disabled, maybeExecute will not trigger any executions.\n * Can be a boolean or a function that returns a boolean.\n * Defaults to true.\n */\n enabled?: boolean | ((throttler: Throttler<TFn>) => boolean)\n /**\n * Initial state for the throttler\n */\n initialState?: Partial<ThrottlerState<TFn>>\n /**\n * A key to identify the throttler.\n * If provided, the throttler will be identified by this key in the devtools and PacerProvider if applicable.\n */\n key?: string\n /**\n * Whether to execute on the leading edge of the timeout.\n * Defaults to true.\n */\n leading?: boolean\n /**\n * Callback function that is called after the function is executed\n */\n onExecute?: (args: Parameters<TFn>, throttler: Throttler<TFn>) => void\n /**\n * Whether to execute on the trailing edge of the timeout.\n * Defaults to true.\n */\n trailing?: boolean\n /**\n * Time window in milliseconds during which the function can only be executed once.\n * Can be a number or a function that returns a number.\n * Defaults to 0ms\n */\n wait: number | ((throttler: Throttler<TFn>) => number)\n}\n\n/**\n * Utility function for sharing common `ThrottlerOptions` options between different `Throttler` instances.\n */\nexport function throttlerOptions<\n TFn extends AnyFunction = AnyFunction,\n TOptions extends Partial<ThrottlerOptions<TFn>> = Partial<\n ThrottlerOptions<TFn>\n >,\n>(options: TOptions): TOptions {\n return options\n}\n\nconst defaultOptions: Omit<\n Required<ThrottlerOptions<any>>,\n 'initialState' | 'onExecute' | 'key'\n> = {\n enabled: true,\n leading: true,\n trailing: true,\n wait: 0,\n}\n\n/**\n * A class that creates a throttled function.\n *\n * Throttling ensures a function is called at most once within a specified time window.\n * Unlike debouncing which waits for a pause in calls, throttling guarantees consistent\n * execution timing regardless of call frequency.\n * This synchronous version is lighter weight and often all you need - upgrade to AsyncThrottler when you need promises, retry support, abort/cancel capabilities, or advanced error handling.\n *\n * Supports both leading and trailing edge execution:\n * - Leading: Execute immediately on first call (default: true)\n * - Trailing: Execute after wait period if called during throttle (default: true)\n *\n * For collapsing rapid-fire events where you only care about the last call, consider using Debouncer.\n *\n * State Management:\n * - Uses TanStack Store for reactive state management\n * - Use `initialState` to provide initial state values when creating the throttler\n * - Use `onExecute` callback to react to function execution and implement custom logic\n * - The state includes execution count, last execution time, pending status, and more\n * - State can be accessed via `throttler.store.state` when using the class directly\n * - When using framework adapters (React/Solid), state is accessed from `throttler.state`\n *\n * @example\n * ```ts\n * const throttler = new Throttler(\n * (id: string) => api.getData(id),\n * { wait: 1000 } // Execute at most once per second\n * );\n *\n * // First call executes immediately\n * throttler.maybeExecute('123');\n *\n * // Subsequent calls within 1000ms are throttled\n * throttler.maybeExecute('123'); // Throttled\n * ```\n */\nexport class Throttler<TFn extends AnyFunction> {\n readonly store: Store<Readonly<ThrottlerState<TFn>>> = new Store(\n getDefaultThrottlerState<TFn>(),\n )\n key: string | undefined\n options: ThrottlerOptions<TFn>\n #timeoutId: NodeJS.Timeout | undefined\n\n constructor(\n public fn: TFn,\n initialOptions: ThrottlerOptions<TFn>,\n ) {\n this.key = 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-Throttler', (event) => {\n if (event.payload.key !== this.key) return\n this.#setState(\n event.payload.store.state as Partial<ThrottlerState<TFn>>,\n )\n this.setOptions(event.payload.options as Partial<ThrottlerOptions<TFn>>)\n })\n }\n }\n\n /**\n * Updates the throttler options\n */\n setOptions = (newOptions: Partial<ThrottlerOptions<TFn>>): void => {\n this.options = { ...this.options, ...newOptions }\n\n // Cancel pending execution if the throttler is disabled\n if (!this.#getEnabled()) {\n this.cancel()\n }\n }\n\n #setState = (newState: Partial<ThrottlerState<TFn>>): void => {\n this.store.setState((state) => {\n const combinedState = {\n ...state,\n ...newState,\n }\n const { isPending } = combinedState\n return {\n ...combinedState,\n status: !this.#getEnabled()\n ? 'disabled'\n : isPending\n ? 'pending'\n : 'idle',\n }\n })\n emitChange('Throttler', this)\n }\n\n #getEnabled = (): boolean => {\n return !!parseFunctionOrValue(this.options.enabled, this)\n }\n\n #getWait = (): number => {\n return parseFunctionOrValue(this.options.wait, this)\n }\n\n /**\n * Attempts to execute the throttled function. The execution behavior depends on the throttler options:\n *\n * - If enough time has passed since the last execution (>= wait period):\n * - With leading=true: Executes immediately\n * - With leading=false: Waits for the next trailing execution\n *\n * - If within the wait period:\n * - With trailing=true: Schedules execution for end of wait period\n * - With trailing=false: Drops the execution\n *\n * @example\n * ```ts\n * const throttled = new Throttler(fn, { wait: 1000 });\n *\n * // First call executes immediately\n * throttled.maybeExecute('a', 'b');\n *\n * // Call during wait period - gets throttled\n * throttled.maybeExecute('c', 'd');\n * ```\n */\n maybeExecute = (...args: Parameters<TFn>): void => {\n this.#setState({\n maybeExecuteCount: this.store.state.maybeExecuteCount + 1,\n })\n\n const now = Date.now()\n const timeSinceLastExecution = now - this.store.state.lastExecutionTime\n const wait = this.#getWait()\n\n // Handle leading execution\n if (this.options.leading && timeSinceLastExecution >= wait) {\n this.#execute(...args)\n } else {\n // Store the most recent arguments for potential trailing execution\n this.#setState({\n lastArgs: args,\n })\n // Set up trailing execution if not already scheduled\n if (!this.#timeoutId && this.options.trailing) {\n // prevent large number if lastExecutionTime is undefined\n const _timeSinceLastExecution = this.store.state.lastExecutionTime\n ? now - this.store.state.lastExecutionTime\n : 0\n const timeoutDuration = wait - _timeSinceLastExecution\n this.#setState({ isPending: true })\n this.#timeoutId = setTimeout(() => {\n const { lastArgs } = this.store.state\n if (lastArgs !== undefined) {\n this.#execute(...lastArgs)\n }\n }, timeoutDuration)\n }\n }\n }\n\n #execute = (...args: Parameters<TFn>): void => {\n if (!this.#getEnabled()) return\n this.fn(...args) // EXECUTE!\n const lastExecutionTime = Date.now()\n const nextExecutionTime = lastExecutionTime + this.#getWait()\n this.#clearTimeout()\n this.#setState({\n executionCount: this.store.state.executionCount + 1,\n lastExecutionTime,\n nextExecutionTime,\n isPending: false,\n lastArgs: undefined,\n })\n this.options.onExecute?.(args, this)\n setTimeout(() => {\n if (!this.store.state.isPending) {\n this.#setState({ nextExecutionTime: undefined })\n }\n }, this.#getWait())\n }\n\n /**\n * Processes the current pending execution immediately\n */\n flush = (): void => {\n if (this.store.state.isPending && this.store.state.lastArgs) {\n this.#execute(...this.store.state.lastArgs)\n }\n }\n\n #clearTimeout = (): void => {\n if (this.#timeoutId) {\n clearTimeout(this.#timeoutId)\n this.#timeoutId = undefined\n }\n }\n\n /**\n * Cancels any pending trailing execution and clears internal state.\n *\n * If a trailing execution is scheduled (due to throttling with trailing=true),\n * this will prevent that execution from occurring. The internal timeout and\n * stored arguments will be cleared.\n *\n * Has no effect if there is no pending execution.\n */\n cancel = (): void => {\n this.#clearTimeout()\n this.#setState({\n lastArgs: undefined,\n isPending: false,\n })\n }\n\n /**\n * Resets the throttler state to its default values\n */\n reset = (): void => {\n this.#setState(getDefaultThrottlerState<TFn>())\n }\n}\n\n/**\n * Creates a throttled function that limits how often the provided function can execute.\n *\n * This synchronous version is lighter weight and often all you need - upgrade to asyncThrottle when you need promises, retry support, abort/cancel capabilities, or advanced error handling.\n *\n * Throttling ensures a function executes at most once within a specified time window,\n * regardless of how many times it is called. This is useful for rate-limiting\n * expensive operations or UI updates.\n *\n * The throttled function can be configured to execute on the leading and/or trailing\n * edge of the throttle window via options.\n *\n * For handling bursts of events, consider using debounce() instead. For hard execution\n * limits, consider using rateLimit().\n *\n * State Management:\n * - Uses TanStack Store for reactive state management\n * - Use `initialState` to provide initial state values when creating the throttler\n * - Use `onExecute` callback to react to function execution and implement custom logic\n * - The state includes execution count, last execution time, pending status, and more\n * - State can be accessed via the underlying Throttler instance's `store.state` property\n * - When using framework adapters (React/Solid), state is accessed from the hook's state property\n *\n * @example\n * ```ts\n * // Basic throttling - max once per second\n * const throttled = throttle(updateUI, { wait: 1000 });\n *\n * // Configure leading/trailing execution\n * const throttled = throttle(saveData, {\n * wait: 2000,\n * leading: true, // Execute immediately on first call\n * trailing: true // Execute again after delay if called during wait\n * });\n * ```\n */\nexport function throttle<TFn extends AnyFunction>(\n fn: TFn,\n initialOptions: ThrottlerOptions<TFn>,\n) {\n const throttler = new Throttler(fn, initialOptions)\n return throttler.maybeExecute\n}\n"],"mappings":"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"}
{"version":3,"file":"throttler.cjs","names":["Store","#getEnabled","#setState","#getWait","#execute","#timeoutId","#clearTimeout","parseFunctionOrValue"],"sources":["../src/throttler.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 ThrottlerState<TFn extends AnyFunction> {\n /**\n * Number of function executions that have been completed\n */\n executionCount: number\n /**\n * Whether the throttler is waiting for the timeout to trigger execution\n */\n isPending: boolean\n /**\n * The arguments from the most recent call to maybeExecute\n */\n lastArgs: Parameters<TFn> | undefined\n /**\n * Timestamp of the last function execution in milliseconds\n */\n lastExecutionTime: number\n /**\n * Number of times maybeExecute has been called (for reduction calculations)\n */\n maybeExecuteCount: number\n /**\n * Timestamp when the next execution can occur in milliseconds\n */\n nextExecutionTime: number | undefined\n /**\n * Current execution status - 'idle' when not active, 'pending' when waiting for timeout\n */\n status: 'disabled' | 'idle' | 'pending'\n}\n\nfunction getDefaultThrottlerState<\n TFn extends AnyFunction,\n>(): ThrottlerState<TFn> {\n return {\n executionCount: 0,\n isPending: false,\n lastArgs: undefined,\n lastExecutionTime: 0,\n nextExecutionTime: 0,\n status: 'idle',\n maybeExecuteCount: 0,\n }\n}\n\n/**\n * Options for configuring a throttled function\n */\nexport interface ThrottlerOptions<TFn extends AnyFunction> {\n /**\n * Whether the throttler is enabled. When disabled, maybeExecute will not trigger any executions.\n * Can be a boolean or a function that returns a boolean.\n * Defaults to true.\n */\n enabled?: boolean | ((throttler: Throttler<TFn>) => boolean)\n /**\n * Initial state for the throttler\n */\n initialState?: Partial<ThrottlerState<TFn>>\n /**\n * A key to identify the throttler.\n * If provided, the throttler will be identified by this key in the devtools and PacerProvider if applicable.\n */\n key?: string\n /**\n * Whether to execute on the leading edge of the timeout.\n * Defaults to true.\n */\n leading?: boolean\n /**\n * Callback function that is called after the function is executed\n */\n onExecute?: (args: Parameters<TFn>, throttler: Throttler<TFn>) => void\n /**\n * Whether to execute on the trailing edge of the timeout.\n * Defaults to true.\n */\n trailing?: boolean\n /**\n * Time window in milliseconds during which the function can only be executed once.\n * Can be a number or a function that returns a number.\n * Defaults to 0ms\n */\n wait: number | ((throttler: Throttler<TFn>) => number)\n}\n\n/**\n * Utility function for sharing common `ThrottlerOptions` options between different `Throttler` instances.\n */\nexport function throttlerOptions<\n TFn extends AnyFunction = AnyFunction,\n TOptions extends Partial<ThrottlerOptions<TFn>> = Partial<\n ThrottlerOptions<TFn>\n >,\n>(options: TOptions): TOptions {\n return options\n}\n\nconst defaultOptions: Omit<\n Required<ThrottlerOptions<any>>,\n 'initialState' | 'onExecute' | 'key'\n> = {\n enabled: true,\n leading: true,\n trailing: true,\n wait: 0,\n}\n\n/**\n * A class that creates a throttled function.\n *\n * Throttling ensures a function is called at most once within a specified time window.\n * Unlike debouncing which waits for a pause in calls, throttling guarantees consistent\n * execution timing regardless of call frequency.\n * This synchronous version is lighter weight and often all you need - upgrade to AsyncThrottler when you need promises, retry support, abort/cancel capabilities, or advanced error handling.\n *\n * Supports both leading and trailing edge execution:\n * - Leading: Execute immediately on first call (default: true)\n * - Trailing: Execute after wait period if called during throttle (default: true)\n *\n * For collapsing rapid-fire events where you only care about the last call, consider using Debouncer.\n *\n * State Management:\n * - Uses TanStack Store for reactive state management\n * - Use `initialState` to provide initial state values when creating the throttler\n * - Use `onExecute` callback to react to function execution and implement custom logic\n * - The state includes execution count, last execution time, pending status, and more\n * - State can be accessed via `throttler.store.state` when using the class directly\n * - When using framework adapters (React/Solid), state is accessed from `throttler.state`\n *\n * @example\n * ```ts\n * const throttler = new Throttler(\n * (id: string) => api.getData(id),\n * { wait: 1000 } // Execute at most once per second\n * );\n *\n * // First call executes immediately\n * throttler.maybeExecute('123');\n *\n * // Subsequent calls within 1000ms are throttled\n * throttler.maybeExecute('123'); // Throttled\n * ```\n */\nexport class Throttler<TFn extends AnyFunction> {\n readonly store: Store<Readonly<ThrottlerState<TFn>>> = new Store(\n getDefaultThrottlerState<TFn>(),\n )\n key: string | undefined\n options: ThrottlerOptions<TFn>\n #timeoutId: ReturnType<typeof setTimeout> | undefined\n\n constructor(\n public fn: TFn,\n initialOptions: ThrottlerOptions<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-Throttler', (event) => {\n if (event.payload.key !== this.key) return\n this.#setState(\n event.payload.store.state as Partial<ThrottlerState<TFn>>,\n )\n this.setOptions(event.payload.options as Partial<ThrottlerOptions<TFn>>)\n })\n }\n }\n\n /**\n * Updates the throttler options\n */\n setOptions = (newOptions: Partial<ThrottlerOptions<TFn>>): void => {\n this.options = { ...this.options, ...newOptions }\n\n // Cancel pending execution if the throttler is disabled\n if (!this.#getEnabled()) {\n this.cancel()\n }\n }\n\n #setState = (newState: Partial<ThrottlerState<TFn>>): void => {\n this.store.setState((state) => {\n const combinedState = {\n ...state,\n ...newState,\n }\n const { isPending } = combinedState\n return {\n ...combinedState,\n status: !this.#getEnabled()\n ? 'disabled'\n : isPending\n ? 'pending'\n : 'idle',\n }\n })\n emitChange('Throttler', this)\n }\n\n #getEnabled = (): boolean => {\n return !!parseFunctionOrValue(this.options.enabled, this)\n }\n\n #getWait = (): number => {\n return parseFunctionOrValue(this.options.wait, this)\n }\n\n /**\n * Attempts to execute the throttled function. The execution behavior depends on the throttler options:\n *\n * - If enough time has passed since the last execution (>= wait period):\n * - With leading=true: Executes immediately\n * - With leading=false: Waits for the next trailing execution\n *\n * - If within the wait period:\n * - With trailing=true: Schedules execution for end of wait period\n * - With trailing=false: Drops the execution\n *\n * @example\n * ```ts\n * const throttled = new Throttler(fn, { wait: 1000 });\n *\n * // First call executes immediately\n * throttled.maybeExecute('a', 'b');\n *\n * // Call during wait period - gets throttled\n * throttled.maybeExecute('c', 'd');\n * ```\n */\n maybeExecute = (...args: Parameters<TFn>): void => {\n this.#setState({\n maybeExecuteCount: this.store.state.maybeExecuteCount + 1,\n })\n\n const now = Date.now()\n const timeSinceLastExecution = now - this.store.state.lastExecutionTime\n const wait = this.#getWait()\n\n // Handle leading execution\n if (this.options.leading && timeSinceLastExecution >= wait) {\n this.#execute(...args)\n } else {\n // Store the most recent arguments for potential trailing execution\n this.#setState({\n lastArgs: args,\n })\n // Set up trailing execution if not already scheduled\n if (!this.#timeoutId && this.options.trailing) {\n // prevent large number if lastExecutionTime is undefined\n const _timeSinceLastExecution = this.store.state.lastExecutionTime\n ? now - this.store.state.lastExecutionTime\n : 0\n const timeoutDuration = wait - _timeSinceLastExecution\n this.#setState({ isPending: true })\n this.#timeoutId = setTimeout(() => {\n const { lastArgs } = this.store.state\n if (lastArgs !== undefined) {\n this.#execute(...lastArgs)\n }\n }, timeoutDuration)\n }\n }\n }\n\n #execute = (...args: Parameters<TFn>): void => {\n if (!this.#getEnabled()) return\n this.fn(...args) // EXECUTE!\n const lastExecutionTime = Date.now()\n const nextExecutionTime = lastExecutionTime + this.#getWait()\n this.#clearTimeout()\n this.#setState({\n executionCount: this.store.state.executionCount + 1,\n lastExecutionTime,\n nextExecutionTime,\n isPending: false,\n lastArgs: undefined,\n })\n this.options.onExecute?.(args, this)\n setTimeout(() => {\n if (!this.store.state.isPending) {\n this.#setState({ nextExecutionTime: undefined })\n }\n }, this.#getWait())\n }\n\n /**\n * Processes the current pending execution immediately\n */\n flush = (): void => {\n if (this.store.state.isPending && this.store.state.lastArgs) {\n this.#execute(...this.store.state.lastArgs)\n }\n }\n\n #clearTimeout = (): void => {\n if (this.#timeoutId) {\n clearTimeout(this.#timeoutId)\n this.#timeoutId = undefined\n }\n }\n\n /**\n * Cancels any pending trailing execution and clears internal state.\n *\n * If a trailing execution is scheduled (due to throttling with trailing=true),\n * this will prevent that execution from occurring. The internal timeout and\n * stored arguments will be cleared.\n *\n * Has no effect if there is no pending execution.\n */\n cancel = (): void => {\n this.#clearTimeout()\n this.#setState({\n lastArgs: undefined,\n isPending: false,\n })\n }\n\n /**\n * Resets the throttler state to its default values\n */\n reset = (): void => {\n this.#setState(getDefaultThrottlerState<TFn>())\n }\n}\n\n/**\n * Creates a throttled function that limits how often the provided function can execute.\n *\n * This synchronous version is lighter weight and often all you need - upgrade to asyncThrottle when you need promises, retry support, abort/cancel capabilities, or advanced error handling.\n *\n * Throttling ensures a function executes at most once within a specified time window,\n * regardless of how many times it is called. This is useful for rate-limiting\n * expensive operations or UI updates.\n *\n * The throttled function can be configured to execute on the leading and/or trailing\n * edge of the throttle window via options.\n *\n * For handling bursts of events, consider using debounce() instead. For hard execution\n * limits, consider using rateLimit().\n *\n * State Management:\n * - Uses TanStack Store for reactive state management\n * - Use `initialState` to provide initial state values when creating the throttler\n * - Use `onExecute` callback to react to function execution and implement custom logic\n * - The state includes execution count, last execution time, pending status, and more\n * - State can be accessed via the underlying Throttler instance's `store.state` property\n * - When using framework adapters (React/Solid), state is accessed from the hook's state property\n *\n * @example\n * ```ts\n * // Basic throttling - max once per second\n * const throttled = throttle(updateUI, { wait: 1000 });\n *\n * // Configure leading/trailing execution\n * const throttled = throttle(saveData, {\n * wait: 2000,\n * leading: true, // Execute immediately on first call\n * trailing: true // Execute again after delay if called during wait\n * });\n * ```\n */\nexport function throttle<TFn extends AnyFunction>(\n fn: TFn,\n initialOptions: ThrottlerOptions<TFn>,\n) {\n const throttler = new Throttler(fn, initialOptions)\n return throttler.maybeExecute\n}\n"],"mappings":"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"}

@@ -1,1 +0,1 @@

{"version":3,"file":"throttler.js","names":["#getEnabled","#setState","#getWait","#execute","#timeoutId","#clearTimeout"],"sources":["../src/throttler.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 ThrottlerState<TFn extends AnyFunction> {\n /**\n * Number of function executions that have been completed\n */\n executionCount: number\n /**\n * Whether the throttler is waiting for the timeout to trigger execution\n */\n isPending: boolean\n /**\n * The arguments from the most recent call to maybeExecute\n */\n lastArgs: Parameters<TFn> | undefined\n /**\n * Timestamp of the last function execution in milliseconds\n */\n lastExecutionTime: number\n /**\n * Number of times maybeExecute has been called (for reduction calculations)\n */\n maybeExecuteCount: number\n /**\n * Timestamp when the next execution can occur in milliseconds\n */\n nextExecutionTime: number | undefined\n /**\n * Current execution status - 'idle' when not active, 'pending' when waiting for timeout\n */\n status: 'disabled' | 'idle' | 'pending'\n}\n\nfunction getDefaultThrottlerState<\n TFn extends AnyFunction,\n>(): ThrottlerState<TFn> {\n return {\n executionCount: 0,\n isPending: false,\n lastArgs: undefined,\n lastExecutionTime: 0,\n nextExecutionTime: 0,\n status: 'idle',\n maybeExecuteCount: 0,\n }\n}\n\n/**\n * Options for configuring a throttled function\n */\nexport interface ThrottlerOptions<TFn extends AnyFunction> {\n /**\n * Whether the throttler is enabled. When disabled, maybeExecute will not trigger any executions.\n * Can be a boolean or a function that returns a boolean.\n * Defaults to true.\n */\n enabled?: boolean | ((throttler: Throttler<TFn>) => boolean)\n /**\n * Initial state for the throttler\n */\n initialState?: Partial<ThrottlerState<TFn>>\n /**\n * A key to identify the throttler.\n * If provided, the throttler will be identified by this key in the devtools and PacerProvider if applicable.\n */\n key?: string\n /**\n * Whether to execute on the leading edge of the timeout.\n * Defaults to true.\n */\n leading?: boolean\n /**\n * Callback function that is called after the function is executed\n */\n onExecute?: (args: Parameters<TFn>, throttler: Throttler<TFn>) => void\n /**\n * Whether to execute on the trailing edge of the timeout.\n * Defaults to true.\n */\n trailing?: boolean\n /**\n * Time window in milliseconds during which the function can only be executed once.\n * Can be a number or a function that returns a number.\n * Defaults to 0ms\n */\n wait: number | ((throttler: Throttler<TFn>) => number)\n}\n\n/**\n * Utility function for sharing common `ThrottlerOptions` options between different `Throttler` instances.\n */\nexport function throttlerOptions<\n TFn extends AnyFunction = AnyFunction,\n TOptions extends Partial<ThrottlerOptions<TFn>> = Partial<\n ThrottlerOptions<TFn>\n >,\n>(options: TOptions): TOptions {\n return options\n}\n\nconst defaultOptions: Omit<\n Required<ThrottlerOptions<any>>,\n 'initialState' | 'onExecute' | 'key'\n> = {\n enabled: true,\n leading: true,\n trailing: true,\n wait: 0,\n}\n\n/**\n * A class that creates a throttled function.\n *\n * Throttling ensures a function is called at most once within a specified time window.\n * Unlike debouncing which waits for a pause in calls, throttling guarantees consistent\n * execution timing regardless of call frequency.\n * This synchronous version is lighter weight and often all you need - upgrade to AsyncThrottler when you need promises, retry support, abort/cancel capabilities, or advanced error handling.\n *\n * Supports both leading and trailing edge execution:\n * - Leading: Execute immediately on first call (default: true)\n * - Trailing: Execute after wait period if called during throttle (default: true)\n *\n * For collapsing rapid-fire events where you only care about the last call, consider using Debouncer.\n *\n * State Management:\n * - Uses TanStack Store for reactive state management\n * - Use `initialState` to provide initial state values when creating the throttler\n * - Use `onExecute` callback to react to function execution and implement custom logic\n * - The state includes execution count, last execution time, pending status, and more\n * - State can be accessed via `throttler.store.state` when using the class directly\n * - When using framework adapters (React/Solid), state is accessed from `throttler.state`\n *\n * @example\n * ```ts\n * const throttler = new Throttler(\n * (id: string) => api.getData(id),\n * { wait: 1000 } // Execute at most once per second\n * );\n *\n * // First call executes immediately\n * throttler.maybeExecute('123');\n *\n * // Subsequent calls within 1000ms are throttled\n * throttler.maybeExecute('123'); // Throttled\n * ```\n */\nexport class Throttler<TFn extends AnyFunction> {\n readonly store: Store<Readonly<ThrottlerState<TFn>>> = new Store(\n getDefaultThrottlerState<TFn>(),\n )\n key: string | undefined\n options: ThrottlerOptions<TFn>\n #timeoutId: NodeJS.Timeout | undefined\n\n constructor(\n public fn: TFn,\n initialOptions: ThrottlerOptions<TFn>,\n ) {\n this.key = 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-Throttler', (event) => {\n if (event.payload.key !== this.key) return\n this.#setState(\n event.payload.store.state as Partial<ThrottlerState<TFn>>,\n )\n this.setOptions(event.payload.options as Partial<ThrottlerOptions<TFn>>)\n })\n }\n }\n\n /**\n * Updates the throttler options\n */\n setOptions = (newOptions: Partial<ThrottlerOptions<TFn>>): void => {\n this.options = { ...this.options, ...newOptions }\n\n // Cancel pending execution if the throttler is disabled\n if (!this.#getEnabled()) {\n this.cancel()\n }\n }\n\n #setState = (newState: Partial<ThrottlerState<TFn>>): void => {\n this.store.setState((state) => {\n const combinedState = {\n ...state,\n ...newState,\n }\n const { isPending } = combinedState\n return {\n ...combinedState,\n status: !this.#getEnabled()\n ? 'disabled'\n : isPending\n ? 'pending'\n : 'idle',\n }\n })\n emitChange('Throttler', this)\n }\n\n #getEnabled = (): boolean => {\n return !!parseFunctionOrValue(this.options.enabled, this)\n }\n\n #getWait = (): number => {\n return parseFunctionOrValue(this.options.wait, this)\n }\n\n /**\n * Attempts to execute the throttled function. The execution behavior depends on the throttler options:\n *\n * - If enough time has passed since the last execution (>= wait period):\n * - With leading=true: Executes immediately\n * - With leading=false: Waits for the next trailing execution\n *\n * - If within the wait period:\n * - With trailing=true: Schedules execution for end of wait period\n * - With trailing=false: Drops the execution\n *\n * @example\n * ```ts\n * const throttled = new Throttler(fn, { wait: 1000 });\n *\n * // First call executes immediately\n * throttled.maybeExecute('a', 'b');\n *\n * // Call during wait period - gets throttled\n * throttled.maybeExecute('c', 'd');\n * ```\n */\n maybeExecute = (...args: Parameters<TFn>): void => {\n this.#setState({\n maybeExecuteCount: this.store.state.maybeExecuteCount + 1,\n })\n\n const now = Date.now()\n const timeSinceLastExecution = now - this.store.state.lastExecutionTime\n const wait = this.#getWait()\n\n // Handle leading execution\n if (this.options.leading && timeSinceLastExecution >= wait) {\n this.#execute(...args)\n } else {\n // Store the most recent arguments for potential trailing execution\n this.#setState({\n lastArgs: args,\n })\n // Set up trailing execution if not already scheduled\n if (!this.#timeoutId && this.options.trailing) {\n // prevent large number if lastExecutionTime is undefined\n const _timeSinceLastExecution = this.store.state.lastExecutionTime\n ? now - this.store.state.lastExecutionTime\n : 0\n const timeoutDuration = wait - _timeSinceLastExecution\n this.#setState({ isPending: true })\n this.#timeoutId = setTimeout(() => {\n const { lastArgs } = this.store.state\n if (lastArgs !== undefined) {\n this.#execute(...lastArgs)\n }\n }, timeoutDuration)\n }\n }\n }\n\n #execute = (...args: Parameters<TFn>): void => {\n if (!this.#getEnabled()) return\n this.fn(...args) // EXECUTE!\n const lastExecutionTime = Date.now()\n const nextExecutionTime = lastExecutionTime + this.#getWait()\n this.#clearTimeout()\n this.#setState({\n executionCount: this.store.state.executionCount + 1,\n lastExecutionTime,\n nextExecutionTime,\n isPending: false,\n lastArgs: undefined,\n })\n this.options.onExecute?.(args, this)\n setTimeout(() => {\n if (!this.store.state.isPending) {\n this.#setState({ nextExecutionTime: undefined })\n }\n }, this.#getWait())\n }\n\n /**\n * Processes the current pending execution immediately\n */\n flush = (): void => {\n if (this.store.state.isPending && this.store.state.lastArgs) {\n this.#execute(...this.store.state.lastArgs)\n }\n }\n\n #clearTimeout = (): void => {\n if (this.#timeoutId) {\n clearTimeout(this.#timeoutId)\n this.#timeoutId = undefined\n }\n }\n\n /**\n * Cancels any pending trailing execution and clears internal state.\n *\n * If a trailing execution is scheduled (due to throttling with trailing=true),\n * this will prevent that execution from occurring. The internal timeout and\n * stored arguments will be cleared.\n *\n * Has no effect if there is no pending execution.\n */\n cancel = (): void => {\n this.#clearTimeout()\n this.#setState({\n lastArgs: undefined,\n isPending: false,\n })\n }\n\n /**\n * Resets the throttler state to its default values\n */\n reset = (): void => {\n this.#setState(getDefaultThrottlerState<TFn>())\n }\n}\n\n/**\n * Creates a throttled function that limits how often the provided function can execute.\n *\n * This synchronous version is lighter weight and often all you need - upgrade to asyncThrottle when you need promises, retry support, abort/cancel capabilities, or advanced error handling.\n *\n * Throttling ensures a function executes at most once within a specified time window,\n * regardless of how many times it is called. This is useful for rate-limiting\n * expensive operations or UI updates.\n *\n * The throttled function can be configured to execute on the leading and/or trailing\n * edge of the throttle window via options.\n *\n * For handling bursts of events, consider using debounce() instead. For hard execution\n * limits, consider using rateLimit().\n *\n * State Management:\n * - Uses TanStack Store for reactive state management\n * - Use `initialState` to provide initial state values when creating the throttler\n * - Use `onExecute` callback to react to function execution and implement custom logic\n * - The state includes execution count, last execution time, pending status, and more\n * - State can be accessed via the underlying Throttler instance's `store.state` property\n * - When using framework adapters (React/Solid), state is accessed from the hook's state property\n *\n * @example\n * ```ts\n * // Basic throttling - max once per second\n * const throttled = throttle(updateUI, { wait: 1000 });\n *\n * // Configure leading/trailing execution\n * const throttled = throttle(saveData, {\n * wait: 2000,\n * leading: true, // Execute immediately on first call\n * trailing: true // Execute again after delay if called during wait\n * });\n * ```\n */\nexport function throttle<TFn extends AnyFunction>(\n fn: TFn,\n initialOptions: ThrottlerOptions<TFn>,\n) {\n const throttler = new Throttler(fn, initialOptions)\n return throttler.maybeExecute\n}\n"],"mappings":"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"}
{"version":3,"file":"throttler.js","names":["#getEnabled","#setState","#getWait","#execute","#timeoutId","#clearTimeout"],"sources":["../src/throttler.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 ThrottlerState<TFn extends AnyFunction> {\n /**\n * Number of function executions that have been completed\n */\n executionCount: number\n /**\n * Whether the throttler is waiting for the timeout to trigger execution\n */\n isPending: boolean\n /**\n * The arguments from the most recent call to maybeExecute\n */\n lastArgs: Parameters<TFn> | undefined\n /**\n * Timestamp of the last function execution in milliseconds\n */\n lastExecutionTime: number\n /**\n * Number of times maybeExecute has been called (for reduction calculations)\n */\n maybeExecuteCount: number\n /**\n * Timestamp when the next execution can occur in milliseconds\n */\n nextExecutionTime: number | undefined\n /**\n * Current execution status - 'idle' when not active, 'pending' when waiting for timeout\n */\n status: 'disabled' | 'idle' | 'pending'\n}\n\nfunction getDefaultThrottlerState<\n TFn extends AnyFunction,\n>(): ThrottlerState<TFn> {\n return {\n executionCount: 0,\n isPending: false,\n lastArgs: undefined,\n lastExecutionTime: 0,\n nextExecutionTime: 0,\n status: 'idle',\n maybeExecuteCount: 0,\n }\n}\n\n/**\n * Options for configuring a throttled function\n */\nexport interface ThrottlerOptions<TFn extends AnyFunction> {\n /**\n * Whether the throttler is enabled. When disabled, maybeExecute will not trigger any executions.\n * Can be a boolean or a function that returns a boolean.\n * Defaults to true.\n */\n enabled?: boolean | ((throttler: Throttler<TFn>) => boolean)\n /**\n * Initial state for the throttler\n */\n initialState?: Partial<ThrottlerState<TFn>>\n /**\n * A key to identify the throttler.\n * If provided, the throttler will be identified by this key in the devtools and PacerProvider if applicable.\n */\n key?: string\n /**\n * Whether to execute on the leading edge of the timeout.\n * Defaults to true.\n */\n leading?: boolean\n /**\n * Callback function that is called after the function is executed\n */\n onExecute?: (args: Parameters<TFn>, throttler: Throttler<TFn>) => void\n /**\n * Whether to execute on the trailing edge of the timeout.\n * Defaults to true.\n */\n trailing?: boolean\n /**\n * Time window in milliseconds during which the function can only be executed once.\n * Can be a number or a function that returns a number.\n * Defaults to 0ms\n */\n wait: number | ((throttler: Throttler<TFn>) => number)\n}\n\n/**\n * Utility function for sharing common `ThrottlerOptions` options between different `Throttler` instances.\n */\nexport function throttlerOptions<\n TFn extends AnyFunction = AnyFunction,\n TOptions extends Partial<ThrottlerOptions<TFn>> = Partial<\n ThrottlerOptions<TFn>\n >,\n>(options: TOptions): TOptions {\n return options\n}\n\nconst defaultOptions: Omit<\n Required<ThrottlerOptions<any>>,\n 'initialState' | 'onExecute' | 'key'\n> = {\n enabled: true,\n leading: true,\n trailing: true,\n wait: 0,\n}\n\n/**\n * A class that creates a throttled function.\n *\n * Throttling ensures a function is called at most once within a specified time window.\n * Unlike debouncing which waits for a pause in calls, throttling guarantees consistent\n * execution timing regardless of call frequency.\n * This synchronous version is lighter weight and often all you need - upgrade to AsyncThrottler when you need promises, retry support, abort/cancel capabilities, or advanced error handling.\n *\n * Supports both leading and trailing edge execution:\n * - Leading: Execute immediately on first call (default: true)\n * - Trailing: Execute after wait period if called during throttle (default: true)\n *\n * For collapsing rapid-fire events where you only care about the last call, consider using Debouncer.\n *\n * State Management:\n * - Uses TanStack Store for reactive state management\n * - Use `initialState` to provide initial state values when creating the throttler\n * - Use `onExecute` callback to react to function execution and implement custom logic\n * - The state includes execution count, last execution time, pending status, and more\n * - State can be accessed via `throttler.store.state` when using the class directly\n * - When using framework adapters (React/Solid), state is accessed from `throttler.state`\n *\n * @example\n * ```ts\n * const throttler = new Throttler(\n * (id: string) => api.getData(id),\n * { wait: 1000 } // Execute at most once per second\n * );\n *\n * // First call executes immediately\n * throttler.maybeExecute('123');\n *\n * // Subsequent calls within 1000ms are throttled\n * throttler.maybeExecute('123'); // Throttled\n * ```\n */\nexport class Throttler<TFn extends AnyFunction> {\n readonly store: Store<Readonly<ThrottlerState<TFn>>> = new Store(\n getDefaultThrottlerState<TFn>(),\n )\n key: string | undefined\n options: ThrottlerOptions<TFn>\n #timeoutId: ReturnType<typeof setTimeout> | undefined\n\n constructor(\n public fn: TFn,\n initialOptions: ThrottlerOptions<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-Throttler', (event) => {\n if (event.payload.key !== this.key) return\n this.#setState(\n event.payload.store.state as Partial<ThrottlerState<TFn>>,\n )\n this.setOptions(event.payload.options as Partial<ThrottlerOptions<TFn>>)\n })\n }\n }\n\n /**\n * Updates the throttler options\n */\n setOptions = (newOptions: Partial<ThrottlerOptions<TFn>>): void => {\n this.options = { ...this.options, ...newOptions }\n\n // Cancel pending execution if the throttler is disabled\n if (!this.#getEnabled()) {\n this.cancel()\n }\n }\n\n #setState = (newState: Partial<ThrottlerState<TFn>>): void => {\n this.store.setState((state) => {\n const combinedState = {\n ...state,\n ...newState,\n }\n const { isPending } = combinedState\n return {\n ...combinedState,\n status: !this.#getEnabled()\n ? 'disabled'\n : isPending\n ? 'pending'\n : 'idle',\n }\n })\n emitChange('Throttler', this)\n }\n\n #getEnabled = (): boolean => {\n return !!parseFunctionOrValue(this.options.enabled, this)\n }\n\n #getWait = (): number => {\n return parseFunctionOrValue(this.options.wait, this)\n }\n\n /**\n * Attempts to execute the throttled function. The execution behavior depends on the throttler options:\n *\n * - If enough time has passed since the last execution (>= wait period):\n * - With leading=true: Executes immediately\n * - With leading=false: Waits for the next trailing execution\n *\n * - If within the wait period:\n * - With trailing=true: Schedules execution for end of wait period\n * - With trailing=false: Drops the execution\n *\n * @example\n * ```ts\n * const throttled = new Throttler(fn, { wait: 1000 });\n *\n * // First call executes immediately\n * throttled.maybeExecute('a', 'b');\n *\n * // Call during wait period - gets throttled\n * throttled.maybeExecute('c', 'd');\n * ```\n */\n maybeExecute = (...args: Parameters<TFn>): void => {\n this.#setState({\n maybeExecuteCount: this.store.state.maybeExecuteCount + 1,\n })\n\n const now = Date.now()\n const timeSinceLastExecution = now - this.store.state.lastExecutionTime\n const wait = this.#getWait()\n\n // Handle leading execution\n if (this.options.leading && timeSinceLastExecution >= wait) {\n this.#execute(...args)\n } else {\n // Store the most recent arguments for potential trailing execution\n this.#setState({\n lastArgs: args,\n })\n // Set up trailing execution if not already scheduled\n if (!this.#timeoutId && this.options.trailing) {\n // prevent large number if lastExecutionTime is undefined\n const _timeSinceLastExecution = this.store.state.lastExecutionTime\n ? now - this.store.state.lastExecutionTime\n : 0\n const timeoutDuration = wait - _timeSinceLastExecution\n this.#setState({ isPending: true })\n this.#timeoutId = setTimeout(() => {\n const { lastArgs } = this.store.state\n if (lastArgs !== undefined) {\n this.#execute(...lastArgs)\n }\n }, timeoutDuration)\n }\n }\n }\n\n #execute = (...args: Parameters<TFn>): void => {\n if (!this.#getEnabled()) return\n this.fn(...args) // EXECUTE!\n const lastExecutionTime = Date.now()\n const nextExecutionTime = lastExecutionTime + this.#getWait()\n this.#clearTimeout()\n this.#setState({\n executionCount: this.store.state.executionCount + 1,\n lastExecutionTime,\n nextExecutionTime,\n isPending: false,\n lastArgs: undefined,\n })\n this.options.onExecute?.(args, this)\n setTimeout(() => {\n if (!this.store.state.isPending) {\n this.#setState({ nextExecutionTime: undefined })\n }\n }, this.#getWait())\n }\n\n /**\n * Processes the current pending execution immediately\n */\n flush = (): void => {\n if (this.store.state.isPending && this.store.state.lastArgs) {\n this.#execute(...this.store.state.lastArgs)\n }\n }\n\n #clearTimeout = (): void => {\n if (this.#timeoutId) {\n clearTimeout(this.#timeoutId)\n this.#timeoutId = undefined\n }\n }\n\n /**\n * Cancels any pending trailing execution and clears internal state.\n *\n * If a trailing execution is scheduled (due to throttling with trailing=true),\n * this will prevent that execution from occurring. The internal timeout and\n * stored arguments will be cleared.\n *\n * Has no effect if there is no pending execution.\n */\n cancel = (): void => {\n this.#clearTimeout()\n this.#setState({\n lastArgs: undefined,\n isPending: false,\n })\n }\n\n /**\n * Resets the throttler state to its default values\n */\n reset = (): void => {\n this.#setState(getDefaultThrottlerState<TFn>())\n }\n}\n\n/**\n * Creates a throttled function that limits how often the provided function can execute.\n *\n * This synchronous version is lighter weight and often all you need - upgrade to asyncThrottle when you need promises, retry support, abort/cancel capabilities, or advanced error handling.\n *\n * Throttling ensures a function executes at most once within a specified time window,\n * regardless of how many times it is called. This is useful for rate-limiting\n * expensive operations or UI updates.\n *\n * The throttled function can be configured to execute on the leading and/or trailing\n * edge of the throttle window via options.\n *\n * For handling bursts of events, consider using debounce() instead. For hard execution\n * limits, consider using rateLimit().\n *\n * State Management:\n * - Uses TanStack Store for reactive state management\n * - Use `initialState` to provide initial state values when creating the throttler\n * - Use `onExecute` callback to react to function execution and implement custom logic\n * - The state includes execution count, last execution time, pending status, and more\n * - State can be accessed via the underlying Throttler instance's `store.state` property\n * - When using framework adapters (React/Solid), state is accessed from the hook's state property\n *\n * @example\n * ```ts\n * // Basic throttling - max once per second\n * const throttled = throttle(updateUI, { wait: 1000 });\n *\n * // Configure leading/trailing execution\n * const throttled = throttle(saveData, {\n * wait: 2000,\n * leading: true, // Execute immediately on first call\n * trailing: true // Execute again after delay if called during wait\n * });\n * ```\n */\nexport function throttle<TFn extends AnyFunction>(\n fn: TFn,\n initialOptions: ThrottlerOptions<TFn>,\n) {\n const throttler = new Throttler(fn, initialOptions)\n return throttler.maybeExecute\n}\n"],"mappings":"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"}
{
"name": "@tanstack/pacer",
"version": "0.20.0",
"version": "0.20.1",
"description": "Utilities for debouncing, throttling, rate-limiting, queuing, and more.",

@@ -102,3 +102,3 @@ "author": "Tanner Linsley",

"@tanstack/devtools-event-client": "^0.4.3",
"@tanstack/store": "^0.9.2"
"@tanstack/store": "^0.9.3"
},

@@ -105,0 +105,0 @@ "scripts": {

@@ -275,3 +275,3 @@ import { Store } from '@tanstack/store'

>()
#timeoutId: NodeJS.Timeout | null = null
#timeoutId: ReturnType<typeof setTimeout> | null = null

@@ -278,0 +278,0 @@ constructor(

@@ -225,3 +225,3 @@ import { Store } from '@tanstack/store'

asyncRetryers = new Map<number, AsyncRetryer<TFn>>()
#timeoutId: NodeJS.Timeout | null = null
#timeoutId: ReturnType<typeof setTimeout> | null = null
#resolvePreviousPromise:

@@ -228,0 +228,0 @@ | ((value?: ReturnType<TFn> | undefined) => void)

@@ -325,3 +325,3 @@ import { Store } from '@tanstack/store'

>()
#timeoutIds: Set<NodeJS.Timeout> = new Set()
#timeoutIds: Set<ReturnType<typeof setTimeout>> = new Set()

@@ -328,0 +328,0 @@ constructor(

@@ -252,3 +252,3 @@ import { Store } from '@tanstack/store'

asyncRetryers = new Map<number, AsyncRetryer<TFn>>()
#timeoutIds: Set<NodeJS.Timeout> = new Set()
#timeoutIds: Set<ReturnType<typeof setTimeout>> = new Set()

@@ -473,3 +473,3 @@ constructor(

#clearTimeout = (timeoutId: NodeJS.Timeout): void => {
#clearTimeout = (timeoutId: ReturnType<typeof setTimeout>): void => {
clearTimeout(timeoutId)

@@ -476,0 +476,0 @@ this.#timeoutIds.delete(timeoutId)

@@ -454,3 +454,3 @@ import { Store } from '@tanstack/store'

// Set up total execution timeout
let totalTimeoutId: NodeJS.Timeout | undefined
let totalTimeoutId: ReturnType<typeof setTimeout> | undefined
if (this.options.maxTotalExecutionTime !== Infinity) {

@@ -457,0 +457,0 @@ totalTimeoutId = setTimeout(() => {

@@ -237,3 +237,3 @@ import { Store } from '@tanstack/store'

asyncRetryers = new Map<number, AsyncRetryer<TFn>>()
#timeoutId: NodeJS.Timeout | null = null
#timeoutId: ReturnType<typeof setTimeout> | null = null
#resolvePreviousPromise:

@@ -240,0 +240,0 @@ | ((value?: ReturnType<TFn> | undefined) => void)

@@ -151,3 +151,3 @@ import { Store } from '@tanstack/store'

options: BatcherOptionsWithOptionalCallbacks<TValue>
#timeoutId: NodeJS.Timeout | null = null
#timeoutId: ReturnType<typeof setTimeout> | null = null

@@ -154,0 +154,0 @@ constructor(

@@ -148,3 +148,3 @@ import { Store } from '@tanstack/store'

options: DebouncerOptions<TFn>
#timeoutId: NodeJS.Timeout | undefined
#timeoutId: ReturnType<typeof setTimeout> | undefined

@@ -151,0 +151,0 @@ constructor(

@@ -275,3 +275,3 @@ import { Store } from '@tanstack/store'

options: QueuerOptions<TValue>
#timeoutId: NodeJS.Timeout | null = null
#timeoutId: ReturnType<typeof setTimeout> | null = null

@@ -278,0 +278,0 @@ constructor(

@@ -161,3 +161,3 @@ import { Store } from '@tanstack/store'

options: RateLimiterOptions<TFn>
#timeoutIds: Set<NodeJS.Timeout> = new Set()
#timeoutIds: Set<ReturnType<typeof setTimeout>> = new Set()

@@ -332,3 +332,3 @@ constructor(

#clearTimeout = (timeoutId: NodeJS.Timeout): void => {
#clearTimeout = (timeoutId: ReturnType<typeof setTimeout>): void => {
clearTimeout(timeoutId)

@@ -335,0 +335,0 @@ this.#timeoutIds.delete(timeoutId)

@@ -156,3 +156,3 @@ import { Store } from '@tanstack/store'

options: ThrottlerOptions<TFn>
#timeoutId: NodeJS.Timeout | undefined
#timeoutId: ReturnType<typeof setTimeout> | undefined

@@ -159,0 +159,0 @@ constructor(