feat: Parallel Execution of Nodes in Workflows (#8192)

Co-authored-by: StyleZhang <jasonapring2015@outlook.com>
Co-authored-by: Yi <yxiaoisme@gmail.com>
Co-authored-by: -LAN- <laipz8200@outlook.com>
This commit is contained in:
takatost
2024-09-10 15:23:16 +08:00
committed by GitHub
parent 5da0182800
commit dabfd74622
156 changed files with 11158 additions and 5605 deletions

View File

@@ -16,6 +16,7 @@ import {
useReactFlow,
useStoreApi,
} from 'reactflow'
import { unionBy } from 'lodash-es'
import type { ToolDefaultValue } from '../block-selector/types'
import type {
Edge,
@@ -25,6 +26,7 @@ import type {
import { BlockEnum } from '../types'
import { useWorkflowStore } from '../store'
import {
CUSTOM_EDGE,
ITERATION_CHILDREN_Z_INDEX,
ITERATION_PADDING,
NODES_INITIAL_DATA,
@@ -40,6 +42,7 @@ import {
} from '../utils'
import { CUSTOM_NOTE_NODE } from '../note-node/constants'
import type { IterationNodeType } from '../nodes/iteration/types'
import { CUSTOM_ITERATION_START_NODE } from '../nodes/iteration-start/constants'
import type { VariableAssignerNodeType } from '../nodes/variable-assigner/types'
import { useNodeIterationInteractions } from '../nodes/iteration/use-interactions'
import { useWorkflowHistoryStore } from '../workflow-history-store'
@@ -60,6 +63,7 @@ export const useNodesInteractions = () => {
const { store: workflowHistoryStore } = useWorkflowHistoryStore()
const { handleSyncWorkflowDraft } = useNodesSyncDraft()
const {
checkNestedParallelLimit,
getAfterNodesInSameBranch,
} = useWorkflow()
const { getNodesReadOnly } = useNodesReadOnly()
@@ -79,7 +83,7 @@ export const useNodesInteractions = () => {
if (getNodesReadOnly())
return
if (node.data.isIterationStart || node.type === CUSTOM_NOTE_NODE)
if (node.type === CUSTOM_ITERATION_START_NODE || node.type === CUSTOM_NOTE_NODE)
return
dragNodeStartPosition.current = { x: node.position.x, y: node.position.y }
@@ -89,7 +93,7 @@ export const useNodesInteractions = () => {
if (getNodesReadOnly())
return
if (node.data.isIterationStart)
if (node.type === CUSTOM_ITERATION_START_NODE)
return
const {
@@ -156,7 +160,7 @@ export const useNodesInteractions = () => {
if (getNodesReadOnly())
return
if (node.type === CUSTOM_NOTE_NODE)
if (node.type === CUSTOM_NOTE_NODE || node.type === CUSTOM_ITERATION_START_NODE)
return
const {
@@ -207,13 +211,30 @@ export const useNodesInteractions = () => {
})
})
setEdges(newEdges)
const connectedEdges = getConnectedEdges([node], edges).filter(edge => edge.target === node.id)
const targetNodes: Node[] = []
for (let i = 0; i < connectedEdges.length; i++) {
const sourceConnectedEdges = getConnectedEdges([{ id: connectedEdges[i].source } as Node], edges).filter(edge => edge.source === connectedEdges[i].source && edge.sourceHandle === connectedEdges[i].sourceHandle)
targetNodes.push(...sourceConnectedEdges.map(edge => nodes.find(n => n.id === edge.target)!))
}
const uniqTargetNodes = unionBy(targetNodes, 'id')
if (uniqTargetNodes.length > 1) {
const newNodes = produce(nodes, (draft) => {
draft.forEach((n) => {
if (uniqTargetNodes.some(targetNode => n.id === targetNode.id))
n.data._inParallelHovering = true
})
})
setNodes(newNodes)
}
}, [store, workflowStore, getNodesReadOnly])
const handleNodeLeave = useCallback<NodeMouseHandler>((_, node) => {
if (getNodesReadOnly())
return
if (node.type === CUSTOM_NOTE_NODE)
if (node.type === CUSTOM_NOTE_NODE || node.type === CUSTOM_ITERATION_START_NODE)
return
const {
@@ -229,6 +250,7 @@ export const useNodesInteractions = () => {
const newNodes = produce(getNodes(), (draft) => {
draft.forEach((node) => {
node.data._isEntering = false
node.data._inParallelHovering = false
})
})
setNodes(newNodes)
@@ -287,6 +309,8 @@ export const useNodesInteractions = () => {
}, [store, handleSyncWorkflowDraft])
const handleNodeClick = useCallback<NodeMouseHandler>((_, node) => {
if (node.type === CUSTOM_ITERATION_START_NODE)
return
handleNodeSelect(node.id)
}, [handleNodeSelect])
@@ -314,25 +338,15 @@ export const useNodesInteractions = () => {
if (targetNode?.parentId !== sourceNode?.parentId)
return
if (targetNode?.data.isIterationStart)
return
if (sourceNode?.type === CUSTOM_NOTE_NODE || targetNode?.type === CUSTOM_NOTE_NODE)
return
const needDeleteEdges = edges.filter((edge) => {
if (
(edge.source === source && edge.sourceHandle === sourceHandle)
|| (edge.target === target && edge.targetHandle === targetHandle && targetNode?.data.type !== BlockEnum.VariableAssigner && targetNode?.data.type !== BlockEnum.VariableAggregator)
)
return true
if (edges.find(edge => edge.source === source && edge.sourceHandle === sourceHandle && edge.target === target && edge.targetHandle === targetHandle))
return
return false
})
const needDeleteEdgesIds = needDeleteEdges.map(edge => edge.id)
const newEdge = {
id: `${source}-${sourceHandle}-${target}-${targetHandle}`,
type: 'custom',
type: CUSTOM_EDGE,
source: source!,
target: target!,
sourceHandle,
@@ -347,7 +361,6 @@ export const useNodesInteractions = () => {
}
const nodesConnectedSourceOrTargetHandleIdsMap = getNodesConnectedSourceOrTargetHandleIdsMap(
[
...needDeleteEdges.map(edge => ({ type: 'remove', edge })),
{ type: 'add', edge: newEdge },
],
nodes,
@@ -362,19 +375,26 @@ export const useNodesInteractions = () => {
}
})
})
setNodes(newNodes)
const newEdges = produce(edges, (draft) => {
const filtered = draft.filter(edge => !needDeleteEdgesIds.includes(edge.id))
filtered.push(newEdge)
return filtered
draft.push(newEdge)
})
setEdges(newEdges)
handleSyncWorkflowDraft()
saveStateToHistory(WorkflowHistoryEvent.NodeConnect)
}, [getNodesReadOnly, store, handleSyncWorkflowDraft, saveStateToHistory])
if (checkNestedParallelLimit(newNodes, newEdges, targetNode?.parentId)) {
setNodes(newNodes)
setEdges(newEdges)
handleSyncWorkflowDraft()
saveStateToHistory(WorkflowHistoryEvent.NodeConnect)
}
else {
const {
setConnectingNodePayload,
setEnteringNodePayload,
} = workflowStore.getState()
setConnectingNodePayload(undefined)
setEnteringNodePayload(undefined)
}
}, [getNodesReadOnly, store, workflowStore, handleSyncWorkflowDraft, saveStateToHistory, checkNestedParallelLimit])
const handleNodeConnectStart = useCallback<OnConnectStart>((_, { nodeId, handleType, handleId }) => {
if (getNodesReadOnly())
@@ -393,14 +413,12 @@ export const useNodesInteractions = () => {
return
}
if (!node.data.isIterationStart) {
setConnectingNodePayload({
nodeId,
nodeType: node.data.type,
handleType,
handleId,
})
}
setConnectingNodePayload({
nodeId,
nodeType: node.data.type,
handleType,
handleId,
})
}
}, [store, workflowStore, getNodesReadOnly])
@@ -510,6 +528,12 @@ export const useNodesInteractions = () => {
return handleNodeDelete(nodeId)
}
else {
if (iterationChildren.length === 1) {
handleNodeDelete(iterationChildren[0].id)
handleNodeDelete(nodeId)
return
}
const { setShowConfirm, showConfirm } = workflowStore.getState()
if (!showConfirm) {
@@ -541,14 +565,8 @@ export const useNodesInteractions = () => {
}
}
if (node.id === currentNode.parentId) {
if (node.id === currentNode.parentId)
node.data._children = node.data._children?.filter(child => child !== nodeId)
if (currentNode.id === (node as Node<IterationNodeType>).data.start_node_id) {
(node as Node<IterationNodeType>).data.start_node_id = '';
(node as Node<IterationNodeType>).data.startNodeType = undefined
}
}
})
draft.splice(currentNodeIndex, 1)
})
@@ -559,7 +577,7 @@ export const useNodesInteractions = () => {
setEdges(newEdges)
handleSyncWorkflowDraft()
if (currentNode.type === 'custom-note')
if (currentNode.type === CUSTOM_NOTE_NODE)
saveStateToHistory(WorkflowHistoryEvent.NoteDelete)
else
@@ -591,7 +609,10 @@ export const useNodesInteractions = () => {
} = store.getState()
const nodes = getNodes()
const nodesWithSameType = nodes.filter(node => node.data.type === nodeType)
const newNode = generateNewNode({
const {
newNode,
newIterationStartNode,
} = generateNewNode({
data: {
...NODES_INITIAL_DATA[nodeType],
title: nodesWithSameType.length > 0 ? `${t(`workflow.blocks.${nodeType}`)} ${nodesWithSameType.length + 1}` : t(`workflow.blocks.${nodeType}`),
@@ -627,7 +648,7 @@ export const useNodesInteractions = () => {
const newEdge: Edge = {
id: `${prevNodeId}-${prevNodeSourceHandle}-${newNode.id}-${targetHandle}`,
type: 'custom',
type: CUSTOM_EDGE,
source: prevNodeId,
sourceHandle: prevNodeSourceHandle,
target: newNode.id,
@@ -662,8 +683,10 @@ export const useNodesInteractions = () => {
node.data._children?.push(newNode.id)
})
draft.push(newNode)
if (newIterationStartNode)
draft.push(newIterationStartNode)
})
setNodes(newNodes)
if (newNode.data.type === BlockEnum.VariableAssigner || newNode.data.type === BlockEnum.VariableAggregator) {
const { setShowAssignVariablePopup } = workflowStore.getState()
@@ -687,7 +710,14 @@ export const useNodesInteractions = () => {
})
draft.push(newEdge)
})
setEdges(newEdges)
if (checkNestedParallelLimit(newNodes, newEdges, prevNode.parentId)) {
setNodes(newNodes)
setEdges(newEdges)
}
else {
return false
}
}
if (!prevNodeId && nextNodeId) {
const nextNodeIndex = nodes.findIndex(node => node.id === nextNodeId)
@@ -706,15 +736,13 @@ export const useNodesInteractions = () => {
newNode.data.iteration_id = nextNode.parentId
newNode.zIndex = ITERATION_CHILDREN_Z_INDEX
}
if (nextNode.data.isIterationStart)
newNode.data.isIterationStart = true
let newEdge
if ((nodeType !== BlockEnum.IfElse) && (nodeType !== BlockEnum.QuestionClassifier)) {
newEdge = {
id: `${newNode.id}-${sourceHandle}-${nextNodeId}-${nextNodeTargetHandle}`,
type: 'custom',
type: CUSTOM_EDGE,
source: newNode.id,
sourceHandle,
target: nextNodeId,
@@ -763,13 +791,11 @@ export const useNodesInteractions = () => {
node.data.start_node_id = newNode.id
node.data.startNodeType = newNode.data.type
}
if (node.id === nextNodeId && node.data.isIterationStart)
node.data.isIterationStart = false
})
draft.push(newNode)
if (newIterationStartNode)
draft.push(newIterationStartNode)
})
setNodes(newNodes)
if (newEdge) {
const newEdges = produce(edges, (draft) => {
draft.forEach((item) => {
@@ -780,7 +806,21 @@ export const useNodesInteractions = () => {
})
draft.push(newEdge)
})
setEdges(newEdges)
if (checkNestedParallelLimit(newNodes, newEdges, nextNode.parentId)) {
setNodes(newNodes)
setEdges(newEdges)
}
else {
return false
}
}
else {
if (checkNestedParallelLimit(newNodes, edges))
setNodes(newNodes)
else
return false
}
}
if (prevNodeId && nextNodeId) {
@@ -804,7 +844,7 @@ export const useNodesInteractions = () => {
const currentEdgeIndex = edges.findIndex(edge => edge.source === prevNodeId && edge.target === nextNodeId)
const newPrevEdge = {
id: `${prevNodeId}-${prevNodeSourceHandle}-${newNode.id}-${targetHandle}`,
type: 'custom',
type: CUSTOM_EDGE,
source: prevNodeId,
sourceHandle: prevNodeSourceHandle,
target: newNode.id,
@@ -822,7 +862,7 @@ export const useNodesInteractions = () => {
if (nodeType !== BlockEnum.IfElse && nodeType !== BlockEnum.QuestionClassifier) {
newNextEdge = {
id: `${newNode.id}-${sourceHandle}-${nextNodeId}-${nextNodeTargetHandle}`,
type: 'custom',
type: CUSTOM_EDGE,
source: newNode.id,
sourceHandle,
target: nextNodeId,
@@ -865,6 +905,8 @@ export const useNodesInteractions = () => {
node.data._children?.push(newNode.id)
})
draft.push(newNode)
if (newIterationStartNode)
draft.push(newIterationStartNode)
})
setNodes(newNodes)
if (newNode.data.type === BlockEnum.VariableAssigner || newNode.data.type === BlockEnum.VariableAggregator) {
@@ -898,7 +940,7 @@ export const useNodesInteractions = () => {
}
handleSyncWorkflowDraft()
saveStateToHistory(WorkflowHistoryEvent.NodeAdd)
}, [getNodesReadOnly, store, t, handleSyncWorkflowDraft, saveStateToHistory, workflowStore, getAfterNodesInSameBranch])
}, [getNodesReadOnly, store, t, handleSyncWorkflowDraft, saveStateToHistory, workflowStore, getAfterNodesInSameBranch, checkNestedParallelLimit])
const handleNodeChange = useCallback((
currentNodeId: string,
@@ -919,7 +961,10 @@ export const useNodesInteractions = () => {
const currentNode = nodes.find(node => node.id === currentNodeId)!
const connectedEdges = getConnectedEdges([currentNode], edges)
const nodesWithSameType = nodes.filter(node => node.data.type === nodeType)
const newCurrentNode = generateNewNode({
const {
newNode: newCurrentNode,
newIterationStartNode,
} = generateNewNode({
data: {
...NODES_INITIAL_DATA[nodeType],
title: nodesWithSameType.length > 0 ? `${t(`workflow.blocks.${nodeType}`)} ${nodesWithSameType.length + 1}` : t(`workflow.blocks.${nodeType}`),
@@ -929,7 +974,6 @@ export const useNodesInteractions = () => {
selected: currentNode.data.selected,
isInIteration: currentNode.data.isInIteration,
iteration_id: currentNode.data.iteration_id,
isIterationStart: currentNode.data.isIterationStart,
},
position: {
x: currentNode.position.x,
@@ -955,18 +999,12 @@ export const useNodesInteractions = () => {
...nodesConnectedSourceOrTargetHandleIdsMap[node.id],
}
}
if (node.id === currentNode.parentId && currentNode.data.isIterationStart) {
node.data._children = [
newCurrentNode.id,
...(node.data._children || []),
].filter(child => child !== currentNodeId)
node.data.start_node_id = newCurrentNode.id
node.data.startNodeType = newCurrentNode.data.type
}
})
const index = draft.findIndex(node => node.id === currentNodeId)
draft.splice(index, 1, newCurrentNode)
if (newIterationStartNode)
draft.push(newIterationStartNode)
})
setNodes(newNodes)
const newEdges = produce(edges, (draft) => {
@@ -1011,7 +1049,7 @@ export const useNodesInteractions = () => {
}, [store])
const handleNodeContextMenu = useCallback((e: MouseEvent, node: Node) => {
if (node.type === CUSTOM_NOTE_NODE)
if (node.type === CUSTOM_NOTE_NODE || node.type === CUSTOM_ITERATION_START_NODE)
return
e.preventDefault()
@@ -1041,7 +1079,7 @@ export const useNodesInteractions = () => {
if (nodeId) {
// If nodeId is provided, copy that specific node
const nodeToCopy = nodes.find(node => node.id === nodeId && node.data.type !== BlockEnum.Start)
const nodeToCopy = nodes.find(node => node.id === nodeId && node.data.type !== BlockEnum.Start && node.type !== CUSTOM_ITERATION_START_NODE)
if (nodeToCopy)
setClipboardElements([nodeToCopy])
}
@@ -1087,7 +1125,10 @@ export const useNodesInteractions = () => {
clipboardElements.forEach((nodeToPaste, index) => {
const nodeType = nodeToPaste.data.type
const newNode = generateNewNode({
const {
newNode,
newIterationStartNode,
} = generateNewNode({
type: nodeToPaste.type,
data: {
...NODES_INITIAL_DATA[nodeType],
@@ -1106,24 +1147,17 @@ export const useNodesInteractions = () => {
zIndex: nodeToPaste.zIndex,
})
newNode.id = newNode.id + index
// If only the iteration start node is copied, remove the isIterationStart flag
// This new node is movable and can be placed anywhere
if (clipboardElements.length === 1 && newNode.data.isIterationStart)
newNode.data.isIterationStart = false
let newChildren: Node[] = []
if (nodeToPaste.data.type === BlockEnum.Iteration) {
newNode.data._children = [];
(newNode.data as IterationNodeType).start_node_id = ''
newIterationStartNode!.parentId = newNode.id;
(newNode.data as IterationNodeType).start_node_id = newIterationStartNode!.id
newChildren = handleNodeIterationChildrenCopy(nodeToPaste.id, newNode.id)
newChildren.forEach((child) => {
newNode.data._children?.push(child.id)
if (child.data.isIterationStart)
(newNode.data as IterationNodeType).start_node_id = child.id
})
newChildren.push(newIterationStartNode!)
}
nodesToPaste.push(newNode)
@@ -1230,6 +1264,42 @@ export const useNodesInteractions = () => {
saveStateToHistory(WorkflowHistoryEvent.NodeResize)
}, [getNodesReadOnly, store, handleSyncWorkflowDraft, saveStateToHistory])
const handleNodeDisconnect = useCallback((nodeId: string) => {
if (getNodesReadOnly())
return
const {
getNodes,
setNodes,
edges,
setEdges,
} = store.getState()
const nodes = getNodes()
const currentNode = nodes.find(node => node.id === nodeId)!
const connectedEdges = getConnectedEdges([currentNode], edges)
const nodesConnectedSourceOrTargetHandleIdsMap = getNodesConnectedSourceOrTargetHandleIdsMap(
connectedEdges.map(edge => ({ type: 'remove', edge })),
nodes,
)
const newNodes = produce(nodes, (draft: Node[]) => {
draft.forEach((node) => {
if (nodesConnectedSourceOrTargetHandleIdsMap[node.id]) {
node.data = {
...node.data,
...nodesConnectedSourceOrTargetHandleIdsMap[node.id],
}
}
})
})
setNodes(newNodes)
const newEdges = produce(edges, (draft) => {
return draft.filter(edge => !connectedEdges.find(connectedEdge => connectedEdge.id === edge.id))
})
setEdges(newEdges)
handleSyncWorkflowDraft()
saveStateToHistory(WorkflowHistoryEvent.EdgeDelete)
}, [store, getNodesReadOnly, handleSyncWorkflowDraft, saveStateToHistory])
const handleHistoryBack = useCallback(() => {
if (getNodesReadOnly() || getWorkflowReadOnly())
return
@@ -1282,6 +1352,7 @@ export const useNodesInteractions = () => {
handleNodesDuplicate,
handleNodesDelete,
handleNodeResize,
handleNodeDisconnect,
handleHistoryBack,
handleHistoryForward,
}