@symbo.ls/utils 3.2.3 → 3.14.1

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,837 @@
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
+ * Destringify a globalScope object so that function strings become real functions.
454
+ * All globalScope values are made available as local variables when eval'ing each
455
+ * function, so helpers can reference constants and other helpers naturally.
456
+ */
457
+ export const destringifyGlobalScope = (gs) => {
458
+ if (!gs || typeof gs !== 'object') return gs
459
+
460
+ // First pass: collect non-function values (constants, arrays, objects)
461
+ const result = {}
462
+ const fnEntries = []
463
+ for (const key of Object.keys(gs)) {
464
+ const val = gs[key]
465
+ if (isString(val) && hasFunction(val)) {
466
+ fnEntries.push([key, val])
467
+ } else {
468
+ result[key] = val
469
+ }
470
+ }
471
+
472
+ // Second pass: eval functions in a closure with all values in scope
473
+ for (const [key, fnStr] of fnEntries) {
474
+ try {
475
+ // Build a closure that exposes all current globalScope values
476
+ const varDecls = Object.keys(result)
477
+ .map((k) => `var ${k} = __gs__[${JSON.stringify(k)}];`)
478
+ .join('\n')
479
+ result[key] = window.eval(
480
+ `(function(__gs__) { ${varDecls}\n return (${fnStr}); })`
481
+ )(result)
482
+ } catch (e) {
483
+ // Fallback: try plain eval
484
+ try {
485
+ result[key] = window.eval(`(${fnStr})`)
486
+ } catch (_) {
487
+ result[key] = fnStr
488
+ }
489
+ }
490
+ }
491
+
492
+ return result
493
+ }
494
+
495
+ export const stringToObject = (str, opts = { verbose: true }) => {
496
+ try {
497
+ return str ? window.eval('(' + str + ')') : {} // eslint-disable-line
498
+ } catch (e) {
499
+ if (opts.verbose) console.warn(e)
500
+ }
501
+ }
502
+
503
+ export const hasOwnProperty = (o, ...args) =>
504
+ Object.prototype.hasOwnProperty.call(o, ...args)
505
+
506
+ export const isEmpty = (o) => {
507
+ for (const _ in o) return false // eslint-disable-line
508
+ return true
509
+ }
510
+
511
+ export const isEmptyObject = (o) => isObject(o) && isEmpty(o)
512
+
513
+ export const makeObjectWithoutPrototype = () => Object.create(null)
514
+
515
+ /**
516
+ * Overwrites object properties with another
517
+ */
518
+ export const overwrite = (element, params, opts = {}) => {
519
+ const excl = opts.exclude || []
520
+ const allowDunder = opts.preventUnderscore
521
+
522
+ for (const e in params) {
523
+ if (excl.includes(e) || (!allowDunder && _startsWithDunder(e))) continue
524
+ // Block prototype-pollution writes (see _deepMerge for rationale).
525
+ if (e === 'constructor' || e === 'prototype') continue
526
+ if (params[e] !== undefined) {
527
+ element[e] = params[e]
528
+ }
529
+ }
530
+
531
+ return element
532
+ }
533
+
534
+ export const overwriteShallow = (obj, params, excludeFrom = []) => {
535
+ const useSet = excludeFrom instanceof Set
536
+ for (const e in params) {
537
+ if (_startsWithDunder(e)) continue
538
+ if (e === 'constructor' || e === 'prototype') continue
539
+ if (useSet ? excludeFrom.has(e) : excludeFrom.includes(e)) continue
540
+ obj[e] = params[e]
541
+ }
542
+ return obj
543
+ }
544
+
545
+ /**
546
+ * Overwrites DEEPLY object properties with another
547
+ */
548
+ export const overwriteDeep = (
549
+ obj,
550
+ params,
551
+ opts = {},
552
+ visited = new WeakMap()
553
+ ) => {
554
+ if (
555
+ !isObjectLike(obj) ||
556
+ !isObjectLike(params) ||
557
+ isDOMNode(obj) ||
558
+ isDOMNode(params)
559
+ ) {
560
+ return params
561
+ }
562
+
563
+ if (visited.has(obj)) return visited.get(obj)
564
+ visited.set(obj, obj)
565
+
566
+ const excl = opts.exclude
567
+ const exclSet = excl ? (excl instanceof Set ? excl : new Set(excl)) : null
568
+ const forcedExclude = !opts.preventForce
569
+
570
+ for (const e in params) {
571
+ if (!Object.prototype.hasOwnProperty.call(params, e)) continue
572
+ if ((exclSet && exclSet.has(e)) || (forcedExclude && _startsWithDunder(e)))
573
+ continue
574
+ if (e === 'constructor' || e === 'prototype') continue
575
+
576
+ const objProp = obj[e]
577
+ const paramsProp = params[e]
578
+
579
+ if (isDOMNode(paramsProp)) {
580
+ obj[e] = paramsProp
581
+ } else if (isObjectLike(objProp) && isObjectLike(paramsProp)) {
582
+ obj[e] = overwriteDeep(objProp, paramsProp, opts, visited)
583
+ } else if (paramsProp !== undefined) {
584
+ obj[e] = paramsProp
585
+ }
586
+ }
587
+
588
+ return obj
589
+ }
590
+
591
+ /**
592
+ * Recursively compares two values to determine if they are deeply equal.
593
+ */
594
+ export const isEqualDeep = (param, element, visited = new Set()) => {
595
+ if (
596
+ typeof param !== 'object' ||
597
+ typeof element !== 'object' ||
598
+ param === null ||
599
+ element === null
600
+ ) {
601
+ return param === element
602
+ }
603
+
604
+ if (visited.has(param) || visited.has(element)) {
605
+ return true
606
+ }
607
+
608
+ visited.add(param)
609
+ visited.add(element)
610
+
611
+ const keysParam = Object.keys(param)
612
+ const keysElement = Object.keys(element)
613
+
614
+ if (keysParam.length !== keysElement.length) {
615
+ return false
616
+ }
617
+
618
+ for (let i = 0; i < keysParam.length; i++) {
619
+ const key = keysParam[i]
620
+ if (!Object.prototype.hasOwnProperty.call(element, key)) {
621
+ return false
622
+ }
623
+ if (!isEqualDeep(param[key], element[key], visited)) {
624
+ return false
625
+ }
626
+ }
627
+
628
+ return true
629
+ }
630
+
631
+ const DEEP_CONTAINS_IGNORED = new Set(['node', '__ref'])
632
+
633
+ export const deepContains = (
634
+ obj1,
635
+ obj2,
636
+ ignoredKeys = DEEP_CONTAINS_IGNORED
637
+ ) => {
638
+ if (obj1 === obj2) return true
639
+ if (!isObjectLike(obj1) || !isObjectLike(obj2)) return obj1 === obj2
640
+ if (isDOMNode(obj1) || isDOMNode(obj2)) return obj1 === obj2
641
+
642
+ const ignored =
643
+ ignoredKeys instanceof Set ? ignoredKeys : new Set(ignoredKeys)
644
+ const visited = new WeakSet()
645
+
646
+ function checkContains(target, source) {
647
+ if (visited.has(source)) return true
648
+ visited.add(source)
649
+
650
+ for (const key in source) {
651
+ if (!Object.prototype.hasOwnProperty.call(source, key)) continue
652
+ if (ignored.has(key)) continue
653
+ if (!Object.prototype.hasOwnProperty.call(target, key)) return false
654
+
655
+ const sourceValue = source[key]
656
+ const targetValue = target[key]
657
+
658
+ if (isDOMNode(sourceValue) || isDOMNode(targetValue)) {
659
+ if (sourceValue !== targetValue) return false
660
+ } else if (isObjectLike(sourceValue) && isObjectLike(targetValue)) {
661
+ if (!checkContains(targetValue, sourceValue)) return false
662
+ } else if (sourceValue !== targetValue) {
663
+ return false
664
+ }
665
+ }
666
+
667
+ return true
668
+ }
669
+
670
+ return checkContains(obj1, obj2)
671
+ }
672
+
673
+ export const removeFromObject = (obj, props) => {
674
+ if (props === undefined || props === null) return obj
675
+ if (is(props)('string', 'number')) {
676
+ delete obj[props]
677
+ } else if (isArray(props)) {
678
+ for (let i = 0; i < props.length; i++) delete obj[props[i]]
679
+ } else {
680
+ throw new Error(
681
+ 'Invalid input: props must be a string or an array of strings'
682
+ )
683
+ }
684
+ return obj
685
+ }
686
+
687
+ export const createObjectWithoutPrototype = (obj) => {
688
+ if (obj === null || typeof obj !== 'object') {
689
+ return obj
690
+ }
691
+
692
+ const newObj = Object.create(null)
693
+
694
+ for (const key in obj) {
695
+ if (Object.prototype.hasOwnProperty.call(obj, key)) {
696
+ newObj[key] = createObjectWithoutPrototype(obj[key])
697
+ }
698
+ }
699
+
700
+ return newObj
701
+ }
702
+
703
+ export const createNestedObject = (arr, lastValue) => {
704
+ if (arr.length === 0) return lastValue
705
+
706
+ const nestedObject = {}
707
+ let current = nestedObject
708
+
709
+ for (let i = 0; i < arr.length; i++) {
710
+ if (i === arr.length - 1 && lastValue) {
711
+ current[arr[i]] = lastValue
712
+ } else {
713
+ current[arr[i]] = {}
714
+ current = current[arr[i]]
715
+ }
716
+ }
717
+
718
+ return nestedObject
719
+ }
720
+
721
+ export const removeNestedKeyByPath = (obj, path) => {
722
+ if (!Array.isArray(path)) {
723
+ throw new Error('Path must be an array.')
724
+ }
725
+
726
+ let current = obj
727
+
728
+ for (let i = 0; i < path.length - 1; i++) {
729
+ if (current[path[i]] === undefined) return
730
+ current = current[path[i]]
731
+ }
732
+
733
+ const lastKey = path[path.length - 1]
734
+ if (current && Object.prototype.hasOwnProperty.call(current, lastKey)) {
735
+ delete current[lastKey]
736
+ }
737
+ }
738
+
739
+ export const setInObjectByPath = (obj, path, value) => {
740
+ if (!Array.isArray(path)) {
741
+ throw new Error('Path must be an array.')
742
+ }
743
+
744
+ let current = obj
745
+
746
+ for (let i = 0; i < path.length - 1; i++) {
747
+ if (!current[path[i]] || typeof current[path[i]] !== 'object') {
748
+ current[path[i]] = {}
749
+ }
750
+ current = current[path[i]]
751
+ }
752
+
753
+ current[path[path.length - 1]] = value
754
+
755
+ return obj
756
+ }
757
+
758
+ export const getInObjectByPath = (obj, path) => {
759
+ if (!Array.isArray(path)) {
760
+ throw new Error('Path must be an array.')
761
+ }
762
+
763
+ let current = obj
764
+
765
+ for (let i = 0; i < path.length; i++) {
766
+ if (current === undefined || current === null) {
767
+ return undefined
768
+ }
769
+ current = current[path[i]]
770
+ }
771
+
772
+ return current
773
+ }
774
+
775
+ export const detectInfiniteLoop = (arr) => {
776
+ const maxRepeats = 10
777
+ let pattern = []
778
+ let repeatCount = 0
779
+
780
+ for (let i = 0; i < arr.length; i++) {
781
+ if (pattern.length < 2) {
782
+ pattern.push(arr[i])
783
+ } else {
784
+ if (arr[i] === pattern[i % 2]) {
785
+ repeatCount++
786
+ } else {
787
+ pattern = [arr[i - 1], arr[i]]
788
+ repeatCount = 1
789
+ }
790
+
791
+ if (repeatCount >= maxRepeats * 2) {
792
+ if (ENV === 'test' || ENV === 'development') {
793
+ console.warn(
794
+ 'Warning: Potential infinite loop detected due to repeated sequence:',
795
+ pattern
796
+ )
797
+ }
798
+ return true
799
+ }
800
+ }
801
+ }
802
+ }
803
+
804
+ export const isCyclic = (obj) => {
805
+ const seen = new WeakSet()
806
+
807
+ function detect(obj) {
808
+ if (obj && typeof obj === 'object') {
809
+ if (seen.has(obj)) return true
810
+ seen.add(obj)
811
+ for (const key in obj) {
812
+ if (
813
+ Object.prototype.hasOwnProperty.call(obj, key) &&
814
+ detect(obj[key])
815
+ ) {
816
+ console.log(obj, 'cycle at ' + key)
817
+ return true
818
+ }
819
+ }
820
+ }
821
+ return false
822
+ }
823
+
824
+ return detect(obj)
825
+ }
826
+
827
+ export const excludeKeysFromObject = (obj, excludedKeys) => {
828
+ const excluded =
829
+ excludedKeys instanceof Set ? excludedKeys : new Set(excludedKeys)
830
+ const result = {}
831
+ for (const key in obj) {
832
+ if (Object.prototype.hasOwnProperty.call(obj, key) && !excluded.has(key)) {
833
+ result[key] = obj[key]
834
+ }
835
+ }
836
+ return result
837
+ }