@ix-xs/node-comfort 2.1.1 → 2.2.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 (68) hide show
  1. package/CHANGELOG.md +135 -0
  2. package/README.md +2 -2
  3. package/package.json +2 -2
  4. package/src/Arr.js +13 -5
  5. package/src/Checker.js +56 -8
  6. package/src/Cli.js +60 -18
  7. package/src/Color.js +32 -6
  8. package/src/Crypto.js +429 -78
  9. package/src/Emitter.js +9 -9
  10. package/src/Env.js +90 -8
  11. package/src/FS.js +366 -56
  12. package/src/Http.js +263 -72
  13. package/src/Id.js +53 -15
  14. package/src/Logger.js +100 -22
  15. package/src/Num.js +2 -1
  16. package/src/Obj.js +225 -36
  17. package/src/SQLite.js +154 -97
  18. package/src/Schema.js +281 -40
  19. package/src/Str.js +77 -29
  20. package/src/Sys.js +123 -22
  21. package/src/Time.js +96 -22
  22. package/src/Utils.js +18 -3
  23. package/src/errors.js +75 -4
  24. package/src/internal/shared.js +68 -14
  25. package/src/internal/terminal.js +49 -0
  26. package/types/index.d.ts +28 -5
  27. package/types/src/Arr.d.ts +4 -2
  28. package/types/src/Arr.d.ts.map +1 -1
  29. package/types/src/Checker.d.ts +29 -3
  30. package/types/src/Checker.d.ts.map +1 -1
  31. package/types/src/Cli.d.ts +6 -4
  32. package/types/src/Cli.d.ts.map +1 -1
  33. package/types/src/Color.d.ts +2 -2
  34. package/types/src/Color.d.ts.map +1 -1
  35. package/types/src/Crypto.d.ts +126 -28
  36. package/types/src/Crypto.d.ts.map +1 -1
  37. package/types/src/Emitter.d.ts +6 -6
  38. package/types/src/Emitter.d.ts.map +1 -1
  39. package/types/src/Env.d.ts +19 -1
  40. package/types/src/Env.d.ts.map +1 -1
  41. package/types/src/FS.d.ts +27 -20
  42. package/types/src/FS.d.ts.map +1 -1
  43. package/types/src/Http.d.ts +42 -5
  44. package/types/src/Http.d.ts.map +1 -1
  45. package/types/src/Id.d.ts.map +1 -1
  46. package/types/src/Logger.d.ts +11 -3
  47. package/types/src/Logger.d.ts.map +1 -1
  48. package/types/src/Num.d.ts.map +1 -1
  49. package/types/src/Obj.d.ts +15 -2
  50. package/types/src/Obj.d.ts.map +1 -1
  51. package/types/src/SQLite.d.ts +104 -12
  52. package/types/src/SQLite.d.ts.map +1 -1
  53. package/types/src/Schema.d.ts +61 -5
  54. package/types/src/Schema.d.ts.map +1 -1
  55. package/types/src/Str.d.ts +32 -7
  56. package/types/src/Str.d.ts.map +1 -1
  57. package/types/src/Sys.d.ts +42 -9
  58. package/types/src/Sys.d.ts.map +1 -1
  59. package/types/src/Time.d.ts +10 -5
  60. package/types/src/Time.d.ts.map +1 -1
  61. package/types/src/Utils.d.ts +16 -6
  62. package/types/src/Utils.d.ts.map +1 -1
  63. package/types/src/errors.d.ts +6 -2
  64. package/types/src/errors.d.ts.map +1 -1
  65. package/types/src/internal/shared.d.ts +7 -0
  66. package/types/src/internal/shared.d.ts.map +1 -1
  67. package/types/src/internal/terminal.d.ts +13 -0
  68. package/types/src/internal/terminal.d.ts.map +1 -0
package/src/Obj.js CHANGED
@@ -102,15 +102,84 @@ const _NOT_FOUND = Symbol("not-found");
102
102
 
103
103
  const _isPlain = shared.isPlainObject;
104
104
 
105
+ /**
106
+ * What to throw when a walk failed: running out of stack on data nested
107
+ * thousands of levels deep becomes an error that says so.
108
+ * @param {unknown} error
109
+ * @param {string} name
110
+ * @returns {unknown}
111
+ */
112
+ const _nestedError = (error, name) =>
113
+ error instanceof RangeError && /call stack/i.test(error.message) ? new RangeError(`obj.${name}(): the object is nested too deeply`, { cause: error }) : error;
114
+
105
115
  /** @param {ReadonlyArray<string | number>} keys */
