@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.
Files changed (77) hide show
  1. package/CHANGELOG.md +150 -0
  2. package/README.md +32 -30
  3. package/index.js +3 -3
  4. package/package.json +8 -7
  5. package/src/Arr.js +222 -58
  6. package/src/Async.js +322 -90
  7. package/src/Cache.js +148 -44
  8. package/src/Checker.js +257 -26
  9. package/src/Cli.js +404 -145
  10. package/src/Color.js +224 -83
  11. package/src/Crypto.js +481 -106
  12. package/src/Emitter.js +104 -37
  13. package/src/Env.js +176 -42
  14. package/src/FS.js +203 -60
  15. package/src/Func.js +226 -76
  16. package/src/Http.js +311 -149
  17. package/src/Id.js +281 -96
  18. package/src/Logger.js +302 -78
  19. package/src/Num.js +182 -60
  20. package/src/Obj.js +340 -125
  21. package/src/SQLite.js +426 -105
  22. package/src/Schema.js +602 -87
  23. package/src/Str.js +316 -86
  24. package/src/Sys.js +197 -54
  25. package/src/Time.js +572 -317
  26. package/src/Utils.js +62 -41
  27. package/src/errors.js +54 -11
  28. package/src/internal/shared.js +415 -0
  29. package/types/index.d.ts +59 -17
  30. package/types/src/Arr.d.ts +78 -0
  31. package/types/src/Arr.d.ts.map +1 -1
  32. package/types/src/Async.d.ts +52 -1
  33. package/types/src/Async.d.ts.map +1 -1
  34. package/types/src/Cache.d.ts +33 -7
  35. package/types/src/Cache.d.ts.map +1 -1
  36. package/types/src/Checker.d.ts +155 -2
  37. package/types/src/Checker.d.ts.map +1 -1
  38. package/types/src/Cli.d.ts +97 -15
  39. package/types/src/Cli.d.ts.map +1 -1
  40. package/types/src/Color.d.ts +7 -1
  41. package/types/src/Color.d.ts.map +1 -1
  42. package/types/src/Crypto.d.ts +99 -13
  43. package/types/src/Crypto.d.ts.map +1 -1
  44. package/types/src/Emitter.d.ts +36 -17
  45. package/types/src/Emitter.d.ts.map +1 -1
  46. package/types/src/Env.d.ts +31 -1
  47. package/types/src/Env.d.ts.map +1 -1
  48. package/types/src/FS.d.ts +36 -5
  49. package/types/src/FS.d.ts.map +1 -1
  50. package/types/src/Func.d.ts +147 -19
  51. package/types/src/Func.d.ts.map +1 -1
  52. package/types/src/Http.d.ts +113 -43
  53. package/types/src/Http.d.ts.map +1 -1
  54. package/types/src/Id.d.ts +60 -8
  55. package/types/src/Id.d.ts.map +1 -1
  56. package/types/src/Logger.d.ts +53 -2
  57. package/types/src/Logger.d.ts.map +1 -1
  58. package/types/src/Num.d.ts +48 -7
  59. package/types/src/Num.d.ts.map +1 -1
  60. package/types/src/Obj.d.ts +67 -4
  61. package/types/src/Obj.d.ts.map +1 -1
  62. package/types/src/SQLite.d.ts +123 -14
  63. package/types/src/SQLite.d.ts.map +1 -1
  64. package/types/src/Schema.d.ts +256 -7
  65. package/types/src/Schema.d.ts.map +1 -1
  66. package/types/src/Str.d.ts +49 -1
  67. package/types/src/Str.d.ts.map +1 -1
  68. package/types/src/Sys.d.ts +51 -6
  69. package/types/src/Sys.d.ts.map +1 -1
  70. package/types/src/Time.d.ts +130 -34
  71. package/types/src/Time.d.ts.map +1 -1
  72. package/types/src/Utils.d.ts +15 -11
  73. package/types/src/Utils.d.ts.map +1 -1
  74. package/types/src/errors.d.ts +27 -3
  75. package/types/src/errors.d.ts.map +1 -1
  76. package/types/src/internal/shared.d.ts +123 -0
  77. package/types/src/internal/shared.d.ts.map +1 -0
