@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.
Files changed (78) hide show
  1. package/CHANGELOG.md +340 -0
  2. package/README.md +137 -548
  3. package/index.js +80 -35
  4. package/package.json +161 -33
  5. package/src/Arr.js +1050 -418
  6. package/src/Async.js +817 -0
  7. package/src/Cache.js +433 -0
  8. package/src/Checker.js +1202 -539
  9. package/src/Cli.js +1293 -0
  10. package/src/Color.js +556 -0
  11. package/src/Crypto.js +1082 -0
  12. package/src/Emitter.js +356 -0
  13. package/src/Env.js +689 -0
  14. package/src/FS.js +1626 -894
  15. package/src/Func.js +823 -356
  16. package/src/Http.js +700 -0
  17. package/src/Id.js +568 -200
  18. package/src/Logger.js +963 -378
  19. package/src/Num.js +965 -360
  20. package/src/Obj.js +1077 -379
  21. package/src/SQLite.js +1629 -1005
  22. package/src/Schema.js +2101 -0
  23. package/src/Str.js +1209 -432
  24. package/src/Sys.js +654 -0
  25. package/src/Time.js +1533 -440
  26. package/src/Utils.js +342 -325
  27. package/src/errors.js +335 -0
  28. package/src/internal/shared.js +415 -0
  29. package/types/index.d.ts +735 -526
  30. package/types/src/Arr.d.ts +533 -145
  31. package/types/src/Arr.d.ts.map +1 -1
  32. package/types/src/Async.d.ts +372 -0
  33. package/types/src/Async.d.ts.map +1 -0
  34. package/types/src/Cache.d.ts +221 -0
  35. package/types/src/Cache.d.ts.map +1 -0
  36. package/types/src/Checker.d.ts +651 -232
  37. package/types/src/Checker.d.ts.map +1 -1
  38. package/types/src/Cli.d.ts +664 -0
  39. package/types/src/Cli.d.ts.map +1 -0
  40. package/types/src/Color.d.ts +300 -0
  41. package/types/src/Color.d.ts.map +1 -0
  42. package/types/src/Crypto.d.ts +620 -0
  43. package/types/src/Crypto.d.ts.map +1 -0
  44. package/types/src/Emitter.d.ts +189 -0
  45. package/types/src/Emitter.d.ts.map +1 -0
  46. package/types/src/Env.d.ts +412 -0
  47. package/types/src/Env.d.ts.map +1 -0
  48. package/types/src/FS.d.ts +717 -238
  49. package/types/src/FS.d.ts.map +1 -1
  50. package/types/src/Func.d.ts +508 -135
  51. package/types/src/Func.d.ts.map +1 -1
  52. package/types/src/Http.d.ts +444 -0
  53. package/types/src/Http.d.ts.map +1 -0
  54. package/types/src/Id.d.ts +199 -66
  55. package/types/src/Id.d.ts.map +1 -1
  56. package/types/src/Logger.d.ts +414 -46
  57. package/types/src/Logger.d.ts.map +1 -1
  58. package/types/src/Num.d.ts +514 -137
  59. package/types/src/Num.d.ts.map +1 -1
  60. package/types/src/Obj.d.ts +453 -104
  61. package/types/src/Obj.d.ts.map +1 -1
  62. package/types/src/SQLite.d.ts +754 -308
  63. package/types/src/SQLite.d.ts.map +1 -1
  64. package/types/src/Schema.d.ts +1189 -0
  65. package/types/src/Schema.d.ts.map +1 -0
  66. package/types/src/Str.d.ts +617 -166
  67. package/types/src/Str.d.ts.map +1 -1
  68. package/types/src/Sys.d.ts +314 -0
  69. package/types/src/Sys.d.ts.map +1 -0
  70. package/types/src/Time.d.ts +759 -204
  71. package/types/src/Time.d.ts.map +1 -1
  72. package/types/src/Utils.d.ts +158 -102
  73. package/types/src/Utils.d.ts.map +1 -1
  74. package/types/src/errors.d.ts +316 -0
  75. package/types/src/errors.d.ts.map +1 -0
  76. package/types/src/internal/shared.d.ts +123 -0
  77. package/types/src/internal/shared.d.ts.map +1 -0
  78. 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 & control-flow utilities.
4
+ * Function helpers: debounce, throttle, memoize, retry with backoff,
5
+ * timeouts, error handling without try/catch, and typed composition.
3
6
  *
