Drivers

A program describes state. A driver is the boundary to the outside world. It takes the leftover emission streams. A name the program emits under but never gathers stays in the root scope's emissions. The driver does real work with it: paint the DOM, start a timer, run a fetch. It is also the one place a write starts from outside the graph. You register a driver by name when the program starts, in Root.

A driver is a sink for one stream

A driver is a plain function (requests, set), registered by name in the driver set that Reactive.Root(program, drivers) takes as its second argument. requests is that name's stream of emitted entries, the leftover emissions read by the driver's name. Whatever the program emits under 'dom' and never gathers arrives at the 'dom' driver:

const root = Reactive.Root(program, {
  dom: (requests, set) => {
    // requests is the 'dom' stream; paint each entry to the page
  },
});
A driver is a named function that consumes one stream

The write capability

Application code never writes into the graph directly. A write enters only at the boundary, and the boundary is the driver. So Root builds a single set, bound to this root, and gives it to every driver. That is the runtime's only write entry point from outside the graph:

Reactive.Root = (programCallback, drivers) => {
  const previousRoot = Reactive._currentRoot;
  const root = {
    drivers: drivers || {},
    rootScope: undefined,
    nextReverseId: 1,
  };
  root.set = (reference, value) => {
    return Reactive._write(root, reference, value);
  };
  Reactive._currentRoot = root;
  try {
    root.rootScope = Reactive._scopeCreate(undefined, programCallback);
    Reactive._dispatchDrivers(root);
  } finally {
    Reactive._currentRoot = previousRoot;
  }
  return root;
};
Root binds a single write capability and dispatches once the program is built

Dispatch

_dispatchDrivers reads the root scope's final emissions, then for each registered driver name with a non-empty stream, calls the driver with that stream's entries and the write capability. An emissions value is { pending, emissionsByName }, and emissionsByName maps each name to a stream { array, override }, so it reads stream.array and flattens it to the entries the driver receives. Every entry is a real value, never a stand-in for a pending one, so a driver can act on each request it is handed:

Reactive._dispatchDrivers = (root) => {
  const rootEmissions = Reactive._value(root.rootScope.currentEmissionsRef);
  Object.keys(root.drivers).forEach((driverName) => {
    const stream = rootEmissions.emissionsByName[driverName];
    const requests = stream === undefined ? [] : Reactive._arrayToList(stream.array);
    if (requests.length > 0) {
      root.drivers[driverName](requests, root.set);
    }
  });
};
Read each driver's stream from the root emissions and call it

The loop turns itself

Drivers run after every settle: once at Root, once after each write. A driver's set runs another settle and another dispatch, so the loop continues by itself. That is what makes a write from a driver reach the graph the same way any change does:

Reactive._write = (root, reference, value) => {
  // ... admit the write to a state or run a reverse lens ...
    if (isTopLevel) {
      Reactive._update(root);
      Reactive._dispatchDrivers(root);
    }
  // ...
};
Every top-level write settles the graph, then dispatches drivers again

This is how a Pending state unlocks. A Pending starts locked at a stand-in value, and that pending flag flows through every calculation over it. A driver's set fills the state, which unlocks it, and the next dispatch continues with the state now settled. The driver both supplies the missing value and lets the loop continue.

An animation

An animation is a driver over a Pending frame. The frame starts locked at a stand-in, a calculation doubles it, and both are pending while the frame is. The program emits the doubled value under 'display' and a single request under 'animation'. When the 'animation' driver is called it schedules frames; each scheduled tick calls set(frame, n), which fills the frame, unlocks it, and lets the next dispatch continue:

const frame = {};
const frameQueue = [];
let started = false;

const root = Reactive.Root(() => {
  frame.value   = Reactive.Pending(0);                    // locked at 0
  frame.doubled = Reactive.Calculate([frame.value], (n) => n * 2);  // pending too
  Reactive.Emit('display', frame.doubled);
  Reactive.Emit('animation', 1);              // one request kicks the driver off
}, {
  animation: (requests, set) => {
    if (started) return;
    started = true;
    [1, 2, 3].forEach((nextFrame) => {
      frameQueue.push(() => { set(frame.value, nextFrame); });   // each tick fills the frame
    });
  },
  display: () => {},                        // paints the doubled value
});

frameQueue.shift()();   // set frame to 1: unlocks it, doubled becomes 2, dispatch runs again
A driver drives a Pending frame; each Set unlocks it and the loop advances

A manual frame queue stands in for a real scheduler here, so the walk is easy to follow. In a running program the ticks come from requestAnimationFrame or a timer, and the loop turns on their schedule instead of on a call to shift.

Next Steps

With drivers the framework has a full circle: a program describes state, emissions carry its output to the boundary, drivers turn that output into effect, and their one write capability returns the world to the graph. Next, Scopes looks at how those blocks of code nest, appear, and disappear.