diff --git a/stores/workflow/store.ts b/stores/workflow/store.ts index 3f3f14650d..14dfc171de 100644 --- a/stores/workflow/store.ts +++ b/stores/workflow/store.ts @@ -5,6 +5,7 @@ import { getBlock } from '@/blocks' import { resolveOutputType } from '@/blocks/utils' import { WorkflowStoreWithHistory, pushHistory, withHistory } from './middleware' import { Position, SubBlockState } from './types' +import { detectCycle } from './utils' const initialState = { blocks: {}, @@ -179,18 +180,28 @@ export const useWorkflowStore = create()( }, addEdge: (edge: Edge) => { + // First create the new edge + const newEdge = { + id: edge.id || crypto.randomUUID(), + source: edge.source, + target: edge.target, + sourceHandle: edge.sourceHandle, + targetHandle: edge.targetHandle, + } + + // Create temporary edges array with the new edge + const newEdges = [...get().edges, newEdge] + + // Check for cycles starting from the source node + const { hasCycle, path } = detectCycle(newEdges, edge.source) + + if (hasCycle) { + console.log('Loop detected through nodes:', path.join(' → ')) + } + const newState = { blocks: { ...get().blocks }, - edges: [ - ...get().edges, - { - id: edge.id || crypto.randomUUID(), - source: edge.source, - target: edge.target, - sourceHandle: edge.sourceHandle, - targetHandle: edge.targetHandle, - }, - ], + edges: newEdges, } set(newState) diff --git a/stores/workflow/utils.ts b/stores/workflow/utils.ts new file mode 100644 index 0000000000..e3afec448a --- /dev/null +++ b/stores/workflow/utils.ts @@ -0,0 +1,61 @@ +import { Edge } from 'reactflow' + +/** + * Performs a depth-first search to detect cycles in the graph + * @param edges - List of all edges in the graph + * @param startNode - Starting node for cycle detection + * @returns boolean indicating if a cycle was detected and the path of the cycle if found + */ +export function detectCycle(edges: Edge[], startNode: string): { hasCycle: boolean; path: string[] } { + const visited = new Set() + const recursionStack = new Set() + const pathMap = new Map() + + function dfs(node: string): boolean { + // Add to both visited and recursion stack + visited.add(node) + recursionStack.add(node) + + // Get all neighbors of current node + const neighbors = edges + .filter(edge => edge.source === node) + .map(edge => edge.target) + + for (const neighbor of neighbors) { + // If not visited, explore that path + if (!visited.has(neighbor)) { + pathMap.set(neighbor, node) + if (dfs(neighbor)) return true + } + // If the neighbor is in recursion stack, we found a cycle + else if (recursionStack.has(neighbor)) { + // Record the last edge of the cycle + pathMap.set(neighbor, node) + return true + } + } + + // Remove from recursion stack when backtracking + recursionStack.delete(node) + return false + } + + // Perform DFS and construct cycle path if found + const hasCycle = dfs(startNode) + + // If cycle found, construct the path + const cyclePath: string[] = [] + if (hasCycle) { + let current = startNode + do { + cyclePath.unshift(current) + current = pathMap.get(current)! + } while (current !== startNode && current !== undefined) + + if (current === startNode) { + cyclePath.unshift(startNode) + } + } + + return { hasCycle, path: cyclePath } +} \ No newline at end of file