diff --git a/packages/next/client/app-index.tsx b/packages/next/client/app-index.tsx index a1631e60685a0a3..84fa2cb088d5ce9 100644 --- a/packages/next/client/app-index.tsx +++ b/packages/next/client/app-index.tsx @@ -56,7 +56,9 @@ let initialServerDataWriter: ReadableStreamDefaultController | undefined = let initialServerDataLoaded = false let initialServerDataFlushed = false -function nextServerDataCallback(seg: [number, string, string]) { +function nextServerDataCallback( + seg: [isBootStrap: 0] | [isNotBootstrap: 1, responsePartial: string] +): void { if (seg[0] === 0) { initialServerDataBuffer = [] } else { @@ -64,9 +66,9 @@ function nextServerDataCallback(seg: [number, string, string]) { throw new Error('Unexpected server data: missing bootstrap script.') if (initialServerDataWriter) { - initialServerDataWriter.enqueue(encoder.encode(seg[2])) + initialServerDataWriter.enqueue(encoder.encode(seg[1])) } else { - initialServerDataBuffer.push(seg[2]) + initialServerDataBuffer.push(seg[1]) } } } diff --git a/packages/next/server/app-render.tsx b/packages/next/server/app-render.tsx index 7fe57797fb4c157..c24206ba2420561 100644 --- a/packages/next/server/app-render.tsx +++ b/packages/next/server/app-render.tsx @@ -54,8 +54,6 @@ function interopDefault(mod: any) { return mod.default || mod } -const rscCache = new Map() - // Shadowing check does not work with TypeScript enums // eslint-disable-next-line no-shadow const enum RecordStatus { @@ -134,57 +132,59 @@ function preloadDataFetchingRecord( */ function useFlightResponse( writable: WritableStream, - cachePrefix: string, req: ReadableStream, serverComponentManifest: any, + flightResponseRef: { + current: ReturnType | null + }, nonce?: string ) { - const id = cachePrefix + ',' + (React as any).useId() - let entry = rscCache.get(id) - if (!entry) { - const [renderStream, forwardStream] = readableStreamTee(req) - entry = createFromReadableStream(renderStream, { - moduleMap: serverComponentManifest.__ssr_module_mapping__, - }) - rscCache.set(id, entry) - - let bootstrapped = false - // We only attach CSS chunks to the inlined data. - const forwardReader = forwardStream.getReader() - const writer = writable.getWriter() - const startScriptTag = nonce - ? `` - ) + if (flightResponseRef.current) { + return flightResponseRef.current + } + + const [renderStream, forwardStream] = readableStreamTee(req) + flightResponseRef.current = createFromReadableStream(renderStream, { + moduleMap: serverComponentManifest.__ssr_module_mapping__, + }) + + let bootstrapped = false + // We only attach CSS chunks to the inlined data. + const forwardReader = forwardStream.getReader() + const writer = writable.getWriter() + const startScriptTag = nonce + ? `` ) - } - if (done) { - rscCache.delete(id) - writer.close() - } else { - const responsePartial = decodeText(value) - const scripts = `${startScriptTag}(self.__next_s=self.__next_s||[]).push(${htmlEscapeJsonString( - JSON.stringify([1, id, responsePartial]) - )})` - - writer.write(encodeText(scripts)) - process() - } - }) - } - process() + ) + } + if (done) { + flightResponseRef.current = null + writer.close() + } else { + const responsePartial = decodeText(value) + const scripts = `${startScriptTag}(self.__next_s=self.__next_s||[]).push(${htmlEscapeJsonString( + JSON.stringify([1, responsePartial]) + )})` + + writer.write(encodeText(scripts)) + process() + } + }) } - return entry + process() + + return flightResponseRef.current } /** @@ -200,12 +200,10 @@ function createServerComponentRenderer( } }, { - cachePrefix, transformStream, serverComponentManifest, serverContexts, }: { - cachePrefix: string transformStream: TransformStream serverComponentManifest: NonNullable serverContexts: Array< @@ -240,14 +238,16 @@ function createServerComponentRenderer( return RSCStream } + const flightResponseRef = { current: null } + const writable = transformStream.writable return function ServerComponentWrapper() { const reqStream = createRSCStream() const response = useFlightResponse( writable, - cachePrefix, reqStream, serverComponentManifest, + flightResponseRef, nonce ) return response.readRoot() @@ -1110,7 +1110,6 @@ export async function renderToHTMLOrFlight( }, ComponentMod, { - cachePrefix: initialCanonicalUrl, transformStream: serverComponentsInlinedTransformStream, serverComponentManifest, serverContexts, diff --git a/test/e2e/app-dir/app/app/loading-bug/[categorySlug]/loading.js b/test/e2e/app-dir/app/app/loading-bug/[categorySlug]/loading.js new file mode 100644 index 000000000000000..f1ca6af341511b2 --- /dev/null +++ b/test/e2e/app-dir/app/app/loading-bug/[categorySlug]/loading.js @@ -0,0 +1,3 @@ +export default function Loading() { + return

Loading...

+} diff --git a/test/e2e/app-dir/app/app/loading-bug/[categorySlug]/page.server.js b/test/e2e/app-dir/app/app/loading-bug/[categorySlug]/page.server.js new file mode 100644 index 000000000000000..9352955a50433cc --- /dev/null +++ b/test/e2e/app-dir/app/app/loading-bug/[categorySlug]/page.server.js @@ -0,0 +1,15 @@ +// @ts-ignore +import { experimental_use as use } from 'react' + +const fetchCategory = async (categorySlug) => { + // artificial delay + await new Promise((resolve) => setTimeout(resolve, 3000)) + + return categorySlug + 'abc' +} + +export default function Page({ params }) { + const category = use(fetchCategory(params.categorySlug)) + + return
{category}
+} diff --git a/test/e2e/app-dir/index.test.ts b/test/e2e/app-dir/index.test.ts index 4cd2ec592a71e56..6af635e765ed484 100644 --- a/test/e2e/app-dir/index.test.ts +++ b/test/e2e/app-dir/index.test.ts @@ -1419,6 +1419,21 @@ describe('app dir', () => { expect(errors).toInclude('Error during SSR') }) }) + + describe('known bugs', () => { + it('should not share flight data between requests', async () => { + const fetches = await Promise.all( + [...new Array(5)].map(() => + renderViaHTTP(next.url, '/loading-bug/electronics') + ) + ) + + for (const text of fetches) { + const $ = cheerio.load(text) + expect($('#category-id').text()).toBe('electronicsabc') + } + }) + }) } describe('without assetPrefix', () => {