Phase 5: async IO rendering — components call IO primitives client-side
Wire async rendering into client-side routing: pages whose component trees reference IO primitives (highlight, current-user, etc.) now render client-side via Promise-aware asyncRenderToDom. IO calls proxy through /sx/io/<name> endpoint, which falls back to page helpers. - Add has-io flag to page registry entries (helpers.py) - Remove IO purity filter — include IO-dependent components in bundles - Extend try-client-route with 4 paths: pure, data, IO, data+IO - Convert tryAsyncEvalContent to callback style, add platform mapping - IO proxy falls back to page helpers (highlight works via proxy) - Demo page: /isomorphism/async-io with inline highlight calls Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -344,6 +344,7 @@
|
||||
(log-info (str "sx-browser " SX_VERSION))
|
||||
(init-css-tracking)
|
||||
(init-style-dict)
|
||||
(init-io-primitives)
|
||||
(process-page-scripts)
|
||||
(process-sx-scripts nil)
|
||||
(sx-hydrate-elements nil)
|
||||
|
||||
@@ -384,6 +384,7 @@ class JSEmitter:
|
||||
"bind-client-route-click": "bindClientRouteClick",
|
||||
"try-client-route": "tryClientRoute",
|
||||
"try-eval-content": "tryEvalContent",
|
||||
"try-async-eval-content": "tryAsyncEvalContent",
|
||||
"url-pathname": "urlPathname",
|
||||
"bind-inline-handler": "bindInlineHandler",
|
||||
"bind-preload": "bindPreload",
|
||||
@@ -476,6 +477,7 @@ class JSEmitter:
|
||||
"process-sx-scripts": "processSxScripts",
|
||||
"process-component-script": "processComponentScript",
|
||||
"init-style-dict": "initStyleDict",
|
||||
"init-io-primitives": "initIoPrimitives",
|
||||
"SX_VERSION": "SX_VERSION",
|
||||
"boot-init": "bootInit",
|
||||
"resolve-mount-target": "resolveMountTarget",
|
||||
@@ -1144,6 +1146,607 @@ CONTINUATIONS_JS = '''
|
||||
'''
|
||||
|
||||
|
||||
ASYNC_IO_JS = '''
|
||||
// =========================================================================
|
||||
// Async IO: Promise-aware rendering for client-side IO primitives
|
||||
// =========================================================================
|
||||
//
|
||||
// IO primitives (query, current-user, etc.) return Promises on the client.
|
||||
// asyncRenderToDom walks the component tree; when it encounters an IO
|
||||
// primitive, it awaits the Promise and continues rendering.
|
||||
//
|
||||
// The sync evaluator/renderer is untouched. This is a separate async path
|
||||
// used only when a page's component tree contains IO references.
|
||||
|
||||
var IO_PRIMITIVES = {};
|
||||
|
||||
function registerIoPrimitive(name, fn) {
|
||||
IO_PRIMITIVES[name] = fn;
|
||||
}
|
||||
|
||||
function isPromise(x) {
|
||||
return x != null && typeof x === "object" && typeof x.then === "function";
|
||||
}
|
||||
|
||||
// Async trampoline: resolves thunks, awaits Promises
|
||||
function asyncTrampoline(val) {
|
||||
if (isPromise(val)) return val.then(asyncTrampoline);
|
||||
if (isThunk(val)) return asyncTrampoline(evalExpr(thunkExpr(val), thunkEnv(val)));
|
||||
return val;
|
||||
}
|
||||
|
||||
// Async eval: like trampoline(evalExpr(...)) but handles IO primitives
|
||||
function asyncEval(expr, env) {
|
||||
// Intercept IO primitive calls at the AST level
|
||||
if (Array.isArray(expr) && expr.length > 0) {
|
||||
var head = expr[0];
|
||||
if (head && head._sym) {
|
||||
var name = head.name;
|
||||
if (IO_PRIMITIVES[name]) {
|
||||
// Evaluate args, then call the IO primitive
|
||||
return asyncEvalIoCall(name, expr.slice(1), env);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Non-IO: use sync eval, but result might be a thunk
|
||||
var result = evalExpr(expr, env);
|
||||
return asyncTrampoline(result);
|
||||
}
|
||||
|
||||
function asyncEvalIoCall(name, rawArgs, env) {
|
||||
// Parse keyword args and positional args, evaluating each (may be async)
|
||||
var kwargs = {};
|
||||
var args = [];
|
||||
var promises = [];
|
||||
var i = 0;
|
||||
while (i < rawArgs.length) {
|
||||
var arg = rawArgs[i];
|
||||
if (arg && arg._kw && (i + 1) < rawArgs.length) {
|
||||
var kName = arg.name;
|
||||
var kVal = asyncEval(rawArgs[i + 1], env);
|
||||
if (isPromise(kVal)) {
|
||||
(function(k) { promises.push(kVal.then(function(v) { kwargs[k] = v; })); })(kName);
|
||||
} else {
|
||||
kwargs[kName] = kVal;
|
||||
}
|
||||
i += 2;
|
||||
} else {
|
||||
var aVal = asyncEval(arg, env);
|
||||
if (isPromise(aVal)) {
|
||||
(function(idx) { promises.push(aVal.then(function(v) { args[idx] = v; })); })(args.length);
|
||||
args.push(null); // placeholder
|
||||
} else {
|
||||
args.push(aVal);
|
||||
}
|
||||
i++;
|
||||
}
|
||||
}
|
||||
var ioFn = IO_PRIMITIVES[name];
|
||||
if (promises.length > 0) {
|
||||
return Promise.all(promises).then(function() { return ioFn(args, kwargs); });
|
||||
}
|
||||
return ioFn(args, kwargs);
|
||||
}
|
||||
|
||||
// Async render-to-dom: returns Promise<Node> or Node
|
||||
function asyncRenderToDom(expr, env, ns) {
|
||||
// Literals
|
||||
if (expr === NIL || expr === null || expr === undefined) return null;
|
||||
if (expr === true || expr === false) return null;
|
||||
if (typeof expr === "string") return document.createTextNode(expr);
|
||||
if (typeof expr === "number") return document.createTextNode(String(expr));
|
||||
|
||||
// Symbol -> async eval then render
|
||||
if (expr && expr._sym) {
|
||||
var val = asyncEval(expr, env);
|
||||
if (isPromise(val)) return val.then(function(v) { return asyncRenderToDom(v, env, ns); });
|
||||
return asyncRenderToDom(val, env, ns);
|
||||
}
|
||||
|
||||
// Keyword
|
||||
if (expr && expr._kw) return document.createTextNode(expr.name);
|
||||
|
||||
// DocumentFragment / DOM nodes pass through
|
||||
if (expr instanceof DocumentFragment || (expr && expr.nodeType)) return expr;
|
||||
|
||||
// Dict -> skip
|
||||
if (expr && typeof expr === "object" && !Array.isArray(expr)) return null;
|
||||
|
||||
// List
|
||||
if (!Array.isArray(expr) || expr.length === 0) return null;
|
||||
|
||||
var head = expr[0];
|
||||
if (!head) return null;
|
||||
|
||||
// Symbol head
|
||||
if (head._sym) {
|
||||
var hname = head.name;
|
||||
|
||||
// IO primitive
|
||||
if (IO_PRIMITIVES[hname]) {
|
||||
var ioResult = asyncEval(expr, env);
|
||||
if (isPromise(ioResult)) return ioResult.then(function(v) { return asyncRenderToDom(v, env, ns); });
|
||||
return asyncRenderToDom(ioResult, env, ns);
|
||||
}
|
||||
|
||||
// Fragment
|
||||
if (hname === "<>") return asyncRenderChildren(expr.slice(1), env, ns);
|
||||
|
||||
// raw!
|
||||
if (hname === "raw!") {
|
||||
return asyncEvalRaw(expr.slice(1), env);
|
||||
}
|
||||
|
||||
// Special forms that need async handling
|
||||
if (hname === "if") return asyncRenderIf(expr, env, ns);
|
||||
if (hname === "when") return asyncRenderWhen(expr, env, ns);
|
||||
if (hname === "cond") return asyncRenderCond(expr, env, ns);
|
||||
if (hname === "case") return asyncRenderCase(expr, env, ns);
|
||||
if (hname === "let" || hname === "let*") return asyncRenderLet(expr, env, ns);
|
||||
if (hname === "begin" || hname === "do") return asyncRenderChildren(expr.slice(1), env, ns);
|
||||
if (hname === "map") return asyncRenderMap(expr, env, ns);
|
||||
if (hname === "map-indexed") return asyncRenderMapIndexed(expr, env, ns);
|
||||
if (hname === "for-each") return asyncRenderMap(expr, env, ns);
|
||||
|
||||
// define/defcomp/defmacro — eval for side effects
|
||||
if (hname === "define" || hname === "defcomp" || hname === "defmacro" ||
|
||||
hname === "defstyle" || hname === "defkeyframes" || hname === "defhandler") {
|
||||
trampoline(evalExpr(expr, env));
|
||||
return null;
|
||||
}
|
||||
|
||||
// quote
|
||||
if (hname === "quote") return null;
|
||||
|
||||
// lambda/fn
|
||||
if (hname === "lambda" || hname === "fn") {
|
||||
trampoline(evalExpr(expr, env));
|
||||
return null;
|
||||
}
|
||||
|
||||
// and/or — eval and render result
|
||||
if (hname === "and" || hname === "or" || hname === "->") {
|
||||
var aoResult = asyncEval(expr, env);
|
||||
if (isPromise(aoResult)) return aoResult.then(function(v) { return asyncRenderToDom(v, env, ns); });
|
||||
return asyncRenderToDom(aoResult, env, ns);
|
||||
}
|
||||
|
||||
// set!
|
||||
if (hname === "set!") {
|
||||
asyncEval(expr, env);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Component
|
||||
if (hname.charAt(0) === "~") {
|
||||
var comp = env[hname];
|
||||
if (comp && comp._component) return asyncRenderComponent(comp, expr.slice(1), env, ns);
|
||||
if (comp && comp._macro) {
|
||||
var expanded = trampoline(expandMacro(comp, expr.slice(1), env));
|
||||
return asyncRenderToDom(expanded, env, ns);
|
||||
}
|
||||
}
|
||||
|
||||
// Macro
|
||||
if (env[hname] && env[hname]._macro) {
|
||||
var mac = env[hname];
|
||||
var expanded = trampoline(expandMacro(mac, expr.slice(1), env));
|
||||
return asyncRenderToDom(expanded, env, ns);
|
||||
}
|
||||
|
||||
// HTML tag
|
||||
if (typeof renderDomElement === "function" && contains(HTML_TAGS, hname)) {
|
||||
return asyncRenderElement(hname, expr.slice(1), env, ns);
|
||||
}
|
||||
|
||||
// html: prefix
|
||||
if (hname.indexOf("html:") === 0) {
|
||||
return asyncRenderElement(hname.slice(5), expr.slice(1), env, ns);
|
||||
}
|
||||
|
||||
// Custom element
|
||||
if (hname.indexOf("-") >= 0 && expr.length > 1 && expr[1] && expr[1]._kw) {
|
||||
return asyncRenderElement(hname, expr.slice(1), env, ns);
|
||||
}
|
||||
|
||||
// SVG context
|
||||
if (ns) return asyncRenderElement(hname, expr.slice(1), env, ns);
|
||||
|
||||
// Fallback: eval and render
|
||||
var fResult = asyncEval(expr, env);
|
||||
if (isPromise(fResult)) return fResult.then(function(v) { return asyncRenderToDom(v, env, ns); });
|
||||
return asyncRenderToDom(fResult, env, ns);
|
||||
}
|
||||
|
||||
// Non-symbol head: eval call
|
||||
var cResult = asyncEval(expr, env);
|
||||
if (isPromise(cResult)) return cResult.then(function(v) { return asyncRenderToDom(v, env, ns); });
|
||||
return asyncRenderToDom(cResult, env, ns);
|
||||
}
|
||||
|
||||
function asyncRenderChildren(exprs, env, ns) {
|
||||
var frag = document.createDocumentFragment();
|
||||
var pending = [];
|
||||
for (var i = 0; i < exprs.length; i++) {
|
||||
var result = asyncRenderToDom(exprs[i], env, ns);
|
||||
if (isPromise(result)) {
|
||||
// Insert placeholder, replace when resolved
|
||||
var placeholder = document.createComment("async");
|
||||
frag.appendChild(placeholder);
|
||||
(function(ph) {
|
||||
pending.push(result.then(function(node) {
|
||||
if (node) ph.parentNode.replaceChild(node, ph);
|
||||
else ph.parentNode.removeChild(ph);
|
||||
}));
|
||||
})(placeholder);
|
||||
} else if (result) {
|
||||
frag.appendChild(result);
|
||||
}
|
||||
}
|
||||
if (pending.length > 0) {
|
||||
return Promise.all(pending).then(function() { return frag; });
|
||||
}
|
||||
return frag;
|
||||
}
|
||||
|
||||
function asyncRenderElement(tag, args, env, ns) {
|
||||
var newNs = tag === "svg" ? SVG_NS : tag === "math" ? MATH_NS : ns;
|
||||
var el = domCreateElement(tag, newNs);
|
||||
var pending = [];
|
||||
var isVoid = contains(VOID_ELEMENTS, tag);
|
||||
for (var i = 0; i < args.length; i++) {
|
||||
var arg = args[i];
|
||||
if (arg && arg._kw && (i + 1) < args.length) {
|
||||
var attrName = arg.name;
|
||||
var attrVal = asyncEval(args[i + 1], env);
|
||||
i++;
|
||||
if (isPromise(attrVal)) {
|
||||
(function(an, av) {
|
||||
pending.push(av.then(function(v) {
|
||||
if (!isNil(v) && v !== false) {
|
||||
if (contains(BOOLEAN_ATTRS, an)) { if (isSxTruthy(v)) el.setAttribute(an, ""); }
|
||||
else if (v === true) el.setAttribute(an, "");
|
||||
else el.setAttribute(an, String(v));
|
||||
}
|
||||
}));
|
||||
})(attrName, attrVal);
|
||||
} else {
|
||||
if (!isNil(attrVal) && attrVal !== false) {
|
||||
if (attrName === "class" && attrVal && attrVal._styleValue) {
|
||||
el.setAttribute("class", (el.getAttribute("class") || "") + " " + attrVal.className);
|
||||
} else if (attrName === "style" && attrVal && attrVal._styleValue) {
|
||||
el.setAttribute("class", (el.getAttribute("class") || "") + " " + attrVal.className);
|
||||
} else if (contains(BOOLEAN_ATTRS, attrName)) {
|
||||
if (isSxTruthy(attrVal)) el.setAttribute(attrName, "");
|
||||
} else if (attrVal === true) {
|
||||
el.setAttribute(attrName, "");
|
||||
} else {
|
||||
el.setAttribute(attrName, String(attrVal));
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (!isVoid) {
|
||||
var child = asyncRenderToDom(arg, env, newNs);
|
||||
if (isPromise(child)) {
|
||||
var placeholder = document.createComment("async");
|
||||
el.appendChild(placeholder);
|
||||
(function(ph) {
|
||||
pending.push(child.then(function(node) {
|
||||
if (node) ph.parentNode.replaceChild(node, ph);
|
||||
else ph.parentNode.removeChild(ph);
|
||||
}));
|
||||
})(placeholder);
|
||||
} else if (child) {
|
||||
el.appendChild(child);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (pending.length > 0) return Promise.all(pending).then(function() { return el; });
|
||||
return el;
|
||||
}
|
||||
|
||||
function asyncRenderComponent(comp, args, env, ns) {
|
||||
var kwargs = {};
|
||||
var children = [];
|
||||
var pending = [];
|
||||
for (var i = 0; i < args.length; i++) {
|
||||
var arg = args[i];
|
||||
if (arg && arg._kw && (i + 1) < args.length) {
|
||||
var kName = arg.name;
|
||||
var kVal = asyncEval(args[i + 1], env);
|
||||
if (isPromise(kVal)) {
|
||||
(function(k) { pending.push(kVal.then(function(v) { kwargs[k] = v; })); })(kName);
|
||||
} else {
|
||||
kwargs[kName] = kVal;
|
||||
}
|
||||
i++;
|
||||
} else {
|
||||
children.push(arg);
|
||||
}
|
||||
}
|
||||
|
||||
function doRender() {
|
||||
var local = Object.create(componentClosure(comp));
|
||||
for (var k in env) if (env.hasOwnProperty(k)) local[k] = env[k];
|
||||
var params = componentParams(comp);
|
||||
for (var j = 0; j < params.length; j++) {
|
||||
local[params[j]] = params[j] in kwargs ? kwargs[params[j]] : NIL;
|
||||
}
|
||||
if (componentHasChildren(comp)) {
|
||||
var childResult = asyncRenderChildren(children, env, ns);
|
||||
if (isPromise(childResult)) {
|
||||
return childResult.then(function(childFrag) {
|
||||
local["children"] = childFrag;
|
||||
return asyncRenderToDom(componentBody(comp), local, ns);
|
||||
});
|
||||
}
|
||||
local["children"] = childResult;
|
||||
}
|
||||
return asyncRenderToDom(componentBody(comp), local, ns);
|
||||
}
|
||||
|
||||
if (pending.length > 0) return Promise.all(pending).then(doRender);
|
||||
return doRender();
|
||||
}
|
||||
|
||||
function asyncRenderIf(expr, env, ns) {
|
||||
var cond = asyncEval(expr[1], env);
|
||||
if (isPromise(cond)) {
|
||||
return cond.then(function(v) {
|
||||
return isSxTruthy(v)
|
||||
? asyncRenderToDom(expr[2], env, ns)
|
||||
: (expr.length > 3 ? asyncRenderToDom(expr[3], env, ns) : null);
|
||||
});
|
||||
}
|
||||
return isSxTruthy(cond)
|
||||
? asyncRenderToDom(expr[2], env, ns)
|
||||
: (expr.length > 3 ? asyncRenderToDom(expr[3], env, ns) : null);
|
||||
}
|
||||
|
||||
function asyncRenderWhen(expr, env, ns) {
|
||||
var cond = asyncEval(expr[1], env);
|
||||
if (isPromise(cond)) {
|
||||
return cond.then(function(v) {
|
||||
return isSxTruthy(v) ? asyncRenderChildren(expr.slice(2), env, ns) : null;
|
||||
});
|
||||
}
|
||||
return isSxTruthy(cond) ? asyncRenderChildren(expr.slice(2), env, ns) : null;
|
||||
}
|
||||
|
||||
function asyncRenderCond(expr, env, ns) {
|
||||
var clauses = expr.slice(1);
|
||||
function step(idx) {
|
||||
if (idx >= clauses.length) return null;
|
||||
var clause = clauses[idx];
|
||||
if (!Array.isArray(clause) || clause.length < 2) return step(idx + 1);
|
||||
var test = clause[0];
|
||||
if ((test && test._sym && (test.name === "else" || test.name === ":else")) ||
|
||||
(test && test._kw && test.name === "else")) {
|
||||
return asyncRenderToDom(clause[1], env, ns);
|
||||
}
|
||||
var v = asyncEval(test, env);
|
||||
if (isPromise(v)) return v.then(function(r) { return isSxTruthy(r) ? asyncRenderToDom(clause[1], env, ns) : step(idx + 1); });
|
||||
return isSxTruthy(v) ? asyncRenderToDom(clause[1], env, ns) : step(idx + 1);
|
||||
}
|
||||
return step(0);
|
||||
}
|
||||
|
||||
function asyncRenderCase(expr, env, ns) {
|
||||
var matchVal = asyncEval(expr[1], env);
|
||||
function doCase(mv) {
|
||||
var clauses = expr.slice(2);
|
||||
for (var i = 0; i < clauses.length - 1; i += 2) {
|
||||
var test = clauses[i];
|
||||
if ((test && test._kw && test.name === "else") ||
|
||||
(test && test._sym && (test.name === "else" || test.name === ":else"))) {
|
||||
return asyncRenderToDom(clauses[i + 1], env, ns);
|
||||
}
|
||||
var tv = trampoline(evalExpr(test, env));
|
||||
if (mv === tv || (typeof mv === "string" && typeof tv === "string" && mv === tv)) {
|
||||
return asyncRenderToDom(clauses[i + 1], env, ns);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
if (isPromise(matchVal)) return matchVal.then(doCase);
|
||||
return doCase(matchVal);
|
||||
}
|
||||
|
||||
function asyncRenderLet(expr, env, ns) {
|
||||
var bindings = expr[1];
|
||||
var local = Object.create(env);
|
||||
for (var k in env) if (env.hasOwnProperty(k)) local[k] = env[k];
|
||||
function bindStep(idx) {
|
||||
if (!Array.isArray(bindings)) return asyncRenderChildren(expr.slice(2), local, ns);
|
||||
// Nested pairs: ((a 1) (b 2))
|
||||
if (bindings.length > 0 && Array.isArray(bindings[0])) {
|
||||
if (idx >= bindings.length) return asyncRenderChildren(expr.slice(2), local, ns);
|
||||
var b = bindings[idx];
|
||||
var vname = b[0]._sym ? b[0].name : String(b[0]);
|
||||
var val = asyncEval(b[1], local);
|
||||
if (isPromise(val)) return val.then(function(v) { local[vname] = v; return bindStep(idx + 1); });
|
||||
local[vname] = val;
|
||||
return bindStep(idx + 1);
|
||||
}
|
||||
// Flat pairs: (a 1 b 2)
|
||||
if (idx >= bindings.length) return asyncRenderChildren(expr.slice(2), local, ns);
|
||||
var vn = bindings[idx]._sym ? bindings[idx].name : String(bindings[idx]);
|
||||
var vv = asyncEval(bindings[idx + 1], local);
|
||||
if (isPromise(vv)) return vv.then(function(v) { local[vn] = v; return bindStep(idx + 2); });
|
||||
local[vn] = vv;
|
||||
return bindStep(idx + 2);
|
||||
}
|
||||
return bindStep(0);
|
||||
}
|
||||
|
||||
function asyncRenderMap(expr, env, ns) {
|
||||
var fn = asyncEval(expr[1], env);
|
||||
var coll = asyncEval(expr[2], env);
|
||||
function doMap(f, c) {
|
||||
if (!Array.isArray(c)) return null;
|
||||
var frag = document.createDocumentFragment();
|
||||
var pending = [];
|
||||
for (var i = 0; i < c.length; i++) {
|
||||
var item = c[i];
|
||||
var result;
|
||||
if (f && f._lambda) {
|
||||
var lenv = Object.create(f._closure || env);
|
||||
for (var k in env) if (env.hasOwnProperty(k)) lenv[k] = env[k];
|
||||
lenv[f._params[0]] = item;
|
||||
result = asyncRenderToDom(f._body, lenv, null);
|
||||
} else if (typeof f === "function") {
|
||||
var r = f(item);
|
||||
result = isPromise(r) ? r.then(function(v) { return asyncRenderToDom(v, env, null); }) : asyncRenderToDom(r, env, null);
|
||||
} else {
|
||||
result = asyncRenderToDom(item, env, null);
|
||||
}
|
||||
if (isPromise(result)) {
|
||||
var ph = document.createComment("async");
|
||||
frag.appendChild(ph);
|
||||
(function(p) { pending.push(result.then(function(n) { if (n) p.parentNode.replaceChild(n, p); else p.parentNode.removeChild(p); })); })(ph);
|
||||
} else if (result) {
|
||||
frag.appendChild(result);
|
||||
}
|
||||
}
|
||||
if (pending.length) return Promise.all(pending).then(function() { return frag; });
|
||||
return frag;
|
||||
}
|
||||
if (isPromise(fn) || isPromise(coll)) {
|
||||
return Promise.all([isPromise(fn) ? fn : Promise.resolve(fn), isPromise(coll) ? coll : Promise.resolve(coll)])
|
||||
.then(function(r) { return doMap(r[0], r[1]); });
|
||||
}
|
||||
return doMap(fn, coll);
|
||||
}
|
||||
|
||||
function asyncRenderMapIndexed(expr, env, ns) {
|
||||
var fn = asyncEval(expr[1], env);
|
||||
var coll = asyncEval(expr[2], env);
|
||||
function doMap(f, c) {
|
||||
if (!Array.isArray(c)) return null;
|
||||
var frag = document.createDocumentFragment();
|
||||
var pending = [];
|
||||
for (var i = 0; i < c.length; i++) {
|
||||
var item = c[i];
|
||||
var result;
|
||||
if (f && f._lambda) {
|
||||
var lenv = Object.create(f._closure || env);
|
||||
for (var k in env) if (env.hasOwnProperty(k)) lenv[k] = env[k];
|
||||
lenv[f._params[0]] = i;
|
||||
lenv[f._params[1]] = item;
|
||||
result = asyncRenderToDom(f._body, lenv, null);
|
||||
} else if (typeof f === "function") {
|
||||
var r = f(i, item);
|
||||
result = isPromise(r) ? r.then(function(v) { return asyncRenderToDom(v, env, null); }) : asyncRenderToDom(r, env, null);
|
||||
} else {
|
||||
result = asyncRenderToDom(item, env, null);
|
||||
}
|
||||
if (isPromise(result)) {
|
||||
var ph = document.createComment("async");
|
||||
frag.appendChild(ph);
|
||||
(function(p) { pending.push(result.then(function(n) { if (n) p.parentNode.replaceChild(n, p); else p.parentNode.removeChild(p); })); })(ph);
|
||||
} else if (result) {
|
||||
frag.appendChild(result);
|
||||
}
|
||||
}
|
||||
if (pending.length) return Promise.all(pending).then(function() { return frag; });
|
||||
return frag;
|
||||
}
|
||||
if (isPromise(fn) || isPromise(coll)) {
|
||||
return Promise.all([isPromise(fn) ? fn : Promise.resolve(fn), isPromise(coll) ? coll : Promise.resolve(coll)])
|
||||
.then(function(r) { return doMap(r[0], r[1]); });
|
||||
}
|
||||
return doMap(fn, coll);
|
||||
}
|
||||
|
||||
function asyncEvalRaw(args, env) {
|
||||
var parts = [];
|
||||
var pending = [];
|
||||
for (var i = 0; i < args.length; i++) {
|
||||
var val = asyncEval(args[i], env);
|
||||
if (isPromise(val)) {
|
||||
(function(idx) {
|
||||
pending.push(val.then(function(v) { parts[idx] = v; }));
|
||||
})(parts.length);
|
||||
parts.push(null);
|
||||
} else {
|
||||
parts.push(val);
|
||||
}
|
||||
}
|
||||
function assemble() {
|
||||
var html = "";
|
||||
for (var j = 0; j < parts.length; j++) {
|
||||
var p = parts[j];
|
||||
if (p && p._rawHtml) html += p.html;
|
||||
else if (typeof p === "string") html += p;
|
||||
else if (p != null && !isNil(p)) html += String(p);
|
||||
}
|
||||
var el = document.createElement("span");
|
||||
el.innerHTML = html;
|
||||
var frag = document.createDocumentFragment();
|
||||
while (el.firstChild) frag.appendChild(el.firstChild);
|
||||
return frag;
|
||||
}
|
||||
if (pending.length) return Promise.all(pending).then(assemble);
|
||||
return assemble();
|
||||
}
|
||||
|
||||
// Async version of sxRenderWithEnv — returns Promise<DocumentFragment>
|
||||
function asyncSxRenderWithEnv(source, extraEnv) {
|
||||
var env = extraEnv ? merge(componentEnv, extraEnv) : componentEnv;
|
||||
var exprs = parse(source);
|
||||
if (!_hasDom) return Promise.resolve(null);
|
||||
return asyncRenderChildren(exprs, env, null);
|
||||
}
|
||||
|
||||
// Register a server-proxied IO primitive: fetches from /sx/io/<name>
|
||||
function registerProxiedIo(name) {
|
||||
registerIoPrimitive(name, function(args, kwargs) {
|
||||
var url = "/sx/io/" + encodeURIComponent(name);
|
||||
var qs = [];
|
||||
for (var i = 0; i < args.length; i++) {
|
||||
qs.push("_arg" + i + "=" + encodeURIComponent(String(args[i])));
|
||||
}
|
||||
for (var k in kwargs) {
|
||||
if (kwargs.hasOwnProperty(k)) {
|
||||
qs.push(encodeURIComponent(k) + "=" + encodeURIComponent(String(kwargs[k])));
|
||||
}
|
||||
}
|
||||
if (qs.length) url += "?" + qs.join("&");
|
||||
return fetch(url, { headers: { "SX-Request": "true" } })
|
||||
.then(function(resp) {
|
||||
if (!resp.ok) {
|
||||
logWarn("sx:io " + name + " failed " + resp.status);
|
||||
return NIL;
|
||||
}
|
||||
return resp.text();
|
||||
})
|
||||
.then(function(text) {
|
||||
if (!text || text === "nil") return NIL;
|
||||
try {
|
||||
var exprs = parse(text);
|
||||
return exprs.length === 1 ? exprs[0] : exprs;
|
||||
} catch (e) {
|
||||
logWarn("sx:io " + name + " parse error: " + (e && e.message ? e.message : e));
|
||||
return NIL;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Register default proxied IO primitives
|
||||
function initIoPrimitives() {
|
||||
var defaults = [
|
||||
"highlight", "current-user", "request-arg", "request-path",
|
||||
"app-url", "asset-url", "config"
|
||||
];
|
||||
for (var i = 0; i < defaults.length; i++) {
|
||||
registerProxiedIo(defaults[i]);
|
||||
}
|
||||
logInfo("sx:io registered " + defaults.length + " proxied primitives");
|
||||
}
|
||||
'''
|
||||
|
||||
|
||||
def compile_ref_to_js(
|
||||
adapters: list[str] | None = None,
|
||||
modules: list[str] | None = None,
|
||||
@@ -1290,6 +1893,8 @@ def compile_ref_to_js(
|
||||
parts.append(fixups_js(has_html, has_sx, has_dom))
|
||||
if has_continuations:
|
||||
parts.append(CONTINUATIONS_JS)
|
||||
if has_dom:
|
||||
parts.append(ASYNC_IO_JS)
|
||||
parts.append(public_api_js(has_html, has_sx, has_dom, has_engine, has_orch, has_cssx, has_boot, has_parser, adapter_label, has_deps, has_router))
|
||||
parts.append(EPILOGUE)
|
||||
from datetime import datetime, timezone
|
||||
@@ -2783,6 +3388,32 @@ PLATFORM_ORCHESTRATION_JS = """
|
||||
}
|
||||
}
|
||||
|
||||
// Async eval with callback — used for pages with IO deps.
|
||||
// Calls callback(rendered) when done, callback(null) on failure.
|
||||
function tryAsyncEvalContent(source, env, callback) {
|
||||
var merged = merge(componentEnv);
|
||||
if (env && !isNil(env)) {
|
||||
var ks = Object.keys(env);
|
||||
for (var i = 0; i < ks.length; i++) merged[ks[i]] = env[ks[i]];
|
||||
}
|
||||
try {
|
||||
var result = asyncSxRenderWithEnv(source, merged);
|
||||
if (isPromise(result)) {
|
||||
result.then(function(rendered) {
|
||||
callback(rendered);
|
||||
}).catch(function(e) {
|
||||
logInfo("sx:async eval miss: " + (e && e.message ? e.message : e));
|
||||
callback(null);
|
||||
});
|
||||
} else {
|
||||
callback(result);
|
||||
}
|
||||
} catch (e) {
|
||||
logInfo("sx:async eval miss: " + (e && e.message ? e.message : e));
|
||||
callback(null);
|
||||
}
|
||||
}
|
||||
|
||||
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) —
|
||||
@@ -3360,6 +3991,10 @@ def public_api_js(has_html, has_sx, has_dom, has_engine, has_orch, has_cssx, has
|
||||
api_lines.append(' matchRoute: matchRoute,')
|
||||
api_lines.append(' findMatchingRoute: findMatchingRoute,')
|
||||
|
||||
if has_dom:
|
||||
api_lines.append(' registerIo: typeof registerIoPrimitive === "function" ? registerIoPrimitive : null,')
|
||||
api_lines.append(' asyncRender: typeof asyncSxRenderWithEnv === "function" ? asyncSxRenderWithEnv : null,')
|
||||
api_lines.append(' asyncRenderToDom: typeof asyncRenderToDom === "function" ? asyncRenderToDom : null,')
|
||||
api_lines.append(f' _version: "{version}"')
|
||||
api_lines.append(' };')
|
||||
api_lines.append('')
|
||||
|
||||
@@ -648,40 +648,71 @@
|
||||
(do (log-warn (str "sx:route target not found: " target-sel)) false)
|
||||
(if (not (deps-satisfied? match))
|
||||
(do (log-info (str "sx:route deps miss for " page-name)) false)
|
||||
(if (get match "has-data")
|
||||
;; Data page: check cache, else resolve asynchronously
|
||||
(let ((cache-key (page-data-cache-key page-name params))
|
||||
(cached (page-data-cache-get cache-key)))
|
||||
(if cached
|
||||
;; Cache hit: render immediately
|
||||
(let ((env (merge closure params cached))
|
||||
(let ((has-io (get match "has-io")))
|
||||
(if (get match "has-data")
|
||||
;; Data page: check cache, else resolve asynchronously
|
||||
(let ((cache-key (page-data-cache-key page-name params))
|
||||
(cached (page-data-cache-get cache-key)))
|
||||
(if cached
|
||||
;; Cache hit
|
||||
(let ((env (merge closure params cached)))
|
||||
(if has-io
|
||||
;; Async render (data+IO)
|
||||
(do
|
||||
(log-info (str "sx:route client+cache+async " pathname))
|
||||
(try-async-eval-content content-src env
|
||||
(fn (rendered)
|
||||
(if (nil? rendered)
|
||||
(log-warn (str "sx:route async eval failed for " pathname))
|
||||
(swap-rendered-content target rendered pathname))))
|
||||
true)
|
||||
;; Sync render (data only)
|
||||
(let ((rendered (try-eval-content content-src env)))
|
||||
(if (nil? rendered)
|
||||
(do (log-warn (str "sx:route cached eval failed for " pathname)) false)
|
||||
(do
|
||||
(log-info (str "sx:route client+cache " pathname))
|
||||
(swap-rendered-content target rendered pathname)
|
||||
true)))))
|
||||
;; Cache miss: fetch, cache, render
|
||||
(do
|
||||
(log-info (str "sx:route client+data " pathname))
|
||||
(resolve-page-data page-name params
|
||||
(fn (data)
|
||||
(page-data-cache-set cache-key data)
|
||||
(let ((env (merge closure params data)))
|
||||
(if has-io
|
||||
;; Async render (data+IO)
|
||||
(try-async-eval-content content-src env
|
||||
(fn (rendered)
|
||||
(if (nil? rendered)
|
||||
(log-warn (str "sx:route data+async eval failed for " pathname))
|
||||
(swap-rendered-content target rendered pathname))))
|
||||
;; Sync render (data only)
|
||||
(let ((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)))
|
||||
;; Non-data page
|
||||
(if has-io
|
||||
;; Async render (IO only, no data)
|
||||
(do
|
||||
(log-info (str "sx:route client+async " pathname))
|
||||
(try-async-eval-content content-src (merge closure params)
|
||||
(fn (rendered)
|
||||
(if (nil? rendered)
|
||||
(log-warn (str "sx:route async 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-warn (str "sx:route cached eval failed for " pathname)) false)
|
||||
(do (log-info (str "sx:route server (eval failed) " pathname)) false)
|
||||
(do
|
||||
(log-info (str "sx:route client+cache " pathname))
|
||||
(swap-rendered-content target rendered pathname)
|
||||
true)))
|
||||
;; Cache miss: fetch, cache, render
|
||||
(do
|
||||
(log-info (str "sx:route client+data " pathname))
|
||||
(resolve-page-data page-name params
|
||||
(fn (data)
|
||||
(page-data-cache-set cache-key data)
|
||||
(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
|
||||
(swap-rendered-content target rendered pathname)
|
||||
true)))))))))))))
|
||||
true)))))))))))))))
|
||||
|
||||
|
||||
(define bind-client-route-link
|
||||
@@ -991,6 +1022,8 @@
|
||||
;;
|
||||
;; === Client-side routing ===
|
||||
;; (try-eval-content source env) → DOM node or nil (catches eval errors)
|
||||
;; (try-async-eval-content source env callback) → void; async render,
|
||||
;; calls (callback rendered-or-nil). Used for pages with IO deps.
|
||||
;; (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)
|
||||
|
||||
Reference in New Issue
Block a user