@@ -45,6 +45,99 @@ const instantTestStaticFetch: Promise<Response> | undefined =
4545 ? ( self . __next_instant_test as unknown as Promise < Response > )
4646 : undefined
4747
48+ function isOfflineNavigationFallbackDocument ( ) : boolean {
49+ return Boolean (
50+ process . env . __NEXT_OFFLINE_NAVIGATIONS &&
51+ ! process . env . __NEXT_DEV_SERVER &&
52+ document . documentElement . hasAttribute (
53+ 'data-next-offline-navigation-fallback'
54+ )
55+ )
56+ }
57+
58+ function showOfflineNavigationCacheMiss ( ) : void {
59+ const cacheMissElement = document . getElementById (
60+ '__NEXT_OFFLINE_NAVIGATION_CACHE_MISS'
61+ )
62+ if ( cacheMissElement !== null ) {
63+ cacheMissElement . hidden = false
64+ }
65+ }
66+
67+ function neverResolveOfflineNavigationResponse ( ) : Promise < Response > {
68+ return new Promise < Response > ( ( ) => { } )
69+ }
70+
71+ type OfflineNavigationFallbackResponse = {
72+ requestKind : 'client-resume' | 'initial-load'
73+ response : Response
74+ }
75+
76+ function createOfflineNavigationFallbackResponse ( ) :
77+ | Promise < OfflineNavigationFallbackResponse >
78+ | undefined {
79+ if ( ! isOfflineNavigationFallbackDocument ( ) ) {
80+ return undefined
81+ }
82+
83+ return ( async ( ) : Promise < OfflineNavigationFallbackResponse > => {
84+ const {
85+ createOfflineNavigationRSCResponse,
86+ isOfflineNavigationRSCResponsePayload,
87+ readOfflineNavigationCacheEntry,
88+ } =
89+ require ( './components/router-reducer/offline-navigation-cache' ) as typeof import ( './components/router-reducer/offline-navigation-cache' )
90+
91+ const buildId =
92+ getDeploymentId ( ) ??
93+ document . documentElement . getAttribute ( 'data-build-id' ) ??
94+ undefined
95+ const entry = await readOfflineNavigationCacheEntry ( location . href , {
96+ buildId,
97+ } )
98+ const payload = entry ?. payload
99+
100+ if (
101+ ! isOfflineNavigationRSCResponsePayload ( payload ) ||
102+ ( payload . requestKind !== 'client-resume' &&
103+ payload . requestKind !== 'initial-load' )
104+ ) {
105+ showOfflineNavigationCacheMiss ( )
106+ return {
107+ requestKind : 'client-resume' ,
108+ response : await neverResolveOfflineNavigationResponse ( ) ,
109+ }
110+ }
111+
112+ const requestKind : OfflineNavigationFallbackResponse [ 'requestKind' ] =
113+ payload . requestKind === 'initial-load' ? 'initial-load' : 'client-resume'
114+
115+ return {
116+ requestKind,
117+ response : createOfflineNavigationRSCResponse ( payload ) ,
118+ }
119+ } ) ( ) . catch ( async ( ) : Promise < OfflineNavigationFallbackResponse > => {
120+ showOfflineNavigationCacheMiss ( )
121+ return {
122+ requestKind : 'client-resume' ,
123+ response : await neverResolveOfflineNavigationResponse ( ) ,
124+ }
125+ } )
126+ }
127+
128+ const offlineNavigationFallbackResponse =
129+ createOfflineNavigationFallbackResponse ( )
130+ const offlineNavigationClientResumeFetch =
131+ offlineNavigationFallbackResponse ?. then ( ( { response } ) => response )
132+
133+ const hasClientResumeShell =
134+ // @ts -expect-error
135+ Boolean ( window . __NEXT_CLIENT_RESUME )
136+ const hasLockedStaticShell =
137+ Boolean ( instantTestStaticFetch ) ||
138+ Boolean ( offlineNavigationClientResumeFetch ) ||
139+ hasClientResumeShell
140+
48141const encoder = new TextEncoder ( )
49142
50143let initialServerDataBuffer : ( string | Uint8Array ) [ ] | undefined = undefined
@@ -128,14 +221,11 @@ function nextServerDataRegisterWriter(ctr: ReadableStreamDefaultController) {
128221 ctr . enqueue ( typeof val === 'string' ? encoder . encode ( val ) : val )
129222 } )
130223 if ( initialServerDataLoaded && ! initialServerDataFlushed ) {
131- // Instant Navigation Testing API: don't close or error the inline
132- // Flight stream. The static shell has no inline Flight data, so the
133- // stream is empty. Closing it would cause React to log an error about
134- // missing data. Leaving it open lets React treat any holes as
135- // "still suspended." Hydration uses the separately fetched RSC payload
136- // (self.__next_instant_test), not this stream.
224+ // Locked static shells do not have a real inline Flight stream. Closing
225+ // or erroring this stream causes React to report a missing-data failure,
226+ // but the actual hydration data arrives through a separate response.
137227 if ( isStreamErrorOrUnfinished ( ctr ) ) {
138- if ( ! instantTestStaticFetch ) {
228+ if ( ! hasLockedStaticShell ) {
139229 ctr . error (
140230 new Error (
141231 'The connection to the page was unexpectedly closed, possibly due to the stop button being clicked, loss of Wi-Fi, or an unstable internet connection.'
@@ -155,7 +245,11 @@ function nextServerDataRegisterWriter(ctr: ReadableStreamDefaultController) {
155245
156246// When `DOMContentLoaded`, we can close all pending writers to finish hydration.
157247const DOMContentLoaded = function ( ) {
158- if ( initialServerDataWriter && ! initialServerDataFlushed ) {
248+ if (
249+ initialServerDataWriter &&
250+ ! initialServerDataFlushed &&
251+ ! hasLockedStaticShell
252+ ) {
159253 initialServerDataWriter . close ( )
160254 initialServerDataFlushed = true
161255 initialServerDataBuffer = undefined
@@ -196,9 +290,23 @@ if (process.env.NODE_ENV !== 'production') {
196290// know if `l` is present until React decodes the payload, so always tee and
197291// cancel the clone if not needed.
198292let initialFlightStreamForCache : ReadableStream < Uint8Array > | null = null
293+ let initialFlightStreamForOfflineNavigationCache : ReadableStream < Uint8Array > | null =
294+ null
295+ if (
296+ process . env . __NEXT_OFFLINE_NAVIGATIONS &&
297+ process . env . NODE_ENV === 'production' &&
298+ process . env . __NEXT_CONFIG_OUTPUT !== 'export' &&
299+ ! process . env . __NEXT_DEV_SERVER &&
300+ ! hasLockedStaticShell
301+ ) {
302+ const [ forApp , forOfflineNavigationCache ] = readable . tee ( )
303+ readable = forApp
304+ initialFlightStreamForOfflineNavigationCache = forOfflineNavigationCache
305+ }
199306if (
200307 process . env . __NEXT_CACHE_COMPONENTS &&
201- process . env . __NEXT_EXPERIMENTAL_CACHED_NAVIGATIONS
308+ process . env . __NEXT_EXPERIMENTAL_CACHED_NAVIGATIONS &&
309+ ! offlineNavigationClientResumeFetch
202310) {
203311 const [ forReact , forCache ] = readable . tee ( )
204312 readable = forReact
@@ -242,6 +350,25 @@ if (instantTestStaticFetch) {
242350 initialRSCPayload
243351 )
244352 } )
353+ } else if ( offlineNavigationClientResumeFetch ) {
354+ initialServerResponse = Promise . resolve (
355+ createFromFetch < InitialRSCPayload > ( offlineNavigationClientResumeFetch , {
356+ callServer,
357+ findSourceMapURL,
358+ debugChannel,
359+ unstable_allowPartialStream : true ,
360+ } )
361+ ) . then ( async ( fallbackInitialRSCPayload ) => {
362+ const fallbackResponse = await offlineNavigationFallbackResponse !
363+ if ( fallbackResponse . requestKind === 'initial-load' ) {
364+ return fallbackInitialRSCPayload
365+ }
366+
367+ return createInitialRSCPayloadFromFallbackPrerender (
368+ fallbackResponse . response ,
369+ fallbackInitialRSCPayload
370+ )
371+ } )
245372} else if (
246373 // @ts -expect-error
247374 window . __NEXT_CLIENT_RESUME
@@ -365,7 +492,11 @@ export async function hydrate(
365492 // Initialize the offline module to register browser event listeners
366493 // (offline/online) before any components hydrate.
367494 if ( process . env . __NEXT_USE_OFFLINE ) {
368- require ( './components/offline' ) as typeof import ( './components/offline' )
495+ const { notifyOffline } =
496+ require ( './components/offline' ) as typeof import ( './components/offline' )
497+ if ( offlineNavigationClientResumeFetch ) {
498+ notifyOffline ( )
499+ }
369500 }
370501
371502 // setNavigationBuildId should be called only once, during JS initialization
@@ -391,6 +522,7 @@ export async function hydrate(
391522 navigatedAt : initialTimestamp ,
392523 initialRSCPayload,
393524 initialFlightStreamForCache,
525+ initialFlightStreamForOfflineNavigationCache,
394526 location : window . location ,
395527 } ) ,
396528 instrumentationHooks
@@ -411,9 +543,13 @@ export async function hydrate(
411543 </ StrictModeIfEnabled >
412544 )
413545
414- if ( document . documentElement . id === '__next_error__' ) {
546+ if (
547+ document . documentElement . id === '__next_error__' ||
548+ isOfflineNavigationFallbackDocument ( )
549+ ) {
415550 let element = reactEl
416- // Server rendering failed, fall back to client-side rendering
551+ // Error documents and generated offline navigation fallback documents do
552+ // not contain route HTML that can be hydrated.
417553 if ( process . env . NODE_ENV !== 'production' ) {
418554 const { RootLevelDevOverlayElement } =
419555 require ( '../next-devtools/userspace/app/client-entry' ) as typeof import ( '../next-devtools/userspace/app/client-entry' )
0 commit comments