Specialized components
Visual scripting
Socket & link
23 min
overview sockets and links form the connection system of the ir engine visual scripting framework sockets are the connection points on nodes that allow them to receive input or send output, while links are the connections between these sockets that define how data and execution flow through the visual script together, they create the pathways that enable nodes to communicate and work together to form a complete logical system this chapter explores the concept, structure, and implementation of sockets and links within the ir engine core concepts socket purpose and types sockets serve as the interface points for nodes, allowing them to communicate with other nodes in the visual script input sockets located on the left side of a node, these receive data or execution signals from other nodes output sockets located on the right side of a node, these send data or execution signals to other nodes sockets can handle two fundamental types of connections data sockets transfer information values (numbers, strings, booleans, vectors, etc ) between nodes execution sockets transfer control flow signals that determine the order of node execution this dual system allows visual scripts to manage both the flow of information and the sequence of operations link function links are the connections that join an output socket on one node to an input socket on another node links always flow from an output socket to an input socket a single output socket can connect to multiple input sockets (one to many relationship) an input socket can only connect to one output socket (many to one relationship) links define both data pathways and execution sequences by connecting nodes through links, users create the logical structure of their visual script implementation socket implementation the socket class represents a connection point on a node // simplified from src/engine/sockets/socket ts export class socket { public readonly links link\[] = \[]; // links starting from this socket (if it's an output) constructor( public readonly valuetypename string, // data type "number", "string", "boolean", "flow", etc public readonly name string, // unique identifier "value", "execute", etc public value any | undefined = undefined, // current data value public readonly label string | undefined = undefined, // display name public readonly valuechoices? string\[] // optional list of allowed values ) {} // methods for managing connections and values connect(targetnode inode, targetsocketname string) link { / / } disconnect(link link) boolean { / / } getvalue() any { / / } setvalue(value any) void { / / } } key properties valuetypename defines what kind of data the socket can handle (e g , "number", "string", "flow") name a unique identifier for the socket within its node value the actual data value stored in or transmitted through the socket links for output sockets, an array of link objects representing connections to input sockets link implementation the link class represents a connection between two sockets // simplified from src/engine/nodes/link ts export class link { // references to the actual node and socket objects (resolved at runtime) public targetnode inode | undefined = undefined; public targetsocket socket | undefined = undefined; constructor( public nodeid string = '', // id of the target node public socketname string = '' // name of the target socket ) {} // methods for managing the connection isresolved() boolean { return !!this targetnode && !!this targetsocket; } resolve(nodes map\<string, inode>) boolean { / / } transmit(value any) void { / / } } key properties nodeid the unique identifier of the target node socketname the name of the target socket on that node targetnode and targetsocket direct references to the actual node and socket objects socket creation sockets are created when a node is instantiated, based on the node's definition // simplified from src/engine/nodes/node ts function createinputsockets( inputdefinitions socketdefinition\[], node inode ) socket\[] { return inputdefinitions map(def => { return new socket( def valuetypename, def name, def defaultvalue, def label, def valuechoices ); }); } function createoutputsockets( outputdefinitions socketdefinition\[], node inode ) socket\[] { return outputdefinitions map(def => { return new socket( def valuetypename, def name, undefined, // output sockets typically don't have default values def label, def valuechoices ); }); } these functions take an array of socket definitions from the node's description create a new socket instance for each definition return an array of socket objects that the node can use for inputs or outputs link creation links are created when the user connects two sockets in the visual editor // simplified concept from src/engine/graphs/graph ts function connectsockets( sourcenode inode, sourcesocketname string, targetnode inode, targetsocketname string ) link | null { // find the source and target sockets const sourcesocket = sourcenode outputs find(s => s name === sourcesocketname); const targetsocket = targetnode inputs find(s => s name === targetsocketname); if (!sourcesocket || !targetsocket) { return null; // socket not found } // check if the connection is valid (compatible types) if (!aresocketscompatible(sourcesocket, targetsocket)) { return null; // incompatible socket types } // create and store the link const link = new link(targetnode id, targetsocketname); link targetnode = targetnode; link targetsocket = targetsocket; sourcesocket links push(link); return link; } this function finds the source output socket and target input socket validates that the connection is allowed (compatible types) creates a new link object pointing to the target adds the link to the source socket's links array returns the created link type compatibility before creating a link, the system checks if the sockets are compatible // simplified concept from src/engine/sockets/socketcompatibility ts function aresocketscompatible( sourcesocket socket, targetsocket socket ) boolean { // check if both are execution sockets or both are data sockets if (sourcesocket valuetypename === 'flow' && targetsocket valuetypename === 'flow') { return true; // execution sockets are always compatible with each other } if (sourcesocket valuetypename === 'flow' || targetsocket valuetypename === 'flow') { return false; // can't connect execution to data or vice versa } // check data type compatibility return istypecompatible(sourcesocket valuetypename, targetsocket valuetypename); } function istypecompatible( sourcetype string, targettype string ) boolean { if (sourcetype === targettype) { return true; // exact match is always compatible } // check for implicit conversions (e g , int to float) const conversiontable = { 'int' \['float', 'string'], 'float' \['string'], // additional conversion rules }; return conversiontable\[sourcetype]? includes(targettype) || false; } this ensures that execution sockets can only connect to other execution sockets data sockets can only connect to compatible data sockets some automatic type conversions are allowed (e g , integer to float) socket and link operations reading input values when a node needs to read a value from an input socket // simplified from src/engine/nodes/nodesockets ts export function readinputfromsocket\<t>(socket socket) t { // if the socket has a direct value, return it if (socket value !== undefined) { return socket value as t; } // otherwise, we need to find which output socket is connected to this input // this is typically done by the execution engine, which maintains a map // of input sockets to their source output sockets // find the source node and socket that connect to this input const sourcenodeandsocket = findsourceforinputsocket(socket); if (!sourcenodeandsocket) { return undefined as unknown as t; // no connection found } const { sourcenode, sourcesocket } = sourcenodeandsocket; // if the source is a function node, we might need to execute it first if (sourcenode nodetype === nodetype function) { sourcenode exec(); // this will update the output socket's value } // return the value from the source output socket return sourcesocket value as t; } this function checks if the socket has a direct value (e g , a constant or user set value) if not, finds the output socket that's connected to this input ensures the source node has calculated its output value returns the value from the source output socket writing output values when a node needs to write a value to an output socket // simplified from src/engine/nodes/nodesockets ts export function writeoutputtosocket\<t>(socket socket, value t) void { // set the value on the output socket socket value = value; // propagate the value to all connected input sockets for (const link of socket links) { if (link targetsocket) { link targetsocket value = value; } } } this function sets the value on the output socket propagates the value to all connected input sockets through links execution flow for execution sockets, the process is slightly different // simplified concept from src/engine/execution/fiber ts export class fiber { // other properties // called by a node to continue execution through a specific output socket commit(node inode, outputsocketname string) void { // find the output socket const outputsocket = node outputs find(s => s name === outputsocketname); if (!outputsocket || outputsocket valuetypename !== 'flow') { return; // not an execution socket or not found } // for each link from this output socket for (const link of outputsocket links) { if (link targetnode) { // queue the target node for execution this queuenodeexecution(link targetnode, link targetsocket! name); } } } // queue a node to be executed private queuenodeexecution(node inode, inputsocketname string) void { // add to execution queue this executionqueue push({ node, inputsocketname }); // if not already running, start processing the queue if (!this isrunning) { this processexecutionqueue(); } } // process the execution queue private processexecutionqueue() void { this isrunning = true; while (this executionqueue length > 0) { const { node, inputsocketname } = this executionqueue shift()!; // execute the node if (node nodetype === nodetype flow) { (node as flownode) triggered(this, inputsocketname); } } this isrunning = false; } } this system allows nodes to signal which output execution path to follow queues target nodes for execution processes the execution queue in the correct order example use case let's examine the socket and link structure in our enhanced alarm system graph td trigger\["event ontriggeractivated"] getvar\["function get variable 'isalarmenabled'"] branch{"flow branch (if/else)"} playsound\["flow play alarm sound"] trigger >|"execution link"| branch getvar >|"data link"| branch branch >|"true execution link"| playsound in this example execution link from trigger to branch trigger has an output execution socket named "triggered" branch has an input execution socket named "execute" a link connects "triggered" to "execute" when the trigger event occurs, execution flows through this link data link from getvar to branch getvar has an output data socket named "value" (type boolean) branch has an input data socket named "condition" (type boolean) a link connects "value" to "condition" the branch node reads the boolean value through this link execution link from branch to playsound branch has an output execution socket named "true" playsound has an input execution socket named "execute" a link connects "true" to "execute" if the condition is true, execution flows through this link the socket and link objects might be structured like // conceptual representation // trigger node const triggernode = { // outputs \[ new socket("flow", "triggered", undefined, "when triggered"), ], // }; // getvar node const getvarnode = { // outputs \[ new socket("boolean", "value", undefined, "variable value"), ], // }; // branch node const branchnode = { // inputs \[ new socket("flow", "execute", undefined, "execution input"), new socket("boolean", "condition", undefined, "branch condition"), ], outputs \[ new socket("flow", "true", undefined, "if condition is true"), new socket("flow", "false", undefined, "if condition is false"), ], // }; // playsound node const playsoundnode = { // inputs \[ new socket("flow", "execute", undefined, "play the sound"), new socket("string", "soundfile", "alarm wav", "sound file to play"), ], // }; // links triggernode outputs\[0] links push(new link(branchnode id, "execute")); getvarnode outputs\[0] links push(new link(branchnode id, "condition")); branchnode outputs\[0] links push(new link(playsoundnode id, "execute")); link creation workflow when a user creates a link in the visual editor, the following process occurs sequencediagram participant user participant editor participant outputsocket participant inputsocket participant linkobject user >>editor drag from output to input socket editor >>editor identify source and target sockets editor >>editor check type compatibility alt types are compatible editor >>linkobject create new link(targetnodeid, targetsocketname) editor >>outputsocket add link to links array editor >>linkobject set targetnode and targetsocket references editor >>user show visual connection else types are incompatible editor >>user show error message end this process ensures that only valid connections are created the link is properly registered with the source socket direct references are established for efficient execution next steps with an understanding of how sockets and links enable communication between nodes, the next chapter explores how different types of nodes with their specific sets of sockets are defined in the system next node definition docid\ k2a 8xvflvwqntqq8bymy