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.
- Introduction — what the JavaScript target is, and how to read the rest of this section.
- Capabilities — which high-level features are possible, built in or through a library.
- Coding standards — the Frenzi coding standards the generated code follows, global and JavaScript-specific.
- Example — a full program using every keyword, and the JavaScript it renders to.
- Naming considerations — how Program's dotted names are carried into JavaScript.
- Entry point — the generated
main()that wires up the drivers and starts the reactive cycle. - Drivers — the native drivers that provide capabilities like the user interface and animation, draining each cycle's emissions.
- Runtime — the Reactive and Context machinery the rendered code links against.
- Native modules — the hand-written, per-platform JavaScript behind
NATIVEfunctions. - Keywords — how each of Program's keywords renders to JavaScript.
- Project setup — the files and commands to build and run the rendered program.
- Reserved words — the words JavaScript keeps for itself, and how a Program name that collides with one is escaped.
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:
- a new id — start a
fetch; - an id whose signature changed — cancel the old request and start a fresh one;
- an id that has vanished — cancel it.
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:
- Tree renderer — the user-interface driver. It drains the nested interface tree and reconciles it into the DOM, reusing elements by a stable id so only what changed is touched.
- Request set — network requests, socket channels. Each emission is one ongoing process; the driver starts new ones, keeps live ones, cancels those that have vanished, and feeds results back.
- Shared resource — the animation ticker. One
requestAnimationFrameloop serves every live animation; it starts when the first appears and stops when the last is gone.
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.