re-frame2 — API¶
Type: Reference Reference for the CLJS implementation's API: signatures, status, cross-references. No rationale — per-Spec docs own the why. Pattern-level contracts live in 000-Vision §The pattern and the per-Spec docs.
:fx-overridesasymmetry: id-valued at the pattern level; CLJS reference also accepts fn values — see 002 §:fx-overrides.
Conventions¶
- Status — exactly one base value, optionally combined with one or more parenthesised qualifiers. The closed set below is the same vocabulary the generated
api-manifest.edncurates (its:statusfield) — the two MUST agree:- Base values:
v1(ships in v1).v1 (preserved)(exists in current re-frame; preserved unchanged).v1 (preserved + extended)(exists today; v1 adds new arity or behaviour).EP-NNNN(a surface introduced or reshaped by a named pre-alpha EP, shipping in v1 — e.g.reg-event(EP-0018),reg-interceptor(EP-0022); the canonical lineage is the named EP, cited in the row's Notes).post-v1 lib(design spec in v1 Specs but ships in a post-v1 library).post-v1 (planned, rf2-<id>)(specced normatively but not yet shipped; the impl is tracked by the named bead — per the Projection-maintenance rule below).
- Qualifiers (parenthesised, combinable):
dev-only(elided in production builds — the macro emit site or runtime body, depending on the API);changed, EP-NNNN(a preserved v1 surface a named EP changed — e.g.v1 (changed, EP-0017));optional capability(ships only when the owning optional artefact is on the classpath, optionally narrowedoptional capability, dev/test);internal lowering only(an EP surface retained as a framework-internal lowering seam, not a public authoring form — e.g.EP-0022 (internal lowering only)). - Examples:
v1,v1 (preserved),v1 (dev-only),v1 (preserved, dev-only),v1 (changed, EP-0017),EP-0018,EP-0022 (internal lowering only),v1 (optional capability),post-v1 lib,post-v1 (planned, rf2-<id>). - The
re-frame.alphanamespace is dissolved — no APIs in this reference live outsidere-frame.core(with the documented per-namespace exceptions:re-frame.test-supportandre-frame.test-helpers).
- Base values:
- Macro/Fn: marked
M(macro) orFn. - Spec column — names exactly the canonical owning Spec (the per-Spec doc whose contract this API implements). Migration rules and other cross-references are NOT in the Spec column; they appear in the Notes column when relevant.
- Configure keys — runtime configuration is uniformly via
(rf/configure! {<key> <opts>, …}), a single nested map. Every<key>is enumerated in §Configure keys below; per-area tables call out which keys their APIs read but do not redefine the key's vocabulary. -
Per-artefact public namespaces. The core surfaces live in
re-frame.core. Per-feature artefacts ship their own public namespace; consumers:requirethe namespace directly (with the documented exception of the epoch surface, which late-bind re-exports throughre-frame.core). Front-porch boundary.re-frame.coreis the small app-developer front porch — registration, dispatch, subscribe, the frame basics, interceptors, lifecycle, configure, and the core trace / egress / registrar-query / app-realm surfaces. The optional-feature registration MACROS stay on the façade (reg-route,reg-flow,reg-app-schema/reg-app-schemas,reg-machine/defmachine,reg-resource/reg-mutation/reg-resource-scope,reg-error-projector,reg-head,reg-http-interceptor) because they capture call-site source-coords and have no owned-namespace macro form — registration stays central. But the optional features' non-registration query / introspection / lifecycle helpers are NOT re-exported fromre-frame.core; reach them through their owning namespace:re-frame.schemas(app-schemas/app-schema-meta/app-schemas-digest/set-schema-fns!/schema-fns/default-schema-fns),re-frame.machines(reg-machine*/make-machine-handler/machine-transition),re-frame.routing(match-url/route-url). The registrar INVERSE is the exception to that rule and stays on the façade as the one kind-keyedclear(§Clearing registrations): it dispatches across four optional artefacts and core, so no owning namespace could carry it, and the façade row is what turns an absent artefact into a documented:rf.error/<artefact>-artefact-missingrather than a hard require failure. The epoch re-exports remain on the façade as the one documented late-bind exception (rows below); the SSR QUERY surface is deliberately NOT among them (loadingre-frame.ssris what installs the SSR runtime, so every SSR app already names the artefact namespace):Namespace Artefact Surfaces re-frame.corecore the registration / dispatch / subscribe / interceptor / lifecycle / configure surfaces; late-binds re-exports for ONE artefact: the re-frame.epochsurface (epoch-history,restore-epoch!,replay-epoch!,replace-frame-state!) — the epoch listener stream is reached through the core-native(register-listener! :epoch …)verb, not a per-channel re-export. SSR contributes only its two REGISTRATION macros (reg-head/reg-error-projector, rowed in §Registration like every other artefact's registrar); its query surface is reached at home onre-frame.ssr/re-frame.ssr.head. Thestreaming-render-shell/streaming-render-continuation/streaming-build-final-payloadtriple is not re-exported — the streaming surface is host-adapter territory and the SSR-aware host (re-frame.ssr.ring/ equivalents) requires[re-frame.ssr :as ssr]directly. Re-exports activate when the named artefact is on the classpath; absent artefacts surface:rf.error/<feature>-artefact-missingerrors.re-frame.test-supportcore assert-path-equals,poll-until, fixture machinery (per §Testing). Runtime-state axis — registrar, frames,app-db, drain.assert-path-equalsmirrors the:rf.assert/path-equalsStory event. View-tree assertions live in the siblingre-frame.test-helpers.re-frame.test-helperscore View-assertion helpers — hiccup-walk ( find-by-testid/find-by-attrfamily,text-content,extract-handler,invoke-handler), thetestidauthoring helper, and theexpand-treewalker (per §Testing — View-assertion helpers). View-tree axis — hiccup data, testids, attached handlers. Runtime-state assertions live in the siblingre-frame.test-support.re-frame.ssrday8/re-frame2-ssrrender-to-string,emit-ui-tree,render-tree-hash,streaming-render-*,project-error,head-model->html,hydrate!(per §SSR).reg-head,head-modelanddefault-headare defined in the siblingre-frame.ssr.head, which consumers(:require [re-frame.ssr.head :as head])directly; thereg-headREGISTRAR also rides there-frame.corefaçade.re-frame.ssrre-exportshead-modelandhead-model->htmlfrom that sibling, so the whole read side of the head contract sits on the same door asrender-to-string.re-frame.ssr.ringday8/re-frame2-ssr-ringthe Ring host-adapter (default-html-shell, streaming-prefix/suffix, trusted-shell hooks per Spec 011); ssr-handler's:rendererconstruction opt is the render-body seam —(fn [{:keys [frame-id request opts]}] → {:body-html :render-hash}), default the local:root-viewrender, refused bystream-handler(Spec 011 §HTTP response contract).re-frame.schemasday8/re-frame2-schemasapp-schemas,app-schema-meta,app-schemas-digest,set-schema-fns!,schema-fns,default-schema-fns(per §Schemas).re-frame.machinesday8/re-frame2-machines(post-v1 scaffolding)reg-machine/defmachine(registration),make-machine-handler(pure factory: Level-2 testing, the spawn path),machine-transition, the:rf.machine/spawn/:rf.machine/destroyfx (per §Machines).re-frame.epochday8/re-frame2-epochepoch-history,restore-epoch!,replay-epoch!,replace-frame-state!,(rf/configure! {:epoch-history ...}), and the epoch listener stream reached via(rf/register-listener! :epoch id f)/(rf/unregister-listener! :epoch id). Re-exported / late-bound throughre-frame.corevia late-bind hooks —(:require [re-frame.epoch])at boot before consuming the surfaces throughre-frame.core(per Tool-Pair §Time-travel — Artefact home).re-frame.adapter.uixday8/re-frame2-uixUIx-specific surfaces (per §UIx adapter). -
Projection-maintenance rule. This doc is a non-canonical projection — the canonical contract lives in the per-Spec docs cited in each row's Spec column. The projection MUST stay in sync with shipped artefacts. Every row carries: owner (Spec column) — the canonical spec doc; artefact / namespace — where the public-var lives (table above); public-var status —
v1/v1 (preserved)/post-v1 lib/post-v1 (planned, rf2-<id>)for surfaces specced normatively but not yet shipped (the spec contract holds; the impl is tracked by the named bead); verification pointer — the conformance fixture, the per-artefact test, or the AI-Audit row that asserts the row holds. Rows that document a surface neither shipped nor on a tracking bead MUST be cut from this projection — the design's normative claim then lives only in the owner spec.
Tier taxonomy¶
Every public-surface row in this document is either supported — a documented API that downstream apps and tools may rely on, in the status the row's Status column gives — or, for the one implementation tier, public-for-technical-reasons only (exported but explicitly not a surface to depend on; see below). Supported is not the same as front-porch: a system that ships restore-epoch!, project-egress, adapter hooks, and replace-frame-state! has a large public surface, but only a small slice of it is what a new app developer should ever reach for. The Tier column on every table below records that slice, using a closed vocabulary of eight values:
| Tier | Meaning | Who reaches for it |
|---|---|---|
| front-porch | The tight set a new app developer needs to build a working app — register / dispatch / subscribe, the frame basics, the everyday reads. Loaded by default by the Guide and the skills. | Every app author, day one. |
| advanced | Power-user surfaces that solve real problems but are not first-reach — epoch-listener registration, trace projection, flows internals, the *-twin fn forms, the lower-level lifecycle (destroy-adapter!). Opt-in. |
Experienced authors, library authors, niche cases. |
| tooling | Dev / inspection surfaces consumed by Story, Xray, the pair-MCP servers, and other Spec 009 / Tool-Pair tools — trace listeners, the registrar query API, off-box-egress projections, the epoch query surface, the Story run-result read accessors. Not for application logic. | Tool and instrumentation authors. |
| adapter | The substrate-adapter hooks — the per-substrate adapter Var, the hooks and set-*! seams a substrate adapter implements or installs. |
Adapter authors (Reagent / UIx / custom). |
| testing | Test-only helpers — fixture machinery, assertion helpers, view-tree walkers, the HTTP-stub surfaces. Elided or simply absent from production paths. | Test authors. |
| internal-public | A supported host/tool embedding point: stable, exported, and safe to call, but not an application surface — provided for a specific tool/host integration, not for app logic. The Xray mount-<panel>! / mount-shell! family is the canonical (and sole) instance — a host that builds its own Xray chrome may call them; an app never should. Narrower than it looks: it is not the bucket for every exported helper (those are implementation); it is the small set a host may legitimately embed against. |
Specific host/tool integrations only. |
| implementation | Public-for-technical-reasons only — exported because a sibling namespace, the tool chrome, or a test must reach it across a namespace boundary (ClojureScript has no cheap cross-namespace-private seam), but it is NOT a supported surface: an app or tool MUST NOT depend on it, and it may change or vanish without notice. The per-feature artefact lifecycle/validation/cache helpers (re-frame.schemas, re-frame.ssr, re-frame.routing, re-frame.machines, re-frame.flows), the Ring host-adapter internals, and the Xray panel-leaf Panel reg-views live here. No facade export may sit at this tier — a facade export tiered implementation is annotation rather than removal, and the api-manifest drift-check refuses it (per Conventions §Removing or demoting a facade export); that is why the Story vocabulary is tooling and match-schema-expectations is not on the facade at all. |
Nobody downstream — internal plumbing that merely happens to be public. |
| deprecated | On the way out; a replacement exists. Retained only long enough for callers to migrate. (Pre-alpha currently carries none — removed surfaces live in §Removed / not shipped, not here.) | Nobody new — migrate off. |
The Guide and the skills load FRONT-PORCH only by default. A new app developer reading the Guide, or an AI agent operating through the skills, sees the front-porch tier and nothing else unless they opt in. Advanced, tooling, adapter, testing, and internal-public surfaces are opt-in — reached by explicitly pulling in the relevant chapter, the relevant artefact, or the relevant tool. The implementation tier is never reached for: it is not opt-in, it is off-limits — exported only for technical reasons (see the table) and excluded from the supported surface entirely. The acceptance bar for this taxonomy: a new app developer can read the one-page front-porch list (the union of all front-porch rows below) and never trip over trace projection, epoch-listener registration, adapter hooks, or Xray internals.
Tier vs Status. Tier and Status are orthogonal axes. Status records shipping lineage (v1 / v1 (preserved) / post-v1 lib / …). Tier records who-reaches-for-it. A surface can be v1 and tooling (register-listener!), or post-v1 lib and front-porch (the Story run verb is post-v1 lib/tooling; a hypothetical post-v1 ergonomic core helper would be post-v1 lib/front-porch). Read the two columns together.
The front-porch / back-room split is the first instance, generalised. The multi-frame surface (§View ergonomics, per 002 §The multi-frame surface) is organised as a front-porch / back-room split — dispatch / subscribe / with-frame on the porch, the {:frame …} override in the back room (and capture-frame, the ONE public HOLD primitive, alongside it). The Tier column generalises that split across the whole API: front-porch is the porch; advanced / tooling / adapter / testing / internal-public are the back rooms (all still supported); implementation is below the floorboards — exported but off the supported surface entirely.
Closed vocabulary, restrictive-by-default. The eight values above are the complete set — no ninth tier is added without an explicit governance decision. When a surface is genuinely ambiguous between two tiers, pick the more restrictive one (advanced over front-porch; internal-public over advanced; implementation over internal-public when the var is not actually a host/tool integration point but merely an exported helper) and note the call in the row's Notes. The downstream manifest consumes Tier as a first-class field, so the value must come from this closed set. The internal-public / implementation boundary: ask "is this a surface a host or tool legitimately embeds against?" — if yes, internal-public (the Xray mount family); if it is exported only so the framework's own namespaces / tests can reach it across a boundary, implementation. The split keeps the supported surface honest.
What the Tier column covers. Tier is a property of a public var (a fn / macro / Var). The tables that enumerate keyword-addressed registrations — standard events (:rf.route/navigate, :rf/hydrate, …), standard subs (:rf/route, :rf/response, …), standard fx (:rf.nav/push-url, :rf.http/managed, …), standard cofx, reserved fx-ids, the :fx-entry catalogue, the spec-internal schemas, the configure keys, and the error/trace-event catalogues — are not vars and carry no Tier column. They are part of the contract of whichever artefact owns them; their availability follows that artefact's tier (e.g. the :rf.route/* events ship with the advanced routing artefact). The front-porch one-page list is the union of front-porch-tiered var rows.
Tiering of cross-tool surfaces (Story, Xray, pair-MCP)¶
Story, Xray, and the MCP support namespaces ship their own public-var rows in their own specs (007-Stories.md, tools/xray/spec, Tool-Pair.md); this projection rows only the slices that surface through re-frame.core or the per-feature artefacts. The Tier taxonomy is nonetheless repo-wide and authoritative over those surfaces — the per-tool specs classify against this closed vocabulary rather than inventing local terms. The standing classifications (resolved here, so the per-tool specs reference rather than re-litigate):
- Story facade — every
re-frame.storyexport istooling, with noimplementationcarve-out. The public execution verbsrun/is/explainand the registration macros (reg-story/reg-variant/reg-workspace/reg-tag/reg-decorator/reg-story-panel/reg-fragment/reg-check/reg-mode, each with its*-fn partner) are tooling — a Storybook-shaped dev surface, not application logic. So are the run-result read accessors (run-result,result-status,result-passed?,run-result-schema,valid-run-result?,explain-run-result), the statement-of-record for reading what arunproduced (per story spec 017 §Run result); the variant lifecycle (run-variant/reset-variant/watch-variant/destroy-variant!/render-variant); the registry query, canonical vocabulary, assertion, recorder, fingerprint, run-artifact, determinism and golden-slice families; and the*-idVars for the built-in decorators. Therun-variant/watch-variant/reset-variantvocabulary is notimplementation("public-for-technical-reasons only"), because 007-Stories.md — the owning spec — namesrun-variant/reset-variantamong the API the Story-as-test duality leans on; tools/story/spec/API.md §Facade re-export discipline lists the run/reset/watch/destroy lifecycle, the assertion and recorder facades andvariant-share-urlas user-callable; and the Story tutorials teach(story/run-variant …)directly. Per Conventions §Story / Xray nuance, for a tooling product tooling can legitimately be the front porch — the discriminator is the user's workflow, not the tier label. (There are nois-variant/run-plan/is-planvars.) An export with no such workflow,match-schema-expectations, lives at its owningre-frame.story.resultand not on the facade, rather than being annotated internal. - Xray facade (
day8.re-frame2-xray.core) — every one of its 16 exports istooling, and all 16 are:action :keep. The facade is the canonical entry point most hosts ever touch: the install verb (init!), the mount/visibility verbs (open!/open-overlay!/close!/toggle!/popout!/status), the inspected-frame pair (target-frame/set-target-frame!), the Story→Xray focus door (focus!+valid-focus-panels),load-theme!, and the four highest-traffic config setters (configure!/set-auto-open!/set-editor!/set-egress-profile!). Per Conventions §Story / Xray nuance, for a tooling product tooling can legitimately be the front porch. tools/xray/spec/API.md §Wider public surface draws the porch/workshop line and names the two surfaces deliberately kept OFF the facade — the per-key config setters beyond the four above, and thekeybinding/attach!/detach!lifecycle pair. Unlike the other Xray rows these are:cljs-onlymanifest rows carrying:facade? trueper row (the namespace is not JVM-loadable), and the CLJS probe holds it fully-rowed in both directions — the only Xray namespace held to completeness.load-theme!is the sole export here that mutates, andload-*sits on no row of the §Lifecycle-verb law closed roster; it carries its bang under §Naming bucket 3 alone, with no roster row — the roster names which lifecycle verb a surface takes; §Naming decides whether it carries!; a verb missing from the roster is not a reason to drop the bang.register-listener!takes its bang by the same reading, likewise without a roster row. - Xray
mount-<panel>!family (mount-epoch-panel!,mount-app-db-diff!,mount-trace!,mount-machine-inspector!,mount-routing!, the mastermount-shell!, …) — internal-public, the canonical (and sole) instance of that tier. They are exported and stable, but they are a host-embed surface, not an application or even general-tool surface: a host that builds its own Xray chrome may call amount-<panel>!; an app never should. - Xray panel-leaf
Panelreg-views (day8.re-frame2-xray.panels.<area>/Panel, the Static-modepanel) —implementation. They are exported only so the shell can compose them across namespaces; they are NOT a host-facing single-panel embed surface — a host embeds the full shell viamount-shell!(per 008-Embedding-Contract), never a barePanel. They are notinternal-public(that tier is reserved for the supported mount-embed surface), so the leaves do not read as a supported embed API. - Xray panel-helper functions (the per-panel render/projection helpers beneath the mount-fns) — tooling where they are a documented panel-author surface, otherwise unrowed-internal.
- pair-MCP support namespaces — the trace/egress surfaces they consume (
sensitive?, the registrar query API, the epoch query surface) are tooling, tiered at theirre-frame.core/ artefact home rows below.
Not-rowed internal carve-outs¶
The only internal (unrowed, MUST-NOT-depend-on) carve-outs in re-frame.core are two JVM-only macro-helpers re-exposed purely so tests can reach them:
re-frame.core/expand-reg-view—^:no-doc; the canonical home isre-frame.core-reg-view-macro/expand-reg-view.re-frame.core/parse-reg-view-args—^:no-doc; the canonical home isre-frame.core-reg-view-macro/parse-reg-view-args.
Neither is rowed in this projection. Applications and tools MUST NOT depend on these re-exports; reach the canonical homes directly. These are not a tier — they are below the public surface entirely.
Registration¶
Return value. Every
reg-*row below returns its primary id — the keyword (or path, forreg-app-schema) the caller registered with (its first positional argument). Per Conventions §reg-*return-value convention.
| API | M/Fn | Signature | Status | Tier | Spec | Notes |
|---|---|---|---|---|---|---|
reg-event |
M | (reg-event id ?metadata handler) |
EP-0018 | front-porch | 002 | The ONE public event form — a two-arg (fn [coeffects event-vec] effect-map) handler, coeffects in, a closed seven-key effects map out — #{:db :rf.db/runtime :fx} plus the four commit-plane classification effects #{:sensitive :large :clear-sensitive :clear-large} (see §Effect-map shape) — or nil no-op. The db write is an explicit {:db …} effect; there is no db-only return shape. Coeffects declared uniformly via :rf.cofx/requires. Full-context work is expressed with a registered interceptor (reg-interceptor, referenced by id). Metadata-map superset middle slot carries the reserved :interceptors key (a vector of interceptor refs). There is no reg-event-db/reg-event-fx, and reg-event-ctx is a framework-internal primitive — calling any of those names is a hard error (see §The retired event-registration names + the Removed §). |
reg-sub |
M | (reg-sub id ?metadata computation-fn) |
v1 name; grammar changed to :inputs |
front-porch | 002 | The only sub-registration form in v2. Dependencies are DECLARED under :inputs in the metadata map — a literal vector of query vectors (:static) or a producer fn of the query vector (:parametric) — and declared inputs always reach the body as a VECTOR. See §reg-sub input-production modes. Omitting :inputs is the layer-1 app-db reader. |
reg-fx |
M | (reg-fx id ?metadata handler) |
v1 (preserved + extended) | front-porch | 002 | The handler is binary, context-first: (fn [ctx args]) per 002 §The binary fx-handler signature. ctx is a small map carrying :frame (the active frame id), :event (the originating event vector), and a runtime-internal :envelope; args is the value the event handler placed beside the fx-id in its :fx vector. A unary (fn [args]) handler survives on CLJS only via JS argument-dropping — it is not the blessed contract. :platforms metadata (a set of :server / :client) gates execution by active platform (default universal). |
reg-cofx |
M | (reg-cofx id ?metadata supplier) |
v1 (changed, EP-0017) | front-porch | 001, 002 | Register a coeffect id with a value-returning supplier ((fn [] v) / (fn [arg] v)) and a registration grade — ambient (default) or recordable (:recordable? true, optionally :provided? true). A handler takes delivery by declaring :rf.cofx/requires (the value arrives FLAT under the id; 001 §:rf.cofx/requires). :rf/time-ms is the framework's one provided recordable registration. There is no ctx→ctx handler shape and no public inject-cofx. See §Coeffects. |
reg-interceptor |
M | (reg-interceptor id ?metadata descriptor) |
EP-0022 | front-porch | 001, 002 | The public application-authoring form for an interceptor — a first-class registered program member (registrar kind :interceptor). descriptor is one of {:before f} / {:after f} / {:before f :after f} (static) or {:factory f} (a parameterized family; the factory takes ONE arg and is the mechanism the standard [:rf.interceptor/path …] rides). Event/frame :interceptors chains reference registered interceptors by id (bare keyword) or [id arg], never inline values. Captures source coords; surfaces via handler-meta :interceptor. A migration value carrying an :id is accepted at this boundary only (the id must match). There is no public ->interceptor authoring form. See 001 §Interceptors + 002 §Registered interceptors and the chain grammar. |
make-frame |
Fn | (make-frame opts) / (make-frame opts descriptors) → frame value |
EP-0024 | advanced | 002 | The ONE frame constructor: builds a live frame and returns the frame value (the lifecycle token — the routing ops dispatch / subscribe / frame-provider accept the value directly OR its id, normalizing a value to its id; destroy-frame! accepts either too but is the one lifecycle exception — the value is an exact-incarnation token that tears down only the incarnation it names (a stale value no-ops against a same-id successor), while the id is address-directed, per §Destroy. There is no frame-value->id accessor and no need to unwrap the value). opts is a map (required — a non-map opts, including nil, fails loud with :rf.error/make-frame-bad-opts; the all-defaults frame is (make-frame {})). Accepts BOTH image-selection opts AND record-config opts in one call: :images (a non-empty vector; present ⇒ the selected generation; [] is an error — :rf.error/make-frame-bad-images; absent ⇒ the default image generation over the whole source store + framework standards, failing loud on a cross-namespace same-[kind id] collision — :rf.error/image-duplicate-id; app isolation is named via :select-ns, not absence) / :id (registers in the one frame registry; a duplicate id is idempotent replacement — config + generation refresh, durable state preserved) / :adapter, plus any record-config key (:initial-events — seed app-db via a leading [:rf/set-db {…}] step — :fx-overrides, :platform, :ssr, :doc, :preset, :tags, …) honoured in the same call. (There is no :capabilities image-selection key and there are no image-declared host capabilities; :capabilities flows through as ordinary record-config.) Re-calling make-frame against the SAME :id with a NEW :images vector IS image hot-reload — the generation swaps while durable state is preserved (there is no dedicated reload-images! verb; read the diff via generation-diff). make-frame is the ONE programmatic constructor (tools / tests / SSR / dynamic / image-loaded frames — there is no reg-frame spelling); the day-1 mount recipe is frame-root (ENSURE). (re-frame.frame/make-anon-frame-record! is the internal no-:id record helper.) |
image |
M | (image spec) → normalized, INERT image value — spec carries :id (optional), :select-ns ({:include [globs] :exclude [globs]} provenance selection), :registrations (inline registrar-keyed sections). Pure data — no registrar, no side effect; supplied to make-frame via :images (later image wins; shadows reported on :rf.gen/shadows) |
EP-0023 | advanced | 002 | The one public image constructor. A macro for documentation elision only — literal inline :registrations :doc bytes DCE in production (001 §Production elision contract) — that delegates to the runtime value constructor and stamps no source coordinates. The cross-host contract is the value (Conventions §The public rf/image source keys). |
generation-diff |
Fn | (generation-diff before after) → {:added #{[kind id] …} :changed #{…} :removed #{…} :retained #{…}} — a PURE diff between two sealed image generations (read via frame-generation before/after a re-make-frame reload). |
EP-0023 | tooling | 002 | The reload report — a read over two generation values, not a bespoke verb. |
reg-view |
M | (reg-view sym [args] body+) / (reg-view sym docstring [args] body+) / (reg-view ^{:rf/id :explicit/id} sym [args] body+) |
v1 | front-porch | 001, 002 | Defn-shape; auto-defs the symbol; auto-derives id from (keyword *ns* sym); auto-injects dispatch / subscribe as lexical bindings; rejects non-defn-shape bodies at macroexpand. |
reg-view* |
Fn | (reg-view* id render-fn) / (reg-view* id metadata render-fn) |
v1 | advanced | 001, 002 | Plain-fn surface beneath reg-view. No auto-def, no auto-inject, no compile check. Use for computed ids, library-generated views, Reagent Form-3 (create-class), or registration without a Var. The * follows Clojure's let/let*, fn/fn* idiom (per Conventions). |
reg-machine |
M | (reg-machine machine-id machine-spec) / (reg-machine machine-id opts machine-spec) |
v1 | advanced | 005 | Optional re-frame.machines artefact. Walks the literal spec form at expansion time; co-locates per-element source on each :guards / :actions entry + a reference-site :source-coords on each :states-tree map node. Top-level call-site coords land on handler-meta. The optional opts metadata map is the canonical Spec 001 MIDDLE slot; it carries an event-vector :schema (the :where :event boundary on the dispatched outer vector) — the machine + event-vector-schema shape. |
defmachine |
M | (defmachine name spec) / (defmachine name docstring spec) |
v1 | advanced | 005 | def-shape for the def-then-register pattern. Walks the literal spec at the definition site, stamping per-element source onto the def'd value so it travels into a later (reg-machine id name). Does not register. |
reg-app-schema |
M | (reg-app-schema path schema) / (reg-app-schema path metadata schema) — the schema is the positional value slot; the optional middle metadata map carries the :frame target (e.g. (reg-app-schema [:user] {:frame :session} UserSchema)) |
v1 | advanced | 010 | Optional re-frame.schemas artefact. Path is the registration id. App-db schemas are path-keyed and live in the schemas artefact's per-frame side-table (app-db schemas are NOT a registrar kind). Every other reg-* is keyword-id-keyed; here the first arg is the path vector (e.g. [:user]) and (app-schemas {:frame f}) / (app-schema-meta {:frame f :path [:user]}) look up by the same vector. The path-as-id asymmetry is principled (paths are first-class in get-in / assoc-in grain — schemas-at-paths matches the dataflow grain), not accidental; otherwise reg-app-schema is an ordinary family member — the schema is the positional value slot uniform with the rest of the reg-* family. A non-map middle metadata arg (the 3-slot form) is a loud :rf.error/app-schema-bad-metadata. Per Conventions §reg-* return-value rule. |
reg-app-schemas |
M | (reg-app-schemas {path-1 schema-1, path-2 schema-2, ...}) / (reg-app-schemas {…} opts) — bulk plural form for feature-modular apps that register 5–20 paths against the same prefix (per Conventions §Feature-modularity prefix convention). The bulk {path -> schema} shape needs no positional slot (the map value IS the schema — no positional ambiguity in a bulk map); the plural form exists for its all-or-nothing atomic-batch contract (every path validated before any mutation — a naive doseq does not preserve it). Each entry routes through the singular reg-app-schema positionally and is stamped with this call's source-coords. Returns the vector of paths registered |
v1 | advanced | 010 | |
reg-flow |
M | (reg-flow flow-id metadata derive-fn) |
v1 | advanced | 013 | Optional flows artefact. Per the canonical Spec 001 3-slot grammar: the pure :derive fn is the third VALUE slot; metadata carries :inputs / :output-path (both REQUIRED) plus optional :doc / :schema / the EP-0025 classification keys and the :frame mounting key. A :derive left inside the metadata map is rejected loudly (:rf.error/invalid-flow-metadata). Returns flow-id (per Conventions §reg-* return-value convention). Flows are frame-scoped and single-store: the :flow registrar kind is RESERVED-but-empty; introspection is via the frame-scoped re-frame.flows/flows / flow-meta and the whole-registry flows-snapshot (not handler-meta :flow). Per 013-Flows.md §The registration shape / §Frame-scoping. |
reg-route |
M | (reg-route id metadata path) — canonical 3-slot grammar: the URL :path pattern is the third VALUE slot, metadata the pure reflection map |
v1 | advanced | 012 | Optional routing artefact. A :path left inside the metadata map is a loud :rf.error/route-bad-metadata. |
reg-head |
M | (reg-head id ?metadata head-fn) |
v1 | advanced | 011 | Optional SSR artefact. New registry kind :head; routes name a registered head via :head route metadata. Captures source-coords; under the optional-artefact wrapper convention the surface routes through the :ssr/reg-head late-bind hook. |
reg-error-projector |
M | (reg-error-projector id ?metadata projector-fn) |
v1 | advanced | 011 | Optional SSR artefact. New registry kind :error-projector; named per-frame via the frame's :ssr {:public-error-id ...} config (per make-frame / frame-root). |
reg-sub input-production modes¶
reg-sub supports three input-production modes. Every subscription has an input query-vector producer, and it is declared ONCE — under :inputs in the metadata map, the same slot reg-flow uses. Layer-1 has no producer; a literal :inputs vector is the static producer; an :inputs fn is the query-parametric producer.
| Mode | Form | Meaning |
|---|---|---|
| App-db reader | (reg-sub id computation-fn) |
:inputs OMITTED. No upstream subscriptions. The computation fn receives app-db and the outer query-v. |
| Static inputs | (reg-sub id {:inputs [q1 q2]} computation-fn) |
Inputs are literal query vectors known — and shape-checked — at registration. |
| Parametric inputs | (reg-sub id {:inputs producer-fn} computation-fn) |
Inputs are computed from the outer query-v when a concrete cache entry is materialized. |
Declared inputs always arrive as a vector. At zero, one or many, the computation fn receives [v0 v1 …] in declaration order. Moving a dependency between the literal and the producer form never changes the body, and adding a second input never turns a scalar argument into a vector. An explicit {:inputs []} declares no dependencies and delivers []; OMITTING :inputs is the layer-1 reader, which receives app-db itself. The two are distinct by design.
(rf/reg-sub :cart/by-price {:inputs [[:cart/items]]}
(fn [[items] _] (sort-by :price items)))
(rf/reg-sub :cart/visible {:inputs [[:cart/by-price] [:cart/filter]]}
(fn [[items f] _] (filter f items)))
An :inputs producer fn is a pure function from the outer query-v to a vector of input query vectors. It is not a v1 signal function: it must not call subscribe, deref app-db, dispatch, mutate, or perform IO; it receives only the outer query-v; and it must not return live reactions. It is never executed at registration — only at materialization.
(rf/reg-sub
:article/page
{:inputs (fn [[_ article-id]]
[[:article/by-id article-id]
[:comments/for-article article-id]
[:viewer/current]])}
(fn computation-fn [[article comments viewer] [_ article-id]]
{:id article-id :article article :comments comments
:can-edit? (:edit? viewer)}))
Input grammar. A literal :inputs MUST be a vector, and every element MUST be a query vector (a vector whose first element is a keyword); a producer fn MUST return that same shape:
;; Accepted
[[:article/by-id id] [:viewer/current]] ;; multiple inputs
[[:item/by-id id]] ;; single input — still a vector OF query vectors
[] ;; no inputs (unusual but valid)
;; Rejected
:viewer/current ;; bare keyword
[:article/by-id id] ;; scalar query vector (ambiguous: arg vs two inputs)
[[:article/by-id id] :viewer] ;; mixed vector + bare keyword
{:article [:article/by-id id]} ;; map return
The scalar query-vector rejection is deliberate: [:x :y] is ambiguous at this boundary (one query with argument :y, vs two inputs). The only accepted single-query spelling is [[:x :y]]. No bare keyword shorthand, no map return, no reaction/derefable. A literal :inputs is checked at registration (:rf.error/reg-sub-bad-args) against the same grammar a producer's return is checked against at materialization (:rf.error/sub-input-fn-bad-return) — one grammar, two moments. An explicit {:inputs nil} is refused: nil is not "absent". The literal check is SHAPE-only and never a registry lookup, so {:inputs [[:a]]} may be registered before :a exists. Reach for a producer fn only when the upstream query vectors need values from the outer query-v; a literal vector is exactly a constant producer, and it is the form a tool can read as a static edge. Per 006 §Subscription input producers, 008 §compute-sub algorithm, and Conventions §reg-sub input grammar. Registration-shape and input-return errors signal loudly via :rf.error/reg-sub-bad-args, :rf.error/sub-input-fn-exception, and :rf.error/sub-input-fn-bad-return (catalogued in 009 §Error event catalogue).
:inputs is a vector here, and whether the family should instead take a named map is an open question shared with reg-flow — see 013 §Map-keyed :inputs instead of vector. It will be ruled ONCE, for reg-flow and reg-sub together; until then both ship the vector.
Clearing registrations¶
One verb, kind-keyed. The registrar is ONE map,
(kind, id) → metadata(001 §Registry model), andclearis its inverse in the same grammar the read side already speaks. There are no per-kind names (clear-event,clear-sub,clear-fx,clear-flow,clear-route,clear-http-interceptor,clear-resource,clear-mutation,clear-resource-scope): per-kind names would differ on where the name lives, whether a nilary clear-all exists, how the frame is named, and what comes back.(rf/clear :sub :cart/total) ;; => :cart/total (rf/clear :cofx :now) ;; every registrar kind has an inverse (rf/clear :flow :cart/total) ;; ambient frame (rf/clear :flow :cart/total {:frame :session}) ;; explicit frameEach kind routes to its OWNING lifecycle fn —
:flowvacates its output path and settles dependents,:routeemits:rf.route/cleared, the resources kinds dispose per-frame runtime state — rather than short-cutting to the registrar. Kinds with no owning lifecycle fn (:event:sub:fx:cofx:interceptor:view:head:error-projector) go straight tore-frame.registrar/unregister!, which IS their owner. Returns theidfor every kind. The per-kindclearcolumn is in 001 §Registry model.Opts are EXACT and frame-scoped-only.
{:frame f}is accepted for:flowand:http-interceptorand nothing else, and it must be exactly that — sole key:frame, value a frame-id keyword or a live frame value. A near-miss like{:fram :session}THROWS:rf.error/registrar-clear-bad-requestbefore any frame is resolved, where a tolerant destructure would have cleared the AMBIENT frame's registration silently (Principles §No silent swallow). An unknown kind fails closed the same way, naming the closed set. Omitting opts reaches the owning fn's own ambient arity.There is NO clear-all arity. Bulk clearing is a fixture concern:
re-frame.registrar/clear-kind!andre-frame.test-supportown it, and they are the only callers a nilary arity would have. The cache / bufferclear-*!names (clear-sub-cache!,clear-trace-buffer!…) are a DIFFERENT axis — they clear runtime state rather than registrations — and keep both their names and their bangs.
| API | M/Fn | Signature | Status | Tier |
|---|---|---|---|---|
clear |
Fn | (clear kind id) / (clear kind id {:frame f}) → id. The ONE registrar inverse. kind is the registrar kind set MINUS :frame (a live runtime object, torn down by destroy-frame!) PLUS :http-interceptor (a per-frame side table). Opts accepted for :flow / :http-interceptor only, and EXACT. See the note above. |
v1 (preserved) | advanced |
destroy-frame! |
Fn | (destroy-frame! frame-id) — the normative teardown boundary. A frame value target carries exact-incarnation authority (tears down only the incarnation it names — a stale value no-ops against a same-id successor), while a frame-id keyword is address-directed (tears down whatever incarnation is currently live), per 002 §Destroy. Per-feature artefacts (flows, machines, schemas, SSR, epoch) hang their frame-scoped cleanup off this call; flows release per 013 §Frame-destroy teardown. |
v1 | front-porch |
clear-sub-cache! |
Fn | (clear-sub-cache! frame-id?) |
v1 (preserved) | advanced |
Dispatch and subscribe¶
| API | M/Fn | Signature | Status | Tier | Spec |
|---|---|---|---|---|---|
dispatch |
M/Fn (CLJS) | (dispatch event) / (dispatch event opts) |
v1 (preserved + extended); macro captures call-site for :rf.trace/call-site; on CLJS the same name is ALSO a plain-fn value-alias (Convention A) for HoF / programmatic dispatch with no call-site capture |
front-porch | 002 |
dispatch-sync |
M/Fn (CLJS) | (dispatch-sync event) / (dispatch-sync event opts) |
v1 (preserved + extended); macro captures call-site for :rf.trace/call-site; on CLJS the same name is ALSO a plain-fn value-alias (Convention A) for HoF / programmatic sync dispatch with no call-site capture |
front-porch | 002 |
subscribe |
M/Fn (CLJS) | (subscribe query-v) / (subscribe query-v opts) |
v1 (preserved + extended); opts may carry :frame (mirrors dispatch); macro captures call-site for :rf.trace/call-site; on CLJS the same name is ALSO a plain-fn value-alias (Convention A) for HoF / programmatic subscribe with no call-site capture |
front-porch | 002 |
subscribe-once |
Fn | (subscribe-once query-v) / (subscribe-once query-v opts) → value (subscribe + deref + immediate unsubscribe; one-shot, non-reactive read for handler bodies, REPL — never inside a machine callback: an in-callback ambient read is unrecorded and breaks 005's replay contract; a machine reads facts via payload or a declared recordable cofx) |
v1 (preserved + extended); opts may carry :frame (mirrors subscribe, so an author carrying the opts form over from subscribe is not misbound) |
advanced | 006 |
unsubscribe |
Fn | (unsubscribe query-v) / (unsubscribe frame-id query-v) → nil (decrement the cache ref-count; on the 1 → 0 transition the cache slot is disposed synchronously in-tick — no grace-period timer, per 006 §Reference counting and disposal). Carved out from the Conventions §Tear-down verb axis — the registrar decrement is (clear :sub id), so un- is reserved as the singular form for the sub-cache ref-count decrement. The two are different operations on different state. |
v1 | advanced | 006 |
To read a machine's snapshot, subscribe to the canonical [:rf/machine machine-id] vector (see §Standard registered subs (machines)).
opts map keys: :frame, :fx-overrides, :interceptor-overrides, :trace-id, :source. Envelope shape and semantics: see 002 §Routing: the dispatch envelope.
Canonical event / query-v shape (best practice). [<id>] (trivial), [<id> <single-scalar>] (single-arg), [<id> {<k> <v>}] (multi-arg → single map payload). Variadic [<id> a b c] is tolerated by the runtime for v1-migration and caller convenience; the linter nudges new code toward the map form. Full rationale and cross-refs: Conventions §Canonical event-vector shape.
Standard frame events (registered by core in the regular registrar and the image standard registry, so every frame resolves them):
| Event | What it does | Spec |
|---|---|---|
:rf/set-db |
Replace the whole app-db partition with one map argument — the app-db seed step of :initial-events. A missing / nil / non-map argument, or an extra one, throws :rf.error/set-db-bad-value. |
002 / EP-0027 |
:rf/install-frame-state |
Install a persisted frame-state {:rf.db/app <map>? :rf.db/runtime <map>?}, read earlier with frame-state-value: a present :rf.db/app replaces app-db, each runtime-db subtree a present :rf.db/runtime carries replaces that subtree while the rest are preserved, and restored machines' :after timers are re-armed after the commit. A malformed payload or a resource-runtime subtree throws and surfaces as :rf.error/handler-exception. Per 002 §Installing a persisted frame-state. |
002 |
dispatch-* family taxonomy¶
The dispatch-* family has two sub-shapes that look alike on first read but answer different questions. Both are dispatch operations — the family-prefix is honest — but they sit in different sub-families.
Stamping-pair sub-family (dispatch / dispatch-sync / subscribe).
The pair-shape question is "do you want call-site stamping or not?" In call position the macro form captures :rf.trace/call-site from the surrounding source position so tooling can navigate from a trace event back to the originating expression. On CLJS the same name is ALSO a plain-fn value-alias (Convention A — mirrors reg-event / reg-sub / etc.) for HoF composition ((map dispatch events)) where a macro can't sit inside the higher-order call — no call-site capture on that path. There is no *-suffixed twin (dispatch* / dispatch-sync* / subscribe*); a JVM programmatic caller reaches the owning ns fn directly (re-frame.router/dispatch! / -dispatch-sync!, re-frame.subs/subscribe). Both the macro path and the value-alias route through the same underlying dispatcher; only the trace stamping differs.
View ergonomics¶
The multi-frame surface is organised by intent, not mechanism (a front-porch / back-room split — per 002 §The multi-frame surface):
- Single-frame (no frames in play):
dispatch,dispatch-sync,subscribe. - Scope:
with-frame,with-new-frame,frame-provider {:frame …}(SCOPE-only — scope an existing frame into a React subtree; fails loud if absent). (ENSURE is its sibling componentframe-root {:id …}— see below.) - Hold (carry a frame's ops as a value, across async):
capture-frame— the ONE public carry primitive (there is noframe-bound-fn/frame-bound-fn*—capture-frameor an explicit{:frame …}opt expresses the real use cases). - Override: the
{:frame …}opt — first-class explicit routing for tools / tests / SSR / fx handlers. - Reads / lifecycle:
app-db-value,frame-state-value,current-frame-id,destroy-frame!,make-frame,frame-ids,frame-meta(see §Public registrar query API).
| API | M/Fn | Signature | Status | Tier | Spec |
|---|---|---|---|---|---|
frame-provider |
Fn (Reagent component) | SCOPE-only (roots ensure; providers scope). [rf/frame-provider {:frame :todo} & children]: provides an ALREADY-CREATED frame id through React context; :frame accepts a frame-id keyword OR a live frame value; creates / refreshes / destroys nothing; FAILS LOUD when the frame is absent (:rf.error/frame-provider-frame-absent; a value/keyword that is neither raises :rf.error/bad-frame-provider-arg; a nil :frame → :rf.error/no-frame-context). Given an :id (the ENSURE key), FAILS LOUD naming frame-root (:rf.error/frame-provider-given-id). For create-if-absent, use frame-root. |
v1 | front-porch | 002 |
frame-root |
Fn (Reagent component) | ENSURE — a COMMIT-OWNED TWO-PASS boundary. [rf/frame-root {:id :todo :images […] :initial-events [[:rf/set-db {}]]} & children]: creates the frame if absent (via make-frame) in a client useLayoutEffect (NOT during render — first render emits no descendant subtree; a Suspense-aborted render creates + seeds nothing, no ghost frame), REUSES it WITHOUT re-seeding if present (idempotent re-mount / keyed remount preserves durable state and does NOT replay :initial-events; StrictMode-safe), provides its id to descendants; NO destroy-on-unmount. :id required (a missing/non-keyword :id → :rf.error/frame-root-missing-id); a mounted :id/opts change → :rf.error/frame-root-reconfigured; a stray :frame → :rf.error/frame-root-given-frame. True ownership stays make-frame + destroy-frame! in a create-class. |
v1 | front-porch | 002 |
with-frame |
M | (with-frame :keyword body) — pin to an existing frame-id. Vector arg is a compile-time error (use with-new-frame) |
v1 | front-porch | 002 |
with-new-frame |
M | (with-new-frame [sym expr] body) — eval expr, bind sym, run body, destroy frame on exit. Keyword arg is a compile-time error (use with-frame) |
v1 | front-porch | 002 |
capture-frame |
Fn | (capture-frame) or (capture-frame frame-id) → {:frame :dispatch :dispatch-sync :subscribe} — the keystone OPERATION BUNDLE. Captures the frame at CREATION; its ops always target the captured frame and survive async. Read app-db via (app-db-value (:frame h)), not the handle |
v1 | front-porch | 002 |
view |
Fn | (view view-id) → the installed substrate's own mountable head for that registration, or nil when nothing is registered. Not hiccup, and (on CLJS) not the raw render-fn: the head is re-derived against the adapter installed now and memoized, so repeat lookups are reference-stable and React reconciles them as one component type. Reagent: the :contextType-carrying head, mounted as [(rf/view :id) args…]; UIx: a substrate-marked component type, mounted as ($ (rf/view ::row) props); Fresco: the minted boundary itself, mounted the way h/defview documents (inside a body as [head props], from outside via h/as-element) — h/defview's registration is debug-gated, so nil in a release build is the documented answer for a Fresco view. On the JVM the answer is the stored :handler-fn. The lookup form for late-binding a registered view by id. |
v1 | advanced | 001 |
with-frame (pin) and with-new-frame (eval-bind-run-destroy) are documented in 002 §with-frame and with-new-frame. The macros are non-overlapping: each rejects the other's argument shape at compile time, with :recovery pointing the caller at the right sibling.
capture-frame is the keystone affordance — there are no dispatcher / subscriber nouns; it is the single answer to "carry a frame's dispatch/subscribe ops across an async boundary." The handle is locked: a per-call :frame opt MUST NOT override the frame captured at handle creation — the captured frame always wins (per 002 §capture-frame). There is no public frame-bound-fn / frame-bound-fn* — capture-frame (or an explicit {:frame …} opt) expresses the real use cases and is the ONE public carry primitive; the frame-rebinding closure semantics live internally as re-frame.frame/bind-fn for the framework's own reach.
Reagent adapter (Spec 006)¶
Reagent-specific surfaces live in re-frame.adapter.reagent (artefact day8/re-frame2-reagent, the browser default). Reagent is the default substrate; its frame-provider Component is rowed in §View ergonomics and the reg-view macro in §Registration, not here. Apps targeting Reagent :require [re-frame.adapter.reagent :as rf.adapter.reagent] and pass rf.adapter.reagent/adapter to (rf/init! …).
| API | M/Fn | Signature | Status | Tier | Spec |
|---|---|---|---|---|---|
rf.adapter.reagent/adapter |
Var (map) | the 11-key adapter spec map {:kind :make-state-container :read-container :replace-container! :subscribe-container :make-derived-value :render :render-to-string :register-context-provider :flush-render! :dispose-adapter!} (ratom-backed; per Spec 006 §Adapter contract) |
v1 | adapter | 006 |
rf.adapter.reagent/flush-views! |
Fn | (flush-views!) / (flush-views! f) — flush pending Reagent renders synchronously (wraps React's act() for tests); the canonical cross-substrate test-flush hook |
v1 | adapter | 006, 008 |
rf.adapter.reagent/set-hiccup-emitter! |
Fn | (set-hiccup-emitter! f) — install the render-tree → HTML fn for render-to-string (the SSR late-bind seam, published through the :reagent/set-hiccup-emitter! hook) |
v1 | adapter | 006, 011 |
rf.adapter.reagent/client-root |
Fn | (client-root) → an inert handle — no DOM work at allocation, so it is defonce-safe at namespace load and Node-safe. Opaque: hold it, hand it to render! and unmount!, and nothing else (per Spec 006 §The client root) |
v1 | adapter | 006 |
rf.adapter.reagent/render! |
Fn | (render! handle render-tree mount-point) / (render! handle render-tree mount-point opts) → nil — the first call creates the React Root at mount-point and renders into it, or with {:hydrate? true} hydrates the server markup once; every later call updates that SAME Root, so one call is both the boot path and the ^:dev/after-load hook. render-tree is hiccup |
v1 | adapter | 006, 011 |
rf.adapter.reagent/unmount! |
Fn | (unmount! handle) → nil — release the Root and return the handle to inert. Idempotent, and a no-op after rf/destroy-adapter! has already drained it; a later render! mounts afresh |
v1 | adapter | 006 |
UIx adapter (Spec 006)¶
UIx-specific surfaces live in re-frame.adapter.uix (artefact day8/re-frame2-uix) — they are NOT re-exported from re-frame.core because core has no static dependency on the adapter (the dependency direction is adapter → core per Conventions §Adapter shipping convention). Apps targeting UIx :require [re-frame.adapter.uix :as rf.adapter.uix] and call the surfaces directly.
| API | M/Fn | Signature | Status | Tier | Spec |
|---|---|---|---|---|---|
rf.adapter.uix/adapter |
Var (map) | the 11-key adapter spec map {:kind :make-state-container :read-container :replace-container! :subscribe-container :make-derived-value :render :render-to-string :register-context-provider :flush-render! :dispose-adapter!} (6 required + 3 optional + :dispose-adapter! + the :kind discriminator, per Spec 006 §Adapter contract) |
v1 | adapter | 006 |
rf.adapter.uix/use-sub |
Fn (UIx hook) | (use-sub query-v) → the current sub value, re-rendering the caller when it changes; resolves the frame from React context ONLY — the surrounding frame-provider / frame-root, never a with-frame dynamic scope (no boundary above → :rf.error/no-frame-context). (use-sub query-v {:frame target}) pins ONE read to an explicit frame — the same opts form subscribe publishes, target a frame-id keyword or a live frame value, :frame required in that arity. The ONE value-hook name across React function components: re-frame.fresco.native/use-sub is the same operation for an island under Fresco |
v1 | adapter | 006 |
rf.adapter.uix/use-frame |
Fn (UIx hook) | (use-frame) → the frame api for the ambient provider frame — EXACTLY what (rf/capture-frame) returns ({:frame :dispatch :dispatch-sync :subscribe}), capture-frame in hook position; resolves from React context ONLY, exactly as the ambient use-sub does — the surrounding frame-provider / frame-root, never a with-frame dynamic scope (no boundary above → :rf.error/no-frame-context); reference-stable across re-renders for the same resolved frame incarnation — a same-id destroy-and-recreate retargets it, because the bundle is pinned to the incarnation capture-frame ran against and not to the address. No opts, no variants — explicit frames use (rf/capture-frame frame-id). |
v1 | adapter | 002, 006 |
rf.adapter.uix/frame-provider |
Fn (UIx component) | SCOPE-only (roots ensure; providers scope) — ($ rf.adapter.uix/frame-provider {:frame :session} child-1 child-2) provides an existing frame id; creates nothing; fails loud if absent (:rf.error/frame-provider-frame-absent); given an :id (the ENSURE key) fails loud naming frame-root (:rf.error/frame-provider-given-id); idiomatic $ trailing children |
v1 | adapter | 002, 006 |
rf.adapter.uix/frame-root |
Fn (UIx component) | ENSURE — a commit-owned two-pass boundary — ($ rf.adapter.uix/frame-root {:id :session :images […]} child-1 child-2) creates the frame if absent in a client useLayoutEffect (not during render — a discarded render creates nothing), reuses it without re-seeding if present, provides its id to descendants; no destroy-on-unmount; takes make-frame opts; a missing/non-keyword :id → :rf.error/frame-root-missing-id, a stray :frame → :rf.error/frame-root-given-frame; idiomatic $ trailing children |
v1 | adapter | 002, 006 |
rf.adapter.uix/flush-views! |
Fn | (flush-views!) / (flush-views! f) — wraps React's act() for tests |
v1 | adapter | 006, 008 |
rf.adapter.uix/set-hiccup-emitter! |
Fn | (set-hiccup-emitter! f) — install render-tree → HTML fn (parity with the Reagent adapter's late-bind seam) |
v1 | adapter | 006, 011 |
rf.adapter.uix/client-root |
Fn | (client-root) → an inert handle — no DOM work at allocation, so it is defonce-safe at namespace load and Node-safe. Opaque: hold it, hand it to render! and unmount!, and nothing else (per Spec 006 §The client root) |
v1 | adapter | 006 |
rf.adapter.uix/render! |
Fn | (render! handle element mount-point) / (render! handle element mount-point opts) → nil — the first call creates the React Root at mount-point and renders into it, or with {:hydrate? true} hydrates the server markup once; every later call updates that SAME Root. element is a React element built with uix.core/$ — CLJS data raises :rf.error/hiccup-on-element-render-slot, on the first render and every later one alike. No com.pitch/uix.dom dependency: the Root is minted by the shared React spine |
v1 | adapter | 006, 011 |
rf.adapter.uix/unmount! |
Fn | (unmount! handle) → nil — release the Root and return the handle to inert. Idempotent, and a no-op after rf/destroy-adapter! has already drained it; a later render! mounts afresh |
v1 | adapter | 006 |
Per Decision 1 the hook is named use-sub — ONE value-hook name for every React function component, the same spelling re-frame.fresco.native publishes for a Fresco island: the VERB subscribe returns a subscription, the NOUN use-sub returns its value. The adapter publishes no raw useContext frame reader and no source-coord wrapper Var; use-frame answers "which frame am I in" and reg-view* is how a view reaches the :adapter/wrap-view late-bind hook. Per Decision 3 there is no auto-injection — UIx components read via the hook and hold frame ops via use-frame (capture-frame in hook position: (let [{:keys [dispatch]} (use-frame)] …)); capture-frame is THE hold primitive, and reg-view injection / use-frame are its two ergonomic spellings. Per Decision 4 reg-view (the Reagent macro) does NOT cover UIx; UIx users register with rf/reg-view* if they need registry-keyed view addressing.
The shared React Context that backs frame-provider / frame-root lives in re-frame.adapter.context (CLJS-only file in core, factored out per Decision 2) — the Reagent and UIx adapters both consume the same createContext object so a mixed-substrate app's frame-provider chain composes across substrates.
Routing (Spec 012)¶
reg-route is rowed canonically in §Registration.
| API | M/Fn | Signature | Status | Tier | Spec |
|---|---|---|---|---|---|
match-url |
Fn | (match-url url) → {:route-id :params :query :fragment :validation-failed? ?:validation-error} or nil (:validation-error present only on a validation failure) |
v1 | advanced | 012 |
route-url |
Fn | (route-url {:to route-id :params path-params :query query-params :fragment fragment}) → URL string (single address-map arity; strictly address-only — :url / :query-merge / policy / unknown keys reject loud) |
v1 | advanced | 012 |
route-link |
Fn (registered view at :route/link) |
[rf/route-link {:to :route-id :params {...} :query {...} :fragment "..." :prefetch :intent & html-attrs} & children] |
v1 | advanced | 012 |
history-url-strategy |
Var (map) | The DEFAULT :url-strategy — HTML5 History, path-form. re-frame.routing/history-url-strategy, NOT on the re-frame.core façade (routing bundle isolation) |
v1 | advanced | 012 |
hash-url-strategy |
Var (map) | #-prefixed :url-strategy for no-server-rewrite static hosting / secretary-era v1 migrations. re-frame.routing/hash-url-strategy |
v1 | advanced | 012 |
with-base-path |
Fn | (with-base-path strategy base) — STRATEGY COMBINATOR: wraps strategy (either shipped strategy, or a custom one) so a deployment sub-path (e.g. an app served from /realworld/) is stripped off every inbound URL and re-added to every outbound one, at all four egress/ingress consult points. A blank/nil base returns strategy unchanged. re-frame.routing/with-base-path |
v1 | advanced | 012 |
A :url-bound? true frame's browser URL-change listener is installed / removed by the FRAME LIFECYCLE, automatically — creation installs (strategy-aware: popstate for history, hashchange for hash, per :url-strategy), destroy removes. There is no imperative install/remove pair to call — no install-url-listener! / remove-url-listener! / install-history-listener! / remove-history-listener!.
reg-route's routing-owned metadata reserved keys: :doc, :params, :query, :query-defaults, :tags, :parent, :on-match, :can-leave, :can-enter, :scroll, :sensitive, :large. Two cross-feature bare keys are accepted beside them: :head, owned by SSR (011) and always accepted; and :resources, owned by the Resources artefact (016) and accepted only when that artefact publishes its :routing/extra-route-keys hook. The URL :path pattern is the third VALUE slot, not a metadata key. Canonical detail in 012-Routing.md §Reserved route-metadata keys; shape in Spec-Schemas §:rf/route-metadata.
route-link click rules: a plain primary-button click (no modifier keys, no defaultPrevented) calls .preventDefault and dispatches [:rf.route/url-requested {:url <synthesised>}] — one key, because a raw URL IS the address (§The request grammar) and the handler re-derives the route from it. Modifier-key clicks (cmd / ctrl / shift / alt) and auxiliary-button clicks (middle-click) defer to the browser so the native href opens in a new tab. A caller-supplied :on-click runs first; if it calls .preventDefault (or otherwise leaves defaultPrevented true) the framework's interception is skipped. Keys other than :to / :params / :query / :fragment / :prefetch / :on-click pass through to the underlying <a> element. Detailed semantics in 012-Routing.md §Linking from views.
Standard route-related events:
| Event | Notes | Spec |
|---|---|---|
:rf.route/navigate |
Navigate to a registered route. | 012 |
:rf.route/handle-url-change |
URL-change handler for link / popstate / initial load / SSR; the cause rides :rf.route/cause (:link, :popstate, :initial, :ssr are the framework's own feeds); default scroll :top for :link, else :restore. |
012 |
:rf.route/url-requested |
The user clicked a framework-owned link. | 012 |
:rf.route/navigation-blocked |
A :can-leave guard rejected a navigation. |
012 |
:rf.route/entry-denied |
A :can-enter guard rejected navigation into a route. TERMINAL — nothing commits and no pending value is created; dispatched exactly once per attempt, carrying {:destination :target :cause :requested-url :guard}. A framework no-op default handler ships, so denial is safe with no application handler. |
012 |
:rf.route/continue |
User-dispatched event proceeding a blocked navigation. | 012 |
:rf.route/cancel |
User-dispatched event abandoning a blocked navigation. | 012 |
:rf.route/prefetch |
Warm-mode resource-only intent preload: [:rf.route/prefetch {address}] runs a named destination's effective resource plan ownerlessly WITHOUT navigating (no route state, guards, :on-match, or readiness change). |
012 |
:rf.route/replan-resources |
Same-token replan of the ACTIVE route: [:rf.route/replan-resources {:cause <edn>}] reruns its effective resource plan against the current app-db WITHOUT navigating — same address, same nav-token, same owner; kept identities adopted, added ensured with the caller's :cause, dropped released; readiness re-projected. :cause is required. Not a reload; no guards, :on-match, URL or scroll work. |
012 |
Standard route-related subs:
| Sub | Returns | Spec |
|---|---|---|
:rf/route |
The full :rf/route slice {:route-id :params :query :fragment :transition :error :nav-token} |
012 |
:rf.route/id |
Current route id | 012 |
:rf.route/params |
Current path params | 012 |
:rf.route/query |
Current query params | 012 |
:rf.route/transition |
:idle / :loading / :error |
012 |
:rf.route/error |
Current error map (when :transition = :error) |
012 |
:rf.route/fragment |
Current URL fragment (string or nil) | 012 |
:rf.route/chain |
Vector of route ids from parent-most to current (per :parent links) |
012 |
:rf/pending-navigation |
The pending-nav slot (per :rf/pending-navigation schema) when a navigation is blocked; nil otherwise |
012 |
The route and the pending-nav slot are read as ordinary subscription vectors — @(rf/subscribe [:rf/route]) and @(rf/subscribe [:rf/pending-navigation]); the :rf.route/* granular subs above chain off [:rf/route]. There is no named-read-sugar fn: a runtime-db framework read is a subscription vector, one grammar (per Conventions §Reserved sub-ids).
Standard route-related fx (canonical detail in 012-Routing.md):
| Fx | Args | Platforms |
|---|---|---|
:rf.nav/push-url |
URL string | :client |
:rf.nav/replace-url |
URL string | :client |
:rf.nav/scroll |
scroll-spec map | :client |
:rf.nav/capture-scroll |
{:url <leaving-route-url>} |
:client |
:rf.route/with-nav-token |
{:rf/reply-to <reply-target> :value <v> :nav-token <token> :route-id <route-id>} (per 012 §Threading the nav-token) |
universal |
Standard route-related cofx (canonical detail in 012-Routing.md):
| Cofx | Delivers | Spec |
|---|---|---|
:rf.route/nav-token |
The active navigation epoch token (read from [:rf.runtime/routing :current :nav-token]), delivered flat under the coeffect key :rf.route/nav-token — declare via {:rf.cofx/requires [:rf.route/nav-token]} in an :on-match-reached handler to capture the epoch live at scheduling time for stale-result suppression. Per 012 §Navigation tokens. |
012 |
:rf.route/route-id |
The current route id (read from [:rf.runtime/routing :current :route-id]), delivered flat under the coeffect key :rf.route/route-id. The capture-side companion to :rf.route/nav-token: a route loader declares both ({:rf.cofx/requires [:rf.route/nav-token :rf.route/route-id]}) so it captures the two facts the route-loader work id [:rf.work/route route-id nav-token loader-id] needs at scheduling time, rather than reading the route id from the live slice at stale-arrival — where a cross-route completion would read the superseding route's. Per 012 §Navigation tokens. |
012 |
SSR (Spec 011)¶
Namespace: the surfaces below live in
re-frame.ssr(artefactday8/re-frame2-ssr); consumers(:require [re-frame.ssr :as ssr]). The head surface is defined in the siblingre-frame.ssr.head, which consumers may(:require [re-frame.ssr.head :as head])directly —re-frame.ssrre-exportshead-modelandhead-model->htmlfrom it, so the whole read side of the head contract sits besiderender-to-string. Thereg-headandreg-error-projectorREGISTRARS are rowed in §Registration and ride there-frame.corefaçade like every other artefact's registration macro. The Ring host-adapter lives inre-frame.ssr.ring(artefactday8/re-frame2-ssr-ring); its three handler constructors are its entry points and are rowed below, namespace-qualified. The SSR query surface is NOT re-exported throughre-frame.core: loadingre-frame.ssris what installs the SSR runtime — the:rf/hydrateevent, the:rf.ssr/check-*fx, the seven:rf.server/*fx and the:rf.server/requestcofx — so no SSR app can be one that has not named the artefact namespace, and the guided:rf.error/ssr-artefact-missinga façade copy would exist to give would be unreachable. The streaming surface (streaming-render-*) and the Ring host-adapter (re-frame.ssr.ring) are likewise reached at home; an SSR-aware host requires the namespace directly. Apps targeting SSR add the artefacts to their deps regardless. Epoch is the ONE late-bind façade exception.The ssr-node crossing has three host-adapter namespaces, all requires-directly and none re-exported.
re-frame.ssr.ring.node(inday8/re-frame2-ssr-ring) providesrenderer, the one non-local:rendererthe reference ships — the JVM→Node adapter over the bounded sidecar atimplementation/ssr-node.re-frame.ssr.render-state(inday8/re-frame2-ssr) is the render-visible projection the seam runs —project/serialize/deserialize/restore!over the two-partition envelope.re-frame.fresco.server(inday8/re-frame2-fresco, CLJS) providesrender-body, the body-only entry a server bundle calls. Their contracts are 011 §Client-side hydration boot helper; the recipe is Render on Node. These are:implementation-tier host-adapter plumbing on the same footing asre-frame.ssr.ring's own vars other than its three handler constructors, and are not rowed below; all three are carried byspec/api-manifest.ednat that tier — the two JVM-loadable ones by generator introspection, andre-frame.fresco.serverby curated:cljs-onlyrows, since it requiresreact-dom/serverand cannot be loaded on the JVM.
reg-head and reg-error-projector are rowed canonically in §Registration. The head-fn signature is (fn [db route] head-model); the projector-fn signature is (fn [trace-event] :rf/public-error).
| API | M/Fn | Signature | Status | Tier | Spec |
|---|---|---|---|---|---|
render-to-string |
Fn | (render-to-string view-or-hiccup opts) → HTML string |
v1 | advanced | 011 |
emit-ui-tree |
Fn | (emit-ui-tree tree) / (emit-ui-tree tree {:doctype? bool}) → HTML string. Folds an already-rendered version-1 structural tree to HTML — pure, deterministic to the byte, JVM-runnable; it calls no view and resolves no subscription. Validates the root :rf.ui/tree-version first, before any emission: a missing / non-integer / unsupported version throws :rf.error/ssr-ui-tree-version-unsupported ({:got … :supported #{1}}); a malformed node past the gate throws the shared :rf.error/ui-tree-malformed. :doctype? is the only current option — it prefixes <!DOCTYPE html>. Per 004B §The SSR consumption boundary. |
v1 | advanced | 004B |
render-tree-hash |
Fn | (render-tree-hash render-tree) → 32-bit FNV-1a structural hash (lowercase hex). Identical output on JVM and CLJS for the same canonical-EDN representation. Per 011 §Hydration-mismatch detection. |
v1 | advanced | 011 |
project-error |
Fn | (project-error frame-id trace-event) → :rf/public-error. Applies the active error-projector (selected by the frame's :ssr {:public-error-id ...} metadata) for the named frame. Per 011 §Server error projection. |
v1 | advanced | 011 |
head-model |
Fn | (head-model frame-id) / (head-model frame-id {:head-id id :route route}) → :rf/head-model. The ONE head read (there is no render-head / active-head). Selection: an explicit :head-id, else the effective route's :head, else default-head; a selected-but-unregistered id raises :rf.error/no-such-head. The effective route is :route when the key is present (an explicit nil means no route), else the frame's [:rf.runtime/routing :current] slice — and the head fn is evaluated against that SAME route. Re-exported on re-frame.ssr from re-frame.ssr.head. Head rendering is a frame-scoped read, so the frame is carried, not ambient: there is no no-arg form (EP-0002) and a nil frame-id raises :rf.error/no-frame-context rather than resolving against a synthesised default frame. |
v1 | advanced | 011 |
head-model->html |
Fn | (head-model->html head-model) / (head-model->html head-model {:wrap? bool}) → inner-head HTML string. Re-exported on re-frame.ssr from re-frame.ssr.head |
v1 | advanced | 011 |
hydrate! |
Fn | (hydrate! {:frame f :payload p :render-tree-fn f :root-id id}) → the applied payload, or nil on a client-only first load and for a :frame that is not live. The supported client boot: READ the payload (from :payload, else the DOM's __rf_payload) → dispatch-sync [:rf/hydrate …] against :frame → VERIFY via verify-hydration! when :render-tree-fn is supplied. Install is idempotent across a page's roots; a conflicting payload for the same id raises :rf.error/frame-payload-conflict. Per 011 §Client-side hydration boot helper |
v1 | advanced | 011 |
streaming-render-shell |
Fn | (streaming-render-shell root-hiccup) → {:shell-html "…" :continuations [{:id <id> :subtree <hiccup>} …]}. Walks the tree once; at each :rf/suspense-boundary emits a <template …suspense-fallback> placeholder + records a continuation. Per 011 §Streaming SSR — (a). |
v1 | advanced | 011 |
streaming-render-continuation |
Fn | (streaming-render-continuation frame-id entry) → {:id … :html "…" :delta {…} :failed? bool :continuations [{:id <id> :subtree <hiccup>} …]} (:continuations are nested boundaries discovered while draining — [] in the common case). Drains one continuation against frame-id's app-db; snapshots before-db / after-db and computes the per-subtree delta. Catches throws and surfaces the original fallback HTML inline (per 011 §Failure semantics — inline fallback). |
v1 | advanced | 011 |
streaming-build-final-payload |
Fn | (streaming-build-final-payload frame-id render-hash opts) → canonical :rf/hydration-payload. Called after all continuations drain to populate the __rf_payload final chunk. |
v1 | advanced | 011 |
re-frame.ssr.ring/ssr-handler |
Fn | (ssr-handler opts) → a synchronous Ring handler (fn [ring-request] ring-response) rendering one request per call: a per-request frame, the :initial-events drain, the :root-view render in the HTML shell with the hydration payload. Per 011 §HTTP response contract |
v1 | advanced | 011 |
re-frame.ssr.ring/stream-handler |
Fn | (stream-handler opts) → a synchronous Ring handler whose body streams the shell first and each boundary region after it; refuses :html-shell and :renderer. Per 011 §Streaming SSR |
v1 | advanced | 011 |
re-frame.ssr.ring/ssr-middleware |
Fn | (ssr-middleware opts) → Ring middleware (fn [handler] wrapped-handler) that renders the requests its :match? predicate accepts through ssr-handler and passes the rest to the wrapped handler |
v1 | advanced | 011 |
Standard SSR-related events:
| Event | What it does | Spec |
|---|---|---|
:rf/server-init |
Per-request server-side initialisation. Reads request cofx; dispatches setup events. Server-only because it is dispatched only from the per-request frame's :initial-events; :platforms gates effects and coeffects, never events. |
011 |
:rf/hydrate |
Seed the client-side app-db from the server-supplied payload. Runs once on client bootstrap. |
011 |
Standard SSR-related fx (server-only; :platforms #{:server}):
| Fx | Args | Spec |
|---|---|---|
:rf.server/set-status |
:int (per :rf.fx.server/set-status-args) |
011 |
:rf.server/set-header |
{:name :value} (per :rf.fx.server/set-header-args) |
011 |
:rf.server/append-header |
{:name :value} (per :rf.fx.server/append-header-args) |
011 |
:rf.server/set-cookie |
:rf.server/cookie map |
011 |
:rf.server/delete-cookie |
{:name ?:path ?:domain} |
011 |
:rf.server/redirect |
{:location ?:status} (default :status 302); truncates HTML. Caller-trusted :location |
011 |
:rf.server/safe-redirect |
{:location ?:relative-only? ?:allow ?:status} — the caller-untrusted variant; parses :location, rejects javascript: / data: / vbscript: schemes, and enforces :relative-only? / :allow allowlist before setting :redirect. Open-redirect mitigation for attacker-controlled ?next= strings |
011 |
Standard SSR-related fx (client-side hydration compatibility checks; :platforms #{:client}):
| Fx | What it does | Spec |
|---|---|---|
:rf.ssr/check-version |
Hydration-side framework version-compatibility check (emits :rf.ssr/version-mismatch on drift). |
011 |
:rf.ssr/check-schema-digest |
Hydration-side schema-digest compatibility check (emits :rf.ssr/schema-digest-mismatch on drift). |
011 |
Implementation-tier SSR internals (not rowed). The remaining client hydration / streaming lifecycle —
verify-hydration!,read-server-payload,streaming-install!,drain-blocking-resources!, and the per-request response / request accessors (get-response/peek-response/flush-response!/get-request/set-request!/clear-request!) — lives inre-frame.ssrbut is:implementation-tier host-adapter plumbing. The supported client path ishydrate!(rowed above,:advanced); a host that must observe the MOUNTED tree splits it intoread-server-payload+:rf/hydrate+verify-hydration!(011 §Client-side hydration boot helper writes that discriminator). Per the projection's implementation-tier policy (§Tier taxonomy) they are carried by the manifest but not rowed here — the per-request response accumulator (:rf/response) is read through these accessors, not a subscription.
Standard SSR-related subs: there are none. re-frame.ssr and re-frame.ssr.ring register no subscriptions at all — their only registrations are the :rf/hydrate event, the server-only and client-only fx above, and the :rf.server/request coeffect. In particular there is no :rf/head sub and no :rf/public-error sub; @(rf/subscribe [:rf/head]) cannot resolve. Both keywords name a data shape registered in Spec-Schemas (:rf/head-model and :rf/public-error), not a registry entry. Read them through fns instead: the head model via head-model (it RETURNS the model — there is no side-channel register to read back), and the public-error projection via project-error. (docs/api/re-frame.ssr.md §Subscriptions says the same. api_md_check skips keyword rows by design, so no gate checks this statement.)
Standard cofx (server-only):
| Cofx | Returns | Spec |
|---|---|---|
:rf.server/request |
The active HTTP request map | 011 |
reg-fx's :platforms metadata key (a set containing :server and/or :client) gates fx execution by active platform; default #{:server :client} (universal) when the key is absent. Skipped fx emit a :rf.fx/skipped-on-platform trace event. Detail in 011 §:platforms metadata on reg-fx.
SSR error-projection policy is per-frame metadata (see Conventions §Configuration surfaces bucket 3): a frame opts in via the :ssr {:public-error-id ... :dev-error-detail? ...} map on its make-frame / frame-root config. See 011 §Server error projection for the keys.
HTTP requests (Spec 014)¶
:rf.http/managed is the canonical, optional HTTP-request fx — v1 (optional capability). CLJS reference ships it on Fetch (browser) and java.net.http.HttpClient (JVM). Args, behaviours, decode pipeline, retry semantics, abort surface, failure taxonomy, and reply addressing are normatively defined in 014-HTTPRequests.md; the surface below is the API-level summary.
| API | Kind | Signature / shape | Status | Tier | Spec |
|---|---|---|---|---|---|
:rf.http/managed |
fx | [:rf.http/managed args-map] — args per 014 §The args map and :rf.fx/managed-args |
v1 (optional capability) | — (fx-id; follows the advanced HTTP artefact) |
014 |
:rf.http/managed-abort |
fx | [:rf.http/managed-abort request-id] — abort the in-flight request with the given :request-id |
v1 (optional capability) | — (fx-id) | 014 |
:rf.http/managed-canned-success |
fx | [:rf.http/managed-canned-success {:value v :reply-to reply-event}] — synthesises the canonical success reply (per 014 §Testing); optional :meta and :after-ms. The args address the reply as the live fx's do (:reply-to, or :on-success / :on-failure), and the stub refuses args the live fx refuses (:rf.error/http-no-reply-target, :rf.error/http-bad-reply-target). Registered at load of re-frame.http.test-support (NOT re-frame.http.managed); the stub family ships in the same namespace. |
v1 (optional capability, dev/test) | — (fx-id; test) | 014 |
:rf.http/managed-canned-failure |
fx | [:rf.http/managed-canned-failure {:kind <:rf.http/*> :tags {...} :reply-to reply-event}] — synthesises the canonical failure reply; optional :after-ms. Same reply addressing and refusals as the success stub, same registration gate (re-frame.http.test-support) and same co-location with the stub family. |
v1 (optional capability, dev/test) | — (fx-id; test) | 014 |
reg-http-interceptor |
M | (reg-http-interceptor id interceptor-map) — register an HTTP interceptor on a frame's :rf.http/managed middleware chain (per 014 §Middleware). A façade macro (re-frame.core, capturing call-site source-coords; the fn form is re-frame.http.middleware/reg-http-interceptor). id is a keyword; interceptor-map carries at least one of :before (fn [ctx] ctx') and :after (fn [ctx response] response'), plus optional :frame (the EP-0002 override; absent, the carried scope it registers under resolves it — registering under no scope raises :rf.error/no-frame-context, never :rf/default) and any :rf/registration-metadata (:doc / :tags / :schema). The surface mirrors the event-interceptor {:id :before :after} shape — symmetric request/response sides; :before chain in registration order, :after chain in reverse. |
v1 (optional capability) | advanced | 014 |
| — | — | Clearing an HTTP interceptor is (rf/clear :http-interceptor id) / (rf/clear :http-interceptor id {:frame target}), rowed in §Clearing registrations — there is no clear-http-interceptor name. :http-interceptor is one of the two frame-scoped kinds, so the opts map is accepted, and it is exact and fail-closed as that section sets out. Omitting it resolves the frame from the carried scope; under no scope that raises :rf.error/no-frame-context — no :rf/default fallback (EP-0002). A valid explicit target need not name a currently live frame: clearing an absent frame's chain is an idempotent no-op, not an error. The frame-first (frame id) spelling is the artefact-internal clear-http-interceptor* seam, not a public arity. |
— | — | 014 |
Public API surface in re-frame.core for ports that ship Spec 014. Ports that omit it MUST NOT register :rf.http/* for any other purpose (per Conventions §Reserved namespaces).
The HTTP stub family is NOT a re-frame.core façade export — it is test-support infrastructure reached through its home namespace re-frame.http.test-support ((:require [re-frame.http.test-support :as http-test-support])), so it carries no rows above. (with-request-stubs route-map body-fn) is the scoped helper — route-map {[<method> <url>] {:reply ...}}, routed for the dynamic extent of body-fn per 014 §Testing; use the raw install-managed-request-stubs! / uninstall-managed-request-stubs! pair only when stubs must span multiple deftests.
Managed HTTP ships no public var namespace of its own. The request surface is the keyword-addressed [:rf.http/managed args-map] fx and its siblings above; the interceptor surface reaches users through the re-frame.core façade, and the test-stub helpers directly through re-frame.http.test-support. There is no per-verb call-site helper namespace re-frame.http — an app that wants a shorter call site writes its own request-builder fn over the args map (base URL, default headers, a default :decode — policy a per-verb fn cannot carry).
Reply-payload shape¶
Every reply lands as the canonical uniform reply envelope (one dialect, no {:kind :success/:failure} reshape): {:status :ok :value v …} on success, {:status :error :error {:kind <:rf.http/*> …} …} on failure, {:status :cancelled :error {:kind :rf.http/aborted …} …} on abort. Reply addressing is explicit: :reply-to is the unified app-facing spelling — one event vector for both the success and the failure reply, the envelope appended as the last event-vector arg (the app branches on (:status reply)); the same :reply-to key resources / mutations use. :on-success / :on-failure are the split routing sugar — a named target per branch, both receiving the identical envelope. The two styles are exclusive: a map carrying :reply-to beside either branch key raises :rf.error/http-bad-reply-target (:reason :mixed-addressing) at dispatch, on key presence rather than value. All lower to the one internal / normalized :rf/reply-to descriptor (a conformance surface, not an everyday spelling). Omitting all reply targets raises :rf.error/http-no-reply-target at dispatch — the framework never addresses a reply implicitly (there is no co-located default routing the reply back to the originating event), because an implicit target can swallow failures. Detailed in 014 §Reply addressing and §Reply payload shape.
Failure categories (closed set)¶
The eight :kind values inside a failure reply, all reserved under :rf.http/* (per Conventions §Reserved namespaces). See 014 §Failure categories for tags-by-kind:
:kind |
Meaning |
|---|---|
:rf.http/transport |
Network / DNS / connection error pre-HTTP |
:rf.http/cors |
CORS preflight rejected (CLJS-only) |
:rf.http/timeout |
Per-attempt timeout fired |
:rf.http/http-4xx |
Non-2xx 4xx response |
:rf.http/http-5xx |
Non-2xx 5xx response |
:rf.http/decode-failure |
2xx response but decode rejected the body |
:rf.http/accept-failure |
:accept returned {:failure user-map} |
:rf.http/aborted |
Request aborted via :request-id or :abort-signal |
Trace events emitted by :rf.http/managed¶
:operation |
:op-type |
When |
|---|---|---|
:rf.http/retry-attempt |
:info |
Per intermediate attempt that matched :retry :on, plus a terminal retry-sequence stop marker once the sequence ends (budget spent, or a later attempt failed outside :retry :on); carries :request-id, :url, :attempt, :max-attempts, :failure, :next-backoff-ms (nil on the terminal stop marker) |
:rf.http.interceptor/registered |
:info |
A reg-http-interceptor succeeded; carries :frame, :id (per 014 §Middleware) |
:rf.http.interceptor/cleared |
:info |
A (clear :http-interceptor id) removed an existing slot; carries :frame, :id |
:rf.error/http-interceptor-failed |
:error |
An interceptor :before or :after threw; carries :frame, :interceptor-id, :url, :cause (plus :phase :after on the response side). Request side: the request is NOT dispatched; response side: the reply is suppressed (per 014 §Middleware §Failure mode) |
Resources (Spec 016)¶
The Resources artefact (day8/re-frame2-resources, post-v1 optional) ships declarative cached server-state. Its full registration / event / sub / accessor surface is normatively defined in 016-Resources.md; this projection rows the facade exports classified by the standing diff-time rule (every new re-frame.core facade export is classified + justified when it lands). The resource/mutation registrars and the :rf.resource/* / :rf.mutation/* keyword-addressed events, subs, and trace ops follow the artefact's tier (optional capability) and carry no Tier column (they are not vars).
| API | M/Fn | Signature | Status | Tier | Spec | Notes |
|---|---|---|---|---|---|---|
reg-resource |
M | (reg-resource resource-id metadata request-fn) — canonical 3-slot grammar: the :request fetch fn is the third VALUE slot, metadata the reflection + config map (:scope, :params-schema, :data-schema, …) |
post-v1 lib (optional capability) | advanced | 016 | The :resource registrar kind. Late-bound by the Resources artefact. A :request left inside the metadata map is a loud :rf.error/resource-bad-spec. Front-room of the resources surface but advanced (not front-porch): an optional post-v1 capability a new app does not reach for on day one. |
| — | — | (rf/clear :resource resource-id) — rowed in §Clearing registrations; there is no clear-resource name. Registration-lifecycle removal (the registrar decrement per Conventions §Tear-down verb axis); disposes resource-runtime state per 016 §Registration. |
— | — | 016 | |
reg-mutation |
M | (reg-mutation mutation-id metadata request-fn) — canonical 3-slot grammar: the :request write fn is the third VALUE slot, metadata the reflection + config map (:params-schema, :invalidates, …) |
post-v1 lib (optional capability) | advanced | 016 | The :mutation registrar kind — a named causal write. A :request left inside the metadata map is a loud :rf.error/mutation-bad-spec. :rf.mutation/execute carries the call-site :reply-to continuation target. |
| — | — | (rf/clear :mutation mutation-id) — rowed in §Clearing registrations; there is no clear-mutation name. Registration-lifecycle removal of a mutation. |
— | — | 016 | |
reg-resource-scope |
M | (reg-resource-scope scope-id metadata resolve-fn) — register a pure named db-derived scope resolver in the canonical 3-slot grammar: the :resolve fn is the value slot, and metadata carries the declared :inputs {name [:db <rf-path>]} (plus optional :doc). :inputs is REQUIRED and there is ONE arity: the resolver's first arg is ALWAYS the resolved inputs map. Reading the whole db is spelled {:inputs {:db [:db []]}}, from which the tooling-marked explicit-cost :whole-db? is DERIVED. A :resolve inside the metadata map is rejected loudly. The :resource-scope registrar kind. |
post-v1 lib (optional capability) | advanced | 016 | Facade classification (EP-0016 D3): a re-frame.core export of the optional Resources artefact, justified as the single scope-resolution currency reused by resource registration, route resources, event ensure, subscriptions, invalidation descriptors, exact targets, and clear-scope. advanced — an optional-artefact authoring surface, not front-porch. Per 016 §Named resource-scope resolvers. |
| — | — | (rf/clear :resource-scope scope-id) — rowed in §Clearing registrations; there is no clear-resource-scope name. The registrar decrement counterpart of reg-resource-scope (per Conventions §Tear-down verb axis). |
— | — | 016 | |
resolve-resource-scope |
Fn | (resolve-resource-scope db scope-id) — resolve a named scope resolver against a supplied db value; returns the canonical scope or nil. A pure resolver helper, not an effect (no app-state / dispatch side effects) and no observability side effect — it routes through the trace-free pure evaluator, so unlike the causal resolution sites it does not emit :rf.resource/scope-resolved. |
post-v1 lib (optional capability) | advanced | 016 | Facade classification (EP-0016 D3 / issue 7): a re-frame.core export justified as the ergonomic helper for the logout/account-switch idiom — resolve the concrete old scope from the handler's coeffect db (no :snapshot-db payload, which would be an egress-bearing record under EP-0015). A plain function over the resolver registry; no new effect-API surface, no resolution-timing ambiguity. It routes through the trace-free pure evaluator, so — unlike the causal {:from-db …} / route-entry / mutation-settle resolution sites — it does not emit :rf.resource/scope-resolved (a passive read advertised as pure has no observability side effect). Per 016 §clear-scope resolves the concrete scope from the coeffect db. |
| — | — | A registered resource's / mutation's spec map has no per-kind accessor: it is (:rf/resource (rf/handler-meta {:source :store :kind :resource :id id})) and (:rf/mutation (rf/handler-meta {:source :store :kind :mutation :id id})) — the generic registrar query plus the documented inner-key projection, rowed in §Public registrar query API. |
— | — | 016 | |
resource-state |
Fn | (resource-state {:resource … :scope … :params … :frame …}) — a resource instance's live runtime state (explicit frame target) |
v1 (optional capability) | advanced | 016 | |
mutation-state |
Fn | (mutation-state {:instance … :frame …}) — a mutation instance's durable runtime row ({:status :result :error …}), or nil (explicit frame target) |
v1 (optional capability) | advanced | 016 |
A :revalidate-on #{:focus :reconnect} frame's focus/reconnect listeners are installed / reconciled / removed by the FRAME LIFECYCLE, automatically — creation installs exactly the declared subset (:focus covers window focus AND document visibilitychange-to-visible; :reconnect covers window online), re-registration reconciles (replace-don't-stack, and a re-registration that drops the key relinquishes), destroy removes. :revalidate-on is a frame-config key on make-frame / frame-root, not a call: there is no install/remove fn (no install-revalidation-listeners! / remove-revalidation-listeners!). An absent key or an explicit #{} installs nothing; declaring the key without the Resources artefact on the classpath fails loud with :rf.error/resources-artefact-missing. This is the same shape as :url-bound? — see §Routing.
Request decoration (EP-0016 Rider 3) reuses the HTTP facade. Auth/tracing/base-URL decoration for resources and mutations is not a resources surface — it is
reg-http-interceptor/(rf/clear :http-interceptor id)(§HTTP requests), registered once per frame and applied to every:rf.http/managedrequest (resource reads, mutations, plain managed calls). An interceptor decorates the:requestenvelope only; retry is per request, a top-level:retrythe resource or mutation:requestfn returns. There is no decoration facade export; the doctrine is ownership (transport decoration lives in the managed-HTTP seam), per 016 §Request decoration belongs to the managed-HTTP seam.
Effect-map shape¶
Closed: seven top-level keys — #{:db :rf.db/runtime :fx} plus the four EP-0025 commit-plane classification effects #{:sensitive :large :clear-sensitive :clear-large}. Ordinary app handlers return only :db + :fx; :rf.db/runtime is reserved by convention for framework / runtime-extension authority (it writes the runtime-db partition — non-framework handlers emitting it are surfaced by dev diagnostics, not silently dropped). See Spec-Schemas §:rf/effect-map. Top-level :dispatch / :dispatch-later / :dispatch-n from v1 migrate via MIGRATION.md §M-8.
A foreign top-level key REFUSES the event. All seven keys above are commit-plane effects, applied together at the atomic commit boundary; any other top-level key is caught pre-commit at the router's FINAL-effects boundary, emits :rf.error/effect-map-shape on the always-on error channel, and aborts the event — no :db, no :rf.db/runtime, no classification install, no :fx. No partial commit, and nothing is silently dropped.
| Key | Notes |
|---|---|
:db |
New app-db partition (replaces). The app-facing state key. |
:rf.db/runtime |
New runtime-db partition (replaces). Reserved by convention for framework / runtime-extension authority — ordinary app handlers do not emit it. |
:fx |
Vector of [fx-id args] pairs. |
:sensitive |
EP-0025 commit-plane classification. A vector of :rf/path vectors, classified sensitive (durable app-db egress redaction). Applied WITH the :db write into the per-frame elision registry — not routed through :fx. |
:large |
EP-0025 commit-plane classification. A vector of :rf/path vectors, classified large (durable app-db egress size marker). Applied WITH the :db write. |
:clear-sensitive |
EP-0025 commit-plane classification. A vector of :rf/path vectors, un-classified from the sensitive axis (independent of :large). |
:clear-large |
EP-0025 commit-plane classification. A vector of :rf/path vectors, un-classified from the large axis (independent of :sensitive). |
Standard :fx entries:
[fx-id args] |
Args | Status | Spec | Notes |
|---|---|---|---|---|
[:dispatch [event-id ...]] |
event vector | v1 | 002 | |
[:dispatch-later {:ms ms :event event-vec}] |
options map | v1 | 002 | |
[:rf.http/managed args-map] |
args per 014 §The args map | v1 (optional capability) | 014 | Framework-provided when the implementation ships Spec 014. CLJS reference: ships on Fetch + JVM HttpClient. See also :rf.http/managed-abort, :rf.http/managed-canned-success, :rf.http/managed-canned-failure. |
[:rf.nav/push-url url-string] |
URL string | v1 | 012 | |
[:raise event-vec] |
event vector | v1 | 005 | machine-only: reserved fx-id recognised by the machine handler; routes the event back into the same machine, atomic and pre-commit. Outside a machine action's :fx, this fx-id is unbound. |
[:rf.machine/spawn spawn-spec] |
spawn-spec map (per :rf.fx/spawn-args: :machine-id/:definition, :id-prefix, :data, :start) |
v1 | 005 | Canonical actor-lifecycle fx (registered globally by re-frame.machines); installs a new dynamic actor (whose snapshot lives at [:rf.runtime/machines :snapshots <gensym'd-id>]). On the declarative :spawn / :spawn-all path the reducer binds the assigned id into the spawning machine's own :data under :rf/spawned (XState-context parity, per 005 §Recording the spawned id user-side). Emitted from any event handler's :fx (including machine actions and the :spawn desugar). |
[:rf.machine/destroy actor-id] |
actor id (keyword) | v1 | 005 | Canonical actor-destroy fx (registered globally by re-frame.machines); runs the actor's :exit action, dissociates [:rf.runtime/machines :snapshots <actor-id>], and clears the actor's event-handler registration. Symmetric counterpart to :rf.machine/spawn. |
Public registrar query API¶
For tooling, agents, story tools, 10x.
One query map, naming EXACTLY ONE source. Each of
registrations/handler-metatakes exactly one argument: a query map carrying either:source :storeor:frame f, never both and never neither.(rf/registrations {:source :store :kind :event}) ; => {id metadata} or {} (rf/registrations {:frame app-frame :kind :event}) (rf/handler-meta {:source :store :kind :event :id :cart/add}) ; => metadata or nil (rf/handler-meta {:frame app-frame :kind :resource :id :articles/list})Why the source is explicit. A positional arity —
(registrations :event)— would read as the default source store while delegating tore-frame.registrar's generation-aware reads, which consult the dynamic image generation FIRST. That generation is bound around every subscribe build, dispatch, fx and view resolution against an image-loaded frame, so a "store" read issued from inside a sub computation or an event handler would silently read that frame's image instead. A public inspection request has to say which source it means; a bare keyword cannot.Failures. A map carrying BOTH selectors, NEITHER, a
:sourceother than:store, or a non-map argument throws:rf.error/registrar-query-needs-source.:sourceadmits only:storetoday — the key exists so a future source can be added without another arity.:kind :flowor:kind :frame(the reserved-but-EMPTY registrar slots) throws:rf.error/registrar-kind-not-queryable, whose message names the real door —re-frame.flows/flows/flow-meta/flows-snapshot,frame-ids/frame-meta— rather than returning an authoritative-looking{}. Any other non-registry kind throws:rf.error/unknown-registry-kind.Projections are Clojure, not library operations. There is no
:predkey and no predicate arity:(keys (rf/registrations {:source :store :kind :route})), or(into {} (filter (fn [[_ m]] (:rf/machine? m))) (rf/registrations {:source :store :kind :event})).Inner-key projection is the documented contract. A registration that carries a feature SPEC exposes it under a reserved inner key on its own metadata, and reading that key is the supported way to reach it — there is no per-kind
<kind>-metaaccessor. A machine is an:eventregistration carrying:rf/machine? true, and its registered spec reads back as:which is
nilunless that:eventregistration is a machine. See 005 §Querying machines.The complete set of inner keys is one per registrar kind that nests a spec:
Kind Inner key Reads back :event(with:rf/machine? true):rf/machinethe reg-machinespec:resource:rf/resourcethe reg-resourcespec:mutation:rf/mutationthe reg-mutationspec:resource-scope:rf/resource-scopethe reg-resource-scopespecEvery other kind —
:routeamong them — carries its metadata at the TOP level of the registration, sohandler-metaalone is the whole read. Enumeration is likewise generic:(keys (rf/registrations {:source :store :kind :resource})), and so on for every kind.No realm coordinate.
(registrations {:realm r :kind k})and friends do not exist, there is nore-frame.realmnamespace, and frame resolution routes directly through the process registrar (re-frame.registrar/re-frame.frame/re-frame.image).Frame-targeted query. The map-shaped form —
(registrations {:frame f :kind k}),(handler-meta {:frame f :kind k :id id})— resolves the(kind, id)set through live framef's own sealed image generation ("target frame → resolved image generation → registration resolution"), surfacing the:rf.provenance/ns+ inline/image facts the resolved descriptors carry (cross-image overrides are reported on the generation's:rf.gen/shadowsshadow report).:frameis a registered frame id (keyword) OR a frame value (rf/make-frame's return token) — the same target shaperf/frame-generationaccepts; the public routing address is the frame id. This is the READ of the image→frame model: the public tooling surface a tool reaches instead of the internalre-frame.live-frame/re-frame.image-assemblynamespaces. Fail-loud: a:framethat does not resolve to a live frame carrying a generation throws:rf.error/frame-no-generation(no fallback to the default registrar — the read needs a live frame). The dedicated raw readframe-generation(below) returns the whole sealed generation. The{:source :store …}form never touches the generation path — it reads the process source store atom directly, so it answers the same from inside a frame-resolved context as it does from the REPL.
| API | M/Fn | Signature | Status | Tier | JVM-runnable? | Spec |
|---|---|---|---|---|---|---|
registrations |
Fn | (registrations {:source :store :kind k}) / (registrations {:frame f :kind k}) → {id metadata-map}, {} if none. Use when you want metadata — registry walks that read source-coords, :sensitive, :rf/machine?, :platforms, etc. :source :store reads the process source store and never consults a bound image generation. :frame f resolves through live frame f's own sealed image generation (only the ids that frame's image carries, with :rf.provenance/ns provenance facts); f is a frame id or a direct frame object, fail-loud on an unresolvable one. Exactly one selector, else :rf.error/registrar-query-needs-source; filtering is filter over the result (no :pred). See the notes above. |
v1 | tooling | ✓ | 002 |
handler-meta |
Fn | (handler-meta {:source :store :kind k :id id}) / (handler-meta {:frame f :kind k :id id}) → registration-metadata map, or nil. View registrations include source-coord keys (:ns / :line / :column / :file) per :rf/source-coord-meta (Spec-Schemas); pair tools resolve data-rf2-source-coord DOM annotations to :file via this lookup. :source :store reads the process source store and never consults a bound image generation; it is also the form the two DERIVED machine kinds take — (handler-meta {:source :store :kind :machine-guard :id [machine-id guard-id]}). :frame f resolves through live frame f's sealed image generation (with :rf.provenance/ns provenance facts), or nil when that frame's image carries no such [k id]; fail-loud on an unresolvable f. Exactly one selector, else :rf.error/registrar-query-needs-source. See the notes above. |
v1 | tooling | ✓ | 002 |
frame-generation |
Fn | (frame-generation f) → the sealed, resolved image generation live frame f is running — the inert image-assembly value with the four documented stable public keys :rf.gen/resolver ({[kind id] descriptor}), :rf.gen/images (the normalized images in :images order — later wins), :rf.gen/kinds, and :rf.gen/shadows (the cross-image shadow report — [{:registration [kind id] :image <defined-in> :shadowed-by <winner>} …]; [] when nothing was overridden — read the report directly off this key). f is a registered frame id or a direct frame object. The dedicated raw read over the frame→generation model — for describe-image-style views that want the whole generation (selected registrations, shadow report, provenance) without per-kind round-trips; the {:frame f …} form of the pair above gives per-(kind, id) resolution with provenance. Fail-loud :rf.error/frame-no-generation when f does not resolve to a live frame carrying a generation (no nil-as-default, no realm fallback). (There is no :rf.gen/requires capability slot and there are no image-declared host capabilities.) The public tooling surface tools (Pair MCP, Xray) reach instead of re-frame.live-frame / re-frame.image-assembly internals. |
EP-0023 | tooling | ✓ | 002 |
frame-ids |
Fn | (frame-ids) / (frame-ids ns-prefix) |
v1 | tooling | ✓ | 002 |
frame-meta |
Fn | (frame-meta frame-id) |
v1 | tooling | ✓ | 002 |
app-db-value |
Fn | (app-db-value frame-id) → the app-db partition value (plain map) — the out-of-band value read. The front-porch read is subscribe; app-db-value is the non-reactive snapshot read for tools, tests, REPL, and fx/handler bodies. |
v1 | advanced | ✓ | 002 |
frame-state-value |
Fn | (frame-state-value frame-id) → the coherent frame-state projection {:rf.db/app <app-db> :rf.db/runtime <runtime-db>}. The full-frame read for SSR / epoch / time-travel / Xray (EP-0001), and the supported production read for persistence — the value an app saves and installs back with :rf/install-frame-state. The runtime-db-only read (there is no runtime-db-value) is (:rf.db/runtime (frame-state-value frame-id)). |
v1 | tooling | ✓ | 002 |
The static subscription-topology query and the runtime sub-cache snapshot are subscription-tooling surfaces, not re-frame.core facade reads. Reach them through their owning namespace re-frame.subs.tooling — (sub-topology) (static dependency graph over the registrar) and (sub-cache-snapshot frame-id) (live cache state) — with subs/sub-topology / subs/sub-cache-snapshot as JVM aliases.
Schema-introspection accessors — app-schemas, app-schema-meta, app-schemas-digest — are rowed canonically in §Schemas.
compute-sub is rowed canonically in §Testing (pure sub computation against an app-db value).
App values and composition (EP-0013)¶
Not on the public facade. This section is a stable anchor for inbound cross-references; the app/realm/module composition surface is not public.
The composition vocabulary is not on the public facade. The app/realm/module composition surface —
rf/app/rf/module(+ therf/app-registrations/rf/app-requires/rf/app-ownsinspectors),rf/install!/rf/reinstall!,rf/realm/rf/dispose-realm!, and the realm readsrf/realm-ids/rf/frame-realm/rf/installed-app— is not part ofre-frame.core. The public model isimage → frame → event stream: a feature namespace registers ordinaryreg-*forms; anrf/imageselects them by:select-nsprovenance (or defines them inline via:registrations) and is supplied tomake-frame(see §Registration) via:images(the later image wins; the generation's:rf.gen/shadowsreport — read viaframe-generation— names cross-image overrides); re-callingmake-frameagainst the SAME:idwith a new:imagesvector hot-reloads a frame's image generation in place (preserving frame memory; the reload diff is a read —generation-diffover twoframe-generationvalues, not a bespoke verb); and a frame is addressed by its process-local frame id (or a frame value — the routing ops normalize a value to its id, so it is accepted directly), with no realm coordinate. There is nore-frame.realmnamespace, no installed-app value, and no realm coordinate on any wire record (see Spec-Schemas §:rf/realm): frame resolution routes directly through the process registrar (re-frame.registrar/re-frame.frame/re-frame.image), not a realm. The app/realm/module construction model — and the EP-0013→EP-0023 migration mapping — is documented in EP-0023 §Backwards Compatibility and the EP history; it is not a live data surface (there are norf/migration-map/rf/migration-explainfacade reads). For tooling that reads a frame's resolved registrations, use the frame-targeted{:frame f …}registrar queries +frame-generation(§Public registrar query API).
Schemas¶
Namespace: the introspection surfaces below live in
re-frame.schemas(artefactday8/re-frame2-schemas); consumers(:require [re-frame.schemas :as schemas]). They are not re-exported fromre-frame.core— apps targeting schemas add the artefact and require the namespace directly. The registration macros (reg-app-schema/reg-app-schemas) live inre-frame.coreand route through the schemas artefact at registration time. Per the §Conventions per-artefact namespace table.
reg-app-schema is rowed canonically in §Registration.
The
:frameopt is a frame TARGET. Every schema opts surface that names a frame — the:framekey ofreg-app-schema's metadata map, theoptsofreg-app-schemas, and the:frameslot of the readsapp-schemas/app-schema-meta/app-schemas-digest— accepts either a frame-id keyword OR a frame value (rf/make-frame's return token), the same target shapes the registrar query API's:frameaccepts. A frame value is normalized to its frame id (the routing address) before it keys the per-frame schema store, so a schema registered against a frame value is found by a later read-by-id (and vice versa). An explicit:framethat resolves to a non-keyword target (a string, a non-frame map, a vector) fails loud with:rf.error/app-schemas-bad-argrather than silently becoming an unreachable registry key.The READS take ONE map, and
:frameis REQUIRED on it.app-schemas/app-schema-meta/app-schemas-digesteach take a single opts map. There is no ambient-frame default — a schema read is a tooling question ABOUT a named frame, not an operation inside one — and no bare-frame-id or trailing frame-target sugar: a live frame VALUE is itself a map, so a type-sniffing positional argument could never be read locally (Principles §Name over place). A frameless or non-map call raises the catalogued:rf.error/no-frame-contextwith a message naming the{:frame f}spelling; there is no new error id. The frame need not be LIVE — no liveness or image resolution happens on this lane, so a read of a frame holding nothing answers{}/nil/ the empty-set digest. Thereg-*WRITE surfaces keep their own opts sugar; that is a Spec 010 registration contract, not a read.The validator port is a VALUE, reached through one door. To swap the validator / explainer / printer at boot (e.g. drop Malli for a clojure.spec or Zod-style port), use
set-schema-fns!— the single installer, which takes any subset of the three from one map so they never drift mid-boot.schema-fnsreads back what is installed, anddefault-schema-fnsis the framework's own bundle as a value. Those three cover every use: swapping one fn is a one-key install, restoring the framework defaults is(set-schema-fns! default-schema-fns), and capture / stub / restore is alet+finallyover(schema-fns).
| API | M/Fn | Signature | Status | Tier | Spec |
|---|---|---|---|---|---|
app-schemas |
Fn | (app-schemas {:frame frame-id}) → {path registration-metadata} — a frame's WHOLE app-db schema registration map, or {}. The same {id → meta} shape registrations answers for registrar kinds; each value carries :path, :schema, :frame and the source-coords :ns / :line / :file. (update-vals … :schema) projects the schema values alone. |
v1 | tooling | 010 |
app-schema-meta |
Fn | (app-schema-meta {:frame frame-id :path path}) — return the full registration-metadata map (:path, :schema, :frame, plus source-coords :ns / :line / :file and the rest of :rf/registration-metadata) for ONE registered app-db schema, or nil. Both keys are required. Pair-tool and 10x consumers reach for this when they need the registration anchor (e.g. click-back-to-code); (:schema …) is the schema value alone. Per 010 §Schemas as a tooling/agent surface and Spec-Schemas §:rf/app-schema-meta. |
v1 | tooling | 010 |
app-schemas-digest |
Fn | (app-schemas-digest {:frame frame-id}) → string — computed over the {path → schema} projection, so its bytes are independent of the metadata riding alongside. |
v1 | tooling | 010 |
set-schema-fns! |
Fn | (set-schema-fns! {:validate validate-fn :explain explain-fn :print print-fn}) — the one validator-port door. Install any subset of the validator / explainer / printer bundle from one map. Each key is optional; an absent key leaves the existing registration in place, so a one-key call is the way to swap a single fn. The one-call substitute-Malli boot pattern (a Zod / clojure.spec port installs all three together so they never drift mid-boot). :print nil coerces to the default EDN canonicaliser so the digest is never undefined; :validate nil / :explain nil disable that fn. Last-write-wins per key; writes are NOT transactional. Returns the installed bundle as a map {:validate … :explain … :print …} reflecting the live state of all three fns after the call — including keys the call did not touch, so a caller wanting back just the fn it installed selects that key. Also the restore path: install a value read from schema-fns, or default-schema-fns. Per 010 §Default validator and the validator-fn extension point. |
v1 | advanced | 010 |
schema-fns |
Fn | (schema-fns) → {:validate … :explain … :print …} — the READ half of the port. Returns the installed validator / explainer / printer in the same shape set-schema-fns! accepts and returns, so (set-schema-fns! (schema-fns)) is a no-op. :validate / :explain may be nil; :print never is. This is what test isolation is built from — capture, stub, and reinstate in a let + finally, without a dedicated snapshot or restore verb and without reaching the framework-internal atoms. Per 010 §Default validator and the validator-fn extension point. |
v1 | advanced | 010 |
default-schema-fns |
Var (bundle value) | default-schema-fns — the framework's own validator bundle as a plain map carrying exactly :validate / :explain / :print; the state the port holds before an app installs anything. (set-schema-fns! default-schema-fns) restores the framework defaults, and because the value carries the same fn objects the port was seeded with, the :rf.warning/schema-validator-unavailable check for "still on the framework default" answers true again afterwards. Named for what it is — the FRAMEWORK default, not "the Malli bundle": its :print is the EDN canonicaliser rather than anything Malli supplies, and its :validate / :explain soft-pass while the Malli adapter is unloaded. Per 010 §Default validator and the validator-fn extension point. |
v1 | advanced | 010 |
See 010 §Schemas for :schema metadata, validation timing, and dev/prod elision. (The framework does not accept a :spec metadata key on reg-* metadata; the key is :schema. Per MIGRATION §M-54.)
Event-emit (always-on, production-survivable)¶
Per 009 §What IS available in production (#2). A minimal always-on listener surface that survives :advanced + goog.DEBUG=false and delivers one tight record per processed event. Parallel to (not a fallback for) the dev-only trace surface; per-event only — no per-sub, per-fx, or per-:rf.event/db-changed records. Record shape {:event :event-id :frame :time :outcome :elapsed-ms} (the always-on event-emit substrate's own record fields, distinct from trace :tags keys); the :event slot is passed through the size-elision wire-boundary walker once before fan-out, so schema-marked :sensitive? paths land as :rf/redacted and :large? paths land as :rf.size/large-elided. :outcome is one of :ok (clean settle), :error (the interceptor chain threw, or a coeffect supplier or recordable generator threw while the context was assembled, before the chain ran; either way nothing is installed and no :fx runs), :rolled-back (a candidate :db / machine-data transition was refused before install), :flow-error (a flow's :output threw), or :rejected (a :boundary? true handler's :schema refused the event's payload, so the handler never ran) — an aborted dispatch is never reported as a clean :ok. Two of those five behave differently under a release build, and the pair is worth reading together: :rolled-back has no producer in production (candidate validation is dev-only), while :rejected does, because boundary validation is ungated per 010 §Production builds. It is not the only schema check a release build keeps — C-000.35 settles that by what the check is for, not by who declared the schema it reads, and the framework's other load-bearing checks are ungated alongside it — but it is the only one this :outcome vocabulary names in its own right; the others throw on their own paths, and a throw reports :error. See 009 §What IS available in production for the full per-member contract.
This substrate is IMPLEMENTATION tier; the public door is the sink. The registry fans an UNPROJECTED record across EVERY frame and is not routed under any frame's egress policy, so it is not a surface an application registers on:
register-listener!has no:eventsstream (see §Observation listeners). Production observation for hosted back-ends (Datadog / Honeycomb / Sentry / …) is the frame-owned:observability :handled-eventssink (Spec 015 §Frame-owned observability sink policy), declared on the frame'smake-frame/frame-rootconfig — or once per process with(rf/configure! {:observability …})(§Configure keys) — and wired withregister-observability-sink!, which hands the sink an already-PROJECTED record. A cross-frame seat is the process default; a raw record is an explicit:rf.egress/local-rawprofile on that entry.Sensitive data marking is path-based per Spec 015. There is no handler-meta
:sensitive?annotation.
The always-on event-emit registry is addressable via re-frame.event-emit for framework-internal consumers and for tests (which also own between-scenario clears). It carries no public registration verb: an application declares a :handled-events sink instead.
Error-emit (always-on, production-survivable)¶
Per 009 §What IS available in production. Sibling of the event-emit surface above; runs through the always-on error-emit substrate. Survives :advanced + goog.DEBUG=false.
This substrate is IMPLEMENTATION tier; the public door is the sink. The registry fans the record across EVERY frame UNPROJECTED — the
:eventvector is wire-elided, but the:exceptionrides RAW and no frame egress policy is applied, so raw owner-local data can leave a frame here. That fail-open posture is why it is not an application-facing surface:register-listener!has no:errorsstream (see §Observation listeners), and independent corpus observation regardless of a frame's policy is not a public primitive. The off-box error-observation surface is the frame-owned:observability :errorssink (Spec 015 §Frame-owned observability sink policy), declared on the frame'smake-frame/frame-rootconfig — or once per process with(rf/configure! {:observability …})(§Configure keys) — and wired withregister-observability-sink!, which PROJECTS the record under the owning frame's classification + the sink's egress profile (sensitive paths redacted,:exceptiondropped under:rf.egress/public-error) BEFORE the sink sees it.:exceptionwas never the discriminator between the two doors: the sink also delivers:exceptionunder the default:rf.egress/off-box-observabilityprofile — only:rf.egress/public-errordrops it, and then only the record's top-level throwable. The process default carries the two cases no frame's sink can: a FRAMELESS record (:frame nil) and a record whose frame incarnation is already dead both reach(rf/configure! {:observability …})(§Configure keys) under an explicitly nil governing frame, and a dead frame's id still never resolves to a same-id successor's sink. A post-mortem shipper that needs the host throwable + stack declares:rf.egress/local-rawon its entry.
The listener payload is a union of three record shapes: (a) the per-event error record — {:error :event :event-id :frame :time :exception :elapsed-ms} — fanned out by dispatch-on-error! once per catalogued production-reachable per-event runtime :rf.error/* event; (b) the frame-teardown report — {:error :rf.error/frame-teardown-failed :frame :hook-failures :reason :recovery :time} — one bounded record per frame destroy whose best-effort cleanup hooks threw (EP-0008 promotion criterion; see 009 §Observability channels and the promotion criterion); and (c) the six EP-0008-promoted SSR non-event categories (:rf.error/ssr-render-failed, :rf.error/ssr-streaming-writer-failed, :rf.error/malformed-hydration-payload — incl. a pre-frame frameless :frame nil sub-path — :rf.error/ssr-head-resolution-failed, :rf.error/sanitised-on-projection, :rf.error/ssr-ring-error-view-failed), flat union records {:error :frame :time …category keys…}. The two non-event arms (b) and (c) ride the general dispatch-error-record! helper and carry no :event / :event-id; the teardown report carries a :hook-failures vector instead of the per-event :exception / :elapsed-ms slots. Listener bodies MUST branch on (:error record) (or otherwise tolerate a record with no top-level :event / :exception) rather than assuming the per-event shape. The per-event record's :event slot is passed through the size-elision wire-boundary walker once before fan-out, so schema-marked :sensitive? paths land as :rf/redacted and :large? paths land as :rf.size/large-elided. This is the single error-observability surface; recovery is the framework's typed per-category default, not app-steerable (there is no per-frame :on-error recovery policy). Per-listener exceptions are isolated — a buggy listener cannot block siblings or the run.
Sensitive data marking on the error-emit substrate is path-based per Spec 015. There is no handler-meta
:sensitive?annotation — the per-path elision wire-walker is the sole redaction surface on this path.
The always-on error-emit registry is addressable via re-frame.error-emit + the :error-emit/register-error-listener! late-bind hooks for framework-internal consumers (router fan-out, the SSR error projector, the Fresco server's one-render window) and for tests, which also own between-scenario clears. It carries no public registration verb: an application declares an :errors sink instead.
Observation listeners¶
One stream-parameterized listener verb registers an observation callback across the two raw dev observation streams — the differentiator is data (which stream), so it rides in a leading required stream keyword (in place of per-channel register-(trace|epoch)-listener! pairs). The closed stream vocabulary is :trace / :epoch; an unknown stream throws :rf.error/unknown-listener-stream (no bare trace default, no compatibility aliases). Both members are raw and DCE-able, so the vocabulary carries its own tier: this verb means raw dev stream.
Production observation is a different verb. The frame-owned observability sink is NOT a :sink stream here — it is declared as frame policy, or once per process via configure!, and delivers an already-PROJECTED record. This vocabulary has no always-on :events / :errors stream: one would be a second, fail-open production door — unprojected, raw :exception, no frame policy, fanned across every frame. Independent corpus observation regardless of a frame's policy is not a public primitive; the substrates are the implementation-tier registries re-frame.event-emit / re-frame.error-emit, for the framework's own capture sites and for tests.
| Stream | Axis | Record |
|---|---|---|
:trace |
dev-only (DCE'd in production) | one trace event per call |
:epoch |
optional artefact (dev-only) | one :rf/epoch-record per dequeued event; no-op returning nil when day8/re-frame2-epoch is absent |
| API | M/Fn | Signature | Status | Tier | Spec |
|---|---|---|---|---|---|
register-listener! |
Fn | (register-listener! stream id listener-fn) — register listener-fn under id on stream (:trace / :epoch). Re-registering the same id on the same stream replaces. Returns id (or nil on :epoch when the epoch artefact is absent). Unknown stream throws :rf.error/unknown-listener-stream. |
v1 | tooling | 009 |
unregister-listener! |
Fn | (unregister-listener! stream id) → nil. No-op on :epoch when the epoch artefact is absent. |
v1 | tooling | 009 |
There is deliberately no facade clear-listeners! verb: dropping every listener on a stream is a test-isolation concern owned by the fixture layer, not the public facade. re-frame.test-support's reset clears the registries through the lower-level sinks directly (re-frame.trace.tooling/clear-listeners!, re-frame.event-emit/clear-event-listeners!, re-frame.error-emit/clear-error-listeners!, and the :epoch/clear-epoch-listeners! reset hook).
Tracing¶
All tracing is dev-only (elided in production). See 009 §Tracing for emit semantics and synchronous listener delivery. Trace-listener registration uses the stream-parameterized listener verb with the :trace stream.
| API | M/Fn | Signature | Status | Tier | Spec |
|---|---|---|---|---|---|
emit-trace-event! |
Fn | (emit-trace-event! op-type operation tags) → nil |
v1 (dev-only) | tooling | 009 |
re-frame.interop/debug-enabled? |
Var | ^boolean. CLJS: alias of goog.DEBUG — constant-folded by Closure under :advanced, so :advanced + goog.DEBUG=false builds DCE every (when interop/debug-enabled? ...) branch. JVM: a def read ONCE at ns-load from the Java system property -Dre-frame.debug (winning on conflict) or the environment variable RE_FRAME_DEBUG; defaults true (dev parity). Accepts the conventional false-y vocabulary case-insensitively (false, 0, no, off, empty string) with whitespace trimmed; anything else leaves the flag at true. Set BEFORE re-frame.interop loads. SSR / webhook receivers / long-running JVMs facing untrusted input MUST set the gate false explicitly — per 009 §JVM builds and Security §Production gates. |
v1 | tooling | 009 |
re-frame.performance/enabled? |
Var | ^boolean goog-defined (CLJS) / ^:const false (JVM). Set via :closure-defines {re-frame.performance/enabled? true} to bracket event dispatch / sub recompute / fx walk / view render, emitting a single options-bag performance.measure(name, {start, end}) per bracket (User-Timing measure entries rf:event:*, rf:sub:*, rf:fx:*, rf:render:*; no performance.mark entries are allocated). The measure is cleared by name after emit unless the companion re-frame.performance/retain-entries? goog-define (default false) is set — when set, entries persist in the host User-Timing buffer for one-shot getEntriesByType("measure") readers. Compile-time only — not a (rf/configure! ...) knob; runtime mutation has no effect. Default false; under :advanced + default the bracket DCEs and shipped binaries carry zero User-Timing instrumentation. CLJS-only — JVM is a no-op. See 009 §Performance instrumentation and Tool-Pair §Performance API consumption |
v1 | tooling | 009 |
trace-buffer |
Fn | (trace-buffer frame-id) / (trace-buffer frame-id opts) → the named frame's event-keyed ring, oldest-first (event bundles by default; {:flat true} for raw trace events). [] for a destroyed / never-registered frame and in production |
v1 (dev-only) | tooling | 009 |
clear-trace-buffer! |
Fn | (clear-trace-buffer! frame-id) / (clear-trace-buffer!) → nil — empty the named frame's ring, or every frame's. A data clear: retention policy is preserved (the :trace-buffer process default and each frame's explicit :rf.trace/events-retained override both survive) and the hot-reload dedup table is untouched. The fixture-grade reset that also resets policy is re-frame.trace.tooling's concern, not a public verb. No-op for an unknown frame and in production |
v1 (dev-only) | tooling | 009 |
(rf/configure! {:trace-buffer {:events-retained N}}) |
— | See §Configure keys. | v1 (dev-only) | — (configure key) | 009 |
re-frame.trace.projection/group-by-event |
Fn | (group-by-event events) → vector of event bundles {:dispatch-id :event :handler :fx :effects :subs :renders :other}, sorted by emission order. Pure data; JVM-runnable. Not on the facade — require re-frame.trace.projection directly (see 009 §Event-bundle projection). |
v1 (dev-only) | tooling | 009 |
re-frame.trace.projection/domino-bucket |
Fn | (domino-bucket trace-event) → #{:event :handler :fx :effect :sub :render :other}. Classifies a raw trace event into the six-domino slot used by group-by-event. Pure data. Same namespace, not on the facade. |
v1 (dev-only) | tooling | 009 |
Trace-emission opt-out (per-handler metadata)¶
Event-handler registration accepts a :rf.trace/no-emit? true metadata flag. When set, the runtime suppresses every trace emission and event-emit record within the handler's scope — the handler runs invisibly to the trace surface, the event-emit substrate, the frame-owned :handled-events observability sink, and (transitively) the epoch buffer. Used by framework-internal bookkeeping handlers (Xray, Story, re-frame2-pair-mcp, story-mcp) that would otherwise saturate the trace stream. Per Conventions §Reserved namespaces the :rf.trace/* namespace is framework-owned.
| Metadata key | Where | Value | Default | Effect |
|---|---|---|---|---|
:rf.trace/no-emit? |
reg-event metadata map |
boolean | false |
When true, suppresses all trace + event-emit emissions inside the handler's scope, and the :handled-events sink route drops the handler's record. Per 009 §Trace-emission opt-out. |
:rf.trace/frame-no-emit? |
frame config map (make-frame / frame-root) |
boolean | false |
When true, marks the frame a tool / inspector frame: the runtime suppresses every trace emission tagged with that frame, so the inspector's own reactivity does not flood the shared ring it inspects. The frame-scoped sibling of :rf.trace/no-emit?. Per 009 §Frame-level trace-emission opt-out. |
Epoch history (per Tool-Pair)¶
Per-frame epoch snapshots, recorded once per dequeued event (at each event's run-to-completion boundary — not once per drain: a drain that processes a parent event and the :fx [[:dispatch …]] child it queued commits two records) in dev builds. Used by pair-shaped tools for time-travel and post-mortem analysis. Production builds elide entirely.
All rows below are the Tool-Pair time-travel surface — pair-shaped dev tools (Xray, the pair-MCP servers), so they tier tooling (the classification guidance also calls epoch-listener registration "advanced power-user"; either way the surface is back-room and opt-in — never front-porch). The state-injection member is the ONE partial-map mutator replace-frame-state! (there are no per-partition replace-app-db! / reset-app-db! / replace-runtime-db! mutators): a present partition key replaces that partition, an absent key is preserved — a db-shaped key never silently replaces the other partition.
| API | M/Fn | Signature | Status | Tier | Spec |
|---|---|---|---|---|---|
epoch-history |
Fn | (epoch-history frame-id) → vector of epoch records. Returns [] for an unknown / destroyed frame (per Tool-Pair §Surface behaviour against destroyed frames). |
v1 (dev-only) | tooling | Tool-Pair |
restore-epoch! |
Fn | (restore-epoch! frame-id epoch-id) → boolean (true on success). Rewinds to the record's canonical :frame-state-after (BOTH partitions — reviving machines / routes / elision / SSR), not just the app-db projection (EP-0001). Returns false and emits a structured trace on any of its seven failure modes — an unknown / destroyed frame (:rf.error/no-such-handler, kind :frame), plus the six :rf.epoch/restore-* preconditions (drain-in-progress, unknown epoch, non-ok record, schema-digest mismatch, missing handler, machine version mismatch) enumerated in the epoch trace-events table below (per Tool-Pair §Surface behaviour against destroyed frames). |
v1 (dev-only) | tooling | Tool-Pair |
replay-epoch! |
Fn | (replay-epoch! frame-id epoch-id) / (replay-epoch! frame-id epoch-id opts) → structured envelope. Re-drives the named retained epoch's recorded event through the frame's own handlers in ONE call as a strict replay — the raw :trigger-event, the recorded post-generation :rf.cofx under :rf.cofx/mint-policy :strict, and the record's own :fx-overrides / :interceptor-overrides, all resolved in-process (nothing exported or copied by hand). Same frame in and out; no implicit restore; records a new ordinary epoch. {:ok? true … :epoch-id <the new epoch>} on success; {:ok? false :reason …} decided BEFORE dispatch for an unknown / destroyed frame (:rf.error/no-such-handler, kind :frame), a drain in flight (:rf.epoch/replay-during-drain), an unknown / aged-out id (:rf.epoch/replay-unknown-epoch), a halted / synthetic / incomplete record (:rf.epoch/replay-non-replayable-record + :cause), or a recorded :rf/fn-override (:rf.epoch/replay-unreplayable-fx-override + :fx-ids) — refusals ride in the envelope, no trace is emitted; a declared fact absent from the token stays the canonical :rf.error/missing-required-cofx hard error. false when elided / artefact absent (per Tool-Pair §Replay). |
v1 (dev-only) | tooling | Tool-Pair |
replace-frame-state! |
Fn | (replace-frame-state! frame-id frame-state) → boolean — atomically install a PARTIAL frame-state map (any subset of {:rf.db/app … :rf.db/runtime …}); a present key replaces that partition, an absent key is preserved unchanged. The ONE frame-state write surface: an app-only map ({:rf.db/app v}) replaces app-db; {:rf.db/app {}} resets it; a runtime-only map ({:rf.db/runtime v}) replaces runtime-db; a both-key map is the full-frame atomic install for tool-driven replay / fixture install. Records a synthetic epoch. Rejects a map with no recognized partition key, or an unrecognized key, as :rf.error/replace-frame-state-bad-keys (checked before frame resolution). Emits :rf.error/no-such-handler (kind :frame) / returns false for an unknown / destroyed frame. |
v1 (dev-only) | tooling | Tool-Pair |
(rf/configure! {:epoch-history {:depth N}}) |
— | See §Configure keys. | v1 (dev-only) | — (configure key) | Tool-Pair |
epoch-silence-current? |
Fn | (epoch-silence-current? tags) → true when a received :rf.epoch.cb/silenced-on-frame-destroy signal still names a CURRENT fact: the carried :observed-gen is still the generation registered under :cb-id, AND that registration is not observing :frame right now. false otherwise — including an absent/nil :observed-gen, and when the artefact is absent. THE supported receiver decision: pass the signal's :tags map straight back. ONE atomic operation, not two composable reads — registration identity (a same-id replacement or unregister-drop makes a different generation current) and observation continuum (a same-id successor frame re-arms by DELIVERY, which mints no generation, so :observed-gen still matches while the callback is live again) are weighed under a SINGLE ledger snapshot. Composing those two facts from separate reads is not linearizable: a replacement landing between them accepts a silence for an already-superseded registration, an answer no single point in time held — which is why the low-level halves are not published (per Tool-Pair §Surface behaviour against destroyed frames + 009 §The delayed-silence emission linearization law). |
v1 (dev-only) | tooling | Tool-Pair |
app-db-value / frame-state-value (cross-ref to §Public registrar query API) |
Fn | Partition readers — app-db, and the coherent frame-state projection (the runtime-db-only read is (:rf.db/runtime (frame-state-value frame-id))). Each returns nil for an unknown / destroyed frame (per Tool-Pair §Surface behaviour against destroyed frames). |
v1 | advanced/tooling | 002 |
Epoch-settled listeners are the :epoch stream of the stream-parameterized listener verb — (rf/register-listener! :epoch id callback-fn) / (rf/unregister-listener! :epoch id) — not a separate facade fn (there is no per-channel register-epoch-listener! / unregister-epoch-listener! facade pair; the epoch stream registers through the one verb exactly like :trace). The callback is a record-publication notification, not a once-per-event clock: an ordinary handled event publishes one record when it settles, and the SAME record re-publishes — carrying the same :epoch-id — when a post-settle render / sub-run / unmount back-fills into that already-settled epoch; synthetic records publish too (:rf.epoch/db-replaced per replace-frame-state! write and :halted-depth at the depth ceiling, both ring-retained when depth permits, plus the terminal :halted-destroy — an already-started event interrupted by frame destruction, delivered to listeners only and never retained). Because the listener is process-global while :epoch-id is unique only within one frame, reconcile on the pair [(:frame record) (:epoch-id record)] — cache under that key and replace on re-publication rather than counting callbacks; :outcome is record state (:ok / :halted-depth / :halted-destroy), not identity. A dequeued event rejected before it runs (no handler) publishes nothing. id may be any comparable value and re-registering the same id replaces. Process-global; a callback whose previously-observed frame is destroyed receives a one-shot :rf.epoch.cb/silenced-on-frame-destroy trace (per Tool-Pair §Surface behaviour against destroyed frames) carrying :observed-gen — the generation the silence is attributed to. A consumer decides whether a received signal is current with epoch-silence-current? (row above), the one supported receiver operation: it discards a signal owed to a replaced/dropped registration AND one superseded by a fresh delivery on the same registration, from a single ledger snapshot and without reading private registry state. Returns id, or nil when day8/re-frame2-epoch is absent.
Trace events emitted by epoch-history machinery:
:operation |
Tags |
|---|---|
:rf.epoch/snapshotted |
:frame, :rf.epoch/id, :rf.trace/event-id, :outcome (the detailed cause enum :ok / :halted-depth / :halted-destroy; :halted-handler-exception is schema-reserved, not currently emitted) |
:rf.epoch/outcome |
:frame, :rf.epoch/id, :rf.trace/event-id, :outcome (the consumer-facing :ok / :blocked / :error projection; fires paired with :rf.epoch/snapshotted per dequeued event) |
:rf.epoch/restored |
:frame, :rf.epoch/id |
:rf.epoch/db-replaced |
:frame, :rf.epoch/id |
:rf.epoch/restore-unknown-epoch |
:frame, :rf.epoch/id, :history-size |
:rf.epoch/restore-schema-mismatch |
:frame, :rf.epoch/id, :schema-digest-recorded, :schema-digest-current, :failing-paths |
:rf.epoch/restore-missing-handler |
:frame, :rf.epoch/id, :missing |
:rf.epoch/restore-version-mismatch |
:frame, :rf.epoch/id, :machine-id, :version-recorded, :version-current |
:rf.epoch/restore-during-drain |
:frame, :rf.epoch/id |
:rf.epoch/restore-non-ok-record |
:frame, :rf.epoch/id, :rf.epoch/outcome, :halt-reason |
:rf.epoch/replace-during-drain |
:frame |
:rf.epoch/replace-schema-mismatch |
:frame, :failing-paths |
:rf.epoch/replace-history-disabled |
:frame (a replace-frame-state! precondition failure when the history ring is disabled) |
:rf.error/replace-frame-state-bad-keys |
:frame, :reason (:no-recognized-keys / :unknown-keys), :keys (a replace-frame-state! precondition failure — no recognized partition key, or an unrecognized key; checked before frame resolution) |
:rf.epoch.cb/silenced-on-frame-destroy |
:frame, :cb-id, :observed-gen (the reserved callback generation the silence is attributed to; a consumer calls epoch-silence-current? on the tags to discard a superseded signal, per 009 §The delayed-silence emission linearization law) |
:rf.epoch.cb/listener-exception |
:frame, :cb-id, :rf.epoch/id, :message (an :epoch-stream listener callback threw) |
:rf.warning/restore-quiesce-hook-exception |
:category, :hook, :frame, :exception (a restore-time async-quiesce hook threw) |
Size-elision wire-boundary walker¶
Cross-reference: see Security.md §Privacy / secret handling —
re-frame.elision/elide-wire-valueis named there as the single normative emission site for the:rf/redactedsentinel. Every off-box egress (trace forwarders, MCP servers, error monitors) routes throughrf/project-egress, which resolves a named boundary and delegates the per-slot walk to this walker; the trust-boundary surfaces catalogued in Security.md compose against that door.
The framework primitive that walks tree-shaped values at the wire boundary and substitutes elision markers for sensitive or large slots. Reached by every tool that emits wire data (the off-box error-monitor forwarders, the Xray-MCP / re-frame2-pair-mcp / story-mcp servers per Tool-Pair.md, the on-box dev panels) through rf/project-egress, never directly. The walker is the single normative emission site for the :rf/redacted sensitive sentinel and the :rf.size/large-elided marker; per-tool reimplementation is prohibited.
Internal mechanism, not a façade door. re-frame.elision/elide-wire-value carries no re-frame.core re-export and no manifest row. It reads no :rf.egress/profile, so a caller reaching it directly would have to hand-assemble the :rf.egress/* floor that a named boundary already carries — two spellings for one boundary, and the weaker of the two silent. rf/project-egress is the single projection door on the façade. The walker is public at its re-frame.elision home for the framework's own emit-time chokepoints (classification, event / error emit, flows, reply, route-sub egress, the Fresco tool read, and the projector's own slot walk) — every one of which passes {:frame f} and never a profile, because that internal walk is always the maximal floor.
The walker's opts contract — what project-egress resolves a profile into and passes down. (re-frame.elision/elide-wire-value v opts) → v or an elision-marker substitution. opts is a closed map — {:frame <frame-id> :path [...] :query-v [...] :rf.egress/include-sensitive? <bool> :rf.egress/include-large? <bool> :rf.egress/include-digests? <bool> :rf.egress/threshold-bytes <int>} and nothing else. Any other key raises :rf.error/bad-egress-opts naming the offending key(s); in particular a :rf.egress/profile is not accepted here — a profile names a boundary and is resolved by rf/project-egress, which passes the resolved :rf.egress/* opt-set down. Nor are the unqualified include-sensitive? / include-large? spellings: :rf.egress/* is the one egress vocabulary. Closing the map cannot widen egress — in an open map an unknown key would be a silent no-op. The :frame opt is read by key presence, not truthiness: an explicit :frame nil means no governing frame and fails closed; only an ABSENT :frame key falls through to the carried scope. Defaults: both include-* flags false (maximum elision); :rf.egress/threshold-bytes falls back to (rf/configure! {:elision ...}) then 16384. Walks v consulting [:rf.runtime/elision :declarations] and [:rf.runtime/elision :sensitive-declarations] of the named frame's runtime-db; substitutes :rf/redacted for sensitive slots and :rf.size/large-elided markers for large slots. Composition rule (normative): when both predicates match the sensitive drop wins — the size marker is suppressed because it would leak :path / :bytes / :digest. Per 009 §Size elision in traces and Spec-Schemas §:rf/elision-marker.
Commit-plane declaration path (EP-0025). The [:rf.runtime/elision] registry has exactly two slots: :declarations (:large? paths) and :sensitive-declarations (:sensitive? paths). Durable app-db classification rides the four commit-plane classification effects — a reg-event returns :large / :sensitive (or :clear-large / :clear-sensitive) alongside :db, installed by re-frame.elision/apply-classification-effects under :source :effect. A reg-app-schema {:large? true} / {:sensitive? true} slot prop is not a route into this registry: schemas describe shape and validation, not durable app-db egress policy (the schema's :sensitive? still drives schema-validation-failure-trace redaction — the machine [:schemas :data] and resource :params-schema per-slot props validate their owner's value and redact only the validator's own failure trace, and a resource :data-schema is a statically reflected shape fact with no runtime validation consumer). Durable machine and resource :data classification is likewise schema-independent: it rides the subsystem's own projection-relative :sensitive / :large declaration on the reg-machine / reg-resource spec, lowered per instance into the frame's elision registry, per 015 §Subsystem projection-relative classification.. The registry's declaration readers (re-frame.elision/declarations / sensitive-declarations) and the derived-tree value-match arms are reachable through the re-frame.elision home namespace; the user-facing façade exposes only project-egress. Per 015 §Durable app-db — the four commit-plane effects and implementation/core/src/re_frame/elision.cljc L1-16.
Record-level egress projection (EP-0015 / Spec 015)¶
Cross-reference: 015 §Projection is the normative home;
project-egressis the public, record-level boundary primitive layered over the internalre-frame.elision/elide-wire-valuewalker. The six-member:rf.egress/*profile enum is the named-boundary vocabulary; the boolean:rf.egress/*flags are the advanced override layer beneath it (EP-0015 §10/§11).
rf/project-egress is the required projection step before any off-box sink. It dispatches on a record's :kind — the three :rf.observe/* kinds plus :rf/epoch-record — to a private per-kind projector — only project-egress is public (EP-0015 issue 2: the name names the boundary, not a record kind) — and delegates every tree-shaped slot to the internal re-frame.elision/elide-wire-value walker. A profile resolves to a :rf.egress/* opt-set; an explicit :rf.egress/* boolean composes on top (the override wins).
| API | M/Fn | Signature | Status | Tier | Spec |
|---|---|---|---|---|---|
project-egress |
Fn | (project-egress record-or-value) / (project-egress record-or-value opts) → a value safe to ship under the resolved profile. opts conforms to :rf/project-egress-opts, which is closed: {:rf.egress/profile <closed six-member enum> :frame <frame-id> :path [...] :query-v [...] :rf.egress/include-sensitive? <bool> :rf.egress/include-large? <bool> :rf.egress/include-digests? <bool> :rf.egress/threshold-bytes <int> :rf.egress/include-fx-args? <bool> :rf.egress/include-runtime-db? <bool> :rf.egress/include-event-args? <bool>} — the walker's closed key set, the one key this layer owns, and the three trusted-local axes that govern keyspaces only an :rf/epoch-record has (effect :args, the :rf.db/runtime frame-state partition, trigger / trace event args; all default false, and each is stripped before the walker, which knows nothing of them). Those three belong to this door because it owns record-level projection (there is no rf/projected-record), and they share the :rf.egress/* namespace with the rest of the egress opts. An unrecognised key raises :rf.error/bad-egress-opts naming it, on every record kind and on the kindless value path alike. Dispatches on a record's :kind (:rf.observe/handled-event / :rf.observe/error / :rf.observe/derived-tree / :rf/epoch-record) to a private per-kind projector, falling back to walking a kindless input as a tree-shaped value (the direct-read path); delegates tree-shaped slots to the internal re-frame.elision/elide-wire-value walker. The three :rf.observe/* projectors are private to re-frame.projection; :rf/epoch-record's is late-bound (:epoch/project-record) because core may not require the optional epoch artefact — a recognised :rf/epoch-record whose projector is absent raises :rf.error/epoch-artefact-missing naming the kind rather than bare-walking the record (which would start at :path [] and ship declared-sensitive :db-* slots raw). Frame ownership resolves in three steps, each by key presence, not truthiness: (1) an explicit :frame key in opts wins, nil included; (2) else a recognised record's own top-level :frame slot, nil included — every recognised kind is frame-bearing, carrying the frame whose classification governs its tree-shaped slots; (3) else the carried scope. A record is recognised by its :kind, never by a loose shape test, so a bare value that happens to carry a :frame key is a VALUE and seeds nothing. An explicit nil at step 1 or 2 therefore fails closed rather than borrowing the ambient frame. Profile resolves to a :rf.egress/* opt-set; explicit :rf.egress/* booleans compose on top (override wins). Unknown profile raises :rf.error/unknown-egress-profile (closed enum); an unrecognised opts key raises :rf.error/bad-egress-opts. Fail-closed when no frame is known from any of the three steps — an explicit nil included (no :rf/default synthesis). Per 015 §project-egress. |
v1 | tooling | 015 |
Frame-owned observability sinks (EP-0015 §9 / Spec 015)¶
Cross-reference: 015 §Frame-owned observability sink policy is the normative home. The normal production observability story (Datadog / Sentry / Honeycomb): an app declares a sink under a frame's
:observabilityconfig ({:handled-events [{:sink <id> :rf.egress/profile …}] :errors [...]}) and registers the concrete sink fn against that<id>withrf/register-observability-sink!. A sink entry is a closed map —:sinkplus the optional:rf.egress/profile, nothing else; any other key fails loud atmake-framewith the key named, so no slot is accepted and then dropped. Vendor configuration is the registered fn's own (it closes over it); per-frame branching reads the record's:frame. The runtime routes one:rf.observe/handled-eventrecord per processed event and one:rf.observe/errorrecord per:rf.error/*site throughrf/project-egress— under the owning frame's classification and the entry's egress profile (default:rf.egress/off-box-observability) — to the declared sink. Sinks consume already-projected records only (no sink-local redaction). Always-on (survives:advanced+goog.DEBUG=false). This is the only production observation door: the same entry grammar declared once per process with(rf/configure! {:observability …})covers a cross-frame seat and every record whose frame does not resolve. The lower-level corpus-wide event-emit / error-emit registries are implementation tier only — they carry no public registration verb. Routing is fail-closed: the runtime never synthesizes:rf/defaultand never borrows another frame's policy, and a throwing sink is isolated from its siblings.The same policy can be declared once per process with
(rf/configure! {:observability …})— §Configure keys — and precedence is per stream: a frame that declares a stream uses its own entries for it, a frame that omits the stream inherits the process default's, and{:errors []}on a frame is that frame's opt-out (declaration is read by key presence, not truthiness). Exactly ONE source is consulted per record per stream, so a sink id named by both is invoked once. Inheritance moves the sink list, never the redaction authority: an inheriting frame's records are still projected under its own classification. A record with no frame authority — one of the frameless three, or one whose:frameno longer resolves — reaches the process default alone, projected with the governing frame explicitly nil (tree slots:rf/redacted, summary ids intact, a stale:frameid kept as a diagnostic and never re-resolved). With neither a frame policy nor a process default, nothing routes. 015 §The process default is normative.
| API | M/Fn | Signature | Status | Tier | Spec |
|---|---|---|---|---|---|
register-observability-sink! |
Fn | (register-observability-sink! sink-id f) — register the concrete observability sink fn f under the keyword sink-id the frame's :observability {:sink <sink-id> …} entry names. f receives one already-projected :rf.observe/handled-event / :rf.observe/error record (projected under the owning frame's classification + the entry's egress profile); its return value is ignored. Re-registering the same id replaces. Returns sink-id. The framework ships no Datadog / Sentry client (EP-0015 Non-Goals); the sink fn is an app / integration-library concern. Always-on. Per 015 §Frame-owned observability sink policy. |
v1 | tooling | 015 |
unregister-observability-sink! |
Fn | (unregister-observability-sink! sink-id) → nil |
v1 | tooling | 015 |
DOM source-coord annotations (mandatory)¶
Per Spec 006 §Source-coord annotation and Tool-Pair §Source-mapping, every adapter whose host has a DOM-attribute concept MUST inject data-rf2-source-coord="<ns>:<sym>:<line>:<col>" on the rendered root DOM element of each registered view. Format and exemptions (Fragments, non-DOM roots) are documented in Spec 006 §Source-coord annotation. Annotation is gated on interop/debug-enabled? (the CLJS mirror of goog.DEBUG); production :advanced builds elide the attribute via dead-code elimination — there is no DOM-bytes cost in shipped bundles. On the JVM the same annotation is stamped at the reg-view* registration boundary — a debug-gated wrapper on the stored :handler-fn (re-frame.views.jvm-source-coord-annotation), the server-side twin of the CLJS substrate wrappers — not by the SSR emitter, which carries no annotation logic and merely serialises the hiccup the registration boundary already annotated; see Spec 011 §Source-coord annotation under SSR.
Error contract¶
Errors are emitted as structured trace events with :op-type :error (or :warning / :info / :fx / :flow / :frame) and a per-category :operation keyword. The complete normative catalogue — every :rf.error/*, :rf.warning/*, :rf.fx/*, :rf.cofx/*, :rf.ssr/*, :rf.epoch/*, :rf.flow/*, :rf.http/*, :rf.http.interceptor/*, :rf.frame/*, and :rf.route.nav-token/* event the runtime emits — lives at 009 §Error event catalogue (single source of truth for category names, :op-type discriminator, trigger conditions, default :recovery, and :tags payload keys). Per-category Malli :tags schemas are canonicalised at Spec-Schemas §Per-category :tags schemas — one schema per catalogue row.
Categories consumers should be aware of include :rf.ssr/version-mismatch, :rf.ssr/schema-digest-mismatch, :rf.ssr/compatibility-check-skipped (the SSR hydration compatibility-check trio), and :rf.cofx/skipped-on-platform (the platform-gating mirror of :rf.fx/skipped-on-platform). The catalogue at 009 is the single source of truth — do not duplicate the table here.
Per-Spec emit-sites: 002-Frames, 005-StateMachines, 006-ReactiveSubstrate, 010-Schemas, 011-SSR, 012-Routing, 013-Flows, 014-HTTPRequests, Tool-Pair. Each catalogue row's "Per [N]" cross-link names the owning Spec section.
Privacy (Spec 009 §Privacy / sensitive data in traces)¶
Cross-reference: see Security.md §Privacy / secret handling for the framework-wide pattern-level posture; the trust-boundary catalogue lives in Security.md. The cross-artefact inventory + composition order (every privacy surface in
re-frame.core,re-frame.http,re-frame.schemas,re-frame.epoch,tools/mcp-base, with the data-flow from handler exit to off-box wire) lives in Privacy.md. The public classification model is the four commit-plane classification effects (:sensitive/:large/:clear-sensitive/:clear-large, returned by areg-eventalongside:db) for durable app-db (see 015 §Durable app-db — the four commit-plane effects) and registration-owned:sensitivepayload classification onreg-event/reg-sub/reg-flow, projected at trust boundaries byproject-egressand the:rf.egress/*profiles. There is no imperativeadd-marks/set-markspath-marks API, nore-frame.marksnamespace, no frame:sensitive {:app-db …}annotation, and no positionalredact-interceptor; the marks projection substrate lives in the marks-freere-frame.classification/re-frame.elisionengine.
Per Spec 009 §Privacy the runtime stamps :sensitive? true at the top level of every trace event emitted inside the scope of a handler whose declared path overlap classifies sensitivity. (There is no handler-meta :sensitive? annotation; sensitive-data marking is path-based per the data-classification mechanism in Spec 015.) Framework-published trace consumers (Sentry/Honeybadger forwarders, re-frame2-pair server, Xray, Story, story-mcp, re-frame2-pair-mcp) MUST default-drop the stamped events at their egress boundary.
| API | M/Fn | Signature | Status | Tier | Spec |
|---|---|---|---|---|---|
sensitive? |
Fn | (sensitive? trace-event) → boolean. True iff trace-event is a map carrying a truthy :sensitive? stamp at the top level (not under :tags). The framework-published predicate every consumer composes against — Xray, Story and the pair-MCP server call it directly rather than reimplementing the same five-token check. Fail-closed: true is sensitive, false / nil / absent are not, and any OTHER truthy value (a string "true", a keyword :yes, a number) counts as SENSITIVE — the schema types the stamp as a boolean, so a non-boolean is a contract violation, and a (true? …) reading would forward the event in exactly the case where the producer has already proved unreliable. Matches the posture re-frame.mcp-base.sensitive/sensitive-stamp? applies on the MCP wire. |
v1 | tooling | 009 |
Spec-internal schemas¶
Per Spec-Schemas.md, the spec's own runtime shapes are described as Malli schemas registered at runtime. These are the conformance contract an implementation validates against.
| Schema | Describes | Spec |
|---|---|---|
:rf/dispatch-envelope |
Internal envelope wrapping every dispatch | 002 |
:rf/dispatch-opts |
The user-facing opts map for dispatch / dispatch-sync / subscribe |
002 |
:rf/registration-metadata |
Common metadata-map shape across reg-* |
001 / 010 |
:rf/effect-map |
Return value of reg-event handlers — closed, seven keys: #{:db :rf.db/runtime :fx} plus the four EP-0025 commit-plane classification effects #{:sensitive :large :clear-sensitive :clear-large} (:rf.db/runtime reserved by convention for framework authority; app handlers use only :db / :fx day to day) |
002 |
:rf/trace-event |
Universal trace event shape | 009 |
:rf/error-event |
Refinement of :rf/trace-event for :op-type :error / :warning (unified error/warning envelope) |
009 |
:rf/handler-body-dsl |
Conformance corpus handler-body DSL (host-agnostic event/sub bodies; small-DSL grammar) | 008 / Spec-Schemas |
:rf/transition-table |
State-machine transition table grammar | 005 |
:rf/machine-snapshot |
Runtime snapshot of a machine instance | 005 |
:rf/hydration-payload |
Wire format for SSR hydration | 011 |
:rf/response |
HTTP-response accumulator owned by the request frame during SSR | 011 |
:rf.server/cookie |
Structured-cookie shape for :rf.server/set-cookie / :rf.server/delete-cookie |
011 |
:rf/head-model |
SSR head/meta data model (title, meta, link, json-ld, html/body attrs) | 011 |
:rf/public-error |
Sanitised, client-safe projection of an internal error trace event | 011 |
:rf.fx.server/set-status-args / :rf.fx.server/set-header-args / :rf.fx.server/append-header-args / :rf.fx.server/set-cookie-args / :rf.fx.server/delete-cookie-args / :rf.fx.server/redirect-args / :rf.fx.server/safe-redirect-args |
Args of standard :rf.server/* SSR fx |
011 |
:rf/frame-meta |
Returned by (frame-meta frame-id) |
002 |
:rf/variant |
Story-variant artefact contract (post-v1 lib) — variants are data, no fn-valued slots | 007 |
:rf/epoch-record |
Per-frame epoch snapshot record (Tool-Pair) | Tool-Pair |
:rf.fx/dispatch-args |
Args of standard :dispatch fx (and :raise, same shape) |
002 / 005 |
:rf.fx/dispatch-later-args |
Args of standard :dispatch-later fx |
002 |
:rf.fx/http-args |
Args of :http fx (user-owned recommendation) |
Pattern-RemoteData |
:rf.fx.nav/push-url-args |
Args of :rf.nav/push-url fx |
012 |
:rf.fx.nav/replace-url-args |
Args of :rf.nav/replace-url fx |
012 |
:rf.fx.nav/scroll-args |
Args of :rf.nav/scroll fx |
012 |
:rf.fx.nav/capture-scroll-args |
Args of :rf.nav/capture-scroll fx |
012 |
:rf.fx/with-nav-token-args |
Args of :rf.route/with-nav-token fx wrapper |
012 |
:rf.fx/spawn-args |
Args of :rf.machine/spawn fx (the canonical actor-lifecycle fx-id; emitted from any event handler's :fx) |
005 |
:rf.fx/managed-args |
Args of :rf.http/managed fx (request envelope, decode, accept, retry, timeout-ms, on-success/on-failure, request-id, abort-signal) |
014 |
:rf.fx/managed-abort-args |
Args of :rf.http/managed-abort fx (request-id) |
014 |
:rf.http/reply |
Canonical reply envelope {:status :ok :value v …} / {:status :error :error {:kind <:rf.http/*> …} …} / {:status :cancelled :error {:kind :rf.http/aborted …} …} appended to the reply target (:reply-to unified, or :on-success/:on-failure split sugar) |
014 |
:rf/route-rank |
Structural-rank tuple for route-precedence sorting | 012 |
:rf/pending-navigation |
Pending-navigation slot when :can-leave guard rejects |
012 |
:rf/elision-registry |
Per-frame size-elision declaration registry in the reserved runtime-db child [:rf.runtime/elision] |
009 |
:rf/elision-marker |
Wire shape re-frame.elision/elide-wire-value substitutes for an elided large value (:rf.size/large-elided) |
009 |
:rf/project-egress-opts |
The opts map rf/project-egress accepts (:rf.egress/profile + advanced :rf.egress/* overrides) |
015 |
Schemas are open by default (consumers tolerate unknown keys; producers grow shapes additively); :closed true is opt-in at boundary-validation sites and on the effect-map.
Testing¶
The testing surface lives across three namespaces. re-frame.core carries the production primitives that double as testing entry points (make-frame, with-frame, with-new-frame, dispatch-sync, with-fx-overrides, app-db-value, compute-sub); the static sub-graph query sub-topology is not among them — it is subscription tooling, reached through re-frame.subs.tooling (see §Public registrar query API). Pure machine simulation is the one exception: machine-transition is owned by re-frame.machines and is not re-exported from re-frame.core (per the front-porch boundary above). re-frame.test-support ships the test-only fixture machinery and test-flavoured helpers. re-frame.test-helpers ships the view-assertion helpers (hiccup-walk + testid authoring). re-frame.test-support does not re-export from re-frame.core — a test file requires both [re-frame.core :as rf] and [re-frame.test-support :as ts], and additionally [re-frame.test-helpers :as th] for view-assertion tests. See 008-Testing.md for fixtures, framework adapters, and re-frame-test compatibility.
| API | M/Fn | Signature | Status | Tier | Spec | Notes |
|---|---|---|---|---|---|---|
assert-path-equals |
Fn | (assert-path-equals path expected-val) / (assert-path-equals path expected-val opts) |
v1 | testing | 008 | Path-form sync assertion. Mismatch fires clojure.test/is-style failure via do-report. Lives in re-frame.test-support. Mirrors the :rf.assert/path-equals event used inside a Story :script block — same name root so the fn-side and event-side are navigable without a translation table. The wider sibling event family (:rf.assert/sub-equals, :rf.assert/state-is, :rf.assert/dispatched?, :rf.assert/no-warnings, :rf.assert/effect-emitted, :rf.assert/path-matches) lives in 007 §Play functions; runner and reporting channel differ. Choose by test surface: assert-path-equals from a deftest body, :rf.assert/path-equals from a story variant's :script vector. |
poll-until |
Fn | (poll-until pred) / (poll-until pred opts) |
v1 | testing | 008 | Bounded-deadline poll. JVM: synchronous — returns the truthy value, throws ex-info carrying :rf.error/id :rf.error/poll-until-timeout (the canonical discriminator, per Spec 009) on timeout. CLJS: returns a js/Promise resolving with the truthy value or rejecting on timeout. Opts: :timeout-ms (default 2000), :interval-ms (default 5), :label. Lives in re-frame.test-support. |
with-fx-overrides |
M | (with-fx-overrides {fx-id -> override, …} body+) |
v1 | testing | 002, 008 | Lexical-scope :fx-overrides binding. Every dispatch / dispatch-sync inside the body merges the supplied map into its envelope's :fx-overrides. Precedence: per-call opt > lexical with-fx-overrides > per-frame :fx-overrides. Composes with with-frame. Lives in re-frame.core. |
compute-sub |
Fn | (compute-sub query-v db) |
v1 | testing | 008 | Pure sub computation against an app-db value. Lives in re-frame.core. |
snapshot-registrar / restore-registrar! / make-reset-runtime-fixture |
Fn | per docstring | v1 | testing | 008 | Fixture machinery. make-reset-runtime-fixture builds the :each fixture (registrar snapshot/restore + per-process reset); the raw snapshot-registrar / restore-registrar! pair composes hand-rolled fixtures. Lives in re-frame.test-support. |
with-trace-recorder! |
M | (with-trace-recorder! [recs-sym opts?] body+) |
v1 | testing | 008 | Bracket body with a fresh trace-tooling listener that accumulates matching trace events into an atom bound to recs-sym; unregisters in a finally. Opts: :pred, :shape (:flat / :by-op), :key. Lives in re-frame.test-support. |
with-emit-recorder! |
M | (with-emit-recorder! [recs-sym opts?] body+) |
v1 | testing | 008 | The always-on sibling of the row above. Brackets body with a fresh listener on one of the two IMPLEMENTATION-tier always-on registries — re-frame.error-emit (:stream :errors, the default) or re-frame.event-emit (:stream :events) — accumulating records into an atom bound to recs-sym; unregisters in a finally. Opts: :stream, :pred, :key. Those registries carry no public registration verb (register-listener! has no :events / :errors stream); a test is one of the two consumers they serve, and an application observes production records through an :observability sink instead. Lives in re-frame.test-support. |
Testing — view-assertion helpers¶
re-frame.test-helpers ships the hiccup-walk view-assertion surface — call the view-fn directly, walk the returned hiccup, assert on content or invoke a handler. JVM-runnable; no JSDOM, no React, no act(). Pairs with render-to-string (the HTML-string view-test path per Spec 011): hiccup-walk for structure / handler assertions, render-to-string for HTML-markup assertions. Per 008-Testing §View-assertion helpers.
| API | M/Fn | Signature | Status | Tier | Spec | Notes |
|---|---|---|---|---|---|---|
expand-tree |
Fn | (expand-tree tree) → tree |
v1 | testing | 008 | Recursively expand fn-components and Form-3 class components inside a hiccup tree. After expansion every vector's first element is a keyword tag or a non-component value. Lives in re-frame.test-helpers. |
attrs |
Fn | (attrs node) → map? |
v1 | testing | 008 | Return the attrs map of a hiccup node, or nil. Lives in re-frame.test-helpers. |
children |
Fn | (children node) → vector |
v1 | testing | 008 | Return the child elements — everything after the tag (and optional attrs map). Lives in re-frame.test-helpers. |
text-content |
Fn | (text-content node) → string |
v1 | testing | 008 | Recursively collect string leaves under node and join. Numbers coerce to strings; nils are skipped. Lives in re-frame.test-helpers. |
extract-handler |
Fn | (extract-handler node event-key) → fn? |
v1 | testing | 008 | Return the value of event-key from node's attrs map, or nil. Lives in re-frame.test-helpers. |
find-by-attr |
Fn | (find-by-attr tree attr val) → node? |
v1 | testing | 008 | First hiccup node whose attrs map carries attr == val, or nil. Generic over the attribute keyword (:data-testid, :data-test, :id, custom). Lives in re-frame.test-helpers. |
find-all-by-attr |
Fn | (find-all-by-attr tree attr val) → vector |
v1 | testing | 008 | Every matching node, in depth-first order. Lives in re-frame.test-helpers. |
find-by-attr-prefix |
Fn | (find-by-attr-prefix tree attr prefix) → vector |
v1 | testing | 008 | Every node whose attr value (a string) STARTS with prefix. Non-string attr values do not match. Lives in re-frame.test-helpers. |
find-by-testid |
Fn | (find-by-testid tree test-id) → node? |
v1 | testing | 008 | Convenience over find-by-attr keyed on :data-testid. Lives in re-frame.test-helpers. |
find-all-by-testid |
Fn | (find-all-by-testid tree test-id) → vector |
v1 | testing | 008 | Convenience over find-all-by-attr keyed on :data-testid. Lives in re-frame.test-helpers. |
find-by-testid-prefix |
Fn | (find-by-testid-prefix tree prefix) → vector |
v1 | testing | 008 | Convenience over find-by-attr-prefix keyed on :data-testid. Lives in re-frame.test-helpers. |
invoke-handler |
Fn | (invoke-handler node event-key & args) → any |
v1 | testing | 008 | Find the handler under event-key on node and call it with args. Returns the handler's return value. THROWS when node is not a hiccup vector, the node has no attrs map, or no handler is registered — the throwing failure mode is deliberate (a missing handler is almost always a test bug). Lives in re-frame.test-helpers. |
testid |
Fn | (testid id) / (testid id extra) → map |
v1 | testing | 008 | Build an attrs map carrying :data-testid id. The 2-arity merges extra into the map; :data-testid always wins on collision. Authoring helper at the view call site. Lives in re-frame.test-helpers. |
The single-frame view test is a composition, not a bespoke fixture: re-frame.test-support/make-reset-runtime-fixture (an :adapter + optional :init-fn) seats the ambient :rf/default frame and runs the install thunk; the walkers above call the root view fn directly and assert on the returned hiccup; re-frame.test-support/poll-until covers the async case. Per 008-Testing §Single-frame view test.
Standard interceptors¶
Under EP-0022 the public interceptor-authoring surface is reg-interceptor (§Registration), and event/frame :interceptors chains carry interceptor references (a bare keyword id or [id arg]), never inline values. The v2 framework-standard interceptor surface is exactly one interceptor — :rf.interceptor/path — referenced as [:rf.interceptor/path <path-vector>]. There is no public rf/path value constructor and no standard unwrap (EP-0022 §No standard unwrap — handler destructuring, or a project-registered interceptor for intentional chain-wide event reshaping, replaces it). The standard set is path-only because path is coupled to app-db commit no-op semantics and justifies the :factory mechanism; a trivial :before-only helper is not worth a standard slot. Five v1 interceptors are not in v2: debug, trim-v, on-changes, enrich, after (per MIGRATION §M-21).
inject-cofx / inject-cofx* — the v1 coeffect-injection interceptors — are not in v2 (no alias). They are not on the public re-frame.core facade and carry no API-manifest row (a removed surface is not part of the canonical public API): there is no public inject-cofx var to call. Coeffect delivery is not a chain member: a handler declares :rf.cofx/requires on its registration metadata and the value-returning supplier's result arrives flat in the coeffects map (§Registration — reg-cofx, 001 §:rf.cofx/requires). The removal alarm lives internally: a stale call raises the always-on hard error :rf.error/inject-cofx-removed naming :rf.cofx/requires.
| Surface | Shape | Tier | Purpose |
|---|---|---|---|
[:rf.interceptor/path <path-vector>] |
interceptor reference (the one standard interceptor; the canonical :factory consumer) |
front-porch | Focus a handler on an app-db sub-slice: :before stages the focused slice as :db, :after widens the returned slice back into full app-db. Preserves the frame-commit identical? no-op — an unchanged focused slice widens back to the original app-db object, not an assoc-in allocation (002 §Standard :rf.interceptor/path). A non-vector/malformed path arg is :rf.error/path-interceptor-bad-path. |
reg-interceptor |
M (registrar) | front-porch | The public application-authoring form for any non-standard interceptor — analytics, logging, validation, ad-hoc context manipulation. The resulting interceptor is named, addressable, queryable, and referenced by id from chains. Rowed in §Registration. |
The v1 public interceptor-authoring helpers and their v2 replacements:
| v1 API | v2 replacement |
|---|---|
path (public value constructor) |
the standard reference [:rf.interceptor/path <path-vector>] (EP-0022) |
unwrap |
handler destructuring (the M-19 canonical map-payload form), or a project-registered interceptor for intentional chain-wide event reshaping (EP-0022 — no standard unwrap) |
->interceptor (public authoring macro) |
reg-interceptor (the lowering constructor is re-frame.interceptor/->interceptor*, framework-internal) |
debug |
Trace surface (009) + 10x / re-frame-pair |
trim-v |
Canonical map-payload call shape (M-19) |
on-changes |
Flows (Spec 013) |
enrich |
Flows (derived state) / :schema (validation) / a project-registered interceptor (escape hatch) |
after |
Registered fx (:fx [[:my-fx ...]]) for side-effects; a project-registered interceptor for context-shaped work |
reg-flow / (clear :flow id) (Spec 013)¶
reg-flow is rowed canonically in §Registration. Per the canonical 3-slot grammar it is (reg-flow flow-id metadata derive-fn): flow-id first, the pure :derive fn last, and metadata carrying :inputs / :output-path (both REQUIRED) plus optional :doc / :schema / EP-0025 classification keys and the :frame mounting key (which selects the owning frame — the metadata middle slot, like every other 3-slot reg-* surface). The inverse is the kind-keyed (rf/clear :flow id) / (rf/clear :flow id {:frame f}) — :flow is one of the two frame-scoped kinds, so it takes the trailing opts map for the frame override, and that map is EXACT. It is rowed canonically in §Clearing registrations; it deregisters the flow from the named frame and dissoc-ins its :output-path from that frame's app-db only (per Spec 013 §Frame-scoping).
Frame-destroy teardown. destroy-frame! releases every per-frame piece of flow state (the destroyed frame's slot in the per-frame flow registry, its last-inputs dirty-check rows, and its pending abandoned-output-paths) per Spec 013 §Frame-destroy teardown. Under single-store there is no registrar-slot prune (the :flow registrar kind is RESERVED-but-empty, so there is no "last owner" unregister / realign). Sibling frames' state is preserved.
Flow-eval failures in production. A throw inside a flow's :derive fn surfaces as :rf.error/flow-eval-exception on the always-on error-emit substrate, which survives CLJS :advanced + goog.DEBUG=false and routes the record to the owning frame's :observability :errors sinks, or the process default's (§Error-emit). The error is NOT trace-only. Per Spec 013 §Failure semantics rule 4 and 009 §Production builds.
Reserved fx-ids for runtime flow management via :fx:
| Name | Kind | Signature | Status |
|---|---|---|---|
:rf.fx/reg-flow |
Reserved fx-id | [:rf.fx/reg-flow [flow-id metadata derive-fn]] — register a flow at runtime via :fx (the same 3-slot triple reg-flow takes; the dispatching frame threads through as the :frame metadata key) |
v1 (optional capability) |
:rf.fx/clear-flow |
Reserved fx-id | [:rf.fx/clear-flow id] — clear a registered flow via :fx: the effect form of (rf/clear :flow id), with the dispatching frame threaded as :frame |
v1 (optional capability) |
Interceptor / context plumbing¶
The interceptor context accessors get-coeffect / assoc-coeffect / get-effect / assoc-effect are not on the public re-frame.core façade and carry no API-manifest row (a removed surface is not part of the canonical public API). They have no public audience: the intended interceptor model is to author with reg-interceptor and let the :before / :after fns receive and return the context map directly: read coeffects with (get-in ctx [:coeffects k]) and write effects with (assoc-in ctx [:effects k] v). The underlying re-frame.interceptor/{get,assoc}-{coeffect,effect} fns live in their owning namespace as framework-internal context helpers; they are not a public surface. See §Removed / not shipped.
Lifecycle / utility¶
| API | M/Fn | Signature | Status | Tier | Spec |
|---|---|---|---|---|---|
init! |
Fn | (init! adapter-map) — boot; idempotent for the seated adapter (same canonical :rf.adapter/* :kind, or the identical custom map), so a ^:dev/after-load re-call is a no-op. A different adapter raises :rf.error/adapter-already-installed and leaves the seated adapter untouched — destroy-adapter! first to swap. Required arg: the adapter spec map. Each adapter ns exports an adapter Var; consumers require the ns and pass the Var, e.g. (rf/init! rf.adapter.reagent/adapter). Calling (init!) with no args raises a language-level ArityException at compile/load time (there is no no-arg arity, so the missing-adapter mistake surfaces before runtime). Calling (init! nil) or (init! :reagent) raises :rf.error/no-adapter-specified at runtime. Per 006 §Adapter selection at boot. Installs adapters and runtime capabilities only — it creates no frame (EP-0002: no auto :rf/default); the app mounts its frame at the root with frame-root (ENSURE) — or constructs one programmatically with make-frame |
v1 | front-porch | 006 |
destroy-adapter! |
Fn | (destroy-adapter!) — tear down the exact installed adapter generation. This is a one-way terminal boundary: calls the adapter spec's :dispose-adapter! fn (if present), attempts all owned cleanup, finally clears only the generation it claimed, and leaves adapter-disposed? true even when the primary cleanup failure is rethrown. Later cleanup failures remain secondary diagnostic evidence. A fresh adapter may install afterward; stale finalization never clears a replacement. Per Conventions §Tear-down verb axis — destroy- cluster (lifecycle boundary; symmetric with install-adapter! and with destroy-frame!). The adapter-spec map key :dispose-adapter! is an internal contract slot adapters implement, distinct from this verb. |
v1 | advanced | 006 |
current-adapter |
Fn | (current-adapter) → the installed adapter SPEC MAP — the exact value passed to (rf/init! …) — or nil when no adapter is installed. Carries the adapter contract fns (:make-state-container, :replace-container!, :make-derived-value, …) plus a :kind discriminator. ONE read, map-shaped (there is no keyword-returning spelling and no current-adapter-spec twin; a keyword form would be the :kind of this same map). Branch code reads the KEY: (:kind (current-adapter)) → :rf.adapter/reagent / :rf.adapter/reagent-slim / :rf.adapter/uix / :rf.adapter/fresco / :rf.adapter/plain-atom / :rf.adapter/ssr, or nil for a custom map that picked no canonical kind — nothing is synthesised for it (:rf.adapter/freehand / :rf.adapter/ui are reserved and never recycled, and nothing produces them). A PRESENCE check inspects the MAP, never :kind. Per 006 §Adapter introspection. |
v1 | advanced | 006 |
configure! |
Fn | (configure! {key opts, …}) — runtime config from a single nested map; non-map arg fails loud, missing top-level key leaves that subsystem untouched. An unknown bare or rf-namespaced top-level key applies nothing and emits :rf.warning/unknown-configure-key in dev builds (dev-gated, DCE'd in production, :recovery :ignored — observational, never a refusal); an unknown user-namespaced key (e.g. :myapp/thing) passes through silently. Key vocabulary in §Configure keys. One of three orthogonal configuration surfaces per Conventions §Configuration surfaces (configure! for process-level data knobs; set-! / install-! for adapter-pluggable hooks; per-frame metadata for frame-scoped overrides). |
v1 | front-porch | — |
current-config |
Fn | (current-config) → the process-level config values currently in effect, in configure!'s own nested shape — the read twin of configure!, per Conventions §configure! vs current-config. PROCESS values only: no per-frame effective values, no transactional snapshot, and no promise of wire-serialisability. A subsystem key is ABSENT — not nil, not a fabricated default — when its own producer is unavailable, and the two optional keys are independent: :epoch-history is missing without the optional day8/re-frame2-epoch artefact, :trace-buffer is missing from a production bundle that DCEs the dev-only trace-tooling sibling, and neither absence implies the other; (get-in (current-config) [:epoch-history :depth]) reads nil when the epoch artefact is absent, not when trace tooling is. The user-namespaced pass-through carve-out is write-only: configure! accepts :myapp/thing in silence, and current-config does not reflect it back — the vocabulary is closed, so only the keys the runtime READS have live values to report. Key vocabulary in §Configure keys. |
v1 | advanced | — |
Feature inspection¶
re-frame2's optional capabilities ship as separate Maven artefacts (day8/re-frame2-<feature>) whose implementation namespaces core reaches through the late-bind hook registry at call time (per Conventions §Facade re-export, artefact require). The upside is bundle-isolation — an app that omits a feature does not carry its code. The downside this front-porch closes: the late-binding is otherwise invisible, so a developer who forgets to :require the impl artefact calls a re-exported fn that exists and is met with an opaque artefact-missing error. One surface — features — makes the optional-feature inventory self-explaining; the boolean is a lookup into the map it returns.
The known optional features are :schemas, :machines, :routing, :flows, :http, :ssr, :epoch, :resources (the closed per-feature split set per Conventions §Artefact tiers).
features ships to production. It is a runtime query, not dev-time instrumentation, so — unlike the trace / epoch surfaces — it is NOT gated on interop/debug-enabled? and does NOT elide under :advanced + goog.DEBUG=false. A production caller may legitimately read (get-in (rf/features) [:routing :loaded?]) before taking a routing-dependent path. The feature→coordinate mapping is static data in the always-loaded re-frame.core facade (a plain table of {:feature {:maven … :require … :spec …}} strings), never a live :require reaching into the optional impl namespaces — a live reach-in would create a hard facade→optionals reference that pulls every optional artefact into every production bundle, breaking bundle-isolation. Presence is detected by a pure keyword lookup in the always-loaded late-bind hooks atom against a representative key the impl publishes at ns-load — no reach into the optional namespace.
| API | M/Fn | Signature | Status | Tier | Spec |
|---|---|---|---|---|---|
features |
Fn | (features) → map of every optional feature keyword to its inspection entry: the static coordinate data (:maven / :require / :spec) merged with the live :loaded? boolean. E.g. {:epoch {:maven "day8/re-frame2-epoch" :require "re-frame.epoch" :spec "Tool-Pair (Time-travel / epoch)" :loaded? true} …}. Ships to production (NOT elided). |
v1 | advanced | — |
Reading the boolean, and the boot-time guard. features is the ONE door — data before magic. The per-feature boolean is a lookup into its map, and an unknown feature keyword has no entry, so the lookup reads nil, not false, for it.
(get-in (rf/features) [:epoch :loaded?]) ;=> true
;; Want boot-time failure rather than first-call failure? Write the guard.
;; One line, no framework verb, and NOT an `assert` (asserts are elidable):
(when-not (get-in (rf/features) [:epoch :loaded?])
(throw (ex-info "re-frame.epoch is not on the classpath"
(get (rf/features) :epoch))))
That guard carries the copy-pasteable coordinate data, because the inventory entry is the throw's data.
Artefact-missing errors carry the require. This front-porch is paired with a hard rule: every artefact-missing error in the framework — including the late-bind facade throws (
:rf.error/<feature>-artefact-missing, raised viare-frame.late-bind/require-fn!from there-frame.core-<feature>wrappers) — carries the exact copy-pasteable Maven coordinate and the namespace to require at app boot in its:reasonslot. The named pattern is documented once at Conventions §Facade re-export, artefact require.
Configure keys¶
Runtime configuration is uniformly via (rf/configure! {<key> <opts>, …}) — a single nested map keyed by the top-level keys enumerated here. The argument MUST be a map (a non-map argument fails loudly); a missing top-level key leaves that subsystem untouched; a present key delegates to that subsystem's configurator in table order (:epoch-history, :trace-buffer, :elision, :observability) preserving its slot-merge semantics; an unknown top-level key applies nothing (closed-and-additive). Keys are plural-noun-shaped; opts are an open map of per-key settings.
An unknown top-level key applies nothing, but it is not necessarily silent. The vocabulary above is CLOSED and its keys are BARE, so an unrecognised bare key reads as a typo of a real key rather than as an extension point — the shape Conventions §No silent swallow says MUST signal. A bare (:epoch-histroy) or framework-namespaced (:rf.foo/bar) unknown key therefore emits :rf.warning/unknown-configure-key in dev builds, naming every offending key and the full known set. A user-namespaced key (:myapp/thing) passes in silence — that is the extension-key carve-out the same section reserves. The warning is dev-gated (rf.interop/debug-enabled?, so the whole surface DCEs under :advanced + goog.DEBUG=false) and observational (:recovery :ignored): the call still returns nil and still applies nothing.
(rf/configure! {:epoch-history {:depth 100}
:trace-buffer {:events-retained 25}
:elision {:rf.egress/threshold-bytes 8192}
:observability {:errors [{:sink :my-app.sinks/sentry}]}})
| Key | Opts shape | Default | Status | Spec |
|---|---|---|---|---|
:epoch-history |
{:depth N :trace-events-keep N} — :depth non-negative integer (0 disables the ring); :trace-events-keep non-negative integer caps raw :trace-events retention (per Security §Epoch privacy posture). There is no post-projection scrub hook: the in-process ring buffer and every :epoch-stream listener deliver the RAW record (EP-0010 causal replay material), and an off-box forwarder that wants a further scrub composes it — (-> r (rf/project-egress opts) scrub) — at the sink. Per Tool-Pair §Time-travel. |
{:depth 50, :trace-events-keep 50} |
v1 (dev-only) | Tool-Pair |
:trace-buffer |
{:events-retained N} — non-negative integer event-slot count (one slot per event / pipeline run); 0 disables retention (the surface stays live) |
{:events-retained 50} |
v1 (dev-only) | 009 |
:elision |
{:rf.egress/threshold-bytes N} — non-negative integer; 0 disables runtime auto-detect (only declared / schema entries elide) |
{:rf.egress/threshold-bytes 16384} |
v1 | 009 |
:observability |
{:handled-events [<sink-entry>…] :errors [<sink-entry>…]} — the PROCESS-DEFAULT production observation sink policy, in the same closed grammar a frame's :observability takes (Spec-Schemas §:rf/frame-meta FrameObservability). Precedence is per stream: a frame declaring a stream uses its own entries for it, a frame omitting the stream inherits this default's, and {:errors []} on a frame is that frame's opt-out — so exactly one source is consulted per record per stream. Inheritance moves the sink list, not the redaction authority: an inheriting frame's records still project under its OWN classification. Records with no frame authority (the frameless producers, and any record whose :frame no longer resolves) reach this default ALONE, projected with the governing frame explicitly nil. Validated against that grammar at CALL time, fail-loud with :rf.error/bad-frame-classification and :where 'rf/configure! — one grammar, two doors. Unlike every other key here an explicit nil CLEARS it (a policy must be removable without knowing what installed it); a second call REPLACES rather than merges. Per 015 §The process default and §Frame-owned observability sinks. |
none declared | v1 | 015 |
Reading the values back. (rf/current-config) returns the live values in this same nested shape — the read twin of configure! per Conventions §configure! vs current-config. Three properties are normative. (1) Process values only — a frame carrying its own :rf.trace/events-retained metadata is not reflected; this reads the same slots configure! writes and nothing else. (2) A subsystem's key is ABSENT when its own producer is unavailable, never nil and never a fabricated default. The two optional keys are independent — each is read through its own late-bind hook, so an absence never implies the other's: :epoch-history requires the optional day8/re-frame2-epoch artefact and :trace-buffer the dev-only trace-tooling sibling, so a production bundle that DCEs the latter omits :trace-buffer alone while a loaded epoch artefact still reports :epoch-history beside it. (get-in (rf/current-config) [:epoch-history :depth]) reads nil when the epoch artefact is absent, not when trace tooling is. :observability follows the same absent-not-nil rule for a different reason and the distinction is normative: re-frame.observability is always loaded, so its key is absent when no process default has been declared — a statement about CONFIGURATION, never about the build — and it reports the declared policy verbatim, never any frame's effective policy, which is per-stream and resolved per record. (3) The user-namespaced carve-out is write-only — a :myapp/thing key configure! accepted in silence is not reflected back, because the vocabulary is closed and only keys the runtime READS have live values to report. The result is a key-by-key snapshot rather than a transactional one, and it is not promised to be wire-serialisable.
No
:sub-cachekey. There is no:sub-cache {:grace-period-ms N}knob: sub-cache disposal is synchronous on derefer-count → 0 (per 006 §Reference counting and disposal); there is no deferred-grace timer to configure.
SSR error-projection policy (:public-error-id, :dev-error-detail?) is not a configure key — it is per-frame metadata on the frame's :ssr map (see Conventions §Configuration surfaces bucket 3 and 011 §Server error projection). Different frames in the same process can carry different projector / dev-detail settings, so the natural lifetime is per-frame, not process-global.
Opts-key naming rule¶
The opts map for any configure key mixes two shapes — it encodes which contract owns the sub-key:
- Framework-owned semantic sub-keys use a namespaced keyword under a reserved
:rf.<area>/*sub-namespace (per Conventions §Reserved namespaces). The namespace identifies the cross-spec policy area the sub-key participates in — the same key shape appears verbatim wherever that policy is consumed, not only insideconfigure. Example::elisioncarries{:rf.egress/threshold-bytes N}because:rf.egress/threshold-bytesis the same per-call policy key consumed byrf/project-egressand the wire-elision walker beneath it — one key, one fact, three readers, so the process-level knob and the per-call opt are literally the same keyword (per Conventions §Reserved namespaces —:rf.egress/*). The namespaced form makes the cross-surface identity grep-visible and prevents collision with adjacent per-knob settings. - Ergonomic per-knob sub-keys are unqualified bare keywords (
:depth,:trace-events-keep). These sub-keys are local to a singleconfigurekey's opts map — they do not appear elsewhere in the framework's vocabulary, so a framework-owned namespace would add noise without adding identity. The bare form is the default at this leaf position; reach for it whenever the knob is unique to oneconfigurekey.
The discriminator is whether the sub-key names a cross-surface policy slot or a one-off knob. A sub-key earns a :rf.<area>/* namespace when it names a contract that lives in more than one place (:rf.egress/threshold-bytes is read by :elision, by the wire-elision walker, and by the MCP wire walker). A sub-key stays bare when it is local to its parent configure key (:events-retained under :trace-buffer and :depth under :epoch-history each tune one ring's slot count and live nowhere else in the vocabulary — separate knobs, no shared contract, so each stays bare).
New configure keys MUST apply the same rule: if a sub-key participates in a cross-spec policy area, qualify it under the area's reserved namespace; otherwise leave it bare. The rule is closed — there is no third shape (no :configure/depth, no :rf.configure/* prefix). A sub-key that would want a third shape is evidence the proposed knob is doing two things and should be split.
Fixed-and-additive¶
The configure-keys vocabulary is fixed-and-additive (Spec-ulation): existing keys cannot be renamed or removed; new keys are added by extending the table. Because configure! takes a single map and ignores unknown top-level keys, user code that wraps configure can pass a composed config value straight through — unknown keys no-op rather than throw. That pass-through is what the user-namespaced carve-out above protects: a wrapper's own :myapp/* keys ride through in silence. A wrapper composing BARE keys it invented will additionally see the dev-only :rf.warning/unknown-configure-key — which is the point, since a bare key is a name this vocabulary is entitled to own.
Machines¶
Split between the v1 machine-as-event-handler foundation and the post-v1 re-frame.machines scaffolding library — see 005-StateMachines.md §Disposition. The machine is the event handler: reg-machine / defmachine registers a machine into the same event-handler slot an ordinary reg-event writes.
| API | M/Fn | Signature | Status | Tier | Spec |
|---|---|---|---|---|---|
reg-machine |
M | (reg-machine machine-id machine-spec) / (reg-machine machine-id opts machine-spec) — registers a machine as an event handler. Walks the literal spec form at expansion time and co-locates per-element source on each :guards / :actions entry + a reference-site :source-coords on each :states-tree map node. The optional opts metadata map is the canonical Spec 001 MIDDLE slot; it carries an event-vector :schema — the machine + event-vector-schema shape. |
v1 | advanced | 005 |
reg-machine* |
Fn | (reg-machine* machine-id machine-spec) / (reg-machine* machine-id opts machine-spec) — plain-fn surface beneath the macro. No source-coord walking. The optional opts metadata map is the MIDDLE slot (uniform with the macro); it carries the event-vector :schema. |
v1 | advanced | 005 |
defmachine |
M | (defmachine name [docstring] spec) — def-shape that walks the literal spec at the definition site and co-locates per-element source onto the def'd value, for the def-then-register shape (defmachine m {…}) / (reg-machine :id m). Does not register. |
v1 | advanced | 005 |
make-machine-handler |
Fn | (make-machine-handler spec) → event-handler fn. Pure factory (registers nothing); the handler reg-machine registers and the :rf.machine/spawn fx installs. For Level-2 tests, not a registration door. |
v1 | advanced | 005 |
machine-transition |
Fn | (machine-transition definition snapshot event) → one plain map: {:status :ok :snapshot … :fx […]} on success, {:status :error :error {:kind …}} when a guard / action / :data fn threw or a depth limit tripped (per 005 §Level 1) |
v1 | advanced | 005 |
| — | — | Enumerating registered machines has no per-kind accessor: it is (keys (into {} (filter (fn [[_ m]] (:rf/machine? m))) (rf/registrations {:source :store :kind :event}))) — the generic registrar query plus the :rf/machine? discriminator, rowed in §Public registrar query API. |
— | — | 005 |
:rf.machine/spawn (fx) |
— | Canonical actor-lifecycle fx (registered globally by re-frame.machines). Args per :rf.fx/spawn-args. |
v1 | — (fx-id) | 005 |
:rf.machine/destroy (fx) |
— | Canonical actor-destroy fx (registered globally by re-frame.machines). Args: an actor id. |
v1 | — (fx-id) | 005 |
:raise (fx) |
— | Reserved fx-id inside a machine action's :fx (machine-internal, routed pre-commit). Args: an event vector. |
v1 | — (fx-id) | 005 |
:final? / :output-key (state-node keys) |
— | :final? marks a leaf state as terminal — entering it auto-destroys the machine. :output-key (requires :final?) designates the child's :data slot reported back via the parent's :on-done. Capability axis :fsm/final-states. See 005 §Final states. |
v1 | — (spec key) | 005 |
:on-done (:spawn spec key) |
— | (fn [{:keys [data result]}] new-data) on the parent's :spawn map. Fires on the parent's next macrostep when the spawned child enters a (non-error) :final? state; result is the child's :data slot named by the final state's :output-key (or nil). Returns the parent's new :data map. |
v1 | — (spec key) | 005 |
machine->xstate-json |
Fn | (machine->xstate-json definition) → JSON |
post-v1 lib | tooling | 005 |
machine->mermaid |
Fn | (machine->mermaid definition) → string |
post-v1 lib | tooling | 005 |
Canonical descriptions (factory purity, spec keys, snapshot location, registration-time validation, etc.) in 005-StateMachines.md and Spec-Schemas.
v1 transition-table grammar subset is enumerated in 005 §Capability matrix; shape in Spec-Schemas §:rf/transition-table.
Standard registered subs (machines)¶
| Standard sub | Returns | Spec |
|---|---|---|
[:rf/machine <machine-id>] |
The machine's snapshot {:state :data :tags} (or nil if not yet initialised) |
005 |
[:rf.machine/has-tag? <machine-id> <tag>] |
true iff the machine's current snapshot's :tags set contains tag (false for an unknown / not-yet-initialised machine) |
005 |
The canonical machine read is the registered [:rf/machine machine-id] subscription vector — see 005 §Subscribing to machines. It is read like any other subscription — @(rf/subscribe [:rf/machine machine-id]), @(rf/subscribe [:rf.machine/has-tag? machine-id tag]) — and named projections chain off it by declaring it under :inputs. There is no named-read-sugar fn: a runtime-db framework read is a subscription vector, one grammar.
Story / variant / workspace library (post-v1)¶
See 007-Stories.md.
All Story surfaces are tooling (a Storybook-shaped dev surface — registration, execution, introspection — not application logic). That statement is exact rather than a summary with an exception: every re-frame.story manifest row carries :tier :tooling, and the facade holds no implementation carve-out. All but five are JVM-introspected; those five are the #?(:cljs …) arm of a split-host facade (register-substrate! / registered-substrates / mount-shell! / unmount-shell! / active-shell), rowed under :cljs-only with :facade? true per row and held fully-rowed by the CLJS enumeration probe. See §Tiering of cross-tool surfaces.
| API | M/Fn | Signature | Status | Tier | Spec |
|---|---|---|---|---|---|
reg-story |
M | (reg-story id metadata) |
post-v1 lib | tooling | 007 |
reg-variant |
M | (reg-variant id metadata) |
post-v1 lib | tooling | 007 |
reg-workspace |
M | (reg-workspace id metadata) |
post-v1 lib | tooling | 007 |
reg-tag |
M | (reg-tag id metadata) |
post-v1 lib | tooling | 007 |
reg-decorator |
M | (reg-decorator id metadata) |
post-v1 lib | tooling | 007 |
reg-story-panel |
M | (reg-story-panel id metadata) |
post-v1 lib | tooling | 007 |
run |
Fn | (run target) / (run target opts) → promise/future of the unified run-result. target is a keyword (registered variant) OR a map (inline plan). The single execution verb. |
post-v1 lib | tooling | 007 |
is |
Fn | (is target) / (is target opts) → runs target and reports each assertion to clojure.test / cljs.test. JVM blocks (bounded by :timeout-ms, default 30000) and returns the run-result; CLJS returns the run promise. |
post-v1 lib | tooling | 007 |
explain |
Fn | (explain target) / (explain target opts) → the plan's :explain map (args-validation, sub-overrides, decorators, …) without running. |
post-v1 lib | tooling | 007 |
variants-with-tags |
Fn | (variants-with-tags tag-set) → seq of variant ids |
post-v1 lib | tooling | 007 |
snapshot-identity |
Fn | (snapshot-identity variant-id) → {:variant-id ... :content-hash "..."} |
post-v1 lib | tooling | 007 |
story-view |
Fn | (story-view variant-id) → hiccup |
post-v1 lib | tooling | 007 |
The recommended execution surface is the three verbs run / is / explain (each accepts a registered-variant keyword OR an inline-plan map) — the vocabulary the Guide and the skills lead with, and what a Story author should reach for first. The lower-level variant lifecycle — run-variant / reset-variant / watch-variant / destroy-variant! / render-variant — is a supported tooling surface too, not implementation vocabulary: custom shells, test fixtures and one-shot screenshot pipelines call it directly, and the Story tutorials teach it. It is not rowed in the table above because this projection rows the slices that surface through re-frame.core or the per-feature artefacts; the complete Story surface is rowed in tools/story/spec/API.md and carried in the api-manifest. The unified run-result is the single execution-record boundary; read its verdict via result-status / result-passed? (:pass / :fail / :cannot-run / :error — there is no :passing? boolean). Canonical execution model: story spec 017 §Public execution API (the re-frame2-story library spec, the normative home for the verbs + run-result shape).
Removed / not shipped¶
These surfaces are removed or renamed — not part of the public projection and not a tier (the deprecated tier is reserved for surfaces still shipping while on the way out; pre-alpha carries none). This is a migration table: each row names what to use instead.
| API | What to do | Reference |
|---|---|---|
dispatch-with (master) |
Use (dispatch event {:fx-overrides {...}}) |
MIGRATION M-4 |
rf/install-adapter! / rf/adapter-disposed? / rf/current-adapter-spec |
Not on the re-frame.core facade. Seat the adapter with rf/init!, which throws :rf.error/adapter-already-installed on a DIFFERENT adapter, so a facade install-adapter! would add no observable difference. Read the adapter with rf/current-adapter, which answers the spec map; the discriminator is (:kind (rf/current-adapter)). The two owning-namespace surfaces live on re-frame.substrate.adapter — install-adapter! is the strict primitive (throws on ANY second call, carries the generation / rollback machinery) and adapter-disposed? is the breadcrumb that picks :rf.error/adapter-disposed over :rf.error/no-adapter-installed. No shim, no deprecation alias. |
006 |
dispatch-sync-with (master) |
Use (dispatch-sync event {:fx-overrides {...}}) |
MIGRATION M-4 |
dispatch-to (not shipped) |
Use (dispatch event {:frame :todo}) |
002 |
subscribe-to (not shipped) |
Use (subscribe query-v {:frame :todo}) |
002 |
frame-dispatcher / bound-dispatcher / bound-subscriber (not shipped) |
Use (rf/capture-frame) (the keystone OPERATION BUNDLE — captures the frame at creation; safe during render and from async callbacks) |
002 |
bound-fn (CLJS macro) |
Use (rf/capture-frame) — the keystone OPERATION BUNDLE captures the frame and carries dispatch / subscribe across the boundary. |
002 |
frame-bound-fn (macro) / frame-bound-fn* (fn) |
Not on the facade — capture-frame is the ONE public carry primitive. The frame-rebinding closure semantics (re-establish *current-frame* around an arbitrary already-held fn) live internally as re-frame.frame/bind-fn for framework / test / tooling reach; not app-facing. |
002 |
frame-value->id (fn) |
Not on the facade — every public surface accepts a frame VALUE or its id: the routing ops (dispatch / subscribe / app-db-value / frame-provider / …) normalize a value to its id interchangeably, and destroy-frame! accepts either but reads the value's exact-incarnation lifecycle authority (a stale value no-ops against a same-id successor; a keyword is address-directed). Either way there is no need to unwrap a value to its id. |
002 |
dispatcher |
Use (:dispatch (rf/capture-frame)) or the dispatch injected in a reg-view body |
002 |
subscriber |
Use (:subscribe (rf/capture-frame)) or the subscribe injected in a reg-view body |
002 |
current-frame |
Renamed to current-frame-id (returns a frame-id keyword) |
002 |
get-frame-db |
Renamed to app-db-value (returns the app-db VALUE, a plain map) |
002 |
(dispatch frame event) / (dispatch-sync frame event) / (subscribe frame-id query-v) / (subscribe-once frame-id query-v) frame-first positional forms |
Not supported — every sig is [payload] / [payload opts]; target an explicit frame via the trailing {:frame …} opt (a frame-id keyword or a live frame value). |
002 |
enable-performance-api-tracing! (not shipped) |
Performance-API instrumentation is gated on the compile-time re-frame.performance/enabled? goog-define, not a runtime toggle (see 009 §Performance instrumentation) |
009 |
add-trace-listener / remove-trace-listener (not shipped) |
Use register-listener! / unregister-listener! |
009 |
register-trace-listener / unregister-trace-listener (no-bang, not shipped) |
Renamed to register-listener! / unregister-listener! (bang form matches the side-effecting nature of listener registration) |
009 |
Bare [:my-view "args"] keyword-tagged hiccup |
Use the Var form [my-view "args"] (canonical) or [(rf/view :my-view) "args"] for late-binding by id |
Conventions |
h macro (not shipped) |
Use the Var form [my-view "args"] or [(rf/view :my-view) "args"] |
Conventions |
reg-global-interceptor |
Use the frame-level :interceptors config key (frame-level is the canonical "global within this frame"). For cross-frame observation use register-listener!. |
MIGRATION M-17 |
clear-global-interceptor |
No replacement needed — re-declare the frame (make-frame with an updated :interceptors vector; absent-key semantics clear it). |
MIGRATION M-17 |
reg-sub-raw |
Use reg-sub (app-db reads), Pattern-AsyncEffect (non-app-db sources), state machines (lifecycle), or the 006 adapter contract (bridging external reactivity). |
MIGRATION M-18 |
reg-event-db |
Use reg-event (no alias) — destructure :db from the coeffects map and wrap the return in {:db …}: (reg-event id (fn [{:keys [db]} ev] {:db BODY})). A stale call raises the always-on hard error :rf.error/reg-event-db-removed naming reg-event. The ^:no-doc facade throwing stub carries no API-manifest row. |
001 §The retired event-registration names |
reg-event-fx |
Use reg-event (no alias) — reg-event IS the identical shape under the bare name (coeffects in, effects out); just rename the call. A stale call raises :rf.error/reg-event-fx-removed naming reg-event. |
001 §The retired event-registration names |
reg-event-ctx |
A framework-internal primitive. Express application full-context work as a registered interceptor (reg-interceptor with :before/:after, referenced by id from a reg-event chain). A stale public call raises :rf.error/reg-event-ctx-removed. |
001 §The retired event-registration names |
reset-frame! |
Use (destroy-frame! id) then (make-frame config), re-supplying the SAME config (which carries :id, and :images for an image-loaded frame) you already hold. A stale call raises :rf.error/reset-frame-removed naming the composition. The ^:no-doc facade throwing stub carries no API-manifest row. |
002 §Resetting a frame — destroy + make-frame |
reload-images! |
Image hot-reload is re-construction — re-call make-frame against the SAME :id with a new :images vector; it swaps the generation while preserving frame memory. To read the reload diff, call frame-generation before/after and diff with generation-diff. A stale call raises :rf.error/reload-images-removed. The ^:no-doc facade throwing stub carries no API-manifest row. |
002 §Image resolution and composition |
get-coeffect / get-effect |
Not on the façade (no audience; no manifest row). Inside a reg-interceptor :before/:after fn read the context map directly: (get-in ctx [:coeffects k]) / (get-in ctx [:effects k]). The owning-namespace re-frame.interceptor/get-coeffect / get-effect fns are framework-internal. |
001, 002 |
assoc-coeffect / assoc-effect |
Not on the façade (no audience; no manifest row). Inside a reg-interceptor :before/:after fn write the context map directly: (assoc-in ctx [:coeffects k] v) / (assoc-in ctx [:effects k] v). The owning-namespace re-frame.interceptor/assoc-coeffect / assoc-effect fns are framework-internal. |
001, 002 |
re-frame.alpha/reg |
The shipped per-kind registrars: reg-event / reg-sub / reg-fx / reg-cofx / reg-flow. (The v1 event trio reg-event-db / reg-event-fx / reg-event-ctx is not a v2 target — those are removed/withdrawn throwing stubs and migration inputs only, see the rows above and EP-0018; reg-event is the single event-registration form.) |
MIGRATION M-23 |
re-frame.alpha/sub |
Vector-form (rf/subscribe [::id arg]). |
MIGRATION M-23 |
re-frame.alpha/reg-sub-lifecycle and built-in lifecycle policies (:safe, :no-cache, :reactive, :forever) |
Sub-cache uses a single algorithm — synchronous ref-counting (dispose on derefer-count → 0), per Spec 006 §Reference counting and disposal. For specific edge cases file a follow-up bead. | MIGRATION M-23 |
feature-loaded? |
Use the lookup: (get-in (rf/features) [:epoch :loaded?]). Note the contract — an UNKNOWN feature keyword reads nil there, not false. |
§Feature inspection |
require-feature! |
Write the guard: (when-not (get-in (rf/features) [:epoch :loaded?]) (throw (ex-info "…" (get (rf/features) :epoch)))). Not an assert — asserts are elidable. The inventory entry carries the Maven coordinate + require ns. There are no :rf.error/feature-not-loaded / :rf.error/unknown-feature ids. |
§Feature inspection |
init-platform |
Platform is the HOST DEFAULT — :client on CLJS (including CLJS-on-Node), :server on the JVM — overridden PER FRAME: (rf/make-frame {:id :ssr/request :platform :server}). There is no process-wide marker and no setter; re-frame.interop/active-platform is a per-host constant the frame's :platform key wins over. A CLJS-on-Node SSR host tags its request frames :platform :server rather than setting a process-wide platform at boot. |
002, 011 |
:preset :ssr-server |
Write the one key it would expand to: {:platform :server}. The closed preset set is :default / :test / :story. |
002 |
Cross-references¶
- 000-Vision.md — principles and design decisions
- 002-Frames.md — frames, dispatch envelope, drain semantics, overrides, machine foundations
- 005-StateMachines.md — machine library design (post-v1)
- 007-Stories.md — story/variant/workspace library design (post-v1)
- 008-Testing.md — testing API and patterns
- 009-Instrumentation.md — trace event stream, listeners, error contract
- 010-Schemas.md — Malli schemas
- 014-HTTPRequests.md —
:rf.http/managedrequest fx (optional capability) - MIGRATION.md — AI-driven migration spec