Files
rose-ash/lib/host/blog.sx
giles 7c11d4edaa
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 16s
host: per-request IO kernel fix + fully-dynamic blog (no cache), 159/159
KERNEL (sx_server.ml): route http-listen handlers through cek_run_with_io
instead of bare Sx_runtime.sx_call, so handlers resolve per-request IO
(durable persist reads/writes) via the same IO-driving runner the REPL uses.
Verified: per-request read+write, 10 concurrent writes (15 on disk, no
corruption), handler errors don't crash the server, http contract 6/6.

BLOG: fully dynamic — host/blog-post reads the post from the durable store
(content/head) AND renders (content/html) per request, no in-memory view, no
cached output. Possible because of the IO fix. Honest ~2s due to interpreted
Smalltalk render.

Render speed is NOT solved here: the JIT (precompiler) isn't installed in the
serving mode and currently miscompiles the Smalltalk evaluator's nested ASTs
(enabling it breaks ~60% of tests). Fixing the JIT is a separate, high-payoff
effort. Documented in the plan.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 18:59:36 +00:00

69 lines
3.3 KiB
Plaintext

;; lib/host/blog.sx — Blog domain on the host. Posts are content-on-sx documents
;; whose source of truth is the durable SX store (persist op-log on disk). Serving
;; GET /<slug>/ is FULLY DYNAMIC: the handler reads the post from the store and
;; renders it to HTML, per request — no in-memory view, no cached output. This is
;; possible because http-listen handlers now resolve per-request IO (the
;; cek_run_with_io kernel fix). The original strangler target (Quart blog
;; post_detail); published posts are world-visible, so this endpoint is ANONYMOUS.
;;
;; NOTE ON SPEED: content/html runs the interpreted Smalltalk-on-SX dispatch
;; (~2s for a tiny doc) because the JIT is not installed in this serving mode AND
;; currently miscompiles the Smalltalk evaluator's nested ASTs. Making the render
;; fast is a JIT-compiler fix (or a Smalltalk-interpreter optimisation), tracked
;; separately — it is NOT solved by caching the output.
;; Depends on lib/content/* (+ Smalltalk + persist preloads) + lib/dream/* +
;; lib/host/handler.sx.
;; Register content classes + render methods (idempotent); called at load below.
(define host/blog-bootstrap!
(fn () (begin (st-bootstrap-classes!) (content/bootstrap!))))
;; ── store (durable source of truth, injectable) ─────────────────────
(define host/blog-store (persist/open))
(define host/blog-use-store! (fn (b) (set! host/blog-store b)))
;; ── publish + lookup (per-request, against the store) ───────────────
;; Publish a simple post (title heading + body paragraph): append its insert ops
;; to the durable store. `at` is a caller-supplied logical timestamp.
(define host/blog-publish!
(fn (slug title body at)
(let ((hid (str slug "-h")) (tid (str slug "-body")))
(content/commit-all! host/blog-store slug
(list
(op-insert (mk-heading hid 1 title) nil)
(op-insert (mk-text tid body) hid))
at))))
;; Idempotent seed: publish only if the slug has no content yet (so a restart
;; replaying serve.sh doesn't append duplicate blocks to a persisted post).
(define host/blog-seed!
(fn (slug title body at)
(when (= (content/count (content/head host/blog-store slug)) 0)
(host/blog-publish! slug title body at))))
;; Materialise the post from the store by replaying its op-log; nil if no content.
;; Reads the durable store via per-request IO (works inside the handler thread).
(define host/blog-lookup
(fn (slug)
(let ((doc (content/head host/blog-store slug)))
(if (> (content/count doc) 0) doc nil))))
;; ── handler: GET /<slug>/ -> rendered HTML (200) or 404 ─────────────
(define host/blog-post
(fn (req)
(let ((slug (dream-param req "slug")))
(let ((doc (host/blog-lookup slug)))
(if doc
(dream-html (content/html doc))
(dream-html-status 404
(str "<!doctype html><title>Not found</title>"
"<h1>404</h1><p>No published post: " slug "</p>")))))))
;; Anonymous read route. MUST be mounted LAST: the :slug pattern matches any
;; single-segment path, so domain routes (/feed, /health) take precedence.
(define host/blog-routes
(list (dream-get "/:slug" host/blog-post)))
;; Self-bootstrap at load (content modules are loaded before this one).
(host/blog-bootstrap!)