Skip to content

Commit b125e0a

Browse files
[feat] Move partial execution to the backend and make work with subgraphs (#4624)
1 parent aabea4b commit b125e0a

File tree

8 files changed

+663
-59
lines changed

8 files changed

+663
-59
lines changed

src/composables/useCoreCommands.ts

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,11 @@ import { useBottomPanelStore } from '@/stores/workspace/bottomPanelStore'
2929
import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'
3030
import { useSearchBoxStore } from '@/stores/workspace/searchBoxStore'
3131
import { useWorkspaceStore } from '@/stores/workspaceStore'
32-
import { getAllNonIoNodesInSubgraph } from '@/utils/graphTraversalUtil'
32+
import {
33+
getAllNonIoNodesInSubgraph,
34+
getExecutionIdsForSelectedNodes
35+
} from '@/utils/graphTraversalUtil'
36+
import { filterOutputNodes } from '@/utils/nodeFilterUtil'
3337

3438
const moveSelectedNodesVersionAdded = '1.22.2'
3539

@@ -363,10 +367,10 @@ export function useCoreCommands(): ComfyCommand[] {
363367
versionAdded: '1.19.6',
364368
function: async () => {
365369
const batchCount = useQueueSettingsStore().batchCount
366-
const queueNodeIds = getSelectedNodes()
367-
.filter((node) => node.constructor.nodeData?.output_node)
368-
.map((node) => node.id)
369-
if (queueNodeIds.length === 0) {
370+
const selectedNodes = getSelectedNodes()
371+
const selectedOutputNodes = filterOutputNodes(selectedNodes)
372+
373+
if (selectedOutputNodes.length === 0) {
370374
toastStore.add({
371375
severity: 'error',
372376
summary: t('toastMessages.nothingToQueue'),
@@ -375,7 +379,11 @@ export function useCoreCommands(): ComfyCommand[] {
375379
})
376380
return
377381
}
378-
await app.queuePrompt(0, batchCount, queueNodeIds)
382+
383+
// Get execution IDs for all selected output nodes and their descendants
384+
const executionIds =
385+
getExecutionIdsForSelectedNodes(selectedOutputNodes)
386+
await app.queuePrompt(0, batchCount, executionIds)
379387
}
380388
},
381389
{

src/scripts/api.ts

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,13 @@ import type {
3535
NodeId
3636
} from '@/schemas/comfyWorkflowSchema'
3737
import type { ComfyNodeDef } from '@/schemas/nodeDefSchema'
38+
import type { NodeExecutionId } from '@/types/nodeIdentification'
3839
import { WorkflowTemplates } from '@/types/workflowTemplateTypes'
3940

4041
interface QueuePromptRequestBody {
4142
client_id: string
4243
prompt: ComfyApiWorkflow
44+
partial_execution_targets?: NodeExecutionId[]
4345
extra_data: {
4446
extra_pnginfo: {
4547
workflow: ComfyWorkflowJSON
@@ -80,6 +82,18 @@ interface QueuePromptRequestBody {
8082
number?: number
8183
}
8284

85+
/**
86+
* Options for queuePrompt method
87+
*/
88+
interface QueuePromptOptions {
89+
/**
90+
* Optional list of node execution IDs to execute (partial execution).
91+
* Each ID represents a node's position in nested subgraphs.
92+
* Format: Colon-separated path of node IDs (e.g., "123:456:789")
93+
*/
94+
partialExecutionTargets?: NodeExecutionId[]
95+
}
96+
8397
/** Dictionary of Frontend-generated API calls */
8498
interface FrontendApiCalls {
8599
graphChanged: ComfyWorkflowJSON
@@ -610,18 +624,23 @@ export class ComfyApi extends EventTarget {
610624
/**
611625
* Queues a prompt to be executed
612626
* @param {number} number The index at which to queue the prompt, passing -1 will insert the prompt at the front of the queue
613-
* @param {object} prompt The prompt data to queue
627+
* @param {object} data The prompt data to queue
628+
* @param {QueuePromptOptions} options Optional execution options
614629
* @throws {PromptExecutionError} If the prompt fails to execute
615630
*/
616631
async queuePrompt(
617632
number: number,
618-
data: { output: ComfyApiWorkflow; workflow: ComfyWorkflowJSON }
633+
data: { output: ComfyApiWorkflow; workflow: ComfyWorkflowJSON },
634+
options?: QueuePromptOptions
619635
): Promise<PromptResponse> {
620636
const { output: prompt, workflow } = data
621637

622638
const body: QueuePromptRequestBody = {
623639
client_id: this.clientId ?? '', // TODO: Unify clientId access
624640
prompt,
641+
...(options?.partialExecutionTargets && {
642+
partial_execution_targets: options.partialExecutionTargets
643+
}),
625644
extra_data: {
626645
auth_token_comfy_org: this.authToken,
627646
api_key_comfy_org: this.apiKey,

src/scripts/app.ts

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'
5959
import { useWorkspaceStore } from '@/stores/workspaceStore'
6060
import type { ComfyExtension, MissingNodeType } from '@/types/comfy'
6161
import { ExtensionManager } from '@/types/extensionTypes'
62+
import type { NodeExecutionId } from '@/types/nodeIdentification'
6263
import { ColorAdjustOptions, adjustColor } from '@/utils/colorUtil'
6364
import { graphToPrompt } from '@/utils/executionUtil'
6465
import {
@@ -127,7 +128,7 @@ export class ComfyApp {
127128
#queueItems: {
128129
number: number
129130
batchCount: number
130-
queueNodeIds?: NodeId[]
131+
queueNodeIds?: NodeExecutionId[]
131132
}[] = []
132133
/**
133134
* If the queue is currently being processed
@@ -1239,20 +1240,16 @@ export class ComfyApp {
12391240
})
12401241
}
12411242

1242-
async graphToPrompt(
1243-
graph = this.graph,
1244-
options: { queueNodeIds?: NodeId[] } = {}
1245-
) {
1243+
async graphToPrompt(graph = this.graph) {
12461244
return graphToPrompt(graph, {
1247-
sortNodes: useSettingStore().get('Comfy.Workflow.SortNodeIdOnSave'),
1248-
queueNodeIds: options.queueNodeIds
1245+
sortNodes: useSettingStore().get('Comfy.Workflow.SortNodeIdOnSave')
12491246
})
12501247
}
12511248

12521249
async queuePrompt(
12531250
number: number,
12541251
batchCount: number = 1,
1255-
queueNodeIds?: NodeId[]
1252+
queueNodeIds?: NodeExecutionId[]
12561253
): Promise<boolean> {
12571254
this.#queueItems.push({ number, batchCount, queueNodeIds })
12581255

@@ -1281,11 +1278,13 @@ export class ComfyApp {
12811278
executeWidgetsCallback(subgraph.nodes, 'beforeQueued')
12821279
}
12831280

1284-
const p = await this.graphToPrompt(this.graph, { queueNodeIds })
1281+
const p = await this.graphToPrompt(this.graph)
12851282
try {
12861283
api.authToken = comfyOrgAuthToken
12871284
api.apiKey = comfyOrgApiKey ?? undefined
1288-
const res = await api.queuePrompt(number, p)
1285+
const res = await api.queuePrompt(number, p, {
1286+
partialExecutionTargets: queueNodeIds
1287+
})
12891288
delete api.authToken
12901289
delete api.apiKey
12911290
executionStore.lastNodeErrors = res.node_errors ?? null

src/utils/executionUtil.ts

Lines changed: 4 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
import type {
22
ExecutableLGraphNode,
33
ExecutionId,
4-
LGraph,
5-
NodeId
4+
LGraph
65
} from '@comfyorg/litegraph'
76
import {
87
ExecutableNodeDTO,
@@ -18,46 +17,20 @@ import type {
1817
import { ExecutableGroupNodeDTO, isGroupNode } from './executableGroupNodeDto'
1918
import { compressWidgetInputSlots } from './litegraphUtil'
2019

21-
/**
22-
* Recursively target node's parent nodes to the new output.
23-
* @param nodeId The node id to add.
24-
* @param oldOutput The old output.
25-
* @param newOutput The new output.
26-
* @returns The new output.
27-
*/
28-
function recursiveAddNodes(
29-
nodeId: NodeId,
30-
oldOutput: ComfyApiWorkflow,
31-
newOutput: ComfyApiWorkflow
32-
) {
33-
const currentId = String(nodeId)
34-
const currentNode = oldOutput[currentId]!
35-
if (newOutput[currentId] == null) {
36-
newOutput[currentId] = currentNode
37-
for (const inputValue of Object.values(currentNode.inputs || [])) {
38-
if (Array.isArray(inputValue)) {
39-
recursiveAddNodes(inputValue[0], oldOutput, newOutput)
40-
}
41-
}
42-
}
43-
return newOutput
44-
}
45-
4620
/**
4721
* Converts the current graph workflow for sending to the API.
4822
* @note Node widgets are updated before serialization to prepare queueing.
4923
*
5024
* @param graph The graph to convert.
5125
* @param options The options for the conversion.
5226
* - `sortNodes`: Whether to sort the nodes by execution order.
53-
* - `queueNodeIds`: The output nodes to execute. Execute all output nodes if not provided.
5427
* @returns The workflow and node links
5528
*/
5629
export const graphToPrompt = async (
5730
graph: LGraph,
58-
options: { sortNodes?: boolean; queueNodeIds?: NodeId[] } = {}
31+
options: { sortNodes?: boolean } = {}
5932
): Promise<{ workflow: ComfyWorkflowJSON; output: ComfyApiWorkflow }> => {
60-
const { sortNodes = false, queueNodeIds } = options
33+
const { sortNodes = false } = options
6134

6235
for (const node of graph.computeExecutionOrder(false)) {
6336
const innerNodes = node.getInnerNodes
@@ -104,7 +77,7 @@ export const graphToPrompt = async (
10477
nodeDtoMap.set(dto.id, dto)
10578
}
10679

107-
let output: ComfyApiWorkflow = {}
80+
const output: ComfyApiWorkflow = {}
10881
// Process nodes in order of execution
10982
for (const node of nodeDtoMap.values()) {
11083
// Don't serialize muted nodes
@@ -180,14 +153,5 @@ export const graphToPrompt = async (
180153
}
181154
}
182155

183-
// Partial execution
184-
if (queueNodeIds?.length) {
185-
const newOutput = {}
186-
for (const queueNodeId of queueNodeIds) {
187-
recursiveAddNodes(queueNodeId, output, newOutput)
188-
}
189-
output = newOutput
190-
}
191-
192156
return { workflow: workflow as ComfyWorkflowJSON, output }
193157
}

src/utils/graphTraversalUtil.ts

Lines changed: 104 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { LGraph, LGraphNode, Subgraph } from '@comfyorg/litegraph'
22

3-
import type { NodeLocatorId } from '@/types/nodeIdentification'
3+
import type { NodeExecutionId, NodeLocatorId } from '@/types/nodeIdentification'
44
import { parseNodeLocatorId } from '@/types/nodeIdentification'
55

66
import { isSubgraphIoNode } from './typeGuardUtil'
@@ -351,3 +351,106 @@ export function mapSubgraphNodes<T>(
351351
export function getAllNonIoNodesInSubgraph(subgraph: Subgraph): LGraphNode[] {
352352
return subgraph.nodes.filter((node) => !isSubgraphIoNode(node))
353353
}
354+
355+
/**
356+
* Performs depth-first traversal of nodes and their subgraphs.
357+
* Generic visitor pattern that can be used for various node processing tasks.
358+
*
359+
* @param nodes - Starting nodes for traversal
360+
* @param visitor - Function called for each node with its context
361+
* @param expandSubgraphs - Whether to traverse into subgraph nodes (default: true)
362+
*/
363+
export function traverseNodesDepthFirst<T>(
364+
nodes: LGraphNode[],
365+
visitor: (node: LGraphNode, context: T) => T,
366+
initialContext: T,
367+
expandSubgraphs: boolean = true
368+
): void {
369+
type StackItem = { node: LGraphNode; context: T }
370+
const stack: StackItem[] = []
371+
372+
// Initialize stack with starting nodes
373+
for (const node of nodes) {
374+
stack.push({ node, context: initialContext })
375+
}
376+
377+
// Process stack iteratively (DFS)
378+
while (stack.length > 0) {
379+
const { node, context } = stack.pop()!
380+
381+
// Visit node and get updated context for children
382+
const childContext = visitor(node, context)
383+
384+
// If it's a subgraph and we should expand, add children to stack
385+
if (expandSubgraphs && node.isSubgraphNode?.() && node.subgraph) {
386+
// Process children in reverse order to maintain left-to-right DFS processing
387+
// when popping from stack (LIFO). Iterate backwards to avoid array reversal.
388+
const children = node.subgraph.nodes
389+
for (let i = children.length - 1; i >= 0; i--) {
390+
stack.push({ node: children[i], context: childContext })
391+
}
392+
}
393+
}
394+
}
395+
396+
/**
397+
* Collects nodes with custom data during depth-first traversal.
398+
* Generic collector that can gather any type of data per node.
399+
*
400+
* @param nodes - Starting nodes for traversal
401+
* @param collector - Function that returns data to collect for each node
402+
* @param contextBuilder - Function that builds context for child nodes
403+
* @param expandSubgraphs - Whether to traverse into subgraph nodes
404+
* @returns Array of collected data
405+
*/
406+
export function collectFromNodes<T, C>(
407+
nodes: LGraphNode[],
408+
collector: (node: LGraphNode, context: C) => T | null,
409+
contextBuilder: (node: LGraphNode, parentContext: C) => C,
410+
initialContext: C,
411+
expandSubgraphs: boolean = true
412+
): T[] {
413+
const results: T[] = []
414+
415+
traverseNodesDepthFirst(
416+
nodes,
417+
(node, context) => {
418+
const data = collector(node, context)
419+
if (data !== null) {
420+
results.push(data)
421+
}
422+
return contextBuilder(node, context)
423+
},
424+
initialContext,
425+
expandSubgraphs
426+
)
427+
428+
return results
429+
}
430+
431+
/**
432+
* Collects execution IDs for selected nodes and all their descendants.
433+
* Uses the generic DFS traversal with optimized string building.
434+
*
435+
* @param selectedNodes - The selected nodes to process
436+
* @returns Array of execution IDs for selected nodes and all nodes within selected subgraphs
437+
*/
438+
export function getExecutionIdsForSelectedNodes(
439+
selectedNodes: LGraphNode[]
440+
): NodeExecutionId[] {
441+
return collectFromNodes(
442+
selectedNodes,
443+
// Collector: build execution ID for each node
444+
(node, parentExecutionId: string): NodeExecutionId => {
445+
const nodeId = String(node.id)
446+
return parentExecutionId ? `${parentExecutionId}:${nodeId}` : nodeId
447+
},
448+
// Context builder: pass execution ID to children
449+
(node, parentExecutionId: string) => {
450+
const nodeId = String(node.id)
451+
return parentExecutionId ? `${parentExecutionId}:${nodeId}` : nodeId
452+
},
453+
'', // Initial context: empty parent execution ID
454+
true // Expand subgraphs
455+
)
456+
}

src/utils/nodeFilterUtil.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import type { LGraphNode } from '@comfyorg/litegraph'
2+
3+
/**
4+
* Checks if a node is an output node.
5+
* Output nodes are nodes that have the output_node flag set in their nodeData.
6+
*
7+
* @param node - The node to check
8+
* @returns True if the node is an output node, false otherwise
9+
*/
10+
export const isOutputNode = (node: LGraphNode) =>
11+
node.constructor.nodeData?.output_node
12+
13+
/**
14+
* Filters nodes to find only output nodes.
15+
* Output nodes are nodes that have the output_node flag set in their nodeData.
16+
*
17+
* @param nodes - Array of nodes to filter
18+
* @returns Array of output nodes only
19+
*/
20+
export const filterOutputNodes = (nodes: LGraphNode[]): LGraphNode[] =>
21+
nodes.filter(isOutputNode)

0 commit comments

Comments
 (0)