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 8, 2024
1 parent 4251ca3 commit cd9837c
Show file tree
Hide file tree
Showing 10 changed files with 230 additions and 242 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,9 @@ import {
} from '../app-router-headers'
import { urlToUrlWithoutFlightMarker } from '../app-router'
import { callServer } from '../../app-call-server'
import { PrefetchKind } from './router-reducer-types'
import { PrefetchKind, type PrefetchCacheEntry } from './router-reducer-types'
import { hexHash } from '../../../shared/lib/hash'
import { prefixExistingPrefetchCacheEntry } from './prefetch-cache-utils'

export type FetchServerResponseResult = [
flightData: FlightData,
Expand All @@ -49,6 +50,7 @@ export async function fetchServerResponse(
flightRouterState: FlightRouterState,
nextUrl: string | null,
currentBuildId: string,
prefetchCache?: Map<string, PrefetchCacheEntry>,
prefetchKind?: PrefetchKind
): Promise<FetchServerResponseResult> {
const headers: {
Expand Down Expand Up @@ -116,6 +118,16 @@ export async function fetchServerResponse(
const interception = !!res.headers.get('vary')?.includes(NEXT_URL)
let isFlightResponse = contentType === RSC_CONTENT_TYPE_HEADER

if (prefetchCache && interception) {
// If this fetch response corresponds with a prefetch to an interception route, we want to move it to
// a prefixed cache key to avoid clobbering an existing entry.
prefixExistingPrefetchCacheEntry({
url,
nextUrl,
prefetchCache,
})
}

if (process.env.NODE_ENV === 'production') {
if (process.env.__NEXT_CONFIG_OUTPUT === 'export') {
if (!isFlightResponse) {
Expand Down
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,171 @@ 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
}

export 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)
}

type GetOrCreatePrefetchCacheEntryParams = Pick<
ReadonlyReducerState,
'nextUrl' | 'prefetchCache' | 'tree' | 'buildId'
> & {
url: URL
createIfNotFound?: true
}

type GetPrefetchCacheEntryParams = Pick<
ReadonlyReducerState,
'nextUrl' | 'prefetchCache'
> & {
tree?: ReadonlyReducerState['tree']
buildId?: ReadonlyReducerState['buildId']
url: URL
createIfNotFound?: false
}

/**
* Returns a prefetch cache entry if one exists. Optionally creates a new one.
*/
export function getPrefetchCacheEntry({
url,
nextUrl,
tree,
buildId,
prefetchCache,
createIfNotFound,
}: GetOrCreatePrefetchCacheEntryParams): PrefetchCacheEntry
export function getPrefetchCacheEntry({
url,
nextUrl,
prefetchCache,
createIfNotFound,
}: GetPrefetchCacheEntryParams): PrefetchCacheEntry | undefined
export function getPrefetchCacheEntry({
url,
nextUrl,
tree,
buildId,
prefetchCache,
createIfNotFound,
}: GetOrCreatePrefetchCacheEntryParams | GetPrefetchCacheEntryParams):
| PrefetchCacheEntry
| undefined {
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
}
}

// We found an entry, so we can return it
if (existingCacheEntry) {
// Grab the latest status of the cache entry and update it
existingCacheEntry.status = getPrefetchEntryCacheStatus(existingCacheEntry)
return existingCacheEntry
}

// When retrieving a prefetch entry, we usually want to create one if it doesn't exist
// This let's us create a new one if it doesn't exist to avoid needing typeguards in the calling code
if (createIfNotFound) {
// If we don't have a prefetch value, we need to create one
return createLazyPrefetchEntry({
tree,
url,
buildId,
nextUrl,
prefetchCache,
// 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,
})
}
}

/**
* Creates a prefetch entry for data that has not been resolved. This will add the prefetch request to a promise queue.
*/
export 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, prefetchCache, kind)
)

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 +194,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,
prunePrefetchCache,
createPrefetchCacheEntry,
} from './prefetch-cache-utils'
} from '../prefetch-cache-utils'

export function handleExternalUrl(
state: ReadonlyReducerState,
Expand Down Expand Up @@ -128,27 +125,20 @@ 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 = getPrefetchCacheEntry({
url,
nextUrl: state.nextUrl,
tree: state.tree,
buildId: state.buildId,
prefetchCache: state.prefetchCache,
createIfNotFound: true,
})
const {
treeAtTimeOfPrefetch,
data,
status: prefetchEntryCacheStatus,
} = prefetchValues

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

return data.then(
Expand Down Expand Up @@ -305,27 +295,20 @@ 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 = getPrefetchCacheEntry({
url,
nextUrl: state.nextUrl,
tree: state.tree,
buildId: state.buildId,
prefetchCache: state.prefetchCache,
createIfNotFound: true,
})
const {
treeAtTimeOfPrefetch,
data,
status: prefetchEntryCacheStatus,
} = prefetchValues

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

return data.then(
Expand Down
Loading

0 comments on commit cd9837c

Please sign in to comment.