@tanstack/pacer
Advanced tools
| "use strict"; | ||
| Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" }); | ||
| const store = require("@tanstack/store"); | ||
| const utils = require("./utils.cjs"); | ||
| function getDefaultAsyncBatcherState() { | ||
| return { | ||
| errorCount: 0, | ||
| failedItems: [], | ||
| isEmpty: true, | ||
| isExecuting: false, | ||
| isPending: false, | ||
| isRunning: true, | ||
| items: [], | ||
| lastResult: void 0, | ||
| settleCount: 0, | ||
| size: 0, | ||
| status: "idle", | ||
| successCount: 0, | ||
| totalItemsProcessed: 0, | ||
| totalItemsFailed: 0 | ||
| }; | ||
| } | ||
| const defaultOptions = { | ||
| getShouldExecute: () => false, | ||
| maxSize: Infinity, | ||
| started: true, | ||
| throwOnError: true, | ||
| wait: Infinity | ||
| }; | ||
| class AsyncBatcher { | ||
| constructor(fn, initialOptions) { | ||
| this.fn = fn; | ||
| this.store = new store.Store( | ||
| getDefaultAsyncBatcherState() | ||
| ); | ||
| this.#timeoutId = null; | ||
| this.setOptions = (newOptions) => { | ||
| this.options = { ...this.options, ...newOptions }; | ||
| }; | ||
| this.#setState = (newState) => { | ||
| this.store.setState((state) => { | ||
| const combinedState = { | ||
| ...state, | ||
| ...newState | ||
| }; | ||
| const { isExecuting, isPending, items } = combinedState; | ||
| const size = items.length; | ||
| const isEmpty = size === 0; | ||
| return { | ||
| ...combinedState, | ||
| isEmpty, | ||
| size, | ||
| status: isExecuting ? "executing" : isPending ? "pending" : isEmpty ? "idle" : "populated" | ||
| }; | ||
| }); | ||
| }; | ||
| this.#getWait = () => { | ||
| return utils.parseFunctionOrValue(this.options.wait, this); | ||
| }; | ||
| this.addItem = (item) => { | ||
| this.#setState({ | ||
| items: [...this.store.state.items, item], | ||
| isPending: this.options.wait !== Infinity | ||
| }); | ||
| this.options.onItemsChange?.(this); | ||
| const shouldProcess = this.store.state.items.length >= this.options.maxSize || this.options.getShouldExecute(this.store.state.items, this); | ||
| if (shouldProcess) { | ||
| this.#execute(); | ||
| } else if (this.store.state.isRunning && this.options.wait !== Infinity) { | ||
| this.#clearTimeout(); | ||
| this.#timeoutId = setTimeout(() => this.#execute(), this.#getWait()); | ||
| } | ||
| }; | ||
| this.#execute = async () => { | ||
| if (this.store.state.items.length === 0) { | ||
| return void 0; | ||
| } | ||
| const batch = this.peekAllItems(); | ||
| this.clear(); | ||
| this.options.onItemsChange?.(this); | ||
| this.#setState({ isExecuting: true }); | ||
| try { | ||
| const result = await this.fn(batch); | ||
| this.#setState({ | ||
| totalItemsProcessed: this.store.state.totalItemsProcessed + batch.length, | ||
| lastResult: result, | ||
| successCount: this.store.state.successCount + 1 | ||
| }); | ||
| this.options.onSuccess?.(result, this); | ||
| return result; | ||
| } catch (error) { | ||
| this.#setState({ | ||
| errorCount: this.store.state.errorCount + 1, | ||
| failedItems: [...this.store.state.failedItems, ...batch], | ||
| totalItemsFailed: this.store.state.totalItemsFailed + batch.length | ||
| }); | ||
| this.options.onError?.(error, batch, this); | ||
| if (this.options.throwOnError) { | ||
| throw error; | ||
| } | ||
| return void 0; | ||
| } finally { | ||
| this.#setState({ | ||
| isExecuting: false, | ||
| settleCount: this.store.state.settleCount + 1 | ||
| }); | ||
| this.options.onSettled?.(this); | ||
| this.options.onExecute?.(this); | ||
| } | ||
| }; | ||
| this.flush = async () => { | ||
| this.#clearTimeout(); | ||
| return await this.#execute(); | ||
| }; | ||
| this.stop = () => { | ||
| this.#setState({ isRunning: false }); | ||
| this.#clearTimeout(); | ||
| }; | ||
| this.start = () => { | ||
| this.#setState({ isRunning: true }); | ||
| if (this.store.state.items.length > 0 && !this.#timeoutId) { | ||
| this.#timeoutId = setTimeout(() => this.#execute(), this.#getWait()); | ||
| } | ||
| }; | ||
| this.peekAllItems = () => { | ||
| return [...this.store.state.items]; | ||
| }; | ||
| this.peekFailedItems = () => { | ||
| return [...this.store.state.failedItems]; | ||
| }; | ||
| this.#clearTimeout = () => { | ||
| if (this.#timeoutId) { | ||
| clearTimeout(this.#timeoutId); | ||
| this.#timeoutId = null; | ||
| } | ||
| }; | ||
| this.clear = () => { | ||
| this.#setState({ items: [], failedItems: [], isPending: false }); | ||
| }; | ||
| this.reset = () => { | ||
| this.#setState(getDefaultAsyncBatcherState()); | ||
| this.options.onItemsChange?.(this); | ||
| }; | ||
| this.options = { | ||
| ...defaultOptions, | ||
| ...initialOptions, | ||
| throwOnError: initialOptions.throwOnError ?? !initialOptions.onError | ||
| }; | ||
| this.#setState(this.options.initialState ?? {}); | ||
| } | ||
| #timeoutId; | ||
| #setState; | ||
| #getWait; | ||
| #execute; | ||
| #clearTimeout; | ||
| } | ||
| function asyncBatch(fn, options) { | ||
| const batcher = new AsyncBatcher(fn, options); | ||
| return batcher.addItem; | ||
| } | ||
| exports.AsyncBatcher = AsyncBatcher; | ||
| exports.asyncBatch = asyncBatch; | ||
| //# sourceMappingURL=async-batcher.cjs.map |
| {"version":3,"file":"async-batcher.cjs","sources":["../../src/async-batcher.ts"],"sourcesContent":["import { Store } from '@tanstack/store'\nimport { parseFunctionOrValue } from './utils'\nimport type { OptionalKeys } from './types'\n\nexport interface AsyncBatcherState<TValue> {\n /**\n * Number of batch executions that have resulted in errors\n */\n errorCount: number\n /**\n * Array of items that failed during batch processing\n */\n failedItems: Array<TValue>\n /**\n * Whether the batcher has no items to process (items array is empty)\n */\n isEmpty: boolean\n /**\n * Whether a batch is currently being processed asynchronously\n */\n isExecuting: boolean\n /**\n * Whether the batcher is waiting for the timeout to trigger batch processing\n */\n isPending: boolean\n /**\n * Whether the batcher is active and will process items automatically\n */\n isRunning: 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 been processed across all batches\n */\n totalItemsProcessed: number\n /**\n * Total number of items that have failed processing across all batches\n */\n totalItemsFailed: number\n}\n\nfunction getDefaultAsyncBatcherState<TValue>(): AsyncBatcherState<TValue> {\n return {\n errorCount: 0,\n failedItems: [],\n isEmpty: true,\n isExecuting: false,\n isPending: false,\n isRunning: true,\n items: [],\n lastResult: undefined,\n settleCount: 0,\n size: 0,\n status: 'idle',\n successCount: 0,\n totalItemsProcessed: 0,\n totalItemsFailed: 0,\n }\n}\n\n/**\n * Options for configuring an AsyncBatcher instance\n */\nexport interface AsyncBatcherOptions<TValue> {\n /**\n * Custom function to determine if a batch should be processed\n * Return true to process the batch immediately\n */\n getShouldExecute?: (\n items: Array<TValue>,\n batcher: AsyncBatcher<TValue>,\n ) => boolean\n /**\n * Initial state for the async batcher\n */\n initialState?: Partial<AsyncBatcherState<TValue>>\n /**\n * 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 and batcher instance.\n * This can be used alongside throwOnError - the handler will be called before any error is thrown.\n */\n onError?: (\n error: unknown,\n failedItems: Array<TValue>,\n batcher: AsyncBatcher<TValue>,\n ) => void\n /**\n * Callback fired after a batch is processed\n */\n onExecute?: (batcher: AsyncBatcher<TValue>) => 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?: (batcher: AsyncBatcher<TValue>) => void\n /**\n * Optional callback to call when a batch succeeds\n */\n onSuccess?: (result: any, batcher: AsyncBatcher<TValue>) => void\n /**\n * Whether the batcher should start processing immediately\n * @default true\n */\n started?: boolean\n /**\n * Whether to throw errors when they occur.\n * Defaults to true if no onError handler is provided, false if an onError handler is provided.\n * Can be explicitly set to override these defaults.\n */\n throwOnError?: boolean\n /**\n * Maximum time in milliseconds to wait before processing a batch.\n * If the wait duration has elapsed, the batch will be processed.\n * If not provided, the batch will not be triggered by a timeout.\n * @default Infinity\n */\n wait?: number | ((asyncBatcher: AsyncBatcher<TValue>) => number)\n}\n\ntype AsyncBatcherOptionsWithOptionalCallbacks<TValue> = OptionalKeys<\n Required<AsyncBatcherOptions<TValue>>,\n | 'initialState'\n | 'onError'\n | 'onExecute'\n | 'onItemsChange'\n | 'onSettled'\n | 'onSuccess'\n>\n\nconst defaultOptions: AsyncBatcherOptionsWithOptionalCallbacks<any> = {\n getShouldExecute: () => false,\n maxSize: Infinity,\n started: true,\n throwOnError: true,\n wait: Infinity,\n}\n\n/**\n * A class that collects items and processes them in batches asynchronously.\n *\n * This is the async version of the Batcher class. Unlike the sync version, this async batcher:\n * - Handles promises and returns results from batch executions\n * - Provides error handling with configurable error behavior\n * - Tracks success, error, and settle counts separately\n * - Has state tracking for when batches are executing\n * - Returns the result of the batch function execution\n *\n * Batching is a technique for grouping multiple operations together to be processed as a single unit.\n *\n * The AsyncBatcher provides a flexible way to implement async batching with configurable:\n * - Maximum batch size (number of items per batch)\n * - Time-based batching (process after X milliseconds)\n * - Custom batch processing logic via getShouldExecute\n * - Event callbacks for monitoring batch operations\n * - Error handling for failed batch operations\n *\n * Error Handling:\n * - If an `onError` handler is provided, it will be called with the error 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 options: AsyncBatcherOptionsWithOptionalCallbacks<TValue>\n #timeoutId: NodeJS.Timeout | null = null\n\n constructor(\n private fn: (items: Array<TValue>) => Promise<any>,\n initialOptions: AsyncBatcherOptions<TValue>,\n ) {\n this.options = {\n ...defaultOptions,\n ...initialOptions,\n throwOnError: initialOptions.throwOnError ?? !initialOptions.onError,\n }\n this.#setState(this.options.initialState ?? {})\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 }\n\n #getWait = (): number => {\n return parseFunctionOrValue(this.options.wait, this)\n }\n\n /**\n * Adds an item to the async batcher\n * If the batch size is reached, timeout occurs, or shouldProcess returns true, the batch will be processed\n */\n addItem = (item: TValue): void => {\n this.#setState({\n items: [...this.store.state.items, item],\n isPending: this.options.wait !== Infinity,\n })\n this.options.onItemsChange?.(this)\n\n const shouldProcess =\n this.store.state.items.length >= this.options.maxSize ||\n this.options.getShouldExecute(this.store.state.items, this)\n\n if (shouldProcess) {\n this.#execute()\n } else if (this.store.state.isRunning && this.options.wait !== Infinity) {\n this.#clearTimeout() // clear any pending timeout to replace it with a new one\n this.#timeoutId = setTimeout(() => this.#execute(), this.#getWait())\n }\n }\n\n /**\n * Processes the current batch of items asynchronously.\n * This method will automatically be triggered if the batcher is running and any of these conditions are met:\n * - The number of items reaches maxSize\n * - The wait duration has elapsed\n * - The getShouldExecute function returns true upon adding an item\n *\n * You can also call this method manually to process the current batch at any time.\n *\n * @returns A promise that resolves with the result of the batch function, or undefined if an error occurred and was handled by onError\n * @throws The error from the batch function if no onError handler is configured\n */\n #execute = async (): Promise<any> => {\n if (this.store.state.items.length === 0) {\n return undefined\n }\n\n const batch = this.peekAllItems() // copy of the items to be processed (to prevent race conditions)\n this.clear() // Clear items before processing to prevent race conditions\n this.options.onItemsChange?.(this) // Call onItemsChange to notify listeners that the items have changed\n\n this.#setState({ isExecuting: true })\n\n try {\n const result = await this.fn(batch) // EXECUTE\n this.#setState({\n totalItemsProcessed:\n this.store.state.totalItemsProcessed + batch.length,\n lastResult: result,\n successCount: this.store.state.successCount + 1,\n })\n this.options.onSuccess?.(result, this)\n return result\n } catch (error) {\n this.#setState({\n errorCount: this.store.state.errorCount + 1,\n failedItems: [...this.store.state.failedItems, ...batch],\n totalItemsFailed: this.store.state.totalItemsFailed + batch.length,\n })\n this.options.onError?.(error, batch, this)\n if (this.options.throwOnError) {\n throw error\n }\n return undefined\n } finally {\n this.#setState({\n isExecuting: false,\n settleCount: this.store.state.settleCount + 1,\n })\n this.options.onSettled?.(this)\n this.options.onExecute?.(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 * Stops the async batcher from processing batches\n */\n stop = (): void => {\n this.#setState({ isRunning: false })\n this.#clearTimeout()\n }\n\n /**\n * Starts the async batcher and processes any pending items\n */\n start = (): void => {\n this.#setState({ isRunning: true })\n if (this.store.state.items.length > 0 && !this.#timeoutId) {\n this.#timeoutId = setTimeout(() => this.#execute(), this.#getWait())\n }\n }\n\n /**\n * Returns a copy of all items in the async batcher\n */\n peekAllItems = (): Array<TValue> => {\n return [...this.store.state.items]\n }\n\n peekFailedItems = (): Array<TValue> => {\n return [...this.store.state.failedItems]\n }\n\n #clearTimeout = (): void => {\n if (this.#timeoutId) {\n clearTimeout(this.#timeoutId)\n this.#timeoutId = null\n }\n }\n\n /**\n * Removes all items from the async batcher\n */\n clear = (): void => {\n this.#setState({ items: [], failedItems: [], isPending: false })\n }\n\n /**\n * Resets the async batcher state to its default values\n */\n reset = (): void => {\n this.#setState(getDefaultAsyncBatcherState<TValue>())\n this.options.onItemsChange?.(this)\n }\n}\n\n/**\n * Creates an async batcher that processes items in batches\n *\n * Unlike the sync batcher, this async version:\n * - Handles promises and returns results from batch executions\n * - Provides error handling with configurable error behavior\n * - Tracks success, error, and settle counts separately\n * - Has state tracking for when batches are executing\n *\n * Error Handling:\n * - If an `onError` handler is provided, it will be called with the error and batcher instance\n * - If `throwOnError` is true (default when no onError handler is provided), the error will be thrown\n * - If `throwOnError` is false (default when onError handler is provided), the error will be swallowed\n * - Both onError and throwOnError can be used together - the handler will be called before any error is thrown\n * - The error state can be checked using the underlying AsyncBatcher instance\n *\n * State Management:\n * - Uses TanStack Store for reactive state management\n * - Use `initialState` to provide initial state values when creating the async batcher\n * - Use `onSuccess` callback to react to successful batch execution and implement custom logic\n * - Use `onError` callback to react to batch execution errors and implement custom error handling\n * - Use `onSettled` callback to react to batch execution completion (success or error) and implement custom logic\n * - Use `onExecute` callback to react to batch execution and implement custom logic\n * - Use `onItemsChange` callback to react to items being added or removed from the batcher\n * - The state includes total items processed, success/error counts, and execution status\n * - State can be accessed via the underlying AsyncBatcher instance's `store.state` property\n * - When using framework adapters (React/Solid), state is accessed from the hook's state property\n *\n * @example\n * ```ts\n * const batchItems = asyncBatch<number>(\n * async (items) => {\n * const result = await processApiCall(items);\n * console.log('Processing:', items);\n * return result;\n * },\n * {\n * maxSize: 3,\n * wait: 1000,\n * onSuccess: (result) => console.log('Batch succeeded:', result),\n * onError: (error) => console.error('Batch failed:', error)\n * }\n * );\n *\n * batchItems(1);\n * batchItems(2);\n * batchItems(3); // Triggers batch processing\n * ```\n */\nexport function asyncBatch<TValue>(\n fn: (items: Array<TValue>) => Promise<any>,\n options: AsyncBatcherOptions<TValue>,\n) {\n const batcher = new AsyncBatcher<TValue>(fn, options)\n return batcher.addItem\n}\n"],"names":["Store","parseFunctionOrValue"],"mappings":"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"} |
| import { Store } from '@tanstack/store'; | ||
| import { OptionalKeys } from './types.cjs'; | ||
| export interface AsyncBatcherState<TValue> { | ||
| /** | ||
| * Number of batch executions that have resulted in errors | ||
| */ | ||
| errorCount: number; | ||
| /** | ||
| * Array of items that failed during batch processing | ||
| */ | ||
| failedItems: Array<TValue>; | ||
| /** | ||
| * Whether the batcher has no items to process (items array is empty) | ||
| */ | ||
| isEmpty: boolean; | ||
| /** | ||
| * Whether a batch is currently being processed asynchronously | ||
| */ | ||
| isExecuting: boolean; | ||
| /** | ||
| * Whether the batcher is waiting for the timeout to trigger batch processing | ||
| */ | ||
| isPending: boolean; | ||
| /** | ||
| * Whether the batcher is active and will process items automatically | ||
| */ | ||
| isRunning: boolean; | ||
| /** | ||
| * Array of items currently queued for batch processing | ||
| */ | ||
| items: Array<TValue>; | ||
| /** | ||
| * The result from the most recent batch execution | ||
| */ | ||
| lastResult: any; | ||
| /** | ||
| * Number of batch executions that have completed (either successfully or with errors) | ||
| */ | ||
| settleCount: number; | ||
| /** | ||
| * Number of items currently in the batch queue | ||
| */ | ||
| size: number; | ||
| /** | ||
| * 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 | ||
| */ | ||
| status: 'idle' | 'pending' | 'executing' | 'populated'; | ||
| /** | ||
| * Number of batch executions that have completed successfully | ||
| */ | ||
| successCount: number; | ||
| /** | ||
| * Total number of items that have been processed across all batches | ||
| */ | ||
| totalItemsProcessed: number; | ||
| /** | ||
| * Total number of items that have failed processing across all batches | ||
| */ | ||
| totalItemsFailed: number; | ||
| } | ||
| /** | ||
| * Options for configuring an AsyncBatcher instance | ||
| */ | ||
| export interface AsyncBatcherOptions<TValue> { | ||
| /** | ||
| * Custom function to determine if a batch should be processed | ||
| * Return true to process the batch immediately | ||
| */ | ||
| getShouldExecute?: (items: Array<TValue>, batcher: AsyncBatcher<TValue>) => boolean; | ||
| /** | ||
| * Initial state for the async batcher | ||
| */ | ||
| initialState?: Partial<AsyncBatcherState<TValue>>; | ||
| /** | ||
| * Maximum number of items in a batch | ||
| * @default Infinity | ||
| */ | ||
| maxSize?: number; | ||
| /** | ||
| * Optional error handler for when the batch function throws. | ||
| * If provided, the handler will be called with the error and batcher instance. | ||
| * This can be used alongside throwOnError - the handler will be called before any error is thrown. | ||
| */ | ||
| onError?: (error: unknown, failedItems: Array<TValue>, batcher: AsyncBatcher<TValue>) => void; | ||
| /** | ||
| * Callback fired after a batch is processed | ||
| */ | ||
| onExecute?: (batcher: AsyncBatcher<TValue>) => void; | ||
| /** | ||
| * Callback fired after items are added to the batcher | ||
| */ | ||
| onItemsChange?: (batcher: AsyncBatcher<TValue>) => void; | ||
| /** | ||
| * Optional callback to call when a batch is settled (completed or failed) | ||
| */ | ||
| onSettled?: (batcher: AsyncBatcher<TValue>) => void; | ||
| /** | ||
| * Optional callback to call when a batch succeeds | ||
| */ | ||
| onSuccess?: (result: any, batcher: AsyncBatcher<TValue>) => void; | ||
| /** | ||
| * Whether the batcher should start processing immediately | ||
| * @default true | ||
| */ | ||
| started?: boolean; | ||
| /** | ||
| * Whether to throw errors when they occur. | ||
| * Defaults to true if no onError handler is provided, false if an onError handler is provided. | ||
| * Can be explicitly set to override these defaults. | ||
| */ | ||
| throwOnError?: boolean; | ||
| /** | ||
| * Maximum time in milliseconds to wait before processing a batch. | ||
| * If the wait duration has elapsed, the batch will be processed. | ||
| * If not provided, the batch will not be triggered by a timeout. | ||
| * @default Infinity | ||
| */ | ||
| wait?: number | ((asyncBatcher: AsyncBatcher<TValue>) => number); | ||
| } | ||
| type AsyncBatcherOptionsWithOptionalCallbacks<TValue> = OptionalKeys<Required<AsyncBatcherOptions<TValue>>, 'initialState' | 'onError' | 'onExecute' | 'onItemsChange' | 'onSettled' | 'onSuccess'>; | ||
| /** | ||
| * A class that collects items and processes them in batches asynchronously. | ||
| * | ||
| * This is the async version of the Batcher class. Unlike the sync version, this async batcher: | ||
| * - Handles promises and returns results from batch executions | ||
| * - Provides error handling with configurable error behavior | ||
| * - Tracks success, error, and settle counts separately | ||
| * - Has state tracking for when batches are executing | ||
| * - Returns the result of the batch function execution | ||
| * | ||
| * Batching is a technique for grouping multiple operations together to be processed as a single unit. | ||
| * | ||
| * The AsyncBatcher provides a flexible way to implement async batching with configurable: | ||
| * - Maximum batch size (number of items per batch) | ||
| * - Time-based batching (process after X milliseconds) | ||
| * - Custom batch processing logic via getShouldExecute | ||
| * - Event callbacks for monitoring batch operations | ||
| * - Error handling for failed batch operations | ||
| * | ||
| * Error Handling: | ||
| * - If an `onError` handler is provided, it will be called with the error and batcher instance | ||
| * - If `throwOnError` is true (default when no onError handler is provided), the error will be thrown | ||
| * - If `throwOnError` is false (default when onError handler is provided), the error will be swallowed | ||
| * - Both onError and throwOnError can be used together - the handler will be called before any error is thrown | ||
| * - The error state can be checked using the AsyncBatcher instance | ||
| * | ||
| * State Management: | ||
| * - Uses TanStack Store for reactive state management | ||
| * - Use `initialState` to provide initial state values when creating the async batcher | ||
| * - Use `onSuccess` callback to react to successful batch execution and implement custom logic | ||
| * - Use `onError` callback to react to batch execution errors and implement custom error handling | ||
| * - Use `onSettled` callback to react to batch execution completion (success or error) and implement custom logic | ||
| * - Use `onExecute` callback to react to batch execution and implement custom logic | ||
| * - Use `onItemsChange` callback to react to items being added or removed from the batcher | ||
| * - The state includes total items processed, success/error counts, and execution status | ||
| * - State can be accessed via `asyncBatcher.store.state` when using the class directly | ||
| * - When using framework adapters (React/Solid), state is accessed from `asyncBatcher.state` | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * const batcher = new AsyncBatcher<number>( | ||
| * async (items) => { | ||
| * const result = await processItems(items); | ||
| * console.log('Processing batch:', items); | ||
| * return result; | ||
| * }, | ||
| * { | ||
| * maxSize: 5, | ||
| * wait: 2000, | ||
| * onSuccess: (result) => console.log('Batch succeeded:', result), | ||
| * onError: (error) => console.error('Batch failed:', error) | ||
| * } | ||
| * ); | ||
| * | ||
| * batcher.addItem(1); | ||
| * batcher.addItem(2); | ||
| * // After 2 seconds or when 5 items are added, whichever comes first, | ||
| * // the batch will be processed and the result will be available | ||
| * // batcher.execute() // manually trigger a batch | ||
| * ``` | ||
| */ | ||
| export declare class AsyncBatcher<TValue> { | ||
| #private; | ||
| private fn; | ||
| readonly store: Store<Readonly<AsyncBatcherState<TValue>>>; | ||
| options: AsyncBatcherOptionsWithOptionalCallbacks<TValue>; | ||
| constructor(fn: (items: Array<TValue>) => Promise<any>, initialOptions: AsyncBatcherOptions<TValue>); | ||
| /** | ||
| * Updates the async batcher options | ||
| */ | ||
| setOptions: (newOptions: Partial<AsyncBatcherOptions<TValue>>) => void; | ||
| /** | ||
| * Adds an item to the async batcher | ||
| * If the batch size is reached, timeout occurs, or shouldProcess returns true, the batch will be processed | ||
| */ | ||
| addItem: (item: TValue) => void; | ||
| /** | ||
| * Processes the current batch of items immediately | ||
| */ | ||
| flush: () => Promise<any>; | ||
| /** | ||
| * Stops the async batcher from processing batches | ||
| */ | ||
| stop: () => void; | ||
| /** | ||
| * Starts the async batcher and processes any pending items | ||
| */ | ||
| start: () => void; | ||
| /** | ||
| * Returns a copy of all items in the async batcher | ||
| */ | ||
| peekAllItems: () => Array<TValue>; | ||
| peekFailedItems: () => Array<TValue>; | ||
| /** | ||
| * Removes all items from the async batcher | ||
| */ | ||
| clear: () => void; | ||
| /** | ||
| * Resets the async batcher state to its default values | ||
| */ | ||
| reset: () => void; | ||
| } | ||
| /** | ||
| * Creates an async batcher that processes items in batches | ||
| * | ||
| * Unlike the sync batcher, this async version: | ||
| * - Handles promises and returns results from batch executions | ||
| * - Provides error handling with configurable error behavior | ||
| * - Tracks success, error, and settle counts separately | ||
| * - Has state tracking for when batches are executing | ||
| * | ||
| * Error Handling: | ||
| * - If an `onError` handler is provided, it will be called with the error and batcher instance | ||
| * - If `throwOnError` is true (default when no onError handler is provided), the error will be thrown | ||
| * - If `throwOnError` is false (default when onError handler is provided), the error will be swallowed | ||
| * - Both onError and throwOnError can be used together - the handler will be called before any error is thrown | ||
| * - The error state can be checked using the underlying AsyncBatcher instance | ||
| * | ||
| * State Management: | ||
| * - Uses TanStack Store for reactive state management | ||
| * - Use `initialState` to provide initial state values when creating the async batcher | ||
| * - Use `onSuccess` callback to react to successful batch execution and implement custom logic | ||
| * - Use `onError` callback to react to batch execution errors and implement custom error handling | ||
| * - Use `onSettled` callback to react to batch execution completion (success or error) and implement custom logic | ||
| * - Use `onExecute` callback to react to batch execution and implement custom logic | ||
| * - Use `onItemsChange` callback to react to items being added or removed from the batcher | ||
| * - The state includes total items processed, success/error counts, and execution status | ||
| * - State can be accessed via the underlying AsyncBatcher instance's `store.state` property | ||
| * - When using framework adapters (React/Solid), state is accessed from the hook's state property | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * const batchItems = asyncBatch<number>( | ||
| * async (items) => { | ||
| * const result = await processApiCall(items); | ||
| * console.log('Processing:', items); | ||
| * return result; | ||
| * }, | ||
| * { | ||
| * maxSize: 3, | ||
| * wait: 1000, | ||
| * onSuccess: (result) => console.log('Batch succeeded:', result), | ||
| * onError: (error) => console.error('Batch failed:', error) | ||
| * } | ||
| * ); | ||
| * | ||
| * batchItems(1); | ||
| * batchItems(2); | ||
| * batchItems(3); // Triggers batch processing | ||
| * ``` | ||
| */ | ||
| export declare function asyncBatch<TValue>(fn: (items: Array<TValue>) => Promise<any>, options: AsyncBatcherOptions<TValue>): (item: TValue) => void; | ||
| export {}; |
| import { Store } from '@tanstack/store'; | ||
| import { OptionalKeys } from './types.js'; | ||
| export interface AsyncBatcherState<TValue> { | ||
| /** | ||
| * Number of batch executions that have resulted in errors | ||
| */ | ||
| errorCount: number; | ||
| /** | ||
| * Array of items that failed during batch processing | ||
| */ | ||
| failedItems: Array<TValue>; | ||
| /** | ||
| * Whether the batcher has no items to process (items array is empty) | ||
| */ | ||
| isEmpty: boolean; | ||
| /** | ||
| * Whether a batch is currently being processed asynchronously | ||
| */ | ||
| isExecuting: boolean; | ||
| /** | ||
| * Whether the batcher is waiting for the timeout to trigger batch processing | ||
| */ | ||
| isPending: boolean; | ||
| /** | ||
| * Whether the batcher is active and will process items automatically | ||
| */ | ||
| isRunning: boolean; | ||
| /** | ||
| * Array of items currently queued for batch processing | ||
| */ | ||
| items: Array<TValue>; | ||
| /** | ||
| * The result from the most recent batch execution | ||
| */ | ||
| lastResult: any; | ||
| /** | ||
| * Number of batch executions that have completed (either successfully or with errors) | ||
| */ | ||
| settleCount: number; | ||
| /** | ||
| * Number of items currently in the batch queue | ||
| */ | ||
| size: number; | ||
| /** | ||
| * 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 | ||
| */ | ||
| status: 'idle' | 'pending' | 'executing' | 'populated'; | ||
| /** | ||
| * Number of batch executions that have completed successfully | ||
| */ | ||
| successCount: number; | ||
| /** | ||
| * Total number of items that have been processed across all batches | ||
| */ | ||
| totalItemsProcessed: number; | ||
| /** | ||
| * Total number of items that have failed processing across all batches | ||
| */ | ||
| totalItemsFailed: number; | ||
| } | ||
| /** | ||
| * Options for configuring an AsyncBatcher instance | ||
| */ | ||
| export interface AsyncBatcherOptions<TValue> { | ||
| /** | ||
| * Custom function to determine if a batch should be processed | ||
| * Return true to process the batch immediately | ||
| */ | ||
| getShouldExecute?: (items: Array<TValue>, batcher: AsyncBatcher<TValue>) => boolean; | ||
| /** | ||
| * Initial state for the async batcher | ||
| */ | ||
| initialState?: Partial<AsyncBatcherState<TValue>>; | ||
| /** | ||
| * Maximum number of items in a batch | ||
| * @default Infinity | ||
| */ | ||
| maxSize?: number; | ||
| /** | ||
| * Optional error handler for when the batch function throws. | ||
| * If provided, the handler will be called with the error and batcher instance. | ||
| * This can be used alongside throwOnError - the handler will be called before any error is thrown. | ||
| */ | ||
| onError?: (error: unknown, failedItems: Array<TValue>, batcher: AsyncBatcher<TValue>) => void; | ||
| /** | ||
| * Callback fired after a batch is processed | ||
| */ | ||
| onExecute?: (batcher: AsyncBatcher<TValue>) => void; | ||
| /** | ||
| * Callback fired after items are added to the batcher | ||
| */ | ||
| onItemsChange?: (batcher: AsyncBatcher<TValue>) => void; | ||
| /** | ||
| * Optional callback to call when a batch is settled (completed or failed) | ||
| */ | ||
| onSettled?: (batcher: AsyncBatcher<TValue>) => void; | ||
| /** | ||
| * Optional callback to call when a batch succeeds | ||
| */ | ||
| onSuccess?: (result: any, batcher: AsyncBatcher<TValue>) => void; | ||
| /** | ||
| * Whether the batcher should start processing immediately | ||
| * @default true | ||
| */ | ||
| started?: boolean; | ||
| /** | ||
| * Whether to throw errors when they occur. | ||
| * Defaults to true if no onError handler is provided, false if an onError handler is provided. | ||
| * Can be explicitly set to override these defaults. | ||
| */ | ||
| throwOnError?: boolean; | ||
| /** | ||
| * Maximum time in milliseconds to wait before processing a batch. | ||
| * If the wait duration has elapsed, the batch will be processed. | ||
| * If not provided, the batch will not be triggered by a timeout. | ||
| * @default Infinity | ||
| */ | ||
| wait?: number | ((asyncBatcher: AsyncBatcher<TValue>) => number); | ||
| } | ||
| type AsyncBatcherOptionsWithOptionalCallbacks<TValue> = OptionalKeys<Required<AsyncBatcherOptions<TValue>>, 'initialState' | 'onError' | 'onExecute' | 'onItemsChange' | 'onSettled' | 'onSuccess'>; | ||
| /** | ||
| * A class that collects items and processes them in batches asynchronously. | ||
| * | ||
| * This is the async version of the Batcher class. Unlike the sync version, this async batcher: | ||
| * - Handles promises and returns results from batch executions | ||
| * - Provides error handling with configurable error behavior | ||
| * - Tracks success, error, and settle counts separately | ||
| * - Has state tracking for when batches are executing | ||
| * - Returns the result of the batch function execution | ||
| * | ||
| * Batching is a technique for grouping multiple operations together to be processed as a single unit. | ||
| * | ||
| * The AsyncBatcher provides a flexible way to implement async batching with configurable: | ||
| * - Maximum batch size (number of items per batch) | ||
| * - Time-based batching (process after X milliseconds) | ||
| * - Custom batch processing logic via getShouldExecute | ||
| * - Event callbacks for monitoring batch operations | ||
| * - Error handling for failed batch operations | ||
| * | ||
| * Error Handling: | ||
| * - If an `onError` handler is provided, it will be called with the error and batcher instance | ||
| * - If `throwOnError` is true (default when no onError handler is provided), the error will be thrown | ||
| * - If `throwOnError` is false (default when onError handler is provided), the error will be swallowed | ||
| * - Both onError and throwOnError can be used together - the handler will be called before any error is thrown | ||
| * - The error state can be checked using the AsyncBatcher instance | ||
| * | ||
| * State Management: | ||
| * - Uses TanStack Store for reactive state management | ||
| * - Use `initialState` to provide initial state values when creating the async batcher | ||
| * - Use `onSuccess` callback to react to successful batch execution and implement custom logic | ||
| * - Use `onError` callback to react to batch execution errors and implement custom error handling | ||
| * - Use `onSettled` callback to react to batch execution completion (success or error) and implement custom logic | ||
| * - Use `onExecute` callback to react to batch execution and implement custom logic | ||
| * - Use `onItemsChange` callback to react to items being added or removed from the batcher | ||
| * - The state includes total items processed, success/error counts, and execution status | ||
| * - State can be accessed via `asyncBatcher.store.state` when using the class directly | ||
| * - When using framework adapters (React/Solid), state is accessed from `asyncBatcher.state` | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * const batcher = new AsyncBatcher<number>( | ||
| * async (items) => { | ||
| * const result = await processItems(items); | ||
| * console.log('Processing batch:', items); | ||
| * return result; | ||
| * }, | ||
| * { | ||
| * maxSize: 5, | ||
| * wait: 2000, | ||
| * onSuccess: (result) => console.log('Batch succeeded:', result), | ||
| * onError: (error) => console.error('Batch failed:', error) | ||
| * } | ||
| * ); | ||
| * | ||
| * batcher.addItem(1); | ||
| * batcher.addItem(2); | ||
| * // After 2 seconds or when 5 items are added, whichever comes first, | ||
| * // the batch will be processed and the result will be available | ||
| * // batcher.execute() // manually trigger a batch | ||
| * ``` | ||
| */ | ||
| export declare class AsyncBatcher<TValue> { | ||
| #private; | ||
| private fn; | ||
| readonly store: Store<Readonly<AsyncBatcherState<TValue>>>; | ||
| options: AsyncBatcherOptionsWithOptionalCallbacks<TValue>; | ||
| constructor(fn: (items: Array<TValue>) => Promise<any>, initialOptions: AsyncBatcherOptions<TValue>); | ||
| /** | ||
| * Updates the async batcher options | ||
| */ | ||
| setOptions: (newOptions: Partial<AsyncBatcherOptions<TValue>>) => void; | ||
| /** | ||
| * Adds an item to the async batcher | ||
| * If the batch size is reached, timeout occurs, or shouldProcess returns true, the batch will be processed | ||
| */ | ||
| addItem: (item: TValue) => void; | ||
| /** | ||
| * Processes the current batch of items immediately | ||
| */ | ||
| flush: () => Promise<any>; | ||
| /** | ||
| * Stops the async batcher from processing batches | ||
| */ | ||
| stop: () => void; | ||
| /** | ||
| * Starts the async batcher and processes any pending items | ||
| */ | ||
| start: () => void; | ||
| /** | ||
| * Returns a copy of all items in the async batcher | ||
| */ | ||
| peekAllItems: () => Array<TValue>; | ||
| peekFailedItems: () => Array<TValue>; | ||
| /** | ||
| * Removes all items from the async batcher | ||
| */ | ||
| clear: () => void; | ||
| /** | ||
| * Resets the async batcher state to its default values | ||
| */ | ||
| reset: () => void; | ||
| } | ||
| /** | ||
| * Creates an async batcher that processes items in batches | ||
| * | ||
| * Unlike the sync batcher, this async version: | ||
| * - Handles promises and returns results from batch executions | ||
| * - Provides error handling with configurable error behavior | ||
| * - Tracks success, error, and settle counts separately | ||
| * - Has state tracking for when batches are executing | ||
| * | ||
| * Error Handling: | ||
| * - If an `onError` handler is provided, it will be called with the error and batcher instance | ||
| * - If `throwOnError` is true (default when no onError handler is provided), the error will be thrown | ||
| * - If `throwOnError` is false (default when onError handler is provided), the error will be swallowed | ||
| * - Both onError and throwOnError can be used together - the handler will be called before any error is thrown | ||
| * - The error state can be checked using the underlying AsyncBatcher instance | ||
| * | ||
| * State Management: | ||
| * - Uses TanStack Store for reactive state management | ||
| * - Use `initialState` to provide initial state values when creating the async batcher | ||
| * - Use `onSuccess` callback to react to successful batch execution and implement custom logic | ||
| * - Use `onError` callback to react to batch execution errors and implement custom error handling | ||
| * - Use `onSettled` callback to react to batch execution completion (success or error) and implement custom logic | ||
| * - Use `onExecute` callback to react to batch execution and implement custom logic | ||
| * - Use `onItemsChange` callback to react to items being added or removed from the batcher | ||
| * - The state includes total items processed, success/error counts, and execution status | ||
| * - State can be accessed via the underlying AsyncBatcher instance's `store.state` property | ||
| * - When using framework adapters (React/Solid), state is accessed from the hook's state property | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * const batchItems = asyncBatch<number>( | ||
| * async (items) => { | ||
| * const result = await processApiCall(items); | ||
| * console.log('Processing:', items); | ||
| * return result; | ||
| * }, | ||
| * { | ||
| * maxSize: 3, | ||
| * wait: 1000, | ||
| * onSuccess: (result) => console.log('Batch succeeded:', result), | ||
| * onError: (error) => console.error('Batch failed:', error) | ||
| * } | ||
| * ); | ||
| * | ||
| * batchItems(1); | ||
| * batchItems(2); | ||
| * batchItems(3); // Triggers batch processing | ||
| * ``` | ||
| */ | ||
| export declare function asyncBatch<TValue>(fn: (items: Array<TValue>) => Promise<any>, options: AsyncBatcherOptions<TValue>): (item: TValue) => void; | ||
| export {}; |
| import { Store } from "@tanstack/store"; | ||
| import { parseFunctionOrValue } from "./utils.js"; | ||
| function getDefaultAsyncBatcherState() { | ||
| return { | ||
| errorCount: 0, | ||
| failedItems: [], | ||
| isEmpty: true, | ||
| isExecuting: false, | ||
| isPending: false, | ||
| isRunning: true, | ||
| items: [], | ||
| lastResult: void 0, | ||
| settleCount: 0, | ||
| size: 0, | ||
| status: "idle", | ||
| successCount: 0, | ||
| totalItemsProcessed: 0, | ||
| totalItemsFailed: 0 | ||
| }; | ||
| } | ||
| const defaultOptions = { | ||
| getShouldExecute: () => false, | ||
| maxSize: Infinity, | ||
| started: true, | ||
| throwOnError: true, | ||
| wait: Infinity | ||
| }; | ||
| class AsyncBatcher { | ||
| constructor(fn, initialOptions) { | ||
| this.fn = fn; | ||
| this.store = new Store( | ||
| getDefaultAsyncBatcherState() | ||
| ); | ||
| this.#timeoutId = null; | ||
| this.setOptions = (newOptions) => { | ||
| this.options = { ...this.options, ...newOptions }; | ||
| }; | ||
| this.#setState = (newState) => { | ||
| this.store.setState((state) => { | ||
| const combinedState = { | ||
| ...state, | ||
| ...newState | ||
| }; | ||
| const { isExecuting, isPending, items } = combinedState; | ||
| const size = items.length; | ||
| const isEmpty = size === 0; | ||
| return { | ||
| ...combinedState, | ||
| isEmpty, | ||
| size, | ||
| status: isExecuting ? "executing" : isPending ? "pending" : isEmpty ? "idle" : "populated" | ||
| }; | ||
| }); | ||
| }; | ||
| this.#getWait = () => { | ||
| return parseFunctionOrValue(this.options.wait, this); | ||
| }; | ||
| this.addItem = (item) => { | ||
| this.#setState({ | ||
| items: [...this.store.state.items, item], | ||
| isPending: this.options.wait !== Infinity | ||
| }); | ||
| this.options.onItemsChange?.(this); | ||
| const shouldProcess = this.store.state.items.length >= this.options.maxSize || this.options.getShouldExecute(this.store.state.items, this); | ||
| if (shouldProcess) { | ||
| this.#execute(); | ||
| } else if (this.store.state.isRunning && this.options.wait !== Infinity) { | ||
| this.#clearTimeout(); | ||
| this.#timeoutId = setTimeout(() => this.#execute(), this.#getWait()); | ||
| } | ||
| }; | ||
| this.#execute = async () => { | ||
| if (this.store.state.items.length === 0) { | ||
| return void 0; | ||
| } | ||
| const batch = this.peekAllItems(); | ||
| this.clear(); | ||
| this.options.onItemsChange?.(this); | ||
| this.#setState({ isExecuting: true }); | ||
| try { | ||
| const result = await this.fn(batch); | ||
| this.#setState({ | ||
| totalItemsProcessed: this.store.state.totalItemsProcessed + batch.length, | ||
| lastResult: result, | ||
| successCount: this.store.state.successCount + 1 | ||
| }); | ||
| this.options.onSuccess?.(result, this); | ||
| return result; | ||
| } catch (error) { | ||
| this.#setState({ | ||
| errorCount: this.store.state.errorCount + 1, | ||
| failedItems: [...this.store.state.failedItems, ...batch], | ||
| totalItemsFailed: this.store.state.totalItemsFailed + batch.length | ||
| }); | ||
| this.options.onError?.(error, batch, this); | ||
| if (this.options.throwOnError) { | ||
| throw error; | ||
| } | ||
| return void 0; | ||
| } finally { | ||
| this.#setState({ | ||
| isExecuting: false, | ||
| settleCount: this.store.state.settleCount + 1 | ||
| }); | ||
| this.options.onSettled?.(this); | ||
| this.options.onExecute?.(this); | ||
| } | ||
| }; | ||
| this.flush = async () => { | ||
| this.#clearTimeout(); | ||
| return await this.#execute(); | ||
| }; | ||
| this.stop = () => { | ||
| this.#setState({ isRunning: false }); | ||
| this.#clearTimeout(); | ||
| }; | ||
| this.start = () => { | ||
| this.#setState({ isRunning: true }); | ||
| if (this.store.state.items.length > 0 && !this.#timeoutId) { | ||
| this.#timeoutId = setTimeout(() => this.#execute(), this.#getWait()); | ||
| } | ||
| }; | ||
| this.peekAllItems = () => { | ||
| return [...this.store.state.items]; | ||
| }; | ||
| this.peekFailedItems = () => { | ||
| return [...this.store.state.failedItems]; | ||
| }; | ||
| this.#clearTimeout = () => { | ||
| if (this.#timeoutId) { | ||
| clearTimeout(this.#timeoutId); | ||
| this.#timeoutId = null; | ||
| } | ||
| }; | ||
| this.clear = () => { | ||
| this.#setState({ items: [], failedItems: [], isPending: false }); | ||
| }; | ||
| this.reset = () => { | ||
| this.#setState(getDefaultAsyncBatcherState()); | ||
| this.options.onItemsChange?.(this); | ||
| }; | ||
| this.options = { | ||
| ...defaultOptions, | ||
| ...initialOptions, | ||
| throwOnError: initialOptions.throwOnError ?? !initialOptions.onError | ||
| }; | ||
| this.#setState(this.options.initialState ?? {}); | ||
| } | ||
| #timeoutId; | ||
| #setState; | ||
| #getWait; | ||
| #execute; | ||
| #clearTimeout; | ||
| } | ||
| function asyncBatch(fn, options) { | ||
| const batcher = new AsyncBatcher(fn, options); | ||
| return batcher.addItem; | ||
| } | ||
| export { | ||
| AsyncBatcher, | ||
| asyncBatch | ||
| }; | ||
| //# sourceMappingURL=async-batcher.js.map |
| {"version":3,"file":"async-batcher.js","sources":["../../src/async-batcher.ts"],"sourcesContent":["import { Store } from '@tanstack/store'\nimport { parseFunctionOrValue } from './utils'\nimport type { OptionalKeys } from './types'\n\nexport interface AsyncBatcherState<TValue> {\n /**\n * Number of batch executions that have resulted in errors\n */\n errorCount: number\n /**\n * Array of items that failed during batch processing\n */\n failedItems: Array<TValue>\n /**\n * Whether the batcher has no items to process (items array is empty)\n */\n isEmpty: boolean\n /**\n * Whether a batch is currently being processed asynchronously\n */\n isExecuting: boolean\n /**\n * Whether the batcher is waiting for the timeout to trigger batch processing\n */\n isPending: boolean\n /**\n * Whether the batcher is active and will process items automatically\n */\n isRunning: 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 been processed across all batches\n */\n totalItemsProcessed: number\n /**\n * Total number of items that have failed processing across all batches\n */\n totalItemsFailed: number\n}\n\nfunction getDefaultAsyncBatcherState<TValue>(): AsyncBatcherState<TValue> {\n return {\n errorCount: 0,\n failedItems: [],\n isEmpty: true,\n isExecuting: false,\n isPending: false,\n isRunning: true,\n items: [],\n lastResult: undefined,\n settleCount: 0,\n size: 0,\n status: 'idle',\n successCount: 0,\n totalItemsProcessed: 0,\n totalItemsFailed: 0,\n }\n}\n\n/**\n * Options for configuring an AsyncBatcher instance\n */\nexport interface AsyncBatcherOptions<TValue> {\n /**\n * Custom function to determine if a batch should be processed\n * Return true to process the batch immediately\n */\n getShouldExecute?: (\n items: Array<TValue>,\n batcher: AsyncBatcher<TValue>,\n ) => boolean\n /**\n * Initial state for the async batcher\n */\n initialState?: Partial<AsyncBatcherState<TValue>>\n /**\n * 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 and batcher instance.\n * This can be used alongside throwOnError - the handler will be called before any error is thrown.\n */\n onError?: (\n error: unknown,\n failedItems: Array<TValue>,\n batcher: AsyncBatcher<TValue>,\n ) => void\n /**\n * Callback fired after a batch is processed\n */\n onExecute?: (batcher: AsyncBatcher<TValue>) => 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?: (batcher: AsyncBatcher<TValue>) => void\n /**\n * Optional callback to call when a batch succeeds\n */\n onSuccess?: (result: any, batcher: AsyncBatcher<TValue>) => void\n /**\n * Whether the batcher should start processing immediately\n * @default true\n */\n started?: boolean\n /**\n * Whether to throw errors when they occur.\n * Defaults to true if no onError handler is provided, false if an onError handler is provided.\n * Can be explicitly set to override these defaults.\n */\n throwOnError?: boolean\n /**\n * Maximum time in milliseconds to wait before processing a batch.\n * If the wait duration has elapsed, the batch will be processed.\n * If not provided, the batch will not be triggered by a timeout.\n * @default Infinity\n */\n wait?: number | ((asyncBatcher: AsyncBatcher<TValue>) => number)\n}\n\ntype AsyncBatcherOptionsWithOptionalCallbacks<TValue> = OptionalKeys<\n Required<AsyncBatcherOptions<TValue>>,\n | 'initialState'\n | 'onError'\n | 'onExecute'\n | 'onItemsChange'\n | 'onSettled'\n | 'onSuccess'\n>\n\nconst defaultOptions: AsyncBatcherOptionsWithOptionalCallbacks<any> = {\n getShouldExecute: () => false,\n maxSize: Infinity,\n started: true,\n throwOnError: true,\n wait: Infinity,\n}\n\n/**\n * A class that collects items and processes them in batches asynchronously.\n *\n * This is the async version of the Batcher class. Unlike the sync version, this async batcher:\n * - Handles promises and returns results from batch executions\n * - Provides error handling with configurable error behavior\n * - Tracks success, error, and settle counts separately\n * - Has state tracking for when batches are executing\n * - Returns the result of the batch function execution\n *\n * Batching is a technique for grouping multiple operations together to be processed as a single unit.\n *\n * The AsyncBatcher provides a flexible way to implement async batching with configurable:\n * - Maximum batch size (number of items per batch)\n * - Time-based batching (process after X milliseconds)\n * - Custom batch processing logic via getShouldExecute\n * - Event callbacks for monitoring batch operations\n * - Error handling for failed batch operations\n *\n * Error Handling:\n * - If an `onError` handler is provided, it will be called with the error 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 options: AsyncBatcherOptionsWithOptionalCallbacks<TValue>\n #timeoutId: NodeJS.Timeout | null = null\n\n constructor(\n private fn: (items: Array<TValue>) => Promise<any>,\n initialOptions: AsyncBatcherOptions<TValue>,\n ) {\n this.options = {\n ...defaultOptions,\n ...initialOptions,\n throwOnError: initialOptions.throwOnError ?? !initialOptions.onError,\n }\n this.#setState(this.options.initialState ?? {})\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 }\n\n #getWait = (): number => {\n return parseFunctionOrValue(this.options.wait, this)\n }\n\n /**\n * Adds an item to the async batcher\n * If the batch size is reached, timeout occurs, or shouldProcess returns true, the batch will be processed\n */\n addItem = (item: TValue): void => {\n this.#setState({\n items: [...this.store.state.items, item],\n isPending: this.options.wait !== Infinity,\n })\n this.options.onItemsChange?.(this)\n\n const shouldProcess =\n this.store.state.items.length >= this.options.maxSize ||\n this.options.getShouldExecute(this.store.state.items, this)\n\n if (shouldProcess) {\n this.#execute()\n } else if (this.store.state.isRunning && this.options.wait !== Infinity) {\n this.#clearTimeout() // clear any pending timeout to replace it with a new one\n this.#timeoutId = setTimeout(() => this.#execute(), this.#getWait())\n }\n }\n\n /**\n * Processes the current batch of items asynchronously.\n * This method will automatically be triggered if the batcher is running and any of these conditions are met:\n * - The number of items reaches maxSize\n * - The wait duration has elapsed\n * - The getShouldExecute function returns true upon adding an item\n *\n * You can also call this method manually to process the current batch at any time.\n *\n * @returns A promise that resolves with the result of the batch function, or undefined if an error occurred and was handled by onError\n * @throws The error from the batch function if no onError handler is configured\n */\n #execute = async (): Promise<any> => {\n if (this.store.state.items.length === 0) {\n return undefined\n }\n\n const batch = this.peekAllItems() // copy of the items to be processed (to prevent race conditions)\n this.clear() // Clear items before processing to prevent race conditions\n this.options.onItemsChange?.(this) // Call onItemsChange to notify listeners that the items have changed\n\n this.#setState({ isExecuting: true })\n\n try {\n const result = await this.fn(batch) // EXECUTE\n this.#setState({\n totalItemsProcessed:\n this.store.state.totalItemsProcessed + batch.length,\n lastResult: result,\n successCount: this.store.state.successCount + 1,\n })\n this.options.onSuccess?.(result, this)\n return result\n } catch (error) {\n this.#setState({\n errorCount: this.store.state.errorCount + 1,\n failedItems: [...this.store.state.failedItems, ...batch],\n totalItemsFailed: this.store.state.totalItemsFailed + batch.length,\n })\n this.options.onError?.(error, batch, this)\n if (this.options.throwOnError) {\n throw error\n }\n return undefined\n } finally {\n this.#setState({\n isExecuting: false,\n settleCount: this.store.state.settleCount + 1,\n })\n this.options.onSettled?.(this)\n this.options.onExecute?.(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 * Stops the async batcher from processing batches\n */\n stop = (): void => {\n this.#setState({ isRunning: false })\n this.#clearTimeout()\n }\n\n /**\n * Starts the async batcher and processes any pending items\n */\n start = (): void => {\n this.#setState({ isRunning: true })\n if (this.store.state.items.length > 0 && !this.#timeoutId) {\n this.#timeoutId = setTimeout(() => this.#execute(), this.#getWait())\n }\n }\n\n /**\n * Returns a copy of all items in the async batcher\n */\n peekAllItems = (): Array<TValue> => {\n return [...this.store.state.items]\n }\n\n peekFailedItems = (): Array<TValue> => {\n return [...this.store.state.failedItems]\n }\n\n #clearTimeout = (): void => {\n if (this.#timeoutId) {\n clearTimeout(this.#timeoutId)\n this.#timeoutId = null\n }\n }\n\n /**\n * Removes all items from the async batcher\n */\n clear = (): void => {\n this.#setState({ items: [], failedItems: [], isPending: false })\n }\n\n /**\n * Resets the async batcher state to its default values\n */\n reset = (): void => {\n this.#setState(getDefaultAsyncBatcherState<TValue>())\n this.options.onItemsChange?.(this)\n }\n}\n\n/**\n * Creates an async batcher that processes items in batches\n *\n * Unlike the sync batcher, this async version:\n * - Handles promises and returns results from batch executions\n * - Provides error handling with configurable error behavior\n * - Tracks success, error, and settle counts separately\n * - Has state tracking for when batches are executing\n *\n * Error Handling:\n * - If an `onError` handler is provided, it will be called with the error and batcher instance\n * - If `throwOnError` is true (default when no onError handler is provided), the error will be thrown\n * - If `throwOnError` is false (default when onError handler is provided), the error will be swallowed\n * - Both onError and throwOnError can be used together - the handler will be called before any error is thrown\n * - The error state can be checked using the underlying AsyncBatcher instance\n *\n * State Management:\n * - Uses TanStack Store for reactive state management\n * - Use `initialState` to provide initial state values when creating the async batcher\n * - Use `onSuccess` callback to react to successful batch execution and implement custom logic\n * - Use `onError` callback to react to batch execution errors and implement custom error handling\n * - Use `onSettled` callback to react to batch execution completion (success or error) and implement custom logic\n * - Use `onExecute` callback to react to batch execution and implement custom logic\n * - Use `onItemsChange` callback to react to items being added or removed from the batcher\n * - The state includes total items processed, success/error counts, and execution status\n * - State can be accessed via the underlying AsyncBatcher instance's `store.state` property\n * - When using framework adapters (React/Solid), state is accessed from the hook's state property\n *\n * @example\n * ```ts\n * const batchItems = asyncBatch<number>(\n * async (items) => {\n * const result = await processApiCall(items);\n * console.log('Processing:', items);\n * return result;\n * },\n * {\n * maxSize: 3,\n * wait: 1000,\n * onSuccess: (result) => console.log('Batch succeeded:', result),\n * onError: (error) => console.error('Batch failed:', error)\n * }\n * );\n *\n * batchItems(1);\n * batchItems(2);\n * batchItems(3); // Triggers batch processing\n * ```\n */\nexport function asyncBatch<TValue>(\n fn: (items: Array<TValue>) => Promise<any>,\n options: AsyncBatcherOptions<TValue>,\n) {\n const batcher = new AsyncBatcher<TValue>(fn, options)\n return batcher.addItem\n}\n"],"names":[],"mappings":";;AA+DA,SAAS,8BAAiE;AACxE,SAAO;AAAA,IACL,YAAY;AAAA,IACZ,aAAa,CAAA;AAAA,IACb,SAAS;AAAA,IACT,aAAa;AAAA,IACb,WAAW;AAAA,IACX,WAAW;AAAA,IACX,OAAO,CAAA;AAAA,IACP,YAAY;AAAA,IACZ,aAAa;AAAA,IACb,MAAM;AAAA,IACN,QAAQ;AAAA,IACR,cAAc;AAAA,IACd,qBAAqB;AAAA,IACrB,kBAAkB;AAAA,EAAA;AAEtB;AA+EA,MAAM,iBAAgE;AAAA,EACpE,kBAAkB,MAAM;AAAA,EACxB,SAAS;AAAA,EACT,SAAS;AAAA,EACT,cAAc;AAAA,EACd,MAAM;AACR;AA+DO,MAAM,aAAqB;AAAA,EAOhC,YACU,IACR,gBACA;AAFQ,SAAA,KAAA;AAPV,SAAS,QAAoD,IAAI;AAAA,MAC/D,4BAAA;AAAA,IAAoC;AAGtC,SAAA,aAAoC;AAiBpC,SAAA,aAAa,CAAC,eAA2D;AACvE,WAAK,UAAU,EAAE,GAAG,KAAK,SAAS,GAAG,WAAA;AAAA,IAAW;AAGlD,SAAA,YAAY,CAAC,aAAuD;AAClE,WAAK,MAAM,SAAS,CAAC,UAAU;AAC7B,cAAM,gBAAgB;AAAA,UACpB,GAAG;AAAA,UACH,GAAG;AAAA,QAAA;AAEL,cAAM,EAAE,aAAa,WAAW,MAAA,IAAU;AAC1C,cAAM,OAAO,MAAM;AACnB,cAAM,UAAU,SAAS;AACzB,eAAO;AAAA,UACL,GAAG;AAAA,UACH;AAAA,UACA;AAAA,UACA,QAAQ,cACJ,cACA,YACE,YACA,UACE,SACA;AAAA,QAAA;AAAA,MACV,CACD;AAAA,IAAA;AAGH,SAAA,WAAW,MAAc;AACvB,aAAO,qBAAqB,KAAK,QAAQ,MAAM,IAAI;AAAA,IAAA;AAOrD,SAAA,UAAU,CAAC,SAAuB;AAChC,WAAK,UAAU;AAAA,QACb,OAAO,CAAC,GAAG,KAAK,MAAM,MAAM,OAAO,IAAI;AAAA,QACvC,WAAW,KAAK,QAAQ,SAAS;AAAA,MAAA,CAClC;AACD,WAAK,QAAQ,gBAAgB,IAAI;AAEjC,YAAM,gBACJ,KAAK,MAAM,MAAM,MAAM,UAAU,KAAK,QAAQ,WAC9C,KAAK,QAAQ,iBAAiB,KAAK,MAAM,MAAM,OAAO,IAAI;AAE5D,UAAI,eAAe;AACjB,aAAK,SAAA;AAAA,MAAS,WACL,KAAK,MAAM,MAAM,aAAa,KAAK,QAAQ,SAAS,UAAU;AACvE,aAAK,cAAA;AACL,aAAK,aAAa,WAAW,MAAM,KAAK,YAAY,KAAK,UAAU;AAAA,MAAA;AAAA,IACrE;AAeF,SAAA,WAAW,YAA0B;AACnC,UAAI,KAAK,MAAM,MAAM,MAAM,WAAW,GAAG;AACvC,eAAO;AAAA,MAAA;AAGT,YAAM,QAAQ,KAAK,aAAA;AACnB,WAAK,MAAA;AACL,WAAK,QAAQ,gBAAgB,IAAI;AAEjC,WAAK,UAAU,EAAE,aAAa,KAAA,CAAM;AAEpC,UAAI;AACF,cAAM,SAAS,MAAM,KAAK,GAAG,KAAK;AAClC,aAAK,UAAU;AAAA,UACb,qBACE,KAAK,MAAM,MAAM,sBAAsB,MAAM;AAAA,UAC/C,YAAY;AAAA,UACZ,cAAc,KAAK,MAAM,MAAM,eAAe;AAAA,QAAA,CAC/C;AACD,aAAK,QAAQ,YAAY,QAAQ,IAAI;AACrC,eAAO;AAAA,MAAA,SACA,OAAO;AACd,aAAK,UAAU;AAAA,UACb,YAAY,KAAK,MAAM,MAAM,aAAa;AAAA,UAC1C,aAAa,CAAC,GAAG,KAAK,MAAM,MAAM,aAAa,GAAG,KAAK;AAAA,UACvD,kBAAkB,KAAK,MAAM,MAAM,mBAAmB,MAAM;AAAA,QAAA,CAC7D;AACD,aAAK,QAAQ,UAAU,OAAO,OAAO,IAAI;AACzC,YAAI,KAAK,QAAQ,cAAc;AAC7B,gBAAM;AAAA,QAAA;AAER,eAAO;AAAA,MAAA,UACT;AACE,aAAK,UAAU;AAAA,UACb,aAAa;AAAA,UACb,aAAa,KAAK,MAAM,MAAM,cAAc;AAAA,QAAA,CAC7C;AACD,aAAK,QAAQ,YAAY,IAAI;AAC7B,aAAK,QAAQ,YAAY,IAAI;AAAA,MAAA;AAAA,IAC/B;AAMF,SAAA,QAAQ,YAA0B;AAChC,WAAK,cAAA;AACL,aAAO,MAAM,KAAK,SAAA;AAAA,IAAS;AAM7B,SAAA,OAAO,MAAY;AACjB,WAAK,UAAU,EAAE,WAAW,MAAA,CAAO;AACnC,WAAK,cAAA;AAAA,IAAc;AAMrB,SAAA,QAAQ,MAAY;AAClB,WAAK,UAAU,EAAE,WAAW,KAAA,CAAM;AAClC,UAAI,KAAK,MAAM,MAAM,MAAM,SAAS,KAAK,CAAC,KAAK,YAAY;AACzD,aAAK,aAAa,WAAW,MAAM,KAAK,YAAY,KAAK,UAAU;AAAA,MAAA;AAAA,IACrE;AAMF,SAAA,eAAe,MAAqB;AAClC,aAAO,CAAC,GAAG,KAAK,MAAM,MAAM,KAAK;AAAA,IAAA;AAGnC,SAAA,kBAAkB,MAAqB;AACrC,aAAO,CAAC,GAAG,KAAK,MAAM,MAAM,WAAW;AAAA,IAAA;AAGzC,SAAA,gBAAgB,MAAY;AAC1B,UAAI,KAAK,YAAY;AACnB,qBAAa,KAAK,UAAU;AAC5B,aAAK,aAAa;AAAA,MAAA;AAAA,IACpB;AAMF,SAAA,QAAQ,MAAY;AAClB,WAAK,UAAU,EAAE,OAAO,CAAA,GAAI,aAAa,CAAA,GAAI,WAAW,OAAO;AAAA,IAAA;AAMjE,SAAA,QAAQ,MAAY;AAClB,WAAK,UAAU,6BAAqC;AACpD,WAAK,QAAQ,gBAAgB,IAAI;AAAA,IAAA;AAhLjC,SAAK,UAAU;AAAA,MACb,GAAG;AAAA,MACH,GAAG;AAAA,MACH,cAAc,eAAe,gBAAgB,CAAC,eAAe;AAAA,IAAA;AAE/D,SAAK,UAAU,KAAK,QAAQ,gBAAgB,CAAA,CAAE;AAAA,EAAA;AAAA,EAXhD;AAAA,EAqBA;AAAA,EAwBA;AAAA,EAuCA;AAAA,EA+EA;AAqBF;AAmDO,SAAS,WACd,IACA,SACA;AACA,QAAM,UAAU,IAAI,aAAqB,IAAI,OAAO;AACpD,SAAO,QAAQ;AACjB;"} |
| import { Store } from '@tanstack/store' | ||
| import { parseFunctionOrValue } from './utils' | ||
| import type { OptionalKeys } from './types' | ||
| export interface AsyncBatcherState<TValue> { | ||
| /** | ||
| * Number of batch executions that have resulted in errors | ||
| */ | ||
| errorCount: number | ||
| /** | ||
| * Array of items that failed during batch processing | ||
| */ | ||
| failedItems: Array<TValue> | ||
| /** | ||
| * Whether the batcher has no items to process (items array is empty) | ||
| */ | ||
| isEmpty: boolean | ||
| /** | ||
| * Whether a batch is currently being processed asynchronously | ||
| */ | ||
| isExecuting: boolean | ||
| /** | ||
| * Whether the batcher is waiting for the timeout to trigger batch processing | ||
| */ | ||
| isPending: boolean | ||
| /** | ||
| * Whether the batcher is active and will process items automatically | ||
| */ | ||
| isRunning: boolean | ||
| /** | ||
| * Array of items currently queued for batch processing | ||
| */ | ||
| items: Array<TValue> | ||
| /** | ||
| * The result from the most recent batch execution | ||
| */ | ||
| lastResult: any | ||
| /** | ||
| * Number of batch executions that have completed (either successfully or with errors) | ||
| */ | ||
| settleCount: number | ||
| /** | ||
| * Number of items currently in the batch queue | ||
| */ | ||
| size: number | ||
| /** | ||
| * 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 | ||
| */ | ||
| status: 'idle' | 'pending' | 'executing' | 'populated' | ||
| /** | ||
| * Number of batch executions that have completed successfully | ||
| */ | ||
| successCount: number | ||
| /** | ||
| * Total number of items that have been processed across all batches | ||
| */ | ||
| totalItemsProcessed: number | ||
| /** | ||
| * Total number of items that have failed processing across all batches | ||
| */ | ||
| totalItemsFailed: number | ||
| } | ||
| function getDefaultAsyncBatcherState<TValue>(): AsyncBatcherState<TValue> { | ||
| return { | ||
| errorCount: 0, | ||
| failedItems: [], | ||
| isEmpty: true, | ||
| isExecuting: false, | ||
| isPending: false, | ||
| isRunning: true, | ||
| items: [], | ||
| lastResult: undefined, | ||
| settleCount: 0, | ||
| size: 0, | ||
| status: 'idle', | ||
| successCount: 0, | ||
| totalItemsProcessed: 0, | ||
| totalItemsFailed: 0, | ||
| } | ||
| } | ||
| /** | ||
| * Options for configuring an AsyncBatcher instance | ||
| */ | ||
| export interface AsyncBatcherOptions<TValue> { | ||
| /** | ||
| * Custom function to determine if a batch should be processed | ||
| * Return true to process the batch immediately | ||
| */ | ||
| getShouldExecute?: ( | ||
| items: Array<TValue>, | ||
| batcher: AsyncBatcher<TValue>, | ||
| ) => boolean | ||
| /** | ||
| * Initial state for the async batcher | ||
| */ | ||
| initialState?: Partial<AsyncBatcherState<TValue>> | ||
| /** | ||
| * Maximum number of items in a batch | ||
| * @default Infinity | ||
| */ | ||
| maxSize?: number | ||
| /** | ||
| * Optional error handler for when the batch function throws. | ||
| * If provided, the handler will be called with the error and batcher instance. | ||
| * This can be used alongside throwOnError - the handler will be called before any error is thrown. | ||
| */ | ||
| onError?: ( | ||
| error: unknown, | ||
| failedItems: Array<TValue>, | ||
| batcher: AsyncBatcher<TValue>, | ||
| ) => void | ||
| /** | ||
| * Callback fired after a batch is processed | ||
| */ | ||
| onExecute?: (batcher: AsyncBatcher<TValue>) => void | ||
| /** | ||
| * Callback fired after items are added to the batcher | ||
| */ | ||
| onItemsChange?: (batcher: AsyncBatcher<TValue>) => void | ||
| /** | ||
| * Optional callback to call when a batch is settled (completed or failed) | ||
| */ | ||
| onSettled?: (batcher: AsyncBatcher<TValue>) => void | ||
| /** | ||
| * Optional callback to call when a batch succeeds | ||
| */ | ||
| onSuccess?: (result: any, batcher: AsyncBatcher<TValue>) => void | ||
| /** | ||
| * Whether the batcher should start processing immediately | ||
| * @default true | ||
| */ | ||
| started?: boolean | ||
| /** | ||
| * Whether to throw errors when they occur. | ||
| * Defaults to true if no onError handler is provided, false if an onError handler is provided. | ||
| * Can be explicitly set to override these defaults. | ||
| */ | ||
| throwOnError?: boolean | ||
| /** | ||
| * Maximum time in milliseconds to wait before processing a batch. | ||
| * If the wait duration has elapsed, the batch will be processed. | ||
| * If not provided, the batch will not be triggered by a timeout. | ||
| * @default Infinity | ||
| */ | ||
| wait?: number | ((asyncBatcher: AsyncBatcher<TValue>) => number) | ||
| } | ||
| type AsyncBatcherOptionsWithOptionalCallbacks<TValue> = OptionalKeys< | ||
| Required<AsyncBatcherOptions<TValue>>, | ||
| | 'initialState' | ||
| | 'onError' | ||
| | 'onExecute' | ||
| | 'onItemsChange' | ||
| | 'onSettled' | ||
| | 'onSuccess' | ||
| > | ||
| const defaultOptions: AsyncBatcherOptionsWithOptionalCallbacks<any> = { | ||
| getShouldExecute: () => false, | ||
| maxSize: Infinity, | ||
| started: true, | ||
| throwOnError: true, | ||
| wait: Infinity, | ||
| } | ||
| /** | ||
| * A class that collects items and processes them in batches asynchronously. | ||
| * | ||
| * This is the async version of the Batcher class. Unlike the sync version, this async batcher: | ||
| * - Handles promises and returns results from batch executions | ||
| * - Provides error handling with configurable error behavior | ||
| * - Tracks success, error, and settle counts separately | ||
| * - Has state tracking for when batches are executing | ||
| * - Returns the result of the batch function execution | ||
| * | ||
| * Batching is a technique for grouping multiple operations together to be processed as a single unit. | ||
| * | ||
| * The AsyncBatcher provides a flexible way to implement async batching with configurable: | ||
| * - Maximum batch size (number of items per batch) | ||
| * - Time-based batching (process after X milliseconds) | ||
| * - Custom batch processing logic via getShouldExecute | ||
| * - Event callbacks for monitoring batch operations | ||
| * - Error handling for failed batch operations | ||
| * | ||
| * Error Handling: | ||
| * - If an `onError` handler is provided, it will be called with the error and batcher instance | ||
| * - If `throwOnError` is true (default when no onError handler is provided), the error will be thrown | ||
| * - If `throwOnError` is false (default when onError handler is provided), the error will be swallowed | ||
| * - Both onError and throwOnError can be used together - the handler will be called before any error is thrown | ||
| * - The error state can be checked using the AsyncBatcher instance | ||
| * | ||
| * State Management: | ||
| * - Uses TanStack Store for reactive state management | ||
| * - Use `initialState` to provide initial state values when creating the async batcher | ||
| * - Use `onSuccess` callback to react to successful batch execution and implement custom logic | ||
| * - Use `onError` callback to react to batch execution errors and implement custom error handling | ||
| * - Use `onSettled` callback to react to batch execution completion (success or error) and implement custom logic | ||
| * - Use `onExecute` callback to react to batch execution and implement custom logic | ||
| * - Use `onItemsChange` callback to react to items being added or removed from the batcher | ||
| * - The state includes total items processed, success/error counts, and execution status | ||
| * - State can be accessed via `asyncBatcher.store.state` when using the class directly | ||
| * - When using framework adapters (React/Solid), state is accessed from `asyncBatcher.state` | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * const batcher = new AsyncBatcher<number>( | ||
| * async (items) => { | ||
| * const result = await processItems(items); | ||
| * console.log('Processing batch:', items); | ||
| * return result; | ||
| * }, | ||
| * { | ||
| * maxSize: 5, | ||
| * wait: 2000, | ||
| * onSuccess: (result) => console.log('Batch succeeded:', result), | ||
| * onError: (error) => console.error('Batch failed:', error) | ||
| * } | ||
| * ); | ||
| * | ||
| * batcher.addItem(1); | ||
| * batcher.addItem(2); | ||
| * // After 2 seconds or when 5 items are added, whichever comes first, | ||
| * // the batch will be processed and the result will be available | ||
| * // batcher.execute() // manually trigger a batch | ||
| * ``` | ||
| */ | ||
| export class AsyncBatcher<TValue> { | ||
| readonly store: Store<Readonly<AsyncBatcherState<TValue>>> = new Store( | ||
| getDefaultAsyncBatcherState<TValue>(), | ||
| ) | ||
| options: AsyncBatcherOptionsWithOptionalCallbacks<TValue> | ||
| #timeoutId: NodeJS.Timeout | null = null | ||
| constructor( | ||
| private fn: (items: Array<TValue>) => Promise<any>, | ||
| initialOptions: AsyncBatcherOptions<TValue>, | ||
| ) { | ||
| this.options = { | ||
| ...defaultOptions, | ||
| ...initialOptions, | ||
| throwOnError: initialOptions.throwOnError ?? !initialOptions.onError, | ||
| } | ||
| this.#setState(this.options.initialState ?? {}) | ||
| } | ||
| /** | ||
| * Updates the async batcher options | ||
| */ | ||
| setOptions = (newOptions: Partial<AsyncBatcherOptions<TValue>>): void => { | ||
| this.options = { ...this.options, ...newOptions } | ||
| } | ||
| #setState = (newState: Partial<AsyncBatcherState<TValue>>): void => { | ||
| this.store.setState((state) => { | ||
| const combinedState = { | ||
| ...state, | ||
| ...newState, | ||
| } | ||
| const { isExecuting, isPending, items } = combinedState | ||
| const size = items.length | ||
| const isEmpty = size === 0 | ||
| return { | ||
| ...combinedState, | ||
| isEmpty, | ||
| size, | ||
| status: isExecuting | ||
| ? 'executing' | ||
| : isPending | ||
| ? 'pending' | ||
| : isEmpty | ||
| ? 'idle' | ||
| : 'populated', | ||
| } | ||
| }) | ||
| } | ||
| #getWait = (): number => { | ||
| return parseFunctionOrValue(this.options.wait, this) | ||
| } | ||
| /** | ||
| * Adds an item to the async batcher | ||
| * If the batch size is reached, timeout occurs, or shouldProcess returns true, the batch will be processed | ||
| */ | ||
| addItem = (item: TValue): void => { | ||
| this.#setState({ | ||
| items: [...this.store.state.items, item], | ||
| isPending: this.options.wait !== Infinity, | ||
| }) | ||
| this.options.onItemsChange?.(this) | ||
| const shouldProcess = | ||
| this.store.state.items.length >= this.options.maxSize || | ||
| this.options.getShouldExecute(this.store.state.items, this) | ||
| if (shouldProcess) { | ||
| this.#execute() | ||
| } else if (this.store.state.isRunning && this.options.wait !== Infinity) { | ||
| this.#clearTimeout() // clear any pending timeout to replace it with a new one | ||
| this.#timeoutId = setTimeout(() => this.#execute(), this.#getWait()) | ||
| } | ||
| } | ||
| /** | ||
| * Processes the current batch of items asynchronously. | ||
| * This method will automatically be triggered if the batcher is running and any of these conditions are met: | ||
| * - The number of items reaches maxSize | ||
| * - The wait duration has elapsed | ||
| * - The getShouldExecute function returns true upon adding an item | ||
| * | ||
| * You can also call this method manually to process the current batch at any time. | ||
| * | ||
| * @returns A promise that resolves with the result of the batch function, or undefined if an error occurred and was handled by onError | ||
| * @throws The error from the batch function if no onError handler is configured | ||
| */ | ||
| #execute = async (): Promise<any> => { | ||
| if (this.store.state.items.length === 0) { | ||
| return undefined | ||
| } | ||
| const batch = this.peekAllItems() // copy of the items to be processed (to prevent race conditions) | ||
| this.clear() // Clear items before processing to prevent race conditions | ||
| this.options.onItemsChange?.(this) // Call onItemsChange to notify listeners that the items have changed | ||
| this.#setState({ isExecuting: true }) | ||
| try { | ||
| const result = await this.fn(batch) // EXECUTE | ||
| this.#setState({ | ||
| totalItemsProcessed: | ||
| this.store.state.totalItemsProcessed + batch.length, | ||
| lastResult: result, | ||
| successCount: this.store.state.successCount + 1, | ||
| }) | ||
| this.options.onSuccess?.(result, this) | ||
| return result | ||
| } catch (error) { | ||
| this.#setState({ | ||
| errorCount: this.store.state.errorCount + 1, | ||
| failedItems: [...this.store.state.failedItems, ...batch], | ||
| totalItemsFailed: this.store.state.totalItemsFailed + batch.length, | ||
| }) | ||
| this.options.onError?.(error, batch, this) | ||
| if (this.options.throwOnError) { | ||
| throw error | ||
| } | ||
| return undefined | ||
| } finally { | ||
| this.#setState({ | ||
| isExecuting: false, | ||
| settleCount: this.store.state.settleCount + 1, | ||
| }) | ||
| this.options.onSettled?.(this) | ||
| this.options.onExecute?.(this) | ||
| } | ||
| } | ||
| /** | ||
| * Processes the current batch of items immediately | ||
| */ | ||
| flush = async (): Promise<any> => { | ||
| this.#clearTimeout() // clear any pending timeout | ||
| return await this.#execute() | ||
| } | ||
| /** | ||
| * Stops the async batcher from processing batches | ||
| */ | ||
| stop = (): void => { | ||
| this.#setState({ isRunning: false }) | ||
| this.#clearTimeout() | ||
| } | ||
| /** | ||
| * Starts the async batcher and processes any pending items | ||
| */ | ||
| start = (): void => { | ||
| this.#setState({ isRunning: true }) | ||
| if (this.store.state.items.length > 0 && !this.#timeoutId) { | ||
| this.#timeoutId = setTimeout(() => this.#execute(), this.#getWait()) | ||
| } | ||
| } | ||
| /** | ||
| * Returns a copy of all items in the async batcher | ||
| */ | ||
| peekAllItems = (): Array<TValue> => { | ||
| return [...this.store.state.items] | ||
| } | ||
| peekFailedItems = (): Array<TValue> => { | ||
| return [...this.store.state.failedItems] | ||
| } | ||
| #clearTimeout = (): void => { | ||
| if (this.#timeoutId) { | ||
| clearTimeout(this.#timeoutId) | ||
| this.#timeoutId = null | ||
| } | ||
| } | ||
| /** | ||
| * Removes all items from the async batcher | ||
| */ | ||
| clear = (): void => { | ||
| this.#setState({ items: [], failedItems: [], isPending: false }) | ||
| } | ||
| /** | ||
| * Resets the async batcher state to its default values | ||
| */ | ||
| reset = (): void => { | ||
| this.#setState(getDefaultAsyncBatcherState<TValue>()) | ||
| this.options.onItemsChange?.(this) | ||
| } | ||
| } | ||
| /** | ||
| * Creates an async batcher that processes items in batches | ||
| * | ||
| * Unlike the sync batcher, this async version: | ||
| * - Handles promises and returns results from batch executions | ||
| * - Provides error handling with configurable error behavior | ||
| * - Tracks success, error, and settle counts separately | ||
| * - Has state tracking for when batches are executing | ||
| * | ||
| * Error Handling: | ||
| * - If an `onError` handler is provided, it will be called with the error and batcher instance | ||
| * - If `throwOnError` is true (default when no onError handler is provided), the error will be thrown | ||
| * - If `throwOnError` is false (default when onError handler is provided), the error will be swallowed | ||
| * - Both onError and throwOnError can be used together - the handler will be called before any error is thrown | ||
| * - The error state can be checked using the underlying AsyncBatcher instance | ||
| * | ||
| * State Management: | ||
| * - Uses TanStack Store for reactive state management | ||
| * - Use `initialState` to provide initial state values when creating the async batcher | ||
| * - Use `onSuccess` callback to react to successful batch execution and implement custom logic | ||
| * - Use `onError` callback to react to batch execution errors and implement custom error handling | ||
| * - Use `onSettled` callback to react to batch execution completion (success or error) and implement custom logic | ||
| * - Use `onExecute` callback to react to batch execution and implement custom logic | ||
| * - Use `onItemsChange` callback to react to items being added or removed from the batcher | ||
| * - The state includes total items processed, success/error counts, and execution status | ||
| * - State can be accessed via the underlying AsyncBatcher instance's `store.state` property | ||
| * - When using framework adapters (React/Solid), state is accessed from the hook's state property | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * const batchItems = asyncBatch<number>( | ||
| * async (items) => { | ||
| * const result = await processApiCall(items); | ||
| * console.log('Processing:', items); | ||
| * return result; | ||
| * }, | ||
| * { | ||
| * maxSize: 3, | ||
| * wait: 1000, | ||
| * onSuccess: (result) => console.log('Batch succeeded:', result), | ||
| * onError: (error) => console.error('Batch failed:', error) | ||
| * } | ||
| * ); | ||
| * | ||
| * batchItems(1); | ||
| * batchItems(2); | ||
| * batchItems(3); // Triggers batch processing | ||
| * ``` | ||
| */ | ||
| export function asyncBatch<TValue>( | ||
| fn: (items: Array<TValue>) => Promise<any>, | ||
| options: AsyncBatcherOptions<TValue>, | ||
| ) { | ||
| const batcher = new AsyncBatcher<TValue>(fn, options) | ||
| return batcher.addItem | ||
| } |
+149
-162
| "use strict"; | ||
| Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" }); | ||
| const store = require("@tanstack/store"); | ||
| const utils = require("./utils.cjs"); | ||
| function getDefaultAsyncDebouncerState() { | ||
| return structuredClone({ | ||
| canLeadingExecute: true, | ||
| errorCount: 0, | ||
| isExecuting: false, | ||
| isPending: false, | ||
| lastArgs: void 0, | ||
| lastResult: void 0, | ||
| settleCount: 0, | ||
| successCount: 0, | ||
| status: "idle" | ||
| }); | ||
| } | ||
| const defaultOptions = { | ||
@@ -13,12 +27,125 @@ enabled: true, | ||
| this.fn = fn; | ||
| this._abortController = null; | ||
| this._canLeadingExecute = true; | ||
| this._errorCount = 0; | ||
| this._isExecuting = false; | ||
| this._isPending = false; | ||
| this._settleCount = 0; | ||
| this._successCount = 0; | ||
| this._timeoutId = null; | ||
| this._resolvePreviousPromise = null; | ||
| this._options = { | ||
| this.store = new store.Store(getDefaultAsyncDebouncerState()); | ||
| this.#abortController = null; | ||
| this.#timeoutId = null; | ||
| this.#resolvePreviousPromise = null; | ||
| this.setOptions = (newOptions) => { | ||
| this.options = { ...this.options, ...newOptions }; | ||
| if (!this.#getEnabled()) { | ||
| this.cancel(); | ||
| } | ||
| }; | ||
| this.#setState = (newState) => { | ||
| this.store.setState((state) => { | ||
| const combinedState = { | ||
| ...state, | ||
| ...newState | ||
| }; | ||
| const { isPending, isExecuting, settleCount } = combinedState; | ||
| return { | ||
| ...combinedState, | ||
| status: !this.#getEnabled() ? "disabled" : isPending ? "pending" : isExecuting ? "executing" : settleCount > 0 ? "settled" : "idle" | ||
| }; | ||
| }); | ||
| }; | ||
| this.#getEnabled = () => { | ||
| return !!utils.parseFunctionOrValue(this.options.enabled, this); | ||
| }; | ||
| this.#getWait = () => { | ||
| return utils.parseFunctionOrValue(this.options.wait, this); | ||
| }; | ||
| this.maybeExecute = async (...args) => { | ||
| if (!this.#getEnabled()) return void 0; | ||
| this.#cancelPendingExecution(); | ||
| this.#setState({ lastArgs: args }); | ||
| if (this.options.leading && this.store.state.canLeadingExecute) { | ||
| this.#setState({ canLeadingExecute: false }); | ||
| await this.#execute(...args); | ||
| return this.store.state.lastResult; | ||
| } | ||
| if (this.options.trailing && this.#getEnabled()) { | ||
| this.#setState({ isPending: true }); | ||
| } | ||
| return new Promise((resolve) => { | ||
| this.#resolvePreviousPromise = resolve; | ||
| this.#timeoutId = setTimeout(async () => { | ||
| if (this.options.trailing && this.store.state.lastArgs) { | ||
| await this.#execute(...this.store.state.lastArgs); | ||
| } | ||
| this.#setState({ canLeadingExecute: true }); | ||
| this.#resolvePreviousPromise = null; | ||
| resolve(this.store.state.lastResult); | ||
| }, this.#getWait()); | ||
| }); | ||
| }; | ||
| this.#execute = async (...args) => { | ||
| if (!this.#getEnabled()) return void 0; | ||
| this.#abortController = new AbortController(); | ||
| try { | ||
| this.#setState({ isExecuting: true }); | ||
| const result = await this.fn(...args); | ||
| this.#setState({ | ||
| lastResult: result, | ||
| successCount: this.store.state.successCount + 1 | ||
| }); | ||
| this.options.onSuccess?.(result, this); | ||
| } catch (error) { | ||
| this.#setState({ | ||
| errorCount: this.store.state.errorCount + 1 | ||
| }); | ||
| this.options.onError?.(error, this); | ||
| if (this.options.throwOnError) { | ||
| throw error; | ||
| } | ||
| } finally { | ||
| this.#setState({ | ||
| isExecuting: false, | ||
| isPending: false, | ||
| settleCount: this.store.state.settleCount + 1 | ||
| }); | ||
| this.#abortController = null; | ||
| this.options.onSettled?.(this); | ||
| } | ||
| return this.store.state.lastResult; | ||
| }; | ||
| this.flush = () => { | ||
| if (this.store.state.isPending && this.store.state.lastArgs) { | ||
| this.#abortExecution(); | ||
| this.#clearTimeout(); | ||
| this.#execute(...this.store.state.lastArgs); | ||
| } | ||
| }; | ||
| this.#clearTimeout = () => { | ||
| if (this.#timeoutId) { | ||
| clearTimeout(this.#timeoutId); | ||
| this.#timeoutId = null; | ||
| } | ||
| }; | ||
| this.#cancelPendingExecution = () => { | ||
| this.#clearTimeout(); | ||
| if (this.#resolvePreviousPromise) { | ||
| this.#resolvePreviousPromise(this.store.state.lastResult); | ||
| this.#resolvePreviousPromise = null; | ||
| } | ||
| this.#setState({ | ||
| isPending: false, | ||
| isExecuting: false, | ||
| lastArgs: void 0 | ||
| }); | ||
| }; | ||
| this.#abortExecution = () => { | ||
| if (this.#abortController) { | ||
| this.#abortController.abort(); | ||
| this.#abortController = null; | ||
| } | ||
| }; | ||
| this.cancel = () => { | ||
| this.#cancelPendingExecution(); | ||
| this.#abortExecution(); | ||
| this.#setState({ canLeadingExecute: true }); | ||
| }; | ||
| this.reset = () => { | ||
| this.#setState(getDefaultAsyncDebouncerState()); | ||
| }; | ||
| this.options = { | ||
| ...defaultOptions, | ||
@@ -28,158 +155,18 @@ ...initialOptions, | ||
| }; | ||
| this.#setState(this.options.initialState ?? {}); | ||
| } | ||
| /** | ||
| * Updates the debouncer options | ||
| */ | ||
| setOptions(newOptions) { | ||
| this._options = { ...this._options, ...newOptions }; | ||
| if (!this._options.enabled) { | ||
| this._isPending = false; | ||
| } | ||
| } | ||
| /** | ||
| * Returns the current debouncer options | ||
| */ | ||
| getOptions() { | ||
| return this._options; | ||
| } | ||
| /** | ||
| * Returns the current debouncer enabled state | ||
| */ | ||
| getEnabled() { | ||
| return !!utils.parseFunctionOrValue(this._options.enabled, this); | ||
| } | ||
| /** | ||
| * Returns the current debouncer wait state | ||
| */ | ||
| getWait() { | ||
| return utils.parseFunctionOrValue(this._options.wait, this); | ||
| } | ||
| /** | ||
| * Attempts to execute the debounced function. | ||
| * If a call is already in progress, it will be queued. | ||
| * | ||
| * Error Handling: | ||
| * - If the debounced function throws and no `onError` handler is configured, | ||
| * the error will be thrown from this method. | ||
| * - If an `onError` handler is configured, errors will be caught and passed to the handler, | ||
| * and this method will return undefined. | ||
| * - The error state can be checked using `getErrorCount()` and `getIsExecuting()`. | ||
| * | ||
| * @returns A promise that resolves with the function's return value, or undefined if an error occurred and was handled by onError | ||
| * @throws The error from the debounced function if no onError handler is configured | ||
| */ | ||
| async maybeExecute(...args) { | ||
| this._cancel(); | ||
| this._lastArgs = args; | ||
| if (this._options.leading && this._canLeadingExecute) { | ||
| this._canLeadingExecute = false; | ||
| await this.execute(...args); | ||
| return this._lastResult; | ||
| } | ||
| if (this._options.trailing) { | ||
| this._isPending = true; | ||
| } | ||
| return new Promise((resolve) => { | ||
| this._resolvePreviousPromise = resolve; | ||
| this._timeoutId = setTimeout(async () => { | ||
| if (this._options.trailing && this._lastArgs) { | ||
| await this.execute(...this._lastArgs); | ||
| } | ||
| this._canLeadingExecute = true; | ||
| this._resolvePreviousPromise = null; | ||
| resolve(this._lastResult); | ||
| }, this.getWait()); | ||
| }); | ||
| } | ||
| async execute(...args) { | ||
| var _a, _b, _c, _d, _e, _f; | ||
| if (!this.getEnabled()) return void 0; | ||
| this._abortController = new AbortController(); | ||
| try { | ||
| this._isExecuting = true; | ||
| this._lastResult = await this.fn(...args); | ||
| this._successCount++; | ||
| (_b = (_a = this._options).onSuccess) == null ? void 0 : _b.call(_a, this._lastResult, this); | ||
| } catch (error) { | ||
| this._errorCount++; | ||
| (_d = (_c = this._options).onError) == null ? void 0 : _d.call(_c, error, this); | ||
| if (this._options.throwOnError) { | ||
| throw error; | ||
| } | ||
| } finally { | ||
| this._isExecuting = false; | ||
| this._isPending = false; | ||
| this._settleCount++; | ||
| this._abortController = null; | ||
| (_f = (_e = this._options).onSettled) == null ? void 0 : _f.call(_e, this); | ||
| } | ||
| return this._lastResult; | ||
| } | ||
| /** | ||
| * Cancel without resetting _canLeadingExecute | ||
| */ | ||
| _cancel() { | ||
| if (this._timeoutId) { | ||
| clearTimeout(this._timeoutId); | ||
| this._timeoutId = null; | ||
| } | ||
| if (this._abortController) { | ||
| this._abortController.abort(); | ||
| this._abortController = null; | ||
| } | ||
| if (this._resolvePreviousPromise) { | ||
| this._resolvePreviousPromise(this._lastResult); | ||
| this._resolvePreviousPromise = null; | ||
| } | ||
| this._lastArgs = void 0; | ||
| this._isPending = false; | ||
| this._isExecuting = false; | ||
| } | ||
| /** | ||
| * Cancels any pending execution or aborts any execution in progress | ||
| */ | ||
| cancel() { | ||
| this._canLeadingExecute = true; | ||
| this._cancel(); | ||
| } | ||
| /** | ||
| * Returns the last result of the debounced function | ||
| */ | ||
| getLastResult() { | ||
| return this._lastResult; | ||
| } | ||
| /** | ||
| * Returns the number of times the function has been executed successfully | ||
| */ | ||
| getSuccessCount() { | ||
| return this._successCount; | ||
| } | ||
| /** | ||
| * Returns the number of times the function has settled (completed or errored) | ||
| */ | ||
| getSettleCount() { | ||
| return this._settleCount; | ||
| } | ||
| /** | ||
| * Returns the number of times the function has errored | ||
| */ | ||
| getErrorCount() { | ||
| return this._errorCount; | ||
| } | ||
| /** | ||
| * Returns `true` if there is a pending execution queued up for trailing execution | ||
| */ | ||
| getIsPending() { | ||
| return this.getEnabled() && this._isPending; | ||
| } | ||
| /** | ||
| * Returns `true` if there is currently an execution in progress | ||
| */ | ||
| getIsExecuting() { | ||
| return this._isExecuting; | ||
| } | ||
| #abortController; | ||
| #timeoutId; | ||
| #resolvePreviousPromise; | ||
| #setState; | ||
| #getEnabled; | ||
| #getWait; | ||
| #execute; | ||
| #clearTimeout; | ||
| #cancelPendingExecution; | ||
| #abortExecution; | ||
| } | ||
| function asyncDebounce(fn, initialOptions) { | ||
| const asyncDebouncer = new AsyncDebouncer(fn, initialOptions); | ||
| return asyncDebouncer.maybeExecute.bind(asyncDebouncer); | ||
| return asyncDebouncer.maybeExecute; | ||
| } | ||
@@ -186,0 +173,0 @@ exports.AsyncDebouncer = AsyncDebouncer; |
@@ -1,1 +0,1 @@ | ||
| {"version":3,"file":"async-debouncer.cjs","sources":["../../src/async-debouncer.ts"],"sourcesContent":["import { parseFunctionOrValue } from './utils'\nimport type { AnyAsyncFunction, OptionalKeys } from './types'\n\n/**\n * Options for configuring an async debounced function\n */\nexport interface AsyncDebouncerOptions<TFn extends AnyAsyncFunction> {\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 * 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?: (error: unknown, debouncer: AsyncDebouncer<TFn>) => void\n /**\n * Optional callback to call when the debounced function is executed\n */\n onSettled?: (debouncer: AsyncDebouncer<TFn>) => void\n /**\n * Optional callback to call when the debounced function is executed\n */\n onSuccess?: (result: ReturnType<TFn>, debouncer: AsyncDebouncer<TFn>) => 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\ntype AsyncDebouncerOptionsWithOptionalCallbacks = OptionalKeys<\n AsyncDebouncerOptions<any>,\n 'onError' | 'onSettled' | 'onSuccess'\n>\n\nconst defaultOptions: AsyncDebouncerOptionsWithOptionalCallbacks = {\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 * 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 * Unlike the non-async Debouncer, this async version supports returning values from the debounced function,\n * making it ideal for API calls and other async operations where you want the result of the `maybeExecute` call\n * instead of setting the result on a state variable from within the debounced function.\n *\n * Error Handling:\n * - If an error occurs during execution and no `onError` handler is provided, the error will be thrown and propagate up to the caller.\n * - If an `onError` handler is provided, errors will be caught and passed to the handler instead of being thrown.\n * - The error count can be tracked using `getErrorCount()`.\n * - The debouncer maintains its state and can continue to be used after an error occurs.\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 private _options: AsyncDebouncerOptionsWithOptionalCallbacks\n private _abortController: AbortController | null = null\n private _canLeadingExecute = true\n private _errorCount = 0\n private _isExecuting = false\n private _isPending = false\n private _lastArgs: Parameters<TFn> | undefined\n private _lastResult: ReturnType<TFn> | undefined\n private _settleCount = 0\n private _successCount = 0\n private _timeoutId: NodeJS.Timeout | null = null\n private _resolvePreviousPromise:\n | ((value?: ReturnType<TFn> | undefined) => void)\n | null = null\n\n constructor(\n private fn: TFn,\n initialOptions: AsyncDebouncerOptions<TFn>,\n ) {\n this._options = {\n ...defaultOptions,\n ...initialOptions,\n throwOnError: initialOptions.throwOnError ?? !initialOptions.onError,\n }\n }\n\n /**\n * Updates the debouncer options\n */\n setOptions(newOptions: Partial<AsyncDebouncerOptions<TFn>>): void {\n this._options = { ...this._options, ...newOptions }\n\n // End the pending state if the debouncer is disabled\n if (!this._options.enabled) {\n this._isPending = false\n }\n }\n\n /**\n * Returns the current debouncer options\n */\n getOptions(): AsyncDebouncerOptions<TFn> {\n return this._options\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 async maybeExecute(\n ...args: Parameters<TFn>\n ): Promise<ReturnType<TFn> | undefined> {\n this._cancel()\n this._lastArgs = args\n\n // Handle leading execution\n if (this._options.leading && this._canLeadingExecute) {\n this._canLeadingExecute = false\n await this.execute(...args)\n return this._lastResult\n }\n\n // Handle trailing execution\n if (this._options.trailing) {\n this._isPending = true\n }\n\n return new Promise((resolve) => {\n this._resolvePreviousPromise = resolve\n this._timeoutId = setTimeout(async () => {\n // Execute trailing if enabled\n if (this._options.trailing && this._lastArgs) {\n await this.execute(...this._lastArgs)\n }\n\n // Reset state and resolve\n this._canLeadingExecute = true\n this._resolvePreviousPromise = null\n resolve(this._lastResult)\n }, this.getWait())\n })\n }\n\n private async execute(\n ...args: Parameters<TFn>\n ): Promise<ReturnType<TFn> | undefined> {\n if (!this.getEnabled()) return undefined\n this._abortController = new AbortController()\n try {\n this._isExecuting = true\n this._lastResult = await this.fn(...args) // EXECUTE!\n this._successCount++\n this._options.onSuccess?.(this._lastResult!, this)\n } catch (error) {\n this._errorCount++\n this._options.onError?.(error, this)\n if (this._options.throwOnError) {\n throw error\n }\n } finally {\n this._isExecuting = false\n this._isPending = false\n this._settleCount++\n this._abortController = null\n this._options.onSettled?.(this)\n }\n return this._lastResult\n }\n\n /**\n * Cancel without resetting _canLeadingExecute\n */\n private _cancel(): void {\n if (this._timeoutId) {\n clearTimeout(this._timeoutId)\n this._timeoutId = null\n }\n if (this._abortController) {\n this._abortController.abort()\n this._abortController = null\n }\n if (this._resolvePreviousPromise) {\n this._resolvePreviousPromise(this._lastResult)\n this._resolvePreviousPromise = null\n }\n this._lastArgs = undefined\n this._isPending = false\n this._isExecuting = false\n }\n\n /**\n * Cancels any pending execution or aborts any execution in progress\n */\n cancel(): void {\n this._canLeadingExecute = true\n this._cancel()\n }\n\n /**\n * Returns the last result of the debounced function\n */\n getLastResult(): ReturnType<TFn> | undefined {\n return this._lastResult\n }\n\n /**\n * Returns the number of times the function has been executed successfully\n */\n getSuccessCount(): number {\n return this._successCount\n }\n\n /**\n * Returns the number of times the function has settled (completed or errored)\n */\n getSettleCount(): number {\n return this._settleCount\n }\n\n /**\n * Returns the number of times the function has errored\n */\n getErrorCount(): number {\n return this._errorCount\n }\n\n /**\n * Returns `true` if there is a pending execution queued up for trailing execution\n */\n getIsPending(): boolean {\n return this.getEnabled() && this._isPending\n }\n\n /**\n * Returns `true` if there is currently an execution in progress\n */\n getIsExecuting(): boolean {\n return this._isExecuting\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 * Unlike the non-async Debouncer, this async version supports returning values from the debounced function,\n * making it ideal for API calls and other async operations where you want the result of the `maybeExecute` call\n * instead of setting the result on a state variable from within the debounced function.\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 * @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.bind(asyncDebouncer)\n}\n"],"names":["parseFunctionOrValue"],"mappings":"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"} | ||
| {"version":3,"file":"async-debouncer.cjs","sources":["../../src/async-debouncer.ts"],"sourcesContent":["import { Store } from '@tanstack/store'\nimport { parseFunctionOrValue } from './utils'\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 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 structuredClone({\n canLeadingExecute: true,\n errorCount: 0,\n isExecuting: false,\n isPending: false,\n lastArgs: undefined,\n lastResult: undefined,\n settleCount: 0,\n successCount: 0,\n status: 'idle',\n })\n}\n\n/**\n * Options for configuring an async debounced function\n */\nexport interface AsyncDebouncerOptions<TFn extends AnyAsyncFunction> {\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 * 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?: (error: unknown, debouncer: AsyncDebouncer<TFn>) => void\n /**\n * Optional callback to call when the debounced function is executed\n */\n onSettled?: (debouncer: AsyncDebouncer<TFn>) => void\n /**\n * Optional callback to call when the debounced function is executed\n */\n onSuccess?: (result: ReturnType<TFn>, debouncer: AsyncDebouncer<TFn>) => 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\ntype AsyncDebouncerOptionsWithOptionalCallbacks = OptionalKeys<\n AsyncDebouncerOptions<any>,\n 'initialState' | 'onError' | 'onSettled' | 'onSuccess'\n>\n\nconst defaultOptions: AsyncDebouncerOptionsWithOptionalCallbacks = {\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 * 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 * Unlike the non-async Debouncer, this async version supports returning values from the debounced function,\n * making it ideal for API calls and other async operations where you want the result of the `maybeExecute` call\n * instead of setting the result on a state variable from within the debounced function.\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 options: AsyncDebouncerOptions<TFn>\n #abortController: AbortController | null = null\n #timeoutId: NodeJS.Timeout | null = null\n #resolvePreviousPromise:\n | ((value?: ReturnType<TFn> | undefined) => void)\n | null = null\n\n constructor(\n private fn: TFn,\n initialOptions: AsyncDebouncerOptions<TFn>,\n ) {\n this.options = {\n ...defaultOptions,\n ...initialOptions,\n throwOnError: initialOptions.throwOnError ?? !initialOptions.onError,\n }\n this.#setState(this.options.initialState ?? {})\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 }\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({ lastArgs: args })\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) => {\n this.#resolvePreviousPromise = resolve\n this.#timeoutId = setTimeout(async () => {\n // Execute trailing if enabled\n if (this.options.trailing && this.store.state.lastArgs) {\n await this.#execute(...this.store.state.lastArgs)\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 this.#abortController = new AbortController()\n try {\n this.#setState({ isExecuting: true })\n const result = await this.fn(...args) // EXECUTE!\n this.#setState({\n lastResult: result,\n successCount: this.store.state.successCount + 1,\n })\n this.options.onSuccess?.(result, this)\n } catch (error) {\n this.#setState({\n errorCount: this.store.state.errorCount + 1,\n })\n this.options.onError?.(error, this)\n if (this.options.throwOnError) {\n throw error\n }\n } finally {\n this.#setState({\n isExecuting: false,\n isPending: false,\n settleCount: this.store.state.settleCount + 1,\n })\n this.#abortController = null\n this.options.onSettled?.(this)\n }\n return this.store.state.lastResult\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.#abortExecution() // abort any current execution\n this.#clearTimeout() // clear any existing timeout\n this.#execute(...this.store.state.lastArgs)\n }\n }\n\n #clearTimeout = (): void => {\n if (this.#timeoutId) {\n clearTimeout(this.#timeoutId)\n this.#timeoutId = null\n }\n }\n\n #cancelPendingExecution = (): void => {\n this.#clearTimeout()\n if (this.#resolvePreviousPromise) {\n this.#resolvePreviousPromise(this.store.state.lastResult)\n this.#resolvePreviousPromise = null\n }\n this.#setState({\n isPending: false,\n isExecuting: false,\n lastArgs: undefined,\n })\n }\n\n #abortExecution = (): void => {\n if (this.#abortController) {\n this.#abortController.abort()\n this.#abortController = null\n }\n }\n\n /**\n * Cancels any pending execution or aborts any execution in progress\n */\n cancel = (): void => {\n this.#cancelPendingExecution()\n this.#abortExecution()\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 }\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 * Unlike the non-async Debouncer, this async version supports returning values from the debounced function,\n * making it ideal for API calls and other async operations where you want the result of the `maybeExecute` call\n * instead of setting the result on a state variable from within the debounced function.\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"],"names":["Store","parseFunctionOrValue"],"mappings":";;;;AA2CA,SAAS,gCAEqB;AAC5B,SAAO,gBAAgB;AAAA,IACrB,mBAAmB;AAAA,IACnB,YAAY;AAAA,IACZ,aAAa;AAAA,IACb,WAAW;AAAA,IACX,UAAU;AAAA,IACV,YAAY;AAAA,IACZ,aAAa;AAAA,IACb,cAAc;AAAA,IACd,QAAQ;AAAA,EAAA,CACT;AACH;AA2DA,MAAM,iBAA6D;AAAA,EACjE,SAAS;AAAA,EACT,SAAS;AAAA,EACT,UAAU;AAAA,EACV,MAAM;AACR;AA+CO,MAAM,eAA6C;AAAA,EAWxD,YACU,IACR,gBACA;AAFQ,SAAA,KAAA;AAXV,SAAS,QAAmD,IAAIA,MAAAA,MAE9D,8BAAA,CAAoC;AAEtC,SAAA,mBAA2C;AAC3C,SAAA,aAAoC;AACpC,SAAA,0BAEW;AAiBX,SAAA,aAAa,CAAC,eAA0D;AACtE,WAAK,UAAU,EAAE,GAAG,KAAK,SAAS,GAAG,WAAA;AAGrC,UAAI,CAAC,KAAK,eAAe;AACvB,aAAK,OAAA;AAAA,MAAO;AAAA,IACd;AAGF,SAAA,YAAY,CAAC,aAAsD;AACjE,WAAK,MAAM,SAAS,CAAC,UAAU;AAC7B,cAAM,gBAAgB;AAAA,UACpB,GAAG;AAAA,UACH,GAAG;AAAA,QAAA;AAEL,cAAM,EAAE,WAAW,aAAa,YAAA,IAAgB;AAChD,eAAO;AAAA,UACL,GAAG;AAAA,UACH,QAAQ,CAAC,KAAK,YAAA,IACV,aACA,YACE,YACA,cACE,cACA,cAAc,IACZ,YACA;AAAA,QAAA;AAAA,MACZ,CACD;AAAA,IAAA;AAMH,SAAA,cAAc,MAAe;AAC3B,aAAO,CAAC,CAACC,MAAAA,qBAAqB,KAAK,QAAQ,SAAS,IAAI;AAAA,IAAA;AAM1D,SAAA,WAAW,MAAc;AACvB,aAAOA,MAAAA,qBAAqB,KAAK,QAAQ,MAAM,IAAI;AAAA,IAAA;AAiBrD,SAAA,eAAe,UACV,SACsC;AACzC,UAAI,CAAC,KAAK,YAAA,EAAe,QAAO;AAChC,WAAK,wBAAA;AACL,WAAK,UAAU,EAAE,UAAU,KAAA,CAAM;AAGjC,UAAI,KAAK,QAAQ,WAAW,KAAK,MAAM,MAAM,mBAAmB;AAC9D,aAAK,UAAU,EAAE,mBAAmB,MAAA,CAAO;AAC3C,cAAM,KAAK,SAAS,GAAG,IAAI;AAC3B,eAAO,KAAK,MAAM,MAAM;AAAA,MAAA;AAI1B,UAAI,KAAK,QAAQ,YAAY,KAAK,eAAe;AAC/C,aAAK,UAAU,EAAE,WAAW,KAAA,CAAM;AAAA,MAAA;AAGpC,aAAO,IAAI,QAAQ,CAAC,YAAY;AAC9B,aAAK,0BAA0B;AAC/B,aAAK,aAAa,WAAW,YAAY;AAEvC,cAAI,KAAK,QAAQ,YAAY,KAAK,MAAM,MAAM,UAAU;AACtD,kBAAM,KAAK,SAAS,GAAG,KAAK,MAAM,MAAM,QAAQ;AAAA,UAAA;AAIlD,eAAK,UAAU,EAAE,mBAAmB,KAAA,CAAM;AAC1C,eAAK,0BAA0B;AAC/B,kBAAQ,KAAK,MAAM,MAAM,UAAU;AAAA,QAAA,GAClC,KAAK,UAAU;AAAA,MAAA,CACnB;AAAA,IAAA;AAGH,SAAA,WAAW,UACN,SACsC;AACzC,UAAI,CAAC,KAAK,YAAA,EAAe,QAAO;AAChC,WAAK,mBAAmB,IAAI,gBAAA;AAC5B,UAAI;AACF,aAAK,UAAU,EAAE,aAAa,KAAA,CAAM;AACpC,cAAM,SAAS,MAAM,KAAK,GAAG,GAAG,IAAI;AACpC,aAAK,UAAU;AAAA,UACb,YAAY;AAAA,UACZ,cAAc,KAAK,MAAM,MAAM,eAAe;AAAA,QAAA,CAC/C;AACD,aAAK,QAAQ,YAAY,QAAQ,IAAI;AAAA,MAAA,SAC9B,OAAO;AACd,aAAK,UAAU;AAAA,UACb,YAAY,KAAK,MAAM,MAAM,aAAa;AAAA,QAAA,CAC3C;AACD,aAAK,QAAQ,UAAU,OAAO,IAAI;AAClC,YAAI,KAAK,QAAQ,cAAc;AAC7B,gBAAM;AAAA,QAAA;AAAA,MACR,UACF;AACE,aAAK,UAAU;AAAA,UACb,aAAa;AAAA,UACb,WAAW;AAAA,UACX,aAAa,KAAK,MAAM,MAAM,cAAc;AAAA,QAAA,CAC7C;AACD,aAAK,mBAAmB;AACxB,aAAK,QAAQ,YAAY,IAAI;AAAA,MAAA;AAE/B,aAAO,KAAK,MAAM,MAAM;AAAA,IAAA;AAM1B,SAAA,QAAQ,MAAY;AAClB,UAAI,KAAK,MAAM,MAAM,aAAa,KAAK,MAAM,MAAM,UAAU;AAC3D,aAAK,gBAAA;AACL,aAAK,cAAA;AACL,aAAK,SAAS,GAAG,KAAK,MAAM,MAAM,QAAQ;AAAA,MAAA;AAAA,IAC5C;AAGF,SAAA,gBAAgB,MAAY;AAC1B,UAAI,KAAK,YAAY;AACnB,qBAAa,KAAK,UAAU;AAC5B,aAAK,aAAa;AAAA,MAAA;AAAA,IACpB;AAGF,SAAA,0BAA0B,MAAY;AACpC,WAAK,cAAA;AACL,UAAI,KAAK,yBAAyB;AAChC,aAAK,wBAAwB,KAAK,MAAM,MAAM,UAAU;AACxD,aAAK,0BAA0B;AAAA,MAAA;AAEjC,WAAK,UAAU;AAAA,QACb,WAAW;AAAA,QACX,aAAa;AAAA,QACb,UAAU;AAAA,MAAA,CACX;AAAA,IAAA;AAGH,SAAA,kBAAkB,MAAY;AAC5B,UAAI,KAAK,kBAAkB;AACzB,aAAK,iBAAiB,MAAA;AACtB,aAAK,mBAAmB;AAAA,MAAA;AAAA,IAC1B;AAMF,SAAA,SAAS,MAAY;AACnB,WAAK,wBAAA;AACL,WAAK,gBAAA;AACL,WAAK,UAAU,EAAE,mBAAmB,KAAA,CAAM;AAAA,IAAA;AAM5C,SAAA,QAAQ,MAAY;AAClB,WAAK,UAAU,+BAAoC;AAAA,IAAA;AA7LnD,SAAK,UAAU;AAAA,MACb,GAAG;AAAA,MACH,GAAG;AAAA,MACH,cAAc,eAAe,gBAAgB,CAAC,eAAe;AAAA,IAAA;AAE/D,SAAK,UAAU,KAAK,QAAQ,gBAAgB,CAAA,CAAE;AAAA,EAAA;AAAA,EAfhD;AAAA,EACA;AAAA,EACA;AAAA,EA4BA;AAAA,EAyBA;AAAA,EAOA;AAAA,EAqDA;AAAA,EA4CA;AAAA,EAOA;AAAA,EAaA;AAsBF;AA8CO,SAAS,cACd,IACA,gBACA;AACA,QAAM,iBAAiB,IAAI,eAAe,IAAI,cAAc;AAC5D,SAAO,eAAe;AACxB;;;"} |
@@ -0,2 +1,41 @@ | ||
| import { Store } from '@tanstack/store'; | ||
| import { AnyAsyncFunction } from './types.cjs'; | ||
| export interface AsyncDebouncerState<TFn extends AnyAsyncFunction> { | ||
| /** | ||
| * Whether the debouncer can execute on the leading edge of the timeout | ||
| */ | ||
| canLeadingExecute: boolean; | ||
| /** | ||
| * Number of function executions that have resulted in errors | ||
| */ | ||
| errorCount: number; | ||
| /** | ||
| * Whether the debounced function is currently executing asynchronously | ||
| */ | ||
| isExecuting: boolean; | ||
| /** | ||
| * Whether the debouncer is waiting for the timeout to trigger execution | ||
| */ | ||
| isPending: boolean; | ||
| /** | ||
| * The arguments from the most recent call to maybeExecute | ||
| */ | ||
| lastArgs: Parameters<TFn> | undefined; | ||
| /** | ||
| * The result from the most recent successful function execution | ||
| */ | ||
| lastResult: ReturnType<TFn> | undefined; | ||
| /** | ||
| * Number of function executions that have completed (either successfully or with errors) | ||
| */ | ||
| settleCount: number; | ||
| /** | ||
| * Current execution status - 'idle' when not active, 'pending' when waiting, 'executing' when running, 'settled' when completed | ||
| */ | ||
| status: 'disabled' | 'idle' | 'pending' | 'executing' | 'settled'; | ||
| /** | ||
| * Number of function executions that have completed successfully | ||
| */ | ||
| successCount: number; | ||
| } | ||
| /** | ||
@@ -13,2 +52,6 @@ * Options for configuring an async debounced function | ||
| /** | ||
| * Initial state for the async debouncer | ||
| */ | ||
| initialState?: Partial<AsyncDebouncerState<TFn>>; | ||
| /** | ||
| * Whether to execute on the leading edge of the timeout. | ||
@@ -65,7 +108,15 @@ * Defaults to false. | ||
| * Error Handling: | ||
| * - If an error occurs during execution and no `onError` handler is provided, the error will be thrown and propagate up to the caller. | ||
| * - If an `onError` handler is provided, errors will be caught and passed to the handler instead of being thrown. | ||
| * - The error count can be tracked using `getErrorCount()`. | ||
| * - The debouncer maintains its state and can continue to be used after an error occurs. | ||
| * - If an `onError` handler is provided, it will be called with the error and debouncer instance | ||
| * - If `throwOnError` is true (default when no onError handler is provided), the error will be thrown | ||
| * - If `throwOnError` is false (default when onError handler is provided), the error will be swallowed | ||
| * - Both onError and throwOnError can be used together - the handler will be called before any error is thrown | ||
| * - The error state can be checked using the underlying store | ||
| * | ||
| * State Management: | ||
| * - The debouncer uses a reactive store for state management | ||
| * - Use `initialState` to provide initial state values when creating the async debouncer | ||
| * - The state includes canLeadingExecute, error count, execution status, and success/settle counts | ||
| * - State can be accessed via the `store` property and its `state` getter | ||
| * - The store is reactive and will notify subscribers of state changes | ||
| * | ||
| * @example | ||
@@ -89,33 +140,12 @@ * ```ts | ||
| export declare class AsyncDebouncer<TFn extends AnyAsyncFunction> { | ||
| #private; | ||
| private fn; | ||
| private _options; | ||
| private _abortController; | ||
| private _canLeadingExecute; | ||
| private _errorCount; | ||
| private _isExecuting; | ||
| private _isPending; | ||
| private _lastArgs; | ||
| private _lastResult; | ||
| private _settleCount; | ||
| private _successCount; | ||
| private _timeoutId; | ||
| private _resolvePreviousPromise; | ||
| readonly store: Store<Readonly<AsyncDebouncerState<TFn>>>; | ||
| options: AsyncDebouncerOptions<TFn>; | ||
| constructor(fn: TFn, initialOptions: AsyncDebouncerOptions<TFn>); | ||
| /** | ||
| * Updates the debouncer options | ||
| * Updates the async debouncer options | ||
| */ | ||
| setOptions(newOptions: Partial<AsyncDebouncerOptions<TFn>>): void; | ||
| setOptions: (newOptions: Partial<AsyncDebouncerOptions<TFn>>) => void; | ||
| /** | ||
| * Returns the current debouncer options | ||
| */ | ||
| getOptions(): AsyncDebouncerOptions<TFn>; | ||
| /** | ||
| * Returns the current debouncer enabled state | ||
| */ | ||
| getEnabled(): boolean; | ||
| /** | ||
| * Returns the current debouncer wait state | ||
| */ | ||
| getWait(): number; | ||
| /** | ||
| * Attempts to execute the debounced function. | ||
@@ -134,36 +164,15 @@ * If a call is already in progress, it will be queued. | ||
| */ | ||
| maybeExecute(...args: Parameters<TFn>): Promise<ReturnType<TFn> | undefined>; | ||
| private execute; | ||
| maybeExecute: (...args: Parameters<TFn>) => Promise<ReturnType<TFn> | undefined>; | ||
| /** | ||
| * Cancel without resetting _canLeadingExecute | ||
| * Processes the current pending execution immediately | ||
| */ | ||
| private _cancel; | ||
| flush: () => void; | ||
| /** | ||
| * Cancels any pending execution or aborts any execution in progress | ||
| */ | ||
| cancel(): void; | ||
| cancel: () => void; | ||
| /** | ||
| * Returns the last result of the debounced function | ||
| * Resets the debouncer state to its default values | ||
| */ | ||
| getLastResult(): ReturnType<TFn> | undefined; | ||
| /** | ||
| * Returns the number of times the function has been executed successfully | ||
| */ | ||
| getSuccessCount(): number; | ||
| /** | ||
| * Returns the number of times the function has settled (completed or errored) | ||
| */ | ||
| getSettleCount(): number; | ||
| /** | ||
| * Returns the number of times the function has errored | ||
| */ | ||
| getErrorCount(): number; | ||
| /** | ||
| * Returns `true` if there is a pending execution queued up for trailing execution | ||
| */ | ||
| getIsPending(): boolean; | ||
| /** | ||
| * Returns `true` if there is currently an execution in progress | ||
| */ | ||
| getIsExecuting(): boolean; | ||
| reset: () => void; | ||
| } | ||
@@ -186,2 +195,12 @@ /** | ||
| * | ||
| * State Management: | ||
| * - Uses TanStack Store for reactive state management | ||
| * - Use `initialState` to provide initial state values when creating the async debouncer | ||
| * - Use `onSuccess` callback to react to successful function execution and implement custom logic | ||
| * - Use `onError` callback to react to function execution errors and implement custom error handling | ||
| * - Use `onSettled` callback to react to function execution completion (success or error) and implement custom logic | ||
| * - The state includes canLeadingExecute, error count, execution status, and success/settle counts | ||
| * - State can be accessed via `asyncDebouncer.store.state` when using the class directly | ||
| * - When using framework adapters (React/Solid), state is accessed from `asyncDebouncer.state` | ||
| * | ||
| * @example | ||
@@ -188,0 +207,0 @@ * ```ts |
+282
-343
| "use strict"; | ||
| Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" }); | ||
| const store = require("@tanstack/store"); | ||
| const utils = require("./utils.cjs"); | ||
| function getDefaultAsyncQueuerState() { | ||
| return structuredClone({ | ||
| activeItems: [], | ||
| errorCount: 0, | ||
| expirationCount: 0, | ||
| isEmpty: true, | ||
| isFull: false, | ||
| isIdle: true, | ||
| isRunning: true, | ||
| itemTimestamps: [], | ||
| items: [], | ||
| lastResult: null, | ||
| pendingTick: false, | ||
| rejectionCount: 0, | ||
| settledCount: 0, | ||
| size: 0, | ||
| status: "idle", | ||
| successCount: 0 | ||
| }); | ||
| } | ||
| const defaultOptions = { | ||
@@ -10,3 +31,3 @@ addItemsTo: "back", | ||
| getItemsFrom: "front", | ||
| getPriority: (item) => (item == null ? void 0 : item.priority) ?? 0, | ||
| getPriority: (item) => item?.priority ?? 0, | ||
| initialItems: [], | ||
@@ -18,361 +39,279 @@ maxSize: Infinity, | ||
| class AsyncQueuer { | ||
| constructor(fn, initialOptions) { | ||
| constructor(fn, initialOptions = {}) { | ||
| this.fn = fn; | ||
| this._activeItems = /* @__PURE__ */ new Set(); | ||
| this._successCount = 0; | ||
| this._errorCount = 0; | ||
| this._settledCount = 0; | ||
| this._rejectionCount = 0; | ||
| this._expirationCount = 0; | ||
| this._items = []; | ||
| this._itemTimestamps = []; | ||
| this._pendingTick = false; | ||
| this._options = { | ||
| ...defaultOptions, | ||
| ...initialOptions, | ||
| throwOnError: initialOptions.throwOnError ?? !initialOptions.onError | ||
| this.store = new store.Store(getDefaultAsyncQueuerState()); | ||
| this.#timeoutIds = /* @__PURE__ */ new Set(); | ||
| this.setOptions = (newOptions) => { | ||
| this.options = { ...this.options, ...newOptions }; | ||
| }; | ||
| this._running = this._options.started; | ||
| for (let i = 0; i < this._options.initialItems.length; i++) { | ||
| const item = this._options.initialItems[i]; | ||
| const isLast = i === this._options.initialItems.length - 1; | ||
| this.addItem(item, this._options.addItemsTo, isLast); | ||
| } | ||
| } | ||
| /** | ||
| * Updates the queuer options. New options are merged with existing options. | ||
| */ | ||
| setOptions(newOptions) { | ||
| this._options = { ...this._options, ...newOptions }; | ||
| } | ||
| /** | ||
| * Returns the current queuer options, including defaults and any overrides. | ||
| */ | ||
| getOptions() { | ||
| return this._options; | ||
| } | ||
| /** | ||
| * Returns the current wait time (in milliseconds) between processing items. | ||
| * If a function is provided, it is called with the queuer instance. | ||
| */ | ||
| getWait() { | ||
| return utils.parseFunctionOrValue(this._options.wait, this); | ||
| } | ||
| /** | ||
| * Returns the current concurrency limit for processing items. | ||
| * If a function is provided, it is called with the queuer instance. | ||
| */ | ||
| getConcurrency() { | ||
| return utils.parseFunctionOrValue(this._options.concurrency, this); | ||
| } | ||
| /** | ||
| * Processes items in the queue up to the concurrency limit. Internal use only. | ||
| */ | ||
| tick() { | ||
| var _a, _b; | ||
| if (!this._running) { | ||
| this._pendingTick = false; | ||
| return; | ||
| } | ||
| this.checkExpiredItems(); | ||
| while (this._activeItems.size < this.getConcurrency() && !this.getIsEmpty()) { | ||
| const nextItem = this.peekNextItem(); | ||
| if (!nextItem) { | ||
| break; | ||
| this.#setState = (newState) => { | ||
| this.store.setState((state) => { | ||
| const combinedState = { | ||
| ...state, | ||
| ...newState | ||
| }; | ||
| const { activeItems, items, isRunning } = combinedState; | ||
| const size = items.length; | ||
| const isFull = size >= (this.options.maxSize ?? Infinity); | ||
| const isEmpty = size === 0; | ||
| const isIdle = isRunning && isEmpty && activeItems.length === 0; | ||
| const status = isIdle ? "idle" : isRunning ? "running" : "stopped"; | ||
| return { | ||
| ...combinedState, | ||
| isEmpty, | ||
| isFull, | ||
| isIdle, | ||
| size, | ||
| status | ||
| }; | ||
| }); | ||
| }; | ||
| this.#getWait = () => { | ||
| return utils.parseFunctionOrValue(this.options.wait ?? 0, this); | ||
| }; | ||
| this.#getConcurrency = () => { | ||
| return utils.parseFunctionOrValue(this.options.concurrency ?? 1, this); | ||
| }; | ||
| this.#tick = () => { | ||
| if (!this.store.state.isRunning) { | ||
| this.#setState({ pendingTick: false }); | ||
| return; | ||
| } | ||
| this._activeItems.add(nextItem); | ||
| (_b = (_a = this._options).onItemsChange) == null ? void 0 : _b.call(_a, this); | ||
| (async () => { | ||
| this._lastResult = await this.execute(); | ||
| const wait = this.getWait(); | ||
| if (wait > 0) { | ||
| setTimeout(() => this.tick(), wait); | ||
| return; | ||
| this.#setState({ pendingTick: true }); | ||
| this.#checkExpiredItems(); | ||
| const activeItems = this.store.state.activeItems; | ||
| while (activeItems.length < this.#getConcurrency() && !this.store.state.isEmpty) { | ||
| const nextItem = this.peekNextItem(); | ||
| if (!nextItem) { | ||
| break; | ||
| } | ||
| this.tick(); | ||
| })(); | ||
| } | ||
| this._pendingTick = false; | ||
| } | ||
| /** | ||
| * Starts processing items in the queue. If already running, does nothing. | ||
| */ | ||
| start() { | ||
| var _a, _b; | ||
| this._running = true; | ||
| if (!this._pendingTick && !this.getIsEmpty()) { | ||
| this._pendingTick = true; | ||
| this.tick(); | ||
| } | ||
| (_b = (_a = this._options).onIsRunningChange) == null ? void 0 : _b.call(_a, this); | ||
| } | ||
| /** | ||
| * Stops processing items in the queue. Does not clear the queue. | ||
| */ | ||
| stop() { | ||
| var _a, _b; | ||
| this._running = false; | ||
| this._pendingTick = false; | ||
| (_b = (_a = this._options).onIsRunningChange) == null ? void 0 : _b.call(_a, this); | ||
| } | ||
| /** | ||
| * Removes all pending items from the queue. Does not affect active tasks. | ||
| */ | ||
| clear() { | ||
| var _a, _b; | ||
| this._items = []; | ||
| (_b = (_a = this._options).onItemsChange) == null ? void 0 : _b.call(_a, this); | ||
| } | ||
| /** | ||
| * Resets the queuer to its initial state. Optionally repopulates with initial items. | ||
| * Does not affect callbacks or options. | ||
| */ | ||
| reset(withInitialItems) { | ||
| this.clear(); | ||
| this._successCount = 0; | ||
| this._errorCount = 0; | ||
| this._settledCount = 0; | ||
| if (withInitialItems) { | ||
| this._items = [...this._options.initialItems]; | ||
| } | ||
| this._running = this._options.started; | ||
| } | ||
| /** | ||
| * Adds an item to the queue. If the queue is full, the item is rejected and onReject is called. | ||
| * Items can be inserted based on priority or at the front/back depending on configuration. | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * queuer.addItem({ value: 'task', priority: 10 }); | ||
| * queuer.addItem('task2', 'front'); | ||
| * ``` | ||
| */ | ||
| addItem(item, position = this._options.addItemsTo, runOnItemsChange = true) { | ||
| var _a, _b, _c, _d; | ||
| if (this.getIsFull()) { | ||
| this._rejectionCount++; | ||
| (_b = (_a = this._options).onReject) == null ? void 0 : _b.call(_a, item, this); | ||
| return; | ||
| } | ||
| const priority = this._options.getPriority !== defaultOptions.getPriority ? this._options.getPriority(item) : item.priority; | ||
| if (priority !== void 0) { | ||
| const insertIndex = this._items.findIndex((existing) => { | ||
| const existingPriority = this._options.getPriority !== defaultOptions.getPriority ? this._options.getPriority(existing) : existing.priority; | ||
| return existingPriority < priority; | ||
| }); | ||
| if (insertIndex === -1) { | ||
| this._items.push(item); | ||
| this._itemTimestamps.push(Date.now()); | ||
| activeItems.push(nextItem); | ||
| this.#setState({ | ||
| activeItems | ||
| }); | ||
| (async () => { | ||
| const result = await this.execute(); | ||
| this.#setState({ lastResult: result }); | ||
| const wait = this.#getWait(); | ||
| if (wait > 0) { | ||
| const timeoutId = setTimeout(() => this.#tick(), wait); | ||
| this.#timeoutIds.add(timeoutId); | ||
| return; | ||
| } | ||
| this.#tick(); | ||
| })(); | ||
| } | ||
| this.#setState({ pendingTick: false }); | ||
| }; | ||
| this.addItem = (item, position = this.options.addItemsTo ?? "back", runOnItemsChange = true) => { | ||
| if (this.store.state.isFull) { | ||
| this.#setState({ | ||
| rejectionCount: this.store.state.rejectionCount + 1 | ||
| }); | ||
| this.options.onReject?.(item, this); | ||
| return false; | ||
| } | ||
| const priority = this.options.getPriority !== defaultOptions.getPriority ? this.options.getPriority(item) : item.priority; | ||
| const items = this.store.state.items; | ||
| const itemTimestamps = this.store.state.itemTimestamps; | ||
| if (priority !== void 0) { | ||
| const insertIndex = items.findIndex((existing) => { | ||
| const existingPriority = this.options.getPriority !== defaultOptions.getPriority ? this.options.getPriority(existing) : existing.priority; | ||
| return existingPriority < priority; | ||
| }); | ||
| if (insertIndex === -1) { | ||
| items.push(item); | ||
| itemTimestamps.push(Date.now()); | ||
| } else { | ||
| items.splice(insertIndex, 0, item); | ||
| itemTimestamps.splice(insertIndex, 0, Date.now()); | ||
| } | ||
| } else { | ||
| this._items.splice(insertIndex, 0, item); | ||
| this._itemTimestamps.splice(insertIndex, 0, Date.now()); | ||
| if (position === "front") { | ||
| items.unshift(item); | ||
| itemTimestamps.unshift(Date.now()); | ||
| } else { | ||
| items.push(item); | ||
| itemTimestamps.push(Date.now()); | ||
| } | ||
| } | ||
| } else { | ||
| this.#setState({ | ||
| items, | ||
| itemTimestamps | ||
| }); | ||
| if (runOnItemsChange) { | ||
| this.options.onItemsChange?.(this); | ||
| } | ||
| if (this.store.state.isRunning && !this.store.state.pendingTick) { | ||
| this.#tick(); | ||
| } | ||
| return true; | ||
| }; | ||
| this.getNextItem = (position = this.options.getItemsFrom ?? "front") => { | ||
| const { items, itemTimestamps } = this.store.state; | ||
| let item; | ||
| if (position === "front") { | ||
| this._items.unshift(item); | ||
| this._itemTimestamps.unshift(Date.now()); | ||
| item = items[0]; | ||
| if (item !== void 0) { | ||
| this.#setState({ | ||
| items: items.slice(1), | ||
| itemTimestamps: itemTimestamps.slice(1) | ||
| }); | ||
| } | ||
| } else { | ||
| this._items.push(item); | ||
| this._itemTimestamps.push(Date.now()); | ||
| item = items[items.length - 1]; | ||
| if (item !== void 0) { | ||
| this.#setState({ | ||
| items: items.slice(0, -1), | ||
| itemTimestamps: itemTimestamps.slice(0, -1) | ||
| }); | ||
| } | ||
| } | ||
| } | ||
| if (runOnItemsChange) { | ||
| (_d = (_c = this._options).onItemsChange) == null ? void 0 : _d.call(_c, this); | ||
| } | ||
| if (this._running && !this._pendingTick) { | ||
| this._pendingTick = true; | ||
| this.tick(); | ||
| } | ||
| } | ||
| /** | ||
| * Removes and returns the next item from the queue without executing the task function. | ||
| * Use for manual queue management. Normally, use execute() to process items. | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * // FIFO | ||
| * queuer.getNextItem(); | ||
| * // LIFO | ||
| * queuer.getNextItem('back'); | ||
| * ``` | ||
| */ | ||
| getNextItem(position = this._options.getItemsFrom) { | ||
| var _a, _b; | ||
| let item; | ||
| if (position === "front") { | ||
| item = this._items.shift(); | ||
| this._itemTimestamps.shift(); | ||
| } else { | ||
| item = this._items.pop(); | ||
| this._itemTimestamps.pop(); | ||
| } | ||
| if (item !== void 0) { | ||
| (_b = (_a = this._options).onItemsChange) == null ? void 0 : _b.call(_a, this); | ||
| } | ||
| return item; | ||
| } | ||
| /** | ||
| * Removes and returns the next item from the queue and executes the task function with it. | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * queuer.execute(); | ||
| * // LIFO | ||
| * queuer.execute('back'); | ||
| * ``` | ||
| */ | ||
| async execute(position) { | ||
| var _a, _b, _c, _d, _e, _f, _g, _h; | ||
| const item = this.getNextItem(position); | ||
| if (item !== void 0) { | ||
| try { | ||
| this._lastResult = await this.fn(item); | ||
| this._successCount++; | ||
| (_b = (_a = this._options).onSuccess) == null ? void 0 : _b.call(_a, this._lastResult, this); | ||
| } catch (error) { | ||
| this._errorCount++; | ||
| (_d = (_c = this._options).onError) == null ? void 0 : _d.call(_c, error, this); | ||
| if (this._options.throwOnError) { | ||
| throw error; | ||
| if (item !== void 0) { | ||
| this.options.onItemsChange?.(this); | ||
| } | ||
| return item; | ||
| }; | ||
| this.execute = async (position) => { | ||
| const item = this.getNextItem(position); | ||
| if (item !== void 0) { | ||
| try { | ||
| const lastResult = await this.fn(item); | ||
| this.#setState({ | ||
| successCount: this.store.state.successCount + 1, | ||
| lastResult | ||
| }); | ||
| this.options.onSuccess?.(lastResult, this); | ||
| } catch (error) { | ||
| this.#setState({ | ||
| errorCount: this.store.state.errorCount + 1 | ||
| }); | ||
| this.options.onError?.(error, this); | ||
| if (this.options.throwOnError) { | ||
| throw error; | ||
| } | ||
| } finally { | ||
| this.#setState({ | ||
| activeItems: this.store.state.activeItems.filter( | ||
| (activeItem) => activeItem !== item | ||
| ), | ||
| settledCount: this.store.state.settledCount + 1 | ||
| }); | ||
| this.options.onSettled?.(this); | ||
| } | ||
| } finally { | ||
| this._settledCount++; | ||
| this._activeItems.delete(item); | ||
| (_f = (_e = this._options).onItemsChange) == null ? void 0 : _f.call(_e, this); | ||
| (_h = (_g = this._options).onSettled) == null ? void 0 : _h.call(_g, this); | ||
| } | ||
| } | ||
| return item; | ||
| } | ||
| /** | ||
| * Checks for expired items in the queue and removes them. Calls onExpire for each expired item. | ||
| * Internal use only. | ||
| */ | ||
| checkExpiredItems() { | ||
| var _a, _b, _c, _d; | ||
| if (this._options.expirationDuration === Infinity && this._options.getIsExpired === defaultOptions.getIsExpired) | ||
| return; | ||
| const now = Date.now(); | ||
| const expiredIndices = []; | ||
| for (let i = 0; i < this._items.length; i++) { | ||
| const timestamp = this._itemTimestamps[i]; | ||
| if (timestamp === void 0) continue; | ||
| const item = this._items[i]; | ||
| if (item === void 0) continue; | ||
| const isExpired = this._options.getIsExpired !== defaultOptions.getIsExpired ? this._options.getIsExpired(item, timestamp) : now - timestamp > this._options.expirationDuration; | ||
| if (isExpired) { | ||
| expiredIndices.push(i); | ||
| return item; | ||
| }; | ||
| this.flush = (numberOfItems = this.store.state.items.length, position) => { | ||
| this.#clearTimeouts(); | ||
| for (let i = 0; i < numberOfItems; i++) { | ||
| this.execute(position); | ||
| } | ||
| }; | ||
| this.#checkExpiredItems = () => { | ||
| if ((this.options.expirationDuration ?? Infinity) === Infinity && this.options.getIsExpired === defaultOptions.getIsExpired) { | ||
| return; | ||
| } | ||
| const now = Date.now(); | ||
| const expiredIndices = []; | ||
| for (let i = 0; i < this.store.state.size; i++) { | ||
| const timestamp = this.store.state.itemTimestamps[i]; | ||
| if (timestamp === void 0) continue; | ||
| const item = this.store.state.items[i]; | ||
| if (item === void 0) continue; | ||
| const isExpired = this.options.getIsExpired !== defaultOptions.getIsExpired ? this.options.getIsExpired(item, timestamp) : now - timestamp > (this.options.expirationDuration ?? Infinity); | ||
| if (isExpired) { | ||
| expiredIndices.push(i); | ||
| } | ||
| } | ||
| for (let i = expiredIndices.length - 1; i >= 0; i--) { | ||
| const index = expiredIndices[i]; | ||
| if (index === void 0) continue; | ||
| const expiredItem = this.store.state.items[index]; | ||
| if (expiredItem === void 0) continue; | ||
| const newItems = [...this.store.state.items]; | ||
| const newTimestamps = [...this.store.state.itemTimestamps]; | ||
| newItems.splice(index, 1); | ||
| newTimestamps.splice(index, 1); | ||
| this.#setState({ | ||
| items: newItems, | ||
| itemTimestamps: newTimestamps, | ||
| expirationCount: this.store.state.expirationCount + 1 | ||
| }); | ||
| this.options.onExpire?.(expiredItem, this); | ||
| } | ||
| if (expiredIndices.length > 0) { | ||
| this.options.onItemsChange?.(this); | ||
| } | ||
| }; | ||
| this.peekNextItem = (position = "front") => { | ||
| if (position === "front") { | ||
| return this.store.state.items[0]; | ||
| } | ||
| return this.store.state.items[this.store.state.size - 1]; | ||
| }; | ||
| this.peekAllItems = () => { | ||
| return [...this.peekActiveItems(), ...this.peekPendingItems()]; | ||
| }; | ||
| this.peekActiveItems = () => { | ||
| return [...this.store.state.activeItems]; | ||
| }; | ||
| this.peekPendingItems = () => { | ||
| return [...this.store.state.items]; | ||
| }; | ||
| this.start = () => { | ||
| this.#setState({ isRunning: true }); | ||
| if (!this.store.state.pendingTick && !this.store.state.isEmpty) { | ||
| this.#tick(); | ||
| } | ||
| }; | ||
| this.stop = () => { | ||
| this.#clearTimeouts(); | ||
| this.#setState({ isRunning: false, pendingTick: false }); | ||
| }; | ||
| this.#clearTimeouts = () => { | ||
| this.#timeoutIds.forEach((timeoutId) => clearTimeout(timeoutId)); | ||
| this.#timeoutIds.clear(); | ||
| }; | ||
| this.clear = () => { | ||
| this.#setState({ items: [], itemTimestamps: [] }); | ||
| this.options.onItemsChange?.(this); | ||
| }; | ||
| this.reset = () => { | ||
| this.#setState(getDefaultAsyncQueuerState()); | ||
| this.options.onItemsChange?.(this); | ||
| }; | ||
| this.options = { | ||
| ...defaultOptions, | ||
| ...initialOptions, | ||
| throwOnError: initialOptions.throwOnError ?? !initialOptions.onError | ||
| }; | ||
| const isInitiallyRunning = this.options.initialState?.isRunning ?? this.options.started ?? true; | ||
| this.#setState({ | ||
| ...this.options.initialState, | ||
| isRunning: isInitiallyRunning | ||
| }); | ||
| if (this.options.initialState?.items) { | ||
| if (this.store.state.isRunning) { | ||
| this.#tick(); | ||
| } | ||
| } else { | ||
| for (let i = 0; i < (this.options.initialItems?.length ?? 0); i++) { | ||
| const item = this.options.initialItems[i]; | ||
| const isLast = i === (this.options.initialItems?.length ?? 0) - 1; | ||
| this.addItem(item, this.options.addItemsTo ?? "back", isLast); | ||
| } | ||
| } | ||
| for (let i = expiredIndices.length - 1; i >= 0; i--) { | ||
| const index = expiredIndices[i]; | ||
| if (index === void 0) continue; | ||
| const expiredItem = this._items[index]; | ||
| if (expiredItem === void 0) continue; | ||
| this._items.splice(index, 1); | ||
| this._itemTimestamps.splice(index, 1); | ||
| this._expirationCount++; | ||
| (_b = (_a = this._options).onExpire) == null ? void 0 : _b.call(_a, expiredItem, this); | ||
| } | ||
| if (expiredIndices.length > 0) { | ||
| (_d = (_c = this._options).onItemsChange) == null ? void 0 : _d.call(_c, this); | ||
| } | ||
| } | ||
| /** | ||
| * Returns the next item in the queue without removing it. | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * queuer.peekNextItem(); // front | ||
| * queuer.peekNextItem('back'); // back | ||
| * ``` | ||
| */ | ||
| peekNextItem(position = "front") { | ||
| if (position === "front") { | ||
| return this._items[0]; | ||
| } | ||
| return this._items[this._items.length - 1]; | ||
| } | ||
| /** | ||
| * Returns true if the queue is empty (no pending items). | ||
| */ | ||
| getIsEmpty() { | ||
| return this._items.length === 0; | ||
| } | ||
| /** | ||
| * Returns true if the queue is full (reached maxSize). | ||
| */ | ||
| getIsFull() { | ||
| return this._items.length >= this._options.maxSize; | ||
| } | ||
| /** | ||
| * Returns the number of pending items in the queue. | ||
| */ | ||
| getSize() { | ||
| return this._items.length; | ||
| } | ||
| /** | ||
| * Returns a copy of all items in the queue, including active and pending items. | ||
| */ | ||
| peekAllItems() { | ||
| return [...this.peekActiveItems(), ...this.peekPendingItems()]; | ||
| } | ||
| /** | ||
| * Returns the items currently being processed (active tasks). | ||
| */ | ||
| peekActiveItems() { | ||
| return Array.from(this._activeItems); | ||
| } | ||
| /** | ||
| * Returns the items waiting to be processed (pending tasks). | ||
| */ | ||
| peekPendingItems() { | ||
| return [...this._items]; | ||
| } | ||
| /** | ||
| * Returns the number of items that have been successfully processed. | ||
| */ | ||
| getSuccessCount() { | ||
| return this._successCount; | ||
| } | ||
| /** | ||
| * Returns the number of items that have failed processing. | ||
| */ | ||
| getErrorCount() { | ||
| return this._errorCount; | ||
| } | ||
| /** | ||
| * Returns the number of items that have completed processing (success or error). | ||
| */ | ||
| getSettledCount() { | ||
| return this._settledCount; | ||
| } | ||
| /** | ||
| * Returns the number of items that have been rejected from being added to the queue. | ||
| */ | ||
| getRejectionCount() { | ||
| return this._rejectionCount; | ||
| } | ||
| /** | ||
| * Returns true if the queuer is currently running (processing items). | ||
| */ | ||
| getIsRunning() { | ||
| return this._running; | ||
| } | ||
| /** | ||
| * Returns true if the queuer is running but has no items to process and no active tasks. | ||
| */ | ||
| getIsIdle() { | ||
| return this._running && this.getIsEmpty() && this._activeItems.size === 0; | ||
| } | ||
| /** | ||
| * Returns the number of items that have expired and been removed from the queue. | ||
| */ | ||
| getExpirationCount() { | ||
| return this._expirationCount; | ||
| } | ||
| #timeoutIds; | ||
| #setState; | ||
| #getWait; | ||
| #getConcurrency; | ||
| #tick; | ||
| #checkExpiredItems; | ||
| #clearTimeouts; | ||
| } | ||
| function asyncQueue(fn, initialOptions) { | ||
| const asyncQueuer = new AsyncQueuer(fn, initialOptions); | ||
| return asyncQueuer.addItem.bind(asyncQueuer); | ||
| return asyncQueuer.addItem; | ||
| } | ||
@@ -379,0 +318,0 @@ exports.AsyncQueuer = AsyncQueuer; |
@@ -1,1 +0,1 @@ | ||
| {"version":3,"file":"async-queuer.cjs","sources":["../../src/async-queuer.ts"],"sourcesContent":["import { parseFunctionOrValue } from './utils'\nimport type { OptionalKeys } from './types'\nimport type { QueuePosition } from './queuer'\n\nexport interface AsyncQueuerOptions<TValue> {\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 * 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: unknown, 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 the queuer's running state changes\n */\n onIsRunningChange?: (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?: (queuer: AsyncQueuer<TValue>) => void\n /**\n * Optional callback to call when a task succeeds\n */\n onSuccess?: (result: 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\ntype AsyncQueuerOptionsWithOptionalCallbacks = OptionalKeys<\n Required<AsyncQueuerOptions<any>>,\n | 'throwOnError'\n | 'onSuccess'\n | 'onSettled'\n | 'onReject'\n | 'onItemsChange'\n | 'onIsRunningChange'\n | 'onExpire'\n | 'onError'\n>\n\nconst defaultOptions: AsyncQueuerOptionsWithOptionalCallbacks = {\n addItemsTo: 'back',\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 * 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 * - Task cancellation\n * - Item expiration to remove stale items from the queue\n *\n * Tasks are processed concurrently up to the configured concurrency limit. When a task completes,\n * the next pending task is processed if the concurrency limit allows.\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 * 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 private _options: AsyncQueuerOptionsWithOptionalCallbacks\n private _activeItems: Set<TValue> = new Set()\n private _successCount = 0\n private _errorCount = 0\n private _settledCount = 0\n private _rejectionCount = 0\n private _expirationCount = 0\n private _items: Array<TValue> = []\n private _itemTimestamps: Array<number> = []\n private _pendingTick = false\n private _running: boolean\n private _lastResult: any\n\n constructor(\n private fn: (value: TValue) => Promise<any>,\n initialOptions: AsyncQueuerOptions<TValue>,\n ) {\n this._options = {\n ...defaultOptions,\n ...initialOptions,\n throwOnError: initialOptions.throwOnError ?? !initialOptions.onError,\n }\n this._running = this._options.started\n\n for (let i = 0; i < this._options.initialItems.length; i++) {\n const item = this._options.initialItems[i]!\n const isLast = i === this._options.initialItems.length - 1\n this.addItem(item, this._options.addItemsTo, isLast)\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 /**\n * Returns the current queuer options, including defaults and any overrides.\n */\n getOptions(): AsyncQueuerOptions<TValue> {\n return this._options\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, 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, this)\n }\n\n /**\n * Processes items in the queue up to the concurrency limit. Internal use only.\n */\n private tick() {\n if (!this._running) {\n this._pendingTick = false\n return\n }\n\n // Check for expired items\n this.checkExpiredItems()\n\n // Process items concurrently up to the concurrency limit\n while (\n this._activeItems.size < this.getConcurrency() &&\n !this.getIsEmpty()\n ) {\n const nextItem = this.peekNextItem()\n if (!nextItem) {\n break\n }\n this._activeItems.add(nextItem)\n this._options.onItemsChange?.(this)\n ;(async () => {\n this._lastResult = await this.execute()\n\n const wait = this.getWait()\n if (wait > 0) {\n setTimeout(() => this.tick(), wait)\n return\n }\n\n this.tick()\n })()\n }\n\n this._pendingTick = false\n }\n\n /**\n * Starts processing items in the queue. If already running, does nothing.\n */\n start(): void {\n this._running = true\n if (!this._pendingTick && !this.getIsEmpty()) {\n this._pendingTick = true\n this.tick()\n }\n this._options.onIsRunningChange?.(this)\n }\n\n /**\n * Stops processing items in the queue. Does not clear the queue.\n */\n stop(): void {\n this._running = false\n this._pendingTick = false\n this._options.onIsRunningChange?.(this)\n }\n\n /**\n * Removes all pending items from the queue. Does not affect active tasks.\n */\n clear(): void {\n this._items = []\n this._options.onItemsChange?.(this)\n }\n\n /**\n * Resets the queuer to its initial state. Optionally repopulates with initial items.\n * Does not affect callbacks or options.\n */\n reset(withInitialItems?: boolean): void {\n this.clear()\n this._successCount = 0\n this._errorCount = 0\n this._settledCount = 0\n if (withInitialItems) {\n this._items = [...this._options.initialItems]\n }\n this._running = this._options.started\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 & { priority?: number },\n position: QueuePosition = this._options.addItemsTo,\n runOnItemsChange: boolean = true,\n ): void {\n if (this.getIsFull()) {\n this._rejectionCount++\n this._options.onReject?.(item, this)\n return\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.priority\n\n if (priority !== undefined) {\n // Insert based on priority - higher priority items go to front\n const insertIndex = this._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 this._items.push(item)\n this._itemTimestamps.push(Date.now())\n } else {\n this._items.splice(insertIndex, 0, item)\n this._itemTimestamps.splice(insertIndex, 0, Date.now())\n }\n } else {\n if (position === 'front') {\n // Default FIFO/LIFO behavior\n this._items.unshift(item)\n this._itemTimestamps.unshift(Date.now())\n } else {\n // LIFO\n this._items.push(item)\n this._itemTimestamps.push(Date.now())\n }\n }\n\n if (runOnItemsChange) {\n this._options.onItemsChange?.(this)\n }\n\n if (this._running && !this._pendingTick) {\n this._pendingTick = true\n this.tick()\n }\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,\n ): TValue | undefined {\n let item: TValue | undefined\n\n if (position === 'front') {\n item = this._items.shift()\n this._itemTimestamps.shift()\n } else {\n item = this._items.pop()\n this._itemTimestamps.pop()\n }\n\n if (item !== undefined) {\n this._options.onItemsChange?.(this)\n }\n\n return item\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 async execute(position?: QueuePosition): Promise<any> {\n const item = this.getNextItem(position)\n if (item !== undefined) {\n try {\n this._lastResult = await this.fn(item)\n this._successCount++\n this._options.onSuccess?.(this._lastResult, this)\n } catch (error) {\n this._errorCount++\n this._options.onError?.(error, this)\n if (this._options.throwOnError) {\n throw error\n }\n } finally {\n this._settledCount++\n this._activeItems.delete(item)\n this._options.onItemsChange?.(this)\n this._options.onSettled?.(this)\n }\n }\n return item\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 private checkExpiredItems(): void {\n if (\n this._options.expirationDuration === Infinity &&\n this._options.getIsExpired === defaultOptions.getIsExpired\n )\n return\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._items.length; i++) {\n const timestamp = this._itemTimestamps[i]\n if (timestamp === undefined) continue\n\n const item = this._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\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._items[index]\n if (expiredItem === undefined) continue\n\n this._items.splice(index, 1)\n this._itemTimestamps.splice(index, 1)\n this._expirationCount++\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._items[0]\n }\n return this._items[this._items.length - 1]\n }\n\n /**\n * Returns true if the queue is empty (no pending items).\n */\n getIsEmpty(): boolean {\n return this._items.length === 0\n }\n\n /**\n * Returns true if the queue is full (reached maxSize).\n */\n getIsFull(): boolean {\n return this._items.length >= this._options.maxSize\n }\n\n /**\n * Returns the number of pending items in the queue.\n */\n getSize(): number {\n return this._items.length\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 Array.from(this._activeItems)\n }\n\n /**\n * Returns the items waiting to be processed (pending tasks).\n */\n peekPendingItems(): Array<TValue> {\n return [...this._items]\n }\n\n /**\n * Returns the number of items that have been successfully processed.\n */\n getSuccessCount(): number {\n return this._successCount\n }\n\n /**\n * Returns the number of items that have failed processing.\n */\n getErrorCount(): number {\n return this._errorCount\n }\n\n /**\n * Returns the number of items that have completed processing (success or error).\n */\n getSettledCount(): number {\n return this._settledCount\n }\n\n /**\n * Returns the number of items that have been rejected from being added to the queue.\n */\n getRejectionCount(): number {\n return this._rejectionCount\n }\n\n /**\n * Returns true if the queuer is currently running (processing items).\n */\n getIsRunning(): boolean {\n return this._running\n }\n\n /**\n * Returns true if the queuer is running but has no items to process and no active tasks.\n */\n getIsIdle(): boolean {\n return this._running && this.getIsEmpty() && this._activeItems.size === 0\n }\n\n /**\n * Returns the number of items that have expired and been removed from the queue.\n */\n getExpirationCount(): number {\n return this._expirationCount\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 * 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 * Example usage:\n * ```ts\n * const enqueue = asyncQueue<string>(async (item) => {\n * return item.toUpperCase();\n * }, {...options});\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.bind(asyncQueuer)\n}\n"],"names":["parseFunctionOrValue"],"mappings":"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"} | ||
| {"version":3,"file":"async-queuer.cjs","sources":["../../src/async-queuer.ts"],"sourcesContent":["import { Store } from '@tanstack/store'\nimport { parseFunctionOrValue } from './utils'\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 task executions that have resulted in errors\n */\n errorCount: 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 * Timestamps when items were added to the queue for expiration tracking\n */\n itemTimestamps: Array<number>\n /**\n * Array of items currently waiting to be processed\n */\n items: Array<TValue>\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 structuredClone({\n activeItems: [],\n errorCount: 0,\n expirationCount: 0,\n isEmpty: true,\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 * 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 * 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: unknown, 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?: (queuer: AsyncQueuer<TValue>) => void\n /**\n * Optional callback to call when a task succeeds\n */\n onSuccess?: (result: 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\ntype AsyncQueuerOptionsWithOptionalCallbacks = OptionalKeys<\n Required<AsyncQueuerOptions<any>>,\n | 'initialState'\n | 'throwOnError'\n | 'onSuccess'\n | 'onSettled'\n | 'onReject'\n | 'onItemsChange'\n | 'onExpire'\n | 'onError'\n>\n\nconst defaultOptions: AsyncQueuerOptionsWithOptionalCallbacks = {\n addItemsTo: 'back',\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 * 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 * - Task cancellation\n * - Item expiration to remove stale items from the queue\n *\n * Tasks are processed concurrently up to the configured concurrency limit. When a task completes,\n * the next pending task is processed if the concurrency limit allows.\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 options: AsyncQueuerOptions<TValue>\n #timeoutIds: Set<NodeJS.Timeout> = new Set()\n\n constructor(\n private fn: (item: TValue) => Promise<any>,\n initialOptions: AsyncQueuerOptions<TValue> = {},\n ) {\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\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 }\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.isEmpty\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 const result = await this.execute()\n this.#setState({ lastResult: result })\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 if (this.store.state.isFull) {\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 if (position === 'front') {\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 /**\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 if (item !== undefined) {\n try {\n const lastResult = await this.fn(item)\n this.#setState({\n successCount: this.store.state.successCount + 1,\n lastResult,\n })\n this.options.onSuccess?.(lastResult, this)\n } catch (error) {\n this.#setState({\n errorCount: this.store.state.errorCount + 1,\n })\n this.options.onError?.(error, this)\n if (this.options.throwOnError) {\n throw error\n }\n } finally {\n this.#setState({\n activeItems: this.store.state.activeItems.filter(\n (activeItem) => activeItem !== item,\n ),\n settledCount: this.store.state.settledCount + 1,\n })\n this.options.onSettled?.(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 = (\n numberOfItems: number = this.store.state.items.length,\n position?: QueuePosition,\n ): void => {\n this.#clearTimeouts() // clear any pending timeouts\n for (let i = 0; i < numberOfItems; i++) {\n this.execute(position)\n }\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.size; 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.size - 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.isEmpty) {\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. Does not affect active tasks.\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(getDefaultAsyncQueuerState<TValue>())\n this.options.onItemsChange?.(this)\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 * 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 usage:\n * ```ts\n * const enqueue = asyncQueue<string>(async (item) => {\n * return item.toUpperCase();\n * }, {...options});\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"],"names":["Store","parseFunctionOrValue"],"mappings":"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"} |
+121
-100
@@ -0,2 +1,69 @@ | ||
| import { Store } from '@tanstack/store'; | ||
| import { QueuePosition } from './queuer.cjs'; | ||
| export interface AsyncQueuerState<TValue> { | ||
| /** | ||
| * Items currently being processed by the queuer | ||
| */ | ||
| activeItems: Array<TValue>; | ||
| /** | ||
| * Number of task executions that have resulted in errors | ||
| */ | ||
| errorCount: number; | ||
| /** | ||
| * Number of items that have been removed from the queue due to expiration | ||
| */ | ||
| expirationCount: number; | ||
| /** | ||
| * Whether the queuer has no items to process (items array is empty) | ||
| */ | ||
| isEmpty: boolean; | ||
| /** | ||
| * Whether the queuer has reached its maximum capacity | ||
| */ | ||
| isFull: boolean; | ||
| /** | ||
| * Whether the queuer is not currently processing any items | ||
| */ | ||
| isIdle: boolean; | ||
| /** | ||
| * Whether the queuer is active and will process items automatically | ||
| */ | ||
| isRunning: boolean; | ||
| /** | ||
| * Timestamps when items were added to the queue for expiration tracking | ||
| */ | ||
| itemTimestamps: Array<number>; | ||
| /** | ||
| * Array of items currently waiting to be processed | ||
| */ | ||
| items: Array<TValue>; | ||
| /** | ||
| * The result from the most recent task execution | ||
| */ | ||
| lastResult: any; | ||
| /** | ||
| * Whether the queuer has a pending timeout for processing the next item | ||
| */ | ||
| pendingTick: boolean; | ||
| /** | ||
| * Number of items that have been rejected from being added to the queue | ||
| */ | ||
| rejectionCount: number; | ||
| /** | ||
| * Number of task executions that have completed (either successfully or with errors) | ||
| */ | ||
| settledCount: number; | ||
| /** | ||
| * Number of items currently in the queue | ||
| */ | ||
| size: number; | ||
| /** | ||
| * Current processing status - 'idle' when not processing, 'running' when active, 'stopped' when paused | ||
| */ | ||
| status: 'idle' | 'running' | 'stopped'; | ||
| /** | ||
| * Number of task executions that have completed successfully | ||
| */ | ||
| successCount: number; | ||
| } | ||
| export interface AsyncQueuerOptions<TValue> { | ||
@@ -40,2 +107,6 @@ /** | ||
| /** | ||
| * Initial state for the async queuer | ||
| */ | ||
| initialState?: Partial<AsyncQueuerState<TValue>>; | ||
| /** | ||
| * Maximum number of items allowed in the queuer | ||
@@ -55,6 +126,2 @@ */ | ||
| /** | ||
| * Callback fired whenever the queuer's running state changes | ||
| */ | ||
| onIsRunningChange?: (queuer: AsyncQueuer<TValue>) => void; | ||
| /** | ||
| * Callback fired whenever an item is added or removed from the queuer | ||
@@ -114,2 +181,15 @@ */ | ||
| * | ||
| * State Management: | ||
| * - Uses TanStack Store for reactive state management | ||
| * - Use `initialState` to provide initial state values when creating the async queuer | ||
| * - Use `onSuccess` callback to react to successful task execution and implement custom logic | ||
| * - Use `onError` callback to react to task execution errors and implement custom error handling | ||
| * - Use `onSettled` callback to react to task execution completion (success or error) and implement custom logic | ||
| * - Use `onItemsChange` callback to react to items being added or removed from the queue | ||
| * - Use `onExpire` callback to react to items expiring and implement custom logic | ||
| * - Use `onReject` callback to react to items being rejected when the queue is full | ||
| * - The state includes error count, expiration count, rejection count, running status, and success/settle counts | ||
| * - State can be accessed via `asyncQueuer.store.state` when using the class directly | ||
| * - When using framework adapters (React/Solid), state is accessed from `asyncQueuer.state` | ||
| * | ||
| * Example usage: | ||
@@ -132,56 +212,12 @@ * ```ts | ||
| export declare class AsyncQueuer<TValue> { | ||
| #private; | ||
| private fn; | ||
| private _options; | ||
| private _activeItems; | ||
| private _successCount; | ||
| private _errorCount; | ||
| private _settledCount; | ||
| private _rejectionCount; | ||
| private _expirationCount; | ||
| private _items; | ||
| private _itemTimestamps; | ||
| private _pendingTick; | ||
| private _running; | ||
| private _lastResult; | ||
| constructor(fn: (value: TValue) => Promise<any>, initialOptions: AsyncQueuerOptions<TValue>); | ||
| readonly store: Store<Readonly<AsyncQueuerState<TValue>>>; | ||
| options: AsyncQueuerOptions<TValue>; | ||
| constructor(fn: (item: TValue) => Promise<any>, initialOptions?: AsyncQueuerOptions<TValue>); | ||
| /** | ||
| * Updates the queuer options. New options are merged with existing options. | ||
| */ | ||
| setOptions(newOptions: Partial<AsyncQueuerOptions<TValue>>): void; | ||
| setOptions: (newOptions: Partial<AsyncQueuerOptions<TValue>>) => void; | ||
| /** | ||
| * Returns the current queuer options, including defaults and any overrides. | ||
| */ | ||
| getOptions(): AsyncQueuerOptions<TValue>; | ||
| /** | ||
| * Returns the current wait time (in milliseconds) between processing items. | ||
| * If a function is provided, it is called with the queuer instance. | ||
| */ | ||
| getWait(): number; | ||
| /** | ||
| * Returns the current concurrency limit for processing items. | ||
| * If a function is provided, it is called with the queuer instance. | ||
| */ | ||
| getConcurrency(): number; | ||
| /** | ||
| * Processes items in the queue up to the concurrency limit. Internal use only. | ||
| */ | ||
| private tick; | ||
| /** | ||
| * Starts processing items in the queue. If already running, does nothing. | ||
| */ | ||
| start(): void; | ||
| /** | ||
| * Stops processing items in the queue. Does not clear the queue. | ||
| */ | ||
| stop(): void; | ||
| /** | ||
| * Removes all pending items from the queue. Does not affect active tasks. | ||
| */ | ||
| clear(): void; | ||
| /** | ||
| * Resets the queuer to its initial state. Optionally repopulates with initial items. | ||
| * Does not affect callbacks or options. | ||
| */ | ||
| reset(withInitialItems?: boolean): void; | ||
| /** | ||
| * Adds an item to the queue. If the queue is full, the item is rejected and onReject is called. | ||
@@ -196,5 +232,3 @@ * Items can be inserted based on priority or at the front/back depending on configuration. | ||
| */ | ||
| addItem(item: TValue & { | ||
| priority?: number; | ||
| }, position?: QueuePosition, runOnItemsChange?: boolean): void; | ||
| addItem: (item: TValue, position?: QueuePosition, runOnItemsChange?: boolean) => boolean; | ||
| /** | ||
@@ -212,3 +246,3 @@ * Removes and returns the next item from the queue without executing the task function. | ||
| */ | ||
| getNextItem(position?: QueuePosition): TValue | undefined; | ||
| getNextItem: (position?: QueuePosition) => TValue | undefined; | ||
| /** | ||
@@ -224,8 +258,8 @@ * Removes and returns the next item from the queue and executes the task function with it. | ||
| */ | ||
| execute(position?: QueuePosition): Promise<any>; | ||
| execute: (position?: QueuePosition) => Promise<any>; | ||
| /** | ||
| * Checks for expired items in the queue and removes them. Calls onExpire for each expired item. | ||
| * Internal use only. | ||
| * Processes a specified number of items to execute immediately with no wait time | ||
| * If no numberOfItems is provided, all items will be processed | ||
| */ | ||
| private checkExpiredItems; | ||
| flush: (numberOfItems?: number, position?: QueuePosition) => void; | ||
| /** | ||
@@ -240,55 +274,31 @@ * Returns the next item in the queue without removing it. | ||
| */ | ||
| peekNextItem(position?: QueuePosition): TValue | undefined; | ||
| peekNextItem: (position?: QueuePosition) => TValue | undefined; | ||
| /** | ||
| * Returns true if the queue is empty (no pending items). | ||
| */ | ||
| getIsEmpty(): boolean; | ||
| /** | ||
| * Returns true if the queue is full (reached maxSize). | ||
| */ | ||
| getIsFull(): boolean; | ||
| /** | ||
| * Returns the number of pending items in the queue. | ||
| */ | ||
| getSize(): number; | ||
| /** | ||
| * Returns a copy of all items in the queue, including active and pending items. | ||
| */ | ||
| peekAllItems(): Array<TValue>; | ||
| peekAllItems: () => Array<TValue>; | ||
| /** | ||
| * Returns the items currently being processed (active tasks). | ||
| */ | ||
| peekActiveItems(): Array<TValue>; | ||
| peekActiveItems: () => Array<TValue>; | ||
| /** | ||
| * Returns the items waiting to be processed (pending tasks). | ||
| */ | ||
| peekPendingItems(): Array<TValue>; | ||
| peekPendingItems: () => Array<TValue>; | ||
| /** | ||
| * Returns the number of items that have been successfully processed. | ||
| * Starts processing items in the queue. If already running, does nothing. | ||
| */ | ||
| getSuccessCount(): number; | ||
| start: () => void; | ||
| /** | ||
| * Returns the number of items that have failed processing. | ||
| * Stops processing items in the queue. Does not clear the queue. | ||
| */ | ||
| getErrorCount(): number; | ||
| stop: () => void; | ||
| /** | ||
| * Returns the number of items that have completed processing (success or error). | ||
| * Removes all pending items from the queue. Does not affect active tasks. | ||
| */ | ||
| getSettledCount(): number; | ||
| clear: () => void; | ||
| /** | ||
| * Returns the number of items that have been rejected from being added to the queue. | ||
| * Resets the queuer state to its default values | ||
| */ | ||
| getRejectionCount(): number; | ||
| /** | ||
| * Returns true if the queuer is currently running (processing items). | ||
| */ | ||
| getIsRunning(): boolean; | ||
| /** | ||
| * Returns true if the queuer is running but has no items to process and no active tasks. | ||
| */ | ||
| getIsIdle(): boolean; | ||
| /** | ||
| * Returns the number of items that have expired and been removed from the queue. | ||
| */ | ||
| getExpirationCount(): number; | ||
| reset: () => void; | ||
| } | ||
@@ -306,2 +316,15 @@ /** | ||
| * | ||
| * State Management: | ||
| * - Uses TanStack Store for reactive state management | ||
| * - Use `initialState` to provide initial state values when creating the async queuer | ||
| * - Use `onSuccess` callback to react to successful task execution and implement custom logic | ||
| * - Use `onError` callback to react to task execution errors and implement custom error handling | ||
| * - Use `onSettled` callback to react to task execution completion (success or error) and implement custom logic | ||
| * - Use `onItemsChange` callback to react to items being added or removed from the queue | ||
| * - Use `onExpire` callback to react to items expiring and implement custom logic | ||
| * - Use `onReject` callback to react to items being rejected when the queue is full | ||
| * - The state includes error count, expiration count, rejection count, running status, and success/settle counts | ||
| * - State can be accessed via the underlying AsyncQueuer instance's `store.state` property | ||
| * - When using framework adapters (React/Solid), state is accessed from the hook's state property | ||
| * | ||
| * Example usage: | ||
@@ -316,4 +339,2 @@ * ```ts | ||
| */ | ||
| export declare function asyncQueue<TValue>(fn: (value: TValue) => Promise<any>, initialOptions: AsyncQueuerOptions<TValue>): (item: TValue & { | ||
| priority?: number; | ||
| }, position?: QueuePosition, runOnItemsChange?: boolean) => void; | ||
| export declare function asyncQueue<TValue>(fn: (value: TValue) => Promise<any>, initialOptions: AsyncQueuerOptions<TValue>): (item: TValue, position?: QueuePosition, runOnItemsChange?: boolean) => boolean; |
+127
-184
| "use strict"; | ||
| Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" }); | ||
| const store = require("@tanstack/store"); | ||
| const utils = require("./utils.cjs"); | ||
| function getDefaultAsyncRateLimiterState() { | ||
| return { | ||
| errorCount: 0, | ||
| executionTimes: [], | ||
| isExecuting: false, | ||
| lastResult: void 0, | ||
| rejectionCount: 0, | ||
| settleCount: 0, | ||
| successCount: 0 | ||
| }; | ||
| } | ||
| const defaultOptions = { | ||
| enabled: true, | ||
| windowType: "fixed" | ||
| limit: 1, | ||
| window: 0, | ||
| windowType: "fixed", | ||
| throwOnError: true | ||
| }; | ||
@@ -11,195 +26,123 @@ class AsyncRateLimiter { | ||
| this.fn = fn; | ||
| this._errorCount = 0; | ||
| this._executionTimes = []; | ||
| this._rejectionCount = 0; | ||
| this._settleCount = 0; | ||
| this._successCount = 0; | ||
| this._isExecuting = false; | ||
| this._options = { | ||
| ...defaultOptions, | ||
| ...initialOptions, | ||
| throwOnError: initialOptions.throwOnError ?? !initialOptions.onError | ||
| this.store = new store.Store(getDefaultAsyncRateLimiterState()); | ||
| this.setOptions = (newOptions) => { | ||
| this.options = { ...this.options, ...newOptions }; | ||
| }; | ||
| } | ||
| /** | ||
| * Updates the rate limiter options | ||
| */ | ||
| setOptions(newOptions) { | ||
| this._options = { ...this._options, ...newOptions }; | ||
| } | ||
| /** | ||
| * Returns the current rate limiter options | ||
| */ | ||
| getOptions() { | ||
| return this._options; | ||
| } | ||
| /** | ||
| * Returns the current enabled state of the rate limiter | ||
| */ | ||
| getEnabled() { | ||
| return !!utils.parseFunctionOrValue(this._options.enabled, this); | ||
| } | ||
| /** | ||
| * Returns the current limit of executions allowed within the time window | ||
| */ | ||
| getLimit() { | ||
| return utils.parseFunctionOrValue(this._options.limit, this); | ||
| } | ||
| /** | ||
| * Returns the current time window in milliseconds | ||
| */ | ||
| getWindow() { | ||
| return utils.parseFunctionOrValue(this._options.window, this); | ||
| } | ||
| /** | ||
| * Attempts to execute the rate-limited function if within the configured limits. | ||
| * Will reject execution if the number of calls in the current window exceeds the limit. | ||
| * If execution is allowed, waits for any previous execution to complete before proceeding. | ||
| * | ||
| * Error Handling: | ||
| * - If the rate-limited function throws and no `onError` handler is configured, | ||
| * the error will be thrown from this method. | ||
| * - If an `onError` handler is configured, errors will be caught and passed to the handler, | ||
| * and this method will return undefined. | ||
| * - If the rate limit is exceeded, the execution will be rejected and the `onReject` handler | ||
| * will be called if configured. | ||
| * - The error state can be checked using `getErrorCount()` and `getIsExecuting()`. | ||
| * - Rate limit rejections can be tracked using `getRejectionCount()`. | ||
| * | ||
| * @returns A promise that resolves with the function's return value, or undefined if an error occurred and was handled by onError | ||
| * @throws The error from the rate-limited function if no onError handler is configured | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * const rateLimiter = new AsyncRateLimiter(fn, { limit: 5, window: 1000 }); | ||
| * | ||
| * // First 5 calls will execute | ||
| * await rateLimiter.maybeExecute('arg1', 'arg2'); | ||
| * | ||
| * // Additional calls within the window will be rejected | ||
| * await rateLimiter.maybeExecute('arg1', 'arg2'); // Rejected | ||
| * ``` | ||
| */ | ||
| async maybeExecute(...args) { | ||
| this.cleanupOldExecutions(); | ||
| const limit = this.getLimit(); | ||
| const window = this.getWindow(); | ||
| if (this._options.windowType === "sliding") { | ||
| if (this._executionTimes.length < limit) { | ||
| await this.execute(...args); | ||
| return this._lastResult; | ||
| this.#setState = (newState) => { | ||
| this.store.setState((state) => { | ||
| const combinedState = { | ||
| ...state, | ||
| ...newState | ||
| }; | ||
| return combinedState; | ||
| }); | ||
| }; | ||
| this.#getEnabled = () => { | ||
| return !!utils.parseFunctionOrValue(this.options.enabled, this); | ||
| }; | ||
| this.#getLimit = () => { | ||
| return utils.parseFunctionOrValue(this.options.limit, this); | ||
| }; | ||
| this.#getWindow = () => { | ||
| return utils.parseFunctionOrValue(this.options.window, this); | ||
| }; | ||
| this.maybeExecute = async (...args) => { | ||
| this.#cleanupOldExecutions(); | ||
| const relevantExecutionTimes = this.#getRelevantExecutionTimes(); | ||
| if (relevantExecutionTimes.length < this.#getLimit()) { | ||
| await this.#execute(...args); | ||
| return this.store.state.lastResult; | ||
| } | ||
| } else { | ||
| this.#setState({ | ||
| rejectionCount: this.store.state.rejectionCount + 1 | ||
| }); | ||
| this.options.onReject?.(this); | ||
| return void 0; | ||
| }; | ||
| this.#execute = async (...args) => { | ||
| if (!this.#getEnabled()) return; | ||
| const now = Date.now(); | ||
| const oldestExecution = Math.min(...this._executionTimes); | ||
| const isNewWindow = oldestExecution + window <= now; | ||
| if (isNewWindow || this._executionTimes.length < limit) { | ||
| await this.execute(...args); | ||
| return this._lastResult; | ||
| const executionTimes = [...this.store.state.executionTimes, now]; | ||
| this.#setState({ | ||
| isExecuting: true, | ||
| executionTimes | ||
| }); | ||
| try { | ||
| const result = await this.fn(...args); | ||
| this.#setState({ | ||
| successCount: this.store.state.successCount + 1, | ||
| lastResult: result | ||
| }); | ||
| this.options.onSuccess?.(result, this); | ||
| } catch (error) { | ||
| this.#setState({ | ||
| errorCount: this.store.state.errorCount + 1 | ||
| }); | ||
| this.options.onError?.(error, this); | ||
| if (this.options.throwOnError) { | ||
| throw error; | ||
| } | ||
| } finally { | ||
| this.#setState({ | ||
| isExecuting: false, | ||
| settleCount: this.store.state.settleCount + 1 | ||
| }); | ||
| this.options.onSettled?.(this); | ||
| } | ||
| } | ||
| this.rejectFunction(); | ||
| return void 0; | ||
| } | ||
| async execute(...args) { | ||
| var _a, _b, _c, _d, _e, _f; | ||
| if (!this.getEnabled()) return; | ||
| this._isExecuting = true; | ||
| const now = Date.now(); | ||
| this._executionTimes.push(now); | ||
| try { | ||
| this._lastResult = await this.fn(...args); | ||
| this._successCount++; | ||
| (_b = (_a = this._options).onSuccess) == null ? void 0 : _b.call(_a, this._lastResult, this); | ||
| } catch (error) { | ||
| this._errorCount++; | ||
| (_d = (_c = this._options).onError) == null ? void 0 : _d.call(_c, error, this); | ||
| if (this._options.throwOnError) { | ||
| throw error; | ||
| return this.store.state.lastResult; | ||
| }; | ||
| this.#getRelevantExecutionTimes = () => { | ||
| if (this.options.windowType === "sliding") { | ||
| return this.store.state.executionTimes.filter( | ||
| (time) => time > Date.now() - this.#getWindow() | ||
| ); | ||
| } else { | ||
| console.error(error); | ||
| const oldestExecution = Math.min(...this.store.state.executionTimes); | ||
| const windowStart = oldestExecution; | ||
| return this.store.state.executionTimes.filter( | ||
| (time) => time >= windowStart && time <= windowStart + this.#getWindow() | ||
| ); | ||
| } | ||
| } finally { | ||
| this._isExecuting = false; | ||
| this._settleCount++; | ||
| (_f = (_e = this._options).onSettled) == null ? void 0 : _f.call(_e, this); | ||
| } | ||
| return this._lastResult; | ||
| }; | ||
| this.#cleanupOldExecutions = () => { | ||
| const now = Date.now(); | ||
| const windowStart = now - this.#getWindow(); | ||
| this.#setState({ | ||
| executionTimes: this.store.state.executionTimes.filter( | ||
| (time) => time > windowStart | ||
| ) | ||
| }); | ||
| }; | ||
| this.getRemainingInWindow = () => { | ||
| const relevantExecutionTimes = this.#getRelevantExecutionTimes(); | ||
| return Math.max(0, this.#getLimit() - relevantExecutionTimes.length); | ||
| }; | ||
| this.getMsUntilNextWindow = () => { | ||
| if (this.getRemainingInWindow() > 0) { | ||
| return 0; | ||
| } | ||
| const oldestExecution = this.store.state.executionTimes[0] ?? Infinity; | ||
| return oldestExecution + this.#getWindow() - Date.now(); | ||
| }; | ||
| this.reset = () => { | ||
| this.#setState(getDefaultAsyncRateLimiterState()); | ||
| }; | ||
| this.options = { | ||
| ...defaultOptions, | ||
| ...initialOptions, | ||
| throwOnError: initialOptions.throwOnError ?? !initialOptions.onError | ||
| }; | ||
| this.#setState(this.options.initialState ?? {}); | ||
| } | ||
| rejectFunction() { | ||
| this._rejectionCount++; | ||
| if (this._options.onReject) { | ||
| this._options.onReject(this); | ||
| } | ||
| } | ||
| cleanupOldExecutions() { | ||
| const now = Date.now(); | ||
| const windowStart = now - this.getWindow(); | ||
| this._executionTimes = this._executionTimes.filter( | ||
| (time) => time > windowStart | ||
| ); | ||
| } | ||
| /** | ||
| * Returns the number of remaining executions allowed in the current window | ||
| */ | ||
| getRemainingInWindow() { | ||
| this.cleanupOldExecutions(); | ||
| return Math.max(0, this.getLimit() - this._executionTimes.length); | ||
| } | ||
| /** | ||
| * Returns the number of milliseconds until the next execution will be possible | ||
| * For fixed windows, this is the time until the current window resets | ||
| * For sliding windows, this is the time until the oldest execution expires | ||
| */ | ||
| getMsUntilNextWindow() { | ||
| if (this.getRemainingInWindow() > 0) { | ||
| return 0; | ||
| } | ||
| const oldestExecution = Math.min(...this._executionTimes); | ||
| return oldestExecution + this.getWindow() - Date.now(); | ||
| } | ||
| /** | ||
| * Returns the number of times the function has been executed | ||
| */ | ||
| getSuccessCount() { | ||
| return this._successCount; | ||
| } | ||
| /** | ||
| * Returns the number of times the function has been settled | ||
| */ | ||
| getSettleCount() { | ||
| return this._settleCount; | ||
| } | ||
| /** | ||
| * Returns the number of times the function has errored | ||
| */ | ||
| getErrorCount() { | ||
| return this._errorCount; | ||
| } | ||
| /** | ||
| * Returns the number of times the function has been rejected | ||
| */ | ||
| getRejectionCount() { | ||
| return this._rejectionCount; | ||
| } | ||
| /** | ||
| * Returns whether the function is currently executing | ||
| */ | ||
| getIsExecuting() { | ||
| return this._isExecuting; | ||
| } | ||
| /** | ||
| * Resets the rate limiter state | ||
| */ | ||
| reset() { | ||
| this._executionTimes = []; | ||
| this._successCount = 0; | ||
| this._errorCount = 0; | ||
| this._rejectionCount = 0; | ||
| this._settleCount = 0; | ||
| } | ||
| #setState; | ||
| #getEnabled; | ||
| #getLimit; | ||
| #getWindow; | ||
| #execute; | ||
| #getRelevantExecutionTimes; | ||
| #cleanupOldExecutions; | ||
| } | ||
| function asyncRateLimit(fn, initialOptions) { | ||
| const rateLimiter = new AsyncRateLimiter(fn, initialOptions); | ||
| return rateLimiter.maybeExecute.bind(rateLimiter); | ||
| return rateLimiter.maybeExecute; | ||
| } | ||
@@ -206,0 +149,0 @@ exports.AsyncRateLimiter = AsyncRateLimiter; |
@@ -1,1 +0,1 @@ | ||
| {"version":3,"file":"async-rate-limiter.cjs","sources":["../../src/async-rate-limiter.ts"],"sourcesContent":["import { parseFunctionOrValue } from './utils'\nimport type { AnyAsyncFunction, OptionalKeys } from './types'\n\n/**\n * Options for configuring an async rate-limited function\n */\nexport interface AsyncRateLimiterOptions<TFn extends AnyAsyncFunction> {\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 * 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?: (error: unknown, rateLimiter: AsyncRateLimiter<TFn>) => void\n /**\n * Optional callback function that is called when an execution is rejected due to rate limiting\n */\n onReject?: (rateLimiter: AsyncRateLimiter<TFn>) => void\n /**\n * Optional function to call when the rate-limited function is executed\n */\n onSettled?: (rateLimiter: AsyncRateLimiter<TFn>) => void\n /**\n * Optional function to call when the rate-limited function is executed\n */\n onSuccess?: (\n result: ReturnType<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\ntype AsyncRateLimiterOptionsWithOptionalCallbacks = OptionalKeys<\n AsyncRateLimiterOptions<any>,\n 'onError' | 'onReject' | 'onSettled' | 'onSuccess'\n>\n\nconst defaultOptions: Omit<\n AsyncRateLimiterOptionsWithOptionalCallbacks,\n 'limit' | 'window'\n> = {\n enabled: true,\n windowType: 'fixed',\n}\n\n/**\n * A class that creates an async 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 *\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 * Unlike the non-async RateLimiter, this async version supports returning values from the rate-limited function,\n * making it ideal for API calls and other async operations where you want the result of the `maybeExecute` call\n * instead of setting the result on a state variable from within the rate-limited function.\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 * 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 private _options: AsyncRateLimiterOptionsWithOptionalCallbacks\n private _errorCount = 0\n private _executionTimes: Array<number> = []\n private _lastResult: ReturnType<TFn> | undefined\n private _rejectionCount = 0\n private _settleCount = 0\n private _successCount = 0\n private _isExecuting = false\n\n constructor(\n private fn: TFn,\n initialOptions: AsyncRateLimiterOptions<TFn>,\n ) {\n this._options = {\n ...defaultOptions,\n ...initialOptions,\n throwOnError: initialOptions.throwOnError ?? !initialOptions.onError,\n }\n }\n\n /**\n * Updates the rate limiter options\n */\n setOptions(newOptions: Partial<AsyncRateLimiterOptions<TFn>>): void {\n this._options = { ...this._options, ...newOptions }\n }\n\n /**\n * Returns the current rate limiter options\n */\n getOptions(): AsyncRateLimiterOptions<TFn> {\n return this._options\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 * If execution is allowed, waits for any previous execution to complete before proceeding.\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 * - If the rate limit is exceeded, the execution will be rejected and the `onReject` handler\n * will be called if configured.\n * - The error state can be checked using `getErrorCount()` and `getIsExecuting()`.\n * - Rate limit rejections can be tracked using `getRejectionCount()`.\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 execute\n * await rateLimiter.maybeExecute('arg1', 'arg2');\n *\n * // Additional calls within the window will be rejected\n * await rateLimiter.maybeExecute('arg1', 'arg2'); // Rejected\n * ```\n */\n async maybeExecute(\n ...args: Parameters<TFn>\n ): Promise<ReturnType<TFn> | undefined> {\n this.cleanupOldExecutions()\n\n const limit = this.getLimit()\n const window = this.getWindow()\n\n if (this._options.windowType === 'sliding') {\n // For sliding window, we can execute if we have capacity in the current window\n if (this._executionTimes.length < limit) {\n await this.execute(...args)\n return this._lastResult\n }\n } else {\n // For fixed window, we need to check if we're in a new window\n const now = Date.now()\n const oldestExecution = Math.min(...this._executionTimes)\n const isNewWindow = oldestExecution + window <= now\n\n if (isNewWindow || this._executionTimes.length < limit) {\n await this.execute(...args)\n return this._lastResult\n }\n }\n\n this.rejectFunction()\n return undefined\n }\n\n private async execute(\n ...args: Parameters<TFn>\n ): Promise<ReturnType<TFn> | undefined> {\n if (!this.getEnabled()) return\n this._isExecuting = true\n const now = Date.now()\n this._executionTimes.push(now)\n\n try {\n this._lastResult = await this.fn(...args)\n this._successCount++\n this._options.onSuccess?.(this._lastResult!, this)\n } catch (error) {\n this._errorCount++\n this._options.onError?.(error, this)\n if (this._options.throwOnError) {\n throw error\n } else {\n console.error(error)\n }\n } finally {\n this._isExecuting = false\n this._settleCount++\n this._options.onSettled?.(this)\n }\n\n return this._lastResult\n }\n\n private rejectFunction(): void {\n this._rejectionCount++\n if (this._options.onReject) {\n this._options.onReject(this)\n }\n }\n\n private cleanupOldExecutions(): void {\n const now = Date.now()\n const windowStart = now - this.getWindow()\n this._executionTimes = this._executionTimes.filter(\n (time) => time > windowStart,\n )\n }\n\n /**\n * Returns the number of remaining executions allowed in the current window\n */\n getRemainingInWindow(): number {\n this.cleanupOldExecutions()\n return Math.max(0, this.getLimit() - this._executionTimes.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 = Math.min(...this._executionTimes)\n return oldestExecution + this.getWindow() - Date.now()\n }\n\n /**\n * Returns the number of times the function has been executed\n */\n getSuccessCount(): number {\n return this._successCount\n }\n\n /**\n * Returns the number of times the function has been settled\n */\n getSettleCount(): number {\n return this._settleCount\n }\n\n /**\n * Returns the number of times the function has errored\n */\n getErrorCount(): number {\n return this._errorCount\n }\n\n /**\n * Returns the number of times the function has been rejected\n */\n getRejectionCount(): number {\n return this._rejectionCount\n }\n\n /**\n * Returns whether the function is currently executing\n */\n getIsExecuting(): boolean {\n return this._isExecuting\n }\n\n /**\n * Resets the rate limiter state\n */\n reset(): void {\n this._executionTimes = []\n this._successCount = 0\n this._errorCount = 0\n this._rejectionCount = 0\n this._settleCount = 0\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 * Unlike the non-async rate limiter, this async version supports returning values from the rate-limited function,\n * making it ideal for API calls and other async operations where you want the result of the `maybeExecute` call\n * instead of setting the result on a state variable from within the rate-limited function.\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 * 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 * 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 * 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 * // 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.bind(rateLimiter)\n}\n"],"names":["parseFunctionOrValue"],"mappings":"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"} | ||
| {"version":3,"file":"async-rate-limiter.cjs","sources":["../../src/async-rate-limiter.ts"],"sourcesContent":["import { Store } from '@tanstack/store'\nimport { parseFunctionOrValue } from './utils'\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-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 * Number of function executions that have completed successfully\n */\n successCount: number\n}\n\nfunction getDefaultAsyncRateLimiterState<\n TFn extends AnyAsyncFunction,\n>(): AsyncRateLimiterState<TFn> {\n return {\n errorCount: 0,\n executionTimes: [],\n isExecuting: false,\n lastResult: undefined,\n rejectionCount: 0,\n settleCount: 0,\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 * 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 * 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?: (error: unknown, rateLimiter: AsyncRateLimiter<TFn>) => void\n /**\n * Optional callback function that is called when an execution is rejected due to rate limiting\n */\n onReject?: (rateLimiter: AsyncRateLimiter<TFn>) => void\n /**\n * Optional function to call when the rate-limited function is executed\n */\n onSettled?: (rateLimiter: AsyncRateLimiter<TFn>) => void\n /**\n * Optional function to call when the rate-limited function is executed\n */\n onSuccess?: (\n result: ReturnType<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\nconst defaultOptions: Omit<\n Required<AsyncRateLimiterOptions<any>>,\n 'initialState' | 'onError' | 'onReject' | 'onSettled' | 'onSuccess'\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 * 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 *\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 * Unlike the non-async RateLimiter, this async version supports returning values from the rate-limited function,\n * making it ideal for API calls and other async operations where you want the result of the `maybeExecute` call\n * instead of setting the result on a state variable from within the rate-limited function.\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 * - `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 options: AsyncRateLimiterOptions<TFn>\n\n constructor(\n private fn: TFn,\n initialOptions: AsyncRateLimiterOptions<TFn>,\n ) {\n this.options = {\n ...defaultOptions,\n ...initialOptions,\n throwOnError: initialOptions.throwOnError ?? !initialOptions.onError,\n }\n this.#setState(this.options.initialState ?? {})\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 return combinedState\n })\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.#cleanupOldExecutions()\n\n const relevantExecutionTimes = this.#getRelevantExecutionTimes()\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?.(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 now = Date.now()\n const executionTimes = [...this.store.state.executionTimes, now]\n this.#setState({\n isExecuting: true,\n executionTimes,\n })\n\n try {\n const result = await this.fn(...args)\n this.#setState({\n successCount: this.store.state.successCount + 1,\n lastResult: result,\n })\n this.options.onSuccess?.(result, this)\n } catch (error) {\n this.#setState({\n errorCount: this.store.state.errorCount + 1,\n })\n this.options.onError?.(error, this)\n if (this.options.throwOnError) {\n throw error\n }\n } finally {\n this.#setState({\n isExecuting: false,\n settleCount: this.store.state.settleCount + 1,\n })\n this.options.onSettled?.(this)\n }\n\n return this.store.state.lastResult\n }\n\n #getRelevantExecutionTimes = (): 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 const oldestExecution = Math.min(...this.store.state.executionTimes)\n const windowStart = oldestExecution\n return this.store.state.executionTimes.filter(\n (time) =>\n time >= windowStart && time <= windowStart + this.#getWindow(),\n )\n }\n }\n\n #cleanupOldExecutions = (): void => {\n const now = Date.now()\n const windowStart = now - this.#getWindow()\n this.#setState({\n executionTimes: this.store.state.executionTimes.filter(\n (time) => time > windowStart,\n ),\n })\n }\n\n /**\n * Returns the number of remaining executions allowed in the current window\n */\n getRemainingInWindow = (): number => {\n const relevantExecutionTimes = this.#getRelevantExecutionTimes()\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 * Resets the rate limiter state\n */\n reset = (): void => {\n this.#setState(getDefaultAsyncRateLimiterState())\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 * Unlike the non-async rate limiter, this async version supports returning values from the rate-limited function,\n * making it ideal for API calls and other async operations where you want the result of the `maybeExecute` call\n * instead of setting the result on a state variable from within the rate-limited function.\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 * 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 * 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 * 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 * 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 * // 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"],"names":["Store","parseFunctionOrValue"],"mappings":"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"} |
@@ -0,2 +1,33 @@ | ||
| import { Store } from '@tanstack/store'; | ||
| import { AnyAsyncFunction } from './types.cjs'; | ||
| export interface AsyncRateLimiterState<TFn extends AnyAsyncFunction> { | ||
| /** | ||
| * Number of function executions that have resulted in errors | ||
| */ | ||
| errorCount: number; | ||
| /** | ||
| * Array of timestamps when executions occurred for rate limiting calculations | ||
| */ | ||
| executionTimes: Array<number>; | ||
| /** | ||
| * Whether the rate-limited function is currently executing asynchronously | ||
| */ | ||
| isExecuting: boolean; | ||
| /** | ||
| * The result from the most recent successful function execution | ||
| */ | ||
| lastResult: ReturnType<TFn> | undefined; | ||
| /** | ||
| * Number of function executions that have been rejected due to rate limiting | ||
| */ | ||
| rejectionCount: number; | ||
| /** | ||
| * Number of function executions that have completed (either successfully or with errors) | ||
| */ | ||
| settleCount: number; | ||
| /** | ||
| * Number of function executions that have completed successfully | ||
| */ | ||
| successCount: number; | ||
| } | ||
| /** | ||
@@ -13,2 +44,6 @@ * Options for configuring an async rate-limited function | ||
| /** | ||
| * Initial state for the rate limiter | ||
| */ | ||
| initialState?: Partial<AsyncRateLimiterState<TFn>>; | ||
| /** | ||
| * Maximum number of executions allowed within the time window. | ||
@@ -79,2 +114,14 @@ * Can be a number or a function that returns a number. | ||
| * | ||
| * State Management: | ||
| * - Uses TanStack Store for reactive state management | ||
| * - Use `initialState` to provide initial state values when creating the rate limiter | ||
| * - `initialState` can be a partial state object | ||
| * - Use `onSuccess` callback to react to successful function execution and implement custom logic | ||
| * - Use `onError` callback to react to function execution errors and implement custom error handling | ||
| * - Use `onSettled` callback to react to function execution completion (success or error) and implement custom logic | ||
| * - Use `onReject` callback to react to executions being rejected when rate limit is exceeded | ||
| * - The state includes execution times, success/error counts, and current execution status | ||
| * - State can be accessed via `asyncRateLimiter.store.state` when using the class directly | ||
| * - When using framework adapters (React/Solid), state is accessed from `asyncRateLimiter.state` | ||
| * | ||
| * Error Handling: | ||
@@ -111,36 +158,14 @@ * - If an `onError` handler is provided, it will be called with the error and rate limiter instance | ||
| export declare class AsyncRateLimiter<TFn extends AnyAsyncFunction> { | ||
| #private; | ||
| private fn; | ||
| private _options; | ||
| private _errorCount; | ||
| private _executionTimes; | ||
| private _lastResult; | ||
| private _rejectionCount; | ||
| private _settleCount; | ||
| private _successCount; | ||
| private _isExecuting; | ||
| readonly store: Store<Readonly<AsyncRateLimiterState<TFn>>>; | ||
| options: AsyncRateLimiterOptions<TFn>; | ||
| constructor(fn: TFn, initialOptions: AsyncRateLimiterOptions<TFn>); | ||
| /** | ||
| * Updates the rate limiter options | ||
| * Updates the async rate limiter options | ||
| */ | ||
| setOptions(newOptions: Partial<AsyncRateLimiterOptions<TFn>>): void; | ||
| setOptions: (newOptions: Partial<AsyncRateLimiterOptions<TFn>>) => void; | ||
| /** | ||
| * Returns the current rate limiter options | ||
| */ | ||
| getOptions(): AsyncRateLimiterOptions<TFn>; | ||
| /** | ||
| * Returns the current enabled state of the rate limiter | ||
| */ | ||
| getEnabled(): boolean; | ||
| /** | ||
| * Returns the current limit of executions allowed within the time window | ||
| */ | ||
| getLimit(): number; | ||
| /** | ||
| * Returns the current time window in milliseconds | ||
| */ | ||
| getWindow(): number; | ||
| /** | ||
| * Attempts to execute the rate-limited function if within the configured limits. | ||
| * Will reject execution if the number of calls in the current window exceeds the limit. | ||
| * If execution is allowed, waits for any previous execution to complete before proceeding. | ||
| * | ||
@@ -152,6 +177,3 @@ * Error Handling: | ||
| * and this method will return undefined. | ||
| * - If the rate limit is exceeded, the execution will be rejected and the `onReject` handler | ||
| * will be called if configured. | ||
| * - The error state can be checked using `getErrorCount()` and `getIsExecuting()`. | ||
| * - Rate limit rejections can be tracked using `getRejectionCount()`. | ||
| * | ||
@@ -165,17 +187,14 @@ * @returns A promise that resolves with the function's return value, or undefined if an error occurred and was handled by onError | ||
| * | ||
| * // First 5 calls will execute | ||
| * await rateLimiter.maybeExecute('arg1', 'arg2'); | ||
| * // First 5 calls will return a promise that resolves with the result | ||
| * const result = await rateLimiter.maybeExecute('arg1', 'arg2'); | ||
| * | ||
| * // Additional calls within the window will be rejected | ||
| * await rateLimiter.maybeExecute('arg1', 'arg2'); // Rejected | ||
| * // Additional calls within the window will return undefined | ||
| * const result2 = await rateLimiter.maybeExecute('arg1', 'arg2'); // undefined | ||
| * ``` | ||
| */ | ||
| maybeExecute(...args: Parameters<TFn>): Promise<ReturnType<TFn> | undefined>; | ||
| private execute; | ||
| private rejectFunction; | ||
| private cleanupOldExecutions; | ||
| maybeExecute: (...args: Parameters<TFn>) => Promise<ReturnType<TFn> | undefined>; | ||
| /** | ||
| * Returns the number of remaining executions allowed in the current window | ||
| */ | ||
| getRemainingInWindow(): number; | ||
| getRemainingInWindow: () => number; | ||
| /** | ||
@@ -186,27 +205,7 @@ * Returns the number of milliseconds until the next execution will be possible | ||
| */ | ||
| getMsUntilNextWindow(): number; | ||
| getMsUntilNextWindow: () => number; | ||
| /** | ||
| * Returns the number of times the function has been executed | ||
| */ | ||
| getSuccessCount(): number; | ||
| /** | ||
| * Returns the number of times the function has been settled | ||
| */ | ||
| getSettleCount(): number; | ||
| /** | ||
| * Returns the number of times the function has errored | ||
| */ | ||
| getErrorCount(): number; | ||
| /** | ||
| * Returns the number of times the function has been rejected | ||
| */ | ||
| getRejectionCount(): number; | ||
| /** | ||
| * Returns whether the function is currently executing | ||
| */ | ||
| getIsExecuting(): boolean; | ||
| /** | ||
| * Resets the rate limiter state | ||
| */ | ||
| reset(): void; | ||
| reset: () => void; | ||
| } | ||
@@ -231,2 +230,14 @@ /** | ||
| * | ||
| * State Management: | ||
| * - Uses TanStack Store for reactive state management | ||
| * - Use `initialState` to provide initial state values when creating the rate limiter | ||
| * - `initialState` can be a partial state object | ||
| * - Use `onSuccess` callback to react to successful function execution and implement custom logic | ||
| * - Use `onError` callback to react to function execution errors and implement custom error handling | ||
| * - Use `onSettled` callback to react to function execution completion (success or error) and implement custom logic | ||
| * - Use `onReject` callback to react to executions being rejected when rate limit is exceeded | ||
| * - The state includes execution times, success/error counts, and current execution status | ||
| * - State can be accessed via the underlying AsyncRateLimiter instance's `store.state` property | ||
| * - When using framework adapters (React/Solid), state is accessed from the hook's state property | ||
| * | ||
| * Consider using throttle() or debounce() if you need more intelligent execution control. Use rate limiting when you specifically | ||
@@ -233,0 +244,0 @@ * need to enforce a hard limit on the number of executions within a time period. |
+167
-177
| "use strict"; | ||
| Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" }); | ||
| const store = require("@tanstack/store"); | ||
| const utils = require("./utils.cjs"); | ||
| function getDefaultAsyncThrottlerState() { | ||
| return structuredClone({ | ||
| errorCount: 0, | ||
| isExecuting: false, | ||
| isPending: false, | ||
| lastArgs: void 0, | ||
| lastExecutionTime: 0, | ||
| lastResult: void 0, | ||
| nextExecutionTime: 0, | ||
| settleCount: 0, | ||
| status: "idle", | ||
| successCount: 0 | ||
| }); | ||
| } | ||
| const defaultOptions = { | ||
@@ -13,12 +28,141 @@ enabled: true, | ||
| this.fn = fn; | ||
| this._abortController = null; | ||
| this._errorCount = 0; | ||
| this._isExecuting = false; | ||
| this._lastExecutionTime = 0; | ||
| this._nextExecutionTime = 0; | ||
| this._settleCount = 0; | ||
| this._successCount = 0; | ||
| this._timeoutId = null; | ||
| this._resolvePreviousPromise = null; | ||
| this._options = { | ||
| this.store = new store.Store(getDefaultAsyncThrottlerState()); | ||
| this.#abortController = null; | ||
| this.#timeoutId = null; | ||
| this.#resolvePreviousPromise = null; | ||
| this.setOptions = (newOptions) => { | ||
| this.options = { ...this.options, ...newOptions }; | ||
| if (!this.#getEnabled()) { | ||
| this.cancel(); | ||
| } | ||
| }; | ||
| this.#setState = (newState) => { | ||
| this.store.setState((state) => { | ||
| const combinedState = { | ||
| ...state, | ||
| ...newState | ||
| }; | ||
| const { isPending, isExecuting, settleCount } = combinedState; | ||
| return { | ||
| ...combinedState, | ||
| status: !this.#getEnabled() ? "disabled" : isPending ? "pending" : isExecuting ? "executing" : settleCount > 0 ? "settled" : "idle" | ||
| }; | ||
| }); | ||
| }; | ||
| this.#getEnabled = () => { | ||
| return !!utils.parseFunctionOrValue(this.options.enabled, this); | ||
| }; | ||
| this.#getWait = () => { | ||
| return utils.parseFunctionOrValue(this.options.wait, this); | ||
| }; | ||
| this.maybeExecute = async (...args) => { | ||
| if (!this.#getEnabled()) return void 0; | ||
| const now = Date.now(); | ||
| const timeSinceLastExecution = now - this.store.state.lastExecutionTime; | ||
| const wait = this.#getWait(); | ||
| this.#setState({ lastArgs: args }); | ||
| this.#resolvePreviousPromiseInternal(); | ||
| if (this.options.leading && timeSinceLastExecution >= wait) { | ||
| await this.#execute(...args); | ||
| return this.store.state.lastResult; | ||
| } else { | ||
| return new Promise((resolve) => { | ||
| this.#resolvePreviousPromise = resolve; | ||
| this.#clearTimeout(); | ||
| if (this.options.trailing) { | ||
| const _timeSinceLastExecution = this.store.state.lastExecutionTime ? now - this.store.state.lastExecutionTime : 0; | ||
| const timeoutDuration = wait - _timeSinceLastExecution; | ||
| this.#setState({ isPending: true }); | ||
| this.#timeoutId = setTimeout(async () => { | ||
| if (this.store.state.lastArgs !== void 0) { | ||
| await this.#execute(...this.store.state.lastArgs); | ||
| } | ||
| this.#resolvePreviousPromise = null; | ||
| resolve(this.store.state.lastResult); | ||
| }, timeoutDuration); | ||
| } | ||
| }); | ||
| } | ||
| }; | ||
| this.#execute = async (...args) => { | ||
| if (!this.#getEnabled() || this.store.state.isExecuting) return void 0; | ||
| this.#abortController = new AbortController(); | ||
| try { | ||
| this.#setState({ isExecuting: true }); | ||
| const result = await this.fn(...args); | ||
| this.#setState({ | ||
| lastResult: result, | ||
| successCount: this.store.state.successCount + 1 | ||
| }); | ||
| this.options.onSuccess?.(result, this); | ||
| } catch (error) { | ||
| this.#setState({ | ||
| errorCount: this.store.state.errorCount + 1 | ||
| }); | ||
| this.options.onError?.(error, this); | ||
| if (this.options.throwOnError) { | ||
| throw error; | ||
| } else { | ||
| console.error(error); | ||
| } | ||
| } finally { | ||
| const lastExecutionTime = Date.now(); | ||
| const nextExecutionTime = lastExecutionTime + this.#getWait(); | ||
| this.#setState({ | ||
| isExecuting: false, | ||
| isPending: false, | ||
| settleCount: this.store.state.settleCount + 1, | ||
| lastExecutionTime, | ||
| nextExecutionTime | ||
| }); | ||
| this.#abortController = null; | ||
| this.options.onSettled?.(this); | ||
| } | ||
| return this.store.state.lastResult; | ||
| }; | ||
| this.flush = () => { | ||
| if (this.store.state.isPending && this.store.state.lastArgs) { | ||
| this.#abortExecution(); | ||
| this.#clearTimeout(); | ||
| this.#execute(...this.store.state.lastArgs); | ||
| } | ||
| }; | ||
| this.#resolvePreviousPromiseInternal = () => { | ||
| if (this.#resolvePreviousPromise) { | ||
| this.#resolvePreviousPromise(this.store.state.lastResult); | ||
| this.#resolvePreviousPromise = null; | ||
| } | ||
| }; | ||
| this.#clearTimeout = () => { | ||
| if (this.#timeoutId) { | ||
| clearTimeout(this.#timeoutId); | ||
| this.#timeoutId = null; | ||
| } | ||
| }; | ||
| this.#cancelPendingExecution = () => { | ||
| this.#clearTimeout(); | ||
| if (this.#resolvePreviousPromise) { | ||
| this.#resolvePreviousPromise(this.store.state.lastResult); | ||
| this.#resolvePreviousPromise = null; | ||
| } | ||
| this.#setState({ | ||
| isPending: false, | ||
| isExecuting: false, | ||
| lastArgs: void 0 | ||
| }); | ||
| }; | ||
| this.#abortExecution = () => { | ||
| if (this.#abortController) { | ||
| this.#abortController.abort(); | ||
| this.#abortController = null; | ||
| } | ||
| }; | ||
| this.cancel = () => { | ||
| this.#cancelPendingExecution(); | ||
| this.#abortExecution(); | ||
| }; | ||
| this.reset = () => { | ||
| this.#setState(getDefaultAsyncThrottlerState()); | ||
| }; | ||
| this.options = { | ||
| ...defaultOptions, | ||
@@ -28,173 +172,19 @@ ...initialOptions, | ||
| }; | ||
| this.#setState(this.options.initialState ?? {}); | ||
| } | ||
| /** | ||
| * Updates the throttler options | ||
| */ | ||
| setOptions(newOptions) { | ||
| this._options = { ...this._options, ...newOptions }; | ||
| if (!this._options.enabled) { | ||
| this.cancel(); | ||
| } | ||
| } | ||
| /** | ||
| * Returns the current options | ||
| */ | ||
| getOptions() { | ||
| return this._options; | ||
| } | ||
| /** | ||
| * Returns the current enabled state of the throttler | ||
| */ | ||
| getEnabled() { | ||
| return !!utils.parseFunctionOrValue(this._options.enabled, this); | ||
| } | ||
| /** | ||
| * Returns the current wait time in milliseconds | ||
| */ | ||
| getWait() { | ||
| return utils.parseFunctionOrValue(this._options.wait, this); | ||
| } | ||
| /** | ||
| * Attempts to execute the throttled function. | ||
| * If a call is already in progress, it may be blocked or queued depending on the `wait` option. | ||
| * | ||
| * Error Handling: | ||
| * - If the throttled function throws and no `onError` handler is configured, | ||
| * the error will be thrown from this method. | ||
| * - If an `onError` handler is configured, errors will be caught and passed to the handler, | ||
| * and this method will return undefined. | ||
| * - The error state can be checked using `getErrorCount()` and `getIsExecuting()`. | ||
| * | ||
| * @returns A promise that resolves with the function's return value, or undefined if an error occurred and was handled by onError | ||
| * @throws The error from the throttled function if no onError handler is configured | ||
| */ | ||
| async maybeExecute(...args) { | ||
| const now = Date.now(); | ||
| const timeSinceLastExecution = now - this._lastExecutionTime; | ||
| const wait = this.getWait(); | ||
| this.resolvePreviousPromise(); | ||
| if (this._options.leading && timeSinceLastExecution >= wait) { | ||
| await this.execute(...args); | ||
| return this._lastResult; | ||
| } else { | ||
| this._lastArgs = args; | ||
| return new Promise((resolve) => { | ||
| this._resolvePreviousPromise = resolve; | ||
| if (this._timeoutId) { | ||
| clearTimeout(this._timeoutId); | ||
| } | ||
| if (this._options.trailing) { | ||
| const _timeSinceLastExecution = this._lastExecutionTime ? now - this._lastExecutionTime : 0; | ||
| const timeoutDuration = wait - _timeSinceLastExecution; | ||
| this._timeoutId = setTimeout(async () => { | ||
| if (this._lastArgs !== void 0) { | ||
| await this.execute(...this._lastArgs); | ||
| } | ||
| this._resolvePreviousPromise = null; | ||
| resolve(this._lastResult); | ||
| }, timeoutDuration); | ||
| } | ||
| }); | ||
| } | ||
| } | ||
| async execute(...args) { | ||
| var _a, _b, _c, _d, _e, _f; | ||
| if (!this.getEnabled() || this._isExecuting) return void 0; | ||
| this._abortController = new AbortController(); | ||
| try { | ||
| this._isExecuting = true; | ||
| this._lastResult = await this.fn(...args); | ||
| this._successCount++; | ||
| (_b = (_a = this._options).onSuccess) == null ? void 0 : _b.call(_a, this._lastResult, this); | ||
| } catch (error) { | ||
| this._errorCount++; | ||
| (_d = (_c = this._options).onError) == null ? void 0 : _d.call(_c, error, this); | ||
| if (this._options.throwOnError) { | ||
| throw error; | ||
| } else { | ||
| console.error(error); | ||
| } | ||
| } finally { | ||
| this._isExecuting = false; | ||
| this._settleCount++; | ||
| this._abortController = null; | ||
| this._lastExecutionTime = Date.now(); | ||
| this._nextExecutionTime = this._lastExecutionTime + this.getWait(); | ||
| (_f = (_e = this._options).onSettled) == null ? void 0 : _f.call(_e, this); | ||
| } | ||
| return this._lastResult; | ||
| } | ||
| resolvePreviousPromise() { | ||
| if (this._resolvePreviousPromise) { | ||
| this._resolvePreviousPromise(this._lastResult); | ||
| this._resolvePreviousPromise = null; | ||
| } | ||
| } | ||
| /** | ||
| * Cancels any pending execution or aborts any execution in progress | ||
| */ | ||
| cancel() { | ||
| if (this._timeoutId) { | ||
| clearTimeout(this._timeoutId); | ||
| this._timeoutId = null; | ||
| } | ||
| if (this._abortController) { | ||
| this._abortController.abort(); | ||
| this._abortController = null; | ||
| } | ||
| this.resolvePreviousPromise(); | ||
| this._lastArgs = void 0; | ||
| } | ||
| /** | ||
| * Returns the last execution time | ||
| */ | ||
| getLastExecutionTime() { | ||
| return this._lastExecutionTime; | ||
| } | ||
| /** | ||
| * Returns the next execution time | ||
| */ | ||
| getNextExecutionTime() { | ||
| return this._nextExecutionTime; | ||
| } | ||
| /** | ||
| * Returns the last result of the debounced function | ||
| */ | ||
| getLastResult() { | ||
| return this._lastResult; | ||
| } | ||
| /** | ||
| * Returns the number of times the function has been executed successfully | ||
| */ | ||
| getSuccessCount() { | ||
| return this._successCount; | ||
| } | ||
| /** | ||
| * Returns the number of times the function has settled (completed or errored) | ||
| */ | ||
| getSettleCount() { | ||
| return this._settleCount; | ||
| } | ||
| /** | ||
| * Returns the number of times the function has errored | ||
| */ | ||
| getErrorCount() { | ||
| return this._errorCount; | ||
| } | ||
| /** | ||
| * Returns the current pending state | ||
| */ | ||
| getIsPending() { | ||
| return this.getEnabled() && !!this._timeoutId; | ||
| } | ||
| /** | ||
| * Returns the current executing state | ||
| */ | ||
| getIsExecuting() { | ||
| return this._isExecuting; | ||
| } | ||
| #abortController; | ||
| #timeoutId; | ||
| #resolvePreviousPromise; | ||
| #setState; | ||
| #getEnabled; | ||
| #getWait; | ||
| #execute; | ||
| #resolvePreviousPromiseInternal; | ||
| #clearTimeout; | ||
| #cancelPendingExecution; | ||
| #abortExecution; | ||
| } | ||
| function asyncThrottle(fn, initialOptions) { | ||
| const asyncThrottler = new AsyncThrottler(fn, initialOptions); | ||
| return asyncThrottler.maybeExecute.bind(asyncThrottler); | ||
| return asyncThrottler.maybeExecute; | ||
| } | ||
@@ -201,0 +191,0 @@ exports.AsyncThrottler = AsyncThrottler; |
@@ -1,1 +0,1 @@ | ||
| {"version":3,"file":"async-throttler.cjs","sources":["../../src/async-throttler.ts"],"sourcesContent":["import { parseFunctionOrValue } from './utils'\nimport type { AnyAsyncFunction, OptionalKeys } from './types'\n\n/**\n * Options for configuring an async throttled function\n */\nexport interface AsyncThrottlerOptions<TFn extends AnyAsyncFunction> {\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 * 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?: (error: unknown, asyncThrottler: AsyncThrottler<TFn>) => void\n /**\n * Optional function to call when the throttled function is executed\n */\n onSettled?: (asyncThrottler: AsyncThrottler<TFn>) => void\n /**\n * Optional function to call when the throttled function is executed\n */\n onSuccess?: (\n result: ReturnType<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\ntype AsyncThrottlerOptionsWithOptionalCallbacks = OptionalKeys<\n AsyncThrottlerOptions<any>,\n 'onError' | 'onSettled' | 'onSuccess'\n>\n\nconst defaultOptions: AsyncThrottlerOptionsWithOptionalCallbacks = {\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 * 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 * Unlike the non-async Throttler, this async version supports returning values from the throttled function,\n * making it ideal for API calls and other async operations where you want the result of the `maybeExecute` call\n * instead of setting the result on a state variable from within the throttled function.\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 * @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 private _options: AsyncThrottlerOptionsWithOptionalCallbacks\n private _abortController: AbortController | null = null\n private _errorCount = 0\n private _isExecuting = false\n private _lastArgs: Parameters<TFn> | undefined\n private _lastExecutionTime = 0\n private _lastResult: ReturnType<TFn> | undefined\n private _nextExecutionTime = 0\n private _settleCount = 0\n private _successCount = 0\n private _timeoutId: NodeJS.Timeout | null = null\n private _resolvePreviousPromise:\n | ((value?: ReturnType<TFn> | undefined) => void)\n | null = null\n\n constructor(\n private fn: TFn,\n initialOptions: AsyncThrottlerOptions<TFn>,\n ) {\n this._options = {\n ...defaultOptions,\n ...initialOptions,\n throwOnError: initialOptions.throwOnError ?? !initialOptions.onError,\n }\n }\n\n /**\n * Updates the throttler options\n */\n setOptions(newOptions: Partial<AsyncThrottlerOptions<TFn>>): void {\n this._options = { ...this._options, ...newOptions }\n\n // End the pending state if the debouncer is disabled\n if (!this._options.enabled) {\n this.cancel()\n }\n }\n\n /**\n * Returns the current options\n */\n getOptions(): AsyncThrottlerOptions<TFn> {\n return this._options\n }\n\n /**\n * Returns the current enabled state of the 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.\n * If a call is already in progress, it may be blocked or queued depending on the `wait` option.\n *\n * Error Handling:\n * - If the throttled 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 throttled function if no onError handler is configured\n */\n async maybeExecute(\n ...args: Parameters<TFn>\n ): Promise<ReturnType<TFn> | undefined> {\n const now = Date.now()\n const timeSinceLastExecution = now - this._lastExecutionTime\n const wait = this.getWait()\n\n this.resolvePreviousPromise()\n\n // Handle leading execution\n if (this._options.leading && timeSinceLastExecution >= wait) {\n await this.execute(...args)\n return this._lastResult\n } else {\n // Store the most recent arguments for potential trailing execution\n this._lastArgs = args\n\n return new Promise((resolve) => {\n this._resolvePreviousPromise = resolve\n // Clear any existing timeout to ensure we use the latest arguments\n if (this._timeoutId) {\n clearTimeout(this._timeoutId)\n }\n\n // Set up trailing execution if enabled\n if (this._options.trailing) {\n const _timeSinceLastExecution = this._lastExecutionTime\n ? now - this._lastExecutionTime\n : 0\n const timeoutDuration = wait - _timeSinceLastExecution\n this._timeoutId = setTimeout(async () => {\n if (this._lastArgs !== undefined) {\n await this.execute(...this._lastArgs)\n }\n this._resolvePreviousPromise = null\n resolve(this._lastResult)\n }, timeoutDuration)\n }\n })\n }\n }\n\n private async execute(\n ...args: Parameters<TFn>\n ): Promise<ReturnType<TFn> | undefined> {\n if (!this.getEnabled() || this._isExecuting) return undefined\n this._abortController = new AbortController()\n try {\n this._isExecuting = true\n this._lastResult = await this.fn(...args) // EXECUTE!\n this._successCount++\n this._options.onSuccess?.(this._lastResult!, this)\n } catch (error) {\n this._errorCount++\n this._options.onError?.(error, this)\n if (this._options.throwOnError) {\n throw error\n } else {\n console.error(error)\n }\n } finally {\n this._isExecuting = false\n this._settleCount++\n this._abortController = null\n this._lastExecutionTime = Date.now()\n this._nextExecutionTime = this._lastExecutionTime + this.getWait()\n this._options.onSettled?.(this)\n }\n return this._lastResult\n }\n\n private resolvePreviousPromise(): void {\n if (this._resolvePreviousPromise) {\n this._resolvePreviousPromise(this._lastResult)\n this._resolvePreviousPromise = null\n }\n }\n\n /**\n * Cancels any pending execution or aborts any execution in progress\n */\n cancel(): void {\n if (this._timeoutId) {\n clearTimeout(this._timeoutId)\n this._timeoutId = null\n }\n if (this._abortController) {\n this._abortController.abort()\n this._abortController = null\n }\n this.resolvePreviousPromise()\n this._lastArgs = undefined\n }\n\n /**\n * Returns the last execution time\n */\n getLastExecutionTime(): number {\n return this._lastExecutionTime\n }\n\n /**\n * Returns the next execution time\n */\n getNextExecutionTime(): number {\n return this._nextExecutionTime\n }\n\n /**\n * Returns the last result of the debounced function\n */\n getLastResult(): ReturnType<TFn> | undefined {\n return this._lastResult\n }\n\n /**\n * Returns the number of times the function has been executed successfully\n */\n getSuccessCount(): number {\n return this._successCount\n }\n\n /**\n * Returns the number of times the function has settled (completed or errored)\n */\n getSettleCount(): number {\n return this._settleCount\n }\n\n /**\n * Returns the number of times the function has errored\n */\n getErrorCount(): number {\n return this._errorCount\n }\n\n /**\n * Returns the current pending state\n */\n getIsPending(): boolean {\n return this.getEnabled() && !!this._timeoutId\n }\n\n /**\n * Returns the current executing state\n */\n getIsExecuting(): boolean {\n return this._isExecuting\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 * Unlike the non-async Throttler, this async version supports returning values from the throttled function,\n * making it ideal for API calls and other async operations where you want the result of the `maybeExecute` call\n * instead of setting the result on a state variable from within the throttled function.\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 * @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.bind(asyncThrottler)\n}\n"],"names":["parseFunctionOrValue"],"mappings":"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"} | ||
| {"version":3,"file":"async-throttler.cjs","sources":["../../src/async-throttler.ts"],"sourcesContent":["import { Store } from '@tanstack/store'\nimport { parseFunctionOrValue } from './utils'\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 * Timestamp when the next execution can occur in milliseconds\n */\n nextExecutionTime: 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 getDefaultAsyncThrottlerState<\n TFn extends AnyAsyncFunction,\n>(): AsyncThrottlerState<TFn> {\n return structuredClone({\n errorCount: 0,\n isExecuting: false,\n isPending: false,\n lastArgs: undefined,\n lastExecutionTime: 0,\n lastResult: undefined,\n nextExecutionTime: 0,\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 * 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 * 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?: (error: unknown, asyncThrottler: AsyncThrottler<TFn>) => void\n /**\n * Optional function to call when the throttled function is executed\n */\n onSettled?: (asyncThrottler: AsyncThrottler<TFn>) => void\n /**\n * Optional function to call when the throttled function is executed\n */\n onSuccess?: (\n result: ReturnType<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\ntype AsyncThrottlerOptionsWithOptionalCallbacks = OptionalKeys<\n AsyncThrottlerOptions<any>,\n 'initialState' | 'onError' | 'onSettled' | 'onSuccess'\n>\n\nconst defaultOptions: AsyncThrottlerOptionsWithOptionalCallbacks = {\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 * 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 * Unlike the non-async Throttler, this async version supports returning values from the throttled function,\n * making it ideal for API calls and other async operations where you want the result of the `maybeExecute` call\n * instead of setting the result on a state variable from within the throttled function.\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 options: AsyncThrottlerOptions<TFn>\n #abortController: AbortController | null = null\n #timeoutId: NodeJS.Timeout | null = null\n #resolvePreviousPromise:\n | ((value?: ReturnType<TFn> | undefined) => void)\n | null = null\n\n constructor(\n private fn: TFn,\n initialOptions: AsyncThrottlerOptions<TFn>,\n ) {\n this.options = {\n ...defaultOptions,\n ...initialOptions,\n throwOnError: initialOptions.throwOnError ?? !initialOptions.onError,\n }\n this.#setState(this.options.initialState ?? {})\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 }\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 const now = Date.now()\n const timeSinceLastExecution = now - this.store.state.lastExecutionTime\n const wait = this.#getWait()\n // Store the most recent arguments for potential trailing execution\n this.#setState({ lastArgs: args })\n\n this.#resolvePreviousPromiseInternal()\n\n // Handle leading execution\n if (this.options.leading && timeSinceLastExecution >= wait) {\n await this.#execute(...args)\n return this.store.state.lastResult\n } else {\n return new Promise((resolve) => {\n this.#resolvePreviousPromise = resolve\n // Clear any existing timeout to ensure we use the latest arguments\n this.#clearTimeout()\n\n // Set up trailing execution if enabled\n if (this.options.trailing) {\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(async () => {\n if (this.store.state.lastArgs !== undefined) {\n await this.#execute(...this.store.state.lastArgs)\n }\n this.#resolvePreviousPromise = null\n resolve(this.store.state.lastResult)\n }, timeoutDuration)\n }\n })\n }\n }\n\n #execute = async (\n ...args: Parameters<TFn>\n ): Promise<ReturnType<TFn> | undefined> => {\n if (!this.#getEnabled() || this.store.state.isExecuting) return undefined\n this.#abortController = new AbortController()\n try {\n this.#setState({ isExecuting: true })\n const result = await this.fn(...args) // EXECUTE!\n this.#setState({\n lastResult: result,\n successCount: this.store.state.successCount + 1,\n })\n this.options.onSuccess?.(result, this)\n } catch (error) {\n this.#setState({\n errorCount: this.store.state.errorCount + 1,\n })\n this.options.onError?.(error, this)\n if (this.options.throwOnError) {\n throw error\n } else {\n console.error(error)\n }\n } finally {\n const lastExecutionTime = Date.now()\n const nextExecutionTime = lastExecutionTime + this.#getWait()\n this.#setState({\n isExecuting: false,\n isPending: false,\n settleCount: this.store.state.settleCount + 1,\n lastExecutionTime,\n nextExecutionTime,\n })\n this.#abortController = null\n this.options.onSettled?.(this)\n }\n return this.store.state.lastResult\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.#abortExecution() // abort any current execution\n this.#clearTimeout() // clear any existing timeout\n this.#execute(...this.store.state.lastArgs)\n }\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 #cancelPendingExecution = (): void => {\n this.#clearTimeout()\n if (this.#resolvePreviousPromise) {\n this.#resolvePreviousPromise(this.store.state.lastResult)\n this.#resolvePreviousPromise = null\n }\n this.#setState({\n isPending: false,\n isExecuting: false,\n lastArgs: undefined,\n })\n }\n\n #abortExecution = (): void => {\n if (this.#abortController) {\n this.#abortController.abort()\n this.#abortController = null\n }\n }\n\n /**\n * Cancels any pending execution or aborts any execution in progress\n */\n cancel = (): void => {\n this.#cancelPendingExecution()\n this.#abortExecution()\n }\n\n /**\n * Resets the debouncer state to its default values\n */\n reset = (): void => {\n this.#setState(getDefaultAsyncThrottlerState<TFn>())\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 * Unlike the non-async Throttler, this async version supports returning values from the throttled function,\n * making it ideal for API calls and other async operations where you want the result of the `maybeExecute` call\n * instead of setting the result on a state variable from within the throttled function.\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"],"names":["Store","parseFunctionOrValue"],"mappings":";;;;AA+CA,SAAS,gCAEqB;AAC5B,SAAO,gBAAgB;AAAA,IACrB,YAAY;AAAA,IACZ,aAAa;AAAA,IACb,WAAW;AAAA,IACX,UAAU;AAAA,IACV,mBAAmB;AAAA,IACnB,YAAY;AAAA,IACZ,mBAAmB;AAAA,IACnB,aAAa;AAAA,IACb,QAAQ;AAAA,IACR,cAAc;AAAA,EAAA,CACf;AACH;AA8DA,MAAM,iBAA6D;AAAA,EACjE,SAAS;AAAA,EACT,SAAS;AAAA,EACT,UAAU;AAAA,EACV,MAAM;AACR;AAkDO,MAAM,eAA6C;AAAA,EAWxD,YACU,IACR,gBACA;AAFQ,SAAA,KAAA;AAXV,SAAS,QAAmD,IAAIA,MAAAA,MAE9D,8BAAA,CAAoC;AAEtC,SAAA,mBAA2C;AAC3C,SAAA,aAAoC;AACpC,SAAA,0BAEW;AAiBX,SAAA,aAAa,CAAC,eAA0D;AACtE,WAAK,UAAU,EAAE,GAAG,KAAK,SAAS,GAAG,WAAA;AAGrC,UAAI,CAAC,KAAK,eAAe;AACvB,aAAK,OAAA;AAAA,MAAO;AAAA,IACd;AAGF,SAAA,YAAY,CAAC,aAAsD;AACjE,WAAK,MAAM,SAAS,CAAC,UAAU;AAC7B,cAAM,gBAAgB;AAAA,UACpB,GAAG;AAAA,UACH,GAAG;AAAA,QAAA;AAEL,cAAM,EAAE,WAAW,aAAa,YAAA,IAAgB;AAChD,eAAO;AAAA,UACL,GAAG;AAAA,UACH,QAAQ,CAAC,KAAK,YAAA,IACV,aACA,YACE,YACA,cACE,cACA,cAAc,IACZ,YACA;AAAA,QAAA;AAAA,MACZ,CACD;AAAA,IAAA;AAMH,SAAA,cAAc,MAAe;AAC3B,aAAO,CAAC,CAACC,MAAAA,qBAAqB,KAAK,QAAQ,SAAS,IAAI;AAAA,IAAA;AAM1D,SAAA,WAAW,MAAc;AACvB,aAAOA,MAAAA,qBAAqB,KAAK,QAAQ,MAAM,IAAI;AAAA,IAAA;AAyBrD,SAAA,eAAe,UACV,SACsC;AACzC,UAAI,CAAC,KAAK,YAAA,EAAe,QAAO;AAChC,YAAM,MAAM,KAAK,IAAA;AACjB,YAAM,yBAAyB,MAAM,KAAK,MAAM,MAAM;AACtD,YAAM,OAAO,KAAK,SAAA;AAElB,WAAK,UAAU,EAAE,UAAU,KAAA,CAAM;AAEjC,WAAK,gCAAA;AAGL,UAAI,KAAK,QAAQ,WAAW,0BAA0B,MAAM;AAC1D,cAAM,KAAK,SAAS,GAAG,IAAI;AAC3B,eAAO,KAAK,MAAM,MAAM;AAAA,MAAA,OACnB;AACL,eAAO,IAAI,QAAQ,CAAC,YAAY;AAC9B,eAAK,0BAA0B;AAE/B,eAAK,cAAA;AAGL,cAAI,KAAK,QAAQ,UAAU;AACzB,kBAAM,0BAA0B,KAAK,MAAM,MAAM,oBAC7C,MAAM,KAAK,MAAM,MAAM,oBACvB;AACJ,kBAAM,kBAAkB,OAAO;AAC/B,iBAAK,UAAU,EAAE,WAAW,KAAA,CAAM;AAClC,iBAAK,aAAa,WAAW,YAAY;AACvC,kBAAI,KAAK,MAAM,MAAM,aAAa,QAAW;AAC3C,sBAAM,KAAK,SAAS,GAAG,KAAK,MAAM,MAAM,QAAQ;AAAA,cAAA;AAElD,mBAAK,0BAA0B;AAC/B,sBAAQ,KAAK,MAAM,MAAM,UAAU;AAAA,YAAA,GAClC,eAAe;AAAA,UAAA;AAAA,QACpB,CACD;AAAA,MAAA;AAAA,IACH;AAGF,SAAA,WAAW,UACN,SACsC;AACzC,UAAI,CAAC,KAAK,iBAAiB,KAAK,MAAM,MAAM,YAAa,QAAO;AAChE,WAAK,mBAAmB,IAAI,gBAAA;AAC5B,UAAI;AACF,aAAK,UAAU,EAAE,aAAa,KAAA,CAAM;AACpC,cAAM,SAAS,MAAM,KAAK,GAAG,GAAG,IAAI;AACpC,aAAK,UAAU;AAAA,UACb,YAAY;AAAA,UACZ,cAAc,KAAK,MAAM,MAAM,eAAe;AAAA,QAAA,CAC/C;AACD,aAAK,QAAQ,YAAY,QAAQ,IAAI;AAAA,MAAA,SAC9B,OAAO;AACd,aAAK,UAAU;AAAA,UACb,YAAY,KAAK,MAAM,MAAM,aAAa;AAAA,QAAA,CAC3C;AACD,aAAK,QAAQ,UAAU,OAAO,IAAI;AAClC,YAAI,KAAK,QAAQ,cAAc;AAC7B,gBAAM;AAAA,QAAA,OACD;AACL,kBAAQ,MAAM,KAAK;AAAA,QAAA;AAAA,MACrB,UACF;AACE,cAAM,oBAAoB,KAAK,IAAA;AAC/B,cAAM,oBAAoB,oBAAoB,KAAK,SAAA;AACnD,aAAK,UAAU;AAAA,UACb,aAAa;AAAA,UACb,WAAW;AAAA,UACX,aAAa,KAAK,MAAM,MAAM,cAAc;AAAA,UAC5C;AAAA,UACA;AAAA,QAAA,CACD;AACD,aAAK,mBAAmB;AACxB,aAAK,QAAQ,YAAY,IAAI;AAAA,MAAA;AAE/B,aAAO,KAAK,MAAM,MAAM;AAAA,IAAA;AAM1B,SAAA,QAAQ,MAAY;AAClB,UAAI,KAAK,MAAM,MAAM,aAAa,KAAK,MAAM,MAAM,UAAU;AAC3D,aAAK,gBAAA;AACL,aAAK,cAAA;AACL,aAAK,SAAS,GAAG,KAAK,MAAM,MAAM,QAAQ;AAAA,MAAA;AAAA,IAC5C;AAGF,SAAA,kCAAkC,MAAY;AAC5C,UAAI,KAAK,yBAAyB;AAChC,aAAK,wBAAwB,KAAK,MAAM,MAAM,UAAU;AACxD,aAAK,0BAA0B;AAAA,MAAA;AAAA,IACjC;AAGF,SAAA,gBAAgB,MAAY;AAC1B,UAAI,KAAK,YAAY;AACnB,qBAAa,KAAK,UAAU;AAC5B,aAAK,aAAa;AAAA,MAAA;AAAA,IACpB;AAGF,SAAA,0BAA0B,MAAY;AACpC,WAAK,cAAA;AACL,UAAI,KAAK,yBAAyB;AAChC,aAAK,wBAAwB,KAAK,MAAM,MAAM,UAAU;AACxD,aAAK,0BAA0B;AAAA,MAAA;AAEjC,WAAK,UAAU;AAAA,QACb,WAAW;AAAA,QACX,aAAa;AAAA,QACb,UAAU;AAAA,MAAA,CACX;AAAA,IAAA;AAGH,SAAA,kBAAkB,MAAY;AAC5B,UAAI,KAAK,kBAAkB;AACzB,aAAK,iBAAiB,MAAA;AACtB,aAAK,mBAAmB;AAAA,MAAA;AAAA,IAC1B;AAMF,SAAA,SAAS,MAAY;AACnB,WAAK,wBAAA;AACL,WAAK,gBAAA;AAAA,IAAgB;AAMvB,SAAA,QAAQ,MAAY;AAClB,WAAK,UAAU,+BAAoC;AAAA,IAAA;AAvNnD,SAAK,UAAU;AAAA,MACb,GAAG;AAAA,MACH,GAAG;AAAA,MACH,cAAc,eAAe,gBAAgB,CAAC,eAAe;AAAA,IAAA;AAE/D,SAAK,UAAU,KAAK,QAAQ,gBAAgB,CAAA,CAAE;AAAA,EAAA;AAAA,EAfhD;AAAA,EACA;AAAA,EACA;AAAA,EA4BA;AAAA,EAyBA;AAAA,EAOA;AAAA,EAmEA;AAAA,EAkDA;AAAA,EAOA;AAAA,EAOA;AAAA,EAaA;AAqBF;AA6CO,SAAS,cACd,IACA,gBACA;AACA,QAAM,iBAAiB,IAAI,eAAe,IAAI,cAAc;AAC5D,SAAO,eAAe;AACxB;;;"} |
@@ -0,2 +1,45 @@ | ||
| import { Store } from '@tanstack/store'; | ||
| import { AnyAsyncFunction } from './types.cjs'; | ||
| export interface AsyncThrottlerState<TFn extends AnyAsyncFunction> { | ||
| /** | ||
| * Number of function executions that have resulted in errors | ||
| */ | ||
| errorCount: number; | ||
| /** | ||
| * Whether the throttled function is currently executing asynchronously | ||
| */ | ||
| isExecuting: boolean; | ||
| /** | ||
| * Whether the throttler is waiting for the timeout to trigger execution | ||
| */ | ||
| isPending: boolean; | ||
| /** | ||
| * The arguments from the most recent call to maybeExecute | ||
| */ | ||
| lastArgs: Parameters<TFn> | undefined; | ||
| /** | ||
| * Timestamp of the last function execution in milliseconds | ||
| */ | ||
| lastExecutionTime: number; | ||
| /** | ||
| * The result from the most recent successful function execution | ||
| */ | ||
| lastResult: ReturnType<TFn> | undefined; | ||
| /** | ||
| * Timestamp when the next execution can occur in milliseconds | ||
| */ | ||
| nextExecutionTime: number; | ||
| /** | ||
| * Number of function executions that have completed (either successfully or with errors) | ||
| */ | ||
| settleCount: number; | ||
| /** | ||
| * Current execution status - 'idle' when not active, 'pending' when waiting, 'executing' when running, 'settled' when completed | ||
| */ | ||
| status: 'disabled' | 'idle' | 'pending' | 'executing' | 'settled'; | ||
| /** | ||
| * Number of function executions that have completed successfully | ||
| */ | ||
| successCount: number; | ||
| } | ||
| /** | ||
@@ -13,2 +56,6 @@ * Options for configuring an async throttled function | ||
| /** | ||
| * Initial state for the async throttler | ||
| */ | ||
| initialState?: Partial<AsyncThrottlerState<TFn>>; | ||
| /** | ||
| * Whether to execute the function immediately when called | ||
@@ -71,2 +118,12 @@ * Defaults to true | ||
| * | ||
| * State Management: | ||
| * - Uses TanStack Store for reactive state management | ||
| * - Use `initialState` to provide initial state values when creating the async throttler | ||
| * - Use `onSuccess` callback to react to successful function execution and implement custom logic | ||
| * - Use `onError` callback to react to function execution errors and implement custom error handling | ||
| * - Use `onSettled` callback to react to function execution completion (success or error) and implement custom logic | ||
| * - The state includes error count, execution status, last execution time, and success/settle counts | ||
| * - State can be accessed via `asyncThrottler.store.state` when using the class directly | ||
| * - When using framework adapters (React/Solid), state is accessed from `asyncThrottler.state` | ||
| * | ||
| * @example | ||
@@ -90,85 +147,46 @@ * ```ts | ||
| export declare class AsyncThrottler<TFn extends AnyAsyncFunction> { | ||
| #private; | ||
| private fn; | ||
| private _options; | ||
| private _abortController; | ||
| private _errorCount; | ||
| private _isExecuting; | ||
| private _lastArgs; | ||
| private _lastExecutionTime; | ||
| private _lastResult; | ||
| private _nextExecutionTime; | ||
| private _settleCount; | ||
| private _successCount; | ||
| private _timeoutId; | ||
| private _resolvePreviousPromise; | ||
| readonly store: Store<Readonly<AsyncThrottlerState<TFn>>>; | ||
| options: AsyncThrottlerOptions<TFn>; | ||
| constructor(fn: TFn, initialOptions: AsyncThrottlerOptions<TFn>); | ||
| /** | ||
| * Updates the throttler options | ||
| * Updates the async throttler options | ||
| */ | ||
| setOptions(newOptions: Partial<AsyncThrottlerOptions<TFn>>): void; | ||
| setOptions: (newOptions: Partial<AsyncThrottlerOptions<TFn>>) => void; | ||
| /** | ||
| * Returns the current options | ||
| */ | ||
| getOptions(): AsyncThrottlerOptions<TFn>; | ||
| /** | ||
| * Returns the current enabled state of the throttler | ||
| */ | ||
| getEnabled(): boolean; | ||
| /** | ||
| * Returns the current wait time in milliseconds | ||
| */ | ||
| getWait(): number; | ||
| /** | ||
| * Attempts to execute the throttled function. | ||
| * If a call is already in progress, it may be blocked or queued depending on the `wait` option. | ||
| * Attempts to execute the throttled function. The execution behavior depends on the throttler options: | ||
| * | ||
| * Error Handling: | ||
| * - If the throttled function throws and no `onError` handler is configured, | ||
| * the error will be thrown from this method. | ||
| * - If an `onError` handler is configured, errors will be caught and passed to the handler, | ||
| * and this method will return undefined. | ||
| * - The error state can be checked using `getErrorCount()` and `getIsExecuting()`. | ||
| * - If enough time has passed since the last execution (>= wait period): | ||
| * - With leading=true: Executes immediately | ||
| * - With leading=false: Waits for the next trailing execution | ||
| * | ||
| * @returns A promise that resolves with the function's return value, or undefined if an error occurred and was handled by onError | ||
| * @throws The error from the throttled function if no onError handler is configured | ||
| * - If within the wait period: | ||
| * - With trailing=true: Schedules execution for end of wait period | ||
| * - With trailing=false: Drops the execution | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * const throttled = new AsyncThrottler(fn, { wait: 1000 }); | ||
| * | ||
| * // First call executes immediately | ||
| * await throttled.maybeExecute('a', 'b'); | ||
| * | ||
| * // Call during wait period - gets throttled | ||
| * await throttled.maybeExecute('c', 'd'); | ||
| * ``` | ||
| */ | ||
| maybeExecute(...args: Parameters<TFn>): Promise<ReturnType<TFn> | undefined>; | ||
| private execute; | ||
| private resolvePreviousPromise; | ||
| maybeExecute: (...args: Parameters<TFn>) => Promise<ReturnType<TFn> | undefined>; | ||
| /** | ||
| * Cancels any pending execution or aborts any execution in progress | ||
| * Processes the current pending execution immediately | ||
| */ | ||
| cancel(): void; | ||
| flush: () => void; | ||
| /** | ||
| * Returns the last execution time | ||
| * Cancels any pending execution or aborts any execution in progress | ||
| */ | ||
| getLastExecutionTime(): number; | ||
| cancel: () => void; | ||
| /** | ||
| * Returns the next execution time | ||
| * Resets the debouncer state to its default values | ||
| */ | ||
| getNextExecutionTime(): number; | ||
| /** | ||
| * Returns the last result of the debounced function | ||
| */ | ||
| getLastResult(): ReturnType<TFn> | undefined; | ||
| /** | ||
| * Returns the number of times the function has been executed successfully | ||
| */ | ||
| getSuccessCount(): number; | ||
| /** | ||
| * Returns the number of times the function has settled (completed or errored) | ||
| */ | ||
| getSettleCount(): number; | ||
| /** | ||
| * Returns the number of times the function has errored | ||
| */ | ||
| getErrorCount(): number; | ||
| /** | ||
| * Returns the current pending state | ||
| */ | ||
| getIsPending(): boolean; | ||
| /** | ||
| * Returns the current executing state | ||
| */ | ||
| getIsExecuting(): boolean; | ||
| reset: () => void; | ||
| } | ||
@@ -191,2 +209,12 @@ /** | ||
| * | ||
| * State Management: | ||
| * - Uses TanStack Store for reactive state management | ||
| * - Use `initialState` to provide initial state values when creating the async throttler | ||
| * - Use `onSuccess` callback to react to successful function execution and implement custom logic | ||
| * - Use `onError` callback to react to function execution errors and implement custom error handling | ||
| * - Use `onSettled` callback to react to function execution completion (success or error) and implement custom logic | ||
| * - The state includes error count, execution status, last execution time, and success/settle counts | ||
| * - State can be accessed via the underlying AsyncThrottler instance's `store.state` property | ||
| * - When using framework adapters (React/Solid), state is accessed from the hook's state property | ||
| * | ||
| * @example | ||
@@ -193,0 +221,0 @@ * ```ts |
+110
-119
| "use strict"; | ||
| Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" }); | ||
| const store = require("@tanstack/store"); | ||
| const utils = require("./utils.cjs"); | ||
| function getDefaultBatcherState() { | ||
| return { | ||
| executionCount: 0, | ||
| isEmpty: true, | ||
| isPending: false, | ||
| isRunning: true, | ||
| totalItemsProcessed: 0, | ||
| items: [], | ||
| size: 0, | ||
| status: "idle" | ||
| }; | ||
| } | ||
| const defaultOptions = { | ||
@@ -12,125 +26,102 @@ getShouldExecute: () => false, | ||
| this.fn = fn; | ||
| this._batchExecutionCount = 0; | ||
| this._itemExecutionCount = 0; | ||
| this._items = []; | ||
| this._timeoutId = null; | ||
| this._options = { ...defaultOptions, ...initialOptions }; | ||
| this._running = this._options.started; | ||
| this.store = new store.Store( | ||
| getDefaultBatcherState() | ||
| ); | ||
| this.#timeoutId = null; | ||
| this.setOptions = (newOptions) => { | ||
| this.options = { ...this.options, ...newOptions }; | ||
| }; | ||
| this.#setState = (newState) => { | ||
| this.store.setState((state) => { | ||
| const combinedState = { | ||
| ...state, | ||
| ...newState | ||
| }; | ||
| const { isPending, items } = combinedState; | ||
| const size = items.length; | ||
| const isEmpty = size === 0; | ||
| return { | ||
| ...combinedState, | ||
| isEmpty, | ||
| size, | ||
| status: isPending ? "pending" : "idle" | ||
| }; | ||
| }); | ||
| }; | ||
| this.#getWait = () => { | ||
| return utils.parseFunctionOrValue(this.options.wait, this); | ||
| }; | ||
| this.addItem = (item) => { | ||
| this.#setState({ | ||
| items: [...this.store.state.items, item], | ||
| isPending: this.options.wait !== Infinity | ||
| }); | ||
| this.options.onItemsChange?.(this); | ||
| const shouldProcess = this.store.state.items.length >= this.options.maxSize || this.options.getShouldExecute(this.store.state.items, this); | ||
| if (shouldProcess) { | ||
| this.#execute(); | ||
| } else if (this.store.state.isRunning && this.options.wait !== Infinity) { | ||
| this.#clearTimeout(); | ||
| this.#timeoutId = setTimeout(() => this.#execute(), this.#getWait()); | ||
| } | ||
| }; | ||
| this.#execute = () => { | ||
| if (this.store.state.items.length === 0) { | ||
| return; | ||
| } | ||
| const batch2 = this.peekAllItems(); | ||
| this.clear(); | ||
| this.options.onItemsChange?.(this); | ||
| this.fn(batch2); | ||
| this.#setState({ | ||
| executionCount: this.store.state.executionCount + 1, | ||
| totalItemsProcessed: this.store.state.totalItemsProcessed + batch2.length | ||
| }); | ||
| this.options.onExecute?.(this); | ||
| }; | ||
| this.flush = () => { | ||
| this.#clearTimeout(); | ||
| this.#execute(); | ||
| }; | ||
| this.stop = () => { | ||
| this.#setState({ isRunning: false }); | ||
| this.#clearTimeout(); | ||
| }; | ||
| this.start = () => { | ||
| this.#setState({ isRunning: true }); | ||
| if (this.store.state.items.length > 0) { | ||
| this.#execute(); | ||
| } | ||
| }; | ||
| this.peekAllItems = () => { | ||
| return [...this.store.state.items]; | ||
| }; | ||
| this.#clearTimeout = () => { | ||
| if (this.#timeoutId) { | ||
| clearTimeout(this.#timeoutId); | ||
| this.#timeoutId = null; | ||
| } | ||
| }; | ||
| this.clear = () => { | ||
| this.#setState({ items: [], isPending: false }); | ||
| }; | ||
| this.reset = () => { | ||
| this.#setState(getDefaultBatcherState()); | ||
| this.options.onItemsChange?.(this); | ||
| }; | ||
| this.options = { | ||
| ...defaultOptions, | ||
| ...initialOptions | ||
| }; | ||
| this.#setState(this.options.initialState ?? {}); | ||
| } | ||
| /** | ||
| * Updates the batcher options | ||
| */ | ||
| setOptions(newOptions) { | ||
| this._options = { ...this._options, ...newOptions }; | ||
| } | ||
| /** | ||
| * Returns the current batcher options | ||
| */ | ||
| getOptions() { | ||
| return this._options; | ||
| } | ||
| /** | ||
| * Adds an item to the batcher | ||
| * If the batch size is reached, timeout occurs, or shouldProcess returns true, the batch will be processed | ||
| */ | ||
| addItem(item) { | ||
| var _a, _b; | ||
| this._items.push(item); | ||
| (_b = (_a = this._options).onItemsChange) == null ? void 0 : _b.call(_a, this); | ||
| const shouldProcess = this._items.length >= this._options.maxSize || this._options.getShouldExecute(this._items, this); | ||
| if (shouldProcess) { | ||
| this.execute(); | ||
| } else if (this._running && !this._timeoutId && this._options.wait !== Infinity) { | ||
| this._timeoutId = setTimeout(() => this.execute(), this._options.wait); | ||
| } | ||
| } | ||
| /** | ||
| * Processes the current batch of items. | ||
| * This method will automatically be triggered if the batcher is running and any of these conditions are met: | ||
| * - The number of items reaches batchSize | ||
| * - The wait duration has elapsed | ||
| * - The getShouldExecute function returns true upon adding an item | ||
| * | ||
| * You can also call this method manually to process the current batch at any time. | ||
| */ | ||
| execute() { | ||
| var _a, _b, _c, _d; | ||
| if (this._timeoutId) { | ||
| clearTimeout(this._timeoutId); | ||
| this._timeoutId = null; | ||
| } | ||
| if (this._items.length === 0) { | ||
| return; | ||
| } | ||
| const batch2 = this.peekAllItems(); | ||
| this._items = []; | ||
| (_b = (_a = this._options).onItemsChange) == null ? void 0 : _b.call(_a, this); | ||
| this.fn(batch2); | ||
| this._batchExecutionCount++; | ||
| this._itemExecutionCount += batch2.length; | ||
| (_d = (_c = this._options).onExecute) == null ? void 0 : _d.call(_c, this); | ||
| } | ||
| /** | ||
| * Stops the batcher from processing batches | ||
| */ | ||
| stop() { | ||
| var _a, _b; | ||
| this._running = false; | ||
| (_b = (_a = this._options).onIsRunningChange) == null ? void 0 : _b.call(_a, this); | ||
| if (this._timeoutId) { | ||
| clearTimeout(this._timeoutId); | ||
| this._timeoutId = null; | ||
| } | ||
| } | ||
| /** | ||
| * Starts the batcher and processes any pending items | ||
| */ | ||
| start() { | ||
| var _a, _b; | ||
| this._running = true; | ||
| (_b = (_a = this._options).onIsRunningChange) == null ? void 0 : _b.call(_a, this); | ||
| if (this._items.length > 0 && !this._timeoutId) { | ||
| this._timeoutId = setTimeout(() => this.execute(), this._options.wait); | ||
| } | ||
| } | ||
| /** | ||
| * Returns the current number of items in the batcher | ||
| */ | ||
| getSize() { | ||
| return this._items.length; | ||
| } | ||
| /** | ||
| * Returns true if the batcher is empty | ||
| */ | ||
| getIsEmpty() { | ||
| return this._items.length === 0; | ||
| } | ||
| /** | ||
| * Returns true if the batcher is running | ||
| */ | ||
| getIsRunning() { | ||
| return this._running; | ||
| } | ||
| /** | ||
| * Returns a copy of all items currently in the batcher | ||
| */ | ||
| peekAllItems() { | ||
| return [...this._items]; | ||
| } | ||
| /** | ||
| * Returns the number of times batches have been processed | ||
| */ | ||
| getBatchExecutionCount() { | ||
| return this._batchExecutionCount; | ||
| } | ||
| /** | ||
| * Returns the total number of individual items that have been processed | ||
| */ | ||
| getItemExecutionCount() { | ||
| return this._itemExecutionCount; | ||
| } | ||
| #timeoutId; | ||
| #setState; | ||
| #getWait; | ||
| #execute; | ||
| #clearTimeout; | ||
| } | ||
| function batch(fn, options) { | ||
| const batcher = new Batcher(fn, options); | ||
| return batcher.addItem.bind(batcher); | ||
| return batcher.addItem; | ||
| } | ||
@@ -137,0 +128,0 @@ exports.Batcher = Batcher; |
@@ -1,1 +0,1 @@ | ||
| {"version":3,"file":"batcher.cjs","sources":["../../src/batcher.ts"],"sourcesContent":["import type { OptionalKeys } from './types'\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 * 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?: (batcher: Batcher<TValue>) => void\n /**\n * Callback fired when the batcher's running state changes\n */\n onIsRunningChange?: (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\n}\n\ntype BatcherOptionsWithOptionalCallbacks<TValue> = OptionalKeys<\n Required<BatcherOptions<TValue>>,\n 'onExecute' | 'onItemsChange' | 'onIsRunningChange'\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 *\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 * @example\n * ```ts\n * const batcher = new Batcher<number>(\n * (items) => console.log('Processing batch:', items),\n * {\n * maxSize: 5,\n * wait: 2000,\n * onExecuteBatch: (items) => console.log('Batch executed:', items)\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.execute() // manually trigger a batch\n * ```\n */\nexport class Batcher<TValue> {\n private _options: BatcherOptionsWithOptionalCallbacks<TValue>\n private _batchExecutionCount = 0\n private _itemExecutionCount = 0\n private _items: Array<TValue> = []\n private _running: boolean\n private _timeoutId: NodeJS.Timeout | null = null\n\n constructor(\n private fn: (items: Array<TValue>) => void,\n initialOptions: BatcherOptions<TValue>,\n ) {\n this._options = { ...defaultOptions, ...initialOptions }\n this._running = this._options.started\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 /**\n * Returns the current batcher options\n */\n getOptions(): BatcherOptions<TValue> {\n return this._options\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._items.push(item)\n this._options.onItemsChange?.(this)\n\n const shouldProcess =\n this._items.length >= this._options.maxSize ||\n this._options.getShouldExecute(this._items, this)\n\n if (shouldProcess) {\n this.execute()\n } else if (\n this._running &&\n !this._timeoutId &&\n this._options.wait !== Infinity\n ) {\n this._timeoutId = setTimeout(() => this.execute(), this._options.wait)\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._timeoutId) {\n clearTimeout(this._timeoutId)\n this._timeoutId = null\n }\n\n if (this._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._items = [] // 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)\n this._batchExecutionCount++\n this._itemExecutionCount += batch.length\n this._options.onExecute?.(this)\n }\n\n /**\n * Stops the batcher from processing batches\n */\n stop(): void {\n this._running = false\n this._options.onIsRunningChange?.(this)\n if (this._timeoutId) {\n clearTimeout(this._timeoutId)\n this._timeoutId = null\n }\n }\n\n /**\n * Starts the batcher and processes any pending items\n */\n start(): void {\n this._running = true\n this._options.onIsRunningChange?.(this)\n if (this._items.length > 0 && !this._timeoutId) {\n this._timeoutId = setTimeout(() => this.execute(), this._options.wait)\n }\n }\n\n /**\n * Returns the current number of items in the batcher\n */\n getSize(): number {\n return this._items.length\n }\n\n /**\n * Returns true if the batcher is empty\n */\n getIsEmpty(): boolean {\n return this._items.length === 0\n }\n\n /**\n * Returns true if the batcher is running\n */\n getIsRunning(): boolean {\n return this._running\n }\n\n /**\n * Returns a copy of all items currently in the batcher\n */\n peekAllItems(): Array<TValue> {\n return [...this._items]\n }\n\n /**\n * Returns the number of times batches have been processed\n */\n getBatchExecutionCount(): number {\n return this._batchExecutionCount\n }\n\n /**\n * Returns the total number of individual items that have been processed\n */\n getItemExecutionCount(): number {\n return this._itemExecutionCount\n }\n}\n\n/**\n * Creates a batcher that processes items in batches\n *\n * @example\n * ```ts\n * const batchItems = batch<number>({\n * batchSize: 3,\n * processBatch: (items) => console.log('Processing:', items)\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.bind(batcher)\n}\n"],"names":["batch"],"mappings":";;AA+CA,MAAM,iBAA2D;AAAA,EAC/D,kBAAkB,MAAM;AAAA,EACxB,SAAS;AAAA,EACT,SAAS;AAAA,EACT,MAAM;AACR;AA+BO,MAAM,QAAgB;AAAA,EAQ3B,YACU,IACR,gBACA;AAFQ,SAAA,KAAA;AAPV,SAAQ,uBAAuB;AAC/B,SAAQ,sBAAsB;AAC9B,SAAQ,SAAwB,CAAC;AAEjC,SAAQ,aAAoC;AAM1C,SAAK,WAAW,EAAE,GAAG,gBAAgB,GAAG,eAAe;AAClD,SAAA,WAAW,KAAK,SAAS;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA,EAMhC,WAAW,YAAmD;AAC5D,SAAK,WAAW,EAAE,GAAG,KAAK,UAAU,GAAG,WAAW;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA,EAMpD,aAAqC;AACnC,WAAO,KAAK;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOd,QAAQ,MAAoB;;AACrB,SAAA,OAAO,KAAK,IAAI;AAChB,qBAAA,UAAS,kBAAT,4BAAyB;AAE9B,UAAM,gBACJ,KAAK,OAAO,UAAU,KAAK,SAAS,WACpC,KAAK,SAAS,iBAAiB,KAAK,QAAQ,IAAI;AAElD,QAAI,eAAe;AACjB,WAAK,QAAQ;AAAA,IAAA,WAEb,KAAK,YACL,CAAC,KAAK,cACN,KAAK,SAAS,SAAS,UACvB;AACK,WAAA,aAAa,WAAW,MAAM,KAAK,WAAW,KAAK,SAAS,IAAI;AAAA,IAAA;AAAA,EACvE;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAYF,UAAgB;;AACd,QAAI,KAAK,YAAY;AACnB,mBAAa,KAAK,UAAU;AAC5B,WAAK,aAAa;AAAA,IAAA;AAGhB,QAAA,KAAK,OAAO,WAAW,GAAG;AAC5B;AAAA,IAAA;AAGIA,UAAAA,SAAQ,KAAK,aAAa;AAChC,SAAK,SAAS,CAAC;AACV,qBAAA,UAAS,kBAAT,4BAAyB;AAE9B,SAAK,GAAGA,MAAK;AACR,SAAA;AACL,SAAK,uBAAuBA,OAAM;AAC7B,qBAAA,UAAS,cAAT,4BAAqB;AAAA,EAAI;AAAA;AAAA;AAAA;AAAA,EAMhC,OAAa;;AACX,SAAK,WAAW;AACX,qBAAA,UAAS,sBAAT,4BAA6B;AAClC,QAAI,KAAK,YAAY;AACnB,mBAAa,KAAK,UAAU;AAC5B,WAAK,aAAa;AAAA,IAAA;AAAA,EACpB;AAAA;AAAA;AAAA;AAAA,EAMF,QAAc;;AACZ,SAAK,WAAW;AACX,qBAAA,UAAS,sBAAT,4BAA6B;AAClC,QAAI,KAAK,OAAO,SAAS,KAAK,CAAC,KAAK,YAAY;AACzC,WAAA,aAAa,WAAW,MAAM,KAAK,WAAW,KAAK,SAAS,IAAI;AAAA,IAAA;AAAA,EACvE;AAAA;AAAA;AAAA;AAAA,EAMF,UAAkB;AAChB,WAAO,KAAK,OAAO;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA,EAMrB,aAAsB;AACb,WAAA,KAAK,OAAO,WAAW;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA,EAMhC,eAAwB;AACtB,WAAO,KAAK;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA,EAMd,eAA8B;AACrB,WAAA,CAAC,GAAG,KAAK,MAAM;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA,EAMxB,yBAAiC;AAC/B,WAAO,KAAK;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA,EAMd,wBAAgC;AAC9B,WAAO,KAAK;AAAA,EAAA;AAEhB;AAiBgB,SAAA,MACd,IACA,SACA;AACA,QAAM,UAAU,IAAI,QAAgB,IAAI,OAAO;AACxC,SAAA,QAAQ,QAAQ,KAAK,OAAO;AACrC;;;"} | ||
| {"version":3,"file":"batcher.cjs","sources":["../../src/batcher.ts"],"sourcesContent":["import { Store } from '@tanstack/store'\nimport { parseFunctionOrValue } from './utils'\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 * Whether the batcher is active and will process items automatically\n */\n isRunning: boolean\n /**\n * Total number of items that have been processed across all batches\n */\n totalItemsProcessed: number\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\nfunction getDefaultBatcherState<TValue>(): BatcherState<TValue> {\n return {\n executionCount: 0,\n isEmpty: true,\n isPending: false,\n isRunning: true,\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 * 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?: (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'\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 *\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: (batcher) => console.log('Batch executed:', batcher.peekAllItems())\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 options: BatcherOptionsWithOptionalCallbacks<TValue>\n #timeoutId: NodeJS.Timeout | null = null\n\n constructor(\n private fn: (items: Array<TValue>) => void,\n initialOptions: BatcherOptions<TValue>,\n ) {\n this.options = {\n ...defaultOptions,\n ...initialOptions,\n }\n this.#setState(this.options.initialState ?? {})\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 }\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.store.state.isRunning && 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?.(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 * Stops the batcher from processing batches\n */\n stop = (): void => {\n this.#setState({ isRunning: false })\n this.#clearTimeout()\n }\n\n /**\n * Starts the batcher and processes any pending items\n */\n start = (): void => {\n this.#setState({ isRunning: true })\n if (this.store.state.items.length > 0) {\n this.#execute()\n }\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 * 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 * @example\n * ```ts\n * const batchItems = batch<number>(\n * (items) => console.log('Processing:', items),\n * {\n * maxSize: 3,\n * onExecute: (batcher) => console.log('Batch executed')\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"],"names":["Store","parseFunctionOrValue","batch"],"mappings":"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"} |
+76
-51
@@ -0,1 +1,37 @@ | ||
| import { Store } from '@tanstack/store'; | ||
| import { OptionalKeys } from './types.cjs'; | ||
| export interface BatcherState<TValue> { | ||
| /** | ||
| * Number of batch executions that have been completed | ||
| */ | ||
| executionCount: number; | ||
| /** | ||
| * Whether the batcher has no items to process (items array is empty) | ||
| */ | ||
| isEmpty: boolean; | ||
| /** | ||
| * Whether the batcher is waiting for the timeout to trigger batch processing | ||
| */ | ||
| isPending: boolean; | ||
| /** | ||
| * Whether the batcher is active and will process items automatically | ||
| */ | ||
| isRunning: boolean; | ||
| /** | ||
| * Total number of items that have been processed across all batches | ||
| */ | ||
| totalItemsProcessed: number; | ||
| /** | ||
| * Array of items currently queued for batch processing | ||
| */ | ||
| items: Array<TValue>; | ||
| /** | ||
| * Number of items currently in the batch queue | ||
| */ | ||
| size: number; | ||
| /** | ||
| * Current processing status - 'idle' when not processing, 'pending' when waiting for timeout | ||
| */ | ||
| status: 'idle' | 'pending'; | ||
| } | ||
| /** | ||
@@ -11,2 +47,6 @@ * Options for configuring a Batcher instance | ||
| /** | ||
| * Initial state for the batcher | ||
| */ | ||
| initialState?: Partial<BatcherState<TValue>>; | ||
| /** | ||
| * Maximum number of items in a batch | ||
@@ -21,6 +61,2 @@ * @default Infinity | ||
| /** | ||
| * Callback fired when the batcher's running state changes | ||
| */ | ||
| onIsRunningChange?: (batcher: Batcher<TValue>) => void; | ||
| /** | ||
| * Callback fired after items are added to the batcher | ||
@@ -40,4 +76,5 @@ */ | ||
| */ | ||
| wait?: number; | ||
| wait?: number | ((batcher: Batcher<TValue>) => number); | ||
| } | ||
| type BatcherOptionsWithOptionalCallbacks<TValue> = OptionalKeys<Required<BatcherOptions<TValue>>, 'initialState' | 'onExecute' | 'onItemsChange'>; | ||
| /** | ||
@@ -54,2 +91,11 @@ * A class that collects items and processes them in batches. | ||
| * | ||
| * State Management: | ||
| * - Uses TanStack Store for reactive state management | ||
| * - Use `initialState` to provide initial state values when creating the batcher | ||
| * - Use `onExecute` callback to react to batch execution and implement custom logic | ||
| * - Use `onItemsChange` callback to react to items being added or removed from the batcher | ||
| * - The state includes batch execution count, total items processed, items, and running status | ||
| * - State can be accessed via `batcher.store.state` when using the class directly | ||
| * - When using framework adapters (React/Solid), state is accessed from `batcher.state` | ||
| * | ||
| * @example | ||
@@ -62,3 +108,3 @@ * ```ts | ||
| * wait: 2000, | ||
| * onExecuteBatch: (items) => console.log('Batch executed:', items) | ||
| * onExecute: (batcher) => console.log('Batch executed:', batcher.peekAllItems()) | ||
| * } | ||
@@ -71,13 +117,10 @@ * ); | ||
| * // the batch will be processed | ||
| * // batcher.execute() // manually trigger a batch | ||
| * // batcher.flush() // manually trigger a batch | ||
| * ``` | ||
| */ | ||
| export declare class Batcher<TValue> { | ||
| #private; | ||
| private fn; | ||
| private _options; | ||
| private _batchExecutionCount; | ||
| private _itemExecutionCount; | ||
| private _items; | ||
| private _running; | ||
| private _timeoutId; | ||
| readonly store: Store<Readonly<BatcherState<TValue>>>; | ||
| options: BatcherOptionsWithOptionalCallbacks<TValue>; | ||
| constructor(fn: (items: Array<TValue>) => void, initialOptions: BatcherOptions<TValue>); | ||
@@ -87,54 +130,32 @@ /** | ||
| */ | ||
| setOptions(newOptions: Partial<BatcherOptions<TValue>>): void; | ||
| setOptions: (newOptions: Partial<BatcherOptions<TValue>>) => void; | ||
| /** | ||
| * Returns the current batcher options | ||
| */ | ||
| getOptions(): BatcherOptions<TValue>; | ||
| /** | ||
| * Adds an item to the batcher | ||
| * If the batch size is reached, timeout occurs, or shouldProcess returns true, the batch will be processed | ||
| */ | ||
| addItem(item: TValue): void; | ||
| addItem: (item: TValue) => void; | ||
| /** | ||
| * Processes the current batch of items. | ||
| * This method will automatically be triggered if the batcher is running and any of these conditions are met: | ||
| * - The number of items reaches batchSize | ||
| * - The wait duration has elapsed | ||
| * - The getShouldExecute function returns true upon adding an item | ||
| * | ||
| * You can also call this method manually to process the current batch at any time. | ||
| * Processes the current batch of items immediately | ||
| */ | ||
| execute(): void; | ||
| flush: () => void; | ||
| /** | ||
| * Stops the batcher from processing batches | ||
| */ | ||
| stop(): void; | ||
| stop: () => void; | ||
| /** | ||
| * Starts the batcher and processes any pending items | ||
| */ | ||
| start(): void; | ||
| start: () => void; | ||
| /** | ||
| * Returns the current number of items in the batcher | ||
| * Returns a copy of all items in the batcher | ||
| */ | ||
| getSize(): number; | ||
| peekAllItems: () => Array<TValue>; | ||
| /** | ||
| * Returns true if the batcher is empty | ||
| * Removes all items from the batcher | ||
| */ | ||
| getIsEmpty(): boolean; | ||
| clear: () => void; | ||
| /** | ||
| * Returns true if the batcher is running | ||
| * Resets the batcher state to its default values | ||
| */ | ||
| getIsRunning(): boolean; | ||
| /** | ||
| * Returns a copy of all items currently in the batcher | ||
| */ | ||
| peekAllItems(): Array<TValue>; | ||
| /** | ||
| * Returns the number of times batches have been processed | ||
| */ | ||
| getBatchExecutionCount(): number; | ||
| /** | ||
| * Returns the total number of individual items that have been processed | ||
| */ | ||
| getItemExecutionCount(): number; | ||
| reset: () => void; | ||
| } | ||
@@ -146,6 +167,9 @@ /** | ||
| * ```ts | ||
| * const batchItems = batch<number>({ | ||
| * batchSize: 3, | ||
| * processBatch: (items) => console.log('Processing:', items) | ||
| * }); | ||
| * const batchItems = batch<number>( | ||
| * (items) => console.log('Processing:', items), | ||
| * { | ||
| * maxSize: 3, | ||
| * onExecute: (batcher) => console.log('Batch executed') | ||
| * } | ||
| * ); | ||
| * | ||
@@ -158,1 +182,2 @@ * batchItems(1); | ||
| export declare function batch<TValue>(fn: (items: Array<TValue>) => void, options: BatcherOptions<TValue>): (item: TValue) => void; | ||
| export {}; |
+97
-85
| "use strict"; | ||
| Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" }); | ||
| const store = require("@tanstack/store"); | ||
| const utils = require("./utils.cjs"); | ||
| function getDefaultDebouncerState() { | ||
| return structuredClone({ | ||
| canLeadingExecute: true, | ||
| executionCount: 0, | ||
| isPending: false, | ||
| lastArgs: void 0, | ||
| status: "idle" | ||
| }); | ||
| } | ||
| const defaultOptions = { | ||
| enabled: true, | ||
| leading: false, | ||
| onExecute: () => { | ||
| }, | ||
| trailing: true, | ||
@@ -15,92 +23,96 @@ wait: 0 | ||
| this.fn = fn; | ||
| this._canLeadingExecute = true; | ||
| this._executionCount = 0; | ||
| this._isPending = false; | ||
| this._options = { | ||
| this.store = new store.Store( | ||
| getDefaultDebouncerState() | ||
| ); | ||
| this.setOptions = (newOptions) => { | ||
| this.options = { ...this.options, ...newOptions }; | ||
| if (!this.#getEnabled()) { | ||
| this.cancel(); | ||
| } | ||
| }; | ||
| this.#setState = (newState) => { | ||
| this.store.setState((state) => { | ||
| const combinedState = { | ||
| ...state, | ||
| ...newState | ||
| }; | ||
| const { isPending } = combinedState; | ||
| return { | ||
| ...combinedState, | ||
| status: !this.#getEnabled() ? "disabled" : isPending ? "pending" : "idle" | ||
| }; | ||
| }); | ||
| }; | ||
| this.#getEnabled = () => { | ||
| return !!utils.parseFunctionOrValue(this.options.enabled, this); | ||
| }; | ||
| this.#getWait = () => { | ||
| return utils.parseFunctionOrValue(this.options.wait, this); | ||
| }; | ||
| this.maybeExecute = (...args) => { | ||
| if (!this.#getEnabled()) return void 0; | ||
| let _didLeadingExecute = false; | ||
| if (this.options.leading && this.store.state.canLeadingExecute) { | ||
| this.#setState({ canLeadingExecute: false }); | ||
| _didLeadingExecute = true; | ||
| this.#execute(...args); | ||
| } | ||
| if (this.options.trailing) { | ||
| this.#setState({ isPending: true, lastArgs: args }); | ||
| } | ||
| if (this.#timeoutId) clearTimeout(this.#timeoutId); | ||
| this.#timeoutId = setTimeout(() => { | ||
| this.#setState({ canLeadingExecute: true }); | ||
| if (this.options.trailing && !_didLeadingExecute) { | ||
| this.#execute(...args); | ||
| } | ||
| }, this.#getWait()); | ||
| }; | ||
| this.#execute = (...args) => { | ||
| if (!this.#getEnabled()) return void 0; | ||
| this.fn(...args); | ||
| this.#setState({ | ||
| isPending: false, | ||
| executionCount: this.store.state.executionCount + 1 | ||
| }); | ||
| this.options.onExecute?.(this); | ||
| }; | ||
| this.flush = () => { | ||
| if (this.store.state.isPending && this.store.state.lastArgs) { | ||
| this.#clearTimeout(); | ||
| this.#execute(...this.store.state.lastArgs); | ||
| } | ||
| }; | ||
| this.#clearTimeout = () => { | ||
| if (this.#timeoutId) { | ||
| clearTimeout(this.#timeoutId); | ||
| this.#timeoutId = void 0; | ||
| } | ||
| }; | ||
| this.cancel = () => { | ||
| this.#clearTimeout(); | ||
| this.#setState({ | ||
| canLeadingExecute: true, | ||
| isPending: false | ||
| }); | ||
| }; | ||
| this.reset = () => { | ||
| this.#setState(getDefaultDebouncerState()); | ||
| }; | ||
| this.options = { | ||
| ...defaultOptions, | ||
| ...initialOptions | ||
| }; | ||
| this.#setState(this.options.initialState ?? {}); | ||
| } | ||
| /** | ||
| * Updates the debouncer options | ||
| */ | ||
| setOptions(newOptions) { | ||
| this._options = { ...this._options, ...newOptions }; | ||
| if (!this._options.enabled) { | ||
| this._isPending = false; | ||
| } | ||
| } | ||
| /** | ||
| * Returns the current debouncer options | ||
| */ | ||
| getOptions() { | ||
| return this._options; | ||
| } | ||
| /** | ||
| * Returns the current enabled state of the debouncer | ||
| */ | ||
| getEnabled() { | ||
| return utils.parseFunctionOrValue(this._options.enabled, this); | ||
| } | ||
| /** | ||
| * Returns the current wait time in milliseconds | ||
| */ | ||
| getWait() { | ||
| return utils.parseFunctionOrValue(this._options.wait, this); | ||
| } | ||
| /** | ||
| * Attempts to execute the debounced function | ||
| * If a call is already in progress, it will be queued | ||
| */ | ||
| maybeExecute(...args) { | ||
| let _didLeadingExecute = false; | ||
| if (this._options.leading && this._canLeadingExecute) { | ||
| this._canLeadingExecute = false; | ||
| _didLeadingExecute = true; | ||
| this.execute(...args); | ||
| } | ||
| if (this._options.trailing) { | ||
| this._isPending = true; | ||
| } | ||
| if (this._timeoutId) clearTimeout(this._timeoutId); | ||
| this._timeoutId = setTimeout(() => { | ||
| this._canLeadingExecute = true; | ||
| if (this._options.trailing && !_didLeadingExecute) { | ||
| this.execute(...args); | ||
| } | ||
| }, this.getWait()); | ||
| } | ||
| execute(...args) { | ||
| if (!this.getEnabled()) return void 0; | ||
| this.fn(...args); | ||
| this._isPending = false; | ||
| this._executionCount++; | ||
| this._options.onExecute(this); | ||
| } | ||
| /** | ||
| * Cancels any pending execution | ||
| */ | ||
| cancel() { | ||
| if (this._timeoutId) { | ||
| clearTimeout(this._timeoutId); | ||
| this._canLeadingExecute = true; | ||
| this._isPending = false; | ||
| } | ||
| } | ||
| /** | ||
| * Returns the number of times the function has been executed | ||
| */ | ||
| getExecutionCount() { | ||
| return this._executionCount; | ||
| } | ||
| /** | ||
| * Returns `true` if debouncing | ||
| */ | ||
| getIsPending() { | ||
| return this.getEnabled() && this._isPending; | ||
| } | ||
| #timeoutId; | ||
| #setState; | ||
| #getEnabled; | ||
| #getWait; | ||
| #execute; | ||
| #clearTimeout; | ||
| } | ||
| function debounce(fn, initialOptions) { | ||
| const debouncer = new Debouncer(fn, initialOptions); | ||
| return debouncer.maybeExecute.bind(debouncer); | ||
| return debouncer.maybeExecute; | ||
| } | ||
@@ -107,0 +119,0 @@ exports.Debouncer = Debouncer; |
@@ -1,1 +0,1 @@ | ||
| {"version":3,"file":"debouncer.cjs","sources":["../../src/debouncer.ts"],"sourcesContent":["import { parseFunctionOrValue } from './utils'\nimport type { AnyFunction } from './types'\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 * 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?: (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\nconst defaultOptions: Required<DebouncerOptions<any>> = {\n enabled: true,\n leading: false,\n onExecute: () => {},\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 *\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 * @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 private _canLeadingExecute = true\n private _executionCount = 0\n private _isPending = false\n private _options: Required<DebouncerOptions<TFn>>\n private _timeoutId: NodeJS.Timeout | undefined\n\n constructor(\n private fn: TFn,\n initialOptions: DebouncerOptions<TFn>,\n ) {\n this._options = {\n ...defaultOptions,\n ...initialOptions,\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 // End the pending state if the debouncer is disabled\n if (!this._options.enabled) {\n this._isPending = false\n }\n }\n\n /**\n * Returns the current debouncer options\n */\n getOptions(): Required<DebouncerOptions<TFn>> {\n return this._options\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 let _didLeadingExecute = false\n\n // Handle leading execution\n if (this._options.leading && this._canLeadingExecute) {\n this._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._isPending = true\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._canLeadingExecute = true\n if (this._options.trailing && !_didLeadingExecute) {\n this.execute(...args)\n }\n }, this.getWait())\n }\n\n private execute(...args: Parameters<TFn>): void {\n if (!this.getEnabled()) return undefined\n this.fn(...args) // EXECUTE!\n this._isPending = false\n this._executionCount++\n this._options.onExecute(this)\n }\n\n /**\n * Cancels any pending execution\n */\n cancel(): void {\n if (this._timeoutId) {\n clearTimeout(this._timeoutId)\n this._canLeadingExecute = true\n this._isPending = false\n }\n }\n\n /**\n * Returns the number of times the function has been executed\n */\n getExecutionCount(): number {\n return this._executionCount\n }\n\n /**\n * Returns `true` if debouncing\n */\n getIsPending(): boolean {\n return this.getEnabled() && this._isPending\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 the the simple function wrapper implementation pulled from the Debouncer class. If you need\n * more control over the debouncing behavior, use the Debouncer class directly.\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 * @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.bind(debouncer)\n}\n"],"names":["parseFunctionOrValue"],"mappings":";;;AAoCA,MAAM,iBAAkD;AAAA,EACtD,SAAS;AAAA,EACT,SAAS;AAAA,EACT,WAAW,MAAM;AAAA,EAAC;AAAA,EAClB,UAAU;AAAA,EACV,MAAM;AACR;AAyBO,MAAM,UAAmC;AAAA,EAO9C,YACU,IACR,gBACA;AAFQ,SAAA,KAAA;AAPV,SAAQ,qBAAqB;AAC7B,SAAQ,kBAAkB;AAC1B,SAAQ,aAAa;AAQnB,SAAK,WAAW;AAAA,MACd,GAAG;AAAA,MACH,GAAG;AAAA,IACL;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA,EAMF,WAAW,YAAkD;AAC3D,SAAK,WAAW,EAAE,GAAG,KAAK,UAAU,GAAG,WAAW;AAG9C,QAAA,CAAC,KAAK,SAAS,SAAS;AAC1B,WAAK,aAAa;AAAA,IAAA;AAAA,EACpB;AAAA;AAAA;AAAA;AAAA,EAMF,aAA8C;AAC5C,WAAO,KAAK;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA,EAMd,aAAsB;AACpB,WAAOA,MAAqB,qBAAA,KAAK,SAAS,SAAS,IAAI;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA,EAMzD,UAAkB;AAChB,WAAOA,MAAqB,qBAAA,KAAK,SAAS,MAAM,IAAI;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOtD,gBAAgB,MAA6B;AAC3C,QAAI,qBAAqB;AAGzB,QAAI,KAAK,SAAS,WAAW,KAAK,oBAAoB;AACpD,WAAK,qBAAqB;AACL,2BAAA;AAChB,WAAA,QAAQ,GAAG,IAAI;AAAA,IAAA;AAIlB,QAAA,KAAK,SAAS,UAAU;AAC1B,WAAK,aAAa;AAAA,IAAA;AAIpB,QAAI,KAAK,WAAyB,cAAA,KAAK,UAAU;AAG5C,SAAA,aAAa,WAAW,MAAM;AACjC,WAAK,qBAAqB;AAC1B,UAAI,KAAK,SAAS,YAAY,CAAC,oBAAoB;AAC5C,aAAA,QAAQ,GAAG,IAAI;AAAA,MAAA;AAAA,IACtB,GACC,KAAK,SAAS;AAAA,EAAA;AAAA,EAGX,WAAW,MAA6B;AAC9C,QAAI,CAAC,KAAK,WAAW,EAAU,QAAA;AAC1B,SAAA,GAAG,GAAG,IAAI;AACf,SAAK,aAAa;AACb,SAAA;AACA,SAAA,SAAS,UAAU,IAAI;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA,EAM9B,SAAe;AACb,QAAI,KAAK,YAAY;AACnB,mBAAa,KAAK,UAAU;AAC5B,WAAK,qBAAqB;AAC1B,WAAK,aAAa;AAAA,IAAA;AAAA,EACpB;AAAA;AAAA;AAAA;AAAA,EAMF,oBAA4B;AAC1B,WAAO,KAAK;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA,EAMd,eAAwB;AACf,WAAA,KAAK,gBAAgB,KAAK;AAAA,EAAA;AAErC;AAsBgB,SAAA,SACd,IACA,gBACoC;AACpC,QAAM,YAAY,IAAI,UAAU,IAAI,cAAc;AAC3C,SAAA,UAAU,aAAa,KAAK,SAAS;AAC9C;;;"} | ||
| {"version":3,"file":"debouncer.cjs","sources":["../../src/debouncer.ts"],"sourcesContent":["import { Store } from '@tanstack/store'\nimport { parseFunctionOrValue } from './utils'\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 * 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 structuredClone({\n canLeadingExecute: true,\n executionCount: 0,\n isPending: false,\n lastArgs: undefined,\n status: 'idle',\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 * 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?: (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\nconst defaultOptions: Omit<\n Required<DebouncerOptions<any>>,\n 'initialState' | 'onExecute'\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 *\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 options: DebouncerOptions<TFn>\n #timeoutId: NodeJS.Timeout | undefined\n\n constructor(\n private fn: TFn,\n initialOptions: DebouncerOptions<TFn>,\n ) {\n this.options = {\n ...defaultOptions,\n ...initialOptions,\n }\n this.#setState(this.options.initialState ?? {})\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 }\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 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 isPending: false,\n executionCount: this.store.state.executionCount + 1,\n })\n this.options.onExecute?.(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 the the simple function wrapper implementation pulled from the Debouncer class. If you need\n * more control over the debouncing behavior, use the Debouncer class directly.\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"],"names":["Store","parseFunctionOrValue"],"mappings":"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"} |
+54
-26
@@ -0,2 +1,25 @@ | ||
| import { Store } from '@tanstack/store'; | ||
| import { AnyFunction } from './types.cjs'; | ||
| export interface DebouncerState<TFn extends AnyFunction> { | ||
| /** | ||
| * Whether the debouncer can execute on the leading edge of the timeout | ||
| */ | ||
| canLeadingExecute: boolean; | ||
| /** | ||
| * Number of function executions that have been completed | ||
| */ | ||
| executionCount: number; | ||
| /** | ||
| * Whether the debouncer is waiting for the timeout to trigger execution | ||
| */ | ||
| isPending: boolean; | ||
| /** | ||
| * The arguments from the most recent call to maybeExecute | ||
| */ | ||
| lastArgs: Parameters<TFn> | undefined; | ||
| /** | ||
| * Current execution status - 'idle' when not active, 'pending' when waiting for timeout | ||
| */ | ||
| status: 'disabled' | 'idle' | 'pending'; | ||
| } | ||
| /** | ||
@@ -13,2 +36,6 @@ * Options for configuring a debounced function | ||
| /** | ||
| * Initial state for the debouncer | ||
| */ | ||
| initialState?: Partial<DebouncerState<TFn>>; | ||
| /** | ||
| * Whether to execute on the leading edge of the timeout. | ||
@@ -46,2 +73,10 @@ * The first call will execute immediately and the rest will wait the delay. | ||
| * | ||
| * State Management: | ||
| * - Uses TanStack Store for reactive state management | ||
| * - Use `initialState` to provide initial state values when creating the debouncer | ||
| * - Use `onExecute` callback to react to function execution and implement custom logic | ||
| * - The state includes canLeadingExecute, execution count, and isPending status | ||
| * - State can be accessed via `debouncer.store.state` when using the class directly | ||
| * - When using framework adapters (React/Solid), state is accessed from `debouncer.state` | ||
| * | ||
| * @example | ||
@@ -60,8 +95,6 @@ * ```ts | ||
| export declare class Debouncer<TFn extends AnyFunction> { | ||
| #private; | ||
| private fn; | ||
| private _canLeadingExecute; | ||
| private _executionCount; | ||
| private _isPending; | ||
| private _options; | ||
| private _timeoutId; | ||
| readonly store: Store<Readonly<DebouncerState<TFn>>>; | ||
| options: DebouncerOptions<TFn>; | ||
| constructor(fn: TFn, initialOptions: DebouncerOptions<TFn>); | ||
@@ -71,33 +104,20 @@ /** | ||
| */ | ||
| setOptions(newOptions: Partial<DebouncerOptions<TFn>>): void; | ||
| setOptions: (newOptions: Partial<DebouncerOptions<TFn>>) => void; | ||
| /** | ||
| * Returns the current debouncer options | ||
| */ | ||
| getOptions(): Required<DebouncerOptions<TFn>>; | ||
| /** | ||
| * Returns the current enabled state of the debouncer | ||
| */ | ||
| getEnabled(): boolean; | ||
| /** | ||
| * Returns the current wait time in milliseconds | ||
| */ | ||
| getWait(): number; | ||
| /** | ||
| * Attempts to execute the debounced function | ||
| * If a call is already in progress, it will be queued | ||
| */ | ||
| maybeExecute(...args: Parameters<TFn>): void; | ||
| private execute; | ||
| maybeExecute: (...args: Parameters<TFn>) => void; | ||
| /** | ||
| * Cancels any pending execution | ||
| * Processes the current pending execution immediately | ||
| */ | ||
| cancel(): void; | ||
| flush: () => void; | ||
| /** | ||
| * Returns the number of times the function has been executed | ||
| * Cancels any pending execution | ||
| */ | ||
| getExecutionCount(): number; | ||
| cancel: () => void; | ||
| /** | ||
| * Returns `true` if debouncing | ||
| * Resets the debouncer state to its default values | ||
| */ | ||
| getIsPending(): boolean; | ||
| reset: () => void; | ||
| } | ||
@@ -114,2 +134,10 @@ /** | ||
| * | ||
| * State Management: | ||
| * - Uses TanStack Store for reactive state management | ||
| * - Use `initialState` to provide initial state values when creating the debouncer | ||
| * - Use `onExecute` callback to react to function execution and implement custom logic | ||
| * - The state includes canLeadingExecute, execution count, and isPending status | ||
| * - State can be accessed via the underlying Debouncer instance's `store.state` property | ||
| * - When using framework adapters (React/Solid), state is accessed from the hook's state property | ||
| * | ||
| * @example | ||
@@ -116,0 +144,0 @@ * ```ts |
| "use strict"; | ||
| Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" }); | ||
| const asyncBatcher = require("./async-batcher.cjs"); | ||
| const asyncDebouncer = require("./async-debouncer.cjs"); | ||
@@ -8,3 +9,2 @@ const asyncQueuer = require("./async-queuer.cjs"); | ||
| const batcher = require("./batcher.cjs"); | ||
| const compare = require("./compare.cjs"); | ||
| const debouncer = require("./debouncer.cjs"); | ||
@@ -15,2 +15,4 @@ const queuer = require("./queuer.cjs"); | ||
| const utils = require("./utils.cjs"); | ||
| exports.AsyncBatcher = asyncBatcher.AsyncBatcher; | ||
| exports.asyncBatch = asyncBatcher.asyncBatch; | ||
| exports.AsyncDebouncer = asyncDebouncer.AsyncDebouncer; | ||
@@ -26,6 +28,2 @@ exports.asyncDebounce = asyncDebouncer.asyncDebounce; | ||
| exports.batch = batcher.batch; | ||
| exports.isPlainArray = compare.isPlainArray; | ||
| exports.isPlainObject = compare.isPlainObject; | ||
| exports.replaceEqualDeep = compare.replaceEqualDeep; | ||
| exports.shallowEqualObjects = compare.shallowEqualObjects; | ||
| exports.Debouncer = debouncer.Debouncer; | ||
@@ -39,5 +37,4 @@ exports.debounce = debouncer.debounce; | ||
| exports.throttle = throttler.throttle; | ||
| exports.bindInstanceMethods = utils.bindInstanceMethods; | ||
| exports.isFunction = utils.isFunction; | ||
| exports.parseFunctionOrValue = utils.parseFunctionOrValue; | ||
| //# sourceMappingURL=index.cjs.map |
@@ -1,1 +0,1 @@ | ||
| {"version":3,"file":"index.cjs","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"} | ||
| {"version":3,"file":"index.cjs","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"} |
@@ -0,1 +1,2 @@ | ||
| export * from './async-batcher.cjs'; | ||
| export * from './async-debouncer.cjs'; | ||
@@ -6,3 +7,2 @@ export * from './async-queuer.cjs'; | ||
| export * from './batcher.cjs'; | ||
| export * from './compare.cjs'; | ||
| export * from './debouncer.cjs'; | ||
@@ -9,0 +9,0 @@ export * from './queuer.cjs'; |
+246
-294
| "use strict"; | ||
| Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" }); | ||
| const store = require("@tanstack/store"); | ||
| const utils = require("./utils.cjs"); | ||
| function getDefaultQueuerState() { | ||
| return { | ||
| executionCount: 0, | ||
| expirationCount: 0, | ||
| isEmpty: true, | ||
| isFull: false, | ||
| isIdle: true, | ||
| isRunning: true, | ||
| itemTimestamps: [], | ||
| items: [], | ||
| pendingTick: false, | ||
| rejectionCount: 0, | ||
| size: 0, | ||
| status: "idle" | ||
| }; | ||
| } | ||
| const defaultOptions = { | ||
| addItemsTo: "back", | ||
| getItemsFrom: "front", | ||
| getPriority: (item) => (item == null ? void 0 : item.priority) ?? 0, | ||
| getPriority: (item) => item?.priority ?? 0, | ||
| getIsExpired: () => false, | ||
@@ -12,12 +29,2 @@ expirationDuration: Infinity, | ||
| maxSize: Infinity, | ||
| onExecute: () => { | ||
| }, | ||
| onIsRunningChange: () => { | ||
| }, | ||
| onItemsChange: () => { | ||
| }, | ||
| onReject: () => { | ||
| }, | ||
| onExpire: () => { | ||
| }, | ||
| started: true, | ||
@@ -29,297 +36,242 @@ wait: 0 | ||
| this.fn = fn; | ||
| this._items = []; | ||
| this._itemTimestamps = []; | ||
| this._executionCount = 0; | ||
| this._rejectionCount = 0; | ||
| this._expirationCount = 0; | ||
| this._onItemsChanges = []; | ||
| this._pendingTick = false; | ||
| this._options = { ...defaultOptions, ...initialOptions }; | ||
| this._running = this._options.started; | ||
| for (let i = 0; i < this._options.initialItems.length; i++) { | ||
| const item = this._options.initialItems[i]; | ||
| const isLast = i === this._options.initialItems.length - 1; | ||
| this.addItem(item, this._options.addItemsTo, isLast); | ||
| } | ||
| } | ||
| /** | ||
| * Updates the queuer options. New options are merged with existing options. | ||
| */ | ||
| setOptions(newOptions) { | ||
| this._options = { ...this._options, ...newOptions }; | ||
| } | ||
| /** | ||
| * Returns the current queuer options, including defaults and any overrides. | ||
| */ | ||
| getOptions() { | ||
| return this._options; | ||
| } | ||
| /** | ||
| * Returns the current wait time (in milliseconds) between processing items. | ||
| * If a function is provided, it is called with the queuer instance. | ||
| */ | ||
| getWait() { | ||
| return utils.parseFunctionOrValue(this._options.wait, this); | ||
| } | ||
| /** | ||
| * Processes items in the queue up to the wait interval. Internal use only. | ||
| */ | ||
| tick() { | ||
| if (!this._running) { | ||
| this._pendingTick = false; | ||
| return; | ||
| } | ||
| this.checkExpiredItems(); | ||
| while (!this.getIsEmpty()) { | ||
| const nextItem = this.execute(this._options.getItemsFrom); | ||
| if (nextItem === void 0) { | ||
| break; | ||
| } | ||
| this._onItemsChanges.forEach((cb) => cb(nextItem)); | ||
| const wait = this.getWait(); | ||
| if (wait > 0) { | ||
| setTimeout(() => this.tick(), wait); | ||
| this.store = new store.Store( | ||
| getDefaultQueuerState() | ||
| ); | ||
| this.#timeoutId = null; | ||
| this.setOptions = (newOptions) => { | ||
| this.options = { ...this.options, ...newOptions }; | ||
| }; | ||
| this.#setState = (newState) => { | ||
| this.store.setState((state) => { | ||
| const combinedState = { | ||
| ...state, | ||
| ...newState | ||
| }; | ||
| const { items, isRunning } = combinedState; | ||
| const size = items.length; | ||
| const isFull = size >= (this.options.maxSize ?? Infinity); | ||
| const isEmpty = size === 0; | ||
| const isIdle = isRunning && isEmpty; | ||
| const status = isIdle ? "idle" : isRunning ? "running" : "stopped"; | ||
| return { | ||
| ...combinedState, | ||
| isEmpty, | ||
| isFull, | ||
| isIdle, | ||
| size, | ||
| status | ||
| }; | ||
| }); | ||
| }; | ||
| this.#getWait = () => { | ||
| return utils.parseFunctionOrValue(this.options.wait ?? 0, this); | ||
| }; | ||
| this.#tick = () => { | ||
| if (!this.store.state.isRunning) { | ||
| this.#setState({ pendingTick: false }); | ||
| return; | ||
| } | ||
| this.tick(); | ||
| } | ||
| this._pendingTick = false; | ||
| } | ||
| /** | ||
| * Checks for expired items in the queue and removes them. Calls onExpire for each expired item. | ||
| * Internal use only. | ||
| */ | ||
| checkExpiredItems() { | ||
| if (this._options.expirationDuration === Infinity && this._options.getIsExpired === defaultOptions.getIsExpired) | ||
| return; | ||
| const now = Date.now(); | ||
| const expiredIndices = []; | ||
| for (let i = 0; i < this._items.length; i++) { | ||
| const timestamp = this._itemTimestamps[i]; | ||
| if (timestamp === void 0) continue; | ||
| const item = this._items[i]; | ||
| if (item === void 0) continue; | ||
| const isExpired = this._options.getIsExpired !== defaultOptions.getIsExpired ? this._options.getIsExpired(item, timestamp) : now - timestamp > this._options.expirationDuration; | ||
| if (isExpired) { | ||
| expiredIndices.push(i); | ||
| this.#setState({ pendingTick: true }); | ||
| this.#checkExpiredItems(); | ||
| while (!this.store.state.isEmpty) { | ||
| const nextItem = this.execute(this.options.getItemsFrom ?? "front"); | ||
| if (nextItem === void 0) { | ||
| break; | ||
| } | ||
| const wait = this.#getWait(); | ||
| if (wait > 0) { | ||
| this.#timeoutId = setTimeout(() => this.#tick(), wait); | ||
| return; | ||
| } | ||
| this.#tick(); | ||
| } | ||
| } | ||
| for (let i = expiredIndices.length - 1; i >= 0; i--) { | ||
| const index = expiredIndices[i]; | ||
| if (index === void 0) continue; | ||
| const expiredItem = this._items[index]; | ||
| if (expiredItem === void 0) continue; | ||
| this._items.splice(index, 1); | ||
| this._itemTimestamps.splice(index, 1); | ||
| this._expirationCount++; | ||
| this._options.onExpire(expiredItem, this); | ||
| } | ||
| if (expiredIndices.length > 0) { | ||
| this._options.onItemsChange(this); | ||
| } | ||
| } | ||
| /** | ||
| * Stops processing items in the queue. Does not clear the queue. | ||
| */ | ||
| stop() { | ||
| this._running = false; | ||
| this._pendingTick = false; | ||
| this._options.onIsRunningChange(this); | ||
| } | ||
| /** | ||
| * Starts processing items in the queue. If already running, does nothing. | ||
| */ | ||
| start() { | ||
| this._running = true; | ||
| if (!this._pendingTick && !this.getIsEmpty()) { | ||
| this._pendingTick = true; | ||
| this.tick(); | ||
| } | ||
| this._options.onIsRunningChange(this); | ||
| } | ||
| /** | ||
| * Removes all pending items from the queue. Does not affect items being processed. | ||
| */ | ||
| clear() { | ||
| this._items = []; | ||
| this._options.onItemsChange(this); | ||
| } | ||
| /** | ||
| * Resets the queuer to its initial state. Optionally repopulates with initial items. | ||
| * Does not affect callbacks or options. | ||
| */ | ||
| reset(withInitialItems) { | ||
| this.clear(); | ||
| this._executionCount = 0; | ||
| if (withInitialItems) { | ||
| this._items = [...this._options.initialItems]; | ||
| } | ||
| this._running = this._options.started; | ||
| } | ||
| /** | ||
| * Adds an item to the queue. If the queue is full, the item is rejected and onReject is called. | ||
| * Items can be inserted based on priority or at the front/back depending on configuration. | ||
| * | ||
| * Returns true if the item was added, false if the queue is full. | ||
| * | ||
| * Example usage: | ||
| * ```ts | ||
| * queuer.addItem('task'); | ||
| * queuer.addItem('task2', 'front'); | ||
| * ``` | ||
| */ | ||
| addItem(item, position = this._options.addItemsTo, runOnUpdate = true) { | ||
| if (this.getIsFull()) { | ||
| this._rejectionCount++; | ||
| this._options.onReject(item, this); | ||
| return false; | ||
| } | ||
| if (this._options.getPriority !== defaultOptions.getPriority) { | ||
| const priority = this._options.getPriority(item); | ||
| const insertIndex = this._items.findIndex( | ||
| (existing) => this._options.getPriority(existing) < priority | ||
| ); | ||
| if (insertIndex === -1) { | ||
| this._items.push(item); | ||
| this._itemTimestamps.push(Date.now()); | ||
| this.#setState({ pendingTick: false }); | ||
| }; | ||
| this.addItem = (item, position = this.options.addItemsTo ?? "back", runOnItemsChange = true) => { | ||
| if (this.store.state.isFull) { | ||
| this.#setState({ | ||
| rejectionCount: this.store.state.rejectionCount + 1 | ||
| }); | ||
| this.options.onReject?.(item, this); | ||
| return false; | ||
| } | ||
| const priority = this.options.getPriority !== defaultOptions.getPriority ? this.options.getPriority(item) : item.priority; | ||
| const items = this.store.state.items; | ||
| const itemTimestamps = this.store.state.itemTimestamps; | ||
| if (priority !== void 0) { | ||
| const insertIndex = items.findIndex((existing) => { | ||
| const existingPriority = this.options.getPriority !== defaultOptions.getPriority ? this.options.getPriority(existing) : existing.priority; | ||
| return existingPriority < priority; | ||
| }); | ||
| if (insertIndex === -1) { | ||
| items.push(item); | ||
| itemTimestamps.push(Date.now()); | ||
| } else { | ||
| items.splice(insertIndex, 0, item); | ||
| itemTimestamps.splice(insertIndex, 0, Date.now()); | ||
| } | ||
| } else { | ||
| this._items.splice(insertIndex, 0, item); | ||
| this._itemTimestamps.splice(insertIndex, 0, Date.now()); | ||
| if (position === "front") { | ||
| items.unshift(item); | ||
| itemTimestamps.unshift(Date.now()); | ||
| } else { | ||
| items.push(item); | ||
| itemTimestamps.push(Date.now()); | ||
| } | ||
| } | ||
| } else { | ||
| this.#setState({ | ||
| items, | ||
| itemTimestamps | ||
| }); | ||
| if (runOnItemsChange) { | ||
| this.options.onItemsChange?.(this); | ||
| } | ||
| if (this.store.state.isRunning && !this.store.state.pendingTick) { | ||
| this.#setState({ pendingTick: true }); | ||
| this.#tick(); | ||
| } | ||
| return true; | ||
| }; | ||
| this.getNextItem = (position = this.options.getItemsFrom ?? "front") => { | ||
| const { items, itemTimestamps } = this.store.state; | ||
| let item; | ||
| if (position === "front") { | ||
| this._items.unshift(item); | ||
| this._itemTimestamps.unshift(Date.now()); | ||
| item = items[0]; | ||
| if (item !== void 0) { | ||
| this.#setState({ | ||
| items: items.slice(1), | ||
| itemTimestamps: itemTimestamps.slice(1) | ||
| }); | ||
| } | ||
| } else { | ||
| this._items.push(item); | ||
| this._itemTimestamps.push(Date.now()); | ||
| item = items[items.length - 1]; | ||
| if (item !== void 0) { | ||
| this.#setState({ | ||
| items: items.slice(0, -1), | ||
| itemTimestamps: itemTimestamps.slice(0, -1) | ||
| }); | ||
| } | ||
| } | ||
| } | ||
| if (this._running && !this._pendingTick) { | ||
| this._pendingTick = true; | ||
| this.tick(); | ||
| } | ||
| if (runOnUpdate) { | ||
| this._options.onItemsChange(this); | ||
| } | ||
| return true; | ||
| } | ||
| /** | ||
| * Removes and returns the next item from the queue without executing the function. | ||
| * Use for manual queue management. Normally, use execute() to process items. | ||
| * | ||
| * Example usage: | ||
| * ```ts | ||
| * // FIFO | ||
| * queuer.getNextItem(); | ||
| * // LIFO | ||
| * queuer.getNextItem('back'); | ||
| * ``` | ||
| */ | ||
| getNextItem(position = this._options.getItemsFrom) { | ||
| let item; | ||
| if (position === "front") { | ||
| item = this._items.shift(); | ||
| this._itemTimestamps.shift(); | ||
| if (item !== void 0) { | ||
| this.options.onItemsChange?.(this); | ||
| } | ||
| return item; | ||
| }; | ||
| this.execute = (position) => { | ||
| const item = this.getNextItem(position); | ||
| if (item !== void 0) { | ||
| this.fn(item); | ||
| this.#setState({ | ||
| executionCount: this.store.state.executionCount + 1 | ||
| }); | ||
| this.options.onExecute?.(item, this); | ||
| } | ||
| return item; | ||
| }; | ||
| this.flush = (numberOfItems = this.store.state.items.length, position) => { | ||
| this.#clearTimeout(); | ||
| for (let i = 0; i < numberOfItems; i++) { | ||
| this.execute(position); | ||
| } | ||
| }; | ||
| this.#checkExpiredItems = () => { | ||
| if ((this.options.expirationDuration ?? Infinity) === Infinity && this.options.getIsExpired === defaultOptions.getIsExpired) { | ||
| return; | ||
| } | ||
| const now = Date.now(); | ||
| const expiredIndices = []; | ||
| for (let i = 0; i < this.store.state.items.length; i++) { | ||
| const timestamp = this.store.state.itemTimestamps[i]; | ||
| if (timestamp === void 0) continue; | ||
| const item = this.store.state.items[i]; | ||
| if (item === void 0) continue; | ||
| const isExpired = this.options.getIsExpired !== defaultOptions.getIsExpired ? this.options.getIsExpired(item, timestamp) : now - timestamp > (this.options.expirationDuration ?? Infinity); | ||
| if (isExpired) { | ||
| expiredIndices.push(i); | ||
| } | ||
| } | ||
| for (let i = expiredIndices.length - 1; i >= 0; i--) { | ||
| const index = expiredIndices[i]; | ||
| if (index === void 0) continue; | ||
| const expiredItem = this.store.state.items[index]; | ||
| if (expiredItem === void 0) continue; | ||
| const newItems = [...this.store.state.items]; | ||
| const newTimestamps = [...this.store.state.itemTimestamps]; | ||
| newItems.splice(index, 1); | ||
| newTimestamps.splice(index, 1); | ||
| this.#setState({ | ||
| items: newItems, | ||
| itemTimestamps: newTimestamps, | ||
| expirationCount: this.store.state.expirationCount + 1 | ||
| }); | ||
| this.options.onExpire?.(expiredItem, this); | ||
| } | ||
| if (expiredIndices.length > 0) { | ||
| this.options.onItemsChange?.(this); | ||
| } | ||
| }; | ||
| this.peekNextItem = (position = "front") => { | ||
| if (position === "front") { | ||
| return this.store.state.items[0]; | ||
| } | ||
| return this.store.state.items[this.store.state.size - 1]; | ||
| }; | ||
| this.peekAllItems = () => { | ||
| return [...this.store.state.items]; | ||
| }; | ||
| this.start = () => { | ||
| this.#setState({ isRunning: true }); | ||
| if (!this.store.state.pendingTick && !this.store.state.isEmpty) { | ||
| this.#tick(); | ||
| } | ||
| }; | ||
| this.stop = () => { | ||
| this.#clearTimeout(); | ||
| this.#setState({ isRunning: false, pendingTick: false }); | ||
| }; | ||
| this.#clearTimeout = () => { | ||
| if (this.#timeoutId) { | ||
| clearTimeout(this.#timeoutId); | ||
| this.#timeoutId = null; | ||
| } | ||
| }; | ||
| this.clear = () => { | ||
| this.#setState({ items: [], itemTimestamps: [] }); | ||
| this.options.onItemsChange?.(this); | ||
| }; | ||
| this.reset = () => { | ||
| this.#setState(getDefaultQueuerState()); | ||
| this.options.onItemsChange?.(this); | ||
| }; | ||
| this.options = { | ||
| ...defaultOptions, | ||
| ...initialOptions | ||
| }; | ||
| const isInitiallyRunning = this.options.initialState?.isRunning ?? this.options.started ?? true; | ||
| this.#setState({ | ||
| ...this.options.initialState, | ||
| isRunning: isInitiallyRunning | ||
| }); | ||
| if (this.options.initialState?.items) { | ||
| if (this.store.state.isRunning) { | ||
| this.#tick(); | ||
| } | ||
| } else { | ||
| item = this._items.pop(); | ||
| this._itemTimestamps.pop(); | ||
| for (let i = 0; i < (this.options.initialItems?.length ?? 0); i++) { | ||
| const item = this.options.initialItems[i]; | ||
| const isLast = i === (this.options.initialItems?.length ?? 0) - 1; | ||
| this.addItem(item, this.options.addItemsTo ?? "back", isLast); | ||
| } | ||
| } | ||
| if (item !== void 0) { | ||
| this._options.onItemsChange(this); | ||
| } | ||
| return item; | ||
| } | ||
| /** | ||
| * Removes and returns the next item from the queue and processes it using the provided function. | ||
| * | ||
| * Example usage: | ||
| * ```ts | ||
| * queuer.execute(); | ||
| * // LIFO | ||
| * queuer.execute('back'); | ||
| * ``` | ||
| */ | ||
| execute(position) { | ||
| const item = this.getNextItem(position); | ||
| if (item !== void 0) { | ||
| this.fn(item); | ||
| this._executionCount++; | ||
| this._options.onExecute(item, this); | ||
| } | ||
| return item; | ||
| } | ||
| /** | ||
| * Returns the next item in the queue without removing it. | ||
| * | ||
| * Example usage: | ||
| * ```ts | ||
| * queuer.peekNextItem(); // front | ||
| * queuer.peekNextItem('back'); // back | ||
| * ``` | ||
| */ | ||
| peekNextItem(position = this._options.getItemsFrom) { | ||
| if (position === "front") { | ||
| return this._items[0]; | ||
| } | ||
| return this._items[this._items.length - 1]; | ||
| } | ||
| /** | ||
| * Returns true if the queue is empty (no pending items). | ||
| */ | ||
| getIsEmpty() { | ||
| return this._items.length === 0; | ||
| } | ||
| /** | ||
| * Returns true if the queue is full (reached maxSize). | ||
| */ | ||
| getIsFull() { | ||
| return this._items.length >= this._options.maxSize; | ||
| } | ||
| /** | ||
| * Returns the number of pending items in the queue. | ||
| */ | ||
| getSize() { | ||
| return this._items.length; | ||
| } | ||
| /** | ||
| * Returns a copy of all items in the queue. | ||
| */ | ||
| peekAllItems() { | ||
| return [...this._items]; | ||
| } | ||
| /** | ||
| * Returns the number of items that have been processed and removed from the queue. | ||
| */ | ||
| getExecutionCount() { | ||
| return this._executionCount; | ||
| } | ||
| /** | ||
| * Returns the number of items that have been rejected from being added to the queue. | ||
| */ | ||
| getRejectionCount() { | ||
| return this._rejectionCount; | ||
| } | ||
| /** | ||
| * Returns the number of items that have expired and been removed from the queue. | ||
| */ | ||
| getExpirationCount() { | ||
| return this._expirationCount; | ||
| } | ||
| /** | ||
| * Returns true if the queuer is currently running (processing items). | ||
| */ | ||
| getIsRunning() { | ||
| return this._running; | ||
| } | ||
| /** | ||
| * Returns true if the queuer is running but has no items to process. | ||
| */ | ||
| getIsIdle() { | ||
| return this._running && this.getIsEmpty(); | ||
| } | ||
| #timeoutId; | ||
| #setState; | ||
| #getWait; | ||
| #tick; | ||
| #checkExpiredItems; | ||
| #clearTimeout; | ||
| } | ||
| function queue(fn, options) { | ||
| const queuer = new Queuer(fn, options); | ||
| return queuer.addItem.bind(queuer); | ||
| function queue(fn, initialOptions) { | ||
| const queuer = new Queuer(fn, initialOptions); | ||
| return queuer.addItem; | ||
| } | ||
@@ -326,0 +278,0 @@ exports.Queuer = Queuer; |
@@ -1,1 +0,1 @@ | ||
| {"version":3,"file":"queuer.cjs","sources":["../../src/queuer.ts"],"sourcesContent":["import { parseFunctionOrValue } from './utils'\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 * Maximum number of items allowed in the queuer\n */\n maxSize?: number\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 removed from the queuer\n */\n onExecute?: (item: TValue, queuer: Queuer<TValue>) => void\n /**\n * Callback fired whenever the queuer's running state changes\n */\n onIsRunningChange?: (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\nconst defaultOptions: Required<QueuerOptions<any>> = {\n addItemsTo: 'back',\n getItemsFrom: 'front',\n getPriority: (item) => item?.priority ?? 0,\n getIsExpired: () => false,\n expirationDuration: Infinity,\n initialItems: [],\n maxSize: Infinity,\n onExecute: () => {},\n onIsRunningChange: () => {},\n onItemsChange: () => {},\n onReject: () => {},\n onExpire: () => {},\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 * 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 running)\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 * 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) => 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 private _options: Required<QueuerOptions<TValue>>\n private _items: Array<TValue> = []\n private _itemTimestamps: Array<number> = []\n private _executionCount = 0\n private _rejectionCount = 0\n private _expirationCount = 0\n private _onItemsChanges: Array<(item: TValue) => void> = []\n private _running: boolean\n private _pendingTick = false\n\n constructor(\n private fn: (item: TValue) => void,\n initialOptions: QueuerOptions<TValue> = {},\n ) {\n this._options = { ...defaultOptions, ...initialOptions }\n this._running = this._options.started\n\n for (let i = 0; i < this._options.initialItems.length; i++) {\n const item = this._options.initialItems[i]!\n const isLast = i === this._options.initialItems.length - 1\n this.addItem(item, this._options.addItemsTo, isLast)\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 /**\n * Returns the current queuer options, including defaults and any overrides.\n */\n getOptions(): Required<QueuerOptions<TValue>> {\n return this._options\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, this)\n }\n\n /**\n * Processes items in the queue up to the wait interval. Internal use only.\n */\n private tick() {\n if (!this._running) {\n this._pendingTick = false\n return\n }\n\n // Check for expired items\n this.checkExpiredItems()\n\n while (!this.getIsEmpty()) {\n const nextItem = this.execute(this._options.getItemsFrom)\n if (nextItem === undefined) {\n break\n }\n this._onItemsChanges.forEach((cb) => cb(nextItem))\n\n const wait = this.getWait()\n if (wait > 0) {\n // Use setTimeout to wait before processing next item\n setTimeout(() => this.tick(), wait)\n return\n }\n\n this.tick()\n }\n this._pendingTick = false\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 private checkExpiredItems() {\n if (\n this._options.expirationDuration === Infinity &&\n this._options.getIsExpired === defaultOptions.getIsExpired\n )\n return\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._items.length; i++) {\n const timestamp = this._itemTimestamps[i]\n if (timestamp === undefined) continue\n\n const item = this._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\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._items[index]\n if (expiredItem === undefined) continue\n\n this._items.splice(index, 1)\n this._itemTimestamps.splice(index, 1)\n this._expirationCount++\n this._options.onExpire(expiredItem, this)\n }\n\n if (expiredIndices.length > 0) {\n this._options.onItemsChange(this)\n }\n }\n\n /**\n * Stops processing items in the queue. Does not clear the queue.\n */\n stop() {\n this._running = false\n this._pendingTick = false\n this._options.onIsRunningChange(this)\n }\n\n /**\n * Starts processing items in the queue. If already running, does nothing.\n */\n start() {\n this._running = true\n if (!this._pendingTick && !this.getIsEmpty()) {\n this._pendingTick = true\n this.tick()\n }\n this._options.onIsRunningChange(this)\n }\n\n /**\n * Removes all pending items from the queue. Does not affect items being processed.\n */\n clear(): void {\n this._items = []\n this._options.onItemsChange(this)\n }\n\n /**\n * Resets the queuer to its initial state. Optionally repopulates with initial items.\n * Does not affect callbacks or options.\n */\n reset(withInitialItems?: boolean): void {\n this.clear()\n this._executionCount = 0\n if (withInitialItems) {\n this._items = [...this._options.initialItems]\n }\n this._running = this._options.started\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,\n runOnUpdate: boolean = true,\n ): boolean {\n if (this.getIsFull()) {\n this._rejectionCount++\n this._options.onReject(item, this)\n return false\n }\n\n if (this._options.getPriority !== defaultOptions.getPriority) {\n // If custom priority function is provided, insert based on priority\n const priority = this._options.getPriority(item)\n const insertIndex = this._items.findIndex(\n (existing) => this._options.getPriority(existing) < priority,\n )\n\n if (insertIndex === -1) {\n this._items.push(item)\n this._itemTimestamps.push(Date.now())\n } else {\n this._items.splice(insertIndex, 0, item)\n this._itemTimestamps.splice(insertIndex, 0, Date.now())\n }\n } else {\n // Default FIFO/LIFO behavior\n if (position === 'front') {\n this._items.unshift(item)\n this._itemTimestamps.unshift(Date.now())\n } else {\n this._items.push(item)\n this._itemTimestamps.push(Date.now())\n }\n }\n\n if (this._running && !this._pendingTick) {\n this._pendingTick = true\n this.tick()\n }\n if (runOnUpdate) {\n this._options.onItemsChange(this)\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,\n ): TValue | undefined {\n let item: TValue | undefined\n\n if (position === 'front') {\n item = this._items.shift()\n this._itemTimestamps.shift()\n } else {\n item = this._items.pop()\n this._itemTimestamps.pop()\n }\n\n if (item !== undefined) {\n this._options.onItemsChange(this)\n }\n\n return item\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._executionCount++\n this._options.onExecute(item, this)\n }\n return item\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(\n position: QueuePosition = this._options.getItemsFrom,\n ): TValue | undefined {\n if (position === 'front') {\n return this._items[0]\n }\n return this._items[this._items.length - 1]\n }\n\n /**\n * Returns true if the queue is empty (no pending items).\n */\n getIsEmpty(): boolean {\n return this._items.length === 0\n }\n\n /**\n * Returns true if the queue is full (reached maxSize).\n */\n getIsFull(): boolean {\n return this._items.length >= this._options.maxSize\n }\n\n /**\n * Returns the number of pending items in the queue.\n */\n getSize(): number {\n return this._items.length\n }\n\n /**\n * Returns a copy of all items in the queue.\n */\n peekAllItems(): Array<TValue> {\n return [...this._items]\n }\n\n /**\n * Returns the number of items that have been processed and removed from the queue.\n */\n getExecutionCount(): number {\n return this._executionCount\n }\n\n /**\n * Returns the number of items that have been rejected from being added to the queue.\n */\n getRejectionCount(): number {\n return this._rejectionCount\n }\n\n /**\n * Returns the number of items that have expired and been removed from the queue.\n */\n getExpirationCount(): number {\n return this._expirationCount\n }\n\n /**\n * Returns true if the queuer is currently running (processing items).\n */\n getIsRunning() {\n return this._running\n }\n\n /**\n * Returns true if the queuer is running but has no items to process.\n */\n getIsIdle() {\n return this._running && this.getIsEmpty()\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 is a simplified wrapper around the Queuer class that only exposes the\n * `addItem` method. The queue is always running and will process items as they are added.\n * For more control over queue processing, use the Queuer class directly.\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 options: QueuerOptions<TValue>,\n) {\n const queuer = new Queuer<TValue>(fn, options)\n return queuer.addItem.bind(queuer)\n}\n"],"names":["parseFunctionOrValue"],"mappings":"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"} | ||
| {"version":3,"file":"queuer.cjs","sources":["../../src/queuer.ts"],"sourcesContent":["import { Store } from '@tanstack/store'\nimport { parseFunctionOrValue } from './utils'\n\nexport interface QueuerState<TValue> {\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 * Timestamps when items were added to the queue for expiration tracking\n */\n itemTimestamps: Array<number>\n /**\n * Array of items currently waiting to be processed\n */\n items: Array<TValue>\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 }\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 * Maximum number of items allowed in the queuer\n */\n maxSize?: number\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 removed from the queuer\n */\n onExecute?: (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\nconst defaultOptions: Omit<\n Required<QueuerOptions<any>>,\n | 'initialState'\n | 'onExecute'\n | 'onIsRunningChange'\n | 'onItemsChange'\n | 'onReject'\n | 'onExpire'\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 * 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) => 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 options: QueuerOptions<TValue>\n #timeoutId: NodeJS.Timeout | null = null\n\n constructor(\n private fn: (item: TValue) => void,\n initialOptions: QueuerOptions<TValue> = {},\n ) {\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\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 }\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.isEmpty) {\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 if (this.store.state.isFull) {\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 if (position === 'front') {\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 /**\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 }\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.size - 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.isEmpty) {\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 is a simplified wrapper around the Queuer class that only exposes the\n * `addItem` method. The queue is always isRunning and will process items as they are added.\n * For more control over queue processing, use the Queuer class directly.\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"],"names":["Store","parseFunctionOrValue"],"mappings":"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"} |
+102
-81
@@ -0,1 +1,52 @@ | ||
| import { Store } from '@tanstack/store'; | ||
| export interface QueuerState<TValue> { | ||
| /** | ||
| * Number of items that have been processed by the queuer | ||
| */ | ||
| executionCount: number; | ||
| /** | ||
| * Number of items that have been removed from the queue due to expiration | ||
| */ | ||
| expirationCount: number; | ||
| /** | ||
| * Whether the queuer has no items to process (items array is empty) | ||
| */ | ||
| isEmpty: boolean; | ||
| /** | ||
| * Whether the queuer has reached its maximum capacity | ||
| */ | ||
| isFull: boolean; | ||
| /** | ||
| * Whether the queuer is not currently processing any items | ||
| */ | ||
| isIdle: boolean; | ||
| /** | ||
| * Whether the queuer is active and will process items automatically | ||
| */ | ||
| isRunning: boolean; | ||
| /** | ||
| * Timestamps when items were added to the queue for expiration tracking | ||
| */ | ||
| itemTimestamps: Array<number>; | ||
| /** | ||
| * Array of items currently waiting to be processed | ||
| */ | ||
| items: Array<TValue>; | ||
| /** | ||
| * Whether the queuer has a pending timeout for processing the next item | ||
| */ | ||
| pendingTick: boolean; | ||
| /** | ||
| * Number of items that have been rejected from being added to the queue | ||
| */ | ||
| rejectionCount: number; | ||
| /** | ||
| * Number of items currently in the queue | ||
| */ | ||
| size: number; | ||
| /** | ||
| * Current processing status - 'idle' when not processing, 'running' when active, 'stopped' when paused | ||
| */ | ||
| status: 'idle' | 'running' | 'stopped'; | ||
| } | ||
| /** | ||
@@ -37,2 +88,6 @@ * Options for configuring a Queuer instance. | ||
| /** | ||
| * Initial state for the queuer | ||
| */ | ||
| initialState?: Partial<QueuerState<TValue>>; | ||
| /** | ||
| * Maximum number of items allowed in the queuer | ||
@@ -50,6 +105,2 @@ */ | ||
| /** | ||
| * Callback fired whenever the queuer's running state changes | ||
| */ | ||
| onIsRunningChange?: (queuer: Queuer<TValue>) => void; | ||
| /** | ||
| * Callback fired whenever an item is added or removed from the queuer | ||
@@ -91,3 +142,3 @@ */ | ||
| * Running behavior: | ||
| * - `start()`: Begins automatically processing items in the queue (defaults to running) | ||
| * - `start()`: Begins automatically processing items in the queue (defaults to isRunning) | ||
| * - `stop()`: Pauses processing but maintains queue state | ||
@@ -121,2 +172,13 @@ * - `wait`: Configurable delay between processing items | ||
| * | ||
| * State Management: | ||
| * - Uses TanStack Store for reactive state management | ||
| * - Use `initialState` to provide initial state values when creating the queuer | ||
| * - Use `onExecute` callback to react to item execution and implement custom logic | ||
| * - Use `onItemsChange` callback to react to items being added or removed from the queue | ||
| * - Use `onExpire` callback to react to items expiring and implement custom logic | ||
| * - Use `onReject` callback to react to items being rejected when the queue is full | ||
| * - The state includes execution count, expiration count, rejection count, and isRunning status | ||
| * - State can be accessed via `queuer.store.state` when using the class directly | ||
| * - When using framework adapters (React/Solid), state is accessed from `queuer.state` | ||
| * | ||
| * Example usage: | ||
@@ -144,12 +206,6 @@ * ```ts | ||
| export declare class Queuer<TValue> { | ||
| #private; | ||
| private fn; | ||
| private _options; | ||
| private _items; | ||
| private _itemTimestamps; | ||
| private _executionCount; | ||
| private _rejectionCount; | ||
| private _expirationCount; | ||
| private _onItemsChanges; | ||
| private _running; | ||
| private _pendingTick; | ||
| readonly store: Store<Readonly<QueuerState<TValue>>>; | ||
| options: QueuerOptions<TValue>; | ||
| constructor(fn: (item: TValue) => void, initialOptions?: QueuerOptions<TValue>); | ||
@@ -159,39 +215,4 @@ /** | ||
| */ | ||
| setOptions(newOptions: Partial<QueuerOptions<TValue>>): void; | ||
| setOptions: (newOptions: Partial<QueuerOptions<TValue>>) => void; | ||
| /** | ||
| * Returns the current queuer options, including defaults and any overrides. | ||
| */ | ||
| getOptions(): Required<QueuerOptions<TValue>>; | ||
| /** | ||
| * Returns the current wait time (in milliseconds) between processing items. | ||
| * If a function is provided, it is called with the queuer instance. | ||
| */ | ||
| getWait(): number; | ||
| /** | ||
| * Processes items in the queue up to the wait interval. Internal use only. | ||
| */ | ||
| private tick; | ||
| /** | ||
| * Checks for expired items in the queue and removes them. Calls onExpire for each expired item. | ||
| * Internal use only. | ||
| */ | ||
| private checkExpiredItems; | ||
| /** | ||
| * Stops processing items in the queue. Does not clear the queue. | ||
| */ | ||
| stop(): void; | ||
| /** | ||
| * Starts processing items in the queue. If already running, does nothing. | ||
| */ | ||
| start(): void; | ||
| /** | ||
| * Removes all pending items from the queue. Does not affect items being processed. | ||
| */ | ||
| clear(): void; | ||
| /** | ||
| * Resets the queuer to its initial state. Optionally repopulates with initial items. | ||
| * Does not affect callbacks or options. | ||
| */ | ||
| reset(withInitialItems?: boolean): void; | ||
| /** | ||
| * Adds an item to the queue. If the queue is full, the item is rejected and onReject is called. | ||
@@ -208,3 +229,3 @@ * Items can be inserted based on priority or at the front/back depending on configuration. | ||
| */ | ||
| addItem(item: TValue, position?: QueuePosition, runOnUpdate?: boolean): boolean; | ||
| addItem: (item: TValue, position?: QueuePosition, runOnItemsChange?: boolean) => boolean; | ||
| /** | ||
@@ -222,3 +243,3 @@ * Removes and returns the next item from the queue without executing the function. | ||
| */ | ||
| getNextItem(position?: QueuePosition): TValue | undefined; | ||
| getNextItem: (position?: QueuePosition) => TValue | undefined; | ||
| /** | ||
@@ -234,4 +255,9 @@ * Removes and returns the next item from the queue and processes it using the provided function. | ||
| */ | ||
| execute(position?: QueuePosition): TValue | undefined; | ||
| execute: (position?: QueuePosition) => TValue | undefined; | ||
| /** | ||
| * Processes a specified number of items to execute immediately with no wait time | ||
| * If no numberOfItems is provided, all items will be processed | ||
| */ | ||
| flush: (numberOfItems?: number, position?: QueuePosition) => void; | ||
| /** | ||
| * Returns the next item in the queue without removing it. | ||
@@ -245,39 +271,23 @@ * | ||
| */ | ||
| peekNextItem(position?: QueuePosition): TValue | undefined; | ||
| peekNextItem: (position?: QueuePosition) => TValue | undefined; | ||
| /** | ||
| * Returns true if the queue is empty (no pending items). | ||
| */ | ||
| getIsEmpty(): boolean; | ||
| /** | ||
| * Returns true if the queue is full (reached maxSize). | ||
| */ | ||
| getIsFull(): boolean; | ||
| /** | ||
| * Returns the number of pending items in the queue. | ||
| */ | ||
| getSize(): number; | ||
| /** | ||
| * Returns a copy of all items in the queue. | ||
| */ | ||
| peekAllItems(): Array<TValue>; | ||
| peekAllItems: () => Array<TValue>; | ||
| /** | ||
| * Returns the number of items that have been processed and removed from the queue. | ||
| * Starts processing items in the queue. If already isRunning, does nothing. | ||
| */ | ||
| getExecutionCount(): number; | ||
| start: () => void; | ||
| /** | ||
| * Returns the number of items that have been rejected from being added to the queue. | ||
| * Stops processing items in the queue. Does not clear the queue. | ||
| */ | ||
| getRejectionCount(): number; | ||
| stop: () => void; | ||
| /** | ||
| * Returns the number of items that have expired and been removed from the queue. | ||
| * Removes all pending items from the queue. Does not affect items being processed. | ||
| */ | ||
| getExpirationCount(): number; | ||
| clear: () => void; | ||
| /** | ||
| * Returns true if the queuer is currently running (processing items). | ||
| * Resets the queuer state to its default values | ||
| */ | ||
| getIsRunning(): boolean; | ||
| /** | ||
| * Returns true if the queuer is running but has no items to process. | ||
| */ | ||
| getIsIdle(): boolean; | ||
| reset: () => void; | ||
| } | ||
@@ -289,5 +299,16 @@ /** | ||
| * This is a simplified wrapper around the Queuer class that only exposes the | ||
| * `addItem` method. The queue is always running and will process items as they are added. | ||
| * `addItem` method. The queue is always isRunning and will process items as they are added. | ||
| * For more control over queue processing, use the Queuer class directly. | ||
| * | ||
| * State Management: | ||
| * - Uses TanStack Store for reactive state management | ||
| * - Use `initialState` to provide initial state values when creating the queuer | ||
| * - Use `onExecute` callback to react to item execution and implement custom logic | ||
| * - Use `onItemsChange` callback to react to items being added or removed from the queue | ||
| * - Use `onExpire` callback to react to items expiring and implement custom logic | ||
| * - Use `onReject` callback to react to items being rejected when the queue is full | ||
| * - The state includes execution count, expiration count, rejection count, and isRunning status | ||
| * - State can be accessed via the underlying Queuer instance's `store.state` property | ||
| * - When using framework adapters (React/Solid), state is accessed from the hook's state property | ||
| * | ||
| * Example usage: | ||
@@ -311,2 +332,2 @@ * ```ts | ||
| */ | ||
| export declare function queue<TValue>(fn: (item: TValue) => void, options: QueuerOptions<TValue>): (item: TValue, position?: QueuePosition, runOnUpdate?: boolean) => boolean; | ||
| export declare function queue<TValue>(fn: (item: TValue) => void, initialOptions: QueuerOptions<TValue>): (item: TValue, position?: QueuePosition, runOnItemsChange?: boolean) => boolean; |
+97
-130
| "use strict"; | ||
| Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" }); | ||
| const store = require("@tanstack/store"); | ||
| const utils = require("./utils.cjs"); | ||
| function getDefaultRateLimiterState() { | ||
| return structuredClone({ | ||
| executionCount: 0, | ||
| executionTimes: [], | ||
| rejectionCount: 0 | ||
| }); | ||
| } | ||
| const defaultOptions = { | ||
| enabled: true, | ||
| limit: 1, | ||
| onExecute: () => { | ||
| }, | ||
| onReject: () => { | ||
| }, | ||
| window: 0, | ||
@@ -17,137 +21,100 @@ windowType: "fixed" | ||
| this.fn = fn; | ||
| this._executionCount = 0; | ||
| this._rejectionCount = 0; | ||
| this._executionTimes = []; | ||
| this._options = { | ||
| ...defaultOptions, | ||
| ...initialOptions | ||
| this.store = new store.Store(getDefaultRateLimiterState()); | ||
| this.setOptions = (newOptions) => { | ||
| this.options = { ...this.options, ...newOptions }; | ||
| }; | ||
| } | ||
| /** | ||
| * Updates the rate limiter options | ||
| */ | ||
| setOptions(newOptions) { | ||
| this._options = { ...this._options, ...newOptions }; | ||
| } | ||
| /** | ||
| * Returns the current rate limiter options | ||
| */ | ||
| getOptions() { | ||
| return this._options; | ||
| } | ||
| /** | ||
| * Returns the current enabled state of the rate limiter | ||
| */ | ||
| getEnabled() { | ||
| return utils.parseFunctionOrValue(this._options.enabled, this); | ||
| } | ||
| /** | ||
| * Returns the current limit of executions allowed within the time window | ||
| */ | ||
| getLimit() { | ||
| return utils.parseFunctionOrValue(this._options.limit, this); | ||
| } | ||
| /** | ||
| * Returns the current time window in milliseconds | ||
| */ | ||
| getWindow() { | ||
| return utils.parseFunctionOrValue(this._options.window, this); | ||
| } | ||
| /** | ||
| * Attempts to execute the rate-limited function if within the configured limits. | ||
| * Will reject execution if the number of calls in the current window exceeds the limit. | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * const rateLimiter = new RateLimiter(fn, { limit: 5, window: 1000 }); | ||
| * | ||
| * // First 5 calls will return true | ||
| * rateLimiter.maybeExecute('arg1', 'arg2'); // true | ||
| * | ||
| * // Additional calls within the window will return false | ||
| * rateLimiter.maybeExecute('arg1', 'arg2'); // false | ||
| * ``` | ||
| */ | ||
| maybeExecute(...args) { | ||
| this.cleanupOldExecutions(); | ||
| if (this._options.windowType === "sliding") { | ||
| if (this._executionTimes.length < this.getLimit()) { | ||
| this.execute(...args); | ||
| this.#setState = (newState) => { | ||
| this.store.setState((state) => { | ||
| const combinedState = { | ||
| ...state, | ||
| ...newState | ||
| }; | ||
| return combinedState; | ||
| }); | ||
| }; | ||
| this.#getEnabled = () => { | ||
| return !!utils.parseFunctionOrValue(this.options.enabled, this); | ||
| }; | ||
| this.#getLimit = () => { | ||
| return utils.parseFunctionOrValue(this.options.limit, this); | ||
| }; | ||
| this.#getWindow = () => { | ||
| return utils.parseFunctionOrValue(this.options.window, this); | ||
| }; | ||
| this.maybeExecute = (...args) => { | ||
| this.#cleanupOldExecutions(); | ||
| const relevantExecutionTimes = this.#getRelevantExecutionTimes(); | ||
| if (relevantExecutionTimes.length < this.#getLimit()) { | ||
| this.#execute(...args); | ||
| return true; | ||
| } | ||
| } else { | ||
| this.#setState({ | ||
| rejectionCount: this.store.state.rejectionCount + 1 | ||
| }); | ||
| this.options.onReject?.(this); | ||
| return false; | ||
| }; | ||
| this.#execute = (...args) => { | ||
| if (!this.#getEnabled()) return; | ||
| const now = Date.now(); | ||
| const oldestExecution = Math.min(...this._executionTimes); | ||
| const isNewWindow = oldestExecution + this.getWindow() <= now; | ||
| if (isNewWindow || this._executionTimes.length < this.getLimit()) { | ||
| this.execute(...args); | ||
| return true; | ||
| this.fn(...args); | ||
| this.store.state.executionTimes.push(now); | ||
| this.#setState({ | ||
| executionCount: this.store.state.executionCount + 1 | ||
| }); | ||
| this.options.onExecute?.(this); | ||
| }; | ||
| this.#getRelevantExecutionTimes = () => { | ||
| if (this.options.windowType === "sliding") { | ||
| return this.store.state.executionTimes.filter( | ||
| (time) => time > Date.now() - this.#getWindow() | ||
| ); | ||
| } else { | ||
| const oldestExecution = Math.min(...this.store.state.executionTimes); | ||
| const windowStart = oldestExecution; | ||
| return this.store.state.executionTimes.filter( | ||
| (time) => time >= windowStart && time <= windowStart + this.#getWindow() | ||
| ); | ||
| } | ||
| } | ||
| this.rejectFunction(); | ||
| return false; | ||
| }; | ||
| this.#cleanupOldExecutions = () => { | ||
| const now = Date.now(); | ||
| const windowStart = now - this.#getWindow(); | ||
| this.#setState({ | ||
| executionTimes: this.store.state.executionTimes.filter( | ||
| (time) => time > windowStart | ||
| ) | ||
| }); | ||
| }; | ||
| this.getRemainingInWindow = () => { | ||
| const relevantExecutionTimes = this.#getRelevantExecutionTimes(); | ||
| return Math.max(0, this.#getLimit() - relevantExecutionTimes.length); | ||
| }; | ||
| this.getMsUntilNextWindow = () => { | ||
| if (this.getRemainingInWindow() > 0) { | ||
| return 0; | ||
| } | ||
| const oldestExecution = this.store.state.executionTimes[0] ?? Infinity; | ||
| return oldestExecution + this.#getWindow() - Date.now(); | ||
| }; | ||
| this.reset = () => { | ||
| this.#setState(getDefaultRateLimiterState()); | ||
| }; | ||
| this.options = { | ||
| ...defaultOptions, | ||
| ...initialOptions | ||
| }; | ||
| this.#setState(this.options.initialState ?? {}); | ||
| } | ||
| execute(...args) { | ||
| var _a, _b; | ||
| if (!this.getEnabled()) return; | ||
| const now = Date.now(); | ||
| this._executionCount++; | ||
| this._executionTimes.push(now); | ||
| this.fn(...args); | ||
| (_b = (_a = this._options).onExecute) == null ? void 0 : _b.call(_a, this); | ||
| } | ||
| rejectFunction() { | ||
| this._rejectionCount++; | ||
| if (this._options.onReject) { | ||
| this._options.onReject(this); | ||
| } | ||
| } | ||
| cleanupOldExecutions() { | ||
| const now = Date.now(); | ||
| const windowStart = now - this.getWindow(); | ||
| this._executionTimes = this._executionTimes.filter( | ||
| (time) => time > windowStart | ||
| ); | ||
| } | ||
| /** | ||
| * Returns the number of times the function has been executed | ||
| */ | ||
| getExecutionCount() { | ||
| return this._executionCount; | ||
| } | ||
| /** | ||
| * Returns the number of times the function has been rejected | ||
| */ | ||
| getRejectionCount() { | ||
| return this._rejectionCount; | ||
| } | ||
| /** | ||
| * Returns the number of remaining executions allowed in the current window | ||
| */ | ||
| getRemainingInWindow() { | ||
| this.cleanupOldExecutions(); | ||
| return Math.max(0, this.getLimit() - this._executionTimes.length); | ||
| } | ||
| /** | ||
| * Returns the number of milliseconds until the next execution will be possible | ||
| */ | ||
| getMsUntilNextWindow() { | ||
| if (this.getRemainingInWindow() > 0) { | ||
| return 0; | ||
| } | ||
| const oldestExecution = Math.min(...this._executionTimes); | ||
| return oldestExecution + this.getWindow() - Date.now(); | ||
| } | ||
| /** | ||
| * Resets the rate limiter state | ||
| */ | ||
| reset() { | ||
| this._executionTimes = []; | ||
| this._executionCount = 0; | ||
| this._rejectionCount = 0; | ||
| } | ||
| #setState; | ||
| #getEnabled; | ||
| #getLimit; | ||
| #getWindow; | ||
| #execute; | ||
| #getRelevantExecutionTimes; | ||
| #cleanupOldExecutions; | ||
| } | ||
| function rateLimit(fn, initialOptions) { | ||
| const rateLimiter = new RateLimiter(fn, initialOptions); | ||
| return rateLimiter.maybeExecute.bind(rateLimiter); | ||
| return rateLimiter.maybeExecute; | ||
| } | ||
@@ -154,0 +121,0 @@ exports.RateLimiter = RateLimiter; |
@@ -1,1 +0,1 @@ | ||
| {"version":3,"file":"rate-limiter.cjs","sources":["../../src/rate-limiter.ts"],"sourcesContent":["import { parseFunctionOrValue } from './utils'\nimport type { AnyFunction } from './types'\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 * 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?: (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\nconst defaultOptions: Required<RateLimiterOptions<any>> = {\n enabled: true,\n limit: 1,\n onExecute: () => {},\n onReject: () => {},\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 *\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 * @example\n * ```ts\n * const rateLimiter = new RateLimiter(\n * (id: string) => api.getData(id),\n * { limit: 5, window: 1000, windowType: 'sliding' } // 5 calls per second with sliding window\n * );\n *\n * // Will execute immediately until limit reached, then block\n * rateLimiter.maybeExecute('123');\n * ```\n */\nexport class RateLimiter<TFn extends AnyFunction> {\n private _executionCount = 0\n private _rejectionCount = 0\n private _executionTimes: Array<number> = []\n private _options: RateLimiterOptions<TFn>\n\n constructor(\n private fn: TFn,\n initialOptions: RateLimiterOptions<TFn>,\n ) {\n this._options = {\n ...defaultOptions,\n ...initialOptions,\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 /**\n * Returns the current rate limiter options\n */\n getOptions(): Required<RateLimiterOptions<TFn>> {\n return this._options as Required<RateLimiterOptions<TFn>>\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.cleanupOldExecutions()\n\n if (this._options.windowType === 'sliding') {\n // For sliding window, we can execute if we have capacity in the current window\n if (this._executionTimes.length < this.getLimit()) {\n this.execute(...args)\n return true\n }\n } else {\n // For fixed window, we need to check if we're in a new window\n const now = Date.now()\n const oldestExecution = Math.min(...this._executionTimes)\n const isNewWindow = oldestExecution + this.getWindow() <= now\n\n if (isNewWindow || this._executionTimes.length < this.getLimit()) {\n this.execute(...args)\n return true\n }\n }\n\n this.rejectFunction()\n return false\n }\n\n private execute(...args: Parameters<TFn>): void {\n if (!this.getEnabled()) return\n const now = Date.now()\n this._executionCount++\n this._executionTimes.push(now)\n this.fn(...args) // execute the function\n this._options.onExecute?.(this)\n }\n\n private rejectFunction(): void {\n this._rejectionCount++\n if (this._options.onReject) {\n this._options.onReject(this)\n }\n }\n\n private cleanupOldExecutions(): void {\n const now = Date.now()\n const windowStart = now - this.getWindow()\n this._executionTimes = this._executionTimes.filter(\n (time) => time > windowStart,\n )\n }\n\n /**\n * Returns the number of times the function has been executed\n */\n getExecutionCount(): number {\n return this._executionCount\n }\n\n /**\n * Returns the number of times the function has been rejected\n */\n getRejectionCount(): number {\n return this._rejectionCount\n }\n\n /**\n * Returns the number of remaining executions allowed in the current window\n */\n getRemainingInWindow(): number {\n this.cleanupOldExecutions()\n return Math.max(0, this.getLimit() - this._executionTimes.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 = Math.min(...this._executionTimes)\n return oldestExecution + this.getWindow() - Date.now()\n }\n\n /**\n * Resets the rate limiter state\n */\n reset(): void {\n this._executionTimes = []\n this._executionCount = 0\n this._rejectionCount = 0\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 * 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 * 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.bind(rateLimiter)\n}\n"],"names":["parseFunctionOrValue"],"mappings":"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"} | ||
| {"version":3,"file":"rate-limiter.cjs","sources":["../../src/rate-limiter.ts"],"sourcesContent":["import { Store } from '@tanstack/store'\nimport { parseFunctionOrValue } from './utils'\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 * Number of function executions that have been rejected due to rate limiting\n */\n rejectionCount: number\n}\n\nfunction getDefaultRateLimiterState(): RateLimiterState {\n return structuredClone({\n executionCount: 0,\n executionTimes: [],\n rejectionCount: 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 * 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?: (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\nconst defaultOptions: Omit<\n Required<RateLimiterOptions<any>>,\n 'initialState' | 'onExecute' | 'onReject'\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 *\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 options: RateLimiterOptions<TFn>\n\n constructor(\n private fn: TFn,\n initialOptions: RateLimiterOptions<TFn>,\n ) {\n this.options = {\n ...defaultOptions,\n ...initialOptions,\n }\n this.#setState(this.options.initialState ?? {})\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 return combinedState\n })\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.#cleanupOldExecutions()\n\n const relevantExecutionTimes = this.#getRelevantExecutionTimes()\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 this.#setState({\n executionCount: this.store.state.executionCount + 1,\n })\n this.options.onExecute?.(this)\n }\n\n #getRelevantExecutionTimes = (): 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 const oldestExecution = Math.min(...this.store.state.executionTimes)\n const windowStart = oldestExecution\n return this.store.state.executionTimes.filter(\n (time) =>\n time >= windowStart && time <= windowStart + this.#getWindow(),\n )\n }\n }\n\n #cleanupOldExecutions = (): void => {\n const now = Date.now()\n const windowStart = now - this.#getWindow()\n this.#setState({\n executionTimes: this.store.state.executionTimes.filter(\n (time) => time > windowStart,\n ),\n })\n }\n\n /**\n * Returns the number of remaining executions allowed in the current window\n */\n getRemainingInWindow = (): number => {\n const relevantExecutionTimes = this.#getRelevantExecutionTimes()\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 }\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 * 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"],"names":["Store","parseFunctionOrValue"],"mappings":";;;;AAmBA,SAAS,6BAA+C;AACtD,SAAO,gBAAgB;AAAA,IACrB,gBAAgB;AAAA,IAChB,gBAAgB,CAAA;AAAA,IAChB,gBAAgB;AAAA,EAAA,CACjB;AACH;AA0CA,MAAM,iBAGF;AAAA,EACF,SAAS;AAAA,EACT,OAAO;AAAA,EACP,QAAQ;AAAA,EACR,YAAY;AACd;AA8CO,MAAM,YAAqC;AAAA,EAKhD,YACU,IACR,gBACA;AAFQ,SAAA,KAAA;AALV,SAAS,QACP,IAAIA,MAAAA,MAAwB,2BAAA,CAA4B;AAiB1D,SAAA,aAAa,CAAC,eAAuD;AACnE,WAAK,UAAU,EAAE,GAAG,KAAK,SAAS,GAAG,WAAA;AAAA,IAAW;AAGlD,SAAA,YAAY,CAAC,aAA8C;AACzD,WAAK,MAAM,SAAS,CAAC,UAAU;AAC7B,cAAM,gBAAgB;AAAA,UACpB,GAAG;AAAA,UACH,GAAG;AAAA,QAAA;AAEL,eAAO;AAAA,MAAA,CACR;AAAA,IAAA;AAMH,SAAA,cAAc,MAAe;AAC3B,aAAO,CAAC,CAACC,MAAAA,qBAAqB,KAAK,QAAQ,SAAS,IAAI;AAAA,IAAA;AAM1D,SAAA,YAAY,MAAc;AACxB,aAAOA,MAAAA,qBAAqB,KAAK,QAAQ,OAAO,IAAI;AAAA,IAAA;AAMtD,SAAA,aAAa,MAAc;AACzB,aAAOA,MAAAA,qBAAqB,KAAK,QAAQ,QAAQ,IAAI;AAAA,IAAA;AAkBvD,SAAA,eAAe,IAAI,SAAmC;AACpD,WAAK,sBAAA;AAEL,YAAM,yBAAyB,KAAK,2BAAA;AAEpC,UAAI,uBAAuB,SAAS,KAAK,UAAA,GAAa;AACpD,aAAK,SAAS,GAAG,IAAI;AACrB,eAAO;AAAA,MAAA;AAGT,WAAK,UAAU;AAAA,QACb,gBAAgB,KAAK,MAAM,MAAM,iBAAiB;AAAA,MAAA,CACnD;AACD,WAAK,QAAQ,WAAW,IAAI;AAC5B,aAAO;AAAA,IAAA;AAGT,SAAA,WAAW,IAAI,SAAgC;AAC7C,UAAI,CAAC,KAAK,cAAe;AACzB,YAAM,MAAM,KAAK,IAAA;AACjB,WAAK,GAAG,GAAG,IAAI;AACf,WAAK,MAAM,MAAM,eAAe,KAAK,GAAG;AACxC,WAAK,UAAU;AAAA,QACb,gBAAgB,KAAK,MAAM,MAAM,iBAAiB;AAAA,MAAA,CACnD;AACD,WAAK,QAAQ,YAAY,IAAI;AAAA,IAAA;AAG/B,SAAA,6BAA6B,MAAqB;AAChD,UAAI,KAAK,QAAQ,eAAe,WAAW;AAEzC,eAAO,KAAK,MAAM,MAAM,eAAe;AAAA,UACrC,CAAC,SAAS,OAAO,KAAK,IAAA,IAAQ,KAAK,WAAA;AAAA,QAAW;AAAA,MAChD,OACK;AAGL,cAAM,kBAAkB,KAAK,IAAI,GAAG,KAAK,MAAM,MAAM,cAAc;AACnE,cAAM,cAAc;AACpB,eAAO,KAAK,MAAM,MAAM,eAAe;AAAA,UACrC,CAAC,SACC,QAAQ,eAAe,QAAQ,cAAc,KAAK,WAAA;AAAA,QAAW;AAAA,MACjE;AAAA,IACF;AAGF,SAAA,wBAAwB,MAAY;AAClC,YAAM,MAAM,KAAK,IAAA;AACjB,YAAM,cAAc,MAAM,KAAK,WAAA;AAC/B,WAAK,UAAU;AAAA,QACb,gBAAgB,KAAK,MAAM,MAAM,eAAe;AAAA,UAC9C,CAAC,SAAS,OAAO;AAAA,QAAA;AAAA,MACnB,CACD;AAAA,IAAA;AAMH,SAAA,uBAAuB,MAAc;AACnC,YAAM,yBAAyB,KAAK,2BAAA;AACpC,aAAO,KAAK,IAAI,GAAG,KAAK,UAAA,IAAc,uBAAuB,MAAM;AAAA,IAAA;AAMrE,SAAA,uBAAuB,MAAc;AACnC,UAAI,KAAK,qBAAA,IAAyB,GAAG;AACnC,eAAO;AAAA,MAAA;AAET,YAAM,kBAAkB,KAAK,MAAM,MAAM,eAAe,CAAC,KAAK;AAC9D,aAAO,kBAAkB,KAAK,WAAA,IAAe,KAAK,IAAA;AAAA,IAAI;AAMxD,SAAA,QAAQ,MAAY;AAClB,WAAK,UAAU,4BAA4B;AAAA,IAAA;AA3I3C,SAAK,UAAU;AAAA,MACb,GAAG;AAAA,MACH,GAAG;AAAA,IAAA;AAEL,SAAK,UAAU,KAAK,QAAQ,gBAAgB,CAAA,CAAE;AAAA,EAAA;AAAA,EAUhD;AAAA,EAaA;AAAA,EAOA;AAAA,EAOA;AAAA,EAoCA;AAAA,EAWA;AAAA,EAkBA;AAmCF;AAgDO,SAAS,UACd,IACA,gBACA;AACA,QAAM,cAAc,IAAI,YAAY,IAAI,cAAc;AACtD,SAAO,YAAY;AACrB;;;"} |
@@ -0,2 +1,17 @@ | ||
| import { Store } from '@tanstack/store'; | ||
| import { AnyFunction } from './types.cjs'; | ||
| export interface RateLimiterState { | ||
| /** | ||
| * Number of function executions that have been completed | ||
| */ | ||
| executionCount: number; | ||
| /** | ||
| * Array of timestamps when executions occurred for rate limiting calculations | ||
| */ | ||
| executionTimes: Array<number>; | ||
| /** | ||
| * Number of function executions that have been rejected due to rate limiting | ||
| */ | ||
| rejectionCount: number; | ||
| } | ||
| /** | ||
@@ -12,2 +27,6 @@ * Options for configuring a rate-limited function | ||
| /** | ||
| * Initial state for the rate limiter | ||
| */ | ||
| initialState?: Partial<RateLimiterState>; | ||
| /** | ||
| * Maximum number of executions allowed within the time window. | ||
@@ -58,2 +77,11 @@ * Can be a number or a callback function that receives the rate limiter instance and returns a number. | ||
| * | ||
| * State Management: | ||
| * - Uses TanStack Store for reactive state management | ||
| * - Use `initialState` to provide initial state values when creating the rate limiter | ||
| * - Use `onExecute` callback to react to function execution and implement custom logic | ||
| * - Use `onReject` callback to react to executions being rejected when rate limit is exceeded | ||
| * - The state includes execution count, execution times, and rejection count | ||
| * - State can be accessed via `rateLimiter.store.state` when using the class directly | ||
| * - When using framework adapters (React/Solid), state is accessed from `rateLimiter.state` | ||
| * | ||
| * @example | ||
@@ -63,3 +91,7 @@ * ```ts | ||
| * (id: string) => api.getData(id), | ||
| * { limit: 5, window: 1000, windowType: 'sliding' } // 5 calls per second with sliding window | ||
| * { | ||
| * limit: 5, | ||
| * window: 1000, | ||
| * windowType: 'sliding', | ||
| * } | ||
| * ); | ||
@@ -72,7 +104,6 @@ * | ||
| export declare class RateLimiter<TFn extends AnyFunction> { | ||
| #private; | ||
| private fn; | ||
| private _executionCount; | ||
| private _rejectionCount; | ||
| private _executionTimes; | ||
| private _options; | ||
| readonly store: Store<Readonly<RateLimiterState>>; | ||
| options: RateLimiterOptions<TFn>; | ||
| constructor(fn: TFn, initialOptions: RateLimiterOptions<TFn>); | ||
@@ -82,20 +113,4 @@ /** | ||
| */ | ||
| setOptions(newOptions: Partial<RateLimiterOptions<TFn>>): void; | ||
| setOptions: (newOptions: Partial<RateLimiterOptions<TFn>>) => void; | ||
| /** | ||
| * Returns the current rate limiter options | ||
| */ | ||
| getOptions(): Required<RateLimiterOptions<TFn>>; | ||
| /** | ||
| * Returns the current enabled state of the rate limiter | ||
| */ | ||
| getEnabled(): boolean; | ||
| /** | ||
| * Returns the current limit of executions allowed within the time window | ||
| */ | ||
| getLimit(): number; | ||
| /** | ||
| * Returns the current time window in milliseconds | ||
| */ | ||
| getWindow(): number; | ||
| /** | ||
| * Attempts to execute the rate-limited function if within the configured limits. | ||
@@ -115,26 +130,15 @@ * Will reject execution if the number of calls in the current window exceeds the limit. | ||
| */ | ||
| maybeExecute(...args: Parameters<TFn>): boolean; | ||
| private execute; | ||
| private rejectFunction; | ||
| private cleanupOldExecutions; | ||
| maybeExecute: (...args: Parameters<TFn>) => boolean; | ||
| /** | ||
| * Returns the number of times the function has been executed | ||
| */ | ||
| getExecutionCount(): number; | ||
| /** | ||
| * Returns the number of times the function has been rejected | ||
| */ | ||
| getRejectionCount(): number; | ||
| /** | ||
| * Returns the number of remaining executions allowed in the current window | ||
| */ | ||
| getRemainingInWindow(): number; | ||
| getRemainingInWindow: () => number; | ||
| /** | ||
| * Returns the number of milliseconds until the next execution will be possible | ||
| */ | ||
| getMsUntilNextWindow(): number; | ||
| getMsUntilNextWindow: () => number; | ||
| /** | ||
| * Resets the rate limiter state | ||
| */ | ||
| reset(): void; | ||
| reset: () => void; | ||
| } | ||
@@ -155,2 +159,11 @@ /** | ||
| * | ||
| * State Management: | ||
| * - Uses TanStack Store for reactive state management | ||
| * - Use `initialState` to provide initial state values when creating the rate limiter | ||
| * - Use `onExecute` callback to react to function execution and implement custom logic | ||
| * - Use `onReject` callback to react to executions being rejected when rate limit is exceeded | ||
| * - The state includes execution count, execution times, and rejection count | ||
| * - State can be accessed via the underlying RateLimiter instance's `store.state` property | ||
| * - When using framework adapters (React/Solid), state is accessed from the hook's state property | ||
| * | ||
| * Consider using throttle() or debounce() if you need more intelligent execution control. Use rate limiting when you specifically | ||
@@ -157,0 +170,0 @@ * need to enforce a hard limit on the number of executions within a time period. |
+107
-123
| "use strict"; | ||
| Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" }); | ||
| const store = require("@tanstack/store"); | ||
| const utils = require("./utils.cjs"); | ||
| function getDefaultThrottlerState() { | ||
| return structuredClone({ | ||
| executionCount: 0, | ||
| isPending: false, | ||
| lastArgs: void 0, | ||
| lastExecutionTime: 0, | ||
| nextExecutionTime: 0, | ||
| status: "idle" | ||
| }); | ||
| } | ||
| const defaultOptions = { | ||
| enabled: true, | ||
| leading: true, | ||
| onExecute: () => { | ||
| }, | ||
| trailing: true, | ||
@@ -15,130 +24,105 @@ wait: 0 | ||
| this.fn = fn; | ||
| this._executionCount = 0; | ||
| this._lastExecutionTime = 0; | ||
| this._options = { | ||
| this.store = new store.Store( | ||
| getDefaultThrottlerState() | ||
| ); | ||
| this.setOptions = (newOptions) => { | ||
| this.options = { ...this.options, ...newOptions }; | ||
| if (!this.#getEnabled()) { | ||
| this.cancel(); | ||
| } | ||
| }; | ||
| this.#setState = (newState) => { | ||
| this.store.setState((state) => { | ||
| const combinedState = { | ||
| ...state, | ||
| ...newState | ||
| }; | ||
| const { isPending } = combinedState; | ||
| return { | ||
| ...combinedState, | ||
| status: !this.#getEnabled() ? "disabled" : isPending ? "pending" : "idle" | ||
| }; | ||
| }); | ||
| }; | ||
| this.#getEnabled = () => { | ||
| return !!utils.parseFunctionOrValue(this.options.enabled, this); | ||
| }; | ||
| this.#getWait = () => { | ||
| return utils.parseFunctionOrValue(this.options.wait, this); | ||
| }; | ||
| this.maybeExecute = (...args) => { | ||
| const now = Date.now(); | ||
| const timeSinceLastExecution = now - this.store.state.lastExecutionTime; | ||
| const wait = this.#getWait(); | ||
| if (this.options.leading && timeSinceLastExecution >= wait) { | ||
| this.#execute(...args); | ||
| } else { | ||
| this.#setState({ | ||
| lastArgs: args | ||
| }); | ||
| if (!this.#timeoutId && this.options.trailing) { | ||
| const _timeSinceLastExecution = this.store.state.lastExecutionTime ? now - this.store.state.lastExecutionTime : 0; | ||
| const timeoutDuration = wait - _timeSinceLastExecution; | ||
| this.#setState({ isPending: true }); | ||
| this.#timeoutId = setTimeout(() => { | ||
| const { lastArgs } = this.store.state; | ||
| if (lastArgs !== void 0) { | ||
| this.#execute(...lastArgs); | ||
| } | ||
| }, timeoutDuration); | ||
| } | ||
| } | ||
| }; | ||
| this.#execute = (...args) => { | ||
| if (!this.#getEnabled()) return; | ||
| this.fn(...args); | ||
| const lastExecutionTime = Date.now(); | ||
| const nextExecutionTime = lastExecutionTime + this.#getWait(); | ||
| this.#clearTimeout(); | ||
| this.#setState({ | ||
| executionCount: this.store.state.executionCount + 1, | ||
| lastExecutionTime, | ||
| nextExecutionTime, | ||
| isPending: false, | ||
| lastArgs: void 0 | ||
| }); | ||
| this.options.onExecute?.(this); | ||
| }; | ||
| this.flush = () => { | ||
| if (this.store.state.isPending && this.store.state.lastArgs) { | ||
| this.#execute(...this.store.state.lastArgs); | ||
| } | ||
| }; | ||
| this.#clearTimeout = () => { | ||
| if (this.#timeoutId) { | ||
| clearTimeout(this.#timeoutId); | ||
| this.#timeoutId = void 0; | ||
| } | ||
| }; | ||
| this.cancel = () => { | ||
| this.#clearTimeout(); | ||
| this.#setState({ | ||
| lastArgs: void 0, | ||
| isPending: false | ||
| }); | ||
| }; | ||
| this.reset = () => { | ||
| this.#setState(getDefaultThrottlerState()); | ||
| }; | ||
| this.options = { | ||
| ...defaultOptions, | ||
| ...initialOptions | ||
| }; | ||
| this.#setState(this.options.initialState ?? {}); | ||
| } | ||
| /** | ||
| * Updates the throttler options | ||
| */ | ||
| setOptions(newOptions) { | ||
| this._options = { ...this._options, ...newOptions }; | ||
| if (!this._options.enabled) { | ||
| this.cancel(); | ||
| } | ||
| } | ||
| /** | ||
| * Returns the current throttler options | ||
| */ | ||
| getOptions() { | ||
| return this._options; | ||
| } | ||
| /** | ||
| * Returns the current enabled state of the throttler | ||
| */ | ||
| getEnabled() { | ||
| return utils.parseFunctionOrValue(this._options.enabled, this); | ||
| } | ||
| /** | ||
| * Returns the current wait time in milliseconds | ||
| */ | ||
| getWait() { | ||
| return utils.parseFunctionOrValue(this._options.wait, this); | ||
| } | ||
| /** | ||
| * Attempts to execute the throttled function. The execution behavior depends on the throttler options: | ||
| * | ||
| * - If enough time has passed since the last execution (>= wait period): | ||
| * - With leading=true: Executes immediately | ||
| * - With leading=false: Waits for the next trailing execution | ||
| * | ||
| * - If within the wait period: | ||
| * - With trailing=true: Schedules execution for end of wait period | ||
| * - With trailing=false: Drops the execution | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * const throttled = new Throttler(fn, { wait: 1000 }); | ||
| * | ||
| * // First call executes immediately | ||
| * throttled.maybeExecute('a', 'b'); | ||
| * | ||
| * // Call during wait period - gets throttled | ||
| * throttled.maybeExecute('c', 'd'); | ||
| * ``` | ||
| */ | ||
| maybeExecute(...args) { | ||
| const now = Date.now(); | ||
| const timeSinceLastExecution = now - this._lastExecutionTime; | ||
| const wait = this.getWait(); | ||
| if (this._options.leading && timeSinceLastExecution >= wait) { | ||
| this.execute(...args); | ||
| } else { | ||
| this._lastArgs = args; | ||
| if (!this._timeoutId && this._options.trailing) { | ||
| const _timeSinceLastExecution = this._lastExecutionTime ? now - this._lastExecutionTime : 0; | ||
| const timeoutDuration = wait - _timeSinceLastExecution; | ||
| this._timeoutId = setTimeout(() => { | ||
| if (this._lastArgs !== void 0) { | ||
| this.execute(...this._lastArgs); | ||
| } | ||
| }, timeoutDuration); | ||
| } | ||
| } | ||
| } | ||
| execute(...args) { | ||
| if (!this.getEnabled()) return; | ||
| this.fn(...args); | ||
| this._executionCount++; | ||
| this._lastExecutionTime = Date.now(); | ||
| this._timeoutId = void 0; | ||
| this._lastArgs = void 0; | ||
| this._options.onExecute(this); | ||
| } | ||
| /** | ||
| * Cancels any pending trailing execution and clears internal state. | ||
| * | ||
| * If a trailing execution is scheduled (due to throttling with trailing=true), | ||
| * this will prevent that execution from occurring. The internal timeout and | ||
| * stored arguments will be cleared. | ||
| * | ||
| * Has no effect if there is no pending execution. | ||
| */ | ||
| cancel() { | ||
| if (this._timeoutId) { | ||
| clearTimeout(this._timeoutId); | ||
| this._timeoutId = void 0; | ||
| this._lastArgs = void 0; | ||
| } | ||
| } | ||
| /** | ||
| * Returns the last execution time | ||
| */ | ||
| getLastExecutionTime() { | ||
| return this._lastExecutionTime; | ||
| } | ||
| /** | ||
| * Returns the next execution time | ||
| */ | ||
| getNextExecutionTime() { | ||
| return this._lastExecutionTime + this.getWait(); | ||
| } | ||
| /** | ||
| * Returns the number of times the function has been executed | ||
| */ | ||
| getExecutionCount() { | ||
| return this._executionCount; | ||
| } | ||
| /** | ||
| * Returns `true` if there is a pending execution | ||
| */ | ||
| getIsPending() { | ||
| return this.getEnabled() && !!this._timeoutId; | ||
| } | ||
| #timeoutId; | ||
| #setState; | ||
| #getEnabled; | ||
| #getWait; | ||
| #execute; | ||
| #clearTimeout; | ||
| } | ||
| function throttle(fn, initialOptions) { | ||
| const throttler = new Throttler(fn, initialOptions); | ||
| return throttler.maybeExecute.bind(throttler); | ||
| return throttler.maybeExecute; | ||
| } | ||
@@ -145,0 +129,0 @@ exports.Throttler = Throttler; |
@@ -1,1 +0,1 @@ | ||
| {"version":3,"file":"throttler.cjs","sources":["../../src/throttler.ts"],"sourcesContent":["import { parseFunctionOrValue } from './utils'\nimport type { AnyFunction } from './types'\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 * 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?: (throttler: Throttler<TFn>) => void\n /**\n * Whether to execute on the trailing edge of the timeout.\n * Defaults to true.\n */\n trailing?: boolean\n /**\n * Time window in milliseconds during which the function can only be executed once.\n * Can be a number or a function that returns a number.\n * Defaults to 0ms\n */\n wait: number | ((throttler: Throttler<TFn>) => number)\n}\n\nconst defaultOptions: Required<ThrottlerOptions<any>> = {\n enabled: true,\n leading: true,\n onExecute: () => {},\n trailing: true,\n wait: 0,\n}\n\n/**\n * A class that creates a throttled function.\n *\n * Throttling ensures a function is called at most once within a specified time window.\n * Unlike debouncing which waits for a pause in calls, throttling guarantees consistent\n * execution timing regardless of call frequency.\n *\n * Supports both leading and trailing edge execution:\n * - Leading: Execute immediately on first call (default: true)\n * - Trailing: Execute after wait period if called during throttle (default: true)\n *\n * For collapsing rapid-fire events where you only care about the last call, consider using Debouncer.\n *\n * @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 private _executionCount = 0\n private _lastArgs: Parameters<TFn> | undefined\n private _lastExecutionTime = 0\n private _options: Required<ThrottlerOptions<TFn>>\n private _timeoutId: NodeJS.Timeout | undefined\n\n constructor(\n private fn: TFn,\n initialOptions: ThrottlerOptions<TFn>,\n ) {\n this._options = {\n ...defaultOptions,\n ...initialOptions,\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 // End the pending state if the debouncer is disabled\n if (!this._options.enabled) {\n this.cancel()\n }\n }\n\n /**\n * Returns the current throttler options\n */\n getOptions(): Required<ThrottlerOptions<TFn>> {\n return this._options\n }\n\n /**\n * Returns the current enabled state of the 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 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 const now = Date.now()\n const timeSinceLastExecution = now - this._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._lastArgs = args\n\n // Set up trailing execution if not already scheduled\n if (!this._timeoutId && this._options.trailing) {\n const _timeSinceLastExecution = this._lastExecutionTime\n ? now - this._lastExecutionTime\n : 0\n const timeoutDuration = wait - _timeSinceLastExecution\n this._timeoutId = setTimeout(() => {\n if (this._lastArgs !== undefined) {\n this.execute(...this._lastArgs)\n }\n }, timeoutDuration)\n }\n }\n }\n\n private execute(...args: Parameters<TFn>): void {\n if (!this.getEnabled()) return\n this.fn(...args) // EXECUTE!\n this._executionCount++\n this._lastExecutionTime = Date.now()\n this._timeoutId = undefined\n this._lastArgs = undefined\n this._options.onExecute(this)\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 if (this._timeoutId) {\n clearTimeout(this._timeoutId)\n this._timeoutId = undefined\n this._lastArgs = undefined\n }\n }\n\n /**\n * Returns the last execution time\n */\n getLastExecutionTime(): number {\n return this._lastExecutionTime\n }\n\n /**\n * Returns the next execution time\n */\n getNextExecutionTime(): number {\n return this._lastExecutionTime + this.getWait()\n }\n\n /**\n * Returns the number of times the function has been executed\n */\n getExecutionCount(): number {\n return this._executionCount\n }\n\n /**\n * Returns `true` if there is a pending execution\n */\n getIsPending(): boolean {\n return this.getEnabled() && !!this._timeoutId\n }\n}\n\n/**\n * Creates a throttled function that limits how often the provided function can execute.\n *\n * Throttling ensures a function executes at most once within a specified time window,\n * regardless of how many times it is called. This is useful for rate-limiting\n * expensive operations or UI updates.\n *\n * The throttled function can be configured to execute on the leading and/or trailing\n * edge of the throttle window via options.\n *\n * For handling bursts of events, consider using debounce() instead. For hard execution\n * limits, consider using rateLimit().\n *\n * @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.bind(throttler)\n}\n"],"names":["parseFunctionOrValue"],"mappings":";;;AAmCA,MAAM,iBAAkD;AAAA,EACtD,SAAS;AAAA,EACT,SAAS;AAAA,EACT,WAAW,MAAM;AAAA,EAAC;AAAA,EAClB,UAAU;AAAA,EACV,MAAM;AACR;AA6BO,MAAM,UAAmC;AAAA,EAO9C,YACU,IACR,gBACA;AAFQ,SAAA,KAAA;AAPV,SAAQ,kBAAkB;AAE1B,SAAQ,qBAAqB;AAQ3B,SAAK,WAAW;AAAA,MACd,GAAG;AAAA,MACH,GAAG;AAAA,IACL;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA,EAMF,WAAW,YAAkD;AAC3D,SAAK,WAAW,EAAE,GAAG,KAAK,UAAU,GAAG,WAAW;AAG9C,QAAA,CAAC,KAAK,SAAS,SAAS;AAC1B,WAAK,OAAO;AAAA,IAAA;AAAA,EACd;AAAA;AAAA;AAAA;AAAA,EAMF,aAA8C;AAC5C,WAAO,KAAK;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA,EAMd,aAAsB;AACpB,WAAOA,MAAqB,qBAAA,KAAK,SAAS,SAAS,IAAI;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA,EAMzD,UAAkB;AAChB,WAAOA,MAAqB,qBAAA,KAAK,SAAS,MAAM,IAAI;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAyBtD,gBAAgB,MAA6B;AACrC,UAAA,MAAM,KAAK,IAAI;AACf,UAAA,yBAAyB,MAAM,KAAK;AACpC,UAAA,OAAO,KAAK,QAAQ;AAG1B,QAAI,KAAK,SAAS,WAAW,0BAA0B,MAAM;AACtD,WAAA,QAAQ,GAAG,IAAI;AAAA,IAAA,OACf;AAEL,WAAK,YAAY;AAGjB,UAAI,CAAC,KAAK,cAAc,KAAK,SAAS,UAAU;AAC9C,cAAM,0BAA0B,KAAK,qBACjC,MAAM,KAAK,qBACX;AACJ,cAAM,kBAAkB,OAAO;AAC1B,aAAA,aAAa,WAAW,MAAM;AAC7B,cAAA,KAAK,cAAc,QAAW;AAC3B,iBAAA,QAAQ,GAAG,KAAK,SAAS;AAAA,UAAA;AAAA,WAE/B,eAAe;AAAA,MAAA;AAAA,IACpB;AAAA,EACF;AAAA,EAGM,WAAW,MAA6B;AAC1C,QAAA,CAAC,KAAK,aAAc;AACnB,SAAA,GAAG,GAAG,IAAI;AACV,SAAA;AACA,SAAA,qBAAqB,KAAK,IAAI;AACnC,SAAK,aAAa;AAClB,SAAK,YAAY;AACZ,SAAA,SAAS,UAAU,IAAI;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAY9B,SAAe;AACb,QAAI,KAAK,YAAY;AACnB,mBAAa,KAAK,UAAU;AAC5B,WAAK,aAAa;AAClB,WAAK,YAAY;AAAA,IAAA;AAAA,EACnB;AAAA;AAAA;AAAA;AAAA,EAMF,uBAA+B;AAC7B,WAAO,KAAK;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA,EAMd,uBAA+B;AACtB,WAAA,KAAK,qBAAqB,KAAK,QAAQ;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA,EAMhD,oBAA4B;AAC1B,WAAO,KAAK;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA,EAMd,eAAwB;AACtB,WAAO,KAAK,WAAA,KAAgB,CAAC,CAAC,KAAK;AAAA,EAAA;AAEvC;AA4BgB,SAAA,SACd,IACA,gBACA;AACA,QAAM,YAAY,IAAI,UAAU,IAAI,cAAc;AAC3C,SAAA,UAAU,aAAa,KAAK,SAAS;AAC9C;;;"} | ||
| {"version":3,"file":"throttler.cjs","sources":["../../src/throttler.ts"],"sourcesContent":["import { Store } from '@tanstack/store'\nimport { parseFunctionOrValue } from './utils'\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 * 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 * Timestamp when the next execution can occur in milliseconds\n */\n nextExecutionTime: number\n /**\n * Whether the throttler is waiting for the timeout to trigger execution\n */\n isPending: boolean\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 structuredClone({\n executionCount: 0,\n isPending: false,\n lastArgs: undefined,\n lastExecutionTime: 0,\n nextExecutionTime: 0,\n status: 'idle',\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 * 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?: (throttler: Throttler<TFn>) => void\n /**\n * Whether to execute on the trailing edge of the timeout.\n * Defaults to true.\n */\n trailing?: boolean\n /**\n * Time window in milliseconds during which the function can only be executed once.\n * Can be a number or a function that returns a number.\n * Defaults to 0ms\n */\n wait: number | ((throttler: Throttler<TFn>) => number)\n}\n\nconst defaultOptions: Omit<\n Required<ThrottlerOptions<any>>,\n 'initialState' | 'onExecute'\n> = {\n enabled: true,\n leading: true,\n trailing: true,\n wait: 0,\n}\n\n/**\n * A class that creates a throttled function.\n *\n * Throttling ensures a function is called at most once within a specified time window.\n * Unlike debouncing which waits for a pause in calls, throttling guarantees consistent\n * execution timing regardless of call frequency.\n *\n * Supports both leading and trailing edge execution:\n * - Leading: Execute immediately on first call (default: true)\n * - Trailing: Execute after wait period if called during throttle (default: true)\n *\n * For collapsing rapid-fire events where you only care about the last call, consider using Debouncer.\n *\n * State Management:\n * - Uses TanStack Store for reactive state management\n * - Use `initialState` to provide initial state values when creating the throttler\n * - Use `onExecute` callback to react to function execution and implement custom logic\n * - The state includes execution count, last execution time, pending status, and more\n * - State can be accessed via `throttler.store.state` when using the class directly\n * - When using framework adapters (React/Solid), state is accessed from `throttler.state`\n *\n * @example\n * ```ts\n * const throttler = new Throttler(\n * (id: string) => api.getData(id),\n * { wait: 1000 } // Execute at most once per second\n * );\n *\n * // First call executes immediately\n * throttler.maybeExecute('123');\n *\n * // Subsequent calls within 1000ms are throttled\n * throttler.maybeExecute('123'); // Throttled\n * ```\n */\nexport class Throttler<TFn extends AnyFunction> {\n readonly store: Store<Readonly<ThrottlerState<TFn>>> = new Store(\n getDefaultThrottlerState(),\n )\n options: ThrottlerOptions<TFn>\n #timeoutId: NodeJS.Timeout | undefined\n\n constructor(\n private fn: TFn,\n initialOptions: ThrottlerOptions<TFn>,\n ) {\n this.options = {\n ...defaultOptions,\n ...initialOptions,\n }\n this.#setState(this.options.initialState ?? {})\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 }\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 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?.(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.#execute(...this.store.state.lastArgs)\n }\n }\n\n #clearTimeout = (): void => {\n if (this.#timeoutId) {\n clearTimeout(this.#timeoutId)\n this.#timeoutId = undefined\n }\n }\n\n /**\n * Cancels any pending trailing execution and clears internal state.\n *\n * If a trailing execution is scheduled (due to throttling with trailing=true),\n * this will prevent that execution from occurring. The internal timeout and\n * stored arguments will be cleared.\n *\n * Has no effect if there is no pending execution.\n */\n cancel = (): void => {\n this.#clearTimeout()\n this.#setState({\n lastArgs: undefined,\n isPending: false,\n })\n }\n\n /**\n * Resets the throttler state to its default values\n */\n reset = (): void => {\n this.#setState(getDefaultThrottlerState<TFn>())\n }\n}\n\n/**\n * Creates a throttled function that limits how often the provided function can execute.\n *\n * Throttling ensures a function executes at most once within a specified time window,\n * regardless of how many times it is called. This is useful for rate-limiting\n * expensive operations or UI updates.\n *\n * The throttled function can be configured to execute on the leading and/or trailing\n * edge of the throttle window via options.\n *\n * For handling bursts of events, consider using debounce() instead. For hard execution\n * limits, consider using rateLimit().\n *\n * State Management:\n * - Uses TanStack Store for reactive state management\n * - Use `initialState` to provide initial state values when creating the throttler\n * - Use `onExecute` callback to react to function execution and implement custom logic\n * - The state includes execution count, last execution time, pending status, and more\n * - State can be accessed via the underlying Throttler instance's `store.state` property\n * - When using framework adapters (React/Solid), state is accessed from the hook's state property\n *\n * @example\n * ```ts\n * // Basic throttling - max once per second\n * const throttled = throttle(updateUI, { wait: 1000 });\n *\n * // Configure leading/trailing execution\n * const throttled = throttle(saveData, {\n * wait: 2000,\n * leading: true, // Execute immediately on first call\n * trailing: true // Execute again after delay if called during wait\n * });\n * ```\n */\nexport function throttle<TFn extends AnyFunction>(\n fn: TFn,\n initialOptions: ThrottlerOptions<TFn>,\n) {\n const throttler = new Throttler(fn, initialOptions)\n return throttler.maybeExecute\n}\n"],"names":["Store","parseFunctionOrValue"],"mappings":"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"} |
+59
-35
@@ -0,2 +1,29 @@ | ||
| import { Store } from '@tanstack/store'; | ||
| import { AnyFunction } from './types.cjs'; | ||
| export interface ThrottlerState<TFn extends AnyFunction> { | ||
| /** | ||
| * Number of function executions that have been completed | ||
| */ | ||
| executionCount: number; | ||
| /** | ||
| * The arguments from the most recent call to maybeExecute | ||
| */ | ||
| lastArgs: Parameters<TFn> | undefined; | ||
| /** | ||
| * Timestamp of the last function execution in milliseconds | ||
| */ | ||
| lastExecutionTime: number; | ||
| /** | ||
| * Timestamp when the next execution can occur in milliseconds | ||
| */ | ||
| nextExecutionTime: number; | ||
| /** | ||
| * Whether the throttler is waiting for the timeout to trigger execution | ||
| */ | ||
| isPending: boolean; | ||
| /** | ||
| * Current execution status - 'idle' when not active, 'pending' when waiting for timeout | ||
| */ | ||
| status: 'disabled' | 'idle' | 'pending'; | ||
| } | ||
| /** | ||
@@ -13,2 +40,6 @@ * Options for configuring a throttled function | ||
| /** | ||
| * Initial state for the throttler | ||
| */ | ||
| initialState?: Partial<ThrottlerState<TFn>>; | ||
| /** | ||
| * Whether to execute on the leading edge of the timeout. | ||
@@ -47,2 +78,10 @@ * Defaults to true. | ||
| * | ||
| * State Management: | ||
| * - Uses TanStack Store for reactive state management | ||
| * - Use `initialState` to provide initial state values when creating the throttler | ||
| * - Use `onExecute` callback to react to function execution and implement custom logic | ||
| * - The state includes execution count, last execution time, pending status, and more | ||
| * - State can be accessed via `throttler.store.state` when using the class directly | ||
| * - When using framework adapters (React/Solid), state is accessed from `throttler.state` | ||
| * | ||
| * @example | ||
@@ -63,8 +102,6 @@ * ```ts | ||
| export declare class Throttler<TFn extends AnyFunction> { | ||
| #private; | ||
| private fn; | ||
| private _executionCount; | ||
| private _lastArgs; | ||
| private _lastExecutionTime; | ||
| private _options; | ||
| private _timeoutId; | ||
| readonly store: Store<Readonly<ThrottlerState<TFn>>>; | ||
| options: ThrottlerOptions<TFn>; | ||
| constructor(fn: TFn, initialOptions: ThrottlerOptions<TFn>); | ||
@@ -74,16 +111,4 @@ /** | ||
| */ | ||
| setOptions(newOptions: Partial<ThrottlerOptions<TFn>>): void; | ||
| setOptions: (newOptions: Partial<ThrottlerOptions<TFn>>) => void; | ||
| /** | ||
| * Returns the current throttler options | ||
| */ | ||
| getOptions(): Required<ThrottlerOptions<TFn>>; | ||
| /** | ||
| * Returns the current enabled state of the throttler | ||
| */ | ||
| getEnabled(): boolean; | ||
| /** | ||
| * Returns the current wait time in milliseconds | ||
| */ | ||
| getWait(): number; | ||
| /** | ||
| * Attempts to execute the throttled function. The execution behavior depends on the throttler options: | ||
@@ -110,5 +135,8 @@ * | ||
| */ | ||
| maybeExecute(...args: Parameters<TFn>): void; | ||
| private execute; | ||
| maybeExecute: (...args: Parameters<TFn>) => void; | ||
| /** | ||
| * Processes the current pending execution immediately | ||
| */ | ||
| flush: () => void; | ||
| /** | ||
| * Cancels any pending trailing execution and clears internal state. | ||
@@ -122,19 +150,7 @@ * | ||
| */ | ||
| cancel(): void; | ||
| cancel: () => void; | ||
| /** | ||
| * Returns the last execution time | ||
| * Resets the throttler state to its default values | ||
| */ | ||
| getLastExecutionTime(): number; | ||
| /** | ||
| * Returns the next execution time | ||
| */ | ||
| getNextExecutionTime(): number; | ||
| /** | ||
| * Returns the number of times the function has been executed | ||
| */ | ||
| getExecutionCount(): number; | ||
| /** | ||
| * Returns `true` if there is a pending execution | ||
| */ | ||
| getIsPending(): boolean; | ||
| reset: () => void; | ||
| } | ||
@@ -154,2 +170,10 @@ /** | ||
| * | ||
| * State Management: | ||
| * - Uses TanStack Store for reactive state management | ||
| * - Use `initialState` to provide initial state values when creating the throttler | ||
| * - Use `onExecute` callback to react to function execution and implement custom logic | ||
| * - The state includes execution count, last execution time, pending status, and more | ||
| * - State can be accessed via the underlying Throttler instance's `store.state` property | ||
| * - When using framework adapters (React/Solid), state is accessed from the hook's state property | ||
| * | ||
| * @example | ||
@@ -156,0 +180,0 @@ * ```ts |
+0
-13
@@ -9,17 +9,4 @@ "use strict"; | ||
| } | ||
| function bindInstanceMethods(instance) { | ||
| return Object.getOwnPropertyNames(Object.getPrototypeOf(instance)).reduce( | ||
| (acc, key) => { | ||
| const method = instance[key]; | ||
| if (isFunction(method)) { | ||
| acc[key] = method.bind(instance); | ||
| } | ||
| return acc; | ||
| }, | ||
| instance | ||
| ); | ||
| } | ||
| exports.bindInstanceMethods = bindInstanceMethods; | ||
| exports.isFunction = isFunction; | ||
| exports.parseFunctionOrValue = parseFunctionOrValue; | ||
| //# sourceMappingURL=utils.cjs.map |
@@ -1,1 +0,1 @@ | ||
| {"version":3,"file":"utils.cjs","sources":["../../src/utils.ts"],"sourcesContent":["import type { AnyFunction } from './types'\n\nexport function isFunction<T extends AnyFunction>(value: any): value is T {\n return typeof value === 'function'\n}\n\nexport function parseFunctionOrValue<T, TArgs extends Array<any>>(\n value: T | ((...args: TArgs) => T),\n ...args: TArgs\n): T {\n return isFunction(value) ? value(...args) : value\n}\n\nexport function bindInstanceMethods<T extends Record<string, any>>(\n instance: T,\n): T {\n return Object.getOwnPropertyNames(Object.getPrototypeOf(instance)).reduce(\n (acc: any, key) => {\n const method = instance[key as keyof T]\n if (isFunction(method)) {\n acc[key] = method.bind(instance)\n }\n return acc\n },\n instance,\n )\n}\n"],"names":[],"mappings":";;AAEO,SAAS,WAAkC,OAAwB;AACxE,SAAO,OAAO,UAAU;AAC1B;AAEgB,SAAA,qBACd,UACG,MACA;AACH,SAAO,WAAW,KAAK,IAAI,MAAM,GAAG,IAAI,IAAI;AAC9C;AAEO,SAAS,oBACd,UACG;AACH,SAAO,OAAO,oBAAoB,OAAO,eAAe,QAAQ,CAAC,EAAE;AAAA,IACjE,CAAC,KAAU,QAAQ;AACX,YAAA,SAAS,SAAS,GAAc;AAClC,UAAA,WAAW,MAAM,GAAG;AACtB,YAAI,GAAG,IAAI,OAAO,KAAK,QAAQ;AAAA,MAAA;AAE1B,aAAA;AAAA,IACT;AAAA,IACA;AAAA,EACF;AACF;;;;"} | ||
| {"version":3,"file":"utils.cjs","sources":["../../src/utils.ts"],"sourcesContent":["import type { AnyFunction } from './types'\n\nexport function isFunction<T extends AnyFunction>(value: any): value is T {\n return typeof value === 'function'\n}\n\nexport function parseFunctionOrValue<T, TArgs extends Array<any>>(\n value: T | ((...args: TArgs) => T),\n ...args: TArgs\n): T {\n return isFunction(value) ? value(...args) : value\n}\n"],"names":[],"mappings":";;AAEO,SAAS,WAAkC,OAAwB;AACxE,SAAO,OAAO,UAAU;AAC1B;AAEO,SAAS,qBACd,UACG,MACA;AACH,SAAO,WAAW,KAAK,IAAI,MAAM,GAAG,IAAI,IAAI;AAC9C;;;"} |
| import { AnyFunction } from './types.cjs'; | ||
| export declare function isFunction<T extends AnyFunction>(value: any): value is T; | ||
| export declare function parseFunctionOrValue<T, TArgs extends Array<any>>(value: T | ((...args: TArgs) => T), ...args: TArgs): T; | ||
| export declare function bindInstanceMethods<T extends Record<string, any>>(instance: T): T; |
@@ -0,2 +1,41 @@ | ||
| import { Store } from '@tanstack/store'; | ||
| import { AnyAsyncFunction } from './types.js'; | ||
| export interface AsyncDebouncerState<TFn extends AnyAsyncFunction> { | ||
| /** | ||
| * Whether the debouncer can execute on the leading edge of the timeout | ||
| */ | ||
| canLeadingExecute: boolean; | ||
| /** | ||
| * Number of function executions that have resulted in errors | ||
| */ | ||
| errorCount: number; | ||
| /** | ||
| * Whether the debounced function is currently executing asynchronously | ||
| */ | ||
| isExecuting: boolean; | ||
| /** | ||
| * Whether the debouncer is waiting for the timeout to trigger execution | ||
| */ | ||
| isPending: boolean; | ||
| /** | ||
| * The arguments from the most recent call to maybeExecute | ||
| */ | ||
| lastArgs: Parameters<TFn> | undefined; | ||
| /** | ||
| * The result from the most recent successful function execution | ||
| */ | ||
| lastResult: ReturnType<TFn> | undefined; | ||
| /** | ||
| * Number of function executions that have completed (either successfully or with errors) | ||
| */ | ||
| settleCount: number; | ||
| /** | ||
| * Current execution status - 'idle' when not active, 'pending' when waiting, 'executing' when running, 'settled' when completed | ||
| */ | ||
| status: 'disabled' | 'idle' | 'pending' | 'executing' | 'settled'; | ||
| /** | ||
| * Number of function executions that have completed successfully | ||
| */ | ||
| successCount: number; | ||
| } | ||
| /** | ||
@@ -13,2 +52,6 @@ * Options for configuring an async debounced function | ||
| /** | ||
| * Initial state for the async debouncer | ||
| */ | ||
| initialState?: Partial<AsyncDebouncerState<TFn>>; | ||
| /** | ||
| * Whether to execute on the leading edge of the timeout. | ||
@@ -65,7 +108,15 @@ * Defaults to false. | ||
| * Error Handling: | ||
| * - If an error occurs during execution and no `onError` handler is provided, the error will be thrown and propagate up to the caller. | ||
| * - If an `onError` handler is provided, errors will be caught and passed to the handler instead of being thrown. | ||
| * - The error count can be tracked using `getErrorCount()`. | ||
| * - The debouncer maintains its state and can continue to be used after an error occurs. | ||
| * - If an `onError` handler is provided, it will be called with the error and debouncer instance | ||
| * - If `throwOnError` is true (default when no onError handler is provided), the error will be thrown | ||
| * - If `throwOnError` is false (default when onError handler is provided), the error will be swallowed | ||
| * - Both onError and throwOnError can be used together - the handler will be called before any error is thrown | ||
| * - The error state can be checked using the underlying store | ||
| * | ||
| * State Management: | ||
| * - The debouncer uses a reactive store for state management | ||
| * - Use `initialState` to provide initial state values when creating the async debouncer | ||
| * - The state includes canLeadingExecute, error count, execution status, and success/settle counts | ||
| * - State can be accessed via the `store` property and its `state` getter | ||
| * - The store is reactive and will notify subscribers of state changes | ||
| * | ||
| * @example | ||
@@ -89,33 +140,12 @@ * ```ts | ||
| export declare class AsyncDebouncer<TFn extends AnyAsyncFunction> { | ||
| #private; | ||
| private fn; | ||
| private _options; | ||
| private _abortController; | ||
| private _canLeadingExecute; | ||
| private _errorCount; | ||
| private _isExecuting; | ||
| private _isPending; | ||
| private _lastArgs; | ||
| private _lastResult; | ||
| private _settleCount; | ||
| private _successCount; | ||
| private _timeoutId; | ||
| private _resolvePreviousPromise; | ||
| readonly store: Store<Readonly<AsyncDebouncerState<TFn>>>; | ||
| options: AsyncDebouncerOptions<TFn>; | ||
| constructor(fn: TFn, initialOptions: AsyncDebouncerOptions<TFn>); | ||
| /** | ||
| * Updates the debouncer options | ||
| * Updates the async debouncer options | ||
| */ | ||
| setOptions(newOptions: Partial<AsyncDebouncerOptions<TFn>>): void; | ||
| setOptions: (newOptions: Partial<AsyncDebouncerOptions<TFn>>) => void; | ||
| /** | ||
| * Returns the current debouncer options | ||
| */ | ||
| getOptions(): AsyncDebouncerOptions<TFn>; | ||
| /** | ||
| * Returns the current debouncer enabled state | ||
| */ | ||
| getEnabled(): boolean; | ||
| /** | ||
| * Returns the current debouncer wait state | ||
| */ | ||
| getWait(): number; | ||
| /** | ||
| * Attempts to execute the debounced function. | ||
@@ -134,36 +164,15 @@ * If a call is already in progress, it will be queued. | ||
| */ | ||
| maybeExecute(...args: Parameters<TFn>): Promise<ReturnType<TFn> | undefined>; | ||
| private execute; | ||
| maybeExecute: (...args: Parameters<TFn>) => Promise<ReturnType<TFn> | undefined>; | ||
| /** | ||
| * Cancel without resetting _canLeadingExecute | ||
| * Processes the current pending execution immediately | ||
| */ | ||
| private _cancel; | ||
| flush: () => void; | ||
| /** | ||
| * Cancels any pending execution or aborts any execution in progress | ||
| */ | ||
| cancel(): void; | ||
| cancel: () => void; | ||
| /** | ||
| * Returns the last result of the debounced function | ||
| * Resets the debouncer state to its default values | ||
| */ | ||
| getLastResult(): ReturnType<TFn> | undefined; | ||
| /** | ||
| * Returns the number of times the function has been executed successfully | ||
| */ | ||
| getSuccessCount(): number; | ||
| /** | ||
| * Returns the number of times the function has settled (completed or errored) | ||
| */ | ||
| getSettleCount(): number; | ||
| /** | ||
| * Returns the number of times the function has errored | ||
| */ | ||
| getErrorCount(): number; | ||
| /** | ||
| * Returns `true` if there is a pending execution queued up for trailing execution | ||
| */ | ||
| getIsPending(): boolean; | ||
| /** | ||
| * Returns `true` if there is currently an execution in progress | ||
| */ | ||
| getIsExecuting(): boolean; | ||
| reset: () => void; | ||
| } | ||
@@ -186,2 +195,12 @@ /** | ||
| * | ||
| * State Management: | ||
| * - Uses TanStack Store for reactive state management | ||
| * - Use `initialState` to provide initial state values when creating the async debouncer | ||
| * - Use `onSuccess` callback to react to successful function execution and implement custom logic | ||
| * - Use `onError` callback to react to function execution errors and implement custom error handling | ||
| * - Use `onSettled` callback to react to function execution completion (success or error) and implement custom logic | ||
| * - The state includes canLeadingExecute, error count, execution status, and success/settle counts | ||
| * - State can be accessed via `asyncDebouncer.store.state` when using the class directly | ||
| * - When using framework adapters (React/Solid), state is accessed from `asyncDebouncer.state` | ||
| * | ||
| * @example | ||
@@ -188,0 +207,0 @@ * ```ts |
+149
-162
@@ -0,2 +1,16 @@ | ||
| import { Store } from "@tanstack/store"; | ||
| import { parseFunctionOrValue } from "./utils.js"; | ||
| function getDefaultAsyncDebouncerState() { | ||
| return structuredClone({ | ||
| canLeadingExecute: true, | ||
| errorCount: 0, | ||
| isExecuting: false, | ||
| isPending: false, | ||
| lastArgs: void 0, | ||
| lastResult: void 0, | ||
| settleCount: 0, | ||
| successCount: 0, | ||
| status: "idle" | ||
| }); | ||
| } | ||
| const defaultOptions = { | ||
@@ -11,12 +25,125 @@ enabled: true, | ||
| this.fn = fn; | ||
| this._abortController = null; | ||
| this._canLeadingExecute = true; | ||
| this._errorCount = 0; | ||
| this._isExecuting = false; | ||
| this._isPending = false; | ||
| this._settleCount = 0; | ||
| this._successCount = 0; | ||
| this._timeoutId = null; | ||
| this._resolvePreviousPromise = null; | ||
| this._options = { | ||
| this.store = new Store(getDefaultAsyncDebouncerState()); | ||
| this.#abortController = null; | ||
| this.#timeoutId = null; | ||
| this.#resolvePreviousPromise = null; | ||
| this.setOptions = (newOptions) => { | ||
| this.options = { ...this.options, ...newOptions }; | ||
| if (!this.#getEnabled()) { | ||
| this.cancel(); | ||
| } | ||
| }; | ||
| this.#setState = (newState) => { | ||
| this.store.setState((state) => { | ||
| const combinedState = { | ||
| ...state, | ||
| ...newState | ||
| }; | ||
| const { isPending, isExecuting, settleCount } = combinedState; | ||
| return { | ||
| ...combinedState, | ||
| status: !this.#getEnabled() ? "disabled" : isPending ? "pending" : isExecuting ? "executing" : settleCount > 0 ? "settled" : "idle" | ||
| }; | ||
| }); | ||
| }; | ||
| this.#getEnabled = () => { | ||
| return !!parseFunctionOrValue(this.options.enabled, this); | ||
| }; | ||
| this.#getWait = () => { | ||
| return parseFunctionOrValue(this.options.wait, this); | ||
| }; | ||
| this.maybeExecute = async (...args) => { | ||
| if (!this.#getEnabled()) return void 0; | ||
| this.#cancelPendingExecution(); | ||
| this.#setState({ lastArgs: args }); | ||
| if (this.options.leading && this.store.state.canLeadingExecute) { | ||
| this.#setState({ canLeadingExecute: false }); | ||
| await this.#execute(...args); | ||
| return this.store.state.lastResult; | ||
| } | ||
| if (this.options.trailing && this.#getEnabled()) { | ||
| this.#setState({ isPending: true }); | ||
| } | ||
| return new Promise((resolve) => { | ||
| this.#resolvePreviousPromise = resolve; | ||
| this.#timeoutId = setTimeout(async () => { | ||
| if (this.options.trailing && this.store.state.lastArgs) { | ||
| await this.#execute(...this.store.state.lastArgs); | ||
| } | ||
| this.#setState({ canLeadingExecute: true }); | ||
| this.#resolvePreviousPromise = null; | ||
| resolve(this.store.state.lastResult); | ||
| }, this.#getWait()); | ||
| }); | ||
| }; | ||
| this.#execute = async (...args) => { | ||
| if (!this.#getEnabled()) return void 0; | ||
| this.#abortController = new AbortController(); | ||
| try { | ||
| this.#setState({ isExecuting: true }); | ||
| const result = await this.fn(...args); | ||
| this.#setState({ | ||
| lastResult: result, | ||
| successCount: this.store.state.successCount + 1 | ||
| }); | ||
| this.options.onSuccess?.(result, this); | ||
| } catch (error) { | ||
| this.#setState({ | ||
| errorCount: this.store.state.errorCount + 1 | ||
| }); | ||
| this.options.onError?.(error, this); | ||
| if (this.options.throwOnError) { | ||
| throw error; | ||
| } | ||
| } finally { | ||
| this.#setState({ | ||
| isExecuting: false, | ||
| isPending: false, | ||
| settleCount: this.store.state.settleCount + 1 | ||
| }); | ||
| this.#abortController = null; | ||
| this.options.onSettled?.(this); | ||
| } | ||
| return this.store.state.lastResult; | ||
| }; | ||
| this.flush = () => { | ||
| if (this.store.state.isPending && this.store.state.lastArgs) { | ||
| this.#abortExecution(); | ||
| this.#clearTimeout(); | ||
| this.#execute(...this.store.state.lastArgs); | ||
| } | ||
| }; | ||
| this.#clearTimeout = () => { | ||
| if (this.#timeoutId) { | ||
| clearTimeout(this.#timeoutId); | ||
| this.#timeoutId = null; | ||
| } | ||
| }; | ||
| this.#cancelPendingExecution = () => { | ||
| this.#clearTimeout(); | ||
| if (this.#resolvePreviousPromise) { | ||
| this.#resolvePreviousPromise(this.store.state.lastResult); | ||
| this.#resolvePreviousPromise = null; | ||
| } | ||
| this.#setState({ | ||
| isPending: false, | ||
| isExecuting: false, | ||
| lastArgs: void 0 | ||
| }); | ||
| }; | ||
| this.#abortExecution = () => { | ||
| if (this.#abortController) { | ||
| this.#abortController.abort(); | ||
| this.#abortController = null; | ||
| } | ||
| }; | ||
| this.cancel = () => { | ||
| this.#cancelPendingExecution(); | ||
| this.#abortExecution(); | ||
| this.#setState({ canLeadingExecute: true }); | ||
| }; | ||
| this.reset = () => { | ||
| this.#setState(getDefaultAsyncDebouncerState()); | ||
| }; | ||
| this.options = { | ||
| ...defaultOptions, | ||
@@ -26,158 +153,18 @@ ...initialOptions, | ||
| }; | ||
| this.#setState(this.options.initialState ?? {}); | ||
| } | ||
| /** | ||
| * Updates the debouncer options | ||
| */ | ||
| setOptions(newOptions) { | ||
| this._options = { ...this._options, ...newOptions }; | ||
| if (!this._options.enabled) { | ||
| this._isPending = false; | ||
| } | ||
| } | ||
| /** | ||
| * Returns the current debouncer options | ||
| */ | ||
| getOptions() { | ||
| return this._options; | ||
| } | ||
| /** | ||
| * Returns the current debouncer enabled state | ||
| */ | ||
| getEnabled() { | ||
| return !!parseFunctionOrValue(this._options.enabled, this); | ||
| } | ||
| /** | ||
| * Returns the current debouncer wait state | ||
| */ | ||
| getWait() { | ||
| return parseFunctionOrValue(this._options.wait, this); | ||
| } | ||
| /** | ||
| * Attempts to execute the debounced function. | ||
| * If a call is already in progress, it will be queued. | ||
| * | ||
| * Error Handling: | ||
| * - If the debounced function throws and no `onError` handler is configured, | ||
| * the error will be thrown from this method. | ||
| * - If an `onError` handler is configured, errors will be caught and passed to the handler, | ||
| * and this method will return undefined. | ||
| * - The error state can be checked using `getErrorCount()` and `getIsExecuting()`. | ||
| * | ||
| * @returns A promise that resolves with the function's return value, or undefined if an error occurred and was handled by onError | ||
| * @throws The error from the debounced function if no onError handler is configured | ||
| */ | ||
| async maybeExecute(...args) { | ||
| this._cancel(); | ||
| this._lastArgs = args; | ||
| if (this._options.leading && this._canLeadingExecute) { | ||
| this._canLeadingExecute = false; | ||
| await this.execute(...args); | ||
| return this._lastResult; | ||
| } | ||
| if (this._options.trailing) { | ||
| this._isPending = true; | ||
| } | ||
| return new Promise((resolve) => { | ||
| this._resolvePreviousPromise = resolve; | ||
| this._timeoutId = setTimeout(async () => { | ||
| if (this._options.trailing && this._lastArgs) { | ||
| await this.execute(...this._lastArgs); | ||
| } | ||
| this._canLeadingExecute = true; | ||
| this._resolvePreviousPromise = null; | ||
| resolve(this._lastResult); | ||
| }, this.getWait()); | ||
| }); | ||
| } | ||
| async execute(...args) { | ||
| var _a, _b, _c, _d, _e, _f; | ||
| if (!this.getEnabled()) return void 0; | ||
| this._abortController = new AbortController(); | ||
| try { | ||
| this._isExecuting = true; | ||
| this._lastResult = await this.fn(...args); | ||
| this._successCount++; | ||
| (_b = (_a = this._options).onSuccess) == null ? void 0 : _b.call(_a, this._lastResult, this); | ||
| } catch (error) { | ||
| this._errorCount++; | ||
| (_d = (_c = this._options).onError) == null ? void 0 : _d.call(_c, error, this); | ||
| if (this._options.throwOnError) { | ||
| throw error; | ||
| } | ||
| } finally { | ||
| this._isExecuting = false; | ||
| this._isPending = false; | ||
| this._settleCount++; | ||
| this._abortController = null; | ||
| (_f = (_e = this._options).onSettled) == null ? void 0 : _f.call(_e, this); | ||
| } | ||
| return this._lastResult; | ||
| } | ||
| /** | ||
| * Cancel without resetting _canLeadingExecute | ||
| */ | ||
| _cancel() { | ||
| if (this._timeoutId) { | ||
| clearTimeout(this._timeoutId); | ||
| this._timeoutId = null; | ||
| } | ||
| if (this._abortController) { | ||
| this._abortController.abort(); | ||
| this._abortController = null; | ||
| } | ||
| if (this._resolvePreviousPromise) { | ||
| this._resolvePreviousPromise(this._lastResult); | ||
| this._resolvePreviousPromise = null; | ||
| } | ||
| this._lastArgs = void 0; | ||
| this._isPending = false; | ||
| this._isExecuting = false; | ||
| } | ||
| /** | ||
| * Cancels any pending execution or aborts any execution in progress | ||
| */ | ||
| cancel() { | ||
| this._canLeadingExecute = true; | ||
| this._cancel(); | ||
| } | ||
| /** | ||
| * Returns the last result of the debounced function | ||
| */ | ||
| getLastResult() { | ||
| return this._lastResult; | ||
| } | ||
| /** | ||
| * Returns the number of times the function has been executed successfully | ||
| */ | ||
| getSuccessCount() { | ||
| return this._successCount; | ||
| } | ||
| /** | ||
| * Returns the number of times the function has settled (completed or errored) | ||
| */ | ||
| getSettleCount() { | ||
| return this._settleCount; | ||
| } | ||
| /** | ||
| * Returns the number of times the function has errored | ||
| */ | ||
| getErrorCount() { | ||
| return this._errorCount; | ||
| } | ||
| /** | ||
| * Returns `true` if there is a pending execution queued up for trailing execution | ||
| */ | ||
| getIsPending() { | ||
| return this.getEnabled() && this._isPending; | ||
| } | ||
| /** | ||
| * Returns `true` if there is currently an execution in progress | ||
| */ | ||
| getIsExecuting() { | ||
| return this._isExecuting; | ||
| } | ||
| #abortController; | ||
| #timeoutId; | ||
| #resolvePreviousPromise; | ||
| #setState; | ||
| #getEnabled; | ||
| #getWait; | ||
| #execute; | ||
| #clearTimeout; | ||
| #cancelPendingExecution; | ||
| #abortExecution; | ||
| } | ||
| function asyncDebounce(fn, initialOptions) { | ||
| const asyncDebouncer = new AsyncDebouncer(fn, initialOptions); | ||
| return asyncDebouncer.maybeExecute.bind(asyncDebouncer); | ||
| return asyncDebouncer.maybeExecute; | ||
| } | ||
@@ -184,0 +171,0 @@ export { |
@@ -1,1 +0,1 @@ | ||
| {"version":3,"file":"async-debouncer.js","sources":["../../src/async-debouncer.ts"],"sourcesContent":["import { parseFunctionOrValue } from './utils'\nimport type { AnyAsyncFunction, OptionalKeys } from './types'\n\n/**\n * Options for configuring an async debounced function\n */\nexport interface AsyncDebouncerOptions<TFn extends AnyAsyncFunction> {\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 * 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?: (error: unknown, debouncer: AsyncDebouncer<TFn>) => void\n /**\n * Optional callback to call when the debounced function is executed\n */\n onSettled?: (debouncer: AsyncDebouncer<TFn>) => void\n /**\n * Optional callback to call when the debounced function is executed\n */\n onSuccess?: (result: ReturnType<TFn>, debouncer: AsyncDebouncer<TFn>) => 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\ntype AsyncDebouncerOptionsWithOptionalCallbacks = OptionalKeys<\n AsyncDebouncerOptions<any>,\n 'onError' | 'onSettled' | 'onSuccess'\n>\n\nconst defaultOptions: AsyncDebouncerOptionsWithOptionalCallbacks = {\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 * 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 * Unlike the non-async Debouncer, this async version supports returning values from the debounced function,\n * making it ideal for API calls and other async operations where you want the result of the `maybeExecute` call\n * instead of setting the result on a state variable from within the debounced function.\n *\n * Error Handling:\n * - If an error occurs during execution and no `onError` handler is provided, the error will be thrown and propagate up to the caller.\n * - If an `onError` handler is provided, errors will be caught and passed to the handler instead of being thrown.\n * - The error count can be tracked using `getErrorCount()`.\n * - The debouncer maintains its state and can continue to be used after an error occurs.\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 private _options: AsyncDebouncerOptionsWithOptionalCallbacks\n private _abortController: AbortController | null = null\n private _canLeadingExecute = true\n private _errorCount = 0\n private _isExecuting = false\n private _isPending = false\n private _lastArgs: Parameters<TFn> | undefined\n private _lastResult: ReturnType<TFn> | undefined\n private _settleCount = 0\n private _successCount = 0\n private _timeoutId: NodeJS.Timeout | null = null\n private _resolvePreviousPromise:\n | ((value?: ReturnType<TFn> | undefined) => void)\n | null = null\n\n constructor(\n private fn: TFn,\n initialOptions: AsyncDebouncerOptions<TFn>,\n ) {\n this._options = {\n ...defaultOptions,\n ...initialOptions,\n throwOnError: initialOptions.throwOnError ?? !initialOptions.onError,\n }\n }\n\n /**\n * Updates the debouncer options\n */\n setOptions(newOptions: Partial<AsyncDebouncerOptions<TFn>>): void {\n this._options = { ...this._options, ...newOptions }\n\n // End the pending state if the debouncer is disabled\n if (!this._options.enabled) {\n this._isPending = false\n }\n }\n\n /**\n * Returns the current debouncer options\n */\n getOptions(): AsyncDebouncerOptions<TFn> {\n return this._options\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 async maybeExecute(\n ...args: Parameters<TFn>\n ): Promise<ReturnType<TFn> | undefined> {\n this._cancel()\n this._lastArgs = args\n\n // Handle leading execution\n if (this._options.leading && this._canLeadingExecute) {\n this._canLeadingExecute = false\n await this.execute(...args)\n return this._lastResult\n }\n\n // Handle trailing execution\n if (this._options.trailing) {\n this._isPending = true\n }\n\n return new Promise((resolve) => {\n this._resolvePreviousPromise = resolve\n this._timeoutId = setTimeout(async () => {\n // Execute trailing if enabled\n if (this._options.trailing && this._lastArgs) {\n await this.execute(...this._lastArgs)\n }\n\n // Reset state and resolve\n this._canLeadingExecute = true\n this._resolvePreviousPromise = null\n resolve(this._lastResult)\n }, this.getWait())\n })\n }\n\n private async execute(\n ...args: Parameters<TFn>\n ): Promise<ReturnType<TFn> | undefined> {\n if (!this.getEnabled()) return undefined\n this._abortController = new AbortController()\n try {\n this._isExecuting = true\n this._lastResult = await this.fn(...args) // EXECUTE!\n this._successCount++\n this._options.onSuccess?.(this._lastResult!, this)\n } catch (error) {\n this._errorCount++\n this._options.onError?.(error, this)\n if (this._options.throwOnError) {\n throw error\n }\n } finally {\n this._isExecuting = false\n this._isPending = false\n this._settleCount++\n this._abortController = null\n this._options.onSettled?.(this)\n }\n return this._lastResult\n }\n\n /**\n * Cancel without resetting _canLeadingExecute\n */\n private _cancel(): void {\n if (this._timeoutId) {\n clearTimeout(this._timeoutId)\n this._timeoutId = null\n }\n if (this._abortController) {\n this._abortController.abort()\n this._abortController = null\n }\n if (this._resolvePreviousPromise) {\n this._resolvePreviousPromise(this._lastResult)\n this._resolvePreviousPromise = null\n }\n this._lastArgs = undefined\n this._isPending = false\n this._isExecuting = false\n }\n\n /**\n * Cancels any pending execution or aborts any execution in progress\n */\n cancel(): void {\n this._canLeadingExecute = true\n this._cancel()\n }\n\n /**\n * Returns the last result of the debounced function\n */\n getLastResult(): ReturnType<TFn> | undefined {\n return this._lastResult\n }\n\n /**\n * Returns the number of times the function has been executed successfully\n */\n getSuccessCount(): number {\n return this._successCount\n }\n\n /**\n * Returns the number of times the function has settled (completed or errored)\n */\n getSettleCount(): number {\n return this._settleCount\n }\n\n /**\n * Returns the number of times the function has errored\n */\n getErrorCount(): number {\n return this._errorCount\n }\n\n /**\n * Returns `true` if there is a pending execution queued up for trailing execution\n */\n getIsPending(): boolean {\n return this.getEnabled() && this._isPending\n }\n\n /**\n * Returns `true` if there is currently an execution in progress\n */\n getIsExecuting(): boolean {\n return this._isExecuting\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 * Unlike the non-async Debouncer, this async version supports returning values from the debounced function,\n * making it ideal for API calls and other async operations where you want the result of the `maybeExecute` call\n * instead of setting the result on a state variable from within the debounced function.\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 * @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.bind(asyncDebouncer)\n}\n"],"names":[],"mappings":"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"} | ||
| {"version":3,"file":"async-debouncer.js","sources":["../../src/async-debouncer.ts"],"sourcesContent":["import { Store } from '@tanstack/store'\nimport { parseFunctionOrValue } from './utils'\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 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 structuredClone({\n canLeadingExecute: true,\n errorCount: 0,\n isExecuting: false,\n isPending: false,\n lastArgs: undefined,\n lastResult: undefined,\n settleCount: 0,\n successCount: 0,\n status: 'idle',\n })\n}\n\n/**\n * Options for configuring an async debounced function\n */\nexport interface AsyncDebouncerOptions<TFn extends AnyAsyncFunction> {\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 * 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?: (error: unknown, debouncer: AsyncDebouncer<TFn>) => void\n /**\n * Optional callback to call when the debounced function is executed\n */\n onSettled?: (debouncer: AsyncDebouncer<TFn>) => void\n /**\n * Optional callback to call when the debounced function is executed\n */\n onSuccess?: (result: ReturnType<TFn>, debouncer: AsyncDebouncer<TFn>) => 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\ntype AsyncDebouncerOptionsWithOptionalCallbacks = OptionalKeys<\n AsyncDebouncerOptions<any>,\n 'initialState' | 'onError' | 'onSettled' | 'onSuccess'\n>\n\nconst defaultOptions: AsyncDebouncerOptionsWithOptionalCallbacks = {\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 * 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 * Unlike the non-async Debouncer, this async version supports returning values from the debounced function,\n * making it ideal for API calls and other async operations where you want the result of the `maybeExecute` call\n * instead of setting the result on a state variable from within the debounced function.\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 options: AsyncDebouncerOptions<TFn>\n #abortController: AbortController | null = null\n #timeoutId: NodeJS.Timeout | null = null\n #resolvePreviousPromise:\n | ((value?: ReturnType<TFn> | undefined) => void)\n | null = null\n\n constructor(\n private fn: TFn,\n initialOptions: AsyncDebouncerOptions<TFn>,\n ) {\n this.options = {\n ...defaultOptions,\n ...initialOptions,\n throwOnError: initialOptions.throwOnError ?? !initialOptions.onError,\n }\n this.#setState(this.options.initialState ?? {})\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 }\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({ lastArgs: args })\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) => {\n this.#resolvePreviousPromise = resolve\n this.#timeoutId = setTimeout(async () => {\n // Execute trailing if enabled\n if (this.options.trailing && this.store.state.lastArgs) {\n await this.#execute(...this.store.state.lastArgs)\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 this.#abortController = new AbortController()\n try {\n this.#setState({ isExecuting: true })\n const result = await this.fn(...args) // EXECUTE!\n this.#setState({\n lastResult: result,\n successCount: this.store.state.successCount + 1,\n })\n this.options.onSuccess?.(result, this)\n } catch (error) {\n this.#setState({\n errorCount: this.store.state.errorCount + 1,\n })\n this.options.onError?.(error, this)\n if (this.options.throwOnError) {\n throw error\n }\n } finally {\n this.#setState({\n isExecuting: false,\n isPending: false,\n settleCount: this.store.state.settleCount + 1,\n })\n this.#abortController = null\n this.options.onSettled?.(this)\n }\n return this.store.state.lastResult\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.#abortExecution() // abort any current execution\n this.#clearTimeout() // clear any existing timeout\n this.#execute(...this.store.state.lastArgs)\n }\n }\n\n #clearTimeout = (): void => {\n if (this.#timeoutId) {\n clearTimeout(this.#timeoutId)\n this.#timeoutId = null\n }\n }\n\n #cancelPendingExecution = (): void => {\n this.#clearTimeout()\n if (this.#resolvePreviousPromise) {\n this.#resolvePreviousPromise(this.store.state.lastResult)\n this.#resolvePreviousPromise = null\n }\n this.#setState({\n isPending: false,\n isExecuting: false,\n lastArgs: undefined,\n })\n }\n\n #abortExecution = (): void => {\n if (this.#abortController) {\n this.#abortController.abort()\n this.#abortController = null\n }\n }\n\n /**\n * Cancels any pending execution or aborts any execution in progress\n */\n cancel = (): void => {\n this.#cancelPendingExecution()\n this.#abortExecution()\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 }\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 * Unlike the non-async Debouncer, this async version supports returning values from the debounced function,\n * making it ideal for API calls and other async operations where you want the result of the `maybeExecute` call\n * instead of setting the result on a state variable from within the debounced function.\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"],"names":[],"mappings":"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"} |
+121
-100
@@ -0,2 +1,69 @@ | ||
| import { Store } from '@tanstack/store'; | ||
| import { QueuePosition } from './queuer.js'; | ||
| export interface AsyncQueuerState<TValue> { | ||
| /** | ||
| * Items currently being processed by the queuer | ||
| */ | ||
| activeItems: Array<TValue>; | ||
| /** | ||
| * Number of task executions that have resulted in errors | ||
| */ | ||
| errorCount: number; | ||
| /** | ||
| * Number of items that have been removed from the queue due to expiration | ||
| */ | ||
| expirationCount: number; | ||
| /** | ||
| * Whether the queuer has no items to process (items array is empty) | ||
| */ | ||
| isEmpty: boolean; | ||
| /** | ||
| * Whether the queuer has reached its maximum capacity | ||
| */ | ||
| isFull: boolean; | ||
| /** | ||
| * Whether the queuer is not currently processing any items | ||
| */ | ||
| isIdle: boolean; | ||
| /** | ||
| * Whether the queuer is active and will process items automatically | ||
| */ | ||
| isRunning: boolean; | ||
| /** | ||
| * Timestamps when items were added to the queue for expiration tracking | ||
| */ | ||
| itemTimestamps: Array<number>; | ||
| /** | ||
| * Array of items currently waiting to be processed | ||
| */ | ||
| items: Array<TValue>; | ||
| /** | ||
| * The result from the most recent task execution | ||
| */ | ||
| lastResult: any; | ||
| /** | ||
| * Whether the queuer has a pending timeout for processing the next item | ||
| */ | ||
| pendingTick: boolean; | ||
| /** | ||
| * Number of items that have been rejected from being added to the queue | ||
| */ | ||
| rejectionCount: number; | ||
| /** | ||
| * Number of task executions that have completed (either successfully or with errors) | ||
| */ | ||
| settledCount: number; | ||
| /** | ||
| * Number of items currently in the queue | ||
| */ | ||
| size: number; | ||
| /** | ||
| * Current processing status - 'idle' when not processing, 'running' when active, 'stopped' when paused | ||
| */ | ||
| status: 'idle' | 'running' | 'stopped'; | ||
| /** | ||
| * Number of task executions that have completed successfully | ||
| */ | ||
| successCount: number; | ||
| } | ||
| export interface AsyncQueuerOptions<TValue> { | ||
@@ -40,2 +107,6 @@ /** | ||
| /** | ||
| * Initial state for the async queuer | ||
| */ | ||
| initialState?: Partial<AsyncQueuerState<TValue>>; | ||
| /** | ||
| * Maximum number of items allowed in the queuer | ||
@@ -55,6 +126,2 @@ */ | ||
| /** | ||
| * Callback fired whenever the queuer's running state changes | ||
| */ | ||
| onIsRunningChange?: (queuer: AsyncQueuer<TValue>) => void; | ||
| /** | ||
| * Callback fired whenever an item is added or removed from the queuer | ||
@@ -114,2 +181,15 @@ */ | ||
| * | ||
| * State Management: | ||
| * - Uses TanStack Store for reactive state management | ||
| * - Use `initialState` to provide initial state values when creating the async queuer | ||
| * - Use `onSuccess` callback to react to successful task execution and implement custom logic | ||
| * - Use `onError` callback to react to task execution errors and implement custom error handling | ||
| * - Use `onSettled` callback to react to task execution completion (success or error) and implement custom logic | ||
| * - Use `onItemsChange` callback to react to items being added or removed from the queue | ||
| * - Use `onExpire` callback to react to items expiring and implement custom logic | ||
| * - Use `onReject` callback to react to items being rejected when the queue is full | ||
| * - The state includes error count, expiration count, rejection count, running status, and success/settle counts | ||
| * - State can be accessed via `asyncQueuer.store.state` when using the class directly | ||
| * - When using framework adapters (React/Solid), state is accessed from `asyncQueuer.state` | ||
| * | ||
| * Example usage: | ||
@@ -132,56 +212,12 @@ * ```ts | ||
| export declare class AsyncQueuer<TValue> { | ||
| #private; | ||
| private fn; | ||
| private _options; | ||
| private _activeItems; | ||
| private _successCount; | ||
| private _errorCount; | ||
| private _settledCount; | ||
| private _rejectionCount; | ||
| private _expirationCount; | ||
| private _items; | ||
| private _itemTimestamps; | ||
| private _pendingTick; | ||
| private _running; | ||
| private _lastResult; | ||
| constructor(fn: (value: TValue) => Promise<any>, initialOptions: AsyncQueuerOptions<TValue>); | ||
| readonly store: Store<Readonly<AsyncQueuerState<TValue>>>; | ||
| options: AsyncQueuerOptions<TValue>; | ||
| constructor(fn: (item: TValue) => Promise<any>, initialOptions?: AsyncQueuerOptions<TValue>); | ||
| /** | ||
| * Updates the queuer options. New options are merged with existing options. | ||
| */ | ||
| setOptions(newOptions: Partial<AsyncQueuerOptions<TValue>>): void; | ||
| setOptions: (newOptions: Partial<AsyncQueuerOptions<TValue>>) => void; | ||
| /** | ||
| * Returns the current queuer options, including defaults and any overrides. | ||
| */ | ||
| getOptions(): AsyncQueuerOptions<TValue>; | ||
| /** | ||
| * Returns the current wait time (in milliseconds) between processing items. | ||
| * If a function is provided, it is called with the queuer instance. | ||
| */ | ||
| getWait(): number; | ||
| /** | ||
| * Returns the current concurrency limit for processing items. | ||
| * If a function is provided, it is called with the queuer instance. | ||
| */ | ||
| getConcurrency(): number; | ||
| /** | ||
| * Processes items in the queue up to the concurrency limit. Internal use only. | ||
| */ | ||
| private tick; | ||
| /** | ||
| * Starts processing items in the queue. If already running, does nothing. | ||
| */ | ||
| start(): void; | ||
| /** | ||
| * Stops processing items in the queue. Does not clear the queue. | ||
| */ | ||
| stop(): void; | ||
| /** | ||
| * Removes all pending items from the queue. Does not affect active tasks. | ||
| */ | ||
| clear(): void; | ||
| /** | ||
| * Resets the queuer to its initial state. Optionally repopulates with initial items. | ||
| * Does not affect callbacks or options. | ||
| */ | ||
| reset(withInitialItems?: boolean): void; | ||
| /** | ||
| * Adds an item to the queue. If the queue is full, the item is rejected and onReject is called. | ||
@@ -196,5 +232,3 @@ * Items can be inserted based on priority or at the front/back depending on configuration. | ||
| */ | ||
| addItem(item: TValue & { | ||
| priority?: number; | ||
| }, position?: QueuePosition, runOnItemsChange?: boolean): void; | ||
| addItem: (item: TValue, position?: QueuePosition, runOnItemsChange?: boolean) => boolean; | ||
| /** | ||
@@ -212,3 +246,3 @@ * Removes and returns the next item from the queue without executing the task function. | ||
| */ | ||
| getNextItem(position?: QueuePosition): TValue | undefined; | ||
| getNextItem: (position?: QueuePosition) => TValue | undefined; | ||
| /** | ||
@@ -224,8 +258,8 @@ * Removes and returns the next item from the queue and executes the task function with it. | ||
| */ | ||
| execute(position?: QueuePosition): Promise<any>; | ||
| execute: (position?: QueuePosition) => Promise<any>; | ||
| /** | ||
| * Checks for expired items in the queue and removes them. Calls onExpire for each expired item. | ||
| * Internal use only. | ||
| * Processes a specified number of items to execute immediately with no wait time | ||
| * If no numberOfItems is provided, all items will be processed | ||
| */ | ||
| private checkExpiredItems; | ||
| flush: (numberOfItems?: number, position?: QueuePosition) => void; | ||
| /** | ||
@@ -240,55 +274,31 @@ * Returns the next item in the queue without removing it. | ||
| */ | ||
| peekNextItem(position?: QueuePosition): TValue | undefined; | ||
| peekNextItem: (position?: QueuePosition) => TValue | undefined; | ||
| /** | ||
| * Returns true if the queue is empty (no pending items). | ||
| */ | ||
| getIsEmpty(): boolean; | ||
| /** | ||
| * Returns true if the queue is full (reached maxSize). | ||
| */ | ||
| getIsFull(): boolean; | ||
| /** | ||
| * Returns the number of pending items in the queue. | ||
| */ | ||
| getSize(): number; | ||
| /** | ||
| * Returns a copy of all items in the queue, including active and pending items. | ||
| */ | ||
| peekAllItems(): Array<TValue>; | ||
| peekAllItems: () => Array<TValue>; | ||
| /** | ||
| * Returns the items currently being processed (active tasks). | ||
| */ | ||
| peekActiveItems(): Array<TValue>; | ||
| peekActiveItems: () => Array<TValue>; | ||
| /** | ||
| * Returns the items waiting to be processed (pending tasks). | ||
| */ | ||
| peekPendingItems(): Array<TValue>; | ||
| peekPendingItems: () => Array<TValue>; | ||
| /** | ||
| * Returns the number of items that have been successfully processed. | ||
| * Starts processing items in the queue. If already running, does nothing. | ||
| */ | ||
| getSuccessCount(): number; | ||
| start: () => void; | ||
| /** | ||
| * Returns the number of items that have failed processing. | ||
| * Stops processing items in the queue. Does not clear the queue. | ||
| */ | ||
| getErrorCount(): number; | ||
| stop: () => void; | ||
| /** | ||
| * Returns the number of items that have completed processing (success or error). | ||
| * Removes all pending items from the queue. Does not affect active tasks. | ||
| */ | ||
| getSettledCount(): number; | ||
| clear: () => void; | ||
| /** | ||
| * Returns the number of items that have been rejected from being added to the queue. | ||
| * Resets the queuer state to its default values | ||
| */ | ||
| getRejectionCount(): number; | ||
| /** | ||
| * Returns true if the queuer is currently running (processing items). | ||
| */ | ||
| getIsRunning(): boolean; | ||
| /** | ||
| * Returns true if the queuer is running but has no items to process and no active tasks. | ||
| */ | ||
| getIsIdle(): boolean; | ||
| /** | ||
| * Returns the number of items that have expired and been removed from the queue. | ||
| */ | ||
| getExpirationCount(): number; | ||
| reset: () => void; | ||
| } | ||
@@ -306,2 +316,15 @@ /** | ||
| * | ||
| * State Management: | ||
| * - Uses TanStack Store for reactive state management | ||
| * - Use `initialState` to provide initial state values when creating the async queuer | ||
| * - Use `onSuccess` callback to react to successful task execution and implement custom logic | ||
| * - Use `onError` callback to react to task execution errors and implement custom error handling | ||
| * - Use `onSettled` callback to react to task execution completion (success or error) and implement custom logic | ||
| * - Use `onItemsChange` callback to react to items being added or removed from the queue | ||
| * - Use `onExpire` callback to react to items expiring and implement custom logic | ||
| * - Use `onReject` callback to react to items being rejected when the queue is full | ||
| * - The state includes error count, expiration count, rejection count, running status, and success/settle counts | ||
| * - State can be accessed via the underlying AsyncQueuer instance's `store.state` property | ||
| * - When using framework adapters (React/Solid), state is accessed from the hook's state property | ||
| * | ||
| * Example usage: | ||
@@ -316,4 +339,2 @@ * ```ts | ||
| */ | ||
| export declare function asyncQueue<TValue>(fn: (value: TValue) => Promise<any>, initialOptions: AsyncQueuerOptions<TValue>): (item: TValue & { | ||
| priority?: number; | ||
| }, position?: QueuePosition, runOnItemsChange?: boolean) => void; | ||
| export declare function asyncQueue<TValue>(fn: (value: TValue) => Promise<any>, initialOptions: AsyncQueuerOptions<TValue>): (item: TValue, position?: QueuePosition, runOnItemsChange?: boolean) => boolean; |
+282
-343
@@ -0,2 +1,23 @@ | ||
| import { Store } from "@tanstack/store"; | ||
| import { parseFunctionOrValue } from "./utils.js"; | ||
| function getDefaultAsyncQueuerState() { | ||
| return structuredClone({ | ||
| activeItems: [], | ||
| errorCount: 0, | ||
| expirationCount: 0, | ||
| isEmpty: true, | ||
| isFull: false, | ||
| isIdle: true, | ||
| isRunning: true, | ||
| itemTimestamps: [], | ||
| items: [], | ||
| lastResult: null, | ||
| pendingTick: false, | ||
| rejectionCount: 0, | ||
| settledCount: 0, | ||
| size: 0, | ||
| status: "idle", | ||
| successCount: 0 | ||
| }); | ||
| } | ||
| const defaultOptions = { | ||
@@ -8,3 +29,3 @@ addItemsTo: "back", | ||
| getItemsFrom: "front", | ||
| getPriority: (item) => (item == null ? void 0 : item.priority) ?? 0, | ||
| getPriority: (item) => item?.priority ?? 0, | ||
| initialItems: [], | ||
@@ -16,361 +37,279 @@ maxSize: Infinity, | ||
| class AsyncQueuer { | ||
| constructor(fn, initialOptions) { | ||
| constructor(fn, initialOptions = {}) { | ||
| this.fn = fn; | ||
| this._activeItems = /* @__PURE__ */ new Set(); | ||
| this._successCount = 0; | ||
| this._errorCount = 0; | ||
| this._settledCount = 0; | ||
| this._rejectionCount = 0; | ||
| this._expirationCount = 0; | ||
| this._items = []; | ||
| this._itemTimestamps = []; | ||
| this._pendingTick = false; | ||
| this._options = { | ||
| ...defaultOptions, | ||
| ...initialOptions, | ||
| throwOnError: initialOptions.throwOnError ?? !initialOptions.onError | ||
| this.store = new Store(getDefaultAsyncQueuerState()); | ||
| this.#timeoutIds = /* @__PURE__ */ new Set(); | ||
| this.setOptions = (newOptions) => { | ||
| this.options = { ...this.options, ...newOptions }; | ||
| }; | ||
| this._running = this._options.started; | ||
| for (let i = 0; i < this._options.initialItems.length; i++) { | ||
| const item = this._options.initialItems[i]; | ||
| const isLast = i === this._options.initialItems.length - 1; | ||
| this.addItem(item, this._options.addItemsTo, isLast); | ||
| } | ||
| } | ||
| /** | ||
| * Updates the queuer options. New options are merged with existing options. | ||
| */ | ||
| setOptions(newOptions) { | ||
| this._options = { ...this._options, ...newOptions }; | ||
| } | ||
| /** | ||
| * Returns the current queuer options, including defaults and any overrides. | ||
| */ | ||
| getOptions() { | ||
| return this._options; | ||
| } | ||
| /** | ||
| * Returns the current wait time (in milliseconds) between processing items. | ||
| * If a function is provided, it is called with the queuer instance. | ||
| */ | ||
| getWait() { | ||
| return parseFunctionOrValue(this._options.wait, this); | ||
| } | ||
| /** | ||
| * Returns the current concurrency limit for processing items. | ||
| * If a function is provided, it is called with the queuer instance. | ||
| */ | ||
| getConcurrency() { | ||
| return parseFunctionOrValue(this._options.concurrency, this); | ||
| } | ||
| /** | ||
| * Processes items in the queue up to the concurrency limit. Internal use only. | ||
| */ | ||
| tick() { | ||
| var _a, _b; | ||
| if (!this._running) { | ||
| this._pendingTick = false; | ||
| return; | ||
| } | ||
| this.checkExpiredItems(); | ||
| while (this._activeItems.size < this.getConcurrency() && !this.getIsEmpty()) { | ||
| const nextItem = this.peekNextItem(); | ||
| if (!nextItem) { | ||
| break; | ||
| this.#setState = (newState) => { | ||
| this.store.setState((state) => { | ||
| const combinedState = { | ||
| ...state, | ||
| ...newState | ||
| }; | ||
| const { activeItems, items, isRunning } = combinedState; | ||
| const size = items.length; | ||
| const isFull = size >= (this.options.maxSize ?? Infinity); | ||
| const isEmpty = size === 0; | ||
| const isIdle = isRunning && isEmpty && activeItems.length === 0; | ||
| const status = isIdle ? "idle" : isRunning ? "running" : "stopped"; | ||
| return { | ||
| ...combinedState, | ||
| isEmpty, | ||
| isFull, | ||
| isIdle, | ||
| size, | ||
| status | ||
| }; | ||
| }); | ||
| }; | ||
| this.#getWait = () => { | ||
| return parseFunctionOrValue(this.options.wait ?? 0, this); | ||
| }; | ||
| this.#getConcurrency = () => { | ||
| return parseFunctionOrValue(this.options.concurrency ?? 1, this); | ||
| }; | ||
| this.#tick = () => { | ||
| if (!this.store.state.isRunning) { | ||
| this.#setState({ pendingTick: false }); | ||
| return; | ||
| } | ||
| this._activeItems.add(nextItem); | ||
| (_b = (_a = this._options).onItemsChange) == null ? void 0 : _b.call(_a, this); | ||
| (async () => { | ||
| this._lastResult = await this.execute(); | ||
| const wait = this.getWait(); | ||
| if (wait > 0) { | ||
| setTimeout(() => this.tick(), wait); | ||
| return; | ||
| this.#setState({ pendingTick: true }); | ||
| this.#checkExpiredItems(); | ||
| const activeItems = this.store.state.activeItems; | ||
| while (activeItems.length < this.#getConcurrency() && !this.store.state.isEmpty) { | ||
| const nextItem = this.peekNextItem(); | ||
| if (!nextItem) { | ||
| break; | ||
| } | ||
| this.tick(); | ||
| })(); | ||
| } | ||
| this._pendingTick = false; | ||
| } | ||
| /** | ||
| * Starts processing items in the queue. If already running, does nothing. | ||
| */ | ||
| start() { | ||
| var _a, _b; | ||
| this._running = true; | ||
| if (!this._pendingTick && !this.getIsEmpty()) { | ||
| this._pendingTick = true; | ||
| this.tick(); | ||
| } | ||
| (_b = (_a = this._options).onIsRunningChange) == null ? void 0 : _b.call(_a, this); | ||
| } | ||
| /** | ||
| * Stops processing items in the queue. Does not clear the queue. | ||
| */ | ||
| stop() { | ||
| var _a, _b; | ||
| this._running = false; | ||
| this._pendingTick = false; | ||
| (_b = (_a = this._options).onIsRunningChange) == null ? void 0 : _b.call(_a, this); | ||
| } | ||
| /** | ||
| * Removes all pending items from the queue. Does not affect active tasks. | ||
| */ | ||
| clear() { | ||
| var _a, _b; | ||
| this._items = []; | ||
| (_b = (_a = this._options).onItemsChange) == null ? void 0 : _b.call(_a, this); | ||
| } | ||
| /** | ||
| * Resets the queuer to its initial state. Optionally repopulates with initial items. | ||
| * Does not affect callbacks or options. | ||
| */ | ||
| reset(withInitialItems) { | ||
| this.clear(); | ||
| this._successCount = 0; | ||
| this._errorCount = 0; | ||
| this._settledCount = 0; | ||
| if (withInitialItems) { | ||
| this._items = [...this._options.initialItems]; | ||
| } | ||
| this._running = this._options.started; | ||
| } | ||
| /** | ||
| * Adds an item to the queue. If the queue is full, the item is rejected and onReject is called. | ||
| * Items can be inserted based on priority or at the front/back depending on configuration. | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * queuer.addItem({ value: 'task', priority: 10 }); | ||
| * queuer.addItem('task2', 'front'); | ||
| * ``` | ||
| */ | ||
| addItem(item, position = this._options.addItemsTo, runOnItemsChange = true) { | ||
| var _a, _b, _c, _d; | ||
| if (this.getIsFull()) { | ||
| this._rejectionCount++; | ||
| (_b = (_a = this._options).onReject) == null ? void 0 : _b.call(_a, item, this); | ||
| return; | ||
| } | ||
| const priority = this._options.getPriority !== defaultOptions.getPriority ? this._options.getPriority(item) : item.priority; | ||
| if (priority !== void 0) { | ||
| const insertIndex = this._items.findIndex((existing) => { | ||
| const existingPriority = this._options.getPriority !== defaultOptions.getPriority ? this._options.getPriority(existing) : existing.priority; | ||
| return existingPriority < priority; | ||
| }); | ||
| if (insertIndex === -1) { | ||
| this._items.push(item); | ||
| this._itemTimestamps.push(Date.now()); | ||
| activeItems.push(nextItem); | ||
| this.#setState({ | ||
| activeItems | ||
| }); | ||
| (async () => { | ||
| const result = await this.execute(); | ||
| this.#setState({ lastResult: result }); | ||
| const wait = this.#getWait(); | ||
| if (wait > 0) { | ||
| const timeoutId = setTimeout(() => this.#tick(), wait); | ||
| this.#timeoutIds.add(timeoutId); | ||
| return; | ||
| } | ||
| this.#tick(); | ||
| })(); | ||
| } | ||
| this.#setState({ pendingTick: false }); | ||
| }; | ||
| this.addItem = (item, position = this.options.addItemsTo ?? "back", runOnItemsChange = true) => { | ||
| if (this.store.state.isFull) { | ||
| this.#setState({ | ||
| rejectionCount: this.store.state.rejectionCount + 1 | ||
| }); | ||
| this.options.onReject?.(item, this); | ||
| return false; | ||
| } | ||
| const priority = this.options.getPriority !== defaultOptions.getPriority ? this.options.getPriority(item) : item.priority; | ||
| const items = this.store.state.items; | ||
| const itemTimestamps = this.store.state.itemTimestamps; | ||
| if (priority !== void 0) { | ||
| const insertIndex = items.findIndex((existing) => { | ||
| const existingPriority = this.options.getPriority !== defaultOptions.getPriority ? this.options.getPriority(existing) : existing.priority; | ||
| return existingPriority < priority; | ||
| }); | ||
| if (insertIndex === -1) { | ||
| items.push(item); | ||
| itemTimestamps.push(Date.now()); | ||
| } else { | ||
| items.splice(insertIndex, 0, item); | ||
| itemTimestamps.splice(insertIndex, 0, Date.now()); | ||
| } | ||
| } else { | ||
| this._items.splice(insertIndex, 0, item); | ||
| this._itemTimestamps.splice(insertIndex, 0, Date.now()); | ||
| if (position === "front") { | ||
| items.unshift(item); | ||
| itemTimestamps.unshift(Date.now()); | ||
| } else { | ||
| items.push(item); | ||
| itemTimestamps.push(Date.now()); | ||
| } | ||
| } | ||
| } else { | ||
| this.#setState({ | ||
| items, | ||
| itemTimestamps | ||
| }); | ||
| if (runOnItemsChange) { | ||
| this.options.onItemsChange?.(this); | ||
| } | ||
| if (this.store.state.isRunning && !this.store.state.pendingTick) { | ||
| this.#tick(); | ||
| } | ||
| return true; | ||
| }; | ||
| this.getNextItem = (position = this.options.getItemsFrom ?? "front") => { | ||
| const { items, itemTimestamps } = this.store.state; | ||
| let item; | ||
| if (position === "front") { | ||
| this._items.unshift(item); | ||
| this._itemTimestamps.unshift(Date.now()); | ||
| item = items[0]; | ||
| if (item !== void 0) { | ||
| this.#setState({ | ||
| items: items.slice(1), | ||
| itemTimestamps: itemTimestamps.slice(1) | ||
| }); | ||
| } | ||
| } else { | ||
| this._items.push(item); | ||
| this._itemTimestamps.push(Date.now()); | ||
| item = items[items.length - 1]; | ||
| if (item !== void 0) { | ||
| this.#setState({ | ||
| items: items.slice(0, -1), | ||
| itemTimestamps: itemTimestamps.slice(0, -1) | ||
| }); | ||
| } | ||
| } | ||
| } | ||
| if (runOnItemsChange) { | ||
| (_d = (_c = this._options).onItemsChange) == null ? void 0 : _d.call(_c, this); | ||
| } | ||
| if (this._running && !this._pendingTick) { | ||
| this._pendingTick = true; | ||
| this.tick(); | ||
| } | ||
| } | ||
| /** | ||
| * Removes and returns the next item from the queue without executing the task function. | ||
| * Use for manual queue management. Normally, use execute() to process items. | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * // FIFO | ||
| * queuer.getNextItem(); | ||
| * // LIFO | ||
| * queuer.getNextItem('back'); | ||
| * ``` | ||
| */ | ||
| getNextItem(position = this._options.getItemsFrom) { | ||
| var _a, _b; | ||
| let item; | ||
| if (position === "front") { | ||
| item = this._items.shift(); | ||
| this._itemTimestamps.shift(); | ||
| } else { | ||
| item = this._items.pop(); | ||
| this._itemTimestamps.pop(); | ||
| } | ||
| if (item !== void 0) { | ||
| (_b = (_a = this._options).onItemsChange) == null ? void 0 : _b.call(_a, this); | ||
| } | ||
| return item; | ||
| } | ||
| /** | ||
| * Removes and returns the next item from the queue and executes the task function with it. | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * queuer.execute(); | ||
| * // LIFO | ||
| * queuer.execute('back'); | ||
| * ``` | ||
| */ | ||
| async execute(position) { | ||
| var _a, _b, _c, _d, _e, _f, _g, _h; | ||
| const item = this.getNextItem(position); | ||
| if (item !== void 0) { | ||
| try { | ||
| this._lastResult = await this.fn(item); | ||
| this._successCount++; | ||
| (_b = (_a = this._options).onSuccess) == null ? void 0 : _b.call(_a, this._lastResult, this); | ||
| } catch (error) { | ||
| this._errorCount++; | ||
| (_d = (_c = this._options).onError) == null ? void 0 : _d.call(_c, error, this); | ||
| if (this._options.throwOnError) { | ||
| throw error; | ||
| if (item !== void 0) { | ||
| this.options.onItemsChange?.(this); | ||
| } | ||
| return item; | ||
| }; | ||
| this.execute = async (position) => { | ||
| const item = this.getNextItem(position); | ||
| if (item !== void 0) { | ||
| try { | ||
| const lastResult = await this.fn(item); | ||
| this.#setState({ | ||
| successCount: this.store.state.successCount + 1, | ||
| lastResult | ||
| }); | ||
| this.options.onSuccess?.(lastResult, this); | ||
| } catch (error) { | ||
| this.#setState({ | ||
| errorCount: this.store.state.errorCount + 1 | ||
| }); | ||
| this.options.onError?.(error, this); | ||
| if (this.options.throwOnError) { | ||
| throw error; | ||
| } | ||
| } finally { | ||
| this.#setState({ | ||
| activeItems: this.store.state.activeItems.filter( | ||
| (activeItem) => activeItem !== item | ||
| ), | ||
| settledCount: this.store.state.settledCount + 1 | ||
| }); | ||
| this.options.onSettled?.(this); | ||
| } | ||
| } finally { | ||
| this._settledCount++; | ||
| this._activeItems.delete(item); | ||
| (_f = (_e = this._options).onItemsChange) == null ? void 0 : _f.call(_e, this); | ||
| (_h = (_g = this._options).onSettled) == null ? void 0 : _h.call(_g, this); | ||
| } | ||
| } | ||
| return item; | ||
| } | ||
| /** | ||
| * Checks for expired items in the queue and removes them. Calls onExpire for each expired item. | ||
| * Internal use only. | ||
| */ | ||
| checkExpiredItems() { | ||
| var _a, _b, _c, _d; | ||
| if (this._options.expirationDuration === Infinity && this._options.getIsExpired === defaultOptions.getIsExpired) | ||
| return; | ||
| const now = Date.now(); | ||
| const expiredIndices = []; | ||
| for (let i = 0; i < this._items.length; i++) { | ||
| const timestamp = this._itemTimestamps[i]; | ||
| if (timestamp === void 0) continue; | ||
| const item = this._items[i]; | ||
| if (item === void 0) continue; | ||
| const isExpired = this._options.getIsExpired !== defaultOptions.getIsExpired ? this._options.getIsExpired(item, timestamp) : now - timestamp > this._options.expirationDuration; | ||
| if (isExpired) { | ||
| expiredIndices.push(i); | ||
| return item; | ||
| }; | ||
| this.flush = (numberOfItems = this.store.state.items.length, position) => { | ||
| this.#clearTimeouts(); | ||
| for (let i = 0; i < numberOfItems; i++) { | ||
| this.execute(position); | ||
| } | ||
| }; | ||
| this.#checkExpiredItems = () => { | ||
| if ((this.options.expirationDuration ?? Infinity) === Infinity && this.options.getIsExpired === defaultOptions.getIsExpired) { | ||
| return; | ||
| } | ||
| const now = Date.now(); | ||
| const expiredIndices = []; | ||
| for (let i = 0; i < this.store.state.size; i++) { | ||
| const timestamp = this.store.state.itemTimestamps[i]; | ||
| if (timestamp === void 0) continue; | ||
| const item = this.store.state.items[i]; | ||
| if (item === void 0) continue; | ||
| const isExpired = this.options.getIsExpired !== defaultOptions.getIsExpired ? this.options.getIsExpired(item, timestamp) : now - timestamp > (this.options.expirationDuration ?? Infinity); | ||
| if (isExpired) { | ||
| expiredIndices.push(i); | ||
| } | ||
| } | ||
| for (let i = expiredIndices.length - 1; i >= 0; i--) { | ||
| const index = expiredIndices[i]; | ||
| if (index === void 0) continue; | ||
| const expiredItem = this.store.state.items[index]; | ||
| if (expiredItem === void 0) continue; | ||
| const newItems = [...this.store.state.items]; | ||
| const newTimestamps = [...this.store.state.itemTimestamps]; | ||
| newItems.splice(index, 1); | ||
| newTimestamps.splice(index, 1); | ||
| this.#setState({ | ||
| items: newItems, | ||
| itemTimestamps: newTimestamps, | ||
| expirationCount: this.store.state.expirationCount + 1 | ||
| }); | ||
| this.options.onExpire?.(expiredItem, this); | ||
| } | ||
| if (expiredIndices.length > 0) { | ||
| this.options.onItemsChange?.(this); | ||
| } | ||
| }; | ||
| this.peekNextItem = (position = "front") => { | ||
| if (position === "front") { | ||
| return this.store.state.items[0]; | ||
| } | ||
| return this.store.state.items[this.store.state.size - 1]; | ||
| }; | ||
| this.peekAllItems = () => { | ||
| return [...this.peekActiveItems(), ...this.peekPendingItems()]; | ||
| }; | ||
| this.peekActiveItems = () => { | ||
| return [...this.store.state.activeItems]; | ||
| }; | ||
| this.peekPendingItems = () => { | ||
| return [...this.store.state.items]; | ||
| }; | ||
| this.start = () => { | ||
| this.#setState({ isRunning: true }); | ||
| if (!this.store.state.pendingTick && !this.store.state.isEmpty) { | ||
| this.#tick(); | ||
| } | ||
| }; | ||
| this.stop = () => { | ||
| this.#clearTimeouts(); | ||
| this.#setState({ isRunning: false, pendingTick: false }); | ||
| }; | ||
| this.#clearTimeouts = () => { | ||
| this.#timeoutIds.forEach((timeoutId) => clearTimeout(timeoutId)); | ||
| this.#timeoutIds.clear(); | ||
| }; | ||
| this.clear = () => { | ||
| this.#setState({ items: [], itemTimestamps: [] }); | ||
| this.options.onItemsChange?.(this); | ||
| }; | ||
| this.reset = () => { | ||
| this.#setState(getDefaultAsyncQueuerState()); | ||
| this.options.onItemsChange?.(this); | ||
| }; | ||
| this.options = { | ||
| ...defaultOptions, | ||
| ...initialOptions, | ||
| throwOnError: initialOptions.throwOnError ?? !initialOptions.onError | ||
| }; | ||
| const isInitiallyRunning = this.options.initialState?.isRunning ?? this.options.started ?? true; | ||
| this.#setState({ | ||
| ...this.options.initialState, | ||
| isRunning: isInitiallyRunning | ||
| }); | ||
| if (this.options.initialState?.items) { | ||
| if (this.store.state.isRunning) { | ||
| this.#tick(); | ||
| } | ||
| } else { | ||
| for (let i = 0; i < (this.options.initialItems?.length ?? 0); i++) { | ||
| const item = this.options.initialItems[i]; | ||
| const isLast = i === (this.options.initialItems?.length ?? 0) - 1; | ||
| this.addItem(item, this.options.addItemsTo ?? "back", isLast); | ||
| } | ||
| } | ||
| for (let i = expiredIndices.length - 1; i >= 0; i--) { | ||
| const index = expiredIndices[i]; | ||
| if (index === void 0) continue; | ||
| const expiredItem = this._items[index]; | ||
| if (expiredItem === void 0) continue; | ||
| this._items.splice(index, 1); | ||
| this._itemTimestamps.splice(index, 1); | ||
| this._expirationCount++; | ||
| (_b = (_a = this._options).onExpire) == null ? void 0 : _b.call(_a, expiredItem, this); | ||
| } | ||
| if (expiredIndices.length > 0) { | ||
| (_d = (_c = this._options).onItemsChange) == null ? void 0 : _d.call(_c, this); | ||
| } | ||
| } | ||
| /** | ||
| * Returns the next item in the queue without removing it. | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * queuer.peekNextItem(); // front | ||
| * queuer.peekNextItem('back'); // back | ||
| * ``` | ||
| */ | ||
| peekNextItem(position = "front") { | ||
| if (position === "front") { | ||
| return this._items[0]; | ||
| } | ||
| return this._items[this._items.length - 1]; | ||
| } | ||
| /** | ||
| * Returns true if the queue is empty (no pending items). | ||
| */ | ||
| getIsEmpty() { | ||
| return this._items.length === 0; | ||
| } | ||
| /** | ||
| * Returns true if the queue is full (reached maxSize). | ||
| */ | ||
| getIsFull() { | ||
| return this._items.length >= this._options.maxSize; | ||
| } | ||
| /** | ||
| * Returns the number of pending items in the queue. | ||
| */ | ||
| getSize() { | ||
| return this._items.length; | ||
| } | ||
| /** | ||
| * Returns a copy of all items in the queue, including active and pending items. | ||
| */ | ||
| peekAllItems() { | ||
| return [...this.peekActiveItems(), ...this.peekPendingItems()]; | ||
| } | ||
| /** | ||
| * Returns the items currently being processed (active tasks). | ||
| */ | ||
| peekActiveItems() { | ||
| return Array.from(this._activeItems); | ||
| } | ||
| /** | ||
| * Returns the items waiting to be processed (pending tasks). | ||
| */ | ||
| peekPendingItems() { | ||
| return [...this._items]; | ||
| } | ||
| /** | ||
| * Returns the number of items that have been successfully processed. | ||
| */ | ||
| getSuccessCount() { | ||
| return this._successCount; | ||
| } | ||
| /** | ||
| * Returns the number of items that have failed processing. | ||
| */ | ||
| getErrorCount() { | ||
| return this._errorCount; | ||
| } | ||
| /** | ||
| * Returns the number of items that have completed processing (success or error). | ||
| */ | ||
| getSettledCount() { | ||
| return this._settledCount; | ||
| } | ||
| /** | ||
| * Returns the number of items that have been rejected from being added to the queue. | ||
| */ | ||
| getRejectionCount() { | ||
| return this._rejectionCount; | ||
| } | ||
| /** | ||
| * Returns true if the queuer is currently running (processing items). | ||
| */ | ||
| getIsRunning() { | ||
| return this._running; | ||
| } | ||
| /** | ||
| * Returns true if the queuer is running but has no items to process and no active tasks. | ||
| */ | ||
| getIsIdle() { | ||
| return this._running && this.getIsEmpty() && this._activeItems.size === 0; | ||
| } | ||
| /** | ||
| * Returns the number of items that have expired and been removed from the queue. | ||
| */ | ||
| getExpirationCount() { | ||
| return this._expirationCount; | ||
| } | ||
| #timeoutIds; | ||
| #setState; | ||
| #getWait; | ||
| #getConcurrency; | ||
| #tick; | ||
| #checkExpiredItems; | ||
| #clearTimeouts; | ||
| } | ||
| function asyncQueue(fn, initialOptions) { | ||
| const asyncQueuer = new AsyncQueuer(fn, initialOptions); | ||
| return asyncQueuer.addItem.bind(asyncQueuer); | ||
| return asyncQueuer.addItem; | ||
| } | ||
@@ -377,0 +316,0 @@ export { |
@@ -1,1 +0,1 @@ | ||
| {"version":3,"file":"async-queuer.js","sources":["../../src/async-queuer.ts"],"sourcesContent":["import { parseFunctionOrValue } from './utils'\nimport type { OptionalKeys } from './types'\nimport type { QueuePosition } from './queuer'\n\nexport interface AsyncQueuerOptions<TValue> {\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 * 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: unknown, 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 the queuer's running state changes\n */\n onIsRunningChange?: (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?: (queuer: AsyncQueuer<TValue>) => void\n /**\n * Optional callback to call when a task succeeds\n */\n onSuccess?: (result: 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\ntype AsyncQueuerOptionsWithOptionalCallbacks = OptionalKeys<\n Required<AsyncQueuerOptions<any>>,\n | 'throwOnError'\n | 'onSuccess'\n | 'onSettled'\n | 'onReject'\n | 'onItemsChange'\n | 'onIsRunningChange'\n | 'onExpire'\n | 'onError'\n>\n\nconst defaultOptions: AsyncQueuerOptionsWithOptionalCallbacks = {\n addItemsTo: 'back',\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 * 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 * - Task cancellation\n * - Item expiration to remove stale items from the queue\n *\n * Tasks are processed concurrently up to the configured concurrency limit. When a task completes,\n * the next pending task is processed if the concurrency limit allows.\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 * 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 private _options: AsyncQueuerOptionsWithOptionalCallbacks\n private _activeItems: Set<TValue> = new Set()\n private _successCount = 0\n private _errorCount = 0\n private _settledCount = 0\n private _rejectionCount = 0\n private _expirationCount = 0\n private _items: Array<TValue> = []\n private _itemTimestamps: Array<number> = []\n private _pendingTick = false\n private _running: boolean\n private _lastResult: any\n\n constructor(\n private fn: (value: TValue) => Promise<any>,\n initialOptions: AsyncQueuerOptions<TValue>,\n ) {\n this._options = {\n ...defaultOptions,\n ...initialOptions,\n throwOnError: initialOptions.throwOnError ?? !initialOptions.onError,\n }\n this._running = this._options.started\n\n for (let i = 0; i < this._options.initialItems.length; i++) {\n const item = this._options.initialItems[i]!\n const isLast = i === this._options.initialItems.length - 1\n this.addItem(item, this._options.addItemsTo, isLast)\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 /**\n * Returns the current queuer options, including defaults and any overrides.\n */\n getOptions(): AsyncQueuerOptions<TValue> {\n return this._options\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, 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, this)\n }\n\n /**\n * Processes items in the queue up to the concurrency limit. Internal use only.\n */\n private tick() {\n if (!this._running) {\n this._pendingTick = false\n return\n }\n\n // Check for expired items\n this.checkExpiredItems()\n\n // Process items concurrently up to the concurrency limit\n while (\n this._activeItems.size < this.getConcurrency() &&\n !this.getIsEmpty()\n ) {\n const nextItem = this.peekNextItem()\n if (!nextItem) {\n break\n }\n this._activeItems.add(nextItem)\n this._options.onItemsChange?.(this)\n ;(async () => {\n this._lastResult = await this.execute()\n\n const wait = this.getWait()\n if (wait > 0) {\n setTimeout(() => this.tick(), wait)\n return\n }\n\n this.tick()\n })()\n }\n\n this._pendingTick = false\n }\n\n /**\n * Starts processing items in the queue. If already running, does nothing.\n */\n start(): void {\n this._running = true\n if (!this._pendingTick && !this.getIsEmpty()) {\n this._pendingTick = true\n this.tick()\n }\n this._options.onIsRunningChange?.(this)\n }\n\n /**\n * Stops processing items in the queue. Does not clear the queue.\n */\n stop(): void {\n this._running = false\n this._pendingTick = false\n this._options.onIsRunningChange?.(this)\n }\n\n /**\n * Removes all pending items from the queue. Does not affect active tasks.\n */\n clear(): void {\n this._items = []\n this._options.onItemsChange?.(this)\n }\n\n /**\n * Resets the queuer to its initial state. Optionally repopulates with initial items.\n * Does not affect callbacks or options.\n */\n reset(withInitialItems?: boolean): void {\n this.clear()\n this._successCount = 0\n this._errorCount = 0\n this._settledCount = 0\n if (withInitialItems) {\n this._items = [...this._options.initialItems]\n }\n this._running = this._options.started\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 & { priority?: number },\n position: QueuePosition = this._options.addItemsTo,\n runOnItemsChange: boolean = true,\n ): void {\n if (this.getIsFull()) {\n this._rejectionCount++\n this._options.onReject?.(item, this)\n return\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.priority\n\n if (priority !== undefined) {\n // Insert based on priority - higher priority items go to front\n const insertIndex = this._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 this._items.push(item)\n this._itemTimestamps.push(Date.now())\n } else {\n this._items.splice(insertIndex, 0, item)\n this._itemTimestamps.splice(insertIndex, 0, Date.now())\n }\n } else {\n if (position === 'front') {\n // Default FIFO/LIFO behavior\n this._items.unshift(item)\n this._itemTimestamps.unshift(Date.now())\n } else {\n // LIFO\n this._items.push(item)\n this._itemTimestamps.push(Date.now())\n }\n }\n\n if (runOnItemsChange) {\n this._options.onItemsChange?.(this)\n }\n\n if (this._running && !this._pendingTick) {\n this._pendingTick = true\n this.tick()\n }\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,\n ): TValue | undefined {\n let item: TValue | undefined\n\n if (position === 'front') {\n item = this._items.shift()\n this._itemTimestamps.shift()\n } else {\n item = this._items.pop()\n this._itemTimestamps.pop()\n }\n\n if (item !== undefined) {\n this._options.onItemsChange?.(this)\n }\n\n return item\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 async execute(position?: QueuePosition): Promise<any> {\n const item = this.getNextItem(position)\n if (item !== undefined) {\n try {\n this._lastResult = await this.fn(item)\n this._successCount++\n this._options.onSuccess?.(this._lastResult, this)\n } catch (error) {\n this._errorCount++\n this._options.onError?.(error, this)\n if (this._options.throwOnError) {\n throw error\n }\n } finally {\n this._settledCount++\n this._activeItems.delete(item)\n this._options.onItemsChange?.(this)\n this._options.onSettled?.(this)\n }\n }\n return item\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 private checkExpiredItems(): void {\n if (\n this._options.expirationDuration === Infinity &&\n this._options.getIsExpired === defaultOptions.getIsExpired\n )\n return\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._items.length; i++) {\n const timestamp = this._itemTimestamps[i]\n if (timestamp === undefined) continue\n\n const item = this._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\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._items[index]\n if (expiredItem === undefined) continue\n\n this._items.splice(index, 1)\n this._itemTimestamps.splice(index, 1)\n this._expirationCount++\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._items[0]\n }\n return this._items[this._items.length - 1]\n }\n\n /**\n * Returns true if the queue is empty (no pending items).\n */\n getIsEmpty(): boolean {\n return this._items.length === 0\n }\n\n /**\n * Returns true if the queue is full (reached maxSize).\n */\n getIsFull(): boolean {\n return this._items.length >= this._options.maxSize\n }\n\n /**\n * Returns the number of pending items in the queue.\n */\n getSize(): number {\n return this._items.length\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 Array.from(this._activeItems)\n }\n\n /**\n * Returns the items waiting to be processed (pending tasks).\n */\n peekPendingItems(): Array<TValue> {\n return [...this._items]\n }\n\n /**\n * Returns the number of items that have been successfully processed.\n */\n getSuccessCount(): number {\n return this._successCount\n }\n\n /**\n * Returns the number of items that have failed processing.\n */\n getErrorCount(): number {\n return this._errorCount\n }\n\n /**\n * Returns the number of items that have completed processing (success or error).\n */\n getSettledCount(): number {\n return this._settledCount\n }\n\n /**\n * Returns the number of items that have been rejected from being added to the queue.\n */\n getRejectionCount(): number {\n return this._rejectionCount\n }\n\n /**\n * Returns true if the queuer is currently running (processing items).\n */\n getIsRunning(): boolean {\n return this._running\n }\n\n /**\n * Returns true if the queuer is running but has no items to process and no active tasks.\n */\n getIsIdle(): boolean {\n return this._running && this.getIsEmpty() && this._activeItems.size === 0\n }\n\n /**\n * Returns the number of items that have expired and been removed from the queue.\n */\n getExpirationCount(): number {\n return this._expirationCount\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 * 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 * Example usage:\n * ```ts\n * const enqueue = asyncQueue<string>(async (item) => {\n * return item.toUpperCase();\n * }, {...options});\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.bind(asyncQueuer)\n}\n"],"names":[],"mappings":"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"} | ||
| {"version":3,"file":"async-queuer.js","sources":["../../src/async-queuer.ts"],"sourcesContent":["import { Store } from '@tanstack/store'\nimport { parseFunctionOrValue } from './utils'\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 task executions that have resulted in errors\n */\n errorCount: 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 * Timestamps when items were added to the queue for expiration tracking\n */\n itemTimestamps: Array<number>\n /**\n * Array of items currently waiting to be processed\n */\n items: Array<TValue>\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 structuredClone({\n activeItems: [],\n errorCount: 0,\n expirationCount: 0,\n isEmpty: true,\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 * 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 * 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: unknown, 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?: (queuer: AsyncQueuer<TValue>) => void\n /**\n * Optional callback to call when a task succeeds\n */\n onSuccess?: (result: 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\ntype AsyncQueuerOptionsWithOptionalCallbacks = OptionalKeys<\n Required<AsyncQueuerOptions<any>>,\n | 'initialState'\n | 'throwOnError'\n | 'onSuccess'\n | 'onSettled'\n | 'onReject'\n | 'onItemsChange'\n | 'onExpire'\n | 'onError'\n>\n\nconst defaultOptions: AsyncQueuerOptionsWithOptionalCallbacks = {\n addItemsTo: 'back',\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 * 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 * - Task cancellation\n * - Item expiration to remove stale items from the queue\n *\n * Tasks are processed concurrently up to the configured concurrency limit. When a task completes,\n * the next pending task is processed if the concurrency limit allows.\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 options: AsyncQueuerOptions<TValue>\n #timeoutIds: Set<NodeJS.Timeout> = new Set()\n\n constructor(\n private fn: (item: TValue) => Promise<any>,\n initialOptions: AsyncQueuerOptions<TValue> = {},\n ) {\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\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 }\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.isEmpty\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 const result = await this.execute()\n this.#setState({ lastResult: result })\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 if (this.store.state.isFull) {\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 if (position === 'front') {\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 /**\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 if (item !== undefined) {\n try {\n const lastResult = await this.fn(item)\n this.#setState({\n successCount: this.store.state.successCount + 1,\n lastResult,\n })\n this.options.onSuccess?.(lastResult, this)\n } catch (error) {\n this.#setState({\n errorCount: this.store.state.errorCount + 1,\n })\n this.options.onError?.(error, this)\n if (this.options.throwOnError) {\n throw error\n }\n } finally {\n this.#setState({\n activeItems: this.store.state.activeItems.filter(\n (activeItem) => activeItem !== item,\n ),\n settledCount: this.store.state.settledCount + 1,\n })\n this.options.onSettled?.(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 = (\n numberOfItems: number = this.store.state.items.length,\n position?: QueuePosition,\n ): void => {\n this.#clearTimeouts() // clear any pending timeouts\n for (let i = 0; i < numberOfItems; i++) {\n this.execute(position)\n }\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.size; 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.size - 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.isEmpty) {\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. Does not affect active tasks.\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(getDefaultAsyncQueuerState<TValue>())\n this.options.onItemsChange?.(this)\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 * 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 usage:\n * ```ts\n * const enqueue = asyncQueue<string>(async (item) => {\n * return item.toUpperCase();\n * }, {...options});\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"],"names":[],"mappings":"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"} |
@@ -0,2 +1,33 @@ | ||
| import { Store } from '@tanstack/store'; | ||
| import { AnyAsyncFunction } from './types.js'; | ||
| export interface AsyncRateLimiterState<TFn extends AnyAsyncFunction> { | ||
| /** | ||
| * Number of function executions that have resulted in errors | ||
| */ | ||
| errorCount: number; | ||
| /** | ||
| * Array of timestamps when executions occurred for rate limiting calculations | ||
| */ | ||
| executionTimes: Array<number>; | ||
| /** | ||
| * Whether the rate-limited function is currently executing asynchronously | ||
| */ | ||
| isExecuting: boolean; | ||
| /** | ||
| * The result from the most recent successful function execution | ||
| */ | ||
| lastResult: ReturnType<TFn> | undefined; | ||
| /** | ||
| * Number of function executions that have been rejected due to rate limiting | ||
| */ | ||
| rejectionCount: number; | ||
| /** | ||
| * Number of function executions that have completed (either successfully or with errors) | ||
| */ | ||
| settleCount: number; | ||
| /** | ||
| * Number of function executions that have completed successfully | ||
| */ | ||
| successCount: number; | ||
| } | ||
| /** | ||
@@ -13,2 +44,6 @@ * Options for configuring an async rate-limited function | ||
| /** | ||
| * Initial state for the rate limiter | ||
| */ | ||
| initialState?: Partial<AsyncRateLimiterState<TFn>>; | ||
| /** | ||
| * Maximum number of executions allowed within the time window. | ||
@@ -79,2 +114,14 @@ * Can be a number or a function that returns a number. | ||
| * | ||
| * State Management: | ||
| * - Uses TanStack Store for reactive state management | ||
| * - Use `initialState` to provide initial state values when creating the rate limiter | ||
| * - `initialState` can be a partial state object | ||
| * - Use `onSuccess` callback to react to successful function execution and implement custom logic | ||
| * - Use `onError` callback to react to function execution errors and implement custom error handling | ||
| * - Use `onSettled` callback to react to function execution completion (success or error) and implement custom logic | ||
| * - Use `onReject` callback to react to executions being rejected when rate limit is exceeded | ||
| * - The state includes execution times, success/error counts, and current execution status | ||
| * - State can be accessed via `asyncRateLimiter.store.state` when using the class directly | ||
| * - When using framework adapters (React/Solid), state is accessed from `asyncRateLimiter.state` | ||
| * | ||
| * Error Handling: | ||
@@ -111,36 +158,14 @@ * - If an `onError` handler is provided, it will be called with the error and rate limiter instance | ||
| export declare class AsyncRateLimiter<TFn extends AnyAsyncFunction> { | ||
| #private; | ||
| private fn; | ||
| private _options; | ||
| private _errorCount; | ||
| private _executionTimes; | ||
| private _lastResult; | ||
| private _rejectionCount; | ||
| private _settleCount; | ||
| private _successCount; | ||
| private _isExecuting; | ||
| readonly store: Store<Readonly<AsyncRateLimiterState<TFn>>>; | ||
| options: AsyncRateLimiterOptions<TFn>; | ||
| constructor(fn: TFn, initialOptions: AsyncRateLimiterOptions<TFn>); | ||
| /** | ||
| * Updates the rate limiter options | ||
| * Updates the async rate limiter options | ||
| */ | ||
| setOptions(newOptions: Partial<AsyncRateLimiterOptions<TFn>>): void; | ||
| setOptions: (newOptions: Partial<AsyncRateLimiterOptions<TFn>>) => void; | ||
| /** | ||
| * Returns the current rate limiter options | ||
| */ | ||
| getOptions(): AsyncRateLimiterOptions<TFn>; | ||
| /** | ||
| * Returns the current enabled state of the rate limiter | ||
| */ | ||
| getEnabled(): boolean; | ||
| /** | ||
| * Returns the current limit of executions allowed within the time window | ||
| */ | ||
| getLimit(): number; | ||
| /** | ||
| * Returns the current time window in milliseconds | ||
| */ | ||
| getWindow(): number; | ||
| /** | ||
| * Attempts to execute the rate-limited function if within the configured limits. | ||
| * Will reject execution if the number of calls in the current window exceeds the limit. | ||
| * If execution is allowed, waits for any previous execution to complete before proceeding. | ||
| * | ||
@@ -152,6 +177,3 @@ * Error Handling: | ||
| * and this method will return undefined. | ||
| * - If the rate limit is exceeded, the execution will be rejected and the `onReject` handler | ||
| * will be called if configured. | ||
| * - The error state can be checked using `getErrorCount()` and `getIsExecuting()`. | ||
| * - Rate limit rejections can be tracked using `getRejectionCount()`. | ||
| * | ||
@@ -165,17 +187,14 @@ * @returns A promise that resolves with the function's return value, or undefined if an error occurred and was handled by onError | ||
| * | ||
| * // First 5 calls will execute | ||
| * await rateLimiter.maybeExecute('arg1', 'arg2'); | ||
| * // First 5 calls will return a promise that resolves with the result | ||
| * const result = await rateLimiter.maybeExecute('arg1', 'arg2'); | ||
| * | ||
| * // Additional calls within the window will be rejected | ||
| * await rateLimiter.maybeExecute('arg1', 'arg2'); // Rejected | ||
| * // Additional calls within the window will return undefined | ||
| * const result2 = await rateLimiter.maybeExecute('arg1', 'arg2'); // undefined | ||
| * ``` | ||
| */ | ||
| maybeExecute(...args: Parameters<TFn>): Promise<ReturnType<TFn> | undefined>; | ||
| private execute; | ||
| private rejectFunction; | ||
| private cleanupOldExecutions; | ||
| maybeExecute: (...args: Parameters<TFn>) => Promise<ReturnType<TFn> | undefined>; | ||
| /** | ||
| * Returns the number of remaining executions allowed in the current window | ||
| */ | ||
| getRemainingInWindow(): number; | ||
| getRemainingInWindow: () => number; | ||
| /** | ||
@@ -186,27 +205,7 @@ * Returns the number of milliseconds until the next execution will be possible | ||
| */ | ||
| getMsUntilNextWindow(): number; | ||
| getMsUntilNextWindow: () => number; | ||
| /** | ||
| * Returns the number of times the function has been executed | ||
| */ | ||
| getSuccessCount(): number; | ||
| /** | ||
| * Returns the number of times the function has been settled | ||
| */ | ||
| getSettleCount(): number; | ||
| /** | ||
| * Returns the number of times the function has errored | ||
| */ | ||
| getErrorCount(): number; | ||
| /** | ||
| * Returns the number of times the function has been rejected | ||
| */ | ||
| getRejectionCount(): number; | ||
| /** | ||
| * Returns whether the function is currently executing | ||
| */ | ||
| getIsExecuting(): boolean; | ||
| /** | ||
| * Resets the rate limiter state | ||
| */ | ||
| reset(): void; | ||
| reset: () => void; | ||
| } | ||
@@ -231,2 +230,14 @@ /** | ||
| * | ||
| * State Management: | ||
| * - Uses TanStack Store for reactive state management | ||
| * - Use `initialState` to provide initial state values when creating the rate limiter | ||
| * - `initialState` can be a partial state object | ||
| * - Use `onSuccess` callback to react to successful function execution and implement custom logic | ||
| * - Use `onError` callback to react to function execution errors and implement custom error handling | ||
| * - Use `onSettled` callback to react to function execution completion (success or error) and implement custom logic | ||
| * - Use `onReject` callback to react to executions being rejected when rate limit is exceeded | ||
| * - The state includes execution times, success/error counts, and current execution status | ||
| * - State can be accessed via the underlying AsyncRateLimiter instance's `store.state` property | ||
| * - When using framework adapters (React/Solid), state is accessed from the hook's state property | ||
| * | ||
| * Consider using throttle() or debounce() if you need more intelligent execution control. Use rate limiting when you specifically | ||
@@ -233,0 +244,0 @@ * need to enforce a hard limit on the number of executions within a time period. |
+127
-184
@@ -0,5 +1,20 @@ | ||
| import { Store } from "@tanstack/store"; | ||
| import { parseFunctionOrValue } from "./utils.js"; | ||
| function getDefaultAsyncRateLimiterState() { | ||
| return { | ||
| errorCount: 0, | ||
| executionTimes: [], | ||
| isExecuting: false, | ||
| lastResult: void 0, | ||
| rejectionCount: 0, | ||
| settleCount: 0, | ||
| successCount: 0 | ||
| }; | ||
| } | ||
| const defaultOptions = { | ||
| enabled: true, | ||
| windowType: "fixed" | ||
| limit: 1, | ||
| window: 0, | ||
| windowType: "fixed", | ||
| throwOnError: true | ||
| }; | ||
@@ -9,195 +24,123 @@ class AsyncRateLimiter { | ||
| this.fn = fn; | ||
| this._errorCount = 0; | ||
| this._executionTimes = []; | ||
| this._rejectionCount = 0; | ||
| this._settleCount = 0; | ||
| this._successCount = 0; | ||
| this._isExecuting = false; | ||
| this._options = { | ||
| ...defaultOptions, | ||
| ...initialOptions, | ||
| throwOnError: initialOptions.throwOnError ?? !initialOptions.onError | ||
| this.store = new Store(getDefaultAsyncRateLimiterState()); | ||
| this.setOptions = (newOptions) => { | ||
| this.options = { ...this.options, ...newOptions }; | ||
| }; | ||
| } | ||
| /** | ||
| * Updates the rate limiter options | ||
| */ | ||
| setOptions(newOptions) { | ||
| this._options = { ...this._options, ...newOptions }; | ||
| } | ||
| /** | ||
| * Returns the current rate limiter options | ||
| */ | ||
| getOptions() { | ||
| return this._options; | ||
| } | ||
| /** | ||
| * Returns the current enabled state of the rate limiter | ||
| */ | ||
| getEnabled() { | ||
| return !!parseFunctionOrValue(this._options.enabled, this); | ||
| } | ||
| /** | ||
| * Returns the current limit of executions allowed within the time window | ||
| */ | ||
| getLimit() { | ||
| return parseFunctionOrValue(this._options.limit, this); | ||
| } | ||
| /** | ||
| * Returns the current time window in milliseconds | ||
| */ | ||
| getWindow() { | ||
| return parseFunctionOrValue(this._options.window, this); | ||
| } | ||
| /** | ||
| * Attempts to execute the rate-limited function if within the configured limits. | ||
| * Will reject execution if the number of calls in the current window exceeds the limit. | ||
| * If execution is allowed, waits for any previous execution to complete before proceeding. | ||
| * | ||
| * Error Handling: | ||
| * - If the rate-limited function throws and no `onError` handler is configured, | ||
| * the error will be thrown from this method. | ||
| * - If an `onError` handler is configured, errors will be caught and passed to the handler, | ||
| * and this method will return undefined. | ||
| * - If the rate limit is exceeded, the execution will be rejected and the `onReject` handler | ||
| * will be called if configured. | ||
| * - The error state can be checked using `getErrorCount()` and `getIsExecuting()`. | ||
| * - Rate limit rejections can be tracked using `getRejectionCount()`. | ||
| * | ||
| * @returns A promise that resolves with the function's return value, or undefined if an error occurred and was handled by onError | ||
| * @throws The error from the rate-limited function if no onError handler is configured | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * const rateLimiter = new AsyncRateLimiter(fn, { limit: 5, window: 1000 }); | ||
| * | ||
| * // First 5 calls will execute | ||
| * await rateLimiter.maybeExecute('arg1', 'arg2'); | ||
| * | ||
| * // Additional calls within the window will be rejected | ||
| * await rateLimiter.maybeExecute('arg1', 'arg2'); // Rejected | ||
| * ``` | ||
| */ | ||
| async maybeExecute(...args) { | ||
| this.cleanupOldExecutions(); | ||
| const limit = this.getLimit(); | ||
| const window = this.getWindow(); | ||
| if (this._options.windowType === "sliding") { | ||
| if (this._executionTimes.length < limit) { | ||
| await this.execute(...args); | ||
| return this._lastResult; | ||
| this.#setState = (newState) => { | ||
| this.store.setState((state) => { | ||
| const combinedState = { | ||
| ...state, | ||
| ...newState | ||
| }; | ||
| return combinedState; | ||
| }); | ||
| }; | ||
| this.#getEnabled = () => { | ||
| return !!parseFunctionOrValue(this.options.enabled, this); | ||
| }; | ||
| this.#getLimit = () => { | ||
| return parseFunctionOrValue(this.options.limit, this); | ||
| }; | ||
| this.#getWindow = () => { | ||
| return parseFunctionOrValue(this.options.window, this); | ||
| }; | ||
| this.maybeExecute = async (...args) => { | ||
| this.#cleanupOldExecutions(); | ||
| const relevantExecutionTimes = this.#getRelevantExecutionTimes(); | ||
| if (relevantExecutionTimes.length < this.#getLimit()) { | ||
| await this.#execute(...args); | ||
| return this.store.state.lastResult; | ||
| } | ||
| } else { | ||
| this.#setState({ | ||
| rejectionCount: this.store.state.rejectionCount + 1 | ||
| }); | ||
| this.options.onReject?.(this); | ||
| return void 0; | ||
| }; | ||
| this.#execute = async (...args) => { | ||
| if (!this.#getEnabled()) return; | ||
| const now = Date.now(); | ||
| const oldestExecution = Math.min(...this._executionTimes); | ||
| const isNewWindow = oldestExecution + window <= now; | ||
| if (isNewWindow || this._executionTimes.length < limit) { | ||
| await this.execute(...args); | ||
| return this._lastResult; | ||
| const executionTimes = [...this.store.state.executionTimes, now]; | ||
| this.#setState({ | ||
| isExecuting: true, | ||
| executionTimes | ||
| }); | ||
| try { | ||
| const result = await this.fn(...args); | ||
| this.#setState({ | ||
| successCount: this.store.state.successCount + 1, | ||
| lastResult: result | ||
| }); | ||
| this.options.onSuccess?.(result, this); | ||
| } catch (error) { | ||
| this.#setState({ | ||
| errorCount: this.store.state.errorCount + 1 | ||
| }); | ||
| this.options.onError?.(error, this); | ||
| if (this.options.throwOnError) { | ||
| throw error; | ||
| } | ||
| } finally { | ||
| this.#setState({ | ||
| isExecuting: false, | ||
| settleCount: this.store.state.settleCount + 1 | ||
| }); | ||
| this.options.onSettled?.(this); | ||
| } | ||
| } | ||
| this.rejectFunction(); | ||
| return void 0; | ||
| } | ||
| async execute(...args) { | ||
| var _a, _b, _c, _d, _e, _f; | ||
| if (!this.getEnabled()) return; | ||
| this._isExecuting = true; | ||
| const now = Date.now(); | ||
| this._executionTimes.push(now); | ||
| try { | ||
| this._lastResult = await this.fn(...args); | ||
| this._successCount++; | ||
| (_b = (_a = this._options).onSuccess) == null ? void 0 : _b.call(_a, this._lastResult, this); | ||
| } catch (error) { | ||
| this._errorCount++; | ||
| (_d = (_c = this._options).onError) == null ? void 0 : _d.call(_c, error, this); | ||
| if (this._options.throwOnError) { | ||
| throw error; | ||
| return this.store.state.lastResult; | ||
| }; | ||
| this.#getRelevantExecutionTimes = () => { | ||
| if (this.options.windowType === "sliding") { | ||
| return this.store.state.executionTimes.filter( | ||
| (time) => time > Date.now() - this.#getWindow() | ||
| ); | ||
| } else { | ||
| console.error(error); | ||
| const oldestExecution = Math.min(...this.store.state.executionTimes); | ||
| const windowStart = oldestExecution; | ||
| return this.store.state.executionTimes.filter( | ||
| (time) => time >= windowStart && time <= windowStart + this.#getWindow() | ||
| ); | ||
| } | ||
| } finally { | ||
| this._isExecuting = false; | ||
| this._settleCount++; | ||
| (_f = (_e = this._options).onSettled) == null ? void 0 : _f.call(_e, this); | ||
| } | ||
| return this._lastResult; | ||
| }; | ||
| this.#cleanupOldExecutions = () => { | ||
| const now = Date.now(); | ||
| const windowStart = now - this.#getWindow(); | ||
| this.#setState({ | ||
| executionTimes: this.store.state.executionTimes.filter( | ||
| (time) => time > windowStart | ||
| ) | ||
| }); | ||
| }; | ||
| this.getRemainingInWindow = () => { | ||
| const relevantExecutionTimes = this.#getRelevantExecutionTimes(); | ||
| return Math.max(0, this.#getLimit() - relevantExecutionTimes.length); | ||
| }; | ||
| this.getMsUntilNextWindow = () => { | ||
| if (this.getRemainingInWindow() > 0) { | ||
| return 0; | ||
| } | ||
| const oldestExecution = this.store.state.executionTimes[0] ?? Infinity; | ||
| return oldestExecution + this.#getWindow() - Date.now(); | ||
| }; | ||
| this.reset = () => { | ||
| this.#setState(getDefaultAsyncRateLimiterState()); | ||
| }; | ||
| this.options = { | ||
| ...defaultOptions, | ||
| ...initialOptions, | ||
| throwOnError: initialOptions.throwOnError ?? !initialOptions.onError | ||
| }; | ||
| this.#setState(this.options.initialState ?? {}); | ||
| } | ||
| rejectFunction() { | ||
| this._rejectionCount++; | ||
| if (this._options.onReject) { | ||
| this._options.onReject(this); | ||
| } | ||
| } | ||
| cleanupOldExecutions() { | ||
| const now = Date.now(); | ||
| const windowStart = now - this.getWindow(); | ||
| this._executionTimes = this._executionTimes.filter( | ||
| (time) => time > windowStart | ||
| ); | ||
| } | ||
| /** | ||
| * Returns the number of remaining executions allowed in the current window | ||
| */ | ||
| getRemainingInWindow() { | ||
| this.cleanupOldExecutions(); | ||
| return Math.max(0, this.getLimit() - this._executionTimes.length); | ||
| } | ||
| /** | ||
| * Returns the number of milliseconds until the next execution will be possible | ||
| * For fixed windows, this is the time until the current window resets | ||
| * For sliding windows, this is the time until the oldest execution expires | ||
| */ | ||
| getMsUntilNextWindow() { | ||
| if (this.getRemainingInWindow() > 0) { | ||
| return 0; | ||
| } | ||
| const oldestExecution = Math.min(...this._executionTimes); | ||
| return oldestExecution + this.getWindow() - Date.now(); | ||
| } | ||
| /** | ||
| * Returns the number of times the function has been executed | ||
| */ | ||
| getSuccessCount() { | ||
| return this._successCount; | ||
| } | ||
| /** | ||
| * Returns the number of times the function has been settled | ||
| */ | ||
| getSettleCount() { | ||
| return this._settleCount; | ||
| } | ||
| /** | ||
| * Returns the number of times the function has errored | ||
| */ | ||
| getErrorCount() { | ||
| return this._errorCount; | ||
| } | ||
| /** | ||
| * Returns the number of times the function has been rejected | ||
| */ | ||
| getRejectionCount() { | ||
| return this._rejectionCount; | ||
| } | ||
| /** | ||
| * Returns whether the function is currently executing | ||
| */ | ||
| getIsExecuting() { | ||
| return this._isExecuting; | ||
| } | ||
| /** | ||
| * Resets the rate limiter state | ||
| */ | ||
| reset() { | ||
| this._executionTimes = []; | ||
| this._successCount = 0; | ||
| this._errorCount = 0; | ||
| this._rejectionCount = 0; | ||
| this._settleCount = 0; | ||
| } | ||
| #setState; | ||
| #getEnabled; | ||
| #getLimit; | ||
| #getWindow; | ||
| #execute; | ||
| #getRelevantExecutionTimes; | ||
| #cleanupOldExecutions; | ||
| } | ||
| function asyncRateLimit(fn, initialOptions) { | ||
| const rateLimiter = new AsyncRateLimiter(fn, initialOptions); | ||
| return rateLimiter.maybeExecute.bind(rateLimiter); | ||
| return rateLimiter.maybeExecute; | ||
| } | ||
@@ -204,0 +147,0 @@ export { |
@@ -1,1 +0,1 @@ | ||
| {"version":3,"file":"async-rate-limiter.js","sources":["../../src/async-rate-limiter.ts"],"sourcesContent":["import { parseFunctionOrValue } from './utils'\nimport type { AnyAsyncFunction, OptionalKeys } from './types'\n\n/**\n * Options for configuring an async rate-limited function\n */\nexport interface AsyncRateLimiterOptions<TFn extends AnyAsyncFunction> {\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 * 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?: (error: unknown, rateLimiter: AsyncRateLimiter<TFn>) => void\n /**\n * Optional callback function that is called when an execution is rejected due to rate limiting\n */\n onReject?: (rateLimiter: AsyncRateLimiter<TFn>) => void\n /**\n * Optional function to call when the rate-limited function is executed\n */\n onSettled?: (rateLimiter: AsyncRateLimiter<TFn>) => void\n /**\n * Optional function to call when the rate-limited function is executed\n */\n onSuccess?: (\n result: ReturnType<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\ntype AsyncRateLimiterOptionsWithOptionalCallbacks = OptionalKeys<\n AsyncRateLimiterOptions<any>,\n 'onError' | 'onReject' | 'onSettled' | 'onSuccess'\n>\n\nconst defaultOptions: Omit<\n AsyncRateLimiterOptionsWithOptionalCallbacks,\n 'limit' | 'window'\n> = {\n enabled: true,\n windowType: 'fixed',\n}\n\n/**\n * A class that creates an async 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 *\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 * Unlike the non-async RateLimiter, this async version supports returning values from the rate-limited function,\n * making it ideal for API calls and other async operations where you want the result of the `maybeExecute` call\n * instead of setting the result on a state variable from within the rate-limited function.\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 * 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 private _options: AsyncRateLimiterOptionsWithOptionalCallbacks\n private _errorCount = 0\n private _executionTimes: Array<number> = []\n private _lastResult: ReturnType<TFn> | undefined\n private _rejectionCount = 0\n private _settleCount = 0\n private _successCount = 0\n private _isExecuting = false\n\n constructor(\n private fn: TFn,\n initialOptions: AsyncRateLimiterOptions<TFn>,\n ) {\n this._options = {\n ...defaultOptions,\n ...initialOptions,\n throwOnError: initialOptions.throwOnError ?? !initialOptions.onError,\n }\n }\n\n /**\n * Updates the rate limiter options\n */\n setOptions(newOptions: Partial<AsyncRateLimiterOptions<TFn>>): void {\n this._options = { ...this._options, ...newOptions }\n }\n\n /**\n * Returns the current rate limiter options\n */\n getOptions(): AsyncRateLimiterOptions<TFn> {\n return this._options\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 * If execution is allowed, waits for any previous execution to complete before proceeding.\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 * - If the rate limit is exceeded, the execution will be rejected and the `onReject` handler\n * will be called if configured.\n * - The error state can be checked using `getErrorCount()` and `getIsExecuting()`.\n * - Rate limit rejections can be tracked using `getRejectionCount()`.\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 execute\n * await rateLimiter.maybeExecute('arg1', 'arg2');\n *\n * // Additional calls within the window will be rejected\n * await rateLimiter.maybeExecute('arg1', 'arg2'); // Rejected\n * ```\n */\n async maybeExecute(\n ...args: Parameters<TFn>\n ): Promise<ReturnType<TFn> | undefined> {\n this.cleanupOldExecutions()\n\n const limit = this.getLimit()\n const window = this.getWindow()\n\n if (this._options.windowType === 'sliding') {\n // For sliding window, we can execute if we have capacity in the current window\n if (this._executionTimes.length < limit) {\n await this.execute(...args)\n return this._lastResult\n }\n } else {\n // For fixed window, we need to check if we're in a new window\n const now = Date.now()\n const oldestExecution = Math.min(...this._executionTimes)\n const isNewWindow = oldestExecution + window <= now\n\n if (isNewWindow || this._executionTimes.length < limit) {\n await this.execute(...args)\n return this._lastResult\n }\n }\n\n this.rejectFunction()\n return undefined\n }\n\n private async execute(\n ...args: Parameters<TFn>\n ): Promise<ReturnType<TFn> | undefined> {\n if (!this.getEnabled()) return\n this._isExecuting = true\n const now = Date.now()\n this._executionTimes.push(now)\n\n try {\n this._lastResult = await this.fn(...args)\n this._successCount++\n this._options.onSuccess?.(this._lastResult!, this)\n } catch (error) {\n this._errorCount++\n this._options.onError?.(error, this)\n if (this._options.throwOnError) {\n throw error\n } else {\n console.error(error)\n }\n } finally {\n this._isExecuting = false\n this._settleCount++\n this._options.onSettled?.(this)\n }\n\n return this._lastResult\n }\n\n private rejectFunction(): void {\n this._rejectionCount++\n if (this._options.onReject) {\n this._options.onReject(this)\n }\n }\n\n private cleanupOldExecutions(): void {\n const now = Date.now()\n const windowStart = now - this.getWindow()\n this._executionTimes = this._executionTimes.filter(\n (time) => time > windowStart,\n )\n }\n\n /**\n * Returns the number of remaining executions allowed in the current window\n */\n getRemainingInWindow(): number {\n this.cleanupOldExecutions()\n return Math.max(0, this.getLimit() - this._executionTimes.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 = Math.min(...this._executionTimes)\n return oldestExecution + this.getWindow() - Date.now()\n }\n\n /**\n * Returns the number of times the function has been executed\n */\n getSuccessCount(): number {\n return this._successCount\n }\n\n /**\n * Returns the number of times the function has been settled\n */\n getSettleCount(): number {\n return this._settleCount\n }\n\n /**\n * Returns the number of times the function has errored\n */\n getErrorCount(): number {\n return this._errorCount\n }\n\n /**\n * Returns the number of times the function has been rejected\n */\n getRejectionCount(): number {\n return this._rejectionCount\n }\n\n /**\n * Returns whether the function is currently executing\n */\n getIsExecuting(): boolean {\n return this._isExecuting\n }\n\n /**\n * Resets the rate limiter state\n */\n reset(): void {\n this._executionTimes = []\n this._successCount = 0\n this._errorCount = 0\n this._rejectionCount = 0\n this._settleCount = 0\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 * Unlike the non-async rate limiter, this async version supports returning values from the rate-limited function,\n * making it ideal for API calls and other async operations where you want the result of the `maybeExecute` call\n * instead of setting the result on a state variable from within the rate-limited function.\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 * 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 * 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 * 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 * // 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.bind(rateLimiter)\n}\n"],"names":[],"mappings":"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"} | ||
| {"version":3,"file":"async-rate-limiter.js","sources":["../../src/async-rate-limiter.ts"],"sourcesContent":["import { Store } from '@tanstack/store'\nimport { parseFunctionOrValue } from './utils'\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-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 * Number of function executions that have completed successfully\n */\n successCount: number\n}\n\nfunction getDefaultAsyncRateLimiterState<\n TFn extends AnyAsyncFunction,\n>(): AsyncRateLimiterState<TFn> {\n return {\n errorCount: 0,\n executionTimes: [],\n isExecuting: false,\n lastResult: undefined,\n rejectionCount: 0,\n settleCount: 0,\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 * 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 * 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?: (error: unknown, rateLimiter: AsyncRateLimiter<TFn>) => void\n /**\n * Optional callback function that is called when an execution is rejected due to rate limiting\n */\n onReject?: (rateLimiter: AsyncRateLimiter<TFn>) => void\n /**\n * Optional function to call when the rate-limited function is executed\n */\n onSettled?: (rateLimiter: AsyncRateLimiter<TFn>) => void\n /**\n * Optional function to call when the rate-limited function is executed\n */\n onSuccess?: (\n result: ReturnType<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\nconst defaultOptions: Omit<\n Required<AsyncRateLimiterOptions<any>>,\n 'initialState' | 'onError' | 'onReject' | 'onSettled' | 'onSuccess'\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 * 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 *\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 * Unlike the non-async RateLimiter, this async version supports returning values from the rate-limited function,\n * making it ideal for API calls and other async operations where you want the result of the `maybeExecute` call\n * instead of setting the result on a state variable from within the rate-limited function.\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 * - `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 options: AsyncRateLimiterOptions<TFn>\n\n constructor(\n private fn: TFn,\n initialOptions: AsyncRateLimiterOptions<TFn>,\n ) {\n this.options = {\n ...defaultOptions,\n ...initialOptions,\n throwOnError: initialOptions.throwOnError ?? !initialOptions.onError,\n }\n this.#setState(this.options.initialState ?? {})\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 return combinedState\n })\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.#cleanupOldExecutions()\n\n const relevantExecutionTimes = this.#getRelevantExecutionTimes()\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?.(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 now = Date.now()\n const executionTimes = [...this.store.state.executionTimes, now]\n this.#setState({\n isExecuting: true,\n executionTimes,\n })\n\n try {\n const result = await this.fn(...args)\n this.#setState({\n successCount: this.store.state.successCount + 1,\n lastResult: result,\n })\n this.options.onSuccess?.(result, this)\n } catch (error) {\n this.#setState({\n errorCount: this.store.state.errorCount + 1,\n })\n this.options.onError?.(error, this)\n if (this.options.throwOnError) {\n throw error\n }\n } finally {\n this.#setState({\n isExecuting: false,\n settleCount: this.store.state.settleCount + 1,\n })\n this.options.onSettled?.(this)\n }\n\n return this.store.state.lastResult\n }\n\n #getRelevantExecutionTimes = (): 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 const oldestExecution = Math.min(...this.store.state.executionTimes)\n const windowStart = oldestExecution\n return this.store.state.executionTimes.filter(\n (time) =>\n time >= windowStart && time <= windowStart + this.#getWindow(),\n )\n }\n }\n\n #cleanupOldExecutions = (): void => {\n const now = Date.now()\n const windowStart = now - this.#getWindow()\n this.#setState({\n executionTimes: this.store.state.executionTimes.filter(\n (time) => time > windowStart,\n ),\n })\n }\n\n /**\n * Returns the number of remaining executions allowed in the current window\n */\n getRemainingInWindow = (): number => {\n const relevantExecutionTimes = this.#getRelevantExecutionTimes()\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 * Resets the rate limiter state\n */\n reset = (): void => {\n this.#setState(getDefaultAsyncRateLimiterState())\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 * Unlike the non-async rate limiter, this async version supports returning values from the rate-limited function,\n * making it ideal for API calls and other async operations where you want the result of the `maybeExecute` call\n * instead of setting the result on a state variable from within the rate-limited function.\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 * 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 * 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 * 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 * 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 * // 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"],"names":[],"mappings":";;AAmCA,SAAS,kCAEuB;AAC9B,SAAO;AAAA,IACL,YAAY;AAAA,IACZ,gBAAgB,CAAA;AAAA,IAChB,aAAa;AAAA,IACb,YAAY;AAAA,IACZ,gBAAgB;AAAA,IAChB,aAAa;AAAA,IACb,cAAc;AAAA,EAAA;AAElB;AA8DA,MAAM,iBAGF;AAAA,EACF,SAAS;AAAA,EACT,OAAO;AAAA,EACP,QAAQ;AAAA,EACR,YAAY;AAAA,EACZ,cAAc;AAChB;AAoEO,MAAM,iBAA+C;AAAA,EAM1D,YACU,IACR,gBACA;AAFQ,SAAA,KAAA;AANV,SAAS,QAAqD,IAAI,MAEhE,gCAAA,CAAsC;AAkBxC,SAAA,aAAa,CAAC,eAA4D;AACxE,WAAK,UAAU,EAAE,GAAG,KAAK,SAAS,GAAG,WAAA;AAAA,IAAW;AAGlD,SAAA,YAAY,CAAC,aAAwD;AACnE,WAAK,MAAM,SAAS,CAAC,UAAU;AAC7B,cAAM,gBAAgB;AAAA,UACpB,GAAG;AAAA,UACH,GAAG;AAAA,QAAA;AAEL,eAAO;AAAA,MAAA,CACR;AAAA,IAAA;AAMH,SAAA,cAAc,MAAe;AAC3B,aAAO,CAAC,CAAC,qBAAqB,KAAK,QAAQ,SAAS,IAAI;AAAA,IAAA;AAM1D,SAAA,YAAY,MAAc;AACxB,aAAO,qBAAqB,KAAK,QAAQ,OAAO,IAAI;AAAA,IAAA;AAMtD,SAAA,aAAa,MAAc;AACzB,aAAO,qBAAqB,KAAK,QAAQ,QAAQ,IAAI;AAAA,IAAA;AA4BvD,SAAA,eAAe,UACV,SACsC;AACzC,WAAK,sBAAA;AAEL,YAAM,yBAAyB,KAAK,2BAAA;AAEpC,UAAI,uBAAuB,SAAS,KAAK,UAAA,GAAa;AACpD,cAAM,KAAK,SAAS,GAAG,IAAI;AAC3B,eAAO,KAAK,MAAM,MAAM;AAAA,MAAA;AAG1B,WAAK,UAAU;AAAA,QACb,gBAAgB,KAAK,MAAM,MAAM,iBAAiB;AAAA,MAAA,CACnD;AACD,WAAK,QAAQ,WAAW,IAAI;AAC5B,aAAO;AAAA,IAAA;AAGT,SAAA,WAAW,UACN,SACsC;AACzC,UAAI,CAAC,KAAK,cAAe;AAEzB,YAAM,MAAM,KAAK,IAAA;AACjB,YAAM,iBAAiB,CAAC,GAAG,KAAK,MAAM,MAAM,gBAAgB,GAAG;AAC/D,WAAK,UAAU;AAAA,QACb,aAAa;AAAA,QACb;AAAA,MAAA,CACD;AAED,UAAI;AACF,cAAM,SAAS,MAAM,KAAK,GAAG,GAAG,IAAI;AACpC,aAAK,UAAU;AAAA,UACb,cAAc,KAAK,MAAM,MAAM,eAAe;AAAA,UAC9C,YAAY;AAAA,QAAA,CACb;AACD,aAAK,QAAQ,YAAY,QAAQ,IAAI;AAAA,MAAA,SAC9B,OAAO;AACd,aAAK,UAAU;AAAA,UACb,YAAY,KAAK,MAAM,MAAM,aAAa;AAAA,QAAA,CAC3C;AACD,aAAK,QAAQ,UAAU,OAAO,IAAI;AAClC,YAAI,KAAK,QAAQ,cAAc;AAC7B,gBAAM;AAAA,QAAA;AAAA,MACR,UACF;AACE,aAAK,UAAU;AAAA,UACb,aAAa;AAAA,UACb,aAAa,KAAK,MAAM,MAAM,cAAc;AAAA,QAAA,CAC7C;AACD,aAAK,QAAQ,YAAY,IAAI;AAAA,MAAA;AAG/B,aAAO,KAAK,MAAM,MAAM;AAAA,IAAA;AAG1B,SAAA,6BAA6B,MAAqB;AAChD,UAAI,KAAK,QAAQ,eAAe,WAAW;AAEzC,eAAO,KAAK,MAAM,MAAM,eAAe;AAAA,UACrC,CAAC,SAAS,OAAO,KAAK,IAAA,IAAQ,KAAK,WAAA;AAAA,QAAW;AAAA,MAChD,OACK;AAGL,cAAM,kBAAkB,KAAK,IAAI,GAAG,KAAK,MAAM,MAAM,cAAc;AACnE,cAAM,cAAc;AACpB,eAAO,KAAK,MAAM,MAAM,eAAe;AAAA,UACrC,CAAC,SACC,QAAQ,eAAe,QAAQ,cAAc,KAAK,WAAA;AAAA,QAAW;AAAA,MACjE;AAAA,IACF;AAGF,SAAA,wBAAwB,MAAY;AAClC,YAAM,MAAM,KAAK,IAAA;AACjB,YAAM,cAAc,MAAM,KAAK,WAAA;AAC/B,WAAK,UAAU;AAAA,QACb,gBAAgB,KAAK,MAAM,MAAM,eAAe;AAAA,UAC9C,CAAC,SAAS,OAAO;AAAA,QAAA;AAAA,MACnB,CACD;AAAA,IAAA;AAMH,SAAA,uBAAuB,MAAc;AACnC,YAAM,yBAAyB,KAAK,2BAAA;AACpC,aAAO,KAAK,IAAI,GAAG,KAAK,UAAA,IAAc,uBAAuB,MAAM;AAAA,IAAA;AAQrE,SAAA,uBAAuB,MAAc;AACnC,UAAI,KAAK,qBAAA,IAAyB,GAAG;AACnC,eAAO;AAAA,MAAA;AAET,YAAM,kBAAkB,KAAK,MAAM,MAAM,eAAe,CAAC,KAAK;AAC9D,aAAO,kBAAkB,KAAK,WAAA,IAAe,KAAK,IAAA;AAAA,IAAI;AAMxD,SAAA,QAAQ,MAAY;AAClB,WAAK,UAAU,iCAAiC;AAAA,IAAA;AArLhD,SAAK,UAAU;AAAA,MACb,GAAG;AAAA,MACH,GAAG;AAAA,MACH,cAAc,eAAe,gBAAgB,CAAC,eAAe;AAAA,IAAA;AAE/D,SAAK,UAAU,KAAK,QAAQ,gBAAgB,CAAA,CAAE;AAAA,EAAA;AAAA,EAUhD;AAAA,EAaA;AAAA,EAOA;AAAA,EAOA;AAAA,EAgDA;AAAA,EAsCA;AAAA,EAkBA;AAqCF;AAmEO,SAAS,eACd,IACA,gBACA;AACA,QAAM,cAAc,IAAI,iBAAiB,IAAI,cAAc;AAC3D,SAAO,YAAY;AACrB;"} |
@@ -0,2 +1,45 @@ | ||
| import { Store } from '@tanstack/store'; | ||
| import { AnyAsyncFunction } from './types.js'; | ||
| export interface AsyncThrottlerState<TFn extends AnyAsyncFunction> { | ||
| /** | ||
| * Number of function executions that have resulted in errors | ||
| */ | ||
| errorCount: number; | ||
| /** | ||
| * Whether the throttled function is currently executing asynchronously | ||
| */ | ||
| isExecuting: boolean; | ||
| /** | ||
| * Whether the throttler is waiting for the timeout to trigger execution | ||
| */ | ||
| isPending: boolean; | ||
| /** | ||
| * The arguments from the most recent call to maybeExecute | ||
| */ | ||
| lastArgs: Parameters<TFn> | undefined; | ||
| /** | ||
| * Timestamp of the last function execution in milliseconds | ||
| */ | ||
| lastExecutionTime: number; | ||
| /** | ||
| * The result from the most recent successful function execution | ||
| */ | ||
| lastResult: ReturnType<TFn> | undefined; | ||
| /** | ||
| * Timestamp when the next execution can occur in milliseconds | ||
| */ | ||
| nextExecutionTime: number; | ||
| /** | ||
| * Number of function executions that have completed (either successfully or with errors) | ||
| */ | ||
| settleCount: number; | ||
| /** | ||
| * Current execution status - 'idle' when not active, 'pending' when waiting, 'executing' when running, 'settled' when completed | ||
| */ | ||
| status: 'disabled' | 'idle' | 'pending' | 'executing' | 'settled'; | ||
| /** | ||
| * Number of function executions that have completed successfully | ||
| */ | ||
| successCount: number; | ||
| } | ||
| /** | ||
@@ -13,2 +56,6 @@ * Options for configuring an async throttled function | ||
| /** | ||
| * Initial state for the async throttler | ||
| */ | ||
| initialState?: Partial<AsyncThrottlerState<TFn>>; | ||
| /** | ||
| * Whether to execute the function immediately when called | ||
@@ -71,2 +118,12 @@ * Defaults to true | ||
| * | ||
| * State Management: | ||
| * - Uses TanStack Store for reactive state management | ||
| * - Use `initialState` to provide initial state values when creating the async throttler | ||
| * - Use `onSuccess` callback to react to successful function execution and implement custom logic | ||
| * - Use `onError` callback to react to function execution errors and implement custom error handling | ||
| * - Use `onSettled` callback to react to function execution completion (success or error) and implement custom logic | ||
| * - The state includes error count, execution status, last execution time, and success/settle counts | ||
| * - State can be accessed via `asyncThrottler.store.state` when using the class directly | ||
| * - When using framework adapters (React/Solid), state is accessed from `asyncThrottler.state` | ||
| * | ||
| * @example | ||
@@ -90,85 +147,46 @@ * ```ts | ||
| export declare class AsyncThrottler<TFn extends AnyAsyncFunction> { | ||
| #private; | ||
| private fn; | ||
| private _options; | ||
| private _abortController; | ||
| private _errorCount; | ||
| private _isExecuting; | ||
| private _lastArgs; | ||
| private _lastExecutionTime; | ||
| private _lastResult; | ||
| private _nextExecutionTime; | ||
| private _settleCount; | ||
| private _successCount; | ||
| private _timeoutId; | ||
| private _resolvePreviousPromise; | ||
| readonly store: Store<Readonly<AsyncThrottlerState<TFn>>>; | ||
| options: AsyncThrottlerOptions<TFn>; | ||
| constructor(fn: TFn, initialOptions: AsyncThrottlerOptions<TFn>); | ||
| /** | ||
| * Updates the throttler options | ||
| * Updates the async throttler options | ||
| */ | ||
| setOptions(newOptions: Partial<AsyncThrottlerOptions<TFn>>): void; | ||
| setOptions: (newOptions: Partial<AsyncThrottlerOptions<TFn>>) => void; | ||
| /** | ||
| * Returns the current options | ||
| */ | ||
| getOptions(): AsyncThrottlerOptions<TFn>; | ||
| /** | ||
| * Returns the current enabled state of the throttler | ||
| */ | ||
| getEnabled(): boolean; | ||
| /** | ||
| * Returns the current wait time in milliseconds | ||
| */ | ||
| getWait(): number; | ||
| /** | ||
| * Attempts to execute the throttled function. | ||
| * If a call is already in progress, it may be blocked or queued depending on the `wait` option. | ||
| * Attempts to execute the throttled function. The execution behavior depends on the throttler options: | ||
| * | ||
| * Error Handling: | ||
| * - If the throttled function throws and no `onError` handler is configured, | ||
| * the error will be thrown from this method. | ||
| * - If an `onError` handler is configured, errors will be caught and passed to the handler, | ||
| * and this method will return undefined. | ||
| * - The error state can be checked using `getErrorCount()` and `getIsExecuting()`. | ||
| * - If enough time has passed since the last execution (>= wait period): | ||
| * - With leading=true: Executes immediately | ||
| * - With leading=false: Waits for the next trailing execution | ||
| * | ||
| * @returns A promise that resolves with the function's return value, or undefined if an error occurred and was handled by onError | ||
| * @throws The error from the throttled function if no onError handler is configured | ||
| * - If within the wait period: | ||
| * - With trailing=true: Schedules execution for end of wait period | ||
| * - With trailing=false: Drops the execution | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * const throttled = new AsyncThrottler(fn, { wait: 1000 }); | ||
| * | ||
| * // First call executes immediately | ||
| * await throttled.maybeExecute('a', 'b'); | ||
| * | ||
| * // Call during wait period - gets throttled | ||
| * await throttled.maybeExecute('c', 'd'); | ||
| * ``` | ||
| */ | ||
| maybeExecute(...args: Parameters<TFn>): Promise<ReturnType<TFn> | undefined>; | ||
| private execute; | ||
| private resolvePreviousPromise; | ||
| maybeExecute: (...args: Parameters<TFn>) => Promise<ReturnType<TFn> | undefined>; | ||
| /** | ||
| * Cancels any pending execution or aborts any execution in progress | ||
| * Processes the current pending execution immediately | ||
| */ | ||
| cancel(): void; | ||
| flush: () => void; | ||
| /** | ||
| * Returns the last execution time | ||
| * Cancels any pending execution or aborts any execution in progress | ||
| */ | ||
| getLastExecutionTime(): number; | ||
| cancel: () => void; | ||
| /** | ||
| * Returns the next execution time | ||
| * Resets the debouncer state to its default values | ||
| */ | ||
| getNextExecutionTime(): number; | ||
| /** | ||
| * Returns the last result of the debounced function | ||
| */ | ||
| getLastResult(): ReturnType<TFn> | undefined; | ||
| /** | ||
| * Returns the number of times the function has been executed successfully | ||
| */ | ||
| getSuccessCount(): number; | ||
| /** | ||
| * Returns the number of times the function has settled (completed or errored) | ||
| */ | ||
| getSettleCount(): number; | ||
| /** | ||
| * Returns the number of times the function has errored | ||
| */ | ||
| getErrorCount(): number; | ||
| /** | ||
| * Returns the current pending state | ||
| */ | ||
| getIsPending(): boolean; | ||
| /** | ||
| * Returns the current executing state | ||
| */ | ||
| getIsExecuting(): boolean; | ||
| reset: () => void; | ||
| } | ||
@@ -191,2 +209,12 @@ /** | ||
| * | ||
| * State Management: | ||
| * - Uses TanStack Store for reactive state management | ||
| * - Use `initialState` to provide initial state values when creating the async throttler | ||
| * - Use `onSuccess` callback to react to successful function execution and implement custom logic | ||
| * - Use `onError` callback to react to function execution errors and implement custom error handling | ||
| * - Use `onSettled` callback to react to function execution completion (success or error) and implement custom logic | ||
| * - The state includes error count, execution status, last execution time, and success/settle counts | ||
| * - State can be accessed via the underlying AsyncThrottler instance's `store.state` property | ||
| * - When using framework adapters (React/Solid), state is accessed from the hook's state property | ||
| * | ||
| * @example | ||
@@ -193,0 +221,0 @@ * ```ts |
+167
-177
@@ -0,2 +1,17 @@ | ||
| import { Store } from "@tanstack/store"; | ||
| import { parseFunctionOrValue } from "./utils.js"; | ||
| function getDefaultAsyncThrottlerState() { | ||
| return structuredClone({ | ||
| errorCount: 0, | ||
| isExecuting: false, | ||
| isPending: false, | ||
| lastArgs: void 0, | ||
| lastExecutionTime: 0, | ||
| lastResult: void 0, | ||
| nextExecutionTime: 0, | ||
| settleCount: 0, | ||
| status: "idle", | ||
| successCount: 0 | ||
| }); | ||
| } | ||
| const defaultOptions = { | ||
@@ -11,12 +26,141 @@ enabled: true, | ||
| this.fn = fn; | ||
| this._abortController = null; | ||
| this._errorCount = 0; | ||
| this._isExecuting = false; | ||
| this._lastExecutionTime = 0; | ||
| this._nextExecutionTime = 0; | ||
| this._settleCount = 0; | ||
| this._successCount = 0; | ||
| this._timeoutId = null; | ||
| this._resolvePreviousPromise = null; | ||
| this._options = { | ||
| this.store = new Store(getDefaultAsyncThrottlerState()); | ||
| this.#abortController = null; | ||
| this.#timeoutId = null; | ||
| this.#resolvePreviousPromise = null; | ||
| this.setOptions = (newOptions) => { | ||
| this.options = { ...this.options, ...newOptions }; | ||
| if (!this.#getEnabled()) { | ||
| this.cancel(); | ||
| } | ||
| }; | ||
| this.#setState = (newState) => { | ||
| this.store.setState((state) => { | ||
| const combinedState = { | ||
| ...state, | ||
| ...newState | ||
| }; | ||
| const { isPending, isExecuting, settleCount } = combinedState; | ||
| return { | ||
| ...combinedState, | ||
| status: !this.#getEnabled() ? "disabled" : isPending ? "pending" : isExecuting ? "executing" : settleCount > 0 ? "settled" : "idle" | ||
| }; | ||
| }); | ||
| }; | ||
| this.#getEnabled = () => { | ||
| return !!parseFunctionOrValue(this.options.enabled, this); | ||
| }; | ||
| this.#getWait = () => { | ||
| return parseFunctionOrValue(this.options.wait, this); | ||
| }; | ||
| this.maybeExecute = async (...args) => { | ||
| if (!this.#getEnabled()) return void 0; | ||
| const now = Date.now(); | ||
| const timeSinceLastExecution = now - this.store.state.lastExecutionTime; | ||
| const wait = this.#getWait(); | ||
| this.#setState({ lastArgs: args }); | ||
| this.#resolvePreviousPromiseInternal(); | ||
| if (this.options.leading && timeSinceLastExecution >= wait) { | ||
| await this.#execute(...args); | ||
| return this.store.state.lastResult; | ||
| } else { | ||
| return new Promise((resolve) => { | ||
| this.#resolvePreviousPromise = resolve; | ||
| this.#clearTimeout(); | ||
| if (this.options.trailing) { | ||
| const _timeSinceLastExecution = this.store.state.lastExecutionTime ? now - this.store.state.lastExecutionTime : 0; | ||
| const timeoutDuration = wait - _timeSinceLastExecution; | ||
| this.#setState({ isPending: true }); | ||
| this.#timeoutId = setTimeout(async () => { | ||
| if (this.store.state.lastArgs !== void 0) { | ||
| await this.#execute(...this.store.state.lastArgs); | ||
| } | ||
| this.#resolvePreviousPromise = null; | ||
| resolve(this.store.state.lastResult); | ||
| }, timeoutDuration); | ||
| } | ||
| }); | ||
| } | ||
| }; | ||
| this.#execute = async (...args) => { | ||
| if (!this.#getEnabled() || this.store.state.isExecuting) return void 0; | ||
| this.#abortController = new AbortController(); | ||
| try { | ||
| this.#setState({ isExecuting: true }); | ||
| const result = await this.fn(...args); | ||
| this.#setState({ | ||
| lastResult: result, | ||
| successCount: this.store.state.successCount + 1 | ||
| }); | ||
| this.options.onSuccess?.(result, this); | ||
| } catch (error) { | ||
| this.#setState({ | ||
| errorCount: this.store.state.errorCount + 1 | ||
| }); | ||
| this.options.onError?.(error, this); | ||
| if (this.options.throwOnError) { | ||
| throw error; | ||
| } else { | ||
| console.error(error); | ||
| } | ||
| } finally { | ||
| const lastExecutionTime = Date.now(); | ||
| const nextExecutionTime = lastExecutionTime + this.#getWait(); | ||
| this.#setState({ | ||
| isExecuting: false, | ||
| isPending: false, | ||
| settleCount: this.store.state.settleCount + 1, | ||
| lastExecutionTime, | ||
| nextExecutionTime | ||
| }); | ||
| this.#abortController = null; | ||
| this.options.onSettled?.(this); | ||
| } | ||
| return this.store.state.lastResult; | ||
| }; | ||
| this.flush = () => { | ||
| if (this.store.state.isPending && this.store.state.lastArgs) { | ||
| this.#abortExecution(); | ||
| this.#clearTimeout(); | ||
| this.#execute(...this.store.state.lastArgs); | ||
| } | ||
| }; | ||
| this.#resolvePreviousPromiseInternal = () => { | ||
| if (this.#resolvePreviousPromise) { | ||
| this.#resolvePreviousPromise(this.store.state.lastResult); | ||
| this.#resolvePreviousPromise = null; | ||
| } | ||
| }; | ||
| this.#clearTimeout = () => { | ||
| if (this.#timeoutId) { | ||
| clearTimeout(this.#timeoutId); | ||
| this.#timeoutId = null; | ||
| } | ||
| }; | ||
| this.#cancelPendingExecution = () => { | ||
| this.#clearTimeout(); | ||
| if (this.#resolvePreviousPromise) { | ||
| this.#resolvePreviousPromise(this.store.state.lastResult); | ||
| this.#resolvePreviousPromise = null; | ||
| } | ||
| this.#setState({ | ||
| isPending: false, | ||
| isExecuting: false, | ||
| lastArgs: void 0 | ||
| }); | ||
| }; | ||
| this.#abortExecution = () => { | ||
| if (this.#abortController) { | ||
| this.#abortController.abort(); | ||
| this.#abortController = null; | ||
| } | ||
| }; | ||
| this.cancel = () => { | ||
| this.#cancelPendingExecution(); | ||
| this.#abortExecution(); | ||
| }; | ||
| this.reset = () => { | ||
| this.#setState(getDefaultAsyncThrottlerState()); | ||
| }; | ||
| this.options = { | ||
| ...defaultOptions, | ||
@@ -26,173 +170,19 @@ ...initialOptions, | ||
| }; | ||
| this.#setState(this.options.initialState ?? {}); | ||
| } | ||
| /** | ||
| * Updates the throttler options | ||
| */ | ||
| setOptions(newOptions) { | ||
| this._options = { ...this._options, ...newOptions }; | ||
| if (!this._options.enabled) { | ||
| this.cancel(); | ||
| } | ||
| } | ||
| /** | ||
| * Returns the current options | ||
| */ | ||
| getOptions() { | ||
| return this._options; | ||
| } | ||
| /** | ||
| * Returns the current enabled state of the throttler | ||
| */ | ||
| getEnabled() { | ||
| return !!parseFunctionOrValue(this._options.enabled, this); | ||
| } | ||
| /** | ||
| * Returns the current wait time in milliseconds | ||
| */ | ||
| getWait() { | ||
| return parseFunctionOrValue(this._options.wait, this); | ||
| } | ||
| /** | ||
| * Attempts to execute the throttled function. | ||
| * If a call is already in progress, it may be blocked or queued depending on the `wait` option. | ||
| * | ||
| * Error Handling: | ||
| * - If the throttled function throws and no `onError` handler is configured, | ||
| * the error will be thrown from this method. | ||
| * - If an `onError` handler is configured, errors will be caught and passed to the handler, | ||
| * and this method will return undefined. | ||
| * - The error state can be checked using `getErrorCount()` and `getIsExecuting()`. | ||
| * | ||
| * @returns A promise that resolves with the function's return value, or undefined if an error occurred and was handled by onError | ||
| * @throws The error from the throttled function if no onError handler is configured | ||
| */ | ||
| async maybeExecute(...args) { | ||
| const now = Date.now(); | ||
| const timeSinceLastExecution = now - this._lastExecutionTime; | ||
| const wait = this.getWait(); | ||
| this.resolvePreviousPromise(); | ||
| if (this._options.leading && timeSinceLastExecution >= wait) { | ||
| await this.execute(...args); | ||
| return this._lastResult; | ||
| } else { | ||
| this._lastArgs = args; | ||
| return new Promise((resolve) => { | ||
| this._resolvePreviousPromise = resolve; | ||
| if (this._timeoutId) { | ||
| clearTimeout(this._timeoutId); | ||
| } | ||
| if (this._options.trailing) { | ||
| const _timeSinceLastExecution = this._lastExecutionTime ? now - this._lastExecutionTime : 0; | ||
| const timeoutDuration = wait - _timeSinceLastExecution; | ||
| this._timeoutId = setTimeout(async () => { | ||
| if (this._lastArgs !== void 0) { | ||
| await this.execute(...this._lastArgs); | ||
| } | ||
| this._resolvePreviousPromise = null; | ||
| resolve(this._lastResult); | ||
| }, timeoutDuration); | ||
| } | ||
| }); | ||
| } | ||
| } | ||
| async execute(...args) { | ||
| var _a, _b, _c, _d, _e, _f; | ||
| if (!this.getEnabled() || this._isExecuting) return void 0; | ||
| this._abortController = new AbortController(); | ||
| try { | ||
| this._isExecuting = true; | ||
| this._lastResult = await this.fn(...args); | ||
| this._successCount++; | ||
| (_b = (_a = this._options).onSuccess) == null ? void 0 : _b.call(_a, this._lastResult, this); | ||
| } catch (error) { | ||
| this._errorCount++; | ||
| (_d = (_c = this._options).onError) == null ? void 0 : _d.call(_c, error, this); | ||
| if (this._options.throwOnError) { | ||
| throw error; | ||
| } else { | ||
| console.error(error); | ||
| } | ||
| } finally { | ||
| this._isExecuting = false; | ||
| this._settleCount++; | ||
| this._abortController = null; | ||
| this._lastExecutionTime = Date.now(); | ||
| this._nextExecutionTime = this._lastExecutionTime + this.getWait(); | ||
| (_f = (_e = this._options).onSettled) == null ? void 0 : _f.call(_e, this); | ||
| } | ||
| return this._lastResult; | ||
| } | ||
| resolvePreviousPromise() { | ||
| if (this._resolvePreviousPromise) { | ||
| this._resolvePreviousPromise(this._lastResult); | ||
| this._resolvePreviousPromise = null; | ||
| } | ||
| } | ||
| /** | ||
| * Cancels any pending execution or aborts any execution in progress | ||
| */ | ||
| cancel() { | ||
| if (this._timeoutId) { | ||
| clearTimeout(this._timeoutId); | ||
| this._timeoutId = null; | ||
| } | ||
| if (this._abortController) { | ||
| this._abortController.abort(); | ||
| this._abortController = null; | ||
| } | ||
| this.resolvePreviousPromise(); | ||
| this._lastArgs = void 0; | ||
| } | ||
| /** | ||
| * Returns the last execution time | ||
| */ | ||
| getLastExecutionTime() { | ||
| return this._lastExecutionTime; | ||
| } | ||
| /** | ||
| * Returns the next execution time | ||
| */ | ||
| getNextExecutionTime() { | ||
| return this._nextExecutionTime; | ||
| } | ||
| /** | ||
| * Returns the last result of the debounced function | ||
| */ | ||
| getLastResult() { | ||
| return this._lastResult; | ||
| } | ||
| /** | ||
| * Returns the number of times the function has been executed successfully | ||
| */ | ||
| getSuccessCount() { | ||
| return this._successCount; | ||
| } | ||
| /** | ||
| * Returns the number of times the function has settled (completed or errored) | ||
| */ | ||
| getSettleCount() { | ||
| return this._settleCount; | ||
| } | ||
| /** | ||
| * Returns the number of times the function has errored | ||
| */ | ||
| getErrorCount() { | ||
| return this._errorCount; | ||
| } | ||
| /** | ||
| * Returns the current pending state | ||
| */ | ||
| getIsPending() { | ||
| return this.getEnabled() && !!this._timeoutId; | ||
| } | ||
| /** | ||
| * Returns the current executing state | ||
| */ | ||
| getIsExecuting() { | ||
| return this._isExecuting; | ||
| } | ||
| #abortController; | ||
| #timeoutId; | ||
| #resolvePreviousPromise; | ||
| #setState; | ||
| #getEnabled; | ||
| #getWait; | ||
| #execute; | ||
| #resolvePreviousPromiseInternal; | ||
| #clearTimeout; | ||
| #cancelPendingExecution; | ||
| #abortExecution; | ||
| } | ||
| function asyncThrottle(fn, initialOptions) { | ||
| const asyncThrottler = new AsyncThrottler(fn, initialOptions); | ||
| return asyncThrottler.maybeExecute.bind(asyncThrottler); | ||
| return asyncThrottler.maybeExecute; | ||
| } | ||
@@ -199,0 +189,0 @@ export { |
@@ -1,1 +0,1 @@ | ||
| {"version":3,"file":"async-throttler.js","sources":["../../src/async-throttler.ts"],"sourcesContent":["import { parseFunctionOrValue } from './utils'\nimport type { AnyAsyncFunction, OptionalKeys } from './types'\n\n/**\n * Options for configuring an async throttled function\n */\nexport interface AsyncThrottlerOptions<TFn extends AnyAsyncFunction> {\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 * 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?: (error: unknown, asyncThrottler: AsyncThrottler<TFn>) => void\n /**\n * Optional function to call when the throttled function is executed\n */\n onSettled?: (asyncThrottler: AsyncThrottler<TFn>) => void\n /**\n * Optional function to call when the throttled function is executed\n */\n onSuccess?: (\n result: ReturnType<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\ntype AsyncThrottlerOptionsWithOptionalCallbacks = OptionalKeys<\n AsyncThrottlerOptions<any>,\n 'onError' | 'onSettled' | 'onSuccess'\n>\n\nconst defaultOptions: AsyncThrottlerOptionsWithOptionalCallbacks = {\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 * 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 * Unlike the non-async Throttler, this async version supports returning values from the throttled function,\n * making it ideal for API calls and other async operations where you want the result of the `maybeExecute` call\n * instead of setting the result on a state variable from within the throttled function.\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 * @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 private _options: AsyncThrottlerOptionsWithOptionalCallbacks\n private _abortController: AbortController | null = null\n private _errorCount = 0\n private _isExecuting = false\n private _lastArgs: Parameters<TFn> | undefined\n private _lastExecutionTime = 0\n private _lastResult: ReturnType<TFn> | undefined\n private _nextExecutionTime = 0\n private _settleCount = 0\n private _successCount = 0\n private _timeoutId: NodeJS.Timeout | null = null\n private _resolvePreviousPromise:\n | ((value?: ReturnType<TFn> | undefined) => void)\n | null = null\n\n constructor(\n private fn: TFn,\n initialOptions: AsyncThrottlerOptions<TFn>,\n ) {\n this._options = {\n ...defaultOptions,\n ...initialOptions,\n throwOnError: initialOptions.throwOnError ?? !initialOptions.onError,\n }\n }\n\n /**\n * Updates the throttler options\n */\n setOptions(newOptions: Partial<AsyncThrottlerOptions<TFn>>): void {\n this._options = { ...this._options, ...newOptions }\n\n // End the pending state if the debouncer is disabled\n if (!this._options.enabled) {\n this.cancel()\n }\n }\n\n /**\n * Returns the current options\n */\n getOptions(): AsyncThrottlerOptions<TFn> {\n return this._options\n }\n\n /**\n * Returns the current enabled state of the 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.\n * If a call is already in progress, it may be blocked or queued depending on the `wait` option.\n *\n * Error Handling:\n * - If the throttled 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 throttled function if no onError handler is configured\n */\n async maybeExecute(\n ...args: Parameters<TFn>\n ): Promise<ReturnType<TFn> | undefined> {\n const now = Date.now()\n const timeSinceLastExecution = now - this._lastExecutionTime\n const wait = this.getWait()\n\n this.resolvePreviousPromise()\n\n // Handle leading execution\n if (this._options.leading && timeSinceLastExecution >= wait) {\n await this.execute(...args)\n return this._lastResult\n } else {\n // Store the most recent arguments for potential trailing execution\n this._lastArgs = args\n\n return new Promise((resolve) => {\n this._resolvePreviousPromise = resolve\n // Clear any existing timeout to ensure we use the latest arguments\n if (this._timeoutId) {\n clearTimeout(this._timeoutId)\n }\n\n // Set up trailing execution if enabled\n if (this._options.trailing) {\n const _timeSinceLastExecution = this._lastExecutionTime\n ? now - this._lastExecutionTime\n : 0\n const timeoutDuration = wait - _timeSinceLastExecution\n this._timeoutId = setTimeout(async () => {\n if (this._lastArgs !== undefined) {\n await this.execute(...this._lastArgs)\n }\n this._resolvePreviousPromise = null\n resolve(this._lastResult)\n }, timeoutDuration)\n }\n })\n }\n }\n\n private async execute(\n ...args: Parameters<TFn>\n ): Promise<ReturnType<TFn> | undefined> {\n if (!this.getEnabled() || this._isExecuting) return undefined\n this._abortController = new AbortController()\n try {\n this._isExecuting = true\n this._lastResult = await this.fn(...args) // EXECUTE!\n this._successCount++\n this._options.onSuccess?.(this._lastResult!, this)\n } catch (error) {\n this._errorCount++\n this._options.onError?.(error, this)\n if (this._options.throwOnError) {\n throw error\n } else {\n console.error(error)\n }\n } finally {\n this._isExecuting = false\n this._settleCount++\n this._abortController = null\n this._lastExecutionTime = Date.now()\n this._nextExecutionTime = this._lastExecutionTime + this.getWait()\n this._options.onSettled?.(this)\n }\n return this._lastResult\n }\n\n private resolvePreviousPromise(): void {\n if (this._resolvePreviousPromise) {\n this._resolvePreviousPromise(this._lastResult)\n this._resolvePreviousPromise = null\n }\n }\n\n /**\n * Cancels any pending execution or aborts any execution in progress\n */\n cancel(): void {\n if (this._timeoutId) {\n clearTimeout(this._timeoutId)\n this._timeoutId = null\n }\n if (this._abortController) {\n this._abortController.abort()\n this._abortController = null\n }\n this.resolvePreviousPromise()\n this._lastArgs = undefined\n }\n\n /**\n * Returns the last execution time\n */\n getLastExecutionTime(): number {\n return this._lastExecutionTime\n }\n\n /**\n * Returns the next execution time\n */\n getNextExecutionTime(): number {\n return this._nextExecutionTime\n }\n\n /**\n * Returns the last result of the debounced function\n */\n getLastResult(): ReturnType<TFn> | undefined {\n return this._lastResult\n }\n\n /**\n * Returns the number of times the function has been executed successfully\n */\n getSuccessCount(): number {\n return this._successCount\n }\n\n /**\n * Returns the number of times the function has settled (completed or errored)\n */\n getSettleCount(): number {\n return this._settleCount\n }\n\n /**\n * Returns the number of times the function has errored\n */\n getErrorCount(): number {\n return this._errorCount\n }\n\n /**\n * Returns the current pending state\n */\n getIsPending(): boolean {\n return this.getEnabled() && !!this._timeoutId\n }\n\n /**\n * Returns the current executing state\n */\n getIsExecuting(): boolean {\n return this._isExecuting\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 * Unlike the non-async Throttler, this async version supports returning values from the throttled function,\n * making it ideal for API calls and other async operations where you want the result of the `maybeExecute` call\n * instead of setting the result on a state variable from within the throttled function.\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 * @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.bind(asyncThrottler)\n}\n"],"names":[],"mappings":"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"} | ||
| {"version":3,"file":"async-throttler.js","sources":["../../src/async-throttler.ts"],"sourcesContent":["import { Store } from '@tanstack/store'\nimport { parseFunctionOrValue } from './utils'\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 * Timestamp when the next execution can occur in milliseconds\n */\n nextExecutionTime: 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 getDefaultAsyncThrottlerState<\n TFn extends AnyAsyncFunction,\n>(): AsyncThrottlerState<TFn> {\n return structuredClone({\n errorCount: 0,\n isExecuting: false,\n isPending: false,\n lastArgs: undefined,\n lastExecutionTime: 0,\n lastResult: undefined,\n nextExecutionTime: 0,\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 * 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 * 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?: (error: unknown, asyncThrottler: AsyncThrottler<TFn>) => void\n /**\n * Optional function to call when the throttled function is executed\n */\n onSettled?: (asyncThrottler: AsyncThrottler<TFn>) => void\n /**\n * Optional function to call when the throttled function is executed\n */\n onSuccess?: (\n result: ReturnType<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\ntype AsyncThrottlerOptionsWithOptionalCallbacks = OptionalKeys<\n AsyncThrottlerOptions<any>,\n 'initialState' | 'onError' | 'onSettled' | 'onSuccess'\n>\n\nconst defaultOptions: AsyncThrottlerOptionsWithOptionalCallbacks = {\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 * 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 * Unlike the non-async Throttler, this async version supports returning values from the throttled function,\n * making it ideal for API calls and other async operations where you want the result of the `maybeExecute` call\n * instead of setting the result on a state variable from within the throttled function.\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 options: AsyncThrottlerOptions<TFn>\n #abortController: AbortController | null = null\n #timeoutId: NodeJS.Timeout | null = null\n #resolvePreviousPromise:\n | ((value?: ReturnType<TFn> | undefined) => void)\n | null = null\n\n constructor(\n private fn: TFn,\n initialOptions: AsyncThrottlerOptions<TFn>,\n ) {\n this.options = {\n ...defaultOptions,\n ...initialOptions,\n throwOnError: initialOptions.throwOnError ?? !initialOptions.onError,\n }\n this.#setState(this.options.initialState ?? {})\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 }\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 const now = Date.now()\n const timeSinceLastExecution = now - this.store.state.lastExecutionTime\n const wait = this.#getWait()\n // Store the most recent arguments for potential trailing execution\n this.#setState({ lastArgs: args })\n\n this.#resolvePreviousPromiseInternal()\n\n // Handle leading execution\n if (this.options.leading && timeSinceLastExecution >= wait) {\n await this.#execute(...args)\n return this.store.state.lastResult\n } else {\n return new Promise((resolve) => {\n this.#resolvePreviousPromise = resolve\n // Clear any existing timeout to ensure we use the latest arguments\n this.#clearTimeout()\n\n // Set up trailing execution if enabled\n if (this.options.trailing) {\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(async () => {\n if (this.store.state.lastArgs !== undefined) {\n await this.#execute(...this.store.state.lastArgs)\n }\n this.#resolvePreviousPromise = null\n resolve(this.store.state.lastResult)\n }, timeoutDuration)\n }\n })\n }\n }\n\n #execute = async (\n ...args: Parameters<TFn>\n ): Promise<ReturnType<TFn> | undefined> => {\n if (!this.#getEnabled() || this.store.state.isExecuting) return undefined\n this.#abortController = new AbortController()\n try {\n this.#setState({ isExecuting: true })\n const result = await this.fn(...args) // EXECUTE!\n this.#setState({\n lastResult: result,\n successCount: this.store.state.successCount + 1,\n })\n this.options.onSuccess?.(result, this)\n } catch (error) {\n this.#setState({\n errorCount: this.store.state.errorCount + 1,\n })\n this.options.onError?.(error, this)\n if (this.options.throwOnError) {\n throw error\n } else {\n console.error(error)\n }\n } finally {\n const lastExecutionTime = Date.now()\n const nextExecutionTime = lastExecutionTime + this.#getWait()\n this.#setState({\n isExecuting: false,\n isPending: false,\n settleCount: this.store.state.settleCount + 1,\n lastExecutionTime,\n nextExecutionTime,\n })\n this.#abortController = null\n this.options.onSettled?.(this)\n }\n return this.store.state.lastResult\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.#abortExecution() // abort any current execution\n this.#clearTimeout() // clear any existing timeout\n this.#execute(...this.store.state.lastArgs)\n }\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 #cancelPendingExecution = (): void => {\n this.#clearTimeout()\n if (this.#resolvePreviousPromise) {\n this.#resolvePreviousPromise(this.store.state.lastResult)\n this.#resolvePreviousPromise = null\n }\n this.#setState({\n isPending: false,\n isExecuting: false,\n lastArgs: undefined,\n })\n }\n\n #abortExecution = (): void => {\n if (this.#abortController) {\n this.#abortController.abort()\n this.#abortController = null\n }\n }\n\n /**\n * Cancels any pending execution or aborts any execution in progress\n */\n cancel = (): void => {\n this.#cancelPendingExecution()\n this.#abortExecution()\n }\n\n /**\n * Resets the debouncer state to its default values\n */\n reset = (): void => {\n this.#setState(getDefaultAsyncThrottlerState<TFn>())\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 * Unlike the non-async Throttler, this async version supports returning values from the throttled function,\n * making it ideal for API calls and other async operations where you want the result of the `maybeExecute` call\n * instead of setting the result on a state variable from within the throttled function.\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"],"names":[],"mappings":"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"} |
+76
-51
@@ -0,1 +1,37 @@ | ||
| import { Store } from '@tanstack/store'; | ||
| import { OptionalKeys } from './types.js'; | ||
| export interface BatcherState<TValue> { | ||
| /** | ||
| * Number of batch executions that have been completed | ||
| */ | ||
| executionCount: number; | ||
| /** | ||
| * Whether the batcher has no items to process (items array is empty) | ||
| */ | ||
| isEmpty: boolean; | ||
| /** | ||
| * Whether the batcher is waiting for the timeout to trigger batch processing | ||
| */ | ||
| isPending: boolean; | ||
| /** | ||
| * Whether the batcher is active and will process items automatically | ||
| */ | ||
| isRunning: boolean; | ||
| /** | ||
| * Total number of items that have been processed across all batches | ||
| */ | ||
| totalItemsProcessed: number; | ||
| /** | ||
| * Array of items currently queued for batch processing | ||
| */ | ||
| items: Array<TValue>; | ||
| /** | ||
| * Number of items currently in the batch queue | ||
| */ | ||
| size: number; | ||
| /** | ||
| * Current processing status - 'idle' when not processing, 'pending' when waiting for timeout | ||
| */ | ||
| status: 'idle' | 'pending'; | ||
| } | ||
| /** | ||
@@ -11,2 +47,6 @@ * Options for configuring a Batcher instance | ||
| /** | ||
| * Initial state for the batcher | ||
| */ | ||
| initialState?: Partial<BatcherState<TValue>>; | ||
| /** | ||
| * Maximum number of items in a batch | ||
@@ -21,6 +61,2 @@ * @default Infinity | ||
| /** | ||
| * Callback fired when the batcher's running state changes | ||
| */ | ||
| onIsRunningChange?: (batcher: Batcher<TValue>) => void; | ||
| /** | ||
| * Callback fired after items are added to the batcher | ||
@@ -40,4 +76,5 @@ */ | ||
| */ | ||
| wait?: number; | ||
| wait?: number | ((batcher: Batcher<TValue>) => number); | ||
| } | ||
| type BatcherOptionsWithOptionalCallbacks<TValue> = OptionalKeys<Required<BatcherOptions<TValue>>, 'initialState' | 'onExecute' | 'onItemsChange'>; | ||
| /** | ||
@@ -54,2 +91,11 @@ * A class that collects items and processes them in batches. | ||
| * | ||
| * State Management: | ||
| * - Uses TanStack Store for reactive state management | ||
| * - Use `initialState` to provide initial state values when creating the batcher | ||
| * - Use `onExecute` callback to react to batch execution and implement custom logic | ||
| * - Use `onItemsChange` callback to react to items being added or removed from the batcher | ||
| * - The state includes batch execution count, total items processed, items, and running status | ||
| * - State can be accessed via `batcher.store.state` when using the class directly | ||
| * - When using framework adapters (React/Solid), state is accessed from `batcher.state` | ||
| * | ||
| * @example | ||
@@ -62,3 +108,3 @@ * ```ts | ||
| * wait: 2000, | ||
| * onExecuteBatch: (items) => console.log('Batch executed:', items) | ||
| * onExecute: (batcher) => console.log('Batch executed:', batcher.peekAllItems()) | ||
| * } | ||
@@ -71,13 +117,10 @@ * ); | ||
| * // the batch will be processed | ||
| * // batcher.execute() // manually trigger a batch | ||
| * // batcher.flush() // manually trigger a batch | ||
| * ``` | ||
| */ | ||
| export declare class Batcher<TValue> { | ||
| #private; | ||
| private fn; | ||
| private _options; | ||
| private _batchExecutionCount; | ||
| private _itemExecutionCount; | ||
| private _items; | ||
| private _running; | ||
| private _timeoutId; | ||
| readonly store: Store<Readonly<BatcherState<TValue>>>; | ||
| options: BatcherOptionsWithOptionalCallbacks<TValue>; | ||
| constructor(fn: (items: Array<TValue>) => void, initialOptions: BatcherOptions<TValue>); | ||
@@ -87,54 +130,32 @@ /** | ||
| */ | ||
| setOptions(newOptions: Partial<BatcherOptions<TValue>>): void; | ||
| setOptions: (newOptions: Partial<BatcherOptions<TValue>>) => void; | ||
| /** | ||
| * Returns the current batcher options | ||
| */ | ||
| getOptions(): BatcherOptions<TValue>; | ||
| /** | ||
| * Adds an item to the batcher | ||
| * If the batch size is reached, timeout occurs, or shouldProcess returns true, the batch will be processed | ||
| */ | ||
| addItem(item: TValue): void; | ||
| addItem: (item: TValue) => void; | ||
| /** | ||
| * Processes the current batch of items. | ||
| * This method will automatically be triggered if the batcher is running and any of these conditions are met: | ||
| * - The number of items reaches batchSize | ||
| * - The wait duration has elapsed | ||
| * - The getShouldExecute function returns true upon adding an item | ||
| * | ||
| * You can also call this method manually to process the current batch at any time. | ||
| * Processes the current batch of items immediately | ||
| */ | ||
| execute(): void; | ||
| flush: () => void; | ||
| /** | ||
| * Stops the batcher from processing batches | ||
| */ | ||
| stop(): void; | ||
| stop: () => void; | ||
| /** | ||
| * Starts the batcher and processes any pending items | ||
| */ | ||
| start(): void; | ||
| start: () => void; | ||
| /** | ||
| * Returns the current number of items in the batcher | ||
| * Returns a copy of all items in the batcher | ||
| */ | ||
| getSize(): number; | ||
| peekAllItems: () => Array<TValue>; | ||
| /** | ||
| * Returns true if the batcher is empty | ||
| * Removes all items from the batcher | ||
| */ | ||
| getIsEmpty(): boolean; | ||
| clear: () => void; | ||
| /** | ||
| * Returns true if the batcher is running | ||
| * Resets the batcher state to its default values | ||
| */ | ||
| getIsRunning(): boolean; | ||
| /** | ||
| * Returns a copy of all items currently in the batcher | ||
| */ | ||
| peekAllItems(): Array<TValue>; | ||
| /** | ||
| * Returns the number of times batches have been processed | ||
| */ | ||
| getBatchExecutionCount(): number; | ||
| /** | ||
| * Returns the total number of individual items that have been processed | ||
| */ | ||
| getItemExecutionCount(): number; | ||
| reset: () => void; | ||
| } | ||
@@ -146,6 +167,9 @@ /** | ||
| * ```ts | ||
| * const batchItems = batch<number>({ | ||
| * batchSize: 3, | ||
| * processBatch: (items) => console.log('Processing:', items) | ||
| * }); | ||
| * const batchItems = batch<number>( | ||
| * (items) => console.log('Processing:', items), | ||
| * { | ||
| * maxSize: 3, | ||
| * onExecute: (batcher) => console.log('Batch executed') | ||
| * } | ||
| * ); | ||
| * | ||
@@ -158,1 +182,2 @@ * batchItems(1); | ||
| export declare function batch<TValue>(fn: (items: Array<TValue>) => void, options: BatcherOptions<TValue>): (item: TValue) => void; | ||
| export {}; |
+110
-119
@@ -0,1 +1,15 @@ | ||
| import { Store } from "@tanstack/store"; | ||
| import { parseFunctionOrValue } from "./utils.js"; | ||
| function getDefaultBatcherState() { | ||
| return { | ||
| executionCount: 0, | ||
| isEmpty: true, | ||
| isPending: false, | ||
| isRunning: true, | ||
| totalItemsProcessed: 0, | ||
| items: [], | ||
| size: 0, | ||
| status: "idle" | ||
| }; | ||
| } | ||
| const defaultOptions = { | ||
@@ -10,125 +24,102 @@ getShouldExecute: () => false, | ||
| this.fn = fn; | ||
| this._batchExecutionCount = 0; | ||
| this._itemExecutionCount = 0; | ||
| this._items = []; | ||
| this._timeoutId = null; | ||
| this._options = { ...defaultOptions, ...initialOptions }; | ||
| this._running = this._options.started; | ||
| this.store = new Store( | ||
| getDefaultBatcherState() | ||
| ); | ||
| this.#timeoutId = null; | ||
| this.setOptions = (newOptions) => { | ||
| this.options = { ...this.options, ...newOptions }; | ||
| }; | ||
| this.#setState = (newState) => { | ||
| this.store.setState((state) => { | ||
| const combinedState = { | ||
| ...state, | ||
| ...newState | ||
| }; | ||
| const { isPending, items } = combinedState; | ||
| const size = items.length; | ||
| const isEmpty = size === 0; | ||
| return { | ||
| ...combinedState, | ||
| isEmpty, | ||
| size, | ||
| status: isPending ? "pending" : "idle" | ||
| }; | ||
| }); | ||
| }; | ||
| this.#getWait = () => { | ||
| return parseFunctionOrValue(this.options.wait, this); | ||
| }; | ||
| this.addItem = (item) => { | ||
| this.#setState({ | ||
| items: [...this.store.state.items, item], | ||
| isPending: this.options.wait !== Infinity | ||
| }); | ||
| this.options.onItemsChange?.(this); | ||
| const shouldProcess = this.store.state.items.length >= this.options.maxSize || this.options.getShouldExecute(this.store.state.items, this); | ||
| if (shouldProcess) { | ||
| this.#execute(); | ||
| } else if (this.store.state.isRunning && this.options.wait !== Infinity) { | ||
| this.#clearTimeout(); | ||
| this.#timeoutId = setTimeout(() => this.#execute(), this.#getWait()); | ||
| } | ||
| }; | ||
| this.#execute = () => { | ||
| if (this.store.state.items.length === 0) { | ||
| return; | ||
| } | ||
| const batch2 = this.peekAllItems(); | ||
| this.clear(); | ||
| this.options.onItemsChange?.(this); | ||
| this.fn(batch2); | ||
| this.#setState({ | ||
| executionCount: this.store.state.executionCount + 1, | ||
| totalItemsProcessed: this.store.state.totalItemsProcessed + batch2.length | ||
| }); | ||
| this.options.onExecute?.(this); | ||
| }; | ||
| this.flush = () => { | ||
| this.#clearTimeout(); | ||
| this.#execute(); | ||
| }; | ||
| this.stop = () => { | ||
| this.#setState({ isRunning: false }); | ||
| this.#clearTimeout(); | ||
| }; | ||
| this.start = () => { | ||
| this.#setState({ isRunning: true }); | ||
| if (this.store.state.items.length > 0) { | ||
| this.#execute(); | ||
| } | ||
| }; | ||
| this.peekAllItems = () => { | ||
| return [...this.store.state.items]; | ||
| }; | ||
| this.#clearTimeout = () => { | ||
| if (this.#timeoutId) { | ||
| clearTimeout(this.#timeoutId); | ||
| this.#timeoutId = null; | ||
| } | ||
| }; | ||
| this.clear = () => { | ||
| this.#setState({ items: [], isPending: false }); | ||
| }; | ||
| this.reset = () => { | ||
| this.#setState(getDefaultBatcherState()); | ||
| this.options.onItemsChange?.(this); | ||
| }; | ||
| this.options = { | ||
| ...defaultOptions, | ||
| ...initialOptions | ||
| }; | ||
| this.#setState(this.options.initialState ?? {}); | ||
| } | ||
| /** | ||
| * Updates the batcher options | ||
| */ | ||
| setOptions(newOptions) { | ||
| this._options = { ...this._options, ...newOptions }; | ||
| } | ||
| /** | ||
| * Returns the current batcher options | ||
| */ | ||
| getOptions() { | ||
| return this._options; | ||
| } | ||
| /** | ||
| * Adds an item to the batcher | ||
| * If the batch size is reached, timeout occurs, or shouldProcess returns true, the batch will be processed | ||
| */ | ||
| addItem(item) { | ||
| var _a, _b; | ||
| this._items.push(item); | ||
| (_b = (_a = this._options).onItemsChange) == null ? void 0 : _b.call(_a, this); | ||
| const shouldProcess = this._items.length >= this._options.maxSize || this._options.getShouldExecute(this._items, this); | ||
| if (shouldProcess) { | ||
| this.execute(); | ||
| } else if (this._running && !this._timeoutId && this._options.wait !== Infinity) { | ||
| this._timeoutId = setTimeout(() => this.execute(), this._options.wait); | ||
| } | ||
| } | ||
| /** | ||
| * Processes the current batch of items. | ||
| * This method will automatically be triggered if the batcher is running and any of these conditions are met: | ||
| * - The number of items reaches batchSize | ||
| * - The wait duration has elapsed | ||
| * - The getShouldExecute function returns true upon adding an item | ||
| * | ||
| * You can also call this method manually to process the current batch at any time. | ||
| */ | ||
| execute() { | ||
| var _a, _b, _c, _d; | ||
| if (this._timeoutId) { | ||
| clearTimeout(this._timeoutId); | ||
| this._timeoutId = null; | ||
| } | ||
| if (this._items.length === 0) { | ||
| return; | ||
| } | ||
| const batch2 = this.peekAllItems(); | ||
| this._items = []; | ||
| (_b = (_a = this._options).onItemsChange) == null ? void 0 : _b.call(_a, this); | ||
| this.fn(batch2); | ||
| this._batchExecutionCount++; | ||
| this._itemExecutionCount += batch2.length; | ||
| (_d = (_c = this._options).onExecute) == null ? void 0 : _d.call(_c, this); | ||
| } | ||
| /** | ||
| * Stops the batcher from processing batches | ||
| */ | ||
| stop() { | ||
| var _a, _b; | ||
| this._running = false; | ||
| (_b = (_a = this._options).onIsRunningChange) == null ? void 0 : _b.call(_a, this); | ||
| if (this._timeoutId) { | ||
| clearTimeout(this._timeoutId); | ||
| this._timeoutId = null; | ||
| } | ||
| } | ||
| /** | ||
| * Starts the batcher and processes any pending items | ||
| */ | ||
| start() { | ||
| var _a, _b; | ||
| this._running = true; | ||
| (_b = (_a = this._options).onIsRunningChange) == null ? void 0 : _b.call(_a, this); | ||
| if (this._items.length > 0 && !this._timeoutId) { | ||
| this._timeoutId = setTimeout(() => this.execute(), this._options.wait); | ||
| } | ||
| } | ||
| /** | ||
| * Returns the current number of items in the batcher | ||
| */ | ||
| getSize() { | ||
| return this._items.length; | ||
| } | ||
| /** | ||
| * Returns true if the batcher is empty | ||
| */ | ||
| getIsEmpty() { | ||
| return this._items.length === 0; | ||
| } | ||
| /** | ||
| * Returns true if the batcher is running | ||
| */ | ||
| getIsRunning() { | ||
| return this._running; | ||
| } | ||
| /** | ||
| * Returns a copy of all items currently in the batcher | ||
| */ | ||
| peekAllItems() { | ||
| return [...this._items]; | ||
| } | ||
| /** | ||
| * Returns the number of times batches have been processed | ||
| */ | ||
| getBatchExecutionCount() { | ||
| return this._batchExecutionCount; | ||
| } | ||
| /** | ||
| * Returns the total number of individual items that have been processed | ||
| */ | ||
| getItemExecutionCount() { | ||
| return this._itemExecutionCount; | ||
| } | ||
| #timeoutId; | ||
| #setState; | ||
| #getWait; | ||
| #execute; | ||
| #clearTimeout; | ||
| } | ||
| function batch(fn, options) { | ||
| const batcher = new Batcher(fn, options); | ||
| return batcher.addItem.bind(batcher); | ||
| return batcher.addItem; | ||
| } | ||
@@ -135,0 +126,0 @@ export { |
@@ -1,1 +0,1 @@ | ||
| {"version":3,"file":"batcher.js","sources":["../../src/batcher.ts"],"sourcesContent":["import type { OptionalKeys } from './types'\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 * 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?: (batcher: Batcher<TValue>) => void\n /**\n * Callback fired when the batcher's running state changes\n */\n onIsRunningChange?: (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\n}\n\ntype BatcherOptionsWithOptionalCallbacks<TValue> = OptionalKeys<\n Required<BatcherOptions<TValue>>,\n 'onExecute' | 'onItemsChange' | 'onIsRunningChange'\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 *\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 * @example\n * ```ts\n * const batcher = new Batcher<number>(\n * (items) => console.log('Processing batch:', items),\n * {\n * maxSize: 5,\n * wait: 2000,\n * onExecuteBatch: (items) => console.log('Batch executed:', items)\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.execute() // manually trigger a batch\n * ```\n */\nexport class Batcher<TValue> {\n private _options: BatcherOptionsWithOptionalCallbacks<TValue>\n private _batchExecutionCount = 0\n private _itemExecutionCount = 0\n private _items: Array<TValue> = []\n private _running: boolean\n private _timeoutId: NodeJS.Timeout | null = null\n\n constructor(\n private fn: (items: Array<TValue>) => void,\n initialOptions: BatcherOptions<TValue>,\n ) {\n this._options = { ...defaultOptions, ...initialOptions }\n this._running = this._options.started\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 /**\n * Returns the current batcher options\n */\n getOptions(): BatcherOptions<TValue> {\n return this._options\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._items.push(item)\n this._options.onItemsChange?.(this)\n\n const shouldProcess =\n this._items.length >= this._options.maxSize ||\n this._options.getShouldExecute(this._items, this)\n\n if (shouldProcess) {\n this.execute()\n } else if (\n this._running &&\n !this._timeoutId &&\n this._options.wait !== Infinity\n ) {\n this._timeoutId = setTimeout(() => this.execute(), this._options.wait)\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._timeoutId) {\n clearTimeout(this._timeoutId)\n this._timeoutId = null\n }\n\n if (this._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._items = [] // 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)\n this._batchExecutionCount++\n this._itemExecutionCount += batch.length\n this._options.onExecute?.(this)\n }\n\n /**\n * Stops the batcher from processing batches\n */\n stop(): void {\n this._running = false\n this._options.onIsRunningChange?.(this)\n if (this._timeoutId) {\n clearTimeout(this._timeoutId)\n this._timeoutId = null\n }\n }\n\n /**\n * Starts the batcher and processes any pending items\n */\n start(): void {\n this._running = true\n this._options.onIsRunningChange?.(this)\n if (this._items.length > 0 && !this._timeoutId) {\n this._timeoutId = setTimeout(() => this.execute(), this._options.wait)\n }\n }\n\n /**\n * Returns the current number of items in the batcher\n */\n getSize(): number {\n return this._items.length\n }\n\n /**\n * Returns true if the batcher is empty\n */\n getIsEmpty(): boolean {\n return this._items.length === 0\n }\n\n /**\n * Returns true if the batcher is running\n */\n getIsRunning(): boolean {\n return this._running\n }\n\n /**\n * Returns a copy of all items currently in the batcher\n */\n peekAllItems(): Array<TValue> {\n return [...this._items]\n }\n\n /**\n * Returns the number of times batches have been processed\n */\n getBatchExecutionCount(): number {\n return this._batchExecutionCount\n }\n\n /**\n * Returns the total number of individual items that have been processed\n */\n getItemExecutionCount(): number {\n return this._itemExecutionCount\n }\n}\n\n/**\n * Creates a batcher that processes items in batches\n *\n * @example\n * ```ts\n * const batchItems = batch<number>({\n * batchSize: 3,\n * processBatch: (items) => console.log('Processing:', items)\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.bind(batcher)\n}\n"],"names":["batch"],"mappings":"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"} | ||
| {"version":3,"file":"batcher.js","sources":["../../src/batcher.ts"],"sourcesContent":["import { Store } from '@tanstack/store'\nimport { parseFunctionOrValue } from './utils'\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 * Whether the batcher is active and will process items automatically\n */\n isRunning: boolean\n /**\n * Total number of items that have been processed across all batches\n */\n totalItemsProcessed: number\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\nfunction getDefaultBatcherState<TValue>(): BatcherState<TValue> {\n return {\n executionCount: 0,\n isEmpty: true,\n isPending: false,\n isRunning: true,\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 * 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?: (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'\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 *\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: (batcher) => console.log('Batch executed:', batcher.peekAllItems())\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 options: BatcherOptionsWithOptionalCallbacks<TValue>\n #timeoutId: NodeJS.Timeout | null = null\n\n constructor(\n private fn: (items: Array<TValue>) => void,\n initialOptions: BatcherOptions<TValue>,\n ) {\n this.options = {\n ...defaultOptions,\n ...initialOptions,\n }\n this.#setState(this.options.initialState ?? {})\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 }\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.store.state.isRunning && 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?.(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 * Stops the batcher from processing batches\n */\n stop = (): void => {\n this.#setState({ isRunning: false })\n this.#clearTimeout()\n }\n\n /**\n * Starts the batcher and processes any pending items\n */\n start = (): void => {\n this.#setState({ isRunning: true })\n if (this.store.state.items.length > 0) {\n this.#execute()\n }\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 * 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 * @example\n * ```ts\n * const batchItems = batch<number>(\n * (items) => console.log('Processing:', items),\n * {\n * maxSize: 3,\n * onExecute: (batcher) => console.log('Batch executed')\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"],"names":["batch"],"mappings":"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"} |
+54
-26
@@ -0,2 +1,25 @@ | ||
| import { Store } from '@tanstack/store'; | ||
| import { AnyFunction } from './types.js'; | ||
| export interface DebouncerState<TFn extends AnyFunction> { | ||
| /** | ||
| * Whether the debouncer can execute on the leading edge of the timeout | ||
| */ | ||
| canLeadingExecute: boolean; | ||
| /** | ||
| * Number of function executions that have been completed | ||
| */ | ||
| executionCount: number; | ||
| /** | ||
| * Whether the debouncer is waiting for the timeout to trigger execution | ||
| */ | ||
| isPending: boolean; | ||
| /** | ||
| * The arguments from the most recent call to maybeExecute | ||
| */ | ||
| lastArgs: Parameters<TFn> | undefined; | ||
| /** | ||
| * Current execution status - 'idle' when not active, 'pending' when waiting for timeout | ||
| */ | ||
| status: 'disabled' | 'idle' | 'pending'; | ||
| } | ||
| /** | ||
@@ -13,2 +36,6 @@ * Options for configuring a debounced function | ||
| /** | ||
| * Initial state for the debouncer | ||
| */ | ||
| initialState?: Partial<DebouncerState<TFn>>; | ||
| /** | ||
| * Whether to execute on the leading edge of the timeout. | ||
@@ -46,2 +73,10 @@ * The first call will execute immediately and the rest will wait the delay. | ||
| * | ||
| * State Management: | ||
| * - Uses TanStack Store for reactive state management | ||
| * - Use `initialState` to provide initial state values when creating the debouncer | ||
| * - Use `onExecute` callback to react to function execution and implement custom logic | ||
| * - The state includes canLeadingExecute, execution count, and isPending status | ||
| * - State can be accessed via `debouncer.store.state` when using the class directly | ||
| * - When using framework adapters (React/Solid), state is accessed from `debouncer.state` | ||
| * | ||
| * @example | ||
@@ -60,8 +95,6 @@ * ```ts | ||
| export declare class Debouncer<TFn extends AnyFunction> { | ||
| #private; | ||
| private fn; | ||
| private _canLeadingExecute; | ||
| private _executionCount; | ||
| private _isPending; | ||
| private _options; | ||
| private _timeoutId; | ||
| readonly store: Store<Readonly<DebouncerState<TFn>>>; | ||
| options: DebouncerOptions<TFn>; | ||
| constructor(fn: TFn, initialOptions: DebouncerOptions<TFn>); | ||
@@ -71,33 +104,20 @@ /** | ||
| */ | ||
| setOptions(newOptions: Partial<DebouncerOptions<TFn>>): void; | ||
| setOptions: (newOptions: Partial<DebouncerOptions<TFn>>) => void; | ||
| /** | ||
| * Returns the current debouncer options | ||
| */ | ||
| getOptions(): Required<DebouncerOptions<TFn>>; | ||
| /** | ||
| * Returns the current enabled state of the debouncer | ||
| */ | ||
| getEnabled(): boolean; | ||
| /** | ||
| * Returns the current wait time in milliseconds | ||
| */ | ||
| getWait(): number; | ||
| /** | ||
| * Attempts to execute the debounced function | ||
| * If a call is already in progress, it will be queued | ||
| */ | ||
| maybeExecute(...args: Parameters<TFn>): void; | ||
| private execute; | ||
| maybeExecute: (...args: Parameters<TFn>) => void; | ||
| /** | ||
| * Cancels any pending execution | ||
| * Processes the current pending execution immediately | ||
| */ | ||
| cancel(): void; | ||
| flush: () => void; | ||
| /** | ||
| * Returns the number of times the function has been executed | ||
| * Cancels any pending execution | ||
| */ | ||
| getExecutionCount(): number; | ||
| cancel: () => void; | ||
| /** | ||
| * Returns `true` if debouncing | ||
| * Resets the debouncer state to its default values | ||
| */ | ||
| getIsPending(): boolean; | ||
| reset: () => void; | ||
| } | ||
@@ -114,2 +134,10 @@ /** | ||
| * | ||
| * State Management: | ||
| * - Uses TanStack Store for reactive state management | ||
| * - Use `initialState` to provide initial state values when creating the debouncer | ||
| * - Use `onExecute` callback to react to function execution and implement custom logic | ||
| * - The state includes canLeadingExecute, execution count, and isPending status | ||
| * - State can be accessed via the underlying Debouncer instance's `store.state` property | ||
| * - When using framework adapters (React/Solid), state is accessed from the hook's state property | ||
| * | ||
| * @example | ||
@@ -116,0 +144,0 @@ * ```ts |
+97
-85
@@ -0,7 +1,15 @@ | ||
| import { Store } from "@tanstack/store"; | ||
| import { parseFunctionOrValue } from "./utils.js"; | ||
| function getDefaultDebouncerState() { | ||
| return structuredClone({ | ||
| canLeadingExecute: true, | ||
| executionCount: 0, | ||
| isPending: false, | ||
| lastArgs: void 0, | ||
| status: "idle" | ||
| }); | ||
| } | ||
| const defaultOptions = { | ||
| enabled: true, | ||
| leading: false, | ||
| onExecute: () => { | ||
| }, | ||
| trailing: true, | ||
@@ -13,92 +21,96 @@ wait: 0 | ||
| this.fn = fn; | ||
| this._canLeadingExecute = true; | ||
| this._executionCount = 0; | ||
| this._isPending = false; | ||
| this._options = { | ||
| this.store = new Store( | ||
| getDefaultDebouncerState() | ||
| ); | ||
| this.setOptions = (newOptions) => { | ||
| this.options = { ...this.options, ...newOptions }; | ||
| if (!this.#getEnabled()) { | ||
| this.cancel(); | ||
| } | ||
| }; | ||
| this.#setState = (newState) => { | ||
| this.store.setState((state) => { | ||
| const combinedState = { | ||
| ...state, | ||
| ...newState | ||
| }; | ||
| const { isPending } = combinedState; | ||
| return { | ||
| ...combinedState, | ||
| status: !this.#getEnabled() ? "disabled" : isPending ? "pending" : "idle" | ||
| }; | ||
| }); | ||
| }; | ||
| this.#getEnabled = () => { | ||
| return !!parseFunctionOrValue(this.options.enabled, this); | ||
| }; | ||
| this.#getWait = () => { | ||
| return parseFunctionOrValue(this.options.wait, this); | ||
| }; | ||
| this.maybeExecute = (...args) => { | ||
| if (!this.#getEnabled()) return void 0; | ||
| let _didLeadingExecute = false; | ||
| if (this.options.leading && this.store.state.canLeadingExecute) { | ||
| this.#setState({ canLeadingExecute: false }); | ||
| _didLeadingExecute = true; | ||
| this.#execute(...args); | ||
| } | ||
| if (this.options.trailing) { | ||
| this.#setState({ isPending: true, lastArgs: args }); | ||
| } | ||
| if (this.#timeoutId) clearTimeout(this.#timeoutId); | ||
| this.#timeoutId = setTimeout(() => { | ||
| this.#setState({ canLeadingExecute: true }); | ||
| if (this.options.trailing && !_didLeadingExecute) { | ||
| this.#execute(...args); | ||
| } | ||
| }, this.#getWait()); | ||
| }; | ||
| this.#execute = (...args) => { | ||
| if (!this.#getEnabled()) return void 0; | ||
| this.fn(...args); | ||
| this.#setState({ | ||
| isPending: false, | ||
| executionCount: this.store.state.executionCount + 1 | ||
| }); | ||
| this.options.onExecute?.(this); | ||
| }; | ||
| this.flush = () => { | ||
| if (this.store.state.isPending && this.store.state.lastArgs) { | ||
| this.#clearTimeout(); | ||
| this.#execute(...this.store.state.lastArgs); | ||
| } | ||
| }; | ||
| this.#clearTimeout = () => { | ||
| if (this.#timeoutId) { | ||
| clearTimeout(this.#timeoutId); | ||
| this.#timeoutId = void 0; | ||
| } | ||
| }; | ||
| this.cancel = () => { | ||
| this.#clearTimeout(); | ||
| this.#setState({ | ||
| canLeadingExecute: true, | ||
| isPending: false | ||
| }); | ||
| }; | ||
| this.reset = () => { | ||
| this.#setState(getDefaultDebouncerState()); | ||
| }; | ||
| this.options = { | ||
| ...defaultOptions, | ||
| ...initialOptions | ||
| }; | ||
| this.#setState(this.options.initialState ?? {}); | ||
| } | ||
| /** | ||
| * Updates the debouncer options | ||
| */ | ||
| setOptions(newOptions) { | ||
| this._options = { ...this._options, ...newOptions }; | ||
| if (!this._options.enabled) { | ||
| this._isPending = false; | ||
| } | ||
| } | ||
| /** | ||
| * Returns the current debouncer options | ||
| */ | ||
| getOptions() { | ||
| return this._options; | ||
| } | ||
| /** | ||
| * Returns the current enabled state of the debouncer | ||
| */ | ||
| getEnabled() { | ||
| return parseFunctionOrValue(this._options.enabled, this); | ||
| } | ||
| /** | ||
| * Returns the current wait time in milliseconds | ||
| */ | ||
| getWait() { | ||
| return parseFunctionOrValue(this._options.wait, this); | ||
| } | ||
| /** | ||
| * Attempts to execute the debounced function | ||
| * If a call is already in progress, it will be queued | ||
| */ | ||
| maybeExecute(...args) { | ||
| let _didLeadingExecute = false; | ||
| if (this._options.leading && this._canLeadingExecute) { | ||
| this._canLeadingExecute = false; | ||
| _didLeadingExecute = true; | ||
| this.execute(...args); | ||
| } | ||
| if (this._options.trailing) { | ||
| this._isPending = true; | ||
| } | ||
| if (this._timeoutId) clearTimeout(this._timeoutId); | ||
| this._timeoutId = setTimeout(() => { | ||
| this._canLeadingExecute = true; | ||
| if (this._options.trailing && !_didLeadingExecute) { | ||
| this.execute(...args); | ||
| } | ||
| }, this.getWait()); | ||
| } | ||
| execute(...args) { | ||
| if (!this.getEnabled()) return void 0; | ||
| this.fn(...args); | ||
| this._isPending = false; | ||
| this._executionCount++; | ||
| this._options.onExecute(this); | ||
| } | ||
| /** | ||
| * Cancels any pending execution | ||
| */ | ||
| cancel() { | ||
| if (this._timeoutId) { | ||
| clearTimeout(this._timeoutId); | ||
| this._canLeadingExecute = true; | ||
| this._isPending = false; | ||
| } | ||
| } | ||
| /** | ||
| * Returns the number of times the function has been executed | ||
| */ | ||
| getExecutionCount() { | ||
| return this._executionCount; | ||
| } | ||
| /** | ||
| * Returns `true` if debouncing | ||
| */ | ||
| getIsPending() { | ||
| return this.getEnabled() && this._isPending; | ||
| } | ||
| #timeoutId; | ||
| #setState; | ||
| #getEnabled; | ||
| #getWait; | ||
| #execute; | ||
| #clearTimeout; | ||
| } | ||
| function debounce(fn, initialOptions) { | ||
| const debouncer = new Debouncer(fn, initialOptions); | ||
| return debouncer.maybeExecute.bind(debouncer); | ||
| return debouncer.maybeExecute; | ||
| } | ||
@@ -105,0 +117,0 @@ export { |
@@ -1,1 +0,1 @@ | ||
| {"version":3,"file":"debouncer.js","sources":["../../src/debouncer.ts"],"sourcesContent":["import { parseFunctionOrValue } from './utils'\nimport type { AnyFunction } from './types'\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 * 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?: (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\nconst defaultOptions: Required<DebouncerOptions<any>> = {\n enabled: true,\n leading: false,\n onExecute: () => {},\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 *\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 * @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 private _canLeadingExecute = true\n private _executionCount = 0\n private _isPending = false\n private _options: Required<DebouncerOptions<TFn>>\n private _timeoutId: NodeJS.Timeout | undefined\n\n constructor(\n private fn: TFn,\n initialOptions: DebouncerOptions<TFn>,\n ) {\n this._options = {\n ...defaultOptions,\n ...initialOptions,\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 // End the pending state if the debouncer is disabled\n if (!this._options.enabled) {\n this._isPending = false\n }\n }\n\n /**\n * Returns the current debouncer options\n */\n getOptions(): Required<DebouncerOptions<TFn>> {\n return this._options\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 let _didLeadingExecute = false\n\n // Handle leading execution\n if (this._options.leading && this._canLeadingExecute) {\n this._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._isPending = true\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._canLeadingExecute = true\n if (this._options.trailing && !_didLeadingExecute) {\n this.execute(...args)\n }\n }, this.getWait())\n }\n\n private execute(...args: Parameters<TFn>): void {\n if (!this.getEnabled()) return undefined\n this.fn(...args) // EXECUTE!\n this._isPending = false\n this._executionCount++\n this._options.onExecute(this)\n }\n\n /**\n * Cancels any pending execution\n */\n cancel(): void {\n if (this._timeoutId) {\n clearTimeout(this._timeoutId)\n this._canLeadingExecute = true\n this._isPending = false\n }\n }\n\n /**\n * Returns the number of times the function has been executed\n */\n getExecutionCount(): number {\n return this._executionCount\n }\n\n /**\n * Returns `true` if debouncing\n */\n getIsPending(): boolean {\n return this.getEnabled() && this._isPending\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 the the simple function wrapper implementation pulled from the Debouncer class. If you need\n * more control over the debouncing behavior, use the Debouncer class directly.\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 * @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.bind(debouncer)\n}\n"],"names":[],"mappings":";AAoCA,MAAM,iBAAkD;AAAA,EACtD,SAAS;AAAA,EACT,SAAS;AAAA,EACT,WAAW,MAAM;AAAA,EAAC;AAAA,EAClB,UAAU;AAAA,EACV,MAAM;AACR;AAyBO,MAAM,UAAmC;AAAA,EAO9C,YACU,IACR,gBACA;AAFQ,SAAA,KAAA;AAPV,SAAQ,qBAAqB;AAC7B,SAAQ,kBAAkB;AAC1B,SAAQ,aAAa;AAQnB,SAAK,WAAW;AAAA,MACd,GAAG;AAAA,MACH,GAAG;AAAA,IACL;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA,EAMF,WAAW,YAAkD;AAC3D,SAAK,WAAW,EAAE,GAAG,KAAK,UAAU,GAAG,WAAW;AAG9C,QAAA,CAAC,KAAK,SAAS,SAAS;AAC1B,WAAK,aAAa;AAAA,IAAA;AAAA,EACpB;AAAA;AAAA;AAAA;AAAA,EAMF,aAA8C;AAC5C,WAAO,KAAK;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA,EAMd,aAAsB;AACpB,WAAO,qBAAqB,KAAK,SAAS,SAAS,IAAI;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA,EAMzD,UAAkB;AAChB,WAAO,qBAAqB,KAAK,SAAS,MAAM,IAAI;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOtD,gBAAgB,MAA6B;AAC3C,QAAI,qBAAqB;AAGzB,QAAI,KAAK,SAAS,WAAW,KAAK,oBAAoB;AACpD,WAAK,qBAAqB;AACL,2BAAA;AAChB,WAAA,QAAQ,GAAG,IAAI;AAAA,IAAA;AAIlB,QAAA,KAAK,SAAS,UAAU;AAC1B,WAAK,aAAa;AAAA,IAAA;AAIpB,QAAI,KAAK,WAAyB,cAAA,KAAK,UAAU;AAG5C,SAAA,aAAa,WAAW,MAAM;AACjC,WAAK,qBAAqB;AAC1B,UAAI,KAAK,SAAS,YAAY,CAAC,oBAAoB;AAC5C,aAAA,QAAQ,GAAG,IAAI;AAAA,MAAA;AAAA,IACtB,GACC,KAAK,SAAS;AAAA,EAAA;AAAA,EAGX,WAAW,MAA6B;AAC9C,QAAI,CAAC,KAAK,WAAW,EAAU,QAAA;AAC1B,SAAA,GAAG,GAAG,IAAI;AACf,SAAK,aAAa;AACb,SAAA;AACA,SAAA,SAAS,UAAU,IAAI;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA,EAM9B,SAAe;AACb,QAAI,KAAK,YAAY;AACnB,mBAAa,KAAK,UAAU;AAC5B,WAAK,qBAAqB;AAC1B,WAAK,aAAa;AAAA,IAAA;AAAA,EACpB;AAAA;AAAA;AAAA;AAAA,EAMF,oBAA4B;AAC1B,WAAO,KAAK;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA,EAMd,eAAwB;AACf,WAAA,KAAK,gBAAgB,KAAK;AAAA,EAAA;AAErC;AAsBgB,SAAA,SACd,IACA,gBACoC;AACpC,QAAM,YAAY,IAAI,UAAU,IAAI,cAAc;AAC3C,SAAA,UAAU,aAAa,KAAK,SAAS;AAC9C;"} | ||
| {"version":3,"file":"debouncer.js","sources":["../../src/debouncer.ts"],"sourcesContent":["import { Store } from '@tanstack/store'\nimport { parseFunctionOrValue } from './utils'\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 * 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 structuredClone({\n canLeadingExecute: true,\n executionCount: 0,\n isPending: false,\n lastArgs: undefined,\n status: 'idle',\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 * 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?: (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\nconst defaultOptions: Omit<\n Required<DebouncerOptions<any>>,\n 'initialState' | 'onExecute'\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 *\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 options: DebouncerOptions<TFn>\n #timeoutId: NodeJS.Timeout | undefined\n\n constructor(\n private fn: TFn,\n initialOptions: DebouncerOptions<TFn>,\n ) {\n this.options = {\n ...defaultOptions,\n ...initialOptions,\n }\n this.#setState(this.options.initialState ?? {})\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 }\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 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 isPending: false,\n executionCount: this.store.state.executionCount + 1,\n })\n this.options.onExecute?.(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 the the simple function wrapper implementation pulled from the Debouncer class. If you need\n * more control over the debouncing behavior, use the Debouncer class directly.\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"],"names":[],"mappings":"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"} |
@@ -0,1 +1,2 @@ | ||
| export * from './async-batcher.js'; | ||
| export * from './async-debouncer.js'; | ||
@@ -6,3 +7,2 @@ export * from './async-queuer.js'; | ||
| export * from './batcher.js'; | ||
| export * from './compare.js'; | ||
| export * from './debouncer.js'; | ||
@@ -9,0 +9,0 @@ export * from './queuer.js'; |
@@ -0,1 +1,2 @@ | ||
| import { AsyncBatcher, asyncBatch } from "./async-batcher.js"; | ||
| import { AsyncDebouncer, asyncDebounce } from "./async-debouncer.js"; | ||
@@ -6,3 +7,2 @@ import { AsyncQueuer, asyncQueue } from "./async-queuer.js"; | ||
| import { Batcher, batch } from "./batcher.js"; | ||
| import { isPlainArray, isPlainObject, replaceEqualDeep, shallowEqualObjects } from "./compare.js"; | ||
| import { Debouncer, debounce } from "./debouncer.js"; | ||
@@ -12,4 +12,5 @@ import { Queuer, queue } from "./queuer.js"; | ||
| import { Throttler, throttle } from "./throttler.js"; | ||
| import { bindInstanceMethods, isFunction, parseFunctionOrValue } from "./utils.js"; | ||
| import { isFunction, parseFunctionOrValue } from "./utils.js"; | ||
| export { | ||
| AsyncBatcher, | ||
| AsyncDebouncer, | ||
@@ -24,2 +25,3 @@ AsyncQueuer, | ||
| Throttler, | ||
| asyncBatch, | ||
| asyncDebounce, | ||
@@ -30,14 +32,9 @@ asyncQueue, | ||
| batch, | ||
| bindInstanceMethods, | ||
| debounce, | ||
| isFunction, | ||
| isPlainArray, | ||
| isPlainObject, | ||
| parseFunctionOrValue, | ||
| queue, | ||
| rateLimit, | ||
| replaceEqualDeep, | ||
| shallowEqualObjects, | ||
| throttle | ||
| }; | ||
| //# sourceMappingURL=index.js.map |
+102
-81
@@ -0,1 +1,52 @@ | ||
| import { Store } from '@tanstack/store'; | ||
| export interface QueuerState<TValue> { | ||
| /** | ||
| * Number of items that have been processed by the queuer | ||
| */ | ||
| executionCount: number; | ||
| /** | ||
| * Number of items that have been removed from the queue due to expiration | ||
| */ | ||
| expirationCount: number; | ||
| /** | ||
| * Whether the queuer has no items to process (items array is empty) | ||
| */ | ||
| isEmpty: boolean; | ||
| /** | ||
| * Whether the queuer has reached its maximum capacity | ||
| */ | ||
| isFull: boolean; | ||
| /** | ||
| * Whether the queuer is not currently processing any items | ||
| */ | ||
| isIdle: boolean; | ||
| /** | ||
| * Whether the queuer is active and will process items automatically | ||
| */ | ||
| isRunning: boolean; | ||
| /** | ||
| * Timestamps when items were added to the queue for expiration tracking | ||
| */ | ||
| itemTimestamps: Array<number>; | ||
| /** | ||
| * Array of items currently waiting to be processed | ||
| */ | ||
| items: Array<TValue>; | ||
| /** | ||
| * Whether the queuer has a pending timeout for processing the next item | ||
| */ | ||
| pendingTick: boolean; | ||
| /** | ||
| * Number of items that have been rejected from being added to the queue | ||
| */ | ||
| rejectionCount: number; | ||
| /** | ||
| * Number of items currently in the queue | ||
| */ | ||
| size: number; | ||
| /** | ||
| * Current processing status - 'idle' when not processing, 'running' when active, 'stopped' when paused | ||
| */ | ||
| status: 'idle' | 'running' | 'stopped'; | ||
| } | ||
| /** | ||
@@ -37,2 +88,6 @@ * Options for configuring a Queuer instance. | ||
| /** | ||
| * Initial state for the queuer | ||
| */ | ||
| initialState?: Partial<QueuerState<TValue>>; | ||
| /** | ||
| * Maximum number of items allowed in the queuer | ||
@@ -50,6 +105,2 @@ */ | ||
| /** | ||
| * Callback fired whenever the queuer's running state changes | ||
| */ | ||
| onIsRunningChange?: (queuer: Queuer<TValue>) => void; | ||
| /** | ||
| * Callback fired whenever an item is added or removed from the queuer | ||
@@ -91,3 +142,3 @@ */ | ||
| * Running behavior: | ||
| * - `start()`: Begins automatically processing items in the queue (defaults to running) | ||
| * - `start()`: Begins automatically processing items in the queue (defaults to isRunning) | ||
| * - `stop()`: Pauses processing but maintains queue state | ||
@@ -121,2 +172,13 @@ * - `wait`: Configurable delay between processing items | ||
| * | ||
| * State Management: | ||
| * - Uses TanStack Store for reactive state management | ||
| * - Use `initialState` to provide initial state values when creating the queuer | ||
| * - Use `onExecute` callback to react to item execution and implement custom logic | ||
| * - Use `onItemsChange` callback to react to items being added or removed from the queue | ||
| * - Use `onExpire` callback to react to items expiring and implement custom logic | ||
| * - Use `onReject` callback to react to items being rejected when the queue is full | ||
| * - The state includes execution count, expiration count, rejection count, and isRunning status | ||
| * - State can be accessed via `queuer.store.state` when using the class directly | ||
| * - When using framework adapters (React/Solid), state is accessed from `queuer.state` | ||
| * | ||
| * Example usage: | ||
@@ -144,12 +206,6 @@ * ```ts | ||
| export declare class Queuer<TValue> { | ||
| #private; | ||
| private fn; | ||
| private _options; | ||
| private _items; | ||
| private _itemTimestamps; | ||
| private _executionCount; | ||
| private _rejectionCount; | ||
| private _expirationCount; | ||
| private _onItemsChanges; | ||
| private _running; | ||
| private _pendingTick; | ||
| readonly store: Store<Readonly<QueuerState<TValue>>>; | ||
| options: QueuerOptions<TValue>; | ||
| constructor(fn: (item: TValue) => void, initialOptions?: QueuerOptions<TValue>); | ||
@@ -159,39 +215,4 @@ /** | ||
| */ | ||
| setOptions(newOptions: Partial<QueuerOptions<TValue>>): void; | ||
| setOptions: (newOptions: Partial<QueuerOptions<TValue>>) => void; | ||
| /** | ||
| * Returns the current queuer options, including defaults and any overrides. | ||
| */ | ||
| getOptions(): Required<QueuerOptions<TValue>>; | ||
| /** | ||
| * Returns the current wait time (in milliseconds) between processing items. | ||
| * If a function is provided, it is called with the queuer instance. | ||
| */ | ||
| getWait(): number; | ||
| /** | ||
| * Processes items in the queue up to the wait interval. Internal use only. | ||
| */ | ||
| private tick; | ||
| /** | ||
| * Checks for expired items in the queue and removes them. Calls onExpire for each expired item. | ||
| * Internal use only. | ||
| */ | ||
| private checkExpiredItems; | ||
| /** | ||
| * Stops processing items in the queue. Does not clear the queue. | ||
| */ | ||
| stop(): void; | ||
| /** | ||
| * Starts processing items in the queue. If already running, does nothing. | ||
| */ | ||
| start(): void; | ||
| /** | ||
| * Removes all pending items from the queue. Does not affect items being processed. | ||
| */ | ||
| clear(): void; | ||
| /** | ||
| * Resets the queuer to its initial state. Optionally repopulates with initial items. | ||
| * Does not affect callbacks or options. | ||
| */ | ||
| reset(withInitialItems?: boolean): void; | ||
| /** | ||
| * Adds an item to the queue. If the queue is full, the item is rejected and onReject is called. | ||
@@ -208,3 +229,3 @@ * Items can be inserted based on priority or at the front/back depending on configuration. | ||
| */ | ||
| addItem(item: TValue, position?: QueuePosition, runOnUpdate?: boolean): boolean; | ||
| addItem: (item: TValue, position?: QueuePosition, runOnItemsChange?: boolean) => boolean; | ||
| /** | ||
@@ -222,3 +243,3 @@ * Removes and returns the next item from the queue without executing the function. | ||
| */ | ||
| getNextItem(position?: QueuePosition): TValue | undefined; | ||
| getNextItem: (position?: QueuePosition) => TValue | undefined; | ||
| /** | ||
@@ -234,4 +255,9 @@ * Removes and returns the next item from the queue and processes it using the provided function. | ||
| */ | ||
| execute(position?: QueuePosition): TValue | undefined; | ||
| execute: (position?: QueuePosition) => TValue | undefined; | ||
| /** | ||
| * Processes a specified number of items to execute immediately with no wait time | ||
| * If no numberOfItems is provided, all items will be processed | ||
| */ | ||
| flush: (numberOfItems?: number, position?: QueuePosition) => void; | ||
| /** | ||
| * Returns the next item in the queue without removing it. | ||
@@ -245,39 +271,23 @@ * | ||
| */ | ||
| peekNextItem(position?: QueuePosition): TValue | undefined; | ||
| peekNextItem: (position?: QueuePosition) => TValue | undefined; | ||
| /** | ||
| * Returns true if the queue is empty (no pending items). | ||
| */ | ||
| getIsEmpty(): boolean; | ||
| /** | ||
| * Returns true if the queue is full (reached maxSize). | ||
| */ | ||
| getIsFull(): boolean; | ||
| /** | ||
| * Returns the number of pending items in the queue. | ||
| */ | ||
| getSize(): number; | ||
| /** | ||
| * Returns a copy of all items in the queue. | ||
| */ | ||
| peekAllItems(): Array<TValue>; | ||
| peekAllItems: () => Array<TValue>; | ||
| /** | ||
| * Returns the number of items that have been processed and removed from the queue. | ||
| * Starts processing items in the queue. If already isRunning, does nothing. | ||
| */ | ||
| getExecutionCount(): number; | ||
| start: () => void; | ||
| /** | ||
| * Returns the number of items that have been rejected from being added to the queue. | ||
| * Stops processing items in the queue. Does not clear the queue. | ||
| */ | ||
| getRejectionCount(): number; | ||
| stop: () => void; | ||
| /** | ||
| * Returns the number of items that have expired and been removed from the queue. | ||
| * Removes all pending items from the queue. Does not affect items being processed. | ||
| */ | ||
| getExpirationCount(): number; | ||
| clear: () => void; | ||
| /** | ||
| * Returns true if the queuer is currently running (processing items). | ||
| * Resets the queuer state to its default values | ||
| */ | ||
| getIsRunning(): boolean; | ||
| /** | ||
| * Returns true if the queuer is running but has no items to process. | ||
| */ | ||
| getIsIdle(): boolean; | ||
| reset: () => void; | ||
| } | ||
@@ -289,5 +299,16 @@ /** | ||
| * This is a simplified wrapper around the Queuer class that only exposes the | ||
| * `addItem` method. The queue is always running and will process items as they are added. | ||
| * `addItem` method. The queue is always isRunning and will process items as they are added. | ||
| * For more control over queue processing, use the Queuer class directly. | ||
| * | ||
| * State Management: | ||
| * - Uses TanStack Store for reactive state management | ||
| * - Use `initialState` to provide initial state values when creating the queuer | ||
| * - Use `onExecute` callback to react to item execution and implement custom logic | ||
| * - Use `onItemsChange` callback to react to items being added or removed from the queue | ||
| * - Use `onExpire` callback to react to items expiring and implement custom logic | ||
| * - Use `onReject` callback to react to items being rejected when the queue is full | ||
| * - The state includes execution count, expiration count, rejection count, and isRunning status | ||
| * - State can be accessed via the underlying Queuer instance's `store.state` property | ||
| * - When using framework adapters (React/Solid), state is accessed from the hook's state property | ||
| * | ||
| * Example usage: | ||
@@ -311,2 +332,2 @@ * ```ts | ||
| */ | ||
| export declare function queue<TValue>(fn: (item: TValue) => void, options: QueuerOptions<TValue>): (item: TValue, position?: QueuePosition, runOnUpdate?: boolean) => boolean; | ||
| export declare function queue<TValue>(fn: (item: TValue) => void, initialOptions: QueuerOptions<TValue>): (item: TValue, position?: QueuePosition, runOnItemsChange?: boolean) => boolean; |
+246
-294
@@ -0,6 +1,23 @@ | ||
| import { Store } from "@tanstack/store"; | ||
| import { parseFunctionOrValue } from "./utils.js"; | ||
| function getDefaultQueuerState() { | ||
| return { | ||
| executionCount: 0, | ||
| expirationCount: 0, | ||
| isEmpty: true, | ||
| isFull: false, | ||
| isIdle: true, | ||
| isRunning: true, | ||
| itemTimestamps: [], | ||
| items: [], | ||
| pendingTick: false, | ||
| rejectionCount: 0, | ||
| size: 0, | ||
| status: "idle" | ||
| }; | ||
| } | ||
| const defaultOptions = { | ||
| addItemsTo: "back", | ||
| getItemsFrom: "front", | ||
| getPriority: (item) => (item == null ? void 0 : item.priority) ?? 0, | ||
| getPriority: (item) => item?.priority ?? 0, | ||
| getIsExpired: () => false, | ||
@@ -10,12 +27,2 @@ expirationDuration: Infinity, | ||
| maxSize: Infinity, | ||
| onExecute: () => { | ||
| }, | ||
| onIsRunningChange: () => { | ||
| }, | ||
| onItemsChange: () => { | ||
| }, | ||
| onReject: () => { | ||
| }, | ||
| onExpire: () => { | ||
| }, | ||
| started: true, | ||
@@ -27,297 +34,242 @@ wait: 0 | ||
| this.fn = fn; | ||
| this._items = []; | ||
| this._itemTimestamps = []; | ||
| this._executionCount = 0; | ||
| this._rejectionCount = 0; | ||
| this._expirationCount = 0; | ||
| this._onItemsChanges = []; | ||
| this._pendingTick = false; | ||
| this._options = { ...defaultOptions, ...initialOptions }; | ||
| this._running = this._options.started; | ||
| for (let i = 0; i < this._options.initialItems.length; i++) { | ||
| const item = this._options.initialItems[i]; | ||
| const isLast = i === this._options.initialItems.length - 1; | ||
| this.addItem(item, this._options.addItemsTo, isLast); | ||
| } | ||
| } | ||
| /** | ||
| * Updates the queuer options. New options are merged with existing options. | ||
| */ | ||
| setOptions(newOptions) { | ||
| this._options = { ...this._options, ...newOptions }; | ||
| } | ||
| /** | ||
| * Returns the current queuer options, including defaults and any overrides. | ||
| */ | ||
| getOptions() { | ||
| return this._options; | ||
| } | ||
| /** | ||
| * Returns the current wait time (in milliseconds) between processing items. | ||
| * If a function is provided, it is called with the queuer instance. | ||
| */ | ||
| getWait() { | ||
| return parseFunctionOrValue(this._options.wait, this); | ||
| } | ||
| /** | ||
| * Processes items in the queue up to the wait interval. Internal use only. | ||
| */ | ||
| tick() { | ||
| if (!this._running) { | ||
| this._pendingTick = false; | ||
| return; | ||
| } | ||
| this.checkExpiredItems(); | ||
| while (!this.getIsEmpty()) { | ||
| const nextItem = this.execute(this._options.getItemsFrom); | ||
| if (nextItem === void 0) { | ||
| break; | ||
| } | ||
| this._onItemsChanges.forEach((cb) => cb(nextItem)); | ||
| const wait = this.getWait(); | ||
| if (wait > 0) { | ||
| setTimeout(() => this.tick(), wait); | ||
| this.store = new Store( | ||
| getDefaultQueuerState() | ||
| ); | ||
| this.#timeoutId = null; | ||
| this.setOptions = (newOptions) => { | ||
| this.options = { ...this.options, ...newOptions }; | ||
| }; | ||
| this.#setState = (newState) => { | ||
| this.store.setState((state) => { | ||
| const combinedState = { | ||
| ...state, | ||
| ...newState | ||
| }; | ||
| const { items, isRunning } = combinedState; | ||
| const size = items.length; | ||
| const isFull = size >= (this.options.maxSize ?? Infinity); | ||
| const isEmpty = size === 0; | ||
| const isIdle = isRunning && isEmpty; | ||
| const status = isIdle ? "idle" : isRunning ? "running" : "stopped"; | ||
| return { | ||
| ...combinedState, | ||
| isEmpty, | ||
| isFull, | ||
| isIdle, | ||
| size, | ||
| status | ||
| }; | ||
| }); | ||
| }; | ||
| this.#getWait = () => { | ||
| return parseFunctionOrValue(this.options.wait ?? 0, this); | ||
| }; | ||
| this.#tick = () => { | ||
| if (!this.store.state.isRunning) { | ||
| this.#setState({ pendingTick: false }); | ||
| return; | ||
| } | ||
| this.tick(); | ||
| } | ||
| this._pendingTick = false; | ||
| } | ||
| /** | ||
| * Checks for expired items in the queue and removes them. Calls onExpire for each expired item. | ||
| * Internal use only. | ||
| */ | ||
| checkExpiredItems() { | ||
| if (this._options.expirationDuration === Infinity && this._options.getIsExpired === defaultOptions.getIsExpired) | ||
| return; | ||
| const now = Date.now(); | ||
| const expiredIndices = []; | ||
| for (let i = 0; i < this._items.length; i++) { | ||
| const timestamp = this._itemTimestamps[i]; | ||
| if (timestamp === void 0) continue; | ||
| const item = this._items[i]; | ||
| if (item === void 0) continue; | ||
| const isExpired = this._options.getIsExpired !== defaultOptions.getIsExpired ? this._options.getIsExpired(item, timestamp) : now - timestamp > this._options.expirationDuration; | ||
| if (isExpired) { | ||
| expiredIndices.push(i); | ||
| this.#setState({ pendingTick: true }); | ||
| this.#checkExpiredItems(); | ||
| while (!this.store.state.isEmpty) { | ||
| const nextItem = this.execute(this.options.getItemsFrom ?? "front"); | ||
| if (nextItem === void 0) { | ||
| break; | ||
| } | ||
| const wait = this.#getWait(); | ||
| if (wait > 0) { | ||
| this.#timeoutId = setTimeout(() => this.#tick(), wait); | ||
| return; | ||
| } | ||
| this.#tick(); | ||
| } | ||
| } | ||
| for (let i = expiredIndices.length - 1; i >= 0; i--) { | ||
| const index = expiredIndices[i]; | ||
| if (index === void 0) continue; | ||
| const expiredItem = this._items[index]; | ||
| if (expiredItem === void 0) continue; | ||
| this._items.splice(index, 1); | ||
| this._itemTimestamps.splice(index, 1); | ||
| this._expirationCount++; | ||
| this._options.onExpire(expiredItem, this); | ||
| } | ||
| if (expiredIndices.length > 0) { | ||
| this._options.onItemsChange(this); | ||
| } | ||
| } | ||
| /** | ||
| * Stops processing items in the queue. Does not clear the queue. | ||
| */ | ||
| stop() { | ||
| this._running = false; | ||
| this._pendingTick = false; | ||
| this._options.onIsRunningChange(this); | ||
| } | ||
| /** | ||
| * Starts processing items in the queue. If already running, does nothing. | ||
| */ | ||
| start() { | ||
| this._running = true; | ||
| if (!this._pendingTick && !this.getIsEmpty()) { | ||
| this._pendingTick = true; | ||
| this.tick(); | ||
| } | ||
| this._options.onIsRunningChange(this); | ||
| } | ||
| /** | ||
| * Removes all pending items from the queue. Does not affect items being processed. | ||
| */ | ||
| clear() { | ||
| this._items = []; | ||
| this._options.onItemsChange(this); | ||
| } | ||
| /** | ||
| * Resets the queuer to its initial state. Optionally repopulates with initial items. | ||
| * Does not affect callbacks or options. | ||
| */ | ||
| reset(withInitialItems) { | ||
| this.clear(); | ||
| this._executionCount = 0; | ||
| if (withInitialItems) { | ||
| this._items = [...this._options.initialItems]; | ||
| } | ||
| this._running = this._options.started; | ||
| } | ||
| /** | ||
| * Adds an item to the queue. If the queue is full, the item is rejected and onReject is called. | ||
| * Items can be inserted based on priority or at the front/back depending on configuration. | ||
| * | ||
| * Returns true if the item was added, false if the queue is full. | ||
| * | ||
| * Example usage: | ||
| * ```ts | ||
| * queuer.addItem('task'); | ||
| * queuer.addItem('task2', 'front'); | ||
| * ``` | ||
| */ | ||
| addItem(item, position = this._options.addItemsTo, runOnUpdate = true) { | ||
| if (this.getIsFull()) { | ||
| this._rejectionCount++; | ||
| this._options.onReject(item, this); | ||
| return false; | ||
| } | ||
| if (this._options.getPriority !== defaultOptions.getPriority) { | ||
| const priority = this._options.getPriority(item); | ||
| const insertIndex = this._items.findIndex( | ||
| (existing) => this._options.getPriority(existing) < priority | ||
| ); | ||
| if (insertIndex === -1) { | ||
| this._items.push(item); | ||
| this._itemTimestamps.push(Date.now()); | ||
| this.#setState({ pendingTick: false }); | ||
| }; | ||
| this.addItem = (item, position = this.options.addItemsTo ?? "back", runOnItemsChange = true) => { | ||
| if (this.store.state.isFull) { | ||
| this.#setState({ | ||
| rejectionCount: this.store.state.rejectionCount + 1 | ||
| }); | ||
| this.options.onReject?.(item, this); | ||
| return false; | ||
| } | ||
| const priority = this.options.getPriority !== defaultOptions.getPriority ? this.options.getPriority(item) : item.priority; | ||
| const items = this.store.state.items; | ||
| const itemTimestamps = this.store.state.itemTimestamps; | ||
| if (priority !== void 0) { | ||
| const insertIndex = items.findIndex((existing) => { | ||
| const existingPriority = this.options.getPriority !== defaultOptions.getPriority ? this.options.getPriority(existing) : existing.priority; | ||
| return existingPriority < priority; | ||
| }); | ||
| if (insertIndex === -1) { | ||
| items.push(item); | ||
| itemTimestamps.push(Date.now()); | ||
| } else { | ||
| items.splice(insertIndex, 0, item); | ||
| itemTimestamps.splice(insertIndex, 0, Date.now()); | ||
| } | ||
| } else { | ||
| this._items.splice(insertIndex, 0, item); | ||
| this._itemTimestamps.splice(insertIndex, 0, Date.now()); | ||
| if (position === "front") { | ||
| items.unshift(item); | ||
| itemTimestamps.unshift(Date.now()); | ||
| } else { | ||
| items.push(item); | ||
| itemTimestamps.push(Date.now()); | ||
| } | ||
| } | ||
| } else { | ||
| this.#setState({ | ||
| items, | ||
| itemTimestamps | ||
| }); | ||
| if (runOnItemsChange) { | ||
| this.options.onItemsChange?.(this); | ||
| } | ||
| if (this.store.state.isRunning && !this.store.state.pendingTick) { | ||
| this.#setState({ pendingTick: true }); | ||
| this.#tick(); | ||
| } | ||
| return true; | ||
| }; | ||
| this.getNextItem = (position = this.options.getItemsFrom ?? "front") => { | ||
| const { items, itemTimestamps } = this.store.state; | ||
| let item; | ||
| if (position === "front") { | ||
| this._items.unshift(item); | ||
| this._itemTimestamps.unshift(Date.now()); | ||
| item = items[0]; | ||
| if (item !== void 0) { | ||
| this.#setState({ | ||
| items: items.slice(1), | ||
| itemTimestamps: itemTimestamps.slice(1) | ||
| }); | ||
| } | ||
| } else { | ||
| this._items.push(item); | ||
| this._itemTimestamps.push(Date.now()); | ||
| item = items[items.length - 1]; | ||
| if (item !== void 0) { | ||
| this.#setState({ | ||
| items: items.slice(0, -1), | ||
| itemTimestamps: itemTimestamps.slice(0, -1) | ||
| }); | ||
| } | ||
| } | ||
| } | ||
| if (this._running && !this._pendingTick) { | ||
| this._pendingTick = true; | ||
| this.tick(); | ||
| } | ||
| if (runOnUpdate) { | ||
| this._options.onItemsChange(this); | ||
| } | ||
| return true; | ||
| } | ||
| /** | ||
| * Removes and returns the next item from the queue without executing the function. | ||
| * Use for manual queue management. Normally, use execute() to process items. | ||
| * | ||
| * Example usage: | ||
| * ```ts | ||
| * // FIFO | ||
| * queuer.getNextItem(); | ||
| * // LIFO | ||
| * queuer.getNextItem('back'); | ||
| * ``` | ||
| */ | ||
| getNextItem(position = this._options.getItemsFrom) { | ||
| let item; | ||
| if (position === "front") { | ||
| item = this._items.shift(); | ||
| this._itemTimestamps.shift(); | ||
| if (item !== void 0) { | ||
| this.options.onItemsChange?.(this); | ||
| } | ||
| return item; | ||
| }; | ||
| this.execute = (position) => { | ||
| const item = this.getNextItem(position); | ||
| if (item !== void 0) { | ||
| this.fn(item); | ||
| this.#setState({ | ||
| executionCount: this.store.state.executionCount + 1 | ||
| }); | ||
| this.options.onExecute?.(item, this); | ||
| } | ||
| return item; | ||
| }; | ||
| this.flush = (numberOfItems = this.store.state.items.length, position) => { | ||
| this.#clearTimeout(); | ||
| for (let i = 0; i < numberOfItems; i++) { | ||
| this.execute(position); | ||
| } | ||
| }; | ||
| this.#checkExpiredItems = () => { | ||
| if ((this.options.expirationDuration ?? Infinity) === Infinity && this.options.getIsExpired === defaultOptions.getIsExpired) { | ||
| return; | ||
| } | ||
| const now = Date.now(); | ||
| const expiredIndices = []; | ||
| for (let i = 0; i < this.store.state.items.length; i++) { | ||
| const timestamp = this.store.state.itemTimestamps[i]; | ||
| if (timestamp === void 0) continue; | ||
| const item = this.store.state.items[i]; | ||
| if (item === void 0) continue; | ||
| const isExpired = this.options.getIsExpired !== defaultOptions.getIsExpired ? this.options.getIsExpired(item, timestamp) : now - timestamp > (this.options.expirationDuration ?? Infinity); | ||
| if (isExpired) { | ||
| expiredIndices.push(i); | ||
| } | ||
| } | ||
| for (let i = expiredIndices.length - 1; i >= 0; i--) { | ||
| const index = expiredIndices[i]; | ||
| if (index === void 0) continue; | ||
| const expiredItem = this.store.state.items[index]; | ||
| if (expiredItem === void 0) continue; | ||
| const newItems = [...this.store.state.items]; | ||
| const newTimestamps = [...this.store.state.itemTimestamps]; | ||
| newItems.splice(index, 1); | ||
| newTimestamps.splice(index, 1); | ||
| this.#setState({ | ||
| items: newItems, | ||
| itemTimestamps: newTimestamps, | ||
| expirationCount: this.store.state.expirationCount + 1 | ||
| }); | ||
| this.options.onExpire?.(expiredItem, this); | ||
| } | ||
| if (expiredIndices.length > 0) { | ||
| this.options.onItemsChange?.(this); | ||
| } | ||
| }; | ||
| this.peekNextItem = (position = "front") => { | ||
| if (position === "front") { | ||
| return this.store.state.items[0]; | ||
| } | ||
| return this.store.state.items[this.store.state.size - 1]; | ||
| }; | ||
| this.peekAllItems = () => { | ||
| return [...this.store.state.items]; | ||
| }; | ||
| this.start = () => { | ||
| this.#setState({ isRunning: true }); | ||
| if (!this.store.state.pendingTick && !this.store.state.isEmpty) { | ||
| this.#tick(); | ||
| } | ||
| }; | ||
| this.stop = () => { | ||
| this.#clearTimeout(); | ||
| this.#setState({ isRunning: false, pendingTick: false }); | ||
| }; | ||
| this.#clearTimeout = () => { | ||
| if (this.#timeoutId) { | ||
| clearTimeout(this.#timeoutId); | ||
| this.#timeoutId = null; | ||
| } | ||
| }; | ||
| this.clear = () => { | ||
| this.#setState({ items: [], itemTimestamps: [] }); | ||
| this.options.onItemsChange?.(this); | ||
| }; | ||
| this.reset = () => { | ||
| this.#setState(getDefaultQueuerState()); | ||
| this.options.onItemsChange?.(this); | ||
| }; | ||
| this.options = { | ||
| ...defaultOptions, | ||
| ...initialOptions | ||
| }; | ||
| const isInitiallyRunning = this.options.initialState?.isRunning ?? this.options.started ?? true; | ||
| this.#setState({ | ||
| ...this.options.initialState, | ||
| isRunning: isInitiallyRunning | ||
| }); | ||
| if (this.options.initialState?.items) { | ||
| if (this.store.state.isRunning) { | ||
| this.#tick(); | ||
| } | ||
| } else { | ||
| item = this._items.pop(); | ||
| this._itemTimestamps.pop(); | ||
| for (let i = 0; i < (this.options.initialItems?.length ?? 0); i++) { | ||
| const item = this.options.initialItems[i]; | ||
| const isLast = i === (this.options.initialItems?.length ?? 0) - 1; | ||
| this.addItem(item, this.options.addItemsTo ?? "back", isLast); | ||
| } | ||
| } | ||
| if (item !== void 0) { | ||
| this._options.onItemsChange(this); | ||
| } | ||
| return item; | ||
| } | ||
| /** | ||
| * Removes and returns the next item from the queue and processes it using the provided function. | ||
| * | ||
| * Example usage: | ||
| * ```ts | ||
| * queuer.execute(); | ||
| * // LIFO | ||
| * queuer.execute('back'); | ||
| * ``` | ||
| */ | ||
| execute(position) { | ||
| const item = this.getNextItem(position); | ||
| if (item !== void 0) { | ||
| this.fn(item); | ||
| this._executionCount++; | ||
| this._options.onExecute(item, this); | ||
| } | ||
| return item; | ||
| } | ||
| /** | ||
| * Returns the next item in the queue without removing it. | ||
| * | ||
| * Example usage: | ||
| * ```ts | ||
| * queuer.peekNextItem(); // front | ||
| * queuer.peekNextItem('back'); // back | ||
| * ``` | ||
| */ | ||
| peekNextItem(position = this._options.getItemsFrom) { | ||
| if (position === "front") { | ||
| return this._items[0]; | ||
| } | ||
| return this._items[this._items.length - 1]; | ||
| } | ||
| /** | ||
| * Returns true if the queue is empty (no pending items). | ||
| */ | ||
| getIsEmpty() { | ||
| return this._items.length === 0; | ||
| } | ||
| /** | ||
| * Returns true if the queue is full (reached maxSize). | ||
| */ | ||
| getIsFull() { | ||
| return this._items.length >= this._options.maxSize; | ||
| } | ||
| /** | ||
| * Returns the number of pending items in the queue. | ||
| */ | ||
| getSize() { | ||
| return this._items.length; | ||
| } | ||
| /** | ||
| * Returns a copy of all items in the queue. | ||
| */ | ||
| peekAllItems() { | ||
| return [...this._items]; | ||
| } | ||
| /** | ||
| * Returns the number of items that have been processed and removed from the queue. | ||
| */ | ||
| getExecutionCount() { | ||
| return this._executionCount; | ||
| } | ||
| /** | ||
| * Returns the number of items that have been rejected from being added to the queue. | ||
| */ | ||
| getRejectionCount() { | ||
| return this._rejectionCount; | ||
| } | ||
| /** | ||
| * Returns the number of items that have expired and been removed from the queue. | ||
| */ | ||
| getExpirationCount() { | ||
| return this._expirationCount; | ||
| } | ||
| /** | ||
| * Returns true if the queuer is currently running (processing items). | ||
| */ | ||
| getIsRunning() { | ||
| return this._running; | ||
| } | ||
| /** | ||
| * Returns true if the queuer is running but has no items to process. | ||
| */ | ||
| getIsIdle() { | ||
| return this._running && this.getIsEmpty(); | ||
| } | ||
| #timeoutId; | ||
| #setState; | ||
| #getWait; | ||
| #tick; | ||
| #checkExpiredItems; | ||
| #clearTimeout; | ||
| } | ||
| function queue(fn, options) { | ||
| const queuer = new Queuer(fn, options); | ||
| return queuer.addItem.bind(queuer); | ||
| function queue(fn, initialOptions) { | ||
| const queuer = new Queuer(fn, initialOptions); | ||
| return queuer.addItem; | ||
| } | ||
@@ -324,0 +276,0 @@ export { |
@@ -1,1 +0,1 @@ | ||
| {"version":3,"file":"queuer.js","sources":["../../src/queuer.ts"],"sourcesContent":["import { parseFunctionOrValue } from './utils'\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 * Maximum number of items allowed in the queuer\n */\n maxSize?: number\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 removed from the queuer\n */\n onExecute?: (item: TValue, queuer: Queuer<TValue>) => void\n /**\n * Callback fired whenever the queuer's running state changes\n */\n onIsRunningChange?: (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\nconst defaultOptions: Required<QueuerOptions<any>> = {\n addItemsTo: 'back',\n getItemsFrom: 'front',\n getPriority: (item) => item?.priority ?? 0,\n getIsExpired: () => false,\n expirationDuration: Infinity,\n initialItems: [],\n maxSize: Infinity,\n onExecute: () => {},\n onIsRunningChange: () => {},\n onItemsChange: () => {},\n onReject: () => {},\n onExpire: () => {},\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 * 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 running)\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 * 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) => 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 private _options: Required<QueuerOptions<TValue>>\n private _items: Array<TValue> = []\n private _itemTimestamps: Array<number> = []\n private _executionCount = 0\n private _rejectionCount = 0\n private _expirationCount = 0\n private _onItemsChanges: Array<(item: TValue) => void> = []\n private _running: boolean\n private _pendingTick = false\n\n constructor(\n private fn: (item: TValue) => void,\n initialOptions: QueuerOptions<TValue> = {},\n ) {\n this._options = { ...defaultOptions, ...initialOptions }\n this._running = this._options.started\n\n for (let i = 0; i < this._options.initialItems.length; i++) {\n const item = this._options.initialItems[i]!\n const isLast = i === this._options.initialItems.length - 1\n this.addItem(item, this._options.addItemsTo, isLast)\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 /**\n * Returns the current queuer options, including defaults and any overrides.\n */\n getOptions(): Required<QueuerOptions<TValue>> {\n return this._options\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, this)\n }\n\n /**\n * Processes items in the queue up to the wait interval. Internal use only.\n */\n private tick() {\n if (!this._running) {\n this._pendingTick = false\n return\n }\n\n // Check for expired items\n this.checkExpiredItems()\n\n while (!this.getIsEmpty()) {\n const nextItem = this.execute(this._options.getItemsFrom)\n if (nextItem === undefined) {\n break\n }\n this._onItemsChanges.forEach((cb) => cb(nextItem))\n\n const wait = this.getWait()\n if (wait > 0) {\n // Use setTimeout to wait before processing next item\n setTimeout(() => this.tick(), wait)\n return\n }\n\n this.tick()\n }\n this._pendingTick = false\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 private checkExpiredItems() {\n if (\n this._options.expirationDuration === Infinity &&\n this._options.getIsExpired === defaultOptions.getIsExpired\n )\n return\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._items.length; i++) {\n const timestamp = this._itemTimestamps[i]\n if (timestamp === undefined) continue\n\n const item = this._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\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._items[index]\n if (expiredItem === undefined) continue\n\n this._items.splice(index, 1)\n this._itemTimestamps.splice(index, 1)\n this._expirationCount++\n this._options.onExpire(expiredItem, this)\n }\n\n if (expiredIndices.length > 0) {\n this._options.onItemsChange(this)\n }\n }\n\n /**\n * Stops processing items in the queue. Does not clear the queue.\n */\n stop() {\n this._running = false\n this._pendingTick = false\n this._options.onIsRunningChange(this)\n }\n\n /**\n * Starts processing items in the queue. If already running, does nothing.\n */\n start() {\n this._running = true\n if (!this._pendingTick && !this.getIsEmpty()) {\n this._pendingTick = true\n this.tick()\n }\n this._options.onIsRunningChange(this)\n }\n\n /**\n * Removes all pending items from the queue. Does not affect items being processed.\n */\n clear(): void {\n this._items = []\n this._options.onItemsChange(this)\n }\n\n /**\n * Resets the queuer to its initial state. Optionally repopulates with initial items.\n * Does not affect callbacks or options.\n */\n reset(withInitialItems?: boolean): void {\n this.clear()\n this._executionCount = 0\n if (withInitialItems) {\n this._items = [...this._options.initialItems]\n }\n this._running = this._options.started\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,\n runOnUpdate: boolean = true,\n ): boolean {\n if (this.getIsFull()) {\n this._rejectionCount++\n this._options.onReject(item, this)\n return false\n }\n\n if (this._options.getPriority !== defaultOptions.getPriority) {\n // If custom priority function is provided, insert based on priority\n const priority = this._options.getPriority(item)\n const insertIndex = this._items.findIndex(\n (existing) => this._options.getPriority(existing) < priority,\n )\n\n if (insertIndex === -1) {\n this._items.push(item)\n this._itemTimestamps.push(Date.now())\n } else {\n this._items.splice(insertIndex, 0, item)\n this._itemTimestamps.splice(insertIndex, 0, Date.now())\n }\n } else {\n // Default FIFO/LIFO behavior\n if (position === 'front') {\n this._items.unshift(item)\n this._itemTimestamps.unshift(Date.now())\n } else {\n this._items.push(item)\n this._itemTimestamps.push(Date.now())\n }\n }\n\n if (this._running && !this._pendingTick) {\n this._pendingTick = true\n this.tick()\n }\n if (runOnUpdate) {\n this._options.onItemsChange(this)\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,\n ): TValue | undefined {\n let item: TValue | undefined\n\n if (position === 'front') {\n item = this._items.shift()\n this._itemTimestamps.shift()\n } else {\n item = this._items.pop()\n this._itemTimestamps.pop()\n }\n\n if (item !== undefined) {\n this._options.onItemsChange(this)\n }\n\n return item\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._executionCount++\n this._options.onExecute(item, this)\n }\n return item\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(\n position: QueuePosition = this._options.getItemsFrom,\n ): TValue | undefined {\n if (position === 'front') {\n return this._items[0]\n }\n return this._items[this._items.length - 1]\n }\n\n /**\n * Returns true if the queue is empty (no pending items).\n */\n getIsEmpty(): boolean {\n return this._items.length === 0\n }\n\n /**\n * Returns true if the queue is full (reached maxSize).\n */\n getIsFull(): boolean {\n return this._items.length >= this._options.maxSize\n }\n\n /**\n * Returns the number of pending items in the queue.\n */\n getSize(): number {\n return this._items.length\n }\n\n /**\n * Returns a copy of all items in the queue.\n */\n peekAllItems(): Array<TValue> {\n return [...this._items]\n }\n\n /**\n * Returns the number of items that have been processed and removed from the queue.\n */\n getExecutionCount(): number {\n return this._executionCount\n }\n\n /**\n * Returns the number of items that have been rejected from being added to the queue.\n */\n getRejectionCount(): number {\n return this._rejectionCount\n }\n\n /**\n * Returns the number of items that have expired and been removed from the queue.\n */\n getExpirationCount(): number {\n return this._expirationCount\n }\n\n /**\n * Returns true if the queuer is currently running (processing items).\n */\n getIsRunning() {\n return this._running\n }\n\n /**\n * Returns true if the queuer is running but has no items to process.\n */\n getIsIdle() {\n return this._running && this.getIsEmpty()\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 is a simplified wrapper around the Queuer class that only exposes the\n * `addItem` method. The queue is always running and will process items as they are added.\n * For more control over queue processing, use the Queuer class directly.\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 options: QueuerOptions<TValue>,\n) {\n const queuer = new Queuer<TValue>(fn, options)\n return queuer.addItem.bind(queuer)\n}\n"],"names":[],"mappings":"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"} | ||
| {"version":3,"file":"queuer.js","sources":["../../src/queuer.ts"],"sourcesContent":["import { Store } from '@tanstack/store'\nimport { parseFunctionOrValue } from './utils'\n\nexport interface QueuerState<TValue> {\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 * Timestamps when items were added to the queue for expiration tracking\n */\n itemTimestamps: Array<number>\n /**\n * Array of items currently waiting to be processed\n */\n items: Array<TValue>\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 }\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 * Maximum number of items allowed in the queuer\n */\n maxSize?: number\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 removed from the queuer\n */\n onExecute?: (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\nconst defaultOptions: Omit<\n Required<QueuerOptions<any>>,\n | 'initialState'\n | 'onExecute'\n | 'onIsRunningChange'\n | 'onItemsChange'\n | 'onReject'\n | 'onExpire'\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 * 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) => 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 options: QueuerOptions<TValue>\n #timeoutId: NodeJS.Timeout | null = null\n\n constructor(\n private fn: (item: TValue) => void,\n initialOptions: QueuerOptions<TValue> = {},\n ) {\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\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 }\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.isEmpty) {\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 if (this.store.state.isFull) {\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 if (position === 'front') {\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 /**\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 }\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.size - 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.isEmpty) {\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 is a simplified wrapper around the Queuer class that only exposes the\n * `addItem` method. The queue is always isRunning and will process items as they are added.\n * For more control over queue processing, use the Queuer class directly.\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"],"names":[],"mappings":";;AAsDA,SAAS,wBAAqD;AAC5D,SAAO;AAAA,IACL,gBAAgB;AAAA,IAChB,iBAAiB;AAAA,IACjB,SAAS;AAAA,IACT,QAAQ;AAAA,IACR,QAAQ;AAAA,IACR,WAAW;AAAA,IACX,gBAAgB,CAAA;AAAA,IAChB,OAAO,CAAA;AAAA,IACP,aAAa;AAAA,IACb,gBAAgB;AAAA,IAChB,MAAM;AAAA,IACN,QAAQ;AAAA,EAAA;AAEZ;AAyEA,MAAM,iBAQF;AAAA,EACF,YAAY;AAAA,EACZ,cAAc;AAAA,EACd,aAAa,CAAC,SAAS,MAAM,YAAY;AAAA,EACzC,cAAc,MAAM;AAAA,EACpB,oBAAoB;AAAA,EACpB,cAAc,CAAA;AAAA,EACd,SAAS;AAAA,EACT,SAAS;AAAA,EACT,MAAM;AACR;AAkFO,MAAM,OAAe;AAAA,EAO1B,YACU,IACR,iBAAwC,IACxC;AAFQ,SAAA,KAAA;AAPV,SAAS,QAA8C,IAAI;AAAA,MACzD,sBAAA;AAAA,IAA8B;AAGhC,SAAA,aAAoC;AAiCpC,SAAA,aAAa,CAAC,eAAqD;AACjE,WAAK,UAAU,EAAE,GAAG,KAAK,SAAS,GAAG,WAAA;AAAA,IAAW;AAGlD,SAAA,YAAY,CAAC,aAAiD;AAC5D,WAAK,MAAM,SAAS,CAAC,UAAU;AAC7B,cAAM,gBAAgB;AAAA,UACpB,GAAG;AAAA,UACH,GAAG;AAAA,QAAA;AAGL,cAAM,EAAE,OAAO,UAAA,IAAc;AAE7B,cAAM,OAAO,MAAM;AACnB,cAAM,SAAS,SAAS,KAAK,QAAQ,WAAW;AAChD,cAAM,UAAU,SAAS;AACzB,cAAM,SAAS,aAAa;AAE5B,cAAM,SAAS,SAAS,SAAS,YAAY,YAAY;AAEzD,eAAO;AAAA,UACL,GAAG;AAAA,UACH;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,QAAA;AAAA,MACF,CACD;AAAA,IAAA;AAOH,SAAA,WAAW,MAAc;AACvB,aAAO,qBAAqB,KAAK,QAAQ,QAAQ,GAAG,IAAI;AAAA,IAAA;AAM1D,SAAA,QAAQ,MAAM;AACZ,UAAI,CAAC,KAAK,MAAM,MAAM,WAAW;AAC/B,aAAK,UAAU,EAAE,aAAa,MAAA,CAAO;AACrC;AAAA,MAAA;AAGF,WAAK,UAAU,EAAE,aAAa,KAAA,CAAM;AAGpC,WAAK,mBAAA;AAEL,aAAO,CAAC,KAAK,MAAM,MAAM,SAAS;AAChC,cAAM,WAAW,KAAK,QAAQ,KAAK,QAAQ,gBAAgB,OAAO;AAClE,YAAI,aAAa,QAAW;AAC1B;AAAA,QAAA;AAGF,cAAM,OAAO,KAAK,SAAA;AAClB,YAAI,OAAO,GAAG;AAEZ,eAAK,aAAa,WAAW,MAAM,KAAK,MAAA,GAAS,IAAI;AACrD;AAAA,QAAA;AAGF,aAAK,MAAA;AAAA,MAAM;AAEb,WAAK,UAAU,EAAE,aAAa,MAAA,CAAO;AAAA,IAAA;AAevC,SAAA,UAAU,CACR,MACA,WAA0B,KAAK,QAAQ,cAAc,QACrD,mBAA4B,SAChB;AACZ,UAAI,KAAK,MAAM,MAAM,QAAQ;AAC3B,aAAK,UAAU;AAAA,UACb,gBAAgB,KAAK,MAAM,MAAM,iBAAiB;AAAA,QAAA,CACnD;AACD,aAAK,QAAQ,WAAW,MAAM,IAAI;AAClC,eAAO;AAAA,MAAA;AAIT,YAAM,WACJ,KAAK,QAAQ,gBAAgB,eAAe,cACxC,KAAK,QAAQ,YAAa,IAAI,IAC7B,KAAa;AAEpB,YAAM,QAAQ,KAAK,MAAM,MAAM;AAC/B,YAAM,iBAAiB,KAAK,MAAM,MAAM;AAExC,UAAI,aAAa,QAAW;AAE1B,cAAM,cAAc,MAAM,UAAU,CAAC,aAAa;AAChD,gBAAM,mBACJ,KAAK,QAAQ,gBAAgB,eAAe,cACxC,KAAK,QAAQ,YAAa,QAAQ,IACjC,SAAiB;AACxB,iBAAO,mBAAmB;AAAA,QAAA,CAC3B;AAED,YAAI,gBAAgB,IAAI;AACtB,gBAAM,KAAK,IAAI;AACf,yBAAe,KAAK,KAAK,KAAK;AAAA,QAAA,OACzB;AACL,gBAAM,OAAO,aAAa,GAAG,IAAI;AACjC,yBAAe,OAAO,aAAa,GAAG,KAAK,KAAK;AAAA,QAAA;AAAA,MAClD,OACK;AACL,YAAI,aAAa,SAAS;AAExB,gBAAM,QAAQ,IAAI;AAClB,yBAAe,QAAQ,KAAK,KAAK;AAAA,QAAA,OAC5B;AAEL,gBAAM,KAAK,IAAI;AACf,yBAAe,KAAK,KAAK,KAAK;AAAA,QAAA;AAAA,MAChC;AAGF,WAAK,UAAU;AAAA,QACb;AAAA,QACA;AAAA,MAAA,CACD;AAED,UAAI,kBAAkB;AACpB,aAAK,QAAQ,gBAAgB,IAAI;AAAA,MAAA;AAGnC,UAAI,KAAK,MAAM,MAAM,aAAa,CAAC,KAAK,MAAM,MAAM,aAAa;AAC/D,aAAK,UAAU,EAAE,aAAa,KAAA,CAAM;AACpC,aAAK,MAAA;AAAA,MAAM;AAGb,aAAO;AAAA,IAAA;AAeT,SAAA,cAAc,CACZ,WAA0B,KAAK,QAAQ,gBAAgB,YAChC;AACvB,YAAM,EAAE,OAAO,eAAA,IAAmB,KAAK,MAAM;AAC7C,UAAI;AAEJ,UAAI,aAAa,SAAS;AACxB,eAAO,MAAM,CAAC;AACd,YAAI,SAAS,QAAW;AACtB,eAAK,UAAU;AAAA,YACb,OAAO,MAAM,MAAM,CAAC;AAAA,YACpB,gBAAgB,eAAe,MAAM,CAAC;AAAA,UAAA,CACvC;AAAA,QAAA;AAAA,MACH,OACK;AACL,eAAO,MAAM,MAAM,SAAS,CAAC;AAC7B,YAAI,SAAS,QAAW;AACtB,eAAK,UAAU;AAAA,YACb,OAAO,MAAM,MAAM,GAAG,EAAE;AAAA,YACxB,gBAAgB,eAAe,MAAM,GAAG,EAAE;AAAA,UAAA,CAC3C;AAAA,QAAA;AAAA,MACH;AAGF,UAAI,SAAS,QAAW;AACtB,aAAK,QAAQ,gBAAgB,IAAI;AAAA,MAAA;AAGnC,aAAO;AAAA,IAAA;AAaT,SAAA,UAAU,CAAC,aAAiD;AAC1D,YAAM,OAAO,KAAK,YAAY,QAAQ;AACtC,UAAI,SAAS,QAAW;AACtB,aAAK,GAAG,IAAI;AACZ,aAAK,UAAU;AAAA,UACb,gBAAgB,KAAK,MAAM,MAAM,iBAAiB;AAAA,QAAA,CACnD;AACD,aAAK,QAAQ,YAAY,MAAM,IAAI;AAAA,MAAA;AAErC,aAAO;AAAA,IAAA;AAOT,SAAA,QAAQ,CACN,gBAAwB,KAAK,MAAM,MAAM,MAAM,QAC/C,aACS;AACT,WAAK,cAAA;AACL,eAAS,IAAI,GAAG,IAAI,eAAe,KAAK;AACtC,aAAK,QAAQ,QAAQ;AAAA,MAAA;AAAA,IACvB;AAOF,SAAA,qBAAqB,MAAY;AAC/B,WACG,KAAK,QAAQ,sBAAsB,cAAc,YAClD,KAAK,QAAQ,iBAAiB,eAAe,cAC7C;AACA;AAAA,MAAA;AAGF,YAAM,MAAM,KAAK,IAAA;AACjB,YAAM,iBAAgC,CAAA;AAGtC,eAAS,IAAI,GAAG,IAAI,KAAK,MAAM,MAAM,MAAM,QAAQ,KAAK;AACtD,cAAM,YAAY,KAAK,MAAM,MAAM,eAAe,CAAC;AACnD,YAAI,cAAc,OAAW;AAE7B,cAAM,OAAO,KAAK,MAAM,MAAM,MAAM,CAAC;AACrC,YAAI,SAAS,OAAW;AAExB,cAAM,YACJ,KAAK,QAAQ,iBAAiB,eAAe,eACzC,KAAK,QAAQ,aAAc,MAAM,SAAS,IAC1C,MAAM,aAAa,KAAK,QAAQ,sBAAsB;AAE5D,YAAI,WAAW;AACb,yBAAe,KAAK,CAAC;AAAA,QAAA;AAAA,MACvB;AAIF,eAAS,IAAI,eAAe,SAAS,GAAG,KAAK,GAAG,KAAK;AACnD,cAAM,QAAQ,eAAe,CAAC;AAC9B,YAAI,UAAU,OAAW;AAEzB,cAAM,cAAc,KAAK,MAAM,MAAM,MAAM,KAAK;AAChD,YAAI,gBAAgB,OAAW;AAE/B,cAAM,WAAW,CAAC,GAAG,KAAK,MAAM,MAAM,KAAK;AAC3C,cAAM,gBAAgB,CAAC,GAAG,KAAK,MAAM,MAAM,cAAc;AACzD,iBAAS,OAAO,OAAO,CAAC;AACxB,sBAAc,OAAO,OAAO,CAAC;AAC7B,aAAK,UAAU;AAAA,UACb,OAAO;AAAA,UACP,gBAAgB;AAAA,UAChB,iBAAiB,KAAK,MAAM,MAAM,kBAAkB;AAAA,QAAA,CACrD;AACD,aAAK,QAAQ,WAAW,aAAa,IAAI;AAAA,MAAA;AAG3C,UAAI,eAAe,SAAS,GAAG;AAC7B,aAAK,QAAQ,gBAAgB,IAAI;AAAA,MAAA;AAAA,IACnC;AAYF,SAAA,eAAe,CAAC,WAA0B,YAAgC;AACxE,UAAI,aAAa,SAAS;AACxB,eAAO,KAAK,MAAM,MAAM,MAAM,CAAC;AAAA,MAAA;AAEjC,aAAO,KAAK,MAAM,MAAM,MAAM,KAAK,MAAM,MAAM,OAAO,CAAC;AAAA,IAAA;AAMzD,SAAA,eAAe,MAAqB;AAClC,aAAO,CAAC,GAAG,KAAK,MAAM,MAAM,KAAK;AAAA,IAAA;AAMnC,SAAA,QAAQ,MAAM;AACZ,WAAK,UAAU,EAAE,WAAW,KAAA,CAAM;AAClC,UAAI,CAAC,KAAK,MAAM,MAAM,eAAe,CAAC,KAAK,MAAM,MAAM,SAAS;AAC9D,aAAK,MAAA;AAAA,MAAM;AAAA,IACb;AAMF,SAAA,OAAO,MAAM;AACX,WAAK,cAAA;AACL,WAAK,UAAU,EAAE,WAAW,OAAO,aAAa,OAAO;AAAA,IAAA;AAGzD,SAAA,gBAAgB,MAAY;AAC1B,UAAI,KAAK,YAAY;AACnB,qBAAa,KAAK,UAAU;AAC5B,aAAK,aAAa;AAAA,MAAA;AAAA,IACpB;AAMF,SAAA,QAAQ,MAAY;AAClB,WAAK,UAAU,EAAE,OAAO,CAAA,GAAI,gBAAgB,CAAA,GAAI;AAChD,WAAK,QAAQ,gBAAgB,IAAI;AAAA,IAAA;AAMnC,SAAA,QAAQ,MAAY;AAClB,WAAK,UAAU,uBAA+B;AAC9C,WAAK,QAAQ,gBAAgB,IAAI;AAAA,IAAA;AAxXjC,SAAK,UAAU;AAAA,MACb,GAAG;AAAA,MACH,GAAG;AAAA,IAAA;AAEL,UAAM,qBACJ,KAAK,QAAQ,cAAc,aAAa,KAAK,QAAQ,WAAW;AAClE,SAAK,UAAU;AAAA,MACb,GAAG,KAAK,QAAQ;AAAA,MAChB,WAAW;AAAA,IAAA,CACZ;AAED,QAAI,KAAK,QAAQ,cAAc,OAAO;AACpC,UAAI,KAAK,MAAM,MAAM,WAAW;AAC9B,aAAK,MAAA;AAAA,MAAM;AAAA,IACb,OACK;AACL,eAAS,IAAI,GAAG,KAAK,KAAK,QAAQ,cAAc,UAAU,IAAI,KAAK;AACjE,cAAM,OAAO,KAAK,QAAQ,aAAc,CAAC;AACzC,cAAM,SAAS,OAAO,KAAK,QAAQ,cAAc,UAAU,KAAK;AAChE,aAAK,QAAQ,MAAM,KAAK,QAAQ,cAAc,QAAQ,MAAM;AAAA,MAAA;AAAA,IAC9D;AAAA,EACF;AAAA,EA3BF;AAAA,EAqCA;AAAA,EA+BA;AAAA,EAOA;AAAA,EAgMA;AAAA,EA+FA;AAsBF;AAuCO,SAAS,MACd,IACA,gBACA;AACA,QAAM,SAAS,IAAI,OAAe,IAAI,cAAc;AACpD,SAAO,OAAO;AAChB;"} |
@@ -0,2 +1,17 @@ | ||
| import { Store } from '@tanstack/store'; | ||
| import { AnyFunction } from './types.js'; | ||
| export interface RateLimiterState { | ||
| /** | ||
| * Number of function executions that have been completed | ||
| */ | ||
| executionCount: number; | ||
| /** | ||
| * Array of timestamps when executions occurred for rate limiting calculations | ||
| */ | ||
| executionTimes: Array<number>; | ||
| /** | ||
| * Number of function executions that have been rejected due to rate limiting | ||
| */ | ||
| rejectionCount: number; | ||
| } | ||
| /** | ||
@@ -12,2 +27,6 @@ * Options for configuring a rate-limited function | ||
| /** | ||
| * Initial state for the rate limiter | ||
| */ | ||
| initialState?: Partial<RateLimiterState>; | ||
| /** | ||
| * Maximum number of executions allowed within the time window. | ||
@@ -58,2 +77,11 @@ * Can be a number or a callback function that receives the rate limiter instance and returns a number. | ||
| * | ||
| * State Management: | ||
| * - Uses TanStack Store for reactive state management | ||
| * - Use `initialState` to provide initial state values when creating the rate limiter | ||
| * - Use `onExecute` callback to react to function execution and implement custom logic | ||
| * - Use `onReject` callback to react to executions being rejected when rate limit is exceeded | ||
| * - The state includes execution count, execution times, and rejection count | ||
| * - State can be accessed via `rateLimiter.store.state` when using the class directly | ||
| * - When using framework adapters (React/Solid), state is accessed from `rateLimiter.state` | ||
| * | ||
| * @example | ||
@@ -63,3 +91,7 @@ * ```ts | ||
| * (id: string) => api.getData(id), | ||
| * { limit: 5, window: 1000, windowType: 'sliding' } // 5 calls per second with sliding window | ||
| * { | ||
| * limit: 5, | ||
| * window: 1000, | ||
| * windowType: 'sliding', | ||
| * } | ||
| * ); | ||
@@ -72,7 +104,6 @@ * | ||
| export declare class RateLimiter<TFn extends AnyFunction> { | ||
| #private; | ||
| private fn; | ||
| private _executionCount; | ||
| private _rejectionCount; | ||
| private _executionTimes; | ||
| private _options; | ||
| readonly store: Store<Readonly<RateLimiterState>>; | ||
| options: RateLimiterOptions<TFn>; | ||
| constructor(fn: TFn, initialOptions: RateLimiterOptions<TFn>); | ||
@@ -82,20 +113,4 @@ /** | ||
| */ | ||
| setOptions(newOptions: Partial<RateLimiterOptions<TFn>>): void; | ||
| setOptions: (newOptions: Partial<RateLimiterOptions<TFn>>) => void; | ||
| /** | ||
| * Returns the current rate limiter options | ||
| */ | ||
| getOptions(): Required<RateLimiterOptions<TFn>>; | ||
| /** | ||
| * Returns the current enabled state of the rate limiter | ||
| */ | ||
| getEnabled(): boolean; | ||
| /** | ||
| * Returns the current limit of executions allowed within the time window | ||
| */ | ||
| getLimit(): number; | ||
| /** | ||
| * Returns the current time window in milliseconds | ||
| */ | ||
| getWindow(): number; | ||
| /** | ||
| * Attempts to execute the rate-limited function if within the configured limits. | ||
@@ -115,26 +130,15 @@ * Will reject execution if the number of calls in the current window exceeds the limit. | ||
| */ | ||
| maybeExecute(...args: Parameters<TFn>): boolean; | ||
| private execute; | ||
| private rejectFunction; | ||
| private cleanupOldExecutions; | ||
| maybeExecute: (...args: Parameters<TFn>) => boolean; | ||
| /** | ||
| * Returns the number of times the function has been executed | ||
| */ | ||
| getExecutionCount(): number; | ||
| /** | ||
| * Returns the number of times the function has been rejected | ||
| */ | ||
| getRejectionCount(): number; | ||
| /** | ||
| * Returns the number of remaining executions allowed in the current window | ||
| */ | ||
| getRemainingInWindow(): number; | ||
| getRemainingInWindow: () => number; | ||
| /** | ||
| * Returns the number of milliseconds until the next execution will be possible | ||
| */ | ||
| getMsUntilNextWindow(): number; | ||
| getMsUntilNextWindow: () => number; | ||
| /** | ||
| * Resets the rate limiter state | ||
| */ | ||
| reset(): void; | ||
| reset: () => void; | ||
| } | ||
@@ -155,2 +159,11 @@ /** | ||
| * | ||
| * State Management: | ||
| * - Uses TanStack Store for reactive state management | ||
| * - Use `initialState` to provide initial state values when creating the rate limiter | ||
| * - Use `onExecute` callback to react to function execution and implement custom logic | ||
| * - Use `onReject` callback to react to executions being rejected when rate limit is exceeded | ||
| * - The state includes execution count, execution times, and rejection count | ||
| * - State can be accessed via the underlying RateLimiter instance's `store.state` property | ||
| * - When using framework adapters (React/Solid), state is accessed from the hook's state property | ||
| * | ||
| * Consider using throttle() or debounce() if you need more intelligent execution control. Use rate limiting when you specifically | ||
@@ -157,0 +170,0 @@ * need to enforce a hard limit on the number of executions within a time period. |
+97
-130
@@ -0,9 +1,13 @@ | ||
| import { Store } from "@tanstack/store"; | ||
| import { parseFunctionOrValue } from "./utils.js"; | ||
| function getDefaultRateLimiterState() { | ||
| return structuredClone({ | ||
| executionCount: 0, | ||
| executionTimes: [], | ||
| rejectionCount: 0 | ||
| }); | ||
| } | ||
| const defaultOptions = { | ||
| enabled: true, | ||
| limit: 1, | ||
| onExecute: () => { | ||
| }, | ||
| onReject: () => { | ||
| }, | ||
| window: 0, | ||
@@ -15,137 +19,100 @@ windowType: "fixed" | ||
| this.fn = fn; | ||
| this._executionCount = 0; | ||
| this._rejectionCount = 0; | ||
| this._executionTimes = []; | ||
| this._options = { | ||
| ...defaultOptions, | ||
| ...initialOptions | ||
| this.store = new Store(getDefaultRateLimiterState()); | ||
| this.setOptions = (newOptions) => { | ||
| this.options = { ...this.options, ...newOptions }; | ||
| }; | ||
| } | ||
| /** | ||
| * Updates the rate limiter options | ||
| */ | ||
| setOptions(newOptions) { | ||
| this._options = { ...this._options, ...newOptions }; | ||
| } | ||
| /** | ||
| * Returns the current rate limiter options | ||
| */ | ||
| getOptions() { | ||
| return this._options; | ||
| } | ||
| /** | ||
| * Returns the current enabled state of the rate limiter | ||
| */ | ||
| getEnabled() { | ||
| return parseFunctionOrValue(this._options.enabled, this); | ||
| } | ||
| /** | ||
| * Returns the current limit of executions allowed within the time window | ||
| */ | ||
| getLimit() { | ||
| return parseFunctionOrValue(this._options.limit, this); | ||
| } | ||
| /** | ||
| * Returns the current time window in milliseconds | ||
| */ | ||
| getWindow() { | ||
| return parseFunctionOrValue(this._options.window, this); | ||
| } | ||
| /** | ||
| * Attempts to execute the rate-limited function if within the configured limits. | ||
| * Will reject execution if the number of calls in the current window exceeds the limit. | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * const rateLimiter = new RateLimiter(fn, { limit: 5, window: 1000 }); | ||
| * | ||
| * // First 5 calls will return true | ||
| * rateLimiter.maybeExecute('arg1', 'arg2'); // true | ||
| * | ||
| * // Additional calls within the window will return false | ||
| * rateLimiter.maybeExecute('arg1', 'arg2'); // false | ||
| * ``` | ||
| */ | ||
| maybeExecute(...args) { | ||
| this.cleanupOldExecutions(); | ||
| if (this._options.windowType === "sliding") { | ||
| if (this._executionTimes.length < this.getLimit()) { | ||
| this.execute(...args); | ||
| this.#setState = (newState) => { | ||
| this.store.setState((state) => { | ||
| const combinedState = { | ||
| ...state, | ||
| ...newState | ||
| }; | ||
| return combinedState; | ||
| }); | ||
| }; | ||
| this.#getEnabled = () => { | ||
| return !!parseFunctionOrValue(this.options.enabled, this); | ||
| }; | ||
| this.#getLimit = () => { | ||
| return parseFunctionOrValue(this.options.limit, this); | ||
| }; | ||
| this.#getWindow = () => { | ||
| return parseFunctionOrValue(this.options.window, this); | ||
| }; | ||
| this.maybeExecute = (...args) => { | ||
| this.#cleanupOldExecutions(); | ||
| const relevantExecutionTimes = this.#getRelevantExecutionTimes(); | ||
| if (relevantExecutionTimes.length < this.#getLimit()) { | ||
| this.#execute(...args); | ||
| return true; | ||
| } | ||
| } else { | ||
| this.#setState({ | ||
| rejectionCount: this.store.state.rejectionCount + 1 | ||
| }); | ||
| this.options.onReject?.(this); | ||
| return false; | ||
| }; | ||
| this.#execute = (...args) => { | ||
| if (!this.#getEnabled()) return; | ||
| const now = Date.now(); | ||
| const oldestExecution = Math.min(...this._executionTimes); | ||
| const isNewWindow = oldestExecution + this.getWindow() <= now; | ||
| if (isNewWindow || this._executionTimes.length < this.getLimit()) { | ||
| this.execute(...args); | ||
| return true; | ||
| this.fn(...args); | ||
| this.store.state.executionTimes.push(now); | ||
| this.#setState({ | ||
| executionCount: this.store.state.executionCount + 1 | ||
| }); | ||
| this.options.onExecute?.(this); | ||
| }; | ||
| this.#getRelevantExecutionTimes = () => { | ||
| if (this.options.windowType === "sliding") { | ||
| return this.store.state.executionTimes.filter( | ||
| (time) => time > Date.now() - this.#getWindow() | ||
| ); | ||
| } else { | ||
| const oldestExecution = Math.min(...this.store.state.executionTimes); | ||
| const windowStart = oldestExecution; | ||
| return this.store.state.executionTimes.filter( | ||
| (time) => time >= windowStart && time <= windowStart + this.#getWindow() | ||
| ); | ||
| } | ||
| } | ||
| this.rejectFunction(); | ||
| return false; | ||
| }; | ||
| this.#cleanupOldExecutions = () => { | ||
| const now = Date.now(); | ||
| const windowStart = now - this.#getWindow(); | ||
| this.#setState({ | ||
| executionTimes: this.store.state.executionTimes.filter( | ||
| (time) => time > windowStart | ||
| ) | ||
| }); | ||
| }; | ||
| this.getRemainingInWindow = () => { | ||
| const relevantExecutionTimes = this.#getRelevantExecutionTimes(); | ||
| return Math.max(0, this.#getLimit() - relevantExecutionTimes.length); | ||
| }; | ||
| this.getMsUntilNextWindow = () => { | ||
| if (this.getRemainingInWindow() > 0) { | ||
| return 0; | ||
| } | ||
| const oldestExecution = this.store.state.executionTimes[0] ?? Infinity; | ||
| return oldestExecution + this.#getWindow() - Date.now(); | ||
| }; | ||
| this.reset = () => { | ||
| this.#setState(getDefaultRateLimiterState()); | ||
| }; | ||
| this.options = { | ||
| ...defaultOptions, | ||
| ...initialOptions | ||
| }; | ||
| this.#setState(this.options.initialState ?? {}); | ||
| } | ||
| execute(...args) { | ||
| var _a, _b; | ||
| if (!this.getEnabled()) return; | ||
| const now = Date.now(); | ||
| this._executionCount++; | ||
| this._executionTimes.push(now); | ||
| this.fn(...args); | ||
| (_b = (_a = this._options).onExecute) == null ? void 0 : _b.call(_a, this); | ||
| } | ||
| rejectFunction() { | ||
| this._rejectionCount++; | ||
| if (this._options.onReject) { | ||
| this._options.onReject(this); | ||
| } | ||
| } | ||
| cleanupOldExecutions() { | ||
| const now = Date.now(); | ||
| const windowStart = now - this.getWindow(); | ||
| this._executionTimes = this._executionTimes.filter( | ||
| (time) => time > windowStart | ||
| ); | ||
| } | ||
| /** | ||
| * Returns the number of times the function has been executed | ||
| */ | ||
| getExecutionCount() { | ||
| return this._executionCount; | ||
| } | ||
| /** | ||
| * Returns the number of times the function has been rejected | ||
| */ | ||
| getRejectionCount() { | ||
| return this._rejectionCount; | ||
| } | ||
| /** | ||
| * Returns the number of remaining executions allowed in the current window | ||
| */ | ||
| getRemainingInWindow() { | ||
| this.cleanupOldExecutions(); | ||
| return Math.max(0, this.getLimit() - this._executionTimes.length); | ||
| } | ||
| /** | ||
| * Returns the number of milliseconds until the next execution will be possible | ||
| */ | ||
| getMsUntilNextWindow() { | ||
| if (this.getRemainingInWindow() > 0) { | ||
| return 0; | ||
| } | ||
| const oldestExecution = Math.min(...this._executionTimes); | ||
| return oldestExecution + this.getWindow() - Date.now(); | ||
| } | ||
| /** | ||
| * Resets the rate limiter state | ||
| */ | ||
| reset() { | ||
| this._executionTimes = []; | ||
| this._executionCount = 0; | ||
| this._rejectionCount = 0; | ||
| } | ||
| #setState; | ||
| #getEnabled; | ||
| #getLimit; | ||
| #getWindow; | ||
| #execute; | ||
| #getRelevantExecutionTimes; | ||
| #cleanupOldExecutions; | ||
| } | ||
| function rateLimit(fn, initialOptions) { | ||
| const rateLimiter = new RateLimiter(fn, initialOptions); | ||
| return rateLimiter.maybeExecute.bind(rateLimiter); | ||
| return rateLimiter.maybeExecute; | ||
| } | ||
@@ -152,0 +119,0 @@ export { |
@@ -1,1 +0,1 @@ | ||
| {"version":3,"file":"rate-limiter.js","sources":["../../src/rate-limiter.ts"],"sourcesContent":["import { parseFunctionOrValue } from './utils'\nimport type { AnyFunction } from './types'\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 * 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?: (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\nconst defaultOptions: Required<RateLimiterOptions<any>> = {\n enabled: true,\n limit: 1,\n onExecute: () => {},\n onReject: () => {},\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 *\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 * @example\n * ```ts\n * const rateLimiter = new RateLimiter(\n * (id: string) => api.getData(id),\n * { limit: 5, window: 1000, windowType: 'sliding' } // 5 calls per second with sliding window\n * );\n *\n * // Will execute immediately until limit reached, then block\n * rateLimiter.maybeExecute('123');\n * ```\n */\nexport class RateLimiter<TFn extends AnyFunction> {\n private _executionCount = 0\n private _rejectionCount = 0\n private _executionTimes: Array<number> = []\n private _options: RateLimiterOptions<TFn>\n\n constructor(\n private fn: TFn,\n initialOptions: RateLimiterOptions<TFn>,\n ) {\n this._options = {\n ...defaultOptions,\n ...initialOptions,\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 /**\n * Returns the current rate limiter options\n */\n getOptions(): Required<RateLimiterOptions<TFn>> {\n return this._options as Required<RateLimiterOptions<TFn>>\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.cleanupOldExecutions()\n\n if (this._options.windowType === 'sliding') {\n // For sliding window, we can execute if we have capacity in the current window\n if (this._executionTimes.length < this.getLimit()) {\n this.execute(...args)\n return true\n }\n } else {\n // For fixed window, we need to check if we're in a new window\n const now = Date.now()\n const oldestExecution = Math.min(...this._executionTimes)\n const isNewWindow = oldestExecution + this.getWindow() <= now\n\n if (isNewWindow || this._executionTimes.length < this.getLimit()) {\n this.execute(...args)\n return true\n }\n }\n\n this.rejectFunction()\n return false\n }\n\n private execute(...args: Parameters<TFn>): void {\n if (!this.getEnabled()) return\n const now = Date.now()\n this._executionCount++\n this._executionTimes.push(now)\n this.fn(...args) // execute the function\n this._options.onExecute?.(this)\n }\n\n private rejectFunction(): void {\n this._rejectionCount++\n if (this._options.onReject) {\n this._options.onReject(this)\n }\n }\n\n private cleanupOldExecutions(): void {\n const now = Date.now()\n const windowStart = now - this.getWindow()\n this._executionTimes = this._executionTimes.filter(\n (time) => time > windowStart,\n )\n }\n\n /**\n * Returns the number of times the function has been executed\n */\n getExecutionCount(): number {\n return this._executionCount\n }\n\n /**\n * Returns the number of times the function has been rejected\n */\n getRejectionCount(): number {\n return this._rejectionCount\n }\n\n /**\n * Returns the number of remaining executions allowed in the current window\n */\n getRemainingInWindow(): number {\n this.cleanupOldExecutions()\n return Math.max(0, this.getLimit() - this._executionTimes.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 = Math.min(...this._executionTimes)\n return oldestExecution + this.getWindow() - Date.now()\n }\n\n /**\n * Resets the rate limiter state\n */\n reset(): void {\n this._executionTimes = []\n this._executionCount = 0\n this._rejectionCount = 0\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 * 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 * 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.bind(rateLimiter)\n}\n"],"names":[],"mappings":"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"} | ||
| {"version":3,"file":"rate-limiter.js","sources":["../../src/rate-limiter.ts"],"sourcesContent":["import { Store } from '@tanstack/store'\nimport { parseFunctionOrValue } from './utils'\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 * Number of function executions that have been rejected due to rate limiting\n */\n rejectionCount: number\n}\n\nfunction getDefaultRateLimiterState(): RateLimiterState {\n return structuredClone({\n executionCount: 0,\n executionTimes: [],\n rejectionCount: 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 * 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?: (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\nconst defaultOptions: Omit<\n Required<RateLimiterOptions<any>>,\n 'initialState' | 'onExecute' | 'onReject'\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 *\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 options: RateLimiterOptions<TFn>\n\n constructor(\n private fn: TFn,\n initialOptions: RateLimiterOptions<TFn>,\n ) {\n this.options = {\n ...defaultOptions,\n ...initialOptions,\n }\n this.#setState(this.options.initialState ?? {})\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 return combinedState\n })\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.#cleanupOldExecutions()\n\n const relevantExecutionTimes = this.#getRelevantExecutionTimes()\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 this.#setState({\n executionCount: this.store.state.executionCount + 1,\n })\n this.options.onExecute?.(this)\n }\n\n #getRelevantExecutionTimes = (): 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 const oldestExecution = Math.min(...this.store.state.executionTimes)\n const windowStart = oldestExecution\n return this.store.state.executionTimes.filter(\n (time) =>\n time >= windowStart && time <= windowStart + this.#getWindow(),\n )\n }\n }\n\n #cleanupOldExecutions = (): void => {\n const now = Date.now()\n const windowStart = now - this.#getWindow()\n this.#setState({\n executionTimes: this.store.state.executionTimes.filter(\n (time) => time > windowStart,\n ),\n })\n }\n\n /**\n * Returns the number of remaining executions allowed in the current window\n */\n getRemainingInWindow = (): number => {\n const relevantExecutionTimes = this.#getRelevantExecutionTimes()\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 }\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 * 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"],"names":[],"mappings":"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"} |
+59
-35
@@ -0,2 +1,29 @@ | ||
| import { Store } from '@tanstack/store'; | ||
| import { AnyFunction } from './types.js'; | ||
| export interface ThrottlerState<TFn extends AnyFunction> { | ||
| /** | ||
| * Number of function executions that have been completed | ||
| */ | ||
| executionCount: number; | ||
| /** | ||
| * The arguments from the most recent call to maybeExecute | ||
| */ | ||
| lastArgs: Parameters<TFn> | undefined; | ||
| /** | ||
| * Timestamp of the last function execution in milliseconds | ||
| */ | ||
| lastExecutionTime: number; | ||
| /** | ||
| * Timestamp when the next execution can occur in milliseconds | ||
| */ | ||
| nextExecutionTime: number; | ||
| /** | ||
| * Whether the throttler is waiting for the timeout to trigger execution | ||
| */ | ||
| isPending: boolean; | ||
| /** | ||
| * Current execution status - 'idle' when not active, 'pending' when waiting for timeout | ||
| */ | ||
| status: 'disabled' | 'idle' | 'pending'; | ||
| } | ||
| /** | ||
@@ -13,2 +40,6 @@ * Options for configuring a throttled function | ||
| /** | ||
| * Initial state for the throttler | ||
| */ | ||
| initialState?: Partial<ThrottlerState<TFn>>; | ||
| /** | ||
| * Whether to execute on the leading edge of the timeout. | ||
@@ -47,2 +78,10 @@ * Defaults to true. | ||
| * | ||
| * State Management: | ||
| * - Uses TanStack Store for reactive state management | ||
| * - Use `initialState` to provide initial state values when creating the throttler | ||
| * - Use `onExecute` callback to react to function execution and implement custom logic | ||
| * - The state includes execution count, last execution time, pending status, and more | ||
| * - State can be accessed via `throttler.store.state` when using the class directly | ||
| * - When using framework adapters (React/Solid), state is accessed from `throttler.state` | ||
| * | ||
| * @example | ||
@@ -63,8 +102,6 @@ * ```ts | ||
| export declare class Throttler<TFn extends AnyFunction> { | ||
| #private; | ||
| private fn; | ||
| private _executionCount; | ||
| private _lastArgs; | ||
| private _lastExecutionTime; | ||
| private _options; | ||
| private _timeoutId; | ||
| readonly store: Store<Readonly<ThrottlerState<TFn>>>; | ||
| options: ThrottlerOptions<TFn>; | ||
| constructor(fn: TFn, initialOptions: ThrottlerOptions<TFn>); | ||
@@ -74,16 +111,4 @@ /** | ||
| */ | ||
| setOptions(newOptions: Partial<ThrottlerOptions<TFn>>): void; | ||
| setOptions: (newOptions: Partial<ThrottlerOptions<TFn>>) => void; | ||
| /** | ||
| * Returns the current throttler options | ||
| */ | ||
| getOptions(): Required<ThrottlerOptions<TFn>>; | ||
| /** | ||
| * Returns the current enabled state of the throttler | ||
| */ | ||
| getEnabled(): boolean; | ||
| /** | ||
| * Returns the current wait time in milliseconds | ||
| */ | ||
| getWait(): number; | ||
| /** | ||
| * Attempts to execute the throttled function. The execution behavior depends on the throttler options: | ||
@@ -110,5 +135,8 @@ * | ||
| */ | ||
| maybeExecute(...args: Parameters<TFn>): void; | ||
| private execute; | ||
| maybeExecute: (...args: Parameters<TFn>) => void; | ||
| /** | ||
| * Processes the current pending execution immediately | ||
| */ | ||
| flush: () => void; | ||
| /** | ||
| * Cancels any pending trailing execution and clears internal state. | ||
@@ -122,19 +150,7 @@ * | ||
| */ | ||
| cancel(): void; | ||
| cancel: () => void; | ||
| /** | ||
| * Returns the last execution time | ||
| * Resets the throttler state to its default values | ||
| */ | ||
| getLastExecutionTime(): number; | ||
| /** | ||
| * Returns the next execution time | ||
| */ | ||
| getNextExecutionTime(): number; | ||
| /** | ||
| * Returns the number of times the function has been executed | ||
| */ | ||
| getExecutionCount(): number; | ||
| /** | ||
| * Returns `true` if there is a pending execution | ||
| */ | ||
| getIsPending(): boolean; | ||
| reset: () => void; | ||
| } | ||
@@ -154,2 +170,10 @@ /** | ||
| * | ||
| * State Management: | ||
| * - Uses TanStack Store for reactive state management | ||
| * - Use `initialState` to provide initial state values when creating the throttler | ||
| * - Use `onExecute` callback to react to function execution and implement custom logic | ||
| * - The state includes execution count, last execution time, pending status, and more | ||
| * - State can be accessed via the underlying Throttler instance's `store.state` property | ||
| * - When using framework adapters (React/Solid), state is accessed from the hook's state property | ||
| * | ||
| * @example | ||
@@ -156,0 +180,0 @@ * ```ts |
+107
-123
@@ -0,7 +1,16 @@ | ||
| import { Store } from "@tanstack/store"; | ||
| import { parseFunctionOrValue } from "./utils.js"; | ||
| function getDefaultThrottlerState() { | ||
| return structuredClone({ | ||
| executionCount: 0, | ||
| isPending: false, | ||
| lastArgs: void 0, | ||
| lastExecutionTime: 0, | ||
| nextExecutionTime: 0, | ||
| status: "idle" | ||
| }); | ||
| } | ||
| const defaultOptions = { | ||
| enabled: true, | ||
| leading: true, | ||
| onExecute: () => { | ||
| }, | ||
| trailing: true, | ||
@@ -13,130 +22,105 @@ wait: 0 | ||
| this.fn = fn; | ||
| this._executionCount = 0; | ||
| this._lastExecutionTime = 0; | ||
| this._options = { | ||
| this.store = new Store( | ||
| getDefaultThrottlerState() | ||
| ); | ||
| this.setOptions = (newOptions) => { | ||
| this.options = { ...this.options, ...newOptions }; | ||
| if (!this.#getEnabled()) { | ||
| this.cancel(); | ||
| } | ||
| }; | ||
| this.#setState = (newState) => { | ||
| this.store.setState((state) => { | ||
| const combinedState = { | ||
| ...state, | ||
| ...newState | ||
| }; | ||
| const { isPending } = combinedState; | ||
| return { | ||
| ...combinedState, | ||
| status: !this.#getEnabled() ? "disabled" : isPending ? "pending" : "idle" | ||
| }; | ||
| }); | ||
| }; | ||
| this.#getEnabled = () => { | ||
| return !!parseFunctionOrValue(this.options.enabled, this); | ||
| }; | ||
| this.#getWait = () => { | ||
| return parseFunctionOrValue(this.options.wait, this); | ||
| }; | ||
| this.maybeExecute = (...args) => { | ||
| const now = Date.now(); | ||
| const timeSinceLastExecution = now - this.store.state.lastExecutionTime; | ||
| const wait = this.#getWait(); | ||
| if (this.options.leading && timeSinceLastExecution >= wait) { | ||
| this.#execute(...args); | ||
| } else { | ||
| this.#setState({ | ||
| lastArgs: args | ||
| }); | ||
| if (!this.#timeoutId && this.options.trailing) { | ||
| const _timeSinceLastExecution = this.store.state.lastExecutionTime ? now - this.store.state.lastExecutionTime : 0; | ||
| const timeoutDuration = wait - _timeSinceLastExecution; | ||
| this.#setState({ isPending: true }); | ||
| this.#timeoutId = setTimeout(() => { | ||
| const { lastArgs } = this.store.state; | ||
| if (lastArgs !== void 0) { | ||
| this.#execute(...lastArgs); | ||
| } | ||
| }, timeoutDuration); | ||
| } | ||
| } | ||
| }; | ||
| this.#execute = (...args) => { | ||
| if (!this.#getEnabled()) return; | ||
| this.fn(...args); | ||
| const lastExecutionTime = Date.now(); | ||
| const nextExecutionTime = lastExecutionTime + this.#getWait(); | ||
| this.#clearTimeout(); | ||
| this.#setState({ | ||
| executionCount: this.store.state.executionCount + 1, | ||
| lastExecutionTime, | ||
| nextExecutionTime, | ||
| isPending: false, | ||
| lastArgs: void 0 | ||
| }); | ||
| this.options.onExecute?.(this); | ||
| }; | ||
| this.flush = () => { | ||
| if (this.store.state.isPending && this.store.state.lastArgs) { | ||
| this.#execute(...this.store.state.lastArgs); | ||
| } | ||
| }; | ||
| this.#clearTimeout = () => { | ||
| if (this.#timeoutId) { | ||
| clearTimeout(this.#timeoutId); | ||
| this.#timeoutId = void 0; | ||
| } | ||
| }; | ||
| this.cancel = () => { | ||
| this.#clearTimeout(); | ||
| this.#setState({ | ||
| lastArgs: void 0, | ||
| isPending: false | ||
| }); | ||
| }; | ||
| this.reset = () => { | ||
| this.#setState(getDefaultThrottlerState()); | ||
| }; | ||
| this.options = { | ||
| ...defaultOptions, | ||
| ...initialOptions | ||
| }; | ||
| this.#setState(this.options.initialState ?? {}); | ||
| } | ||
| /** | ||
| * Updates the throttler options | ||
| */ | ||
| setOptions(newOptions) { | ||
| this._options = { ...this._options, ...newOptions }; | ||
| if (!this._options.enabled) { | ||
| this.cancel(); | ||
| } | ||
| } | ||
| /** | ||
| * Returns the current throttler options | ||
| */ | ||
| getOptions() { | ||
| return this._options; | ||
| } | ||
| /** | ||
| * Returns the current enabled state of the throttler | ||
| */ | ||
| getEnabled() { | ||
| return parseFunctionOrValue(this._options.enabled, this); | ||
| } | ||
| /** | ||
| * Returns the current wait time in milliseconds | ||
| */ | ||
| getWait() { | ||
| return parseFunctionOrValue(this._options.wait, this); | ||
| } | ||
| /** | ||
| * Attempts to execute the throttled function. The execution behavior depends on the throttler options: | ||
| * | ||
| * - If enough time has passed since the last execution (>= wait period): | ||
| * - With leading=true: Executes immediately | ||
| * - With leading=false: Waits for the next trailing execution | ||
| * | ||
| * - If within the wait period: | ||
| * - With trailing=true: Schedules execution for end of wait period | ||
| * - With trailing=false: Drops the execution | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * const throttled = new Throttler(fn, { wait: 1000 }); | ||
| * | ||
| * // First call executes immediately | ||
| * throttled.maybeExecute('a', 'b'); | ||
| * | ||
| * // Call during wait period - gets throttled | ||
| * throttled.maybeExecute('c', 'd'); | ||
| * ``` | ||
| */ | ||
| maybeExecute(...args) { | ||
| const now = Date.now(); | ||
| const timeSinceLastExecution = now - this._lastExecutionTime; | ||
| const wait = this.getWait(); | ||
| if (this._options.leading && timeSinceLastExecution >= wait) { | ||
| this.execute(...args); | ||
| } else { | ||
| this._lastArgs = args; | ||
| if (!this._timeoutId && this._options.trailing) { | ||
| const _timeSinceLastExecution = this._lastExecutionTime ? now - this._lastExecutionTime : 0; | ||
| const timeoutDuration = wait - _timeSinceLastExecution; | ||
| this._timeoutId = setTimeout(() => { | ||
| if (this._lastArgs !== void 0) { | ||
| this.execute(...this._lastArgs); | ||
| } | ||
| }, timeoutDuration); | ||
| } | ||
| } | ||
| } | ||
| execute(...args) { | ||
| if (!this.getEnabled()) return; | ||
| this.fn(...args); | ||
| this._executionCount++; | ||
| this._lastExecutionTime = Date.now(); | ||
| this._timeoutId = void 0; | ||
| this._lastArgs = void 0; | ||
| this._options.onExecute(this); | ||
| } | ||
| /** | ||
| * Cancels any pending trailing execution and clears internal state. | ||
| * | ||
| * If a trailing execution is scheduled (due to throttling with trailing=true), | ||
| * this will prevent that execution from occurring. The internal timeout and | ||
| * stored arguments will be cleared. | ||
| * | ||
| * Has no effect if there is no pending execution. | ||
| */ | ||
| cancel() { | ||
| if (this._timeoutId) { | ||
| clearTimeout(this._timeoutId); | ||
| this._timeoutId = void 0; | ||
| this._lastArgs = void 0; | ||
| } | ||
| } | ||
| /** | ||
| * Returns the last execution time | ||
| */ | ||
| getLastExecutionTime() { | ||
| return this._lastExecutionTime; | ||
| } | ||
| /** | ||
| * Returns the next execution time | ||
| */ | ||
| getNextExecutionTime() { | ||
| return this._lastExecutionTime + this.getWait(); | ||
| } | ||
| /** | ||
| * Returns the number of times the function has been executed | ||
| */ | ||
| getExecutionCount() { | ||
| return this._executionCount; | ||
| } | ||
| /** | ||
| * Returns `true` if there is a pending execution | ||
| */ | ||
| getIsPending() { | ||
| return this.getEnabled() && !!this._timeoutId; | ||
| } | ||
| #timeoutId; | ||
| #setState; | ||
| #getEnabled; | ||
| #getWait; | ||
| #execute; | ||
| #clearTimeout; | ||
| } | ||
| function throttle(fn, initialOptions) { | ||
| const throttler = new Throttler(fn, initialOptions); | ||
| return throttler.maybeExecute.bind(throttler); | ||
| return throttler.maybeExecute; | ||
| } | ||
@@ -143,0 +127,0 @@ export { |
@@ -1,1 +0,1 @@ | ||
| {"version":3,"file":"throttler.js","sources":["../../src/throttler.ts"],"sourcesContent":["import { parseFunctionOrValue } from './utils'\nimport type { AnyFunction } from './types'\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 * 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?: (throttler: Throttler<TFn>) => void\n /**\n * Whether to execute on the trailing edge of the timeout.\n * Defaults to true.\n */\n trailing?: boolean\n /**\n * Time window in milliseconds during which the function can only be executed once.\n * Can be a number or a function that returns a number.\n * Defaults to 0ms\n */\n wait: number | ((throttler: Throttler<TFn>) => number)\n}\n\nconst defaultOptions: Required<ThrottlerOptions<any>> = {\n enabled: true,\n leading: true,\n onExecute: () => {},\n trailing: true,\n wait: 0,\n}\n\n/**\n * A class that creates a throttled function.\n *\n * Throttling ensures a function is called at most once within a specified time window.\n * Unlike debouncing which waits for a pause in calls, throttling guarantees consistent\n * execution timing regardless of call frequency.\n *\n * Supports both leading and trailing edge execution:\n * - Leading: Execute immediately on first call (default: true)\n * - Trailing: Execute after wait period if called during throttle (default: true)\n *\n * For collapsing rapid-fire events where you only care about the last call, consider using Debouncer.\n *\n * @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 private _executionCount = 0\n private _lastArgs: Parameters<TFn> | undefined\n private _lastExecutionTime = 0\n private _options: Required<ThrottlerOptions<TFn>>\n private _timeoutId: NodeJS.Timeout | undefined\n\n constructor(\n private fn: TFn,\n initialOptions: ThrottlerOptions<TFn>,\n ) {\n this._options = {\n ...defaultOptions,\n ...initialOptions,\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 // End the pending state if the debouncer is disabled\n if (!this._options.enabled) {\n this.cancel()\n }\n }\n\n /**\n * Returns the current throttler options\n */\n getOptions(): Required<ThrottlerOptions<TFn>> {\n return this._options\n }\n\n /**\n * Returns the current enabled state of the 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 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 const now = Date.now()\n const timeSinceLastExecution = now - this._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._lastArgs = args\n\n // Set up trailing execution if not already scheduled\n if (!this._timeoutId && this._options.trailing) {\n const _timeSinceLastExecution = this._lastExecutionTime\n ? now - this._lastExecutionTime\n : 0\n const timeoutDuration = wait - _timeSinceLastExecution\n this._timeoutId = setTimeout(() => {\n if (this._lastArgs !== undefined) {\n this.execute(...this._lastArgs)\n }\n }, timeoutDuration)\n }\n }\n }\n\n private execute(...args: Parameters<TFn>): void {\n if (!this.getEnabled()) return\n this.fn(...args) // EXECUTE!\n this._executionCount++\n this._lastExecutionTime = Date.now()\n this._timeoutId = undefined\n this._lastArgs = undefined\n this._options.onExecute(this)\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 if (this._timeoutId) {\n clearTimeout(this._timeoutId)\n this._timeoutId = undefined\n this._lastArgs = undefined\n }\n }\n\n /**\n * Returns the last execution time\n */\n getLastExecutionTime(): number {\n return this._lastExecutionTime\n }\n\n /**\n * Returns the next execution time\n */\n getNextExecutionTime(): number {\n return this._lastExecutionTime + this.getWait()\n }\n\n /**\n * Returns the number of times the function has been executed\n */\n getExecutionCount(): number {\n return this._executionCount\n }\n\n /**\n * Returns `true` if there is a pending execution\n */\n getIsPending(): boolean {\n return this.getEnabled() && !!this._timeoutId\n }\n}\n\n/**\n * Creates a throttled function that limits how often the provided function can execute.\n *\n * Throttling ensures a function executes at most once within a specified time window,\n * regardless of how many times it is called. This is useful for rate-limiting\n * expensive operations or UI updates.\n *\n * The throttled function can be configured to execute on the leading and/or trailing\n * edge of the throttle window via options.\n *\n * For handling bursts of events, consider using debounce() instead. For hard execution\n * limits, consider using rateLimit().\n *\n * @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.bind(throttler)\n}\n"],"names":[],"mappings":";AAmCA,MAAM,iBAAkD;AAAA,EACtD,SAAS;AAAA,EACT,SAAS;AAAA,EACT,WAAW,MAAM;AAAA,EAAC;AAAA,EAClB,UAAU;AAAA,EACV,MAAM;AACR;AA6BO,MAAM,UAAmC;AAAA,EAO9C,YACU,IACR,gBACA;AAFQ,SAAA,KAAA;AAPV,SAAQ,kBAAkB;AAE1B,SAAQ,qBAAqB;AAQ3B,SAAK,WAAW;AAAA,MACd,GAAG;AAAA,MACH,GAAG;AAAA,IACL;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA,EAMF,WAAW,YAAkD;AAC3D,SAAK,WAAW,EAAE,GAAG,KAAK,UAAU,GAAG,WAAW;AAG9C,QAAA,CAAC,KAAK,SAAS,SAAS;AAC1B,WAAK,OAAO;AAAA,IAAA;AAAA,EACd;AAAA;AAAA;AAAA;AAAA,EAMF,aAA8C;AAC5C,WAAO,KAAK;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA,EAMd,aAAsB;AACpB,WAAO,qBAAqB,KAAK,SAAS,SAAS,IAAI;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA,EAMzD,UAAkB;AAChB,WAAO,qBAAqB,KAAK,SAAS,MAAM,IAAI;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAyBtD,gBAAgB,MAA6B;AACrC,UAAA,MAAM,KAAK,IAAI;AACf,UAAA,yBAAyB,MAAM,KAAK;AACpC,UAAA,OAAO,KAAK,QAAQ;AAG1B,QAAI,KAAK,SAAS,WAAW,0BAA0B,MAAM;AACtD,WAAA,QAAQ,GAAG,IAAI;AAAA,IAAA,OACf;AAEL,WAAK,YAAY;AAGjB,UAAI,CAAC,KAAK,cAAc,KAAK,SAAS,UAAU;AAC9C,cAAM,0BAA0B,KAAK,qBACjC,MAAM,KAAK,qBACX;AACJ,cAAM,kBAAkB,OAAO;AAC1B,aAAA,aAAa,WAAW,MAAM;AAC7B,cAAA,KAAK,cAAc,QAAW;AAC3B,iBAAA,QAAQ,GAAG,KAAK,SAAS;AAAA,UAAA;AAAA,WAE/B,eAAe;AAAA,MAAA;AAAA,IACpB;AAAA,EACF;AAAA,EAGM,WAAW,MAA6B;AAC1C,QAAA,CAAC,KAAK,aAAc;AACnB,SAAA,GAAG,GAAG,IAAI;AACV,SAAA;AACA,SAAA,qBAAqB,KAAK,IAAI;AACnC,SAAK,aAAa;AAClB,SAAK,YAAY;AACZ,SAAA,SAAS,UAAU,IAAI;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAY9B,SAAe;AACb,QAAI,KAAK,YAAY;AACnB,mBAAa,KAAK,UAAU;AAC5B,WAAK,aAAa;AAClB,WAAK,YAAY;AAAA,IAAA;AAAA,EACnB;AAAA;AAAA;AAAA;AAAA,EAMF,uBAA+B;AAC7B,WAAO,KAAK;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA,EAMd,uBAA+B;AACtB,WAAA,KAAK,qBAAqB,KAAK,QAAQ;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA,EAMhD,oBAA4B;AAC1B,WAAO,KAAK;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA,EAMd,eAAwB;AACtB,WAAO,KAAK,WAAA,KAAgB,CAAC,CAAC,KAAK;AAAA,EAAA;AAEvC;AA4BgB,SAAA,SACd,IACA,gBACA;AACA,QAAM,YAAY,IAAI,UAAU,IAAI,cAAc;AAC3C,SAAA,UAAU,aAAa,KAAK,SAAS;AAC9C;"} | ||
| {"version":3,"file":"throttler.js","sources":["../../src/throttler.ts"],"sourcesContent":["import { Store } from '@tanstack/store'\nimport { parseFunctionOrValue } from './utils'\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 * 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 * Timestamp when the next execution can occur in milliseconds\n */\n nextExecutionTime: number\n /**\n * Whether the throttler is waiting for the timeout to trigger execution\n */\n isPending: boolean\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 structuredClone({\n executionCount: 0,\n isPending: false,\n lastArgs: undefined,\n lastExecutionTime: 0,\n nextExecutionTime: 0,\n status: 'idle',\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 * 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?: (throttler: Throttler<TFn>) => void\n /**\n * Whether to execute on the trailing edge of the timeout.\n * Defaults to true.\n */\n trailing?: boolean\n /**\n * Time window in milliseconds during which the function can only be executed once.\n * Can be a number or a function that returns a number.\n * Defaults to 0ms\n */\n wait: number | ((throttler: Throttler<TFn>) => number)\n}\n\nconst defaultOptions: Omit<\n Required<ThrottlerOptions<any>>,\n 'initialState' | 'onExecute'\n> = {\n enabled: true,\n leading: true,\n trailing: true,\n wait: 0,\n}\n\n/**\n * A class that creates a throttled function.\n *\n * Throttling ensures a function is called at most once within a specified time window.\n * Unlike debouncing which waits for a pause in calls, throttling guarantees consistent\n * execution timing regardless of call frequency.\n *\n * Supports both leading and trailing edge execution:\n * - Leading: Execute immediately on first call (default: true)\n * - Trailing: Execute after wait period if called during throttle (default: true)\n *\n * For collapsing rapid-fire events where you only care about the last call, consider using Debouncer.\n *\n * State Management:\n * - Uses TanStack Store for reactive state management\n * - Use `initialState` to provide initial state values when creating the throttler\n * - Use `onExecute` callback to react to function execution and implement custom logic\n * - The state includes execution count, last execution time, pending status, and more\n * - State can be accessed via `throttler.store.state` when using the class directly\n * - When using framework adapters (React/Solid), state is accessed from `throttler.state`\n *\n * @example\n * ```ts\n * const throttler = new Throttler(\n * (id: string) => api.getData(id),\n * { wait: 1000 } // Execute at most once per second\n * );\n *\n * // First call executes immediately\n * throttler.maybeExecute('123');\n *\n * // Subsequent calls within 1000ms are throttled\n * throttler.maybeExecute('123'); // Throttled\n * ```\n */\nexport class Throttler<TFn extends AnyFunction> {\n readonly store: Store<Readonly<ThrottlerState<TFn>>> = new Store(\n getDefaultThrottlerState(),\n )\n options: ThrottlerOptions<TFn>\n #timeoutId: NodeJS.Timeout | undefined\n\n constructor(\n private fn: TFn,\n initialOptions: ThrottlerOptions<TFn>,\n ) {\n this.options = {\n ...defaultOptions,\n ...initialOptions,\n }\n this.#setState(this.options.initialState ?? {})\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 }\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 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?.(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.#execute(...this.store.state.lastArgs)\n }\n }\n\n #clearTimeout = (): void => {\n if (this.#timeoutId) {\n clearTimeout(this.#timeoutId)\n this.#timeoutId = undefined\n }\n }\n\n /**\n * Cancels any pending trailing execution and clears internal state.\n *\n * If a trailing execution is scheduled (due to throttling with trailing=true),\n * this will prevent that execution from occurring. The internal timeout and\n * stored arguments will be cleared.\n *\n * Has no effect if there is no pending execution.\n */\n cancel = (): void => {\n this.#clearTimeout()\n this.#setState({\n lastArgs: undefined,\n isPending: false,\n })\n }\n\n /**\n * Resets the throttler state to its default values\n */\n reset = (): void => {\n this.#setState(getDefaultThrottlerState<TFn>())\n }\n}\n\n/**\n * Creates a throttled function that limits how often the provided function can execute.\n *\n * Throttling ensures a function executes at most once within a specified time window,\n * regardless of how many times it is called. This is useful for rate-limiting\n * expensive operations or UI updates.\n *\n * The throttled function can be configured to execute on the leading and/or trailing\n * edge of the throttle window via options.\n *\n * For handling bursts of events, consider using debounce() instead. For hard execution\n * limits, consider using rateLimit().\n *\n * State Management:\n * - Uses TanStack Store for reactive state management\n * - Use `initialState` to provide initial state values when creating the throttler\n * - Use `onExecute` callback to react to function execution and implement custom logic\n * - The state includes execution count, last execution time, pending status, and more\n * - State can be accessed via the underlying Throttler instance's `store.state` property\n * - When using framework adapters (React/Solid), state is accessed from the hook's state property\n *\n * @example\n * ```ts\n * // Basic throttling - max once per second\n * const throttled = throttle(updateUI, { wait: 1000 });\n *\n * // Configure leading/trailing execution\n * const throttled = throttle(saveData, {\n * wait: 2000,\n * leading: true, // Execute immediately on first call\n * trailing: true // Execute again after delay if called during wait\n * });\n * ```\n */\nexport function throttle<TFn extends AnyFunction>(\n fn: TFn,\n initialOptions: ThrottlerOptions<TFn>,\n) {\n const throttler = new Throttler(fn, initialOptions)\n return throttler.maybeExecute\n}\n"],"names":[],"mappings":"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"} |
| import { AnyFunction } from './types.js'; | ||
| export declare function isFunction<T extends AnyFunction>(value: any): value is T; | ||
| export declare function parseFunctionOrValue<T, TArgs extends Array<any>>(value: T | ((...args: TArgs) => T), ...args: TArgs): T; | ||
| export declare function bindInstanceMethods<T extends Record<string, any>>(instance: T): T; |
+0
-13
@@ -7,16 +7,3 @@ function isFunction(value) { | ||
| } | ||
| function bindInstanceMethods(instance) { | ||
| return Object.getOwnPropertyNames(Object.getPrototypeOf(instance)).reduce( | ||
| (acc, key) => { | ||
| const method = instance[key]; | ||
| if (isFunction(method)) { | ||
| acc[key] = method.bind(instance); | ||
| } | ||
| return acc; | ||
| }, | ||
| instance | ||
| ); | ||
| } | ||
| export { | ||
| bindInstanceMethods, | ||
| isFunction, | ||
@@ -23,0 +10,0 @@ parseFunctionOrValue |
@@ -1,1 +0,1 @@ | ||
| {"version":3,"file":"utils.js","sources":["../../src/utils.ts"],"sourcesContent":["import type { AnyFunction } from './types'\n\nexport function isFunction<T extends AnyFunction>(value: any): value is T {\n return typeof value === 'function'\n}\n\nexport function parseFunctionOrValue<T, TArgs extends Array<any>>(\n value: T | ((...args: TArgs) => T),\n ...args: TArgs\n): T {\n return isFunction(value) ? value(...args) : value\n}\n\nexport function bindInstanceMethods<T extends Record<string, any>>(\n instance: T,\n): T {\n return Object.getOwnPropertyNames(Object.getPrototypeOf(instance)).reduce(\n (acc: any, key) => {\n const method = instance[key as keyof T]\n if (isFunction(method)) {\n acc[key] = method.bind(instance)\n }\n return acc\n },\n instance,\n )\n}\n"],"names":[],"mappings":"AAEO,SAAS,WAAkC,OAAwB;AACxE,SAAO,OAAO,UAAU;AAC1B;AAEgB,SAAA,qBACd,UACG,MACA;AACH,SAAO,WAAW,KAAK,IAAI,MAAM,GAAG,IAAI,IAAI;AAC9C;AAEO,SAAS,oBACd,UACG;AACH,SAAO,OAAO,oBAAoB,OAAO,eAAe,QAAQ,CAAC,EAAE;AAAA,IACjE,CAAC,KAAU,QAAQ;AACX,YAAA,SAAS,SAAS,GAAc;AAClC,UAAA,WAAW,MAAM,GAAG;AACtB,YAAI,GAAG,IAAI,OAAO,KAAK,QAAQ;AAAA,MAAA;AAE1B,aAAA;AAAA,IACT;AAAA,IACA;AAAA,EACF;AACF;"} | ||
| {"version":3,"file":"utils.js","sources":["../../src/utils.ts"],"sourcesContent":["import type { AnyFunction } from './types'\n\nexport function isFunction<T extends AnyFunction>(value: any): value is T {\n return typeof value === 'function'\n}\n\nexport function parseFunctionOrValue<T, TArgs extends Array<any>>(\n value: T | ((...args: TArgs) => T),\n ...args: TArgs\n): T {\n return isFunction(value) ? value(...args) : value\n}\n"],"names":[],"mappings":"AAEO,SAAS,WAAkC,OAAwB;AACxE,SAAO,OAAO,UAAU;AAC1B;AAEO,SAAS,qBACd,UACG,MACA;AACH,SAAO,WAAW,KAAK,IAAI,MAAM,GAAG,IAAI,IAAI;AAC9C;"} |
+14
-11
| { | ||
| "name": "@tanstack/pacer", | ||
| "version": "0.8.0", | ||
| "version": "0.9.0", | ||
| "description": "Utilities for debouncing, throttling, rate-limiting, queuing, and more.", | ||
@@ -40,2 +40,12 @@ "author": "Tanner Linsley", | ||
| }, | ||
| "./async-batcher": { | ||
| "import": { | ||
| "types": "./dist/esm/async-batcher.d.ts", | ||
| "default": "./dist/esm/async-batcher.js" | ||
| }, | ||
| "require": { | ||
| "types": "./dist/cjs/async-batcher.d.cts", | ||
| "default": "./dist/cjs/async-batcher.cjs" | ||
| } | ||
| }, | ||
| "./async-debouncer": { | ||
@@ -91,12 +101,2 @@ "import": { | ||
| }, | ||
| "./compare": { | ||
| "import": { | ||
| "types": "./dist/esm/compare.d.ts", | ||
| "default": "./dist/esm/compare.js" | ||
| }, | ||
| "require": { | ||
| "types": "./dist/cjs/compare.d.cts", | ||
| "default": "./dist/cjs/compare.cjs" | ||
| } | ||
| }, | ||
| "./debouncer": { | ||
@@ -165,2 +165,5 @@ "import": { | ||
| ], | ||
| "dependencies": { | ||
| "@tanstack/store": "^0.7.2" | ||
| }, | ||
| "scripts": { | ||
@@ -167,0 +170,0 @@ "clean": "premove ./build ./dist", |
+201
-121
@@ -0,4 +1,60 @@ | ||
| import { Store } from '@tanstack/store' | ||
| import { parseFunctionOrValue } from './utils' | ||
| import type { AnyAsyncFunction, OptionalKeys } from './types' | ||
| export interface AsyncDebouncerState<TFn extends AnyAsyncFunction> { | ||
| /** | ||
| * Whether the debouncer can execute on the leading edge of the timeout | ||
| */ | ||
| canLeadingExecute: boolean | ||
| /** | ||
| * Number of function executions that have resulted in errors | ||
| */ | ||
| errorCount: number | ||
| /** | ||
| * Whether the debounced function is currently executing asynchronously | ||
| */ | ||
| isExecuting: boolean | ||
| /** | ||
| * Whether the debouncer is waiting for the timeout to trigger execution | ||
| */ | ||
| isPending: boolean | ||
| /** | ||
| * The arguments from the most recent call to maybeExecute | ||
| */ | ||
| lastArgs: Parameters<TFn> | undefined | ||
| /** | ||
| * The result from the most recent successful function execution | ||
| */ | ||
| lastResult: ReturnType<TFn> | undefined | ||
| /** | ||
| * Number of function executions that have completed (either successfully or with errors) | ||
| */ | ||
| settleCount: number | ||
| /** | ||
| * Current execution status - 'idle' when not active, 'pending' when waiting, 'executing' when running, 'settled' when completed | ||
| */ | ||
| status: 'disabled' | 'idle' | 'pending' | 'executing' | 'settled' | ||
| /** | ||
| * Number of function executions that have completed successfully | ||
| */ | ||
| successCount: number | ||
| } | ||
| function getDefaultAsyncDebouncerState< | ||
| TFn extends AnyAsyncFunction, | ||
| >(): AsyncDebouncerState<TFn> { | ||
| return structuredClone({ | ||
| canLeadingExecute: true, | ||
| errorCount: 0, | ||
| isExecuting: false, | ||
| isPending: false, | ||
| lastArgs: undefined, | ||
| lastResult: undefined, | ||
| settleCount: 0, | ||
| successCount: 0, | ||
| status: 'idle', | ||
| }) | ||
| } | ||
| /** | ||
@@ -15,2 +71,6 @@ * Options for configuring an async debounced function | ||
| /** | ||
| * Initial state for the async debouncer | ||
| */ | ||
| initialState?: Partial<AsyncDebouncerState<TFn>> | ||
| /** | ||
| * Whether to execute on the leading edge of the timeout. | ||
@@ -55,3 +115,3 @@ * Defaults to false. | ||
| AsyncDebouncerOptions<any>, | ||
| 'onError' | 'onSettled' | 'onSuccess' | ||
| 'initialState' | 'onError' | 'onSettled' | 'onSuccess' | ||
| > | ||
@@ -81,7 +141,15 @@ | ||
| * Error Handling: | ||
| * - If an error occurs during execution and no `onError` handler is provided, the error will be thrown and propagate up to the caller. | ||
| * - If an `onError` handler is provided, errors will be caught and passed to the handler instead of being thrown. | ||
| * - The error count can be tracked using `getErrorCount()`. | ||
| * - The debouncer maintains its state and can continue to be used after an error occurs. | ||
| * - If an `onError` handler is provided, it will be called with the error and debouncer instance | ||
| * - If `throwOnError` is true (default when no onError handler is provided), the error will be thrown | ||
| * - If `throwOnError` is false (default when onError handler is provided), the error will be swallowed | ||
| * - Both onError and throwOnError can be used together - the handler will be called before any error is thrown | ||
| * - The error state can be checked using the underlying store | ||
| * | ||
| * State Management: | ||
| * - The debouncer uses a reactive store for state management | ||
| * - Use `initialState` to provide initial state values when creating the async debouncer | ||
| * - The state includes canLeadingExecute, error count, execution status, and success/settle counts | ||
| * - State can be accessed via the `store` property and its `state` getter | ||
| * - The store is reactive and will notify subscribers of state changes | ||
| * | ||
| * @example | ||
@@ -105,14 +173,9 @@ * ```ts | ||
| export class AsyncDebouncer<TFn extends AnyAsyncFunction> { | ||
| private _options: AsyncDebouncerOptionsWithOptionalCallbacks | ||
| private _abortController: AbortController | null = null | ||
| private _canLeadingExecute = true | ||
| private _errorCount = 0 | ||
| private _isExecuting = false | ||
| private _isPending = false | ||
| private _lastArgs: Parameters<TFn> | undefined | ||
| private _lastResult: ReturnType<TFn> | undefined | ||
| private _settleCount = 0 | ||
| private _successCount = 0 | ||
| private _timeoutId: NodeJS.Timeout | null = null | ||
| private _resolvePreviousPromise: | ||
| readonly store: Store<Readonly<AsyncDebouncerState<TFn>>> = new Store< | ||
| AsyncDebouncerState<TFn> | ||
| >(getDefaultAsyncDebouncerState<TFn>()) | ||
| options: AsyncDebouncerOptions<TFn> | ||
| #abortController: AbortController | null = null | ||
| #timeoutId: NodeJS.Timeout | null = null | ||
| #resolvePreviousPromise: | ||
| | ((value?: ReturnType<TFn> | undefined) => void) | ||
@@ -125,3 +188,3 @@ | null = null | ||
| ) { | ||
| this._options = { | ||
| this.options = { | ||
| ...defaultOptions, | ||
@@ -131,21 +194,37 @@ ...initialOptions, | ||
| } | ||
| this.#setState(this.options.initialState ?? {}) | ||
| } | ||
| /** | ||
| * Updates the debouncer options | ||
| * Updates the async debouncer options | ||
| */ | ||
| setOptions(newOptions: Partial<AsyncDebouncerOptions<TFn>>): void { | ||
| this._options = { ...this._options, ...newOptions } | ||
| setOptions = (newOptions: Partial<AsyncDebouncerOptions<TFn>>): void => { | ||
| this.options = { ...this.options, ...newOptions } | ||
| // End the pending state if the debouncer is disabled | ||
| if (!this._options.enabled) { | ||
| this._isPending = false | ||
| // Cancel pending execution if the debouncer is disabled | ||
| if (!this.#getEnabled()) { | ||
| this.cancel() | ||
| } | ||
| } | ||
| /** | ||
| * Returns the current debouncer options | ||
| */ | ||
| getOptions(): AsyncDebouncerOptions<TFn> { | ||
| return this._options | ||
| #setState = (newState: Partial<AsyncDebouncerState<TFn>>): void => { | ||
| this.store.setState((state) => { | ||
| const combinedState = { | ||
| ...state, | ||
| ...newState, | ||
| } | ||
| const { isPending, isExecuting, settleCount } = combinedState | ||
| return { | ||
| ...combinedState, | ||
| status: !this.#getEnabled() | ||
| ? 'disabled' | ||
| : isPending | ||
| ? 'pending' | ||
| : isExecuting | ||
| ? 'executing' | ||
| : settleCount > 0 | ||
| ? 'settled' | ||
| : 'idle', | ||
| } | ||
| }) | ||
| } | ||
@@ -156,4 +235,4 @@ | ||
| */ | ||
| getEnabled(): boolean { | ||
| return !!parseFunctionOrValue(this._options.enabled, this) | ||
| #getEnabled = (): boolean => { | ||
| return !!parseFunctionOrValue(this.options.enabled, this) | ||
| } | ||
@@ -164,4 +243,4 @@ | ||
| */ | ||
| getWait(): number { | ||
| return parseFunctionOrValue(this._options.wait, this) | ||
| #getWait = (): number => { | ||
| return parseFunctionOrValue(this.options.wait, this) | ||
| } | ||
@@ -183,132 +262,123 @@ | ||
| */ | ||
| async maybeExecute( | ||
| maybeExecute = async ( | ||
| ...args: Parameters<TFn> | ||
| ): Promise<ReturnType<TFn> | undefined> { | ||
| this._cancel() | ||
| this._lastArgs = args | ||
| ): Promise<ReturnType<TFn> | undefined> => { | ||
| if (!this.#getEnabled()) return undefined | ||
| this.#cancelPendingExecution() | ||
| this.#setState({ lastArgs: args }) | ||
| // Handle leading execution | ||
| if (this._options.leading && this._canLeadingExecute) { | ||
| this._canLeadingExecute = false | ||
| await this.execute(...args) | ||
| return this._lastResult | ||
| if (this.options.leading && this.store.state.canLeadingExecute) { | ||
| this.#setState({ canLeadingExecute: false }) | ||
| await this.#execute(...args) | ||
| return this.store.state.lastResult | ||
| } | ||
| // Handle trailing execution | ||
| if (this._options.trailing) { | ||
| this._isPending = true | ||
| if (this.options.trailing && this.#getEnabled()) { | ||
| this.#setState({ isPending: true }) | ||
| } | ||
| return new Promise((resolve) => { | ||
| this._resolvePreviousPromise = resolve | ||
| this._timeoutId = setTimeout(async () => { | ||
| this.#resolvePreviousPromise = resolve | ||
| this.#timeoutId = setTimeout(async () => { | ||
| // Execute trailing if enabled | ||
| if (this._options.trailing && this._lastArgs) { | ||
| await this.execute(...this._lastArgs) | ||
| if (this.options.trailing && this.store.state.lastArgs) { | ||
| await this.#execute(...this.store.state.lastArgs) | ||
| } | ||
| // Reset state and resolve | ||
| this._canLeadingExecute = true | ||
| this._resolvePreviousPromise = null | ||
| resolve(this._lastResult) | ||
| }, this.getWait()) | ||
| this.#setState({ canLeadingExecute: true }) | ||
| this.#resolvePreviousPromise = null | ||
| resolve(this.store.state.lastResult) | ||
| }, this.#getWait()) | ||
| }) | ||
| } | ||
| private async execute( | ||
| #execute = async ( | ||
| ...args: Parameters<TFn> | ||
| ): Promise<ReturnType<TFn> | undefined> { | ||
| if (!this.getEnabled()) return undefined | ||
| this._abortController = new AbortController() | ||
| ): Promise<ReturnType<TFn> | undefined> => { | ||
| if (!this.#getEnabled()) return undefined | ||
| this.#abortController = new AbortController() | ||
| try { | ||
| this._isExecuting = true | ||
| this._lastResult = await this.fn(...args) // EXECUTE! | ||
| this._successCount++ | ||
| this._options.onSuccess?.(this._lastResult!, this) | ||
| this.#setState({ isExecuting: true }) | ||
| const result = await this.fn(...args) // EXECUTE! | ||
| this.#setState({ | ||
| lastResult: result, | ||
| successCount: this.store.state.successCount + 1, | ||
| }) | ||
| this.options.onSuccess?.(result, this) | ||
| } catch (error) { | ||
| this._errorCount++ | ||
| this._options.onError?.(error, this) | ||
| if (this._options.throwOnError) { | ||
| this.#setState({ | ||
| errorCount: this.store.state.errorCount + 1, | ||
| }) | ||
| this.options.onError?.(error, this) | ||
| if (this.options.throwOnError) { | ||
| throw error | ||
| } | ||
| } finally { | ||
| this._isExecuting = false | ||
| this._isPending = false | ||
| this._settleCount++ | ||
| this._abortController = null | ||
| this._options.onSettled?.(this) | ||
| this.#setState({ | ||
| isExecuting: false, | ||
| isPending: false, | ||
| settleCount: this.store.state.settleCount + 1, | ||
| }) | ||
| this.#abortController = null | ||
| this.options.onSettled?.(this) | ||
| } | ||
| return this._lastResult | ||
| return this.store.state.lastResult | ||
| } | ||
| /** | ||
| * Cancel without resetting _canLeadingExecute | ||
| * Processes the current pending execution immediately | ||
| */ | ||
| private _cancel(): void { | ||
| if (this._timeoutId) { | ||
| clearTimeout(this._timeoutId) | ||
| this._timeoutId = null | ||
| flush = (): void => { | ||
| if (this.store.state.isPending && this.store.state.lastArgs) { | ||
| this.#abortExecution() // abort any current execution | ||
| this.#clearTimeout() // clear any existing timeout | ||
| this.#execute(...this.store.state.lastArgs) | ||
| } | ||
| if (this._abortController) { | ||
| this._abortController.abort() | ||
| this._abortController = null | ||
| } | ||
| if (this._resolvePreviousPromise) { | ||
| this._resolvePreviousPromise(this._lastResult) | ||
| this._resolvePreviousPromise = null | ||
| } | ||
| this._lastArgs = undefined | ||
| this._isPending = false | ||
| this._isExecuting = false | ||
| } | ||
| /** | ||
| * Cancels any pending execution or aborts any execution in progress | ||
| */ | ||
| cancel(): void { | ||
| this._canLeadingExecute = true | ||
| this._cancel() | ||
| #clearTimeout = (): void => { | ||
| if (this.#timeoutId) { | ||
| clearTimeout(this.#timeoutId) | ||
| this.#timeoutId = null | ||
| } | ||
| } | ||
| /** | ||
| * Returns the last result of the debounced function | ||
| */ | ||
| getLastResult(): ReturnType<TFn> | undefined { | ||
| return this._lastResult | ||
| #cancelPendingExecution = (): void => { | ||
| this.#clearTimeout() | ||
| if (this.#resolvePreviousPromise) { | ||
| this.#resolvePreviousPromise(this.store.state.lastResult) | ||
| this.#resolvePreviousPromise = null | ||
| } | ||
| this.#setState({ | ||
| isPending: false, | ||
| isExecuting: false, | ||
| lastArgs: undefined, | ||
| }) | ||
| } | ||
| /** | ||
| * Returns the number of times the function has been executed successfully | ||
| */ | ||
| getSuccessCount(): number { | ||
| return this._successCount | ||
| #abortExecution = (): void => { | ||
| if (this.#abortController) { | ||
| this.#abortController.abort() | ||
| this.#abortController = null | ||
| } | ||
| } | ||
| /** | ||
| * Returns the number of times the function has settled (completed or errored) | ||
| * Cancels any pending execution or aborts any execution in progress | ||
| */ | ||
| getSettleCount(): number { | ||
| return this._settleCount | ||
| cancel = (): void => { | ||
| this.#cancelPendingExecution() | ||
| this.#abortExecution() | ||
| this.#setState({ canLeadingExecute: true }) | ||
| } | ||
| /** | ||
| * Returns the number of times the function has errored | ||
| * Resets the debouncer state to its default values | ||
| */ | ||
| getErrorCount(): number { | ||
| return this._errorCount | ||
| reset = (): void => { | ||
| this.#setState(getDefaultAsyncDebouncerState<TFn>()) | ||
| } | ||
| /** | ||
| * Returns `true` if there is a pending execution queued up for trailing execution | ||
| */ | ||
| getIsPending(): boolean { | ||
| return this.getEnabled() && this._isPending | ||
| } | ||
| /** | ||
| * Returns `true` if there is currently an execution in progress | ||
| */ | ||
| getIsExecuting(): boolean { | ||
| return this._isExecuting | ||
| } | ||
| } | ||
@@ -332,2 +402,12 @@ | ||
| * | ||
| * State Management: | ||
| * - Uses TanStack Store for reactive state management | ||
| * - Use `initialState` to provide initial state values when creating the async debouncer | ||
| * - Use `onSuccess` callback to react to successful function execution and implement custom logic | ||
| * - Use `onError` callback to react to function execution errors and implement custom error handling | ||
| * - Use `onSettled` callback to react to function execution completion (success or error) and implement custom logic | ||
| * - The state includes canLeadingExecute, error count, execution status, and success/settle counts | ||
| * - State can be accessed via `asyncDebouncer.store.state` when using the class directly | ||
| * - When using framework adapters (React/Solid), state is accessed from `asyncDebouncer.state` | ||
| * | ||
| * @example | ||
@@ -356,3 +436,3 @@ * ```ts | ||
| const asyncDebouncer = new AsyncDebouncer(fn, initialOptions) | ||
| return asyncDebouncer.maybeExecute.bind(asyncDebouncer) | ||
| return asyncDebouncer.maybeExecute | ||
| } |
+336
-215
@@ -0,1 +1,2 @@ | ||
| import { Store } from '@tanstack/store' | ||
| import { parseFunctionOrValue } from './utils' | ||
@@ -5,2 +6,90 @@ import type { OptionalKeys } from './types' | ||
| export interface AsyncQueuerState<TValue> { | ||
| /** | ||
| * Items currently being processed by the queuer | ||
| */ | ||
| activeItems: Array<TValue> | ||
| /** | ||
| * Number of task executions that have resulted in errors | ||
| */ | ||
| errorCount: number | ||
| /** | ||
| * Number of items that have been removed from the queue due to expiration | ||
| */ | ||
| expirationCount: number | ||
| /** | ||
| * Whether the queuer has no items to process (items array is empty) | ||
| */ | ||
| isEmpty: boolean | ||
| /** | ||
| * Whether the queuer has reached its maximum capacity | ||
| */ | ||
| isFull: boolean | ||
| /** | ||
| * Whether the queuer is not currently processing any items | ||
| */ | ||
| isIdle: boolean | ||
| /** | ||
| * Whether the queuer is active and will process items automatically | ||
| */ | ||
| isRunning: boolean | ||
| /** | ||
| * Timestamps when items were added to the queue for expiration tracking | ||
| */ | ||
| itemTimestamps: Array<number> | ||
| /** | ||
| * Array of items currently waiting to be processed | ||
| */ | ||
| items: Array<TValue> | ||
| /** | ||
| * The result from the most recent task execution | ||
| */ | ||
| lastResult: any | ||
| /** | ||
| * Whether the queuer has a pending timeout for processing the next item | ||
| */ | ||
| pendingTick: boolean | ||
| /** | ||
| * Number of items that have been rejected from being added to the queue | ||
| */ | ||
| rejectionCount: number | ||
| /** | ||
| * Number of task executions that have completed (either successfully or with errors) | ||
| */ | ||
| settledCount: number | ||
| /** | ||
| * Number of items currently in the queue | ||
| */ | ||
| size: number | ||
| /** | ||
| * Current processing status - 'idle' when not processing, 'running' when active, 'stopped' when paused | ||
| */ | ||
| status: 'idle' | 'running' | 'stopped' | ||
| /** | ||
| * Number of task executions that have completed successfully | ||
| */ | ||
| successCount: number | ||
| } | ||
| function getDefaultAsyncQueuerState<TValue>(): AsyncQueuerState<TValue> { | ||
| return structuredClone({ | ||
| activeItems: [], | ||
| errorCount: 0, | ||
| expirationCount: 0, | ||
| isEmpty: true, | ||
| isFull: false, | ||
| isIdle: true, | ||
| isRunning: true, | ||
| itemTimestamps: [], | ||
| items: [], | ||
| lastResult: null, | ||
| pendingTick: false, | ||
| rejectionCount: 0, | ||
| settledCount: 0, | ||
| size: 0, | ||
| status: 'idle', | ||
| successCount: 0, | ||
| }) | ||
| } | ||
| export interface AsyncQueuerOptions<TValue> { | ||
@@ -44,2 +133,6 @@ /** | ||
| /** | ||
| * Initial state for the async queuer | ||
| */ | ||
| initialState?: Partial<AsyncQueuerState<TValue>> | ||
| /** | ||
| * Maximum number of items allowed in the queuer | ||
@@ -59,6 +152,2 @@ */ | ||
| /** | ||
| * Callback fired whenever the queuer's running state changes | ||
| */ | ||
| onIsRunningChange?: (queuer: AsyncQueuer<TValue>) => void | ||
| /** | ||
| * Callback fired whenever an item is added or removed from the queuer | ||
@@ -99,2 +188,3 @@ */ | ||
| Required<AsyncQueuerOptions<any>>, | ||
| | 'initialState' | ||
| | 'throwOnError' | ||
@@ -105,3 +195,2 @@ | 'onSuccess' | ||
| | 'onItemsChange' | ||
| | 'onIsRunningChange' | ||
| | 'onExpire' | ||
@@ -146,2 +235,15 @@ | 'onError' | ||
| * | ||
| * State Management: | ||
| * - Uses TanStack Store for reactive state management | ||
| * - Use `initialState` to provide initial state values when creating the async queuer | ||
| * - Use `onSuccess` callback to react to successful task execution and implement custom logic | ||
| * - Use `onError` callback to react to task execution errors and implement custom error handling | ||
| * - Use `onSettled` callback to react to task execution completion (success or error) and implement custom logic | ||
| * - Use `onItemsChange` callback to react to items being added or removed from the queue | ||
| * - Use `onExpire` callback to react to items expiring and implement custom logic | ||
| * - Use `onReject` callback to react to items being rejected when the queue is full | ||
| * - The state includes error count, expiration count, rejection count, running status, and success/settle counts | ||
| * - State can be accessed via `asyncQueuer.store.state` when using the class directly | ||
| * - When using framework adapters (React/Solid), state is accessed from `asyncQueuer.state` | ||
| * | ||
| * Example usage: | ||
@@ -164,20 +266,13 @@ * ```ts | ||
| export class AsyncQueuer<TValue> { | ||
| private _options: AsyncQueuerOptionsWithOptionalCallbacks | ||
| private _activeItems: Set<TValue> = new Set() | ||
| private _successCount = 0 | ||
| private _errorCount = 0 | ||
| private _settledCount = 0 | ||
| private _rejectionCount = 0 | ||
| private _expirationCount = 0 | ||
| private _items: Array<TValue> = [] | ||
| private _itemTimestamps: Array<number> = [] | ||
| private _pendingTick = false | ||
| private _running: boolean | ||
| private _lastResult: any | ||
| readonly store: Store<Readonly<AsyncQueuerState<TValue>>> = new Store< | ||
| AsyncQueuerState<TValue> | ||
| >(getDefaultAsyncQueuerState<TValue>()) | ||
| options: AsyncQueuerOptions<TValue> | ||
| #timeoutIds: Set<NodeJS.Timeout> = new Set() | ||
| constructor( | ||
| private fn: (value: TValue) => Promise<any>, | ||
| initialOptions: AsyncQueuerOptions<TValue>, | ||
| private fn: (item: TValue) => Promise<any>, | ||
| initialOptions: AsyncQueuerOptions<TValue> = {}, | ||
| ) { | ||
| this._options = { | ||
| this.options = { | ||
| ...defaultOptions, | ||
@@ -187,8 +282,19 @@ ...initialOptions, | ||
| } | ||
| this._running = this._options.started | ||
| const isInitiallyRunning = | ||
| this.options.initialState?.isRunning ?? this.options.started ?? true | ||
| this.#setState({ | ||
| ...this.options.initialState, | ||
| isRunning: isInitiallyRunning, | ||
| }) | ||
| for (let i = 0; i < this._options.initialItems.length; i++) { | ||
| const item = this._options.initialItems[i]! | ||
| const isLast = i === this._options.initialItems.length - 1 | ||
| this.addItem(item, this._options.addItemsTo, isLast) | ||
| if (this.options.initialState?.items) { | ||
| if (this.store.state.isRunning) { | ||
| this.#tick() | ||
| } | ||
| } else { | ||
| for (let i = 0; i < (this.options.initialItems?.length ?? 0); i++) { | ||
| const item = this.options.initialItems![i]! | ||
| const isLast = i === (this.options.initialItems?.length ?? 0) - 1 | ||
| this.addItem(item, this.options.addItemsTo ?? 'back', isLast) | ||
| } | ||
| } | ||
@@ -200,11 +306,31 @@ } | ||
| */ | ||
| setOptions(newOptions: Partial<AsyncQueuerOptions<TValue>>): void { | ||
| this._options = { ...this._options, ...newOptions } | ||
| setOptions = (newOptions: Partial<AsyncQueuerOptions<TValue>>): void => { | ||
| this.options = { ...this.options, ...newOptions } | ||
| } | ||
| /** | ||
| * Returns the current queuer options, including defaults and any overrides. | ||
| */ | ||
| getOptions(): AsyncQueuerOptions<TValue> { | ||
| return this._options | ||
| #setState = (newState: Partial<AsyncQueuerState<TValue>>): void => { | ||
| this.store.setState((state) => { | ||
| const combinedState = { | ||
| ...state, | ||
| ...newState, | ||
| } | ||
| const { activeItems, items, isRunning } = combinedState | ||
| const size = items.length | ||
| const isFull = size >= (this.options.maxSize ?? Infinity) | ||
| const isEmpty = size === 0 | ||
| const isIdle = isRunning && isEmpty && activeItems.length === 0 | ||
| const status = isIdle ? 'idle' : isRunning ? 'running' : 'stopped' | ||
| return { | ||
| ...combinedState, | ||
| isEmpty, | ||
| isFull, | ||
| isIdle, | ||
| size, | ||
| status, | ||
| } | ||
| }) | ||
| } | ||
@@ -216,4 +342,4 @@ | ||
| */ | ||
| getWait(): number { | ||
| return parseFunctionOrValue(this._options.wait, this) | ||
| #getWait = (): number => { | ||
| return parseFunctionOrValue(this.options.wait ?? 0, this) | ||
| } | ||
@@ -225,4 +351,4 @@ | ||
| */ | ||
| getConcurrency(): number { | ||
| return parseFunctionOrValue(this._options.concurrency, this) | ||
| #getConcurrency = (): number => { | ||
| return parseFunctionOrValue(this.options.concurrency ?? 1, this) | ||
| } | ||
@@ -233,15 +359,17 @@ | ||
| */ | ||
| private tick() { | ||
| if (!this._running) { | ||
| this._pendingTick = false | ||
| #tick = () => { | ||
| if (!this.store.state.isRunning) { | ||
| this.#setState({ pendingTick: false }) | ||
| return | ||
| } | ||
| this.#setState({ pendingTick: true }) | ||
| // Check for expired items | ||
| this.checkExpiredItems() | ||
| this.#checkExpiredItems() | ||
| // Process items concurrently up to the concurrency limit | ||
| const activeItems = this.store.state.activeItems | ||
| while ( | ||
| this._activeItems.size < this.getConcurrency() && | ||
| !this.getIsEmpty() | ||
| activeItems.length < this.#getConcurrency() && | ||
| !this.store.state.isEmpty | ||
| ) { | ||
@@ -252,65 +380,25 @@ const nextItem = this.peekNextItem() | ||
| } | ||
| this._activeItems.add(nextItem) | ||
| this._options.onItemsChange?.(this) | ||
| activeItems.push(nextItem) | ||
| this.#setState({ | ||
| activeItems, | ||
| }) | ||
| ;(async () => { | ||
| this._lastResult = await this.execute() | ||
| const result = await this.execute() | ||
| this.#setState({ lastResult: result }) | ||
| const wait = this.getWait() | ||
| const wait = this.#getWait() | ||
| if (wait > 0) { | ||
| setTimeout(() => this.tick(), wait) | ||
| const timeoutId = setTimeout(() => this.#tick(), wait) | ||
| this.#timeoutIds.add(timeoutId) | ||
| return | ||
| } | ||
| this.tick() | ||
| this.#tick() | ||
| })() | ||
| } | ||
| this._pendingTick = false | ||
| this.#setState({ pendingTick: false }) | ||
| } | ||
| /** | ||
| * Starts processing items in the queue. If already running, does nothing. | ||
| */ | ||
| start(): void { | ||
| this._running = true | ||
| if (!this._pendingTick && !this.getIsEmpty()) { | ||
| this._pendingTick = true | ||
| this.tick() | ||
| } | ||
| this._options.onIsRunningChange?.(this) | ||
| } | ||
| /** | ||
| * Stops processing items in the queue. Does not clear the queue. | ||
| */ | ||
| stop(): void { | ||
| this._running = false | ||
| this._pendingTick = false | ||
| this._options.onIsRunningChange?.(this) | ||
| } | ||
| /** | ||
| * Removes all pending items from the queue. Does not affect active tasks. | ||
| */ | ||
| clear(): void { | ||
| this._items = [] | ||
| this._options.onItemsChange?.(this) | ||
| } | ||
| /** | ||
| * Resets the queuer to its initial state. Optionally repopulates with initial items. | ||
| * Does not affect callbacks or options. | ||
| */ | ||
| reset(withInitialItems?: boolean): void { | ||
| this.clear() | ||
| this._successCount = 0 | ||
| this._errorCount = 0 | ||
| this._settledCount = 0 | ||
| if (withInitialItems) { | ||
| this._items = [...this._options.initialItems] | ||
| } | ||
| this._running = this._options.started | ||
| } | ||
| /** | ||
| * Adds an item to the queue. If the queue is full, the item is rejected and onReject is called. | ||
@@ -325,11 +413,13 @@ * Items can be inserted based on priority or at the front/back depending on configuration. | ||
| */ | ||
| addItem( | ||
| item: TValue & { priority?: number }, | ||
| position: QueuePosition = this._options.addItemsTo, | ||
| addItem = ( | ||
| item: TValue, | ||
| position: QueuePosition = this.options.addItemsTo ?? 'back', | ||
| runOnItemsChange: boolean = true, | ||
| ): void { | ||
| if (this.getIsFull()) { | ||
| this._rejectionCount++ | ||
| this._options.onReject?.(item, this) | ||
| return | ||
| ): boolean => { | ||
| if (this.store.state.isFull) { | ||
| this.#setState({ | ||
| rejectionCount: this.store.state.rejectionCount + 1, | ||
| }) | ||
| this.options.onReject?.(item, this) | ||
| return false | ||
| } | ||
@@ -339,12 +429,15 @@ | ||
| const priority = | ||
| this._options.getPriority !== defaultOptions.getPriority | ||
| ? this._options.getPriority(item) | ||
| : item.priority | ||
| this.options.getPriority !== defaultOptions.getPriority | ||
| ? this.options.getPriority!(item) | ||
| : (item as any).priority | ||
| const items = this.store.state.items | ||
| const itemTimestamps = this.store.state.itemTimestamps | ||
| if (priority !== undefined) { | ||
| // Insert based on priority - higher priority items go to front | ||
| const insertIndex = this._items.findIndex((existing) => { | ||
| const insertIndex = items.findIndex((existing) => { | ||
| const existingPriority = | ||
| this._options.getPriority !== defaultOptions.getPriority | ||
| ? this._options.getPriority(existing) | ||
| this.options.getPriority !== defaultOptions.getPriority | ||
| ? this.options.getPriority!(existing) | ||
| : (existing as any).priority | ||
@@ -355,7 +448,7 @@ return existingPriority < priority | ||
| if (insertIndex === -1) { | ||
| this._items.push(item) | ||
| this._itemTimestamps.push(Date.now()) | ||
| items.push(item) | ||
| itemTimestamps.push(Date.now()) | ||
| } else { | ||
| this._items.splice(insertIndex, 0, item) | ||
| this._itemTimestamps.splice(insertIndex, 0, Date.now()) | ||
| items.splice(insertIndex, 0, item) | ||
| itemTimestamps.splice(insertIndex, 0, Date.now()) | ||
| } | ||
@@ -365,19 +458,25 @@ } else { | ||
| // Default FIFO/LIFO behavior | ||
| this._items.unshift(item) | ||
| this._itemTimestamps.unshift(Date.now()) | ||
| items.unshift(item) | ||
| itemTimestamps.unshift(Date.now()) | ||
| } else { | ||
| // LIFO | ||
| this._items.push(item) | ||
| this._itemTimestamps.push(Date.now()) | ||
| items.push(item) | ||
| itemTimestamps.push(Date.now()) | ||
| } | ||
| } | ||
| this.#setState({ | ||
| items, | ||
| itemTimestamps, | ||
| }) | ||
| if (runOnItemsChange) { | ||
| this._options.onItemsChange?.(this) | ||
| this.options.onItemsChange?.(this) | ||
| } | ||
| if (this._running && !this._pendingTick) { | ||
| this._pendingTick = true | ||
| this.tick() | ||
| if (this.store.state.isRunning && !this.store.state.pendingTick) { | ||
| this.#tick() | ||
| } | ||
| return true | ||
| } | ||
@@ -397,17 +496,28 @@ | ||
| */ | ||
| getNextItem( | ||
| position: QueuePosition = this._options.getItemsFrom, | ||
| ): TValue | undefined { | ||
| getNextItem = ( | ||
| position: QueuePosition = this.options.getItemsFrom ?? 'front', | ||
| ): TValue | undefined => { | ||
| const { items, itemTimestamps } = this.store.state | ||
| let item: TValue | undefined | ||
| if (position === 'front') { | ||
| item = this._items.shift() | ||
| this._itemTimestamps.shift() | ||
| item = items[0] | ||
| if (item !== undefined) { | ||
| this.#setState({ | ||
| items: items.slice(1), | ||
| itemTimestamps: itemTimestamps.slice(1), | ||
| }) | ||
| } | ||
| } else { | ||
| item = this._items.pop() | ||
| this._itemTimestamps.pop() | ||
| item = items[items.length - 1] | ||
| if (item !== undefined) { | ||
| this.#setState({ | ||
| items: items.slice(0, -1), | ||
| itemTimestamps: itemTimestamps.slice(0, -1), | ||
| }) | ||
| } | ||
| } | ||
| if (item !== undefined) { | ||
| this._options.onItemsChange?.(this) | ||
| this.options.onItemsChange?.(this) | ||
| } | ||
@@ -428,20 +538,28 @@ | ||
| */ | ||
| async execute(position?: QueuePosition): Promise<any> { | ||
| execute = async (position?: QueuePosition): Promise<any> => { | ||
| const item = this.getNextItem(position) | ||
| if (item !== undefined) { | ||
| try { | ||
| this._lastResult = await this.fn(item) | ||
| this._successCount++ | ||
| this._options.onSuccess?.(this._lastResult, this) | ||
| const lastResult = await this.fn(item) | ||
| this.#setState({ | ||
| successCount: this.store.state.successCount + 1, | ||
| lastResult, | ||
| }) | ||
| this.options.onSuccess?.(lastResult, this) | ||
| } catch (error) { | ||
| this._errorCount++ | ||
| this._options.onError?.(error, this) | ||
| if (this._options.throwOnError) { | ||
| this.#setState({ | ||
| errorCount: this.store.state.errorCount + 1, | ||
| }) | ||
| this.options.onError?.(error, this) | ||
| if (this.options.throwOnError) { | ||
| throw error | ||
| } | ||
| } finally { | ||
| this._settledCount++ | ||
| this._activeItems.delete(item) | ||
| this._options.onItemsChange?.(this) | ||
| this._options.onSettled?.(this) | ||
| this.#setState({ | ||
| activeItems: this.store.state.activeItems.filter( | ||
| (activeItem) => activeItem !== item, | ||
| ), | ||
| settledCount: this.store.state.settledCount + 1, | ||
| }) | ||
| this.options.onSettled?.(this) | ||
| } | ||
@@ -453,11 +571,26 @@ } | ||
| /** | ||
| * Processes a specified number of items to execute immediately with no wait time | ||
| * If no numberOfItems is provided, all items will be processed | ||
| */ | ||
| flush = ( | ||
| numberOfItems: number = this.store.state.items.length, | ||
| position?: QueuePosition, | ||
| ): void => { | ||
| this.#clearTimeouts() // clear any pending timeouts | ||
| for (let i = 0; i < numberOfItems; i++) { | ||
| this.execute(position) | ||
| } | ||
| } | ||
| /** | ||
| * Checks for expired items in the queue and removes them. Calls onExpire for each expired item. | ||
| * Internal use only. | ||
| */ | ||
| private checkExpiredItems(): void { | ||
| #checkExpiredItems = (): void => { | ||
| if ( | ||
| this._options.expirationDuration === Infinity && | ||
| this._options.getIsExpired === defaultOptions.getIsExpired | ||
| ) | ||
| (this.options.expirationDuration ?? Infinity) === Infinity && | ||
| this.options.getIsExpired === defaultOptions.getIsExpired | ||
| ) { | ||
| return | ||
| } | ||
@@ -468,13 +601,13 @@ const now = Date.now() | ||
| // Find indices of expired items | ||
| for (let i = 0; i < this._items.length; i++) { | ||
| const timestamp = this._itemTimestamps[i] | ||
| for (let i = 0; i < this.store.state.size; i++) { | ||
| const timestamp = this.store.state.itemTimestamps[i] | ||
| if (timestamp === undefined) continue | ||
| const item = this._items[i] | ||
| const item = this.store.state.items[i] | ||
| if (item === undefined) continue | ||
| const isExpired = | ||
| this._options.getIsExpired !== defaultOptions.getIsExpired | ||
| ? this._options.getIsExpired(item, timestamp) | ||
| : now - timestamp > this._options.expirationDuration | ||
| this.options.getIsExpired !== defaultOptions.getIsExpired | ||
| ? this.options.getIsExpired!(item, timestamp) | ||
| : now - timestamp > (this.options.expirationDuration ?? Infinity) | ||
@@ -491,13 +624,19 @@ if (isExpired) { | ||
| const expiredItem = this._items[index] | ||
| const expiredItem = this.store.state.items[index] | ||
| if (expiredItem === undefined) continue | ||
| this._items.splice(index, 1) | ||
| this._itemTimestamps.splice(index, 1) | ||
| this._expirationCount++ | ||
| this._options.onExpire?.(expiredItem, this) | ||
| const newItems = [...this.store.state.items] | ||
| const newTimestamps = [...this.store.state.itemTimestamps] | ||
| newItems.splice(index, 1) | ||
| newTimestamps.splice(index, 1) | ||
| this.#setState({ | ||
| items: newItems, | ||
| itemTimestamps: newTimestamps, | ||
| expirationCount: this.store.state.expirationCount + 1, | ||
| }) | ||
| this.options.onExpire?.(expiredItem, this) | ||
| } | ||
| if (expiredIndices.length > 0) { | ||
| this._options.onItemsChange?.(this) | ||
| this.options.onItemsChange?.(this) | ||
| } | ||
@@ -515,34 +654,13 @@ } | ||
| */ | ||
| peekNextItem(position: QueuePosition = 'front'): TValue | undefined { | ||
| peekNextItem = (position: QueuePosition = 'front'): TValue | undefined => { | ||
| if (position === 'front') { | ||
| return this._items[0] | ||
| return this.store.state.items[0] | ||
| } | ||
| return this._items[this._items.length - 1] | ||
| return this.store.state.items[this.store.state.size - 1] | ||
| } | ||
| /** | ||
| * Returns true if the queue is empty (no pending items). | ||
| */ | ||
| getIsEmpty(): boolean { | ||
| return this._items.length === 0 | ||
| } | ||
| /** | ||
| * Returns true if the queue is full (reached maxSize). | ||
| */ | ||
| getIsFull(): boolean { | ||
| return this._items.length >= this._options.maxSize | ||
| } | ||
| /** | ||
| * Returns the number of pending items in the queue. | ||
| */ | ||
| getSize(): number { | ||
| return this._items.length | ||
| } | ||
| /** | ||
| * Returns a copy of all items in the queue, including active and pending items. | ||
| */ | ||
| peekAllItems(): Array<TValue> { | ||
| peekAllItems = (): Array<TValue> => { | ||
| return [...this.peekActiveItems(), ...this.peekPendingItems()] | ||
@@ -554,4 +672,4 @@ } | ||
| */ | ||
| peekActiveItems(): Array<TValue> { | ||
| return Array.from(this._activeItems) | ||
| peekActiveItems = (): Array<TValue> => { | ||
| return [...this.store.state.activeItems] | ||
| } | ||
@@ -562,54 +680,44 @@ | ||
| */ | ||
| peekPendingItems(): Array<TValue> { | ||
| return [...this._items] | ||
| peekPendingItems = (): Array<TValue> => { | ||
| return [...this.store.state.items] | ||
| } | ||
| /** | ||
| * Returns the number of items that have been successfully processed. | ||
| * Starts processing items in the queue. If already running, does nothing. | ||
| */ | ||
| getSuccessCount(): number { | ||
| return this._successCount | ||
| start = (): void => { | ||
| this.#setState({ isRunning: true }) | ||
| if (!this.store.state.pendingTick && !this.store.state.isEmpty) { | ||
| this.#tick() | ||
| } | ||
| } | ||
| /** | ||
| * Returns the number of items that have failed processing. | ||
| * Stops processing items in the queue. Does not clear the queue. | ||
| */ | ||
| getErrorCount(): number { | ||
| return this._errorCount | ||
| stop = (): void => { | ||
| this.#clearTimeouts() | ||
| this.#setState({ isRunning: false, pendingTick: false }) | ||
| } | ||
| /** | ||
| * Returns the number of items that have completed processing (success or error). | ||
| */ | ||
| getSettledCount(): number { | ||
| return this._settledCount | ||
| #clearTimeouts = (): void => { | ||
| this.#timeoutIds.forEach((timeoutId) => clearTimeout(timeoutId)) | ||
| this.#timeoutIds.clear() | ||
| } | ||
| /** | ||
| * Returns the number of items that have been rejected from being added to the queue. | ||
| * Removes all pending items from the queue. Does not affect active tasks. | ||
| */ | ||
| getRejectionCount(): number { | ||
| return this._rejectionCount | ||
| clear = (): void => { | ||
| this.#setState({ items: [], itemTimestamps: [] }) | ||
| this.options.onItemsChange?.(this) | ||
| } | ||
| /** | ||
| * Returns true if the queuer is currently running (processing items). | ||
| * Resets the queuer state to its default values | ||
| */ | ||
| getIsRunning(): boolean { | ||
| return this._running | ||
| reset = (): void => { | ||
| this.#setState(getDefaultAsyncQueuerState<TValue>()) | ||
| this.options.onItemsChange?.(this) | ||
| } | ||
| /** | ||
| * Returns true if the queuer is running but has no items to process and no active tasks. | ||
| */ | ||
| getIsIdle(): boolean { | ||
| return this._running && this.getIsEmpty() && this._activeItems.size === 0 | ||
| } | ||
| /** | ||
| * Returns the number of items that have expired and been removed from the queue. | ||
| */ | ||
| getExpirationCount(): number { | ||
| return this._expirationCount | ||
| } | ||
| } | ||
@@ -628,2 +736,15 @@ | ||
| * | ||
| * State Management: | ||
| * - Uses TanStack Store for reactive state management | ||
| * - Use `initialState` to provide initial state values when creating the async queuer | ||
| * - Use `onSuccess` callback to react to successful task execution and implement custom logic | ||
| * - Use `onError` callback to react to task execution errors and implement custom error handling | ||
| * - Use `onSettled` callback to react to task execution completion (success or error) and implement custom logic | ||
| * - Use `onItemsChange` callback to react to items being added or removed from the queue | ||
| * - Use `onExpire` callback to react to items expiring and implement custom logic | ||
| * - Use `onReject` callback to react to items being rejected when the queue is full | ||
| * - The state includes error count, expiration count, rejection count, running status, and success/settle counts | ||
| * - State can be accessed via the underlying AsyncQueuer instance's `store.state` property | ||
| * - When using framework adapters (React/Solid), state is accessed from the hook's state property | ||
| * | ||
| * Example usage: | ||
@@ -643,3 +764,3 @@ * ```ts | ||
| const asyncQueuer = new AsyncQueuer<TValue>(fn, initialOptions) | ||
| return asyncQueuer.addItem.bind(asyncQueuer) | ||
| return asyncQueuer.addItem | ||
| } |
+176
-136
@@ -0,4 +1,50 @@ | ||
| import { Store } from '@tanstack/store' | ||
| import { parseFunctionOrValue } from './utils' | ||
| import type { AnyAsyncFunction, OptionalKeys } from './types' | ||
| import type { AnyAsyncFunction } from './types' | ||
| export interface AsyncRateLimiterState<TFn extends AnyAsyncFunction> { | ||
| /** | ||
| * Number of function executions that have resulted in errors | ||
| */ | ||
| errorCount: number | ||
| /** | ||
| * Array of timestamps when executions occurred for rate limiting calculations | ||
| */ | ||
| executionTimes: Array<number> | ||
| /** | ||
| * Whether the rate-limited function is currently executing asynchronously | ||
| */ | ||
| isExecuting: boolean | ||
| /** | ||
| * The result from the most recent successful function execution | ||
| */ | ||
| lastResult: ReturnType<TFn> | undefined | ||
| /** | ||
| * Number of function executions that have been rejected due to rate limiting | ||
| */ | ||
| rejectionCount: number | ||
| /** | ||
| * Number of function executions that have completed (either successfully or with errors) | ||
| */ | ||
| settleCount: number | ||
| /** | ||
| * Number of function executions that have completed successfully | ||
| */ | ||
| successCount: number | ||
| } | ||
| function getDefaultAsyncRateLimiterState< | ||
| TFn extends AnyAsyncFunction, | ||
| >(): AsyncRateLimiterState<TFn> { | ||
| return { | ||
| errorCount: 0, | ||
| executionTimes: [], | ||
| isExecuting: false, | ||
| lastResult: undefined, | ||
| rejectionCount: 0, | ||
| settleCount: 0, | ||
| successCount: 0, | ||
| } | ||
| } | ||
| /** | ||
@@ -15,2 +61,6 @@ * Options for configuring an async rate-limited function | ||
| /** | ||
| * Initial state for the rate limiter | ||
| */ | ||
| initialState?: Partial<AsyncRateLimiterState<TFn>> | ||
| /** | ||
| * Maximum number of executions allowed within the time window. | ||
@@ -61,13 +111,11 @@ * Can be a number or a function that returns a number. | ||
| type AsyncRateLimiterOptionsWithOptionalCallbacks = OptionalKeys< | ||
| AsyncRateLimiterOptions<any>, | ||
| 'onError' | 'onReject' | 'onSettled' | 'onSuccess' | ||
| > | ||
| const defaultOptions: Omit< | ||
| AsyncRateLimiterOptionsWithOptionalCallbacks, | ||
| 'limit' | 'window' | ||
| Required<AsyncRateLimiterOptions<any>>, | ||
| 'initialState' | 'onError' | 'onReject' | 'onSettled' | 'onSuccess' | ||
| > = { | ||
| enabled: true, | ||
| limit: 1, | ||
| window: 0, | ||
| windowType: 'fixed', | ||
| throwOnError: true, | ||
| } | ||
@@ -99,2 +147,14 @@ | ||
| * | ||
| * State Management: | ||
| * - Uses TanStack Store for reactive state management | ||
| * - Use `initialState` to provide initial state values when creating the rate limiter | ||
| * - `initialState` can be a partial state object | ||
| * - Use `onSuccess` callback to react to successful function execution and implement custom logic | ||
| * - Use `onError` callback to react to function execution errors and implement custom error handling | ||
| * - Use `onSettled` callback to react to function execution completion (success or error) and implement custom logic | ||
| * - Use `onReject` callback to react to executions being rejected when rate limit is exceeded | ||
| * - The state includes execution times, success/error counts, and current execution status | ||
| * - State can be accessed via `asyncRateLimiter.store.state` when using the class directly | ||
| * - When using framework adapters (React/Solid), state is accessed from `asyncRateLimiter.state` | ||
| * | ||
| * Error Handling: | ||
@@ -131,10 +191,6 @@ * - If an `onError` handler is provided, it will be called with the error and rate limiter instance | ||
| export class AsyncRateLimiter<TFn extends AnyAsyncFunction> { | ||
| private _options: AsyncRateLimiterOptionsWithOptionalCallbacks | ||
| private _errorCount = 0 | ||
| private _executionTimes: Array<number> = [] | ||
| private _lastResult: ReturnType<TFn> | undefined | ||
| private _rejectionCount = 0 | ||
| private _settleCount = 0 | ||
| private _successCount = 0 | ||
| private _isExecuting = false | ||
| readonly store: Store<Readonly<AsyncRateLimiterState<TFn>>> = new Store< | ||
| AsyncRateLimiterState<TFn> | ||
| >(getDefaultAsyncRateLimiterState<TFn>()) | ||
| options: AsyncRateLimiterOptions<TFn> | ||
@@ -145,3 +201,3 @@ constructor( | ||
| ) { | ||
| this._options = { | ||
| this.options = { | ||
| ...defaultOptions, | ||
@@ -151,23 +207,27 @@ ...initialOptions, | ||
| } | ||
| this.#setState(this.options.initialState ?? {}) | ||
| } | ||
| /** | ||
| * Updates the rate limiter options | ||
| * Updates the async rate limiter options | ||
| */ | ||
| setOptions(newOptions: Partial<AsyncRateLimiterOptions<TFn>>): void { | ||
| this._options = { ...this._options, ...newOptions } | ||
| setOptions = (newOptions: Partial<AsyncRateLimiterOptions<TFn>>): void => { | ||
| this.options = { ...this.options, ...newOptions } | ||
| } | ||
| /** | ||
| * Returns the current rate limiter options | ||
| */ | ||
| getOptions(): AsyncRateLimiterOptions<TFn> { | ||
| return this._options | ||
| #setState = (newState: Partial<AsyncRateLimiterState<TFn>>): void => { | ||
| this.store.setState((state) => { | ||
| const combinedState = { | ||
| ...state, | ||
| ...newState, | ||
| } | ||
| return combinedState | ||
| }) | ||
| } | ||
| /** | ||
| * Returns the current enabled state of the rate limiter | ||
| * Returns the current enabled state of the async rate limiter | ||
| */ | ||
| getEnabled(): boolean { | ||
| return !!parseFunctionOrValue(this._options.enabled, this) | ||
| #getEnabled = (): boolean => { | ||
| return !!parseFunctionOrValue(this.options.enabled, this) | ||
| } | ||
@@ -178,4 +238,4 @@ | ||
| */ | ||
| getLimit(): number { | ||
| return parseFunctionOrValue(this._options.limit, this) | ||
| #getLimit = (): number => { | ||
| return parseFunctionOrValue(this.options.limit, this) | ||
| } | ||
@@ -186,4 +246,4 @@ | ||
| */ | ||
| getWindow(): number { | ||
| return parseFunctionOrValue(this._options.window, this) | ||
| #getWindow = (): number => { | ||
| return parseFunctionOrValue(this.options.window, this) | ||
| } | ||
@@ -194,3 +254,2 @@ | ||
| * Will reject execution if the number of calls in the current window exceeds the limit. | ||
| * If execution is allowed, waits for any previous execution to complete before proceeding. | ||
| * | ||
@@ -202,6 +261,3 @@ * Error Handling: | ||
| * and this method will return undefined. | ||
| * - If the rate limit is exceeded, the execution will be rejected and the `onReject` handler | ||
| * will be called if configured. | ||
| * - The error state can be checked using `getErrorCount()` and `getIsExecuting()`. | ||
| * - Rate limit rejections can be tracked using `getRejectionCount()`. | ||
| * | ||
@@ -215,81 +271,92 @@ * @returns A promise that resolves with the function's return value, or undefined if an error occurred and was handled by onError | ||
| * | ||
| * // First 5 calls will execute | ||
| * await rateLimiter.maybeExecute('arg1', 'arg2'); | ||
| * // First 5 calls will return a promise that resolves with the result | ||
| * const result = await rateLimiter.maybeExecute('arg1', 'arg2'); | ||
| * | ||
| * // Additional calls within the window will be rejected | ||
| * await rateLimiter.maybeExecute('arg1', 'arg2'); // Rejected | ||
| * // Additional calls within the window will return undefined | ||
| * const result2 = await rateLimiter.maybeExecute('arg1', 'arg2'); // undefined | ||
| * ``` | ||
| */ | ||
| async maybeExecute( | ||
| maybeExecute = async ( | ||
| ...args: Parameters<TFn> | ||
| ): Promise<ReturnType<TFn> | undefined> { | ||
| this.cleanupOldExecutions() | ||
| ): Promise<ReturnType<TFn> | undefined> => { | ||
| this.#cleanupOldExecutions() | ||
| const limit = this.getLimit() | ||
| const window = this.getWindow() | ||
| const relevantExecutionTimes = this.#getRelevantExecutionTimes() | ||
| if (this._options.windowType === 'sliding') { | ||
| // For sliding window, we can execute if we have capacity in the current window | ||
| if (this._executionTimes.length < limit) { | ||
| await this.execute(...args) | ||
| return this._lastResult | ||
| } | ||
| } else { | ||
| // For fixed window, we need to check if we're in a new window | ||
| const now = Date.now() | ||
| const oldestExecution = Math.min(...this._executionTimes) | ||
| const isNewWindow = oldestExecution + window <= now | ||
| if (isNewWindow || this._executionTimes.length < limit) { | ||
| await this.execute(...args) | ||
| return this._lastResult | ||
| } | ||
| if (relevantExecutionTimes.length < this.#getLimit()) { | ||
| await this.#execute(...args) | ||
| return this.store.state.lastResult | ||
| } | ||
| this.rejectFunction() | ||
| this.#setState({ | ||
| rejectionCount: this.store.state.rejectionCount + 1, | ||
| }) | ||
| this.options.onReject?.(this) | ||
| return undefined | ||
| } | ||
| private async execute( | ||
| #execute = async ( | ||
| ...args: Parameters<TFn> | ||
| ): Promise<ReturnType<TFn> | undefined> { | ||
| if (!this.getEnabled()) return | ||
| this._isExecuting = true | ||
| ): Promise<ReturnType<TFn> | undefined> => { | ||
| if (!this.#getEnabled()) return | ||
| const now = Date.now() | ||
| this._executionTimes.push(now) | ||
| const executionTimes = [...this.store.state.executionTimes, now] | ||
| this.#setState({ | ||
| isExecuting: true, | ||
| executionTimes, | ||
| }) | ||
| try { | ||
| this._lastResult = await this.fn(...args) | ||
| this._successCount++ | ||
| this._options.onSuccess?.(this._lastResult!, this) | ||
| const result = await this.fn(...args) | ||
| this.#setState({ | ||
| successCount: this.store.state.successCount + 1, | ||
| lastResult: result, | ||
| }) | ||
| this.options.onSuccess?.(result, this) | ||
| } catch (error) { | ||
| this._errorCount++ | ||
| this._options.onError?.(error, this) | ||
| if (this._options.throwOnError) { | ||
| this.#setState({ | ||
| errorCount: this.store.state.errorCount + 1, | ||
| }) | ||
| this.options.onError?.(error, this) | ||
| if (this.options.throwOnError) { | ||
| throw error | ||
| } else { | ||
| console.error(error) | ||
| } | ||
| } finally { | ||
| this._isExecuting = false | ||
| this._settleCount++ | ||
| this._options.onSettled?.(this) | ||
| this.#setState({ | ||
| isExecuting: false, | ||
| settleCount: this.store.state.settleCount + 1, | ||
| }) | ||
| this.options.onSettled?.(this) | ||
| } | ||
| return this._lastResult | ||
| return this.store.state.lastResult | ||
| } | ||
| private rejectFunction(): void { | ||
| this._rejectionCount++ | ||
| if (this._options.onReject) { | ||
| this._options.onReject(this) | ||
| #getRelevantExecutionTimes = (): Array<number> => { | ||
| if (this.options.windowType === 'sliding') { | ||
| // For sliding window, return all executions within the current window | ||
| return this.store.state.executionTimes.filter( | ||
| (time) => time > Date.now() - this.#getWindow(), | ||
| ) | ||
| } else { | ||
| // For fixed window, return all executions in the current window | ||
| // The window starts from the oldest execution time | ||
| const oldestExecution = Math.min(...this.store.state.executionTimes) | ||
| const windowStart = oldestExecution | ||
| return this.store.state.executionTimes.filter( | ||
| (time) => | ||
| time >= windowStart && time <= windowStart + this.#getWindow(), | ||
| ) | ||
| } | ||
| } | ||
| private cleanupOldExecutions(): void { | ||
| #cleanupOldExecutions = (): void => { | ||
| const now = Date.now() | ||
| const windowStart = now - this.getWindow() | ||
| this._executionTimes = this._executionTimes.filter( | ||
| (time) => time > windowStart, | ||
| ) | ||
| const windowStart = now - this.#getWindow() | ||
| this.#setState({ | ||
| executionTimes: this.store.state.executionTimes.filter( | ||
| (time) => time > windowStart, | ||
| ), | ||
| }) | ||
| } | ||
@@ -300,5 +367,5 @@ | ||
| */ | ||
| getRemainingInWindow(): number { | ||
| this.cleanupOldExecutions() | ||
| return Math.max(0, this.getLimit() - this._executionTimes.length) | ||
| getRemainingInWindow = (): number => { | ||
| const relevantExecutionTimes = this.#getRelevantExecutionTimes() | ||
| return Math.max(0, this.#getLimit() - relevantExecutionTimes.length) | ||
| } | ||
@@ -311,54 +378,15 @@ | ||
| */ | ||
| getMsUntilNextWindow(): number { | ||
| getMsUntilNextWindow = (): number => { | ||
| if (this.getRemainingInWindow() > 0) { | ||
| return 0 | ||
| } | ||
| const oldestExecution = Math.min(...this._executionTimes) | ||
| return oldestExecution + this.getWindow() - Date.now() | ||
| const oldestExecution = this.store.state.executionTimes[0] ?? Infinity | ||
| return oldestExecution + this.#getWindow() - Date.now() | ||
| } | ||
| /** | ||
| * Returns the number of times the function has been executed | ||
| */ | ||
| getSuccessCount(): number { | ||
| return this._successCount | ||
| } | ||
| /** | ||
| * Returns the number of times the function has been settled | ||
| */ | ||
| getSettleCount(): number { | ||
| return this._settleCount | ||
| } | ||
| /** | ||
| * Returns the number of times the function has errored | ||
| */ | ||
| getErrorCount(): number { | ||
| return this._errorCount | ||
| } | ||
| /** | ||
| * Returns the number of times the function has been rejected | ||
| */ | ||
| getRejectionCount(): number { | ||
| return this._rejectionCount | ||
| } | ||
| /** | ||
| * Returns whether the function is currently executing | ||
| */ | ||
| getIsExecuting(): boolean { | ||
| return this._isExecuting | ||
| } | ||
| /** | ||
| * Resets the rate limiter state | ||
| */ | ||
| reset(): void { | ||
| this._executionTimes = [] | ||
| this._successCount = 0 | ||
| this._errorCount = 0 | ||
| this._rejectionCount = 0 | ||
| this._settleCount = 0 | ||
| reset = (): void => { | ||
| this.#setState(getDefaultAsyncRateLimiterState()) | ||
| } | ||
@@ -385,2 +413,14 @@ } | ||
| * | ||
| * State Management: | ||
| * - Uses TanStack Store for reactive state management | ||
| * - Use `initialState` to provide initial state values when creating the rate limiter | ||
| * - `initialState` can be a partial state object | ||
| * - Use `onSuccess` callback to react to successful function execution and implement custom logic | ||
| * - Use `onError` callback to react to function execution errors and implement custom error handling | ||
| * - Use `onSettled` callback to react to function execution completion (success or error) and implement custom logic | ||
| * - Use `onReject` callback to react to executions being rejected when rate limit is exceeded | ||
| * - The state includes execution times, success/error counts, and current execution status | ||
| * - State can be accessed via the underlying AsyncRateLimiter instance's `store.state` property | ||
| * - When using framework adapters (React/Solid), state is accessed from the hook's state property | ||
| * | ||
| * Consider using throttle() or debounce() if you need more intelligent execution control. Use rate limiting when you specifically | ||
@@ -426,3 +466,3 @@ * need to enforce a hard limit on the number of executions within a time period. | ||
| const rateLimiter = new AsyncRateLimiter(fn, initialOptions) | ||
| return rateLimiter.maybeExecute.bind(rateLimiter) | ||
| return rateLimiter.maybeExecute | ||
| } |
+233
-139
@@ -0,4 +1,65 @@ | ||
| import { Store } from '@tanstack/store' | ||
| import { parseFunctionOrValue } from './utils' | ||
| import type { AnyAsyncFunction, OptionalKeys } from './types' | ||
| export interface AsyncThrottlerState<TFn extends AnyAsyncFunction> { | ||
| /** | ||
| * Number of function executions that have resulted in errors | ||
| */ | ||
| errorCount: number | ||
| /** | ||
| * Whether the throttled function is currently executing asynchronously | ||
| */ | ||
| isExecuting: boolean | ||
| /** | ||
| * Whether the throttler is waiting for the timeout to trigger execution | ||
| */ | ||
| isPending: boolean | ||
| /** | ||
| * The arguments from the most recent call to maybeExecute | ||
| */ | ||
| lastArgs: Parameters<TFn> | undefined | ||
| /** | ||
| * Timestamp of the last function execution in milliseconds | ||
| */ | ||
| lastExecutionTime: number | ||
| /** | ||
| * The result from the most recent successful function execution | ||
| */ | ||
| lastResult: ReturnType<TFn> | undefined | ||
| /** | ||
| * Timestamp when the next execution can occur in milliseconds | ||
| */ | ||
| nextExecutionTime: number | ||
| /** | ||
| * Number of function executions that have completed (either successfully or with errors) | ||
| */ | ||
| settleCount: number | ||
| /** | ||
| * Current execution status - 'idle' when not active, 'pending' when waiting, 'executing' when running, 'settled' when completed | ||
| */ | ||
| status: 'disabled' | 'idle' | 'pending' | 'executing' | 'settled' | ||
| /** | ||
| * Number of function executions that have completed successfully | ||
| */ | ||
| successCount: number | ||
| } | ||
| function getDefaultAsyncThrottlerState< | ||
| TFn extends AnyAsyncFunction, | ||
| >(): AsyncThrottlerState<TFn> { | ||
| return structuredClone({ | ||
| errorCount: 0, | ||
| isExecuting: false, | ||
| isPending: false, | ||
| lastArgs: undefined, | ||
| lastExecutionTime: 0, | ||
| lastResult: undefined, | ||
| nextExecutionTime: 0, | ||
| settleCount: 0, | ||
| status: 'idle', | ||
| successCount: 0, | ||
| }) | ||
| } | ||
| /** | ||
@@ -15,2 +76,6 @@ * Options for configuring an async throttled function | ||
| /** | ||
| * Initial state for the async throttler | ||
| */ | ||
| initialState?: Partial<AsyncThrottlerState<TFn>> | ||
| /** | ||
| * Whether to execute the function immediately when called | ||
@@ -58,3 +123,3 @@ * Defaults to true | ||
| AsyncThrottlerOptions<any>, | ||
| 'onError' | 'onSettled' | 'onSuccess' | ||
| 'initialState' | 'onError' | 'onSettled' | 'onSuccess' | ||
| > | ||
@@ -90,2 +155,12 @@ | ||
| * | ||
| * State Management: | ||
| * - Uses TanStack Store for reactive state management | ||
| * - Use `initialState` to provide initial state values when creating the async throttler | ||
| * - Use `onSuccess` callback to react to successful function execution and implement custom logic | ||
| * - Use `onError` callback to react to function execution errors and implement custom error handling | ||
| * - Use `onSettled` callback to react to function execution completion (success or error) and implement custom logic | ||
| * - The state includes error count, execution status, last execution time, and success/settle counts | ||
| * - State can be accessed via `asyncThrottler.store.state` when using the class directly | ||
| * - When using framework adapters (React/Solid), state is accessed from `asyncThrottler.state` | ||
| * | ||
| * @example | ||
@@ -109,14 +184,9 @@ * ```ts | ||
| export class AsyncThrottler<TFn extends AnyAsyncFunction> { | ||
| private _options: AsyncThrottlerOptionsWithOptionalCallbacks | ||
| private _abortController: AbortController | null = null | ||
| private _errorCount = 0 | ||
| private _isExecuting = false | ||
| private _lastArgs: Parameters<TFn> | undefined | ||
| private _lastExecutionTime = 0 | ||
| private _lastResult: ReturnType<TFn> | undefined | ||
| private _nextExecutionTime = 0 | ||
| private _settleCount = 0 | ||
| private _successCount = 0 | ||
| private _timeoutId: NodeJS.Timeout | null = null | ||
| private _resolvePreviousPromise: | ||
| readonly store: Store<Readonly<AsyncThrottlerState<TFn>>> = new Store< | ||
| AsyncThrottlerState<TFn> | ||
| >(getDefaultAsyncThrottlerState<TFn>()) | ||
| options: AsyncThrottlerOptions<TFn> | ||
| #abortController: AbortController | null = null | ||
| #timeoutId: NodeJS.Timeout | null = null | ||
| #resolvePreviousPromise: | ||
| | ((value?: ReturnType<TFn> | undefined) => void) | ||
@@ -129,3 +199,3 @@ | null = null | ||
| ) { | ||
| this._options = { | ||
| this.options = { | ||
| ...defaultOptions, | ||
@@ -135,12 +205,13 @@ ...initialOptions, | ||
| } | ||
| this.#setState(this.options.initialState ?? {}) | ||
| } | ||
| /** | ||
| * Updates the throttler options | ||
| * Updates the async throttler options | ||
| */ | ||
| setOptions(newOptions: Partial<AsyncThrottlerOptions<TFn>>): void { | ||
| this._options = { ...this._options, ...newOptions } | ||
| setOptions = (newOptions: Partial<AsyncThrottlerOptions<TFn>>): void => { | ||
| this.options = { ...this.options, ...newOptions } | ||
| // End the pending state if the debouncer is disabled | ||
| if (!this._options.enabled) { | ||
| // End the pending state if the throttler is disabled | ||
| if (!this.#getEnabled()) { | ||
| this.cancel() | ||
@@ -150,14 +221,29 @@ } | ||
| /** | ||
| * Returns the current options | ||
| */ | ||
| getOptions(): AsyncThrottlerOptions<TFn> { | ||
| return this._options | ||
| #setState = (newState: Partial<AsyncThrottlerState<TFn>>): void => { | ||
| this.store.setState((state) => { | ||
| const combinedState = { | ||
| ...state, | ||
| ...newState, | ||
| } | ||
| const { isPending, isExecuting, settleCount } = combinedState | ||
| return { | ||
| ...combinedState, | ||
| status: !this.#getEnabled() | ||
| ? 'disabled' | ||
| : isPending | ||
| ? 'pending' | ||
| : isExecuting | ||
| ? 'executing' | ||
| : settleCount > 0 | ||
| ? 'settled' | ||
| : 'idle', | ||
| } | ||
| }) | ||
| } | ||
| /** | ||
| * Returns the current enabled state of the throttler | ||
| * Returns the current enabled state of the async throttler | ||
| */ | ||
| getEnabled(): boolean { | ||
| return !!parseFunctionOrValue(this._options.enabled, this) | ||
| #getEnabled = (): boolean => { | ||
| return !!parseFunctionOrValue(this.options.enabled, this) | ||
| } | ||
@@ -168,56 +254,63 @@ | ||
| */ | ||
| getWait(): number { | ||
| return parseFunctionOrValue(this._options.wait, this) | ||
| #getWait = (): number => { | ||
| return parseFunctionOrValue(this.options.wait, this) | ||
| } | ||
| /** | ||
| * Attempts to execute the throttled function. | ||
| * If a call is already in progress, it may be blocked or queued depending on the `wait` option. | ||
| * Attempts to execute the throttled function. The execution behavior depends on the throttler options: | ||
| * | ||
| * Error Handling: | ||
| * - If the throttled function throws and no `onError` handler is configured, | ||
| * the error will be thrown from this method. | ||
| * - If an `onError` handler is configured, errors will be caught and passed to the handler, | ||
| * and this method will return undefined. | ||
| * - The error state can be checked using `getErrorCount()` and `getIsExecuting()`. | ||
| * - If enough time has passed since the last execution (>= wait period): | ||
| * - With leading=true: Executes immediately | ||
| * - With leading=false: Waits for the next trailing execution | ||
| * | ||
| * @returns A promise that resolves with the function's return value, or undefined if an error occurred and was handled by onError | ||
| * @throws The error from the throttled function if no onError handler is configured | ||
| * - If within the wait period: | ||
| * - With trailing=true: Schedules execution for end of wait period | ||
| * - With trailing=false: Drops the execution | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * const throttled = new AsyncThrottler(fn, { wait: 1000 }); | ||
| * | ||
| * // First call executes immediately | ||
| * await throttled.maybeExecute('a', 'b'); | ||
| * | ||
| * // Call during wait period - gets throttled | ||
| * await throttled.maybeExecute('c', 'd'); | ||
| * ``` | ||
| */ | ||
| async maybeExecute( | ||
| maybeExecute = async ( | ||
| ...args: Parameters<TFn> | ||
| ): Promise<ReturnType<TFn> | undefined> { | ||
| ): Promise<ReturnType<TFn> | undefined> => { | ||
| if (!this.#getEnabled()) return undefined | ||
| const now = Date.now() | ||
| const timeSinceLastExecution = now - this._lastExecutionTime | ||
| const wait = this.getWait() | ||
| const timeSinceLastExecution = now - this.store.state.lastExecutionTime | ||
| const wait = this.#getWait() | ||
| // Store the most recent arguments for potential trailing execution | ||
| this.#setState({ lastArgs: args }) | ||
| this.resolvePreviousPromise() | ||
| this.#resolvePreviousPromiseInternal() | ||
| // Handle leading execution | ||
| if (this._options.leading && timeSinceLastExecution >= wait) { | ||
| await this.execute(...args) | ||
| return this._lastResult | ||
| if (this.options.leading && timeSinceLastExecution >= wait) { | ||
| await this.#execute(...args) | ||
| return this.store.state.lastResult | ||
| } else { | ||
| // Store the most recent arguments for potential trailing execution | ||
| this._lastArgs = args | ||
| return new Promise((resolve) => { | ||
| this._resolvePreviousPromise = resolve | ||
| this.#resolvePreviousPromise = resolve | ||
| // Clear any existing timeout to ensure we use the latest arguments | ||
| if (this._timeoutId) { | ||
| clearTimeout(this._timeoutId) | ||
| } | ||
| this.#clearTimeout() | ||
| // Set up trailing execution if enabled | ||
| if (this._options.trailing) { | ||
| const _timeSinceLastExecution = this._lastExecutionTime | ||
| ? now - this._lastExecutionTime | ||
| if (this.options.trailing) { | ||
| const _timeSinceLastExecution = this.store.state.lastExecutionTime | ||
| ? now - this.store.state.lastExecutionTime | ||
| : 0 | ||
| const timeoutDuration = wait - _timeSinceLastExecution | ||
| this._timeoutId = setTimeout(async () => { | ||
| if (this._lastArgs !== undefined) { | ||
| await this.execute(...this._lastArgs) | ||
| this.#setState({ isPending: true }) | ||
| this.#timeoutId = setTimeout(async () => { | ||
| if (this.store.state.lastArgs !== undefined) { | ||
| await this.#execute(...this.store.state.lastArgs) | ||
| } | ||
| this._resolvePreviousPromise = null | ||
| resolve(this._lastResult) | ||
| this.#resolvePreviousPromise = null | ||
| resolve(this.store.state.lastResult) | ||
| }, timeoutDuration) | ||
@@ -229,16 +322,21 @@ } | ||
| private async execute( | ||
| #execute = async ( | ||
| ...args: Parameters<TFn> | ||
| ): Promise<ReturnType<TFn> | undefined> { | ||
| if (!this.getEnabled() || this._isExecuting) return undefined | ||
| this._abortController = new AbortController() | ||
| ): Promise<ReturnType<TFn> | undefined> => { | ||
| if (!this.#getEnabled() || this.store.state.isExecuting) return undefined | ||
| this.#abortController = new AbortController() | ||
| try { | ||
| this._isExecuting = true | ||
| this._lastResult = await this.fn(...args) // EXECUTE! | ||
| this._successCount++ | ||
| this._options.onSuccess?.(this._lastResult!, this) | ||
| this.#setState({ isExecuting: true }) | ||
| const result = await this.fn(...args) // EXECUTE! | ||
| this.#setState({ | ||
| lastResult: result, | ||
| successCount: this.store.state.successCount + 1, | ||
| }) | ||
| this.options.onSuccess?.(result, this) | ||
| } catch (error) { | ||
| this._errorCount++ | ||
| this._options.onError?.(error, this) | ||
| if (this._options.throwOnError) { | ||
| this.#setState({ | ||
| errorCount: this.store.state.errorCount + 1, | ||
| }) | ||
| this.options.onError?.(error, this) | ||
| if (this.options.throwOnError) { | ||
| throw error | ||
@@ -249,90 +347,76 @@ } else { | ||
| } finally { | ||
| this._isExecuting = false | ||
| this._settleCount++ | ||
| this._abortController = null | ||
| this._lastExecutionTime = Date.now() | ||
| this._nextExecutionTime = this._lastExecutionTime + this.getWait() | ||
| this._options.onSettled?.(this) | ||
| const lastExecutionTime = Date.now() | ||
| const nextExecutionTime = lastExecutionTime + this.#getWait() | ||
| this.#setState({ | ||
| isExecuting: false, | ||
| isPending: false, | ||
| settleCount: this.store.state.settleCount + 1, | ||
| lastExecutionTime, | ||
| nextExecutionTime, | ||
| }) | ||
| this.#abortController = null | ||
| this.options.onSettled?.(this) | ||
| } | ||
| return this._lastResult | ||
| return this.store.state.lastResult | ||
| } | ||
| private resolvePreviousPromise(): void { | ||
| if (this._resolvePreviousPromise) { | ||
| this._resolvePreviousPromise(this._lastResult) | ||
| this._resolvePreviousPromise = null | ||
| } | ||
| } | ||
| /** | ||
| * Cancels any pending execution or aborts any execution in progress | ||
| * Processes the current pending execution immediately | ||
| */ | ||
| cancel(): void { | ||
| if (this._timeoutId) { | ||
| clearTimeout(this._timeoutId) | ||
| this._timeoutId = null | ||
| flush = (): void => { | ||
| if (this.store.state.isPending && this.store.state.lastArgs) { | ||
| this.#abortExecution() // abort any current execution | ||
| this.#clearTimeout() // clear any existing timeout | ||
| this.#execute(...this.store.state.lastArgs) | ||
| } | ||
| if (this._abortController) { | ||
| this._abortController.abort() | ||
| this._abortController = null | ||
| } | ||
| this.resolvePreviousPromise() | ||
| this._lastArgs = undefined | ||
| } | ||
| /** | ||
| * Returns the last execution time | ||
| */ | ||
| getLastExecutionTime(): number { | ||
| return this._lastExecutionTime | ||
| #resolvePreviousPromiseInternal = (): void => { | ||
| if (this.#resolvePreviousPromise) { | ||
| this.#resolvePreviousPromise(this.store.state.lastResult) | ||
| this.#resolvePreviousPromise = null | ||
| } | ||
| } | ||
| /** | ||
| * Returns the next execution time | ||
| */ | ||
| getNextExecutionTime(): number { | ||
| return this._nextExecutionTime | ||
| #clearTimeout = (): void => { | ||
| if (this.#timeoutId) { | ||
| clearTimeout(this.#timeoutId) | ||
| this.#timeoutId = null | ||
| } | ||
| } | ||
| /** | ||
| * Returns the last result of the debounced function | ||
| */ | ||
| getLastResult(): ReturnType<TFn> | undefined { | ||
| return this._lastResult | ||
| #cancelPendingExecution = (): void => { | ||
| this.#clearTimeout() | ||
| if (this.#resolvePreviousPromise) { | ||
| this.#resolvePreviousPromise(this.store.state.lastResult) | ||
| this.#resolvePreviousPromise = null | ||
| } | ||
| this.#setState({ | ||
| isPending: false, | ||
| isExecuting: false, | ||
| lastArgs: undefined, | ||
| }) | ||
| } | ||
| /** | ||
| * Returns the number of times the function has been executed successfully | ||
| */ | ||
| getSuccessCount(): number { | ||
| return this._successCount | ||
| #abortExecution = (): void => { | ||
| if (this.#abortController) { | ||
| this.#abortController.abort() | ||
| this.#abortController = null | ||
| } | ||
| } | ||
| /** | ||
| * Returns the number of times the function has settled (completed or errored) | ||
| * Cancels any pending execution or aborts any execution in progress | ||
| */ | ||
| getSettleCount(): number { | ||
| return this._settleCount | ||
| cancel = (): void => { | ||
| this.#cancelPendingExecution() | ||
| this.#abortExecution() | ||
| } | ||
| /** | ||
| * Returns the number of times the function has errored | ||
| * Resets the debouncer state to its default values | ||
| */ | ||
| getErrorCount(): number { | ||
| return this._errorCount | ||
| reset = (): void => { | ||
| this.#setState(getDefaultAsyncThrottlerState<TFn>()) | ||
| } | ||
| /** | ||
| * Returns the current pending state | ||
| */ | ||
| getIsPending(): boolean { | ||
| return this.getEnabled() && !!this._timeoutId | ||
| } | ||
| /** | ||
| * Returns the current executing state | ||
| */ | ||
| getIsExecuting(): boolean { | ||
| return this._isExecuting | ||
| } | ||
| } | ||
@@ -356,2 +440,12 @@ | ||
| * | ||
| * State Management: | ||
| * - Uses TanStack Store for reactive state management | ||
| * - Use `initialState` to provide initial state values when creating the async throttler | ||
| * - Use `onSuccess` callback to react to successful function execution and implement custom logic | ||
| * - Use `onError` callback to react to function execution errors and implement custom error handling | ||
| * - Use `onSettled` callback to react to function execution completion (success or error) and implement custom logic | ||
| * - The state includes error count, execution status, last execution time, and success/settle counts | ||
| * - State can be accessed via the underlying AsyncThrottler instance's `store.state` property | ||
| * - When using framework adapters (React/Solid), state is accessed from the hook's state property | ||
| * | ||
| * @example | ||
@@ -379,3 +473,3 @@ * ```ts | ||
| const asyncThrottler = new AsyncThrottler(fn, initialOptions) | ||
| return asyncThrottler.maybeExecute.bind(asyncThrottler) | ||
| return asyncThrottler.maybeExecute | ||
| } |
+158
-92
@@ -0,3 +1,53 @@ | ||
| import { Store } from '@tanstack/store' | ||
| import { parseFunctionOrValue } from './utils' | ||
| import type { OptionalKeys } from './types' | ||
| export interface BatcherState<TValue> { | ||
| /** | ||
| * Number of batch executions that have been completed | ||
| */ | ||
| executionCount: number | ||
| /** | ||
| * Whether the batcher has no items to process (items array is empty) | ||
| */ | ||
| isEmpty: boolean | ||
| /** | ||
| * Whether the batcher is waiting for the timeout to trigger batch processing | ||
| */ | ||
| isPending: boolean | ||
| /** | ||
| * Whether the batcher is active and will process items automatically | ||
| */ | ||
| isRunning: boolean | ||
| /** | ||
| * Total number of items that have been processed across all batches | ||
| */ | ||
| totalItemsProcessed: number | ||
| /** | ||
| * Array of items currently queued for batch processing | ||
| */ | ||
| items: Array<TValue> | ||
| /** | ||
| * Number of items currently in the batch queue | ||
| */ | ||
| size: number | ||
| /** | ||
| * Current processing status - 'idle' when not processing, 'pending' when waiting for timeout | ||
| */ | ||
| status: 'idle' | 'pending' | ||
| } | ||
| function getDefaultBatcherState<TValue>(): BatcherState<TValue> { | ||
| return { | ||
| executionCount: 0, | ||
| isEmpty: true, | ||
| isPending: false, | ||
| isRunning: true, | ||
| totalItemsProcessed: 0, | ||
| items: [], | ||
| size: 0, | ||
| status: 'idle', | ||
| } | ||
| } | ||
| /** | ||
@@ -13,2 +63,6 @@ * Options for configuring a Batcher instance | ||
| /** | ||
| * Initial state for the batcher | ||
| */ | ||
| initialState?: Partial<BatcherState<TValue>> | ||
| /** | ||
| * Maximum number of items in a batch | ||
@@ -23,6 +77,2 @@ * @default Infinity | ||
| /** | ||
| * Callback fired when the batcher's running state changes | ||
| */ | ||
| onIsRunningChange?: (batcher: Batcher<TValue>) => void | ||
| /** | ||
| * Callback fired after items are added to the batcher | ||
@@ -42,3 +92,3 @@ */ | ||
| */ | ||
| wait?: number | ||
| wait?: number | ((batcher: Batcher<TValue>) => number) | ||
| } | ||
@@ -48,3 +98,3 @@ | ||
| Required<BatcherOptions<TValue>>, | ||
| 'onExecute' | 'onItemsChange' | 'onIsRunningChange' | ||
| 'initialState' | 'onExecute' | 'onItemsChange' | ||
| > | ||
@@ -70,2 +120,11 @@ | ||
| * | ||
| * State Management: | ||
| * - Uses TanStack Store for reactive state management | ||
| * - Use `initialState` to provide initial state values when creating the batcher | ||
| * - Use `onExecute` callback to react to batch execution and implement custom logic | ||
| * - Use `onItemsChange` callback to react to items being added or removed from the batcher | ||
| * - The state includes batch execution count, total items processed, items, and running status | ||
| * - State can be accessed via `batcher.store.state` when using the class directly | ||
| * - When using framework adapters (React/Solid), state is accessed from `batcher.state` | ||
| * | ||
| * @example | ||
@@ -78,3 +137,3 @@ * ```ts | ||
| * wait: 2000, | ||
| * onExecuteBatch: (items) => console.log('Batch executed:', items) | ||
| * onExecute: (batcher) => console.log('Batch executed:', batcher.peekAllItems()) | ||
| * } | ||
@@ -87,12 +146,11 @@ * ); | ||
| * // the batch will be processed | ||
| * // batcher.execute() // manually trigger a batch | ||
| * // batcher.flush() // manually trigger a batch | ||
| * ``` | ||
| */ | ||
| export class Batcher<TValue> { | ||
| private _options: BatcherOptionsWithOptionalCallbacks<TValue> | ||
| private _batchExecutionCount = 0 | ||
| private _itemExecutionCount = 0 | ||
| private _items: Array<TValue> = [] | ||
| private _running: boolean | ||
| private _timeoutId: NodeJS.Timeout | null = null | ||
| readonly store: Store<Readonly<BatcherState<TValue>>> = new Store( | ||
| getDefaultBatcherState<TValue>(), | ||
| ) | ||
| options: BatcherOptionsWithOptionalCallbacks<TValue> | ||
| #timeoutId: NodeJS.Timeout | null = null | ||
@@ -103,4 +161,7 @@ constructor( | ||
| ) { | ||
| this._options = { ...defaultOptions, ...initialOptions } | ||
| this._running = this._options.started | ||
| this.options = { | ||
| ...defaultOptions, | ||
| ...initialOptions, | ||
| } | ||
| this.#setState(this.options.initialState ?? {}) | ||
| } | ||
@@ -111,13 +172,28 @@ | ||
| */ | ||
| setOptions(newOptions: Partial<BatcherOptions<TValue>>): void { | ||
| this._options = { ...this._options, ...newOptions } | ||
| setOptions = (newOptions: Partial<BatcherOptions<TValue>>): void => { | ||
| this.options = { ...this.options, ...newOptions } | ||
| } | ||
| /** | ||
| * Returns the current batcher options | ||
| */ | ||
| getOptions(): BatcherOptions<TValue> { | ||
| return this._options | ||
| #setState = (newState: Partial<BatcherState<TValue>>): void => { | ||
| this.store.setState((state) => { | ||
| const combinedState = { | ||
| ...state, | ||
| ...newState, | ||
| } | ||
| const { isPending, items } = combinedState | ||
| const size = items.length | ||
| const isEmpty = size === 0 | ||
| return { | ||
| ...combinedState, | ||
| isEmpty, | ||
| size, | ||
| status: isPending ? 'pending' : 'idle', | ||
| } | ||
| }) | ||
| } | ||
| #getWait = (): number => { | ||
| return parseFunctionOrValue(this.options.wait, this) | ||
| } | ||
| /** | ||
@@ -127,18 +203,18 @@ * Adds an item to the batcher | ||
| */ | ||
| addItem(item: TValue): void { | ||
| this._items.push(item) | ||
| this._options.onItemsChange?.(this) | ||
| addItem = (item: TValue): void => { | ||
| this.#setState({ | ||
| items: [...this.store.state.items, item], | ||
| isPending: this.options.wait !== Infinity, | ||
| }) | ||
| this.options.onItemsChange?.(this) | ||
| const shouldProcess = | ||
| this._items.length >= this._options.maxSize || | ||
| this._options.getShouldExecute(this._items, this) | ||
| this.store.state.items.length >= this.options.maxSize || | ||
| this.options.getShouldExecute(this.store.state.items, this) | ||
| if (shouldProcess) { | ||
| this.execute() | ||
| } else if ( | ||
| this._running && | ||
| !this._timeoutId && | ||
| this._options.wait !== Infinity | ||
| ) { | ||
| this._timeoutId = setTimeout(() => this.execute(), this._options.wait) | ||
| this.#execute() | ||
| } else if (this.store.state.isRunning && this.options.wait !== Infinity) { | ||
| this.#clearTimeout() // clear any pending timeout to replace it with a new one | ||
| this.#timeoutId = setTimeout(() => this.#execute(), this.#getWait()) | ||
| } | ||
@@ -156,9 +232,4 @@ } | ||
| */ | ||
| execute(): void { | ||
| if (this._timeoutId) { | ||
| clearTimeout(this._timeoutId) | ||
| this._timeoutId = null | ||
| } | ||
| if (this._items.length === 0) { | ||
| #execute = (): void => { | ||
| if (this.store.state.items.length === 0) { | ||
| return | ||
@@ -168,75 +239,67 @@ } | ||
| const batch = this.peekAllItems() // copy of the items to be processed (to prevent race conditions) | ||
| this._items = [] // Clear items before processing to prevent race conditions | ||
| this._options.onItemsChange?.(this) // Call onItemsChange to notify listeners that the items have changed | ||
| this.clear() // Clear items before processing to prevent race conditions | ||
| this.options.onItemsChange?.(this) // Call onItemsChange to notify listeners that the items have changed | ||
| this.fn(batch) | ||
| this._batchExecutionCount++ | ||
| this._itemExecutionCount += batch.length | ||
| this._options.onExecute?.(this) | ||
| this.fn(batch) // EXECUTE | ||
| this.#setState({ | ||
| executionCount: this.store.state.executionCount + 1, | ||
| totalItemsProcessed: this.store.state.totalItemsProcessed + batch.length, | ||
| }) | ||
| this.options.onExecute?.(this) | ||
| } | ||
| /** | ||
| * Stops the batcher from processing batches | ||
| * Processes the current batch of items immediately | ||
| */ | ||
| stop(): void { | ||
| this._running = false | ||
| this._options.onIsRunningChange?.(this) | ||
| if (this._timeoutId) { | ||
| clearTimeout(this._timeoutId) | ||
| this._timeoutId = null | ||
| } | ||
| flush = (): void => { | ||
| this.#clearTimeout() // clear any pending timeout | ||
| this.#execute() // execute immediately | ||
| } | ||
| /** | ||
| * Starts the batcher and processes any pending items | ||
| * Stops the batcher from processing batches | ||
| */ | ||
| start(): void { | ||
| this._running = true | ||
| this._options.onIsRunningChange?.(this) | ||
| if (this._items.length > 0 && !this._timeoutId) { | ||
| this._timeoutId = setTimeout(() => this.execute(), this._options.wait) | ||
| } | ||
| stop = (): void => { | ||
| this.#setState({ isRunning: false }) | ||
| this.#clearTimeout() | ||
| } | ||
| /** | ||
| * Returns the current number of items in the batcher | ||
| * Starts the batcher and processes any pending items | ||
| */ | ||
| getSize(): number { | ||
| return this._items.length | ||
| start = (): void => { | ||
| this.#setState({ isRunning: true }) | ||
| if (this.store.state.items.length > 0) { | ||
| this.#execute() | ||
| } | ||
| } | ||
| /** | ||
| * Returns true if the batcher is empty | ||
| * Returns a copy of all items in the batcher | ||
| */ | ||
| getIsEmpty(): boolean { | ||
| return this._items.length === 0 | ||
| peekAllItems = (): Array<TValue> => { | ||
| return [...this.store.state.items] | ||
| } | ||
| /** | ||
| * Returns true if the batcher is running | ||
| */ | ||
| getIsRunning(): boolean { | ||
| return this._running | ||
| #clearTimeout = (): void => { | ||
| if (this.#timeoutId) { | ||
| clearTimeout(this.#timeoutId) | ||
| this.#timeoutId = null | ||
| } | ||
| } | ||
| /** | ||
| * Returns a copy of all items currently in the batcher | ||
| * Removes all items from the batcher | ||
| */ | ||
| peekAllItems(): Array<TValue> { | ||
| return [...this._items] | ||
| clear = (): void => { | ||
| this.#setState({ items: [], isPending: false }) | ||
| } | ||
| /** | ||
| * Returns the number of times batches have been processed | ||
| * Resets the batcher state to its default values | ||
| */ | ||
| getBatchExecutionCount(): number { | ||
| return this._batchExecutionCount | ||
| reset = (): void => { | ||
| this.#setState(getDefaultBatcherState<TValue>()) | ||
| this.options.onItemsChange?.(this) | ||
| } | ||
| /** | ||
| * Returns the total number of individual items that have been processed | ||
| */ | ||
| getItemExecutionCount(): number { | ||
| return this._itemExecutionCount | ||
| } | ||
| } | ||
@@ -249,6 +312,9 @@ | ||
| * ```ts | ||
| * const batchItems = batch<number>({ | ||
| * batchSize: 3, | ||
| * processBatch: (items) => console.log('Processing:', items) | ||
| * }); | ||
| * const batchItems = batch<number>( | ||
| * (items) => console.log('Processing:', items), | ||
| * { | ||
| * maxSize: 3, | ||
| * onExecute: (batcher) => console.log('Batch executed') | ||
| * } | ||
| * ); | ||
| * | ||
@@ -265,3 +331,3 @@ * batchItems(1); | ||
| const batcher = new Batcher<TValue>(fn, options) | ||
| return batcher.addItem.bind(batcher) | ||
| return batcher.addItem | ||
| } |
+135
-52
@@ -0,4 +1,40 @@ | ||
| import { Store } from '@tanstack/store' | ||
| import { parseFunctionOrValue } from './utils' | ||
| import type { AnyFunction } from './types' | ||
| export interface DebouncerState<TFn extends AnyFunction> { | ||
| /** | ||
| * Whether the debouncer can execute on the leading edge of the timeout | ||
| */ | ||
| canLeadingExecute: boolean | ||
| /** | ||
| * Number of function executions that have been completed | ||
| */ | ||
| executionCount: number | ||
| /** | ||
| * Whether the debouncer is waiting for the timeout to trigger execution | ||
| */ | ||
| isPending: boolean | ||
| /** | ||
| * The arguments from the most recent call to maybeExecute | ||
| */ | ||
| lastArgs: Parameters<TFn> | undefined | ||
| /** | ||
| * Current execution status - 'idle' when not active, 'pending' when waiting for timeout | ||
| */ | ||
| status: 'disabled' | 'idle' | 'pending' | ||
| } | ||
| function getDefaultDebouncerState< | ||
| TFn extends AnyFunction, | ||
| >(): DebouncerState<TFn> { | ||
| return structuredClone({ | ||
| canLeadingExecute: true, | ||
| executionCount: 0, | ||
| isPending: false, | ||
| lastArgs: undefined, | ||
| status: 'idle', | ||
| }) | ||
| } | ||
| /** | ||
@@ -15,2 +51,6 @@ * Options for configuring a debounced function | ||
| /** | ||
| * Initial state for the debouncer | ||
| */ | ||
| initialState?: Partial<DebouncerState<TFn>> | ||
| /** | ||
| * Whether to execute on the leading edge of the timeout. | ||
@@ -38,6 +78,8 @@ * The first call will execute immediately and the rest will wait the delay. | ||
| const defaultOptions: Required<DebouncerOptions<any>> = { | ||
| const defaultOptions: Omit< | ||
| Required<DebouncerOptions<any>>, | ||
| 'initialState' | 'onExecute' | ||
| > = { | ||
| enabled: true, | ||
| leading: false, | ||
| onExecute: () => {}, | ||
| trailing: true, | ||
@@ -58,2 +100,10 @@ wait: 0, | ||
| * | ||
| * State Management: | ||
| * - Uses TanStack Store for reactive state management | ||
| * - Use `initialState` to provide initial state values when creating the debouncer | ||
| * - Use `onExecute` callback to react to function execution and implement custom logic | ||
| * - The state includes canLeadingExecute, execution count, and isPending status | ||
| * - State can be accessed via `debouncer.store.state` when using the class directly | ||
| * - When using framework adapters (React/Solid), state is accessed from `debouncer.state` | ||
| * | ||
| * @example | ||
@@ -72,7 +122,7 @@ * ```ts | ||
| export class Debouncer<TFn extends AnyFunction> { | ||
| private _canLeadingExecute = true | ||
| private _executionCount = 0 | ||
| private _isPending = false | ||
| private _options: Required<DebouncerOptions<TFn>> | ||
| private _timeoutId: NodeJS.Timeout | undefined | ||
| readonly store: Store<Readonly<DebouncerState<TFn>>> = new Store( | ||
| getDefaultDebouncerState<TFn>(), | ||
| ) | ||
| options: DebouncerOptions<TFn> | ||
| #timeoutId: NodeJS.Timeout | undefined | ||
@@ -83,6 +133,7 @@ constructor( | ||
| ) { | ||
| this._options = { | ||
| this.options = { | ||
| ...defaultOptions, | ||
| ...initialOptions, | ||
| } | ||
| this.#setState(this.options.initialState ?? {}) | ||
| } | ||
@@ -93,16 +144,27 @@ | ||
| */ | ||
| setOptions(newOptions: Partial<DebouncerOptions<TFn>>): void { | ||
| this._options = { ...this._options, ...newOptions } | ||
| setOptions = (newOptions: Partial<DebouncerOptions<TFn>>): void => { | ||
| this.options = { ...this.options, ...newOptions } | ||
| // End the pending state if the debouncer is disabled | ||
| if (!this._options.enabled) { | ||
| this._isPending = false | ||
| // Cancel pending execution if the debouncer is disabled | ||
| if (!this.#getEnabled()) { | ||
| this.cancel() | ||
| } | ||
| } | ||
| /** | ||
| * Returns the current debouncer options | ||
| */ | ||
| getOptions(): Required<DebouncerOptions<TFn>> { | ||
| return this._options | ||
| #setState = (newState: Partial<DebouncerState<TFn>>): void => { | ||
| this.store.setState((state) => { | ||
| const combinedState = { | ||
| ...state, | ||
| ...newState, | ||
| } | ||
| const { isPending } = combinedState | ||
| return { | ||
| ...combinedState, | ||
| status: !this.#getEnabled() | ||
| ? 'disabled' | ||
| : isPending | ||
| ? 'pending' | ||
| : 'idle', | ||
| } | ||
| }) | ||
| } | ||
@@ -113,4 +175,4 @@ | ||
| */ | ||
| getEnabled(): boolean { | ||
| return parseFunctionOrValue(this._options.enabled, this) | ||
| #getEnabled = (): boolean => { | ||
| return !!parseFunctionOrValue(this.options.enabled, this) | ||
| } | ||
@@ -121,4 +183,4 @@ | ||
| */ | ||
| getWait(): number { | ||
| return parseFunctionOrValue(this._options.wait, this) | ||
| #getWait = (): number => { | ||
| return parseFunctionOrValue(this.options.wait, this) | ||
| } | ||
@@ -130,60 +192,73 @@ | ||
| */ | ||
| maybeExecute(...args: Parameters<TFn>): void { | ||
| maybeExecute = (...args: Parameters<TFn>): void => { | ||
| if (!this.#getEnabled()) return undefined | ||
| let _didLeadingExecute = false | ||
| // Handle leading execution | ||
| if (this._options.leading && this._canLeadingExecute) { | ||
| this._canLeadingExecute = false | ||
| if (this.options.leading && this.store.state.canLeadingExecute) { | ||
| this.#setState({ canLeadingExecute: false }) | ||
| _didLeadingExecute = true | ||
| this.execute(...args) | ||
| this.#execute(...args) | ||
| } | ||
| // Start pending state to indicate that the debouncer is waiting for the trailing edge | ||
| if (this._options.trailing) { | ||
| this._isPending = true | ||
| if (this.options.trailing) { | ||
| this.#setState({ isPending: true, lastArgs: args }) | ||
| } | ||
| // Clear any existing timeout | ||
| if (this._timeoutId) clearTimeout(this._timeoutId) | ||
| if (this.#timeoutId) clearTimeout(this.#timeoutId) | ||
| // Set new timeout that will reset canLeadingExecute and execute trailing only if enabled and did not execute leading | ||
| this._timeoutId = setTimeout(() => { | ||
| this._canLeadingExecute = true | ||
| if (this._options.trailing && !_didLeadingExecute) { | ||
| this.execute(...args) | ||
| this.#timeoutId = setTimeout(() => { | ||
| this.#setState({ canLeadingExecute: true }) | ||
| if (this.options.trailing && !_didLeadingExecute) { | ||
| this.#execute(...args) | ||
| } | ||
| }, this.getWait()) | ||
| }, this.#getWait()) | ||
| } | ||
| private execute(...args: Parameters<TFn>): void { | ||
| if (!this.getEnabled()) return undefined | ||
| #execute = (...args: Parameters<TFn>): void => { | ||
| if (!this.#getEnabled()) return undefined | ||
| this.fn(...args) // EXECUTE! | ||
| this._isPending = false | ||
| this._executionCount++ | ||
| this._options.onExecute(this) | ||
| this.#setState({ | ||
| isPending: false, | ||
| executionCount: this.store.state.executionCount + 1, | ||
| }) | ||
| this.options.onExecute?.(this) | ||
| } | ||
| /** | ||
| * Cancels any pending execution | ||
| * Processes the current pending execution immediately | ||
| */ | ||
| cancel(): void { | ||
| if (this._timeoutId) { | ||
| clearTimeout(this._timeoutId) | ||
| this._canLeadingExecute = true | ||
| this._isPending = false | ||
| flush = (): void => { | ||
| if (this.store.state.isPending && this.store.state.lastArgs) { | ||
| this.#clearTimeout() // clear any pending timeout | ||
| this.#execute(...this.store.state.lastArgs) // execute immediately | ||
| } | ||
| } | ||
| #clearTimeout = (): void => { | ||
| if (this.#timeoutId) { | ||
| clearTimeout(this.#timeoutId) | ||
| this.#timeoutId = undefined | ||
| } | ||
| } | ||
| /** | ||
| * Returns the number of times the function has been executed | ||
| * Cancels any pending execution | ||
| */ | ||
| getExecutionCount(): number { | ||
| return this._executionCount | ||
| cancel = (): void => { | ||
| this.#clearTimeout() | ||
| this.#setState({ | ||
| canLeadingExecute: true, | ||
| isPending: false, | ||
| }) | ||
| } | ||
| /** | ||
| * Returns `true` if debouncing | ||
| * Resets the debouncer state to its default values | ||
| */ | ||
| getIsPending(): boolean { | ||
| return this.getEnabled() && this._isPending | ||
| reset = (): void => { | ||
| this.#setState(getDefaultDebouncerState<TFn>()) | ||
| } | ||
@@ -202,2 +277,10 @@ } | ||
| * | ||
| * State Management: | ||
| * - Uses TanStack Store for reactive state management | ||
| * - Use `initialState` to provide initial state values when creating the debouncer | ||
| * - Use `onExecute` callback to react to function execution and implement custom logic | ||
| * - The state includes canLeadingExecute, execution count, and isPending status | ||
| * - State can be accessed via the underlying Debouncer instance's `store.state` property | ||
| * - When using framework adapters (React/Solid), state is accessed from the hook's state property | ||
| * | ||
| * @example | ||
@@ -218,3 +301,3 @@ * ```ts | ||
| const debouncer = new Debouncer(fn, initialOptions) | ||
| return debouncer.maybeExecute.bind(debouncer) | ||
| return debouncer.maybeExecute | ||
| } |
+1
-1
@@ -0,1 +1,2 @@ | ||
| export * from './async-batcher' | ||
| export * from './async-debouncer' | ||
@@ -6,3 +7,2 @@ export * from './async-queuer' | ||
| export * from './batcher' | ||
| export * from './compare' | ||
| export * from './debouncer' | ||
@@ -9,0 +9,0 @@ export * from './queuer' |
+347
-225
@@ -0,3 +1,72 @@ | ||
| import { Store } from '@tanstack/store' | ||
| import { parseFunctionOrValue } from './utils' | ||
| export interface QueuerState<TValue> { | ||
| /** | ||
| * Number of items that have been processed by the queuer | ||
| */ | ||
| executionCount: number | ||
| /** | ||
| * Number of items that have been removed from the queue due to expiration | ||
| */ | ||
| expirationCount: number | ||
| /** | ||
| * Whether the queuer has no items to process (items array is empty) | ||
| */ | ||
| isEmpty: boolean | ||
| /** | ||
| * Whether the queuer has reached its maximum capacity | ||
| */ | ||
| isFull: boolean | ||
| /** | ||
| * Whether the queuer is not currently processing any items | ||
| */ | ||
| isIdle: boolean | ||
| /** | ||
| * Whether the queuer is active and will process items automatically | ||
| */ | ||
| isRunning: boolean | ||
| /** | ||
| * Timestamps when items were added to the queue for expiration tracking | ||
| */ | ||
| itemTimestamps: Array<number> | ||
| /** | ||
| * Array of items currently waiting to be processed | ||
| */ | ||
| items: Array<TValue> | ||
| /** | ||
| * Whether the queuer has a pending timeout for processing the next item | ||
| */ | ||
| pendingTick: boolean | ||
| /** | ||
| * Number of items that have been rejected from being added to the queue | ||
| */ | ||
| rejectionCount: number | ||
| /** | ||
| * Number of items currently in the queue | ||
| */ | ||
| size: number | ||
| /** | ||
| * Current processing status - 'idle' when not processing, 'running' when active, 'stopped' when paused | ||
| */ | ||
| status: 'idle' | 'running' | 'stopped' | ||
| } | ||
| function getDefaultQueuerState<TValue>(): QueuerState<TValue> { | ||
| return { | ||
| executionCount: 0, | ||
| expirationCount: 0, | ||
| isEmpty: true, | ||
| isFull: false, | ||
| isIdle: true, | ||
| isRunning: true, | ||
| itemTimestamps: [], | ||
| items: [], | ||
| pendingTick: false, | ||
| rejectionCount: 0, | ||
| size: 0, | ||
| status: 'idle', | ||
| } | ||
| } | ||
| /** | ||
@@ -39,2 +108,6 @@ * Options for configuring a Queuer instance. | ||
| /** | ||
| * Initial state for the queuer | ||
| */ | ||
| initialState?: Partial<QueuerState<TValue>> | ||
| /** | ||
| * Maximum number of items allowed in the queuer | ||
@@ -52,6 +125,2 @@ */ | ||
| /** | ||
| * Callback fired whenever the queuer's running state changes | ||
| */ | ||
| onIsRunningChange?: (queuer: Queuer<TValue>) => void | ||
| /** | ||
| * Callback fired whenever an item is added or removed from the queuer | ||
@@ -76,3 +145,11 @@ */ | ||
| const defaultOptions: Required<QueuerOptions<any>> = { | ||
| const defaultOptions: Omit< | ||
| Required<QueuerOptions<any>>, | ||
| | 'initialState' | ||
| | 'onExecute' | ||
| | 'onIsRunningChange' | ||
| | 'onItemsChange' | ||
| | 'onReject' | ||
| | 'onExpire' | ||
| > = { | ||
| addItemsTo: 'back', | ||
@@ -85,7 +162,2 @@ getItemsFrom: 'front', | ||
| maxSize: Infinity, | ||
| onExecute: () => {}, | ||
| onIsRunningChange: () => {}, | ||
| onItemsChange: () => {}, | ||
| onReject: () => {}, | ||
| onExpire: () => {}, | ||
| started: true, | ||
@@ -114,3 +186,3 @@ wait: 0, | ||
| * Running behavior: | ||
| * - `start()`: Begins automatically processing items in the queue (defaults to running) | ||
| * - `start()`: Begins automatically processing items in the queue (defaults to isRunning) | ||
| * - `stop()`: Pauses processing but maintains queue state | ||
@@ -144,2 +216,13 @@ * - `wait`: Configurable delay between processing items | ||
| * | ||
| * State Management: | ||
| * - Uses TanStack Store for reactive state management | ||
| * - Use `initialState` to provide initial state values when creating the queuer | ||
| * - Use `onExecute` callback to react to item execution and implement custom logic | ||
| * - Use `onItemsChange` callback to react to items being added or removed from the queue | ||
| * - Use `onExpire` callback to react to items expiring and implement custom logic | ||
| * - Use `onReject` callback to react to items being rejected when the queue is full | ||
| * - The state includes execution count, expiration count, rejection count, and isRunning status | ||
| * - State can be accessed via `queuer.store.state` when using the class directly | ||
| * - When using framework adapters (React/Solid), state is accessed from `queuer.state` | ||
| * | ||
| * Example usage: | ||
@@ -167,11 +250,7 @@ * ```ts | ||
| export class Queuer<TValue> { | ||
| private _options: Required<QueuerOptions<TValue>> | ||
| private _items: Array<TValue> = [] | ||
| private _itemTimestamps: Array<number> = [] | ||
| private _executionCount = 0 | ||
| private _rejectionCount = 0 | ||
| private _expirationCount = 0 | ||
| private _onItemsChanges: Array<(item: TValue) => void> = [] | ||
| private _running: boolean | ||
| private _pendingTick = false | ||
| readonly store: Store<Readonly<QueuerState<TValue>>> = new Store( | ||
| getDefaultQueuerState<TValue>(), | ||
| ) | ||
| options: QueuerOptions<TValue> | ||
| #timeoutId: NodeJS.Timeout | null = null | ||
@@ -182,9 +261,23 @@ constructor( | ||
| ) { | ||
| this._options = { ...defaultOptions, ...initialOptions } | ||
| this._running = this._options.started | ||
| this.options = { | ||
| ...defaultOptions, | ||
| ...initialOptions, | ||
| } | ||
| const isInitiallyRunning = | ||
| this.options.initialState?.isRunning ?? this.options.started ?? true | ||
| this.#setState({ | ||
| ...this.options.initialState, | ||
| isRunning: isInitiallyRunning, | ||
| }) | ||
| for (let i = 0; i < this._options.initialItems.length; i++) { | ||
| const item = this._options.initialItems[i]! | ||
| const isLast = i === this._options.initialItems.length - 1 | ||
| this.addItem(item, this._options.addItemsTo, isLast) | ||
| if (this.options.initialState?.items) { | ||
| if (this.store.state.isRunning) { | ||
| this.#tick() | ||
| } | ||
| } else { | ||
| for (let i = 0; i < (this.options.initialItems?.length ?? 0); i++) { | ||
| const item = this.options.initialItems![i]! | ||
| const isLast = i === (this.options.initialItems?.length ?? 0) - 1 | ||
| this.addItem(item, this.options.addItemsTo ?? 'back', isLast) | ||
| } | ||
| } | ||
@@ -196,11 +289,31 @@ } | ||
| */ | ||
| setOptions(newOptions: Partial<QueuerOptions<TValue>>): void { | ||
| this._options = { ...this._options, ...newOptions } | ||
| setOptions = (newOptions: Partial<QueuerOptions<TValue>>): void => { | ||
| this.options = { ...this.options, ...newOptions } | ||
| } | ||
| /** | ||
| * Returns the current queuer options, including defaults and any overrides. | ||
| */ | ||
| getOptions(): Required<QueuerOptions<TValue>> { | ||
| return this._options | ||
| #setState = (newState: Partial<QueuerState<TValue>>): void => { | ||
| this.store.setState((state) => { | ||
| const combinedState = { | ||
| ...state, | ||
| ...newState, | ||
| } | ||
| const { items, isRunning } = combinedState | ||
| const size = items.length | ||
| const isFull = size >= (this.options.maxSize ?? Infinity) | ||
| const isEmpty = size === 0 | ||
| const isIdle = isRunning && isEmpty | ||
| const status = isIdle ? 'idle' : isRunning ? 'running' : 'stopped' | ||
| return { | ||
| ...combinedState, | ||
| isEmpty, | ||
| isFull, | ||
| isIdle, | ||
| size, | ||
| status, | ||
| } | ||
| }) | ||
| } | ||
@@ -212,4 +325,4 @@ | ||
| */ | ||
| getWait(): number { | ||
| return parseFunctionOrValue(this._options.wait, this) | ||
| #getWait = (): number => { | ||
| return parseFunctionOrValue(this.options.wait ?? 0, this) | ||
| } | ||
@@ -220,124 +333,32 @@ | ||
| */ | ||
| private tick() { | ||
| if (!this._running) { | ||
| this._pendingTick = false | ||
| #tick = () => { | ||
| if (!this.store.state.isRunning) { | ||
| this.#setState({ pendingTick: false }) | ||
| return | ||
| } | ||
| this.#setState({ pendingTick: true }) | ||
| // Check for expired items | ||
| this.checkExpiredItems() | ||
| this.#checkExpiredItems() | ||
| while (!this.getIsEmpty()) { | ||
| const nextItem = this.execute(this._options.getItemsFrom) | ||
| while (!this.store.state.isEmpty) { | ||
| const nextItem = this.execute(this.options.getItemsFrom ?? 'front') | ||
| if (nextItem === undefined) { | ||
| break | ||
| } | ||
| this._onItemsChanges.forEach((cb) => cb(nextItem)) | ||
| const wait = this.getWait() | ||
| const wait = this.#getWait() | ||
| if (wait > 0) { | ||
| // Use setTimeout to wait before processing next item | ||
| setTimeout(() => this.tick(), wait) | ||
| this.#timeoutId = setTimeout(() => this.#tick(), wait) | ||
| return | ||
| } | ||
| this.tick() | ||
| this.#tick() | ||
| } | ||
| this._pendingTick = false | ||
| this.#setState({ pendingTick: false }) | ||
| } | ||
| /** | ||
| * Checks for expired items in the queue and removes them. Calls onExpire for each expired item. | ||
| * Internal use only. | ||
| */ | ||
| private checkExpiredItems() { | ||
| if ( | ||
| this._options.expirationDuration === Infinity && | ||
| this._options.getIsExpired === defaultOptions.getIsExpired | ||
| ) | ||
| return | ||
| const now = Date.now() | ||
| const expiredIndices: Array<number> = [] | ||
| // Find indices of expired items | ||
| for (let i = 0; i < this._items.length; i++) { | ||
| const timestamp = this._itemTimestamps[i] | ||
| if (timestamp === undefined) continue | ||
| const item = this._items[i] | ||
| if (item === undefined) continue | ||
| const isExpired = | ||
| this._options.getIsExpired !== defaultOptions.getIsExpired | ||
| ? this._options.getIsExpired(item, timestamp) | ||
| : now - timestamp > this._options.expirationDuration | ||
| if (isExpired) { | ||
| expiredIndices.push(i) | ||
| } | ||
| } | ||
| // Remove expired items from back to front to maintain indices | ||
| for (let i = expiredIndices.length - 1; i >= 0; i--) { | ||
| const index = expiredIndices[i] | ||
| if (index === undefined) continue | ||
| const expiredItem = this._items[index] | ||
| if (expiredItem === undefined) continue | ||
| this._items.splice(index, 1) | ||
| this._itemTimestamps.splice(index, 1) | ||
| this._expirationCount++ | ||
| this._options.onExpire(expiredItem, this) | ||
| } | ||
| if (expiredIndices.length > 0) { | ||
| this._options.onItemsChange(this) | ||
| } | ||
| } | ||
| /** | ||
| * Stops processing items in the queue. Does not clear the queue. | ||
| */ | ||
| stop() { | ||
| this._running = false | ||
| this._pendingTick = false | ||
| this._options.onIsRunningChange(this) | ||
| } | ||
| /** | ||
| * Starts processing items in the queue. If already running, does nothing. | ||
| */ | ||
| start() { | ||
| this._running = true | ||
| if (!this._pendingTick && !this.getIsEmpty()) { | ||
| this._pendingTick = true | ||
| this.tick() | ||
| } | ||
| this._options.onIsRunningChange(this) | ||
| } | ||
| /** | ||
| * Removes all pending items from the queue. Does not affect items being processed. | ||
| */ | ||
| clear(): void { | ||
| this._items = [] | ||
| this._options.onItemsChange(this) | ||
| } | ||
| /** | ||
| * Resets the queuer to its initial state. Optionally repopulates with initial items. | ||
| * Does not affect callbacks or options. | ||
| */ | ||
| reset(withInitialItems?: boolean): void { | ||
| this.clear() | ||
| this._executionCount = 0 | ||
| if (withInitialItems) { | ||
| this._items = [...this._options.initialItems] | ||
| } | ||
| this._running = this._options.started | ||
| } | ||
| /** | ||
| * Adds an item to the queue. If the queue is full, the item is rejected and onReject is called. | ||
@@ -354,45 +375,67 @@ * Items can be inserted based on priority or at the front/back depending on configuration. | ||
| */ | ||
| addItem( | ||
| addItem = ( | ||
| item: TValue, | ||
| position: QueuePosition = this._options.addItemsTo, | ||
| runOnUpdate: boolean = true, | ||
| ): boolean { | ||
| if (this.getIsFull()) { | ||
| this._rejectionCount++ | ||
| this._options.onReject(item, this) | ||
| position: QueuePosition = this.options.addItemsTo ?? 'back', | ||
| runOnItemsChange: boolean = true, | ||
| ): boolean => { | ||
| if (this.store.state.isFull) { | ||
| this.#setState({ | ||
| rejectionCount: this.store.state.rejectionCount + 1, | ||
| }) | ||
| this.options.onReject?.(item, this) | ||
| return false | ||
| } | ||
| if (this._options.getPriority !== defaultOptions.getPriority) { | ||
| // If custom priority function is provided, insert based on priority | ||
| const priority = this._options.getPriority(item) | ||
| const insertIndex = this._items.findIndex( | ||
| (existing) => this._options.getPriority(existing) < priority, | ||
| ) | ||
| // Get priority either from the function or from getPriority option | ||
| const priority = | ||
| this.options.getPriority !== defaultOptions.getPriority | ||
| ? this.options.getPriority!(item) | ||
| : (item as any).priority | ||
| const items = this.store.state.items | ||
| const itemTimestamps = this.store.state.itemTimestamps | ||
| if (priority !== undefined) { | ||
| // Insert based on priority - higher priority items go to front | ||
| const insertIndex = items.findIndex((existing) => { | ||
| const existingPriority: number = | ||
| this.options.getPriority !== defaultOptions.getPriority | ||
| ? this.options.getPriority!(existing) | ||
| : (existing as any).priority | ||
| return existingPriority < priority | ||
| }) | ||
| if (insertIndex === -1) { | ||
| this._items.push(item) | ||
| this._itemTimestamps.push(Date.now()) | ||
| items.push(item) | ||
| itemTimestamps.push(Date.now()) | ||
| } else { | ||
| this._items.splice(insertIndex, 0, item) | ||
| this._itemTimestamps.splice(insertIndex, 0, Date.now()) | ||
| items.splice(insertIndex, 0, item) | ||
| itemTimestamps.splice(insertIndex, 0, Date.now()) | ||
| } | ||
| } else { | ||
| // Default FIFO/LIFO behavior | ||
| if (position === 'front') { | ||
| this._items.unshift(item) | ||
| this._itemTimestamps.unshift(Date.now()) | ||
| // Default FIFO/LIFO behavior | ||
| items.unshift(item) | ||
| itemTimestamps.unshift(Date.now()) | ||
| } else { | ||
| this._items.push(item) | ||
| this._itemTimestamps.push(Date.now()) | ||
| // LIFO | ||
| items.push(item) | ||
| itemTimestamps.push(Date.now()) | ||
| } | ||
| } | ||
| if (this._running && !this._pendingTick) { | ||
| this._pendingTick = true | ||
| this.tick() | ||
| this.#setState({ | ||
| items, | ||
| itemTimestamps, | ||
| }) | ||
| if (runOnItemsChange) { | ||
| this.options.onItemsChange?.(this) | ||
| } | ||
| if (runOnUpdate) { | ||
| this._options.onItemsChange(this) | ||
| if (this.store.state.isRunning && !this.store.state.pendingTick) { | ||
| this.#setState({ pendingTick: true }) | ||
| this.#tick() | ||
| } | ||
| return true | ||
@@ -413,17 +456,28 @@ } | ||
| */ | ||
| getNextItem( | ||
| position: QueuePosition = this._options.getItemsFrom, | ||
| ): TValue | undefined { | ||
| getNextItem = ( | ||
| position: QueuePosition = this.options.getItemsFrom ?? 'front', | ||
| ): TValue | undefined => { | ||
| const { items, itemTimestamps } = this.store.state | ||
| let item: TValue | undefined | ||
| if (position === 'front') { | ||
| item = this._items.shift() | ||
| this._itemTimestamps.shift() | ||
| item = items[0] | ||
| if (item !== undefined) { | ||
| this.#setState({ | ||
| items: items.slice(1), | ||
| itemTimestamps: itemTimestamps.slice(1), | ||
| }) | ||
| } | ||
| } else { | ||
| item = this._items.pop() | ||
| this._itemTimestamps.pop() | ||
| item = items[items.length - 1] | ||
| if (item !== undefined) { | ||
| this.#setState({ | ||
| items: items.slice(0, -1), | ||
| itemTimestamps: itemTimestamps.slice(0, -1), | ||
| }) | ||
| } | ||
| } | ||
| if (item !== undefined) { | ||
| this._options.onItemsChange(this) | ||
| this.options.onItemsChange?.(this) | ||
| } | ||
@@ -444,8 +498,10 @@ | ||
| */ | ||
| execute(position?: QueuePosition): TValue | undefined { | ||
| execute = (position?: QueuePosition): TValue | undefined => { | ||
| const item = this.getNextItem(position) | ||
| if (item !== undefined) { | ||
| this.fn(item) | ||
| this._executionCount++ | ||
| this._options.onExecute(item, this) | ||
| this.#setState({ | ||
| executionCount: this.store.state.executionCount + 1, | ||
| }) | ||
| this.options.onExecute?.(item, this) | ||
| } | ||
@@ -456,38 +512,87 @@ return item | ||
| /** | ||
| * Returns the next item in the queue without removing it. | ||
| * | ||
| * Example usage: | ||
| * ```ts | ||
| * queuer.peekNextItem(); // front | ||
| * queuer.peekNextItem('back'); // back | ||
| * ``` | ||
| * Processes a specified number of items to execute immediately with no wait time | ||
| * If no numberOfItems is provided, all items will be processed | ||
| */ | ||
| peekNextItem( | ||
| position: QueuePosition = this._options.getItemsFrom, | ||
| ): TValue | undefined { | ||
| if (position === 'front') { | ||
| return this._items[0] | ||
| flush = ( | ||
| numberOfItems: number = this.store.state.items.length, | ||
| position?: QueuePosition, | ||
| ): void => { | ||
| this.#clearTimeout() // clear any pending timeout | ||
| for (let i = 0; i < numberOfItems; i++) { | ||
| this.execute(position) | ||
| } | ||
| return this._items[this._items.length - 1] | ||
| } | ||
| /** | ||
| * Returns true if the queue is empty (no pending items). | ||
| * Checks for expired items in the queue and removes them. Calls onExpire for each expired item. | ||
| * Internal use only. | ||
| */ | ||
| getIsEmpty(): boolean { | ||
| return this._items.length === 0 | ||
| } | ||
| #checkExpiredItems = (): void => { | ||
| if ( | ||
| (this.options.expirationDuration ?? Infinity) === Infinity && | ||
| this.options.getIsExpired === defaultOptions.getIsExpired | ||
| ) { | ||
| return | ||
| } | ||
| /** | ||
| * Returns true if the queue is full (reached maxSize). | ||
| */ | ||
| getIsFull(): boolean { | ||
| return this._items.length >= this._options.maxSize | ||
| const now = Date.now() | ||
| const expiredIndices: Array<number> = [] | ||
| // Find indices of expired items | ||
| for (let i = 0; i < this.store.state.items.length; i++) { | ||
| const timestamp = this.store.state.itemTimestamps[i] | ||
| if (timestamp === undefined) continue | ||
| const item = this.store.state.items[i] | ||
| if (item === undefined) continue | ||
| const isExpired = | ||
| this.options.getIsExpired !== defaultOptions.getIsExpired | ||
| ? this.options.getIsExpired!(item, timestamp) | ||
| : now - timestamp > (this.options.expirationDuration ?? Infinity) | ||
| if (isExpired) { | ||
| expiredIndices.push(i) | ||
| } | ||
| } | ||
| // Remove expired items from back to front to maintain indices | ||
| for (let i = expiredIndices.length - 1; i >= 0; i--) { | ||
| const index = expiredIndices[i] | ||
| if (index === undefined) continue | ||
| const expiredItem = this.store.state.items[index] | ||
| if (expiredItem === undefined) continue | ||
| const newItems = [...this.store.state.items] | ||
| const newTimestamps = [...this.store.state.itemTimestamps] | ||
| newItems.splice(index, 1) | ||
| newTimestamps.splice(index, 1) | ||
| this.#setState({ | ||
| items: newItems, | ||
| itemTimestamps: newTimestamps, | ||
| expirationCount: this.store.state.expirationCount + 1, | ||
| }) | ||
| this.options.onExpire?.(expiredItem, this) | ||
| } | ||
| if (expiredIndices.length > 0) { | ||
| this.options.onItemsChange?.(this) | ||
| } | ||
| } | ||
| /** | ||
| * Returns the number of pending items in the queue. | ||
| * Returns the next item in the queue without removing it. | ||
| * | ||
| * Example usage: | ||
| * ```ts | ||
| * queuer.peekNextItem(); // front | ||
| * queuer.peekNextItem('back'); // back | ||
| * ``` | ||
| */ | ||
| getSize(): number { | ||
| return this._items.length | ||
| peekNextItem = (position: QueuePosition = 'front'): TValue | undefined => { | ||
| if (position === 'front') { | ||
| return this.store.state.items[0] | ||
| } | ||
| return this.store.state.items[this.store.state.size - 1] | ||
| } | ||
@@ -498,39 +603,45 @@ | ||
| */ | ||
| peekAllItems(): Array<TValue> { | ||
| return [...this._items] | ||
| peekAllItems = (): Array<TValue> => { | ||
| return [...this.store.state.items] | ||
| } | ||
| /** | ||
| * Returns the number of items that have been processed and removed from the queue. | ||
| * Starts processing items in the queue. If already isRunning, does nothing. | ||
| */ | ||
| getExecutionCount(): number { | ||
| return this._executionCount | ||
| start = () => { | ||
| this.#setState({ isRunning: true }) | ||
| if (!this.store.state.pendingTick && !this.store.state.isEmpty) { | ||
| this.#tick() | ||
| } | ||
| } | ||
| /** | ||
| * Returns the number of items that have been rejected from being added to the queue. | ||
| * Stops processing items in the queue. Does not clear the queue. | ||
| */ | ||
| getRejectionCount(): number { | ||
| return this._rejectionCount | ||
| stop = () => { | ||
| this.#clearTimeout() | ||
| this.#setState({ isRunning: false, pendingTick: false }) | ||
| } | ||
| /** | ||
| * Returns the number of items that have expired and been removed from the queue. | ||
| */ | ||
| getExpirationCount(): number { | ||
| return this._expirationCount | ||
| #clearTimeout = (): void => { | ||
| if (this.#timeoutId) { | ||
| clearTimeout(this.#timeoutId) | ||
| this.#timeoutId = null | ||
| } | ||
| } | ||
| /** | ||
| * Returns true if the queuer is currently running (processing items). | ||
| * Removes all pending items from the queue. Does not affect items being processed. | ||
| */ | ||
| getIsRunning() { | ||
| return this._running | ||
| clear = (): void => { | ||
| this.#setState({ items: [], itemTimestamps: [] }) | ||
| this.options.onItemsChange?.(this) | ||
| } | ||
| /** | ||
| * Returns true if the queuer is running but has no items to process. | ||
| * Resets the queuer state to its default values | ||
| */ | ||
| getIsIdle() { | ||
| return this._running && this.getIsEmpty() | ||
| reset = (): void => { | ||
| this.#setState(getDefaultQueuerState<TValue>()) | ||
| this.options.onItemsChange?.(this) | ||
| } | ||
@@ -544,5 +655,16 @@ } | ||
| * This is a simplified wrapper around the Queuer class that only exposes the | ||
| * `addItem` method. The queue is always running and will process items as they are added. | ||
| * `addItem` method. The queue is always isRunning and will process items as they are added. | ||
| * For more control over queue processing, use the Queuer class directly. | ||
| * | ||
| * State Management: | ||
| * - Uses TanStack Store for reactive state management | ||
| * - Use `initialState` to provide initial state values when creating the queuer | ||
| * - Use `onExecute` callback to react to item execution and implement custom logic | ||
| * - Use `onItemsChange` callback to react to items being added or removed from the queue | ||
| * - Use `onExpire` callback to react to items expiring and implement custom logic | ||
| * - Use `onReject` callback to react to items being rejected when the queue is full | ||
| * - The state includes execution count, expiration count, rejection count, and isRunning status | ||
| * - State can be accessed via the underlying Queuer instance's `store.state` property | ||
| * - When using framework adapters (React/Solid), state is accessed from the hook's state property | ||
| * | ||
| * Example usage: | ||
@@ -568,6 +690,6 @@ * ```ts | ||
| fn: (item: TValue) => void, | ||
| options: QueuerOptions<TValue>, | ||
| initialOptions: QueuerOptions<TValue>, | ||
| ) { | ||
| const queuer = new Queuer<TValue>(fn, options) | ||
| return queuer.addItem.bind(queuer) | ||
| const queuer = new Queuer<TValue>(fn, initialOptions) | ||
| return queuer.addItem | ||
| } |
+125
-80
@@ -0,4 +1,28 @@ | ||
| import { Store } from '@tanstack/store' | ||
| import { parseFunctionOrValue } from './utils' | ||
| import type { AnyFunction } from './types' | ||
| export interface RateLimiterState { | ||
| /** | ||
| * Number of function executions that have been completed | ||
| */ | ||
| executionCount: number | ||
| /** | ||
| * Array of timestamps when executions occurred for rate limiting calculations | ||
| */ | ||
| executionTimes: Array<number> | ||
| /** | ||
| * Number of function executions that have been rejected due to rate limiting | ||
| */ | ||
| rejectionCount: number | ||
| } | ||
| function getDefaultRateLimiterState(): RateLimiterState { | ||
| return structuredClone({ | ||
| executionCount: 0, | ||
| executionTimes: [], | ||
| rejectionCount: 0, | ||
| }) | ||
| } | ||
| /** | ||
@@ -14,2 +38,6 @@ * Options for configuring a rate-limited function | ||
| /** | ||
| * Initial state for the rate limiter | ||
| */ | ||
| initialState?: Partial<RateLimiterState> | ||
| /** | ||
| * Maximum number of executions allowed within the time window. | ||
@@ -41,7 +69,8 @@ * Can be a number or a callback function that receives the rate limiter instance and returns a number. | ||
| const defaultOptions: Required<RateLimiterOptions<any>> = { | ||
| const defaultOptions: Omit< | ||
| Required<RateLimiterOptions<any>>, | ||
| 'initialState' | 'onExecute' | 'onReject' | ||
| > = { | ||
| enabled: true, | ||
| limit: 1, | ||
| onExecute: () => {}, | ||
| onReject: () => {}, | ||
| window: 0, | ||
@@ -71,2 +100,11 @@ windowType: 'fixed', | ||
| * | ||
| * State Management: | ||
| * - Uses TanStack Store for reactive state management | ||
| * - Use `initialState` to provide initial state values when creating the rate limiter | ||
| * - Use `onExecute` callback to react to function execution and implement custom logic | ||
| * - Use `onReject` callback to react to executions being rejected when rate limit is exceeded | ||
| * - The state includes execution count, execution times, and rejection count | ||
| * - State can be accessed via `rateLimiter.store.state` when using the class directly | ||
| * - When using framework adapters (React/Solid), state is accessed from `rateLimiter.state` | ||
| * | ||
| * @example | ||
@@ -76,3 +114,7 @@ * ```ts | ||
| * (id: string) => api.getData(id), | ||
| * { limit: 5, window: 1000, windowType: 'sliding' } // 5 calls per second with sliding window | ||
| * { | ||
| * limit: 5, | ||
| * window: 1000, | ||
| * windowType: 'sliding', | ||
| * } | ||
| * ); | ||
@@ -85,6 +127,5 @@ * | ||
| export class RateLimiter<TFn extends AnyFunction> { | ||
| private _executionCount = 0 | ||
| private _rejectionCount = 0 | ||
| private _executionTimes: Array<number> = [] | ||
| private _options: RateLimiterOptions<TFn> | ||
| readonly store: Store<Readonly<RateLimiterState>> = | ||
| new Store<RateLimiterState>(getDefaultRateLimiterState()) | ||
| options: RateLimiterOptions<TFn> | ||
@@ -95,6 +136,7 @@ constructor( | ||
| ) { | ||
| this._options = { | ||
| this.options = { | ||
| ...defaultOptions, | ||
| ...initialOptions, | ||
| } | ||
| this.#setState(this.options.initialState ?? {}) | ||
| } | ||
@@ -105,11 +147,14 @@ | ||
| */ | ||
| setOptions(newOptions: Partial<RateLimiterOptions<TFn>>): void { | ||
| this._options = { ...this._options, ...newOptions } | ||
| setOptions = (newOptions: Partial<RateLimiterOptions<TFn>>): void => { | ||
| this.options = { ...this.options, ...newOptions } | ||
| } | ||
| /** | ||
| * Returns the current rate limiter options | ||
| */ | ||
| getOptions(): Required<RateLimiterOptions<TFn>> { | ||
| return this._options as Required<RateLimiterOptions<TFn>> | ||
| #setState = (newState: Partial<RateLimiterState>): void => { | ||
| this.store.setState((state) => { | ||
| const combinedState = { | ||
| ...state, | ||
| ...newState, | ||
| } | ||
| return combinedState | ||
| }) | ||
| } | ||
@@ -120,4 +165,4 @@ | ||
| */ | ||
| getEnabled(): boolean { | ||
| return parseFunctionOrValue(this._options.enabled, this)! | ||
| #getEnabled = (): boolean => { | ||
| return !!parseFunctionOrValue(this.options.enabled, this) | ||
| } | ||
@@ -128,4 +173,4 @@ | ||
| */ | ||
| getLimit(): number { | ||
| return parseFunctionOrValue(this._options.limit, this) | ||
| #getLimit = (): number => { | ||
| return parseFunctionOrValue(this.options.limit, this) | ||
| } | ||
@@ -136,4 +181,4 @@ | ||
| */ | ||
| getWindow(): number { | ||
| return parseFunctionOrValue(this._options.window, this) | ||
| #getWindow = (): number => { | ||
| return parseFunctionOrValue(this.options.window, this) | ||
| } | ||
@@ -156,71 +201,64 @@ | ||
| */ | ||
| maybeExecute(...args: Parameters<TFn>): boolean { | ||
| this.cleanupOldExecutions() | ||
| maybeExecute = (...args: Parameters<TFn>): boolean => { | ||
| this.#cleanupOldExecutions() | ||
| if (this._options.windowType === 'sliding') { | ||
| // For sliding window, we can execute if we have capacity in the current window | ||
| if (this._executionTimes.length < this.getLimit()) { | ||
| this.execute(...args) | ||
| return true | ||
| } | ||
| } else { | ||
| // For fixed window, we need to check if we're in a new window | ||
| const now = Date.now() | ||
| const oldestExecution = Math.min(...this._executionTimes) | ||
| const isNewWindow = oldestExecution + this.getWindow() <= now | ||
| const relevantExecutionTimes = this.#getRelevantExecutionTimes() | ||
| if (isNewWindow || this._executionTimes.length < this.getLimit()) { | ||
| this.execute(...args) | ||
| return true | ||
| } | ||
| if (relevantExecutionTimes.length < this.#getLimit()) { | ||
| this.#execute(...args) | ||
| return true | ||
| } | ||
| this.rejectFunction() | ||
| this.#setState({ | ||
| rejectionCount: this.store.state.rejectionCount + 1, | ||
| }) | ||
| this.options.onReject?.(this) | ||
| return false | ||
| } | ||
| private execute(...args: Parameters<TFn>): void { | ||
| if (!this.getEnabled()) return | ||
| #execute = (...args: Parameters<TFn>): void => { | ||
| if (!this.#getEnabled()) return | ||
| const now = Date.now() | ||
| this._executionCount++ | ||
| this._executionTimes.push(now) | ||
| this.fn(...args) // execute the function | ||
| this._options.onExecute?.(this) | ||
| this.fn(...args) // EXECUTE! | ||
| this.store.state.executionTimes.push(now) // mutate state directly for performance | ||
| this.#setState({ | ||
| executionCount: this.store.state.executionCount + 1, | ||
| }) | ||
| this.options.onExecute?.(this) | ||
| } | ||
| private rejectFunction(): void { | ||
| this._rejectionCount++ | ||
| if (this._options.onReject) { | ||
| this._options.onReject(this) | ||
| #getRelevantExecutionTimes = (): Array<number> => { | ||
| if (this.options.windowType === 'sliding') { | ||
| // For sliding window, return all executions within the current window | ||
| return this.store.state.executionTimes.filter( | ||
| (time) => time > Date.now() - this.#getWindow(), | ||
| ) | ||
| } else { | ||
| // For fixed window, return all executions in the current window | ||
| // The window starts from the oldest execution time | ||
| const oldestExecution = Math.min(...this.store.state.executionTimes) | ||
| const windowStart = oldestExecution | ||
| return this.store.state.executionTimes.filter( | ||
| (time) => | ||
| time >= windowStart && time <= windowStart + this.#getWindow(), | ||
| ) | ||
| } | ||
| } | ||
| private cleanupOldExecutions(): void { | ||
| #cleanupOldExecutions = (): void => { | ||
| const now = Date.now() | ||
| const windowStart = now - this.getWindow() | ||
| this._executionTimes = this._executionTimes.filter( | ||
| (time) => time > windowStart, | ||
| ) | ||
| const windowStart = now - this.#getWindow() | ||
| this.#setState({ | ||
| executionTimes: this.store.state.executionTimes.filter( | ||
| (time) => time > windowStart, | ||
| ), | ||
| }) | ||
| } | ||
| /** | ||
| * Returns the number of times the function has been executed | ||
| */ | ||
| getExecutionCount(): number { | ||
| return this._executionCount | ||
| } | ||
| /** | ||
| * Returns the number of times the function has been rejected | ||
| */ | ||
| getRejectionCount(): number { | ||
| return this._rejectionCount | ||
| } | ||
| /** | ||
| * Returns the number of remaining executions allowed in the current window | ||
| */ | ||
| getRemainingInWindow(): number { | ||
| this.cleanupOldExecutions() | ||
| return Math.max(0, this.getLimit() - this._executionTimes.length) | ||
| getRemainingInWindow = (): number => { | ||
| const relevantExecutionTimes = this.#getRelevantExecutionTimes() | ||
| return Math.max(0, this.#getLimit() - relevantExecutionTimes.length) | ||
| } | ||
@@ -231,8 +269,8 @@ | ||
| */ | ||
| getMsUntilNextWindow(): number { | ||
| getMsUntilNextWindow = (): number => { | ||
| if (this.getRemainingInWindow() > 0) { | ||
| return 0 | ||
| } | ||
| const oldestExecution = Math.min(...this._executionTimes) | ||
| return oldestExecution + this.getWindow() - Date.now() | ||
| const oldestExecution = this.store.state.executionTimes[0] ?? Infinity | ||
| return oldestExecution + this.#getWindow() - Date.now() | ||
| } | ||
@@ -243,6 +281,4 @@ | ||
| */ | ||
| reset(): void { | ||
| this._executionTimes = [] | ||
| this._executionCount = 0 | ||
| this._rejectionCount = 0 | ||
| reset = (): void => { | ||
| this.#setState(getDefaultRateLimiterState()) | ||
| } | ||
@@ -265,2 +301,11 @@ } | ||
| * | ||
| * State Management: | ||
| * - Uses TanStack Store for reactive state management | ||
| * - Use `initialState` to provide initial state values when creating the rate limiter | ||
| * - Use `onExecute` callback to react to function execution and implement custom logic | ||
| * - Use `onReject` callback to react to executions being rejected when rate limit is exceeded | ||
| * - The state includes execution count, execution times, and rejection count | ||
| * - State can be accessed via the underlying RateLimiter instance's `store.state` property | ||
| * - When using framework adapters (React/Solid), state is accessed from the hook's state property | ||
| * | ||
| * Consider using throttle() or debounce() if you need more intelligent execution control. Use rate limiting when you specifically | ||
@@ -294,3 +339,3 @@ * need to enforce a hard limit on the number of executions within a time period. | ||
| const rateLimiter = new RateLimiter(fn, initialOptions) | ||
| return rateLimiter.maybeExecute.bind(rateLimiter) | ||
| return rateLimiter.maybeExecute | ||
| } |
+152
-78
@@ -0,4 +1,45 @@ | ||
| import { Store } from '@tanstack/store' | ||
| import { parseFunctionOrValue } from './utils' | ||
| import type { AnyFunction } from './types' | ||
| export interface ThrottlerState<TFn extends AnyFunction> { | ||
| /** | ||
| * Number of function executions that have been completed | ||
| */ | ||
| executionCount: number | ||
| /** | ||
| * The arguments from the most recent call to maybeExecute | ||
| */ | ||
| lastArgs: Parameters<TFn> | undefined | ||
| /** | ||
| * Timestamp of the last function execution in milliseconds | ||
| */ | ||
| lastExecutionTime: number | ||
| /** | ||
| * Timestamp when the next execution can occur in milliseconds | ||
| */ | ||
| nextExecutionTime: number | ||
| /** | ||
| * Whether the throttler is waiting for the timeout to trigger execution | ||
| */ | ||
| isPending: boolean | ||
| /** | ||
| * Current execution status - 'idle' when not active, 'pending' when waiting for timeout | ||
| */ | ||
| status: 'disabled' | 'idle' | 'pending' | ||
| } | ||
| function getDefaultThrottlerState< | ||
| TFn extends AnyFunction, | ||
| >(): ThrottlerState<TFn> { | ||
| return structuredClone({ | ||
| executionCount: 0, | ||
| isPending: false, | ||
| lastArgs: undefined, | ||
| lastExecutionTime: 0, | ||
| nextExecutionTime: 0, | ||
| status: 'idle', | ||
| }) | ||
| } | ||
| /** | ||
@@ -15,2 +56,6 @@ * Options for configuring a throttled function | ||
| /** | ||
| * Initial state for the throttler | ||
| */ | ||
| initialState?: Partial<ThrottlerState<TFn>> | ||
| /** | ||
| * Whether to execute on the leading edge of the timeout. | ||
@@ -37,6 +82,8 @@ * Defaults to true. | ||
| const defaultOptions: Required<ThrottlerOptions<any>> = { | ||
| const defaultOptions: Omit< | ||
| Required<ThrottlerOptions<any>>, | ||
| 'initialState' | 'onExecute' | ||
| > = { | ||
| enabled: true, | ||
| leading: true, | ||
| onExecute: () => {}, | ||
| trailing: true, | ||
@@ -59,2 +106,10 @@ wait: 0, | ||
| * | ||
| * State Management: | ||
| * - Uses TanStack Store for reactive state management | ||
| * - Use `initialState` to provide initial state values when creating the throttler | ||
| * - Use `onExecute` callback to react to function execution and implement custom logic | ||
| * - The state includes execution count, last execution time, pending status, and more | ||
| * - State can be accessed via `throttler.store.state` when using the class directly | ||
| * - When using framework adapters (React/Solid), state is accessed from `throttler.state` | ||
| * | ||
| * @example | ||
@@ -75,7 +130,7 @@ * ```ts | ||
| export class Throttler<TFn extends AnyFunction> { | ||
| private _executionCount = 0 | ||
| private _lastArgs: Parameters<TFn> | undefined | ||
| private _lastExecutionTime = 0 | ||
| private _options: Required<ThrottlerOptions<TFn>> | ||
| private _timeoutId: NodeJS.Timeout | undefined | ||
| readonly store: Store<Readonly<ThrottlerState<TFn>>> = new Store( | ||
| getDefaultThrottlerState(), | ||
| ) | ||
| options: ThrottlerOptions<TFn> | ||
| #timeoutId: NodeJS.Timeout | undefined | ||
@@ -86,6 +141,7 @@ constructor( | ||
| ) { | ||
| this._options = { | ||
| this.options = { | ||
| ...defaultOptions, | ||
| ...initialOptions, | ||
| } | ||
| this.#setState(this.options.initialState ?? {}) | ||
| } | ||
@@ -96,7 +152,7 @@ | ||
| */ | ||
| setOptions(newOptions: Partial<ThrottlerOptions<TFn>>): void { | ||
| this._options = { ...this._options, ...newOptions } | ||
| setOptions = (newOptions: Partial<ThrottlerOptions<TFn>>): void => { | ||
| this.options = { ...this.options, ...newOptions } | ||
| // End the pending state if the debouncer is disabled | ||
| if (!this._options.enabled) { | ||
| // Cancel pending execution if the throttler is disabled | ||
| if (!this.#getEnabled()) { | ||
| this.cancel() | ||
@@ -106,21 +162,26 @@ } | ||
| /** | ||
| * Returns the current throttler options | ||
| */ | ||
| getOptions(): Required<ThrottlerOptions<TFn>> { | ||
| return this._options | ||
| #setState = (newState: Partial<ThrottlerState<TFn>>): void => { | ||
| this.store.setState((state) => { | ||
| const combinedState = { | ||
| ...state, | ||
| ...newState, | ||
| } | ||
| const { isPending } = combinedState | ||
| return { | ||
| ...combinedState, | ||
| status: !this.#getEnabled() | ||
| ? 'disabled' | ||
| : isPending | ||
| ? 'pending' | ||
| : 'idle', | ||
| } | ||
| }) | ||
| } | ||
| /** | ||
| * Returns the current enabled state of the throttler | ||
| */ | ||
| getEnabled(): boolean { | ||
| return parseFunctionOrValue(this._options.enabled, this) | ||
| #getEnabled = (): boolean => { | ||
| return !!parseFunctionOrValue(this.options.enabled, this) | ||
| } | ||
| /** | ||
| * Returns the current wait time in milliseconds | ||
| */ | ||
| getWait(): number { | ||
| return parseFunctionOrValue(this._options.wait, this) | ||
| #getWait = (): number => { | ||
| return parseFunctionOrValue(this.options.wait, this) | ||
| } | ||
@@ -150,23 +211,27 @@ | ||
| */ | ||
| maybeExecute(...args: Parameters<TFn>): void { | ||
| maybeExecute = (...args: Parameters<TFn>): void => { | ||
| const now = Date.now() | ||
| const timeSinceLastExecution = now - this._lastExecutionTime | ||
| const wait = this.getWait() | ||
| const timeSinceLastExecution = now - this.store.state.lastExecutionTime | ||
| const wait = this.#getWait() | ||
| // Handle leading execution | ||
| if (this._options.leading && timeSinceLastExecution >= wait) { | ||
| this.execute(...args) | ||
| if (this.options.leading && timeSinceLastExecution >= wait) { | ||
| this.#execute(...args) | ||
| } else { | ||
| // Store the most recent arguments for potential trailing execution | ||
| this._lastArgs = args | ||
| this.#setState({ | ||
| lastArgs: args, | ||
| }) | ||
| // Set up trailing execution if not already scheduled | ||
| if (!this._timeoutId && this._options.trailing) { | ||
| const _timeSinceLastExecution = this._lastExecutionTime | ||
| ? now - this._lastExecutionTime | ||
| if (!this.#timeoutId && this.options.trailing) { | ||
| // prevent large number if lastExecutionTime is undefined | ||
| const _timeSinceLastExecution = this.store.state.lastExecutionTime | ||
| ? now - this.store.state.lastExecutionTime | ||
| : 0 | ||
| const timeoutDuration = wait - _timeSinceLastExecution | ||
| this._timeoutId = setTimeout(() => { | ||
| if (this._lastArgs !== undefined) { | ||
| this.execute(...this._lastArgs) | ||
| this.#setState({ isPending: true }) | ||
| this.#timeoutId = setTimeout(() => { | ||
| const { lastArgs } = this.store.state | ||
| if (lastArgs !== undefined) { | ||
| this.#execute(...lastArgs) | ||
| } | ||
@@ -178,13 +243,35 @@ }, timeoutDuration) | ||
| private execute(...args: Parameters<TFn>): void { | ||
| if (!this.getEnabled()) return | ||
| #execute = (...args: Parameters<TFn>): void => { | ||
| if (!this.#getEnabled()) return | ||
| this.fn(...args) // EXECUTE! | ||
| this._executionCount++ | ||
| this._lastExecutionTime = Date.now() | ||
| this._timeoutId = undefined | ||
| this._lastArgs = undefined | ||
| this._options.onExecute(this) | ||
| const lastExecutionTime = Date.now() | ||
| const nextExecutionTime = lastExecutionTime + this.#getWait() | ||
| this.#clearTimeout() | ||
| this.#setState({ | ||
| executionCount: this.store.state.executionCount + 1, | ||
| lastExecutionTime, | ||
| nextExecutionTime, | ||
| isPending: false, | ||
| lastArgs: undefined, | ||
| }) | ||
| this.options.onExecute?.(this) | ||
| } | ||
| /** | ||
| * Processes the current pending execution immediately | ||
| */ | ||
| flush = (): void => { | ||
| if (this.store.state.isPending && this.store.state.lastArgs) { | ||
| this.#execute(...this.store.state.lastArgs) | ||
| } | ||
| } | ||
| #clearTimeout = (): void => { | ||
| if (this.#timeoutId) { | ||
| clearTimeout(this.#timeoutId) | ||
| this.#timeoutId = undefined | ||
| } | ||
| } | ||
| /** | ||
| * Cancels any pending trailing execution and clears internal state. | ||
@@ -198,37 +285,16 @@ * | ||
| */ | ||
| cancel(): void { | ||
| if (this._timeoutId) { | ||
| clearTimeout(this._timeoutId) | ||
| this._timeoutId = undefined | ||
| this._lastArgs = undefined | ||
| } | ||
| cancel = (): void => { | ||
| this.#clearTimeout() | ||
| this.#setState({ | ||
| lastArgs: undefined, | ||
| isPending: false, | ||
| }) | ||
| } | ||
| /** | ||
| * Returns the last execution time | ||
| * Resets the throttler state to its default values | ||
| */ | ||
| getLastExecutionTime(): number { | ||
| return this._lastExecutionTime | ||
| reset = (): void => { | ||
| this.#setState(getDefaultThrottlerState<TFn>()) | ||
| } | ||
| /** | ||
| * Returns the next execution time | ||
| */ | ||
| getNextExecutionTime(): number { | ||
| return this._lastExecutionTime + this.getWait() | ||
| } | ||
| /** | ||
| * Returns the number of times the function has been executed | ||
| */ | ||
| getExecutionCount(): number { | ||
| return this._executionCount | ||
| } | ||
| /** | ||
| * Returns `true` if there is a pending execution | ||
| */ | ||
| getIsPending(): boolean { | ||
| return this.getEnabled() && !!this._timeoutId | ||
| } | ||
| } | ||
@@ -249,2 +315,10 @@ | ||
| * | ||
| * State Management: | ||
| * - Uses TanStack Store for reactive state management | ||
| * - Use `initialState` to provide initial state values when creating the throttler | ||
| * - Use `onExecute` callback to react to function execution and implement custom logic | ||
| * - The state includes execution count, last execution time, pending status, and more | ||
| * - State can be accessed via the underlying Throttler instance's `store.state` property | ||
| * - When using framework adapters (React/Solid), state is accessed from the hook's state property | ||
| * | ||
| * @example | ||
@@ -268,3 +342,3 @@ * ```ts | ||
| const throttler = new Throttler(fn, initialOptions) | ||
| return throttler.maybeExecute.bind(throttler) | ||
| return throttler.maybeExecute | ||
| } |
+0
-15
@@ -13,16 +13,1 @@ import type { AnyFunction } from './types' | ||
| } | ||
| export function bindInstanceMethods<T extends Record<string, any>>( | ||
| instance: T, | ||
| ): T { | ||
| return Object.getOwnPropertyNames(Object.getPrototypeOf(instance)).reduce( | ||
| (acc: any, key) => { | ||
| const method = instance[key as keyof T] | ||
| if (isFunction(method)) { | ||
| acc[key] = method.bind(instance) | ||
| } | ||
| return acc | ||
| }, | ||
| instance, | ||
| ) | ||
| } |
| "use strict"; | ||
| Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" }); | ||
| function replaceEqualDeep(a, b) { | ||
| if (a === b) { | ||
| return a; | ||
| } | ||
| const array = isPlainArray(a) && isPlainArray(b); | ||
| if (array || isPlainObject(a) && isPlainObject(b)) { | ||
| const aItems = array ? a : Object.keys(a); | ||
| const aSize = aItems.length; | ||
| const bItems = array ? b : Object.keys(b); | ||
| const bSize = bItems.length; | ||
| const copy = array ? [] : {}; | ||
| let equalItems = 0; | ||
| for (let i = 0; i < bSize; i++) { | ||
| const key = array ? i : bItems[i]; | ||
| if ((!array && aItems.includes(key) || array) && a[key] === void 0 && b[key] === void 0) { | ||
| copy[key] = void 0; | ||
| equalItems++; | ||
| } else { | ||
| copy[key] = replaceEqualDeep(a[key], b[key]); | ||
| if (copy[key] === a[key] && a[key] !== void 0) { | ||
| equalItems++; | ||
| } | ||
| } | ||
| } | ||
| return aSize === bSize && equalItems === aSize ? a : copy; | ||
| } | ||
| return b; | ||
| } | ||
| function shallowEqualObjects(a, b) { | ||
| if (!b || Object.keys(a).length !== Object.keys(b).length) { | ||
| return false; | ||
| } | ||
| for (const key in a) { | ||
| if (a[key] !== b[key]) { | ||
| return false; | ||
| } | ||
| } | ||
| return true; | ||
| } | ||
| function isPlainArray(value) { | ||
| return Array.isArray(value) && value.length === Object.keys(value).length; | ||
| } | ||
| function isPlainObject(o) { | ||
| if (!hasObjectPrototype(o)) { | ||
| return false; | ||
| } | ||
| const ctor = o.constructor; | ||
| if (ctor === void 0) { | ||
| return true; | ||
| } | ||
| const prot = ctor.prototype; | ||
| if (!hasObjectPrototype(prot)) { | ||
| return false; | ||
| } | ||
| if (!prot.hasOwnProperty("isPrototypeOf")) { | ||
| return false; | ||
| } | ||
| if (Object.getPrototypeOf(o) !== Object.prototype) { | ||
| return false; | ||
| } | ||
| return true; | ||
| } | ||
| function hasObjectPrototype(o) { | ||
| return Object.prototype.toString.call(o) === "[object Object]"; | ||
| } | ||
| exports.isPlainArray = isPlainArray; | ||
| exports.isPlainObject = isPlainObject; | ||
| exports.replaceEqualDeep = replaceEqualDeep; | ||
| exports.shallowEqualObjects = shallowEqualObjects; | ||
| //# sourceMappingURL=compare.cjs.map |
| {"version":3,"file":"compare.cjs","sources":["../../src/compare.ts"],"sourcesContent":["/**\n * This function returns `a` if `b` is deeply equal.\n * If not, it will replace any deeply equal children of `b` with those of `a`.\n * This can be used for structural sharing between JSON values for example.\n */\nexport function replaceEqualDeep<T>(a: unknown, b: T): T\nexport function replaceEqualDeep(a: any, b: any): any {\n if (a === b) {\n return a\n }\n\n const array = isPlainArray(a) && isPlainArray(b)\n\n if (array || (isPlainObject(a) && isPlainObject(b))) {\n const aItems = array ? a : Object.keys(a)\n const aSize = aItems.length\n const bItems = array ? b : Object.keys(b)\n const bSize = bItems.length\n const copy: any = array ? [] : {}\n\n let equalItems = 0\n\n for (let i = 0; i < bSize; i++) {\n const key = array ? i : bItems[i]\n if (\n ((!array && aItems.includes(key)) || array) &&\n a[key] === undefined &&\n b[key] === undefined\n ) {\n copy[key] = undefined\n equalItems++\n } else {\n copy[key] = replaceEqualDeep(a[key], b[key])\n if (copy[key] === a[key] && a[key] !== undefined) {\n equalItems++\n }\n }\n }\n\n return aSize === bSize && equalItems === aSize ? a : copy\n }\n\n return b\n}\n\n/**\n * Shallow compare objects.\n */\nexport function shallowEqualObjects<T extends Record<string, any>>(\n a: T,\n b: T | undefined,\n): boolean {\n if (!b || Object.keys(a).length !== Object.keys(b).length) {\n return false\n }\n\n for (const key in a) {\n if (a[key] !== b[key]) {\n return false\n }\n }\n\n return true\n}\n\nexport function isPlainArray(value: unknown) {\n return Array.isArray(value) && value.length === Object.keys(value).length\n}\n\n// Copied from: https://github.com/jonschlinkert/is-plain-object\n// eslint-disable-next-line @typescript-eslint/no-wrapper-object-types\nexport function isPlainObject(o: any): o is Object {\n if (!hasObjectPrototype(o)) {\n return false\n }\n\n // If has no constructor\n const ctor = o.constructor\n if (ctor === undefined) {\n return true\n }\n\n // If has modified prototype\n const prot = ctor.prototype\n if (!hasObjectPrototype(prot)) {\n return false\n }\n\n // If constructor does not have an Object-specific method\n if (!prot.hasOwnProperty('isPrototypeOf')) {\n return false\n }\n\n // Handles Objects created by Object.create(<arbitrary prototype>)\n if (Object.getPrototypeOf(o) !== Object.prototype) {\n return false\n }\n\n // Most likely a plain Object\n return true\n}\n\nfunction hasObjectPrototype(o: any): boolean {\n return Object.prototype.toString.call(o) === '[object Object]'\n}\n"],"names":[],"mappings":";;AAMgB,SAAA,iBAAiB,GAAQ,GAAa;AACpD,MAAI,MAAM,GAAG;AACJ,WAAA;AAAA,EAAA;AAGT,QAAM,QAAQ,aAAa,CAAC,KAAK,aAAa,CAAC;AAE/C,MAAI,SAAU,cAAc,CAAC,KAAK,cAAc,CAAC,GAAI;AACnD,UAAM,SAAS,QAAQ,IAAI,OAAO,KAAK,CAAC;AACxC,UAAM,QAAQ,OAAO;AACrB,UAAM,SAAS,QAAQ,IAAI,OAAO,KAAK,CAAC;AACxC,UAAM,QAAQ,OAAO;AACrB,UAAM,OAAY,QAAQ,CAAA,IAAK,CAAC;AAEhC,QAAI,aAAa;AAEjB,aAAS,IAAI,GAAG,IAAI,OAAO,KAAK;AAC9B,YAAM,MAAM,QAAQ,IAAI,OAAO,CAAC;AAChC,WACI,CAAC,SAAS,OAAO,SAAS,GAAG,KAAM,UACrC,EAAE,GAAG,MAAM,UACX,EAAE,GAAG,MAAM,QACX;AACA,aAAK,GAAG,IAAI;AACZ;AAAA,MAAA,OACK;AACA,aAAA,GAAG,IAAI,iBAAiB,EAAE,GAAG,GAAG,EAAE,GAAG,CAAC;AACvC,YAAA,KAAK,GAAG,MAAM,EAAE,GAAG,KAAK,EAAE,GAAG,MAAM,QAAW;AAChD;AAAA,QAAA;AAAA,MACF;AAAA,IACF;AAGF,WAAO,UAAU,SAAS,eAAe,QAAQ,IAAI;AAAA,EAAA;AAGhD,SAAA;AACT;AAKgB,SAAA,oBACd,GACA,GACS;AACL,MAAA,CAAC,KAAK,OAAO,KAAK,CAAC,EAAE,WAAW,OAAO,KAAK,CAAC,EAAE,QAAQ;AAClD,WAAA;AAAA,EAAA;AAGT,aAAW,OAAO,GAAG;AACnB,QAAI,EAAE,GAAG,MAAM,EAAE,GAAG,GAAG;AACd,aAAA;AAAA,IAAA;AAAA,EACT;AAGK,SAAA;AACT;AAEO,SAAS,aAAa,OAAgB;AACpC,SAAA,MAAM,QAAQ,KAAK,KAAK,MAAM,WAAW,OAAO,KAAK,KAAK,EAAE;AACrE;AAIO,SAAS,cAAc,GAAqB;AAC7C,MAAA,CAAC,mBAAmB,CAAC,GAAG;AACnB,WAAA;AAAA,EAAA;AAIT,QAAM,OAAO,EAAE;AACf,MAAI,SAAS,QAAW;AACf,WAAA;AAAA,EAAA;AAIT,QAAM,OAAO,KAAK;AACd,MAAA,CAAC,mBAAmB,IAAI,GAAG;AACtB,WAAA;AAAA,EAAA;AAIT,MAAI,CAAC,KAAK,eAAe,eAAe,GAAG;AAClC,WAAA;AAAA,EAAA;AAIT,MAAI,OAAO,eAAe,CAAC,MAAM,OAAO,WAAW;AAC1C,WAAA;AAAA,EAAA;AAIF,SAAA;AACT;AAEA,SAAS,mBAAmB,GAAiB;AAC3C,SAAO,OAAO,UAAU,SAAS,KAAK,CAAC,MAAM;AAC/C;;;;;"} |
| /** | ||
| * This function returns `a` if `b` is deeply equal. | ||
| * If not, it will replace any deeply equal children of `b` with those of `a`. | ||
| * This can be used for structural sharing between JSON values for example. | ||
| */ | ||
| export declare function replaceEqualDeep<T>(a: unknown, b: T): T; | ||
| /** | ||
| * Shallow compare objects. | ||
| */ | ||
| export declare function shallowEqualObjects<T extends Record<string, any>>(a: T, b: T | undefined): boolean; | ||
| export declare function isPlainArray(value: unknown): boolean; | ||
| export declare function isPlainObject(o: any): o is Object; |
| /** | ||
| * This function returns `a` if `b` is deeply equal. | ||
| * If not, it will replace any deeply equal children of `b` with those of `a`. | ||
| * This can be used for structural sharing between JSON values for example. | ||
| */ | ||
| export declare function replaceEqualDeep<T>(a: unknown, b: T): T; | ||
| /** | ||
| * Shallow compare objects. | ||
| */ | ||
| export declare function shallowEqualObjects<T extends Record<string, any>>(a: T, b: T | undefined): boolean; | ||
| export declare function isPlainArray(value: unknown): boolean; | ||
| export declare function isPlainObject(o: any): o is Object; |
| function replaceEqualDeep(a, b) { | ||
| if (a === b) { | ||
| return a; | ||
| } | ||
| const array = isPlainArray(a) && isPlainArray(b); | ||
| if (array || isPlainObject(a) && isPlainObject(b)) { | ||
| const aItems = array ? a : Object.keys(a); | ||
| const aSize = aItems.length; | ||
| const bItems = array ? b : Object.keys(b); | ||
| const bSize = bItems.length; | ||
| const copy = array ? [] : {}; | ||
| let equalItems = 0; | ||
| for (let i = 0; i < bSize; i++) { | ||
| const key = array ? i : bItems[i]; | ||
| if ((!array && aItems.includes(key) || array) && a[key] === void 0 && b[key] === void 0) { | ||
| copy[key] = void 0; | ||
| equalItems++; | ||
| } else { | ||
| copy[key] = replaceEqualDeep(a[key], b[key]); | ||
| if (copy[key] === a[key] && a[key] !== void 0) { | ||
| equalItems++; | ||
| } | ||
| } | ||
| } | ||
| return aSize === bSize && equalItems === aSize ? a : copy; | ||
| } | ||
| return b; | ||
| } | ||
| function shallowEqualObjects(a, b) { | ||
| if (!b || Object.keys(a).length !== Object.keys(b).length) { | ||
| return false; | ||
| } | ||
| for (const key in a) { | ||
| if (a[key] !== b[key]) { | ||
| return false; | ||
| } | ||
| } | ||
| return true; | ||
| } | ||
| function isPlainArray(value) { | ||
| return Array.isArray(value) && value.length === Object.keys(value).length; | ||
| } | ||
| function isPlainObject(o) { | ||
| if (!hasObjectPrototype(o)) { | ||
| return false; | ||
| } | ||
| const ctor = o.constructor; | ||
| if (ctor === void 0) { | ||
| return true; | ||
| } | ||
| const prot = ctor.prototype; | ||
| if (!hasObjectPrototype(prot)) { | ||
| return false; | ||
| } | ||
| if (!prot.hasOwnProperty("isPrototypeOf")) { | ||
| return false; | ||
| } | ||
| if (Object.getPrototypeOf(o) !== Object.prototype) { | ||
| return false; | ||
| } | ||
| return true; | ||
| } | ||
| function hasObjectPrototype(o) { | ||
| return Object.prototype.toString.call(o) === "[object Object]"; | ||
| } | ||
| export { | ||
| isPlainArray, | ||
| isPlainObject, | ||
| replaceEqualDeep, | ||
| shallowEqualObjects | ||
| }; | ||
| //# sourceMappingURL=compare.js.map |
| {"version":3,"file":"compare.js","sources":["../../src/compare.ts"],"sourcesContent":["/**\n * This function returns `a` if `b` is deeply equal.\n * If not, it will replace any deeply equal children of `b` with those of `a`.\n * This can be used for structural sharing between JSON values for example.\n */\nexport function replaceEqualDeep<T>(a: unknown, b: T): T\nexport function replaceEqualDeep(a: any, b: any): any {\n if (a === b) {\n return a\n }\n\n const array = isPlainArray(a) && isPlainArray(b)\n\n if (array || (isPlainObject(a) && isPlainObject(b))) {\n const aItems = array ? a : Object.keys(a)\n const aSize = aItems.length\n const bItems = array ? b : Object.keys(b)\n const bSize = bItems.length\n const copy: any = array ? [] : {}\n\n let equalItems = 0\n\n for (let i = 0; i < bSize; i++) {\n const key = array ? i : bItems[i]\n if (\n ((!array && aItems.includes(key)) || array) &&\n a[key] === undefined &&\n b[key] === undefined\n ) {\n copy[key] = undefined\n equalItems++\n } else {\n copy[key] = replaceEqualDeep(a[key], b[key])\n if (copy[key] === a[key] && a[key] !== undefined) {\n equalItems++\n }\n }\n }\n\n return aSize === bSize && equalItems === aSize ? a : copy\n }\n\n return b\n}\n\n/**\n * Shallow compare objects.\n */\nexport function shallowEqualObjects<T extends Record<string, any>>(\n a: T,\n b: T | undefined,\n): boolean {\n if (!b || Object.keys(a).length !== Object.keys(b).length) {\n return false\n }\n\n for (const key in a) {\n if (a[key] !== b[key]) {\n return false\n }\n }\n\n return true\n}\n\nexport function isPlainArray(value: unknown) {\n return Array.isArray(value) && value.length === Object.keys(value).length\n}\n\n// Copied from: https://github.com/jonschlinkert/is-plain-object\n// eslint-disable-next-line @typescript-eslint/no-wrapper-object-types\nexport function isPlainObject(o: any): o is Object {\n if (!hasObjectPrototype(o)) {\n return false\n }\n\n // If has no constructor\n const ctor = o.constructor\n if (ctor === undefined) {\n return true\n }\n\n // If has modified prototype\n const prot = ctor.prototype\n if (!hasObjectPrototype(prot)) {\n return false\n }\n\n // If constructor does not have an Object-specific method\n if (!prot.hasOwnProperty('isPrototypeOf')) {\n return false\n }\n\n // Handles Objects created by Object.create(<arbitrary prototype>)\n if (Object.getPrototypeOf(o) !== Object.prototype) {\n return false\n }\n\n // Most likely a plain Object\n return true\n}\n\nfunction hasObjectPrototype(o: any): boolean {\n return Object.prototype.toString.call(o) === '[object Object]'\n}\n"],"names":[],"mappings":"AAMgB,SAAA,iBAAiB,GAAQ,GAAa;AACpD,MAAI,MAAM,GAAG;AACJ,WAAA;AAAA,EAAA;AAGT,QAAM,QAAQ,aAAa,CAAC,KAAK,aAAa,CAAC;AAE/C,MAAI,SAAU,cAAc,CAAC,KAAK,cAAc,CAAC,GAAI;AACnD,UAAM,SAAS,QAAQ,IAAI,OAAO,KAAK,CAAC;AACxC,UAAM,QAAQ,OAAO;AACrB,UAAM,SAAS,QAAQ,IAAI,OAAO,KAAK,CAAC;AACxC,UAAM,QAAQ,OAAO;AACrB,UAAM,OAAY,QAAQ,CAAA,IAAK,CAAC;AAEhC,QAAI,aAAa;AAEjB,aAAS,IAAI,GAAG,IAAI,OAAO,KAAK;AAC9B,YAAM,MAAM,QAAQ,IAAI,OAAO,CAAC;AAChC,WACI,CAAC,SAAS,OAAO,SAAS,GAAG,KAAM,UACrC,EAAE,GAAG,MAAM,UACX,EAAE,GAAG,MAAM,QACX;AACA,aAAK,GAAG,IAAI;AACZ;AAAA,MAAA,OACK;AACA,aAAA,GAAG,IAAI,iBAAiB,EAAE,GAAG,GAAG,EAAE,GAAG,CAAC;AACvC,YAAA,KAAK,GAAG,MAAM,EAAE,GAAG,KAAK,EAAE,GAAG,MAAM,QAAW;AAChD;AAAA,QAAA;AAAA,MACF;AAAA,IACF;AAGF,WAAO,UAAU,SAAS,eAAe,QAAQ,IAAI;AAAA,EAAA;AAGhD,SAAA;AACT;AAKgB,SAAA,oBACd,GACA,GACS;AACL,MAAA,CAAC,KAAK,OAAO,KAAK,CAAC,EAAE,WAAW,OAAO,KAAK,CAAC,EAAE,QAAQ;AAClD,WAAA;AAAA,EAAA;AAGT,aAAW,OAAO,GAAG;AACnB,QAAI,EAAE,GAAG,MAAM,EAAE,GAAG,GAAG;AACd,aAAA;AAAA,IAAA;AAAA,EACT;AAGK,SAAA;AACT;AAEO,SAAS,aAAa,OAAgB;AACpC,SAAA,MAAM,QAAQ,KAAK,KAAK,MAAM,WAAW,OAAO,KAAK,KAAK,EAAE;AACrE;AAIO,SAAS,cAAc,GAAqB;AAC7C,MAAA,CAAC,mBAAmB,CAAC,GAAG;AACnB,WAAA;AAAA,EAAA;AAIT,QAAM,OAAO,EAAE;AACf,MAAI,SAAS,QAAW;AACf,WAAA;AAAA,EAAA;AAIT,QAAM,OAAO,KAAK;AACd,MAAA,CAAC,mBAAmB,IAAI,GAAG;AACtB,WAAA;AAAA,EAAA;AAIT,MAAI,CAAC,KAAK,eAAe,eAAe,GAAG;AAClC,WAAA;AAAA,EAAA;AAIT,MAAI,OAAO,eAAe,CAAC,MAAM,OAAO,WAAW;AAC1C,WAAA;AAAA,EAAA;AAIF,SAAA;AACT;AAEA,SAAS,mBAAmB,GAAiB;AAC3C,SAAO,OAAO,UAAU,SAAS,KAAK,CAAC,MAAM;AAC/C;"} |
-105
| /** | ||
| * This function returns `a` if `b` is deeply equal. | ||
| * If not, it will replace any deeply equal children of `b` with those of `a`. | ||
| * This can be used for structural sharing between JSON values for example. | ||
| */ | ||
| export function replaceEqualDeep<T>(a: unknown, b: T): T | ||
| export function replaceEqualDeep(a: any, b: any): any { | ||
| if (a === b) { | ||
| return a | ||
| } | ||
| const array = isPlainArray(a) && isPlainArray(b) | ||
| if (array || (isPlainObject(a) && isPlainObject(b))) { | ||
| const aItems = array ? a : Object.keys(a) | ||
| const aSize = aItems.length | ||
| const bItems = array ? b : Object.keys(b) | ||
| const bSize = bItems.length | ||
| const copy: any = array ? [] : {} | ||
| let equalItems = 0 | ||
| for (let i = 0; i < bSize; i++) { | ||
| const key = array ? i : bItems[i] | ||
| if ( | ||
| ((!array && aItems.includes(key)) || array) && | ||
| a[key] === undefined && | ||
| b[key] === undefined | ||
| ) { | ||
| copy[key] = undefined | ||
| equalItems++ | ||
| } else { | ||
| copy[key] = replaceEqualDeep(a[key], b[key]) | ||
| if (copy[key] === a[key] && a[key] !== undefined) { | ||
| equalItems++ | ||
| } | ||
| } | ||
| } | ||
| return aSize === bSize && equalItems === aSize ? a : copy | ||
| } | ||
| return b | ||
| } | ||
| /** | ||
| * Shallow compare objects. | ||
| */ | ||
| export function shallowEqualObjects<T extends Record<string, any>>( | ||
| a: T, | ||
| b: T | undefined, | ||
| ): boolean { | ||
| if (!b || Object.keys(a).length !== Object.keys(b).length) { | ||
| return false | ||
| } | ||
| for (const key in a) { | ||
| if (a[key] !== b[key]) { | ||
| return false | ||
| } | ||
| } | ||
| return true | ||
| } | ||
| export function isPlainArray(value: unknown) { | ||
| return Array.isArray(value) && value.length === Object.keys(value).length | ||
| } | ||
| // Copied from: https://github.com/jonschlinkert/is-plain-object | ||
| // eslint-disable-next-line @typescript-eslint/no-wrapper-object-types | ||
| export function isPlainObject(o: any): o is Object { | ||
| if (!hasObjectPrototype(o)) { | ||
| return false | ||
| } | ||
| // If has no constructor | ||
| const ctor = o.constructor | ||
| if (ctor === undefined) { | ||
| return true | ||
| } | ||
| // If has modified prototype | ||
| const prot = ctor.prototype | ||
| if (!hasObjectPrototype(prot)) { | ||
| return false | ||
| } | ||
| // If constructor does not have an Object-specific method | ||
| if (!prot.hasOwnProperty('isPrototypeOf')) { | ||
| return false | ||
| } | ||
| // Handles Objects created by Object.create(<arbitrary prototype>) | ||
| if (Object.getPrototypeOf(o) !== Object.prototype) { | ||
| return false | ||
| } | ||
| // Most likely a plain Object | ||
| return true | ||
| } | ||
| function hasObjectPrototype(o: any): boolean { | ||
| return Object.prototype.toString.call(o) === '[object Object]' | ||
| } |
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
832906
29.12%10241
13.24%1
Infinity%1
Infinity%+ Added
+ Added