@ix-xs/node-comfort 1.2.0 → 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/README.md +136 -549
- package/index.js +80 -35
- package/package.json +160 -33
- package/src/Arr.js +887 -419
- package/src/Async.js +585 -0
- package/src/Cache.js +329 -0
- package/src/Checker.js +971 -539
- 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 -894
- package/src/Func.js +670 -353
- package/src/Http.js +538 -0
- package/src/Id.js +384 -201
- package/src/Logger.js +739 -378
- package/src/Num.js +844 -361
- package/src/Obj.js +855 -372
- package/src/SQLite.js +1308 -1005
- package/src/Schema.js +1586 -0
- package/src/Str.js +983 -436
- package/src/Sys.js +511 -0
- package/src/Time.js +1260 -422
- package/src/Utils.js +321 -325
- package/src/errors.js +292 -0
- package/types/index.d.ts +693 -526
- package/types/src/Arr.d.ts +455 -145
- package/types/src/Arr.d.ts.map +1 -1
- 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 +501 -235
- 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 +686 -238
- package/types/src/FS.d.ts.map +1 -1
- package/types/src/Func.d.ts +380 -135
- package/types/src/Func.d.ts.map +1 -1
- package/types/src/Http.d.ts +374 -0
- package/types/src/Http.d.ts.map +1 -0
- package/types/src/Id.d.ts +149 -68
- package/types/src/Id.d.ts.map +1 -1
- package/types/src/Logger.d.ts +363 -46
- package/types/src/Logger.d.ts.map +1 -1
- package/types/src/Num.d.ts +473 -137
- package/types/src/Num.d.ts.map +1 -1
- package/types/src/Obj.d.ts +390 -104
- package/types/src/Obj.d.ts.map +1 -1
- package/types/src/SQLite.d.ts +646 -309
- 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 +569 -166
- package/types/src/Str.d.ts.map +1 -1
- package/types/src/Sys.d.ts +269 -0
- package/types/src/Sys.d.ts.map +1 -0
- package/types/src/Time.d.ts +663 -204
- package/types/src/Time.d.ts.map +1 -1
- 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/types/index.d.ts.map +0 -1
package/src/Func.js
CHANGED
|
@@ -1,375 +1,692 @@
|
|
|
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));
|
|
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.
|
|
7
69
|
*/
|
|
8
70
|
|
|
9
71
|
/**
|
|
10
|
-
* Sleeps for a number of milliseconds.
|
|
11
|
-
* @private
|
|
12
72
|
* @param {number} ms
|
|
73
|
+
* @param {AbortSignal} [signal]
|
|
13
74
|
* @returns {Promise<void>}
|
|
14
75
|
*/
|
|
15
|
-
const
|
|
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
|
+
});
|
|
16
89
|
|
|
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
|
-
if (timer) clearTimeout(timer);
|
|
47
|
-
timer = setTimeout(() => {
|
|
48
|
-
timer = null;
|
|
49
|
-
if (!leading) fn.apply(lastThis, lastArgs);
|
|
50
|
-
}, wait);
|
|
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;
|
|
51
118
|
|
|
52
|
-
|
|
53
|
-
|
|
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
|
+
};
|
|
54
132
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
+
}
|
|
67
165
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
};
|
|
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;
|
|
112
189
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
+
}
|
|
118
220
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
let result;
|
|
136
|
-
return /** @type {any} */ (function(...args) {
|
|
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) {
|
|
137
237
|
if (!called) {
|
|
138
238
|
called = true;
|
|
139
239
|
result = fn.apply(this, args);
|
|
140
240
|
}
|
|
141
241
|
return result;
|
|
142
|
-
});
|
|
143
|
-
},
|
|
144
|
-
|
|
145
|
-
/**
|
|
146
|
-
* Memoizes a function, caching results by argument key.
|
|
147
|
-
*
|
|
148
|
-
* By default the cache key is a JSON serialization of the arguments; pass a
|
|
149
|
-
* `resolver` for custom keys.
|
|
150
|
-
*
|
|
151
|
-
* @example
|
|
152
|
-
* const fib = nodeComfort.func.memoize((n) => (n < 2 ? n : fib(n - 1) + fib(n - 2)));
|
|
153
|
-
*
|
|
154
|
-
* @template {(...args: any[]) => any} F
|
|
155
|
-
* @param {F} fn - Function to memoize.
|
|
156
|
-
* @param {(...args: any[]) => any} [resolver] - Cache-key resolver.
|
|
157
|
-
* @returns {F & { cache: Map<any, any> }} The memoized function.
|
|
158
|
-
*/
|
|
159
|
-
memoize(fn, resolver) {
|
|
160
|
-
const cache = new Map();
|
|
161
|
-
const memoized = function(...args) {
|
|
162
|
-
const key = resolver ? resolver.apply(this, args) : JSON.stringify(args);
|
|
163
|
-
if (cache.has(key)) return cache.get(key);
|
|
164
|
-
const result = fn.apply(this, args);
|
|
165
|
-
cache.set(key, result);
|
|
166
|
-
return result;
|
|
167
|
-
};
|
|
168
|
-
memoized.cache = cache;
|
|
169
|
-
return /** @type {any} */ (memoized);
|
|
170
|
-
},
|
|
171
|
-
|
|
172
|
-
/**
|
|
173
|
-
* Retries an async function until it succeeds or the attempt budget runs out.
|
|
174
|
-
*
|
|
175
|
-
* Supports fixed or exponential backoff and a `shouldRetry` guard.
|
|
176
|
-
*
|
|
177
|
-
* @example
|
|
178
|
-
* const data = await nodeComfort.func.retry(() => fetch(url), {
|
|
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);
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
242
|
}
|
|
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
|
-
* Composes functions right-to-left: `compose(f, g)(x)` is `f(g(x))`.
|
|
265
|
-
*
|
|
266
|
-
* @example
|
|
267
|
-
* const process = nodeComfort.func.compose(
|
|
268
|
-
* (s) => s.toUpperCase(),
|
|
269
|
-
* (s) => s.trim(),
|
|
270
|
-
* );
|
|
271
|
-
* process(" hi "); // "HI"
|
|
272
|
-
*
|
|
273
|
-
* @param {...((arg: any) => any)} fns - Functions to compose.
|
|
274
|
-
* @returns {(...args: any[]) => any} The composed function.
|
|
275
|
-
*/
|
|
276
|
-
compose(...fns) {
|
|
277
|
-
return this.pipe(...fns.reverse());
|
|
278
|
-
},
|
|
279
|
-
|
|
280
|
-
/**
|
|
281
|
-
* Curries a function so it can be called with arguments one (or a few) at a time.
|
|
282
|
-
*
|
|
283
|
-
* @example
|
|
284
|
-
* const add = nodeComfort.func.curry((a, b, c) => a + b + c);
|
|
285
|
-
* add(1)(2)(3); // 6
|
|
286
|
-
* add(1, 2)(3); // 6
|
|
287
|
-
*
|
|
288
|
-
* @param {(...args: any[]) => any} fn - Function to curry.
|
|
289
|
-
* @param {number} [arity=fn.length] - Number of arguments to wait for.
|
|
290
|
-
* @returns {(...args: any[]) => any} The curried function.
|
|
291
|
-
*/
|
|
292
|
-
curry(fn, arity = fn.length) {
|
|
293
|
-
return function curried(...args) {
|
|
294
|
-
if (args.length >= arity) return fn.apply(this, args);
|
|
295
|
-
return (...next) => curried.apply(this, [...args, ...next]);
|
|
296
|
-
};
|
|
297
|
-
},
|
|
298
|
-
|
|
299
|
-
/**
|
|
300
|
-
* Returns a function that negates the result of `predicate`.
|
|
301
|
-
*
|
|
302
|
-
* @example
|
|
303
|
-
* const isOdd = nodeComfort.func.negate((n) => n % 2 === 0);
|
|
304
|
-
*
|
|
305
|
-
* @param {(...args: any[]) => boolean} predicate - Predicate to negate.
|
|
306
|
-
* @returns {(...args: any[]) => boolean} The negated predicate.
|
|
307
|
-
*/
|
|
308
|
-
negate(predicate) {
|
|
309
|
-
return function(...args) {
|
|
310
|
-
return !predicate.apply(this, args);
|
|
311
|
-
};
|
|
312
|
-
},
|
|
313
|
-
|
|
314
|
-
/**
|
|
315
|
-
* Wraps a function so that it is only actually invoked from the `n`-th call on.
|
|
316
|
-
*
|
|
317
|
-
* @example
|
|
318
|
-
* const save = nodeComfort.func.after(2, () => persist());
|
|
319
|
-
* save(); save(); // persists on the 2nd call
|
|
320
|
-
*
|
|
321
|
-
* @template {(...args: any[]) => any} F
|
|
322
|
-
* @param {number} n - Number of calls before `fn` fires.
|
|
323
|
-
* @param {F} fn - Function to wrap.
|
|
324
|
-
* @returns {F} The wrapped function.
|
|
325
|
-
*/
|
|
326
|
-
after(n, fn) {
|
|
327
|
-
let count = 0;
|
|
328
|
-
return /** @type {any} */ (function(...args) {
|
|
329
|
-
count++;
|
|
330
|
-
if (count >= n) return fn.apply(this, args);
|
|
331
|
-
});
|
|
332
|
-
},
|
|
333
|
-
|
|
334
|
-
/**
|
|
335
|
-
* Turns a Node-style callback function (`(err, result) => ...`) into one that
|
|
336
|
-
* returns a Promise.
|
|
337
|
-
*
|
|
338
|
-
* @example
|
|
339
|
-
* const readFile = nodeComfort.func.promisify(fs.readFile);
|
|
340
|
-
* const data = await readFile("a.txt", "utf8");
|
|
341
|
-
*
|
|
342
|
-
* @param {(...args: any[]) => void} fn - Callback-style function.
|
|
343
|
-
* @returns {(...args: any[]) => Promise<any>} A promise-returning function.
|
|
344
|
-
*/
|
|
345
|
-
promisify(fn) {
|
|
346
|
-
return function(...args) {
|
|
347
|
-
return new Promise((resolve, reject) => {
|
|
348
|
-
fn.call(this, ...args, (err, result) => (err ? reject(err) : resolve(result)));
|
|
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);
|
|
349
287
|
});
|
|
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
|
-
|
|
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,
|
|
375
692
|
};
|