180 lines
8.7 KiB
JavaScript
180 lines
8.7 KiB
JavaScript
import { createHrefFromUrl } from "./create-href-from-url";
|
|
import { fetchServerResponse } from "./fetch-server-response";
|
|
import { PrefetchCacheEntryStatus, PrefetchKind } from "./router-reducer-types";
|
|
import { prefetchQueue } from "./reducers/prefetch-reducer";
|
|
/**
|
|
* Creates a cache key for the router prefetch cache
|
|
*
|
|
* @param url - The URL being navigated to
|
|
* @param nextUrl - an internal URL, primarily used for handling rewrites. Defaults to '/'.
|
|
* @return The generated prefetch cache key.
|
|
*/ function createPrefetchCacheKey(url, nextUrl) {
|
|
const pathnameFromUrl = createHrefFromUrl(url, // Ensures the hash is not part of the cache key as it does not impact the server fetch
|
|
false);
|
|
// nextUrl is used as a cache key delimiter since entries can vary based on the Next-URL header
|
|
if (nextUrl) {
|
|
return nextUrl + "%" + pathnameFromUrl;
|
|
}
|
|
return pathnameFromUrl;
|
|
}
|
|
/**
|
|
* Returns a prefetch cache entry if one exists. Otherwise creates a new one and enqueues a fetch request
|
|
* to retrieve the prefetch data from the server.
|
|
*/ export function getOrCreatePrefetchCacheEntry(param) {
|
|
let { url, nextUrl, tree, buildId, prefetchCache, kind } = param;
|
|
let existingCacheEntry = undefined;
|
|
// We first check if there's a more specific interception route prefetch entry
|
|
// This is because when we detect a prefetch that corresponds with an interception route, we prefix it with nextUrl (see `createPrefetchCacheKey`)
|
|
// to avoid conflicts with other pages that may have the same URL but render different things depending on the `Next-URL` header.
|
|
const interceptionCacheKey = createPrefetchCacheKey(url, nextUrl);
|
|
const interceptionData = prefetchCache.get(interceptionCacheKey);
|
|
if (interceptionData) {
|
|
existingCacheEntry = interceptionData;
|
|
} else {
|
|
// If we dont find a more specific interception route prefetch entry, we check for a regular prefetch entry
|
|
const prefetchCacheKey = createPrefetchCacheKey(url);
|
|
const prefetchData = prefetchCache.get(prefetchCacheKey);
|
|
if (prefetchData) {
|
|
existingCacheEntry = prefetchData;
|
|
}
|
|
}
|
|
if (existingCacheEntry) {
|
|
// Grab the latest status of the cache entry and update it
|
|
existingCacheEntry.status = getPrefetchEntryCacheStatus(existingCacheEntry);
|
|
// when `kind` is provided, an explicit prefetch was requested.
|
|
// if the requested prefetch is "full" and the current cache entry wasn't, we want to re-prefetch with the new intent
|
|
const switchedToFullPrefetch = existingCacheEntry.kind !== PrefetchKind.FULL && kind === PrefetchKind.FULL;
|
|
if (switchedToFullPrefetch) {
|
|
return createLazyPrefetchEntry({
|
|
tree,
|
|
url,
|
|
buildId,
|
|
nextUrl,
|
|
prefetchCache,
|
|
// If we didn't get an explicit prefetch kind, we want to set a temporary kind
|
|
// rather than assuming the same intent as the previous entry, to be consistent with how we
|
|
// lazily create prefetch entries when intent is left unspecified.
|
|
kind: kind != null ? kind : PrefetchKind.TEMPORARY
|
|
});
|
|
}
|
|
// If the existing cache entry was marked as temporary, it means it was lazily created when attempting to get an entry,
|
|
// where we didn't have the prefetch intent. Now that we have the intent (in `kind`), we want to update the entry to the more accurate kind.
|
|
if (kind && existingCacheEntry.kind === PrefetchKind.TEMPORARY) {
|
|
existingCacheEntry.kind = kind;
|
|
}
|
|
// We've determined that the existing entry we found is still valid, so we return it.
|
|
return existingCacheEntry;
|
|
}
|
|
// If we didn't return an entry, create a new one.
|
|
return createLazyPrefetchEntry({
|
|
tree,
|
|
url,
|
|
buildId,
|
|
nextUrl,
|
|
prefetchCache,
|
|
kind: kind || // in dev, there's never gonna be a prefetch entry so we want to prefetch here
|
|
(process.env.NODE_ENV === "development" ? PrefetchKind.AUTO : PrefetchKind.TEMPORARY)
|
|
});
|
|
}
|
|
/*
|
|
* Used to take an existing cache entry and prefix it with the nextUrl, if it exists.
|
|
* This ensures that we don't have conflicting cache entries for the same URL (as is the case with route interception).
|
|
*/ function prefixExistingPrefetchCacheEntry(param) {
|
|
let { url, nextUrl, prefetchCache } = param;
|
|
const existingCacheKey = createPrefetchCacheKey(url);
|
|
const existingCacheEntry = prefetchCache.get(existingCacheKey);
|
|
if (!existingCacheEntry) {
|
|
// no-op -- there wasn't an entry to move
|
|
return;
|
|
}
|
|
const newCacheKey = createPrefetchCacheKey(url, nextUrl);
|
|
prefetchCache.set(newCacheKey, existingCacheEntry);
|
|
prefetchCache.delete(existingCacheKey);
|
|
}
|
|
/**
|
|
* Use to seed the prefetch cache with data that has already been fetched.
|
|
*/ export function createPrefetchCacheEntryForInitialLoad(param) {
|
|
let { nextUrl, tree, prefetchCache, url, kind, data } = param;
|
|
const [, , , intercept] = data;
|
|
// if the prefetch corresponds with an interception route, we use the nextUrl to prefix the cache key
|
|
const prefetchCacheKey = intercept ? createPrefetchCacheKey(url, nextUrl) : createPrefetchCacheKey(url);
|
|
const prefetchEntry = {
|
|
treeAtTimeOfPrefetch: tree,
|
|
data: Promise.resolve(data),
|
|
kind,
|
|
prefetchTime: Date.now(),
|
|
lastUsedTime: Date.now(),
|
|
key: prefetchCacheKey,
|
|
status: PrefetchCacheEntryStatus.fresh
|
|
};
|
|
prefetchCache.set(prefetchCacheKey, prefetchEntry);
|
|
return prefetchEntry;
|
|
}
|
|
/**
|
|
* Creates a prefetch entry entry and enqueues a fetch request to retrieve the data.
|
|
*/ function createLazyPrefetchEntry(param) {
|
|
let { url, kind, tree, nextUrl, buildId, prefetchCache } = param;
|
|
const prefetchCacheKey = createPrefetchCacheKey(url);
|
|
// initiates the fetch request for the prefetch and attaches a listener
|
|
// to the promise to update the prefetch cache entry when the promise resolves (if necessary)
|
|
const data = prefetchQueue.enqueue(()=>fetchServerResponse(url, tree, nextUrl, buildId, kind).then((prefetchResponse)=>{
|
|
// TODO: `fetchServerResponse` should be more tighly coupled to these prefetch cache operations
|
|
// to avoid drift between this cache key prefixing logic
|
|
// (which is currently directly influenced by the server response)
|
|
const [, , , intercepted] = prefetchResponse;
|
|
if (intercepted) {
|
|
prefixExistingPrefetchCacheEntry({
|
|
url,
|
|
nextUrl,
|
|
prefetchCache
|
|
});
|
|
}
|
|
return prefetchResponse;
|
|
}));
|
|
const prefetchEntry = {
|
|
treeAtTimeOfPrefetch: tree,
|
|
data,
|
|
kind,
|
|
prefetchTime: Date.now(),
|
|
lastUsedTime: null,
|
|
key: prefetchCacheKey,
|
|
status: PrefetchCacheEntryStatus.fresh
|
|
};
|
|
prefetchCache.set(prefetchCacheKey, prefetchEntry);
|
|
return prefetchEntry;
|
|
}
|
|
export function prunePrefetchCache(prefetchCache) {
|
|
for (const [href, prefetchCacheEntry] of prefetchCache){
|
|
if (getPrefetchEntryCacheStatus(prefetchCacheEntry) === PrefetchCacheEntryStatus.expired) {
|
|
prefetchCache.delete(href);
|
|
}
|
|
}
|
|
}
|
|
// These values are set by `define-env-plugin` (based on `nextConfig.experimental.staleTimes`)
|
|
// and default to 5 minutes (static) / 30 seconds (dynamic)
|
|
const DYNAMIC_STALETIME_MS = Number(process.env.__NEXT_CLIENT_ROUTER_DYNAMIC_STALETIME) * 1000;
|
|
const STATIC_STALETIME_MS = Number(process.env.__NEXT_CLIENT_ROUTER_STATIC_STALETIME) * 1000;
|
|
function getPrefetchEntryCacheStatus(param) {
|
|
let { kind, prefetchTime, lastUsedTime } = param;
|
|
// We will re-use the cache entry data for up to the `dynamic` staletime window.
|
|
if (Date.now() < (lastUsedTime != null ? lastUsedTime : prefetchTime) + DYNAMIC_STALETIME_MS) {
|
|
return lastUsedTime ? PrefetchCacheEntryStatus.reusable : PrefetchCacheEntryStatus.fresh;
|
|
}
|
|
// For "auto" prefetching, we'll re-use only the loading boundary for up to `static` staletime window.
|
|
// A stale entry will only re-use the `loading` boundary, not the full data.
|
|
// This will trigger a "lazy fetch" for the full data.
|
|
if (kind === "auto") {
|
|
if (Date.now() < prefetchTime + STATIC_STALETIME_MS) {
|
|
return PrefetchCacheEntryStatus.stale;
|
|
}
|
|
}
|
|
// for "full" prefetching, we'll re-use the cache entry data for up to `static` staletime window.
|
|
if (kind === "full") {
|
|
if (Date.now() < prefetchTime + STATIC_STALETIME_MS) {
|
|
return PrefetchCacheEntryStatus.reusable;
|
|
}
|
|
}
|
|
return PrefetchCacheEntryStatus.expired;
|
|
}
|
|
|
|
//# sourceMappingURL=prefetch-cache-utils.js.map
|