@@ -36,6 +36,7 @@ import { createHrefFromUrl } from '../router-reducer/create-href-from-url'
36
36
import type {
37
37
NormalizedHref ,
38
38
NormalizedNextUrl ,
39
+ NormalizedSearch ,
39
40
RouteCacheKey ,
40
41
} from './cache-key'
41
42
import { createTupleMap , type TupleMap , type Prefix } from './tuple-map'
@@ -53,6 +54,7 @@ import type {
53
54
import { normalizeFlightData } from '../../flight-data-helpers'
54
55
import { STATIC_STALETIME_MS } from '../router-reducer/prefetch-cache-utils'
55
56
import { pingVisibleLinks } from '../links'
57
+ import { PAGE_SEGMENT_KEY } from '../../../shared/lib/segment'
56
58
57
59
// A note on async/await when working in the prefetch cache:
58
60
//
@@ -157,9 +159,9 @@ type SegmentCacheEntryShared = {
157
159
revalidating : SegmentCacheEntry | null
158
160
159
161
// LRU-related fields
160
- key : null | string
161
- next : null | RouteCacheEntry
162
- prev : null | RouteCacheEntry
162
+ keypath : null | Prefix < SegmentCacheKeypath >
163
+ next : null | SegmentCacheEntry
164
+ prev : null | SegmentCacheEntry
163
165
size : number
164
166
}
165
167
@@ -230,9 +232,9 @@ let routeCacheLru = createLRU<RouteCacheEntry>(
230
232
onRouteLRUEviction
231
233
)
232
234
233
- // TODO: We may eventually store segment entries in a tuple map, too, to
234
- // account for search params.
235
- let segmentCacheMap = new Map < string , SegmentCacheEntry > ( )
235
+ type SegmentCacheKeypath = [ string , NormalizedSearch ]
236
+ let segmentCacheMap : TupleMap < SegmentCacheKeypath , SegmentCacheEntry > =
237
+ createTupleMap ( )
236
238
// NOTE: Segments and Route entries are managed by separate LRUs. We could
237
239
// combine them into a single LRU, but because they are separate types, we'd
238
240
// need to wrap each one in an extra LRU node (to maintain monomorphism, at the
@@ -269,7 +271,7 @@ export function revalidateEntireCache(
269
271
// correctly: background revalidations. See note in `upsertSegmentEntry`.
270
272
routeCacheMap = createTupleMap ( )
271
273
routeCacheLru = createLRU ( maxRouteLruSize , onRouteLRUEviction )
272
- segmentCacheMap = new Map ( )
274
+ segmentCacheMap = createTupleMap ( )
273
275
segmentCacheLru = createLRU ( maxSegmentLruSize , onSegmentLRUEviction )
274
276
275
277
// Prefetch all the currently visible links again, to re-fill the cache.
@@ -316,12 +318,61 @@ export function readRouteCacheEntry(
316
318
return readExactRouteCacheEntry ( now , key . href , key . nextUrl )
317
319
}
318
320
321
+ export function getSegmentKeypathForTask (
322
+ task : PrefetchTask ,
323
+ route : FulfilledRouteCacheEntry ,
324
+ path : string
325
+ ) : Prefix < SegmentCacheKeypath > {
326
+ // When a prefetch includes dynamic data, the search params are included
327
+ // in the result, so we must include the search string in the segment
328
+ // cache key. (Note that this is true even if the search string is empty.)
329
+ //
330
+ // If we're fetching using PPR, we do not need to include the search params in
331
+ // the cache key, because the search params are treated as dynamic data. The
332
+ // cache entry is valid for all possible search param values.
333
+ const isDynamicTask = task . includeDynamicData || ! route . isPPREnabled
334
+ return isDynamicTask && path . endsWith ( '/' + PAGE_SEGMENT_KEY )
335
+ ? [ path , task . key . search ]
336
+ : [ path ]
337
+ }
338
+
319
339
export function readSegmentCacheEntry (
320
340
now : number ,
341
+ routeCacheKey : RouteCacheKey ,
321
342
path : string
322
343
) : SegmentCacheEntry | null {
323
- const existingEntry = segmentCacheMap . get ( path )
324
- if ( existingEntry !== undefined ) {
344
+ if ( ! path . endsWith ( '/' + PAGE_SEGMENT_KEY ) ) {
345
+ // Fast path. Search params only exist on page segments.
346
+ return readExactSegmentCacheEntry ( now , [ path ] )
347
+ }
348
+
349
+ // Page segments may or may not contain search params. If they were prefetched
350
+ // using a dynamic request, then we will have an entry with search params.
351
+ // Check for that case first.
352
+ const entryWithSearchParams = readExactSegmentCacheEntry ( now , [
353
+ path ,
354
+ routeCacheKey . search ,
355
+ ] )
356
+ if ( entryWithSearchParams !== null ) {
357
+ return entryWithSearchParams
358
+ }
359
+
360
+ // If we did not find an entry with the given search params, check for a
361
+ // "fallback" entry, where the search params are treated as dynamic data. This
362
+ // is the common case because PPR/static prerenders always treat search params
363
+ // as dynamic.
364
+ //
365
+ // See corresponding logic in `getSegmentKeypathForTask`.
366
+ const entryWithoutSearchParams = readExactSegmentCacheEntry ( now , [ path ] )
367
+ return entryWithoutSearchParams
368
+ }
369
+
370
+ function readExactSegmentCacheEntry (
371
+ now : number ,
372
+ keypath : Prefix < SegmentCacheKeypath >
373
+ ) : SegmentCacheEntry | null {
374
+ const existingEntry = segmentCacheMap . get ( keypath )
375
+ if ( existingEntry !== null ) {
325
376
// Check if the entry is stale
326
377
if ( existingEntry . staleAt > now ) {
327
378
// Reuse the existing entry.
@@ -335,14 +386,18 @@ export function readSegmentCacheEntry(
335
386
const revalidatingEntry = existingEntry . revalidating
336
387
if ( revalidatingEntry !== null ) {
337
388
// There's a revalidation in progress. Upsert it.
338
- const upsertedEntry = upsertSegmentEntry ( now , path , revalidatingEntry )
389
+ const upsertedEntry = upsertSegmentEntry (
390
+ now ,
391
+ keypath ,
392
+ revalidatingEntry
393
+ )
339
394
if ( upsertedEntry !== null && upsertedEntry . staleAt > now ) {
340
395
// We can use the upserted revalidation entry.
341
396
return upsertedEntry
342
397
}
343
398
} else {
344
399
// Evict the stale entry from the cache.
345
- deleteSegmentFromCache ( existingEntry , path )
400
+ deleteSegmentFromCache ( existingEntry , keypath )
346
401
}
347
402
}
348
403
}
@@ -435,20 +490,21 @@ export function readOrCreateRouteCacheEntry(
435
490
*/
436
491
export function readOrCreateSegmentCacheEntry (
437
492
now : number ,
438
- // TODO: Don't need to pass the whole route. Just `staleAt`.
493
+ task : PrefetchTask ,
439
494
route : FulfilledRouteCacheEntry ,
440
495
path : string
441
496
) : SegmentCacheEntry {
442
- const existingEntry = readSegmentCacheEntry ( now , path )
497
+ const keypath = getSegmentKeypathForTask ( task , route , path )
498
+ const existingEntry = readExactSegmentCacheEntry ( now , keypath )
443
499
if ( existingEntry !== null ) {
444
500
return existingEntry
445
501
}
446
502
// Create a pending entry and add it to the cache.
447
503
const pendingEntry = createDetachedSegmentCacheEntry ( route . staleAt )
448
- segmentCacheMap . set ( path , pendingEntry )
504
+ segmentCacheMap . set ( keypath , pendingEntry )
449
505
// Stash the keypath on the entry so we know how to remove it from the map
450
506
// if it gets evicted from the LRU.
451
- pendingEntry . key = path
507
+ pendingEntry . keypath = keypath
452
508
segmentCacheLru . put ( pendingEntry )
453
509
return pendingEntry
454
510
}
@@ -480,7 +536,7 @@ export function readOrCreateRevalidatingSegmentEntry(
480
536
481
537
export function upsertSegmentEntry (
482
538
now : number ,
483
- segmentKeyPath : string ,
539
+ keypath : Prefix < SegmentCacheKeypath > ,
484
540
candidateEntry : SegmentCacheEntry
485
541
) : SegmentCacheEntry | null {
486
542
// We have a new entry that has not yet been inserted into the cache. Before
@@ -489,7 +545,7 @@ export function upsertSegmentEntry(
489
545
// TODO: We should not upsert an entry if its key was invalidated in the time
490
546
// since the request was made. We can do that by passing the "owner" entry to
491
547
// this function and confirming it's the same as `existingEntry`.
492
- const existingEntry = readSegmentCacheEntry ( now , segmentKeyPath )
548
+ const existingEntry = readExactSegmentCacheEntry ( now , keypath )
493
549
if ( existingEntry !== null ) {
494
550
if ( candidateEntry . isPartial && ! existingEntry . isPartial ) {
495
551
// Don't replace a full segment with a partial one. A case where this
@@ -508,12 +564,12 @@ export function upsertSegmentEntry(
508
564
return null
509
565
}
510
566
// Evict the existing entry from the cache.
511
- deleteSegmentFromCache ( existingEntry , segmentKeyPath )
567
+ deleteSegmentFromCache ( existingEntry , keypath )
512
568
}
513
- segmentCacheMap . set ( segmentKeyPath , candidateEntry )
569
+ segmentCacheMap . set ( keypath , candidateEntry )
514
570
// Stash the keypath on the entry so we know how to remove it from the map
515
571
// if it gets evicted from the LRU.
516
- candidateEntry . key = segmentKeyPath
572
+ candidateEntry . keypath = keypath
517
573
segmentCacheLru . put ( candidateEntry )
518
574
return candidateEntry
519
575
}
@@ -534,7 +590,7 @@ export function createDetachedSegmentCacheEntry(
534
590
promise : null ,
535
591
536
592
// LRU-related fields
537
- key : null ,
593
+ keypath : null ,
538
594
next : null ,
539
595
prev : null ,
540
596
size : 0 ,
@@ -561,9 +617,12 @@ function deleteRouteFromCache(
561
617
routeCacheLru . delete ( entry )
562
618
}
563
619
564
- function deleteSegmentFromCache ( entry : SegmentCacheEntry , key : string ) : void {
620
+ function deleteSegmentFromCache (
621
+ entry : SegmentCacheEntry ,
622
+ keypath : Prefix < SegmentCacheKeypath >
623
+ ) : void {
565
624
cancelEntryListeners ( entry )
566
- segmentCacheMap . delete ( key )
625
+ segmentCacheMap . delete ( keypath )
567
626
segmentCacheLru . delete ( entry )
568
627
clearRevalidatingSegmentFromOwner ( entry )
569
628
}
@@ -601,11 +660,11 @@ function onRouteLRUEviction(entry: RouteCacheEntry): void {
601
660
602
661
function onSegmentLRUEviction ( entry : SegmentCacheEntry ) : void {
603
662
// The LRU evicted this entry. Remove it from the map.
604
- const key = entry . key
605
- if ( key !== null ) {
606
- entry . key = null
663
+ const keypath = entry . keypath
664
+ if ( keypath !== null ) {
665
+ entry . keypath = null
607
666
cancelEntryListeners ( entry )
608
- segmentCacheMap . delete ( key )
667
+ segmentCacheMap . delete ( keypath )
609
668
}
610
669
}
611
670
@@ -785,9 +844,25 @@ function convertFlightRouterStateToRouteTree(
785
844
}
786
845
}
787
846
847
+ // The navigation implementation expects the search params to be included
848
+ // in the segment. However, in the case of a static response, the search
849
+ // params are omitted. So the client needs to add them back in when reading
850
+ // from the Segment Cache.
851
+ //
852
+ // For consistency, we'll do this for dynamic responses, too.
853
+ //
854
+ // TODO: We should move search params out of FlightRouterState and handle them
855
+ // entirely on the client, similar to our plan for dynamic params.
856
+ const originalSegment = flightRouterState [ 0 ]
857
+ const segmentWithoutSearchParams =
858
+ typeof originalSegment === 'string' &&
859
+ originalSegment . startsWith ( PAGE_SEGMENT_KEY )
860
+ ? PAGE_SEGMENT_KEY
861
+ : originalSegment
862
+
788
863
return {
789
864
key,
790
- segment : flightRouterState [ 0 ] ,
865
+ segment : segmentWithoutSearchParams ,
791
866
slots,
792
867
isRootLayout : flightRouterState [ 4 ] === true ,
793
868
}
@@ -1174,6 +1249,7 @@ export async function fetchSegmentPrefetchesUsingDynamicRequest(
1174
1249
// in the LRU as more data comes in.
1175
1250
fulfilledEntries = writeDynamicRenderResponseIntoCache (
1176
1251
Date . now ( ) ,
1252
+ task ,
1177
1253
response ,
1178
1254
serverData ,
1179
1255
route ,
@@ -1262,6 +1338,7 @@ function rejectSegmentEntriesIfStillPending(
1262
1338
1263
1339
function writeDynamicRenderResponseIntoCache (
1264
1340
now : number ,
1341
+ task : PrefetchTask ,
1265
1342
response : Response ,
1266
1343
serverData : NavigationFlightResponse ,
1267
1344
route : FulfilledRouteCacheEntry ,
@@ -1312,6 +1389,7 @@ function writeDynamicRenderResponseIntoCache(
1312
1389
: STATIC_STALETIME_MS
1313
1390
writeSeedDataIntoCache (
1314
1391
now ,
1392
+ task ,
1315
1393
route ,
1316
1394
now + staleTimeMs ,
1317
1395
seedData ,
@@ -1337,6 +1415,7 @@ function writeDynamicRenderResponseIntoCache(
1337
1415
1338
1416
function writeSeedDataIntoCache (
1339
1417
now : number ,
1418
+ task : PrefetchTask ,
1340
1419
route : FulfilledRouteCacheEntry ,
1341
1420
staleAt : number ,
1342
1421
seedData : CacheNodeSeedData ,
@@ -1360,7 +1439,12 @@ function writeSeedDataIntoCache(
1360
1439
fulfillSegmentCacheEntry ( ownedEntry , rsc , loading , staleAt , isPartial )
1361
1440
} else {
1362
1441
// There's no matching entry. Attempt to create a new one.
1363
- const possiblyNewEntry = readOrCreateSegmentCacheEntry ( now , route , key )
1442
+ const possiblyNewEntry = readOrCreateSegmentCacheEntry (
1443
+ now ,
1444
+ task ,
1445
+ route ,
1446
+ key
1447
+ )
1364
1448
if ( possiblyNewEntry . status === EntryStatus . Empty ) {
1365
1449
// Confirmed this is a new entry. We can fulfill it.
1366
1450
const newEntry = possiblyNewEntry
@@ -1375,7 +1459,11 @@ function writeSeedDataIntoCache(
1375
1459
staleAt ,
1376
1460
isPartial
1377
1461
)
1378
- upsertSegmentEntry ( now , key , newEntry )
1462
+ upsertSegmentEntry (
1463
+ now ,
1464
+ getSegmentKeypathForTask ( task , route , key ) ,
1465
+ newEntry
1466
+ )
1379
1467
}
1380
1468
}
1381
1469
// Recursively write the child data into the cache.
@@ -1387,6 +1475,7 @@ function writeSeedDataIntoCache(
1387
1475
const childSegment = childSeedData [ 0 ]
1388
1476
writeSeedDataIntoCache (
1389
1477
now ,
1478
+ task ,
1390
1479
route ,
1391
1480
staleAt ,
1392
1481
childSeedData ,
0 commit comments