From 1c785aa0391b4986ea3cc483c1493bfca023776e Mon Sep 17 00:00:00 2001 From: shrimbly Date: Thu, 2 Apr 2026 06:05:53 +1300 Subject: [PATCH] fix: replace JSON deep-clone with clonePreservingStrings in clipboard and snapshot copySelectedNodes, pasteNodes, and AI captureSnapshot were still using JSON.parse(JSON.stringify(...)) which duplicates multi-MB base64 blobs. Switched to clonePreservingStrings which shares immutable string refs. Also clarified docstring re: toJSON not being called. Co-Authored-By: Claude Sonnet 4.5 --- src/store/undoHistory.ts | 6 +++++- src/store/workflowStore.ts | 16 ++++++++-------- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/src/store/undoHistory.ts b/src/store/undoHistory.ts index f820c945..579ef318 100644 --- a/src/store/undoHistory.ts +++ b/src/store/undoHistory.ts @@ -57,9 +57,13 @@ export class UndoManager { * snapshots, but strings (immutable in JS) are returned by reference. * This avoids duplicating multi-MB base64 blobs across undo history. * - * Matches JSON.parse(JSON.stringify()) semantics: + * Matches JSON.parse(JSON.stringify()) semantics for plain JSON-like + * objects/arrays: * - `undefined` values are dropped from objects, become `null` in arrays * - functions are dropped from objects, become `null` in arrays + * + * Does NOT call toJSON() on objects. Objects with custom toJSON methods + * are treated as plain objects (their enumerable own properties are cloned). */ export function clonePreservingStrings(value: T): T { if (value === null || typeof value !== "object") { diff --git a/src/store/workflowStore.ts b/src/store/workflowStore.ts index 5ec2c5d3..e97e3478 100644 --- a/src/store/workflowStore.ts +++ b/src/store/workflowStore.ts @@ -907,8 +907,8 @@ const workflowStoreImpl: StateCreator = (set, get) => ({ ); // Deep clone the nodes and edges to avoid reference issues - const clonedNodes = JSON.parse(JSON.stringify(selectedNodes)) as WorkflowNode[]; - const clonedEdges = JSON.parse(JSON.stringify(connectedEdges)) as WorkflowEdge[]; + const clonedNodes = clonePreservingStrings(selectedNodes) as WorkflowNode[]; + const clonedEdges = clonePreservingStrings(connectedEdges) as WorkflowEdge[]; set({ clipboard: { nodes: clonedNodes, edges: clonedEdges } }); }, @@ -946,7 +946,7 @@ const workflowStoreImpl: StateCreator = (set, get) => ({ width: undefined, height: undefined, measured: undefined, - data: JSON.parse(JSON.stringify(node.data)), + data: clonePreservingStrings(node.data), }; }); @@ -2536,12 +2536,12 @@ const workflowStoreImpl: StateCreator = (set, get) => ({ captureSnapshot: () => { const state = get(); // Deep copy the current workflow state to avoid reference sharing - const snapshot = { - nodes: JSON.parse(JSON.stringify(state.nodes)), - edges: JSON.parse(JSON.stringify(state.edges)), - groups: JSON.parse(JSON.stringify(state.groups)), + const snapshot = clonePreservingStrings({ + nodes: state.nodes, + edges: state.edges, + groups: state.groups, edgeStyle: state.edgeStyle, - }; + }); set({ previousWorkflowSnapshot: snapshot, manualChangeCount: 0,