@symbo.ls/utils 3.14.10 → 3.14.12

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/object.js ADDED
@@ -0,0 +1,859 @@
1
+ 'use strict'
2
+
3
+ import { window } from './globals.js'
4
+ import {
5
+ isFunction,
6
+ isObjectLike,
7
+ isObject,
8
+ isArray,
9
+ isString,
10
+ is
11
+ } from './types.js'
12
+ import { unstackArrayOfObjects } from './array.js'
13
+ import { stringIncludesAny } from './string.js'
14
+ import { isDOMNode } from './node.js'
15
+ import { METHODS_EXL } from './keys.js'
16
+
17
+ const ENV = process.env.NODE_ENV
18
+
19
+ const _startsWithDunder = (e) =>
20
+ e.charCodeAt(0) === 95 && e.charCodeAt(1) === 95
21
+
22
+ export const exec = (param, element, state, context) => {
23
+ if (isFunction(param)) {
24
+ if (!element) return
25
+ if (typeof param.call !== 'function') return param
26
+ return param.call(
27
+ element,
28
+ element,
29
+ state || element.state,
30
+ context || element.context
31
+ )
32
+ }
33
+ // If param is a non-function value and the context has handler-resolving
34
+ // plugins (e.g. funcql), try to resolve it into a callable function.
35
+ // This enables funcql schemas as property values (text, if, style, etc.).
36
+ // Only attempt for array/object params that could be schemas — skip
37
+ // primitives and DOM nodes to avoid overhead on normal DOMQL values.
38
+ if (
39
+ param != null &&
40
+ element?.context?.plugins &&
41
+ (isArray(param) || (isObject(param) && !isDOMNode(param)))
42
+ ) {
43
+ const plugins = element.context.plugins
44
+ for (const plugin of plugins) {
45
+ if (plugin.resolveHandler) {
46
+ const resolved = plugin.resolveHandler(param, element)
47
+ if (typeof resolved === 'function') {
48
+ return exec(resolved, element, state, context)
49
+ }
50
+ }
51
+ }
52
+ }
53
+ return param
54
+ }
55
+
56
+ export const map = (obj, extention, element) => {
57
+ for (const e in extention) {
58
+ obj[e] = exec(extention[e], element)
59
+ }
60
+ }
61
+
62
+ export const merge = (element, obj, excludeFrom = []) => {
63
+ const useSet = excludeFrom instanceof Set
64
+ for (const e in obj) {
65
+ if (!Object.prototype.hasOwnProperty.call(obj, e)) continue
66
+ if (_startsWithDunder(e)) continue
67
+ if (useSet ? excludeFrom.has(e) : excludeFrom.includes(e)) continue
68
+ if (element[e] === undefined) {
69
+ element[e] = obj[e]
70
+ }
71
+ }
72
+ return element
73
+ }
74
+
75
+ export const deepMerge = (element, extend, excludeFrom = METHODS_EXL) => {
76
+ return _deepMerge(element, extend, excludeFrom, null)
77
+ }
78
+
79
+ // Internal worker that carries an ancestors stack to break cycles. The stack
80
+ // is allocated lazily on first recursion so non-cyclic merges have zero
81
+ // overhead. We track only the (element, extend) pairs in the *current
82
+ // descent* — once a pair pops off the stack we forget it, so legitimate
83
+ // reuse of shared extend objects across siblings is unaffected.
84
+ //
85
+ // PROTOTYPE POLLUTION GUARD: dunder keys are already skipped via
86
+ // `_startsWithDunder`, but `constructor` and `prototype` are NOT dunder
87
+ // and would otherwise be merged through. JSON.parse('{"constructor":
88
+ // {"prototype": {"isAdmin": true}}}') passed as `extend` would walk into
89
+ // `element.constructor` (the real Object constructor), then recurse and
90
+ // set `element.constructor.prototype.isAdmin = true` — polluting every
91
+ // object globally. Drop those segments.
92
+ const _deepMerge = (element, extend, excludeFrom, ancestors) => {
93
+ if (element === extend) return element
94
+ if (ancestors) {
95
+ for (let i = 0; i < ancestors.length; i += 2) {
96
+ if (ancestors[i] === element && ancestors[i + 1] === extend) return element
97
+ }
98
+ }
99
+ const useSet = excludeFrom instanceof Set
100
+ for (const e in extend) {
101
+ if (!Object.prototype.hasOwnProperty.call(extend, e)) continue
102
+ if (_startsWithDunder(e)) continue
103
+ if (e === 'constructor' || e === 'prototype') continue
104
+ if (useSet ? excludeFrom.has(e) : excludeFrom.includes(e)) continue
105
+ const elementProp = element[e]
106
+ const extendProp = extend[e]
107
+ if (isObjectLike(elementProp) && isObjectLike(extendProp)) {
108
+ const stack = ancestors || []
109
+ stack.push(element, extend)
110
+ _deepMerge(elementProp, extendProp, excludeFrom, stack)
111
+ stack.length -= 2
112
+ } else if (elementProp === undefined) {
113
+ element[e] = extendProp
114
+ }
115
+ }
116
+ return element
117
+ }
118
+
119
+ export const clone = (obj, excludeFrom = []) => {
120
+ const useSet = excludeFrom instanceof Set
121
+ const o = {}
122
+ for (const prop in obj) {
123
+ if (!Object.prototype.hasOwnProperty.call(obj, prop)) continue
124
+ if (_startsWithDunder(prop)) continue
125
+ if (useSet ? excludeFrom.has(prop) : excludeFrom.includes(prop)) continue
126
+ o[prop] = obj[prop]
127
+ }
128
+ return o
129
+ }
130
+
131
+ /**
132
+ * Enhanced deep clone function that combines features from multiple implementations
133
+ * @param {any} obj - Object to clone
134
+ * @param {Object} options - Configuration options
135
+ * @param {string[]} options.exclude - Properties to exclude from cloning
136
+ * @param {boolean} options.cleanUndefined - Remove undefined values
137
+ * @param {boolean} options.cleanNull - Remove null values
138
+ * @param {Window} options.window - Window object for cross-frame cloning
139
+ * @param {WeakMap} options.visited - WeakMap for tracking circular references
140
+ * @param {boolean} options.handleExtends - Whether to handle 'extends' arrays specially
141
+ * @returns {any} Cloned object
142
+ */
143
+ export const deepClone = (obj, options = {}) => {
144
+ const {
145
+ exclude = [],
146
+ cleanUndefined = false,
147
+ cleanNull = false,
148
+ window: targetWindow,
149
+ visited = new WeakMap(),
150
+ handleExtends = false
151
+ } = options
152
+
153
+ const contentWindow = targetWindow || window || globalThis
154
+
155
+ // Handle non-object types and special cases
156
+ if (!isObjectLike(obj) || isDOMNode(obj)) {
157
+ return obj
158
+ }
159
+
160
+ // Handle circular references
161
+ if (visited.has(obj)) {
162
+ return visited.get(obj)
163
+ }
164
+
165
+ // Create appropriate container based on type and window context
166
+ const isArr = isArray(obj)
167
+ const clone = isArr ? [] : {}
168
+
169
+ // Store the clone to handle circular references
170
+ visited.set(obj, clone)
171
+
172
+ // Convert exclude to Set for O(1) lookups when list is non-trivial
173
+ const excludeSet =
174
+ exclude instanceof Set
175
+ ? exclude
176
+ : exclude.length > 3
177
+ ? new Set(exclude)
178
+ : null
179
+
180
+ // Clone properties
181
+ for (const key in obj) {
182
+ if (!Object.prototype.hasOwnProperty.call(obj, key)) continue
183
+
184
+ // Skip excluded properties
185
+ if (_startsWithDunder(key) || key === '__proto__') continue
186
+ if (excludeSet ? excludeSet.has(key) : exclude.includes(key)) continue
187
+
188
+ const value = obj[key]
189
+
190
+ // Skip based on cleanup options
191
+ if (cleanUndefined && value === undefined) continue
192
+ if (cleanNull && value === null) continue
193
+
194
+ // Handle special cases
195
+ if (isDOMNode(value)) {
196
+ clone[key] = value
197
+ continue
198
+ }
199
+
200
+ // Handle 'extends' array if enabled
201
+ if (handleExtends && key === 'extends' && isArray(value)) {
202
+ clone[key] = unstackArrayOfObjects(value, exclude)
203
+ continue
204
+ }
205
+
206
+ // Handle functions in cross-frame scenario
207
+ // Keep original function references — DOMQL handles window/document
208
+ // isolation via element.context, so eval'ing into iframe scope is
209
+ // unnecessary and breaks closure-dependent functions (e.g. resolveTheme)
210
+ if (isFunction(value)) {
211
+ clone[key] = value
212
+ continue
213
+ }
214
+
215
+ // Recursively clone objects
216
+ if (isObjectLike(value)) {
217
+ clone[key] = deepClone(value, {
218
+ ...options,
219
+ visited
220
+ })
221
+ } else {
222
+ clone[key] = value
223
+ }
224
+ }
225
+
226
+ return clone
227
+ }
228
+
229
+ /**
230
+ * Stringify object
231
+ */
232
+ export const deepStringifyFunctions = (obj, stringified = {}) => {
233
+ if (obj.node || obj.__ref || obj.parent || obj.__element || obj.parse) {
234
+ ;(obj.__element || obj.parent?.__element).warn(
235
+ 'Trying to clone element or state at',
236
+ obj
237
+ )
238
+ obj = obj.parse?.()
239
+ }
240
+
241
+ for (const prop in obj) {
242
+ const objProp = obj[prop]
243
+ if (isFunction(objProp)) {
244
+ stringified[prop] = objProp.toString()
245
+ } else if (isObject(objProp)) {
246
+ stringified[prop] = {}
247
+ deepStringifyFunctions(objProp, stringified[prop])
248
+ } else if (isArray(objProp)) {
249
+ const arr = (stringified[prop] = [])
250
+ for (let i = 0; i < objProp.length; i++) {
251
+ const v = objProp[i]
252
+ if (isObject(v)) {
253
+ arr[i] = {}
254
+ deepStringifyFunctions(v, arr[i])
255
+ } else if (isFunction(v)) {
256
+ arr[i] = v.toString()
257
+ } else {
258
+ arr[i] = v
259
+ }
260
+ }
261
+ } else {
262
+ stringified[prop] = objProp
263
+ }
264
+ }
265
+ return stringified
266
+ }
267
+
268
+ const OBJ_TO_STR_SPECIAL_CHARS = new Set([
269
+ '&',
270
+ '*',
271
+ '-',
272
+ ':',
273
+ '%',
274
+ '{',
275
+ '}',
276
+ '>',
277
+ '<',
278
+ '@',
279
+ '.',
280
+ '/',
281
+ '!',
282
+ ' '
283
+ ])
284
+
285
+ export const objectToString = (obj = {}, indent = 0) => {
286
+ // Handle empty object case
287
+ if (obj === null || typeof obj !== 'object') {
288
+ return String(obj)
289
+ }
290
+
291
+ // Handle empty object case - avoid Object.keys allocation
292
+ let hasKeys = false
293
+ for (const _k in obj) {
294
+ hasKeys = true
295
+ break
296
+ } // eslint-disable-line
297
+ if (!hasKeys) return '{}'
298
+
299
+ const spaces = ' '.repeat(indent)
300
+ let str = '{\n'
301
+
302
+ for (const key in obj) {
303
+ if (!Object.prototype.hasOwnProperty.call(obj, key)) continue
304
+ const value = obj[key]
305
+ let keyNeedsQuotes = false
306
+ for (let i = 0; i < key.length; i++) {
307
+ if (OBJ_TO_STR_SPECIAL_CHARS.has(key[i])) {
308
+ keyNeedsQuotes = true
309
+ break
310
+ }
311
+ }
312
+ const stringedKey = keyNeedsQuotes ? `'${key}'` : key
313
+ str += `${spaces} ${stringedKey}: `
314
+
315
+ if (isArray(value)) {
316
+ str += '[\n'
317
+ for (const element of value) {
318
+ if (isObjectLike(element) && element !== null) {
319
+ str += `${spaces} ${objectToString(element, indent + 2)},\n`
320
+ } else if (isString(element)) {
321
+ str += `${spaces} '${element}',\n`
322
+ } else {
323
+ str += `${spaces} ${element},\n`
324
+ }
325
+ }
326
+ str += `${spaces} ]`
327
+ } else if (isObjectLike(value)) {
328
+ str += objectToString(value, indent + 1)
329
+ } else if (isString(value)) {
330
+ str += stringIncludesAny(value, ['\n', "'"])
331
+ ? `\`${value}\``
332
+ : `'${value}'`
333
+ } else {
334
+ str += value
335
+ }
336
+
337
+ str += ',\n'
338
+ }
339
+
340
+ str += `${spaces}}`
341
+ return str
342
+ }
343
+
344
+ const FN_PATTERNS = [
345
+ /^\(\s*\{[^}]*\}\s*\)\s*=>/,
346
+ /^(\([^)]*\)|[^=]*)\s*=>/,
347
+ /^function[\s(]/,
348
+ /^async\s+/,
349
+ /^\(\s*function/,
350
+ /^[a-zA-Z_$][a-zA-Z0-9_$]*\s*=>/
351
+ ]
352
+ const RE_JSON_LIKE = /^["[{]/
353
+
354
+ export const hasFunction = (str) => {
355
+ if (!str) return false
356
+
357
+ const trimmed = str.trim().replace(/\n\s*/g, ' ').trim()
358
+
359
+ if (trimmed === '' || trimmed === '{}' || trimmed === '[]') return false
360
+
361
+ const isFn = FN_PATTERNS.some((pattern) => pattern.test(trimmed))
362
+ if (!isFn) return false
363
+
364
+ const firstChar = trimmed.charCodeAt(0)
365
+ const hasArrow = trimmed.includes('=>')
366
+ // '{' = 123, '[' = 91
367
+ if (firstChar === 123 && !hasArrow) return false // object literal
368
+ if (firstChar === 91) return false // array literal
369
+ if (RE_JSON_LIKE.test(trimmed) && !hasArrow) return false
370
+
371
+ return true
372
+ }
373
+
374
+ // Indirect eval — invokes the global eval, which works in both browser and
375
+ // Node without depending on a `window` object. Required for SSR / Brender
376
+ // passes that hit prepareContext without a real DOM.
377
+ const __globalEval = (src) => (0, eval)(src) // eslint-disable-line no-eval
378
+
379
+ export const deepDestringifyFunctions = (
380
+ obj,
381
+ destringified = {},
382
+ opts = { window: { eval: __globalEval } }
383
+ ) => {
384
+ for (const prop in obj) {
385
+ if (!Object.prototype.hasOwnProperty.call(obj, prop)) continue
386
+
387
+ const objProp = obj[prop]
388
+
389
+ if (isString(objProp)) {
390
+ if (hasFunction(objProp)) {
391
+ try {
392
+ destringified[prop] = opts.window.eval(`(${objProp})`)
393
+ } catch (e) {
394
+ // FR-1 (FRANK-RUNNER.md): the silent string-fallback used to
395
+ // mask broken serialization — eight aihouse functions stored
396
+ // as strings instead of functions, `el.call(name)` no-op'd
397
+ // with no error. Surface the failure so consumers can audit
398
+ // what frank produced. Keep the fallback (don't crash boot)
399
+ // but make it loud.
400
+ if (typeof console !== 'undefined' && console.warn) {
401
+ console.warn(
402
+ '[smbls] deepDestringifyFunctions: eval failed on "' + prop + '" — ' +
403
+ 'function will be left as a string and el.call("' + prop + '") ' +
404
+ 'will silently no-op. Reason: ' + (e && e.message ? e.message : String(e)) + '.\n' +
405
+ 'First 200 chars of source: ' + String(objProp).slice(0, 200)
406
+ )
407
+ }
408
+ destringified[prop] = objProp
409
+ }
410
+ } else {
411
+ destringified[prop] = objProp
412
+ }
413
+ } else if (isArray(objProp)) {
414
+ const arr = (destringified[prop] = [])
415
+ for (let i = 0; i < objProp.length; i++) {
416
+ const arrProp = objProp[i]
417
+ if (isString(arrProp)) {
418
+ if (hasFunction(arrProp)) {
419
+ try {
420
+ arr.push(opts.window.eval(`(${arrProp})`))
421
+ } catch (e) {
422
+ if (typeof console !== 'undefined' && console.warn) {
423
+ console.warn(
424
+ `[smbls] deepDestringifyFunctions: eval failed in array at index ${i} ` +
425
+ `(prop "${prop}"). Reason: ${e && e.message ? e.message : String(e)}.\n` +
426
+ `First 200 chars: ${String(arrProp).slice(0, 200)}`
427
+ )
428
+ }
429
+ arr.push(arrProp)
430
+ }
431
+ } else {
432
+ arr.push(arrProp)
433
+ }
434
+ } else if (isObject(arrProp)) {
435
+ arr.push(deepDestringifyFunctions(arrProp))
436
+ } else {
437
+ arr.push(arrProp)
438
+ }
439
+ }
440
+ } else if (isObject(objProp)) {
441
+ destringified[prop] = deepDestringifyFunctions(
442
+ objProp,
443
+ destringified[prop]
444
+ )
445
+ } else {
446
+ destringified[prop] = objProp
447
+ }
448
+ }
449
+ return destringified
450
+ }
451
+
452
+ /**
453
+ * Rehydrate Set / Map tagged forms (`{ __type: 'Set', values: […] }`,
454
+ * `{ __type: 'Map', entries: [[k,v],…] }`) into real Set / Map
455
+ * instances. frank's serializer emits these for live Set/Map values on
456
+ * globalScope so consumers' `.has()` / `.get()` calls still work after
457
+ * the JSON round-trip (FT-FRANK-1).
458
+ *
459
+ * Pass-through for any other value. Cheap shape check by `__type` so
460
+ * normal user objects aren't perturbed.
461
+ */
462
+ const _rehydrateTaggedValue = (val) => {
463
+ if (!val || typeof val !== 'object') return val
464
+ if (val.__type === 'Set' && Array.isArray(val.values)) return new Set(val.values)
465
+ if (val.__type === 'Map' && Array.isArray(val.entries)) return new Map(val.entries)
466
+ return val
467
+ }
468
+
469
+ /**
470
+ * Destringify a globalScope object so that function strings become real functions.
471
+ * All globalScope values are made available as local variables when eval'ing each
472
+ * function, so helpers can reference constants and other helpers naturally.
473
+ *
474
+ * Also rehydrates Set/Map tagged forms (FT-FRANK-1) so `globalScope.X.has(y)`
475
+ * works for the original Set/Map constructors authors put on globalScope.js.
476
+ */
477
+ export const destringifyGlobalScope = (gs) => {
478
+ if (!gs || typeof gs !== 'object') return gs
479
+
480
+ // First pass: collect non-function values (constants, arrays, objects).
481
+ // Set/Map tagged forms get rehydrated here so they're available as
482
+ // live instances in the closure that wraps function destringify below.
483
+ const result = {}
484
+ const fnEntries = []
485
+ for (const key of Object.keys(gs)) {
486
+ const val = gs[key]
487
+ if (isString(val) && hasFunction(val)) {
488
+ fnEntries.push([key, val])
489
+ } else {
490
+ result[key] = _rehydrateTaggedValue(val)
491
+ }
492
+ }
493
+
494
+ // Second pass: eval functions in a closure with all values in scope
495
+ for (const [key, fnStr] of fnEntries) {
496
+ try {
497
+ // Build a closure that exposes all current globalScope values
498
+ const varDecls = Object.keys(result)
499
+ .map((k) => `var ${k} = __gs__[${JSON.stringify(k)}];`)
500
+ .join('\n')
501
+ result[key] = window.eval(
502
+ `(function(__gs__) { ${varDecls}\n return (${fnStr}); })`
503
+ )(result)
504
+ } catch (e) {
505
+ // Fallback: try plain eval
506
+ try {
507
+ result[key] = window.eval(`(${fnStr})`)
508
+ } catch (_) {
509
+ result[key] = fnStr
510
+ }
511
+ }
512
+ }
513
+
514
+ return result
515
+ }
516
+
517
+ export const stringToObject = (str, opts = { verbose: true }) => {
518
+ try {
519
+ return str ? window.eval('(' + str + ')') : {} // eslint-disable-line
520
+ } catch (e) {
521
+ if (opts.verbose) console.warn(e)
522
+ }
523
+ }
524
+
525
+ export const hasOwnProperty = (o, ...args) =>
526
+ Object.prototype.hasOwnProperty.call(o, ...args)
527
+
528
+ export const isEmpty = (o) => {
529
+ for (const _ in o) return false // eslint-disable-line
530
+ return true
531
+ }
532
+
533
+ export const isEmptyObject = (o) => isObject(o) && isEmpty(o)
534
+
535
+ export const makeObjectWithoutPrototype = () => Object.create(null)
536
+
537
+ /**
538
+ * Overwrites object properties with another
539
+ */
540
+ export const overwrite = (element, params, opts = {}) => {
541
+ const excl = opts.exclude || []
542
+ const allowDunder = opts.preventUnderscore
543
+
544
+ for (const e in params) {
545
+ if (excl.includes(e) || (!allowDunder && _startsWithDunder(e))) continue
546
+ // Block prototype-pollution writes (see _deepMerge for rationale).
547
+ if (e === 'constructor' || e === 'prototype') continue
548
+ if (params[e] !== undefined) {
549
+ element[e] = params[e]
550
+ }
551
+ }
552
+
553
+ return element
554
+ }
555
+
556
+ export const overwriteShallow = (obj, params, excludeFrom = []) => {
557
+ const useSet = excludeFrom instanceof Set
558
+ for (const e in params) {
559
+ if (_startsWithDunder(e)) continue
560
+ if (e === 'constructor' || e === 'prototype') continue
561
+ if (useSet ? excludeFrom.has(e) : excludeFrom.includes(e)) continue
562
+ obj[e] = params[e]
563
+ }
564
+ return obj
565
+ }
566
+
567
+ /**
568
+ * Overwrites DEEPLY object properties with another
569
+ */
570
+ export const overwriteDeep = (
571
+ obj,
572
+ params,
573
+ opts = {},
574
+ visited = new WeakMap()
575
+ ) => {
576
+ if (
577
+ !isObjectLike(obj) ||
578
+ !isObjectLike(params) ||
579
+ isDOMNode(obj) ||
580
+ isDOMNode(params)
581
+ ) {
582
+ return params
583
+ }
584
+
585
+ if (visited.has(obj)) return visited.get(obj)
586
+ visited.set(obj, obj)
587
+
588
+ const excl = opts.exclude
589
+ const exclSet = excl ? (excl instanceof Set ? excl : new Set(excl)) : null
590
+ const forcedExclude = !opts.preventForce
591
+
592
+ for (const e in params) {
593
+ if (!Object.prototype.hasOwnProperty.call(params, e)) continue
594
+ if ((exclSet && exclSet.has(e)) || (forcedExclude && _startsWithDunder(e)))
595
+ continue
596
+ if (e === 'constructor' || e === 'prototype') continue
597
+
598
+ const objProp = obj[e]
599
+ const paramsProp = params[e]
600
+
601
+ if (isDOMNode(paramsProp)) {
602
+ obj[e] = paramsProp
603
+ } else if (isObjectLike(objProp) && isObjectLike(paramsProp)) {
604
+ obj[e] = overwriteDeep(objProp, paramsProp, opts, visited)
605
+ } else if (paramsProp !== undefined) {
606
+ obj[e] = paramsProp
607
+ }
608
+ }
609
+
610
+ return obj
611
+ }
612
+
613
+ /**
614
+ * Recursively compares two values to determine if they are deeply equal.
615
+ */
616
+ export const isEqualDeep = (param, element, visited = new Set()) => {
617
+ if (
618
+ typeof param !== 'object' ||
619
+ typeof element !== 'object' ||
620
+ param === null ||
621
+ element === null
622
+ ) {
623
+ return param === element
624
+ }
625
+
626
+ if (visited.has(param) || visited.has(element)) {
627
+ return true
628
+ }
629
+
630
+ visited.add(param)
631
+ visited.add(element)
632
+
633
+ const keysParam = Object.keys(param)
634
+ const keysElement = Object.keys(element)
635
+
636
+ if (keysParam.length !== keysElement.length) {
637
+ return false
638
+ }
639
+
640
+ for (let i = 0; i < keysParam.length; i++) {
641
+ const key = keysParam[i]
642
+ if (!Object.prototype.hasOwnProperty.call(element, key)) {
643
+ return false
644
+ }
645
+ if (!isEqualDeep(param[key], element[key], visited)) {
646
+ return false
647
+ }
648
+ }
649
+
650
+ return true
651
+ }
652
+
653
+ const DEEP_CONTAINS_IGNORED = new Set(['node', '__ref'])
654
+
655
+ export const deepContains = (
656
+ obj1,
657
+ obj2,
658
+ ignoredKeys = DEEP_CONTAINS_IGNORED
659
+ ) => {
660
+ if (obj1 === obj2) return true
661
+ if (!isObjectLike(obj1) || !isObjectLike(obj2)) return obj1 === obj2
662
+ if (isDOMNode(obj1) || isDOMNode(obj2)) return obj1 === obj2
663
+
664
+ const ignored =
665
+ ignoredKeys instanceof Set ? ignoredKeys : new Set(ignoredKeys)
666
+ const visited = new WeakSet()
667
+
668
+ function checkContains(target, source) {
669
+ if (visited.has(source)) return true
670
+ visited.add(source)
671
+
672
+ for (const key in source) {
673
+ if (!Object.prototype.hasOwnProperty.call(source, key)) continue
674
+ if (ignored.has(key)) continue
675
+ if (!Object.prototype.hasOwnProperty.call(target, key)) return false
676
+
677
+ const sourceValue = source[key]
678
+ const targetValue = target[key]
679
+
680
+ if (isDOMNode(sourceValue) || isDOMNode(targetValue)) {
681
+ if (sourceValue !== targetValue) return false
682
+ } else if (isObjectLike(sourceValue) && isObjectLike(targetValue)) {
683
+ if (!checkContains(targetValue, sourceValue)) return false
684
+ } else if (sourceValue !== targetValue) {
685
+ return false
686
+ }
687
+ }
688
+
689
+ return true
690
+ }
691
+
692
+ return checkContains(obj1, obj2)
693
+ }
694
+
695
+ export const removeFromObject = (obj, props) => {
696
+ if (props === undefined || props === null) return obj
697
+ if (is(props)('string', 'number')) {
698
+ delete obj[props]
699
+ } else if (isArray(props)) {
700
+ for (let i = 0; i < props.length; i++) delete obj[props[i]]
701
+ } else {
702
+ throw new Error(
703
+ 'Invalid input: props must be a string or an array of strings'
704
+ )
705
+ }
706
+ return obj
707
+ }
708
+
709
+ export const createObjectWithoutPrototype = (obj) => {
710
+ if (obj === null || typeof obj !== 'object') {
711
+ return obj
712
+ }
713
+
714
+ const newObj = Object.create(null)
715
+
716
+ for (const key in obj) {
717
+ if (Object.prototype.hasOwnProperty.call(obj, key)) {
718
+ newObj[key] = createObjectWithoutPrototype(obj[key])
719
+ }
720
+ }
721
+
722
+ return newObj
723
+ }
724
+
725
+ export const createNestedObject = (arr, lastValue) => {
726
+ if (arr.length === 0) return lastValue
727
+
728
+ const nestedObject = {}
729
+ let current = nestedObject
730
+
731
+ for (let i = 0; i < arr.length; i++) {
732
+ if (i === arr.length - 1 && lastValue) {
733
+ current[arr[i]] = lastValue
734
+ } else {
735
+ current[arr[i]] = {}
736
+ current = current[arr[i]]
737
+ }
738
+ }
739
+
740
+ return nestedObject
741
+ }
742
+
743
+ export const removeNestedKeyByPath = (obj, path) => {
744
+ if (!Array.isArray(path)) {
745
+ throw new Error('Path must be an array.')
746
+ }
747
+
748
+ let current = obj
749
+
750
+ for (let i = 0; i < path.length - 1; i++) {
751
+ if (current[path[i]] === undefined) return
752
+ current = current[path[i]]
753
+ }
754
+
755
+ const lastKey = path[path.length - 1]
756
+ if (current && Object.prototype.hasOwnProperty.call(current, lastKey)) {
757
+ delete current[lastKey]
758
+ }
759
+ }
760
+
761
+ export const setInObjectByPath = (obj, path, value) => {
762
+ if (!Array.isArray(path)) {
763
+ throw new Error('Path must be an array.')
764
+ }
765
+
766
+ let current = obj
767
+
768
+ for (let i = 0; i < path.length - 1; i++) {
769
+ if (!current[path[i]] || typeof current[path[i]] !== 'object') {
770
+ current[path[i]] = {}
771
+ }
772
+ current = current[path[i]]
773
+ }
774
+
775
+ current[path[path.length - 1]] = value
776
+
777
+ return obj
778
+ }
779
+
780
+ export const getInObjectByPath = (obj, path) => {
781
+ if (!Array.isArray(path)) {
782
+ throw new Error('Path must be an array.')
783
+ }
784
+
785
+ let current = obj
786
+
787
+ for (let i = 0; i < path.length; i++) {
788
+ if (current === undefined || current === null) {
789
+ return undefined
790
+ }
791
+ current = current[path[i]]
792
+ }
793
+
794
+ return current
795
+ }
796
+
797
+ export const detectInfiniteLoop = (arr) => {
798
+ const maxRepeats = 10
799
+ let pattern = []
800
+ let repeatCount = 0
801
+
802
+ for (let i = 0; i < arr.length; i++) {
803
+ if (pattern.length < 2) {
804
+ pattern.push(arr[i])
805
+ } else {
806
+ if (arr[i] === pattern[i % 2]) {
807
+ repeatCount++
808
+ } else {
809
+ pattern = [arr[i - 1], arr[i]]
810
+ repeatCount = 1
811
+ }
812
+
813
+ if (repeatCount >= maxRepeats * 2) {
814
+ if (ENV === 'test' || ENV === 'development') {
815
+ console.warn(
816
+ 'Warning: Potential infinite loop detected due to repeated sequence:',
817
+ pattern
818
+ )
819
+ }
820
+ return true
821
+ }
822
+ }
823
+ }
824
+ }
825
+
826
+ export const isCyclic = (obj) => {
827
+ const seen = new WeakSet()
828
+
829
+ function detect(obj) {
830
+ if (obj && typeof obj === 'object') {
831
+ if (seen.has(obj)) return true
832
+ seen.add(obj)
833
+ for (const key in obj) {
834
+ if (
835
+ Object.prototype.hasOwnProperty.call(obj, key) &&
836
+ detect(obj[key])
837
+ ) {
838
+ console.log(obj, 'cycle at ' + key)
839
+ return true
840
+ }
841
+ }
842
+ }
843
+ return false
844
+ }
845
+
846
+ return detect(obj)
847
+ }
848
+
849
+ export const excludeKeysFromObject = (obj, excludedKeys) => {
850
+ const excluded =
851
+ excludedKeys instanceof Set ? excludedKeys : new Set(excludedKeys)
852
+ const result = {}
853
+ for (const key in obj) {
854
+ if (Object.prototype.hasOwnProperty.call(obj, key) && !excluded.has(key)) {
855
+ result[key] = obj[key]
856
+ }
857
+ }
858
+ return result
859
+ }