host: durable lazy sessions — logins survive a restart

Sessions were in-memory, so a restart logged everyone out (same class as the
relation wipe). Move them to the durable store, but LAZILY so anonymous/crawler
traffic doesn't spam it: session/create mints a sid with no row; the row appears
on the first session/set (a login). A per-boot epoch (one durable write at
startup, host/session-init!) keeps sids unique across restarts without a write
per request.

- lib/host/session.sx: lazy backend (create = no row, set = create row,
  exists = row written) + epoch/in-memory-counter sid generation.
- serve.sh: point the session store at the durable backend + host/session-init!.
- blog.sx: host/current-principal is now a durable read, so host/auth-footer
  (home + post footers) had to move OUT of the quasiquote into let bindings —
  a perform during page-tree build raises VmSuspended (the whole site 500'd for
  a beat). Principal computed once per page.
- 2 session tests: create writes no row, set creates the row.

249/249. Verified live: site renders (anon + authed), login + footer survive a
container force-recreate.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-28 16:37:26 +00:00
parent 71dd040d80
commit b0b0a0592b
4 changed files with 73 additions and 38 deletions

View File

@@ -326,26 +326,28 @@
(let ((slug (dream-param req "slug")))
(let ((r (host/blog-get slug)))
(if r
;; Compute the rendered body + related block in let bindings BEFORE the
;; quasiquote — host/blog--related-block does durable reads, and IO must
;; happen in the handler body, not while the page tree is being built.
(let ((body-html (host/blog-render r))
(related-block (host/blog--related-block slug
(not (nil? (host/current-principal req))))))
(dream-html
(host/blog--page (get r :title)
(quasiquote
(div
(article (raw! (unquote body-html)))
(unquote related-block)
(p :style "margin-top:2em;font-size:0.9em;opacity:0.8"
(a :href (unquote (str "/" slug "/source")) "view source")
" · "
(a :href (unquote (str "/" slug "/edit")) "edit")
" · "
(a :href "/" "all posts")
" · "
(unquote (host/auth-footer req))))))))
;; Compute everything that does durable reads — body, related block, AND
;; the auth footer (a durable session read now) — in let bindings BEFORE
;; the quasiquote. IO must run in the handler body, never while the page
;; tree is built (a perform there raises VmSuspended under http-listen).
(let ((principal (host/current-principal req)))
(let ((body-html (host/blog-render r))
(related-block (host/blog--related-block slug (not (nil? principal))))
(auth-foot (host/auth-footer req)))
(dream-html
(host/blog--page (get r :title)
(quasiquote
(div
(article (raw! (unquote body-html)))
(unquote related-block)
(p :style "margin-top:2em;font-size:0.9em;opacity:0.8"
(a :href (unquote (str "/" slug "/source")) "view source")
" · "
(a :href (unquote (str "/" slug "/edit")) "edit")
" · "
(a :href "/" "all posts")
" · "
(unquote auth-foot))))))))
(dream-html-status 404
(host/blog--page "Not found"
(quasiquote
@@ -364,7 +366,10 @@
posts)))
(let ((listing (if (> (len posts) 0)
(list (quote ul) items)
(quote (p "No posts yet.")))))
(quote (p "No posts yet."))))
;; auth-footer does a durable session read — bind it BEFORE the
;; quasiquote (a perform during tree-build raises VmSuspended).
(auth-foot (host/auth-footer req)))
(dream-html
(host/blog--page "Blog"
(quasiquote
@@ -372,7 +377,7 @@
(unquote listing)
(p (a :href "/new" "+ New post"))
(p :style "margin-top:2em;font-size:0.9em;opacity:0.8"
(unquote (host/auth-footer req))))))))))))
(unquote auth-foot)))))))))))
(define host/blog-index (fn (req) (host/ok (host/blog-list))))