Skip to content

fix(layout): enforce per-instance geometry ownership - #14480

Closed
DrJKL wants to merge 64 commits into
feature/ecs-migrationfrom
drjkl/layout-crdt-safety
Closed

fix(layout): enforce per-instance geometry ownership#14480
DrJKL wants to merge 64 commits into
feature/ecs-migrationfrom
drjkl/layout-crdt-safety

Conversation

@DrJKL

@DrJKL DrJKL commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Summary

Protects layout geometry from stale LiteGraph instances: only the instance that registered a node/group/reroute layout may mutate or delete it, ownership transfers explicitly during node replacement, and every layout operation reports applied / no-op / rejected. Node position and size are stored as independent Yjs registers.

An earlier revision of this PR also shipped retry/compensation sagas, two-phase prepare/revalidate mutations, pending-registration reconciliation, and schema-migration fallbacks aimed at a multiplayer transport that does not exist yet. The layout store is a private, synchronous, in-memory Y.Doc with no provider, no remote writer, and no persistence — so that machinery defended unreachable states and has been removed (−877 net production lines, −849 net test lines vs. that revision). It should return together with the ingress path (provider/update application) when multiplayer actually ships.

Changes

  • What: Per-instance ownership tokens for node/group/reroute layout registrations, enforced in the layout store; stale instances (e.g. a detached graph still holding references) get no-op/rejected instead of clobbering live geometry.
  • What: Ownership token transfer during node replacement, duplicate-ID handling, and empty-token rejection at the operation boundary (undefined = legacy tokenless; non-empty string = owned; '' is invalid).
  • What: Graph lifecycle linearized as validate → fallible extension callbacks → irreversible teardown, so callback throws no longer require restoring already-destroyed layout state; root cleanup is store-owned.
  • What: Node position/size split into independent Yjs registers (no legacy rect fallback — the doc is never persisted, so no old documents exist).
  • What: Link-topology adoption now requires matching endpoints, not just a matching link ID — pasted subgraphs reuse internal link IDs within the root-graph bucket, and adopt-by-id hijacked live links, pruning pasted reroutes.
  • What: Layout notifications batch via withDeferredNotifications during multi-entity operations.

Review Focus

  • Ownership enforcement lives in the layout store, the single owner of the data; entity classes and mappers pass an opaque token through and make no ownership decisions.
  • Teardown ordering in LGraph — fallible callbacks run before any layout unregistration, which is what made compensation machinery unnecessary.
  • hasSameEndpoints guard in LLink.registerLinkTopology: adoption of persisted topology is now conditional on origin/target node+slot equality.
  • Tests are behavioral: stale-owner, replacement transfer, duplicate IDs, real reentrancy via beforeTransaction, and callback-failure rollback. Synthetic tests that patched Yjs internals to fabricate impossible failures were deleted along with the code they justified.

Test Plan

  • Focused layout/LiteGraph suites: layoutStore 57, LGraphGroup 46, Reroute.store 53, LGraph 95, clipboard/LLink 20, five canvas suites 42
  • pnpm test:unit --changed origin/feature/ecs-migration: 9,479 passed; the 5 persistent failures reproduce identically on the base branch (pre-existing, verified via detached checkout)
  • pnpm typecheck, pnpm knip, ESLint, Oxfmt, git diff --check
  • Browser specs (nodeReplacement.spec.ts, rerouteGeometry.spec.ts) via CI

Stack notes

Builds on #14133 (store-backed node geometry) and #14128 (attach/detach layout lifetime), both already in the feature/ecs-migration base. The public pos/size facade from #14133 is preserved; no registration token is exposed through the public LiteGraph geometry API.

@coderabbitai

coderabbitai Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 09237480-d00f-4a25-9e77-c021113866fd

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The PR introduces registration-aware layout storage for nodes, groups, and reroutes. It adds ownership validation, operation results, geometry notifications, rollback and compensation during graph lifecycle changes, subgraph configuration handling, and extensive test coverage.

Changes

Layout storage and operation contracts

Layer / File(s) Summary
Layout store and geometry contracts
src/renderer/core/layout/store/..., src/renderer/core/layout/types.ts, src/renderer/core/layout/utils/..., src/renderer/core/layout/operations/layoutMutations.ts
Layout entries now store registration IDs and separate position and size fields. Operations return applied, no-op, or rejected, validate ownership, support rollback, and clone exposed data.
Registration orchestration
src/renderer/core/layout/operations/graphLayoutRegistration.ts
Registration helpers now attach, adopt, detach, restore, and batch node, group, and reroute layouts across nested graphs.

Graph lifecycle integration

