@superhero/deep 4.1.0 → 4.3.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/README.md CHANGED
@@ -101,7 +101,40 @@ console.log(clone === obj) // false
101
101
 
102
102
  ---
103
103
 
104
- ## 5. **Deep**
104
+ ## 5. **DeepIntersect**
105
+
106
+ ### Purpose:
107
+ Performs a deep intersection between two or more values. Supports objects, arrays, and nested structures, and handles circular references. Values are strictly compared when considered intersecting.
108
+
109
+ ### Features:
110
+ - Intersects arrays by values.
111
+ - Intersects objects only when the key exists in all sources and the values intersect deeply.
112
+ - Preserves intersected restrictive property descriptors (`writable`, `configurable`, `enumerable`).
113
+ - Detects and throws on circular references.
114
+
115
+ ### Example:
116
+ ```javascript
117
+ import deepintersect from '@superhero/deep/intersect'
118
+
119
+ const a = { foo: { bar: 1 }, arr: [1, 2, 3] }
120
+ const b = { foo: { bar: 1, baz: 2 }, arr: [1, 4, 3] }
121
+
122
+ const result = deepintersect(a, b)
123
+
124
+ console.log(result)
125
+ // { foo: { bar: 1 }, arr: [1, 3] }
126
+ ```
127
+
128
+ #### Note:
129
+ - If two values are loosely equal, but of different types, they do not intersect.
130
+
131
+ - Arrays with nested objects can only be compared if the nested structure shares the same index between the compared arrays.
132
+
133
+ - A circular structure throws a CircularReferenceError (ReferenceError) with the code `E_DEEP_INTERSECT_CIRCULAR_REFERENCE`.
134
+
135
+ ---
136
+
137
+ ## 6. **Deep**
105
138
 
106
139
  ### Purpose:
107
140
  Makes the functions accessible through the imported default object.
@@ -117,16 +150,18 @@ import deep from '@superhero/deep'
117
150
  deep.assign(/* ... */)
118
151
  deep.clone(/* ... */)
119
152
  deep.freeze(/* ... */)
153
+ deep.intersect(/* ... */)
120
154
  deep.merge(/* ... */)
121
155
  ```
122
156
 
123
157
  ### Example:
124
158
  ```javascript
125
- import { assign, clone, freeze, merge } from '@superhero/deep'
159
+ import { assign, clone, freeze, intersect, merge } from '@superhero/deep'
126
160
 
127
161
  assign(/* ... */)
128
162
  clone(/* ... */)
129
163
  freeze(/* ... */)
164
+ intersect(/* ... */)
130
165
  merge(/* ... */)
131
166
  ```
132
167
 
@@ -145,84 +180,104 @@ npm test
145
180
 
146
181
  ```
147
182
  ▶ @superhero/deep/assign
148
- ✔ Assigns arrays correctly (1.993351ms)
149
- ✔ Assigns objects correctly (0.797187ms)
150
- ✔ Overwrites non-object properties correctly (0.311862ms)
151
- ✔ Handles undefined values correctly (0.67959ms)
183
+ ✔ Assigns arrays correctly (3.552823ms)
184
+ ✔ Assigns objects correctly (0.702394ms)
185
+ ✔ Overwrites non-object properties correctly (1.106746ms)
186
+ ✔ Handles undefined values correctly (0.592877ms)
187
+
152
188
  ▶ Descriptor properties
153
189
  ▶ Retains
154
- ✔ non-writable, non-configurable and non-enumarable (0.365558ms)
155
- ✔ writable but non-configurable and non-enumarable (0.384859ms)
156
- ✔ writable and configurable but non-enumarable (0.24546ms)
157
- ✔ Retains (1.395563ms)
190
+ ✔ non-writable, non-configurable and non-enumarable (0.774514ms)
191
+ ✔ writable but non-configurable and non-enumarable (0.490026ms)
192
+ ✔ writable and configurable but non-enumarable (0.314062ms)
193
+ ✔ Retains (2.377181ms)
194
+
158
195
  ▶ Assigns
