Files
ComfyUI_frontend/docs/architecture/entity-interactions.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

12 KiB

Entity Interactions (Current System)

This document maps the relationships and interaction patterns between all entity types in the litegraph layer as it exists today. It serves as a baseline for the ECS migration planned in ADR 0008.

Entities

Entity Class ID Type Primary Location
Graph LGraph UUID src/lib/litegraph/src/LGraph.ts
Node LGraphNode NodeId src/lib/litegraph/src/LGraphNode.ts
Link LLink LinkId src/lib/litegraph/src/LLink.ts
Subgraph Subgraph UUID src/lib/litegraph/src/LGraph.ts (ECS: node component, not separate entity)
Widget BaseWidget name + nodeId src/lib/litegraph/src/widgets/BaseWidget.ts
Slot SlotBase index on parent src/lib/litegraph/src/node/SlotBase.ts
Reroute Reroute RerouteId src/lib/litegraph/src/Reroute.ts
Group LGraphGroup number src/lib/litegraph/src/LGraphGroup.ts

Under the ECS model, subgraphs are not a separate entity kind — they are nodes with SubgraphStructure and SubgraphMeta components. See Subgraph Boundaries.

1. Overview

High-level ownership and reference relationships between all entities.

graph TD
    subgraph Legend
        direction LR
        L1[A] -->|owns| L2[B]
        L3[C] -.->|references| L4[D]
        L5[E] ==>|extends| L6[F]
    end

    Graph["LGraph
(UUID)"]
    Node["LGraphNode
(NodeId)"]
    SubgraphEntity["Subgraph
(UUID)"]
    SubgraphNode["SubgraphNode"]
    Link["LLink
(LinkId)"]
    Widget["BaseWidget
(name)"]
    Slot["SlotBase
(index)"]
    Reroute["Reroute
(RerouteId)"]
    Group["LGraphGroup
(number)"]
    Canvas["LGraphCanvas"]

    %% Ownership (solid)
    Graph -->|"_nodes[]"| Node
    Graph -->|"_links Map"| Link
    Graph -->|"reroutes Map"| Reroute
    Graph -->|"_groups[]"| Group
    Graph -->|"_subgraphs Map"| SubgraphEntity
    Node -->|"inputs[], outputs[]"| Slot
    Node -->|"widgets[]"| Widget

    %% Extends (thick)
    SubgraphEntity ==>|extends| Graph
    SubgraphNode ==>|extends| Node

    %% References (dashed)
    Link -.->|"origin_id, target_id"| Node
    Link -.->|"parentId"| Reroute
    Slot -.->|"link / links[]"| Link
    Reroute -.->|"linkIds"| Link
    Reroute -.->|"parentId"| Reroute
    Group -.->|"_children Set"| Node
    Group -.->|"_children Set"| Reroute
    SubgraphNode -.->|"subgraph"| SubgraphEntity
    Node -.->|"graph"| Graph
    Canvas -.->|"graph"| Graph
    Canvas -.->|"selectedItems"| Node
    Canvas -.->|"selectedItems"| Group
    Canvas -.->|"selectedItems"| Reroute

2. Connectivity

How Nodes, Slots, Links, and Reroutes form the graph topology.

graph LR
    subgraph OutputNode["Origin Node"]
        OSlot["Output Slot
links: LinkId[]"]
    end

    subgraph InputNode["Target Node"]
        ISlot["Input Slot
link: LinkId | null"]
    end

    OSlot -->|"LinkId ref"| Link["LLink
origin_id + origin_slot
target_id + target_slot
type: ISlotType"]
    Link -->|"LinkId ref"| ISlot

    Link -.->|"parentId"| R1["Reroute A"]
    R1 -.->|"parentId"| R2["Reroute B"]

    R1 -.-|"linkIds Set"| Link
    R2 -.-|"linkIds Set"| Link

Subgraph Boundary Connections

graph TD
    subgraph ParentGraph["Parent Graph"]
        ExtNode["External Node"]
        SGNode["SubgraphNode
(in parent graph)"]
    end

    subgraph SubgraphDef["Subgraph"]
        SInput["SubgraphInput"]
        SInputNode["SubgraphInputNode
(virtual)"]
        InternalNode["Internal Node"]
        SOutputNode["SubgraphOutputNode
(virtual)"]
        SOutput["SubgraphOutput"]
    end

    ExtNode -->|"Link (parent graph)"| SGNode
    SGNode -.->|"maps to"| SInput
    SInput -->|"owns"| SInputNode
    SInputNode -->|"Link (subgraph)"| InternalNode
    InternalNode -->|"Link (subgraph)"| SOutputNode
    SOutputNode -->|"owned by"| SOutput
    SOutput -.->|"maps to"| SGNode
    SGNode -->|"Link (parent graph)"| ExtNode
graph LR
    Slot["Source Slot"] -->|"drag starts"| FL["Floating LLink
origin_id=-1 or target_id=-1"]
    FL -->|"stored in"| FLMap["graph.floatingLinks Map"]
    FL -.->|"may pass through"| Reroute
    Reroute -.-|"floatingLinkIds Set"| FL
    FL -->|"on drop"| Permanent["Permanent LLink
(registered in graph._links)"]

3. Rendering

How LGraphCanvas draws each entity type.

graph TD
    Canvas["LGraphCanvas
render loop"]

    Canvas -->|"1. background"| DrawGroups["drawGroups()"]
    Canvas -->|"2. connections"| DrawConns["drawConnections()"]
    Canvas -->|"3. foreground"| DrawNodes["drawNode() per node"]
    Canvas -->|"4. in-progress"| DrawLC["LinkConnector.renderLinks"]

    DrawGroups --> Group["group.draw(canvas, ctx)"]

    DrawConns --> LinkSeg["LinkSegment interface"]
    LinkSeg --> Link["LLink path rendering"]
    LinkSeg --> RerouteRender["Reroute inline rendering
(draw, drawSlots)"]

    DrawNodes --> NodeDraw["node drawing pipeline"]
    NodeDraw -->|"drawSlots()"| SlotDraw["slot.draw() per slot"]
    NodeDraw -->|"drawWidgets()"| WidgetDraw["widget.drawWidget() per widget"]
    NodeDraw -->|"title, badges"| NodeChrome["title bar, buttons, badges"]

    DrawLC --> FloatingViz["Floating link visualization"]

Rendering Order Detail

sequenceDiagram
    participant C as Canvas
    participant Gr as Groups
    participant L as Links/Reroutes
    participant N as Nodes
    participant S as Slots
    participant W as Widgets

    C->>Gr: drawGroups() — background layer
    Gr-->>C: group shapes + titles

    C->>L: drawConnections() — middle layer
    L-->>C: bezier paths + reroute dots

    loop each node (back to front)
        C->>N: drawNode()
        N->>N: drawNodeShape() (background, title)
        N->>S: drawSlots() (input/output circles)
        S-->>N: slot shapes + labels
        N->>W: drawWidgets() (if not collapsed)
        W-->>N: widget UI elements
        N-->>C: complete node
    end

    C->>C: overlay (tooltips, debug)

4. Lifecycle

Creation and destruction flows for each entity.

Node Lifecycle

stateDiagram-v2
    [*] --> Created: new LGraphNode(title)
    Created --> Configured: node.configure(data)
    Configured --> InGraph: graph.add(node)

    state InGraph {
        [*] --> Active
        Active --> Active: connect/disconnect slots
        Active --> Active: add/remove widgets
        Active --> Active: move, resize, collapse
    }

    InGraph --> Removed: graph.remove(node)
    Removed --> [*]

    note right of Created
        Constructor sets defaults.
        No graph reference yet.
    end note

    note right of InGraph
        node.onAdded(graph) called.
        ID assigned from graph.state.
        Slots may trigger onConnectionsChange.
    end note

    note right of Removed
        All links disconnected.
        node.onRemoved() called.
        Removed from graph._nodes.
    end note
stateDiagram-v2
    [*] --> Created: node.connect() or connectSlots()
    Created --> Registered: graph._links.set(id, link)

    state Registered {
        [*] --> Active
        Active --> Active: data flows through
        Active --> Active: reroutes added/removed
    }

    Registered --> Disconnected: node.disconnectInput/Output()
    Disconnected --> Removed: link.disconnect(network)
    Removed --> [*]

    note right of Created
        new LLink(id, type, origin, slot, target, slot)
        Output slot.links[] updated.
        Input slot.link set.
    end note

    note right of Removed
        Removed from graph._links.
        Orphaned reroutes cleaned up.
        graph._version incremented.
    end note

Widget Lifecycle

stateDiagram-v2
    [*] --> Created: node.addWidget(type, name, value, options)
    Created --> Concrete: toConcreteWidget()
    Concrete --> Bound: widget.setNodeId(nodeId)

    state Bound {
        [*] --> Active
        Active --> Active: setValue() → store + node callback
        Active --> Active: draw(), onClick(), onDrag()
    }

    Bound --> Removed: node.removeWidget(widget)
    Removed --> [*]

    note right of Bound
        Registered in WidgetValueStore.
        State keyed by graphId:nodeId:name.
        Value reads/writes via store.
    end note

Subgraph Lifecycle

stateDiagram-v2
    [*] --> Created: graph.createSubgraph(data)

    state Created {
        [*] --> Defined
        Defined: registered in rootGraph._subgraphs
    }

    Created --> Instantiated: new SubgraphNode(subgraph)
    Instantiated --> InGraph: graph.add(subgraphNode)

    state InGraph {
        [*] --> Active
        Active --> Active: add/remove inputs/outputs
        Active --> Active: promote/demote widgets
        Active --> Active: edit internal nodes
    }

    InGraph --> Unpacked: graph.unpackSubgraph(node)
    Unpacked --> [*]

    InGraph --> NodeRemoved: graph.remove(subgraphNode)
    NodeRemoved --> MaybePurged: no other SubgraphNodes reference it?
    MaybePurged --> [*]

    note right of Instantiated
        SubgraphNode.subgraph = subgraph.
        Inputs/outputs synced from subgraph.
    end note

    note right of Unpacked
        Internal nodes cloned to parent.
        Links remapped. SubgraphNode removed.
        Subgraph def removed if unreferenced.
    end note

5. State Management

External stores and their relationships to entities.

graph TD
    subgraph Entities
        Node["LGraphNode"]
        Widget["BaseWidget"]
        Reroute["Reroute"]
        Link["LLink"]
        Graph["LGraph"]
        SGNode["SubgraphNode"]
    end

    subgraph Stores
        WVS["WidgetValueStore
(Pinia)"]
        PS["PromotionStore
(Pinia)"]
        LM["LayoutMutations
(composable)"]
    end

    subgraph GraphState["Graph Internal State"]
        Version["graph._version"]
        LGState["graph.state
(lastNodeId, lastLinkId,
lastRerouteId, lastGroupId)"]
    end

    %% WidgetValueStore
    Widget -->|"setNodeId() registers"| WVS
    Widget <-->|"value, label, disabled"| WVS
    WVS -.->|"keyed by graphId:nodeId:name"| Widget

    %% PromotionStore
    SGNode -->|"tracks promoted widgets"| PS
    Widget -.->|"isPromotedByAny() query"| PS

    %% LayoutMutations
    Node -->|"pos/size setter"| LM
    Reroute -->|"move()"| LM
    Link -->|"connectSlots()/disconnect()"| LM
    Graph -->|"add()/remove()"| LM

    %% Graph state
    Node -->|"connect/disconnect"| Version
    Widget -->|"setValue()"| Version
    Node -->|"collapse/toggleAdvanced"| Version
    Graph -->|"add/remove entities"| LGState

Change Notification Flow

sequenceDiagram
    participant E as Entity (Node/Widget/Link)
    participant G as LGraph
    participant C as LGraphCanvas
    participant R as Render Loop

    E->>G: graph._version++
    E->>G: graph.beforeChange() (undo checkpoint)

    Note over E,G: ... mutation happens ...

    E->>G: graph.afterChange() (undo checkpoint)
    E->>G: graph.change()
    G->>C: canvasAction → canvas.setDirty(true, true)
    C->>R: dirty flags checked on next frame
    R->>C: full redraw

Widget State Delegation

sequenceDiagram
    participant N as Node
    participant W as Widget
    participant S as WidgetValueStore
    participant G as Graph

    N->>W: addWidget(type, name, value)
    W->>W: toConcreteWidget()
    N->>W: setNodeId(nodeId)
    W->>S: registerWidget(graphId, state)
    S-->>W: state reference stored in widget._state

    Note over W,S: All value access now goes through store

    W->>S: widget.value = newVal (setter)
    S-->>S: store.state.value = newVal
    W->>N: node.onWidgetChanged?.(name, val)
    W->>G: graph._version++