Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 37s
Three composing pieces that make the blog SPA correct and resilient.
Content-addressed module cache (lib/host/static.sx, serve.sh, blog.sx shell,
conformance.sh): index each web-stack .sxbc by the content hash in its head,
serve GET /sx/h/{hash} immutable text/sx, and emit <script data-sx-manifest>
{file->hash} so the WASM client loads modules content-addressed (localStorage +
immutable) instead of path + max-age. serve.sh builds the index at boot;
conformance.sh now loads static.sx before blog.sx (the shell calls
host/static-manifest-json).
Declarative relate picker (lib/host/blog.sx, lib/dream/form.sx): replace the
inline /relate-picker.js blob — which never ran on swapped-in content, so the
candidate list was empty after a boosted nav to /<slug>/edit — with a declarative
SX-htmx form: sx-get relate-options on "load" + debounced "input", innerHTML-swap
the results ul; infinite scroll via a server-emitted "load more" sentinel
(sx-trigger revealed, sx-swap outerHTML) that pages the rest, q preserved via a
new symmetric dr/url-encode. The engine re-binds these triggers on swapped
content, so the picker populates on full load AND boosted SPA nav. Candidate
relate forms get :sx-disable (plain POST->303->reload, their original behavior;
the engine would otherwise boost them and swap the redirect unreliably).
sx-retry "exponential:1000:30000" on the form+sentinel retries a dropped/offline
fetch forever (the cap bounds the interval, not the attempts).
SIGPIPE hardening (hosts/ocaml/bin/sx_server.ml): the native http-listen server
had no SIGPIPE handler, so a client aborting an in-flight fetch (the engine
cancels superseded requests on a debounced filter/fast nav) closed the socket
mid-write and killed the whole process (exit 141). Ignore SIGPIPE so the failed
write becomes a catchable Sys_error the per-connection handler already swallows.
Tests: host conformance 272/272; relate-picker.spec.js 5/5 incl. a boosted-nav
populate regression; spa-check 4/4.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
dream-on-sx
OCaml's Dream web framework, reimplemented in plain SX on the CEK evaluator. Dream is the cleanest middleware-shaped HTTP framework in any language, and it maps onto SX with almost no impedance:
| Dream | SX |
|---|---|
handler = request -> response promise |
(fn (req) … (perform …)) |
middleware = handler -> handler |
(fn (next) (fn (req) …)) |
m1 @@ m2 @@ handler |
(m1 (m2 handler)) — left fold |
Dream.run handler |
(dream-run handler) → (perform (:http/listen …)) |
There are five types — request, response, route, and (as plain functions) handler and middleware. Everything else is a function over them.
Quickstart
(dream-run
(dream-make-app
(list
(dream-get "/" (fn (req) (dream-html "<h1>Hello, World!</h1>")))
(dream-get "/hello/:name"
(fn (req) (dream-text (str "Hi, " (dream-param req "name"))))))))
dream-make-app wraps the router in the default stack (error catch + content-type).
dream-run installs the root handler on the existing SX HTTP server — it does not
open its own socket.
Public surface
- types —
dream-request/dream-response/dream-route, accessors (dream-method/-path/-body/-header/-query-param/-param), smart constructors (dream-html/-text/-json/-empty/-not-found/-redirect), convenience (dream-queries,*-ordefaults,dream-accepts?/dream-wants-json?). - router —
dream-get/-post/-put/-delete/-patch/-head/-options/-any,dream-router,dream-scope(prefix + middleware),:nameparams +**catch-all, 405 +Allow, automatic HEAD. - middleware —
dream-pipeline,dream-no-middleware,dream-logger,dream-content-type,dream-set-header,dream-tap-request. - session —
dream-sessions/dream-sessions-signed,dream-session-field/dream-set-session-field/dream-session-all/dream-invalidate-session; cookie helpers (dream-cookie,dream-set-cookie,dream-cookie-sign/-unsign). - flash —
dream-flash,dream-add-flash-message,dream-flash-messages. - form —
dream-form(Ok/Err),dream-form-fields,dream-multipart, CSRF (dream-csrf/dream-csrf-protect/dream-csrf-token/dream-csrf-tag). - websocket —
dream-websocket,dream-send/-receive/-close/-broadcast. - static —
dream-static(mime, ETags, 304, ranges, traversal guard). - error —
dream-catch,dream-status-text/-line,dream-status-page. - cors —
dream-cors,dream-cors-origin,dream-cors-with. - json —
dream-json-encode/-parse,dream-json-value,dream-json-body. - run / api —
dream-run/-port/-opts,dream-app,dream-make-app,dream-serve.
Testing story
Every effectful concern is dependency-injected, so the whole framework is testable without a running host:
- sessions take a backend
(fn (op) …)—dream-memory-sessionsfor tests,dream-perform-sessionsin production; - static files take an fs —
dream-memory-fsvsdream-static-perform-fs; - websockets take an io —
dream-mock-wsvsdream-ws-perform-io; dream-runtakes a listen transport (dream-run-with).
Run the suite: bash lib/dream/conformance.sh (367 tests, 14 suites).
Notes & caveats
- Headers are dicts with lowercased string keys (in SX keywords are strings, so
:content-type=="content-type"). - Outgoing cookies accumulate in a
:set-cookieslist on the response so multipleSet-Cookieheaders don't collide. - The CSRF/cookie/ETag signing uses a pure-SX keyed hash — not cryptographic.
Production should inject a host HMAC (
dream-csrf-with, and the signed-session secret path). - JSON and multipart are in-memory (not streaming).