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:
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user