@ix-xs/node-comfort 1.2.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/CHANGELOG.md +340 -0
  2. package/README.md +137 -548
  3. package/index.js +80 -35
  4. package/package.json +161 -33
  5. package/src/Arr.js +1050 -418
  6. package/src/Async.js +817 -0
  7. package/src/Cache.js +433 -0
  8. package/src/Checker.js +1202 -539
  9. package/src/Cli.js +1293 -0
  10. package/src/Color.js +556 -0
  11. package/src/Crypto.js +1082 -0
  12. package/src/Emitter.js +356 -0
  13. package/src/Env.js +689 -0
  14. package/src/FS.js +1626 -894
  15. package/src/Func.js +823 -356
  16. package/src/Http.js +700 -0
  17. package/src/Id.js +568 -200
  18. package/src/Logger.js +963 -378
  19. package/src/Num.js +965 -360
  20. package/src/Obj.js +1077 -379
  21. package/src/SQLite.js +1629 -1005
  22. package/src/Schema.js +2101 -0
  23. package/src/Str.js +1209 -432
  24. package/src/Sys.js +654 -0
  25. package/src/Time.js +1533 -440
  26. package/src/Utils.js +342 -325
  27. package/src/errors.js +335 -0
  28. package/src/internal/shared.js +415 -0
  29. package/types/index.d.ts +735 -526
  30. package/types/src/Arr.d.ts +533 -145
  31. package/types/src/Arr.d.ts.map +1 -1
  32. package/types/src/Async.d.ts +372 -0
  33. package/types/src/Async.d.ts.map +1 -0
  34. package/types/src/Cache.d.ts +221 -0
  35. package/types/src/Cache.d.ts.map +1 -0
  36. package/types/src/Checker.d.ts +651 -232
  37. package/types/src/Checker.d.ts.map +1 -1
  38. package/types/src/Cli.d.ts +664 -0
  39. package/types/src/Cli.d.ts.map +1 -0
  40. package/types/src/Color.d.ts +300 -0
  41. package/types/src/Color.d.ts.map +1 -0
  42. package/types/src/Crypto.d.ts +620 -0
  43. package/types/src/Crypto.d.ts.map +1 -0
  44. package/types/src/Emitter.d.ts +189 -0
  45. package/types/src/Emitter.d.ts.map +1 -0
  46. package/types/src/Env.d.ts +412 -0
  47. package/types/src/Env.d.ts.map +1 -0
  48. package/types/src/FS.d.ts +717 -238
  49. package/types/src/FS.d.ts.map +1 -1
  50. package/types/src/Func.d.ts +508 -135
  51. package/types/src/Func.d.ts.map +1 -1
  52. package/types/src/Http.d.ts +444 -0
  53. package/types/src/Http.d.ts.map +1 -0
  54. package/types/src/Id.d.ts +199 -66
  55. package/types/src/Id.d.ts.map +1 -1
  56. package/types/src/Logger.d.ts +414 -46
  57. package/types/src/Logger.d.ts.map +1 -1
  58. package/types/src/Num.d.ts +514 -137
  59. package/types/src/Num.d.ts.map +1 -1
  60. package/types/src/Obj.d.ts +453 -104
  61. package/types/src/Obj.d.ts.map +1 -1
  62. package/types/src/SQLite.d.ts +754 -308
  63. package/types/src/SQLite.d.ts.map +1 -1
  64. package/types/src/Schema.d.ts +1189 -0
  65. package/types/src/Schema.d.ts.map +1 -0
  66. package/types/src/Str.d.ts +617 -166
  67. package/types/src/Str.d.ts.map +1 -1
  68. package/types/src/Sys.d.ts +314 -0
  69. package/types/src/Sys.d.ts.map +1 -0
  70. package/types/src/Time.d.ts +759 -204
  71. package/types/src/Time.d.ts.map +1 -1
  72. package/types/src/Utils.d.ts +158 -102
  73. package/types/src/Utils.d.ts.map +1 -1
  74. package/types/src/errors.d.ts +316 -0
  75. package/types/src/errors.d.ts.map +1 -0
  76. package/types/src/internal/shared.d.ts +123 -0
  77. package/types/src/internal/shared.d.ts.map +1 -0
  78. package/types/index.d.ts.map +0 -1
