Rete.js Framework

repository·main·Indexed 11 days ago

https://github.com/retejs/rete

A JavaScript framework for building visual programming interfaces and workflows, supporting both dataflow and control flow graph processing. It is compatible with React, Vue, Angular, and Svelte. Version 2.0.6 features the NodeEditor for graph management, a reactive state system using Scope and Signal, and the ClassicPreset for defining nodes, ports, and connections.

Tokens
5.1K
Snippets
18
Records
21
Agent score
95%

What's inside Rete.js

  1. What is Rete.js?

    main

    Rete.js is a JavaScript framework designed for creating visual interfaces and workflows. It is built to handle two primary types of graph processing:

    1. Dataflow: Processing based on the flow of data through nodes.
    2. Control flow: Processing based on the execution path or logic flow.

    The framework provides out-of-the-box solutions for visualization that can be integrated with various UI libraries and frameworks.

  2. Quickly set up a Rete.js application with Rete Kit

    main

    To quickly scaffold a Rete.js application, use rete-kit. This tool allows you to select your preferred technology stack (React.js, Vue.js, Angular, or Svelte) and choose the specific set of features you want to include in your project.

    npx rete-kit app
  3. Use Scope for reactive dataflow and middleware

    main

    A Scope is a base class used by the core and plugins to provide a signal mechanism for modifying data through a chain of middleware (pipes). It allows for hierarchical dataflow where a scope can have a parent, enabling nested signal execution.

    Key Capabilities:

    • Middleware (Pipes): Add functions to a Scope using addPipe to transform data as it flows through the signal.
    • Hierarchical Scopes: Use .use(scope) to connect a child scope to a parent. When the parent emits data, it automatically triggers the child's signal chain.
    • Data Emission: Use .emit(context) to start the middleware chain with an initial value.

    Middleware (Pipe) Behavior:

    • A pipe can be synchronous or asynchronous (Promise).
    • If a pipe returns undefined, the entire execution chain stops immediately.
    import { Scope } from 'rete';
    
    // Create a scope for a specific data type
    const myScope = new Scope<'string' | 'number'>('my-scope');
    
    // Add a middleware pipe
    myScope.addPipe((data) => {
      console.log('Current data:', data);
      return typeof data === 'string' ? data.length : data;
    });
    
    // Emit data to trigger the pipes
    async function run() {
      const result = await myScope.emit('hello');
      console.log('Result:', result); // Result: 5
    }
  4. Define custom Node and Connection schemes using GetSchemes

    main

    When building custom node or connection data structures, use the GetSchemes type to define the relationship between your Node data and Connection data. This ensures your editor or engine knows the exact shape of the data being passed through the graph.

    GetSchemes takes two generic parameters: NodeData (which must extend NodeBase) and ConnectionData (which must extend ConnectionBase).

    // Example: Defining a custom scheme where nodes have a 'myProp' and connections are standard
    type MySchemes = GetSchemes<Node & { myProp: number }, Connection>;
  5. Use NodeEditor to manage the graph

    main

    The NodeEditor class is the primary entry point for managing a visual programming graph. It handles the lifecycle of nodes and connections, providing methods to add, remove, and retrieve them.

    NodeEditor extends Scope, meaning it uses an event-driven system where operations (like adding a node) emit signals. You can intercept these signals to implement validation or custom logic. If a signal handler returns false, the operation is cancelled (e.g., addNode will not complete if the nodecreate signal is cancelled).

    Key capabilities:

    • Node Management: Add, remove, and retrieve nodes by ID.
    • Connection Management: Add, remove, and retrieve connections by ID.
    • Graph Clearing: Remove all nodes and connections via clear().
    • Signal Interception: Listen to lifecycle events like nodecreate, connectionremove, and cleared.
    import { NodeEditor } from 'rete';
    
    const editor = new NodeEditor<MyScheme>();
    
    // Adding a node
    await editor.addNode({ id: 'node_1', ... });
    
    // Removing a node
    await editor.removeNode('node_1');
    
    // Getting all nodes
    const nodes = editor.getNodes();
  6. Define Sockets and Ports

    main

    In the classic scheme, Socket and Port define how connections are structured:

    • Socket: A named object used to group compatible ports. Ports can only connect if they share the same socket type.
    • Port: A base class for Input and Output.
      • label: A string displayed to the user.
      • multipleConnections: A boolean determining if multiple connections can attach to this port. For Output, this defaults to true. For Input, it defaults to false.
    • Input: An extension of Port that can host a Control. Use addControl(control) to attach UI elements to the input.
    • Output: An extension of Port used to send data from a node.
    import { Socket, Input, Output } from 'rete';
    
    const mySocket = new Socket('number-socket');
    
    // Input port: only one connection allowed by default
    const input = new Input(mySocket, 'Input Label');
    
    // Output port: multiple connections allowed by default
    const output = new Output(mySocket, 'Output Label');
    
    // Output port: explicitly restricting to a single connection
    const singleOutput = new Output(mySocket, 'Single Output', false);
  7. Connect scopes using the use() method

    main

    The use method allows you to nest one Scope inside another. When you call scopeA.use(scopeB), scopeB becomes a child of scopeA. Any data emitted by scopeA will be passed into scopeB's signal chain.

    Note: The TypeScript types enforce that the child scope's required signals (parents) are satisfied by the parent scope's produced signals. If there is a type mismatch, the compiler will provide an error message.

    If you encounter type errors during scope connection, you can use the .debug($ => $) helper to inspect the assignment error.

    const parentScope = new Scope<{ a: number }>('parent');
    const childScope = new Scope<{ b: string }>('child');
    
    // Connect child to parent
    // This automatically adds a pipe to parentScope that emits to childScope
    parentScope.use(childScope);
    
    // When parent emits, child receives it
    await parentScope.emit({ a: 10 });
  8. Configure Rete.js build settings via rete.config.ts

    main

    The rete.config.ts file is used to define the build configuration for the Rete project, utilizing the ReteOptions type from rete-cli. This configuration typically specifies the entry point, the output name, global dependencies, and Rollup plugins used during the build process.

    import { ReteOptions } from 'rete-cli'
    import copy from 'rollup-plugin-copy'
    
    export default <ReteOptions>{
      input: 'src/index.ts',
      name: 'Rete',
      globals: {
        crypto: 'crypto'
      },
      plugins: [
        copy({
          targets: [
            { src: 'postinstall.js', dest: 'dist' }
          ]
        })
      ]
    }
  9. Use Scope and Signal for reactive state management

    main

    Rete.js uses Scope and Signal to manage data flow and reactivity within the editor. Signal is used for reactive values, while Scope provides a mechanism for managing hierarchical data or parameters.

    import { Scope, Signal } from 'rete';
    
    // Signal can be used to track changes to specific values
    const mySignal = new Signal<number>(0);
    
    // Scope can be used to manage nested contexts
    const myScope = new Scope();
  10. Use InputControl for Node Data

    main

    The InputControl class allows you to add interactive elements (like text fields or number inputs) directly to a node. It extends the base Control class.

    Supported types: 'text' | 'number'

    Options:

    • readonly: (boolean) If true, the control cannot be edited. Defaults to false.
    • initial: The starting value of the control.
    • change: A callback function (value: N) => void triggered when the value is updated via setValue().

    Use setValue(value) to programmatically update the control's value.

    import { InputControl } from 'rete';
    
    const textControl = new InputControl('text', {
      initial: 'Hello',
      readonly: false,
      change: (val) => console.log('New value:', val)
    });
    
    // Later in your code...
    textControl.setValue('World');
  11. Create a Node with Inputs, Outputs, and Controls

    main

    The Node class is the primary building block of a Rete.js graph. You can define a node by providing a label and then adding Input, Output, and Control instances to it.

    Key methods:

    • addInput(key, input): Adds an input port.
    • addOutput(key, output): Adds an output port.
    • addControl(key, control): Adds a control (like a text or number input) to the node.
    • removeInput(key), removeOutput(key), removeControl(key): Removes the specified element.
    • hasInput(key), hasOutput(key), hasControl(key): Checks for existence.
    import { Node, Input, Output, Socket, Control, InputControl } from 'rete';
    
    const socket = new Socket('socket');
    const node = new Node('Math Node');
    
    // Add an input
    node.addInput('a', new Input(socket, 'Number A'));
    
    // Add an output
    node.addOutput('res', new Output(socket, 'Result'));
    
    // Add a control (e.g., a text input)
    node.addControl('name', new InputControl('text', { 
      initial: 'Default Name', 
      change: (val) => console.log('Changed to:', val) 
    }));
  12. Create a Connection between Nodes

    main

    The Connection class represents a link between a source node's output and a target node's input. When instantiating a Connection, Rete.js validates that the specified output key exists on the source node and the input key exists on the target node.

    Constructor parameters:

    • source: The source Node instance.
    • sourceOutput: The key of the output port on the source node.
    • target: The target Node instance.
    • targetInput: The key of the input port on the target node.
    import { Connection, Node, Input, Output, Socket } from 'rete';
    
    const socket = new Socket('s');
    
    const nodeA = new Node('A');
    nodeA.addOutput('out', new Output(socket, 'Out'));
    
    const nodeB = new Node('B');
    nodeB.addInput('in', new Input(socket, 'In'));
    
    // Create the connection
    const connection = new Connection(nodeA, 'out', nodeB, 'in');