Islands as functions (defisland), rendered per browser tab on the server and patched into the page by Datastar:
hooks keyed by name (use-watch, use-state, use-hold, and use-session, with the request that created the
session), ids scoped by parent, and frames that
send only the topmost islands that changed. A list that gains or loses children sends only those, inserted next to
a kept sibling or removed by id, and a child whose key changed, starting over, goes in the old one's place without
its parent.
Tab sessions that outlive their SSE connection, each run by one pump, with a grace period, coalescing backpressure
and heartbeats. A session whose pump fails closes its stream, so its client reconnects. A session's first frame can
wait briefly for reads still pending (:first-frame-ms), so a cold page arrives with what lands in time.
One Ring handler per page, serving the page, its stream and its actions at the page's URL, so all three pass the
route's middleware, and a tab the server no longer knows is rebuilt from its route. request-kind tells its requests
apart for middleware that runs before routing, and an action's body is capped. The page mounts the app with
ring/mount, an element of its choosing that holds the first frame, or a placeholder the stream fills, and opens
the stream, and its head takes the islands' scripts with ring/scripts. A page that opens no stream throws, and
one without ui/page-attrs is logged.
The stream compressed, opted into with :compression, such as [:zstd :br :gzip]: with the first encoding the
request accepts, one encoder for the stream's life, and each frame flushed through it at once
(co.multiply.tropical.compression). gzip is the JDK's. zstd and brotli are tropical's: libzstd and libbrotlienc,
built from their release source for macOS and Linux and shipped in the jar, called through the JDK's foreign function
API (co.multiply.tropical.sse.ZstdEncoder, BrotliEncoder), or the libraries the system properties
co.multiply.tropical.zstd.library and co.multiply.tropical.brotli.library name.
A frame's events go out in one flush (sse/together).
:open-when-hidden in the app, keeping the stream open while the page is hidden, for browser automation in
development. A page left behind still lets go of its stream, and one restored from the back/forward cache opens it
again.
session/close-all! as the last step of a reload in development: every open tab reconnects into a session of the
new code, and is updated in place.
A refused stream reloads its page, so the page request gets the app's own answer: wrap-refused-streams for refusals
by the app's middleware, and the handler's own. A page whose stream is refused again is left as it is.
Shared, reference-counted resources with a linger, read through observe: keyed by call site and arguments, and
opened by a fn of emit!, as Missionary's observe is. The same read made again in one render, as by two hooks of
an app's own over one call site, returns the same value from the one resource (use-hold's :idempotent).
State the server shares across sessions (co.multiply.tropical.shared): declared with defshared, read by key with
use-shared, created by its first reader and held while read, lingering after so a page load keeps it, and changed
from anywhere with shared/swap!. The key decides how shared it is: a user's, a document's, everyone's.
State the browser owns, in two stores, each a namespace, kept in Datastar signals and never undone by a patch or a
remount, with ui/page-attrs on the element the app mounts in keeping both:
co.multiply.tropical.cookie), such as whether a section is open: declared with defcookie, taken
up by any island with use-cookie, and right in the first frame. An action sets one with cookie/patch.co.multiply.tropical.storage), such as a draft the user is writing: JSON of any size,
declared with defstorage with its encoding, taken up with use-storage, which a reload and a server restart
leave, and which the server stores none of. An action takes one as its value, sets it with storage/patch and
drops it with storage/forget; storage/observe reads it, for what only the server can render from it. Entries
are the user's, kept only while they differ from their default, and dropped by age and version.ui/attr, under ui/shown and ui/aria-expanded, has any attribute follow any expression from the first paint,
with aria-* states as the words true and false, ui/path names a signal for data-bind, and ui/json writes
a value of the server's into an expression.
Scripts the page needs, such as a web component (co.multiply.tropical.script): declared with defscript and taken
up with use by the islands that need them, served by ring/wrap-scripts at URLs named by their content, and loaded
once per page: in its head when the first frame needs them, or over the stream when an island needing one is patched
in.
Actions as capabilities: tokens bound to the user, revoked when their island stops rendering them. A handler opens
another page on the page's origin by returning (action/navigate path), and with :signals, the signals it would
otherwise return are taken first, so what they write to the cookie or the browser's storage, such as a new item's
state or the draft the action used up, is written before that page loads. An island whose page belongs elsewhere
now, such as one showing an item that moved, renders (action/navigate-element path), which takes every tab
showing it there. An action can take a value only the browser knows when it is invoked (use-action's :value),
such as the clicked element's or a custom event's, passed to its handler beside the signals, in its own body.
use-token gives a script that posts to the page itself an action's token. A handler reads the request it came on
with action/request, and opens a page on another origin, such as a sign-in provider's, with navigate's
:external. Work that outlives its action patches the page's signals when it lands, through the island's
use-patch-signals.
Files the browser sends straight to the app's storage, such as a bucket taking presigned PUTs
(co.multiply.tropical.upload): use-uploads asks the app where each file goes, keeps the list for the island to
render, with each file's progress in a signal, and takes a report on an upload only for one of the island's own, named
by the id it gave. :settled takes a pick in whole, once nothing is on its way. drop-area makes any element a drop
area, the innermost under the pointer taking a drag, with dropping for the app's own overlay, and ui/page-attrs
refuses files dropped outside every area.
The page sees opaque, deterministic digests of the app's names: an island's element id, a cookie or storage state's
signal, a script's URL (co.multiply.tropical.names). The JVM property co.multiply.tropical.readable-names shows
them as written, for debugging. The default error view no longer names the island.
Islands in an app's tests (co.multiply.tropical.test), rendered as a session renders them, without a server or a
browser: render! waits briefly for pending reads, action invokes an action an island renders, found by its
island's path and its key, and failures lists the renders that threw.
A Claude Code plugin in the repository, whose tropical skill gives Claude what it can't infer from the API when it
writes islands.
A clj-kondo config in the jar, for apps to import, that lints defisland as defn, and defcookie, defstorage,
defscript and defshared as def.
An app-defined error view for islands that fail, which by default shows no exception message, and logging through
clojure.tools.logging.
Signals in Java: Cell and RefSignal mark a session's subscription at most once until it catches up, without
allocating.
Can you improve this documentation?Edit on GitHub
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 |