Targets · JavaScript · Drivers

Drivers

Drivers are the bridge between a Program's reactive core and the outside world. Each cycle the program emits descriptors into named context channels; a driver drains its channel and reconciles the real resources it owns — DOM nodes, animation frames, network requests — creating, updating and destroying them to match. Events flow back the same way, re-entering the reactive cycle.

Contents

How a Program becomes JavaScript, in parts. Start with the introduction, then follow each part into the rendered output and the runtime it links against.

Defining a driver

A driver is a small object with a fixed shape. It names the channel it drains, says whether that channel is a flat list or the nested interface tree, sets itself up once, and then reconciles real resources against the emissions of each cycle. Here is a console driver — it drains the Console.Lines channel and prints each line that has changed since the last cycle.

function ConsoleDriver() {
  const lastById = {};
  return {
    channel: 'Console.Lines',        // the context channel this driver drains
    kind: 'array',                   // 'array' (flat emissions) or 'tree' (the interface)
    init(addUpdates) { /* nothing to set up */ },
    dispatch(emissions) {
      for (const line of emissions) {
        if (lastById[line.id] !== line.text) {
          console.log(line.text);
          lastById[line.id] = line.text;
        }
      }
    },
  };
}

Registering it

A program lists the drivers it uses; the runtime opens a channel for each and never hardcodes one.

Program.Application(() => Main(), [ ConsoleDriver() ]);

How the runtime runs it

Each cycle the runtime opens the driver's channel, runs the reactive program (which emits into it), drains it, and hands the list to dispatch:

Context.openArray('Console.Lines');
program = Reactive.Run(program, updates);
driver.dispatch(Context.drainArray('Console.Lines'));

A driver that produces events — a timer, a network reply, an animation frame — calls the addUpdates it was given in init, which schedules the next cycle. That is the whole loop: emissions out through dispatch, events back through addUpdates.

A driver in full: the web client

The console driver is the whole contract in miniature. The web client driver is that same contract doing real work — and it is worth following closely, because loading, errors and retries all come from it. It drains the NetworkWeb.RequestContext array channel, and is registered like any other driver:

Program.Application(() => App(), [ Network.Web.Driver() ]);

Reconciling requests

Each cycle the program emits one descriptor per request it wants in flight. The driver keeps its live requests keyed by a settable id (the response cell to write back to) and a signature (url, method, headers, body, and a reload counter), and compares the new emissions to what it is running:

Loading

The response lives in a LOCKED state (Reactive.State(zero, true)), so it starts in the loading state. While a request is in flight the cell stays locked; the program reads it and shows a spinner with no special-casing. When a reused request's parameters change, the driver re-locks by writing a fresh loading value back:

var loading = new ReactiveGettable(new ReactiveLock(undefined), emptyResponse);
updates[id] = new ReactiveValue(loading, new ReactiveSettable(id, undefined));
addUpdates(updates);

A lock with no error is the loading state. (A polling request that already has a result skips the re-lock, so it updates in place instead of flashing a spinner.)

Errors

When the response settles, the driver writes it back. A success — or a 4xx — is an ordinary unlocked value. A server error (5xx) or a failed fetch is written back locked with an error, and the error carries the id of a retry flag:

var error = new ReactiveError('HTTP ' + status, retryFlagId);
var errored = new ReactiveGettable(new ReactiveLock(error), responseData);
updates[id] = new ReactiveValue(errored, new ReactiveSettable(id, undefined));
addUpdates(updates);

The program reads it with Reactive.HasError and Reactive.OptionalErrorMessage (see runtime), and because the error rides on the value, anything derived from it is errored too.

Retry

The retry flag is a real settable. The program pulls it off the error and sets it true — typically from a “try again” button:

var retry = Reactive.OptionalRetryFlag(response);
// on click:
Reactive.Set(retry, true);

Setting it runs the request's reverse body, which bumps a reload counter. That counter is part of the request's signature — so on the next cycle the descriptor looks changed, the driver cancels and re-fetches, and the loading/error path runs again. Retry is not a special case; it is just a write that makes the request look new.

How a driver writes back

Notice what the driver never does: touch the program directly. Every result — loading, success, error — is delivered by calling addUpdates(updates), a map from settable id to a new value. That schedules the next cycle, and Reactive.Run applies each update: an id that is a plain state cell is replaced with the new value; an id that carries a reverse callback runs that callback. This is the one path by which anything outside the program — a reply, a click, a frame — gets back in.

Choosing drivers for an embed

An embed lists exactly the drivers it needs. A static counter needs only the interface; a page with live data adds the network driver; an animated one adds the frame ticker. Nothing else is created or run.

Program.Application(() => Dashboard(), [
  Ui.Dom.Driver('app'),            // the interface
  Network.Web.Driver(),            // http requests
  Animate.Frame.Driver(),          // a shared animation ticker
]);

Three shapes of driver

Every driver is one of three shapes:

Custom interface components

The interface driver can be handed custom components a module provides — a map, a chart, a payment field — anything that needs bespoke native code. A custom component gives a name and a render function; wherever a program uses that name, the driver calls the function to build, and later update, the real element.

const MapView = {
  name: 'Map.View',
  render(attributes, renderChildren, addUpdates) {
    const el = document.createElement('div');
    // … create the map inside el, using attributes …
    return el;                     // the driver reconciles this into the page
  },
};

Program.Application(() => App(), [
  Ui.Dom.Driver('app', { components: [MapView] }),
]);

The driver keeps a registry of these by name. A built-in component (text, button, layout) is just one that ships with the driver; a custom one is registered the same way, so program code cannot tell them apart. renderChildren lets a custom component host ordinary interface inside itself, and addUpdates lets it feed events back.

Events flow back

A driver never writes to the program directly. When something happens outside — a click, a response, a frame — the driver calls addUpdates, which schedules the next cycle with the new values. The reactive body runs again, the interface settles, and the drivers reconcile once more. One direction out, one direction back — see the runtime.