Files
ComfyUI_frontend/docs/architecture/ecs-lifecycle-scenarios.md
Alexander Brown 3e197b5c57 docs: ADR 0008 — Entity Component System (#10420)
## Summary

Architecture documentation proposing an Entity Component System for the
litegraph layer.

```mermaid
graph LR
    subgraph Today["Today: Spaghetti"]
        God["🍝 God Objects"]
        Circ["🔄 Circular Deps"]
        Mut["💥 Render Mutations"]
    end

    subgraph Tomorrow["Tomorrow: ECS"]
        ID["🏷️ Branded IDs"]
        Comp["📦 Components"]
        Sys["⚙️ Systems"]
        World["🌍 World"]
    end

    God -->|"decompose"| Comp
    Circ -->|"flatten"| ID
    Mut -->|"separate"| Sys
    Comp --> World
    ID --> World
    Sys -->|"query"| World
```

## Changes

- **What**: ADR 0008 + 4 architecture docs (no code changes)
- `docs/adr/0008-entity-component-system.md` — entity taxonomy, branded
IDs, component decomposition, migration strategy
- `docs/architecture/entity-interactions.md` — as-is Mermaid diagrams of
all entity relationships
- `docs/architecture/entity-problems.md` — structural problems with
file:line evidence
- `docs/architecture/ecs-target-architecture.md` — target architecture
diagrams
- `docs/architecture/proto-ecs-stores.md` — analysis of existing Pinia
stores as proto-ECS patterns

## Review Focus

- Does the entity taxonomy (Node, Link, Subgraph, Widget, Slot, Reroute,
Group) cover all cases?
- Are the component decompositions reasonable starting points?
- Is the migration strategy (bridge layer, incremental extraction)
feasible?
- Are there entity interactions or problems we missed?

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10420-docs-ADR-0008-Entity-Component-System-32d6d73d365081feb048d16a5231d350)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: GitHub Action <action@github.com>
Co-authored-by: Amp <amp@ampcode.com>
Co-authored-by: Christian Byrne <cbyrne@comfy.org>
2026-03-26 16:14:44 -07:00

745 lines
27 KiB
Markdown

