Browse Source
- Install zundo temporal middleware for Zustand - Create src/store/undoUtils.ts with: - BINARY_DATA_KEYS: Set of 20+ binary field names to exclude - stripBinaryData(): Deep-clone nodes without binary data - partializeForUndo(): Extract trackable state slice - undoStateEquality(): Fast referential equality check - UndoState type for type safetyhandoff-20260429-1057
3 changed files with 143 additions and 2 deletions
@ -0,0 +1,121 @@ |
|||||
|
import type { WorkflowNode, WorkflowEdge, NodeGroup } from "@/types"; |
||||
|
import type { EdgeStyle } from "./workflowStore"; |
||||
|
|
||||
|
/** |
||||
|
* Binary data field names that must be excluded from undo snapshots. |
||||
|
* These fields contain base64 data URLs that are large and unnecessary for undo/redo. |
||||
|
*/ |
||||
|
export const BINARY_DATA_KEYS = new Set([ |
||||
|
// Image fields
|
||||
|
"image", |
||||
|
"outputImage", |
||||
|
"sourceImage", |
||||
|
"inputImages", |
||||
|
"images", |
||||
|
"imageA", |
||||
|
"imageB", |
||||
|
"capturedImage", |
||||
|
|
||||
|
// Video fields
|
||||
|
"outputVideo", |
||||
|
"video", |
||||
|
|
||||
|
// Audio fields
|
||||
|
"audioFile", |
||||
|
"outputAudio", |
||||
|
"audio", |
||||
|
|
||||
|
// 3D fields
|
||||
|
"glbUrl", |
||||
|
"output3dUrl", |
||||
|
|
||||
|
// History arrays (can be large)
|
||||
|
"imageHistory", |
||||
|
"videoHistory", |
||||
|
"audioHistory", |
||||
|
"globalImageHistory", |
||||
|
|
||||
|
// Thumbnail fields
|
||||
|
"thumbnail", |
||||
|
"thumbnails", |
||||
|
]); |
||||
|
|
||||
|
/** |
||||
|
* Strips binary data from node data objects to reduce undo snapshot size. |
||||
|
* Creates new node objects without mutating the originals. |
||||
|
* |
||||
|
* @param nodes - Array of workflow nodes |
||||
|
* @returns New array of nodes with binary data fields removed from node.data |
||||
|
*/ |
||||
|
export function stripBinaryData(nodes: WorkflowNode[]): WorkflowNode[] { |
||||
|
return nodes.map(node => { |
||||
|
const strippedData: Record<string, any> = {}; |
||||
|
|
||||
|
// Copy all non-binary fields
|
||||
|
for (const key of Object.keys(node.data)) { |
||||
|
if (!BINARY_DATA_KEYS.has(key)) { |
||||
|
strippedData[key] = node.data[key]; |
||||
|
} |
||||
|
// Binary fields are simply omitted (will be undefined in the result)
|
||||
|
} |
||||
|
|
||||
|
return { |
||||
|
...node, |
||||
|
data: strippedData as typeof node.data, |
||||
|
}; |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* State shape tracked by undo/redo. |
||||
|
* Only includes fields that affect the workflow graph structure. |
||||
|
*/ |
||||
|
export type UndoState = { |
||||
|
nodes: WorkflowNode[]; |
||||
|
edges: WorkflowEdge[]; |
||||
|
edgeStyle: EdgeStyle; |
||||
|
groups: Record<string, NodeGroup>; |
||||
|
}; |
||||
|
|
||||
|
/** |
||||
|
* Partializes the store state for undo tracking. |
||||
|
* Returns only the fields we want to track in undo history. |
||||
|
* |
||||
|
* @param state - Full workflow store state |
||||
|
* @returns Partialized state with binary data stripped from nodes |
||||
|
*/ |
||||
|
export function partializeForUndo(state: any): UndoState { |
||||
|
return { |
||||
|
nodes: stripBinaryData(state.nodes), |
||||
|
edges: state.edges, |
||||
|
edgeStyle: state.edgeStyle, |
||||
|
groups: state.groups, |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Fast equality check for undo states using referential comparison. |
||||
|
* Zustand creates new object/array references on every mutation, |
||||
|
* so we can use === checks instead of deep equality. |
||||
|
* |
||||
|
* @param past - Previous undo state |
||||
|
* @param current - Current undo state |
||||
|
* @returns true if states are equal (skip snapshot), false otherwise |
||||
|
*/ |
||||
|
export function undoStateEquality(past: UndoState, current: UndoState): boolean { |
||||
|
// Fast checks for primitive/reference changes
|
||||
|
if (past.edges !== current.edges) return false; |
||||
|
if (past.edgeStyle !== current.edgeStyle) return false; |
||||
|
if (past.groups !== current.groups) return false; |
||||
|
|
||||
|
// Check nodes array
|
||||
|
if (past.nodes.length !== current.nodes.length) return false; |
||||
|
|
||||
|
// Check each node reference (Zustand creates new refs on change)
|
||||
|
for (let i = 0; i < past.nodes.length; i++) { |
||||
|
if (past.nodes[i] !== current.nodes[i]) return false; |
||||
|
} |
||||
|
|
||||
|
// All checks passed - states are equal
|
||||
|
return true; |
||||
|
} |
||||
Loading…
Reference in new issue