Step 17: streaming render — chunked transfer, shell-first suspense, resolve scripts

Server (sx_server.ml):
- Chunked HTTP transport (Transfer-Encoding: chunked)
- Streaming page detection via scan_defpages (:stream true)
- Shell-first render: outer layout + shell AST → aser → SSR → flush
- Data resolution: evaluate :data, render :content per slot, flush __sxResolve scripts
- AJAX streaming: synchronous eval + OOB swaps for SPA navigation
- SX URL → flat path conversion for defpage matching
- Error boundaries per resolve section
- streaming-demo-data helper for the demo page

Client (sx-platform.js):
- Sx.resolveSuspense: finds [data-suspense] element, parses SX, renders to DOM
- Fallback define for resolve-suspense when boot.sx imports fail in WASM
- __sxPending drain on boot (queued resolves from before sx.js loads)
- __sxResolve direct dispatch after boot

Tests (streaming.spec.js):
- 5 sandbox tests using real WASM kernel
- Suspense placeholder rendering, __sxResolve replacement, independent slot resolution
- Full layout with gutters, end-to-end resolve with streaming-demo/chunk components

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-11 16:12:28 +00:00
parent ccd89dfa53
commit eaf5af4cd8
3 changed files with 701 additions and 0 deletions

View File

@@ -682,10 +682,38 @@
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);
}
}
};