@tanstack/pacer 0.9.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/async-batcher.cjs +1 -12
- package/dist/cjs/async-batcher.cjs.map +1 -1
- package/dist/cjs/async-batcher.d.cts +4 -16
- package/dist/cjs/async-debouncer.cjs +25 -8
- package/dist/cjs/async-debouncer.cjs.map +1 -1
- package/dist/cjs/async-debouncer.d.cts +1 -1
- package/dist/cjs/async-queuer.cjs +20 -9
- package/dist/cjs/async-queuer.cjs.map +1 -1
- package/dist/cjs/async-queuer.d.cts +10 -5
- package/dist/cjs/async-rate-limiter.cjs +53 -12
- package/dist/cjs/async-rate-limiter.cjs.map +1 -1
- package/dist/cjs/async-rate-limiter.d.cts +8 -0
- package/dist/cjs/async-throttler.cjs +17 -6
- package/dist/cjs/async-throttler.cjs.map +1 -1
- package/dist/cjs/async-throttler.d.cts +1 -1
- package/dist/cjs/batcher.cjs +1 -12
- package/dist/cjs/batcher.cjs.map +1 -1
- package/dist/cjs/batcher.d.cts +4 -16
- package/dist/cjs/queuer.cjs +16 -4
- package/dist/cjs/queuer.cjs.map +1 -1
- package/dist/cjs/queuer.d.cts +13 -8
- package/dist/cjs/rate-limiter.cjs +53 -12
- package/dist/cjs/rate-limiter.cjs.map +1 -1
- package/dist/cjs/rate-limiter.d.cts +8 -0
- package/dist/cjs/throttler.cjs.map +1 -1
- package/dist/cjs/throttler.d.cts +4 -4
- package/dist/esm/async-batcher.d.ts +4 -16
- package/dist/esm/async-batcher.js +1 -12
- package/dist/esm/async-batcher.js.map +1 -1
- package/dist/esm/async-debouncer.d.ts +1 -1
- package/dist/esm/async-debouncer.js +25 -8
- package/dist/esm/async-debouncer.js.map +1 -1
- package/dist/esm/async-queuer.d.ts +10 -5
- package/dist/esm/async-queuer.js +20 -9
- package/dist/esm/async-queuer.js.map +1 -1
- package/dist/esm/async-rate-limiter.d.ts +8 -0
- package/dist/esm/async-rate-limiter.js +53 -12
- package/dist/esm/async-rate-limiter.js.map +1 -1
- package/dist/esm/async-throttler.d.ts +1 -1
- package/dist/esm/async-throttler.js +17 -6
- package/dist/esm/async-throttler.js.map +1 -1
- package/dist/esm/batcher.d.ts +4 -16
- package/dist/esm/batcher.js +1 -12
- package/dist/esm/batcher.js.map +1 -1
- package/dist/esm/queuer.d.ts +13 -8
- package/dist/esm/queuer.js +16 -4
- package/dist/esm/queuer.js.map +1 -1
- package/dist/esm/rate-limiter.d.ts +8 -0
- package/dist/esm/rate-limiter.js +53 -12
- package/dist/esm/rate-limiter.js.map +1 -1
- package/dist/esm/throttler.d.ts +4 -4
- package/dist/esm/throttler.js.map +1 -1
- package/package.json +1 -1
- package/src/async-batcher.ts +5 -28
- package/src/async-debouncer.ts +27 -8
- package/src/async-queuer.ts +32 -14
- package/src/async-rate-limiter.ts +72 -12
- package/src/async-throttler.ts +19 -6
- package/src/batcher.ts +5 -28
- package/src/queuer.ts +29 -12
- package/src/rate-limiter.ts +71 -11
- package/src/throttler.ts +4 -4
|
@@ -7,7 +7,6 @@ function getDefaultAsyncBatcherState() {
|
|
|
7
7
|
isEmpty: true,
|
|
8
8
|
isExecuting: false,
|
|
9
9
|
isPending: false,
|
|
10
|
-
isRunning: true,
|
|
11
10
|
items: [],
|
|
12
11
|
lastResult: void 0,
|
|
13
12
|
settleCount: 0,
|
|
@@ -64,7 +63,7 @@ class AsyncBatcher {
|
|
|
64
63
|
const shouldProcess = this.store.state.items.length >= this.options.maxSize || this.options.getShouldExecute(this.store.state.items, this);
|
|
65
64
|
if (shouldProcess) {
|
|
66
65
|
this.#execute();
|
|
67
|
-
} else if (this.
|
|
66
|
+
} else if (this.options.wait !== Infinity) {
|
|
68
67
|
this.#clearTimeout();
|
|
69
68
|
this.#timeoutId = setTimeout(() => this.#execute(), this.#getWait());
|
|
70
69
|
}
|
|
@@ -110,16 +109,6 @@ class AsyncBatcher {
|
|
|
110
109
|
this.#clearTimeout();
|
|
111
110
|
return await this.#execute();
|
|
112
111
|
};
|
|
113
|
-
this.stop = () => {
|
|
114
|
-
this.#setState({ isRunning: false });
|
|
115
|
-
this.#clearTimeout();
|
|
116
|
-
};
|
|
117
|
-
this.start = () => {
|
|
118
|
-
this.#setState({ isRunning: true });
|
|
119
|
-
if (this.store.state.items.length > 0 && !this.#timeoutId) {
|
|
120
|
-
this.#timeoutId = setTimeout(() => this.#execute(), this.#getWait());
|
|
121
|
-
}
|
|
122
|
-
};
|
|
123
112
|
this.peekAllItems = () => {
|
|
124
113
|
return [...this.store.state.items];
|
|
125
114
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"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;"}
|
|
1
|
+
{"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 * Array of items currently queued for batch processing\n */\n items: Array<TValue>\n /**\n * The result from the most recent batch execution\n */\n lastResult: any\n /**\n * Number of batch executions that have completed (either successfully or with errors)\n */\n settleCount: number\n /**\n * Number of items currently in the batch queue\n */\n size: number\n /**\n * Current processing status - 'idle' when not processing, 'pending' when waiting for timeout, 'executing' when processing, 'populated' when items are present, but no wait is configured\n */\n status: 'idle' | 'pending' | 'executing' | 'populated'\n /**\n * Number of batch executions that have completed successfully\n */\n successCount: number\n /**\n * Total number of items that have failed processing across all batches\n */\n totalItemsFailed: number\n /**\n * Total number of items that have been processed across all batches\n */\n totalItemsProcessed: number\n}\n\nfunction getDefaultAsyncBatcherState<TValue>(): AsyncBatcherState<TValue> {\n return {\n errorCount: 0,\n failedItems: [],\n isEmpty: true,\n isExecuting: false,\n isPending: false,\n items: [],\n lastResult: undefined,\n settleCount: 0,\n size: 0,\n status: 'idle',\n successCount: 0,\n totalItemsProcessed: 0,\n totalItemsFailed: 0,\n }\n}\n\n/**\n * Options for configuring an AsyncBatcher instance\n */\nexport interface AsyncBatcherOptions<TValue> {\n /**\n * Custom function to determine if a batch should be processed\n * Return true to process the batch immediately\n */\n getShouldExecute?: (\n items: Array<TValue>,\n batcher: AsyncBatcher<TValue>,\n ) => boolean\n /**\n * Initial state for the async batcher\n */\n initialState?: Partial<AsyncBatcherState<TValue>>\n /**\n * 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.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 * 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":"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"}
|
|
@@ -162,7 +162,7 @@ export declare class AsyncDebouncer<TFn extends AnyAsyncFunction> {
|
|
|
162
162
|
/**
|
|
163
163
|
* Processes the current pending execution immediately
|
|
164
164
|
*/
|
|
165
|
-
flush: () =>
|
|
165
|
+
flush: () => Promise<ReturnType<TFn> | undefined>;
|
|
166
166
|
/**
|
|
167
167
|
* Cancels any pending execution or aborts any execution in progress
|
|
168
168
|
*/
|
|
@@ -26,6 +26,7 @@ class AsyncDebouncer {
|
|
|
26
26
|
this.#abortController = null;
|
|
27
27
|
this.#timeoutId = null;
|
|
28
28
|
this.#resolvePreviousPromise = null;
|
|
29
|
+
this.#rejectPreviousPromise = null;
|
|
29
30
|
this.setOptions = (newOptions) => {
|
|
30
31
|
this.options = { ...this.options, ...newOptions };
|
|
31
32
|
if (!this.#getEnabled()) {
|
|
@@ -63,8 +64,9 @@ class AsyncDebouncer {
|
|
|
63
64
|
if (this.options.trailing && this.#getEnabled()) {
|
|
64
65
|
this.#setState({ isPending: true });
|
|
65
66
|
}
|
|
66
|
-
return new Promise((resolve) => {
|
|
67
|
+
return new Promise((resolve, reject) => {
|
|
67
68
|
this.#resolvePreviousPromise = resolve;
|
|
69
|
+
this.#rejectPreviousPromise = reject;
|
|
68
70
|
this.#timeoutId = setTimeout(async () => {
|
|
69
71
|
if (this.options.trailing && this.store.state.lastArgs) {
|
|
70
72
|
await this.#execute(...this.store.state.lastArgs);
|
|
@@ -92,7 +94,7 @@ class AsyncDebouncer {
|
|
|
92
94
|
});
|
|
93
95
|
this.options.onError?.(error, this);
|
|
94
96
|
if (this.options.throwOnError) {
|
|
95
|
-
|
|
97
|
+
this.#rejectPreviousPromiseInternal(error);
|
|
96
98
|
}
|
|
97
99
|
} finally {
|
|
98
100
|
this.#setState({
|
|
@@ -105,11 +107,26 @@ class AsyncDebouncer {
|
|
|
105
107
|
}
|
|
106
108
|
return this.store.state.lastResult;
|
|
107
109
|
};
|
|
108
|
-
this.flush = () => {
|
|
110
|
+
this.flush = async () => {
|
|
109
111
|
if (this.store.state.isPending && this.store.state.lastArgs) {
|
|
110
112
|
this.#abortExecution();
|
|
111
113
|
this.#clearTimeout();
|
|
112
|
-
this.#execute(...this.store.state.lastArgs);
|
|
114
|
+
const result = await this.#execute(...this.store.state.lastArgs);
|
|
115
|
+
this.#resolvePreviousPromiseInternal();
|
|
116
|
+
return result;
|
|
117
|
+
}
|
|
118
|
+
return void 0;
|
|
119
|
+
};
|
|
120
|
+
this.#resolvePreviousPromiseInternal = () => {
|
|
121
|
+
if (this.#resolvePreviousPromise) {
|
|
122
|
+
this.#resolvePreviousPromise(this.store.state.lastResult);
|
|
123
|
+
this.#resolvePreviousPromise = null;
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
this.#rejectPreviousPromiseInternal = (error) => {
|
|
127
|
+
if (this.#rejectPreviousPromise) {
|
|
128
|
+
this.#rejectPreviousPromise(error);
|
|
129
|
+
this.#rejectPreviousPromise = null;
|
|
113
130
|
}
|
|
114
131
|
};
|
|
115
132
|
this.#clearTimeout = () => {
|
|
@@ -120,10 +137,7 @@ class AsyncDebouncer {
|
|
|
120
137
|
};
|
|
121
138
|
this.#cancelPendingExecution = () => {
|
|
122
139
|
this.#clearTimeout();
|
|
123
|
-
|
|
124
|
-
this.#resolvePreviousPromise(this.store.state.lastResult);
|
|
125
|
-
this.#resolvePreviousPromise = null;
|
|
126
|
-
}
|
|
140
|
+
this.#resolvePreviousPromiseInternal();
|
|
127
141
|
this.#setState({
|
|
128
142
|
isPending: false,
|
|
129
143
|
isExecuting: false,
|
|
@@ -154,10 +168,13 @@ class AsyncDebouncer {
|
|
|
154
168
|
#abortController;
|
|
155
169
|
#timeoutId;
|
|
156
170
|
#resolvePreviousPromise;
|
|
171
|
+
#rejectPreviousPromise;
|
|
157
172
|
#setState;
|
|
158
173
|
#getEnabled;
|
|
159
174
|
#getWait;
|
|
160
175
|
#execute;
|
|
176
|
+
#resolvePreviousPromiseInternal;
|
|
177
|
+
#rejectPreviousPromiseInternal;
|
|
161
178
|
#clearTimeout;
|
|
162
179
|
#cancelPendingExecution;
|
|
163
180
|
#abortExecution;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"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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"}
|
|
1
|
+
{"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 #rejectPreviousPromise: ((reason?: unknown) => void) | 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, reject) => {\n this.#resolvePreviousPromise = resolve\n this.#rejectPreviousPromise = reject\n this.#timeoutId = setTimeout(async () => {\n // Execute trailing if enabled\n if (this.options.trailing && this.store.state.lastArgs) {\n 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 this.#rejectPreviousPromiseInternal(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 = async (): Promise<ReturnType<TFn> | undefined> => {\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 const result = await this.#execute(...this.store.state.lastArgs)\n\n // Resolve any pending promise from maybeExecute\n this.#resolvePreviousPromiseInternal()\n\n return result\n }\n return undefined\n }\n\n #resolvePreviousPromiseInternal = (): void => {\n if (this.#resolvePreviousPromise) {\n this.#resolvePreviousPromise(this.store.state.lastResult)\n this.#resolvePreviousPromise = null\n }\n }\n\n #rejectPreviousPromiseInternal = (error: unknown): void => {\n if (this.#rejectPreviousPromise) {\n this.#rejectPreviousPromise(error)\n this.#rejectPreviousPromise = 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 this.#resolvePreviousPromiseInternal()\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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"}
|
|
@@ -29,14 +29,14 @@ export interface AsyncQueuerState<TValue> {
|
|
|
29
29
|
* Whether the queuer is active and will process items automatically
|
|
30
30
|
*/
|
|
31
31
|
isRunning: boolean;
|
|
32
|
-
/**
|
|
33
|
-
* Timestamps when items were added to the queue for expiration tracking
|
|
34
|
-
*/
|
|
35
|
-
itemTimestamps: Array<number>;
|
|
36
32
|
/**
|
|
37
33
|
* Array of items currently waiting to be processed
|
|
38
34
|
*/
|
|
39
35
|
items: Array<TValue>;
|
|
36
|
+
/**
|
|
37
|
+
* Timestamps when items were added to the queue for expiration tracking
|
|
38
|
+
*/
|
|
39
|
+
itemTimestamps: Array<number>;
|
|
40
40
|
/**
|
|
41
41
|
* The result from the most recent task execution
|
|
42
42
|
*/
|
|
@@ -254,7 +254,12 @@ export declare class AsyncQueuer<TValue> {
|
|
|
254
254
|
* Processes a specified number of items to execute immediately with no wait time
|
|
255
255
|
* If no numberOfItems is provided, all items will be processed
|
|
256
256
|
*/
|
|
257
|
-
flush: (numberOfItems?: number, position?: QueuePosition) => void
|
|
257
|
+
flush: (numberOfItems?: number, position?: QueuePosition) => Promise<void>;
|
|
258
|
+
/**
|
|
259
|
+
* Processes all items in the queue as a batch using the provided function
|
|
260
|
+
* The queue is cleared after processing
|
|
261
|
+
*/
|
|
262
|
+
flushAsBatch: (batchFunction: (items: Array<TValue>) => Promise<any>) => Promise<void>;
|
|
258
263
|
/**
|
|
259
264
|
* Returns the next item in the queue without removing it.
|
|
260
265
|
*
|
package/dist/esm/async-queuer.js
CHANGED
|
@@ -76,7 +76,7 @@ class AsyncQueuer {
|
|
|
76
76
|
this.#setState({ pendingTick: true });
|
|
77
77
|
this.#checkExpiredItems();
|
|
78
78
|
const activeItems = this.store.state.activeItems;
|
|
79
|
-
while (activeItems.length < this.#getConcurrency() &&
|
|
79
|
+
while (activeItems.length < this.#getConcurrency() && this.store.state.items.length > 0) {
|
|
80
80
|
const nextItem = this.peekNextItem();
|
|
81
81
|
if (!nextItem) {
|
|
82
82
|
break;
|
|
@@ -100,7 +100,7 @@ class AsyncQueuer {
|
|
|
100
100
|
this.#setState({ pendingTick: false });
|
|
101
101
|
};
|
|
102
102
|
this.addItem = (item, position = this.options.addItemsTo ?? "back", runOnItemsChange = true) => {
|
|
103
|
-
if (this.store.state.
|
|
103
|
+
if (this.store.state.items.length >= (this.options.maxSize ?? Infinity)) {
|
|
104
104
|
this.#setState({
|
|
105
105
|
rejectionCount: this.store.state.rejectionCount + 1
|
|
106
106
|
});
|
|
@@ -168,6 +168,11 @@ class AsyncQueuer {
|
|
|
168
168
|
}
|
|
169
169
|
return item;
|
|
170
170
|
};
|
|
171
|
+
this.#getAllItems = () => {
|
|
172
|
+
const items = this.peekAllItems();
|
|
173
|
+
this.clear();
|
|
174
|
+
return items;
|
|
175
|
+
};
|
|
171
176
|
this.execute = async (position) => {
|
|
172
177
|
const item = this.getNextItem(position);
|
|
173
178
|
if (item !== void 0) {
|
|
@@ -198,11 +203,16 @@ class AsyncQueuer {
|
|
|
198
203
|
}
|
|
199
204
|
return item;
|
|
200
205
|
};
|
|
201
|
-
this.flush = (numberOfItems = this.store.state.items.length, position) => {
|
|
206
|
+
this.flush = async (numberOfItems = this.store.state.items.length, position) => {
|
|
202
207
|
this.#clearTimeouts();
|
|
203
|
-
|
|
204
|
-
this.execute(position)
|
|
205
|
-
|
|
208
|
+
await Promise.all(
|
|
209
|
+
Array.from({ length: numberOfItems }, () => this.execute(position))
|
|
210
|
+
);
|
|
211
|
+
};
|
|
212
|
+
this.flushAsBatch = async (batchFunction) => {
|
|
213
|
+
this.#clearTimeouts();
|
|
214
|
+
const items = this.#getAllItems();
|
|
215
|
+
await batchFunction(items);
|
|
206
216
|
};
|
|
207
217
|
this.#checkExpiredItems = () => {
|
|
208
218
|
if ((this.options.expirationDuration ?? Infinity) === Infinity && this.options.getIsExpired === defaultOptions.getIsExpired) {
|
|
@@ -210,7 +220,7 @@ class AsyncQueuer {
|
|
|
210
220
|
}
|
|
211
221
|
const now = Date.now();
|
|
212
222
|
const expiredIndices = [];
|
|
213
|
-
for (let i = 0; i < this.store.state.
|
|
223
|
+
for (let i = 0; i < this.store.state.items.length; i++) {
|
|
214
224
|
const timestamp = this.store.state.itemTimestamps[i];
|
|
215
225
|
if (timestamp === void 0) continue;
|
|
216
226
|
const item = this.store.state.items[i];
|
|
@@ -244,7 +254,7 @@ class AsyncQueuer {
|
|
|
244
254
|
if (position === "front") {
|
|
245
255
|
return this.store.state.items[0];
|
|
246
256
|
}
|
|
247
|
-
return this.store.state.items[this.store.state.
|
|
257
|
+
return this.store.state.items[this.store.state.items.length - 1];
|
|
248
258
|
};
|
|
249
259
|
this.peekAllItems = () => {
|
|
250
260
|
return [...this.peekActiveItems(), ...this.peekPendingItems()];
|
|
@@ -257,7 +267,7 @@ class AsyncQueuer {
|
|
|
257
267
|
};
|
|
258
268
|
this.start = () => {
|
|
259
269
|
this.#setState({ isRunning: true });
|
|
260
|
-
if (!this.store.state.pendingTick &&
|
|
270
|
+
if (!this.store.state.pendingTick && this.store.state.items.length > 0) {
|
|
261
271
|
this.#tick();
|
|
262
272
|
}
|
|
263
273
|
};
|
|
@@ -304,6 +314,7 @@ class AsyncQueuer {
|
|
|
304
314
|
#getWait;
|
|
305
315
|
#getConcurrency;
|
|
306
316
|
#tick;
|
|
317
|
+
#getAllItems;
|
|
307
318
|
#checkExpiredItems;
|
|
308
319
|
#clearTimeouts;
|
|
309
320
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"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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"}
|
|
1
|
+
{"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 * Array of items currently waiting to be processed\n */\n items: Array<TValue>\n /**\n * Timestamps when items were added to the queue for expiration tracking\n */\n itemTimestamps: Array<number>\n /**\n * The result from the most recent task execution\n */\n lastResult: any\n /**\n * Whether the queuer has a pending timeout for processing the next item\n */\n pendingTick: boolean\n /**\n * Number of items that have been rejected from being added to the queue\n */\n rejectionCount: number\n /**\n * Number of task executions that have completed (either successfully or with errors)\n */\n settledCount: number\n /**\n * Number of items currently in the queue\n */\n size: number\n /**\n * Current processing status - 'idle' when not processing, 'running' when active, 'stopped' when paused\n */\n status: 'idle' | 'running' | 'stopped'\n /**\n * Number of task executions that have completed successfully\n */\n successCount: number\n}\n\nfunction getDefaultAsyncQueuerState<TValue>(): AsyncQueuerState<TValue> {\n return 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.items.length > 0\n ) {\n const nextItem = this.peekNextItem()\n if (!nextItem) {\n break\n }\n activeItems.push(nextItem)\n this.#setState({\n activeItems,\n })\n ;(async () => {\n 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.items.length >= (this.options.maxSize ?? Infinity)) {\n this.#setState({\n rejectionCount: this.store.state.rejectionCount + 1,\n })\n this.options.onReject?.(item, this)\n return false\n }\n\n // Get priority either from the function or from getPriority option\n const priority =\n this.options.getPriority !== defaultOptions.getPriority\n ? this.options.getPriority!(item)\n : (item as any).priority\n\n const items = this.store.state.items\n const itemTimestamps = this.store.state.itemTimestamps\n\n if (priority !== undefined) {\n // Insert based on priority - higher priority items go to front\n const insertIndex = items.findIndex((existing) => {\n const existingPriority =\n this.options.getPriority !== defaultOptions.getPriority\n ? this.options.getPriority!(existing)\n : (existing as any).priority\n return existingPriority < priority\n })\n\n if (insertIndex === -1) {\n items.push(item)\n itemTimestamps.push(Date.now())\n } else {\n items.splice(insertIndex, 0, item)\n itemTimestamps.splice(insertIndex, 0, Date.now())\n }\n } else {\n if (position === 'front') {\n // Default FIFO/LIFO behavior\n items.unshift(item)\n itemTimestamps.unshift(Date.now())\n } else {\n // LIFO\n items.push(item)\n itemTimestamps.push(Date.now())\n }\n }\n\n this.#setState({\n items,\n itemTimestamps,\n })\n\n if (runOnItemsChange) {\n this.options.onItemsChange?.(this)\n }\n\n if (this.store.state.isRunning && !this.store.state.pendingTick) {\n this.#tick()\n }\n\n return true\n }\n\n /**\n * Removes and returns the next item from the queue without executing the task function.\n * Use for manual queue management. Normally, use execute() to process items.\n *\n * @example\n * ```ts\n * // FIFO\n * queuer.getNextItem();\n * // LIFO\n * queuer.getNextItem('back');\n * ```\n */\n getNextItem = (\n position: QueuePosition = this.options.getItemsFrom ?? 'front',\n ): TValue | undefined => {\n const { items, itemTimestamps } = this.store.state\n let item: TValue | undefined\n\n 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 #getAllItems = (): Array<TValue> => {\n const items = this.peekAllItems()\n this.clear()\n return items\n }\n\n /**\n * Removes and returns the next item from the queue and executes the task function with it.\n *\n * @example\n * ```ts\n * queuer.execute();\n * // LIFO\n * queuer.execute('back');\n * ```\n */\n execute = async (position?: QueuePosition): Promise<any> => {\n const item = this.getNextItem(position)\n 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 = async (\n numberOfItems: number = this.store.state.items.length,\n position?: QueuePosition,\n ): Promise<void> => {\n this.#clearTimeouts() // clear any pending timeouts\n await Promise.all(\n Array.from({ length: numberOfItems }, () => this.execute(position)),\n )\n }\n\n /**\n * Processes all items in the queue as a batch using the provided function\n * The queue is cleared after processing\n */\n flushAsBatch = async (\n batchFunction: (items: Array<TValue>) => Promise<any>,\n ): Promise<void> => {\n this.#clearTimeouts() // clear any pending timeouts\n const items = this.#getAllItems()\n await batchFunction(items)\n }\n\n /**\n * Checks for expired items in the queue and removes them. Calls onExpire for each expired item.\n * Internal use only.\n */\n #checkExpiredItems = (): void => {\n if (\n (this.options.expirationDuration ?? Infinity) === Infinity &&\n this.options.getIsExpired === defaultOptions.getIsExpired\n ) {\n return\n }\n\n const now = Date.now()\n const expiredIndices: Array<number> = []\n\n // Find indices of expired items\n for (let i = 0; i < this.store.state.items.length; i++) {\n const timestamp = this.store.state.itemTimestamps[i]\n if (timestamp === undefined) continue\n\n const item = this.store.state.items[i]\n if (item === undefined) continue\n\n const isExpired =\n this.options.getIsExpired !== defaultOptions.getIsExpired\n ? this.options.getIsExpired!(item, timestamp)\n : now - timestamp > (this.options.expirationDuration ?? Infinity)\n\n if (isExpired) {\n expiredIndices.push(i)\n }\n }\n\n // Remove expired items from back to front to maintain indices\n for (let i = expiredIndices.length - 1; i >= 0; i--) {\n const index = expiredIndices[i]\n if (index === undefined) continue\n\n const expiredItem = this.store.state.items[index]\n if (expiredItem === undefined) continue\n\n const newItems = [...this.store.state.items]\n const newTimestamps = [...this.store.state.itemTimestamps]\n newItems.splice(index, 1)\n newTimestamps.splice(index, 1)\n this.#setState({\n items: newItems,\n itemTimestamps: newTimestamps,\n expirationCount: this.store.state.expirationCount + 1,\n })\n this.options.onExpire?.(expiredItem, this)\n }\n\n if (expiredIndices.length > 0) {\n this.options.onItemsChange?.(this)\n }\n }\n\n /**\n * Returns the next item in the queue without removing it.\n *\n * @example\n * ```ts\n * queuer.peekNextItem(); // front\n * queuer.peekNextItem('back'); // back\n * ```\n */\n peekNextItem = (position: QueuePosition = 'front'): TValue | undefined => {\n if (position === 'front') {\n return this.store.state.items[0]\n }\n return this.store.state.items[this.store.state.items.length - 1]\n }\n\n /**\n * Returns a copy of all items in the queue, including active and pending items.\n */\n peekAllItems = (): Array<TValue> => {\n return [...this.peekActiveItems(), ...this.peekPendingItems()]\n }\n\n /**\n * Returns the items currently being processed (active tasks).\n */\n peekActiveItems = (): Array<TValue> => {\n return [...this.store.state.activeItems]\n }\n\n /**\n * Returns the items waiting to be processed (pending tasks).\n */\n peekPendingItems = (): Array<TValue> => {\n return [...this.store.state.items]\n }\n\n /**\n * Starts processing items in the queue. If already running, does nothing.\n */\n start = (): void => {\n this.#setState({ isRunning: true })\n if (!this.store.state.pendingTick && this.store.state.items.length > 0) {\n this.#tick()\n }\n }\n\n /**\n * Stops processing items in the queue. Does not clear the queue.\n */\n stop = (): void => {\n this.#clearTimeouts()\n this.#setState({ isRunning: false, pendingTick: false })\n }\n\n #clearTimeouts = (): void => {\n this.#timeoutIds.forEach((timeoutId) => clearTimeout(timeoutId))\n this.#timeoutIds.clear()\n }\n\n /**\n * Removes all pending items from the queue. 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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"}
|
|
@@ -9,6 +9,10 @@ export interface AsyncRateLimiterState<TFn extends AnyAsyncFunction> {
|
|
|
9
9
|
* Array of timestamps when executions occurred for rate limiting calculations
|
|
10
10
|
*/
|
|
11
11
|
executionTimes: Array<number>;
|
|
12
|
+
/**
|
|
13
|
+
* Whether the rate limiter has exceeded the limit
|
|
14
|
+
*/
|
|
15
|
+
isExceeded: boolean;
|
|
12
16
|
/**
|
|
13
17
|
* Whether the rate-limited function is currently executing asynchronously
|
|
14
18
|
*/
|
|
@@ -25,6 +29,10 @@ export interface AsyncRateLimiterState<TFn extends AnyAsyncFunction> {
|
|
|
25
29
|
* Number of function executions that have completed (either successfully or with errors)
|
|
26
30
|
*/
|
|
27
31
|
settleCount: number;
|
|
32
|
+
/**
|
|
33
|
+
* Current execution status - 'disabled' when not active, 'executing' when executing, 'idle' when not executing, 'exceeded' when rate limit is exceeded
|
|
34
|
+
*/
|
|
35
|
+
status: 'disabled' | 'executing' | 'exceeded' | 'idle';
|
|
28
36
|
/**
|
|
29
37
|
* Number of function executions that have completed successfully
|
|
30
38
|
*/
|