@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.
- package/CHANGELOG.md +150 -0
- package/README.md +32 -30
- package/index.js +3 -3
- package/package.json +8 -7
- package/src/Arr.js +222 -58
- package/src/Async.js +322 -90
- package/src/Cache.js +148 -44
- package/src/Checker.js +257 -26
- package/src/Cli.js +404 -145
- package/src/Color.js +224 -83
- package/src/Crypto.js +481 -106
- package/src/Emitter.js +104 -37
- package/src/Env.js +176 -42
- package/src/FS.js +203 -60
- package/src/Func.js +226 -76
- package/src/Http.js +311 -149
- package/src/Id.js +281 -96
- package/src/Logger.js +302 -78
- package/src/Num.js +182 -60
- package/src/Obj.js +340 -125
- package/src/SQLite.js +426 -105
- package/src/Schema.js +602 -87
- package/src/Str.js +316 -86
- package/src/Sys.js +197 -54
- package/src/Time.js +572 -317
- package/src/Utils.js +62 -41
- package/src/errors.js +54 -11
- package/src/internal/shared.js +415 -0
- package/types/index.d.ts +59 -17
- package/types/src/Arr.d.ts +78 -0
- package/types/src/Arr.d.ts.map +1 -1
- package/types/src/Async.d.ts +52 -1
- package/types/src/Async.d.ts.map +1 -1
- package/types/src/Cache.d.ts +33 -7
- package/types/src/Cache.d.ts.map +1 -1
- package/types/src/Checker.d.ts +155 -2
- package/types/src/Checker.d.ts.map +1 -1
- package/types/src/Cli.d.ts +97 -15
- package/types/src/Cli.d.ts.map +1 -1
- package/types/src/Color.d.ts +7 -1
- package/types/src/Color.d.ts.map +1 -1
- package/types/src/Crypto.d.ts +99 -13
- package/types/src/Crypto.d.ts.map +1 -1
- package/types/src/Emitter.d.ts +36 -17
- package/types/src/Emitter.d.ts.map +1 -1
- package/types/src/Env.d.ts +31 -1
- package/types/src/Env.d.ts.map +1 -1
- package/types/src/FS.d.ts +36 -5
- package/types/src/FS.d.ts.map +1 -1
- package/types/src/Func.d.ts +147 -19
- package/types/src/Func.d.ts.map +1 -1
- package/types/src/Http.d.ts +113 -43
- package/types/src/Http.d.ts.map +1 -1
- package/types/src/Id.d.ts +60 -8
- package/types/src/Id.d.ts.map +1 -1
- package/types/src/Logger.d.ts +53 -2
- package/types/src/Logger.d.ts.map +1 -1
- package/types/src/Num.d.ts +48 -7
- package/types/src/Num.d.ts.map +1 -1
- package/types/src/Obj.d.ts +67 -4
- package/types/src/Obj.d.ts.map +1 -1
- package/types/src/SQLite.d.ts +123 -14
- package/types/src/SQLite.d.ts.map +1 -1
- package/types/src/Schema.d.ts +256 -7
- package/types/src/Schema.d.ts.map +1 -1
- package/types/src/Str.d.ts +49 -1
- package/types/src/Str.d.ts.map +1 -1
- package/types/src/Sys.d.ts +51 -6
- package/types/src/Sys.d.ts.map +1 -1
- package/types/src/Time.d.ts +130 -34
- package/types/src/Time.d.ts.map +1 -1
- package/types/src/Utils.d.ts +15 -11
- package/types/src/Utils.d.ts.map +1 -1
- package/types/src/errors.d.ts +27 -3
- package/types/src/errors.d.ts.map +1 -1
- package/types/src/internal/shared.d.ts +123 -0
- 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 =
|
|
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
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
return
|
|
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 {
|
|
107
|
-
*/
|
|
108
|
-
const
|
|
109
|
-
if (Array.isArray(path)) return path;
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
.
|
|
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
|
|
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 {
|
|
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
|
-
|
|
129
|
-
if (
|
|
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
|
-
|
|
147
|
+
/** @type {any[]} */
|
|
148
|
+
const out = new Array(value.length);
|
|
133
149
|
seen.set(value, out);
|
|
134
|
-
for (
|
|
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)
|
|
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
|
-
|
|
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
|
-
*
|
|
165
|
-
*
|
|
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
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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 =
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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
|
|
244
|
+
return target;
|
|
189
245
|
};
|
|
190
246
|
|
|
247
|
+
|
|
191
248
|
/**
|
|
192
|
-
* Deep copy.
|
|
193
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
422
|
+
* @param {object[]} stackA
|
|
423
|
+
* @param {object[]} stackB
|
|
344
424
|
* @returns {boolean}
|
|
345
425
|
*/
|
|
346
|
-
function _equal(a, b,
|
|
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
|
-
|
|
350
|
-
if (
|
|
351
|
-
|
|
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 (
|
|
356
|
-
|
|
357
|
-
const
|
|
358
|
-
|
|
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),
|
|
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,
|
|
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,
|
|
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
|
-
|
|
382
|
-
|
|
383
|
-
if (
|
|
384
|
-
|
|
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 {
|
|
406
|
-
const
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
if (
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
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 =
|
|
466
|
-
if (!
|
|
582
|
+
const { keys, safe } = _parsePath(path);
|
|
583
|
+
if (!safe) return fallback;
|
|
467
584
|
/** @type {any} */
|
|
468
585
|
let current = object;
|
|
469
|
-
for (
|
|
586
|
+
for (let i = 0; i < keys.length; i++) {
|
|
470
587
|
if (current === null || current === undefined) return fallback;
|
|
471
|
-
current = current[
|
|
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 =
|
|
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 || !
|
|
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 =
|
|
655
|
+
const { keys, safe } = _parsePath(path);
|
|
521
656
|
/** @type {any} */
|
|
522
657
|
const root = clone(object);
|
|
523
|
-
if (!keys.length || !
|
|
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 =
|
|
549
|
-
if (!keys.length || !
|
|
683
|
+
const { keys, safe } = _parsePath(path);
|
|
684
|
+
if (!keys.length || !safe) return false;
|
|
550
685
|
/** @type {any} */
|
|
551
686
|
let current = object;
|
|
552
|
-
for (
|
|
553
|
-
if (current === null || current === undefined || !Object.prototype.hasOwnProperty.call(Object(current),
|
|
554
|
-
current = current[
|
|
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 (
|
|
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
|
|
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
|
|
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
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
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 =
|
|
768
|
-
if (!keys.length || !
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
|
870
|
-
|
|
871
|
-
|
|
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,
|