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

27 KiB

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.

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:

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

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:

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

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:

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

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:

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

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:

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

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:

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

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.

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

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