LGraph.js

import defaultConfig from "./utils/defaultConfig";
import getTime from "./utils/time";
import LGraphNode from "./LGraphNode";
import LGraphGroup from "./LGraphGroup";
import LGraphCanvas from "./LGraphCanvas";
import LLink from "./LLink";

/**
 * LGraph is the class that contain a full graph.
 * We instantiate one and add nodes to it, and then we can run the execution loop.
 * supported callbacks:
 + onNodeAdded: when a new node is added to the graph
 + onNodeRemoved: when a node inside this graph is removed
 + onNodeConnectionChange: some connection has changed in the graph (connected or disconnected)
 *
 * @class LGraph
 * @constructor
 * @param {Object} o data from previous serialization [optional]
 */
export default class LGraph {
    constructor(o) {
        if (defaultConfig.debug) {
            console.log("Graph created");
        }
        this.list_of_graphcanvas = null;
        this.clear();

        if (o) {
            this.configure(o);
        }
    }

    getSupportedTypes() {
        return this.supportedTypes || LGraph.supportedTypes;
    }

    STATUS_STOPPED = 1;

    STATUS_RUNNING = 2;

    supportedTypes = ["number", "string", "boolean"];

    static supportedTypes = ["number", "string", "boolean"];
    // used to know which types of connections support this graph (some graphs do not allow certain
    // types)

    /**
     * Removes all nodes from this graph
     * @method clear
     * @memberOf LGraph
     */
    clear() {
        this.stop();
        this.status = this.STATUS_STOPPED;

        this.last_node_id = 0;
        this.last_link_id = 0;

        this._version = -1; // used to detect changes

        // safe clear
        if (this._nodes) {
            for (const node of this._nodes) {
                if (node.onRemoved) node.onRemoved();
            }
        }

        // nodes
        this._nodes = [];
        this._nodes_by_id = {};
        this._nodes_in_order = []; // nodes sorted in execution order
        this._nodes_executable = null; // nodes that contain onExecute sorted in execution order

        // other scene stuff
        this._groups = [];

        // links
        this.links = {}; // container with all the links

        // iterations
        this.iteration = 0;

        // custom data
        this.config = {};
        this.vars = {};
        this.extra = {}; // to store custom data

        // timing
        this.globaltime = 0;
        this.runningtime = 0;
        this.fixedtime = 0;
        this.fixedtime_lapse = 0.01;
        this.elapsed_time = 0.01;
        this.last_update_time = 0;
        this.starttime = 0;

        this.catch_errors = true;

        // subgraph_data
        this.inputs = {};
        this.outputs = {};

        // notify canvas to redraw
        this.change();

        this.sendActionToCanvas("clear");
    }

    /**
     * Attach Canvas to this graph
     * @method attachCanvas
     * @param {GraphCanvas} graphcanvas
     * @memberOf LGraph
     */
    attachCanvas(graphcanvas) {
        if (graphcanvas.constructor !== LGraphCanvas) {
            throw new Error("attachCanvas expects a LGraphCanvas instance");
        }
        if (graphcanvas.graph && graphcanvas.graph !== this) {
            graphcanvas.graph.detachCanvas(graphcanvas);
        }

        graphcanvas.graph = this;

        if (!this.list_of_graphcanvas) this.list_of_graphcanvas = [];
        this.list_of_graphcanvas.push(graphcanvas);
    }

    /**
     * Detach Canvas from this graph
     * @method detachCanvas
     * @param {GraphCanvas} graphcanvas
     * @memberOf LGraph
     */
    detachCanvas(graphcanvas) {
        if (!this.list_of_graphcanvas) {
            return;
        }

        const pos = this.list_of_graphcanvas.indexOf(graphcanvas);
        if (pos === -1) {
            return;
        }
        graphcanvas.graph = null;
        this.list_of_graphcanvas.splice(pos, 1);
    }

    /**
     * Starts running this graph every interval milliseconds.
     * @method start
     * @param {number} interval amount of milliseconds between executions, if 0 then it renders to
     *     the monitor refresh rate
     * @memberOf LGraph
     */
    start(interval) {
        if (this.status === LGraph.STATUS_RUNNING) {
            return;
        }
        this.status = LGraph.STATUS_RUNNING;

        if (this.onPlayEvent) {
            this.onPlayEvent();
        }

        this.sendEventToAllNodes("onStart");

        // launch
        this.starttime = getTime();
        this.last_update_time = this.starttime;
        interval = interval || 0;
        const that = this;

        // execute once per frame
        if (interval === 0 && typeof window !== "undefined" && window.requestAnimationFrame) {
            // eslint-disable-next-line no-inner-declarations
            function onFrame() {
                if (that.execution_timer_id !== -1) {
                    return;
                }
                window.requestAnimationFrame(onFrame);
                if (that.onBeforeStep) that.onBeforeStep();
                that.runStep(1, !that.catch_errors);
                if (that.onAfterStep) that.onAfterStep();
            }

            this.execution_timer_id = -1;
            onFrame();
        } else { // execute every 'interval' ms
            this.execution_timer_id = setInterval(() => {
                // execute
                if (that.onBeforeStep) that.onBeforeStep();
                that.runStep(1, !that.catch_errors);
                if (that.onAfterStep) that.onAfterStep();
            }, interval);
        }
    }

