host: blog persisted in durable SX store + materialised view, 158/158
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 16s
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 16s
Blog posts now live in the durable SX store (persist/durable-backend, on-disk under $SX_PERSIST_DIR — already built: sx_persist_store.ml + lib/persist/ durable.sx). Publishing appends insert ops to the slug's content stream; posts survive restarts (verified: seq/log stable across container restart, re-seed idempotent). Read path: http-listen handlers can't drive per-request perform/IO (sx_call doesn't resolve the CEK IO suspension the way the main loop does), so posts are materialised from the store into an in-memory view at boot (host/blog-load-all! + host/blog-seed!) and request handlers read the view — perform-free. Store is source of truth; view is a boot-rebuilt cache. Deploy: docker-compose.dev-sx-host.yml mounts /root/sx-host-persist (chowned to appuser 10001) at /data/persist; SX_PERSIST_DIR set. blog.rose-ash.com/welcome/ live. Per-request-IO kernel fix tracked in the plan as the next task. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1,34 +1,72 @@
|
||||
;; lib/host/blog.sx — Blog domain on the host. Serves published posts as HTML at
|
||||
;; GET /<slug>/ — the original strangler target (Quart: blog/bp/post/routes.py,
|
||||
;; handler post_detail). Published posts are world-visible, so this endpoint is
|
||||
;; ANONYMOUS — no auth, visibility is trivially "visible".
|
||||
;; 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): a post
|
||||
;; is published by appending insert ops to its stream. Serving GET /<slug>/ renders
|
||||
;; the post to HTML via content/html. The original strangler target (Quart blog
|
||||
;; post_detail); published posts are world-visible, so this endpoint is ANONYMOUS.
|
||||
;;
|
||||
;; A post is a content-on-sx document (CtDoc) rendered to HTML via the content
|
||||
;; facade (content/html). Posts live in an in-memory registry keyed by slug: this
|
||||
;; is the "prove the machinery" step — swap host/blog-lookup for a persist-backed
|
||||
;; content stream later without touching the handler or the route.
|
||||
;; Depends on lib/content/* (+ the Smalltalk + persist preloads its classes need)
|
||||
;; + lib/dream/* + lib/host/handler.sx.
|
||||
;; READ PATH — materialised view, not per-request IO. The durable backend reads
|
||||
;; via `perform` (kernel IO suspension), which is serviceable on the main thread
|
||||
;; (boot) but NOT inside an http-listen request handler thread. So posts are
|
||||
;; materialised from the store into an in-memory view at boot (and on publish),
|
||||
;; and request handlers read that view — fast, perform-free. The store stays the
|
||||
;; source of truth; the view is a cache rebuilt from it on startup.
|
||||
;; Depends on lib/content/* (+ Smalltalk + persist preloads) + lib/dream/* +
|
||||
;; lib/host/handler.sx.
|
||||
|
||||
;; Register the content class table + render methods (idempotent). Must run before
|
||||
;; any CtDoc is built/rendered; called at module load below.
|
||||
;; Register content classes + render methods (idempotent); called at load below.
|
||||
(define host/blog-bootstrap!
|
||||
(fn () (begin (st-bootstrap-classes!) (content/bootstrap!))))
|
||||
|
||||
;; ── in-memory post registry (slug -> CtDoc) ─────────────────────────
|
||||
(define host/blog-posts {})
|
||||
(define host/blog-register!
|
||||
(fn (slug doc) (set! host/blog-posts (assoc host/blog-posts slug doc))))
|
||||
(define host/blog-lookup (fn (slug) (get host/blog-posts slug)))
|
||||
(define host/blog-reset! (fn () (set! host/blog-posts {})))
|
||||
;; ── store (durable source of truth) + view (in-memory serving cache) ─
|
||||
(define host/blog-store (persist/open))
|
||||
(define host/blog-view {})
|
||||
(define host/blog-use-store!
|
||||
(fn (b) (begin (set! host/blog-store b) (set! host/blog-view {}))))
|
||||
|
||||
;; Build a simple post doc (title heading + body paragraph). Convenience for
|
||||
;; seeding and tests; real posts arrive from the content store.
|
||||
(define host/blog-make
|
||||
(fn (slug title body)
|
||||
(doc-append
|
||||
(doc-append (doc-empty slug) (mk-heading (str slug "-h") 1 title))
|
||||
(mk-text (str slug "-body") body))))
|
||||
;; content streams are keyed "content:<slug>"; recover the slug.
|
||||
(define host/blog--stream-slug
|
||||
(fn (stream)
|
||||
(if (starts-with? stream "content:") (substr stream 8) nil)))
|
||||
|
||||
;; ── publish + lookup ────────────────────────────────────────────────
|
||||
;; Publish a simple post (title heading + body paragraph): append its insert ops
|
||||
;; to the durable store, then refresh the in-memory view. `at` is a logical ts.
|
||||
(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)
|
||||
(set! host/blog-view
|
||||
(assoc host/blog-view slug (content/head host/blog-store slug))))))
|
||||
|
||||
;; Materialise every persisted post from the store into the view. Run at boot on
|
||||
;; the main thread (content/head performs IO, fine here, not in a request).
|
||||
(define host/blog-load-all!
|
||||
(fn ()
|
||||
(for-each
|
||||
(fn (stream)
|
||||
(let ((slug (host/blog--stream-slug stream)))
|
||||
(when slug
|
||||
(let ((doc (content/head host/blog-store slug)))
|
||||
(when (> (content/count doc) 0)
|
||||
(set! host/blog-view (assoc host/blog-view slug doc)))))))
|
||||
(persist/backend-streams host/blog-store))))
|
||||
|
||||
;; Idempotent seed: if the slug isn't already materialised, recover it from the
|
||||
;; store (prior run) or publish it fresh. No duplicate ops on restart.
|
||||
(define host/blog-seed!
|
||||
(fn (slug title body at)
|
||||
(when (nil? (get host/blog-view slug))
|
||||
(let ((existing (content/head host/blog-store slug)))
|
||||
(if (> (content/count existing) 0)
|
||||
(set! host/blog-view (assoc host/blog-view slug existing))
|
||||
(host/blog-publish! slug title body at))))))
|
||||
|
||||
;; Lookup is pure in-memory (no perform) — safe inside a request handler.
|
||||
(define host/blog-lookup (fn (slug) (get host/blog-view slug)))
|
||||
|
||||
;; ── handler: GET /<slug>/ -> rendered HTML (200) or 404 ─────────────
|
||||
(define host/blog-post
|
||||
|
||||
Reference in New Issue
Block a user