host: content-addressed SPA cache + declarative SX-htmx relate picker + SIGPIPE hardening
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>
This commit is contained in:
2026-06-29 14:30:17 +00:00
parent b9a24d5870
commit bdc7e02fbc
8 changed files with 257 additions and 50 deletions

View File

@@ -58,6 +58,43 @@
((s2 (replace s "+" " ")))
(dr/url-decode-loop s2 0 (string-length s2) ""))))
;; ── percent encoding (symmetric with dr/url-decode) ────────────────
;; RFC3986 unreserved set passes through; everything else is %XX (uppercase
;; hex). Space becomes %20 (not +), so the result is safe in a query value.
(define dr/hex-chars "0123456789ABCDEF")
(define
dr/url-encode-char
(fn
(c)
(let
((n (char-code c)))
(if
(or
(and (>= n 48) (<= n 57)) ;; 0-9
(and (>= n 65) (<= n 90)) ;; A-Z
(and (>= n 97) (<= n 122)) ;; a-z
(= c "-") (= c "_") (= c ".") (= c "~"))
c
(str "%"
(char-at dr/hex-chars (quotient n 16))
(char-at dr/hex-chars (mod n 16)))))))
(define
dr/url-encode-loop
(fn
(s i n acc)
(if
(>= i n)
acc
(dr/url-encode-loop s (+ i 1) n
(str acc (dr/url-encode-char (char-at s i)))))))
(define
dr/url-encode
(fn
(s)
(dr/url-encode-loop (or s "") 0 (string-length (or s "")) "")))
;; ── substring splitter (split primitive is char-class based) ───────
(define
dr/split-on