@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.
Files changed (77) hide show
  1. package/CHANGELOG.md +190 -0
  2. package/LICENSE +21 -0
  3. package/README.md +210 -748
  4. package/index.js +84 -13
  5. package/package.json +201 -21
  6. package/src/Arr.js +901 -0
  7. package/src/Async.js +585 -0
  8. package/src/Cache.js +329 -0
  9. package/src/Checker.js +979 -188
  10. package/src/Cli.js +1034 -0
  11. package/src/Color.js +415 -0
  12. package/src/Crypto.js +707 -0
  13. package/src/Emitter.js +289 -0
  14. package/src/Env.js +555 -0
  15. package/src/FS.js +1483 -768
  16. package/src/Func.js +692 -0
  17. package/src/Http.js +538 -0
  18. package/src/Id.js +418 -0
  19. package/src/Logger.js +739 -307
  20. package/src/Num.js +849 -0
  21. package/src/Obj.js +904 -0
  22. package/src/SQLite.js +1308 -994
  23. package/src/Schema.js +1586 -0
  24. package/src/Str.js +999 -0
  25. package/src/Sys.js +511 -0
  26. package/src/Time.js +1323 -0
  27. package/src/Utils.js +321 -325
  28. package/src/errors.js +292 -0
  29. package/types/index.d.ts +693 -120
  30. package/types/src/Arr.d.ts +562 -0
  31. package/types/src/Arr.d.ts.map +1 -0
  32. package/types/src/Async.d.ts +321 -0
  33. package/types/src/Async.d.ts.map +1 -0
  34. package/types/src/Cache.d.ts +195 -0
  35. package/types/src/Cache.d.ts.map +1 -0
  36. package/types/src/Checker.d.ts +609 -81
  37. package/types/src/Checker.d.ts.map +1 -1
  38. package/types/src/Cli.d.ts +582 -0
  39. package/types/src/Cli.d.ts.map +1 -0
  40. package/types/src/Color.d.ts +294 -0
  41. package/types/src/Color.d.ts.map +1 -0
  42. package/types/src/Crypto.d.ts +534 -0
  43. package/types/src/Crypto.d.ts.map +1 -0
  44. package/types/src/Emitter.d.ts +170 -0
  45. package/types/src/Emitter.d.ts.map +1 -0
  46. package/types/src/Env.d.ts +382 -0
  47. package/types/src/Env.d.ts.map +1 -0
  48. package/types/src/FS.d.ts +709 -201
  49. package/types/src/FS.d.ts.map +1 -1
  50. package/types/src/Func.d.ts +455 -0
  51. package/types/src/Func.d.ts.map +1 -0
  52. package/types/src/Http.d.ts +374 -0
  53. package/types/src/Http.d.ts.map +1 -0
  54. package/types/src/Id.d.ts +200 -0
  55. package/types/src/Id.d.ts.map +1 -0
  56. package/types/src/Logger.d.ts +374 -24
  57. package/types/src/Logger.d.ts.map +1 -1
  58. package/types/src/Num.d.ts +578 -0
  59. package/types/src/Num.d.ts.map +1 -0
  60. package/types/src/Obj.d.ts +475 -0
  61. package/types/src/Obj.d.ts.map +1 -0
  62. package/types/src/SQLite.d.ts +647 -305
  63. package/types/src/SQLite.d.ts.map +1 -1
  64. package/types/src/Schema.d.ts +940 -0
  65. package/types/src/Schema.d.ts.map +1 -0
  66. package/types/src/Str.d.ts +696 -0
  67. package/types/src/Str.d.ts.map +1 -0
  68. package/types/src/Sys.d.ts +269 -0
  69. package/types/src/Sys.d.ts.map +1 -0
  70. package/types/src/Time.d.ts +664 -0
  71. package/types/src/Time.d.ts.map +1 -0
  72. package/types/src/Utils.d.ts +154 -102
  73. package/types/src/Utils.d.ts.map +1 -1
  74. package/types/src/errors.d.ts +292 -0
  75. package/types/src/errors.d.ts.map +1 -0
  76. package/tsconfig.json +0 -18
  77. 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
+ };