diff --git a/src/value/clone/clone.ts b/src/value/clone/clone.ts index 7ea2261b4..3a7d36964 100644 --- a/src/value/clone/clone.ts +++ b/src/value/clone/clone.ts @@ -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): any { + if (cache.has(value)) return cache.get(value) const Acc = {} as Record + 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): 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() @@ -67,13 +75,13 @@ function FromValue(value: ValueType): any { // Clone // ------------------------------------------------------------------ /** Returns a clone of the given value */ -export function Clone(value: T): T { - if (IsArray(value)) return FromArray(value) +export function Clone(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') } diff --git a/test/runtime/value/clone/clone.ts b/test/runtime/value/clone/clone.ts index 15ecb1f57..b12edaf96 100644 --- a/test/runtime/value/clone/clone.ts +++ b/test/runtime/value/clone/clone.ts @@ -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) + }) })