package/src/Id.js CHANGED
@@ -1,27 +1,33 @@
1
1
  "use strict";
2
2
 
3
3
  /**
4
- * Unique ids: UUID v4 and v7, ULID, nanoid-style ids, Snowflakes, tokens
5
- * and short codes. All of them use the secure random generator of
6
- * `node:crypto`, with no bias.
4
+ * Unique ids: UUID v4, v5 and v7, ULID, ObjectId, nanoid-style ids,
5
+ * Snowflakes, tokens and short codes. Random ones use the secure generator
6
+ * of `node:crypto`, with no bias.
7
7
  *
8
8
  * @example
9
- * id.uuidv7(); // "0190a5c4-7c1e-7b2a-9f3e-1c2d3e4f5a6b", sorts by creation time
10
- * id.nano(); // "V1StGXR8_Z5jdHi6B-myT"
11
- * id.code(); // "K7QF9X"
12
- * id.token(32); // for API keys and reset links
9
+ * id.uuidv7(); // "0190a5c4-7c1e-7b2a-9f3e-1c2d3e4f5a6b", sorts by creation time
10
+ * id.nano(); // "V1StGXR8_Z5jdHi6B-myT"
11
+ * id.prefixed("user"); // "user_7Hq2mXc0LrT9aZ4bWk1PdE8s"
12
+ * id.code(); // "K7QF9X"
13
+ * id.token(32); // for API keys and reset links
13
14
  */
14
15
 
15
16
  /** @type {typeof import("node:crypto") | undefined} */
16
17
  let _nodeCrypto;
17
18
  const _crypto = () => (_nodeCrypto ??= require("node:crypto"));
18
- const nodeCrypto = require("./Crypto.js");
19
19
 
20
20
  /**
21
21
  * Encodings for `token()`.
22
22
  * @typedef {"hex" | "base64" | "base64url" | "base58" | "base32"} TokenEncoding
23
23
  */
24
24
 
25
+ /**
26
+ * A namespace for `uuidv5()`: one of the four standard ones, or any UUID of
27
+ * your own.
28
+ * @typedef {"dns" | "url" | "oid" | "x500" | (string & {})} UUIDNamespace
29
+ */
30
+
25
31
  /**
26
32
  * Options for `snowflake()` and `parseSnowflake()`.
27
33
  * @typedef {object} SnowflakeOptions
@@ -42,32 +48,98 @@ const nodeCrypto = require("./Crypto.js");
42
48
 
43
49
  const NANO_ALPHABET = "useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict";
44
50
  const BASE58_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
51
+ const BASE62_ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
52
+ const CODE_ALPHABET = "ABCDEFGHJKMNPQRSTUVWXYZ23456789";
45
53
  const CROCKFORD = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
46
54
  const DISCORD_EPOCH = 1420070400000;
55
+ const MAX_TIME = 2 ** 48 - 1;
56
+
57
+ /** @type {Record<string, string>} */
58
+ const NAMESPACES = {
59
+ dns: "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
60
+ url: "6ba7b811-9dad-11d1-80b4-00c04fd430c8",
61
+ oid: "6ba7b812-9dad-11d1-80b4-00c04fd430c8",
62
+ x500: "6ba7b814-9dad-11d1-80b4-00c04fd430c8",
63
+ };
47
64
 
