The rete-engine
is a package that implements two approaches for processing graph: Dataflow and Control flow
The dataflow approach is focused solely on data, where the target node requests data from incoming nodes. Graph processing proceeds from left to right, passing the output of nodes as input arguments to outgoing nodes.
This approach is commonly used in products with node editors such as Blender.
The code below represents the basic constructs required for the DataflowEngine
to work:
data
method: this method accepts data from incoming nodesfetch
initiates a graph traversal starting from the target node and visiting all its predecessorsimport { ClassicPreset } from 'rete-engine'
import { DataflowEngine } from 'rete-engine'
const { Node, Socket } = ClassicPreset
class NodeAdd extends Node<{ left: Socket, right: Socket }, { value: Socket }, { }> {
constructor() {
// init controls and ports
}
// mandatory method
data(inputs: { left?: number[], right?: number[] }): { value: number } {
const left = inputs.left[0] || 0
const right = inputs.right[0] || 0
return {
value: left + right
}
}
}
const engine = new DataflowEngine<Schemes>()
editor.use(engine)
const nodeOutput = await engine.fetch(resultNode.id)
Control flow is a node traversal approach that allows you to determine how to pass control to the next nodes.
The processing starts at the start node, which specifies how control is passed through its outgoing connections. For instance, it can be a delay or a branching. The closest example is UE4 Blueprints.
import { ControlFlowEngine } from 'rete-engine'
const { Node, Socket } = ClassicPreset
class Log extends Node<{ enter: Socket }, { out: Socket }, {}> {
constructor() {
// init ports
}
// mandatory method
execute(input: 'enter', forward: (output: 'out') => void) {
console.log('log something')
forward('out')
}
}
const engine = new ControlFlowEngine<Schemes>()
editor.use(engine)
engine.execute(startNode.id)
In addition, these approaches can be combined. For example, ports named 'exec' can be used to control flow, while other ports manage data.
const controlflow = new ControlFlowEngine<Schemes>(node => {
return {
inputs: () => ['exec'],
outputs: () => ['exec']
}
})
const dataflow = new DataflowEngine<Schemes>(({ inputs, outputs }) => {
return {
inputs: () => Object.keys(inputs).filter(name => name !== 'exec'),
outputs: () => Object.keys(outputs).filter(name => name !== 'exec')
}
})
Alternatively, you can use the Dataflow
and ControlFlow
classes directly, affording more precise control over the graph processing.
import { ControlFlow, Dataflow } from 'rete-engine'
const control = new ControlFlow(editor)
const dataflow = new Dataflow(editor)
control.add(startNode, {
inputs: () => [],
outputs: () => ['exec'],
async execute(input, forward) {
const inputs = await dataflow.fetchInputs(startNode.id)
forward('exec')
}
})
dataflow.add(startNode, {
inputs: () => ['data'],
outputs: () => ['data'],
data(fetchInputs) {
const inputs = await fetchInputs()
const data = {
data: inputs.data[0] // forward input data (assuming there is only one input connection to port "data")
}
return data
}
})
This engine version incorporates revised approaches to graph processing and addresses the shortcomings of the previous version, which was initially geared towards strict dataflow without recursion support.
When it comes to graph processing, there's no one-size-fits-all solution. In simple cases, you can use DataflowEngine
and ControlFlowEngine
, while in more complex cases, you can use ControlFlow
and Dataflow
or write your own solution by studying the source code of the rete-engine
package