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