Updates

Everything so far builds a program and computes it once. This step makes it react. A change enters at one node and flows to everything that reads it, settled in dependency order. As a result a rerun costs what the change costs and no more. The order comes from two things already in place. Within a scope, a node's inputs were built before it, so index order is dependency order. Scopes also nest into a tree. The walk descends that tree and loops each scope's nodes in order. Two flags on a node carry the change, with no heap and no queue.

Two Flags Carry a Change

A node gains two flags. inputsChanged means the node's own inputs moved, so its value must be recomputed. contentsUpdated means the node owns a scope whose insides moved, so that scope must be walked. The first says what changed; the second says where to look. Together they let the walk start at the root scope and descend only into the scopes that hold changed work.

What a Node Gains

The flags live on the node, so _nodeCreate seeds them both to false. Two new lines:

Reactive._nodeCreate = (payload, value, inputNodeRefs) => {
  const scope = Reactive._currentScope;
  const node = {
    [Reactive._reactive]: true,
    scope,
    payload,
    value,
    inputNodeRefs,
    inputsChanged: false,
    contentsUpdated: false,
    listeners: new Set(),
  };
  scope.nodes.push(node);
  inputNodeRefs.forEach((inputNode) => {
    inputNode.listeners.add(node);
  });
  return node;
};
Every node carries its two change flags, both clear to start

Nothing else changes. The scope tree from before, each scope's parentNode, is the path the walk goes up to mark contentsUpdated. The root scope's undefined parent is where that climb stops.

Marking, Up the Scope Tree

A node's value is stored through _updateNodeValue, which only reacts when the value actually changed. When it does, every node that reads it, grouped by scope, is marked inputsChanged:

Reactive._updateNodeValue = (node, nextValue) => {
  if (nextValue !== node.value) {
    node.value = nextValue;
    Reactive._updateMarkListenersChanged(node);
  }
};

Reactive._updateMarkListenersChanged = (node) => {
  node.listeners.forEach((listenerNode) => {
    Reactive._updateMarkInputsChanged(listenerNode);
  });
};
A real value change marks every reader inputs-changed

Marking a node inputsChanged then threads a path of contentsUpdated up the scope tree, so the walk can reach it from the root. The climb follows each scope's parentNode and marks each owning node. The climb stops the moment it meets an owner already marked, because that chain was threaded before. The climb also stops when it reaches the root scope, whose parent node is undefined:

Reactive._updateMarkInputsChanged = (node) => {
  if (!node.inputsChanged) {
    node.inputsChanged = true;
    Reactive._updateMarkScopeChanged(node.scope);
  }
};

Reactive._updateMarkScopeChanged = (scope) => {
  let currentScope = scope;
  while (currentScope.parentNode !== undefined) {
    const owner = currentScope.parentNode;
    if (owner.contentsUpdated) {
      return;   // this owner's chain up was threaded before
    }
    owner.contentsUpdated = true;
    currentScope = owner.scope;   // climb by the owner's own scope object
  }
};
One reader marked; a path of contents-updated threaded up to the root
This upward thread is what lets an If read only its condition again. A change inside its body needs no edge back to the if node. Marking a body node threads contentsUpdated through the if node, and the walk starts from there.

Settling a Node

A node marked inputsChanged is settled by _updateNodeInputs, which dispatches on its kind. A calculation and an emit recompute their value. An if reacts to its condition:

Reactive._updateNodeInputs = (node) => {
  const payload = node.payload;
  if (payload.calculate) {
    Reactive._updateCalculateNodeInputs(node);
  } else if (payload.emit) {
    Reactive._updateEmitNodeInputs(node);
  } else if (payload.if_) {
    Reactive._updateIfInputs(node);
  }
};
Settle a node whose inputs changed, by kind

A calculation reruns its callback over its inputs' current values, and carries its inputs' pending with it. An emit recomputes its stream with _emitCompute, reappending (or withholding) its own stable leaf. Each stores the result through _updateNodeState, so the value and its pending flag flow on only if something actually moved:

Reactive._updateCalculateNodeInputs = (node) => {
  const inputValues = Reactive._values(node.inputNodeRefs);
  const calculatedValue = node.payload.calculate(...inputValues);
  const pending = Reactive._anyPending(node.inputNodeRefs);
  Reactive._updateNodeState(node, calculatedValue, pending);
};

