Skip to content

Commit

Permalink
Expire loading state cache separately from cache node data
Browse files Browse the repository at this point in the history
  • Loading branch information
ztanner committed Mar 5, 2024
1 parent ff8cb80 commit af906b6
Show file tree
Hide file tree
Showing 6 changed files with 51 additions and 7 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -108,8 +108,10 @@ describe('createInitialRouterState', () => {
prefetchTime: expect.any(Number),
kind: PrefetchKind.AUTO,
lastUsedTime: null,
renewalTime: null,
treeAtTimeOfPrefetch: initialTree,
status: PrefetchCacheEntryStatus.fresh,
loadingStatus: null,
},
],
]),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ export function fillCacheWithDataProperty(
lazyData: childCacheNode.lazyData,
rsc: childCacheNode.rsc,
prefetchRsc: childCacheNode.prefetchRsc,
loading: childCacheNode.loading,
parallelRoutes: new Map(childCacheNode.parallelRoutes),
} as CacheNode
childSegmentMap.set(cacheKey, childCacheNode)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@ import type {
import { invalidateCacheByRouterState } from './invalidate-cache-by-router-state'
import { fillLazyItemsTillLeafWithHead } from './fill-lazy-items-till-leaf-with-head'
import { createRouterCacheKey } from './create-router-cache-key'
import type { PrefetchCacheEntry } from './router-reducer-types'
import {
PrefetchCacheEntryStatus,
type PrefetchCacheEntry,
} from './router-reducer-types'

/**
* Fill cache with rsc based on flightDataPath
Expand Down Expand Up @@ -49,11 +52,18 @@ export function fillCacheWithNewSubTreeData(
const seedData: CacheNodeSeedData = flightDataPath[3]
const rsc = seedData[2]
const loading = seedData[3]
const canReuseLoadingState =
prefetchEntry?.loadingStatus === PrefetchCacheEntryStatus.reusable

childCacheNode = {
lazyData: null,
rsc,
prefetchRsc: null,
loading,
// Preserve the existing loading node if it exists & is reusable
loading:
canReuseLoadingState && existingChildCacheNode?.loading
? existingChildCacheNode.loading
: loading,
// Ensure segments other than the one we got data for are preserved.
parallelRoutes: existingChildCacheNode
? new Map(existingChildCacheNode.parallelRoutes)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,9 @@ export function getOrCreatePrefetchCacheEntry({
if (existingCacheEntry) {
// Grab the latest status of the cache entry and update it
existingCacheEntry.status = getPrefetchEntryCacheStatus(existingCacheEntry)
existingCacheEntry.loadingStatus = getLoadingCacheStatus(
existingCacheEntry.renewalTime
)

// when `kind` is provided, an explicit prefetch was requested.
// if the requested prefetch is "full" and the current cache entry wasn't, we want to re-prefetch with the new intent
Expand All @@ -91,6 +94,7 @@ export function getOrCreatePrefetchCacheEntry({
buildId,
nextUrl,
prefetchCache,
isRenewal: true,
// If we didn't get an explicit prefetch kind, we want to set a temporary kind
// rather than assuming the same intent as the previous entry, to be consistent with how we
// lazily create prefetch entries when intent is left unspecified.
Expand Down Expand Up @@ -174,8 +178,10 @@ export function createPrefetchCacheEntryForInitialLoad({
kind,
prefetchTime: Date.now(),
lastUsedTime: null,
renewalTime: null,
key: prefetchCacheKey,
status: PrefetchCacheEntryStatus.fresh,
loadingStatus: null,
}

prefetchCache.set(prefetchCacheKey, prefetchEntry)
Expand All @@ -193,12 +199,14 @@ function createLazyPrefetchEntry({
nextUrl,
buildId,
prefetchCache,
isRenewal,
}: Pick<
ReadonlyReducerState,
'nextUrl' | 'tree' | 'buildId' | 'prefetchCache'
> & {
url: URL
kind: PrefetchKind
isRenewal?: boolean
}): PrefetchCacheEntry {
const prefetchCacheKey = createPrefetchCacheKey(url)

Expand Down Expand Up @@ -226,8 +234,10 @@ function createLazyPrefetchEntry({
kind,
prefetchTime: Date.now(),
lastUsedTime: null,
renewalTime: isRenewal ? Date.now() : null,
key: prefetchCacheKey,
status: PrefetchCacheEntryStatus.fresh,
loadingStatus: PrefetchCacheEntryStatus.fresh,
}

prefetchCache.set(prefetchCacheKey, prefetchEntry)
Expand All @@ -251,6 +261,27 @@ export function prunePrefetchCache(
const FIVE_MINUTES = 5 * 60 * 1000
const THIRTY_SECONDS = 30 * 1000

/**
* This function is used to determine the cache status of the loading state of a prefetch cache entry.
*/
function getLoadingCacheStatus(time: number | null) {
// a null value here means the time entry hasn't yet been renewed
// therefore we assume it's fresh
if (!time) {
return PrefetchCacheEntryStatus.fresh
}

// once renewed, the loading state can be reused for up to 5 minutes
if (Date.now() < time + FIVE_MINUTES) {
return PrefetchCacheEntryStatus.reusable
}

return PrefetchCacheEntryStatus.expired
}

/**
* This function is used to determine the cache status of the data of a prefetch cache entry.
*/
function getPrefetchEntryCacheStatus({
kind,
prefetchTime,
Expand All @@ -263,14 +294,14 @@ function getPrefetchEntryCacheStatus({
: PrefetchCacheEntryStatus.fresh
}

// if the cache entry was prefetched less than 5 mins ago, then we want to re-use only the loading state
// if the cache entry was prefetched greater than 30s ago but less than 5 mins ago, then it's stale
if (kind === 'auto') {
if (Date.now() < prefetchTime + FIVE_MINUTES) {
return PrefetchCacheEntryStatus.stale
}
}

// if the cache entry was prefetched less than 5 mins ago and was a "full" prefetch, then we want to re-use it "full
// if the cache entry was prefetched less than 5 mins ago and was a "full" prefetch, then we want to re-use it
if (kind === 'full') {
if (Date.now() < prefetchTime + FIVE_MINUTES) {
return PrefetchCacheEntryStatus.reusable
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -204,8 +204,10 @@ export type PrefetchCacheEntry = {
kind: PrefetchKind
prefetchTime: number
lastUsedTime: number | null
renewalTime: number | null
key: string
status: PrefetchCacheEntryStatus
loadingStatus: PrefetchCacheEntryStatus | null
}

export enum PrefetchCacheEntryStatus {
Expand Down
4 changes: 1 addition & 3 deletions test/e2e/app-dir/app-client-cache/client-cache.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -373,9 +373,7 @@ createNextDescribe(
expect(newNumber).toBe(initialNumber)
})

// TODO: Rather than reusing parts of a stale prefetch cache entry to make this work,
// we should be able to copy over the existing loading from a previous cache node on navigation.
it.skip('should refetch below the fold after 30 seconds', async () => {
it('should refetch below the fold after 30 seconds', async () => {
const randomLoadingNumber = await browser
.elementByCss('[href="/1?timeout=1000"]')
.click()
Expand Down

0 comments on commit af906b6

Please sign in to comment.