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 ,
@@ -37,6 +40,8 @@ import { urlToUrlWithoutFlightMarker } from '../../route-params'
3740
3841const createFromReadableStream =
3942 createFromReadableStreamBrowser as ( typeof import ( 'react-server-dom-webpack/client.browser' ) ) [ 'createFromReadableStream' ]
43+ const createFromFetch =
44+ createFromFetchBrowser as ( typeof import ( 'react-server-dom-webpack/client.browser' ) ) [ 'createFromFetch' ]
4045
4146let createDebugChannel :
4247 | typeof import ( '../../dev/debug-channel' ) . createDebugChannel
@@ -65,6 +70,7 @@ export type FetchServerResponseResult = {
6570 prerendered : boolean
6671 postponed : boolean
6772 staleTime : number
73+ debugInfo : Array < any > | null
6874}
6975
7076export type RequestHeaders = {
@@ -91,6 +97,7 @@ function doMpaNavigation(url: string): FetchServerResponseResult {
9197 prerendered : false ,
9298 postponed : false ,
9399 staleTime : - 1 ,
100+ debugInfo : null ,
94101 }
95102}
96103
@@ -175,10 +182,17 @@ export async function fetchServerResponse(
175182 }
176183 }
177184
178- const res = await createFetch (
185+ // Typically, during a navigation, we decode the response using Flight's
186+ // `createFromFetch` API, which accepts a `fetch` promise.
187+ // TODO: Remove this check once the old PPR flag is removed
188+ const isLegacyPPR =
189+ process . env . __NEXT_PPR && ! process . env . __NEXT_CACHE_COMPONENTS
190+ const shouldImmediatelyDecode = ! isLegacyPPR
191+ const res = await createFetch < NavigationFlightResponse > (
179192 url ,
180193 headers ,
181194 fetchPriority ,
195+ shouldImmediatelyDecode ,
182196 abortController . signal
183197 )
184198
@@ -226,26 +240,37 @@ export async function fetchServerResponse(
226240 ) . waitForWebpackRuntimeHotUpdate ( )
227241 }
228242
229- // Handle the `fetch` readable stream that can be unwrapped by `React.use`.
230- const flightStream = postponed
231- ? createUnclosingPrefetchStream ( res . body )
232- : res . body
233- const response = await ( createFromNextReadableStream (
234- flightStream ,
235- headers
236- ) as Promise < NavigationFlightResponse > )
243+ let flightResponsePromise = res . flightResponse
244+ if ( flightResponsePromise === null ) {
245+ // Typically, `createFetch` would have already started decoding the
246+ // Flight response. If it hasn't, though, we need to decode it now.
247+ // TODO: This should only be reachable if legacy PPR is enabled (i.e. PPR
248+ // without Cache Components). Remove this branch once legacy PPR
249+ // is deleted.
250+ const flightStream = postponed
251+ ? createUnclosingPrefetchStream ( res . body )
252+ : res . body
253+ flightResponsePromise =
254+ createFromNextReadableStream < NavigationFlightResponse > (
255+ flightStream ,
256+ headers
257+ )
258+ }
259+
260+ const flightResponse = await flightResponsePromise
237261
238- if ( getAppBuildId ( ) !== response . b ) {
262+ if ( getAppBuildId ( ) !== flightResponse . b ) {
239263 return doMpaNavigation ( res . url )
240264 }
241265
242266 return {
243- flightData : normalizeFlightData ( response . f ) ,
267+ flightData : normalizeFlightData ( flightResponse . f ) ,
244268 canonicalUrl : canonicalUrl ,
245269 couldBeIntercepted : interception ,
246- prerendered : response . S ,
270+ prerendered : flightResponse . S ,
247271 postponed,
248272 staleTime,
273+ debugInfo : flightResponsePromise . _debugInfo ?? null ,
249274 }
250275 } catch ( err ) {
251276 if ( ! abortController . signal . aborted ) {
@@ -265,6 +290,7 @@ export async function fetchServerResponse(
265290 prerendered : false ,
266291 postponed : false ,
267292 staleTime : - 1 ,
293+ debugInfo : null ,
268294 }
269295 }
270296}
@@ -274,21 +300,23 @@ export async function fetchServerResponse(
274300// the codebase. For example, there's some custom logic for manually following
275301// redirects, so "redirected" in this type could be a composite of multiple
276302// browser fetch calls; however, this fact should not leak to the caller.
277- export type RSCResponse = {
303+ export type RSCResponse < T > = {
278304 ok : boolean
279305 redirected : boolean
280306 headers : Headers
281307 body : ReadableStream < Uint8Array > | null
282308 status : number
283309 url : string
310+ flightResponse : ( Promise < T > & { _debugInfo ?: Array < any > } ) | null
284311}
285312
286- export async function createFetch (
313+ export async function createFetch < T > (
287314 url : URL ,
288315 headers : RequestHeaders ,
289316 fetchPriority : 'auto' | 'high' | 'low' | null ,
317+ shouldImmediatelyDecode : boolean ,
290318 signal ?: AbortSignal
291- ) : Promise < RSCResponse > {
319+ ) : Promise < RSCResponse < T > > {
292320 // TODO: In output: "export" mode, the headers do nothing. Omit them (and the
293321 // cache busting search param) from the request so they're
294322 // maximally cacheable.
@@ -326,7 +354,21 @@ export async function createFetch(
326354 // track them separately.
327355 let fetchUrl = new URL ( url )
328356 setCacheBustingSearchParam ( fetchUrl , headers )
329- let browserResponse = await fetch ( fetchUrl , fetchOptions )
357+ let fetchPromise = fetch ( fetchUrl , fetchOptions )
358+ // Immediately pass the fetch promise to the Flight client so that the debug
359+ // info includes the latency from the client to the server. The internal timer
360+ // in React starts as soon as `createFromFetch` is called.
361+ //
362+ // The only case where we don't do this is during a prefetch, because we have
363+ // to do some extra processing of the response stream (see
364+ // `createUnclosingPrefetchStream`). But this is fine, because a top-level
365+ // prefetch response never blocks a navigation; if it hasn't already been
366+ // written into the cache by the time the navigation happens, the router will
367+ // go straight to a dynamic request.
368+ let flightResponsePromise = shouldImmediatelyDecode
369+ ? createFromNextFetch < T > ( fetchPromise , headers )
370+ : null
371+ let browserResponse = await fetchPromise
330372
331373 // If the server responds with a redirect (e.g. 307), and the redirected
332374 // location does not contain the cache busting search param set in the
@@ -379,9 +421,14 @@ export async function createFetch(
379421 //
380422 // Append the cache busting search param to the redirected URL and
381423 // fetch again.
424+ // TODO: We should abort the previous request.
382425 fetchUrl = new URL ( responseUrl )
383426 setCacheBustingSearchParam ( fetchUrl , headers )
384- browserResponse = await fetch ( fetchUrl , fetchOptions )
427+ fetchPromise = fetch ( fetchUrl , fetchOptions )
428+ flightResponsePromise = shouldImmediatelyDecode
429+ ? createFromNextFetch < T > ( fetchPromise , headers )
430+ : null
431+ browserResponse = await fetchPromise
385432 // We just performed a manual redirect, so this is now true.
386433 redirected = true
387434 }
@@ -392,7 +439,7 @@ export async function createFetch(
392439 const responseUrl = new URL ( browserResponse . url , fetchUrl )
393440 responseUrl . searchParams . delete ( NEXT_RSC_UNION_QUERY )
394441
395- const rscResponse : RSCResponse = {
442+ const rscResponse : RSCResponse < T > = {
396443 url : responseUrl . href ,
397444
398445 // This is true if any redirects occurred, either automatically by the
@@ -408,22 +455,38 @@ export async function createFetch(
408455 headers : browserResponse . headers ,
409456 body : browserResponse . body ,
410457 status : browserResponse . status ,
458+
459+ // This is the exact promise returned by `createFromFetch`. It contains
460+ // debug information that we need to transfer to any derived promises that
461+ // are later rendered by React.
462+ flightResponse : flightResponsePromise ,
411463 }
412464
413465 return rscResponse
414466}
415467
416- export function createFromNextReadableStream (
468+ export function createFromNextReadableStream < T > (
417469 flightStream : ReadableStream < Uint8Array > ,
418470 requestHeaders : RequestHeaders
419- ) : Promise < unknown > {
471+ ) : Promise < T > {
420472 return createFromReadableStream ( flightStream , {
421473 callServer,
422474 findSourceMapURL,
423475 debugChannel : createDebugChannel && createDebugChannel ( requestHeaders ) ,
424476 } )
425477}
426478
479+ function createFromNextFetch < T > (
480+ promiseForResponse : Promise < Response > ,
481+ requestHeaders : RequestHeaders
482+ ) : Promise < T > & { _debugInfo ?: Array < any > } {
483+ return createFromFetch ( promiseForResponse , {
484+ callServer,
485+ findSourceMapURL,
486+ debugChannel : createDebugChannel && createDebugChannel ( requestHeaders ) ,
487+ } )
488+ }
489+
427490function createUnclosingPrefetchStream (
428491 originalFlightStream : ReadableStream < Uint8Array >
429492) : ReadableStream < Uint8Array > {
0 commit comments