Phase 6: Streaming & Suspense — chunked HTML with suspense resolution
Server streams HTML shell with ~suspense placeholders immediately, then sends resolution <script> chunks as async IO completes. Browser renders loading skeletons instantly, replacing them with real content as data arrives via __sxResolve(). - defpage :stream true opts pages into streaming response - ~suspense component renders fallback with data-suspense attr - resolve-suspense in boot.sx (spec) + bootstrapped to sx-browser.js - __sxPending queue handles resolution before sx-browser.js loads - execute_page_streaming() async generator with concurrent IO tasks - Streaming demo page at /isomorphism/streaming with 1.5s simulated delay Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1588,6 +1588,32 @@
|
||||
isTruthy: isSxTruthy,
|
||||
isNil: isNil,
|
||||
|
||||
/**
|
||||
* Resolve a streaming suspense placeholder.
|
||||
* Called by inline <script> tags that arrive during chunked transfer:
|
||||
* __sxResolve("content", "(~article :title \"Hello\")")
|
||||
*
|
||||
* Finds the suspense wrapper by data-suspense attribute, renders the
|
||||
* new SX content, and replaces the wrapper's children.
|
||||
*/
|
||||
resolveSuspense: function (id, sx) {
|
||||
var el = document.querySelector('[data-suspense="' + id + '"]');
|
||||
if (!el) {
|
||||
console.warn("[sx] resolveSuspense: no element for id=" + id);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
var node = Sx.render(sx);
|
||||
el.textContent = "";
|
||||
el.appendChild(node);
|
||||
if (typeof SxEngine !== "undefined") SxEngine.process(el);
|
||||
Sx.hydrate(el);
|
||||
el.dispatchEvent(new CustomEvent("sx:resolved", { bubbles: true, detail: { id: id } }));
|
||||
} catch (e) {
|
||||
console.error("[sx] resolveSuspense error for id=" + id, e);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Mount a sx expression into a DOM element, replacing its contents.
|
||||
* Sx.mount(el, '(~card :title "Hi")')
|
||||
@@ -3164,6 +3190,15 @@
|
||||
Sx.processScripts();
|
||||
Sx.hydrate();
|
||||
SxEngine.process();
|
||||
// Process any streaming suspense resolutions that arrived before init
|
||||
if (global.__sxPending) {
|
||||
for (var pi = 0; pi < global.__sxPending.length; pi++) {
|
||||
Sx.resolveSuspense(global.__sxPending[pi].id, global.__sxPending[pi].sx);
|
||||
}
|
||||
global.__sxPending = null;
|
||||
}
|
||||
// Replace bootstrap resolver with direct calls
|
||||
global.__sxResolve = function (id, sx) { Sx.resolveSuspense(id, sx); };
|
||||
};
|
||||
if (document.readyState === "loading") {
|
||||
document.addEventListener("DOMContentLoaded", init);
|
||||
|
||||
Reference in New Issue
Block a user