Step 17: streaming render — hyperscript enhancements, WASM builds, live server tests

Streaming chunked transfer with shell-first suspense and resolve scripts.
Hyperscript parser/compiler/runtime expanded for conformance. WASM static
assets added to OCaml host. Playwright streaming and page-level test suites.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-12 08:41:38 +00:00
parent 7aefe4da8f
commit 6e27442d57
29 changed files with 65959 additions and 628 deletions

View File

@@ -635,6 +635,13 @@
renderToHtml: function(expr) { return K.renderToHtml(expr); },
callFn: function(fn, args) { return K.callFn(fn, args); },
engine: function() { return K.engine(); },
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);
}
},
// Boot entry point (called by auto-init or manually)
init: function() {
if (typeof K.eval === "function") {
@@ -682,51 +689,34 @@
var scrollY = (state && state.scrollY) ? state.scrollY : 0;
K.eval("(handle-popstate " + scrollY + ")");
});
// Process any streaming suspense resolutions that arrived before boot
if (globalThis.__sxPending) {
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");
}
},
// Resolve a streaming suspense placeholder via the SX kernel.
// boot.sx defines resolve-suspense but its imports may fail in WASM,
// so we define it here as a fallback using primitives that DO load.
resolveSuspense: function(id, sx) {
try {
// Ensure resolve-suspense exists (boot.sx imports may not have loaded)
if (!Sx._resolveReady) {
try { K.eval('(type-of resolve-suspense)'); } catch(_) {
K.eval('(define resolve-suspense (fn (id sx) (let ((el (dom-query (str "[data-suspense=\\"" id "\\"]")))) (when el (let ((exprs (sx-parse sx)) (env (get-render-env nil))) (dom-set-text-content el "") (for-each (fn (expr) (dom-append el (render-to-dom expr env nil))) exprs))))))');
}
Sx._resolveReady = true;
}
K.eval('(resolve-suspense "' + id.replace(/\\/g, '\\\\').replace(/"/g, '\\"') + '" "' + sx.replace(/\\/g, '\\\\').replace(/"/g, '\\"') + '")');
} catch(e) {
console.error("[sx] resolveSuspense error for id=" + id, e);
}
}
};
// ================================================================
// Auto-init: load web stack and boot on DOMContentLoaded
// Auto-init: load web stack eagerly, boot on DOMContentLoaded
// ================================================================
if (typeof document !== "undefined") {
loadWebStack();
// Pre-process component scripts eagerly so resolve-suspense doesn't
// hit the 'Undefined symbol: default' error on first call.
// The error occurs during component loading but is non-fatal.
try { K.eval("(process-sx-scripts nil)"); } catch(e) {}
var _doInit = function() {
loadWebStack();
Sx.init();
// Enable JIT after all boot code has run.
// Lazy-load the compiler first — JIT needs it to compile functions.
// Drain streaming resolves that arrived before boot or were deferred on error
if (window.__sxPending) {
for (var pi = 0; pi < window.__sxPending.length; pi++) {
Sx.resolveSuspense(window.__sxPending[pi].id, window.__sxPending[pi].sx);
}
window.__sxPending = null;
}
window.__sxResolve = function(id, sx) { Sx.resolveSuspense(id, sx); };
setTimeout(function() {
if (K.beginModuleLoad) K.beginModuleLoad();
loadLibrary("sx compiler", {});