Adds the missing link topology store design record, amends ADR 0008 (linkStore/rerouteStore amendment, Link and Slot component notes, new supporting-document rows), and corrects the migration plan, target architecture, proto-ECS store inventory, lifecycle scenarios, entity interactions, and entity problems docs to match the shipped stores: target-input-slot keying, root-graph-scoped buckets, derived reroute membership, deleted layout-store connectivity mirror, and deleted slot._floatingLinks sets.
14 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, whose target is realized as a set of dedicated Pinia stores (see Proto-ECS Stores).
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 (derived, rerouteStore)"| 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 (derived)"| Link
R2 -.-|"linkIds (derived)"| Link
Reroute.linkIds / floatingLinkIds are read-only accessors derived from the
links' own parentId chains by rerouteStore — membership is never stored
(see reroute-chain-store.md).
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
Floating Links (In-Progress Connections)
graph LR
Slot["Source Slot"] -->|"drag starts"| FL["Floating LLink
origin or target = UNASSIGNED_NODE_ID"]
FL -->|"stored in"| FLMap["graph.floatingLinks Map"]
FL -->|"registered in"| SideSet["linkStore unkeyed side set
(no unique target slot)"]
FL -.->|"may pass through"| Reroute
Reroute -.-|"floatingLinkIds (derived)"| FL
FL -->|"on drop"| Permanent["Permanent LLink
(graph._links + linkStore target index)"]
A floating link's slot attachment is fully encoded in its own endpoints —
slots hold no floating-link sets (slotFloatingLinks() in LLink.ts derives
attachment by scanning graph.floatingLinks).
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
Link Lifecycle
stateDiagram-v2
[*] --> Created: node.connect() or connectSlots()
Created --> Registered: graph._addLink(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)
_addLink sets graph._links entry and
registers topology in linkStore
(keyed by target input slot).
Output slot.links[] updated.
Input slot.link set.
end note
note right of Removed
Removed from graph._links and
unregistered from linkStore
(via _removeLink or link.disconnect).
Link geometry dropped from layoutStore.
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)"]
PES["PreviewExposureStore
(Pinia)"]
LKS["LinkStore
(Pinia)"]
RRS["RerouteStore
(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
%% PreviewExposureStore
SGNode -->|"host-scoped preview exposures"| PES
PES -.->|"keyed by host node locator"| SGNode
%% LinkStore
Graph -->|"_addLink()/_removeLink() register/unregister"| LKS
Link <-->|"_state IS the store entry (endpoint accessors)"| LKS
LKS -.->|"keyed by rootGraphId + targetNodeId:targetSlot"| Link
%% RerouteStore
Graph -->|"_addReroute()/_removeReroute()"| RRS
Reroute <-->|"_chain (parentId, floating)"| RRS
RRS -.->|"derived linkIds membership"| Reroute
%% LayoutMutations (geometry only)
Node -->|"pos/size setter"| LM
Reroute -->|"move()"| LM
Link -->|"disconnect(): drop link geometry"| 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++