ui-nyla/src/components/FlowEditor/nodes/shared/dataFlowGraph.ts
2026-04-07 00:49:12 +02:00

97 lines
2.7 KiB
TypeScript

/**
* 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<string, string[]> {
const rev: Record<string, string[]> = {};
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<string>();
const queue = [currentNodeId];
const visited = new Set<string>([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<string, string[]> = {};
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<string>();
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);
}