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:
@@ -2,30 +2,57 @@
|
||||
;;
|
||||
;; This page uses :stream true to enable chunked transfer encoding.
|
||||
;; The browser receives the HTML shell immediately with loading skeletons,
|
||||
;; then the content fills in when the (deliberately slow) data resolves.
|
||||
;; then content fills in as each IO resolves at staggered intervals.
|
||||
;;
|
||||
;; The :data expression simulates 1.5s IO delay. Without streaming, the
|
||||
;; browser would wait the full 1.5s before seeing anything. With streaming,
|
||||
;; the page skeleton appears instantly.
|
||||
;; The :data expression is an async generator that yields three chunks
|
||||
;; at 1s, 3s, and 5s. Each chunk resolves a different ~suspense slot.
|
||||
|
||||
(defcomp ~streaming-demo-content (&key streamed-at message items)
|
||||
;; Color map for stream chunk styling (all string keys for get compatibility)
|
||||
(define stream-colors
|
||||
{"green" {"border" "border-green-200" "bg" "bg-green-50" "title" "text-green-900"
|
||||
"text" "text-green-800" "sub" "text-green-700" "code" "bg-green-100"
|
||||
"dot" "bg-green-400"}
|
||||
"blue" {"border" "border-blue-200" "bg" "bg-blue-50" "title" "text-blue-900"
|
||||
"text" "text-blue-800" "sub" "text-blue-700" "code" "bg-blue-100"
|
||||
"dot" "bg-blue-400"}
|
||||
"amber" {"border" "border-amber-200" "bg" "bg-amber-50" "title" "text-amber-900"
|
||||
"text" "text-amber-800" "sub" "text-amber-700" "code" "bg-amber-100"
|
||||
"dot" "bg-amber-400"}})
|
||||
|
||||
;; Generic streamed content chunk — rendered once per yield from the
|
||||
;; async generator. The :content expression receives different bindings
|
||||
;; each time, and the _stream_id determines which ~suspense slot it fills.
|
||||
(defcomp ~streaming-demo-chunk (&key stream-label stream-color stream-message stream-time)
|
||||
(let ((colors (get stream-colors stream-color)))
|
||||
(div :class (str "rounded-lg border p-5 space-y-3 " (get colors "border") " " (get colors "bg"))
|
||||
(div :class "flex items-center gap-2"
|
||||
(div :class (str "w-3 h-3 rounded-full " (get colors "dot")))
|
||||
(h2 :class (str "text-lg font-semibold " (get colors "title")) stream-label))
|
||||
(p :class (get colors "text") stream-message)
|
||||
(p :class (str "text-sm " (get colors "sub"))
|
||||
"Resolved at: " (code :class (str "px-1 rounded " (get colors "code")) stream-time)))))
|
||||
|
||||
;; Skeleton placeholder for a stream slot
|
||||
(defcomp ~stream-skeleton ()
|
||||
(div :class "rounded-lg border border-stone-200 bg-stone-50 p-5 space-y-3 animate-pulse"
|
||||
(div :class "flex items-center gap-2"
|
||||
(div :class "w-3 h-3 rounded-full bg-stone-300")
|
||||
(div :class "h-6 bg-stone-200 rounded w-1/3"))
|
||||
(div :class "h-4 bg-stone-200 rounded w-2/3")
|
||||
(div :class "h-4 bg-stone-200 rounded w-1/2")))
|
||||
|
||||
;; Static layout — takes &rest children where the three suspense slots go.
|
||||
(defcomp ~streaming-demo-layout (&rest children)
|
||||
(div :class "space-y-8"
|
||||
(div :class "border-b border-stone-200 pb-6"
|
||||
(h1 :class "text-2xl font-bold text-stone-900" "Streaming & Suspense Demo")
|
||||
(p :class "mt-2 text-stone-600"
|
||||
"This page uses " (code :class "bg-stone-100 px-1 rounded text-violet-700" ":stream true")
|
||||
" in its defpage declaration. The browser receives the page skeleton instantly, "
|
||||
"then content fills in as IO resolves."))
|
||||
"then three IO sources resolve at staggered intervals (1s, 3s, 5s)."))
|
||||
|
||||
;; Timestamp proves this was streamed
|
||||
(div :class "rounded-lg border border-green-200 bg-green-50 p-5 space-y-3"
|
||||
(h2 :class "text-lg font-semibold text-green-900" "Streamed Content")
|
||||
(p :class "text-green-800" message)
|
||||
(p :class "text-green-700 text-sm"
|
||||
"Data resolved at: " (code :class "bg-green-100 px-1 rounded" streamed-at))
|
||||
(p :class "text-green-700 text-sm"
|
||||
"This content arrived via a " (code :class "bg-green-100 px-1 rounded" "<script>__sxResolve(...)</script>")
|
||||
" chunk streamed after the initial HTML shell."))
|
||||
;; Slot: suspense placeholders (or resolved content)
|
||||
(div :class "grid gap-4" children)
|
||||
|
||||
;; Flow diagram
|
||||
(div :class "space-y-4"
|
||||
@@ -36,25 +63,31 @@
|
||||
(div :class "flex-shrink-0 w-8 h-8 rounded-full bg-violet-100 flex items-center justify-center text-violet-700 font-bold text-sm"
|
||||
(get item "label"))
|
||||
(p :class "text-stone-700 text-sm pt-1" (get item "detail"))))
|
||||
items)))
|
||||
(list
|
||||
{:label "Shell" :detail "HTML shell with three suspense placeholders sent immediately"}
|
||||
{:label "Boot" :detail "sx-browser.js loads, renders fallback skeletons"}
|
||||
{:label "1s" :detail "Fast API responds — first skeleton replaced with green box"}
|
||||
{:label "3s" :detail "Database query completes — second skeleton replaced with blue box"}
|
||||
{:label "5s" :detail "ML inference finishes — third skeleton replaced with amber box"}))))
|
||||
|
||||
;; How it works
|
||||
(div :class "rounded-lg border border-blue-200 bg-blue-50 p-5 space-y-3"
|
||||
(h2 :class "text-lg font-semibold text-blue-900" "How Streaming Works")
|
||||
(ol :class "list-decimal list-inside text-blue-800 space-y-2 text-sm"
|
||||
(li "Server starts data fetch and header fetch " (em "concurrently"))
|
||||
(li "HTML shell with " (code "~suspense") " placeholders is sent immediately")
|
||||
(li "Browser loads sx-browser.js, renders the page with loading skeletons")
|
||||
(li "Data IO completes — server sends " (code "<script>__sxResolve(\"stream-content\", ...)</script>"))
|
||||
(li "sx.js calls " (code "Sx.resolveSuspense()") " — replaces skeleton with real content")
|
||||
(li "Header IO completes — same process for header area")))
|
||||
(div :class "rounded-lg border border-violet-200 bg-violet-50 p-5 space-y-3"
|
||||
(h2 :class "text-lg font-semibold text-violet-900" "How Multi-Stream Works")
|
||||
(ol :class "list-decimal list-inside text-violet-800 space-y-2 text-sm"
|
||||
(li "Server evaluates " (code ":data") " — gets an " (em "async generator"))
|
||||
(li "HTML shell with three " (code "~suspense") " placeholders sent immediately")
|
||||
(li "Generator yields first chunk after 1s — server sends " (code "__sxResolve(\"stream-fast\", ...)"))
|
||||
(li "Generator yields second chunk after 3s — " (code "__sxResolve(\"stream-medium\", ...)"))
|
||||
(li "Generator yields third chunk after 5s — " (code "__sxResolve(\"stream-slow\", ...)"))
|
||||
(li "Each resolve replaces its skeleton independently")))
|
||||
|
||||
;; Technical details
|
||||
(div :class "rounded-lg border border-amber-200 bg-amber-50 p-4 text-sm space-y-2"
|
||||
(p :class "font-semibold text-amber-800" "Implementation details")
|
||||
(ul :class "list-disc list-inside text-amber-700 space-y-1"
|
||||
(li (code "defpage :stream true") " — opts the page into streaming response")
|
||||
(li (code "~suspense :id \"...\" :fallback (...)") " — renders loading skeleton until resolved")
|
||||
(li "Quart async generator response — yields chunks as they become available")
|
||||
(li "Resolution via " (code "__sxResolve(id, sx)") " inline scripts in the stream")
|
||||
(li "Falls back to standard (non-streaming) response for SX/HTMX requests")))))
|
||||
(div :class "rounded-lg border border-stone-200 bg-stone-50 p-4 text-sm space-y-2"
|
||||
(p :class "font-semibold text-stone-800" "Implementation details")
|
||||
(ul :class "list-disc list-inside text-stone-600 space-y-1"
|
||||
(li (code "defpage :stream true") " — opts the page into chunked transfer encoding")
|
||||
(li (code ":data") " helper is an async generator — each " (code "yield") " resolves a different suspense slot")
|
||||
(li "Each yield includes " (code "_stream_id") " matching a " (code "~suspense :id") " in the shell")
|
||||
(li (code ":content") " expression is re-evaluated with each yield's bindings")
|
||||
(li "Headers stream concurrently — independent of the data generator")
|
||||
(li "Future: SSE/WebSocket for re-resolving slots after initial page load")))))
|
||||
|
||||
Reference in New Issue
Block a user