    /**
     * Stops the execution loop of the graph
     * @method stop execution
     * @memberOf LGraph
     */
    stop() {
        if (this.status === LGraph.STATUS_STOPPED) {
            return;
        }

        this.status = LGraph.STATUS_STOPPED;

        if (this.onStopEvent) {
            this.onStopEvent();
        }

        if (this.execution_timer_id) {
            if (this.execution_timer_id !== -1) {
                clearInterval(this.execution_timer_id);
            }
            this.execution_timer_id = null;
        }

        this.sendEventToAllNodes("onStop");
    }

    /**
     * Run N steps (cycles) of the graph
     * @method runStep
     * @param {number} num number of steps to run, default is 1
     * @param {Boolean} doNotCatchError [optional] if you want to try/catch errors
     * @param {number} limit max number of nodes to execute (used to execute from start to a node)
     * @memberOf LGraph
     */
    runStep(num, doNotCatchError, limit) {
        num = num || 1;

        const start = getTime();
        this.globaltime = 0.001 * (start - this.starttime);

        const nodes = this._nodes_executable
            ? this._nodes_executable
            : this._nodes;
        if (!nodes) {
            return;
        }

        limit = limit || nodes.length;

        if (doNotCatchError) {
            // iterations
            for (let i = 0; i < num; i++) {
                for (let j = 0; j < limit; j++) {
                    const node = nodes[j];
                    if (node.mode === defaultConfig.ALWAYS && node.onExecute) {
                        node.onExecute(); // hard to send elapsed time
                    }
                }

                this.fixedtime += this.fixedtime_lapse;
                if (this.onExecuteStep) {
                    this.onExecuteStep();
                }
            }

            if (this.onAfterExecute) {
                this.onAfterExecute();
            }
        } else {
            try {
                // iterations
                for (let i = 0; i < num; i++) {
                    for (let j = 0; j < limit; ++j) {
                        const node = nodes[j];
                        if (node.mode === defaultConfig.ALWAYS && node.onExecute) {
                            node.onExecute();
                        }
                    }

                    this.fixedtime += this.fixedtime_lapse;
                    if (this.onExecuteStep) {
                        this.onExecuteStep();
                    }
                }

                if (this.onAfterExecute) {
                    this.onAfterExecute();
                }
                this.errors_in_execution = false;
            } catch (err) {
                this.errors_in_execution = true;
                if (defaultConfig.throw_errors) {
                    throw err;
                }
                if (defaultConfig.debug) {
                    console.log(`Error during execution: ${err}`);
                }
                this.stop();
            }
        }

        const now = getTime();
        let elapsed = now - start;
        if (elapsed === 0) {
            elapsed = 1;
        }
        this.execution_time = 0.001 * elapsed;
        this.globaltime += 0.001 * elapsed;
        this.iteration += 1;
        this.elapsed_time = (now - this.last_update_time) * 0.001;
        this.last_update_time = now;
    }

    /**
     * Updates the graph execution order according to relevance of the nodes (nodes with only
     * outputs have more relevance than nodes with only inputs.
     * @method updateExecutionOrder
     * @memberOf LGraph
     */
    updateExecutionOrder() {
        this._nodes_in_order = this.computeExecutionOrder(false);
        this._nodes_executable = [];
        for (const node of this._nodes_in_order) {
            if (node.onExecute) {
                this._nodes_executable.push(node);
            }
        }
    }

