@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/Obj.js CHANGED
@@ -13,6 +13,7 @@
13
13
  */
14
14
 
15
15
  const { types } = require("node:util");
16
+ const shared = require("./internal/shared.js");
16
17
 
17
18
  /**
18
19
  * Values that end a path: dates, regexes, maps, sets, functions and arrays.
@@ -80,6 +81,13 @@ const { types } = require("node:util");
80
81
  * @property {boolean} [skipUndefined] Don't let `undefined` overwrite an existing value.
81
82
  */
82
83
 
84
+ /**
85
+ * Options for `sortKeys()`.
86
+ * @typedef {object} SortKeysOptions
87
+ * @property {boolean} [deep] Sort nested objects too, arrays included. Defaults to `true`. With `false`, only the first level is copied and sorted: what it holds is shared with the original.
88
+ * @property {(a: string, b: string) => number} [compare] How to order two keys. Defaults to code-unit order, which is stable across machines.
89
+ */
90
+
83
91
  /**
84
92
  * Options for `compact()`.
85
93
  * @typedef {object} CompactOptions
@@ -87,51 +95,81 @@ const { types } = require("node:util");
87
95
  * @property {boolean} [removeEmpty] Also remove `""`, `[]` and `{}`.
88
96
  */
89
97
 
90
- const _UNSAFE = new Set(["__proto__", "constructor", "prototype"]);
98
+ const _UNSAFE = shared.UNSAFE_KEYS;
91
99
 
92
100
  /** Marks an unknown path segment in `PathStep`. */
93
101
  const _NOT_FOUND = Symbol("not-found");
94
102
 
