@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.
@@ -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 { AsyncMemoCache, MethodDecorator } from './memo.util.js';
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
- export declare function _debounce<T extends AnyFunction>(func: T, wait: number, opt?: DebounceOptions): T & Cancelable;
31
- export declare function _throttle<T extends AnyFunction>(func: T, wait: number, opt?: ThrottleOptions): T & Cancelable;
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 { DebounceOptions, ThrottleOptions } from './debounce.js';
2
- export declare function _Debounce(wait: number, opt?: DebounceOptions): MethodDecorator;
3
- export declare function _Throttle(wait: number, opt?: ThrottleOptions): MethodDecorator;
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 maxWait = maxing ? Math.max(Number(opt.maxWait) || 0, wait) : opt.maxWait;
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
- return trailingEdge(time);
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
- lastArgs = lastCallTime = lastThis = timerId = undefined;
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 { MemoCache, MethodDecorator } from './memo.util.js';
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 { AnyObject, UnixTimestamp } from '../types.js';
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
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@naturalcycles/js-lib",
3
3
  "type": "module",
4
- "version": "15.83.0",
4
+ "version": "15.84.0",
5
5
  "dependencies": {
6
6
  "tslib": "^2"
7
7
  },
@@ -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, MethodDecorator } from './memo.util.js'
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 { DebounceOptions, ThrottleOptions } from './debounce.js'
2
- import { _debounce, _throttle } from './debounce.js'
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(wait: number, opt: DebounceOptions = {}): MethodDecorator {
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 as any, wait, opt)
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(wait: number, opt: ThrottleOptions = {}): MethodDecorator {
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 as any, wait, opt)
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 type { AnyFunction } from '../types.js'
2
-
3
- export interface Cancelable {
4
- cancel: () => void
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: any
43
- let lastThis: any
44
- let result: any
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 maxWait = maxing ? Math.max(Number(opt.maxWait) || 0, wait) : opt.maxWait
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): any {
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 startTimer(pendingFunc: AnyFunction, wait: number): number {
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 remainingWait(time: number): number {
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
- return trailingEdge(time)
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): any {
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
- lastArgs = lastCallTime = lastThis = timerId = undefined
71
+ state.lastInvokeTime = 0
72
+ state.lastCallTime = undefined
73
+ lastArgs = lastThis = timerId = undefined
131
74
  }
132
75
 
133
- function flush(): any {
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: any, ...args: any[]): any {
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, MethodDecorator } from './memo.util.js'
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 { AnyObject, UnixTimestamp } from '../types.js'
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