Liking cljdoc? Tell your friends :D

Spindel

Clojars Project CircleCI Slack

Reactive computation you can fork.

Spindel is a reactive programming system for Clojure / ClojureScript in which the execution context — every signal, every cached spin result, every continuation — is a first-class value. Fork it in O(1), let two branches diverge, then commit or discard. The same shape that makes Datahike databases branchable, applied to running reactive state.

(require '[org.replikativ.spindel.core                :as s
                                                      :refer [spin signal track]]
         '[org.replikativ.spindel.incremental.interval :as iv]
         ;; For REPL determinism only — production reactive flows go
         ;; through render-spin! / spawn! and don't need this.
         '[org.replikativ.spindel.engine.impl.simple   :as engine])

(def root (s/create-execution-context))

;; A signal and a spin that tracks it.
(s/with-context root
  (def counter (signal 0))
  (def doubled (spin (* 2 (iv/get-new (track counter))))))

(s/with-context root
  @doubled                          ; => 0  (initial run)
  (swap! counter inc)
  ;; Signal changes drain asynchronously on a background thread.
  ;; `await-drain-complete!` blocks until the engine is idle so
  ;; the next read is deterministic.
  (engine/await-drain-complete! root)
  @doubled)                         ; => 2

;; Fork the whole reactive system. Mutations in the fork don't
;; reach the parent; reads fall through to parent's unchanged state.
(def fork (s/fork-context root))

(s/with-context fork
  (swap! counter inc)
  (engine/await-drain-complete! fork)
  @doubled)                         ; => 4   (fork sees its own counter)

(s/with-context root
  @doubled)                         ; => 2   (parent unchanged)

Most of the API surface ships through the convenience namespace org.replikativ.spindel.core (aliased s above) — spin, signal, track, await, the combinators (parallel, race, timeout, …), fork-context, atoms, pub/sub. Two things require their own require: iv/get-new (typed-interval accessor in incremental.interval) and await-drain-complete! (the REPL barrier in engine.impl.simple — production code rarely needs it).

Why Spindel

  1. Fork the running reactive system, not just the code. Execution contexts are values. Snapshot one, restore it later, serialize it across a network, or fork it for speculative computation — the engine guarantees O(1) forking via overlay backends with copy-on-write semantics. Forks share unmodified state structurally; mutations isolate. Compare to most FRP / signal libraries where the reactive graph is global mutable state.

  2. CPS + effects as the extension axis. The spin macro is a partial-CPS transform with track, await, and yield registered as plug-in effects. Libraries register their own — sample / observe for probabilistic programming, custom suspension points for distributed RPC, IO bindings for web frameworks — and the new effects participate in spin expansion as first-class citizens. No core changes required.

  3. Typed delta algebra end-to-end. Reactive combinators (imap, ifilter, islice, ifor-each, ireduce) emit algebra records carrying their composition laws inline (sequence / map / scalar). The DOM discharge consumes the typed shape directly — a 10 000-item virtual list only touches the items entering or exiting the visible window per scroll, the same shape end-to-end from the signal mutation through the DOM patch.

Use Cases

  • Branching document editorexamples/versioned-editor forks the entire document state per author, lets them iterate independently, then merges accepted changes back. Time-travel preview by clicking any DAG row. Real-world prototype for AI-coded document inference (see simmis).
  • Virtual scroll over 10 000 itemsexamples/infinite-scroll with islice keeps a window over a long source; each scroll emits a single typed :seq-diff carrying the items entering and exiting, and the DOM discharge applies only that delta.
  • TODO MVC + block editorexamples/todo, examples/block-editor show the deltaable-collection pipeline end-to-end and the ifor-each macro for keyed lists.
  • Foreign-node integrationexamples/tiptap embeds a TipTap (ProseMirror) editor as a foreign subtree; Spindel manages the surrounding vnode tree, TipTap owns its subtree, and a signal feeds the live content back through Spindel.
  • Probabilistic programming (src/.../inference) — sample / observe / constrain are registered as Spindel effects so inference programs are spins, MCMC kernels are forks, and trace addresses are deterministic by source-loc.
  • Distributed scopesdefn-spin-remote lets you define a function that executes on a remote peer with explicit boundary-crossing arguments; pairs with distributed-scope for kabel-WebSocket transport.

Status

