Embeddable stube runtime instances.
dev.zeko.stube.kernel remains the pure effect fold. This namespace
holds the small amount of mutable runtime state needed to embed that
fold in a host Ring app: live conversations, SSE channels, timers,
subscriptions, and the pending-root baton between shell render and
first SSE attach.
Embeddable stube runtime instances. `dev.zeko.stube.kernel` remains the pure effect fold. This namespace holds the small amount of mutable runtime state needed to embed that fold in a host Ring app: live conversations, SSE channels, timers, subscriptions, and the pending-root baton between shell render and first SSE attach.
(active-conversations k)Snapshot of all active conversations in kernel k.
Snapshot of all active conversations in kernel `k`.
(apply-conv! k cid f)Apply (f conv) → [conv' fragments], push fragments over SSE, and
end the conversation if the kernel marked it ended.
Apply `(f conv) → [conv' fragments]`, push fragments over SSE, and end the conversation if the kernel marked it ended.
(authorized? k request cid)True when request owns conversation cid under kernel k.
True when `request` owns conversation `cid` under kernel `k`.
(conversation k cid)Snapshot of conversation cid in kernel k, or nil.
Snapshot of conversation `cid` in kernel `k`, or nil.
(conversation-csrf-token k cid)The CSRF nonce recorded on conversation cid, or nil.
The CSRF nonce recorded on conversation `cid`, or nil.
(create-conversation! k root-id)(create-conversation! k root-id owner-token)Compatibility helper for standalone server code that already resolved
the owner token. Does not mint a CSRF token — that is the
mint-conversation! (GET shell) path's job; conversations created
here fall back to cookie + SameSite=Lax for cross-site protection.
Compatibility helper for standalone server code that already resolved the owner token. Does not mint a CSRF token — that is the [[mint-conversation!]] (GET shell) path's job; conversations created here fall back to cookie + `SameSite=Lax` for cross-site protection.
(dispatch! k cid {:keys [instance-id] :as event})Dispatch one event into a live conversation and return the fragments produced. Also pushes those fragments to an open SSE stream.
Dispatch one event into a live conversation and return the fragments produced. Also pushes those fragments to an open SSE stream.
(dispatch-to! k {:keys [cid target-iid event]})Asynchronously deliver event to target-iid in conversation cid
under kernel k. Fired by the :dispatch-to effect; runs on a
background future so the current handler can complete first. The
matching event is dropped (the standard stale-event path) if the
target instance is gone by the time the future runs.
Asynchronously deliver `event` to `target-iid` in conversation `cid` under kernel `k`. Fired by the `:dispatch-to` effect; runs on a background future so the current handler can complete first. The matching event is dropped (the standard stale-event path) if the target instance is gone by the time the future runs.
(end-conversation! k cid)Drop a conversation, any SSE binding, pending root, timers, subscriptions, and persisted copy.
Drop a conversation, any SSE binding, pending root, timers, subscriptions, and persisted copy.
(ensure-session k request)Return [sid set-cookie-header-or-nil] for request under kernel
k. Kernels with a custom :session-id-fn default to host-managed
sessions and do not mint a stube cookie.
Return `[sid set-cookie-header-or-nil]` for `request` under kernel `k`. Kernels with a custom `:session-id-fn` default to host-managed sessions and do not mint a stube cookie.
(halt! k)Drain a kernel. Sequence:
:stop hooks for every live instance (children before
their frame, top stack frame first).:close fragment.save! per conversation.Returns nil. Idempotent: subsequent calls on the same kernel are cheap no-ops.
Drain a kernel. Sequence:
1. Mark the kernel as shutting down so HTTP adapters can refuse
new conversation mints.
2. Cancel pending scheduled events.
3. Run `:stop` hooks for every live instance (children before
their frame, top stack frame first).
4. Drain open SSE streams with a final `:close` fragment.
5. Flush the store with one last `save!` per conversation.
6. Clear per-kernel runtime registries.
Returns nil. Idempotent: subsequent calls on the same kernel are
cheap no-ops.(head-tags k)Return Hiccup head nodes required by shell-for for kernel k.
Return Hiccup head nodes required by [[shell-for]] for kernel `k`.
(make-kernel)(make-kernel opts)Create an embeddable stube runtime instance.
Stable embedder options:
:context-fn — (fn [request] ctx) stored on each conversation and
readable in handlers with (s/context self).:app — opaque host value (typically a small map of dependencies
such as {:db ds :mail mailer}). Read from component code with
(s/app). Not persisted; rebuild from live JVM state on each
make-kernel call.:principal-fn — (fn [request] principal-or-nil) invoked once at
mint time. Result is persisted on the conversation as
:conv/principal; component code reads it with (s/principal).
Re-authentication is the host's job — if the principal needs to
change, end the conversation and re-mint.:store — persistence backend from dev.zeko.stube.store.:base-path — URL prefix used by shell/render helpers.:session-id-fn — host session lookup; defaults to stube_sid.:on-conv-mint — optional (fn [conv request] conv') hook.:on-error — reserved hook for adapter error reporting.:base-css — vector of stylesheet URLs (strings) that
head-tags should emit unconditionally, before any
component-derived stylesheet. Use this when the host has
page-wide CSS that must appear on every page — including pages
that do not embed a stube shell (so component-scoped
stube_styles/<ns>/<name>.css is not enough). URLs are emitted
verbatim; relative URLs resolve against the host page, absolute
URLs are passed through unchanged.:css-layer-order — optional vector of CSS layer names (strings or
keywords). When provided, head-tags emits a top-level
@layer name1, name2, …; declaration before any
component-derived stylesheet <link>. Because CSS layers honour
the first declaration order in the document, this fixes the
cascade order across the per-component stylesheets that
head-tags auto-emits alphabetically from
resources/stube_styles/<ns>/<name>.css — host CSS no longer has
to live in a single ordered file just to control layer order.:eager-scripts — vector of inline JS snippets (strings)
head-tags should emit as synchronous <script> blocks in
<head> before any type="module" script. Use this to seed
window.<X> namespaces that inline Datastar expressions
(data-on:input="window.X.foo(...)") need available from
frame 1, before the deferred ESM module graph finishes. Snippets
are emitted verbatim and concatenated into a single <script>
block; the host is responsible for the contents (no escaping).:signal-case — :kebab (default) or :camel. Picks the wire
casing for every signal helper that renders a data-bind:<key>
attribute, builds an inline-expression $ref, or reads a signal
off a posted event (dev.zeko.stube.render/bind,
dev.zeko.stube.render/local-bind, dev.zeko.stube.render/$,
dev.zeko.stube.render/signal). Per-call {:case ...} opts
still win. Choose :camel when any inline Datastar expression
references a signal (JS identifiers can't contain dashes);
:kebab when all signal access is pure-Clojure.:sse-keepalive-ms — interval in milliseconds for the SSE
heartbeat that keeps reverse-proxy idle timers happy. Defaults
to 15000. Set to nil or 0 to disable (e.g. when the host's proxy
has no idle timeout, or in tests).:max-signals-bytes — cap (in bytes) on the JSON signals body of
an event POST. Default 64 KiB. Oversize requests get a 413
without the body being parsed or fully buffered.:max-payload-bytes — cap (in bytes) on the EDN payload query
param of an event POST. Default 4 KiB. Oversize → 413;
unparseable → 400. The bound also caps EDN nesting depth, so a
deeply-nested value cannot exhaust the parser stack.:dev-cookie? — when true, the stube_sid cookie is minted
without the Secure attribute. Default false (secure): an
embedded kernel runs behind the host's TLS. Flip this on only when
the same kernel serves plain HTTP (localhost dev) — otherwise the
browser refuses to send the cookie back and every conversation
appears cross-session. Ignored when the host supplies its own
:session-id-fn / :ensure-session-fn.:cookie-domain / :cookie-path — scope the stube_sid cookie.
Default no Domain and Path=/. Set :cookie-path to a mount
prefix when several independent stube apps share an origin.:max-upload-bytes — cap (in bytes) on a multipart upload body,
checked against Content-Length before parsing. Default 10 MiB.
Oversize → 413.:keep-upload? — by default the upload handler deletes ring's
multipart tempfiles once the :upload-received dispatch has
consumed them. Set this true when a handler hands the tempfile to
asynchronous processing (e.g. an :io thunk) and takes
responsibility for deleting it itself.:on-auth-fail / :on-stale / :on-shell-mint — optional audit
hooks, each (fn [info]), default nil (no-op). :on-auth-fail
fires when an owner-cookie or CSRF check rejects a request
(info has :request :cid :route :reason); :on-stale
fires when a 410-stale response goes out (:cid);
:on-shell-mint fires when a GET mints a conversation (:request
:cid :flow-id). A throwing hook is swallowed and logged.:before-dispatch — optional authz / rate-limit seam,
(fn [conv event request]) run just before an event is dispatched.
Return :continue to proceed or [:reject status body] to short-
circuit with that response. Fails closed: a throwing hook rejects
the request.Create an embeddable stube runtime instance.
Stable embedder options:
* `:context-fn` — `(fn [request] ctx)` stored on each conversation and
readable in handlers with `(s/context self)`.
* `:app` — opaque host value (typically a small map of dependencies
such as `{:db ds :mail mailer}`). Read from component code with
`(s/app)`. Not persisted; rebuild from live JVM state on each
`make-kernel` call.
* `:principal-fn` — `(fn [request] principal-or-nil)` invoked once at
mint time. Result is persisted on the conversation as
`:conv/principal`; component code reads it with `(s/principal)`.
Re-authentication is the host's job — if the principal needs to
change, end the conversation and re-mint.
* `:store` — persistence backend from `dev.zeko.stube.store`.
* `:base-path` — URL prefix used by shell/render helpers.
* `:session-id-fn` — host session lookup; defaults to `stube_sid`.
* `:on-conv-mint` — optional `(fn [conv request] conv')` hook.
* `:on-error` — reserved hook for adapter error reporting.
* `:base-css` — vector of stylesheet URLs (strings) that
[[head-tags]] should emit unconditionally, before any
component-derived stylesheet. Use this when the host has
page-wide CSS that must appear on every page — including pages
that do not embed a stube shell (so component-scoped
`stube_styles/<ns>/<name>.css` is not enough). URLs are emitted
verbatim; relative URLs resolve against the host page, absolute
URLs are passed through unchanged.
* `:css-layer-order` — optional vector of CSS layer names (strings or
keywords). When provided, [[head-tags]] emits a top-level
`@layer name1, name2, …;` declaration before any
component-derived stylesheet `<link>`. Because CSS layers honour
the *first* declaration order in the document, this fixes the
cascade order across the per-component stylesheets that
`head-tags` auto-emits alphabetically from
`resources/stube_styles/<ns>/<name>.css` — host CSS no longer has
to live in a single ordered file just to control layer order.
* `:eager-scripts` — vector of inline JS snippets (strings)
[[head-tags]] should emit as synchronous `<script>` blocks in
`<head>` *before* any `type="module"` script. Use this to seed
`window.<X>` namespaces that inline Datastar expressions
(`data-on:input="window.X.foo(...)"`) need available from
frame 1, before the deferred ESM module graph finishes. Snippets
are emitted verbatim and concatenated into a single `<script>`
block; the host is responsible for the contents (no escaping).
* `:signal-case` — `:kebab` (default) or `:camel`. Picks the wire
casing for every signal helper that renders a `data-bind:<key>`
attribute, builds an inline-expression `$ref`, or reads a signal
off a posted event ([[dev.zeko.stube.render/bind]],
[[dev.zeko.stube.render/local-bind]], [[dev.zeko.stube.render/$]],
[[dev.zeko.stube.render/signal]]). Per-call `{:case ...}` opts
still win. Choose `:camel` when any inline Datastar expression
references a signal (JS identifiers can't contain dashes);
`:kebab` when all signal access is pure-Clojure.
* `:sse-keepalive-ms` — interval in milliseconds for the SSE
heartbeat that keeps reverse-proxy idle timers happy. Defaults
to 15000. Set to nil or 0 to disable (e.g. when the host's proxy
has no idle timeout, or in tests).
* `:max-signals-bytes` — cap (in bytes) on the JSON signals body of
an event POST. Default 64 KiB. Oversize requests get a `413`
without the body being parsed or fully buffered.
* `:max-payload-bytes` — cap (in bytes) on the EDN `payload` query
param of an event POST. Default 4 KiB. Oversize → `413`;
unparseable → `400`. The bound also caps EDN nesting depth, so a
deeply-nested value cannot exhaust the parser stack.
* `:dev-cookie?` — when true, the `stube_sid` cookie is minted
*without* the `Secure` attribute. Default false (secure): an
embedded kernel runs behind the host's TLS. Flip this on only when
the same kernel serves plain HTTP (localhost dev) — otherwise the
browser refuses to send the cookie back and every conversation
appears cross-session. Ignored when the host supplies its own
`:session-id-fn` / `:ensure-session-fn`.
* `:cookie-domain` / `:cookie-path` — scope the `stube_sid` cookie.
Default no `Domain` and `Path=/`. Set `:cookie-path` to a mount
prefix when several independent stube apps share an origin.
* `:max-upload-bytes` — cap (in bytes) on a multipart upload body,
checked against `Content-Length` before parsing. Default 10 MiB.
Oversize → `413`.
* `:keep-upload?` — by default the upload handler deletes ring's
multipart tempfiles once the `:upload-received` dispatch has
consumed them. Set this true when a handler hands the tempfile to
asynchronous processing (e.g. an `:io` thunk) and takes
responsibility for deleting it itself.
* `:on-auth-fail` / `:on-stale` / `:on-shell-mint` — optional audit
hooks, each `(fn [info])`, default nil (no-op). `:on-auth-fail`
fires when an owner-cookie or CSRF check rejects a request
(`info` has `:request` `:cid` `:route` `:reason`); `:on-stale`
fires when a `410`-stale response goes out (`:cid`);
`:on-shell-mint` fires when a GET mints a conversation (`:request`
`:cid` `:flow-id`). A throwing hook is swallowed and logged.
* `:before-dispatch` — optional authz / rate-limit seam,
`(fn [conv event request])` run just before an event is dispatched.
Return `:continue` to proceed or `[:reject status body]` to short-
circuit with that response. Fails closed: a throwing hook rejects
the request.(mint-conversation! k root-id request)(mint-conversation! k root-id init-args request)(mint-conversation! k root-id init-args request owner-token)Register a new conversation for root-id and return its cid.
The 4-arity calls ensure-session internally; the 5-arity accepts
a pre-computed owner-token from a caller that already ran
ensure-session and needs the conversation to be owned by that
exact session (otherwise ensure-session, called with a still-
cookieless request, would mint a second sid and own the conv with
one the browser will never present).
Register a new conversation for `root-id` and return its cid. The 4-arity calls [[ensure-session]] internally; the 5-arity accepts a pre-computed `owner-token` from a caller that already ran ensure-session and needs the conversation to be owned by *that* exact session (otherwise ensure-session, called with a still- cookieless request, would mint a second sid and own the conv with one the browser will never present).
(pending-root k cid)Pop and return the root embed/flow for cid, if any.
Pop and return the root embed/flow for `cid`, if any.
(publish! k topic msg)Asynchronously deliver msg to every live subscriber of topic in
kernel k.
Asynchronously deliver `msg` to every live subscriber of `topic` in kernel `k`.
(publish-local! k cid topic msg)Like publish!, but only delivers to subscribers in conversation
cid. Use this for parent/child or sibling channels that must
stay within one browser tab; other conversations' subscribers on
the same topic do not see the message.
Returns the number of subscribers targeted.
Like [[publish!]], but only delivers to subscribers in conversation `cid`. Use this for parent/child or sibling channels that must stay within one browser tab; other conversations' subscribers on the same topic do not see the message. Returns the number of subscribers targeted.
(reap! k ttl)End conversations whose :conv/touched is older than ttl.
End conversations whose `:conv/touched` is older than `ttl`.
(rendered-shell-for! k root-id request)(rendered-shell-for! k root-id init-args request)Mint a conversation for root-id, boot it server-side, and return a
Hiccup shell whose #root already contains the rendered first
paint.
Pairs with shell-for for cases where the host needs a readable
GET response (static /about pages, SEO-visible content, no-JS
fallbacks) instead of an empty <div id="root"> that fills in
over the SSE connection.
Returns {:cid <cid> :shell <hiccup>}. The shell carries the same
data-init that opens the SSE stream, so once the browser
connects the conversation is fully interactive — but the initial
HTML is already there at first paint.
Marks the conversation :conv/server-rendered? true; the SSE
handler reads this flag and skips the resume-render that path 2
would otherwise fire (which would re-emit the same HTML and run
:wakeup hooks meant for crash-resume, not for first attach).
The flag is cleared on the first SSE attach, so subsequent
reattaches (a network blip, a hot reload) behave like the normal
restore path.
Limitations: if the root component's :start emits more than one
visible fragment (e.g. a call-in-slot during boot), only the
primary #root inner fragment is inlined; the extras would be
pushed over SSE on attach, the same way they are today.
Mint a conversation for `root-id`, boot it server-side, and return a
Hiccup shell whose `#root` already contains the rendered first
paint.
Pairs with [[shell-for]] for cases where the host needs a readable
GET response (static `/about` pages, SEO-visible content, no-JS
fallbacks) instead of an empty `<div id="root">` that fills in
over the SSE connection.
Returns `{:cid <cid> :shell <hiccup>}`. The shell carries the same
`data-init` that opens the SSE stream, so once the browser
connects the conversation is fully interactive — but the initial
HTML is already there at first paint.
Marks the conversation `:conv/server-rendered? true`; the SSE
handler reads this flag and skips the resume-render that path 2
would otherwise fire (which would re-emit the same HTML and run
`:wakeup` hooks meant for crash-resume, not for first attach).
The flag is cleared on the first SSE attach, so subsequent
reattaches (a network blip, a hot reload) behave like the normal
restore path.
Limitations: if the root component's `:start` emits more than one
visible fragment (e.g. a `call-in-slot` during boot), only the
primary `#root inner` fragment is inlined; the extras would be
pushed over SSE on attach, the same way they are today.(replay-with k root-id events)Purely replay events against a fresh conversation rooted at
root-id, using k's render configuration but mutating no runtime
state. See dev.zeko.stube.embed/replay-with for the public-facing
forwarder.
Purely replay `events` against a fresh conversation rooted at `root-id`, using `k`'s render configuration but mutating no runtime state. See [[dev.zeko.stube.embed/replay-with]] for the public-facing forwarder.
(rotate-session! k cid)Rotate the session that owns conversation cid: mint a fresh
stube_sid, record it as the new :conv/owner-token, and return the
Set-Cookie header string the host must attach to its response (built
with this kernel's cookie attributes). Returns nil if cid is
unknown or the kernel uses host-managed sessions.
Call this on login / logout: a fixed session id surviving a privilege
change is the classic session-fixation hazard. The conversation's
CSRF token is left intact, so the current page keeps working; the
old stube_sid stops authorizing as soon as the new cookie lands.
Rotate the session that owns conversation `cid`: mint a fresh `stube_sid`, record it as the new `:conv/owner-token`, and return the `Set-Cookie` header string the host must attach to its response (built with this kernel's cookie attributes). Returns nil if `cid` is unknown or the kernel uses host-managed sessions. Call this on login / logout: a fixed session id surviving a privilege change is the classic session-fixation hazard. The conversation's CSRF token is left intact, so the *current* page keeps working; the old `stube_sid` stops authorizing as soon as the new cookie lands.
(schedule-event! k {:keys [cid instance-id delay-ms event]})Schedule a future event for a cid/iid within kernel k.
Schedule a future event for a cid/iid within kernel `k`.
(shell-for k cid)Return the embeddable Hiccup shell fragment for conversation cid.
Return the embeddable Hiccup shell fragment for conversation `cid`.
(shutting-down? k)True once halt! has begun the shutdown sequence for k. HTTP
adapters should refuse new conversation mints (typically 503) while
this is true.
True once [[halt!]] has begun the shutdown sequence for `k`. HTTP adapters should refuse new conversation mints (typically 503) while this is true.
(subscribe! k {:keys [cid instance-id topic event]})Subscribe cid/iid to topic within kernel k.
Subscribe cid/iid to `topic` within kernel `k`.
(swap-conv! k cid f)Apply (f conv) → [conv' fragments] to conversation cid under the
per-cid lock, then atomically commit conv'. f is called exactly
once — unlike a bare swap! whose retry semantics would re-run f
(and its side effects) under contention.
Apply `(f conv) → [conv' fragments]` to conversation `cid` under the per-cid lock, then atomically commit `conv'`. `f` is called exactly once — unlike a bare `swap!` whose retry semantics would re-run `f` (and its side effects) under contention.
(unsubscribe! k {:keys [cid instance-id topic]})Remove one cid/iid subscription. If topic is nil, remove all of
the instance's subscriptions.
Remove one cid/iid subscription. If `topic` is nil, remove all of the instance's subscriptions.
(with-kernel-bindings k cid f)Run f with render URL context and async hooks for kernel k.
Run `f` with render URL context and async hooks for kernel `k`.
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 |