import { PassThrough } from "stream"; import { renderToPipeableStream } from "react-dom/server"; import { ServerRouter } from "react-router"; import { createReadableStreamFromReadable } from "@react-router/node"; import { type EntryContext } from "react-router"; import { isbot } from "isbot"; import { addDocumentResponseHeaders } from "./shopify.server"; import { startScheduler } from "./lib/scheduler.server"; import { assertEncryptionKey } from "./lib/crypto.server"; // Re-publishes offer configurations at every start/end boundary (brief ยง11.5). if (process.env.NODE_ENV === "production") assertEncryptionKey(); startScheduler(); export const streamTimeout = 5000; export default async function handleRequest( request: Request, responseStatusCode: number, responseHeaders: Headers, reactRouterContext: EntryContext ) { addDocumentResponseHeaders(request, responseHeaders); const userAgent = request.headers.get("user-agent"); const callbackName = isbot(userAgent ?? '') ? "onAllReady" : "onShellReady"; return new Promise((resolve, reject) => { const { pipe, abort } = renderToPipeableStream( , { [callbackName]: () => { const body = new PassThrough(); const stream = createReadableStreamFromReadable(body); responseHeaders.set("Content-Type", "text/html"); resolve( new Response(stream, { headers: responseHeaders, status: responseStatusCode, }) ); pipe(body); }, onShellError(error) { reject(error); }, onError(error) { responseStatusCode = 500; console.error(error); }, } ); // Automatically timeout the React renderer after 6 seconds, which ensures // React has enough time to flush down the rejected boundary contents setTimeout(abort, streamTimeout + 1000); }); }