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 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 isNil(x) { return x === NIL || x === null || x === undefined; }
|
||||||
function isSxTruthy(x) { return x !== false && !isNil(x); }
|
function isSxTruthy(x) { return x !== false && !isNil(x); }
|
||||||
@@ -565,92 +565,6 @@
|
|||||||
return makeThunk(componentBody(comp), local);
|
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
|
// Platform interface — Parser
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
@@ -2116,24 +2030,31 @@ return domAppendToHead(link); }, domQueryAll(container, "link[rel=\"stylesheet\"
|
|||||||
var pageName = get(match, "name");
|
var pageName = get(match, "name");
|
||||||
return (isSxTruthy(sxOr(isNil(contentSrc), isEmpty(contentSrc))) ? (logWarn((String("sx:route no content for ") + String(pathname))), false) : (function() {
|
return (isSxTruthy(sxOr(isNil(contentSrc), isEmpty(contentSrc))) ? (logWarn((String("sx:route no content for ") + String(pathname))), false) : (function() {
|
||||||
var target = resolveRouteTarget(targetSel);
|
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 cacheKey = pageDataCacheKey(pageName, params);
|
||||||
var cached = pageDataCacheGet(cacheKey);
|
var cached = pageDataCacheGet(cacheKey);
|
||||||
return (isSxTruthy(cached) ? (function() {
|
return (isSxTruthy(cached) ? (function() {
|
||||||
var env = merge(closure, params, cached);
|
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);
|
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));
|
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);
|
})() : (logInfo((String("sx:route client+data ") + String(pathname))), resolvePageData(pageName, params, function(data) { pageDataCacheSet(cacheKey, data);
|
||||||
return (function() {
|
return (function() {
|
||||||
var env = merge(closure, params, data);
|
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);
|
var rendered = tryEvalContent(contentSrc, env);
|
||||||
return (isSxTruthy(isNil(rendered)) ? logWarn((String("sx:route data eval failed for ") + String(pathname))) : swapRenderedContent(target, rendered, pathname));
|
return (isSxTruthy(isNil(rendered)) ? logWarn((String("sx:route data eval failed for ") + String(pathname))) : swapRenderedContent(target, rendered, pathname));
|
||||||
|
})());
|
||||||
})(); }), true));
|
})(); }), 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 env = merge(closure, params);
|
||||||
var rendered = tryEvalContent(contentSrc, env);
|
var rendered = tryEvalContent(contentSrc, env);
|
||||||
return (isSxTruthy(isNil(rendered)) ? (logInfo((String("sx:route server (eval failed) ") + String(pathname))), false) : (swapRenderedContent(target, rendered, pathname), true));
|
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
|
// boot-init
|
||||||
var bootInit = function() { return (logInfo((String("sx-browser ") + String(SX_VERSION))), initCssTracking(), initStyleDict(), processPageScripts(), processSxScripts(NIL), sxHydrateElements(NIL), processElements(NIL)); };
|
var bootInit = function() { return (logInfo((String("sx-browser ") + String(SX_VERSION))), initCssTracking(), initStyleDict(), initIoPrimitives(), 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)); };
|
|
||||||
|
|
||||||
|
|
||||||
// === Transpiled from router (client-side route matching) ===
|
// === 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) {
|
function resolvePageData(pageName, params, callback) {
|
||||||
// Platform implementation: fetch page data via HTTP from /sx/data/ endpoint.
|
// Platform implementation: fetch page data via HTTP from /sx/data/ endpoint.
|
||||||
// The spec only knows about resolve-page-data(name, params, callback) —
|
// 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 aser === "function") PRIMITIVES["aser"] = aser;
|
||||||
if (typeof renderToDom === "function") PRIMITIVES["render-to-dom"] = renderToDom;
|
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)
|
// Parser — compiled from parser.sx (see PLATFORM_PARSER_JS for ident char classes)
|
||||||
var parse = sxParse;
|
var parse = sxParse;
|
||||||
|
|
||||||
@@ -4007,20 +4440,13 @@ callExpr.push(dictGet(kwargs, k)); } }
|
|||||||
renderComponent: typeof sxRenderComponent === "function" ? sxRenderComponent : null,
|
renderComponent: typeof sxRenderComponent === "function" ? sxRenderComponent : null,
|
||||||
getEnv: function() { return componentEnv; },
|
getEnv: function() { return componentEnv; },
|
||||||
init: typeof bootInit === "function" ? bootInit : null,
|
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,
|
splitPathSegments: splitPathSegments,
|
||||||
parseRoutePattern: parseRoutePattern,
|
parseRoutePattern: parseRoutePattern,
|
||||||
matchRoute: matchRoute,
|
matchRoute: matchRoute,
|
||||||
findMatchingRoute: findMatchingRoute,
|
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)"
|
_version: "ref-2.0 (boot+cssx+dom+engine+html+orchestration+parser+sx, bootstrap-compiled)"
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -691,6 +691,15 @@ def _build_pages_sx(service: str) -> str:
|
|||||||
deps = components_needed(content_src, _COMPONENT_ENV)
|
deps = components_needed(content_src, _COMPONENT_ENV)
|
||||||
deps_sx = "(" + " ".join(_sx_literal(d) for d in sorted(deps)) + ")"
|
deps_sx = "(" + " ".join(_sx_literal(d) for d in sorted(deps)) + ")"
|
||||||
|
|
||||||
|
# Check if any dep component has IO refs (needs async rendering)
|
||||||
|
from .types import Component as _Comp
|
||||||
|
has_io = "false"
|
||||||
|
for dep_name in deps:
|
||||||
|
comp = _COMPONENT_ENV.get(dep_name)
|
||||||
|
if isinstance(comp, _Comp) and comp.io_refs:
|
||||||
|
has_io = "true"
|
||||||
|
break
|
||||||
|
|
||||||
# Build closure as SX dict
|
# Build closure as SX dict
|
||||||
closure_parts: list[str] = []
|
closure_parts: list[str] = []
|
||||||
for k, v in page_def.closure.items():
|
for k, v in page_def.closure.items():
|
||||||
@@ -703,6 +712,7 @@ def _build_pages_sx(service: str) -> str:
|
|||||||
+ " :path " + _sx_literal(page_def.path)
|
+ " :path " + _sx_literal(page_def.path)
|
||||||
+ " :auth " + _sx_literal(auth)
|
+ " :auth " + _sx_literal(auth)
|
||||||
+ " :has-data " + has_data
|
+ " :has-data " + has_data
|
||||||
|
+ " :has-io " + has_io
|
||||||
+ " :content " + _sx_literal(content_src)
|
+ " :content " + _sx_literal(content_src)
|
||||||
+ " :deps " + deps_sx
|
+ " :deps " + deps_sx
|
||||||
+ " :closure " + closure_sx + "}"
|
+ " :closure " + closure_sx + "}"
|
||||||
|
|||||||
@@ -349,23 +349,15 @@ def components_for_page(page_sx: str, service: str | None = None) -> tuple[str,
|
|||||||
|
|
||||||
needed = components_needed(page_sx, _COMPONENT_ENV)
|
needed = components_needed(page_sx, _COMPONENT_ENV)
|
||||||
|
|
||||||
# Include deps for :data pages whose component trees are fully pure
|
# Include deps for all :data pages so the client can render them.
|
||||||
# (no IO refs). Pages with IO deps must render server-side.
|
# Pages with IO deps use the async render path (Phase 5) — the IO
|
||||||
|
# primitives are proxied via /sx/io/<name>.
|
||||||
if service:
|
if service:
|
||||||
from .pages import get_all_pages
|
from .pages import get_all_pages
|
||||||
for page_def in get_all_pages(service).values():
|
for page_def in get_all_pages(service).values():
|
||||||
if page_def.data_expr is not None and page_def.content_expr is not None:
|
if page_def.data_expr is not None and page_def.content_expr is not None:
|
||||||
content_src = serialize(page_def.content_expr)
|
content_src = serialize(page_def.content_expr)
|
||||||
data_deps = components_needed(content_src, _COMPONENT_ENV)
|
needed |= components_needed(content_src, _COMPONENT_ENV)
|
||||||
# Check if any dep component has IO refs
|
|
||||||
has_io = False
|
|
||||||
for dep_name in data_deps:
|
|
||||||
comp = _COMPONENT_ENV.get(dep_name)
|
|
||||||
if isinstance(comp, Component) and comp.io_refs:
|
|
||||||
has_io = True
|
|
||||||
break
|
|
||||||
if not has_io:
|
|
||||||
needed |= data_deps
|
|
||||||
|
|
||||||
if not needed:
|
if not needed:
|
||||||
return "", ""
|
return "", ""
|
||||||
@@ -416,13 +408,7 @@ def css_classes_for_page(page_sx: str, service: str | None = None) -> set[str]:
|
|||||||
for page_def in get_all_pages(service).values():
|
for page_def in get_all_pages(service).values():
|
||||||
if page_def.data_expr is not None and page_def.content_expr is not None:
|
if page_def.data_expr is not None and page_def.content_expr is not None:
|
||||||
content_src = serialize(page_def.content_expr)
|
content_src = serialize(page_def.content_expr)
|
||||||
data_deps = components_needed(content_src, _COMPONENT_ENV)
|
needed |= components_needed(content_src, _COMPONENT_ENV)
|
||||||
has_io = any(
|
|
||||||
isinstance(_COMPONENT_ENV.get(d), Component) and _COMPONENT_ENV.get(d).io_refs
|
|
||||||
for d in data_deps
|
|
||||||
)
|
|
||||||
if not has_io:
|
|
||||||
needed |= data_deps
|
|
||||||
classes: set[str] = set()
|
classes: set[str] = set()
|
||||||
|
|
||||||
for key, val in _COMPONENT_ENV.items():
|
for key, val in _COMPONENT_ENV.items():
|
||||||
|
|||||||
@@ -331,6 +331,9 @@ def auto_mount_pages(app: Any, service_name: str) -> None:
|
|||||||
if has_data_pages:
|
if has_data_pages:
|
||||||
auto_mount_page_data(app, service_name)
|
auto_mount_page_data(app, service_name)
|
||||||
|
|
||||||
|
# Mount IO proxy endpoint for Phase 5: client-side IO primitives
|
||||||
|
mount_io_endpoint(app, service_name)
|
||||||
|
|
||||||
|
|
||||||
def mount_pages(bp: Any, service_name: str,
|
def mount_pages(bp: Any, service_name: str,
|
||||||
names: set[str] | list[str] | None = None) -> None:
|
names: set[str] | list[str] | None = None) -> None:
|
||||||
@@ -535,3 +538,73 @@ def auto_mount_page_data(app: Any, service_name: str) -> None:
|
|||||||
methods=["GET"],
|
methods=["GET"],
|
||||||
)
|
)
|
||||||
logger.info("Mounted page data endpoint for %s at /sx/data/<page_name>", service_name)
|
logger.info("Mounted page data endpoint for %s at /sx/data/<page_name>", service_name)
|
||||||
|
|
||||||
|
|
||||||
|
def mount_io_endpoint(app: Any, service_name: str) -> None:
|
||||||
|
"""Mount /sx/io/<name> endpoint for client-side IO primitive calls.
|
||||||
|
|
||||||
|
The client can call any allowed IO primitive or page helper via GET/POST.
|
||||||
|
Result is returned as SX wire format (text/sx).
|
||||||
|
|
||||||
|
Falls back to page helpers when the name isn't a global IO primitive,
|
||||||
|
so service-specific functions like ``highlight`` work via the proxy.
|
||||||
|
"""
|
||||||
|
import asyncio as _asyncio
|
||||||
|
from quart import make_response, request, abort as quart_abort
|
||||||
|
from .primitives_io import IO_PRIMITIVES, execute_io
|
||||||
|
from .jinja_bridge import _get_request_context
|
||||||
|
from .parser import serialize
|
||||||
|
|
||||||
|
# Allowlist of IO primitives + page helpers the client may call
|
||||||
|
_ALLOWED_IO = {
|
||||||
|
"highlight", "current-user", "request-arg", "request-path",
|
||||||
|
"htmx-request?", "app-url", "asset-url", "config",
|
||||||
|
}
|
||||||
|
|
||||||
|
async def io_proxy(name: str) -> Any:
|
||||||
|
if name not in _ALLOWED_IO:
|
||||||
|
quart_abort(403)
|
||||||
|
|
||||||
|
# Parse args from query string or JSON body
|
||||||
|
args: list = []
|
||||||
|
kwargs: dict = {}
|
||||||
|
if request.method == "GET":
|
||||||
|
for k, v in request.args.items():
|
||||||
|
if k.startswith("_arg"):
|
||||||
|
args.append(v)
|
||||||
|
else:
|
||||||
|
kwargs[k] = v
|
||||||
|
else:
|
||||||
|
data = await request.get_json(silent=True) or {}
|
||||||
|
args = data.get("args", [])
|
||||||
|
kwargs = data.get("kwargs", {})
|
||||||
|
|
||||||
|
# Try global IO primitives first
|
||||||
|
if name in IO_PRIMITIVES:
|
||||||
|
ctx = _get_request_context()
|
||||||
|
result = await execute_io(name, args, kwargs, ctx)
|
||||||
|
else:
|
||||||
|
# Fall back to page helpers (service-specific functions)
|
||||||
|
helpers = get_page_helpers(service_name)
|
||||||
|
helper_fn = helpers.get(name)
|
||||||
|
if helper_fn is None:
|
||||||
|
quart_abort(404)
|
||||||
|
result = helper_fn(*args, **kwargs) if kwargs else helper_fn(*args)
|
||||||
|
if _asyncio.iscoroutine(result):
|
||||||
|
result = await result
|
||||||
|
|
||||||
|
result_sx = serialize(result) if result is not None else "nil"
|
||||||
|
resp = await make_response(result_sx, 200)
|
||||||
|
resp.content_type = "text/sx; charset=utf-8"
|
||||||
|
return resp
|
||||||
|
|
||||||
|
io_proxy.__name__ = "sx_io_proxy"
|
||||||
|
io_proxy.__qualname__ = "sx_io_proxy"
|
||||||
|
|
||||||
|
app.add_url_rule(
|
||||||
|
"/sx/io/<name>",
|
||||||
|
endpoint="sx_io_proxy",
|
||||||
|
view_func=io_proxy,
|
||||||
|
methods=["GET", "POST"],
|
||||||
|
)
|
||||||
|
logger.info("Mounted IO proxy endpoint for %s at /sx/io/<name>", service_name)
|
||||||
|
|||||||
@@ -344,6 +344,7 @@
|
|||||||
(log-info (str "sx-browser " SX_VERSION))
|
(log-info (str "sx-browser " SX_VERSION))
|
||||||
(init-css-tracking)
|
(init-css-tracking)
|
||||||
(init-style-dict)
|
(init-style-dict)
|
||||||
|
(init-io-primitives)
|
||||||
(process-page-scripts)
|
(process-page-scripts)
|
||||||
(process-sx-scripts nil)
|
(process-sx-scripts nil)
|
||||||
(sx-hydrate-elements nil)
|
(sx-hydrate-elements nil)
|
||||||
|
|||||||
@@ -384,6 +384,7 @@ class JSEmitter:
|
|||||||
"bind-client-route-click": "bindClientRouteClick",
|
"bind-client-route-click": "bindClientRouteClick",
|
||||||
"try-client-route": "tryClientRoute",
|
"try-client-route": "tryClientRoute",
|
||||||
"try-eval-content": "tryEvalContent",
|
"try-eval-content": "tryEvalContent",
|
||||||
|
"try-async-eval-content": "tryAsyncEvalContent",
|
||||||
"url-pathname": "urlPathname",
|
"url-pathname": "urlPathname",
|
||||||
"bind-inline-handler": "bindInlineHandler",
|
"bind-inline-handler": "bindInlineHandler",
|
||||||
"bind-preload": "bindPreload",
|
"bind-preload": "bindPreload",
|
||||||
@@ -476,6 +477,7 @@ class JSEmitter:
|
|||||||
"process-sx-scripts": "processSxScripts",
|
"process-sx-scripts": "processSxScripts",
|
||||||
"process-component-script": "processComponentScript",
|
"process-component-script": "processComponentScript",
|
||||||
"init-style-dict": "initStyleDict",
|
"init-style-dict": "initStyleDict",
|
||||||
|
"init-io-primitives": "initIoPrimitives",
|
||||||
"SX_VERSION": "SX_VERSION",
|
"SX_VERSION": "SX_VERSION",
|
||||||
"boot-init": "bootInit",
|
"boot-init": "bootInit",
|
||||||
"resolve-mount-target": "resolveMountTarget",
|
"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(
|
def compile_ref_to_js(
|
||||||
adapters: list[str] | None = None,
|
adapters: list[str] | None = None,
|
||||||
modules: 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))
|
parts.append(fixups_js(has_html, has_sx, has_dom))
|
||||||
if has_continuations:
|
if has_continuations:
|
||||||
parts.append(CONTINUATIONS_JS)
|
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(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)
|
parts.append(EPILOGUE)
|
||||||
from datetime import datetime, timezone
|
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) {
|
function resolvePageData(pageName, params, callback) {
|
||||||
// Platform implementation: fetch page data via HTTP from /sx/data/ endpoint.
|
// Platform implementation: fetch page data via HTTP from /sx/data/ endpoint.
|
||||||
// The spec only knows about resolve-page-data(name, params, callback) —
|
// 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(' matchRoute: matchRoute,')
|
||||||
api_lines.append(' findMatchingRoute: findMatchingRoute,')
|
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(f' _version: "{version}"')
|
||||||
api_lines.append(' };')
|
api_lines.append(' };')
|
||||||
api_lines.append('')
|
api_lines.append('')
|
||||||
|
|||||||
@@ -648,40 +648,71 @@
|
|||||||
(do (log-warn (str "sx:route target not found: " target-sel)) false)
|
(do (log-warn (str "sx:route target not found: " target-sel)) false)
|
||||||
(if (not (deps-satisfied? match))
|
(if (not (deps-satisfied? match))
|
||||||
(do (log-info (str "sx:route deps miss for " page-name)) false)
|
(do (log-info (str "sx:route deps miss for " page-name)) false)
|
||||||
(if (get match "has-data")
|
(let ((has-io (get match "has-io")))
|
||||||
;; Data page: check cache, else resolve asynchronously
|
(if (get match "has-data")
|
||||||
(let ((cache-key (page-data-cache-key page-name params))
|
;; Data page: check cache, else resolve asynchronously
|
||||||
(cached (page-data-cache-get cache-key)))
|
(let ((cache-key (page-data-cache-key page-name params))
|
||||||
(if cached
|
(cached (page-data-cache-get cache-key)))
|
||||||
;; Cache hit: render immediately
|
(if cached
|
||||||
(let ((env (merge closure params 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)))
|
(rendered (try-eval-content content-src env)))
|
||||||
(if (nil? rendered)
|
(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
|
(do
|
||||||
(log-info (str "sx:route client+cache " pathname))
|
|
||||||
(swap-rendered-content target rendered pathname)
|
(swap-rendered-content target rendered pathname)
|
||||||
true)))
|
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)))))))))))))
|
|
||||||
|
|
||||||
|
|
||||||
(define bind-client-route-link
|
(define bind-client-route-link
|
||||||
@@ -991,6 +1022,8 @@
|
|||||||
;;
|
;;
|
||||||
;; === Client-side routing ===
|
;; === Client-side routing ===
|
||||||
;; (try-eval-content source env) → DOM node or nil (catches eval errors)
|
;; (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
|
;; (url-pathname href) → extract pathname from URL string
|
||||||
;; (resolve-page-data name params cb) → void; resolves data for a named page.
|
;; (resolve-page-data name params cb) → void; resolves data for a named page.
|
||||||
;; Platform decides transport (HTTP, cache, IPC, etc). Calls (cb data-dict)
|
;; Platform decides transport (HTTP, cache, IPC, etc). Calls (cb data-dict)
|
||||||
|
|||||||
62
sx/sx/async-io-demo.sx
Normal file
62
sx/sx/async-io-demo.sx
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
;; Async IO demo — Phase 5 client-side rendering with IO primitives.
|
||||||
|
;;
|
||||||
|
;; This component calls `highlight` inline — an IO primitive that runs
|
||||||
|
;; server-side Python (pygments). When rendered on the server, it
|
||||||
|
;; executes synchronously. When rendered client-side, the async
|
||||||
|
;; renderer proxies the call via /sx/io/highlight and awaits the result.
|
||||||
|
;;
|
||||||
|
;; Open browser console and look for:
|
||||||
|
;; "sx:route client+async" — async render with IO proxy
|
||||||
|
;; "sx:io registered N proxied primitives" — IO proxy initialization
|
||||||
|
|
||||||
|
(defcomp ~async-io-demo-content ()
|
||||||
|
(div :class "space-y-8"
|
||||||
|
(div :class "border-b border-stone-200 pb-6"
|
||||||
|
(h1 :class "text-2xl font-bold text-stone-900" "Async IO Demo")
|
||||||
|
(p :class "mt-2 text-stone-600"
|
||||||
|
"This page calls " (code :class "bg-stone-100 px-1 rounded text-violet-700" "highlight")
|
||||||
|
" inline — an IO primitive. On the server it runs Python (pygments). "
|
||||||
|
"On the client it proxies via " (code :class "bg-stone-100 px-1 rounded text-violet-700" "/sx/io/highlight")
|
||||||
|
" and the async renderer awaits the result."))
|
||||||
|
|
||||||
|
;; Live syntax-highlighted code blocks — each is an IO call
|
||||||
|
(div :class "space-y-6"
|
||||||
|
(h2 :class "text-lg font-semibold text-stone-800" "Live IO: syntax highlighting")
|
||||||
|
|
||||||
|
(div :class "rounded-lg border border-stone-200 bg-white p-5 space-y-3"
|
||||||
|
(h3 :class "text-sm font-medium text-stone-500 uppercase tracking-wide" "SX component definition")
|
||||||
|
(div :class "rounded bg-stone-900 p-4 text-sm overflow-x-auto"
|
||||||
|
(raw! (highlight "(defcomp ~card (&key title subtitle &rest children)\n (div :class \"border rounded-lg p-4 shadow-sm\"\n (h2 :class \"text-lg font-bold\" title)\n (when subtitle\n (p :class \"text-stone-500 text-sm\" subtitle))\n (div :class \"mt-3\" children)))" "lisp"))))
|
||||||
|
|
||||||
|
(div :class "rounded-lg border border-stone-200 bg-white p-5 space-y-3"
|
||||||
|
(h3 :class "text-sm font-medium text-stone-500 uppercase tracking-wide" "Python server code")
|
||||||
|
(div :class "rounded bg-stone-900 p-4 text-sm overflow-x-auto"
|
||||||
|
(raw! (highlight "from shared.sx.pages import mount_io_endpoint\n\n# The IO proxy serves any allowed primitive:\n# GET /sx/io/highlight?_arg0=code&_arg1=lisp\nasync def io_proxy(name):\n result = await execute_io(name, args, kwargs, ctx)\n return serialize(result)" "python"))))
|
||||||
|
|
||||||
|
(div :class "rounded-lg border border-stone-200 bg-white p-5 space-y-3"
|
||||||
|
(h3 :class "text-sm font-medium text-stone-500 uppercase tracking-wide" "JavaScript async renderer")
|
||||||
|
(div :class "rounded bg-stone-900 p-4 text-sm overflow-x-auto"
|
||||||
|
(raw! (highlight "// The async renderer intercepts IO primitive calls\nfunction asyncEval(expr, env) {\n if (IO_PRIMITIVES[head.name]) {\n return asyncEvalIoCall(name, args, env);\n }\n return asyncTrampoline(evalExpr(expr, env));\n}" "javascript")))))
|
||||||
|
|
||||||
|
;; Architecture explanation
|
||||||
|
(div :class "rounded-lg border border-blue-200 bg-blue-50 p-5 space-y-3"
|
||||||
|
(h2 :class "text-lg font-semibold text-blue-900" "How it works")
|
||||||
|
(ol :class "list-decimal list-inside text-blue-800 space-y-2 text-sm"
|
||||||
|
(li "Server renders the page — " (code "highlight") " runs Python pygments directly")
|
||||||
|
(li "Client receives page with component definitions including " (code "~async-io-demo-content"))
|
||||||
|
(li "On client navigation, " (code "has-io") " flag routes to async renderer")
|
||||||
|
(li "Async renderer encounters " (code "(highlight ...)") " — checks " (code "IO_PRIMITIVES"))
|
||||||
|
(li "Proxied call: " (code "fetch(\"/sx/io/highlight?_arg0=...&_arg1=lisp\")"))
|
||||||
|
(li "Server executes, returns SX wire format (quoted HTML string)")
|
||||||
|
(li "Async renderer inserts result via " (code "(raw! ...)") " — renders identically")))
|
||||||
|
|
||||||
|
;; Verification instructions
|
||||||
|
(div :class "rounded-lg border border-amber-200 bg-amber-50 p-4 text-sm space-y-2"
|
||||||
|
(p :class "font-semibold text-amber-800" "How to verify async IO rendering")
|
||||||
|
(ol :class "list-decimal list-inside text-amber-700 space-y-1"
|
||||||
|
(li "Open the browser console (F12)")
|
||||||
|
(li "Navigate to another page (e.g. Data Test)")
|
||||||
|
(li "Click back to this page")
|
||||||
|
(li "Look for: " (code :class "bg-amber-100 px-1 rounded" "sx:route client+async /isomorphism/async-io"))
|
||||||
|
(li "The code blocks should render identically — same syntax highlighting")
|
||||||
|
(li "Check Network tab: you'll see 3 requests to " (code :class "bg-amber-100 px-1 rounded" "/sx/io/highlight"))))))
|
||||||
@@ -107,7 +107,8 @@
|
|||||||
(dict :label "Roadmap" :href "/isomorphism/")
|
(dict :label "Roadmap" :href "/isomorphism/")
|
||||||
(dict :label "Bundle Analyzer" :href "/isomorphism/bundle-analyzer")
|
(dict :label "Bundle Analyzer" :href "/isomorphism/bundle-analyzer")
|
||||||
(dict :label "Routing Analyzer" :href "/isomorphism/routing-analyzer")
|
(dict :label "Routing Analyzer" :href "/isomorphism/routing-analyzer")
|
||||||
(dict :label "Data Test" :href "/isomorphism/data-test")))
|
(dict :label "Data Test" :href "/isomorphism/data-test")
|
||||||
|
(dict :label "Async IO" :href "/isomorphism/async-io")))
|
||||||
|
|
||||||
(define plans-nav-items (list
|
(define plans-nav-items (list
|
||||||
(dict :label "Reader Macros" :href "/plans/reader-macros"
|
(dict :label "Reader Macros" :href "/plans/reader-macros"
|
||||||
|
|||||||
@@ -444,6 +444,17 @@
|
|||||||
:server-time server-time :items items
|
:server-time server-time :items items
|
||||||
:phase phase :transport transport))
|
:phase phase :transport transport))
|
||||||
|
|
||||||
|
(defpage async-io-demo
|
||||||
|
:path "/isomorphism/async-io"
|
||||||
|
:auth :public
|
||||||
|
:layout (:sx-section
|
||||||
|
:section "Isomorphism"
|
||||||
|
:sub-label "Isomorphism"
|
||||||
|
:sub-href "/isomorphism/"
|
||||||
|
:sub-nav (~section-nav :items isomorphism-nav-items :current "Async IO")
|
||||||
|
:selected "Async IO")
|
||||||
|
:content (~async-io-demo-content))
|
||||||
|
|
||||||
;; Wildcard must come AFTER specific routes (first-match routing)
|
;; Wildcard must come AFTER specific routes (first-match routing)
|
||||||
(defpage isomorphism-page
|
(defpage isomorphism-page
|
||||||
:path "/isomorphism/<slug>"
|
:path "/isomorphism/<slug>"
|
||||||
|
|||||||
Reference in New Issue
Block a user