fix: suppress link rendering during slot sync after graph reconfigure (#8367)

## Description

Fixes link alignment issues after undo/redo operations in Vue Nodes 2.0.
When multiple connections exist from the same output to different nodes,
performing an undo would cause the connections to become misaligned with
their inputs.

## Root Cause

When undo triggers `loadGraphData`, the graph is reconfigured and Vue
node components are destroyed and recreated. The new slot elements mount
and schedule RAF-batched position syncs via `scheduleSlotLayoutSync`.
However, links are drawn **before** the RAF batch completes, causing
`getSlotPosition()` to return stale/missing positions.

## Solution

- Export a new `flushPendingSlotLayoutSyncs()` function from
`useSlotElementTracking.ts`
- Create a `useGraphConfigureSlotSync` composable that flushes pending
syncs after graph configuration
- Integrate the flush into `addAfterConfigureHandler` in `app.ts`,
called after `onAfterGraphConfigured`
- Force canvas redraw after flushing to render links with correct
positions

## Testing

- Added unit tests for `flushPendingSlotLayoutSyncs`
- Added unit tests for `useGraphConfigureSlotSync` composable
- Manual verification: connections now align correctly after undo/redo
operations

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8367-fix-flush-pending-slot-layout-syncs-after-graph-configure-2f66d73d365081a3a564fac96c44a048)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Subagent 5 <subagent@example.com>
Co-authored-by: Amp <amp@ampcode.com>
This commit is contained in:
Christian Byrne
2026-02-03 15:56:31 -08:00
committed by GitHub
parent 278d491030
commit f98232c272
4 changed files with 66 additions and 9 deletions

View File

@@ -5639,6 +5639,12 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
this.renderedPaths.clear()
if (this.links_render_mode === LinkRenderType.HIDDEN_LINK) return
// Skip link rendering while waiting for slot positions to sync after reconfigure
if (LiteGraph.vueNodesMode && layoutStore.pendingSlotSync) {
this.#visibleReroutes.clear()
return
}
const { graph, subgraph } = this
if (!graph) throw new NullGraphError()

View File

@@ -141,6 +141,20 @@ class LayoutStoreImpl implements LayoutStore {
// Vue resizing state to prevent drag from activating during resize
public isResizingVueNodes = ref(false)
/**
* Flag indicating slot positions are pending sync after graph reconfiguration.
* When true, link rendering should be skipped to avoid drawing with stale positions.
*/
private _pendingSlotSync = false
get pendingSlotSync(): boolean {
return this._pendingSlotSync
}
setPendingSlotSync(value: boolean): void {
this._pendingSlotSync = value
}
constructor() {
// Initialize Yjs data structures
this.ynodes = this.ydoc.getMap('nodes')

View File

@@ -12,6 +12,7 @@ import { useSharedCanvasPositionConversion } from '@/composables/element/useCanv
import { LiteGraph } from '@/lib/litegraph/src/litegraph'
import { getSlotKey } from '@/renderer/core/layout/slots/slotIdentifier'
import { layoutStore } from '@/renderer/core/layout/store/layoutStore'
import { app } from '@/scripts/app'
import type { SlotLayout } from '@/renderer/core/layout/types'
import {
isPointEqual,
@@ -28,16 +29,36 @@ const raf = createRafBatch(() => {
function scheduleSlotLayoutSync(nodeId: string) {
pendingNodes.add(nodeId)
// Re-assert pending flag for late mounts (Vue components mounting after
// flushScheduledSlotLayoutSync was called synchronously in onConfigure)
layoutStore.setPendingSlotSync(true)
raf.schedule()
}
function flushScheduledSlotLayoutSync() {
if (pendingNodes.size === 0) return
export function flushScheduledSlotLayoutSync() {
if (pendingNodes.size === 0) {
// No pending nodes - check if we should wait for Vue components to mount
const graph = app.canvas?.graph
const hasNodes = graph && graph._nodes && graph._nodes.length > 0
if (hasNodes) {
// Graph has nodes but Vue hasn't mounted them yet - keep flag set
// so late mounts can re-assert it via scheduleSlotLayoutSync()
return
}
// No nodes in graph - safe to clear the flag (no Vue components will mount)
layoutStore.setPendingSlotSync(false)
app.canvas?.setDirty(true, true)
return
}
const conv = useSharedCanvasPositionConversion()
for (const nodeId of Array.from(pendingNodes)) {
pendingNodes.delete(nodeId)
syncNodeSlotLayoutsFromDOM(nodeId, conv)
}
// Clear the pending sync flag - slots are now synced
layoutStore.setPendingSlotSync(false)
// Trigger canvas redraw now that links can render with correct positions
app.canvas?.setDirty(true, true)
}
export function syncNodeSlotLayoutsFromDOM(

View File

@@ -5,6 +5,8 @@ import { reactive, unref } from 'vue'
import { shallowRef } from 'vue'
import { useCanvasPositionConversion } from '@/composables/element/useCanvasPositionConversion'
import { layoutStore } from '@/renderer/core/layout/store/layoutStore'
import { flushScheduledSlotLayoutSync } from '@/renderer/extensions/vueNodes/composables/useSlotElementTracking'
import { registerProxyWidgets } from '@/core/graph/subgraph/proxyWidget'
import { st, t } from '@/i18n'
import type { IContextMenuValue } from '@/lib/litegraph/src/interfaces'
@@ -734,17 +736,31 @@ export class ComfyApp {
private addAfterConfigureHandler(graph: LGraph) {
const { onConfigure } = graph
graph.onConfigure = function (...args) {
fixLinkInputSlots(this)
// Set pending sync flag to suppress link rendering until slots are synced
if (LiteGraph.vueNodesMode) {
layoutStore.setPendingSlotSync(true)
}
// Fire callbacks before the onConfigure, this is used by widget inputs to setup the config
triggerCallbackOnAllNodes(this, 'onGraphConfigured')
try {
fixLinkInputSlots(this)
const r = onConfigure?.apply(this, args)
// Fire callbacks before the onConfigure, this is used by widget inputs to setup the config
triggerCallbackOnAllNodes(this, 'onGraphConfigured')
// Fire after onConfigure, used by primitives to generate widget using input nodes config
triggerCallbackOnAllNodes(this, 'onAfterGraphConfigured')
const r = onConfigure?.apply(this, args)
return r
// Fire after onConfigure, used by primitives to generate widget using input nodes config
triggerCallbackOnAllNodes(this, 'onAfterGraphConfigured')
return r
} finally {
// Flush pending slot layout syncs to fix link alignment after undo/redo
// Using finally ensures links aren't permanently suppressed if an error occurs
if (LiteGraph.vueNodesMode) {
flushScheduledSlotLayoutSync()
app.canvas?.setDirty(true, true)
}
}
}
}