## Summary
Wraps `LGraphNode.configure()`'s widget-value restoration phase in a
hydration transaction (`beginHydration` / `commitHydration`), preventing
derived-state callbacks (e.g. CustomCombo's `updateCombo`) from firing
before all widget values are restored.
### Architecture
```mermaid
flowchart LR
subgraph core["Core Layer"]
LGN["LGraphNode.configure()"]
WVS["widgetValueStore"]
end
subgraph extensions["Extension Layer"]
CW["CustomCombo\n(customWidgets.ts)"]
PN["PrimitiveNode\n(widgetInputs.ts)"]
end
LGN -- "beginHydration /\ncommitHydration" --> WVS
CW -- "isHydrating() guard" --> WVS
CW -- "onHydrationComplete()" --> WVS
PN -- "serialize() override\npreserves widgets_values" --> LGN
```
### Hydration Sequence
```mermaid
sequenceDiagram
participant Caller as Paste / Load
participant Node as LGraphNode.configure()
participant Store as widgetValueStore
participant Widget as Widget Setters
participant Combo as CustomCombo.updateCombo
Caller->>Node: configure(serializedData)
Node->>Store: beginHydration(nodeId)
activate Store
Note over Store: hydratingNodes.add(nodeId)
loop For each widget
Node->>Widget: widget.value = restored_value
Widget->>Combo: updateCombo() triggered
Combo->>Store: isHydrating(nodeId)?
Store-->>Combo: true - early return
Note over Combo: Side effect suppressed
end
Node->>Node: onConfigure(info)
Note over Node: Extensions register onHydrationComplete callbacks
Node->>Store: commitHydration(nodeId)
Note over Store: hydratingNodes.delete(nodeId)
Store->>Combo: fire queued callbacks
Note over Combo: updateCombo() runs once with all values present
deactivate Store
```
### Before / After
```mermaid
flowchart TB
subgraph before["Before: Race Condition"]
direction TB
B1["configure starts"] --> B2["widget1.value = optionA"]
B2 --> B3["updateCombo fires immediately"]
B3 --> B4["values list incomplete,\ncomboWidget.value reset"]
B4 --> B5["widget2.value = optionB"]
B5 --> B6["updateCombo fires again"]
B6 --> B7["Combo has wrong value"]
end
subgraph after["After: Hydration Transaction"]
direction TB
A1["configure starts"] --> A2["beginHydration nodeId"]
A2 --> A3["widget1.value = optionA"]
A3 --> A4["updateCombo skipped"]
A4 --> A5["widget2.value = optionB"]
A5 --> A6["updateCombo skipped"]
A6 --> A7["commitHydration nodeId"]
A7 --> A8["updateCombo runs once,\nall values present"]
end
```
### Changes
- **`LGraphNode.configure()`**: Wraps widget restoration + `onConfigure`
in `beginHydration`/`commitHydration` with `try/finally` for exception
safety
- **`PrimitiveNode.serialize()`**: Adds override to preserve
`widgets_values` when widgets are dynamically disconnected
- **`customWidgets.ts`**: Simplifies CustomCombo's `onConfigure` to use
`onHydrationComplete()` instead of manually managing hydration state
- **3 new tests**: Hydration transaction wrapping, `onHydrationComplete`
callback firing, and exception safety
### Part of
Stacked on #10010. Implements Design A from the widget state
architecture RFC.
2.9 KiB
6. PrimitiveNode Copy/Paste Lifecycle
Date: 2026-02-22
Status
Accepted (Option A)
Context
PrimitiveNode creates widgets dynamically on connection. When copied, the clone has no this.widgets, so LGraphNode.serialize() drops widgets_values from the clipboard data. This causes secondary widget values (e.g., control_after_generate) to be lost on paste. See WIDGET_SERIALIZATION.md for the full mechanism.
Options
A. Minimal fix: override serialize() on PrimitiveNode
Override serialize() to fall back to this.widgets_values (set during configure()) when the base implementation omits it due to missing this.widgets.
- Pro: No change to connection lifecycle semantics. Lowest risk.
- Pro: Doesn't affect workflow save/load (which already works via
onAfterGraphConfigured). - Con: Doesn't address the deeper design issue — primitives are still empty on copy.
B. Clone-configured-instance lifecycle
On copy, the primitive is a clone of the configured instance (with widgets intact). On disconnect or paste without connections, it returns to empty state.
- Pro: Copy→serialize captures
widgets_valuescorrectly. Matches OOP expectations. - Pro: Secondary widget state survives round-trips without special-casing.
- Con:
input.widget[CONFIG]allows extensions to make PrimitiveNode create a different widget than the target. Widget config is derived at connection time, not stored, so cloning the configured state may not be faithful. - Con: Deserialization ordering —
configure()runs before links are restored. PrimitiveNode needs links to know what widgets to create.onAfterGraphConfigured()handles this for workflow load, but copy/paste uses a different code path. - Con: Higher risk of regressions in extension compatibility.
C. Projection model (like Subgraph widgets)
Primitives act as a synchronization mechanism — no own state, just a projection of the target widget's resolved value.
- Pro: Cleanest conceptual model. Eliminates state duplication.
- Con: Primitives can connect to multiple targets. Projection with multiple targets is ambiguous.
- Con: Major architectural change with broad impact.
Decision
Option A. Override serialize() on PrimitiveNode to preserve widgets_values through copy-paste. This is the lowest-risk fix with no change to connection lifecycle semantics.
Prerequisite: PR #10010 replaced clone().serialize() with direct serialization in _serializeItems, eliminating the code path that dropped widgets_values for widget-less clones. Option A provides the PrimitiveNode-specific fallback for any remaining edge cases.
Option B can be revisited after Option A ships and stabilizes.