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:
@@ -14,7 +14,7 @@
|
||||
// =========================================================================
|
||||
|
||||
var NIL = Object.freeze({ _nil: true, toString: function() { return "nil"; } });
|
||||
var SX_VERSION = "2026-03-07T02:10:28Z";
|
||||
var SX_VERSION = "2026-03-07T02:32:34Z";
|
||||
|
||||
function isNil(x) { return x === NIL || x === null || x === undefined; }
|
||||
function isSxTruthy(x) { return x !== false && !isNil(x); }
|
||||
@@ -565,92 +565,6 @@
|
||||
return makeThunk(componentBody(comp), local);
|
||||
};
|
||||
|
||||
// =========================================================================
|
||||
// Platform: deps module — component dependency analysis
|
||||
// =========================================================================
|
||||
|
||||
function componentDeps(c) {
|
||||
return c.deps ? c.deps.slice() : [];
|
||||
}
|
||||
|
||||
function componentSetDeps(c, deps) {
|
||||
c.deps = deps;
|
||||
}
|
||||
|
||||
function componentCssClasses(c) {
|
||||
return c.cssClasses ? c.cssClasses.slice() : [];
|
||||
}
|
||||
|
||||
function envComponents(env) {
|
||||
var names = [];
|
||||
for (var k in env) {
|
||||
var v = env[k];
|
||||
if (v && (v._component || v._macro)) names.push(k);
|
||||
}
|
||||
return names;
|
||||
}
|
||||
|
||||
function regexFindAll(pattern, source) {
|
||||
var re = new RegExp(pattern, "g");
|
||||
var results = [];
|
||||
var m;
|
||||
while ((m = re.exec(source)) !== null) {
|
||||
if (m[1] !== undefined) results.push(m[1]);
|
||||
else results.push(m[0]);
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
function scanCssClasses(source) {
|
||||
var classes = {};
|
||||
var result = [];
|
||||
var m;
|
||||
var re1 = /:class\s+"([^"]*)"/g;
|
||||
while ((m = re1.exec(source)) !== null) {
|
||||
var parts = m[1].split(/\s+/);
|
||||
for (var i = 0; i < parts.length; i++) {
|
||||
if (parts[i] && !classes[parts[i]]) {
|
||||
classes[parts[i]] = true;
|
||||
result.push(parts[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
var re2 = /:class\s+\(str\s+((?:"[^"]*"\s*)+)\)/g;
|
||||
while ((m = re2.exec(source)) !== null) {
|
||||
var re3 = /"([^"]*)"/g;
|
||||
var m2;
|
||||
while ((m2 = re3.exec(m[1])) !== null) {
|
||||
var parts2 = m2[1].split(/\s+/);
|
||||
for (var j = 0; j < parts2.length; j++) {
|
||||
if (parts2[j] && !classes[parts2[j]]) {
|
||||
classes[parts2[j]] = true;
|
||||
result.push(parts2[j]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
var re4 = /;;\s*@css\s+(.+)/g;
|
||||
while ((m = re4.exec(source)) !== null) {
|
||||
var parts3 = m[1].split(/\s+/);
|
||||
for (var k = 0; k < parts3.length; k++) {
|
||||
if (parts3[k] && !classes[parts3[k]]) {
|
||||
classes[parts3[k]] = true;
|
||||
result.push(parts3[k]);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function componentIoRefs(c) {
|
||||
return c.ioRefs ? c.ioRefs.slice() : [];
|
||||
}
|
||||
|
||||
function componentSetIoRefs(c, refs) {
|
||||
c.ioRefs = refs;
|
||||
}
|
||||
|
||||
|
||||
// =========================================================================
|
||||
// Platform interface — Parser
|
||||
// =========================================================================
|
||||
@@ -2116,24 +2030,31 @@ return domAppendToHead(link); }, domQueryAll(container, "link[rel=\"stylesheet\"
|
||||
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(!isSxTruthy(depsSatisfied_p(match))) ? (logInfo((String("sx:route deps miss for ") + String(pageName))), false) : (isSxTruthy(get(match, "has-data")) ? (function() {
|
||||
return (isSxTruthy(isNil(target)) ? (logWarn((String("sx:route target not found: ") + String(targetSel))), false) : (isSxTruthy(!isSxTruthy(depsSatisfied_p(match))) ? (logInfo((String("sx:route deps miss for ") + String(pageName))), false) : (function() {
|
||||
var hasIo = get(match, "has-io");
|
||||
return (isSxTruthy(get(match, "has-data")) ? (function() {
|
||||
var cacheKey = pageDataCacheKey(pageName, params);
|
||||
var cached = pageDataCacheGet(cacheKey);
|
||||
return (isSxTruthy(cached) ? (function() {
|
||||
var env = merge(closure, params, cached);
|
||||
return (isSxTruthy(hasIo) ? (logInfo((String("sx:route client+cache+async ") + String(pathname))), tryAsyncEvalContent(contentSrc, env, function(rendered) { return (isSxTruthy(isNil(rendered)) ? logWarn((String("sx:route async eval failed for ") + String(pathname))) : swapRenderedContent(target, rendered, pathname)); }), true) : (function() {
|
||||
var rendered = tryEvalContent(contentSrc, env);
|
||||
return (isSxTruthy(isNil(rendered)) ? (logWarn((String("sx:route cached eval failed for ") + String(pathname))), false) : (logInfo((String("sx:route client+cache ") + String(pathname))), swapRenderedContent(target, rendered, pathname), true));
|
||||
})());
|
||||
})() : (logInfo((String("sx:route client+data ") + String(pathname))), resolvePageData(pageName, params, function(data) { pageDataCacheSet(cacheKey, data);
|
||||
return (function() {
|
||||
var env = merge(closure, params, data);
|
||||
return (isSxTruthy(hasIo) ? tryAsyncEvalContent(contentSrc, env, function(rendered) { return (isSxTruthy(isNil(rendered)) ? logWarn((String("sx:route data+async eval failed for ") + String(pathname))) : swapRenderedContent(target, rendered, pathname)); }) : (function() {
|
||||
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() {
|
||||
})() : (isSxTruthy(hasIo) ? (logInfo((String("sx:route client+async ") + String(pathname))), tryAsyncEvalContent(contentSrc, merge(closure, params), function(rendered) { return (isSxTruthy(isNil(rendered)) ? logWarn((String("sx:route async 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) : (swapRenderedContent(target, rendered, pathname), true));
|
||||
})())));
|
||||
})()));
|
||||
})()));
|
||||
})());
|
||||
})());
|
||||
})(); };
|
||||
@@ -2555,120 +2476,7 @@ callExpr.push(dictGet(kwargs, k)); } }
|
||||
})(); };
|
||||
|
||||
// boot-init
|
||||
var bootInit = function() { return (logInfo((String("sx-browser ") + String(SX_VERSION))), initCssTracking(), initStyleDict(), processPageScripts(), processSxScripts(NIL), sxHydrateElements(NIL), processElements(NIL)); };
|
||||
|
||||
|
||||
// === Transpiled from deps (component dependency analysis) ===
|
||||
|
||||
// scan-refs
|
||||
var scanRefs = function(node) { return (function() {
|
||||
var refs = [];
|
||||
scanRefsWalk(node, refs);
|
||||
return refs;
|
||||
})(); };
|
||||
|
||||
// scan-refs-walk
|
||||
var scanRefsWalk = function(node, refs) { return (isSxTruthy((typeOf(node) == "symbol")) ? (function() {
|
||||
var name = symbolName(node);
|
||||
return (isSxTruthy(startsWith(name, "~")) ? (isSxTruthy(!isSxTruthy(contains(refs, name))) ? append_b(refs, name) : NIL) : NIL);
|
||||
})() : (isSxTruthy((typeOf(node) == "list")) ? forEach(function(item) { return scanRefsWalk(item, refs); }, node) : (isSxTruthy((typeOf(node) == "dict")) ? forEach(function(key) { return scanRefsWalk(dictGet(node, key), refs); }, keys(node)) : NIL))); };
|
||||
|
||||
// transitive-deps-walk
|
||||
var transitiveDepsWalk = function(n, seen, env) { return (isSxTruthy(!isSxTruthy(contains(seen, n))) ? (append_b(seen, n), (function() {
|
||||
var val = envGet(env, n);
|
||||
return (isSxTruthy((typeOf(val) == "component")) ? forEach(function(ref) { return transitiveDepsWalk(ref, seen, env); }, scanRefs(componentBody(val))) : (isSxTruthy((typeOf(val) == "macro")) ? forEach(function(ref) { return transitiveDepsWalk(ref, seen, env); }, scanRefs(macroBody(val))) : NIL));
|
||||
})()) : NIL); };
|
||||
|
||||
// transitive-deps
|
||||
var transitiveDeps = function(name, env) { return (function() {
|
||||
var seen = [];
|
||||
var key = (isSxTruthy(startsWith(name, "~")) ? name : (String("~") + String(name)));
|
||||
transitiveDepsWalk(key, seen, env);
|
||||
return filter(function(x) { return !isSxTruthy((x == key)); }, seen);
|
||||
})(); };
|
||||
|
||||
// compute-all-deps
|
||||
var computeAllDeps = function(env) { return forEach(function(name) { return (function() {
|
||||
var val = envGet(env, name);
|
||||
return (isSxTruthy((typeOf(val) == "component")) ? componentSetDeps(val, transitiveDeps(name, env)) : NIL);
|
||||
})(); }, envComponents(env)); };
|
||||
|
||||
// scan-components-from-source
|
||||
var scanComponentsFromSource = function(source) { return (function() {
|
||||
var matches = regexFindAll("\\(~([a-zA-Z_][a-zA-Z0-9_\\-]*)", source);
|
||||
return map(function(m) { return (String("~") + String(m)); }, matches);
|
||||
})(); };
|
||||
|
||||
// components-needed
|
||||
var componentsNeeded = function(pageSource, env) { return (function() {
|
||||
var direct = scanComponentsFromSource(pageSource);
|
||||
var allNeeded = [];
|
||||
{ var _c = direct; for (var _i = 0; _i < _c.length; _i++) { var name = _c[_i]; if (isSxTruthy(!isSxTruthy(contains(allNeeded, name)))) {
|
||||
allNeeded.push(name);
|
||||
}
|
||||
(function() {
|
||||
var val = envGet(env, name);
|
||||
return (function() {
|
||||
var deps = (isSxTruthy((isSxTruthy((typeOf(val) == "component")) && !isSxTruthy(isEmpty(componentDeps(val))))) ? componentDeps(val) : transitiveDeps(name, env));
|
||||
return forEach(function(dep) { return (isSxTruthy(!isSxTruthy(contains(allNeeded, dep))) ? append_b(allNeeded, dep) : NIL); }, deps);
|
||||
})();
|
||||
})(); } }
|
||||
return allNeeded;
|
||||
})(); };
|
||||
|
||||
// page-component-bundle
|
||||
var pageComponentBundle = function(pageSource, env) { return componentsNeeded(pageSource, env); };
|
||||
|
||||
// page-css-classes
|
||||
var pageCssClasses = function(pageSource, env) { return (function() {
|
||||
var needed = componentsNeeded(pageSource, env);
|
||||
var classes = [];
|
||||
{ var _c = needed; for (var _i = 0; _i < _c.length; _i++) { var name = _c[_i]; (function() {
|
||||
var val = envGet(env, name);
|
||||
return (isSxTruthy((typeOf(val) == "component")) ? forEach(function(cls) { return (isSxTruthy(!isSxTruthy(contains(classes, cls))) ? append_b(classes, cls) : NIL); }, componentCssClasses(val)) : NIL);
|
||||
})(); } }
|
||||
{ var _c = scanCssClasses(pageSource); for (var _i = 0; _i < _c.length; _i++) { var cls = _c[_i]; if (isSxTruthy(!isSxTruthy(contains(classes, cls)))) {
|
||||
classes.push(cls);
|
||||
} } }
|
||||
return classes;
|
||||
})(); };
|
||||
|
||||
// scan-io-refs-walk
|
||||
var scanIoRefsWalk = function(node, ioNames, refs) { return (isSxTruthy((typeOf(node) == "symbol")) ? (function() {
|
||||
var name = symbolName(node);
|
||||
return (isSxTruthy(contains(ioNames, name)) ? (isSxTruthy(!isSxTruthy(contains(refs, name))) ? append_b(refs, name) : NIL) : NIL);
|
||||
})() : (isSxTruthy((typeOf(node) == "list")) ? forEach(function(item) { return scanIoRefsWalk(item, ioNames, refs); }, node) : (isSxTruthy((typeOf(node) == "dict")) ? forEach(function(key) { return scanIoRefsWalk(dictGet(node, key), ioNames, refs); }, keys(node)) : NIL))); };
|
||||
|
||||
// scan-io-refs
|
||||
var scanIoRefs = function(node, ioNames) { return (function() {
|
||||
var refs = [];
|
||||
scanIoRefsWalk(node, ioNames, refs);
|
||||
return refs;
|
||||
})(); };
|
||||
|
||||
// transitive-io-refs-walk
|
||||
var transitiveIoRefsWalk = function(n, seen, allRefs, env, ioNames) { return (isSxTruthy(!isSxTruthy(contains(seen, n))) ? (append_b(seen, n), (function() {
|
||||
var val = envGet(env, n);
|
||||
return (isSxTruthy((typeOf(val) == "component")) ? (forEach(function(ref) { return (isSxTruthy(!isSxTruthy(contains(allRefs, ref))) ? append_b(allRefs, ref) : NIL); }, scanIoRefs(componentBody(val), ioNames)), forEach(function(dep) { return transitiveIoRefsWalk(dep, seen, allRefs, env, ioNames); }, scanRefs(componentBody(val)))) : (isSxTruthy((typeOf(val) == "macro")) ? (forEach(function(ref) { return (isSxTruthy(!isSxTruthy(contains(allRefs, ref))) ? append_b(allRefs, ref) : NIL); }, scanIoRefs(macroBody(val), ioNames)), forEach(function(dep) { return transitiveIoRefsWalk(dep, seen, allRefs, env, ioNames); }, scanRefs(macroBody(val)))) : NIL));
|
||||
})()) : NIL); };
|
||||
|
||||
// transitive-io-refs
|
||||
var transitiveIoRefs = function(name, env, ioNames) { return (function() {
|
||||
var allRefs = [];
|
||||
var seen = [];
|
||||
var key = (isSxTruthy(startsWith(name, "~")) ? name : (String("~") + String(name)));
|
||||
transitiveIoRefsWalk(key, seen, allRefs, env, ioNames);
|
||||
return allRefs;
|
||||
})(); };
|
||||
|
||||
// compute-all-io-refs
|
||||
var computeAllIoRefs = function(env, ioNames) { return forEach(function(name) { return (function() {
|
||||
var val = envGet(env, name);
|
||||
return (isSxTruthy((typeOf(val) == "component")) ? componentSetIoRefs(val, transitiveIoRefs(name, env, ioNames)) : NIL);
|
||||
})(); }, envComponents(env)); };
|
||||
|
||||
// component-pure?
|
||||
var componentPure_p = function(name, env, ioNames) { return isEmpty(transitiveIoRefs(name, env, ioNames)); };
|
||||
var bootInit = function() { return (logInfo((String("sx-browser ") + String(SX_VERSION))), initCssTracking(), initStyleDict(), initIoPrimitives(), processPageScripts(), processSxScripts(NIL), sxHydrateElements(NIL), processElements(NIL)); };
|
||||
|
||||
|
||||
// === Transpiled from router (client-side route matching) ===
|
||||
@@ -3510,6 +3318,32 @@ callExpr.push(dictGet(kwargs, k)); } }
|
||||
}
|
||||
}
|
||||
|
||||
// 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) —
|
||||
@@ -3939,6 +3773,605 @@ callExpr.push(dictGet(kwargs, k)); } }
|
||||
if (typeof aser === "function") PRIMITIVES["aser"] = aser;
|
||||
if (typeof renderToDom === "function") PRIMITIVES["render-to-dom"] = renderToDom;
|
||||
|
||||
// =========================================================================
|
||||
// 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");
|
||||
}
|
||||
|
||||
|
||||
// Parser — compiled from parser.sx (see PLATFORM_PARSER_JS for ident char classes)
|
||||
var parse = sxParse;
|
||||
|
||||
@@ -4007,20 +4440,13 @@ callExpr.push(dictGet(kwargs, k)); } }
|
||||
renderComponent: typeof sxRenderComponent === "function" ? sxRenderComponent : null,
|
||||
getEnv: function() { return componentEnv; },
|
||||
init: typeof bootInit === "function" ? bootInit : null,
|
||||
scanRefs: scanRefs,
|
||||
transitiveDeps: transitiveDeps,
|
||||
computeAllDeps: computeAllDeps,
|
||||
componentsNeeded: componentsNeeded,
|
||||
pageComponentBundle: pageComponentBundle,
|
||||
pageCssClasses: pageCssClasses,
|
||||
scanIoRefs: scanIoRefs,
|
||||
transitiveIoRefs: transitiveIoRefs,
|
||||
computeAllIoRefs: computeAllIoRefs,
|
||||
componentPure_p: componentPure_p,
|
||||
splitPathSegments: splitPathSegments,
|
||||
parseRoutePattern: parseRoutePattern,
|
||||
matchRoute: matchRoute,
|
||||
findMatchingRoute: findMatchingRoute,
|
||||
registerIo: typeof registerIoPrimitive === "function" ? registerIoPrimitive : null,
|
||||
asyncRender: typeof asyncSxRenderWithEnv === "function" ? asyncSxRenderWithEnv : null,
|
||||
asyncRenderToDom: typeof asyncRenderToDom === "function" ? asyncRenderToDom : null,
|
||||
_version: "ref-2.0 (boot+cssx+dom+engine+html+orchestration+parser+sx, bootstrap-compiled)"
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user