@ix-xs/node-comfort 1.1.31 → 2.0.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 +190 -0
- package/LICENSE +21 -0
- package/README.md +210 -748
- package/index.js +84 -13
- package/package.json +201 -21
- package/src/Arr.js +901 -0
- package/src/Async.js +585 -0
- package/src/Cache.js +329 -0
- package/src/Checker.js +979 -188
- package/src/Cli.js +1034 -0
- package/src/Color.js +415 -0
- package/src/Crypto.js +707 -0
- package/src/Emitter.js +289 -0
- package/src/Env.js +555 -0
- package/src/FS.js +1483 -768
- package/src/Func.js +692 -0
- package/src/Http.js +538 -0
- package/src/Id.js +418 -0
- package/src/Logger.js +739 -307
- package/src/Num.js +849 -0
- package/src/Obj.js +904 -0
- package/src/SQLite.js +1308 -994
- package/src/Schema.js +1586 -0
- package/src/Str.js +999 -0
- package/src/Sys.js +511 -0
- package/src/Time.js +1323 -0
- package/src/Utils.js +321 -325
- package/src/errors.js +292 -0
- package/types/index.d.ts +693 -120
- package/types/src/Arr.d.ts +562 -0
- package/types/src/Arr.d.ts.map +1 -0
- package/types/src/Async.d.ts +321 -0
- package/types/src/Async.d.ts.map +1 -0
- package/types/src/Cache.d.ts +195 -0
- package/types/src/Cache.d.ts.map +1 -0
- package/types/src/Checker.d.ts +609 -81
- package/types/src/Checker.d.ts.map +1 -1
- package/types/src/Cli.d.ts +582 -0
- package/types/src/Cli.d.ts.map +1 -0
- package/types/src/Color.d.ts +294 -0
- package/types/src/Color.d.ts.map +1 -0
- package/types/src/Crypto.d.ts +534 -0
- package/types/src/Crypto.d.ts.map +1 -0
- package/types/src/Emitter.d.ts +170 -0
- package/types/src/Emitter.d.ts.map +1 -0
- package/types/src/Env.d.ts +382 -0
- package/types/src/Env.d.ts.map +1 -0
- package/types/src/FS.d.ts +709 -201
- package/types/src/FS.d.ts.map +1 -1
- package/types/src/Func.d.ts +455 -0
- package/types/src/Func.d.ts.map +1 -0
- package/types/src/Http.d.ts +374 -0
- package/types/src/Http.d.ts.map +1 -0
- package/types/src/Id.d.ts +200 -0
- package/types/src/Id.d.ts.map +1 -0
- package/types/src/Logger.d.ts +374 -24
- package/types/src/Logger.d.ts.map +1 -1
- package/types/src/Num.d.ts +578 -0
- package/types/src/Num.d.ts.map +1 -0
- package/types/src/Obj.d.ts +475 -0
- package/types/src/Obj.d.ts.map +1 -0
- package/types/src/SQLite.d.ts +647 -305
- package/types/src/SQLite.d.ts.map +1 -1
- package/types/src/Schema.d.ts +940 -0
- package/types/src/Schema.d.ts.map +1 -0
- package/types/src/Str.d.ts +696 -0
- package/types/src/Str.d.ts.map +1 -0
- package/types/src/Sys.d.ts +269 -0
- package/types/src/Sys.d.ts.map +1 -0
- package/types/src/Time.d.ts +664 -0
- package/types/src/Time.d.ts.map +1 -0
- package/types/src/Utils.d.ts +154 -102
- package/types/src/Utils.d.ts.map +1 -1
- package/types/src/errors.d.ts +292 -0
- package/types/src/errors.d.ts.map +1 -0
- package/tsconfig.json +0 -18
- package/types/index.d.ts.map +0 -1
package/src/Func.js
ADDED
|
@@ -0,0 +1,692 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Function helpers: debounce, throttle, memoize, retry with backoff,
|
|
5
|
+
* timeouts, error handling without try/catch, and typed composition.
|
|
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));
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const { TimeoutError, AbortError } = require("./errors.js");
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Options for `debounce()`.
|
|
17
|
+
* @typedef {object} DebounceOptions
|
|
18
|
+
* @property {boolean} [leading] Call right away at the start of a burst.
|
|
19
|
+
* @property {boolean} [trailing] Call once the burst is over. Defaults to `true`.
|
|
20
|
+
* @property {number} [maxWait] Never postpone a call longer than this, in ms, even if calls keep coming.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* A debounced function. `flush()` runs the pending call now, `cancel()`
|
|
25
|
+
* drops it, `pending()` tells you if there's one.
|
|
26
|
+
* @template {(...args: any[]) => any} F
|
|
27
|
+
* @typedef {((...args: Parameters<F>) => void) & { cancel(): void, flush(): void, pending(): boolean }} DebouncedFunction
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Options for `throttle()`.
|
|
32
|
+
* @typedef {object} ThrottleOptions
|
|
33
|
+
* @property {boolean} [leading] Call right away on the first call. Defaults to `true`.
|
|
34
|
+
* @property {boolean} [trailing] Make a last call with the latest arguments at the end of the interval. Defaults to `true`.
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* A throttled function, with `cancel()`.
|
|
39
|
+
* @template {(...args: any[]) => any} F
|
|
40
|
+
* @typedef {((...args: Parameters<F>) => void) & { cancel(): void }} ThrottledFunction
|
|
41
|
+
*/
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Options for `memoize()`.
|
|
45
|
+
* @typedef {object} MemoizeOptions
|
|
46
|
+
* @property {(...args: any[]) => unknown} [resolver] Builds the cache key from the arguments. Defaults to the arguments as JSON.
|
|
47
|
+
* @property {number} [ttl] How long a result stays cached, in ms.
|
|
48
|
+
* @property {number} [max] Maximum number of results; the least recently used goes first.
|
|
49
|
+
* @property {boolean} [cacheRejections] Keep rejected promises. By default they're dropped so the next call tries again.
|
|
50
|
+
*/
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* A memoized function, with its `cache`, `clear()` and `delete(...args)`.
|
|
54
|
+
* @template {(...args: any[]) => any} F
|
|
55
|
+
* @typedef {F & { cache: Map<unknown, { value: ReturnType<F>, expires: number }>, clear(): void, delete(...args: Parameters<F>): boolean }} MemoizedFunction
|
|
56
|
+
*/
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Options for `retry()`.
|
|
60
|
+
* @typedef {object} RetryOptions
|
|
61
|
+
* @property {number} [attempts] Total attempts, the first one included. Defaults to `3`.
|
|
62
|
+
* @property {number | ((attempt: number, error: unknown) => number)} [delay] Wait before the next attempt, in ms, or a function that computes it.
|
|
63
|
+
* @property {number} [backoff] Multiplies the delay after each failure: `2` gives 200, 400, 800... Defaults to `1`.
|
|
64
|
+
* @property {number} [maxDelay] Cap for the computed delay, in ms.
|
|
65
|
+
* @property {number} [jitter] Randomizes the delay, from `0` (none) to `1` (anywhere between 0 and 2x). Stops many clients from retrying in sync.
|
|
66
|
+
* @property {(error: unknown, attempt: number) => boolean | Promise<boolean>} [shouldRetry] Return `false` to give up early, for example on a 4xx error.
|
|
67
|
+
* @property {(error: unknown, attempt: number, delay: number) => void} [onRetry] Called before each wait, for logs or metrics.
|
|
68
|
+
* @property {AbortSignal} [signal] Stops retrying. The attempt in progress isn't interrupted.
|
|
69
|
+
*/
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* @param {number} ms
|
|
73
|
+
* @param {AbortSignal} [signal]
|
|
74
|
+
* @returns {Promise<void>}
|
|
75
|
+
*/
|
|
76
|
+
const _sleep = (ms, signal) =>
|
|
77
|
+
new Promise((resolve, reject) => {
|
|
78
|
+
if (signal?.aborted) return reject(new AbortError(undefined, { cause: signal.reason }));
|
|
79
|
+
const timer = setTimeout(() => {
|
|
80
|
+
signal?.removeEventListener("abort", onAbort);
|
|
81
|
+
resolve();
|
|
82
|
+
}, ms);
|
|
83
|
+
const onAbort = () => {
|
|
84
|
+
clearTimeout(timer);
|
|
85
|
+
reject(new AbortError(undefined, { cause: signal?.reason }));
|
|
86
|
+
};
|
|
87
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Waits for a pause of `wait` ms between calls, then runs once with the
|
|
92
|
+
* latest arguments. For search boxes, resizing, auto-saving...
|
|
93
|
+
*
|
|
94
|
+
* @example
|
|
95
|
+
* const search = func.debounce((query) => api.search(query), 300);
|
|
96
|
+
* input.on("input", (e) => search(e.target.value));
|
|
97
|
+
*
|
|
98
|
+
* const save = func.debounce(persist, 1000, { maxWait: 5000 }); // saves at least every 5s
|
|
99
|
+
*
|
|
100
|
+
* @template {(...args: any[]) => any} F
|
|
101
|
+
* @param {F} fn
|
|
102
|
+
* @param {number} wait Pause needed, in ms.
|
|
103
|
+
* @param {DebounceOptions} [options]
|
|
104
|
+
* @returns {DebouncedFunction<F>}
|
|
105
|
+
*/
|
|
106
|
+
function debounce(fn, wait, options = {}) {
|
|
107
|
+
const leading = options.leading ?? false;
|
|
108
|
+
const trailing = options.trailing ?? true;
|
|
109
|
+
const maxWait = options.maxWait;
|
|
110
|
+
/** @type {NodeJS.Timeout | undefined} */
|
|
111
|
+
let timer;
|
|
112
|
+
/** @type {NodeJS.Timeout | undefined} */
|
|
113
|
+
let maxTimer;
|
|
114
|
+
/** @type {any[] | undefined} */
|
|
115
|
+
let lastArgs;
|
|
116
|
+
/** @type {unknown} */
|
|
117
|
+
let lastThis;
|
|
118
|
+
|
|
119
|
+
const invoke = () => {
|
|
120
|
+
const args = lastArgs;
|
|
121
|
+
const self = lastThis;
|
|
122
|
+
lastArgs = undefined;
|
|
123
|
+
lastThis = undefined;
|
|
124
|
+
if (args) fn.apply(self, args);
|
|
125
|
+
};
|
|
126
|
+
const clearTimers = () => {
|
|
127
|
+
clearTimeout(timer);
|
|
128
|
+
clearTimeout(maxTimer);
|
|
129
|
+
timer = undefined;
|
|
130
|
+
maxTimer = undefined;
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
/** @type {any} */
|
|
134
|
+
const debounced = /** @this {any} */ function(/** @type {any[]} */ ...args) {
|
|
135
|
+
const isFirst = timer === undefined;
|
|
136
|
+
lastArgs = args;
|
|
137
|
+
lastThis = this;
|
|
138
|
+
clearTimeout(timer);
|
|
139
|
+
timer = setTimeout(() => {
|
|
140
|
+
clearTimers();
|
|
141
|
+
if (trailing) invoke();
|
|
142
|
+
else lastArgs = undefined;
|
|
143
|
+
}, wait);
|
|
144
|
+
if (maxWait !== undefined && maxTimer === undefined) {
|
|
145
|
+
maxTimer = setTimeout(() => {
|
|
146
|
+
clearTimers();
|
|
147
|
+
invoke();
|
|
148
|
+
}, maxWait);
|
|
149
|
+
}
|
|
150
|
+
if (leading && isFirst) {
|
|
151
|
+
invoke();
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
debounced.cancel = () => {
|
|
155
|
+
clearTimers();
|
|
156
|
+
lastArgs = undefined;
|
|
157
|
+
};
|
|
158
|
+
debounced.flush = () => {
|
|
159
|
+
clearTimers();
|
|
160
|
+
invoke();
|
|
161
|
+
};
|
|
162
|
+
debounced.pending = () => timer !== undefined && lastArgs !== undefined;
|
|
163
|
+
return debounced;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Runs at most once every `wait` ms, however often it's called. For scroll
|
|
168
|
+
* handlers, progress reports, rate-limited APIs...
|
|
169
|
+
*
|
|
170
|
+
* @example
|
|
171
|
+
* const report = func.throttle((pct) => console.log(`${pct}%`), 1000);
|
|
172
|
+
*
|
|
173
|
+
* @template {(...args: any[]) => any} F
|
|
174
|
+
* @param {F} fn
|
|
175
|
+
* @param {number} wait Minimum gap between calls, in ms.
|
|
176
|
+
* @param {ThrottleOptions} [options]
|
|
177
|
+
* @returns {ThrottledFunction<F>}
|
|
178
|
+
*/
|
|
179
|
+
function throttle(fn, wait, options = {}) {
|
|
180
|
+
const leading = options.leading ?? true;
|
|
181
|
+
const trailing = options.trailing ?? true;
|
|
182
|
+
let last = 0;
|
|
183
|
+
/** @type {NodeJS.Timeout | undefined} */
|
|
184
|
+
let timer;
|
|
185
|
+
/** @type {any[] | undefined} */
|
|
186
|
+
let lastArgs;
|
|
187
|
+
/** @type {unknown} */
|
|
188
|
+
let lastThis;
|
|
189
|
+
|
|
190
|
+
/** @type {any} */
|
|
191
|
+
const throttled = /** @this {any} */ function(/** @type {any[]} */ ...args) {
|
|
192
|
+
const now = Date.now();
|
|
193
|
+
if (!last && !leading) last = now;
|
|
194
|
+
const remaining = wait - (now - last);
|
|
195
|
+
lastArgs = args;
|
|
196
|
+
lastThis = this;
|
|
197
|
+
if (remaining <= 0 || remaining > wait) {
|
|
198
|
+
clearTimeout(timer);
|
|
199
|
+
timer = undefined;
|
|
200
|
+
last = now;
|
|
201
|
+
fn.apply(lastThis, lastArgs);
|
|
202
|
+
lastArgs = undefined;
|
|
203
|
+
} else if (trailing && timer === undefined) {
|
|
204
|
+
timer = setTimeout(() => {
|
|
205
|
+
last = leading ? Date.now() : 0;
|
|
206
|
+
timer = undefined;
|
|
207
|
+
if (lastArgs) fn.apply(lastThis, lastArgs);
|
|
208
|
+
lastArgs = undefined;
|
|
209
|
+
}, remaining);
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
throttled.cancel = () => {
|
|
213
|
+
clearTimeout(timer);
|
|
214
|
+
timer = undefined;
|
|
215
|
+
last = 0;
|
|
216
|
+
lastArgs = undefined;
|
|
217
|
+
};
|
|
218
|
+
return throttled;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Runs `fn` the first time only; later calls return the same result.
|
|
223
|
+
*
|
|
224
|
+
* @example
|
|
225
|
+
* const connect = func.once(() => createPool(config));
|
|
226
|
+
*
|
|
227
|
+
* @template {(...args: any[]) => any} F
|
|
228
|
+
* @param {F} fn
|
|
229
|
+
* @returns {F}
|
|
230
|
+
*/
|
|
231
|
+
function once(fn) {
|
|
232
|
+
let called = false;
|
|
233
|
+
/** @type {ReturnType<F>} */
|
|
234
|
+
let result;
|
|
235
|
+
return /** @type {F} */ (
|
|
236
|
+
/** @this {any} */ function(/** @type {any[]} */ ...args) {
|
|
237
|
+
if (!called) {
|
|
238
|
+
called = true;
|
|
239
|
+
result = fn.apply(this, args);
|
|
240
|
+
}
|
|
241
|
+
return result;
|
|
242
|
+
}
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Caches results by arguments. Supports expiry (`ttl`), a size limit
|
|
248
|
+
* (`max`) and async functions: a failed promise isn't cached, so the next
|
|
249
|
+
* call tries again.
|
|
250
|
+
*
|
|
251
|
+
* @example
|
|
252
|
+
* const getUser = func.memoize((id) => db.users.find(id), { ttl: 60_000, max: 1000 });
|
|
253
|
+
* getUser.delete(42); // forget one entry
|
|
254
|
+
* getUser.clear(); // forget everything
|
|
255
|
+
*
|
|
256
|
+
* @template {(...args: any[]) => any} F
|
|
257
|
+
* @param {F} fn
|
|
258
|
+
* @param {MemoizeOptions | ((...args: Parameters<F>) => unknown)} [options] Options, or a function that builds the cache key.
|
|
259
|
+
* @returns {MemoizedFunction<F>}
|
|
260
|
+
*/
|
|
261
|
+
function memoize(fn, options = {}) {
|
|
262
|
+
const opts = typeof options === "function" ? { resolver: options } : options;
|
|
263
|
+
/** @type {Map<unknown, { value: any, expires: number }>} */
|
|
264
|
+
const cache = new Map();
|
|
265
|
+
/** @type {(args: any[]) => unknown} */
|
|
266
|
+
const keyOf = (args) => {
|
|
267
|
+
if (opts.resolver) return opts.resolver(...args);
|
|
268
|
+
if (args.length === 1 && (args[0] === null || typeof args[0] !== "object")) return args[0];
|
|
269
|
+
return JSON.stringify(args);
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
/** @type {any} */
|
|
273
|
+
const memoized = /** @this {any} */ function(/** @type {any[]} */ ...args) {
|
|
274
|
+
const key = keyOf(args);
|
|
275
|
+
const hit = cache.get(key);
|
|
276
|
+
if (hit && (hit.expires === 0 || hit.expires > Date.now())) {
|
|
277
|
+
cache.delete(key);
|
|
278
|
+
cache.set(key, hit);
|
|
279
|
+
return hit.value;
|
|
280
|
+
}
|
|
281
|
+
const value = fn.apply(this, args);
|
|
282
|
+
cache.set(key, { value, expires: opts.ttl ? Date.now() + opts.ttl : 0 });
|
|
283
|
+
if (opts.max !== undefined && cache.size > opts.max) cache.delete(cache.keys().next().value);
|
|
284
|
+
if (!opts.cacheRejections && value && typeof value.then === "function") {
|
|
285
|
+
value.then(undefined, () => {
|
|
286
|
+
if (cache.get(key)?.value === value) cache.delete(key);
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
return value;
|
|
290
|
+
};
|
|
291
|
+
memoized.cache = cache;
|
|
292
|
+
memoized.clear = () => cache.clear();
|
|
293
|
+
memoized.delete = (/** @type {any[]} */ ...args) => cache.delete(keyOf(args));
|
|
294
|
+
return memoized;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Calls `fn` until it succeeds, waiting between attempts. If every attempt
|
|
299
|
+
* fails, the last error is thrown.
|
|
300
|
+
*
|
|
301
|
+
* @example
|
|
302
|
+
* const data = await func.retry(() => fetchJSON(url), {
|
|
303
|
+
* attempts: 5,
|
|
304
|
+
* delay: 200,
|
|
305
|
+
* backoff: 2, // 200, 400, 800, 1600 ms
|
|
306
|
+
* shouldRetry: (error) => !(error instanceof nc.errors.HttpError && error.status < 500),
|
|
307
|
+
* });
|
|
308
|
+
*
|
|
309
|
+
* @template T
|
|
310
|
+
* @param {(attempt: number) => T | PromiseLike<T>} fn Receives the attempt number, starting at 1.
|
|
311
|
+
* @param {RetryOptions} [options]
|
|
312
|
+
* @returns {Promise<Awaited<T>>}
|
|
313
|
+
* @throws The last error, or an `AbortError` if the signal fires.
|
|
314
|
+
*/
|
|
315
|
+
async function retry(fn, options = {}) {
|
|
316
|
+
const attempts = Math.max(1, options.attempts ?? 3);
|
|
317
|
+
const backoff = options.backoff ?? 1;
|
|
318
|
+
const maxDelay = options.maxDelay ?? Infinity;
|
|
319
|
+
const jitter = Math.min(1, Math.max(0, options.jitter ?? 0));
|
|
320
|
+
/** @type {unknown} */
|
|
321
|
+
let lastError;
|
|
322
|
+
for (let attempt = 1; attempt <= attempts; attempt++) {
|
|
323
|
+
if (options.signal?.aborted) throw new AbortError(undefined, { cause: options.signal.reason });
|
|
324
|
+
try {
|
|
325
|
+
return await fn(attempt);
|
|
326
|
+
} catch (error) {
|
|
327
|
+
lastError = error;
|
|
328
|
+
if (attempt >= attempts) break;
|
|
329
|
+
if (options.shouldRetry && !(await options.shouldRetry(error, attempt))) break;
|
|
330
|
+
const base = typeof options.delay === "function" ? options.delay(attempt, error) : (options.delay ?? 0) * backoff ** (attempt - 1);
|
|
331
|
+
const wait = Math.max(0, Math.round(Math.min(base, maxDelay) * (1 + jitter * (Math.random() * 2 - 1))));
|
|
332
|
+
options.onRetry?.(error, attempt, wait);
|
|
333
|
+
if (wait > 0) await _sleep(wait, options.signal);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
throw lastError;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Rejects with a `TimeoutError` if the promise takes longer than `ms`.
|
|
341
|
+
*
|
|
342
|
+
* @example
|
|
343
|
+
* const res = await func.timeout(fetch(url), 5000);
|
|
344
|
+
* const rows = await func.timeout(() => slowQuery(), 2000, "Database too slow");
|
|
345
|
+
*
|
|
346
|
+
* @template T
|
|
347
|
+
* @param {PromiseLike<T> | (() => PromiseLike<T> | T)} promise A promise, or a function that returns one.
|
|
348
|
+
* @param {number} ms
|
|
349
|
+
* @param {string} [message]
|
|
350
|
+
* @returns {Promise<Awaited<T>>}
|
|
351
|
+
* @throws {TimeoutError}
|
|
352
|
+
*/
|
|
353
|
+
function timeout(promise, ms, message) {
|
|
354
|
+
/** @type {NodeJS.Timeout | undefined} */
|
|
355
|
+
let timer;
|
|
356
|
+
const expired = new Promise((_, reject) => {
|
|
357
|
+
timer = setTimeout(() => reject(new TimeoutError(message ?? `Operation timed out after ${ms}ms`, { timeout: ms })), ms);
|
|
358
|
+
});
|
|
359
|
+
const target = Promise.resolve().then(() => (typeof promise === "function" ? promise() : promise));
|
|
360
|
+
return /** @type {Promise<Awaited<T>>} */ (Promise.race([target, expired]).finally(() => clearTimeout(timer)));
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Waits `ms`, then calls `fn` with the arguments and resolves with its result.
|
|
365
|
+
*
|
|
366
|
+
* @example
|
|
367
|
+
* await func.delay(() => console.log("1s later"), 1000);
|
|
368
|
+
*
|
|
369
|
+
* @template {(...args: any[]) => any} F
|
|
370
|
+
* @param {F} fn
|
|
371
|
+
* @param {number} ms
|
|
372
|
+
* @param {Parameters<F>} args
|
|
373
|
+
* @returns {Promise<Awaited<ReturnType<F>>>}
|
|
374
|
+
*/
|
|
375
|
+
async function delay(fn, ms, ...args) {
|
|
376
|
+
await _sleep(ms);
|
|
377
|
+
return fn(...args);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Awaits a promise and gives you `[error, value]` instead of throwing, so
|
|
382
|
+
* you don't need a try/catch.
|
|
383
|
+
*
|
|
384
|
+
* @example
|
|
385
|
+
* const [error, user] = await func.to(db.users.find(id));
|
|
386
|
+
* if (error) return res.status(500).send(error.message);
|
|
387
|
+
*
|
|
388
|
+
* @template T
|
|
389
|
+
* @template [E=Error]
|
|
390
|
+
* @param {PromiseLike<T> | (() => PromiseLike<T> | T)} promise A promise, or a function (sync errors are caught too).
|
|
391
|
+
* @returns {Promise<[E, undefined] | [null, Awaited<T>]>}
|
|
392
|
+
*/
|
|
393
|
+
async function to(promise) {
|
|
394
|
+
try {
|
|
395
|
+
const value = await (typeof promise === "function" ? promise() : promise);
|
|
396
|
+
return [null, /** @type {Awaited<T>} */ (value)];
|
|
397
|
+
} catch (error) {
|
|
398
|
+
return [/** @type {E} */ (error), undefined];
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Wraps a function so it resolves to `[error, value]` instead of throwing.
|
|
404
|
+
*
|
|
405
|
+
* @example
|
|
406
|
+
* const safeParse = func.attempt(JSON.parse);
|
|
407
|
+
* const [error, data] = await safeParse(input);
|
|
408
|
+
*
|
|
409
|
+
* @template {(...args: any[]) => any} F
|
|
410
|
+
* @param {F} fn
|
|
411
|
+
* @returns {(...args: Parameters<F>) => Promise<[unknown, undefined] | [null, Awaited<ReturnType<F>>]>}
|
|
412
|
+
*/
|
|
413
|
+
function attempt(fn) {
|
|
414
|
+
return /** @this {any} */ function(...args) {
|
|
415
|
+
return to(() => fn.apply(this, args));
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* Turns a callback-style function into one that returns a promise.
|
|
421
|
+
*
|
|
422
|
+
* @example
|
|
423
|
+
* const lookup = func.promisify(dns.lookup);
|
|
424
|
+
* const address = await lookup("nodejs.org");
|
|
425
|
+
*
|
|
426
|
+
* @param {(...args: any[]) => void} fn
|
|
427
|
+
* @returns {(...args: any[]) => Promise<any>}
|
|
428
|
+
*/
|
|
429
|
+
function promisify(fn) {
|
|
430
|
+
return /** @this {any} */ function(...args) {
|
|
431
|
+
return new Promise((resolve, reject) => {
|
|
432
|
+
fn.call(this, ...args, (/** @type {unknown} */ error, /** @type {unknown} */ result) => (error ? reject(error) : resolve(result)));
|
|
433
|
+
});
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* Chains functions from left to right: `pipe(f, g)(x)` is `g(f(x))`.
|
|
439
|
+
* Types flow through up to 6 functions.
|
|
440
|
+
*
|
|
441
|
+
* @example
|
|
442
|
+
* const toSlug = func.pipe(nc.str.deburr, nc.str.kebabCase);
|
|
443
|
+
* toSlug("Crème Brûlée"); // "creme-brulee"
|
|
444
|
+
*
|
|
445
|
+
* @template {any[]} A
|
|
446
|
+
* @template B
|
|
447
|
+
* @overload
|
|
448
|
+
* @param {(...args: A) => B} f1
|
|
449
|
+
* @returns {(...args: A) => B}
|
|
450
|
+
*/
|
|
451
|
+
/**
|
|
452
|
+
* Chains functions from left to right.
|
|
453
|
+
*
|
|
454
|
+
* @template {any[]} A
|
|
455
|
+
* @template B
|
|
456
|
+
* @template C
|
|
457
|
+
* @overload
|
|
458
|
+
* @param {(...args: A) => B} f1
|
|
459
|
+
* @param {(b: B) => C} f2
|
|
460
|
+
* @returns {(...args: A) => C}
|
|
461
|
+
*/
|
|
462
|
+
/**
|
|
463
|
+
* Chains functions from left to right.
|
|
464
|
+
*
|
|
465
|
+
* @template {any[]} A
|
|
466
|
+
* @template B
|
|
467
|
+
* @template C
|
|
468
|
+
* @template D
|
|
469
|
+
* @overload
|
|
470
|
+
* @param {(...args: A) => B} f1
|
|
471
|
+
* @param {(b: B) => C} f2
|
|
472
|
+
* @param {(c: C) => D} f3
|
|
473
|
+
* @returns {(...args: A) => D}
|
|
474
|
+
*/
|
|
475
|
+
/**
|
|
476
|
+
* Chains functions from left to right.
|
|
477
|
+
*
|
|
478
|
+
* @template {any[]} A
|
|
479
|
+
* @template B
|
|
480
|
+
* @template C
|
|
481
|
+
* @template D
|
|
482
|
+
* @template E
|
|
483
|
+
* @overload
|
|
484
|
+
* @param {(...args: A) => B} f1
|
|
485
|
+
* @param {(b: B) => C} f2
|
|
486
|
+
* @param {(c: C) => D} f3
|
|
487
|
+
* @param {(d: D) => E} f4
|
|
488
|
+
* @returns {(...args: A) => E}
|
|
489
|
+
*/
|
|
490
|
+
/**
|
|
491
|
+
* Chains functions from left to right.
|
|
492
|
+
*
|
|
493
|
+
* @template {any[]} A
|
|
494
|
+
* @template B
|
|
495
|
+
* @template C
|
|
496
|
+
* @template D
|
|
497
|
+
* @template E
|
|
498
|
+
* @template F
|
|
499
|
+
* @overload
|
|
500
|
+
* @param {(...args: A) => B} f1
|
|
501
|
+
* @param {(b: B) => C} f2
|
|
502
|
+
* @param {(c: C) => D} f3
|
|
503
|
+
* @param {(d: D) => E} f4
|
|
504
|
+
* @param {(e: E) => F} f5
|
|
505
|
+
* @returns {(...args: A) => F}
|
|
506
|
+
*/
|
|
507
|
+
/**
|
|
508
|
+
* Chains functions from left to right.
|
|
509
|
+
*
|
|
510
|
+
* @template {any[]} A
|
|
511
|
+
* @template B
|
|
512
|
+
* @template C
|
|
513
|
+
* @template D
|
|
514
|
+
* @template E
|
|
515
|
+
* @template F
|
|
516
|
+
* @template G
|
|
517
|
+
* @overload
|
|
518
|
+
* @param {(...args: A) => B} f1
|
|
519
|
+
* @param {(b: B) => C} f2
|
|
520
|
+
* @param {(c: C) => D} f3
|
|
521
|
+
* @param {(d: D) => E} f4
|
|
522
|
+
* @param {(e: E) => F} f5
|
|
523
|
+
* @param {(f: F) => G} f6
|
|
524
|
+
* @returns {(...args: A) => G}
|
|
525
|
+
*/
|
|
526
|
+
/**
|
|
527
|
+
* Chains any number of functions from left to right.
|
|
528
|
+
*
|
|
529
|
+
* @overload
|
|
530
|
+
* @param {...((arg: any) => any)} fns
|
|
531
|
+
* @returns {(...args: any[]) => any}
|
|
532
|
+
*/
|
|
533
|
+
/**
|
|
534
|
+
* @param {...((...args: any[]) => any)} fns
|
|
535
|
+
* @returns {(...args: any[]) => any}
|
|
536
|
+
*/
|
|
537
|
+
function pipe(...fns) {
|
|
538
|
+
return (...args) => {
|
|
539
|
+
if (!fns.length) return args[0];
|
|
540
|
+
let result = fns[0](...args);
|
|
541
|
+
for (let i = 1; i < fns.length; i++) result = fns[i](result);
|
|
542
|
+
return result;
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/**
|
|
547
|
+
* Chains functions from right to left: `compose(f, g)(x)` is `f(g(x))`.
|
|
548
|
+
* `pipe()` reads in execution order and is better typed.
|
|
549
|
+
*
|
|
550
|
+
* @example
|
|
551
|
+
* const shout = func.compose((s) => s + "!", (s) => s.toUpperCase());
|
|
552
|
+
* shout("hi"); // "HI!"
|
|
553
|
+
*
|
|
554
|
+
* @param {...((...args: any[]) => any)} fns
|
|
555
|
+
* @returns {(...args: any[]) => any}
|
|
556
|
+
*/
|
|
557
|
+
function compose(...fns) {
|
|
558
|
+
return /** @type {(...f: Array<(...args: any[]) => any>) => (...args: any[]) => any} */ (pipe)(...[...fns].reverse());
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
/**
|
|
562
|
+
* Lets you pass a function's arguments a few at a time.
|
|
563
|
+
*
|
|
564
|
+
* @example
|
|
565
|
+
* const add = func.curry((a, b, c) => a + b + c);
|
|
566
|
+
* add(1)(2)(3); // 6
|
|
567
|
+
* add(1, 2)(3); // 6
|
|
568
|
+
*
|
|
569
|
+
* @param {(...args: any[]) => any} fn
|
|
570
|
+
* @param {number} [arity=fn.length] How many arguments to collect before calling.
|
|
571
|
+
* @returns {(...args: any[]) => any}
|
|
572
|
+
*/
|
|
573
|
+
function curry(fn, arity = fn.length) {
|
|
574
|
+
/** @type {(...args: any[]) => any} */
|
|
575
|
+
const curried = (...args) => (args.length >= arity ? fn(...args) : (/** @type {any[]} */ ...next) => curried(...args, ...next));
|
|
576
|
+
return curried;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
/**
|
|
580
|
+
* Fixes the first arguments of a function.
|
|
581
|
+
*
|
|
582
|
+
* @example
|
|
583
|
+
* const hello = func.partial((greeting, name) => `${greeting}, ${name}!`, "Hello");
|
|
584
|
+
* hello("Ada"); // "Hello, Ada!"
|
|
585
|
+
*
|
|
586
|
+
* @template {any[]} P
|
|
587
|
+
* @template {any[]} R
|
|
588
|
+
* @template T
|
|
589
|
+
* @param {(...args: [...P, ...R]) => T} fn
|
|
590
|
+
* @param {P} preset
|
|
591
|
+
* @returns {(...args: R) => T}
|
|
592
|
+
*/
|
|
593
|
+
function partial(fn, ...preset) {
|
|
594
|
+
return (...args) => fn(.../** @type {[...P, ...R]} */ ([...preset, ...args]));
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
/**
|
|
598
|
+
* The opposite of a predicate.
|
|
599
|
+
*
|
|
600
|
+
* @example
|
|
601
|
+
* files.filter(func.negate((name) => name.startsWith(".")));
|
|
602
|
+
*
|
|
603
|
+
* @template {any[]} A
|
|
604
|
+
* @param {(...args: A) => unknown} predicate
|
|
605
|
+
* @returns {(...args: A) => boolean}
|
|
606
|
+
*/
|
|
607
|
+
function negate(predicate) {
|
|
608
|
+
return (...args) => !predicate(...args);
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
/**
|
|
612
|
+
* Does nothing for the first `n - 1` calls, then calls `fn` every time.
|
|
613
|
+
*
|
|
614
|
+
* @example
|
|
615
|
+
* const done = func.after(3, () => console.log("All 3 uploads finished"));
|
|
616
|
+
* uploads.forEach((upload) => upload.on("end", done));
|
|
617
|
+
*
|
|
618
|
+
* @template {(...args: any[]) => any} F
|
|
619
|
+
* @param {number} n
|
|
620
|
+
* @param {F} fn
|
|
621
|
+
* @returns {(...args: Parameters<F>) => ReturnType<F> | undefined}
|
|
622
|
+
*/
|
|
623
|
+
function after(n, fn) {
|
|
624
|
+
let count = 0;
|
|
625
|
+
return (...args) => (++count >= n ? fn(...args) : undefined);
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
/**
|
|
629
|
+
* Calls `fn` for the first `n - 1` calls, then keeps returning the last
|
|
630
|
+
* result.
|
|
631
|
+
*
|
|
632
|
+
* @example
|
|
633
|
+
* const tryLogin = func.before(4, login); // 3 real attempts at most
|
|
634
|
+
*
|
|
635
|
+
* @template {(...args: any[]) => any} F
|
|
636
|
+
* @param {number} n
|
|
637
|
+
* @param {F} fn
|
|
638
|
+
* @returns {(...args: Parameters<F>) => ReturnType<F> | undefined}
|
|
639
|
+
*/
|
|
640
|
+
function before(n, fn) {
|
|
641
|
+
let count = 0;
|
|
642
|
+
/** @type {ReturnType<F> | undefined} */
|
|
643
|
+
let last;
|
|
644
|
+
return (...args) => {
|
|
645
|
+
if (++count < n) last = fn(...args);
|
|
646
|
+
return last;
|
|
647
|
+
};
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
/**
|
|
651
|
+
* Does nothing. Useful as a default callback.
|
|
652
|
+
*
|
|
653
|
+
* @param {...unknown} _args
|
|
654
|
+
* @returns {void}
|
|
655
|
+
*/
|
|
656
|
+
function noop(..._args) {}
|
|
657
|
+
|
|
658
|
+
/**
|
|
659
|
+
* Returns its argument.
|
|
660
|
+
*
|
|
661
|
+
* @example
|
|
662
|
+
* [0, 1, 2].filter(func.identity); // [1, 2]
|
|
663
|
+
*
|
|
664
|
+
* @template T
|
|
665
|
+
* @param {T} value
|
|
666
|
+
* @returns {T}
|
|
667
|
+
*/
|
|
668
|
+
function identity(value) {
|
|
669
|
+
return value;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
module.exports = {
|
|
673
|
+
debounce,
|
|
674
|
+
throttle,
|
|
675
|
+
once,
|
|
676
|
+
memoize,
|
|
677
|
+
retry,
|
|
678
|
+
timeout,
|
|
679
|
+
delay,
|
|
680
|
+
to,
|
|
681
|
+
attempt,
|
|
682
|
+
promisify,
|
|
683
|
+
pipe,
|
|
684
|
+
compose,
|
|
685
|
+
curry,
|
|
686
|
+
partial,
|
|
687
|
+
negate,
|
|
688
|
+
after,
|
|
689
|
+
before,
|
|
690
|
+
noop,
|
|
691
|
+
identity,
|
|
692
|
+
};
|