    /**
     * It computes the executable nodes in order and returns it
     * @param onlyOnExecute
     * @param setLevel
     * @returns {this}
     * @internal
     * @memberOf LGraph
     */
    computeExecutionOrder(onlyOnExecute, setLevel) {
        let L = [];
        const S = [];
        const M = {};
        const visitedLinks = {}; // to avoid repeating links
        const remainingLinks = {}; // to a

        // search for the nodes without inputs (starting nodes)
        for (const node of this._nodes) {
            if (onlyOnExecute && !node.onExecute) {
                continue;
            }

            M[node.id] = node; // add to pending nodes

            let num = 0; // num of input connections
            if (node.inputs) {
                for (let j = 0, l2 = node.inputs.length; j < l2; j++) {
                    if (node.inputs[j] && node.inputs[j].link != null) {
                        num += 1;
                    }
                }
            }

            if (num === 0) {
                // is a starting node
                S.push(node);
                if (setLevel) {
                    node._level = 1;
                }
            } else {
                if (setLevel) {
                    node._level = 0;
                }
                remainingLinks[node.id] = num;
            }
        }

        while (true) {
            if (S.length === 0) {
                break;
            }

            // get an starting node
            const node = S.shift();
            L.push(node); // add to ordered list
            delete M[node.id]; // remove from the pending nodes

            if (!node.outputs) {
                continue;
            }

            // for every output
            for (const output of node.outputs) {
                if (
                    output == null
                    || output.links == null
                    || output.links.length === 0
                ) {
                    continue;
                }

                // for every connection
                for (const linkId of output.links) {
                    const link = this.links[linkId];
                    if (!link) {
                        continue;
                    }

                    // already visited link (ignore it)
                    if (visitedLinks[link.id]) {
                        continue;
                    }

                    const targetNode = this.getNodeById(link.target_id);
                    if (targetNode == null) {
                        visitedLinks[link.id] = true;
                        continue;
                    }

                    if (
                        setLevel
                        && (!targetNode._level
                        || targetNode._level <= node._level)
                    ) {
                        targetNode._level = node._level + 1;
                    }

                    visitedLinks[link.id] = true; // mark as visited
                    remainingLinks[targetNode.id] -= 1; // reduce the number of links remaining
                    if (remainingLinks[targetNode.id] === 0) {
                        S.push(targetNode);
                    } // if no more links, then add to starters array
                }
            }
        }

        // the remaining ones (loops)
        // eslint-disable-next-line guard-for-in,no-restricted-syntax
        for (const i in M) L.push(M[i]);

        if (L.length !== this._nodes.length && defaultConfig.debug) {
            console.warn("something went wrong, nodes missing");
        }

        const l = L.length;

        // save order number in the node
        for (let i = 0; i < l; i++) L[i].order = i;

        // sort now by priority
        L = L.sort((A, B) => {
            const Ap = A.constructor.priority || A.priority || 0;
            const Bp = B.constructor.priority || B.priority || 0;
            if (Ap === Bp) {
                // if same priority, sort by order
                return A.order - B.order;
            }
            return Ap - Bp; // sort by priority
        });

        // save order number in the node, again...
        for (let i = 0; i < l; ++i) L[i].order = i;

        return L;
    }

    /**
     * Returns all the nodes that could affect this one (ancestors) by crawling all the inputs
     * recursively. It doesn't include the node itself
     * @method getAncestors
     * @memberOf LGraph
     * @return {Array} an array with all the LGraphNodes that affect this node, in order of
     *     execution
     */
    // eslint-disable-next-line class-methods-use-this
    getAncestors(node) {
        const ancestors = [];
        const pending = [node];
        const visited = {};

        while (pending.length) {
            const current = pending.shift();
            if (!current.inputs) {
                continue;
            }
            if (!visited[current.id] && current !== node) {
                visited[current.id] = true;
                ancestors.push(current);
            }

            for (let i = 0; i < current.inputs.length; ++i) {
                const input = current.getInputNode(i);
                if (input && ancestors.indexOf(input) === -1) {
                    pending.push(input);
                }
            }
        }

        ancestors.sort((a, b) => a.order - b.order);
        return ancestors;
    }

    /**
     * Positions every node in a more readable manner
     * @method arrange
     * @memberOf LGraph
     */
    arrange(margin) {
        margin = margin || 100;

        const nodes = this.computeExecutionOrder(false, true);
        const columns = [];
        for (const node of nodes) {
            const col = node._level || 1;
            if (!columns[col]) {
                columns[col] = [];
            }
            columns[col].push(node);
        }

        let x = margin;

        for (const column of columns) {
            if (!column) {
                continue;
            }
            let maxSize = 100;
            let y = margin + defaultConfig.NODE_TITLE_HEIGHT;
            for (const node of column) {
                node.pos[0] = x;
                node.pos[1] = y;
                if (node.size[0] > maxSize) maxSize = node.size[0];
                y += node.size[1] + margin + defaultConfig.NODE_TITLE_HEIGHT;
            }
            x += maxSize + margin;
        }

        this.setDirtyCanvas(true, true);
    }

