The live engine: component registry, live contexts, and the update cycle.
Shaped per 1 — engine returns an immutable map of pieces and
starts nothing; start!/stop! handle lifecycle. That is what makes a
framework adapter mechanically derivable rather than bespoke.
Deliberately transport-agnostic (4). The engine never imports a
server or the datastar SDK. A connection is represented by a send! function
of one argument, which is also the shape of the datastar SDK's own seam, so
swapping Jetty for http-kit or Aleph should not reach into this namespace.
What lives here:
What does not:
send!)render-fn)The live engine: component registry, live contexts, and the update cycle. Shaped per 1 — `engine` returns an immutable map of pieces and starts nothing; `start!`/`stop!` handle lifecycle. That is what makes a framework adapter mechanically derivable rather than bespoke. Deliberately transport-agnostic (4). The engine never imports a server or the datastar SDK. A connection is represented by a `send!` function of one argument, which is also the shape of the datastar SDK's own seam, so swapping Jetty for http-kit or Aleph should not reach into this namespace. What lives here: - the component registry (name -> component contract) - live contexts (one per connected component instance) - the update cycle: run a handler, diff, resolve patches, render, send What does not: - HTTP, SSE framing, or any server (the caller supplies `send!`) - rendering to a string (the caller supplies a `render-fn`)
(connect! {:keys [registry components]}
component-name
{:keys [send! close! params]})Registers a new live context for component-name and returns its id.
send! is a one-argument function receiving patch instructions to deliver to
this browser. close! is optional and called by stop!.
Does not render or push anything — the caller decides whether a connection begins with a full render (first mount) or a rebuild (reconnect). Those are different paths , and conflating them here would hide that.
Stores the component name, not the component. See component-of.
Registers a new live context for `component-name` and returns its id. `send!` is a one-argument function receiving patch instructions to deliver to this browser. `close!` is optional and called by `stop!`. Does not render or push anything — the caller decides whether a connection begins with a full render (first mount) or a rebuild (reconnect). Those are different paths , and conflating them here would hide that. Stores the component *name*, not the component. See `component-of`.
(dispatch! eng id event args opts)Runs an :on handler for a live context, then pushes the resulting patches.
Returns the instructions sent, which is what makes this testable without a
server: pass a send! that records.
Runs an `:on` handler for a live context, then pushes the resulting patches. Returns the instructions sent, which is what makes this testable without a server: pass a `send!` that records.
(engine {:keys [components render-fn registry]})Creates an engine. Starts nothing.
Options:
:components map of component name -> component contract:render-fn (fn [tree] -> string). Supplied by the caller so the engine
depends on no rendering library, and so the output need not be
HTML.:registry optional atom for live contexts; one is created if absent.
Pass your own when it must survive reload.Returns a map of pieces. Nothing in it is running.
Creates an engine. Starts nothing.
Options:
- `:components` map of component name -> component contract
- `:render-fn` (fn [tree] -> string). Supplied by the caller so the engine
depends on no rendering library, and so the output need not be
HTML.
- `:registry` optional atom for live contexts; one is created if absent.
Pass your own when it must survive reload.
Returns a map of pieces. Nothing in it is running.(live-context {:keys [registry] :as eng} id)The live context for id, with its component resolved by name.
:component is attached fresh on every lookup so callers always see current
behaviour.
The live context for `id`, with its component resolved by name. `:component` is attached fresh on every lookup so callers always see current behaviour.
(mount! eng id ctx-extra)First connection: runs :mount and returns the full rendered HTML.
First connection: runs `:mount` and returns the full rendered HTML.
(reconnect! eng id ctx-extra client-snapshot)Rebuilds a live context after a disconnect and returns the full HTML.
client-snapshot is the :state map from an already-verified recovery
snapshot (snapshot/recoverable-state). Verification is the caller's job,
deliberately: this namespace stays free of the signing secret and of any
opinion about how the snapshot reached it, so the engine can be driven by a
transport that carries it differently.
Even with a verified snapshot, tiers/restore applies the tier filter, so only
declared-:recoverable fields are replayed. A client cannot
reinstate a :sourced field this way.
Sourced state comes from :mount — a fresh query, so a reconnect picks up
anything that changed while the connection was down. That is the point of :
the process held a rebuildable projection, not the truth.
Rebuilds a live context after a disconnect and returns the full HTML. `client-snapshot` is the `:state` map from an already-verified recovery snapshot (`snapshot/recoverable-state`). **Verification is the caller's job**, deliberately: this namespace stays free of the signing secret and of any opinion about how the snapshot reached it, so the engine can be driven by a transport that carries it differently. Even with a verified snapshot, `tiers/restore` applies the tier filter, so only declared-`:recoverable` fields are replayed. A client cannot reinstate a `:sourced` field this way. Sourced state comes from `:mount` — a fresh query, so a reconnect picks up anything that changed while the connection was down. That is the point of : the process held a rebuildable projection, not the truth.
(refresh! eng id ctx-extra opts)Re-derives a live context from its Source and pushes the resulting diff.
This is what a PubSub hint triggers. A hint carries no data, so the only
correct reaction is to re-read the source of truth — which is exactly :mount.
Distinct from reconnect!, and the difference is the point: reconnect! returns
full HTML for a caller to deliver over a new connection, whereas refresh!
diffs against the view already on screen and pushes only what changed over an
existing one. Without this there is no path from a hint to a browser at all —
the fan-out benchmark exposed that gap, having measured zero sends.
ctx-extra supplies :source and anything else :mount needs.
Re-derives a live context from its `Source` and **pushes** the resulting diff. This is what a PubSub hint triggers. A hint carries no data, so the only correct reaction is to re-read the source of truth — which is exactly `:mount`. Distinct from `reconnect!`, and the difference is the point: `reconnect!` returns full HTML for a caller to deliver over a *new* connection, whereas `refresh!` diffs against the view already on screen and pushes only what changed over an *existing* one. Without this there is no path from a hint to a browser at all — the fan-out benchmark exposed that gap, having measured zero sends. `ctx-extra` supplies `:source` and anything else `:mount` needs.
(snapshot-data eng id)The data a caller needs to build a recovery snapshot for id.
Returns {:component :params :recoverable}, shaped for snapshot/create.
Returning data rather than a signed string keeps the signing secret out of this
namespace — the engine decides what is recoverable, the caller decides how it
is signed and transported.
No :basis yet: basis tokens require the Source protocol, which is
unwritten. snapshot/create treats it as optional, so adding it later does not
change this shape. Omitting it rather than emitting a permanent nil keeps the
gap visible.
The data a caller needs to build a recovery snapshot for `id`.
Returns `{:component :params :recoverable}`, shaped for `snapshot/create`.
Returning data rather than a signed string keeps the signing secret out of this
namespace — the engine decides *what* is recoverable, the caller decides how it
is signed and transported.
No `:basis` yet: basis tokens require the `Source` protocol, which is
unwritten. `snapshot/create` treats it as optional, so adding it later does not
change this shape. Omitting it rather than emitting a permanent `nil` keeps the
gap visible.(start! engine)Marks the engine started. Present for lifecycle symmetry and so an adapter
has an obvious init-key target; there is nothing to spin up yet because
connections are created by the transport, not by the engine.
Marks the engine started. Present for lifecycle symmetry and so an adapter has an obvious `init-key` target; there is nothing to spin up yet because connections are created by the transport, not by the engine.
(stop! {:keys [registry started?] :as engine})Closes every live context and clears the registry.
Closes every live context and clears the registry.
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 |