Skip to content

Commit 2be5937

Browse files
committed
Reconstruct prefetched routes from offline router cache
1 parent b18a178 commit 2be5937

3 files changed

Lines changed: 517 additions & 56 deletions

File tree

packages/next/src/client/app-index.tsx

Lines changed: 146 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
createMutableActionQueue,
2121
} from './components/app-router-instance'
2222
import AppRouter from './components/app-router'
23+
import DefaultGlobalError from './components/builtin/global-error'
2324
import type { InitialRSCPayload } from '../shared/lib/app-router-types'
2425
import { createInitialRouterState } from './components/router-reducer/create-initial-router-state'
2526
import { 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+
6672
type 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

93105
declare global {
94106
interface Window {
@@ -109,7 +121,7 @@ function reportOfflineNavigationFallbackDiagnostic(
109121
}
110122

111123
function showOfflineNavigationCacheHit(
112-
requestKind: OfflineNavigationFallbackResponse['requestKind'],
124+
requestKind: OfflineNavigationFallbackRequestKind,
113125
buildId: string | undefined
114126
): void {
115127
document.documentElement.setAttribute(
@@ -157,23 +169,39 @@ 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

164176
type 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+
function createOfflineNavigationFallbackBootstrap():
198+
| Promise<OfflineNavigationFallbackBootstrap>
171199
| undefined {
172200
if (!isOfflineNavigationFallbackDocument()) {
173201
return undefined
174202
}
175203

176-
return (async (): Promise<OfflineNavigationFallbackResponse> => {
204+
return (async (): Promise<OfflineNavigationFallbackBootstrap> => {
177205
const {
178206
createOfflineNavigationRSCResponse,
179207
isOfflineNavigationRSCResponsePayload,
@@ -191,13 +219,54 @@ function createOfflineNavigationFallbackResponse():
191219
const payload = entry?.payload
192220

193221
if (!isOfflineNavigationRSCResponsePayload(payload)) {
222+
if (payload === undefined) {
223+
const {
224+
createOfflineNavigationInitialRSCPayloadFromRouterCache,
225+
hydrateOfflineNavigationRouterCache,
226+
} =
227+
require('./components/segment-cache/cache') as typeof import('./components/segment-cache/cache')
228+
229+
const hydrationResult = await hydrateOfflineNavigationRouterCache({
230+
buildId,
231+
})
232+
reportOfflineNavigationFallbackDiagnostic({
233+
type: 'router-cache-hydration',
234+
url: location.href,
235+
buildId,
236+
routes: hydrationResult.routes,
237+
segments: hydrationResult.segments,
238+
})
239+
240+
const reconstruction =
241+
createOfflineNavigationInitialRSCPayloadFromRouterCache({
242+
buildId,
243+
globalErrorState: [DefaultGlobalError, undefined],
244+
now: Date.now(),
245+
url: location.href,
246+
})
247+
if (reconstruction.status === 'fulfilled') {
248+
showOfflineNavigationCacheHit('router-cache', buildId)
249+
return {
250+
kind: 'router-cache',
251+
initialRSCPayload: reconstruction.initialRSCPayload,
252+
buildId,
253+
}
254+
}
255+
reportOfflineNavigationFallbackDiagnostic({
256+
type: 'router-cache-reconstruction-miss',
257+
url: location.href,
258+
buildId,
259+
reason: reconstruction.reason,
260+
})
261+
}
262+
194263
showOfflineNavigationCacheMiss(
195264
payload === undefined ? 'missing-entry' : 'invalid-payload',
196265
buildId
197266
)
198267
return {
199-
requestKind: 'client-resume',
200-
response: await neverResolveOfflineNavigationResponse(),
268+
kind: 'cache-miss',
269+
buildId,
201270
}
202271
}
203272

@@ -207,8 +276,8 @@ function createOfflineNavigationFallbackResponse():
207276
) {
208277
showOfflineNavigationCacheMiss('unsupported-request-kind', buildId)
209278
return {
210-
requestKind: 'client-resume',
211-
response: await neverResolveOfflineNavigationResponse(),
279+
kind: 'cache-miss',
280+
buildId,
212281
}
213282
}
214283

@@ -217,29 +286,37 @@ function createOfflineNavigationFallbackResponse():
217286

218287
showOfflineNavigationCacheHit(requestKind, buildId)
219288
return {
220-
requestKind,
221-
response: createOfflineNavigationRSCResponse(payload),
289+
kind: 'rsc-response',
290+
buildId,
291+
response: {
292+
requestKind,
293+
response: createOfflineNavigationRSCResponse(payload),
294+
},
222295
}
223-
})().catch(async (): Promise<OfflineNavigationFallbackResponse> => {
296+
})().catch((): OfflineNavigationFallbackBootstrap => {
224297
showOfflineNavigationCacheMiss('read-error', undefined)
225298
return {
226-
requestKind: 'client-resume',
227-
response: await neverResolveOfflineNavigationResponse(),
299+
kind: 'cache-miss',
300+
buildId: undefined,
228301
}
229302
})
230303
}
231304

232-
const offlineNavigationFallbackResponse =
233-
createOfflineNavigationFallbackResponse()
234-
const offlineNavigationClientResumeFetch =
235-
offlineNavigationFallbackResponse?.then(({ response }) => response)
305+
const offlineNavigationFallbackBootstrap =
306+
createOfflineNavigationFallbackBootstrap()
307+
308+
if (process.env.__NEXT_USE_OFFLINE && offlineNavigationFallbackBootstrap) {
309+
const { notifyOffline } =
310+
require('./components/offline') as typeof import('./components/offline')
311+
notifyOffline()
312+
}
236313

237314
const hasClientResumeShell =
238315
// @ts-expect-error
239316
Boolean(window.__NEXT_CLIENT_RESUME)
240317
const hasLockedStaticShell =
241318
Boolean(instantTestStaticFetch) ||
242-
Boolean(offlineNavigationClientResumeFetch) ||
319+
Boolean(offlineNavigationFallbackBootstrap) ||
243320
hasClientResumeShell
244321

245322
const encoder = new TextEncoder()
@@ -410,7 +487,7 @@ if (
410487
if (
411488
process.env.__NEXT_CACHE_COMPONENTS &&
412489
process.env.__NEXT_EXPERIMENTAL_CACHED_NAVIGATIONS &&
413-
!offlineNavigationClientResumeFetch
490+
!offlineNavigationFallbackBootstrap
414491
) {
415492
const [forReact, forCache] = readable.tee()
416493
readable = forReact
@@ -454,25 +531,39 @@ if (instantTestStaticFetch) {
454531
initialRSCPayload
455532
)
456533
})
457-
} else if (offlineNavigationClientResumeFetch) {
458-
initialServerResponse = Promise.resolve(
459-
createFromFetch<InitialRSCPayload>(offlineNavigationClientResumeFetch, {
460-
callServer,
461-
findSourceMapURL,
462-
debugChannel,
463-
unstable_allowPartialStream: true,
464-
})
465-
).then(async (fallbackInitialRSCPayload) => {
466-
const fallbackResponse = await offlineNavigationFallbackResponse!
467-
if (fallbackResponse.requestKind === 'initial-load') {
468-
return fallbackInitialRSCPayload
469-
}
534+
} else if (offlineNavigationFallbackBootstrap) {
535+
initialServerResponse = offlineNavigationFallbackBootstrap.then(
536+
async (bootstrap) => {
537+
if (bootstrap.kind === 'cache-miss') {
538+
return await neverResolveInitialRSCPayload()
539+
}
470540

471-
return createInitialRSCPayloadFromFallbackPrerender(
472-
fallbackResponse.response,
473-
fallbackInitialRSCPayload
474-
)
475-
})
541+
if (bootstrap.kind === 'router-cache') {
542+
return bootstrap.initialRSCPayload
543+
}
544+
545+
const fallbackResponse = bootstrap.response
546+
const fallbackInitialRSCPayload =
547+
await createFromFetch<InitialRSCPayload>(
548+
Promise.resolve(fallbackResponse.response),
549+
{
550+
callServer,
551+
findSourceMapURL,
552+
debugChannel,
553+
unstable_allowPartialStream: true,
554+
}
555+
)
556+
557+
if (fallbackResponse.requestKind === 'initial-load') {
558+
return fallbackInitialRSCPayload
559+
}
560+
561+
return createInitialRSCPayloadFromFallbackPrerender(
562+
fallbackResponse.response,
563+
fallbackInitialRSCPayload
564+
)
565+
}
566+
)
476567
} else if (
477568
// @ts-expect-error
478569
window.__NEXT_CLIENT_RESUME
@@ -598,7 +689,7 @@ export async function hydrate(
598689
if (process.env.__NEXT_USE_OFFLINE) {
599690
const { notifyOffline } =
600691
require('./components/offline') as typeof import('./components/offline')
601-
if (offlineNavigationClientResumeFetch) {
692+
if (offlineNavigationFallbackBootstrap) {
602693
notifyOffline()
603694
}
604695
}
@@ -624,19 +715,22 @@ export async function hydrate(
624715
if (
625716
process.env.__NEXT_OFFLINE_NAVIGATIONS &&
626717
!process.env.__NEXT_DEV_SERVER &&
627-
offlineNavigationClientResumeFetch
718+
offlineNavigationFallbackBootstrap
628719
) {
629720
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-
})
721+
const bootstrap = await offlineNavigationFallbackBootstrap
722+
if (bootstrap.kind === 'rsc-response') {
723+
const { hydrateOfflineNavigationRouterCache } =
724+
require('./components/segment-cache/cache') as typeof import('./components/segment-cache/cache')
725+
const result = await hydrateOfflineNavigationRouterCache({ buildId })
726+
reportOfflineNavigationFallbackDiagnostic({
727+
type: 'router-cache-hydration',
728+
url: location.href,
729+
buildId,
730+
routes: result.routes,
731+
segments: result.segments,
732+
})
733+
}
640734
} catch {
641735
// The exact URL fallback already booted. Router cache hydration should
642736
// only improve later navigations, never block the current render.

0 commit comments

Comments
 (0)