hono
Version:
Web framework built on Web Standards
140 lines (139 loc) • 4.45 kB
JavaScript
// 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
};