UNPKG

hono

Version:

Web framework built on Web Standards

140 lines (139 loc) 4.45 kB
// src/jsx/streaming.ts import { raw } from "../helper/html/index.js"; import { HtmlEscapedCallbackPhase, resolveCallback } from "../utils/html.js"; import { childrenToString } from "./components.js"; import { DOM_RENDERER, DOM_STASH } from "./constants.js"; import { Suspense as SuspenseDomRenderer } from "./dom/components.js"; import { buildDataStack } from "./dom/render.js"; var suspenseCounter = 0; var Suspense = async ({ children, fallback }) => { if (!children) { return fallback.toString(); } if (!Array.isArray(children)) { children = [children]; } let resArray = []; const stackNode = { [DOM_STASH]: [0, []] }; const popNodeStack = (value) => { buildDataStack.pop(); return value; }; try { stackNode[DOM_STASH][0] = 0; buildDataStack.push([[], stackNode]); resArray = children.map( (c) => c == null || typeof c === "boolean" ? "" : c.toString() ); } catch (e) { if (e instanceof Promise) { resArray = [ e.then(() => { stackNode[DOM_STASH][0] = 0; buildDataStack.push([[], stackNode]); return childrenToString(children).then(popNodeStack); }) ]; } else { throw e; } } finally { popNodeStack(); } if (resArray.some((res) => res instanceof Promise)) { const index = suspenseCounter++; const fallbackStr = await 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>.*?<!--/\\$-->`), content ); } let html = buffer ? "" : `<template data-hono-target="H:${index}">${content}</template><script> ((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((html2) => html2.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] = SuspenseDomRenderer; var textEncoder = new TextEncoder(); var renderToReadableStream = (str, onError = console.trace) => { const reader = new ReadableStream({ async start(controller) { try { const tmp = str instanceof Promise ? await str : await str.toString(); const context = typeof tmp === "object" ? tmp : {}; const resolved = await resolveCallback( tmp, HtmlEscapedCallbackPhase.BeforeStream, true, context ); 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++; 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); } controller.close(); } }); return reader; }; export { Suspense, renderToReadableStream };