Skip to content

Instantly share code, notes, and snippets.

@xettri
Last active February 28, 2026 13:58
Show Gist options
  • Select an option

  • Save xettri/6aae238a8e7c0a18c62f5e2b3c9b799b to your computer and use it in GitHub Desktop.

Select an option

Save xettri/6aae238a8e7c0a18c62f5e2b3c9b799b to your computer and use it in GitHub Desktop.

FlowEngine Implementation Example

Below is the TypeScript class implementation for managing a flat, normalized state architecture for a visual flow builder. It demonstrates how to initialize the normalized dictionaries and perform operations in $O(1)$ time.

// 1. Define Entity Interfaces / Classes
class FlowNode {
  id: string;
  label: string;
  type: string;

  constructor(id: string, label: string, type: string) {
    this.id = id;
    this.label = label;
    this.type = type;
  }
}

class FlowEdge {
  id: string;
  sourceId: string;
  targetId: string;
  ruleId?: string; // Optional foreign key to a complex routing rule

  constructor(id: string, sourceId: string, targetId: string, ruleId?: string) {
    this.id = id;
    this.sourceId = sourceId;
    this.targetId = targetId;
    this.ruleId = ruleId;
  }
}

class FlowEvent {
  id: string;
  nodeId: string; // Foreign Key linking to FlowNode
  actionType: string;
  payload: any;

  constructor(id: string, nodeId: string, actionType: string, payload: any) {
    this.id = id;
    this.nodeId = nodeId;
    this.actionType = actionType;
    this.payload = payload;
  }
}

// 2. The Engine Manager
class FlowEngine {
  // Flat Normalized Dictionaries (Hash Maps)
  private nodes: Map<string, FlowNode>;
  private edges: Map<string, FlowEdge>;
  private events: Map<string, FlowEvent>;

  // Optional secondary index to make looking up events for a node O(1)
  private nodeEventsIndex: Map<string, Set<string>>;

  constructor() {
    this.nodes = new Map();
    this.edges = new Map();
    this.events = new Map();
    this.nodeEventsIndex = new Map();
  }

  // O(1) - Add a visual block
  public addNode(id: string, label: string, type: string): void {
    const node = new FlowNode(id, label, type);
    this.nodes.set(node.id, node);
    this.nodeEventsIndex.set(node.id, new Set());
  }

  // O(1) - Instantly attach an event/action deep in a node without traversal
  public attachEvent(nodeId: string, actionType: string, payload: any): string {
    if (!this.nodes.has(nodeId)) {
      throw new Error(`Node ${nodeId} does not exist in graph.`);
    }

    const eventId = `evt_${Date.now()}_${Math.random().toString(36).substr(2, 5)}`;
    const newEvent = new FlowEvent(eventId, nodeId, actionType, payload);

    // Direct Dictionary Insertion
    this.events.set(newEvent.id, newEvent);

    // Update Secondary Index
    this.nodeEventsIndex.get(nodeId)?.add(eventId);

    return eventId;
  }

  // O(1) - Fast UI rendering lookup
  public getEventsForNode(nodeId: string): FlowEvent[] {
    const eventIds = this.nodeEventsIndex.get(nodeId) || new Set();
    const result: FlowEvent[] = [];

    eventIds.forEach((id) => {
      const evt = this.events.get(id);
      if (evt) result.push(evt);
    });

    return result;
  }

  // O(E + V) Garbage Collection - Only run when explicitly deleting a node
  public deleteNode(nodeId: string): void {
    if (!this.nodes.has(nodeId)) return;

    // 1. Delete the node
    this.nodes.delete(nodeId);

    // 2. Cascade Delete Events linked to this node
    const eventIds = this.nodeEventsIndex.get(nodeId) || new Set();
    eventIds.forEach((id) => this.events.delete(id));
    this.nodeEventsIndex.delete(nodeId);

    // 3. Cascade Delete Edges that touch this node
    for (const [edgeId, edge] of this.edges.entries()) {
      if (edge.sourceId === nodeId || edge.targetId === nodeId) {
        this.edges.delete(edgeId);
      }
    }
  }
}

