@naturalcycles/js-lib 15.83.0 → 15.84.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/decorators/asyncMemo.decorator.d.ts +2 -1
- package/dist/decorators/debounce.d.ts +73 -3
- package/dist/decorators/debounce.decorator.d.ts +23 -3
- package/dist/decorators/debounce.decorator.js +31 -1
- package/dist/decorators/debounce.js +197 -41
- package/dist/decorators/decorator.util.d.ts +6 -0
- package/dist/decorators/memo.decorator.d.ts +2 -1
- package/dist/decorators/memo.util.d.ts +1 -7
- package/package.json +1 -1
- package/src/decorators/asyncMemo.decorator.ts +2 -1
- package/src/decorators/debounce.decorator.ts +59 -8
- package/src/decorators/debounce.ts +354 -95
- package/src/decorators/decorator.util.ts +11 -0
- package/src/decorators/memo.decorator.ts +2 -1
- package/src/decorators/memo.util.ts +1 -12
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { CommonLogger } from '../log/commonLogger.js';
|
|
2
2
|
import type { AnyAsyncFunction, AnyFunction, AnyObject, MaybeParameters } from '../types.js';
|
|
3
|
-
import type {
|
|
3
|
+
import type { MethodDecorator } from './decorator.util.js';
|
|
4
|
+
import type { AsyncMemoCache } from './memo.util.js';
|
|
4
5
|
export interface AsyncMemoOptions<FN> {
|
|
5
6
|
/**
|
|
6
7
|
* Provide a custom implementation of AsyncMemoCache.
|
|
@@ -1,31 +1,101 @@
|
|
|
1
|
-
import type { AnyFunction } from '../types.js';
|
|
1
|
+
import type { AnyAsyncFunction, AnyFunction } from '../types.js';
|
|
2
|
+
export declare function _debounce<T extends AnyFunction>(func: T, wait: number, opt?: DebounceOptions): T & Cancelable;
|
|
3
|
+
export declare function _throttle<T extends AnyFunction>(func: T, wait: number, opt?: ThrottleOptions): T & Cancelable;
|
|
4
|
+
/**
|
|
5
|
+
* Like `_debounce`, but for async functions.
|
|
6
|
+
*
|
|
7
|
+
* Unlike `_debounce` (which returns a stale `result`/`undefined` for suppressed calls),
|
|
8
|
+
* `_asyncDebounce` always returns a real Promise, resolving with the coalesced invocation's result.
|
|
9
|
+
*
|
|
10
|
+
* @experimental
|
|
11
|
+
*/
|
|
12
|
+
export declare function _asyncDebounce<T extends AnyAsyncFunction>(func: T, wait: number, opt?: AsyncDebounceOptions): AsyncDebounced<T>;
|
|
13
|
+
/**
|
|
14
|
+
* Like `_throttle`, but for async functions. See `_asyncDebounce` for the Promise semantics.
|
|
15
|
+
*
|
|
16
|
+
* @experimental
|
|
17
|
+
*/
|
|
18
|
+
export declare function _asyncThrottle<T extends AnyAsyncFunction>(func: T, wait: number, opt?: AsyncThrottleOptions): AsyncDebounced<T>;
|
|
2
19
|
export interface Cancelable {
|
|
3
20
|
cancel: () => void;
|
|
4
21
|
flush: () => void;
|
|
5
22
|
}
|
|
23
|
+
/**
|
|
24
|
+
* Like `Cancelable`, but for the async debounced functions: `flush()` resolves with the flushed
|
|
25
|
+
* invocation's result (or `undefined` if nothing was pending), and `pending()` is exposed.
|
|
26
|
+
*/
|
|
27
|
+
export interface AsyncCancelable<T> {
|
|
28
|
+
cancel: () => void;
|
|
29
|
+
flush: () => Promise<T | undefined>;
|
|
30
|
+
pending: () => boolean;
|
|
31
|
+
}
|
|
6
32
|
export interface ThrottleOptions {
|
|
7
33
|
/**
|
|
34
|
+
* Invoke on the leading edge of the window (immediately, on the first call).
|
|
35
|
+
*
|
|
8
36
|
* @default true
|
|
9
37
|
*/
|
|
10
38
|
leading?: boolean;
|
|
11
39
|
/**
|
|
40
|
+
* Invoke on the trailing edge of the window (after `wait` has elapsed).
|
|
41
|
+
*
|
|
12
42
|
* @default true
|
|
13
43
|
*/
|
|
14
44
|
trailing?: boolean;
|
|
15
45
|
}
|
|
16
46
|
export interface DebounceOptions {
|
|
17
47
|
/**
|
|
48
|
+
* Invoke on the leading edge of the window (immediately, on the first call).
|
|
49
|
+
*
|
|
18
50
|
* @default false
|
|
19
51
|
*/
|
|
20
52
|
leading?: boolean;
|
|
21
53
|
/**
|
|
54
|
+
* Invoke on the trailing edge of the window (after `wait` has elapsed).
|
|
55
|
+
*
|
|
56
|
+
* @default true
|
|
57
|
+
*/
|
|
58
|
+
trailing?: boolean;
|
|
59
|
+
/**
|
|
60
|
+
* Maximum time `func` is allowed to be delayed before it's forcibly invoked.
|
|
61
|
+
*/
|
|
62
|
+
maxWait?: number;
|
|
63
|
+
}
|
|
64
|
+
export interface AsyncThrottleOptions {
|
|
65
|
+
/**
|
|
66
|
+
* Invoke on the leading edge of the window (immediately, on the first call).
|
|
67
|
+
*
|
|
68
|
+
* @default true
|
|
69
|
+
*/
|
|
70
|
+
leading?: boolean;
|
|
71
|
+
/**
|
|
72
|
+
* Invoke on the trailing edge of the window (after `wait` has elapsed).
|
|
73
|
+
*
|
|
74
|
+
* When `false`, calls dropped within the window (not served by a leading invocation) resolve
|
|
75
|
+
* with `undefined`. There's no invocation for them to await. This is why the returned function
|
|
76
|
+
* can resolve with `undefined` even though the original function's return type doesn't express that.
|
|
77
|
+
*
|
|
22
78
|
* @default true
|
|
23
79
|
*/
|
|
24
80
|
trailing?: boolean;
|
|
81
|
+
}
|
|
82
|
+
export interface AsyncDebounceOptions extends AsyncThrottleOptions {
|
|
25
83
|
/**
|
|
84
|
+
* Invoke on the leading edge of the window (immediately, on the first call).
|
|
26
85
|
*
|
|
86
|
+
* @default false
|
|
87
|
+
*/
|
|
88
|
+
leading?: boolean;
|
|
89
|
+
/**
|
|
90
|
+
* Maximum time `func` is allowed to be delayed before it's forcibly invoked.
|
|
27
91
|
*/
|
|
28
92
|
maxWait?: number;
|
|
29
93
|
}
|
|
30
|
-
|
|
31
|
-
|
|
94
|
+
/**
|
|
95
|
+
* The function returned by `_asyncDebounce`/`_asyncThrottle`. Same signature as `T`, but resolves
|
|
96
|
+
* `Awaited<ReturnType<T>> | undefined`
|
|
97
|
+
*
|
|
98
|
+
* This is because some configurations (e.g. `trailing: false`) may drop calls without invoking
|
|
99
|
+
* the original function, so there's no result to await.
|
|
100
|
+
*/
|
|
101
|
+
export type AsyncDebounced<T extends AnyAsyncFunction> = ((this: ThisParameterType<T>, ...args: Parameters<T>) => Promise<Awaited<ReturnType<T>> | undefined>) & AsyncCancelable<Awaited<ReturnType<T>>>;
|
|
@@ -1,3 +1,23 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import type { AnyAsyncFunction, AnyFunction } from '../types.js';
|
|
2
|
+
import type { AsyncDebounceOptions, AsyncThrottleOptions, DebounceOptions, ThrottleOptions } from './debounce.js';
|
|
3
|
+
import type { MethodDecorator } from './decorator.util.js';
|
|
4
|
+
export declare function _Debounce<T extends AnyFunction>(wait: number, opt?: DebounceOptions): MethodDecorator<T>;
|
|
5
|
+
export declare function _Throttle<T extends AnyFunction>(wait: number, opt?: ThrottleOptions): MethodDecorator<T>;
|
|
6
|
+
/**
|
|
7
|
+
* Like `@_Debounce`, but for async methods: every call returns a real Promise resolving with the
|
|
8
|
+
* coalesced invocation's result, so `await`-ing never yields a stale value or a non-promise
|
|
9
|
+
*
|
|
10
|
+
* Be aware that the decorated method may resolve `undefined` if the config yields no invocation,
|
|
11
|
+
* which the method's `T` type can't express.
|
|
12
|
+
*
|
|
13
|
+
* @experimental
|
|
14
|
+
*/
|
|
15
|
+
export declare function _AsyncDebounce<T extends AnyAsyncFunction>(wait: number, opt?: AsyncDebounceOptions): MethodDecorator<T>;
|
|
16
|
+
/**
|
|
17
|
+
* Like `@_Throttle`, but for async methods.
|
|
18
|
+
*
|
|
19
|
+
* @see {@link _AsyncDebounce}
|
|
20
|
+
*
|
|
21
|
+
* @experimental
|
|
22
|
+
*/
|
|
23
|
+
export declare function _AsyncThrottle<T extends AnyAsyncFunction>(wait: number, opt?: AsyncThrottleOptions): MethodDecorator<T>;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { _debounce, _throttle } from './debounce.js';
|
|
1
|
+
import { _asyncDebounce, _asyncThrottle, _debounce, _throttle } from './debounce.js';
|
|
2
2
|
export function _Debounce(wait, opt = {}) {
|
|
3
3
|
return (_target, _key, descriptor) => {
|
|
4
4
|
const originalFn = descriptor.value;
|
|
@@ -13,3 +13,33 @@ export function _Throttle(wait, opt = {}) {
|
|
|
13
13
|
return descriptor;
|
|
14
14
|
};
|
|
15
15
|
}
|
|
16
|
+
/**
|
|
17
|
+
* Like `@_Debounce`, but for async methods: every call returns a real Promise resolving with the
|
|
18
|
+
* coalesced invocation's result, so `await`-ing never yields a stale value or a non-promise
|
|
19
|
+
*
|
|
20
|
+
* Be aware that the decorated method may resolve `undefined` if the config yields no invocation,
|
|
21
|
+
* which the method's `T` type can't express.
|
|
22
|
+
*
|
|
23
|
+
* @experimental
|
|
24
|
+
*/
|
|
25
|
+
export function _AsyncDebounce(wait, opt = {}) {
|
|
26
|
+
return (_target, _key, descriptor) => {
|
|
27
|
+
const originalFn = descriptor.value;
|
|
28
|
+
descriptor.value = _asyncDebounce(originalFn, wait, opt);
|
|
29
|
+
return descriptor;
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Like `@_Throttle`, but for async methods.
|
|
34
|
+
*
|
|
35
|
+
* @see {@link _AsyncDebounce}
|
|
36
|
+
*
|
|
37
|
+
* @experimental
|
|
38
|
+
*/
|
|
39
|
+
export function _AsyncThrottle(wait, opt = {}) {
|
|
40
|
+
return (_target, _key, descriptor) => {
|
|
41
|
+
const originalFn = descriptor.value;
|
|
42
|
+
descriptor.value = _asyncThrottle(originalFn, wait, opt);
|
|
43
|
+
return descriptor;
|
|
44
|
+
};
|
|
45
|
+
}
|
|
@@ -1,59 +1,43 @@
|
|
|
1
|
+
import { _anyToError } from '../error/error.util.js';
|
|
2
|
+
import { pDefer } from '../promise/pDefer.js';
|
|
1
3
|
export function _debounce(func, wait, opt = {}) {
|
|
2
4
|
let lastArgs;
|
|
3
5
|
let lastThis;
|
|
4
6
|
let result;
|
|
5
7
|
let timerId;
|
|
6
|
-
let lastCallTime;
|
|
7
|
-
let lastInvokeTime = 0;
|
|
8
8
|
const maxing = 'maxWait' in opt;
|
|
9
9
|
const { leading = false, trailing = true } = opt;
|
|
10
|
-
const
|
|
10
|
+
const state = {
|
|
11
|
+
lastCallTime: undefined,
|
|
12
|
+
lastInvokeTime: 0,
|
|
13
|
+
wait,
|
|
14
|
+
maxing,
|
|
15
|
+
maxWait: maxing ? Math.max(Number(opt.maxWait) || 0, wait) : opt.maxWait,
|
|
16
|
+
};
|
|
11
17
|
function invokeFunc(time) {
|
|
12
18
|
const args = lastArgs;
|
|
13
19
|
const thisArg = lastThis;
|
|
14
20
|
lastArgs = lastThis = undefined;
|
|
15
|
-
lastInvokeTime = time;
|
|
21
|
+
state.lastInvokeTime = time;
|
|
16
22
|
result = func.apply(thisArg, args);
|
|
17
23
|
return result;
|
|
18
24
|
}
|
|
19
|
-
function startTimer(pendingFunc, wait) {
|
|
20
|
-
return setTimeout(pendingFunc, wait);
|
|
21
|
-
}
|
|
22
|
-
function cancelTimer(id) {
|
|
23
|
-
clearTimeout(id);
|
|
24
|
-
}
|
|
25
25
|
function leadingEdge(time) {
|
|
26
26
|
// Reset any `maxWait` timer.
|
|
27
|
-
lastInvokeTime = time;
|
|
27
|
+
state.lastInvokeTime = time;
|
|
28
28
|
// Start the timer for the trailing edge.
|
|
29
|
-
timerId = startTimer(timerExpired, wait);
|
|
29
|
+
timerId = startTimer(timerExpired, state.wait);
|
|
30
30
|
// Invoke the leading edge.
|
|
31
31
|
return leading ? invokeFunc(time) : result;
|
|
32
32
|
}
|
|
33
|
-
function remainingWait(time) {
|
|
34
|
-
const timeSinceLastCall = time - lastCallTime;
|
|
35
|
-
const timeSinceLastInvoke = time - lastInvokeTime;
|
|
36
|
-
const timeWaiting = wait - timeSinceLastCall;
|
|
37
|
-
return maxing ? Math.min(timeWaiting, maxWait - timeSinceLastInvoke) : timeWaiting;
|
|
38
|
-
}
|
|
39
|
-
function shouldInvoke(time) {
|
|
40
|
-
const timeSinceLastCall = time - lastCallTime;
|
|
41
|
-
const timeSinceLastInvoke = time - lastInvokeTime;
|
|
42
|
-
// Either this is the first call, activity has stopped and we're at the
|
|
43
|
-
// trailing edge, the system time has gone backwards and we're treating
|
|
44
|
-
// it as the trailing edge, or we've hit the `maxWait` limit.
|
|
45
|
-
return (lastCallTime === undefined ||
|
|
46
|
-
timeSinceLastCall >= wait ||
|
|
47
|
-
timeSinceLastCall < 0 ||
|
|
48
|
-
(maxing && timeSinceLastInvoke >= maxWait));
|
|
49
|
-
}
|
|
50
33
|
function timerExpired() {
|
|
51
34
|
const time = Date.now();
|
|
52
|
-
if (shouldInvoke(time)) {
|
|
53
|
-
|
|
35
|
+
if (shouldInvoke(time, state)) {
|
|
36
|
+
trailingEdge(time);
|
|
37
|
+
return;
|
|
54
38
|
}
|
|
55
39
|
// Restart the timer.
|
|
56
|
-
timerId = startTimer(timerExpired, remainingWait(time));
|
|
40
|
+
timerId = startTimer(timerExpired, remainingWait(time, state));
|
|
57
41
|
}
|
|
58
42
|
function trailingEdge(time) {
|
|
59
43
|
timerId = undefined;
|
|
@@ -69,8 +53,9 @@ export function _debounce(func, wait, opt = {}) {
|
|
|
69
53
|
if (timerId !== undefined) {
|
|
70
54
|
cancelTimer(timerId);
|
|
71
55
|
}
|
|
72
|
-
lastInvokeTime = 0;
|
|
73
|
-
|
|
56
|
+
state.lastInvokeTime = 0;
|
|
57
|
+
state.lastCallTime = undefined;
|
|
58
|
+
lastArgs = lastThis = timerId = undefined;
|
|
74
59
|
}
|
|
75
60
|
function flush() {
|
|
76
61
|
return timerId === undefined ? result : trailingEdge(Date.now());
|
|
@@ -80,22 +65,22 @@ export function _debounce(func, wait, opt = {}) {
|
|
|
80
65
|
}
|
|
81
66
|
function debounced(...args) {
|
|
82
67
|
const time = Date.now();
|
|
83
|
-
const isInvoking = shouldInvoke(time);
|
|
68
|
+
const isInvoking = shouldInvoke(time, state);
|
|
84
69
|
lastArgs = args;
|
|
85
70
|
lastThis = this;
|
|
86
|
-
lastCallTime = time;
|
|
71
|
+
state.lastCallTime = time;
|
|
87
72
|
if (isInvoking) {
|
|
88
73
|
if (timerId === undefined) {
|
|
89
|
-
return leadingEdge(lastCallTime);
|
|
74
|
+
return leadingEdge(state.lastCallTime);
|
|
90
75
|
}
|
|
91
|
-
if (maxing) {
|
|
76
|
+
if (state.maxing) {
|
|
92
77
|
// Handle invocations in a tight loop.
|
|
93
|
-
timerId = startTimer(timerExpired, wait);
|
|
94
|
-
return invokeFunc(lastCallTime);
|
|
78
|
+
timerId = startTimer(timerExpired, state.wait);
|
|
79
|
+
return invokeFunc(state.lastCallTime);
|
|
95
80
|
}
|
|
96
81
|
}
|
|
97
82
|
if (timerId === undefined) {
|
|
98
|
-
timerId = startTimer(timerExpired, wait);
|
|
83
|
+
timerId = startTimer(timerExpired, state.wait);
|
|
99
84
|
}
|
|
100
85
|
return result;
|
|
101
86
|
}
|
|
@@ -112,3 +97,174 @@ export function _throttle(func, wait, opt = {}) {
|
|
|
112
97
|
maxWait: wait,
|
|
113
98
|
});
|
|
114
99
|
}
|
|
100
|
+
/**
|
|
101
|
+
* Like `_debounce`, but for async functions.
|
|
102
|
+
*
|
|
103
|
+
* Unlike `_debounce` (which returns a stale `result`/`undefined` for suppressed calls),
|
|
104
|
+
* `_asyncDebounce` always returns a real Promise, resolving with the coalesced invocation's result.
|
|
105
|
+
*
|
|
106
|
+
* @experimental
|
|
107
|
+
*/
|
|
108
|
+
export function _asyncDebounce(func, wait, opt = {}) {
|
|
109
|
+
let lastArgs;
|
|
110
|
+
let lastThis;
|
|
111
|
+
let timerId;
|
|
112
|
+
// Promise shared by all calls coalesced into the next invocation (undefined when none pending).
|
|
113
|
+
let deferred;
|
|
114
|
+
const { leading = false, trailing = true } = opt;
|
|
115
|
+
const maxing = 'maxWait' in opt;
|
|
116
|
+
const maxWait = maxing ? Math.max(Number(opt.maxWait) || 0, wait) : opt.maxWait;
|
|
117
|
+
const state = {
|
|
118
|
+
lastCallTime: undefined,
|
|
119
|
+
lastInvokeTime: 0,
|
|
120
|
+
wait,
|
|
121
|
+
maxing,
|
|
122
|
+
maxWait,
|
|
123
|
+
};
|
|
124
|
+
async function invokeFunc(time) {
|
|
125
|
+
const args = lastArgs;
|
|
126
|
+
const thisArg = lastThis;
|
|
127
|
+
lastArgs = lastThis = undefined;
|
|
128
|
+
state.lastInvokeTime = time;
|
|
129
|
+
// Detach and settle the current batch.
|
|
130
|
+
const d = deferred;
|
|
131
|
+
deferred = undefined;
|
|
132
|
+
try {
|
|
133
|
+
const result = await func.apply(thisArg, args);
|
|
134
|
+
d.resolve(result);
|
|
135
|
+
}
|
|
136
|
+
catch (err) {
|
|
137
|
+
d.reject(_anyToError(err));
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
async function leadingEdge(time) {
|
|
141
|
+
// Reset any `maxWait` timer.
|
|
142
|
+
state.lastInvokeTime = time;
|
|
143
|
+
// Start the timer for the trailing edge.
|
|
144
|
+
timerId = startTimer(timerExpired, state.wait);
|
|
145
|
+
// Capture the batch before `invokeFunc` detaches it.
|
|
146
|
+
const d = deferred;
|
|
147
|
+
// Invoke the leading edge.
|
|
148
|
+
if (leading) {
|
|
149
|
+
await invokeFunc(time);
|
|
150
|
+
}
|
|
151
|
+
return d;
|
|
152
|
+
}
|
|
153
|
+
function timerExpired() {
|
|
154
|
+
const time = Date.now();
|
|
155
|
+
if (shouldInvoke(time, state)) {
|
|
156
|
+
void trailingEdge(time);
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
// Restart the timer.
|
|
160
|
+
timerId = startTimer(timerExpired, remainingWait(time, state));
|
|
161
|
+
}
|
|
162
|
+
async function trailingEdge(time) {
|
|
163
|
+
timerId = undefined;
|
|
164
|
+
// Only invoke if we have `lastArgs` which means `func` has been
|
|
165
|
+
// debounced at least once.
|
|
166
|
+
if (trailing && lastArgs && deferred) {
|
|
167
|
+
await invokeFunc(time);
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
lastArgs = lastThis = undefined;
|
|
171
|
+
// Dropped batch (only reachable with `trailing: false`): no invocation, so resolve `undefined`.
|
|
172
|
+
if (deferred) {
|
|
173
|
+
const d = deferred;
|
|
174
|
+
deferred = undefined;
|
|
175
|
+
d.resolve(undefined);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
function cancel() {
|
|
179
|
+
if (timerId !== undefined) {
|
|
180
|
+
cancelTimer(timerId);
|
|
181
|
+
}
|
|
182
|
+
state.lastInvokeTime = 0;
|
|
183
|
+
state.lastCallTime = undefined;
|
|
184
|
+
lastArgs = lastThis = timerId = undefined;
|
|
185
|
+
// Reject pending awaiters.
|
|
186
|
+
if (deferred) {
|
|
187
|
+
const d = deferred;
|
|
188
|
+
deferred = undefined;
|
|
189
|
+
d.reject(new Error('asyncDebounce cancelled'));
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
async function flush() {
|
|
193
|
+
if (timerId === undefined || !deferred) {
|
|
194
|
+
return undefined;
|
|
195
|
+
}
|
|
196
|
+
const d = deferred;
|
|
197
|
+
cancelTimer(timerId);
|
|
198
|
+
await trailingEdge(Date.now());
|
|
199
|
+
return d;
|
|
200
|
+
}
|
|
201
|
+
function pending() {
|
|
202
|
+
return timerId !== undefined;
|
|
203
|
+
}
|
|
204
|
+
async function debounced(...args) {
|
|
205
|
+
const time = Date.now();
|
|
206
|
+
const isInvoking = shouldInvoke(time, state);
|
|
207
|
+
lastArgs = args;
|
|
208
|
+
lastThis = this;
|
|
209
|
+
state.lastCallTime = time;
|
|
210
|
+
deferred ||= pDefer();
|
|
211
|
+
const currentDeferred = deferred;
|
|
212
|
+
if (isInvoking) {
|
|
213
|
+
if (timerId === undefined) {
|
|
214
|
+
return leadingEdge(state.lastCallTime);
|
|
215
|
+
}
|
|
216
|
+
if (state.maxing) {
|
|
217
|
+
// Handle invocations in a tight loop.
|
|
218
|
+
timerId = startTimer(timerExpired, state.wait);
|
|
219
|
+
await invokeFunc(state.lastCallTime);
|
|
220
|
+
return currentDeferred;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
if (timerId === undefined) {
|
|
224
|
+
timerId = startTimer(timerExpired, state.wait);
|
|
225
|
+
}
|
|
226
|
+
return currentDeferred;
|
|
227
|
+
}
|
|
228
|
+
debounced.cancel = cancel;
|
|
229
|
+
debounced.flush = flush;
|
|
230
|
+
debounced.pending = pending;
|
|
231
|
+
return debounced;
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Like `_throttle`, but for async functions. See `_asyncDebounce` for the Promise semantics.
|
|
235
|
+
*
|
|
236
|
+
* @experimental
|
|
237
|
+
*/
|
|
238
|
+
export function _asyncThrottle(func, wait, opt = {}) {
|
|
239
|
+
return _asyncDebounce(func, wait, {
|
|
240
|
+
leading: true,
|
|
241
|
+
trailing: true,
|
|
242
|
+
...opt,
|
|
243
|
+
maxWait: wait,
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
function remainingWait(time, state) {
|
|
247
|
+
const { lastCallTime, lastInvokeTime, wait, maxing, maxWait } = state;
|
|
248
|
+
const timeSinceLastCall = time - lastCallTime;
|
|
249
|
+
const timeSinceLastInvoke = time - lastInvokeTime;
|
|
250
|
+
const timeWaiting = wait - timeSinceLastCall;
|
|
251
|
+
return maxing ? Math.min(timeWaiting, maxWait - timeSinceLastInvoke) : timeWaiting;
|
|
252
|
+
}
|
|
253
|
+
function shouldInvoke(time, state) {
|
|
254
|
+
const { lastCallTime, lastInvokeTime, wait, maxing, maxWait } = state;
|
|
255
|
+
const timeSinceLastCall = time - lastCallTime;
|
|
256
|
+
const timeSinceLastInvoke = time - lastInvokeTime;
|
|
257
|
+
// Either this is the first call, activity has stopped and we're at the
|
|
258
|
+
// trailing edge, the system time has gone backwards and we're treating
|
|
259
|
+
// it as the trailing edge, or we've hit the `maxWait` limit.
|
|
260
|
+
return (lastCallTime === undefined ||
|
|
261
|
+
timeSinceLastCall >= wait ||
|
|
262
|
+
timeSinceLastCall < 0 ||
|
|
263
|
+
(maxing && timeSinceLastInvoke >= maxWait));
|
|
264
|
+
}
|
|
265
|
+
function startTimer(pendingFunc, wait) {
|
|
266
|
+
return setTimeout(pendingFunc, wait);
|
|
267
|
+
}
|
|
268
|
+
function cancelTimer(id) {
|
|
269
|
+
clearTimeout(id);
|
|
270
|
+
}
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
import type { AnyObject } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Generic override of TypeScript's built-in legacy MethodDecorator, that
|
|
4
|
+
* allows us to infer the parameters of the decorated method from the parameters
|
|
5
|
+
* of a decorator.
|
|
6
|
+
*/
|
|
7
|
+
export type MethodDecorator<T> = (target: AnyObject, propertyKey: string | symbol, descriptor: TypedPropertyDescriptor<T>) => TypedPropertyDescriptor<T> | undefined;
|
|
2
8
|
/**
|
|
3
9
|
* @returns
|
|
4
10
|
* e.g `NameOfYourClass.methodName`
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { CommonLogger } from '../log/commonLogger.js';
|
|
2
2
|
import type { AnyFunction, AnyObject, MaybeParameters } from '../types.js';
|
|
3
|
-
import type {
|
|
3
|
+
import type { MethodDecorator } from './decorator.util.js';
|
|
4
|
+
import type { MemoCache } from './memo.util.js';
|
|
4
5
|
export interface MemoOptions<FN> {
|
|
5
6
|
/**
|
|
6
7
|
* Provide a custom implementation of MemoCache.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { UnixTimestamp } from '../types.js';
|
|
2
2
|
import { MISS } from '../types.js';
|
|
3
3
|
export type MemoSerializer = (args: any[]) => any;
|
|
4
4
|
export declare const jsonMemoSerializer: MemoSerializer;
|
|
@@ -59,9 +59,3 @@ export declare class MapAsyncMemoCache<KEY = any, VALUE = any> implements AsyncM
|
|
|
59
59
|
set(k: KEY, v: VALUE): Promise<void>;
|
|
60
60
|
clear(): Promise<void>;
|
|
61
61
|
}
|
|
62
|
-
/**
|
|
63
|
-
* Generic override of Typescript's built in legacy MethodDecorator, that
|
|
64
|
-
* allows us to infer the parameters of the decorated method from the parameters
|
|
65
|
-
* of a decorator.
|
|
66
|
-
*/
|
|
67
|
-
export type MethodDecorator<T> = (target: AnyObject, propertyKey: string | symbol, descriptor: TypedPropertyDescriptor<T>) => TypedPropertyDescriptor<T> | undefined;
|
package/package.json
CHANGED
|
@@ -2,8 +2,9 @@ import { _assert, _assertTypeOf } from '../error/assert.js'
|
|
|
2
2
|
import type { CommonLogger } from '../log/commonLogger.js'
|
|
3
3
|
import type { AnyAsyncFunction, AnyFunction, AnyObject, MaybeParameters } from '../types.js'
|
|
4
4
|
import { _objectAssign, MISS } from '../types.js'
|
|
5
|
+
import type { MethodDecorator } from './decorator.util.js'
|
|
5
6
|
import { _getTargetMethodSignature } from './decorator.util.js'
|
|
6
|
-
import type { AsyncMemoCache
|
|
7
|
+
import type { AsyncMemoCache } from './memo.util.js'
|
|
7
8
|
import { jsonMemoSerializer } from './memo.util.js'
|
|
8
9
|
|
|
9
10
|
export interface AsyncMemoOptions<FN> {
|
|
@@ -1,18 +1,69 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import {
|
|
1
|
+
import type { AnyAsyncFunction, AnyFunction } from '../types.js'
|
|
2
|
+
import type {
|
|
3
|
+
AsyncDebounceOptions,
|
|
4
|
+
AsyncThrottleOptions,
|
|
5
|
+
DebounceOptions,
|
|
6
|
+
ThrottleOptions,
|
|
7
|
+
} from './debounce.js'
|
|
8
|
+
import { _asyncDebounce, _asyncThrottle, _debounce, _throttle } from './debounce.js'
|
|
9
|
+
import type { MethodDecorator } from './decorator.util.js'
|
|
3
10
|
|
|
4
|
-
export function _Debounce
|
|
11
|
+
export function _Debounce<T extends AnyFunction>(
|
|
12
|
+
wait: number,
|
|
13
|
+
opt: DebounceOptions = {},
|
|
14
|
+
): MethodDecorator<T> {
|
|
5
15
|
return (_target, _key, descriptor) => {
|
|
6
|
-
const originalFn = descriptor.value
|
|
7
|
-
descriptor.value = _debounce(originalFn
|
|
16
|
+
const originalFn = descriptor.value!
|
|
17
|
+
descriptor.value = _debounce<T>(originalFn, wait, opt)
|
|
8
18
|
return descriptor
|
|
9
19
|
}
|
|
10
20
|
}
|
|
11
21
|
|
|
12
|
-
export function _Throttle
|
|
22
|
+
export function _Throttle<T extends AnyFunction>(
|
|
23
|
+
wait: number,
|
|
24
|
+
opt: ThrottleOptions = {},
|
|
25
|
+
): MethodDecorator<T> {
|
|
13
26
|
return (_target, _key, descriptor) => {
|
|
14
|
-
const originalFn = descriptor.value
|
|
15
|
-
descriptor.value = _throttle(originalFn
|
|
27
|
+
const originalFn = descriptor.value!
|
|
28
|
+
descriptor.value = _throttle<T>(originalFn, wait, opt)
|
|
29
|
+
return descriptor
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Like `@_Debounce`, but for async methods: every call returns a real Promise resolving with the
|
|
35
|
+
* coalesced invocation's result, so `await`-ing never yields a stale value or a non-promise
|
|
36
|
+
*
|
|
37
|
+
* Be aware that the decorated method may resolve `undefined` if the config yields no invocation,
|
|
38
|
+
* which the method's `T` type can't express.
|
|
39
|
+
*
|
|
40
|
+
* @experimental
|
|
41
|
+
*/
|
|
42
|
+
export function _AsyncDebounce<T extends AnyAsyncFunction>(
|
|
43
|
+
wait: number,
|
|
44
|
+
opt: AsyncDebounceOptions = {},
|
|
45
|
+
): MethodDecorator<T> {
|
|
46
|
+
return (_target, _key, descriptor) => {
|
|
47
|
+
const originalFn = descriptor.value!
|
|
48
|
+
descriptor.value = _asyncDebounce<T>(originalFn, wait, opt) as any
|
|
49
|
+
return descriptor
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Like `@_Throttle`, but for async methods.
|
|
55
|
+
*
|
|
56
|
+
* @see {@link _AsyncDebounce}
|
|
57
|
+
*
|
|
58
|
+
* @experimental
|
|
59
|
+
*/
|
|
60
|
+
export function _AsyncThrottle<T extends AnyAsyncFunction>(
|
|
61
|
+
wait: number,
|
|
62
|
+
opt: AsyncThrottleOptions = {},
|
|
63
|
+
): MethodDecorator<T> {
|
|
64
|
+
return (_target, _key, descriptor) => {
|
|
65
|
+
const originalFn = descriptor.value!
|
|
66
|
+
descriptor.value = _asyncThrottle<T>(originalFn, wait, opt) as any
|
|
16
67
|
return descriptor
|
|
17
68
|
}
|
|
18
69
|
}
|
|
@@ -1,116 +1,58 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
flush: () => void
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
export interface ThrottleOptions {
|
|
9
|
-
/**
|
|
10
|
-
* @default true
|
|
11
|
-
*/
|
|
12
|
-
leading?: boolean
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* @default true
|
|
16
|
-
*/
|
|
17
|
-
trailing?: boolean
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export interface DebounceOptions {
|
|
21
|
-
/**
|
|
22
|
-
* @default false
|
|
23
|
-
*/
|
|
24
|
-
leading?: boolean
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* @default true
|
|
28
|
-
*/
|
|
29
|
-
trailing?: boolean
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
*
|
|
33
|
-
*/
|
|
34
|
-
maxWait?: number
|
|
35
|
-
}
|
|
1
|
+
import { _anyToError } from '../error/error.util.js'
|
|
2
|
+
import { pDefer } from '../promise/pDefer.js'
|
|
3
|
+
import type { DeferredPromise } from '../promise/pDefer.js'
|
|
4
|
+
import type { AnyAsyncFunction, AnyFunction } from '../types.js'
|
|
36
5
|
|
|
37
6
|
export function _debounce<T extends AnyFunction>(
|
|
38
7
|
func: T,
|
|
39
8
|
wait: number,
|
|
40
9
|
opt: DebounceOptions = {},
|
|
41
10
|
): T & Cancelable {
|
|
42
|
-
let lastArgs:
|
|
43
|
-
let lastThis:
|
|
44
|
-
let result:
|
|
11
|
+
let lastArgs: Parameters<T> | undefined
|
|
12
|
+
let lastThis: ThisParameterType<T> | undefined
|
|
13
|
+
let result: ReturnType<T>
|
|
45
14
|
let timerId: number | undefined
|
|
46
|
-
let lastCallTime: number | undefined
|
|
47
15
|
|
|
48
|
-
let lastInvokeTime = 0
|
|
49
16
|
const maxing = 'maxWait' in opt
|
|
50
|
-
|
|
51
17
|
const { leading = false, trailing = true } = opt
|
|
52
|
-
const
|
|
18
|
+
const state: DebounceTimerState = {
|
|
19
|
+
lastCallTime: undefined,
|
|
20
|
+
lastInvokeTime: 0,
|
|
21
|
+
wait,
|
|
22
|
+
maxing,
|
|
23
|
+
maxWait: maxing ? Math.max(Number(opt.maxWait) || 0, wait) : opt.maxWait,
|
|
24
|
+
}
|
|
53
25
|
|
|
54
|
-
function invokeFunc(time: number):
|
|
26
|
+
function invokeFunc(time: number): ReturnType<T> {
|
|
55
27
|
const args = lastArgs
|
|
56
28
|
const thisArg = lastThis
|
|
57
29
|
|
|
58
30
|
lastArgs = lastThis = undefined
|
|
59
|
-
lastInvokeTime = time
|
|
60
|
-
result = func.apply(thisArg, args)
|
|
31
|
+
state.lastInvokeTime = time
|
|
32
|
+
result = func.apply(thisArg, args!)
|
|
61
33
|
return result
|
|
62
34
|
}
|
|
63
35
|
|
|
64
|
-
function
|
|
65
|
-
return setTimeout(pendingFunc, wait)
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function cancelTimer(id: number): void {
|
|
69
|
-
clearTimeout(id)
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function leadingEdge(time: number): any {
|
|
36
|
+
function leadingEdge(time: number): ReturnType<T> {
|
|
73
37
|
// Reset any `maxWait` timer.
|
|
74
|
-
lastInvokeTime = time
|
|
38
|
+
state.lastInvokeTime = time
|
|
75
39
|
// Start the timer for the trailing edge.
|
|
76
|
-
timerId = startTimer(timerExpired, wait)
|
|
40
|
+
timerId = startTimer(timerExpired, state.wait)
|
|
77
41
|
// Invoke the leading edge.
|
|
78
42
|
return leading ? invokeFunc(time) : result
|
|
79
43
|
}
|
|
80
44
|
|
|
81
|
-
function
|
|
82
|
-
const timeSinceLastCall = time - lastCallTime!
|
|
83
|
-
const timeSinceLastInvoke = time - lastInvokeTime
|
|
84
|
-
const timeWaiting = wait - timeSinceLastCall
|
|
85
|
-
|
|
86
|
-
return maxing ? Math.min(timeWaiting, maxWait! - timeSinceLastInvoke) : timeWaiting
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
function shouldInvoke(time: number): boolean {
|
|
90
|
-
const timeSinceLastCall = time - lastCallTime!
|
|
91
|
-
const timeSinceLastInvoke = time - lastInvokeTime
|
|
92
|
-
|
|
93
|
-
// Either this is the first call, activity has stopped and we're at the
|
|
94
|
-
// trailing edge, the system time has gone backwards and we're treating
|
|
95
|
-
// it as the trailing edge, or we've hit the `maxWait` limit.
|
|
96
|
-
return (
|
|
97
|
-
lastCallTime === undefined ||
|
|
98
|
-
timeSinceLastCall >= wait ||
|
|
99
|
-
timeSinceLastCall < 0 ||
|
|
100
|
-
(maxing && timeSinceLastInvoke >= maxWait!)
|
|
101
|
-
)
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
function timerExpired(): any {
|
|
45
|
+
function timerExpired(): void {
|
|
105
46
|
const time = Date.now()
|
|
106
|
-
if (shouldInvoke(time)) {
|
|
107
|
-
|
|
47
|
+
if (shouldInvoke(time, state)) {
|
|
48
|
+
trailingEdge(time)
|
|
49
|
+
return
|
|
108
50
|
}
|
|
109
51
|
// Restart the timer.
|
|
110
|
-
timerId = startTimer(timerExpired, remainingWait(time))
|
|
52
|
+
timerId = startTimer(timerExpired, remainingWait(time, state))
|
|
111
53
|
}
|
|
112
54
|
|
|
113
|
-
function trailingEdge(time: number):
|
|
55
|
+
function trailingEdge(time: number): ReturnType<T> {
|
|
114
56
|
timerId = undefined
|
|
115
57
|
|
|
116
58
|
// Only invoke if we have `lastArgs` which means `func` has been
|
|
@@ -126,11 +68,12 @@ export function _debounce<T extends AnyFunction>(
|
|
|
126
68
|
if (timerId !== undefined) {
|
|
127
69
|
cancelTimer(timerId)
|
|
128
70
|
}
|
|
129
|
-
lastInvokeTime = 0
|
|
130
|
-
|
|
71
|
+
state.lastInvokeTime = 0
|
|
72
|
+
state.lastCallTime = undefined
|
|
73
|
+
lastArgs = lastThis = timerId = undefined
|
|
131
74
|
}
|
|
132
75
|
|
|
133
|
-
function flush():
|
|
76
|
+
function flush(): ReturnType<T> {
|
|
134
77
|
return timerId === undefined ? result : trailingEdge(Date.now())
|
|
135
78
|
}
|
|
136
79
|
|
|
@@ -138,26 +81,26 @@ export function _debounce<T extends AnyFunction>(
|
|
|
138
81
|
return timerId !== undefined
|
|
139
82
|
}
|
|
140
83
|
|
|
141
|
-
function debounced(this:
|
|
84
|
+
function debounced(this: ThisParameterType<T>, ...args: Parameters<T>): ReturnType<T> {
|
|
142
85
|
const time = Date.now()
|
|
143
|
-
const isInvoking = shouldInvoke(time)
|
|
86
|
+
const isInvoking = shouldInvoke(time, state)
|
|
144
87
|
|
|
145
88
|
lastArgs = args
|
|
146
89
|
lastThis = this
|
|
147
|
-
lastCallTime = time
|
|
90
|
+
state.lastCallTime = time
|
|
148
91
|
|
|
149
92
|
if (isInvoking) {
|
|
150
93
|
if (timerId === undefined) {
|
|
151
|
-
return leadingEdge(lastCallTime)
|
|
94
|
+
return leadingEdge(state.lastCallTime)
|
|
152
95
|
}
|
|
153
|
-
if (maxing) {
|
|
96
|
+
if (state.maxing) {
|
|
154
97
|
// Handle invocations in a tight loop.
|
|
155
|
-
timerId = startTimer(timerExpired, wait)
|
|
156
|
-
return invokeFunc(lastCallTime)
|
|
98
|
+
timerId = startTimer(timerExpired, state.wait)
|
|
99
|
+
return invokeFunc(state.lastCallTime)
|
|
157
100
|
}
|
|
158
101
|
}
|
|
159
102
|
if (timerId === undefined) {
|
|
160
|
-
timerId = startTimer(timerExpired, wait)
|
|
103
|
+
timerId = startTimer(timerExpired, state.wait)
|
|
161
104
|
}
|
|
162
105
|
return result
|
|
163
106
|
}
|
|
@@ -180,3 +123,319 @@ export function _throttle<T extends AnyFunction>(
|
|
|
180
123
|
maxWait: wait,
|
|
181
124
|
})
|
|
182
125
|
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Like `_debounce`, but for async functions.
|
|
129
|
+
*
|
|
130
|
+
* Unlike `_debounce` (which returns a stale `result`/`undefined` for suppressed calls),
|
|
131
|
+
* `_asyncDebounce` always returns a real Promise, resolving with the coalesced invocation's result.
|
|
132
|
+
*
|
|
133
|
+
* @experimental
|
|
134
|
+
*/
|
|
135
|
+
export function _asyncDebounce<T extends AnyAsyncFunction>(
|
|
136
|
+
func: T,
|
|
137
|
+
wait: number,
|
|
138
|
+
opt: AsyncDebounceOptions = {},
|
|
139
|
+
): AsyncDebounced<T> {
|
|
140
|
+
let lastArgs: Parameters<T> | undefined
|
|
141
|
+
let lastThis: ThisParameterType<T> | undefined
|
|
142
|
+
let timerId: number | undefined
|
|
143
|
+
// Promise shared by all calls coalesced into the next invocation (undefined when none pending).
|
|
144
|
+
let deferred: DeferredPromise<Awaited<ReturnType<T>> | undefined> | undefined
|
|
145
|
+
|
|
146
|
+
const { leading = false, trailing = true } = opt
|
|
147
|
+
|
|
148
|
+
const maxing = 'maxWait' in opt
|
|
149
|
+
const maxWait = maxing ? Math.max(Number(opt.maxWait) || 0, wait) : opt.maxWait
|
|
150
|
+
|
|
151
|
+
const state: DebounceTimerState = {
|
|
152
|
+
lastCallTime: undefined,
|
|
153
|
+
lastInvokeTime: 0,
|
|
154
|
+
wait,
|
|
155
|
+
maxing,
|
|
156
|
+
maxWait,
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async function invokeFunc(time: number): Promise<void> {
|
|
160
|
+
const args = lastArgs
|
|
161
|
+
const thisArg = lastThis
|
|
162
|
+
|
|
163
|
+
lastArgs = lastThis = undefined
|
|
164
|
+
state.lastInvokeTime = time
|
|
165
|
+
|
|
166
|
+
// Detach and settle the current batch.
|
|
167
|
+
const d = deferred!
|
|
168
|
+
deferred = undefined
|
|
169
|
+
try {
|
|
170
|
+
const result = await func.apply(thisArg, args!)
|
|
171
|
+
d.resolve(result)
|
|
172
|
+
} catch (err) {
|
|
173
|
+
d.reject(_anyToError(err))
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async function leadingEdge(time: number): Promise<Awaited<ReturnType<T>> | undefined> {
|
|
178
|
+
// Reset any `maxWait` timer.
|
|
179
|
+
state.lastInvokeTime = time
|
|
180
|
+
// Start the timer for the trailing edge.
|
|
181
|
+
timerId = startTimer(timerExpired, state.wait)
|
|
182
|
+
// Capture the batch before `invokeFunc` detaches it.
|
|
183
|
+
const d = deferred!
|
|
184
|
+
// Invoke the leading edge.
|
|
185
|
+
if (leading) {
|
|
186
|
+
await invokeFunc(time)
|
|
187
|
+
}
|
|
188
|
+
return d
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function timerExpired(): void {
|
|
192
|
+
const time = Date.now()
|
|
193
|
+
if (shouldInvoke(time, state)) {
|
|
194
|
+
void trailingEdge(time)
|
|
195
|
+
return
|
|
196
|
+
}
|
|
197
|
+
// Restart the timer.
|
|
198
|
+
timerId = startTimer(timerExpired, remainingWait(time, state))
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
async function trailingEdge(time: number): Promise<void> {
|
|
202
|
+
timerId = undefined
|
|
203
|
+
|
|
204
|
+
// Only invoke if we have `lastArgs` which means `func` has been
|
|
205
|
+
// debounced at least once.
|
|
206
|
+
if (trailing && lastArgs && deferred) {
|
|
207
|
+
await invokeFunc(time)
|
|
208
|
+
return
|
|
209
|
+
}
|
|
210
|
+
lastArgs = lastThis = undefined
|
|
211
|
+
// Dropped batch (only reachable with `trailing: false`): no invocation, so resolve `undefined`.
|
|
212
|
+
if (deferred) {
|
|
213
|
+
const d = deferred
|
|
214
|
+
deferred = undefined
|
|
215
|
+
d.resolve(undefined)
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function cancel(): void {
|
|
220
|
+
if (timerId !== undefined) {
|
|
221
|
+
cancelTimer(timerId)
|
|
222
|
+
}
|
|
223
|
+
state.lastInvokeTime = 0
|
|
224
|
+
state.lastCallTime = undefined
|
|
225
|
+
lastArgs = lastThis = timerId = undefined
|
|
226
|
+
// Reject pending awaiters.
|
|
227
|
+
if (deferred) {
|
|
228
|
+
const d = deferred
|
|
229
|
+
deferred = undefined
|
|
230
|
+
d.reject(new Error('asyncDebounce cancelled'))
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
async function flush(): Promise<Awaited<ReturnType<T>> | undefined> {
|
|
235
|
+
if (timerId === undefined || !deferred) {
|
|
236
|
+
return undefined
|
|
237
|
+
}
|
|
238
|
+
const d = deferred
|
|
239
|
+
cancelTimer(timerId)
|
|
240
|
+
await trailingEdge(Date.now())
|
|
241
|
+
return d
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function pending(): boolean {
|
|
245
|
+
return timerId !== undefined
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
async function debounced(
|
|
249
|
+
this: ThisParameterType<T>,
|
|
250
|
+
...args: Parameters<T>
|
|
251
|
+
): Promise<Awaited<ReturnType<T>> | undefined> {
|
|
252
|
+
const time = Date.now()
|
|
253
|
+
const isInvoking = shouldInvoke(time, state)
|
|
254
|
+
|
|
255
|
+
lastArgs = args
|
|
256
|
+
lastThis = this
|
|
257
|
+
state.lastCallTime = time
|
|
258
|
+
|
|
259
|
+
deferred ||= pDefer<Awaited<ReturnType<T>> | undefined>()
|
|
260
|
+
const currentDeferred = deferred
|
|
261
|
+
|
|
262
|
+
if (isInvoking) {
|
|
263
|
+
if (timerId === undefined) {
|
|
264
|
+
return leadingEdge(state.lastCallTime)
|
|
265
|
+
}
|
|
266
|
+
if (state.maxing) {
|
|
267
|
+
// Handle invocations in a tight loop.
|
|
268
|
+
timerId = startTimer(timerExpired, state.wait)
|
|
269
|
+
await invokeFunc(state.lastCallTime)
|
|
270
|
+
return currentDeferred
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
if (timerId === undefined) {
|
|
274
|
+
timerId = startTimer(timerExpired, state.wait)
|
|
275
|
+
}
|
|
276
|
+
return currentDeferred
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
debounced.cancel = cancel
|
|
280
|
+
debounced.flush = flush
|
|
281
|
+
debounced.pending = pending
|
|
282
|
+
return debounced as any
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Like `_throttle`, but for async functions. See `_asyncDebounce` for the Promise semantics.
|
|
287
|
+
*
|
|
288
|
+
* @experimental
|
|
289
|
+
*/
|
|
290
|
+
export function _asyncThrottle<T extends AnyAsyncFunction>(
|
|
291
|
+
func: T,
|
|
292
|
+
wait: number,
|
|
293
|
+
opt: AsyncThrottleOptions = {},
|
|
294
|
+
): AsyncDebounced<T> {
|
|
295
|
+
return _asyncDebounce(func, wait, {
|
|
296
|
+
leading: true,
|
|
297
|
+
trailing: true,
|
|
298
|
+
...opt,
|
|
299
|
+
maxWait: wait,
|
|
300
|
+
})
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function remainingWait(time: number, state: DebounceTimerState): number {
|
|
304
|
+
const { lastCallTime, lastInvokeTime, wait, maxing, maxWait } = state
|
|
305
|
+
const timeSinceLastCall = time - lastCallTime!
|
|
306
|
+
const timeSinceLastInvoke = time - lastInvokeTime
|
|
307
|
+
const timeWaiting = wait - timeSinceLastCall
|
|
308
|
+
|
|
309
|
+
return maxing ? Math.min(timeWaiting, maxWait! - timeSinceLastInvoke) : timeWaiting
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function shouldInvoke(time: number, state: DebounceTimerState): boolean {
|
|
313
|
+
const { lastCallTime, lastInvokeTime, wait, maxing, maxWait } = state
|
|
314
|
+
const timeSinceLastCall = time - lastCallTime!
|
|
315
|
+
const timeSinceLastInvoke = time - lastInvokeTime
|
|
316
|
+
|
|
317
|
+
// Either this is the first call, activity has stopped and we're at the
|
|
318
|
+
// trailing edge, the system time has gone backwards and we're treating
|
|
319
|
+
// it as the trailing edge, or we've hit the `maxWait` limit.
|
|
320
|
+
return (
|
|
321
|
+
lastCallTime === undefined ||
|
|
322
|
+
timeSinceLastCall >= wait ||
|
|
323
|
+
timeSinceLastCall < 0 ||
|
|
324
|
+
(maxing && timeSinceLastInvoke >= maxWait!)
|
|
325
|
+
)
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function startTimer(pendingFunc: AnyFunction, wait: number): number {
|
|
329
|
+
return setTimeout(pendingFunc, wait)
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function cancelTimer(id: number): void {
|
|
333
|
+
clearTimeout(id)
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
interface DebounceTimerState {
|
|
337
|
+
lastCallTime: number | undefined
|
|
338
|
+
lastInvokeTime: number
|
|
339
|
+
wait: number
|
|
340
|
+
maxing: boolean
|
|
341
|
+
maxWait: number | undefined
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
export interface Cancelable {
|
|
345
|
+
cancel: () => void
|
|
346
|
+
flush: () => void
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Like `Cancelable`, but for the async debounced functions: `flush()` resolves with the flushed
|
|
351
|
+
* invocation's result (or `undefined` if nothing was pending), and `pending()` is exposed.
|
|
352
|
+
*/
|
|
353
|
+
export interface AsyncCancelable<T> {
|
|
354
|
+
cancel: () => void
|
|
355
|
+
flush: () => Promise<T | undefined>
|
|
356
|
+
pending: () => boolean
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
export interface ThrottleOptions {
|
|
360
|
+
/**
|
|
361
|
+
* Invoke on the leading edge of the window (immediately, on the first call).
|
|
362
|
+
*
|
|
363
|
+
* @default true
|
|
364
|
+
*/
|
|
365
|
+
leading?: boolean
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Invoke on the trailing edge of the window (after `wait` has elapsed).
|
|
369
|
+
*
|
|
370
|
+
* @default true
|
|
371
|
+
*/
|
|
372
|
+
trailing?: boolean
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
export interface DebounceOptions {
|
|
376
|
+
/**
|
|
377
|
+
* Invoke on the leading edge of the window (immediately, on the first call).
|
|
378
|
+
*
|
|
379
|
+
* @default false
|
|
380
|
+
*/
|
|
381
|
+
leading?: boolean
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Invoke on the trailing edge of the window (after `wait` has elapsed).
|
|
385
|
+
*
|
|
386
|
+
* @default true
|
|
387
|
+
*/
|
|
388
|
+
trailing?: boolean
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Maximum time `func` is allowed to be delayed before it's forcibly invoked.
|
|
392
|
+
*/
|
|
393
|
+
maxWait?: number
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
export interface AsyncThrottleOptions {
|
|
397
|
+
/**
|
|
398
|
+
* Invoke on the leading edge of the window (immediately, on the first call).
|
|
399
|
+
*
|
|
400
|
+
* @default true
|
|
401
|
+
*/
|
|
402
|
+
leading?: boolean
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Invoke on the trailing edge of the window (after `wait` has elapsed).
|
|
406
|
+
*
|
|
407
|
+
* When `false`, calls dropped within the window (not served by a leading invocation) resolve
|
|
408
|
+
* with `undefined`. There's no invocation for them to await. This is why the returned function
|
|
409
|
+
* can resolve with `undefined` even though the original function's return type doesn't express that.
|
|
410
|
+
*
|
|
411
|
+
* @default true
|
|
412
|
+
*/
|
|
413
|
+
trailing?: boolean
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
export interface AsyncDebounceOptions extends AsyncThrottleOptions {
|
|
417
|
+
/**
|
|
418
|
+
* Invoke on the leading edge of the window (immediately, on the first call).
|
|
419
|
+
*
|
|
420
|
+
* @default false
|
|
421
|
+
*/
|
|
422
|
+
leading?: boolean
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* Maximum time `func` is allowed to be delayed before it's forcibly invoked.
|
|
426
|
+
*/
|
|
427
|
+
maxWait?: number
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* The function returned by `_asyncDebounce`/`_asyncThrottle`. Same signature as `T`, but resolves
|
|
432
|
+
* `Awaited<ReturnType<T>> | undefined`
|
|
433
|
+
*
|
|
434
|
+
* This is because some configurations (e.g. `trailing: false`) may drop calls without invoking
|
|
435
|
+
* the original function, so there's no result to await.
|
|
436
|
+
*/
|
|
437
|
+
export type AsyncDebounced<T extends AnyAsyncFunction> = ((
|
|
438
|
+
this: ThisParameterType<T>,
|
|
439
|
+
...args: Parameters<T>
|
|
440
|
+
) => Promise<Awaited<ReturnType<T>> | undefined>) &
|
|
441
|
+
AsyncCancelable<Awaited<ReturnType<T>>>
|
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
import type { AnyObject, InstanceId } from '../types.js'
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Generic override of TypeScript's built-in legacy MethodDecorator, that
|
|
5
|
+
* allows us to infer the parameters of the decorated method from the parameters
|
|
6
|
+
* of a decorator.
|
|
7
|
+
*/
|
|
8
|
+
export type MethodDecorator<T> = (
|
|
9
|
+
target: AnyObject,
|
|
10
|
+
propertyKey: string | symbol,
|
|
11
|
+
descriptor: TypedPropertyDescriptor<T>,
|
|
12
|
+
) => TypedPropertyDescriptor<T> | undefined
|
|
13
|
+
|
|
3
14
|
/**
|
|
4
15
|
* @returns
|
|
5
16
|
* e.g `NameOfYourClass.methodName`
|
|
@@ -2,8 +2,9 @@ import { _assert, _assertTypeOf } from '../error/assert.js'
|
|
|
2
2
|
import type { CommonLogger } from '../log/commonLogger.js'
|
|
3
3
|
import type { AnyFunction, AnyObject, MaybeParameters } from '../types.js'
|
|
4
4
|
import { _objectAssign } from '../types.js'
|
|
5
|
+
import type { MethodDecorator } from './decorator.util.js'
|
|
5
6
|
import { _getTargetMethodSignature } from './decorator.util.js'
|
|
6
|
-
import type { MemoCache
|
|
7
|
+
import type { MemoCache } from './memo.util.js'
|
|
7
8
|
import { jsonMemoSerializer, MapMemoCache } from './memo.util.js'
|
|
8
9
|
|
|
9
10
|
export interface MemoOptions<FN> {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { _isPrimitive } from '../is.util.js'
|
|
2
2
|
import { pDelay } from '../promise/pDelay.js'
|
|
3
|
-
import type {
|
|
3
|
+
import type { UnixTimestamp } from '../types.js'
|
|
4
4
|
import { MISS } from '../types.js'
|
|
5
5
|
|
|
6
6
|
export type MemoSerializer = (args: any[]) => any
|
|
@@ -147,14 +147,3 @@ export class MapAsyncMemoCache<KEY = any, VALUE = any> implements AsyncMemoCache
|
|
|
147
147
|
this.m.clear()
|
|
148
148
|
}
|
|
149
149
|
}
|
|
150
|
-
|
|
151
|
-
/**
|
|
152
|
-
* Generic override of Typescript's built in legacy MethodDecorator, that
|
|
153
|
-
* allows us to infer the parameters of the decorated method from the parameters
|
|
154
|
-
* of a decorator.
|
|
155
|
-
*/
|
|
156
|
-
export type MethodDecorator<T> = (
|
|
157
|
-
target: AnyObject,
|
|
158
|
-
propertyKey: string | symbol,
|
|
159
|
-
descriptor: TypedPropertyDescriptor<T>,
|
|
160
|
-
) => TypedPropertyDescriptor<T> | undefined
|