Restore stashed WIP: live streaming plan, forms, CI pipeline, streaming demo
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-07T20:18:37Z";
|
||||
var SX_VERSION = "2026-03-07T21:45:27Z";
|
||||
|
||||
function isNil(x) { return x === NIL || x === null || x === undefined; }
|
||||
function isSxTruthy(x) { return x !== false && !isNil(x); }
|
||||
@@ -2046,7 +2046,7 @@ return domAppendToHead(link); }, domQueryAll(container, "link[rel=\"stylesheet\"
|
||||
if (isSxTruthy(hasIo)) {
|
||||
registerIoDeps(ioDeps);
|
||||
}
|
||||
return (isSxTruthy(get(match, "has-data")) ? (function() {
|
||||
return (isSxTruthy(get(match, "stream")) ? (logInfo((String("sx:route streaming ") + String(pathname))), fetchStreaming(target, pathname, buildRequestHeaders(target, loadedComponentNames(), _cssHash)), true) : (isSxTruthy(get(match, "has-data")) ? (function() {
|
||||
var cacheKey = pageDataCacheKey(pageName, params);
|
||||
var cached = pageDataCacheGet(cacheKey);
|
||||
return (isSxTruthy(cached) ? (function() {
|
||||
@@ -2067,7 +2067,7 @@ return (function() {
|
||||
var env = merge(closure, params);
|
||||
var rendered = tryEvalContent(contentSrc, env);
|
||||
return (isSxTruthy(isNil(rendered)) ? (logInfo((String("sx:route server (eval failed) ") + String(pathname))), false) : (swapRenderedContent(target, rendered, pathname), true));
|
||||
})()));
|
||||
})())));
|
||||
})()));
|
||||
})());
|
||||
})());
|
||||
@@ -3033,6 +3033,134 @@ callExpr.push(dictGet(kwargs, k)); } }
|
||||
}).catch(function() { location.reload(); });
|
||||
}
|
||||
|
||||
function fetchStreaming(target, url, headers) {
|
||||
// Streaming fetch for multi-stream pages.
|
||||
// First chunk = OOB SX swap (shell with skeletons).
|
||||
// Subsequent chunks = __sxResolve script tags filling suspense slots.
|
||||
var opts = { headers: headers };
|
||||
try {
|
||||
var h = new URL(url, location.href).hostname;
|
||||
if (h !== location.hostname &&
|
||||
(h.indexOf(".rose-ash.com") >= 0 || h.indexOf(".localhost") >= 0)) {
|
||||
opts.credentials = "include";
|
||||
}
|
||||
} catch (e) {}
|
||||
|
||||
fetch(url, opts).then(function(resp) {
|
||||
if (!resp.ok || !resp.body) {
|
||||
// Fallback: non-streaming
|
||||
return resp.text().then(function(text) {
|
||||
text = stripComponentScripts(text);
|
||||
text = extractResponseCss(text);
|
||||
text = text.trim();
|
||||
if (text.charAt(0) === "(") {
|
||||
var dom = sxRender(text);
|
||||
var container = document.createElement("div");
|
||||
container.appendChild(dom);
|
||||
processOobSwaps(container, function(t, oob, s) {
|
||||
swapDomNodes(t, oob, s);
|
||||
sxHydrate(t);
|
||||
processElements(t);
|
||||
});
|
||||
var newMain = container.querySelector("#main-panel");
|
||||
morphChildren(target, newMain || container);
|
||||
postSwap(target);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
var reader = resp.body.getReader();
|
||||
var decoder = new TextDecoder();
|
||||
var buffer = "";
|
||||
var initialSwapDone = false;
|
||||
// Regex to match __sxResolve script tags
|
||||
var RESOLVE_START = "<script>window.__sxResolve&&window.__sxResolve(";
|
||||
var RESOLVE_END = ")</script>";
|
||||
|
||||
function processResolveScripts() {
|
||||
// Strip and load any extra component defs before resolve scripts
|
||||
buffer = stripSxScripts(buffer);
|
||||
var idx;
|
||||
while ((idx = buffer.indexOf(RESOLVE_START)) >= 0) {
|
||||
var endIdx = buffer.indexOf(RESOLVE_END, idx);
|
||||
if (endIdx < 0) break; // incomplete, wait for more data
|
||||
var argsStr = buffer.substring(idx + RESOLVE_START.length, endIdx);
|
||||
buffer = buffer.substring(endIdx + RESOLVE_END.length);
|
||||
// argsStr is: "stream-id","sx source"
|
||||
var commaIdx = argsStr.indexOf(",");
|
||||
if (commaIdx >= 0) {
|
||||
try {
|
||||
var id = JSON.parse(argsStr.substring(0, commaIdx));
|
||||
var sx = JSON.parse(argsStr.substring(commaIdx + 1));
|
||||
if (typeof Sx !== "undefined" && Sx.resolveSuspense) {
|
||||
Sx.resolveSuspense(id, sx);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("[sx-ref] resolve parse error:", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function pump() {
|
||||
return reader.read().then(function(result) {
|
||||
buffer += decoder.decode(result.value || new Uint8Array(), { stream: !result.done });
|
||||
|
||||
if (!initialSwapDone) {
|
||||
// Look for the first resolve script — everything before it is OOB content
|
||||
var scriptIdx = buffer.indexOf("<script>window.__sxResolve");
|
||||
// If we found a script tag, or the stream is done, process OOB
|
||||
var oobEnd = scriptIdx >= 0 ? scriptIdx : (result.done ? buffer.length : -1);
|
||||
if (oobEnd >= 0) {
|
||||
var oobContent = buffer.substring(0, oobEnd);
|
||||
buffer = buffer.substring(oobEnd);
|
||||
initialSwapDone = true;
|
||||
|
||||
// Process OOB SX content (same as fetchAndRestore)
|
||||
oobContent = stripComponentScripts(oobContent);
|
||||
// Also strip bare <script type="text/sx"> (extra defs from resolve chunks)
|
||||
oobContent = stripSxScripts(oobContent);
|
||||
oobContent = extractResponseCss(oobContent);
|
||||
oobContent = oobContent.trim();
|
||||
if (oobContent.charAt(0) === "(") {
|
||||
try {
|
||||
var dom = sxRender(oobContent);
|
||||
var container = document.createElement("div");
|
||||
container.appendChild(dom);
|
||||
processOobSwaps(container, function(t, oob, s) {
|
||||
swapDomNodes(t, oob, s);
|
||||
sxHydrate(t);
|
||||
processElements(t);
|
||||
});
|
||||
var newMain = container.querySelector("#main-panel");
|
||||
morphChildren(target, newMain || container);
|
||||
postSwap(target);
|
||||
// Dispatch clientRoute so nav links update active state
|
||||
domDispatch(target, "sx:clientRoute",
|
||||
{ pathname: new URL(url, location.href).pathname });
|
||||
} catch (err) {
|
||||
console.error("[sx-ref] streaming OOB swap error:", err);
|
||||
}
|
||||
}
|
||||
// Process any resolve scripts already in buffer
|
||||
processResolveScripts();
|
||||
}
|
||||
} else {
|
||||
// Process resolve scripts as they arrive
|
||||
processResolveScripts();
|
||||
}
|
||||
|
||||
if (!result.done) return pump();
|
||||
});
|
||||
}
|
||||
|
||||
return pump();
|
||||
}).catch(function(err) {
|
||||
console.error("[sx-ref] streaming fetch error:", err);
|
||||
location.reload();
|
||||
});
|
||||
}
|
||||
|
||||
function fetchPreload(url, headers, cache) {
|
||||
fetch(url, { headers: headers }).then(function(resp) {
|
||||
if (!resp.ok) return;
|
||||
@@ -3497,6 +3625,14 @@ callExpr.push(dictGet(kwargs, k)); } }
|
||||
function(_, defs) { if (SxObj && SxObj.loadComponents) SxObj.loadComponents(defs); return ""; });
|
||||
}
|
||||
|
||||
function stripSxScripts(text) {
|
||||
// Strip <script type="text/sx">...</script> (without data-components).
|
||||
// These contain extra component defs from streaming resolve chunks.
|
||||
var SxObj = typeof Sx !== "undefined" ? Sx : null;
|
||||
return text.replace(/<script[^>]*type="text\/sx"[^>]*>([\s\S]*?)<\/script>/gi,
|
||||
function(_, defs) { if (SxObj && SxObj.loadComponents) SxObj.loadComponents(defs); return ""; });
|
||||
}
|
||||
|
||||
function extractResponseCss(text) {
|
||||
if (!_hasDom) return text;
|
||||
var target = document.getElementById("sx-css");
|
||||
|
||||
Reference in New Issue
Block a user