    /**
     * Returns the amount of time the graph has been running in milliseconds
     * @method getTime
     * @return {number} number of milliseconds the graph has been running
     * @memberOf LGraph
     */
    getTime() {
        return this.globaltime;
    }

    /**
     * Returns the amount of time accumulated using the fixedtime_lapse var. This is used in
     * context where the time increments should be constant
     * @method getFixedTime
     * @return {number} number of milliseconds the graph has been running
     * @memberOf LGraph
     */

    getFixedTime() {
        return this.fixedtime;
    }

    /**
     * Returns the amount of time it took to compute the latest iteration. Take into account that
     * this number could be not correct if the nodes are using graphical actions
     * @method getElapsedTime
     * @return {number} number of milliseconds it took the last cycle
     * @memberOf LGraph
     */

    getElapsedTime() {
        return this.elapsed_time;
    }

    /**
     * Sends an event to all the nodes, useful to trigger stuff
     * @method sendEventToAllNodes
     * @param {String} eventname the name of the event (function to be called)
     * @param {Array} params parameters in array format
     * @memberOf LGraph
     */
    sendEventToAllNodes(eventname, params, mode) {
        mode = mode || defaultConfig.ALWAYS;

        const nodes = this._nodes_in_order ? this._nodes_in_order : this._nodes;
        if (!nodes) {
            return;
        }

        for (let j = 0, l = nodes.length; j < l; ++j) {
            const node = nodes[j];

            if (
                node.constructor.name === "Subgraph"
                && eventname !== "onExecute"
            ) {
                if (node.mode === mode) {
                    node.sendEventToAllNodes(eventname, params, mode);
                }
                continue;
            }

            if (!node[eventname] || node.mode !== mode) {
                continue;
            }
            if (params === undefined) {
                node[eventname]();
            } else if (params && params.constructor === Array) {
                node[eventname](...params);
            } else {
                node[eventname](params);
            }
        }
    }

    sendActionToCanvas(action, params = []) {
        if (!this.list_of_graphcanvas) {
            return;
        }

        for (const c of this.list_of_graphcanvas) {
            if (c[action]) {
                c[action](...params);
            }
        }
    }

    /**
     * Adds a new node instance to this graph
     * @method add
     * @param {LGraphNode} node the instance of the node
     * @param {boolean} skipComputeOrder
     * @memberOf LGraph
     */

    add(node, skipComputeOrder) {
        if (!node) {
            return;
        }

        // groups
        if (node.constructor === LGraphGroup) {
            this._groups.push(node);
            this.setDirtyCanvas(true);
            this.change();
            node.graph = this;
            this._version++;
            return;
        }

        // nodes
        if (node.id !== -1 && this._nodes_by_id[node.id]) {
            console.warn(
                "LiteGraph: there is already a node with this ID, changing it",
            );
            node.id = ++this.last_node_id;
        }

        if (this._nodes.length >= defaultConfig.MAX_NUMBER_OF_NODES) {
            throw new Error("LiteGraph: max number of nodes in a graph reached");
        }

        // give him an id
        if (!node.id || node.id === -1) {
            node.id = ++this.last_node_id;
        } else if (this.last_node_id < node.id) {
            this.last_node_id = node.id;
        }

        node.graph = this;
        this._version++;

        this._nodes.push(node);
        this._nodes_by_id[node.id] = node;

        if (node.onAdded) node.onAdded(this);

        if (this.config.align_to_grid) node.alignToGrid();

        if (!skipComputeOrder) this.updateExecutionOrder();

        if (this.onNodeAdded) this.onNodeAdded(node);

        this.setDirtyCanvas(true);
        this.change();

        return node; // to chain actions
    }

    /**
     * Removes a node from the graph
     * @method remove
     * @param {LGraphNode} node the instance of the node
     * @memberOf LGraph
     */

