export type DefinitionGraphPosition = { x: number; y: number; }; export type DefinitionGraphPort = { id: string; label: string; required: boolean; multiple: boolean; minimum_connections: number; }; export type DefinitionGraphConfigField = { id: string; label: string; kind: string; required: boolean; description?: string | null; options: [string, string][]; }; export type DefinitionGraphNodeType = { type: string; category: string; category_label: string; label: string; description: string; icon: string; input_ports: DefinitionGraphPort[]; output_ports: DefinitionGraphPort[]; config_fields: DefinitionGraphConfigField[]; default_config: Record; metadata?: Record; }; export type DefinitionGraphNode = { id: string; type: string; label: string; position: DefinitionGraphPosition; config: Record; }; export type DefinitionGraphEdge = { id: string; source: string; target: string; source_port?: string; target_port?: string; }; export type DefinitionGraph = { schema_version: number; nodes: DefinitionGraphNode[]; edges: DefinitionGraphEdge[]; }; export type DefinitionGraphConnection = { source: string; target: string; sourcePort?: string | null; targetPort?: string | null; }; export function createDefinitionGraphNode( type: string, position: DefinitionGraphPosition, library: DefinitionGraphNodeType[] ): TNode { const definition = library.find((item) => item.type === type); return { id: `${type.replace(/\./g, "-")}-${crypto.randomUUID()}`, type, label: definition?.label ?? type, position, config: structuredClone(definition?.default_config ?? {}) } as TNode; } export function definitionConnectionError( graph: DefinitionGraph, library: DefinitionGraphNodeType[], connection: DefinitionGraphConnection, options: { allowCycles?: boolean } = {} ): string | null { if (!connection.source || !connection.target) return "Both endpoints are required."; if (connection.source === connection.target) return "A node cannot connect to itself."; const source = graph.nodes.find((node) => node.id === connection.source); const target = graph.nodes.find((node) => node.id === connection.target); if (!source || !target) return "The connection references an unknown node."; const sourceDefinition = library.find((item) => item.type === source.type); const targetDefinition = library.find((item) => item.type === target.type); if (!sourceDefinition || !targetDefinition) return "The node type is not in this library."; const sourcePort = connection.sourcePort ?? "output"; const targetPort = connection.targetPort ?? "input"; if (!sourceDefinition.output_ports.some((port) => port.id === sourcePort)) { return "The source port is not available."; } const port = targetDefinition.input_ports.find((item) => item.id === targetPort); if (!port) return "The target port is not available."; if (graph.edges.some( (edge) => edge.source === connection.source && edge.target === connection.target && (edge.source_port ?? "output") === sourcePort && (edge.target_port ?? "input") === targetPort )) { return "This connection already exists."; } if (!port.multiple && graph.edges.some( (edge) => edge.target === connection.target && (edge.target_port ?? "input") === targetPort )) { return "The target port accepts only one connection."; } if (!options.allowCycles && wouldCreateDefinitionCycle( graph, connection.source, connection.target )) { return "This connection would create a cycle."; } return null; } export function wouldCreateDefinitionCycle( graph: DefinitionGraph, source: string, target: string ): boolean { const outgoing = new Map(); graph.edges.forEach((edge) => { outgoing.set(edge.source, [...(outgoing.get(edge.source) ?? []), edge.target]); }); const pending = [target]; const visited = new Set(); while (pending.length) { const nodeId = pending.pop(); if (!nodeId || visited.has(nodeId)) continue; if (nodeId === source) return true; visited.add(nodeId); pending.push(...(outgoing.get(nodeId) ?? [])); } return false; }