Reactive._updateEmitNodeInputs = (node) => {
  const emissions = Reactive._emitCompute(node.payload.emit, node.inputNodeRefs);
  Reactive._updateNodeState(node, emissions, false);
};
Recompute a value with its pending, then store it through _updateNodeState
An emit keeps its stable leafId, so a changed value updates the leaf in place. The array diff reports it as an updated, not a remove and an add. And a value that resolves from pending makes its withheld entry appear, in its leaf-id order.

An If Reacts

An if with a changed condition settles differently. By whether a body already stands, the if raises a fresh body, removes one, or leaves it. Then it takes its value from the body. The value is the body scope's emissions when one stands, or the shared empty object when none does. The if reads its body across scopes, with no listener edge, so that value is refreshed by hand:

Reactive._updateIfInputs = (ifNode) => {
  const ifPayload = ifNode.payload.if_;
  const conditionNode = ifNode.inputNodeRefs[0];
  const conditionIsPresent = conditionNode.value;
  const hadBody = ifPayload.optionalScope !== undefined;
  if (conditionIsPresent && !hadBody) {
    ifPayload.optionalScope = Reactive._scopeCreate(ifNode, ifPayload.callback);
  } else if (!conditionIsPresent && hadBody) {
    Reactive._scopeDestroy(ifPayload.optionalScope);
    ifPayload.optionalScope = undefined;
  }
  Reactive._refreshIfValue(ifNode);
};

Reactive._refreshIfValue = (ifNode) => {
  const conditionPending = Reactive._anyPending(ifNode.inputNodeRefs);
  const ifValue = Reactive._emitIfValue(conditionPending, ifNode.payload.if_.optionalScope);
  Reactive._updateNodeValue(ifNode, ifValue);
};
_updateIfInputs: raise, tear down, then take the body's emissions

When it is the if's contents that changed, the walk processes its body scope so its own changed nodes settle. Then the body's fresh emissions are re-read as the if's value. _updateNodeContents dispatches this, the mirror of _updateNodeInputs for the contentsUpdated flag:

Reactive._updateIfContents = (ifNode) => {
  const ifPayload = ifNode.payload.if_;
  if (ifPayload.optionalScope !== undefined) {
    Reactive._processScope(ifPayload.optionalScope);
  }
  Reactive._refreshIfValue(ifNode);
};

Reactive._updateNodeContents = (node) => {
  if (node.payload.if_) {
    Reactive._updateIfContents(node);
  }
};
Settle a node whose contents changed: walk its body, re-read its value
An If owns one body, so one flag says "walk it". A structure that owns many child scopes, a list (Each) or the reverse transactions host, keeps the marker on its own payload and records the set of child scopes that moved, so the walk descends into just those. _updateNodeContents gains a branch per such kind; the shape is the same.

To remove a body is _scopeDestroy. It collects the whole subtree of scopes. It unhooks each of their nodes from the listener sets of the surviving nodes they read. Then it drops the scopes, so nothing left standing points at a node that is gone:

Reactive._scopeDestroy = (scope) => {
  const removing = new Set();
  const queue = [scope];
  while (queue.length > 0) {
    const removeScope = queue.pop();
    removing.add(removeScope);
    removeScope.nodes.forEach((node) => {
      const ifPayload = node.payload.if_;
      if (ifPayload && ifPayload.optionalScope !== undefined) {
        queue.push(ifPayload.optionalScope);
      }
    });
  }
  removing.forEach((removeScope) => {
    removeScope.nodes.forEach((node) => {
      node.inputNodeRefs.forEach((inputNode) => {
        if (!removing.has(inputNode.scope)) {
          inputNode.listeners.delete(node);
        }
      });
    });
  });
  removing.forEach((removeScope) => {
    removeScope.destroyed = true;
  });
};
_scopeDestroy: drop a whole subtree, unhooking its outward edges first

The Walk

The walk is a plain in-order loop over a scope's nodes, with no heap and no queue. Each node whose inputs changed is settled. Then the walk processes the body of each node whose contents changed, both in index order, which is dependency order. An if can be both, when its condition and its body have moved in the one pass. New readers marked at a larger index during the pass are reached later in the same loop:

