/** * @file Connection validation helpers for node port compatibility checks. * Pure domain functions for validating connections between ports. */ import type { Node, Port, Connection, NodeId } from "../../types/core"; import type { NodeDefinition, PortConnectionContext } from "../../types/NodeDefinition"; import { arePortDataTypesCompatible, mergePortDataTypes } from "../port/connectivity/dataType"; import { getPortDefinition } from "../port/identity/definition"; import { checkPortCapacity } from "../port/connectivity/queries"; import type { NormalizedConnectionContext } from "./normalization"; import { normalizeConnectionContext } from "./normalization"; // Re-export for backwards compatibility export { getPortDefinition } from "../port/identity/definition"; export type ConnectionValidationOptions = { nodes?: Record; }; /** * Check if a connection between two ports already exists. * Checks both directions since connections are stored as from(output)->to(input). */ const connectionExists = ( portA: Port, portB: Port, connections: Record, ): boolean => { return Object.values(connections).some( (conn) => (conn.fromNodeId === portA.nodeId && conn.fromPortId === portA.id && conn.toNodeId === portB.nodeId && conn.toPortId === portB.id) || (conn.fromNodeId === portB.nodeId && conn.fromPortId === portB.id && conn.toNodeId === portA.nodeId && conn.toPortId === portA.id), ); }; /** * Internal validation for already-normalized port pairs. * Use this when you've already called normalizeConnectionContext/normalizeConnectionPorts. * * @param normalized - Pre-normalized connection context with sourcePort (output) and targetPort (input) * @param connections - Existing connections to check for duplicates and capacity * @param options - Additional validation options * @returns true if connection is valid, false otherwise */ export const canConnectNormalizedPorts = ( normalized: NormalizedConnectionContext, connections?: Record, options?: ConnectionValidationOptions, ): boolean => { const { sourcePort, targetPort, sourceDefinition, targetDefinition } = normalized; // Check if identical connection already exists if (connections && connectionExists(sourcePort, targetPort, connections)) { return false; } // Check custom validation functions (both must allow) // validateConnection always receives (outputPort, inputPort) const nodeDefValidators = [sourceDefinition, targetDefinition].filter(Boolean) as NodeDefinition[]; if (nodeDefValidators.some((def) => def.validateConnection && !def.validateConnection(sourcePort, targetPort))) { return false; } // Get port definitions for source and target const sourcePortDef = getPortDefinition(sourcePort, sourceDefinition); const targetPortDef = getPortDefinition(targetPort, targetDefinition); // Compute default data type compatibility // Port.dataType is set during derivation from the active PortDefinition, // so it takes priority. Only fall back to PortDefinition lookup when Port.dataType is not set. const sourceTypes = sourcePort.dataType ?? mergePortDataTypes(sourcePortDef?.dataType, sourcePortDef?.dataTypes); const targetTypes = targetPort.dataType ?? mergePortDataTypes(targetPortDef?.dataType, targetPortDef?.dataTypes); const dataTypeCompatible = arePortDataTypesCompatible(sourceTypes, targetTypes); // Build port connection context for canConnect predicates // Includes the default data type compatibility result so canConnect can override it const portContext: PortConnectionContext = { fromPort: sourcePort, toPort: targetPort, fromNode: options?.nodes?.[sourcePort.nodeId], toNode: options?.nodes?.[targetPort.nodeId], fromDefinition: sourceDefinition, toDefinition: targetDefinition, allConnections: connections, dataTypeCompatible, }; // Check port-level canConnect predicates // When canConnect is defined, it overrides the default data type compatibility check. // If not defined, the default data type check result is used. const sourceHasCanConnect = sourcePortDef?.canConnect !== undefined; const targetHasCanConnect = targetPortDef?.canConnect !== undefined; if (sourceHasCanConnect || targetHasCanConnect) { // canConnect takes full control - it can accept connections regardless of data type // by returning true, or reject them by returning false if (sourceHasCanConnect && !sourcePortDef!.canConnect!(portContext)) { return false; } if (targetHasCanConnect && !targetPortDef!.canConnect!(portContext)) { return false; } } else { // No canConnect defined - use default data type compatibility check if (!dataTypeCompatible) { return false; } } // Check max connections limit (port object takes priority over definition) if (connections) { // targetPort (input) receives connections, check "to" direction if (checkPortCapacity(targetPort, connections, "to", targetPortDef).atCapacity) { return false; } // sourcePort (output) sends connections, check "from" direction if (checkPortCapacity(sourcePort, connections, "from", sourcePortDef).atCapacity) { return false; } } return true; }; /** * Check if two ports can be connected. * Accepts ports in either order - internally normalizes to source(output)/target(input). * * @param fromPort - First port in the connection attempt (can be input or output) * @param toPort - Second port in the connection attempt (can be input or output) * @param fromNodeDef - Node definition for fromPort's node * @param toNodeDef - Node definition for toPort's node * @param connections - Existing connections to check for duplicates and capacity * @param options - Additional validation options * @returns true if connection is valid, false otherwise */ export const canConnectPorts = ( fromPort: Port, toPort: Port, fromNodeDef?: NodeDefinition, toNodeDef?: NodeDefinition, connections?: Record, options?: ConnectionValidationOptions, ): boolean => { // Normalize to source(output)/target(input) form // This handles both directions: output->input and input->output const normalized = normalizeConnectionContext(fromPort, toPort, fromNodeDef, toNodeDef); if (!normalized) { // Invalid pairing: same type, same node, or other invalid combination return false; } return canConnectNormalizedPorts(normalized, connections, options); };