deploy: v0.12 fix 存档key名

This commit is contained in:
2026-06-24 03:09:02 +00:00
parent 7a08d49fe3
commit e0a5281119
58743 changed files with 7297269 additions and 39505 deletions
@@ -0,0 +1,12 @@
// TypeScript trick to simulate opaque types, like in Flow.
export function createCacheKey(originalHref, nextUrl) {
const originalUrl = new URL(originalHref);
const cacheKey = {
pathname: originalUrl.pathname,
search: originalUrl.search,
nextUrl: nextUrl
};
return cacheKey;
}
//# 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;AAmB3D,OAAO,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]}
+253
View File
@@ -0,0 +1,253 @@
import { lruPut, updateLruSize, deleteFromLru } from './lru';
export 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 = {};
export 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;
}
export 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.
lruPut(entry);
return entry.value;
}
export 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;
}
export 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.
lruPut(entry);
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;
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);
}
}
export 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;
}
export function deleteMapEntry(entry) {
// Delete the entry from the cache.
entry.value = null;
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);
}
}
}
export 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;
updateLruSize(entry, size);
}
//# sourceMappingURL=cache-map.js.map
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
+113
View File
@@ -0,0 +1,113 @@
import { deleteMapEntry } from './cache-map';
// 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 didScheduleCleanup = false;
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
;
export 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;
}
export 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();
}
export 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 (didScheduleCleanup || lruSize <= maxLruSize) {
return;
}
didScheduleCleanup = true;
requestCleanupCallback(cleanup);
}
function cleanup() {
didScheduleCleanup = false;
// 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.
deleteMapEntry(tail);
}
}
}
const requestCleanupCallback = typeof requestIdleCallback === 'function' ? requestIdleCallback : (cb)=>setTimeout(cb, 0);
//# sourceMappingURL=lru.js.map
File diff suppressed because one or more lines are too long
@@ -0,0 +1,434 @@
import { fetchServerResponse } from '../router-reducer/fetch-server-response';
import { startPPRNavigation, spawnDynamicRequests, FreshnessPolicy } from '../router-reducer/ppr-navigations';
import { createHrefFromUrl } from '../router-reducer/create-href-from-url';
import { EntryStatus, readRouteCacheEntry, readSegmentCacheEntry, waitForSegmentCacheEntry, requestOptimisticRouteCacheEntry } from './cache';
import { createCacheKey } from './cache-key';
import { addSearchParamsIfPageSegment } from '../../../shared/lib/segment';
import { NavigationResultTag } from './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 function navigate(url, currentUrl, currentCacheNode, currentFlightRouterState, nextUrl, freshnessPolicy, shouldScroll, accumulation) {
const now = Date.now();
const href = url.href;
// 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 = href === currentUrl.href;
const cacheKey = createCacheKey(href, nextUrl);
const route = readRouteCacheEntry(now, cacheKey);
if (route !== null && route.status === EntryStatus.Fulfilled) {
// We have a matching prefetch.
const snapshot = readRenderSnapshotFromCache(now, route, route.tree);
const prefetchFlightRouterState = snapshot.flightRouterState;
const prefetchSeedData = snapshot.seedData;
const headSnapshot = readHeadSnapshotFromCache(now, route);
const prefetchHead = headSnapshot.rsc;
const isPrefetchHeadPartial = headSnapshot.isPartial;
// TODO: The "canonicalUrl" stored in the cache doesn't include the hash,
// because hash entries do not vary by hash fragment. However, the one
// we set in the router state *does* include the hash, and it's used to
// sync with the actual browser location. To make this less of a refactor
// hazard, we should always track the hash separately from the rest of
// the URL.
const newCanonicalUrl = route.canonicalUrl + url.hash;
const renderedSearch = route.renderedSearch;
return navigateUsingPrefetchedRouteTree(now, url, currentUrl, nextUrl, isSamePageNavigation, currentCacheNode, currentFlightRouterState, prefetchFlightRouterState, prefetchSeedData, prefetchHead, isPrefetchHeadPartial, newCanonicalUrl, renderedSearch, freshnessPolicy, shouldScroll);
}
// There was no matching route tree in the cache. Let's see if we can
// construct an "optimistic" route tree.
//
// 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 (route === null || route.status !== EntryStatus.Rejected) {
const optimisticRoute = requestOptimisticRouteCacheEntry(now, url, nextUrl);
if (optimisticRoute !== null) {
// We have an optimistic route tree. Proceed with the normal flow.
const snapshot = readRenderSnapshotFromCache(now, optimisticRoute, optimisticRoute.tree);
const prefetchFlightRouterState = snapshot.flightRouterState;
const prefetchSeedData = snapshot.seedData;
const headSnapshot = readHeadSnapshotFromCache(now, optimisticRoute);
const prefetchHead = headSnapshot.rsc;
const isPrefetchHeadPartial = headSnapshot.isPartial;
const newCanonicalUrl = optimisticRoute.canonicalUrl + url.hash;
const newRenderedSearch = optimisticRoute.renderedSearch;
return navigateUsingPrefetchedRouteTree(now, url, currentUrl, nextUrl, isSamePageNavigation, currentCacheNode, currentFlightRouterState, prefetchFlightRouterState, prefetchSeedData, prefetchHead, isPrefetchHeadPartial, newCanonicalUrl, newRenderedSearch, freshnessPolicy, shouldScroll);
}
}
// There's no matching prefetch for this route in the cache.
let collectedDebugInfo = accumulation.collectedDebugInfo ?? [];
if (accumulation.collectedDebugInfo === undefined) {
collectedDebugInfo = accumulation.collectedDebugInfo = [];
}
return {
tag: NavigationResultTag.Async,
data: navigateDynamicallyWithNoPrefetch(now, url, currentUrl, nextUrl, currentCacheNode, currentFlightRouterState, freshnessPolicy, shouldScroll, collectedDebugInfo)
};
}
export function navigateToSeededRoute(now, url, canonicalUrl, navigationSeed, currentUrl, currentCacheNode, currentFlightRouterState, freshnessPolicy, nextUrl, shouldScroll) {
// A version of navigate() that accepts the target route tree as an argument
// rather than reading it from the prefetch cache.
const accumulation = {
scrollableSegments: null,
separateRefreshUrls: null
};
const isSamePageNavigation = url.href === currentUrl.href;
const task = startPPRNavigation(now, currentUrl, currentCacheNode, currentFlightRouterState, navigationSeed.tree, freshnessPolicy, navigationSeed.data, navigationSeed.head, null, null, false, isSamePageNavigation, accumulation);
if (task !== null) {
spawnDynamicRequests(task, url, nextUrl, freshnessPolicy, accumulation);
return navigationTaskToResult(task, canonicalUrl, navigationSeed.renderedSearch, accumulation.scrollableSegments, shouldScroll, url.hash);
}
// Could not perform a SPA navigation. Revert to a full-page (MPA) navigation.
return {
tag: NavigationResultTag.MPA,
data: canonicalUrl
};
}
function navigateUsingPrefetchedRouteTree(now, url, currentUrl, nextUrl, isSamePageNavigation, currentCacheNode, currentFlightRouterState, prefetchFlightRouterState, prefetchSeedData, prefetchHead, isPrefetchHeadPartial, canonicalUrl, renderedSearch, freshnessPolicy, shouldScroll) {
// Recursively construct a prefetch tree by reading from the Segment Cache. To
// maintain compatibility, we output the same data structures as the old
// prefetching implementation: FlightRouterState and CacheNodeSeedData.
// TODO: Eventually updateCacheNodeOnNavigation (or the equivalent) should
// read from the Segment Cache directly. It's only structured this way for now
// so we can share code with the old prefetching implementation.
const accumulation = {
scrollableSegments: null,
separateRefreshUrls: null
};
const seedData = null;
const seedHead = null;
const task = startPPRNavigation(now, currentUrl, currentCacheNode, currentFlightRouterState, prefetchFlightRouterState, freshnessPolicy, seedData, seedHead, prefetchSeedData, prefetchHead, isPrefetchHeadPartial, isSamePageNavigation, accumulation);
if (task !== null) {
spawnDynamicRequests(task, url, nextUrl, freshnessPolicy, accumulation);
return navigationTaskToResult(task, canonicalUrl, renderedSearch, accumulation.scrollableSegments, shouldScroll, url.hash);
}
// Could not perform a SPA navigation. Revert to a full-page (MPA) navigation.
return {
tag: NavigationResultTag.MPA,
data: canonicalUrl
};
}
function navigationTaskToResult(task, canonicalUrl, renderedSearch, scrollableSegments, shouldScroll, hash) {
return {
tag: NavigationResultTag.Success,
data: {
flightRouterState: task.route,
cacheNode: task.node,
canonicalUrl,
renderedSearch,
scrollableSegments,
shouldScroll,
hash
}
};
}
function readRenderSnapshotFromCache(now, route, tree) {
let childRouterStates = {};
let childSeedDatas = {};
const slots = tree.slots;
if (slots !== null) {
for(const parallelRouteKey in slots){
const childTree = slots[parallelRouteKey];
const childResult = readRenderSnapshotFromCache(now, route, childTree);
childRouterStates[parallelRouteKey] = childResult.flightRouterState;
childSeedDatas[parallelRouteKey] = childResult.seedData;
}
}
let rsc = null;
let loading = null;
let isPartial = true;
const segmentEntry = readSegmentCacheEntry(now, tree.varyPath);
if (segmentEntry !== null) {
switch(segmentEntry.status){
case EntryStatus.Fulfilled:
{
// Happy path: a cache hit
rsc = segmentEntry.rsc;
loading = segmentEntry.loading;
isPartial = segmentEntry.isPartial;
break;
}
case EntryStatus.Pending:
{
// We haven't received data for this segment yet, but there's already
// an in-progress request. Since it's extremely likely to arrive
// before the dynamic data response, we might as well use it.
const promiseForFulfilledEntry = waitForSegmentCacheEntry(segmentEntry);
rsc = promiseForFulfilledEntry.then((entry)=>entry !== null ? entry.rsc : null);
loading = promiseForFulfilledEntry.then((entry)=>entry !== null ? entry.loading : null);
// Because the request is still pending, we typically don't know yet
// whether the response will be partial. We shouldn't skip this segment
// during the dynamic navigation request. Otherwise, we might need to
// do yet another request to fill in the remaining data, creating
// a waterfall.
//
// The one exception is if this segment is being fetched with via
// prefetch={true} (i.e. the "force stale" or "full" strategy). If so,
// we can assume the response will be full. This field is set to `false`
// for such segments.
isPartial = segmentEntry.isPartial;
break;
}
case EntryStatus.Empty:
case EntryStatus.Rejected:
break;
default:
segmentEntry;
}
}
// The navigation implementation expects the search params to be
// included in the segment. However, the Segment Cache tracks search
// params separately from the rest of the segment key. So we need to
// add them back here.
//
// See corresponding comment in convertFlightRouterStateToTree.
//
// TODO: What we should do instead is update the navigation diffing
// logic to compare search params explicitly. This is a temporary
// solution until more of the Segment Cache implementation has settled.
const segment = addSearchParamsIfPageSegment(tree.segment, Object.fromEntries(new URLSearchParams(route.renderedSearch)));
// We don't need this information in a render snapshot, so this can just be a placeholder.
const hasRuntimePrefetch = false;
return {
flightRouterState: [
segment,
childRouterStates,
null,
null,
tree.isRootLayout
],
seedData: [
rsc,
childSeedDatas,
loading,
isPartial,
hasRuntimePrefetch
]
};
}
function readHeadSnapshotFromCache(now, route) {
// Same as readRenderSnapshotFromCache, but for the head
let rsc = null;
let isPartial = true;
const segmentEntry = readSegmentCacheEntry(now, route.metadata.varyPath);
if (segmentEntry !== null) {
switch(segmentEntry.status){
case EntryStatus.Fulfilled:
{
rsc = segmentEntry.rsc;
isPartial = segmentEntry.isPartial;
break;
}
case EntryStatus.Pending:
{
const promiseForFulfilledEntry = waitForSegmentCacheEntry(segmentEntry);
rsc = promiseForFulfilledEntry.then((entry)=>entry !== null ? entry.rsc : null);
isPartial = segmentEntry.isPartial;
break;
}
case EntryStatus.Empty:
case EntryStatus.Rejected:
break;
default:
segmentEntry;
}
}
return {
rsc,
isPartial
};
}
// 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 navigateDynamicallyWithNoPrefetch(now, url, currentUrl, nextUrl, currentCacheNode, currentFlightRouterState, freshnessPolicy, shouldScroll, collectedDebugInfo) {
// 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 FreshnessPolicy.Default:
case FreshnessPolicy.HistoryTraversal:
dynamicRequestTree = currentFlightRouterState;
break;
case FreshnessPolicy.Hydration:
case FreshnessPolicy.RefreshAll:
case FreshnessPolicy.HMRRefresh:
dynamicRequestTree = DynamicRequestTreeForEntireRoute;
break;
default:
freshnessPolicy;
dynamicRequestTree = currentFlightRouterState;
break;
}
const promiseForDynamicServerResponse = fetchServerResponse(url, {
flightRouterState: dynamicRequestTree,
nextUrl
});
const result = await promiseForDynamicServerResponse;
if (typeof result === 'string') {
// This is an MPA navigation.
const newUrl = result;
return {
tag: NavigationResultTag.MPA,
data: newUrl
};
}
const { flightData, canonicalUrl, renderedSearch, debugInfo: debugInfoFromResponse } = result;
if (debugInfoFromResponse !== null) {
collectedDebugInfo.push(...debugInfoFromResponse);
}
// 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(currentFlightRouterState, flightData, renderedSearch);
return navigateToSeededRoute(now, url, createHrefFromUrl(canonicalUrl), navigationSeed, currentUrl, currentCacheNode, currentFlightRouterState, freshnessPolicy, nextUrl, shouldScroll);
}
export function convertServerPatchToFullTree(currentTree, flightData, renderedSearch) {
// 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;
for (const { segmentPath, tree: treePatch, seedData: dataPatch, head: headPatch } of flightData){
const result = convertServerPatchToFullTreeImpl(baseTree, baseData, treePatch, dataPatch, segmentPath, 0);
baseTree = result.tree;
baseData = result.data;
// This is the same for all patches per response, so just pick an
// arbitrary one
head = headPatch;
}
return {
tree: baseTree,
data: baseData,
renderedSearch,
head
};
}
function convertServerPatchToFullTreeImpl(baseRouterState, baseData, treePatch, dataPatch, segmentPath, 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, // 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) {
clonedTree[2] = baseRouterState[2];
}
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,
false
];
return {
tree: clonedTree,
data: clonedSeedData
};
}
//# sourceMappingURL=navigation.js.map
File diff suppressed because one or more lines are too long
+34
View File
@@ -0,0 +1,34 @@
import { createPrefetchURL } from '../app-router-utils';
import { createCacheKey } from './cache-key';
import { schedulePrefetchTask } from './scheduler';
import { PrefetchPriority } 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 function prefetch(href, nextUrl, treeAtTimeOfPrefetch, fetchStrategy, onInvalidate) {
const url = createPrefetchURL(href);
if (url === null) {
// This href should not be prefetched.
return;
}
const cacheKey = createCacheKey(url.href, nextUrl);
schedulePrefetchTask(cacheKey, treeAtTimeOfPrefetch, fetchStrategy, PrefetchPriority.Default, onInvalidate);
}
//# 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":["createPrefetchURL","createCacheKey","schedulePrefetchTask","PrefetchPriority","prefetch","href","nextUrl","treeAtTimeOfPrefetch","fetchStrategy","onInvalidate","url","cacheKey","Default"],"mappings":"AACA,SAASA,iBAAiB,QAAQ,sBAAqB;AACvD,SAASC,cAAc,QAAQ,cAAa;AAC5C,SAASC,oBAAoB,QAAQ,cAAa;AAClD,SAASC,gBAAgB,QAAwC,UAAS;AAE1E;;;;;;;;;;;;;;;;;;;CAmBC,GACD,OAAO,SAASC,SACdC,IAAY,EACZC,OAAsB,EACtBC,oBAAuC,EACvCC,aAAwC,EACxCC,YAAiC;IAEjC,MAAMC,MAAMV,kBAAkBK;IAC9B,IAAIK,QAAQ,MAAM;QAChB,sCAAsC;QACtC;IACF;IACA,MAAMC,WAAWV,eAAeS,IAAIL,IAAI,EAAEC;IAC1CJ,qBACES,UACAJ,sBACAC,eACAL,iBAAiBS,OAAO,EACxBH;AAEJ","ignoreList":[0]}
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
+39
View File
@@ -0,0 +1,39 @@
/**
* Shared types and constants for the Segment Cache.
*/ export 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;
}({});
/**
* The priority of the prefetch task. Higher numbers are higher priority.
*/ export 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;
}({});
export 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;
}({});
//# 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":["NavigationResultTag","PrefetchPriority","FetchStrategy"],"mappings":"AAAA;;CAEC,GAED,OAAO,IAAA,AAAWA,6CAAAA;;;;;WAAAA;MAKjB;AAED;;CAEC,GACD,OAAO,IAAA,AAAWC,0CAAAA;IAChB;;;;;GAKC;IAED;;GAEC;IAED;;;GAGC;WAfeA;MAiBjB;AAED,OAAO,IAAA,AAAWC,uCAAAA;IAChB,6DAA6D;IAC7D,+DAA+D;IAC/D,mDAAmD;;;;;WAHnCA;MAQjB","ignoreList":[0]}
+164
View File
@@ -0,0 +1,164 @@
import { FetchStrategy } from './types';
import { Fallback } from './cache-map';
import { HEAD_REQUEST_KEY } from '../../../shared/lib/segment-cache/segment-value-encoding';
export function getRouteVaryPath(pathname, search, nextUrl) {
// requestKey -> searchParams -> nextUrl
const varyPath = {
value: pathname,
parent: {
value: search,
parent: {
value: nextUrl,
parent: null
}
}
};
return varyPath;
}
export 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 = {
value: pathname,
parent: {
value: search,
parent: {
value: couldBeIntercepted ? nextUrl : Fallback,
parent: null
}
}
};
return varyPath;
}
export function appendLayoutVaryPath(parentPath, cacheKey) {
const varyPathPart = {
value: cacheKey,
parent: parentPath
};
return varyPathPart;
}
export function finalizeLayoutVaryPath(requestKey, varyPath) {
const layoutVaryPath = {
value: requestKey,
parent: varyPath
};
return layoutVaryPath;
}
export function finalizePageVaryPath(requestKey, renderedSearch, varyPath) {
// Unlike layouts, a page segment's vary path also includes the search string.
// requestKey -> searchParams -> pathParams
const pageVaryPath = {
value: requestKey,
parent: {
value: renderedSearch,
parent: varyPath
}
};
return pageVaryPath;
}
export 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 = {
// Append the actual metadata request key to the page request key. Note
// that we're not using a separate vary path part; it's unnecessary because
// these are not conceptually separate inputs.
value: pageRequestKey + HEAD_REQUEST_KEY,
parent: {
value: renderedSearch,
parent: varyPath
}
};
return pageVaryPath;
}
export 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 === FetchStrategy.Full || fetchStrategy === FetchStrategy.PPRRuntime;
if (!doesVaryOnSearchParams) {
// The response from the the server will not vary on search params. Clone
// the end of the original vary path to replace the search params
// with Fallback.
//
// requestKey -> searchParams -> pathParams
// ^ This part gets replaced with Fallback
const searchParamsVaryPath = originalVaryPath.parent;
const pathParamsVaryPath = searchParamsVaryPath.parent;
const patchedVaryPath = {
value: originalVaryPath.value,
parent: {
value: Fallback,
parent: pathParamsVaryPath
}
};
return patchedVaryPath;
}
}
// The request does vary on search params. We don't need to modify anything.
return originalVaryPath;
}
export function clonePageVaryPathWithNewSearchParams(originalVaryPath, newSearch) {
// requestKey -> searchParams -> pathParams
// ^ This part gets replaced with newSearch
const searchParamsVaryPath = originalVaryPath.parent;
const clonedVaryPath = {
value: originalVaryPath.value,
parent: {
value: newSearch,
parent: searchParamsVaryPath.parent
}
};
return clonedVaryPath;
}
//# sourceMappingURL=vary-path.js.map
File diff suppressed because one or more lines are too long