@ix-xs/node-comfort 1.2.0 → 2.1.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/CHANGELOG.md +340 -0
- package/README.md +137 -548
- package/index.js +80 -35
- package/package.json +161 -33
- package/src/Arr.js +1050 -418
- package/src/Async.js +817 -0
- package/src/Cache.js +433 -0
- package/src/Checker.js +1202 -539
- package/src/Cli.js +1293 -0
- package/src/Color.js +556 -0
- package/src/Crypto.js +1082 -0
- package/src/Emitter.js +356 -0
- package/src/Env.js +689 -0
- package/src/FS.js +1626 -894
- package/src/Func.js +823 -356
- package/src/Http.js +700 -0
- package/src/Id.js +568 -200
- package/src/Logger.js +963 -378
- package/src/Num.js +965 -360
- package/src/Obj.js +1077 -379
- package/src/SQLite.js +1629 -1005
- package/src/Schema.js +2101 -0
- package/src/Str.js +1209 -432
- package/src/Sys.js +654 -0
- package/src/Time.js +1533 -440
- package/src/Utils.js +342 -325
- package/src/errors.js +335 -0
- package/src/internal/shared.js +415 -0
- package/types/index.d.ts +735 -526
- package/types/src/Arr.d.ts +533 -145
- package/types/src/Arr.d.ts.map +1 -1
- package/types/src/Async.d.ts +372 -0
- package/types/src/Async.d.ts.map +1 -0
- package/types/src/Cache.d.ts +221 -0
- package/types/src/Cache.d.ts.map +1 -0
- package/types/src/Checker.d.ts +651 -232
- package/types/src/Checker.d.ts.map +1 -1
- package/types/src/Cli.d.ts +664 -0
- package/types/src/Cli.d.ts.map +1 -0
- package/types/src/Color.d.ts +300 -0
- package/types/src/Color.d.ts.map +1 -0
- package/types/src/Crypto.d.ts +620 -0
- package/types/src/Crypto.d.ts.map +1 -0
- package/types/src/Emitter.d.ts +189 -0
- package/types/src/Emitter.d.ts.map +1 -0
- package/types/src/Env.d.ts +412 -0
- package/types/src/Env.d.ts.map +1 -0
- package/types/src/FS.d.ts +717 -238
- package/types/src/FS.d.ts.map +1 -1
- package/types/src/Func.d.ts +508 -135
- package/types/src/Func.d.ts.map +1 -1
- package/types/src/Http.d.ts +444 -0
- package/types/src/Http.d.ts.map +1 -0
- package/types/src/Id.d.ts +199 -66
- package/types/src/Id.d.ts.map +1 -1
- package/types/src/Logger.d.ts +414 -46
- package/types/src/Logger.d.ts.map +1 -1
- package/types/src/Num.d.ts +514 -137
- package/types/src/Num.d.ts.map +1 -1
- package/types/src/Obj.d.ts +453 -104
- package/types/src/Obj.d.ts.map +1 -1
- package/types/src/SQLite.d.ts +754 -308
- package/types/src/SQLite.d.ts.map +1 -1
- package/types/src/Schema.d.ts +1189 -0
- package/types/src/Schema.d.ts.map +1 -0
- package/types/src/Str.d.ts +617 -166
- package/types/src/Str.d.ts.map +1 -1
- package/types/src/Sys.d.ts +314 -0
- package/types/src/Sys.d.ts.map +1 -0
- package/types/src/Time.d.ts +759 -204
- package/types/src/Time.d.ts.map +1 -1
- package/types/src/Utils.d.ts +158 -102
- package/types/src/Utils.d.ts.map +1 -1
- package/types/src/errors.d.ts +316 -0
- package/types/src/errors.d.ts.map +1 -0
- package/types/src/internal/shared.d.ts +123 -0
- package/types/src/internal/shared.d.ts.map +1 -0
- package/types/index.d.ts.map +0 -1
package/src/Func.js
CHANGED
|
@@ -1,375 +1,842 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
|
-
* Function
|
|
4
|
+
* Function helpers: debounce, throttle, memoize, retry with backoff,
|
|
5
|
+
* timeouts, error handling without try/catch, and typed composition.
|
|
3
6
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
+
* @example
|
|
8
|
+
* const save = func.debounce(persist, 300);
|
|
9
|
+
* const data = await func.retry(() => fetchJSON(url), { attempts: 5, delay: 200, backoff: 2 });
|
|
10
|
+
* const [error, user] = await func.to(getUser(id));
|
|
7
11
|
*/
|
|
8
12
|
|
|
13
|
+
const { types } = require("node:util");
|
|
14
|
+
const { TimeoutError, AbortError } = require("./errors.js");
|
|
15
|
+
const { toMs, sleep: _sleep, setLongTimeout, argsKey } = require("./internal/shared.js");
|
|
16
|
+
|
|
9
17
|
/**
|
|
10
|
-
*
|
|
11
|
-
* @
|
|
12
|
-
* @
|
|
13
|
-
* @
|
|
18
|
+
* Options for `debounce()`.
|
|
19
|
+
* @typedef {object} DebounceOptions
|
|
20
|
+
* @property {boolean} [leading] Call right away at the start of a burst.
|
|
21
|
+
* @property {boolean} [trailing] Call once the burst is over. Defaults to `true`.
|
|
22
|
+
* @property {number | string} [maxWait] Never postpone a call longer than this, even if calls keep coming. In ms, or like `"2s"`.
|
|
14
23
|
*/
|
|
15
|
-
const _wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
16
24
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
25
|
+
/**
|
|
26
|
+
* A debounced function. `flush()` runs the pending call now, `cancel()`
|
|
27
|
+
* drops it, `pending()` tells you if there's one.
|
|
28
|
+
* @template {(...args: any[]) => any} F
|
|
29
|
+
* @typedef {((...args: Parameters<F>) => void) & { cancel(): void, flush(): void, pending(): boolean }} DebouncedFunction
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Options for `throttle()`.
|
|
34
|
+
* @typedef {object} ThrottleOptions
|
|
35
|
+
* @property {boolean} [leading] Call right away on the first call. Defaults to `true`.
|
|
36
|
+
* @property {boolean} [trailing] Make a last call with the latest arguments at the end of the interval. Defaults to `true`.
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* A throttled function, with `cancel()`.
|
|
41
|
+
* @template {(...args: any[]) => any} F
|
|
42
|
+
* @typedef {((...args: Parameters<F>) => void) & { cancel(): void }} ThrottledFunction
|
|
43
|
+
*/
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Options for `memoize()`.
|
|
47
|
+
* @typedef {object} MemoizeOptions
|
|
48
|
+
* @property {(...args: any[]) => unknown} [resolver] Builds the cache key from the arguments. By default, primitives are compared by value, plain objects and arrays by content, anything else by identity.
|
|
49
|
+
* @property {number | string} [ttl] How long a result stays cached, in ms or like `"5m"`.
|
|
50
|
+
* @property {number} [max] Maximum number of results; the least recently used goes first.
|
|
51
|
+
* @property {boolean} [cacheRejections] Keep rejected promises. By default they're dropped so the next call tries again.
|
|
52
|
+
*/
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* A memoized function, with its `cache`, `clear()` and `delete(...args)`.
|
|
56
|
+
* @template {(...args: any[]) => any} F
|
|
57
|
+
* @typedef {F & { cache: Map<unknown, { value: ReturnType<F>, expires: number }>, clear(): void, delete(...args: Parameters<F>): boolean }} MemoizedFunction
|
|
58
|
+
*/
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Options for `retry()`.
|
|
62
|
+
* @typedef {object} RetryOptions
|
|
63
|
+
* @property {number} [attempts] Total attempts, the first one included. Defaults to `3`.
|
|
64
|
+
* @property {number | string | ((attempt: number, error: unknown) => number)} [delay] Wait before the next attempt, in ms or like `"1s"`, or a function that computes it.
|
|
65
|
+
* @property {number} [backoff] Multiplies the delay after each failure: `2` gives 200, 400, 800... Defaults to `1`.
|
|
66
|
+
* @property {number | string} [maxDelay] Cap for the computed delay.
|
|
67
|
+
* @property {number} [jitter] Randomizes the delay, from `0` (none) to `1` (anywhere between 0 and 2x). Stops many clients from retrying in sync.
|
|
68
|
+
* @property {(error: unknown, attempt: number) => boolean | Promise<boolean>} [shouldRetry] Return `false` to give up early, for example on a 4xx error.
|
|
69
|
+
* @property {(error: unknown, attempt: number, delay: number) => void} [onRetry] Called before each wait, for logs or metrics.
|
|
70
|
+
* @property {AbortSignal} [signal] Stops retrying. The attempt in progress isn't interrupted.
|
|
71
|
+
*/
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Options for `batch()`.
|
|
75
|
+
* @typedef {object} BatchOptions
|
|
76
|
+
* @property {number | string} [wait] How long to collect calls before running. By default, calls made in the same tick go together.
|
|
77
|
+
* @property {number} [max] Run as soon as this many calls are waiting.
|
|
78
|
+
*/
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* A batched function. `flush()` runs the waiting calls now.
|
|
82
|
+
* @template I
|
|
83
|
+
* @template R
|
|
84
|
+
* @typedef {((item: I) => Promise<R>) & { flush(): void }} BatchedFunction
|
|
85
|
+
*/
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Waits for a pause of `wait` ms between calls, then runs once with the
|
|
89
|
+
* latest arguments. For search boxes, resizing, auto-saving...
|
|
90
|
+
*
|
|
91
|
+
* @example
|
|
92
|
+
* const search = func.debounce((query) => api.search(query), 300);
|
|
93
|
+
* input.on("input", (e) => search(e.target.value));
|
|
94
|
+
*
|
|
95
|
+
* const save = func.debounce(persist, 1000, { maxWait: 5000 }); // saves at least every 5s
|
|
96
|
+
*
|
|
97
|
+
* @template {(...args: any[]) => any} F
|
|
98
|
+
* @param {F} fn
|
|
99
|
+
* @param {number | string} wait Pause needed, in ms or like `"300ms"`.
|
|
100
|
+
* @param {DebounceOptions} [options]
|
|
101
|
+
* @returns {DebouncedFunction<F>}
|
|
102
|
+
*/
|
|
103
|
+
function debounce(fn, wait, options = {}) {
|
|
104
|
+
const delay = toMs(wait, "wait");
|
|
105
|
+
const leading = options.leading ?? false;
|
|
106
|
+
const trailing = options.trailing ?? true;
|
|
107
|
+
const maxWait = options.maxWait === undefined ? undefined : toMs(options.maxWait, "maxWait");
|
|
108
|
+
/** @type {NodeJS.Timeout | undefined} */
|
|
109
|
+
let timer;
|
|
110
|
+
let active = false;
|
|
111
|
+
let lastCall = 0;
|
|
112
|
+
let burstStart = 0;
|
|
113
|
+
/** @type {any[] | undefined} */
|
|
114
|
+
let lastArgs;
|
|
115
|
+
/** @type {unknown} */
|
|
116
|
+
let lastThis;
|
|
117
|
+
|
|
118
|
+
const invoke = () => {
|
|
119
|
+
const args = lastArgs;
|
|
120
|
+
const self = lastThis;
|
|
121
|
+
lastArgs = undefined;
|
|
122
|
+
lastThis = undefined;
|
|
123
|
+
if (args) fn.apply(self, args);
|
|
124
|
+
};
|
|
125
|
+
// One timer per burst: it checks how long ago the last call was instead of
|
|
126
|
+
// being reset on every call, which keeps busy call sites cheap.
|
|
127
|
+
const check = () => {
|
|
128
|
+
const now = Date.now();
|
|
129
|
+
const quiet = now - lastCall;
|
|
130
|
+
const overdue = maxWait !== undefined && now - burstStart >= maxWait;
|
|
131
|
+
if (quiet >= delay || quiet < 0 || overdue) {
|
|
132
|
+
timer = undefined;
|
|
133
|
+
active = false;
|
|
134
|
+
if (trailing || overdue) invoke();
|
|
135
|
+
else lastArgs = lastThis = undefined;
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
const untilMax = maxWait === undefined ? Infinity : maxWait - (now - burstStart);
|
|
139
|
+
timer = setTimeout(check, Math.min(delay - quiet, untilMax));
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
/** @type {any} */
|
|
143
|
+
const debounced = /** @this {any} */ function(/** @type {any[]} */ ...args) {
|
|
144
|
+
lastArgs = args;
|
|
145
|
+
lastThis = this;
|
|
146
|
+
lastCall = Date.now();
|
|
147
|
+
if (active) return;
|
|
148
|
+
active = true;
|
|
149
|
+
burstStart = lastCall;
|
|
150
|
+
timer = setTimeout(check, maxWait === undefined ? delay : Math.min(delay, maxWait));
|
|
151
|
+
if (leading) invoke();
|
|
152
|
+
};
|
|
153
|
+
const stop = () => {
|
|
154
|
+
clearTimeout(timer);
|
|
155
|
+
timer = undefined;
|
|
156
|
+
active = false;
|
|
157
|
+
};
|
|
158
|
+
debounced.cancel = () => {
|
|
159
|
+
stop();
|
|
160
|
+
lastArgs = lastThis = undefined;
|
|
161
|
+
};
|
|
162
|
+
debounced.flush = () => {
|
|
163
|
+
stop();
|
|
164
|
+
invoke();
|
|
165
|
+
};
|
|
166
|
+
debounced.pending = () => active && lastArgs !== undefined;
|
|
167
|
+
return debounced;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Runs at most once every `wait` ms, however often it's called. For scroll
|
|
172
|
+
* handlers, progress reports, rate-limited APIs...
|
|
173
|
+
*
|
|
174
|
+
* @example
|
|
175
|
+
* const report = func.throttle((pct) => console.log(`${pct}%`), 1000);
|
|
176
|
+
*
|
|
177
|
+
* @template {(...args: any[]) => any} F
|
|
178
|
+
* @param {F} fn
|
|
179
|
+
* @param {number | string} interval Minimum gap between calls, in ms or like `"1s"`.
|
|
180
|
+
* @param {ThrottleOptions} [options]
|
|
181
|
+
* @returns {ThrottledFunction<F>}
|
|
182
|
+
*/
|
|
183
|
+
function throttle(fn, interval, options = {}) {
|
|
184
|
+
const wait = toMs(interval, "wait");
|
|
185
|
+
const leading = options.leading ?? true;
|
|
186
|
+
const trailing = options.trailing ?? true;
|
|
187
|
+
let last = 0;
|
|
188
|
+
/** @type {NodeJS.Timeout | undefined} */
|
|
189
|
+
let timer;
|
|
190
|
+
/** @type {any[] | undefined} */
|
|
191
|
+
let lastArgs;
|
|
192
|
+
/** @type {unknown} */
|
|
193
|
+
let lastThis;
|
|
194
|
+
|
|
195
|
+
/** @type {any} */
|
|
196
|
+
const throttled = /** @this {any} */ function(/** @type {any[]} */ ...args) {
|
|
197
|
+
const now = Date.now();
|
|
198
|
+
if (!last && !leading) last = now;
|
|
199
|
+
const remaining = wait - (now - last);
|
|
200
|
+
lastArgs = args;
|
|
201
|
+
lastThis = this;
|
|
202
|
+
if (remaining <= 0 || remaining > wait) {
|
|
203
|
+
clearTimeout(timer);
|
|
204
|
+
timer = undefined;
|
|
205
|
+
last = now;
|
|
206
|
+
fn.apply(lastThis, lastArgs);
|
|
207
|
+
lastArgs = undefined;
|
|
208
|
+
} else if (trailing && timer === undefined) {
|
|
47
209
|
timer = setTimeout(() => {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
* @param {number} wait - Minimum interval in milliseconds.
|
|
81
|
-
* @param {{ trailing?: boolean }} [options] - Set `trailing` (default true) to run a final call.
|
|
82
|
-
* @returns {F & { cancel: () => void }} The throttled function.
|
|
83
|
-
*/
|
|
84
|
-
throttle(fn, wait, options = {}) {
|
|
85
|
-
let last = 0;
|
|
86
|
-
let timer = null;
|
|
87
|
-
let lastArgs = null;
|
|
88
|
-
let lastThis = null;
|
|
89
|
-
const trailing = options.trailing ?? true;
|
|
90
|
-
|
|
91
|
-
const throttled = function(...args) {
|
|
92
|
-
const now = Date.now();
|
|
93
|
-
const remaining = wait - (now - last);
|
|
94
|
-
lastArgs = args;
|
|
95
|
-
lastThis = this;
|
|
96
|
-
|
|
97
|
-
if (remaining <= 0) {
|
|
98
|
-
if (timer) {
|
|
99
|
-
clearTimeout(timer);
|
|
100
|
-
timer = null;
|
|
101
|
-
}
|
|
102
|
-
last = now;
|
|
103
|
-
fn.apply(lastThis, lastArgs);
|
|
104
|
-
} else if (trailing && !timer) {
|
|
105
|
-
timer = setTimeout(() => {
|
|
106
|
-
last = Date.now();
|
|
107
|
-
timer = null;
|
|
108
|
-
fn.apply(lastThis, lastArgs);
|
|
109
|
-
}, remaining);
|
|
110
|
-
}
|
|
111
|
-
};
|
|
112
|
-
|
|
113
|
-
throttled.cancel = () => {
|
|
114
|
-
if (timer) clearTimeout(timer);
|
|
115
|
-
timer = null;
|
|
116
|
-
last = 0;
|
|
117
|
-
};
|
|
118
|
-
|
|
119
|
-
return /** @type {any} */ (throttled);
|
|
120
|
-
},
|
|
121
|
-
|
|
122
|
-
/**
|
|
123
|
-
* Wraps a function so it can only ever run once; subsequent calls return
|
|
124
|
-
* the first result.
|
|
125
|
-
*
|
|
126
|
-
* @example
|
|
127
|
-
* const init = nodeComfort.func.once(() => expensiveSetup());
|
|
128
|
-
*
|
|
129
|
-
* @template {(...args: any[]) => any} F
|
|
130
|
-
* @param {F} fn - Function to wrap.
|
|
131
|
-
* @returns {F} The single-call function.
|
|
132
|
-
*/
|
|
133
|
-
once(fn) {
|
|
134
|
-
let called = false;
|
|
135
|
-
let result;
|
|
136
|
-
return /** @type {any} */ (function(...args) {
|
|
210
|
+
last = leading ? Date.now() : 0;
|
|
211
|
+
timer = undefined;
|
|
212
|
+
if (lastArgs) fn.apply(lastThis, lastArgs);
|
|
213
|
+
lastArgs = undefined;
|
|
214
|
+
}, remaining);
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
throttled.cancel = () => {
|
|
218
|
+
clearTimeout(timer);
|
|
219
|
+
timer = undefined;
|
|
220
|
+
last = 0;
|
|
221
|
+
lastArgs = undefined;
|
|
222
|
+
};
|
|
223
|
+
return throttled;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Runs `fn` the first time only; later calls return the same result.
|
|
228
|
+
*
|
|
229
|
+
* @example
|
|
230
|
+
* const connect = func.once(() => createPool(config));
|
|
231
|
+
*
|
|
232
|
+
* @template {(...args: any[]) => any} F
|
|
233
|
+
* @param {F} fn
|
|
234
|
+
* @returns {F}
|
|
235
|
+
*/
|
|
236
|
+
function once(fn) {
|
|
237
|
+
let called = false;
|
|
238
|
+
/** @type {ReturnType<F>} */
|
|
239
|
+
let result;
|
|
240
|
+
return /** @type {F} */ (
|
|
241
|
+
/** @this {any} */ function(/** @type {any[]} */ ...args) {
|
|
137
242
|
if (!called) {
|
|
138
243
|
called = true;
|
|
139
244
|
result = fn.apply(this, args);
|
|
140
245
|
}
|
|
141
246
|
return result;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
* attempts: 5,
|
|
180
|
-
* delay: 200,
|
|
181
|
-
* backoff: 2, // 200, 400, 800, ...
|
|
182
|
-
* });
|
|
183
|
-
*
|
|
184
|
-
* @template T
|
|
185
|
-
* @param {() => T|Promise<T>} fn - Function to run.
|
|
186
|
-
* @param {{
|
|
187
|
-
* attempts?: number,
|
|
188
|
-
* delay?: number,
|
|
189
|
-
* backoff?: number,
|
|
190
|
-
* maxDelay?: number,
|
|
191
|
-
* shouldRetry?: (error: unknown, attempt: number) => boolean,
|
|
192
|
-
* onRetry?: (error: unknown, attempt: number) => void
|
|
193
|
-
* }} [options] - Retry options.
|
|
194
|
-
* @returns {Promise<T>} The resolved value.
|
|
195
|
-
* @throws Re-throws the last error if all attempts fail.
|
|
196
|
-
*/
|
|
197
|
-
async retry(fn, options = {}) {
|
|
198
|
-
const attempts = options.attempts ?? 3;
|
|
199
|
-
const delay = options.delay ?? 0;
|
|
200
|
-
const backoff = options.backoff ?? 1;
|
|
201
|
-
const maxDelay = options.maxDelay ?? Infinity;
|
|
202
|
-
const shouldRetry = options.shouldRetry ?? (() => true);
|
|
203
|
-
|
|
204
|
-
let lastError;
|
|
205
|
-
for (let attempt = 1; attempt <= attempts; attempt++) {
|
|
206
|
-
try {
|
|
207
|
-
return await fn();
|
|
208
|
-
} catch (error) {
|
|
209
|
-
lastError = error;
|
|
210
|
-
if (attempt >= attempts || !shouldRetry(error, attempt)) break;
|
|
211
|
-
if (options.onRetry) options.onRetry(error, attempt);
|
|
212
|
-
if (delay > 0) {
|
|
213
|
-
const wait = Math.min(delay * backoff ** (attempt - 1), maxDelay);
|
|
214
|
-
await _wait(wait);
|
|
247
|
+
}
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Caches results by arguments. Supports expiry (`ttl`), a size limit
|
|
253
|
+
* (`max`) and async functions: a failed promise isn't cached, so the next
|
|
254
|
+
* call tries again.
|
|
255
|
+
*
|
|
256
|
+
* @example
|
|
257
|
+
* const getUser = func.memoize((id) => db.users.find(id), { ttl: 60_000, max: 1000 });
|
|
258
|
+
* getUser.delete(42); // forget one entry
|
|
259
|
+
* getUser.clear(); // forget everything
|
|
260
|
+
*
|
|
261
|
+
* @template {(...args: any[]) => any} F
|
|
262
|
+
* @param {F} fn
|
|
263
|
+
* @param {MemoizeOptions | ((...args: Parameters<F>) => unknown)} [options] Options, or a function that builds the cache key.
|
|
264
|
+
* @returns {MemoizedFunction<F>}
|
|
265
|
+
*/
|
|
266
|
+
function memoize(fn, options = {}) {
|
|
267
|
+
const opts = typeof options === "function" ? { resolver: options } : options;
|
|
268
|
+
const ttl = opts.ttl === undefined ? 0 : toMs(opts.ttl, "ttl");
|
|
269
|
+
const max = opts.max;
|
|
270
|
+
const resolver = opts.resolver;
|
|
271
|
+
/** @type {Map<unknown, { value: any, expires: number }>} */
|
|
272
|
+
const cache = new Map();
|
|
273
|
+
let sweepAt = 256;
|
|
274
|
+
|
|
275
|
+
/** @type {any} */
|
|
276
|
+
const memoized = /** @this {any} */ function(/** @type {any[]} */ ...args) {
|
|
277
|
+
const key = resolver ? resolver(...args) : argsKey(args);
|
|
278
|
+
const hit = cache.get(key);
|
|
279
|
+
if (hit !== undefined) {
|
|
280
|
+
if (hit.expires === 0 || hit.expires > Date.now()) {
|
|
281
|
+
if (max !== undefined) {
|
|
282
|
+
cache.delete(key);
|
|
283
|
+
cache.set(key, hit);
|
|
215
284
|
}
|
|
285
|
+
return hit.value;
|
|
216
286
|
}
|
|
287
|
+
cache.delete(key);
|
|
217
288
|
}
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
289
|
+
const value = fn.apply(this, args);
|
|
290
|
+
cache.set(key, { value, expires: ttl ? Date.now() + ttl : 0 });
|
|
291
|
+
if (max !== undefined) {
|
|
292
|
+
if (cache.size > max) cache.delete(cache.keys().next().value);
|
|
293
|
+
} else if (ttl && cache.size >= sweepAt) {
|
|
294
|
+
// Without a size limit, expired results would pile up: drop them now and then.
|
|
295
|
+
const now = Date.now();
|
|
296
|
+
for (const [k, entry] of cache) if (entry.expires <= now) cache.delete(k);
|
|
297
|
+
sweepAt = Math.max(256, cache.size * 2);
|
|
298
|
+
}
|
|
299
|
+
if (!opts.cacheRejections && value && typeof value.then === "function") {
|
|
300
|
+
value.then(undefined, () => {
|
|
301
|
+
if (cache.get(key)?.value === value) cache.delete(key);
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
return value;
|
|
305
|
+
};
|
|
306
|
+
memoized.cache = cache;
|
|
307
|
+
memoized.clear = () => cache.clear();
|
|
308
|
+
memoized.delete = (/** @type {any[]} */ ...args) => cache.delete(resolver ? resolver(...args) : argsKey(args));
|
|
309
|
+
return memoized;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Calls `fn` until it succeeds, waiting between attempts. If every attempt
|
|
314
|
+
* fails, the last error is thrown.
|
|
315
|
+
*
|
|
316
|
+
* @example
|
|
317
|
+
* const data = await func.retry(() => fetchJSON(url), {
|
|
318
|
+
* attempts: 5,
|
|
319
|
+
* delay: 200,
|
|
320
|
+
* backoff: 2, // 200, 400, 800, 1600 ms
|
|
321
|
+
* shouldRetry: (error) => !(error instanceof nc.errors.HttpError && error.status < 500),
|
|
322
|
+
* });
|
|
323
|
+
*
|
|
324
|
+
* @template T
|
|
325
|
+
* @param {(attempt: number) => T | PromiseLike<T>} fn Receives the attempt number, starting at 1.
|
|
326
|
+
* @param {RetryOptions} [options]
|
|
327
|
+
* @returns {Promise<Awaited<T>>}
|
|
328
|
+
* @throws The last error, or an `AbortError` if the signal fires.
|
|
329
|
+
*/
|
|
330
|
+
async function retry(fn, options = {}) {
|
|
331
|
+
const attempts = Math.max(1, options.attempts ?? 3);
|
|
332
|
+
const backoff = options.backoff ?? 1;
|
|
333
|
+
const maxDelay = options.maxDelay === undefined ? Infinity : toMs(options.maxDelay, "maxDelay");
|
|
334
|
+
const delay = typeof options.delay === "function" || options.delay === undefined ? 0 : toMs(options.delay, "delay");
|
|
335
|
+
const jitter = Math.min(1, Math.max(0, options.jitter ?? 0));
|
|
336
|
+
/** @type {unknown} */
|
|
337
|
+
let lastError;
|
|
338
|
+
for (let attempt = 1; attempt <= attempts; attempt++) {
|
|
339
|
+
if (options.signal?.aborted) throw new AbortError(undefined, { cause: options.signal.reason });
|
|
340
|
+
try {
|
|
341
|
+
return await fn(attempt);
|
|
342
|
+
} catch (error) {
|
|
343
|
+
lastError = error;
|
|
344
|
+
if (attempt >= attempts) break;
|
|
345
|
+
// A cancelled operation was stopped on purpose: trying again would undo that.
|
|
346
|
+
if (error instanceof Error && error.name === "AbortError") break;
|
|
347
|
+
if (options.shouldRetry && !(await options.shouldRetry(error, attempt))) break;
|
|
348
|
+
const base = typeof options.delay === "function" ? options.delay(attempt, error) : delay * backoff ** (attempt - 1);
|
|
349
|
+
const wait = Math.max(0, Math.round(Math.min(base, maxDelay) * (1 + jitter * (Math.random() * 2 - 1))));
|
|
350
|
+
options.onRetry?.(error, attempt, wait);
|
|
351
|
+
if (wait > 0) await _sleep(wait, options.signal);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
throw lastError;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Rejects with a `TimeoutError` if the promise takes longer than `ms`. Pass
|
|
359
|
+
* a function to get an `AbortSignal` that fires when time is up, so the work
|
|
360
|
+
* actually stops instead of running on in the background.
|
|
361
|
+
*
|
|
362
|
+
* @example
|
|
363
|
+
* const res = await func.timeout(fetch(url), 5000);
|
|
364
|
+
* const rows = await func.timeout(() => slowQuery(), 2000, "Database too slow");
|
|
365
|
+
*
|
|
366
|
+
* @template T
|
|
367
|
+
* @param {PromiseLike<T> | ((signal: AbortSignal) => PromiseLike<T> | T)} promise A promise, or a function that returns one. The function gets a signal that aborts on timeout.
|
|
368
|
+
* @param {number | string} ms The limit, in ms or like `"5s"`. `Infinity` means no limit.
|
|
369
|
+
* @param {string} [message]
|
|
370
|
+
* @returns {Promise<Awaited<T>>}
|
|
371
|
+
* @throws {TimeoutError}
|
|
372
|
+
*/
|
|
373
|
+
function timeout(promise, ms, message) {
|
|
374
|
+
const limit = toMs(ms, "timeout");
|
|
375
|
+
const controller = typeof promise === "function" ? new AbortController() : undefined;
|
|
376
|
+
const target = Promise.resolve().then(() => (typeof promise === "function" ? promise(/** @type {AbortController} */ (controller).signal) : promise));
|
|
377
|
+
if (limit === Infinity) return /** @type {Promise<Awaited<T>>} */ (target);
|
|
378
|
+
/** @type {() => void} */
|
|
379
|
+
let cancel = () => {};
|
|
380
|
+
const expired = new Promise((_, reject) => {
|
|
381
|
+
cancel = setLongTimeout(() => {
|
|
382
|
+
const error = new TimeoutError(message ?? `Operation timed out after ${limit}ms`, { timeout: limit });
|
|
383
|
+
controller?.abort(error);
|
|
384
|
+
reject(error);
|
|
385
|
+
}, limit);
|
|
386
|
+
});
|
|
387
|
+
return /** @type {Promise<Awaited<T>>} */ (Promise.race([target, expired]).finally(() => cancel()));
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Waits `ms`, then calls `fn` with the arguments and resolves with its result.
|
|
392
|
+
*
|
|
393
|
+
* @example
|
|
394
|
+
* await func.delay(() => console.log("1s later"), 1000);
|
|
395
|
+
*
|
|
396
|
+
* @template {(...args: any[]) => any} F
|
|
397
|
+
* @param {F} fn
|
|
398
|
+
* @param {number | string} ms In ms, or like `"2s"`.
|
|
399
|
+
* @param {Parameters<F>} args
|
|
400
|
+
* @returns {Promise<Awaited<ReturnType<F>>>}
|
|
401
|
+
*/
|
|
402
|
+
async function delay(fn, ms, ...args) {
|
|
403
|
+
await _sleep(toMs(ms, "delay"));
|
|
404
|
+
return fn(...args);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Awaits a promise and gives you `[error, value]` instead of throwing, so
|
|
409
|
+
* you don't need a try/catch.
|
|
410
|
+
*
|
|
411
|
+
* @example
|
|
412
|
+
* const [error, user] = await func.to(db.users.find(id));
|
|
413
|
+
* if (error) return res.status(500).send(error.message);
|
|
414
|
+
*
|
|
415
|
+
* @template T
|
|
416
|
+
* @template [E=Error]
|
|
417
|
+
* @param {PromiseLike<T> | (() => PromiseLike<T> | T)} promise A promise, or a function (sync errors are caught too).
|
|
418
|
+
* @returns {Promise<[E, undefined] | [null, Awaited<T>]>}
|
|
419
|
+
*/
|
|
420
|
+
async function to(promise) {
|
|
421
|
+
try {
|
|
422
|
+
const value = await (typeof promise === "function" ? promise() : promise);
|
|
423
|
+
return [null, /** @type {Awaited<T>} */ (value)];
|
|
424
|
+
} catch (error) {
|
|
425
|
+
return [/** @type {E} */ (error), undefined];
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* Wraps a function so it resolves to `[error, value]` instead of throwing.
|
|
431
|
+
*
|
|
432
|
+
* @example
|
|
433
|
+
* const safeParse = func.attempt(JSON.parse);
|
|
434
|
+
* const [error, data] = await safeParse(input);
|
|
435
|
+
*
|
|
436
|
+
* @template {(...args: any[]) => any} F
|
|
437
|
+
* @param {F} fn
|
|
438
|
+
* @returns {(...args: Parameters<F>) => Promise<[unknown, undefined] | [null, Awaited<ReturnType<F>>]>}
|
|
439
|
+
*/
|
|
440
|
+
function attempt(fn) {
|
|
441
|
+
return /** @this {any} */ function(...args) {
|
|
442
|
+
return to(() => fn.apply(this, args));
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Turns a callback-style function into one that returns a promise.
|
|
448
|
+
*
|
|
449
|
+
* @example
|
|
450
|
+
* const lookup = func.promisify(dns.lookup);
|
|
451
|
+
* const address = await lookup("nodejs.org");
|
|
452
|
+
*
|
|
453
|
+
* @param {(...args: any[]) => void} fn
|
|
454
|
+
* @returns {(...args: any[]) => Promise<any>}
|
|
455
|
+
*/
|
|
456
|
+
function promisify(fn) {
|
|
457
|
+
return /** @this {any} */ function(...args) {
|
|
458
|
+
return new Promise((resolve, reject) => {
|
|
459
|
+
fn.call(this, ...args, (/** @type {unknown} */ error, /** @type {unknown} */ result) => (error ? reject(error) : resolve(result)));
|
|
331
460
|
});
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* Chains functions from left to right: `pipe(f, g)(x)` is `g(f(x))`.
|
|
466
|
+
* Types flow through up to 6 functions.
|
|
467
|
+
*
|
|
468
|
+
* @example
|
|
469
|
+
* const toSlug = func.pipe(nc.str.deburr, nc.str.kebabCase);
|
|
470
|
+
* toSlug("Crème Brûlée"); // "creme-brulee"
|
|
471
|
+
*
|
|
472
|
+
* @template {any[]} A
|
|
473
|
+
* @template B
|
|
474
|
+
* @overload
|
|
475
|
+
* @param {(...args: A) => B} f1
|
|
476
|
+
* @returns {(...args: A) => B}
|
|
477
|
+
*/
|
|
478
|
+
/**
|
|
479
|
+
* Chains functions from left to right.
|
|
480
|
+
*
|
|
481
|
+
* @template {any[]} A
|
|
482
|
+
* @template B
|
|
483
|
+
* @template C
|
|
484
|
+
* @overload
|
|
485
|
+
* @param {(...args: A) => B} f1
|
|
486
|
+
* @param {(b: B) => C} f2
|
|
487
|
+
* @returns {(...args: A) => C}
|
|
488
|
+
*/
|
|
489
|
+
/**
|
|
490
|
+
* Chains functions from left to right.
|
|
491
|
+
*
|
|
492
|
+
* @template {any[]} A
|
|
493
|
+
* @template B
|
|
494
|
+
* @template C
|
|
495
|
+
* @template D
|
|
496
|
+
* @overload
|
|
497
|
+
* @param {(...args: A) => B} f1
|
|
498
|
+
* @param {(b: B) => C} f2
|
|
499
|
+
* @param {(c: C) => D} f3
|
|
500
|
+
* @returns {(...args: A) => D}
|
|
501
|
+
*/
|
|
502
|
+
/**
|
|
503
|
+
* Chains functions from left to right.
|
|
504
|
+
*
|
|
505
|
+
* @template {any[]} A
|
|
506
|
+
* @template B
|
|
507
|
+
* @template C
|
|
508
|
+
* @template D
|
|
509
|
+
* @template E
|
|
510
|
+
* @overload
|
|
511
|
+
* @param {(...args: A) => B} f1
|
|
512
|
+
* @param {(b: B) => C} f2
|
|
513
|
+
* @param {(c: C) => D} f3
|
|
514
|
+
* @param {(d: D) => E} f4
|
|
515
|
+
* @returns {(...args: A) => E}
|
|
516
|
+
*/
|
|
517
|
+
/**
|
|
518
|
+
* Chains functions from left to right.
|
|
519
|
+
*
|
|
520
|
+
* @template {any[]} A
|
|
521
|
+
* @template B
|
|
522
|
+
* @template C
|
|
523
|
+
* @template D
|
|
524
|
+
* @template E
|
|
525
|
+
* @template F
|
|
526
|
+
* @overload
|
|
527
|
+
* @param {(...args: A) => B} f1
|
|
528
|
+
* @param {(b: B) => C} f2
|
|
529
|
+
* @param {(c: C) => D} f3
|
|
530
|
+
* @param {(d: D) => E} f4
|
|
531
|
+
* @param {(e: E) => F} f5
|
|
532
|
+
* @returns {(...args: A) => F}
|
|
533
|
+
*/
|
|
534
|
+
/**
|
|
535
|
+
* Chains functions from left to right.
|
|
536
|
+
*
|
|
537
|
+
* @template {any[]} A
|
|
538
|
+
* @template B
|
|
539
|
+
* @template C
|
|
540
|
+
* @template D
|
|
541
|
+
* @template E
|
|
542
|
+
* @template F
|
|
543
|
+
* @template G
|
|
544
|
+
* @overload
|
|
545
|
+
* @param {(...args: A) => B} f1
|
|
546
|
+
* @param {(b: B) => C} f2
|
|
547
|
+
* @param {(c: C) => D} f3
|
|
548
|
+
* @param {(d: D) => E} f4
|
|
549
|
+
* @param {(e: E) => F} f5
|
|
550
|
+
* @param {(f: F) => G} f6
|
|
551
|
+
* @returns {(...args: A) => G}
|
|
552
|
+
*/
|
|
553
|
+
/**
|
|
554
|
+
* Chains any number of functions from left to right.
|
|
555
|
+
*
|
|
556
|
+
* @overload
|
|
557
|
+
* @param {...((arg: any) => any)} fns
|
|
558
|
+
* @returns {(...args: any[]) => any}
|
|
559
|
+
*/
|
|
560
|
+
/**
|
|
561
|
+
* @param {...((...args: any[]) => any)} fns
|
|
562
|
+
* @returns {(...args: any[]) => any}
|
|
563
|
+
*/
|
|
564
|
+
function pipe(...fns) {
|
|
565
|
+
return (...args) => {
|
|
566
|
+
if (!fns.length) return args[0];
|
|
567
|
+
let result = fns[0](...args);
|
|
568
|
+
for (let i = 1; i < fns.length; i++) result = fns[i](result);
|
|
569
|
+
return result;
|
|
570
|
+
};
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
/**
|
|
574
|
+
* Chains functions from right to left: `compose(f, g)(x)` is `f(g(x))`.
|
|
575
|
+
* `pipe()` reads in execution order and is better typed.
|
|
576
|
+
*
|
|
577
|
+
* @example
|
|
578
|
+
* const shout = func.compose((s) => s + "!", (s) => s.toUpperCase());
|
|
579
|
+
* shout("hi"); // "HI!"
|
|
580
|
+
*
|
|
581
|
+
* @param {...((...args: any[]) => any)} fns
|
|
582
|
+
* @returns {(...args: any[]) => any}
|
|
583
|
+
*/
|
|
584
|
+
function compose(...fns) {
|
|
585
|
+
return /** @type {(...f: Array<(...args: any[]) => any>) => (...args: any[]) => any} */ (pipe)(...[...fns].reverse());
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
/**
|
|
589
|
+
* Like `pipe()`, for functions that may be async: each one is awaited
|
|
590
|
+
* before the next runs.
|
|
591
|
+
*
|
|
592
|
+
* @example
|
|
593
|
+
* const publish = func.pipeAsync(loadDraft, renderMarkdown, uploadToCDN);
|
|
594
|
+
* const url = await publish(postId);
|
|
595
|
+
*
|
|
596
|
+
* @param {...((...args: any[]) => any)} fns
|
|
597
|
+
* @returns {(...args: any[]) => Promise<any>}
|
|
598
|
+
*/
|
|
599
|
+
function pipeAsync(...fns) {
|
|
600
|
+
return async (...args) => {
|
|
601
|
+
if (!fns.length) return args[0];
|
|
602
|
+
let result = await fns[0](...args);
|
|
603
|
+
for (let i = 1; i < fns.length; i++) result = await fns[i](result);
|
|
604
|
+
return result;
|
|
605
|
+
};
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
/**
|
|
609
|
+
* Turns many single calls into few grouped ones. Calls made close together
|
|
610
|
+
* are collected, `fn` runs once with all their items, and each caller gets
|
|
611
|
+
* its own result. The classic cure for "N+1 queries".
|
|
612
|
+
*
|
|
613
|
+
* @example
|
|
614
|
+
* const loadUser = func.batch(async (ids) => {
|
|
615
|
+
* const users = await db.getAll("users", { id: ids });
|
|
616
|
+
* return ids.map((id) => users.find((user) => user.id === id));
|
|
617
|
+
* });
|
|
618
|
+
* // One query, however many places ask at once:
|
|
619
|
+
* const [ada, bob] = await Promise.all([loadUser(1), loadUser(2)]);
|
|
620
|
+
*
|
|
621
|
+
* @template I
|
|
622
|
+
* @template R
|
|
623
|
+
* @param {(items: I[]) => ReadonlyArray<R | Error> | Map<I, R | Error> | PromiseLike<ReadonlyArray<R | Error> | Map<I, R | Error>>} fn Gets every waiting item and returns one result per item, in the same order, or a `Map` keyed by item. An `Error` as a result rejects that call only.
|
|
624
|
+
* @param {BatchOptions} [options]
|
|
625
|
+
* @returns {BatchedFunction<I, R>}
|
|
626
|
+
*/
|
|
627
|
+
function batch(fn, options = {}) {
|
|
628
|
+
const wait = options.wait === undefined ? 0 : toMs(options.wait, "wait");
|
|
629
|
+
const max = Math.max(1, options.max ?? Infinity);
|
|
630
|
+
/** @type {Array<{ item: I, resolve: (value: R) => void, reject: (reason: unknown) => void }>} */
|
|
631
|
+
let waiting = [];
|
|
632
|
+
/** @type {NodeJS.Timeout | undefined} */
|
|
633
|
+
let timer;
|
|
634
|
+
let scheduled = false;
|
|
635
|
+
|
|
636
|
+
const flush = () => {
|
|
637
|
+
scheduled = false;
|
|
638
|
+
clearTimeout(timer);
|
|
639
|
+
const current = waiting;
|
|
640
|
+
waiting = [];
|
|
641
|
+
if (!current.length) return;
|
|
642
|
+
Promise.resolve()
|
|
643
|
+
.then(() => fn(current.map((entry) => entry.item)))
|
|
644
|
+
.then((results) => {
|
|
645
|
+
const byItem = types.isMap(results);
|
|
646
|
+
if (!byItem && (!Array.isArray(results) || results.length !== current.length)) {
|
|
647
|
+
throw new TypeError(`batch(): expected ${current.length} results, one per item, got ${Array.isArray(results) ? results.length : typeof results}`);
|
|
648
|
+
}
|
|
649
|
+
current.forEach((entry, i) => {
|
|
650
|
+
const result = byItem ? /** @type {Map<I, R | Error>} */ (results).get(entry.item) : /** @type {ReadonlyArray<R | Error>} */ (results)[i];
|
|
651
|
+
if (types.isNativeError(result)) entry.reject(result);
|
|
652
|
+
else entry.resolve(/** @type {R} */ (result));
|
|
653
|
+
});
|
|
654
|
+
})
|
|
655
|
+
.catch((error) => {
|
|
656
|
+
for (const entry of current) entry.reject(error);
|
|
349
657
|
});
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
/**
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
658
|
+
};
|
|
659
|
+
|
|
660
|
+
/** @type {any} */
|
|
661
|
+
const batched = (/** @type {I} */ item) =>
|
|
662
|
+
new Promise((resolve, reject) => {
|
|
663
|
+
waiting.push({ item, resolve, reject });
|
|
664
|
+
if (waiting.length >= max) return flush();
|
|
665
|
+
if (scheduled) return;
|
|
666
|
+
scheduled = true;
|
|
667
|
+
if (wait > 0) timer = setTimeout(flush, wait);
|
|
668
|
+
// After the promise jobs of this tick, so calls made from resolved promises join the batch too.
|
|
669
|
+
else void Promise.resolve().then(() => process.nextTick(flush));
|
|
670
|
+
});
|
|
671
|
+
batched.flush = flush;
|
|
672
|
+
return batched;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
/**
|
|
676
|
+
* Makes concurrent calls with the same arguments share one run. Once it
|
|
677
|
+
* settles, the next call starts a new one: nothing is cached.
|
|
678
|
+
*
|
|
679
|
+
* @example
|
|
680
|
+
* const refreshToken = func.dedupe(async () => (await api.post("auth/refresh")).data);
|
|
681
|
+
* // Ten requests hit a 401 at once: one refresh, ten answers.
|
|
682
|
+
*
|
|
683
|
+
* @template {(...args: any[]) => any} F
|
|
684
|
+
* @param {F} fn
|
|
685
|
+
* @param {(...args: Parameters<F>) => unknown} [keyOf] Builds the key that identifies a call. Defaults to its arguments.
|
|
686
|
+
* @returns {(...args: Parameters<F>) => Promise<Awaited<ReturnType<F>>>}
|
|
687
|
+
*/
|
|
688
|
+
function dedupe(fn, keyOf) {
|
|
689
|
+
/** @type {Map<unknown, Promise<any>>} */
|
|
690
|
+
const running = new Map();
|
|
691
|
+
return /** @this {any} */ function(...args) {
|
|
692
|
+
const key = keyOf ? keyOf(...args) : argsKey(args);
|
|
693
|
+
const current = running.get(key);
|
|
694
|
+
if (current) return current;
|
|
695
|
+
/** @type {Promise<any>} */
|
|
696
|
+
let started;
|
|
697
|
+
try {
|
|
698
|
+
started = Promise.resolve(fn.apply(this, args));
|
|
699
|
+
} catch (error) {
|
|
700
|
+
started = Promise.reject(error);
|
|
701
|
+
}
|
|
702
|
+
const promise = started.finally(() => running.delete(key));
|
|
703
|
+
running.set(key, promise);
|
|
704
|
+
return promise;
|
|
705
|
+
};
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
/**
|
|
709
|
+
* Lets you pass a function's arguments a few at a time.
|
|
710
|
+
*
|
|
711
|
+
* @example
|
|
712
|
+
* const add = func.curry((a, b, c) => a + b + c);
|
|
713
|
+
* add(1)(2)(3); // 6
|
|
714
|
+
* add(1, 2)(3); // 6
|
|
715
|
+
*
|
|
716
|
+
* @param {(...args: any[]) => any} fn
|
|
717
|
+
* @param {number} [arity=fn.length] How many arguments to collect before calling.
|
|
718
|
+
* @returns {(...args: any[]) => any}
|
|
719
|
+
*/
|
|
720
|
+
function curry(fn, arity = fn.length) {
|
|
721
|
+
/** @type {(...args: any[]) => any} */
|
|
722
|
+
const curried = (...args) => (args.length >= arity ? fn(...args) : (/** @type {any[]} */ ...next) => curried(...args, ...next));
|
|
723
|
+
return curried;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
/**
|
|
727
|
+
* Fixes the first arguments of a function.
|
|
728
|
+
*
|
|
729
|
+
* @example
|
|
730
|
+
* const hello = func.partial((greeting, name) => `${greeting}, ${name}!`, "Hello");
|
|
731
|
+
* hello("Ada"); // "Hello, Ada!"
|
|
732
|
+
*
|
|
733
|
+
* @template {any[]} P
|
|
734
|
+
* @template {any[]} R
|
|
735
|
+
* @template T
|
|
736
|
+
* @param {(...args: [...P, ...R]) => T} fn
|
|
737
|
+
* @param {P} preset
|
|
738
|
+
* @returns {(...args: R) => T}
|
|
739
|
+
*/
|
|
740
|
+
function partial(fn, ...preset) {
|
|
741
|
+
return (...args) => fn(.../** @type {[...P, ...R]} */ ([...preset, ...args]));
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
/**
|
|
745
|
+
* The opposite of a predicate.
|
|
746
|
+
*
|
|
747
|
+
* @example
|
|
748
|
+
* files.filter(func.negate((name) => name.startsWith(".")));
|
|
749
|
+
*
|
|
750
|
+
* @template {any[]} A
|
|
751
|
+
* @param {(...args: A) => unknown} predicate
|
|
752
|
+
* @returns {(...args: A) => boolean}
|
|
753
|
+
*/
|
|
754
|
+
function negate(predicate) {
|
|
755
|
+
return (...args) => !predicate(...args);
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
/**
|
|
759
|
+
* Does nothing for the first `n - 1` calls, then calls `fn` every time.
|
|
760
|
+
*
|
|
761
|
+
* @example
|
|
762
|
+
* const done = func.after(3, () => console.log("All 3 uploads finished"));
|
|
763
|
+
* uploads.forEach((upload) => upload.on("end", done));
|
|
764
|
+
*
|
|
765
|
+
* @template {(...args: any[]) => any} F
|
|
766
|
+
* @param {number} n
|
|
767
|
+
* @param {F} fn
|
|
768
|
+
* @returns {(...args: Parameters<F>) => ReturnType<F> | undefined}
|
|
769
|
+
*/
|
|
770
|
+
function after(n, fn) {
|
|
771
|
+
let count = 0;
|
|
772
|
+
return (...args) => (++count >= n ? fn(...args) : undefined);
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
/**
|
|
776
|
+
* Calls `fn` for the first `n - 1` calls, then keeps returning the last
|
|
777
|
+
* result.
|
|
778
|
+
*
|
|
779
|
+
* @example
|
|
780
|
+
* const tryLogin = func.before(4, login); // 3 real attempts at most
|
|
781
|
+
*
|
|
782
|
+
* @template {(...args: any[]) => any} F
|
|
783
|
+
* @param {number} n
|
|
784
|
+
* @param {F} fn
|
|
785
|
+
* @returns {(...args: Parameters<F>) => ReturnType<F> | undefined}
|
|
786
|
+
*/
|
|
787
|
+
function before(n, fn) {
|
|
788
|
+
let count = 0;
|
|
789
|
+
/** @type {ReturnType<F> | undefined} */
|
|
790
|
+
let last;
|
|
791
|
+
return (...args) => {
|
|
792
|
+
if (++count < n) last = fn(...args);
|
|
793
|
+
return last;
|
|
794
|
+
};
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
/**
|
|
798
|
+
* Does nothing. Useful as a default callback.
|
|
799
|
+
*
|
|
800
|
+
* @param {...unknown} _args
|
|
801
|
+
* @returns {void}
|
|
802
|
+
*/
|
|
803
|
+
function noop(..._args) {}
|
|
804
|
+
|
|
805
|
+
/**
|
|
806
|
+
* Returns its argument.
|
|
807
|
+
*
|
|
808
|
+
* @example
|
|
809
|
+
* [0, 1, 2].filter(func.identity); // [1, 2]
|
|
810
|
+
*
|
|
811
|
+
* @template T
|
|
812
|
+
* @param {T} value
|
|
813
|
+
* @returns {T}
|
|
814
|
+
*/
|
|
815
|
+
function identity(value) {
|
|
816
|
+
return value;
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
module.exports = {
|
|
820
|
+
debounce,
|
|
821
|
+
throttle,
|
|
822
|
+
once,
|
|
823
|
+
memoize,
|
|
824
|
+
retry,
|
|
825
|
+
timeout,
|
|
826
|
+
delay,
|
|
827
|
+
to,
|
|
828
|
+
attempt,
|
|
829
|
+
promisify,
|
|
830
|
+
batch,
|
|
831
|
+
dedupe,
|
|
832
|
+
pipe,
|
|
833
|
+
pipeAsync,
|
|
834
|
+
compose,
|
|
835
|
+
curry,
|
|
836
|
+
partial,
|
|
837
|
+
negate,
|
|
838
|
+
after,
|
|
839
|
+
before,
|
|
840
|
+
noop,
|
|
841
|
+
identity,
|
|
375
842
|
};
|