Spec 004C — Root identity and mount¶
Status: v1-required. Root identity, the two rules a root keeps at mount, idempotent payload install across a page's roots, and fail-loud payload conflict. The identity model is exact — a root is one React DOM render/hydration unit, a frame is one re-frame2 state world, roots ↔ frames are many-to-many, and mount position is never identity.
What realises this contract.
re-frame.ssr.installkeeps the payload-install ledger (§6, §7), andre-frame.ssr/hydrate!runs a hydrating root's preflight and seed in the order of §10. This Spec owns no mount verb: the client-root grammar is 006 §The client root, which every React view adapter publishes,re-frame.frescoincluded.
1. Root identity — required, host-authored, derivable¶
Every root has a root-id, and it is the root's identity. A hydrating root's
root-id is the :root-id its host passes (§3), and the payload-install ledger
attributes each claim to it (§6). A root-id is a qualified keyword (canonical:
:page/shop) or a vector of a qualified keyword plus scalar disambiguators — keyword,
string, or integer ([:shop/product-panel :left]).
3. The mount grammar and the host signature set¶
This Spec owns no mount verb. The client-root grammar — client-root, render!,
unmount! — is 006 §The client root,
and re-frame.fresco realises it as h/client-root, h/render! and h/unmount!:
the first render! through a handle creates its root, or hydrates one under
{:hydrate? true}. What this section owns is the two rules a root keeps about identity
and ordering.
- A hydrating root hydrates as the server rendered it. Its root-id is the
:root-idits host passes tore-frame.ssr/hydrate!(or on itshydrate-page!entry), which records it as the payload's installer and names it in a conflict. ItsidentifierPrefixmust be the one the server rendered under — React's empty prefix""when the server set none — or everyuseIdresolves differently from the server's bytes. The host passes it as the:identifier-prefixrender!opt, which Fresco's door and the Reagent and UIx client roots all take. - Frame preflight runs before React. A root door that ensures its frame does so
before
createRoot: ENSURE creates the frame if absent and drains its:initial-eventssynchronously, so the first paint is the seeded one, and a frame already live is joined as it stands — no re-seed, no config refresh.re-frame.fresco.impl.mount/root!keeps this order throughensure-frame!; Fresco's public door names no frame, because its tree does, onh/frame-rootorh/frame-provider. A hydrating root's frame state arrives throughre-frame.ssr/hydrate!, which seeds it before the host mounts (§10). Whatmake-frameconstruction and the synchronous, ordered:initial-eventsdrain do is Spec 002's. Theframe-roottwo-pass contract (empty first render → commit-phaseuseLayoutEffectENSURE → populated second render) runs the opposite order and scopes a component subtree, not a host root.
6. Payload references and idempotent install¶
- A payload id is a frame id: the payload a root references is the one for the frame it hydrates into, so the two are one identifier, not two.
- Payload install is idempotent and order-independent: the first
hydrating root referencing a payload installs it; later roots find it live and do
not re-seed. Conflict is the exception, and it is fail-loud (§7). Install is the
state-boot call's work, not the DOM-adoption call's: it is
re-frame.ssr/hydrate!that reads the payload and installs it, and a view layer's hydrating root never does (011 §Client-side hydration boot helper). "The first hydrating root installs it" therefore describes the ORDER the two-call boot runs in across N roots on a page, not a step hidden inside the hydrating root.
7. Conflict detection — fail-loud¶
All ids below follow the one-catalogue :rf.error/* scheme; each carries a data map
naming both parties.
| Id | When |
|---|---|
:rf.error/frame-payload-conflict |
below |
:rf.ssr/hydration-mismatch |
server↔client render-tree fingerprint/digest disagreement at hydration (a Spec 009 catalogue row this Spec does not own) |
Payload conflict — fail-loud at hydration preflight. At a hydrating root's
preflight, before any install, a referenced payload id already installed with a
different content digest fails that root with :rf.error/frame-payload-conflict
(re-frame.ssr.install/payload-install-decision!). Its data names both parties — the
:payload-id, the :installed record (:digest, :installed-by), and the
:arriving {:digest … :root-id …} — with recovery
:render-the-page-from-one-response. The installed payload and the roots already using
it are untouched — failure scoping is precise: a bad frame payload affects exactly the
roots referencing it. There is no first-wins silent merge and no last-wins
overwrite; an equal digest is the idempotent no-op of §6.
10. Stage placement¶
| Surface | Stage |
|---|---|
The hydrating-root boot sequence (payload frame-id check → install decision → hydrate) — re-frame.ssr/hydrate! validates the payload's :rf/frame-id, runs re-frame.ssr.install/preflight! (the install decision) and then :rf/hydrate, before the view layer's hydrating root adopts the DOM — and the payload-content-digest conflict preflight (:rf.error/frame-payload-conflict) |
S5 (stage roster per EP-0030 §Stages S1–S7) |
Q24–Q28 coverage¶
- Q24 (render root-or-view forms; props/frames/registrations/overrides) → no Spec:
there is no
ui.test/render(008 §Theui.testcontract). - Q25 (where root-id is authored/derived; the full signature set) → §1, §3.
- Q26–Q28 (a static root descriptor and its evolution, locator generation and
duplicate detection across page fragments, frame-plan extraction) → no Spec: a root's
identity is the
:root-idits host passes (§1), its container is the DOM node the host handsrender!(§3), and a payload reference is a frame id (§6).