@@ -37,6 +37,7 @@ import { createHrefFromUrl } from '../router-reducer/create-href-from-url'
37
37
import type {
38
38
NormalizedHref ,
39
39
NormalizedNextUrl ,
40
+ NormalizedSearch ,
40
41
RouteCacheKey ,
41
42
} from './cache-key'
42
43
import { createTupleMap , type TupleMap , type Prefix } from './tuple-map'
@@ -52,6 +53,7 @@ import type {
52
53
} from '../../../server/app-render/types'
53
54
import { normalizeFlightData } from '../../flight-data-helpers'
54
55
import { STATIC_STALETIME_MS } from '../router-reducer/prefetch-cache-utils'
56
+ import { PAGE_SEGMENT_KEY } from '../../../shared/lib/segment'
55
57
56
58
// A note on async/await when working in the prefetch cache:
57
59
//
@@ -156,9 +158,9 @@ type SegmentCacheEntryShared = {
156
158
revalidating : SegmentCacheEntry | null
157
159
158
160
// LRU-related fields
159
- key : null | string
160
- next : null | RouteCacheEntry
161
- prev : null | RouteCacheEntry
161
+ keypath : null | Prefix < SegmentCacheKeypath >
162
+ next : null | SegmentCacheEntry
163
+ prev : null | SegmentCacheEntry
162
164
size : number
163
165
}
164
166
@@ -225,9 +227,9 @@ let routeCacheLru = createLRU<RouteCacheEntry>(
225
227
onRouteLRUEviction
226
228
)
227
229
228
- // TODO: We may eventually store segment entries in a tuple map, too, to
229
- // account for search params.
230
- let segmentCacheMap = new Map < string , SegmentCacheEntry > ( )
230
+ type SegmentCacheKeypath = [ string , NormalizedSearch ]
231
+ let segmentCacheMap : TupleMap < SegmentCacheKeypath , SegmentCacheEntry > =
232
+ createTupleMap ( )
231
233
// NOTE: Segments and Route entries are managed by separate LRUs. We could
232
234
// combine them into a single LRU, but because they are separate types, we'd
233
235
// need to wrap each one in an extra LRU node (to maintain monomorphism, at the
@@ -252,7 +254,7 @@ export function revalidateEntireCache() {
252
254
// correctly: background revalidations. See note in `upsertSegmentEntry`.
253
255
routeCacheMap = createTupleMap ( )
254
256
routeCacheLru = createLRU ( maxRouteLruSize , onRouteLRUEviction )
255
- segmentCacheMap = new Map ( )
257
+ segmentCacheMap = createTupleMap ( )
256
258
segmentCacheLru = createLRU ( maxSegmentLruSize , onSegmentLRUEviction )
257
259
}
258
260
@@ -296,12 +298,59 @@ export function readRouteCacheEntry(
296
298
return readExactRouteCacheEntry ( now , key . href , key . nextUrl )
297
299
}
298
300
301
+ export function getSegmentKeypathForTask (
302
+ task : PrefetchTask ,
303
+ path : string
304
+ ) : Prefix < SegmentCacheKeypath > {
305
+ // When a prefetch includes dynamic data, the search params are included
306
+ // in the result, so we must include the search string in the segment
307
+ // cache key. (Note that this is true even if the search string is empty.)
308
+ //
309
+ // If we're fetching using PPR, we do not need to include the search params in
310
+ // the cache key, because the search params are treated as dynamic data. The
311
+ // cache entry is valid for all possible search param values.
312
+ return task . includeDynamicData && path . endsWith ( '/' + PAGE_SEGMENT_KEY )
313
+ ? [ path , task . key . search ]
314
+ : [ path ]
315
+ }
316
+
299
317
export function readSegmentCacheEntry (
300
318
now : number ,
319
+ routeCacheKey : RouteCacheKey ,
301
320
path : string
302
321
) : SegmentCacheEntry | null {
303
- const existingEntry = segmentCacheMap . get ( path )
304
- if ( existingEntry !== undefined ) {
322
+ if ( ! path . endsWith ( '/' + PAGE_SEGMENT_KEY ) ) {
323
+ // Fast path. Search params only exist on page segments.
324
+ return readExactSegmentCacheEntry ( now , [ path ] )
325
+ }
326
+
327
+ // Page segments may or may not contain search params. If they were prefetched
328
+ // using a dynamic request, then we will have an entry with search params.
329
+ // Check for that case first.
330
+ const entryWithSearchParams = readExactSegmentCacheEntry ( now , [
331
+ path ,
332
+ routeCacheKey . search ,
333
+ ] )
334
+ if ( entryWithSearchParams !== null ) {
335
+ return entryWithSearchParams
336
+ }
337
+
338
+ // If we did not find an entry with the given search params, check for a
339
+ // "fallback" entry, where the search params are treated as dynamic data. This
340
+ // is the common case because PPR/static prerenders always treat search params
341
+ // as dynamic.
342
+ //
343
+ // See corresponding logic in `getSegmentKeypathForTask`.
344
+ const entryWithoutSearchParams = readExactSegmentCacheEntry ( now , [ path ] )
345
+ return entryWithoutSearchParams
346
+ }
347
+
348
+ function readExactSegmentCacheEntry (
349
+ now : number ,
350
+ keypath : Prefix < SegmentCacheKeypath >
351
+ ) : SegmentCacheEntry | null {
352
+ const existingEntry = segmentCacheMap . get ( keypath )
353
+ if ( existingEntry !== null ) {
305
354
// Check if the entry is stale
306
355
if ( existingEntry . staleAt > now ) {
307
356
// Reuse the existing entry.
@@ -315,14 +364,18 @@ export function readSegmentCacheEntry(
315
364
const revalidatingEntry = existingEntry . revalidating
316
365
if ( revalidatingEntry !== null ) {
317
366
// There's a revalidation in progress. Upsert it.
318
- const upsertedEntry = upsertSegmentEntry ( now , path , revalidatingEntry )
367
+ const upsertedEntry = upsertSegmentEntry (
368
+ now ,
369
+ keypath ,
370
+ revalidatingEntry
371
+ )
319
372
if ( upsertedEntry !== null && upsertedEntry . staleAt > now ) {
320
373
// We can use the upserted revalidation entry.
321
374
return upsertedEntry
322
375
}
323
376
} else {
324
377
// Evict the stale entry from the cache.
325
- deleteSegmentFromCache ( existingEntry , path )
378
+ deleteSegmentFromCache ( existingEntry , keypath )
326
379
}
327
380
}
328
381
}
@@ -415,20 +468,22 @@ export function readOrCreateRouteCacheEntry(
415
468
*/
416
469
export function readOrCreateSegmentCacheEntry (
417
470
now : number ,
471
+ task : PrefetchTask ,
418
472
// TODO: Don't need to pass the whole route. Just `staleAt`.
419
473
route : FulfilledRouteCacheEntry ,
420
474
path : string
421
475
) : SegmentCacheEntry {
422
- const existingEntry = readSegmentCacheEntry ( now , path )
476
+ const keypath = getSegmentKeypathForTask ( task , path )
477
+ const existingEntry = readExactSegmentCacheEntry ( now , keypath )
423
478
if ( existingEntry !== null ) {
424
479
return existingEntry
425
480
}
426
481
// Create a pending entry and add it to the cache.
427
482
const pendingEntry = createDetachedSegmentCacheEntry ( route . staleAt )
428
- segmentCacheMap . set ( path , pendingEntry )
483
+ segmentCacheMap . set ( keypath , pendingEntry )
429
484
// Stash the keypath on the entry so we know how to remove it from the map
430
485
// if it gets evicted from the LRU.
431
- pendingEntry . key = path
486
+ pendingEntry . keypath = keypath
432
487
segmentCacheLru . put ( pendingEntry )
433
488
return pendingEntry
434
489
}
@@ -460,7 +515,7 @@ export function readOrCreateRevalidatingSegmentEntry(
460
515
461
516
export function upsertSegmentEntry (
462
517
now : number ,
463
- segmentKeyPath : string ,
518
+ keypath : Prefix < SegmentCacheKeypath > ,
464
519
candidateEntry : SegmentCacheEntry
465
520
) : SegmentCacheEntry | null {
466
521
// We have a new entry that has not yet been inserted into the cache. Before
@@ -469,7 +524,7 @@ export function upsertSegmentEntry(
469
524
// TODO: We should not upsert an entry if its key was invalidated in the time
470
525
// since the request was made. We can do that by passing the "owner" entry to
471
526
// this function and confirming it's the same as `existingEntry`.
472
- const existingEntry = readSegmentCacheEntry ( now , segmentKeyPath )
527
+ const existingEntry = readExactSegmentCacheEntry ( now , keypath )
473
528
if ( existingEntry !== null ) {
474
529
if ( candidateEntry . isPartial && ! existingEntry . isPartial ) {
475
530
// Don't replace a full segment with a partial one. A case where this
@@ -488,12 +543,12 @@ export function upsertSegmentEntry(
488
543
return null
489
544
}
490
545
// Evict the existing entry from the cache.
491
- deleteSegmentFromCache ( existingEntry , segmentKeyPath )
546
+ deleteSegmentFromCache ( existingEntry , keypath )
492
547
}
493
- segmentCacheMap . set ( segmentKeyPath , candidateEntry )
548
+ segmentCacheMap . set ( keypath , candidateEntry )
494
549
// Stash the keypath on the entry so we know how to remove it from the map
495
550
// if it gets evicted from the LRU.
496
- candidateEntry . key = segmentKeyPath
551
+ candidateEntry . keypath = keypath
497
552
segmentCacheLru . put ( candidateEntry )
498
553
return candidateEntry
499
554
}
@@ -514,7 +569,7 @@ export function createDetachedSegmentCacheEntry(
514
569
promise : null ,
515
570
516
571
// LRU-related fields
517
- key : null ,
572
+ keypath : null ,
518
573
next : null ,
519
574
prev : null ,
520
575
size : 0 ,
@@ -541,9 +596,12 @@ function deleteRouteFromCache(
541
596
routeCacheLru . delete ( entry )
542
597
}
543
598
544
- function deleteSegmentFromCache ( entry : SegmentCacheEntry , key : string ) : void {
599
+ function deleteSegmentFromCache (
600
+ entry : SegmentCacheEntry ,
601
+ keypath : Prefix < SegmentCacheKeypath >
602
+ ) : void {
545
603
cancelEntryListeners ( entry )
546
- segmentCacheMap . delete ( key )
604
+ segmentCacheMap . delete ( keypath )
547
605
segmentCacheLru . delete ( entry )
548
606
clearRevalidatingSegmentFromOwner ( entry )
549
607
}
@@ -581,11 +639,11 @@ function onRouteLRUEviction(entry: RouteCacheEntry): void {
581
639
582
640
function onSegmentLRUEviction ( entry : SegmentCacheEntry ) : void {
583
641
// The LRU evicted this entry. Remove it from the map.
584
- const key = entry . key
585
- if ( key !== null ) {
586
- entry . key = null
642
+ const keypath = entry . keypath
643
+ if ( keypath !== null ) {
644
+ entry . keypath = null
587
645
cancelEntryListeners ( entry )
588
- segmentCacheMap . delete ( key )
646
+ segmentCacheMap . delete ( keypath )
589
647
}
590
648
}
591
649
@@ -765,9 +823,25 @@ function convertFlightRouterStateToRouteTree(
765
823
}
766
824
}
767
825
826
+ // The navigation implementation expects the search params to be included
827
+ // in the segment. However, in the case of a static response, the search
828
+ // params are omitted. So the client needs to add them back in when reading
829
+ // from the Segment Cache.
830
+ //
831
+ // For consistency, we'll do this for dynamic responses, too.
832
+ //
833
+ // TODO: We should move search params out of FlightRouterState and handle them
834
+ // entirely on the client, similar to our plan for dynamic params.
835
+ const originalSegment = flightRouterState [ 0 ]
836
+ const segmentWithoutSearchParams =
837
+ typeof originalSegment === 'string' &&
838
+ originalSegment . startsWith ( PAGE_SEGMENT_KEY )
839
+ ? PAGE_SEGMENT_KEY
840
+ : originalSegment
841
+
768
842
return {
769
843
key,
770
- segment : flightRouterState [ 0 ] ,
844
+ segment : segmentWithoutSearchParams ,
771
845
slots,
772
846
isRootLayout : flightRouterState [ 4 ] === true ,
773
847
}
@@ -1093,6 +1167,7 @@ export async function fetchSegmentPrefetchesUsingDynamicRequest(
1093
1167
// in the LRU as more data comes in.
1094
1168
fulfilledEntries = writeDynamicRenderResponseIntoCache (
1095
1169
Date . now ( ) ,
1170
+ task ,
1096
1171
response ,
1097
1172
serverData ,
1098
1173
route ,
@@ -1181,6 +1256,7 @@ function rejectSegmentEntriesIfStillPending(
1181
1256
1182
1257
function writeDynamicRenderResponseIntoCache (
1183
1258
now : number ,
1259
+ task : PrefetchTask ,
1184
1260
response : Response ,
1185
1261
serverData : NavigationFlightResponse ,
1186
1262
route : FulfilledRouteCacheEntry ,
@@ -1231,6 +1307,7 @@ function writeDynamicRenderResponseIntoCache(
1231
1307
: STATIC_STALETIME_MS
1232
1308
writeSeedDataIntoCache (
1233
1309
now ,
1310
+ task ,
1234
1311
route ,
1235
1312
now + staleTimeMs ,
1236
1313
seedData ,
@@ -1256,6 +1333,7 @@ function writeDynamicRenderResponseIntoCache(
1256
1333
1257
1334
function writeSeedDataIntoCache (
1258
1335
now : number ,
1336
+ task : PrefetchTask ,
1259
1337
route : FulfilledRouteCacheEntry ,
1260
1338
staleAt : number ,
1261
1339
seedData : CacheNodeSeedData ,
@@ -1279,7 +1357,12 @@ function writeSeedDataIntoCache(
1279
1357
fulfillSegmentCacheEntry ( ownedEntry , rsc , loading , staleAt , isPartial )
1280
1358
} else {
1281
1359
// There's no matching entry. Attempt to create a new one.
1282
- const possiblyNewEntry = readOrCreateSegmentCacheEntry ( now , route , key )
1360
+ const possiblyNewEntry = readOrCreateSegmentCacheEntry (
1361
+ now ,
1362
+ task ,
1363
+ route ,
1364
+ key
1365
+ )
1283
1366
if ( possiblyNewEntry . status === EntryStatus . Empty ) {
1284
1367
// Confirmed this is a new entry. We can fulfill it.
1285
1368
const newEntry = possiblyNewEntry
@@ -1294,7 +1377,7 @@ function writeSeedDataIntoCache(
1294
1377
staleAt ,
1295
1378
isPartial
1296
1379
)
1297
- upsertSegmentEntry ( now , key , newEntry )
1380
+ upsertSegmentEntry ( now , getSegmentKeypathForTask ( task , key ) , newEntry )
1298
1381
}
1299
1382
}
1300
1383
// Recursively write the child data into the cache.
@@ -1306,6 +1389,7 @@ function writeSeedDataIntoCache(
1306
1389
const childSegment = childSeedData [ 0 ]
1307
1390
writeSeedDataIntoCache (
1308
1391
now ,
1392
+ task ,
1309
1393
route ,
1310
1394
staleAt ,
1311
1395
childSeedData ,
0 commit comments