97 lines
2.7 KiB
TypeScript
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);
|
|
}
|