Commit b1468ac
fix: clear widget-order and preview-exposure entries on node teardown (#14276)
**STACKED — merging lands on `matt/be-5050-node-shell-state` (owned by
@mattmillerai), NOT `main`.** That branch is itself based on
`feature/ecs-migration`, where `src/core/graph/nodeShell/` lives. Do not
merge before its parent lands; GitHub retargets this PR as the stack
unwinds.
## ELI-5
When a node joins a graph we write it into three places: its shell
state, its widget bindings, and its widget ordering (plus preview
exposures accumulate against the same bucket). When a node left, we only
erased the first one. The leftovers piled up in a bucket shared by the
whole workflow and stayed there until the entire graph was thrown away.
This adds the missing "undo" for the other two and points every teardown
path at it.
## Summary
`attachNodeToStores` registers three things; every teardown path only
undid one, so widget-order and preview-exposure entries orphaned in the
shared root-graph bucket. This adds `detachNodeFromStores` as its exact
inverse and routes the teardown paths through it.
## Changes
- **What**: New `detachNodeFromStores(graph, node, mode)` in
`nodeShellLifecycle.ts` — unregisters the shell state, removes each
`getWidgetIds(node.widgets)` entry from the widget order, and clears the
node's preview exposures (`String(node.id)` host locator, the same key
`promotionUtils`/`SubgraphNode` write under). `unregisterAllNodeStates`
moves out of `nodeShellState.ts` and becomes `detachAllNodesFromStores`
in the same module — it needs the root graph to reach those buckets, so
it now takes the graph (which carries `rootGraph`) rather than just
`_nodes`/`_subgraphs`. Call sites: `LGraph.remove` (both the per-node
teardown and the released-subgraph-definition walk) and
`releaseGraphStores`'s non-root branch.
- **Breaking**: none — `unregisterAllNodeStates` had exactly one
external caller (`LGraph.ts`), and no behavior the extension surface can
observe changes.
## Reachability the leak had (all three are load-bearing, not
theoretical)
- Subgraphs always take the non-root `else` branch of
`releaseGraphStores`, and `LGraph.configure` calls `clear()` first — so
re-configuring a subgraph orphaned its nodes' entries.
- `clear()` sets `id = zeroUuid` on exit, so a root graph whose
serialized data carries no id keeps taking the `else` branch for the
rest of the session.
- Plain `LGraph.remove(node)` leaked per-node on every branch, since it
never removed the widget order it added.
## Review Focus
**The judgment call the ticket left open — `deleteWidget` vs
`removeNodeWidgetOrder` — is decided per call site, and both are covered
by tests.** `LGraph.remove(node)` drops only the ordering and **keeps**
the stored widget values: a single node removal feeds undo/redo and a
node moved between graphs (`remove` then `add`) must not lose its
values. Graph and subgraph-definition teardown
(`detachAllNodesFromStores`) **discards** them, which is what the root
branch already does one line up — `clearGraph` on `widgetValueStore`
wipes that graph's widget states wholesale, so the non-root path was the
inconsistent one.
**Riskiest line: `deleteWidget` in the discard path**, because it is the
one edit that removes data rather than an index. It is safe because
`deleteWidget` only drops the store's map entry — the widget instance's
`_state` object is untouched, so a widget that survives the clear still
reads its own value, and re-registration (`setNodeId` →
`registerWidget`) writes it back from the widget/serialized data rather
than from store leftovers. Without this, a re-configure would silently
reuse the *stale* store value: `registerWidget` returns the existing
state when the type matches and ignores the incoming `init.value`.
**Deliberately not routed through the new helper:**
`useNodeReplacement.replaceWithMapping`. It calls
`unregisterNodeState`/`registerNodeState` for an in-place swap where the
new node inherits the same id and re-registers the same widgets —
clearing exposures there would drop a replaced host node's promoted
previews. It is a replacement, not a teardown.
**Deviation from the ticket's sketch:** preview exposures are cleared
with `setExposures(rootGraphId, locator, [])` (guarded by a
`getExposures(...).length` read) rather than a `removeExposure` per
name. Same result, one write instead of N, and the guard keeps the
reactive `exposures` ref from being touched on every ordinary node
deletion.
## Verification
New `src/core/graph/nodeShell/nodeShellLifecycle.test.ts` covers all
three: widget order dropped and values kept on `subgraph.remove(node)`;
exposures dropped when a host `SubgraphNode` is removed; order, values
and exposures all released when a zero-uuid root graph is cleared. **All
three fail on the base branch** (verified by stashing the source change
and re-running — `expected [ Array(1) ] to deeply equal []`).
`vitest run src/lib/litegraph src/core/graph
src/platform/nodeReplacement src/stores/widgetValueStore.test.ts
src/composables/node` — 1591 passed. `pnpm typecheck`, `pnpm knip`,
eslint and oxfmt on the changed files: clean.
Amp-Thread-ID: https://ampcode.com/threads/T-01a027af-1c32-7532-84f9-b94aa90620fa1 parent 50e6c4c commit b1468ac
5 files changed
Lines changed: 195 additions & 26 deletions
File tree
- src
- core/graph/nodeShell
- lib/litegraph/src
- stores
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | 2 | | |
| 3 | + | |
3 | 4 | | |
4 | 5 | | |
5 | | - | |
| 6 | + | |
6 | 7 | | |
7 | 8 | | |
8 | 9 | | |
9 | 10 | | |
10 | 11 | | |
| 12 | + | |
11 | 13 | | |
12 | 14 | | |
13 | 15 | | |
| |||
31 | 33 | | |
32 | 34 | | |
33 | 35 | | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
110 | 110 | | |
111 | 111 | | |
112 | 112 | | |
113 | | - | |
114 | | - | |
115 | | - | |
116 | | - | |
117 | | - | |
118 | | - | |
119 | | - | |
120 | | - | |
121 | | - | |
122 | | - | |
123 | | - | |
124 | | - | |
125 | | - | |
126 | | - | |
127 | | - | |
128 | | - | |
129 | 113 | | |
130 | 114 | | |
131 | 115 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
5 | 5 | | |
6 | 6 | | |
7 | 7 | | |
8 | | - | |
9 | 8 | | |
10 | | - | |
11 | | - | |
12 | | - | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
13 | 13 | | |
14 | 14 | | |
15 | 15 | | |
| |||
24 | 24 | | |
25 | 25 | | |
26 | 26 | | |
| 27 | + | |
27 | 28 | | |
| 29 | + | |
28 | 30 | | |
29 | 31 | | |
30 | 32 | | |
| |||
48 | 50 | | |
49 | 51 | | |
50 | 52 | | |
51 | | - | |
52 | | - | |
53 | 53 | | |
54 | 54 | | |
55 | 55 | | |
| |||
251 | 251 | | |
252 | 252 | | |
253 | 253 | | |
254 | | - | |
| 254 | + | |
255 | 255 | | |
256 | 256 | | |
257 | 257 | | |
| |||
1284 | 1284 | | |
1285 | 1285 | | |
1286 | 1286 | | |
1287 | | - | |
| 1287 | + | |
1288 | 1288 | | |
1289 | 1289 | | |
1290 | 1290 | | |
| |||
1293 | 1293 | | |
1294 | 1294 | | |
1295 | 1295 | | |
1296 | | - | |
| 1296 | + | |
1297 | 1297 | | |
1298 | 1298 | | |
1299 | 1299 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
240 | 240 | | |
241 | 241 | | |
242 | 242 | | |
| 243 | + | |
| 244 | + | |
| 245 | + | |
| 246 | + | |
| 247 | + | |
| 248 | + | |
| 249 | + | |
| 250 | + | |
| 251 | + | |
| 252 | + | |
| 253 | + | |
| 254 | + | |
| 255 | + | |
| 256 | + | |
| 257 | + | |
| 258 | + | |
| 259 | + | |
| 260 | + | |
| 261 | + | |
| 262 | + | |
| 263 | + | |
| 264 | + | |
| 265 | + | |
| 266 | + | |
| 267 | + | |
| 268 | + | |
243 | 269 | | |
244 | 270 | | |
245 | 271 | | |
| |||
258 | 284 | | |
259 | 285 | | |
260 | 286 | | |
| 287 | + | |
261 | 288 | | |
262 | 289 | | |
263 | 290 | | |
0 commit comments