/** * Pure graph-mutation helpers for the workflow editor. * * The editor canvas is controlled — every mutation produces a NEW * {@link WorkflowGraph} (never mutates the input) which the consumer feeds * back through `onGraphChange`. Kept framework-free and side-effect-free so * they're trivially unit-testable and reusable outside React. */ import type { WfEdge, WorkflowGraph } from './types'; const DEFAULT_CONNECTION_TYPE = 'main'; /** A minimal xyflow `Connection`-like shape (avoids an @xyflow/react import). */ export interface GraphConnection { source: string; target: string; sourceHandle?: string | null; targetHandle?: string | null; } /** Parse a handle id ("0", "1", null, "") into its numeric index (default 0). */ function handleIndex(handle: string | null | undefined): number { if (handle === null || handle === undefined || handle === '') return 0; const n = Number.parseInt(handle, 10); return Number.isNaN(n) ? 0 : n; } /** True when two edges connect the same source-output → target-input. */ function sameEdge(a: WfEdge, b: WfEdge): boolean { return ( a.source === b.source && a.target === b.target && (a.sourceOutput ?? 0) === (b.sourceOutput ?? 0) && (a.targetInput ?? 0) === (b.targetInput ?? 0) ); } /** * Map an xyflow {@link GraphConnection} (handle ids are output/input index * strings) to a logical {@link WfEdge}. */ export function connectionToEdge(conn: GraphConnection): WfEdge { return { source: conn.source, target: conn.target, sourceOutput: handleIndex(conn.sourceHandle), targetInput: handleIndex(conn.targetHandle), type: DEFAULT_CONNECTION_TYPE, }; } /** Append an edge, ignoring exact duplicates. Returns a new graph. */ export function addEdgeToGraph( graph: WorkflowGraph, edge: WfEdge ): WorkflowGraph { if (graph.edges.some((e) => sameEdge(e, edge))) return graph; return { ...graph, edges: [...graph.edges, edge] }; } /** * Remove the given nodes AND every edge incident to them (as source or * target). Returns a new graph. */ export function removeNodesFromGraph( graph: WorkflowGraph, nodeIds: string[] ): WorkflowGraph { const dropped = new Set(nodeIds); return { ...graph, nodes: graph.nodes.filter((n) => !dropped.has(n.id)), edges: graph.edges.filter( (e) => !dropped.has(e.source) && !dropped.has(e.target) ), }; } /** Remove the given edges (matched by source/target/output/input). New graph. */ export function removeEdgesFromGraph( graph: WorkflowGraph, edges: WfEdge[] ): WorkflowGraph { return { ...graph, edges: graph.edges.filter((e) => !edges.some((d) => sameEdge(e, d))), }; }