The conversation data model — pure helpers, no I/O.
A conversation is the entire server-side state of a single user's session against one mounted flow. It is a plain map; persistence, history, and concurrency are all handled by working with these values.
{:conv/id "cv-019"
:conv/instances {"ix-7e2" {…instance map…} …}
:conv/stack ["ix-7c1" "ix-7e2"] ; bottom → top
:conv/history [previous-conv …]
:conv/created #inst "…"
:conv/touched #inst "…"}
An instance is the merged shape of an instantiated component:
{:instance/id "ix-7e2"
:instance/type :auth/login
:instance/parent "ix-7c1" | nil
:instance/resume :on-login | nil
:instance/rendered? false ; toggled on first emitted patch
…user state from (:component/init cdef)…}
The user-defined state lives at the top level of the instance map (not
under a :state key). Handler functions therefore see one merged map
and can both read instance metadata (:instance/id) and their own
domain fields by simple keyword lookup. Handlers must not clobber the
:instance/* keys.
The conversation data model — pure helpers, no I/O.
A *conversation* is the entire server-side state of a single user's
session against one mounted flow. It is a plain map; persistence,
history, and concurrency are all handled by working with these values.
----------------------------------------------------------------------
Shape
----------------------------------------------------------------------
{:conv/id "cv-019"
:conv/instances {"ix-7e2" {…instance map…} …}
:conv/stack ["ix-7c1" "ix-7e2"] ; bottom → top
:conv/history [previous-conv …]
:conv/created #inst "…"
:conv/touched #inst "…"}
An *instance* is the merged shape of an instantiated component:
{:instance/id "ix-7e2"
:instance/type :auth/login
:instance/parent "ix-7c1" | nil
:instance/resume :on-login | nil
:instance/rendered? false ; toggled on first emitted patch
…user state from (:component/init cdef)…}
The user-defined state lives at the top level of the instance map (not
under a `:state` key). Handler functions therefore see one merged map
and can both read instance metadata (`:instance/id`) and their own
domain fields by simple keyword lookup. Handlers must not clobber the
`:instance/*` keys.(child-slot parent child-iid)Return the slot key in parent currently pointing at child-iid, or nil.
Return the slot key in `parent` currently pointing at `child-iid`, or nil.
(descendant-ids conv iid)Return a vector of all instance ids transitively reachable from iid
via :instance/children or :instance/keyed-slots, including iid
itself, in pre-order.
Does not follow :instance/previous chains (the displaced slot
occupants stashed by [:call-in-slot …] while their replacement is
pending). Use this on the slot-answer path, where the previous gets
restored, and for render-side walks like mark-rendered where
previous-chained instances are not visible in the DOM. When the
whole subtree is being destroyed (frame pop, :replace, :end,
keyed removal), use subtree-ids instead.
Return a vector of all instance ids transitively reachable from `iid` via `:instance/children` or `:instance/keyed-slots`, including `iid` itself, in pre-order. Does **not** follow `:instance/previous` chains (the displaced slot occupants stashed by `[:call-in-slot …]` while their replacement is pending). Use this on the slot-answer path, where the previous gets restored, and for render-side walks like [[mark-rendered]] where previous-chained instances are not visible in the DOM. When the whole subtree is being destroyed (frame pop, `:replace`, `:end`, keyed removal), use [[subtree-ids]] instead.
(embed type)(embed type args)Return an embed spec for component type initialised with args.
Return an embed spec for component `type` initialised with `args`.
(embed? x)True if x looks like an embed spec.
True if `x` looks like an embed spec.
(instance conv iid)The instance map for iid, or nil.
The instance map for `iid`, or nil.
Keys the kernel manages on every instance map. Handlers must treat these as read-only; the kernel ignores any user changes to them.
:instance/children is the slot→iid map populated when a component
declares :children (lifted to :component/children) in its
definition. See instantiate-tree.
:instance/slot and :instance/previous are used by the
[:call-in-slot …] overlay primitive: the temporary child remembers
which parent slot it occupies and which child iid should be restored
when it answers.
:instance/keyed-slots is populated by the keyed-children primitive
and is just as framework-owned as fixed :instance/children slots.
:stube/context is adapter-supplied request/application context. It
is protected like instance metadata so handlers can read it via
s/context without accidentally persisting edits to the context map.
Keys the kernel manages on every instance map. Handlers must treat these as read-only; the kernel ignores any user changes to them. `:instance/children` is the slot→iid map populated when a component declares `:children` (lifted to `:component/children`) in its definition. See [[instantiate-tree]]. `:instance/slot` and `:instance/previous` are used by the `[:call-in-slot …]` overlay primitive: the temporary child remembers which parent slot it occupies and which child iid should be restored when it answers. `:instance/keyed-slots` is populated by the keyed-children primitive and is just as framework-owned as fixed `:instance/children` slots. `:stube/context` is adapter-supplied request/application context. It is protected like instance metadata so handlers can read it via `s/context` without accidentally persisting edits to the context map.
(instantiate cdef {:keys [embed/args]} parent-id resume-key)Build a fresh instance map from a component definition and an embed
spec. parent-id and resume-key may be nil for root instances.
This is the flat constructor: it does not look at :component/children.
Use instantiate-tree when you want the kernel to materialise the
whole subtree.
Build a fresh instance map from a component definition and an embed spec. `parent-id` and `resume-key` may be nil for root instances. This is the *flat* constructor: it does not look at `:component/children`. Use [[instantiate-tree]] when you want the kernel to materialise the whole subtree.
(instantiate-tree cdef embed-spec parent-id resume-key lookup-cdef)Build a parent instance plus every child eagerly declared by its
component definition's :component/children map (lifted from
:children at registration time by registry/register!).
:component/children is either a map {slot-key embed-spec} or a function of
the freshly-initialised parent state returning such a map. Slot keys
are arbitrary keywords the parent can reference from its :render
via (s/render-slot self slot-key).
lookup-cdef is a 1-arg function (component-id) → cdef-map. Pass
dev.zeko.stube.registry/lookup! from the kernel; the indirection lets this
namespace stay registry-agnostic and easy to test in isolation.
Returns [parent-inst descendants] where descendants is a
flat seq of every transitively-instantiated child instance, ready to
be merged into :conv/instances. The returned parent-inst carries
:instance/children populated with {slot-key child-iid}.
Build a parent instance plus every child eagerly declared by its
component definition's `:component/children` map (lifted from
`:children` at registration time by `registry/register!`).
`:component/children` is either a map `{slot-key embed-spec}` or a function of
the freshly-initialised parent state returning such a map. Slot keys
are arbitrary keywords the parent can reference from its `:render`
via `(s/render-slot self slot-key)`.
`lookup-cdef` is a 1-arg function `(component-id) → cdef-map`. Pass
`dev.zeko.stube.registry/lookup!` from the kernel; the indirection lets this
namespace stay registry-agnostic and easy to test in isolation.
Returns `[parent-inst descendants]` where `descendants` is a
flat seq of every transitively-instantiated child instance, ready to
be merged into `:conv/instances`. The returned `parent-inst` carries
`:instance/children` populated with `{slot-key child-iid}`.(local-signal self signal)Return the per-instance signal key for logical signal on self.
Datastar signals are page-global. Binding two embedded components to
the same $answer would therefore make them share client-side state.
A local signal keeps the user-facing logical name (:answer) while
suffixing the actual wire key with the instance id:
(local-signal {:instance/id "ix-1"} :answer)
;; => :answer-ix-1
merge-kept-signals maps local signal keys back to their logical
names when the component lists the logical key in :component/keep.
Return the per-instance signal key for logical `signal` on `self`.
Datastar signals are page-global. Binding two embedded components to
the same `$answer` would therefore make them share client-side state.
A local signal keeps the user-facing logical name (`:answer`) while
suffixing the actual wire key with the instance id:
(local-signal {:instance/id "ix-1"} :answer)
;; => :answer-ix-1
[[merge-kept-signals]] maps local signal keys back to their logical
names when the component lists the logical key in `:component/keep`.(mark-rendered conv iid)Set :instance/rendered? true on iid and every descendant the
parent's render placed into the DOM. Called once the kernel has
emitted a frame's first patch onto the wire.
Marking the whole subtree at once matches reality: Datastar morphs
the parent's HTML into the page in one shot, so children that the
parent inlined via s/render-slot are now in the DOM and any
future render of that child can use the (cheaper) morph-by-id
default instead of re-emitting the shell.
Set `:instance/rendered? true` on `iid` and *every* descendant the parent's render placed into the DOM. Called once the kernel has emitted a frame's first patch onto the wire. Marking the whole subtree at once matches reality: Datastar morphs the parent's HTML into the page in one shot, so children that the parent inlined via `s/render-slot` are now in the DOM and any *future* render of that child can use the (cheaper) morph-by-id default instead of re-emitting the shell.
(merge-kept-signals inst signals keep-keys)Lift the entries of signals whose keys appear in keep-keys onto the
instance map. This is the per-event two-way binding step: the user
types in the browser, Datastar updates the signal client-side, and on
every event the relevant signals land back on self before the handler
sees it.
If the browser sends a per-instance key produced by local-signal,
that value is lifted onto the logical kept key. Local values win over
same-named global values so a component can safely say :keep #{:answer}
and render (s/local-bind self :answer).
Lift the entries of `signals` whose keys appear in `keep-keys` onto the
instance map. This is the per-event two-way binding step: the user
types in the browser, Datastar updates the signal client-side, and on
every event the relevant signals land back on `self` before the handler
sees it.
If the browser sends a per-instance key produced by [[local-signal]],
that value is lifted onto the logical kept key. Local values win over
same-named global values so a component can safely say `:keep #{:answer}`
and render `(s/local-bind self :answer)`.(merged-self conv iid signals)Look up iid in conv, find its component definition, and return the
instance with kept signals merged in. This is the value passed to
:render and :handle.
Look up `iid` in `conv`, find its component definition, and return the instance with kept signals merged in. This is the value passed to `:render` and `:handle`.
(new-conversation)Build an empty conversation.
Build an empty conversation.
(new-instance-id)Mint a fresh instance id.
Mint a fresh instance id.
(pop-top conv)Pop the top frame and remove its entire subtree from the
conversation — embedded descendants and every previous-chained slot
occupant. Returns [conv' popped-id].
The wider sweep matters: a frame can have called-in-slot one or
more times before being popped, leaving a previous-chain dangling
off each new slot child. The narrow descendant-ids walk would
miss those instances and leave them orphaned in :conv/instances.
Pop the top frame and remove its entire subtree from the conversation — embedded descendants and every previous-chained slot occupant. Returns `[conv' popped-id]`. The wider sweep matters: a frame can have called-in-slot one or more times before being popped, leaving a previous-chain dangling off each new slot child. The narrow [[descendant-ids]] walk would miss those instances and leave them orphaned in `:conv/instances`.
(preserve-meta old-instance new-state)Merge new-state over old-instance while protecting the
:instance/* keys. Used after a handler returns self' so a buggy
handler can't break the instance metadata.
Merge `new-state` over `old-instance` while protecting the `:instance/*` keys. Used after a handler returns `self'` so a buggy handler can't break the instance metadata.
(push-instance conv inst)Add inst to :conv/instances and push its id onto the stack.
Add `inst` to `:conv/instances` and push its id onto the stack.
(put-instance conv inst)Replace inst in the instances map without touching the stack.
Replace `inst` in the instances map without touching the stack.
(put-many conv instances)Add a flat seq of instances to :conv/instances without touching
the stack. Used by the kernel after instantiate-tree to deposit
the eagerly-built children alongside their parent.
Add a flat seq of instances to `:conv/instances` without touching the stack. Used by the kernel after [[instantiate-tree]] to deposit the eagerly-built children alongside their parent.
(remove-subtree conv iid)Remove iid and every embedded descendant from :conv/instances
without touching the call stack. Mirrors descendant-ids — does
not follow :instance/previous chains.
Right for the slot-answer path, where the previous occupant gets
restored as the new slot value. Wrong for whole-subtree teardown;
use remove-subtree+previous there.
Remove `iid` and every embedded descendant from `:conv/instances` without touching the call stack. Mirrors [[descendant-ids]] — does *not* follow `:instance/previous` chains. Right for the slot-answer path, where the previous occupant gets restored as the new slot value. Wrong for whole-subtree teardown; use [[remove-subtree+previous]] there.
(remove-subtree+previous conv iid)Like remove-subtree but follows :instance/previous chains.
Use this when the whole subtree is being destroyed (keyed-child
removal, frame replace/end).
Like [[remove-subtree]] but follows `:instance/previous` chains. Use this when the whole subtree is being destroyed (keyed-child removal, frame replace/end).
(replay-event conv event)Normalise a replay event against conv: if event is a function,
invoke it on the conversation; then default :instance-id to the
top frame and :signals to {}. Used by core/replay and
runtime/replay-with (and any future playback tool) so the
event-shape rule lives in one place.
Normalise a replay event against `conv`: if `event` is a function,
invoke it on the conversation; then default `:instance-id` to the
top frame and `:signals` to `{}`. Used by `core/replay` and
`runtime/replay-with` (and any future playback tool) so the
event-shape rule lives in one place.(set-child-slot conv parent-id slot child-iid)Point parent-id's slot at child-iid. If child-iid is nil,
remove the slot.
Point `parent-id`'s `slot` at `child-iid`. If `child-iid` is nil, remove the slot.
(snapshot conv)Append the current value of conv to its own :conv/history so the
back button can rewind to it. Persistent maps make this essentially
free in space. We strip the previous :conv/history from the snapshot
so history doesn't grow quadratically.
Append the current value of `conv` to its own `:conv/history` so the back button can rewind to it. Persistent maps make this essentially free in space. We strip the previous `:conv/history` from the snapshot so history doesn't grow quadratically.
(snapshot-for-dispatch conv event-summary back?)Apply the per-dispatch snapshot + touch + :conv/last-event
update, with the [:back] carve-out: handlers that walk history
backwards must NOT have their own pre-state pushed onto that
history first. If it were, :back would just pop the snapshot we
just took and "restore" the same state, leaving the user stuck.
Every other dispatch behaves as before.
Apply the per-dispatch `snapshot` + `touch` + `:conv/last-event` update, with the `[:back]` carve-out: handlers that walk history backwards must NOT have their own pre-state pushed onto that history first. If it were, `:back` would just pop the snapshot we just took and "restore" the same state, leaving the user stuck. Every other dispatch behaves as before.
(subtree-ids conv iid)Like descendant-ids but also follows each instance's
:instance/previous chain.
[:call-in-slot …] stashes the displaced slot occupant on the new
child's :instance/previous so the slot can revert on :answer.
When the parent frame goes away before that answer arrives, those
previous-chained instances have no live referent and would otherwise
leak in :conv/instances. Frame-destruction paths use this wider
walk so the chain is swept and each ancestor's :stop hook fires
exactly once.
Visited iids are tracked in a set, but a previous-chain can only ever be a strict tree (each entry is set once at the moment of call-in-slot to an existing, distinct iid).
Like [[descendant-ids]] but also follows each instance's `:instance/previous` chain. `[:call-in-slot …]` stashes the displaced slot occupant on the new child's `:instance/previous` so the slot can revert on `:answer`. When the *parent* frame goes away before that answer arrives, those previous-chained instances have no live referent and would otherwise leak in `:conv/instances`. Frame-destruction paths use this wider walk so the chain is swept and each ancestor's `:stop` hook fires exactly once. Visited iids are tracked in a set, but a previous-chain can only ever be a strict tree (each entry is set once at the moment of call-in-slot to an existing, distinct iid).
(top-id conv)Id of the topmost frame on the stack, or nil if the stack is empty.
Id of the topmost frame on the stack, or nil if the stack is empty.
(top-instance conv)The instance map at the top of the stack, or nil if empty.
The instance map at the top of the stack, or nil if empty.
(touch conv)Bump the :conv/touched timestamp.
Bump the `:conv/touched` timestamp.
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 |