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:
@@ -14,7 +14,7 @@
|
||||
// =========================================================================
|
||||
|
||||
var NIL = Object.freeze({ _nil: true, toString: function() { return "nil"; } });
|
||||
var SX_VERSION = "2026-03-06T23:02:44Z";
|
||||
var SX_VERSION = "2026-03-06T23:36:38Z";
|
||||
|
||||
function isNil(x) { return x === NIL || x === null || x === undefined; }
|
||||
function isSxTruthy(x) { return x !== false && !isNil(x); }
|
||||
@@ -1930,22 +1930,33 @@ return domAppendToHead(link); }, domQueryAll(container, "link[rel=\"stylesheet\"
|
||||
})()) : NIL); }, domQueryAll(container, "form"));
|
||||
})(); };
|
||||
|
||||
// swap-rendered-content
|
||||
var swapRenderedContent = function(target, rendered, pathname) { return (domSetTextContent(target, ""), domAppend(target, rendered), hoistHeadElementsFull(target), processElements(target), sxHydrateElements(target), domDispatch(target, "sx:clientRoute", {["pathname"]: pathname}), logInfo((String("sx:route client ") + String(pathname)))); };
|
||||
|
||||
// resolve-route-target
|
||||
var resolveRouteTarget = function(targetSel) { return (isSxTruthy((isSxTruthy(targetSel) && !isSxTruthy((targetSel == "true")))) ? domQuery(targetSel) : NIL); };
|
||||
|
||||
// try-client-route
|
||||
var tryClientRoute = function(pathname, targetSel) { return (function() {
|
||||
var match = findMatchingRoute(pathname, _pageRoutes);
|
||||
return (isSxTruthy(isNil(match)) ? (logInfo((String("sx:route no match (") + String(len(_pageRoutes)) + String(" routes) ") + String(pathname))), false) : (isSxTruthy(get(match, "has-data")) ? (logInfo((String("sx:route server (has data) ") + String(pathname))), false) : (function() {
|
||||
return (isSxTruthy(isNil(match)) ? (logInfo((String("sx:route no match (") + String(len(_pageRoutes)) + String(" routes) ") + String(pathname))), false) : (function() {
|
||||
var contentSrc = get(match, "content");
|
||||
var closure = sxOr(get(match, "closure"), {});
|
||||
var params = get(match, "params");
|
||||
var pageName = get(match, "name");
|
||||
return (isSxTruthy(sxOr(isNil(contentSrc), isEmpty(contentSrc))) ? (logWarn((String("sx:route no content for ") + String(pathname))), false) : (function() {
|
||||
var target = resolveRouteTarget(targetSel);
|
||||
return (isSxTruthy(isNil(target)) ? (logWarn((String("sx:route target not found: ") + String(targetSel))), false) : (isSxTruthy(get(match, "has-data")) ? (logInfo((String("sx:route client+data ") + String(pathname))), resolvePageData(pageName, params, function(data) { return (function() {
|
||||
var env = merge(closure, params, data);
|
||||
var rendered = tryEvalContent(contentSrc, env);
|
||||
return (isSxTruthy(isNil(rendered)) ? logWarn((String("sx:route data eval failed for ") + String(pathname))) : swapRenderedContent(target, rendered, pathname));
|
||||
})(); }), true) : (function() {
|
||||
var env = merge(closure, params);
|
||||
var rendered = tryEvalContent(contentSrc, env);
|
||||
return (isSxTruthy(isNil(rendered)) ? (logInfo((String("sx:route server (eval failed) ") + String(pathname))), false) : (function() {
|
||||
var target = (isSxTruthy((isSxTruthy(targetSel) && !isSxTruthy((targetSel == "true")))) ? domQuery(targetSel) : NIL);
|
||||
return (isSxTruthy(isNil(target)) ? (logWarn((String("sx:route target not found: ") + String(targetSel))), false) : (domSetTextContent(target, ""), domAppend(target, rendered), hoistHeadElementsFull(target), processElements(target), sxHydrateElements(target), domDispatch(target, "sx:clientRoute", {["pathname"]: pathname}), logInfo((String("sx:route client ") + String(pathname))), true));
|
||||
})());
|
||||
})());
|
||||
return (isSxTruthy(isNil(rendered)) ? (logInfo((String("sx:route server (eval failed) ") + String(pathname))), false) : (swapRenderedContent(target, rendered, pathname), true));
|
||||
})()));
|
||||
})());
|
||||
})());
|
||||
})(); };
|
||||
|
||||
// bind-client-route-link
|
||||
@@ -2368,76 +2379,6 @@ callExpr.push(dictGet(kwargs, k)); } }
|
||||
var bootInit = function() { return (logInfo((String("sx-browser ") + String(SX_VERSION))), initCssTracking(), initStyleDict(), processPageScripts(), processSxScripts(NIL), sxHydrateElements(NIL), processElements(NIL)); };
|
||||
|
||||
|
||||
// === Transpiled from router (client-side route matching) ===
|
||||
|
||||
// split-path-segments
|
||||
var splitPathSegments = function(path) { return (function() {
|
||||
var trimmed = (isSxTruthy(startsWith(path, "/")) ? slice(path, 1) : path);
|
||||
return (function() {
|
||||
var trimmed2 = (isSxTruthy((isSxTruthy(!isSxTruthy(isEmpty(trimmed))) && endsWith(trimmed, "/"))) ? slice(trimmed, 0, (len(trimmed) - 1)) : trimmed);
|
||||
return (isSxTruthy(isEmpty(trimmed2)) ? [] : split(trimmed2, "/"));
|
||||
})();
|
||||
})(); };
|
||||
|
||||
// make-route-segment
|
||||
var makeRouteSegment = function(seg) { return (isSxTruthy((isSxTruthy(startsWith(seg, "<")) && endsWith(seg, ">"))) ? (function() {
|
||||
var paramName = slice(seg, 1, (len(seg) - 1));
|
||||
return (function() {
|
||||
var d = {};
|
||||
d["type"] = "param";
|
||||
d["value"] = paramName;
|
||||
return d;
|
||||
})();
|
||||
})() : (function() {
|
||||
var d = {};
|
||||
d["type"] = "literal";
|
||||
d["value"] = seg;
|
||||
return d;
|
||||
})()); };
|
||||
|
||||
// parse-route-pattern
|
||||
var parseRoutePattern = function(pattern) { return (function() {
|
||||
var segments = splitPathSegments(pattern);
|
||||
return map(makeRouteSegment, segments);
|
||||
})(); };
|
||||
|
||||
// match-route-segments
|
||||
var matchRouteSegments = function(pathSegs, parsedSegs) { return (isSxTruthy(!isSxTruthy((len(pathSegs) == len(parsedSegs)))) ? NIL : (function() {
|
||||
var params = {};
|
||||
var matched = true;
|
||||
forEachIndexed(function(i, parsedSeg) { return (isSxTruthy(matched) ? (function() {
|
||||
var pathSeg = nth(pathSegs, i);
|
||||
var segType = get(parsedSeg, "type");
|
||||
return (isSxTruthy((segType == "literal")) ? (isSxTruthy(!isSxTruthy((pathSeg == get(parsedSeg, "value")))) ? (matched = false) : NIL) : (isSxTruthy((segType == "param")) ? dictSet(params, get(parsedSeg, "value"), pathSeg) : (matched = false)));
|
||||
})() : NIL); }, parsedSegs);
|
||||
return (isSxTruthy(matched) ? params : NIL);
|
||||
})()); };
|
||||
|
||||
// match-route
|
||||
var matchRoute = function(path, pattern) { return (function() {
|
||||
var pathSegs = splitPathSegments(path);
|
||||
var parsedSegs = parseRoutePattern(pattern);
|
||||
return matchRouteSegments(pathSegs, parsedSegs);
|
||||
})(); };
|
||||
|
||||
// find-matching-route
|
||||
var findMatchingRoute = function(path, routes) { return (function() {
|
||||
var pathSegs = splitPathSegments(path);
|
||||
var result = NIL;
|
||||
{ var _c = routes; for (var _i = 0; _i < _c.length; _i++) { var route = _c[_i]; if (isSxTruthy(isNil(result))) {
|
||||
(function() {
|
||||
var params = matchRouteSegments(pathSegs, get(route, "parsed"));
|
||||
return (isSxTruthy(!isSxTruthy(isNil(params))) ? (function() {
|
||||
var matched = merge(route, {});
|
||||
matched["params"] = params;
|
||||
return (result = matched);
|
||||
})() : NIL);
|
||||
})();
|
||||
} } }
|
||||
return result;
|
||||
})(); };
|
||||
|
||||
|
||||
// =========================================================================
|
||||
// Platform interface — DOM adapter (browser-only)
|
||||
// =========================================================================
|
||||
@@ -3116,6 +3057,42 @@ callExpr.push(dictGet(kwargs, k)); } }
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -3577,10 +3554,6 @@ callExpr.push(dictGet(kwargs, k)); } }
|
||||
renderComponent: typeof sxRenderComponent === "function" ? sxRenderComponent : null,
|
||||
getEnv: function() { return componentEnv; },
|
||||
init: typeof bootInit === "function" ? bootInit : null,
|
||||
splitPathSegments: splitPathSegments,
|
||||
parseRoutePattern: parseRoutePattern,
|
||||
matchRoute: matchRoute,
|
||||
findMatchingRoute: findMatchingRoute,
|
||||
_version: "ref-2.0 (boot+cssx+dom+engine+html+orchestration+parser+sx, bootstrap-compiled)"
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user