Layer / File(s) Summary
Graph and entity lifecycle
src/lib/litegraph/src/LGraph.ts, src/lib/litegraph/src/LGraphNode.ts, src/lib/litegraph/src/LGraphGroup.ts, src/lib/litegraph/src/Reroute.ts
Graph add, remove, clear, reroute, and configure flows now coordinate layout ownership and restore state after rejected or failed operations.
Lifecycle and ownership tests
src/lib/litegraph/src/*.test.ts, src/lib/litegraph/src/subgraph/__fixtures__/*
Tests cover registration conflicts, subgraph transfers, serialization, reentrant operations, foreign replacements, teardown compensation, and retry behavior.

Rendering and UI synchronization

Layer / File(s) Summary
Canvas and panel updates
src/renderer/core/canvas/litegraph/notifyLayoutChanges.ts, src/components/rightSidePanel/RightSidePanel.vue
Canvas operation and geometry listeners now have separate behavior and cleanup. Invalid panel tabs use deferred availability checks.
Supporting rendering tests
src/renderer/core/canvas/litegraph/notifyLayoutChanges.test.ts, src/lib/litegraph/src/LGraphCanvas.cloneZIndex.test.ts
Tests cover direct and batched geometry updates, raw Yjs changes, cleanup, and clone z-index behavior.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Graph as LGraph
  participant Registration as graphLayoutRegistration
  participant Store as layoutStore
  participant Canvas as LGraphCanvas
  Graph->>Registration: register or detach entity layouts
  Registration->>Store: apply ownership-aware operations
  Store-->>Registration: applied, no-op, or rejected
  Store->>Canvas: notify graph geometry changes
  Canvas->>Canvas: mark rendering dirty or resize affected nodes
Loading

Possibly related PRs

Suggested labels: size:XXL

Suggested reviewers: austinmroz, christian-byrne


Important

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
End-To-End Regression Coverage For Fixes ❓ Inconclusive The review context provides changed files and the PR description, but it does not provide the PR title or commit subjects required to detect bug-fix language. Provide the PR title and commit subjects so the bug-fix signal and end-to-end regression-test requirement can be assessed.
✅ Passed checks (5 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Adr Compliance For Entity/Litegraph Changes ✅ Passed Applicable paths changed, but metadata shows geometry routed through layout operations, result-based APIs, and an extracted config hook; no flagged ADR 0003/0008 patterns or extension callback chan...
Title check ✅ Passed The title clearly summarizes the main change: enforcing per-instance ownership of layout geometry.
Description check ✅ Passed The description covers the required summary, changes, review focus, and validation details with clear technical context.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch drjkl/layout-crdt-safety

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Jul 31, 2026

Copy link
Copy Markdown

🎨 Storybook: ✅ Built — View Storybook

Details

⏰ Completed at: 08/09/2026, 11:52:46 AM UTC

Links

🎭 Playwright: ✅ 1767 passed, 0 failed · 1 flaky

📊 Browser Reports
  • chromium: View Report (✅ 1748 / ❌ 0 / ⚠️ 0 / ⏭️ 5)
  • chromium-2x: View Report (✅ 2 / ❌ 0 / ⚠️ 0 / ⏭️ 0)
  • chromium-0.5x: View Report (✅ 1 / ❌ 0 / ⚠️ 0 / ⏭️ 0)
  • mobile-chrome: View Report (✅ 16 / ❌ 0 / ⚠️ 1 / ⏭️ 0)

📦 Bundle Size

⏳ Size data collection in progress…

⚡ Performance Report

canvas-idle: · 60.0 avg FPS · 59.5 P5 FPS ✅ (target: ≥52) · 0ms TBT · 70.9 MB heap
canvas-mouse-sweep: · 60.0 avg FPS · 59.9 P5 FPS ✅ (target: ≥52) · 0ms TBT · 57.2 MB heap
canvas-zoom-sweep: · 60.0 avg FPS · 59.9 P5 FPS ✅ (target: ≥52) · 0ms TBT · 48.7 MB heap
dom-widget-clipping: · 60.0 avg FPS · 59.7 P5 FPS ✅ (target: ≥52) · 0ms TBT · 57.3 MB heap
large-graph-idle: · 60.0 avg FPS · 59.9 P5 FPS ✅ (target: ≥52) · 0ms TBT · 63.7 MB heap
large-graph-pan: · 60.0 avg FPS · 59.9 P5 FPS ✅ (target: ≥52) · 0ms TBT · 72.5 MB heap
large-graph-zoom: · 60.0 avg FPS · 59.9 P5 FPS ✅ (target: ≥52) · 0ms TBT · 78.5 MB heap
legacy-node-drag: · 60.0 avg FPS · 59.9 P5 FPS ✅ (target: ≥52) · 0ms TBT · 67.0 MB heap
minimap-idle: · 60.0 avg FPS · 59.9 P5 FPS ✅ (target: ≥52) · 0ms TBT · 72.7 MB heap
subgraph-dom-widget-clipping: · 60.0 avg FPS · 59.7 P5 FPS ✅ (target: ≥52) · 0ms TBT · 57.2 MB heap
subgraph-idle: · 60.0 avg FPS · 59.9 P5 FPS ✅ (target: ≥52) · 0ms TBT · 58.8 MB heap
subgraph-mouse-sweep: · 60.0 avg FPS · 59.9 P5 FPS ✅ (target: ≥52) · 0ms TBT · 47.6 MB heap
subgraph-transition-enter: · 60.0 avg FPS · 59.5 P5 FPS ✅ (target: ≥52) · 131ms TBT · 92.1 MB heap
viewport-pan-sweep: · 60.0 avg FPS · 59.9 P5 FPS ✅ (target: ≥52) · 0ms TBT · 85.7 MB heap
vue-large-graph-idle: · 56.3 avg FPS · 59.7 P5 FPS ✅ (target: ≥52) · 0ms TBT · 157.5 MB heap
vue-large-graph-pan: · 55.4 avg FPS · 59.5 P5 FPS ✅ (target: ≥52) · 468ms TBT · 160.3 MB heap
workflow-execution: · 60.0 avg FPS · 59.7 P5 FPS ✅ (target: ≥52) · 0ms TBT · 50.2 MB heap

ℹ️ No baseline found — significance unavailable.

Absolute values
Metric Value
canvas-idle: avg frame time 17ms
canvas-idle: p95 frame time 17ms
canvas-idle: layout duration 0ms
canvas-idle: style recalc duration 10ms
canvas-idle: layout count 0
canvas-idle: style recalc count 10
canvas-idle: task duration 520ms
canvas-idle: script duration 27ms
canvas-idle: TBT 0ms
canvas-idle: heap used 70.9 MB
canvas-idle: DOM nodes 19
canvas-idle: event listeners 4
canvas-mouse-sweep: avg frame time 17ms
canvas-mouse-sweep: p95 frame time 17ms
canvas-mouse-sweep: layout duration 4ms
canvas-mouse-sweep: style recalc duration 45ms
canvas-mouse-sweep: layout count 12
canvas-mouse-sweep: style recalc count 78
canvas-mouse-sweep: task duration 1042ms
canvas-mouse-sweep: script duration 196ms
canvas-mouse-sweep: TBT 0ms
canvas-mouse-sweep: heap used 57.2 MB
canvas-mouse-sweep: DOM nodes -108
canvas-mouse-sweep: event listeners -73
canvas-zoom-sweep: avg frame time 17ms
canvas-zoom-sweep: p95 frame time 17ms
canvas-zoom-sweep: layout duration 1ms
canvas-zoom-sweep: style recalc duration 19ms
canvas-zoom-sweep: layout count 6
canvas-zoom-sweep: style recalc count 32
canvas-zoom-sweep: task duration 443ms
canvas-zoom-sweep: script duration 35ms
canvas-zoom-sweep: TBT 0ms
canvas-zoom-sweep: heap used 48.7 MB
canvas-zoom-sweep: DOM nodes 78
canvas-zoom-sweep: event listeners 19
dom-widget-clipping: avg frame time 17ms
dom-widget-clipping: p95 frame time 17ms
dom-widget-clipping: layout duration 0ms
dom-widget-clipping: style recalc duration 8ms
dom-widget-clipping: layout count 0
dom-widget-clipping: style recalc count 11
dom-widget-clipping: task duration 421ms
dom-widget-clipping: script duration 94ms
dom-widget-clipping: TBT 0ms
dom-widget-clipping: heap used 57.3 MB
dom-widget-clipping: DOM nodes 18
dom-widget-clipping: event listeners 2
large-graph-idle: avg frame time 17ms
large-graph-idle: p95 frame time 17ms
large-graph-idle: layout duration 0ms
large-graph-idle: style recalc duration 7ms
large-graph-idle: layout count 0
large-graph-idle: style recalc count 8
large-graph-idle: task duration 951ms
large-graph-idle: script duration 304ms
large-graph-idle: TBT 0ms
large-graph-idle: heap used 63.7 MB
large-graph-idle: DOM nodes -273
large-graph-idle: event listeners -148
large-graph-pan: avg frame time 17ms
large-graph-pan: p95 frame time 17ms
large-graph-pan: layout duration 0ms
large-graph-pan: style recalc duration 13ms
large-graph-pan: layout count 0
large-graph-pan: style recalc count 68
large-graph-pan: task duration 2087ms
large-graph-pan: script duration 1201ms
large-graph-pan: TBT 0ms
large-graph-pan: heap used 72.5 MB
large-graph-pan: DOM nodes -279
large-graph-pan: event listeners -146
large-graph-zoom: avg frame time 17ms
large-graph-zoom: p95 frame time 17ms
large-graph-zoom: layout duration 8ms
large-graph-zoom: style recalc duration 13ms
large-graph-zoom: layout count 60
large-graph-zoom: style recalc count 63
large-graph-zoom: task duration 2268ms
large-graph-zoom: script duration 1280ms
large-graph-zoom: TBT 0ms
large-graph-zoom: heap used 78.5 MB
large-graph-zoom: DOM nodes -137
large-graph-zoom: event listeners -68
legacy-node-drag: avg frame time 17ms
legacy-node-drag: p95 frame time 17ms
legacy-node-drag: layout duration 0ms
legacy-node-drag: style recalc duration 7ms
legacy-node-drag: layout count 0
legacy-node-drag: style recalc count 44
legacy-node-drag: task duration 2967ms
legacy-node-drag: script duration 698ms
legacy-node-drag: TBT 0ms
legacy-node-drag: heap used 67.0 MB
legacy-node-drag: DOM nodes -284
legacy-node-drag: event listeners -104
minimap-idle: avg frame time 17ms
minimap-idle: p95 frame time 17ms
minimap-idle: layout duration 0ms
minimap-idle: style recalc duration 6ms
minimap-idle: layout count 0
minimap-idle: style recalc count 6
minimap-idle: task duration 920ms
minimap-idle: script duration 294ms
minimap-idle: TBT 0ms
minimap-idle: heap used 72.7 MB
minimap-idle: DOM nodes -279
minimap-idle: event listeners -148
subgraph-dom-widget-clipping: avg frame time 17ms
subgraph-dom-widget-clipping: p95 frame time 17ms
subgraph-dom-widget-clipping: layout duration 0ms
subgraph-dom-widget-clipping: style recalc duration 11ms
subgraph-dom-widget-clipping: layout count 0
subgraph-dom-widget-clipping: style recalc count 47
subgraph-dom-widget-clipping: task duration 444ms
subgraph-dom-widget-clipping: script duration 150ms
subgraph-dom-widget-clipping: TBT 0ms
subgraph-dom-widget-clipping: heap used 57.2 MB
subgraph-dom-widget-clipping: DOM nodes 19
subgraph-dom-widget-clipping: event listeners 8
subgraph-idle: avg frame time 17ms
subgraph-idle: p95 frame time 17ms
subgraph-idle: layout duration 0ms
subgraph-idle: style recalc duration 10ms
subgraph-idle: layout count 0
subgraph-idle: style recalc count 10
subgraph-idle: task duration 527ms
subgraph-idle: script duration 21ms
subgraph-idle: TBT 0ms
subgraph-idle: heap used 58.8 MB
subgraph-idle: DOM nodes -126
subgraph-idle: event listeners -74
subgraph-mouse-sweep: avg frame time 17ms
subgraph-mouse-sweep: p95 frame time 17ms
subgraph-mouse-sweep: layout duration 5ms
subgraph-mouse-sweep: style recalc duration 39ms
subgraph-mouse-sweep: layout count 16
subgraph-mouse-sweep: style recalc count 76
subgraph-mouse-sweep: task duration 884ms
subgraph-mouse-sweep: script duration 124ms
subgraph-mouse-sweep: TBT 0ms
subgraph-mouse-sweep: heap used 47.6 MB
subgraph-mouse-sweep: DOM nodes -274
subgraph-mouse-sweep: event listeners -152
subgraph-transition-enter: avg frame time 17ms
subgraph-transition-enter: p95 frame time 17ms
subgraph-transition-enter: layout duration 13ms
subgraph-transition-enter: style recalc duration 31ms
subgraph-transition-enter: layout count 14
subgraph-transition-enter: style recalc count 18
subgraph-transition-enter: task duration 1024ms
subgraph-transition-enter: script duration 57ms
subgraph-transition-enter: TBT 131ms
subgraph-transition-enter: heap used 92.1 MB
subgraph-transition-enter: DOM nodes 13671
subgraph-transition-enter: event listeners 2377
viewport-pan-sweep: avg frame time 17ms
viewport-pan-sweep: p95 frame time 17ms
viewport-pan-sweep: layout duration 0ms
viewport-pan-sweep: style recalc duration 37ms
viewport-pan-sweep: layout count 0
viewport-pan-sweep: style recalc count 250
viewport-pan-sweep: task duration 7323ms
viewport-pan-sweep: script duration 4024ms
viewport-pan-sweep: TBT 0ms
viewport-pan-sweep: heap used 85.7 MB
viewport-pan-sweep: DOM nodes -275
viewport-pan-sweep: event listeners -126
vue-large-graph-idle: avg frame time 18ms
vue-large-graph-idle: p95 frame time 17ms
vue-large-graph-idle: layout duration 0ms
vue-large-graph-idle: style recalc duration 0ms
vue-large-graph-idle: layout count 0
vue-large-graph-idle: style recalc count 0
vue-large-graph-idle: task duration 17690ms
vue-large-graph-idle: script duration 1231ms
vue-large-graph-idle: TBT 0ms
vue-large-graph-idle: heap used 157.5 MB
vue-large-graph-idle: DOM nodes -6550
vue-large-graph-idle: event listeners -7470
vue-large-graph-pan: avg frame time 18ms
vue-large-graph-pan: p95 frame time 17ms
vue-large-graph-pan: layout duration 0ms
vue-large-graph-pan: style recalc duration 21ms
vue-large-graph-pan: layout count 0
vue-large-graph-pan: style recalc count 154
vue-large-graph-pan: task duration 21565ms
vue-large-graph-pan: script duration 1760ms
vue-large-graph-pan: TBT 468ms
vue-large-graph-pan: heap used 160.3 MB
vue-large-graph-pan: DOM nodes -6550
vue-large-graph-pan: event listeners -7464
workflow-execution: avg frame time 17ms
workflow-execution: p95 frame time 17ms
workflow-execution: layout duration 1ms
workflow-execution: style recalc duration 21ms
workflow-execution: layout count 4
workflow-execution: style recalc count 13
workflow-execution: task duration 133ms
workflow-execution: script duration 11ms
workflow-execution: TBT 0ms
workflow-execution: heap used 50.2 MB
workflow-execution: DOM nodes 125
workflow-execution: event listeners 65
Raw data
{
  "timestamp": "2026-08-09T12:03:53.501Z",
  "gitSha": "c998a58ccb13bcd778ac52325f93332694eabb36",
  "branch": "drjkl/layout-crdt-safety",
  "measurements": [
    {
      "name": "canvas-idle",
      "durationMs": 2011.8499999999813,
      "styleRecalcs": 9,
      "styleRecalcDurationMs": 10.442000000000002,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 521.572,
      "heapDeltaBytes": 4543876,
      "heapUsedBytes": 74447424,
      "domNodes": 18,
      "jsHeapTotalBytes": 19001344,
      "scriptDurationMs": 27.384999999999998,
      "eventListeners": 4,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.66999999999998,
      "p95FrameDurationMs": 16.799999999999272
    },
    {
      "name": "canvas-idle",
      "durationMs": 2046.0390000000075,
      "styleRecalcs": 10,
      "styleRecalcDurationMs": 10.445,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 517.734,
      "heapDeltaBytes": 4472080,
      "heapUsedBytes": 74262732,
      "domNodes": 20,
      "jsHeapTotalBytes": 19263488,
      "scriptDurationMs": 26.581,
      "eventListeners": 4,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.66333333333332,
      "p95FrameDurationMs": 16.800000000000182
    },
    {
      "name": "canvas-mouse-sweep",
      "durationMs": 2079.7430000000077,
      "styleRecalcs": 80,
      "styleRecalcDurationMs": 48.540000000000006,
      "layouts": 12,
      "layoutDurationMs": 4.131,
      "taskDurationMs": 1089.9950000000001,
      "heapDeltaBytes": 2147708,
      "heapUsedBytes": 72029128,
      "domNodes": 62,
      "jsHeapTotalBytes": 20312064,
      "scriptDurationMs": 202.53000000000003,
      "eventListeners": 6,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.699999999999818
    },
    {
      "name": "canvas-mouse-sweep",
      "durationMs": 1904.2409999999563,
      "styleRecalcs": 75,
      "styleRecalcDurationMs": 41.749,
      "layouts": 12,
      "layoutDurationMs": 3.8189999999999995,
      "taskDurationMs": 993.8609999999999,
      "heapDeltaBytes": -22083856,
      "heapUsedBytes": 47886360,
      "domNodes": -277,
      "jsHeapTotalBytes": 19390464,
      "scriptDurationMs": 189.61299999999997,
      "eventListeners": -152,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "canvas-zoom-sweep",
      "durationMs": 1756.0740000000123,
      "styleRecalcs": 33,
      "styleRecalcDurationMs": 19.988,
      "layouts": 6,
      "layoutDurationMs": 0.6510000000000001,
      "taskDurationMs": 429.229,
      "heapDeltaBytes": -18782540,
      "heapUsedBytes": 51043844,
      "domNodes": 79,
      "jsHeapTotalBytes": 18739200,
      "scriptDurationMs": 31.392000000000003,
      "eventListeners": 19,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "canvas-zoom-sweep",
      "durationMs": 1744.401000000039,
      "styleRecalcs": 31,
      "styleRecalcDurationMs": 17.770999999999994,
      "layouts": 6,
      "layoutDurationMs": 0.6539999999999998,
      "taskDurationMs": 455.878,
      "heapDeltaBytes": -18845124,
      "heapUsedBytes": 51066176,
      "domNodes": 76,
      "jsHeapTotalBytes": 18739200,
      "scriptDurationMs": 37.986,
      "eventListeners": 19,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.66333333333332,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "dom-widget-clipping",
      "durationMs": 624.0349999999921,
      "styleRecalcs": 11,
      "styleRecalcDurationMs": 7.418000000000001,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 413.811,
      "heapDeltaBytes": -9992772,
      "heapUsedBytes": 59988404,
      "domNodes": 18,
      "jsHeapTotalBytes": 20574208,
      "scriptDurationMs": 92.32399999999998,
      "eventListeners": 2,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.66333333333335,
      "p95FrameDurationMs": 16.799999999999272
    },
    {
      "name": "dom-widget-clipping",
      "durationMs": 628.1460000000152,
      "styleRecalcs": 11,
      "styleRecalcDurationMs": 7.854000000000002,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 429.145,
      "heapDeltaBytes": -9734944,
      "heapUsedBytes": 60247968,
      "domNodes": 18,
      "jsHeapTotalBytes": 19263488,
      "scriptDurationMs": 94.85700000000001,
      "eventListeners": 2,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.699999999999818
    },
    {
      "name": "large-graph-idle",
      "durationMs": 2069.2339999999945,
      "styleRecalcs": 9,
      "styleRecalcDurationMs": 8.7,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 929.1099999999999,
      "heapDeltaBytes": -27398592,
      "heapUsedBytes": 55010856,
      "domNodes": -271,
      "jsHeapTotalBytes": 2277376,
      "scriptDurationMs": 300.48900000000003,
      "eventListeners": -148,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.699999999999818
    },
    {
      "name": "large-graph-idle",
      "durationMs": 2041.3849999999911,
      "styleRecalcs": 7,
      "styleRecalcDurationMs": 6.034999999999998,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 972.049,
      "heapDeltaBytes": 17928924,
      "heapUsedBytes": 78612896,
      "domNodes": -274,
      "jsHeapTotalBytes": -987136,
      "scriptDurationMs": 306.556,
      "eventListeners": -148,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "large-graph-pan",
      "durationMs": 3014.1679999999837,
      "styleRecalcs": 67,
      "styleRecalcDurationMs": 11.919999999999996,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 2043.0410000000002,
      "heapDeltaBytes": 23665436,
      "heapUsedBytes": 85613080,
      "domNodes": -279,
      "jsHeapTotalBytes": 528384,
      "scriptDurationMs": 1168.943,
      "eventListeners": -146,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.66333333333335,
      "p95FrameDurationMs": 16.699999999999818
    },
    {
      "name": "large-graph-pan",
      "durationMs": 3188.3209999999735,
      "styleRecalcs": 68,
      "styleRecalcDurationMs": 13.212000000000002,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 2130.402,
      "heapDeltaBytes": 4412744,
      "heapUsedBytes": 66447816,
      "domNodes": -278,
      "jsHeapTotalBytes": 1052672,
      "scriptDurationMs": 1233.8120000000001,
      "eventListeners": -146,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "large-graph-zoom",
      "durationMs": 4187.2950000000055,
      "styleRecalcs": 62,
      "styleRecalcDurationMs": 11.382,
      "layouts": 60,
      "layoutDurationMs": 7.526000000000001,
      "taskDurationMs": 2304.6530000000002,
      "heapDeltaBytes": 12633936,
      "heapUsedBytes": 78242728,
      "domNodes": -284,
      "jsHeapTotalBytes": 3207168,
      "scriptDurationMs": 1287.4460000000001,
      "eventListeners": -148,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "large-graph-zoom",
      "durationMs": 4177.065999999968,
      "styleRecalcs": 64,
      "styleRecalcDurationMs": 13.689,
      "layouts": 60,
      "layoutDurationMs": 7.8069999999999995,
      "taskDurationMs": 2231.678,
      "heapDeltaBytes": 22428772,
      "heapUsedBytes": 86475424,
      "domNodes": 10,
      "jsHeapTotalBytes": 4194304,
      "scriptDurationMs": 1271.744,
      "eventListeners": 12,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "legacy-node-drag",
      "durationMs": 4164.017000000002,
      "styleRecalcs": 44,
      "styleRecalcDurationMs": 7.483999999999997,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 2899.839,
      "heapDeltaBytes": 6710004,
      "heapUsedBytes": 69938124,
      "domNodes": -282,
      "jsHeapTotalBytes": 9641984,
      "scriptDurationMs": 689.661,
      "eventListeners": -103,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "legacy-node-drag",
      "durationMs": 4200.832999999989,
      "styleRecalcs": 44,
      "styleRecalcDurationMs": 6.857999999999996,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 3033.418,
      "heapDeltaBytes": 7647536,
      "heapUsedBytes": 70531756,
      "domNodes": -285,
      "jsHeapTotalBytes": 10690560,
      "scriptDurationMs": 706.6320000000001,
      "eventListeners": -104,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "minimap-idle",
      "durationMs": 2046.0310000000277,
      "styleRecalcs": 6,
      "styleRecalcDurationMs": 5.4079999999999995,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 902.772,
      "heapDeltaBytes": 18358588,
      "heapUsedBytes": 80531140,
      "domNodes": -278,
      "jsHeapTotalBytes": -462848,
      "scriptDurationMs": 288.93100000000004,
      "eventListeners": -148,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "minimap-idle",
      "durationMs": 2042.4270000000888,
      "styleRecalcs": 6,
      "styleRecalcDurationMs": 6.242000000000001,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 936.51,
      "heapDeltaBytes": 9633048,
      "heapUsedBytes": 71952376,
      "domNodes": -279,
      "jsHeapTotalBytes": -200704,
      "scriptDurationMs": 298.481,
      "eventListeners": -148,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.699999999999818
    },
    {
      "name": "subgraph-dom-widget-clipping",
      "durationMs": 649.1479999999683,
      "styleRecalcs": 47,
      "styleRecalcDurationMs": 11.148,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 442.87999999999994,
      "heapDeltaBytes": -9780132,
      "heapUsedBytes": 60225224,
      "domNodes": 20,
      "jsHeapTotalBytes": 19525632,
      "scriptDurationMs": 146.92200000000003,
      "eventListeners": 8,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.699999999999818
    },
    {
      "name": "subgraph-dom-widget-clipping",
      "durationMs": 669.1260000000057,
      "styleRecalcs": 46,
      "styleRecalcDurationMs": 10.581999999999999,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 444.5559999999999,
      "heapDeltaBytes": -10121720,
      "heapUsedBytes": 59830496,
      "domNodes": 18,
      "jsHeapTotalBytes": 19001344,
      "scriptDurationMs": 153.81,
      "eventListeners": 8,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.800000000000182
    },
    {
      "name": "subgraph-idle",
      "durationMs": 2040.5790000000366,
      "styleRecalcs": 10,
      "styleRecalcDurationMs": 9.937999999999999,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 499.006,
      "heapDeltaBytes": 3835372,
      "heapUsedBytes": 73819204,
      "domNodes": 20,
      "jsHeapTotalBytes": 19001344,
      "scriptDurationMs": 20.34,
      "eventListeners": 4,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.66333333333335,
      "p95FrameDurationMs": 16.699999999999818
    },
    {
      "name": "subgraph-idle",
      "durationMs": 2004.9840000000358,
      "styleRecalcs": 10,
      "styleRecalcDurationMs": 10.795,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 554.008,
      "heapDeltaBytes": -20517244,
      "heapUsedBytes": 49505676,
      "domNodes": -272,
      "jsHeapTotalBytes": 18341888,
      "scriptDurationMs": 20.873,
      "eventListeners": -152,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.66333333333332,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "subgraph-mouse-sweep",
      "durationMs": 1741.0390000000007,
      "styleRecalcs": 76,
      "styleRecalcDurationMs": 39.797,
      "layouts": 16,
      "layoutDurationMs": 5.235,
      "taskDurationMs": 892.1640000000001,
      "heapDeltaBytes": -20689664,
      "heapUsedBytes": 49337828,
      "domNodes": -273,
      "jsHeapTotalBytes": 18866176,
      "scriptDurationMs": 126.284,
      "eventListeners": -152,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.670000000000012,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "subgraph-mouse-sweep",
      "durationMs": 1721.8619999999873,
      "styleRecalcs": 75,
      "styleRecalcDurationMs": 39.046,
      "layouts": 16,
      "layoutDurationMs": 4.917000000000001,
      "taskDurationMs": 875.146,
      "heapDeltaBytes": -19637200,
      "heapUsedBytes": 50402204,
      "domNodes": -275,
      "jsHeapTotalBytes": 18604032,
      "scriptDurationMs": 122.20800000000001,
      "eventListeners": -152,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "subgraph-transition-enter",
      "durationMs": 1402.5869999999827,
      "styleRecalcs": 18,
      "styleRecalcDurationMs": 31.125,
      "layouts": 14,
      "layoutDurationMs": 12.544,
      "taskDurationMs": 1023.6160000000001,
      "heapDeltaBytes": 4587220,
      "heapUsedBytes": 96547048,
      "domNodes": 13671,
      "jsHeapTotalBytes": 10485760,
      "scriptDurationMs": 56.641999999999996,
      "eventListeners": 2377,
      "totalBlockingTimeMs": 131,
      "frameDurationMs": 16.66333333333338,
      "p95FrameDurationMs": 16.799999999999272
    },
    {
      "name": "viewport-pan-sweep",
      "durationMs": 10617.895999999973,
      "styleRecalcs": 250,
      "styleRecalcDurationMs": 38.195,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 7283.721,
      "heapDeltaBytes": 26895900,
      "heapUsedBytes": 87559516,
      "domNodes": -275,
      "jsHeapTotalBytes": 499712,
      "scriptDurationMs": 3997.198,
      "eventListeners": -126,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "viewport-pan-sweep",
      "durationMs": 10845.394000000055,
      "styleRecalcs": 249,
      "styleRecalcDurationMs": 36.25599999999999,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 7362.995,
      "heapDeltaBytes": 9678040,
      "heapUsedBytes": 92259668,
      "domNodes": -274,
      "jsHeapTotalBytes": 4550656,
      "scriptDurationMs": 4050.8889999999997,
      "eventListeners": -126,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "vue-large-graph-idle",
      "durationMs": 17521.634000000005,
      "styleRecalcs": 0,
      "styleRecalcDurationMs": 0,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 17500.879999999997,
      "heapDeltaBytes": -42380184,
      "heapUsedBytes": 165435704,
      "domNodes": -6548,
      "jsHeapTotalBytes": 5828608,
      "scriptDurationMs": 1162.3709999999999,
      "eventListeners": -7471,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 17.776666666666763,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "vue-large-graph-idle",
      "durationMs": 17899.871000000076,
      "styleRecalcs": 0,
      "styleRecalcDurationMs": 0,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 17879.426000000003,
      "heapDeltaBytes": -24189324,
      "heapUsedBytes": 164801184,
      "domNodes": -6552,
      "jsHeapTotalBytes": 323584,
      "scriptDurationMs": 1299.564,
      "eventListeners": -7469,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 17.776666666666642,
      "p95FrameDurationMs": 16.799999999999272
    },
    {
      "name": "vue-large-graph-pan",
      "durationMs": 21429.883000000016,
      "styleRecalcs": 152,
      "styleRecalcDurationMs": 20.71500000000004,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 21405.408000000003,
      "heapDeltaBytes": -36375060,
      "heapUsedBytes": 168908128,
      "domNodes": -6552,
      "jsHeapTotalBytes": 1372160,
      "scriptDurationMs": 1707.699,
      "eventListeners": -7465,
      "totalBlockingTimeMs": 430,
      "frameDurationMs": 18.333333333333332,
      "p95FrameDurationMs": 16.80000000000291
    },
    {
      "name": "vue-large-graph-pan",
      "durationMs": 21748.080999999955,
      "styleRecalcs": 156,
      "styleRecalcDurationMs": 21.780999999999995,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 21724.951,
      "heapDeltaBytes": -21329368,
      "heapUsedBytes": 167330948,
      "domNodes": -6548,
      "jsHeapTotalBytes": 1372160,
      "scriptDurationMs": 1812.0529999999999,
      "eventListeners": -7463,
      "totalBlockingTimeMs": 505,
      "frameDurationMs": 17.776666666666642,
      "p95FrameDurationMs": 16.799999999999272
    },
    {
      "name": "workflow-execution",
      "durationMs": 453.9310000000114,
      "styleRecalcs": 13,
      "styleRecalcDurationMs": 19.871,
      "layouts": 3,
      "layoutDurationMs": 0.6519999999999998,
      "taskDurationMs": 134.01100000000002,
      "heapDeltaBytes": -15739292,
      "heapUsedBytes": 52827652,
      "domNodes": 130,
      "jsHeapTotalBytes": 6418432,
      "scriptDurationMs": 11.622,
      "eventListeners": 65,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "workflow-execution",
      "durationMs": 455.1370000000361,
      "styleRecalcs": 13,
      "styleRecalcDurationMs": 23.113,
      "layouts": 4,
      "layoutDurationMs": 1.3579999999999999,
      "taskDurationMs": 131.45800000000003,
      "heapDeltaBytes": -15908924,
      "heapUsedBytes": 52496672,
      "domNodes": 119,
      "jsHeapTotalBytes": 6418432,
      "scriptDurationMs": 11.151,
      "eventListeners": 65,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.800000000000182
    }
  ]
}

@DrJKL DrJKL self-assigned this Jul 31, 2026
@DrJKL
DrJKL force-pushed the drjkl/node-geometry-facade branch from 2e3020c to 6946098 Compare July 31, 2026 18:16
@DrJKL
DrJKL force-pushed the drjkl/layout-crdt-safety branch from 5e85c8f to 48073f7 Compare July 31, 2026 18:16
@DrJKL DrJKL added the refactor label Jul 31, 2026
@DrJKL
DrJKL force-pushed the drjkl/node-geometry-facade branch from 6946098 to 513d33f Compare July 31, 2026 21:00
@DrJKL
DrJKL force-pushed the drjkl/layout-crdt-safety branch from f3e0c79 to 885b8db Compare July 31, 2026 21:01
@DrJKL
DrJKL force-pushed the drjkl/node-geometry-facade branch from 513d33f to d52bd10 Compare August 1, 2026 00:45
@DrJKL
DrJKL force-pushed the drjkl/layout-crdt-safety branch from 885b8db to ef7dae4 Compare August 1, 2026 00:54
@DrJKL
DrJKL force-pushed the drjkl/node-geometry-facade branch from d52bd10 to b6c1c5c Compare August 4, 2026 17:19
@DrJKL
DrJKL force-pushed the drjkl/layout-crdt-safety branch from ef7dae4 to 0c4bb12 Compare August 4, 2026 17:20
@codecov

codecov Bot commented Aug 4, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 97.39027% with 22 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
...er/core/layout/operations/graphLayoutAttachment.ts 97.34% 7 Missing ⚠️
src/platform/nodeReplacement/useNodeReplacement.ts 87.87% 2 Missing and 2 partials ⚠️
src/renderer/core/layout/store/layoutStore.ts 98.62% 4 Missing ⚠️
src/components/builder/AppModeWidgetList.vue 0.00% 3 Missing ⚠️
...enderer/core/layout/store/__mocks__/layoutStore.ts 50.00% 2 Missing ⚠️
...rc/lib/litegraph/src/subgraph/SubgraphInputNode.ts 0.00% 1 Missing ⚠️
...renderer/core/layout/store/layoutStoreTestUtils.ts 75.00% 1 Missing ⚠️
@@                   Coverage Diff                    @@
##             feature/ecs-migration   #14480   +/-   ##
========================================================
  Coverage                         ?   70.01%           
========================================================
  Files                            ?     1681           
  Lines                            ?    86124           
  Branches                         ?    24702           
========================================================
  Hits                             ?    60301           
  Misses                           ?    25713           
  Partials                         ?      110           
Flag Coverage Δ
unit 70.01% <97.39%> (?)

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
...omponents/rightSidePanel/parameters/WidgetItem.vue 80.55% <100.00%> (ø)
...e/graph/subgraph/migration/proxyWidgetMigration.ts 90.09% <100.00%> (ø)
src/core/graph/widgets/dynamicWidgets.ts 88.46% <ø> (ø)
src/extensions/core/widgetValuePropagation.ts 78.12% <ø> (ø)
src/lib/litegraph/src/node/slotLinks.ts 93.90% <100.00%> (ø)
src/lib/litegraph/src/subgraph/SubgraphInput.ts 72.72% <100.00%> (ø)
src/lib/litegraph/src/subgraph/SubgraphOutput.ts 85.41% <100.00%> (ø)
src/lib/litegraph/src/subgraph/SubgraphSlotBase.ts 85.71% <100.00%> (ø)
...graph/src/subgraph/__fixtures__/subgraphHelpers.ts 97.34% <100.00%> (ø)
...ib/litegraph/src/subgraph/subgraphDeduplication.ts 98.70% <100.00%> (ø)
... and 22 more
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@DrJKL
DrJKL force-pushed the drjkl/layout-crdt-safety branch from 0c4bb12 to 65ffdc3 Compare August 4, 2026 18:14
@DrJKL
DrJKL force-pushed the drjkl/node-geometry-facade branch 2 times, most recently from 2f38faa to eb06b67 Compare August 5, 2026 04:05
@DrJKL
DrJKL force-pushed the drjkl/layout-crdt-safety branch from 65ffdc3 to b7ca00a Compare August 5, 2026 04:05
@DrJKL
DrJKL force-pushed the drjkl/node-geometry-facade branch from eb06b67 to 1d5cb40 Compare August 5, 2026 20:56
@DrJKL
DrJKL force-pushed the drjkl/layout-crdt-safety branch from c95b8eb to 65f0318 Compare August 5, 2026 20:56
@DrJKL
DrJKL force-pushed the drjkl/layout-crdt-safety branch 2 times, most recently from 329456c to b481407 Compare August 6, 2026 22:29
DrJKL added a commit that referenced this pull request Aug 6, 2026
…14133)

## Summary

Make `layoutStore` authoritative for node geometry and remove the Layout
→ LiteGraph writeback loop. `LGraphNode.pos` / `size` remain stable
compatibility views backed by a lazily refreshed `Rectangle` projection.

## Why a projection

At 1000 nodes, a `Rectangle` read measured ~6 ns versus ~63.5 ns for
`ynodes.get(id)` → rect. The legacy canvas reads node geometry ~44 times
per frame, making direct Yjs reads cost roughly 2.5 ms/frame. A
geometry-version check keeps the common path local and refreshes each
accessed projection once after invalidation.

## Changes

- Project `pos` / `size` from the store while preserving indexed
mutation compatibility and command-based writes.
- Register and unregister projection lifetime with node layout lifetime.
- Delete `useLayoutSync`, including its RAF/microtask batching and
writeback machinery.
- Observe layout changes through a Vue-scoped effect: invalidate the
canvas for node changes and forward actual non-Canvas size changes to
legacy `onResize` callbacks. Canvas resizes retain synchronous callback
ownership through `setSize()`.
- Share the stored `[x, y, width, height]` tuple type between Yjs
mappers and validation.

## Review Focus

- The projection stays inert until the node's own layout entry exists,
preventing accidental adoption of geometry from an ID collision during
attach.
- Two `test.fails` cases pin known whole-value setter cache gaps in this
PR alone. Child PR [#14480](../pull/14480) fixes both; rebasing it
should produce unexpected passes, after which the `.fails` markers must
be removed.
- Projection lifecycle extraction and node-scoped invalidation are
deferred until after #14480 and documented in the ECS migration plan.

## Tests

- Stable-view refresh and indexed mutation behavior
- Removal/re-addition and graph-scoped layout state
- Exact-once Canvas resize callbacks and store-originated resize
callbacks
- Layout notification graph filtering and cleanup

---------

Co-authored-by: Amp <amp@ampcode.com>
Co-authored-by: GitHub Action <action@github.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Base automatically changed from drjkl/node-geometry-facade to feature/ecs-migration August 6, 2026 23:35
DrJKL added 3 commits August 6, 2026 16:37
Preserve concurrent node geometry updates and project remote CRDT
updates through the store instead of overwriting local state.
Make layout command outcomes explicit and guard ownership teardown so
a rejected claim cannot tear down layout it does not own.
Preserve layout state when a teardown claim is rejected instead of
applying a partial removal.
DrJKL and others added 18 commits August 8, 2026 16:46
Introduce branded RootGraphId/OwningGraphId and GraphScope in a
dependency-free leaf module and thread (root, owner) scope through the
link and reroute store APIs and all call sites. Storage stays keyed by
root graph only; the owning graph is retained but not yet load-bearing,
in preparation for owner-scoped topology buckets.

- NodeSlots resolves the owner from the rendered node's graphId, not
  the active canvas graph, so previews and off-screen graphs cannot
  read another graph's same-ID slots once storage is owner-keyed
- LLink retains a single _graphScope registration identity (legacy
  _graphId removed)
- Private store maps are keyed by RootGraphId so the root-only storage
  invariant is compiler-enforced
- Tests query each entity through the scope of the graph that directly
  contains it, and capture scopes before clear()/configure() so
  teardown assertions survive graph id reassignment

Amp-Thread-ID: https://ampcode.com/threads/T-019fe0df-168d-72fb-9ce6-b64cd5911165
Co-authored-by: Amp <amp@ampcode.com>
Partition link and reroute topology stores into per-owner buckets
(Map<RootGraphId, Map<OwningGraphId, bucket>>) so sibling subgraph
definitions can hold identical link IDs and endpoints without
collision. getLink is now O(1) via an owner-local byId index.

- add owner-local clearOwner alongside root-wide clearGraph
- prune empty owner buckets on deletion; reads never create state
- advance the shared link ID counter when floating links are minted
  or restored, and remint explicit floating IDs that collide with
  live or floating links (legacy workflows may contain duplicates)
- derive reroute membership caches per owner bucket
- remove expected-failure markers from the subgraph link identity
  repros, which now pass

Amp-Thread-ID: https://ampcode.com/threads/T-019fe0df-168d-72fb-9ce6-b64cd5911165
Co-authored-by: Amp <amp@ampcode.com>
…lisions

configure now clears the configured graph's owner-scoped link/reroute
topology before materializing the payload, making serialized data
authoritative:

- Remove collision-based adoption from registerLinkTopology and
  registerRerouteChain; delete hasSameEndpoints. Serialized parentId
  (reroute chains) can no longer be silently discarded in favor of
  stale persisted topology.
- keep_old retains graph identity, counters, and node semantics but
  rebuilds all topology from the payload: old registrations are
  detached, owner store buckets cleared, graph link/reroute/floating
  maps reset, and attached canvases cleared to drop stale selections.
- Reserve reroute IDs only for root-partition owners that are NOT
  being rebuilt (retained sibling definitions), so incoming
  definitions deduplicate against them while the configured owner's
  own IDs are free to be reused.
- _addLink is first-wins on duplicate serialized link IDs, matching
  the store policy and preventing graph-map/store desync.
- _addReroute's remaining flag is layout-only (adoptLayout).

The keep_old reroute-chain reload repro (LLink.adoptionIdentity)
now passes and the file pins rebuild semantics: stale-topology
removal, sibling-owner isolation, and deterministic repeated
configure.

Amp-Thread-ID: https://ampcode.com/threads/T-019fe0df-168d-72fb-9ce6-b64cd5911165
Co-authored-by: Amp <amp@ampcode.com>
Adds a behavioral test freezing first-wins restore semantics: a stale detach restore yields to an intervening entry at the same key.

Amp-Thread-ID: https://ampcode.com/threads/T-019fe0df-168d-72fb-9ce6-b64cd5911165
Co-authored-by: Amp <amp@ampcode.com>
DrJKL added a commit that referenced this pull request Aug 10, 2026
Split 1/6 of #14480 (see that PR for the full map).

Collapses the copy-pasted
`lastLinkId`/`lastNodeId`/`lastGroupId`/`lastRerouteId`
increment-and-sync idioms across `LGraph`, `LGraphNode`, subgraph slots,
and `subgraphDeduplication` into `mint*`/`observe*` helpers in
`src/lib/litegraph/src/idAllocation.ts`. No behavior change.

Notes for review:
- Module lives in `src/lib/litegraph/src/` (litegraph-only behavior),
not `src/types/` as in #14480.
- `snapshotIdState`/`restoreIdState` from #14480 are omitted — no
callers until the layout rollback PR; they land there.
- `LGraphState` is re-exported from `LGraph.ts` so existing importers
are unaffected.
@DrJKL

DrJKL commented Aug 10, 2026

Copy link
Copy Markdown
Contributor Author

Superseded — split into reviewable PRs after the net diff was mapped and pruned:

Branch kept for reference.

@DrJKL DrJKL closed this Aug 10, 2026
DrJKL added a commit that referenced this pull request Aug 13, 2026
Split 2/6 of #14480. Stacked on #15001.

Fixes cross-subgraph-definition topology contamination:
`linkStore`/`rerouteStore` buckets were keyed only by root graph id, but
every subgraph definition numbers links from 1 and shares the IO
pseudo-node ids (−10/−20). Two definitions under one root therefore
shared an origin index and reroute membership — one definition's links
joined another definition's reroute chains, driving wrong reroute
pruning on configure. Repro:
`browser_tests/tests/subgraph/subgraphLinkIdentity.spec.ts` + fixture.

Changes:
- Adds `GraphScope` (`rootGraphId`, `owningGraphId`) and scopes topology
queries, owner-local iteration, and teardown to the graph that directly
owns each entity. Root-wide identity lookup remains keyed by
`rootGraphId`.
- Makes `linkStore`, `rerouteStore`, and `nodeDataStore` the
authoritative collision boundaries. Registrations are first-wins;
rejected entities remain detached and are not assigned to the requesting
owner.
- Shares link and reroute ID allocation across a root and its subgraphs.
Serialized subgraphs are deduplicated before registration so link,
reroute, node, widget, and promoted-widget references are remapped
together.
- Keeps endpoint swaps, rotations, and removals atomic within
`linkStore`: an entire batch is validated before indexes or topology
fields change. `LLink` retains the store-held reactive topology object
across ID and endpoint updates.
- Treats configure payloads as authoritative for their owner by clearing
owner-local registrations before rebuilding them, with empty root-bucket
pruning.
- Derives topology scope from the directly owning graph at callers such
as `NodeSlots.vue` and `useProcessedWidgets.ts`.
- Positions promoted DOM widgets immediately on mount instead of waiting
for a watched value to change.
- Removes redundant graph-local collision checks, floating-link ID
bookkeeping, unreachable tuple deduplication, obsolete compatibility
types, and mock-only tests.
- Documents root-vs-owner query guidance, topology transaction
boundaries, and the deferred measurement of root-wide group scans before
introducing another persistent index.

Behavior notes:
- Constructing or configuring an `LGraph` from serialized data requires
an active Pinia because entities register with topology stores during
deserialization.
- Runtime duplicate IDs are rejected by the root-scoped store; persisted
collisions are remapped before object registration.
- Registration failures remain explicit return values and do not throw
errors.

---------

Co-authored-by: Amp <amp@ampcode.com>
Co-authored-by: github-actions <github-actions@github.com>
DrJKL added a commit that referenced this pull request Aug 15, 2026
Split 3/6 of #14480. Stacked on #15009.

The original version replaced every rewired link with a fresh ID. #15009
now addresses that root cause more simply: endpoint changes are
validated and applied atomically at the link-store mutation boundary
while preserving link identity. This PR has therefore been reduced to
the remaining independent cleanup.

`LGraph._unpackSubgraphImpl()` previously rewrote the registered links
in the shared subgraph definition while calculating their parent-graph
endpoints. Unpacking one instance could transiently reindex or partially
mutate topology still used by another instance.

This change computes mapped origin/target endpoints in local variables,
leaving the source definition and its store registration untouched. The
behavioral test keeps another instance of the definition alive, unpacks
one instance, and verifies both the original topology and copied parent
link.

Deliberately omitted:
- no `linkReplacement.ts`
- no fresh IDs for endpoint changes
- no immutable/warn-and-ignore endpoint setters
- no second topology mutation boundary outside `linkStore`

Test plan:
- `pnpm test:unit
src/lib/litegraph/src/subgraph/SubgraphConversion.test.ts`
- typecheck, changed-file lint/format, and diff checks

---------

Co-authored-by: Amp <amp@ampcode.com>
DrJKL added a commit that referenced this pull request Aug 15, 2026
Split 5/6 of #14480. Stacked on #15017.

Node replacement bypasses `graph.add()`. Copying `pos` and `size`
preserved legacy canvas geometry only; the replacement did not own the
existing layout attachment and could lose its position when Vue Nodes
read from the layout store.

This change centralizes replacement ownership transfer in the existing
node-replacement subsystem:
- Preflights graph indices, node-state store ownership, same-ID
identity, and layout attachment before mutation.
- Runs the existing `onRemoved` callback while ownership is unchanged,
then revalidates before committing.
- Moves the existing registered node-state proxy and layout attachment
to the replacement without delete/recreate windows.
- Reads canonical geometry before stamping the replacement’s geometry
version.
- Detaches the old instance from graph, state, and layout ownership so
retained stale references cannot mutate the replacement.
- Keeps orchestration private to `useNodeReplacement`; no new public
`LGraph` mutation API.

The browser regression performs replacement, drags the node on the
legacy canvas, enables Vue Nodes, and verifies the rendered position
remains within 2px. Unit coverage verifies exact state transfer, graph
identity preservation, stale-instance isolation, mismatched ownership
rejection, and latest-geometry adoption.

Test plan:
- 178 focused node replacement, node store, LGraph, and LGraphNode tests
- application and browser typechecks
- changed-file lint/format and diff checks

---------

Co-authored-by: Amp <amp@ampcode.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants