JSFiddle - React, Tailwind, and code Playground

by Josh Pullen

JavaScript

/* Node values are recomputed when their definition changes
	 or when one of their dependencies from last run changes. */

class Node {
  constructor(data, computeValue, graph = null) {
  	this.data = data;
    this.computeValue = computeValue;
    this.dependencies = [];
    
    this.cachedValue = undefined;
    
    this.listeners = {
      update: []
    };
    
    this.graph = graph;
  }
  
  value() {
  	if (!this.graph) {
    	console.log("Cannot compute value of node without graph");
      return null;
    }
    
    if (this.cachedValue !== undefined) {
    	return this.cachedValue;
    }
    
    const computedValue = this.computeValue(this.data, this.accessNodeValues());
    this.cachedValue = computedValue;

    return computedValue;
  }
  
  accessNodeValues() {
  	return new Proxy(this.graph.nodes, {
    	get: (obj, prop) => {
      	const node = obj[prop];
      	node.on("update", this.invalidateCache.bind(this));
        return node;
      }
    });
  }
  
  invalidateCache() {
  	this.cachedValue = undefined;
  }
  
  update(data) {
    this.data = { ...this.data, ...data };
    this.invalidateCache();
  	this._fireEvent("update");
  }
  
  on(event, callback) {
  	this.listeners[event].push(callback);
  }
  
  _fireEvent(event) {
  	this.listeners[event].forEach(callback => callback());
    this.listeners[event] = [];
  }
}

class Graph {
	constructor(nodes) {
  	this.nodes = nodes;
    
    for (const node of Object.values(this.nodes)) {
    	node.graph = this;
    }
  }
}

const graph = new Graph({
	nodeA: new Node(
  	{
      name: "nodeA",
      number: 5
    },
    (data) => data.number
  ),
  nodeB: new Node(
  	{
      name: "nodeB",
      number: 15
    },
    (data) => data.number
  ),
  nodeC: new Node(
  	{
    	name: "nodeC",
      target: "nodeA"
    },
    (data, nodes) => nodes[data.target].value() ** 2
  ),
  nodeD: new Node(
  	{
    	name: "nodeD",
    },
    (data, nodes) => nodes.nodeC.value() +...