Restore stashed WIP: live streaming plan, forms, CI pipeline, streaming demo

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-07 22:07:59 +00:00
parent df1aa4e1d1
commit 5a68046bd8
21 changed files with 1463 additions and 120 deletions

View File

@@ -116,3 +116,114 @@
(let ((pdef (make-page-def name slots env)))
(env-set! env (str "page:" name) pdef)
pdef))))
;; ==========================================================================
;; Page Execution Semantics
;; ==========================================================================
;;
;; A PageDef describes what to render for a route. The host evaluates slots
;; at request time. This section specifies the data → content protocol that
;; every host must implement identically.
;;
;; Slots (all unevaluated AST):
;; :path — route pattern (string)
;; :auth — "public" | "login" | "admin"
;; :layout — layout reference + kwargs
;; :stream — boolean, opt into chunked transfer
;; :shell — immediate content (contains ~suspense placeholders)
;; :fallback — loading skeleton for single-stream mode
;; :data — IO expression producing bindings
;; :content — template expression evaluated with data bindings
;; :filter, :aside, :menu — additional content slots
;;
;; --------------------------------------------------------------------------
;; Data Protocol
;; --------------------------------------------------------------------------
;;
;; The :data expression is evaluated at request time. It returns one of:
;;
;; 1. A dict — single-stream mode (default).
;; Each key becomes an env binding (underscores → hyphens).
;; Then :content is evaluated once with those bindings.
;; Result resolves the "stream-content" suspense slot.
;;
;; 2. A sequence of dicts — multi-stream mode.
;; The host delivers items over time (async generator, channel, etc.).
;; Each dict:
;; - MUST contain "stream-id" → string matching a ~suspense :id
;; - Remaining keys become env bindings (underscores → hyphens)
;; - :content is re-evaluated with those bindings
;; - Result resolves the ~suspense slot matching "stream-id"
;; If "stream-id" is absent, defaults to "stream-content".
;;
;; The host is free to choose the timing mechanism:
;; Python — async generator (yield dicts at intervals)
;; Go — channel of dicts
;; Haskell — conduit / streaming
;; JS — async iterator
;;
;; The spec requires:
;; (a) Each item's bindings are isolated (fresh env per item)
;; (b) :content is evaluated independently for each item
;; (c) Resolution is incremental — each item resolves as it arrives
;; (d) "stream-id" routes to the correct ~suspense slot
;;
;; --------------------------------------------------------------------------
;; Streaming Execution Order
;; --------------------------------------------------------------------------
;;
;; When :stream is true:
;;
;; 1. Evaluate :shell (if present) → HTML for immediate content slot
;; :shell typically contains ~suspense placeholders with :fallback
;; 2. Render HTML shell with suspense placeholders → send to client
;; 3. Start :data evaluation concurrently with header resolution
;; 4. As each data item arrives:
;; a. Bind item keys into fresh env
;; b. Evaluate :content with those bindings → SX wire format
;; c. Send resolve script: __sxResolve(stream-id, sx)
;; 5. Close response when all items + headers have resolved
;;
;; Non-streaming pages evaluate :data then :content sequentially and
;; return the complete page in a single response.
;;
;; --------------------------------------------------------------------------
;; Spec helpers for multi-stream data protocol
;; --------------------------------------------------------------------------
;; Extract stream-id from a data chunk dict, defaulting to "stream-content"
(define stream-chunk-id
(fn (chunk)
(if (has-key? chunk "stream-id")
(get chunk "stream-id")
"stream-content")))
;; Remove stream-id from chunk, returning only the bindings
(define stream-chunk-bindings
(fn (chunk)
(dissoc chunk "stream-id")))
;; Normalize binding keys: underscore → hyphen
(define normalize-binding-key
(fn (key)
(replace key "_" "-")))
;; Bind a data chunk's keys into a fresh env (isolated per chunk)
(define bind-stream-chunk
(fn (chunk base-env)
(let ((env (merge {} base-env))
(bindings (stream-chunk-bindings chunk)))
(for-each
(fn (key)
(env-set! env (normalize-binding-key key)
(get bindings key)))
(keys bindings))
env)))
;; Validate a multi-stream data result: must be a list of dicts
(define validate-stream-data
(fn (data)
(and (= (type-of data) "list")
(every? (fn (item) (= (type-of item) "dict")) data))))