@tanstack/pacer 0.2.0 → 0.3.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 +78 -45
- package/dist/cjs/async-debouncer.cjs.map +1 -1
- package/dist/cjs/async-debouncer.d.cts +44 -16
- package/dist/cjs/async-queuer.cjs +53 -3
- package/dist/cjs/async-queuer.cjs.map +1 -1
- package/dist/cjs/async-queuer.d.cts +28 -3
- package/dist/cjs/async-rate-limiter.cjs +46 -35
- package/dist/cjs/async-rate-limiter.cjs.map +1 -1
- package/dist/cjs/async-rate-limiter.d.cts +34 -19
- package/dist/cjs/async-throttler.cjs +85 -57
- package/dist/cjs/async-throttler.cjs.map +1 -1
- package/dist/cjs/async-throttler.d.cts +49 -17
- package/dist/cjs/debouncer.cjs +12 -14
- package/dist/cjs/debouncer.cjs.map +1 -1
- package/dist/cjs/debouncer.d.cts +10 -9
- package/dist/cjs/queuer.cjs +51 -1
- package/dist/cjs/queuer.cjs.map +1 -1
- package/dist/cjs/queuer.d.cts +30 -1
- package/dist/cjs/rate-limiter.cjs +1 -5
- package/dist/cjs/rate-limiter.cjs.map +1 -1
- package/dist/cjs/rate-limiter.d.cts +9 -9
- package/dist/cjs/throttler.cjs +29 -36
- package/dist/cjs/throttler.cjs.map +1 -1
- package/dist/cjs/throttler.d.cts +16 -17
- package/dist/cjs/types.d.cts +2 -6
- package/dist/cjs/utils.cjs.map +1 -1
- package/dist/cjs/utils.d.cts +1 -1
- package/dist/esm/async-debouncer.d.ts +44 -16
- package/dist/esm/async-debouncer.js +78 -45
- package/dist/esm/async-debouncer.js.map +1 -1
- package/dist/esm/async-queuer.d.ts +28 -3
- package/dist/esm/async-queuer.js +53 -3
- package/dist/esm/async-queuer.js.map +1 -1
- package/dist/esm/async-rate-limiter.d.ts +34 -19
- package/dist/esm/async-rate-limiter.js +46 -35
- package/dist/esm/async-rate-limiter.js.map +1 -1
- package/dist/esm/async-throttler.d.ts +49 -17
- package/dist/esm/async-throttler.js +85 -57
- package/dist/esm/async-throttler.js.map +1 -1
- package/dist/esm/debouncer.d.ts +10 -9
- package/dist/esm/debouncer.js +12 -14
- package/dist/esm/debouncer.js.map +1 -1
- package/dist/esm/queuer.d.ts +30 -1
- package/dist/esm/queuer.js +51 -1
- package/dist/esm/queuer.js.map +1 -1
- package/dist/esm/rate-limiter.d.ts +9 -9
- package/dist/esm/rate-limiter.js +1 -5
- package/dist/esm/rate-limiter.js.map +1 -1
- package/dist/esm/throttler.d.ts +16 -17
- package/dist/esm/throttler.js +29 -36
- package/dist/esm/throttler.js.map +1 -1
- package/dist/esm/types.d.ts +2 -6
- package/dist/esm/utils.d.ts +1 -1
- package/dist/esm/utils.js.map +1 -1
- package/package.json +1 -1
- package/src/async-debouncer.ts +114 -73
- package/src/async-queuer.ts +93 -8
- package/src/async-rate-limiter.ts +74 -60
- package/src/async-throttler.ts +135 -86
- package/src/debouncer.ts +26 -33
- package/src/queuer.ts +92 -4
- package/src/rate-limiter.ts +14 -26
- package/src/throttler.ts +45 -53
- package/src/types.ts +2 -10
- package/src/utils.ts +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"throttler.js","sources":["../../src/throttler.ts"],"sourcesContent":["import type { AnyFunction } from './types'\n\n/**\n * Options for configuring a throttled function\n */\nexport interface ThrottlerOptions
|
|
1
|
+
{"version":3,"file":"throttler.js","sources":["../../src/throttler.ts"],"sourcesContent":["import type { AnyFunction } from './types'\n\n/**\n * Options for configuring a throttled function\n */\nexport interface ThrottlerOptions<TFn extends AnyFunction> {\n /**\n * Whether the throttler is enabled. When disabled, maybeExecute will not trigger any executions.\n * Defaults to true.\n */\n enabled?: boolean\n /**\n * Whether to execute on the leading edge of the timeout.\n * Defaults to true.\n */\n leading?: boolean\n /**\n * Callback function that is called after the function is executed\n */\n onExecute?: (throttler: Throttler<TFn>) => void\n /**\n * Whether to execute on the trailing edge of the timeout.\n * Defaults to true.\n */\n trailing?: boolean\n /**\n * Time window in milliseconds during which the function can only be executed once\n */\n wait: number\n}\n\nconst defaultOptions: Required<ThrottlerOptions<any>> = {\n enabled: true,\n leading: true,\n onExecute: () => {},\n trailing: true,\n wait: 0,\n}\n\n/**\n * A class that creates a throttled function.\n *\n * Throttling ensures a function is called at most once within a specified time window.\n * Unlike debouncing which waits for a pause in calls, throttling guarantees consistent\n * execution timing regardless of call frequency.\n *\n * Supports both leading and trailing edge execution:\n * - Leading: Execute immediately on first call (default: true)\n * - Trailing: Execute after wait period if called during throttle (default: true)\n *\n * For collapsing rapid-fire events where you only care about the last call, consider using Debouncer.\n *\n * @example\n * ```ts\n * const throttler = new Throttler(\n * (id: string) => api.getData(id),\n * { wait: 1000 } // Execute at most once per second\n * );\n *\n * // First call executes immediately\n * throttler.maybeExecute('123');\n *\n * // Subsequent calls within 1000ms are throttled\n * throttler.maybeExecute('123'); // Throttled\n * ```\n */\nexport class Throttler<TFn extends AnyFunction> {\n private _executionCount = 0\n private _lastArgs: Parameters<TFn> | undefined\n private _lastExecutionTime = 0\n private _options: Required<ThrottlerOptions<TFn>>\n private _timeoutId: NodeJS.Timeout | undefined\n\n constructor(\n private fn: TFn,\n initialOptions: ThrottlerOptions<TFn>,\n ) {\n this._options = {\n ...defaultOptions,\n ...initialOptions,\n }\n }\n\n /**\n * Updates the throttler options\n * Returns the new options state\n */\n setOptions(newOptions: Partial<ThrottlerOptions<TFn>>): void {\n this._options = { ...this._options, ...newOptions }\n\n // End the pending state if the debouncer is disabled\n if (!this._options.enabled) {\n this.cancel()\n }\n }\n\n /**\n * Returns the current throttler options\n */\n getOptions(): Required<ThrottlerOptions<TFn>> {\n return this._options\n }\n\n /**\n * Attempts to execute the throttled function. The execution behavior depends on the throttler options:\n *\n * - If enough time has passed since the last execution (>= wait period):\n * - With leading=true: Executes immediately\n * - With leading=false: Waits for the next trailing execution\n *\n * - If within the wait period:\n * - With trailing=true: Schedules execution for end of wait period\n * - With trailing=false: Drops the execution\n *\n * @example\n * ```ts\n * const throttled = new Throttler(fn, { wait: 1000 });\n *\n * // First call executes immediately\n * throttled.maybeExecute('a', 'b');\n *\n * // Call during wait period - gets throttled\n * throttled.maybeExecute('c', 'd');\n * ```\n */\n maybeExecute(...args: Parameters<TFn>): void {\n const now = Date.now()\n const timeSinceLastExecution = now - this._lastExecutionTime\n\n // Handle leading execution\n if (this._options.leading && timeSinceLastExecution >= this._options.wait) {\n this.executeFunction(...args)\n } else {\n // Store the most recent arguments for potential trailing execution\n this._lastArgs = args\n\n // Set up trailing execution if not already scheduled\n if (!this._timeoutId && this._options.trailing) {\n const _timeSinceLastExecution = this._lastExecutionTime\n ? now - this._lastExecutionTime\n : 0\n const timeoutDuration = this._options.wait - _timeSinceLastExecution\n this._timeoutId = setTimeout(() => {\n if (this._lastArgs !== undefined) {\n this.executeFunction(...this._lastArgs)\n }\n }, timeoutDuration)\n }\n }\n }\n\n private executeFunction(...args: Parameters<TFn>): void {\n if (!this._options.enabled) return\n this.fn(...args) // EXECUTE!\n this._executionCount++\n this._lastExecutionTime = Date.now()\n this._timeoutId = undefined\n this._lastArgs = undefined\n this._options.onExecute(this)\n }\n\n /**\n * Cancels any pending trailing execution and clears internal state.\n *\n * If a trailing execution is scheduled (due to throttling with trailing=true),\n * this will prevent that execution from occurring. The internal timeout and\n * stored arguments will be cleared.\n *\n * Has no effect if there is no pending execution.\n */\n cancel(): void {\n if (this._timeoutId) {\n clearTimeout(this._timeoutId)\n this._timeoutId = undefined\n this._lastArgs = undefined\n }\n }\n\n /**\n * Returns the last execution time\n */\n getLastExecutionTime(): number {\n return this._lastExecutionTime\n }\n\n /**\n * Returns the next execution time\n */\n getNextExecutionTime(): number {\n return this._lastExecutionTime + this._options.wait\n }\n\n /**\n * Returns the number of times the function has been executed\n */\n getExecutionCount(): number {\n return this._executionCount\n }\n\n /**\n * Returns `true` if there is a pending execution\n */\n getIsPending(): boolean {\n return this._options.enabled && !!this._timeoutId\n }\n}\n\n/**\n * Creates a throttled function that limits how often the provided function can execute.\n *\n * Throttling ensures a function executes at most once within a specified time window,\n * regardless of how many times it is called. This is useful for rate-limiting\n * expensive operations or UI updates.\n *\n * The throttled function can be configured to execute on the leading and/or trailing\n * edge of the throttle window via options.\n *\n * For handling bursts of events, consider using debounce() instead. For hard execution\n * limits, consider using rateLimit().\n *\n * @example\n * ```ts\n * // Basic throttling - max once per second\n * const throttled = throttle(updateUI, { wait: 1000 });\n *\n * // Configure leading/trailing execution\n * const throttled = throttle(saveData, {\n * wait: 2000,\n * leading: true, // Execute immediately on first call\n * trailing: true // Execute again after delay if called during wait\n * });\n * ```\n */\nexport function throttle<TFn extends AnyFunction>(\n fn: TFn,\n initialOptions: Omit<ThrottlerOptions<TFn>, 'enabled'>,\n) {\n const throttler = new Throttler(fn, initialOptions)\n return throttler.maybeExecute.bind(throttler)\n}\n"],"names":[],"mappings":"AA+BA,MAAM,iBAAkD;AAAA,EACtD,SAAS;AAAA,EACT,SAAS;AAAA,EACT,WAAW,MAAM;AAAA,EAAC;AAAA,EAClB,UAAU;AAAA,EACV,MAAM;AACR;AA6BO,MAAM,UAAmC;AAAA,EAO9C,YACU,IACR,gBACA;AAFQ,SAAA,KAAA;AAPV,SAAQ,kBAAkB;AAE1B,SAAQ,qBAAqB;AAQ3B,SAAK,WAAW;AAAA,MACd,GAAG;AAAA,MACH,GAAG;AAAA,IACL;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOF,WAAW,YAAkD;AAC3D,SAAK,WAAW,EAAE,GAAG,KAAK,UAAU,GAAG,WAAW;AAG9C,QAAA,CAAC,KAAK,SAAS,SAAS;AAC1B,WAAK,OAAO;AAAA,IAAA;AAAA,EACd;AAAA;AAAA;AAAA;AAAA,EAMF,aAA8C;AAC5C,WAAO,KAAK;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAyBd,gBAAgB,MAA6B;AACrC,UAAA,MAAM,KAAK,IAAI;AACf,UAAA,yBAAyB,MAAM,KAAK;AAG1C,QAAI,KAAK,SAAS,WAAW,0BAA0B,KAAK,SAAS,MAAM;AACpE,WAAA,gBAAgB,GAAG,IAAI;AAAA,IAAA,OACvB;AAEL,WAAK,YAAY;AAGjB,UAAI,CAAC,KAAK,cAAc,KAAK,SAAS,UAAU;AAC9C,cAAM,0BAA0B,KAAK,qBACjC,MAAM,KAAK,qBACX;AACE,cAAA,kBAAkB,KAAK,SAAS,OAAO;AACxC,aAAA,aAAa,WAAW,MAAM;AAC7B,cAAA,KAAK,cAAc,QAAW;AAC3B,iBAAA,gBAAgB,GAAG,KAAK,SAAS;AAAA,UAAA;AAAA,WAEvC,eAAe;AAAA,MAAA;AAAA,IACpB;AAAA,EACF;AAAA,EAGM,mBAAmB,MAA6B;AAClD,QAAA,CAAC,KAAK,SAAS,QAAS;AACvB,SAAA,GAAG,GAAG,IAAI;AACV,SAAA;AACA,SAAA,qBAAqB,KAAK,IAAI;AACnC,SAAK,aAAa;AAClB,SAAK,YAAY;AACZ,SAAA,SAAS,UAAU,IAAI;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAY9B,SAAe;AACb,QAAI,KAAK,YAAY;AACnB,mBAAa,KAAK,UAAU;AAC5B,WAAK,aAAa;AAClB,WAAK,YAAY;AAAA,IAAA;AAAA,EACnB;AAAA;AAAA;AAAA;AAAA,EAMF,uBAA+B;AAC7B,WAAO,KAAK;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA,EAMd,uBAA+B;AACtB,WAAA,KAAK,qBAAqB,KAAK,SAAS;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA,EAMjD,oBAA4B;AAC1B,WAAO,KAAK;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA,EAMd,eAAwB;AACtB,WAAO,KAAK,SAAS,WAAW,CAAC,CAAC,KAAK;AAAA,EAAA;AAE3C;AA4BgB,SAAA,SACd,IACA,gBACA;AACA,QAAM,YAAY,IAAI,UAAU,IAAI,cAAc;AAC3C,SAAA,UAAU,aAAa,KAAK,SAAS;AAC9C;"}
|
package/dist/esm/types.d.ts
CHANGED
|
@@ -1,12 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Represents a function that can be called with any arguments and returns any value.
|
|
3
|
-
* @template TArgs - The type of the arguments the function can be called with.
|
|
4
|
-
* @returns The return value of the function.
|
|
5
3
|
*/
|
|
6
|
-
export type AnyFunction
|
|
4
|
+
export type AnyFunction = (...args: Array<any>) => any;
|
|
7
5
|
/**
|
|
8
6
|
* Represents an asynchronous function that can be called with any arguments and returns a promise.
|
|
9
|
-
* @template TArgs - The type of the arguments the function can be called with.
|
|
10
|
-
* @returns A promise that resolves to the return value of the function.
|
|
11
7
|
*/
|
|
12
|
-
export type AnyAsyncFunction
|
|
8
|
+
export type AnyAsyncFunction = (...args: Array<any>) => Promise<any>;
|
package/dist/esm/utils.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare function bindInstanceMethods<T extends Record<string, any>>(instance: T):
|
|
1
|
+
export declare function bindInstanceMethods<T extends Record<string, any>>(instance: T): T;
|
package/dist/esm/utils.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"utils.js","sources":["../../src/utils.ts"],"sourcesContent":["export function bindInstanceMethods<T extends Record<string, any>>(\n instance: T,\n) {\n return Object.getOwnPropertyNames(Object.getPrototypeOf(instance))\n .filter((key) => typeof instance[key as keyof T] === 'function')\n .reduce((acc: any, key) => {\n const method = instance[key as keyof T]\n if (typeof method === 'function') {\n acc[key] = method.bind(instance)\n }\n return acc\n }, {} as T)\n}\n"],"names":[],"mappings":"AAAO,SAAS,oBACd,
|
|
1
|
+
{"version":3,"file":"utils.js","sources":["../../src/utils.ts"],"sourcesContent":["export function bindInstanceMethods<T extends Record<string, any>>(\n instance: T,\n): T {\n return Object.getOwnPropertyNames(Object.getPrototypeOf(instance))\n .filter((key) => typeof instance[key as keyof T] === 'function')\n .reduce((acc: any, key) => {\n const method = instance[key as keyof T]\n if (typeof method === 'function') {\n acc[key] = method.bind(instance)\n }\n return acc\n }, {} as T)\n}\n"],"names":[],"mappings":"AAAO,SAAS,oBACd,UACG;AACH,SAAO,OAAO,oBAAoB,OAAO,eAAe,QAAQ,CAAC,EAC9D,OAAO,CAAC,QAAQ,OAAO,SAAS,GAAc,MAAM,UAAU,EAC9D,OAAO,CAAC,KAAU,QAAQ;AACnB,UAAA,SAAS,SAAS,GAAc;AAClC,QAAA,OAAO,WAAW,YAAY;AAChC,UAAI,GAAG,IAAI,OAAO,KAAK,QAAQ;AAAA,IAAA;AAE1B,WAAA;AAAA,EACT,GAAG,EAAO;AACd;"}
|
package/package.json
CHANGED
package/src/async-debouncer.ts
CHANGED
|
@@ -3,10 +3,7 @@ import type { AnyAsyncFunction } from './types'
|
|
|
3
3
|
/**
|
|
4
4
|
* Options for configuring an async debounced function
|
|
5
5
|
*/
|
|
6
|
-
export interface AsyncDebouncerOptions<
|
|
7
|
-
TFn extends AnyAsyncFunction,
|
|
8
|
-
TArgs extends Parameters<TFn>,
|
|
9
|
-
> {
|
|
6
|
+
export interface AsyncDebouncerOptions<TFn extends AnyAsyncFunction> {
|
|
10
7
|
/**
|
|
11
8
|
* Whether the debouncer is enabled. When disabled, maybeExecute will not trigger any executions.
|
|
12
9
|
* Defaults to true.
|
|
@@ -20,11 +17,15 @@ export interface AsyncDebouncerOptions<
|
|
|
20
17
|
/**
|
|
21
18
|
* Optional error handler for when the debounced function throws
|
|
22
19
|
*/
|
|
23
|
-
onError?: (error: unknown) => void
|
|
20
|
+
onError?: (error: unknown, debouncer: AsyncDebouncer<TFn>) => void
|
|
24
21
|
/**
|
|
25
|
-
* Optional
|
|
22
|
+
* Optional callback to call when the debounced function is executed
|
|
26
23
|
*/
|
|
27
|
-
|
|
24
|
+
onSettled?: (debouncer: AsyncDebouncer<TFn>) => void
|
|
25
|
+
/**
|
|
26
|
+
* Optional callback to call when the debounced function is executed
|
|
27
|
+
*/
|
|
28
|
+
onSuccess?: (result: ReturnType<TFn>, debouncer: AsyncDebouncer<TFn>) => void
|
|
28
29
|
/**
|
|
29
30
|
* Whether to execute on the trailing edge of the timeout.
|
|
30
31
|
* Defaults to true.
|
|
@@ -37,12 +38,13 @@ export interface AsyncDebouncerOptions<
|
|
|
37
38
|
wait: number
|
|
38
39
|
}
|
|
39
40
|
|
|
40
|
-
const defaultOptions: Required<AsyncDebouncerOptions<any
|
|
41
|
+
const defaultOptions: Required<AsyncDebouncerOptions<any>> = {
|
|
41
42
|
enabled: true,
|
|
42
43
|
leading: false,
|
|
43
|
-
trailing: true,
|
|
44
44
|
onError: () => {},
|
|
45
|
-
|
|
45
|
+
onSettled: () => {},
|
|
46
|
+
onSuccess: () => {},
|
|
47
|
+
trailing: true,
|
|
46
48
|
wait: 0,
|
|
47
49
|
}
|
|
48
50
|
|
|
@@ -68,21 +70,22 @@ const defaultOptions: Required<AsyncDebouncerOptions<any, any>> = {
|
|
|
68
70
|
* });
|
|
69
71
|
* ```
|
|
70
72
|
*/
|
|
71
|
-
export class AsyncDebouncer<
|
|
72
|
-
TFn extends AnyAsyncFunction,
|
|
73
|
-
TArgs extends Parameters<TFn>,
|
|
74
|
-
> {
|
|
73
|
+
export class AsyncDebouncer<TFn extends AnyAsyncFunction> {
|
|
75
74
|
private _abortController: AbortController | null = null
|
|
76
|
-
private _executionCount = 0
|
|
77
|
-
private _isExecuting = false
|
|
78
|
-
private _lastArgs: TArgs | undefined
|
|
79
|
-
private _options: Required<AsyncDebouncerOptions<TFn, TArgs>>
|
|
80
|
-
private _timeoutId: ReturnType<typeof setTimeout> | null = null
|
|
81
75
|
private _canLeadingExecute = true
|
|
76
|
+
private _errorCount = 0
|
|
77
|
+
private _isExecuting = false
|
|
78
|
+
private _isPending = false
|
|
79
|
+
private _lastArgs: Parameters<TFn> | undefined
|
|
80
|
+
private _lastResult: ReturnType<TFn> | undefined
|
|
81
|
+
private _options: Required<AsyncDebouncerOptions<TFn>>
|
|
82
|
+
private _settleCount = 0
|
|
83
|
+
private _successCount = 0
|
|
84
|
+
private _timeoutId: NodeJS.Timeout | null = null
|
|
82
85
|
|
|
83
86
|
constructor(
|
|
84
87
|
private fn: TFn,
|
|
85
|
-
initialOptions: AsyncDebouncerOptions<TFn
|
|
88
|
+
initialOptions: AsyncDebouncerOptions<TFn>,
|
|
86
89
|
) {
|
|
87
90
|
this._options = {
|
|
88
91
|
...defaultOptions,
|
|
@@ -94,20 +97,19 @@ export class AsyncDebouncer<
|
|
|
94
97
|
* Updates the debouncer options
|
|
95
98
|
* Returns the new options state
|
|
96
99
|
*/
|
|
97
|
-
setOptions(
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
100
|
+
setOptions(newOptions: Partial<AsyncDebouncerOptions<TFn>>): void {
|
|
101
|
+
this._options = { ...this._options, ...newOptions }
|
|
102
|
+
|
|
103
|
+
// End the pending state if the debouncer is disabled
|
|
104
|
+
if (!this._options.enabled) {
|
|
105
|
+
this._isPending = false
|
|
103
106
|
}
|
|
104
|
-
return this._options
|
|
105
107
|
}
|
|
106
108
|
|
|
107
109
|
/**
|
|
108
110
|
* Returns the current debouncer options
|
|
109
111
|
*/
|
|
110
|
-
getOptions(): Required<AsyncDebouncerOptions<TFn
|
|
112
|
+
getOptions(): Required<AsyncDebouncerOptions<TFn>> {
|
|
111
113
|
return this._options
|
|
112
114
|
}
|
|
113
115
|
|
|
@@ -115,61 +117,65 @@ export class AsyncDebouncer<
|
|
|
115
117
|
* Attempts to execute the debounced function
|
|
116
118
|
* If a call is already in progress, it will be queued
|
|
117
119
|
*/
|
|
118
|
-
async maybeExecute(
|
|
119
|
-
|
|
120
|
+
async maybeExecute(
|
|
121
|
+
...args: Parameters<TFn>
|
|
122
|
+
): Promise<ReturnType<TFn> | undefined> {
|
|
123
|
+
this._cancel()
|
|
120
124
|
this._lastArgs = args
|
|
121
125
|
|
|
122
126
|
// Handle leading execution
|
|
123
127
|
if (this._options.leading && this._canLeadingExecute) {
|
|
124
128
|
this._canLeadingExecute = false
|
|
125
129
|
await this.executeFunction(...args)
|
|
130
|
+
return this._lastResult
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Handle trailing execution
|
|
134
|
+
if (this._options.trailing) {
|
|
135
|
+
this._isPending = true
|
|
126
136
|
}
|
|
127
137
|
|
|
128
138
|
return new Promise((resolve) => {
|
|
129
139
|
this._timeoutId = setTimeout(async () => {
|
|
130
|
-
if
|
|
131
|
-
|
|
132
|
-
|
|
140
|
+
// Execute trailing if enabled
|
|
141
|
+
if (this._options.trailing && this._lastArgs) {
|
|
142
|
+
await this.executeFunction(...this._lastArgs)
|
|
133
143
|
}
|
|
134
144
|
|
|
145
|
+
// Reset state and resolve
|
|
135
146
|
this._canLeadingExecute = true
|
|
136
|
-
|
|
137
|
-
if (this._options.trailing) {
|
|
138
|
-
this._abortController = new AbortController()
|
|
139
|
-
try {
|
|
140
|
-
this._isExecuting = true
|
|
141
|
-
if (this._lastArgs) {
|
|
142
|
-
await this.executeFunction(...this._lastArgs)
|
|
143
|
-
}
|
|
144
|
-
} catch (error) {
|
|
145
|
-
try {
|
|
146
|
-
this._options.onError(error)
|
|
147
|
-
} catch {
|
|
148
|
-
console.error('Error in error handler', error)
|
|
149
|
-
}
|
|
150
|
-
} finally {
|
|
151
|
-
this._isExecuting = false
|
|
152
|
-
this._abortController = null
|
|
153
|
-
resolve()
|
|
154
|
-
}
|
|
155
|
-
} else {
|
|
156
|
-
resolve()
|
|
157
|
-
}
|
|
147
|
+
resolve(this._lastResult)
|
|
158
148
|
}, this._options.wait)
|
|
159
149
|
})
|
|
160
150
|
}
|
|
161
151
|
|
|
162
|
-
private async executeFunction(
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
this.
|
|
152
|
+
private async executeFunction(
|
|
153
|
+
...args: Parameters<TFn>
|
|
154
|
+
): Promise<ReturnType<TFn> | undefined> {
|
|
155
|
+
if (!this._options.enabled) return undefined
|
|
156
|
+
this._abortController = new AbortController()
|
|
157
|
+
try {
|
|
158
|
+
this._isExecuting = true
|
|
159
|
+
this._lastResult = await this.fn(...args) // EXECUTE!
|
|
160
|
+
this._successCount++
|
|
161
|
+
this._options.onSuccess(this._lastResult!, this)
|
|
162
|
+
} catch (error) {
|
|
163
|
+
this._errorCount++
|
|
164
|
+
this._options.onError(error, this)
|
|
165
|
+
} finally {
|
|
166
|
+
this._isExecuting = false
|
|
167
|
+
this._isPending = false
|
|
168
|
+
this._settleCount++
|
|
169
|
+
this._abortController = null
|
|
170
|
+
this._options.onSettled(this)
|
|
171
|
+
}
|
|
172
|
+
return this._lastResult
|
|
167
173
|
}
|
|
168
174
|
|
|
169
175
|
/**
|
|
170
|
-
*
|
|
176
|
+
* Cancel without resetting _canLeadingExecute
|
|
171
177
|
*/
|
|
172
|
-
|
|
178
|
+
private _cancel(): void {
|
|
173
179
|
if (this._timeoutId) {
|
|
174
180
|
clearTimeout(this._timeoutId)
|
|
175
181
|
this._timeoutId = null
|
|
@@ -179,23 +185,58 @@ export class AsyncDebouncer<
|
|
|
179
185
|
this._abortController = null
|
|
180
186
|
}
|
|
181
187
|
this._lastArgs = undefined
|
|
188
|
+
this._isPending = false
|
|
189
|
+
this._isExecuting = false
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Cancels any pending execution or aborts any execution in progress
|
|
194
|
+
*/
|
|
195
|
+
cancel(): void {
|
|
182
196
|
this._canLeadingExecute = true
|
|
197
|
+
this._cancel()
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Returns the last result of the debounced function
|
|
202
|
+
*/
|
|
203
|
+
getLastResult(): ReturnType<TFn> | undefined {
|
|
204
|
+
return this._lastResult
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Returns the number of times the function has been executed successfully
|
|
209
|
+
*/
|
|
210
|
+
getSuccessCount(): number {
|
|
211
|
+
return this._successCount
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Returns the number of times the function has settled (completed or errored)
|
|
216
|
+
*/
|
|
217
|
+
getSettleCount(): number {
|
|
218
|
+
return this._settleCount
|
|
183
219
|
}
|
|
184
220
|
|
|
185
221
|
/**
|
|
186
|
-
* Returns the number of times the function has
|
|
222
|
+
* Returns the number of times the function has errored
|
|
187
223
|
*/
|
|
188
|
-
|
|
189
|
-
return this.
|
|
224
|
+
getErrorCount(): number {
|
|
225
|
+
return this._errorCount
|
|
190
226
|
}
|
|
191
227
|
|
|
192
228
|
/**
|
|
193
|
-
* Returns `true` if there is a pending execution
|
|
229
|
+
* Returns `true` if there is a pending execution queued up for trailing execution
|
|
194
230
|
*/
|
|
195
231
|
getIsPending(): boolean {
|
|
196
|
-
return
|
|
197
|
-
|
|
198
|
-
|
|
232
|
+
return this._options.enabled && this._isPending
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Returns `true` if there is currently an execution in progress
|
|
237
|
+
*/
|
|
238
|
+
getIsExecuting(): boolean {
|
|
239
|
+
return this._isExecuting
|
|
199
240
|
}
|
|
200
241
|
}
|
|
201
242
|
|
|
@@ -216,10 +257,10 @@ export class AsyncDebouncer<
|
|
|
216
257
|
* await debounced("third"); // Executes after 1s
|
|
217
258
|
* ```
|
|
218
259
|
*/
|
|
219
|
-
export function asyncDebounce<
|
|
220
|
-
TFn
|
|
221
|
-
|
|
222
|
-
|
|
260
|
+
export function asyncDebounce<TFn extends AnyAsyncFunction>(
|
|
261
|
+
fn: TFn,
|
|
262
|
+
initialOptions: Omit<AsyncDebouncerOptions<TFn>, 'enabled'>,
|
|
263
|
+
) {
|
|
223
264
|
const asyncDebouncer = new AsyncDebouncer(fn, initialOptions)
|
|
224
265
|
return asyncDebouncer.maybeExecute.bind(asyncDebouncer)
|
|
225
266
|
}
|
package/src/async-queuer.ts
CHANGED
|
@@ -10,6 +10,16 @@ export interface AsyncQueuerOptions<TValue> {
|
|
|
10
10
|
* Maximum number of concurrent tasks to process
|
|
11
11
|
*/
|
|
12
12
|
concurrency?: number
|
|
13
|
+
/**
|
|
14
|
+
* Maximum time in milliseconds that an item can stay in the queue
|
|
15
|
+
* If not provided, items will never expire
|
|
16
|
+
*/
|
|
17
|
+
expirationDuration?: number
|
|
18
|
+
/**
|
|
19
|
+
* Function to determine if an item has expired
|
|
20
|
+
* If provided, this overrides the expirationDuration behavior
|
|
21
|
+
*/
|
|
22
|
+
getIsExpired?: (item: () => Promise<TValue>, addedAt: number) => boolean
|
|
13
23
|
/**
|
|
14
24
|
* Default position to get items from during processing
|
|
15
25
|
* @default 'front'
|
|
@@ -49,7 +59,11 @@ export interface AsyncQueuerOptions<TValue> {
|
|
|
49
59
|
*/
|
|
50
60
|
onReject?: (item: () => Promise<TValue>, queuer: AsyncQueuer<TValue>) => void
|
|
51
61
|
/**
|
|
52
|
-
*
|
|
62
|
+
* Callback fired whenever an item expires in the queuer
|
|
63
|
+
*/
|
|
64
|
+
onExpire?: (item: () => Promise<TValue>, queuer: AsyncQueuer<TValue>) => void
|
|
65
|
+
/**
|
|
66
|
+
* Whether the queuer should start processing tasks immediately or not.
|
|
53
67
|
*/
|
|
54
68
|
started?: boolean
|
|
55
69
|
/**
|
|
@@ -61,6 +75,8 @@ export interface AsyncQueuerOptions<TValue> {
|
|
|
61
75
|
const defaultOptions: Required<AsyncQueuerOptions<any>> = {
|
|
62
76
|
addItemsTo: 'back',
|
|
63
77
|
concurrency: 1,
|
|
78
|
+
expirationDuration: Infinity,
|
|
79
|
+
getIsExpired: () => false,
|
|
64
80
|
getItemsFrom: 'front',
|
|
65
81
|
getPriority: (item) => (item as any)?.priority ?? 0,
|
|
66
82
|
initialItems: [],
|
|
@@ -69,7 +85,8 @@ const defaultOptions: Required<AsyncQueuerOptions<any>> = {
|
|
|
69
85
|
onIsRunningChange: () => {},
|
|
70
86
|
onItemsChange: () => {},
|
|
71
87
|
onReject: () => {},
|
|
72
|
-
|
|
88
|
+
onExpire: () => {},
|
|
89
|
+
started: true,
|
|
73
90
|
wait: 0,
|
|
74
91
|
}
|
|
75
92
|
|
|
@@ -83,6 +100,7 @@ const defaultOptions: Required<AsyncQueuerOptions<any>> = {
|
|
|
83
100
|
* - FIFO (First In First Out) or LIFO (Last In First Out) queue behavior
|
|
84
101
|
* - Pause/resume task processing
|
|
85
102
|
* - Task cancellation
|
|
103
|
+
* - Item expiration to clear stale items from the queue
|
|
86
104
|
*
|
|
87
105
|
* Tasks are processed concurrently up to the configured concurrency limit. When a task completes,
|
|
88
106
|
* the next pending task is processed if below the concurrency limit.
|
|
@@ -107,7 +125,9 @@ export class AsyncQueuer<TValue> {
|
|
|
107
125
|
private _activeItems: Set<() => Promise<TValue>> = new Set()
|
|
108
126
|
private _executionCount = 0
|
|
109
127
|
private _rejectionCount = 0
|
|
128
|
+
private _expirationCount = 0
|
|
110
129
|
private _items: Array<() => Promise<TValue>> = []
|
|
130
|
+
private _itemTimestamps: Array<number> = []
|
|
111
131
|
private _onErrorCallbacks: Array<(error: Error) => void> = []
|
|
112
132
|
private _onSettledCallbacks: Array<(result: TValue | Error) => void> = []
|
|
113
133
|
private _onSuccessCallbacks: Array<(result: TValue) => void> = []
|
|
@@ -129,11 +149,8 @@ export class AsyncQueuer<TValue> {
|
|
|
129
149
|
* Updates the queuer options
|
|
130
150
|
* Returns the new options state
|
|
131
151
|
*/
|
|
132
|
-
setOptions(
|
|
133
|
-
newOptions: Partial<AsyncQueuerOptions<TValue>>,
|
|
134
|
-
): AsyncQueuerOptions<TValue> {
|
|
152
|
+
setOptions(newOptions: Partial<AsyncQueuerOptions<TValue>>): void {
|
|
135
153
|
this._options = { ...this._options, ...newOptions }
|
|
136
|
-
return this._options
|
|
137
154
|
}
|
|
138
155
|
|
|
139
156
|
/**
|
|
@@ -152,6 +169,9 @@ export class AsyncQueuer<TValue> {
|
|
|
152
169
|
return
|
|
153
170
|
}
|
|
154
171
|
|
|
172
|
+
// Check for expired items
|
|
173
|
+
this.checkExpiredItems()
|
|
174
|
+
|
|
155
175
|
while (
|
|
156
176
|
this._activeItems.size < this._options.concurrency &&
|
|
157
177
|
!this.getIsEmpty()
|
|
@@ -196,6 +216,56 @@ export class AsyncQueuer<TValue> {
|
|
|
196
216
|
this._pendingTick = false
|
|
197
217
|
}
|
|
198
218
|
|
|
219
|
+
/**
|
|
220
|
+
* Checks for and removes expired items from the queuer
|
|
221
|
+
*/
|
|
222
|
+
private checkExpiredItems() {
|
|
223
|
+
if (
|
|
224
|
+
this._options.expirationDuration === Infinity &&
|
|
225
|
+
this._options.getIsExpired === defaultOptions.getIsExpired
|
|
226
|
+
)
|
|
227
|
+
return
|
|
228
|
+
|
|
229
|
+
const now = Date.now()
|
|
230
|
+
const expiredIndices: Array<number> = []
|
|
231
|
+
|
|
232
|
+
// Find indices of expired items
|
|
233
|
+
for (let i = 0; i < this._items.length; i++) {
|
|
234
|
+
const timestamp = this._itemTimestamps[i]
|
|
235
|
+
if (timestamp === undefined) continue
|
|
236
|
+
|
|
237
|
+
const item = this._items[i]
|
|
238
|
+
if (item === undefined) continue
|
|
239
|
+
|
|
240
|
+
const isExpired =
|
|
241
|
+
this._options.getIsExpired !== defaultOptions.getIsExpired
|
|
242
|
+
? this._options.getIsExpired(item, timestamp)
|
|
243
|
+
: now - timestamp > this._options.expirationDuration
|
|
244
|
+
|
|
245
|
+
if (isExpired) {
|
|
246
|
+
expiredIndices.push(i)
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Remove expired items from back to front to maintain indices
|
|
251
|
+
for (let i = expiredIndices.length - 1; i >= 0; i--) {
|
|
252
|
+
const index = expiredIndices[i]
|
|
253
|
+
if (index === undefined) continue
|
|
254
|
+
|
|
255
|
+
const expiredItem = this._items[index]
|
|
256
|
+
if (expiredItem === undefined) continue
|
|
257
|
+
|
|
258
|
+
this._items.splice(index, 1)
|
|
259
|
+
this._itemTimestamps.splice(index, 1)
|
|
260
|
+
this._expirationCount++
|
|
261
|
+
this._options.onExpire(expiredItem, this)
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (expiredIndices.length > 0) {
|
|
265
|
+
this._options.onItemsChange(this)
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
199
269
|
/**
|
|
200
270
|
* Starts the queuer and processes items
|
|
201
271
|
*/
|
|
@@ -295,15 +365,19 @@ export class AsyncQueuer<TValue> {
|
|
|
295
365
|
|
|
296
366
|
if (insertIndex === -1) {
|
|
297
367
|
this._items.push(task)
|
|
368
|
+
this._itemTimestamps.push(Date.now())
|
|
298
369
|
} else {
|
|
299
370
|
this._items.splice(insertIndex, 0, task)
|
|
371
|
+
this._itemTimestamps.splice(insertIndex, 0, Date.now())
|
|
300
372
|
}
|
|
301
373
|
} else {
|
|
302
374
|
// Default FIFO/LIFO behavior
|
|
303
375
|
if (position === 'front') {
|
|
304
376
|
this._items.unshift(task)
|
|
377
|
+
this._itemTimestamps.unshift(Date.now())
|
|
305
378
|
} else {
|
|
306
379
|
this._items.push(task)
|
|
380
|
+
this._itemTimestamps.push(Date.now())
|
|
307
381
|
}
|
|
308
382
|
}
|
|
309
383
|
|
|
@@ -328,8 +402,10 @@ export class AsyncQueuer<TValue> {
|
|
|
328
402
|
|
|
329
403
|
if (position === 'front') {
|
|
330
404
|
item = this._items.shift()
|
|
405
|
+
this._itemTimestamps.shift()
|
|
331
406
|
} else {
|
|
332
407
|
item = this._items.pop()
|
|
408
|
+
this._itemTimestamps.pop()
|
|
333
409
|
}
|
|
334
410
|
|
|
335
411
|
if (item !== undefined) {
|
|
@@ -455,6 +531,13 @@ export class AsyncQueuer<TValue> {
|
|
|
455
531
|
)
|
|
456
532
|
}
|
|
457
533
|
}
|
|
534
|
+
|
|
535
|
+
/**
|
|
536
|
+
* Returns the number of items that have expired from the queuer
|
|
537
|
+
*/
|
|
538
|
+
getExpirationCount(): number {
|
|
539
|
+
return this._expirationCount
|
|
540
|
+
}
|
|
458
541
|
}
|
|
459
542
|
|
|
460
543
|
/**
|
|
@@ -474,7 +557,9 @@ export class AsyncQueuer<TValue> {
|
|
|
474
557
|
* @param options - Configuration options for the AsyncQueuer
|
|
475
558
|
* @returns A bound addItem function that can be used to add tasks to the queuer
|
|
476
559
|
*/
|
|
477
|
-
export function asyncQueue<TValue>(
|
|
478
|
-
|
|
560
|
+
export function asyncQueue<TValue>(
|
|
561
|
+
options: Omit<AsyncQueuerOptions<TValue>, 'started'> = {},
|
|
562
|
+
) {
|
|
563
|
+
const queuer = new AsyncQueuer<TValue>(options)
|
|
479
564
|
return queuer.addItem.bind(queuer)
|
|
480
565
|
}
|