mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-07-11 01:28:03 +00:00
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.
225 lines
12 KiB
Markdown
225 lines
12 KiB
Markdown
# Entity System Structural Problems
|
|
|
|
This document catalogs the structural problems in the current litegraph entity system. It provides the concrete "why" behind the ECS migration proposed in [ADR 0008](../adr/0008-entity-component-system.md). For the as-is relationship map, see [Entity Interactions](entity-interactions.md).
|
|
|
|
All file references are relative to `src/lib/litegraph/src/`.
|
|
|
|
## 1. God Objects
|
|
|
|
The three largest classes carry far too many responsibilities:
|
|
|
|
| Class | Lines | Responsibilities |
|
|
| -------------- | ------ | ---------------------------------------------------------------------------------------------------------------------------- |
|
|
| `LGraphCanvas` | ~9,100 | Rendering, input handling, selection, link dragging, context menus, clipboard, undo/redo hooks, node layout triggers |
|
|
| `LGraphNode` | ~4,300 | Domain model, connectivity, serialization, rendering (slots, widgets, badges, title), layout, execution, property management |
|
|
| `LGraph` | ~3,100 | Container management, serialization, canvas notification, subgraph lifecycle, execution ordering, link deduplication |
|
|
|
|
`LGraphNode` alone has ~539 method/property definitions. A sampling of the concerns it mixes:
|
|
|
|
| Concern | Examples |
|
|
| ------------- | ---------------------------------------------------------------------------------------------------------------------- |
|
|
| Rendering | `renderingColor` (line 328), `renderingBgColor` (line 335), `drawSlots()`, `drawWidgets()`, `measure(ctx)` (line 2074) |
|
|
| Serialization | `serialize()` (line 943), `configure()` (line 831), `toJSON()` (line 1033) |
|
|
| Connectivity | `connect()`, `connectSlots()`, `disconnectInput()`, `disconnectOutput()` |
|
|
| Execution | `execute()` (line 1418), `triggerSlot()` |
|
|
| Layout | `arrange()`, `_arrangeWidgets()`, `computeSize()` |
|
|
| State mgmt | `setProperty()`, `onWidgetChanged()`, direct `graph._version++` |
|
|
|
|
## 2. Circular Dependencies
|
|
|
|
**LGraph ↔ Subgraph**: `Subgraph` extends `LGraph`, but `LGraph` creates and manages `Subgraph` instances. This forces:
|
|
|
|
- A barrel export in `litegraph.ts` that re-exports 40+ modules with **order-dependent imports**
|
|
- An explicit comment at `litegraph.ts:15`: _"Must remain above LiteGraphGlobal (circular dependency due to abstract factory behaviour in 'configure')"_
|
|
- Test files must use the barrel import (`import { LGraph, Subgraph } from '.../litegraph'`) rather than direct imports, or they break
|
|
|
|
The `Subgraph` class is defined inside `LGraph.ts` (line 2761) rather than in its own file — a symptom of the circular dependency being unresolvable with the current class hierarchy.
|
|
|
|
## 3. Mixed Concerns
|
|
|
|
### Rendering in Domain Objects
|
|
|
|
`LGraphNode.measure()` (line 2074) accepts a `CanvasRenderingContext2D` parameter and sets `ctx.font` — a rendering operation embedded in what should be a domain model:
|
|
|
|
```
|
|
measure(ctx?: CanvasRenderingContext2D, options?: MeasureOptions): void {
|
|
...
|
|
if (ctx) ctx.font = this.innerFontStyle
|
|
```
|
|
|
|
### State Mutation During Render
|
|
|
|
`LGraphCanvas.drawNode()` (line 5554) mutates node state as a side effect of rendering:
|
|
|
|
- Line 5562: `node._setConcreteSlots()` — rebuilds slot arrays
|
|
- Line 5564: `node.arrange()` — recalculates widget layout
|
|
- Lines 5653-5655: same mutations repeated for a second code path
|
|
|
|
This means the render pass is not idempotent — drawing a node changes its state.
|
|
|
|
### Store Dependencies in Domain Objects
|
|
|
|
`BaseWidget` imports a Pinia store at the module level:
|
|
|
|
- `useWidgetValueStore` — widget state delegation via `setNodeId()`
|
|
|
|
Similarly, `LGraph` imports `useLayoutMutations` and `useWidgetValueStore`. Domain objects should not have direct dependencies on UI framework stores.
|
|
|
|
### Serialization Interleaved with Container Logic
|
|
|
|
`LGraph.configure()` (line 2400) mixes deserialization, event dispatch, store clearing, and container state setup in a single 180-line method. A change to serialization format risks breaking container lifecycle, and vice versa.
|
|
|
|
## 4. Inconsistent ID Systems
|
|
|
|
### Ambiguous NodeId
|
|
|
|
```ts
|
|
export type NodeId = number | string // LGraphNode.ts:100
|
|
```
|
|
|
|
Most nodes use numeric IDs, but subgraph-related nodes use strings. Code must use runtime type guards (`typeof node.id === 'number'` at LGraph.ts:978, LGraphCanvas.ts:9045). This is a source of subtle bugs.
|
|
|
|
### Magic Numbers
|
|
|
|
```ts
|
|
export const SUBGRAPH_INPUT_ID = -10 // constants.ts:8
|
|
export const SUBGRAPH_OUTPUT_ID = -20 // constants.ts:11
|
|
```
|
|
|
|
Negative sentinel values in the ID space. Links check `origin_id === SUBGRAPH_INPUT_ID` to determine if they cross a subgraph boundary — a special case baked into the general-purpose `LLink` class.
|
|
|
|
### No Independent Widget or Slot IDs
|
|
|
|
**Widgets** are identified by `name + parent node`. Code searches by name in multiple places:
|
|
|
|
- `LGraphNode.ts:904` — `this.inputs.find((i) => i.widget?.name === w.name)`
|
|
- `LGraphNode.ts:4077` — `slot.widget.name === widget.name`
|
|
- `LGraphNode.ts:4086` — `this.widgets?.find((w) => w.name === slot.widget.name)`
|
|
|
|
If a widget is renamed, all these lookups silently break.
|
|
|
|
**Slots** are identified by their array index on the parent node. The serialized link format (`SerialisedLLinkArray`) stores slot indices:
|
|
|
|
```ts
|
|
type SerialisedLLinkArray = [
|
|
id,
|
|
origin_id,
|
|
origin_slot,
|
|
target_id,
|
|
target_slot,
|
|
type
|
|
]
|
|
```
|
|
|
|
If slots are reordered (e.g., by an extension adding a slot), all links referencing that node become stale.
|
|
|
|
### No Cross-Kind ID Safety
|
|
|
|
Nothing prevents passing a `LinkId` where a `NodeId` is expected — they're both `number`. The dedicated-store direction addresses this with branded string keys where cross-kind safety pays off (for example `WidgetId` in `widgetValueStore`, `src/types/widgetId.ts`).
|
|
|
|
## 5. Law of Demeter Violations
|
|
|
|
Entities routinely reach through their container to access internal state and sibling entities.
|
|
|
|
### Nodes Reaching Into Graph Internals
|
|
|
|
8+ locations in `LGraphNode` access the graph's private `_links` map directly:
|
|
|
|
- Line 877: `this.graph._links.get(input.link)`
|
|
- Line 891: `this.graph._links.get(linkId)`
|
|
- Line 1254: `const link_info = this.graph._links.get(input.link)`
|
|
|
|
Nodes also reach through the graph to access sibling nodes' slots:
|
|
|
|
- Line 1150: `this.graph.getNodeById(link.origin_id)` → read origin's outputs
|
|
- Line 1342: `this.graph.getNodeById(link.target_id)` → read target's inputs
|
|
- Line 1556: `node.inputs[link_info.target_slot]` (accessing a sibling's slot by index)
|
|
|
|
### Canvas Mutating Graph Internals
|
|
|
|
`LGraphCanvas` directly increments the graph's version counter:
|
|
|
|
- Line 3084: `node.graph._version++`
|
|
- Line 7880: `node.graph._version++`
|
|
|
|
The canvas also reaches through nodes to their container:
|
|
|
|
- Line 8337: `node.graph.remove(node)` — canvas deletes a node by reaching through the node to its graph
|
|
|
|
### Entities Mutating Container State
|
|
|
|
`LGraphNode` directly mutates `graph._version++` from 8+ locations (lines 833, 2989, 3138, 3176, 3304, 3539, 3550, 3567). There is no encapsulated method for signaling a version change — every call site manually increments the counter.
|
|
|
|
## 6. Scattered Side Effects
|
|
|
|
### Version Counter
|
|
|
|
`graph._version` is incremented from **15+ locations** across three files:
|
|
|
|
| File | Locations |
|
|
| ----------------- | --------------------------------------------------- |
|
|
| `LGraph.ts` | Lines 956, 989, 1042, 1109, 2643 |
|
|
| `LGraphNode.ts` | Lines 833, 2989, 3138, 3176, 3304, 3539, 3550, 3567 |
|
|
| `LGraphCanvas.ts` | Lines 3084, 7880 |
|
|
|
|
No central mechanism exists. It's easy to forget an increment (stale render) or add a redundant one (wasted work).
|
|
|
|
### Module-Scope Store Access
|
|
|
|
Domain objects call Pinia composables at the module level or in methods, creating implicit dependencies on the Vue runtime:
|
|
|
|
- `Reroute.ts:31` — `const layoutMutations = useLayoutMutations()` (module scope)
|
|
- `LLink.ts:7` — imports the `layoutStore` singleton at module scope; `useLinkStore()` is called inside methods and helpers (needs an active Pinia, but not eagerly at import time)
|
|
- `Reroute.ts` — `useRerouteStore()` called inside methods for derived membership
|
|
- `BaseWidget.ts` — imports `useWidgetValueStore`
|
|
|
|
These make the domain objects untestable without a Vue app context.
|
|
|
|
### Reroute Membership Dual-Writes (solved)
|
|
|
|
`Reroute.linkIds` / `floatingLinkIds` were hand-maintained `Set`s written from
|
|
~10 scattered call sites (connect, disconnect, paste, configure, subgraph
|
|
pack/unpack, ...), with `Reroute.validateLinks()` repairing the inevitable
|
|
drift on load. Membership is now derived from the links' own `parentId`
|
|
chains via `rerouteStore` — the accessors are read-only, the write sites and
|
|
`validateLinks` are deleted, and orphaned reroutes are pruned by the derived
|
|
`totalLinks === 0` instead of set-repair. See
|
|
[reroute-chain-store.md](reroute-chain-store.md) (Decision 1).
|
|
|
|
### Change Notification Sprawl
|
|
|
|
`beforeChange()` and `afterChange()` (undo/redo checkpoints) are called from
|
|
**12+ locations** in `LGraphCanvas` alone (lines 1574, 1592, 1604, 1620, 1752,
|
|
1770, 8754, 8760, 8771, 8777, 8803, 8811). These calls are grouping brackets:
|
|
misplaced or missing pairs can split one logical operation across multiple undo
|
|
entries, while unmatched extra calls can delay checkpoint emission until the
|
|
nesting counter returns to zero.
|
|
|
|
## 7. Render-Time Mutations
|
|
|
|
The render pass is not pure — it mutates state as a side effect:
|
|
|
|
| Location | Mutation |
|
|
| ----------------------------------- | ------------------------------------------------------------------- |
|
|
| `LGraphCanvas.drawNode()` line 5562 | `node._setConcreteSlots()` — rebuilds concrete slot arrays |
|
|
| `LGraphCanvas.drawNode()` line 5564 | `node.arrange()` — recalculates widget positions and sizes |
|
|
| Link rendering | Caches `_pos` center point and `_centreAngle` on the LLink instance |
|
|
|
|
This means:
|
|
|
|
- Rendering order matters (later nodes see side effects from earlier nodes)
|
|
- Performance profiling conflates render cost with layout cost
|
|
- Concurrent or partial renders would produce inconsistent state
|
|
|
|
## How ECS Addresses These Problems
|
|
|
|
| Problem | ECS Solution |
|
|
| ---------------------- | ------------------------------------------------------------------------------------------- |
|
|
| God objects | Data split into small, focused components in dedicated stores; behavior lives in systems |
|
|
| Circular dependencies | Entities are addressed by string keys; components have no inheritance hierarchy |
|
|
| Mixed concerns | Each system handles exactly one concern (render, serialize, execute) |
|
|
| Inconsistent IDs | Branded string keys per store (for example `WidgetId`) for cross-kind safety |
|
|
| Demeter violations | Systems query the relevant store directly; no entity-to-entity references |
|
|
| Scattered side effects | Version tracking becomes a system responsibility; mutations flow through store command APIs |
|
|
| Render-time mutations | Render system reads components without writing; layout system runs separately |
|