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.
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, including iid itself, in pre-order.
Return a vector of all instance ids transitively reachable from `iid` via `:instance/children`, including `iid` itself, in pre-order.
(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 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.
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` 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.
(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 :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 `: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 :children map.
: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 `:children` map.
`: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 instance — and every embedded
descendant — from the conversation. Returns [conv' popped-id].
Pop the top frame and remove its instance — and every embedded descendant — from the conversation. Returns `[conv' popped-id]`.
(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.
Remove `iid` and every embedded descendant from `:conv/instances` without touching the call stack.
(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.
(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 |