Badges go straight to plain BadgeData rows in a dedicated store, written by a reactive badge system; core badges materialize as rows too, killing the dual derivation and positional slice(1). Adds badge vocabulary to the domain glossary and repoints the node-data-store badges row. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
7.2 KiB
Node Data Store
Date: 2026-07-05 Status: Draft (design interview in progress; follow-up to the link topology store and reroute chain store)
Design record for extracting the remaining Node-owned components into a
dedicated store per ADR 0008,
eliminating the VueNodeData mirror and most of
src/composables/graph/useGraphNodeManager.ts.
Decision 1: One store, one plain state object per node
nodeDataStore holds a single plain NodeState object per node,
registered by reference with proxy-returning registration (the
BaseWidget pattern, reroute store Decision 4).
ADR 0008's Node component rows (NodeVisual, Execution, ...) become
field groupings inside NodeState, not separate records or stores.
Buckets are root-graph-scoped (rootGraph.id), keyed by NodeId.
Node-id uniqueness across sibling subgraph definitions is already
guaranteed by the load-time dedup pass
(src/lib/litegraph/src/subgraph/subgraphDeduplication.ts).
Decision 2: Field set — what is NodeState, what is elsewhere
NodeState {
id: NodeId
graphId: UUID // owning (sub)graph — partitioning + locator ids
type: string // identity, with apiNode?: boolean
title: string
titleMode?: TitleMode
mode: number
flags: { collapsed?, pinned?, ghost? }
color?: string
bgcolor?: string
shape?: number
resizable?: boolean
showAdvanced?: boolean
}
Excluded — owned or derived elsewhere; referencing them here would be a mirror (the hard constraint of this phase):
| Field | Owner |
|---|---|
selected |
canvasStore.selectedNodeIds (already what LGraphNode.vue reads) |
executing |
executionStore via useNodeExecutionState (already what Vue reads) |
hasErrors |
derived from executionErrorStore / missing-model/media stores; node.has_errors stays a legacy-canvas class field written by useNodeErrorFlagSync |
| position / size / z | layoutStore |
| widget values / order | widgetValueStore |
| input link connectivity | linkStore (getInputSlotLink / isInputSlotConnected) |
badges |
nodeBadgeStore — plain BadgeData rows written by a badge system; see Node Badge Store |
inputs / outputs |
deferred — see Decision 3 |
VueNodeData.selected and .executing are dead fields today (no
production consumer reads them); they are deleted, not migrated.
Decision 3: Slot arrays deferred; inputs[].link readers migrate now
NodeInputSlot / NodeOutputSlot are class instances with methods —
Slot entity extraction (ADR 0008 SlotIdentity etc.) is its own future
phase. The slot arrays stay class-side, keeping the shallowReactive
graft for renderer reactivity.
What this phase does remove is the last inputs[].link dependency: the
three remaining readers (nodeDataUtils.linkedWidgetedInputs used by
NodeSlots, and usePartitionedBadges' badge computed plus its
exported trackNodePrice) move to linkStore.isInputSlotConnected
queries — presence is all any of them needed — which deletes the
node:slot-links:changed → refreshNodeInputs reprojection in
useGraphNodeManager, the dead node:slot-errors:changed handler
(zero emitters repo-wide), and the node-removal refresh-all loop.
Shipped ahead of the store itself (2026-07-05).
Decision 4: Renderer consumes the NodeState proxy, VueNodeData dies
GraphCanvas iterates the store's bucket for the active graph (filtered
by NodeState.graphId) and passes the reactive NodeState proxy down
the existing prop-drilling path (LGraphNode → NodeHeader /
NodeSlots / NodeContent / NodeWidgets). Children read proxy fields
directly; Vue tracks the store state, so the per-property
node:property:changed → snapshot-rewrite handlers in
useGraphNodeManager are deleted wholesale.
Slot arrays reach NodeSlots via the existing live-node access
(getNodeByLocatorId), not through NodeState.
LGraphNodePreview constructs a synthetic NodeState (as it does a
synthetic VueNodeData today). AppModeWidgetList stops calling
extractVueNodeData and reads the registered NodeState + live node.
Decision 5: Registration lifecycle and class adoption
Follows the shipped trio convention (LLink / Reroute):
LGraphNodeconstructs its_state: NodeStateat instantiation;registerNodeState(graph, node)inserts it by reference and the class adopts the returned reactive proxy;node._graphId(root id) is the registration-ownership marker.- Chokepoints:
LGraph.add/LGraph.remove(the canonical sites),unregisterAllNodeStates(graph)on graphclear(), identity-checked delete (toRawcompare) so only the registered state vacates its key. - Class fields become accessors reading through
_state.LGraphNodeProperties' instrumented descriptors keep their get/set +node:property:changedemission but store the value in_stateinstead of a closure — trigger consumers (minimap,useErrorClearingHooks) keep working unchanged. - Serialization is unaffected:
serialize()reads the same properties through the accessors.
Decision 6: What remains of useGraphNodeManager
Deleted: extractVueNodeData, the vueNodeData map, all
node:property:changed handlers, syncWithGraph, getNode()
(consumers use graph.getNodeById), the node:slot-links:changed
handler (Decision 3).
Remaining renderer-side lifecycle, slimmed into useVueNodeLifecycle
(or a small successor):
- layoutStore seeding on node add/remove (
createNode/deleteNodelayout mutations, including theonAfterGraphConfigureddeferral) — layout is renderer policy, not entity data. node:slot-label:changedslot-array reprojection — dies with the Slot extraction phase.
Scope
Covers node shell state, the VueNodeData deletion, and the
inputs[].link reader migration. Out of scope: Slot entity extraction,
Properties (properties / properties_info) and NodeType metadata
beyond type/apiNode (category, nodeData, description remain on
the class/constructor), badges, WidgetContainer (already owned by
widgetValueStore), and command-pattern mutators (future work per
ADR 0003/0008).