[backport cloud/1.42] feat: resolveVirtualOutput for cross-subgraph virtual nodes (eg. Set/Get) (#10182)

Backport of #10111 to `cloud/1.42`

Automatically created by backport workflow.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10182-backport-cloud-1-42-feat-resolveVirtualOutput-for-cross-subgraph-virtual-nodes-eg-S-3266d73d36508133b43bda8a89aeaf7f)
by [Unito](https://www.unito.io)

Co-authored-by: Jukka Seppänen <40791699+kijai@users.noreply.github.com>
This commit is contained in:
Comfy Org PR Bot
2026-03-17 23:07:16 +09:00
committed by GitHub
parent ed40779cdc
commit ccb3e33eb8
3 changed files with 118 additions and 0 deletions

View File

@@ -1207,6 +1207,14 @@ export class LGraphNode
: this.inputs[slot]
}
/**
* Resolves the output source for cross-graph virtual nodes (e.g. Set/Get),
* bypassing {@link getInputLink} when the source lives in a different graph.
*/
resolveVirtualOutput?(
slot: number
): { node: LGraphNode; slot: number } | undefined
/**
* Returns the link info in the connection of an input slot
* @returns object or null

View File

@@ -382,6 +382,102 @@ describe('ALWAYS mode node output resolution', () => {
})
})
describe('Virtual node resolveVirtualOutput', () => {
beforeEach(() => {
setActivePinia(createTestingPinia({ stubActions: false }))
})
it('should resolve through resolveVirtualOutput when implemented', () => {
const graph = new LGraph()
const sourceNode = new LGraphNode('Source')
sourceNode.addOutput('out', 'IMAGE')
graph.add(sourceNode)
const virtualNode = new LGraphNode('Virtual Get')
virtualNode.addOutput('out', 'IMAGE')
virtualNode.isVirtualNode = true
virtualNode.resolveVirtualOutput = () => ({ node: sourceNode, slot: 0 })
graph.add(virtualNode)
const nodeDtoMap = new Map()
const sourceDto = new ExecutableNodeDTO(
sourceNode,
[],
nodeDtoMap,
undefined
)
nodeDtoMap.set(sourceDto.id, sourceDto)
const virtualDto = new ExecutableNodeDTO(
virtualNode,
[],
nodeDtoMap,
undefined
)
nodeDtoMap.set(virtualDto.id, virtualDto)
const resolved = virtualDto.resolveOutput(0, 'IMAGE', new Set())
expect(resolved).toBeDefined()
expect(resolved?.node).toBe(sourceDto)
expect(resolved?.origin_slot).toBe(0)
})
it('should throw when resolveVirtualOutput returns a node with no matching DTO', () => {
const graph = new LGraph()
const unmappedNode = new LGraphNode('Unmapped Source')
unmappedNode.addOutput('out', 'IMAGE')
graph.add(unmappedNode)
const virtualNode = new LGraphNode('Virtual Get')
virtualNode.addOutput('out', 'IMAGE')
virtualNode.isVirtualNode = true
virtualNode.resolveVirtualOutput = () => ({
node: unmappedNode,
slot: 0
})
graph.add(virtualNode)
const nodeDtoMap = new Map()
const virtualDto = new ExecutableNodeDTO(
virtualNode,
[],
nodeDtoMap,
undefined
)
nodeDtoMap.set(virtualDto.id, virtualDto)
expect(() => virtualDto.resolveOutput(0, 'IMAGE', new Set())).toThrow(
'No DTO found for virtual source node'
)
})
it('should fall through to getInputLink when resolveVirtualOutput returns undefined', () => {
const graph = new LGraph()
const virtualNode = new LGraphNode('Virtual Passthrough')
virtualNode.addOutput('out', 'IMAGE')
virtualNode.isVirtualNode = true
virtualNode.resolveVirtualOutput = () => undefined
graph.add(virtualNode)
const nodeDtoMap = new Map()
const virtualDto = new ExecutableNodeDTO(
virtualNode,
[],
nodeDtoMap,
undefined
)
nodeDtoMap.set(virtualDto.id, virtualDto)
const spy = vi.spyOn(virtualNode, 'getInputLink')
const resolved = virtualDto.resolveOutput(0, 'IMAGE', new Set())
expect(resolved).toBeUndefined()
expect(spy).toHaveBeenCalledWith(0)
})
})
describe.skip('ExecutableNodeDTO Properties', () => {
it('should provide access to basic properties', () => {
const graph = new LGraph()

View File

@@ -291,6 +291,20 @@ export class ExecutableNodeDTO implements ExecutableLGraphNode {
return this._resolveSubgraphOutput(slot, type, visited)
if (node.isVirtualNode) {
// Cross-graph virtual nodes (e.g. Set/Get) resolve their source directly.
const virtualSource = this.node.resolveVirtualOutput?.(slot)
if (virtualSource) {
const inputNodeDto = [...this.nodesByExecutionId.values()].find(
(dto) =>
dto instanceof ExecutableNodeDTO && dto.node === virtualSource.node
)
if (!inputNodeDto)
throw new Error(
`No DTO found for virtual source node [${virtualSource.node.id}]`
)
return inputNodeDto.resolveOutput(virtualSource.slot, type, visited)
}
const virtualLink = this.node.getInputLink(slot)
if (virtualLink) {
const { inputNode } = virtualLink.resolve(this.graph)