import type { Edge } from "@xyflow/react" import type { MessageSpec, NodeDef_Input } from "@/client" /** Resolve a message name against its flow: bare names belong to this flow. */ export function qualify(flow: string, name: string): string { if (!name) return "" return name.includes(".") ? name : `${flow}.${name}` } /** The flow a qualified message belongs to. */ export function flowOf(message: string): string { return message.split(".", 1)[0] } /** How a message reads inside its own flow. */ export function displayName(flow: string, message: string): string { return message.startsWith(`${flow}.`) ? message.slice(flow.length + 1) : message } export function portOf(spec: MessageSpec): string { return spec.port || spec.name?.split(".").pop() || "" } export type FlowEdgeData = { message: string flow: string /** Whose publication this edge represents, so only it pulses. */ producerId: string /** The producing port publishes repeatedly during one run, not once at its end. */ stream?: boolean [key: string]: unknown } /** * Turn name bindings into canvas edges. * * Nodes never point at each other: a node declares the messages it consumes, * and every node providing that message is upstream of it. Two producers of one * message therefore draw two edges converging on the same input. */ export function deriveEdges(nodes: NodeDef_Input[], flow: string): Edge[] { const producers = new Map< string, { node: string; port: string; stream?: boolean }[] >() for (const node of nodes) { for (const spec of node.provides ?? []) { const message = qualify(flow, spec.name ?? "") if (!message) continue const list = producers.get(message) ?? [] list.push({ node: node.id, port: portOf(spec), stream: spec.stream }) producers.set(message, list) } } const edges: Edge[] = [] for (const node of nodes) { for (const spec of node.requires ?? []) { const message = qualify(flow, spec.name ?? "") if (!message) continue const targetPort = portOf(spec) for (const producer of producers.get(message) ?? []) { if (producer.node === node.id) continue edges.push({ id: `${producer.node}:${producer.port}->${node.id}:${targetPort}`, source: producer.node, sourceHandle: producer.port, target: node.id, targetHandle: targetPort, type: "live", data: { message, flow, producerId: `${flow}.${producer.node}`, stream: producer.stream, } satisfies FlowEdgeData, }) } } } return edges } /** * A cheap fingerprint of the flow's boundary — what it takes from outside and, * for a batch flow, what it reports — since those are drawn like wiring too. */ export function boundaryKey(doc: { mode?: string | null inputs?: { spec?: MessageSpec | null }[] | null outputs?: string[] | null }): string { const inputs = (doc.inputs ?? []) .map((one) => `${one.spec?.name ?? ""}:${one.spec?.dtype ?? ""}`) .join(",") return `${doc.mode ?? "live"}|${inputs}|${(doc.outputs ?? []).join(",")}` } /** * A cheap fingerprint of everything edges depend on, so dragging a node (which * replaces the array every frame) does not re-derive them. */ export function bindingsKey(nodes: NodeDef_Input[]): string { return nodes .map( (node) => `${node.id}|${(node.requires ?? []) .map((s) => `${portOf(s)}=${s.name ?? ""}`) .join(",")}|${(node.provides ?? []) .map((s) => `${portOf(s)}=${s.name ?? ""}${s.stream ? "*" : ""}`) .join(",")}`, ) .join(";") }