@ix-xs/node-comfort 1.1.31 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +623 -748
- package/index.js +39 -13
- package/package.json +74 -21
- package/src/Arr.js +433 -0
- package/src/Checker.js +547 -188
- package/src/FS.js +127 -1
- package/src/Func.js +375 -0
- package/src/Id.js +235 -0
- package/src/Logger.js +71 -0
- package/src/Num.js +366 -0
- package/src/Obj.js +421 -0
- package/src/SQLite.js +20 -9
- package/src/Str.js +452 -0
- package/src/Time.js +485 -0
- package/types/index.d.ts +406 -0
- package/types/index.d.ts.map +1 -1
- package/types/src/Arr.d.ts +252 -0
- package/types/src/Arr.d.ts.map +1 -0
- package/types/src/Checker.d.ts +292 -30
- package/types/src/Checker.d.ts.map +1 -1
- package/types/src/FS.d.ts +60 -0
- package/types/src/FS.d.ts.map +1 -1
- package/types/src/Func.d.ts +210 -0
- package/types/src/Func.d.ts.map +1 -0
- package/types/src/Id.d.ts +119 -0
- package/types/src/Id.d.ts.map +1 -0
- package/types/src/Logger.d.ts +33 -0
- package/types/src/Logger.d.ts.map +1 -1
- package/types/src/Num.d.ts +242 -0
- package/types/src/Num.d.ts.map +1 -0
- package/types/src/Obj.d.ts +189 -0
- package/types/src/Obj.d.ts.map +1 -0
- package/types/src/SQLite.d.ts +6 -1
- package/types/src/SQLite.d.ts.map +1 -1
- package/types/src/Str.d.ts +293 -0
- package/types/src/Str.d.ts.map +1 -0
- package/types/src/Time.d.ts +205 -0
- package/types/src/Time.d.ts.map +1 -0
- package/tsconfig.json +0 -18
package/src/FS.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
const { readdirSync, existsSync, mkdirSync, rmSync, writeFileSync, unlinkSync, copyFileSync, readFileSync, watch } = require("node:fs");
|
|
1
|
+
const { readdirSync, existsSync, mkdirSync, rmSync, writeFileSync, unlinkSync, copyFileSync, readFileSync, appendFileSync, statSync, watch } = require("node:fs");
|
|
2
2
|
const { resolve, dirname, sep, isAbsolute, join, relative, basename } = require("node:path");
|
|
3
|
+
const { createHash } = require("node:crypto");
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Normalizes a path to the current platform separator.
|
|
@@ -655,6 +656,131 @@ module.exports = {
|
|
|
655
656
|
}
|
|
656
657
|
},
|
|
657
658
|
|
|
659
|
+
/**
|
|
660
|
+
* Reads and parses a JSON file.
|
|
661
|
+
* @template T
|
|
662
|
+
* @param {string} path - The JSON file path.
|
|
663
|
+
* @param {T} [fallback] - Value returned when the file is missing or invalid.
|
|
664
|
+
* @returns {T|any|undefined} The parsed value, or the fallback.
|
|
665
|
+
*/
|
|
666
|
+
readJSON(path, fallback) {
|
|
667
|
+
const content = this.readFile(path);
|
|
668
|
+
if (content === undefined) return fallback;
|
|
669
|
+
try {
|
|
670
|
+
return JSON.parse(content);
|
|
671
|
+
} catch {
|
|
672
|
+
return fallback;
|
|
673
|
+
}
|
|
674
|
+
},
|
|
675
|
+
|
|
676
|
+
/**
|
|
677
|
+
* Serializes a value to JSON and writes it to a file (creating folders as needed).
|
|
678
|
+
* @param {string} path - The destination file path.
|
|
679
|
+
* @param {*} data - The value to serialize.
|
|
680
|
+
* @param {{ spaces?: number, force?: boolean }} [options] - Formatting options (`spaces` default 2).
|
|
681
|
+
* @returns {boolean} True on success, false on failure.
|
|
682
|
+
*/
|
|
683
|
+
writeJSON(path, data, options = {}) {
|
|
684
|
+
const spaces = options.spaces ?? 2;
|
|
685
|
+
try {
|
|
686
|
+
const target = this.createPath(path);
|
|
687
|
+
if (!this.getFolder(dirname(target))) this.createFolder(dirname(target));
|
|
688
|
+
writeFileSync(target, JSON.stringify(data, null, spaces), "utf8");
|
|
689
|
+
return true;
|
|
690
|
+
} catch {
|
|
691
|
+
return false;
|
|
692
|
+
}
|
|
693
|
+
},
|
|
694
|
+
|
|
695
|
+
/**
|
|
696
|
+
* Appends text to a file, creating it (and its folders) if necessary.
|
|
697
|
+
* @param {string} path - The file path.
|
|
698
|
+
* @param {string} data - The text to append.
|
|
699
|
+
* @returns {boolean} True on success, false on failure.
|
|
700
|
+
*/
|
|
701
|
+
appendFile(path, data) {
|
|
702
|
+
try {
|
|
703
|
+
const target = _target(path, "file") ?? this.createPath(path);
|
|
704
|
+
if (!this.getFolder(dirname(target))) this.createFolder(dirname(target));
|
|
705
|
+
appendFileSync(target, typeof data === "string" ? data : String(data), "utf8");
|
|
706
|
+
return true;
|
|
707
|
+
} catch {
|
|
708
|
+
return false;
|
|
709
|
+
}
|
|
710
|
+
},
|
|
711
|
+
|
|
712
|
+
/**
|
|
713
|
+
* Checks whether a path (file or folder) exists.
|
|
714
|
+
* @param {string} path - The path to check.
|
|
715
|
+
* @returns {boolean} True if the path exists.
|
|
716
|
+
*/
|
|
717
|
+
exists(path) {
|
|
718
|
+
if (!path) return false;
|
|
719
|
+
const direct = this.createPath(path);
|
|
720
|
+
if (existsSync(direct)) return true;
|
|
721
|
+
return Boolean(_target(path, "file") || _target(path, "folder"));
|
|
722
|
+
},
|
|
723
|
+
|
|
724
|
+
/**
|
|
725
|
+
* Returns filesystem stats for a path, or undefined if it does not exist.
|
|
726
|
+
* @param {string} path - The path to stat.
|
|
727
|
+
* @returns {import("node:fs").Stats|undefined} The stats object, or undefined.
|
|
728
|
+
*/
|
|
729
|
+
stat(path) {
|
|
730
|
+
const target = _target(path, "file") ?? _target(path, "folder");
|
|
731
|
+
if (!target) return undefined;
|
|
732
|
+
try {
|
|
733
|
+
return statSync(target);
|
|
734
|
+
} catch {
|
|
735
|
+
return undefined;
|
|
736
|
+
}
|
|
737
|
+
},
|
|
738
|
+
|
|
739
|
+
/**
|
|
740
|
+
* Returns the size of a file in bytes.
|
|
741
|
+
* @param {string} path - The file path.
|
|
742
|
+
* @returns {number|undefined} Size in bytes, or undefined if not found.
|
|
743
|
+
*/
|
|
744
|
+
fileSize(path) {
|
|
745
|
+
const stats = this.stat(path);
|
|
746
|
+
return stats ? stats.size : undefined;
|
|
747
|
+
},
|
|
748
|
+
|
|
749
|
+
/**
|
|
750
|
+
* Computes a hash digest of a file's contents.
|
|
751
|
+
* @param {string} path - The file path.
|
|
752
|
+
* @param {{ algorithm?: string, encoding?: "hex"|"base64"|"base64url" }} [options] - Hash options.
|
|
753
|
+
* @returns {string|undefined} The digest, or undefined if the file cannot be read.
|
|
754
|
+
*/
|
|
755
|
+
hashFile(path, options = {}) {
|
|
756
|
+
const target = _target(path, "file");
|
|
757
|
+
if (!target) return undefined;
|
|
758
|
+
try {
|
|
759
|
+
const buffer = readFileSync(target);
|
|
760
|
+
return createHash(options.algorithm ?? "sha256").update(buffer).digest(options.encoding ?? "hex");
|
|
761
|
+
} catch {
|
|
762
|
+
return undefined;
|
|
763
|
+
}
|
|
764
|
+
},
|
|
765
|
+
|
|
766
|
+
/**
|
|
767
|
+
* Empties a folder's contents while keeping the folder itself.
|
|
768
|
+
* @param {string} path - The folder to empty.
|
|
769
|
+
* @returns {boolean|undefined} True on success, false on failure, undefined if not found.
|
|
770
|
+
*/
|
|
771
|
+
emptyFolder(path) {
|
|
772
|
+
const target = _target(path);
|
|
773
|
+
if (!target) return undefined;
|
|
774
|
+
try {
|
|
775
|
+
for (const entry of readdirSync(target)) {
|
|
776
|
+
rmSync(resolve(target, entry), { recursive: true, force: true });
|
|
777
|
+
}
|
|
778
|
+
return true;
|
|
779
|
+
} catch {
|
|
780
|
+
return false;
|
|
781
|
+
}
|
|
782
|
+
},
|
|
783
|
+
|
|
658
784
|
/**
|
|
659
785
|
* Watches a path and exposes event listeners.
|
|
660
786
|
* @param {Object} [options] - Watch options.
|
package/src/Func.js
ADDED
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Function & control-flow utilities.
|
|
3
|
+
*
|
|
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
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Sleeps for a number of milliseconds.
|
|
11
|
+
* @private
|
|
12
|
+
* @param {number} ms
|
|
13
|
+
* @returns {Promise<void>}
|
|
14
|
+
*/
|
|
15
|
+
const _wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
16
|
+
|
|
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);
|
|
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) {
|
|
137
|
+
if (!called) {
|
|
138
|
+
called = true;
|
|
139
|
+
result = fn.apply(this, args);
|
|
140
|
+
}
|
|
141
|
+
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
|
+
}
|
|
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)));
|
|
349
|
+
});
|
|
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
|
+
},
|
|
375
|
+
};
|