@ -6,6 +6,7 @@
* /
import { WorkflowNode , WorkflowEdge , WorkflowNodeData } from "@/types" ;
import { getSourceOutput } from "./connectedInputs" ;
// Concurrency settings
export const CONCURRENCY_SETTINGS_KEY = "node-banana-concurrency-limit" ;
@ -139,3 +140,138 @@ export function clearNodeImageRefs(nodes: WorkflowNode[]): WorkflowNode[] {
return { . . . node , data : data as WorkflowNodeData } as WorkflowNode ;
} ) ;
}
/ * *
* Check if adding an edge from sourceId to targetId would create a cycle .
* Uses iterative DFS to check if targetId can reach sourceId through existing edges .
* /
export function wouldCreateCycle (
sourceId : string ,
targetId : string ,
edges : WorkflowEdge [ ]
) : boolean {
// Self-loop check
if ( sourceId === targetId ) return true ;
// Build adjacency list (edge.source → edge.target)
const adjList = new Map < string , string [ ] > ( ) ;
edges . forEach ( ( edge ) = > {
if ( ! adjList . has ( edge . source ) ) {
adjList . set ( edge . source , [ ] ) ;
}
adjList . get ( edge . source ) ! . push ( edge . target ) ;
} ) ;
// DFS from targetId to see if we can reach sourceId
const visited = new Set < string > ( ) ;
const stack = [ targetId ] ;
while ( stack . length > 0 ) {
const current = stack . pop ( ) ! ;
if ( current === sourceId ) return true ;
if ( visited . has ( current ) ) continue ;
visited . add ( current ) ;
const neighbors = adjList . get ( current ) || [ ] ;
for ( const neighbor of neighbors ) {
if ( ! visited . has ( neighbor ) ) {
stack . push ( neighbor ) ;
}
}
}
return false ;
}
/ * *
* Find all nodes that are part of a loop body .
* Returns the intersection of nodes reachable forward from loopTarget
* and nodes reachable backward from loopSource .
* /
export function findLoopSubgraph (
loopSource : string ,
loopTarget : string ,
forwardEdges : WorkflowEdge [ ]
) : string [ ] {
// Build adjacency lists
const forward = new Map < string , string [ ] > ( ) ;
const backward = new Map < string , string [ ] > ( ) ;
forwardEdges . forEach ( ( edge ) = > {
if ( ! forward . has ( edge . source ) ) {
forward . set ( edge . source , [ ] ) ;
}
forward . get ( edge . source ) ! . push ( edge . target ) ;
if ( ! backward . has ( edge . target ) ) {
backward . set ( edge . target , [ ] ) ;
}
backward . get ( edge . target ) ! . push ( edge . source ) ;
} ) ;
// BFS forward from loopTarget
const forwardReachable = new Set < string > ( ) ;
const forwardQueue = [ loopTarget ] ;
while ( forwardQueue . length > 0 ) {
const current = forwardQueue . shift ( ) ! ;
if ( forwardReachable . has ( current ) ) continue ;
forwardReachable . add ( current ) ;
const neighbors = forward . get ( current ) || [ ] ;
for ( const neighbor of neighbors ) {
if ( ! forwardReachable . has ( neighbor ) ) {
forwardQueue . push ( neighbor ) ;
}
}
}
// BFS backward from loopSource
const backwardReachable = new Set < string > ( ) ;
const backwardQueue = [ loopSource ] ;
while ( backwardQueue . length > 0 ) {
const current = backwardQueue . shift ( ) ! ;
if ( backwardReachable . has ( current ) ) continue ;
backwardReachable . add ( current ) ;
const neighbors = backward . get ( current ) || [ ] ;
for ( const neighbor of neighbors ) {
if ( ! backwardReachable . has ( neighbor ) ) {
backwardQueue . push ( neighbor ) ;
}
}
}
// Return intersection
const intersection = Array . from ( forwardReachable ) . filter ( ( node ) = >
backwardReachable . has ( node )
) ;
return intersection ;
}
/ * *
* Copy output data from source node to target node input field .
* Used for loop edges to transfer data from loop end back to loop start .
* /
export function copyLoopOutput (
sourceNode : WorkflowNode ,
sourceHandle : string | null ,
targetNode : WorkflowNode ,
targetHandle : string | null ,
updateNodeData : ( nodeId : string , data : Partial < WorkflowNodeData > ) = > void
) : void {
const { type , value } = getSourceOutput ( sourceNode , sourceHandle ) ;
// If value is null, do nothing
if ( value === null ) return ;
// Map output type to target input field based on node type
if ( type === "image" && targetNode . type === "imageInput" ) {
updateNodeData ( targetNode . id , { image : value } ) ;
} else if ( type === "video" && targetNode . type === "videoInput" ) {
updateNodeData ( targetNode . id , { video : value } ) ;
} else if ( type === "text" && targetNode . type === "prompt" ) {
updateNodeData ( targetNode . id , { prompt : value } ) ;
} else if ( type === "audio" && targetNode . type === "audioInput" ) {
updateNodeData ( targetNode . id , { audioFile : value } ) ;
}
}