Big-rock changes to stube. Released versions may batch more than one development entry.
(No changes yet.)
:e2e deps alias and
make e2e target run a browser-driven sanity pass over the live
examples catalogue. Twenty tests pin one stube primitive each:
morph-by-id, s/keyed-children, :call-in-slot, :url projection,
s/decorate, s/preserve, :principal-fn gating, error-frame /
s/answer-error, defflow/s/await/wizard-back, dialogs,
recursive renderers, structured event payloads, table sort,
pagination, inactive-child preservation. The suite is gated to
make e2e and make release; make test stays Clojure-only and
fast. See test-e2e/dev/zeko/stube/e2e/ for the harness.mint-conversation! bugfix. A fresh shell visit minted a
session id for Set-Cookie, then ignored it and minted a second
id inside mint-conversation!. The conversation ended up owned by
the second id while the browser was told to send the first — every
subsequent SSE GET hit the cross-session check and returned 403.
Manual browser sessions papered over this by reusing a cookie from
an earlier tab, but a fully fresh BrowserContext (or any new
install) was broken. shell-handler now threads its sid through to
a new 5-arity mint-conversation!. Pinned by
shell-set-cookie-matches-conv-owner-token in http_test.examples/reading_list.clj Close button. The per-card Close
button used (s/on self :click :as [:close id]), where self is
the item — but :reading/item has no :handle, so the click was a
no-op and the desk's :close resume never ran. Switched to
s/on-target against (:instance/parent self) so the click POSTs
to the desk that owns the handler. The e2e test for URL-driven
restore + close now exercises the full round-trip.dev.zeko.stube.routes
namespace and the stray test that exercised its private fn.
The standalone server has long since used
dev.zeko.stube.adapter.ring/ring-handler; the internals
module map drops the line too.clj-kondo warnings (one in
src/, six in test/). The genuine false positive — kondo
can't see through runtime/cid-lock — gets an
#_:clj-kondo/ignore with a one-line explanation; the rest
are real cleanups.make lint target and made
make test depend on it. clj-kondo exits non-zero on any
warning, so the standard pre-PR check now catches lint
regressions before tests even run. AGENTS.md documents the
workflow and the #_:clj-kondo/ignore escape hatch.defalias macro in
dev.zeko.stube.core and rewrote every plain
(def ^{:doc "..."} foo target/foo) re-export with it.
:arglists now flows through, so (doc s/answer),
(doc s/patch), (doc s/end) — and CIDER eldoc — show the
target signature. clj-kondo learns the form via
:lint-as clj-kondo.lint-as/def-catch-all so dependent
namespaces still resolve s/... names.core_test/every-public-name-has-doc-and-arglists
sweeps ns-publics of dev.zeko.stube.core and fails if any
non-:no-doc var loses its docstring, or any function var
loses :arglists. Locks the R1-04 invariant against
regression.replay-event helper
out of core.clj and runtime.clj into a single public
conversation/replay-event. Both callers (core/replay and
runtime/replay-with) now share the event-shape normalisation
rule.conversation/snapshot-for-dispatch
from inside kernel/dispatch. The [:back] carve-out (a
handler walking history backwards must not have its own
pre-state pushed onto that history first) now lives next to
the conversation data it shapes; kernel/dispatch reads
the snapshot decision in one line instead of ten.registry/register! now throws ex-info
when a defcomponent form declares both :foo and
:component/foo for any lifecycle key (silent lift used to
overwrite the long-form value). The registry ns docstring and
docs/api.md document why lifecycle keys are a closed set
while resume keys (:on-foo, :on-error-foo) pass through
verbatim.subvec + apply hash-map
accessors in effects/call-resume and
effects/slot-call-resume with positional destructure
(Option A from the issue). The wire shape stays a vector and
the kernel multimethod is unchanged; the per-effect
allocation overhead in call/call-in-slot goes away.answer-error fallback warning's
once-per-pair dedup from a JVM-global atom in kernel.clj
onto the kernel value (:!answer-error-warned, reset on
halt!). Two embedded kernels in the same JVM now each emit
their own one-time message. Kernel-less paths (pure
s/dispatch / s/replay) skip the warning entirely.Dynamic bindings section in
docs/internals.md catalogues every ^:dynamic var (15 of
them across kernel/render/effects/errors/flow/dev) by where
it is bound, where it is read, and what nil means. Also
fixes the stale render/*conv* docstring (frame/render-frame
is the binder, not the kernel).load_direction_test codifies the
pure/impure split. Walks ns-aliases transitively from each
pure namespace (conversation, effects, fragments,
kernel, frame, lifecycle, registry, render, plus
the dev/errors/keyed helpers they pull) and fails if
any reach runtime, server, http, the adapters, or the
kit glue.AGENTS.md documents the convention.:url machinery
(S-11) is wired directly into kernel/dispatch via
maybe-emit-url; a hook list would generalise it, but a
hook list with one consumer is harder to read than the
straight call. Decision: not yet. docs/internals.md picks
up a one-paragraph note next to the dispatch-path diagram so
the seam is grep-able when a second consumer appears.requiring-resolve indirection
in dev.zeko.stube.embed. The namespace now contains only
the ten documented public fns
(make-kernel/mint-conversation!/shell-for/head-tags/
dispatch!/replay-with/halt!/shutting-down?/publish!),
each a thin direct delegate to dev.zeko.stube.runtime. The
~25 ^:no-doc plumbing fns it used to expose for adapters
have moved back to runtime; http, halos/http,
adapter/ring, and server :require runtime directly.dev.zeko.stube.server to
lifecycle (start!, stop!, mount!, unmount!, mounts,
reset-state!, the reaper) plus a small default-kernel
convenience surface (default-kernel, conversation,
active-conversations, end!, inspect, publish!). The
~20 wrapper fns that nothing outside the namespace needed
are gone; the two consumer test files (server_test,
http_test) switch to rt/foo (server/default-kernel) ….docs/decisions/0006-embed-as-direct-runtime-facade.md
records the decision behind R1-05 and R1-06. The original
requiring-resolve choice was never written down; this
ADR is the first written form, and the index table also
picks up the missing 0005 entry along the way.nix develop,
clojure -M:examples, the examples table, Datastar
Inspector and (s/inspect cid) hints). Content was reordered,
not rewritten.:example-ring deps.edn alias runs the plain-Ring embedded
example (examples/dev/zeko/stube/examples/embedded_ring.clj)
via clojure -M:example-ring instead of needing a REPL.Road to 1.0 (breaking, pre-1.0): trimmed pre-1.0 surface in preparation for stability.
:emit-on-mount colocated key; use :start
directly. :start already covers the effect-only case
((fn [self] [self effects])), so the sugar layer was net
cruft. Tutorial chapter and reading_list.clj updated. The
S-12 CHANGELOG note below describes the original sugar.:legacy standalone
paths (/conv/:cid/sse, /conv/:cid/:iid/:event, …) are gone;
every kernel now uses the :adapter paths (/sse/:cid,
/event/:cid/:iid/:event, …). :route-style is no longer
an option to s/start!, make-kernel, or the shell.s/with-app and s/with-principal macros so component
tests no longer reach into dev.zeko.stube.kernel/*current-*
directly. Docstrings, docs/api.md, docs/tutorial.md, and
ADR 0004 updated to use them.S-15: s/on-unmount Hiccup helper for preserved hosts. Mirrors
s/on-mount: returns a data-stube-on-unmount attribute carrying
a synchronous JS expression with el bound to the host element.
preserve.js grew a document-wide MutationObserver that fires the
expression once when the host genuinely detaches — queueMicrotask
defers the check so Idiomorph's detach+reattach swap dance can't
double-fire. Logs to console.error on throws; never blocks the
morph. CodeMirror/Chart.js/<video> integrations now have a real
teardown path. README and preserved_widget.clj updated.
S-14: (s/answer-error ex) + :on-error-<key> resume keys.
Symmetric child→parent failure routing — the child catches its
exception and emits (s/answer-error ex); the parent declares
:on-error-saved next to :on-saved and receives the exception
verbatim. Three-tier fallback: explicit :on-error-<key> →
:on-<key> with [:error ex] plus a one-time deprecation
warning per cdef → the default error-frame banner. New ADR
0005-answer-error-and-resume.md; new worked example
error_answer.clj; todo.md §2 entry removed.
S-13: New docs/api.md section "Reading dependencies — app
vs context vs principal" with a comparison table, decision
tree by question, three common mistakes (notably reading
(s/context self) from :init), and a worked migration moving
the DB choice from :app to :context-fn. Cross-refs from the
existing s/app / s/context / s/principal entries, from the
defcomponent :init row, and from make-kernel's opts list.
S-12: Shareable-URL bootstrap recipe and :emit-on-mount sugar.
Declaring :emit-on-mount (fn [self] effects) lifts to
:component/start at registration; declaring both is a register-time
error. New worked example reading_list.clj demonstrates the
three-piece pattern (:init-args-fn → :emit-on-mount →
:url) end-to-end. New tutorial chapter "Shareable views — URL as
durable state" walks through the same flow. docs/api.md
cross-refs from mount! and keyed-children.
S-11: Root-component :url key for declarative URL sync. Returns
nil, a string, or [:replace|:push url]; the kernel diffs against
:conv/last-url after every dispatch and auto-emits a [:history …]
effect on change. Explicit (s/history …) from the handler always
wins. Only the root frame's :url applies. :init-args-fn on
mount! pairs with this to read the URL back in on a fresh mount.
url_state_counter.clj refactored to use the new form;
url_state_counter_manual.clj preserves the hand-rolled version for
comparison.
:call-in-slot previous-chain leak surfaced by
kernel-property-test during the 0.1.1 sweep. New
conversation/subtree-ids walks :instance/previous chains
alongside :instance/children and :instance/keyed-slots; the
frame-destruction paths (pop-top, :replace, :end, root-frame
:answer, keyed-child removal, runtime halt!) use it so
previous-chain instances get their :stop hooks and are swept
from :conv/instances. The narrow descendant-ids survives for
paths where the previous gets restored (answer-from-slot,
mark-rendered, history wakeup). Pinned by a focused regression in
embed-test and by tightened structural assertions in
kernel-property-test.Road-to-1.0 sweep. See todo.md for the items deliberately deferred
past this release (composition spikes, popstate, extra HTTP routes,
the :call-in-slot previous-chain leak surfaced by the new property
test).
kernel_property_test uses test.check
to walk random event sequences through kernel/dispatch against a
stub registry, asserting after every step that (a) no throw,
(b) pr-str/read-string round-trip yields an equal conversation,
(c) every :elements/:error fragment that names an iid selector
refers to an instance that exists post-dispatch, and (d) the call
stack and :instance/children only reference live instances.
The test surfaced a real leak: :call-in-slot's
:instance/previous chain is orphaned when the parent frame is
replaced or ended before the slot child answers. Out of scope here;
named and bounded by a comment in the test so it does not get
forgotten.kernel, server, conversation, runtime,
render, frame, fragments, http, lifecycle, effects,
registry). embedded_ring.clj stays the documented exception
for dev.zeko.stube.embed as the host-embedder surface.docs/internals.md, with a sketched
Publisher protocol for the eventual seam if a real bus is ever
wanted, plus a working-today recipe using :app as a bring-your-own
bus. New docs/decisions/ folder with four short ADRs covering
resume-key naming, EDN-clean conversation state, the embed/call
split, and the :app + :principal-fn contract.stube-keepalive
event (an SSE event-type Datastar ignores) every
:sse-keepalive-ms milliseconds, default 15000. The heartbeat
stops when the SSE channel unregisters or the kernel halts. Pass
nil or 0 to disable. docs/internals.md carries an "SSE behind
a reverse proxy" section with nginx, ALB, Caddy, and HAProxy knobs.defflow durability is now documented as a deliberate property,
not a pending gap. A conversation containing a defflow is
in-memory only by design (its cloroutine continuation is not
EDN-serialisable). For long-running flows that must survive a
restart, write the same shape as a hand-rolled task component with
:start + named resume keys; the tutorial now shows the two side
by side. The store's skip-on-defflow warning is more pointed about
the workaround.:app option on make-kernel carries an opaque host value
(typically a map of long-lived dependencies) that component code
reads via (s/app). The value is not serialised with the
conversation; rebuild it from live JVM state on each
make-kernel call.:principal-fn option is invoked once when a conversation is
minted; its result is persisted on the conversation under
:conv/principal and surfaced through (s/principal). The
principal is fixed for the life of the conversation — end and
re-mint to change identity. There is no set-principal!
operation by design.s/start! for the standalone
server. The protected_counter example now reads its principal
via (s/principal) instead of carrying app-level login state on
the conversation.dev.zeko.stube.embed as the documented host-embedder
namespace. dev.zeko.stube.kernel is back to being just the pure
effect fold (step, run-effects, dispatch, boot,
resume-top, redraw-top). Internal callers, tests, examples, and
docs have been updated. The §15.4 line-count invariant has been
replaced by a structural one: the runtime stays organised around a
single effect multimethod.s/back is now a zero-arity function (s/back) returning the
[:back] effect, matching every other effect constructor. Call
sites that used the bare value need a pair of parens.dev.zeko.stube.kernel/replay is now kernel/replay-with so the
kernel-aware host helper no longer collides in name with the
kernel-less core/replay used by component-author tests.registry/register! now lifts every colocated author key —
:start, :stop, :wakeup, :children in addition to the
previous :init/:render/:handle/:keep/:doc/:state — to
its :component/<name> home. Component definitions registered via
any entry point (defcomponent, register-component!,
decorate!) are now uniform; the kernel reads them under a single
namespace.s/on-target
against the parent iid instead of synthesising a fake parent
instance map.204 no-ops instead of ending the
whole conversation; missing/ended conversations still surface the
stale-page response.s/publish! routes
to the active embedded kernel when called from component code.:io a runtime-interpreted effect. Pure dispatch/replay
keep it inert unless a runtime hook is bound, preserving the kernel's
value-oriented testability.s/on-target, with
s/event-url documented as the low-level escape hatch.s/call, s/become, and s/call-in-slot accept existing embed
specs directly, so stock helpers like (s/confirm "?") compose
without reaching into internal effect constructors.stube/head-tags now returns the stock CSS,
preserve bridge, Datastar, and optional halos assets for the current
kernel/base path; examples no longer reach into shell internals.Can you improve this documentation?Edit on GitHub
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 |