Runtime Subsystems¶
Type: Reference The unifying conceptual frame for every framework-owned durable-state surface in re-frame2 — the children of the runtime-db partition (machines, routing, SSR, and EP-0003's shipped resources / mutations). Names the five clauses every conformant runtime subsystem inherits, so a new subsystem (a 3rd-party
:rf.runtime/<lib>extension) can be evaluated against a single checklist instead of re-deriving the shape each time. This is the durable-state analogue of Managed-Effects.md — Managed-Effects names the eight-property shape every framework-owned effect surface satisfies; this doc names the five-clause shape every framework-owned state subtree satisfies.The same doc also owns the operational environment (§The operational environment) — the non-durable layer beside the durable subsystems: the process-global registrar, the installed adapter, the late-bind hook table, and the host-transient subsystem state. The durable subsystems live inside each frame; the operational environment surrounds them at the process and frame boundaries.
Why this doc exists¶
re-frame2's frame value has two partitions — :rf.db/app (the app's map) and :rf.db/runtime (framework-owned durable state). The runtime-db partition is not a single blob: it is a map whose top-level children are each a framework subsystem's reserved subtree, qualified under :rf.runtime/* (per Conventions §Reserved runtime-db keys). The shipped children are the three v1 subsystems — :rf.runtime/machines, :rf.runtime/routing, :rf.runtime/ssr — plus EP-0003's shipped resource pair, :rf.runtime/resources and :rf.runtime/mutations (the latter present only when the app registers a mutation).
Each child independently re-implements the same five properties: it owns a reserved subtree, it is written only by framework code (via a named authority path), it is read by app code only through public subscriptions/accessors, it has a serialization/elision/projection policy for the SSR and off-box wire boundaries, and it has a teardown contract distinguishing durable facts (kept) from transient handles (dropped). Before this doc those five properties lived implicitly across 002 §Write authority, 002 §Subscriptions read the partition they belong to, 002 §Durable vs transient, 002 §Destroy, and each subsystem's own Spec — never named as one shape.
A runtime subsystem is a framework-owned, durable, revertible slice of frame-state living under a reserved :rf.runtime/<name> child of the runtime-db partition, whose mutation is framework-only, whose reads go through a public sub/accessor API, and whose serialization and teardown obey an explicit per-subsystem policy. App code never reaches into a subsystem's subtree directly; it dispatches the subsystem's events and reads its public subs.
Naming the shape turns the ad-hoc child list into gradeable instances: a new subtree is admitted by answering all five clauses, and the framework-authority question ("who may write runtime-db?") becomes enumerable — whoever registers a runtime subsystem — rather than a special-case carve-out per feature.
One frame value owns every per-frame subsystem — one registry, one teardown path¶
EP-0024. This subsection records the EP-0024 ownership backbone the five-clause contract sits on. The vocabulary it leans on —
frame value,frame id,resolved image generation— is defined in Conventions §Frame vocabulary.
A live frame is one value — the frame value, the lifecycle token make-frame returns — found in one live frame registry that maps a frame id to that value. The frame value is the single conceptual owner of, or reaches through one registry entry, every per-frame runtime fact: both durable partitions (:rf.db/app, :rf.db/runtime), every :rf.runtime/* subsystem subtree the five-clause contract grades, the event queue and drain state, the subscription cache and topology, the epoch/history projection state, the host-transient subsystem state keyed beside it, the lifecycle hooks, and the resolved image generation the frame runs against.
There is no second public "live frame" registry — no separate object registry kept coherent with a backing-record registry, no two-constructor split that creates one half and then the other in a load-bearing order. The implementation MAY split storage internally for layering or performance, but there is exactly one public and conceptual owner (the frame value found by frame id), reached through make-frame as the single public constructor (per EP-0024 §One constructor). This is the substrate the five-clause subsystem grading assumes: a subsystem's subtree, its host-transient side tables, and its teardown hook all hang off the same frame value, so destroying a frame runs one teardown path (see clause 5) rather than two registries whose cleanups can succeed or fail independently.
Scope — this is NOT the generic N-partition design¶
This doc organizes the runtime-db partition's own children. It does not add partitions to the frame value, and it does not revisit the two-partition app/runtime split.
The frame value stays exactly two partitions: :rf.db/app and :rf.db/runtime (per 002 §The two partitions and Conventions §Reserved partition keys). There is no "generic N-partition app-db" — letting arbitrary top-level framework partitions sit beside :db would cost app-db its standing as a pure application contract an AI agent reads without framework noise. This doc describes how the single framework partition (:rf.db/runtime) sub-divides into subsystem children; the two-partition frame stands as it is.
The five-clause contract¶
Every runtime subsystem MUST answer these five clauses. A subtree that answers fewer is not a runtime subsystem — it is ad-hoc runtime state, outside the contract that tools, SSR projection, and the AI-Audit assume.
1. Subtree¶
The subsystem owns exactly one reserved :rf.runtime/<name> child of the runtime-db partition, plus the closed set of slots beneath it. The name is reserved in Conventions §Reserved runtime-db keys (the reserved-key table) and detailed in Conventions §Runtime-db sub-container catalogue (the per-slot path catalogue). The reserved set is fixed-and-additive: existing children cannot be repurposed; new children are added by Spec change. Each child is allocated lazily — absent until the first subsystem write — and per-frame isolated (each frame owns its own runtime-db).
The reserved
:rf.runtime/*key table is owned by Conventions.md; this doc references it and does not duplicate it. When a new subsystem lands, its child key is added to the Conventions table and a grading row is added here.
2. Write authority¶
Only framework (and runtime-extension) code mutates the subtree; ordinary application code never writes it directly. The authority is by convention surfaced through dev diagnostics, not a security boundary (per 002 §Write authority is by convention). A subsystem mints write authority through one of two named paths (per 002 §Minting framework-write authority):
- Event-handler authority — the subsystem's
reg-eventregistrations stamp the general:rf/framework-authority? trueregistration-meta key (or carry the:rf/machine? truestamp, which the runtime folds in). The runtime reads the stamp when assembling the event context; a returned:rf.db/runtimeeffect from a stamped handler is in-bounds, and an unstamped (ordinary app) handler returning:rf.db/runtimefires the:rf.warning/app-handler-runtime-effectdev diagnostic. - Privileged-helper authority — the subsystem writes through
swap-runtime-db!/replace-frame-state!(the privileged frame-state mutators of 002 §Frame-state value accessors and mutators), bypassing the event-handler path entirely (and so bypassing the diagnostic).
A subsystem MUST name which path(s) carry its writes. A subsystem that writes runtime-db from an undeclared handler is the gap the diagnostic catches.
3. Read API¶
App code reads subsystem state only through the subsystem's public subscriptions or accessor fns — never raw runtime-db paths. Framework subscriptions read the runtime-db partition (per 002 §Subscriptions read the partition they belong to); the projection-equality model makes a runtime-only change visible to framework subs and invisible to ordinary app subs. A subsystem MUST name its public read surface. A subsystem with no app-facing read surface (a purely internal registry) states that explicitly — it is read only by framework code and tools, never by app subs.
4. Projection / elision policy¶
The subsystem declares what crosses each wire boundary:
- SSR / hydration projection — what serializes onto the
:rf/hydration-payload's optional:rf/runtime-dbslice and rehydrates client-side. The projection is an allowlist by subsystem child (symmetric to the:rf/app-dbfail-closed allowlist): a new transient sub-key under a shipped subsystem does NOT silently leak. (See 011 §The hydration payload and the referenceproject-runtime-dbprojector.) - Off-box redaction — what is redacted before leaving the box, composed through the single shared
re-frame.elision/elide-wire-valuewalker (per 009 §Privacy and Managed-Effects §5). - Production elision — what is elided in production builds.
A subsystem MUST name which of its slots are durable-and-shipped vs transient-and-stripped at the SSR boundary.
5. Teardown¶
On destroy-frame!, the subsystem distinguishes durable facts (which ride frame-state serialization, hydration, restore, and time-travel) from transient handles (host handles, in-flight registries, caches — torn down and never serialized), per 002 §Durable vs transient. The subsystem hangs its frame-scoped cleanup off the single normative teardown boundary — a published teardown hook the core invokes in the strict order of 002 §Destroy. A subsystem holding frame-scoped state without a teardown hook leaks definitions and cached state on every destroy-frame!. A subsystem MUST name its teardown hook and what it releases.
One teardown path, not two (EP-0024 §Teardown). Because the frame value is the single owner of every per-frame subsystem (see §One frame value owns every per-frame subsystem), destroying a frame removes one unified value from one live registry and runs each subsystem's teardown hook exactly once. Teardown stays best-effort where individual hooks are host-transient — a host handle that fails to release does not abort the others — but the ownership path is one path: there is no separate object registry whose cleanup can succeed or fail independently of a backing frame-record registry. A subsystem's durable subtree, its host-transient side tables, and its teardown hook are released against the same frame value, so the strict destroy order of 002 §Destroy is the only teardown sequence a grader (or a tool reconstructing teardown) has to reason about.
Two derived rules¶
The five clauses imply two cross-cutting rules every conformant subsystem MUST satisfy. They are not a sixth clause — they constrain how clauses 4 and 5 are answered — but they are normative in their own right, because each names a hazard a subsystem can pass the bare clause checklist and still get wrong.
Derived rule 1 — the restore question is mandatory; allocators never rewind¶
Clause 5 (teardown) is incomplete unless it also answers the epoch-restore question: what does an epoch restore do to every value in this sub-tree? Restore is not destroy — it overwrites the durable slice with its value as of the restored epoch while the live transient world (host handles, in-flight replies already beyond cancellation) keeps running on the post-restore timeline. A subsystem MUST state, per durable slot, what restore does to it.
The load-bearing case is monotonic allocators: any counter that mints a fresh identity (a spawn counter, a generation, a work-id sequence, a nav-token) MUST NOT rewind on restore. Rewinding an allocator lets a post-restore allocation collide with a pre-restore identity still carried by an uncancellable in-flight reply or an already-emitted side effect — a stale generation mistaken for a live one. The discipline is the anti-recycling rule (the routing discipline, generalized): the canonical resolution is to hold such allocators outside the revertible frame-state entirely (the host-side transient cache, as routing does with its nav-token / pending-nav counters — see the :rf.runtime/routing grading row clause 1), so an epoch restore physically cannot rewind them.
A subsystem whose durable slice contains a recycled or rewindable identity is a contract violation even if it names a teardown hook (clause 5) and a projection policy (clause 4): the restore question is the part of clause 5 a bare destroy-only reading misses.
Derived rule 2 — one authoritative home per fact; mirrors are recomputable projections¶
Where a subsystem holds a denormalized copy of data whose authoritative home is elsewhere — an index, a denormalized owner set, a derived lookup, a cached cross-reference — the copy is a recomputable projection of its authoritative home, never a second source of truth. Such a mirror MUST be declared recomputable-from-source, and on both restore and SSR hydration it is rebuilt from the authoritative source rather than trusted from (or carried on) the serialized snapshot. The durable wire payload (clause 4) therefore need not carry the mirror at all, and a stale or partial mirror can never outlive the facts it describes.
This rule constrains clauses 4 and 5 together: clause 4's projection policy declares the mirror out of the durable payload (it is re-derived, not shipped), and clause 5's restore answer (derived rule 1) recomputes it rather than installing it. It is the durable-state half of the one-name-per-fact discipline — a mirror MUST NOT become a co-equal home that can drift from its source. (The same discipline appears as a naming rule for cross-layer spellings; here it is a state-ownership rule for denormalized copies.)
Why the contract matters — the routing write-authority worked example¶
Routing is the worked example of why naming clause 2 (write authority) pays off. Routing's event handlers (:rf.route/navigate, :rf.route/handle-url-change, …) read and return the reserved route slice — they are legitimate framework writers. Were event-handler authority minted from :rf/machine? only, every navigation would trip the :rf.warning/app-handler-runtime-effect ownership diagnostic in dev — polluting the Xray Issues lens and training users that the warning is noise.
So authority is minted from the general :rf/framework-authority? true registration-meta key the routing façade stamps on its reg-event registrations, which the router folds into the authority check alongside :rf/machine?.
The contract is the policy layer; the registration-meta stamp is the mechanism. This doc names who gets framework-write authority (clause 2: whoever registers a runtime subsystem, via a named path). The :rf/framework-authority? stamp is the concrete CLJS-reference mechanism that implements it — owned by 002 §Minting framework-write authority, and decoupled from this doc. This doc does not redefine or re-touch that mechanism; it names the shape the mechanism serves.
The plugin / extension seam¶
A 3rd-party library that needs durable, revertible, framework-owned frame-state becomes a new graded instance of this contract. It registers a :rf.runtime/<lib> subsystem and answers all five clauses:
- Reserve the
:rf.runtime/<lib>child (the:rf.runtime/*namespace is the framework-owned durable-state seam; a feature-modular library claims a child under it, per Conventions §Feature-modularity prefix convention). - Mint write authority — stamp
:rf/framework-authority? trueon its event handlers, or write through the privileged frame-state helpers. - Ship a public sub/accessor for app reads.
- Declare its SSR projection (which slots ship) and off-box/production elision.
- Publish a teardown hook off
destroy-frame!.
A library that satisfies all five is a runtime subsystem on equal footing with the shipped four; one that satisfies fewer is ad-hoc runtime state, outside the contract tools and SSR projection assume. This is the principled home the framework-authority question ("who may write runtime-db?") points to — it is enumerable: the runtime subsystems are exactly the children that answer the five clauses.
Grading table — the shipped subsystems¶
One row per existing runtime-db child, graded against all five clauses. ✅ = the clause is satisfied with a named mechanism; (implicit) flags a clause a subsystem leaves implicit rather than spelling out at its own surface. The three v1 subsystems (machines / routing / SSR) are graded first; EP-0003's shipped resource pair (resources / mutations) follows — its rows are lifted from 016 §Runtime-subsystem graduation, which is the canonical source for the resource grading content.
:rf.runtime/machines — state-machine runtime (Spec 005)¶
| Clause | Grade |
|---|---|
| 1 Subtree | ✅ :rf.runtime/machines with the closed slot set :snapshots / :spawned / :spawn-counter / :spawn-order (per Conventions §Runtime-db sub-container catalogue; slots 005 §Where snapshots live). Each slot allocated lazily. |
| 2 Write authority | ✅ Event-handler path — machine handlers carry the framework-owned :rf/machine? true stamp (minted by the machine registrar), which the runtime folds into the authority check, so a machine implies framework-write authority without a separate :rf/framework-authority? key (per 002 §Minting framework-write authority). Snapshot commits + the spawn-fx write through this path. |
| 3 Read API | ✅ [:rf/machine <id>] (plus [:rf.machine/has-tag? …]) — "User code MUST NOT write under [:rf.runtime/machines …]; it reads machine state through the [:rf/machine <id>] subscription, never raw runtime-db paths" (005 §where snapshots live). |
| 4 Projection / elision | ✅ Shipped whole on the hydration payload — snapshots + spawn registry are durable serializable facts the client re-materialises actors from (the reference project-runtime-db ships :rf.runtime/machines verbatim; 011). The [:rf/runtime :machine-type] snapshot slot is the load-bearing durable fact (liveness rides the revertible snapshot). Per-slot :sensitive? on machine :data composes through re-frame.elision/elide-wire-value. |
| 5 Teardown | ✅ The machines lifecycle contribution's pre-dead member (step 3 of 002 §Destroy) — walks active machines in reverse-creation order (taken from the durable :spawn-order vector, reversed — which is why that order survives epoch restore and SSR hydration), runs each :exit cascade, applies the unified teardown projection (snapshot + :spawn-order entry + spawn-slot prune), unregisters per-actor handlers, emits :rf.machine.lifecycle/destroyed. Durable kept: the snapshots (they ride restore/SSR). Transient dropped: the per-actor handler registrations + the :after timer table (its post-dead member, step 6). |
:rf.runtime/routing — routing runtime (Spec 012)¶
| Clause | Grade |
|---|---|
| 1 Subtree | ✅ :rf.runtime/routing with :current (the route slice, schema :rf/route-slice) + :pending-navigation (per Conventions §Runtime-db sub-container catalogue; 012 §The route slice). (implicit, excluded): the nav-token / pending-nav counters and saved scroll positions are host-side transient caches, NOT runtime-db — held outside the frame value so an epoch restore cannot rewind + recycle a token. The split is explicit in Conventions but the subtree's boundary (what is absent) is the subtle part a grader must check. |
| 2 Write authority | ✅ Event-handler path — the routing façade stamps :rf/framework-authority? true on every reg-event it registers (:rf.route/navigate, :rf.route/handle-url-change, :rf.route/continue / :rf.route/cancel) — every one reads and returns the reserved route slice (per 002 §Minting framework-write authority). This is the clause the ownership-diagnostic worked example above turns on. |
| 3 Read API | ✅ The :rf.route/* sub family — [:rf.route/id], [:rf.route/params], [:rf.route/query], [:rf.route/fragment], [:rf.route/transition], plus (rf/sub :rf/route) (012 §Subscriptions). App code reads these, never [:rf.runtime/routing …]. |
| 4 Projection / elision | ✅ Allowlist-by-key — only the durable :current slice ships (durable-routing-keys [:current] in the reference project-runtime-db); :pending-navigation is a local-subscribable runtime-db key that stays client-local / SSR-stripped (fail-closed) (Conventions §Runtime-db sub-container catalogue, 011). The host-side counter/scroll caches are not runtime-db at all, so the allowlist would strip them regardless. |
| 5 Teardown | ✅ The routing lifecycle contribution's post-dead member (012 §Scroll restoration) — releases the host-side saved scroll-position LRU and reconciles the browser URL listener. The nav-token / pending-nav allocators are host-side too, but process-global: they outlive the frame, so a same-id successor never re-issues a token. Durable kept: the runtime-db route state (:current / :pending-navigation) is durable and rides frame-state, so it is released atomically when the frame value is dropped at destroy — it needs no separate cleanup pass. Transient dropped: the host-side scroll-position cache the member releases (host-derived, ephemeral, meaningless after a restore — transient per 002 §Durable vs transient). |
:rf.runtime/ssr — SSR / hydration metadata (Spec 011)¶
| Clause | Grade |
|---|---|
| 1 Subtree | ✅ :rf.runtime/ssr with :hydration → {:version <int-or-absent>} (per Conventions §Runtime-db sub-container catalogue; 011 §The :rf/hydrate event). Allocated lazily — absent on frames that never hydrated. |
| 2 Write authority | ✅ Both paths. Event-handler path — the SSR façade stamps :rf/framework-authority? true on :rf/hydrate, which installs the hydration metadata ([:rf.runtime/ssr :hydration :version]) into the runtime-db partition. SSR's non-event writes (full-frame install / restore on the server render path) go through the privileged frame-state helpers (replace-frame-state!), bypassing the diagnostic (per 002 §Minting framework-write authority). The reference handler exports the write-fn; the reg-event lives in the re-frame.ssr façade. |
| 3 Read API | ✅ The :rf.ssr/check-version fx reads :version for the compatibility check (011 §The :rf/hydrate event). (implicit): there is no app-facing :rf.ssr/* subscription — the metadata is consumed by the framework's compatibility check, not app subs. |
| 4 Projection / elision | ✅ Ships on the hydration payload (the reference project-runtime-db ships :rf.runtime/ssr whenever present) — the metadata is small framework state (the payload's version) the client's compatibility check reads. It is the durable arm of the SSR surface; the SSR request/response accumulators and pending-error buffers are transient and explicitly MUST NOT ride the wire (per 002 §Durable vs transient, 011 §payload policy). |
| 5 Teardown | ✅ The SSR lifecycle contribution's post-dead member (step 6 of 002 §Destroy) — clears the per-request side-channels. Durable kept: the :hydration metadata is durable runtime-db that rides the frame value (released atomically when the frame is dropped). Transient dropped: the request/response accumulators and pending-error buffers (the side-channels the member clears). Head reads keep no per-frame state — head-model RETURNS the model — so teardown has nothing to release for them. |
:rf.runtime/resources — resource cache (Spec 016)¶
The runtime-subsystem contract's first graduating instance outside the v1 four — shipped with EP-0003. Lifted from 016 §Runtime-subsystem graduation (the canonical home); see that section for the full prose.
| Clause | Grade |
|---|---|
| 1 Subtree | ✅ :rf.runtime/resources with the closed slot set :entries / :tag-index / :owner-index / :last-gc-window (per Conventions §Runtime-db sub-container catalogue; 016 §Cache home and write authority). Each entry carries its live attempt under :work and its terminal tail under :recent-work (016 §In-flight attempts). Allocated lazily — absent until the first resource write — and per-frame isolated. |
| 2 Write authority | ✅ Event-handler path — every resource reg-event (:rf.resource/ensure, :rf.resource/refetch, :rf.resource/invalidate-tags, :rf.resource/release-owner, :rf.resource/clear-scope, :rf.resource/remove, plus the internal :rf.resource.internal/* replies) stamps :rf/framework-authority? true (the framework-authority mechanism), so a returned :rf.db/runtime effect is in-bounds (per 002 §Minting framework-write authority). Stale/GC and host-handle bookkeeping done outside the event-handler path writes through the privileged frame-state helpers (swap-runtime-db! / replace-frame-state!). See 016 §Write authority. |
| 3 Read API | ✅ The :rf.resource/* sub family (:rf/resource, :rf.resource/data, :rf.resource/status, :rf.resource/loading?, :rf.resource/fetching?, :rf.resource/stale?, :rf.resource/error, :rf.resource/refresh-error, :rf.resource/has-data?, :rf.resource/previous-data) plus tool reads (the :rf/resource registration projection, resource-state, the list-resource-instances / get-resource-state family, and Xray's live-attempt table). App code never reads raw [:rf.runtime/resources …] paths. |
| 4 Projection / elision | ✅ Allowlist-shaped — only the durable resource projection rides the :rf/hydration-payload :rf/runtime-db slice via the explicit projection hook (016 §SSR and hydration); :tag-index / :owner-index are recomputable-from-:entries (per derived rule 2) and need not ride the wire, and the projection strips each entry's attempts, since in-flight work belongs to the server timeline. Params, scopes, and data carry :sensitive? / :large? classification through the shared re-frame.elision/elide-wire-value walker; Xray sees redacted summaries, not raw values. |
| 5 Teardown | ✅ The Resources lifecycle contribution's post-dead member (016 §Stale and GC scheduling, 016 §Restore and replay) — frame destroy cancels all resource timers and clears the per-frame host handles (side tables keyed by frame id and work id). Durable kept: :entries (cache facts ride restore/SSR; each entry's attempts ride restore). Transient dropped: AbortControllers, stale/GC timers, transport promises (never serialized); :tag-index / :owner-index are recomputed from :entries on install (derived rule 2); a restored live attempt is immediately reconciled to dangling — its :work/id can never re-match a live entry because the generation allocator is monotonic and host-side (per derived rule 1). |
:rf.runtime/mutations — mutation-instance runtime (Spec 016)¶
The causal-write counterpart of :rf.runtime/resources, shipped with the first public-beta gate. It owns its own subtree (instance rows keyed by mutation instance id), and each instance carries its own attempt (work-kind :mutation) exactly as a resource entry does. Present only when the app registers a mutation.
| Clause | Grade |
|---|---|
| 1 Subtree | ✅ :rf.runtime/mutations with serializable mutation instance rows keyed by instance id (:mutation/id / :instance/id / :status / :result / :error / :scope / :params / :generation / :current-work / :work / :recent-work / :started-at / :settled-at / :affected-keys / :patch-summary; schema MutationInstance in Spec-Schemas). Keyed by instance id (NOT mutation id) so concurrent submissions never clobber each other. Allocated lazily — absent in an app that registers no mutations (per Conventions §Runtime-db sub-container catalogue; 016 §Mutations). |
| 2 Write authority | ✅ Event-handler path — :rf.mutation/execute / :rf.mutation/clear (and the internal reply handlers) stamp :rf/framework-authority? true; the in-flight write lowers through the same managed-HTTP transport as resources, its attempt (work-kind :mutation) kept on the instance. Generation + work-id stale suppression is the correctness boundary as for resources (per 016 §Mutations). |
| 3 Read API | ✅ The passive :rf.mutation/* sub family — :rf/mutation, :rf.mutation/status, :rf.mutation/pending?, :rf.mutation/result, :rf.mutation/error — keyed by instance id, projecting the instance view-model. App code reads these, never raw [:rf.runtime/mutations …] paths. |
| 4 Projection / elision | ✅ The instance rows store facts, not derived booleans (:pending? / :success? / :settled? are computed in the subs layer), so the durable row is a small projectable fact; :affected-keys / :patch-summary carry the optimistic-rollback trace shape (016 §Optimistic mutations, EP-0019). Params, result, and error carry :sensitive? / :large? classification through re-frame.elision/elide-wire-value; the :error envelope is the closed :rf.http/* failure shape. |
| 5 Teardown | ✅ Host handles live in the shared [frame-id work-id] side tables (cleared on frame destroy with the resource hook); :rf.mutation/clear is the causal instance reset (clears the runtime instance and best-effort aborts in-flight work). Durable kept: the instance rows (facts). Transient dropped: host handles; the instance's live attempt reconciles to dangling on restore exactly as an entry's does (the generation allocator is monotonic and host-side — derived rule 1). |
Summary — clauses left implicit¶
Every graded subsystem satisfies all five clauses; the table flags where a clause is satisfied implicitly rather than spelled out at the subsystem's own surface — the seams a new subsystem author should make explicit when grading their own instance:
:rf.runtime/routing— clause 1 (the subtree boundary: which routing facts are excluded and held host-side). Clause 5 is spelled out — routing's post-dead member releases the host-side scroll cache while the durable route slice rides the frame value — but the split (durable rides frame-value-drop; only the host-side cache needs the member) is the subtle part a grader should note.:rf.runtime/ssr— clause 3 (read surface is the framework's compatibility check, not an app sub).:rf.runtime/machines,:rf.runtime/resources,:rf.runtime/mutations— all five clauses explicit; the cleanest worked instances of the contract.
These are not gaps in the framework — every clause is satisfied — but they are the clauses a future subsystem (a 3rd-party :rf.runtime/<lib>) should answer explicitly at its own surface rather than leaving the grader to trace through. The resource pair (resources / mutations) grades clean against this checklist.
How a new runtime subsystem inherits the contract¶
:rf.runtime/resources and :rf.runtime/mutations are each graded against this checklist — each carries a grading row in the table above, answering all five clauses with named mechanisms. Any future subsystem follows the same path. Concretely, a new subsystem:
- Reserves its
:rf.runtime/<name>child in Conventions §Reserved runtime-db keys (the reserved-key table MUST be amended) with its closed slot set catalogued in Conventions §Runtime-db sub-container catalogue. - Mints write authority via
:rf/framework-authority? trueon its event handlers, or via the privileged frame-state helpers, naming the path (per 002 §Minting framework-write authority). - Ships a public sub/accessor for app reads (or states explicitly that it has none).
- Declares its SSR projection (which slots ship, allowlist-shaped) and off-box/production elision.
- Publishes a teardown hook off
destroy-frame!(per 002 §Destroy), naming durable-kept vs transient-dropped.
A subtree that answers fewer than five is ad-hoc runtime state, not a runtime subsystem; tools, SSR projection, and the AI-Audit treat it as out-of-contract.
The operational environment — process + frame ownership¶
Public model:
image → frame → event stream(EP-0023, final; EP-0024, final). This section names the non-durable operational layer beside the durable subsystems — the registrar, the adapter, the late-bind hook table, and the host-transient side tables — and says who owns each. There is no runtime-realm container, no per-container registrar, and no realm/app/module constructor / install / query facade export; there is no(realm, frame)two-part address — a frame is addressed by its id alone (per 002 §Frame addressing).
The five-clause contract above organises the durable runtime-db partition: the :rf.runtime/* children of :rf.db/runtime, which ride frame-state serialization, restore, and SSR hydration. But a running app also holds a second framework-owned layer that is not durable frame-state — the registrar it dispatches against, the reactive adapter it renders through, the late-bind hooks through which core reaches its optional artefacts, and the host-transient side tables (HTTP abort handles, timers, nav counters, scroll caches, flow last-input caches, machine timer handles). This section names where that layer lives.
The operational environment splits across two owners. The process owns the singletons every frame shares — the registrar, the installed adapter selection, the late-bind hook table. Each frame owns its own non-durable lifecycle state — its host-transient side tables (mostly keyed per-frame) and its teardown bookkeeping — torn down on destroy-frame! (per EP-0024, every per-frame fact hangs off the one frame value). The durable runtime-db subsystems live inside each frame; the operational environment surrounds them at these two boundaries.
This completes the ownership picture for framework-owned state:
| Layer | Owner | Lifecycle | This doc |
|---|---|---|---|
Durable frame-state (:rf.runtime/* children) |
the frame (per-frame, isolated) | rides restore / SSR / time-travel | the five-clause contract + grading table above |
| Host-transient subsystem state (abort handles, timers, caches) | the frame (keyed per-frame, beside the frame value) | torn down on destroy-frame!; never serialized |
§Host-transient subsystem state below |
| Operational environment (registrar, adapter, late-bind hooks) | the process | created at boot; lives for the process | §The operational environment (this section) |
The environment is process-global; resolution is frame-derived¶
The registrar's source store, the installed adapter, and the late-bind hook table are process-global singletons — every reg-* writes to the one source store, every frame in the process renders through the same adapter, and every late-bind lookup reads the same hook table. Handler resolution is frame-derived: a frame resolves event / subscription / fx / cofx handlers through its own resolved image generation, which make-frame assembles from that source store, or, for a frame made without :images, through that store live, and supplies the state a resolved handler runs against (its app-db / runtime-db) (per 002 §Frame addressing).
This is operational resolution, not a frame-target fallback. "Which source store / adapter / late-bind hook does this operation use?" is answered by the process singletons, and "which handler does this id resolve to?" by the targeted frame's own view of the registrations. "Which frame does this dispatch target?" is a separate question: dispatch, subscribe, and other frame-scoped operations still require an explicit frame, a carried frame, or an established frame scope, and still fail with :rf.error/no-frame-context per EP-0002 / 002 §Frame target resolution when no frame is known. The runtime never synthesises a frame from absence.
What the process owns; what the frame owns¶
The process owns:
- the registrar every frame dispatches/subscribes/resolves against (the
(kind, id) → registration descriptortable — owned by 001-Registration; the surface existing specs call "global"); - the installed adapter selection (§Adapter ownership);
- the late-bind hook table (§Late-bind hooks);
- the live frame registry — one entry per frame id (frame ids are unique in the one process-local frame-id space, per 002 §Frame addressing).
Each frame owns:
- its host-transient subsystem state (§Host-transient subsystem state), keyed per-frame beside the frame value;
- its lifecycle / disposal state — the teardown bookkeeping
destroy-frame!walks (per §One frame value owns every per-frame subsystem).
The registrar, adapter, and late-bind hook table are process singletons because every frame shares them — duplicating them per frame would multiply identical registrations across the common many-frames-one-app case, with no consumer use case. The host-transient side tables are per-frame because they hold a frame's in-flight handles and caches, which must be released exactly when that frame is destroyed.
Adapter ownership¶
The CLJS reference rule "one adapter per process" is the implementation strategy: adapter selection is a process singleton.
The boundary (issue 4 disposition):
- the process owns adapter selection — which reactive substrate (Reagent / UIx / plain-atom) the app renders through (the capability);
- render roots own concrete mount/disposer instances of that adapter.
The invariant:
Reactive values created under a render subtree use the process adapter
consistently, and are disposed when the subtree or frame is disposed.
The framework does not make mixing two adapters inside one frame's reactive graph legal. The no-mixing-within-one-frame's-graph invariant is the conformance test. The adapter contract itself is owned by 006-ReactiveSubstrate §The adapter contract; this doc names only adapter ownership — process (selection) + root (instances). (Running two adapters in one process via two realms is not part of the shipped model.)
Service dependencies¶
A frame reaches the runtime services it depends on — HTTP execution, schema validation, route integration, SSR hooks — through four channels:
- Registrations, selected by images. An fx or cofx handler is the service, and an image that registers a different handler under the same id replaces it. A hermetic test composes a later overrides image whose
:registrationsstub exactly the effects and coeffects it controls — HTTP, a fixed clock, a seeded random source (008 §Hermetic-frame testing). - Frame config. The record-config keys of
make-frame—:fx-overrides,:platform,:ssrand:interceptorsamong them — configure one frame without touching the process (002 §:fx-overrides). - The adapter. The installed reactive substrate supplies reactive values and rendering (§Adapter ownership).
- Late-bind hooks. Core reaches an optional artefact's functions through the late-bind hook table (§Late-bind hooks).
A host fact that a durable decision depends on — the time, a random draw, a generated UUID — arrives as a recorded coeffect on the event's :rf.cofx, never as an ambient host read (005 §Causal host facts).
There is no capability map. make-frame gives a :capabilities key no special meaning — it is ordinary record-config — and no runtime check reports a missing capability. A port MAY add a capability layer, an explicit map of runtime services handed to a frame, under the :rf.capability/* namespace; Conventions §Reserved namespaces reserves it for that purpose whether or not a port ships one.
Late-bind hooks¶
The late-bind hook table (re-frame.late-bind) is the process-global registry through which a namespace calls a function it cannot :require: an optional artefact (schemas, flows, routing, machines, SSR, HTTP) that may be absent from the build, or a higher-level namespace that a leaf namespace would otherwise form a load cycle with. The producing namespace publishes at load time with set-fn! (one key) or set-fns! (a map of keys); the consumer resolves at call time with get-fn, which reads the table on every call. A key whose artefact is absent resolves to nil, and the consumer no-ops or fails with :rf.error/<artefact>-artefact-missing. re-frame.late-bind.directory lists every published key, and a drift test holds the directory to the publication sites.
An artefact's frame-lifecycle work does not go through the table: it is the artefact's lifecycle contribution (§Host-transient subsystem state).
The hook table is load-order plumbing, not a service locator for app architecture. An app that wants a dependency explicit and swappable — HTTP, routing, schemas, a clock — models it as a registration and swaps it by image composition or frame config (§Service dependencies).
Host-transient subsystem state¶
Host-transient subsystem state belongs to the frame. It is the framework-owned operational state that is not durable frame-state: HTTP in-flight handles and abort controllers, routing nav counters and scroll caches, machine :after timers, flow last-input caches, SSR request side channels, and adapter render roots and disposers. Subsystems key entries per-frame (beside the frame value), and that state is released on destroy-frame! — not held in an arbitrary namespace-level singleton with no teardown owner.
This is the non-durable sibling of the five-clause contract. A durable runtime subsystem (the rows in the grading table above) lives under :rf.runtime/* and rides restore/SSR; a host-transient table is dropped on teardown and MUST NOT ride the wire. Most shipped subsystems have both halves — :rf.runtime/routing keeps its :current route slice durable while its nav-token / scroll caches are host-side transient (the routing grading row's clause 1 + clause 5 above already records exactly this split); :rf.runtime/resources keeps :entries durable while its AbortControllers and stale/GC timers are host-side (the resources grading row's clause 5 above).
A subsystem's host-transient descriptor is its artefact's lifecycle contribution (002 §Lifecycle contributions) — the one entry through which the artefact releases that state on destroy, resets it between tests, and installs whatever host listeners a frame's config names. The http artefact's:
{:id :http
:phase #{:post-dead} ;; the destroy phases it joins
:config-keys #{:http-interceptors} ;; the bare frame-config keys it reads
:on-frame-destroyed (fn [frame-id _phase] ;; abort the frame's in-flight requests
(abort-in-flight-on-frame-destroyed! frame-id))
:test-reset (fn [] ;; the hermetic-test reset
(clear-all-in-flight!)
(clear-all-http-interceptors!))}
A contribution may also carry :on-frame-registered, which installs or reconciles the host listeners a frame's config declares, and :preflight-config, which refuses a bad declaration before anything is written. It declares nothing about durability, snapshots or classification: what a contribution owns is host-transient by construction, so it never rides the wire.
Host-transient is the fourth member of the storage axis owned by Derivations §the storage axis (:ephemeral / :app-db / :runtime-db / :host-transient). The contribution shape is :rf/lifecycle-contribution in Spec-Schemas.
The host-transient grading column¶
This doc carries a host-transient grading column — the ownership-and-lifecycle grade for each subsystem's host-transient half (issue 13 disposition: EP-0006 owns the durable five-clause contract; this column complements it). One row per shipped subsystem that holds host-transient state, graded on scope (frame vs process), teardown (the lifecycle contribution, by :id, and the destroy phase that releases it), and test-reset (the hermetic reset). ✅ = a named mechanism; (none) = the subsystem holds no host-transient state of that kind.
| Subsystem | Host-transient state | Scope | Teardown | Test-reset |
|---|---|---|---|---|
:rf.runtime/machines |
per-actor handler registrations; :after timer table |
frame | ✅ :machines, post-dead (the timer cancel, step 6 of 002 §Destroy); the per-actor handler unregistration runs inside its pre-dead :exit cascade (step 3) |
via the reset-runtime fixture |
:rf.runtime/routing |
nav-token / pending-nav counters; saved-scroll-position LRU | frame (held outside frame-state — anti-recycling, derived rule 1) | ✅ :routing, post-dead — the scroll LRU; the nav allocators are process-global and outlive the frame |
via the reset-runtime fixture |
egress classification (re-frame.elision; no runtime-db child) |
the per-frame classification-policy cache; elision dedupe (warn) caches | frame | ✅ :elision, post-dead (drops the frame's policy-cache entry and resets the warn caches, step 6 of 002 §Destroy) |
reset with the warn caches |
:rf.runtime/ssr |
request/response accumulators; pending-error buffers | frame | ✅ :ssr, post-dead (step 6 of 002 §Destroy) |
via the reset-runtime fixture |
:rf.runtime/resources + :rf.runtime/mutations |
AbortControllers; stale/GC + timeout/poll timers; transport promises (side tables keyed by [frame-id work-id]) |
frame | ✅ :resources, post-dead (cancels timers, clears the [frame-id work-id] side tables) |
via the reset-runtime fixture |
| adapter (the process reactive substrate) | the install slot; render roots + disposers | process (selection) / root (instances) | root disposal (§Adapter ownership) | adapter install/test-reset |
The grading column keeps every subsystem's host-transient half honest: the conformance drift test that keeps the durable grading table honest covers this column too, so a host-transient row cannot silently go missing. This grading column is part of the Runtime-Subsystems.md contract, not a second one.
Cross-references¶
- Conventions §Reserved runtime-db keys — the reserved
:rf.runtime/*key table (clause 1's canonical home; this doc references, does not duplicate). - Conventions §Runtime-db sub-container catalogue — the per-slot path catalogue for every shipped child.
- 016 §Runtime-subsystem graduation — the canonical source for the resource grading rows (resources / mutations) mirrored into the grading table above.
- 002 §Write authority is by convention and §Minting framework-write authority — clause 2's canonical home (the
:rf/framework-authority?mechanism). - 002 §Subscriptions read the partition they belong to — clause 3's canonical home.
- 002 §Durable vs transient — clause 4/5's durable-fact-vs-transient-handle split.
- 002 §Destroy — clause 5's teardown phases; 002 §Lifecycle contributions — the per-artefact contributions that join them.
- 011 §The hydration payload — clause 4's SSR projection (the allowlist-by-subsystem-child
project-runtime-db). - Managed-Effects.md — the effect-surface sibling of this doc (names the effect shape; this names the state shape).
- EP-0023 — establishes
image → frame → event streamas the public model for frame-loaded registration sets; the frame id is the whole address. - EP-0024 — the one-frame-value / one-registry / one-teardown-path ownership backbone the host-transient state and its teardown hang off.
- EP-0013 — the runtime-realm container model record (status
superseded-by EP-0023). - EP-0006 — owns the durable five-clause contract; §The host-transient grading column complements it, not a second contract.
- 002 §Frame addressing — the frame id is the whole public address; resolution is frame-derived.
- Spec-Schemas §
:rf/lifecycle-contribution— the lifecycle-contribution shape. - Conventions §Reserved namespaces — the framework-owned
:rf.*reserved-namespace table (the:rf.capability/*row is reserved here). - Ownership — the contract-surface → owning-Spec map; consult before naming a new runtime subsystem.