vite-plugin-react-server
Version:
Vite plugin for React Server Components (RSC)
148 lines (147 loc) • 5.5 kB
JavaScript
/**
* createBufferedRscStream.ts
*
* PURPOSE: Creates a buffered RSC stream factory that can generate multiple readable streams
*
* PROBLEM: Node.js streams can only be consumed once, but in client-side static generation
* we need to consume the RSC stream twice:
* 1. Write RSC content to index.rsc file
* 2. Transform RSC content to HTML for index.html file
*
* SOLUTION: Buffer all RSC chunks and create a factory that can generate multiple
* readable streams from the same buffered data.
*
* USAGE:
* ```typescript
* const bufferedStreamFactory = createBufferedRscStream(rscStream, {
* route: "/",
* logger: myLogger,
* verbose: true
* });
*
* // Create separate streams for each consumer
* const rscStream = bufferedStreamFactory.createStream();
* const htmlStream = bufferedStreamFactory.createStream();
*
* // Can be piped to different destinations
* rscStream.pipe(rscFileWriter);
* htmlStream.pipe(htmlTransform);
* ```
*/
import { Readable } from "node:stream";
/**
* Creates a buffered RSC stream factory that can generate multiple readable streams
*
* @param rscStream - The original RSC stream from the worker
* @param options - Configuration options
* @returns A factory that can create multiple readable streams from the same buffered data
*/
export function createBufferedRscStream(rscStream, options) {
const { route, logger, verbose = false } = options;
if (verbose) {
logger?.info(`[createBufferedRscStream:${route}] Creating buffered RSC stream factory for dual consumption`);
}
// Buffer to store all RSC chunks
const rscBuffer = [];
let totalBytes = 0;
let isStreamEnded = false;
let hasError = false;
let error = null;
const consumers = [];
// Collect all RSC chunks into buffer
rscStream.on("data", (chunk) => {
if (hasError)
return; // Don't buffer if there was an error
rscBuffer.push(chunk);
totalBytes += chunk.length;
if (verbose) {
logger?.info(`[createBufferedRscStream:${route}] Buffered chunk: ${chunk.length} bytes, total: ${totalBytes} bytes`);
}
// Push the chunk to all existing consumers immediately
for (const consumer of consumers) {
if (!consumer.destroyed) {
consumer.push(chunk);
}
}
});
rscStream.on("end", () => {
isStreamEnded = true;
if (verbose) {
logger?.info(`[createBufferedRscStream:${route}] RSC stream ended, buffered ${rscBuffer.length} chunks (${totalBytes} bytes)`);
}
// End all consumers
for (const consumer of consumers) {
if (!consumer.destroyed) {
consumer.push(null);
}
}
});
rscStream.on("error", (streamError) => {
hasError = true;
error = streamError;
if (verbose) {
logger?.error(`[createBufferedRscStream:${route}] RSC stream error: ${streamError.message}`);
}
// Emit error on all consumers
for (const consumer of consumers) {
if (!consumer.destroyed) {
consumer.emit("error", streamError);
}
}
});
return {
createStream() {
if (hasError && error) {
// If there was an error, create a stream that immediately emits the error
const errorStream = new Readable();
setImmediate(() => {
errorStream.emit("error", error);
});
return errorStream;
}
const consumer = new Readable({
read() {
if (verbose) {
logger?.info(`[createBufferedRscStream:${route}] Read requested, buffered chunks: ${rscBuffer.length}, isStreamEnded: ${isStreamEnded}`);
}
// If the stream has already ended, push all remaining buffered content
if (isStreamEnded) {
// Push all buffered content that hasn't been pushed yet
for (const chunk of rscBuffer) {
this.push(chunk);
}
// End the stream
this.push(null);
if (verbose) {
logger?.info(`[createBufferedRscStream:${route}] Pushed ${rscBuffer.length} chunks, ending stream`);
}
}
// If the stream hasn't ended yet, we'll push chunks as they arrive via the 'data' event
}
});
// Add this consumer to the list
consumers.push(consumer);
// If the stream has already ended, push all buffered content immediately
if (isStreamEnded) {
for (const chunk of rscBuffer) {
consumer.push(chunk);
}
consumer.push(null);
}
// Clean up when the consumer is destroyed
consumer.on("close", () => {
const index = consumers.indexOf(consumer);
if (index > -1) {
consumers.splice(index, 1);
}
});
return consumer;
},
getBufferInfo() {
return {
chunks: rscBuffer.length,
bytes: totalBytes
};
}
};
}