Spaces:
Running
Running
| ; | |
| Object.defineProperty(exports, "__esModule", { | |
| value: true | |
| }); | |
| Object.defineProperty(exports, "default", { | |
| enumerable: true, | |
| get: function() { | |
| return ResponseCache; | |
| } | |
| }); | |
| 0 && __export(require("./types")); | |
| const _batcher = require("../../lib/batcher"); | |
| const _lrucache = require("../lib/lru-cache"); | |
| const _log = require("../../build/output/log"); | |
| const _scheduler = require("../../lib/scheduler"); | |
| const _utils = require("./utils"); | |
| _export_star(require("./types"), exports); | |
| function _export_star(from, to) { | |
| Object.keys(from).forEach(function(k) { | |
| if (k !== "default" && !Object.prototype.hasOwnProperty.call(to, k)) { | |
| Object.defineProperty(to, k, { | |
| enumerable: true, | |
| get: function() { | |
| return from[k]; | |
| } | |
| }); | |
| } | |
| }); | |
| return from; | |
| } | |
| /** | |
| * Parses an environment variable as a positive integer, returning the fallback | |
| * if the value is missing, not a number, or not positive. | |
| */ function parsePositiveInt(envValue, fallback) { | |
| if (!envValue) return fallback; | |
| const parsed = parseInt(envValue, 10); | |
| return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback; | |
| } | |
| /** | |
| * Default TTL (in milliseconds) for minimal mode response cache entries. | |
| * Used for cache hit validation as a fallback for providers that don't | |
| * send the x-invocation-id header yet. | |
| * | |
| * 10 seconds chosen because: | |
| * - Long enough to dedupe rapid successive requests (e.g., page + data) | |
| * - Short enough to not serve stale data across unrelated requests | |
| * | |
| * Can be configured via `NEXT_PRIVATE_RESPONSE_CACHE_TTL` environment variable. | |
| */ const DEFAULT_TTL_MS = parsePositiveInt(process.env.NEXT_PRIVATE_RESPONSE_CACHE_TTL, 10000); | |
| /** | |
| * Default maximum number of entries in the response cache. | |
| * Can be configured via `NEXT_PRIVATE_RESPONSE_CACHE_MAX_SIZE` environment variable. | |
| */ const DEFAULT_MAX_SIZE = parsePositiveInt(process.env.NEXT_PRIVATE_RESPONSE_CACHE_MAX_SIZE, 150); | |
| /** | |
| * Separator used in compound cache keys to join pathname and invocationID. | |
| * Using null byte (\0) since it cannot appear in valid URL paths or UUIDs. | |
| */ const KEY_SEPARATOR = '\0'; | |
| /** | |
| * Sentinel value used for TTL-based cache entries (when invocationID is undefined). | |
| * Chosen to be a clearly reserved marker for internal cache keys. | |
| */ const TTL_SENTINEL = '__ttl_sentinel__'; | |
| /** | |
| * Creates a compound cache key from pathname and invocationID. | |
| */ function createCacheKey(pathname, invocationID) { | |
| return `${pathname}${KEY_SEPARATOR}${invocationID ?? TTL_SENTINEL}`; | |
| } | |
| /** | |
| * Extracts the invocationID from a compound cache key. | |
| * Returns undefined if the key used TTL_SENTINEL. | |
| */ function extractInvocationID(compoundKey) { | |
| const separatorIndex = compoundKey.lastIndexOf(KEY_SEPARATOR); | |
| if (separatorIndex === -1) return undefined; | |
| const invocationID = compoundKey.slice(separatorIndex + 1); | |
| return invocationID === TTL_SENTINEL ? undefined : invocationID; | |
| } | |
| class ResponseCache { | |
| constructor(minimal_mode, maxSize = DEFAULT_MAX_SIZE, ttl = DEFAULT_TTL_MS){ | |
| this.getBatcher = _batcher.Batcher.create({ | |
| // Ensure on-demand revalidate doesn't block normal requests, it should be | |
| // safe to run an on-demand revalidate for the same key as a normal request. | |
| cacheKeyFn: ({ key, isOnDemandRevalidate })=>`${key}-${isOnDemandRevalidate ? '1' : '0'}`, | |
| // We wait to do any async work until after we've added our promise to | |
| // `pendingResponses` to ensure that any any other calls will reuse the | |
| // same promise until we've fully finished our work. | |
| schedulerFn: _scheduler.scheduleOnNextTick | |
| }); | |
| this.revalidateBatcher = _batcher.Batcher.create({ | |
| // We wait to do any async work until after we've added our promise to | |
| // `pendingResponses` to ensure that any any other calls will reuse the | |
| // same promise until we've fully finished our work. | |
| schedulerFn: _scheduler.scheduleOnNextTick | |
| }); | |
| /** | |
| * Set of invocation IDs that have had cache entries evicted. | |
| * Used to detect when the cache size may be too small. | |
| * Bounded to prevent memory growth. | |
| */ this.evictedInvocationIDs = new Set(); | |
| this.minimal_mode = minimal_mode; | |
| this.maxSize = maxSize; | |
| this.ttl = ttl; | |
| // Create the LRU cache with eviction tracking | |
| this.cache = new _lrucache.LRUCache(maxSize, undefined, (compoundKey)=>{ | |
| const invocationID = extractInvocationID(compoundKey); | |
| if (invocationID) { | |
| // Bound to 100 entries to prevent unbounded memory growth. | |
| // FIFO eviction is acceptable here because: | |
| // 1. Invocations are short-lived (single request lifecycle), so older | |
| // invocations are unlikely to still be active after 100 newer ones | |
| // 2. This warning mechanism is best-effort for developer guidance— | |
| // missing occasional eviction warnings doesn't affect correctness | |
| // 3. If a long-running invocation is somehow evicted and then has | |
| // another cache entry evicted, it will simply be re-added | |
| if (this.evictedInvocationIDs.size >= 100) { | |
| const first = this.evictedInvocationIDs.values().next().value; | |
| if (first) this.evictedInvocationIDs.delete(first); | |
| } | |
| this.evictedInvocationIDs.add(invocationID); | |
| } | |
| }); | |
| } | |
| /** | |
| * Gets the response cache entry for the given key. | |
| * | |
| * @param key - The key to get the response cache entry for. | |
| * @param responseGenerator - The response generator to use to generate the response cache entry. | |
| * @param context - The context for the get request. | |
| * @returns The response cache entry. | |
| */ async get(key, responseGenerator, context) { | |
| // If there is no key for the cache, we can't possibly look this up in the | |
| // cache so just return the result of the response generator. | |
| if (!key) { | |
| return responseGenerator({ | |
| hasResolved: false, | |
| previousCacheEntry: null | |
| }); | |
| } | |
| // Check minimal mode cache before doing any other work. | |
| if (this.minimal_mode) { | |
| const cacheKey = createCacheKey(key, context.invocationID); | |
| const cachedItem = this.cache.get(cacheKey); | |
| if (cachedItem) { | |
| // With invocationID: exact match found - always a hit | |
| // With TTL mode: must check expiration | |
| if (context.invocationID !== undefined) { | |
| return (0, _utils.toResponseCacheEntry)(cachedItem.entry); | |
| } | |
| // TTL mode: check expiration | |
| const now = Date.now(); | |
| if (cachedItem.expiresAt > now) { | |
| return (0, _utils.toResponseCacheEntry)(cachedItem.entry); | |
| } | |
| // TTL expired - clean up | |
| this.cache.remove(cacheKey); | |
| } | |
| // Warn if this invocation had entries evicted - indicates cache may be too small. | |
| if (context.invocationID && this.evictedInvocationIDs.has(context.invocationID)) { | |
| (0, _log.warnOnce)(`Response cache entry was evicted for invocation ${context.invocationID}. ` + `Consider increasing NEXT_PRIVATE_RESPONSE_CACHE_MAX_SIZE (current: ${this.maxSize}).`); | |
| } | |
| } | |
| const { incrementalCache, isOnDemandRevalidate = false, isFallback = false, isRoutePPREnabled = false, isPrefetch = false, waitUntil, routeKind, invocationID } = context; | |
| const response = await this.getBatcher.batch({ | |
| key, | |
| isOnDemandRevalidate | |
| }, ({ resolve })=>{ | |
| const promise = this.handleGet(key, responseGenerator, { | |
| incrementalCache, | |
| isOnDemandRevalidate, | |
| isFallback, | |
| isRoutePPREnabled, | |
| isPrefetch, | |
| routeKind, | |
| invocationID | |
| }, resolve); | |
| // We need to ensure background revalidates are passed to waitUntil. | |
| if (waitUntil) waitUntil(promise); | |
| return promise; | |
| }); | |
| return (0, _utils.toResponseCacheEntry)(response); | |
| } | |
| /** | |
| * Handles the get request for the response cache. | |
| * | |
| * @param key - The key to get the response cache entry for. | |
| * @param responseGenerator - The response generator to use to generate the response cache entry. | |
| * @param context - The context for the get request. | |
| * @param resolve - The resolve function to use to resolve the response cache entry. | |
| * @returns The response cache entry. | |
| */ async handleGet(key, responseGenerator, context, resolve) { | |
| let previousIncrementalCacheEntry = null; | |
| let resolved = false; | |
| try { | |
| // Get the previous cache entry if not in minimal mode | |
| previousIncrementalCacheEntry = !this.minimal_mode ? await context.incrementalCache.get(key, { | |
| kind: (0, _utils.routeKindToIncrementalCacheKind)(context.routeKind), | |
| isRoutePPREnabled: context.isRoutePPREnabled, | |
| isFallback: context.isFallback | |
| }) : null; | |
| if (previousIncrementalCacheEntry && !context.isOnDemandRevalidate) { | |
| resolve(previousIncrementalCacheEntry); | |
| resolved = true; | |
| if (!previousIncrementalCacheEntry.isStale || context.isPrefetch) { | |
| // The cached value is still valid, so we don't need to update it yet. | |
| return previousIncrementalCacheEntry; | |
| } | |
| } | |
| // Revalidate the cache entry | |
| const incrementalResponseCacheEntry = await this.revalidate(key, context.incrementalCache, context.isRoutePPREnabled, context.isFallback, responseGenerator, previousIncrementalCacheEntry, previousIncrementalCacheEntry !== null && !context.isOnDemandRevalidate, undefined, context.invocationID); | |
| // Handle null response | |
| if (!incrementalResponseCacheEntry) { | |
| // Remove the cache item if it was set so we don't use it again. | |
| if (this.minimal_mode) { | |
| const cacheKey = createCacheKey(key, context.invocationID); | |
| this.cache.remove(cacheKey); | |
| } | |
| return null; | |
| } | |
| // Resolve for on-demand revalidation or if not already resolved | |
| if (context.isOnDemandRevalidate && !resolved) { | |
| return incrementalResponseCacheEntry; | |
| } | |
| return incrementalResponseCacheEntry; | |
| } catch (err) { | |
| // If we've already resolved the cache entry, we can't reject as we | |
| // already resolved the cache entry so log the error here. | |
| if (resolved) { | |
| console.error(err); | |
| return null; | |
| } | |
| throw err; | |
| } | |
| } | |
| /** | |
| * Revalidates the cache entry for the given key. | |
| * | |
| * @param key - The key to revalidate the cache entry for. | |
| * @param incrementalCache - The incremental cache to use to revalidate the cache entry. | |
| * @param isRoutePPREnabled - Whether the route is PPR enabled. | |
| * @param isFallback - Whether the route is a fallback. | |
| * @param responseGenerator - The response generator to use to generate the response cache entry. | |
| * @param previousIncrementalCacheEntry - The previous cache entry to use to revalidate the cache entry. | |
| * @param hasResolved - Whether the response has been resolved. | |
| * @param waitUntil - Optional function to register background work. | |
| * @param invocationID - The invocation ID for cache key scoping. | |
| * @returns The revalidated cache entry. | |
| */ async revalidate(key, incrementalCache, isRoutePPREnabled, isFallback, responseGenerator, previousIncrementalCacheEntry, hasResolved, waitUntil, invocationID) { | |
| return this.revalidateBatcher.batch(key, ()=>{ | |
| const promise = this.handleRevalidate(key, incrementalCache, isRoutePPREnabled, isFallback, responseGenerator, previousIncrementalCacheEntry, hasResolved, invocationID); | |
| // We need to ensure background revalidates are passed to waitUntil. | |
| if (waitUntil) waitUntil(promise); | |
| return promise; | |
| }); | |
| } | |
| async handleRevalidate(key, incrementalCache, isRoutePPREnabled, isFallback, responseGenerator, previousIncrementalCacheEntry, hasResolved, invocationID) { | |
| try { | |
| // Generate the response cache entry using the response generator. | |
| const responseCacheEntry = await responseGenerator({ | |
| hasResolved, | |
| previousCacheEntry: previousIncrementalCacheEntry, | |
| isRevalidating: true | |
| }); | |
| if (!responseCacheEntry) { | |
| return null; | |
| } | |
| // Convert the response cache entry to an incremental response cache entry. | |
| const incrementalResponseCacheEntry = await (0, _utils.fromResponseCacheEntry)({ | |
| ...responseCacheEntry, | |
| isMiss: !previousIncrementalCacheEntry | |
| }); | |
| // We want to persist the result only if it has a cache control value | |
| // defined. | |
| if (incrementalResponseCacheEntry.cacheControl) { | |
| if (this.minimal_mode) { | |
| // Set TTL expiration for cache hit validation. Entries are validated | |
| // by invocationID when available, with TTL as a fallback for providers | |
| // that don't send x-invocation-id. Memory is managed by LRU eviction. | |
| const cacheKey = createCacheKey(key, invocationID); | |
| this.cache.set(cacheKey, { | |
| entry: incrementalResponseCacheEntry, | |
| expiresAt: Date.now() + this.ttl | |
| }); | |
| } else { | |
| await incrementalCache.set(key, incrementalResponseCacheEntry.value, { | |
| cacheControl: incrementalResponseCacheEntry.cacheControl, | |
| isRoutePPREnabled, | |
| isFallback | |
| }); | |
| } | |
| } | |
| return incrementalResponseCacheEntry; | |
| } catch (err) { | |
| // When a path is erroring we automatically re-set the existing cache | |
| // with new revalidate and expire times to prevent non-stop retrying. | |
| if (previousIncrementalCacheEntry == null ? void 0 : previousIncrementalCacheEntry.cacheControl) { | |
| const revalidate = Math.min(Math.max(previousIncrementalCacheEntry.cacheControl.revalidate || 3, 3), 30); | |
| const expire = previousIncrementalCacheEntry.cacheControl.expire === undefined ? undefined : Math.max(revalidate + 3, previousIncrementalCacheEntry.cacheControl.expire); | |
| await incrementalCache.set(key, previousIncrementalCacheEntry.value, { | |
| cacheControl: { | |
| revalidate: revalidate, | |
| expire: expire | |
| }, | |
| isRoutePPREnabled, | |
| isFallback | |
| }); | |
| } | |
| // We haven't resolved yet, so let's throw to indicate an error. | |
| throw err; | |
| } | |
| } | |
| } | |
| //# sourceMappingURL=index.js.map |