From c1a569211dc26872b4a1c36a80389b7c9409d682 Mon Sep 17 00:00:00 2001 From: Christian Byrne Date: Fri, 20 Feb 2026 19:44:34 -0800 Subject: [PATCH] fix: skip MatchType recalculation during graph configuration (#9004) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Fixes COM-14955: "Bug: Switch node in subgraph causes link disconnection on export" ## Problem When a MatchType node (like Switch) inside a subgraph is configured/restored, `LGraphNode.configure()` calls `onConnectionsChange` for each input sequentially. The `withComfyMatchType` callback was running before all links were restored, seeing incomplete state and incorrectly computing types, which could cause link disconnection. ## Solution Add early return when `app.configuringGraph` is true to defer type recalculation until after all links are restored. This pattern is already used throughout the codebase: - `widgetInputs.ts` - `rerouteNode.ts` - `customWidgets.ts` Post-configure recomputation is handled by the existing `requestAnimationFrame` callback in `applyMatchType`. ## Changes - `src/core/graph/widgets/dynamicWidgets.ts` - Added 1 line: `if (app.configuringGraph) return` - `src/core/graph/widgets/matchTypeConfiguring.test.ts` - New test file with 3 tests ## Testing - All existing tests pass - Added 3 new tests: - `skips type recalculation when configuringGraph is true` - `performs type recalculation during normal operation` - `connects both inputs with same type` ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-9004-fix-skip-MatchType-recalculation-during-graph-configuration-30d6d73d365081339088ffd8aebba107) by [Unito](https://www.unito.io) --- src/core/graph/widgets/dynamicWidgets.ts | 1 + .../widgets/matchTypeConfiguring.test.ts | 129 ++++++++++++++++++ 2 files changed, 130 insertions(+) create mode 100644 src/core/graph/widgets/matchTypeConfiguring.test.ts diff --git a/src/core/graph/widgets/dynamicWidgets.ts b/src/core/graph/widgets/dynamicWidgets.ts index 3058e8131..21913dedd 100644 --- a/src/core/graph/widgets/dynamicWidgets.ts +++ b/src/core/graph/widgets/dynamicWidgets.ts @@ -277,6 +277,7 @@ function withComfyMatchType(node: LGraphNode): asserts node is MatchTypeNode { ) { const input = this.inputs[slot] if (contype !== LiteGraph.INPUT || !this.graph || !input) return + if (app.configuringGraph) return const [matchKey, matchGroup] = Object.entries( this.comfyDynamic.matchType ).find(([, group]) => input.name in group) ?? ['', undefined] diff --git a/src/core/graph/widgets/matchTypeConfiguring.test.ts b/src/core/graph/widgets/matchTypeConfiguring.test.ts new file mode 100644 index 000000000..45179b0f5 --- /dev/null +++ b/src/core/graph/widgets/matchTypeConfiguring.test.ts @@ -0,0 +1,129 @@ +import { createTestingPinia } from '@pinia/testing' +import { setActivePinia } from 'pinia' +import { beforeEach, describe, expect, test, vi } from 'vitest' + +import { LGraph, LGraphNode, LiteGraph } from '@/lib/litegraph/src/litegraph' +import { transformInputSpecV1ToV2 } from '@/schemas/nodeDef/migration' +import { app } from '@/scripts/app' +import { useLitegraphService } from '@/services/litegraphService' + +setActivePinia(createTestingPinia()) + +const { addNodeInput } = useLitegraphService() + +function createMatchTypeNode(graph: LGraph) { + const node = new LGraphNode('switch') + ;(node.constructor as { nodeData: unknown }).nodeData = { + name: 'ComfySwitchAny', + output_matchtypes: ['a'] + } + node.addOutput('out', '*') + graph.add(node) + + addNodeInput( + node, + transformInputSpecV1ToV2( + [ + 'COMFY_MATCHTYPE_V3', + { template: { allowed_types: '*', template_id: 'a' } } + ], + { name: 'on_true', isOptional: false } + ) + ) + addNodeInput( + node, + transformInputSpecV1ToV2( + [ + 'COMFY_MATCHTYPE_V3', + { template: { allowed_types: '*', template_id: 'a' } } + ], + { name: 'on_false', isOptional: false } + ) + ) + + return node +} + +function createSourceNode(graph: LGraph, type: string) { + const node = new LGraphNode('source') + node.addOutput('out', type) + graph.add(node) + return node +} + +describe('MatchType during configure', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + test('skips type recalculation when configuringGraph is true', () => { + const graph = new LGraph() + const switchNode = createMatchTypeNode(graph) + const source1 = createSourceNode(graph, 'IMAGE') + const source2 = createSourceNode(graph, 'IMAGE') + + source1.connect(0, switchNode, 0) + source2.connect(0, switchNode, 1) + + expect(switchNode.inputs[0].link).not.toBeNull() + expect(switchNode.inputs[1].link).not.toBeNull() + + const link1Id = switchNode.inputs[0].link! + const link2Id = switchNode.inputs[1].link! + + const outputTypeBefore = switchNode.outputs[0].type + ;( + app as unknown as { configuringGraphLevel: number } + ).configuringGraphLevel = 1 + + try { + const link1 = graph.links[link1Id] + switchNode.onConnectionsChange?.( + LiteGraph.INPUT, + 0, + true, + link1, + switchNode.inputs[0] + ) + + expect(switchNode.inputs[0].link).toBe(link1Id) + expect(switchNode.inputs[1].link).toBe(link2Id) + expect(graph.links[link1Id]).toBeDefined() + expect(graph.links[link2Id]).toBeDefined() + expect(switchNode.outputs[0].type).toBe(outputTypeBefore) + } finally { + ;( + app as unknown as { configuringGraphLevel: number } + ).configuringGraphLevel = 0 + } + }) + + test('performs type recalculation during normal operation', () => { + const graph = new LGraph() + const switchNode = createMatchTypeNode(graph) + const source1 = createSourceNode(graph, 'IMAGE') + + expect(app.configuringGraph).toBe(false) + + source1.connect(0, switchNode, 0) + + expect(switchNode.inputs[0].link).not.toBeNull() + expect(switchNode.outputs[0].type).toBe('IMAGE') + }) + + test('connects both inputs with same type', () => { + const graph = new LGraph() + const switchNode = createMatchTypeNode(graph) + const source1 = createSourceNode(graph, 'IMAGE') + const source2 = createSourceNode(graph, 'IMAGE') + + expect(app.configuringGraph).toBe(false) + + source1.connect(0, switchNode, 0) + source2.connect(0, switchNode, 1) + + expect(switchNode.inputs[0].link).not.toBeNull() + expect(switchNode.inputs[1].link).not.toBeNull() + expect(switchNode.outputs[0].type).toBe('IMAGE') + }) +})