Source: core/program.mjs

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

import { EventEmitter } from "events";
import { Enter } from "./enter.mjs";
import { Exit } from "./exit.mjs";
import { Node } from "./node.mjs";
import {
  InputSocket,
  NextSocket,
  OutputSocket,
  PrevSocket,
} from "./socket.mjs";
import { Types } from "./type.mjs";

/**
 * A program is a special node that contains nodes. The program
 * manages the flow of the global execution by starting from the
 * "Enter" default, autocreated node, call its process() method and receive the next
 * "next". A program also store a global variable space
 */
export class Program extends Node {
  // Provide a node instance
  static instance = () => new Program();

  /** Engine version */
  static version = 1;

  /** The nodes in this program */
  #nodes = [];

  /** The Enter node */
  #enter = null;

  /** The Exit node */
  #exit = null;

  /** The instruction pointer equivalent :) */
  #currentNode = null;

  /** The variable global space */
  #vars = new Map();

  /** The event emitter connected to the program */
  static events = new EventEmitter();

  /**
   * Construct a new Program node
   */
  constructor() {
    super("Program");
    this.inputs = [new InputSocket("Val", this, Types.ANY, 0)];
    this.outputs = [new OutputSocket("Val", this, Types.ANY, 0)];
    this.nexts = [new NextSocket("Out", this)];
    this.prev = new PrevSocket("In", this);

    // Create default enter, exit nodes
    this.addNode((this.#enter = new Enter())).addNode(
      (this.#exit = new Exit())
    );
  }

  /**
   * Clone this node
   * @param {Function} factory The factory class function
   */
  clone(factory = Program.instance) {
    let retNode = super.clone(factory);

    // Clone internal nodes
    retNode.nodes = Program.cloneNodes(this.#nodes);

    // Connect actual Enter and Exit
    retNode.enter = retNode.nodes.find((n) => n instanceof Enter);
    retNode.exit = retNode.nodes.find((n) => n instanceof Exit);

    return retNode;
  }

  /**
   * This method clone a group of nodes, by reconstructing the
   * connections from sockets too. All connections involvong nodes
   * outside this set will be not reconstructed.
   * @param {Node[]} nodes Nodes (and) connections to clone
   */
  static cloneNodes(nodes) {
    // First of all, clone all nodes
    let retNodes = [];

    for (let n of nodes) {
      let cloneN = n.clone();

      // Setup a temporary link between each node and its peer
      cloneN.__peer = n;
      n.__peer = cloneN;

      retNodes.push(cloneN);
    }

    // Reconstruct all links by traversong all nodes and
    // consider all output-->input and next-->prev connections
    // and duplicate them in clone nodes
    for (let n of nodes) {
      // Clone output->input
      for (let o of n.outputs) {
        for (let p of o.peers) {
          if (nodes.includes(p.node)) {
            n.__peer.output(o.name).connect(p.node.__peer.input(p.name));
          }
        }
      }
      // Clone next->prev
      for (let nx of n.nexts) {
        if (nx.peer) {
          if (nodes.includes(nx.peer.node)) {
            n.__peer.next(nx.name).connect(nx.peer.node.__peer.prev);
          }
        }
      }
    }

    // Remove __peer fields
    for (let n of nodes) {
      n.__peer = undefined;
    }
    for (let n of retNodes) {
      n.__peer = undefined;
    }

    return retNodes;
  }

  get vars() {
    return this.#vars;
  }
  set vars(val) {
    this.#vars = val;
  }
  get enter() {
    return this.#enter;
  }
  set enter(val) {
    this.#enter = val;
  }
  get exit() {
    return this.#exit;
  }
  set exit(val) {
    this.#exit = val;
  }
  get currentNode() {
    return this.#currentNode;
  }
  set currentNode(val) {
    this.#currentNode = val;
  }
  get nodes() {
    return this.#nodes;
  }
  set nodes(val) {
    this.#nodes = val;
  }

  /**
   * Add a new node to this program
   * @param {Node} node The node to add
   */
  addNode(node) {
    this.#nodes.push(node);

    // Set this program to the node
    node.program = this;
    return this;
  }

  /**
   * Removes a node from this program, disconnect all sockets
   * @param {Node} node The node to remove
   */
  removeNode(node) {
    // Disconnect its sockets
    node.disconnectAllSockets();

    this.#nodes = this.#nodes.filter((n) => n.id !== node.id);
    node.program = null;
    return this;
  }

  /**
   * The process method will start from the Enter node and
   * cycle over nexts returned by the process functions of nodes.
   * The Program node couldn't be a top-level program, but a sub-nod
   * of another program. For that reason, the process() method copy the
   * value of the only input in the Program node to the only one
   * output of the "Enter" node.
   * This is a limitation: The Program node can be actually only 1 input
   * and only 1 output. At the same, Enter and Exit nodes will have only
   * 1 output and 1 input respectively.
   * At the end, the process() methos of the Program node, will copy the
   * value of the Exit's intput to the unique output of the Program node
   */
  async process() {
    await this.evaluateInputs();

    this.#enter.output("Val").value = this.input("Val").value;

    await this.processFrom(this.#enter);

    this.output("Val").value = this.#exit.input("Val").value;

    return this.getFlowResult(this.next("Out"));
  }

  /**
   * Execute a program useng node as starting point
   * @param {Node} node Starting point node
   */
  async processFrom(node) {
    this.currentNode = node;
    while (this.currentNode !== null) {
      let result = await this.currentNode.process();
      this.currentNode = result.next;
    }
  }
}