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 778a4bb
Show file tree
Hide file tree
Showing 10 changed files with 216 additions and 272 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ 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'

export type FetchServerResponseResult = [
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,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 778a4bb

Please sign in to comment.