// ==========================================
// Example Execution
// ==========================================

const engine = new FlowEngine();

// UI creates a new node
engine.addNode("node_A", "My Condition", "trigger");

// The user interacts with UI and attaches "Run API" to Node A.
// Time Complexity: O(1). Zero nodes were traversed to find Node A.
const newEventId = engine.attachEvent("node_A", "webhook_trigger", {
  url: "https://api.example.com/v1/trigger",
});

console.log(engine.getEventsForNode("node_A"));

High-Level Design (HLD): State Architecture for Flow Builders

1. Executive Summary

When engineering visual flow builders, automation sequences, or user journey editors, the core frontend state architecture strictly dictates the application's scalability.

This document outlines the fatal performance bottleneck caused by representing a runtime graph as a deeply nested tree. It presents a normalized, relational data architecture as the industry-standard solution, complete with an algorithmic time complexity analysis and implementation patterns.


2. The Core Bottleneck: The Nested Tree Anti-Pattern

Let's assume a standard decision-tree flow:

(Node A) ---> (Node B) ---> (Node C)
                 |
                 v
              (Node D)

The Declarative Nested JSON Structure

A common, yet fundamentally flawed, approach is to model this flow using a declarative nested JSON tree where child branches are stored inside the properties of their parent nodes.

{
  "id": "Node_A",
  "label": "Start Process",
  "children": [
    {
      "id": "Node_B",
      "label": "Check Condition",
      "children": [
        { "id": "Node_C", "label": "Send Email", "children": [] },
        { "id": "Node_D", "label": "Send SMS", "children": [] }
      ]
    }
  ]
}

The Performance Cost

If the application allows a user to select Node D and attach an event trigger (On Click -> Trigger Webhook), the nested architecture fails at scale due to compounding algorithmic penalties. Let $N$ be the total number of nodes and $D$ be the depth of the tree.

  1. Traversal Overhead: To locate Node D in memory, the engine must recursively execute Depth-First Search (DFS) or Breadth-First Search (BFS).
    • Time Complexity: $O(N)$
  2. Immutability Penalty: Modern UI frameworks (like React) require immutable state updates. Updating Node D requires allocating memory for a clone of D, cloning B to point to the new D, and cloning A to point to the new B.
    • Space/Time Complexity: $O(D)$ allocations per keystroke.
  3. Cascading UI Rendering: Because the root container (Node A) reference changed in memory, the UI rendering engine invalidates the entire tree, forcing a destructive re-render of every visual element on the canvas.
    • Render Complexity: $O(N)$

3. The Solution: Relational Flat State (Entity-System)

To resolve the $O(N)$ traversal and rendering penalties, the directed graph must be flattened into independent dictionaries (Hash Maps). The UI state should act as a normalized relational database.

Entities are split by their responsibility:

  1. Nodes Table: Canvas visualization data (X/Y coordinates, labels).
  2. Edges Table: Connection relationships (Source ID -> Target ID).
  3. Events Table: Runtime logic and attachments bound by nodeId.
  4. Rules Table: Conditional traversal logic bound by edgeId.

The Optimized Data Structure (Normalized Dictionary)

{
  "nodes": {
    "Node_A": { "id": "Node_A", "label": "Start Process" },
    "Node_B": { "id": "Node_B", "label": "Check Condition" },
    "Node_C": { "id": "Node_C", "label": "Send Email" },
    "Node_D": { "id": "Node_D", "label": "Send SMS" }
  },
  "edges": {
    "edge_1": { "id": "edge_1", "source": "Node_A", "target": "Node_B" },
    "edge_2": { "id": "edge_2", "source": "Node_B", "target": "Node_C" },
    "edge_3": { "id": "edge_3", "source": "Node_B", "target": "Node_D" }
  },
  "events": {}
}

