@tanstack/pacer 0.21.1 → 0.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -2
- package/dist/async-batcher.cjs +1 -1
- package/dist/async-batcher.cjs.map +1 -1
- package/dist/async-batcher.d.cts +0 -1
- package/dist/async-batcher.d.ts +0 -1
- package/dist/async-batcher.js +1 -1
- package/dist/async-batcher.js.map +1 -1
- package/dist/async-debouncer.cjs +2 -5
- package/dist/async-debouncer.cjs.map +1 -1
- package/dist/async-debouncer.d.cts +5 -6
- package/dist/async-debouncer.d.ts +5 -6
- package/dist/async-debouncer.js +2 -5
- package/dist/async-debouncer.js.map +1 -1
- package/dist/async-queuer.cjs +49 -17
- package/dist/async-queuer.cjs.map +1 -1
- package/dist/async-queuer.d.cts +1 -1
- package/dist/async-queuer.d.ts +1 -1
- package/dist/async-queuer.js +49 -17
- package/dist/async-queuer.js.map +1 -1
- package/dist/async-rate-limiter.cjs +3 -6
- package/dist/async-rate-limiter.cjs.map +1 -1
- package/dist/async-rate-limiter.d.cts +4 -5
- package/dist/async-rate-limiter.d.ts +4 -5
- package/dist/async-rate-limiter.js +3 -6
- package/dist/async-rate-limiter.js.map +1 -1
- package/dist/async-retryer.cjs +2 -11
- package/dist/async-retryer.cjs.map +1 -1
- package/dist/async-retryer.d.cts +4 -2
- package/dist/async-retryer.d.ts +4 -2
- package/dist/async-retryer.js +2 -11
- package/dist/async-retryer.js.map +1 -1
- package/dist/async-throttler.cjs +2 -5
- package/dist/async-throttler.cjs.map +1 -1
- package/dist/async-throttler.d.cts +5 -6
- package/dist/async-throttler.d.ts +5 -6
- package/dist/async-throttler.js +2 -5
- package/dist/async-throttler.js.map +1 -1
- package/dist/batcher.cjs.map +1 -1
- package/dist/batcher.d.cts +0 -1
- package/dist/batcher.d.ts +0 -1
- package/dist/batcher.js.map +1 -1
- package/dist/debouncer.cjs.map +1 -1
- package/dist/debouncer.d.cts +0 -1
- package/dist/debouncer.d.ts +0 -1
- package/dist/debouncer.js.map +1 -1
- package/dist/event-client.cjs.map +1 -1
- package/dist/event-client.d.cts +0 -1
- package/dist/event-client.d.ts +0 -1
- package/dist/event-client.js.map +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.js +1 -1
- package/dist/queuer.cjs +7 -2
- package/dist/queuer.cjs.map +1 -1
- package/dist/queuer.d.cts +1 -1
- package/dist/queuer.d.ts +1 -1
- package/dist/queuer.js +7 -2
- package/dist/queuer.js.map +1 -1
- package/dist/rate-limiter.cjs.map +1 -1
- package/dist/rate-limiter.d.cts +0 -1
- package/dist/rate-limiter.d.ts +0 -1
- package/dist/rate-limiter.js.map +1 -1
- package/dist/throttler.cjs.map +1 -1
- package/dist/throttler.d.cts +0 -1
- package/dist/throttler.d.ts +0 -1
- package/dist/throttler.js.map +1 -1
- package/dist/utils.cjs +0 -1
- package/dist/utils.cjs.map +1 -1
- package/dist/utils.d.cts +0 -1
- package/dist/utils.d.ts +0 -1
- package/dist/utils.js.map +1 -1
- package/package.json +3 -3
- package/src/async-debouncer.ts +30 -29
- package/src/async-queuer.ts +83 -20
- package/src/async-rate-limiter.ts +11 -10
- package/src/async-retryer.ts +4 -15
- package/src/async-throttler.ts +33 -32
- package/src/queuer.ts +13 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"async-debouncer.js","names":["#getEnabled","#cancelPendingExecution","#setState","#execute","#resolvePreviousPromise","#timeoutId","#getWait","#clearTimeout","#resolvePreviousPromiseInternal"],"sources":["../src/async-debouncer.ts"],"sourcesContent":["import { Store } from '@tanstack/store'\nimport { AsyncRetryer } from './async-retryer'\nimport { parseFunctionOrValue } from './utils'\nimport { emitChange, pacerEventClient } from './event-client'\nimport type { AsyncRetryerOptions } from './async-retryer'\nimport type { AnyAsyncFunction, OptionalKeys } from './types'\n\nexport interface AsyncDebouncerState<TFn extends AnyAsyncFunction> {\n /**\n * Whether the debouncer can execute on the leading edge of the timeout\n */\n canLeadingExecute: boolean\n /**\n * Number of function executions that have resulted in errors\n */\n errorCount: number\n /**\n * Whether the debounced function is currently executing asynchronously\n */\n isExecuting: boolean\n /**\n * Whether the debouncer is waiting for the timeout to trigger execution\n */\n isPending: boolean\n /**\n * The arguments from the most recent call to maybeExecute\n */\n lastArgs: Parameters<TFn> | undefined\n /**\n * The result from the most recent successful function execution\n */\n lastResult: ReturnType<TFn> | undefined\n /**\n * Number of times maybeExecute has been called (for reduction calculations)\n */\n maybeExecuteCount: number\n /**\n * Number of function executions that have completed (either successfully or with errors)\n */\n settleCount: number\n /**\n * Current execution status - 'idle' when not active, 'pending' when waiting, 'executing' when running, 'settled' when completed\n */\n status: 'disabled' | 'idle' | 'pending' | 'executing' | 'settled'\n /**\n * Number of function executions that have completed successfully\n */\n successCount: number\n}\n\nfunction getDefaultAsyncDebouncerState<\n TFn extends AnyAsyncFunction,\n>(): AsyncDebouncerState<TFn> {\n return {\n canLeadingExecute: true,\n errorCount: 0,\n isExecuting: false,\n isPending: false,\n lastArgs: undefined,\n lastResult: undefined,\n maybeExecuteCount: 0,\n settleCount: 0,\n status: 'idle',\n successCount: 0,\n }\n}\n\n/**\n * Options for configuring an async debounced function\n */\nexport interface AsyncDebouncerOptions<TFn extends AnyAsyncFunction> {\n /**\n * Options for configuring the underlying async retryer\n */\n asyncRetryerOptions?: AsyncRetryerOptions<TFn>\n /**\n * Whether the debouncer is enabled. When disabled, maybeExecute will not trigger any executions.\n * Can be a boolean or a function that returns a boolean.\n * Defaults to true.\n */\n enabled?: boolean | ((debouncer: AsyncDebouncer<TFn>) => boolean)\n /**\n * Initial state for the async debouncer\n */\n initialState?: Partial<AsyncDebouncerState<TFn>>\n /**\n * Optional key to identify this async debouncer instance.\n * If provided, the async debouncer will be identified by this key in the devtools and PacerProvider if applicable.\n */\n key?: string\n /**\n * Whether to execute on the leading edge of the timeout.\n * Defaults to false.\n */\n leading?: boolean\n /**\n * Optional error handler for when the debounced function throws.\n * If provided, the handler will be called with the error and debouncer instance.\n * This can be used alongside throwOnError - the handler will be called before any error is thrown.\n */\n onError?: (\n error: Error,\n args: Parameters<TFn>,\n debouncer: AsyncDebouncer<TFn>,\n ) => void\n /**\n * Optional callback to call when the debounced function is executed\n */\n onSettled?: (args: Parameters<TFn>, debouncer: AsyncDebouncer<TFn>) => void\n /**\n * Optional callback to call when the debounced function is executed\n */\n onSuccess?: (\n result: ReturnType<TFn>,\n args: Parameters<TFn>,\n debouncer: AsyncDebouncer<TFn>,\n ) => void\n /**\n * Whether to throw errors when they occur.\n * Defaults to true if no onError handler is provided, false if an onError handler is provided.\n * Can be explicitly set to override these defaults.\n */\n throwOnError?: boolean\n /**\n * Whether to execute on the trailing edge of the timeout.\n * Defaults to true.\n */\n trailing?: boolean\n /**\n * Delay in milliseconds to wait after the last call before executing.\n * Can be a number or a function that returns a number.\n * Defaults to 0ms\n */\n wait: number | ((debouncer: AsyncDebouncer<TFn>) => number)\n}\n\n/**\n * Utility function for sharing common `AsyncDebouncerOptions` options between different `AsyncDebouncer` instances.\n */\nexport function asyncDebouncerOptions<\n TFn extends AnyAsyncFunction = AnyAsyncFunction,\n TOptions extends Partial<AsyncDebouncerOptions<TFn>> = Partial<\n AsyncDebouncerOptions<TFn>\n >,\n>(options: TOptions): TOptions {\n return options\n}\n\ntype AsyncDebouncerOptionsWithOptionalCallbacks = OptionalKeys<\n AsyncDebouncerOptions<any>,\n 'initialState' | 'onError' | 'onSettled' | 'onSuccess' | 'key'\n>\n\nconst defaultOptions: AsyncDebouncerOptionsWithOptionalCallbacks = {\n asyncRetryerOptions: {\n maxAttempts: 1,\n },\n enabled: true,\n leading: false,\n trailing: true,\n wait: 0,\n}\n\n/**\n * A class that creates an async debounced function.\n *\n * Async vs Sync Versions:\n * The async version provides advanced features over the sync Debouncer:\n * - Returns promises that can be awaited for debounced function results\n * - Built-in retry support via AsyncRetryer integration\n * - Abort support to cancel in-flight executions\n * - Cancel support to prevent pending executions from starting\n * - Comprehensive error handling with onError callbacks and throwOnError control\n * - Detailed execution tracking (success/error/settle counts)\n *\n * The sync Debouncer is lighter weight and simpler when you don't need async features,\n * return values, or execution control.\n *\n * What is Debouncing?\n * Debouncing ensures that a function is only executed after a specified delay has passed since its last invocation.\n * Each new invocation resets the delay timer. This is useful for handling frequent events like window resizing\n * or input changes where you only want to execute the handler after the events have stopped occurring.\n *\n * Unlike throttling which allows execution at regular intervals, debouncing prevents any execution until\n * the function stops being called for the specified delay period.\n *\n * Error Handling:\n * - If an `onError` handler is provided, it will be called with the error and debouncer instance\n * - If `throwOnError` is true (default when no onError handler is provided), the error will be thrown\n * - If `throwOnError` is false (default when onError handler is provided), the error will be swallowed\n * - Both onError and throwOnError can be used together - the handler will be called before any error is thrown\n * - The error state can be checked using the underlying store\n *\n * State Management:\n * - The debouncer uses a reactive store for state management\n * - Use `initialState` to provide initial state values when creating the async debouncer\n * - The state includes canLeadingExecute, error count, execution status, and success/settle counts\n * - State can be accessed via the `store` property and its `state` getter\n * - The store is reactive and will notify subscribers of state changes\n *\n * @example\n * ```ts\n * const asyncDebouncer = new AsyncDebouncer(async (value: string) => {\n * const results = await searchAPI(value);\n * return results; // Return value is preserved\n * }, {\n * wait: 500,\n * onError: (error) => {\n * console.error('Search failed:', error);\n * }\n * });\n *\n * // Called on each keystroke but only executes after 500ms of no typing\n * // Returns the API response directly\n * const results = await asyncDebouncer.maybeExecute(inputElement.value);\n * ```\n */\nexport class AsyncDebouncer<TFn extends AnyAsyncFunction> {\n readonly store: Store<Readonly<AsyncDebouncerState<TFn>>> = new Store<\n AsyncDebouncerState<TFn>\n >(getDefaultAsyncDebouncerState<TFn>())\n key: string | undefined\n options: AsyncDebouncerOptions<TFn>\n asyncRetryers = new Map<number, AsyncRetryer<TFn>>()\n #timeoutId: ReturnType<typeof setTimeout> | null = null\n #resolvePreviousPromise:\n | ((value?: ReturnType<TFn> | undefined) => void)\n | null = null\n\n constructor(\n public fn: TFn,\n initialOptions: AsyncDebouncerOptions<TFn>,\n ) {\n this.key = initialOptions.key\n this.options = {\n ...defaultOptions,\n ...initialOptions,\n throwOnError: initialOptions.throwOnError ?? !initialOptions.onError,\n }\n this.#setState(this.options.initialState ?? {})\n\n if (this.key) {\n pacerEventClient.on('d-AsyncDebouncer', (event) => {\n if (event.payload.key !== this.key) return\n this.#setState(\n event.payload.store.state as Partial<AsyncDebouncerState<TFn>>,\n )\n this.setOptions(\n event.payload.options as Partial<AsyncDebouncerOptions<TFn>>,\n )\n })\n }\n }\n\n /**\n * Updates the async debouncer options\n */\n setOptions = (newOptions: Partial<AsyncDebouncerOptions<TFn>>): void => {\n this.options = { ...this.options, ...newOptions }\n\n // Cancel pending execution if the debouncer is disabled\n if (!this.#getEnabled()) {\n this.cancel()\n }\n }\n\n #setState = (newState: Partial<AsyncDebouncerState<TFn>>): void => {\n this.store.setState((state) => {\n const combinedState = {\n ...state,\n ...newState,\n }\n const { isPending, isExecuting, settleCount } = combinedState\n return {\n ...combinedState,\n status: !this.#getEnabled()\n ? 'disabled'\n : isPending\n ? 'pending'\n : isExecuting\n ? 'executing'\n : settleCount > 0\n ? 'settled'\n : 'idle',\n }\n })\n emitChange('AsyncDebouncer', this)\n }\n\n /**\n * Returns the current debouncer enabled state\n */\n #getEnabled = (): boolean => {\n return !!parseFunctionOrValue(this.options.enabled, this)\n }\n\n /**\n * Returns the current debouncer wait state\n */\n #getWait = (): number => {\n return parseFunctionOrValue(this.options.wait, this)\n }\n\n /**\n * Attempts to execute the debounced function.\n * If a call is already in progress, it will be queued.\n *\n * Error Handling:\n * - If the debounced function throws and no `onError` handler is configured,\n * the error will be thrown from this method.\n * - If an `onError` handler is configured, errors will be caught and passed to the handler,\n * and this method will return undefined.\n * - The error state can be checked using `getErrorCount()` and `getIsExecuting()`.\n *\n * @returns A promise that resolves with the function's return value, or undefined if an error occurred and was handled by onError\n * @throws The error from the debounced function if no onError handler is configured\n */\n maybeExecute = async (\n ...args: Parameters<TFn>\n ): Promise<ReturnType<TFn> | undefined> => {\n if (!this.#getEnabled()) return undefined\n this.#cancelPendingExecution()\n this.#setState({\n lastArgs: args,\n maybeExecuteCount: this.store.state.maybeExecuteCount + 1,\n })\n\n // Handle leading execution\n if (this.options.leading && this.store.state.canLeadingExecute) {\n this.#setState({ canLeadingExecute: false })\n await this.#execute(...args)\n return this.store.state.lastResult\n }\n\n // Handle trailing execution\n if (this.options.trailing && this.#getEnabled()) {\n this.#setState({ isPending: true })\n }\n\n return new Promise((resolve, reject) => {\n this.#resolvePreviousPromise = resolve\n // this.#rejectPreviousPromise = reject\n this.#timeoutId = setTimeout(async () => {\n // Execute trailing if enabled\n if (this.options.trailing && this.store.state.lastArgs) {\n try {\n await this.#execute(...this.store.state.lastArgs)\n } catch (error) {\n reject(error)\n }\n }\n\n // Reset state and resolve\n this.#setState({ canLeadingExecute: true })\n this.#resolvePreviousPromise = null\n resolve(this.store.state.lastResult)\n }, this.#getWait())\n })\n }\n\n #execute = async (\n ...args: Parameters<TFn>\n ): Promise<ReturnType<TFn> | undefined> => {\n if (!this.#getEnabled()) return undefined\n const currentMaybeExecuteCount = this.store.state.maybeExecuteCount + 1\n\n try {\n this.#setState({ isExecuting: true })\n const currentAsyncRetryer = new AsyncRetryer(this.fn, {\n ...this.options.asyncRetryerOptions,\n key: `${this.key}-retryer-${currentMaybeExecuteCount}`,\n })\n this.asyncRetryers.set(currentMaybeExecuteCount, currentAsyncRetryer)\n const result = await currentAsyncRetryer.execute(...args) // EXECUTE!\n this.#setState({\n lastResult: result,\n successCount: this.store.state.successCount + 1,\n })\n this.options.onSuccess?.(result as ReturnType<TFn>, args, this)\n } catch (error) {\n this.#setState({\n errorCount: this.store.state.errorCount + 1,\n })\n this.options.onError?.(error as Error, args, this)\n if (this.options.throwOnError) {\n throw error\n }\n } finally {\n this.asyncRetryers.delete(currentMaybeExecuteCount) // dispose retryer\n this.#setState({\n isExecuting: false,\n isPending: false,\n lastArgs: undefined,\n settleCount: this.store.state.settleCount + 1,\n })\n this.options.onSettled?.(args, this)\n }\n return this.store.state.lastResult\n }\n\n /**\n * Processes the current pending execution immediately\n */\n flush = async (): Promise<ReturnType<TFn> | undefined> => {\n if (this.store.state.isPending && this.store.state.lastArgs) {\n const { lastArgs } = this.store.state\n this.#cancelPendingExecution()\n return await this.#execute(...lastArgs)\n }\n return undefined\n }\n\n #resolvePreviousPromiseInternal = (): void => {\n if (this.#resolvePreviousPromise) {\n this.#resolvePreviousPromise(this.store.state.lastResult)\n this.#resolvePreviousPromise = null\n }\n }\n\n #clearTimeout = (): void => {\n if (this.#timeoutId) {\n clearTimeout(this.#timeoutId)\n this.#timeoutId = null\n }\n }\n\n /**\n * Internal cancel without resetting the leading execute state\n */\n #cancelPendingExecution = (): void => {\n this.#clearTimeout()\n this.#resolvePreviousPromiseInternal()\n this.#setState({\n isPending: false,\n lastArgs: undefined,\n })\n }\n\n /**\n * Returns the AbortSignal for a specific execution.\n * If no maybeExecuteCount is provided, returns the signal for the most recent execution.\n * Returns null if no execution is found or not currently executing.\n *\n * @param maybeExecuteCount - Optional specific execution to get signal for\n * @example\n * ```typescript\n * const debouncer = new AsyncDebouncer(\n * async (searchTerm: string) => {\n * const signal = debouncer.getAbortSignal()\n * if (signal) {\n * const response = await fetch(`/api/search?q=${searchTerm}`, { signal })\n * return response.json()\n * }\n * },\n * { wait: 300 }\n * )\n * ```\n */\n getAbortSignal = (maybeExecuteCount?: number): AbortSignal | null => {\n const count = maybeExecuteCount ?? this.store.state.maybeExecuteCount\n const retryer = this.asyncRetryers.get(count)\n return retryer?.getAbortSignal() ?? null\n }\n\n /**\n * Aborts all ongoing executions with the internal abort controllers.\n * Does NOT cancel any pending execution that have not started yet.\n */\n abort = (): void => {\n this.asyncRetryers.forEach((retryer) => retryer.abort())\n this.asyncRetryers.clear()\n this.#setState({\n isExecuting: false,\n })\n }\n\n /**\n * Cancels any pending execution that have not started yet.\n * Does NOT abort any execution already in progress.\n */\n cancel = (): void => {\n this.#cancelPendingExecution()\n this.#setState({ canLeadingExecute: true })\n }\n\n /**\n * Resets the debouncer state to its default values\n */\n reset = (): void => {\n this.#setState(getDefaultAsyncDebouncerState<TFn>())\n this.asyncRetryers.forEach((retryer) => retryer.reset())\n }\n}\n\n/**\n * Creates an async debounced function that delays execution until after a specified wait time.\n * The debounced function will only execute once the wait period has elapsed without any new calls.\n * If called again during the wait period, the timer resets and a new wait period begins.\n *\n * Async vs Sync Versions:\n * The async version provides advanced features over the sync debounce function:\n * - Returns promises that can be awaited for debounced function results\n * - Built-in retry support via AsyncRetryer integration\n * - Abort support to cancel in-flight executions\n * - Cancel support to prevent pending executions from starting\n * - Comprehensive error handling with onError callbacks and throwOnError control\n * - Detailed execution tracking (success/error/settle counts)\n *\n * The sync debounce function is lighter weight and simpler when you don't need async features,\n * return values, or execution control.\n *\n * What is Debouncing?\n * Debouncing ensures that a function is only executed after a specified delay has passed since its last invocation.\n * Each new invocation resets the delay timer. This is useful for handling frequent events like window resizing\n * or input changes where you only want to execute the handler after the events have stopped occurring.\n *\n * Configuration Options:\n * - `wait`: Delay in milliseconds to wait after the last call (required)\n * - `leading`: Execute on the leading edge of the timeout (default: false)\n * - `trailing`: Execute on the trailing edge of the timeout (default: true)\n * - `enabled`: Whether the debouncer is enabled (default: true)\n * - `asyncRetryerOptions`: Configure retry behavior for executions\n *\n * Error Handling:\n * - If an `onError` handler is provided, it will be called with the error and debouncer instance\n * - If `throwOnError` is true (default when no onError handler is provided), the error will be thrown\n * - If `throwOnError` is false (default when onError handler is provided), the error will be swallowed\n * - The error state can be checked using the underlying AsyncDebouncer instance\n * - Both onError and throwOnError can be used together - the handler will be called before any error is thrown\n *\n * State Management:\n * - Uses TanStack Store for reactive state management\n * - Use `initialState` to provide initial state values when creating the async debouncer\n * - Use `onSuccess` callback to react to successful function execution and implement custom logic\n * - Use `onError` callback to react to function execution errors and implement custom error handling\n * - Use `onSettled` callback to react to function execution completion (success or error) and implement custom logic\n * - The state includes canLeadingExecute, error count, execution status, and success/settle counts\n * - State can be accessed via `asyncDebouncer.store.state` when using the class directly\n * - When using framework adapters (React/Solid), state is accessed from `asyncDebouncer.state`\n *\n * @example\n * ```ts\n * const debounced = asyncDebounce(async (value: string) => {\n * const result = await saveToAPI(value);\n * return result; // Return value is preserved\n * }, {\n * wait: 1000,\n * onError: (error) => {\n * console.error('API call failed:', error);\n * },\n * throwOnError: true // Will both log the error and throw it\n * });\n *\n * // Will only execute once, 1 second after the last call\n * // Returns the API response directly\n * const result = await debounced(\"third\");\n * ```\n */\nexport function asyncDebounce<TFn extends AnyAsyncFunction>(\n fn: TFn,\n initialOptions: AsyncDebouncerOptions<TFn>,\n) {\n const asyncDebouncer = new AsyncDebouncer(fn, initialOptions)\n return asyncDebouncer.maybeExecute\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"async-debouncer.js","names":["#getEnabled","#cancelPendingExecution","#setState","#execute","#resolvePreviousPromise","#timeoutId","#getWait","#clearTimeout","#resolvePreviousPromiseInternal"],"sources":["../src/async-debouncer.ts"],"sourcesContent":["import { Store } from '@tanstack/store'\nimport { AsyncRetryer } from './async-retryer'\nimport { parseFunctionOrValue } from './utils'\nimport { emitChange, pacerEventClient } from './event-client'\nimport type { AsyncRetryerOptions } from './async-retryer'\nimport type { AnyAsyncFunction, OptionalKeys } from './types'\n\nexport interface AsyncDebouncerState<TFn extends AnyAsyncFunction> {\n /**\n * Whether the debouncer can execute on the leading edge of the timeout\n */\n canLeadingExecute: boolean\n /**\n * Number of function executions that have resulted in errors\n */\n errorCount: number\n /**\n * Whether the debounced function is currently executing asynchronously\n */\n isExecuting: boolean\n /**\n * Whether the debouncer is waiting for the timeout to trigger execution\n */\n isPending: boolean\n /**\n * The arguments from the most recent call to maybeExecute\n */\n lastArgs: Parameters<TFn> | undefined\n /**\n * The result from the most recent successful function execution\n */\n lastResult: Awaited<ReturnType<TFn>> | undefined\n /**\n * Number of times maybeExecute has been called (for reduction calculations)\n */\n maybeExecuteCount: number\n /**\n * Number of function executions that have completed (either successfully or with errors)\n */\n settleCount: number\n /**\n * Current execution status - 'idle' when not active, 'pending' when waiting, 'executing' when running, 'settled' when completed\n */\n status: 'disabled' | 'idle' | 'pending' | 'executing' | 'settled'\n /**\n * Number of function executions that have completed successfully\n */\n successCount: number\n}\n\nfunction getDefaultAsyncDebouncerState<\n TFn extends AnyAsyncFunction,\n>(): AsyncDebouncerState<TFn> {\n return {\n canLeadingExecute: true,\n errorCount: 0,\n isExecuting: false,\n isPending: false,\n lastArgs: undefined,\n lastResult: undefined,\n maybeExecuteCount: 0,\n settleCount: 0,\n status: 'idle',\n successCount: 0,\n }\n}\n\n/**\n * Options for configuring an async debounced function\n */\nexport interface AsyncDebouncerOptions<TFn extends AnyAsyncFunction> {\n /**\n * Options for configuring the underlying async retryer\n */\n asyncRetryerOptions?: AsyncRetryerOptions<TFn>\n /**\n * Whether the debouncer is enabled. When disabled, maybeExecute will not trigger any executions.\n * Can be a boolean or a function that returns a boolean.\n * Defaults to true.\n */\n enabled?: boolean | ((debouncer: AsyncDebouncer<TFn>) => boolean)\n /**\n * Initial state for the async debouncer\n */\n initialState?: Partial<AsyncDebouncerState<TFn>>\n /**\n * Optional key to identify this async debouncer instance.\n * If provided, the async debouncer will be identified by this key in the devtools and PacerProvider if applicable.\n */\n key?: string\n /**\n * Whether to execute on the leading edge of the timeout.\n * Defaults to false.\n */\n leading?: boolean\n /**\n * Optional error handler for when the debounced function throws.\n * If provided, the handler will be called with the error and debouncer instance.\n * This can be used alongside throwOnError - the handler will be called before any error is thrown.\n */\n onError?: (\n error: Error,\n args: Parameters<TFn>,\n debouncer: AsyncDebouncer<TFn>,\n ) => void\n /**\n * Optional callback to call when the debounced function is executed\n */\n onSettled?: (args: Parameters<TFn>, debouncer: AsyncDebouncer<TFn>) => void\n /**\n * Optional callback to call when the debounced function is executed\n */\n onSuccess?: (\n result: Awaited<ReturnType<TFn>>,\n args: Parameters<TFn>,\n debouncer: AsyncDebouncer<TFn>,\n ) => void\n /**\n * Whether to throw errors when they occur.\n * Defaults to true if no onError handler is provided, false if an onError handler is provided.\n * Can be explicitly set to override these defaults.\n */\n throwOnError?: boolean\n /**\n * Whether to execute on the trailing edge of the timeout.\n * Defaults to true.\n */\n trailing?: boolean\n /**\n * Delay in milliseconds to wait after the last call before executing.\n * Can be a number or a function that returns a number.\n * Defaults to 0ms\n */\n wait: number | ((debouncer: AsyncDebouncer<TFn>) => number)\n}\n\n/**\n * Utility function for sharing common `AsyncDebouncerOptions` options between different `AsyncDebouncer` instances.\n */\nexport function asyncDebouncerOptions<\n TFn extends AnyAsyncFunction = AnyAsyncFunction,\n TOptions extends Partial<AsyncDebouncerOptions<TFn>> = Partial<\n AsyncDebouncerOptions<TFn>\n >,\n>(options: TOptions): TOptions {\n return options\n}\n\ntype AsyncDebouncerOptionsWithOptionalCallbacks = OptionalKeys<\n AsyncDebouncerOptions<any>,\n 'initialState' | 'onError' | 'onSettled' | 'onSuccess' | 'key'\n>\n\nconst defaultOptions: AsyncDebouncerOptionsWithOptionalCallbacks = {\n asyncRetryerOptions: {\n maxAttempts: 1,\n },\n enabled: true,\n leading: false,\n trailing: true,\n wait: 0,\n}\n\n/**\n * A class that creates an async debounced function.\n *\n * Async vs Sync Versions:\n * The async version provides advanced features over the sync Debouncer:\n * - Returns promises that can be awaited for debounced function results\n * - Built-in retry support via AsyncRetryer integration\n * - Abort support to cancel in-flight executions\n * - Cancel support to prevent pending executions from starting\n * - Comprehensive error handling with onError callbacks and throwOnError control\n * - Detailed execution tracking (success/error/settle counts)\n *\n * The sync Debouncer is lighter weight and simpler when you don't need async features,\n * return values, or execution control.\n *\n * What is Debouncing?\n * Debouncing ensures that a function is only executed after a specified delay has passed since its last invocation.\n * Each new invocation resets the delay timer. This is useful for handling frequent events like window resizing\n * or input changes where you only want to execute the handler after the events have stopped occurring.\n *\n * Unlike throttling which allows execution at regular intervals, debouncing prevents any execution until\n * the function stops being called for the specified delay period.\n *\n * Error Handling:\n * - If an `onError` handler is provided, it will be called with the error and debouncer instance\n * - If `throwOnError` is true (default when no onError handler is provided), the error will be thrown\n * - If `throwOnError` is false (default when onError handler is provided), the error will be swallowed\n * - Both onError and throwOnError can be used together - the handler will be called before any error is thrown\n * - The error state can be checked using the underlying store\n *\n * State Management:\n * - The debouncer uses a reactive store for state management\n * - Use `initialState` to provide initial state values when creating the async debouncer\n * - The state includes canLeadingExecute, error count, execution status, and success/settle counts\n * - State can be accessed via the `store` property and its `state` getter\n * - The store is reactive and will notify subscribers of state changes\n *\n * @example\n * ```ts\n * const asyncDebouncer = new AsyncDebouncer(async (value: string) => {\n * const results = await searchAPI(value);\n * return results; // Return value is preserved\n * }, {\n * wait: 500,\n * onError: (error) => {\n * console.error('Search failed:', error);\n * }\n * });\n *\n * // Called on each keystroke but only executes after 500ms of no typing\n * // Returns the API response directly\n * const results = await asyncDebouncer.maybeExecute(inputElement.value);\n * ```\n */\nexport class AsyncDebouncer<TFn extends AnyAsyncFunction> {\n readonly store: Store<Readonly<AsyncDebouncerState<TFn>>> = new Store<\n AsyncDebouncerState<TFn>\n >(getDefaultAsyncDebouncerState<TFn>())\n key: string | undefined\n options: AsyncDebouncerOptions<TFn>\n asyncRetryers = new Map<number, AsyncRetryer<TFn>>()\n #timeoutId: ReturnType<typeof setTimeout> | null = null\n #resolvePreviousPromise:\n ((value?: Awaited<ReturnType<TFn>> | undefined) => void) | null = null\n\n constructor(\n public fn: TFn,\n initialOptions: AsyncDebouncerOptions<TFn>,\n ) {\n this.key = initialOptions.key\n this.options = {\n ...defaultOptions,\n ...initialOptions,\n throwOnError: initialOptions.throwOnError ?? !initialOptions.onError,\n }\n this.#setState(this.options.initialState ?? {})\n\n if (this.key) {\n pacerEventClient.on('d-AsyncDebouncer', (event) => {\n if (event.payload.key !== this.key) return\n this.#setState(\n event.payload.store.state as Partial<AsyncDebouncerState<TFn>>,\n )\n this.setOptions(\n event.payload.options as Partial<AsyncDebouncerOptions<TFn>>,\n )\n })\n }\n }\n\n /**\n * Updates the async debouncer options\n */\n setOptions = (newOptions: Partial<AsyncDebouncerOptions<TFn>>): void => {\n this.options = { ...this.options, ...newOptions }\n\n // Cancel pending execution if the debouncer is disabled\n if (!this.#getEnabled()) {\n this.cancel()\n }\n }\n\n #setState = (newState: Partial<AsyncDebouncerState<TFn>>): void => {\n this.store.setState((state) => {\n const combinedState = {\n ...state,\n ...newState,\n }\n const { isPending, isExecuting, settleCount } = combinedState\n return {\n ...combinedState,\n status: !this.#getEnabled()\n ? 'disabled'\n : isPending\n ? 'pending'\n : isExecuting\n ? 'executing'\n : settleCount > 0\n ? 'settled'\n : 'idle',\n }\n })\n emitChange('AsyncDebouncer', this)\n }\n\n /**\n * Returns the current debouncer enabled state\n */\n #getEnabled = (): boolean => {\n return !!parseFunctionOrValue(this.options.enabled, this)\n }\n\n /**\n * Returns the current debouncer wait state\n */\n #getWait = (): number => {\n return parseFunctionOrValue(this.options.wait, this)\n }\n\n /**\n * Attempts to execute the debounced function.\n * If a call is already in progress, it will be queued.\n *\n * Error Handling:\n * - If the debounced function throws and no `onError` handler is configured,\n * the error will be thrown from this method.\n * - If an `onError` handler is configured, errors will be caught and passed to the handler,\n * and this method will return undefined.\n * - The error state can be checked using `getErrorCount()` and `getIsExecuting()`.\n *\n * @returns A promise that resolves with the function's return value, or undefined if an error occurred and was handled by onError\n * @throws The error from the debounced function if no onError handler is configured\n */\n maybeExecute = async (\n ...args: Parameters<TFn>\n ): Promise<Awaited<ReturnType<TFn>> | undefined> => {\n if (!this.#getEnabled()) return undefined\n this.#cancelPendingExecution()\n this.#setState({\n lastArgs: args,\n maybeExecuteCount: this.store.state.maybeExecuteCount + 1,\n })\n\n // Handle leading execution\n if (this.options.leading && this.store.state.canLeadingExecute) {\n this.#setState({ canLeadingExecute: false })\n await this.#execute(...args)\n return this.store.state.lastResult\n }\n\n // Handle trailing execution\n if (this.options.trailing && this.#getEnabled()) {\n this.#setState({ isPending: true })\n }\n\n return new Promise<Awaited<ReturnType<TFn>> | undefined>(\n (resolve, reject) => {\n this.#resolvePreviousPromise = resolve\n // this.#rejectPreviousPromise = reject\n this.#timeoutId = setTimeout(async () => {\n // Execute trailing if enabled\n if (this.options.trailing && this.store.state.lastArgs) {\n try {\n await this.#execute(...this.store.state.lastArgs)\n } catch (error) {\n reject(error)\n }\n }\n\n // Reset state and resolve\n this.#setState({ canLeadingExecute: true })\n this.#resolvePreviousPromise = null\n resolve(this.store.state.lastResult)\n }, this.#getWait())\n },\n )\n }\n\n #execute = async (\n ...args: Parameters<TFn>\n ): Promise<Awaited<ReturnType<TFn>> | undefined> => {\n if (!this.#getEnabled()) return undefined\n const currentMaybeExecuteCount = this.store.state.maybeExecuteCount + 1\n\n try {\n this.#setState({ isExecuting: true })\n const currentAsyncRetryer = new AsyncRetryer(\n this.fn,\n this.options.asyncRetryerOptions,\n )\n this.asyncRetryers.set(currentMaybeExecuteCount, currentAsyncRetryer)\n const result = await currentAsyncRetryer.execute(...args) // EXECUTE!\n this.#setState({\n lastResult: result,\n successCount: this.store.state.successCount + 1,\n })\n this.options.onSuccess?.(result as Awaited<ReturnType<TFn>>, args, this)\n } catch (error) {\n this.#setState({\n errorCount: this.store.state.errorCount + 1,\n })\n this.options.onError?.(error as Error, args, this)\n if (this.options.throwOnError) {\n throw error\n }\n } finally {\n this.asyncRetryers.delete(currentMaybeExecuteCount) // dispose retryer\n this.#setState({\n isExecuting: false,\n isPending: false,\n lastArgs: undefined,\n settleCount: this.store.state.settleCount + 1,\n })\n this.options.onSettled?.(args, this)\n }\n return this.store.state.lastResult\n }\n\n /**\n * Processes the current pending execution immediately\n */\n flush = async (): Promise<Awaited<ReturnType<TFn>> | undefined> => {\n if (this.store.state.isPending && this.store.state.lastArgs) {\n const { lastArgs } = this.store.state\n this.#cancelPendingExecution()\n return await this.#execute(...lastArgs)\n }\n return undefined\n }\n\n #resolvePreviousPromiseInternal = (): void => {\n if (this.#resolvePreviousPromise) {\n this.#resolvePreviousPromise(this.store.state.lastResult)\n this.#resolvePreviousPromise = null\n }\n }\n\n #clearTimeout = (): void => {\n if (this.#timeoutId) {\n clearTimeout(this.#timeoutId)\n this.#timeoutId = null\n }\n }\n\n /**\n * Internal cancel without resetting the leading execute state\n */\n #cancelPendingExecution = (): void => {\n this.#clearTimeout()\n this.#resolvePreviousPromiseInternal()\n this.#setState({\n isPending: false,\n lastArgs: undefined,\n })\n }\n\n /**\n * Returns the AbortSignal for a specific execution.\n * If no maybeExecuteCount is provided, returns the signal for the most recent execution.\n * Returns null if no execution is found or not currently executing.\n *\n * @param maybeExecuteCount - Optional specific execution to get signal for\n * @example\n * ```typescript\n * const debouncer = new AsyncDebouncer(\n * async (searchTerm: string) => {\n * const signal = debouncer.getAbortSignal()\n * if (signal) {\n * const response = await fetch(`/api/search?q=${searchTerm}`, { signal })\n * return response.json()\n * }\n * },\n * { wait: 300 }\n * )\n * ```\n */\n getAbortSignal = (maybeExecuteCount?: number): AbortSignal | null => {\n const count = maybeExecuteCount ?? this.store.state.maybeExecuteCount\n const retryer = this.asyncRetryers.get(count)\n return retryer?.getAbortSignal() ?? null\n }\n\n /**\n * Aborts all ongoing executions with the internal abort controllers.\n * Does NOT cancel any pending execution that have not started yet.\n */\n abort = (): void => {\n this.asyncRetryers.forEach((retryer) => retryer.abort())\n this.asyncRetryers.clear()\n this.#setState({\n isExecuting: false,\n })\n }\n\n /**\n * Cancels any pending execution that have not started yet.\n * Does NOT abort any execution already in progress.\n */\n cancel = (): void => {\n this.#cancelPendingExecution()\n this.#setState({ canLeadingExecute: true })\n }\n\n /**\n * Resets the debouncer state to its default values\n */\n reset = (): void => {\n this.#setState(getDefaultAsyncDebouncerState<TFn>())\n this.asyncRetryers.forEach((retryer) => retryer.reset())\n }\n}\n\n/**\n * Creates an async debounced function that delays execution until after a specified wait time.\n * The debounced function will only execute once the wait period has elapsed without any new calls.\n * If called again during the wait period, the timer resets and a new wait period begins.\n *\n * Async vs Sync Versions:\n * The async version provides advanced features over the sync debounce function:\n * - Returns promises that can be awaited for debounced function results\n * - Built-in retry support via AsyncRetryer integration\n * - Abort support to cancel in-flight executions\n * - Cancel support to prevent pending executions from starting\n * - Comprehensive error handling with onError callbacks and throwOnError control\n * - Detailed execution tracking (success/error/settle counts)\n *\n * The sync debounce function is lighter weight and simpler when you don't need async features,\n * return values, or execution control.\n *\n * What is Debouncing?\n * Debouncing ensures that a function is only executed after a specified delay has passed since its last invocation.\n * Each new invocation resets the delay timer. This is useful for handling frequent events like window resizing\n * or input changes where you only want to execute the handler after the events have stopped occurring.\n *\n * Configuration Options:\n * - `wait`: Delay in milliseconds to wait after the last call (required)\n * - `leading`: Execute on the leading edge of the timeout (default: false)\n * - `trailing`: Execute on the trailing edge of the timeout (default: true)\n * - `enabled`: Whether the debouncer is enabled (default: true)\n * - `asyncRetryerOptions`: Configure retry behavior for executions\n *\n * Error Handling:\n * - If an `onError` handler is provided, it will be called with the error and debouncer instance\n * - If `throwOnError` is true (default when no onError handler is provided), the error will be thrown\n * - If `throwOnError` is false (default when onError handler is provided), the error will be swallowed\n * - The error state can be checked using the underlying AsyncDebouncer instance\n * - Both onError and throwOnError can be used together - the handler will be called before any error is thrown\n *\n * State Management:\n * - Uses TanStack Store for reactive state management\n * - Use `initialState` to provide initial state values when creating the async debouncer\n * - Use `onSuccess` callback to react to successful function execution and implement custom logic\n * - Use `onError` callback to react to function execution errors and implement custom error handling\n * - Use `onSettled` callback to react to function execution completion (success or error) and implement custom logic\n * - The state includes canLeadingExecute, error count, execution status, and success/settle counts\n * - State can be accessed via `asyncDebouncer.store.state` when using the class directly\n * - When using framework adapters (React/Solid), state is accessed from `asyncDebouncer.state`\n *\n * @example\n * ```ts\n * const debounced = asyncDebounce(async (value: string) => {\n * const result = await saveToAPI(value);\n * return result; // Return value is preserved\n * }, {\n * wait: 1000,\n * onError: (error) => {\n * console.error('API call failed:', error);\n * },\n * throwOnError: true // Will both log the error and throw it\n * });\n *\n * // Will only execute once, 1 second after the last call\n * // Returns the API response directly\n * const result = await debounced(\"third\");\n * ```\n */\nexport function asyncDebounce<TFn extends AnyAsyncFunction>(\n fn: TFn,\n initialOptions: AsyncDebouncerOptions<TFn>,\n) {\n const asyncDebouncer = new AsyncDebouncer(fn, initialOptions)\n return asyncDebouncer.maybeExecute\n}\n"],"mappings":"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"}
|
package/dist/async-queuer.cjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
|
2
2
|
const require_utils = require('./utils.cjs');
|
|
3
|
-
const require_event_client = require('./event-client.cjs');
|
|
4
3
|
const require_async_retryer = require('./async-retryer.cjs');
|
|
4
|
+
const require_event_client = require('./event-client.cjs');
|
|
5
5
|
let _tanstack_store = require("@tanstack/store");
|
|
6
6
|
|
|
7
7
|
//#region src/async-queuer.ts
|
|
@@ -160,38 +160,50 @@ var AsyncQueuer = class {
|
|
|
160
160
|
}
|
|
161
161
|
this.#setState({ pendingTick: true });
|
|
162
162
|
this.#checkExpiredItems();
|
|
163
|
-
|
|
163
|
+
let scheduledAsyncWork = false;
|
|
164
|
+
const activeItems = [...this.store.state.activeItems];
|
|
164
165
|
while (activeItems.length < this.#getConcurrency() && this.store.state.items.length > 0) {
|
|
165
166
|
const nextItem = this.peekNextItem();
|
|
166
|
-
if (
|
|
167
|
+
if (nextItem === void 0) break;
|
|
167
168
|
activeItems.push(nextItem);
|
|
168
|
-
this.#setState({ activeItems });
|
|
169
|
+
this.#setState({ activeItems: [...activeItems] });
|
|
170
|
+
scheduledAsyncWork = true;
|
|
169
171
|
(async () => {
|
|
170
|
-
|
|
172
|
+
try {
|
|
173
|
+
await this.execute();
|
|
174
|
+
} catch {}
|
|
171
175
|
const wait = this.#getWait();
|
|
172
176
|
if (wait > 0) {
|
|
173
|
-
const timeoutId = setTimeout(() =>
|
|
177
|
+
const timeoutId = setTimeout(() => {
|
|
178
|
+
this.#timeoutIds.delete(timeoutId);
|
|
179
|
+
this.#tick();
|
|
180
|
+
}, wait);
|
|
174
181
|
this.#timeoutIds.add(timeoutId);
|
|
175
182
|
return;
|
|
176
183
|
}
|
|
177
184
|
this.#tick();
|
|
178
185
|
})();
|
|
179
186
|
}
|
|
180
|
-
this.#setState({ pendingTick: false });
|
|
187
|
+
if (!scheduledAsyncWork) this.#setState({ pendingTick: false });
|
|
181
188
|
};
|
|
182
189
|
this.addItem = (item, position = this.options.addItemsTo ?? "back", runOnItemsChange = true) => {
|
|
183
190
|
this.#setState({ addItemCount: this.store.state.addItemCount + 1 });
|
|
191
|
+
if (item === void 0) {
|
|
192
|
+
this.#setState({ rejectionCount: this.store.state.rejectionCount + 1 });
|
|
193
|
+
this.options.onReject?.(item, this);
|
|
194
|
+
return false;
|
|
195
|
+
}
|
|
184
196
|
if (this.store.state.items.length >= (this.options.maxSize ?? Infinity)) {
|
|
185
197
|
this.#setState({ rejectionCount: this.store.state.rejectionCount + 1 });
|
|
186
198
|
this.options.onReject?.(item, this);
|
|
187
199
|
return false;
|
|
188
200
|
}
|
|
189
|
-
const priority = this.options.getPriority !== defaultOptions.getPriority ? this.options.getPriority(item) : item
|
|
201
|
+
const priority = this.options.getPriority !== defaultOptions.getPriority ? this.options.getPriority(item) : item?.priority;
|
|
190
202
|
const items = this.store.state.items;
|
|
191
203
|
const itemTimestamps = this.store.state.itemTimestamps;
|
|
192
204
|
if (priority !== void 0) {
|
|
193
205
|
const insertIndex = items.findIndex((existing) => {
|
|
194
|
-
return (this.options.getPriority !== defaultOptions.getPriority ? this.options.getPriority(existing) : existing
|
|
206
|
+
return (this.options.getPriority !== defaultOptions.getPriority ? this.options.getPriority(existing) : existing?.priority) < priority;
|
|
195
207
|
});
|
|
196
208
|
if (insertIndex === -1) {
|
|
197
209
|
items.push(item);
|
|
@@ -248,10 +260,7 @@ var AsyncQueuer = class {
|
|
|
248
260
|
isExecuting: true
|
|
249
261
|
});
|
|
250
262
|
try {
|
|
251
|
-
const currentAsyncRetryer = new require_async_retryer.AsyncRetryer(this.fn,
|
|
252
|
-
...this.options.asyncRetryerOptions,
|
|
253
|
-
key: `${this.key}-retryer-${currentExecuteCount}`
|
|
254
|
-
});
|
|
263
|
+
const currentAsyncRetryer = new require_async_retryer.AsyncRetryer(this.fn, this.options.asyncRetryerOptions);
|
|
255
264
|
this.asyncRetryers.set(currentExecuteCount, currentAsyncRetryer);
|
|
256
265
|
const lastResult = await currentAsyncRetryer.execute(item);
|
|
257
266
|
this.#setState({
|
|
@@ -265,9 +274,12 @@ var AsyncQueuer = class {
|
|
|
265
274
|
if (this.options.throwOnError) throw error;
|
|
266
275
|
} finally {
|
|
267
276
|
this.asyncRetryers.delete(currentExecuteCount);
|
|
277
|
+
const remainingActiveItems = [...this.store.state.activeItems];
|
|
278
|
+
const activeItemIndex = remainingActiveItems.indexOf(item);
|
|
279
|
+
if (activeItemIndex !== -1) remainingActiveItems.splice(activeItemIndex, 1);
|
|
268
280
|
this.#setState({
|
|
269
|
-
activeItems:
|
|
270
|
-
isExecuting:
|
|
281
|
+
activeItems: remainingActiveItems,
|
|
282
|
+
isExecuting: this.asyncRetryers.size > 0,
|
|
271
283
|
settledCount: this.store.state.settledCount + 1
|
|
272
284
|
});
|
|
273
285
|
this.options.onSettled?.(item, this);
|
|
@@ -277,11 +289,31 @@ var AsyncQueuer = class {
|
|
|
277
289
|
};
|
|
278
290
|
this.flush = async (numberOfItems = this.store.state.items.length, position) => {
|
|
279
291
|
this.#clearTimeouts();
|
|
280
|
-
await Promise.
|
|
292
|
+
const results = await Promise.allSettled(Array.from({ length: numberOfItems }, () => this.execute(position)));
|
|
293
|
+
this.#setState({ pendingTick: false });
|
|
294
|
+
if (this.store.state.isRunning && this.store.state.items.length > 0) {
|
|
295
|
+
const wait = this.#getWait();
|
|
296
|
+
if (wait > 0) {
|
|
297
|
+
this.#setState({ pendingTick: true });
|
|
298
|
+
const timeoutId = setTimeout(() => {
|
|
299
|
+
this.#timeoutIds.delete(timeoutId);
|
|
300
|
+
this.#tick();
|
|
301
|
+
}, wait);
|
|
302
|
+
this.#timeoutIds.add(timeoutId);
|
|
303
|
+
} else this.#tick();
|
|
304
|
+
}
|
|
305
|
+
const failure = results.find((result) => result.status === "rejected");
|
|
306
|
+
if (failure) throw failure.reason;
|
|
281
307
|
};
|
|
282
308
|
this.flushAsBatch = async (batchFunction) => {
|
|
283
309
|
this.#clearTimeouts();
|
|
284
|
-
|
|
310
|
+
const items = this.#getAllItems();
|
|
311
|
+
try {
|
|
312
|
+
await batchFunction(items);
|
|
313
|
+
} finally {
|
|
314
|
+
this.#setState({ pendingTick: false });
|
|
315
|
+
if (this.store.state.isRunning && this.store.state.items.length > 0) this.#tick();
|
|
316
|
+
}
|
|
285
317
|
};
|
|
286
318
|
this.#checkExpiredItems = () => {
|
|
287
319
|
if ((this.options.expirationDuration ?? Infinity) === Infinity && this.options.getIsExpired === defaultOptions.getIsExpired) return;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"async-queuer.cjs","names":["Store","parseFunctionOrValue","#setState","#checkExpiredItems","#getConcurrency","#getWait","#tick","#timeoutIds","AsyncRetryer","#clearTimeouts","#getAllItems"],"sources":["../src/async-queuer.ts"],"sourcesContent":["import { Store } from '@tanstack/store'\nimport { AsyncRetryer } from './async-retryer'\nimport { parseFunctionOrValue } from './utils'\nimport { emitChange, pacerEventClient } from './event-client'\nimport type { AsyncRetryerOptions } from './async-retryer'\nimport type { OptionalKeys } from './types'\nimport type { QueuePosition } from './queuer'\n\nexport interface AsyncQueuerState<TValue> {\n /**\n * Items currently being processed by the queuer\n */\n activeItems: Array<TValue>\n /**\n * Number of times addItem has been called (for reduction calculations)\n */\n addItemCount: number\n /**\n * Number of task executions that have resulted in errors\n */\n errorCount: number\n /**\n * Number of times execute has been called\n */\n executeCount: number\n /**\n * Number of items that have been removed from the queue due to expiration\n */\n expirationCount: number\n /**\n * Whether the queuer has no items to process (items array is empty)\n */\n isEmpty: boolean\n /**\n * Whether the queuer is currently executing\n */\n isExecuting: boolean\n /**\n * Whether the queuer has reached its maximum capacity\n */\n isFull: boolean\n /**\n * Whether the queuer is not currently processing any items\n */\n isIdle: boolean\n /**\n * Whether the queuer is active and will process items automatically\n */\n isRunning: boolean\n /**\n * Array of items currently waiting to be processed\n */\n items: Array<TValue>\n /**\n * Timestamps when items were added to the queue for expiration tracking\n */\n itemTimestamps: Array<number>\n /**\n * The result from the most recent task execution\n */\n lastResult: any\n /**\n * Whether the queuer has a pending timeout for processing the next item\n */\n pendingTick: boolean\n /**\n * Number of items that have been rejected from being added to the queue\n */\n rejectionCount: number\n /**\n * Number of task executions that have completed (either successfully or with errors)\n */\n settledCount: number\n /**\n * Number of items currently in the queue\n */\n size: number\n /**\n * Current processing status - 'idle' when not processing, 'running' when active, 'stopped' when paused\n */\n status: 'idle' | 'running' | 'stopped'\n /**\n * Number of task executions that have completed successfully\n */\n successCount: number\n}\n\nfunction getDefaultAsyncQueuerState<TValue>(): AsyncQueuerState<TValue> {\n return {\n activeItems: [],\n addItemCount: 0,\n errorCount: 0,\n executeCount: 0,\n expirationCount: 0,\n isEmpty: true,\n isExecuting: false,\n isFull: false,\n isIdle: true,\n isRunning: true,\n itemTimestamps: [],\n items: [],\n lastResult: null,\n pendingTick: false,\n rejectionCount: 0,\n settledCount: 0,\n size: 0,\n status: 'idle',\n successCount: 0,\n }\n}\n\nexport interface AsyncQueuerOptions<TValue> {\n /**\n * Options for configuring the underlying async retryer\n */\n asyncRetryerOptions?: AsyncRetryerOptions<(item: TValue) => Promise<any>>\n /**\n * Default position to add items to the queuer\n * @default 'back'\n */\n addItemsTo?: QueuePosition\n /**\n * Maximum number of concurrent tasks to process.\n * Can be a number or a function that returns a number.\n * @default 1\n */\n concurrency?: number | ((queuer: AsyncQueuer<TValue>) => number)\n /**\n * Maximum time in milliseconds that an item can stay in the queue\n * If not provided, items will never expire\n */\n expirationDuration?: number\n /**\n * Function to determine if an item has expired\n * If provided, this overrides the expirationDuration behavior\n */\n getIsExpired?: (item: TValue, addedAt: number) => boolean\n /**\n * Default position to get items from during processing\n * @default 'front'\n */\n getItemsFrom?: QueuePosition\n /**\n * Function to determine priority of items in the queuer\n * Higher priority items will be processed first\n * If not provided, will use static priority values attached to tasks\n */\n getPriority?: (item: TValue) => number\n /**\n * Initial items to populate the queuer with\n */\n initialItems?: Array<TValue>\n /**\n * Initial state for the async queuer\n */\n initialState?: Partial<AsyncQueuerState<TValue>>\n /**\n * Optional key to identify this async queuer instance.\n * If provided, the async queuer will be identified by this key in the devtools and PacerProvider if applicable.\n */\n key?: string\n /**\n * Maximum number of items allowed in the queuer\n */\n maxSize?: number\n /**\n * Optional error handler for when a task throws.\n * If provided, the handler will be called with the error and queuer instance.\n * This can be used alongside throwOnError - the handler will be called before any error is thrown.\n */\n onError?: (error: Error, item: TValue, queuer: AsyncQueuer<TValue>) => void\n /**\n * Callback fired whenever an item expires in the queuer\n */\n onExpire?: (item: TValue, queuer: AsyncQueuer<TValue>) => void\n /**\n * Callback fired whenever an item is added or removed from the queuer\n */\n onItemsChange?: (queuer: AsyncQueuer<TValue>) => void\n /**\n * Callback fired whenever an item is rejected from being added to the queuer\n */\n onReject?: (item: TValue, queuer: AsyncQueuer<TValue>) => void\n /**\n * Optional callback to call when a task is settled\n */\n onSettled?: (item: TValue, queuer: AsyncQueuer<TValue>) => void\n /**\n * Optional callback to call when a task succeeds\n */\n onSuccess?: (result: any, item: TValue, queuer: AsyncQueuer<TValue>) => void\n /**\n * Whether the queuer should start processing tasks immediately or not.\n */\n started?: boolean\n /**\n * Whether to throw errors when they occur.\n * Defaults to true if no onError handler is provided, false if an onError handler is provided.\n * Can be explicitly set to override these defaults.\n */\n throwOnError?: boolean\n /**\n * Time in milliseconds to wait between processing items.\n * Can be a number or a function that returns a number.\n * @default 0\n */\n wait?: number | ((queuer: AsyncQueuer<TValue>) => number)\n}\n\n/**\n * Utility function for sharing common `AsyncQueuerOptions` options between different `AsyncQueuer` instances.\n */\nexport function asyncQueuerOptions<\n TValue = any,\n TOptions extends Partial<AsyncQueuerOptions<TValue>> = Partial<\n AsyncQueuerOptions<TValue>\n >,\n>(options: TOptions): TOptions {\n return options\n}\n\ntype AsyncQueuerOptionsWithOptionalCallbacks = OptionalKeys<\n Required<AsyncQueuerOptions<any>>,\n | 'initialState'\n | 'throwOnError'\n | 'onSuccess'\n | 'onSettled'\n | 'onReject'\n | 'onItemsChange'\n | 'onExpire'\n | 'onError'\n | 'key'\n>\n\nconst defaultOptions: AsyncQueuerOptionsWithOptionalCallbacks = {\n addItemsTo: 'back',\n asyncRetryerOptions: {\n maxAttempts: 1,\n },\n concurrency: 1,\n expirationDuration: Infinity,\n getIsExpired: () => false,\n getItemsFrom: 'front',\n getPriority: (item: any) => item?.priority ?? 0,\n initialItems: [],\n maxSize: Infinity,\n started: true,\n wait: 0,\n}\n\n/**\n * A flexible asynchronous queue for processing tasks with configurable concurrency, priority, and expiration.\n *\n * Async vs Sync Versions:\n * The async version provides advanced features over the sync Queuer:\n * - Returns promises that can be awaited for task results\n * - Built-in retry support via AsyncRetryer integration for each queued task\n * - Abort support to cancel in-flight task executions\n * - Comprehensive error handling with onError callbacks and throwOnError control\n * - Detailed execution tracking (success/error/settle counts)\n * - Concurrent execution support (process multiple items simultaneously)\n *\n * The sync Queuer is lighter weight and simpler when you don't need async features,\n * return values, or execution control.\n *\n * What is Queuing?\n * Queuing is a technique for managing and processing items sequentially or with controlled concurrency.\n * Tasks are processed up to the configured concurrency limit. When a task completes,\n * the next pending task is processed if the concurrency limit allows.\n *\n * Key Features:\n * - Priority queue support via the getPriority option\n * - Configurable concurrency limit\n * - Callbacks for task success, error, completion, and queue state changes\n * - FIFO (First In First Out) or LIFO (Last In First Out) queue behavior\n * - Pause and resume processing\n * - Item expiration to remove stale items from the queue\n *\n * Error Handling:\n * - If an `onError` handler is provided, it will be called with the error and queuer instance\n * - If `throwOnError` is true (default when no onError handler is provided), the error will be thrown\n * - If `throwOnError` is false (default when onError handler is provided), the error will be swallowed\n * - Both onError and throwOnError can be used together; the handler will be called before any error is thrown\n * - The error state can be checked using the AsyncQueuer instance\n *\n * State Management:\n * - Uses TanStack Store for reactive state management\n * - Use `initialState` to provide initial state values when creating the async queuer\n * - Use `onSuccess` callback to react to successful task execution and implement custom logic\n * - Use `onError` callback to react to task execution errors and implement custom error handling\n * - Use `onSettled` callback to react to task execution completion (success or error) and implement custom logic\n * - Use `onItemsChange` callback to react to items being added or removed from the queue\n * - Use `onExpire` callback to react to items expiring and implement custom logic\n * - Use `onReject` callback to react to items being rejected when the queue is full\n * - The state includes error count, expiration count, rejection count, running status, and success/settle counts\n * - State can be accessed via `asyncQueuer.store.state` when using the class directly\n * - When using framework adapters (React/Solid), state is accessed from `asyncQueuer.state`\n *\n * Example usage:\n * ```ts\n * const asyncQueuer = new AsyncQueuer<string>(async (item) => {\n * // process item\n * return item.toUpperCase();\n * }, {\n * concurrency: 2,\n * onSuccess: (result) => {\n * console.log(result);\n * }\n * });\n *\n * asyncQueuer.addItem('hello');\n * asyncQueuer.start();\n * ```\n */\nexport class AsyncQueuer<TValue> {\n readonly store: Store<Readonly<AsyncQueuerState<TValue>>> = new Store<\n AsyncQueuerState<TValue>\n >(getDefaultAsyncQueuerState<TValue>())\n key: string | undefined\n options: AsyncQueuerOptions<TValue>\n asyncRetryers = new Map<\n number,\n AsyncRetryer<(item: TValue) => Promise<any>>\n >()\n #timeoutIds: Set<ReturnType<typeof setTimeout>> = new Set()\n\n constructor(\n public fn: (item: TValue) => Promise<any>,\n initialOptions: AsyncQueuerOptions<TValue> = {},\n ) {\n this.key = initialOptions.key\n this.options = {\n ...defaultOptions,\n ...initialOptions,\n throwOnError: initialOptions.throwOnError ?? !initialOptions.onError,\n }\n const isInitiallyRunning =\n this.options.initialState?.isRunning ?? this.options.started ?? true\n this.#setState({\n ...this.options.initialState,\n isRunning: isInitiallyRunning,\n })\n\n if (this.options.initialState?.items) {\n if (this.store.state.isRunning) {\n this.#tick()\n }\n } else {\n for (let i = 0; i < (this.options.initialItems?.length ?? 0); i++) {\n const item = this.options.initialItems![i]!\n const isLast = i === (this.options.initialItems?.length ?? 0) - 1\n this.addItem(item, this.options.addItemsTo ?? 'back', isLast)\n }\n }\n\n if (this.key) {\n pacerEventClient.on('d-AsyncQueuer', (e) => {\n if (e.payload.key !== this.key) return\n this.#setState(\n e.payload.store.state as Partial<AsyncQueuerState<TValue>>,\n )\n this.setOptions(\n e.payload.options as Partial<AsyncQueuerOptions<TValue>>,\n )\n })\n }\n }\n\n /**\n * Updates the queuer options. New options are merged with existing options.\n */\n setOptions = (newOptions: Partial<AsyncQueuerOptions<TValue>>): void => {\n this.options = { ...this.options, ...newOptions }\n }\n\n #setState = (newState: Partial<AsyncQueuerState<TValue>>): void => {\n this.store.setState((state) => {\n const combinedState = {\n ...state,\n ...newState,\n }\n\n const { activeItems, items, isRunning } = combinedState\n\n const size = items.length\n const isFull = size >= (this.options.maxSize ?? Infinity)\n const isEmpty = size === 0\n const isIdle = isRunning && isEmpty && activeItems.length === 0\n\n const status = isIdle ? 'idle' : isRunning ? 'running' : 'stopped'\n\n return {\n ...combinedState,\n isEmpty,\n isFull,\n isIdle,\n size,\n status,\n }\n })\n emitChange('AsyncQueuer', this)\n }\n\n /**\n * Returns the current wait time (in milliseconds) between processing items.\n * If a function is provided, it is called with the queuer instance.\n */\n #getWait = (): number => {\n return parseFunctionOrValue(this.options.wait ?? 0, this)\n }\n\n /**\n * Returns the current concurrency limit for processing items.\n * If a function is provided, it is called with the queuer instance.\n */\n #getConcurrency = (): number => {\n return parseFunctionOrValue(this.options.concurrency ?? 1, this)\n }\n\n /**\n * Processes items in the queue up to the concurrency limit. Internal use only.\n */\n #tick = () => {\n if (!this.store.state.isRunning) {\n this.#setState({ pendingTick: false })\n return\n }\n this.#setState({ pendingTick: true })\n\n // Check for expired items\n this.#checkExpiredItems()\n\n // Process items concurrently up to the concurrency limit\n const activeItems = this.store.state.activeItems\n while (\n activeItems.length < this.#getConcurrency() &&\n this.store.state.items.length > 0\n ) {\n const nextItem = this.peekNextItem()\n if (!nextItem) {\n break\n }\n activeItems.push(nextItem)\n this.#setState({\n activeItems,\n })\n ;(async () => {\n await this.execute()\n\n const wait = this.#getWait()\n if (wait > 0) {\n const timeoutId = setTimeout(() => this.#tick(), wait)\n this.#timeoutIds.add(timeoutId)\n return\n }\n\n this.#tick()\n })()\n }\n\n this.#setState({ pendingTick: false })\n }\n\n /**\n * Adds an item to the queue. If the queue is full, the item is rejected and onReject is called.\n * Items can be inserted based on priority or at the front/back depending on configuration.\n *\n * @example\n * ```ts\n * queuer.addItem({ value: 'task', priority: 10 });\n * queuer.addItem('task2', 'front');\n * ```\n */\n addItem = (\n item: TValue,\n position: QueuePosition = this.options.addItemsTo ?? 'back',\n runOnItemsChange: boolean = true,\n ): boolean => {\n this.#setState({\n addItemCount: this.store.state.addItemCount + 1,\n })\n\n if (this.store.state.items.length >= (this.options.maxSize ?? Infinity)) {\n this.#setState({\n rejectionCount: this.store.state.rejectionCount + 1,\n })\n this.options.onReject?.(item, this)\n return false\n }\n\n // Get priority either from the function or from getPriority option\n const priority =\n this.options.getPriority !== defaultOptions.getPriority\n ? this.options.getPriority!(item)\n : (item as any).priority\n\n const items = this.store.state.items\n const itemTimestamps = this.store.state.itemTimestamps\n\n if (priority !== undefined) {\n // Insert based on priority - higher priority items go to front\n const insertIndex = items.findIndex((existing) => {\n const existingPriority =\n this.options.getPriority !== defaultOptions.getPriority\n ? this.options.getPriority!(existing)\n : (existing as any).priority\n return existingPriority < priority\n })\n\n if (insertIndex === -1) {\n items.push(item)\n itemTimestamps.push(Date.now())\n } else {\n items.splice(insertIndex, 0, item)\n itemTimestamps.splice(insertIndex, 0, Date.now())\n }\n } else {\n if (position === 'front') {\n // Default FIFO/LIFO behavior\n items.unshift(item)\n itemTimestamps.unshift(Date.now())\n } else {\n // LIFO\n items.push(item)\n itemTimestamps.push(Date.now())\n }\n }\n\n this.#setState({\n items,\n itemTimestamps,\n })\n\n if (runOnItemsChange) {\n this.options.onItemsChange?.(this)\n }\n\n if (this.store.state.isRunning && !this.store.state.pendingTick) {\n this.#tick()\n }\n\n return true\n }\n\n /**\n * Removes and returns the next item from the queue without executing the task function.\n * Use for manual queue management. Normally, use execute() to process items.\n *\n * @example\n * ```ts\n * // FIFO\n * queuer.getNextItem();\n * // LIFO\n * queuer.getNextItem('back');\n * ```\n */\n getNextItem = (\n position: QueuePosition = this.options.getItemsFrom ?? 'front',\n ): TValue | undefined => {\n const { items, itemTimestamps } = this.store.state\n let item: TValue | undefined\n\n // When priority function is provided or position is 'front', always get from front (highest priority)\n // Priority takes precedence over FIFO/LIFO behavior\n if (\n this.options.getPriority !== defaultOptions.getPriority ||\n position === 'front'\n ) {\n item = items[0]\n if (item !== undefined) {\n this.#setState({\n items: items.slice(1),\n itemTimestamps: itemTimestamps.slice(1),\n })\n }\n } else {\n item = items[items.length - 1]\n if (item !== undefined) {\n this.#setState({\n items: items.slice(0, -1),\n itemTimestamps: itemTimestamps.slice(0, -1),\n })\n }\n }\n\n if (item !== undefined) {\n this.options.onItemsChange?.(this)\n }\n\n return item\n }\n\n #getAllItems = (): Array<TValue> => {\n const items = this.peekAllItems()\n this.clear()\n return items\n }\n\n /**\n * Removes and returns the next item from the queue and executes the task function with it.\n *\n * @example\n * ```ts\n * queuer.execute();\n * // LIFO\n * queuer.execute('back');\n * ```\n */\n execute = async (position?: QueuePosition): Promise<any> => {\n const item = this.getNextItem(position)\n\n if (item !== undefined) {\n const currentExecuteCount = this.store.state.executeCount + 1\n this.#setState({\n executeCount: currentExecuteCount,\n isExecuting: true,\n })\n try {\n const currentAsyncRetryer = new AsyncRetryer(this.fn, {\n ...this.options.asyncRetryerOptions,\n key: `${this.key}-retryer-${currentExecuteCount}`,\n })\n this.asyncRetryers.set(currentExecuteCount, currentAsyncRetryer)\n const lastResult = await currentAsyncRetryer.execute(item) // EXECUTE!\n this.#setState({\n successCount: this.store.state.successCount + 1,\n lastResult,\n })\n this.options.onSuccess?.(lastResult, item, this)\n } catch (error) {\n this.#setState({\n errorCount: this.store.state.errorCount + 1,\n })\n this.options.onError?.(error as Error, item, this)\n if (this.options.throwOnError) {\n throw error\n }\n } finally {\n this.asyncRetryers.delete(currentExecuteCount) // dispose retryer\n this.#setState({\n activeItems: this.store.state.activeItems.filter(\n (activeItem) => activeItem !== item,\n ),\n isExecuting: false,\n settledCount: this.store.state.settledCount + 1,\n })\n this.options.onSettled?.(item, this)\n }\n }\n return item\n }\n\n /**\n * Processes a specified number of items to execute immediately with no wait time\n * If no numberOfItems is provided, all items will be processed\n */\n flush = async (\n numberOfItems: number = this.store.state.items.length,\n position?: QueuePosition,\n ): Promise<void> => {\n this.#clearTimeouts() // clear any pending timeouts\n await Promise.all(\n Array.from({ length: numberOfItems }, () => this.execute(position)),\n )\n }\n\n /**\n * Processes all items in the queue as a batch using the provided function as an argument\n * The queue is cleared after processing\n */\n flushAsBatch = async (\n batchFunction: (items: Array<TValue>) => Promise<any>,\n ): Promise<void> => {\n this.#clearTimeouts() // clear any pending timeouts\n const items = this.#getAllItems()\n await batchFunction(items)\n }\n\n /**\n * Checks for expired items in the queue and removes them. Calls onExpire for each expired item.\n * Internal use only.\n */\n #checkExpiredItems = (): void => {\n if (\n (this.options.expirationDuration ?? Infinity) === Infinity &&\n this.options.getIsExpired === defaultOptions.getIsExpired\n ) {\n return\n }\n\n const now = Date.now()\n const expiredIndices: Array<number> = []\n\n // Find indices of expired items\n for (let i = 0; i < this.store.state.items.length; i++) {\n const timestamp = this.store.state.itemTimestamps[i]\n if (timestamp === undefined) continue\n\n const item = this.store.state.items[i]\n if (item === undefined) continue\n\n const isExpired =\n this.options.getIsExpired !== defaultOptions.getIsExpired\n ? this.options.getIsExpired!(item, timestamp)\n : now - timestamp > (this.options.expirationDuration ?? Infinity)\n\n if (isExpired) {\n expiredIndices.push(i)\n }\n }\n\n // Remove expired items from back to front to maintain indices\n for (let i = expiredIndices.length - 1; i >= 0; i--) {\n const index = expiredIndices[i]\n if (index === undefined) continue\n\n const expiredItem = this.store.state.items[index]\n if (expiredItem === undefined) continue\n\n const newItems = [...this.store.state.items]\n const newTimestamps = [...this.store.state.itemTimestamps]\n newItems.splice(index, 1)\n newTimestamps.splice(index, 1)\n this.#setState({\n items: newItems,\n itemTimestamps: newTimestamps,\n expirationCount: this.store.state.expirationCount + 1,\n })\n this.options.onExpire?.(expiredItem, this)\n }\n\n if (expiredIndices.length > 0) {\n this.options.onItemsChange?.(this)\n }\n }\n\n /**\n * Returns the next item in the queue without removing it.\n *\n * @example\n * ```ts\n * queuer.peekNextItem(); // front\n * queuer.peekNextItem('back'); // back\n * ```\n */\n peekNextItem = (position: QueuePosition = 'front'): TValue | undefined => {\n if (position === 'front') {\n return this.store.state.items[0]\n }\n return this.store.state.items[this.store.state.items.length - 1]\n }\n\n /**\n * Returns a copy of all items in the queue, including active and pending items.\n */\n peekAllItems = (): Array<TValue> => {\n return [...this.peekActiveItems(), ...this.peekPendingItems()]\n }\n\n /**\n * Returns the items currently being processed (active tasks).\n */\n peekActiveItems = (): Array<TValue> => {\n return [...this.store.state.activeItems]\n }\n\n /**\n * Returns the items waiting to be processed (pending tasks).\n */\n peekPendingItems = (): Array<TValue> => {\n return [...this.store.state.items]\n }\n\n /**\n * Starts processing items in the queue. If already running, does nothing.\n */\n start = (): void => {\n this.#setState({ isRunning: true })\n if (!this.store.state.pendingTick && this.store.state.items.length > 0) {\n this.#tick()\n }\n }\n\n /**\n * Stops processing items in the queue. Does not clear the queue.\n */\n stop = (): void => {\n this.#clearTimeouts()\n this.#setState({ isRunning: false, pendingTick: false })\n }\n\n #clearTimeouts = (): void => {\n this.#timeoutIds.forEach((timeoutId) => clearTimeout(timeoutId))\n this.#timeoutIds.clear()\n }\n\n /**\n * Removes all pending items from the queue.\n * Does NOT affect active tasks.\n */\n clear = (): void => {\n this.#setState({ items: [], itemTimestamps: [] })\n this.options.onItemsChange?.(this)\n }\n\n /**\n * Returns the AbortSignal for a specific execution.\n * If no executeCount is provided, returns the signal for the most recent execution.\n * Returns null if no execution is found or not currently executing.\n *\n * @param executeCount - Optional specific execution to get signal for\n * @example\n * ```typescript\n * const queuer = new AsyncQueuer(\n * async (item: string) => {\n * const signal = queuer.getAbortSignal()\n * if (signal) {\n * const response = await fetch(`/api/process/${item}`, { signal })\n * return response.json()\n * }\n * },\n * { concurrency: 2 }\n * )\n * ```\n */\n getAbortSignal = (executeCount?: number): AbortSignal | null => {\n const count = executeCount ?? this.store.state.executeCount\n const retryer = this.asyncRetryers.get(count)\n return retryer?.getAbortSignal() ?? null\n }\n\n /**\n * Aborts all ongoing executions with the internal abort controllers.\n * Does NOT clear out the items.\n */\n abort = (): void => {\n this.asyncRetryers.forEach((retryer) => retryer.abort())\n this.asyncRetryers.clear()\n this.#setState({\n isExecuting: false,\n })\n }\n\n /**\n * Resets the queuer state to its default values\n */\n reset = (): void => {\n this.#setState(getDefaultAsyncQueuerState<TValue>())\n this.options.onItemsChange?.(this)\n this.asyncRetryers.forEach((retryer) => retryer.reset())\n }\n}\n\n/**\n * Creates a new AsyncQueuer instance and returns a bound addItem function for adding tasks.\n * The queuer is started automatically and ready to process items.\n *\n * Async vs Sync Versions:\n * The async version provides advanced features over the sync queue function:\n * - Returns promises that can be awaited for task results\n * - Built-in retry support via AsyncRetryer integration for each queued task\n * - Abort support to cancel in-flight task executions\n * - Comprehensive error handling with onError callbacks and throwOnError control\n * - Detailed execution tracking (success/error/settle counts)\n * - Concurrent execution support (process multiple items simultaneously)\n *\n * The sync queue function is lighter weight and simpler when you don't need async features,\n * return values, or execution control.\n *\n * What is Queuing?\n * Queuing is a technique for managing and processing items sequentially or with controlled concurrency.\n * Tasks are processed up to the configured concurrency limit. When a task completes,\n * the next pending task is processed if the concurrency limit allows.\n *\n * Configuration Options:\n * - `concurrency`: Maximum number of concurrent tasks (default: 1)\n * - `wait`: Time to wait between processing items (default: 0)\n * - `maxSize`: Maximum number of items allowed in the queue (default: Infinity)\n * - `getPriority`: Function to determine item priority\n * - `addItemsTo`: Default position to add items ('back' or 'front', default: 'back')\n * - `getItemsFrom`: Default position to get items ('front' or 'back', default: 'front')\n * - `expirationDuration`: Maximum time items can stay in queue\n * - `started`: Whether to start processing immediately (default: true)\n * - `asyncRetryerOptions`: Configure retry behavior for task executions\n *\n * Error Handling:\n * - If an `onError` handler is provided, it will be called with the error and queuer instance\n * - If `throwOnError` is true (default when no onError handler is provided), the error will be thrown\n * - If `throwOnError` is false (default when onError handler is provided), the error will be swallowed\n * - Both onError and throwOnError can be used together; the handler will be called before any error is thrown\n * - The error state can be checked using the underlying AsyncQueuer instance\n *\n * State Management:\n * - Uses TanStack Store for reactive state management\n * - Use `initialState` to provide initial state values when creating the async queuer\n * - Use `onSuccess` callback to react to successful task execution and implement custom logic\n * - Use `onError` callback to react to task execution errors and implement custom error handling\n * - Use `onSettled` callback to react to task execution completion (success or error) and implement custom logic\n * - Use `onItemsChange` callback to react to items being added or removed from the queue\n * - Use `onExpire` callback to react to items expiring and implement custom logic\n * - Use `onReject` callback to react to items being rejected when the queue is full\n * - The state includes error count, expiration count, rejection count, running status, and success/settle counts\n * - State can be accessed via the underlying AsyncQueuer instance's `store.state` property\n * - When using framework adapters (React/Solid), state is accessed from the hook's state property\n *\n * @example\n * ```ts\n * const enqueue = asyncQueue<string>(async (item) => {\n * return item.toUpperCase();\n * }, {\n * concurrency: 2,\n * wait: 100,\n * onSuccess: (result) => console.log('Processed:', result)\n * });\n *\n * enqueue('hello');\n * ```\n */\nexport function asyncQueue<TValue>(\n fn: (value: TValue) => Promise<any>,\n initialOptions: AsyncQueuerOptions<TValue>,\n) {\n const asyncQueuer = new AsyncQueuer<TValue>(fn, initialOptions)\n return asyncQueuer.addItem\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"async-queuer.cjs","names":["Store","parseFunctionOrValue","#setState","#checkExpiredItems","#getConcurrency","#getWait","#timeoutIds","#tick","AsyncRetryer","#clearTimeouts","#getAllItems"],"sources":["../src/async-queuer.ts"],"sourcesContent":["import { Store } from '@tanstack/store'\nimport { AsyncRetryer } from './async-retryer'\nimport { parseFunctionOrValue } from './utils'\nimport { emitChange, pacerEventClient } from './event-client'\nimport type { AsyncRetryerOptions } from './async-retryer'\nimport type { OptionalKeys } from './types'\nimport type { QueuePosition } from './queuer'\n\nexport interface AsyncQueuerState<TValue> {\n /**\n * Items currently being processed by the queuer\n */\n activeItems: Array<TValue>\n /**\n * Number of times addItem has been called (for reduction calculations)\n */\n addItemCount: number\n /**\n * Number of task executions that have resulted in errors\n */\n errorCount: number\n /**\n * Number of times execute has been called\n */\n executeCount: number\n /**\n * Number of items that have been removed from the queue due to expiration\n */\n expirationCount: number\n /**\n * Whether the queuer has no items to process (items array is empty)\n */\n isEmpty: boolean\n /**\n * Whether the queuer is currently executing\n */\n isExecuting: boolean\n /**\n * Whether the queuer has reached its maximum capacity\n */\n isFull: boolean\n /**\n * Whether the queuer is not currently processing any items\n */\n isIdle: boolean\n /**\n * Whether the queuer is active and will process items automatically\n */\n isRunning: boolean\n /**\n * Array of items currently waiting to be processed\n */\n items: Array<TValue>\n /**\n * Timestamps when items were added to the queue for expiration tracking\n */\n itemTimestamps: Array<number>\n /**\n * The result from the most recent task execution\n */\n lastResult: any\n /**\n * Whether the queuer has a pending timeout for processing the next item\n */\n pendingTick: boolean\n /**\n * Number of items that have been rejected from being added to the queue\n */\n rejectionCount: number\n /**\n * Number of task executions that have completed (either successfully or with errors)\n */\n settledCount: number\n /**\n * Number of items currently in the queue\n */\n size: number\n /**\n * Current processing status - 'idle' when not processing, 'running' when active, 'stopped' when paused\n */\n status: 'idle' | 'running' | 'stopped'\n /**\n * Number of task executions that have completed successfully\n */\n successCount: number\n}\n\nfunction getDefaultAsyncQueuerState<TValue>(): AsyncQueuerState<TValue> {\n return {\n activeItems: [],\n addItemCount: 0,\n errorCount: 0,\n executeCount: 0,\n expirationCount: 0,\n isEmpty: true,\n isExecuting: false,\n isFull: false,\n isIdle: true,\n isRunning: true,\n itemTimestamps: [],\n items: [],\n lastResult: null,\n pendingTick: false,\n rejectionCount: 0,\n settledCount: 0,\n size: 0,\n status: 'idle',\n successCount: 0,\n }\n}\n\nexport interface AsyncQueuerOptions<TValue> {\n /**\n * Options for configuring the underlying async retryer\n */\n asyncRetryerOptions?: AsyncRetryerOptions<(item: TValue) => Promise<any>>\n /**\n * Default position to add items to the queuer\n * @default 'back'\n */\n addItemsTo?: QueuePosition\n /**\n * Maximum number of concurrent tasks to process.\n * Can be a number or a function that returns a number.\n * @default 1\n */\n concurrency?: number | ((queuer: AsyncQueuer<TValue>) => number)\n /**\n * Maximum time in milliseconds that an item can stay in the queue\n * If not provided, items will never expire\n */\n expirationDuration?: number\n /**\n * Function to determine if an item has expired\n * If provided, this overrides the expirationDuration behavior\n */\n getIsExpired?: (item: TValue, addedAt: number) => boolean\n /**\n * Default position to get items from during processing\n * @default 'front'\n */\n getItemsFrom?: QueuePosition\n /**\n * Function to determine priority of items in the queuer\n * Higher priority items will be processed first\n * If not provided, will use static priority values attached to tasks\n */\n getPriority?: (item: TValue) => number\n /**\n * Initial items to populate the queuer with\n */\n initialItems?: Array<TValue>\n /**\n * Initial state for the async queuer\n */\n initialState?: Partial<AsyncQueuerState<TValue>>\n /**\n * Optional key to identify this async queuer instance.\n * If provided, the async queuer will be identified by this key in the devtools and PacerProvider if applicable.\n */\n key?: string\n /**\n * Maximum number of items allowed in the queuer\n */\n maxSize?: number\n /**\n * Optional error handler for when a task throws.\n * If provided, the handler will be called with the error and queuer instance.\n * This can be used alongside throwOnError - the handler will be called before any error is thrown.\n */\n onError?: (error: Error, item: TValue, queuer: AsyncQueuer<TValue>) => void\n /**\n * Callback fired whenever an item expires in the queuer\n */\n onExpire?: (item: TValue, queuer: AsyncQueuer<TValue>) => void\n /**\n * Callback fired whenever an item is added or removed from the queuer\n */\n onItemsChange?: (queuer: AsyncQueuer<TValue>) => void\n /**\n * Callback fired whenever an item is rejected from being added to the queuer\n */\n onReject?: (item: TValue, queuer: AsyncQueuer<TValue>) => void\n /**\n * Optional callback to call when a task is settled\n */\n onSettled?: (item: TValue, queuer: AsyncQueuer<TValue>) => void\n /**\n * Optional callback to call when a task succeeds\n */\n onSuccess?: (result: any, item: TValue, queuer: AsyncQueuer<TValue>) => void\n /**\n * Whether the queuer should start processing tasks immediately or not.\n */\n started?: boolean\n /**\n * Whether to throw errors when they occur.\n * Defaults to true if no onError handler is provided, false if an onError handler is provided.\n * Can be explicitly set to override these defaults.\n */\n throwOnError?: boolean\n /**\n * Time in milliseconds to wait between processing items.\n * Can be a number or a function that returns a number.\n * @default 0\n */\n wait?: number | ((queuer: AsyncQueuer<TValue>) => number)\n}\n\n/**\n * Utility function for sharing common `AsyncQueuerOptions` options between different `AsyncQueuer` instances.\n */\nexport function asyncQueuerOptions<\n TValue = any,\n TOptions extends Partial<AsyncQueuerOptions<TValue>> = Partial<\n AsyncQueuerOptions<TValue>\n >,\n>(options: TOptions): TOptions {\n return options\n}\n\ntype AsyncQueuerOptionsWithOptionalCallbacks = OptionalKeys<\n Required<AsyncQueuerOptions<any>>,\n | 'initialState'\n | 'throwOnError'\n | 'onSuccess'\n | 'onSettled'\n | 'onReject'\n | 'onItemsChange'\n | 'onExpire'\n | 'onError'\n | 'key'\n>\n\nconst defaultOptions: AsyncQueuerOptionsWithOptionalCallbacks = {\n addItemsTo: 'back',\n asyncRetryerOptions: {\n maxAttempts: 1,\n },\n concurrency: 1,\n expirationDuration: Infinity,\n getIsExpired: () => false,\n getItemsFrom: 'front',\n getPriority: (item: any) => item?.priority ?? 0,\n initialItems: [],\n maxSize: Infinity,\n started: true,\n wait: 0,\n}\n\n/**\n * A flexible asynchronous queue for processing tasks with configurable concurrency, priority, and expiration.\n *\n * Async vs Sync Versions:\n * The async version provides advanced features over the sync Queuer:\n * - Returns promises that can be awaited for task results\n * - Built-in retry support via AsyncRetryer integration for each queued task\n * - Abort support to cancel in-flight task executions\n * - Comprehensive error handling with onError callbacks and throwOnError control\n * - Detailed execution tracking (success/error/settle counts)\n * - Concurrent execution support (process multiple items simultaneously)\n *\n * The sync Queuer is lighter weight and simpler when you don't need async features,\n * return values, or execution control.\n *\n * What is Queuing?\n * Queuing is a technique for managing and processing items sequentially or with controlled concurrency.\n * Tasks are processed up to the configured concurrency limit. When a task completes,\n * the next pending task is processed if the concurrency limit allows.\n *\n * Key Features:\n * - Priority queue support via the getPriority option\n * - Configurable concurrency limit\n * - Callbacks for task success, error, completion, and queue state changes\n * - FIFO (First In First Out) or LIFO (Last In First Out) queue behavior\n * - Pause and resume processing\n * - Item expiration to remove stale items from the queue\n *\n * Error Handling:\n * - If an `onError` handler is provided, it will be called with the error and queuer instance\n * - If `throwOnError` is true (default when no onError handler is provided), the error will be thrown\n * - If `throwOnError` is false (default when onError handler is provided), the error will be swallowed\n * - Both onError and throwOnError can be used together; the handler will be called before any error is thrown\n * - The error state can be checked using the AsyncQueuer instance\n *\n * State Management:\n * - Uses TanStack Store for reactive state management\n * - Use `initialState` to provide initial state values when creating the async queuer\n * - Use `onSuccess` callback to react to successful task execution and implement custom logic\n * - Use `onError` callback to react to task execution errors and implement custom error handling\n * - Use `onSettled` callback to react to task execution completion (success or error) and implement custom logic\n * - Use `onItemsChange` callback to react to items being added or removed from the queue\n * - Use `onExpire` callback to react to items expiring and implement custom logic\n * - Use `onReject` callback to react to items being rejected when the queue is full\n * - The state includes error count, expiration count, rejection count, running status, and success/settle counts\n * - State can be accessed via `asyncQueuer.store.state` when using the class directly\n * - When using framework adapters (React/Solid), state is accessed from `asyncQueuer.state`\n *\n * Example usage:\n * ```ts\n * const asyncQueuer = new AsyncQueuer<string>(async (item) => {\n * // process item\n * return item.toUpperCase();\n * }, {\n * concurrency: 2,\n * onSuccess: (result) => {\n * console.log(result);\n * }\n * });\n *\n * asyncQueuer.addItem('hello');\n * asyncQueuer.start();\n * ```\n */\nexport class AsyncQueuer<TValue> {\n readonly store: Store<Readonly<AsyncQueuerState<TValue>>> = new Store<\n AsyncQueuerState<TValue>\n >(getDefaultAsyncQueuerState<TValue>())\n key: string | undefined\n options: AsyncQueuerOptions<TValue>\n asyncRetryers = new Map<\n number,\n AsyncRetryer<(item: TValue) => Promise<any>>\n >()\n #timeoutIds: Set<ReturnType<typeof setTimeout>> = new Set()\n\n constructor(\n public fn: (item: TValue) => Promise<any>,\n initialOptions: AsyncQueuerOptions<TValue> = {},\n ) {\n this.key = initialOptions.key\n this.options = {\n ...defaultOptions,\n ...initialOptions,\n throwOnError: initialOptions.throwOnError ?? !initialOptions.onError,\n }\n const isInitiallyRunning =\n this.options.initialState?.isRunning ?? this.options.started ?? true\n this.#setState({\n ...this.options.initialState,\n isRunning: isInitiallyRunning,\n })\n\n if (this.options.initialState?.items) {\n if (this.store.state.isRunning) {\n this.#tick()\n }\n } else {\n for (let i = 0; i < (this.options.initialItems?.length ?? 0); i++) {\n const item = this.options.initialItems![i]!\n const isLast = i === (this.options.initialItems?.length ?? 0) - 1\n this.addItem(item, this.options.addItemsTo ?? 'back', isLast)\n }\n }\n\n if (this.key) {\n pacerEventClient.on('d-AsyncQueuer', (e) => {\n if (e.payload.key !== this.key) return\n this.#setState(\n e.payload.store.state as Partial<AsyncQueuerState<TValue>>,\n )\n this.setOptions(\n e.payload.options as Partial<AsyncQueuerOptions<TValue>>,\n )\n })\n }\n }\n\n /**\n * Updates the queuer options. New options are merged with existing options.\n */\n setOptions = (newOptions: Partial<AsyncQueuerOptions<TValue>>): void => {\n this.options = { ...this.options, ...newOptions }\n }\n\n #setState = (newState: Partial<AsyncQueuerState<TValue>>): void => {\n this.store.setState((state) => {\n const combinedState = {\n ...state,\n ...newState,\n }\n\n const { activeItems, items, isRunning } = combinedState\n\n const size = items.length\n const isFull = size >= (this.options.maxSize ?? Infinity)\n const isEmpty = size === 0\n const isIdle = isRunning && isEmpty && activeItems.length === 0\n\n const status = isIdle ? 'idle' : isRunning ? 'running' : 'stopped'\n\n return {\n ...combinedState,\n isEmpty,\n isFull,\n isIdle,\n size,\n status,\n }\n })\n emitChange('AsyncQueuer', this)\n }\n\n /**\n * Returns the current wait time (in milliseconds) between processing items.\n * If a function is provided, it is called with the queuer instance.\n */\n #getWait = (): number => {\n return parseFunctionOrValue(this.options.wait ?? 0, this)\n }\n\n /**\n * Returns the current concurrency limit for processing items.\n * If a function is provided, it is called with the queuer instance.\n */\n #getConcurrency = (): number => {\n return parseFunctionOrValue(this.options.concurrency ?? 1, this)\n }\n\n /**\n * Processes items in the queue up to the concurrency limit. Internal use only.\n */\n #tick = () => {\n if (!this.store.state.isRunning) {\n this.#setState({ pendingTick: false })\n return\n }\n this.#setState({ pendingTick: true })\n\n // Check for expired items\n this.#checkExpiredItems()\n\n // Process items concurrently up to the concurrency limit\n let scheduledAsyncWork = false\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 === undefined) {\n break\n }\n activeItems.push(nextItem)\n this.#setState({\n activeItems: [...activeItems],\n })\n scheduledAsyncWork = true\n ;(async () => {\n try {\n await this.execute()\n } catch {\n // errors are already surfaced via onError/errorCount (and rethrown to\n // direct execute/flush callers); swallowing here prevents unhandled\n // rejections and keeps the processing chain alive\n }\n\n const wait = this.#getWait()\n if (wait > 0) {\n const timeoutId = setTimeout(() => {\n this.#timeoutIds.delete(timeoutId)\n this.#tick()\n }, wait)\n this.#timeoutIds.add(timeoutId)\n return\n }\n\n this.#tick()\n })()\n }\n\n // pendingTick must stay true while executions or wait timers are pending so\n // that addItem does not trigger an extra tick that bypasses the wait period\n if (!scheduledAsyncWork) {\n this.#setState({ pendingTick: false })\n }\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 * `undefined` cannot be queued (it is the internal \"no item\" sentinel) and is always rejected.\n *\n * @example\n * ```ts\n * queuer.addItem({ value: 'task', priority: 10 });\n * queuer.addItem('task2', 'front');\n * ```\n */\n addItem = (\n item: TValue,\n position: QueuePosition = this.options.addItemsTo ?? 'back',\n runOnItemsChange: boolean = true,\n ): boolean => {\n this.#setState({\n addItemCount: this.store.state.addItemCount + 1,\n })\n\n // undefined is the internal \"no item\" sentinel (peekNextItem/getNextItem);\n // queuing it would wedge the processing loop and block items behind it\n if (item === undefined) {\n this.#setState({\n rejectionCount: this.store.state.rejectionCount + 1,\n })\n this.options.onReject?.(item, this)\n return false\n }\n\n if (this.store.state.items.length >= (this.options.maxSize ?? Infinity)) {\n this.#setState({\n rejectionCount: this.store.state.rejectionCount + 1,\n })\n this.options.onReject?.(item, this)\n return false\n }\n\n // Get priority either from the function or from getPriority option\n const priority =\n this.options.getPriority !== defaultOptions.getPriority\n ? this.options.getPriority!(item)\n : (item as any)?.priority\n\n const items = this.store.state.items\n const itemTimestamps = this.store.state.itemTimestamps\n\n if (priority !== undefined) {\n // Insert based on priority - higher priority items go to front\n const insertIndex = items.findIndex((existing) => {\n const existingPriority =\n this.options.getPriority !== defaultOptions.getPriority\n ? this.options.getPriority!(existing)\n : (existing as any)?.priority\n return existingPriority < priority\n })\n\n if (insertIndex === -1) {\n items.push(item)\n itemTimestamps.push(Date.now())\n } else {\n items.splice(insertIndex, 0, item)\n itemTimestamps.splice(insertIndex, 0, Date.now())\n }\n } else {\n if (position === 'front') {\n // Default FIFO/LIFO behavior\n items.unshift(item)\n itemTimestamps.unshift(Date.now())\n } else {\n // LIFO\n items.push(item)\n itemTimestamps.push(Date.now())\n }\n }\n\n this.#setState({\n items,\n itemTimestamps,\n })\n\n if (runOnItemsChange) {\n this.options.onItemsChange?.(this)\n }\n\n if (this.store.state.isRunning && !this.store.state.pendingTick) {\n this.#tick()\n }\n\n return true\n }\n\n /**\n * Removes and returns the next item from the queue without executing the task function.\n * Use for manual queue management. Normally, use execute() to process items.\n *\n * @example\n * ```ts\n * // FIFO\n * queuer.getNextItem();\n * // LIFO\n * queuer.getNextItem('back');\n * ```\n */\n getNextItem = (\n position: QueuePosition = this.options.getItemsFrom ?? 'front',\n ): TValue | undefined => {\n const { items, itemTimestamps } = this.store.state\n let item: TValue | undefined\n\n // When priority function is provided or position is 'front', always get from front (highest priority)\n // Priority takes precedence over FIFO/LIFO behavior\n if (\n this.options.getPriority !== defaultOptions.getPriority ||\n position === 'front'\n ) {\n item = items[0]\n if (item !== undefined) {\n this.#setState({\n items: items.slice(1),\n itemTimestamps: itemTimestamps.slice(1),\n })\n }\n } else {\n item = items[items.length - 1]\n if (item !== undefined) {\n this.#setState({\n items: items.slice(0, -1),\n itemTimestamps: itemTimestamps.slice(0, -1),\n })\n }\n }\n\n if (item !== undefined) {\n this.options.onItemsChange?.(this)\n }\n\n return item\n }\n\n #getAllItems = (): Array<TValue> => {\n const items = this.peekAllItems()\n this.clear()\n return items\n }\n\n /**\n * Removes and returns the next item from the queue and executes the task function with it.\n *\n * @example\n * ```ts\n * queuer.execute();\n * // LIFO\n * queuer.execute('back');\n * ```\n */\n execute = async (position?: QueuePosition): Promise<any> => {\n const item = this.getNextItem(position)\n\n if (item !== undefined) {\n const currentExecuteCount = this.store.state.executeCount + 1\n this.#setState({\n executeCount: currentExecuteCount,\n isExecuting: true,\n })\n try {\n const currentAsyncRetryer = new AsyncRetryer(\n this.fn,\n this.options.asyncRetryerOptions,\n )\n this.asyncRetryers.set(currentExecuteCount, currentAsyncRetryer)\n const lastResult = await currentAsyncRetryer.execute(item) // EXECUTE!\n this.#setState({\n successCount: this.store.state.successCount + 1,\n lastResult,\n })\n this.options.onSuccess?.(lastResult, item, this)\n } catch (error) {\n this.#setState({\n errorCount: this.store.state.errorCount + 1,\n })\n this.options.onError?.(error as Error, item, this)\n if (this.options.throwOnError) {\n throw error\n }\n } finally {\n this.asyncRetryers.delete(currentExecuteCount) // dispose retryer\n // remove only one occurrence so duplicate item values keep accurate\n // concurrency accounting\n const remainingActiveItems = [...this.store.state.activeItems]\n const activeItemIndex = remainingActiveItems.indexOf(item)\n if (activeItemIndex !== -1) {\n remainingActiveItems.splice(activeItemIndex, 1)\n }\n this.#setState({\n activeItems: remainingActiveItems,\n // other executions may still be in flight (concurrency > 1 or flush)\n isExecuting: this.asyncRetryers.size > 0,\n settledCount: this.store.state.settledCount + 1,\n })\n this.options.onSettled?.(item, this)\n }\n }\n return item\n }\n\n /**\n * Processes a specified number of items to execute immediately with no wait time\n * If no numberOfItems is provided, all items will be processed\n */\n flush = async (\n numberOfItems: number = this.store.state.items.length,\n position?: QueuePosition,\n ): Promise<void> => {\n this.#clearTimeouts() // clear any pending timeouts (kills the tick chain)\n const results = await Promise.allSettled(\n Array.from({ length: numberOfItems }, () => this.execute(position)),\n )\n // the tick chain was killed above; restart it so remaining and future items\n // process even when a flushed task rejected. Remaining items resume with the\n // normal wait spacing after the flushed executions.\n this.#setState({ pendingTick: false })\n if (this.store.state.isRunning && this.store.state.items.length > 0) {\n const wait = this.#getWait()\n if (wait > 0) {\n this.#setState({ pendingTick: true })\n const timeoutId = setTimeout(() => {\n this.#timeoutIds.delete(timeoutId)\n this.#tick()\n }, wait)\n this.#timeoutIds.add(timeoutId)\n } else {\n this.#tick()\n }\n }\n const failure = results.find(\n (result): result is PromiseRejectedResult => result.status === 'rejected',\n )\n if (failure) {\n throw failure.reason\n }\n }\n\n /**\n * Processes all items in the queue as a batch using the provided function as an argument\n * The queue is cleared after processing\n */\n flushAsBatch = async (\n batchFunction: (items: Array<TValue>) => Promise<any>,\n ): Promise<void> => {\n this.#clearTimeouts() // clear any pending timeouts (kills the tick chain)\n const items = this.#getAllItems()\n try {\n await batchFunction(items)\n } finally {\n // restore the tick chain even when the batch function rejects\n this.#setState({ pendingTick: false })\n if (this.store.state.isRunning && this.store.state.items.length > 0) {\n this.#tick()\n }\n }\n }\n\n /**\n * Checks for expired items in the queue and removes them. Calls onExpire for each expired item.\n * Internal use only.\n */\n #checkExpiredItems = (): void => {\n if (\n (this.options.expirationDuration ?? Infinity) === Infinity &&\n this.options.getIsExpired === defaultOptions.getIsExpired\n ) {\n return\n }\n\n const now = Date.now()\n const expiredIndices: Array<number> = []\n\n // Find indices of expired items\n for (let i = 0; i < this.store.state.items.length; i++) {\n const timestamp = this.store.state.itemTimestamps[i]\n if (timestamp === undefined) continue\n\n const item = this.store.state.items[i]\n if (item === undefined) continue\n\n const isExpired =\n this.options.getIsExpired !== defaultOptions.getIsExpired\n ? this.options.getIsExpired!(item, timestamp)\n : now - timestamp > (this.options.expirationDuration ?? Infinity)\n\n if (isExpired) {\n expiredIndices.push(i)\n }\n }\n\n // Remove expired items from back to front to maintain indices\n for (let i = expiredIndices.length - 1; i >= 0; i--) {\n const index = expiredIndices[i]\n if (index === undefined) continue\n\n const expiredItem = this.store.state.items[index]\n if (expiredItem === undefined) continue\n\n const newItems = [...this.store.state.items]\n const newTimestamps = [...this.store.state.itemTimestamps]\n newItems.splice(index, 1)\n newTimestamps.splice(index, 1)\n this.#setState({\n items: newItems,\n itemTimestamps: newTimestamps,\n expirationCount: this.store.state.expirationCount + 1,\n })\n this.options.onExpire?.(expiredItem, this)\n }\n\n if (expiredIndices.length > 0) {\n this.options.onItemsChange?.(this)\n }\n }\n\n /**\n * Returns the next item in the queue without removing it.\n *\n * @example\n * ```ts\n * queuer.peekNextItem(); // front\n * queuer.peekNextItem('back'); // back\n * ```\n */\n peekNextItem = (position: QueuePosition = 'front'): TValue | undefined => {\n if (position === 'front') {\n return this.store.state.items[0]\n }\n return this.store.state.items[this.store.state.items.length - 1]\n }\n\n /**\n * Returns a copy of all items in the queue, including active and pending items.\n */\n peekAllItems = (): Array<TValue> => {\n return [...this.peekActiveItems(), ...this.peekPendingItems()]\n }\n\n /**\n * Returns the items currently being processed (active tasks).\n */\n peekActiveItems = (): Array<TValue> => {\n return [...this.store.state.activeItems]\n }\n\n /**\n * Returns the items waiting to be processed (pending tasks).\n */\n peekPendingItems = (): Array<TValue> => {\n return [...this.store.state.items]\n }\n\n /**\n * Starts processing items in the queue. If already running, does nothing.\n */\n start = (): void => {\n this.#setState({ isRunning: true })\n if (!this.store.state.pendingTick && this.store.state.items.length > 0) {\n this.#tick()\n }\n }\n\n /**\n * Stops processing items in the queue. Does not clear the queue.\n */\n stop = (): void => {\n this.#clearTimeouts()\n this.#setState({ isRunning: false, pendingTick: false })\n }\n\n #clearTimeouts = (): void => {\n this.#timeoutIds.forEach((timeoutId) => clearTimeout(timeoutId))\n this.#timeoutIds.clear()\n }\n\n /**\n * Removes all pending items from the queue.\n * Does NOT affect active tasks.\n */\n clear = (): void => {\n this.#setState({ items: [], itemTimestamps: [] })\n this.options.onItemsChange?.(this)\n }\n\n /**\n * Returns the AbortSignal for a specific execution.\n * If no executeCount is provided, returns the signal for the most recent execution.\n * Returns null if no execution is found or not currently executing.\n *\n * @param executeCount - Optional specific execution to get signal for\n * @example\n * ```typescript\n * const queuer = new AsyncQueuer(\n * async (item: string) => {\n * const signal = queuer.getAbortSignal()\n * if (signal) {\n * const response = await fetch(`/api/process/${item}`, { signal })\n * return response.json()\n * }\n * },\n * { concurrency: 2 }\n * )\n * ```\n */\n getAbortSignal = (executeCount?: number): AbortSignal | null => {\n const count = executeCount ?? this.store.state.executeCount\n const retryer = this.asyncRetryers.get(count)\n return retryer?.getAbortSignal() ?? null\n }\n\n /**\n * Aborts all ongoing executions with the internal abort controllers.\n * Does NOT clear out the items.\n */\n abort = (): void => {\n this.asyncRetryers.forEach((retryer) => retryer.abort())\n this.asyncRetryers.clear()\n this.#setState({\n isExecuting: false,\n })\n }\n\n /**\n * Resets the queuer state to its default values\n */\n reset = (): void => {\n this.#setState(getDefaultAsyncQueuerState<TValue>())\n this.options.onItemsChange?.(this)\n this.asyncRetryers.forEach((retryer) => retryer.reset())\n }\n}\n\n/**\n * Creates a new AsyncQueuer instance and returns a bound addItem function for adding tasks.\n * The queuer is started automatically and ready to process items.\n *\n * Async vs Sync Versions:\n * The async version provides advanced features over the sync queue function:\n * - Returns promises that can be awaited for task results\n * - Built-in retry support via AsyncRetryer integration for each queued task\n * - Abort support to cancel in-flight task executions\n * - Comprehensive error handling with onError callbacks and throwOnError control\n * - Detailed execution tracking (success/error/settle counts)\n * - Concurrent execution support (process multiple items simultaneously)\n *\n * The sync queue function is lighter weight and simpler when you don't need async features,\n * return values, or execution control.\n *\n * What is Queuing?\n * Queuing is a technique for managing and processing items sequentially or with controlled concurrency.\n * Tasks are processed up to the configured concurrency limit. When a task completes,\n * the next pending task is processed if the concurrency limit allows.\n *\n * Configuration Options:\n * - `concurrency`: Maximum number of concurrent tasks (default: 1)\n * - `wait`: Time to wait between processing items (default: 0)\n * - `maxSize`: Maximum number of items allowed in the queue (default: Infinity)\n * - `getPriority`: Function to determine item priority\n * - `addItemsTo`: Default position to add items ('back' or 'front', default: 'back')\n * - `getItemsFrom`: Default position to get items ('front' or 'back', default: 'front')\n * - `expirationDuration`: Maximum time items can stay in queue\n * - `started`: Whether to start processing immediately (default: true)\n * - `asyncRetryerOptions`: Configure retry behavior for task executions\n *\n * Error Handling:\n * - If an `onError` handler is provided, it will be called with the error and queuer instance\n * - If `throwOnError` is true (default when no onError handler is provided), the error will be thrown\n * - If `throwOnError` is false (default when onError handler is provided), the error will be swallowed\n * - Both onError and throwOnError can be used together; the handler will be called before any error is thrown\n * - The error state can be checked using the underlying AsyncQueuer instance\n *\n * State Management:\n * - Uses TanStack Store for reactive state management\n * - Use `initialState` to provide initial state values when creating the async queuer\n * - Use `onSuccess` callback to react to successful task execution and implement custom logic\n * - Use `onError` callback to react to task execution errors and implement custom error handling\n * - Use `onSettled` callback to react to task execution completion (success or error) and implement custom logic\n * - Use `onItemsChange` callback to react to items being added or removed from the queue\n * - Use `onExpire` callback to react to items expiring and implement custom logic\n * - Use `onReject` callback to react to items being rejected when the queue is full\n * - The state includes error count, expiration count, rejection count, running status, and success/settle counts\n * - State can be accessed via the underlying AsyncQueuer instance's `store.state` property\n * - When using framework adapters (React/Solid), state is accessed from the hook's state property\n *\n * @example\n * ```ts\n * const enqueue = asyncQueue<string>(async (item) => {\n * return item.toUpperCase();\n * }, {\n * concurrency: 2,\n * wait: 100,\n * onSuccess: (result) => console.log('Processed:', result)\n * });\n *\n * enqueue('hello');\n * ```\n */\nexport function asyncQueue<TValue>(\n fn: (value: TValue) => Promise<any>,\n initialOptions: AsyncQueuerOptions<TValue>,\n) {\n const asyncQueuer = new AsyncQueuer<TValue>(fn, initialOptions)\n return asyncQueuer.addItem\n}\n"],"mappings":"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"}
|
package/dist/async-queuer.d.cts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { AsyncRetryer, AsyncRetryerOptions } from "./async-retryer.cjs";
|
|
2
2
|
import { QueuePosition } from "./queuer.cjs";
|
|
3
3
|
import { Store } from "@tanstack/store";
|
|
4
|
-
|
|
5
4
|
//#region src/async-queuer.d.ts
|
|
6
5
|
interface AsyncQueuerState<TValue> {
|
|
7
6
|
/**
|
|
@@ -261,6 +260,7 @@ declare class AsyncQueuer<TValue> {
|
|
|
261
260
|
/**
|
|
262
261
|
* Adds an item to the queue. If the queue is full, the item is rejected and onReject is called.
|
|
263
262
|
* Items can be inserted based on priority or at the front/back depending on configuration.
|
|
263
|
+
* `undefined` cannot be queued (it is the internal "no item" sentinel) and is always rejected.
|
|
264
264
|
*
|
|
265
265
|
* @example
|
|
266
266
|
* ```ts
|
package/dist/async-queuer.d.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { AsyncRetryer, AsyncRetryerOptions } from "./async-retryer.js";
|
|
2
2
|
import { QueuePosition } from "./queuer.js";
|
|
3
3
|
import { Store } from "@tanstack/store";
|
|
4
|
-
|
|
5
4
|
//#region src/async-queuer.d.ts
|
|
6
5
|
interface AsyncQueuerState<TValue> {
|
|
7
6
|
/**
|
|
@@ -261,6 +260,7 @@ declare class AsyncQueuer<TValue> {
|
|
|
261
260
|
/**
|
|
262
261
|
* Adds an item to the queue. If the queue is full, the item is rejected and onReject is called.
|
|
263
262
|
* Items can be inserted based on priority or at the front/back depending on configuration.
|
|
263
|
+
* `undefined` cannot be queued (it is the internal "no item" sentinel) and is always rejected.
|
|
264
264
|
*
|
|
265
265
|
* @example
|
|
266
266
|
* ```ts
|
package/dist/async-queuer.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { parseFunctionOrValue } from "./utils.js";
|
|
2
|
-
import { emitChange, pacerEventClient } from "./event-client.js";
|
|
3
2
|
import { AsyncRetryer } from "./async-retryer.js";
|
|
3
|
+
import { emitChange, pacerEventClient } from "./event-client.js";
|
|
4
4
|
import { Store } from "@tanstack/store";
|
|
5
5
|
|
|
6
6
|
//#region src/async-queuer.ts
|
|
@@ -159,38 +159,50 @@ var AsyncQueuer = class {
|
|
|
159
159
|
}
|
|
160
160
|
this.#setState({ pendingTick: true });
|
|
161
161
|
this.#checkExpiredItems();
|
|
162
|
-
|
|
162
|
+
let scheduledAsyncWork = false;
|
|
163
|
+
const activeItems = [...this.store.state.activeItems];
|
|
163
164
|
while (activeItems.length < this.#getConcurrency() && this.store.state.items.length > 0) {
|
|
164
165
|
const nextItem = this.peekNextItem();
|
|
165
|
-
if (
|
|
166
|
+
if (nextItem === void 0) break;
|
|
166
167
|
activeItems.push(nextItem);
|
|
167
|
-
this.#setState({ activeItems });
|
|
168
|
+
this.#setState({ activeItems: [...activeItems] });
|
|
169
|
+
scheduledAsyncWork = true;
|
|
168
170
|
(async () => {
|
|
169
|
-
|
|
171
|
+
try {
|
|
172
|
+
await this.execute();
|
|
173
|
+
} catch {}
|
|
170
174
|
const wait = this.#getWait();
|
|
171
175
|
if (wait > 0) {
|
|
172
|
-
const timeoutId = setTimeout(() =>
|
|
176
|
+
const timeoutId = setTimeout(() => {
|
|
177
|
+
this.#timeoutIds.delete(timeoutId);
|
|
178
|
+
this.#tick();
|
|
179
|
+
}, wait);
|
|
173
180
|
this.#timeoutIds.add(timeoutId);
|
|
174
181
|
return;
|
|
175
182
|
}
|
|
176
183
|
this.#tick();
|
|
177
184
|
})();
|
|
178
185
|
}
|
|
179
|
-
this.#setState({ pendingTick: false });
|
|
186
|
+
if (!scheduledAsyncWork) this.#setState({ pendingTick: false });
|
|
180
187
|
};
|
|
181
188
|
this.addItem = (item, position = this.options.addItemsTo ?? "back", runOnItemsChange = true) => {
|
|
182
189
|
this.#setState({ addItemCount: this.store.state.addItemCount + 1 });
|
|
190
|
+
if (item === void 0) {
|
|
191
|
+
this.#setState({ rejectionCount: this.store.state.rejectionCount + 1 });
|
|
192
|
+
this.options.onReject?.(item, this);
|
|
193
|
+
return false;
|
|
194
|
+
}
|
|
183
195
|
if (this.store.state.items.length >= (this.options.maxSize ?? Infinity)) {
|
|
184
196
|
this.#setState({ rejectionCount: this.store.state.rejectionCount + 1 });
|
|
185
197
|
this.options.onReject?.(item, this);
|
|
186
198
|
return false;
|
|
187
199
|
}
|
|
188
|
-
const priority = this.options.getPriority !== defaultOptions.getPriority ? this.options.getPriority(item) : item
|
|
200
|
+
const priority = this.options.getPriority !== defaultOptions.getPriority ? this.options.getPriority(item) : item?.priority;
|
|
189
201
|
const items = this.store.state.items;
|
|
190
202
|
const itemTimestamps = this.store.state.itemTimestamps;
|
|
191
203
|
if (priority !== void 0) {
|
|
192
204
|
const insertIndex = items.findIndex((existing) => {
|
|
193
|
-
return (this.options.getPriority !== defaultOptions.getPriority ? this.options.getPriority(existing) : existing
|
|
205
|
+
return (this.options.getPriority !== defaultOptions.getPriority ? this.options.getPriority(existing) : existing?.priority) < priority;
|
|
194
206
|
});
|
|
195
207
|
if (insertIndex === -1) {
|
|
196
208
|
items.push(item);
|
|
@@ -247,10 +259,7 @@ var AsyncQueuer = class {
|
|
|
247
259
|
isExecuting: true
|
|
248
260
|
});
|
|
249
261
|
try {
|
|
250
|
-
const currentAsyncRetryer = new AsyncRetryer(this.fn,
|
|
251
|
-
...this.options.asyncRetryerOptions,
|
|
252
|
-
key: `${this.key}-retryer-${currentExecuteCount}`
|
|
253
|
-
});
|
|
262
|
+
const currentAsyncRetryer = new AsyncRetryer(this.fn, this.options.asyncRetryerOptions);
|
|
254
263
|
this.asyncRetryers.set(currentExecuteCount, currentAsyncRetryer);
|
|
255
264
|
const lastResult = await currentAsyncRetryer.execute(item);
|
|
256
265
|
this.#setState({
|
|
@@ -264,9 +273,12 @@ var AsyncQueuer = class {
|
|
|
264
273
|
if (this.options.throwOnError) throw error;
|
|
265
274
|
} finally {
|
|
266
275
|
this.asyncRetryers.delete(currentExecuteCount);
|
|
276
|
+
const remainingActiveItems = [...this.store.state.activeItems];
|
|
277
|
+
const activeItemIndex = remainingActiveItems.indexOf(item);
|
|
278
|
+
if (activeItemIndex !== -1) remainingActiveItems.splice(activeItemIndex, 1);
|
|
267
279
|
this.#setState({
|
|
268
|
-
activeItems:
|
|
269
|
-
isExecuting:
|
|
280
|
+
activeItems: remainingActiveItems,
|
|
281
|
+
isExecuting: this.asyncRetryers.size > 0,
|
|
270
282
|
settledCount: this.store.state.settledCount + 1
|
|
271
283
|
});
|
|
272
284
|
this.options.onSettled?.(item, this);
|
|
@@ -276,11 +288,31 @@ var AsyncQueuer = class {
|
|
|
276
288
|
};
|
|
277
289
|
this.flush = async (numberOfItems = this.store.state.items.length, position) => {
|
|
278
290
|
this.#clearTimeouts();
|
|
279
|
-
await Promise.
|
|
291
|
+
const results = await Promise.allSettled(Array.from({ length: numberOfItems }, () => this.execute(position)));
|
|
292
|
+
this.#setState({ pendingTick: false });
|
|
293
|
+
if (this.store.state.isRunning && this.store.state.items.length > 0) {
|
|
294
|
+
const wait = this.#getWait();
|
|
295
|
+
if (wait > 0) {
|
|
296
|
+
this.#setState({ pendingTick: true });
|
|
297
|
+
const timeoutId = setTimeout(() => {
|
|
298
|
+
this.#timeoutIds.delete(timeoutId);
|
|
299
|
+
this.#tick();
|
|
300
|
+
}, wait);
|
|
301
|
+
this.#timeoutIds.add(timeoutId);
|
|
302
|
+
} else this.#tick();
|
|
303
|
+
}
|
|
304
|
+
const failure = results.find((result) => result.status === "rejected");
|
|
305
|
+
if (failure) throw failure.reason;
|
|
280
306
|
};
|
|
281
307
|
this.flushAsBatch = async (batchFunction) => {
|
|
282
308
|
this.#clearTimeouts();
|
|
283
|
-
|
|
309
|
+
const items = this.#getAllItems();
|
|
310
|
+
try {
|
|
311
|
+
await batchFunction(items);
|
|
312
|
+
} finally {
|
|
313
|
+
this.#setState({ pendingTick: false });
|
|
314
|
+
if (this.store.state.isRunning && this.store.state.items.length > 0) this.#tick();
|
|
315
|
+
}
|
|
284
316
|
};
|
|
285
317
|
this.#checkExpiredItems = () => {
|
|
286
318
|
if ((this.options.expirationDuration ?? Infinity) === Infinity && this.options.getIsExpired === defaultOptions.getIsExpired) return;
|