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 reads several inputs, so it depends on the two-way edges the node already carries: inputNodeRefs, the nodes it reads, and listeners, a Set of the listener nodes that read this one. These edges point both ways, so from either end the graph reads out the other side.

The node builder shown on the values page already carries them. _nodeCreate takes inputNodeRefs, stores them with an empty listener Set on the node, and once the node exists wires a listener edge from each input back to it. A calculation relies on exactly this: it hands _nodeCreate the inputs it read, and the builder records both directions of the edge.

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 };
  const pending = Reactive._anyPending(inputNodeRefs);
  return Reactive._nodeCreate(payload, calculatedValue, inputNodeRefs, pending, false);
};
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((input) => {
    return Reactive._resolveInput(input);
  });
};

Reactive._values = (inputNodeRefs) => {
  return inputNodeRefs.map((inputNodeRef) => {
    return Reactive._value(inputNodeRef);
  });
};
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.