106
116
  const _isSafePath = (keys) => {
107
117
  for (let i = 0; i < keys.length; i++) if (_UNSAFE.has(String(keys[i]))) return false;
108
118
  return true;
109
119
  };
110
120
 
121
+ /** After this many missing items in a row, an array is walked by its keys. */
122
+ const _MAX_HOLES = 1024;
123
+
124
+ /**
125
+ * The indexes an array holds after `from`, in order.
126
+ * @param {readonly unknown[]} array
127
+ * @param {number} from
128
+ * @returns {number[]}
129
+ */
130
+ const _indexesAfter = (array, from) => {
131
+ /** @type {number[]} */
132
+ const out = [];
133
+ for (const key of Object.keys(array)) {
134
+ const index = Number(key);
135
+ if (index > from && String(index) === key) out.push(index);
136
+ }
137
+ return out;
138
+ };
139
+
140
+ /**
141
+ * An array to fill up to `length`. Past `_MAX_INDEX` it starts empty: it
142
+ * may be sparse, and allocating every slot up front would cost far more
143
+ * than its content.
144
+ * @param {number} length
145
+ * @returns {any[]}
146
+ */
147
+ const _newArray = (length) => (length > _MAX_INDEX ? [] : new Array(length));
148
+
149
+ /**
150
+ * Calls `fn` on each item an array holds, in order, skipping holes. A
151
+ * sparse array is walked by its keys rather than by every index.
152
+ * @param {readonly unknown[]} array
153
+ * @param {(item: any, index: number) => void} fn
154
+ */
155
+ const _forEachItem = (array, fn) => {
156
+ let holes = 0;
157
+ for (let i = 0; i < array.length; i++) {
158
+ if (i in array) {
159
+ fn(array[i], i);
160
+ holes = 0;
161
+ } else if (++holes > _MAX_HOLES) {
162
+ for (const index of _indexesAfter(array, i)) fn(array[index], index);
163
+ return;
164
+ }
165
+ }
166
+ };
167
+
111
168
  /** @type {Map<string, { keys: Array<string | number>, safe: boolean }>} */
112
169
  const _paths = new Map();
113
170
 
171
+ /** Longer paths are not remembered: they come from data, not from code. */
172
+ const _MAX_CACHED_PATH = 256;
173
+
174
+ /** An array index up to this creates an array; a larger one creates an object. */
175
+ const _MAX_INDEX = 10_000;
176
+
177
+ /**
178
+ * Does the key after this one ask for an array?
179
+ * @param {string | number} next
180
+ */
181
+ const _wantsArray = (next) => (typeof next === "number" ? Number.isInteger(next) && next >= 0 && next <= _MAX_INDEX : typeof next === "string" && /^\d{1,5}$/.test(next) && Number(next) <= _MAX_INDEX);
182
+
114
183
  /**
115
184
  * Parses `"a.b[0].c"` or `["a", "b", 0, "c"]` into keys, and says whether
116
185
  * the path stays clear of prototypes. Parsed strings are remembered: paths
@@ -119,15 +188,33 @@ const _paths = new Map();
119
188
  * @returns {{ keys: ReadonlyArray<string | number>, safe: boolean }}
120
189
  */
