Liking cljdoc? Tell your friends :D

co.multiply.tropical.ring

A Ring handler for a page: the page itself, the stream it opens and the actions its islands render, all at the page's URL.

All three are requests to the page's route, so they pass through the same middleware: what it binds from the request, such as the account from the login or a workspace from the path, renders and actions see alike, and a tab the server no longer knows is rebuilt from the route it was on. The handler tells them apart by method, and by the header the page's expressions send:

  • GET the page: a new tab's session, server-side rendered from its first frame.
  • GET with Tropical-Tab the tab's stream, which the page opens with mount, or stream-init.
  • POST with Tropical-Action an action use-action rendered, its signals, and a value if it takes one, a JSON body; or a script's request to a token of its island (action/use-token), as an upload's.

request-kind tells them apart, for middleware that runs before routing and must let them through, such as one that refuses or parses request bodies.

A stream passes the route's middleware each time it connects, and a refusal, because the login expired or what the page shows is gone, would leave the tab stale: Datastar retries it, then gives up. wrap-refused-streams answers such a refusal with a reload of the page, so the page request gets the app's own answer, and the handler answers its own refusals of a stream that way. An open stream isn't asked again: what a page shows going away while it is open is for its islands to render.

The app the sessions run (co.multiply.tropical.session) takes more keys here, and its :root also gets the :request that created the session:

  • :uid (fn [request] uid): the user the request is made on behalf of, or nil. Authentication is the app's; the session and its actions belong to that user. A page or action without one gets a 403, a stream a reload.
  • :page (fn [{:keys [tab frame scripts request]}] html): the page, server-side rendered from the session's first frame. It mounts the app with mount, which holds the frame, opens the stream and keeps the state the browser owns, and puts the scripts the frame's islands declared in its head with scripts. A page that opens no stream for its tab, which could never update, throws; one without ui/page-attrs is logged, once.
  • :open-when-hidden optional: true keeps each tab's stream open while its page is hidden, for browser automation in development (see stream-init).
  • :max-signals-bytes optional: the largest action body read, in bytes; larger gets a 413. Defaults to max-signals-bytes.
  • :compression optional: the encodings to compress the stream with, in order of preference, such as [:zstd :br :gzip] (co.multiply.tropical.compression). A stream is compressed with the one its request accepts, or not at all. The page and the actions are left to the server's own compression.

The scripts islands declare (co.multiply.tropical.script) are served by wrap-scripts, outside the routes.

A session is created in the scope of the browser's state its request carries (co.multiply.tropical.cookie), so its islands render that state in the first frame. The page is rendered for its user, whose entries in the browser's storage ui/page-attrs keeps (co.multiply.tropical.storage).

A session renders in the scope of the request that created it, fixed for its life. An action's handler runs in the scope of its own request, and action/request returns it: what may differ from the session's, such as a cookie the page's own response set.

The handler is sync or async. Called sync, it holds the stream request's thread for as long as the stream is open.

What goes over the stream, and how the signals are read, follow Datastar's SDK specification, as co.multiply.tropical.sse describes.

A Ring handler for a page: the page itself, the stream it opens and the
actions its islands render, all at the page's URL.

All three are requests to the page's route, so they pass through the same
middleware: what it binds from the request, such as the account from the
login or a workspace from the path, renders and actions see alike, and a tab
the server no longer knows is rebuilt from the route it was on. The handler
tells them apart by method, and by the header the page's expressions send:

- `GET`                          the page: a new tab's session, server-side
                                 rendered from its first frame.
- `GET`  with `Tropical-Tab`     the tab's stream, which the page opens with
                                 `mount`, or `stream-init`.
- `POST` with `Tropical-Action`  an action `use-action` rendered, its signals,
                                 and a value if it takes one, a JSON body; or
                                 a script's request to a token of its island
                                 (`action/use-token`), as an upload's.

`request-kind` tells them apart, for middleware that runs before routing and
must let them through, such as one that refuses or parses request bodies.

A stream passes the route's middleware each time it connects, and a refusal,
because the login expired or what the page shows is gone, would leave the
tab stale: Datastar retries it, then gives up. `wrap-refused-streams` answers
such a refusal with a reload of the page, so the page request gets the app's
own answer, and the handler answers its own refusals of a stream that way.
An open stream isn't asked again: what a page shows going away while it is
open is for its islands to render.

The app the sessions run (`co.multiply.tropical.session`) takes more keys
here, and its `:root` also gets the `:request` that created the session:

- `:uid`               `(fn [request] uid)`: the user the request is made on
                       behalf of, or nil. Authentication is the app's; the
                       session and its actions belong to that user. A page or
                       action without one gets a 403, a stream a reload.
- `:page`              `(fn [{:keys [tab frame scripts request]}] html)`: the
                       page, server-side rendered from the session's first
                       `frame`. It mounts the app with `mount`, which holds
                       the frame, opens the stream and keeps the state the
                       browser owns, and puts the scripts the frame's islands
                       declared in its head with `scripts`. A page that opens
                       no stream for its tab, which could never update,
                       throws; one without `ui/page-attrs` is logged, once.
