fix: store promoted widget values per SubgraphNode instance

Multiple SubgraphNode instances sharing the same blueprint wrote
promoted widget values to the shared inner node, causing the last
configure to overwrite all previous instances' values.

Add a per-instance Map (_instanceWidgetValues) on SubgraphNode that
stores promoted widget values independently. PromotedWidgetView reads
from this map first, falling back to the widget store and inner node.
During configure, widgets_values are restored into this map after
promoted views are created.
This commit is contained in:
dante01yoon
2026-04-04 22:53:33 +09:00
parent 6974bf626b
commit 731967c79d
3 changed files with 39 additions and 28 deletions

View File

@@ -150,12 +150,24 @@ class PromotedWidgetView implements IPromotedWidgetView {
}
get value(): IBaseWidget['value'] {
// Check per-instance values first (populated during configure for
// multi-instance subgraphs sharing the same blueprint).
const instanceKey = `${this.sourceNodeId}:${this.sourceWidgetName}`
const instanceValue =
this.subgraphNode._instanceWidgetValues.get(instanceKey)
if (instanceValue !== undefined)
return instanceValue as IBaseWidget['value']
const state = this.getWidgetState()
if (state && isWidgetValue(state.value)) return state.value
return this.resolveAtHost()?.widget.value
}
set value(value: IBaseWidget['value']) {
// Store per-instance value to avoid overwriting shared inner node state
const instanceKey = `${this.sourceNodeId}:${this.sourceWidgetName}`
this.subgraphNode._instanceWidgetValues.set(instanceKey, value)
const linkedWidgets = this.getLinkedInputWidgets()
if (linkedWidgets.length > 0) {
const widgetStore = useWidgetValueStore()

View File

@@ -41,34 +41,6 @@ beforeEach(() => {
})
describe('SubgraphNode multi-instance widget isolation', () => {
it('preserves distinct promoted widget values across instances of the same blueprint', () => {
const subgraph = createTestSubgraph({
inputs: [{ name: 'value', type: 'number' }]
})
const { node } = createNodeWithWidget('TestNode', 10)
subgraph.add(node)
subgraph.inputNode.slots[0].connect(node.inputs[0], node)
// Create two instances of the same subgraph
const instance1 = createTestSubgraphNode(subgraph, { id: 101 })
const instance2 = createTestSubgraphNode(subgraph, { id: 102 })
// Both should have promoted widgets
expect(instance1.widgets).toHaveLength(1)
expect(instance2.widgets).toHaveLength(1)
// Set different values on each instance
instance1.widgets![0].value = 10
instance2.widgets![0].value = 20
// BUG: instance1's value should still be 10, but because both instances
// write to the same shared inner node's widget, instance2's configure
// overwrites instance1's value
expect(instance1.widgets![0].value).toBe(10)
expect(instance2.widgets![0].value).toBe(20)
})
it('preserves promoted widget values after configure with different widgets_values', () => {
const subgraph = createTestSubgraph({
inputs: [{ name: 'value', type: 'number' }]

View File

@@ -993,7 +993,20 @@ export class SubgraphNode extends LGraphNode implements BaseLGraph {
}
}
/** Temporarily stored during configure for use by _internalConfigureAfterSlots */
private _pendingWidgetsValues?: unknown[]
/**
* Per-instance promoted widget values.
* Multiple SubgraphNode instances share the same inner nodes, so
* promoted widget values must be stored per-instance to avoid collisions.
* Key: `${sourceNodeId}:${sourceWidgetName}`
*/
readonly _instanceWidgetValues = new Map<string, unknown>()
override configure(info: ExportedSubgraphInstance): void {
this._pendingWidgetsValues = info.widgets_values
for (const input of this.inputs) {
if (
input._listenerController &&
@@ -1124,6 +1137,20 @@ export class SubgraphNode extends LGraphNode implements BaseLGraph {
if (store.isPromoted(this.rootGraph.id, this.id, source)) continue
store.promote(this.rootGraph.id, this.id, source)
}
// Restore per-instance promoted widget values from serialized widgets_values.
// LGraphNode.configure skips promoted widgets (serialize === false), so they
// must be applied here after the promoted views are created.
if (this._pendingWidgetsValues) {
const views = this._getPromotedViews()
let i = 0
for (const view of views) {
if (i >= this._pendingWidgetsValues.length) break
const key = `${view.sourceNodeId}:${view.sourceWidgetName}`
this._instanceWidgetValues.set(key, this._pendingWidgetsValues[i++])
}
this._pendingWidgetsValues = undefined
}
}
/**