Phase 4: Client-side rendering of :data pages via abstract resolve-page-data

Spec layer (orchestration.sx):
- try-client-route now handles :data pages instead of falling back to server
- New abstract primitive resolve-page-data(name, params, callback) — platform
  decides transport (HTTP, IPC, cache, etc)
- Extracted swap-rendered-content and resolve-route-target helpers

Platform layer (bootstrap_js.py):
- resolvePageData() browser implementation: fetches /sx/data/<name>, parses
  SX response, calls callback. Other hosts provide their own transport.

Server layer (pages.py):
- evaluate_page_data() evaluates :data expr, serializes result as SX
- auto_mount_page_data() mounts /sx/data/ endpoint with per-page auth
- _build_pages_sx now computes component deps for all pages (not just pure)

Test page at /isomorphism/data-test exercises the full pipeline.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-06 23:46:30 +00:00
parent 9d0cffb84d
commit a657d0831c
10 changed files with 375 additions and 108 deletions

View File

@@ -54,3 +54,8 @@
:params ()
:returns "dict"
:service "sx")
(define-page-helper "data-test-data"
:params ()
:returns "dict"
:service "sx")

52
sx/sx/data-test.sx Normal file
View File

@@ -0,0 +1,52 @@
;; Data test page — exercises Phase 4 client-side data rendering.
;;
;; This page has a :data expression. When navigated to:
;; - Full page load: server evaluates data + renders content (normal path)
;; - Client route: client fetches /sx/data/data-test, parses SX, renders locally
;;
;; Open browser console and look for "sx:route client+data" to confirm
;; client-side rendering happened.
(defcomp ~data-test-content (&key server-time items phase transport)
(div :class "space-y-8"
(div :class "border-b border-stone-200 pb-6"
(h1 :class "text-2xl font-bold text-stone-900" "Data Test")
(p :class "mt-2 text-stone-600"
"This page tests the Phase 4 data endpoint. The content you see was "
"rendered using data from the server, but the rendering itself may have "
"happened client-side."))
;; Server-provided metadata
(div :class "rounded-lg border border-stone-200 bg-white p-6 space-y-3"
(h2 :class "text-lg font-semibold text-stone-800" "Data from server")
(dl :class "grid grid-cols-2 gap-2 text-sm"
(dt :class "font-medium text-stone-600" "Phase")
(dd :class "text-stone-900" phase)
(dt :class "font-medium text-stone-600" "Transport")
(dd :class "text-stone-900" transport)
(dt :class "font-medium text-stone-600" "Server time")
(dd :class "font-mono text-stone-900" server-time)))
;; Pipeline steps from data
(div :class "space-y-3"
(h2 :class "text-lg font-semibold text-stone-800" "Pipeline steps")
(div :class "space-y-2"
(map-indexed
(fn (i item)
(div :class "flex items-start gap-3 rounded border border-stone-100 bg-white p-3"
(span :class "flex-none rounded-full bg-violet-100 text-violet-700 w-6 h-6 flex items-center justify-center text-xs font-bold"
(str (+ i 1)))
(div
(div :class "font-medium text-stone-900" (get item "label"))
(div :class "text-sm text-stone-500" (get item "detail")))))
items)))
;; How to verify
(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" "How to verify client-side rendering")
(ol :class "list-decimal list-inside text-amber-700 space-y-1"
(li "Open the browser console (F12)")
(li "Navigate to this page from another page using a link")
(li "Look for: " (code :class "bg-amber-100 px-1 rounded" "sx:route client+data /isomorphism/data-test"))
(li "That log line means the data was fetched and rendered client-side")
(li "A full page reload will show server-side rendering instead")))))

View File

@@ -106,7 +106,8 @@
(define isomorphism-nav-items (list
(dict :label "Roadmap" :href "/isomorphism/")
(dict :label "Bundle Analyzer" :href "/isomorphism/bundle-analyzer")
(dict :label "Routing Analyzer" :href "/isomorphism/routing-analyzer")))
(dict :label "Routing Analyzer" :href "/isomorphism/routing-analyzer")
(dict :label "Data Test" :href "/isomorphism/data-test")))
(define plans-nav-items (list
(dict :label "Reader Macros" :href "/plans/reader-macros"

View File

@@ -449,6 +449,20 @@
:pages pages :total-pages total-pages :client-count client-count
:server-count server-count :registry-sample registry-sample))
(defpage data-test
:path "/isomorphism/data-test"
:auth :public
:layout (:sx-section
:section "Isomorphism"
:sub-label "Isomorphism"
:sub-href "/isomorphism/"
:sub-nav (~section-nav :items isomorphism-nav-items :current "Data Test")
:selected "Data Test")
:data (data-test-data)
:content (~data-test-content
:server-time server-time :items items
:phase phase :transport transport))
;; ---------------------------------------------------------------------------
;; Plans section
;; ---------------------------------------------------------------------------

View File

@@ -23,6 +23,7 @@ def _register_sx_helpers() -> None:
"bootstrapper-data": _bootstrapper_data,
"bundle-analyzer-data": _bundle_analyzer_data,
"routing-analyzer-data": _routing_analyzer_data,
"data-test-data": _data_test_data,
})
@@ -488,3 +489,26 @@ def _event_detail_data(slug: str) -> dict:
"event-example": detail.get("example"),
"event-demo": sx_call(demo_name) if demo_name else None,
}
def _data_test_data() -> dict:
"""Return test data for the client-side data rendering test page.
This exercises the Phase 4 data endpoint: server evaluates this
helper, serializes the result as SX, the client fetches and parses
it, then renders the page content with these bindings.
"""
from datetime import datetime, timezone
return {
"server-time": datetime.now(timezone.utc).isoformat(timespec="seconds"),
"items": [
{"label": "Eval", "detail": "Server evaluates :data expression"},
{"label": "Serialize", "detail": "Result serialized as SX wire format"},
{"label": "Fetch", "detail": "Client calls resolve-page-data"},
{"label": "Parse", "detail": "Client parses SX response to dict"},
{"label": "Render", "detail": "Client merges data into env, renders content"},
],
"phase": "Phase 4 — Client Async & IO Bridge",
"transport": "SX wire format (text/sx)",
}