Liking cljdoc? Tell your friends :D

remuda.tiers

State tiers: how each field of a view survives a reconnect.

The central discipline — a live view is a rebuildable projection, never the source of truth — only pays off if every field says how it is rebuilt. That is what this namespace is for.

TierOn reconnect
:sourcedre-derived by :mount from the Source
:recoverablereplayed from the client recovery snapshot
:disposablereset to :default
:derivedrecomputed by its :from fn

What :sourced does NOT mean

Worth stating first, because the obvious reading is wrong. :sourced is a claim about rebuild, not about reads. :mount runs on first connection and on reconnect — not per render and not per interaction. Measured: five interactions produce five renders and one mount.

So a :sourced field:

  • is not read-only. Handlers mutate it freely; that is normal. The tier only says who supplies it when the view is rebuilt from nothing.
  • is not continuously fresh. Between mounts the view is a cache. A row changed by another user does not appear until a reconnect or a PubSub hint triggers a rebuild. Expecting freshness here is the mistake this section exists to prevent.
  • is not persisted for you. Nothing writes back. If a handler changes the view but not the Source, the next rebuild will contradict it — the view was wrong, and the rebuild corrected it in the direction you did not want.

The name was chosen over :authoritative for exactly this reason: that word suggests continuous authority, when the contract is rebuild-time authority. :sourced says where the field comes from, and pairs with :derived — one fetched from outside, one computed from other view fields.

:derived is recompute-on-build, not recompute-in-render

An earlier draft described :derived as "never stored; recomputed in render". Building tiers showed that is too narrow, and the gap had a concrete failure.

Consider a component holding :next-id for allocating keys. It is derived from :items, but it is consumed by an :on handler, never by :render, so "recomputed in render" does not describe it. And the other three tiers are all wrong for it:

  • :sourced — derivable, but not in the Source; the component invented it.
  • :recoverable — not UI state; the client has no business holding it.
  • :disposable — resetting to a default is actively wrong. With items 1,2,3,4 persisted and :next-id reset to its default of 3, the next add produces ids [1 2 3 4 3] — a duplicate key, which corrupts the keyed diff.
  • :derived (as originally worded) — never rendered, so the wording excludes it.

Recomputing from the rebuilt view fixes it: (inc (max ids)) yields 5, and the next add gives [1 2 3 4 5]. So :derived fields declare a :from function of the view, and the engine recomputes them whenever a view is built — on mount and on reconnect alike.

Derived fields are also excluded from the diff, since recomputing them from fields that are themselves diffed would produce redundant patches for state no boundary owns. That is the other half of the :next-id problem: it was widening every patch to a full component re-render.

State tiers: how each field of a view survives a reconnect.

The central discipline — a live view is a rebuildable *projection*, never the
source of truth — only pays off if every field says how it is rebuilt. That is
what this namespace is for.

| Tier           | On reconnect                               |
|----------------|--------------------------------------------|
| `:sourced`     | re-derived by `:mount` from the `Source`   |
| `:recoverable` | replayed from the client recovery snapshot  |
| `:disposable`  | reset to `:default`                        |
| `:derived`     | recomputed by its `:from` fn               |

## What `:sourced` does NOT mean

Worth stating first, because the obvious reading is wrong. `:sourced` is a claim
about *rebuild*, not about *reads*. `:mount` runs on first connection and on
reconnect — **not per render and not per interaction.** Measured: five
interactions produce five renders and one mount.

So a `:sourced` field:

- **is not read-only.** Handlers mutate it freely; that is normal. The tier only
  says who supplies it when the view is rebuilt from nothing.
- **is not continuously fresh.** Between mounts the view is a cache. A row
  changed by another user does not appear until a reconnect or a PubSub hint
  triggers a rebuild. Expecting freshness here is the mistake this section
  exists to prevent.
- **is not persisted for you.** Nothing writes back. If a handler changes the
  view but not the `Source`, the next rebuild will contradict it — the view was
  wrong, and the rebuild corrected it in the direction you did not want.

The name was chosen over `:authoritative` for exactly this reason: that word
suggests continuous authority, when the contract is rebuild-time authority.
`:sourced` says where the field comes from, and pairs with `:derived` — one
fetched from outside, one computed from other view fields.

## `:derived` is recompute-on-build, not recompute-in-render

An earlier draft described `:derived` as "never stored; recomputed in render".
Building tiers showed that is too narrow, and the gap had a concrete failure.

Consider a component holding `:next-id` for allocating keys. It is derived from
`:items`, but it is consumed by an `:on` handler, never by `:render`, so
"recomputed in render" does not describe it. And the other three tiers are all
wrong for it:

- `:sourced` — derivable, but not *in* the `Source`; the component invented it.
- `:recoverable` — not UI state; the client has no business holding it.
- `:disposable` — resetting to a default is **actively wrong**. With items
  1,2,3,4 persisted and `:next-id` reset to its default of 3, the next add
  produces ids `[1 2 3 4 3]` — a duplicate key, which corrupts the keyed diff.
