Files
SnowLuma/.pnpm-store/v11/files/3c/4eaff0ec703c5db7cf15a28e6b0640fa73ef85ad8da0e8e96c8d091f7f90c36126893c3d9db817af7963cfbb4f328d2f678a958f31db60083320b3eed423d3
T
TanTan 2a69b23b47
Dev Build / cleanup (push) Waiting to run
Dev Build / typecheck (push) Waiting to run
docs: 仓库链接指向本镜像仓库
2026-11-13 09:27:07 +08:00

133 lines
5.0 KiB
Plaintext

import { HtmlEscapedCallbackPhase, raw, resolveCallback } from "../utils/html.js";
import { DOM_RENDERER, DOM_STASH } from "./constants.js";
import { captureRenderContext, createContext, useContext } from "./context.js";
import { JSXNode, isUntrustedObject, renderChildren, renderUntrustedObject } from "./base.js";
import { buildDataStack } from "./dom/render.js";
import { Suspense as Suspense$1 } from "./dom/components.js";
import { childrenToString } from "./components.js";
//#region src/jsx/streaming.ts
/**
* @module
* This module enables JSX to supports streaming Response.
*/
/**
* Used to specify nonce for scripts generated by `Suspense` and `ErrorBoundary`.
*
* @example
* ```tsx
* <StreamingContext.Provider value={{ scriptNonce: 'test-nonce' }}>
* <Suspense fallback={<p>Loading...</p>}>
* <Content />
* </Suspense>
* </StreamingContext.Provider>
* ```
*/
const StreamingContext = createContext(null);
let suspenseCounter = 0;
const serializeBoundaryChild = (child) => typeof child === "string" || Array.isArray(child) ? renderChildren([child]) : child == null || typeof child === "boolean" ? "" : isUntrustedObject(child) ? renderUntrustedObject(child) : child.toString();
/**
* @experimental
* `Suspense` is an experimental feature.
* The API might be changed.
*/
const Suspense = async ({ children, fallback }) => {
if (!Array.isArray(children)) children = [children];
const nonce = useContext(StreamingContext)?.scriptNonce;
let resArray = [];
const stackNode = { [DOM_STASH]: [0, []] };
const popNodeStack = () => {
buildDataStack.pop();
};
try {
stackNode[DOM_STASH][0] = 0;
buildDataStack.push([[], stackNode]);
resArray = children.map(serializeBoundaryChild);
} catch (e) {
if (e instanceof Promise) {
const resume = captureRenderContext();
resArray = [e.then(() => resume(() => {
stackNode[DOM_STASH][0] = 0;
buildDataStack.push([[], stackNode]);
return childrenToString(children).finally(popNodeStack);
}))];
} else throw e;
} finally {
popNodeStack();
}
if (resArray.some((res) => res instanceof Promise)) {
const index = suspenseCounter++;
const fallbackStr = await (typeof fallback === "string" || Array.isArray(fallback) ? renderChildren([fallback]) : isUntrustedObject(fallback) ? renderUntrustedObject(fallback) : fallback.toString());
return raw(`<template id="H:${index}"></template>${fallbackStr}<!--/$-->`, [...fallbackStr.callbacks || [], ({ phase, buffer, context }) => {
if (phase === HtmlEscapedCallbackPhase.BeforeStream) return;
return Promise.all(resArray).then(async (htmlArray) => {
htmlArray = htmlArray.flat();
const content = htmlArray.join("");
if (buffer) buffer[0] = buffer[0].replace(new RegExp(`<template id="H:${index}"></template>.*?<!--/\\$-->`, "s"), () => content);
let html = buffer ? "" : `<template data-hono-target="H:${index}">${content}</template><script${nonce ? ` nonce="${nonce}"` : ""}>
((d,c,n) => {
c=d.currentScript.previousSibling
d=d.getElementById('H:${index}')
if(!d)return
do{n=d.nextSibling;n.remove()}while(n.nodeType!=8||n.nodeValue!='/$')
d.replaceWith(c.content)
})(document)
<\/script>`;
const callbacks = htmlArray.map((html) => html.callbacks || []).flat();
if (!callbacks.length) return html;
if (phase === HtmlEscapedCallbackPhase.Stream) html = await resolveCallback(html, HtmlEscapedCallbackPhase.BeforeStream, true, context);
return raw(html, callbacks);
});
}]);
} else return raw(resArray.join(""));
};
Suspense[DOM_RENDERER] = Suspense$1;
const textEncoder = new TextEncoder();
/**
* @experimental
* `renderToReadableStream()` is an experimental feature.
* The API might be changed.
*/
const renderToReadableStream = (content, onError = console.trace) => {
let cancelled = false;
return new ReadableStream({
async start(controller) {
try {
if (content instanceof JSXNode) content = content.toString();
const context = typeof content === "object" ? content : {};
const resolved = await resolveCallback(content, HtmlEscapedCallbackPhase.BeforeStream, true, context);
if (!cancelled) controller.enqueue(textEncoder.encode(resolved));
let resolvedCount = 0;
const callbacks = [];
const then = (promise) => {
callbacks.push(promise.catch((err) => {
console.log(err);
onError(err);
return "";
}).then(async (res) => {
res = await resolveCallback(res, HtmlEscapedCallbackPhase.BeforeStream, true, context);
res.callbacks?.map((c) => c({
phase: HtmlEscapedCallbackPhase.Stream,
context
})).filter(Boolean).forEach(then);
resolvedCount++;
if (!cancelled) controller.enqueue(textEncoder.encode(res));
}));
};
resolved.callbacks?.map((c) => c({
phase: HtmlEscapedCallbackPhase.Stream,
context
})).filter(Boolean).forEach(then);
while (resolvedCount !== callbacks.length) await Promise.all(callbacks);
} catch (e) {
onError(e);
}
if (!cancelled) controller.close();
},
cancel() {
cancelled = true;
}
});
};
//#endregion
export { StreamingContext, Suspense, renderToReadableStream };