@tanstack/pacer 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/async-debouncer.cjs +10 -6
- package/dist/cjs/async-debouncer.cjs.map +1 -1
- package/dist/cjs/async-debouncer.d.cts +2 -2
- package/dist/cjs/async-queuer.cjs +135 -106
- package/dist/cjs/async-queuer.cjs.map +1 -1
- package/dist/cjs/async-queuer.d.cts +121 -93
- package/dist/cjs/async-rate-limiter.cjs +3 -4
- package/dist/cjs/async-rate-limiter.cjs.map +1 -1
- package/dist/cjs/async-rate-limiter.d.cts +1 -2
- package/dist/cjs/async-throttler.cjs +14 -4
- package/dist/cjs/async-throttler.cjs.map +1 -1
- package/dist/cjs/async-throttler.d.cts +3 -2
- package/dist/cjs/batcher.cjs +138 -0
- package/dist/cjs/batcher.cjs.map +1 -0
- package/dist/cjs/batcher.d.cts +149 -0
- package/dist/cjs/debouncer.cjs +3 -4
- package/dist/cjs/debouncer.cjs.map +1 -1
- package/dist/cjs/debouncer.d.cts +1 -2
- package/dist/cjs/index.cjs +3 -0
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/index.d.cts +1 -0
- package/dist/cjs/queuer.cjs +68 -41
- package/dist/cjs/queuer.cjs.map +1 -1
- package/dist/cjs/queuer.d.cts +123 -92
- package/dist/cjs/rate-limiter.cjs +3 -4
- package/dist/cjs/rate-limiter.cjs.map +1 -1
- package/dist/cjs/rate-limiter.d.cts +1 -2
- package/dist/cjs/throttler.cjs +3 -4
- package/dist/cjs/throttler.cjs.map +1 -1
- package/dist/cjs/throttler.d.cts +1 -2
- package/dist/esm/async-debouncer.d.ts +2 -2
- package/dist/esm/async-debouncer.js +10 -6
- package/dist/esm/async-debouncer.js.map +1 -1
- package/dist/esm/async-queuer.d.ts +121 -93
- package/dist/esm/async-queuer.js +135 -106
- package/dist/esm/async-queuer.js.map +1 -1
- package/dist/esm/async-rate-limiter.d.ts +1 -2
- package/dist/esm/async-rate-limiter.js +3 -4
- package/dist/esm/async-rate-limiter.js.map +1 -1
- package/dist/esm/async-throttler.d.ts +3 -2
- package/dist/esm/async-throttler.js +14 -4
- package/dist/esm/async-throttler.js.map +1 -1
- package/dist/esm/batcher.d.ts +149 -0
- package/dist/esm/batcher.js +138 -0
- package/dist/esm/batcher.js.map +1 -0
- package/dist/esm/debouncer.d.ts +1 -2
- package/dist/esm/debouncer.js +3 -4
- package/dist/esm/debouncer.js.map +1 -1
- package/dist/esm/index.d.ts +1 -0
- package/dist/esm/index.js +3 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/queuer.d.ts +123 -92
- package/dist/esm/queuer.js +68 -41
- package/dist/esm/queuer.js.map +1 -1
- package/dist/esm/rate-limiter.d.ts +1 -2
- package/dist/esm/rate-limiter.js +3 -4
- package/dist/esm/rate-limiter.js.map +1 -1
- package/dist/esm/throttler.d.ts +1 -2
- package/dist/esm/throttler.js +3 -4
- package/dist/esm/throttler.js.map +1 -1
- package/package.json +11 -1
- package/src/async-debouncer.ts +12 -6
- package/src/async-queuer.ts +216 -193
- package/src/async-rate-limiter.ts +3 -4
- package/src/async-throttler.ts +18 -4
- package/src/batcher.ts +253 -0
- package/src/debouncer.ts +3 -4
- package/src/index.ts +1 -0
- package/src/queuer.ts +142 -98
- package/src/rate-limiter.ts +3 -4
- package/src/throttler.ts +3 -4
|
@@ -18,6 +18,7 @@ class AsyncDebouncer {
|
|
|
18
18
|
this._settleCount = 0;
|
|
19
19
|
this._successCount = 0;
|
|
20
20
|
this._timeoutId = null;
|
|
21
|
+
this._resolvePreviousPromise = null;
|
|
21
22
|
this._options = {
|
|
22
23
|
...defaultOptions,
|
|
23
24
|
...initialOptions,
|
|
@@ -26,7 +27,6 @@ class AsyncDebouncer {
|
|
|
26
27
|
}
|
|
27
28
|
/**
|
|
28
29
|
* Updates the debouncer options
|
|
29
|
-
* Returns the new options state
|
|
30
30
|
*/
|
|
31
31
|
setOptions(newOptions) {
|
|
32
32
|
this._options = { ...this._options, ...newOptions };
|
|
@@ -71,23 +71,25 @@ class AsyncDebouncer {
|
|
|
71
71
|
this._lastArgs = args;
|
|
72
72
|
if (this._options.leading && this._canLeadingExecute) {
|
|
73
73
|
this._canLeadingExecute = false;
|
|
74
|
-
await this.
|
|
74
|
+
await this.execute(...args);
|
|
75
75
|
return this._lastResult;
|
|
76
76
|
}
|
|
77
77
|
if (this._options.trailing) {
|
|
78
78
|
this._isPending = true;
|
|
79
79
|
}
|
|
80
80
|
return new Promise((resolve) => {
|
|
81
|
+
this._resolvePreviousPromise = resolve;
|
|
81
82
|
this._timeoutId = setTimeout(async () => {
|
|
82
83
|
if (this._options.trailing && this._lastArgs) {
|
|
83
|
-
await this.
|
|
84
|
+
await this.execute(...this._lastArgs);
|
|
84
85
|
}
|
|
85
86
|
this._canLeadingExecute = true;
|
|
87
|
+
this._resolvePreviousPromise = null;
|
|
86
88
|
resolve(this._lastResult);
|
|
87
89
|
}, this.getWait());
|
|
88
90
|
});
|
|
89
91
|
}
|
|
90
|
-
async
|
|
92
|
+
async execute(...args) {
|
|
91
93
|
var _a, _b, _c, _d, _e, _f;
|
|
92
94
|
if (!this.getEnabled()) return void 0;
|
|
93
95
|
this._abortController = new AbortController();
|
|
@@ -101,8 +103,6 @@ class AsyncDebouncer {
|
|
|
101
103
|
(_d = (_c = this._options).onError) == null ? void 0 : _d.call(_c, error, this);
|
|
102
104
|
if (this._options.throwOnError) {
|
|
103
105
|
throw error;
|
|
104
|
-
} else {
|
|
105
|
-
console.error(error);
|
|
106
106
|
}
|
|
107
107
|
} finally {
|
|
108
108
|
this._isExecuting = false;
|
|
@@ -125,6 +125,10 @@ class AsyncDebouncer {
|
|
|
125
125
|
this._abortController.abort();
|
|
126
126
|
this._abortController = null;
|
|
127
127
|
}
|
|
128
|
+
if (this._resolvePreviousPromise) {
|
|
129
|
+
this._resolvePreviousPromise(this._lastResult);
|
|
130
|
+
this._resolvePreviousPromise = null;
|
|
131
|
+
}
|
|
128
132
|
this._lastArgs = void 0;
|
|
129
133
|
this._isPending = false;
|
|
130
134
|
this._isExecuting = false;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"async-debouncer.cjs","sources":["../../src/async-debouncer.ts"],"sourcesContent":["import { parseFunctionOrValue } from './utils'\nimport type { AnyAsyncFunction, OptionalKeys } from './types'\n\n/**\n * Options for configuring an async debounced function\n */\nexport interface AsyncDebouncerOptions<TFn extends AnyAsyncFunction> {\n /**\n * Whether the debouncer is enabled. When disabled, maybeExecute will not trigger any executions.\n * Can be a boolean or a function that returns a boolean.\n * Defaults to true.\n */\n enabled?: boolean | ((debouncer: AsyncDebouncer<TFn>) => boolean)\n /**\n * Whether to execute on the leading edge of the timeout.\n * Defaults to false.\n */\n leading?: boolean\n /**\n * Optional error handler for when the debounced function throws.\n * If provided, the handler will be called with the error and debouncer instance.\n * This can be used alongside throwOnError - the handler will be called before any error is thrown.\n */\n onError?: (error: unknown, debouncer: AsyncDebouncer<TFn>) => void\n /**\n * Optional callback to call when the debounced function is executed\n */\n onSettled?: (debouncer: AsyncDebouncer<TFn>) => void\n /**\n * Optional callback to call when the debounced function is executed\n */\n onSuccess?: (result: ReturnType<TFn>, debouncer: AsyncDebouncer<TFn>) => void\n /**\n * Whether to throw errors when they occur.\n * Defaults to true if no onError handler is provided, false if an onError handler is provided.\n * Can be explicitly set to override these defaults.\n */\n throwOnError?: boolean\n /**\n * Whether to execute on the trailing edge of the timeout.\n * Defaults to true.\n */\n trailing?: boolean\n /**\n * Delay in milliseconds to wait after the last call before executing.\n * Can be a number or a function that returns a number.\n * Defaults to 0ms\n */\n wait: number | ((debouncer: AsyncDebouncer<TFn>) => number)\n}\n\ntype AsyncDebouncerOptionsWithOptionalCallbacks = OptionalKeys<\n AsyncDebouncerOptions<any>,\n 'onError' | 'onSettled' | 'onSuccess'\n>\n\nconst defaultOptions: AsyncDebouncerOptionsWithOptionalCallbacks = {\n enabled: true,\n leading: false,\n trailing: true,\n wait: 0,\n}\n\n/**\n * A class that creates an async debounced function.\n *\n * Debouncing ensures that a function is only executed after a specified delay has passed since its last invocation.\n * Each new invocation resets the delay timer. This is useful for handling frequent events like window resizing\n * or input changes where you only want to execute the handler after the events have stopped occurring.\n *\n * Unlike throttling which allows execution at regular intervals, debouncing prevents any execution until\n * the function stops being called for the specified delay period.\n *\n * Unlike the non-async Debouncer, this async version supports returning values from the debounced function,\n * making it ideal for API calls and other async operations where you want the result of the `maybeExecute` call\n * instead of setting the result on a state variable from within the debounced function.\n *\n * Error Handling:\n * - If an error occurs during execution and no `onError` handler is provided, the error will be thrown and propagate up to the caller.\n * - If an `onError` handler is provided, errors will be caught and passed to the handler instead of being thrown.\n * - The error count can be tracked using `getErrorCount()`.\n * - The debouncer maintains its state and can continue to be used after an error occurs.\n *\n * @example\n * ```ts\n * const asyncDebouncer = new AsyncDebouncer(async (value: string) => {\n * const results = await searchAPI(value);\n * return results; // Return value is preserved\n * }, {\n * wait: 500,\n * onError: (error) => {\n * console.error('Search failed:', error);\n * }\n * });\n *\n * // Called on each keystroke but only executes after 500ms of no typing\n * // Returns the API response directly\n * const results = await asyncDebouncer.maybeExecute(inputElement.value);\n * ```\n */\nexport class AsyncDebouncer<TFn extends AnyAsyncFunction> {\n private _options: AsyncDebouncerOptionsWithOptionalCallbacks\n private _abortController: AbortController | null = null\n private _canLeadingExecute = true\n private _errorCount = 0\n private _isExecuting = false\n private _isPending = false\n private _lastArgs: Parameters<TFn> | undefined\n private _lastResult: ReturnType<TFn> | undefined\n private _settleCount = 0\n private _successCount = 0\n private _timeoutId: NodeJS.Timeout | null = null\n\n constructor(\n private fn: TFn,\n initialOptions: AsyncDebouncerOptions<TFn>,\n ) {\n this._options = {\n ...defaultOptions,\n ...initialOptions,\n throwOnError: initialOptions.throwOnError ?? !initialOptions.onError,\n }\n }\n\n /**\n * Updates the debouncer options\n * Returns the new options state\n */\n setOptions(newOptions: Partial<AsyncDebouncerOptions<TFn>>): void {\n this._options = { ...this._options, ...newOptions }\n\n // End the pending state if the debouncer is disabled\n if (!this._options.enabled) {\n this._isPending = false\n }\n }\n\n /**\n * Returns the current debouncer options\n */\n getOptions(): AsyncDebouncerOptions<TFn> {\n return this._options\n }\n\n /**\n * Returns the current debouncer enabled state\n */\n getEnabled(): boolean {\n return !!parseFunctionOrValue(this._options.enabled, this)\n }\n\n /**\n * Returns the current debouncer wait state\n */\n getWait(): number {\n return parseFunctionOrValue(this._options.wait, this)\n }\n\n /**\n * Attempts to execute the debounced function.\n * If a call is already in progress, it will be queued.\n *\n * Error Handling:\n * - If the debounced function throws and no `onError` handler is configured,\n * the error will be thrown from this method.\n * - If an `onError` handler is configured, errors will be caught and passed to the handler,\n * and this method will return undefined.\n * - The error state can be checked using `getErrorCount()` and `getIsExecuting()`.\n *\n * @returns A promise that resolves with the function's return value, or undefined if an error occurred and was handled by onError\n * @throws The error from the debounced function if no onError handler is configured\n */\n async maybeExecute(\n ...args: Parameters<TFn>\n ): Promise<ReturnType<TFn> | undefined> {\n this._cancel()\n this._lastArgs = args\n\n // Handle leading execution\n if (this._options.leading && this._canLeadingExecute) {\n this._canLeadingExecute = false\n await this.executeFunction(...args)\n return this._lastResult\n }\n\n // Handle trailing execution\n if (this._options.trailing) {\n this._isPending = true\n }\n\n return new Promise((resolve) => {\n this._timeoutId = setTimeout(async () => {\n // Execute trailing if enabled\n if (this._options.trailing && this._lastArgs) {\n await this.executeFunction(...this._lastArgs)\n }\n\n // Reset state and resolve\n this._canLeadingExecute = true\n resolve(this._lastResult)\n }, this.getWait())\n })\n }\n\n private async executeFunction(\n ...args: Parameters<TFn>\n ): Promise<ReturnType<TFn> | undefined> {\n if (!this.getEnabled()) return undefined\n this._abortController = new AbortController()\n try {\n this._isExecuting = true\n this._lastResult = await this.fn(...args) // EXECUTE!\n this._successCount++\n this._options.onSuccess?.(this._lastResult!, this)\n } catch (error) {\n this._errorCount++\n this._options.onError?.(error, this)\n if (this._options.throwOnError) {\n throw error\n } else {\n console.error(error)\n }\n } finally {\n this._isExecuting = false\n this._isPending = false\n this._settleCount++\n this._abortController = null\n this._options.onSettled?.(this)\n }\n return this._lastResult\n }\n\n /**\n * Cancel without resetting _canLeadingExecute\n */\n private _cancel(): void {\n if (this._timeoutId) {\n clearTimeout(this._timeoutId)\n this._timeoutId = null\n }\n if (this._abortController) {\n this._abortController.abort()\n this._abortController = null\n }\n this._lastArgs = undefined\n this._isPending = false\n this._isExecuting = false\n }\n\n /**\n * Cancels any pending execution or aborts any execution in progress\n */\n cancel(): void {\n this._canLeadingExecute = true\n this._cancel()\n }\n\n /**\n * Returns the last result of the debounced function\n */\n getLastResult(): ReturnType<TFn> | undefined {\n return this._lastResult\n }\n\n /**\n * Returns the number of times the function has been executed successfully\n */\n getSuccessCount(): number {\n return this._successCount\n }\n\n /**\n * Returns the number of times the function has settled (completed or errored)\n */\n getSettleCount(): number {\n return this._settleCount\n }\n\n /**\n * Returns the number of times the function has errored\n */\n getErrorCount(): number {\n return this._errorCount\n }\n\n /**\n * Returns `true` if there is a pending execution queued up for trailing execution\n */\n getIsPending(): boolean {\n return this.getEnabled() && this._isPending\n }\n\n /**\n * Returns `true` if there is currently an execution in progress\n */\n getIsExecuting(): boolean {\n return this._isExecuting\n }\n}\n\n/**\n * Creates an async debounced function that delays execution until after a specified wait time.\n * The debounced function will only execute once the wait period has elapsed without any new calls.\n * If called again during the wait period, the timer resets and a new wait period begins.\n *\n * Unlike the non-async Debouncer, this async version supports returning values from the debounced function,\n * making it ideal for API calls and other async operations where you want the result of the `maybeExecute` call\n * instead of setting the result on a state variable from within the debounced function.\n *\n * Error Handling:\n * - If an `onError` handler is provided, it will be called with the error and debouncer instance\n * - If `throwOnError` is true (default when no onError handler is provided), the error will be thrown\n * - If `throwOnError` is false (default when onError handler is provided), the error will be swallowed\n * - The error state can be checked using the underlying AsyncDebouncer instance\n * - Both onError and throwOnError can be used together - the handler will be called before any error is thrown\n *\n * @example\n * ```ts\n * const debounced = asyncDebounce(async (value: string) => {\n * const result = await saveToAPI(value);\n * return result; // Return value is preserved\n * }, {\n * wait: 1000,\n * onError: (error) => {\n * console.error('API call failed:', error);\n * },\n * throwOnError: true // Will both log the error and throw it\n * });\n *\n * // Will only execute once, 1 second after the last call\n * // Returns the API response directly\n * const result = await debounced(\"third\");\n * ```\n */\nexport function asyncDebounce<TFn extends AnyAsyncFunction>(\n fn: TFn,\n initialOptions: AsyncDebouncerOptions<TFn>,\n) {\n const asyncDebouncer = new AsyncDebouncer(fn, initialOptions)\n return asyncDebouncer.maybeExecute.bind(asyncDebouncer)\n}\n"],"names":["parseFunctionOrValue"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"async-debouncer.cjs","sources":["../../src/async-debouncer.ts"],"sourcesContent":["import { parseFunctionOrValue } from './utils'\nimport type { AnyAsyncFunction, OptionalKeys } from './types'\n\n/**\n * Options for configuring an async debounced function\n */\nexport interface AsyncDebouncerOptions<TFn extends AnyAsyncFunction> {\n /**\n * Whether the debouncer is enabled. When disabled, maybeExecute will not trigger any executions.\n * Can be a boolean or a function that returns a boolean.\n * Defaults to true.\n */\n enabled?: boolean | ((debouncer: AsyncDebouncer<TFn>) => boolean)\n /**\n * Whether to execute on the leading edge of the timeout.\n * Defaults to false.\n */\n leading?: boolean\n /**\n * Optional error handler for when the debounced function throws.\n * If provided, the handler will be called with the error and debouncer instance.\n * This can be used alongside throwOnError - the handler will be called before any error is thrown.\n */\n onError?: (error: unknown, debouncer: AsyncDebouncer<TFn>) => void\n /**\n * Optional callback to call when the debounced function is executed\n */\n onSettled?: (debouncer: AsyncDebouncer<TFn>) => void\n /**\n * Optional callback to call when the debounced function is executed\n */\n onSuccess?: (result: ReturnType<TFn>, debouncer: AsyncDebouncer<TFn>) => void\n /**\n * Whether to throw errors when they occur.\n * Defaults to true if no onError handler is provided, false if an onError handler is provided.\n * Can be explicitly set to override these defaults.\n */\n throwOnError?: boolean\n /**\n * Whether to execute on the trailing edge of the timeout.\n * Defaults to true.\n */\n trailing?: boolean\n /**\n * Delay in milliseconds to wait after the last call before executing.\n * Can be a number or a function that returns a number.\n * Defaults to 0ms\n */\n wait: number | ((debouncer: AsyncDebouncer<TFn>) => number)\n}\n\ntype AsyncDebouncerOptionsWithOptionalCallbacks = OptionalKeys<\n AsyncDebouncerOptions<any>,\n 'onError' | 'onSettled' | 'onSuccess'\n>\n\nconst defaultOptions: AsyncDebouncerOptionsWithOptionalCallbacks = {\n enabled: true,\n leading: false,\n trailing: true,\n wait: 0,\n}\n\n/**\n * A class that creates an async debounced function.\n *\n * Debouncing ensures that a function is only executed after a specified delay has passed since its last invocation.\n * Each new invocation resets the delay timer. This is useful for handling frequent events like window resizing\n * or input changes where you only want to execute the handler after the events have stopped occurring.\n *\n * Unlike throttling which allows execution at regular intervals, debouncing prevents any execution until\n * the function stops being called for the specified delay period.\n *\n * Unlike the non-async Debouncer, this async version supports returning values from the debounced function,\n * making it ideal for API calls and other async operations where you want the result of the `maybeExecute` call\n * instead of setting the result on a state variable from within the debounced function.\n *\n * Error Handling:\n * - If an error occurs during execution and no `onError` handler is provided, the error will be thrown and propagate up to the caller.\n * - If an `onError` handler is provided, errors will be caught and passed to the handler instead of being thrown.\n * - The error count can be tracked using `getErrorCount()`.\n * - The debouncer maintains its state and can continue to be used after an error occurs.\n *\n * @example\n * ```ts\n * const asyncDebouncer = new AsyncDebouncer(async (value: string) => {\n * const results = await searchAPI(value);\n * return results; // Return value is preserved\n * }, {\n * wait: 500,\n * onError: (error) => {\n * console.error('Search failed:', error);\n * }\n * });\n *\n * // Called on each keystroke but only executes after 500ms of no typing\n * // Returns the API response directly\n * const results = await asyncDebouncer.maybeExecute(inputElement.value);\n * ```\n */\nexport class AsyncDebouncer<TFn extends AnyAsyncFunction> {\n private _options: AsyncDebouncerOptionsWithOptionalCallbacks\n private _abortController: AbortController | null = null\n private _canLeadingExecute = true\n private _errorCount = 0\n private _isExecuting = false\n private _isPending = false\n private _lastArgs: Parameters<TFn> | undefined\n private _lastResult: ReturnType<TFn> | undefined\n private _settleCount = 0\n private _successCount = 0\n private _timeoutId: NodeJS.Timeout | null = null\n private _resolvePreviousPromise:\n | ((value?: ReturnType<TFn> | undefined) => void)\n | null = null\n\n constructor(\n private fn: TFn,\n initialOptions: AsyncDebouncerOptions<TFn>,\n ) {\n this._options = {\n ...defaultOptions,\n ...initialOptions,\n throwOnError: initialOptions.throwOnError ?? !initialOptions.onError,\n }\n }\n\n /**\n * Updates the debouncer options\n */\n setOptions(newOptions: Partial<AsyncDebouncerOptions<TFn>>): void {\n this._options = { ...this._options, ...newOptions }\n\n // End the pending state if the debouncer is disabled\n if (!this._options.enabled) {\n this._isPending = false\n }\n }\n\n /**\n * Returns the current debouncer options\n */\n getOptions(): AsyncDebouncerOptions<TFn> {\n return this._options\n }\n\n /**\n * Returns the current debouncer enabled state\n */\n getEnabled(): boolean {\n return !!parseFunctionOrValue(this._options.enabled, this)\n }\n\n /**\n * Returns the current debouncer wait state\n */\n getWait(): number {\n return parseFunctionOrValue(this._options.wait, this)\n }\n\n /**\n * Attempts to execute the debounced function.\n * If a call is already in progress, it will be queued.\n *\n * Error Handling:\n * - If the debounced function throws and no `onError` handler is configured,\n * the error will be thrown from this method.\n * - If an `onError` handler is configured, errors will be caught and passed to the handler,\n * and this method will return undefined.\n * - The error state can be checked using `getErrorCount()` and `getIsExecuting()`.\n *\n * @returns A promise that resolves with the function's return value, or undefined if an error occurred and was handled by onError\n * @throws The error from the debounced function if no onError handler is configured\n */\n async maybeExecute(\n ...args: Parameters<TFn>\n ): Promise<ReturnType<TFn> | undefined> {\n this._cancel()\n this._lastArgs = args\n\n // Handle leading execution\n if (this._options.leading && this._canLeadingExecute) {\n this._canLeadingExecute = false\n await this.execute(...args)\n return this._lastResult\n }\n\n // Handle trailing execution\n if (this._options.trailing) {\n this._isPending = true\n }\n\n return new Promise((resolve) => {\n this._resolvePreviousPromise = resolve\n this._timeoutId = setTimeout(async () => {\n // Execute trailing if enabled\n if (this._options.trailing && this._lastArgs) {\n await this.execute(...this._lastArgs)\n }\n\n // Reset state and resolve\n this._canLeadingExecute = true\n this._resolvePreviousPromise = null\n resolve(this._lastResult)\n }, this.getWait())\n })\n }\n\n private async execute(\n ...args: Parameters<TFn>\n ): Promise<ReturnType<TFn> | undefined> {\n if (!this.getEnabled()) return undefined\n this._abortController = new AbortController()\n try {\n this._isExecuting = true\n this._lastResult = await this.fn(...args) // EXECUTE!\n this._successCount++\n this._options.onSuccess?.(this._lastResult!, this)\n } catch (error) {\n this._errorCount++\n this._options.onError?.(error, this)\n if (this._options.throwOnError) {\n throw error\n }\n } finally {\n this._isExecuting = false\n this._isPending = false\n this._settleCount++\n this._abortController = null\n this._options.onSettled?.(this)\n }\n return this._lastResult\n }\n\n /**\n * Cancel without resetting _canLeadingExecute\n */\n private _cancel(): void {\n if (this._timeoutId) {\n clearTimeout(this._timeoutId)\n this._timeoutId = null\n }\n if (this._abortController) {\n this._abortController.abort()\n this._abortController = null\n }\n if (this._resolvePreviousPromise) {\n this._resolvePreviousPromise(this._lastResult)\n this._resolvePreviousPromise = null\n }\n this._lastArgs = undefined\n this._isPending = false\n this._isExecuting = false\n }\n\n /**\n * Cancels any pending execution or aborts any execution in progress\n */\n cancel(): void {\n this._canLeadingExecute = true\n this._cancel()\n }\n\n /**\n * Returns the last result of the debounced function\n */\n getLastResult(): ReturnType<TFn> | undefined {\n return this._lastResult\n }\n\n /**\n * Returns the number of times the function has been executed successfully\n */\n getSuccessCount(): number {\n return this._successCount\n }\n\n /**\n * Returns the number of times the function has settled (completed or errored)\n */\n getSettleCount(): number {\n return this._settleCount\n }\n\n /**\n * Returns the number of times the function has errored\n */\n getErrorCount(): number {\n return this._errorCount\n }\n\n /**\n * Returns `true` if there is a pending execution queued up for trailing execution\n */\n getIsPending(): boolean {\n return this.getEnabled() && this._isPending\n }\n\n /**\n * Returns `true` if there is currently an execution in progress\n */\n getIsExecuting(): boolean {\n return this._isExecuting\n }\n}\n\n/**\n * Creates an async debounced function that delays execution until after a specified wait time.\n * The debounced function will only execute once the wait period has elapsed without any new calls.\n * If called again during the wait period, the timer resets and a new wait period begins.\n *\n * Unlike the non-async Debouncer, this async version supports returning values from the debounced function,\n * making it ideal for API calls and other async operations where you want the result of the `maybeExecute` call\n * instead of setting the result on a state variable from within the debounced function.\n *\n * Error Handling:\n * - If an `onError` handler is provided, it will be called with the error and debouncer instance\n * - If `throwOnError` is true (default when no onError handler is provided), the error will be thrown\n * - If `throwOnError` is false (default when onError handler is provided), the error will be swallowed\n * - The error state can be checked using the underlying AsyncDebouncer instance\n * - Both onError and throwOnError can be used together - the handler will be called before any error is thrown\n *\n * @example\n * ```ts\n * const debounced = asyncDebounce(async (value: string) => {\n * const result = await saveToAPI(value);\n * return result; // Return value is preserved\n * }, {\n * wait: 1000,\n * onError: (error) => {\n * console.error('API call failed:', error);\n * },\n * throwOnError: true // Will both log the error and throw it\n * });\n *\n * // Will only execute once, 1 second after the last call\n * // Returns the API response directly\n * const result = await debounced(\"third\");\n * ```\n */\nexport function asyncDebounce<TFn extends AnyAsyncFunction>(\n fn: TFn,\n initialOptions: AsyncDebouncerOptions<TFn>,\n) {\n const asyncDebouncer = new AsyncDebouncer(fn, initialOptions)\n return asyncDebouncer.maybeExecute.bind(asyncDebouncer)\n}\n"],"names":["parseFunctionOrValue"],"mappings":"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"}
|
|
@@ -96,10 +96,10 @@ export declare class AsyncDebouncer<TFn extends AnyAsyncFunction> {
|
|
|
96
96
|
private _settleCount;
|
|
97
97
|
private _successCount;
|
|
98
98
|
private _timeoutId;
|
|
99
|
+
private _resolvePreviousPromise;
|
|
99
100
|
constructor(fn: TFn, initialOptions: AsyncDebouncerOptions<TFn>);
|
|
100
101
|
/**
|
|
101
102
|
* Updates the debouncer options
|
|
102
|
-
* Returns the new options state
|
|
103
103
|
*/
|
|
104
104
|
setOptions(newOptions: Partial<AsyncDebouncerOptions<TFn>>): void;
|
|
105
105
|
/**
|
|
@@ -129,7 +129,7 @@ export declare class AsyncDebouncer<TFn extends AnyAsyncFunction> {
|
|
|
129
129
|
* @throws The error from the debounced function if no onError handler is configured
|
|
130
130
|
*/
|
|
131
131
|
maybeExecute(...args: Parameters<TFn>): Promise<ReturnType<TFn> | undefined>;
|
|
132
|
-
private
|
|
132
|
+
private execute;
|
|
133
133
|
/**
|
|
134
134
|
* Cancel without resetting _canLeadingExecute
|
|
135
135
|
*/
|
|
@@ -14,7 +14,8 @@ const defaultOptions = {
|
|
|
14
14
|
wait: 0
|
|
15
15
|
};
|
|
16
16
|
class AsyncQueuer {
|
|
17
|
-
constructor(initialOptions
|
|
17
|
+
constructor(fn, initialOptions) {
|
|
18
|
+
this.fn = fn;
|
|
18
19
|
this._activeItems = /* @__PURE__ */ new Set();
|
|
19
20
|
this._successCount = 0;
|
|
20
21
|
this._errorCount = 0;
|
|
@@ -37,32 +38,33 @@ class AsyncQueuer {
|
|
|
37
38
|
}
|
|
38
39
|
}
|
|
39
40
|
/**
|
|
40
|
-
* Updates the queuer options
|
|
41
|
-
* Returns the new options state
|
|
41
|
+
* Updates the queuer options. New options are merged with existing options.
|
|
42
42
|
*/
|
|
43
43
|
setOptions(newOptions) {
|
|
44
44
|
this._options = { ...this._options, ...newOptions };
|
|
45
45
|
}
|
|
46
46
|
/**
|
|
47
|
-
* Returns the current queuer options
|
|
47
|
+
* Returns the current queuer options, including defaults and any overrides.
|
|
48
48
|
*/
|
|
49
49
|
getOptions() {
|
|
50
50
|
return this._options;
|
|
51
51
|
}
|
|
52
52
|
/**
|
|
53
|
-
* Returns the current wait time between processing items
|
|
53
|
+
* Returns the current wait time (in milliseconds) between processing items.
|
|
54
|
+
* If a function is provided, it is called with the queuer instance.
|
|
54
55
|
*/
|
|
55
56
|
getWait() {
|
|
56
57
|
return utils.parseFunctionOrValue(this._options.wait, this);
|
|
57
58
|
}
|
|
58
59
|
/**
|
|
59
|
-
* Returns the current concurrency limit
|
|
60
|
+
* Returns the current concurrency limit for processing items.
|
|
61
|
+
* If a function is provided, it is called with the queuer instance.
|
|
60
62
|
*/
|
|
61
63
|
getConcurrency() {
|
|
62
64
|
return utils.parseFunctionOrValue(this._options.concurrency, this);
|
|
63
65
|
}
|
|
64
66
|
/**
|
|
65
|
-
* Processes items in the
|
|
67
|
+
* Processes items in the queue up to the concurrency limit. Internal use only.
|
|
66
68
|
*/
|
|
67
69
|
tick() {
|
|
68
70
|
var _a, _b;
|
|
@@ -72,33 +74,14 @@ class AsyncQueuer {
|
|
|
72
74
|
}
|
|
73
75
|
this.checkExpiredItems();
|
|
74
76
|
while (this._activeItems.size < this.getConcurrency() && !this.getIsEmpty()) {
|
|
75
|
-
const
|
|
76
|
-
if (!
|
|
77
|
+
const nextItem = this.getPeek();
|
|
78
|
+
if (!nextItem) {
|
|
77
79
|
break;
|
|
78
80
|
}
|
|
79
|
-
this._activeItems.add(
|
|
81
|
+
this._activeItems.add(nextItem);
|
|
80
82
|
(_b = (_a = this._options).onItemsChange) == null ? void 0 : _b.call(_a, this);
|
|
81
83
|
(async () => {
|
|
82
|
-
|
|
83
|
-
let res;
|
|
84
|
-
try {
|
|
85
|
-
res = await nextFn();
|
|
86
|
-
this._successCount++;
|
|
87
|
-
(_b2 = (_a2 = this._options).onSuccess) == null ? void 0 : _b2.call(_a2, res, this);
|
|
88
|
-
} catch (error) {
|
|
89
|
-
this._errorCount++;
|
|
90
|
-
(_d = (_c = this._options).onError) == null ? void 0 : _d.call(_c, error, this);
|
|
91
|
-
if (this._options.throwOnError) {
|
|
92
|
-
throw error;
|
|
93
|
-
} else {
|
|
94
|
-
console.error(error);
|
|
95
|
-
}
|
|
96
|
-
} finally {
|
|
97
|
-
this._settledCount++;
|
|
98
|
-
this._activeItems.delete(nextFn);
|
|
99
|
-
(_f = (_e = this._options).onItemsChange) == null ? void 0 : _f.call(_e, this);
|
|
100
|
-
(_h = (_g = this._options).onSettled) == null ? void 0 : _h.call(_g, this);
|
|
101
|
-
}
|
|
84
|
+
this._lastResult = await this.execute();
|
|
102
85
|
const wait = this.getWait();
|
|
103
86
|
if (wait > 0) {
|
|
104
87
|
setTimeout(() => this.tick(), wait);
|
|
@@ -110,40 +93,7 @@ class AsyncQueuer {
|
|
|
110
93
|
this._pendingTick = false;
|
|
111
94
|
}
|
|
112
95
|
/**
|
|
113
|
-
*
|
|
114
|
-
*/
|
|
115
|
-
checkExpiredItems() {
|
|
116
|
-
var _a, _b, _c, _d;
|
|
117
|
-
if (this._options.expirationDuration === Infinity && this._options.getIsExpired === defaultOptions.getIsExpired)
|
|
118
|
-
return;
|
|
119
|
-
const now = Date.now();
|
|
120
|
-
const expiredIndices = [];
|
|
121
|
-
for (let i = 0; i < this._items.length; i++) {
|
|
122
|
-
const timestamp = this._itemTimestamps[i];
|
|
123
|
-
if (timestamp === void 0) continue;
|
|
124
|
-
const item = this._items[i];
|
|
125
|
-
if (item === void 0) continue;
|
|
126
|
-
const isExpired = this._options.getIsExpired !== defaultOptions.getIsExpired ? this._options.getIsExpired(item, timestamp) : now - timestamp > this._options.expirationDuration;
|
|
127
|
-
if (isExpired) {
|
|
128
|
-
expiredIndices.push(i);
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
for (let i = expiredIndices.length - 1; i >= 0; i--) {
|
|
132
|
-
const index = expiredIndices[i];
|
|
133
|
-
if (index === void 0) continue;
|
|
134
|
-
const expiredItem = this._items[index];
|
|
135
|
-
if (expiredItem === void 0) continue;
|
|
136
|
-
this._items.splice(index, 1);
|
|
137
|
-
this._itemTimestamps.splice(index, 1);
|
|
138
|
-
this._expirationCount++;
|
|
139
|
-
(_b = (_a = this._options).onExpire) == null ? void 0 : _b.call(_a, expiredItem, this);
|
|
140
|
-
}
|
|
141
|
-
if (expiredIndices.length > 0) {
|
|
142
|
-
(_d = (_c = this._options).onItemsChange) == null ? void 0 : _d.call(_c, this);
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
/**
|
|
146
|
-
* Starts the queuer and processes items
|
|
96
|
+
* Starts processing items in the queue. If already running, does nothing.
|
|
147
97
|
*/
|
|
148
98
|
start() {
|
|
149
99
|
var _a, _b;
|
|
@@ -153,19 +103,9 @@ class AsyncQueuer {
|
|
|
153
103
|
this.tick();
|
|
154
104
|
}
|
|
155
105
|
(_b = (_a = this._options).onIsRunningChange) == null ? void 0 : _b.call(_a, this);
|
|
156
|
-
return new Promise((resolve) => {
|
|
157
|
-
const checkIdle = () => {
|
|
158
|
-
if (this.getIsIdle()) {
|
|
159
|
-
resolve();
|
|
160
|
-
} else {
|
|
161
|
-
setTimeout(checkIdle, 100);
|
|
162
|
-
}
|
|
163
|
-
};
|
|
164
|
-
checkIdle();
|
|
165
|
-
});
|
|
166
106
|
}
|
|
167
107
|
/**
|
|
168
|
-
* Stops the
|
|
108
|
+
* Stops processing items in the queue. Does not clear the queue.
|
|
169
109
|
*/
|
|
170
110
|
stop() {
|
|
171
111
|
var _a, _b;
|
|
@@ -174,7 +114,7 @@ class AsyncQueuer {
|
|
|
174
114
|
(_b = (_a = this._options).onIsRunningChange) == null ? void 0 : _b.call(_a, this);
|
|
175
115
|
}
|
|
176
116
|
/**
|
|
177
|
-
* Removes all items from the
|
|
117
|
+
* Removes all pending items from the queue. Does not affect active tasks.
|
|
178
118
|
*/
|
|
179
119
|
clear() {
|
|
180
120
|
var _a, _b;
|
|
@@ -182,7 +122,8 @@ class AsyncQueuer {
|
|
|
182
122
|
(_b = (_a = this._options).onItemsChange) == null ? void 0 : _b.call(_a, this);
|
|
183
123
|
}
|
|
184
124
|
/**
|
|
185
|
-
* Resets the queuer to its initial state
|
|
125
|
+
* Resets the queuer to its initial state. Optionally repopulates with initial items.
|
|
126
|
+
* Does not affect callbacks or options.
|
|
186
127
|
*/
|
|
187
128
|
reset(withInitialItems) {
|
|
188
129
|
this.clear();
|
|
@@ -195,34 +136,41 @@ class AsyncQueuer {
|
|
|
195
136
|
this._running = this._options.started;
|
|
196
137
|
}
|
|
197
138
|
/**
|
|
198
|
-
* Adds
|
|
139
|
+
* Adds an item to the queue. If the queue is full, the item is rejected and onReject is called.
|
|
140
|
+
* Items can be inserted based on priority or at the front/back depending on configuration.
|
|
141
|
+
*
|
|
142
|
+
* @example
|
|
143
|
+
* ```ts
|
|
144
|
+
* queuer.addItem({ value: 'task', priority: 10 });
|
|
145
|
+
* queuer.addItem('task2', 'front');
|
|
146
|
+
* ```
|
|
199
147
|
*/
|
|
200
|
-
addItem(
|
|
148
|
+
addItem(item, position = this._options.addItemsTo, runOnItemsChange = true) {
|
|
201
149
|
var _a, _b, _c, _d;
|
|
202
150
|
if (this.getIsFull()) {
|
|
203
151
|
this._rejectionCount++;
|
|
204
|
-
(_b = (_a = this._options).onReject) == null ? void 0 : _b.call(_a,
|
|
152
|
+
(_b = (_a = this._options).onReject) == null ? void 0 : _b.call(_a, item, this);
|
|
205
153
|
return;
|
|
206
154
|
}
|
|
207
|
-
const priority = this._options.getPriority !== defaultOptions.getPriority ? this._options.getPriority(
|
|
155
|
+
const priority = this._options.getPriority !== defaultOptions.getPriority ? this._options.getPriority(item) : item.priority;
|
|
208
156
|
if (priority !== void 0) {
|
|
209
157
|
const insertIndex = this._items.findIndex((existing) => {
|
|
210
158
|
const existingPriority = this._options.getPriority !== defaultOptions.getPriority ? this._options.getPriority(existing) : existing.priority;
|
|
211
|
-
return existingPriority
|
|
159
|
+
return existingPriority < priority;
|
|
212
160
|
});
|
|
213
161
|
if (insertIndex === -1) {
|
|
214
|
-
this._items.push(
|
|
162
|
+
this._items.push(item);
|
|
215
163
|
this._itemTimestamps.push(Date.now());
|
|
216
164
|
} else {
|
|
217
|
-
this._items.splice(insertIndex, 0,
|
|
165
|
+
this._items.splice(insertIndex, 0, item);
|
|
218
166
|
this._itemTimestamps.splice(insertIndex, 0, Date.now());
|
|
219
167
|
}
|
|
220
168
|
} else {
|
|
221
169
|
if (position === "front") {
|
|
222
|
-
this._items.unshift(
|
|
170
|
+
this._items.unshift(item);
|
|
223
171
|
this._itemTimestamps.unshift(Date.now());
|
|
224
172
|
} else {
|
|
225
|
-
this._items.push(
|
|
173
|
+
this._items.push(item);
|
|
226
174
|
this._itemTimestamps.push(Date.now());
|
|
227
175
|
}
|
|
228
176
|
}
|
|
@@ -235,10 +183,19 @@ class AsyncQueuer {
|
|
|
235
183
|
}
|
|
236
184
|
}
|
|
237
185
|
/**
|
|
238
|
-
* Removes and returns
|
|
186
|
+
* Removes and returns the next item from the queue without executing the task function.
|
|
187
|
+
* Use for manual queue management. Normally, use execute() to process items.
|
|
188
|
+
*
|
|
189
|
+
* @example
|
|
190
|
+
* ```ts
|
|
191
|
+
* // FIFO
|
|
192
|
+
* queuer.getNextItem();
|
|
193
|
+
* // LIFO
|
|
194
|
+
* queuer.getNextItem('back');
|
|
195
|
+
* ```
|
|
239
196
|
*/
|
|
240
197
|
getNextItem(position = this._options.getItemsFrom) {
|
|
241
|
-
var _a, _b
|
|
198
|
+
var _a, _b;
|
|
242
199
|
let item;
|
|
243
200
|
if (position === "front") {
|
|
244
201
|
item = this._items.shift();
|
|
@@ -249,12 +206,84 @@ class AsyncQueuer {
|
|
|
249
206
|
}
|
|
250
207
|
if (item !== void 0) {
|
|
251
208
|
(_b = (_a = this._options).onItemsChange) == null ? void 0 : _b.call(_a, this);
|
|
252
|
-
(_d = (_c = this._options).onGetNextItem) == null ? void 0 : _d.call(_c, item, this);
|
|
253
209
|
}
|
|
254
210
|
return item;
|
|
255
211
|
}
|
|
256
212
|
/**
|
|
257
|
-
*
|
|
213
|
+
* Removes and returns the next item from the queue and executes the task function with it.
|
|
214
|
+
*
|
|
215
|
+
* @example
|
|
216
|
+
* ```ts
|
|
217
|
+
* queuer.execute();
|
|
218
|
+
* // LIFO
|
|
219
|
+
* queuer.execute('back');
|
|
220
|
+
* ```
|
|
221
|
+
*/
|
|
222
|
+
async execute(position) {
|
|
223
|
+
var _a, _b, _c, _d, _e, _f, _g, _h;
|
|
224
|
+
const item = this.getNextItem(position);
|
|
225
|
+
if (item !== void 0) {
|
|
226
|
+
try {
|
|
227
|
+
this._lastResult = await this.fn(item);
|
|
228
|
+
this._successCount++;
|
|
229
|
+
(_b = (_a = this._options).onSuccess) == null ? void 0 : _b.call(_a, this._lastResult, this);
|
|
230
|
+
} catch (error) {
|
|
231
|
+
this._errorCount++;
|
|
232
|
+
(_d = (_c = this._options).onError) == null ? void 0 : _d.call(_c, error, this);
|
|
233
|
+
if (this._options.throwOnError) {
|
|
234
|
+
throw error;
|
|
235
|
+
}
|
|
236
|
+
} finally {
|
|
237
|
+
this._settledCount++;
|
|
238
|
+
this._activeItems.delete(item);
|
|
239
|
+
(_f = (_e = this._options).onItemsChange) == null ? void 0 : _f.call(_e, this);
|
|
240
|
+
(_h = (_g = this._options).onSettled) == null ? void 0 : _h.call(_g, this);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
return item;
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Checks for expired items in the queue and removes them. Calls onExpire for each expired item.
|
|
247
|
+
* Internal use only.
|
|
248
|
+
*/
|
|
249
|
+
checkExpiredItems() {
|
|
250
|
+
var _a, _b, _c, _d;
|
|
251
|
+
if (this._options.expirationDuration === Infinity && this._options.getIsExpired === defaultOptions.getIsExpired)
|
|
252
|
+
return;
|
|
253
|
+
const now = Date.now();
|
|
254
|
+
const expiredIndices = [];
|
|
255
|
+
for (let i = 0; i < this._items.length; i++) {
|
|
256
|
+
const timestamp = this._itemTimestamps[i];
|
|
257
|
+
if (timestamp === void 0) continue;
|
|
258
|
+
const item = this._items[i];
|
|
259
|
+
if (item === void 0) continue;
|
|
260
|
+
const isExpired = this._options.getIsExpired !== defaultOptions.getIsExpired ? this._options.getIsExpired(item, timestamp) : now - timestamp > this._options.expirationDuration;
|
|
261
|
+
if (isExpired) {
|
|
262
|
+
expiredIndices.push(i);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
for (let i = expiredIndices.length - 1; i >= 0; i--) {
|
|
266
|
+
const index = expiredIndices[i];
|
|
267
|
+
if (index === void 0) continue;
|
|
268
|
+
const expiredItem = this._items[index];
|
|
269
|
+
if (expiredItem === void 0) continue;
|
|
270
|
+
this._items.splice(index, 1);
|
|
271
|
+
this._itemTimestamps.splice(index, 1);
|
|
272
|
+
this._expirationCount++;
|
|
273
|
+
(_b = (_a = this._options).onExpire) == null ? void 0 : _b.call(_a, expiredItem, this);
|
|
274
|
+
}
|
|
275
|
+
if (expiredIndices.length > 0) {
|
|
276
|
+
(_d = (_c = this._options).onItemsChange) == null ? void 0 : _d.call(_c, this);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Returns the next item in the queue without removing it.
|
|
281
|
+
*
|
|
282
|
+
* @example
|
|
283
|
+
* ```ts
|
|
284
|
+
* queuer.getPeek(); // front
|
|
285
|
+
* queuer.getPeek('back'); // back
|
|
286
|
+
* ```
|
|
258
287
|
*/
|
|
259
288
|
getPeek(position = "front") {
|
|
260
289
|
if (position === "front") {
|
|
@@ -263,87 +292,87 @@ class AsyncQueuer {
|
|
|
263
292
|
return this._items[this._items.length - 1];
|
|
264
293
|
}
|
|
265
294
|
/**
|
|
266
|
-
* Returns true if the
|
|
295
|
+
* Returns true if the queue is empty (no pending items).
|
|
267
296
|
*/
|
|
268
297
|
getIsEmpty() {
|
|
269
298
|
return this._items.length === 0;
|
|
270
299
|
}
|
|
271
300
|
/**
|
|
272
|
-
* Returns true if the
|
|
301
|
+
* Returns true if the queue is full (reached maxSize).
|
|
273
302
|
*/
|
|
274
303
|
getIsFull() {
|
|
275
304
|
return this._items.length >= this._options.maxSize;
|
|
276
305
|
}
|
|
277
306
|
/**
|
|
278
|
-
* Returns the
|
|
307
|
+
* Returns the number of pending items in the queue.
|
|
279
308
|
*/
|
|
280
309
|
getSize() {
|
|
281
310
|
return this._items.length;
|
|
282
311
|
}
|
|
283
312
|
/**
|
|
284
|
-
* Returns a copy of all items in the
|
|
313
|
+
* Returns a copy of all items in the queue, including active and pending items.
|
|
285
314
|
*/
|
|
286
315
|
getAllItems() {
|
|
287
316
|
return [...this.getActiveItems(), ...this.getPendingItems()];
|
|
288
317
|
}
|
|
289
318
|
/**
|
|
290
|
-
* Returns the active
|
|
319
|
+
* Returns the items currently being processed (active tasks).
|
|
291
320
|
*/
|
|
292
321
|
getActiveItems() {
|
|
293
322
|
return Array.from(this._activeItems);
|
|
294
323
|
}
|
|
295
324
|
/**
|
|
296
|
-
* Returns the pending
|
|
325
|
+
* Returns the items waiting to be processed (pending tasks).
|
|
297
326
|
*/
|
|
298
327
|
getPendingItems() {
|
|
299
328
|
return [...this._items];
|
|
300
329
|
}
|
|
301
330
|
/**
|
|
302
|
-
* Returns the number of items that have been successfully processed
|
|
331
|
+
* Returns the number of items that have been successfully processed.
|
|
303
332
|
*/
|
|
304
333
|
getSuccessCount() {
|
|
305
334
|
return this._successCount;
|
|
306
335
|
}
|
|
307
336
|
/**
|
|
308
|
-
* Returns the number of items that have failed processing
|
|
337
|
+
* Returns the number of items that have failed processing.
|
|
309
338
|
*/
|
|
310
339
|
getErrorCount() {
|
|
311
340
|
return this._errorCount;
|
|
312
341
|
}
|
|
313
342
|
/**
|
|
314
|
-
* Returns the number of items that have completed processing (success or error)
|
|
343
|
+
* Returns the number of items that have completed processing (success or error).
|
|
315
344
|
*/
|
|
316
345
|
getSettledCount() {
|
|
317
346
|
return this._settledCount;
|
|
318
347
|
}
|
|
319
348
|
/**
|
|
320
|
-
* Returns the number of items that have been rejected from the
|
|
349
|
+
* Returns the number of items that have been rejected from being added to the queue.
|
|
321
350
|
*/
|
|
322
351
|
getRejectionCount() {
|
|
323
352
|
return this._rejectionCount;
|
|
324
353
|
}
|
|
325
354
|
/**
|
|
326
|
-
* Returns true if the queuer is running
|
|
355
|
+
* Returns true if the queuer is currently running (processing items).
|
|
327
356
|
*/
|
|
328
357
|
getIsRunning() {
|
|
329
358
|
return this._running;
|
|
330
359
|
}
|
|
331
360
|
/**
|
|
332
|
-
* Returns true if the queuer is running but has no items to process
|
|
361
|
+
* Returns true if the queuer is running but has no items to process and no active tasks.
|
|
333
362
|
*/
|
|
334
363
|
getIsIdle() {
|
|
335
364
|
return this._running && this.getIsEmpty() && this._activeItems.size === 0;
|
|
336
365
|
}
|
|
337
366
|
/**
|
|
338
|
-
* Returns the number of items that have expired from the
|
|
367
|
+
* Returns the number of items that have expired and been removed from the queue.
|
|
339
368
|
*/
|
|
340
369
|
getExpirationCount() {
|
|
341
370
|
return this._expirationCount;
|
|
342
371
|
}
|
|
343
372
|
}
|
|
344
|
-
function asyncQueue(
|
|
345
|
-
const
|
|
346
|
-
return
|
|
373
|
+
function asyncQueue(fn, initialOptions) {
|
|
374
|
+
const asyncQueuer = new AsyncQueuer(fn, initialOptions);
|
|
375
|
+
return asyncQueuer.addItem.bind(asyncQueuer);
|
|
347
376
|
}
|
|
348
377
|
exports.AsyncQueuer = AsyncQueuer;
|
|
349
378
|
exports.asyncQueue = asyncQueue;
|