@@ -20,6 +20,7 @@ import {
2020 createMutableActionQueue ,
2121} from './components/app-router-instance'
2222import AppRouter from './components/app-router'
23+ import DefaultGlobalError from './components/builtin/global-error'
2324import type { InitialRSCPayload } from '../shared/lib/app-router-types'
2425import { createInitialRouterState } from './components/router-reducer/create-initial-router-state'
2526import { MissingSlotContext } from '../shared/lib/app-router-context.shared-runtime'
@@ -63,12 +64,17 @@ type OfflineNavigationCacheMissReason =
6364 | 'unsupported-request-kind'
6465 | 'read-error'
6566
67+ type OfflineNavigationFallbackRequestKind =
68+ | 'client-resume'
69+ | 'initial-load'
70+ | 'router-cache'
71+
6672type OfflineNavigationFallbackDiagnostic =
6773 | {
6874 type : 'cache-hit'
6975 url : string
7076 buildId : string | undefined
71- requestKind : OfflineNavigationFallbackResponse [ 'requestKind' ]
77+ requestKind : OfflineNavigationFallbackRequestKind
7278 }
7379 | {
7480 type : 'cache-miss'
@@ -89,6 +95,12 @@ type OfflineNavigationFallbackDiagnostic =
8995 skipped : number
9096 }
9197 }
98+ | {
99+ type : 'router-cache-reconstruction-miss'
100+ url : string
101+ buildId : string | undefined
102+ reason : string
103+ }
92104
93105declare global {
94106 interface Window {
@@ -109,7 +121,7 @@ function reportOfflineNavigationFallbackDiagnostic(
109121}
110122
111123function showOfflineNavigationCacheHit (
112- requestKind : OfflineNavigationFallbackResponse [ 'requestKind' ] ,
124+ requestKind : OfflineNavigationFallbackRequestKind ,
113125 buildId : string | undefined
114126) : void {
115127 document . documentElement . setAttribute (
@@ -157,24 +169,43 @@ function showOfflineNavigationCacheMiss(
157169 } )
158170}
159171
160- function neverResolveOfflineNavigationResponse ( ) : Promise < Response > {
161- return new Promise < Response > ( ( ) => { } )
172+ function neverResolveInitialRSCPayload ( ) : Promise < InitialRSCPayload > {
173+ return new Promise < InitialRSCPayload > ( ( ) => { } )
162174}
163175
164176type OfflineNavigationFallbackResponse = {
165177 requestKind : 'client-resume' | 'initial-load'
166178 response : Response
167179}
168180
169- function createOfflineNavigationFallbackResponse ( ) :
170- | Promise < OfflineNavigationFallbackResponse >
181+ type OfflineNavigationFallbackBootstrap =
182+ | {
183+ kind : 'rsc-response'
184+ response : OfflineNavigationFallbackResponse
185+ buildId : string | undefined
186+ }
187+ | {
188+ kind : 'router-cache'
189+ initialRSCPayload : InitialRSCPayload
190+ buildId : string | undefined
191+ }
192+ | {
193+ kind : 'cache-miss'
194+ buildId : string | undefined
195+ }
196+
197+ // The generated fallback document has no inline Flight data for the current
198+ // URL. Bootstrap from a durable exact-URL RSC response first, then fall back to
199+ // reconstructing the route from persisted segment-cache records.
200+ function createOfflineNavigationFallbackBootstrap ( ) :
201+ | Promise < OfflineNavigationFallbackBootstrap >
171202 | undefined {
172203 if ( process . env . __NEXT_OFFLINE_NAVIGATIONS ) {
173204 if ( ! isOfflineNavigationFallbackDocument ( ) ) {
174205 return undefined
175206 }
176207
177- return ( async ( ) : Promise < OfflineNavigationFallbackResponse > => {
208+ return ( async ( ) : Promise < OfflineNavigationFallbackBootstrap > => {
178209 const {
179210 createOfflineNavigationRSCResponse,
180211 isOfflineNavigationRSCResponsePayload,
@@ -192,13 +223,54 @@ function createOfflineNavigationFallbackResponse():
192223 const payload = entry ?. payload
193224
194225 if ( ! isOfflineNavigationRSCResponsePayload ( payload ) ) {
226+ if ( payload === undefined ) {
227+ const {
228+ createOfflineNavigationInitialRSCPayloadFromRouterCache,
229+ hydrateOfflineNavigationRouterCache,
230+ } =
231+ require ( './components/segment-cache/cache' ) as typeof import ( './components/segment-cache/cache' )
232+
233+ const hydrationResult = await hydrateOfflineNavigationRouterCache ( {
234+ buildId,
235+ } )
236+ reportOfflineNavigationFallbackDiagnostic ( {
237+ type : 'router-cache-hydration' ,
238+ url : location . href ,
239+ buildId,
240+ routes : hydrationResult . routes ,
241+ segments : hydrationResult . segments ,
242+ } )
243+
244+ const reconstruction =
245+ createOfflineNavigationInitialRSCPayloadFromRouterCache ( {
246+ buildId,
247+ globalErrorState : [ DefaultGlobalError , undefined ] ,
248+ now : Date . now ( ) ,
249+ url : location . href ,
250+ } )
251+ if ( reconstruction . status === 'fulfilled' ) {
252+ showOfflineNavigationCacheHit ( 'router-cache' , buildId )
253+ return {
254+ kind : 'router-cache' ,
255+ initialRSCPayload : reconstruction . initialRSCPayload ,
256+ buildId,
257+ }
258+ }
259+ reportOfflineNavigationFallbackDiagnostic ( {
260+ type : 'router-cache-reconstruction-miss' ,
261+ url : location . href ,
262+ buildId,
263+ reason : reconstruction . reason ,
264+ } )
265+ }
266+
195267 showOfflineNavigationCacheMiss (
196268 payload === undefined ? 'missing-entry' : 'invalid-payload' ,
197269 buildId
198270 )
199271 return {
200- requestKind : 'client-resume ' ,
201- response : await neverResolveOfflineNavigationResponse ( ) ,
272+ kind : 'cache-miss ' ,
273+ buildId ,
202274 }
203275 }
204276
@@ -208,8 +280,8 @@ function createOfflineNavigationFallbackResponse():
208280 ) {
209281 showOfflineNavigationCacheMiss ( 'unsupported-request-kind' , buildId )
210282 return {
211- requestKind : 'client-resume ' ,
212- response : await neverResolveOfflineNavigationResponse ( ) ,
283+ kind : 'cache-miss ' ,
284+ buildId ,
213285 }
214286 }
215287
@@ -220,30 +292,42 @@ function createOfflineNavigationFallbackResponse():
220292
221293 showOfflineNavigationCacheHit ( requestKind , buildId )
222294 return {
223- requestKind,
224- response : createOfflineNavigationRSCResponse ( payload ) ,
295+ kind : 'rsc-response' ,
296+ buildId,
297+ response : {
298+ requestKind,
299+ response : createOfflineNavigationRSCResponse ( payload ) ,
300+ } ,
225301 }
226- } ) ( ) . catch ( async ( ) : Promise < OfflineNavigationFallbackResponse > => {
302+ } ) ( ) . catch ( ( ) : OfflineNavigationFallbackBootstrap => {
227303 showOfflineNavigationCacheMiss ( 'read-error' , undefined )
228304 return {
229- requestKind : 'client-resume ' ,
230- response : await neverResolveOfflineNavigationResponse ( ) ,
305+ kind : 'cache-miss ' ,
306+ buildId : undefined ,
231307 }
232308 } )
233309 } else {
234310 return undefined
235311 }
236312}
237313
238- const offlineNavigationFallbackResponse =
239- createOfflineNavigationFallbackResponse ( )
240- const offlineNavigationClientResumeFetch =
241- offlineNavigationFallbackResponse ?. then ( ( { response } ) => response )
314+ const offlineNavigationFallbackBootstrap =
315+ createOfflineNavigationFallbackBootstrap ( )
316+
317+ if ( process . env . __NEXT_USE_OFFLINE ) {
318+ if ( offlineNavigationFallbackBootstrap ) {
319+ const { notifyOffline } =
320+ require ( './components/offline' ) as typeof import ( './components/offline' )
321+ notifyOffline ( )
322+ }
323+ } else {
324+ // Keep the offline event module out of disabled client bundles.
325+ }
242326
243327const hasClientResumeShell = Boolean ( window . __NEXT_CLIENT_RESUME )
244328const hasLockedStaticShell =
245329 Boolean ( instantTestStaticFetch ) ||
246- Boolean ( offlineNavigationClientResumeFetch ) ||
330+ Boolean ( offlineNavigationFallbackBootstrap ) ||
247331 hasClientResumeShell
248332
249333const encoder = new TextEncoder ( )
417501if (
418502 process . env . __NEXT_CACHE_COMPONENTS &&
419503 process . env . __NEXT_EXPERIMENTAL_CACHED_NAVIGATIONS &&
420- ! offlineNavigationClientResumeFetch
504+ ! offlineNavigationFallbackBootstrap
421505) {
422506 const [ forReact , forCache ] = readable . tee ( )
423507 readable = forReact
@@ -461,25 +545,39 @@ if (instantTestStaticFetch) {
461545 initialRSCPayload
462546 )
463547 } )
464- } else if ( offlineNavigationClientResumeFetch ) {
465- initialServerResponse = Promise . resolve (
466- createFromFetch < InitialRSCPayload > ( offlineNavigationClientResumeFetch , {
467- callServer,
468- findSourceMapURL,
469- debugChannel,
470- unstable_allowPartialStream : true ,
471- } )
472- ) . then ( async ( fallbackInitialRSCPayload ) => {
473- const fallbackResponse = await offlineNavigationFallbackResponse !
474- if ( fallbackResponse . requestKind === 'initial-load' ) {
475- return fallbackInitialRSCPayload
476- }
548+ } else if ( offlineNavigationFallbackBootstrap ) {
549+ initialServerResponse = offlineNavigationFallbackBootstrap . then (
550+ async ( bootstrap ) => {
551+ if ( bootstrap . kind === 'cache-miss' ) {
552+ return await neverResolveInitialRSCPayload ( )
553+ }
477554
478- return createInitialRSCPayloadFromFallbackPrerender (
479- fallbackResponse . response ,
480- fallbackInitialRSCPayload
481- )
482- } )
555+ if ( bootstrap . kind === 'router-cache' ) {
556+ return bootstrap . initialRSCPayload
557+ }
558+
559+ const fallbackResponse = bootstrap . response
560+ const fallbackInitialRSCPayload =
561+ await createFromFetch < InitialRSCPayload > (
562+ Promise . resolve ( fallbackResponse . response ) ,
563+ {
564+ callServer,
565+ findSourceMapURL,
566+ debugChannel,
567+ unstable_allowPartialStream : true ,
568+ }
569+ )
570+
571+ if ( fallbackResponse . requestKind === 'initial-load' ) {
572+ return fallbackInitialRSCPayload
573+ }
574+
575+ return createInitialRSCPayloadFromFallbackPrerender (
576+ fallbackResponse . response ,
577+ fallbackInitialRSCPayload
578+ )
579+ }
580+ )
483581} else if ( window . __NEXT_CLIENT_RESUME ) {
484582 const clientResumeFetch : Promise < Response > = window . __NEXT_CLIENT_RESUME
485583 initialServerResponse = Promise . resolve (
@@ -600,7 +698,7 @@ export async function hydrate(
600698 if ( process . env . __NEXT_USE_OFFLINE ) {
601699 const { notifyOffline } =
602700 require ( './components/offline' ) as typeof import ( './components/offline' )
603- if ( offlineNavigationClientResumeFetch ) {
701+ if ( offlineNavigationFallbackBootstrap ) {
604702 notifyOffline ( )
605703 }
606704 }
@@ -625,25 +723,28 @@ export async function hydrate(
625723 }
626724
627725 if ( process . env . __NEXT_OFFLINE_NAVIGATIONS ) {
628- if ( ! process . env . __NEXT_DEV_SERVER && offlineNavigationClientResumeFetch ) {
726+ if ( ! process . env . __NEXT_DEV_SERVER && offlineNavigationFallbackBootstrap ) {
629727 try {
630- const { hydrateOfflineNavigationRouterCache } =
631- require ( './components/segment-cache/cache' ) as typeof import ( './components/segment-cache/cache' )
632- const result = await hydrateOfflineNavigationRouterCache ( )
633- reportOfflineNavigationFallbackDiagnostic ( {
634- type : 'router-cache-hydration' ,
635- url : location . href ,
636- buildId,
637- routes : result . routes ,
638- segments : result . segments ,
639- } )
728+ const bootstrap = await offlineNavigationFallbackBootstrap
729+ if ( bootstrap . kind === 'rsc-response' ) {
730+ const { hydrateOfflineNavigationRouterCache } =
731+ require ( './components/segment-cache/cache' ) as typeof import ( './components/segment-cache/cache' )
732+ const result = await hydrateOfflineNavigationRouterCache ( { buildId } )
733+ reportOfflineNavigationFallbackDiagnostic ( {
734+ type : 'router-cache-hydration' ,
735+ url : location . href ,
736+ buildId,
737+ routes : result . routes ,
738+ segments : result . segments ,
739+ } )
740+ }
640741 } catch {
641742 // The exact URL fallback already booted. Router cache hydration should
642743 // only improve later navigations, never block the current render.
643744 }
644745 }
645746 } else {
646- // Keep router-cache hydration helpers out of disabled client bundles.
747+ // Keep router-cache reconstruction helpers out of disabled client bundles.
647748 }
648749
649750 const initialTimestamp = Date . now ( )
0 commit comments