159
- ✔ non-writable, non-configurable and non-enumarable (0.246451ms)
160
- ✔ Assigns (0.528402ms)
161
- ✔ Descriptor properties (2.438803ms)
162
- ✔ Merges nested arrays correctly (1.67104ms)
163
- ✔ Merges nested objects correctly (0.649403ms)
164
- Does not alter objects with no conflicts (0.219023ms)
165
- @superhero/deep/assign (10.784739ms)
196
+ ✔ non-writable, non-configurable and non-enumarable (0.415235ms)
197
+ ✔ Assigns (0.824789ms)
198
+ ✔ Descriptor properties (3.881836ms)
199
+
200
+ ✔ Merges nested arrays correctly (6.642858ms)
201
+ Merges nested objects correctly (0.731397ms)
202
+ Does not alter objects with no conflicts (0.27423ms)
203
+ ✔ @superhero/deep/assign (20.322649ms)
166
204
 
167
205
  ▶ @superhero/deep/clone
168
- ✔ Clones simple objects (3.898407ms)
169
- ✔ Clones nested objects (0.358366ms)
170
- Clones arrays (0.347023ms)
171
- Handles circular references (0.190668ms)
172
- Clones objects with null prototype (0.302189ms)
173
- @superhero/deep/clone (6.720191ms)
206
+ ✔ Clones simple objects (6.103605ms)
207
+ ✔ Clones nested objects (0.771236ms)
208
+ Preserves descriptors (1.57539ms)
209
+ Clones arrays (1.604074ms)
210
+ Handles circular references (0.507477ms)
211
+ Clones objects with null prototype (1.230476ms)
212
+ ✔ @superhero/deep/clone (14.513864ms)
174
213
 
175
214
  ▶ @superhero/deep/freeze
176
- ✔ Freezes a simple object (2.561531ms)
177
- ✔ Freezes nested objects recursively (0.357866ms)
178
- ✔ Handles circular references gracefully (0.308297ms)
179
- ✔ Freezes objects with symbols (0.210516ms)
180
- ✔ Handles already frozen objects without error (0.157051ms)
181
- ✔ Freezes objects with non-enumerable properties (0.214033ms)
182
- ✔ Freezes arrays (0.244445ms)
183
- ✔ Handles objects with null prototype (0.337528ms)
184
- ✔ Freezes objects with multiple property types (0.525787ms)
185
- ✔ @superhero/deep/freeze (7.261487ms)
215
+ ✔ Freezes a simple object (2.735609ms)
216
+ ✔ Freezes nested objects recursively (0.40638ms)
217
+ ✔ Handles circular references gracefully (0.781639ms)
218
+ ✔ Freezes objects with symbols (0.455776ms)
219
+ ✔ Handles already frozen objects without error (0.342712ms)
220
+ ✔ Freezes objects with non-enumerable properties (0.455515ms)
221
+ ✔ Freezes arrays (0.61924ms)
222
+ ✔ Handles objects with null prototype (0.57424ms)
223
+ ✔ Freezes objects with multiple property types (0.984788ms)
224
+ ✔ @superhero/deep/freeze (12.988297ms)
186
225
 
187
226
  ▶ @superhero/deep
188
- ✔ All functions are accessible as a member to the default import object (1.250244ms)
189
- ✔ All functions are accessible to import from the default import object (0.23689ms)
190
- ✔ @superhero/deep (3.080305ms)
227
+ ✔ All functions are accessible as a member to the default import object (1.908797ms)
228
+ ✔ All functions are accessible to import from the default import object (0.603152ms)
229
+ ✔ @superhero/deep (7.176949ms)
230
+
231
+ ▶ @superhero/deep/intersect
232
+ ✔ Intersects arrays by value and position (5.556412ms)
233
+ ✔ Intersects nested arrays (0.312899ms)
234
+ ✔ Handles empty array intersection (0.297251ms)
235
+ ✔ Intersects objects with matching keys and values (0.554782ms)
236
+ ✔ Deeply intersects nested objects (0.568448ms)
237
+ ✔ Intersection stops at type mismatch (0.278157ms)
238
+ ✔ Throws on circular references (1.779685ms)
239
+ ✔ Intersects arrays with undefined positions (0.702569ms)
240
+ ✔ Handles intersection of primitive types (0.525931ms)
241
+ ✔ Returns undefined for non-intersecting primitives (0.867342ms)
242
+ ✔ Handles multiple sequential intersections (0.836957ms)
243
+ ✔ @superhero/deep/intersect (15.753992ms)
191
244
 
192
245
  ▶ @superhero/deep/merge
