Source: core/node.mjs

/**
 * 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;
  }
}