@tanstack/pacer 0.2.0 → 0.4.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 +60 -24
- 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 +75 -38
- package/dist/cjs/async-rate-limiter.cjs.map +1 -1
- package/dist/cjs/async-rate-limiter.d.cts +76 -24
- package/dist/cjs/async-throttler.cjs +85 -57
- package/dist/cjs/async-throttler.cjs.map +1 -1
- package/dist/cjs/async-throttler.d.cts +66 -25
- 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 +19 -9
- package/dist/cjs/rate-limiter.cjs.map +1 -1
- package/dist/cjs/rate-limiter.d.cts +31 -11
- 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 +60 -24
- 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 +76 -24
- package/dist/esm/async-rate-limiter.js +75 -38
- package/dist/esm/async-rate-limiter.js.map +1 -1
- package/dist/esm/async-throttler.d.ts +66 -25
- 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 +31 -11
- package/dist/esm/rate-limiter.js +19 -9
- 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 +130 -81
- package/src/async-queuer.ts +93 -8
- package/src/async-rate-limiter.ts +141 -67
- package/src/async-throttler.ts +152 -94
- package/src/debouncer.ts +26 -33
- package/src/queuer.ts +92 -4
- package/src/rate-limiter.ts +56 -32
- package/src/throttler.ts +45 -53
- package/src/types.ts +2 -10
- package/src/utils.ts +1 -1
package/dist/esm/throttler.js
CHANGED
|
@@ -1,17 +1,16 @@
|
|
|
1
1
|
const defaultOptions = {
|
|
2
2
|
enabled: true,
|
|
3
3
|
leading: true,
|
|
4
|
-
trailing: true,
|
|
5
|
-
wait: 0,
|
|
6
4
|
onExecute: () => {
|
|
7
|
-
}
|
|
5
|
+
},
|
|
6
|
+
trailing: true,
|
|
7
|
+
wait: 0
|
|
8
8
|
};
|
|
9
9
|
class Throttler {
|
|
10
10
|
constructor(fn, initialOptions) {
|
|
11
11
|
this.fn = fn;
|
|
12
12
|
this._executionCount = 0;
|
|
13
13
|
this._lastExecutionTime = 0;
|
|
14
|
-
this._isPending = false;
|
|
15
14
|
this._options = {
|
|
16
15
|
...defaultOptions,
|
|
17
16
|
...initialOptions
|
|
@@ -22,11 +21,10 @@ class Throttler {
|
|
|
22
21
|
* Returns the new options state
|
|
23
22
|
*/
|
|
24
23
|
setOptions(newOptions) {
|
|
25
|
-
this._options = {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
}
|
|
29
|
-
return this._options;
|
|
24
|
+
this._options = { ...this._options, ...newOptions };
|
|
25
|
+
if (!this._options.enabled) {
|
|
26
|
+
this.cancel();
|
|
27
|
+
}
|
|
30
28
|
}
|
|
31
29
|
/**
|
|
32
30
|
* Returns the current throttler options
|
|
@@ -59,26 +57,18 @@ class Throttler {
|
|
|
59
57
|
maybeExecute(...args) {
|
|
60
58
|
const now = Date.now();
|
|
61
59
|
const timeSinceLastExecution = now - this._lastExecutionTime;
|
|
62
|
-
if (timeSinceLastExecution >= this._options.wait) {
|
|
63
|
-
|
|
64
|
-
this.executeFunction(...args);
|
|
65
|
-
}
|
|
66
|
-
this._lastExecutionTime = now;
|
|
67
|
-
this._isPending = false;
|
|
60
|
+
if (this._options.leading && timeSinceLastExecution >= this._options.wait) {
|
|
61
|
+
this.executeFunction(...args);
|
|
68
62
|
} else {
|
|
69
63
|
this._lastArgs = args;
|
|
70
64
|
if (!this._timeoutId && this._options.trailing) {
|
|
71
|
-
this.
|
|
65
|
+
const _timeSinceLastExecution = this._lastExecutionTime ? now - this._lastExecutionTime : 0;
|
|
66
|
+
const timeoutDuration = this._options.wait - _timeSinceLastExecution;
|
|
72
67
|
this._timeoutId = setTimeout(() => {
|
|
73
|
-
if (this._lastArgs) {
|
|
68
|
+
if (this._lastArgs !== void 0) {
|
|
74
69
|
this.executeFunction(...this._lastArgs);
|
|
75
|
-
this._lastArgs = void 0;
|
|
76
70
|
}
|
|
77
|
-
|
|
78
|
-
this._timeoutId = void 0;
|
|
79
|
-
this._isPending = false;
|
|
80
|
-
this._options.onExecute(this);
|
|
81
|
-
}, this._options.wait - timeSinceLastExecution);
|
|
71
|
+
}, timeoutDuration);
|
|
82
72
|
}
|
|
83
73
|
}
|
|
84
74
|
}
|
|
@@ -86,6 +76,10 @@ class Throttler {
|
|
|
86
76
|
if (!this._options.enabled) return;
|
|
87
77
|
this.fn(...args);
|
|
88
78
|
this._executionCount++;
|
|
79
|
+
this._lastExecutionTime = Date.now();
|
|
80
|
+
this._timeoutId = void 0;
|
|
81
|
+
this._lastArgs = void 0;
|
|
82
|
+
this._options.onExecute(this);
|
|
89
83
|
}
|
|
90
84
|
/**
|
|
91
85
|
* Cancels any pending trailing execution and clears internal state.
|
|
@@ -101,21 +95,8 @@ class Throttler {
|
|
|
101
95
|
clearTimeout(this._timeoutId);
|
|
102
96
|
this._timeoutId = void 0;
|
|
103
97
|
this._lastArgs = void 0;
|
|
104
|
-
this._isPending = false;
|
|
105
98
|
}
|
|
106
99
|
}
|
|
107
|
-
/**
|
|
108
|
-
* Returns the number of times the function has been executed
|
|
109
|
-
*/
|
|
110
|
-
getExecutionCount() {
|
|
111
|
-
return this._executionCount;
|
|
112
|
-
}
|
|
113
|
-
/**
|
|
114
|
-
* Returns `true` if there is a pending execution
|
|
115
|
-
*/
|
|
116
|
-
getIsPending() {
|
|
117
|
-
return this._options.enabled && this._isPending;
|
|
118
|
-
}
|
|
119
100
|
/**
|
|
120
101
|
* Returns the last execution time
|
|
121
102
|
*/
|
|
@@ -128,6 +109,18 @@ class Throttler {
|
|
|
128
109
|
getNextExecutionTime() {
|
|
129
110
|
return this._lastExecutionTime + this._options.wait;
|
|
130
111
|
}
|
|
112
|
+
/**
|
|
113
|
+
* Returns the number of times the function has been executed
|
|
114
|
+
*/
|
|
115
|
+
getExecutionCount() {
|
|
116
|
+
return this._executionCount;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Returns `true` if there is a pending execution
|
|
120
|
+
*/
|
|
121
|
+
getIsPending() {
|
|
122
|
+
return this._options.enabled && !!this._timeoutId;
|
|
123
|
+
}
|
|
131
124
|
}
|
|
132
125
|
function throttle(fn, initialOptions) {
|
|
133
126
|
const throttler = new Throttler(fn, initialOptions);
|
|
@@ -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
|
|
|
@@ -56,33 +58,38 @@ const defaultOptions: Required<AsyncDebouncerOptions<any, any>> = {
|
|
|
56
58
|
* Unlike throttling which allows execution at regular intervals, debouncing prevents any execution until
|
|
57
59
|
* the function stops being called for the specified delay period.
|
|
58
60
|
*
|
|
61
|
+
* Unlike the non-async Debouncer, this async version supports returning values from the debounced function,
|
|
62
|
+
* making it ideal for API calls and other async operations where you want the result of the `maybeExecute` call
|
|
63
|
+
* instead of setting the result on a state variable from within the debounced function.
|
|
64
|
+
*
|
|
59
65
|
* @example
|
|
60
66
|
* ```ts
|
|
61
67
|
* const asyncDebouncer = new AsyncDebouncer(async (value: string) => {
|
|
62
|
-
* await searchAPI(value);
|
|
68
|
+
* const results = await searchAPI(value);
|
|
69
|
+
* return results; // Return value is preserved
|
|
63
70
|
* }, { wait: 500 });
|
|
64
71
|
*
|
|
65
72
|
* // Called on each keystroke but only executes after 500ms of no typing
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
* });
|
|
73
|
+
* // Returns the API response directly
|
|
74
|
+
* const results = await asyncDebouncer.maybeExecute(inputElement.value);
|
|
69
75
|
* ```
|
|
70
76
|
*/
|
|
71
|
-
export class AsyncDebouncer<
|
|
72
|
-
TFn extends AnyAsyncFunction,
|
|
73
|
-
TArgs extends Parameters<TFn>,
|
|
74
|
-
> {
|
|
77
|
+
export class AsyncDebouncer<TFn extends AnyAsyncFunction> {
|
|
75
78
|
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
79
|
private _canLeadingExecute = true
|
|
80
|
+
private _errorCount = 0
|
|
81
|
+
private _isExecuting = false
|
|
82
|
+
private _isPending = false
|
|
83
|
+
private _lastArgs: Parameters<TFn> | undefined
|
|
84
|
+
private _lastResult: ReturnType<TFn> | undefined
|
|
85
|
+
private _options: Required<AsyncDebouncerOptions<TFn>>
|
|
86
|
+
private _settleCount = 0
|
|
87
|
+
private _successCount = 0
|
|
88
|
+
private _timeoutId: NodeJS.Timeout | null = null
|
|
82
89
|
|
|
83
90
|
constructor(
|
|
84
91
|
private fn: TFn,
|
|
85
|
-
initialOptions: AsyncDebouncerOptions<TFn
|
|
92
|
+
initialOptions: AsyncDebouncerOptions<TFn>,
|
|
86
93
|
) {
|
|
87
94
|
this._options = {
|
|
88
95
|
...defaultOptions,
|
|
@@ -94,20 +101,19 @@ export class AsyncDebouncer<
|
|
|
94
101
|
* Updates the debouncer options
|
|
95
102
|
* Returns the new options state
|
|
96
103
|
*/
|
|
97
|
-
setOptions(
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
104
|
+
setOptions(newOptions: Partial<AsyncDebouncerOptions<TFn>>): void {
|
|
105
|
+
this._options = { ...this._options, ...newOptions }
|
|
106
|
+
|
|
107
|
+
// End the pending state if the debouncer is disabled
|
|
108
|
+
if (!this._options.enabled) {
|
|
109
|
+
this._isPending = false
|
|
103
110
|
}
|
|
104
|
-
return this._options
|
|
105
111
|
}
|
|
106
112
|
|
|
107
113
|
/**
|
|
108
114
|
* Returns the current debouncer options
|
|
109
115
|
*/
|
|
110
|
-
getOptions(): Required<AsyncDebouncerOptions<TFn
|
|
116
|
+
getOptions(): Required<AsyncDebouncerOptions<TFn>> {
|
|
111
117
|
return this._options
|
|
112
118
|
}
|
|
113
119
|
|
|
@@ -115,61 +121,65 @@ export class AsyncDebouncer<
|
|
|
115
121
|
* Attempts to execute the debounced function
|
|
116
122
|
* If a call is already in progress, it will be queued
|
|
117
123
|
*/
|
|
118
|
-
async maybeExecute(
|
|
119
|
-
|
|
124
|
+
async maybeExecute(
|
|
125
|
+
...args: Parameters<TFn>
|
|
126
|
+
): Promise<ReturnType<TFn> | undefined> {
|
|
127
|
+
this._cancel()
|
|
120
128
|
this._lastArgs = args
|
|
121
129
|
|
|
122
130
|
// Handle leading execution
|
|
123
131
|
if (this._options.leading && this._canLeadingExecute) {
|
|
124
132
|
this._canLeadingExecute = false
|
|
125
133
|
await this.executeFunction(...args)
|
|
134
|
+
return this._lastResult
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Handle trailing execution
|
|
138
|
+
if (this._options.trailing) {
|
|
139
|
+
this._isPending = true
|
|
126
140
|
}
|
|
127
141
|
|
|
128
142
|
return new Promise((resolve) => {
|
|
129
143
|
this._timeoutId = setTimeout(async () => {
|
|
130
|
-
if
|
|
131
|
-
|
|
132
|
-
|
|
144
|
+
// Execute trailing if enabled
|
|
145
|
+
if (this._options.trailing && this._lastArgs) {
|
|
146
|
+
await this.executeFunction(...this._lastArgs)
|
|
133
147
|
}
|
|
134
148
|
|
|
149
|
+
// Reset state and resolve
|
|
135
150
|
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
|
-
}
|
|
151
|
+
resolve(this._lastResult)
|
|
158
152
|
}, this._options.wait)
|
|
159
153
|
})
|
|
160
154
|
}
|
|
161
155
|
|
|
162
|
-
private async executeFunction(
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
this.
|
|
156
|
+
private async executeFunction(
|
|
157
|
+
...args: Parameters<TFn>
|
|
158
|
+
): Promise<ReturnType<TFn> | undefined> {
|
|
159
|
+
if (!this._options.enabled) return undefined
|
|
160
|
+
this._abortController = new AbortController()
|
|
161
|
+
try {
|
|
162
|
+
this._isExecuting = true
|
|
163
|
+
this._lastResult = await this.fn(...args) // EXECUTE!
|
|
164
|
+
this._successCount++
|
|
165
|
+
this._options.onSuccess(this._lastResult!, this)
|
|
166
|
+
} catch (error) {
|
|
167
|
+
this._errorCount++
|
|
168
|
+
this._options.onError(error, this)
|
|
169
|
+
} finally {
|
|
170
|
+
this._isExecuting = false
|
|
171
|
+
this._isPending = false
|
|
172
|
+
this._settleCount++
|
|
173
|
+
this._abortController = null
|
|
174
|
+
this._options.onSettled(this)
|
|
175
|
+
}
|
|
176
|
+
return this._lastResult
|
|
167
177
|
}
|
|
168
178
|
|
|
169
179
|
/**
|
|
170
|
-
*
|
|
180
|
+
* Cancel without resetting _canLeadingExecute
|
|
171
181
|
*/
|
|
172
|
-
|
|
182
|
+
private _cancel(): void {
|
|
173
183
|
if (this._timeoutId) {
|
|
174
184
|
clearTimeout(this._timeoutId)
|
|
175
185
|
this._timeoutId = null
|
|
@@ -179,23 +189,58 @@ export class AsyncDebouncer<
|
|
|
179
189
|
this._abortController = null
|
|
180
190
|
}
|
|
181
191
|
this._lastArgs = undefined
|
|
192
|
+
this._isPending = false
|
|
193
|
+
this._isExecuting = false
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Cancels any pending execution or aborts any execution in progress
|
|
198
|
+
*/
|
|
199
|
+
cancel(): void {
|
|
182
200
|
this._canLeadingExecute = true
|
|
201
|
+
this._cancel()
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Returns the last result of the debounced function
|
|
206
|
+
*/
|
|
207
|
+
getLastResult(): ReturnType<TFn> | undefined {
|
|
208
|
+
return this._lastResult
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Returns the number of times the function has been executed successfully
|
|
213
|
+
*/
|
|
214
|
+
getSuccessCount(): number {
|
|
215
|
+
return this._successCount
|
|
183
216
|
}
|
|
184
217
|
|
|
185
218
|
/**
|
|
186
|
-
* Returns the number of times the function has
|
|
219
|
+
* Returns the number of times the function has settled (completed or errored)
|
|
187
220
|
*/
|
|
188
|
-
|
|
189
|
-
return this.
|
|
221
|
+
getSettleCount(): number {
|
|
222
|
+
return this._settleCount
|
|
190
223
|
}
|
|
191
224
|
|
|
192
225
|
/**
|
|
193
|
-
* Returns
|
|
226
|
+
* Returns the number of times the function has errored
|
|
227
|
+
*/
|
|
228
|
+
getErrorCount(): number {
|
|
229
|
+
return this._errorCount
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Returns `true` if there is a pending execution queued up for trailing execution
|
|
194
234
|
*/
|
|
195
235
|
getIsPending(): boolean {
|
|
196
|
-
return
|
|
197
|
-
|
|
198
|
-
|
|
236
|
+
return this._options.enabled && this._isPending
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Returns `true` if there is currently an execution in progress
|
|
241
|
+
*/
|
|
242
|
+
getIsExecuting(): boolean {
|
|
243
|
+
return this._isExecuting
|
|
199
244
|
}
|
|
200
245
|
}
|
|
201
246
|
|
|
@@ -204,22 +249,26 @@ export class AsyncDebouncer<
|
|
|
204
249
|
* The debounced function will only execute once the wait period has elapsed without any new calls.
|
|
205
250
|
* If called again during the wait period, the timer resets and a new wait period begins.
|
|
206
251
|
*
|
|
252
|
+
* Unlike the non-async Debouncer, this async version supports returning values from the debounced function,
|
|
253
|
+
* making it ideal for API calls and other async operations where you want the result of the `maybeExecute` call
|
|
254
|
+
* instead of setting the result on a state variable from within the debounced function.
|
|
255
|
+
*
|
|
207
256
|
* @example
|
|
208
257
|
* ```ts
|
|
209
258
|
* const debounced = asyncDebounce(async (value: string) => {
|
|
210
|
-
* await saveToAPI(value);
|
|
259
|
+
* const result = await saveToAPI(value);
|
|
260
|
+
* return result; // Return value is preserved
|
|
211
261
|
* }, { wait: 1000 });
|
|
212
262
|
*
|
|
213
263
|
* // Will only execute once, 1 second after the last call
|
|
214
|
-
*
|
|
215
|
-
* await debounced("
|
|
216
|
-
* await debounced("third"); // Executes after 1s
|
|
264
|
+
* // Returns the API response directly
|
|
265
|
+
* const result = await debounced("third");
|
|
217
266
|
* ```
|
|
218
267
|
*/
|
|
219
|
-
export function asyncDebounce<
|
|
220
|
-
TFn
|
|
221
|
-
|
|
222
|
-
|
|
268
|
+
export function asyncDebounce<TFn extends AnyAsyncFunction>(
|
|
269
|
+
fn: TFn,
|
|
270
|
+
initialOptions: Omit<AsyncDebouncerOptions<TFn>, 'enabled'>,
|
|
271
|
+
) {
|
|
223
272
|
const asyncDebouncer = new AsyncDebouncer(fn, initialOptions)
|
|
224
273
|
return asyncDebouncer.maybeExecute.bind(asyncDebouncer)
|
|
225
274
|
}
|