193
- ✔ Merges arrays with unique values (3.122153ms)
194
- ✔ Merges arrays with order preserved (0.298964ms)
195
- ✔ Handles empty arrays correctly (0.275066ms)
196
- ✔ Handles arrays with duplicate values (0.492229ms)
197
- ✔ Merges objects and prioritizes restrictive descriptors (0.623443ms)
198
- ✔ Merges objects with non-enumerable properties (0.342317ms)
199
- ✔ Handles nested object merging (0.43726ms)
200
- ✔ Stops at circular references (0.827394ms)
201
- ✔ Stops when nested and with circular references (0.720571ms)
202
- ✔ Returns second value for non-object types (0.925228ms)
203
- ✔ Handles multiple merges sequentially (0.359582ms)
204
- ✔ @superhero/deep/merge (12.298476ms)
205
-
206
- tests 38
207
- suites 8
208
- pass 38
209
-
210
- -----------------------------------------------------------------
211
- file | line % | branch % | funcs % | uncovered lines
212
- -----------------------------------------------------------------
213
- assign.js | 100.00 | 100.00 | 100.00 |
214
- assign.test.js | 100.00 | 100.00 | 100.00 |
215
- clone.js | 100.00 | 100.00 | 100.00 |
216
- clone.test.js | 96.34 | 87.50 | 100.00 | 56-58
217
- freeze.js | 100.00 | 100.00 | 100.00 |
218
- freeze.test.js | 100.00 | 100.00 | 100.00 |
219
- index.js | 100.00 | 100.00 | 100.00 |
220
- index.test.js | 100.00 | 100.00 | 100.00 |
221
- merge.js | 100.00 | 100.00 | 100.00 |
222
- merge.test.js | 100.00 | 100.00 | 100.00 |
223
- -----------------------------------------------------------------
224
- all files | 99.66 | 99.19 | 100.00 |
225
- -----------------------------------------------------------------
246
+ ✔ Merges arrays with unique values (4.014593ms)
247
+ ✔ Merges arrays with order preserved (0.431737ms)
248
+ ✔ Handles empty arrays correctly (0.409507ms)
249
+ ✔ Handles arrays with duplicate values (0.501899ms)
250
+ ✔ Merges objects and prioritizes restrictive descriptors (0.804508ms)
251
+ ✔ Merges objects with non-enumerable properties (0.802831ms)
252
+ ✔ Handles nested object merging (0.709563ms)
253
+ ✔ Stops at circular references (0.768717ms)
254
+ ✔ Stops when nested and with circular references (1.386111ms)
255
+ ✔ Returns second value for non-object types (1.438328ms)
256
+ ✔ Handles multiple merges sequentially (0.494506ms)
257
+ ✔ @superhero/deep/merge (14.798621ms)
258
+
259
+ tests 50
260
+ suites 9
261
+ pass 50
262
+
263
+ ------------------------------------------------------------------------
264
+ file | line % | branch % | funcs % | uncovered lines
265
+ ------------------------------------------------------------------------
266
+ assign.js | 97.80 | 96.15 | 100.00 | 15-16
267
+ assign.test.js | 100.00 | 100.00 | 100.00 |
268
+ clone.js | 95.83 | 93.33 | 100.00 | 22-23
269
+ clone.test.js | 100.00 | 100.00 | 100.00 |
270
+ freeze.js | 100.00 | 100.00 | 100.00 |
271
+ freeze.test.js | 100.00 | 100.00 | 100.00 |
272
+ index.js | 100.00 | 100.00 | 100.00 |
273
+ index.test.js | 100.00 | 100.00 | 100.00 |
274
+ intersect.js | 94.77 | 91.18 | 100.00 | 70-71 85-86 134-137
275
+ intersect.test.js | 100.00 | 100.00 | 100.00 |
276
+ merge.js | 98.72 | 96.30 | 100.00 | 81-82
277
+ merge.test.js | 100.00 | 100.00 | 100.00 |
278
+ ------------------------------------------------------------------------
279
+ all files | 98.84 | 96.81 | 100.00 |
280
+ ------------------------------------------------------------------------
226
281
  ```
227
282
 
228
283
  ---
package/assign.js CHANGED
@@ -10,6 +10,11 @@ function assignB2A(a, b)
10
10
  return a
11
11
  }
12
12
 
13
+ if(Object.is(a, b))
14
+ {
15
+ return a
16
+ }
17
+
13
18
  if(Object.prototype.toString.call(a) === '[object Array]'
14
19
  && Object.prototype.toString.call(b) === '[object Array]')
15
20
  {
package/clone.js CHANGED
@@ -1,6 +1,48 @@
1
- export default function clone(a, legacy = false)
1
+ export default function clone(input)
2
2
  {
3
- return structuredClone && false === legacy
4
- ? structuredClone(a)
5
- : JSON.parse(JSON.stringify(a))
3
+ const seen = new WeakSet()
4
+ return deepClone(input, seen)
5
+ }
6
+
7
+ function deepClone(value, seen)
8
+ {
9
+ switch(Object.prototype.toString.call(value))
10
+ {
11
+ case '[object Array]' : return cloneArray(value, seen)
12
+ case '[object Object]' : return cloneObject(value, seen)
13
+ }
14
+
15
+ return structuredClone(value)
16
+ }
17
+
18
+ function cloneArray(array, seen)
19
+ {
20
+ if(seen.has(array))
21
+ {
22
+ return array
23
+ }
24
+
25
+ seen.add(array)
26
+ return array.map((item) => deepClone(item, seen))
27
+ }
28
+
29
+ function cloneObject(obj, seen)
30
+ {
31
+ if(seen.has(obj))
32
+ {
33
+ return obj
34
+ }
35
+
36
+ seen.add(obj)
37
+
38
+ const output = {}
39
+
40
+ for(const key of Object.getOwnPropertyNames(obj))
41
+ {
42
+ const descriptor = Object.getOwnPropertyDescriptor(obj, key)
43
+ Object.defineProperty(output, key,
44
+ { ...descriptor, value : deepClone(descriptor.value, seen) })
45
+ }
46
+
47
+ return output
6
48
  }
package/clone.test.js CHANGED
@@ -8,75 +8,76 @@ suite('@superhero/deep/clone', () =>
8
8
  {
9
9
  const
10
10
  obj = { foo: 'bar', baz: 42 },
11
- result = deepclone(obj),
12
- legacy = deepclone(obj, true)
11
+ cloned = deepclone(obj)
13
12
 
14
- assert.deepStrictEqual(result, obj, 'Cloned object should be equal to the original')
15
- assert.deepStrictEqual(legacy, obj, 'Cloned object should be equal to the original (legacy mode)')
16
-
17
- assert.notStrictEqual(result, obj, 'Cloned object should not be the same reference as the original')
18
- assert.notStrictEqual(legacy, obj, 'Cloned object should not be the same reference as the original (legacy mode)')
13
+ assert.deepStrictEqual(cloned, obj, 'Cloned object should be equal to the original')
14
+ assert.notStrictEqual(cloned, obj, 'Not the same reference as the original')
19
15
  })
20
16
 
21
17
  test('Clones nested objects', () =>
22
18
  {
23
- const obj = { foo: { bar: { baz: 'qux' } } }
24
-
25
19
  const
26
- result = deepclone(obj),
27
- legacy = deepclone(obj, true)
20
+ obj = { foo: { bar: { baz: 'qux' } } },
21
+ cloned = deepclone(obj)
22
+
23
+ assert.deepStrictEqual(cloned, obj, 'Cloned nested object should be equal to the original')
24
+ assert.notStrictEqual(cloned.foo, obj.foo, 'Not the same reference as the original')
25
+ })
26
+
27
+ test('Preserves descriptors', () =>
28
+ {
29
+ const origin = {}
30
+
31
+ Object.defineProperty(origin, 'foo', { value: {}, enumerable: true, writable: true, configurable: true })
32
+ Object.defineProperty(origin, 'bar', { value: {}, enumerable: false, writable: true, configurable: true })
33
+ Object.defineProperty(origin, 'baz', { value: {}, enumerable: false, writable: false, configurable: true })
34
+ Object.defineProperty(origin, 'qux', { value: {}, enumerable: false, writable: false, configurable: false })
35
+
36
+ const cloned = deepclone(origin)
37
+
38
+ assert.deepStrictEqual(cloned, origin, 'Cloned nested object should be equal to the original')
39
+ assert.notStrictEqual(cloned, origin, 'Not the same reference as the original')
40
+ assert.notStrictEqual(cloned.foo, origin.foo, 'Cloned nested object should not share reference with the original')
28
41
 
29
- assert.deepStrictEqual(result, obj, 'Cloned nested object should be equal to the original')
30
- assert.deepStrictEqual(legacy, obj, 'Cloned nested object should be equal to the original (legacy mode)')
42
+ const
43
+ clonedDescriptors = Object.getOwnPropertyDescriptors(cloned),
44
+ originDescriptors = Object.getOwnPropertyDescriptors(origin)
31
45
 
32
- assert.notStrictEqual(result.foo, obj.foo, 'Cloned nested object should not share reference with the original')
33
- assert.notStrictEqual(legacy.foo, obj.foo, 'Cloned nested object should not share reference with the original (legacy mode)')
46
+ for(const key in originDescriptors)
47
+ {
48
+ assert.equal(clonedDescriptors[key].enumerable, originDescriptors[key].enumerable)
49
+ assert.equal(clonedDescriptors[key].writable, originDescriptors[key].writable)
50
+ assert.equal(clonedDescriptors[key].configurable, originDescriptors[key].configurable)
51
+ }
34
52
  })
35
53
 
36
54
  test('Clones arrays', () =>
37
55
  {
38
- const arr = [1, 2, 3, [4, 5]]
39
-
40
56
  const
41
- result = deepclone(arr),
42
- legacy = deepclone(arr, true)
57
+ array = [1, 2, 3, [4, 5]],
58
+ cloned = deepclone(array)
43
59
 
44
- assert.deepStrictEqual(result, arr, 'Cloned array should be equal to the original')
45
- assert.deepStrictEqual(legacy, arr, 'Cloned array should be equal to the original (legacy mode)')
60
+ assert.deepStrictEqual(cloned, array, 'Cloned array should be equal to the original')
61
+ assert.notStrictEqual(cloned, array, 'Not the same reference as the original')
62
+ assert.notStrictEqual(cloned[3], array[3], 'Nested array in clone should not share reference with the original')
63
+ })
46
64
 
47
- assert.notStrictEqual(result, arr, 'Cloned array should not share reference with the original')
48
- assert.notStrictEqual(legacy, arr, 'Cloned array should not share reference with the original (legacy mode)')
65
+ test('Handles circular references', () =>
66
+ {
67
+ const obj = {}
68
+ obj.self = obj
49
69
 
50
- assert.notStrictEqual(result[3], arr[3], 'Nested array in clone should not share reference with the original')
51
- assert.notStrictEqual(legacy[3], arr[3], 'Nested array in clone should not share reference with the original (legacy mode)')
70
+ const cloned = deepclone(obj)
71
+ assert.strictEqual(cloned.self, obj, 'Circular references should be preserved in the clone')
52
72
  })
53
73
 
54
- if(false === !!structuredClone)
55
- {
56
- test.skip('Handles circular references (structuredClone not available)')
57
- test.skip('Clones objects with null prototype (structuredClone not available)')
58
- }
59
- else
74
+ test('Clones objects with null prototype', () =>
60
75
  {
61
- test('Handles circular references', () =>
62
- {
63
- const obj = {}
64
- obj.self = obj
65
-
66
- const result = deepclone(obj)
67
-
68
- assert.strictEqual(result.self, result, 'Circular references should be preserved in the clone')
69
- })
70
-
71
- test('Clones objects with null prototype', () =>
72
- {
73
- const obj = Object.create(null)
74
- obj.foo = 'bar'
75
-
76
- const result = deepclone(obj)
77
-
78
- assert.deepEqual(result, obj, 'Cloned object with null prototype should be equal to the original')
79
- assert.notStrictEqual(result, obj, 'Cloned object with null prototype should not share reference with the original')
80
- })
81
- }
76
+ const obj = Object.create(null)
77
+ obj.foo = 'bar'
78
+
79
+ const cloned = deepclone(obj)
80
+ assert.deepEqual(cloned, obj, 'Cloned object with null prototype should be equal to the original')
81
+ assert.notStrictEqual(cloned, obj, 'Not the same reference as the original')
82
+ })
82
83
  })
package/index.js CHANGED
@@ -1,7 +1,8 @@
1
- import assign from '@superhero/deep/assign'
2
- import clone from '@superhero/deep/clone'
3
- import freeze from '@superhero/deep/freeze'
4
- import merge from '@superhero/deep/merge'
1
+ import assign from '@superhero/deep/assign'
2
+ import clone from '@superhero/deep/clone'
3
+ import freeze from '@superhero/deep/freeze'
4
+ import intersect from '@superhero/deep/intersect'
5
+ import merge from '@superhero/deep/merge'
5
6
 
6
- export { assign, clone, freeze, merge }
7
- export default { assign, clone, freeze, merge }
7
+ export { assign, clone, freeze, intersect, merge }
8
+ export default { assign, clone, freeze, intersect, merge }
package/intersect.js ADDED
@@ -0,0 +1,165 @@
1
+ /**
2
+ * When intersecting two arrays [object Array], the result is a new array
3
+ * containing only values present in both arrays. Duplicate values are removed.
4
+ *
5
+ * @example intersecting [1, 2, 3] and [2, 3, 4] results in [2, 3].
6
+ *
7
+ * ----------------------------------------------------------------------------
8
+ *
9
+ * When intersecting two objects [object Object], the result is a new object
10
+ * with properties that exist in both objects and whose values are also
11
+ * intersecting. The descriptor of the intersected property is defined by
12
+ * the most restrictive rules, same as the merge strategy.
13
+ *
14
+ * @example if key exists in both objects and the values are primitives and
15
+ * equal, that value is kept.
16
+ *
17
+ * @example if the values are nested objects or arrays, the intersection
18
+ * continues recursively.
19
+ *
20
+ * ----------------------------------------------------------------------------
21
+ *
22
+ * When the types differ, no value is intersected.
23
+ *
24
+ * @example intersecting a string and an object results in no match.
25
+ */
26
+ export default function intersect(a, b, ...c)
27
+ {
28
+ const
29
+ seen = new WeakMap,
30
+ output = intersectAandB(a, b, seen)
31
+
32
+ return c.length
33
+ ? intersect(output, ...c)
34
+ : output
35
+ }
36
+
37
+ function intersectAandB(a, b, seen)
38
+ {
39
+ if(Object.is(a, b))
40
+ {
41
+ return a
42
+ }
43
+
44
+ const
45
+ aType = Object.prototype.toString.call(a),
46
+ bType = Object.prototype.toString.call(b)
47
+
48
+ if(aType !== bType)
49
+ {
50
+ return undefined
51
+ }
52
+
53
+ if('[object Array]' === aType)
54
+ {
55
+ return intersectArray(a, b, seen)
56
+ }
57
+
58
+ if('[object Object]' === aType)
59
+ {
60
+ return intersectObject(a, b, seen)
61
+ }
62
+
63
+ return undefined
64
+ }
65
+
66
+ function intersectArray(a, b, seen)
67
+ {
68
+ if(hasSeen(a, b, seen))
69
+ {
70
+ return seen.get(a).get(b)
71
+ }
72
+
73
+ const
74
+ values = a.map((value, i) =>
75
+ {
76
+ if(b.includes(value))
77
+ {
78
+ return value
79
+ }
80
+
81
+ if('object' === typeof value)
82
+ {
83
+ return intersectAandB(value, b[i], seen)
84
+ }
85
+ }),
86
+ output = values.filter((value) => value !== undefined)
87
+
88
+ seen.get(a).set(b, output)
89
+
90
+ return output
91
+ }
92
+
93
+ function intersectObject(a, b, seen)
94
+ {
95
+ if(hasSeen(a, b, seen))
96
+ {
97
+ return seen.get(a).get(b)
98
+ }
99
+
100
+ const output = {}
101
+
102
+ for(const key of Object.getOwnPropertyNames(a))
103
+ {
104
+ if(false === (key in b))
105
+ {
106
+ continue
107
+ }
108
+
109
+ const value = intersectAandB(a[key], b[key], seen)
110
+
111
+ if(undefined === value)
112
+ {
113
+ continue
114
+ }
115
+
116
+ const
117
+ descriptorA = Object.getOwnPropertyDescriptor(a, key),
118
+ descriptorB = Object.getOwnPropertyDescriptor(b, key)
119
+
120
+ Object.defineProperty(output, key,
121
+ {
122
+ configurable : descriptorA.configurable && descriptorB.configurable,
123
+ enumerable : descriptorA.enumerable && descriptorB.enumerable,
124
+ writable : descriptorA.writable && descriptorB.writable,
125
+ value
126
+ })
127
+ }
128
+
129
+ seen.get(a).set(b, output)
130
+
131
+ return output
132
+ }
133
+
134
+ function hasSeen(a, b, seen)
135
+ {
136
+ if(false === seen.has(a))
137
+ {
138
+ seen.set(a, new WeakMap)
139
+ }
140
+ else if(seen.get(a).has(b))
141
+ {
142
+ if(CircularReferenceError === seen.get(a).get(b))
143
+ {
144
+ throw new CircularReferenceError('Circular reference detected')
145
+ }
146
+ else
147
+ {
148
+ return true
149
+ }
150
+ }
151
+
152
+ // Avoid circular references by initiating the weak map with a
153
+ // ReferenceError that is written over later when the object is
154
+ // fully processed and the reference is not circular, and can
155
+ // then instead be set to the output to act as a cache layer.
156
+ seen.get(a).set(b, CircularReferenceError)
157
+
158
+ return false
159
+ }
160
+
161
+ class CircularReferenceError extends ReferenceError
162
+ {
163
+ name = 'CircularReferenceError'
164
+ code = 'E_DEEP_INTERSECT_CIRCULAR_REFERENCE'
165
+ }
@@ -0,0 +1,129 @@
1
+ import assert from 'assert'
2
+ import { suite, test } from 'node:test'
3
+ import deepintersect from '@superhero/deep/intersect'
4
+
5
+ suite('@superhero/deep/intersect', () =>
6
+ {
7
+ test('Intersects arrays by value and position', () =>
8
+ {
9
+ const
10
+ a = [1, 2, 3],
11
+ b = [2, 3, 4],
12
+ expected = [2, 3]
13
+
14
+ const result = deepintersect(a, b)
15
+ assert.deepStrictEqual(result, expected, 'Arrays should intersect by value and position')
16
+ })
17
+
18
+ test('Intersects nested arrays', () =>
19
+ {
20
+ const
21
+ a = [1, [2, 3], 4],
22
+ b = [1, [2, 4], 4],
23
+ expected = [1, [2], 4]
24
+
25
+ const result = deepintersect(a, b)
26
+ assert.deepStrictEqual(result, expected, 'Nested arrays should intersect deeply')
27
+ })
28
+
29
+ test('Handles empty array intersection', () =>
30
+ {
31
+ const
32
+ a = [1, 2, 3],
33
+ b = [],
34
+ expected = []
35
+
36
+ const result = deepintersect(a, b)
37
+ assert.deepStrictEqual(result, expected, 'Intersection with empty array results in empty array')
38
+ })
39
+
40
+ test('Intersects objects with matching keys and values', () =>
41
+ {
42
+ const
43
+ a = { foo: 1, bar: 2 },
44
+ b = { foo: 1, baz: 3 },
45
+ expected = { foo: 1 }
46
+
47
+ const result = deepintersect(a, b)
48
+ assert.deepStrictEqual(result, expected, 'Only matching keys with same values should intersect')
49
+ })
50
+
51
+ test('Deeply intersects nested objects', () =>
52
+ {
53
+ const
54
+ a = { foo: { bar: 1, baz: 2 } },
55
+ b = { foo: { bar: 1, baz: 3 } },
56
+ expected = { foo: { bar: 1 } }
57
+
58
+ const result = deepintersect(a, b)
59
+ assert.deepStrictEqual(result, expected, 'Nested objects should deeply intersect')
60
+ })
61
+
62
+ test('Intersection stops at type mismatch', () =>
63
+ {
64
+ const
65
+ a = { foo: [1, 2] },
66
+ b = { foo: { bar: 1 } },
67
+ expected = {}
68
+
69
+ const result = deepintersect(a, b)
70
+ assert.deepStrictEqual(result, expected, 'Type mismatch should result in empty intersection')
71
+ })
72
+
73
+ test('Throws on circular references', () =>
74
+ {
75
+ const a = {}
76
+ const b = {}
77
+ a.self = a
78
+ b.self = b
79
+
80
+ assert.throws(() => deepintersect(a, b), {
81
+ name: 'CircularReferenceError',
82
+ code: 'E_DEEP_INTERSECT_CIRCULAR_REFERENCE'
83
+ }, 'Circular references should throw CircularReferenceError')
84
+ })
85
+
86
+ test('Intersects arrays with undefined positions', () =>
87
+ {
88
+ const
89
+ a = [1, undefined, 3],
90
+ b = [1, 2, 3],
91
+ expected = [1, 3]
92
+
93
+ const result = deepintersect(a, b)
94
+ assert.deepStrictEqual(result, expected, 'Undefined positions should be excluded')
95
+ })
96
+
97
+ test('Handles intersection of primitive types', () =>
98
+ {
99
+ const
100
+ a = 'string',
101
+ b = 'string',
102
+ expected = 'string'
103
+
104
+ const result = deepintersect(a, b)
105
+ assert.strictEqual(result, expected, 'Matching primitive types should intersect')
106
+ })
107
+
108
+ test('Returns undefined for non-intersecting primitives', () =>
109
+ {
110
+ const
111
+ a = 'string',
112
+ b = 'different'
113
+
114
+ const result = deepintersect(a, b)
115
+ assert.strictEqual(result, undefined, 'Non-matching primitive types should return undefined')
116
+ })
117
+
118
+ test('Handles multiple sequential intersections', () =>
119
+ {
120
+ const
121
+ a = { foo: 1, bar: 2 },
122
+ b = { bar: 2, baz: 3 },
123
+ c = { bar: 2, qux: 4 },
124
+ expected = { bar: 2 }
125
+
126
+ const result = deepintersect(a, b, c)
127
+ assert.deepStrictEqual(result, expected, 'Multiple objects should intersect sequentially')
128
+ })
129
+ })
package/merge.js CHANGED
@@ -1,3 +1,5 @@
1
+ import deepclone from '@superhero/deep/clone'
2
+
1
3
  /**
2
4
  * When merging two objects [object Object], a new object is created with the
3
5
  * properties of both objects defined. The descriptor of the property in the
@@ -64,7 +66,7 @@ export default function merge(a, b, ...c)
64
66
 
65
67
  return c.length
66
68
  ? merge(output, ...c)
67
- : output
69
+ : deepclone(output)
68
70
  }
69
71
 
70
72
  function mergeAandB(a, b, seen)
@@ -74,6 +76,11 @@ function mergeAandB(a, b, seen)
74
76
  return a
75
77
  }
76
78
 
79
+ if(Object.is(a, b))
80
+ {
81
+ return a
82
+ }
83
+
77
84
  const
78
85
  aType = Object.prototype.toString.call(a),
79
86
  bType = Object.prototype.toString.call(b)
package/merge.test.js CHANGED
@@ -134,7 +134,7 @@ suite('@superhero/deep/merge', () =>
134
134
 
135
135
  const result = deepmerge(a, b)
136
136
 
137
- assert.strictEqual(result.self, b.self, 'Circular references should not merge further')
137
+ assert.deepStrictEqual(result.self, b.self, 'Circular references should not merge further')
138
138
  })
139
139
 
140
140
  test('Stops when nested and with circular references', () =>
@@ -149,8 +149,8 @@ suite('@superhero/deep/merge', () =>
149
149
  resultA = deepmerge(a, b),
150
150
  resultB = deepmerge(b, a)
151
151
 
152
- assert.strictEqual(resultA.foo.bar.foo.bar, b, 'Circular references should not interfare with the merged result')
153
- assert.strictEqual(resultB.foo.bar.foo.bar, 'baz', 'Circular references should not interfare with the merged result')
152
+ assert.deepStrictEqual(resultA.foo.bar.foo.bar, b, 'Circular references should not interfare with the merged result')
153
+ assert.deepStrictEqual(resultB.foo.bar.foo.bar, 'baz', 'Circular references should not interfare with the merged result')
154
154
  })
155
155
 
156
156
  test('Returns second value for non-object types', () =>
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@superhero/deep",
3
- "version": "4.1.0",
3
+ "version": "4.3.0",
4
4
  "description": "A collection of deep structure operations",
5
- "keywords": ["deep", "assign", "clone", "freeze", "merge"],
5
+ "keywords": ["deep", "assign", "clone", "freeze", "intersect", "merge"],
6
6
  "main": "index.js",
7
7
  "license": "MIT",
8
8
  "type": "module",