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 gets what the page lacks, without reopening
anything: what changed since, if the last event id it read is the mark that
followed what it was sent (sse/mark!), and otherwise the whole cached
frame. 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 [failure] hiccup): what renders in place
of an island whose render throws, unless the island has
a view of its own (see island/error-view).:first-frame-ms optional: how long, in ms, the session's first frame may
wait for reads still pending (below). Defaults to 0.:stream-max-ms optional: how long, in ms, the tab's stream stays open
before the session ends it, and Datastar reconnects it
at once, to this session, which sends what changed
meanwhile, since the page has the rest. Off by default.
For a balancer whose stickiness lapses, such as a
cookie that lasts ten minutes: set under that, each
reconnect passes the balancer while the tab is open,
and renews it if each request extends the cookie's
life, not if its life counts from when it was set.
A stream ends at a random point in the last tenth of
it, so streams opened together don't reconnect
together.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 gets what the page lacks, without reopening
anything: what changed since, if the last event id it read is the mark that
followed what it was sent (`sse/mark!`), and otherwise the whole cached
frame. 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 [failure] hiccup)`: what renders in place
of an island whose render throws, unless the island has
a view of its own (see `island/error-view`).
- `:first-frame-ms` optional: how long, in ms, the session's first frame may
wait for reads still pending (below). Defaults to 0.
- `:stream-max-ms` optional: how long, in ms, the tab's stream stays open
before the session ends it, and Datastar reconnects it
at once, to this session, which sends what changed
meanwhile, since the page has the rest. Off by default.
For a balancer whose stickiness lapses, such as a
cookie that lasts ten minutes: set under that, each
reconnect passes the balancer while the tab is open,
and renews it if each request extends the cookie's
life, not if its life counts from when it was set.
A stream ends at a random point in the last tenth of
it, so streams opened together don't reconnect
together.
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.
(close-user! uid)Closes every session of the user uid in this JVM, as when they sign out or
are removed: their islands unmount, their actions are revoked, and their
tabs' streams end. Datastar reconnects each about a second later, through the
route's middleware. Once the app has ended the user's sign-in, that refuses
the stream, and ring/wrap-refused-streams reloads the page into the app's
own answer, such as its sign-in page. A user still signed in reconnects into
a fresh session.
End the sign-in first: a tab that reconnects before that gets a fresh session. A request that passed the middleware before the sign-in ended, and creates its session after this, keeps it. With several servers, each closes its own sessions, so the app tells every one of them. The Security guide has an island that follows the user's standing instead, which covers both.
Closes every session of the user `uid` in this JVM, as when they sign out or are removed: their islands unmount, their actions are revoked, and their tabs' streams end. Datastar reconnects each about a second later, through the route's middleware. Once the app has ended the user's sign-in, that refuses the stream, and `ring/wrap-refused-streams` reloads the page into the app's own answer, such as its sign-in page. A user still signed in reconnects into a fresh session. End the sign-in first: a tab that reconnects before that gets a fresh session. A request that passed the middleware before the sign-in ended, and creates its session after this, keeps it. With several servers, each closes its own sessions, so the app tells every one of them. The Security guide has an island that follows the user's standing instead, which covers both.
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 |