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

@@ -2637,6 +2637,42 @@ PLATFORM_ORCHESTRATION_JS = """
}
}
function resolvePageData(pageName, params, callback) {
// Platform implementation: fetch page data via HTTP from /sx/data/ endpoint.
// The spec only knows about resolve-page-data(name, params, callback) —
// this function provides the concrete transport.
var url = "/sx/data/" + encodeURIComponent(pageName);
if (params && !isNil(params)) {
var qs = [];
var ks = Object.keys(params);
for (var i = 0; i < ks.length; i++) {
var v = params[ks[i]];
if (v !== null && v !== undefined && v !== NIL) {
qs.push(encodeURIComponent(ks[i]) + "=" + encodeURIComponent(v));
}
}
if (qs.length) url += "?" + qs.join("&");
}
var headers = { "SX-Request": "true" };
fetch(url, { headers: headers }).then(function(resp) {
if (!resp.ok) {
logWarn("sx:data resolve failed " + resp.status + " for " + pageName);
return;
}
return resp.text().then(function(text) {
try {
var exprs = parse(text);
var data = exprs.length === 1 ? exprs[0] : {};
callback(data || {});
} catch (e) {
logWarn("sx:data parse error for " + pageName + ": " + (e && e.message ? e.message : e));
}
});
}).catch(function(err) {
logWarn("sx:data resolve error for " + pageName + ": " + (err && err.message ? err.message : err));
});
}
function urlPathname(href) {
try {
return new URL(href, location.href).pathname;

View File

@@ -552,39 +552,66 @@
;; No app-specific nav update here — apps handle sx:clientRoute event.
(define swap-rendered-content
(fn (target rendered pathname)
;; Swap rendered DOM content into target and run post-processing.
;; Shared by pure and data page client routes.
(do
(dom-set-text-content target "")
(dom-append target rendered)
(hoist-head-elements-full target)
(process-elements target)
(sx-hydrate-elements target)
(dom-dispatch target "sx:clientRoute"
(dict "pathname" pathname))
(log-info (str "sx:route client " pathname)))))
(define resolve-route-target
(fn (target-sel)
;; Resolve a target selector to a DOM element, or nil.
(if (and target-sel (not (= target-sel "true")))
(dom-query target-sel)
nil)))
(define try-client-route
(fn (pathname target-sel)
;; Try to render a page client-side. Returns true if successful, false otherwise.
;; target-sel is the CSS selector for the swap target (from sx-boost value).
;; Only works for pages without :data dependencies.
;; For pure pages: renders immediately. For :data pages: fetches data then renders.
(let ((match (find-matching-route pathname _page-routes)))
(if (nil? match)
(do (log-info (str "sx:route no match (" (len _page-routes) " routes) " pathname)) false)
(if (get match "has-data")
(do (log-info (str "sx:route server (has data) " pathname)) false)
(let ((content-src (get match "content"))
(closure (or (get match "closure") {}))
(params (get match "params")))
(if (or (nil? content-src) (empty? content-src))
(do (log-warn (str "sx:route no content for " pathname)) false)
(let ((env (merge closure params))
(rendered (try-eval-content content-src env)))
(if (nil? rendered)
(do (log-info (str "sx:route server (eval failed) " pathname)) false)
(let ((target (if (and target-sel (not (= target-sel "true")))
(dom-query target-sel)
nil)))
(if (nil? target)
(do (log-warn (str "sx:route target not found: " target-sel)) false)
(let ((content-src (get match "content"))
(closure (or (get match "closure") {}))
(params (get match "params"))
(page-name (get match "name")))
(if (or (nil? content-src) (empty? content-src))
(do (log-warn (str "sx:route no content for " pathname)) false)
(let ((target (resolve-route-target target-sel)))
(if (nil? target)
(do (log-warn (str "sx:route target not found: " target-sel)) false)
(if (get match "has-data")
;; Data page: resolve data asynchronously, then render client-side
(do
(log-info (str "sx:route client+data " pathname))
(resolve-page-data page-name params
(fn (data)
;; data is a dict — merge into env and render
(let ((env (merge closure params data))
(rendered (try-eval-content content-src env)))
(if (nil? rendered)
(log-warn (str "sx:route data eval failed for " pathname))
(swap-rendered-content target rendered pathname)))))
true)
;; Pure page: render immediately
(let ((env (merge closure params))
(rendered (try-eval-content content-src env)))
(if (nil? rendered)
(do (log-info (str "sx:route server (eval failed) " pathname)) false)
(do
(dom-set-text-content target "")
(dom-append target rendered)
(hoist-head-elements-full target)
(process-elements target)
(sx-hydrate-elements target)
(dom-dispatch target "sx:clientRoute"
(dict "pathname" pathname))
(log-info (str "sx:route client " pathname))
(swap-rendered-content target rendered pathname)
true))))))))))))
@@ -893,6 +920,9 @@
;; === Client-side routing ===
;; (try-eval-content source env) → DOM node or nil (catches eval errors)
;; (url-pathname href) → extract pathname from URL string
;; (resolve-page-data name params cb) → void; resolves data for a named page.
;; Platform decides transport (HTTP, cache, IPC, etc). Calls (cb data-dict)
;; when data is available. params is a dict of URL/route parameters.
;;
;; From boot.sx:
;; _page-routes → list of route entries