121
190
  const _parsePath = (path) => {
122
- if (Array.isArray(path)) return { keys: path, safe: _isSafePath(path) };
191
+ if (Array.isArray(path)) {
192
+ // Each key is read and converted once: an object whose toString() changes
193
+ // can't pass the check with one name and be used with another.
194
+ /** @type {Array<string | number>} */
195
+ const keys = new Array(path.length);
196
+ let safe = true;
197
+ for (let i = 0; i < keys.length; i++) {
198
+ const key = path[i];
199
+ if (typeof key === "number" || typeof key === "symbol") keys[i] = /** @type {any} */ (key);
200
+ else {
201
+ const name = String(key);
202
+ if (_UNSAFE.has(name)) safe = false;
203
+ keys[i] = name;
204
+ }
205
+ }
206
+ return { keys, safe };
207
+ }
123
208
  const text = String(path);
124
209
  let parsed = _paths.get(text);
125
210
  if (parsed === undefined) {
126
- const dotted = text.includes("[") ? text.replace(/\[(\d+)\]/g, ".$1").replace(/\[["']?([^\]"']+)["']?\]/g, ".$1") : text;
211
+ const dotted = text.includes("[") ? text.replace(/\[(\d+)\]/g, ".$1").replace(/\[["']?([^\]"'[]+)["']?\]/g, ".$1") : text;
127
212
  const keys = dotted.includes(".") ? dotted.split(".").filter((part) => part !== "") : dotted === "" ? [] : [dotted];
128
213
  parsed = { keys, safe: _isSafePath(keys) };
129
- if (_paths.size >= 1000) _paths.clear();
130
- _paths.set(text, parsed);
214
+ if (text.length <= _MAX_CACHED_PATH) {
215
+ if (_paths.size >= 1000) _paths.clear();
216
+ _paths.set(text, parsed);
217
+ }
131
218
  }
132
219
  return parsed;
133
220
  };
