mirror of
https://github.com/simstudioai/sim.git
synced 2026-01-07 22:24:06 -05:00
fix(graph): prevent cyclic dependencies in graph following ReactFlow examples (#2439)
* fix(graph): prevent cyclic dependencies in graph following ReactFlow examples * ack PR comment
This commit is contained in:
@@ -40,6 +40,8 @@ import { useSelectorDisplayName } from '@/hooks/use-selector-display-name'
|
||||
import { useVariablesStore } from '@/stores/panel/variables/store'
|
||||
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
|
||||
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
|
||||
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
|
||||
import { wouldCreateCycle } from '@/stores/workflows/workflow/utils'
|
||||
|
||||
const logger = createLogger('WorkflowBlock')
|
||||
|
||||
@@ -844,7 +846,11 @@ export const WorkflowBlock = memo(function WorkflowBlock({
|
||||
data-handleid='target'
|
||||
isConnectableStart={false}
|
||||
isConnectableEnd={true}
|
||||
isValidConnection={(connection) => connection.source !== id}
|
||||
isValidConnection={(connection) => {
|
||||
if (connection.source === id) return false
|
||||
const edges = useWorkflowStore.getState().edges
|
||||
return !wouldCreateCycle(edges, connection.source!, connection.target!)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -1045,7 +1051,11 @@ export const WorkflowBlock = memo(function WorkflowBlock({
|
||||
data-handleid={`condition-${cond.id}`}
|
||||
isConnectableStart={true}
|
||||
isConnectableEnd={false}
|
||||
isValidConnection={(connection) => connection.target !== id}
|
||||
isValidConnection={(connection) => {
|
||||
if (connection.target === id) return false
|
||||
const edges = useWorkflowStore.getState().edges
|
||||
return !wouldCreateCycle(edges, connection.source!, connection.target!)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
@@ -1064,7 +1074,11 @@ export const WorkflowBlock = memo(function WorkflowBlock({
|
||||
data-handleid='error'
|
||||
isConnectableStart={true}
|
||||
isConnectableEnd={false}
|
||||
isValidConnection={(connection) => connection.target !== id}
|
||||
isValidConnection={(connection) => {
|
||||
if (connection.target === id) return false
|
||||
const edges = useWorkflowStore.getState().edges
|
||||
return !wouldCreateCycle(edges, connection.source!, connection.target!)
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
@@ -1081,7 +1095,11 @@ export const WorkflowBlock = memo(function WorkflowBlock({
|
||||
data-handleid='source'
|
||||
isConnectableStart={true}
|
||||
isConnectableEnd={false}
|
||||
isValidConnection={(connection) => connection.target !== id}
|
||||
isValidConnection={(connection) => {
|
||||
if (connection.target === id) return false
|
||||
const edges = useWorkflowStore.getState().edges
|
||||
return !wouldCreateCycle(edges, connection.source!, connection.target!)
|
||||
}}
|
||||
/>
|
||||
|
||||
{shouldShowDefaultHandles && (
|
||||
@@ -1100,7 +1118,11 @@ export const WorkflowBlock = memo(function WorkflowBlock({
|
||||
data-handleid='error'
|
||||
isConnectableStart={true}
|
||||
isConnectableEnd={false}
|
||||
isValidConnection={(connection) => connection.target !== id}
|
||||
isValidConnection={(connection) => {
|
||||
if (connection.target === id) return false
|
||||
const edges = useWorkflowStore.getState().edges
|
||||
return !wouldCreateCycle(edges, connection.source!, connection.target!)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -1642,11 +1642,6 @@ const WorkflowContent = React.memo(() => {
|
||||
const onConnect = useCallback(
|
||||
(connection: any) => {
|
||||
if (connection.source && connection.target) {
|
||||
// Prevent self-connections
|
||||
if (connection.source === connection.target) {
|
||||
return
|
||||
}
|
||||
|
||||
// Check if connecting nodes across container boundaries
|
||||
const sourceNode = getNodes().find((n) => n.id === connection.source)
|
||||
const targetNode = getNodes().find((n) => n.id === connection.target)
|
||||
|
||||
@@ -20,7 +20,11 @@ import type {
|
||||
WorkflowState,
|
||||
WorkflowStore,
|
||||
} from '@/stores/workflows/workflow/types'
|
||||
import { generateLoopBlocks, generateParallelBlocks } from '@/stores/workflows/workflow/utils'
|
||||
import {
|
||||
generateLoopBlocks,
|
||||
generateParallelBlocks,
|
||||
wouldCreateCycle,
|
||||
} from '@/stores/workflows/workflow/utils'
|
||||
|
||||
const logger = createLogger('WorkflowStore')
|
||||
|
||||
@@ -428,6 +432,15 @@ export const useWorkflowStore = create<WorkflowStore>()(
|
||||
return
|
||||
}
|
||||
|
||||
// Prevent self-connections and cycles
|
||||
if (wouldCreateCycle(get().edges, edge.source, edge.target)) {
|
||||
logger.warn('Prevented edge that would create a cycle', {
|
||||
source: edge.source,
|
||||
target: edge.target,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Check for duplicate connections
|
||||
const isDuplicate = get().edges.some(
|
||||
(existingEdge) =>
|
||||
|
||||
@@ -1,7 +1,56 @@
|
||||
import type { Edge } from 'reactflow'
|
||||
import type { BlockState, Loop, Parallel } from '@/stores/workflows/workflow/types'
|
||||
|
||||
const DEFAULT_LOOP_ITERATIONS = 5
|
||||
|
||||
/**
|
||||
* Check if adding an edge would create a cycle in the graph.
|
||||
* Uses depth-first search to detect if the source node is reachable from the target node.
|
||||
*
|
||||
* @param edges - Current edges in the graph
|
||||
* @param sourceId - Source node ID of the proposed edge
|
||||
* @param targetId - Target node ID of the proposed edge
|
||||
* @returns true if adding this edge would create a cycle
|
||||
*/
|
||||
export function wouldCreateCycle(edges: Edge[], sourceId: string, targetId: string): boolean {
|
||||
if (sourceId === targetId) {
|
||||
return true
|
||||
}
|
||||
|
||||
const adjacencyList = new Map<string, string[]>()
|
||||
for (const edge of edges) {
|
||||
if (!adjacencyList.has(edge.source)) {
|
||||
adjacencyList.set(edge.source, [])
|
||||
}
|
||||
adjacencyList.get(edge.source)!.push(edge.target)
|
||||
}
|
||||
|
||||
const visited = new Set<string>()
|
||||
|
||||
function canReachSource(currentNode: string): boolean {
|
||||
if (currentNode === sourceId) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (visited.has(currentNode)) {
|
||||
return false
|
||||
}
|
||||
|
||||
visited.add(currentNode)
|
||||
|
||||
const neighbors = adjacencyList.get(currentNode) || []
|
||||
for (const neighbor of neighbors) {
|
||||
if (canReachSource(neighbor)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
return canReachSource(targetId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert UI loop block to executor Loop format
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user