95
- /** @param {unknown} value */
96
- const _isPlain = (value) => {
97
- if (typeof value !== "object" || value === null) return false;
98
- const proto = Object.getPrototypeOf(value);
99
- // The last check accepts objects from another realm (a worker, a `vm`).
100
- return proto === null || proto === Object.prototype || Object.getPrototypeOf(proto) === null;
103
+ const _isPlain = shared.isPlainObject;
104
+
105
+ /** @param {ReadonlyArray<string | number>} keys */
106
+ const _isSafePath = (keys) => {
107
+ for (let i = 0; i < keys.length; i++) if (_UNSAFE.has(String(keys[i]))) return false;
108
+ return true;
101
109
  };
102
110
 
111
+ /** @type {Map<string, { keys: Array<string | number>, safe: boolean }>} */
112
+ const _paths = new Map();
113
+
103
114
  /**
104
- * Parses `"a.b[0].c"` or `["a", "b", 0, "c"]` into keys.
115
+ * Parses `"a.b[0].c"` or `["a", "b", 0, "c"]` into keys, and says whether
116
+ * the path stays clear of prototypes. Parsed strings are remembered: paths
117
+ * are almost always literals.
105
118
  * @param {unknown} path
106
- * @returns {Array<string | number>}
107
- */
108
- const _toPath = (path) => {
109
- if (Array.isArray(path)) return path;
110
- return String(path)
111
- .replace(/\[(\d+)\]/g, ".$1")
112
- .replace(/\[["']?([^\]"']+)["']?\]/g, ".$1")
113
- .split(".")
114
- .filter((part) => part !== "");
119
+ * @returns {{ keys: ReadonlyArray<string | number>, safe: boolean }}
120
+ */
121
+ const _parsePath = (path) => {
122
+ if (Array.isArray(path)) return { keys: path, safe: _isSafePath(path) };
123
+ const text = String(path);
124
+ let parsed = _paths.get(text);
125
+ if (parsed === undefined) {
126
+ const dotted = text.includes("[") ? text.replace(/\[(\d+)\]/g, ".$1").replace(/\[["']?([^\]"']+)["']?\]/g, ".$1") : text;
127
+ const keys = dotted.includes(".") ? dotted.split(".").filter((part) => part !== "") : dotted === "" ? [] : [dotted];
128
+ parsed = { keys, safe: _isSafePath(keys) };
129
+ if (_paths.size >= 1000) _paths.clear();
130
+ _paths.set(text, parsed);
131
+ }
132
+ return parsed;
115
133
  };
116
134
 
117
- /** @param {Array<string | number>} keys */
118
- const _isSafePath = (keys) => keys.every((key) => !_UNSAFE.has(String(key)));
119
-
120
135
  /**
121
- * Deep copy for what `structuredClone` can't handle (functions, class instances).
136
+ * Deep copy that keeps what `structuredClone` loses: functions and class
137
+ * prototypes. Shared references stay shared and cycles are preserved.
122
138
  * @param {any} value
123
- * @param {WeakMap<object, any>} seen
139
+ * @param {Map<object, any>} seen
124
140
  * @returns {any}
125
141
  */
126
142
  const _deepClone = (value, seen) => {
127
143
  if (value === null || typeof value !== "object") return value;
128
- if (seen.has(value)) return seen.get(value);
129
- if (types.isDate(value)) return new Date(value.getTime());
130
- if (types.isRegExp(value)) return new RegExp(value.source, value.flags);
144
+ const known = seen.get(value);
145
+ if (known !== undefined) return known;
131
146
  if (Array.isArray(value)) {
132
- const out = /** @type {any[]} */ ([]);
147
+ /** @type {any[]} */
148
+ const out = new Array(value.length);
133
149
  seen.set(value, out);
134
- for (const item of value) out.push(_deepClone(item, seen));
150
+ for (let i = 0; i < value.length; i++) if (i in value) out[i] = _deepClone(value[i], seen);
151
+ return out;
152
+ }
153
+ const proto = Object.getPrototypeOf(value);
154
+ if (proto === Object.prototype || proto === null) {
155
+ /** @type {any} */
156
+ const out = proto === null ? Object.create(null) : {};
157
+ seen.set(value, out);
158
+ const keys = Object.keys(value);
159
+ for (let i = 0; i < keys.length; i++) {
160
+ const key = keys[i];
161
+ if (!_UNSAFE.has(key)) out[key] = _deepClone(value[key], seen);
162
+ }
163
+ const symbols = Object.getOwnPropertySymbols(value);
164
+ for (let i = 0; i < symbols.length; i++) {
165
+ if (Object.prototype.propertyIsEnumerable.call(value, symbols[i])) out[symbols[i]] = _deepClone(value[symbols[i]], seen);
166
+ }
167
+ return out;
168
+ }
169
+ if (types.isDate(value)) return new Date(value.getTime());
170
+ if (types.isRegExp(value)) {
171
+ const out = new RegExp(value.source, value.flags);
172
+ out.lastIndex = value.lastIndex;
135
173
  return out;
136
174
  }
137
175
  if (types.isMap(value)) {
@@ -146,10 +184,18 @@ const _deepClone = (value, seen) => {
146
184
  for (const v of value) out.add(_deepClone(v, seen));
147
185
  return out;
148
186
  }
149
- if (ArrayBuffer.isView(value) && !(value instanceof DataView)) {
187
+ if (ArrayBuffer.isView(value)) {
188
+ // Buffer.slice() is a view on the same memory, not a copy.
189
+ if (Buffer.isBuffer(value)) return Buffer.from(value);
190
+ if (types.isDataView(value)) return new DataView(value.buffer.slice(value.byteOffset, value.byteOffset + value.byteLength));
150
191
  return /** @type {any} */ (value).slice();
151
192
  }
152
- const out = Object.create(Object.getPrototypeOf(value));
193
+ if (types.isAnyArrayBuffer(value)) return value.slice(0);
194
+ if (value instanceof URL) return new URL(value.href);
195
+ if (value instanceof URLSearchParams) return new URLSearchParams(value);
196
+ // Things that hold state outside their properties are shared, not copied.
197
+ if (types.isPromise(value) || types.isWeakMap(value) || types.isWeakSet(value) || types.isProxy(value)) return value;
198
+ const out = Object.create(proto);
153
199
  seen.set(value, out);
154
200
  for (const key of Reflect.ownKeys(value)) {
155
201
  if (typeof key === "string" && _UNSAFE.has(key)) continue;
@@ -161,36 +207,48 @@ const _deepClone = (value, seen) => {
161
207
  };
162
208
 
163
209
  /**
164
- * @param {any} target
165
- * @param {any} source
210
+ * Is it plain data, which merge copies, rather than a live object (a class
211
+ * instance, a connection) that it must keep as is?
212
+ * @param {unknown} value
213
+ * @returns {boolean}
214
+ */
215
+ const _isData = (value) =>
216
+ Array.isArray(value) || _isPlain(value) || types.isDate(value) || types.isMap(value) || types.isSet(value) || types.isRegExp(value) || ArrayBuffer.isView(value) || types.isAnyArrayBuffer(value);
217
+
218
+ /**
219
+ * Merges `source` into `target`, which the caller owns. What comes from the
220
+ * source is copied, so the result shares nothing with the inputs.
221
+ * @param {Record<string, any>} target
222
+ * @param {Record<string, any>} source
166
223
  * @param {MergeOptions} options
167
- * @returns {any}
224
+ * @returns {Record<string, any>}
168
225
  */
169
- const _mergeTwo = (target, source, options) => {
170
- if (Array.isArray(target) && Array.isArray(source)) {
171
- if (options.arrays === "concat") return [...target, ...source];
172
- if (options.arrays === "unique") return [...new Set([...target, ...source])];
173
- return [...source];
174
- }
175
- if (!_isPlain(source)) return source;
176
- const out = /** @type {Record<string, any>} */ (_isPlain(target) ? { ...target } : {});
177
- for (const key of Object.keys(source)) {
226
+ const _mergeInto = (target, source, options) => {
227
+ const keys = Object.keys(source);
228
+ for (let i = 0; i < keys.length; i++) {
229
+ const key = keys[i];
178
230
  if (_UNSAFE.has(key)) continue;
179
231
  const value = source[key];
180
232
  if (value === undefined && options.skipUndefined) continue;
181
- const current = out[key];
182
- out[key] = (_isPlain(value) && _isPlain(current)) || (Array.isArray(value) && Array.isArray(current))
183
- ? _mergeTwo(current, value, options)
184
- : _isPlain(value)
185
- ? _mergeTwo({}, value, options)
186
- : value;
233
+ const current = Object.prototype.hasOwnProperty.call(target, key) ? target[key] : undefined;
234
+ if (_isPlain(value)) {
235
+ target[key] = _mergeInto(_isPlain(current) ? current : {}, value, options);
236
+ } else if (Array.isArray(value)) {
237
+ const copy = _deepClone(value, new Map());
238
+ if (!Array.isArray(current) || options.arrays === undefined || options.arrays === "replace") target[key] = copy;
239
+ else target[key] = options.arrays === "unique" ? [...new Set([...current, ...copy])] : [...current, ...copy];
240
+ } else {
241
+ target[key] = _isData(value) ? _deepClone(value, new Map()) : value;
242
+ }
187
243
  }
188
- return out;
244
+ return target;
189
245
  };
190
246
 
247
+
191
248
  /**
192
- * Deep copy. Uses `structuredClone` when it can, and falls back to a copy
193
- * that keeps functions and class prototypes.
249
+ * Deep copy. Unlike `structuredClone`, it keeps functions and class
250
+ * instances (with their prototype), and it's faster on plain data. Shared
251
+ * references and cycles are preserved.
194
252
  *
195
253
  * @example
196
254
  * const copy = obj.clone({ a: { b: [1, 2] }, when: new Date() });
@@ -202,14 +260,7 @@ const _mergeTwo = (target, source, options) => {
202
260
  */
203
261
  function clone(value) {
204
262
  if (value === null || typeof value !== "object") return value;
205
- if (_isPlain(value) || Array.isArray(value)) {
206
- try {
207
- return structuredClone(value);
208
- } catch {
209
- // Functions or exotic values inside: fall through to the manual copy.
210
- }
211
- }
212
- return _deepClone(value, new WeakMap());
263
+ return _deepClone(value, new Map());
213
264
  }
214
265
 
215
266
  /**
@@ -259,7 +310,10 @@ function clone(value) {
259
310
  * @returns {Record<string, any>}
260
311
  */
261
312
  function merge(...sources) {
262
- return sources.reduce((acc, source) => (_isPlain(source) ? _mergeTwo(acc, source, {}) : acc), /** @type {Record<string, any>} */ ({}));
313
+ /** @type {Record<string, any>} */
314
+ const out = {};
315
+ for (const source of sources) if (_isPlain(source)) _mergeInto(out, source, _NO_OPTIONS);
316
+ return out;
263
317
  }
264
318
 
265
319
  /**
@@ -276,7 +330,10 @@ function merge(...sources) {
276
330
  * @returns {Record<string, any>}
277
331
  */
278
332
  function mergeWith(options, ...sources) {
279
- return sources.reduce((acc, source) => (_isPlain(source) ? _mergeTwo(acc, source, options) : acc), /** @type {Record<string, any>} */ ({}));
333
+ /** @type {Record<string, any>} */
334
+ const out = {};
335
+ for (const source of sources) if (_isPlain(source)) _mergeInto(out, source, options ?? _NO_OPTIONS);
336
+ return out;
280
337
  }
281
338
 
282
339
  /**
@@ -321,7 +378,9 @@ function defaults(target, ...sources) {
321
378
  * @returns {boolean}
322
379
  */
323
380
  function equal(a, b) {
324
- return _equal(a, b, new WeakMap());
381
+ if (Object.is(a, b)) return true;
382
+ if (typeof a !== "object" || typeof b !== "object" || a === null || b === null) return false;
383
+ return _equal(a, b, [], []);
325
384
  }
326
385
 
327
386
  /**
@@ -338,33 +397,78 @@ const _sameBuiltIn = (a, b) => {
338
397
  };
339
398
 
340
399
  /**
400
+ * Own enumerable keys and values are the same.
401
+ * @param {any} a
402
+ * @param {any} b
403
+ * @param {object[]} stackA
404
+ * @param {object[]} stackB
405
+ * @returns {boolean}
406
+ */
407
+ const _equalKeys = (a, b, stackA, stackB) => {
408
+ const keys = Object.keys(a);
409
+ if (keys.length !== Object.keys(b).length) return false;
410
+ for (let i = 0; i < keys.length; i++) {
411
+ const key = keys[i];
412
+ if (!Object.prototype.hasOwnProperty.call(b, key) || !_equal(a[key], b[key], stackA, stackB)) return false;
413
+ }
414
+ return true;
415
+ };
416
+
417
+ /**
418
+ * The stacks hold the objects being compared, so a cycle is recognized
419
+ * without a `WeakMap` lookup for every object.
341
420
  * @param {any} a
342
421
  * @param {any} b
343
- * @param {WeakMap<object, object>} seen
422
+ * @param {object[]} stackA
423
+ * @param {object[]} stackB
344
424
  * @returns {boolean}
345
425
  */
346
- function _equal(a, b, seen) {
426
+ function _equal(a, b, stackA, stackB) {
347
427
  if (Object.is(a, b)) return true;
348
428
  if (typeof a !== "object" || typeof b !== "object" || a === null || b === null) return false;
349
- if (Object.getPrototypeOf(a) !== Object.getPrototypeOf(b) && !_sameBuiltIn(a, b)) return false;
350
- if (seen.get(a) === b) return true;
351
- seen.set(a, b);
429
+ const proto = Object.getPrototypeOf(a);
430
+ if (proto !== Object.getPrototypeOf(b) && !_sameBuiltIn(a, b)) return false;
431
+ for (let i = stackA.length - 1; i >= 0; i--) if (stackA[i] === a) return stackB[i] === b;
432
+ stackA.push(a);
433
+ stackB.push(b);
434
+ const result = _equalObjects(a, b, proto, stackA, stackB);
435
+ stackA.pop();
436
+ stackB.pop();
437
+ return result;
438
+ }
352
439
 
440
+ /**
441
+ * @param {any} a
442
+ * @param {any} b
443
+ * @param {object | null} proto
444
+ * @param {object[]} stackA
445
+ * @param {object[]} stackB
446
+ * @returns {boolean}
447
+ */
448
+ const _equalObjects = (a, b, proto, stackA, stackB) => {
449
+ if (Array.isArray(a)) {
450
+ if (a.length !== b.length) return false;
451
+ for (let i = 0; i < a.length; i++) if (!_equal(a[i], b[i], stackA, stackB)) return false;
452
+ return true;
453
+ }
454
+ if (proto === Object.prototype || proto === null) return _equalKeys(a, b, stackA, stackB);
353
455
  if (types.isDate(a)) return a.getTime() === b.getTime();
354
456
  if (types.isRegExp(a)) return a.source === b.source && a.flags === b.flags;
355
- if (Array.isArray(a)) return a.length === b.length && a.every((item, i) => _equal(item, b[i], seen));
356
- if (ArrayBuffer.isView(a) && !(a instanceof DataView)) {
357
- const x = /** @type {any} */ (a);
358
- return x.length === b.length && x.every((/** @type {unknown} */ v, /** @type {number} */ i) => Object.is(v, b[i]));
457
+ if (ArrayBuffer.isView(a) || types.isAnyArrayBuffer(a)) {
458
+ const x = ArrayBuffer.isView(a) ? new Uint8Array(a.buffer, a.byteOffset, a.byteLength) : new Uint8Array(a);
459
+ const y = ArrayBuffer.isView(b) ? new Uint8Array(b.buffer, b.byteOffset, b.byteLength) : new Uint8Array(b);
460
+ if (x.length !== y.length) return false;
461
+ for (let i = 0; i < x.length; i++) if (x[i] !== y[i]) return false;
462
+ return true;
359
463
  }
360
464
  if (types.isMap(a)) {
361
465
  if (a.size !== b.size) return false;
362
466
  outer: for (const [key, value] of a) {
363
467
  if (b.has(key)) {
364
- if (!_equal(value, b.get(key), seen)) return false;
468
+ if (!_equal(value, b.get(key), stackA, stackB)) return false;
365
469
  continue;
366
470
  }
367
- for (const [otherKey, otherValue] of b) if (_equal(key, otherKey, seen) && _equal(value, otherValue, seen)) continue outer;
471
+ for (const [otherKey, otherValue] of b) if (_equal(key, otherKey, stackA, stackB) && _equal(value, otherValue, stackA, stackB)) continue outer;
368
472
  return false;
369
473
  }
370
474
  return true;
@@ -373,16 +477,18 @@ function _equal(a, b, seen) {
373
477
  if (a.size !== b.size) return false;
374
478
  outer: for (const value of a) {
375
479
  if (b.has(value)) continue;
376
- for (const other of b) if (_equal(value, other, seen)) continue outer;
480
+ for (const other of b) if (_equal(value, other, stackA, stackB)) continue outer;
377
481
  return false;
378
482
  }
379
483
  return true;
380
484
  }
381
- const keysA = Object.keys(a);
382
- const keysB = Object.keys(b);
383
- if (keysA.length !== keysB.length) return false;
384
- return keysA.every((key) => Object.prototype.hasOwnProperty.call(b, key) && _equal(a[key], b[key], seen));
385
- }
485
+ // These keep their content outside enumerable properties.
486
+ if (types.isBoxedPrimitive(a)) return Object.is(a.valueOf(), b.valueOf());
487
+ if (types.isNativeError(a)) return a.name === b.name && a.message === b.message && _equalKeys(a, b, stackA, stackB);
488
+ if (a instanceof URL) return a.href === b.href;
489
+ if (a instanceof URLSearchParams) return a.toString() === b.toString();
490
+ return _equalKeys(a, b, stackA, stackB);
491
+ };
386
492
 
387
493
  /**
388
494
  * Lists what changed between two values, deeply. Handy for audit logs and
@@ -402,26 +508,37 @@ function _equal(a, b, seen) {
402
508
  function diff(before, after) {
403
509
  /** @type {Change[]} */
404
510
  const changes = [];
405
- /** @type {(a: any, b: any, path: Array<string | number>) => void} */
406
- const walk = (a, b, path) => {
407
- if (equal(a, b)) return;
408
- const bothArrays = Array.isArray(a) && Array.isArray(b);
409
- if (bothArrays || (_isPlain(a) && _isPlain(b))) {
410
- const keys = bothArrays
411
- ? Array.from({ length: Math.max(a.length, b.length) }, (_, i) => i)
412
- : [...new Set([...Object.keys(a), ...Object.keys(b)])];
413
- for (const key of keys) {
414
- const inA = bothArrays ? /** @type {number} */ (key) < a.length : Object.prototype.hasOwnProperty.call(a, key);
415
- const inB = bothArrays ? /** @type {number} */ (key) < b.length : Object.prototype.hasOwnProperty.call(b, key);
416
- if (inA && !inB) changes.push({ path: [...path, key], type: "removed", from: a[key] });
417
- else if (!inA && inB) changes.push({ path: [...path, key], type: "added", to: b[key] });
418
- else walk(a[key], b[key], [...path, key]);
511
+ /** @type {Array<string | number>} */
512
+ const path = [];
513
+ /** @type {(a: any, b: any) => void} */
514
+ const walk = (a, b) => {
515
+ if (Object.is(a, b)) return;
516
+ if (Array.isArray(a) && Array.isArray(b)) {
517
+ const size = Math.max(a.length, b.length);
518
+ for (let i = 0; i < size; i++) {
519
+ path.push(i);
520
+ if (i >= b.length) changes.push({ path: [...path], type: "removed", from: a[i] });
521
+ else if (i >= a.length) changes.push({ path: [...path], type: "added", to: b[i] });
522
+ else walk(a[i], b[i]);
523
+ path.pop();
524
+ }
525
+ return;
526
+ }
527
+ if (_isPlain(a) && _isPlain(b)) {
528
+ for (const key of Object.keys(a)) {
529
+ path.push(key);
530
+ if (Object.prototype.hasOwnProperty.call(b, key)) walk(a[key], b[key]);
531
+ else changes.push({ path: [...path], type: "removed", from: a[key] });
532
+ path.pop();
533
+ }
534
+ for (const key of Object.keys(b)) {
535
+ if (!Object.prototype.hasOwnProperty.call(a, key)) changes.push({ path: [...path, key], type: "added", to: b[key] });
419
536
  }
420
537
  return;
421
538
  }
422
- changes.push({ path, type: "changed", from: a, to: b });
539
+ if (!equal(a, b)) changes.push({ path: [...path], type: "changed", from: a, to: b });
423
540
  };
424
- walk(before, after, []);
541
+ walk(before, after);
425
542
  return changes;
426
543
  }
427
544
 
@@ -462,13 +579,13 @@ function diff(before, after) {
462
579
  * @returns {any}
463
580
  */
464
581
  function get(object, path, fallback) {
465
- const keys = _toPath(path);
466
- if (!_isSafePath(keys)) return fallback;
582
+ const { keys, safe } = _parsePath(path);
583
+ if (!safe) return fallback;
467
584
  /** @type {any} */
468
585
  let current = object;
469
- for (const key of keys) {
586
+ for (let i = 0; i < keys.length; i++) {
470
587
  if (current === null || current === undefined) return fallback;
471
- current = current[key];
588
+ current = current[keys[i]];
472
589
  }
473
590
  return current === undefined ? fallback : current;
474
591
  }
@@ -488,10 +605,10 @@ function get(object, path, fallback) {
488
605
  * @returns {T}
489
606
  */
490
607
  function set(object, path, value) {
491
- const keys = _toPath(path);
608
+ const { keys, safe } = _parsePath(path);
492
609
  /** @type {any} */
493
610
  const root = object === null || typeof object !== "object" ? {} : clone(object);
494
- if (!keys.length || !_isSafePath(keys)) return root;
611
+ if (!keys.length || !safe) return root;
495
612
  let current = root;
496
613
  for (let i = 0; i < keys.length - 1; i++) {
497
614
  const key = keys[i];
@@ -505,6 +622,24 @@ function set(object, path, value) {
505
622
  return root;
506
623
  }
507
624
 
625
+ /**
626
+ * Returns a copy where the value at `path` is replaced by what `updater`
627
+ * returns for it. Handy for counters and lists.
628
+ *
629
+ * @example
630
+ * obj.update(state, "stats.visits", (n = 0) => n + 1);
631
+ * obj.update(cart, "items", (items = []) => [...items, product]);
632
+ *
633
+ * @template T
634
+ * @param {T} object
635
+ * @param {PathInput<T>} path
636
+ * @param {(value: any) => unknown} updater Gets the current value, `undefined` if there's none.
637
+ * @returns {T}
638
+ */
639
+ function update(object, path, updater) {
640
+ return set(object, path, updater(get(object, /** @type {any} */ (path))));
641
+ }
642
+
508
643
  /**
509
644
  * Returns a copy without the property at `path`.
510
645
  *
@@ -517,17 +652,17 @@ function set(object, path, value) {
517
652
  * @returns {T}
518
653
  */
519
654
  function unset(object, path) {
520
- const keys = _toPath(path);
655
+ const { keys, safe } = _parsePath(path);
521
656
  /** @type {any} */
522
657
  const root = clone(object);
523
- if (!keys.length || !_isSafePath(keys) || root === null || typeof root !== "object") return root;
658
+ if (!keys.length || !safe || root === null || typeof root !== "object") return root;
524
659
  let current = root;
525
660
  for (let i = 0; i < keys.length - 1; i++) {
526
661
  current = current?.[keys[i]];
527
662
  if (current === null || typeof current !== "object") return root;
528
663
  }
529
664
  const lastKey = keys[keys.length - 1];
530
- if (Array.isArray(current) && typeof lastKey === "number") current.splice(lastKey, 1);
665
+ if (Array.isArray(current) && (typeof lastKey === "number" || /^\d+$/.test(lastKey))) current.splice(Number(lastKey), 1);
531
666
  else delete current[lastKey];
532
667
  return root;
533
668
  }
@@ -545,13 +680,13 @@ function unset(object, path) {
545
680
  * @returns {boolean}
546
681
  */
547
682
  function has(object, path) {
548
- const keys = _toPath(path);
549
- if (!keys.length || !_isSafePath(keys)) return false;
683
+ const { keys, safe } = _parsePath(path);
684
+ if (!keys.length || !safe) return false;
550
685
  /** @type {any} */
551
686
  let current = object;
552
- for (const key of keys) {
553
- if (current === null || current === undefined || !Object.prototype.hasOwnProperty.call(Object(current), key)) return false;
554
- current = current[key];
687
+ for (let i = 0; i < keys.length; i++) {
688
+ if (current === null || current === undefined || !Object.prototype.hasOwnProperty.call(Object(current), keys[i])) return false;
689
+ current = current[keys[i]];
555
690
  }
556
691
  return true;
557
692
  }
@@ -571,8 +706,9 @@ function has(object, path) {
571
706
  function pick(object, keys) {
572
707
  /** @type {any} */
573
708
  const out = {};
709
+ if (object === null || object === undefined) return out;
574
710
  for (const key of keys) {
575
- if (object != null && Object.prototype.hasOwnProperty.call(object, key)) out[key] = object[key];
711
+ if (Object.prototype.hasOwnProperty.call(object, key) && !_UNSAFE.has(/** @type {any} */ (key))) out[key] = object[key];
576
712
  }
577
713
  return out;
578
714
  }
@@ -594,7 +730,7 @@ function omit(object, keys) {
594
730
  /** @type {any} */
595
731
  const out = {};
596
732
  for (const key of Object.keys(object ?? {})) {
597
- if (!exclude.has(key)) out[key] = /** @type {any} */ (object)[key];
733
+ if (!exclude.has(key) && !_UNSAFE.has(key)) out[key] = /** @type {any} */ (object)[key];
598
734
  }
599
735
  return out;
600
736
  }
@@ -613,7 +749,9 @@ function omit(object, keys) {
613
749
  function filter(object, predicate) {
614
750
  /** @type {any} */
615
751
  const out = {};
616
- for (const [key, value] of Object.entries(object ?? {})) {
752
+ for (const key of Object.keys(object ?? {})) {
753
+ if (_UNSAFE.has(key)) continue;
754
+ const value = /** @type {any} */ (object)[key];
617
755
  if (predicate(value, /** @type {keyof T & string} */ (key))) out[key] = value;
618
756
  }
619
757
  return out;
@@ -634,7 +772,9 @@ function filter(object, predicate) {
634
772
  function mapValues(object, fn) {
635
773
  /** @type {any} */
636
774
  const out = {};
637
- for (const [key, value] of Object.entries(object ?? {})) out[key] = fn(value, /** @type {keyof T & string} */ (key));
775
+ for (const key of Object.keys(object ?? {})) {
776
+ if (!_UNSAFE.has(key)) out[key] = fn(/** @type {any} */ (object)[key], /** @type {keyof T & string} */ (key));
777
+ }
638
778
  return out;
639
779
  }
640
780
 
@@ -724,6 +864,7 @@ function compact(object, options = {}) {
724
864
  /** @type {any} */
725
865
  const out = {};
726
866
  for (const [key, value] of Object.entries(object ?? {})) {
867
+ if (_UNSAFE.has(key)) continue;
727
868
  const cleaned = clean(value);
728
869
  if (!isEmptyValue(cleaned)) out[key] = cleaned;
729
870
  }
@@ -743,14 +884,33 @@ function compact(object, options = {}) {
743
884
  function flatten(object, prefix = "") {
744
885
  /** @type {Record<string, unknown>} */
745
886
  const out = {};
746
- for (const [key, value] of Object.entries(object ?? {})) {
747
- const path = prefix ? `${prefix}.${key}` : key;
748
- if (_isPlain(value) && Object.keys(/** @type {object} */ (value)).length) Object.assign(out, flatten(/** @type {object} */ (value), path));
749
- else out[path] = value;
750
- }
887
+ /** @type {(value: Record<string, unknown>, path: string) => void} */
888
+ const walk = (value, path) => {
889
+ for (const key of Object.keys(value)) {
890
+ if (_UNSAFE.has(key)) continue;
891
+ const child = value[key];
892
+ const next = path ? `${path}.${key}` : key;
893
+ if (_isPlain(child) && Object.keys(child).length) walk(child, next);
894
+ else out[next] = child;
895
+ }
896
+ };
897
+ walk(/** @type {any} */ (object ?? {}), prefix);
751
898
  return out;
752
899
  }
753
900
 
901
+ /**
902
+ * Every leaf path of an object, in dotted form. Arrays count as leaves.
903
+ *
904
+ * @example
905
+ * obj.paths({ db: { host: "x", port: 5432 }, tags: ["a"] }); // ["db.host", "db.port", "tags"]
906
+ *
907
+ * @param {object} object
908
+ * @returns {string[]}
909
+ */
910
+ function paths(object) {
911
+ return Object.keys(flatten(object));
912
+ }
913
+
754
914
  /**
755
915
  * Rebuilds nested objects from dotted keys. The opposite of `flatten()`.
756
916
  *
@@ -764,8 +924,8 @@ function unflatten(object) {
764
924
  /** @type {Record<string, any>} */
765
925
  const out = {};
766
926
  for (const [path, value] of Object.entries(object ?? {})) {
767
- const keys = _toPath(path);
768
- if (!keys.length || !_isSafePath(keys)) continue;
927
+ const { keys, safe } = _parsePath(path);
928
+ if (!keys.length || !safe) continue;
769
929
  let current = out;
770
930
  for (let i = 0; i < keys.length - 1; i++) {
771
931
  const key = keys[i];
@@ -844,13 +1004,22 @@ function size(value) {
844
1004
  }
845
1005
 
846
1006
  /**
847
- * Does the object have no own keys? `null` and `undefined` count as empty.
1007
+ * Is there nothing in it? No own keys for an object, no entries for a Map
1008
+ * or a Set. `null` and `undefined` count as empty; a date, a regular
1009
+ * expression and an error never do.
1010
+ *
1011
+ * @example
1012
+ * obj.isEmpty({}); // true
1013
+ * obj.isEmpty(new Map([["a", 1]])); // false
848
1014
  *
849
1015
  * @param {unknown} object
850
1016
  * @returns {boolean}
851
1017
  */
852
1018
  function isEmpty(object) {
853
- return object === null || object === undefined || Object.keys(object).length === 0;
1019
+ if (object === null || object === undefined) return true;
1020
+ if (types.isMap(object) || types.isSet(object)) return /** @type {Set<unknown>} */ (object).size === 0;
1021
+ if (types.isDate(object) || types.isRegExp(object) || types.isNativeError(object)) return false;
1022
+ return Object.keys(object).length === 0;
854
1023
  }
855
1024
 
856
1025
  /**
@@ -866,13 +1035,56 @@ function isEmpty(object) {
866
1035
  * @returns {DeepReadonly<T>} The same object.
867
1036
  */
868
1037
  function deepFreeze(object) {
869
- if (object !== null && (typeof object === "object" || typeof object === "function") && !Object.isFrozen(object)) {
870
- Object.freeze(object);
871
- for (const key of Reflect.ownKeys(/** @type {object} */ (object))) deepFreeze(/** @type {any} */ (object)[key]);
872
- }
1038
+ if (object === null || (typeof object !== "object" && typeof object !== "function") || Object.isFrozen(object)) return /** @type {DeepReadonly<T>} */ (object);
1039
+ // Typed arrays and buffers can't be frozen: their memory stays writable.
1040
+ if (ArrayBuffer.isView(object)) return /** @type {DeepReadonly<T>} */ (object);
1041
+ Object.freeze(object);
1042
+ for (const key of Reflect.ownKeys(/** @type {object} */ (object))) deepFreeze(/** @type {any} */ (object)[key]);
873
1043
  return /** @type {DeepReadonly<T>} */ (object);
874
1044
  }
875
1045
 
1046
+ /**
1047
+ * A copy with its keys in order, nested objects included. Two objects with
1048
+ * the same content then give the same JSON, which makes stable hashes,
1049
+ * cache keys and readable diffs.
1050
+ *
1051
+ * @example
1052
+ * obj.sortKeys({ b: 1, a: { d: 1, c: 2 } }); // { a: { c: 2, d: 1 }, b: 1 }
1053
+ * nc.crypto.hash(JSON.stringify(obj.sortKeys(payload)));
1054
+ *
1055
+ * @template T
1056
+ * @param {T} object
1057
+ * @param {SortKeysOptions} [options]
1058
+ * @returns {T}
1059
+ */
1060
+ function sortKeys(object, options = {}) {
1061
+ const deep = options.deep ?? true;
1062
+ /** @type {object[]} */
1063
+ const ancestors = [];
1064
+ /** @type {(value: any) => any} */
1065
+ const walk = (value) => {
1066
+ if (value === null || typeof value !== "object" || ancestors.includes(value)) return value;
1067
+ const isArray = Array.isArray(value);
1068
+ if (!isArray && !_isPlain(value)) return value;
1069
+ ancestors.push(value);
1070
+ /** @type {any} */
1071
+ let out;
1072
+ if (isArray) out = deep ? value.map(walk) : value;
1073
+ else {
1074
+ out = {};
1075
+ for (const key of Object.keys(value).sort(options.compare)) {
1076
+ if (!_UNSAFE.has(key)) out[key] = deep ? walk(value[key]) : value[key];
1077
+ }
1078
+ }
1079
+ ancestors.pop();
1080
+ return out;
1081
+ };
1082
+ return walk(object);
1083
+ }
1084
+
1085
+ /** @type {MergeOptions} */
1086
+ const _NO_OPTIONS = {};
1087
+
876
1088
  module.exports = {
877
1089
  clone,
878
1090
  merge,
@@ -883,6 +1095,7 @@ module.exports = {
883
1095
  // not shorthand: Node's ES module export detection stops at a bare `get`
884
1096
  get: get,
885
1097
  set,
1098
+ update,
886
1099
  unset,
887
1100
  has,
888
1101
  pick,
@@ -895,6 +1108,8 @@ module.exports = {
895
1108
  compact,
896
1109
  flatten,
897
1110
  unflatten,
1111
+ paths,
1112
+ sortKeys,
898
1113
  keys,
899
1114
  entries,
900
1115
  fromEntries,