Skip to content

Pattern — SSR Loaders (RETIRED)

Type: Pattern (retired) Status: RETIRED — do not implement. An SSR loader that fans machine work out and waits for it relies on a render barrier that does not exist. This page says why, where an implementor would look, and names the shape that works — see §What to use instead.

Why this pattern is retired

The shape in question is a "fan-out-then-render" SSR loader: a boot-like state machine whose :loading state fans out N parallel HTTP fetches via :spawn-all, joins on all-complete, writes results into app-db, and lets the drain settle before render-to-string, with a phase-level :after {30000 :timed-out} deadline as the SSR render budget's outer envelope.

The shape cannot work, because the render barrier it relies on does not exist. Two premises it rests on are both false:

  1. "The JVM transport blocks the drain thread." It does not. The managed-HTTP JVM transport is java.net.http.HttpClient.sendAsync (014-HTTPRequests) — the fetch is dispatched off-thread and the reply lands as a later event. The drain reaches fixed point with the machine's children still in flight and renders the :loading skeleton, not the loaded page.
  2. "The :after deadline bounds the fan-out under SSR." It does not. :after no-ops under SSR (005-StateMachines §SSR mode, 011-SSR §:after is no-op under SSR): the entry action skips timer scheduling and the synthetic timer-elapsed event is never queued. A server deadline is a dead remedy — it never fires. A machine that leans on it to bound async work under SSR simply hangs at :loading until the request-scoped frame is destroyed.

So the only real SSR render barrier is the one the runtime actually installs — drain-blocking-resources!, which drains resources and resources only (016-Resources §SSR and hydration). Machines have no equivalent drain-until-quiescent barrier under SSR, and deliberately so: generalising the resources barrier to also drain machine-issued async work would be disproportionate. The consequence is a firm rule:

Machines are synchronous-only under SSR. Async machine work — a :spawn / :spawn-all whose children exist to drive async loads (:rf.http/managed, websocket protocols, polling) — is outside the SSR allowed subset. It is a programmer error, not a supported shape. The synchronous machine substrate (plain transitions, :always, :spawn of synchronous co-effects, hierarchical entry/exit) runs identically on both platforms; async invoke work does not. See Cross-Spec-Interactions §4 — Machines under SSR and 005 §SSR mode.

The spawn-all-under-ssr-ring conformance fixture (conformance/fixtures/spawn-all-under-ssr-ring.edn) pins this: a :spawn-all loader driven under :platform :server has its server deadline skipped (:rf.machine.timer/skipped-on-server) and its snapshot stuck at :loading — the fan-out never settles, exactly the out-of-subset symptom.

What to use instead

For SSR data-loading that must block the render, use resources, not a machine fan-out. Route-owned blocking resources (016-Resources §SSR and hydration) are the shape the runtime's SSR render barrier (drain-blocking-resources!) actually understands:

  • The server route handler resolves the route, computes route resources, and enqueues blocking resource ensures.
  • drain-blocking-resources! pumps the event loop until the current nav-token's blocking resources settle, bounded by a real wall-clock render deadline; on timeout it settles each still-unsettled blocking entry to a structured first-load failure ({:kind :rf.http/timeout :reason :ssr-blocking-timeout} — 016 §SSR and hydration) so the render sees a structured :error rather than a hung :loading skeleton.
  • The settled resource projection rides the hydration payload; the client hydrates without a duplicate immediate fetch for fresh entries.

Resources give the parallel fan-out (N blocking ensures drain together), the per-entry decode / freshness / classification, the deadline that actually fires, and hydration — all of it inside the one barrier the SSR host adapter drives.

Machines still have a place on the server — the synchronous subset. A machine that resolves a route, sets an auth slice from a request cofx, or drives a synchronous boot sequence runs identically server-side. Just don't ask it to await network I/O during the render; that is the resources' job.

Cross-references