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:
2026-04-12 09:58:18 +00:00
parent 6e27442d57
commit 56855eee7f
3 changed files with 131 additions and 93 deletions

View File

@@ -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");