Pending
A value can be loading: a driver has not filled it yet. Reactive.Pending(initial)
is state that holds a realistic stand-in, a 0, an
empty string, or an empty array. The node is marked pending, so a reader has something to use while it
waits. Pending spreads: a calculation over a
pending value is pending too. A whole derived chain stays loading until the source at its root fills.
const profile = Reactive.Pending(''); // a stand-in while it loads
const greeting = Reactive.Calculate([profile], (name) => 'Hi ' + name);
The flag on the node
Every node gains a boolean pending field beside its value. The value is the stand-in a reader uses
now. The flag says the real value is still on its way.
Reactive._nodeCreate = (payload, value, inputNodeRefs, pending) => {
const scope = Reactive._currentScope;
const node = {
[Reactive._reactive]: true,
scope,
payload,
value,
pending,
inputNodeRefs,
listeners: new Set(),
};
scope.nodes.push(node);
inputNodeRefs.forEach((inputNode) => {
inputNode.listeners.add(node);
});
return node;
};
_nodeCreate holds no policy of its own. It stores whatever flag it is handed. The caller
works out whether the node is loading and passes it in. A state is born clear. A Pending is born set. A calculation
is born loading when any input it reads is loading.
Reactive.Pending
Pending is the same shape as State: a state node, seeded from a plain snapshot of the stand-in you
pass. The one difference is the last argument to _nodeCreate, true in
place of false, so the node is born marked as loading:
Reactive.Pending = (initialValue) => {
const payload = { state: {} };
const value = Reactive._value(initialValue);
return Reactive._nodeCreate(payload, value, Reactive._emptyArray, true);
};
An ordinary State passes false for the same argument, so it starts clear.
Because a Pending is plain state underneath, the first write from a driver gives it the real value and turns
the flag off:
Reactive.State = (initialValue) => {
const payload = { state: {} };
const value = Reactive._value(initialValue);
return Reactive._nodeCreate(payload, value, Reactive._emptyArray, false);
};
It spreads through calculations
The flag travels along the same dependency edges values do. _pendingOf reads one reference's flag,
and _anyPending reports whether any of a node's inputs is loading:
Reactive._pendingOf = (reference) => {
return reference.pending;
};
Reactive._anyPending = (inputNodeRefs) => {
return inputNodeRefs.some(Reactive._pendingOf);
};
Reactive.Calculate asks this of its inputs and passes the answer straight to _nodeCreate.
A calculation over a loading value is itself loading, and a chain of them stays loading down to the last:
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);
};
Staying current
When a value settles, both parts of a node can move: its value and its pending flag. A source can
even land on the same value it stood in for, so the flag flips while the value stays put. _updateNodeState
compares both. If either changed, it marks the node's readers, so the change threads on to everything downstream:
Reactive._updateNodeState = (node, nextValue, nextPending) => {
if (nextValue !== node.value || nextPending !== node.pending) {
node.value = nextValue;
node.pending = nextPending;
Reactive._updateMarkListenersChanged(node);
}
};
A recomputed calculation runs this too. It works out its value and its pending flag from its inputs, then stores both, so a source that clears upstream clears every calculation below it.
Reading it
Reactive.isPending(root, ref) returns the flag on the node behind a reference. A piece of UI reads
it to show a spinner while it is true, and the real content once it clears.
Reactive.isPending(root, greeting); // true while profile loads, then false
Running it
A pending profile stands in as an empty string, and a greeting calculated over it is loading too. Both carry the flag from the moment they are built:
const root = Reactive.Root(() => {
const profile = Reactive.Pending(''); // index 0
const greeting = Reactive.Calculate([profile], (name) => 'Hi ' + name); // index 1
});
const nodes = root.rootScope.nodes;
nodes[0].value; // '' the stand-in
nodes[0].pending; // true still loading
nodes[1].value; // 'Hi ' calculated over the stand-in
nodes[1].pending; // true pending spread through the calculation
Later a driver writes the real name into profile. That write settles. profile takes the
value and clears its flag. The greeting recomputes to 'Hi Ada' and clears its own. Then
anything that reads isPending switches from spinner to content.
Next Steps
Pending threads through calculations here as a per-node flag. A program's outputs, its emissions, come next, and a whole named emission stream can be pending too, loading until its source fills.