/** * Automation2 Flow Editor - Graph helpers for data flow (ancestors, topo order). */ import type { CanvasNode, CanvasConnection } from '../../editor/FlowCanvas'; /** Build reverse adjacency: targetId -> sourceId[] */ function buildReverseAdjacency(connections: CanvasConnection[]): Record { const rev: Record = {}; for (const c of connections) { if (!rev[c.targetId]) rev[c.targetId] = []; rev[c.targetId].push(c.sourceId); } return rev; } /** BFS backward from node to collect all ancestor node IDs */ export function getAncestorNodeIds( currentNodeId: string, nodes: CanvasNode[], connections: CanvasConnection[] ): string[] { const nodeIds = new Set(nodes.map((n) => n.id)); const rev = buildReverseAdjacency(connections); const result = new Set(); const queue = [currentNodeId]; const visited = new Set([currentNodeId]); while (queue.length > 0) { const nid = queue.shift()!; const sources = rev[nid] ?? []; for (const src of sources) { if (!visited.has(src) && nodeIds.has(src)) { visited.add(src); result.add(src); queue.push(src); } } } return Array.from(result); } /** Topological order: triggers first, then BFS by connections (mirrors backend topoSort) */ export function topologicalOrder( nodes: CanvasNode[], connections: CanvasConnection[] ): CanvasNode[] { const nodeById = new Map(nodes.map((n) => [n.id, n])); const triggers = nodes.filter((n) => n.type.startsWith('trigger.')); if (triggers.length === 0) return [...nodes]; const fwd: Record = {}; for (const c of connections) { if (!fwd[c.sourceId]) fwd[c.sourceId] = []; fwd[c.sourceId].push(c.targetId); } const order: CanvasNode[] = []; const visited = new Set(); const q = [...triggers.map((t) => t.id)]; for (const tid of triggers.map((t) => t.id)) { if (!visited.has(tid)) { visited.add(tid); const n = nodeById.get(tid); if (n) order.push(n); } } let i = 0; while (i < q.length) { const nid = q[i++]; const targets = fwd[nid] ?? []; for (const tgt of targets) { if (!visited.has(tgt)) { visited.add(tgt); q.push(tgt); const n = nodeById.get(tgt); if (n) order.push(n); } } } for (const n of nodes) { if (n.id && !visited.has(n.id)) order.push(n); } return order; } /** Node IDs that are valid sources for the current node (ancestors in DAG) */ export function getAvailableSources( currentNodeId: string, nodes: CanvasNode[], connections: CanvasConnection[] ): string[] { return getAncestorNodeIds(currentNodeId, nodes, connections); }