4
- * The higher-order helpers you reach for constantly: debounce, throttle,
5
- * memoize, once, retry with backoff, timeout, and functional composition.
6
- * All zero-dependency and Promise-friendly.
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
- * Sleeps for a number of milliseconds.
11
- * @private
12
- * @param {number} ms
13
- * @returns {Promise<void>}
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
- module.exports = {
18
- /**
19
- * Creates a debounced version of a function that delays invocation until
20
- * `wait` ms have elapsed since the last call.
21
- *
22
- * The returned function exposes `.cancel()` and `.flush()`.
23
- *
24
- * @example
25
- * const onResize = nodeComfort.func.debounce(() => render(), 200);
26
- * window.on("resize", onResize);
27
- * onResize.cancel(); // drop any pending call
28
- *
29
- * @template {(...args: any[]) => any} F
30
- * @param {F} fn - Function to debounce.
31
- * @param {number} wait - Delay in milliseconds.
32
- * @param {{ leading?: boolean }} [options] - Set `leading` to fire on the first call.
33
- * @returns {F & { cancel: () => void, flush: () => void }} The debounced function.
34
- */
35
- debounce(fn, wait, options = {}) {
36
- let timer = null;
37
- let lastArgs = null;
38
- let lastThis = null;
39
- const leading = options.leading ?? false;
40
-
41
- const debounced = function(...args) {
42
- lastArgs = args;
43
- lastThis = this;
44
- const callNow = leading && !timer;
45
-
46
- if (timer) clearTimeout(timer);
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
- timer = null;
49
- if (!leading) fn.apply(lastThis, lastArgs);
50
- }, wait);
51
-
52
- if (callNow) fn.apply(lastThis, lastArgs);
53
- };
54
-
55
- debounced.cancel = () => {
56
- if (timer) clearTimeout(timer);
57
- timer = null;
58
- lastArgs = lastThis = null;
59
- };
60
- debounced.flush = () => {
61
- if (timer && lastArgs) {
62
- clearTimeout(timer);
63
- timer = null;
64
- fn.apply(lastThis, lastArgs);
65
- }
66
- };
67
-
68
- return /** @type {any} */ (debounced);
69
- },
70
-
71
- /**
72
- * Creates a throttled version of a function that runs at most once per
73
- * `wait` ms.
74
- *
75
- * @example
76
- * const onScroll = nodeComfort.func.throttle(() => update(), 100);
77
- *
78
- * @template {(...args: any[]) => any} F
79
- * @param {F} fn - Function to throttle.
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
- * 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);
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
- throw lastError;
219
- },
220
-
221
- /**
222
- * Runs a promise (or async function) with a timeout, rejecting if it takes
223
- * too long.
224
- *
225
- * @example
226
- * await nodeComfort.func.timeout(fetch(url), 5000, "fetch too slow");
227
- *
228
- * @template T
229
- * @param {Promise<T>|(() => Promise<T>)} promise - Promise or async function.
230
- * @param {number} ms - Timeout in milliseconds.
231
- * @param {string} [message="Operation timed out"] - Error message on timeout.
232
- * @returns {Promise<T>} The resolved value.
233
- * @throws {Error} If the timeout elapses first.
234
- */
235
- timeout(promise, ms, message = "Operation timed out") {
236
- const target = typeof promise === "function" ? promise() : promise;
237
- let timer;
238
- const timeoutPromise = new Promise((_, reject) => {
239
- timer = setTimeout(() => reject(new Error(message)), ms);
240
- });
241
- return Promise.race([target, timeoutPromise]).finally(() => clearTimeout(timer));
242
- },
243
-
244
- /**
245
- * Composes functions left-to-right: `pipe(f, g)(x)` is `g(f(x))`.
246
- *
247
- * The first function may take multiple arguments; the rest take one.
248
- *
249
- * @example
250
- * const process = nodeComfort.func.pipe(
251
- * (s) => s.trim(),
252
- * (s) => s.toUpperCase(),
253
- * );
254
- * process(" hi "); // "HI"
255
- *
256
- * @param {...((arg: any) => any)} fns - Functions to compose.
257
- * @returns {(...args: any[]) => any} The composed function.
258
- */
259
- pipe(...fns) {
260
- return (...args) => fns.reduce((acc, fn, i) => (i === 0 ? fn(...args) : fn(acc)), undefined);
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);
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
- * 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)));
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
- * Wraps an async function so it resolves to a `[error, result]` tuple instead
355
- * of throwing • Go-style error handling.
356
- *
357
- * @example
358
- * const [err, user] = await nodeComfort.func.attempt(() => getUser(id))();
359
- * if (err) return handle(err);
360
- *
361
- * @template T
362
- * @param {(...args: any[]) => Promise<T>|T} fn - Function to wrap.
363
- * @returns {(...args: any[]) => Promise<[unknown, T|undefined]>} The safe function.
364
- */
365
- attempt(fn) {
366
- return async function(...args) {
367
- try {
368
- const result = await fn.apply(this, args);
369
- return [null, result];
370
- } catch (error) {
371
- return [error, undefined];
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
  };