UNPKG

next

Version:

The React Framework

186 lines (185 loc) • 9 kB
/** * 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