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);
A loading value, and a calculation that inherits its loading state

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;
};
The node builder now stores a pending flag alongside the value

_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);
};
State born pending: the same node, the flag set 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);
};
Ordinary state passes false: born clear

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);
};
A node is pending if any input it reads is

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);
};
A calculation inherits the pending flag from its inputs

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);
  }
};
Value and flag settle together, and either one propagates

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
Read the flag to know whether to show a spinner

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
A loading value and its calculation, both pending

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.