/**
* cnodes
*
* A representation-agnostic library to define and execute nodes based processes
* License: MIT
* Author: Marco Jacovone
* Year: 2020-2021
*/
import { FlowSocket, OutputSocket, Socket } from "./socket.mjs";
import { Types } from "./type.mjs";
/**
* This is the base node class. A node have some input and output
* to exchange data with other nodes, some nexts to determine next
* execution nodes, and a prev to identify the entry point.
* A node can be functional or iterative. If the node is funcitonal
* the execution of the process method is repeated each time other
* nodes read the output values, otherwise output nodes reports
* the last computed value. Each node has a unique id to identify it
*/
export class Node {
/** An incremental index to generate unique node IDs */
static lastNodeIdIndex = 0;
/** The internal unique identifier */
#id = null;
/** The internal name of the node */
#name = "";
/** The external name of the node */
#title = "";
/** Is this node a functional node? */
#functional = false;
/** List of node's inputs */
#inputs = [];
/** List of node's outputs */
#outputs = [];
/** List of node's nexts in execution */
#nexts = [];
/** The execution entry point */
#prev = null;
/** Reference to the enclosing program */
#program = null;
/** Additional info (UIs can write anything to store graphical behaviors) */
#meta = null;
/** Can the node be removed by the user? */
#removable = true;
/** Can the node be created by the user? */
#creatable = true;
/** Can the user add an input? */
#canAddInput = false;
/** Can the user add an output? */
#canAddOutput = false;
/** Can the user add a next? */
#canAddNext = false;
/**
* Construct a new Node
* @param {string} [name] The name of the node
* @param {string} [title] The title of the node
*/
constructor(name, title = name) {
this.#name = name;
this.#title = title;
this.#id = "NID_" + Node.lastNodeIdIndex++;
}
get id() {
return this.#id;
}
set id(val) {
this.#id = val;
}
get name() {
return this.#name;
}
set name(val) {
this.#name = val;
}
get title() {
return this.#title;
}
set title(val) {
this.#title = val;
}
get functional() {
return this.#functional;
}
set functional(val) {
this.#functional = val;
}
get inputs() {
return this.#inputs;
}
set inputs(val) {
this.#inputs = val;
}
get outputs() {
return this.#outputs;
}
set outputs(val) {
this.#outputs = val;
}
get nexts() {
return this.#nexts;
}
set nexts(val) {
this.#nexts = val;
}
get prev() {
return this.#prev;
}
set prev(val) {
this.#prev = val;
}
get program() {
return this.#program;
}
set program(val) {
this.#program = val;
}
get removable() {
return this.#removable;
}
set removable(val) {
this.#removable = val;
}
get creatable() {
return this.#creatable;
}
set creatable(val) {
this.#creatable = val;
}
get canAddInput() {
return this.#canAddInput;
}
set canAddInput(val) {
this.#canAddInput = val;
}
get canAddOutput() {
return this.#canAddOutput;
}
set canAddOutput(val) {
this.#canAddOutput = val;
}
get canAddNext() {
return this.#canAddNext;
}
set canAddNext(val) {
this.#canAddNext = val;
}
get meta() {
return this.#meta;
}
set meta(val) {
this.#meta = val;
}
/**
* Returns the input by name
* @param {string} name Name of the input
*/
input(name) {
return this.inputs.find((i) => i.name === name);
}
/**
* Returns the output by name
* @param {string} name The name of the output
*/
output(name) {
return this.outputs.find((o) => o.name === name);
}
/**
* Returns the next by name
* @param {string} name The name of the next
*/
next(name) {
if (!name) {
return this.nexts[0];
}
return this.nexts.find((n) => n.name === name);
}
/**
* Evaluate all imputs of this node. Inputs are sockets.
* If the socket is connected the evaluation will search
* for the socket's peer and evaluate the output counterpart
* eventually reprocess the output's nod, if the node is
* functional
*/
async evaluateInputs() {
for (let inp of this.inputs) {
await inp.evaluate();
}
}
/**
* This is an helper method to construct a Result instance
* by name
* @param {Socket} socket The Socket on which construct the Result instance
*/
getFlowResult(socket) {
if (socket.peer) {
return new Result(socket.peer.node);
} else {
return new Result();
}
}
/**
* This method disconnect all sockets from the node
*/
disconnectAllSockets() {
if (this.#prev) {
while (this.#prev.peers.length > 0) {
this.#prev.disconnect(this.#prev.peers[0]);
this.#prev.peers.splice(0, 1);
}
}
for (let i of this.#inputs) {
if (i.peer) {
i.disconnect();
}
}
for (let o of this.#outputs) {
while (o.peers.length > 0) {
o.peers[0].disconnect();
o.peers.splice(0, 1);
}
}
for (let n of this.#nexts) {
if (n.peer) {
n.disconnect();
}
}
}
/**
* If this.#canAddInput is true, the user can add an input
* Subclass with variable number of input should override this method
*/
addInput() {
throw new Error("Can't add input!");
}
/**
* This method removes a specific input from the node, if
* this is possible whit this instance
* Subclass with variable number of input should override this method
* @param {InputSocket} input The input to remove
*/
removeInput(input) {
throw new Error("Can't remove input");
}
/**
* Can this node remove a specific input?
* Subclass with variable number of input should override this method
* @param {InputSocket} input The input to remove
*/
canRemoveInput(input) {
return false;
}
/**
* If this.#canAddOutput is true, the user can add an output
* Subclass with variable number of output should override this method
*/
addOutput() {
throw new Error("Can't add output!");
}
/**
* This method removes a specific output from the node, if
* this is possible whit this instance
* Subclass with variable number of output should override this method
* @param {OutputSocket} output The output to remove
*/
removeOutput(output) {
throw new Error("Can't remove output");
}
/**
* Can this node remove a specific output?
* Subclass with variable number of output should override this method
* @param {OutputSocket} output The output to remove
*/
canRemoveOutput(output) {
return false;
}
/**
* This method defines if a particular socket of this node can
* be connected to another one, based on sockets type.
* Default implementation checks for types of sockets, following the rule:
* - if sockets are FlowSockets, return true
* - Otherwise if the type of one socket is Types.ANY, return true
* - Otherwise if the two types are the same, return true
* - Otherwise return false
* @param {Socket} thisSocket The instance of socket of this node
* @param {Socket} otherSocket The other socket
*/
canBeConnected(thisSocket, otherSocket) {
if (
thisSocket instanceof FlowSocket &&
!(otherSocket instanceof FlowSocket)
) {
return false;
}
if (
otherSocket instanceof FlowSocket &&
!(thisSocket instanceof FlowSocket)
) {
return false;
}
if (thisSocket instanceof FlowSocket || otherSocket instanceof FlowSocket) {
return true;
}
if (thisSocket.type === Types.ANY || otherSocket.type === Types.ANY) {
return true;
}
if (thisSocket.type === otherSocket.type) {
return true;
}
return false;
}
/** The base version of the node does nothing */
async process() {
return new Result();
}
/**
* This method clones the node. Cloning will create a new node
* of the same type of the particular node, so each node must
* override this method to return the exact class type to the
* caller. The param "factory" is a function to create the specific
* class instance, to this base version of the method can create
* the instance and clone all sockets, and other propertiesthat
* is a same process for all different instances
*
* @param {Function} factory A function that return a new instance of the class
*/
clone(factory = () => new Node("Node")) {
let n = factory();
// Copy all inputs
n.inputs = [];
for (let i of this.inputs) {
let cloneI = i.clone();
cloneI.node = n;
n.inputs.push(cloneI);
}
// Copy all outputs
n.outputs = [];
for (let o of this.outputs) {
let cloneO = o.clone();
cloneO.node = n;
n.outputs.push(cloneO);
}
// Copy all nexts
n.nexts = [];
for (let nx of this.nexts) {
let cloneNx = nx.clone();
cloneNx.node = n;
n.nexts.push(cloneNx);
}
// Copy prev
n.prev = null;
if (this.prev) {
let clonePrev = this.prev.clone();
clonePrev.node = n;
n.prev = clonePrev;
}
// Copy base properties
n.id = "NID_" + Node.lastNodeIdIndex++;
n.name = this.name;
n.title = this.title;
n.functional = this.functional;
n.program = this.program;
n.meta = this.meta ? JSON.parse(JSON.stringify(this.meta)) : null;
n.removable = this.removable;
n.creatable = this.creatable;
n.canAddInput = this.canAddInput;
n.canAddOutput = this.canAddOutput;
n.canAddNext = this.canAddNext;
return n;
}
}
/**
* The result class used by programs to receive
* the next "next" in the flow
*/
export class Result {
/** The next node */
#next = null;
/**
* Construct a new Result
* @param {Socket} next The next socket to follow
*/
constructor(next = null) {
this.#next = next;
}
get next() {
return this.#next;
}
set next(val) {
this.#next = val;
}
}