# ECS Lifecycle Scenarios
This document walks through the major entity lifecycle operations — showing the current imperative implementation and how each transforms under the ECS architecture from [ADR 0008](../adr/0008-entity-component-system.md).
Each scenario follows the same structure: **Current Flow** (what happens today), **ECS Flow** (what it looks like with the World), and a **Key Differences** table.
## 1. Node Removal
### Current Flow
`LGraph.remove(node)` — 107 lines, touches 6+ entity types and 4+ external systems:
```mermaid
sequenceDiagram
participant Caller
participant G as LGraph
participant N as LGraphNode
participant L as LLink
participant R as Reroute
participant C as LGraphCanvas
participant LS as LayoutStore
Caller->>G: remove(node)
G->>G: beforeChange() [undo checkpoint]
loop each input slot
G->>N: disconnectInput(i)
N->>L: link.disconnect(network)
L->>G: _links.delete(linkId)
L->>R: cleanup orphaned reroutes
N->>LS: layoutMutations.removeLink()
N->>G: _version++
end
loop each output slot
G->>N: disconnectOutput(i)
Note over N,R: same cascade as above
end
G->>G: scan floatingLinks for node refs
G->>G: if SubgraphNode: check refs, maybe delete subgraph def
G->>N: node.onRemoved?.()
G->>N: node.graph = null
G->>G: _version++
loop each canvas
G->>C: deselect(node)
G->>C: delete selected_nodes[id]
end
G->>G: splice from _nodes[], delete from _nodes_by_id
G->>G: onNodeRemoved?.(node)
G->>C: setDirtyCanvas(true, true)
G->>G: afterChange() [undo checkpoint]
G->>G: updateExecutionOrder()
```
Problems: the graph method manually disconnects every slot, cleans up reroutes, scans floating links, checks subgraph references, notifies canvases, and recomputes execution order — all in one method that knows about every entity type.
### ECS Flow
```mermaid
sequenceDiagram
participant Caller
participant CS as ConnectivitySystem
participant W as World
participant VS as VersionSystem
Caller->>CS: removeNode(world, nodeId)
CS->>W: getComponent(nodeId, Connectivity)
W-->>CS: { inputSlotIds, outputSlotIds }
loop each slotId
CS->>W: getComponent(slotId, SlotConnection)
W-->>CS: { linkIds }
loop each linkId
CS->>CS: removeLink(world, linkId)
Note over CS,W: removes Link entity + updates remote slots
end
CS->>W: deleteEntity(slotId)
end
CS->>W: getComponent(nodeId, WidgetContainer)
W-->>CS: { widgetIds }
loop each widgetId
CS->>W: deleteEntity(widgetId)
end
CS->>W: deleteEntity(nodeId)
Note over W: removes Position, NodeVisual, NodeType,<br/>Connectivity, Execution, Properties,<br/>WidgetContainer — all at once
CS->>VS: markChanged()
```
### Key Differences
| Aspect | Current | ECS |
| ------------------- | ------------------------------------------------ | ------------------------------------------------------ |
| Lines of code | ~107 in one method | ~30 in system function |
| Entity types known | Graph knows about all 6+ types | ConnectivitySystem knows Connectivity + SlotConnection |
| Cleanup | Manual per-slot, per-link, per-reroute | `deleteEntity()` removes all components atomically |
| Canvas notification | `setDirtyCanvas()` called explicitly | RenderSystem sees missing entity on next frame |
| Store cleanup | WidgetValueStore/LayoutStore NOT cleaned up | World deletion IS the cleanup |
| Undo/redo | `beforeChange()`/`afterChange()` manually placed | System snapshots affected components before deletion |
| Testability | Needs full LGraph + LGraphCanvas | Needs only World + ConnectivitySystem |
## 2. Serialization
### Current Flow
`LGraph.serialize()``asSerialisable()` — walks every collection manually:
```mermaid
sequenceDiagram
participant Caller
participant G as LGraph
participant N as LGraphNode
participant L as LLink
participant R as Reroute
participant Gr as LGraphGroup
participant SG as Subgraph
Caller->>G: serialize()
G->>G: asSerialisable()
loop each node
G->>N: node.serialize()
N->>N: snapshot inputs, outputs (with link IDs)
N->>N: snapshot properties
N->>N: collect widgets_values[]
N-->>G: ISerialisedNode
end
loop each link
G->>L: link.asSerialisable()
L-->>G: SerialisableLLink
end
loop each reroute
G->>R: reroute.asSerialisable()
R-->>G: SerialisableReroute
end
loop each group
G->>Gr: group.serialize()
Gr-->>G: ISerialisedGroup
end
G->>G: findUsedSubgraphIds()
loop each used subgraph
G->>SG: subgraph.asSerialisable()
Note over SG: recursively serializes internal nodes, links, etc.
SG-->>G: ExportedSubgraph
end
G-->>Caller: ISerialisedGraph
```
Problems: serialization logic lives in 6 different `serialize()` methods across 6 classes. Widget values are collected inline during node serialization. The graph walks its own collections — no separation of "what to serialize" from "how to serialize."
### ECS Flow
```mermaid
sequenceDiagram
participant Caller
participant SS as SerializationSystem
participant W as World
Caller->>SS: serialize(world)
SS->>W: queryAll(NodeType, Position, Properties, WidgetContainer, Connectivity)
W-->>SS: all node entities with their components
SS->>W: queryAll(LinkEndpoints)
W-->>SS: all link entities
SS->>W: queryAll(SlotIdentity, SlotConnection)
W-->>SS: all slot entities
SS->>W: queryAll(RerouteLinks, Position)
W-->>SS: all reroute entities
SS->>W: queryAll(GroupMeta, GroupChildren, Position)
W-->>SS: all group entities
SS->>W: queryAll(SubgraphStructure, SubgraphMeta)
W-->>SS: all subgraph entities
SS->>SS: assemble JSON from component data
SS-->>Caller: SerializedGraph
```
### Key Differences
| Aspect | Current | ECS |
| ---------------------- | ----------------------------------------------- | ---------------------------------------------- |
| Serialization logic | Spread across 6 classes (`serialize()` on each) | Single SerializationSystem |
| Widget values | Collected inline during `node.serialize()` | WidgetValue component queried directly |
| Subgraph recursion | `asSerialisable()` recursively calls itself | Flat query — SubgraphStructure has entity refs |
| Adding a new component | Modify the entity's `serialize()` method | Add component to query in SerializationSystem |
| Testing | Need full object graph to test serialization | Mock World with test components |
## 3. Deserialization
### Current Flow
`LGraph.configure(data)` — ~180 lines, two-phase node creation:
```mermaid
sequenceDiagram
participant Caller
participant G as LGraph
participant N as LGraphNode
participant L as LLink
participant WVS as WidgetValueStore
Caller->>G: configure(data)
G->>G: clear() [destroy all existing entities]
G->>G: _configureBase(data) [set id, extra]
loop each serialized link
G->>L: LLink.create(linkData)
G->>G: _links.set(link.id, link)
end
loop each serialized reroute
G->>G: setReroute(rerouteData)
end
opt has subgraph definitions
G->>G: deduplicateSubgraphNodeIds()
loop each subgraph (topological order)
G->>G: createSubgraph(data)
end
end
rect rgb(60, 40, 40)
Note over G,N: Phase 1: Create nodes (unlinked)
loop each serialized node
G->>N: LiteGraph.createNode(type)
G->>G: graph.add(node) [assigns ID]
end
end
rect rgb(40, 60, 40)
Note over G,N: Phase 2: Configure nodes (links now resolvable)
loop each node
G->>N: node.configure(nodeData)
N->>N: create slots, restore properties
N->>N: resolve links from graph._links
N->>N: restore widget values
N->>WVS: widget.setNodeId() → register in store
N->>N: fire onConnectionsChange per linked slot
end
end
G->>G: add floating links
G->>G: validate reroutes
G->>G: _removeDuplicateLinks()
loop each serialized group
G->>G: create + configure group
end
G->>G: updateExecutionOrder()
```
Problems: two-phase creation is necessary because nodes need to reference each other's links during configure. Widget value restoration happens deep inside `node.configure()`. Store population is a side effect of configuration. Subgraph creation requires topological sorting to handle nested subgraphs.
### ECS Flow
```mermaid
sequenceDiagram
participant Caller
participant SS as SerializationSystem
participant W as World
participant LS as LayoutSystem
participant ES as ExecutionSystem
Caller->>SS: deserialize(world, data)
SS->>W: clear() [remove all entities]
Note over SS,W: All entities created in one pass — no two-phase needed
loop each node in data
SS->>W: createEntity(NodeEntityId)
SS->>W: setComponent(id, Position, {...})
SS->>W: setComponent(id, NodeType, {...})
SS->>W: setComponent(id, NodeVisual, {...})
SS->>W: setComponent(id, Properties, {...})
SS->>W: setComponent(id, Execution, {...})
end
loop each slot in data
SS->>W: createEntity(SlotEntityId)
SS->>W: setComponent(id, SlotIdentity, {...})
SS->>W: setComponent(id, SlotConnection, {...})
end
Note over SS,W: Slots reference links by ID — no resolution needed yet
loop each link in data
SS->>W: createEntity(LinkEntityId)
SS->>W: setComponent(id, LinkEndpoints, {...})
end
Note over SS,W: Connectivity assembled from slot/link components
loop each widget in data
SS->>W: createEntity(WidgetEntityId)
SS->>W: setComponent(id, WidgetIdentity, {...})
SS->>W: setComponent(id, WidgetValue, {...})
end
SS->>SS: create reroutes, groups, subgraphs similarly
Note over SS,W: Systems react to populated World
SS->>LS: runLayout(world)
SS->>ES: computeExecutionOrder(world)
```
### Key Differences
| Aspect | Current | ECS |
| ------------------ | -------------------------------------------------------------------------- | ------------------------------------------------------------ |
| Two-phase creation | Required (nodes must exist before link resolution) | Not needed — components reference IDs, not instances |
| Widget restoration | Hidden inside `node.configure()` line ~900 | Explicit: WidgetValue component written directly |
| Store population | Side effect of `widget.setNodeId()` | World IS the store — writing component IS population |
| Callback cascade | `onConnectionsChange`, `onInputAdded`, `onConfigure` fire during configure | No callbacks — systems query World after deserialization |
| Subgraph ordering | Topological sort required | Flat write — SubgraphStructure just holds entity IDs |
| Error handling | Failed node → placeholder with `has_errors=true` | Failed entity → skip; components that loaded are still valid |
## 4. Pack Subgraph
### Current Flow
`LGraph.convertToSubgraph(items)` — clones nodes, computes boundaries, creates SubgraphNode:
```mermaid
sequenceDiagram
participant Caller
participant G as LGraph
participant N as LGraphNode
participant SG as Subgraph
participant SGN as SubgraphNode
Caller->>G: convertToSubgraph(selectedItems)
G->>G: beforeChange()
G->>G: getBoundaryLinks(items)
Note over G: classify links as internal, boundary-in, boundary-out
G->>G: splitPositionables(items) → nodes, reroutes, groups
G->>N: multiClone(nodes) → cloned nodes (no links)
G->>G: serialize internal links, reroutes
G->>G: mapSubgraphInputsAndLinks(boundaryInputLinks)
G->>G: mapSubgraphOutputsAndLinks(boundaryOutputLinks)
G->>G: createSubgraph(exportedData)
G->>SG: subgraph.configure(data)
loop disconnect boundary links
G->>N: inputNode.disconnectInput()
G->>N: outputNode.disconnectOutput()
end
loop remove original items
G->>G: remove(node), remove(reroute), remove(group)
end
G->>SGN: LiteGraph.createNode(subgraph.id)
G->>G: graph.add(subgraphNode)
loop reconnect boundary inputs
G->>N: externalNode.connectSlots(output, subgraphNode, input)
end
loop reconnect boundary outputs
G->>SGN: subgraphNode.connectSlots(output, externalNode, input)
end
G->>G: afterChange()
```
Problems: 200+ lines in one method. Manual boundary link analysis. Clone-serialize-configure dance. Disconnect-remove-recreate-reconnect sequence with many intermediate states where the graph is inconsistent.
### ECS Flow
```mermaid
sequenceDiagram
participant Caller
participant CS as ConnectivitySystem
participant W as World
Caller->>CS: packSubgraph(world, selectedEntityIds)
CS->>W: query Connectivity + SlotConnection for selected nodes
CS->>CS: classify links as internal vs boundary
CS->>W: create new GraphId scope in scopes registry
Note over CS,W: Create SubgraphNode entity in parent scope
CS->>W: createEntity(NodeEntityId) [the SubgraphNode]
CS->>W: setComponent(nodeId, Position, { center of selection })
CS->>W: setComponent(nodeId, SubgraphStructure, { graphId, interface })
CS->>W: setComponent(nodeId, SubgraphMeta, { name: 'New Subgraph' })
Note over CS,W: Re-parent selected entities into new graph scope
loop each selected entity
CS->>W: update graphScope to new graphId
end
Note over CS,W: Create boundary slots on SubgraphNode
loop each boundary input link
CS->>W: create SlotEntity on SubgraphNode
CS->>W: update LinkEndpoints to target new slot
end
loop each boundary output link
CS->>W: create SlotEntity on SubgraphNode
CS->>W: update LinkEndpoints to source from new slot
end
```
### Key Differences
| Aspect | Current | ECS |
| -------------------------- | ------------------------------------------------- | ------------------------------------------------------- |
| Entity movement | Clone → serialize → configure → remove originals | Re-parent entities: update graphScope to new GraphId |
| Boundary links | Disconnect → remove → recreate → reconnect | Update LinkEndpoints to point at new SubgraphNode slots |
| Intermediate inconsistency | Graph is partially disconnected during operation | Atomic: all component writes happen together |
| Code size | 200+ lines | ~50 lines in system |
| Undo | `beforeChange()`/`afterChange()` wraps everything | Snapshot affected components before mutation |
## 5. Unpack Subgraph
### Current Flow
`LGraph.unpackSubgraph(subgraphNode)` — clones internal nodes, remaps IDs, reconnects boundary:
```mermaid
sequenceDiagram
participant Caller
participant G as LGraph
participant SGN as SubgraphNode
participant SG as Subgraph
participant N as LGraphNode
Caller->>G: unpackSubgraph(subgraphNode)
G->>G: beforeChange()
G->>SG: get internal nodes
G->>N: multiClone(internalNodes)
loop each cloned node
G->>G: assign new ID (++lastNodeId)
G->>G: nodeIdMap[oldId] = newId
G->>G: graph.add(node)
G->>N: node.configure(info)
G->>N: node.setPos(pos + offset)
end
G->>G: clone and add groups
Note over G,SG: Remap internal links
loop each internal link
G->>G: remap origin_id/target_id via nodeIdMap
opt origin is SUBGRAPH_INPUT_ID
G->>G: resolve to external source via subgraphNode.inputs
end
opt target is SUBGRAPH_OUTPUT_ID
G->>G: resolve to external target via subgraphNode.outputs
end
end
G->>G: remove(subgraphNode)
G->>G: deduplicate links
G->>G: create new LLink objects in parent graph
G->>G: remap reroute parentIds
G->>G: afterChange()
```
Problems: ID remapping is complex and error-prone. Magic IDs (SUBGRAPH_INPUT_ID = -10, SUBGRAPH_OUTPUT_ID = -20) require special-case handling. Boundary link resolution requires looking up the SubgraphNode's slots to find external connections.
### ECS Flow
```mermaid
sequenceDiagram
participant Caller
participant CS as ConnectivitySystem
participant W as World
Caller->>CS: unpackSubgraph(world, subgraphNodeId)
CS->>W: getComponent(subgraphNodeId, SubgraphStructure)
W-->>CS: { graphId, interface }
CS->>W: query entities where graphScope = graphId
W-->>CS: all child entities (nodes, links, reroutes, etc.)
Note over CS,W: Re-parent entities to containing graph scope
loop each child entity
CS->>W: update graphScope to parent scope
end
Note over CS,W: Reconnect boundary links
loop each boundary slot in interface
CS->>W: getComponent(slotId, SlotConnection)
CS->>W: update LinkEndpoints: SubgraphNode slot → internal node slot
end
CS->>W: deleteEntity(subgraphNodeId)
CS->>W: remove graphId from scopes registry
Note over CS,W: Offset positions
loop each moved entity
CS->>W: update Position component
end
```
### Key Differences
| Aspect | Current | ECS |
| ----------------- | --------------------------------------------------- | --------------------------------------------------------------- |
| ID remapping | `nodeIdMap[oldId] = newId` for every node and link | No remapping — entities keep their IDs, only graphScope changes |
| Magic IDs | SUBGRAPH_INPUT_ID = -10, SUBGRAPH_OUTPUT_ID = -20 | No magic IDs — boundary modeled as slot entities |
| Clone vs move | Clone nodes, assign new IDs, configure from scratch | Move entity references between scopes |
| Link reconnection | Remap origin_id/target_id, create new LLink objects | Update LinkEndpoints component in place |
| Complexity | ~200 lines with deduplication and reroute remapping | ~40 lines, no remapping needed |
## 6. Connect Slots
### Current Flow
`LGraphNode.connectSlots()` — creates link, updates both endpoints, handles reroutes:
```mermaid
sequenceDiagram
participant Caller
participant N1 as SourceNode
participant N2 as TargetNode
participant G as LGraph
participant L as LLink
participant R as Reroute
participant LS as LayoutStore
Caller->>N1: connectSlots(output, targetNode, input)
N1->>N1: validate slot types
N1->>N2: onConnectInput?() → can reject
N1->>N1: onConnectOutput?() → can reject
opt input already connected
N1->>N2: disconnectInput(inputIndex)
end
N1->>L: new LLink(++lastLinkId, type, ...)
N1->>G: _links.set(link.id, link)
N1->>LS: layoutMutations.createLink()
N1->>N1: output.links.push(link.id)
N1->>N2: input.link = link.id
loop each reroute in path
N1->>R: reroute.linkIds.add(link.id)
end
N1->>G: _version++
N1->>N1: onConnectionsChange?(OUTPUT, ...)
N1->>N2: onConnectionsChange?(INPUT, ...)
N1->>G: setDirtyCanvas()
N1->>G: afterChange()
```
Problems: the source node orchestrates everything — it reaches into the graph's link map, the target node's slot, the layout store, the reroute chain, and the version counter. 19 steps in one method.
### ECS Flow
```mermaid
sequenceDiagram
participant Caller
participant CS as ConnectivitySystem
participant W as World
participant VS as VersionSystem
Caller->>CS: connect(world, outputSlotId, inputSlotId)
CS->>W: getComponent(inputSlotId, SlotConnection)
opt already connected
CS->>CS: removeLink(world, existingLinkId)
end
CS->>W: createEntity(LinkEntityId)
CS->>W: setComponent(linkId, LinkEndpoints, {<br/> originNodeId, originSlotIndex,<br/> targetNodeId, targetSlotIndex, type<br/>})
CS->>W: update SlotConnection on outputSlotId (add linkId)
CS->>W: update SlotConnection on inputSlotId (set linkId)
CS->>VS: markChanged()
```
### Key Differences
| Aspect | Current | ECS |
| ---------------- | ------------------------------------------------------------ | ------------------------------------------------------------- |
| Orchestrator | Source node (reaches into graph, target, reroutes) | ConnectivitySystem (queries World) |
| Side effects | `_version++`, `setDirtyCanvas()`, `afterChange()`, callbacks | `markChanged()` — one call |
| Reroute handling | Manual: iterate chain, add linkId to each | RerouteLinks component updated by system |
| Slot mutation | Direct: `output.links.push()`, `input.link = id` | Component update: `setComponent(slotId, SlotConnection, ...)` |
| Validation | `onConnectInput`/`onConnectOutput` callbacks on nodes | Validation system or guard function |
## 7. Copy / Paste
### Current Flow
Copy: serialize selected items → clipboard. Paste: deserialize with new IDs.
```mermaid
sequenceDiagram
participant User
participant C as LGraphCanvas
participant G as LGraph
participant N as LGraphNode
participant CB as Clipboard
rect rgb(40, 40, 60)
Note over User,CB: Copy
User->>C: Ctrl+C
C->>C: _serializeItems(selectedItems)
loop each selected node
C->>N: node.clone().serialize()
C->>C: collect input links
end
C->>C: collect groups, reroutes
C->>C: recursively collect subgraph definitions
C->>CB: localStorage.setItem(JSON.stringify(data))
end
rect rgb(40, 60, 40)
Note over User,CB: Paste
User->>C: Ctrl+V
C->>CB: localStorage.getItem()
C->>C: _deserializeItems(parsed)
C->>C: remap subgraph IDs (new UUIDs)
C->>C: deduplicateSubgraphNodeIds()
C->>G: beforeChange()
loop each subgraph
C->>G: createSubgraph(data)
end
loop each node (id=-1 forces new ID)
C->>G: graph.add(node)
C->>N: node.configure(info)
end
loop each reroute
C->>G: setReroute(data)
C->>C: remap parentIds
end
loop each link
C->>N: outNode.connect(slot, inNode, slot)
end
C->>C: offset positions to cursor
C->>C: selectItems(created)
C->>G: afterChange()
end
```
Problems: clone-serialize-parse-remap-deserialize dance. Every entity type has
its own ID remapping logic. Subgraph IDs, node IDs, reroute IDs, and link
parent IDs all remapped independently. ~300 lines across multiple methods.
### ECS Flow
```mermaid
sequenceDiagram
participant User
participant CS as ClipboardSystem
participant W as World
participant CB as Clipboard
rect rgb(40, 40, 60)
Note over User,CB: Copy
User->>CS: copy(world, selectedEntityIds)
CS->>W: snapshot all components for selected entities
CS->>W: snapshot components for child entities (slots, widgets)
CS->>W: snapshot connected links (LinkEndpoints)
CS->>CB: store component snapshot
end
rect rgb(40, 60, 40)
Note over User,CB: Paste
User->>CS: paste(world, position)
CS->>CB: retrieve snapshot
CS->>CS: generate ID remap table (old → new branded IDs)
loop each entity in snapshot
CS->>W: createEntity(newId)
loop each component
CS->>W: setComponent(newId, type, remappedData)
Note over CS,W: entity ID refs in component data<br/>are remapped via table
end
end
CS->>CS: offset all Position components to cursor
end
```
### Key Differences
| Aspect | Current | ECS |
| -------------------- | ------------------------------------------------------------------ | ------------------------------------------------------------------ |
| Copy format | Clone → serialize → JSON (format depends on class) | Component snapshot (uniform format for all entities) |
| ID remapping | Separate logic per entity type (nodes, reroutes, subgraphs, links) | Single remap table applied to all entity ID refs in all components |
| Paste reconstruction | `createNode()``add()``configure()``connect()` per node | `createEntity()``setComponent()` per entity (flat) |
| Subgraph handling | Recursive clone + UUID remap + deduplication | Snapshot includes SubgraphStructure component with entity refs |
| Code complexity | ~300 lines across 4 methods | ~60 lines in one system |
## Summary: Cross-Cutting Benefits
| Benefit | Scenarios Where It Applies |
| ----------------------------- | -------------------------------------------------------------------------- |
| **Atomic operations** | Node Removal, Pack/Unpack — no intermediate inconsistent state |
| **No scattered `_version++`** | All scenarios — VersionSystem handles change tracking |
| **No callback cascades** | Deserialization, Connect — systems query World instead of firing callbacks |
| **Uniform ID handling** | Copy/Paste, Unpack — one remap table instead of per-type logic |
| **Entity deletion = cleanup** | Node Removal — `deleteEntity()` removes all components |
| **No two-phase creation** | Deserialization — components reference IDs, not instances |
| **Move instead of clone** | Pack/Unpack — entities keep their IDs, just change scope |
| **Testable in isolation** | All scenarios — mock World, test one system |
| **Undo/redo for free** | All scenarios — snapshot components before mutation, restore on undo |