next
Version:
The React Framework
186 lines (185 loc) • 9 kB
JavaScript
/**
* This is the default "use cache" handler it defaults to an in-memory store.
* In-memory caches are fragile and should not use stale-while-revalidate
* semantics on the caches because it's not worth warming up an entry that's
* likely going to get evicted before we get to use it anyway. However, we also
* don't want to reuse a stale entry for too long so stale entries should be
* considered expired/missing in such cache handlers.
*
* The dev server (`next dev`) is the exception: to keep reloads fast it serves
* stale entries until they expire, relying on the wrapper's
* stale-while-revalidate path to warm a fresh entry in the background. See
* `get` for where this branches.
*/ import { LRUCache } from '../lru-cache';
import { areTagsExpired, areTagsStale, tagsManifest } from '../incremental-cache/tags-manifest.external';
import { MIN_PRERENDERABLE_EXPIRE } from '../../use-cache/constants';
export function createDefaultCacheHandler(maxSize) {
// If the max size is 0, return a cache handler that doesn't cache anything,
// this avoids an unnecessary LRUCache instance and potential memory
// allocation.
if (maxSize === 0) {
return {
get: ()=>Promise.resolve(undefined),
set: ()=>Promise.resolve(),
refreshTags: ()=>Promise.resolve(),
getExpiration: ()=>Promise.resolve(0),
updateTags: ()=>Promise.resolve()
};
}
const memoryCache = new LRUCache(maxSize, (entry, cacheKey)=>entry.size + cacheKey.length);
const pendingSets = new Map();
const debug = process.env.NEXT_PRIVATE_DEBUG_CACHE ? console.debug.bind(console, 'DefaultCacheHandler:') : undefined;
return {
async get (cacheKey) {
const pendingPromise = pendingSets.get(cacheKey);
if (pendingPromise) {
debug == null ? void 0 : debug('get', cacheKey, 'pending');
await pendingPromise;
}
const privateEntry = memoryCache.get(cacheKey);
if (!privateEntry) {
debug == null ? void 0 : debug('get', cacheKey, 'not found');
return undefined;
}
const entry = privateEntry.entry;
// A negative `expire` is an eviction sentinel: the tiered cache handler
// (dev-only) marks a front entry for deletion by overwriting it with a
// negative `expire`, since the cache-handler interface has no per-key
// delete. Treat it as missing here, independently of the minimum
// retention below (which would otherwise keep it alive). This is distinct
// from `revalidate = -1` below, which keeps serving the entry but forces
// a revalidation.
if (entry.expire < 0) {
debug == null ? void 0 : debug('get', cacheKey, 'evicted');
return undefined;
}
// The dev server serves stale entries until they expire (see the file
// overview); production drops them once past the revalidate time. In dev,
// an entry is retained for at least `MIN_PRERENDERABLE_EXPIRE` so that
// entries with a short `expire` (for example a `cacheLife({ expire: 0 })`
// client-only cache) still linger long enough that a reload hits the
// cache. That minimum is the same threshold below which the "use cache"
// wrapper treats an entry as dynamic, so it only extends the retention of
// entries that are dynamic anyway. It affects retention only; the
// returned entry keeps its real `expire`, so staging decisions are
// unchanged.
const maxAgeSeconds = process.env.__NEXT_DEV_SERVER ? Math.max(entry.expire, MIN_PRERENDERABLE_EXPIRE) : entry.revalidate;
if (performance.timeOrigin + performance.now() > entry.timestamp + maxAgeSeconds * 1000) {
debug == null ? void 0 : debug('get', cacheKey, 'expired');
return undefined;
}
let revalidate = entry.revalidate;
if (areTagsExpired(entry.tags, entry.timestamp)) {
debug == null ? void 0 : debug('get', cacheKey, 'had expired tag');
return undefined;
}
if (areTagsStale(entry.tags, entry.timestamp)) {
debug == null ? void 0 : debug('get', cacheKey, 'had stale tag');
revalidate = -1;
}
const [returnStream, newSaved] = entry.value.tee();
entry.value = newSaved;
debug == null ? void 0 : debug('get', cacheKey, 'found', {
tags: entry.tags,
timestamp: entry.timestamp,
expire: entry.expire,
revalidate
});
return {
...entry,
revalidate,
value: returnStream
};
},
async set (cacheKey, pendingEntry) {
debug == null ? void 0 : debug('set', cacheKey, 'start');
let resolvePending = ()=>{};
const pendingPromise = new Promise((resolve)=>{
resolvePending = resolve;
});
pendingSets.set(cacheKey, pendingPromise);
const entry = await pendingEntry;
let size = 0;
try {
// In production an `expire: 0` entry is dynamic: the "use cache"
// wrapper regenerates it on every read instead of serving the stored
// copy, so persisting it would be a wasted write of a value that is
// never served back. Skip storing it so the next read is a plain miss.
// The dev server keeps it, because its minimum retention serves the
// previously cached value across reloads. The `finally` below still
// resolves the pending set.
if (!process.env.__NEXT_DEV_SERVER && entry.expire === 0) {
debug == null ? void 0 : debug('set', cacheKey, 'skipped dynamic entry');
return;
}
const [value, clonedValue] = entry.value.tee();
entry.value = value;
const reader = clonedValue.getReader();
for(let chunk; !(chunk = await reader.read()).done;){
size += Buffer.from(chunk.value).byteLength;
}
memoryCache.set(cacheKey, {
entry,
isErrored: false,
errorRetryCount: 0,
size
});
debug == null ? void 0 : debug('set', cacheKey, 'done');
} catch (err) {
// TODO: store partial buffer with error after we retry 3 times
debug == null ? void 0 : debug('set', cacheKey, 'failed', err);
} finally{
resolvePending();
pendingSets.delete(cacheKey);
}
},
async refreshTags () {
// Nothing to do for an in-memory cache handler.
},
async getExpiration (tags) {
const expirations = tags.map((tag)=>{
const entry = tagsManifest.get(tag);
if (!entry) return 0;
// Return the most recent timestamp (either expired or stale)
return entry.expired || 0;
});
const expiration = Math.max(...expirations, 0);
debug == null ? void 0 : debug('getExpiration', {
tags,
expiration
});
return expiration;
},
async updateTags (tags, durations) {
const now = Math.round(performance.timeOrigin + performance.now());
debug == null ? void 0 : debug('updateTags', {
tags,
timestamp: now
});
for (const tag of tags){
// TODO: update file-system-cache?
const existingEntry = tagsManifest.get(tag) || {};
if (durations) {
// Use provided durations directly
const updates = {
...existingEntry
};
// mark as stale immediately
updates.stale = now;
if (durations.expire !== undefined) {
updates.expired = now + durations.expire * 1000 // Convert seconds to ms
;
}
tagsManifest.set(tag, updates);
} else {
// Update expired field for immediate expiration (default behavior when no durations provided)
tagsManifest.set(tag, {
...existingEntry,
expired: now
});
}
}
}
};
}
//# sourceMappingURL=default.js.map