Source: core/socket.mjs

/**
 * cnodes
 *
 * A representation-agnostic library to define and execute nodes based processes
 * License: MIT
 * Author: Marco Jacovone
 * Year: 2020-2021
 */

import { Types } from "./type.mjs";

/**
 * A socket is an object that represent an input,
 * output, next or prev for the node
 */
export class Socket {
  /** An incremental index to generate unique socket IDs */
  static lastSocketIdIndex = 0;

  /** The internal id of the socket */
  #id = "";

  /** The name of this socket */
  #name = "";

  /** The parent node */
  #node = null;

  /**
   * Construct a new socket on a node with a given name
   * @param {string} name The name of the socket
   * @param {Node} node The parent node of the socket
   */
  constructor(name, node) {
    this.#id = "SID_" + Socket.lastSocketIdIndex++;

    this.#name = name;
    this.#node = node;
  }
  get id() {
    return this.#id;
  }
  set id(val) {
    this.#id = val;
  }
  get name() {
    return this.#name;
  }
  set name(val) {
    this.#name = val;
  }
  get node() {
    return this.#node;
  }
  set node(val) {
    this.#node = val;
  }

  /** Clone the spcket */
  clone() {
    throw new Error("You must override this method");
  }
}

/**
 * The value socket represent a input or a output value
 * for the node, so it has a value and a type
 */
export class ValueSocket extends Socket {
  /** The type for the socket's value */
  #type = Types.NUMBER;

  /** The stored value */
  #value = 0;

  /**
   * Some input/output sockets needs to have a name that users can change.
   * That's because the name of the socket is part of what the user can
   * choose. Think for example at a node that can make a data structure,
   * the user will want configure each field of the structure, in terms of
   * data value and name. The node will have a single output with the structure
   * as output. In this case the user can select a variable number of input
   * sockets, and can configure values (or connection) for each input as well
   * as the name of each socket, that whill be the name of the field in the structure.
   * This type of socket should be represented as a text field in a UI library.
   */
  #canEditName = false;

  /**
   * Some input/output sockets needs to change their type.
   * Nodes are responsible to configure this behavior of
   * sockets during the construction
   */
  #canEditType = false;

  /**
   * Construct a new ValueSocket
   * @param {string} name Name of the socket
   * @param {Node} node The parent node
   * @param {Type} type The type of this socket
   * @param {any} value The default value of the socket
   */
  constructor(name, node, type = Types.NUMBER, value = 0) {
    super(name, node);
    this.type = type;
    this.value = value;
  }

  get type() {
    return this.#type;
  }
  set type(val) {
    this.#type = val;
  }
  get value() {
    return this.#value;
  }
  set value(val) {
    this.#value = val;
  }
  get canEditName() {
    return this.#canEditName;
  }
  set canEditName(val) {
    this.#canEditName = val;
  }
  get canEditType() {
    return this.#canEditType;
  }
  set canEditType(val) {
    this.#canEditType = val;
  }

  /**
   * This method evaluates a socket in terms of the real value
   * that is staying inside. The meaning is different in case of
   * InputSocket and OutputSocket, that re-defines this method
   */
  evaluate() {
    throw new Error("This method must be redefined in a subclass!");
  }

  /** Clone the socket */
  clone() {
    throw new Error("You must override this method");
  }
}

/**
 * This is an input socket value for the node, it
 * can have only a peer socket, because its value
 * have to be defined in a deterministic way
 */
export class InputSocket extends ValueSocket {
  /** The only peer socket */
  #peer = null;

  /**
   * Construct a new InputSocket
   * @param {string} name The name of the socket
   * @param {Node} node The parent node
   * @param {Type} type The type of the socket
   * @param {any} value The default value of the socket
   */
  constructor(name, node, type = Types.NUMBER, value = 0) {
    super(name, node, type, value);
  }
  get peer() {
    return this.#peer;
  }
  set peer(val) {
    this.#peer = val;
  }

  /**
   * Evaluate the socket value. If the socket is connected,
   * this method goes to the peer socket and take the value.
   * Else the current socket value remains unchanged
   */
  async evaluate() {
    if (this.peer !== null) {
      await this.peer.evaluate(); // OutputSocket
      this.value = this.peer.value;
    }
  }

  /**
   * Connect this socket to another (output) socket
   * @param {Socket} socket The output socket to connect
   */
  connect(socket) {
    this.peer = socket;
    if (socket.peers.find((s) => s === this) === undefined) {
      socket.peers.push(this);
    }
  }

  /**
   * Disconnects this socket from its peer
   */
  disconnect() {
    if (this.peer) {
      let index = this.peer.peers.findIndex((s) => s === this);
      if (index !== -1) {
        this.peer.peers.splice(index, 1);
      }
    }
    this.peer = null;
  }

  /** Clone the spcket */
  clone() {
    let s = new InputSocket(this.name, null, this.type, this.value);
    s.id = "SID_" + Socket.lastSocketIdIndex++;
    s.peer = null;
    s.canEditName = this.canEditName;
    s.canEditType = this.canEditType;
    return s;
  }
}

/**
 * This is a output value socket and represent an output
 * value for the node. Output value socket can be connected to
 * many peer input value sockets, because many socket would like
 * to take the value from this.
 */
export class OutputSocket extends ValueSocket {
  /** A list of input value connected sockets */
  #peers = [];

