22
33// TODO: Explicitly import from client.browser
44// eslint-disable-next-line import/no-extraneous-dependencies
5- import { createFromReadableStream as createFromReadableStreamBrowser } from 'react-server-dom-webpack/client'
5+ import {
6+ createFromReadableStream as createFromReadableStreamBrowser ,
7+ createFromFetch as createFromFetchBrowser ,
8+ } from 'react-server-dom-webpack/client'
69
710import type {
811 FlightRouterState ,
@@ -36,6 +39,8 @@ import { urlToUrlWithoutFlightMarker } from '../../route-params'
3639
3740const createFromReadableStream =
3841 createFromReadableStreamBrowser as ( typeof import ( 'react-server-dom-webpack/client.browser' ) ) [ 'createFromReadableStream' ]
42+ const createFromFetch =
43+ createFromFetchBrowser as ( typeof import ( 'react-server-dom-webpack/client.browser' ) ) [ 'createFromFetch' ]
3944
4045let createDebugChannel :
4146 | typeof import ( '../../dev/debug-channel' ) . createDebugChannel
@@ -170,10 +175,17 @@ export async function fetchServerResponse(
170175 }
171176 }
172177
173- const res = await createFetch (
178+ // Typically, during a navigation, we decode the response using Flight's
179+ // `createFromFetch` API, which accepts a `fetch` promise.
180+ // TODO: Remove this check once the old PPR flag is removed
181+ const isLegacyPPR =
182+ process . env . __NEXT_PPR && ! process . env . __NEXT_CACHE_COMPONENTS
183+ const shouldImmediatelyDecode = ! isLegacyPPR
184+ const res = await createFetch < NavigationFlightResponse > (
174185 url ,
175186 headers ,
176187 fetchPriority ,
188+ shouldImmediatelyDecode ,
177189 abortController . signal
178190 )
179191
@@ -221,24 +233,34 @@ export async function fetchServerResponse(
221233 ) . waitForWebpackRuntimeHotUpdate ( )
222234 }
223235
224- // Handle the `fetch` readable stream that can be unwrapped by `React.use`.
225- const flightStream = postponed
226- ? createUnclosingPrefetchStream ( res . body )
227- : res . body
228- const response = await ( createFromNextReadableStream (
229- flightStream ,
230- res . headers
231- ) as Promise < NavigationFlightResponse > )
236+ let flightResponsePromise = res . flightResponse
237+ if ( flightResponsePromise === null ) {
238+ // Typically, `createFetch` would have already started decoding the
239+ // Flight response. If it hasn't, though, we need to decode it now.
240+ // TODO: This should only be reachable if legacy PPR is enabled (i.e. PPR
241+ // without Cache Components). Remove this branch once legacy PPR
242+ // is deleted.
243+ const flightStream = postponed
244+ ? createUnclosingPrefetchStream ( res . body )
245+ : res . body
246+ flightResponsePromise =
247+ createFromNextReadableStream < NavigationFlightResponse > (
248+ flightStream ,
249+ res . headers
250+ )
251+ }
252+
253+ const flightResponse = await flightResponsePromise
232254
233- if ( getAppBuildId ( ) !== response . b ) {
255+ if ( getAppBuildId ( ) !== flightResponse . b ) {
234256 return doMpaNavigation ( res . url )
235257 }
236258
237259 return {
238- flightData : normalizeFlightData ( response . f ) ,
260+ flightData : normalizeFlightData ( flightResponse . f ) ,
239261 canonicalUrl : canonicalUrl ,
240262 couldBeIntercepted : interception ,
241- prerendered : response . S ,
263+ prerendered : flightResponse . S ,
242264 postponed,
243265 staleTime,
244266 }
@@ -269,21 +291,23 @@ export async function fetchServerResponse(
269291// the codebase. For example, there's some custom logic for manually following
270292// redirects, so "redirected" in this type could be a composite of multiple
271293// browser fetch calls; however, this fact should not leak to the caller.
272- export type RSCResponse = {
294+ export type RSCResponse < T > = {
273295 ok : boolean
274296 redirected : boolean
275297 headers : Headers
276298 body : ReadableStream < Uint8Array > | null
277299 status : number
278300 url : string
301+ flightResponse : ( Promise < T > & { _debugInfo ?: Array < any > } ) | null
279302}
280303
281- export async function createFetch (
304+ export async function createFetch < T > (
282305 url : URL ,
283306 headers : RequestHeaders ,
284307 fetchPriority : 'auto' | 'high' | 'low' | null ,
308+ shouldImmediatelyDecode : boolean ,
285309 signal ?: AbortSignal
286- ) : Promise < RSCResponse > {
310+ ) : Promise < RSCResponse < T > > {
287311 // TODO: In output: "export" mode, the headers do nothing. Omit them (and the
288312 // cache busting search param) from the request so they're
289313 // maximally cacheable.
@@ -312,7 +336,21 @@ export async function createFetch(
312336 // track them separately.
313337 let fetchUrl = new URL ( url )
314338 setCacheBustingSearchParam ( fetchUrl , headers )
315- let browserResponse = await fetch ( fetchUrl , fetchOptions )
339+ let fetchPromise = fetch ( fetchUrl , fetchOptions )
340+ // Immediately pass the fetch promise to the Flight client so that the debug
341+ // info includes the latency from the client to the server. The internal timer
342+ // in React starts as soon as `createFromFetch` is called.
343+ //
344+ // The only case where we don't do this is during a prefetch, because we have
345+ // to do some extra processing of the response stream (see
346+ // `createUnclosingPrefetchStream`). But this is fine, because a top-level
347+ // prefetch response never blocks a navigation; if it hasn't already been
348+ // written into the cache by the time the navigation happens, the router will
349+ // go straight to a dynamic request.
350+ let flightResponsePromise = shouldImmediatelyDecode
351+ ? createFromNextFetch < T > ( fetchPromise )
352+ : null
353+ let browserResponse = await fetchPromise
316354
317355 // If the server responds with a redirect (e.g. 307), and the redirected
318356 // location does not contain the cache busting search param set in the
@@ -365,9 +403,14 @@ export async function createFetch(
365403 //
366404 // Append the cache busting search param to the redirected URL and
367405 // fetch again.
406+ // TODO: We should abort the previous request.
368407 fetchUrl = new URL ( responseUrl )
369408 setCacheBustingSearchParam ( fetchUrl , headers )
370- browserResponse = await fetch ( fetchUrl , fetchOptions )
409+ fetchPromise = fetch ( fetchUrl , fetchOptions )
410+ flightResponsePromise = shouldImmediatelyDecode
411+ ? createFromNextFetch < T > ( fetchPromise )
412+ : null
413+ browserResponse = await fetchPromise
371414 // We just performed a manual redirect, so this is now true.
372415 redirected = true
373416 }
@@ -378,7 +421,7 @@ export async function createFetch(
378421 const responseUrl = new URL ( browserResponse . url , fetchUrl )
379422 responseUrl . searchParams . delete ( NEXT_RSC_UNION_QUERY )
380423
381- const rscResponse : RSCResponse = {
424+ const rscResponse : RSCResponse < T > = {
382425 url : responseUrl . href ,
383426
384427 // This is true if any redirects occurred, either automatically by the
@@ -394,22 +437,36 @@ export async function createFetch(
394437 headers : browserResponse . headers ,
395438 body : browserResponse . body ,
396439 status : browserResponse . status ,
440+
441+ // This is the exact promise returned by `createFromFetch`. It contains
442+ // debug information that we need to transfer to any derived promises that
443+ // are later rendered by React.
444+ flightResponse : flightResponsePromise ,
397445 }
398446
399447 return rscResponse
400448}
401449
402- export function createFromNextReadableStream (
450+ export function createFromNextReadableStream < T > (
403451 flightStream : ReadableStream < Uint8Array > ,
404452 responseHeaders : Headers
405- ) : Promise < unknown > {
453+ ) : Promise < T > & { _debugInfo ?: Array < any > } {
406454 return createFromReadableStream ( flightStream , {
407455 callServer,
408456 findSourceMapURL,
409457 debugChannel : createDebugChannel && createDebugChannel ( responseHeaders ) ,
410458 } )
411459}
412460
461+ function createFromNextFetch < T > (
462+ promiseForResponse : Promise < Response >
463+ ) : Promise < T > & { _debugInfo ?: Array < any > } {
464+ return createFromFetch ( promiseForResponse , {
465+ callServer,
466+ findSourceMapURL,
467+ } )
468+ }
469+
413470function createUnclosingPrefetchStream (
414471 originalFlightStream : ReadableStream < Uint8Array >
415472) : ReadableStream < Uint8Array > {
0 commit comments