(observe & args f) (co.multiply.tropical.hooks) reads a resource: a database subscription, a poll loop, a query.
f opens it, as in Missionary's observe: it is called with an emit! fn and args, and returns what undoes it.
(defisland ticker
[topic]
(let [reading (observe topic
(fn [emit! topic]
(let [sub (subscribe-feed topic emit!)]
#(unsubscribe sub))))]
(if (hooks/pending? reading)
[:p "Connecting…"]
[:p (:value reading)])))
args shares one resource,
opened by the first reader and closed after the last leaves, once it has lingered. So args decide who shares it:
everything f depends on must be among them, and f may refer to no local of the render except through its params.
One that does fails to compile. Data that differs per user needs the user, or their account, among the args. Pass
values and stable references: args are compared with =, so a fresh closure would open a new resource each render.args, returns the same value. So an
app's own hook over observe, whose reads all share its call site, may be called by several hooks in one render,
each asking its own question of the same data, over one resource. use-shared is the same for a state's instance.f returns: a fn, called once when the resource closes; or a
Quiescent task, such as a poll loop's, cancelled when it closes, whose
failure fails the resource; or nil.f runs on a virtual thread of its own, so it may block while it connects. It runs in an empty
scope, since the resource belongs to no single reader: read a scoped value in the render and pass it,
(observe (ask *account*) query-account).emit! keeps the latest value. A session renders whatever is current when it gets to it, so values in between
may never be seen. It may be called from any thread, and does nothing once the resource has closed.observe returns hooks/pending until the first value, which hooks/pending? tells
apart; a session's first frame can wait briefly for it (:first-frame-ms, in Serving pages). It
throws in the render when f throws or its task fails, so try handles a failure; uncaught, the island renders the
error view. The failed resource is closed, and the next reader opens a new one.args release the old resource, which lingers, and hold the new one.pending, pending. If one of args is pending, observe returns pending and opens nothing, so a read
given another's value starts once that one has loaded.Reads of a shape of their own build on it: a function, whose one call site every caller shares, so its resources are
keyed by its args alone, or a macro, which makes a call site at each use:
(defn <-
"The value at `path`, through a subscription shared by every reader."
[db path]
(observe db path
(fn [emit! db path]
(let [sub (subscribe db path emit!)]
#(unsubscribe sub)))))
(defmacro ?
"A one-off query: the task `(apply f args)`, once per call site and `args`."
[f & args]
`(observe ~@args
(fn [emit!# & args#]
(q/then (apply ~f args#) emit!#))))
A query read like this is shared and cached as a subscription is: identical queries run once, and a reader arriving while the result is held sees it at once.
A read's value is pending until it has loaded. observe given pending returns pending and opens nothing, but a
value taken from a pending read, such as (:id user), is nil, and a read given it opens with nil. Branching on
pending? avoids that, and renders what has loaded at each step. A read inside a branch is fine, since hooks are keyed
by name, not by call order:
(let [user (observe uid load-user)]
(if (hooks/pending? user)
[:p "Loading…"]
[:div
[:h2 (:name user)]
(let [orders (observe (:id user) load-orders)]
(if (hooks/pending? orders)
[:p "Loading orders…"]
[:ul (for [o orders] [:li (:title o)])]))]))
The inner part could just as well be an island of its own, given user once it has loaded. That is the usual shape
where a read gives a collection: an island per item, keyed by it, each reading its own.
(defisland item-row {:key identity}
[item-id]
(let [item (observe item-id load-item)]
(if (hooks/pending? item)
[:li "Loading…"]
[:li (:title item)])))
(defisland item-list
[list-id]
(let [item-ids (observe list-id load-item-ids)]
(if (hooks/pending? item-ids)
[:p "Loading…"]
[:ul (for [item-id item-ids] (item-row item-id))])))
Each item's read is a resource of its own, shared by every session showing the item, and each row shows as it loads,
sent alone. A session's first frame waits for the rows' reads too, within its budget: they are reads of islands that
appear once the list's read lands. The check isn't optional here, since for over pending throws, and the list would
show its error view until its ids load.
Where reads wait together, with nothing to show until all have loaded, if-loaded is shorter. It binds as let does,
but evaluates a binding only once the bindings it refers to have loaded:
(if-loaded [user (observe uid load-user)
orders (observe (:id user) load-orders)
prefs (observe uid load-prefs)]
[:section {:class (:theme prefs)}
[:ul (for [o orders] [:li (:title o)])]]
[:p "Loading…"])
orders waits for user. prefs refers to neither, so it starts in the same render as user: reads that don't
depend on each other still load side by side. References are found by name, in the expression as written, so a name
bound again inside it still counts, which errs toward waiting. Without an else, if-loaded returns pending, which
can feed another read, or another if-loaded.
A session's first frame waits for its reads, so a read opened with nil may never show on a cold page load, but no
later frame waits: when user's args change, or the island first appears after an action, the read opened with nil
shows, as the error view or as data that isn't. A shared state's key taken from a read is the same:
(use-shared drafts (:id user)), with user pending, reads the instance for nil, which is everyone's.
observe call site and its args, opened by the first subscriber, however many
sessions read it.linger-ms (5 s) before closing. This absorbs remount churn:
switching back and forth, navigation, reconnects.f returned, or cancels the task f returned. A cleanup fn
returned after the resource closed is called as soon as it is returned.:init, lingers for the state's :linger-ms, and changes by shared/swap!.(resource/summary) lists the open resources, each with its state (:connecting, :live or
:lingering) and how many islands read it.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 |