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:
@@ -14,7 +14,7 @@
|
||||
// =========================================================================
|
||||
|
||||
var NIL = Object.freeze({ _nil: true, toString: function() { return "nil"; } });
|
||||
var SX_VERSION = "2026-03-07T09:51:42Z";
|
||||
var SX_VERSION = "2026-03-07T17:30:45Z";
|
||||
|
||||
function isNil(x) { return x === NIL || x === null || x === undefined; }
|
||||
function isSxTruthy(x) { return x !== false && !isNil(x); }
|
||||
@@ -2367,6 +2367,21 @@ allKf = concat(allKf, styleValueKeyframes_(sv)); } }
|
||||
processElements(el);
|
||||
return sxHydrateElements(el);
|
||||
})() : NIL);
|
||||
})(); };
|
||||
|
||||
// resolve-suspense
|
||||
var resolveSuspense = function(id, sx) { return (function() {
|
||||
var el = domQuery((String("[data-suspense=\"") + String(id) + String("\"]")));
|
||||
return (isSxTruthy(el) ? (function() {
|
||||
var ast = parse(sx);
|
||||
var env = getRenderEnv(NIL);
|
||||
var node = renderToDom(ast, env, NIL);
|
||||
domSetTextContent(el, "");
|
||||
domAppend(el, node);
|
||||
processElements(el);
|
||||
sxHydrateElements(el);
|
||||
return domDispatch(el, "sx:resolved", {"id": id});
|
||||
})() : logWarn((String("resolveSuspense: no element for id=") + String(id))));
|
||||
})(); };
|
||||
|
||||
// sx-hydrate-elements
|
||||
@@ -4492,6 +4507,7 @@ callExpr.push(dictGet(kwargs, k)); } }
|
||||
update: typeof sxUpdateElement === "function" ? sxUpdateElement : null,
|
||||
renderComponent: typeof sxRenderComponent === "function" ? sxRenderComponent : null,
|
||||
getEnv: function() { return componentEnv; },
|
||||
resolveSuspense: typeof resolveSuspense === "function" ? resolveSuspense : null,
|
||||
init: typeof bootInit === "function" ? bootInit : null,
|
||||
splitPathSegments: splitPathSegments,
|
||||
parseRoutePattern: parseRoutePattern,
|
||||
@@ -4514,7 +4530,18 @@ callExpr.push(dictGet(kwargs, k)); } }
|
||||
|
||||
// --- Auto-init ---
|
||||
if (typeof document !== "undefined") {
|
||||
var _sxInit = function() { bootInit(); };
|
||||
var _sxInit = function() {
|
||||
bootInit();
|
||||
// Process any suspense resolutions that arrived before init
|
||||
if (global.__sxPending) {
|
||||
for (var pi = 0; pi < global.__sxPending.length; pi++) {
|
||||
resolveSuspense(global.__sxPending[pi].id, global.__sxPending[pi].sx);
|
||||
}
|
||||
global.__sxPending = null;
|
||||
}
|
||||
// Set up direct resolution for future chunks
|
||||
global.__sxResolve = function(id, sx) { resolveSuspense(id, sx); };
|
||||
};
|
||||
if (document.readyState === "loading") {
|
||||
document.addEventListener("DOMContentLoaded", _sxInit);
|
||||
} else {
|
||||
|
||||
Reference in New Issue
Block a user