Targets · JavaScript · Runtime

Runtime

The rendered JavaScript stays small because it links against a runtime. That runtime is three cooperating parts: the Reactive graph that tracks derived values and write-backs, the Context channels that carry emissions out to drivers, and the Native layer the drivers plug into. None of it is user-visible — a Program only ever names the small surface it needs.

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.

Three parts

The runtime is small and has three parts. Reactive holds state and derives values from it. Context carries what the program emits — the interface and other effects — out to the drivers. Native is the drivers and the hand-written pieces they are built from. Rendered code names only a thin surface of the first two; the rest is machinery.

Reactive

Reactive is the heart. A STATE is the one place a value really changes; everything else is derived from it and recomputed only when something it read has changed. Each call-site keeps a stable identity across cycles — state is tracked by position, which is why the rendered code must place its reactive calls in the same order every time (see coding standards).

var count = Reactive.State(0);                                  // STATE
var text = Reactive.Calculate(() => MathsInteger.ToString(Reactive.Value(count)));  // LET
// a write from the interface arrives as Reactive.Set(count, next)

Everything the rendered program names is a static function on the Reactive class; the values that flow between them are instances of a handful of small classes. Both are inventoried below.

A reactive value

A ReactiveValue is the one thing that flows through the whole system. It has two optional halves — a gettable (its readable side) and a settable (its writable side) — and which halves are present says what it is:

The gettable carries the native value and, optionally, a lock. The settable carries a stable id and, optionally, a reverse callback — if it has one, a write runs that callback; if it does not, the write lands in a plain state slot.

ReactiveValue
 ├─ optionalGettable : ReactiveGettable   // present ⇒ readable
 │   ├─ value                             // the native payload
 │   └─ optionalLock : ReactiveLock       // present ⇒ LOADING
 │       └─ optionalError : ReactiveError // present ⇒ ERROR
 │           ├─ message
 │           └─ optionalRetryFlagId       // a settable id ⇒ RETRY
 └─ optionalSettable : ReactiveSettable   // present ⇒ writable
     ├─ id
     └─ optionalReverseCallback           // present ⇒ reverse; absent ⇒ state slot

Loading, error and retry

There is no separate “async” type. Loading and failure are just a lock on an ordinary value, and a lock travels with anything derived from it — so the interface shows a spinner or an error without any code special-casing it.

if (Reactive.IsLocked(response)) { /* loading or error */ }
if (Reactive.HasError(response)) {
    var message = Reactive.OptionalErrorMessage(response);      // the message
    var retry = Reactive.OptionalRetryFlag(response);           // the settable flag, or undefined
    // wire a button to: Reactive.Set(retry, true)
}

The propagation is done once, in Reactive.Calculate: it remembers every value read through Reactive.Value during its callback and, if any of them is locked or errored, its own result is locked or errored too — with the inputs' retry flags combined into one, so a single retry fans back out to every request that failed. Because construction and field access both go through Calculate (see TYPE), a record built from a still-loading field is itself loading, automatically. The web client driver shows the whole cycle end to end.

The Reactive class

These are the functions the rendered code (and the native drivers) call. Everything else in the module is machinery reached through them. A function is marked native (callable anywhere) or reactive (only valid inside a running cycle) where it matters.

Program and cycle

State and values

Calculation and derived values

Reverse and binding

Scope, conditionals and lists

Identity and references

Loading / error / retry inspection

A layer of _-prefixed helpers (lock/error collection, scope-item bookkeeping, update routing) sits under these; being underscore-named, they are internal machinery, never part of the surface a program touches.

Classes in the reactive module

The Reactive functions move instances of these small classes around. Most a program never names directly — they are what a reactive value is made of.

Context

A program does not call the DOM, the network, or the animation clock. It emits into named channels, and a driver drains each channel. The interface is one channel — a nested tree, so components can contain components; most others are flat lists. Channels are cheap to snapshot, which is what lets a cycle be built up and then handed off whole.

One cycle

The entry point drives a loop. Each turn:

  1. open a channel for each active driver;
  2. run the reactive program — write-backs first, then the forward body that emits the interface and effects;
  3. drain the channels;
  4. hand each driver its emissions, which it reconciles against the real world;
  5. wait. When a driver calls addUpdates, the loop turns again — and only then.

Nothing polls. A program at rest costs nothing; a cycle happens exactly when a value it depends on is written from outside.

Kept small

The runtime ships as one bundle the rendered program links against, so an embedded example is the program plus a fixed, shared core — not a framework per page. It is deliberately lean: the reactive value, the context channels, and the driver contract are the whole of it, with validation kept to development builds.