Skip to content

Commit 10fa0a9

Browse files
committed
feat(utils): [clone] support circular references
Signed-off-by: Lexus Drumgold <unicornware@flexdevelopment.llc>
1 parent b6d5a38 commit 10fa0a9

File tree

6 files changed

+110
-76
lines changed

6 files changed

+110
-76
lines changed

.dictionary.txt

+1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ cefc
88
codecov
99
commitlintrc
1010
customizer
11+
dclone
1112
dedupe
1213
dequal
1314
desegment

__fixtures__/today.ts

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
/**
2+
* @file Fixtures - TODAY
3+
* @module fixtures/TODAY
4+
*/
5+
6+
export default new Date()

src/utils/__tests__/clone.spec.ts

+22-10
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,10 @@
44
*/
55

66
import INTEGER from '#fixtures/integer'
7+
import TODAY from '#fixtures/today'
78
import type Vehicle from '#fixtures/types/vehicle'
8-
import VEHICLE from '#fixtures/vehicle'
9+
import VEHICLE, { VEHICLE_TAG } from '#fixtures/vehicle'
910
import testSubject from '../clone'
10-
import isObjectPlain from '../is-object-plain'
1111

1212
describe('unit:utils/clone', () => {
1313
describe('ArrayBuffer', () => {
@@ -52,6 +52,17 @@ describe('unit:utils/clone', () => {
5252
})
5353
})
5454

55+
describe('Date', () => {
56+
it('should return deep cloned Date instance', () => {
57+
// Act
58+
const result = testSubject(TODAY)
59+
60+
// Expect
61+
expect(result).to.be.instanceof(Date)
62+
expect(result).to.eql(TODAY).but.not.equal(TODAY)
63+
})
64+
})
65+
5566
describe('Map', () => {
5667
it('should return deep cloned Map instance', () => {
5768
// Arrange
@@ -290,20 +301,21 @@ describe('unit:utils/clone', () => {
290301
})
291302

292303
describe('pojos', () => {
293-
it('should return deep cloned plain object', () => {
294-
// Arrange
295-
const value: Vehicle & { driver: { id: string } } = {
296-
...VEHICLE,
297-
driver: { id: faker.string.uuid() }
298-
}
304+
let value: Vehicle & { driver: { id: string } }
299305

306+
beforeAll(() => {
307+
value = { ...VEHICLE, driver: { id: faker.string.uuid() } }
308+
value = Object.defineProperty(value, VEHICLE_TAG, { value: VEHICLE.vin })
309+
value = Object.defineProperty(value, '__value', { value })
310+
})
311+
312+
it('should return deep cloned plain object', () => {
300313
// Act
301314
const result = testSubject(value)
302315

303316
// Expect
304-
expect(result).to.satisfy(isObjectPlain)
305317
expect(result).to.eql(value).but.not.equal(value)
306-
expect(result.driver).to.not.equal(value.driver)
318+
expect(result).to.have.property('driver').not.equal(value.driver)
307319
})
308320
})
309321

src/utils/clone.ts

+79-58
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
* @module tutils/utils/clone
44
*/
55

6-
import type { Class, Optional } from '#src/types'
6+
import type { PropertyDescriptor } from '#src/interfaces'
7+
import type { Class, Objectify } from '#src/types'
78
import cast from './cast'
89
import define from './define'
910
import descriptor from './descriptor'
@@ -12,12 +13,14 @@ import isArray from './is-array'
1213
import isArrayBuffer from './is-array-buffer'
1314
import isBuffer from './is-buffer'
1415
import isDataView from './is-data-view'
16+
import isDate from './is-date'
1517
import isFunction from './is-function'
1618
import isMap from './is-map'
1719
import isPrimitive from './is-primitive'
1820
import isRegExp from './is-reg-exp'
1921
import isSet from './is-set'
2022
import isTypedArray from './is-typed-array'
23+
import isUndefined from './is-undefined'
2124
import properties from './properties'
2225

2326
/**
@@ -30,103 +33,121 @@ import properties from './properties'
3033
* @todo document differences between structured clone algorithm
3134
* @todo examples
3235
*
33-
* @template T - Value type
36+
* @template T - Cloned value type
3437
*
3538
* @param {T} value - Value to recursively clone
3639
* @return {T} Deep cloned `value`
3740
*/
3841
const clone = <T>(value: T): T => {
39-
// primitives do not need to be cloned
40-
if (isPrimitive(value)) return value
41-
42-
// bind functions to empty objects to create clones
43-
if (isFunction(value)) return cast(value.bind({}))
44-
4542
/**
46-
* Object to clone.
43+
* Cloned values cache.
4744
*
48-
* @const {T & object} obj
45+
* @const {WeakMap<Objectify<any>, Objectify<any>>} cache
4946
*/
50-
const obj: T & object = cast(value)
47+
const cache: WeakMap<Objectify<any>, Objectify<any>> = new WeakMap()
5148

5249
/**
53-
* Initializes a cloned object.
50+
* Deep clones `value`.
5451
*
55-
* @template T - Object type
52+
* @template T - Value type
5653
*
57-
* @param {T} obj - Object to clone
58-
* @return {T} Initialized clone
54+
* @param {T} value - Value to deep clone
55+
* @return {T} Deep cloned `value`
5956
*/
60-
const init = <T extends object>(obj: T): T => {
57+
const dclone = <T>(value: T): T => {
58+
// primitives do not need to be cloned
59+
if (isPrimitive(value)) return value
60+
61+
// bind functions to empty objects to create clones
62+
if (isFunction(value)) return cast(value.bind({}))
63+
64+
/**
65+
* Object to clone.
66+
*
67+
* @const {Objectify<any> & T} obj
68+
*/
69+
const obj: Objectify<any> & T = cast(value)
70+
6171
/**
6272
* Cloned object constructor.
6373
*
6474
* @const {Class<T>} Clone
6575
*/
66-
const Clone: Class<T> = cast<Class<T>>(obj.constructor)
76+
const Clone: Class<Objectify<any> & T> = cast(obj.constructor)
6777

6878
/**
69-
* Initialized clone.
79+
* Cloned value.
7080
*
71-
* @var {T} result
81+
* @var {Objectify<any> & T} cloned
7282
*/
73-
let result!: T
83+
let cloned!: Objectify<any> & T
7484

75-
// clone array value
76-
if (isArray(obj)) result = new Clone(obj.length)
85+
// init cloned array
86+
if (isArray(obj)) cloned = new Clone(obj.length)
7787

78-
// clone array buffer
88+
// init cloned array buffer
7989
if (isArrayBuffer(obj)) {
80-
result = new Clone(obj.byteLength)
81-
new Uint8Array(cast(result)).set(new Uint8Array(obj))
90+
cloned = new Clone(obj.byteLength)
91+
new Uint8Array(cast(cloned)).set(new Uint8Array(obj))
8292
}
8393

84-
// clone buffer
85-
if (isBuffer(obj)) result = cast(Uint8Array.prototype.slice.call(obj))
94+
// init cloned buffer
95+
if (isBuffer(obj)) cloned = cast(Uint8Array.prototype.slice.call(obj))
8696

87-
// clone dataview
97+
// init cloned data view
8898
if (isDataView(obj)) {
89-
result = new Clone(init(obj.buffer), obj.byteOffset, obj.byteLength)
99+
cloned = new Clone(dclone(obj.buffer), obj.byteOffset, obj.byteLength)
90100
}
91101

92-
// clone regexp
93-
if (isRegExp(obj)) result = new Clone(obj.source, obj.flags)
102+
// init cloned date
103+
if (isDate(obj)) cloned = new Clone(obj.getTime())
104+
105+
// init cloned regex
106+
if (isRegExp(obj)) cloned = new Clone(obj.source, obj.flags)
94107

95-
// clone typed array
108+
// init cloned typed array
96109
if (isTypedArray(obj)) {
97-
result = new Clone(init(obj.buffer), obj.byteOffset, obj.length)
110+
cloned = new Clone(dclone(obj.buffer), obj.byteOffset, obj.length)
98111
}
99112

100-
// clone unknown value
101-
if (!cast<Optional<T>>(result) && isFunction(Clone)) result = new Clone()
113+
// init unknown clone
114+
if (isUndefined(cloned) && isFunction(Clone)) cloned = new Clone()
115+
116+
// check for circular references and return corresponding clone
117+
if (cache.has(obj)) return cast(cache.get(obj))
118+
119+
// cache object and clone
120+
cache.set(obj, (cloned = ifelse(cloned, cloned, cast({}))))
121+
122+
// define own properties of initial object on cloned object
123+
for (const key of properties(obj)) {
124+
/**
125+
* Property descriptor for {@linkcode key}.
126+
*
127+
* @const {PropertyDescriptor} $d
128+
*/
129+
const $d: PropertyDescriptor = descriptor(obj, key)
130+
131+
// define property on cloned object
132+
define(
133+
cloned,
134+
key,
135+
ifelse($d.value, { ...$d, value: dclone($d.value) }, $d)
136+
)
137+
}
102138

103-
return ifelse(result, result, cast({}))
104-
}
139+
// repopulate map
140+
if (isMap(obj) && isMap(cloned)) {
141+
for (const [key, val] of obj.entries()) cloned.set(key, dclone(val))
142+
}
105143

106-
/**
107-
* Cloned {@linkcode obj}.
108-
*
109-
* @const {T} result
110-
*/
111-
const ret: T & object = init(obj)
112-
113-
// redefine own properties on cloned object
114-
for (const key of properties(obj)) {
115-
define(ret, key, {
116-
...descriptor(obj, key),
117-
value: clone(descriptor(obj, key).value)
118-
})
119-
}
144+
// repopulate set
145+
if (isSet(obj) && isSet(cloned)) for (const v of obj) cloned.add(dclone(v))
120146

121-
// repopulate map
122-
if (isMap(obj) && isMap(ret)) {
123-
for (const [key, val] of obj.entries()) ret.set(key, clone(val))
147+
return cloned
124148
}
125149

126-
// repopulate set
127-
if (isSet(obj) && isSet(ret)) for (const val of obj) ret.add(clone(val))
128-
129-
return ret
150+
return dclone(value)
130151
}
131152

132153
export default clone

src/utils/is-array.ts

+1-3
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,6 @@
33
* @module tutils/utils/isArray
44
*/
55

6-
import isObject from './is-object'
7-
86
/**
97
* Checks if `value` is an array.
108
*
@@ -16,7 +14,7 @@ import isObject from './is-object'
1614
* @return {value is ReadonlyArray<T> | T[]} `true` if `value` is an array
1715
*/
1816
const isArray = <T>(value: unknown): value is T[] | readonly T[] => {
19-
return isObject(value) && value.constructor === Array
17+
return Array.isArray(value)
2018
}
2119

2220
export default isArray

src/utils/is-buffer.ts

+1-5
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,6 @@
33
* @module tutils/utils/isBuffer
44
*/
55

6-
import isObject from './is-object'
7-
86
/**
97
* Checks if `value` is a {@linkcode Buffer} instance.
108
*
@@ -15,8 +13,6 @@ import isObject from './is-object'
1513
* @param {unknown} value - Value to check
1614
* @return {value is Buffer} `true` if `value` is a `Buffer`
1715
*/
18-
const isBuffer = (value: unknown): value is Buffer => {
19-
return isObject(value) && value.constructor === Buffer
20-
}
16+
const isBuffer = (value: unknown): value is Buffer => Buffer.isBuffer(value)
2117

2218
export default isBuffer

0 commit comments

Comments
 (0)