@@ -16,8 +16,8 @@ import type {
1616} from '../../../shared/lib/app-router-types'
1717
1818import {
19- type NEXT_ROUTER_PREFETCH_HEADER ,
20- type NEXT_ROUTER_SEGMENT_PREFETCH_HEADER ,
19+ NEXT_ROUTER_PREFETCH_HEADER ,
20+ NEXT_ROUTER_SEGMENT_PREFETCH_HEADER ,
2121 type NEXT_INSTANT_PREFETCH_HEADER ,
2222 NEXT_ROUTER_STATE_TREE_HEADER ,
2323 NEXT_RSC_UNION_QUERY ,
@@ -45,8 +45,14 @@ import { NEXT_NAV_DEPLOYMENT_ID_HEADER } from '../../../lib/constants'
4545import {
4646 stripIsPartialByte ,
4747 createNonTaskyPrefetchResponseStream ,
48+ getStaleAt ,
4849} from '../segment-cache/cache'
4950import { UnknownDynamicStaleTime } from '../segment-cache/bfcache'
51+ import {
52+ createOfflineNavigationRSCResponsePayload ,
53+ writeOfflineNavigationRSCResponseCacheEntry ,
54+ type OfflineNavigationRSCResponsePayload ,
55+ } from './offline-navigation-cache'
5056
5157const createFromReadableStream =
5258 createFromReadableStreamBrowser as ( typeof import ( 'react-server-dom-webpack/client.browser' ) ) [ 'createFromReadableStream' ]
@@ -275,6 +281,16 @@ export async function fetchServerResponse(
275281 return doMpaNavigation ( normalizedFlightData )
276282 }
277283
284+ persistOfflineNavigationResponse ( {
285+ canonicalUrl,
286+ flightResponse,
287+ interception,
288+ isHmrRefresh : options . isHmrRefresh ,
289+ originalUrl,
290+ postponed,
291+ response : res ,
292+ } )
293+
278294 const staticStageData =
279295 cacheData !== null
280296 ? await resolveStaticStageData ( cacheData , flightResponse , headers )
@@ -355,6 +371,7 @@ export type RSCResponse<T> = {
355371 url : string
356372 flightResponsePromise : ( Promise < T > & { _debugInfo ?: Array < any > } ) | null
357373 cacheData : Promise < FetchResponseCacheData | null >
374+ offlineNavigationCachePayload : Promise < OfflineNavigationRSCResponsePayload | null > | null
358375}
359376
360377type FetchResponseCacheData = {
@@ -535,6 +552,8 @@ export async function createFetch<T>(
535552 let fetchUrl = new URL ( url )
536553 await setCacheBustingSearchParam ( fetchUrl , headers )
537554 let processed = fetch ( fetchUrl , fetchOptions ) . then ( processFetch )
555+ let offlineNavigationCachePayload =
556+ createOfflineNavigationCachePayloadFromProcessedResponse ( processed , headers )
538557 let fetchPromise = processed . then ( ( { response } ) => response )
539558
540559 // Immediately pass the fetch promise to the Flight client so that the debug
@@ -605,6 +624,11 @@ export async function createFetch<T>(
605624 fetchUrl = new URL ( responseUrl )
606625 await setCacheBustingSearchParam ( fetchUrl , headers )
607626 processed = fetch ( fetchUrl , fetchOptions ) . then ( processFetch )
627+ offlineNavigationCachePayload =
628+ createOfflineNavigationCachePayloadFromProcessedResponse (
629+ processed ,
630+ headers
631+ )
608632 fetchPromise = processed . then ( ( { response } ) => response )
609633 flightResponsePromise = shouldImmediatelyDecode
610634 ? createFromNextFetch < T > ( fetchPromise , headers )
@@ -643,11 +667,120 @@ export async function createFetch<T>(
643667 flightResponsePromise : flightResponsePromise ,
644668
645669 cacheData : processed . then ( ( { cacheData } ) => cacheData ) ,
670+
671+ offlineNavigationCachePayload,
646672 }
647673
648674 return rscResponse
649675}
650676
677+ function getOfflineNavigationCacheRequestKind (
678+ headers : RequestHeaders
679+ ) : 'navigation' | 'route-prefetch' | 'client-resume' | null {
680+ if (
681+ ! process . env . __NEXT_OFFLINE_NAVIGATIONS ||
682+ process . env . __NEXT_DEV_SERVER ||
683+ process . env . NODE_ENV !== 'production' ||
684+ process . env . __NEXT_CONFIG_OUTPUT === 'export' ||
685+ headers [ NEXT_HMR_REFRESH_HEADER ]
686+ ) {
687+ return null
688+ }
689+
690+ if (
691+ headers [ NEXT_ROUTER_PREFETCH_HEADER ] !== undefined &&
692+ headers [ NEXT_ROUTER_SEGMENT_PREFETCH_HEADER ] === '/_full'
693+ ) {
694+ return 'client-resume'
695+ }
696+
697+ if (
698+ headers [ NEXT_ROUTER_PREFETCH_HEADER ] !== undefined &&
699+ headers [ NEXT_ROUTER_SEGMENT_PREFETCH_HEADER ] === undefined
700+ ) {
701+ return 'route-prefetch'
702+ }
703+
704+ if (
705+ headers [ NEXT_ROUTER_STATE_TREE_HEADER ] !== undefined &&
706+ headers [ NEXT_ROUTER_PREFETCH_HEADER ] === undefined &&
707+ headers [ NEXT_ROUTER_SEGMENT_PREFETCH_HEADER ] === undefined
708+ ) {
709+ return 'navigation'
710+ }
711+
712+ return null
713+ }
714+
715+ function createOfflineNavigationCachePayloadFromProcessedResponse (
716+ processed : Promise < {
717+ response : Response
718+ cacheData : FetchResponseCacheData | null
719+ } > ,
720+ headers : RequestHeaders
721+ ) : Promise < OfflineNavigationRSCResponsePayload | null > | null {
722+ const requestKind = getOfflineNavigationCacheRequestKind ( headers )
723+ if ( requestKind === null ) {
724+ return null
725+ }
726+
727+ return processed
728+ . then ( ( { response } ) => {
729+ if ( ! response . ok || ! response . body ) {
730+ return null
731+ }
732+
733+ return createOfflineNavigationRSCResponsePayload ( response , requestKind )
734+ } )
735+ . catch ( ( ) => null )
736+ }
737+
738+ function persistOfflineNavigationResponse ( {
739+ canonicalUrl,
740+ flightResponse,
741+ interception,
742+ isHmrRefresh,
743+ originalUrl,
744+ postponed,
745+ response,
746+ } : {
747+ canonicalUrl : URL
748+ flightResponse : NavigationFlightResponse
749+ interception : boolean
750+ isHmrRefresh : boolean | undefined
751+ originalUrl : URL
752+ postponed : boolean
753+ response : RSCResponse < NavigationFlightResponse >
754+ } ) : void {
755+ if (
756+ response . offlineNavigationCachePayload === null ||
757+ ! flightResponse . S ||
758+ flightResponse . d !== undefined ||
759+ flightResponse . p !== undefined ||
760+ interception ||
761+ isHmrRefresh ||
762+ postponed ||
763+ response . redirected ||
764+ canonicalUrl . origin !== location . origin
765+ ) {
766+ return
767+ }
768+
769+ void ( async ( ) => {
770+ const now = Date . now ( )
771+ const staleAt = await getStaleAt ( now , flightResponse . s , response )
772+ await writeOfflineNavigationRSCResponseCacheEntry ( {
773+ buildId :
774+ response . headers . get ( NEXT_NAV_DEPLOYMENT_ID_HEADER ) ?? flightResponse . b ,
775+ expiresAt : staleAt ,
776+ payload : response . offlineNavigationCachePayload ! ,
777+ staleAt,
778+ url : originalUrl ,
779+ now,
780+ } )
781+ } ) ( )
782+ }
783+
651784export function createFromNextReadableStream < T > (
652785 flightStream : ReadableStream < Uint8Array > ,
653786 requestHeaders : RequestHeaders | undefined ,
0 commit comments