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). - 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 exceptions of the epoch + SSR-query surfaces, 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-schema-at/app-schema-meta-at/app-schemas/app-schemas-digest/set-schema-validator!/-explainer!/-printer!/set-schema-fns!),re-frame.machines(reg-machine*/make-machine-handler/machine-transition/machines/machine-meta/machine-by-system-id),re-frame.routing(match-url/route-url/current-url/clear-route),re-frame.flows(clear-flow). The epoch + SSR-query re-exports remain on the façade as documented late-bind exceptions (rows below):
| Namespace | Artefact | Surfaces |
|---|---|---|
re-frame.core |
core | the registration / dispatch / subscribe / interceptor / lifecycle / configure surfaces; late-binds re-exports for two artefacts: (a) the re-frame.epoch surface (epoch-history, restore-epoch!, replace-frame-state!, projected-record, projected-history) — the epoch listener stream is reached through the core-native (register-listener! :epoch …) verb, not a per-channel re-export; (b) the re-frame.ssr query surface (render-to-string, render-tree-hash, project-error, render-head, active-head, head-model->html, head-snapshot). The streaming-render-shell / streaming-render-continuation / streaming-build-final-payload triple 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-missing errors. |
re-frame.test-support |
core | assert-path-equals, poll-until, fixture machinery (per §Testing). Runtime-state axis — registrar, frames, app-db, drain. assert-path-equals mirrors the :rf.assert/path-equals Story event. View-tree assertions live in the sibling re-frame.test-helpers. |
re-frame.test-helpers |
core | View-assertion helpers — hiccup-walk (find-by-testid / find-by-attr family, text-content, extract-handler, invoke-handler), the testid authoring helper, and the expand-tree walker (per §Testing — View-assertion helpers). View-tree axis — hiccup data, testids, attached handlers. Runtime-state assertions live in the sibling re-frame.test-support. |
re-frame.ssr |
day8/re-frame2-ssr |
render-to-string, emit-ui-tree, render-tree-hash, streaming-render-*, render-head, active-head, head-model->html, head-snapshot, project-error (per §SSR). |
re-frame.ssr.ring |
day8/re-frame2-ssr-ring |
the Ring host-adapter (default-html-shell, streaming-prefix/suffix, trusted-shell hooks per Spec 011). |
re-frame.ui / re-frame.ui.test / re-frame.ui.react |
day8/re-frame2-ui |
compiled views, the first-party ui/adapter, roots/mounting, the structural/mounted test surface, and the foreign React-interop tier (per §Compiled views, Specs 004/006/008). |
re-frame.schemas |
day8/re-frame2-schemas |
app-schemas, app-schema-at, app-schema-meta-at, app-schemas-digest, set-schema-validator!/-explainer!/-printer!/set-schema-fns!, validate-at-boundary-interceptor (per §Schemas). |
re-frame.http |
day8/re-frame2-http |
the verb helpers get / post / put / delete / patch / head / options (per §HTTP requests). |
re-frame.machines |
day8/re-frame2-machines (post-v1 scaffolding) |
reg-machine, make-machine-handler, machine-transition, machines, machine-meta, the :rf.machine/spawn / :rf.machine/destroy fx (per §Machines). |
re-frame.epoch |
day8/re-frame2-epoch |
epoch-history, restore-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 through re-frame.core via late-bind hooks — (:require [re-frame.epoch]) at boot before consuming the surfaces through re-frame.core (per Tool-Pair §Time-travel — Artefact home). |
re-frame.adapter.uix |
day8/re-frame2-uix |
UIx-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!, projected-history, 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 (install-adapter!, init-platform). 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 Story run-variant / migration vocabulary, 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 all live here. |
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 — the public execution verbs
run/is/explainand the registration macros (reg-story/reg-variant/reg-workspace/reg-tag/reg-decorator/reg-story-panel) are tooling (a Storybook-shaped dev surface, not application logic). The run-result read accessors —run-result,result-status,result-passed?,run-result-schema,valid-run-result?,explain-run-result— are tooling too: the statement-of-record for reading what arunproduced (per story spec 017 §Run result). Therun-variant/is-variant/run-plan/is-plan/watch-variant/reset-variantimplementation/migration vocabulary isimplementation— public-for-technical-reasons only (the chrome and tests reach it across namespaces), explicitly NOT the supported execution surface. Theimplementationtier is its home — public as a var, off the supported surface. (Absorbs story F-8.) - 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 (
elide-wire-value,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 #{:db :rf.db/runtime :fx} effects map out (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 input-fn? computation-fn) |
v1 (preserved + extended) | front-porch | 002 | The only sub-registration form in v2. Three input-production modes (app-db reader / static :<- / parametric input-fn) — see §reg-sub input-production modes. The optional first fn is a v2 input-fn (query-v → vector-of-query-vectors), NOT a v1 reaction-returning signal fn. :<- sugar preserved. |
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 | 004 | 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 | 004 | 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-schema-at [:user]) / (app-schema-meta-at [: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 |
Fn | (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/flow-meta-at / 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: layer-1 has no producer; :<- is the literal producer; input-fn is the query-parametric producer.
| Mode | Form | Meaning |
|---|---|---|
| App-db reader | (reg-sub id computation-fn) |
No upstream subscriptions. The computation fn receives app-db and the outer query-v. |
| Static inputs | (reg-sub id :<- q1 :<- q2 computation-fn) |
Inputs are literal query vectors known at registration. |
| Parametric inputs | (reg-sub id input-fn computation-fn) |
Inputs are computed from the outer query-v when a concrete cache entry is materialized. |
The two-function form's first fn is a v2 input-fn — 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. The computation-fn receives the vector of resolved input values (in the same order) and the outer query-v.
(rf/reg-sub
:article/page
(fn input-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. An input-fn MUST return a vector, and every element of that vector MUST be a query vector (a vector whose first element is a keyword):
;; 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 — see :rf.error/sub-input-fn-bad-return
: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. Use :<- for static inputs; reach for input-fn only when the upstream query vectors need values from the outer query-v. The static :<- form is exactly a constant input-fn ((fn [_] [[:items] [:filter]])). 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).
Clearing registrations¶
| API | M/Fn | Signature | Status | Tier |
|---|---|---|---|---|
clear-event |
Fn | (clear-event) / (clear-event id) |
v1 (preserved) | advanced |
clear-sub |
Fn | (clear-sub) / (clear-sub id) |
v1 (preserved) | advanced |
clear-fx |
Fn | (clear-fx) / (clear-fx id) |
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 — clear-sub is already taken by the symmetric inverse of reg-sub (the registrar decrement), so un- is reserved as the singular form for the sub-cache ref-count decrement. |
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.
Named-target sub-family (the reserved [:rf.machine/dispatch-to-system [system-id event]] fx tuple, per 005 §Cross-machine messaging by name).
This question is "do you have a :system-id instead of a target machine-id?" Named addressing resolves the target through the per-frame [:rf.runtime/machines :system-ids] reverse index first, then dispatches just like the stamping pair. The only surface is the action-side fx tuple; the redundant dispatch-to-system direct-call FN twin was deleted (pre-alpha, zero in-repo consumers — a machine IS an event handler, so the everyday send is plain dispatch to the id you hold). The fx is a parked named-addressing escape (advanced/parity tier), retained for XState v6 actor-system parity (systemId addressing); the facade audit at API-freeze rules on its deletion. The naming reads as "dispatch with extra routing logic on top," not "different kind of dispatch."
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, 004 |
view |
Fn | (view view-id) → render-fn (runtime-lookup handle; returns the registered render-fn, not hiccup). Use in hiccup as [(rf/view :id) args...] — the lookup form for late-binding a registered view by id. |
v1 | advanced | 001, 004 |
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.
Freehand views — re-frame.freehand (Spec 004)¶
The ONE public door of the Freehand view substrate (EP-0036, D001; artefact
day8/re-frame2-freehand, conventionally aliasedv). The namespace is a door, not a layer: everything below is either declared there (the authoring macros) or re-exported from the namespace that owns it. The descriptor type, its constructor, the head classifier, the call normalizer and the render body live in the internalre-frame.freehand.descriptorand are not a supported surface — a published constructor would mint a value that passesview?and classifies as an internal boundary while carrying no view-id, no source and no lowering, which is the one-declaration rule (004 §A declared view cannot be called) with a hole in it.v/defviewis the only way to create an internal mounted boundary, and that is now enforced by where the constructor lives rather than merely stated. The roster below is the whole door; it grew one deliberate row at a time. The compiled tier has LANDED —{:compiled true}on a declaration selects it, per 004D — and so has the OUTWARD half of the host boundary,v/->react, which hands a declared view to React-world as a component value. The INWARD direction is not a vacancy and needs no verb: a finished React element is already an ordinary browser child value, so it enters a Freehand tree in a child position through the existing child fold — ONE shared React tree, with context propagation,v/->reactcontent interleaved back through it, and SYNCHRONOUS teardown. Two boundaries hold: a bare React COMPONENT at a vector head is not a legal Freehand descriptor (a created React ELEMENT in a child position is), and the JVM structural renderer accepts no React elements — the child path is browser-only, like the mount verbs.
| API | M/Fn | Signature | Status | Tier | Spec |
|---|---|---|---|---|---|
defview |
M | (v/defview name docstring? opts? [props] body+) — the ONE declaration form. Exactly one parameter, the props map; no positional view arguments. The var it binds CANNOT be successfully called: (the-view {…}) raises :rf.error/view-called-directly naming the three legal recoveries (mount it, inline it as a plain defn, or extract a shared defn helper). The option roster is CLOSED and every key optional: :children-policy — :optional (default) / :none / :required — and :compiled — false (default) / true, the ONE-LINE PROMOTION selecting the already-landed compiled tier's finite grammar for that declaration (004D; the CALLER is unchanged either way — mounting is [the-view {…}] before and after, because the compiled tier reuses the interpreted descriptor / props contract / boundary node. For a body ALREADY inside the grammar the marker is the whole change, structural output and the view's own tests untouched; a body that is NOT forces the build-named extraction, which adds view-boundary nodes the interpreted twin lacked and moves the structural tree and its tests WITH the declaration) — and :props, an optional Malli-vector props SCHEMA held inert (OPTIONAL in both modes) that CLOSES the delivered props map to the keys it names and drives BOTH the compiled analyzer's build-time key check and the interpreted boundary's development-only runtime check, production rendering the same tree either way; a declared schema is projected by describe under the distinct inspection key :props-schema, reported absent when none was declared. An unknown key — a typo, or a RESERVED option whose owning slice has not landed, the way :compiled and :props were each refused until their tier landed — or a missing body raises :rf.error/defview-bad-args at macro expansion — an intentional no-output view writes an explicit nil body |
EP-0036 | front-porch | 004 |
custom-element |
M | (v/custom-element tag {:properties #{…}}) — declare which of a custom element's props are JS properties rather than attributes: the one fact about a web component Freehand cannot read off the element and will not guess. Undeclared, a kebab prop travels the attribute grammar, whose rule for an unrecognized spelling is to pass it through VERBATIM — so React writes the attribute, the element's property setter never runs, and the component mounts renderable but INERT with nothing reported. Camelising the key by hand fixes the browser and breaks the SERVER, because a server cannot set a property: a declared property is OMITTED from markup and applied at hydration, and only a declaration says which props those are. ONE declaration binds every lowering path — a compiled literal props map (classified at build time under that build's own declaration slice and carried to the canonicaliser as a constant), a compiled element's dynamic props, the interpreted walk, a v/spread / v/spread-safe forwarded map, the React writer, and the JVM structural tree — because the classification belongs to the DECLARATION and not to the tier that happened to see the prop; a rule reaching only the compiled literal map would mean property at a literal site and attribute one v/spread away. A declared name keeps its authored kebab key in the structural tree and its value VERBATIM, outside the attribute-value grammar — which is exactly what admits the map / vector / host-object values the declaration exists for — is recorded in the node's reserved :rf.ui/property-props set the serialiser consumes, and reaches the DOM under the ruled camelCase property name (:help-text sets helpText, deliberately NOT the react-dom attribute table). Top-level and compile-resolvable, registering the way a v/defview does: the tag is an unqualified keyword containing -, the options map is LITERAL, and :properties is the WHOLE v1 grammar — an unknown key, a namespaced tag or a runtime map is :rf.ui.compile/bad-custom-element. So is :class or :style inside :properties: those two are ATTRIBUTES with grammars of their own (:class composes with the .class#id tag sugar, :style carries the CSS map, and on a custom element both follow DOM rules exactly as they do on a <div>), and since a property-classified name is OMITTED from markup, classifying one would render the element with that value dropped from the HTML while the structural fold still carried it — so the declaration is refused rather than quietly rewritten into an attribute, and the refusal roster is exactly those two names rather than a general attribute-versus-property taxonomy. DECLARING IS THE EXCEPTION: undeclared names are attributes and an undeclared ELEMENT needs no declaration at all. One tag has one property manifest — two sources declaring it differently is :rf.ui.compile/custom-element-conflict at build and :rf.error/custom-element-conflict at registration, never a winner picked by load order (004D §Template grammar, 004B §Custom elements) |
EP-0036 | advanced | 004D |
mount |
Fn (CLJS) | (v/mount root-form dom-node) / (v/mount root-form dom-node opts) → root — the browser mount. The minimal one-root spelling is a bare declared view at the head; its :root-id is DERIVED from the mounted view's registered id, so the single-root page authors nothing, and the mount derives the minimal Root Descriptor ({:rf.root/schema-version 1 :root-id :view-id :root-id-provenance :derived}). IDEMPOTENT PER ROOT: re-mounting the same root-id into the same container re-renders the existing host root — the hot-reload path, where the redefined view's fresh descriptor keeps its qualified id, so the reload finds the root live and re-renders without reseeding it. opts is CLOSED — identity (:root-id authored verbatim, :disambiguator a scalar appended to the derived id so ONE view can mount twice on one page, :identifier-prefix), preflight (:frame — a frame-id keyword SCOPES a frame something else owns, a make-frame opts map carrying :id ENSUREs one the root owns for its lifetime, and either way the frame is live and its :initial-events drained before React sees anything — but an ENSURE re-mount takes the idempotent no-op, or a same-root config REFRESH, ONLY while it can PROVE it still owns the incarnation live under the id (the value its install recorded carries the frame's :rf.frame/incarnation-token, and that token is still the live frame's); an equal fingerprint over a frame it can no longer prove it owns — a same-id SUCCESSOR the installed incarnation was destroyed and re-created as, or a token-less legacy row a reload carried — is NOT a no-op but a loud refusal, :rf.error/frame-payload-conflict under :scope-config-less-or-own-the-lifetime, before make-frame and before React, rather than silently taking address-directed ownership of a frame this root never installed), and React's :on-uncaught-error / :on-caught-error / :on-recoverable-error. A root claims its id, its container and its effective identifierPrefix BEFORE it renders, so a collision on any of them fails loud (:rf.error/duplicate-root-id / :rf.error/root-container-in-use / :rf.error/duplicate-identifier-prefix) with the existing roots untouched. Browser-only; the JVM renders the SAME [view {…}] form structurally, so mounting and structural rendering are one spelling (004C) |
EP-0036 | front-porch | 004C |
re-frame.freehand/hydrate-root |
Fn (CLJS) | (v/hydrate-root dom-node root-form) / (v/hydrate-root dom-node root-form opts) → root — the hydrating mount. ADOPTS the server-rendered markup already in dom-node rather than replacing it, so the page the reader has been looking at since first paint becomes the live page. Verification is React's own adoption: a divergence React recovers from — a text mismatch, or a missing / extra / wrong-type element — surfaces as :rf.ssr/hydration-mismatch carrying the :root-id, composed OVER any host :on-recoverable-error (emit first, then delegate) and bounded to the adoption window; an ATTRIBUTE-only divergence is outside that signal by React's own contract. Identity comes from the server, so identity opts client-side are REFUSED (:rf.error/root-manifest-invalid naming the conflicting key) — a client rendering under its own prefix breaks use-id hydration; :frame and the host error callbacks are accepted exactly as at mount. A container carrying nothing to adopt takes the FALLBACK: an ordinary client mount, the client-only first load (011) |
EP-0036 | front-porch | 011 |
re-frame.freehand/unmount! |
Fn (CLJS) | (v/unmount! root) → nil — TOTAL teardown. The registry entry goes, and with it the root-id, container and identifierPrefix claims; the React root unmounts, so every ViewCell below it disconnects, releasing every dependency and retiring every published callback; and the root's reference to its frame is released — a frame the root ENSUREd is DESTROYED once no live root still references it, and destroyed by the EXACT incarnation VALUE the install recorded, never the bare frame-id: teardown consumes precisely that incarnation, so a stale installer whose frame was already destroyed and re-created under the same id no-ops rather than reaching through to kill the SUCCESSOR, and a token-less legacy row likewise leaves the live frame untouched rather than destroying an incarnation it cannot prove it owns. A frame the root merely SCOPED is left alone. GUARDED and idempotent: a root already unmounted, or superseded by a newer root claiming its id, is a no-op rather than a throw, because tearing down on a stale handle's behalf would tear down the successor (004C) |
EP-0036 | front-porch | 004C |
re-frame.freehand/render-static |
M | (v/render-static [view {…}]) → an inert HTML string — the pure :server-phase static render, JVM/SERVER ONLY (Freehand's renderToStaticMarkup). It compiles the LITERAL root form to the versioned JVM structural tree and folds it to a static HTML string: NON-hydrating, with NO Root Manifest, NO hydration payload, and NO phase flip — the static-page path, not the SSR-then-hydrate path (v/hydrate-root + re-frame.ssr/hydrate! own that). The LONE macro on the door — v/mount / v/hydrate-root / v/unmount! are runtime fns — so the root form is LITERAL at the call site (a runtime-assembled vector is the same :rf.ui.compile/runtime-root-form compile error, which only a macro can raise), the root-id derives from the ONE mounted view, and a CLJS expansion is the ruled :rf.ui.compile/ui-render-static-jvm-only. NO SILENT ELISION (Spec 004C §3, EP-0034 §2): a runtime-requiring capability — a subscription, committed handler, effect, foreign head, or v/behavior host attachment (a live host lifecycle a :server render owns no node for) — anywhere in the root's server-reachable view closure fails LOUD, never a capability quietly dropped, and each tier proves this with what it has. A COMPILED view is proven at BUILD time off its manifest's :static-facts closure, so a breach is the compile error :rf.ui.compile/static-root-requires-runtime with source coordinates; an INTERPRETED (paved-path) view has no manifest to consult and is proven at RENDER, where the fold refuses to drop a live capability (:rf.error/static-render-requires-runtime) and a reactive read fails on its own account (:rf.error/view-read-outside-render). A referenced view whose facts cannot be obtained is reported UNPROVEN rather than assumed safe (:rf.ui.compile/static-root-unproven-dependency). v/client-only stays legal (only its capability-free fallback is server-reachable) and a deterministic use-id is exempt. re-frame.freehand takes NO compile-time require on re-frame.ssr: the render reaches the SSR serialiser through the late-resolution seam re-frame.freehand.tree/emit-static-html, so a render-static call in a namespace requiring only re-frame.freehand compiles and renders, and a missing day8/re-frame2-ssr artefact is the ruled :rf.error/ssr-artefact-missing naming the coordinate rather than a raw host exception (011) |
EP-0036 | advanced | 011 |
re-frame.freehand/->react |
Fn (CLJS) | (v/->react view) / (v/->react view {:map-props f}) → a React component — the OUTWARD half of the host boundary (D014): a declared view handed to React-world as the COMPONENT VALUE a library asks for when its API takes a component rather than an element (a grid cellRenderer, a drag overlay, a virtual-list row, a plugin slot). What comes back mounts the descriptor exactly as an ordinary Freehand parent would, so events, subscriptions, error identity and commit fencing inside it are unchanged. DESCRIPTOR ONLY — a plain function, hiccup, a view id keyword or a rendered form is :rf.error/ui-tree-malformed naming a declared view or an explicit wrapper as the recovery — and the option roster is CLOSED at one key, :map-props. Props cross by ONE shallow own-property pass matching EXACT names with values untouched ("person-id" → :person-id, "acme/id" → :acme/id; no camelisation, no deep walk), or by the single :map-props adapter, which receives the raw foreign object and returns the one props map. THREE names are the bridge's: frame is consumed, children becomes the boundary's TRAILING children (so React content nests inside an exported view, under the view's own declared :children-policy), and ref is REFUSED — Freehand has no ref protocol. A props map carrying the reserved :frame is refused naming the view. IDENTITY IS STABLE: one view plus one adapter answers the identical component object, keyed on the VIEW ID so a hot reload republishes the new body through the boundary React already mounted rather than remounting the foreign subtree. A frame is SELECTED, never created: an own frame prop scopes an already-live frame and own-property PRESENCE decides, so an explicit nil (:rf.error/no-frame-context), a malformed target (:rf.error/bad-frame-provider-arg) and a target naming no live frame (:rf.error/frame-provider-frame-absent) each fail loud and self-attributed rather than falling through, while an OMITTED prop resolves ambiently. Browser-only — absent on the JVM like the mount verbs, which is the whole of its SSR policy (004 §The outward React bridge, 011) |
EP-0036 | advanced | 004 |
view? |
Fn | (v/view? x) → boolean — the ONE predicate that answers "is this an internal view boundary?". Total and host-neutral. ifn? is not a proxy for it: a declared view IS IFn, purely so a direct call can raise :rf.error/view-called-directly on both hosts rather than a raw cast failure on the JVM (D002, amended 2026-07-22) |
EP-0036 | advanced | 004 |
describe |
Fn | (v/describe view) → the descriptor's public inspection / registry projection — {:re-frame.freehand/view :view-id :source :lowering :children-policy} plus :props-schema when declared. The key roster is closed in both directions; the render body and the host mount / structural tree entries stay private. Inspection data, never a dispatch surface |
EP-0036 | tooling | 004 |
manifest |
Fn | (v/manifest view) → manifest map, or nil — a COMPILED declaration's analysis as plain data: :view-id, :grammar, the finite lexical-site rosters (:subscriptions / :events / :slots / :html-sites, EVERY entry carrying a :source-coord), their :capabilities union, the :reactive? / :view-cell capability-elision verdict, :crossings — the internal-view boundaries the body mounts, one entry per lexical site, each MARKED with the mode it crosses into, so the manifest says where the compiled tier stops rather than leaving a reader to assume it does not — and :static-facts, the render-static SUMMARY over those sites rather than another site roster: the server-reachable {:caps :deps} projection (the runtime-requiring capability tokens a pure :server render could not honour, and the direct view-ids to close over transitively) that v/render-static's no-silent-elision proof reads. It is computed once and carried on the manifest — the same value the ambient build index takes, so the two cannot drift — which is what keeps a view compiled in ANOTHER build (AOT / precompiled) fact-bearing when this build's index knows nothing of it. nil for an INTERPRETED declaration is the honest answer, not an omission: that mode has no finite grammar and no analysis step, so there is nothing it could claim (004D §Static manifests and capability elision) |
EP-0036 | tooling | 004 |
check |
Fn (JVM) | (v/check "src/app/people.cljc") → one report per v/defview in the file, in declaration order — the READ-ONLY compile checker's published name, and the door's one SOURCE-level read (every other verb here answers about a value the caller already holds). It runs the SAME analyzer the build runs, over the declaration as it stands today, and answers {:view-id :source :current-lowering :target-grammar :compile-eligible? :findings}; a finding carries the analyzer's OWN :rf.ui.compile/* id, the offending form, its coordinates, a :reason and the :recovery ladder whose last rung is always :keep-interpreted. {:compiled true} looks like a one-line change, so trying-and-seeing is the tempting way to learn the grammar and the worst one — the build stops at the first form outside it, and an author who got there by editing has already changed the declaration to find out whether the change was available. check inverts that order; editing becomes the final step. THE COMPILED TIER IS SPECIALISED and this verb recommends nothing: interpreted Hiccup is the paved path and the right answer for ordinary views, the compiled tier serves a performance requirement that a specific SHAPE earns (a sub-free boundary whose reactive ViewCell the analysis can prove away — :reactive? false / :view-cell :elided on v/manifest), and on an ordinary reactive form the two modes are inseparable. So an ELIGIBLE report is not an argument for promoting anything, and there is no score, no percentage and no "compile the hot 5%" — a promotion recommender is an explicit EP-0036 non-goal. WHAT IT DOES NOT CHECK is contract, not omission: nothing about correctness (eligible means the body is inside :re-frame.freehand/v1, not that it renders what you meant, that its subscriptions are registered or that its props satisfy a schema); nothing outside the file it was pointed at; nothing that is not a defview; and ONE finding per declaration, because the analyzer stops where the build stops — take a rung and re-run. A pure .cljs source is REFUSED rather than approximated (there is no build context to resolve heads through), while a .cljc declaration is checked for BOTH reader-conditional branches. It reads SOURCE from disk and resolves heads through the LOADED namespace, so a file edited since it was last required makes the two disagree — reload, then check. JVM only, and honestly ABSENT in ClojureScript rather than present-and-throwing (the policy v/->react states in the other direction). Read-only as a law: no source opened for writing, no emitter run, no registry contributed to, nothing printed (004D §The read-only checker) |
EP-0036 | tooling | 004D |
sub |
Fn | (v/sub [:query …]) → value — the paved path's render-only reactive read. Resolves against the view's frame, returns the current value, and records a render-owned read the SELECTED commit publishes as a bundle dependency; a later value change invalidates exactly this occurrence and recommits the whole bundle atomically. Legal ONLY during an active declared render — captured same-thread, including through an ordinary defn helper — and refused loudly (:rf.error/view-read-outside-render) elsewhere; non-reactive callers use rf/subscribe-once. rf=-stabilized. The subscription law is 006 §The subscription law |
EP-0036 | front-porch | 006 |
event |
M | (v/event [params…] body…) — the explicit conversion seam at a foreign boundary: the body names ONE event vector (dispatched through the shared materializer) or nil (dispatches nothing). Stable per site; may not v/sub, use hooks, refs or effects |
EP-0036 | advanced | 004 |
handler |
M | (v/handler [params…] body…) — the explicit IMPERATIVE foreign callback. Its return is ignored; stable per site, reads the committed body, and is retired with its site |
EP-0036 | advanced | 004 |
render-fn |
M | (v/render-fn [params…] body…) — a PURE callback a foreign owner invokes during ITS render. May return Freehand content; may NOT v/sub, dispatch, use hooks or touch refs, and is deliberately outside the committed-proxy identity scheme |
EP-0036 | advanced | 004 |
re-frame.freehand/slot |
Fn | (v/slot render-fn-value arg…) — render the parameterized content a CALLER supplied, at the site the component chooses, with the arguments the component supplies. The value is a v/render-fn or nil; an absent slot renders nothing, so a component may offer content it does not require. COMMON grammar with ONE deliberate asymmetry: an INTERPRETED body additionally accepts an ordinary pure function of the same arguments, a COMPILED one refuses it at build time naming v/render-fn — the compiled tier lowers what it can SEE, and a function value is exactly what it cannot. The arity is a contract, not a convention: the argument count is checked against the render-fn's declared parameter vector BEFORE the call, so the answer is the same on both hosts rather than JavaScript's silent undefined or the JVM's raw ArityException. The rendered output participates in the surrounding children exactly like any other child — no slot node, no wrapper (004 §Render slots) |
EP-0036 | advanced | 004 |
re-frame.freehand/spread |
Fn | (v/spread base) / (v/spread base overrides) — forward a runtime attribute map onto an element, in its props position. overrides wins every collision, and every key is judged by the rule a LITERAL attribute key is judged by — the same refusals, read off the same emitted slot — so a map assembled at run time cannot smuggle in a spelling the grammar refuses at a visible site. :key is refused outright: it is not an attribute, and it is literal at the element that carries it. This is the VISIBLE-COST forward — whatever the map carries lands, and the author said so at the site (004 §Props forwarding) |
EP-0036 | advanced | 004 |
re-frame.freehand/spread-safe |
Fn | (v/spread-safe owned caller) — forward a CONSUMER's attribute map onto an element the component owns, BOUNDED. The deny law runs in EVERY build, not just dev: :key, :ref, :value, :checked and the component's own on-* handler families (both the bubble and capture phases) may not appear in caller, and an offender is a loud :rf.error/ui-tree-malformed rather than a silent drop — a literal offender is the compile error :rf.ui.compile/spread-safe-owned-key. Alternate spellings do not route around it: a key is judged by the slot it is about to be written into, so a namespaced, string, symbol or already-camel spelling of a denied name is denied with it. Everything that survives folds UNDER the owned props, with :class the one exception — the two class values COMPOSE, owned first. That bound is what lets a component keep a promise about the element it renders: a controlled input stays controlled and an owned handler stays the one that fires, while the consumer still passes aria-*, data-* and a class (004 §Props forwarding) |
EP-0036 | advanced | 004 |
re-frame.freehand/controller-key |
Fn | (v/controller-key kind props) → [kind address] — the key a writable controller's record lives under: the PAIR of the library's controller kind and the caller-supplied :control address carried by props. Asking for the key IS what makes a controller writable — a props-only view never calls it and pays nothing. The kind is half the key, so a dropdown and a field addressed at one domain identity read two records rather than each other's; the address is immutable EDN naming the domain thing that owns the state ([:invoice 42 :amount]), never derived from render position. An absent :control is refused with :rf.error/view-control-address-missing rather than defaulted — every controller that skipped it would otherwise share ONE record keyed by nil. An EXPLICIT nil is a different mistake and gets its own id, :rf.error/view-control-address-nil: the prop is present, so nothing was forgotten at the call site and the repair belongs upstream, in whatever expression answered nothing. Presence decides, never truthiness — false, 0 and "" are ordinary addresses. Two occurrences passed the SAME address share one record on purpose and are not diagnosed. Where the record then lives is the library's choice: Freehand fixes the identity model and no storage path (004 §Controller identity) |
EP-0036 | advanced | 004 |
re-frame.freehand/controller-revision |
Fn | (v/controller-revision kind props) → revision — the GENERATION a buffered controller is rendering under: the caller's :reset-key, taken from props. Asking for it is what makes a controller buffered, exactly as the key is what makes one writable — the two are separable, and a writable-but-unbuffered control takes the key alone. Any EDN the caller advances when it establishes a new baseline; never the value, because the case this exists for is a caller rejecting a draft by REASSERTING what it already had, which value-equality is provably blind to. REQUIRED — an absent :reset-key is refused with :rf.error/view-control-reset-revision-missing, because optional would ship a control that buffers correctly right up to the first rejection; a caller that never resets says so with a stable literal such as 0. An EXPLICIT nil is refused separately, with :rf.error/view-control-reset-revision-nil — nil is not a generation, and the fence would read every draft under one as superseded while the control went on accepting keystrokes (004 §The buffered controller and the reset generation) |
EP-0036 | advanced | 004 |
re-frame.freehand/controller-current? |
Fn | (v/controller-current? stamped revision) → boolean — THE GENERATION FENCE, and ONE predicate asked at BOTH of a buffered controller's boundaries: the READ, where a draft is displayed only while it is current, and the WRITE, where only a current record may produce the caller's intent. They are the same question through the same function because a control whose display and whose commit disagreed about which generation is live would commit something the user could not see. TOTAL and safe in the missing direction — an absent stamp is not current whatever revision is, which is the half a hand-rolled (= a b) gets wrong (two nils compare equal) and what makes a draft written from a superseded render born stale. A pure comparison over two generations by rf=: it reads no record shape, so the record stays the library's (004 §The generation fence) |
EP-0036 | advanced | 004 |
re-frame.freehand/html |
Fn | (v/html s) — render the trusted markup string s VERBATIM, as the SOLE child of a DOM element: [:article.body (v/html (:rendered-html post))]. The ONE place escaping is bypassed, and the call is the mark — there is exactly one spelling, and it is a node form rather than a prop. Every prop-shaped route to the same capability (:dangerouslySetInnerHTML, :dangerously-set-inner-html, :inner-html, literal or smuggled through a runtime v/spread / v/spread-safe caller map) is refused in EVERY build, naming this verb as the replacement. Freehand does not sanitise, and neither does SSR — s is written as given, so a <script>, an onerror= or a javascript: href inside it reaches the document exactly as written; the verb is a derivative projection that fetches nothing, authorises nothing and decides nothing, so trust is established where the markup ENTERED the app. What it buys is enumerability: in a {:compiled true} declaration every site lands on the manifest's :html-sites roster with its source coordinate, so the set of bypasses in a codebase is finite and listable, and the compiled view declares an :html capability. ONE spelling, both modes, both hosts: a node the compiled analyzer lowers (React dangerouslySetInnerHTML; the JVM trusted-markup leaf {:html s}, written verbatim by the SSR serialiser) and this ordinary function call in an interpreted body, through the one canonicaliser. The POSITION is a contract — a sibling, a nested run or a view whose whole body is the call has no element to own the markup and is refused (:rf.ui.compile/html-not-sole-child compiled, :rf.error/ui-tree-malformed interpreted) — as are <textarea> (React sets a textarea's content through :value), every void element, and a non-string value (004B §Trusted markup, and 004D §Trusted markup — what v/html does not do) |
EP-0036 | advanced | 004 |
adapter |
Var (map) | (rf/init! v/adapter) — the Freehand reactive-substrate adapter: install it ONCE at boot, before the first frame is minted. The closed substrate contract plus the canonical discriminator :kind :rf.adapter/freehand, built on the core React spine (re-frame.substrate.spine) every React-shaped adapter shares — so it takes no new dependency, supplying React's own hooks. Installation is IDEMPOTENT across a hot reload and EXPLICIT always: there is no default-adapter registry and no automatic selection, and a second install without an intervening (rf/destroy-adapter!) fails loud (:rf.error/adapter-already-installed) — one adapter per process. Two things it does that the shared spine does not. (rf/destroy-adapter!) unmounts every live Freehand root FIRST — releasing their subscriptions, disconnecting their ViewCells and releasing their frame references — and disposes the spine second, because the drain needs the containers the spine is about to tear down; that drain is a SAFETY NET, not a substitute for orderly teardown, since only v/unmount! runs on a live adapter and can therefore destroy a frame a root owned. And its synchronous flush-render! returns with the Freehand DOM SETTLED: the pending ViewCell window is closed inside React's commit boundary and then converged to a bounded fixed point, rather than left to the microtask a mark arms. The wrapper adapters remain FIRST-CLASS: Reagent, reagent-slim and UIx are independently supported RENDERER adapters and this one competes with none of them — it is the observation wiring of a substrate that renders itself. Browser-only, exactly like the mount verbs (006 §The adapter API contract) |
EP-0036 | front-porch | 006 |
raw-fn |
Fn | (v/raw-fn f) — the expert callback seam: hand a foreign API a function with EXACTLY the supplied identity, for protocols where callback identity is itself protocol data. Every other roster form gets a site-owned stable proxy |
EP-0036 | advanced | 004 |
projections |
Var (set) | The CLOSED reserved scalar-projection roster — ::v/value, ::v/checked, ::v/key, ::v/scroll-top, ::v/new-state. The only markers a declarative event vector may carry, and the exact keys of the payload map a firing site supplies. Closed but not guessed up front: ::v/scroll-top and ::v/new-state were admitted when pilots needed them (a scalar off the event target, demonstrated by a component) |
EP-0036 | advanced | 004 |
route-link |
Var (descriptor) | [v/route-link {:to :route-id …html-attrs} & children] — a navigation anchor, declared with the ordinary v/defview and holding the ordinary descriptor: there is no route-link intrinsic. Renders a REAL <a href=…> with the route's strategy-encoded href, and on a plain in-app left click dispatches the routing cascade to the frame that rendered it. :to required; :params / :query / :fragment feed href and payload; :prefetch :intent warms the destination's resources on credible user intent. Those, together with :on-click, are the CONTROL keys — stripped before DOM emission, so every other key passes through to the <a>. A caller :on-click runs first and may veto; modifier / middle clicks and native anchors defer to the browser. Without day8/re-frame2-routing it fails loud with :rf.error/routing-artefact-missing. The behavioural contract is 012 §The Freehand route-link descriptor — routing owns the law |
EP-0036 | advanced | 012 |
materialize-event |
Fn | (v/materialize-event event payload) → event vector — the ONE pure materializer every path runs through (literal vector, forwarded conj, options-map :event, v/event body; interpreted and compiled; production and test). Exposed so a structural test can assert the exact dispatched vector without a browser |
EP-0036 | advanced | 004 |
presence |
Fn | (v/presence {:timeout-ms n} & keyed-children) — declarative enter/exit retention over keyed children, deliberately bounded and NOT an animation system. Every keyed child passes :mounting → :present; a key leaving the incoming set RETAINS its child :unmounting until the MANDATORY :timeout-ms fires, then removal is terminal and exactly-once. Re-entry before the timeout interrupts the exit and returns the child to :present; children hold first-appearance order and an incoming reorder is ignored. DOM-agnostic — the boundary inserts no wrapper node and stamps nothing; a presence-aware child owns its own exit styling and accessibility through presence-phase. ONE spelling, both modes: a seq form the compiled analyzer lowers, this ordinary function call in an interpreted body, one shared retention runtime (004 §Presence) |
EP-0036 | advanced | 004 |
presence-phase |
Fn | (v/presence-phase) → :mounting / :present / :unmounting — the single presence-phase read: the current phase inside a v/presence boundary, and :present OUTSIDE one, so a presence-aware child stays reusable anywhere. A render-time read (a React context read on ClojureScript); the JVM structural render always yields :present |
EP-0036 | advanced | 004 |
client-only |
Fn | (v/client-only {:fallback tpl} client-tpl) — a subtree only the BROWSER may render, and the capability-free markup that stands in its place everywhere else. :fallback is MANDATORY — there is no arity that omits it and no default, because a browser-only subtree without one is a hole in the server's output; the option roster is closed at that one key, and both refusals are :rf.error/ui-tree-malformed at the call. PHASE decides the arm: the structural render on either host is :server phase and produces the fallback (wrapped in the :rf.ui/boundary :client-only marker, never entering the client subtree), an ordinary v/mount is born :client and produces the client subtree on its first and only render, and a HYDRATING root boots :server so React adopts the server's own fallback markup, then flips ONCE — one root-scoped write swapping EVERY site in the root in a single update, strictly after the adoption commit, with one :rf.ssr/phase-flip info trace (011 §Phase flip). An INTERPRETED form: the compiled grammar REFUSES it, naming the browser-only subtree and the ladder out (extract a declared child, or keep the view interpreted), because a compiled body cannot see through the boundary to analyse what it claims (004 §Client-only subtrees) |
EP-0036 | advanced | 004 |
markup |
Var (descriptor) | [v/markup {:value hiccup}] — the declared boundary markup held as a VALUE crosses at, and — like route-link — an ORDINARY interpreted declaration rather than a mechanism. The compiled tier treats a template as a finite grammar and cannot lower a runtime value, so a compiled body handing one to a child position is refused NAMING this recovery. There is no v/interp and no automatic dynamic-markup walk: nothing in the compiled tier knows this name, so the compiled parent sees one statically named descriptor boundary, the child owns the walk and its own occurrence, and the parent's manifest marks the crossing :interpreted instead of quietly claiming the subtree. :value is anything a view body may return; :children-policy :none, because the value IS the content (004 §Cross-mode children) |
EP-0036 | advanced | 004 |
error-boundary |
Var (descriptor) | [v/error-boundary {:fallback … :reset-key … :on-error …} child] — the framework's RESETTABLE render-failure boundary; a declared descriptor mounted in a vector head, never called. It catches render-class failures below it (a Freehand child body throwing, Hiccup normalization or prop/event validation throwing, and in the browser a descendant foreign component throwing where React boundaries apply) and NOT event-handler, asynchronous or re-frame handler/sub failures, which keep their typed owners. A caught failure shows :fallback and publishes nothing from the failed render; :on-error is one event prefix the framework completes with a bounded SAFE SUMMARY (diagnostic id, failing view id, phase, fingerprint, evidence — never the exception, props, app-db or event payloads) and dispatches exactly once per failure generation, after the fallback commits. Changing :reset-key by rf= clears the failure and re-mounts the child; there is no boundary ref and no imperative reset handle. Production reporting rides a second private channel onto the always-on error axis and the frame-owned observability sink. The option roster is CLOSED — :fallback (required), :reset-key, :on-error; anything else raises :rf.error/error-boundary-bad-args (004 §Error boundaries and error egress) |
EP-0036 | advanced | 004 |
defbehavior |
M | (v/defbehavior name docstring? {:timing … :connect … :update … :disconnect … :commands … :opaque …}) — register an imperative host behavior, the ONE sanctioned way to own DOM or opaque host state, bounded to a single node. The var it binds holds the registered id (the qualified keyword :my.ns/name), not the implementation — which is what keeps a use site data: the tree records an id and the registry holds the code. The definition roster is CLOSED and :timing is the CLOSED pair :layout (before paint, for measure-then-place work) / :passive (default, after paint) — there is no third moment, because the set of moments host state may move at is part of the contract. :connect runs once at the COMMIT that mounts the node and its return ESTABLISHES the connection's PRIVATE memory; :update runs only when the committed :config moves by rf=, with :prev-config alongside, and is called for its effect on the host with its return IGNORED; :disconnect runs exactly once per committed connection, after the connection is released, so its context is inert; :commands is a finite roster reached through the :re-frame.freehand.host/command effect; :opaque declares that the behavior owns the node's descendants. Every entry takes one context map — :node / :config / :memory / :behavior / :target / :generation / :dispatch (a generation-fenced outward dispatch into the committed frame) — and there is no frame query function. ONLY :connect writes the memory: :update, a command and :disconnect receive it and their returns are discarded, because the ordinary host mutator answers nothing at all and a return that replaced the memory would have the first such call erase the instance :disconnect must release — an adapter whose host state genuinely evolves returns a mutable cell from :connect. An unknown key, a bad timing, a non-function entry or a declaration that could never run raises :rf.error/behavior-bad-args (004 §Registered behaviors and commands) |
EP-0036 | advanced | 004 |
behavior |
Var (descriptor) | [v/behavior {:use behavior-id :target … :config …} node] — attach a registered behavior to ONE node; a declared boundary mounted in a vector head, never called. The option roster is CLOSED — :use (required, the id a v/defbehavior declaration binds), :target (the caller-authored semantic id a command addresses, unique among live connections and derived from nothing — not render position, not a key path, not the DOM), :config (the public configuration) — and the child is exactly ONE element, so a declared view, a fragment, a presence boundary or text is refused. :config is DATA at every depth on BOTH hosts: a callback, a node, a ref or a preconstructed host instance is refused, because a configuration the structural tree cannot record is a use site a test and a tool cannot read. Connection is COMMIT-ONLY — the lifecycle rides a ref and an effect, so a render the host abandons performs no host work at all — and teardown is total: after the last unmount there is no connection record, no target claim, no node and no memory. On the JVM it is an INERT MARKER: the boundary node records :use / :target / :config with the decorated element as its child, nothing connects, and a command is refused. Attachment refusals split by WHEN the failure is provable, exactly as the sibling v/error-boundary does: a COMPILED attachment fails at BUILD with :rf.ui.compile/bad-behavior for what the analyzer sees statically — a non-literal opts map, an option outside the closed roster, a missing :use, more than one child, or a child that is not a single element — while the runtime :rf.error/behavior-bad-args owns what turns on a runtime VALUE or the REGISTERED definition: a :use that is not a qualified keyword or names no registered behavior, a non-map or non-data :config, and the opaque-child law (an {:opaque true} behavior's node carrying Freehand children, provable only once the definition resolves). The interpreted tier and the compiled browser path reach that runtime check IDENTICALLY, so a compiled attachment refuses the same as its interpreted twin; a v/defbehavior DECLARATION is not compiled and its refusals stay :rf.error/behavior-bad-args at registration. The command channel raises :rf.error/behavior-command-refused (004 §Registered behaviors and commands) |
EP-0036 | advanced | 004 |
defhost |
M | (v/defhost name docstring? Component {:callbacks … :children … :ssr … :map-props … :props …}) — declare a React component as a Freehand host: the SOLE public inward React boundary, and the only way to mint the third legal vector head. Before it the head existed in vector-head classification and in :rf.error/view-bad-head's own message and NO public verb produced one — an adopter could read what a host boundary was and could not build one. It is deliberately alone: D022 rejects a runtime v/host constructor, a leaf/wrapper :kind split and v/react-el BY NAME, each being the same crossing under a second spelling with a second set of laws to keep in step. The var holds a NON-CALLABLE host descriptor — a deftype, so (the-host {…}) raises :rf.error/view-called-directly rather than answering nil the way a map-shaped descriptor's lookup would — mounted [the-host {…}] and never invoked. ONE descriptor kind: "leaf" and "wrapper" describe the registered React implementation, which may itself use hooks, context, refs, effects, Suspense or a compound protocol, not two ABIs. The option roster is CLOSED and :children / :ssr are REQUIRED — Freehand never executes the registered component on the JVM, so a default would be the substrate choosing a server behaviour silently. Three DISJOINT planes: ordinary props pass shallowly and EXACTLY (:selected → selected; no case conversion, no deep CLJS→JS walk, no per-prop conversion language, and a function in an ordinary slot is refused); :callbacks names the FINITE positions as :event or :handler, never inferred from an on* name and never coerced from a bare event vector, each taking the matching v/event / v/handler carrier and receiving D008's stable per-site identity, latest committed body and frame, abandoned-render silence and retirement; children are ordinary React children in the registered component's tree, under the declared policy. One optional whole-ordinary-props :map-props adapter prepares non-portable host values in the browser, with callbacks, children and the key withheld from it and installed afterwards so it can neither supply nor replace a reserved fact. A structural render emits an honest marker — :rf.ui/host (the id), :rf.ui/host-ssr, :rf.ui/host-children (a COUNT: the caller's children cross into React's tree and are not walked here), :props (authored, a carrier recording as its opaque role marker) — and :children carries the SSR projection only: the declared fallback, or nothing. Host values, React elements, functions, refs and third-party instances are never serialized; v/render-static refuses the crossing outright, as it does a v/behavior. A {:compiled true} parent mounting a host refuses at BUILD with :rf.ui.compile/host-crossing-unsupported, naming the source and two recoveries — it never accepts the view and fails at render, and never silently walks the subtree interpreted. A malformed declaration is :rf.ui.compile/bad-defhost at macro expansion (004 §Qualified host leaves) |
EP-0036 | advanced | 004 |
re-frame.freehand/active-connections |
Fn (CLJS) | (v/active-connections) → a vector of connection maps — the behavior tool plane's first read: every live behavior connection as DATA, oldest first by the monotonic connection generation. :generation and :behavior are always present; :frame, :target and :config appear only where the connection has one, so an absent target is ABSENT rather than nil. What it OMITS is the contract — no node, no private memory, no closure and no route to one — and the omission is BY CONSTRUCTION, because each row is built from a connection's public half rather than filtered out of its whole; a projection that answered with a host instance would be the instance registry the behavior contract exists to refuse, reached through the inspection door instead of the front one. A READ, never a stream: a tool asks and is not called back, nothing here dispatches, and no application event is invented to carry a lifecycle fact. Browser-only, absent on the JVM like the mount verbs and v/->react — a structural render connects nothing, so an eternal [] would be present-and-lying where absence is honest (004 §The tool plane) |
EP-0036 | tooling | 004 |
re-frame.freehand/command-log |
Fn (CLJS) | (v/command-log) → a vector of traffic rows — the tool plane's second read, and a BOUNDED window of the recent behavior commands: {:frame :target :op :behavior :generation :outcome}, with :outcome either :delivered or :refused. A row records what the command NAMED and what the channel DECIDED, never what the host made of it — the operation's return value is IGNORED (no handle, and not the connection's private memory either) and has no representation here — and a delivered row is written BEFORE the operation runs, so a command that crashes its host is in the log rather than missing from it. REFUSALS ARE RECORDED AS FAITHFULLY AS DELIVERIES, because a projection that only saw the successes would be evidence for the one case nobody debugs; :behavior and :generation appear exactly where the channel resolved a connection, and :target / :op only where the command named them. The window is BOUNDED as contract, not as an implementation detail — an unbounded log is a retention leak dressed up as evidence — and there is no retention option, no pagination and no filter. Browser-only, exactly like v/active-connections (004 §The tool plane) |
EP-0036 | tooling | 004 |
The structural test surface — re-frame.freehand.test (Spec 008)¶
The Freehand door's TEST sibling — the second sanctioned
re-frame.freehand.*namespace (Conventions §Freehand — one public namespace, one alias, one reserved root), conventionally aliasedt. Five names query semantic values over the versioned structural tree (004B §The node schema) and one bracket opens the discardable render a state-reading view needs; none simulates behaviour.t/renderruns the real declaration — an interpreted body is walked, a compiled body's structural realisation is run — and answers the SAME tree on the JVM and in ClojureScript, in both modes, so a.cljcstructural test is a cross-host claim rather than a JVM claim wearing a.cljcextension. Handler sites are event vectors AS DATA, so "what does this button do" is an equality check: no browser, no click simulation, no flake. Frame scope is the programmer's ordinary bracket (rf/with-new-frame/rf/with-frame), state is driven withrf/dispatch-sync, and the checkpoint is a freshrender— there is no frame option and no fixture. Host-bearing behaviour (real listeners, focus, presence timing, error recovery) belongs to the mounted browser tier, not this one.Dev/test SCOPE, public SURFACE. Nothing in a production bundle may
:requireit. That is a bundling rule, not a privacy one: the six names are a published authoring surface, so they are rowed here and JVM-introspected + CLJS-probe-reconciled like every other public namespace, and a rename, a signature change or an accidental export reddens the public-API gate. Everything beneath the six — the node discrimination and the diagnostic helpers — isdefn-, so the surface is exact by construction. The contract is 008 §Freehand structural and mounted testing; node reading is 004B §Projections.
| API | M/Fn | Signature | Status | Tier | Spec |
|---|---|---|---|---|---|
render |
Fn | (t/render form) → structural tree — run form (a declared-view call [view props & children], in EITHER execution mode, or arbitrary markup) and answer its versioned structural tree. The return is always the ROOT node, a map carrying :rf.ui/tree-version; a form denoting text, several nodes, or nothing roots in a fragment. Plain serialisable data — (tree-seq map? :children tree) is the whole traversal API and the tree prints and reads back losslessly. The structural walk subscribes to nothing and dispatches nothing, so the events and subs a view under test touches must be .cljc (the standard re-frame discipline) |
EP-0036 | testing | 008 |
with-render |
M | (t/with-render body…) → the body's value — the DISCARDABLE render a state-reading view is rendered inside. v/sub is legal only during an active declared render and render is a walk rather than a host, so a view whose body reads state is refused outside this bracket with :rf.error/view-read-outside-render; inside it the view under test renders AS WRITTEN, because rewriting it to take a one-shot read would be testing something other than the view. It opens the render the host would have opened and NEVER commits it — the shell's own abandoned-render path — so the reads resolve and probe but acquire nothing: no ref-count, no watch, no cache node and no disposal obligation survives the bracket, however many times a test renders. It takes NO frame: frame scope stays the programmer's ordinary rf/with-new-frame / rf/with-frame bracket, so this surface keeps its one law about frames rather than acquiring a second (008 §Freehand structural and mounted testing) |
EP-0036 | testing | 008 |
find |
Fn | (t/find tree pred) → node or nil — the FIRST node under tree (the root included, document order) for which pred is truthy. nil threads through a missed match, so (t/attrs (t/find tree p)) nil-puns rather than throwing |
EP-0036 | testing | 008 |
find-all |
Fn | (t/find-all tree pred) → vector — every matching node in document order, [] when nothing matches. find / find-all are conveniences over the ordinary-Clojure traversal, not a selector language: pred reads node FIELDS — (:tag %) for an element, (:view-id %) for a view boundary |
EP-0036 | testing | 008 |
attrs |
Fn | (t/attrs node) → map or nil — the MERGED attribute projection, and the ONE attribute read: :attrs merged with :events on an element (collision-free by construction — :on-* routes to :events, and handler slots carry event vectors / options maps / opaque markers AS DATA), the :props map on a view boundary, {} on a fragment or trusted-HTML node (total, not an error), nil on nil. A keyword lookup on a node reads its FIELDS, never its attributes — (:on-click node) is a field miss — so intent assertion is (is (= [:cart/add 42] (:on-click (t/attrs node)))). A projection over a malformed value fails loud with :rf.error/ui-tree-malformed rather than reading a plausible answer off a broken tree |
EP-0036 | testing | 008 |
text |
Fn | (t/text node) → string or nil — the concatenation of node's text descendants in document order, descending through elements, fragments and view boundaries alike. Trusted-HTML nodes contribute nothing (their content is unparsed markup). No whitespace normalization beyond what the tree carries; nil → nil |
EP-0036 | testing | 008 |
The tool-tier reader door — re-frame.freehand.tool (Spec 004)¶
The Freehand door's TOOL sibling — the third and last sanctioned
re-frame.freehand.*namespace (Conventions §Freehand — one public namespace, one alias, one reserved root), conventionally aliasedtool, and the one published for a READER rather than an author. Six names, and the split between them is the shape of the namespace.view-manifesttakes a VALUE and is TOTAL over every value there is.read-view-manifest,read-view-dependenciesandread-view-event-sitestake a view ID, because a wire-attached inspector holds no values: it arrives with:app.people/people-listand asks what that view declares, which is a question no value-taking reader can be asked. Those four answer from the manifest the compiled tier already builds, so they compute nothing, retain nothing, and answer BEFORE anything mounts.read-mounted-viewsandexplain-renderanswer what is connected right now and why did it render, which cannot be answered that way; each row below says exactly what it reads instead. Neither door is the other's arity —(tool/view-manifest :a/keyword)answersnilbecause a keyword is not a view, and it must keep answering that — so the id-taking reads carry their own names.Why it is not
v/manifest. The difference is TOTALITY, and it is the whole of the difference.v/manifestis the APPLICATION's read — asked about a view, by code that knows it has one. A tool is asked about whatever it is holding: a var it swept out of a namespace, an id handed to it over a wire, a value from a registry it does not own. A reader that throws on the first non-view is a reader that cannot sweep, soview-manifestanswers for every value, andnilmeans "nothing statically known" — whether because the value is not a view at all or because it is an interpreted declaration with no analysis to report. A caller that needs to tell an interpreted DECLARATION from a non-view asksv/view?, which is the predicate that question belongs to.The four-axis projection, because unknown must not look like none. Every id-taking read answers INSIDE the
:scope/:basis/:complete?/:lossevidence projection each Freehand evidence surface states, stamped with the:schema, the:readthat answered, the:view-idand the:lowering. That is what lets a consumer tell a COMPILED declaration whose rosters really are empty (:basis :static-proof,:complete? true,:loss nil) from an INTERPRETED one that was never analysed (:basis :opaque,:complete? false,:loss {:reason :no-static-analysis :dropped :unknown}) — an empty roster reported as complete would be saying it found nothing where what it means is that it never looked.:loweringis NAMED rather than inferred from the basis, so a tool never has to reverse-engineer how a view was compiled out of how much its evidence could claim.view-manifestalone stands outside the projection: it answers the manifest bare, because a sweep wants the value and not a verdict about it.A READER, not a tool framework. There is no accumulator — the donor's per-occurrence evidence accumulator was ruled out permanently (rf2-drpa3.167), not deferred — no root registry, no interval log and no history store. The two structures the reads resolve through are INTERNAL,
^:no-doc, and have no time axis: a declared-view index holding ONE row per declaration, replaced when that declaration is re-evaluated, and a current-occurrence index holding ONE row per CONNECTED occurrence, dropped at disconnect. History is Spec 009's retained ring, under Spec 009's one knob, andexplain-renderfolds that ring at READ time rather than keeping a fold of its own — so a lifetime render count, a batch count, a hide-versus-unmount interval and an accumulated union of everything an occurrence ever observed are not facts this tier is missing but facts it does not have. The six therefore ARE the whole namespace by construction rather than by a^:no-doccarve-out (everything beneath them isdefn-/def ^:private), and the gate holds it that way —.cljcand host-neutral like the door and the test sibling, so JVM-introspected and CLJS-probe-reconciled, FULLY-ROWED in both directions.DEV SCOPE, public SURFACE — and no bundling rule, because the flag already is one. Unlike
re-frame.freehand.test, this namespace carries NO "nothing in a production bundle may:requireit" rule, and the difference between the two siblings is what a production:requirewould drag in. The structural test surface is a module a production bundle otherwise does not carry. This one is a projection over modules a production bundle already carries: the two internal indexes are written by thev/defviewexpansion and the commit seam, so requiring this namespace adds neither. Five of the six reads are gated onre-frame.interop/debug-enabled?and answernilunder:advanced+goog.DEBUG=false, which is also what both indexes hold there — nothing; the sixth,view-manifest, is ungated and does ship, and is a two-linewhenover the internal descriptor namespace every Freehand bundle already carries. So the boundary is the FLAG, and it is MEASURED rather than asserted: an:advancedbundle whose production entry requires this namespace and calls all six reads carries no string reachable only through them, while itsgoog.DEBUG=truetwin — the same source, one flag moved — carries every one of them (rf2-etqma). A consumer tells a productionnilfrom an unregistered view the way it tells any absence: by asking about a view it knows the application declares.
| API | M/Fn | Signature | Status | Tier | Spec |
|---|---|---|---|---|---|
view-manifest |
Fn | (tool/view-manifest x) → the compile-time manifest x carries, or nil when there is none — the tool tier's read of a declaration's static analysis: its subscription, event, render-slot and trusted-markup site rosters, its boundary crossings, its capability set, its ViewCell verdict, and the compile-tier :diagnostics findings (including the SUPPRESSED ones, carrying the author's reason, which is a suppression's only trace). TOTAL over every value, which is what makes it a tool read rather than an application one — a sweep over a namespace's publics hands this function strings, numbers, plain functions and interpreted views, and each answers nil rather than throwing. nil carries ONE meaning in every case: the compiler knows nothing statically about this value. The manifest is the declaration's own data, returned unchanged; its shape is documented once, on the compiler's structural-manifest. The ONE read here that is not dev-gated, because it is a projection of a value the caller is already holding (004 §The tool plane) |
EP-0036 | tooling | 004 |
read-view-manifest |
Fn | (tool/read-view-manifest view-id) → the same manifest for the view DECLARED as view-id, inside the four-axis projection that says how far to trust it; nil for an id this build declared no view under, and nil in a production build. The id-taking twin of view-manifest, for the caller that holds no values: the id resolves through the internal declared-view index each declaration records itself in, so an inspector that attaches LATE, to an already-running application, still sees every view it declared. The manifest rides VERBATIM under :manifest — the declaration's own data, not a second projection of it, because one value published twice cannot drift while two projections of one value eventually do. An interpreted declaration answers :manifest nil on the :opaque basis with an explicit :no-static-analysis loss, never an empty roster that would read as a clean bill of health (004 §The tool plane) |
EP-0036 | tooling | 004 |
read-view-dependencies |
Fn | (tool/read-view-dependencies view-id) → the reactive dependency SITES that view declares — its v/sub sites — read from the manifest and so answerable BEFORE anything mounts; nil for an unknown id, and nil in a production build. SITES, not reads: the roster is one entry per LEXICAL site, so a site inside a keyed list is one entry however many times it runs, and what a render actually subscribed to is a different quantity that is not derivable from this one. Each entry states its own QUERY-SHAPE honesty — a fully-literal query is projected as the value it is (:dynamic? false :query …), while a query carrying a captured local or a call is :dynamic? true with its literal :query-id still shown where the head is one and the runtime argument left unsaid rather than invented. :source-coord is total or absent — a whole {:file :line :column}, or omitted entirely for a declaration that carries no reader location — never partial (004 §The tool plane) |
EP-0036 | tooling | 004 |
read-view-event-sites |
Fn | (tool/read-view-event-sites view-id) → the event-handler SITES that view declares, read from the manifest; nil for an unknown id, and nil in a production build. SITES, not dispatches, on the same lexical terms as its sibling. Each row says WHERE it dispatches from AND what it dispatches — :prop, :classification, :serializable?, :sync?, :handler and :event-id — with the literal/dynamic split applied to the handler form the way the sibling applies it to a query: a literal handler is projected verbatim, and a handler carrying a captured local, a call, or a callback BODY projects as the :opaque marker with :event-id still shown where the authored form has one. :site-facts accompanies the roster and NAMES the closed set of facts a row states, because naming what an entry states is how an empty-handed row says which hand is empty (004 §The tool plane) |
EP-0036 | tooling | 004 |
read-mounted-views |
Fn | (tool/read-mounted-views) → every Freehand occurrence CONNECTED RIGHT NOW, inside the projection that says how far to trust the roster; nil in a production build. THE READ THAT NEEDS LIVE STATE, because it is the one question a fire-and-forget commit sink cannot answer: an inspector attaches late, so a consumer that starts listening at attach time could only learn about commits from that moment on. Two rows sharing a :view-id with different :occurrence keys are two live occurrences of one view — the fact a declaration-keyed index structurally cannot state. CURRENT STATE, not history: a row carries the LATEST committed :generation and the reads THAT commit staged, WITHOUT the values they returned (a value is application data, and an evidence read is not a second egress path for it), and :occurrence-facts names the closed set. No argument, deliberately — the question is what is mounted. :complete? true is exact about UNDER-reporting; OVER-reporting is stated rather than claimed away, since a host torn down without disconnecting its cells leaves rows behind until it does, and each row's :generation and :at are what let a reader see a stale one for what it is (004 §The tool plane) |
EP-0036 | tooling | 004 |
explain-render |
Fn | (tool/explain-render) / (tool/explain-render view-id) → why the live occurrences of a view rendered, folded from Spec 009's RETAINED WINDOW at read time; no argument spans every current occurrence; nil for an id this build knows no view under, and nil in production. NOTHING IS RETAINED TO ANSWER THIS — the fold runs against the per-frame retained-event ring Spec 009 already owns, under its single knob (:rf.trace/events-retained), so there is no second window, no second knob and no second place a payload could survive. The answer is :cause and it is a JOIN, not a guess: the occurrence row recorded the cascade in scope when the commit ran, so the explanation looks that exact run up in the window. The OUTER roster is complete while an inner explanation often is not, and the two are separate projections — a disabled or empty ring, a provably EVICTED run, and a commit that was never correlated to any run are each reported as :loss with :reason :cap or :uncorrelated, never as an empty-but-confident answer. :candidates is offered in every case and is deliberately NOT the answer: presenting a lead as a cause is exactly the shape a nil :cause reported complete would have (004 §The tool plane) |
EP-0036 | tooling | 004 |
Pure form transitions — re-frame.freehand.form (Spec 004)¶
The Freehand door's FORM sibling — a sanctioned
re-frame.freehand.*namespace (Conventions §Freehand — one public namespace, one alias, one reserved root), conventionally aliasedform. It is the accepted product-completion setpoint's DC-03 slice, kept inside Freehand's own distribution by ER-04 rather than shipped as a second artefact.PURE, and that is the contract rather than an implementation note. It registers no event and no subscription, owns no atom, mounts nothing and reads no frame; every operation is
(f form …) -> form. An application owns event registration, validation policy, app-db placement and rendering, exactly as it does today — this is not a form runtime, and the programme's non-goals (no atom, no validator engine, no schema renderer, no generic local store) stand.The value is a plain map with public slots —
:baseline/:draft/:visited/:edited/:resets/:errors/:submit— read the ordinary way. There is no reader for each key; the two functions that ARE readers derive something a key lookup does not give you. A leaf path is a non-empty VECTOR, so a row addressed by its stable domain id survives an insert, a delete and a re-sort. Per 004 §Pure form transitions.
| API | M/Fn | Signature | Status | Tier | Spec |
|---|---|---|---|---|---|
init |
Fn | (form/init) / (form/init baseline) → a fresh form whose baseline AND draft are baseline, with nothing visited, nothing edited, no leaf reset, no errors and no submit attempted. A later authoritative load is seed or rebase — re-initing on a late reply is the wipe the model exists to stop |
EP-0036 | front-porch | 004 |
edit |
Fn | (form/edit form path value) → the user changed the leaf at path. Writes the draft leaf and marks it EDITED unconditionally, including at a value equal to the baseline: edited means the user has been typing here, which is what protects the leaf from a late seed |
EP-0036 | front-porch | 004 |
visit |
Fn | (form/visit form path) → the user has been at the leaf and left it. Marks it VISITED, which is what lets that leaf's error appear, and changes no value — so a tab-through reveals and protects nothing |
EP-0036 | front-porch | 004 |
seed |
Fn | (form/seed form values) → a late authoritative load, taken LEAFWISE with every EDITED leaf skipped in both the draft and the baseline. Unmentioned leaves are untouched, a keyed row is addressed by its stable domain id, a seeded leaf clears the error its old value carried, and nothing is marked visited or edited. The fix for the whole-map assoc that erases keystrokes |
EP-0036 | front-porch | 004 |
rebase |
Fn | (form/rebase form values) → the baseline moved and the draft STANDS: an acceptance, not a rejection. Clears the edited mark only where the draft now agrees with the new baseline, advances no reset revision, and settles a pending submit |
EP-0036 | front-porch | 004 |
reset |
Fn | (form/reset form) / (form/reset form path) → discard the user's work and ADVANCE the reset revision of every leaf discarded. The revision is the point: a caller rejects a draft by reasserting the value it already had, and advancing the generation is the one signal equality is blind to. Scoped to a leaf, it advances that leaf's generation and no other |
EP-0036 | front-porch | 004 |
set-errors |
Fn | (form/set-errors form errors) → replace the errors with a map of LEAF PATH to structured message. Replacement rather than merge, because that is what a validation pass produces. What counts as an error is the application's; Freehand ships no validator |
EP-0036 | front-porch | 004 |
attempt-submit |
Fn | (form/attempt-submit form) → always marks the form attempted, which reveals every error at once, and — when there are none — opens a pending save at [:submit :pending] stamped with the attempt number. That stamp is the staleness fence: a reply carrying a superseded number is inert. An invalid form remains attemptable |
EP-0036 | front-porch | 004 |
reset-key |
Fn | (form/reset-key form path) → the leaf's current reset revision, 0 for a leaf never reset — the generation a buffered control fences against through v/controller-current? |
EP-0036 | front-porch | 004 |
field |
Fn | (form/field form path) → the NARROW read: {:re-frame.freehand/path :value :baseline :error :visited? :edited? :show-error? :reset-key} for ONE leaf and nothing about any other. ONE reg-sub for a form of any size; :show-error? is the reveal policy (visited, or a submit attempted) and not the validation policy. Reading the container instead makes keystroke latency scale with the size of the form |
EP-0036 | front-porch | 004 |
path-key |
Var | form/path-key → :re-frame.freehand/path, the reserved key a field projection carries its leaf path under — the projection's identity as well as its data |
EP-0036 | advanced | 004 |
The first-party control kit — re-frame.freehand.controls (Spec 004)¶
The Freehand door's CONTROL sibling — a sanctioned
re-frame.freehand.*namespace, conventionally aliasedc. The accepted setpoint's DC-04 slice: a small kit grown through serious witnesses rather than a catalogue promise, shipped inside Freehand's distribution by ER-04. Skins, layouts and application compositions are meant to be copied and adapted; the correctness machinery — the narrow read, the generation fence, the composing-Enter rule and the exact release — is not copy-only.Neither control has a
:valueprop. The only value either accepts is aform/fieldleaf projection, and a caller's forwarded attributes go throughv/spread-safe, whose deny law refusesvalueon the component's own controlled element in every build. The container read is not discouraged; it is unspellable. Both declarations are inside the compiled grammar as they stand, so promotion is a keyword rather than a rewrite. Per 004 §The first-party control kit.
| API | M/Fn | Signature | Status | Tier | Spec |
|---|---|---|---|---|---|
re-frame.freehand.controls/field |
Component | [c/field {:field … :on-edit … :on-visit …}] → one native <input> inside the controlled-input door, publishing every keystroke. :field is a form/field projection; :on-edit is completed with ::v/value; :on-visit is the OPTIONAL blur. Every other prop is forwarded through v/spread-safe. It stamps one attribute of its own beyond the contract — aria-invalid, from the projection's :show-error? — and owns no label, message or layout |
EP-0036 | front-porch | 004 |
re-frame.freehand.controls/buffered-field |
Component | [c/buffered-field {:field … :on-edit … :on-commit … :on-cancel …}] → the same leaf, committing on blur and on non-composing Enter. The draft is the FORM's draft leaf, not a host slot or a controller record, so a re-render, an abandoned candidate and a hot reload are uneventful; :on-commit carries the leaf's reset revision so the handler decides against committed state |
EP-0036 | front-porch | 004 |
key-intent |
Fn | (c/key-intent key composing?) → :commit, :cancel or nil — the buffered field's keyboard law as a pure function. A COMPOSING ENTER COMMITS NOTHING: the Enter that accepts an input-method candidate belongs to the IME, and so does a composing Escape. Public and pure so the law is provable by calling it, with no browser |
EP-0036 | advanced | 004 |
composing? |
Fn | (c/composing? e) → boolean — should this host keyboard event be treated as composition-owned? The scalar key-intent takes, read off the event: the standard isComposing flag asked of nativeEvent where there is one (React 19.2's synthetic keyboard event does not carry it), ORed with the keyCode 229 compatibility fallback for engines that report the candidate-accepting Enter with isComposing false (WebKit bug 165004, fixed April 2026 via bug 311717). Not a composition lifecycle tracker — one boolean about one event. Public so a kit member with its own keyboard grammar shares the host adaptation rather than mirroring it |
EP-0036 | advanced | 004 |
commit-key |
Var | c/commit-key → "Enter" |
EP-0036 | advanced | 004 |
cancel-key |
Var | c/cancel-key → "Escape" |
EP-0036 | advanced | 004 |
release |
Fn | (c/release db form-path …) → the causal owner's clear: the named form slices leave db together, in ONE value. Exact — never "every form" — creating nothing where a path's parent is absent and changing nothing when it runs twice. It exists because there is no lifecycle cleanup hook and there will not be one: unmount is not a domain event, and the absence of a mounted occurrence is not proof of orphaning |
EP-0036 | front-porch | 004 |
The splitter — re-frame.freehand.splitter (Spec 004)¶
The control kit's POINTER witness — a sanctioned
re-frame.freehand.*namespace (Conventions §Freehand — one public namespace, one alias, one reserved root), conventionally aliasedsplit.re-frame.freehand.controlsholds the kit's FORM controls: both read aform/fieldprojection and both sit inside the controlled-input door. A splitter reads no form and enters no door — it is the other half of the accepted setpoint's DC-04, a control whose gesture is a POINTER DRAG and whose keyboard path has to be the same control rather than a second one bolted to the side of it.An offer is not an intent. A pointer offers moves at the host's rate, so every offer is settled first — clamped to
:min/:max, quantized to:step— and an intent is produced only where the settled value DIFFERS from the one on screen. The application picks the stream it wants by wiring or not wiring:on-preview; there is no throttle, no scheduler and no timing verb, because the reduction is arithmetic you can call (settle).The whole gesture is ordinary application data.
{:at … :baseline … :dragging? …}at a path the application chose, moved by the five transitions below. Pointer capture is ROUTING rather than authority — it is what lets the control add nowindowlistener and therefore have nothing to remove at unmount — and liveness is decided in the HANDLER against committed state, so a cancel BEATS a late preview rather than racing it.ADVANCED throughout, and deliberately: a resizable pane divider is opt-in layout machinery, not the day-one set the Guide and the skills load. Per 004 §The first-party control kit.
| API | M/Fn | Signature | Status | Tier | Spec |
|---|---|---|---|---|---|
re-frame.freehand.splitter/splitter |
Component | [split/splitter {:split … :on-start … :on-preview … :on-commit … :on-cancel …}] → one role="separator" element between two panes the CALLER lays out. :split is the value below; :on-commit carries the settled position and a call site wiring only that is complete. Children, cursor, grip and hit area are ordinary CSS and ordinary children — this is a control, not a layout DSL, and there is no h-split here to wrap your panes |
EP-0036 | advanced | 004 |
re-frame.freehand.splitter/settle |
Fn | (split/settle x bounds) → the raw position x as the split it actually NAMES: clamped into :min/:max and quantized to :step. Total over the reals. This is the whole two-clock reduction — an offer that settles to the value already rendered produces no intent, which is most of them |
EP-0036 | advanced | 004 |
re-frame.freehand.splitter/key-intent |
Fn | (split/key-intent key geom) → [:step ±1], [:page ±1], [:to :min], [:to :max], or nil for every key the splitter does not claim. Only the ARROW keys are physical, so only they are mirrored under :rtl?; Home/End/PageUp/PageDown name the VALUE and a mirrored Home would be a bug in every writing direction. Distinct from controls/key-intent, which is the buffered field's law — two laws, two namespaces, no prefix |
EP-0036 | advanced | 004 |
re-frame.freehand.splitter/intent-at |
Fn | (split/intent-at intent at bounds) → the split a key-intent move names, applied to at; nil where intent is nil. It ends at settle, which is the point: the keyboard reaches positions through the same clamp and the same quantum a pointer offer does, so the two paths cannot drift apart by rounding differently |
EP-0036 | advanced | 004 |
re-frame.freehand.splitter/fraction-at |
Fn | (split/fraction-at point rect geom) → the RAW fraction of rect the point names along the :orientation axis, mirrored under :rtl? by the same mirror the arrows get. Unclamped and unquantized — settle is what decides — because keeping the two apart is what lets a bound be proven as a bound rather than inferred from a clamp that already happened. nil where the rect has no extent on that axis |
EP-0036 | advanced | 004 |
re-frame.freehand.splitter/init |
Fn | (split/init at) → {:at at :baseline at :dragging? false}. :at is what renders, :baseline is what a cancel restores, :dragging? is the application's half of the gesture fence. Three keys, all readable, and the whole value serializes |
EP-0036 | advanced | 004 |
re-frame.freehand.splitter/start |
Fn | (split/start s) → mark the gesture live and take the current :at as the baseline a cancel would restore. IDEMPOTENT, because a second pointerdown while a drag is live is a second finger rather than a new gesture, and re-baselining there would quietly make the first finger's movement unrestorable |
EP-0036 | advanced | 004 |
re-frame.freehand.splitter/move |
Fn | (split/move s at) / (split/move s at bounds) → move a LIVE gesture. A move arriving when no gesture is live changes nothing — the pure half of the two-owner fence, not defensive coding: an accepted offer and the frame it is accepted against are a tick apart |
EP-0036 | advanced | 004 |
re-frame.freehand.splitter/commit |
Fn | (split/commit s at) / (split/commit s at bounds) → end the gesture KEEPING it: :at and :baseline both settle there and the gesture is no longer live. It commits from a non-live state too, which is what makes a keystroke a whole gesture — one call, no start to pair with, the same terminal the pointer reaches |
EP-0036 | advanced | 004 |
re-frame.freehand.splitter/cancel |
Fn | (split/cancel s) → end the gesture, restoring the baseline it started from. Ending twice is ending once, and cancelling a gesture that never started restores the baseline the value already had — so an application may cancel from a route change or an Escape of its own without asking whether a drag is in flight |
EP-0036 | advanced | 004 |
re-frame.freehand.splitter/bounds |
Fn | (split/bounds b) → b folded over default-bounds. The ONE place a caller's partial bounds map is completed, so nothing downstream has to ask whether a key is there |
EP-0036 | advanced | 004 |
re-frame.freehand.splitter/default-bounds |
Var | split/default-bounds → {:min 0.0 :max 1.0 :step 0.01 :page-step 0.1}. :step is the control's QUANTUM and BOTH paths use it — an arrow moves one, a pointer offer is accepted only when it crosses one. A single quantum is what makes "a keystroke and a one-step drag agree" a statement about the control rather than a coincidence of two independently-rounded numbers |
EP-0036 | advanced | 004 |
re-frame.freehand.splitter/component-id |
Var | split/component-id → "re-frame/splitter", the data-component scope every part id is addressed under |
EP-0036 | advanced | 004 |
re-frame.freehand.splitter/parts |
Var | split/parts → #{"separator"}, the PUBLIC part roster — a deliberate subset, and here a subset of one, because the control renders one element and everything inside it is the caller's children. A part id is API: a stylesheet reaching [data-part="separator"] breaks silently if it is renamed, exactly as a prop does |
EP-0036 | advanced | 004 |
The fixed-size virtual collection — re-frame.freehand.collection (Spec 004)¶
The Freehand door's VIRTUALIZATION sibling — a sanctioned
re-frame.freehand.*namespace (Conventions §Freehand — one public namespace, one alias, one reserved root), conventionally aliasedcoll. Folding it intore-frame.freehand.controlswas considered and rejected: virtualization is a cohesive capability with an audience of its own, and a library author composing their own list needs the window arithmetic without the input-field kit.Two layers, because virtualization is not a widget.
virtual-collectionis the ENGINE and it is SEMANTIC-NEUTRAL — scroll host, full-height canvas, keyed positioned row shells, window arithmetic — and it names no ARIA role at all.virtual-listis the listbox built on it:role="listbox", options, selection,aria-activedescendant, and nothing else, with no second window arithmetic and no second scroll host. The split is arithmetic against evidence rather than anticipation: W3C's own listbox pattern sends a collection of INTERACTIVE rows to the grid pattern, whose cell and focus contract is materially different, so a virtual list hard-coded tooptioncannot host an editing grid without either a second engine or dishonest semantics.
:scroll-offsetis CONTROLLED, in both directions. The render picks the window from the caller's value, and the engine's guarded:layoutwrite lands that value on the LIVE viewport before paint whenever the two disagree. Each direction alone is a bug — render alone leaves a freshly mounted viewport atscrollTop0 under rows painted at 3072px; the DOM alone leaves the offset somewhere no epoch, snapshot or tool can see. There is no opt-out, because the un-reconciled mode is the blank-viewport bug rather than a lighter configuration of the same idea.ADVANCED throughout, and that overturns a
:front-porchreading ofvirtual-list.:front-porchis the tight day-one surface the Guide and the skills load by default; fixed-size virtualization is opt-in scalability machinery an ordinary list never needs. Four public names, and no fifth. The engine's own row shell, its positional DOM-id scheme and its guarded layout write (virtual-row,row-dom-id,reconcile-scroll) are PRIVATE — each is mounted, called or attached by this namespace and by nothing else, so producer and consumer share a namespace and none of them is a call-site surface. The supported contract for a row's address is the RENDERED relationship: the listbox ownsaria-activedescendant, and each mounted option owns its id,aria-posinsetandaria-setsize. Per 004 §Controller state is ordinary frame data.
| API | M/Fn | Signature | Status | Tier | Spec |
|---|---|---|---|---|---|
re-frame.freehand.collection/virtual-collection |
Component | [coll/virtual-collection {:row-keys … :row-extent … :viewport-extent … :scroll-offset … :row … :on-scroll … :attrs …}] → the ENGINE: a scroll host, a canvas of the collection's full height, and the keyed positioned shells of the visible window. It names NO ARIA role, and that is the whole point of it — the role, the selection, the keyboard and the accessible position are supplied by whoever calls it, through :attrs for the viewport and through the row slot for the rows. The slot is invoked with the row's KEY, its ABSOLUTE INDEX and the collection's TOTAL — the three facts virtualization makes hard to state honestly — and what those are spelled AS is the caller's |
EP-0036 | advanced | 004 |
re-frame.freehand.collection/virtual-list |
Component | [coll/virtual-list {:id … :row-keys … :row-extent … :viewport-extent … :scroll-offset … :row … :on-scroll … :active-index … :on-key … :on-activate …}] → a fixed-extent virtual LISTBOX: virtual-collection wearing role="listbox", and nothing more. It runs no window arithmetic of its own, owns no scroll host, positions nothing and measures nothing. The control never receives an item — :row-keys is the ordered vector of IDENTITIES and :row mounts the caller's declared row view, so a row's content is read by the row's OWN boundary and an edit to one row invalidates one boundary. The viewport is the focus holder and the active row is named by aria-activedescendant, because roving focus is the pattern virtualization breaks |
EP-0036 | advanced | 004 |
re-frame.freehand.collection/window |
Fn | (coll/window {:item-count … :row-extent … :viewport-extent … :scroll-offset … :overscan …}) → {:first … :count … :extent …}, the visible window as pure arithmetic. :count DOES NOT DEPEND ON :item-count — it is bounded above by ceil(viewport/extent) + 1 + 2*overscan for every collection size, which is the control's whole cost claim stated as arithmetic. Total over nonsense: a negative or absent input reads as its floor, an offset past the end clamps to the last full screen, and an empty collection answers :count 0 |
EP-0036 | advanced | 004 |
re-frame.freehand.collection/reveal-offset |
Fn | (coll/reveal-offset geometry index) → the SMALLEST scroll offset that puts a row wholly inside the viewport. A row already whole on screen answers the CURRENT offset unchanged, which is what stops arrow-key navigation inside the window from jerking the list. It is a function rather than a behaviour of the control because the decision is the application's — which key moves the active row, whether it wraps, and whether moving it scrolls at all are policy, and a control owning them would be a keyboard framework |
EP-0036 | advanced | 004 |
Compiled views — re-frame.ui (Spec 004D)¶
Additive, staged. The
re-frame.uinamespace (aliasui, artefactday8/re-frame2-ui) is the compiled-view substrate (Spec 004D). Each row carries the stage (S1–S7) whose conformance slice asserts it (per Spec 004D §Stage conformance profiles); S1 rows are live now, later-stage rows are declared-not-yet-asserted. The blessed S1 export set is#{defview custom-element sub raw html raw-fn spread mount create-root render! hydrate-root unmount! frame-root}; S2 adds the SCOPE-onlyframe-providerform, the first-partyadapterVar, and theframeops-bundle body form; S3 adds the committed handler formevent, the internal compiled render-slot pairrender-fn+slot, the literal safe-spread policyspread-safe, the host hookslocal+ref+effect+dispatch-fn, the explicit callback-boundary formshandler+error-boundary+client-only, androute-link(an ORDINARY compileddefviewover the routing-owned late-bound link seam — the first framework-provided compiled view); S4 addspresence+presence-phase, S5 addsrender-static, and S6 adds the outward migration bridge->react(shipped ahead of its wave, rf2-u53yy.2). So the current livere-frame.uins-publicsis exactly that set plusframe-provider,adapter,frame,event,render-fn,slot,spread-safe,local,ref,effect,dispatch-fn,handler,error-boundary,client-only,route-link,presence,presence-phase,render-static, and->react(thirty-two vars — the Spec 004Dexport-surface-is-exactly-the-blessed-setconformance). Thereg-viewfamily (§Registration) is unchanged; it belongs to the stock-Reagent compatibility/interop tier, a first-class, actively-supported adapter that lives on alongsidere-frame.ui.
| API | M/Fn | Signature | Stage | Tier | Notes |
|---|---|---|---|---|---|
adapter |
Var (map) | (rf/init! ui/adapter); exact ten-function adapter contract plus :kind :rf.adapter/ui |
S2 | front-porch | The first-party compiled-view boot adapter. CLJS is the watchable native React realization; JVM is the headless atom realization under the same public Var and discriminator. The internal observation port is not an extra adapter key. Destroying this adapter owns total teardown of every public compiled Root (exact-generation registry claim, ViewCells, observation handles, and host container fallback after a throwing unmount), not merely the generic React spine. |
defview |
M | (ui/defview name ?doc ?opts [props] body+) |
S1 | front-porch | The one component form — a pure (props) → template fn; defs a Var AND registers under the registrar :view kind. Zero/one arg (a props map; no positional args). Closed opts #{:props :id :display-name}. Memoised by default on the generated rf= comparator. |
custom-element |
M | (ui/custom-element tag {:properties #{…}}) |
S1 | advanced | Declares a custom element's property-vs-attribute classification; :properties is the entire v1 grammar (options map closed). |
sub |
Fn | (ui/sub [:query …]) → value |
S2 | front-porch | Compile-indexed reactive read; one React bridge per view; sub in loops is a compile error. |
frame |
Fn | (ui/frame) → ops bundle |
S2 | advanced | The compiler-owned ops-bundle body form — inside a view returns the frame-locked {:frame :dispatch :dispatch-sync :subscribe} bundle bound to the committed frame; identity is stable for one frame incarnation and its ops fail loud (:rf.error/frame-destroyed) once destroyed. Render-time and finite like sub (loops / deferred callbacks / root expressions are compile errors); outside a view hold a frame with rf/capture-frame. |
event |
Fn | (ui/event [e] … [:vector …]) |
S3 | advanced | The compiler-owned committed handler form for a DOM/custom-element :on-* site whose body needs the live native event — the body runs after commit and its result is the event vector to dispatch (nil ⇒ no dispatch, a filter). At a compiler-proven controlled-input site (a literal :value/:checked co-present with the handler) a synchronous ui/event returning a vector rides the ONE synchronous door beside a literal vector handler — the shape a reusable input control uses to append its live payload to an event prefix; any other synchronous result is a loud runtime diagnostic. A direct call fails loud (:rf.error/ui-tree-malformed) — it exists for symbol resolution only. The literal [:event … :rf.ui/value] vector is the front-porch 90% intent form (Spec 004D). |
handler |
Fn | (ui/handler [x] body…) |
S3 | advanced | The explicit IMPERATIVE committed callback — the imperative sibling of ui/event. Its body runs after commit and its return is IGNORED (no dispatch of a returned vector); it does imperative work. Like every committed callback it is PER-SITE STABLE and reads the COMMITTED values (the stale-closure boundary law). Legal at a DOM/custom-element :on-* site (the explicit spelling of the bare-fn shorthand; the native event binds its parameter) and at a FOREIGN-component or INTERNAL-view prop (the invoker's arguments bind through). At an internal-view seam it gives a bare fn prop the per-site stable identity a fresh closure lacks (C-13a). A direct call fails loud (:rf.error/ui-tree-malformed) — it exists for symbol resolution only. |
render-fn |
Fn | (ui/render-fn [args…] template) |
S3 | advanced | A compiler-owned PURE render-slot callback for an internal library seam. Its body is a template lexically visible at the consumer call site, so both emitters COMPILE it (closed grammar, no runtime hiccup); it renders from its arguments alone. Legal in exactly two positions: a component call-site prop value (the parameterized markup a reusable view accepts) and a ui/slot argument; the seam invokes it through ui/slot. The body is pure render phase — sub/frame (and dispatch/hooks/refs) inside are compile errors (:rf.ui.compile/impure-slot-body); a statically-referenced internal view head REMAINS legal, so a stateful part is a pure slot body mounting a static defview that owns its own state. A direct call fails loud (:rf.error/ui-tree-malformed) — it exists for symbol resolution only. |
slot |
Fn | (ui/slot render-fn-value arg…) |
S3 | advanced | The compiler-owned invocation of a ui/render-fn value at a library seam (child position). The first argument is an inline ui/render-fn or a value carried through a prop; only a ui/render-fn or nil is accepted — any other value is a loud didactic error (:rf.error/ui-tree-malformed). nil renders nothing; a render-fn renders with the supplied args and its output participates in the surrounding children exactly like any other child (child-like memo cost). The load-bearing middle of the library customization taxonomy: data props · pure render slots · registered stateful views (gated). A direct call fails loud — it exists for symbol resolution only. |
local |
Fn | (ui/local init) → [value set! update!] |
S3 | front-porch | Host component-local ephemeral state, outside re-frame2 epochs (the narrow placement rule, Spec 004D). The P0-1 three-tuple: set! stores its argument exactly (a stored fn is a value, never an updater); update! applies (f current & args) to the latest host state so several same-turn writers compose. Setter/updater are host-only — a render-phase mutation fails loud. |
ref |
Fn | (ui/ref) / (ui/ref init) → host ref object |
S3 | front-porch | The substrate-native DOM-node ref — the everyday "I need the DOM node" primitive (focus, measurement, a third-party widget), promoted from the re-frame.ui.react ref hook (rf2-u53yy.9). Bound in a defview top-region let and handed to a :ref position; read/write via (.-current node). Assignment never re-renders (its reason to exist beside local); the preferred object ref for a :ref slot; no deps. A host hook lowered by the same finite-site machinery, obeying the position law and contributing its :ref kind to the hook signature (Spec 004D). On the JVM structural render it is an inert ref (current nil, stays nil). Callback refs via ui/raw-fn remain the expert seam. A direct call fails loud (:rf.error/ui-tree-malformed) — it exists for symbol resolution only. |
effect |
Fn | (ui/effect [deps…] body) / (ui/effect :connect body) |
S3 | advanced | Passive host effect; rf= deps; cleanup on dep change/disconnect/unmount. No "once"/"mount" name. |
dispatch-fn |
Fn | (ui/dispatch-fn) → fn |
S3 | advanced | The stable committed-frame dispatcher for imperative callbacks; fails loud in every non-connected state (:rf.error/dispatch-disconnected). |
presence |
M | (ui/presence {:timeout-ms n} children) |
S4 | advanced | Declarative enter/exit retention; :timeout-ms mandatory. |
presence-phase |
Fn | (ui/presence-phase) → :mounting/:present/:unmounting |
S4 | advanced | The single presence-phase read; :present outside a boundary. |
raw |
Fn | (ui/raw react-element) |
S1 → S4 | advanced | Embed an existing React element (child position; SSR needs a client-only sibling fallback). |
raw-fn |
Fn | (ui/raw-fn f) |
S3 | advanced | Pass a callback through by identity (callback-ref form; identity-as-protocol APIs). |
html |
M | (ui/html string) |
S1 | advanced | Trusted markup — the one explicit escaping bypass; both emitters treat it identically; the site is recorded in the compiler manifest. |
spread |
Fn | (ui/spread base overrides) |
S1 | advanced | The one generic runtime prop-map conversion (the single dynamic-map path, driven by the owning rule table). |
spread-safe |
Fn | (ui/spread-safe owned caller) |
S3 | advanced | The literal safe-spread policy — a component library forwards a consumer's runtime attr map onto an internal element without clobbering owned props or forfeiting the sync door. owned is a LITERAL props map (analysed as element props, so a controlled owned site keeps the door); the structural/controlled/identity keys :key/:ref/:value/:checked and the owned :on-* handlers are denied to caller in EVERY build — a literal offender is a compile error (:rf.ui.compile/spread-safe-owned-key), a runtime offender throws (:rf.error/ui-tree-malformed), neither elided in an advanced build. Allowed :on-* classify through the handler table; aria-*/data-*/title/:class/:style convert per the 004B table; owned props win, :class composes owned-first. General ui/spread remains the visible-cost escape. A direct call fails loud — it exists for symbol resolution only. |
->react |
Fn | (ui/->react view) → React component |
S6 | advanced | Export a compiled view as a React component — the OUTWARD migration bridge (ui/raw is the inward half). Live (rf2-u53yy.2), ahead of the S6 migration wave: memoised per view identity (repeated calls return the identical object); creates no ui React root, mints no manifest, runs no host preflight (the exported subtree renders inside the host root the foreign parent owns); the reserved frame prop resolves by OWN-PROPERTY presence (rf2-01rwd): an OWN frame (a frame-id keyword or live frame value) scopes without owning it and is ALWAYS validated — an explicit frame={null}/frame={undefined} fails loud with :rf.error/no-frame-context rather than adopting the ambient frame, a malformed target is :rf.error/bad-frame-provider-arg, a target naming no live frame is :rf.error/frame-provider-frame-absent, all self-attributed to re-frame.ui/->react; an OMITTED frame resolves from the ambient React-context chain (or fails loud with :rf.error/no-frame-context); ONE shallow props-conversion rule (each own-enumerable prop → the view's ABI slot by exact name, minus the reserved frame), so children and ref pass through preserved. JVM: :rf.error/jvm-host-op. |
error-boundary |
M | (ui/error-boundary {:fallback … :reset-key … :on-error […]} child) |
S3 | advanced | The explicit error component; :on-error dispatches after the failing commit through a captured live frame. |
client-only |
M | (ui/client-only {:fallback tpl} client-tpl) |
S3 → S5 | advanced | Browser-only subtree; mandatory capability-free fallback; one root phase-flip swaps all sites (per 011 §Phase flip). |
route-link |
Fn | [ui/route-link {:to :route-id …html-attrs} & children] |
S3 | advanced | A navigation anchor — the compiled counterpart of the stock-Reagent rf/route-link (§Routing). An ORDINARY compiled defview, not a compiler intrinsic and not a re-frame.ui → routing dependency: it renders a real <a href=…> with the route's strategy-encoded href (copy-link / open-in-new-tab / keyboard activation / no-JS nav all work) and, on a plain in-app left click, dispatches :rf.route/url-requested to the committed frame (:source :router). :to required; :params/:query/:fragment feed the href + payload; :prefetch :intent warms the destination's resources on credible user intent. Those, together with :on-click, are the CONTROL keys — stripped before DOM emission, so every other key (:class, :target, :download, :aria-label, …) passes through to the <a>. Modifier / middle-click and native anchors (:target other than _self, :download) defer to the browser; a caller :on-click runs first and may veto. The routing calculation (href encode, dispatch payload, native detection) and the click law live in the OPTIONAL routing artefact behind the substrate-neutral :routing/link-model / :routing/activate-link! late-bound seam, consumed through core's late-bind registry (ui -> core late-bind <- routing); rendering it without day8/re-frame2-routing fails loud with :rf.error/routing-artefact-missing. JVM/SSR renders the handler-free path-form shell. Behavioural contract + full conformance matrix live in 012 — routing owns the law. |
mount |
M | (ui/mount root-form dom-node ?opts) |
S1 | front-porch | Macro over a literal root form; carries root identity in opts (Root Descriptor v1, Spec 004C). |
create-root |
M | (ui/create-root dom-node opts) → Root |
S1 | advanced | Identity fixed for the Root's lifetime; authored :root-id required — no root form to derive from, so a missing id is :rf.ui.compile/missing-root-id and :disambiguator is invalid (Spec 004C). |
render! |
M | (ui/render! root root-form) |
S1 | advanced | Render/re-render the literal root form into a Root. |
hydrate-root |
M | (ui/hydrate-root dom-node root-form ?opts) → Root |
S1 → S5 | advanced | Hydrating mount; identity comes FROM the manifest (supplying client identity opts is :rf.error/root-manifest-invalid). |
unmount! |
Fn | (ui/unmount! root) |
S1 | advanced | Total teardown; unregisters the root-id. |
frame-root |
— | [ui/frame-root {:id …} …] |
S1 → S2 | front-porch | Top-region root-form marker; scopes an already-live frame (frames are created at host preflight, per 002). |
frame-provider |
— | [ui/frame-provider {:frame f} …] |
S2 | front-porch | Template SCOPE marker; scopes a subtree to an already-live frame (:frame a runtime frame id / value) through the shared React context — creates nothing (roots ensure, providers scope; the 002 split), fails loud when the frame is absent. Descendant boundaries resolve it ambiently via :adapter/current-frame (rf2-vxgfnd.24). |
element / view / portal |
— | — | [WAVE-2] | — | Runtime-chosen element/component (ui/element), registry-addressed component (ui/view), React portal (ui/portal) — demand-gated, no v1 existence. re-frame.ui.data (the runtime UI interpreter) is a reserved future separate artefact. |
re-frame.ui.test |
— | see 008 | S1 → S4 | testing | Tier 1 (JVM structural): render (one literal view form + optional {:sub-overrides …}) / text / attrs; traverse with (tree-seq map? :children tree) + a (:tag %)/(:view-id %) predicate; drive state with rf/dispatch-sync under rf/with-new-frame / rf/with-frame and re-render. Tier 3 (S2, CLJS mounted): with-root binds the connected DOM container (query it with native .querySelector / .querySelectorAll), plus (flush!) / (flush! thunk). On CLJS, with-root and both flush arities return Promises and every mounted operation must be awaited; the thunk runs inside React act and one Promise reaches the framework/React fixed point. Compiled event-vector delivery through native events rides S3's handler contract. Presence adds flush-presence! at S4 (CLJS). Contract in 008; node reading 004B. |
re-frame.ui.react (react/*) |
— | see 004D §The React interop tier | S3 | advanced | The foreign React-interop tier — six wrappers: five host hooks (use-effect, use-layout-effect, use-effect-event, use-context, use-id) plus the def-level lazy code-splitting constructor. The everyday DOM-node ref is not here — it is the substrate-native re-frame.ui/ref (promoted out, rf2-u53yy.9; its lowering target stays re-frame.ui.hooks/use-ref). Not a second state or reactivity model; the interop-tier absences (use-state/use-memo/use-reducer/use-transition, Suspense-as-authoring) are deliberate. The five hooks obey the finite-site position law and contribute to the hook-signature hash (the :react input extension lands S3); deps compare rf= per slot, and JVM host-render behaviour is stated in 004D. Ships on the demand bar; declared, asserted S3. |
re-frame.ui — blessed public-surface freeze (Spec 004 design provenance)¶
Design-time freeze, graduated here. This section is the graduated home of the blessed
re-frame.uipublic-surface freeze (§2), its component-library delta record, and the authoritative name → stage / owner / proof / spec-home matrix (§2b) — relocated from the synthesis tree's implementation plan (12-implementation-plan.md§2/§2b) when that tree was tombstoned at the S3→S4 boundary (rf2-mgy7pz, 2026-07-18). §Compiled views above is the live shipping projection; this section is the design freeze that projection implements, carried here with its row-level delta protocol so re-rulings anchor to a tracked spec home. Backtick path references below name synthesis-tree artefacts as they stood at freeze time (2026-07-12).
Freeze provenance and row-level delta protocol¶
This §2 table blessed by Mike (it is the API freeze for v1) — BLESSED as-is 2026-07-12 00:39 AUSEST (option a). Protocol: any finding from the S0 correctness pass (codex2 disposition + fold-in + coverage pass, executed 2026-07-12) that touches the table returns to Mike as a row-level delta for re-ruling; the freeze itself is not reopened.
Public surface + demand-bar audit (the blessed §2 table)¶
re-frame.ui (v1 unless marked). Consumer = guide fixture/example/tool that exists in
this suite today. Wave-2 = does not ship in v1.
| Name | Kind | Consumer | Verdict |
|---|---|---|---|
defview |
macro | every guide chapter | v1 |
sub local effect frame |
body forms | guide 03/05 | v1 |
ref |
body form (host DOM-node ref) | guide 03 (chart node) | v1 (promoted from the re-frame.ui.react ref hook — rf2-u53yy.9, the everyday DOM-node primitive; row-level delta per the freeze protocol) |
dispatch-fn |
fn | guide 05 (media-bridge) | v1 |
event handler render-fn |
macros | guide 04 table + guide 03 refs | v1 (render-fn widened 2026-07-16: also the value internal slot accepts — delta #4) |
slot |
macro (compiler-owned invocation of a render-fn value at an internal seam) |
component-library readiness — re-com renderer injection (v-table ×9, selection-list, parts), plus grids/design systems generally; drafts/component-library-readiness.md P0-3 |
v1 (lands S3→S4 compiler surface) (delta #4, DIRECTED by Mike 2026-07-16 — row-level delta per the freeze protocol) |
raw-fn |
fn | guide 04 table (callback-ref) + guide 03 refs | v1 |
frame-root frame-provider |
components | guide 01/05, every mount | v1 |
presence + presence-phase |
macro + body form | guide 02 (toasts) | v1 |
error-boundary |
component | guide 02 (route shells) | v1 |
html |
fn | guide 02 (markdown/CMS) | v1 |
raw |
fn | guide 02 interop | v1 |
custom-element |
macro (declaration; {:properties #{…}} closed grammar) |
guide 02 interop — property-accepting web components | v1 (lands with S4) (delta #1, ruled + blessed 2026-07-12) |
client-only |
macro | guide 08 (Mapbox) | v1 |
mount |
macro | guide 01 | v1 |
create-root render! hydrate-root unmount! |
macros, except unmount! (a fn) — create-root/render!/hydrate-root are compile-time macros (root identity/forms are literal); per spec/004C-Roots-and-Mount.md |
guide 08 hydration + testbeds | v1 (host tier) |
render-static |
macro | guide 08 (static footer) | v1 (SSR tier) (delta #6, ruled 2026-07-20 — fn→macro: render-static enforces the LITERAL root form at the call site per 004C §3 — the same compile error rejecting runtime-assembled vectors at mount/render!/hydrate-root — which a fn cannot; the other root entry points are macros) |
adapter |
var | rf/init! everywhere |
v1 |
->react |
fn | guide 02 §interop (view exported to a React codebase); doc 10 per-subtree migration; W1 migrator | v1 (S6 bridge — shipped ahead of the migration wave, rf2-u53yy.2) (delta #2, ruled 2026-07-12 under delegated authority — original "none in guide" premise was wrong) |
spread |
fn | the conversion architecture — 004B §The DOM conversion table (the single dynamic-map conversion); guide 02 interop + guide 07 visible-cost escapes | v1 (delta #3, ruled 2026-07-12 under delegated authority) — the general visible-cost escape; the safe-policy form is the distinct sibling spread-safe below, not an overload of this row |
spread-safe |
fn | component-library readiness — a library forwarding a consumer's runtime attr map onto an internal element without clobbering owned props or forfeiting the sync door; drafts/component-library-readiness.md P1-4 |
v1 (lands S3) (delta #5, DIRECTED by Mike 2026-07-16 — row-level delta per the freeze protocol; the spelling deferred at freeze time resolved at spec landing to the sibling name re-frame.ui/spread-safe, not a second spread arity) |
element view portal |
— | none load-bearing in guide (ui/raw covers runtime-chosen heads) |
wave-2 (view needs prod-registry design first) (2026-07-16: named triggers recorded — view graduates only on the parts-ceiling ruling or a native-table need; portal only on post-parity overlay evidence; re-com is the demand-bar consumer of record — readiness §4) |
re-frame.ui.data/render |
fn | none | wave-2, separate artefact |
re-frame.ui.react/* (use-effect, use-layout-effect, use-effect-event, use-context, use-id, lazy) |
fns | interop/migration tier — foreign-React embedding (guide 03's chart uses ui/ref/effect, NOT these hooks — the DOM-node ref promoted to the substrate ui/ref, rf2-u53yy.9); call shapes = S1 contract item; the S1 demand-bar audit confirms a consumer or returns this row as a Mike delta |
v1 (interop tier) |
re-frame.ui.test/* (render, text, attrs, with-root, flush!, flush-presence!) |
render / with-root macros + text / attrs / flush! / flush-presence! fns; CLJS with-root and zero/thunk flush! return Promises; flush! / flush-presence! are CLJS-only (a JVM Tier-1 checkpoint is a fresh render after rf/dispatch-sync) |
guide 09 + 07 fixtures | v1 (test tier) |
re-frame.ui.tool/* (view-manifest, mounted-views, explain-render, view-dependencies, view-event-sites) |
fns | Xray/Pair (EP-0033 §Tool integration posture) | v1 (tool tier, dev-only) |
Editor/kondo diagnostics layer: wave-2 (EP-0030 §The demand-bar-gated public surface). Anything not in this table does not exist.
Delta record — component-library readiness (DIRECTED by Mike 2026-07-16, in-session;
owning doc drafts/component-library-readiness.md; consensus provenance
ai/findings/re-com-port/synthesis.{fable,codex,grok}.md). Row-level deltas #4
(slot + render-fn internal widening) and #5 (spread-safe, the literal
safe-policy sibling of spread) above;
plus three non-row contract deltas: local returns [value set! update!] (P0-1,
004D §Local state and the placement rule);
the sync door widens to compiler-known ui/event vector outcomes at
proven-controlled sites (P0-2, 004D §The decision table);
the view manifest gains a versioned docs/slot projection
(P1-5, EP-0033 §Two evidence layers). Trigger-gated candidates that are NOT rows until their
triggers fire: registered ui/view (parts-ceiling ruling), ui/portal (post-parity
overlay evidence), defview-alias (façade-prototype proof), reset-key local
(controlled/commit split), lexical ui/tpl (W1/W3 checkpoint) — beads carry the
triggers. The freeze itself is not reopened; these enter under the blessed table's own
delta protocol.
Authoritative surface matrix (§2b — name → stage / owner / proof / spec home)¶
Additive, informational (codex2 Finding 5 fold-in, 2026-07-12). This matrix alters no §2 verdict: §2 stays authoritative on WHAT exists; EP-0030 §Stages S1–S7 stays authoritative on WHEN v1 items land. Rows marked ⟨aligned to EP-0030 §Stages⟩ are stage assignments derived from that stage roster rather than an explicit per-name ruling. "→" in the Stage column splits where a surface first ships from where later-stage behavior completes it. This table is the per-name answer to codex2 Q53; the residual-gates and spec-landing notes below it own Q60/Q61.
Everything below ships in artifact day8/re-frame2-ui, namespace re-frame.ui, unless
a fuller namespace is shown (the wave-2 re-frame.ui.data interpreter is a separate
artefact — §1). Wave-2 rows carry no stage: they do not ship in v1.
| Name(s) | Stage (lands → completes) | Owner (kind/tier) | Proof (fixture / gate) | Spec home |
|---|---|---|---|---|
defview |
S1 | macro; compiler .cljc + client kernel |
parity corpus v0; G-1; G-14; compile-error roster fixtures | Spec 004 rewrite — grammar/template + S1 conformance profile |
sub · frame |
S2 ⟨aligned to EP-0030 §Stages⟩ (grammar parses at S1; reactive/frame semantics S2) | body forms | S2 ownership fixtures; rf= stabilization fixtures; G-3/G-5 |
006 §The internal observation port; Spec 004 rewrite — reactive reads |
local · ref · effect |
S3 ⟨aligned to EP-0030 §Stages⟩ | body forms | narrow-placement-law fixtures (F8 ruling); StrictMode replay/cleanup; JVM initial-value-only + typed setter error (008 §The ui.test contract); batched two-writer atomic-update! matrix + fn-value set! fixtures (readiness P0-1, 2026-07-16); ref (promoted from the re-frame.ui.react ref hook, rf2-u53yy.9) rides the react-interop JVM/DOM suites (inert-ref JVM, no-re-render, hook-signature) |
Spec 004 rewrite — local placement law ([value set! update!]), the DOM-node ref (004D §The DOM node) + effects |
dispatch-fn |
S3 ⟨aligned to EP-0030 §Stages⟩ | fn | event-boundary decision-table fixtures | Spec 004 rewrite — handler boundary law |
event handler render-fn |
S3 ⟨aligned to EP-0030 §Stages⟩ (grammar recognized at S1; committed behavior + sync door S3) | macros | sync-input-door fixture (S-5 predicate + the 2026-07-16 ui/event vector-outcome arm); G-8 real-browser matrix through a reusable event-prefix component (readiness P0-2); foreign-boundary fixtures; internal fn-prop ruling fixtures |
Spec 004 rewrite — event projections + handler boundary law |
slot |
S3 → S4 (compiler surface; delta #4, directed 2026-07-16) | macro (internal render-slot invocation) | client/JVM slotted-output parity; keyed reorder under slots; purity diagnostics (sub/dispatch/hooks inside rejected); manifest slot sites; headless Tier-1 slotted trees (readiness P0-3) |
Spec 004 rewrite — template/interop (render slots) + spec/004B tree representation |
raw-fn |
S3 ⟨aligned to EP-0030 §Stages⟩ (callback-ref form; grammar recognized at S1) | fn | foreign-boundary fixtures | Spec 004 rewrite — event projections (callback-ref form) |
frame-root frame-provider |
S2 ⟨aligned to EP-0030 §Stages⟩ | components | preflight-ENSURE fixtures; G-4/G-6 | Spec-006 amendment (ENSURE preflight — EP-0032 §Preflight ENSURE for frames); Spec 004 rewrite — roots/frames |
presence + presence-phase |
S4 | macro + body form | fake-clock enter/exit fixtures; JVM :present; W14 corpus |
Spec 004 rewrite — presence (004D §Presence) |
error-boundary |
S3 | component | phase-semantics fixtures (004D §Interop and boundaries) | Spec 004 rewrite — error boundary |
html |
S1 (escaping + trusted-HTML node) → S4 hardens head policy + sanitization guidance | fn | dual-emitter agreement fixture; manifest site recording | spec/004B-UI-Tree-and-Conversion.md §Children, text, and escaping (trusted-HTML node) → Spec 004 rewrite — trusted markup |
raw |
S1 compile-time form → foreign-boundary behavior hardens S4 | fn | S1 grammar fixture; S4 boundary corpus (W14) | Spec 004 rewrite — interop/foreign heads |
custom-element |
S4 (blessed delta #1) | macro (declaration) | W14 custom-element fixtures; conversion-table property rows | spec/004B-UI-Tree-and-Conversion.md §Custom elements (Q16 ruling) → Spec 004 rewrite |
client-only |
S3 (per EP-0030 §Stages S1–S7 Stage 3) → SSR phase flip completes S5 | macro | S3 client-gate fixture; S5 phase-flip hydration fixture | Spec 004 rewrite; Spec 011 (phase flip) |
mount |
S1 ⟨aligned to EP-0030 §Stages: root descriptor⟩ → hydration completes S5 | macro (literal root form) | root-descriptor fixtures; duplicate-root-id build error | spec/004C-Roots-and-Mount.md §3 (mount grammar) → Spec 004 rewrite — roots and mounting |
create-root render! hydrate-root unmount! |
S1 signatures + client mount → hydration behavior completes S5 | macros; unmount! a fn (host tier) |
S1 client-mount smoke; S5 multi-root hydration + failed-root isolation fixtures | spec/004C-Roots-and-Mount.md → Spec 004 rewrite — roots; Spec 011 |
render-static |
S5 | macro (SSR tier; enforces the literal root form — the JVM structural tree it emits is folded to HTML by re-frame.ssr/emit-ui-tree, so re-frame.ui never statically requires re-frame2-ssr) |
explicit static-root policy + no-silent-elision fixtures (EP-0034 §2 SSR) | Spec 011; Spec 004 rewrite — roots |
adapter |
S2 — it is the observation-port consumer (rf/init!) |
var | init smoke; G-12 artifact isolation | Spec 006 (adapter contract; port stays outside the ten-fn map) |
->react |
S6 (delta #2 — migration wave; the outward bridge shipped rf2-u53yy.2) | fn | compat-boundary fixtures, both nesting directions (outward: react_export_bridge_dom_cljs_test; inward: the ui/raw corpus) |
drafts/reagent-compat-boundary.md → live Reagent compatibility appendix |
spread |
S1 — part of the conversion architecture (delta #3) | fn | conversion-table fixtures; parity-corpus dynamic-map cases | 004B §The DOM conversion table → Spec 004 rewrite — conversion architecture |
spread-safe |
S3 (delta #5, 2026-07-16 — a distinct sibling name, never an arity of spread) |
fn | G-17 — owned-key deny in dev and advanced builds (:rf.ui.compile/spread-safe-owned-key literal / :rf.error/ui-tree-malformed runtime, neither elided); aria-*/data-* pass; the policy form retains the sync door where general spread forfeits it (readiness P1-4) |
004B §The DOM conversion table → Spec 004 rewrite — conversion architecture; §Compiled views row |
element view portal |
— (wave-2; no v1 stage — any Stage-3 portal mention reads as wave-2) | — | — | — |
re-frame.ui.data/render |
— (wave-2; separate artefact) | — | — | — |
re-frame.ui.react/* (6 wrappers) |
S3 with the events/debugging consumer work; call-shape spec is an S1 contract item | re-frame.ui.react (interop tier) |
hook-signature-hash + HMR-contribution fixtures; SSR/JVM behavior fixtures; native-component-library measure-before-paint blessing: guide recipe + StrictMode/reconnect/HMR/JVM fixtures for ui/ref+use-layout-effect (readiness C-6, 2026-07-16) |
Spec 004 rewrite — interop (call shapes authored as an S1 contract item) |
re-frame.ui.test/* |
S1 core (render/text/attrs over JVM trees; one literal view form + :sub-overrides; frame scope via rf/with-new-frame / rf/with-frame, state via rf/dispatch-sync; traversal is ordinary tree-seq) + S2 mounted semantics (Promise-backed with-root binding the connected DOM container, queried with native .querySelector; Promise-backed zero/thunk flush! on CLJS; platform APIs for already-host-owned DOM mechanics) + S3 compiled event-vector delivery through native events + S4 flush-presence! (CLJS) |
re-frame.ui.test (in-artifact, dev/test scope) |
JVM-subset enforcement; real React mount/native-query/total-teardown/open-drain/forgotten-await fixtures; drain-quiescent recursive framework/React fixed point; pass-scoped-memo leak fixture (008 §The ui.test contract) |
spec/004B-UI-Tree-and-Conversion.md (tree/node reading) → the contract's home, 008 §The ui.test contract |
re-frame.ui.tool/* |
S3 with Xray consumption (W7a) | re-frame.ui.tool (tool tier, dev-only) |
Xray consumption fixtures; G-7/G-11 (tool tier absent from production); versioned docs/slot manifest projection — props schema + per-prop docs/defaults + slot metadata in one stable dev/test shape, production absence proven (readiness P1-5, 2026-07-16) | Spec 009 (instrumentation + catalogue rows) |
Residual named gates (Q60) — feasibility PASS ≠ these are done: real sub-cache graft conformance (S2) · G-8 real-browser input matrix (S3) · root-manifest hydration + failed-root isolation (S5) · G-1 rerun under the revised alternating-rounds estimator (with the S1 gate wiring) · production elision G-7/G-11 (S3) and absence/equivalence/budget gates (S6).
Spec landing rule (Q61) — each stage's spec edits merge atomically with that stage's conformance slice (R-1 framing); the rewrite's per-stage conformance profile matrix (codex2 F4 disposition) defines what "Stage-N-conforming" asserts, so no intermediate checked-in spec claims unimplemented behavior.
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 reagent-adapter] and pass reagent-adapter/adapter to (rf/init! …).
| API | M/Fn | Signature | Status | Tier | Spec |
|---|---|---|---|---|---|
reagent-adapter/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 |
reagent-adapter/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 |
reagent-adapter/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 |
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 uix-adapter] and call the surfaces directly.
| API | M/Fn | Signature | Status | Tier | Spec |
|---|---|---|---|---|---|
uix-adapter/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 |
uix-adapter/use-subscribe |
Fn (UIx hook) | (use-subscribe query-v) / (use-subscribe frame-kw query-v) → current sub value |
v1 | adapter | 006 |
uix-adapter/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 through the same carried-invariant chain as the ambient use-subscribe (no scope → :rf.error/no-frame-context); reference-stable across re-renders for the same resolved frame. No opts, no variants — explicit frames use (rf/capture-frame frame-id). |
v1 | adapter | 002, 006 |
uix-adapter/use-current-frame |
Fn (UIx hook) | (use-current-frame) → frame-kw, or the :rf.frame/no-provider sentinel when neither frame-provider nor frame-root sits above (the narrow raw useContext read of the one shared context both boundaries install — explicitly not :rf/default; use (rf/current-frame-id) for the full dynamic-var → context → nil resolution chain) |
v1 | adapter | 006 |
uix-adapter/frame-provider |
Fn (UIx component) | SCOPE-only (rf2-nyea0r split — roots ensure; providers scope) — ($ uix-adapter/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 |
uix-adapter/frame-root |
Fn (UIx component) | ENSURE — a commit-owned two-pass boundary (rf2-nyea0r split) — ($ uix-adapter/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 |
uix-adapter/wrap-view |
Fn | (wrap-view id metadata user-fn) → wrapped fn (source-coord injection per Spec 006 §Source-coord annotation) |
v1 | adapter | 006 |
uix-adapter/flush-views! |
Fn | (flush-views!) / (flush-views! f) — wraps React's act() for tests |
v1 | adapter | 006, 008 |
uix-adapter/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 |
Per Decision 1 the hook is named use-subscribe (matching the React/UIx idiom). 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> :to <route-id> :params {...} :query {...} :fragment "..."}]. 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 popstate / initial load / SSR (default scroll :restore). Co-equal sibling of :rf.route/transitioned, not a delegate. |
012 |
:rf.route/transitioned |
URL-change handler for forward navigation (link click / programmatic push; default scroll :top). |
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 |
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 |
SSR (Spec 011)¶
Namespace: the surfaces below live in
re-frame.ssr(artefactday8/re-frame2-ssr); consumers(:require [re-frame.ssr :as ssr]). The Ring host-adapter lives inre-frame.ssr.ring(artefactday8/re-frame2-ssr-ring). The SSR query surface (render-to-string,render-tree-hash,project-error,render-head,active-head,head-model->html,head-snapshot) is also late-bind re-exported throughre-frame.core— one of the two documented façade exceptions (the other is epoch), per the §Conventions per-artefact namespace table. The streaming surface (streaming-render-*) and the Ring host-adapter (re-frame.ssr.ring) are NOT re-exported — they are host-adapter territory; an SSR-aware host requires the namespace directly. Apps targeting SSR add the artefacts to their deps regardless.
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 (the value the JVM re-frame.ui emitter produced) 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>. re-frame.ui/render-static reaches this seam by late resolution, so re-frame.ui never statically requires re-frame2-ssr. 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 |
render-head |
Fn | (render-head head-id opts) → :rf/head-model |
v1 | advanced | 011 |
active-head |
Fn | (active-head frame-id) → :rf/head-model. 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 |
v1 | advanced | 011 |
head-snapshot |
Fn | (head-snapshot frame-id) → {head-id → :rf/head-model}. Read the per-frame snapshot of last-produced head-models. Returns {} for a frame that has never seen a render-head call (or whose snapshot has been cleared via per-request frame teardown). Useful for tests, introspection, and tools. Re-exported as rf/head-snapshot. Per 011 §Head/meta contract. |
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 client hydration / streaming lifecycle —
hydrate!,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!) — live inre-frame.ssrbut are:implementation-tier host-adapter plumbing (the supported client path is the:rf/hydrateevent plus there-frame.ssr.ringhost adapter). 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:
| Sub | Returns | Spec |
|---|---|---|
:rf/head |
The head model for the active route (resolved via (active-head frame-id)) |
011 |
:rf/public-error |
The sanitised public-error projection when an error page is being rendered; nil otherwise |
011 |
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 stubbing macros (rows below) ship 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 stubbing macros. |
v1 (optional capability, dev/test) | — (fx-id; test) | 014 |
with-managed-request-stubs |
M | (with-managed-request-stubs route-map body+) — route-map {[<method> <url>] {:reply ...}} per 014 §Testing. Lives in re-frame.http.test-support (the single home for HTTP test surfaces — audit-of-audits #15 closed the prior split that placed the macros in re-frame.http.managed); see Spec 008 §HTTP test surfaces. |
v1 (optional capability, dev/test) | testing | 014 |
with-managed-request-stubs* |
Fn | (with-managed-request-stubs* route-map body-fn) — plain-fn surface beneath with-managed-request-stubs; the * follows the Clojure let/let*, fn/fn* idiom (per Conventions). Ships in re-frame.http.test-support. Use for computed route-maps or non-literal bodies. Per 014 §Testing. |
v1 (optional capability, dev/test) | testing | 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 |
clear-http-interceptor |
Fn | (clear-http-interceptor id) / (clear-http-interceptor id {:frame target}) — unregister an interceptor by id (per 014 §Middleware). The single-arity resolves the frame from the carried scope; under no scope it raises :rf.error/no-frame-context — no :rf/default fallback (EP-0002). The two-arity names the frame via an id-first {:frame target} opts map (the framework-wide (id {:frame …}) shape) and is exact and fail-closed: anything but exactly {:frame target} with a present, non-nil target — {}, {:frame nil}, a typo'd or extra key, a non-map second arg — raises :rf.error/http-bad-interceptor BEFORE any ambient frame is resolved or touched (rf2-s32bf). 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 (used by the :rf.fx/clear-http-interceptor fx), not a public arity. |
v1 (optional capability) | advanced | 014 |
:rf.fx/reg-http-interceptor |
fx | [:rf.fx/reg-http-interceptor {:id <id> :before … :after … ?:frame …}] — register an HTTP interceptor at runtime via :fx (the data-shaped counterpart of the reg-http-interceptor macro; the dispatching frame threads through when :frame is omitted). Registered by re-frame.http.managed; :platforms #{:client :server}. |
v1 (optional capability) | — (fx-id) | 014 |
:rf.fx/clear-http-interceptor |
fx | [:rf.fx/clear-http-interceptor {:id <id> ?:frame …}] — unregister an HTTP interceptor at runtime via :fx (args :frame wins, else the fx-context frame). |
v1 (optional capability) | — (fx-id) | 014 |
re-frame.http/get |
Fn | (rf.http/get url) / (rf.http/get url args) — build a [:rf.http/managed {:request {:method :get :url url} ...}] fx vector (per 014 §Call-site helpers). |
v1 (optional capability) | advanced | 014 |
re-frame.http/post |
Fn | (rf.http/post url) / (rf.http/post url args) — POST helper; same shape as get. |
v1 (optional capability) | advanced | 014 |
re-frame.http/put |
Fn | (rf.http/put url) / (rf.http/put url args) — PUT helper. |
v1 (optional capability) | advanced | 014 |
re-frame.http/delete |
Fn | (rf.http/delete url) / (rf.http/delete url args) — DELETE helper. |
v1 (optional capability) | advanced | 014 |
re-frame.http/patch |
Fn | (rf.http/patch url) / (rf.http/patch url args) — PATCH helper. |
v1 (optional capability) | advanced | 014 |
re-frame.http/head |
Fn | (rf.http/head url) / (rf.http/head url args) — HEAD helper. |
v1 (optional capability) | advanced | 014 |
re-frame.http/options |
Fn | (rf.http/options url) / (rf.http/options url args) — OPTIONS helper. |
v1 (optional capability) | advanced | 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 raw install-managed-request-stubs! / uninstall-managed-request-stubs! pair 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])). The ergonomic with-managed-request-stubs macro is the façade surface; use the raw pair only when stubs must span multiple deftests.
The verb helpers (get / post / put / delete / patch / head / options) live in re-frame.http — users (:require [re-frame.http :as rf.http]) alongside re-frame.core. They're pure synthesis fns that produce the canonical [:rf.http/managed args-map] fx vector; the namespace ships in day8/re-frame2-http (same artefact as the fx they reference) so loading the helpers and the fx are a single dep decision.
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 over it — a named target per branch, both receiving the identical envelope. 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 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) |
Schema-reflection metadata¶
Handlers may declare :rf.http/decode-schemas [<schema> ...] in their reg-event metadata-map; pair tools and generators read it via (rf/handler-meta :event id). Optional, never enforced — see 014 §Schema reflection.
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 |
Fn | (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. |
clear-resource |
Fn | (clear-resource resource-id) |
post-v1 lib (optional capability) | advanced | 016 | Registration-lifecycle removal (the clear- registrar decrement per Conventions §Tear-down verb axis); disposes resource-runtime state per 016 §Registration. |
reg-mutation |
Fn | (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. |
clear-mutation |
Fn | (clear-mutation mutation-id) |
post-v1 lib (optional capability) | advanced | 016 | Registration-lifecycle removal of a mutation. |
reg-resource-scope |
Fn | (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). Omitting :inputs (the 2-arg (reg-resource-scope scope-id resolve-fn) sugar) selects the whole-db form — the resolver reads the db, a tooling-marked explicit-cost dependency. 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. |
clear-resource-scope |
Fn | (clear-resource-scope scope-id) |
post-v1 lib (optional capability) | advanced | 016 | The clear- registrar decrement counterpart of reg-resource-scope (per Conventions §Tear-down verb axis). |
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. |
resources |
Fn | (resources {:frame f}) — frame-targeted introspection: the registered resources plus the live per-frame resource-instance table |
v1 (optional capability) | advanced | 016 | Façade re-export of the Resources artefact (late-bound). |
mutations |
Fn | (mutations {:frame f}) — frame-targeted introspection: the registered mutation ids plus the live per-frame mutation-instance table (keyed by instance id) |
v1 (optional capability) | advanced | 016 | |
resource-meta |
Fn | (resource-meta resource-id) — the registered resource's spec map (:params-schema, :data-schema, :request, :scope, …), or nil |
v1 (optional capability) | advanced | 016 | |
mutation-meta |
Fn | (mutation-meta mutation-id) — the registered mutation's spec map (:request, :params-schema, :invalidates, …), or nil |
v1 (optional capability) | advanced | 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 | |
install-revalidation-listeners! |
Fn | (install-revalidation-listeners! frame-id) — window focus / network-reconnect listeners driving active-stale revalidation for the frame; idempotent; cancelled on frame destroy; CLJS-only (JVM no-op) |
v1 (optional capability) | advanced | 016 | |
remove-revalidation-listeners! |
Fn | (remove-revalidation-listeners! frame-id) — tear down the revalidation listeners; no-op when none installed (and on the JVM) |
v1 (optional capability) | advanced | 016 |
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/clear-http-interceptor(§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: #{:db :rf.db/runtime :fx}. 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.
| 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. |
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 | |
[:http args] |
impl-specific | — | — | user-registered via reg-fx. |
[: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, :on-spawn, :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); :on-spawn is advisory-only and cannot record it (its return is dropped). 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.
Two read shapes, no realm coordinate. The registrar query grammar has exactly two shapes per member (
registrations/handler-meta): the positional-keyword arity reads the default source store ((registrations :event)etc.), and a map is ALWAYS a frame-targeted read (next note). There is no:realmmap arity —(registrations {:realm r :kind k})and friends do not exist; there is no realm coordinate in the public read grammar. A registrar-query map without:frameis an error (:rf.error/registrar-query-needs-frame) — a map is never a "default realm" read. There is nore-frame.realmnamespace and no realm-scoped reader to read; frame resolution routes directly through the process registrar (re-frame.registrar/re-frame.frame/re-frame.image). Tooling that needs the host app's registrations without rebinding to its own image generation (e.g. Xray's host-registry generation-bypass) reads the process source store directly viare-frame.registrar, not a realm coordinate.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 positional-keyword arity (default source store) is byte-identical — only a caller passing a{:frame …}map reaches the generation path.
| API | M/Fn | Signature | Status | Tier | JVM-runnable? | Spec |
|---|---|---|---|---|---|---|
registrations |
Fn | (registrations kind) / (registrations kind pred-fn) → {id metadata-map}. Use when you want metadata — registry walks that read source-coords, :rf/sensitive, :rf/machine?, :platforms, etc. The positional-keyword arity reads the default source store. Frame-targeted: (registrations {:frame f :kind k}) → the {id metadata} for :kind resolved through live frame f's own sealed image generation (only the ids that frame's image carries, with :rf.provenance/ns provenance facts); :frame is a frame id or a direct frame object; an optional :pred filters. A map argument is ALWAYS a frame-targeted read — a map without :frame is an error (:rf.error/registrar-query-needs-frame); there is no :realm map arity. Fail-loud on an unresolvable :frame (see note above). |
v1 | tooling | ✓ | 002 |
handler-meta |
Fn | (handler-meta kind id) → registration-metadata map. 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. Frame-targeted: (handler-meta {:frame f :kind k :id id}) → the metadata for [k id] resolved 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]. A map argument is ALWAYS a frame-targeted read — a map without :frame is an error (:rf.error/registrar-query-needs-frame); there is no :realm map arity. Fail-loud on an unresolvable :frame (see note 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 arities of the pair above give 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-at, 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{:frame …}/ bare-frame-id arity ofapp-schemas/app-schema-at/app-schema-meta-at/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). A bare frame value passed as the opts argument routes to its own frame, not the ambient frame. 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.One preferred validator-extension path. 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 bundle setter that installs all three from one map so they never drift mid-boot. The per-fn singletonsset-schema-validator!/set-schema-explainer!/set-schema-printer!are retained as lower-level convenience setters for adjusting one fn in isolation; they remainadvancedbut are not the preferred boot path.
| API | M/Fn | Signature | Status | Tier | Spec |
|---|---|---|---|---|---|
app-schemas |
Fn | (app-schemas) / (app-schemas {:frame frame-id}) |
v1 | tooling | 010 |
app-schema-at |
Fn | (app-schema-at path) / (app-schema-at path {:frame frame-id}) |
v1 | tooling | 010 |
app-schema-meta-at |
Fn | (app-schema-meta-at path) / (app-schema-meta-at path opts-or-frame-id) — return the full registration-metadata map (:path, :schema, :frame, plus source-coords :ns / :line / :file and the rest of :rf/registration-metadata) for a registered app-db schema, or nil. Pair-tool and 10x consumers reach for this when they need the registration anchor (e.g. click-back-to-code); the lighter app-schema-at is the right call when only the schema value is needed. 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) / (app-schemas-digest {:frame frame-id}) → string |
v1 | tooling | 010 |
set-schema-fns! |
Fn | (set-schema-fns! {:validate validate-fn :explain explain-fn :print print-fn}) — the preferred schema-fns configuration path. Atomically 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. The honest one-call substitute-Malli boot pattern (a Zod / clojure.spec port installs all three together so they never drift mid-boot — the reason this is the blessed entry point over the per-fn singletons below). :print nil coerces to the default EDN canonicaliser (parity with set-schema-printer!); :validate nil / :explain nil disable that fn. Last-write-wins per key; returns the installed bundle as a map {:validate … :explain … :print …} reflecting the live state of all three fns after the call (a bundle setter returns its bundle, not just the validator). Per 010 §Default validator and the validator-fn extension point. |
v1 | advanced | 010 |
set-schema-validator! |
Fn | Lower-level single-fn setter — prefer set-schema-fns! for boot. (set-schema-validator! validate-fn) installs only the validator ((fn [schema value] truthy?), same shape as malli.core/validate) every dev-time schema-validation site routes through; the explainer/printer are left untouched. nil disables validation entirely. Default ships Malli's validate; this seam lets apps swap in their own validator to drop the Malli dep. Last-write-wins; returns the installed validator. Reach for it only when adjusting one fn in isolation (e.g. a runtime toggle); installing a port's full bundle at boot goes through set-schema-fns! so the three never drift mid-boot. Per 010 §Default validator and the validator-fn extension point. |
v1 | advanced | 010 |
set-schema-explainer! |
Fn | Lower-level single-fn setter — prefer set-schema-fns! for boot. (set-schema-explainer! explain-fn) installs only the explainer used to enrich :rf.error/schema-validation-failure traces' :explain key. Companion to set-schema-validator!; subsumed by the :explain key of set-schema-fns!. Per 010 §Default validator and the validator-fn extension point. |
v1 | advanced | 010 |
set-schema-printer! |
Fn | Lower-level single-fn setter — prefer set-schema-fns! for boot. (set-schema-printer! print-fn) installs only the schema-print companion the digest pipeline (per 010 §Schema digest) hashes. print-fn is (fn [schema-value] canonical-string) and MUST be pure + deterministic across runtimes. nil falls back to the default EDN canonicaliser so the digest is never undefined. Parallel to set-schema-validator! / set-schema-explainer!; subsumed by the :print key of set-schema-fns!. Non-Malli ports register their own serialiser so cross-runtime digest comparison reflects their port's contract. Per 010 §Default validator and the validator-fn extension point. |
v1 | advanced | 010 |
validate-at-boundary-interceptor |
Var (interceptor value) | validate-at-boundary-interceptor — a pre-built interceptor value, not a fn (interceptor :id is :rf.schema/at-boundary); do not call it as a fn (invoking (rf/validate-at-boundary-interceptor ...) raises ArityException). Under EP-0022 a public :interceptors chain carries refs, not inline values: reference the registered :rf.schema/at-boundary interceptor by id (the value Var is the registration-boundary input, not a chain entry). |
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 rf/elide-wire-value 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 (the :rf.schema/at-boundary interceptor refused the event's payload against the handler's :schema, 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.
Advanced corpus-wide hook — not the off-box default (EP-0015 §9). This registry fans an UNPROJECTED record across EVERY frame; it is not routed under any frame's egress policy. The NORMAL production observation surface 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 and wired withregister-observability-sink!, which hands the sink an already-PROJECTED record. Reach for the corpus-wide listener only for an intentionally cross-frame hook or a record the sink routing does not carry.Sensitive data marking is path-based per the upcoming data-classification mechanism (separate spec doc; in progress). The handler-meta
:sensitive?annotation is removed.
Registration uses the stream-parameterized listener verb with the :events stream — (register-listener! :events id listener-fn) / (unregister-listener! :events id) (see §Observation listeners). The always-on event-emit registry stays addressable via re-frame.event-emit for internal consumers (which also own between-scenario clears).
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.
Advanced corpus-wide hook — not the off-box default (EP-0015 §9). This 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. It is the documented exception to the always-on axis's 'structured data only' rule, for post-mortem shippers that need the host throwable + stack. The NORMAL 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 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. Off-box shippers (Sentry / Honeybadger / Rollbar) should prefer the frame sink; reach for the corpus-wide listener only for an intentionally cross-frame hook or a frameless record the sink routing does not carry.
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 rf/elide-wire-value 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.
Registration uses the stream-parameterized listener verb with the :errors stream — (register-listener! :errors id listener-fn) / (unregister-listener! :errors id) (see §Observation listeners). The always-on error-emit registry itself stays addressable via re-frame.error-emit + the :error-emit/register-error-listener! late-bind hooks for internal consumers (router fan-out, the SSR error projector), which also own between-scenario clears.
Observation listeners¶
One stream-parameterized listener verb registers an observation callback across the four pure listener streams — the differentiator is data (which stream), so it rides in a leading required stream keyword (in place of per-channel register-(trace|event|error|epoch)-listener! pairs). The closed stream vocabulary is :trace / :events / :errors / :epoch; an unknown stream throws :rf.error/unknown-listener-stream (no bare trace default, no compatibility aliases). The frame-owned observability sink is a distinct verb, NOT a :sink stream.
| Stream | Axis | Record |
|---|---|---|
:trace |
dev-only (DCE'd in production) | one trace event per call |
:events |
always-on (survives :advanced + goog.DEBUG=false) |
one tight event-record per processed event, fanned across EVERY frame, :event wire-elided but otherwise unprojected — the ADVANCED corpus-wide hook, NOT the off-box default (the frame-owned :observability sink is) |
:errors |
always-on | one error-record per :rf.error/* event, fanned across EVERY frame, :event wire-elided, :exception RAW — the ADVANCED corpus-wide hook, NOT the off-box default |
: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 / :events / :errors / :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) → nil — empty the named frame's ring. 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 |
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. Re-exported from re-frame.trace.projection (see 009 §Event-bundle projection). |
v1 (dev-only) | tooling | 009 |
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. |
v1 (dev-only) | tooling | 009 |
group-by-event-with-events |
Fn | (group-by-event-with-events events) — like group-by-event, but each event bundle additionally carries :trace-events (the raw events that composed it), keyed by the same frame-scoped [frame dispatch-id] grouping. Pure data; JVM-runnable |
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 |
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 / :events / :errors). 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/epoch-redact-fn-exception |
:frame, :rf.epoch/id, :ex-msg (an installed :redact-fn threw at projection egress) |
: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 —
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 through this walker; the trust-boundary surfaces catalogued in Security.md compose against this primitive.
The framework primitive that walks tree-shaped values at the wire boundary and substitutes elision markers for sensitive or large slots. Consumed 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). 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.
| API | M/Fn | Signature | Status | Tier | Spec |
|---|---|---|---|---|---|
elide-wire-value |
Fn | (elide-wire-value v opts) → v or an elision-marker substitution. opts is a map: {:rf.size/include-large? <bool> :rf.size/include-sensitive? <bool> :rf.size/include-digests? <bool> :rf.size/threshold-bytes <int> :path [...] :frame <frame-id>}. Defaults: both include-* flags false (maximum elision); :rf.size/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. |
v1 | tooling | 009 |
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, and machine / resource :data-schema per-slot props still classify owner-local data). 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 elide-wire-value and 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 overelide-wire-value. The six-member:rf.egress/*profile enum is the named-boundary vocabulary; the boolean:rf.size/*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 :rf.observe/* record kinds) 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 rf/elide-wire-value. A profile resolves to a :rf.size/* opt-set; an explicit :rf.size/* 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: {:rf.egress/profile <closed six-member enum> :frame <frame-id> :path [...] :rf.size/include-sensitive? <bool> :rf.size/include-large? <bool> :rf.size/include-digests? <bool> :rf.size/threshold-bytes <int> :as-of-epoch <epoch>}. Dispatches on a record's :kind (:rf.observe/handled-event / :rf.observe/error) 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 elide-wire-value. An :rf.observe/* record is frame-bearing: when opts omit :frame, the record's own top-level :frame (its owning frame) is seeded as the governing frame; an explicit :frame opt wins (override). Profile resolves to a :rf.size/* opt-set; explicit :rf.size/* booleans compose on top (override wins). Unknown profile raises :rf.error/unknown-egress-profile (closed enum). Fail-closed when no frame is known from the opt, the record, or the carried scope (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 … :opts {…}}] :errors [...]}) and registers the concrete sink fn against that<id>withrf/register-observability-sink!. 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). The lower-level corpus-wide event-emit / error-emit registries (the:events/:errorsstreams ofregister-listener!) remain for advanced integration. Routing is fail-closed: an unresolved frame or absent:observabilitypolicy routes nothing (no:rf/defaultsynthesis), and a throwing sink is isolated from its siblings.
| 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 :sensitive? true at the top level (not under :tags). The framework-published predicate every consumer composes against — replaces per-consumer reimplementations of the same five-token check. |
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: #{:db :rf.db/runtime :fx} (:rf.db/runtime reserved by convention for framework authority; app handlers use only :db / :fx) |
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 rf/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.size/* 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, machine-transition, sub-topology). 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. |
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. |
->interceptor* |
Fn (internal lowering constructor) | advanced | The framework-internal constructor that lowers a descriptor into an executable chain entry ((->interceptor* & {:keys [id before after source-coord]})). NOT a public application-authoring API and MUST NOT appear in a public event/frame chain (EP-0022). |
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 (->interceptor survives only as the internal lowering constructor) |
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 (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). clear-flow — whose primary arg IS the flow-id — keeps its trailing opts map for the frame override: (clear-flow id) / (clear-flow id opts). clear-flow 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) — idempotent boot. 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! 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 |
init-platform |
Fn | (init-platform p) — set the host-wide active-platform marker (:server or :client). The runtime tracks the active platform so reg-fx/reg-cofx :platforms metadata can gate execution (per 011 §Effect handling on the server). CLJS hosts default to :client, JVM hosts to :server; call at boot to override (CLJS-on-Node SSR runtime: (rf/init-platform :server); JVM-runnable test simulating browser: (rf/init-platform :client)). Per-frame :config :platform (set by the :ssr-server preset) still wins over this host-wide marker. p must be :server or :client; anything else raises :rf.error/invalid-platform. Idempotent / re-callable. Sibling boot call to init! — independent (adapter install vs platform marker). |
v1 | advanced | 011 |
install-adapter! |
Fn | (install-adapter! adapter-map) — must be called before any frame is created. Lower-level than init!; most consumers call init! instead |
v1 | advanced | 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) → discriminator keyword (:rf.adapter/reagent / :rf.adapter/reagent-slim / :rf.adapter/freehand / :rf.adapter/ui / :rf.adapter/uix / :rf.adapter/plain-atom / :rf.adapter/ssr / :custom) or nil when no adapter is installed. Answers "what substrate am I on?" — predicate / branch code. For the spec map (fn handles, identity checks), use current-adapter-spec. |
v1 | advanced | 006 |
current-adapter-spec |
Fn | (current-adapter-spec) → the installed adapter spec map (the value passed to (rf/init! ...)) or nil when no adapter is installed. Answers "give me the adapter fns to call" — tools, routing, identity checks across the install/dispose lifecycle. For the discriminator keyword, use current-adapter. |
v1 | advanced | 006 |
adapter-disposed? |
Fn | (adapter-disposed?) → true iff terminal teardown of the most recent installed generation has been claimed and no subsequent install-adapter! has fired. Cleanup success is not implied: it remains true when destruction rethrows a cleanup failure. false for never-installed (fresh process) and after a fresh install. Read-only — the breadcrumb is owned by the install / destroy pair. Use to distinguish :rf.error/no-adapter-installed (fresh process) from :rf.error/adapter-disposed (torn down). Per 006 §Disposed-vs-never-installed. |
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, unknown top-level key silently no-ops. 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 | — |
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. These three surfaces make the optional-feature inventory self-explaining.
The known optional features are :schemas, :machines, :routing, :flows, :http, :ssr, :epoch (the closed per-feature split set per Conventions §Artefact tiers).
These three fns ship to production. They are runtime queries, not dev-time instrumentation, so — unlike the trace / epoch surfaces — they are NOT gated on interop/debug-enabled? and do NOT elide under :advanced + goog.DEBUG=false. A production caller may legitimately probe (rf/feature-loaded? :routing) 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. feature-loaded? detects presence 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 | — |
feature-loaded? |
Fn | (feature-loaded? feature) → true when the feature's impl artefact is on the classpath, false otherwise (including for an unknown feature keyword). Pure keyword lookup against the always-loaded late-bind hooks atom — no require into the optional namespace. Ships to production (NOT elided). |
v1 | advanced | — |
require-feature! |
Fn | (require-feature! feature) → true when the feature is loaded; otherwise throws :rf.error/feature-not-loaded carrying the EXACT copy-pasteable Maven coordinate (:maven) + require namespace (:require-ns) and a :reason string naming both (an unknown feature keyword throws :rf.error/unknown-feature with the :known set). Use as a self-explaining early guard at the top of feature-dependent code. Ships to production (NOT elided). |
v1 | advanced | — |
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) preserving its slot-merge semantics; an unknown top-level key is a silent no-op (closed-and-additive). Keys are plural-noun-shaped; opts are an open map of per-key settings.
(rf/configure! {:epoch-history {:depth 100}
:trace-buffer {:events-retained 25}
:elision {:rf.size/threshold-bytes 8192}})
| Key | Opts shape | Default | Status | Spec |
|---|---|---|---|---|
:epoch-history |
{:depth N :trace-events-keep N :redact-fn (fn [record] …)} — :depth non-negative integer (0 disables the ring); :trace-events-keep non-negative integer caps raw :trace-events retention (per Security §Epoch privacy posture); :redact-fn is fn? or nil — the advanced PROJECTION-SIDE override invoked once per record at the off-box egress boundary (inside projected-record, after the frame/profile projection), NOT at build/storage time: the in-process ring buffer and every :epoch-stream listener deliver the RAW record (post-EP-0010 causal replay material — storage-side mutation was removed so restore-epoch! fidelity is never affected), with the :rf.epoch/sensitive? rollup computed at build-time from raw signals, throws caught and surfaced as :rf.warning/epoch-redact-fn-exception with fallback to the projected record. Per Tool-Pair §Time-travel. |
{:depth 50, :redact-fn nil} |
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.size/threshold-bytes N} — non-negative integer; 0 disables runtime auto-detect (only declared / schema entries elide) |
{:rf.size/threshold-bytes 16384} |
v1 | 009 |
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.size/threshold-bytes N}because:rf.size/threshold-bytesis the same per-call policy key consumed byrf/elide-wire-valueand the wire-elision walker (per Conventions §Reserved namespaces —:rf.size/*). 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,:redact-fn). 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.size/threshold-bytes is read by :elision, by elide-wire-value, 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 silently 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.
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: a machine is registered as one reg-event whose body comes from make-machine-handler.
| 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 |
v1 | advanced | 005 |
machine-transition |
Fn | (machine-transition definition snapshot event) → re-frame.machines.result value (result/ok?; result/snap / result/fx read the snapshot + fx; result/fail carries result/info) |
v1 | advanced | 005 |
machines |
Fn | (machines) → vector of registered machine-ids |
v1 | tooling | 005 |
machine-meta |
Fn | (machine-meta machine-id) → registration metadata; :guards / :actions entries carry co-located :source-coords / :source-code and each :states-tree map node carries its own reference-site :source-coords when registered via the macro |
v1 | tooling | 005 |
machine-by-system-id |
Fn | (machine-by-system-id system-id) / (machine-by-system-id system-id {:frame target}) → spawned-machine id bound to system-id in the frame's [:rf.runtime/machines :system-ids] reverse index (or nil). The 2-arity takes an {:frame target} opts map (the frame-last positional spelling is retired to internal / test / tooling plumbing). Per 005 §Named addressing via :system-id. |
v1 | advanced | 005 |
:rf.machine/dispatch-to-system (fx) |
— | Named-addressing escape (registered globally by re-frame.machines). From a machine action's :fx, [:rf.machine/dispatch-to-system [system-id event]] resolves system-id through the frame's [:rf.runtime/machines :system-ids] reverse index and dispatches event to the bound actor (no-op when unbound); actions can't read app-db, so the fx is how an action messages a NAMED child. Zero in-repo consumers as of 2026-07-10; retained for XState v6 actor-system parity (systemId addressing). The redundant dispatch-to-system call-site FN twin was deleted. Per 005 §Cross-machine messaging by name. |
v1 | — (fx-id) | 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 |
:child-machine (transition-table key) |
— | Declarative state-scoped child-machine binding. | post-v1 lib | — (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 with :<-. 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). 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 public execution surface is exactly the three verbs run / is / explain (each accepts a registered-variant keyword OR an inline-plan map). run-variant / is-variant / run-plan / is-plan / watch-variant / reset-variant are implementation / migration vocabulary, NOT the P1 public surface — they are not rowed here. 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 |
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 |
004 |
h macro (proposed earlier) |
Removed. Use the Var form [my-view "args"] or [(rf/view :my-view) "args"] |
004 |
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 |
Cross-references¶
- 000-Vision.md — principles and design decisions
- 002-Frames.md — frames, dispatch envelope, drain semantics, overrides, machine foundations
- 004-Views.md — the common Freehand view contract: declaration, authoring, semantics, host boundary
- 004D-Freehand-Compiled-Grammar.md — the compiled-tier grammar: view registration, hiccup forms, the template AST, both emitters
- 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