Commit Graph

8556 Commits

Author SHA1 Message Date
DrJKL
3772615fcb refactor!: delete the input.link runtime mirror
The linkStore is now the single source for slot connectivity in both
directions. The NodeInputSlot.link field and all runtime write sites are
gone; a read-only prototype getter derives the id from the store and
fires warnDeprecated as extension migration telemetry (no setter, so
writes throw). Wire format unchanged: serialization derives
inputs[].link from the store.

The mirror-carried association machinery is reworked rather than
papered over: fixLinkInputSlots consumes the serialized graph data
(where node.configure reorders inputs in place), dynamicWidgets' group
rebuilds carry slot-to-link association in a WeakMap refreshed from the
store, reorderSubgraphInputs captures outer links by pre-reorder index,
and link deduplication selects survivors from the store registration -
repairInputLinks is deleted because the derived view cannot be
inconsistent.

Extension migration: read via node.isInputConnected(slot) /
node.getInputLink(slot); mutate via node.connect() /
node.disconnectInput().
2026-07-07 16:40:51 -07:00
DrJKL
75ad306a80 refactor: litegraph internals read input.link from linkStore
The input query family (isInputConnected, getInputLink, getInputNode,
getInputData/DataType, getInputOrProperty), connect displacement,
disconnectInput, removeInput reindexing, bypass passes, canvas link
highlighting, subgraph boundary resolution, and ExecutableNodeDTO all
derive from the store via the slotLinks input helpers. Serialization
emits inputs[].link from the store; configure fires its callbacks from
the serialized argument. NodeInputSlot.isConnected and toJSON derive.
The slot-connectivity helpers move to module scope: private-field
methods break when nodes are wrapped in Vue reactive proxies.

Mirror still written; linkDeduplication keeps reading it (the store
cannot represent contested duplicate targets, which are its input).
2026-07-07 16:18:08 -07:00
DrJKL
78469817a7 refactor: migrate app-side input.link readers to store-backed queries
App code reads input connectivity through node.isInputConnected /
node.getInputLink or the new slotLinks input helpers (inputHasLink,
inputLinkId, inputLink) instead of the input.link mirror. Serialized
operators (litegraphUtil compression, migrateReroute, linkFixer) and the
mirror-carried association shuffles (dynamicWidgets autogrow,
fixLinkInputSlots, useNodeReplacement transplant writes) stay on the
mirror until it is deleted.
2026-07-07 10:44:16 -07:00
DrJKL
f4ceedb787 refactor!: delete the output.links runtime 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.
2026-07-07 01:40:32 -07:00
DrJKL
b4fa1ed3f1 refactor: litegraph internals read output links from linkStore
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.
2026-07-07 01:12:55 -07:00
DrJKL
a4f38f71e2 refactor: output link queries exclude floating links; migrate last mirror readers
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.
2026-07-07 00:47:13 -07:00
DrJKL
603154e24f refactor: migrate output.links readers to linkStore topologies
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.
2026-07-06 20:51:52 -07:00
DrJKL
5652a1c23f feat: answer output slot connectivity from linkStore and wire slot connected state
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.
2026-07-06 20:22:45 -07:00
DrJKL
b71340970e fix: preserve null widget values across store, live widget, and callback
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>
2026-07-06 13:54:17 -07:00
DrJKL
32b3c28a1c That function was silly. 2026-07-06 13:54:17 -07:00
DrJKL
1b9906efbb refactor: pass single host instead of parents array to widget rows
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>
2026-07-06 13:54:17 -07:00
DrJKL
9f3d11a0ba test: cover host-scoped widget actions and null value forwarding
- WidgetActions: show-input promotion, isLinked gating, favorite toggle
  by host node
- useProcessedWidgets: live update handler forwards null (not undefined)
  to the widget callback

