Skip to content
Open
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
23 changes: 11 additions & 12 deletions src/js/core/tools/Helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,31 +32,30 @@ export default class Helpers{
return output;
}

static deepClone(obj, clone, list = []){
static deepClone(obj, clone, circularRefs = new WeakMap()){
var objectProto = {}.__proto__,
arrayProto = [].__proto__;

if (!clone){
clone = Object.assign(Array.isArray(obj) ? [] : {}, obj);
}

//map each source object to its clone so shared/circular references resolve
//to the same clone instead of being re-cloned (O(1) lookup vs a scanned list)
circularRefs.set(obj, clone);

for(var i in obj) {
let subject = obj[i],
match, copy;
let subject = obj[i];

if(subject != null && typeof subject === "object" && (subject.__proto__ === objectProto || subject.__proto__ === arrayProto)){
match = list.findIndex((item) => {
return item.subject === subject;
});
const existing = circularRefs.get(subject);

if(match > -1){
clone[i] = list[match].copy;
if(existing){
clone[i] = existing;
}else{
copy = Object.assign(Array.isArray(subject) ? [] : {}, subject);

list.unshift({subject, copy});
let copy = Object.assign(Array.isArray(subject) ? [] : {}, subject);

clone[i] = this.deepClone(subject, copy, list);
clone[i] = this.deepClone(subject, copy, circularRefs);
}
}
}
Expand Down
73 changes: 73 additions & 0 deletions test/unit/core/tools/Helpers.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import Helpers from "../../../../src/js/core/tools/Helpers";

// deepClone must produce an independent deep copy while preserving shared and
// circular reference identity (the case the WeakMap keying must get right).

describe("Helpers.deepClone", () => {
test("deep copies nested objects and arrays (values equal, refs distinct)", () => {
const src = { a: 1, nested: { b: 2, list: [1, 2, { c: 3 }] } };
const out = Helpers.deepClone(src);
expect(out).toEqual(src);
expect(out).not.toBe(src);
expect(out.nested).not.toBe(src.nested);
expect(out.nested.list).not.toBe(src.nested.list);
expect(out.nested.list[2]).not.toBe(src.nested.list[2]);
});

test("mutating the clone does not affect the source", () => {
const src = { nested: { n: 1 }, arr: [{ x: 1 }] };
const out = Helpers.deepClone(src);
out.nested.n = 99;
out.arr[0].x = 99;
expect(src.nested.n).toBe(1);
expect(src.arr[0].x).toBe(1);
});

test("preserves shared references (same object referenced twice -> one clone)", () => {
const shared = { v: 1 };
const src = { a: shared, b: shared };
const out = Helpers.deepClone(src);
expect(out.a).toBe(out.b); // same clone instance
expect(out.a).not.toBe(shared); // but a copy, not the original
});

test("handles a nested circular reference (identity preserved, no infinite loop)", () => {
const a = { name: "a" };
const b = { name: "b" };
a.child = b;
b.parent = a; // circular
const out = Helpers.deepClone(a);
expect(out.name).toBe("a");
expect(out.child.name).toBe("b");
expect(out.child.parent).toBe(out); // circular ref points back to the clone of a
expect(out.child.parent).not.toBe(a);
});

test("handles a self (root) circular reference", () => {
const obj = { name: "root" };
obj.self = obj;
const out = Helpers.deepClone(obj);
expect(out.name).toBe("root");
expect(out.self).toBe(out);
});

test("handles many circular refs without blowing the stack or corrupting identity", () => {
const root = {};
let prev = root;
for (let i = 0; i < 500; i++) {
const node = { i, back: prev };
prev.next = node;
prev = node;
}
const out = Helpers.deepClone(root);
// walk and confirm each node's back-ref resolves to the correct cloned node
let cur = out;
let count = 0;
while (cur.next) {
expect(cur.next.back).toBe(cur);
cur = cur.next;
count++;
}
expect(count).toBe(500);
});
});
Loading