Files
app/frontend/src/components/Flow/deriveEdges.ts
T
stroblmeandClaude Opus 5 a00b4eaec2 Flow canvas: mark streaming edges with a marching dash
A port with `stream` set publishes repeatedly during one execution, and
until now nothing on the canvas said so. `deriveEdges` carries the flag
from the producing `MessageSpec` onto the edge, `LiveEdge` turns it into
an `edge-stream` class, and the class draws a dashed stroke whose offset
marches toward the target. The pulse still lands on top: both animations
share one declaration when a value arrives on a streaming edge.

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

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