Component names now reflect filesystem location using / as path separator and : as namespace separator for shared components: ~sx-header → ~layouts/header ~layout-app-body → ~shared:layout/app-body ~blog-admin-dashboard → ~admin/dashboard 209 files, 4,941 replacements across all services. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
111 lines
8.4 KiB
Plaintext
111 lines
8.4 KiB
Plaintext
;; ---------------------------------------------------------------------------
|
|
;; Live Streaming — SSE & WebSocket
|
|
;; ---------------------------------------------------------------------------
|
|
|
|
(defcomp ~plans/live-streaming/plan-live-streaming-content ()
|
|
(~docs/page :title "Live Streaming"
|
|
|
|
(~docs/section :title "Context" :id "context"
|
|
(p "SX streaming currently uses chunked transfer encoding: the server sends an HTML shell with "
|
|
(code "~shared:pages/suspense") " placeholders, then resolves each one via inline "
|
|
(code "<script>__sxResolve(id, sx)</script>") " chunks as IO completes. "
|
|
"Once the response finishes, the connection closes. Each slot resolves exactly once.")
|
|
(p "This is powerful for initial page load but doesn't support live updates "
|
|
"— dashboard metrics, chat messages, collaborative editing, real-time notifications. "
|
|
"For that we need a persistent transport: " (strong "SSE") " (Server-Sent Events) or " (strong "WebSockets") ".")
|
|
(p "The key insight: the client already has " (code "Sx.resolveSuspense(id, sxSource)") " which replaces "
|
|
"DOM content by suspense ID. A persistent connection just needs to keep calling it."))
|
|
|
|
(~docs/section :title "Design" :id "design"
|
|
|
|
(~docs/subsection :title "Transport Hierarchy"
|
|
(p "Three tiers, progressively more capable:")
|
|
(ol :class "list-decimal list-inside space-y-2 text-stone-700 text-sm"
|
|
(li (strong "Chunked streaming") " (done) — single HTTP response, each suspense resolves once. "
|
|
"Best for: initial page load with slow IO.")
|
|
(li (strong "SSE") " — persistent one-way connection, server pushes resolve events. "
|
|
"Best for: dashboards, notifications, progress bars, any read-only live data.")
|
|
(li (strong "WebSocket") " — bidirectional, client can send events back. "
|
|
"Best for: chat, collaborative editing, interactive applications.")))
|
|
|
|
(~docs/subsection :title "SSE Protocol"
|
|
(p "A " (code "~live") " component declares a persistent connection to an SSE endpoint:")
|
|
(~docs/code :code (highlight "(~live :src \"/api/stream/dashboard\"\n (~shared:pages/suspense :id \"cpu\" :fallback (span \"Loading...\"))\n (~shared:pages/suspense :id \"memory\" :fallback (span \"Loading...\"))\n (~shared:pages/suspense :id \"requests\" :fallback (span \"Loading...\")))" "lisp"))
|
|
(p "The server SSE endpoint yields SX resolve events:")
|
|
(~docs/code :code (highlight "async def dashboard_stream():\n while True:\n stats = await get_system_stats()\n yield sx_sse_event(\"cpu\", f'(~stat-badge :value \"{stats.cpu}%\")')\n yield sx_sse_event(\"memory\", f'(~stat-badge :value \"{stats.mem}%\")')\n await asyncio.sleep(1)" "python"))
|
|
(p "SSE wire format — each event is a suspense resolve:")
|
|
(~docs/code :code (highlight "event: sx-resolve\ndata: {\"id\": \"cpu\", \"sx\": \"(~stat-badge :value \\\"42%\\\")\"}\n\nevent: sx-resolve\ndata: {\"id\": \"memory\", \"sx\": \"(~stat-badge :value \\\"68%\\\")\"}" "text")))
|
|
|
|
(~docs/subsection :title "WebSocket Protocol"
|
|
(p "A " (code "~ws") " component establishes a bidirectional channel:")
|
|
(~docs/code :code (highlight "(~ws :src \"/ws/chat\"\n :on-message handle-chat-message\n (~shared:pages/suspense :id \"messages\" :fallback (div \"Connecting...\"))\n (~shared:pages/suspense :id \"typing\" :fallback (span)))" "lisp"))
|
|
(p "Client can send SX expressions back:")
|
|
(~docs/code :code (highlight ";; Client sends:\n(sx-send ws-conn '(chat-message :text \"hello\" :user \"alice\"))\n\n;; Server receives, broadcasts to all connected clients:\n;; event: sx-resolve for \"messages\" suspense" "lisp")))
|
|
|
|
(~docs/subsection :title "Shared Resolution Mechanism"
|
|
(p "All three transports use the same client-side resolution:")
|
|
(ul :class "list-disc list-inside space-y-1 text-stone-600 text-sm"
|
|
(li (code "Sx.resolveSuspense(id, sxSource)") " — already exists, parses SX and renders to DOM")
|
|
(li "SSE: " (code "EventSource") " → " (code "onmessage") " → " (code "resolveSuspense()"))
|
|
(li "WS: " (code "WebSocket") " → " (code "onmessage") " → " (code "resolveSuspense()"))
|
|
(li "The component env (defs needed for rendering) can be sent once on connection open")
|
|
(li "Subsequent events only need the SX expression — lightweight wire format"))))
|
|
|
|
(~docs/section :title "Implementation" :id "implementation"
|
|
|
|
(~docs/subsection :title "Phase 1: SSE Infrastructure"
|
|
(ol :class "list-decimal list-inside space-y-2 text-stone-700 text-sm"
|
|
(li "Add " (code "~live") " component to " (code "shared/sx/templates/") " — renders child suspense placeholders, "
|
|
"emits " (code "data-sx-live") " attribute with SSE endpoint URL")
|
|
(li "Add " (code "sx-live.js") " client module — on boot, finds " (code "[data-sx-live]") " elements, "
|
|
"opens EventSource, routes events to " (code "resolveSuspense()"))
|
|
(li "Add " (code "sx_sse_event(id, sx)") " helper for Python SSE endpoints — formats SSE wire protocol")
|
|
(li "Add " (code "sse_stream()") " Quart helper — returns async generator Response with correct headers")))
|
|
|
|
(~docs/subsection :title "Phase 2: Defpage Integration"
|
|
(ol :class "list-decimal list-inside space-y-2 text-stone-700 text-sm"
|
|
(li "New " (code ":live") " defpage slot — declares SSE endpoint + suspense bindings")
|
|
(li "Auto-mount SSE endpoint alongside the page route")
|
|
(li "Component defs sent as first SSE event on connection open")
|
|
(li "Automatic reconnection with exponential backoff")))
|
|
|
|
(~docs/subsection :title "Phase 3: WebSocket"
|
|
(ol :class "list-decimal list-inside space-y-2 text-stone-700 text-sm"
|
|
(li "Add " (code "~ws") " component — bidirectional channel with send/receive")
|
|
(li "Add " (code "sx-ws.js") " client module — WebSocket management, message routing")
|
|
(li "Server-side: Quart WebSocket handlers that receive and broadcast SX events")
|
|
(li "Client-side: " (code "sx-send") " primitive for sending SX expressions to server")))
|
|
|
|
(~docs/subsection :title "Phase 4: Spec & Boundary"
|
|
(ol :class "list-decimal list-inside space-y-2 text-stone-700 text-sm"
|
|
(li "Spec " (code "~live") " and " (code "~ws") " in " (code "render.sx") " (how they render in each mode)")
|
|
(li "Add SSE/WS IO primitives to " (code "boundary.sx"))
|
|
(li "Bootstrap SSE/WS connection management into " (code "sx-ref.js"))
|
|
(li "Spec-level tests for resolve, reconnection, and message routing"))))
|
|
|
|
(~docs/section :title "Files" :id "files"
|
|
(table :class "w-full text-left border-collapse"
|
|
(thead
|
|
(tr :class "border-b border-stone-200"
|
|
(th :class "px-3 py-2 font-medium text-stone-600" "File")
|
|
(th :class "px-3 py-2 font-medium text-stone-600" "Purpose")))
|
|
(tbody
|
|
(tr :class "border-b border-stone-100"
|
|
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "shared/sx/templates/live.sx")
|
|
(td :class "px-3 py-2 text-stone-700" "~live component definition"))
|
|
(tr :class "border-b border-stone-100"
|
|
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "shared/static/scripts/sx-live.js")
|
|
(td :class "px-3 py-2 text-stone-700" "SSE client — EventSource → resolveSuspense"))
|
|
(tr :class "border-b border-stone-100"
|
|
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "shared/sx/sse.py")
|
|
(td :class "px-3 py-2 text-stone-700" "SSE helpers — event formatting, stream response"))
|
|
(tr :class "border-b border-stone-100"
|
|
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "shared/static/scripts/sx-ws.js")
|
|
(td :class "px-3 py-2 text-stone-700" "WebSocket client — bidirectional SX channel"))
|
|
(tr :class "border-b border-stone-100"
|
|
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "shared/sx/ref/render.sx")
|
|
(td :class "px-3 py-2 text-stone-700" "Spec: ~live and ~ws rendering in all modes"))
|
|
(tr :class "border-b border-stone-100"
|
|
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "shared/sx/ref/boundary.sx")
|
|
(td :class "px-3 py-2 text-stone-700" "SSE/WS IO primitive declarations")))))))
|