diff --git a/stores/workflow/store.ts b/stores/workflow/store.ts index 99f63f41c..b05c5c019 100644 --- a/stores/workflow/store.ts +++ b/stores/workflow/store.ts @@ -192,18 +192,28 @@ export const useWorkflowStore = create()( } const newEdges = [...get().edges, newEdge] - const { hasCycle, path } = detectCycle(newEdges, edge.source) - // Only create a loop if we have a valid cycle with at least 2 unique nodes - const newLoops = { ...get().loops } + // Recalculate all loops after adding the edge + const newLoops: Record = {} + const processedPaths = new Set() - if (hasCycle && path.length > 1) { - const loopId = crypto.randomUUID() - newLoops[loopId] = { - id: loopId, - nodes: path - } - } + // Check for cycles from each node + const nodes = new Set(newEdges.map(e => e.source)) + nodes.forEach(node => { + const { paths } = detectCycle(newEdges, node) + paths.forEach(path => { + // Create a canonical path representation for deduplication + const canonicalPath = [...path].sort().join(',') + if (!processedPaths.has(canonicalPath)) { + const loopId = crypto.randomUUID() + newLoops[loopId] = { + id: loopId, + nodes: path + } + processedPaths.add(canonicalPath) + } + }) + }) const newState = { blocks: { ...get().blocks }, @@ -219,23 +229,26 @@ export const useWorkflowStore = create()( removeEdge: (edgeId: string) => { const newEdges = get().edges.filter((edge) => edge.id !== edgeId) - // Recalculate loops after edge removal + // Recalculate all loops after edge removal const newLoops: Record = {} - const processedNodes = new Set() + const processedPaths = new Set() - // Check for cycles from each source node - newEdges.forEach(edge => { - if (!processedNodes.has(edge.source)) { - const { hasCycle, path } = detectCycle(newEdges, edge.source) - if (hasCycle) { + // Check for cycles from each node + const nodes = new Set(newEdges.map(e => e.source)) + nodes.forEach(node => { + const { paths } = detectCycle(newEdges, node) + paths.forEach(path => { + // Create a canonical path representation for deduplication + const canonicalPath = [...path].sort().join(',') + if (!processedPaths.has(canonicalPath)) { const loopId = crypto.randomUUID() newLoops[loopId] = { id: loopId, nodes: path } + processedPaths.add(canonicalPath) } - processedNodes.add(edge.source) - } + }) }) const newState = { diff --git a/stores/workflow/utils.ts b/stores/workflow/utils.ts index 5a09c1a31..685c27ca7 100644 --- a/stores/workflow/utils.ts +++ b/stores/workflow/utils.ts @@ -1,68 +1,53 @@ import { Edge } from 'reactflow' /** - * Performs a depth-first search to detect cycles in the graph + * Performs a depth-first search to detect all 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 + * @returns Array of all unique cycles found in the graph */ -export function detectCycle(edges: Edge[], startNode: string): { hasCycle: boolean; path: string[] } { +export function detectCycle(edges: Edge[], startNode: string): { hasCycle: boolean; paths: 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 + const allCycles: string[][] = [] + const currentPath: string[] = [] + + function dfs(node: string) { visited.add(node) recursionStack.add(node) - + currentPath.push(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 + if (!recursionStack.has(neighbor)) { + if (!visited.has(neighbor)) { + dfs(neighbor) + } + } else { + // Found a cycle + const cycleStartIndex = currentPath.indexOf(neighbor) + if (cycleStartIndex !== -1) { + const cycle = currentPath.slice(cycleStartIndex) + // Only add cycles with length > 1 + if (cycle.length > 1) { + allCycles.push([...cycle]) + } + } } } - - // Remove from recursion stack when backtracking + + currentPath.pop() recursionStack.delete(node) - return false } - - // Perform DFS and construct cycle path if found - const hasCycle = dfs(startNode) - - // If cycle found, construct the path and ensure no duplicates - const cyclePath: string[] = [] - if (hasCycle) { - let current = startNode - const seenNodes = new Set() - - do { - // Only add node if we haven't seen it before - if (!seenNodes.has(current)) { - cyclePath.unshift(current) - seenNodes.add(current) - } - current = pathMap.get(current)! - } while (current !== startNode && current !== undefined) - - // Add starting node only if it's not already in the path - if (current === startNode && !seenNodes.has(startNode)) { - cyclePath.unshift(startNode) - } + + dfs(startNode) + + return { + hasCycle: allCycles.length > 0, + paths: allCycles } - - return { hasCycle, path: cyclePath } } \ No newline at end of file