  /**
   * This flag indicates that the socket don't try to
   * cal the process() method of its node upon the
   * evaluation. Normally, functional nodes have output
   * sockets not cached, so an evaluation of its values
   * will result in a call of process() method
   */
  #cached = true;

  /**
   * Construct a new OutputSocket
   * @param {string} name The name of the socket
   * @param {Node} node The parent node
   * @param {Type} type The type of the socket
   * @param {any} value The default value of the socket
   * @param {boolean} [cached] This socket is cached?
   */
  constructor(name, node, type = Types.NUMBER, value = 0, cached) {
    super(name, node, type, value);

    // default to true if the node is not functional and false
    // if the node is functional
    this.#cached = cached ?? !node.functional;
  }
  get peers() {
    return this.#peers;
  }
  set peers(val) {
    this.#peers = val;
  }
  get cached() {
    return this.#cached;
  }
  set cached(val) {
    this.#cached = val;
  }

  /**
   * The evaluation of a OutputSocket depends from two factors:
   * - The node of the socket is functional
   * - The socket is market to be "cached"
   *
   * If the parent node is not functional, simply does nothing, because
   * the value of this socket have to be unchanged from mthe last process
   * call
   * Otherwise, if the node is functional, we have to check if this particular
   * socket is marked to be "cached", in that case, the value have to remain unchanged,
   * otherwise the process() method have to be called again
   */
  async evaluate() {
    if (this.node?.functional && !this.cached) {
      await this.node.process();
    }
  }

  /**
   * Connects this socket to a input socket
   * @param {Socket} socket Socket to connect to
   */
  connect(socket) {
    if (this.peers.find((s) => s === socket) === undefined) {
      this.peers.push(socket);
    }
    socket.peer = this;
  }

  /**
   * Disconnect this socket from a specific input peer
   * @param {Socket} socket The socket to disconnect
   */
  disconnect(socket) {
    let index = this.peers.findIndex((s) => s === socket);
    if (index !== undefined) {
      this.peers.splice(index, 1);
      socket.peer = null;
    }
  }

  /** Clone the socket */
  clone() {
    let s = new OutputSocket(
      this.name,
      null,
      this.type,
      this.value,
      this.cached
    );
    s.id = "SID_" + Socket.lastSocketIdIndex++;
    s.peers = [];
    s.canEditName = this.canEditName;
    s.canEditType = this.canEditType;
    return s;
  }
}

/**
 * A flow socket is a socket to connect two nodes in
 * terms of execution flow
 */
export class FlowSocket extends Socket {
  /**
   * Construct a new FlowSocket
   * @param {sring} name Name of the socket
   * @param {Node} node The parent node
   */
  constructor(name, node) {
    super(name, node);
  }

  /** Clone the spcket */
  clone() {
    throw new Error("You must override this method");
  }
}

/**
 * This class representa a prev socket, a socket that
 * can be connected to other nexts sockets. The prev socket
 * cab have many peer (next) socket because the execution
 * can come from anywhere in the program
 */
export class PrevSocket extends FlowSocket {
  /** List of (next) peer sockets */
  #peers = [];

  /**
   * Construct a new PrevSocket
   * @param {string} name Name of the socket
   * @param {Node} node Parent node
   */
  constructor(name, node) {
    super(name, node);
  }
  get peers() {
    return this.#peers;
  }
  set peers(val) {
    this.#peers = val;
  }

  /**
   * Connect this socket to a next socket
   * @param {Socket} socket The next socket to connect
   */
  connect(socket) {
    if (this.peers.find((s) => s === socket) === undefined) {
      this.peers.push(socket);
    }
    socket.peer = this;
  }

  /**
   * Disconnect this socket from a next socket
   * @param {Socket} socket The next socket to disconnect
   */
  disconnect(socket) {
    let index = this.peers.findIndex((s) => s === socket);
    if (index !== undefined) {
      this.peers.splice(index, 1);
      socket.peer = null;
    }
  }

  /** Clone the socket */
  clone() {
    let s = new PrevSocket(this.name, null);
    s.id = "SID_" + Socket.lastSocketIdIndex++;
    s.peers = [];
    return s;
  }
}

/**
 * This class represents a socket to redirect the flow
 * to another node via a node's prev socket. This socket can
 * be connected to only one (prev) socket, because the program
 * flow have to be well defined
 */
export class NextSocket extends FlowSocket {
  /** The peer (prev) socket */
  #peer = null;

  /**
   * Construct a new NextSocket
   * @param {string} name Name of the socket
   * @param {Node} node The parent node of the socket
   */
  constructor(name, node) {
    super(name, node);
  }
  get peer() {
    return this.#peer;
  }
  set peer(val) {
    this.#peer = val;
  }

  /**
   * Connect this socket to another (prev) socket
   * @param {Socket} socket The prev socket to connect to
   */
  connect(socket) {
    this.peer = socket;
    if (socket.peers.find((s) => s === this) === undefined) {
      socket.peers.push(this);
    }
  }

  /**
   * Disconnect this socket from the peer
   */
  disconnect() {
    if (this.peer) {
      let index = this.peer.peers.findIndex((s) => s === this);
      if (index !== -1) {
        this.peer.peers.splice(index, 1);
      }
      this.peer = null;
    }
  }

  /** Clone the spcket */
  clone() {
    let s = new NextSocket(this.name, null);
    s.id = "SID_" + Socket.lastSocketIdIndex++;
    s.peer = null;
    return s;
  }
}