Spindel provides a runtime-backed atom that is API-compatible with
clojure.core/atom but stores its state inside the execution context. The
benefit: atoms participate in fork isolation, snapshot/restore, and
serialization just like signals do.
(require '[org.replikativ.spindel.core :as s])
(s/with-context ctx
(let [cache (s/atom [])]
(swap! cache conj :hello)
@cache)) ;; => [:hello]
clojure.core/atomUse s/atom whenever the atom needs to:
snapshot-context / restore-snapshot round-tripUse clojure.core/atom for ephemeral runtime helpers that have nothing to
do with the reactive context — for instance, a one-shot accumulator inside
a private function. Plain Clojure atoms are NOT fork-safe: writes happen
on a single shared cell regardless of which context is bound.
(s/atom initial-value)
(s/atom initial-value :meta {…})
s/atom reads the dynamically bound *execution-context* at call time. To
construct an atom for a specific context, use s/create-atom:
(s/create-atom initial-value :meta {…})
The returned object implements IDeref, IAtom, and IRef exactly like
clojure.core/atom:
@a ;; deref current value
(swap! a f & args) ;; atomic update
(reset! a v) ;; replace
(add-watch a key callback) ;; classic watch
(remove-watch a key)
Each fork sees its own copy of any atom that has been mutated in the fork:
(s/with-context root
(def cache (s/atom #{})))
(swap! cache conj :a) ;; root: #{:a}
(let [child (s/fork-context root)]
(s/with-context child
(swap! cache conj :b) ;; fork: #{:a :b}
@cache)) ;; => #{:a :b}
(s/with-context root
@cache) ;; root: #{:a} — fork's :b is invisible
Reads fall through to the parent until the fork mutates the atom; the mutation triggers the overlay backend's copy-on-write at the entity level.
Each atom registers a finalizer (Cleaner on the JVM,
FinalizationRegistry in browsers that support it) so that when nothing
references the atom value anymore, its [:atoms id] entry is dropped from
the runtime state map. You don't have to track lifetimes manually.
In CLJS environments without FinalizationRegistry (very old browsers),
the entry persists for the lifetime of the context. Drop the context to
reclaim.
A watcher is side-effecting egress (notify/publish), so it is stored at the
fork-local [:listeners id] path and fired synchronously at the swap commit
site. Two consequences:
The same [:listeners id] mechanism backs add-watch on spindel signals
(SignalRef) too — one fork-correct watch mechanism for both reference types.
Listeners are closures, so they are dropped on serialize-context (like
continuations); re-establish them by re-adding watches after restore-snapshot /
deserialize-context.
Atoms work the same inside spins:
(s/with-context ctx
(let [counter (s/atom 0)
bumper (s/spin
(let [x (s/await some-spin)]
(swap! counter inc)
x))]
@bumper
@counter))
Atoms are not reactive: signals are the right primitive when a value should drive re-execution of dependent spins. Atoms are for non-reactive state that nevertheless needs fork isolation.
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 |