Thin, observable nREPL client for clj/eval.
Connection model:
nrepl.core/connect socket per [host port] key, cached
on a defonce atom so we survive (require :reload) during
development.eval!. This is how Cider/Calva/every editor drives nREPL: no
per-eval clone/close round-trip on the hot path (those were
what blew the native-tool timeout budget under JVM load), nothing
to leak, and session-local state — *1/*2/*3/*e and dynamic
set!s — PERSISTS across calls like a real REPL ((def …) was
already global; now the whole session is).IOException / nil
message stream) and the entry is evicted — closing the socket and
dropping the cached session — so the next call re-dials + re-clones.Returned shape (success) — STRING keys (crosses the strings-only boundary
as a tool :result; enrichment adds "error_message"/"error_data"/"trace"):
{"value" "42" ; pr-str of the LAST form's value, or nil
"values" ["1" "42"] ; pr-str of every emitted value
"out" "hello\n" ; stdout aggregated
"err" "" ; stderr aggregated
"ns" "user" ; final ns name
"status" #{"done"} ; nREPL status set (strings)
"ex" nil ; exception class name, when status :ex
"root_ex" nil ; root exception class name
"ms" 12 ; wall-clock duration
"port" 7888
"timed_out" false}
Failure paths throw ex-info with :type :clj/nrepl-* so the
Vis tool wrapper can surface a clean error to the model.
Thin, observable nREPL client for `clj/eval`.
Connection model:
* One `nrepl.core/connect` socket per `[host port]` key, cached
on a `defonce` atom so we survive `(require :reload)` during
development.
* ONE long-lived nREPL session per connection, cloned lazily on
first use and cached beside the socket — then REUSED by every
`eval!`. This is how Cider/Calva/every editor drives nREPL: no
per-eval `clone`/`close` round-trip on the hot path (those were
what blew the native-tool timeout budget under JVM load), nothing
to leak, and session-local state — `*1`/`*2`/`*3`/`*e` and dynamic
`set!`s — PERSISTS across calls like a real REPL (`(def …)` was
already global; now the whole session is).
* Stale / closed sockets are detected (`IOException` / `nil`
message stream) and the entry is evicted — closing the socket and
dropping the cached session — so the next call re-dials + re-clones.
Returned shape (success) — STRING keys (crosses the strings-only boundary
as a tool `:result`; enrichment adds "error_message"/"error_data"/"trace"):
{"value" "42" ; pr-str of the LAST form's value, or nil
"values" ["1" "42"] ; pr-str of every emitted value
"out" "hello\n" ; stdout aggregated
"err" "" ; stderr aggregated
"ns" "user" ; final *ns* name
"status" #{"done"} ; nREPL status set (strings)
"ex" nil ; exception class name, when status :ex
"root_ex" nil ; root exception class name
"ms" 12 ; wall-clock duration
"port" 7888
"timed_out" false}
Failure paths throw `ex-info` with `:type :clj/nrepl-*` so the
Vis tool wrapper can surface a clean error to the model.(close-all!)Close every cached connection. Idempotent. Useful from tests / doctor / shutdown hooks.
Close every cached connection. Idempotent. Useful from tests / doctor / shutdown hooks.
(eval! {:keys [host port code ns timeout-ms pretty? print-margin]
:or {host "localhost" timeout-ms 30000 print-margin 100}})Evaluate code in the nREPL at host:port. Opts:
:host defaults to "localhost"
:ns starting namespace, e.g. "user"
:timeout-ms default 30000
:pretty? when true, ask nREPL's print middleware to pretty-print the
value(s) SERVER-SIDE via nrepl.util.print/pprint — so the
live object is formatted where it lives (handles unreadable
objects / lazy seqs) and :value/:values come back as
multi-line, indented text. Output stays valid EDN.
:print-margin right-margin columns for pretty printing (default 100)
Always returns a map (see ns docstring). Connection failures
throw :clj/nrepl-connect-failed; everything else (eval error,
timeout) is reported inside the returned map so the model can
read it as data.
Evaluate `code` in the nREPL at `host:port`. Opts:
:host defaults to "localhost"
:ns starting namespace, e.g. "user"
:timeout-ms default 30000
:pretty? when true, ask nREPL's print middleware to pretty-print the
value(s) SERVER-SIDE via `nrepl.util.print/pprint` — so the
live object is formatted where it lives (handles unreadable
objects / lazy seqs) and `:value`/`:values` come back as
multi-line, indented text. Output stays valid EDN.
:print-margin right-margin columns for pretty printing (default 100)
Always returns a map (see ns docstring). Connection failures
throw `:clj/nrepl-connect-failed`; everything else (eval error,
timeout) is reported inside the returned map so the model can
read it as data.(evict! host port)Drop the cached connection (and its long-lived session) for [host port],
CLOSING the socket so BOTH sides let go: the server reaps the session's
executor thread and OUR client transport thread dies instead of parking
forever. Call it whenever a REPL at [host port] goes away (stop, reap,
crash) — a cache entry outliving its process leaks one thread + one socket
per REPL. The next call re-dials and re-clones.
Drop the cached connection (and its long-lived session) for `[host port]`, CLOSING the socket so BOTH sides let go: the server reaps the session's executor thread and OUR client transport thread dies instead of parking forever. Call it whenever a REPL at `[host port]` goes away (stop, reap, crash) — a cache entry outliving its process leaks one thread + one socket per REPL. The next call re-dials and re-clones.
(health-check! {:keys [host port timeout-ms]
:or {host "localhost" timeout-ms 2000}})REAL eval-based health check — the strong signal probe!'s describe can't
give. Runs (+ 1 1) through a DEDICATED health session on the cached
connection under a SHORT timeout and confirms it returns "2". describe
only proves the accept loop answers; this proves the JVM actually EVALUATES
code, so a wedged eval executor / blocked compile lock is caught even while
describe still replies. Returns:
{:status :up :ms N}
— evaluated "2" cleanly; the eval path is healthy.
{:status :unresponsive :form "(+ 1 1)" :ms N :hint "..."}
— connected but the eval TIMED OUT or didn't return "2": the REPL is
wedged and should be killed & restarted (or reprobed).
{:status :down :form "(+ 1 1)"}
— could not connect (stale .nrepl-port / dead process).
Uses a SEPARATE session so it never clobbers the user session's
*1/*2/*3. Never throws. On a hard socket error the connection is evicted
so the next call reconnects fresh.
REAL eval-based health check — the strong signal `probe!`'s `describe` can't
give. Runs `(+ 1 1)` through a DEDICATED health session on the cached
connection under a SHORT timeout and confirms it returns `"2"`. `describe`
only proves the accept loop answers; this proves the JVM actually EVALUATES
code, so a wedged eval executor / blocked compile lock is caught even while
`describe` still replies. Returns:
{:status :up :ms N}
— evaluated `"2"` cleanly; the eval path is healthy.
{:status :unresponsive :form "(+ 1 1)" :ms N :hint "..."}
— connected but the eval TIMED OUT or didn't return `"2"`: the REPL is
wedged and should be killed & restarted (or reprobed).
{:status :down :form "(+ 1 1)"}
— could not connect (stale `.nrepl-port` / dead process).
Uses a SEPARATE session so it never clobbers the user session's
`*1`/`*2`/`*3`. Never throws. On a hard socket error the connection is evicted
so the next call reconnects fresh.The canonical liveness eval: cheap, pure, and an unmistakable "2" result.
The canonical liveness eval: cheap, pure, and an unmistakable `"2"` result.
(probe! {:keys [host port timeout-ms] :or {host "localhost" timeout-ms 100}})Best-effort liveness probe for the nREPL at host:port. Sends a single
describe op (no code execution) under a SHORT timeout and classifies:
{:status :up :versions {:clojure ..} :dialect :clj|:cljs :cwd "/path"}
— connected and the server answered describe.
{:status :unresponsive}
— socket opened but no clean describe reply in budget.
{:status :down} — could not connect (stale .nrepl-port, dead proc).
:dialect (clj vs cljs) is read from the describe metadata; :cwd is the
server JVM's working directory via one tiny eval (nil when unavailable).
Both are best-effort and only present when :up.
Never throws. Reuses the cached connection (warming the same pool
eval! uses). The short :timeout-ms (default 100) is passed to
nrepl/client, which bounds each response read regardless of the
cached connection's transport timeout.
Best-effort liveness probe for the nREPL at `host:port`. Sends a single
`describe` op (no code execution) under a SHORT timeout and classifies:
{:status :up :versions {:clojure ..} :dialect :clj|:cljs :cwd "/path"}
— connected and the server answered describe.
{:status :unresponsive}
— socket opened but no clean describe reply in budget.
{:status :down} — could not connect (stale `.nrepl-port`, dead proc).
`:dialect` (clj vs cljs) is read from the describe metadata; `:cwd` is the
server JVM's working directory via one tiny eval (nil when unavailable).
Both are best-effort and only present when `:up`.
Never throws. Reuses the cached connection (warming the same pool
`eval!` uses). The short `:timeout-ms` (default 100) is passed to
`nrepl/client`, which bounds each response read regardless of the
cached connection's transport timeout.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 |