- `:open-when-hidden`  optional: true keeps each tab's stream open while its
                       page is hidden, for browser automation in
                       development (see `stream-init`).
- `:max-signals-bytes` optional: the largest action body read, in bytes;
                       larger gets a 413. Defaults to `max-signals-bytes`.
- `:compression`       optional: the encodings to compress the stream with,
                       in order of preference, such as `[:zstd :br :gzip]`
                       (`co.multiply.tropical.compression`). A stream is
                       compressed with the one its request accepts, or not
                       at all. The page and the actions are left to the
                       server's own compression.

The scripts islands declare (`co.multiply.tropical.script`) are served by
`wrap-scripts`, outside the routes.

A session is created in the scope of the browser's state its request carries
(`co.multiply.tropical.cookie`), so its islands render that state in the
first frame. The page is rendered for its user, whose entries in the
browser's storage `ui/page-attrs` keeps (`co.multiply.tropical.storage`).

A session renders in the scope of the request that created it, fixed for its
life. An action's handler runs in the scope of its own request, and
`action/request` returns it: what may differ from the session's, such as a
cookie the page's own response set.

The handler is sync or async. Called sync, it holds the stream request's
thread for as long as the stream is open.

What goes over the stream, and how the signals are read, follow Datastar's
SDK specification, as `co.multiply.tropical.sse` describes.
raw docstring

handlerclj

(handler app)

A Ring handler, sync or async, for a page's route, serving the page, its stream and its actions for app (see the namespace docstring). Mount it for GET and POST. It reads an action's body, its signals and any value, itself, and refuses a body read before it, with a 500: middleware that parses or refuses bodies, on the route or before routing, lets actions past ((= :action (request-kind req))).

Called sync, it holds the stream request's thread for as long as the stream is open: run sync handlers on virtual threads.

Throws for an encoding in :compression it can't load.

A Ring handler, sync or async, for a page's route, serving the page, its
stream and its actions for `app` (see the namespace docstring). Mount it for
GET and POST. It reads an action's body, its signals and any value, itself, and
refuses a body read before it, with a 500: middleware that parses or refuses
bodies, on the route or before routing, lets actions past
(`(= :action (request-kind req))`).

Called sync, it holds the stream request's thread for as long as the stream
is open: run sync handlers on virtual threads.

Throws for an encoding in `:compression` it can't load.
sourceraw docstring

max-signals-bytesclj

The default cap on an action's body, its signals and any value: 1 MiB.

The default cap on an action's body, its signals and any value: 1 MiB.
sourceraw docstring

mountclj

(mount tag)
(mount tag attrs & children)

The element the app mounts in, such as the page's body: tag with attrs, holding the session's first frame, which the islands are patched into from then on. Its attributes open the tab's stream (stream-init, kept open while hidden if the app says :open-when-hidden) and keep the state the browser owns (ui/page-attrs), so the page needs nothing else of tropical's but its scripts (scripts):

[:html
 [:head ... (ring/scripts nonce)]
 (ring/mount :body {:class [:min-h-screen]})]

children go after the first frame, outside the islands, such as a container for dialogs. Without a first frame, because the session closed or took too long, it holds a placeholder with the root island's id, which the stream fills in.

The stream needs the element's data-init, so attrs can't have one, nor the attributes page-attrs adds, and a page mounts once: a second element would open a second stream for the tab. Call it in the app's :page, which handler renders; anywhere else it throws.

The element the app mounts in, such as the page's body: `tag` with `attrs`,
holding the session's first frame, which the islands are patched into from
then on. Its attributes open the tab's stream (`stream-init`, kept open
while hidden if the app says `:open-when-hidden`) and keep the state the
browser owns (`ui/page-attrs`), so the page needs nothing else of tropical's
but its scripts (`scripts`):

    [:html
     [:head ... (ring/scripts nonce)]
     (ring/mount :body {:class [:min-h-screen]})]

`children` go after the first frame, outside the islands, such as a
container for dialogs. Without a first frame, because the session closed or
took too long, it holds a placeholder with the root island's id, which the
stream fills in.

The stream needs the element's `data-init`, so `attrs` can't have one, nor
the attributes `page-attrs` adds, and a page mounts once: a second element
would open a second stream for the tab. Call it in the app's `:page`, which
`handler` renders; anywhere else it throws.
sourceraw docstring

request-kindclj

(request-kind req)

What req is to tropical's handler: :page, :stream, :action, or nil for a request it refuses. The handler dispatches on it, so middleware that must tell tropical's requests apart before routing agrees with the handler.

An action is a POST with a Tropical-Action header and a JSON body, which only the handler reads: a body parser that runs first leaves it nothing to read (see handler). Ring's form and multipart parsers ignore JSON.

What `req` is to tropical's handler: `:page`, `:stream`, `:action`, or nil
for a request it refuses. The handler dispatches on it, so middleware that
must tell tropical's requests apart before routing agrees with the handler.

