@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.
Files changed (75) hide show
  1. package/CHANGELOG.md +190 -0
  2. package/README.md +136 -549
  3. package/index.js +80 -35
  4. package/package.json +160 -33
  5. package/src/Arr.js +887 -419
  6. package/src/Async.js +585 -0
  7. package/src/Cache.js +329 -0
  8. package/src/Checker.js +971 -539
  9. package/src/Cli.js +1034 -0
  10. package/src/Color.js +415 -0
  11. package/src/Crypto.js +707 -0
  12. package/src/Emitter.js +289 -0
  13. package/src/Env.js +555 -0
  14. package/src/FS.js +1483 -894
  15. package/src/Func.js +670 -353
  16. package/src/Http.js +538 -0
  17. package/src/Id.js +384 -201
  18. package/src/Logger.js +739 -378
  19. package/src/Num.js +844 -361
  20. package/src/Obj.js +855 -372
  21. package/src/SQLite.js +1308 -1005
  22. package/src/Schema.js +1586 -0
  23. package/src/Str.js +983 -436
  24. package/src/Sys.js +511 -0
  25. package/src/Time.js +1260 -422
  26. package/src/Utils.js +321 -325
  27. package/src/errors.js +292 -0
  28. package/types/index.d.ts +693 -526
  29. package/types/src/Arr.d.ts +455 -145
  30. package/types/src/Arr.d.ts.map +1 -1
  31. package/types/src/Async.d.ts +321 -0
  32. package/types/src/Async.d.ts.map +1 -0
  33. package/types/src/Cache.d.ts +195 -0
  34. package/types/src/Cache.d.ts.map +1 -0
  35. package/types/src/Checker.d.ts +501 -235
  36. package/types/src/Checker.d.ts.map +1 -1
  37. package/types/src/Cli.d.ts +582 -0
  38. package/types/src/Cli.d.ts.map +1 -0
  39. package/types/src/Color.d.ts +294 -0
  40. package/types/src/Color.d.ts.map +1 -0
  41. package/types/src/Crypto.d.ts +534 -0
  42. package/types/src/Crypto.d.ts.map +1 -0
  43. package/types/src/Emitter.d.ts +170 -0
  44. package/types/src/Emitter.d.ts.map +1 -0
  45. package/types/src/Env.d.ts +382 -0
  46. package/types/src/Env.d.ts.map +1 -0
  47. package/types/src/FS.d.ts +686 -238
  48. package/types/src/FS.d.ts.map +1 -1
  49. package/types/src/Func.d.ts +380 -135
  50. package/types/src/Func.d.ts.map +1 -1
  51. package/types/src/Http.d.ts +374 -0
  52. package/types/src/Http.d.ts.map +1 -0
  53. package/types/src/Id.d.ts +149 -68
  54. package/types/src/Id.d.ts.map +1 -1
  55. package/types/src/Logger.d.ts +363 -46
  56. package/types/src/Logger.d.ts.map +1 -1
  57. package/types/src/Num.d.ts +473 -137
  58. package/types/src/Num.d.ts.map +1 -1
  59. package/types/src/Obj.d.ts +390 -104
  60. package/types/src/Obj.d.ts.map +1 -1
  61. package/types/src/SQLite.d.ts +646 -309
  62. package/types/src/SQLite.d.ts.map +1 -1
  63. package/types/src/Schema.d.ts +940 -0
  64. package/types/src/Schema.d.ts.map +1 -0
  65. package/types/src/Str.d.ts +569 -166
  66. package/types/src/Str.d.ts.map +1 -1
  67. package/types/src/Sys.d.ts +269 -0
  68. package/types/src/Sys.d.ts.map +1 -0
  69. package/types/src/Time.d.ts +663 -204
  70. package/types/src/Time.d.ts.map +1 -1
  71. package/types/src/Utils.d.ts +154 -102
  72. package/types/src/Utils.d.ts.map +1 -1
  73. package/types/src/errors.d.ts +292 -0
  74. package/types/src/errors.d.ts.map +1 -0
  75. 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 & 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));
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 _wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
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
- 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);
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
- if (callNow) fn.apply(lastThis, lastArgs);
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
- 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
- };
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
- 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
- };
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
- throttled.cancel = () => {
114
- if (timer) clearTimeout(timer);
115
- timer = null;
116
- last = 0;
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
- 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) {
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
- 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);
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
- * 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
- },
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
  };