@ix-xs/node-comfort 2.0.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.
- package/CHANGELOG.md +150 -0
- package/README.md +32 -30
- package/index.js +3 -3
- package/package.json +8 -7
- package/src/Arr.js +222 -58
- package/src/Async.js +322 -90
- package/src/Cache.js +148 -44
- package/src/Checker.js +257 -26
- package/src/Cli.js +404 -145
- package/src/Color.js +224 -83
- package/src/Crypto.js +481 -106
- package/src/Emitter.js +104 -37
- package/src/Env.js +176 -42
- package/src/FS.js +203 -60
- package/src/Func.js +226 -76
- package/src/Http.js +311 -149
- package/src/Id.js +281 -96
- package/src/Logger.js +302 -78
- package/src/Num.js +182 -60
- package/src/Obj.js +340 -125
- package/src/SQLite.js +426 -105
- package/src/Schema.js +602 -87
- package/src/Str.js +316 -86
- package/src/Sys.js +197 -54
- package/src/Time.js +572 -317
- package/src/Utils.js +62 -41
- package/src/errors.js +54 -11
- package/src/internal/shared.js +415 -0
- package/types/index.d.ts +59 -17
- package/types/src/Arr.d.ts +78 -0
- package/types/src/Arr.d.ts.map +1 -1
- package/types/src/Async.d.ts +52 -1
- package/types/src/Async.d.ts.map +1 -1
- package/types/src/Cache.d.ts +33 -7
- package/types/src/Cache.d.ts.map +1 -1
- package/types/src/Checker.d.ts +155 -2
- package/types/src/Checker.d.ts.map +1 -1
- package/types/src/Cli.d.ts +97 -15
- package/types/src/Cli.d.ts.map +1 -1
- package/types/src/Color.d.ts +7 -1
- package/types/src/Color.d.ts.map +1 -1
- package/types/src/Crypto.d.ts +99 -13
- package/types/src/Crypto.d.ts.map +1 -1
- package/types/src/Emitter.d.ts +36 -17
- package/types/src/Emitter.d.ts.map +1 -1
- package/types/src/Env.d.ts +31 -1
- package/types/src/Env.d.ts.map +1 -1
- package/types/src/FS.d.ts +36 -5
- package/types/src/FS.d.ts.map +1 -1
- package/types/src/Func.d.ts +147 -19
- package/types/src/Func.d.ts.map +1 -1
- package/types/src/Http.d.ts +113 -43
- package/types/src/Http.d.ts.map +1 -1
- package/types/src/Id.d.ts +60 -8
- package/types/src/Id.d.ts.map +1 -1
- package/types/src/Logger.d.ts +53 -2
- package/types/src/Logger.d.ts.map +1 -1
- package/types/src/Num.d.ts +48 -7
- package/types/src/Num.d.ts.map +1 -1
- package/types/src/Obj.d.ts +67 -4
- package/types/src/Obj.d.ts.map +1 -1
- package/types/src/SQLite.d.ts +123 -14
- package/types/src/SQLite.d.ts.map +1 -1
- package/types/src/Schema.d.ts +256 -7
- package/types/src/Schema.d.ts.map +1 -1
- package/types/src/Str.d.ts +49 -1
- package/types/src/Str.d.ts.map +1 -1
- package/types/src/Sys.d.ts +51 -6
- package/types/src/Sys.d.ts.map +1 -1
- package/types/src/Time.d.ts +130 -34
- package/types/src/Time.d.ts.map +1 -1
- package/types/src/Utils.d.ts +15 -11
- package/types/src/Utils.d.ts.map +1 -1
- package/types/src/errors.d.ts +27 -3
- package/types/src/errors.d.ts.map +1 -1
- package/types/src/internal/shared.d.ts +123 -0
- package/types/src/internal/shared.d.ts.map +1 -0
package/src/Utils.js
CHANGED
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
* const settings = nc.JSONParse(text, {});
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
const { AbortError } = require("./errors.js");
|
|
13
12
|
const { types } = require("node:util");
|
|
13
|
+
const { sleep, toMs } = require("./internal/shared.js");
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
16
|
* Options for `wait()`.
|
|
@@ -22,8 +22,8 @@ const { types } = require("node:util");
|
|
|
22
22
|
/**
|
|
23
23
|
* Options for `when()`.
|
|
24
24
|
* @typedef {object} WhenOptions
|
|
25
|
-
* @property {number} [interval]
|
|
26
|
-
* @property {number | null} [timeout] Give up after this
|
|
25
|
+
* @property {number | string} [interval] Time between two checks, in ms or like `"1s"`. Defaults to `50`.
|
|
26
|
+
* @property {number | string | null} [timeout] Give up after this long, in ms or like `"10s"`, and emit `"timeout"`.
|
|
27
27
|
* @property {number | null} [max] Stop after this many triggers.
|
|
28
28
|
*/
|
|
29
29
|
|
|
@@ -62,22 +62,14 @@ const { types } = require("node:util");
|
|
|
62
62
|
* @throws {TypeError} If the duration can't be parsed.
|
|
63
63
|
*/
|
|
64
64
|
function wait(duration, options = {}) {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
};
|
|
74
|
-
const timer = setTimeout(() => {
|
|
75
|
-
signal?.removeEventListener("abort", onAbort);
|
|
76
|
-
resolve();
|
|
77
|
-
}, Math.max(0, ms));
|
|
78
|
-
if (options.unref) timer.unref();
|
|
79
|
-
signal?.addEventListener("abort", onAbort, { once: true });
|
|
80
|
-
});
|
|
65
|
+
let ms;
|
|
66
|
+
try {
|
|
67
|
+
ms = toMs(duration);
|
|
68
|
+
} catch (error) {
|
|
69
|
+
return Promise.reject(error);
|
|
70
|
+
}
|
|
71
|
+
if (!Number.isFinite(ms)) return Promise.reject(new TypeError(`Invalid duration: ${String(duration)}`));
|
|
72
|
+
return sleep(ms, options.signal, options.unref ?? false);
|
|
81
73
|
}
|
|
82
74
|
|
|
83
75
|
/**
|
|
@@ -86,7 +78,7 @@ function wait(duration, options = {}) {
|
|
|
86
78
|
* await a condition, `nc.async.poll()` is simpler.
|
|
87
79
|
*
|
|
88
80
|
* @example
|
|
89
|
-
* nc.when(() => queue.length > 0, { reason: "jobs waiting" }, { interval: 500, max: 1, timeout:
|
|
81
|
+
* nc.when(() => queue.length > 0, { reason: "jobs waiting" }, { interval: 500, max: 1, timeout: "10s" })
|
|
90
82
|
* .on("trigger", (payload) => nc.info(payload.reason))
|
|
91
83
|
* .on("timeout", () => nc.warn("Nothing happened in 10s"))
|
|
92
84
|
* .start();
|
|
@@ -97,8 +89,8 @@ function wait(duration, options = {}) {
|
|
|
97
89
|
* @returns {WhenTask}
|
|
98
90
|
*/
|
|
99
91
|
function when(predicate, payload = {}, options = {}) {
|
|
100
|
-
const interval = options.interval
|
|
101
|
-
const timeout = options.timeout
|
|
92
|
+
const interval = options.interval === undefined ? 50 : toMs(options.interval, "interval");
|
|
93
|
+
const timeout = options.timeout === undefined || options.timeout === null ? null : toMs(options.timeout, "timeout");
|
|
102
94
|
const max = options.max ?? null;
|
|
103
95
|
/** @type {Record<"trigger" | "error" | "timeout", Set<Function>>} */
|
|
104
96
|
const listeners = { trigger: new Set(), error: new Set(), timeout: new Set() };
|
|
@@ -258,12 +250,13 @@ let _dontCrashDispose;
|
|
|
258
250
|
|
|
259
251
|
/**
|
|
260
252
|
* `JSON.stringify` that never throws. Circular references become
|
|
261
|
-
* `"[Circular]"`, bigints become strings, Maps become objects
|
|
262
|
-
* arrays.
|
|
253
|
+
* `"[Circular]"`, bigints become strings, Maps become objects, Sets become
|
|
254
|
+
* arrays, and errors keep their name, message and stack.
|
|
263
255
|
*
|
|
264
256
|
* @example
|
|
265
257
|
* nc.JSONString({ a: 1 }); // '{\n "a": 1\n}'
|
|
266
258
|
* nc.JSONString({ id: 10n, tags: new Set(["x"]) }, 0); // '{"id":"10","tags":["x"]}'
|
|
259
|
+
* nc.JSONString({ error: new Error("boom") }, 0); // '{"error":{"name":"Error","message":"boom","stack":"..."}}'
|
|
267
260
|
*
|
|
268
261
|
* @param {unknown} value
|
|
269
262
|
* @param {number} [spaces=4] Indentation. Use `0` for a single line.
|
|
@@ -272,41 +265,69 @@ let _dontCrashDispose;
|
|
|
272
265
|
function JSONString(value, spaces = 4) {
|
|
273
266
|
/** @type {Array<[holder: object, original: object]>} ancestors of the value being serialized */
|
|
274
267
|
const stack = [];
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
268
|
+
let text;
|
|
269
|
+
try {
|
|
270
|
+
text = JSON.stringify(
|
|
271
|
+
value,
|
|
272
|
+
/** @this {any} */ function(_, raw) {
|
|
273
|
+
if (typeof raw === "bigint") return raw.toString();
|
|
274
|
+
if (raw === null || typeof raw !== "object") return raw;
|
|
275
|
+
while (stack.length && stack[stack.length - 1][0] !== this) stack.pop();
|
|
276
|
+
for (let i = 0; i < stack.length; i++) if (stack[i][1] === raw) return "[Circular]";
|
|
277
|
+
const v = types.isMap(raw) ? Object.fromEntries(raw) : types.isSet(raw) ? [...raw] : types.isNativeError(raw) ? _plainError(raw) : raw;
|
|
278
|
+
stack.push([v, raw]);
|
|
279
|
+
return v;
|
|
280
|
+
},
|
|
281
|
+
spaces,
|
|
282
|
+
);
|
|
283
|
+
} catch (error) {
|
|
284
|
+
// A getter or a toJSON() threw: describe the value instead of failing.
|
|
285
|
+
try {
|
|
286
|
+
text = JSON.stringify(require("node:util").inspect(value, { depth: 4, breakLength: Infinity }));
|
|
287
|
+
} catch {
|
|
288
|
+
text = JSON.stringify(`[Unserializable: ${error instanceof Error ? error.message : String(error)}]`);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
288
291
|
return text ?? String(text);
|
|
289
292
|
}
|
|
290
293
|
|
|
294
|
+
/**
|
|
295
|
+
* The fields of an error that `JSON.stringify` would drop.
|
|
296
|
+
* @param {Error} error
|
|
297
|
+
* @returns {Record<string, unknown>}
|
|
298
|
+
*/
|
|
299
|
+
const _plainError = (error) => {
|
|
300
|
+
/** @type {Record<string, unknown>} */
|
|
301
|
+
const out = { name: error.name, message: error.message };
|
|
302
|
+
for (const key of Object.keys(error)) out[key] = /** @type {any} */ (error)[key];
|
|
303
|
+
if (error.cause !== undefined) out.cause = error.cause;
|
|
304
|
+
if (error.stack) out.stack = error.stack;
|
|
305
|
+
return out;
|
|
306
|
+
};
|
|
307
|
+
|
|
291
308
|
/**
|
|
292
309
|
* `JSON.parse` with an optional fallback. With a fallback, invalid input
|
|
293
|
-
* returns it instead of throwing.
|
|
310
|
+
* returns it instead of throwing. A leading byte order mark, which Windows
|
|
311
|
+
* editors like to add, is ignored.
|
|
294
312
|
*
|
|
295
313
|
* @example
|
|
296
314
|
* nc.JSONParse('{"a":1}'); // { a: 1 }
|
|
297
315
|
* nc.JSONParse("oops", null); // null
|
|
298
316
|
* nc.JSONParse("oops"); // throws SyntaxError
|
|
317
|
+
* nc.JSONParse(text, {}, (key, value) => (key === "at" ? new Date(value) : value));
|
|
299
318
|
*
|
|
300
319
|
* @template [T=any]
|
|
301
320
|
* @param {string} text
|
|
302
321
|
* @param {T} [fallback] Returned when `text` isn't valid JSON.
|
|
322
|
+
* @param {(this: any, key: string, value: any) => any} [reviver] Passed to `JSON.parse`.
|
|
303
323
|
* @returns {T}
|
|
304
324
|
* @throws {SyntaxError} If `text` is invalid and there's no fallback.
|
|
305
325
|
*/
|
|
306
|
-
function JSONParse(text, fallback) {
|
|
307
|
-
|
|
326
|
+
function JSONParse(text, fallback, reviver) {
|
|
327
|
+
const clean = typeof text === "string" && text.charCodeAt(0) === 0xfeff ? text.slice(1) : text;
|
|
328
|
+
if (arguments.length < 2) return JSON.parse(clean);
|
|
308
329
|
try {
|
|
309
|
-
return JSON.parse(
|
|
330
|
+
return JSON.parse(clean, reviver);
|
|
310
331
|
} catch {
|
|
311
332
|
return /** @type {T} */ (fallback);
|
|
312
333
|
}
|
package/src/errors.js
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* The errors node-comfort throws
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* The errors node-comfort throws when something goes wrong at run time: a
|
|
5
|
+
* request fails, data is invalid, a command exits badly. They all extend
|
|
6
|
+
* `NodeComfortError` (itself a regular `Error`) and carry a stable `code`,
|
|
7
|
+
* so you can branch on the kind of failure without parsing messages.
|
|
8
|
+
*
|
|
9
|
+
* A mistake in your own code, like a wrong argument, still throws a plain
|
|
10
|
+
* `TypeError` or `RangeError`, as it does everywhere in JavaScript.
|
|
7
11
|
*
|
|
8
12
|
* @example
|
|
9
13
|
* try {
|
|
@@ -40,6 +44,40 @@ class NodeComfortError extends Error {
|
|
|
40
44
|
/** Stable, machine-readable code. */
|
|
41
45
|
this.code = options.code ?? "ERR_NODE_COMFORT";
|
|
42
46
|
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* What `JSON.stringify()` writes for the error: its name, code, message
|
|
50
|
+
* and own fields, without the stack. Handy for API responses and logs.
|
|
51
|
+
*
|
|
52
|
+
* @example
|
|
53
|
+
* res.status(400).json({ error });
|
|
54
|
+
* // {"error":{"name":"ValidationError","code":"ERR_VALIDATION","message":"...","issues":[...]}}
|
|
55
|
+
*
|
|
56
|
+
* @returns {Record<string, unknown>}
|
|
57
|
+
*/
|
|
58
|
+
toJSON() {
|
|
59
|
+
/** @type {Record<string, unknown>} */
|
|
60
|
+
const out = { name: this.name, code: this.code, message: this.message };
|
|
61
|
+
for (const key of Object.keys(this)) if (!(key in out)) out[key] = /** @type {any} */ (this)[key];
|
|
62
|
+
const cause = /** @type {any} */ (this.cause);
|
|
63
|
+
if (cause !== undefined) out.cause = cause instanceof Error ? { name: cause.name, message: cause.message, code: /** @type {any} */ (cause).code } : cause;
|
|
64
|
+
return out;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Makes `instanceof` work when two copies of the package are installed,
|
|
69
|
+
* which would otherwise give two unrelated `HttpError` classes.
|
|
70
|
+
*
|
|
71
|
+
* @override
|
|
72
|
+
* @param {unknown} value
|
|
73
|
+
* @returns {boolean}
|
|
74
|
+
*/
|
|
75
|
+
static [Symbol.hasInstance](value) {
|
|
76
|
+
if (Function.prototype[Symbol.hasInstance].call(this, value)) return true;
|
|
77
|
+
const brand = Symbol.for(`@ix-xs/node-comfort/${this.name}`);
|
|
78
|
+
if (!Object.prototype.hasOwnProperty.call(this.prototype, brand)) return false;
|
|
79
|
+
return value !== null && typeof value === "object" && /** @type {any} */ (value)[brand] === true;
|
|
80
|
+
}
|
|
43
81
|
}
|
|
44
82
|
|
|
45
83
|
/**
|
|
@@ -54,7 +92,7 @@ class AssertionError extends NodeComfortError {
|
|
|
54
92
|
* @param {NodeComfortErrorOptions} [options]
|
|
55
93
|
*/
|
|
56
94
|
constructor(message = "Assertion failed", options = {}) {
|
|
57
|
-
super(message, { code: "ERR_ASSERTION"
|
|
95
|
+
super(message, { ...options, code: options.code ?? "ERR_ASSERTION" });
|
|
58
96
|
}
|
|
59
97
|
}
|
|
60
98
|
|
|
@@ -75,7 +113,7 @@ class TimeoutError extends NodeComfortError {
|
|
|
75
113
|
* @param {NodeComfortErrorOptions & { timeout?: number }} [options] Can include the time limit in milliseconds.
|
|
76
114
|
*/
|
|
77
115
|
constructor(message = "Operation timed out", options = {}) {
|
|
78
|
-
super(message, { code: "ETIMEDOUT"
|
|
116
|
+
super(message, { ...options, code: options.code ?? "ETIMEDOUT" });
|
|
79
117
|
/** The time limit in milliseconds, when known. */
|
|
80
118
|
this.timeout = options.timeout;
|
|
81
119
|
}
|
|
@@ -91,7 +129,7 @@ class AbortError extends NodeComfortError {
|
|
|
91
129
|
* @param {NodeComfortErrorOptions} [options] Usually `{ cause: signal.reason }`.
|
|
92
130
|
*/
|
|
93
131
|
constructor(message = "The operation was aborted", options = {}) {
|
|
94
|
-
super(message, { code: "ABORT_ERR"
|
|
132
|
+
super(message, { ...options, code: options.code ?? "ABORT_ERR" });
|
|
95
133
|
}
|
|
96
134
|
}
|
|
97
135
|
|
|
@@ -123,7 +161,7 @@ class ValidationError extends NodeComfortError {
|
|
|
123
161
|
* @param {NodeComfortErrorOptions & { message?: string }} [options]
|
|
124
162
|
*/
|
|
125
163
|
constructor(issues, options = {}) {
|
|
126
|
-
super(options.message ?? ValidationError.format(issues), { code: "ERR_VALIDATION"
|
|
164
|
+
super(options.message ?? ValidationError.format(issues), { ...options, code: options.code ?? "ERR_VALIDATION" });
|
|
127
165
|
/** Every problem found, in order. */
|
|
128
166
|
this.issues = issues;
|
|
129
167
|
}
|
|
@@ -179,7 +217,7 @@ class HttpError extends NodeComfortError {
|
|
|
179
217
|
* @param {NodeComfortErrorOptions & { status?: number, statusText?: string, url?: string, method?: string, headers?: Record<string, string>, data?: unknown }} [options]
|
|
180
218
|
*/
|
|
181
219
|
constructor(message, options = {}) {
|
|
182
|
-
super(message, { code: "ERR_HTTP"
|
|
220
|
+
super(message, { ...options, code: options.code ?? "ERR_HTTP" });
|
|
183
221
|
/** HTTP status, like `404`. `0` when no response came back. */
|
|
184
222
|
this.status = options.status ?? 0;
|
|
185
223
|
/** HTTP status text, like `"Not Found"`. */
|
|
@@ -214,7 +252,7 @@ class JWTError extends NodeComfortError {
|
|
|
214
252
|
* @param {NodeComfortErrorOptions & { expiredAt?: Date }} [options]
|
|
215
253
|
*/
|
|
216
254
|
constructor(message, options = {}) {
|
|
217
|
-
super(message, { code: "ERR_JWT"
|
|
255
|
+
super(message, { ...options, code: options.code ?? "ERR_JWT" });
|
|
218
256
|
/** When the token expired. Only set for `ERR_JWT_EXPIRED`. */
|
|
219
257
|
this.expiredAt = options.expiredAt;
|
|
220
258
|
}
|
|
@@ -237,7 +275,7 @@ class ProcessError extends NodeComfortError {
|
|
|
237
275
|
* @param {NodeComfortErrorOptions & { command?: string, exitCode?: number | null, signal?: string | null, stdout?: string, stderr?: string, timedOut?: boolean }} [options]
|
|
238
276
|
*/
|
|
239
277
|
constructor(message, options = {}) {
|
|
240
|
-
super(message, { code: options.timedOut ? "ETIMEDOUT" : "ERR_PROCESS"
|
|
278
|
+
super(message, { ...options, code: options.code ?? (options.timedOut ? "ETIMEDOUT" : "ERR_PROCESS") });
|
|
241
279
|
/** The command that ran. */
|
|
242
280
|
this.command = options.command ?? "";
|
|
243
281
|
/** Exit code, or `null` if a signal killed the process. */
|
|
@@ -271,7 +309,7 @@ class SQLiteError extends NodeComfortError {
|
|
|
271
309
|
* @param {NodeComfortErrorOptions & { sql?: string, sqliteCode?: string }} [options]
|
|
272
310
|
*/
|
|
273
311
|
constructor(message, options = {}) {
|
|
274
|
-
super(message, { code: "ERR_SQLITE"
|
|
312
|
+
super(message, { ...options, code: options.code ?? "ERR_SQLITE" });
|
|
275
313
|
/** The SQL that failed, when there is one. */
|
|
276
314
|
this.sql = options.sql;
|
|
277
315
|
/** SQLite's error code, like `"SQLITE_CONSTRAINT_UNIQUE"`, when known. */
|
|
@@ -279,6 +317,11 @@ class SQLiteError extends NodeComfortError {
|
|
|
279
317
|
}
|
|
280
318
|
}
|
|
281
319
|
|
|
320
|
+
const _classes = [NodeComfortError, AssertionError, TimeoutError, AbortError, ValidationError, HttpError, JWTError, ProcessError, SQLiteError];
|
|
321
|
+
for (const Class of _classes) {
|
|
322
|
+
Object.defineProperty(Class.prototype, Symbol.for(`@ix-xs/node-comfort/${Class.name}`), { value: true });
|
|
323
|
+
}
|
|
324
|
+
|
|
282
325
|
module.exports = {
|
|
283
326
|
NodeComfortError,
|
|
284
327
|
AssertionError,
|