kolu
Docs

Reactive honesty

A live view has one job beyond looking right: it must not lie. A frozen number that looks like a current reading, a spinner that never resolves, an error banner that stays up after the system recovered — each is worse than an obvious crash, because the reader trusts it. The surface framework is built around a handful of rules whose whole purpose is to make a dishonest view hard to write. Together they are what we call reactive honesty.

A stream always opens with a snapshot

Every cell, collection, and stream sends a fresh full snapshot as its first frame, and only then deltas. This is not a nicety — it is load-bearing. The streaming retry plugin re-invokes the source function on every reconnect. If the first frame after a reconnect were a delta, the client would apply a change on top of stale state and silently diverge. Because the first frame is always a snapshot, a reconnect re-seeds from truth. Every server-side helper enforces this in code; you cannot wire a source that skips its snapshot.

An empty stream fails loud, never quiet

The counterpart to “always snapshot” is “never a silent empty.” When a stream is expected to open with a value and instead closes with nothing, that is a link failure, and it surfaces as one — the initial read’s exception propagates to the client on the first frame rather than resolving to an empty view. The framework refuses the tempting fallback where a caught error collapses into a quietly empty, quietly successful result. An empty that means “the pipe broke” must look different from an empty that means “there is genuinely nothing here.”

There is a matching subtlety for keyed reads: asking a collection for a key that does not exist yet does not error. It holds open and yields nothing until the first value arrives. “Absent” is a recoverable waiting state, not a failure — throwing there once turned a normal empty into a non-retriable error that killed the subscription.

Consume fine-grained, or the view freezes

The last rule is about how you read the surface. Use the bound hooks (.cells.X.use(), .collections.X.use(), .streams.X.use()) and each new value flows in through fine-grained reactivity — objects reconciled key-by-key, keyed list rows mounted and torn down individually as the key set changes.

The anti-pattern is a coarse read that copies the whole thing into your own signal and hand-folds updates. Two things go wrong. First, same-shaped frames coalesce and the view stops updating — it looks alive and is frozen. Second, and worse, a hand-fold tends to grow a latch: a ?? previous that keeps showing the last error even after the system recovered, because your fold never clears it. The framework’s per-subscription error is self-clearing by construction; a hand-rolled one is not.

Health is a fact, not a verdict

client.health() returns { live, subs }. live is the honest conjunction — the transport is up and every readiness predicate passes. A subscription error stays in subs; it is never folded into live. That split is the whole point: the framework reports what is true and leaves the verdict — “show the stale value while degraded” versus “hard-gate until ready” — to the app. A component that dims a status dot reads the fact; it can never be handed a “ready” it did not earn.

The through-line

Every rule here is the same instinct applied at a different layer: a caught error must surface, never collapse to an empty or default state. Snapshot-then-deltas keeps a reconnect from lying about state; fail-fast keeps a broken pipe from looking empty; fine-grained consume keeps a recovered view from staying frozen; health-as-fact keeps the app from mistaking “up” for “ready.” The cost is that you give up the easy fallbacks — the ?? {}, the swallowed error, the “just show nothing.” In exchange, when the view says a number, the number is real.