UNPKG

@tokens-studio/graph-engine

Version:

An execution engine to handle Token Studios generators and resolvers

324 lines 9.92 kB
import { Graph } from '../graph/index.js'; import { Input } from './input.js'; import { Output } from './output.js'; import { action, computed, makeObservable, observable } from 'mobx'; import { annotatedNodeRunning } from '../annotations/index.js'; import { v4 as uuid } from 'uuid'; import getDefaults from 'json-schema-defaults-esm'; export function isSubgraphContainer(node) { return (node instanceof Node && typeof node.getSubgraphs === 'function'); } export class Node { /** * Unique instance specific identifier */ id; static description; static title; static annotations = {}; /** * The groups this node belongs to as a string array */ static groups; static type = 'unknown'; /** * This holds the definitions of the inputs and outputs */ inputs = {}; outputs = {}; annotations = {}; lastExecutedDuration = 0; _graph; error = null; constructor(props) { this.id = props.id || uuid(); this._graph = props.graph; if (this._graph) { this._graph.addNode(this); } makeObservable(this, { inputs: observable.shallow, outputs: observable.shallow, error: observable.ref, annotations: observable.shallow, addInput: action, isRunning: computed, run: action, execute: action, addOutput: action, clearOutputs: action, setAnnotation: action, removeInput: action, removeOutput: action }); //Defined nodes would be specified here } /** * Creates a new input and adds it to the node * @param name * @param input */ addInput(name, type) { //Extract the default value from the schema return (this.inputs[name] = new Input({ name, ...type, visible: type.visible !== undefined ? type.visible : true, value: getDefaults(type.type), node: this })); } addOutput(name, type) { this.outputs[name] = new Output({ name, ...type, type: type.type, visible: type.visible !== undefined ? type.visible : true, value: getDefaults(type.type), node: this }); } setAnnotation(key, value) { this.annotations[key] = value; } /** * Removes a named input from the node. This should only be used for dynamic inputs * @param name */ removeInput(name) { if (this._graph) { this._graph.inEdges(this.id, name).forEach(edge => { if (edge.id) { return; } this._graph?.removeEdge(edge.id); }); } delete this.inputs[name]; if (this._graph) { //Ask to be recalculated this._graph?.update(this.id); } } removeOutput(name) { if (this._graph) { this._graph.outEdges(this.id, name).forEach(edge => { if (edge.id) { return; } this._graph?.removeEdge(edge.id); }); } delete this.outputs[name]; //We do not need to be recalculated } /** * This is the place to add applicate specific logic to execute the node. * This can be used directly, but you should preferably never call this and instead execute from the graph which will control the lifecycle of the node * @override */ execute() { } /** * Runs the node. Internally this calls the execute method, but the run entrypoint allows for additional tracking and lifecycle management */ async run() { this.annotations[annotatedNodeRunning] = true; const start = performance.now(); this.getGraph()?.emit('nodeStarted', { node: this, start }); try { await this.execute(); this.error = null; } catch (err) { this._graph.logger.error(err); this.error = err; } const end = performance.now(); delete this.annotations[annotatedNodeRunning]; this.lastExecutedDuration = end - start; const result = { node: this, error: this.error, start, end }; this.getGraph()?.emit('nodeExecuted', result); return result; } /** * Asks the controlling graph to load a resource. * This cannot be called if the node is not part of a graph * @param uri * @param data */ async load(uri, data) { return this._graph?.loadResource(uri, this, data); } get isRunning() { return !!this.annotations[annotatedNodeRunning]; } /** * Clears all the outputs */ clearOutputs() { this.outputs = {}; } setGraph(graph) { this._graph = graph; } getGraph() { return this._graph; } clone(newGraph) { // Create a new instance using the constructor const clonedNode = new this.factory({ graph: newGraph, id: uuid() }); // Copy input values Object.entries(this.inputs).forEach(([key, input]) => { if (input.variadic) return; if (clonedNode.inputs[key]) { clonedNode.inputs[key].setValue(input.value); } else { clonedNode.inputs[key] = input.clone(); } }); Object.entries(this.outputs).forEach(([key, output]) => { clonedNode.outputs[key] = output.clone(); }); clonedNode.annotations = { ...this.annotations }; if (isSubgraphContainer(this)) { const graphProps = this.getGraphProperties(); for (const propertyName in graphProps) { if (Object.hasOwn(graphProps, propertyName)) { const graphInstance = graphProps[propertyName]; if (graphInstance instanceof Graph && typeof graphInstance.clone === 'function') { const clonedGraph = graphInstance.clone(); // assign cloned graph to the same property on the cloned node clonedNode[propertyName] = clonedGraph; } } } } return clonedNode; } /** * Get the type of the nodes * @returns */ nodeType = () => { //@ts-ignore return this.constructor.type; }; /** * Returns the underlying class of the node. Useful for getting class specific properties * @returns */ get factory() { //@ts-ignore return this.constructor; } /** * Serializes the node value for storage * @returns */ serialize() { const serialized = { id: this.id, type: this.nodeType(), //Filter out any inputs that are connected as they will be serialized as part of the edge inputs: Object.values(this.inputs).map(x => x.serialize()) }; if (Object.keys(this.annotations).length > 0) { serialized.annotations = this.annotations; } return serialized; } /** * How to deserialize the node * @param serialized * @returns */ static async deserialize(opts) { const newNode = new this({ id: opts.serialized.id, ...opts }); newNode.annotations = opts.serialized.annotations || {}; //Set the values directly from the save values opts.serialized.inputs.forEach(input => { //Attempt a lookup by name const foundInput = newNode.inputs[input.name]; if (!foundInput) { //We need to create the input, it might be dynamic so we need to create it newNode.inputs[input.name] = new Input({ name: input.name, type: input.type, variadic: input.variadic, visible: input.visible, value: input.value, node: newNode, annotations: input.annotations }); } else { //Set the value from the saved value foundInput.deserialize(input); } }); return newNode; } static getType = () => { return this.type; }; getAllInputs = () => { return Object.fromEntries(Object.entries(this.inputs).map(([key, value]) => [key, value.value])); }; /** * Handles cleanup for nodes with state. * Use the super method to clear the graph reference * * @example * ```typescript * class MyNode extends Node { * dispose() { * * Node.prototype.dispose.call(this); * // or if you have full ES6 support * super.dispose(); * * //Some additional manual cleanup * // ... * } */ dispose = () => { //@ts-ignore This is forcing manual cleanup this._graph = undefined; }; /** * Function to call when the graph has been started. * This is only really necessary for nodes that need to do something when the graph is expected to be running continuously */ onStart = () => { }; onStop = () => { }; /** * By default, the node will stop when the graph is paused */ onPause = () => { this.onStop(); }; /** * By default, the node will start when the graph is resumed */ onResume = () => { this.onStart(); }; } //# sourceMappingURL=node.js.map