48
- /** @type {Record<string, number>} */
49
- const _counters = {};
65
+ const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
66
+ const UUID_V7 = /^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
67
+ const ULID = /^[0-7][0-9A-HJKMNP-TV-Z]{25}$/i;
68
+ const OBJECT_ID = /^[0-9a-f]{24}$/i;
69
+
70
+ /** @type {string[]} */
71
+ const HEX = [];
72
+ for (let i = 0; i < 256; i++) HEX.push(i.toString(16).padStart(2, "0"));
73
+
74
+ // Asking the system for random bytes is slow, so they are fetched 4 KB at a
75
+ // time and handed out from there.
76
+ const POOL_SIZE = 4096;
77
+ /** @type {Buffer | undefined} */
78
+ let _pool;
79
+ let _poolAt = POOL_SIZE;
80
+
81
+ /**
82
+ * Reserves `count` fresh random bytes in the pool.
83
+ * @param {number} count At most `POOL_SIZE`.
84
+ * @returns {number} Where they start.
85
+ */
86
+ const _take = (count) => {
87
+ if (_poolAt + count > POOL_SIZE) {
88
+ _crypto().randomFillSync((_pool ??= Buffer.allocUnsafeSlow(POOL_SIZE)));
89
+ _poolAt = 0;
90
+ }
91
+ const at = _poolAt;
92
+ _poolAt += count;
93
+ return at;
94
+ };
50
95
 
51
96
  /**
52
- * Secure random characters, with rejection sampling so there's no bias.
53
- * @param {string} alphabet
54
97
  * @param {number} size
55
- * @returns {string}
98
+ * @param {string} what
99
+ */
100
+ const _checkSize = (size, what) => {
101
+ if (!Number.isInteger(size) || size < 0) throw new RangeError(`${what} must be a whole number, 0 or more, got ${size}`);
102
+ };
103
+
104
+ /** @type {Map<string, (size: number) => string>} */
105
+ const _samplers = new Map();
106
+
107
+ /**
108
+ * A function that draws random characters from an alphabet. Bytes that fall
109
+ * outside the alphabet are thrown away, so every character is as likely as
110
+ * the others.
111
+ *
112
+ * @param {string} alphabet
113
+ * @returns {(size: number) => string}
56
114
  */
