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 (rf2-kuky.44 — 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 now 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 formerly listed here is now tooling and match-schema-expectations left the facade instead (rf2-i6kh). |
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) was already 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. This flat classification is the settled outcome of the retrospective facade sweep (rf2-i6kh) and it reverses the former carve-out that tiered therun-variant/watch-variant/reset-variantvocabularyimplementationas "public-for-technical-reasons only". That carve-out was the outlier: 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. (is-variant/run-plan/is-plan, named by the former carve-out, no longer exist as vars.) The one export the sweep found with no such workflow,match-schema-expectations, left the facade for its owningre-frame.story.resultrather than being annotated internal. (Absorbs story F-8.) - 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. The retrospective facade sweep (rf2-ar67) recorded no move and no rename, and the reason is that tools/xray/spec/API.md §Wider public surface had already drawn the porch/workshop line and named the two surfaces deliberately kept OFF the facade — the per-key config setters beyond the four above, and thekeybinding/attach!/detach!lifecycle pair — so the audit found the line drawn rather than needing to draw it. 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. One name,load-theme!, was recorded:keepwith its spelling left open for a ruling — it is the sole export here that mutates, yet carried no bang, whileload-*sits on no row of the §Lifecycle-verb law closed roster. Ruled underrf2-7nk1(2026-09-06) and now executed: the export is spelledload-theme!under §Naming bucket 3 alone, with no roster row added and noConventions.mdedit — 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. That is the same readingregister-trace-listener→register-listener!was settled on below, likewise without a roster row. Pre-alpha: no alias, no stub, no deprecation window. - Xray
mount-<panel>!family (mount-epoch-panel!,mount-app-db-diff!,mount-trace!,mount-machine-inspector!,mount-routing!,mount-segment-inspector!, the mastermount-shell!, …) — internal-public, the canonical (and now 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. This resolves the prior "public vs internal-but-stable" question against the closed vocabulary — the answer isinternal-public. (Resolves xray M2.) - 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. Demoted offinternal-public(now 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. (Absorbs xray H6.)
- 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 pre-split 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 the retired 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. The ctx→ctx handler shape and inject-cofx are retired (no alias). 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). Replaces ->interceptor as the public authoring surface. 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. API-shrink #1, rf2-csbbwu removed the frame-value->id accessor; there is 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 — image-declared host capabilities are removed; :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 (rf2-lxwpob folded the dedicated reload-images! verb into this; read the diff via generation-diff). make-frame is the ONE programmatic constructor (tools / tests / SSR / dynamic / image-loaded frames — rf2-h1vqa4 deleted the reg-frame spelling, no alias); 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. |
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 | Replaces the removed reload-images! verb's report — a read over two generation values, not a bespoke verb (rf2-lxwpob). |
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)) (rf2-qm7k83 Part A) |
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 is retained (the map value IS the schema — no positional ambiguity in a bulk map); the plural form is KEPT 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 (rf2-bqstzr): 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. It REPLACED nine per-kind names —clear-event,clear-sub,clear-fx,clear-flow,clear-route,clear-http-interceptor,clear-resource,clear-mutation,clear-resource-scope— which differed on where the name lived (seven on the façade, two off it), whether a nilary clear-all existed, how the frame was named, and what came back. No shim, no alias, no deprecation (rf2-kuky.80).(rf/clear :sub :cart/total) ;; => :cart/total (rf/clear :cofx :now) ;; six kinds that had no public 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 (rf2-s32bf, 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 were the only callers the nilary arities ever had. 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 (rf2-moftbs). 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, rf2-m90brg) 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, rf2-m90brg) 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, rf2-m90brg) 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, rf2-bfadc6) — closes the misbinding footgun for an author carrying the opts form over from subscribe |
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.
dispatch-* family taxonomy¶
Per audit-of-audits state-machines #10, 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, rf2-m90brg — 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 (rf2-m90brg retired dispatch* / dispatch-sync* / subscribe* from the facade); 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 (API-shrink #1, rf2-csbbwu removedframe-bound-fn/frame-bound-fn*from the facade entirely —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 |
Component (Reagent) | SCOPE-only (rf2-nyea0r split — 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 (API-shrink #1, rf2-csbbwu); 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 |
Component (Reagent) | ENSURE — a COMMIT-OWNED TWO-PASS boundary (rf2-nyea0r split). [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 (rf2-oz7wr). 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 — it replaces the removed dispatcher / subscriber nouns and 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). frame-bound-fn / frame-bound-fn* are REMOVED from the facade (API-shrink #1, rf2-csbbwu) — capture-frame (or an explicit {:frame …} opt) expresses the real use cases and is the ONE public carry primitive; the frame-rebinding closure semantics survive 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 (rf2-nyea0r split — 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 (rf2-nyea0r split) — ($ 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 (rf2-kuky.57): 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 (rf2-g8pbwg): 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 (rf2-g8pbwg). There is no imperative install/remove pair to call; the retired install-url-listener! / remove-url-listener! / install-history-listener! / remove-history-listener! exports are GONE (pre-alpha, no back-compat shim).
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). The SSR query surface is NOT re-exported throughre-frame.core(rf2-kuky.44): 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-missingthe façade copies existed to give was 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 now the ONE late-bind façade exception.The ssr-node crossing (
rf2-8arzr) adds 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 and are not rowed below; all three are now 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 (rf2-8arzr.7,rf2-3ne8).
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 (rf2-kuky.89, replacing 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: the no-arg form was removed (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. 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 |
Standard SSR-related events:
| Event | What it does | Spec |
|---|---|---|
:rf/server-init |
Per-request server-side initialisation. Reads request cofx; dispatches setup events. :platforms #{:server}. |
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,:advancedper rf2-kuky.87); 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. (Two rows here previously described them as subscriptions; corrected under rf2-8arzr.6, matching the long-standing statement at docs/api/re-frame.ssr.md §Subscriptions — there are none. Both were keyword rows, which api_md_check skips by design, so no gate contradicted them.)
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}] — synthesises the canonical success reply (per 014 §Testing). Registered at load of re-frame.http.test-support (NOT re-frame.http.managed); the stub family ships in the same namespace per (audit-of-audits #15). |
v1 (optional capability, dev/test) | — (fx-id; test) | 014 |
:rf.http/managed-canned-failure |
fx | [:rf.http/managed-canned-failure {:kind <:rf.http/*> :tags {...}}] — synthesises the canonical failure reply. 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 / :sensitive?). 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 (rf2-kuky.80). :http-interceptor is one of the two frame-scoped kinds, so the opts map is accepted, and it is exact and fail-closed exactly as before (rf2-s32bf). 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. The per-verb call-site helper namespace re-frame.http was deleted pre-alpha (rf2-kuky.11) — 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 (rf2-ibksxg — 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, no longer an everyday spelling). Omitting all reply targets raises :rf.error/http-no-reply-target at dispatch — the co-located default (reply merged under :rf/reply back to the originating event) was retired pre-alpha (rf2-et4c1s). 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 (rf2-kuky.80). 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 (rf2-kuky.80). 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 (rf2-bqstzr): 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 (rf2-kuky.80). 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 (rf2-ru73k6 F3: 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 (rf2-kuky.31): 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, and the retired install-revalidation-listeners! / remove-revalidation-listeners! exports are GONE (pre-alpha, no back-compat shim). 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 :url-bound? fold one key over — see §Routing.
Request decoration (EP-0016 Rider 3) reuses the existing HTTP facade. Auth/tracing/base-URL/retry decoration for resources and mutations is not a new resources surface — it is the existing
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). No new facade export is introduced for decoration; 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. The retired positional arity —
(registrations :event)— documented itself as reading the default source store, but delegated 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 silently read that frame's image instead. A public inspection request has to say which source it means; a bare keyword cannot. (rf2-kuky.30.)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 (rf2-kuky.31). 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, :rf/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 — image-declared host capabilities are removed.) 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). The runtime-db-only read (retired runtime-db-value, rf2-t3lftq — API-shrink #3) 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 the JVM legacy 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)¶
Removed from the public facade. This section is retained as a stable anchor for inbound cross-references; the surface it once documented is no longer public.
The retired 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; API-shrink #1 rf2-csbbwu), 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 (rf2-kuky.84).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), :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:
:eventsLEFT the publicregister-listener!vocabulary (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 the upcoming data-classification mechanism (separate spec doc; in progress). The handler-meta
:sensitive?annotation is removed.
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::errorsLEFT the publicregister-listener!vocabulary (see §Observation listeners), and independent corpus observation regardless of a frame's policy is WITHDRAWN as 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 two carries the raw registry alone had are now the process default's: 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 (the per-frame :on-error recovery policy was removed). 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 the upcoming data-classification mechanism (separate spec doc; in progress). The handler-meta
:sensitive?annotation is removed — 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. The always-on :events / :errors streams that used to sit in this vocabulary were a second, fail-open production door — unprojected, raw :exception, no frame policy, fanned across every frame — and they are RETIRED from the public API. Independent corpus observation regardless of a frame's policy is withdrawn as a public primitive; the substrates survive as 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) — the former per-stream clear-listeners! façade verb was retired in API-shrink #4.
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, 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. 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! (rf2-t3lftq — API-shrink #3 consolidated the former replace-app-db! / reset-app-db! / replace-runtime-db! / replace-frame-state! four-mutator family into this): 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}) is the former replace-app-db!; {:rf.db/app {}} is the former reset-app-db!; a runtime-only map ({:rf.db/runtime v}) is the former replace-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 (rf2-uhouu) — 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 (the per-channel register-epoch-listener! / unregister-epoch-listener! pair was retired in API-shrink #4, rf2-9flalp; 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 (rf2-kuky.9 ruling A, executed by rf2-kuky.90). 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 had to hand-assemble the :rf.egress/* floor that a named boundary already carries — two spellings for one boundary, and the weaker of the two silently. rf/project-egress is the single projection door on the façade. The walker stays 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 [...] :as-of-epoch <epoch> :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 — an unknown key was previously a silent no-op, which is the defect it removes. 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 remain 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> :as-of-epoch <epoch> :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 moved onto this door when rf/projected-record retired (rf2-bv1p); they were spelled bare until rf2-kuky.93 brought the whole opts vocabulary under :rf.egress/*, so all twelve keys of the closed map now read as one namespace. 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 survive as implementation tier only — they carry no public registration verb (rf2-kuky.69). 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.
Recent additions consumers should be aware of: :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. The imperativeadd-marks/set-markspath-marks API, there-frame.marksnamespace, the frame:sensitive {:app-db …}annotation, and the positionalredact-interceptorare all removed; 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. (The legacy handler-meta :sensitive? annotation has been removed; 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 (rf2-kuky.69 retired the register-listener! :events / :errors streams); a test is one of the two consumers they survive for, 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 earlier line (keep specific-work helpers, drop trivial (->interceptor :before f) ones) narrows to path-only because path is coupled to app-db commit no-op semantics and justifies the :factory mechanism. Five v1 interceptors removed: debug, trim-v, on-changes, enrich, after (per MIGRATION §M-21).
inject-cofx / inject-cofx* — the v1 coeffect-injection interceptors — are removed (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 no longer 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 survives internally: a stale call still 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 retired v1 public interceptor-authoring helpers and their replacements:
| Removed / retired API | Replaced by |
|---|---|
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 (rf2-bqstzr) 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 (rf2-kuky.80). 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 old double-store "last owner" unregister / realign is gone — the :flow registrar kind is RESERVED-but-empty). 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 — registered error-emit callbacks (the :errors stream of register-listener!) fire under CLJS :advanced + goog.DEBUG=false. 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 |
v1 (optional capability) |
Interceptor / context plumbing¶
The interceptor context accessors get-coeffect / assoc-coeffect / get-effect / assoc-effect are removed from the public re-frame.core façade and carry no API-manifest row (a removed surface is not part of the canonical public API). Post-EP-0017/EP-0022 they lost their audience — the setters had zero callers, the getters one. 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 remain 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 ( — the no-arg arity was cut 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! (an internal contract slot adapters implement) is unchanged. |
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 (rf2-kuky.4, 2026-09-06 — the former keyword-returning spelling and its current-adapter-spec twin are struck; the keyword form was literally 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 (plus the retired-but-reserved :rf.adapter/freehand / :rf.adapter/ui that nothing produces since 2026-08-16). 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 where the removed feature-loaded? read false; the contracts are not identical and the lookup is what is taught.
(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 same copy-pasteable coordinate data the removed require-feature! threw, 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 existing 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 (post-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.
Retired key. The earlier
:sub-cache {:grace-period-ms N}knob is gone. Sub-cache disposal is now 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 (rf2-kuky.31): 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. Per ; 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 synchronously when the spawned child enters a :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. Per and. |
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). Since the retrospective facade sweep (rf2-i6kh) that statement is exact rather than a summary with an exception: all 121 re-frame.story manifest rows carry :tier :tooling, and the facade holds no implementation carve-out. 116 of the 121 are JVM-introspected; the other 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. Absorbs story F-8; 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 |
DELETED from the re-frame.core facade (rf2-kuky.4, 2026-09-06 — rider A-i). Seat the adapter with rf/init!, which since rf2-kuky.1 throws :rf.error/adapter-already-installed on a DIFFERENT adapter, leaving install-adapter! no observable difference. Read the adapter with rf/current-adapter, which now answers the spec map; the discriminator is (:kind (rf/current-adapter)). The two owning-namespace surfaces SURVIVE 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. Pre-alpha: no shim, no deprecation alias. |
006 |
dispatch-sync-with (master) |
Use (dispatch-sync event {:fx-overrides {...}}) |
MIGRATION M-4 |
dispatch-to (proposed earlier) |
Use (dispatch event {:frame :todo}) |
002 |
subscribe-to (proposed earlier) |
Use (subscribe query-v {:frame :todo}) |
002 |
frame-dispatcher / bound-dispatcher / bound-subscriber (proposed earlier) |
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) |
DELETED from the facade (API-shrink #1, rf2-csbbwu) — capture-frame is the ONE public carry primitive. The frame-rebinding closure semantics (re-establish *current-frame* around an arbitrary already-held fn) survive internally as re-frame.frame/bind-fn for framework / test / tooling reach; not app-facing. |
002 |
frame-value->id (fn) |
DELETED from the facade (API-shrink #1, rf2-csbbwu) — 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 (rf2-moftbs — 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 |
DELETED (API-shrink #1, rf2-csbbwu) — 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! (proposed earlier) |
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 (proposed earlier) |
Use register-listener! / unregister-listener! |
009 |
register-trace-listener / unregister-trace-listener (no-bang, proposed earlier) |
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 (proposed earlier) |
Removed. 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! (rf2-lxwpob) |
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! (rf2-lxwpob) |
Image hot-reload is folded into 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 |
Removed from the façade (no audience; carry 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 remain framework-internal. |
001, 002 |
assoc-coeffect / assoc-effect |
Removed from the façade (zero callers; carry 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 remain 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? (rf2-kuky.4, 2026-09-08) |
Use the lookup: (get-in (rf/features) [:epoch :loaded?]). Note the contract difference — an UNKNOWN feature keyword reads nil there, where feature-loaded? read false. |
§Feature inspection |
require-feature! (rf2-kuky.4, 2026-09-08) |
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 same Maven coordinate + require ns the throw did. The :rf.error/feature-not-loaded and :rf.error/unknown-feature ids retire with it. |
§Feature inspection |
init-platform (rf2-kuky.77, 2026-09-08) |
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 that used to call (rf/init-platform :server) at boot tags its request frames instead, which is what every in-repo SSR host already did. |
002, 011 |
:preset :ssr-server (rf2-kuky.77, 2026-09-08) |
Write the one key it expanded to: {:platform :server}. The closed preset set is now :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