Owned, session-scoped nREPL lifecycle for the Clojure pack.
OWNERSHIP: each vis SESSION owns its own nREPL subprocess(es). The processes
atom is keyed by [session-id dir], so two sessions in the same directory get
two independent REPLs and neither can see or stop the other's. A managed REPL
lives and dies with THIS vis process — there is NO persistent registry and NO
PID re-attach across a vis restart. Restarting vis means a fresh REPL, exactly
like the Python pack.
PORT: we PICK a free ephemeral port ourselves and pass it to the launcher
EXPLICITLY (nrepl.cmdline --port N, lein repl :headless :port N,
bb nrepl-server N), so we always KNOW our port without ever reading a
.nrepl-port file back. Any stray .nrepl-port a tool drops in the project is
deleted after boot — vis never depends on it and never leaves it behind.
ALIASES: a REPL is ALWAYS booted with the project's :dev :test deps + paths
on its classpath (full dependency spec), with the user's :main-opts dropped
(our synthetic :vis/nrepl-launch alias appends last so -m nrepl.cmdline
wins). Unknown :dev/:test aliases are silently ignored by tools.deps, so
this is safe in any project.
Starting/stopping is CORE and ALWAYS allowed — never gated behind a flag.
Owned, session-scoped nREPL lifecycle for the Clojure pack. OWNERSHIP: each vis SESSION owns its own nREPL subprocess(es). The `processes` atom is keyed by `[session-id dir]`, so two sessions in the same directory get two independent REPLs and neither can see or stop the other's. A managed REPL lives and dies with THIS vis process — there is NO persistent registry and NO PID re-attach across a vis restart. Restarting vis means a fresh REPL, exactly like the Python pack. PORT: we PICK a free ephemeral port ourselves and pass it to the launcher EXPLICITLY (`nrepl.cmdline --port N`, `lein repl :headless :port N`, `bb nrepl-server N`), so we always KNOW our port without ever reading a `.nrepl-port` file back. Any stray `.nrepl-port` a tool drops in the project is deleted after boot — vis never depends on it and never leaves it behind. ALIASES: a REPL is ALWAYS booted with the project's `:dev :test` deps + paths on its classpath (full dependency spec), with the user's `:main-opts` dropped (our synthetic `:vis/nrepl-launch` alias appends last so `-m nrepl.cmdline` wins). Unknown `:dev`/`:test` aliases are silently ignored by tools.deps, so this is safe in any project. Starting/stopping is CORE and ALWAYS allowed — never gated behind a flag.
(ensure-repl-for-dir! session-id dir)Return the live REPL info for [session-id dir], AUTOSTARTING one (with the
default :dev :test aliases) when the session owns none for dir — OR when the
recorded process is alive but UNREACHABLE (a boot that never bound its port, or
a wedged server thread that proc-alive? alone cannot see): such a stale
process is stopped and REPLACED instead of silently swallowing every eval on a
dead socket (the failure that stalls run_tests past the tool budget). A
still-booting process is given the REMAINING cold-boot window (see
health-probe-ms) to finish before it is judged wedged, so a legitimately
slow cold boot is never killed + restarted mid-flight (which would spin an
endless restart cycle across evals). When the (re)start does NOT yield a live
process, returns
start!'s STRING-keyed lifecycle result ("failed"/"no-launcher"… with exit +
log_tail) instead of swallowing it — callers tell the cases apart by :port
(live keyword-keyed info) vs "result" (string-keyed lifecycle map).
Return the live REPL info for `[session-id dir]`, AUTOSTARTING one (with the
default :dev :test aliases) when the session owns none for `dir` — OR when the
recorded process is alive but UNREACHABLE (a boot that never bound its port, or
a wedged server thread that `proc-alive?` alone cannot see): such a stale
process is stopped and REPLACED instead of silently swallowing every eval on a
dead socket (the failure that stalls `run_tests` past the tool budget). A
still-booting process is given the REMAINING cold-boot window (see
`health-probe-ms`) to finish before it is judged wedged, so a legitimately
slow cold boot is never killed + restarted mid-flight (which would spin an
endless restart cycle across evals). When the (re)start does NOT yield a live
process, returns
start!'s STRING-keyed lifecycle result ("failed"/"no-launcher"… with exit +
log_tail) instead of swallowing it — callers tell the cases apart by `:port`
(live keyword-keyed info) vs `"result"` (string-keyed lifecycle map).(health session-id dir)Coarse LIVE health of THIS session's managed REPL for dir:
:up — process alive AND the port answers an nREPL describe
:starting — process alive, port not answering yet
:failed — no live process but an UNEXPECTED death is on record
:down — nothing managed (or an intentional stop)
Used as the resource registry's :health-fn, so footer/F4/ctx status tracks
reality instead of the status frozen at registration time.
Coarse LIVE health of THIS session's managed REPL for `dir`: :up — process alive AND the port answers an nREPL describe :starting — process alive, port not answering yet :failed — no live process but an UNEXPECTED death is on record :down — nothing managed (or an intentional stop) Used as the resource registry's `:health-fn`, so footer/F4/ctx status tracks reality instead of the status frozen at registration time.
(id-of dir)Stable session-resource id for the REPL rooted at dir.
Stable session-resource id for the REPL rooted at `dir`.
(inherited-jvm-opts dir aliases)JVM options a nested project should INHERIT from an ancestor deps.edn.
The nREPL is launched with -M:dev:test:vis/nrepl-launch, so any :jvm-opts
dir's OWN deps.edn declares for those aliases already reach the JVM — in that
case nothing is inherited (returns nil, keeping the top-level project unchanged).
But a NESTED project whose deps.edn declares no such aliases (e.g. an extension
with a bare {:deps …} map) would otherwise boot a BARE JVM — missing the
workspace's flags (--enable-native-access, --enable-preview,
--sun-misc-unsafe-memory-access=allow, …) that its code needs, so tests crash
before they run. For that case we walk UP from dir to the nearest ancestor
whose deps.edn declares :jvm-opts for aliases and return them, so the nested
nREPL inherits the workspace's JVM options.
JVM options a nested project should INHERIT from an ancestor deps.edn.
The nREPL is launched with `-M:dev:test:vis/nrepl-launch`, so any `:jvm-opts`
`dir`'s OWN deps.edn declares for those aliases already reach the JVM — in that
case nothing is inherited (returns nil, keeping the top-level project unchanged).
But a NESTED project whose deps.edn declares no such aliases (e.g. an extension
with a bare `{:deps …}` map) would otherwise boot a BARE JVM — missing the
workspace's flags (`--enable-native-access`, `--enable-preview`,
`--sun-misc-unsafe-memory-access=allow`, …) that its code needs, so tests crash
before they run. For that case we walk UP from `dir` to the nearest ancestor
whose deps.edn declares `:jvm-opts` for `aliases` and return them, so the nested
nREPL inherits the workspace's JVM options.(last-failure session-id dir)The last UNEXPECTED launcher death recorded for [session-id dir], or nil.
STRING-keyed ("exit" "at" "log" "log_tail") — safe to splice into
model-facing results.
The last UNEXPECTED launcher death recorded for `[session-id dir]`, or nil.
STRING-keyed ("exit" "at" "log" "log_tail") — safe to splice into
model-facing results.(launcher-for dir aliases port)Subprocess command to boot a project nREPL in dir on the EXPLICIT port,
honouring aliases (deps.edn aliases / lein profiles). Returns
{:tool kw :cmd [strings]} or nil when no known Clojure build file is present.
Subprocess command to boot a project nREPL in `dir` on the EXPLICIT `port`,
honouring `aliases` (deps.edn aliases / lein profiles). Returns
`{:tool kw :cmd [strings]}` or nil when no known Clojure build file is present.(repl-by-id session-id id)The session's live REPL info matching resource id, or nil.
The session's live REPL info matching resource `id`, or nil.
(resolve-target! session-id id default-dir)Resolve — and AUTOSTART when needed — the REPL an eval should hit for
session-id. id is an optional explicit resource id; default-dir is where
we autostart when the session owns no REPL yet. Returns {:id :dir :port}.
Rules (the ownership contract):
id → that REPL (throws if no such live REPL in this session);id = default (any case) → sentinel, treated as no explicit id (below);default-dir with [:dev :test], use it;1 REPLs → use the DEFAULT: the REPL owning
default-dir(the workspace root) when present, else the first (dir-sorted). Never throws on ambiguity — eval always resolves and the result reports which REPL ran it, so the model can pass an explicitidto override.
Resolve — and AUTOSTART when needed — the REPL an eval should hit for
`session-id`. `id` is an optional explicit resource id; `default-dir` is where
we autostart when the session owns no REPL yet. Returns `{:id :dir :port}`.
Rules (the ownership contract):
- explicit `id` → that REPL (throws if no such live REPL in this session);
- `id` = `default` (any case) → sentinel, treated as no explicit id (below);
- 0 REPLs → autostart `default-dir` with [:dev :test], use it;
- 1 REPL → use it (it's the implicit default);
- >1 REPLs → use the DEFAULT: the REPL owning `default-dir` (the
workspace root) when present, else the first (dir-sorted).
Never throws on ambiguity — eval always resolves and the
result reports which REPL ran it, so the model can pass an
explicit `id` to override.(session-repls session-id)Live REPLs OWNED by session-id, as a vec of
{:id :dir :port :tool :aliases :pid} sorted by dir. Prunes dead entries as a
side effect. This is the SINGLE source of truth for ctx + eval/test targeting —
there is no external-port discovery.
Live REPLs OWNED by `session-id`, as a vec of
`{:id :dir :port :tool :aliases :pid}` sorted by dir. Prunes dead entries as a
side effect. This is the SINGLE source of truth for ctx + eval/test targeting —
there is no external-port discovery.(start! session-id dir)(start! session-id dir {:keys [aliases]})Self-start a project nREPL subprocess OWNED by session-id in dir.
Always allowed — never flag-gated. Default :aliases are [:dev :test] (merged
with any explicitly passed). We pick a FREE port, pass it to the launcher, drop
any stray .nrepl-port, and wait for OUR port — SYNCHRONOUSLY: the wait ends
the moment the launcher dies (fast :failed with exit + log tail), and only a
still-alive-but-slow boot can outlive start-deadline-ms (then :starting,
with an .onExit watcher recording any later death as a failure).
[session-id dir] → :already-running.Model-facing: STRING keys + STRING enum values (crosses as a tool :result).
Self-start a project nREPL subprocess OWNED by `session-id` in `dir`. Always allowed — never flag-gated. Default `:aliases` are [:dev :test] (merged with any explicitly passed). We pick a FREE port, pass it to the launcher, drop any stray `.nrepl-port`, and wait for OUR port — SYNCHRONOUSLY: the wait ends the moment the launcher dies (fast :failed with exit + log tail), and only a still-alive-but-slow boot can outlive `start-deadline-ms` (then :starting, with an `.onExit` watcher recording any later death as a failure). - Already ours + alive for `[session-id dir]` → :already-running. - No known build file → :no-launcher. - Launcher exits before binding → :failed with exit code + log tail. - Else :started (port up) or :starting (still coming up; ctx will show it). Model-facing: STRING keys + STRING enum values (crosses as a tool `:result`).
(status session-id dir)Live view of THIS session's managed REPL for dir. Always safe. Model-facing:
STRING keys + STRING enum values (crosses as a tool :result).
Live view of THIS session's managed REPL for `dir`. Always safe. Model-facing: STRING keys + STRING enum values (crosses as a tool `:result`).
(stop! session-id dir)Stop THIS session's managed nREPL for dir (graceful, then forced). The
entry is DEREGISTERED FIRST so the .onExit watcher reads the death as an
intentional stop, never a failure; any remembered failure for dir is
cleared too. No-op-safe. Model-facing STRING-keyed result.
Stop THIS session's managed nREPL for `dir` (graceful, then forced). The entry is DEREGISTERED FIRST so the `.onExit` watcher reads the death as an intentional stop, never a failure; any remembered failure for `dir` is cleared too. No-op-safe. Model-facing STRING-keyed result.
(tail-log log-path)(tail-log log-path n)Tail a managed nREPL launcher log as line strings, reading ONLY the last
tail-read-bytes of the file (never the whole thing). Returns [] when the
log does not exist yet or cannot be read; resource viewers treat that as an
empty but still log-capable resource.
Tail a managed nREPL launcher log as line strings, reading ONLY the last `tail-read-bytes` of the file (never the whole thing). Returns [] when the log does not exist yet or cannot be read; resource viewers treat that as an empty but still log-capable resource.
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 |