Skip to content

Commit

Permalink
Add Segment Cache feature check to prefetch API
Browse files Browse the repository at this point in the history
Wraps the `prefetch` API in a feature check for the Segment Cache, and
forwards the call to a stub for the new implementation.

Unlike the old implementation, the Segment Cache doesn't store its data
in the router reducer state; it writes into a global mutable cache. So
we don't need to dispatch a router action.

Since the Segment Cache isn't actually implemented yet, this effectively
disables prefetching when the experimental flag is enabled.

There's some validation that we do for prefetch URLs that I extracted
into a shared function. (For example, we only prefetch same-origin URLs,
and we don't prefetch anything in development.)
  • Loading branch information
acdlite committed Nov 15, 2024
1 parent c3bc7d0 commit 8db62d9
Show file tree
Hide file tree
Showing 3 changed files with 90 additions and 35 deletions.
90 changes: 58 additions & 32 deletions packages/next/src/client/components/app-router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ import type { FlightRouterState } from '../../server/app-render/types'
import { useNavFailureHandler } from './nav-failure-handler'
import { useServerActionDispatcher } from '../app-call-server'
import type { AppRouterActionQueue } from '../../shared/lib/router/action-queue'
import { prefetch as prefetchWithSegmentCache } from '../components/segment-cache/prefetch'

import {
getRedirectTypeFromError,
getURLFromRedirectError,
Expand All @@ -69,6 +71,43 @@ function isExternalURL(url: URL) {
return url.origin !== window.location.origin
}

/**
* Given a link href, constructs the URL that should be prefetched. Returns null
* in cases where prefetching should be disabled, like external URLs, or
* during development.
* @param href The href passed to <Link>, router.prefetch(), or similar
* @returns A URL object to prefetch, or null if prefetching should be disabled
*/
export function createPrefetchURL(href: string): URL | null {
// Don't prefetch for bots as they don't navigate.
if (isBot(window.navigator.userAgent)) {
return null
}

let url: URL
try {
url = new URL(addBasePath(href), window.location.href)
} catch (_) {
// TODO: Does this need to throw or can we just console.error instead? Does
// anyone rely on this throwing? (Seems unlikely.)
throw new Error(
`Cannot prefetch '${href}' because it cannot be converted to a URL.`
)
}

// Don't prefetch during development (improves compilation performance)
if (process.env.NODE_ENV === 'development') {
return null
}

// External urls can't be prefetched in the same way.
if (isExternalURL(url)) {
return null
}

return url
}

function HistoryUpdater({
appRouterState,
}: {
Expand Down Expand Up @@ -241,38 +280,25 @@ function Router({
const routerInstance: AppRouterInstance = {
back: () => window.history.back(),
forward: () => window.history.forward(),
prefetch: (href, options) => {
// Don't prefetch for bots as they don't navigate.
if (isBot(window.navigator.userAgent)) {
return
}

let url: URL
try {
url = new URL(addBasePath(href), window.location.href)
} catch (_) {
throw new Error(
`Cannot prefetch '${href}' because it cannot be converted to a URL.`
)
}

// Don't prefetch during development (improves compilation performance)
if (process.env.NODE_ENV === 'development') {
return
}

// External urls can't be prefetched in the same way.
if (isExternalURL(url)) {
return
}
startTransition(() => {
dispatch({
type: ACTION_PREFETCH,
url,
kind: options?.kind ?? PrefetchKind.FULL,
})
})
},
prefetch:
process.env.__NEXT_PPR && process.env.__NEXT_CLIENT_SEGMENT_CACHE
? // Unlike the old implementation, the Segment Cache doesn't store its
// data in the router reducer state; it writes into a global mutable
// cache. So we don't need to dispatch an action.
prefetchWithSegmentCache
: (href, options) => {
// Use the old prefetch implementation.
const url = createPrefetchURL(href)
if (url !== null) {
startTransition(() => {
dispatch({
type: ACTION_PREFETCH,
url,
kind: options?.kind ?? PrefetchKind.FULL,
})
})
}
},
replace: (href, options = {}) => {
startTransition(() => {
navigate(href, 'replace', options.scroll ?? true)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import type {
ReducerState,
ReadonlyReducerState,
} from '../router-reducer-types'
import { NEXT_RSC_UNION_QUERY } from '../../app-router-headers'
import { PromiseQueue } from '../../promise-queue'
import {
getOrCreatePrefetchCacheEntry,
Expand All @@ -12,15 +11,29 @@ import {

export const prefetchQueue = new PromiseQueue(5)

export function prefetchReducer(
export const prefetchReducer =
process.env.__NEXT_PPR && process.env.__NEXT_CLIENT_SEGMENT_CACHE
? identityReducerWhenSegmentCacheIsEnabled
: prefetchReducerImpl

function identityReducerWhenSegmentCacheIsEnabled<T>(state: T): T {
// Unlike the old implementation, the Segment Cache doesn't store its data in
// the router reducer state.
//
// This shouldn't be reachable because we wrap the prefetch API in a check,
// too, which prevents the action from being dispatched. But it's here for
// clarity + code elimination.
return state
}

function prefetchReducerImpl(
state: ReadonlyReducerState,
action: PrefetchAction
): ReducerState {
// let's prune the prefetch cache before we do anything else
prunePrefetchCache(state.prefetchCache)

const { url } = action
url.searchParams.delete(NEXT_RSC_UNION_QUERY)

getOrCreatePrefetchCacheEntry({
url,
Expand Down
16 changes: 16 additions & 0 deletions packages/next/src/client/components/segment-cache/prefetch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { createPrefetchURL } from '../../components/app-router'

/**
* Entrypoint for prefetching a URL into the Segment Cache.
* @param href - The URL to prefetch. Typically this will come from a <Link>,
* or router.prefetch. It must be validated before we attempt to prefetch it.
*/
export function prefetch(href: string) {
const url = createPrefetchURL(href)
if (url === null) {
// This href should not be prefetched.
return
}

// TODO: Not yet implemented
}

0 comments on commit 8db62d9

Please sign in to comment.