Amp-Thread-ID: https://ampcode.com/threads/T-019f2964-7a96-7579-8883-caeb3b2b07da
Co-authored-by: Amp <amp@ampcode.com>
2026-07-06 13:54:17 -07:00
DrJKL
e6ab296c94 refactor: un-export isLinkedPromotion, assert demotion via link state
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>
2026-07-06 13:54:17 -07:00
DrJKL
0679deb5f9 refactor: host-scope demoteRow and drop dead renameWidget parents
- 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>
2026-07-06 13:54:17 -07:00
DrJKL
7400d4e6f2 refactor: remove inert isShownOnParents plumbing
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>
2026-07-06 13:54:17 -07:00
DrJKL
1940be51d7 refactor: make promoted widget rows host-scoped, drop source traversal
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>
2026-07-06 13:54:17 -07:00
DrJKL
a71df2f1b5 fix: track badge bucket membership so workflow loads wake the system
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>
2026-07-06 13:54:09 -07:00
DrJKL
a310989209 refactor: trim badge comments to constraints the code cannot show
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 13:54:09 -07:00
DrJKL
84693005e4 fix: resolve badge graph id live and register nodes after onNodeAdded
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>
2026-07-06 13:54:09 -07:00
DrJKL
51ba284f7f fix: preserve legacy subgraph credits semantics and settle pricing cache
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>
2026-07-06 13:54:09 -07:00
DrJKL
4c9a2b1312 docs: record slice B implementation and resolved badge decisions
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 13:54:09 -07:00
DrJKL
7e08de9e3b feat: cut both renderers over to store badge rows
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>
2026-07-06 13:54:09 -07:00
DrJKL
dce951b819 feat: aggregate subgraph credits in the badge system
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>
2026-07-06 13:54:09 -07:00
DrJKL
0d0a3dbdec feat: tag core badge rows with projection part, keep raw text
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>
2026-07-06 13:54:09 -07:00
DrJKL
cb9ebecee8 fix: refuse badge row writes for unregistered nodes
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>
2026-07-06 13:53:59 -07:00
DrJKL
8c43b39e7b docs: record badge store slice A implementation state
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 13:53:59 -07:00
DrJKL
070ea46873 refactor: unexport module-internal NodeDefBadgeSources
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 13:53:59 -07:00
DrJKL
e09b47ffe5 feat: register node badge buckets at LGraph chokepoints
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>
2026-07-06 13:53:59 -07:00
DrJKL
c00b778059 feat: add reactive badge system writing store rows per node
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>
2026-07-06 13:53:59 -07:00
DrJKL
64bce0e602 feat: add nodeBadgeStore with kind-partitioned badge rows
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>
2026-07-06 13:53:59 -07:00
DrJKL
a6b9d4e254 docs: draft node badge store design record
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>
2026-07-06 13:47:07 -07:00
DrJKL
dd3dd63b25 refactor: tighten graphId typing and badge test hygiene
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>
2026-07-06 13:47:07 -07:00
DrJKL
c306c567f1 docs: draft node data store design record
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>
2026-07-06 13:47:07 -07:00
DrJKL
2185f61b51 refactor: drop node-removal input reprojection loop
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>
2026-07-06 13:47:07 -07:00
DrJKL
29c7781d0a refactor: read widgeted-input connectivity from linkStore
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>
2026-07-06 13:47:07 -07:00
DrJKL
d07248890c docs: qualify layout-store cleanup and future SlotConnection writes
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.
2026-07-06 13:46:59 -07:00
DrJKL
f5c5d10897 fix: harden reroute chain, paste, and unpack edge cases
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.
2026-07-06 13:46:59 -07:00
DrJKL
0b959753ee docs: align ECS architecture records with shipped link/reroute stores
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.
2026-07-06 13:46:59 -07:00
DrJKL
2d51f2ca43 fix: address floating-link review findings
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).
2026-07-06 13:46:59 -07:00
DrJKL
45d0566ff9 refactor: restructure subgraph-unpack reroute chain stitching
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.
2026-07-06 13:46:59 -07:00
DrJKL
62dedcc0dd fix: mirror reroute snapToGrid into the layout store
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.
2026-07-06 13:46:59 -07:00
DrJKL
01c0c8e86d refactor: derive slot floating links from link endpoints
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.
2026-07-06 13:46:59 -07:00
DrJKL
fcba5b297c refactor: drop write-only reroute chain data from the layout store
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.
2026-07-06 13:46:51 -07:00
DrJKL
bdf47aebca refactor: extract anchorRerouteChain and collapse chain-splice loops
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.
2026-07-06 13:46:51 -07:00
DrJKL
43a4058be9 refactor: apply reroute-store review cleanups
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).
2026-07-06 13:46:51 -07:00
DrJKL
18c9e769a4 refactor: derive reroute link membership from parentId chains
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.
2026-07-06 13:46:51 -07:00
DrJKL
567643abe3 fix: deduplicate subgraph reroute ids per root graph on load
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.
2026-07-06 13:46:51 -07:00
DrJKL
75feebdf5e feat: reroute chain store with derived link membership
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.
2026-07-06 13:46:51 -07:00
DrJKL
d14ed83e1e test: reroute serialization golden (key order + byte-identical round-trip) 2026-07-06 13:46:51 -07:00
DrJKL
cb3e757497 refactor: adopt the link store's reactive proxy as LLink._state
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.
2026-07-06 13:46:51 -07:00