@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.
- package/CHANGELOG.md +135 -0
- package/README.md +2 -2
- package/package.json +2 -2
- package/src/Arr.js +13 -5
- package/src/Checker.js +56 -8
- package/src/Cli.js +60 -18
- package/src/Color.js +32 -6
- package/src/Crypto.js +429 -78
- package/src/Emitter.js +9 -9
- package/src/Env.js +90 -8
- package/src/FS.js +366 -56
- package/src/Http.js +263 -72
- package/src/Id.js +53 -15
- package/src/Logger.js +100 -22
- package/src/Num.js +2 -1
- package/src/Obj.js +225 -36
- package/src/SQLite.js +154 -97
- package/src/Schema.js +281 -40
- package/src/Str.js +77 -29
- package/src/Sys.js +123 -22
- package/src/Time.js +96 -22
- package/src/Utils.js +18 -3
- package/src/errors.js +75 -4
- package/src/internal/shared.js +68 -14
- package/src/internal/terminal.js +49 -0
- package/types/index.d.ts +28 -5
- package/types/src/Arr.d.ts +4 -2
- package/types/src/Arr.d.ts.map +1 -1
- package/types/src/Checker.d.ts +29 -3
- package/types/src/Checker.d.ts.map +1 -1
- package/types/src/Cli.d.ts +6 -4
- package/types/src/Cli.d.ts.map +1 -1
- package/types/src/Color.d.ts +2 -2
- package/types/src/Color.d.ts.map +1 -1
- package/types/src/Crypto.d.ts +126 -28
- package/types/src/Crypto.d.ts.map +1 -1
- package/types/src/Emitter.d.ts +6 -6
- package/types/src/Emitter.d.ts.map +1 -1
- package/types/src/Env.d.ts +19 -1
- package/types/src/Env.d.ts.map +1 -1
- package/types/src/FS.d.ts +27 -20
- package/types/src/FS.d.ts.map +1 -1
- package/types/src/Http.d.ts +42 -5
- package/types/src/Http.d.ts.map +1 -1
- package/types/src/Id.d.ts.map +1 -1
- package/types/src/Logger.d.ts +11 -3
- package/types/src/Logger.d.ts.map +1 -1
- package/types/src/Num.d.ts.map +1 -1
- package/types/src/Obj.d.ts +15 -2
- package/types/src/Obj.d.ts.map +1 -1
- package/types/src/SQLite.d.ts +104 -12
- package/types/src/SQLite.d.ts.map +1 -1
- package/types/src/Schema.d.ts +61 -5
- package/types/src/Schema.d.ts.map +1 -1
- package/types/src/Str.d.ts +32 -7
- package/types/src/Str.d.ts.map +1 -1
- package/types/src/Sys.d.ts +42 -9
- package/types/src/Sys.d.ts.map +1 -1
- package/types/src/Time.d.ts +10 -5
- package/types/src/Time.d.ts.map +1 -1
- package/types/src/Utils.d.ts +16 -6
- package/types/src/Utils.d.ts.map +1 -1
- package/types/src/errors.d.ts +6 -2
- package/types/src/errors.d.ts.map +1 -1
- package/types/src/internal/shared.d.ts +7 -0
- package/types/src/internal/shared.d.ts.map +1 -1
- package/types/src/internal/terminal.d.ts +13 -0
- 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))
|
|
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 (
|
|
130
|
-
|
|
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 =
|
|
235
|
+
const out = _newArray(value.length);
|
|
149
236
|
seen.set(value, out);
|
|
150
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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] =
|
|
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))
|
|
861
|
-
|
|
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
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
const
|
|
869
|
-
|
|
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
|
-
|
|
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] =
|
|
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
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
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)
|
|
1073
|
-
|
|
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
|
-
|
|
1267
|
+
try {
|
|
1268
|
+
return walk(object);
|
|
1269
|
+
} catch (error) {
|
|
1270
|
+
throw _nestedError(error, "sortKeys");
|
|
1271
|
+
}
|
|
1083
1272
|
}
|
|
1084
1273
|
|
|
1085
1274
|
/** @type {MergeOptions} */
|