import type { FlowModule } from '$lib/gen' import { type Node, type Edge } from '@xyflow/svelte' import { getDependeeAndDependentComponents } from '../flows/flowExplorer' import { dfsByModule } from '../flows/previousResults' import { defaultIfEmptyString } from '$lib/utils' import type { GraphModuleState } from './model' export type GraphEventHandlers = { insert: (detail) => void deleteBranch: (detail, label: string) => void select: (modId: string) => void delete: (detail, label: string) => void newBranch: (module: FlowModule) => void move: (module: FlowModule, modules: FlowModule[]) => void selectedIteration: (detail, moduleId: string) => void changeId: (newId: string) => void addSchedulePoll: () => void removeSchedulePoll: () => void } export default function graphBuilder( modules: FlowModule[] | undefined, extra: Record, failureModule: FlowModule | undefined, preprocessorModule: FlowModule | undefined, eventHandlers: GraphEventHandlers, success: boolean | undefined, useDataflow: boolean | undefined, selectedId: string | undefined, moving: string | undefined, triggerProps?: { path?: string openSchedules?: () => void triggerDetail?: (e) => void isEditor?: boolean }, hasSchedulePoll?: boolean ): { nodes: Node[] edges: Edge[] error?: string | undefined } { const nodes: Node[] = [] const edges: Edge[] = [] try { if (!modules) { return { nodes, edges } } function addNode(module: FlowModule, offset: number, type: string, subModules?: FlowModule[]) { if (nodes.some((n) => n.id === module.id)) { throw new Error(`Duplicated node detected: ${module.id}`) } nodes.push({ id: module.id, data: { value: module.value, offset: offset, module: module, modules: subModules ?? modules, parentIds: [], eventHandlers: eventHandlers, moving: moving, ...extra }, position: { x: -1, y: -1 }, type: type }) return module.id } const parents: { [key: string]: string[] } = {} // function detectCycle(nodeId: string, visited: Set, currentPath: Set): boolean { // If the node hasn't been visited yet if (!visited.has(nodeId)) { visited.add(nodeId) currentPath.add(nodeId) // If the current node has parent nodes if (parents[nodeId]) { // Check each parent node // Nodes can have multiple parents: the node that gathers the result for branches or loops for instance for (const parentNode of parents[nodeId]) { // If the parentNode hasn't been visited and a cycle is detected in its path if (!visited.has(parentNode) && detectCycle(parentNode, visited, currentPath)) { return true // Cycle detected } // If the parentNode is already in the current path, it's a cycle else if (currentPath.has(parentNode)) { return true // Cycle detected } } } } // Remove the node from the current path as we're done processing it currentPath.delete(nodeId) // No cycle detected for this path return false } function addEdge( sourceId: string, targetId: string, options?: { customId?: string type?: string subModules?: FlowModule[] disableMoveIds?: string[] } ) { parents[targetId] = [...(parents[targetId] ?? []), sourceId] const mods = options?.subModules ?? modules let index = mods?.findIndex((m) => m.id === targetId) ?? -1 const visited = new Set() const recStack = new Set() if (detectCycle(sourceId, visited, recStack)) { throw new Error( `Cycle detected: adding edge from '${sourceId}' to '${targetId}' would create a cycle.` ) } edges.push({ id: options?.customId || `edge:${sourceId}->${targetId}`, source: sourceId, target: targetId, type: options?.type ?? 'edge', data: { insertable: extra.insertable, modules: options?.subModules ?? modules, sourceId, targetId, moving, eventHandlers, disableMoveIds: options?.disableMoveIds, enableTrigger: sourceId === 'Input', // If the index is -1, it means that the target module is not in the modules array, so we set it to the length of the array index: index >= 0 ? index : mods?.length ?? 0, ...extra } }) } const inputNode: Node = { id: 'Input', position: { x: -1, y: -1 }, type: 'input2', data: { eventHandlers: eventHandlers, modules: modules, hasPreprocessor: !!preprocessorModule, ...extra } } if (extra.path) { if (hasSchedulePoll) { const schedulePollNode: Node = { id: 'SchedulePoll', position: { x: -1, y: -1 }, type: 'schedulePoll', data: { eventHandlers: eventHandlers, modules: modules, ...extra } } nodes.push(schedulePollNode) if (!!preprocessorModule) { addEdge('SchedulePoll', preprocessorModule.id, { type: 'empty' }) } else { addEdge('SchedulePoll', 'Input', { type: 'empty' }) } } else { const triggerNode: Node = { id: 'Trigger', position: { x: -1, y: -1 }, type: 'trigger', data: { path: triggerProps?.path, openSchedules: triggerProps?.openSchedules, triggerDetail: triggerProps?.triggerDetail, isEditor: triggerProps?.isEditor, newFlow: extra.newFlow, eventHandlers: eventHandlers, modules: modules, ...extra } } nodes.push(triggerNode) if (!!preprocessorModule) { addEdge('Trigger', preprocessorModule.id, { type: 'empty' }) } else { addEdge('Trigger', 'Input', { type: 'empty' }) } } } const resultNode: Node = { id: 'result', data: { eventHandlers: eventHandlers, modules: modules, success: success, ...extra }, position: { x: -1, y: -1 }, type: 'result' } nodes.push(inputNode) nodes.push(resultNode) function processModules( modules: FlowModule[], beforeNode: Node, nextNode: Node, currentOffset = 0, disableMoveIds: string[] = [], parentIndex?: string ) { let previousId: string | undefined = undefined if (modules.length === 0) { addEdge(beforeNode.id, nextNode.id, { subModules: modules, disableMoveIds }) } else { modules.forEach((module, index) => { const localDisableMoveIds = [...disableMoveIds, module.id] // Add the edge between the previous node and the current one if (index > 0 && previousId) { addEdge(previousId, module.id, { subModules: modules, disableMoveIds }) } if (module.value.type === 'branchall') { // Start addNode(module, currentOffset, 'module', modules) // "Collect result of each branch" node const endNode = { id: `${module.id}-end`, data: { offset: currentOffset, id: module.id, module: module, modules: modules, ...extra }, position: { x: -1, y: -1 }, type: 'branchAllEnd' } nodes.push(endNode) if (module.value.branches.length === 0) { // Add a "No branches" node const startNode = { id: `${module.id}-branch-0`, data: { offset: currentOffset, id: module.id, branchIndex: -1, modules: modules, eventHandlers: eventHandlers, ...extra }, position: { x: -1, y: -1 }, type: 'noBranch' } nodes.push(startNode) addEdge(module.id, startNode.id, { type: 'empty' }) addEdge(startNode.id, endNode.id, { type: 'empty' }) } else { module.value.branches.forEach((branch, branchIndex) => { // Start node by branch const startNode = { id: `${module.id}-branch-${branchIndex}`, data: { offset: currentOffset, label: defaultIfEmptyString(branch.summary, `Branch ${branchIndex + 1}`), id: module.id, branchIndex: branchIndex, modules: modules, eventHandlers: eventHandlers, ...extra }, position: { x: -1, y: -1 }, type: 'branchAllStart' } nodes.push(startNode) addEdge(module.id, startNode.id, { type: 'empty' }) processModules( branch.modules, startNode, endNode, currentOffset, localDisableMoveIds, parentIndex ? `${parentIndex}-${index}-${branchIndex}` : `${index}-${branchIndex}` ) }) } previousId = endNode.id } else if (module.value.type === 'forloopflow') { addNode(module, currentOffset, 'module', modules) const startNode = { id: `${module.id}-start`, data: { offset: currentOffset + 25, id: module.id, module: module, modules: modules, eventHandlers: eventHandlers, ...extra }, position: { x: -1, y: -1 }, type: 'forLoopStart' } addEdge(module.id, startNode.id, { type: 'empty' }) const endNode = { id: `${module.id}-end`, data: { offset: currentOffset, id: module.id, module: module, modules: modules, eventHandlers: eventHandlers, ...extra }, position: { x: -1, y: -1 }, type: 'forLoopEnd' } nodes.push(startNode) nodes.push(endNode) const selectedIterIndex = extra.flowModuleStates?.[module.id]?.selectedForloopIndex processModules( module.value.modules, startNode, endNode, currentOffset + 25, localDisableMoveIds, parentIndex ? `${parentIndex}-${index}-${selectedIterIndex ?? '?'}` : `${index}-${selectedIterIndex ?? '?'}` ) previousId = endNode.id } else if (module.value.type === 'whileloopflow') { addNode(module, currentOffset, 'module', modules) const startNode = { id: `${module.id}-start`, data: { offset: currentOffset + 25, module: module, modules: modules, eventHandlers: eventHandlers, ...extra }, position: { x: -1, y: -1 }, type: 'whileLoopStart' } addEdge(module.id, startNode.id, { type: 'empty' }) const endNode = { id: `${module.id}-end`, data: { offset: currentOffset, module: module, modules: modules, ...extra }, position: { x: -1, y: -1 }, type: 'whileLoopEnd' } nodes.push(startNode) nodes.push(endNode) const selectedIterIndex = extra.flowModuleStates?.[module.id]?.selectedForloopIndex processModules( module.value.modules, startNode, endNode, currentOffset + 25, localDisableMoveIds, parentIndex ? `${parentIndex}-${index}-${selectedIterIndex ?? '?'}` : `${index}-${selectedIterIndex ?? '?'}` ) previousId = endNode.id } else if (module.value.type === 'branchone') { addNode(module, currentOffset, 'module', modules) const endNode = { id: `${module.id}-end`, data: { offset: currentOffset, eventHandlers: eventHandlers }, position: { x: -1, y: -1 }, type: 'branchOneEnd' } nodes.push(endNode) // Add default branch const defaultBranch = { id: `${module.id}-default`, data: { offset: currentOffset, label: 'Default', id: module.id, branchIndex: -1, modules: module.value.default, eventHandlers: eventHandlers, branchOne: true, ...extra }, position: { x: -1, y: -1 }, type: 'noBranch' } nodes.push(defaultBranch) addEdge(module.id, defaultBranch.id, { type: 'empty' }) processModules( module.value.default, defaultBranch, endNode, currentOffset, localDisableMoveIds, parentIndex ? `${parentIndex}-${index}` : index.toString() ) module.value.branches.forEach((branch, branchIndex) => { // Start node by branch const startNode = { id: `${module.id}-branch-${branchIndex}`, data: { offset: currentOffset, label: defaultIfEmptyString(branch.summary, 'Branch ' + (branchIndex + 1)), preLabel: branch.summary ? '' : branch.expr, id: module.id, branchIndex: branchIndex, modules: modules, eventHandlers: eventHandlers, ...extra }, position: { x: -1, y: -1 }, type: 'branchOneStart' } nodes.push(startNode) addEdge(module.id, startNode.id, { type: 'empty' }) processModules( branch.modules, startNode, endNode, currentOffset, localDisableMoveIds, parentIndex ? `${parentIndex}-${index}` : index.toString() ) }) previousId = endNode.id } else { addNode(module, currentOffset, 'module', modules) previousId = module.id } if (index === 0) { addEdge(beforeNode.id, module.id, { subModules: modules, disableMoveIds }) } if (index === modules.length - 1 && previousId) { addEdge(previousId, nextNode.id, { subModules: modules, disableMoveIds }) } }) if (failureModule) { const id = parentIndex ? `failure-${parentIndex}` : 'failure' const failureState = extra.flowModuleStates?.[id] as GraphModuleState | undefined if (failureState && failureState.parent_module) { addNode( { ...failureModule, id: id }, 0, 'module' ) addEdge(failureState.parent_module, id, { type: 'empty' }) } } } } processModules(modules, inputNode, resultNode) if (preprocessorModule) { addNode(preprocessorModule, 0, 'module') const id = JSON.parse(JSON.stringify(preprocessorModule.id)) addEdge(id, 'Input', { type: 'empty' }) } if (failureModule && !extra.flowModuleStates) { addNode(failureModule, 0, 'module') } Object.keys(parents).forEach((key) => { const node = nodes.find((n) => n.id === key) if (node) { node.data.parentIds = parents[key] } }) if (useDataflow && selectedId) { let deps = getDependeeAndDependentComponents(selectedId, modules ?? [], failureModule) if (deps) { Object.entries(deps.dependees).forEach((x, i) => { const inputs = x[1] inputs?.forEach((input, index) => { let pid = x[0] if (input?.startsWith('flow_input.iter')) { const parent = dfsByModule(selectedId!, modules ?? [])?.pop() if (parent?.id) { pid = parent.id } } addEdge(pid, selectedId!, { customId: `dep-${pid}-${selectedId}-${input}-${index}`, type: 'dataflowedge' }) }) }) Object.entries(deps.dependents).forEach((x, i) => { let pid = x[0] addEdge(selectedId!, pid, { customId: `dep-${selectedId}-${pid}-${i}`, type: 'dataflowedge' }) }) } } return { nodes, edges } } catch (e) { return { nodes: [], edges: [], error: e } } }