@develia/commons 0.4.22 → 0.4.24
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/collections/cache-dictionary.js +2 -6
- package/dist/cjs/collections/cache-dictionary.js.map +1 -1
- package/dist/cjs/debouncer.js +121 -0
- package/dist/cjs/debouncer.js.map +1 -0
- package/dist/cjs/functions.js +0 -86
- package/dist/cjs/functions.js.map +1 -1
- package/dist/cjs/index.js +8 -1
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/keyed-debouncer.js +144 -0
- package/dist/cjs/keyed-debouncer.js.map +1 -0
- package/dist/cjs/result.js +3 -0
- package/dist/cjs/result.js.map +1 -0
- package/dist/cjs/triggers.js +103 -0
- package/dist/cjs/triggers.js.map +1 -0
- package/dist/cjs/utilities.js +27 -4
- package/dist/cjs/utilities.js.map +1 -1
- package/dist/esm/collections/cache-dictionary.js +2 -6
- package/dist/esm/collections/cache-dictionary.js.map +1 -1
- package/dist/esm/debouncer.js +118 -0
- package/dist/esm/debouncer.js.map +1 -0
- package/dist/esm/functions.js +0 -85
- package/dist/esm/functions.js.map +1 -1
- package/dist/esm/index.js +3 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/keyed-debouncer.js +109 -0
- package/dist/esm/keyed-debouncer.js.map +1 -0
- package/dist/esm/result.js +2 -0
- package/dist/esm/result.js.map +1 -0
- package/dist/esm/triggers.js +98 -0
- package/dist/esm/triggers.js.map +1 -0
- package/dist/esm/utilities.js +27 -4
- package/dist/esm/utilities.js.map +1 -1
- package/dist/types/collections/cache-dictionary.d.ts.map +1 -1
- package/dist/types/debouncer.d.ts +60 -0
- package/dist/types/debouncer.d.ts.map +1 -0
- package/dist/types/functions.d.ts +5 -35
- package/dist/types/functions.d.ts.map +1 -1
- package/dist/types/index.d.ts +6 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/keyed-debouncer.d.ts +56 -0
- package/dist/types/keyed-debouncer.d.ts.map +1 -0
- package/dist/types/result.d.ts +11 -0
- package/dist/types/result.d.ts.map +1 -0
- package/dist/types/triggers.d.ts +64 -0
- package/dist/types/triggers.d.ts.map +1 -0
- package/dist/types/utilities.d.ts +8 -3
- package/dist/types/utilities.d.ts.map +1 -1
- package/dist/web/collections/cache-dictionary.js +2 -6
- package/dist/web/collections/cache-dictionary.js.map +1 -1
- package/dist/web/debouncer.js +118 -0
- package/dist/web/debouncer.js.map +1 -0
- package/dist/web/functions.js +0 -85
- package/dist/web/functions.js.map +1 -1
- package/dist/web/index.js +3 -0
- package/dist/web/index.js.map +1 -1
- package/dist/web/keyed-debouncer.js +109 -0
- package/dist/web/keyed-debouncer.js.map +1 -0
- package/dist/web/result.js +2 -0
- package/dist/web/result.js.map +1 -0
- package/dist/web/triggers.js +99 -0
- package/dist/web/triggers.js.map +1 -0
- package/dist/web/utilities.js +27 -4
- package/dist/web/utilities.js.map +1 -1
- package/package.json +3 -4
- package/src/collections/cache-dictionary.ts +2 -6
- package/src/debouncer.ts +135 -0
- package/src/functions.ts +5 -114
- package/src/index.ts +6 -0
- package/src/keyed-debouncer.ts +121 -0
- package/src/result.ts +6 -0
- package/src/triggers.ts +126 -0
- package/src/utilities.ts +30 -4
- package/dist/cjs/package.json +0 -3
|
@@ -46,9 +46,7 @@ export default class CacheDictionary<T> {
|
|
|
46
46
|
if (duration === null) {
|
|
47
47
|
delete this.expiration[key];
|
|
48
48
|
} else {
|
|
49
|
-
|
|
50
|
-
expiration.setSeconds(expiration.getSeconds() + duration);
|
|
51
|
-
this.expiration[key] = expiration;
|
|
49
|
+
this.expiration[key] = this.calculateExpiration(duration);
|
|
52
50
|
}
|
|
53
51
|
}
|
|
54
52
|
|
|
@@ -109,8 +107,6 @@ export default class CacheDictionary<T> {
|
|
|
109
107
|
return undefined;
|
|
110
108
|
}
|
|
111
109
|
|
|
112
|
-
|
|
113
|
-
expiration.setSeconds(expiration.getSeconds() + duration);
|
|
114
|
-
return expiration;
|
|
110
|
+
return new Date(Date.now() + duration * 1000);
|
|
115
111
|
}
|
|
116
112
|
}
|
package/src/debouncer.ts
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import {delay} from "./utilities.js";
|
|
2
|
+
import {noop} from "./functions.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Cancellable task. It must honour `signal`: pass it to fetch/SDKs and check
|
|
6
|
+
* `signal.aborted` before producing irreversible side effects.
|
|
7
|
+
*/
|
|
8
|
+
export type DebouncedTask = (signal: AbortSignal) => Promise<void>;
|
|
9
|
+
|
|
10
|
+
/** Options accepted by {@link Debouncer}. */
|
|
11
|
+
export interface DebouncerOptions {
|
|
12
|
+
/** Default wait, in milliseconds, before a task is executed. */
|
|
13
|
+
delayMs: number;
|
|
14
|
+
/** Called every time a task is scheduled, after any previous task has been cancelled. */
|
|
15
|
+
onSchedule?: () => void;
|
|
16
|
+
/** Called when the wait elapses and the task is about to run. */
|
|
17
|
+
onStart?: () => void;
|
|
18
|
+
/** Called when a task completes without throwing and without being cancelled. */
|
|
19
|
+
onSuccess?: () => void;
|
|
20
|
+
/** Called when a live task is cancelled, either by {@link Debouncer.cancel} or by being replaced. */
|
|
21
|
+
onCancel?: () => void;
|
|
22
|
+
/** Receives task errors. Cancellations never reach this callback. Defaults to `console.error`. */
|
|
23
|
+
onError?: (error: unknown) => void;
|
|
24
|
+
/** Called every time the debouncer goes from busy to idle. */
|
|
25
|
+
onIdle?: () => void;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Keeps at most one live task.
|
|
30
|
+
* Scheduling a new task cancels the previous one, whether it was still
|
|
31
|
+
* waiting or already running.
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* ```ts
|
|
35
|
+
* const debouncer = new Debouncer({delayMs: 1500});
|
|
36
|
+
*
|
|
37
|
+
* debouncer.schedule(async (signal) => {
|
|
38
|
+
* const reply = await callAI(messages, {signal});
|
|
39
|
+
* if (signal.aborted) return;
|
|
40
|
+
* await sendReply(reply);
|
|
41
|
+
* });
|
|
42
|
+
* ```
|
|
43
|
+
*/
|
|
44
|
+
export default class Debouncer {
|
|
45
|
+
#controller: AbortController | undefined;
|
|
46
|
+
readonly #delayMs: number;
|
|
47
|
+
readonly #onSchedule: () => void;
|
|
48
|
+
readonly #onStart: () => void;
|
|
49
|
+
readonly #onSuccess: () => void;
|
|
50
|
+
readonly #onCancel: () => void;
|
|
51
|
+
readonly #onError: (error: unknown) => void;
|
|
52
|
+
readonly #onIdle: () => void;
|
|
53
|
+
|
|
54
|
+
constructor(options: DebouncerOptions) {
|
|
55
|
+
assertValidDelay(options.delayMs);
|
|
56
|
+
this.#delayMs = options.delayMs;
|
|
57
|
+
this.#onSchedule = options.onSchedule ?? noop;
|
|
58
|
+
this.#onStart = options.onStart ?? noop;
|
|
59
|
+
this.#onSuccess = options.onSuccess ?? noop;
|
|
60
|
+
this.#onCancel = options.onCancel ?? noop;
|
|
61
|
+
this.#onError = options.onError ?? console.error;
|
|
62
|
+
this.#onIdle = options.onIdle ?? noop;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Whether a task is waiting or running. */
|
|
66
|
+
get isPending(): boolean {
|
|
67
|
+
return this.#controller !== undefined;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Schedule `task`, cancelling any previous task.
|
|
72
|
+
*
|
|
73
|
+
* @param task - The task to run once the delay elapses.
|
|
74
|
+
* @param delayMs - Wait for this task only. Defaults to the configured `delayMs`.
|
|
75
|
+
*/
|
|
76
|
+
schedule(task: DebouncedTask, delayMs: number = this.#delayMs): void {
|
|
77
|
+
assertValidDelay(delayMs);
|
|
78
|
+
if (this.#controller !== undefined) {
|
|
79
|
+
this.#controller.abort();
|
|
80
|
+
this.#onCancel();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const controller = new AbortController();
|
|
84
|
+
this.#controller = controller;
|
|
85
|
+
this.#onSchedule();
|
|
86
|
+
void this.#run(task, delayMs, controller);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Cancel the current task, if any. */
|
|
90
|
+
cancel(): void {
|
|
91
|
+
if (this.#controller === undefined) {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
this.#controller.abort();
|
|
95
|
+
this.#onCancel();
|
|
96
|
+
this.#becomeIdle();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async #run(task: DebouncedTask, delayMs: number, controller: AbortController): Promise<void> {
|
|
100
|
+
const signal = controller.signal;
|
|
101
|
+
try {
|
|
102
|
+
await delay(delayMs, signal);
|
|
103
|
+
this.#onStart();
|
|
104
|
+
await task(signal);
|
|
105
|
+
if (!signal.aborted) {
|
|
106
|
+
this.#onSuccess();
|
|
107
|
+
}
|
|
108
|
+
} catch (error) {
|
|
109
|
+
if (!signal.aborted) {
|
|
110
|
+
this.#onError(error);
|
|
111
|
+
}
|
|
112
|
+
} finally {
|
|
113
|
+
// A replaced or cancelled task must not touch the current state.
|
|
114
|
+
if (this.#controller === controller) {
|
|
115
|
+
this.#becomeIdle();
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
#becomeIdle(): void {
|
|
121
|
+
this.#controller = undefined;
|
|
122
|
+
this.#onIdle();
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Throw a `RangeError` unless `delayMs` is a finite number >= 0.
|
|
128
|
+
*
|
|
129
|
+
* @internal Shared with `KeyedDebouncer`; not part of the public API.
|
|
130
|
+
*/
|
|
131
|
+
export function assertValidDelay(delayMs: number): void {
|
|
132
|
+
if (!Number.isFinite(delayMs) || delayMs < 0) {
|
|
133
|
+
throw new RangeError(`delayMs must be a number >= 0 (received: ${delayMs})`);
|
|
134
|
+
}
|
|
135
|
+
}
|
package/src/functions.ts
CHANGED
|
@@ -1,14 +1,6 @@
|
|
|
1
1
|
type AnyFunction = (this: any, ...args: any[]) => any;
|
|
2
2
|
type TimerHandle = ReturnType<typeof setTimeout>;
|
|
3
3
|
|
|
4
|
-
/** Options that control when a debounced function is executed. */
|
|
5
|
-
export interface DebounceOptions {
|
|
6
|
-
/** Execute on the first call in a debounce window. Defaults to false. */
|
|
7
|
-
leading?: boolean;
|
|
8
|
-
/** Execute after the debounce window using the latest arguments. Defaults to true. */
|
|
9
|
-
trailing?: boolean;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
4
|
/** Options that control when a throttled function is executed. */
|
|
13
5
|
export interface ThrottleOptions {
|
|
14
6
|
/** Execute immediately on the first call in a throttle window. Defaults to true. */
|
|
@@ -18,15 +10,15 @@ export interface ThrottleOptions {
|
|
|
18
10
|
}
|
|
19
11
|
|
|
20
12
|
/**
|
|
21
|
-
* A delayed function returned by {@link
|
|
13
|
+
* A delayed function returned by {@link throttle}.
|
|
22
14
|
*
|
|
23
15
|
* @example
|
|
24
16
|
* ```ts
|
|
25
|
-
* const
|
|
26
|
-
*
|
|
17
|
+
* const refresh = throttle(loadData, 250);
|
|
18
|
+
* refresh();
|
|
27
19
|
*
|
|
28
|
-
* if (
|
|
29
|
-
*
|
|
20
|
+
* if (refresh.pending()) {
|
|
21
|
+
* refresh.flush();
|
|
30
22
|
* }
|
|
31
23
|
* ```
|
|
32
24
|
*/
|
|
@@ -199,107 +191,6 @@ export function memoize<F extends AnyFunction, K = Parameters<F>[0]>(
|
|
|
199
191
|
return memoized;
|
|
200
192
|
}
|
|
201
193
|
|
|
202
|
-
/**
|
|
203
|
-
* Delay a function until no calls have been made for the configured wait time.
|
|
204
|
-
* The latest arguments and this-context are used for the trailing execution.
|
|
205
|
-
*
|
|
206
|
-
* @example
|
|
207
|
-
* ```ts
|
|
208
|
-
* const search = debounce((query: string) => {
|
|
209
|
-
* console.log(query);
|
|
210
|
-
* }, 300);
|
|
211
|
-
*
|
|
212
|
-
* search("com");
|
|
213
|
-
* search("commons"); // only this value is printed after 300 ms
|
|
214
|
-
* ```
|
|
215
|
-
*
|
|
216
|
-
* @example
|
|
217
|
-
* ```ts
|
|
218
|
-
* const track = debounce(sendAnalytics, 1000, {leading: true, trailing: false});
|
|
219
|
-
*
|
|
220
|
-
* track("open"); // executed immediately
|
|
221
|
-
* track.cancel(); // cancel the pending debounce window
|
|
222
|
-
* ```
|
|
223
|
-
*/
|
|
224
|
-
export function debounce<F extends AnyFunction>(
|
|
225
|
-
fn: F,
|
|
226
|
-
wait: number,
|
|
227
|
-
options: DebounceOptions = {}
|
|
228
|
-
): DeferredFunction<F> {
|
|
229
|
-
const delay = normalizeDelay(wait);
|
|
230
|
-
const leading = options.leading ?? false;
|
|
231
|
-
const trailing = options.trailing ?? true;
|
|
232
|
-
|
|
233
|
-
let timer: TimerHandle | undefined;
|
|
234
|
-
let lastArgs: Parameters<F> | undefined;
|
|
235
|
-
let lastThis: ThisParameterType<F> | undefined;
|
|
236
|
-
let result: ReturnType<F> | undefined;
|
|
237
|
-
|
|
238
|
-
const invoke = (): ReturnType<F> => {
|
|
239
|
-
const args = lastArgs!;
|
|
240
|
-
const context = lastThis!;
|
|
241
|
-
lastArgs = undefined;
|
|
242
|
-
lastThis = undefined;
|
|
243
|
-
result = fn.apply(context, args);
|
|
244
|
-
return result as ReturnType<F>;
|
|
245
|
-
};
|
|
246
|
-
|
|
247
|
-
const expire = (): void => {
|
|
248
|
-
timer = undefined;
|
|
249
|
-
if (trailing && lastArgs) {
|
|
250
|
-
invoke();
|
|
251
|
-
} else {
|
|
252
|
-
lastArgs = undefined;
|
|
253
|
-
lastThis = undefined;
|
|
254
|
-
}
|
|
255
|
-
};
|
|
256
|
-
|
|
257
|
-
const start = (): void => {
|
|
258
|
-
timer = setTimeout(expire, delay);
|
|
259
|
-
};
|
|
260
|
-
|
|
261
|
-
const debounced = function (this: ThisParameterType<F>, ...args: Parameters<F>): ReturnType<F> | undefined {
|
|
262
|
-
const invokeLeading = leading && timer === undefined;
|
|
263
|
-
lastArgs = args;
|
|
264
|
-
lastThis = this;
|
|
265
|
-
|
|
266
|
-
if (timer !== undefined) {
|
|
267
|
-
clearTimeout(timer);
|
|
268
|
-
}
|
|
269
|
-
start();
|
|
270
|
-
|
|
271
|
-
if (invokeLeading) {
|
|
272
|
-
return invoke();
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
return result;
|
|
276
|
-
} as DeferredFunction<F>;
|
|
277
|
-
|
|
278
|
-
debounced.cancel = (): void => {
|
|
279
|
-
if (timer !== undefined) {
|
|
280
|
-
clearTimeout(timer);
|
|
281
|
-
}
|
|
282
|
-
timer = undefined;
|
|
283
|
-
lastArgs = undefined;
|
|
284
|
-
lastThis = undefined;
|
|
285
|
-
};
|
|
286
|
-
|
|
287
|
-
debounced.flush = (): ReturnType<F> | undefined => {
|
|
288
|
-
if (timer !== undefined) {
|
|
289
|
-
clearTimeout(timer);
|
|
290
|
-
timer = undefined;
|
|
291
|
-
if (lastArgs && trailing) {
|
|
292
|
-
return invoke();
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
return result;
|
|
296
|
-
};
|
|
297
|
-
|
|
298
|
-
debounced.pending = (): boolean => timer !== undefined;
|
|
299
|
-
|
|
300
|
-
return debounced;
|
|
301
|
-
}
|
|
302
|
-
|
|
303
194
|
/**
|
|
304
195
|
* Limit how often a function can run during a time window.
|
|
305
196
|
* Calls made while the function is throttled can optionally be replayed once at the end with the latest arguments.
|
package/src/index.ts
CHANGED
|
@@ -21,7 +21,13 @@ export {default as Order} from './order.js';
|
|
|
21
21
|
export {default as Timer} from './timer.js';
|
|
22
22
|
export {default as TimeSpan} from './timespan.js';
|
|
23
23
|
export {default as Stopwatch} from './stopwatch.js';
|
|
24
|
+
export type {Result} from './result.js';
|
|
25
|
+
export {TimeTrigger, CountTrigger} from './triggers.js';
|
|
24
26
|
export {default as AsyncPoolExecutor} from './async-pool-executor.js';
|
|
27
|
+
export {default as Debouncer} from './debouncer.js';
|
|
28
|
+
export type {DebouncedTask, DebouncerOptions} from './debouncer.js';
|
|
29
|
+
export {default as KeyedDebouncer} from './keyed-debouncer.js';
|
|
30
|
+
export type {KeyedDebouncerOptions} from './keyed-debouncer.js';
|
|
25
31
|
|
|
26
32
|
// ─── Patterns ────────────────────────────────────────
|
|
27
33
|
export {default as Lazy} from './lazy.js';
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import Debouncer, {assertValidDelay, type DebouncedTask} from "./debouncer.js";
|
|
2
|
+
import {noop} from "./functions.js";
|
|
3
|
+
|
|
4
|
+
/** Options accepted by {@link KeyedDebouncer}. Every callback receives the affected key. */
|
|
5
|
+
export interface KeyedDebouncerOptions<K> {
|
|
6
|
+
/** Default wait, in milliseconds, before a task is executed. */
|
|
7
|
+
delayMs: number;
|
|
8
|
+
/** Called every time a task is scheduled for `key`, after any previous task of that key has been cancelled. */
|
|
9
|
+
onSchedule?: (key: K) => void;
|
|
10
|
+
/** Called when the wait elapses and the task of `key` is about to run. */
|
|
11
|
+
onStart?: (key: K) => void;
|
|
12
|
+
/** Called when the task of `key` completes without throwing and without being cancelled. */
|
|
13
|
+
onSuccess?: (key: K) => void;
|
|
14
|
+
/** Called when a live task of `key` is cancelled, either explicitly or by being replaced. */
|
|
15
|
+
onCancel?: (key: K) => void;
|
|
16
|
+
/** Receives task errors. Cancellations never reach this callback. Defaults to `console.error`. */
|
|
17
|
+
onError?: (error: unknown, key: K) => void;
|
|
18
|
+
/** Called when `key` has no more pending work, after it has been removed from the debouncer. */
|
|
19
|
+
onIdle?: (key: K) => void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* An independent {@link Debouncer} per key.
|
|
24
|
+
* Debouncers only exist for keys with pending work: each one is created when
|
|
25
|
+
* its first task is scheduled and removed as soon as it becomes idle.
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* ```ts
|
|
29
|
+
* const debouncer = new KeyedDebouncer<string>({delayMs: 1500});
|
|
30
|
+
*
|
|
31
|
+
* debouncer.schedule(chatId, async (signal) => {
|
|
32
|
+
* const pending = await store.getPending(chatId);
|
|
33
|
+
* const reply = await callAI(pending, {signal});
|
|
34
|
+
* if (signal.aborted) return;
|
|
35
|
+
* await sendReply(chatId, reply);
|
|
36
|
+
* });
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
export default class KeyedDebouncer<K = string> {
|
|
40
|
+
readonly #debouncers = new Map<K, Debouncer>();
|
|
41
|
+
readonly #delayMs: number;
|
|
42
|
+
readonly #onSchedule: (key: K) => void;
|
|
43
|
+
readonly #onStart: (key: K) => void;
|
|
44
|
+
readonly #onSuccess: (key: K) => void;
|
|
45
|
+
readonly #onCancel: (key: K) => void;
|
|
46
|
+
readonly #onError: (error: unknown, key: K) => void;
|
|
47
|
+
readonly #onIdle: (key: K) => void;
|
|
48
|
+
|
|
49
|
+
constructor(options: KeyedDebouncerOptions<K>) {
|
|
50
|
+
assertValidDelay(options.delayMs);
|
|
51
|
+
this.#delayMs = options.delayMs;
|
|
52
|
+
this.#onSchedule = options.onSchedule ?? noop;
|
|
53
|
+
this.#onStart = options.onStart ?? noop;
|
|
54
|
+
this.#onSuccess = options.onSuccess ?? noop;
|
|
55
|
+
this.#onCancel = options.onCancel ?? noop;
|
|
56
|
+
this.#onError = options.onError ?? console.error;
|
|
57
|
+
this.#onIdle = options.onIdle ?? noop;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Number of keys with a task waiting or running. */
|
|
61
|
+
get size(): number {
|
|
62
|
+
return this.#debouncers.size;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Schedule `task` for `key`, cancelling any previous task of that key.
|
|
67
|
+
*
|
|
68
|
+
* @param key - The key whose task is replaced.
|
|
69
|
+
* @param task - The task to run once the delay elapses.
|
|
70
|
+
* @param delayMs - Wait for this task only. Defaults to the configured `delayMs`.
|
|
71
|
+
*/
|
|
72
|
+
schedule(key: K, task: DebouncedTask, delayMs?: number): void {
|
|
73
|
+
const existing = this.#debouncers.get(key);
|
|
74
|
+
if (existing !== undefined) {
|
|
75
|
+
existing.schedule(task, delayMs);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Validate first so an invalid delay never registers the key.
|
|
80
|
+
assertValidDelay(delayMs ?? this.#delayMs);
|
|
81
|
+
const debouncer = this.#create(key);
|
|
82
|
+
this.#debouncers.set(key, debouncer);
|
|
83
|
+
debouncer.schedule(task, delayMs);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Cancel the task of `key`, if any. */
|
|
87
|
+
cancel(key: K): void {
|
|
88
|
+
this.#debouncers.get(key)?.cancel();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Cancel every task. Useful when shutting the process down. */
|
|
92
|
+
cancelAll(): void {
|
|
93
|
+
// Copy first: cancelling removes the debouncer from the map.
|
|
94
|
+
for (const debouncer of [...this.#debouncers.values()]) {
|
|
95
|
+
debouncer.cancel();
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Whether a task is waiting or running for `key`. */
|
|
100
|
+
isPending(key: K): boolean {
|
|
101
|
+
return this.#debouncers.has(key);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
#create(key: K): Debouncer {
|
|
105
|
+
const debouncer: Debouncer = new Debouncer({
|
|
106
|
+
delayMs: this.#delayMs,
|
|
107
|
+
onSchedule: () => this.#onSchedule(key),
|
|
108
|
+
onStart: () => this.#onStart(key),
|
|
109
|
+
onSuccess: () => this.#onSuccess(key),
|
|
110
|
+
onCancel: () => this.#onCancel(key),
|
|
111
|
+
onError: (error) => this.#onError(error, key),
|
|
112
|
+
onIdle: () => {
|
|
113
|
+
if (this.#debouncers.get(key) === debouncer) {
|
|
114
|
+
this.#debouncers.delete(key);
|
|
115
|
+
this.#onIdle(key);
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
});
|
|
119
|
+
return debouncer;
|
|
120
|
+
}
|
|
121
|
+
}
|
package/src/result.ts
ADDED
package/src/triggers.ts
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import type {Result} from './result.js';
|
|
2
|
+
import TimeSpan from './timespan.js';
|
|
3
|
+
import type {Nullable, Supplier} from './types.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Runs a callback at most once during a configured time interval.
|
|
7
|
+
*/
|
|
8
|
+
export class TimeTrigger<T> {
|
|
9
|
+
private callback: Supplier<T | Promise<T>>;
|
|
10
|
+
private interval: TimeSpan;
|
|
11
|
+
private lastRunTime: Nullable<number>;
|
|
12
|
+
|
|
13
|
+
public constructor(callback: Supplier<T | Promise<T>>, interval: TimeSpan) {
|
|
14
|
+
this.callback = callback;
|
|
15
|
+
this.interval = interval;
|
|
16
|
+
this.lastRunTime = null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Executes the callback when the configured interval has elapsed.
|
|
21
|
+
*/
|
|
22
|
+
public async check(): Promise<Result<T>> {
|
|
23
|
+
const currentTime = performance.now();
|
|
24
|
+
|
|
25
|
+
if (this.lastRunTime === null
|
|
26
|
+
|| currentTime - this.lastRunTime >= this.interval.milliseconds
|
|
27
|
+
) {
|
|
28
|
+
return {success: true, output: await this.trigger()};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return {success: false};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Executes the callback regardless of the configured interval.
|
|
36
|
+
*/
|
|
37
|
+
public async trigger(): Promise<T> {
|
|
38
|
+
this.lastRunTime = performance.now();
|
|
39
|
+
return this.callback();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Replaces the callback executed by this trigger.
|
|
44
|
+
*/
|
|
45
|
+
public setCallback(callback: Supplier<T | Promise<T>>): void {
|
|
46
|
+
this.callback = callback;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Gets the configured time interval.
|
|
51
|
+
*/
|
|
52
|
+
public getInterval(): TimeSpan {
|
|
53
|
+
return this.interval;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Replaces the configured time interval.
|
|
58
|
+
*/
|
|
59
|
+
public setInterval(interval: TimeSpan): void {
|
|
60
|
+
this.interval = interval;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Runs a callback after a configured number of checks.
|
|
68
|
+
*/
|
|
69
|
+
export class CountTrigger<T> {
|
|
70
|
+
private callback: Supplier<T | Promise<T>>;
|
|
71
|
+
private interval: number = 1;
|
|
72
|
+
private checkCount: number = 0;
|
|
73
|
+
|
|
74
|
+
public constructor(callback: Supplier<T | Promise<T>>, interval: number) {
|
|
75
|
+
this.callback = callback;
|
|
76
|
+
this.setInterval(interval);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Executes the callback when the configured number of checks is reached.
|
|
81
|
+
*/
|
|
82
|
+
public async check(): Promise<Result<T>> {
|
|
83
|
+
this.checkCount++;
|
|
84
|
+
|
|
85
|
+
if (this.checkCount >= this.interval) {
|
|
86
|
+
return {success: true, output: await this.trigger()};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return {success: false};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Executes the callback regardless of the configured check count.
|
|
94
|
+
*/
|
|
95
|
+
public async trigger(): Promise<T> {
|
|
96
|
+
this.checkCount = 0;
|
|
97
|
+
return this.callback();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Replaces the callback executed by this trigger.
|
|
102
|
+
*/
|
|
103
|
+
public setCallback(callback: Supplier<T | Promise<T>>): void {
|
|
104
|
+
this.callback = callback;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Gets the configured check interval.
|
|
109
|
+
*/
|
|
110
|
+
public getInterval(): number {
|
|
111
|
+
return this.interval;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Replaces the configured check interval.
|
|
116
|
+
*
|
|
117
|
+
* @throws {RangeError} When the interval is less than one.
|
|
118
|
+
*/
|
|
119
|
+
public setInterval(interval: number): void {
|
|
120
|
+
if (interval < 1) {
|
|
121
|
+
throw new RangeError('Interval must be greater than zero.');
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
this.interval = interval;
|
|
125
|
+
}
|
|
126
|
+
}
|
package/src/utilities.ts
CHANGED
|
@@ -357,11 +357,37 @@ export function shallowEqual(a: any, b: any): boolean {
|
|
|
357
357
|
/**
|
|
358
358
|
* Pauses the execution of an asynchronous function for a specified duration.
|
|
359
359
|
*
|
|
360
|
-
*
|
|
361
|
-
*
|
|
360
|
+
* When `signal` is provided, the returned promise rejects with `signal.reason`
|
|
361
|
+
* as soon as the signal is aborted (or immediately if it is already aborted),
|
|
362
|
+
* and the pending timer is cleared.
|
|
363
|
+
*
|
|
364
|
+
* @param ms - The delay duration in milliseconds.
|
|
365
|
+
* @param signal - Optional signal that cancels the wait.
|
|
366
|
+
* @return A promise that resolves after the specified delay.
|
|
362
367
|
*/
|
|
363
|
-
export
|
|
364
|
-
|
|
368
|
+
export function delay(ms: number, signal?: AbortSignal): Promise<void> {
|
|
369
|
+
return new Promise<void>((resolve, reject) => {
|
|
370
|
+
if (signal === undefined) {
|
|
371
|
+
setTimeout(resolve, ms);
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
if (signal.aborted) {
|
|
376
|
+
reject(signal.reason);
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const onAbort = (): void => {
|
|
381
|
+
clearTimeout(timer);
|
|
382
|
+
reject(signal.reason);
|
|
383
|
+
};
|
|
384
|
+
const timer = setTimeout(() => {
|
|
385
|
+
signal.removeEventListener("abort", onAbort);
|
|
386
|
+
resolve();
|
|
387
|
+
}, ms);
|
|
388
|
+
|
|
389
|
+
signal.addEventListener("abort", onAbort, {once: true});
|
|
390
|
+
});
|
|
365
391
|
}
|
|
366
392
|
|
|
367
393
|
/**
|
package/dist/cjs/package.json
DELETED