(nodes: string[], edges: { source: string; target: string }[])
| 17 | import { validateWorkflowGraph } from './validator'; |
| 18 | |
| 19 | function topoSort(nodes: string[], edges: { source: string; target: string }[]): string[] { |
| 20 | const incoming = new Map<string, number>(); |
| 21 | const adjacency = new Map<string, string[]>(); |
| 22 | |
| 23 | nodes.forEach((id) => { |
| 24 | incoming.set(id, 0); |
| 25 | adjacency.set(id, []); |
| 26 | }); |
| 27 | |
| 28 | for (const edge of edges) { |
| 29 | if (!incoming.has(edge.target)) { |
| 30 | throw new Error(`Edge references unknown node ${edge.target}`); |
| 31 | } |
| 32 | if (!incoming.has(edge.source)) { |
| 33 | throw new Error(`Edge references unknown node ${edge.source}`); |
| 34 | } |
| 35 | incoming.set(edge.target, (incoming.get(edge.target) ?? 0) + 1); |
| 36 | adjacency.get(edge.source)?.push(edge.target); |
| 37 | } |
| 38 | |
| 39 | const queue: string[] = nodes.filter((id) => (incoming.get(id) ?? 0) === 0); |
| 40 | const result: string[] = []; |
| 41 | |
| 42 | while (queue.length > 0) { |
| 43 | const current = queue.shift()!; |
| 44 | result.push(current); |
| 45 | |
| 46 | for (const neighbor of adjacency.get(current) ?? []) { |
| 47 | incoming.set(neighbor, (incoming.get(neighbor) ?? 1) - 1); |
| 48 | if ((incoming.get(neighbor) ?? 0) === 0) { |
| 49 | queue.push(neighbor); |
| 50 | } |
| 51 | } |
| 52 | } |
| 53 | |
| 54 | if (result.length !== nodes.length) { |
| 55 | throw new Error('Workflow graph contains a cycle'); |
| 56 | } |
| 57 | |
| 58 | return result; |
| 59 | } |
| 60 | |
| 61 | export function compileWorkflowGraph(graph: WorkflowGraphDto): WorkflowDefinition { |
| 62 | // Filter out UI-only nodes (like text-block) that shouldn't be executed |
no test coverage detected