Recipe-app main

This commit is contained in:
2026-04-09 09:14:39 +02:00
commit 962f4e4be5
10015 changed files with 2445177 additions and 0 deletions
@@ -0,0 +1,36 @@
import type { SegmentVaryPath } from './vary-path';
/**
* Sentinel value indicating that no per-page dynamic stale time was provided.
* When this is the dynamicStaleTime, the default DYNAMIC_STALETIME_MS is used.
*/
export declare const UnknownDynamicStaleTime = -1;
/**
* Converts a dynamic stale time (in seconds, as sent by the server in the `d`
* field of the Flight response) to an absolute staleAt timestamp. When the
* value is unknown, falls back to the global DYNAMIC_STALETIME_MS.
*/
export declare function computeDynamicStaleAt(now: number, dynamicStaleTimeSeconds: number): number;
import { type UnknownMapEntry } from './cache-map';
export type BFCacheEntry = {
rsc: React.ReactNode | null;
prefetchRsc: React.ReactNode | null;
head: React.ReactNode | null;
prefetchHead: React.ReactNode | null;
ref: UnknownMapEntry | null;
size: number;
navigatedAt: number;
staleAt: number;
version: number;
};
export declare function invalidateBfCache(): void;
export declare function writeToBFCache(now: number, varyPath: SegmentVaryPath, rsc: React.ReactNode, prefetchRsc: React.ReactNode, head: React.ReactNode, prefetchHead: React.ReactNode, dynamicStaleAt: number): void;
export declare function writeHeadToBFCache(now: number, varyPath: SegmentVaryPath, head: React.ReactNode, prefetchHead: React.ReactNode, dynamicStaleAt: number): void;
/**
* Update the staleAt of an existing BFCache entry. Used after a dynamic
* response arrives with a per-page stale time from `unstable_dynamicStaleTime`.
* The per-page value is authoritative — it overrides whatever staleAt was set
* by the default DYNAMIC_STALETIME_MS.
*/
export declare function updateBFCacheEntryStaleAt(varyPath: SegmentVaryPath, newStaleAt: number): void;
export declare function readFromBFCache(varyPath: SegmentVaryPath): BFCacheEntry | null;
export declare function readFromBFCacheDuringRegularNavigation(now: number, varyPath: SegmentVaryPath): BFCacheEntry | null;
@@ -0,0 +1,128 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
0 && (module.exports = {
UnknownDynamicStaleTime: null,
computeDynamicStaleAt: null,
invalidateBfCache: null,
readFromBFCache: null,
readFromBFCacheDuringRegularNavigation: null,
updateBFCacheEntryStaleAt: null,
writeHeadToBFCache: null,
writeToBFCache: null
});
function _export(target, all) {
for(var name in all)Object.defineProperty(target, name, {
enumerable: true,
get: all[name]
});
}
_export(exports, {
UnknownDynamicStaleTime: function() {
return UnknownDynamicStaleTime;
},
computeDynamicStaleAt: function() {
return computeDynamicStaleAt;
},
invalidateBfCache: function() {
return invalidateBfCache;
},
readFromBFCache: function() {
return readFromBFCache;
},
readFromBFCacheDuringRegularNavigation: function() {
return readFromBFCacheDuringRegularNavigation;
},
updateBFCacheEntryStaleAt: function() {
return updateBFCacheEntryStaleAt;
},
writeHeadToBFCache: function() {
return writeHeadToBFCache;
},
writeToBFCache: function() {
return writeToBFCache;
}
});
const _navigatereducer = require("../router-reducer/reducers/navigate-reducer");
const _cachemap = require("./cache-map");
const UnknownDynamicStaleTime = -1;
function computeDynamicStaleAt(now, dynamicStaleTimeSeconds) {
return dynamicStaleTimeSeconds !== UnknownDynamicStaleTime ? now + dynamicStaleTimeSeconds * 1000 : now + _navigatereducer.DYNAMIC_STALETIME_MS;
}
const bfcacheMap = (0, _cachemap.createCacheMap)();
let currentBfCacheVersion = 0;
function invalidateBfCache() {
if (typeof window === 'undefined') {
return;
}
currentBfCacheVersion++;
}
function writeToBFCache(now, varyPath, rsc, prefetchRsc, head, prefetchHead, dynamicStaleAt) {
if (typeof window === 'undefined') {
return;
}
const entry = {
rsc,
prefetchRsc,
// TODO: These fields will be removed from both BFCacheEntry and
// SegmentCacheEntry. The head has its own separate cache entry.
head,
prefetchHead,
ref: null,
// TODO: This is just a heuristic. Getting the actual size of the segment
// isn't feasible because it's part of a larger streaming response. The
// LRU will still evict it, we just won't have a fully accurate total
// LRU size. However, we'll probably remove the size tracking from the LRU
// entirely and use memory pressure events instead.
size: 100,
navigatedAt: now,
// A back/forward navigation will disregard the stale time. This field is
// only relevant when staleTimes.dynamic is enabled or unstable_dynamicStaleTime
// is exported by a page.
staleAt: dynamicStaleAt,
version: currentBfCacheVersion
};
const isRevalidation = false;
(0, _cachemap.setInCacheMap)(bfcacheMap, varyPath, entry, isRevalidation);
}
function writeHeadToBFCache(now, varyPath, head, prefetchHead, dynamicStaleAt) {
// Read the special "segment" that represents the head data.
writeToBFCache(now, varyPath, head, prefetchHead, null, null, dynamicStaleAt);
}
function updateBFCacheEntryStaleAt(varyPath, newStaleAt) {
if (typeof window === 'undefined') {
return;
}
const isRevalidation = false;
// Read with staleness bypass (-1) so we can update even stale entries
const entry = (0, _cachemap.getFromCacheMap)(-1, currentBfCacheVersion, bfcacheMap, varyPath, isRevalidation);
if (entry !== null) {
entry.staleAt = newStaleAt;
}
}
function readFromBFCache(varyPath) {
if (typeof window === 'undefined') {
return null;
}
const isRevalidation = false;
return (0, _cachemap.getFromCacheMap)(// During a back/forward navigation, it doesn't matter how stale the data
// might be. Pass -1 instead of the actual current time to bypass
// staleness checks.
-1, currentBfCacheVersion, bfcacheMap, varyPath, isRevalidation);
}
function readFromBFCacheDuringRegularNavigation(now, varyPath) {
if (typeof window === 'undefined') {
return null;
}
const isRevalidation = false;
return (0, _cachemap.getFromCacheMap)(now, currentBfCacheVersion, bfcacheMap, varyPath, isRevalidation);
}
if ((typeof exports.default === 'function' || (typeof exports.default === 'object' && exports.default !== null)) && typeof exports.default.__esModule === 'undefined') {
Object.defineProperty(exports.default, '__esModule', { value: true });
Object.assign(exports.default, exports);
module.exports = exports.default;
}
//# sourceMappingURL=bfcache.js.map
File diff suppressed because one or more lines are too long
@@ -0,0 +1,13 @@
type Opaque<K, T> = T & {
__brand: K;
};
export type NormalizedPathname = Opaque<'NormalizedPathname', string>;
export type NormalizedSearch = Opaque<'NormalizedSearch', string>;
export type NormalizedNextUrl = Opaque<'NormalizedNextUrl', string>;
export type RouteCacheKey = Opaque<'RouteCacheKey', {
pathname: NormalizedPathname;
search: NormalizedSearch;
nextUrl: NormalizedNextUrl | null;
}>;
export declare function createCacheKey(originalHref: string, nextUrl: string | null): RouteCacheKey;
export {};
@@ -0,0 +1,28 @@
// TypeScript trick to simulate opaque types, like in Flow.
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
Object.defineProperty(exports, "createCacheKey", {
enumerable: true,
get: function() {
return createCacheKey;
}
});
function createCacheKey(originalHref, nextUrl) {
const originalUrl = new URL(originalHref);
const cacheKey = {
pathname: originalUrl.pathname,
search: originalUrl.search,
nextUrl: nextUrl
};
return cacheKey;
}
if ((typeof exports.default === 'function' || (typeof exports.default === 'object' && exports.default !== null)) && typeof exports.default.__esModule === 'undefined') {
Object.defineProperty(exports.default, '__esModule', { value: true });
Object.assign(exports.default, exports);
module.exports = exports.default;
}
//# sourceMappingURL=cache-key.js.map
@@ -0,0 +1 @@
{"version":3,"sources":["../../../../src/client/components/segment-cache/cache-key.ts"],"sourcesContent":["// TypeScript trick to simulate opaque types, like in Flow.\ntype Opaque<K, T> = T & { __brand: K }\n\n// Only functions in this module should be allowed to create CacheKeys.\nexport type NormalizedPathname = Opaque<'NormalizedPathname', string>\nexport type NormalizedSearch = Opaque<'NormalizedSearch', string>\nexport type NormalizedNextUrl = Opaque<'NormalizedNextUrl', string>\n\nexport type RouteCacheKey = Opaque<\n 'RouteCacheKey',\n {\n pathname: NormalizedPathname\n search: NormalizedSearch\n nextUrl: NormalizedNextUrl | null\n\n // TODO: Eventually the dynamic params will be added here, too.\n }\n>\n\nexport function createCacheKey(\n originalHref: string,\n nextUrl: string | null\n): RouteCacheKey {\n const originalUrl = new URL(originalHref)\n const cacheKey = {\n pathname: originalUrl.pathname as NormalizedPathname,\n search: originalUrl.search as NormalizedSearch,\n nextUrl: nextUrl as NormalizedNextUrl | null,\n } as RouteCacheKey\n return cacheKey\n}\n"],"names":["createCacheKey","originalHref","nextUrl","originalUrl","URL","cacheKey","pathname","search"],"mappings":"AAAA,2DAA2D;;;;;+BAmB3CA;;;eAAAA;;;AAAT,SAASA,eACdC,YAAoB,EACpBC,OAAsB;IAEtB,MAAMC,cAAc,IAAIC,IAAIH;IAC5B,MAAMI,WAAW;QACfC,UAAUH,YAAYG,QAAQ;QAC9BC,QAAQJ,YAAYI,MAAM;QAC1BL,SAASA;IACX;IACA,OAAOG;AACT","ignoreList":[0]}
@@ -0,0 +1,117 @@
import type { VaryPath } from './vary-path';
/**
* A specialized data type for storing multi-key cache entries.
*
* The basic structure is a map whose keys are tuples, called the keypath.
* When querying the cache, keypaths are compared per-element.
*
* Example:
* set(map, ['https://localhost', 'foo/bar/baz'], 'yay');
* get(map, ['https://localhost', 'foo/bar/baz']) -> 'yay'
*
* NOTE: Array syntax is used in these examples for illustration purposes, but
* in reality the paths are lists.
*
* The parts of the keypath represent the different inputs that contribute
* to the entry value. To illustrate, if you were to use this data type to store
* HTTP responses, the keypath would include the URL and everything listed by
* the Vary header.
*
* See vary-path.ts for more details.
*
* The order of elements in a keypath must be consistent between lookups to
* be considered the same, but besides that, the order of the keys is not
* semantically meaningful.
*
* Keypaths may include a special kind of key called Fallback. When an entry is
* stored with Fallback as part of its keypath, it means that the entry does not
* vary by that key. When querying the cache, if an exact match is not found for
* a keypath, the cache will check for a Fallback match instead. Each element of
* the keypath may have a Fallback, so retrieval is an O(n ^ 2) operation, but
* it's expected that keypaths are relatively short.
*
* Example:
* set(cacheMap, ['store', 'product', 1], PRODUCT_PAGE_1);
* set(cacheMap, ['store', 'product', Fallback], GENERIC_PRODUCT_PAGE);
*
* // Exact match
* get(cacheMap, ['store', 'product', 1]) -> PRODUCT_PAGE_1
*
* // Fallback match
* get(cacheMap, ['store', 'product', 2]) -> GENERIC_PRODUCT_PAGE
*
* Because we have the Fallback mechanism, we can impose a constraint that
* regular JS maps do not have: a value cannot be stored at multiple keypaths
* simultaneously. These cases should be expressed with Fallback keys instead.
*
* Additionally, because values only exist at a single keypath at a time, we
* can optimize successive lookups by caching the internal map entry on the
* value itself, using the `ref` field. This is especially useful because it
* lets us skip the O(n ^ 2) lookup that occurs when Fallback entries
* are present.
*
* How to decide if stuff belongs in here, or in cache.ts?
* -------------------------------------------------------
*
* Anything to do with retrival, lifetimes, or eviction needs to go in this
* module because it affects the fallback algorithm. For example, when
* performing a lookup, if an entry is stale, it needs to be treated as
* semantically equivalent to if the entry was not present at all.
*
* If there's logic that's not related to the fallback algorithm, though, we
* should prefer to put it in cache.ts.
*/
export interface MapValue {
ref: UnknownMapEntry | null;
size: number;
staleAt: number;
version: number;
}
/**
* Represents a node in the cache map and LRU.
* MapEntry<V> structurally satisfies this interface for any V extends MapValue.
*
* The LRU can contain entries of different value types
* (e.g., both RouteCacheEntry and SegmentCacheEntry). This interface captures
* the common structure needed for cache map and LRU operations without
* requiring knowledge of the specific value type.
*/
export interface MapEntry<V extends MapValue> {
parent: MapEntry<V> | null;
key: unknown;
map: Map<unknown, MapEntry<V>> | null;
value: V | null;
prev: MapEntry<V> | null;
next: MapEntry<V> | null;
size: number;
}
/**
* A looser type for MapEntry
* This allows the LRU to work with entries of different
* value types while still providing type safety.
*
* The `map` field lets Map<unknown, MapEntry<V>> be assignable to this
* type since we're only reading from the map, not inserting into it.
*/
export type UnknownMapEntry = {
parent: UnknownMapEntry | null;
key: unknown;
map: Pick<Map<unknown, UnknownMapEntry>, 'get' | 'delete' | 'size'> | null;
value: MapValue | null;
prev: UnknownMapEntry | null;
next: UnknownMapEntry | null;
size: number;
};
export type CacheMap<V extends MapValue> = MapEntry<V>;
export type FallbackType = {
__brand: 'Fallback';
};
export declare const Fallback: FallbackType;
export declare function createCacheMap<V extends MapValue>(): CacheMap<V>;
export declare function getFromCacheMap<V extends MapValue>(now: number, currentCacheVersion: number, rootEntry: CacheMap<V>, keys: VaryPath, isRevalidation: boolean): V | null;
export declare function isValueExpired(now: number, currentCacheVersion: number, value: MapValue): boolean;
export declare function setInCacheMap<V extends MapValue>(cacheMap: CacheMap<V>, keys: VaryPath, value: V, isRevalidation: boolean): void;
export declare function deleteFromCacheMap(value: MapValue): void;
export declare function deleteMapEntry(entry: UnknownMapEntry): void;
export declare function setSizeInCacheMap<V extends MapValue>(value: V, size: number): void;
@@ -0,0 +1,305 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
0 && (module.exports = {
Fallback: null,
createCacheMap: null,
deleteFromCacheMap: null,
deleteMapEntry: null,
getFromCacheMap: null,
isValueExpired: null,
setInCacheMap: null,
setSizeInCacheMap: null
});
function _export(target, all) {
for(var name in all)Object.defineProperty(target, name, {
enumerable: true,
get: all[name]
});
}
_export(exports, {
Fallback: function() {
return Fallback;
},
createCacheMap: function() {
return createCacheMap;
},
deleteFromCacheMap: function() {
return deleteFromCacheMap;
},
deleteMapEntry: function() {
return deleteMapEntry;
},
getFromCacheMap: function() {
return getFromCacheMap;
},
isValueExpired: function() {
return isValueExpired;
},
setInCacheMap: function() {
return setInCacheMap;
},
setSizeInCacheMap: function() {
return setSizeInCacheMap;
}
});
const _lru = require("./lru");
const Fallback = {};
// This is a special internal key that is used for "revalidation" entries. It's
// an implementation detail that shouldn't leak outside of this module.
const Revalidation = {};
function createCacheMap() {
const cacheMap = {
parent: null,
key: null,
value: null,
map: null,
// LRU-related fields
prev: null,
next: null,
size: 0
};
return cacheMap;
}
function getOrInitialize(cacheMap, keys, isRevalidation) {
// Go through each level of keys until we find the entry that matches, or
// create a new entry if one doesn't exist.
//
// This function will only return entries that match the keypath _exactly_.
// Unlike getWithFallback, it will not access fallback entries unless it's
// explicitly part of the keypath.
let entry = cacheMap;
let remainingKeys = keys;
let key = null;
while(true){
const previousKey = key;
if (remainingKeys !== null) {
key = remainingKeys.value;
remainingKeys = remainingKeys.parent;
} else if (isRevalidation && previousKey !== Revalidation) {
// During a revalidation, we append an internal "Revalidation" key to
// the end of the keypath. The "normal" entry is its parent.
// However, if the parent entry is currently empty, we don't need to store
// this as a revalidation entry. Just insert the revalidation into the
// normal slot.
if (entry.value === null) {
return entry;
}
// Otheriwse, create a child entry.
key = Revalidation;
} else {
break;
}
let map = entry.map;
if (map !== null) {
const existingEntry = map.get(key);
if (existingEntry !== undefined) {
// Found a match. Keep going.
entry = existingEntry;
continue;
}
} else {
map = new Map();
entry.map = map;
}
// No entry exists yet at this level. Create a new one.
const newEntry = {
parent: entry,
key,
value: null,
map: null,
// LRU-related fields
prev: null,
next: null,
size: 0
};
map.set(key, newEntry);
entry = newEntry;
}
return entry;
}
function getFromCacheMap(now, currentCacheVersion, rootEntry, keys, isRevalidation) {
const entry = getEntryWithFallbackImpl(now, currentCacheVersion, rootEntry, keys, isRevalidation, 0);
if (entry === null || entry.value === null) {
return null;
}
// This is an LRU access. Move the entry to the front of the list.
(0, _lru.lruPut)(entry);
return entry.value;
}
function isValueExpired(now, currentCacheVersion, value) {
return value.staleAt <= now || value.version < currentCacheVersion;
}
function lazilyEvictIfNeeded(now, currentCacheVersion, entry) {
// We have a matching entry, but before we can return it, we need to check if
// it's still fresh. Otherwise it should be treated the same as a cache miss.
if (entry.value === null) {
// This entry has no value, so there's nothing to evict.
return entry;
}
const value = entry.value;
if (isValueExpired(now, currentCacheVersion, value)) {
// The value expired. Lazily evict it from the cache, and return null. This
// is conceptually the same as a cache miss.
deleteMapEntry(entry);
return null;
}
// The matched entry has not expired. Return it.
return entry;
}
function getEntryWithFallbackImpl(now, currentCacheVersion, entry, keys, isRevalidation, previousKey) {
// This is similar to getExactEntry, but if an exact match is not found for
// a key, it will return the fallback entry instead. This is recursive at
// every level, e.g. an entry with keypath [a, Fallback, c, Fallback] is
// valid match for [a, b, c, d].
//
// It will return the most specific match available.
let key;
let remainingKeys;
if (keys !== null) {
key = keys.value;
remainingKeys = keys.parent;
} else if (isRevalidation && previousKey !== Revalidation) {
// During a revalidation, we append an internal "Revalidation" key to
// the end of the keypath.
key = Revalidation;
remainingKeys = null;
} else {
// There are no more keys. This is the terminal entry.
// TODO: When performing a lookup during a navigation, as opposed to a
// prefetch, we may want to skip entries that are Pending if there's also
// a Fulfilled fallback entry. Tricky to say, though, since if it's
// already pending, it's likely to stream in soon. Maybe we could do this
// just on slow connections and offline mode.
return lazilyEvictIfNeeded(now, currentCacheVersion, entry);
}
const map = entry.map;
if (map !== null) {
const existingEntry = map.get(key);
if (existingEntry !== undefined) {
// Found an exact match for this key. Keep searching.
const result = getEntryWithFallbackImpl(now, currentCacheVersion, existingEntry, remainingKeys, isRevalidation, key);
if (result !== null) {
return result;
}
}
// No match found for this key. Check if there's a fallback.
const fallbackEntry = map.get(Fallback);
if (fallbackEntry !== undefined) {
// Found a fallback for this key. Keep searching.
return getEntryWithFallbackImpl(now, currentCacheVersion, fallbackEntry, remainingKeys, isRevalidation, key);
}
}
return null;
}
function setInCacheMap(cacheMap, keys, value, isRevalidation) {
// Add a value to the map at the given keypath. If the value is already
// part of the map, it's removed from its previous keypath. (NOTE: This is
// unlike a regular JS map, but the behavior is intentional.)
const entry = getOrInitialize(cacheMap, keys, isRevalidation);
setMapEntryValue(entry, value);
// This is an LRU access. Move the entry to the front of the list.
(0, _lru.lruPut)(entry);
(0, _lru.updateLruSize)(entry, value.size);
}
function setMapEntryValue(entry, value) {
if (entry.value !== null) {
// There's already a value at the given keypath. Disconnect the old value
// from the map. We're not calling `deleteMapEntry` here because the
// entry itself is still in the map. We just want to overwrite its value.
dropRef(entry.value);
entry.value = null;
}
// This value may already be in the map at a different keypath.
// Grab a reference before we overwrite it.
const oldEntry = value.ref;
entry.value = value;
value.ref = entry;
(0, _lru.updateLruSize)(entry, value.size);
if (oldEntry !== null && oldEntry !== entry && oldEntry.value === value) {
// This value is already in the map at a different keypath in the map.
// Values only exist at a single keypath at a time. Remove it from the
// previous keypath.
//
// Note that only the internal map entry is garbage collected; we don't
// call `dropRef` here because it's still in the map, just
// at a new keypath (the one we just set, above).
deleteMapEntry(oldEntry);
}
}
function deleteFromCacheMap(value) {
const entry = value.ref;
if (entry === null) {
// This value is not a member of any map.
return;
}
dropRef(value);
deleteMapEntry(entry);
}
function dropRef(value) {
// Drop the value from the map by setting its `ref` backpointer to
// null. This is a separate operation from `deleteMapEntry` because when
// re-keying a value we need to be able to delete the old, internal map
// entry without garbage collecting the value itself.
value.ref = null;
}
function deleteMapEntry(entry) {
// Delete the entry from the cache.
entry.value = null;
(0, _lru.deleteFromLru)(entry);
// Check if we can garbage collect the entry.
const map = entry.map;
if (map === null) {
// Since this entry has no value, and also no child entries, we can
// garbage collect it. Remove it from its parent, and keep garbage
// collecting the parents until we reach a non-empty entry.
let parent = entry.parent;
let key = entry.key;
while(parent !== null){
const parentMap = parent.map;
if (parentMap !== null) {
parentMap.delete(key);
if (parentMap.size === 0) {
// We just removed the last entry in the parent map.
parent.map = null;
if (parent.value === null) {
// The parent node has no child entries, nor does it have a value
// on itself. It can be garbage collected. Keep going.
key = parent.key;
parent = parent.parent;
continue;
}
}
}
break;
}
} else {
// Check if there's a revalidating entry. If so, promote it to a
// "normal" entry, since the normal one was just deleted.
const revalidatingEntry = map.get(Revalidation);
if (revalidatingEntry !== undefined && revalidatingEntry.value !== null) {
setMapEntryValue(entry, revalidatingEntry.value);
}
}
}
function setSizeInCacheMap(value, size) {
const entry = value.ref;
if (entry === null) {
// This value is not a member of any map.
return;
}
// Except during initialization (when the size is set to 0), this is the only
// place the `size` field should be updated, to ensure it's in sync with the
// the LRU.
value.size = size;
(0, _lru.updateLruSize)(entry, size);
}
if ((typeof exports.default === 'function' || (typeof exports.default === 'object' && exports.default !== null)) && typeof exports.default.__esModule === 'undefined') {
Object.defineProperty(exports.default, '__esModule', { value: true });
Object.assign(exports.default, exports);
module.exports = exports.default;
}
//# sourceMappingURL=cache-map.js.map
File diff suppressed because one or more lines are too long
@@ -0,0 +1,267 @@
import type { FlightData, Segment as FlightRouterStateSegment } from '../../../shared/lib/app-router-types';
import { type VaryParams, type VaryParamsThenable } from '../../../shared/lib/segment-cache/vary-params-decoding';
import { type RSCResponse } from '../router-reducer/fetch-server-response';
import { type PrefetchTask, type PrefetchSubtaskResult } from './scheduler';
import { type SegmentVaryPath, type PageVaryPath, type LayoutVaryPath } from './vary-path';
import type { NormalizedPathname, NormalizedSearch, RouteCacheKey } from './cache-key';
import { type UnknownMapEntry } from './cache-map';
import { type SegmentRequestKey } from '../../../shared/lib/segment-cache/segment-value-encoding';
import type { FlightRouterState } from '../../../shared/lib/app-router-types';
import { type NormalizedFlightData } from '../../flight-data-helpers';
import { FetchStrategy } from './types';
import { type NavigationSeed } from './navigation';
/**
* Ensures a minimum stale time of 30s to avoid issues where the server sends a too
* short-lived stale time, which would prevent anything from being prefetched.
*/
export declare function getStaleTimeMs(staleTimeSeconds: number): number;
type RouteTreeShared = {
requestKey: SegmentRequestKey;
segment: FlightRouterStateSegment;
refreshState: RefreshState | null;
slots: null | {
[parallelRouteKey: string]: RouteTree;
};
prefetchHints: number;
};
export type RefreshState = {
canonicalUrl: string;
renderedSearch: NormalizedSearch;
};
type LayoutRouteTree = RouteTreeShared & {
isPage: false;
varyPath: LayoutVaryPath;
};
type PageRouteTree = RouteTreeShared & {
isPage: true;
varyPath: PageVaryPath;
};
export type RouteTree = LayoutRouteTree | PageRouteTree;
type RouteCacheEntryShared = {
couldBeIntercepted: boolean;
ref: UnknownMapEntry | null;
size: number;
staleAt: number;
version: number;
};
/**
* Tracks the status of a cache entry as it progresses from no data (Empty),
* waiting for server data (Pending), and finished (either Fulfilled or
* Rejected depending on the response from the server.
*/
export declare const enum EntryStatus {
Empty = 0,
Pending = 1,
Fulfilled = 2,
Rejected = 3
}
export type PendingRouteCacheEntry = RouteCacheEntryShared & {
status: EntryStatus.Empty | EntryStatus.Pending;
blockedTasks: Set<PrefetchTask> | null;
canonicalUrl: null;
renderedSearch: null;
tree: null;
metadata: null;
supportsPerSegmentPrefetching: false;
};
type RejectedRouteCacheEntry = RouteCacheEntryShared & {
status: EntryStatus.Rejected;
blockedTasks: Set<PrefetchTask> | null;
canonicalUrl: null;
renderedSearch: null;
tree: null;
metadata: null;
supportsPerSegmentPrefetching: boolean;
};
export type FulfilledRouteCacheEntry = RouteCacheEntryShared & {
status: EntryStatus.Fulfilled;
blockedTasks: null;
canonicalUrl: string;
renderedSearch: NormalizedSearch;
tree: RouteTree;
metadata: RouteTree;
supportsPerSegmentPrefetching: boolean;
hasDynamicRewrite: boolean;
};
export type RouteCacheEntry = PendingRouteCacheEntry | FulfilledRouteCacheEntry | RejectedRouteCacheEntry;
type SegmentCacheEntryShared = {
fetchStrategy: FetchStrategy;
ref: UnknownMapEntry | null;
size: number;
staleAt: number;
version: number;
};
export type EmptySegmentCacheEntry = SegmentCacheEntryShared & {
status: EntryStatus.Empty;
rsc: null;
isPartial: true;
promise: null;
};
export type PendingSegmentCacheEntry = SegmentCacheEntryShared & {
status: EntryStatus.Pending;
rsc: null;
isPartial: boolean;
promise: null | PromiseWithResolvers<FulfilledSegmentCacheEntry | null>;
};
type RejectedSegmentCacheEntry = SegmentCacheEntryShared & {
status: EntryStatus.Rejected;
rsc: null;
isPartial: true;
promise: null;
};
export type FulfilledSegmentCacheEntry = SegmentCacheEntryShared & {
status: EntryStatus.Fulfilled;
rsc: React.ReactNode | null;
isPartial: boolean;
promise: null;
};
export type SegmentCacheEntry = EmptySegmentCacheEntry | PendingSegmentCacheEntry | RejectedSegmentCacheEntry | FulfilledSegmentCacheEntry;
export type NonEmptySegmentCacheEntry = Exclude<SegmentCacheEntry, EmptySegmentCacheEntry>;
export declare function getCurrentRouteCacheVersion(): number;
export declare function getCurrentSegmentCacheVersion(): number;
/**
* Invalidates all prefetch cache entries (both route and segment caches).
*
* After invalidation, triggers re-prefetching of visible links and notifies
* invalidation listeners.
*/
export declare function invalidateEntirePrefetchCache(nextUrl: string | null, tree: FlightRouterState): void;
/**
* Invalidates all route cache entries. Route entries contain the tree structure
* (which segments exist at a given URL) but not the segment data itself.
*
* After invalidation, triggers re-prefetching of visible links and notifies
* invalidation listeners.
*/
export declare function invalidateRouteCacheEntries(nextUrl: string | null, tree: FlightRouterState): void;
/**
* Invalidates all segment cache entries. Segment entries contain the actual
* RSC data for each segment.
*
* After invalidation, triggers re-prefetching of visible links and notifies
* invalidation listeners.
*/
export declare function invalidateSegmentCacheEntries(nextUrl: string | null, tree: FlightRouterState): void;
export declare function pingInvalidationListeners(nextUrl: string | null, tree: FlightRouterState): void;
export declare function readRouteCacheEntry(now: number, key: RouteCacheKey): RouteCacheEntry | null;
export declare function readSegmentCacheEntry(now: number, varyPath: SegmentVaryPath): SegmentCacheEntry | null;
export declare function waitForSegmentCacheEntry(pendingEntry: PendingSegmentCacheEntry): Promise<FulfilledSegmentCacheEntry | null>;
/**
* Checks if an entry for a route exists in the cache. If so, it returns the
* entry, If not, it adds an empty entry to the cache and returns it.
*/
export declare function readOrCreateRouteCacheEntry(now: number, task: PrefetchTask, key: RouteCacheKey): RouteCacheEntry;
export declare function deprecated_requestOptimisticRouteCacheEntry(now: number, requestedUrl: URL, nextUrl: string | null): FulfilledRouteCacheEntry | null;
/**
* Checks if an entry for a segment exists in the cache. If so, it returns the
* entry, If not, it adds an empty entry to the cache and returns it.
*/
export declare function readOrCreateSegmentCacheEntry(now: number, fetchStrategy: FetchStrategy, tree: RouteTree): SegmentCacheEntry;
export declare function readOrCreateRevalidatingSegmentEntry(now: number, fetchStrategy: FetchStrategy, tree: RouteTree): SegmentCacheEntry;
export declare function overwriteRevalidatingSegmentCacheEntry(now: number, fetchStrategy: FetchStrategy, tree: RouteTree): EmptySegmentCacheEntry;
export declare function upsertSegmentEntry(now: number, varyPath: SegmentVaryPath, candidateEntry: SegmentCacheEntry): SegmentCacheEntry | null;
export declare function createDetachedSegmentCacheEntry(now: number): EmptySegmentCacheEntry;
export declare function upgradeToPendingSegment(emptyEntry: EmptySegmentCacheEntry, fetchStrategy: FetchStrategy): PendingSegmentCacheEntry;
export declare function attemptToFulfillDynamicSegmentFromBFCache(now: number, segment: EmptySegmentCacheEntry, tree: RouteTree): FulfilledSegmentCacheEntry | null;
/**
* Attempts to replace an existing segment cache entry with data from the
* bfcache. Unlike `attemptToFulfillDynamicSegmentFromBFCache` (which fills an
* empty entry), this creates a new entry and upserts it, so it works even when
* the segment is already fulfilled.
*/
export declare function attemptToUpgradeSegmentFromBFCache(now: number, tree: RouteTree): FulfilledSegmentCacheEntry | null;
export declare function createMetadataRouteTree(metadataVaryPath: PageVaryPath): RouteTree;
export declare function fulfillRouteCacheEntry(now: number, entry: PendingRouteCacheEntry, tree: RouteTree, metadataVaryPath: PageVaryPath, couldBeIntercepted: boolean, canonicalUrl: string, supportsPerSegmentPrefetching: boolean): FulfilledRouteCacheEntry;
export declare function writeRouteIntoCache(now: number, pathname: NormalizedPathname, nextUrl: string | null, tree: RouteTree, metadataVaryPath: PageVaryPath, couldBeIntercepted: boolean, canonicalUrl: string, supportsPerSegmentPrefetching: boolean): FulfilledRouteCacheEntry;
/**
* Marks a route cache entry as having a dynamic rewrite. Called when we
* discover that a route pattern has dynamic rewrite behavior - i.e., we used
* an optimistic route tree for prediction, but the server responded with a
* different rendered pathname.
*
* Once marked, attempts to use this entry as a template for prediction will
* bail out to server resolution.
*/
export declare function markRouteEntryAsDynamicRewrite(entry: FulfilledRouteCacheEntry): void;
type RouteTreeAccumulator = {
metadataVaryPath: PageVaryPath | null;
};
export declare function convertRootFlightRouterStateToRouteTree(flightRouterState: FlightRouterState, renderedSearch: NormalizedSearch, acc: RouteTreeAccumulator): RouteTree;
export declare function convertReusedFlightRouterStateToRouteTree(parentRouteTree: RouteTree, parallelRouteKey: string, flightRouterState: FlightRouterState, renderedSearch: NormalizedSearch, acc: RouteTreeAccumulator): RouteTree;
export declare function convertRouteTreeToFlightRouterState(routeTree: RouteTree): FlightRouterState;
export declare function fetchRouteOnCacheMiss(entry: PendingRouteCacheEntry, key: RouteCacheKey): Promise<PrefetchSubtaskResult<null> | null>;
export declare function fetchSegmentOnCacheMiss(route: FulfilledRouteCacheEntry, segmentCacheEntry: PendingSegmentCacheEntry, routeKey: RouteCacheKey, tree: RouteTree): Promise<PrefetchSubtaskResult<FulfilledSegmentCacheEntry> | null>;
export declare function fetchInlinedSegmentsOnCacheMiss(route: FulfilledRouteCacheEntry, routeKey: RouteCacheKey, tree: RouteTree, spawnedEntries: Map<SegmentRequestKey, PendingSegmentCacheEntry>): Promise<PrefetchSubtaskResult<null> | null>;
export declare function fetchSegmentPrefetchesUsingDynamicRequest(task: PrefetchTask, route: FulfilledRouteCacheEntry, fetchStrategy: FetchStrategy.LoadingBoundary | FetchStrategy.PPRRuntime | FetchStrategy.Full, dynamicRequestTree: FlightRouterState, spawnedEntries: Map<SegmentRequestKey, PendingSegmentCacheEntry>): Promise<PrefetchSubtaskResult<null> | null>;
export declare function writeDynamicRenderResponseIntoCache(now: number, fetchStrategy: FetchStrategy.LoadingBoundary | FetchStrategy.PPR | FetchStrategy.PPRRuntime | FetchStrategy.Full, flightDatas: NormalizedFlightData[], buildId: string | undefined, isResponsePartial: boolean, headVaryParams: VaryParams | null, staleAt: number, navigationSeed: NavigationSeed, spawnedEntries: Map<SegmentRequestKey, PendingSegmentCacheEntry> | null): Array<FulfilledSegmentCacheEntry> | null;
/**
* Checks whether the new fetch strategy is likely to provide more content than the old one.
*
* Generally, when an app uses dynamic data, a "more specific" fetch strategy is expected to provide more content:
* - `LoadingBoundary` only provides static layouts
* - `PPR` can provide shells for each segment (even for segments that use dynamic data)
* - `PPRRuntime` can additionally include content that uses searchParams, params, or cookies
* - `Full` includes all the content, even if it uses dynamic data
*
* However, it's possible that a more specific fetch strategy *won't* give us more content if:
* - a segment is fully static
* (then, `PPR`/`PPRRuntime`/`Full` will all yield equivalent results)
* - providing searchParams/params/cookies doesn't reveal any more content, e.g. because of an `await connection()`
* (then, `PPR` and `PPRRuntime` will yield equivalent results, only `Full` will give us more)
* Because of this, when comparing two segments, we should also check if the existing segment is partial.
* If it's not partial, then there's no need to prefetch it again, even using a "more specific" strategy.
* There's currently no way to know if `PPRRuntime` will yield more data that `PPR`, so we have to assume it will.
*
* Also note that, in practice, we don't expect to be comparing `LoadingBoundary` to `PPR`/`PPRRuntime`,
* because a non-PPR-enabled route wouldn't ever use the latter strategies. It might however use `Full`.
*/
export declare function canNewFetchStrategyProvideMoreContent(currentStrategy: FetchStrategy, newStrategy: FetchStrategy): boolean;
/**
* Reads the stale time from an async iterable or a response header and
* returns a staleAt timestamp.
*
* TODO: Buffer the response and then read the iterable values
* synchronously, similar to readVaryParams. This would avoid the need to
* make this async, and we could also use it in
* writeDynamicTreeResponseIntoCache. This will also be needed when React
* starts leaving async iterables hanging when the outer RSC stream is
* aborted e.g. due to sync I/O (with unstable_allowPartialStream).
*/
export declare function getStaleAt(now: number, staleTimeIterable: AsyncIterable<number> | undefined, response?: RSCResponse<unknown>): Promise<number>;
/**
* Writes the static stage of a navigation response into the segment cache.
* When `isResponsePartial` is false, segments are written as non-partial with
* `FetchStrategy.Full` so no dynamic follow-up is needed. Default segments
* are skipped (by `writeSeedDataIntoCache`) to avoid caching fallback content
* that would block refreshes from overwriting with dynamic data.
*/
export declare function writeStaticStageResponseIntoCache(now: number, flightData: FlightData, buildId: string | undefined, headVaryParamsThenable: VaryParamsThenable | null, staleAt: number, baseTree: FlightRouterState, renderedSearch: string, isResponsePartial: boolean): void;
/**
* Decodes an embedded runtime prefetch Flight stream, normalizes the flight
* data, and derives a `NavigationSeed` from the base tree.
*
* Returns `null` if the response triggers an MPA navigation.
*/
export declare function processRuntimePrefetchStream(now: number, runtimePrefetchStream: ReadableStream<Uint8Array>, baseTree: FlightRouterState, renderedSearch: string): Promise<{
flightDatas: NormalizedFlightData[];
navigationSeed: NavigationSeed;
buildId: string | undefined;
isResponsePartial: boolean;
headVaryParams: VaryParams | null;
staleAt: number;
} | null>;
/**
* Strips the leading isPartial byte from an RSC response stream.
*
* The server prepends a single byte: '~' (0x7e) for partial, '#' (0x23) for
* complete. These bytes cannot appear as the first byte of a valid RSC Flight
* response (Flight rows start with a hex digit or ':').
*
* If the first byte is not a recognized marker, the stream is returned intact
* and `isPartial` is determined by the cachedNavigations experimental flag.
*/
export declare function stripIsPartialByte(stream: ReadableStream<Uint8Array>): Promise<{
stream: ReadableStream<Uint8Array>;
isPartial: boolean;
}>;
export {};
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
@@ -0,0 +1,5 @@
import type { UnknownMapEntry } from './cache-map';
export declare function lruPut(node: UnknownMapEntry): void;
export declare function updateLruSize(node: UnknownMapEntry, newNodeSize: number): void;
export declare function deleteFromLru(deleted: UnknownMapEntry): void;
export declare function cleanup(): void;
+152
View File
@@ -0,0 +1,152 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
0 && (module.exports = {
cleanup: null,
deleteFromLru: null,
lruPut: null,
updateLruSize: null
});
function _export(target, all) {
for(var name in all)Object.defineProperty(target, name, {
enumerable: true,
get: all[name]
});
}
_export(exports, {
cleanup: function() {
return cleanup;
},
deleteFromLru: function() {
return deleteFromLru;
},
lruPut: function() {
return lruPut;
},
updateLruSize: function() {
return updateLruSize;
}
});
const _cachemap = require("./cache-map");
const _scheduler = require("./scheduler");
// We use an LRU for memory management. We must update this whenever we add or
// remove a new cache entry, or when an entry changes size.
let head = null;
let lruSize = 0;
// TODO: I chose the max size somewhat arbitrarily. Consider setting this based
// on navigator.deviceMemory, or some other heuristic. We should make this
// customizable via the Next.js config, too.
const maxLruSize = 50 * 1024 * 1024 // 50 MB
;
function lruPut(node) {
if (head === node) {
// Already at the head
return;
}
const prev = node.prev;
const next = node.next;
if (next === null || prev === null) {
// This is an insertion
lruSize += node.size;
// Whenever we add an entry, we need to check if we've exceeded the
// max size. We don't evict entries immediately; they're evicted later in
// an asynchronous task.
ensureCleanupIsScheduled();
} else {
// This is a move. Remove from its current position.
prev.next = next;
next.prev = prev;
}
// Move to the front of the list
if (head === null) {
// This is the first entry
node.prev = node;
node.next = node;
} else {
// Add to the front of the list
const tail = head.prev;
node.prev = tail;
// In practice, this is never null, but that isn't encoded in the type
if (tail !== null) {
tail.next = node;
}
node.next = head;
head.prev = node;
}
head = node;
}
function updateLruSize(node, newNodeSize) {
// This is a separate function from `put` so that we can resize the entry
// regardless of whether it's currently being tracked by the LRU.
const prevNodeSize = node.size;
node.size = newNodeSize;
if (node.next === null) {
// This entry is not currently being tracked by the LRU.
return;
}
// Update the total LRU size
lruSize = lruSize - prevNodeSize + newNodeSize;
ensureCleanupIsScheduled();
}
function deleteFromLru(deleted) {
const next = deleted.next;
const prev = deleted.prev;
if (next !== null && prev !== null) {
lruSize -= deleted.size;
deleted.next = null;
deleted.prev = null;
// Remove from the list
if (head === deleted) {
// Update the head
if (next === head) {
// This was the last entry
head = null;
} else {
head = next;
prev.next = next;
next.prev = prev;
}
} else {
prev.next = next;
next.prev = prev;
}
} else {
// Already deleted
}
}
function ensureCleanupIsScheduled() {
if (lruSize <= maxLruSize) {
return;
}
// To schedule cleanup, ping the prefetch scheduler. At the end of its work
// loop, once there are no queued tasks and no in-progress requests, it will
// call cleanup().
(0, _scheduler.pingPrefetchScheduler)();
}
function cleanup() {
if (lruSize <= maxLruSize) {
return;
}
// Evict entries until we're at 90% capacity. We can assume this won't
// infinite loop because even if `maxLruSize` were 0, eventually
// `deleteFromLru` sets `head` to `null` when we run out entries.
const ninetyPercentMax = maxLruSize * 0.9;
while(lruSize > ninetyPercentMax && head !== null){
const tail = head.prev;
// In practice, this is never null, but that isn't encoded in the type
if (tail !== null) {
// Delete the entry from the map. In turn, this will remove it from
// the LRU.
(0, _cachemap.deleteMapEntry)(tail);
}
}
}
if ((typeof exports.default === 'function' || (typeof exports.default === 'object' && exports.default !== null)) && typeof exports.default.__esModule === 'undefined') {
Object.defineProperty(exports.default, '__esModule', { value: true });
Object.assign(exports.default, exports);
module.exports = exports.default;
}
//# sourceMappingURL=lru.js.map
File diff suppressed because one or more lines are too long
@@ -0,0 +1,42 @@
/**
* Navigation lock for the Instant Navigation Testing API.
*
* Manages the in-memory lock (a promise) that gates dynamic data writes
* during instant navigation captures, and owns all cookie state
* transitions (pending → captured-MPA, pending → captured-SPA).
*
* External actors (Playwright, devtools) set [0] to start a lock scope
* and delete the cookie to end one. Next.js writes captured values.
* The CookieStore handler distinguishes them by value: pending = external,
* captured = self-write (ignored).
*/
import type { FlightRouterState } from '../../../shared/lib/app-router-types';
/**
* Sets up the cookie-based lock. Handles the initial page load state and
* registers a CookieStore listener for runtime changes.
*
* Called once during page initialization from app-globals.ts.
*/
export declare function startListeningForInstantNavigationCookie(): void;
/**
* Transitions the cookie from pending to captured-SPA. Called when a
* client-side navigation is captured by the lock.
*
* @param fromTree - The flight router state of the from-route
* @param toTree - The flight router state of the to-route (null if not yet known)
*/
export declare function transitionToCapturedSPA(fromTree: FlightRouterState, toTree: FlightRouterState | null): void;
/**
* Updates the captured-SPA cookie with the resolved route trees.
* Called after the prefetch resolves and the target route tree is known.
*/
export declare function updateCapturedSPAToTree(fromTree: FlightRouterState, toTree: FlightRouterState): void;
/**
* Returns true if the navigation lock is currently active.
*/
export declare function isNavigationLocked(): boolean;
/**
* Waits for the navigation lock to be released, if it's currently held.
* No-op if the lock is not acquired.
*/
export declare function waitForNavigationLockIfActive(): Promise<void>;
@@ -0,0 +1,194 @@
/**
* Navigation lock for the Instant Navigation Testing API.
*
* Manages the in-memory lock (a promise) that gates dynamic data writes
* during instant navigation captures, and owns all cookie state
* transitions (pending → captured-MPA, pending → captured-SPA).
*
* External actors (Playwright, devtools) set [0] to start a lock scope
* and delete the cookie to end one. Next.js writes captured values.
* The CookieStore handler distinguishes them by value: pending = external,
* captured = self-write (ignored).
*/ "use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
0 && (module.exports = {
isNavigationLocked: null,
startListeningForInstantNavigationCookie: null,
transitionToCapturedSPA: null,
updateCapturedSPAToTree: null,
waitForNavigationLockIfActive: null
});
function _export(target, all) {
for(var name in all)Object.defineProperty(target, name, {
enumerable: true,
get: all[name]
});
}
_export(exports, {
isNavigationLocked: function() {
return isNavigationLocked;
},
startListeningForInstantNavigationCookie: function() {
return startListeningForInstantNavigationCookie;
},
transitionToCapturedSPA: function() {
return transitionToCapturedSPA;
},
updateCapturedSPAToTree: function() {
return updateCapturedSPAToTree;
},
waitForNavigationLockIfActive: function() {
return waitForNavigationLockIfActive;
}
});
const _approuterheaders = require("../app-router-headers");
const _useactionqueue = require("../use-action-queue");
function parseCookieValue(raw) {
try {
const parsed = JSON.parse(raw);
if (Array.isArray(parsed) && parsed.length >= 3) {
const rawState = parsed[2];
return rawState === null ? 'mpa' : 'spa';
}
} catch {}
return 'pending';
}
function writeCookieValue(value) {
if (typeof cookieStore === 'undefined') {
return;
}
// Read the existing cookie to preserve its attributes (domain, path),
// then write back with the new value. This updates the same cookie
// entry that the external actor created, regardless of how it was
// scoped.
cookieStore.get(_approuterheaders.NEXT_INSTANT_TEST_COOKIE).then((existing)=>{
if (existing) {
const options = {
name: _approuterheaders.NEXT_INSTANT_TEST_COOKIE,
value: JSON.stringify(value),
path: existing.path ?? '/'
};
if (existing.domain) {
options.domain = existing.domain;
}
cookieStore.set(options);
}
});
}
let lockState = null;
function acquireLock() {
if (lockState !== null) {
return;
}
let resolve;
const promise = new Promise((r)=>{
resolve = r;
});
lockState = {
promise,
resolve: resolve
};
}
function releaseLock() {
if (lockState !== null) {
lockState.resolve();
lockState = null;
}
}
function startListeningForInstantNavigationCookie() {
if (process.env.__NEXT_EXPOSE_TESTING_API) {
// If the server served a static shell, this is an MPA page load
// while the lock is held. Transition to captured-MPA and acquire.
if (self.__next_instant_test) {
if (typeof cookieStore !== 'undefined') {
// If the cookie was already cleared during the MPA page
// transition, reload to get the full dynamic page.
cookieStore.get(_approuterheaders.NEXT_INSTANT_TEST_COOKIE).then((cookie)=>{
if (!cookie) {
window.location.reload();
}
});
}
writeCookieValue([
1,
`c${Math.random()}`,
null
]);
acquireLock();
}
if (typeof cookieStore === 'undefined') {
return;
}
cookieStore.addEventListener('change', (event)=>{
for (const cookie of event.changed){
if (cookie.name === _approuterheaders.NEXT_INSTANT_TEST_COOKIE) {
const state = parseCookieValue(cookie.value ?? '');
if (state !== 'pending') {
// Captured value — our own transition. Ignore.
return;
}
// Pending value — external actor starting a new lock scope.
if (lockState !== null) {
releaseLock();
}
acquireLock();
return;
}
}
for (const cookie of event.deleted){
if (cookie.name === _approuterheaders.NEXT_INSTANT_TEST_COOKIE) {
releaseLock();
(0, _useactionqueue.refreshOnInstantNavigationUnlock)();
return;
}
}
});
}
}
function transitionToCapturedSPA(fromTree, toTree) {
if (process.env.__NEXT_EXPOSE_TESTING_API) {
writeCookieValue([
1,
`c${Math.random()}`,
{
from: fromTree,
to: toTree
}
]);
}
}
function updateCapturedSPAToTree(fromTree, toTree) {
if (process.env.__NEXT_EXPOSE_TESTING_API) {
writeCookieValue([
1,
`c${Math.random()}`,
{
from: fromTree,
to: toTree
}
]);
}
}
function isNavigationLocked() {
if (process.env.__NEXT_EXPOSE_TESTING_API) {
return lockState !== null;
}
return false;
}
async function waitForNavigationLockIfActive() {
if (process.env.__NEXT_EXPOSE_TESTING_API) {
if (lockState !== null) {
await lockState.promise;
}
}
}
if ((typeof exports.default === 'function' || (typeof exports.default === 'object' && exports.default !== null)) && typeof exports.default.__esModule === 'undefined') {
Object.defineProperty(exports.default, '__esModule', { value: true });
Object.assign(exports.default, exports);
module.exports = exports.default;
}
//# sourceMappingURL=navigation-testing-lock.js.map
File diff suppressed because one or more lines are too long
@@ -0,0 +1,45 @@
import type { CacheNodeSeedData, FlightRouterState, ScrollRef } from '../../../shared/lib/app-router-types';
import type { CacheNode } from '../../../shared/lib/app-router-types';
import type { HeadData } from '../../../shared/lib/app-router-types';
import type { NormalizedFlightData } from '../../flight-data-helpers';
import { FreshnessPolicy } from '../router-reducer/ppr-navigations';
import { type RouteTree, type FulfilledRouteCacheEntry } from './cache';
import type { PageVaryPath } from './vary-path';
import type { AppRouterState } from '../router-reducer/router-reducer-types';
import { ScrollBehavior } from '../router-reducer/router-reducer-types';
/**
* Navigate to a new URL, using the Segment Cache to construct a response.
*
* To allow for synchronous navigations whenever possible, this is not an async
* function. It returns a promise only if there's no matching prefetch in
* the cache. Otherwise it returns an immediate result and uses Suspense/RSC to
* stream in any missing data.
*/
export declare function navigate(state: AppRouterState, url: URL, currentUrl: URL, currentRenderedSearch: string, currentCacheNode: CacheNode | null, currentFlightRouterState: FlightRouterState, nextUrl: string | null, freshnessPolicy: FreshnessPolicy, scrollBehavior: ScrollBehavior, navigateType: 'push' | 'replace'): AppRouterState | Promise<AppRouterState>;
export declare function navigateToKnownRoute(now: number, state: AppRouterState, url: URL, canonicalUrl: string, navigationSeed: NavigationSeed, currentUrl: URL, currentRenderedSearch: string, currentCacheNode: CacheNode | null, currentFlightRouterState: FlightRouterState, freshnessPolicy: FreshnessPolicy, nextUrl: string | null, scrollBehavior: ScrollBehavior, navigateType: 'push' | 'replace', debugInfo: Array<unknown> | null, routeCacheEntry: FulfilledRouteCacheEntry | null): AppRouterState;
export declare function completeHardNavigation(state: AppRouterState, url: URL, navigateType: 'push' | 'replace'): AppRouterState;
export declare function completeSoftNavigation(oldState: AppRouterState, url: URL, referringNextUrl: string | null, tree: FlightRouterState, cache: CacheNode, renderedSearch: string, canonicalUrl: string, navigateType: 'push' | 'replace', scrollBehavior: ScrollBehavior, scrollRef: ScrollRef | null, collectedDebugInfo: Array<unknown> | null): AppRouterState;
export declare function completeTraverseNavigation(state: AppRouterState, url: URL, renderedSearch: string, cache: CacheNode, tree: FlightRouterState, nextUrl: string | null): {
canonicalUrl: string;
renderedSearch: string;
pushRef: {
pendingPush: boolean;
mpaNavigation: boolean;
preserveCustomHistoryState: boolean;
};
focusAndScrollRef: import("../router-reducer/router-reducer-types").FocusAndScrollRef;
cache: CacheNode;
tree: FlightRouterState;
nextUrl: string | null;
previousNextUrl: null;
debugInfo: null;
};
export type NavigationSeed = {
renderedSearch: string;
routeTree: RouteTree;
metadataVaryPath: PageVaryPath | null;
data: CacheNodeSeedData | null;
head: HeadData | null;
dynamicStaleAt: number;
};
export declare function convertServerPatchToFullTree(now: number, currentTree: FlightRouterState, flightData: Array<NormalizedFlightData> | null, renderedSearch: string, dynamicStaleTimeSeconds: number): NavigationSeed;
@@ -0,0 +1,597 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
0 && (module.exports = {
completeHardNavigation: null,
completeSoftNavigation: null,
completeTraverseNavigation: null,
convertServerPatchToFullTree: null,
navigate: null,
navigateToKnownRoute: null
});
function _export(target, all) {
for(var name in all)Object.defineProperty(target, name, {
enumerable: true,
get: all[name]
});
}
_export(exports, {
completeHardNavigation: function() {
return completeHardNavigation;
},
completeSoftNavigation: function() {
return completeSoftNavigation;
},
completeTraverseNavigation: function() {
return completeTraverseNavigation;
},
convertServerPatchToFullTree: function() {
return convertServerPatchToFullTree;
},
navigate: function() {
return navigate;
},
navigateToKnownRoute: function() {
return navigateToKnownRoute;
}
});
const _fetchserverresponse = require("../router-reducer/fetch-server-response");
const _pprnavigations = require("../router-reducer/ppr-navigations");
const _createhreffromurl = require("../router-reducer/create-href-from-url");
const _constants = require("../../../lib/constants");
const _cache = require("./cache");
const _optimisticroutes = require("./optimistic-routes");
const _cachekey = require("./cache-key");
const _scheduler = require("./scheduler");
const _types = require("./types");
const _links = require("../links");
const _routerreducertypes = require("../router-reducer/router-reducer-types");
const _computechangedpath = require("../router-reducer/compute-changed-path");
const _javascripturl = require("../../lib/javascript-url");
const _bfcache = require("./bfcache");
function navigate(state, url, currentUrl, currentRenderedSearch, currentCacheNode, currentFlightRouterState, nextUrl, freshnessPolicy, scrollBehavior, navigateType) {
// Instant Navigation Testing API: when the lock is active, ensure a
// prefetch task has been initiated before proceeding with the navigation.
// This guarantees that segment data requests are at least pending, even
// for routes that already have a cached route tree. Without this, the
// static shell might be incomplete because some segments were never
// requested.
if (process.env.__NEXT_EXPOSE_TESTING_API) {
const { isNavigationLocked } = require('./navigation-testing-lock');
if (isNavigationLocked()) {
return ensurePrefetchThenNavigate(state, url, currentUrl, currentRenderedSearch, currentCacheNode, currentFlightRouterState, nextUrl, freshnessPolicy, scrollBehavior, navigateType);
}
}
return navigateImpl(state, url, currentUrl, currentRenderedSearch, currentCacheNode, currentFlightRouterState, nextUrl, freshnessPolicy, scrollBehavior, navigateType);
}
function navigateImpl(state, url, currentUrl, currentRenderedSearch, currentCacheNode, currentFlightRouterState, nextUrl, freshnessPolicy, scrollBehavior, navigateType) {
const now = Date.now();
const href = url.href;
const cacheKey = (0, _cachekey.createCacheKey)(href, nextUrl);
const route = (0, _cache.readRouteCacheEntry)(now, cacheKey);
if (route !== null && route.status === _cache.EntryStatus.Fulfilled) {
// We have a matching prefetch.
return navigateUsingPrefetchedRouteTree(now, state, url, currentUrl, currentRenderedSearch, nextUrl, currentCacheNode, currentFlightRouterState, freshnessPolicy, scrollBehavior, navigateType, route);
}
// There was no matching route tree in the cache. Let's see if we can
// construct an "optimistic" route tree using the deprecated search-params
// based matching. This is only used when the new optimisticRouting flag is
// disabled.
//
// Do not construct an optimistic route tree if there was a cache hit, but
// the entry has a rejected status, since it may have been rejected due to a
// rewrite or redirect based on the search params.
//
// TODO: There are multiple reasons a prefetch might be rejected; we should
// track them explicitly and choose what to do here based on that.
if (!process.env.__NEXT_OPTIMISTIC_ROUTING) {
if (route === null || route.status !== _cache.EntryStatus.Rejected) {
const optimisticRoute = (0, _cache.deprecated_requestOptimisticRouteCacheEntry)(now, url, nextUrl);
if (optimisticRoute !== null) {
// We have an optimistic route tree. Proceed with the normal flow.
return navigateUsingPrefetchedRouteTree(now, state, url, currentUrl, currentRenderedSearch, nextUrl, currentCacheNode, currentFlightRouterState, freshnessPolicy, scrollBehavior, navigateType, optimisticRoute);
}
}
}
// There's no matching prefetch for this route in the cache. We must lazily
// fetch it from the server before we can perform the navigation.
//
// TODO: If this is a gesture navigation, instead of performing a
// dynamic request, we should do a runtime prefetch.
return navigateToUnknownRoute(now, state, url, currentUrl, currentRenderedSearch, nextUrl, currentCacheNode, currentFlightRouterState, freshnessPolicy, scrollBehavior, navigateType).catch(()=>{
// If the navigation fails, return the current state
return state;
});
}
function navigateToKnownRoute(now, state, url, canonicalUrl, navigationSeed, currentUrl, currentRenderedSearch, currentCacheNode, currentFlightRouterState, freshnessPolicy, nextUrl, scrollBehavior, navigateType, debugInfo, // The route cache entry used for this navigation, if it came from route
// prediction. Passed through so it can be marked as having a dynamic rewrite
// if the server returns a different pathname (indicating dynamic rewrite
// behavior).
//
// When null, the navigation did not use route prediction - either because
// the route was already fully cached, or it's a navigation that doesn't
// involve prediction (refresh, history traversal, server action, etc.).
// In these cases, if a mismatch occurs, we still mark the route as having a
// dynamic rewrite by traversing the known route tree (see
// dispatchRetryDueToTreeMismatch).
routeCacheEntry) {
// A version of navigate() that accepts the target route tree as an argument
// rather than reading it from the prefetch cache.
const accumulation = {
separateRefreshUrls: null,
scrollRef: null
};
// We special case navigations to the exact same URL as the current location.
// It's a common UI pattern for apps to refresh when you click a link to the
// current page. So when this happens, we refresh the dynamic data in the page
// segments.
//
// Note that this does not apply if the any part of the hash or search query
// has changed. This might feel a bit weird but it makes more sense when you
// consider that the way to trigger this behavior is to click the same link
// multiple times.
//
// TODO: We should probably refresh the *entire* route when this case occurs,
// not just the page segments. Essentially treating it the same as a refresh()
// triggered by an action, which is the more explicit way of modeling the UI
// pattern described above.
//
// Also note that this only refreshes the dynamic data, not static/ cached
// data. If the page segment is fully static and prefetched, the request is
// skipped. (This is also how refresh() works.)
const isSamePageNavigation = url.href === currentUrl.href;
const task = (0, _pprnavigations.startPPRNavigation)(now, currentUrl, currentRenderedSearch, currentCacheNode, currentFlightRouterState, navigationSeed.routeTree, navigationSeed.metadataVaryPath, freshnessPolicy, navigationSeed.data, navigationSeed.head, navigationSeed.dynamicStaleAt, isSamePageNavigation, accumulation);
if (task !== null) {
if (freshnessPolicy !== _pprnavigations.FreshnessPolicy.Gesture) {
(0, _pprnavigations.spawnDynamicRequests)(task, url, nextUrl, freshnessPolicy, accumulation, routeCacheEntry, navigateType);
}
return completeSoftNavigation(state, url, nextUrl, task.route, task.node, navigationSeed.renderedSearch, canonicalUrl, navigateType, scrollBehavior, accumulation.scrollRef, debugInfo);
}
// Could not perform a SPA navigation. Revert to a full-page (MPA) navigation.
return completeHardNavigation(state, url, navigateType);
}
function navigateUsingPrefetchedRouteTree(now, state, url, currentUrl, currentRenderedSearch, nextUrl, currentCacheNode, currentFlightRouterState, freshnessPolicy, scrollBehavior, navigateType, route) {
const routeTree = route.tree;
const canonicalUrl = route.canonicalUrl + url.hash;
const renderedSearch = route.renderedSearch;
const prefetchSeed = {
renderedSearch,
routeTree,
metadataVaryPath: route.metadata.varyPath,
data: null,
head: null,
dynamicStaleAt: (0, _bfcache.computeDynamicStaleAt)(now, _bfcache.UnknownDynamicStaleTime)
};
return navigateToKnownRoute(now, state, url, canonicalUrl, prefetchSeed, currentUrl, currentRenderedSearch, currentCacheNode, currentFlightRouterState, freshnessPolicy, nextUrl, scrollBehavior, navigateType, null, route);
}
// Used to request all the dynamic data for a route, rather than just a subset,
// e.g. during a refresh or a revalidation. Typically this gets constructed
// during the normal flow when diffing the route tree, but for an unprefetched
// navigation, where we don't know the structure of the target route, we use
// this instead.
const DynamicRequestTreeForEntireRoute = [
'',
{},
null,
'refetch'
];
async function navigateToUnknownRoute(now, state, url, currentUrl, currentRenderedSearch, nextUrl, currentCacheNode, currentFlightRouterState, freshnessPolicy, scrollBehavior, navigateType) {
// Runs when a navigation happens but there's no cached prefetch we can use.
// Don't bother to wait for a prefetch response; go straight to a full
// navigation that contains both static and dynamic data in a single stream.
// (This is unlike the old navigation implementation, which instead blocks
// the dynamic request until a prefetch request is received.)
//
// To avoid duplication of logic, we're going to pretend that the tree
// returned by the dynamic request is, in fact, a prefetch tree. Then we can
// use the same server response to write the actual data into the CacheNode
// tree. So it's the same flow as the "happy path" (prefetch, then
// navigation), except we use a single server response for both stages.
let dynamicRequestTree;
switch(freshnessPolicy){
case _pprnavigations.FreshnessPolicy.Default:
case _pprnavigations.FreshnessPolicy.HistoryTraversal:
case _pprnavigations.FreshnessPolicy.Gesture:
dynamicRequestTree = currentFlightRouterState;
break;
case _pprnavigations.FreshnessPolicy.Hydration:
case _pprnavigations.FreshnessPolicy.RefreshAll:
case _pprnavigations.FreshnessPolicy.HMRRefresh:
dynamicRequestTree = DynamicRequestTreeForEntireRoute;
break;
default:
freshnessPolicy;
dynamicRequestTree = currentFlightRouterState;
break;
}
const promiseForDynamicServerResponse = (0, _fetchserverresponse.fetchServerResponse)(url, {
flightRouterState: dynamicRequestTree,
nextUrl
});
const result = await promiseForDynamicServerResponse;
if (typeof result === 'string') {
// This is an MPA navigation.
const redirectUrl = new URL(result, location.origin);
return completeHardNavigation(state, redirectUrl, navigateType);
}
const { flightData, canonicalUrl, renderedSearch, couldBeIntercepted, supportsPerSegmentPrefetching, dynamicStaleTime, staticStageData, runtimePrefetchStream, responseHeaders, debugInfo } = result;
// Since the response format of dynamic requests and prefetches is slightly
// different, we'll need to massage the data a bit. Create FlightRouterState
// tree that simulates what we'd receive as the result of a prefetch.
const navigationSeed = convertServerPatchToFullTree(now, currentFlightRouterState, flightData, renderedSearch, dynamicStaleTime);
// Learn the route pattern so we can predict it for future navigations.
// hasDynamicRewrite is false because this is a fresh navigation to an
// unknown route - any rewrite detection happens during the traversal inside
// discoverKnownRoute. The hasDynamicRewrite param is only set to true when
// retrying after a tree mismatch (see dispatchRetryDueToTreeMismatch).
const metadataVaryPath = navigationSeed.metadataVaryPath;
if (metadataVaryPath !== null) {
(0, _optimisticroutes.discoverKnownRoute)(now, url.pathname, nextUrl, null, navigationSeed.routeTree, metadataVaryPath, couldBeIntercepted, (0, _createhreffromurl.createHrefFromUrl)(canonicalUrl), supportsPerSegmentPrefetching, false // hasDynamicRewrite - not a retry, rewrite detection happens during traversal
);
if (staticStageData !== null) {
const { response: staticStageResponse, isResponsePartial } = staticStageData;
// Write the static stage of the response into the segment cache so that
// subsequent navigations can serve cached static segments instantly.
(0, _cache.getStaleAt)(now, staticStageResponse.s).then((staleAt)=>{
const buildId = responseHeaders.get(_constants.NEXT_NAV_DEPLOYMENT_ID_HEADER) ?? staticStageResponse.b;
(0, _cache.writeStaticStageResponseIntoCache)(now, staticStageResponse.f, buildId, staticStageResponse.h, staleAt, currentFlightRouterState, renderedSearch, isResponsePartial);
}).catch(()=>{
// The static stage processing failed. Not fatal — the navigation
// completed normally, we just won't write into the cache.
});
}
if (runtimePrefetchStream !== null) {
(0, _cache.processRuntimePrefetchStream)(now, runtimePrefetchStream, currentFlightRouterState, renderedSearch).then((processed)=>{
if (processed !== null) {
(0, _cache.writeDynamicRenderResponseIntoCache)(now, _types.FetchStrategy.PPRRuntime, processed.flightDatas, processed.buildId, processed.isResponsePartial, processed.headVaryParams, processed.staleAt, processed.navigationSeed, null);
}
}).catch(()=>{
// The runtime prefetch cache write failed. Not fatal — the
// navigation completed normally, we just won't cache runtime data.
});
}
}
return navigateToKnownRoute(now, state, url, (0, _createhreffromurl.createHrefFromUrl)(canonicalUrl), navigationSeed, currentUrl, currentRenderedSearch, currentCacheNode, currentFlightRouterState, freshnessPolicy, nextUrl, scrollBehavior, navigateType, debugInfo, // Unknown route navigations don't use route prediction - the route tree
// came directly from the server. If a mismatch occurs during dynamic data
// fetch, the retry handler will traverse the known route tree to mark the
// entry as having a dynamic rewrite.
null);
}
function completeHardNavigation(state, url, navigateType) {
if ((0, _javascripturl.isJavaScriptURLString)(url.href)) {
console.error('Next.js has blocked a javascript: URL as a security precaution.');
return state;
}
const newState = {
canonicalUrl: url.origin === location.origin ? (0, _createhreffromurl.createHrefFromUrl)(url) : url.href,
pushRef: {
pendingPush: navigateType === 'push',
mpaNavigation: true,
preserveCustomHistoryState: false
},
// TODO: None of the rest of these values are consistent with the incoming
// navigation. We rely on the fact that AppRouter will suspend and trigger
// a hard navigation before it accesses any of these values. But instead
// we should trigger the hard navigation and blocking any subsequent
// router updates without updating React.
renderedSearch: state.renderedSearch,
focusAndScrollRef: state.focusAndScrollRef,
cache: state.cache,
tree: state.tree,
nextUrl: state.nextUrl,
previousNextUrl: state.previousNextUrl,
debugInfo: null
};
return newState;
}
function completeSoftNavigation(oldState, url, referringNextUrl, tree, cache, renderedSearch, canonicalUrl, navigateType, scrollBehavior, scrollRef, collectedDebugInfo) {
// The "Next-Url" is a special representation of the URL that Next.js
// uses to implement interception routes.
// TODO: Get rid of this extra traversal by computing this during the
// same traversal that computes the tree itself. We should also figure out
// what is the minimum information needed for the server to correctly
// intercept the route.
const changedPath = (0, _computechangedpath.computeChangedPath)(oldState.tree, tree);
const nextUrlForNewRoute = changedPath ? changedPath : oldState.nextUrl;
// This value is stored on the state as `previousNextUrl`; the naming is
// confusing. What it represents is the "Next-Url" header that was used to
// fetch the incoming route. It's essentially the refererer URL, but in a
// Next.js specific format. During refreshes, this is sent back to the server
// instead of the current route's "Next-Url" so that the same interception
// logic is applied as during the original navigation.
const previousNextUrl = referringNextUrl;
// Check if the only thing that changed was the hash fragment.
const oldUrl = new URL(oldState.canonicalUrl, url);
const onlyHashChange = // We don't need to compare the origins, because client-driven
// navigations are always same-origin.
url.pathname === oldUrl.pathname && url.search === oldUrl.search && url.hash !== oldUrl.hash;
// Determine whether and how the page should scroll after this
// navigation.
//
// By default, we scroll to the segments that were navigated to — i.e.
// segments in the new part of the route, as opposed to shared segments
// that were already part of the previous route. All newly navigated
// segments share a single ScrollRef. When they mount, the first one
// to mount initiates the scroll. They share a ref so that only one
// scroll happens per navigation.
//
// If a subsequent navigation produces new segments, those supersede
// any pending scroll from the previous navigation by invalidating its
// ScrollRef. If a navigation doesn't produce any new segments (e.g.
// a refresh where the route structure didn't change), any pending
// scrolls from previous navigations are unaffected.
//
// The branches below handle special cases layered on top of this
// default model.
let activeScrollRef;
let forceScroll;
if (scrollBehavior === _routerreducertypes.ScrollBehavior.NoScroll) {
// The user explicitly opted out of scrolling (e.g. scroll={false}
// on a Link or router.push).
//
// If this navigation created new scroll targets (scrollRef !== null),
// neutralize them. If it didn't, any prior scroll targets carried
// forward on the cache nodes via reuseSharedCacheNode remain active.
if (scrollRef !== null) {
scrollRef.current = false;
}
activeScrollRef = oldState.focusAndScrollRef.scrollRef;
forceScroll = false;
} else if (onlyHashChange) {
// Hash-only navigations should scroll regardless of per-node state.
// Create a fresh ref so the first segment to scroll consumes it.
//
// Invalidate any scroll ref from a prior navigation that hasn't
// been consumed yet.
const oldScrollRef = oldState.focusAndScrollRef.scrollRef;
if (oldScrollRef !== null) {
oldScrollRef.current = false;
}
// Also invalidate any per-node refs that were accumulated during
// this navigation's tree construction — the hash-only ref
// supersedes them.
if (scrollRef !== null) {
scrollRef.current = false;
}
activeScrollRef = {
current: true
};
forceScroll = true;
} else {
// Default case. Use the accumulated scrollRef (may be null if no
// new segments were created). The handler checks per-node refs, so
// unchanged parallel route slots won't scroll.
activeScrollRef = scrollRef;
// If this navigation created new scroll targets, invalidate any
// pending scroll from a previous navigation.
if (scrollRef !== null) {
const oldScrollRef = oldState.focusAndScrollRef.scrollRef;
if (oldScrollRef !== null) {
oldScrollRef.current = false;
}
}
forceScroll = false;
}
const newState = {
canonicalUrl,
renderedSearch,
pushRef: {
pendingPush: navigateType === 'push',
mpaNavigation: false,
preserveCustomHistoryState: false
},
focusAndScrollRef: {
scrollRef: activeScrollRef,
forceScroll,
onlyHashChange,
hashFragment: // Remove leading # and decode hash to make non-latin hashes work.
//
// Empty hash should trigger default behavior of scrolling layout into
// view. #top is handled in layout-router.
//
// Refer to `ScrollAndFocusHandler` for details on how this is used.
scrollBehavior !== _routerreducertypes.ScrollBehavior.NoScroll && url.hash !== '' ? decodeURIComponent(url.hash.slice(1)) : oldState.focusAndScrollRef.hashFragment
},
cache,
tree,
nextUrl: nextUrlForNewRoute,
previousNextUrl,
debugInfo: collectedDebugInfo
};
return newState;
}
function completeTraverseNavigation(state, url, renderedSearch, cache, tree, nextUrl) {
return {
// Set canonical url
canonicalUrl: (0, _createhreffromurl.createHrefFromUrl)(url),
renderedSearch,
pushRef: {
pendingPush: false,
mpaNavigation: false,
// Ensures that the custom history state that was set is preserved when applying this update.
preserveCustomHistoryState: true
},
focusAndScrollRef: state.focusAndScrollRef,
cache,
// Restore provided tree
tree,
nextUrl,
// TODO: We need to restore previousNextUrl, too, which represents the
// Next-Url that was used to fetch the data. Anywhere we fetch using the
// canonical URL, there should be a corresponding Next-Url.
previousNextUrl: null,
debugInfo: null
};
}
function convertServerPatchToFullTree(now, currentTree, flightData, renderedSearch, dynamicStaleTimeSeconds) {
// During a client navigation or prefetch, the server sends back only a patch
// for the parts of the tree that have changed.
//
// This applies the patch to the base tree to create a full representation of
// the resulting tree.
//
// The return type includes a full FlightRouterState tree and a full
// CacheNodeSeedData tree. (Conceptually these are the same tree, and should
// eventually be unified, but there's still lots of existing code that
// operates on FlightRouterState trees alone without the CacheNodeSeedData.)
//
// TODO: This similar to what apply-router-state-patch-to-tree does. It
// will eventually fully replace it. We should get rid of all the remaining
// places where we iterate over the server patch format. This should also
// eventually replace normalizeFlightData.
let baseTree = currentTree;
let baseData = null;
let head = null;
if (flightData !== null) {
for (const { segmentPath, tree: treePatch, seedData: dataPatch, head: headPatch } of flightData){
const result = convertServerPatchToFullTreeImpl(baseTree, baseData, treePatch, dataPatch, segmentPath, renderedSearch, 0);
baseTree = result.tree;
baseData = result.data;
// This is the same for all patches per response, so just pick an
// arbitrary one
head = headPatch;
}
}
const finalFlightRouterState = baseTree;
// Convert the final FlightRouterState into a RouteTree type.
//
// TODO: Eventually, FlightRouterState will evolve to being a transport format
// only. The RouteTree type will become the main type used for dealing with
// routes on the client, and we'll store it in the state directly.
const acc = {
metadataVaryPath: null
};
const routeTree = (0, _cache.convertRootFlightRouterStateToRouteTree)(finalFlightRouterState, renderedSearch, acc);
return {
routeTree,
metadataVaryPath: acc.metadataVaryPath,
data: baseData,
renderedSearch,
head,
dynamicStaleAt: (0, _bfcache.computeDynamicStaleAt)(now, dynamicStaleTimeSeconds)
};
}
function convertServerPatchToFullTreeImpl(baseRouterState, baseData, treePatch, dataPatch, segmentPath, renderedSearch, index) {
if (index === segmentPath.length) {
// We reached the part of the tree that we need to patch.
return {
tree: treePatch,
data: dataPatch
};
}
// segmentPath represents the parent path of subtree. It's a repeating
// pattern of parallel route key and segment:
//
// [string, Segment, string, Segment, string, Segment, ...]
//
// This path tells us which part of the base tree to apply the tree patch.
//
// NOTE: We receive the FlightRouterState patch in the same request as the
// seed data patch. Therefore we don't need to worry about diffing the segment
// values; we can assume the server sent us a correct result.
const updatedParallelRouteKey = segmentPath[index];
// const segment: Segment = segmentPath[index + 1] <-- Not used, see note above
const baseTreeChildren = baseRouterState[1];
const baseSeedDataChildren = baseData !== null ? baseData[1] : null;
const newTreeChildren = {};
const newSeedDataChildren = {};
for(const parallelRouteKey in baseTreeChildren){
const childBaseRouterState = baseTreeChildren[parallelRouteKey];
const childBaseSeedData = baseSeedDataChildren !== null ? baseSeedDataChildren[parallelRouteKey] ?? null : null;
if (parallelRouteKey === updatedParallelRouteKey) {
const result = convertServerPatchToFullTreeImpl(childBaseRouterState, childBaseSeedData, treePatch, dataPatch, segmentPath, renderedSearch, // Advance the index by two and keep cloning until we reach
// the end of the segment path.
index + 2);
newTreeChildren[parallelRouteKey] = result.tree;
newSeedDataChildren[parallelRouteKey] = result.data;
} else {
// This child is not being patched. Copy it over as-is.
newTreeChildren[parallelRouteKey] = childBaseRouterState;
newSeedDataChildren[parallelRouteKey] = childBaseSeedData;
}
}
let clonedTree;
let clonedSeedData;
// Clone all the fields except the children.
// Clone the FlightRouterState tree. Based on equivalent logic in
// apply-router-state-patch-to-tree, but should confirm whether we need to
// copy all of these fields. Not sure the server ever sends, e.g. the
// refetch marker.
clonedTree = [
baseRouterState[0],
newTreeChildren
];
if (2 in baseRouterState) {
const compressedRefreshState = baseRouterState[2];
if (compressedRefreshState !== undefined && compressedRefreshState !== null) {
// Since this part of the tree was patched with new data, any parent
// refresh states should be updated to reflect the new rendered search
// value. (The refresh state acts like a "context provider".) All pages
// within the same server response share the same renderedSearch value,
// but the same RouteTree could be composed from multiple different
// routes, and multiple responses.
clonedTree[2] = [
compressedRefreshState[0],
renderedSearch
];
}
}
if (3 in baseRouterState) {
clonedTree[3] = baseRouterState[3];
}
if (4 in baseRouterState) {
clonedTree[4] = baseRouterState[4];
}
// Clone the CacheNodeSeedData tree.
const isEmptySeedDataPartial = true;
clonedSeedData = [
null,
newSeedDataChildren,
null,
isEmptySeedDataPartial,
null
];
return {
tree: clonedTree,
data: clonedSeedData
};
}
/**
* Instant Navigation Testing API: ensures a prefetch task has been initiated
* and completed before proceeding with the navigation. This guarantees that
* segment data requests are at least pending, even for routes whose route
* tree is already cached.
*
* After the prefetch completes, delegates to the normal navigation flow.
*/ async function ensurePrefetchThenNavigate(state, url, currentUrl, currentRenderedSearch, currentCacheNode, currentFlightRouterState, nextUrl, freshnessPolicy, scrollBehavior, navigateType) {
const link = (0, _links.getLinkForCurrentNavigation)();
const fetchStrategy = link !== null ? link.fetchStrategy : _types.FetchStrategy.PPR;
// Transition the cookie to captured-SPA immediately, before waiting
// for the prefetch. This ensures the devtools panel can update its UI
// right away, even if the prefetch takes time (e.g. dev compilation).
// The "to" tree starts as null and is filled in after the prefetch
// resolves and the navigation produces a new router state.
const { transitionToCapturedSPA, updateCapturedSPAToTree } = require('./navigation-testing-lock');
transitionToCapturedSPA(currentFlightRouterState, null);
const cacheKey = (0, _cachekey.createCacheKey)(url.href, nextUrl);
await new Promise((resolve)=>{
(0, _scheduler.schedulePrefetchTask)(cacheKey, currentFlightRouterState, fetchStrategy, _types.PrefetchPriority.Default, null, resolve // _onComplete callback
);
});
// Prefetch is complete. Proceed with the normal navigation flow, which
// will now find the route in the cache.
const result = await navigateImpl(state, url, currentUrl, currentRenderedSearch, currentCacheNode, currentFlightRouterState, nextUrl, freshnessPolicy, scrollBehavior, navigateType);
// Update the cookie with the resolved "to" tree so the devtools
// panel can display both routes immediately.
updateCapturedSPAToTree(currentFlightRouterState, result.tree);
return result;
}
if ((typeof exports.default === 'function' || (typeof exports.default === 'object' && exports.default !== null)) && typeof exports.default.__esModule === 'undefined') {
Object.defineProperty(exports.default, '__esModule', { value: true });
Object.assign(exports.default, exports);
module.exports = exports.default;
}
//# sourceMappingURL=navigation.js.map
File diff suppressed because one or more lines are too long
@@ -0,0 +1,79 @@
/**
* Optimistic Routing (Known Routes)
*
* This module enables the client to predict route structure for URLs that
* haven't been prefetched yet, based on previously learned route patterns.
* When successful, this allows skipping the route tree prefetch request
* entirely.
*
* The core idea is that many URLs map to the same route structure. For example,
* /blog/post-1 and /blog/post-2 both resolve to /blog/[slug]. Once we've
* prefetched one, we can predict the structure of the other.
*
* However, we can't always make this prediction. Static siblings (like
* /blog/featured alongside /blog/[slug]) have different route structures.
* When we learn a dynamic route, we also learn its static siblings so we
* know when NOT to apply the prediction.
*
* Main entry points:
*
* 1. discoverKnownRoute: Called after receiving a route tree from the server.
* Traverses the route tree, compares URL parts to segments, and populates
* the known route tree if they match. Routes are always inserted into the
* cache.
*
* 2. matchKnownRoute: Called when looking up a route with no cache entry.
* Matches the candidate URL against learned patterns. Returns a synthetic
* cache entry if successful, or null to fall back to server resolution.
*
* Rewrite detection happens during traversal: if a URL path part doesn't match
* the corresponding route segment, we stop populating the known route tree
* (since the mapping is incorrect) but still insert the route into the cache.
*
* The known route tree is append-only with no eviction. Route patterns are
* derived from the filesystem, so they don't become stale within a session.
* Cache invalidation on deploy clears everything anyway.
*
* Current limitations (deopt to server resolution):
* - Rewrites: Detected during traversal (tree not populated, but route cached)
* - Intercepted routes: The route tree varies by referrer (Next-Url header),
* so we can't predict the correct structure from the URL alone. Patterns are
* still stored during discovery (so the trie stays populated for non-
* intercepted siblings), but matching bails out when the pattern is marked
* as interceptable.
*/
import type { RouteTree, FulfilledRouteCacheEntry } from './cache';
import { type PendingRouteCacheEntry } from './cache';
import type { NormalizedSearch } from './cache-key';
import { type PageVaryPath } from './vary-path';
/**
* Learns a route pattern from a server response and inserts it into the cache.
*
* Called after receiving a route tree from the server (initial load, navigation,
* or prefetch). Traverses the route tree, compares URL parts to segments, and
* populates the known route tree if they match. Routes are always inserted into
* the cache regardless of whether the URL matches the route structure.
*
* When pendingEntry is provided, it's fulfilled and used. When null, an entry
* is created and inserted into the route cache map.
*
* When hasDynamicRewrite is true, the route entry is marked as having a
* dynamic rewrite, which prevents it from being used as a template for future
* predictions. This is set when we detect a mismatch between what we predicted
* and what the server returned.
*
* Returns the fulfilled route cache entry.
*/
export declare function discoverKnownRoute(now: number, pathname: string, nextUrl: string | null, pendingEntry: PendingRouteCacheEntry | null, routeTree: RouteTree, metadataVaryPath: PageVaryPath, couldBeIntercepted: boolean, canonicalUrl: string, supportsPerSegmentPrefetching: boolean, hasDynamicRewrite: boolean): FulfilledRouteCacheEntry;
/**
* Attempts to match a URL against learned route patterns.
*
* Returns a synthetic FulfilledRouteCacheEntry if the URL matches a known
* pattern, or null if no match is found (fall back to server resolution).
*/
export declare function matchKnownRoute(pathname: string, search: NormalizedSearch): FulfilledRouteCacheEntry | null;
/**
* Resets the known route tree. Called during development when routes may
* change due to hot reloading.
*/
export declare function resetKnownRoutes(): void;
@@ -0,0 +1,543 @@
/**
* Optimistic Routing (Known Routes)
*
* This module enables the client to predict route structure for URLs that
* haven't been prefetched yet, based on previously learned route patterns.
* When successful, this allows skipping the route tree prefetch request
* entirely.
*
* The core idea is that many URLs map to the same route structure. For example,
* /blog/post-1 and /blog/post-2 both resolve to /blog/[slug]. Once we've
* prefetched one, we can predict the structure of the other.
*
* However, we can't always make this prediction. Static siblings (like
* /blog/featured alongside /blog/[slug]) have different route structures.
* When we learn a dynamic route, we also learn its static siblings so we
* know when NOT to apply the prediction.
*
* Main entry points:
*
* 1. discoverKnownRoute: Called after receiving a route tree from the server.
* Traverses the route tree, compares URL parts to segments, and populates
* the known route tree if they match. Routes are always inserted into the
* cache.
*
* 2. matchKnownRoute: Called when looking up a route with no cache entry.
* Matches the candidate URL against learned patterns. Returns a synthetic
* cache entry if successful, or null to fall back to server resolution.
*
* Rewrite detection happens during traversal: if a URL path part doesn't match
* the corresponding route segment, we stop populating the known route tree
* (since the mapping is incorrect) but still insert the route into the cache.
*
* The known route tree is append-only with no eviction. Route patterns are
* derived from the filesystem, so they don't become stale within a session.
* Cache invalidation on deploy clears everything anyway.
*
* Current limitations (deopt to server resolution):
* - Rewrites: Detected during traversal (tree not populated, but route cached)
* - Intercepted routes: The route tree varies by referrer (Next-Url header),
* so we can't predict the correct structure from the URL alone. Patterns are
* still stored during discovery (so the trie stays populated for non-
* intercepted siblings), but matching bails out when the pattern is marked
* as interceptable.
*/ "use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
0 && (module.exports = {
discoverKnownRoute: null,
matchKnownRoute: null,
resetKnownRoutes: null
});
function _export(target, all) {
for(var name in all)Object.defineProperty(target, name, {
enumerable: true,
get: all[name]
});
}
_export(exports, {
discoverKnownRoute: function() {
return discoverKnownRoute;
},
matchKnownRoute: function() {
return matchKnownRoute;
},
resetKnownRoutes: function() {
return resetKnownRoutes;
}
});
const _cache = require("./cache");
const _routeparams = require("../../route-params");
const _varypath = require("./vary-path");
function createEmptyPart() {
return {
staticChildren: null,
dynamicChild: null,
dynamicChildParamName: null,
dynamicChildParamType: null,
pattern: null
};
}
// The root of the known route tree.
let knownRouteTreeRoot = createEmptyPart();
function discoverKnownRoute(now, pathname, nextUrl, pendingEntry, routeTree, metadataVaryPath, couldBeIntercepted, canonicalUrl, supportsPerSegmentPrefetching, hasDynamicRewrite) {
const tree = routeTree;
const pathnameParts = pathname.split('/').filter((p)=>p !== '');
const firstPart = pathnameParts.length > 0 ? pathnameParts[0] : null;
const remainingParts = pathnameParts.length > 0 ? pathnameParts.slice(1) : [];
if (pendingEntry !== null) {
// Fulfill the pending entry first
const fulfilledEntry = (0, _cache.fulfillRouteCacheEntry)(now, pendingEntry, tree, metadataVaryPath, couldBeIntercepted, canonicalUrl, supportsPerSegmentPrefetching);
if (hasDynamicRewrite) {
fulfilledEntry.hasDynamicRewrite = true;
}
// Populate the known route tree (handles rewrite detection internally).
// The entry is already in the cache; this just stores it as a pattern
// if the URL matches the route structure.
discoverKnownRoutePart(knownRouteTreeRoot, tree, firstPart, remainingParts, fulfilledEntry, now, pathname, nextUrl, tree, metadataVaryPath, couldBeIntercepted, canonicalUrl, supportsPerSegmentPrefetching, hasDynamicRewrite);
return fulfilledEntry;
}
// No pending entry - discoverKnownRoutePart will create one and insert it
// into the cache, or return an existing pattern if one exists.
return discoverKnownRoutePart(knownRouteTreeRoot, tree, firstPart, remainingParts, null, now, pathname, nextUrl, tree, metadataVaryPath, couldBeIntercepted, canonicalUrl, supportsPerSegmentPrefetching, hasDynamicRewrite);
}
/**
* Gets or creates the dynamic child node for a KnownRoutePart.
* A node can have at most one dynamic child (you can't have both [slug] and
* [id] at the same route level), so we either return existing or create new.
*/ function discoverDynamicChild(part, paramName, paramType) {
if (part.dynamicChild !== null) {
return part.dynamicChild;
}
const newChild = createEmptyPart();
// Type assertion needed because we're converting from "without" to "with"
// dynamic child variant.
const mutablePart = part;
mutablePart.dynamicChild = newChild;
mutablePart.dynamicChildParamName = paramName;
mutablePart.dynamicChildParamType = paramType;
return newChild;
}
/**
* Recursive workhorse for discoverKnownRoute.
*
* Walks the route tree and URL parts in parallel, building out the known
* route tree as it goes. At each step:
* 1. Determines if the current segment appears in the URL (dynamic/static)
* 2. Validates URL matches route structure (detects rewrites)
* 3. Creates/updates the corresponding KnownRoutePart node
* 4. Records static siblings for future matching
* 5. Recurses into child slots (parallel routes)
*
* If a URL/route mismatch is detected (rewrite), we stop building the known
* route tree but still cache the route entry for direct lookup.
*/ function discoverKnownRoutePart(parentKnownRoutePart, routeTree, urlPart, remainingParts, existingEntry, // These are passed through unchanged for entry creation at the leaf
now, pathname, nextUrl, fullTree, metadataVaryPath, couldBeIntercepted, canonicalUrl, supportsPerSegmentPrefetching, hasDynamicRewrite) {
const segment = routeTree.segment;
let segmentAppearsInURL;
let paramName = null;
let paramType = null;
let staticSiblings = null;
if (typeof segment === 'string') {
segmentAppearsInURL = (0, _routeparams.doesStaticSegmentAppearInURL)(segment);
} else {
// Dynamic segment tuple: [paramName, paramCacheKey, paramType, staticSiblings]
paramName = segment[0];
paramType = segment[2];
staticSiblings = segment[3];
segmentAppearsInURL = true;
}
let knownRoutePart = parentKnownRoutePart;
let nextUrlPart = urlPart;
let nextRemainingParts = remainingParts;
if (segmentAppearsInURL) {
// Check for mismatch: if this is a static segment, the URL part must match
if (paramName === null && urlPart !== segment) {
// URL doesn't match route structure (likely a rewrite).
// Don't populate the known route tree, just write the route into the
// cache and return immediately.
if (existingEntry !== null) {
return existingEntry;
}
return (0, _cache.writeRouteIntoCache)(now, pathname, nextUrl, fullTree, metadataVaryPath, couldBeIntercepted, canonicalUrl, supportsPerSegmentPrefetching);
}
// URL matches route structure. Build the known route tree.
if (paramName !== null && paramType !== null) {
// Dynamic segment
knownRoutePart = discoverDynamicChild(parentKnownRoutePart, paramName, paramType);
// Record static siblings as placeholder parts.
// IMPORTANT: We use the null vs Map distinction to track whether
// siblings are known at this level:
// - staticChildren: null = siblings unknown (can't safely match dynamic)
// - staticChildren: Map = siblings known (even if empty)
// This matters in dev mode where webpack may not know all siblings yet.
if (staticSiblings !== null) {
// Siblings are known - ensure we have a Map (even if empty)
if (parentKnownRoutePart.staticChildren === null) {
parentKnownRoutePart.staticChildren = new Map();
}
for (const sibling of staticSiblings){
if (!parentKnownRoutePart.staticChildren.has(sibling)) {
parentKnownRoutePart.staticChildren.set(sibling, createEmptyPart());
}
}
}
} else {
// Static segment
if (parentKnownRoutePart.staticChildren === null) {
parentKnownRoutePart.staticChildren = new Map();
}
let existingChild = parentKnownRoutePart.staticChildren.get(urlPart);
if (existingChild === undefined) {
existingChild = createEmptyPart();
parentKnownRoutePart.staticChildren.set(urlPart, existingChild);
}
knownRoutePart = existingChild;
}
// Advance to next URL part
nextUrlPart = remainingParts.length > 0 ? remainingParts[0] : null;
nextRemainingParts = remainingParts.length > 0 ? remainingParts.slice(1) : [];
}
// else: Transparent segment (route group, __PAGE__, etc.)
// Stay at the same known route part, don't advance URL parts
// Recurse into child routes. A route tree can have multiple parallel routes
// (e.g., @modal alongside children). Each parallel route is a separate
// branch, but they all share the same URL - we just need to traverse all
// branches to build out the known route tree.
const slots = routeTree.slots;
let resultFromChildren = null;
if (slots !== null) {
for(const parallelRouteKey in slots){
const childRouteTree = slots[parallelRouteKey];
// Skip branches with refreshState set - these were reused from a
// different route (e.g., a "default" parallel slot) and don't represent
// the actual route structure for this URL.
if (childRouteTree.refreshState !== null) {
continue;
}
const result = discoverKnownRoutePart(knownRoutePart, childRouteTree, nextUrlPart, nextRemainingParts, existingEntry, now, pathname, nextUrl, fullTree, metadataVaryPath, couldBeIntercepted, canonicalUrl, supportsPerSegmentPrefetching, hasDynamicRewrite);
// All parallel route branches share the same URL, so they should all
// reach compatible leaf nodes. We capture any result.
resultFromChildren = result;
}
if (resultFromChildren !== null) {
return resultFromChildren;
}
// Defensive fallback: no children returned a result. This shouldn't happen
// for valid route trees, but handle it gracefully.
if (existingEntry !== null) {
return existingEntry;
}
return (0, _cache.writeRouteIntoCache)(now, pathname, nextUrl, fullTree, metadataVaryPath, couldBeIntercepted, canonicalUrl, supportsPerSegmentPrefetching);
}
// Reached a page node. Create/get the route cache entry and store as a
// pattern. First, check if there's already a pattern for this route.
if (knownRoutePart.pattern !== null) {
// If this route has a dynamic rewrite, mark the existing pattern.
if (hasDynamicRewrite) {
knownRoutePart.pattern.hasDynamicRewrite = true;
}
return knownRoutePart.pattern;
}
// Get or create the entry
let entry;
if (existingEntry !== null) {
// Already have a fulfilled entry, use it directly. It's already in the
// route cache map.
entry = existingEntry;
} else {
// Create the entry and insert it into the route cache map.
entry = (0, _cache.writeRouteIntoCache)(now, pathname, nextUrl, fullTree, metadataVaryPath, couldBeIntercepted, canonicalUrl, supportsPerSegmentPrefetching);
}
if (hasDynamicRewrite) {
entry.hasDynamicRewrite = true;
}
// Store as pattern
knownRoutePart.pattern = entry;
return entry;
}
function matchKnownRoute(pathname, search) {
const pathnameParts = pathname.split('/').filter((p)=>p !== '');
const resolvedParams = new Map();
const match = matchKnownRoutePart(knownRouteTreeRoot, pathnameParts, 0, resolvedParams);
if (match === null) {
return null;
}
const matchedPart = match.part;
const pattern = match.pattern;
// If the pattern could be intercepted, we can't safely use it for prediction.
// Interception routes resolve to different route trees depending on the
// referrer (the Next-Url header), which means the same URL can map to
// different page components depending on where the navigation originated.
// Since the known route tree only stores a single pattern per URL shape, we
// can't distinguish between the intercepted and non-intercepted cases, so we
// bail out to server resolution.
//
// TODO: We could store interception behavior in the known route tree itself
// (e.g., which segments use interception markers and what they resolve to).
// With enough information embedded in the trie, we could match interception
// routes entirely on the client without a server round-trip.
if (pattern.couldBeIntercepted) {
return null;
}
// "Reify" the pattern: clone the template tree with concrete param values.
// This substitutes resolved params (e.g., slug: "hello") into dynamic
// segments and recomputes vary paths for correct segment cache keying.
const acc = {
metadataVaryPath: null
};
const reifiedTree = reifyRouteTree(pattern.tree, resolvedParams, search, null, acc);
// The metadata tree is a flat page node without the intermediate layout
// structure. Clone it with the updated metadata vary path collected during
// the main tree traversal.
const metadataVaryPath = acc.metadataVaryPath;
if (metadataVaryPath === null) {
// This shouldn't be reachable for a valid route tree.
return null;
}
const reifiedMetadata = (0, _cache.createMetadataRouteTree)(metadataVaryPath);
// Create a synthetic (predicted) entry and store it as the new pattern.
//
// Why replace the pattern? We intentionally update the pattern with this
// synthetic entry so that if our prediction was wrong (server returns a
// different pathname due to dynamic rewrite), the entry gets marked with
// hasDynamicRewrite. Future predictions for this route will see the flag
// and bail out to server resolution instead of making the same mistake.
const syntheticEntry = {
canonicalUrl: pathname + search,
status: _cache.EntryStatus.Fulfilled,
blockedTasks: null,
tree: reifiedTree,
metadata: reifiedMetadata,
couldBeIntercepted: pattern.couldBeIntercepted,
supportsPerSegmentPrefetching: pattern.supportsPerSegmentPrefetching,
hasDynamicRewrite: false,
renderedSearch: search,
ref: null,
size: pattern.size,
staleAt: pattern.staleAt,
version: pattern.version
};
matchedPart.pattern = syntheticEntry;
return syntheticEntry;
}
/**
* Recursively matches a URL against the known route tree.
*
* Matching priority (most specific first):
* 1. Static children - exact path segment match
* 2. Dynamic child - [param], [...param], [[...param]]
* 3. Direct pattern - when no more URL parts remain
*
* Collects resolved param values in resolvedParams as it traverses.
* Returns null if no match found (caller should fall back to server).
*/ function matchKnownRoutePart(part, pathnameParts, partIndex, resolvedParams) {
const urlPart = partIndex < pathnameParts.length ? pathnameParts[partIndex] : null;
// If staticChildren is null, we don't know what static routes exist at this
// level. This happens in webpack dev mode where routes are compiled
// on-demand. We can't safely match a dynamicChild because the URL part might
// be a static sibling we haven't discovered yet. Example: We know
// /blog/[slug] exists, but haven't compiled /blog/featured. A request for
// /blog/featured would incorrectly match /blog/[slug].
if (part.staticChildren === null) {
// The only safe match is a direct pattern when no URL parts remain.
if (urlPart === null) {
const pattern = part.pattern;
if (pattern !== null && !pattern.hasDynamicRewrite) {
return {
part,
pattern
};
}
}
return null;
}
// Static children take priority over dynamic. This ensures /blog/featured
// matches its own route rather than /blog/[slug].
if (urlPart !== null) {
const staticChild = part.staticChildren.get(urlPart);
if (staticChild !== undefined) {
// Check if this is an "unknown" placeholder part. These are created when
// we learn about static siblings (from the route tree's staticSiblings
// field) but haven't prefetched them yet. We know the path exists but
// don't know its structure, so we can't predict it.
if (staticChild.pattern === null && staticChild.dynamicChild === null && staticChild.staticChildren === null) {
// Bail out - server must resolve this route.
return null;
}
const match = matchKnownRoutePart(staticChild, pathnameParts, partIndex + 1, resolvedParams);
if (match !== null) {
return match;
}
// Static child is a real node (not a placeholder) but its subtree
// didn't match the remaining URL parts. This means the route exists
// in the static subtree but hasn't been fully discovered yet. Do not
// fall through to try the dynamic child — the static match is
// authoritative. Bail out to server resolution.
return null;
}
}
// Try dynamic child
if (part.dynamicChild !== null) {
const dynamicPart = part.dynamicChild;
const paramName = part.dynamicChildParamName;
const paramType = part.dynamicChildParamType;
const dynamicPattern = dynamicPart.pattern;
switch(paramType){
case 'c':
// Required catch-all [...param]: consumes 1+ URL parts
if (dynamicPattern !== null && !dynamicPattern.hasDynamicRewrite && urlPart !== null) {
resolvedParams.set(paramName, pathnameParts.slice(partIndex));
return {
part: dynamicPart,
pattern: dynamicPattern
};
}
break;
case 'oc':
// Optional catch-all [[...param]]: consumes 0+ URL parts
if (dynamicPattern !== null && !dynamicPattern.hasDynamicRewrite) {
if (urlPart !== null) {
resolvedParams.set(paramName, pathnameParts.slice(partIndex));
return {
part: dynamicPart,
pattern: dynamicPattern
};
}
// urlPart is null - can match with zero parts, but a direct pattern
// (e.g., page.tsx alongside [[...param]]) takes precedence.
if (part.pattern === null || part.pattern.hasDynamicRewrite) {
resolvedParams.set(paramName, []);
return {
part: dynamicPart,
pattern: dynamicPattern
};
}
}
break;
case 'd':
// Regular dynamic [param]: consumes exactly 1 URL part.
// Unlike catch-all which terminates here, regular dynamic must
// continue recursing to find the leaf pattern.
if (urlPart !== null) {
resolvedParams.set(paramName, urlPart);
return matchKnownRoutePart(dynamicPart, pathnameParts, partIndex + 1, resolvedParams);
}
break;
// Intercepted routes use relative path markers like (.), (..), (...)
// Their behavior depends on navigation context (soft vs hard nav),
// so we can't predict them client-side. Defer to server.
case 'ci(..)(..)':
case 'ci(.)':
case 'ci(..)':
case 'ci(...)':
case 'di(..)(..)':
case 'di(.)':
case 'di(..)':
case 'di(...)':
return null;
default:
paramType;
}
}
// No children matched. If we've consumed all URL parts, check for a direct
// pattern at this node (the route terminates here).
if (urlPart === null) {
const pattern = part.pattern;
if (pattern !== null && !pattern.hasDynamicRewrite) {
return {
part,
pattern
};
}
}
return null;
}
/**
* "Reify" means to make concrete - we take an abstract pattern (the template
* route tree) and produce a concrete instance with actual param values.
*
* This function clones a RouteTree, substituting dynamic segment values from
* resolvedParams and computing new vary paths. The vary path encodes param
* values so segment cache entries can be correctly keyed.
*
* Example: Pattern for /blog/[slug] with resolvedParams { slug: "hello" }
* produces a tree where segment [slug] has cacheKey "hello".
*/ function reifyRouteTree(pattern, resolvedParams, search, parentPartialVaryPath, acc) {
const originalSegment = pattern.segment;
let newSegment = originalSegment;
let partialVaryPath;
if (typeof originalSegment !== 'string') {
// Dynamic segment: compute new cache key and append to partial vary path
const paramName = originalSegment[0];
const paramType = originalSegment[2];
const staticSiblings = originalSegment[3];
const newValue = resolvedParams.get(paramName);
if (newValue !== undefined) {
const newCacheKey = Array.isArray(newValue) ? newValue.join('/') : newValue;
newSegment = [
paramName,
newCacheKey,
paramType,
staticSiblings
];
partialVaryPath = (0, _varypath.appendLayoutVaryPath)(parentPartialVaryPath, newCacheKey, paramName);
} else {
// Param not found in resolvedParams - keep original and inherit partial
// TODO: This should never happen. Bail out with null.
partialVaryPath = parentPartialVaryPath;
}
} else {
// Static segment: inherit partial vary path from parent
partialVaryPath = parentPartialVaryPath;
}
// Recurse into children with the (possibly updated) partial vary path
let newSlots = null;
if (pattern.slots !== null) {
newSlots = {};
for(const key in pattern.slots){
newSlots[key] = reifyRouteTree(pattern.slots[key], resolvedParams, search, partialVaryPath, acc);
}
}
if (pattern.isPage) {
// Page segment: finalize with search params
const newVaryPath = (0, _varypath.finalizePageVaryPath)(pattern.requestKey, search, partialVaryPath);
// Collect metadata vary path (first page wins, same as original algorithm)
if (acc.metadataVaryPath === null) {
acc.metadataVaryPath = (0, _varypath.finalizeMetadataVaryPath)(pattern.requestKey, search, partialVaryPath);
}
return {
requestKey: pattern.requestKey,
segment: newSegment,
refreshState: pattern.refreshState,
slots: newSlots,
prefetchHints: pattern.prefetchHints,
isPage: true,
varyPath: newVaryPath
};
} else {
// Layout segment: finalize without search params
const newVaryPath = (0, _varypath.finalizeLayoutVaryPath)(pattern.requestKey, partialVaryPath);
return {
requestKey: pattern.requestKey,
segment: newSegment,
refreshState: pattern.refreshState,
slots: newSlots,
prefetchHints: pattern.prefetchHints,
isPage: false,
varyPath: newVaryPath
};
}
}
function resetKnownRoutes() {
knownRouteTreeRoot = createEmptyPart();
}
if ((typeof exports.default === 'function' || (typeof exports.default === 'object' && exports.default !== null)) && typeof exports.default.__esModule === 'undefined') {
Object.defineProperty(exports.default, '__esModule', { value: true });
Object.assign(exports.default, exports);
module.exports = exports.default;
}
//# sourceMappingURL=optimistic-routes.js.map
File diff suppressed because one or more lines are too long
@@ -0,0 +1,23 @@
import type { FlightRouterState } from '../../../shared/lib/app-router-types';
import { type PrefetchTaskFetchStrategy } from './types';
/**
* 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.
* @param nextUrl - A special header used by the server for interception routes.
* Roughly corresponds to the current URL.
* @param treeAtTimeOfPrefetch - The FlightRouterState at the time the prefetch
* was requested. This is only used when PPR is disabled.
* @param fetchStrategy - Whether to prefetch dynamic data, in addition to
* static data. This is used by `<Link prefetch={true}>`.
* @param onInvalidate - A callback that will be called when the prefetch cache
* When called, it signals to the listener that the data associated with the
* prefetch may have been invalidated from the cache. This is not a live
* subscription — it's called at most once per `prefetch` call. The only
* supported use case is to trigger a new prefetch inside the listener, if
* desired. It also may be called even in cases where the associated data is
* still cached. Prefetching is a poll-based (pull) operation, not an event-
* based (push) one. Rather than subscribe to specific cache entries, you
* occasionally poll the prefetch cache to check if anything is missing.
*/
export declare function prefetch(href: string, nextUrl: string | null, treeAtTimeOfPrefetch: FlightRouterState, fetchStrategy: PrefetchTaskFetchStrategy, onInvalidate: null | (() => void)): void;
@@ -0,0 +1,31 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
Object.defineProperty(exports, "prefetch", {
enumerable: true,
get: function() {
return prefetch;
}
});
const _approuterutils = require("../app-router-utils");
const _cachekey = require("./cache-key");
const _scheduler = require("./scheduler");
const _types = require("./types");
function prefetch(href, nextUrl, treeAtTimeOfPrefetch, fetchStrategy, onInvalidate) {
const url = (0, _approuterutils.createPrefetchURL)(href);
if (url === null) {
// This href should not be prefetched.
return;
}
const cacheKey = (0, _cachekey.createCacheKey)(url.href, nextUrl);
(0, _scheduler.schedulePrefetchTask)(cacheKey, treeAtTimeOfPrefetch, fetchStrategy, _types.PrefetchPriority.Default, onInvalidate);
}
if ((typeof exports.default === 'function' || (typeof exports.default === 'object' && exports.default !== null)) && typeof exports.default.__esModule === 'undefined') {
Object.defineProperty(exports.default, '__esModule', { value: true });
Object.assign(exports.default, exports);
module.exports = exports.default;
}
//# sourceMappingURL=prefetch.js.map
@@ -0,0 +1 @@
{"version":3,"sources":["../../../../src/client/components/segment-cache/prefetch.ts"],"sourcesContent":["import type { FlightRouterState } from '../../../shared/lib/app-router-types'\nimport { createPrefetchURL } from '../app-router-utils'\nimport { createCacheKey } from './cache-key'\nimport { schedulePrefetchTask } from './scheduler'\nimport { PrefetchPriority, type PrefetchTaskFetchStrategy } from './types'\n\n/**\n * Entrypoint for prefetching a URL into the Segment Cache.\n * @param href - The URL to prefetch. Typically this will come from a <Link>,\n * or router.prefetch. It must be validated before we attempt to prefetch it.\n * @param nextUrl - A special header used by the server for interception routes.\n * Roughly corresponds to the current URL.\n * @param treeAtTimeOfPrefetch - The FlightRouterState at the time the prefetch\n * was requested. This is only used when PPR is disabled.\n * @param fetchStrategy - Whether to prefetch dynamic data, in addition to\n * static data. This is used by `<Link prefetch={true}>`.\n * @param onInvalidate - A callback that will be called when the prefetch cache\n * When called, it signals to the listener that the data associated with the\n * prefetch may have been invalidated from the cache. This is not a live\n * subscription — it's called at most once per `prefetch` call. The only\n * supported use case is to trigger a new prefetch inside the listener, if\n * desired. It also may be called even in cases where the associated data is\n * still cached. Prefetching is a poll-based (pull) operation, not an event-\n * based (push) one. Rather than subscribe to specific cache entries, you\n * occasionally poll the prefetch cache to check if anything is missing.\n */\nexport function prefetch(\n href: string,\n nextUrl: string | null,\n treeAtTimeOfPrefetch: FlightRouterState,\n fetchStrategy: PrefetchTaskFetchStrategy,\n onInvalidate: null | (() => void)\n) {\n const url = createPrefetchURL(href)\n if (url === null) {\n // This href should not be prefetched.\n return\n }\n const cacheKey = createCacheKey(url.href, nextUrl)\n schedulePrefetchTask(\n cacheKey,\n treeAtTimeOfPrefetch,\n fetchStrategy,\n PrefetchPriority.Default,\n onInvalidate\n )\n}\n"],"names":["prefetch","href","nextUrl","treeAtTimeOfPrefetch","fetchStrategy","onInvalidate","url","createPrefetchURL","cacheKey","createCacheKey","schedulePrefetchTask","PrefetchPriority","Default"],"mappings":";;;;+BA0BgBA;;;eAAAA;;;gCAzBkB;0BACH;2BACM;uBAC4B;AAsB1D,SAASA,SACdC,IAAY,EACZC,OAAsB,EACtBC,oBAAuC,EACvCC,aAAwC,EACxCC,YAAiC;IAEjC,MAAMC,MAAMC,IAAAA,iCAAiB,EAACN;IAC9B,IAAIK,QAAQ,MAAM;QAChB,sCAAsC;QACtC;IACF;IACA,MAAME,WAAWC,IAAAA,wBAAc,EAACH,IAAIL,IAAI,EAAEC;IAC1CQ,IAAAA,+BAAoB,EAClBF,UACAL,sBACAC,eACAO,uBAAgB,CAACC,OAAO,EACxBP;AAEJ","ignoreList":[0]}
@@ -0,0 +1,145 @@
import type { FlightRouterState } from '../../../shared/lib/app-router-types';
import type { RouteCacheKey } from './cache-key';
import { type PrefetchTaskFetchStrategy, PrefetchPriority } from './types';
import type { SegmentRequestKey } from '../../../shared/lib/segment-cache/segment-value-encoding';
export type PrefetchTask = {
key: RouteCacheKey;
/**
* The FlightRouterState at the time the task was initiated. This is needed
* when falling back to the non-PPR behavior, which only prefetches up to
* the first loading boundary.
*/
treeAtTimeOfPrefetch: FlightRouterState;
/**
* The cache versions at the time the task was initiated. Used to determine
* if the cache was invalidated since the task was initiated. Route and
* segment caches have separate versions so they can be invalidated
* independently.
*/
routeCacheVersion: number;
segmentCacheVersion: number;
/**
* Whether to prefetch dynamic data, in addition to static data. This is
* used by `<Link prefetch={true}>`.
*
* Note that a task with `FetchStrategy.PPR` might need to use
* `FetchStrategy.LoadingBoundary` instead if we find out that a route
* does not support PPR after doing the initial route prefetch.
*/
fetchStrategy: PrefetchTaskFetchStrategy;
/**
* sortId is an incrementing counter
*
* Newer prefetches are prioritized over older ones, so that as new links
* enter the viewport, they are not starved by older links that are no
* longer relevant. In the future, we can add additional prioritization
* heuristics, like removing prefetches once a link leaves the viewport.
*
* The sortId is assigned when the prefetch is initiated, and reassigned if
* the same task is prefetched again (effectively bumping it to the top of
* the queue).
*
* TODO: We can add additional fields here to indicate what kind of prefetch
* it is. For example, was it initiated by a link? Or was it an imperative
* call? If it was initiated by a link, we can remove it from the queue when
* the link leaves the viewport, but if it was an imperative call, then we
* should keep it in the queue until it's fulfilled.
*
* We can also add priority levels. For example, hovering over a link could
* increase the priority of its prefetch.
*/
sortId: number;
/**
* The priority of the task. Like sortId, this affects the task's position in
* the queue, so it must never be updated without resifting the heap.
*/
priority: PrefetchPriority;
/**
* The phase of the task. Tasks are split into multiple phases so that their
* priority can be adjusted based on what kind of work they're doing.
* Concretely, prefetching the route tree is higher priority than prefetching
* segment data.
*/
phase: PrefetchPhase;
/**
* These fields are temporary state for tracking the currently running task.
* They are reset after each iteration of the task queue.
*/
hasBackgroundWork: boolean;
spawnedRuntimePrefetches: Set<SegmentRequestKey> | null;
/**
* True if the prefetch was cancelled.
*/
isCanceled: boolean;
/**
* The callback passed to `router.prefetch`, if given.
*/
onInvalidate: null | (() => void);
/**
* The index of the task in the heap's backing array. Used to efficiently
* change the priority of a task by re-sifting it, which requires knowing
* where it is in the array. This is only used internally by the heap
* algorithm. The naive alternative is indexOf every time a task is queued,
* which has O(n) complexity.
*
* We also use this field to check whether a task is currently in the queue.
*/
_heapIndex: number;
/**
* Called when the prefetch task finishes (either completed or canceled).
* Used by the Instant Navigation Testing API to await prefetch completion.
* Not exposed in production builds by default.
*
* Note: "Complete" means the scheduler has no more work to do for this task
* — all network requests have been spawned. It does not mean all data has
* been retrieved; responses may still be in flight.
*/
_onComplete?: () => void;
};
/**
* Prefetch tasks are processed in two phases: first the route tree is fetched,
* then the segments. We use this to priortize tasks that have not yet fetched
* the route tree.
*/
declare const enum PrefetchPhase {
RouteTree = 1,
Segments = 0
}
export type PrefetchSubtaskResult<T> = {
/**
* A promise that resolves when the network connection is closed.
*/
closed: Promise<void>;
value: T;
};
/**
* Called by the cache when revalidation occurs. Starts a cooldown period
* during which prefetch requests are blocked to allow CDN cache propagation.
*/
export declare function startRevalidationCooldown(): void;
export type IncludeDynamicData = null | 'full' | 'dynamic';
/**
* Initiates a prefetch task for the given URL. If a prefetch for the same URL
* is already in progress, this will bump it to the top of the queue.
*
* This is not a user-facing function. By the time this is called, the href is
* expected to be validated and normalized.
*
* @param key The RouteCacheKey to prefetch.
* @param treeAtTimeOfPrefetch The app's current FlightRouterState
* @param fetchStrategy Whether to prefetch dynamic data, in addition to
* static data. This is used by `<Link prefetch={true}>`.
* @param _onComplete Called when the prefetch task finishes. Testing API only.
*/
export declare function schedulePrefetchTask(key: RouteCacheKey, treeAtTimeOfPrefetch: FlightRouterState, fetchStrategy: PrefetchTaskFetchStrategy, priority: PrefetchPriority, onInvalidate: null | (() => void), _onComplete?: () => void): PrefetchTask;
export declare function cancelPrefetchTask(task: PrefetchTask): void;
export declare function reschedulePrefetchTask(task: PrefetchTask, treeAtTimeOfPrefetch: FlightRouterState, fetchStrategy: PrefetchTaskFetchStrategy, priority: PrefetchPriority): void;
export declare function isPrefetchTaskDirty(task: PrefetchTask, nextUrl: string | null, tree: FlightRouterState): boolean;
export declare function pingPrefetchScheduler(): void;
/**
* Notify the scheduler that we've received new data for an in-progress
* prefetch. The corresponding task will be added back to the queue (unless the
* task has been canceled in the meantime).
*/
export declare function pingPrefetchTask(task: PrefetchTask): void;
export {};
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
@@ -0,0 +1,43 @@
/**
* Shared types and constants for the Segment Cache.
*/
export declare const enum NavigationResultTag {
MPA = 0,
Success = 1,
NoOp = 2,
Async = 3
}
/**
* The priority of the prefetch task. Higher numbers are higher priority.
*/
export declare const enum PrefetchPriority {
/**
* Assigned to the most recently hovered/touched link. Special network
* bandwidth is reserved for this task only. There's only ever one Intent-
* priority task at a time; when a new Intent task is scheduled, the previous
* one is bumped down to Default.
*/
Intent = 2,
/**
* The default priority for prefetch tasks.
*/
Default = 1,
/**
* Assigned to tasks when they spawn non-blocking background work, like
* revalidating a partially cached entry to see if more data is available.
*/
Background = 0
}
export declare const enum FetchStrategy {
LoadingBoundary = 0,
PPR = 1,
PPRRuntime = 2,
Full = 3
}
/**
* A subset of fetch strategies used for prefetch tasks.
* A prefetch task can't know if it should use `PPR` or `LoadingBoundary`
* until we complete the initial tree prefetch request, so we use `PPR` to signal both cases
* and adjust it based on the route when actually fetching.
* */
export type PrefetchTaskFetchStrategy = FetchStrategy.PPR | FetchStrategy.PPRRuntime | FetchStrategy.Full;
@@ -0,0 +1,69 @@
/**
* Shared types and constants for the Segment Cache.
*/ "use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
0 && (module.exports = {
FetchStrategy: null,
NavigationResultTag: null,
PrefetchPriority: null
});
function _export(target, all) {
for(var name in all)Object.defineProperty(target, name, {
enumerable: true,
get: all[name]
});
}
_export(exports, {
FetchStrategy: function() {
return FetchStrategy;
},
NavigationResultTag: function() {
return NavigationResultTag;
},
PrefetchPriority: function() {
return PrefetchPriority;
}
});
var NavigationResultTag = /*#__PURE__*/ function(NavigationResultTag) {
NavigationResultTag[NavigationResultTag["MPA"] = 0] = "MPA";
NavigationResultTag[NavigationResultTag["Success"] = 1] = "Success";
NavigationResultTag[NavigationResultTag["NoOp"] = 2] = "NoOp";
NavigationResultTag[NavigationResultTag["Async"] = 3] = "Async";
return NavigationResultTag;
}({});
var PrefetchPriority = /*#__PURE__*/ function(PrefetchPriority) {
/**
* Assigned to the most recently hovered/touched link. Special network
* bandwidth is reserved for this task only. There's only ever one Intent-
* priority task at a time; when a new Intent task is scheduled, the previous
* one is bumped down to Default.
*/ PrefetchPriority[PrefetchPriority["Intent"] = 2] = "Intent";
/**
* The default priority for prefetch tasks.
*/ PrefetchPriority[PrefetchPriority["Default"] = 1] = "Default";
/**
* Assigned to tasks when they spawn non-blocking background work, like
* revalidating a partially cached entry to see if more data is available.
*/ PrefetchPriority[PrefetchPriority["Background"] = 0] = "Background";
return PrefetchPriority;
}({});
var FetchStrategy = /*#__PURE__*/ function(FetchStrategy) {
// Deliberately ordered so we can easily compare two segments
// and determine if one segment is "more specific" than another
// (i.e. if it's likely that it contains more data)
FetchStrategy[FetchStrategy["LoadingBoundary"] = 0] = "LoadingBoundary";
FetchStrategy[FetchStrategy["PPR"] = 1] = "PPR";
FetchStrategy[FetchStrategy["PPRRuntime"] = 2] = "PPRRuntime";
FetchStrategy[FetchStrategy["Full"] = 3] = "Full";
return FetchStrategy;
}({});
if ((typeof exports.default === 'function' || (typeof exports.default === 'object' && exports.default !== null)) && typeof exports.default.__esModule === 'undefined') {
Object.defineProperty(exports.default, '__esModule', { value: true });
Object.assign(exports.default, exports);
module.exports = exports.default;
}
//# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
{"version":3,"sources":["../../../../src/client/components/segment-cache/types.ts"],"sourcesContent":["/**\n * Shared types and constants for the Segment Cache.\n */\n\nexport const enum NavigationResultTag {\n MPA,\n Success,\n NoOp,\n Async,\n}\n\n/**\n * The priority of the prefetch task. Higher numbers are higher priority.\n */\nexport const enum PrefetchPriority {\n /**\n * Assigned to the most recently hovered/touched link. Special network\n * bandwidth is reserved for this task only. There's only ever one Intent-\n * priority task at a time; when a new Intent task is scheduled, the previous\n * one is bumped down to Default.\n */\n Intent = 2,\n /**\n * The default priority for prefetch tasks.\n */\n Default = 1,\n /**\n * Assigned to tasks when they spawn non-blocking background work, like\n * revalidating a partially cached entry to see if more data is available.\n */\n Background = 0,\n}\n\nexport const enum FetchStrategy {\n // Deliberately ordered so we can easily compare two segments\n // and determine if one segment is \"more specific\" than another\n // (i.e. if it's likely that it contains more data)\n LoadingBoundary = 0,\n PPR = 1,\n PPRRuntime = 2,\n Full = 3,\n}\n\n/**\n * A subset of fetch strategies used for prefetch tasks.\n * A prefetch task can't know if it should use `PPR` or `LoadingBoundary`\n * until we complete the initial tree prefetch request, so we use `PPR` to signal both cases\n * and adjust it based on the route when actually fetching.\n * */\nexport type PrefetchTaskFetchStrategy =\n | FetchStrategy.PPR\n | FetchStrategy.PPRRuntime\n | FetchStrategy.Full\n"],"names":["FetchStrategy","NavigationResultTag","PrefetchPriority"],"mappings":"AAAA;;CAEC;;;;;;;;;;;;;;;;IA+BiBA,aAAa;eAAbA;;IA7BAC,mBAAmB;eAAnBA;;IAUAC,gBAAgB;eAAhBA;;;AAVX,IAAA,AAAWD,6CAAAA;;;;;WAAAA;;AAUX,IAAA,AAAWC,0CAAAA;IAChB;;;;;GAKC;IAED;;GAEC;IAED;;;GAGC;WAfeA;;AAmBX,IAAA,AAAWF,uCAAAA;IAChB,6DAA6D;IAC7D,+DAA+D;IAC/D,mDAAmD;;;;;WAHnCA","ignoreList":[0]}
@@ -0,0 +1,78 @@
import { FetchStrategy } from './types';
import type { NormalizedPathname, NormalizedSearch, NormalizedNextUrl } from './cache-key';
import type { RouteTree } from './cache';
import { type FallbackType } from './cache-map';
type Opaque<T, K> = 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 = {
/**
* Identifies which param this vary path node corresponds to. Used by
* getFulfilledSegmentVaryPath to determine which params to replace with
* Fallback based on the varyParams set from the server.
*
* - For path params: the param name (e.g., 'slug')
* - For search params: '?'
* - For non-param nodes (request keys, etc.): null
*/
id: string | null;
value: string | null | FallbackType;
parent: VaryPath | null;
};
export type RouteVaryPath = Opaque<{
id: null;
value: NormalizedPathname;
parent: {
id: '?';
value: NormalizedSearch;
parent: {
id: null;
value: NormalizedNextUrl | null | FallbackType;
parent: null;
};
};
}, 'RouteVaryPath'>;
export type LayoutVaryPath = Opaque<{
id: null;
value: string;
parent: PartialSegmentVaryPath | null;
}, 'LayoutVaryPath'>;
export type PageVaryPath = Opaque<{
id: null;
value: string;
parent: {
id: '?';
value: NormalizedSearch | FallbackType;
parent: PartialSegmentVaryPath | null;
};
}, 'PageVaryPath'>;
export type SegmentVaryPath = LayoutVaryPath | PageVaryPath;
export type PartialSegmentVaryPath = Opaque<VaryPath, 'PartialSegmentVaryPath'>;
export declare function getRouteVaryPath(pathname: NormalizedPathname, search: NormalizedSearch, nextUrl: NormalizedNextUrl | null): RouteVaryPath;
export declare function getFulfilledRouteVaryPath(pathname: NormalizedPathname, search: NormalizedSearch, nextUrl: NormalizedNextUrl | null, couldBeIntercepted: boolean): RouteVaryPath;
export declare function appendLayoutVaryPath(parentPath: PartialSegmentVaryPath | null, cacheKey: string, paramName: string): PartialSegmentVaryPath;
export declare function finalizeLayoutVaryPath(requestKey: string, varyPath: PartialSegmentVaryPath | null): LayoutVaryPath;
export declare function getPartialLayoutVaryPath(finalizedVaryPath: LayoutVaryPath): PartialSegmentVaryPath | null;
export declare function finalizePageVaryPath(requestKey: string, renderedSearch: NormalizedSearch, varyPath: PartialSegmentVaryPath | null): PageVaryPath;
export declare function getPartialPageVaryPath(finalizedVaryPath: PageVaryPath): PartialSegmentVaryPath | null;
export declare function finalizeMetadataVaryPath(pageRequestKey: string, renderedSearch: NormalizedSearch, varyPath: PartialSegmentVaryPath | null): PageVaryPath;
export declare function getSegmentVaryPathForRequest(fetchStrategy: FetchStrategy, tree: RouteTree): SegmentVaryPath;
export declare function clonePageVaryPathWithNewSearchParams(originalVaryPath: PageVaryPath, newSearch: NormalizedSearch): PageVaryPath;
export declare function getRenderedSearchFromVaryPath(varyPath: PageVaryPath): NormalizedSearch | null;
export declare function getFulfilledSegmentVaryPath(original: VaryPath, varyParams: Set<string>): SegmentVaryPath;
export {};
@@ -0,0 +1,278 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
0 && (module.exports = {
appendLayoutVaryPath: null,
clonePageVaryPathWithNewSearchParams: null,
finalizeLayoutVaryPath: null,
finalizeMetadataVaryPath: null,
finalizePageVaryPath: null,
getFulfilledRouteVaryPath: null,
getFulfilledSegmentVaryPath: null,
getPartialLayoutVaryPath: null,
getPartialPageVaryPath: null,
getRenderedSearchFromVaryPath: null,
getRouteVaryPath: null,
getSegmentVaryPathForRequest: null
});
function _export(target, all) {
for(var name in all)Object.defineProperty(target, name, {
enumerable: true,
get: all[name]
});
}
_export(exports, {
appendLayoutVaryPath: function() {
return appendLayoutVaryPath;
},
clonePageVaryPathWithNewSearchParams: function() {
return clonePageVaryPathWithNewSearchParams;
},
finalizeLayoutVaryPath: function() {
return finalizeLayoutVaryPath;
},
finalizeMetadataVaryPath: function() {
return finalizeMetadataVaryPath;
},
finalizePageVaryPath: function() {
return finalizePageVaryPath;
},
getFulfilledRouteVaryPath: function() {
return getFulfilledRouteVaryPath;
},
getFulfilledSegmentVaryPath: function() {
return getFulfilledSegmentVaryPath;
},
getPartialLayoutVaryPath: function() {
return getPartialLayoutVaryPath;
},
getPartialPageVaryPath: function() {
return getPartialPageVaryPath;
},
getRenderedSearchFromVaryPath: function() {
return getRenderedSearchFromVaryPath;
},
getRouteVaryPath: function() {
return getRouteVaryPath;
},
getSegmentVaryPathForRequest: function() {
return getSegmentVaryPathForRequest;
}
});
const _types = require("./types");
const _cachemap = require("./cache-map");
const _segmentvalueencoding = require("../../../shared/lib/segment-cache/segment-value-encoding");
function getRouteVaryPath(pathname, search, nextUrl) {
// requestKey -> searchParams -> nextUrl
const varyPath = {
id: null,
value: pathname,
parent: {
id: '?',
value: search,
parent: {
id: null,
value: nextUrl,
parent: null
}
}
};
return varyPath;
}
function getFulfilledRouteVaryPath(pathname, search, nextUrl, couldBeIntercepted) {
// 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 = {
id: null,
value: pathname,
parent: {
id: '?',
value: search,
parent: {
id: null,
value: couldBeIntercepted ? nextUrl : _cachemap.Fallback,
parent: null
}
}
};
return varyPath;
}
function appendLayoutVaryPath(parentPath, cacheKey, paramName) {
const varyPathPart = {
id: paramName,
value: cacheKey,
parent: parentPath
};
return varyPathPart;
}
function finalizeLayoutVaryPath(requestKey, varyPath) {
const layoutVaryPath = {
id: null,
value: requestKey,
parent: varyPath
};
return layoutVaryPath;
}
function getPartialLayoutVaryPath(finalizedVaryPath) {
// This is the inverse of finalizeLayoutVaryPath.
return finalizedVaryPath.parent;
}
function finalizePageVaryPath(requestKey, renderedSearch, varyPath) {
// Unlike layouts, a page segment's vary path also includes the search string.
// requestKey -> searchParams -> pathParams
const pageVaryPath = {
id: null,
value: requestKey,
parent: {
id: '?',
value: renderedSearch,
parent: varyPath
}
};
return pageVaryPath;
}
function getPartialPageVaryPath(finalizedVaryPath) {
// This is the inverse of finalizePageVaryPath.
return finalizedVaryPath.parent.parent;
}
function finalizeMetadataVaryPath(pageRequestKey, renderedSearch, varyPath) {
// 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 = {
id: null,
// 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 + _segmentvalueencoding.HEAD_REQUEST_KEY,
parent: {
id: '?',
value: renderedSearch,
parent: varyPath
}
};
return pageVaryPath;
}
function getSegmentVaryPathForRequest(fetchStrategy, tree) {
// 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 === _types.FetchStrategy.Full || fetchStrategy === _types.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.parent;
const pathParamsVaryPath = searchParamsVaryPath.parent;
const patchedVaryPath = {
id: null,
value: originalVaryPath.value,
parent: {
id: '?',
value: _cachemap.Fallback,
parent: pathParamsVaryPath
}
};
return patchedVaryPath;
}
}
// The request does vary on search params. We don't need to modify anything.
return originalVaryPath;
}
function clonePageVaryPathWithNewSearchParams(originalVaryPath, newSearch) {
// requestKey -> searchParams -> pathParams
// ^ This part gets replaced with newSearch
const searchParamsVaryPath = originalVaryPath.parent;
const clonedVaryPath = {
id: null,
value: originalVaryPath.value,
parent: {
id: '?',
value: newSearch,
parent: searchParamsVaryPath.parent
}
};
return clonedVaryPath;
}
function getRenderedSearchFromVaryPath(varyPath) {
const searchParams = varyPath.parent.value;
return typeof searchParams === 'string' ? searchParams : null;
}
function getFulfilledSegmentVaryPath(original, varyParams) {
// Re-keys a segment's vary path based on which params the segment actually
// depends on. Params that are NOT in the varyParams set are replaced with
// Fallback, allowing the cache entry to be reused across different values of
// those params.
// This is called when a segment is fulfilled with data from the server. The
// varyParams set comes from the server and indicates which params were
// accessed during rendering.
const clone = {
id: original.id,
// If the id is null, this node is not a param (e.g., it's a request key).
// If the id is in the varyParams set, keep the original value.
// Otherwise, replace with Fallback to make it reusable.
value: original.id === null || varyParams.has(original.id) ? original.value : _cachemap.Fallback,
parent: original.parent === null ? null : getFulfilledSegmentVaryPath(original.parent, varyParams)
};
return clone;
}
if ((typeof exports.default === 'function' || (typeof exports.default === 'object' && exports.default !== null)) && typeof exports.default.__esModule === 'undefined') {
Object.defineProperty(exports.default, '__esModule', { value: true });
Object.assign(exports.default, exports);
module.exports = exports.default;
}
//# sourceMappingURL=vary-path.js.map
File diff suppressed because one or more lines are too long