UNPKG

next

Version:

The React Framework

479 lines (478 loc) • 24.7 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); 0 && (module.exports = { CacheHandler: null, IncrementalCache: null }); function _export(target, all) { for(var name in all)Object.defineProperty(target, name, { enumerable: true, get: all[name] }); } _export(exports, { CacheHandler: function() { return CacheHandler; }, IncrementalCache: function() { return IncrementalCache; } }); const _responsecache = require("../../response-cache"); const _filesystemcache = /*#__PURE__*/ _interop_require_default(require("./file-system-cache")); const _normalizepagepath = require("../../../shared/lib/page-path/normalize-page-path"); const _constants = require("../../../lib/constants"); const _toroute = require("../to-route"); const _sharedcachecontrolsexternal = require("./shared-cache-controls.external"); const _workunitasyncstorageexternal = require("../../app-render/work-unit-async-storage.external"); const _invarianterror = require("../../../shared/lib/invariant-error"); const _serverutils = require("../../server-utils"); const _workasyncstorageexternal = require("../../app-render/work-async-storage.external"); const _detachedpromise = require("../../../lib/detached-promise"); const _tagsmanifestexternal = require("./tags-manifest.external"); function _interop_require_default(obj) { return obj && obj.__esModule ? obj : { default: obj }; } class CacheHandler { // eslint-disable-next-line constructor(_ctx){} async get(_cacheKey, _ctx) { return {}; } async set(_cacheKey, _data, _ctx) {} async revalidateTag(_tags, _durations) {} resetRequestCache() {} } class IncrementalCache { static #_ = this.debug = !!process.env.NEXT_PRIVATE_DEBUG_CACHE; constructor({ fs, dev, flushToDisk, minimalMode, serverDistDir, requestHeaders, maxMemoryCacheSize, getPrerenderManifest, fetchCacheKeyPrefix, CurCacheHandler, allowedRevalidateHeaderKeys }){ var _this_prerenderManifest_preview, _this_prerenderManifest; this.locks = new Map(); this.hasCustomCacheHandler = Boolean(CurCacheHandler); const cacheHandlersSymbol = Symbol.for('@next/cache-handlers'); const _globalThis = globalThis; if (!CurCacheHandler) { // if we have a global cache handler available leverage it const globalCacheHandler = _globalThis[cacheHandlersSymbol]; if (globalCacheHandler == null ? void 0 : globalCacheHandler.FetchCache) { CurCacheHandler = globalCacheHandler.FetchCache; if (IncrementalCache.debug) { console.log('IncrementalCache: using global FetchCache cache handler'); } } else { if (fs && serverDistDir) { if (IncrementalCache.debug) { console.log('IncrementalCache: using filesystem cache handler'); } CurCacheHandler = _filesystemcache.default; } } } else if (IncrementalCache.debug) { console.log('IncrementalCache: using custom cache handler', CurCacheHandler.name); } if (process.env.__NEXT_TEST_MAX_ISR_CACHE) { // Allow cache size to be overridden for testing purposes maxMemoryCacheSize = parseInt(process.env.__NEXT_TEST_MAX_ISR_CACHE, 10); } this.dev = dev; this.disableForTestmode = process.env.NEXT_PRIVATE_TEST_PROXY === 'true'; // this is a hack to avoid Webpack knowing this is equal to this.minimalMode // because we replace this.minimalMode to true in production bundles. const minimalModeKey = 'minimalMode'; this[minimalModeKey] = minimalMode; this.requestHeaders = requestHeaders; this.allowedRevalidateHeaderKeys = allowedRevalidateHeaderKeys; this.prerenderManifest = getPrerenderManifest(); this.cacheControls = new _sharedcachecontrolsexternal.SharedCacheControls(this.prerenderManifest); this.fetchCacheKeyPrefix = fetchCacheKeyPrefix; let revalidatedTags = []; if (requestHeaders[_constants.PRERENDER_REVALIDATE_HEADER] === ((_this_prerenderManifest = this.prerenderManifest) == null ? void 0 : (_this_prerenderManifest_preview = _this_prerenderManifest.preview) == null ? void 0 : _this_prerenderManifest_preview.previewModeId)) { this.isOnDemandRevalidate = true; } if (minimalMode) { var _this_prerenderManifest_preview1, _this_prerenderManifest1; revalidatedTags = this.revalidatedTags = (0, _serverutils.getPreviouslyRevalidatedTags)(requestHeaders, (_this_prerenderManifest1 = this.prerenderManifest) == null ? void 0 : (_this_prerenderManifest_preview1 = _this_prerenderManifest1.preview) == null ? void 0 : _this_prerenderManifest_preview1.previewModeId); } if (CurCacheHandler) { this.cacheHandler = new CurCacheHandler({ dev, fs, flushToDisk, serverDistDir, revalidatedTags, maxMemoryCacheSize, _requestHeaders: requestHeaders, fetchCacheKeyPrefix }); } } calculateRevalidate(pathname, fromTime, dev, isFallback) { // in development we don't have a prerender-manifest // and default to always revalidating to allow easier debugging if (dev) return Math.floor(performance.timeOrigin + performance.now() - 1000); const cacheControl = this.cacheControls.get((0, _toroute.toRoute)(pathname)); // if an entry isn't present in routes we fallback to a default // of revalidating after 1 second unless it's a fallback request. const initialRevalidateSeconds = cacheControl ? cacheControl.revalidate : isFallback ? false : 1; const revalidateAfter = typeof initialRevalidateSeconds === 'number' ? initialRevalidateSeconds * 1000 + fromTime : initialRevalidateSeconds; return revalidateAfter; } _getPathname(pathname, fetchCache) { return fetchCache ? pathname : (0, _normalizepagepath.normalizePagePath)(pathname); } resetRequestCache() { var _this_cacheHandler_resetRequestCache, _this_cacheHandler; (_this_cacheHandler = this.cacheHandler) == null ? void 0 : (_this_cacheHandler_resetRequestCache = _this_cacheHandler.resetRequestCache) == null ? void 0 : _this_cacheHandler_resetRequestCache.call(_this_cacheHandler); } async lock(cacheKey) { // Wait for any existing lock on this cache key to be released // This implements a simple queue-based locking mechanism while(true){ const lock = this.locks.get(cacheKey); if (IncrementalCache.debug) { console.log('IncrementalCache: lock get', cacheKey, !!lock); } // If no lock exists, we can proceed to acquire it if (!lock) break; // Wait for the existing lock to be released before trying again await lock; } // Create a new detached promise that will represent this lock // The resolve function (unlock) will be returned to the caller const { resolve, promise } = new _detachedpromise.DetachedPromise(); if (IncrementalCache.debug) { console.log('IncrementalCache: successfully locked', cacheKey); } // Store the lock promise in the locks map this.locks.set(cacheKey, promise); return ()=>{ // Resolve the promise to release the lock. resolve(); // Remove the lock from the map once it's released so that future gets // can acquire the lock. this.locks.delete(cacheKey); }; } async revalidateTag(tags, durations) { var _this_cacheHandler; return (_this_cacheHandler = this.cacheHandler) == null ? void 0 : _this_cacheHandler.revalidateTag(tags, durations); } // x-ref: https://github.com/facebook/react/blob/2655c9354d8e1c54ba888444220f63e836925caa/packages/react/src/ReactFetch.js#L23 async generateCacheKey(url, init = {}) { // this should be bumped anytime a fix is made to cache entries // that should bust the cache const MAIN_KEY_PREFIX = 'v3'; const bodyChunks = []; const encoder = new TextEncoder(); const decoder = new TextDecoder(); if (init.body) { // handle Uint8Array body if (init.body instanceof Uint8Array) { bodyChunks.push(decoder.decode(init.body)); init._ogBody = init.body; } else if (typeof init.body.getReader === 'function') { const readableBody = init.body; const chunks = []; try { await readableBody.pipeTo(new WritableStream({ write (chunk) { if (typeof chunk === 'string') { chunks.push(encoder.encode(chunk)); bodyChunks.push(chunk); } else { chunks.push(chunk); bodyChunks.push(decoder.decode(chunk, { stream: true })); } } })); // Flush the decoder. bodyChunks.push(decoder.decode()); // Create a new buffer with all the chunks. const length = chunks.reduce((total, arr)=>total + arr.length, 0); const arrayBuffer = new Uint8Array(length); // Push each of the chunks into the new array buffer. let offset = 0; for (const chunk of chunks){ arrayBuffer.set(chunk, offset); offset += chunk.length; } ; init._ogBody = arrayBuffer; } catch (err) { console.error('Problem reading body', err); } } else if (typeof init.body.keys === 'function') { const formData = init.body; init._ogBody = init.body; for (const key of new Set([ ...formData.keys() ])){ const values = formData.getAll(key); bodyChunks.push(`${key}=${(await Promise.all(values.map(async (val)=>{ if (typeof val === 'string') { return val; } else { return await val.text(); } }))).join(',')}`); } // handle blob body } else if (typeof init.body.arrayBuffer === 'function') { const blob = init.body; const arrayBuffer = await blob.arrayBuffer(); bodyChunks.push(await blob.text()); init._ogBody = new Blob([ arrayBuffer ], { type: blob.type }); } else if (typeof init.body === 'string') { bodyChunks.push(init.body); init._ogBody = init.body; } } const headers = typeof (init.headers || {}).keys === 'function' ? Object.fromEntries(init.headers) : Object.assign({}, init.headers); // w3c trace context headers can break request caching and deduplication // so we remove them from the cache key if ('traceparent' in headers) delete headers['traceparent']; if ('tracestate' in headers) delete headers['tracestate']; const cacheString = JSON.stringify([ MAIN_KEY_PREFIX, this.fetchCacheKeyPrefix || '', url, init.method, headers, init.mode, init.redirect, init.credentials, init.referrer, init.referrerPolicy, init.integrity, init.cache, bodyChunks ]); if (process.env.NEXT_RUNTIME === 'edge') { function bufferToHex(buffer) { return Array.prototype.map.call(new Uint8Array(buffer), (b)=>b.toString(16).padStart(2, '0')).join(''); } const buffer = encoder.encode(cacheString); return bufferToHex(await crypto.subtle.digest('SHA-256', buffer)); } else { const crypto1 = require('crypto'); return crypto1.createHash('sha256').update(cacheString).digest('hex'); } } async get(cacheKey, ctx) { var _this_cacheHandler, _cacheData_value; // Unlike other caches if we have a resume data cache, we use it even if // testmode would normally disable it or if requestHeaders say 'no-cache'. if (ctx.kind === _responsecache.IncrementalCacheKind.FETCH) { const workUnitStore = _workunitasyncstorageexternal.workUnitAsyncStorage.getStore(); const resumeDataCache = workUnitStore ? (0, _workunitasyncstorageexternal.getRenderResumeDataCache)(workUnitStore) : null; if (resumeDataCache) { const memoryCacheData = resumeDataCache.fetch.get(cacheKey); if ((memoryCacheData == null ? void 0 : memoryCacheData.kind) === _responsecache.CachedRouteKind.FETCH) { if (IncrementalCache.debug) { console.log('IncrementalCache: rdc:hit', cacheKey); } return { isStale: false, value: memoryCacheData }; } else if (IncrementalCache.debug) { console.log('IncrementalCache: rdc:miss', cacheKey); } } else { if (IncrementalCache.debug) { console.log('IncrementalCache: rdc:no-resume-data'); } } } // we don't leverage the prerender cache in dev mode // so that getStaticProps is always called for easier debugging if (this.disableForTestmode || this.dev && (ctx.kind !== _responsecache.IncrementalCacheKind.FETCH || this.requestHeaders['cache-control'] === 'no-cache')) { return null; } cacheKey = this._getPathname(cacheKey, ctx.kind === _responsecache.IncrementalCacheKind.FETCH); const cacheData = await ((_this_cacheHandler = this.cacheHandler) == null ? void 0 : _this_cacheHandler.get(cacheKey, ctx)); if (ctx.kind === _responsecache.IncrementalCacheKind.FETCH) { var _cacheData_value1; if (!cacheData) { return null; } if (((_cacheData_value1 = cacheData.value) == null ? void 0 : _cacheData_value1.kind) !== _responsecache.CachedRouteKind.FETCH) { var _cacheData_value2; throw Object.defineProperty(new _invarianterror.InvariantError(`Expected cached value for cache key ${JSON.stringify(cacheKey)} to be a "FETCH" kind, got ${JSON.stringify((_cacheData_value2 = cacheData.value) == null ? void 0 : _cacheData_value2.kind)} instead.`), "__NEXT_ERROR_CODE", { value: "E653", enumerable: false, configurable: true }); } const workStore = _workasyncstorageexternal.workAsyncStorage.getStore(); const combinedTags = [ ...ctx.tags || [], ...ctx.softTags || [] ]; // if a tag was revalidated we don't return stale data if (combinedTags.some((tag)=>{ var _this_revalidatedTags, _workStore_pendingRevalidatedTags; return ((_this_revalidatedTags = this.revalidatedTags) == null ? void 0 : _this_revalidatedTags.includes(tag)) || (workStore == null ? void 0 : (_workStore_pendingRevalidatedTags = workStore.pendingRevalidatedTags) == null ? void 0 : _workStore_pendingRevalidatedTags.some((item)=>item.tag === tag)); })) { if (IncrementalCache.debug) { console.log('IncrementalCache: expired tag', cacheKey); } return null; } // As we're able to get the cache entry for this fetch, and the prerender // resume data cache (RDC) is available, it must have been populated by a // previous fetch, but was not yet present in the in-memory cache. This // could be the case when performing multiple renders in parallel during // build time where we de-duplicate the fetch calls. // // We add it to the RDC so that the next fetch call will be able to use it // and it won't have to reach into the fetch cache implementation. const workUnitStore = _workunitasyncstorageexternal.workUnitAsyncStorage.getStore(); if (workUnitStore) { const prerenderResumeDataCache = (0, _workunitasyncstorageexternal.getPrerenderResumeDataCache)(workUnitStore); if (prerenderResumeDataCache) { if (IncrementalCache.debug) { console.log('IncrementalCache: rdc:set', cacheKey); } prerenderResumeDataCache.fetch.set(cacheKey, cacheData.value); } } const revalidate = ctx.revalidate || cacheData.value.revalidate; const age = (performance.timeOrigin + performance.now() - (cacheData.lastModified || 0)) / 1000; let isStale = age > revalidate; const data = cacheData.value.data; if ((0, _tagsmanifestexternal.areTagsExpired)(combinedTags, cacheData.lastModified)) { return null; } else if ((0, _tagsmanifestexternal.areTagsStale)(combinedTags, cacheData.lastModified)) { isStale = true; } return { isStale, value: { kind: _responsecache.CachedRouteKind.FETCH, data, revalidate } }; } else if ((cacheData == null ? void 0 : (_cacheData_value = cacheData.value) == null ? void 0 : _cacheData_value.kind) === _responsecache.CachedRouteKind.FETCH) { throw Object.defineProperty(new _invarianterror.InvariantError(`Expected cached value for cache key ${JSON.stringify(cacheKey)} not to be a ${JSON.stringify(ctx.kind)} kind, got "FETCH" instead.`), "__NEXT_ERROR_CODE", { value: "E652", enumerable: false, configurable: true }); } let entry = null; const cacheControl = this.cacheControls.get((0, _toroute.toRoute)(cacheKey)); let isStale; let revalidateAfter; if ((cacheData == null ? void 0 : cacheData.lastModified) === -1) { isStale = -1; revalidateAfter = -1 * _constants.CACHE_ONE_YEAR; } else { var _cacheData_value3, _cacheData_value4; const now = performance.timeOrigin + performance.now(); const lastModified = (cacheData == null ? void 0 : cacheData.lastModified) || now; revalidateAfter = this.calculateRevalidate(cacheKey, lastModified, this.dev ?? false, ctx.isFallback); isStale = revalidateAfter !== false && revalidateAfter < now ? true : undefined; // If the stale time couldn't be determined based on the revalidation // time, we check if the tags are expired or stale. if (isStale === undefined && ((cacheData == null ? void 0 : (_cacheData_value3 = cacheData.value) == null ? void 0 : _cacheData_value3.kind) === _responsecache.CachedRouteKind.APP_PAGE || (cacheData == null ? void 0 : (_cacheData_value4 = cacheData.value) == null ? void 0 : _cacheData_value4.kind) === _responsecache.CachedRouteKind.APP_ROUTE)) { var _cacheData_value_headers; const tagsHeader = (_cacheData_value_headers = cacheData.value.headers) == null ? void 0 : _cacheData_value_headers[_constants.NEXT_CACHE_TAGS_HEADER]; if (typeof tagsHeader === 'string') { const cacheTags = tagsHeader.split(','); if (cacheTags.length > 0) { if ((0, _tagsmanifestexternal.areTagsExpired)(cacheTags, lastModified)) { isStale = -1; } else if ((0, _tagsmanifestexternal.areTagsStale)(cacheTags, lastModified)) { isStale = true; } } } } } if (cacheData) { entry = { isStale, cacheControl, revalidateAfter, value: cacheData.value }; } if (!cacheData && this.prerenderManifest.notFoundRoutes.includes(cacheKey)) { // for the first hit after starting the server the cache // may not have a way to save notFound: true so if // the prerender-manifest marks this as notFound then we // return that entry and trigger a cache set to give it a // chance to update in-memory entries entry = { isStale, value: null, cacheControl, revalidateAfter }; this.set(cacheKey, entry.value, { ...ctx, cacheControl }); } return entry; } async set(pathname, data, ctx) { // Even if we otherwise disable caching for testMode or if no fetchCache is // configured we still always stash results in the resume data cache if one // exists. This is because this is a transient in memory cache that // populates caches ahead of a dynamic render in dev mode to allow the RSC // debug info to have the right environment associated to it. if ((data == null ? void 0 : data.kind) === _responsecache.CachedRouteKind.FETCH) { const workUnitStore = _workunitasyncstorageexternal.workUnitAsyncStorage.getStore(); const prerenderResumeDataCache = workUnitStore ? (0, _workunitasyncstorageexternal.getPrerenderResumeDataCache)(workUnitStore) : null; if (prerenderResumeDataCache) { if (IncrementalCache.debug) { console.log('IncrementalCache: rdc:set', pathname); } prerenderResumeDataCache.fetch.set(pathname, data); } } if (this.disableForTestmode || this.dev && !ctx.fetchCache) return; pathname = this._getPathname(pathname, ctx.fetchCache); // FetchCache has upper limit of 2MB per-entry currently const itemSize = JSON.stringify(data).length; if (ctx.fetchCache && itemSize > 2 * 1024 * 1024 && // We ignore the size limit when custom cache handler is being used, as it // might not have this limit !this.hasCustomCacheHandler && // We also ignore the size limit when it's an implicit build-time-only // caching that the user isn't even aware of. !ctx.isImplicitBuildTimeCache) { const warningText = `Failed to set Next.js data cache for ${ctx.fetchUrl || pathname}, items over 2MB can not be cached (${itemSize} bytes)`; if (this.dev) { throw Object.defineProperty(new Error(warningText), "__NEXT_ERROR_CODE", { value: "E394", enumerable: false, configurable: true }); } console.warn(warningText); return; } try { var _this_cacheHandler; if (!ctx.fetchCache && ctx.cacheControl) { this.cacheControls.set((0, _toroute.toRoute)(pathname), ctx.cacheControl); } await ((_this_cacheHandler = this.cacheHandler) == null ? void 0 : _this_cacheHandler.set(pathname, data, ctx)); } catch (error) { console.warn('Failed to update prerender cache for', pathname, error); } } } //# sourceMappingURL=index.js.map