Unified per-tab sub-region lifecycle registry.
A subview is a managed region of a page that owns a slice of lifecycle:
it subscribes to a set of Watchable :deps, optionally renders (the
reactive case), and is swept when it disappears from the view tree. This
is the single mechanism the higher-level subsystems route through:
hyper.reactive (a render-bearing subview, :on-change :partial) —
re-renders only itself when its deps change.h/watch! (a deps-only subview, :on-change :full) and
h/async (a :mount/:unmount-bearing subview) plug into the same
registry.State layout (per tab): [:tabs id :subviews sid] -> the subview spec (see register-subview!) [:tabs id :subview-watches sid] -> {watch-key dep} bookkeeping for teardown
Deps are reference-counted via hyper.watch so a source shared across
subviews/tabs is only disposed when the last consumer releases it. A
subview's :on-change decides what a dep change triggers: :partial
re-renders just this subview (a targeted Datastar fragment), :full
triggers a full page re-render.
Unified per-tab sub-region lifecycle registry.
A *subview* is a managed region of a page that owns a slice of lifecycle:
it subscribes to a set of Watchable `:deps`, optionally renders (the
reactive case), and is swept when it disappears from the view tree. This
is the single mechanism the higher-level subsystems route through:
- `hyper.reactive` (a render-bearing subview, `:on-change :partial`) —
re-renders only itself when its deps change.
- (future) `h/watch!` (a deps-only subview, `:on-change :full`) and
`h/async` (a `:mount`/`:unmount`-bearing subview) plug into the same
registry.
State layout (per tab):
[:tabs id :subviews sid] -> the subview spec (see register-subview!)
[:tabs id :subview-watches sid] -> {watch-key dep} bookkeeping for teardown
Deps are reference-counted via `hyper.watch` so a source shared across
subviews/tabs is only disposed when the last consumer releases it. A
subview's `:on-change` decides what a dep change triggers: `:partial`
re-renders just this subview (a targeted Datastar fragment), `:full`
triggers a full page re-render.(get-subview app-state* tab-id sid)Retrieve a subview's spec from app-state.
Retrieve a subview's spec from app-state.
(key->token k)Derive a stable, id-safe token from a region :key value. Simple values
pass through; anything else collapses to a hash.
Derive a stable, id-safe token from a region `:key` value. Simple values pass through; anything else collapses to a hash.
(partial-render app-state* tab-id sid)Re-render a single render-bearing subview and return the HTML string for a targeted Datastar fragment. Updates the cache. Returns nil when the subview is no longer registered or has no render-fn.
Re-render a single render-bearing subview and return the HTML string for a targeted Datastar fragment. Updates the cache. Returns nil when the subview is no longer registered or has no render-fn.
(register-subview! app-state* tab-id sid spec)Record/refresh a subview during a render, marking it live for this render
cycle so it survives the post-render sweep, and wire its dep watches when a
renderer is present (see wire-subview!). Wiring can enqueue a partial
render if a dep already changed. Idempotent.
spec keys (all optional unless noted):
:scope decides the sweep lifetime: :render subviews (reactive regions)
are re-registered every full render and swept when absent; :mount
subviews (h/watch!, async workers — registered once in form-2 setup / a
form-3 :mount) survive per-render sweeps and are torn down only on
page-view remount (navigation) or tab disconnect.
Returns the stored spec.
Record/refresh a subview during a render, marking it live for this render cycle so it survives the post-render sweep, and wire its dep watches when a renderer is present (see `wire-subview!`). Wiring can enqueue a partial render if a dep already changed. Idempotent. `spec` keys (all optional unless noted): - :deps vector of Watchable sources to subscribe (ref-counted) - :on-change :partial (default) | :full — what a dep change triggers - :scope :render (default) | :mount — sweep lifetime (see below) - :render-fn thunk returning hiccup (render-bearing subviews) - :cached-html last rendered HTML string (for partial re-render) - :html-id the element id targeted by partial fragments - :dep-vals snapshot of (mapv deref deps) at last render - :mount (fn [] -> resource) run once when the subview first appears - :unmount (fn [resource] ...) run when the subview is swept/torn down - :resource value returned by :mount, threaded into :unmount `:scope` decides the sweep lifetime: `:render` subviews (reactive regions) are re-registered every full render and swept when absent; `:mount` subviews (h/watch!, async workers — registered once in form-2 setup / a form-3 :mount) survive per-render sweeps and are torn down only on page-view remount (navigation) or tab disconnect. Returns the stored spec.
(register-watch! app-state* tab-id source)(register-watch! app-state* tab-id source scope)Register an external source as a full-render subview keyed by source
identity (dedup), returning the subview id. This is the shared primitive
behind every kind of external-source watch:
:mount scope (default) — user h/watch! and route-level :watches;
torn down on page-view remount (navigation) / disconnect.:tab scope — framework watches (routes/head Vars, the component
registry); set up once at SSE connect, torn down only on disconnect.A source watched via several paths collapses to a single subview that
triggers a full re-render on change and is wired by setup-new-watches!.
Register an external `source` as a full-render subview keyed by source identity (dedup), returning the subview id. This is the shared primitive behind every kind of external-source watch: - `:mount` scope (default) — user `h/watch!` and route-level `:watches`; torn down on page-view remount (navigation) / disconnect. - `:tab` scope — framework watches (routes/head Vars, the component registry); set up once at SSE connect, torn down only on disconnect. A source watched via several paths collapses to a single subview that triggers a full re-render on change and is wired by `setup-new-watches!`.
(render-async! app-state*
tab-id
session-id
router
key
fallback-id
user-deps
fetch-fn
render-fn)Render an h/async region during a full page render.
Coordinates the backing fetch (see coordinate-async!), renders the body
via (render-fn @cell) where @cell is the status map
{:status :loading|:reloading|:ready|:error :result … :error …}, injects
the component id for targeted partial fragments, caches the HTML, and
registers a render-scoped, partial-on-change subview whose deps are
[cell & user-deps]:
cell (the worker finishing) re-renders only this region;:unmount (run when the region is swept — it disappeared from the view
tree — or on disconnect) interrupts any in-flight fetch and drops the
coordination store. The render body must return a single rooted hiccup
element (as with reactive), so the id can be injected. Returns the
hiccup.
Render an `h/async` region during a full page render.
Coordinates the backing fetch (see `coordinate-async!`), renders the body
via `(render-fn @cell)` where `@cell` is the status map
`{:status :loading|:reloading|:ready|:error :result … :error …}`, injects
the component id for targeted partial fragments, caches the HTML, and
registers a render-scoped, partial-on-change subview whose deps are
`[cell & user-deps]`:
- a write to `cell` (the worker finishing) re-renders only this region;
- a change to a user dep re-runs the body, whose coordination step detects
the change and refetches (stale-while-revalidate).
`:unmount` (run when the region is swept — it disappeared from the view
tree — or on disconnect) interrupts any in-flight fetch and drops the
coordination store. The render body must return a single rooted hiccup
element (as with `reactive`), so the id can be injected. Returns the
hiccup.(render-reactive! app-state* tab-id key fallback-id deps render-fn)Render a render-bearing subview inline during a full page render.
Always re-executes render-fn because the body may close over parent data
(cursor values, fn args, watched snapshots) that changed but is not tracked
in deps. Injects the subview id onto the returned element (or uses an
existing :id), caches the serialized HTML for later partial re-renders, and
registers the subview as live. Returns the hiccup.
Identity resolves as key (path-scoped) > the root element's :id >
fallback-id (the positional id), and keys the registry and morph anchor
alike. A key also scopes the region path for nested regions.
:on-change defaults to :partial — a dep change re-renders only this
region.
Render a render-bearing subview inline during a full page render. Always re-executes `render-fn` because the body may close over parent data (cursor values, fn args, watched snapshots) that changed but is not tracked in `deps`. Injects the subview id onto the returned element (or uses an existing :id), caches the serialized HTML for later partial re-renders, and registers the subview as live. Returns the hiccup. Identity resolves as `key` (path-scoped) > the root element's `:id` > `fallback-id` (the positional id), and keys the registry and morph anchor alike. A `key` also scopes the region path for nested regions. `:on-change` defaults to `:partial` — a dep change re-renders only this region.
(scoped-id prefix tab-id token)Compose a CSS-safe region id from prefix, tab-id, the ambient
*region-path*, and token. A nested region encodes its parent path as a
hex hash segment rather than a path-joined string, so the id stays a valid
#id selector for Datastar morphing.
Compose a CSS-safe region id from `prefix`, `tab-id`, the ambient `*region-path*`, and `token`. A nested region encodes its parent path as a hex hash segment rather than a path-joined string, so the id stays a valid `#id` selector for Datastar morphing.
(setup-new-watches! app-state* tab-id trigger-render! enqueue-partial!)Set up dep watches for subviews that don't already have them. Called after each full render to wire up new/changed subviews.
trigger-render! is a zero-arg fn (full re-render) for :full subviews;
enqueue-partial! is a one-arg fn (component-id) for :partial subviews.
Either may be nil when that change policy is not in use.
Set up dep watches for subviews that don't already have them. Called after each full render to wire up new/changed subviews. `trigger-render!` is a zero-arg fn (full re-render) for `:full` subviews; `enqueue-partial!` is a one-arg fn (component-id) for `:partial` subviews. Either may be nil when that change policy is not in use.
(setup-subview-watches! app-state*
tab-id
sid
deps
on-change
trigger-render!
enqueue-partial!)Set up watches on a subview's deps. When any dep changes, the callback
fires according to on-change: :full invokes trigger-render! (a full
page re-render), anything else (:partial) invokes (enqueue-partial! sid).
Uses reference counting via retain/release on each dep.
Set up watches on a subview's deps. When any dep changes, the callback fires according to `on-change`: `:full` invokes `trigger-render!` (a full page re-render), anything else (`:partial`) invokes `(enqueue-partial! sid)`. Uses reference counting via retain/release on each dep.
(spawn-worker! app-state* tab-id sid session-id router worker-fn)Register (idempotently) a mount-scoped background-worker subview keyed by
sid. On first registration spawns a virtual thread that runs
worker-fn with *request* rebound to this tab's context, stores the
Thread as the subview :resource, and installs an :unmount that
interrupts it.
The worker runs on a fresh virtual thread, which starts with its own thread-locals, so:
*request* is rebound explicitly (like the action macro) so cursor
reads/writes resolve to this tab;Mount-scoped: it survives the per-render sweep and is torn down only on
page-view remount (navigation / handler redefinition) or tab disconnect,
at which point :unmount interrupts the thread.
Idempotent: a form-1 render body re-invokes h/spawn! on every render, so
only the first occurrence (when sid is absent) actually spawns; later
renders are a no-op.
Register (idempotently) a mount-scoped background-worker subview keyed by `sid`. On first registration spawns a virtual thread that runs `worker-fn` with `*request*` rebound to this tab's context, stores the `Thread` as the subview `:resource`, and installs an `:unmount` that interrupts it. The worker runs on a fresh virtual thread, which starts with its own thread-locals, so: - `*request*` is rebound explicitly (like the `action` macro) so cursor reads/writes resolve to this tab; - the render guard sees a clean thread, so the worker may write cursors freely (the whole point of a worker); - cursor writes hit the live app-state directly. Mount-scoped: it survives the per-render sweep and is torn down only on page-view remount (navigation / handler redefinition) or tab disconnect, at which point `:unmount` interrupts the thread. Idempotent: a form-1 render body re-invokes `h/spawn!` on every render, so only the first occurrence (when `sid` is absent) actually spawns; later renders are a no-op.
(sweep-stale! app-state* tab-id live-ids)Remove render-scoped subviews that were not re-registered during the last
full render: tear down their watches, release dep refcounts, and run any
:unmount. Mount-scoped subviews (h/watch!, async) are immune — they are
torn down by teardown-mount-scoped! (on remount) or teardown-all! (on
disconnect).
Remove *render-scoped* subviews that were not re-registered during the last full render: tear down their watches, release dep refcounts, and run any :unmount. Mount-scoped subviews (h/watch!, async) are immune — they are torn down by `teardown-mount-scoped!` (on remount) or `teardown-all!` (on disconnect).
(teardown-all! app-state* tab-id)Remove all subviews for a tab. Called on disconnect.
Remove all subviews for a tab. Called on disconnect.
(teardown-mount-scoped! app-state* tab-id)Tear down all mount-scoped subviews for a tab (h/watch!, async workers). Called when the page-view remounts (navigation / handler redefinition) — the new mount re-registers whatever it still needs.
Tear down all *mount-scoped* subviews for a tab (h/watch!, async workers). Called when the page-view remounts (navigation / handler redefinition) — the new mount re-registers whatever it still needs.
(teardown-subview-watches! app-state* tab-id sid)Remove watches and release dep refcounts for a subview.
Remove watches and release dep refcounts for a subview.
(watched-sources app-state* tab-id)The distinct external sources watched via mount-scoped, full-render
subviews (i.e. h/watch!). Used by hyper.test to report a render's
watches.
The distinct external sources watched via mount-scoped, full-render subviews (i.e. h/watch!). Used by `hyper.test` to report a render's watches.
(wire-subview! app-state* tab-id sid)Wire a subview's dep watches when an SSE renderer is present and the subview
has deps not already wired. A no-op before a renderer exists — the first
full render's setup-new-watches! wires it then.
If a dep changed between the render and this wiring (e.g. an async fetch that landed before the watch was attached), fires the change once so the update is not missed.
Wire a subview's dep watches when an SSE renderer is present and the subview has deps not already wired. A no-op before a renderer exists — the first full render's `setup-new-watches!` wires it then. If a dep changed between the render and this wiring (e.g. an async fetch that landed before the watch was attached), fires the change once so the update is not missed.
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |