@tanstack/pacer 0.1.0 → 0.2.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 +60 -44
- package/dist/cjs/async-debouncer.cjs.map +1 -1
- package/dist/cjs/async-debouncer.d.cts +37 -24
- package/dist/cjs/async-queuer.cjs +149 -125
- package/dist/cjs/async-queuer.cjs.map +1 -1
- package/dist/cjs/async-queuer.d.cts +65 -48
- package/dist/cjs/async-rate-limiter.cjs +63 -46
- package/dist/cjs/async-rate-limiter.cjs.map +1 -1
- package/dist/cjs/async-rate-limiter.d.cts +39 -27
- package/dist/cjs/async-throttler.cjs +70 -47
- package/dist/cjs/async-throttler.cjs.map +1 -1
- package/dist/cjs/async-throttler.d.cts +43 -25
- package/dist/cjs/debouncer.cjs +46 -22
- package/dist/cjs/debouncer.cjs.map +1 -1
- package/dist/cjs/debouncer.d.cts +25 -11
- package/dist/cjs/index.cjs +2 -0
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/index.d.cts +2 -0
- package/dist/cjs/queuer.cjs +114 -104
- package/dist/cjs/queuer.cjs.map +1 -1
- package/dist/cjs/queuer.d.cts +53 -40
- package/dist/cjs/rate-limiter.cjs +54 -42
- package/dist/cjs/rate-limiter.cjs.map +1 -1
- package/dist/cjs/rate-limiter.d.cts +37 -45
- package/dist/cjs/throttler.cjs +61 -41
- package/dist/cjs/throttler.cjs.map +1 -1
- package/dist/cjs/throttler.d.cts +35 -22
- package/dist/cjs/types.d.cts +12 -0
- package/dist/cjs/utils.cjs +13 -0
- package/dist/cjs/utils.cjs.map +1 -0
- package/dist/cjs/utils.d.cts +1 -0
- package/dist/esm/async-debouncer.d.ts +37 -24
- package/dist/esm/async-debouncer.js +60 -44
- package/dist/esm/async-debouncer.js.map +1 -1
- package/dist/esm/async-queuer.d.ts +65 -48
- package/dist/esm/async-queuer.js +149 -125
- package/dist/esm/async-queuer.js.map +1 -1
- package/dist/esm/async-rate-limiter.d.ts +39 -27
- package/dist/esm/async-rate-limiter.js +63 -46
- package/dist/esm/async-rate-limiter.js.map +1 -1
- package/dist/esm/async-throttler.d.ts +43 -25
- package/dist/esm/async-throttler.js +70 -47
- package/dist/esm/async-throttler.js.map +1 -1
- package/dist/esm/debouncer.d.ts +25 -11
- package/dist/esm/debouncer.js +46 -22
- package/dist/esm/debouncer.js.map +1 -1
- package/dist/esm/index.d.ts +2 -0
- package/dist/esm/index.js +2 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/queuer.d.ts +53 -40
- package/dist/esm/queuer.js +114 -104
- package/dist/esm/queuer.js.map +1 -1
- package/dist/esm/rate-limiter.d.ts +37 -45
- package/dist/esm/rate-limiter.js +54 -42
- package/dist/esm/rate-limiter.js.map +1 -1
- package/dist/esm/throttler.d.ts +35 -22
- package/dist/esm/throttler.js +61 -41
- package/dist/esm/throttler.js.map +1 -1
- package/dist/esm/types.d.ts +12 -0
- package/dist/esm/utils.d.ts +1 -0
- package/dist/esm/utils.js +13 -0
- package/dist/esm/utils.js.map +1 -0
- package/package.json +8 -1
- package/src/async-debouncer.ts +90 -62
- package/src/async-queuer.ts +178 -145
- package/src/async-rate-limiter.ts +93 -67
- package/src/async-throttler.ts +98 -63
- package/src/debouncer.ts +71 -35
- package/src/index.ts +2 -0
- package/src/queuer.ts +135 -118
- package/src/rate-limiter.ts +79 -81
- package/src/throttler.ts +87 -61
- package/src/types.ts +17 -0
- package/src/utils.ts +13 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"throttler.js","sources":["../../src/throttler.ts"],"sourcesContent":["/**\n * Options for configuring a throttled function\n */\nexport interface ThrottlerOptions {\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 * 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
|
|
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<\n TFn extends AnyFunction,\n TArgs extends Parameters<TFn>,\n> {\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, TArgs>) => 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, any>> = {\n enabled: true,\n leading: true,\n trailing: true,\n wait: 0,\n onExecute: () => {},\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, TArgs extends Parameters<TFn>> {\n private _executionCount = 0\n private _lastArgs: TArgs | undefined\n private _lastExecutionTime = 0\n private _options: Required<ThrottlerOptions<TFn, TArgs>>\n private _timeoutId: NodeJS.Timeout | undefined\n private _isPending = false\n\n constructor(\n private fn: TFn,\n initialOptions: ThrottlerOptions<TFn, TArgs>,\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(\n newOptions: Partial<ThrottlerOptions<TFn, TArgs>>,\n ): Required<ThrottlerOptions<TFn, TArgs>> {\n this._options = {\n ...this._options,\n ...newOptions,\n }\n return this._options\n }\n\n /**\n * Returns the current throttler options\n */\n getOptions(): Required<ThrottlerOptions<TFn, TArgs>> {\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: TArgs): void {\n const now = Date.now()\n const timeSinceLastExecution = now - this._lastExecutionTime\n\n // Handle leading execution\n if (timeSinceLastExecution >= this._options.wait) {\n if (this._options.leading) {\n this.executeFunction(...args)\n }\n this._lastExecutionTime = now\n this._isPending = false\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 this._isPending = true\n this._timeoutId = setTimeout(() => {\n if (this._lastArgs) {\n this.executeFunction(...this._lastArgs)\n this._lastArgs = undefined\n }\n this._lastExecutionTime = Date.now()\n this._timeoutId = undefined\n this._isPending = false\n this._options.onExecute(this)\n }, this._options.wait - timeSinceLastExecution)\n }\n }\n }\n\n private executeFunction(...args: TArgs): void {\n if (!this._options.enabled) return\n this.fn(...args) // EXECUTE!\n this._executionCount++\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 this._isPending = false\n }\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._isPending\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/**\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<\n TFn extends AnyFunction,\n TArgs extends Parameters<TFn>,\n>(fn: TFn, initialOptions: Omit<ThrottlerOptions<TFn, TArgs>, 'enabled'>) {\n const throttler = new Throttler(fn, initialOptions)\n return throttler.maybeExecute.bind(throttler)\n}\n"],"names":[],"mappings":"AAkCA,MAAM,iBAAuD;AAAA,EAC3D,SAAS;AAAA,EACT,SAAS;AAAA,EACT,UAAU;AAAA,EACV,MAAM;AAAA,EACN,WAAW,MAAM;AAAA,EAAA;AACnB;AA6BO,MAAM,UAAkE;AAAA,EAQ7E,YACU,IACR,gBACA;AAFQ,SAAA,KAAA;AARV,SAAQ,kBAAkB;AAE1B,SAAQ,qBAAqB;AAG7B,SAAQ,aAAa;AAMnB,SAAK,WAAW;AAAA,MACd,GAAG;AAAA,MACH,GAAG;AAAA,IACL;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOF,WACE,YACwC;AACxC,SAAK,WAAW;AAAA,MACd,GAAG,KAAK;AAAA,MACR,GAAG;AAAA,IACL;AACA,WAAO,KAAK;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA,EAMd,aAAqD;AACnD,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,MAAmB;AAC3B,UAAA,MAAM,KAAK,IAAI;AACf,UAAA,yBAAyB,MAAM,KAAK;AAGtC,QAAA,0BAA0B,KAAK,SAAS,MAAM;AAC5C,UAAA,KAAK,SAAS,SAAS;AACpB,aAAA,gBAAgB,GAAG,IAAI;AAAA,MAAA;AAE9B,WAAK,qBAAqB;AAC1B,WAAK,aAAa;AAAA,IAAA,OACb;AAEL,WAAK,YAAY;AAGjB,UAAI,CAAC,KAAK,cAAc,KAAK,SAAS,UAAU;AAC9C,aAAK,aAAa;AACb,aAAA,aAAa,WAAW,MAAM;AACjC,cAAI,KAAK,WAAW;AACb,iBAAA,gBAAgB,GAAG,KAAK,SAAS;AACtC,iBAAK,YAAY;AAAA,UAAA;AAEd,eAAA,qBAAqB,KAAK,IAAI;AACnC,eAAK,aAAa;AAClB,eAAK,aAAa;AACb,eAAA,SAAS,UAAU,IAAI;AAAA,QAC3B,GAAA,KAAK,SAAS,OAAO,sBAAsB;AAAA,MAAA;AAAA,IAChD;AAAA,EACF;AAAA,EAGM,mBAAmB,MAAmB;AACxC,QAAA,CAAC,KAAK,SAAS,QAAS;AACvB,SAAA,GAAG,GAAG,IAAI;AACV,SAAA;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAYP,SAAe;AACb,QAAI,KAAK,YAAY;AACnB,mBAAa,KAAK,UAAU;AAC5B,WAAK,aAAa;AAClB,WAAK,YAAY;AACjB,WAAK,aAAa;AAAA,IAAA;AAAA,EACpB;AAAA;AAAA;AAAA;AAAA,EAMF,oBAA4B;AAC1B,WAAO,KAAK;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA,EAMd,eAAwB;AACf,WAAA,KAAK,SAAS,WAAW,KAAK;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA,EAMvC,uBAA+B;AAC7B,WAAO,KAAK;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA,EAMd,uBAA+B;AACtB,WAAA,KAAK,qBAAqB,KAAK,SAAS;AAAA,EAAA;AAEnD;AA4BgB,SAAA,SAGd,IAAS,gBAA+D;AACxE,QAAM,YAAY,IAAI,UAAU,IAAI,cAAc;AAC3C,SAAA,UAAU,aAAa,KAAK,SAAS;AAC9C;"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
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
|
+
*/
|
|
6
|
+
export type AnyFunction<TArgs extends Array<any> = Array<any>> = (...args: TArgs) => any;
|
|
7
|
+
/**
|
|
8
|
+
* 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
|
+
*/
|
|
12
|
+
export type AnyAsyncFunction<TArgs extends Array<any> = Array<any>> = (...args: TArgs) => Promise<any>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function bindInstanceMethods<T extends Record<string, any>>(instance: T): any;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
function bindInstanceMethods(instance) {
|
|
2
|
+
return Object.getOwnPropertyNames(Object.getPrototypeOf(instance)).filter((key) => typeof instance[key] === "function").reduce((acc, key) => {
|
|
3
|
+
const method = instance[key];
|
|
4
|
+
if (typeof method === "function") {
|
|
5
|
+
acc[key] = method.bind(instance);
|
|
6
|
+
}
|
|
7
|
+
return acc;
|
|
8
|
+
}, {});
|
|
9
|
+
}
|
|
10
|
+
export {
|
|
11
|
+
bindInstanceMethods
|
|
12
|
+
};
|
|
13
|
+
//# sourceMappingURL=utils.js.map
|
|
@@ -0,0 +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,UACA;AACA,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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tanstack/pacer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Utilities for debouncing, throttling, rate-limiting, queuing, and more.",
|
|
5
5
|
"author": "Tanner Linsley",
|
|
6
6
|
"license": "MIT",
|
|
@@ -127,6 +127,13 @@
|
|
|
127
127
|
"default": "./dist/cjs/throttler.cjs"
|
|
128
128
|
}
|
|
129
129
|
},
|
|
130
|
+
"./types": {
|
|
131
|
+
"types": "./dist/esm/types.d.ts"
|
|
132
|
+
},
|
|
133
|
+
"./utils": {
|
|
134
|
+
"types": "./dist/esm/utils.d.ts",
|
|
135
|
+
"default": "./dist/esm/utils.js"
|
|
136
|
+
},
|
|
130
137
|
"./package.json": "./package.json"
|
|
131
138
|
},
|
|
132
139
|
"sideEffects": false,
|
package/src/async-debouncer.ts
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
|
+
import type { AnyAsyncFunction } from './types'
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* Options for configuring an async debounced function
|
|
3
5
|
*/
|
|
4
|
-
export interface AsyncDebouncerOptions
|
|
6
|
+
export interface AsyncDebouncerOptions<
|
|
7
|
+
TFn extends AnyAsyncFunction,
|
|
8
|
+
TArgs extends Parameters<TFn>,
|
|
9
|
+
> {
|
|
5
10
|
/**
|
|
6
11
|
* Whether the debouncer is enabled. When disabled, maybeExecute will not trigger any executions.
|
|
7
12
|
* Defaults to true.
|
|
@@ -12,6 +17,14 @@ export interface AsyncDebouncerOptions {
|
|
|
12
17
|
* Defaults to false.
|
|
13
18
|
*/
|
|
14
19
|
leading?: boolean
|
|
20
|
+
/**
|
|
21
|
+
* Optional error handler for when the debounced function throws
|
|
22
|
+
*/
|
|
23
|
+
onError?: (error: unknown) => void
|
|
24
|
+
/**
|
|
25
|
+
* Optional function to call when the debounced function is executed
|
|
26
|
+
*/
|
|
27
|
+
onExecute?: (debouncer: AsyncDebouncer<TFn, TArgs>) => void
|
|
15
28
|
/**
|
|
16
29
|
* Whether to execute on the trailing edge of the timeout.
|
|
17
30
|
* Defaults to true.
|
|
@@ -22,17 +35,14 @@ export interface AsyncDebouncerOptions {
|
|
|
22
35
|
* Defaults to 0ms
|
|
23
36
|
*/
|
|
24
37
|
wait: number
|
|
25
|
-
/**
|
|
26
|
-
* Optional error handler for when the debounced function throws
|
|
27
|
-
*/
|
|
28
|
-
onError?: (error: unknown) => void
|
|
29
38
|
}
|
|
30
39
|
|
|
31
|
-
const defaultOptions: Required<AsyncDebouncerOptions
|
|
40
|
+
const defaultOptions: Required<AsyncDebouncerOptions<any, any>> = {
|
|
32
41
|
enabled: true,
|
|
33
42
|
leading: false,
|
|
34
43
|
trailing: true,
|
|
35
44
|
onError: () => {},
|
|
45
|
+
onExecute: () => {},
|
|
36
46
|
wait: 0,
|
|
37
47
|
}
|
|
38
48
|
|
|
@@ -48,33 +58,33 @@ const defaultOptions: Required<AsyncDebouncerOptions> = {
|
|
|
48
58
|
*
|
|
49
59
|
* @example
|
|
50
60
|
* ```ts
|
|
51
|
-
* const
|
|
61
|
+
* const asyncDebouncer = new AsyncDebouncer(async (value: string) => {
|
|
52
62
|
* await searchAPI(value);
|
|
53
63
|
* }, { wait: 500 });
|
|
54
64
|
*
|
|
55
65
|
* // Called on each keystroke but only executes after 500ms of no typing
|
|
56
66
|
* inputElement.addEventListener('input', () => {
|
|
57
|
-
*
|
|
67
|
+
* asyncDebouncer.maybeExecute(inputElement.value);
|
|
58
68
|
* });
|
|
59
69
|
* ```
|
|
60
70
|
*/
|
|
61
71
|
export class AsyncDebouncer<
|
|
62
|
-
TFn extends
|
|
72
|
+
TFn extends AnyAsyncFunction,
|
|
63
73
|
TArgs extends Parameters<TFn>,
|
|
64
74
|
> {
|
|
65
|
-
private
|
|
66
|
-
private
|
|
67
|
-
private
|
|
68
|
-
private
|
|
69
|
-
private
|
|
70
|
-
private
|
|
71
|
-
private
|
|
75
|
+
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
|
+
private _canLeadingExecute = true
|
|
72
82
|
|
|
73
83
|
constructor(
|
|
74
84
|
private fn: TFn,
|
|
75
|
-
initialOptions: AsyncDebouncerOptions,
|
|
85
|
+
initialOptions: AsyncDebouncerOptions<TFn, TArgs>,
|
|
76
86
|
) {
|
|
77
|
-
this.
|
|
87
|
+
this._options = {
|
|
78
88
|
...defaultOptions,
|
|
79
89
|
...initialOptions,
|
|
80
90
|
}
|
|
@@ -85,36 +95,20 @@ export class AsyncDebouncer<
|
|
|
85
95
|
* Returns the new options state
|
|
86
96
|
*/
|
|
87
97
|
setOptions(
|
|
88
|
-
newOptions: Partial<AsyncDebouncerOptions
|
|
89
|
-
): Required<AsyncDebouncerOptions
|
|
90
|
-
this.
|
|
91
|
-
...this.
|
|
98
|
+
newOptions: Partial<AsyncDebouncerOptions<TFn, TArgs>>,
|
|
99
|
+
): Required<AsyncDebouncerOptions<TFn, TArgs>> {
|
|
100
|
+
this._options = {
|
|
101
|
+
...this._options,
|
|
92
102
|
...newOptions,
|
|
93
103
|
}
|
|
94
|
-
return this.
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
/**
|
|
98
|
-
* Returns the number of times the function has been executed
|
|
99
|
-
*/
|
|
100
|
-
getExecutionCount(): number {
|
|
101
|
-
return this.executionCount
|
|
104
|
+
return this._options
|
|
102
105
|
}
|
|
103
106
|
|
|
104
107
|
/**
|
|
105
|
-
*
|
|
108
|
+
* Returns the current debouncer options
|
|
106
109
|
*/
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
clearTimeout(this.timeoutId)
|
|
110
|
-
this.timeoutId = null
|
|
111
|
-
}
|
|
112
|
-
if (this.abortController) {
|
|
113
|
-
this.abortController.abort()
|
|
114
|
-
this.abortController = null
|
|
115
|
-
}
|
|
116
|
-
this.lastArgs = undefined
|
|
117
|
-
this.canLeadingExecute = true
|
|
110
|
+
getOptions(): Required<AsyncDebouncerOptions<TFn, TArgs>> {
|
|
111
|
+
return this._options
|
|
118
112
|
}
|
|
119
113
|
|
|
120
114
|
/**
|
|
@@ -123,52 +117,85 @@ export class AsyncDebouncer<
|
|
|
123
117
|
*/
|
|
124
118
|
async maybeExecute(...args: TArgs): Promise<void> {
|
|
125
119
|
this.cancel()
|
|
126
|
-
this.
|
|
120
|
+
this._lastArgs = args
|
|
127
121
|
|
|
128
122
|
// Handle leading execution
|
|
129
|
-
if (this.
|
|
130
|
-
this.
|
|
123
|
+
if (this._options.leading && this._canLeadingExecute) {
|
|
124
|
+
this._canLeadingExecute = false
|
|
131
125
|
await this.executeFunction(...args)
|
|
132
126
|
}
|
|
133
127
|
|
|
134
128
|
return new Promise((resolve) => {
|
|
135
|
-
this.
|
|
136
|
-
if (this.
|
|
129
|
+
this._timeoutId = setTimeout(async () => {
|
|
130
|
+
if (this._isExecuting) {
|
|
137
131
|
resolve()
|
|
138
132
|
return
|
|
139
133
|
}
|
|
140
134
|
|
|
141
|
-
this.
|
|
135
|
+
this._canLeadingExecute = true
|
|
142
136
|
// Execute trailing only if enabled
|
|
143
|
-
if (this.
|
|
144
|
-
this.
|
|
137
|
+
if (this._options.trailing) {
|
|
138
|
+
this._abortController = new AbortController()
|
|
145
139
|
try {
|
|
146
|
-
this.
|
|
147
|
-
if (this.
|
|
148
|
-
await this.executeFunction(...this.
|
|
140
|
+
this._isExecuting = true
|
|
141
|
+
if (this._lastArgs) {
|
|
142
|
+
await this.executeFunction(...this._lastArgs)
|
|
149
143
|
}
|
|
150
144
|
} catch (error) {
|
|
151
145
|
try {
|
|
152
|
-
this.
|
|
146
|
+
this._options.onError(error)
|
|
153
147
|
} catch {
|
|
154
|
-
|
|
148
|
+
console.error('Error in error handler', error)
|
|
155
149
|
}
|
|
156
150
|
} finally {
|
|
157
|
-
this.
|
|
158
|
-
this.
|
|
151
|
+
this._isExecuting = false
|
|
152
|
+
this._abortController = null
|
|
159
153
|
resolve()
|
|
160
154
|
}
|
|
161
155
|
} else {
|
|
162
156
|
resolve()
|
|
163
157
|
}
|
|
164
|
-
}, this.
|
|
158
|
+
}, this._options.wait)
|
|
165
159
|
})
|
|
166
160
|
}
|
|
167
161
|
|
|
168
162
|
private async executeFunction(...args: TArgs): Promise<void> {
|
|
169
|
-
if (!this.
|
|
170
|
-
this.
|
|
163
|
+
if (!this._options.enabled) return
|
|
164
|
+
this._executionCount++
|
|
171
165
|
await this.fn(...args)
|
|
166
|
+
this._options.onExecute(this)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Cancels any pending execution
|
|
171
|
+
*/
|
|
172
|
+
cancel(): void {
|
|
173
|
+
if (this._timeoutId) {
|
|
174
|
+
clearTimeout(this._timeoutId)
|
|
175
|
+
this._timeoutId = null
|
|
176
|
+
}
|
|
177
|
+
if (this._abortController) {
|
|
178
|
+
this._abortController.abort()
|
|
179
|
+
this._abortController = null
|
|
180
|
+
}
|
|
181
|
+
this._lastArgs = undefined
|
|
182
|
+
this._canLeadingExecute = true
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Returns the number of times the function has been executed
|
|
187
|
+
*/
|
|
188
|
+
getExecutionCount(): number {
|
|
189
|
+
return this._executionCount
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Returns `true` if there is a pending execution
|
|
194
|
+
*/
|
|
195
|
+
getIsPending(): boolean {
|
|
196
|
+
return (
|
|
197
|
+
this._options.enabled && (this._timeoutId !== null || this._isExecuting)
|
|
198
|
+
)
|
|
172
199
|
}
|
|
173
200
|
}
|
|
174
201
|
|
|
@@ -190,8 +217,9 @@ export class AsyncDebouncer<
|
|
|
190
217
|
* ```
|
|
191
218
|
*/
|
|
192
219
|
export function asyncDebounce<
|
|
193
|
-
TFn extends
|
|
194
|
-
|
|
220
|
+
TFn extends AnyAsyncFunction,
|
|
221
|
+
TArgs extends Parameters<TFn>,
|
|
222
|
+
>(fn: TFn, initialOptions: Omit<AsyncDebouncerOptions<TFn, TArgs>, 'enabled'>) {
|
|
195
223
|
const asyncDebouncer = new AsyncDebouncer(fn, initialOptions)
|
|
196
224
|
return asyncDebouncer.maybeExecute.bind(asyncDebouncer)
|
|
197
225
|
}
|