Browse Source
The batch textItems loop was only in executeWorkflow. regenerateNode and executeSelectedNodes bypassed batch mode entirely, causing inconsistent behavior when array nodes feed into generate nodes. Extracted runBatchIfApplicable() into src/store/execution/batchExecution.ts and wired it into all three execution entry points. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>handoff-20260429-1057
3 changed files with 115 additions and 68 deletions
@ -0,0 +1,99 @@ |
|||||
|
/** |
||||
|
* Batch Execution Helper |
||||
|
* |
||||
|
* Detects batch mode (textItems from array nodes) and loops through items, |
||||
|
* executing the appropriate node executor for each. Shared by executeWorkflow, |
||||
|
* regenerateNode, and executeSelectedNodes. |
||||
|
*/ |
||||
|
|
||||
|
import { logger } from "@/utils/logger"; |
||||
|
import type { WorkflowNodeData } from "@/types"; |
||||
|
import type { NodeExecutionContext } from "./types"; |
||||
|
import { executeNanoBanana } from "./nanoBananaExecutor"; |
||||
|
import { executeGenerateVideo } from "./generateVideoExecutor"; |
||||
|
import { executeGenerateAudio } from "./generateAudioExecutor"; |
||||
|
import { executeLlmGenerate } from "./llmGenerateExecutor"; |
||||
|
|
||||
|
const BATCH_NODE_TYPES = new Set(["nanoBanana", "generateVideo", "generateAudio", "llmGenerate"]); |
||||
|
|
||||
|
/** |
||||
|
* Attempts to run batch execution for a node. |
||||
|
* |
||||
|
* If the node type supports batching and has textItems from upstream array |
||||
|
* nodes, iterates through each item and runs the executor individually. |
||||
|
* |
||||
|
* @returns `true` if batch execution was performed, `false` if the node |
||||
|
* should proceed with normal single-item execution. |
||||
|
*/ |
||||
|
export async function runBatchIfApplicable( |
||||
|
executionCtx: NodeExecutionContext, |
||||
|
options?: { useStoredFallback?: boolean }, |
||||
|
): Promise<boolean> { |
||||
|
const { node } = executionCtx; |
||||
|
|
||||
|
if (!node.type || !BATCH_NODE_TYPES.has(node.type)) { |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
const connectedInputs = executionCtx.getConnectedInputs(node.id); |
||||
|
if (connectedInputs.textItems.length === 0) { |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
const items = connectedInputs.textItems; |
||||
|
const totalItems = items.length; |
||||
|
|
||||
|
for (let i = 0; i < totalItems; i++) { |
||||
|
if (executionCtx.signal?.aborted) { |
||||
|
throw new DOMException("Aborted", "AbortError"); |
||||
|
} |
||||
|
|
||||
|
executionCtx.updateNodeData(node.id, { |
||||
|
status: "loading", |
||||
|
error: null, |
||||
|
} as Partial<WorkflowNodeData>); |
||||
|
|
||||
|
logger.info("node.execution", `Batch ${i + 1} of ${totalItems}`, { |
||||
|
nodeId: node.id, |
||||
|
nodeType: node.type, |
||||
|
batchIndex: i, |
||||
|
batchTotal: totalItems, |
||||
|
}); |
||||
|
|
||||
|
// Wrap context so getConnectedInputs returns current batch item as text
|
||||
|
const batchCtx: NodeExecutionContext = { |
||||
|
...executionCtx, |
||||
|
getConnectedInputs: (nodeId: string) => { |
||||
|
const inputs = executionCtx.getConnectedInputs(nodeId); |
||||
|
return { |
||||
|
...inputs, |
||||
|
text: items[i], |
||||
|
textItems: [], |
||||
|
}; |
||||
|
}, |
||||
|
}; |
||||
|
|
||||
|
switch (node.type) { |
||||
|
case "nanoBanana": |
||||
|
await executeNanoBanana(batchCtx, options); |
||||
|
break; |
||||
|
case "generateVideo": |
||||
|
await executeGenerateVideo(batchCtx, options); |
||||
|
break; |
||||
|
case "generateAudio": |
||||
|
await executeGenerateAudio(batchCtx, options); |
||||
|
break; |
||||
|
case "llmGenerate": |
||||
|
await executeLlmGenerate(batchCtx, options); |
||||
|
break; |
||||
|
} |
||||
|
|
||||
|
if (i < totalItems - 1) { |
||||
|
executionCtx.updateNodeData(node.id, { |
||||
|
status: "loading", |
||||
|
} as Partial<WorkflowNodeData>); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return true; |
||||
|
} |
||||
Loading…
Reference in new issue