119 lines
5.2 KiB
JavaScript
119 lines
5.2 KiB
JavaScript
import { htmlEscapeJsonString } from "../htmlescape";
|
|
const isEdgeRuntime = process.env.NEXT_RUNTIME === "edge";
|
|
const INLINE_FLIGHT_PAYLOAD_BOOTSTRAP = 0;
|
|
const INLINE_FLIGHT_PAYLOAD_DATA = 1;
|
|
const INLINE_FLIGHT_PAYLOAD_FORM_STATE = 2;
|
|
const flightResponses = new WeakMap();
|
|
const encoder = new TextEncoder();
|
|
/**
|
|
* Render Flight stream.
|
|
* This is only used for renderToHTML, the Flight response does not need additional wrappers.
|
|
*/ export function useFlightStream(flightStream, clientReferenceManifest, nonce) {
|
|
const response = flightResponses.get(flightStream);
|
|
if (response) {
|
|
return response;
|
|
}
|
|
// react-server-dom-webpack/client.edge must not be hoisted for require cache clearing to work correctly
|
|
let createFromReadableStream;
|
|
// @TODO: investigate why the aliasing for turbopack doesn't pick this up, requiring this runtime check
|
|
if (process.env.TURBOPACK) {
|
|
createFromReadableStream = // eslint-disable-next-line import/no-extraneous-dependencies
|
|
require("react-server-dom-turbopack/client.edge").createFromReadableStream;
|
|
} else {
|
|
createFromReadableStream = // eslint-disable-next-line import/no-extraneous-dependencies
|
|
require("react-server-dom-webpack/client.edge").createFromReadableStream;
|
|
}
|
|
const newResponse = createFromReadableStream(flightStream, {
|
|
ssrManifest: {
|
|
moduleLoading: clientReferenceManifest.moduleLoading,
|
|
moduleMap: isEdgeRuntime ? clientReferenceManifest.edgeSSRModuleMapping : clientReferenceManifest.ssrModuleMapping
|
|
},
|
|
nonce
|
|
});
|
|
flightResponses.set(flightStream, newResponse);
|
|
return newResponse;
|
|
}
|
|
/**
|
|
* There are times when an SSR render may be finished but the RSC render
|
|
* is ongoing and we need to wait for it to complete to make some determination
|
|
* about how to handle the render. This function will drain the RSC reader and
|
|
* resolve when completed. This will generally require teeing the RSC stream and it
|
|
* should be noted that it will cause all the RSC chunks to queue in the underlying
|
|
* ReadableStream however given Flight currently is a push stream that doesn't respond
|
|
* to backpressure this shouldn't change how much memory is maximally consumed
|
|
*/ export async function flightRenderComplete(flightStream) {
|
|
const flightReader = flightStream.getReader();
|
|
while(true){
|
|
const { done } = await flightReader.read();
|
|
if (done) {
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
/**
|
|
* Creates a ReadableStream provides inline script tag chunks for writing hydration
|
|
* data to the client outside the React render itself.
|
|
*
|
|
* @param flightStream The RSC render stream
|
|
* @param nonce optionally a nonce used during this particular render
|
|
* @param formState optionally the formState used with this particular render
|
|
* @returns a ReadableStream without the complete property. This signifies a lazy ReadableStream
|
|
*/ export function createInlinedDataReadableStream(flightStream, nonce, formState) {
|
|
const startScriptTag = nonce ? `<script nonce=${JSON.stringify(nonce)}>` : "<script>";
|
|
const decoder = new TextDecoder("utf-8", {
|
|
fatal: true
|
|
});
|
|
const decoderOptions = {
|
|
stream: true
|
|
};
|
|
const flightReader = flightStream.getReader();
|
|
const readable = new ReadableStream({
|
|
type: "bytes",
|
|
start (controller) {
|
|
try {
|
|
writeInitialInstructions(controller, startScriptTag, formState);
|
|
} catch (error) {
|
|
// during encoding or enqueueing forward the error downstream
|
|
controller.error(error);
|
|
}
|
|
},
|
|
async pull (controller) {
|
|
try {
|
|
const { done, value } = await flightReader.read();
|
|
if (done) {
|
|
const tail = decoder.decode(value, {
|
|
stream: false
|
|
});
|
|
if (tail.length) {
|
|
writeFlightDataInstruction(controller, startScriptTag, tail);
|
|
}
|
|
controller.close();
|
|
} else {
|
|
const chunkAsString = decoder.decode(value, decoderOptions);
|
|
writeFlightDataInstruction(controller, startScriptTag, chunkAsString);
|
|
}
|
|
} catch (error) {
|
|
// There was a problem in the upstream reader or during decoding or enqueuing
|
|
// forward the error downstream
|
|
controller.error(error);
|
|
}
|
|
}
|
|
});
|
|
return readable;
|
|
}
|
|
function writeInitialInstructions(controller, scriptStart, formState) {
|
|
controller.enqueue(encoder.encode(`${scriptStart}(self.__next_f=self.__next_f||[]).push(${htmlEscapeJsonString(JSON.stringify([
|
|
INLINE_FLIGHT_PAYLOAD_BOOTSTRAP
|
|
]))});self.__next_f.push(${htmlEscapeJsonString(JSON.stringify([
|
|
INLINE_FLIGHT_PAYLOAD_FORM_STATE,
|
|
formState
|
|
]))})</script>`));
|
|
}
|
|
function writeFlightDataInstruction(controller, scriptStart, chunkAsString) {
|
|
controller.enqueue(encoder.encode(`${scriptStart}self.__next_f.push(${htmlEscapeJsonString(JSON.stringify([
|
|
INLINE_FLIGHT_PAYLOAD_DATA,
|
|
chunkAsString
|
|
]))})</script>`));
|
|
}
|
|
|
|
//# sourceMappingURL=use-flight-response.js.map
|