    remove(node) {
        if (node.constructor.name === "LGraphGroup") {
            const index = this._groups.indexOf(node);
            if (index !== -1) {
                this._groups.splice(index, 1);
            }
            node.graph = null;
            this._version++;
            this.setDirtyCanvas(true, true);
            this.change();
            return;
        }

        if (this._nodes_by_id[node.id] == null) {
            return;
        } // not found

        if (node.ignore_remove) {
            return;
        } // cannot be removed

        this.beforeChange(); // sure?

        // disconnect inputs
        if (node.inputs) {
            for (let i = 0; i < node.inputs.length; i++) {
                const slot = node.inputs[i];
                if (slot.link != null) {
                    node.disconnectInput(i);
                }
            }
        }

        // disconnect outputs
        if (node.outputs) {
            for (let i = 0; i < node.outputs.length; i++) {
                const slot = node.outputs[i];
                if (slot.links != null && slot.links.length) {
                    node.disconnectOutput(i);
                }
            }
        }

        // node.id = -1; //why?

        // callback
        if (node.onRemoved) {
            node.onRemoved();
        }

        node.graph = null;
        this._version++;

        // remove from canvas render
        if (this.list_of_graphcanvas) {
            for (const canvas of this.list_of_graphcanvas) {
                if (canvas.selected_nodes[node.id]) {
                    delete canvas.selected_nodes[node.id];
                }
                if (canvas.node_dragged === node) {
                    canvas.node_dragged = null;
                }
            }
        }

        // remove from containers
        if (this._nodes.includes(node)) {
            this._nodes = this._nodes.filter((n) => n !== node);
        }
        delete this._nodes_by_id[node.id];

        if (this.onNodeRemoved) {
            this.onNodeRemoved(node);
        }

        // close panels
        this.sendActionToCanvas("checkPanels");

        this.setDirtyCanvas(true, true);
        this.afterChange(); // sure?
        this.change();

        this.updateExecutionOrder();
    }

    /**
     * Returns a node by its id.
     * @method getNodeById
     * @param {Number} id
     * @memberOf LGraph
     */

    getNodeById(id) {
        if (id == null) {
            return null;
        }
        return this._nodes_by_id[id];
    }

    /**
     * Returns a list of nodes that matches a class
     * @method findNodesByClass
     * @param {Class} classObject the class itself (not an string)
     * @param {Array} result
     * @return {Array} a list with all the nodes of this type
     * @memberOf LGraph
     */
    findNodesByClass(classObject, result = []) {
        result.length = 0;
        for (const node of this._nodes) {
            if (node.constructor === classObject) result.push(node);
        }
        return result;
    }

    /**
     * Returns a list of nodes that matches a type
     * @method findNodesByType
     * @param {String} type the name of the node type
     * @param {Array} result
     * @return {Array} a list with all the nodes of this type
     * @memberOf LGraph
     */
    findNodesByType(type, result = []) {
        type = type.toLowerCase();
        result = result || [];
        result.length = 0;
        for (const node of this._nodes) {
            if (node.type.toLowerCase() === type) result.push(node);
        }
        return result;
    }

    /**
     * Returns the first node that matches a name in its title
     * @method findNodeByTitle
     * @param {String} title the name of the node to search
     * @return {Node} the node or null
     * @memberOf LGraph
     */
    findNodeByTitle(title) {
        for (const node of this._nodes) {
            if (node.title === title) return node;
        }
        return null;
    }

    /**
     * Returns a list of nodes that matches a name
     * @method findNodesByTitle
     * @param {String} title the name of the node to search
     * @return {Array} a list with all the nodes with this name
     * @memberOf LGraph
     */
    findNodesByTitle(title) {
        const result = [];
        for (const node of this._nodes) {
            if (node.title === title) result.push(node);
        }
        return result;
    }

    /**
     * Returns the top-most node in this position of the canvas
     * @method getNodeOnPos
     * @param {number} x the x coordinate in canvas space
     * @param {number} y the y coordinate in canvas space
     * @param {Array} nodesList a list with all the nodes to search from, by default is all the
     *     nodes in the graph
     * @param {number} margin
     * @return {LGraphNode} the node at this position or null
     * @memberOf LGraph
     */
    getNodeOnPos(x, y, nodesList = this._nodes, margin) {
        for (const n of nodesList) {
            if (n.isPointInside(x, y, margin)) return n;
        }
        return null;
    }

    /**
     * Returns the top-most group in that position
     * @method getGroupOnPos
     * @param {number} x the x coordinate in canvas space
     * @param {number} y the y coordinate in canvas space
     * @return {LGraphGroup} the group or null
     * @memberOf LGraph
     */
    getGroupOnPos(x, y) {
        for (const g of this._groups) {
            if (g.isPointInside(x, y, 2, true)) return g;
        }
        return null;
    }

    /**
     * Checks that the node type matches the node type registered, used when replacing a nodetype
     * by a newer version during execution this replaces the ones using the old version with the
     * new version
     * @method checkNodeTypes
     * @memberOf LGraph
     */
    checkNodeTypes() {
        for (let node of this._nodes) {
            const ctor = defaultConfig.registered_node_types[node.type];
            if (node.constructor === ctor) {
                continue;
            }
            console.log(`node being replaced by newer version: ${node.type}`);
            const newnode = LGraphNode.createNode(node.type);
            node = newnode;
            newnode.configure(node.serialize());
            newnode.graph = this;
            this._nodes_by_id[newnode.id] = newnode;
            if (node.inputs) {
                newnode.inputs = node.inputs.concat();
            }
            if (node.outputs) {
                newnode.outputs = node.outputs.concat();
            }
        }
        this.updateExecutionOrder();
    }

