Files
app/frontend/src/components/Flow/deriveEdges.ts
T
stroblmeandClaude Opus 5 2abeae7f9c Flow inputs and outputs are visible and editable in the UI
A flow's inputs are the messages it takes from outside — a dashboard control,
a run, the API — and its outputs are what a batch run reports. Both existed in
the document and in the engine, and neither had any UI: the values looked
hard-coded on the canvas and the Run button always used the declared defaults.

The canvas now draws each as a labelled endpoint, the way it already draws a
dashboard tile or another flow, skipping an input something else already
accounts for. The flow panel edits them — mode, name, type, starting value,
and for a live flow the value it currently holds with a way to put a new one
in. Pressing Run on a batch flow asks for its parameters first.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NUb8YpL2s3gmN9WTACTt4q
2026-08-20 18:10:16 +02:00

119 lines
3.6 KiB
TypeScript

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(";")
}