Spaces:
Running
Running
File size: 15,639 Bytes
c592d77 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 | "use strict";
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 |