@@ -145,9 +232,20 @@ const _deepClone = (value, seen) => {
145
232
  if (known !== undefined) return known;
146
233
  if (Array.isArray(value)) {
147
234
  /** @type {any[]} */
148
- const out = new Array(value.length);
235
+ const out = _newArray(value.length);
149
236
  seen.set(value, out);
150
- for (let i = 0; i < value.length; i++) if (i in value) out[i] = _deepClone(value[i], seen);
237
+ let holes = 0;
238
+ for (let i = 0; i < value.length; i++) {
239
+ if (i in value) {
240
+ out[i] = _deepClone(value[i], seen);
241
+ holes = 0;
242
+ } else if (++holes > _MAX_HOLES) {
243
+ // A sparse array, like one with only [4000000000] set: copy what it holds rather than walk every index.
244
+ for (const index of _indexesAfter(value, i)) out[index] = _deepClone(value[index], seen);
245
+ break;
246
+ }
247
+ }
248
+ if (out.length !== value.length) out.length = value.length;
151
249
  return out;
152
250
  }
153
251
  const proto = Object.getPrototypeOf(value);
@@ -257,10 +355,15 @@ const _mergeInto = (target, source, options) => {
257
355
  * @template T
258
356
  * @param {T} value
259
357
  * @returns {T}
358
+ * @throws {RangeError} If the value is nested too deeply to walk, thousands of levels.
260
359
  */
261
360
  function clone(value) {
262
361
  if (value === null || typeof value !== "object") return value;
263
- return _deepClone(value, new Map());
362
+ try {
363
+ return _deepClone(value, new Map());
364
+ } catch (error) {
365
+ throw _nestedError(error, "clone");
366
+ }
264
367
  }
265
368
 
266
369
  /**
@@ -304,6 +407,7 @@ function clone(value) {
304
407
  * @overload
305
408
  * @param {...object} sources
306
409
  * @returns {Record<string, any>}
410
+ * @throws {RangeError} If an object is nested too deeply to walk, thousands of levels.
307
411
  */
308
412
  /**
309
413
  * @param {...object} sources
@@ -312,7 +416,11 @@ function clone(value) {
312
416
  function merge(...sources) {
313
417
  /** @type {Record<string, any>} */
314
418
  const out = {};
315
- for (const source of sources) if (_isPlain(source)) _mergeInto(out, source, _NO_OPTIONS);
419
+ try {
420
+ for (const source of sources) if (_isPlain(source)) _mergeInto(out, source, _NO_OPTIONS);
421
+ } catch (error) {
422
+ throw _nestedError(error, "merge");
423
+ }
316
424
  return out;
317
425
  }
318
426
 
@@ -328,11 +436,16 @@ function merge(...sources) {
328
436
  * @param {MergeOptions} options
329
437
  * @param {...object} sources
330
438
  * @returns {Record<string, any>}
439
+ * @throws {RangeError} If an object is nested too deeply to walk, thousands of levels.
331
440
  */
332
441
  function mergeWith(options, ...sources) {
333
442
  /** @type {Record<string, any>} */
334
443
  const out = {};
335
- for (const source of sources) if (_isPlain(source)) _mergeInto(out, source, options ?? _NO_OPTIONS);
444
+ try {
445
+ for (const source of sources) if (_isPlain(source)) _mergeInto(out, source, options ?? _NO_OPTIONS);
446
+ } catch (error) {
447
+ throw _nestedError(error, "mergeWith");
448
+ }
336
449
  return out;
337
450
  }
338
451
 
@@ -349,6 +462,7 @@ function mergeWith(options, ...sources) {
349
462
  * @param {T} target
350
463
  * @param {...D} sources The first one has the highest priority.
351
464
  * @returns {T & D}
465
+ * @throws {RangeError} If an object is nested too deeply to walk, thousands of levels.
352
466
  */
353
467
  function defaults(target, ...sources) {
354
468
  /** @type {(t: any, s: any) => any} */
@@ -361,7 +475,11 @@ function defaults(target, ...sources) {
361
475
  }
362
476
  return out;
363
477
  };
364
- return sources.reduce((acc, source) => (_isPlain(source) ? fill(acc, source) : acc), /** @type {any} */ (clone(target)));
478
+ try {
479
+ return sources.reduce((acc, source) => (_isPlain(source) ? fill(acc, source) : acc), /** @type {any} */ (clone(target)));
480
+ } catch (error) {
481
+ throw _nestedError(error, "defaults");
482
+ }
365
483
  }
366
484
 
367
485
  /**
@@ -376,11 +494,16 @@ function defaults(target, ...sources) {
376
494
  * @param {unknown} a
377
495
  * @param {unknown} b
378
496
  * @returns {boolean}
497
+ * @throws {RangeError} If the values are nested too deeply to walk, thousands of levels.
379
498
  */
380
499
  function equal(a, b) {
381
500
  if (Object.is(a, b)) return true;
382
501
  if (typeof a !== "object" || typeof b !== "object" || a === null || b === null) return false;
383
- return _equal(a, b, [], []);
502
+ try {
503
+ return _equal(a, b, [], []);
504
+ } catch (error) {
505
+ throw _nestedError(error, "equal");
506
+ }
384
507
  }
385
508
 
386
509
  /**
@@ -448,7 +571,19 @@ function _equal(a, b, stackA, stackB) {
448
571
  const _equalObjects = (a, b, proto, stackA, stackB) => {
449
572
  if (Array.isArray(a)) {
450
573
  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;
574
+ let holes = 0;
575
+ for (let i = 0; i < a.length; i++) {
576
+ const x = a[i];
577
+ const y = b[i];
578
+ if (x === undefined && y === undefined && !(i in a) && !(i in b)) {
579
+ if (++holes <= _MAX_HOLES) continue;
580
+ // Sparse on both sides: compare what they hold rather than every index.
581
+ for (const index of [..._indexesAfter(a, i), ..._indexesAfter(b, i)]) if (!_equal(a[index], b[index], stackA, stackB)) return false;
582
+ return true;
583
+ }
584
+ holes = 0;
585
+ if (!_equal(x, y, stackA, stackB)) return false;
586
+ }
452
587
  return true;
453
588
  }
454
589
  if (proto === Object.prototype || proto === null) return _equalKeys(a, b, stackA, stackB);
@@ -504,6 +639,7 @@ const _equalObjects = (a, b, proto, stackA, stackB) => {
504
639
  * @param {unknown} before
505
640
  * @param {unknown} after
506
641
  * @returns {Change[]} Empty when nothing changed.
642
+ * @throws {RangeError} If the values are nested too deeply to walk, thousands of levels.
507
643
  */
508
644
  function diff(before, after) {
509
645
  /** @type {Change[]} */
@@ -538,13 +674,18 @@ function diff(before, after) {
538
674
  }
539
675
  if (!equal(a, b)) changes.push({ path: [...path], type: "changed", from: a, to: b });
540
676
  };
541
- walk(before, after);
677
+ try {
678
+ walk(before, after);
679
+ } catch (error) {
680
+ throw _nestedError(error, "diff");
681
+ }
542
682
  return changes;
543
683
  }
544
684
 
545
685
  /**
546
686
  * Reads a nested value from a dotted path. Your editor completes the paths
547
- * and knows the type of the result.
687
+ * and knows the type of the result. What a plain object inherits is not
688
+ * read: `obj.get({}, "toString")` is `undefined`.
548
689
  *
549
690
  * @example
550
691
  * obj.get(config, "db.pool.max"); // number | undefined
@@ -585,14 +726,18 @@ function get(object, path, fallback) {
585
726
  let current = object;
586
727
  for (let i = 0; i < keys.length; i++) {
587
728
  if (current === null || current === undefined) return fallback;
588
- current = current[keys[i]];
729
+ const next = current[keys[i]];
730
+ // What a plain object inherits, like toString, is not data. Class instances keep their getters and methods.
731
+ if (typeof next === "function" && !Object.hasOwn(current, keys[i]) && Object.getPrototypeOf(current) === Object.prototype) return fallback;
732
+ current = next;
589
733
  }
590
734
  return current === undefined ? fallback : current;
591
735
  }
592
736
 
593
737
  /**
594
738
  * Returns a copy with a nested value set. Missing levels are created
595
- * (arrays when the next key is a number).
739
+ * (arrays when the next key is a number up to 10000, objects otherwise, so
740
+ * a key like `"a.4000000000"` can't make a huge array).
596
741
  *
597
742
  * @example
598
743
  * obj.set({}, "a.b.c", 1); // { a: { b: { c: 1 } } }
@@ -614,7 +759,7 @@ function set(object, path, value) {
614
759
  const key = keys[i];
615
760
  const next = keys[i + 1];
616
761
  if (current[key] === null || typeof current[key] !== "object") {
617
- current[key] = typeof next === "number" || /^\d+$/.test(String(next)) ? [] : {};
762
+ current[key] = _wantsArray(next) ? [] : {};
618
763
  }
619
764
  current = current[key];
620
765
  }
@@ -846,6 +991,7 @@ function invert(object) {
846
991
  * @param {T} object
847
992
  * @param {CompactOptions} [options]
848
993
  * @returns {Partial<T>}
994
+ * @throws {RangeError} With `deep`, if the object is nested too deeply to walk, thousands of levels.
849
995
  */
850
996
  function compact(object, options = {}) {
851
997
  /** @type {(value: any) => boolean} */
@@ -857,18 +1003,34 @@ function compact(object, options = {}) {
857
1003
  /** @type {(value: any) => any} */
858
1004
  const clean = (value) => {
859
1005
  if (!options.deep) return value;
860
- if (Array.isArray(value)) return value.map(clean).filter((v) => !isEmptyValue(v));
861
- if (_isPlain(value)) return compact(value, options);
1006
+ if (Array.isArray(value)) {
1007
+ /** @type {any[]} */
1008
+ const kept = [];
1009
+ _forEachItem(value, (item) => {
1010
+ const cleaned = clean(item);
1011
+ if (!isEmptyValue(cleaned)) kept.push(cleaned);
1012
+ });
1013
+ return kept;
1014
+ }
1015
+ if (_isPlain(value)) return compactObject(value);
862
1016
  return value;
863
1017
  };
864
- /** @type {any} */
865
- const out = {};
866
- for (const [key, value] of Object.entries(object ?? {})) {
867
- if (_UNSAFE.has(key)) continue;
868
- const cleaned = clean(value);
869
- if (!isEmptyValue(cleaned)) out[key] = cleaned;
1018
+ /** @type {(source: any) => any} */
1019
+ const compactObject = (source) => {
1020
+ /** @type {any} */
1021
+ const out = {};
1022
+ for (const [key, value] of Object.entries(source ?? {})) {
1023
+ if (_UNSAFE.has(key)) continue;
1024
+ const cleaned = clean(value);
1025
+ if (!isEmptyValue(cleaned)) out[key] = cleaned;
1026
+ }
1027
+ return out;
1028
+ };
1029
+ try {
1030
+ return compactObject(object);
1031
+ } catch (error) {
1032
+ throw _nestedError(error, "compact");
870
1033
  }
871
- return out;
872
1034
  }
873
1035
 
874
1036
  /**
@@ -880,6 +1042,7 @@ function compact(object, options = {}) {
880
1042
  * @param {object} object
881
1043
  * @param {string} [prefix=""] Added in front of every key.
882
1044
  * @returns {Record<string, unknown>}
1045
+ * @throws {RangeError} If the object is nested too deeply to walk, thousands of levels.
883
1046
  */
884
1047
  function flatten(object, prefix = "") {
885
1048
  /** @type {Record<string, unknown>} */
@@ -894,7 +1057,11 @@ function flatten(object, prefix = "") {
894
1057
  else out[next] = child;
895
1058
  }
896
1059
  };
897
- walk(/** @type {any} */ (object ?? {}), prefix);
1060
+ try {
1061
+ walk(/** @type {any} */ (object ?? {}), prefix);
1062
+ } catch (error) {
1063
+ throw _nestedError(error, "flatten");
1064
+ }
898
1065
  return out;
899
1066
  }
900
1067
 
@@ -913,6 +1080,7 @@ function paths(object) {
913
1080
 
914
1081
  /**
915
1082
  * Rebuilds nested objects from dotted keys. The opposite of `flatten()`.
1083
+ * A number key up to 10000 makes an array, as in `set()`.
916
1084
  *
917
1085
  * @example
918
1086
  * obj.unflatten({ "db.host": "x", "db.port": 5432 }); // { db: { host: "x", port: 5432 } }
@@ -930,7 +1098,7 @@ function unflatten(object) {
930
1098
  for (let i = 0; i < keys.length - 1; i++) {
931
1099
  const key = keys[i];
932
1100
  if (current[key] === null || typeof current[key] !== "object") {
933
- current[key] = /^\d+$/.test(String(keys[i + 1])) ? [] : {};
1101
+ current[key] = _wantsArray(keys[i + 1]) ? [] : {};
934
1102
  }
935
1103
  current = current[key];
936
1104
  }
@@ -1033,16 +1201,26 @@ function isEmpty(object) {
1033
1201
  * @template T
1034
1202
  * @param {T} object
1035
1203
  * @returns {DeepReadonly<T>} The same object.
1204
+ * @throws {RangeError} If the object is nested too deeply to walk, thousands of levels. What was reached by then stays frozen.
1036
1205
  */
1037
1206
  function deepFreeze(object) {
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]);
1207
+ try {
1208
+ _freeze(object);
1209
+ } catch (error) {
1210
+ throw _nestedError(error, "deepFreeze");
1211
+ }
1043
1212
  return /** @type {DeepReadonly<T>} */ (object);
1044
1213
  }
1045
1214
 
1215
+ /** @param {unknown} object */
1216
+ const _freeze = (object) => {
1217
+ if (object === null || (typeof object !== "object" && typeof object !== "function") || Object.isFrozen(object)) return;
1218
+ // Typed arrays and buffers can't be frozen: their memory stays writable.
1219
+ if (ArrayBuffer.isView(object)) return;
1220
+ Object.freeze(object);
1221
+ for (const key of Reflect.ownKeys(/** @type {object} */ (object))) _freeze(/** @type {any} */ (object)[key]);
1222
+ };
1223
+
1046
1224
  /**
1047
1225
  * A copy with its keys in order, nested objects included. Two objects with
1048
1226
  * the same content then give the same JSON, which makes stable hashes,
@@ -1056,6 +1234,7 @@ function deepFreeze(object) {
1056
1234
  * @param {T} object
1057
1235
  * @param {SortKeysOptions} [options]
1058
1236
  * @returns {T}
1237
+ * @throws {RangeError} If the object is nested too deeply to walk, thousands of levels.
1059
1238
  */
1060
1239
  function sortKeys(object, options = {}) {
1061
1240
  const deep = options.deep ?? true;
@@ -1069,8 +1248,14 @@ function sortKeys(object, options = {}) {
1069
1248
  ancestors.push(value);
1070
1249
  /** @type {any} */
1071
1250
  let out;
1072
- if (isArray) out = deep ? value.map(walk) : value;
1073
- else {
1251
+ if (isArray) {
1252
+ if (deep) {
1253
+ const copy = _newArray(value.length);
1254
+ _forEachItem(value, (item, i) => (copy[i] = walk(item)));
1255
+ copy.length = value.length;
1256
+ out = copy;
1257
+ } else out = value;
1258
+ } else {
1074
1259
  out = {};
1075
1260
  for (const key of Object.keys(value).sort(options.compare)) {
1076
1261
  if (!_UNSAFE.has(key)) out[key] = deep ? walk(value[key]) : value[key];
@@ -1079,7 +1264,11 @@ function sortKeys(object, options = {}) {
1079
1264
  ancestors.pop();
1080
1265
  return out;
1081
1266
  };
1082
- return walk(object);
1267
+ try {
1268
+ return walk(object);
1269
+ } catch (error) {
1270
+ throw _nestedError(error, "sortKeys");
1271
+ }
1083
1272
  }
1084
1273
 
1085
1274
  /** @type {MergeOptions} */