Reactive._processScope = (scope) => {
  const nodes = scope.nodes;
  for (let nodeIndex = 0; nodeIndex < nodes.length; nodeIndex = nodeIndex + 1) {
    const node = nodes[nodeIndex];
    if (node.inputsChanged) {
      node.inputsChanged = false;
      Reactive._updateNodeInputs(node);
    }
    if (node.contentsUpdated) {
      node.contentsUpdated = false;
      Reactive._updateNodeContents(node);
    }
  }
};

Reactive._update = (root) => {
  Reactive._processScope(root.rootScope);   // walk from the root scope object
};
_update: walk the root scope, which descends into every scope holding dirt
The walk descends only where contentsUpdated was threaded. This is what lets an If read only its condition. A change inside its body threads contentsUpdated through the if node. The walk processes its body, then re-reads the if's value from the settled body.

Set

Set is the one way in, and the one function that names root. It establishes the ambient _currentRoot that every update step reads. It writes the state's new value through _updateNodeValue, which marks its readers only if it actually changed. Then it runs the walk. The program is data, and this is a mutation on that data.

Reactive.Set = (root, reference, value) => {
  const previousRoot = Reactive._currentRoot;
  Reactive._currentRoot = root;                 // ambient for the whole update
  try {
    const node = reference;
    Reactive._updateNodeValue(node, Reactive._value(value));
    Reactive._update(root);
  } finally {
    Reactive._currentRoot = previousRoot;
  }
};
Set writes the seed through _updateNodeValue, then walks the tree

Running It

A calculation follows a Set to its state, whatever the length of the chain. A diamond joins both updated paths, and each node is recomputed once:

const root = Reactive.Root(() => {
  n = Reactive.State(1);
  const doubled = Reactive.Calculate([n], (x) => x * 2);
  const incremented = Reactive.Calculate([n], (x) => x + 1);
  sum = Reactive.Calculate([doubled, incremented], (a, b) => a + b);
}, {});
// sum -> 4
Reactive.Set(root, n, 10);      // sum -> 31   (10*2 + 10+1)
A change flows through the graph in dependency order

An emitted value reflows its gather and keeps its stable id. It is a value change, not an add and a remove. A change inside a present If, even a state declared in the body or two scopes deep, reflows the same way, in document order, because the walk goes up the scope tree to reach it:

// Emit 1, If(true){ Emit 2, If(true){ Emit s (a State = 5) }, Emit 8 }, Emit 9, Gather('n')
//   ->  [1, 2, 5, 8, 9]
Reactive.Set(root, s, 6);      //   ->  [1, 2, 6, 8, 9]   a change two scopes deep, still in order
The walk reaches a change wherever it nests

A toggle of an If raises or drops its whole block. It keeps document order and cleans up its nodes:

const root = Reactive.Root(() => {
  open = Reactive.State(false);
  Reactive.Emit('n', 1);
  Reactive.If(open, () => { Reactive.Emit('n', 2); });
  Reactive.Emit('n', 3);
  g = Reactive.Gather('n');
}, {});
// g -> [1, 3]
Reactive.Set(root, open, true);     // g -> [1, 2, 3]   the body is raised, in order
Reactive.Set(root, open, false);    // g -> [1, 3]      the body is torn down, its nodes gone
Toggling an If raises and tears down its block

A driver's leftover stream at the root is a reactive value like every other value. A Set reflows it too, and this is how the outside world sees a change:

const root = Reactive.Root(() => {
  s = Reactive.State('a');
  Reactive.Emit('dom', s);
}, { dom: {} });
// the root scope's 'dom' stream  ->  ['a']
Reactive.Set(root, s, 'z');      // the 'dom' stream  ->  ['z']
The root's driver streams are live

Next Steps

With the walk in place the graph is a program that runs and reacts. A state changes, and the change travels exactly as far as it is read. It travels down the scope tree to the branch that holds it, along each scope in index order, each node touched at most once. Calculations rerun, emissions reflow their gathers, and a section appears or disappears as its condition flips.

That is the forward direction complete. A change flows from a state out to everything that reads it. The last piece is the reverse direction. A Reverse lens declares the state it writes and returns the new value. A set of it flows a change back onto real state, held by a write lock that keeps an async write from landing until a driver's result arrives.