An action is a POST with a `Tropical-Action` header and a JSON body, which
only the handler reads: a body parser that runs first leaves it nothing to
read (see `handler`). Ring's form and multipart parsers ignore JSON.
sourceraw docstring

scriptsclj

(scripts nonce)

The script tags for the page's head: the scripts its first frame's islands declared (co.multiply.tropical.script), each carrying nonce, the page's CSP nonce, or none without one. Scripts islands declare later go over the stream. Call it in the app's :page, which handler renders; anywhere else it throws.

The script tags for the page's head: the scripts its first frame's islands
declared (`co.multiply.tropical.script`), each carrying `nonce`, the page's
CSP nonce, or none without one. Scripts islands declare later go over the
stream. Call it in the app's `:page`, which `handler` renders; anywhere else
it throws.
sourceraw docstring

stream-initclj

(stream-init tab)
(stream-init tab {:keys [open-when-hidden] :as opts})

The Datastar expression that opens tab's stream, for the page's data-init.

The stream is requested from the page's own URL. It carries the tab, and an id for this page load, made in the browser, so the session can tell its own page from a copy with the same tab id, such as a duplicated tab. retry: 'always' reconnects a stream the server ended too, not just one that failed: a closed session ends its connection so the client retries into a new one.

Datastar closes the stream while the page is hidden, and reopens it when the page is shown, which resyncs it. An action taken meanwhile runs, and its result shows once the page is shown again, within the session's grace (session/grace-ms).

mount renders it into the element the app mounts in, with the app's own :open-when-hidden. For a page of its own making, opts:

  • :open-when-hidden true keeps the stream open while the page is hidden, for development: browser automation can drive a page its browser keeps hidden, and see each change as it happens. Every hidden tab then holds its connection, its session and, compressed, its encoder. A page that is left still lets go of its stream, as it does without the option, and one the browser restores from its back/forward cache opens it again.
The Datastar expression that opens `tab`'s stream, for the page's `data-init`.

The stream is requested from the page's own URL. It carries the tab, and an
id for this page load, made in the browser, so the session can tell its own
page from a copy with the same tab id, such as a duplicated tab.
`retry: 'always'` reconnects a stream the server ended too, not just one that
failed: a closed session ends its connection so the client retries into a
new one.

Datastar closes the stream while the page is hidden, and reopens it when the
page is shown, which resyncs it. An action taken meanwhile runs, and its
result shows once the page is shown again, within the session's grace
(`session/grace-ms`).

`mount` renders it into the element the app mounts in, with the app's own
`:open-when-hidden`. For a page of its own making, `opts`:

- `:open-when-hidden` true keeps the stream open while the page is hidden,
                      for development: browser automation can drive a page
                      its browser keeps hidden, and see each change as it
                      happens. Every hidden tab then holds its connection,
                      its session and, compressed, its encoder. A page that
                      is left still lets go of its stream, as it does
                      without the option, and one the browser restores
                      from its back/forward cache opens it again.
sourceraw docstring

wrap-refused-streamsclj

(wrap-refused-streams handler)

Middleware that answers a refused stream with a reload of its page.

A tab's stream passes the page route's middleware each time it connects, and that middleware may refuse it: the login expired, or what the page shows is gone. Datastar retries a refused stream with backoff, then gives up, and follows a redirect to a page it can't use as a stream; either way the tab is left stale. So a stream request answered with a redirect, or a 4xx other than 408 and 429, gets a stream that reloads the page instead, and the page request gets the app's own answer. A 5xx, as during a deploy, stays as it is, and the client retries it.

Put it outside everything that may refuse a page's requests, before routing if the router's own not-found should count. A page whose stream is refused again within reload-guard-s of reloading is left as it is.

Middleware that answers a refused stream with a reload of its page.

A tab's stream passes the page route's middleware each time it connects, and
that middleware may refuse it: the login expired, or what the page shows is
gone. Datastar retries a refused stream with backoff, then gives up, and
follows a redirect to a page it can't use as a stream; either way the tab is
left stale. So a stream request answered with a redirect, or a 4xx other than
408 and 429, gets a stream that reloads the page instead, and the page
request gets the app's own answer. A 5xx, as during a deploy, stays as it is,
and the client retries it.

Put it outside everything that may refuse a page's requests, before routing
if the router's own not-found should count. A page whose stream is refused
again within `reload-guard-s` of reloading is left as it is.
sourceraw docstring

wrap-scriptsclj

(wrap-scripts handler)

Middleware that serves the scripts islands declare (co.multiply.tropical.script), at their URLs under script/path. Other requests pass to handler.

Put it outside the routes and the session's middleware: a script is the same for everyone, so the browser and shared caches keep it.

Middleware that serves the scripts islands declare
(`co.multiply.tropical.script`), at their URLs under `script/path`. Other
requests pass to `handler`.

Put it outside the routes and the session's middleware: a script is the same
for everyone, so the browser and shared caches keep it.
sourceraw docstring

cljdoc builds & hosts documentation for Clojure/Script libraries

Keyboard shortcuts
Ctrl+kJump to recent docs
←Move to previous article
→Move to next article
Ctrl+/Jump to the search field
× close