4. Algorithmic Time Complexity Analysis

Mathematical comparison between the Nested Tree anti-pattern and the Flat Map architecture. ($N$ = Nodes, $E$ = Edges, $V$ = Events attached).

Operation Nested Tree (Anti-Pattern) Flat Map (Industry Standard) Why the Flat Map Wins
Find Node/Event $O(N)$ $O(1)$ Hash Map direct key lookup accesses memory instantly.
Attach Event/Action $O(N)$ $O(1)$ Insert directly into events dictionary without rebuilding node trees.
Update Node Label $O(N)$ $O(1)$ Reassign nodes[id].label with zero traversal required.
UI Re-Renders $O(N)$ (Entire Canvas) $O(1)$ (Isolated Component) Immutability is constrained only to the specific dictionary being mutated.
Delete Node $O(1)$ (Subtree drop) $O(E + V)$ (Garbage Collection) Trade-off: Flat models require sweeping edges/events to purge orphaned IDs.

5. Implementation: How to Attach a New Event or Action

Let's revisit the scenario. The user interacts with Node D situated deep in the logical flow and attaches an action: "On Click -> Run API".

The Step-By-Step Process

Under the normalized architecture, the sequence of operations bypasses the nodes entirely.

  1. Generate a Unique Identifier. Create a UUID for the newly instantiated event (evt_99).
  2. Construct the Payload. Define the action logic and map it directly to the target node using a Foreign Key reference ("nodeId": "Node_D").
  3. Dictionary Insertion (O(1)). Write the payload directly to the events table.

State Mutation Example:

// O(1) Insertion - No tree traversal required
state.events["evt_99"] = {
  id: "evt_99",
  nodeId: "Node_D",
  type: "onClick",
  action: "execute_workflow",
  payload: { targetApi: "/v1/trigger" },
};

Why this is mathematically superior:

The operation executed in constant time ($O(1)$). The nodes and edges dictionaries in memory remained strictly untouched. The rendering engine will only flag components specifically subscribed to the events dictionary to repaint, saving thousands of CPU cycles on large canvases.


6. Deep Dive: Complex Attachments (Routing Rules)

In production environments (e.g., e-commerce journey builders), nodes require advanced logic. A node might conditionally branch based on complex Routing Rules.

Suppose Node B (Cart Check) triggers Node C (Email) only if the cart value exceeds $100.

{
  "edges": {
    "edge_2": {
      "id": "edge_2",
      "source": "node_B",
      "target": "node_C",
      "ruleId": "rule_1001" // Reference mapping bypasses nesting
    }
  },
  "rules": {
    "rule_1001": {
      "id": "rule_1001",
      "type": "math_evaluation",
      "operator": "greater_than",
      "value": 100
    }
  }
}

Architectural Benefits of Flat Rules

  1. Reusability and Single Source of Truth: Multiple edges (edge_5, edge_9) can point to the same "ruleId": "rule_1001". Updating one rule instantly propagates logic changes across the entire graph.
  2. Instant Event Binding: Assigning background callbacks (e.g., "Ping Analytics API on node success") is accomplished by injecting a new key into the events map. The visual node schema remains perfectly clean and unmodified.

7. Summary Guidelines

  1. Banish Nested State: Deeply nested JSON trees must never act as the source of truth for highly mutable, interactive graph editors. They degrade logarithmically as applications scale.
  2. Normalize Relationships: Flatten all state entities down to nodes, edges, events, and rules linked exclusively via unique string identifiers (Foreign Keys).
  3. Decouple Data: This architectural paradigm guarantees constant time ($O(1)$) read and update operations, ensuring the user interface maintains 60 Frames-Per-Second (FPS) rendering fidelity across graphs exceeding tens of thousands of simultaneous nodes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment