@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.
- package/CHANGELOG.md +340 -0
- package/README.md +137 -548
- package/index.js +80 -35
- package/package.json +161 -33
- package/src/Arr.js +1050 -418
- package/src/Async.js +817 -0
- package/src/Cache.js +433 -0
- package/src/Checker.js +1202 -539
- package/src/Cli.js +1293 -0
- package/src/Color.js +556 -0
- package/src/Crypto.js +1082 -0
- package/src/Emitter.js +356 -0
- package/src/Env.js +689 -0
- package/src/FS.js +1626 -894
- package/src/Func.js +823 -356
- package/src/Http.js +700 -0
- package/src/Id.js +568 -200
- package/src/Logger.js +963 -378
- package/src/Num.js +965 -360
- package/src/Obj.js +1077 -379
- package/src/SQLite.js +1629 -1005
- package/src/Schema.js +2101 -0
- package/src/Str.js +1209 -432
- package/src/Sys.js +654 -0
- package/src/Time.js +1533 -440
- package/src/Utils.js +342 -325
- package/src/errors.js +335 -0
- package/src/internal/shared.js +415 -0
- package/types/index.d.ts +735 -526
- package/types/src/Arr.d.ts +533 -145
- package/types/src/Arr.d.ts.map +1 -1
- package/types/src/Async.d.ts +372 -0
- package/types/src/Async.d.ts.map +1 -0
- package/types/src/Cache.d.ts +221 -0
- package/types/src/Cache.d.ts.map +1 -0
- package/types/src/Checker.d.ts +651 -232
- package/types/src/Checker.d.ts.map +1 -1
- package/types/src/Cli.d.ts +664 -0
- package/types/src/Cli.d.ts.map +1 -0
- package/types/src/Color.d.ts +300 -0
- package/types/src/Color.d.ts.map +1 -0
- package/types/src/Crypto.d.ts +620 -0
- package/types/src/Crypto.d.ts.map +1 -0
- package/types/src/Emitter.d.ts +189 -0
- package/types/src/Emitter.d.ts.map +1 -0
- package/types/src/Env.d.ts +412 -0
- package/types/src/Env.d.ts.map +1 -0
- package/types/src/FS.d.ts +717 -238
- package/types/src/FS.d.ts.map +1 -1
- package/types/src/Func.d.ts +508 -135
- package/types/src/Func.d.ts.map +1 -1
- package/types/src/Http.d.ts +444 -0
- package/types/src/Http.d.ts.map +1 -0
- package/types/src/Id.d.ts +199 -66
- package/types/src/Id.d.ts.map +1 -1
- package/types/src/Logger.d.ts +414 -46
- package/types/src/Logger.d.ts.map +1 -1
- package/types/src/Num.d.ts +514 -137
- package/types/src/Num.d.ts.map +1 -1
- package/types/src/Obj.d.ts +453 -104
- package/types/src/Obj.d.ts.map +1 -1
- package/types/src/SQLite.d.ts +754 -308
- package/types/src/SQLite.d.ts.map +1 -1
- package/types/src/Schema.d.ts +1189 -0
- package/types/src/Schema.d.ts.map +1 -0
- package/types/src/Str.d.ts +617 -166
- package/types/src/Str.d.ts.map +1 -1
- package/types/src/Sys.d.ts +314 -0
- package/types/src/Sys.d.ts.map +1 -0
- package/types/src/Time.d.ts +759 -204
- package/types/src/Time.d.ts.map +1 -1
- package/types/src/Utils.d.ts +158 -102
- package/types/src/Utils.d.ts.map +1 -1
- package/types/src/errors.d.ts +316 -0
- package/types/src/errors.d.ts.map +1 -0
- package/types/src/internal/shared.d.ts +123 -0
- package/types/src/internal/shared.d.ts.map +1 -0
- 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
|
+
};
|