Skip to content

Commit

Permalink
move code subtleties closer together
Browse files Browse the repository at this point in the history
  • Loading branch information
ztanner committed Feb 9, 2024
1 parent fcb4877 commit 97b0a56
Show file tree
Hide file tree
Showing 9 changed files with 215 additions and 271 deletions.
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { createHrefFromUrl } from './create-href-from-url'
import { fetchServerResponse } from './fetch-server-response'
import {
PrefetchCacheEntryStatus,
type AppRouterState,
type PrefetchCacheEntry,
PrefetchKind,
type ReadonlyReducerState,
} from './router-reducer-types'
import { addPathPrefix } from '../../../shared/lib/router/utils/add-path-prefix'
import { pathHasPrefix } from '../../../shared/lib/router/utils/path-has-prefix'
import { createHrefFromUrl } from './create-href-from-url'
import { prefetchQueue } from './reducers/prefetch-reducer'

/**
* Creates a cache key for the router prefetch cache
Expand All @@ -14,27 +15,177 @@ import { createHrefFromUrl } from './create-href-from-url'
* @param nextUrl - an internal URL, primarily used for handling rewrites. Defaults to '/'.
* @return The generated prefetch cache key.
*/
export function createPrefetchCacheKey(url: URL, nextUrl: string | null) {
function createPrefetchCacheKey(url: URL, nextUrl?: string | null) {
const pathnameFromUrl = createHrefFromUrl(
url,
// Ensures the hash is not part of the cache key as it does not impact the server fetch
false
)

// delimit the prefix so we don't conflict with other pages
const nextUrlPrefix = `${nextUrl}%`

// Route interception depends on `nextUrl` values which aren't a 1:1 mapping to a URL
// The cache key that we store needs to use `nextUrl` to properly distinguish cache entries
if (nextUrl && !pathHasPrefix(pathnameFromUrl, nextUrl)) {
return addPathPrefix(pathnameFromUrl, nextUrlPrefix)
// nextUrl is used as a cache key delimiter since entries can vary based on the Next-URL header
if (nextUrl) {
return `${nextUrl}%${pathnameFromUrl}`
}

return pathnameFromUrl
}

/**
* Returns a prefetch cache entry if one exists. Otherwise creates a new one and enqueues a fetch request
* to retrieve the prefetch data from the server.
*/
export function getOrCreatePrefetchCacheEntry({
url,
nextUrl,
tree,
buildId,
prefetchCache,
kind,
}: Pick<
ReadonlyReducerState,
'nextUrl' | 'prefetchCache' | 'tree' | 'buildId'
> & {
url: URL
kind?: PrefetchKind
}): PrefetchCacheEntry {
let existingCacheEntry: PrefetchCacheEntry | undefined = undefined
// We first check if there's a more specific interception route prefetch entry
// This is because when we detect a prefetch that corresponds with an interception route, we prefix it with nextUrl (see `createPrefetchCacheKey`)
// to avoid conflicts with other pages that may have the same URL but render different things depending on the `Next-URL` header.
const interceptionCacheKey = createPrefetchCacheKey(url, nextUrl)
const interceptionData = prefetchCache.get(interceptionCacheKey)

if (interceptionData) {
existingCacheEntry = interceptionData
} else {
// If we dont find a more specific interception route prefetch entry, we check for a regular prefetch entry
const prefetchCacheKey = createPrefetchCacheKey(url)
const prefetchData = prefetchCache.get(prefetchCacheKey)
if (prefetchData) {
existingCacheEntry = prefetchData
}
}

if (existingCacheEntry) {
// 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
if (
kind &&
existingCacheEntry.kind !== PrefetchKind.FULL &&
kind === PrefetchKind.FULL
) {
return createLazyPrefetchEntry({
tree,
url,
buildId,
nextUrl,
prefetchCache,
kind,
})
}

// Grab the latest status of the cache entry and update it
existingCacheEntry.status = getPrefetchEntryCacheStatus(existingCacheEntry)

// If the existing cache entry was marked as temporary, it means it was lazily created when attempting to get an entry,
// where we didn't have the prefetch intent. Now that we have the intent (in `kind`), we want to update the entry to the more accurate kind.
if (kind && existingCacheEntry.kind === PrefetchKind.TEMPORARY) {
existingCacheEntry.kind = kind
}

// We've determined that the existing entry we found is still valid, so we return it.
return existingCacheEntry
}

// If we didn't return an entry, create a new one.
return createLazyPrefetchEntry({
tree,
url,
buildId,
nextUrl,
prefetchCache,
kind:
kind ||
// in dev, there's never gonna be a prefetch entry so we want to prefetch here
(process.env.NODE_ENV === 'development'
? PrefetchKind.AUTO
: PrefetchKind.TEMPORARY),
})
}

function prefixExistingPrefetchCacheEntry({
url,
nextUrl,
prefetchCache,
}: Pick<ReadonlyReducerState, 'nextUrl' | 'prefetchCache'> & {
url: URL
}) {
const existingCacheKey = createPrefetchCacheKey(url)
const existingCacheEntry = prefetchCache.get(existingCacheKey)
if (!existingCacheEntry) {
// no-op -- there wasn't an entry to move
return
}

const newCacheKey = createPrefetchCacheKey(url, nextUrl)
prefetchCache.set(newCacheKey, existingCacheEntry)
prefetchCache.delete(existingCacheKey)
}

/**
* Creates a prefetch entry for data that has not been resolved. This will add the prefetch request to a promise queue.
*/
function createLazyPrefetchEntry({
url,
kind,
tree,
nextUrl,
buildId,
prefetchCache,
}: Pick<
ReadonlyReducerState,
'nextUrl' | 'tree' | 'buildId' | 'prefetchCache'
> & {
url: URL
kind: PrefetchKind
}): PrefetchCacheEntry {
const prefetchCacheKey = createPrefetchCacheKey(url)

// initiates the fetch request for the prefetch and attaches a listener
// to the promise to update the prefetch cache entry when the promise resolves (if necessary)
const data = prefetchQueue.enqueue(() =>
fetchServerResponse(url, tree, nextUrl, buildId, kind).then(
(prefetchResponse) => {
// TODO: `fetchServerResponse` should be more tighly coupled to these prefetch cache operations
// to avoid drift between this cache key prefixing logic
// (which is currently directly influenced by the server response)
const [, , , intercepted] = prefetchResponse
if (intercepted) {
prefixExistingPrefetchCacheEntry({ url, nextUrl, prefetchCache })
}

return prefetchResponse
}
)
)

const prefetchEntry = {
treeAtTimeOfPrefetch: tree,
data,
kind,
prefetchTime: Date.now(),
lastUsedTime: null,
key: prefetchCacheKey,
status: PrefetchCacheEntryStatus.fresh,
}

prefetchCache.set(prefetchCacheKey, prefetchEntry)

return prefetchEntry
}

export function prunePrefetchCache(
prefetchCache: AppRouterState['prefetchCache']
prefetchCache: ReadonlyReducerState['prefetchCache']
) {
for (const [href, prefetchCacheEntry] of prefetchCache) {
if (
Expand All @@ -49,7 +200,7 @@ export function prunePrefetchCache(
const FIVE_MINUTES = 5 * 60 * 1000
const THIRTY_SECONDS = 30 * 1000

export function getPrefetchEntryCacheStatus({
function getPrefetchEntryCacheStatus({
kind,
prefetchTime,
lastUsedTime,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,7 @@ describe('navigateReducer', () => {
"kind": "temporary",
"lastUsedTime": 1690329600000,
"prefetchTime": 1690329600000,
"status": "fresh",
"treeAtTimeOfPrefetch": [
"",
{
Expand Down Expand Up @@ -455,6 +456,7 @@ describe('navigateReducer', () => {
"kind": "temporary",
"lastUsedTime": 1690329600000,
"prefetchTime": 1690329600000,
"status": "fresh",
"treeAtTimeOfPrefetch": [
"",
{
Expand Down Expand Up @@ -893,6 +895,7 @@ describe('navigateReducer', () => {
"kind": "temporary",
"lastUsedTime": 1690329600000,
"prefetchTime": 1690329600000,
"status": "fresh",
"treeAtTimeOfPrefetch": [
"",
{
Expand Down Expand Up @@ -1120,6 +1123,7 @@ describe('navigateReducer', () => {
"kind": "auto",
"lastUsedTime": 1690329600000,
"prefetchTime": 1690329600000,
"status": "fresh",
"treeAtTimeOfPrefetch": [
"",
{
Expand Down Expand Up @@ -1375,6 +1379,7 @@ describe('navigateReducer', () => {
"kind": "temporary",
"lastUsedTime": 1690329600000,
"prefetchTime": 1690329600000,
"status": "fresh",
"treeAtTimeOfPrefetch": [
"",
{
Expand Down Expand Up @@ -1719,6 +1724,7 @@ describe('navigateReducer', () => {
"kind": "temporary",
"lastUsedTime": 1690329600000,
"prefetchTime": 1690329600000,
"status": "fresh",
"treeAtTimeOfPrefetch": [
"",
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import type {
ReadonlyReducerState,
ReducerState,
} from '../router-reducer-types'
import { PrefetchKind, PrefetchCacheEntryStatus } from '../router-reducer-types'
import { PrefetchCacheEntryStatus } from '../router-reducer-types'
import { handleMutable } from '../handle-mutable'
import { applyFlightData } from '../apply-flight-data'
import { prefetchQueue } from './prefetch-reducer'
Expand All @@ -28,12 +28,9 @@ import {
updateCacheNodeOnNavigation,
} from '../ppr-navigations'
import {
createPrefetchCacheKey,
getPrefetchCacheEntry,
getPrefetchEntryCacheStatus,
getOrCreatePrefetchCacheEntry,
prunePrefetchCache,
createPrefetchCacheEntry,
} from './prefetch-cache-utils'
} from '../prefetch-cache-utils'

export function handleExternalUrl(
state: ReadonlyReducerState,
Expand Down Expand Up @@ -128,27 +125,19 @@ function navigateReducer_noPPR(
return handleExternalUrl(state, mutable, url.toString(), pendingPush)
}

let prefetchValues = getPrefetchCacheEntry(url, state)
// If we don't have a prefetch value, we need to create one
if (!prefetchValues) {
const cacheKey = createPrefetchCacheKey(url)
prefetchValues = createPrefetchCacheEntry({
state,
url,
// in dev, there's never gonna be a prefetch entry so we want to prefetch here
kind:
process.env.NODE_ENV === 'development'
? PrefetchKind.AUTO
: PrefetchKind.TEMPORARY,
prefetchCacheKey: cacheKey,
})

state.prefetchCache.set(cacheKey, prefetchValues)
}

const prefetchEntryCacheStatus = getPrefetchEntryCacheStatus(prefetchValues)
const prefetchValues = getOrCreatePrefetchCacheEntry({
url,
nextUrl: state.nextUrl,
tree: state.tree,
buildId: state.buildId,
prefetchCache: state.prefetchCache,
})
const {
treeAtTimeOfPrefetch,
data,
status: prefetchEntryCacheStatus,
} = prefetchValues

const { treeAtTimeOfPrefetch, data } = prefetchValues
prefetchQueue.bump(data)

return data.then(
Expand Down Expand Up @@ -305,27 +294,19 @@ function navigateReducer_PPR(
return handleExternalUrl(state, mutable, url.toString(), pendingPush)
}

let prefetchValues = getPrefetchCacheEntry(url, state)
// If we don't have a prefetch value, we need to create one
if (!prefetchValues) {
const cacheKey = createPrefetchCacheKey(url)
prefetchValues = createPrefetchCacheEntry({
state,
url,
// in dev, there's never gonna be a prefetch entry so we want to prefetch here
kind:
process.env.NODE_ENV === 'development'
? PrefetchKind.AUTO
: PrefetchKind.TEMPORARY,
prefetchCacheKey: cacheKey,
})

state.prefetchCache.set(cacheKey, prefetchValues)
}

const prefetchEntryCacheStatus = getPrefetchEntryCacheStatus(prefetchValues)
const prefetchValues = getOrCreatePrefetchCacheEntry({
url,
nextUrl: state.nextUrl,
tree: state.tree,
buildId: state.buildId,
prefetchCache: state.prefetchCache,
})
const {
treeAtTimeOfPrefetch,
data,
status: prefetchEntryCacheStatus,
} = prefetchValues

const { treeAtTimeOfPrefetch, data } = prefetchValues
prefetchQueue.bump(data)

return data.then(
Expand Down
Loading

0 comments on commit 97b0a56

Please sign in to comment.