Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 16 additions & 8 deletions src/value/clone/clone.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,18 +35,26 @@ import { IsArray, IsDate, IsMap, IsSet, IsObject, IsTypedArray, IsValueType } fr
// ------------------------------------------------------------------
// Clonable
// ------------------------------------------------------------------
function FromObject(value: FromObject): any {
function FromObject(value: FromObject, cache: WeakMap<object, unknown>): any {
if (cache.has(value)) return cache.get(value)
const Acc = {} as Record<PropertyKey, unknown>
cache.set(value, Acc)
for (const key of Object.getOwnPropertyNames(value)) {
Acc[key] = Clone(value[key])
Acc[key] = Clone(value[key], cache)
}
for (const key of Object.getOwnPropertySymbols(value)) {
Acc[key] = Clone(value[key])
Acc[key] = Clone(value[key], cache)
}
return Acc
}
function FromArray(value: FromArray): any {
return value.map((element: any) => Clone(element))
function FromArray(value: FromArray, cache: WeakMap<object, unknown>): any {
if (cache.has(value)) return cache.get(value)
const Acc: any[] = []
cache.set(value, Acc)
for (let i = 0; i < value.length; i++) {
Acc.push(Clone(value[i], cache))
}
return Acc
}
function FromTypedArray(value: TypedArrayType): any {
return value.slice()
Expand All @@ -67,13 +75,13 @@ function FromValue(value: ValueType): any {
// Clone
// ------------------------------------------------------------------
/** Returns a clone of the given value */
export function Clone<T extends unknown>(value: T): T {
if (IsArray(value)) return FromArray(value)
export function Clone<T extends unknown>(value: T, cache = new WeakMap()): T {
if (IsArray(value)) return FromArray(value, cache)
if (IsDate(value)) return FromDate(value)
if (IsTypedArray(value)) return FromTypedArray(value)
if (IsMap(value)) return FromMap(value)
if (IsSet(value)) return FromSet(value)
if (IsObject(value)) return FromObject(value)
if (IsObject(value)) return FromObject(value, cache)
if (IsValueType(value)) return FromValue(value)
throw new Error('ValueClone: Unable to clone value')
}
97 changes: 97 additions & 0 deletions test/runtime/value/clone/clone.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,4 +153,101 @@ describe('value/clone/Clone', () => {
const R = Value.Clone(V)
Assert.IsEqual(R, V)
})
// ------------------------------------------------------------------------
// ref: https://github.com/sinclairzx81/typebox/issues/1300
// ------------------------------------------------------------------------
it('Should handle circular references #1', () => {
const V = { a: 1, b: { c: 2 } } as any
V.b.d = V.b
const R = Value.Clone(V)
Assert.IsEqual(R, V)
})
it('Should handle circular references #2', () => {
const V = { a: {}, b: {} } as any
V.a.c = V.b
V.b.d = V.a
const R = Value.Clone(V)
console.log(R)
Assert.IsEqual(R, V)
})
it('Should handle indirect circular references #1', () => {
// Create a chain: A -> B -> C -> A
const A = { name: 'A' } as any
const B = { name: 'B' } as any
const C = { name: 'C' } as any

A.next = B
B.next = C
C.next = A // Circular reference through chain

const R = Value.Clone(A)
Assert.IsEqual(R.name, 'A')
Assert.IsEqual(R.next.name, 'B')
Assert.IsEqual(R.next.next.name, 'C')
Assert.IsEqual(R.next.next.next, R) // Should reference back to root
})
it('Should handle indirect circular references #2', () => {
// Create a more complex structure with multiple indirect references
const root = {
data: { value: 1 },
children: [],
metadata: {},
} as any

const child1 = {
id: 1,
parent: root,
siblings: [],
} as any

const child2 = {
id: 2,
parent: root,
siblings: [],
} as any

// Set up the circular references
root.children = [child1, child2]
child1.siblings = [child2]
child2.siblings = [child1]
root.metadata.firstChild = child1

const R = Value.Clone(root)

// Verify structure integrity
Assert.IsEqual(R.data.value, 1)
Assert.IsEqual(R.children.length, 2)
Assert.IsEqual(R.children[0].id, 1)
Assert.IsEqual(R.children[1].id, 2)

// Verify circular references are maintained
Assert.IsEqual(R.children[0].parent, R)
Assert.IsEqual(R.children[1].parent, R)
Assert.IsEqual(R.children[0].siblings[0], R.children[1])
Assert.IsEqual(R.children[1].siblings[0], R.children[0])
Assert.IsEqual(R.metadata.firstChild, R.children[0])
})
it('Should handle deep indirect circular references', () => {
// Create a deeply nested structure with circular reference at the end
const V = {
level1: {
level2: {
level3: {
level4: {
level5: {},
},
},
},
},
} as any

// Create circular reference from deep level back to root
V.level1.level2.level3.level4.level5.backToRoot = V
V.level1.level2.level3.level4.level5.backToLevel2 = V.level1.level2

const R = Value.Clone(V)

// Verify the structure and circular references
Assert.IsEqual(R, V)
})
})
Loading