@tokens-studio/graph-engine
Version:
An execution engine to handle Token Studios generators and resolvers
324 lines • 9.92 kB
JavaScript
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