@@ -26,7 +26,12 @@ import {
2626} from './scheduler'
2727import { getAppBuildId } from '../../app-build-id'
2828import { createHrefFromUrl } from '../router-reducer/create-href-from-url'
29- import type { RouteCacheKey , RouteCacheKeyId } from './cache-key'
29+ import type {
30+ NormalizedHref ,
31+ NormalizedNextUrl ,
32+ RouteCacheKey ,
33+ } from './cache-key'
34+ import { createTupleMap , type TupleMap , type Prefix } from './tuple-map'
3035
3136// A note on async/await when working in the prefetch cache:
3237//
@@ -51,6 +56,9 @@ import type { RouteCacheKey, RouteCacheKeyId } from './cache-key'
5156
5257type RouteCacheEntryShared = {
5358 staleAt : number
59+ // This is false only if we're certain the route cannot be intercepted. It's
60+ // true in all other cases, including on initialization when we haven't yet
61+ // received a response from the server.
5462 couldBeIntercepted : boolean
5563}
5664
@@ -119,27 +127,55 @@ export type SegmentCacheEntry =
119127 | RejectedSegmentCacheEntry
120128 | FulfilledSegmentCacheEntry
121129
122- const routeCache = new Map < RouteCacheKeyId , RouteCacheEntry > ( )
130+ // Route cache entries vary on multiple keys: the href and the Next-Url. Each of
131+ // these parts needs to be included in the internal cache key. Rather than
132+ // concatenate the keys into a single key, we use a multi-level map, where the
133+ // first level is keyed by href, the second level is keyed by Next-Url, and so
134+ // on (if were to add more levels).
135+ type RouteCacheKeypath = [ NormalizedHref , NormalizedNextUrl ]
136+ const routeCacheMap : TupleMap < RouteCacheKeypath , RouteCacheEntry > =
137+ createTupleMap ( )
138+
139+ // TODO: We may eventually store segment entries in a tuple map, too, to
140+ // account for search params.
123141const segmentCache = new Map < string , SegmentCacheEntry > ( )
124142
125- export function readRouteCacheEntry (
143+ export function readExactRouteCacheEntry (
126144 now : number ,
127- key : RouteCacheKey
145+ href : NormalizedHref ,
146+ nextUrl : NormalizedNextUrl | null
128147) : RouteCacheEntry | null {
129- const existingEntry = routeCache . get ( key . id )
130- if ( existingEntry !== undefined ) {
148+ const keypath : Prefix < RouteCacheKeypath > =
149+ nextUrl === null ? [ href ] : [ href , nextUrl ]
150+ const existingEntry = routeCacheMap . get ( keypath )
151+ if ( existingEntry !== null ) {
131152 // Check if the entry is stale
132153 if ( existingEntry . staleAt > now ) {
133154 // Reuse the existing entry.
134155 return existingEntry
135156 } else {
136157 // Evict the stale entry from the cache.
137- evictRouteCacheEntryFromCache ( key )
158+ routeCacheMap . delete ( keypath )
138159 }
139160 }
140161 return null
141162}
142163
164+ export function readRouteCacheEntry (
165+ now : number ,
166+ key : RouteCacheKey
167+ ) : RouteCacheEntry | null {
168+ // First check if there's a non-intercepted entry. Most routes cannot be
169+ // intercepted, so this is the common case.
170+ const nonInterceptedEntry = readExactRouteCacheEntry ( now , key . href , null )
171+ if ( nonInterceptedEntry !== null && ! nonInterceptedEntry . couldBeIntercepted ) {
172+ // Found a match, and the route cannot be intercepted. We can reuse it.
173+ return nonInterceptedEntry
174+ }
175+ // There was no match. Check again but include the Next-Url this time.
176+ return readExactRouteCacheEntry ( now , key . href , key . nextUrl )
177+ }
178+
143179export function readSegmentCacheEntry (
144180 now : number ,
145181 path : string
@@ -182,9 +218,18 @@ export function requestRouteCacheEntryFromCache(
182218 now : number ,
183219 task : PrefetchTask
184220) : RouteCacheEntry {
185- const existingEntry = readRouteCacheEntry ( now , task . key )
186- if ( existingEntry !== null ) {
187- return existingEntry
221+ const key = task . key
222+ // First check if there's a non-intercepted entry. Most routes cannot be
223+ // intercepted, so this is the common case.
224+ const nonInterceptedEntry = readExactRouteCacheEntry ( now , key . href , null )
225+ if ( nonInterceptedEntry !== null && ! nonInterceptedEntry . couldBeIntercepted ) {
226+ // Found a match, and the route cannot be intercepted. We can reuse it.
227+ return nonInterceptedEntry
228+ }
229+ // There was no match. Check again but include the Next-Url this time.
230+ const exactEntry = readExactRouteCacheEntry ( now , key . href , key . nextUrl )
231+ if ( exactEntry !== null ) {
232+ return exactEntry
188233 }
189234 // Create a pending entry and spawn a request for its data.
190235 const pendingEntry : PendingRouteCacheEntry = {
@@ -199,11 +244,15 @@ export function requestRouteCacheEntryFromCache(
199244 // When the response is received, this value will be replaced by a new value
200245 // based on the stale time sent from the server.
201246 staleAt : now + 60 * 1000 ,
202- couldBeIntercepted : false ,
247+ // This is initialized to true because we don't know yet whether the route
248+ // could be intercepted. It's only set to false once we receive a response
249+ // from the server.
250+ couldBeIntercepted : true ,
203251 }
204- const key = task . key
205252 spawnPrefetchSubtask ( fetchRouteOnCacheMiss ( pendingEntry , task ) )
206- routeCache . set ( key . id , pendingEntry )
253+ const keypath : Prefix < RouteCacheKeypath > =
254+ key . nextUrl === null ? [ key . href ] : [ key . href , key . nextUrl ]
255+ routeCacheMap . set ( keypath , pendingEntry )
207256 return pendingEntry
208257}
209258
@@ -244,10 +293,6 @@ export function requestSegmentEntryFromCache(
244293 return pendingEntry
245294}
246295
247- function evictRouteCacheEntryFromCache ( key : RouteCacheKey ) : void {
248- routeCache . delete ( key . id )
249- }
250-
251296function evictSegmentEntryFromCache (
252297 entry : SegmentCacheEntry ,
253298 key : string
@@ -271,7 +316,7 @@ function fulfillRouteCacheEntry(
271316 staleAt : number ,
272317 couldBeIntercepted : boolean ,
273318 canonicalUrl : string
274- ) {
319+ ) : FulfilledRouteCacheEntry {
275320 const fulfilledEntry : FulfilledRouteCacheEntry = entry as any
276321 fulfilledEntry . status = EntryStatus . Fulfilled
277322 fulfilledEntry . tree = tree
@@ -286,6 +331,7 @@ function fulfillRouteCacheEntry(
286331 }
287332 fulfilledEntry . blockedTasks = null
288333 }
334+ return fulfilledEntry
289335}
290336
291337function fulfillSegmentCacheEntry (
@@ -348,8 +394,9 @@ async function fetchRouteOnCacheMiss(
348394 // pings the scheduler to unblock the corresponding prefetch task.
349395 const key = task . key
350396 const href = key . href
397+ const nextUrl = key . nextUrl
351398 try {
352- const response = await fetchSegmentPrefetchResponse ( href , '/_tree' )
399+ const response = await fetchSegmentPrefetchResponse ( href , '/_tree' , nextUrl )
353400 if ( ! response || ! response . ok || ! response . body ) {
354401 // Received an unexpected response.
355402 rejectRouteCacheEntry ( entry , Date . now ( ) + 10 * 1000 )
@@ -376,9 +423,8 @@ async function fetchRouteOnCacheMiss(
376423
377424 // Check whether the response varies based on the Next-Url header.
378425 const varyHeader = response . headers . get ( 'vary' )
379- const couldBeIntercepted = varyHeader
380- ? varyHeader . includes ( NEXT_URL )
381- : false
426+ const couldBeIntercepted =
427+ varyHeader !== null && varyHeader . includes ( NEXT_URL )
382428
383429 fulfillRouteCacheEntry (
384430 entry ,
@@ -388,6 +434,26 @@ async function fetchRouteOnCacheMiss(
388434 couldBeIntercepted ,
389435 canonicalUrl
390436 )
437+
438+ if ( ! couldBeIntercepted && nextUrl !== null ) {
439+ // This route will never be intercepted. So we can use this entry for all
440+ // requests to this route, regardless of the Next-Url header. This works
441+ // because when reading the cache we always check for a valid
442+ // non-intercepted entry first.
443+ //
444+ // Re-key the entry. Since we're in an async task, we must first confirm
445+ // that the entry hasn't been concurrently modified by a different task.
446+ const currentKeypath : Prefix < RouteCacheKeypath > = [ href , nextUrl ]
447+ const expectedEntry = routeCacheMap . get ( currentKeypath )
448+ if ( expectedEntry === entry ) {
449+ routeCacheMap . delete ( currentKeypath )
450+ const newKeypath : Prefix < RouteCacheKeypath > = [ href ]
451+ routeCacheMap . set ( newKeypath , entry )
452+ } else {
453+ // Something else modified this entry already. Since the re-keying is
454+ // just a performance optimization, we can safely skip it.
455+ }
456+ }
391457 } catch ( error ) {
392458 // Either the connection itself failed, or something bad happened while
393459 // decoding the response.
@@ -412,7 +478,8 @@ async function fetchSegmentEntryOnCacheMiss(
412478 try {
413479 const response = await fetchSegmentPrefetchResponse (
414480 href ,
415- accessToken === '' ? segmentPath : `${ segmentPath } .${ accessToken } `
481+ accessToken === '' ? segmentPath : `${ segmentPath } .${ accessToken } ` ,
482+ routeKey . nextUrl
416483 )
417484 if ( ! response || ! response . ok || ! response . body ) {
418485 // Server responded with an error. We should still cache the response, but
@@ -452,14 +519,18 @@ async function fetchSegmentEntryOnCacheMiss(
452519}
453520
454521async function fetchSegmentPrefetchResponse (
455- href : string ,
456- segmentPath : string
522+ href : NormalizedHref ,
523+ segmentPath : string ,
524+ nextUrl : NormalizedNextUrl | null
457525) : Promise < Response | null > {
458526 const headers : RequestHeaders = {
459527 [ RSC_HEADER ] : '1' ,
460528 [ NEXT_ROUTER_PREFETCH_HEADER ] : '1' ,
461529 [ NEXT_ROUTER_SEGMENT_PREFETCH_HEADER ] : segmentPath ,
462530 }
531+ if ( nextUrl !== null ) {
532+ headers [ NEXT_URL ] = nextUrl
533+ }
463534 const fetchPriority = 'low'
464535 const responsePromise = createFetch ( new URL ( href ) , headers , fetchPriority )
465536 trackPrefetchRequestBandwidth ( responsePromise )
0 commit comments