Zodiac extension for remuda + darkstar.
The core is plain Ring and knows nothing about zodiac; this
namespace is the wiring, following the shape of zodiac-hot-reload: integrant
keys, route registration, and a config transformer. No domain logic. If this
file grows past wiring, 's map-of-pieces design has failed.
Usage:
(z/start {:extensions [(z.live/init {:components {:chat #'app/chat}
:render-fn chassis/html
:source my-source
:secret "..."})]})
What it wires:
Redefining :render at a REPL must reach already-connected
contexts. The engine resolves a component by name on every render and derefs it
if it is deref-able, so an adapter should register vars — #'app/chat, not
app/chat. A plain map still works but only whole-map replacement is visible.
The live-context registry, subscription registry and cache are held in
defonced atoms rather than created per init-key, because requires them
to survive tools.namespace/refresh. Recreating them on reload would drop every
connection — the same failure as a server restart, triggered by saving a file.
Zodiac extension for remuda + darkstar.
The core is plain Ring and knows nothing about zodiac; this
namespace is the wiring, following the shape of `zodiac-hot-reload`: integrant
keys, route registration, and a config transformer. **No domain logic.** If this
file grows past wiring, 's map-of-pieces design has failed.
Usage:
(z/start {:extensions [(z.live/init {:components {:chat #'app/chat}
:render-fn chassis/html
:source my-source
:secret "..."})]})
What it wires:
- the engine, the pubsub bus and registry, and the fragment cache as integrant
components, so they participate in zodiac's lifecycle;
- an SSE route and an action route;
- the flush loop that turns coalesced hints into pushes.
## Components are registered as vars, deliberately
Redefining `:render` at a REPL must reach already-connected
contexts. The engine resolves a component by name on every render and derefs it
if it is deref-able, so **an adapter should register vars** — `#'app/chat`, not
`app/chat`. A plain map still works but only whole-map replacement is visible.
## Registries survive reload
The live-context registry, subscription registry and cache are held in
`defonce`d atoms rather than created per `init-key`, because requires them
to survive `tools.namespace/refresh`. Recreating them on reload would drop every
connection — the same failure as a server restart, triggered by saving a file.(action-handler {:keys [params] :as request})The interaction route. Looks up the live context and dispatches an event.
Args arrive as a JSON payload and are passed through uncoerced,
because their types already survived the wire. That is the whole point of the
payload form: the previous query-string encoding forced every receiver to guess a
type, and dev/slice guessed with a hardcoded parse-long that silently turned
string args into nil.
liveId and event fall back to query params so a plain form post still works,
but args are read only from the payload — a query-string arg cannot carry a
type, and admitting one would reopen defect 3 through the back door.
The interaction route. Looks up the live context and dispatches an event. Args arrive as a JSON payload and are passed through **uncoerced**, because their types already survived the wire. That is the whole point of the payload form: the previous query-string encoding forced every receiver to guess a type, and `dev/slice` guessed with a hardcoded `parse-long` that silently turned string args into nil. `liveId` and `event` fall back to query params so a plain form post still works, but *args* are read only from the payload — a query-string arg cannot carry a type, and admitting one would reopen defect 3 through the back door.
Where this extension puts itself in zodiac's request context.
A var rather than a literal keyword in each caller, because a mismatched
namespaced keyword fails silently — the lookup returns nil and the failure
surfaces somewhere unrelated. That already cost a debugging session once in this
project (a ::cache that resolved to the wrong namespace), so the key is named
once and referenced.
Where this extension puts itself in zodiac's request context. A var rather than a literal keyword in each caller, because a mismatched namespaced keyword fails *silently* — the lookup returns nil and the failure surfaces somewhere unrelated. That already cost a debugging session once in this project (a `::cache` that resolved to the wrong namespace), so the key is named once and referenced.
(init {:keys [components render-fn source secret sse-fn signals-fn interval-ms
bus]})Creates a zodiac extension for zodiac-live.
Wires the engine, bus, flusher and handlers as integrant components and injects
them into zodiac's request context, so a handler reaches them via
[::z/context ::live].
Options:
:components map of name -> component (register vars, ):render-fn hiccup -> string:source a remuda.source/Source:secret HMAC key for recovery snapshots:sse-fn (fn [ctx] -> ring response) owning the transport:signals-fn (fn [request] -> map) reading client signals, optional:interval-ms flush interval, default 100:bus a PubSub; an in-process one is created if absentCreates a zodiac extension for zodiac-live. Wires the engine, bus, flusher and handlers as integrant components and injects them into zodiac's request context, so a handler reaches them via `[::z/context ::live]`. Options: - `:components` map of name -> component (register **vars**, ) - `:render-fn` hiccup -> string - `:source` a `remuda.source/Source` - `:secret` HMAC key for recovery snapshots - `:sse-fn` (fn [ctx] -> ring response) owning the transport - `:signals-fn` (fn [request] -> map) reading client signals, optional - `:interval-ms` flush interval, default 100 - `:bus` a `PubSub`; an in-process one is created if absent
(live-ctx request)The extension's components, from zodiac's injected request context.
The extension's components, from zodiac's injected request context.
(publish! live topic)Announces that topic changed.
live is the extension's context map, from live-ctx.
Announces that `topic` changed. `live` is the extension's context map, from `live-ctx`.
(recovery-snapshot system id secret)A signed recovery snapshot for id, for the caller to send to the browser.
A signed recovery snapshot for `id`, for the caller to send to the browser.
id -> live context. Survives namespace reload.
id -> live context. Survives namespace reload.
(routes)(routes {:keys [sse-path action-path]})The routes zodiac-live needs, for the application to splice into its own route vector.
Returned rather than injected because zodiac's :routes is typically a var the
application owns, and an extension reaching in to conj onto it would fight that
ownership — and would break if the application passed a function rather than a
vector. The application writes:
(defn routes []
["" (z.live/routes)
["/" {:handler home}]])
The handlers read their dependencies from the request context, which is how they avoid needing an integrant ref at route-definition time.
The routes zodiac-live needs, for the application to splice into its own route
vector.
Returned rather than injected because zodiac's `:routes` is typically a var the
application owns, and an extension reaching in to `conj` onto it would fight that
ownership — and would break if the application passed a function rather than a
vector. The application writes:
(defn routes []
["" (z.live/routes)
["/" {:handler home}]])
The handlers read their dependencies from the request context, which is how they
avoid needing an integrant ref at route-definition time.(sse-handler request respond _raise)The connection route, in zodiac's async 3-arity form.
Delegates everything to the caller's sse-fn, which owns the transport, because
The server must stay replaceable — this namespace must not name
Jetty, http-kit, or a datastar adapter.
The connection route, in zodiac's async 3-arity form. Delegates everything to the caller's `sse-fn`, which owns the transport, because The server must stay replaceable — this namespace must not name Jetty, http-kit, or a datastar adapter.
(subscribe! live id topics)Subscribes live context id to topics.
Throws on a missing bus rather than silently doing nothing: this runs inside an
SSE on-open callback where a swallowed exception looks like a connection that
opens and then sends nothing at all.
Subscribes live context `id` to `topics`. Throws on a missing bus rather than silently doing nothing: this runs inside an SSE `on-open` callback where a swallowed exception looks like a connection that opens and then sends nothing at all.
Subscription registry. Survives namespace reload.
Subscription registry. Survives namespace reload.
(verify-snapshot secret signed)Verifies a snapshot string, returning {:ok ...}. See snapshot/verify.
Verifies a snapshot string, returning `{:ok ...}`. See `snapshot/verify`.
Cross-viewer cache. Survives namespace reload.
Cross-viewer cache. Survives namespace reload.
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 |