A sketch of an nREPL server for SCI, written in ClojureScript, small enough to run in a web page.
The server itself (nrepl.sci.server) knows nothing about sockets or
threads. nrepl.sci.supervisor stands in front of it, and runs it in a
thread of its own (nrepl.sci.evaluator), so code can wait for input and
be interrupted. There are two hosts for all that:
nrepl.sci.nodeserves it over TCP in Node, which is what clients and proof connect tonrepl.sci.workerruns it in Web Workers, which take and return the bencode of each message, so a page can show exactly what goes over the wire
SCI doesn't come with an nREPL server, but some projects built on it have one:
- sci.nrepl, which scittle uses,
lets an editor evaluate code in a page. The page connects over a
websocket to a relay running in babashka or on the JVM, and the editor
connects to the relay. The relay answers
describeandcloneitself, but every session shares the page's one REPL, printed output isn't sent back as:out, and there's nointerruptorstdin. - nbb has a server of its own, for
Node, with most of the common ops. It's part of nbb, though, its
sessions share one REPL too, and there's no
interruptorstdin. - babashka.nrepl is for SCI in Clojure, on the JVM or in a native image.
nrepl-sci is for something else: a server that runs entirely in a page,
with the page as its client, as in the playground on nrepl.org. Nothing
else has to run, sessions have their own namespace, *1 and *e, as in
nREPL, and code can read input and be interrupted. An editor can't connect
to it there, which is what sci.nrepl is for. The Node server is mostly
there so that clients and proof can test it.
You'll need the Clojure CLI and Node.
make # out/nrepl-sci.js, the Node server
make worker # out/nrepl-sci-worker.js, the Web Worker
make test # runs the tests against the Node server
make proof # runs proof against the Node serverbin/nrepl-sci --port 7888 starts the Node server.
It supports describe, clone, close, ls-sessions, eval,
load-file, interrupt, stdin, completions, complete, lookup,
info and eldoc, and passes every proof check that applies to it.
Completion and the lookups know the special forms too, with
ClojureScript's docs for them, and so does doc, which is referred in
user along with the rest of clojure.repl, as in Clojure's own REPL.
Output goes out a line at a time, as in nREPL, or when it's flushed.
SCI evaluates code in one go, so the evaluator can't take messages while
code runs. What it needs then comes through shared memory instead
(nrepl.sci.mailbox), which it checks as the code calls functions:
whether it's been interrupted, and whether there's a request it could
take in between. That's how a session evaluating something doesn't hold
up the others, and why read-line can wait for input with Atomics.wait
while still answering requests from other sessions. A session's own
evals wait their turn, as in nREPL. Checking only every 256th call keeps
it cheap: code runs about as fast as without any of this.
A request taken in between runs on top of the code it interrupted, so that
code can't stop, even when interrupted, until the request is done. A
read-line taken in between holds it until it gets its input.
The seqs that never end, (range), (repeat x), iterate and cycle,
check too, so (count (range)) stops when interrupted. Code stuck in
JavaScript, like a regex that backtracks forever, never checks, so the
supervisor gives it a second and then kills the evaluator and starts over.
The sessions are still there afterwards, but everything evaluated in them
is gone. A finite seq big enough to take a while ends the same way.
Browsers only give a page SharedArrayBuffer when it's cross-origin
isolated, which takes the Cross-Origin-Opener-Policy and
Cross-Origin-Embedder-Policy headers. Without it, the evaluator takes
requests one at a time, read-line throws and interrupting always means
starting over. bin/nrepl-sci --no-shared-memory runs the Node server
that way too.
Copyright © 2026 Bozhidar Batsov and contributors.
Distributed under the Eclipse Public License 1.0, the same as Clojure.