rwsdk
Version:
Build fast, server-driven webapps on Cloudflare with SSR, RSC, and realtime
144 lines (143 loc) • 6.24 kB
JavaScript
/**
* A utility to orchestrate and interleave two ReadableStreams (a document shell and an app shell)
* based on a set of markers within their content. This is designed to solve a specific
* race condition in streaming Server-Side Rendering (SSR) with Suspense.
*
* The logic is as follows:
* 1. Stream the document until a start marker is found.
* 2. Switch to the app stream and stream it until an end marker is found. This is the non-suspended shell.
* 3. Switch back to the document stream and stream it until the closing body tag. This sends the client script.
* 4. Switch back to the app stream and stream the remainder (the suspended content).
* 5. Switch back to the document stream and stream the remainder (closing body and html tags).
*
* @param outerHtml The stream for the document shell (`<Document>`).
* @param innerHtml The stream for the application's content.
* @param startMarker The marker in the document to start injecting the app.
* @param endMarker The marker in the app stream that signals the end of the initial, non-suspended render.
*/
export function stitchDocumentAndAppStreams(outerHtml, innerHtml, startMarker, endMarker) {
const decoder = new TextDecoder();
const encoder = new TextEncoder();
let outerReader;
let innerReader;
let buffer = "";
let outerBufferRemains = "";
let phase = "outer-head";
const pump = async (controller) => {
try {
if (phase === "outer-head") {
const { done, value } = await outerReader.read();
if (done) {
if (buffer)
controller.enqueue(encoder.encode(buffer));
controller.close();
return;
}
buffer += decoder.decode(value, { stream: true });
const markerIndex = buffer.indexOf(startMarker);
if (markerIndex !== -1) {
controller.enqueue(encoder.encode(buffer.slice(0, markerIndex)));
outerBufferRemains = buffer.slice(markerIndex + startMarker.length);
buffer = "";
phase = "inner-shell";
}
else {
const flushIndex = buffer.lastIndexOf("\n");
if (flushIndex !== -1) {
controller.enqueue(encoder.encode(buffer.slice(0, flushIndex + 1)));
buffer = buffer.slice(flushIndex + 1);
}
}
}
else if (phase === "inner-shell") {
const { done, value } = await innerReader.read();
if (done) {
if (buffer)
controller.enqueue(encoder.encode(buffer));
phase = "outer-tail";
}
else {
buffer += decoder.decode(value, { stream: true });
const markerIndex = buffer.indexOf(endMarker);
if (markerIndex !== -1) {
const endOfMarkerIndex = markerIndex + endMarker.length;
controller.enqueue(encoder.encode(buffer.slice(0, endOfMarkerIndex)));
buffer = buffer.slice(endOfMarkerIndex);
phase = "outer-tail";
}
else {
const flushIndex = buffer.lastIndexOf("\n");
if (flushIndex !== -1) {
controller.enqueue(encoder.encode(buffer.slice(0, flushIndex + 1)));
buffer = buffer.slice(flushIndex + 1);
}
}
}
}
else if (phase === "outer-tail") {
if (outerBufferRemains) {
buffer = outerBufferRemains;
outerBufferRemains = "";
}
const { done, value } = await outerReader.read();
if (done) {
if (buffer)
controller.enqueue(encoder.encode(buffer));
phase = "inner-suspended";
}
else {
buffer += decoder.decode(value, { stream: true });
const markerIndex = buffer.indexOf("</body>");
if (markerIndex !== -1) {
controller.enqueue(encoder.encode(buffer.slice(0, markerIndex)));
buffer = buffer.slice(markerIndex);
phase = "inner-suspended";
}
else {
const flushIndex = buffer.lastIndexOf("\n");
if (flushIndex !== -1) {
controller.enqueue(encoder.encode(buffer.slice(0, flushIndex + 1)));
buffer = buffer.slice(flushIndex + 1);
}
}
}
}
else if (phase === "inner-suspended") {
const { done, value } = await innerReader.read();
if (done) {
phase = "outer-end";
}
else {
controller.enqueue(value);
}
}
else if (phase === "outer-end") {
if (buffer) {
controller.enqueue(encoder.encode(buffer));
buffer = "";
}
const { done, value } = await outerReader.read();
if (done) {
controller.close();
return;
}
controller.enqueue(value);
}
await pump(controller);
}
catch (e) {
controller.error(e);
}
};
return new ReadableStream({
start(controller) {
outerReader = outerHtml.getReader();
innerReader = innerHtml.getReader();
pump(controller).catch((e) => controller.error(e));
},
cancel(reason) {
outerReader?.cancel(reason);
innerReader?.cancel(reason);
},
});
}