import { FetchStrategy } from './types' import type { NormalizedPathname, NormalizedSearch, NormalizedNextUrl, } from './cache-key' import type { RouteTree } from './cache' import { Fallback, type FallbackType } from './cache-map' import { HEAD_REQUEST_KEY } from '../../../shared/lib/segment-cache/segment-value-encoding' type Opaque = T & { __brand: K } /** * A linked-list of all the params (or other param-like) inputs that a cache * entry may vary by. This is used by the CacheMap module to reuse cache entries * across different param values. If a param has a value of Fallback, it means * the cache entry is reusable for all possible values of that param. See * cache-map.ts for details. * * A segment's vary path is a pure function of a segment's position in a * particular route tree and the (post-rewrite) URL that is being queried. More * concretely, successive queries of the cache for the same segment always use * the same vary path. * * A route's vary path is simpler: it's comprised of the pathname, search * string, and Next-URL header. */ export type VaryPath = { value: string | null | FallbackType parent: VaryPath | null } // Because it's so important for vary paths to line up across cache accesses, // we use opaque type aliases to ensure these are only created within // this module. // requestKey -> searchParams -> nextUrl export type RouteVaryPath = Opaque< { value: NormalizedPathname parent: { value: NormalizedSearch parent: { value: NormalizedNextUrl | null | FallbackType parent: null } } }, 'RouteVaryPath' > // requestKey -> pathParams export type LayoutVaryPath = Opaque< { value: string parent: PartialSegmentVaryPath | null }, 'LayoutVaryPath' > // requestKey -> searchParams -> pathParams export type PageVaryPath = Opaque< { value: string parent: { value: NormalizedSearch | FallbackType parent: PartialSegmentVaryPath | null } }, 'PageVaryPath' > export type SegmentVaryPath = LayoutVaryPath | PageVaryPath // Intermediate type used when building a vary path during a recursive traversal // of the route tree. export type PartialSegmentVaryPath = Opaque export function getRouteVaryPath( pathname: NormalizedPathname, search: NormalizedSearch, nextUrl: NormalizedNextUrl | null ): RouteVaryPath { // requestKey -> searchParams -> nextUrl const varyPath: VaryPath = { value: pathname, parent: { value: search, parent: { value: nextUrl, parent: null, }, }, } return varyPath as RouteVaryPath } export function getFulfilledRouteVaryPath( pathname: NormalizedPathname, search: NormalizedSearch, nextUrl: NormalizedNextUrl | null, couldBeIntercepted: boolean ): RouteVaryPath { // This is called when a route's data is fulfilled. The cache entry will be // re-keyed based on which inputs the response varies by. // requestKey -> searchParams -> nextUrl const varyPath: VaryPath = { value: pathname, parent: { value: search, parent: { value: couldBeIntercepted ? nextUrl : Fallback, parent: null, }, }, } return varyPath as RouteVaryPath } export function appendLayoutVaryPath( parentPath: PartialSegmentVaryPath | null, cacheKey: string ): PartialSegmentVaryPath { const varyPathPart: VaryPath = { value: cacheKey, parent: parentPath, } return varyPathPart as PartialSegmentVaryPath } export function finalizeLayoutVaryPath( requestKey: string, varyPath: PartialSegmentVaryPath | null ): LayoutVaryPath { const layoutVaryPath: VaryPath = { value: requestKey, parent: varyPath, } return layoutVaryPath as LayoutVaryPath } export function finalizePageVaryPath( requestKey: string, renderedSearch: NormalizedSearch, varyPath: PartialSegmentVaryPath | null ): PageVaryPath { // Unlike layouts, a page segment's vary path also includes the search string. // requestKey -> searchParams -> pathParams const pageVaryPath: VaryPath = { value: requestKey, parent: { value: renderedSearch, parent: varyPath, }, } return pageVaryPath as PageVaryPath } export function finalizeMetadataVaryPath( pageRequestKey: string, renderedSearch: NormalizedSearch, varyPath: PartialSegmentVaryPath | null ): PageVaryPath { // The metadata "segment" is not a real segment because it doesn't exist in // the normal structure of the route tree, but in terms of caching, it // behaves like a page segment because it varies by all the same params as // a page. // // To keep the protocol for querying the server simple, the request key for // the metadata does not include any path information. It's unnecessary from // the server's perspective, because unlike page segments, there's only one // metadata response per URL, i.e. there's no need to distinguish multiple // parallel pages. // // However, this means the metadata request key is insufficient for // caching the the metadata in the client cache, because on the client we // use the request key to distinguish the metadata entry from all other // page's metadata entries. // // So instead we create a simulated request key based on the page segment. // Conceptually this is equivalent to the request key the server would have // assigned the metadata segment if it treated it as part of the actual // route structure. // If there are multiple parallel pages, we use whichever is the first one. // This is fine because the only difference between request keys for // different parallel pages are things like route groups and parallel // route slots. As long as it's always the same one, it doesn't matter. const pageVaryPath: VaryPath = { // Append the actual metadata request key to the page request key. Note // that we're not using a separate vary path part; it's unnecessary because // these are not conceptually separate inputs. value: pageRequestKey + HEAD_REQUEST_KEY, parent: { value: renderedSearch, parent: varyPath, }, } return pageVaryPath as PageVaryPath } export function getSegmentVaryPathForRequest( fetchStrategy: FetchStrategy, tree: RouteTree ): SegmentVaryPath { // This is used for storing pending requests in the cache. We want to choose // the most generic vary path based on the strategy used to fetch it, i.e. // static/PPR versus runtime prefetching, so that it can be reused as much // as possible. // // We may be able to re-key the response to something even more generic once // we receive it — for example, if the server tells us that the response // doesn't vary on a particular param — but even before we send the request, // we know some params are reusable based on the fetch strategy alone. For // example, a static prefetch will never vary on search params. // // The original vary path with all the params filled in is stored on the // route tree object. We will clone this one to create a new vary path // where certain params are replaced with Fallback. // // This result of this function is not stored anywhere. It's only used to // access the cache a single time. // // TODO: Rather than create a new list object just to access the cache, the // plan is to add the concept of a "vary mask". This will represent all the // params that can be treated as Fallback. (Or perhaps the inverse.) const originalVaryPath = tree.varyPath // Only page segments (and the special "metadata" segment, which is treated // like a page segment for the purposes of caching) may contain search // params. There's no reason to include them in the vary path otherwise. if (tree.isPage) { // Only a runtime prefetch will include search params in the vary path. // Static prefetches never include search params, so they can be reused // across all possible search param values. const doesVaryOnSearchParams = fetchStrategy === FetchStrategy.Full || fetchStrategy === FetchStrategy.PPRRuntime if (!doesVaryOnSearchParams) { // The response from the the server will not vary on search params. Clone // the end of the original vary path to replace the search params // with Fallback. // // requestKey -> searchParams -> pathParams // ^ This part gets replaced with Fallback const searchParamsVaryPath = (originalVaryPath as PageVaryPath).parent const pathParamsVaryPath = searchParamsVaryPath.parent const patchedVaryPath: VaryPath = { value: originalVaryPath.value, parent: { value: Fallback, parent: pathParamsVaryPath, }, } return patchedVaryPath as SegmentVaryPath } } // The request does vary on search params. We don't need to modify anything. return originalVaryPath as SegmentVaryPath } export function clonePageVaryPathWithNewSearchParams( originalVaryPath: PageVaryPath, newSearch: NormalizedSearch ): PageVaryPath { // requestKey -> searchParams -> pathParams // ^ This part gets replaced with newSearch const searchParamsVaryPath = originalVaryPath.parent const clonedVaryPath: VaryPath = { value: originalVaryPath.value, parent: { value: newSearch, parent: searchParamsVaryPath.parent, }, } return clonedVaryPath as PageVaryPath }