deno.land / x / hono@v4.2.5 / jsx / streaming.ts
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180import { raw } from '../helper/html/index.ts'import { HtmlEscapedCallbackPhase, resolveCallback } from '../utils/html.ts'import type { HtmlEscapedString } from '../utils/html.ts'import { childrenToString } from './components.ts'import { DOM_RENDERER, DOM_STASH } from './constants.ts'import { Suspense as SuspenseDomRenderer } from './dom/components.ts'import { buildDataStack } from './dom/render.ts'import type { HasRenderToDom, NodeObject } from './dom/render.ts'import type { FC, PropsWithChildren, Child } from './index.ts'
let suspenseCounter = 0
/** * @experimental * `Suspense` is an experimental feature. * The API might be changed. */// eslint-disable-next-line @typescript-eslint/no-explicit-anyexport const Suspense: FC<PropsWithChildren<{ fallback: any }>> = async ({ children, fallback,}) => { if (!children) { return fallback.toString() } if (!Array.isArray(children)) { children = [children] }
let resArray: HtmlEscapedString[] | Promise<HtmlEscapedString[]>[] = []
// for use() hook const stackNode = { [DOM_STASH]: [0, []] } as unknown as NodeObject const popNodeStack = (value?: unknown) => { buildDataStack.pop() return value }
try { stackNode[DOM_STASH][0] = 0 buildDataStack.push([[], stackNode]) resArray = children.map((c) => c == null || typeof c === 'boolean' ? '' : c.toString() ) as HtmlEscapedString[] } catch (e) { if (e instanceof Promise) { resArray = [ e.then(() => { stackNode[DOM_STASH][0] = 0 buildDataStack.push([[], stackNode]) return childrenToString(children as Child[]).then(popNodeStack) }), ] as Promise<HtmlEscapedString[]>[] } else { throw e } } finally { popNodeStack() }
if (resArray.some((res) => (res as {}) 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.previousSiblingd=d.getElementById('H:${index}')if(!d)returndo{n=d.nextSibling;n.remove()}while(n.nodeType!=8||n.nodeValue!='/$')d.replaceWith(c.content)})(document)</script>`
const callbacks = htmlArray .map((html) => (html as HtmlEscapedString).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 as HasRenderToDom)[DOM_RENDERER] = SuspenseDomRenderer
const textEncoder = new TextEncoder()/** * @experimental * `renderToReadableStream()` is an experimental feature. * The API might be changed. */export const renderToReadableStream = ( str: HtmlEscapedString | Promise<HtmlEscapedString>, onError: (e: unknown) => void = console.trace): ReadableStream<Uint8Array> => { const reader = new ReadableStream<Uint8Array>({ 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: Promise<void>[] = [] const then = (promise: Promise<string>) => { callbacks.push( promise .catch((err) => { console.log(err) onError(err) return '' }) .then(async (res) => { res = await resolveCallback( res, HtmlEscapedCallbackPhase.BeforeStream, true, context ) ;(res as HtmlEscapedString).callbacks ?.map((c) => c({ phase: HtmlEscapedCallbackPhase.Stream, context })) // eslint-disable-next-line @typescript-eslint/no-explicit-any .filter<Promise<string>>(Boolean as any) .forEach(then) resolvedCount++ controller.enqueue(textEncoder.encode(res)) }) ) } ;(resolved as HtmlEscapedString).callbacks ?.map((c) => c({ phase: HtmlEscapedCallbackPhase.Stream, context })) // eslint-disable-next-line @typescript-eslint/no-explicit-any .filter<Promise<string>>(Boolean as any) .forEach(then) while (resolvedCount !== callbacks.length) { await Promise.all(callbacks) } } catch (e) { // maybe the connection was closed onError(e) }
controller.close() }, }) return reader}
Version Info