Survivor selection consulted the link store, whose load-time registration
is first-wins by link map order — when a stale duplicate preceded the
referenced link, the referenced link was purged instead. Combined with
input slot drift, realignInputLinkSlots then had no link to realign and
the surviving registration stayed keyed to the stale slot, reading the
input as unconnected (issue #10291 regressed).
The serialized node data is now the survivor authority, with store
registration as the fallback for data that carries no input reference.
registerReroute silently overwrote existing chain entries, and the
identity-guarded delete meant a displaced owner could never vacate its
entry. Copying a subgraph node configured a throwaway live clone whose
identically-numbered reroutes shadowed the live chains, and pasting one
skipped reroute-id dedup entirely, conflating chains across graphs and
serializing corrupted parentId links. Serialize clipboard data from a
plain snapshot instead of a live clone, dedup reroute ids on paste with
the same machinery configure uses, and make registerReroute warn and
refuse foreign overwrites.
multiClone keeps node ids, so configuring the new subgraph while the
originals were still registered made every interior clone link lose
first-wins registration, hijacked the originals' reroute chains, and let
the later removal loop delete the badge keys the clones were piggybacking
on — converted nodes read disconnected and badge-less until save+reload.
Vacate the originals first: disconnect boundary links, remove interior
links and reroutes before the subgraph configures, and re-register the
subgraph's nodes in the badge store after the originals are removed.
Removing the last instance of a subgraph recursed unconditionally into
nested definitions: a nested definition still instanced elsewhere had its
inner nodes' badge registrations stripped and removal lifecycle fired
while live, while a definition referenced only inside the removed subtree
leaked its link topologies, reroute chains, and registry entry forever.
Compute the releasable set by excluding the removed instance from the
live-reference walk, then release each definition exactly once: lifecycle
first, then badge/link/reroute unregistration and registry removal.
isWidgetValue rejected null while the widget value type includes it, so
every promoted-widget hop coerced a stored null to undefined: serialize
dropped the value (deleting widgets_values entirely when all values were
null, so reload restored interior defaults), projection getters read null
back as undefined, reorder skipped rewriting it, and a null quarantined
host value fell through to stale widgets_values. Accept null in the
guard, use an explicit has-check for quarantine lookup, and compare
Reset-to-default enablement against the store state instead of
null-coalescing to the live widget value.
insertWorkflow configured a throwaway LGraph that adopted the file's
serialized graph id, registering scratch nodes into the root-scoped
stores. Inserting a workflow whose id matched the open one bound scratch
widgets first-wins to the live WidgetState objects and reverted unsaved
widget edits to file values; differing ids leaked the scratch bucket for
the session. Give the scratch graph a synthetic UUID and clear it in a
finally block so all store buckets are disposed even if copying throws.
LGraph.remove and the PrimitiveNode proxy-widget migration read
output.links through the warn-once deprecation getter, consuming the
telemetry budget before any third-party read could be observed. Migrate
both to store-backed queries, migrate two test assertions off the input
mirror, and pin node removal in the zero-deprecation-warnings regression
test.
The badge renderer cutover resolved extension badges as staying on raw
node.badges, leaving registerBadge/deleteBadge and the 'extension' badge
kind with no production callers and no future consumer. Delete the
mutators, shrink BADGE_KIND_ORDER to core/credits, and remove the
unreachable extension-row partition branch.
The linkStore is the single source for input-slot connectivity.
Readers use node.isInputConnected / node.getInputLink or the slotLinks
input helpers; NodeInputSlot.link is a deprecation-telemetry accessor:
reads warn and return the store-derived id, writes warn naming
connect()/disconnectInput() and are ignored. Serialization derives
inputs[].link from the store; wire format unchanged.
Store re-keying is authoritative: moving a registered link evicts any
incumbent under the new key, so slot permutations cannot drop links;
first-wins placement remains only for load-time registration, which
duplicate-link dedup depends on. dynamicWidgets rebuilds capture one
at-rest slot-to-link snapshot and reconcile once at the end.
fixLinkInputSlots is replaced by realignInputLinkSlots inside
LGraph.configure, reading the effective (possibly deduplicated-clone)
node data. disconnectOutput's duplicated branches collapse into one
per-link loop that removes the store entry before onConnectionsChange
fires. 'link' in slot discriminators are deleted in favor of direction
threading; _setConcreteSlots writes wrappers back into node
inputs/outputs so identity-based lookups hold; addInput/addOutput drop
legacy link/links keys from extra_info. First-party code no longer
touches either deprecated mirror.
The linkStore is the single source for output-side connectivity. The
NodeOutputSlot.links field and all nine write sites are gone; a
read-only prototype getter derives the array from the store and fires
warnDeprecated as migration telemetry for extensions (no setter, so
writes throw). INodeOutputSlot keeps links as deprecated readonly so
'links in slot' discriminants still compile. Wire format is unchanged:
serialization derives outputs[].links from the store, and serialized
operators (linkFixer serialized branch, migrateReroute, unpackSubgraph
strip) are untouched; linkFixer now completes a missing input mirror
instead of deleting a store-registered link.
Also fixes createTestSubgraphNode minting duplicate node ids (the
fixture never reserved the id it assigned) and promoted-widget labels
not inheriting the source slot label - both previously masked by the
mirror or the id collision.
Extension migration: read via node.isOutputConnected(slot) /
node.getOutputNodes(slot); mutate via node.connect() /
node.disconnectOutput(). Design record: docs/architecture/
output-slot-connectivity.md Decision 6.
All ~30 internal output.links read sites now go through the slotLinks
helpers or slot.isConnected; the mirror is write-only pending deletion.
Serialization derives outputs[].links from the store, sorted ascending
by id (equal to push order for organically built graphs) and null when
empty. configure() fires its output callbacks from the serialized
argument instead of the mirror copy.
Deliberate bugfix: disconnectInput passed the mirror-array index as the
OUTPUT slot number to onConnectionsChange; it now passes
link_info.origin_slot.
Link queries now return fully-assigned links only: buildOutputIndex skips
floating topologies, matching the output.links mirror they replace. The
domain rule lives in isFloatingTopology, which also replaces the
hand-rolled sentinel checks in rerouteStore, dynamicWidgets, and
widgetValuePropagation. The slot-drag disconnect check keeps its floating
awareness via slotFloatingLinks, its pre-store behavior.
New node/slotLinks helpers (outputHasLinks/outputLinkIds/outputLinks)
back the remaining app readers: rerouteNode, widgetInputs, groupNode,
proxyWidgetMigration, and useNodeReplacement now read the store instead
of output.links. Behavior change, deliberate: PrimitiveNode's
onLastDisconnect fires on disconnect-all, where the stale mirror
previously suppressed it.
getOutputSlotLinks now returns ReadonlySet<LinkTopology> instead of link
ids: floating links draw ids from a separate counter, so a bare id cannot
be resolved safely through graph.links, and the migrated readers need
endpoints anyway. Migrated: minimap link extraction, slot-drag disconnect
check (one store query replaces a mirror read plus a slotFloatingLinks
scan), widget value propagation, and matchType link revalidation.
rerouteNode/widgetInputs/groupNode stay on the mirror deliberately - they
read inside connection callbacks whose behavior depends on mid-mutation
mirror staleness - as do the serialized-workflow repair tools. Design
record updated.
Adds a derived per-graph reverse index over link origins to linkStore,
exposing isOutputSlotConnected / getOutputSlotLinks as the output-side
mirror of the input queries. Target and origin slot keys are branded
separately so a key built for one index cannot be looked up in the other.
NodeSlots now passes connected to InputSlot and OutputSlot from the store;
the lg-slot--connected class reaches the dot via CSS, so SlotConnectionDot
needs no new prop. Design record in docs/architecture.
Default IBaseWidget TValue to the canonical WidgetValue so a widget value
may be null. The Vue update handler now assigns the same normalized value
to the store, live widget .value, and callback instead of coercing .value
to undefined. Coerce null/void to undefined only at the persisted
NodeProperty boundary.
Amp-Thread-ID: https://ampcode.com/threads/T-019f2a75-8d64-7605-980d-7aca51a5f19c
Co-authored-by: Amp <amp@ampcode.com>
The promoted-widget side panel only ever has one host SubgraphNode, so
collapse the parents: SubgraphNode[] prop to host?: SubgraphNode across
SectionWidgets/WidgetItem/WidgetActions and drop the [node] wrapper in
TabSubgraphInputs.
Amp-Thread-ID: https://ampcode.com/threads/T-019f2964-7a96-7579-8883-caeb3b2b07da
Co-authored-by: Amp <amp@ampcode.com>
isLinkedPromotion has no production callers outside promotionUtils.
Drop its dedicated direct-unit describe block and rewrite the demoteWidget
tests that used it as an assertion oracle to check observable interior
link state instead.
Amp-Thread-ID: https://ampcode.com/threads/T-019f2964-7a96-7579-8883-caeb3b2b07da
Co-authored-by: Amp <amp@ampcode.com>
- demoteRow now removes the linked input via the host input slot we
already have, instead of re-resolving the source and calling
demotePromotedInput
- un-export demotePromotedInput (only internal callers remain)
- drop dead parents param from renameWidget/promptRenameWidget
Amp-Thread-ID: https://ampcode.com/threads/T-019f2964-7a96-7579-8883-caeb3b2b07da
Co-authored-by: Amp <amp@ampcode.com>
isShownOnParents never changed observable behavior: for promoted rows
parents[0] === node (so favoriteNode was unchanged) and the toggle was
suppressed by isLinked; for interior rows it was always false. Its
Hide-toggle branch and parent-redirect were unreachable.
- drop isShownOnParents prop from WidgetItem and WidgetActions
- favorites key directly off node (was favoriteNode)
- WidgetActions toggle becomes a one-way Show Input action; remove the
dead handleHideInput + demoteWidget usage and the hideInput locale key
- remove SectionWidgets.isWidgetShownOnParents
Amp-Thread-ID: https://ampcode.com/threads/T-019f2964-7a96-7579-8883-caeb3b2b07da
Co-authored-by: Amp <amp@ampcode.com>
SectionWidgets and WidgetActions identified promoted subgraph-input widgets
by walking live links back to the interior source. Per ADR0009 a promoted
widget is host-scoped (widget.widgetId = graphId:hostNodeId:inputName, also
carried on the host input slot), so reuse that instead:
- isLinked: inputForWidget(node, widget)?.widgetId != null
- isWidgetShownOnParents: match parent inputs by widget.widgetId
- handleHideInput: drop unreachable source-resolution branch, keep demoteWidget
- remove now-unused widgetPromotedSource export
Favorites keep their current (nodeLocatorId, widgetName) key; added a
TODO(ADR0009) to move to a pure WidgetId key (deferred: needs a persisted
format migration).
Amp-Thread-ID: https://ampcode.com/threads/T-019f2964-7a96-7579-8883-caeb3b2b07da
Co-authored-by: Amp <amp@ampcode.com>
The system's watch tracked only the current graph id's bucket key; a
workflow load buckets nodes under a brand-new graph id the watcher had
never observed, so it never fired again and no badges were written
after any configure. registeredNodeIds now also reads the bucket map
size, so bucket creation and deletion wake readers regardless of which
id they last saw.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
LGraph.clear() and configure() reassign the root graph id, so the badge
system now resolves it on every recompute instead of capturing it at
start; a captured id stranded the system on a dead bucket after
Clear Workflow, dropping every badge on nodes added afterwards.
LGraph.add registers the node in the badge store only after
onNodeAdded: the store write wakes the system watcher and queues Vue's
flush, which must not overtake the deferred paste scan that surfaces
missing-model errors before selection-scoped tabs recalculate.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Wrapper aggregation counts inner api nodes (the old unconditional
per-api-node badge basis) rather than resolved credits rows, so a
pending async price still yields 'Partner Nodes x N'; the label is
localized via nodeBadge.partnerNodesCount. useNodePricing now caches
labels per signature: a leaf's own read and its wrapper's
promoted-override read previously shared one cache slot and evicted
each other, re-scheduling evaluations forever once both ran in eager
system effects.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Legacy drawBadges renders the store rows through a per-node memoized
LGraphBadge cache (core parts joined id/lifecycle/source and truncated,
icons resolved via a badge icon registry) followed by the raw
node.badges extension surface, whose thunks still evaluate per frame.
The Vue partition reads the same rows — trimming lifecycle brackets and
swapping a built-in node's source row for the Comfy logo chip — plus the
raw extension surface.
useNodeBadge now bootstraps the badge system per root graph and forwards
the subgraph structure events; dynamic-pricing recalculation wiring is
kept. The usePriceBadge closures, the VueNodeData.badges mirror, its
property trigger, and node.badgePosition (no ecosystem uses; single
writer/reader) are deleted. node.badges itself stays: three custom-node
packs actively push to it.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
SubgraphNode wrappers project their credits row from the inner nodes'
store rows: several priced inner nodes collapse to 'Partner Nodes x N',
a single one shows its price with the wrapper's promoted widget values
overriding the inner node's own. The aggregation tracks inner rows, the
registered-node set, and an exported structure revision for the events
stores cannot observe.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Renderers own presentation: legacy joins id/lifecycle/source in its own
order with brackets intact, Vue trims and reorders. Rows carry the raw
projected text plus a part discriminator instead of pre-trimmed text.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A per-node effect flushing between unregisterNode and its scope being
stopped could re-create the bucket key and resurrect a removed node's
registration. Row writes (registerBadge, setBadgesOfKind) now require
an existing registration; registerBadge returns undefined when refused.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
add/remove/clear (and subgraph-definition GC) now maintain each node's
nodeBadgeStore registration in the root bucket, following the
linkStore/rerouteStore chokepoint convention. Tests that construct
graphs without an active Pinia gain the standard testing-pinia setup.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
startBadgeSystem watches the registered-node set of a root graph bucket
and runs one effect scope per node: a pure computeBadges projection of
nodeDef, badge-mode settings, palette, and pricing sources, written to
nodeBadgeStore as core/credits rows (node-badge-store.md decisions 3-4).
nodeBadgeStore gains the registerNode/unregisterNode/registeredNodeIds
registration surface the system consumes.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Root-graph-scoped buckets keyed by NodeId holding plain BadgeData rows,
per docs/architecture/node-badge-store.md decisions 1-2. Reads are
kind-ordered (core, credits, extension); the badge system rewrites its
kinds wholesale via setBadgesOfKind, extension rows use identity-checked
register/delete.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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>
Review follow-ups: drop comments that paraphrased the call, type the
graphId params as UUID, unify the undefined guard, hoist the mocked
graph id, and thread the shared pinia through renderUnified.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Decisions 1 (single NodeState object, proxy-returning registration) is
agreed; field set, slot scope, and consumer contract are drafted pending
review. Records the shipped inputs[].link reader migration.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The refresh-all loop existed to poke inputs[].link readers after a
neighbor's links were torn down. Those readers now query linkStore,
which unregisters link state reactively during removal, so the loop and
refreshNodeInputs go. Verified against the vue-nodes legacy widget e2e
spec that motivated the loop.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
NodeSlots.linkedWidgetedInputs, usePartitionedBadges, and trackNodePrice
queried the input.link slot mirror, which only stayed reactive through
the node:slot-links:changed -> refreshNodeInputs reprojection. All three
now query linkStore.isInputSlotConnected by root graph id and slot
index, so the slot-links handler and the emitter-less
node:slot-errors:changed handler are deleted.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Link geometry is still removed from the layout store in the current
node-removal flow; the target-architecture matrix now marks the
SlotConnection write as future work; the proto-ECS inventory narrows
LayoutStore's link source to geometry.
Review findings: validate the proposed parent chain in Reroute.parentId
(cycles were checked against the current chain, and a created cycle then
blocked repair); snapshot per-link reroute validity before connecting in
dropOnReroute; remove pruned reroutes from the paste result; truncate
cyclic or broken reroute chains during subgraph unpack instead of
throwing mid-mutation or stitching dangling ids; make RerouteChain.id
readonly; reuse the computed input index in SubgraphInputNode.
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.
connectToSubgraphOutput wrote the floating link's origin instead of its
target — a subgraph output is the consuming end (target_id ===
SUBGRAPH_OUTPUT_ID everywhere else); the deleted slot mirror had masked
the bad endpoints from readers. removeInput/removeOutput now renumber
floating-link slots alongside real links, fixing stale attachment
indices that the mirror's slot-object identity used to paper over at
runtime (and that persisted stale into serialized floating links).
Replace the three interleaved walk-and-write loops (externalFirst
branches with per-hop error handling and outer-loop breaks) with
collect-then-stitch: walk the internal (migrated) and external chain
segments, order them by externalFirst, and write the chain pointers in
one generic pass. A truncated segment stops stitching at the break, as
before. Drops the order-dependent 'Missing Parent ID' false-positive
check from the migration loop.
move() already forwards position changes to the layout store, but
snapToGrid mutated posInternal silently, leaving the Vue hit-testing
spatial index stale after a snapped drag.
A floating link has exactly one assigned endpoint, so its slot
attachment is fully encoded in its own origin/target fields. Replace
the hand-maintained slot._floatingLinks Sets (25+ write sites across
addFloatingLink/removeFloatingLink, setFloatingLinkOrigin, and every
FloatingRenderLink connect method) with the slotFloatingLinks(network,
side, nodeId, slot) derivation, and delete the property.
moveInputLink/moveOutputLink gain the node param their dragNewFrom*
siblings already take. FloatingRenderLink.connectToInput disconnects
the target input before re-targeting the floating link, since the
target slot's floating-link cleanup now sees the re-targeted link
immediately. The dead floating spread in hasRelevantOutputLinks
(LLink objects never passed its typeof-number filter) is removed.
The layout store wrote parentId and linkIds into its reroute Y.Maps but
only ever read position back — a third, dead copy of chain data now that
the reroute store owns the chain and membership is derived. Remove the
fields from CreateRerouteOperation, the mutation API, and the
useVueNodeLifecycle seeding. Also fold Reroute.removeFloatingLink into
removeAllFloatingLinks, its only caller.
Deduplicate the connect-path floating-chain cleanup (LGraphNode,
SubgraphInput, SubgraphOutput) into anchorRerouteChain, add
unregisterAllRerouteChains mirroring the link-side helper, splice
createReroute's live/floating loops into one pass over the actual link
objects (also removing the ambiguous cross-id-space lookup for a link
segment), inline the single-caller Reroute.update into setReroute, and
drop test pinia hooks shadowed by the file-level one.
Share EMPTY_MEMBERSHIP from the store, stop creating buckets inside the
membership computed, align registerRerouteChain's graph param with
registerLinkTopology, unnest the dedup id allocator, tighten store test
assertions, and clarify the design record (updateEndpoint guard,
load-time orphan drop).
Reroute.linkIds/floatingLinkIds become derived accessors over the
reroute store's membership index; the ~10 manual mirror write sites
(connect paths, disconnect, floating add/remove, createReroute, subgraph
unpack, subgraph IO connect, LinkConnector) and validateLinks are
deleted. Configure and paste prune reroutes by derived totalLinks
instead of repairing stored sets, and serialization emits membership in
ascending link-id order.
LLink.disconnect unregisters the link before pruning so derived
counts exclude it. dropOnReroute resolves the drop target's source
output once before connecting moved links: re-anchoring the first
link's chain changes derived membership immediately, so a per-link
lookup could pick a not-yet-moved link's origin.
Reroute ids must be unique within a root graph now that the reroute
store keys every reroute in one root bucket, but subgraph definitions
from older frontends or external tools may number reroutes from
scratch. Add deduplicateSubgraphRerouteIds alongside the node-id dedup:
remaps colliding ids and patches reroute.parentId and link.parentId
references, advancing the shared state.lastRerouteId.
New rerouteStore holds RerouteChain {id, parentId?, floating?} in
root-scoped buckets keyed by RerouteId, registered by reference; the
Reroute class adopts the store proxy as _chain so parentId/floating
writes are tracked. Link membership is derived from the links' parentId
chains via a per-graph computed reverse index (getMembership), split
into live and floating sets by unassigned endpoints.
LGraph._addReroute/_removeReroute are the map chokepoints (register,
unregister, layout cleanup); all reroute map mutation sites route
through them. delete reroute.floating sites become undefined
assignments since floating is now an accessor.
registerLink/updateEndpoint now return the store-held reactive state and
registerLinkTopology assigns it back to link._state (the BaseWidget
pattern), so field writes such as link.parentId are tracked by Vue
instead of silently mutating the raw object.
At most one live link can target a given input slot — litegraph
disconnects the previous link before connecting a new one — and every
store consumer queries by target, so make the target slot the primary
key instead of the link id:
- One target-keyed map per root bucket replaces the id-keyed map plus
the separate target-slot index; connectedness queries are a single
lookup and the index/unindex/reindex machinery disappears.
- Links without a unique target live in a per-graph side collection:
floating links (either endpoint unassigned, and multiple can share an
input slot) and links into SUBGRAPH_OUTPUT_ID (a constant shared by
every subgraph in a bucket, which previously let sibling subgraphs
clobber each other's index entries). Floating links no longer answer
isInputSlotConnected, restoring parity with input.link semantics.
- updateEndpoint/deleteLink take the topology by reference and are
identity-checked; updateEndpoint reports whether the link still holds
a registration so LLink._graphId stays truthful.
- Delete deduplicateSubgraphLinkIds and its configure wiring: link-id
collisions between subgraph definitions no longer touch the store, so
workflows load without link-id rewrites again. Node-id dedup remains
for widget-store keys.
Address code-quality review findings:
- removeFloatingLink now detaches via unregisterLinkTopology instead of
deleting from the store directly, which had left a stale _graphId that
silently dropped subsequent endpoint writes into the store's early
return (covered by a new regression test).
- Extract unregisterAllLinkTopologies for the unregister-a-whole-graph
loops duplicated between non-root clear() and subgraph-definition
removal.
- Merge deduplicateSubgraphNodeIds/LinkIds behind a single
deduplicateSubgraphIds entry point so the mutates-in-place link pass
is an internal detail and configure makes one call; the module now
exports one dedup function instead of two.
Reinstate LGraph._removeLink as the delete-side counterpart to _addLink,
now safe because load-time dedup keeps link ids unique per root bucket:
- purgeOrphanedLinks and linkFixer route removals through _removeLink,
which unregisters the link topology and layout entry; the survivor's
registration is re-asserted since a purged duplicate may have owned its
target-slot index entry (registerLink now re-indexes same-topology
re-registration).
- LLink._graphId is only set when the store keeps the link's state, so a
first-wins collision loser stays detached and its disconnect can no
longer delete the winner's entry.
- Removing the last instance of a subgraph drops the definition's links
(and floating links) from the store along with the definition.
- clear() on subgraphs and unconfigured (zero-uuid) graphs unregisters
each link individually instead of leaking entries into the shared
bucket.
Also cover floating-link id dedup with a fixture test and correct the
doc comment misplaced on numericSerializedNodeId.
Link ids, like node ids, must be unique within a root graph: the link
store keys every link in a root graph (its subgraphs' included) in one
bucket, but subgraph definitions each number their links from scratch, so
a subgraph link would collide with a root/sibling link and clobber its
target-slot index. That dropped externally linked subgraph inputs from the
store (promoted widgets rendered enabled instead of disabled; nested
prompts failed to resolve links).
Add deduplicateSubgraphLinkIds alongside the existing node-id dedup,
sharing its id allocator, and run it in root configure before subgraphs
are created. It remaps colliding link/floating-link ids and patches every
reference (node input.link / output.links, subgraph IO linkIds, reroute
linkIds), advancing state.lastLinkId. With ids unique, the store bucket is
collision-free and registration order no longer matters.
Drop the speculative LLink id-setter re-key and the removeFloatingLink
_graphId clear (no callers). YAGNI.
The previous commit made linkStore.registerLink replace on id collision
and re-assert the survivor after duplicate purging, and routed dedup /
linkFixer removals through a store-deleting chokepoint. That assumed link
ids are unique within a graph bucket, which is false: sibling subgraphs
share one root-keyed bucket and reuse link ids, so a subgraph link would
clobber a root link's target-slot index. This dropped the externally
linked subgraph-node input from the store, so promoted widgets rendered
enabled instead of disabled and nested-subgraph prompts failed to resolve
links (e2e shards 6/7).
Revert to parent link-store semantics: registerLink is first-wins,
purgeOrphanedLinks removes from _links without touching the store (no
survivor re-assert), and linkFixer deletes from the map directly. Drop the
now-unused LGraph._removeLink. Verified the post-configure store state for
the failing fixtures is byte-identical to the pre-change baseline.
Cleanly separating the link store per owning graph (so ids don't collide)
is the real fix for the dedup/linkFixer store orphans; deferred.
Route every graph link-map add/remove through single LGraph._addLink and
_removeLink chokepoints so the reactive linkStore (and layout geometry)
can no longer silently desync. Fixes three real desyncs where raw
_links.set/delete bypassed the store: subgraph reconnect never registered
the re-homed link, and purgeOrphanedLinks / linkFixer deleted links
without cleaning the store.
Also collapse the store's now-unused output-side surface: drop the
originSlotIndex, getOutputSlotLinks, and getNodeLinks (its sole consumer
was a no-op cleanup loop in layoutStore.handleDeleteNode, since node
removal disconnects links before onNodeRemoved fires). Reverting that loop
lets graphId come back off the persisted DeleteNodeOperation and removes
the layout->link cross-store call.
registerLink now always re-asserts its target-slot index and lets the
newest object win an id collision; the LLink id setter re-keys the store;
purgeOrphanedLinks re-asserts the survivor after removing duplicates that
shared its slot. AppModeWidgetList reads connectivity from linkStore for
consistency and reactivity.
- name the nested link index map types and use them in the ref decls
- return the registered topology directly instead of re-reading with a cast
- replace the banned as-unknown-as Path2D stubs with fromPartial
- drop no-op UUID casts and a dead rootGraph optional chain
- clear LLink._graphId in disconnect() so post-disconnect endpoint writes
fall through to the direct _state branch instead of no-opping
- dedupe the four endpoint setters through one applyEndpointPatch helper
- skip the target-slot index for floating links (UNASSIGNED_NODE_ID) in
indexLink/unindexLink so a second floating link can't overwrite a real
slot's entry
- drop the dead getNodeLayoutRef null-set branch and its zeroUuid sentinel;
deletion always flows through layoutMutations.deleteNode with a graphId
NodeSlots.linkedWidgetedInputs and usePartitionedBadges still read
vueNodeData.inputs[].link directly; without refreshNodeInputs firing on
node:slot-links:changed those go stale on connect/disconnect.
buildSlotMetadata fell back to the node.inputs[].link mirror when
graphId was undefined, breaking single-source-of-truth from the
link-topology store and matching a fallback WidgetItem.vue already
lacks. Also collapse the redundant isInputSlotConnected +
getInputSlotLink pair into one getInputSlotLink call.
Binds each LLink._state into useLinkStore by reference at every litegraph
create site (LGraphNode.connectSlots, LGraph.configure, addFloatingLink,
SubgraphInput/Output.connect), deletes on disconnect, and reindexes on
endpoint moves via a guarded setter delegation to updateEndpoint. Adds a
temporary DEV+pinia-gated subset invariant (removed in Task 6) asserting
graph._links is always a subset of the store.
Link changes reach the panel through the node manager re-setting
vueNodeData (refreshNodeInputs), not through in-place mutation of
node.inputs, which fires no reactive signal. Read isLinked from the
manager's projection like the widget grid does, so a linked widget
disables without a bespoke event listener.
Combine widget value + render-state registration into one
registerWidget call so callers can't drift; register render state for
nested promoted inputs. Derive the live widget map once in
computeProcessedWidgets and drop the redundant slot-links listener in
WidgetItem.
Extract WidgetGridItem and WidgetSlotMetadata into a shared types module and
make ProcessedWidget extend it, dropping the dead flat fields it duplicated.
Decompose computeProcessedWidgets into resolveLiveWidgetContext + processWidget,
dedupe ids via a Set, and stop exporting isWidgetVisible/hasWidgetError (tested
through the public compute path instead).
WidgetGrid drives widgets through :model-value so updateHandler is the sole
writer; preview items stay static. removeWidget now drops a widget from the
render order without purging its stored value, so remove-then-re-add restores
what the user set.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
getWidgetIdentity took two params it never read and returned two
identical strings under different names. Inline it to a single renderKey
used for both dedupe and the render key.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
getLiveWidget rescanned every node.widgets entry for each widget id
(O(n^2) per node, per reactive recompute), and the same duplicate-index
scan was hand-copied into LGraphNode.vue. Extract mapLiveWidgetsById so
both build the WidgetId->widget map once. Also resolve the promoted
widget source a single time per widget instead of once for sourceWidget
and again for the error target.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
isDOMBackedWidget and the render-state object were copy-pasted into
BaseWidget, SubgraphNode, and AppModeWidgetList. Collapse both into
deriveWidgetRenderState in the canonical litegraph widget util so all
three register through a single derivation.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The graphWidgetSpecs map, WidgetSpec type, registerWidgetSpec/getWidgetSpec,
and replaceNodeWidgetOrder had no production callers: specs always fell
through to nodeDefStore.getInputSpecForWidget, and no path pruned via
replaceNodeWidgetOrder. Also drop the unused getNodeWidgetIds litegraph
util and an inert File-array branch in normalizeWidgetValue.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`order` (graphNodeWidgetOrders) is already maintained per node on
register/delete, so the reconciliation-by-full-scan in the read helpers
was redundant. getRegisteredNodeWidgetIds and getNodeWidgetIds now return
`[...order]` directly, and replaceNodeWidgetOrder prunes by iterating the
node's registered ids instead of the whole graph.
This removes an O(graph-widgets) scan from hot paths (per-node on load,
per-rendered-node in computeProcessedWidgets) and stops getNodeWidgetIds
from splicing reactive state during a Vue computed read.
The preview's `value` kept an explicit empty-string widget value (nullish
coalescing) while `values` dropped it (truthy check), so a combo whose
provided value is "" no longer listed that value among its options. Match
the presence check so the selected value is always led in.
WidgetGrid now reads name/type/value from the widget's `simplified`
descriptor instead of duplicating them as top-level fields. A preview
item collapses to `{ simplified, vueComponent, visible, renderKey }`.
Build the preview widget list as a plain inline computed in
LGraphNodePreview (like the pre-refactor path) instead of a dedicated
composable. WidgetGrid now takes a narrow WidgetGridItem (a structural
subset of ProcessedWidget, so NodeWidgets is unchanged), defaults the
interactive-only fields, and derives gridTemplateRows itself — removing
that prop from both callers and the gridTemplateRows/visibleWidgets
computeds from useProcessedWidgets. A preview widget is now just its
name, type, value, simplified spec, component, and visibility.
Extract the presentational grid from NodeWidgets into WidgetGrid, driven by
a plain ProcessedWidget[] prop. NodeWidgets keeps the store-backed
useProcessedWidgets path (and its interactivity, forwarded to the grid via
attribute fallthrough); LGraphNodePreview builds ProcessedWidget[] straight
from the node schema via a new pure usePreviewWidgets composable.
The preview no longer fabricates widgetValueStore entries: gone are the
synthetic graph-id counter, the register widget/render-state/spec writes,
the manual state patching, and the onUnmounted cleanup. A static preview
has no live graph, so it now touches no store-backed widget state at all.
LGraphNodePreview derived previewWidgetIds in a computed whose body wrote
to widgetValueStore (register widget/render-state/spec), so a "pure"
derivation was the write path. Split it into a pure previewWidgets
computed plus a watchEffect that performs the store registration, and drop
the redundant state.type reassignment. Behaviour is unchanged; the writes
now live where side effects belong.
The nested promoted-missing-model test asset used readable slug ids for
its subgraph definitions. createNodeLocatorId only accepts UUID subgraph
ids, so getLiveWidget could not resolve the interior nodes, and the stale
promoted combobox never rendered disabled. Real subgraphs always get uuid
ids; the asset now matches, which is the actual fix for the interior
disabled-state assertion.
Revert the useProcessedWidgets disabled-derivation change: with valid
asset ids the original logic is correct, so the refactor was unnecessary.
Query the live node's widget input slot (getSlotFromWidget) as the single
source of truth for whether a widget is linked/disabled, matching
LGraphNode.updateComputedDisabled. This replaces the buildSlotMetadata
snapshot derivation, which missed promoted interior subgraph widgets whose
input carries a link to the subgraph input node, leaving them enabled.
Also drop the runtime import('/src/...') setup from the legacy app-mode
spec (only resolvable against the dev server, not the CI dist build) in
favor of the native devtools widget plus enterAppModeWithInputs.
Some files were not shown because too many files have changed in this diff
Show More
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.