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);
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
};
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;
};
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, []);
};
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);
};
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);
};
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
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.