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
// 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"));