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.
This commit is contained in:
bymyself
2025-08-11 14:27:54 -07:00
parent 16d7436883
commit a2c8f4b3cc
2 changed files with 33 additions and 15 deletions

View File

@@ -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

View File

@@ -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<T, null> {
if (obj == null) return null as WhenNullish<T, null>
const r = JSON.parse(JSON.stringify(obj))
const r = _.cloneDeep(obj)
if (!target) return r
for (const i in r) {