Commit Graph

8513 Commits

Author SHA1 Message Date
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
DrJKL
56f2abc519 docs: record reroute chain store design and start domain glossary 2026-07-06 13:46:51 -07:00
DrJKL
1fb64cec49 refactor: key the link store by target input slot
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.
2026-07-06 13:46:42 -07:00
DrJKL
1038264ae3 refactor: consolidate link-store unregistration and subgraph id dedup
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.
2026-07-06 13:46:42 -07:00
DrJKL
fc55165eae fix: unregister removed links from the link store at every delete site
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.
2026-07-06 13:46:42 -07:00
DrJKL
da9280d13c fix: deduplicate subgraph link ids per root graph on load
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.
2026-07-06 13:46:42 -07:00
DrJKL
056531b59a fix: keep link-store registration first-wins to fix subgraph regression
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.
2026-07-06 13:46:41 -07:00
DrJKL
b54a1a3471 fix: funnel link map mutations through LGraph._addLink/_removeLink
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.
2026-07-06 13:46:41 -07:00
DrJKL
31ef2d0dfe refactor: linkStore/test review cleanups
- 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
2026-07-06 13:46:41 -07:00
DrJKL
1d8e6e2b9b fix: address link store correctness findings
- 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
2026-07-06 13:46:41 -07:00
DrJKL
327e3f84f0 fix: restore node:slot-links reprojection for un-migrated link consumers
NodeSlots.linkedWidgetedInputs and usePartitionedBadges still read
vueNodeData.inputs[].link directly; without refreshNodeInputs firing on
node:slot-links:changed those go stale on connect/disconnect.
2026-07-06 13:46:41 -07:00
DrJKL
cf8f87c37f test: link topology serialization goldens 2026-07-06 13:46:41 -07:00
DrJKL
af77a27567 refactor: drop node.inputs link mirror 2026-07-06 13:46:41 -07:00
DrJKL
8978502e8d refactor: drop node.inputs link fallback in buildSlotMetadata
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.
2026-07-06 13:46:41 -07:00
DrJKL
394ff10b23 refactor: read widget link state from linkStore 2026-07-06 13:46:41 -07:00
DrJKL
53dde242bd refactor: require graphId on DeleteNodeOperation 2026-07-06 13:46:41 -07:00
DrJKL
310de8ee08 refactor: remove layoutStore link connectivity mirror 2026-07-06 13:46:41 -07:00
DrJKL
d4ad799f0e refactor: register link topology into linkStore at canonical sites
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.
2026-07-06 13:46:41 -07:00
DrJKL
2f1e14c53f refactor: back LLink topology with a single _state object 2026-07-06 13:46:41 -07:00
DrJKL
495ea990f3 feat: link topology store 2026-07-06 13:46:41 -07:00
github-actions
c8692de343 [automated] Update test expectations 2026-07-06 20:20:34 +00:00
DrJKL
1597d20bed fix: preserve null as distinct widget value in normalizeWidgetValue
Amp-Thread-ID: https://ampcode.com/threads/T-019f2953-7ed6-7088-826f-490ffa39ed0c
Co-authored-by: Amp <amp@ampcode.com>
2026-07-06 12:36:01 -07:00
DrJKL
e38dca7f5f Revert graph.spec.ts change 2026-07-06 12:36:01 -07:00
DrJKL
02790eaa28 fix: source WidgetItem link state from reactive vueNodeData
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.
2026-07-06 12:36:01 -07:00
DrJKL
068eb13ee0 refactor: fold widget render state into a single store action
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.
2026-07-06 12:36:01 -07:00
GitHub Action
8d17098926 [automated] Apply ESLint and Oxfmt fixes 2026-07-06 12:36:01 -07:00
Alexander Brown
ec7dbde89c Apply suggestions from code review
Co-authored-by: Alexander Brown <DrJKL0424@gmail.com>
2026-07-06 12:36:01 -07:00
DrJKL
0175ab9aa2 refactor: share widget grid types and slim processed widgets
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>
2026-07-06 12:36:01 -07:00
DrJKL
0c073cac53 refactor: inline widget render key derivation
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>
2026-07-06 12:36:01 -07:00
DrJKL
af8812e171 perf: resolve live widgets by id once per node
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>
2026-07-06 12:36:01 -07:00
DrJKL
b8e8b5b53a refactor: derive widget render state through one helper
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>
2026-07-06 12:36:01 -07:00
DrJKL
003dcea90a refactor: drop dead widget spec store and replaceNodeWidgetOrder
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>
2026-07-06 12:36:01 -07:00
DrJKL
0fb8625abf docs: tighten WidgetGridItem comment 2026-07-06 12:36:01 -07:00
DrJKL
510dd01e8b refactor: drop verbose comment from getNodeWidgetIds 2026-07-06 12:36:01 -07:00
DrJKL
b8d319b187 refactor: drop getRegisteredNodeWidgetIds duplicate of getNodeWidgetIds
Once both just return `[...order]` they were identical; use
getNodeWidgetIds at the two internal call sites.
2026-07-06 12:36:01 -07:00
DrJKL
a505780953 perf: read widget order from the incremental index, not a full scan
`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.
2026-07-06 12:36:01 -07:00
DrJKL
ebb0f25231 fix: lead combo preview options with an explicitly empty value
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.
2026-07-06 12:36:01 -07:00
DrJKL
4fad4889fa refactor: drop duplicated name/type/value from WidgetGridItem
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 }`.
2026-07-06 12:36:01 -07:00
DrJKL
68255992ec refactor: drop usePreviewWidgets composable; simplify WidgetGrid
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.
2026-07-06 12:36:01 -07:00
DrJKL
b2c479479e fix: drop unused WidgetSlotMetadata export
Only ProcessedWidget needs to be public; it references WidgetSlotMetadata
internally, so exporting the latter tripped knip.
2026-07-06 12:36:01 -07:00
DrJKL
b49eac4753 refactor: render node preview from plain data, not the widget store
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.
2026-07-06 12:36:01 -07:00
DrJKL
4ddf08240d refactor: move preview widget store writes out of computed
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.
2026-07-06 12:34:28 -07:00
DrJKL
d831b1de54 fix: use uuid subgraph ids in nested promoted model asset
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.
2026-07-06 12:34:28 -07:00
DrJKL
4fb92e0bf6 fix: derive vue widget disabled state from live input link
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.
2026-07-06 12:34:28 -07:00