@@ -0,0 +1,415 @@
1
+ "use strict";
2
+
3
+ // Helpers shared by the modules. Not part of the public API.
4
+
5
+ const { types } = require("node:util");
6
+
7
+ /** Keys that must never be copied onto an object: they reach its prototype. */
8
+ const UNSAFE_KEYS = new Set(["__proto__", "constructor", "prototype"]);
9
+
10
+ /**
11
+ * @param {unknown} value
12
+ * @returns {value is Record<string, any>}
13
+ */
14
+ const isPlainObject = (value) => {
15
+ if (typeof value !== "object" || value === null) return false;
16
+ const proto = Object.getPrototypeOf(value);
17
+ // The last check accepts objects from another realm (a worker, a `vm`).
18
+ return proto === null || proto === Object.prototype || Object.getPrototypeOf(proto) === null;
19
+ };
20
+
21
+ const MS = { ms: 1, s: 1000, m: 60_000, h: 3_600_000, d: 86_400_000, w: 604_800_000 };
22
+
23
+ /**
24
+ * The defaults set with `time.setLocale()` and `time.setTimezone()`. English
25
+ * by default, so output never depends on the machine.
26
+ * @type {{ locale: string, timeZone: string | undefined }}
27
+ */
28
+ const timeConfig = { locale: "en", timeZone: undefined };
29
+
30
+ /** @type {Record<string, keyof typeof MS | "M" | "y">} */
31
+ const UNIT_ALIASES = {
32
+ ms: "ms", msec: "ms", milli: "ms", millis: "ms", millisecond: "ms", milliseconds: "ms",
33
+ s: "s", sec: "s", secs: "s", second: "s", seconds: "s",
34
+ m: "m", min: "m", mins: "m", minute: "m", minutes: "m",
35
+ h: "h", hr: "h", hrs: "h", hour: "h", hours: "h",
36
+ d: "d", day: "d", days: "d",
37
+ w: "w", wk: "w", wks: "w", week: "w", weeks: "w",
38
+ M: "M", mo: "M", month: "M", months: "M",
39
+ y: "y", yr: "y", yrs: "y", year: "y", years: "y",
40
+ };
41
+
42
+ /**
43
+ * @param {string} unit
44
+ * @returns {keyof typeof MS | "M" | "y" | undefined}
45
+ */
46
+ const unitOf = (unit) => UNIT_ALIASES[unit] ?? UNIT_ALIASES[unit.toLowerCase()];
47
+
48
+ /** @type {Map<string, number | null>} */
49
+ const _durations = new Map();
50
+
51
+ /**
52
+ * @param {string} text Already trimmed, not empty.
53
+ * @returns {number | null}
54
+ */
55
+ const _readDuration = (text) => {
56
+ if (/^[-+]?\d+(?:\.\d+)?$/.test(text)) return Number(text);
57
+
58
+ const iso = /^([-+])?P(?:(\d+(?:[.,]\d+)?)W)?(?:(\d+(?:[.,]\d+)?)D)?(?:T(?:(\d+(?:[.,]\d+)?)H)?(?:(\d+(?:[.,]\d+)?)M)?(?:(\d+(?:[.,]\d+)?)S)?)?$/i.exec(text);
59
+ if (iso && /\d/.test(text)) {
60
+ const [, sign, w, d, h, m, s] = iso;
61
+ const n = (/** @type {string | undefined} */ v) => (v ? Number(v.replace(",", ".")) : 0);
62
+ const total = n(w) * MS.w + n(d) * MS.d + n(h) * MS.h + n(m) * MS.m + n(s) * MS.s;
63
+ return sign === "-" ? -total : total;
64
+ }
65
+
66
+ const clock = /^([-+])?(?:(\d+):)?(\d+):(\d{1,2})(?:\.(\d{1,3}))?$/.exec(text);
67
+ if (clock) {
68
+ const [, sign, h, m, s, ms] = clock;
69
+ const total = Number(h ?? 0) * MS.h + Number(m) * MS.m + Number(s) * MS.s + (ms ? Number(ms.padEnd(3, "0")) : 0);
70
+ return sign === "-" ? -total : total;
71
+ }
72
+
73
+ const regex = /(-?\d*\.?\d+)\s*([a-zA-Z]+)/g;
74
+ let total = 0;
75
+ let consumed = "";
76
+ let match;
77
+ while ((match = regex.exec(text)) !== null) {
78
+ const unit = unitOf(match[2]);
79
+ if (!unit || unit === "M" || unit === "y") return null;
80
+ total += Number(match[1]) * MS[unit];
81
+ consumed += match[0];
82
+ }
83
+ if (!consumed || consumed.replace(/\s+/g, "") !== text.replace(/[\s,]+|and/g, "")) return null;
84
+ return total;
85
+ };
86
+
87
+ /**
88
+ * A duration in milliseconds, or `null` when the input isn't one.
89
+ * @param {unknown} input
90
+ * @returns {number | null}
91
+ */
92
+ const parseDuration = (input) => {
93
+ if (typeof input === "number") return Number.isFinite(input) ? input : null;
94
+ if (typeof input !== "string") return null;
95
+ const cached = _durations.get(input);
96
+ if (cached !== undefined) return cached;
97
+ const text = input.trim();
98
+ const result = text ? _readDuration(text) : null;
99
+ // Durations are almost always literals, so a handful of entries covers a whole program.
100
+ if (_durations.size >= 500) _durations.clear();
101
+ _durations.set(input, result);
102
+ return result;
103
+ };
104
+
105
+ /**
106
+ * Milliseconds from a number or a duration string.
107
+ * @param {number | string} value
108
+ * @param {string} [what="duration"] Names the value in the error message.
109
+ * @returns {number}
110
+ * @throws {TypeError} If it can't be read.
111
+ */
112
+ const toMs = (value, what = "duration") => {
113
+ if (typeof value === "number") {
114
+ if (Number.isNaN(value)) throw new TypeError(`Invalid ${what}: NaN`);
115
+ return value;
116
+ }
117
+ const ms = parseDuration(value);
118
+ if (ms === null) throw new TypeError(`Invalid ${what}: ${JSON.stringify(value) ?? String(value)}`);
119
+ return ms;
120
+ };
121
+
122
+ const MAX_TIMEOUT = 2 ** 31 - 1;
123
+
124
+ /**
125
+ * `setTimeout` that also works past its 24.8-day limit. `Infinity` never fires.
126
+ * @param {() => void} fn
127
+ * @param {number} ms
128
+ * @param {boolean} [unref=false]
129
+ * @returns {() => void} Cancels the timer.
130
+ */
131
+ const setLongTimeout = (fn, ms, unref = false) => {
132
+ if (ms === Infinity) return () => {};
133
+ /** @type {NodeJS.Timeout | undefined} */
134
+ let timer;
135
+ const schedule = (/** @type {number} */ remaining) => {
136
+ timer = setTimeout(() => (remaining > MAX_TIMEOUT ? schedule(remaining - MAX_TIMEOUT) : fn()), Math.min(remaining, MAX_TIMEOUT));
137
+ if (unref) timer.unref();
138
+ };
139
+ schedule(Math.max(0, ms) || 0);
140
+ return () => clearTimeout(timer);
141
+ };
142
+
143
+ /**
144
+ * Resolves after `ms`, or rejects with an `AbortError` when the signal fires.
145
+ * @param {number} ms
146
+ * @param {AbortSignal} [signal]
147
+ * @param {boolean} [unref=false]
148
+ * @returns {Promise<void>}
149
+ */
150
+ const sleep = (ms, signal, unref = false) =>
151
+ new Promise((resolve, reject) => {
152
+ const { AbortError } = require("../errors.js");
153
+ if (signal?.aborted) return reject(new AbortError(undefined, { cause: signal.reason }));
154
+ const onAbort = () => {
155
+ cancel();
156
+ reject(new AbortError(undefined, { cause: signal?.reason }));
157
+ };
158
+ const cancel = setLongTimeout(() => {
159
+ signal?.removeEventListener("abort", onAbort);
160
+ resolve();
161
+ }, ms, unref);
162
+ signal?.addEventListener("abort", onAbort, { once: true });
163
+ });
164
+
165
+ /**
166
+ * Builds a cache of `Intl` objects, which are slow to construct (tens of
167
+ * microseconds) and fast to use.
168
+ * @template T
169
+ * @param {(locale: string | undefined, options: any) => T} create
170
+ * @returns {(locale?: string, options?: object) => T}
171
+ */
172
+ const _intlCache = (create) => {
173
+ /** @type {Map<string, T>} */
174
+ const cache = new Map();
175
+ return (locale, options) => {
176
+ const key = options === undefined ? `${locale ?? ""}` : `${locale ?? ""}|${JSON.stringify(options)}`;
177
+ let found = cache.get(key);
178
+ if (found === undefined) {
179
+ found = create(locale, options);
180
+ if (cache.size >= 200) cache.clear();
181
+ cache.set(key, found);
182
+ }
183
+ return found;
184
+ };
185
+ };
186
+
187
+ const intl = {
188
+ number: _intlCache((locale, options) => new Intl.NumberFormat(locale, options)),
189
+ dateTime: _intlCache((locale, options) => new Intl.DateTimeFormat(locale, options)),
190
+ plural: _intlCache((locale, options) => new Intl.PluralRules(locale, options)),
191
+ collator: _intlCache((locale, options) => new Intl.Collator(locale, options)),
192
+ relativeTime: _intlCache((locale, options) => new Intl.RelativeTimeFormat(locale, options)),
193
+ list: _intlCache((locale, options) => new Intl.ListFormat(locale, options)),
194
+ };
195
+
196
+ /** @type {Intl.Segmenter | undefined} */
197
+ let _segmenter;
198
+
199
+ /** The grapheme segmenter, built on first use. */
200
+ const segmenter = () => (_segmenter ??= new Intl.Segmenter(undefined, { granularity: "grapheme" }));
201
+
202
+ const ASCII = /^[\x00-\x7f]*$/;
203
+
204
+ /**
205
+ * Splits text into graphemes, so emoji and accents stay whole.
206
+ * @param {string} input
207
+ * @returns {string[]}
208
+ */
209
+ const graphemes = (input) => {
210
+ if (ASCII.test(input)) return input.split("");
211
+ return Array.from(segmenter().segment(input), (s) => s.segment);
212
+ };
213
+
214
+ /**
215
+ * Counts graphemes without building an array.
216
+ * @param {string} input
217
+ * @returns {number}
218
+ */
219
+ const graphemeCount = (input) => {
220
+ if (ASCII.test(input)) return input.length;
221
+ let count = 0;
222
+ for (const _ of segmenter().segment(input)) count++;
223
+ return count;
224
+ };
225
+
226
+ /**
227
+ * Adds numbers with Neumaier's compensation, so `0.1 + 0.2 + 0.3` is `0.6`.
228
+ * @param {Iterable<number>} values
229
+ * @returns {number}
230
+ */
231
+ const preciseSum = (values) => {
232
+ let sum = 0;
233
+ let compensation = 0;
234
+ for (const value of values) {
235
+ const next = sum + value;
236
+ compensation += Math.abs(sum) >= Math.abs(value) ? sum - next + value : value - next + sum;
237
+ sum = next;
238
+ }
239
+ const total = sum + compensation;
240
+ return Number.isNaN(total) ? sum : total;
241
+ };
242
+
243
+ /** @type {WeakMap<object, number>} */
244
+ const _objectIds = new WeakMap();
245
+ /** @type {Map<symbol, number>} */
246
+ const _symbolIds = new Map();
247
+ let _nextId = 0;
248
+
249
+ /** @param {object} value */
250
+ const _identity = (value) => {
251
+ let id = _objectIds.get(value);
252
+ if (id === undefined) _objectIds.set(value, (id = ++_nextId));
253
+ return id;
254
+ };
255
+
256
+ /**
257
+ * @param {unknown} value
258
+ * @returns {string}
259
+ */
260
+ const _keyPart = (value) => {
261
+ switch (typeof value) {
262
+ case "string": return `s${value.length}:${value}`;
263
+ case "number": return Object.is(value, -0) ? "n-0" : `n${value}`;
264
+ case "boolean": return value ? "t" : "f";
265
+ case "undefined": return "u";
266
+ case "bigint": return `i${value}`;
267
+ case "symbol": {
268
+ let id = _symbolIds.get(value);
269
+ if (id === undefined) _symbolIds.set(value, (id = ++_nextId));
270
+ return `y${id}`;
271
+ }
272
+ case "function": return `F${_identity(value)}`;
273
+ default: {
274
+ if (value === null) return "l";
275
+ const object = /** @type {object} */ (value);
276
+ if (types.isDate(object)) return `d${object.getTime()}`;
277
+ if (!Array.isArray(object) && !isPlainObject(object)) return `O${_identity(object)}`;
278
+ try {
279
+ // The replacer keeps what JSON drops or refuses: undefined, bigint, functions, symbols.
280
+ return `j${JSON.stringify(object, (_, v) => (v !== null && typeof v === "object" ? v : typeof v === "string" || typeof v === "number" || typeof v === "boolean" ? v : _keyPart(v)))}`;
281
+ } catch {
282
+ return `O${_identity(object)}`;
283
+ }
284
+ }
285
+ }
286
+ };
287
+
288
+ /**
289
+ * A cache key for a list of arguments. Primitives are compared by value and
290
+ * type, plain objects and arrays by content, everything else by identity.
291
+ * @param {readonly unknown[]} args
292
+ * @returns {unknown} A primitive usable as a `Map` key.
293
+ */
294
+ const argsKey = (args) => {
295
+ if (args.length === 1) {
296
+ const only = args[0];
297
+ // Numbers and booleans can't clash with the tagged strings used for everything else.
298
+ return typeof only === "number" || typeof only === "boolean" ? only : _keyPart(only);
299
+ }
300
+ if (args.length === 0) return "";
301
+ let key = "";
302
+ for (let i = 0; i < args.length; i++) key += `${_keyPart(args[i])}\u0001`;
303
+ return key;
304
+ };
305
+
306
+ /**
307
+ * Parsed cron fields. `lastDay` is set by `L` in the day-of-month field.
308
+ * @typedef {{ seconds: Set<number>, minutes: Set<number>, hours: Set<number>, days: Set<number>, months: Set<number>, weekdays: Set<number>, anyDay: boolean, anyWeekday: boolean, lastDay: boolean }} CronSpec
309
+ */
310
+
311
+ /** @type {Record<string, string>} */
312
+ const CRON_MACROS = {
313
+ "@yearly": "0 0 1 1 *", "@annually": "0 0 1 1 *", "@monthly": "0 0 1 * *", "@weekly": "0 0 * * 0",
314
+ "@daily": "0 0 * * *", "@midnight": "0 0 * * *", "@hourly": "0 * * * *",
315
+ };
316
+ const MONTH_NAMES = ["jan", "feb", "mar", "apr", "may", "jun", "jul", "aug", "sep", "oct", "nov", "dec"];
317
+ const DAY_NAMES = ["sun", "mon", "tue", "wed", "thu", "fri", "sat"];
318
+
319
+ /** @type {Map<string, CronSpec>} */
320
+ const _cronSpecs = new Map();
321
+
322
+ /**
323
+ * Parses a cron expression with 5 fields, or 6 with seconds first.
324
+ * @param {string} expression
325
+ * @returns {CronSpec}
326
+ * @throws {SyntaxError} If the expression is invalid.
327
+ */
328
+ const parseCron = (expression) => {
329
+ const cached = _cronSpecs.get(expression);
330
+ if (cached) return cached;
331
+ const text = CRON_MACROS[expression.trim().toLowerCase()] ?? expression.trim();
332
+ const fields = text.split(/\s+/);
333
+ if (fields.length !== 5 && fields.length !== 6) throw new SyntaxError(`Invalid cron expression "${expression}": expected 5 or 6 fields`);
334
+ if (fields.length === 5) fields.unshift("0");
335
+ /** @type {(field: string, min: number, max: number, names?: string[]) => Set<number>} */
336
+ const parse = (field, min, max, names) => {
337
+ /** @type {Set<number>} */
338
+ const values = new Set();
339
+ for (const part of field.split(",")) {
340
+ const [rangePart, stepPart] = part.split("/");
341
+ const step = stepPart === undefined ? 1 : Number(stepPart);
342
+ if (!Number.isInteger(step) || step < 1) throw new SyntaxError(`Invalid step "${part}" in cron expression "${expression}"`);
343
+ const toNumber = (/** @type {string} */ token) => {
344
+ const index = names ? names.indexOf(token.toLowerCase()) : -1;
345
+ const n = index >= 0 ? index + (min === 1 ? 1 : 0) : token === "" ? NaN : Number(token);
346
+ if (!Number.isInteger(n)) throw new SyntaxError(`Invalid value "${token}" in cron expression "${expression}"`);
347
+ return n;
348
+ };
349
+ let from = min;
350
+ let to = max;
351
+ if (rangePart !== "*" && rangePart !== "?") {
352
+ const [a, b] = rangePart.split("-");
353
+ from = toNumber(a);
354
+ to = b === undefined ? (stepPart === undefined ? from : max) : toNumber(b);
355
+ }
356
+ if (from < min || to > max || from > to) throw new SyntaxError(`Value out of range in "${part}" (allowed ${min}-${max}) in cron expression "${expression}"`);
357
+ for (let v = from; v <= to; v += step) values.add(v);
358
+ }
359
+ return values;
360
+ };
361
+ const weekdays = parse(fields[5], 0, 7, DAY_NAMES);
362
+ if (weekdays.has(7)) {
363
+ weekdays.delete(7);
364
+ weekdays.add(0);
365
+ }
366
+ // "L" alone, or in a list like "15,L", means the last day of the month.
367
+ const dayParts = fields[3].split(",");
368
+ const lastDay = dayParts.some((part) => part.toUpperCase() === "L");
369
+ const dayField = dayParts.filter((part) => part.toUpperCase() !== "L").join(",");
370
+ /** @type {CronSpec} */
371
+ const spec = {
372
+ seconds: parse(fields[0], 0, 59),
373
+ minutes: parse(fields[1], 0, 59),
374
+ hours: parse(fields[2], 0, 23),
375
+ days: dayField === "" ? new Set() : parse(dayField, 1, 31),
376
+ months: parse(fields[4], 1, 12, MONTH_NAMES),
377
+ weekdays,
378
+ anyDay: fields[3] === "*" || fields[3] === "?",
379
+ anyWeekday: fields[5] === "*" || fields[5] === "?",
380
+ lastDay,
381
+ };
382
+ if (_cronSpecs.size >= 200) _cronSpecs.clear();
383
+ _cronSpecs.set(expression, spec);
384
+ return spec;
385
+ };
386
+
387
+ /**
388
+ * Blocks the thread for a few milliseconds. Only for short retries of
389
+ * synchronous file operations.
390
+ * @param {number} ms
391
+ */
392
+ const sleepSync = (ms) => {
393
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
394
+ };
395
+
396
+ module.exports = {
397
+ UNSAFE_KEYS,
398
+ MS,
399
+ timeConfig,
400
+ ASCII,
401
+ isPlainObject,
402
+ unitOf,
403
+ parseDuration,
404
+ toMs,
405
+ setLongTimeout,
406
+ sleep,
407
+ sleepSync,
408
+ intl,
409
+ segmenter,
410
+ graphemes,
411
+ graphemeCount,
412
+ preciseSum,
413
+ argsKey,
414
+ parseCron,
415
+ };