Engine - Rete.js

Engine

Architecture

The rete-engine is a package that implements two approaches for processing graph: Dataflow and Control flow

Dataflow

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.

Dataflow

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:

  • Nodes must implement a data method: this method accepts data from incoming nodes
  • Connect the engine to the editor: the engine will register every added node for further processing
  • Fetching node data: fetch initiates a graph traversal starting from the target node and visiting all its predecessors
ts
import { 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

Control flow is a node traversal approach that allows you to determine how to pass control to the next nodes.

Control flow

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.

ts
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)

Hybrid

In addition, these approaches can be combined. For example, ports named 'exec' can be used to control flow, while other ports manage data.

ts
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.

ts
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
  }
})

Conclusion

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