    onAction(action, param) {
        this._input_nodes = this.findNodesByClass(
            LiteGraph.GraphInput,
            this._input_nodes,
        );
        for (const node of this._input_nodes) {
            if (node.properties.name !== action) {
                continue;
            }
            node.onAction(action, param);
            break;
        }
    }

    trigger(action, param) {
        if (this.onTrigger) {
            this.onTrigger(action, param);
        }
    }

    /**
     * Tell this graph it has a global graph input of this type
     * @method addGlobalInput
     * @param {String} name
     * @param {String} type
     * @param {*} value [optional]
     * @memberOf LGraph
     */
    addInput(name, type, value) {
        const input = this.inputs[name];
        if (input) {
            // already exist
            return;
        }

        this.beforeChange();
        this.inputs[name] = {
            name,
            type,
            value,
        };
        this._version++;
        this.afterChange();

        if (this.onInputAdded) {
            this.onInputAdded(name, type);
        }

        if (this.onInputsOutputsChange) {
            this.onInputsOutputsChange();
        }
    }

    /**
     * Assign a data to the global graph input
     * @method setGlobalInputData
     * @param {String} name
     * @param {*} data
     * @memberOf LGraph
     */
    setInputData(name, data) {
        const input = this.inputs[name];
        if (!input) {
            return;
        }
        input.value = data;
    }

    /**
     * Returns the current value of a global graph input
     * @method getInputData
     * @param {String} name
     * @return {*} the data
     * @memberOf LGraph
     */
    getInputData(name) {
        const input = this.inputs[name];
        if (!input) {
            return null;
        }
        return input.value;
    }

    /**
     * Changes the newName of a global graph input
     * @method renameInput
     * @param {String} oldName
     * @param {String} new_name
     * @memberOf LGraph
     */
    renameInput(oldName, newName) {
        if (newName === oldName) {
            return;
        }

        if (!this.inputs[oldName]) {
            return false;
        }

        if (this.inputs[newName]) {
            console.error("there is already one input with that newName");
            return false;
        }

        this.inputs[newName] = this.inputs[oldName];
        delete this.inputs[oldName];
        this._version++;

        if (this.onInputRenamed) {
            this.onInputRenamed(oldName, newName);
        }

        if (this.onInputsOutputsChange) {
            this.onInputsOutputsChange();
        }
    }

    /**
     * Changes the type of a global graph input
     * @method changeInputType
     * @param {String} name
     * @param {String} type
     * @memberOf LGraph
     */
    changeInputType(name, type) {
        if (!this.inputs[name]) {
            return false;
        }

        if (
            this.inputs[name].type
            && String(this.inputs[name].type).toLowerCase()
            === String(type).toLowerCase()
        ) {
            return;
        }

        this.inputs[name].type = type;
        this._version++;
        if (this.onInputTypeChanged) {
            this.onInputTypeChanged(name, type);
        }
    }

    /**
     * Removes a global graph input
     * @method removeInput
     * @param {String} name
     * @memberOf LGraph
     */
    removeInput(name) {
        if (!this.inputs[name]) {
            return false;
        }

        delete this.inputs[name];
        this._version++;

        if (this.onInputRemoved) {
            this.onInputRemoved(name);
        }

        if (this.onInputsOutputsChange) {
            this.onInputsOutputsChange();
        }
        return true;
    }

    /**
     * Creates a global graph output
     * @method addOutput
     * @param {String} name
     * @param {String} type
     * @param {*} value
     * @memberOf LGraph
     */
    addOutput(name, type, value) {
        this.outputs[name] = {
            name,
            type,
            value,
        };
        this._version++;

        if (this.onOutputAdded) {
            this.onOutputAdded(name, type);
        }

        if (this.onInputsOutputsChange) {
            this.onInputsOutputsChange();
        }
    }

    /**
     * Assign a data to the global output
     * @method setOutputData
     * @param {String} name
     * @param {String} value
     * @memberOf LGraph
     */
    setOutputData(name, value) {
        const output = this.outputs[name];
        if (!output) {
            return;
        }
        output.value = value;
    }