Beta. JVM: 772 tests / 2606 assertions passing; CLJS: 363 tests / 1374 assertions passing. Public API may evolve before 1.0; we'll call out breaking changes in CHANGELOG.

Install

;; deps.edn
{:deps {org.clojure/clojure {:mvn/version "1.12.0"}
        org.replikativ/spindel {:mvn/version "0.1.0"}}}

ClojureScript builds need org.clojure/clojurescript and shadow-cljs (or equivalent) — see examples/shadow-cljs.edn for a working config.

Documentation

GuideWhat it covers
Getting StartedFirst-spin tutorial, signals, effects, running tests.
ConceptsMental model: spins, signals, runtime, glitch-free FRP.
API ReferenceNamespace-by-namespace listing of every public function and macro.
Effectsawait, track, yield, deferred and mailbox synchronization.
Custom EffectsRegister your own effects with register-effect-by-symbol!.
Combinatorsparallel, race, sleep, timeout, debounce, throttle, accumulate.
IncrementalDeltaable collections, typed delta algebra, Interval 3-state contract.
AtomsFork-safe runtime-backed atoms.
Forkingsnapshot-context, restore-snapshot, fork-context, serialization.
SchedulingEvent queue, drain loop, executors, platform differences (JVM vs CLJS).
EngineState shape, deterministic addressing, CPS / trampoline mechanics, overlay backend, memory invariants. The implementation deep-dive.
Pub/Submult, pub, buffers, async-sequence-based fan-out.
Distributeddefn-spin-remote, spin-remote, spin↔channel bridge, distributed-scope integration.
SCI IntegrationSandboxed spin execution via the Small Clojure Interpreter.

For contributor patterns and AI-assistant guidance (do's/don'ts when modifying the engine, project-specific conventions), see CLAUDE.md.

Ecosystem

Spindel is part of replikativ, a set of composable building blocks for branchable, immutable systems:

  • datahike — durable Datalog database with git-like branching. Pair with Spindel for reactive queries over branchable data.
  • kabel — WebSocket middleware for real-time transport.
  • distributed-scope — peer-to-peer RPC over kabel; what defn-spin-remote builds on.
  • hasch — content- addressed hashing for stable structural identity, used internally by Spindel's deterministic addressing.
  • yggdrasil — branchable memory model protocols spanning Git, ZFS, Datahike, and more. Spindel's fork-context shares the spirit; yggdrasil generalizes it to a cross-system protocol.
  • partial-cps — the CPS transformation engine that powers the spin macro.

A 60-Second Mental Model

Three primitives that compose:

  • A signal is a mutable, reactive source of values.
  • A spin is a cached computation that runs track (to subscribe to a signal) or await (to subscribe to another spin's result). When a tracked dependency changes, the spin re-runs.
  • An execution context holds every signal, every spin result, every continuation. It's a regular value: you can fork-context it (overlay-backed, O(1) copy-on-write), snapshot-context it (an immutable copy for checkpointing), or serialize-context / deserialize-context round-trip it over the wire.

A spin is not an asynchronous task — it's a cached function whose re-execution is triggered by signal changes. Forking the context gives you a private copy of all the cached results; the parent never sees your fork's mutations.

For the full mental model, see docs/concepts.md.

Running Tests

# JVM
clj -M:test

# ClojureScript (Node) — uses root shadow-cljs.edn :test target
npx shadow-cljs compile test     # or `watch test` for re-run on save

CLJS tests run via shadow-cljs's :node-test target (jsdom-backed DOM tests included). See docs/scheduling.md for the JVM / CLJS executor differences and the platform-specific testing patterns.

Critical Rules (or: the two things every new user trips over)

  1. Inside a spin body, use (await x) / (track x) — never @x. The spin macro CPS-transforms await and track into continuation breakpoints; raw @ deref blocks the thread, breaks the continuation chain, and silently produces wrong results. Outside spin bodies (REPL, tests), @ is fine.

  2. Effects don't survive into closures. (spin (map #(await (fetch %)) items)) doesn't work — the macro only transforms its lexical body, and the function passed to map is opaque. Use loop/recur for sequential work, or nest (spin …) per item and use (apply parallel child-spins) for concurrent work. See CLAUDE.md "CPS Transformation Limitations" for the long version.

License

Copyright © 2024–2026 Christian Weilbach.

Licensed under the Apache License 2.0 (see LICENSE).

Can you improve this documentation?Edit on GitHub

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