Compare commits

...

1 Commits

Author SHA1 Message Date
CodeRabbit Fixer
0f763b523d fix: refactor: Rearchitect workflow/graph data sync to prevent desynced state during loading (#9533)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 11:41:39 +01:00
4 changed files with 124 additions and 134 deletions

View File

@@ -379,6 +379,13 @@ export const useWorkflowService = () => {
void workflowThumbnail.storeThumbnail(activeWorkflow) void workflowThumbnail.storeThumbnail(activeWorkflow)
domWidgetStore.clear() domWidgetStore.clear()
} }
// Deactivate the current workflow before the graph is reconfigured.
// This ensures there is never a window where activeWorkflow references
// the OLD workflow while rootGraph already contains NEW data — any
// checkState or data-sync path that reads activeWorkflow will see null
// and naturally skip, without needing a guard flag.
workflowStore.activeWorkflow = null
} }
/** /**

View File

@@ -9,7 +9,6 @@ import { layoutStore } from '@/renderer/core/layout/store/layoutStore'
import { flushScheduledSlotLayoutSync } from '@/renderer/extensions/vueNodes/composables/useSlotElementTracking' import { flushScheduledSlotLayoutSync } from '@/renderer/extensions/vueNodes/composables/useSlotElementTracking'
import { st, t } from '@/i18n' import { st, t } from '@/i18n'
import { ChangeTracker } from '@/scripts/changeTracker'
import type { IContextMenuValue } from '@/lib/litegraph/src/interfaces' import type { IContextMenuValue } from '@/lib/litegraph/src/interfaces'
import { import {
LGraph, LGraph,
@@ -1307,143 +1306,136 @@ export class ComfyApp {
} }
} }
ChangeTracker.isLoadingGraph = true
try { try {
try { // @ts-expect-error Discrepancies between zod and litegraph - in progress
// @ts-expect-error Discrepancies between zod and litegraph - in progress this.rootGraph.configure(graphData)
this.rootGraph.configure(graphData)
// Save original renderer version before scaling (it gets modified during scaling) // Save original renderer version before scaling (it gets modified during scaling)
const originalMainGraphRenderer = const originalMainGraphRenderer =
this.rootGraph.extra.workflowRendererVersion this.rootGraph.extra.workflowRendererVersion
// Scale main graph // Scale main graph
ensureCorrectLayoutScale(originalMainGraphRenderer) ensureCorrectLayoutScale(originalMainGraphRenderer)
// Scale all subgraphs that were loaded with the workflow // Scale all subgraphs that were loaded with the workflow
// Use original main graph renderer as fallback (not the modified one) // Use original main graph renderer as fallback (not the modified one)
for (const subgraph of this.rootGraph.subgraphs.values()) { for (const subgraph of this.rootGraph.subgraphs.values()) {
ensureCorrectLayoutScale( ensureCorrectLayoutScale(
subgraph.extra.workflowRendererVersion || originalMainGraphRenderer, subgraph.extra.workflowRendererVersion || originalMainGraphRenderer,
subgraph subgraph
) )
}
if (canvasVisible) fitView()
} catch (error) {
useDialogService().showErrorDialog(error, {
title: t('errorDialog.loadWorkflowTitle'),
reportType: 'loadWorkflowError'
})
console.error(error)
return
} }
forEachNode(this.rootGraph, (node) => {
const size = node.computeSize() if (canvasVisible) fitView()
size[0] = Math.max(node.size[0], size[0]) } catch (error) {
size[1] = Math.max(node.size[1], size[1]) useDialogService().showErrorDialog(error, {
node.setSize(size) title: t('errorDialog.loadWorkflowTitle'),
if (node.widgets) { reportType: 'loadWorkflowError'
// If you break something in the backend and want to patch workflows in the frontend })
// This is the place to do this console.error(error)
for (let widget of node.widgets) { return
if (node.type == 'KSampler' || node.type == 'KSamplerAdvanced') { }
if (widget.name == 'sampler_name') { forEachNode(this.rootGraph, (node) => {
if ( const size = node.computeSize()
typeof widget.value === 'string' && size[0] = Math.max(node.size[0], size[0])
widget.value.startsWith('sample_') size[1] = Math.max(node.size[1], size[1])
) { node.setSize(size)
widget.value = widget.value.slice(7) if (node.widgets) {
} // If you break something in the backend and want to patch workflows in the frontend
} // This is the place to do this
} for (let widget of node.widgets) {
if ( if (node.type == 'KSampler' || node.type == 'KSamplerAdvanced') {
node.type == 'KSampler' || if (widget.name == 'sampler_name') {
node.type == 'KSamplerAdvanced' ||
node.type == 'PrimitiveNode'
) {
if (widget.name == 'control_after_generate') {
if (widget.value === true) {
widget.value = 'randomize'
} else if (widget.value === false) {
widget.value = 'fixed'
}
}
}
if (widget.type == 'combo') {
const values = widget.options.values as
| (string | number | boolean)[]
| undefined
if ( if (
values && typeof widget.value === 'string' &&
values.length > 0 && widget.value.startsWith('sample_')
(widget.value == null ||
(reset_invalid_values &&
!values.includes(
widget.value as string | number | boolean
)))
) { ) {
widget.value = values[0] widget.value = widget.value.slice(7)
} }
} }
} }
} if (
node.type == 'KSampler' ||
useExtensionService().invokeExtensions('loadedGraphNode', node) node.type == 'KSamplerAdvanced' ||
}) node.type == 'PrimitiveNode'
) {
await useExtensionService().invokeExtensionsAsync( if (widget.name == 'control_after_generate') {
'afterConfigureGraph', if (widget.value === true) {
missingNodeTypes widget.value = 'randomize'
) } else if (widget.value === false) {
widget.value = 'fixed'
const telemetryPayload = { }
missing_node_count: missingNodeTypes.length, }
missing_node_types: missingNodeTypes.map((node) => }
typeof node === 'string' ? node : node.type if (widget.type == 'combo') {
), const values = widget.options.values as
open_source: openSource ?? 'unknown' | (string | number | boolean)[]
} | undefined
useTelemetry()?.trackWorkflowOpened(telemetryPayload) if (
useTelemetry()?.trackWorkflowImported(telemetryPayload) values &&
await useWorkflowService().afterLoadNewGraph( values.length > 0 &&
workflow, (widget.value == null ||
this.rootGraph.serialize() as unknown as ComfyWorkflowJSON (reset_invalid_values &&
) !values.includes(widget.value as string | number | boolean)))
) {
// If the canvas was not visible and we're a fresh load, resize the canvas and fit the view widget.value = values[0]
// This fixes switching from app mode to a new graph mode workflow (e.g. load template) }
if (!canvasVisible && (!workflow || typeof workflow === 'string')) { }
this.canvas.resize()
requestAnimationFrame(() => fitView())
}
// Store pending warnings on the workflow for deferred display
const activeWf = useWorkspaceStore().workflow.activeWorkflow
if (activeWf) {
const warnings: PendingWarnings = {}
if (missingNodeTypes.length && showMissingNodesDialog) {
warnings.missingNodeTypes = missingNodeTypes
}
if (missingModels.length && showMissingModelsDialog) {
const paths = await api.getFolderPaths()
warnings.missingModels = { missingModels: missingModels, paths }
}
if (warnings.missingNodeTypes || warnings.missingModels) {
activeWf.pendingWarnings = warnings
} }
} }
if (!deferWarnings) { useExtensionService().invokeExtensions('loadedGraphNode', node)
useWorkflowService().showPendingWarnings() })
}
requestAnimationFrame(() => { await useExtensionService().invokeExtensionsAsync(
this.canvas.setDirty(true, true) 'afterConfigureGraph',
}) missingNodeTypes
} finally { )
ChangeTracker.isLoadingGraph = false
const telemetryPayload = {
missing_node_count: missingNodeTypes.length,
missing_node_types: missingNodeTypes.map((node) =>
typeof node === 'string' ? node : node.type
),
open_source: openSource ?? 'unknown'
} }
useTelemetry()?.trackWorkflowOpened(telemetryPayload)
useTelemetry()?.trackWorkflowImported(telemetryPayload)
await useWorkflowService().afterLoadNewGraph(
workflow,
this.rootGraph.serialize() as unknown as ComfyWorkflowJSON
)
// If the canvas was not visible and we're a fresh load, resize the canvas and fit the view
// This fixes switching from app mode to a new graph mode workflow (e.g. load template)
if (!canvasVisible && (!workflow || typeof workflow === 'string')) {
this.canvas.resize()
requestAnimationFrame(() => fitView())
}
// Store pending warnings on the workflow for deferred display
const activeWf = useWorkspaceStore().workflow.activeWorkflow
if (activeWf) {
const warnings: PendingWarnings = {}
if (missingNodeTypes.length && showMissingNodesDialog) {
warnings.missingNodeTypes = missingNodeTypes
}
if (missingModels.length && showMissingModelsDialog) {
const paths = await api.getFolderPaths()
warnings.missingModels = { missingModels: missingModels, paths }
}
if (warnings.missingNodeTypes || warnings.missingModels) {
activeWf.pendingWarnings = warnings
}
}
if (!deferWarnings) {
useWorkflowService().showPendingWarnings()
}
requestAnimationFrame(() => {
this.canvas.setDirty(true, true)
})
} }
async graphToPrompt(graph = this.rootGraph) { async graphToPrompt(graph = this.rootGraph) {

View File

@@ -28,14 +28,6 @@ logger.setLevel('info')
export class ChangeTracker { export class ChangeTracker {
static MAX_HISTORY = 50 static MAX_HISTORY = 50
/**
* Guard flag to prevent checkState from running during loadGraphData.
* Between rootGraph.configure() and afterLoadNewGraph(), the rootGraph
* contains the NEW workflow's data while activeWorkflow still points to
* the OLD workflow. Any checkState call in that window would serialize
* the wrong graph into the old workflow's activeState, corrupting it.
*/
static isLoadingGraph = false
/** /**
* The active state of the workflow. * The active state of the workflow.
*/ */
@@ -139,7 +131,7 @@ export class ChangeTracker {
} }
checkState() { checkState() {
if (!app.graph || this.changeCount || ChangeTracker.isLoadingGraph) return if (!app.graph || this.changeCount) return
const currentState = clone(app.rootGraph.serialize()) as ComfyWorkflowJSON const currentState = clone(app.rootGraph.serialize()) as ComfyWorkflowJSON
if (!this.activeState) { if (!this.activeState) {
this.activeState = currentState this.activeState = currentState

View File

@@ -9,7 +9,6 @@ import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore' import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
import { useSidebarTabStore } from '@/stores/workspace/sidebarTabStore' import { useSidebarTabStore } from '@/stores/workspace/sidebarTabStore'
import { app } from '@/scripts/app' import { app } from '@/scripts/app'
import { ChangeTracker } from '@/scripts/changeTracker'
import { resolveNode } from '@/utils/litegraphUtil' import { resolveNode } from '@/utils/litegraphUtil'
export function nodeTypeValidForApp(type: string) { export function nodeTypeValidForApp(type: string) {
@@ -82,7 +81,7 @@ export const useAppModeStore = defineStore('appMode', () => {
? { inputs: selectedInputs, outputs: selectedOutputs } ? { inputs: selectedInputs, outputs: selectedOutputs }
: null, : null,
(data) => { (data) => {
if (!data || ChangeTracker.isLoadingGraph) return if (!data || !workflowStore.activeWorkflow) return
const graph = app.rootGraph const graph = app.rootGraph
if (!graph) return if (!graph) return
const extra = (graph.extra ??= {}) const extra = (graph.extra ??= {})