Calculations

A value on its own does nothing. A calculation reads some values and derives a new one. This is where nodes gain their dependency edges, the graph that the architecture depends on. The edges are inputNodeRefs, the nodes that each node reads, and listeners, the nodes that read each one.

const score       = Reactive.State(1000);
const incremented = Reactive.Calculate([score], (n) => n + 1);
A calculation derives a new value from the ones it reads

Dependency Edges

A calculation depends on other nodes. As a result, a node now carries the graph itself, two fields alongside its payload and value. These fields are inputNodeRefs, the nodes it reads, and listeners, a Set of the listener nodes that read this one.

{
  [Reactive._reactive]: true,
  scope,
  payload,
  value,
  inputNodeRefs,   // the nodes this node reads, in order
  listeners,       // a Set of the listener nodes that read this one
};
A node now holds its own edges, both directions

So _nodeCreate grows. It takes the input nodes. It stores them and an empty listener Set on the node. Once the node exists, it wires a listener edge from each input back to it.

Reactive._nodeCreate = (payload, value, inputNodeRefs) => {
  const scope = Reactive._currentScope;
  const node = {
    [Reactive._reactive]: true,
    scope,
    payload,
    value,
    inputNodeRefs,
    listeners: new Set(),
  };
  scope.nodes.push(node);
  inputNodeRefs.forEach((inputNode) => {
    inputNode.listeners.add(node);
  });
  return node;
};
The node builder now records inputs and wires listener edges

Constant and State read nothing, so they pass an empty list of inputs:

Reactive.Constant = (value) => {
  const payload = { constant: {} };
  return Reactive._nodeCreate(payload, value, []);
};

Reactive.State = (initialValue) => {
  const payload = { state: {} };
  const value = Reactive._value(initialValue);
  return Reactive._nodeCreate(payload, value, []);
};
Input-less nodes pass an empty input list

Reactive.Calculate

Calculate resolves its inputs to references and reads their current values. It runs the callback over them to seed the result. It then creates the node from that value and those inputs. The node keeps only the callback. _nodeCreate wires its edges.

Reactive.Calculate = (inputs, callback) => {
  const inputNodeRefs = Reactive._resolveInputs(inputs);
  const inputValues = Reactive._values(inputNodeRefs);
  const calculatedValue = callback(...inputValues);
  const payload = { calculate: callback };
  return Reactive._nodeCreate(payload, calculatedValue, inputNodeRefs);
};
Read the inputs, run the callback, keep the callback for later

Helper Functions

An input is either a reactive reference or a plain value. _resolveInput turns one into a node: a reference is used as it is, a plain value becomes a Constant. _resolveInputs maps it over a list, and _values reads a list of nodes' current values. Later steps reuse all three:

Reactive._resolveInput = (input) => {
  if (Reactive._isReactive(input)) {
    return input;
  }
  return Reactive.Constant(input);
};

Reactive._resolveInputs = (inputs) => {
  return inputs.map(Reactive._resolveInput);
};

Reactive._values = (inputNodeRefs) => {
  return inputNodeRefs.map(Reactive._value);
};
Resolve one input, a list of inputs, and read a list of values

The listener edge needs no helper of its own: _nodeCreate wires it inline, adding the new node to each input's listeners Set. An input node holds a plain Set of the nodes that read it, so a listener drops out simply by leaving the Set when its scope goes away later.

Running It

A calculation over a constant and a piece of state derives their sum, and leaves the graph behind it. The three nodes are entries 0, 1 and 2 in the root scope:

const root = Reactive.Root(() => {
  const one = Reactive.Constant(1);              // index 0
  const two = Reactive.State(2);                 // index 1
  const sum = Reactive.Calculate([one, two], (a, b) => a + b);  // index 2
});

const nodes = root.rootScope.nodes;
nodes[2].value;           // 3               the sum, computed now
nodes[2].inputNodeRefs;   // [ →0, →1 ]      sum reads one and two
nodes[0].listeners;       // Set { →sum }    one feeds node 2, sum
nodes[1].listeners;       // Set { →sum }    two feeds node 2, sum
The value is computed, and the graph around it is recorded

Next Steps

Now we can derive a value from the ones it reads and keep track of the dependencies. But a value it reads can be not ready yet: a driver has not filled it. Next, Pending marks state that still loads. It lets that flag spread through every calculation built on it.