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 * * Loading...

}> * *
*
* ``` */ 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(`${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(`.*?`, "s"), () => content); let html = buffer ? "" : ` ((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 };