57
- const _random = (alphabet, size) => {
58
- const len = alphabet.length;
115
+ const _sampler = (alphabet) => {
116
+ const known = _samplers.get(alphabet);
117
+ if (known) return known;
118
+ const len = typeof alphabet === "string" ? alphabet.length : 0;
59
119
  if (len < 2 || len > 256) throw new RangeError("The alphabet must contain between 2 and 256 characters");
60
- const mask = (2 << Math.floor(Math.log2(len - 1))) - 1;
61
- const step = Math.ceil((1.6 * mask * size) / len);
62
- let out = "";
63
- while (out.length < size) {
64
- const bytes = _crypto().randomBytes(step);
65
- for (let i = 0; i < step && out.length < size; i++) {
66
- const index = bytes[i] & mask;
67
- if (index < len) out += alphabet[index];
120
+ const mask = (2 << (31 - Math.clz32((len - 1) | 1))) - 1;
121
+ /** @type {(size: number) => string} */
122
+ const sample = (size) => {
123
+ _checkSize(size, "The size");
124
+ let out = "";
125
+ let left = size;
126
+ while (left > 0) {
127
+ const count = Math.min(left, POOL_SIZE);
128
+ const at = _take(count);
129
+ const pool = /** @type {Buffer} */ (_pool);
130
+ for (let i = at; i < at + count; i++) {
131
+ const index = pool[i] & mask;
132
+ if (index < len) {
133
+ out += alphabet[index];
134
+ left--;
135
+ }
136
+ }
68
137
  }
69
- }
70
- return out;
138
+ return out;
139
+ };
140
+ if (_samplers.size >= 100) _samplers.clear();
141
+ _samplers.set(alphabet, sample);
142
+ return sample;
71
143
  };
72
144
 
73
145
  /**
@@ -104,7 +176,7 @@ const _base32 = (buffer) => {
104
176
  let value = 0;
105
177
  let out = "";
106
178
  for (const byte of buffer) {
107
- value = (value << 8) | byte;
179
+ value = ((value << 8) | byte) & 0xffff;
108
180
  bits += 8;
109
181
  while (bits >= 5) {
110
182
  out += CROCKFORD[(value >>> (bits - 5)) & 31];
@@ -115,6 +187,18 @@ const _base32 = (buffer) => {
115
187
  return out;
116
188
  };
117
189
 
190
+ /**
191
+ * @param {number | Date | undefined} time
192
+ * @param {string} what
193
+ * @returns {number | undefined}
194
+ */
195
+ const _time = (time, what) => {
196
+ if (time === undefined) return undefined;
197
+ const ms = +time;
198
+ if (!Number.isInteger(ms) || ms < 0 || ms > MAX_TIME) throw new RangeError(`${what}: the time must be a date or a timestamp in ms, got ${String(time)}`);
199
+ return ms;
200
+ };
201
+
118
202
  /**
119
203
  * A random UUID (version 4). For ids that sort by date, see `uuidv7()`.
120
204
  *
@@ -127,14 +211,41 @@ function uuid() {
127
211
  return _crypto().randomUUID();
128
212
  }
129
213
 
214
+ /**
215
+ * A UUID v5: the same name in the same namespace always gives the same id.
216
+ * Use it to derive a stable id from something you already have, like a URL
217
+ * or an email, without storing a mapping.
218
+ *
219
+ * @example
220
+ * id.uuidv5("example.com", "dns"); // "cfbff0d1-9375-5685-968c-48ce8b15ae17", every time
221
+ * id.uuidv5("https://example.com/a", "url");
222
+ * id.uuidv5("ada@example.com", MY_APP_NAMESPACE); // any UUID works as a namespace
223
+ *
224
+ * @param {string | Buffer | Uint8Array} name
225
+ * @param {UUIDNamespace} namespace `"dns"`, `"url"`, `"oid"`, `"x500"` or a UUID.
226
+ * @returns {string}
227
+ */
228
+ function uuidv5(name, namespace) {
229
+ const space = Object.hasOwn(NAMESPACES, namespace) ? NAMESPACES[namespace] : namespace;
230
+ if (typeof space !== "string" || !UUID.test(space)) {
231
+ throw new TypeError(`uuidv5(): the namespace must be "dns", "url", "oid", "x500" or a UUID, got ${String(namespace)}`);
232
+ }
233
+ const bytes = _crypto().createHash("sha1").update(Buffer.from(space.replace(/-/g, ""), "hex")).update(name).digest();
234
+ bytes[6] = (bytes[6] & 0x0f) | 0x50;
235
+ bytes[8] = (bytes[8] & 0x3f) | 0x80;
236
+ const hex = bytes.toString("hex", 0, 16);
237
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
238
+ }
239
+
130
240
  let _v7LastMs = 0;
131
241
  let _v7Counter = 0;
242
+ let _v7Head = "";
132
243
 
133
244
  /**
134
245
  * A UUID v7: it starts with a timestamp, so ids sort by creation time.
135
246
  * That keeps database indexes compact, which makes them great primary
136
247
  * keys. Ids from the same process always increase, even within a
137
- * millisecond.
248
+ * millisecond, and even if the system clock steps back.
138
249
  *
139
250
  * @example
140
251
  * id.uuidv7(); // "0190a5c4-7c1e-7b2a-9f3e-1c2d3e4f5a6b"
@@ -143,66 +254,108 @@ let _v7Counter = 0;
143
254
  * @returns {string}
144
255
  */
145
256
  function uuidv7() {
146
- let now = Date.now();
147
- if (now <= _v7LastMs) {
148
- _v7Counter++;
149
- if (_v7Counter > 0xfff) {
150
- _v7Counter = 0;
151
- _v7LastMs++;
152
- }
153
- now = _v7LastMs;
257
+ const now = Date.now();
258
+ const at = _take(10);
259
+ const pool = /** @type {Buffer} */ (_pool);
260
+ if (now > _v7LastMs || _v7Counter === 0xfff) {
261
+ // A new millisecond, or the counter is used up and we borrow the next one.
262
+ _v7LastMs = now > _v7LastMs ? now : _v7LastMs + 1;
263
+ _v7Counter = ((pool[at + 8] << 8) | pool[at + 9]) & 0x7ff;
264
+ const time = _v7LastMs.toString(16).padStart(12, "0");
265
+ _v7Head = `${time.slice(0, 8)}-${time.slice(8)}-7`;
154
266
  } else {
155
- _v7LastMs = now;
156
- _v7Counter = _crypto().randomInt(0, 0x800);
267
+ _v7Counter++;
157
268
  }
158
- const bytes = _crypto().randomBytes(16);
159
- bytes.writeUIntBE(now, 0, 6);
160
- bytes[6] = 0x70 | ((_v7Counter >> 8) & 0x0f);
161
- bytes[7] = _v7Counter & 0xff;
162
- bytes[8] = 0x80 | (bytes[8] & 0x3f);
163
- const hex = bytes.toString("hex");
164
- return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
269
+ return (
270
+ `${_v7Head}${HEX[_v7Counter >> 8][1]}${HEX[_v7Counter & 0xff]}-${HEX[0x80 | (pool[at] & 0x3f)]}${HEX[pool[at + 1]]}-` +
271
+ HEX[pool[at + 2]] + HEX[pool[at + 3]] + HEX[pool[at + 4]] + HEX[pool[at + 5]] + HEX[pool[at + 6]] + HEX[pool[at + 7]]
272
+ );
165
273
  }
166
274
 
167
275
  let _ulidLastMs = -1;
168
- /** @type {number[]} */
169
- let _ulidLastRandom = [];
276
+ let _ulidHead = "";
277
+ const _ulidRandom = new Uint8Array(16);
278
+
279
+ /**
280
+ * @param {number} ms
281
+ * @returns {string}
282
+ */
283
+ const _ulidTime = (ms) => {
284
+ let head = "";
285
+ let left = ms;
286
+ for (let i = 0; i < 10; i++) {
287
+ head = CROCKFORD[left % 32] + head;
288
+ left = Math.floor(left / 32);
289
+ }
290
+ return head;
291
+ };
170
292
 
171
293
  /**
172
294
  * A ULID: 26 URL-safe characters that sort by creation time. Ids made in
173
295
  * the same millisecond still increase.
174
296
  *
175
297
  * @example
176
- * id.ulid(); // "01HQ3Z8K9V3N5W7Y2XJ4T6R8PB"
177
- * id.ulid(Date.UTC(2024, 0, 1)); // for a given time
298
+ * id.ulid(); // "01HQ3Z8K9V3N5W7Y2XJ4T6R8PB"
299
+ * id.ulid(Date.UTC(2024, 0, 1)); // for a given time
178
300
  *
179
- * @param {number} [time] Timestamp to encode, in ms. Defaults to now.
301
+ * @param {number | Date} [time] The moment to encode. Defaults to now.
180
302
  * @returns {string}
303
+ * @throws {RangeError} If the time is not a valid timestamp.
181
304
  */
182
305
  function ulid(time) {
183
- const now = time ?? Date.now();
184
- /** @type {number[]} */
185
- let random;
186
- if (time === undefined && now === _ulidLastMs) {
187
- random = [..._ulidLastRandom];
188
- let i = random.length - 1;
189
- while (i >= 0 && random[i] === 31) random[i--] = 0;
190
- if (i < 0) throw new RangeError("ULID overflow: too many ids in the same millisecond");
191
- random[i]++;
192
- } else {
193
- random = [..._crypto().randomBytes(16)].map((b) => b & 31);
306
+ const given = _time(time, "ulid()");
307
+ if (given !== undefined) {
308
+ const at = _take(16);
309
+ const pool = /** @type {Buffer} */ (_pool);
310
+ let out = _ulidTime(given);
311
+ for (let i = at; i < at + 16; i++) out += CROCKFORD[pool[i] & 31];
312
+ return out;
194
313
  }
195
- if (time === undefined) {
314
+ const now = Date.now();
315
+ if (now > _ulidLastMs) {
196
316
  _ulidLastMs = now;
197
- _ulidLastRandom = random;
317
+ _ulidHead = _ulidTime(now);
318
+ const at = _take(16);
319
+ const pool = /** @type {Buffer} */ (_pool);
320
+ for (let i = 0; i < 16; i++) _ulidRandom[i] = pool[at + i] & 31;
321
+ } else {
322
+ // Same millisecond, or the clock stepped back: count up from the last id.
323
+ let i = 15;
324
+ while (i >= 0 && _ulidRandom[i] === 31) _ulidRandom[i--] = 0;
325
+ if (i < 0) throw new RangeError("ULID overflow: too many ids in the same millisecond");
326
+ _ulidRandom[i]++;
198
327
  }
199
- let t = now;
200
- let head = "";
201
- for (let i = 0; i < 10; i++) {
202
- head = CROCKFORD[t % 32] + head;
203
- t = Math.floor(t / 32);
328
+ let out = _ulidHead;
329
+ for (let i = 0; i < 16; i++) out += CROCKFORD[_ulidRandom[i]];
330
+ return out;
331
+ }
332
+
333
+ let _oidProcess = "";
334
+ let _oidCounter = 0;
335
+
336
+ /**
337
+ * A MongoDB-style ObjectId: 24 hex characters that start with the time, in
338
+ * seconds, and increase within the process.
339
+ *
340
+ * @example
341
+ * id.objectId(); // "65f2a9c47b1e4d0c9a3f2b10"
342
+ * id.timestamp(id.objectId()); // when it was created, to the second
343
+ * id.objectId(new Date("2024-01-01")); // for a given time
344
+ *
345
+ * @param {number | Date} [time] The moment to encode. Defaults to now.
346
+ * @returns {string}
347
+ * @throws {RangeError} If the time is not a valid timestamp.
348
+ */
349
+ function objectId(time) {
350
+ if (!_oidProcess) {
351
+ const at = _take(8);
352
+ const pool = /** @type {Buffer} */ (_pool);
353
+ for (let i = at; i < at + 5; i++) _oidProcess += HEX[pool[i]];
354
+ _oidCounter = (pool[at + 5] << 16) | (pool[at + 6] << 8) | pool[at + 7];
204
355
  }
205
- return head + random.map((v) => CROCKFORD[v]).join("");
356
+ const seconds = Math.floor((_time(time, "objectId()") ?? Date.now()) / 1000) % 2 ** 32;
357
+ _oidCounter = (_oidCounter + 1) & 0xffffff;
358
+ return seconds.toString(16).padStart(8, "0") + _oidProcess + HEX[_oidCounter >> 16] + HEX[(_oidCounter >> 8) & 0xff] + HEX[_oidCounter & 0xff];
206
359
  }
207
360
 
208
361
  /**
@@ -219,7 +372,7 @@ function ulid(time) {
219
372
  * @returns {string}
220
373
  */
221
374
  function nano(size = 21, alphabet = NANO_ALPHABET) {
222
- return _random(alphabet, size);
375
+ return _sampler(alphabet)(size);
223
376
  }
224
377
 
225
378
  /**
@@ -234,8 +387,26 @@ function nano(size = 21, alphabet = NANO_ALPHABET) {
234
387
  * @returns {(size?: number) => string}
235
388
  */
236
389
  function customAlphabet(alphabet, size = 21) {
237
- _random(alphabet, 1);
238
- return (length = size) => _random(alphabet, length);
390
+ const sample = _sampler(alphabet);
391
+ return (length = size) => sample(length);
392
+ }
393
+
394
+ /**
395
+ * A random id with a prefix that says what it identifies, like Stripe's
396
+ * `cus_...` and `sub_...`. A glance at a log line or a support ticket tells
397
+ * you what kind of thing the id points to.
398
+ *
399
+ * @example
400
+ * id.prefixed("user"); // "user_7Hq2mXc0LrT9aZ4bWk1PdE8s"
401
+ * id.prefixed("inv", 12); // "inv_Q0f8ZrT1bK4m"
402
+ *
403
+ * @param {string} prefix
404
+ * @param {number} [size=24] How many random characters come after the prefix (letters and digits).
405
+ * @returns {string}
406
+ */
407
+ function prefixed(prefix, size = 24) {
408
+ if (typeof prefix !== "string" || !prefix) throw new TypeError("prefixed(): the prefix must be a non-empty string");
409
+ return `${prefix}_${_sampler(BASE62_ALPHABET)(size)}`;
239
410
  }
240
411
 
241
412
  /**
@@ -251,6 +422,8 @@ function customAlphabet(alphabet, size = 21) {
251
422
  * @returns {string}
252
423
  */
253
424
  function token(bytes = 32, encoding = "hex") {
425
+ _checkSize(bytes, "The number of bytes");
426
+ // Tokens are secrets: their bytes never sit in the shared pool.
254
427
  const buffer = _crypto().randomBytes(bytes);
255
428
  if (encoding === "base58") return _base58(buffer);
256
429
  if (encoding === "base32") return _base32(buffer);
@@ -269,8 +442,8 @@ function token(bytes = 32, encoding = "hex") {
269
442
  * @param {string} [alphabet="ABCDEFGHJKMNPQRSTUVWXYZ23456789"]
270
443
  * @returns {string}
271
444
  */
272
- function code(length = 6, alphabet = "ABCDEFGHJKMNPQRSTUVWXYZ23456789") {
273
- return _random(alphabet, length);
445
+ function code(length = 6, alphabet = CODE_ALPHABET) {
446
+ return _sampler(alphabet)(length);
274
447
  }
275
448
 
276
449
  let _snowflakeLastMs = 0;
@@ -285,6 +458,7 @@ let _snowflakeIncrement = 0;
285
458
  *
286
459
  * @param {SnowflakeOptions} [options]
287
460
  * @returns {string}
461
+ * @throws {RangeError} If the epoch is in the future.
288
462
  */
289
463
  function snowflake(options = {}) {
290
464
  const epoch = options.epoch ?? DISCORD_EPOCH;
@@ -297,9 +471,9 @@ function snowflake(options = {}) {
297
471
  _snowflakeLastMs = now;
298
472
  _snowflakeIncrement = 0;
299
473
  }
300
- const worker = BigInt((options.workerId ?? 0) & 31);
301
- const proc = BigInt((options.processId ?? process.pid) & 31);
302
- return ((BigInt(now - epoch) << 22n) | (worker << 17n) | (proc << 12n) | BigInt(_snowflakeIncrement)).toString();
474
+ if (!(now >= epoch)) throw new RangeError(`snowflake(): the epoch must be a timestamp in the past, got ${epoch}`);
475
+ const low = (((options.workerId ?? 0) & 31) << 17) | (((options.processId ?? process.pid) & 31) << 12) | _snowflakeIncrement;
476
+ return ((BigInt(now - epoch) << 22n) | BigInt(low)).toString();
303
477
  }
304
478
 
305
479
  /**
@@ -311,42 +485,49 @@ function snowflake(options = {}) {
311
485
  * @param {string | bigint} id
312
486
  * @param {SnowflakeOptions} [options] Use the epoch it was created with.
313
487
  * @returns {ParsedSnowflake}
488
+ * @throws {TypeError} If the id is not a string of digits or a bigint.
314
489
  */
315
490
  function parseSnowflake(id, options = {}) {
491
+ if (typeof id !== "bigint" && !(typeof id === "string" && /^\d{1,20}$/.test(id))) {
492
+ throw new TypeError(`parseSnowflake(): expected a string of digits or a bigint, got ${typeof id === "string" ? JSON.stringify(id) : typeof id}`);
493
+ }
316
494
  const value = BigInt(id);
495
+ if (value < 0n) throw new TypeError("parseSnowflake(): a Snowflake can't be negative");
496
+ const low = Number(value & 0x3fffffn);
317
497
  const timestamp = Number(value >> 22n) + (options.epoch ?? DISCORD_EPOCH);
318
498
  return {
319
499
  date: new Date(timestamp),
320
500
  timestamp,
321
- workerId: Number((value >> 17n) & 31n),
322
- processId: Number((value >> 12n) & 31n),
323
- increment: Number(value & 0xfffn),
501
+ workerId: (low >> 17) & 31,
502
+ processId: (low >> 12) & 31,
503
+ increment: low & 0xfff,
324
504
  };
325
505
  }
326
506
 
327
507
  /**
328
- * When a ULID or a UUID v7 was created.
508
+ * When a UUID v7, a ULID or an ObjectId was created.
329
509
  *
330
510
  * @example
331
- * id.timestamp(id.ulid()); // now
332
- * id.timestamp(id.uuid()); // undefined, v4 has no time in it
511
+ * id.timestamp(id.ulid()); // now
512
+ * id.timestamp(id.objectId()); // now, to the second
513
+ * id.timestamp(id.uuid()); // undefined, v4 has no time in it
333
514
  *
334
515
  * @param {string} value
335
516
  * @returns {Date | undefined}
336
517
  */
337
518
  function timestamp(value) {
338
519
  const s = String(value);
339
- if (/^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(s)) {
340
- return new Date(parseInt(s.replace(/-/g, "").slice(0, 12), 16));
341
- }
342
- if (/^[0-7][0-9A-HJKMNP-TV-Z]{25}$/i.test(s)) {
343
- let ms = 0;
344
- for (const char of s.slice(0, 10).toUpperCase()) ms = ms * 32 + CROCKFORD.indexOf(char);
345
- return new Date(ms);
346
- }
347
- return undefined;
520
+ if (s.length === 36) return UUID_V7.test(s) ? new Date(parseInt(s.slice(0, 8) + s.slice(9, 13), 16)) : undefined;
521
+ if (s.length === 24) return OBJECT_ID.test(s) ? new Date(parseInt(s.slice(0, 8), 16) * 1000) : undefined;
522
+ if (s.length !== 26 || !ULID.test(s)) return undefined;
523
+ let ms = 0;
524
+ for (let i = 0; i < 10; i++) ms = ms * 32 + CROCKFORD.indexOf(s[i].toUpperCase());
525
+ return new Date(ms);
348
526
  }
349
527
 
528
+ /** @type {Map<string, number>} */
529
+ const _counters = new Map();
530
+
350
531
  /**
351
532
  * Readable sequential ids like `"user-1"`, `"user-2"`, counted per prefix.
352
533
  * Only unique within the current process; handy in logs and tests.
@@ -359,8 +540,9 @@ function timestamp(value) {
359
540
  * @returns {string}
360
541
  */
361
542
  function seq(prefix = "") {
362
- _counters[prefix] = (_counters[prefix] ?? 0) + 1;
363
- return prefix ? `${prefix}-${_counters[prefix]}` : String(_counters[prefix]);
543
+ const next = (_counters.get(prefix) ?? 0) + 1;
544
+ _counters.set(prefix, next);
545
+ return prefix ? `${prefix}-${next}` : String(next);
364
546
  }
365
547
 
366
548
  /**
@@ -372,7 +554,7 @@ function seq(prefix = "") {
372
554
  * @returns {string}
373
555
  */
374
556
  function hash(value, options) {
375
- return nodeCrypto.hash(value, options);
557
+ return require("./Crypto.js").hash(value, options);
376
558
  }
377
559
 
378
560
  /**
@@ -385,7 +567,7 @@ function hash(value, options) {
385
567
  * @returns {string}
386
568
  */
387
569
  function hmac(value, secret, options) {
388
- return nodeCrypto.hmac(value, secret, options);
570
+ return require("./Crypto.js").hmac(value, secret, options);
389
571
  }
390
572
 
391
573
  /**
@@ -397,15 +579,18 @@ function hmac(value, secret, options) {
397
579
  * @returns {boolean}
398
580
  */
399
581
  function safeEqual(a, b) {
400
- return nodeCrypto.safeEqual(a, b);
582
+ return require("./Crypto.js").safeEqual(a, b);
401
583
  }
402
584
 
403
585
  module.exports = {
404
586
  uuid,
587
+ uuidv5,
405
588
  uuidv7,
406
589
  ulid,
590
+ objectId,
407
591
  nano,
408
592
  customAlphabet,
593
+ prefixed,
409
594
  token,
410
595
  code,
411
596
  snowflake,