    /**
     * Returns the current value of a global graph output
     * @method getOutputData
     * @param {String} name
     * @return {*} the data
     * @memberOf LGraph
     */
    getOutputData(name) {
        const output = this.outputs[name];
        if (!output) {
            return null;
        }
        return output.value;
    }

    /**
     * Renames a global graph output
     * @method renameOutput
     * @param {String} oldName
     * @param {String} newName
     * @memberOf LGraph
     */
    renameOutput(oldName, newName) {
        if (!this.outputs[oldName]) {
            return false;
        }

        if (this.outputs[newName]) {
            console.error("there is already one output with that newName");
            return false;
        }

        this.outputs[newName] = this.outputs[oldName];
        delete this.outputs[oldName];
        this._version++;

        if (this.onOutputRenamed) {
            this.onOutputRenamed(oldName, newName);
        }

        if (this.onInputsOutputsChange) {
            this.onInputsOutputsChange();
        }
    }

    /**
     * Changes the type of a global graph output
     * @method changeOutputType
     * @param {String} name
     * @param {String} type
     * @memberOf LGraph
     */
    changeOutputType(name, type) {
        if (!this.outputs[name]) {
            return false;
        }

        if (
            this.outputs[name].type
            && String(this.outputs[name].type).toLowerCase()
            === String(type).toLowerCase()
        ) {
            return;
        }

        this.outputs[name].type = type;
        this._version++;
        if (this.onOutputTypeChanged) {
            this.onOutputTypeChanged(name, type);
        }
    }

    /**
     * Removes a global graph output
     * @method removeOutput
     * @param {String} name
     * @memberOf LGraph
     */
    removeOutput(name) {
        if (!this.outputs[name]) {
            return false;
        }
        delete this.outputs[name];
        this._version++;

        if (this.onOutputRemoved) {
            this.onOutputRemoved(name);
        }

        if (this.onInputsOutputsChange) {
            this.onInputsOutputsChange();
        }
        return true;
    }

    triggerInput(name, value) {
        const nodes = this.findNodesByTitle(name);
        for (let i = 0; i < nodes.length; ++i) {
            nodes[i].onTrigger(value);
        }
    }

    setCallback(name, func) {
        const nodes = this.findNodesByTitle(name);
        for (let i = 0; i < nodes.length; ++i) {
            nodes[i].setTrigger(func);
        }
    }

    // used for undo, called before any change is made to the graph
    beforeChange(info) {
        if (this.onBeforeChange) {
            this.onBeforeChange(this, info);
        }
        this.sendActionToCanvas("onBeforeChange", this);
    }

    // used to resend actions, called after any change is made to the graph
    afterChange(info) {
        if (this.onAfterChange) {
            this.onAfterChange(this, info);
        }
        this.sendActionToCanvas("onAfterChange", this);
    }

    connectionChange(node) {
        this.updateExecutionOrder();
        if (this.onConnectionChange) {
            this.onConnectionChange(node);
        }
        this._version++;
        this.sendActionToCanvas("onConnectionChange");
    }

    /**
     * returns if the graph is in live mode
     * @method isLive
     * @memberOf LGraph
     */

    isLive() {
        if (!this.list_of_graphcanvas) {
            return false;
        }

        for (let i = 0; i < this.list_of_graphcanvas.length; ++i) {
            const c = this.list_of_graphcanvas[i];
            if (c.live_mode) {
                return true;
            }
        }
        return false;
    }

    /**
     * clears the triggered slot animation in all links (stop visual animation)
     * @method clearTriggeredSlots
     * @memberOf LGraph
     */
    clearTriggeredSlots() {
        // eslint-disable-next-line guard-for-in,no-restricted-syntax
        for (const i in this.links) {
            const linkInfo = this.links[i];
            if (!linkInfo) {
                continue;
            }
            if (linkInfo._last_time) {
                linkInfo._last_time = 0;
            }
        }
    }

    /* Called when something visually changed (not the graph!) */
    change() {
        if (defaultConfig.debug) {
            console.log("Graph changed");
        }
        this.sendActionToCanvas("setDirty", [true, true]);
        if (this.on_change) this.on_change(this);
    }

    setDirtyCanvas(fg, bg) {
        this.sendActionToCanvas("setDirty", [fg, bg]);
    }

    /**
     * Destroys a link
     * @method removeLink
     * @param {Number} linkId
     * @memberOf LGraph
     */
    removeLink(linkId) {
        const link = this.links[linkId];
        if (!link) {
            return;
        }
        const node = this.getNodeById(link.target_id);
        if (node) {
            node.disconnectInput(link.target_slot);
        }
    }