- `:derived` (as originally worded) — never rendered, so the wording excludes
  it.

Recomputing from the rebuilt view fixes it: `(inc (max ids))` yields 5, and the
next add gives `[1 2 3 4 5]`. So `:derived` fields declare a `:from` function
of the view, and the engine recomputes them whenever a view is built — on mount
and on reconnect alike.

Derived fields are also excluded from the diff, since recomputing them from
fields that are themselves diffed would produce redundant patches for state no
boundary owns. That is the other half of the `:next-id` problem: it was
widening every patch to a full component re-render.
raw docstring

apply-derivedclj

(apply-derived state view)

Recomputes every :derived field from the current view.

Applied whenever a view is built — after :mount, after a reconnect rebuild, and after an :on handler — so a derived field can never drift from the state it is derived from.

Fields are computed in declaration order. A derived field that depends on another derived field therefore requires the dependency to be declared first; no dependency graph is computed, deliberately, since resolving one would invite cycles for no clear benefit.

Recomputes every `:derived` field from the current view.

Applied whenever a view is built — after `:mount`, after a reconnect rebuild,
and after an `:on` handler — so a derived field can never drift from the state
it is derived from.

Fields are computed in declaration order. A derived field that depends on
another derived field therefore requires the dependency to be declared first;
no dependency graph is computed, deliberately, since resolving one would invite
cycles for no clear benefit.
sourceraw docstring

errorsclj

(errors state)

Only the problems that should stop a component from running.

Only the problems that should stop a component from running.
sourceraw docstring

fields-of-tierclj

(fields-of-tier state tier)
source

restoreclj

(restore state mounted client-snapshot)

Rebuilds a view after a reconnect.

  • :sourced comes from mounted, the freshly re-derived view;
  • :recoverable is replayed from client-snapshot, but only for fields actually declared recoverable — anything else in the snapshot is ignored rather than trusted, since it arrived from the client;
  • :disposable is reset to its :default;
  • :derived is recomputed last, from the result.

Ordering matters: derived fields must see the restored view, not the mounted one, or they would be computed from state that is about to change.

Rebuilds a view after a reconnect.

- `:sourced` comes from `mounted`, the freshly re-derived view;
- `:recoverable` is replayed from `client-snapshot`, but only for fields
  actually declared recoverable — anything else in the snapshot is ignored
  rather than trusted, since it arrived from the client;
- `:disposable` is reset to its `:default`;
- `:derived` is recomputed last, from the result.

Ordering matters: derived fields must see the restored view, not the mounted
one, or they would be computed from state that is about to change.
sourceraw docstring

snapshotclj

(snapshot state view)

The subset of view the client should hold for recovery.

Only :recoverable fields. Sourced state is re-derived from the Source, disposable state is reset, and derived state is recomputed — none of them belong on the wire, and keeping them off it is what makes the snapshot small enough to ride in a signal.

The subset of `view` the client should hold for recovery.

Only `:recoverable` fields. Sourced state is re-derived from the `Source`,
disposable state is reset, and derived state is recomputed — none of them belong
on the wire, and keeping them off it is what makes the snapshot
small enough to ride in a signal.
sourceraw docstring

strip-derivedclj

(strip-derived state view)

Removes :derived fields from a view.

Used before diffing: a derived field is a function of other fields that are themselves diffed, so including it produces a redundant op — and, since no boundary owns it, one that widens the patch to a full component re-render.

Removes `:derived` fields from a view.

Used before diffing: a derived field is a function of other fields that are
themselves diffed, so including it produces a redundant op — and, since no
boundary owns it, one that widens the patch to a full component re-render.
sourceraw docstring

tier-ofclj

(tier-of state field)

The declared tier of field, defaulting to :sourced.

Undeclared fields are sourced because that is both the common case and the safe one: it means "rebuilt by :mount from the source of truth", so a field nobody classified is reconstructed rather than silently dropped or replayed from the client.

The declared tier of `field`, defaulting to `:sourced`.

Undeclared fields are sourced because that is both the common case and the safe
one: it means "rebuilt by `:mount` from the source of truth", so a field nobody
classified is reconstructed rather than silently dropped or replayed from the
client.
sourceraw docstring

tiersclj

source

validate-stateclj

(validate-state state)

Returns a seq of problems with a :state declaration, empty if sound.

An empty or absent declaration is valid — everything is sourced. Only what is declared is checked, since a mistyped tier or a missing :from would otherwise degrade silently.

Returns a seq of problems with a `:state` declaration, empty if sound.

An empty or absent declaration is valid — everything is sourced. Only
what is declared is checked, since a mistyped tier or a missing `:from` would
otherwise degrade silently.
sourceraw docstring

cljdoc builds & hosts documentation for Clojure/Script libraries

Keyboard shortcuts
Ctrl+kJump to recent docs
Move to previous article
Move to next article
Ctrl+/Jump to the search field
× close