Headless EDN-over-HTTP daemon: one JVM owns one KB and serves it to remote clients
(vaelii.impl.client). A thin reitit-ring + jetty layer over vaelii.core, the
network dual of the in-process API.
Wire format is EDN. A sentence is a symbol s-expression — (dog Fido), ?x,
(genl dog animal) — which EDN round-trips losslessly; JSON would mangle the symbols.
The body of every call is {:op <keyword> :args [...]}, and the reply is
{:ok true :result …} or {:ok false :error "…"}. EDN is read with
clojure.edn/read-string (never clojure.core/read-string), so an untrusted body
cannot evaluate code — EDN has no reader-eval.
The daemon is the single writer (docs/storage.md, the single-writer contract): it owns the one process allowed to mutate the store, so it serializes every op through one monitor. Concurrent client writes therefore apply one at a time and cannot interleave; reads pay the same lock, which is conservative but keeps the contract simple.
Only the allowlisted ops are reachable (ops). Each is a vaelii.core fn with
the KB supplied by the daemon — the client sends only the op and the remaining args —
so no client can reach an arbitrary var. Sentex records in a result are projected to
plain maps before they hit the wire (the sentex-map contract), so the client reads
them back without the impl record class.
Headless EDN-over-HTTP daemon: one JVM owns one KB and serves it to remote clients
(`vaelii.impl.client`). A thin reitit-ring + jetty layer over `vaelii.core`, the
network dual of the in-process API.
**Wire format is EDN.** A sentence is a symbol s-expression — `(dog Fido)`, `?x`,
`(genl dog animal)` — which EDN round-trips losslessly; JSON would mangle the symbols.
The body of every call is `{:op <keyword> :args [...]}`, and the reply is
`{:ok true :result …}` or `{:ok false :error "…"}`. EDN is read with
`clojure.edn/read-string` (never `clojure.core/read-string`), so an untrusted body
cannot evaluate code — EDN has no reader-eval.
**The daemon is the single writer** (docs/storage.md, the single-writer contract): it
owns the one process allowed to mutate the store, so it serializes every op through
one monitor. Concurrent client writes therefore apply one at a time and cannot
interleave; reads pay the same lock, which is conservative but keeps the contract
simple.
**Only the allowlisted ops are reachable** (`ops`). Each is a `vaelii.core` fn with
the KB supplied by the daemon — the client sends only the op and the remaining args —
so no client can reach an arbitrary var. Sentex records in a result are projected to
plain maps before they hit the wire (the `sentex`-map contract), so the client reads
them back without the `impl` record class.(-main & args)Run the daemon in the foreground. Args: [port [dir]] [--listen ADDR] — dir
selects the durable :disk backend (recovered on open, so it persists across
restarts); with no dir the KB is in-memory and lives only as long as the process.
lein run -m vaelii.impl.serve 4200 /var/lib/vaelii lein run -m vaelii.impl.serve 4200 /var/lib/vaelii --listen 0.0.0.0 ; opt-in
It binds loopback unless --listen says otherwise, for the reason on loopback
above: POST /op writes, and nothing authenticates it. Put a reverse proxy that does
in front of it before naming an address.
Run the daemon in the foreground. Args: `[port [dir]] [--listen ADDR]` — `dir` selects the durable `:disk` backend (recovered on open, so it persists across restarts); with no `dir` the KB is in-memory and lives only as long as the process. lein run -m vaelii.impl.serve 4200 /var/lib/vaelii lein run -m vaelii.impl.serve 4200 /var/lib/vaelii --listen 0.0.0.0 ; opt-in It binds **loopback** unless `--listen` says otherwise, for the reason on `loopback` above: `POST /op` writes, and nothing authenticates it. Put a reverse proxy that does in front of it before naming an address.
(app kb)The ring handler for a KB — pure request -> response, so it is tested without a
socket. One monitor per handler serializes the ops (the single-writer contract).
The ring handler for a KB — pure `request -> response`, so it is tested without a socket. One monitor per handler serializes the ops (the single-writer contract).
The reachable operations, keyed by op keyword. Reads, writes, and introspection —
the working set a remote caller needs; extend by adding a vaelii.core fn here.
The reachable operations, keyed by op keyword. Reads, writes, and introspection — the working set a remote caller needs; extend by adding a `vaelii.core` fn here.
(port server)The actual TCP port a started Server is listening on — the ephemeral one when it
was started with :port 0, read off its first connector.
The actual TCP port a started `Server` is listening on — the ephemeral one when it was started with `:port 0`, read off its first connector.
(start kb {:keys [port host] :or {port 4200 host loopback}})Start the daemon over kb and return the running jetty Server (:join? false, so
the caller controls its lifetime — a test stops it in a finally). :port 0 binds
an ephemeral port; read the actual one with port.
:host defaults to loopback; pass an address ("0.0.0.0") to bind publicly, and
read the note on loopback before doing so.
Start the daemon over `kb` and return the running jetty `Server` (`:join? false`, so the caller controls its lifetime — a test stops it in a `finally`). `:port 0` binds an ephemeral port; read the actual one with `port`. `:host` defaults to loopback; pass an address (`"0.0.0.0"`) to bind publicly, and read the note on `loopback` before doing so.
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 |