    // save and recover app state ***************************************
    /**
     * Creates a Object containing all the info about this graph, it can be serialized
     * @method serialize
     * @return {Object} value of the node
     * @memberOf LGraph
     */
    serialize() {
        const nodesInfo = [];
        for (const node of this._nodes) {
            nodesInfo.push(node.serialize());
        }

        // pack link info into a non-verbose format
        const links = [];
        // eslint-disable-next-line guard-for-in,no-restricted-syntax
        for (const i in this.links) {
            // links is an OBJECT
            let link = this.links[i];
            if (!link.serialize) {
                // weird bug I havent solved yet
                console.warn(
                    "weird LLink bug, link info is not a LLink but a regular object",
                );
                const link2 = new LLink();
                // eslint-disable-next-line guard-for-in,no-restricted-syntax
                for (const j in link) {
                    link2[j] = link[j];
                }
                this.links[i] = link2;
                link = link2;
            }

            links.push(link.serialize());
        }

        const groupsInfo = [];
        for (const group of this._groups) groupsInfo.push(group.serialize());

        const data = {
            last_node_id: this.last_node_id,
            last_link_id: this.last_link_id,
            nodes: nodesInfo,
            links,
            groups: groupsInfo,
            config: this.config,
            extra: this.extra,
            version: defaultConfig.VERSION,
        };

        if (this.onSerialize) this.onSerialize(data);

        return data;
    }

    /**
     * Configure a graph from a JSON string
     * @method configure
     * @param {String} str configure a graph from a JSON string
     * @param {Boolean} returns if there was any error parsing
     * @memberOf LGraph
     */
    configure(data, keepOld) {
        if (!data) {
            return;
        }

        if (!keepOld) this.clear();

        const { nodes } = data;

        // decode links info (they are very verbose)
        if (data.links && data.links.constructor === Array) {
            const links = [];
            for (const linkData of data.links) {
                if (!linkData) {
                    console.warn("serialized graph link data contains errors, skipping.");
                    continue;
                }
                const link = new LLink();
                link.configure(linkData);
                links[link.id] = link;
            }
            data.links = links;
        }

        // copy all stored fields
        // eslint-disable-next-line guard-for-in,no-restricted-syntax
        for (const i in data) {
            if (i === "nodes" || i === "groups") {
                continue;
            }
            this[i] = data[i];
        }

        let error = false;

        // create nodes
        this._nodes = [];
        if (nodes) {
            for (const nInfo of nodes) {
                let node = LGraphNode.createNode(nInfo.type, nInfo.title);
                if (!node) {
                    if (defaultConfig.debug) {
                        console.log(
                            `Node not found or has errors: ${nInfo.type}`,
                        );
                    }

                    // in case of error we create a replacement node to avoid losing info
                    node = new LGraphNode();
                    node.last_serialization = nInfo;
                    node.has_errors = true;
                    error = true;
                    // continue;
                }

                node.id = nInfo.id; // id it or it will create a new id
                this.add(node, true); // add before configure, otherwise configure cannot create
                // links
            }

            // configure nodes afterwards so they can reach each other
            for (const nInfo of nodes) {
                const node = this.getNodeById(nInfo.id);
                if (node) {
                    node.configure(nInfo);
                }
            }
        }

        // groups
        this._groups.length = 0;
        if (data.groups) {
            for (const dataGroup of data.groups) {
                const group = new LGraphGroup();
                group.configure(dataGroup);
                this.add(group);
            }
        }

        this.updateExecutionOrder();

        this.extra = data.extra || {};

        if (this.onConfigure) this.onConfigure(data);

        this._version++;
        this.setDirtyCanvas(true, true);
        return error;
    }

    load(url, callback) {
        const that = this;

        // from file
        if (url.constructor === File || url.constructor === Blob) {
            const reader = new FileReader();
            reader.addEventListener("load", (event) => {
                const data = JSON.parse(event.target.result);
                that.configure(data);
                if (callback) callback();
            });

            reader.readAsText(url);
            return;
        }

        // is a string, then an URL
        const req = new XMLHttpRequest();
        req.open("GET", url, true);
        req.send(null);
        req.onload(() => {
            if (req.status !== 200) {
                console.error("Error loading graph:", req.status, req.response);
                return;
            }
            const data = JSON.parse(req.response);
            that.configure(data);
            if (callback) callback();
        });
        req.onerror((err) => {
            console.error("Error loading graph:", err);
        });
    }

    /**
     * Node event manager
     * @todo Need create event
     * @param node
     * @param msg
     * @param color
     * @memberOf LGraph
     */
    onNodeTrace(node, msg, color) {
        // TODO
    }
}