From a2c8f4b3ccd6ea6ebc847d36069ddeb88bc8987c Mon Sep 17 00:00:00 2001 From: bymyself Date: Mon, 11 Aug 2025 14:27:54 -0700 Subject: [PATCH] fix(litegraph): Resolve workflow corruption from subgraph serialization This commit fixes a critical issue where saving a workflow with complex subgraphs could lead to a corrupted file, causing errors upon loading. The root cause was twofold: 1. **Unsafe Object Cloning:** The `LiteGraph.cloneObject` function used `JSON.stringify`, which cannot handle circular references that can exist in a nodes properties, especially in complex graphs with promoted widgets. This has been replaced with `_.cloneDeep` from lodash for robust and safe deep cloning. 2. **Unsafe Node Configuration:** The `LGraphNode.configure` method used a generic loop that would overwrite the `inputs` and `outputs` arrays with plain objects from the serialized data. This created a temporary but dangerous state where class instances were replaced by plain objects, leading to `TypeError` exceptions when methods or private fields were accessed on them. The `configure` method has been refactored to handle `inputs`, `outputs`, and `properties` explicitly and safely, ensuring they are always proper class instances. This fix makes the node loading process more robust and predictable, preventing data corruption and ensuring the stability of workflows with subgraphs. --- src/lib/litegraph/src/LGraphNode.ts | 44 ++++++++++++++++-------- src/lib/litegraph/src/LiteGraphGlobal.ts | 4 ++- 2 files changed, 33 insertions(+), 15 deletions(-) diff --git a/src/lib/litegraph/src/LGraphNode.ts b/src/lib/litegraph/src/LGraphNode.ts index 14bfee485..ac509e993 100644 --- a/src/lib/litegraph/src/LGraphNode.ts +++ b/src/lib/litegraph/src/LGraphNode.ts @@ -769,12 +769,7 @@ export class LGraphNode this.graph._version++ } for (const j in info) { - if (j == 'properties') { - // i don't want to clone properties, I want to reuse the old container - for (const k in info.properties) { - this.properties[k] = info.properties[k] - this.onPropertyChanged?.(k, info.properties[k]) - } + if (j === 'properties' || j === 'inputs' || j === 'outputs') { continue } @@ -798,14 +793,28 @@ export class LGraphNode } } + if (info.properties) { + for (const k in info.properties) { + this.properties[k] = info.properties[k] + this.onPropertyChanged?.(k, info.properties[k]) + } + } + if (!info.title) { this.title = this.constructor.title } - this.inputs ??= [] - this.inputs = this.inputs.map((input) => - toClass(NodeInputSlot, input, this) - ) + if (info.inputs) { + this.inputs = info.inputs.map((input) => + toClass(NodeInputSlot, input, this) + ) + } else { + this.inputs ??= [] + this.inputs = this.inputs.map((input) => + toClass(NodeInputSlot, input, this) + ) + } + for (const [i, input] of this.inputs.entries()) { const link = this.graph && input.link != null @@ -815,10 +824,17 @@ export class LGraphNode this.onInputAdded?.(input) } - this.outputs ??= [] - this.outputs = this.outputs.map((output) => - toClass(NodeOutputSlot, output, this) - ) + if (info.outputs) { + this.outputs = info.outputs.map((output) => + toClass(NodeOutputSlot, output, this) + ) + } else { + this.outputs ??= [] + this.outputs = this.outputs.map((output) => + toClass(NodeOutputSlot, output, this) + ) + } + for (const [i, output] of this.outputs.entries()) { if (!output.links) continue diff --git a/src/lib/litegraph/src/LiteGraphGlobal.ts b/src/lib/litegraph/src/LiteGraphGlobal.ts index e27377fed..738942904 100644 --- a/src/lib/litegraph/src/LiteGraphGlobal.ts +++ b/src/lib/litegraph/src/LiteGraphGlobal.ts @@ -1,3 +1,5 @@ +import _ from 'lodash' + import { ContextMenu } from './ContextMenu' import { CurveEditor } from './CurveEditor' import { DragAndScale } from './DragAndScale' @@ -641,7 +643,7 @@ export class LiteGraphGlobal { ): WhenNullish { if (obj == null) return null as WhenNullish - const r = JSON.parse(JSON.stringify(obj)) + const r = _.cloneDeep(obj) if (!target) return r for (const i in r) {