Fix streaming resolve: color dict keys + defer resolveSuspense until after boot
stream-colors dict had green/blue keys but data used emerald/violet — all three slots now render with correct Tailwind color classes. Platform: resolveSuspense must not exist on Sx until boot completes, otherwise bootstrap __sxResolve calls it before web stack loads and resolves silently fail. Moved to post-boot setup so all pre-boot resolves queue in __sxPending and drain correctly. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -40,7 +40,12 @@
|
||||
var obj = args[0], prop = args[1];
|
||||
if (obj == null) return null;
|
||||
var v = obj[prop];
|
||||
return v === undefined ? null : v;
|
||||
if (v === undefined) return null;
|
||||
// Functions can't cross the WASM boundary — return true as a truthy
|
||||
// sentinel so (host-get el "getAttribute") works as a guard.
|
||||
// Use host-call to actually invoke the method.
|
||||
if (typeof v === "function") return true;
|
||||
return v;
|
||||
});
|
||||
|
||||
K.registerNative("host-set!", function(args) {
|
||||
@@ -79,64 +84,47 @@
|
||||
}
|
||||
});
|
||||
|
||||
// IO suspension driver — resumes suspended callFn results (wait, fetch, etc.)
|
||||
if (!window._driveAsync) {
|
||||
window._driveAsync = function driveAsync(result) {
|
||||
if (!result || !result.suspended) return;
|
||||
var req = result.request;
|
||||
var items = req && (req.items || req);
|
||||
var op = items && items[0];
|
||||
var opName = typeof op === "string" ? op : (op && op.name) || String(op);
|
||||
var arg = items && items[1];
|
||||
if (opName === "io-sleep" || opName === "wait") {
|
||||
setTimeout(function() {
|
||||
try { driveAsync(result.resume(null)); } catch(e) { console.error("[sx] driveAsync:", e.message); }
|
||||
}, typeof arg === "number" ? arg : 0);
|
||||
} else if (opName === "io-fetch") {
|
||||
fetch(typeof arg === "string" ? arg : "").then(function(r) { return r.text(); }).then(function(t) {
|
||||
try { driveAsync(result.resume({ok: true, text: t})); } catch(e) { console.error("[sx] driveAsync:", e.message); }
|
||||
});
|
||||
} else if (opName === "io-navigate") {
|
||||
// navigation — don't resume
|
||||
} else {
|
||||
console.warn("[sx] unhandled IO:", opName);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
K.registerNative("host-callback", function(args) {
|
||||
var fn = args[0];
|
||||
// Native JS function (not SX-origin) — pass through
|
||||
if (typeof fn === "function" && fn.__sx_handle === undefined) return fn;
|
||||
// SX callable (has __sx_handle) — wrap as JS function with suspension handling
|
||||
// Native JS function — pass through
|
||||
if (typeof fn === "function") return fn;
|
||||
// SX callable (has __sx_handle) — wrap as JS function
|
||||
if (fn && fn.__sx_handle !== undefined) {
|
||||
return function() {
|
||||
var a = Array.prototype.slice.call(arguments);
|
||||
var result = K.callFn(fn, a);
|
||||
// Handle IO suspension chain (e.g. wait, fetch, navigate)
|
||||
_driveAsync(result);
|
||||
return result;
|
||||
var r = K.callFn(fn, a);
|
||||
if (window._driveAsync) window._driveAsync(r);
|
||||
return r;
|
||||
};
|
||||
}
|
||||
return function() {};
|
||||
});
|
||||
|
||||
/**
|
||||
* Drive an async suspension chain to completion.
|
||||
* When K.callFn returns {suspended: true, request: ..., resume: fn},
|
||||
* handle the IO operation and resume the VM.
|
||||
*/
|
||||
function _driveAsync(result) {
|
||||
if (!result || !result.suspended) return;
|
||||
console.log("[sx] IO suspension:", JSON.stringify(result.request, null, 2));
|
||||
var req = result.request;
|
||||
if (!req) return;
|
||||
|
||||
// req is an SX list — extract items. K returns SX values.
|
||||
var items = req.items || req;
|
||||
var op = (items && items[0]) || req;
|
||||
// Normalize: op might be a string or {name: "..."} symbol
|
||||
var opName = (typeof op === "string") ? op : (op && op.name) || String(op);
|
||||
|
||||
if (opName === "wait" || opName === "io-sleep") {
|
||||
// (wait ms) or (io-sleep ms) — resume after timeout
|
||||
var ms = (items && items[1]) || 0;
|
||||
if (typeof ms !== "number") ms = parseFloat(ms) || 0;
|
||||
console.log("[sx] IO wait: " + ms + "ms, resuming after timeout");
|
||||
setTimeout(function() {
|
||||
try {
|
||||
var resumed = result.resume(null);
|
||||
console.log("[sx] IO resumed:", typeof resumed, resumed && resumed.suspended ? "suspended-again" : "done", JSON.stringify(resumed));
|
||||
_driveAsync(resumed);
|
||||
} catch(e) {
|
||||
console.error("[sx] IO resume error:", e);
|
||||
}
|
||||
}, ms);
|
||||
} else if (opName === "navigate") {
|
||||
// (navigate url) — browser navigation
|
||||
var url = (items && items[1]) || "/";
|
||||
if (typeof url !== "string") url = String(url);
|
||||
window.location.href = url;
|
||||
} else {
|
||||
console.warn("[sx] Unhandled IO suspension in callback:", opName, req);
|
||||
}
|
||||
}
|
||||
|
||||
K.registerNative("host-typeof", function(args) {
|
||||
var obj = args[0];
|
||||
if (obj == null) return "nil";
|
||||
@@ -518,7 +506,7 @@
|
||||
// will see it as already loaded and skip rather than infinite-looping.
|
||||
_loadedLibs[name] = true;
|
||||
|
||||
// Load this module
|
||||
// Load this module (bytecode first, fallback to source)
|
||||
var ok = loadBytecodeFile("sx/" + info.file);
|
||||
if (!ok) {
|
||||
var sxFile = info.file.replace(/\.sxbc$/, '.sx');
|
||||
@@ -570,10 +558,7 @@
|
||||
"sx/adapter-html.sx", "sx/adapter-sx.sx", "sx/adapter-dom.sx",
|
||||
"sx/boot-helpers.sx", "sx/hypersx.sx", "sx/harness.sx",
|
||||
"sx/harness-reactive.sx", "sx/harness-web.sx",
|
||||
"sx/engine.sx", "sx/orchestration.sx",
|
||||
"sx/hs-tokenizer.sx", "sx/hs-parser.sx", "sx/hs-compiler.sx",
|
||||
"sx/hs-runtime.sx", "sx/hs-integration.sx",
|
||||
"sx/boot.sx",
|
||||
"sx/engine.sx", "sx/orchestration.sx", "sx/boot.sx",
|
||||
];
|
||||
if (K.beginModuleLoad) K.beginModuleLoad();
|
||||
for (var i = 0; i < files.length; i++) {
|
||||
@@ -691,19 +676,32 @@
|
||||
"hydrated:", !!islands[j]._sxBoundislandhydrated || !!islands[j]["_sxBound" + "island-hydrated"],
|
||||
"children:", islands[j].children.length);
|
||||
}
|
||||
// Activate _hyperscript compat on elements with _ attribute
|
||||
if (document.querySelector('[_]')) {
|
||||
if (K.beginModuleLoad) K.beginModuleLoad();
|
||||
loadLibrary("hyperscript integration", {});
|
||||
if (K.endModuleLoad) K.endModuleLoad();
|
||||
K.eval("(hs-boot!)");
|
||||
}
|
||||
// Register popstate handler for back/forward navigation
|
||||
window.addEventListener("popstate", function(e) {
|
||||
var state = e.state;
|
||||
var scrollY = (state && state.scrollY) ? state.scrollY : 0;
|
||||
K.eval("(handle-popstate " + scrollY + ")");
|
||||
});
|
||||
// Define resolveSuspense now that boot is complete and web stack is loaded.
|
||||
// Must happen AFTER boot — resolve-suspense needs dom-query, render-to-dom etc.
|
||||
Sx.resolveSuspense = function(id, sx) {
|
||||
try {
|
||||
K.eval('(resolve-suspense ' + JSON.stringify(id) + ' ' + JSON.stringify(sx) + ')');
|
||||
} catch (e) {
|
||||
console.error("[sx] resolveSuspense error for id=" + id, e);
|
||||
}
|
||||
};
|
||||
// Process any streaming suspense resolutions that arrived before boot
|
||||
if (globalThis.__sxPending && globalThis.__sxPending.length > 0) {
|
||||
for (var pi = 0; pi < globalThis.__sxPending.length; pi++) {
|
||||
try {
|
||||
Sx.resolveSuspense(globalThis.__sxPending[pi].id, globalThis.__sxPending[pi].sx);
|
||||
} catch(e) { console.error("[sx] pending resolve error:", e); }
|
||||
}
|
||||
globalThis.__sxPending = null;
|
||||
}
|
||||
// Set up direct resolution for future streaming chunks
|
||||
globalThis.__sxResolve = function(id, sx) { Sx.resolveSuspense(id, sx); };
|
||||
// Signal boot complete
|
||||
document.documentElement.setAttribute("data-sx-ready", "true");
|
||||
console.log("[sx] boot done");
|
||||
|
||||
Reference in New Issue
Block a user