Tab sessions: one island tree per browser tab, outliving any single SSE connection.
A session is created when the page is served, and the page is rendered from
its first frame. The tab's stream (ring/stream-init) then attaches to it,
and the session belongs to that page load from then on. Datastar drops that
stream routinely: a hidden tab closes it, network blips retry it.
A detached session keeps its islands, and so their resources, for grace-ms.
A reconnect within that window resyncs from the cached frame without
reopening anything. After the window, every island is unmounted and
everything they held is released.
Each session is run by one pump: a virtual thread that owns all of the
session's mutable state, its island runtime included, and consumes the
session's inbox (co.multiply.tropical.signal.Inbox). The inbox holds
lifecycle events (attach, detach, close) and the subscriptions marked since
the pump last looked. A change to something the islands read only marks its subscription,
once until the pump takes it. Rendering happens on the pump, never on the
thread that made the change, so a shared resource's loop never renders on
anyone's behalf. Changes that arrive while a frame is being sent coalesce into
the next one, so a slow client gets fewer, later frames, never a backlog, and
the inbox never holds more than one mark per subscription.
A session runs an app, a map:
:root (fn [{:keys [tab uid request]}] call): the session's root
island, given the session's context, which every island
reads with use-session. :request is the request that
created the session, where co.multiply.tropical.ring
created it.:patch-signals optional, (fn [info] signals): a map of signals to send
with each patch, or nil. See patch-signals.:error-view optional, (fn [id e] hiccup): what renders in place of
an island whose render throws (see runtime/runtime).:first-frame-ms optional: how long, in ms, the session's first frame may
wait for reads still pending (below). Defaults to 0.The first frame a session delivers, to the page or over the stream that
rebuilt the session, is the first a client sees from it. Its reads are often
cold: a new tab, a first visit, every open tab after a restart. Sent at once,
it shows their pending states, which the data replaces a moment later. So it
waits until no read of its islands is pending, or :first-frame-ms passes,
and goes out as it is then. It renders as reads land meanwhile, so islands
that appear only once an earlier read lands get to read too. Later frames
don't wait. A page waits at most 5 s for its frame (render-page!), so keep
the wait well under that: 100 to 200 ms covers the reads worth waiting for.
Scripts the islands declare (co.multiply.tropical.script) go over the
stream once each, appended to the page's head, ahead of the first frame that
needs them: those of the first frame too, unless the page put them in its
head (scripts-in-page!), in case it left them out.
A session belongs to one page load, which keeps the modules it loaded, so a
reconnect doesn't send them again.
A session whose pump fails, because a step outside the islands' renders threw, is closed, and so is its stream: the client reconnects into a new session instead of waiting on one that sends nothing.
Tab sessions: one island tree per browser tab, outliving any single SSE
connection.
A session is created when the page is served, and the page is rendered from
its first frame. The tab's stream (`ring/stream-init`) then attaches to it,
and the session belongs to that page load from then on. Datastar drops that
stream routinely: a hidden tab closes it, network blips retry it.
A detached session keeps its islands, and so their resources, for `grace-ms`.
A reconnect within that window resyncs from the cached frame without
reopening anything. After the window, every island is unmounted and
everything they held is released.
Each session is run by one pump: a virtual thread that owns all of the
session's mutable state, its island runtime included, and consumes the
session's inbox (`co.multiply.tropical.signal.Inbox`). The inbox holds
lifecycle events (attach, detach, close) and the subscriptions marked since
the pump last looked. A change to something the islands read only marks its subscription,
once until the pump takes it. Rendering happens on the pump, never on the
thread that made the change, so a shared resource's loop never renders on
anyone's behalf. Changes that arrive while a frame is being sent coalesce into
the next one, so a slow client gets fewer, later frames, never a backlog, and
the inbox never holds more than one mark per subscription.
A session runs an app, a map:
- `:root` `(fn [{:keys [tab uid request]}] call)`: the session's root
island, given the session's context, which every island
reads with `use-session`. `:request` is the request that
created the session, where `co.multiply.tropical.ring`
created it.
- `:patch-signals` optional, `(fn [info] signals)`: a map of signals to send
with each patch, or nil. See `patch-signals`.
- `:error-view` optional, `(fn [id e] hiccup)`: what renders in place of
an island whose render throws (see `runtime/runtime`).
- `:first-frame-ms` optional: how long, in ms, the session's first frame may
wait for reads still pending (below). Defaults to 0.
The first frame a session delivers, to the page or over the stream that
rebuilt the session, is the first a client sees from it. Its reads are often
cold: a new tab, a first visit, every open tab after a restart. Sent at once,
it shows their pending states, which the data replaces a moment later. So it
waits until no read of its islands is pending, or `:first-frame-ms` passes,
and goes out as it is then. It renders as reads land meanwhile, so islands
that appear only once an earlier read lands get to read too. Later frames
don't wait. A page waits at most 5 s for its frame (`render-page!`), so keep
the wait well under that: 100 to 200 ms covers the reads worth waiting for.
Scripts the islands declare (`co.multiply.tropical.script`) go over the
stream once each, appended to the page's head, ahead of the first frame that
needs them: those of the first frame too, unless the page put them in its
head (`scripts-in-page!`), in case it left them out.
A session belongs to one page load, which keeps the modules it loaded, so a
reconnect doesn't send them again.
A session whose pump fails, because a step outside the islands' renders
threw, is closed, and so is its stream: the client reconnects into a new
session instead of waiting on one that sends nothing.The open sessions, for display: an atom of a map keyed by tab,
which changes only on a session's lifecycle events. Watch it through
summary.
The open sessions, for display: an atom of a map keyed by tab, which changes only on a session's lifecycle events. Watch it through `summary`.
(close-all!)Closes every session, as after reloading code in development. Each tab's stream ends, and Datastar reconnects about a second later into a fresh session, rendered by the code loaded then, whose first frame updates the page in place.
Call it once the reloaded code serves requests: a tab that reconnects before that gets a session of the old code. What only a page load renders, such as the page's head or an edited script, shows once the page is reloaded.
Closes every session, as after reloading code in development. Each tab's stream ends, and Datastar reconnects about a second later into a fresh session, rendered by the code loaded then, whose first frame updates the page in place. Call it once the reloaded code serves requests: a tab that reconnects before that gets a session of the old code. What only a page load renders, such as the page's head or an edited script, shows once the page is reloaded.
The least time, in ms, between a session's frames. Changes within it coalesce into the next frame, and the latest wins.
The least time, in ms, between a session's frames. Changes within it coalesce into the next frame, and the latest wins.
How long, in ms, a session whose stream went away keeps its islands, and what they hold, for the tab to reconnect, before it closes.
How long, in ms, a session whose stream went away keeps its islands, and what they hold, for the tab to reconnect, before it closes.
How long, in ms, a quiet stream goes before it writes a heartbeat, an SSE comment: a dead connection is noticed only on write, and a proxy's idle timeout would close a live one.
How long, in ms, a quiet stream goes before it writes a heartbeat, an SSE comment: a dead connection is noticed only on write, and a proxy's idle timeout would close a live one.
(summary)(summary sessions)How many sessions have their stream, and how many are waiting for it, as
{:attached :detached}. Of every session, or of sessions, a value
!sessions held.
How many sessions have their stream, and how many are waiting for it, as
`{:attached :detached}`. Of every session, or of `sessions`, a value
`!sessions` held.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 |