Last active
March 5, 2025 13:11
-
-
Save minibox24/b2dc210086d4ff8d1b1f9984835354a8 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
diff --git a/dist/server/response-cache/index.js b/dist/server/response-cache/index.js | |
index 829fb2785144f8d7fcd9cfa98af8c3f6e7fd6188..9645b592f849288b0fe3e31b6ee84619a36e4bde 100644 | |
--- a/dist/server/response-cache/index.js | |
+++ b/dist/server/response-cache/index.js | |
@@ -1,12 +1,12 @@ | |
"use strict"; | |
Object.defineProperty(exports, "__esModule", { | |
- value: true | |
+ value: true, | |
}); | |
Object.defineProperty(exports, "default", { | |
- enumerable: true, | |
- get: function() { | |
- return ResponseCache; | |
- } | |
+ enumerable: true, | |
+ get: function () { | |
+ return ResponseCache; | |
+ }, | |
}); | |
0 && __export(require("./types")); | |
const _types = _export_star(require("./types"), exports); | |
@@ -14,147 +14,347 @@ const _batcher = require("../../lib/batcher"); | |
const _scheduler = require("../../lib/scheduler"); | |
const _utils = require("./utils"); | |
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; | |
+ 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; | |
} | |
class ResponseCache { | |
- constructor(minimalMode){ | |
- this.batcher = _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 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; | |
+ constructor(minimalMode) { | |
+ this.batcher = _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 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; | |
+ } | |
+ 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, | |
+ }); | |
} | |
- 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 | |
- }); | |
+ const { | |
+ incrementalCache, | |
+ isOnDemandRevalidate = false, | |
+ isFallback = false, | |
+ isRoutePPREnabled = false, | |
+ } = context; | |
+ | |
+ // Check if incrementalCache exists and has required methods | |
+ if (!incrementalCache) { | |
+ console.error("[CACHE-DEBUG] incrementalCache is missing in context!"); | |
+ } | |
+ | |
+ const response = await this.batcher.batch( | |
+ { | |
+ key, | |
+ isOnDemandRevalidate, | |
+ }, | |
+ async (cacheKey, resolve) => { | |
+ var _this_previousCacheItem; | |
+ // We keep the previous cache entry around to leverage when the | |
+ // incremental cache is disabled in minimal mode. | |
+ if ( | |
+ this.minimalMode && | |
+ ((_this_previousCacheItem = this.previousCacheItem) == null | |
+ ? void 0 | |
+ : _this_previousCacheItem.key) === cacheKey && | |
+ this.previousCacheItem.expiresAt > Date.now() | |
+ ) { | |
+ return this.previousCacheItem.entry; | |
} | |
- const { incrementalCache, isOnDemandRevalidate = false, isFallback = false, isRoutePPREnabled = false } = context; | |
- const response = await this.batcher.batch({ | |
- key, | |
- isOnDemandRevalidate | |
- }, async (cacheKey, resolve)=>{ | |
- var _this_previousCacheItem; | |
- // We keep the previous cache entry around to leverage when the | |
- // incremental cache is disabled in minimal mode. | |
- if (this.minimalMode && ((_this_previousCacheItem = this.previousCacheItem) == null ? void 0 : _this_previousCacheItem.key) === cacheKey && this.previousCacheItem.expiresAt > Date.now()) { | |
- return this.previousCacheItem.entry; | |
+ // Coerce the kindHint into a given kind for the incremental cache. | |
+ const kind = (0, _utils.routeKindToIncrementalCacheKind)( | |
+ context.routeKind | |
+ ); | |
+ | |
+ let resolved = false; | |
+ let cachedResponse = null; | |
+ try { | |
+ cachedResponse = !this.minimalMode | |
+ ? await incrementalCache.get(key, { | |
+ kind, | |
+ isRoutePPREnabled: context.isRoutePPREnabled, | |
+ isFallback, | |
+ }) | |
+ : null; | |
+ | |
+ if (cachedResponse && !isOnDemandRevalidate) { | |
+ var _cachedResponse_value; | |
+ if ( | |
+ ((_cachedResponse_value = cachedResponse.value) == null | |
+ ? void 0 | |
+ : _cachedResponse_value.kind) === _types.CachedRouteKind.FETCH | |
+ ) { | |
+ console.error( | |
+ "[CACHE-DEBUG] Unexpected cachedResponse of kind fetch" | |
+ ); | |
+ throw new Error( | |
+ `invariant: unexpected cachedResponse of kind fetch in response cache` | |
+ ); | |
} | |
- // Coerce the kindHint into a given kind for the incremental cache. | |
- const kind = (0, _utils.routeKindToIncrementalCacheKind)(context.routeKind); | |
- let resolved = false; | |
- let cachedResponse = null; | |
- try { | |
- cachedResponse = !this.minimalMode ? await incrementalCache.get(key, { | |
- kind, | |
- isRoutePPREnabled: context.isRoutePPREnabled, | |
- isFallback | |
- }) : null; | |
- if (cachedResponse && !isOnDemandRevalidate) { | |
- var _cachedResponse_value; | |
- if (((_cachedResponse_value = cachedResponse.value) == null ? void 0 : _cachedResponse_value.kind) === _types.CachedRouteKind.FETCH) { | |
- throw new Error(`invariant: unexpected cachedResponse of kind fetch in response cache`); | |
- } | |
- resolve({ | |
- ...cachedResponse, | |
- revalidate: cachedResponse.curRevalidate | |
- }); | |
- resolved = true; | |
- if (!cachedResponse.isStale || context.isPrefetch) { | |
- // The cached value is still valid, so we don't need | |
- // to update it yet. | |
- return null; | |
- } | |
- } | |
- const cacheEntry = await responseGenerator({ | |
- hasResolved: resolved, | |
- previousCacheEntry: cachedResponse, | |
- isRevalidating: true | |
- }); | |
- // If the cache entry couldn't be generated, we don't want to cache | |
- // the result. | |
- if (!cacheEntry) { | |
- // Unset the previous cache item if it was set. | |
- if (this.minimalMode) this.previousCacheItem = undefined; | |
- return null; | |
- } | |
- const resolveValue = await (0, _utils.fromResponseCacheEntry)({ | |
- ...cacheEntry, | |
- isMiss: !cachedResponse | |
+ resolve({ | |
+ ...cachedResponse, | |
+ revalidate: cachedResponse.curRevalidate, | |
+ }); | |
+ resolved = true; | |
+ | |
+ // CLOUDFLARE PATCH: Always check if cache is stale, ignoring isPrefetch | |
+ if (cachedResponse.isStale) { | |
+ try { | |
+ // Generate new data immediately with a timeout | |
+ | |
+ // Create a promise that resolves immediately with the cached data | |
+ // This ensures we have something to work with even if responseGenerator is interrupted | |
+ const fallbackPromise = Promise.resolve({ | |
+ value: cachedResponse.value, | |
+ revalidate: cachedResponse.curRevalidate || 5, | |
}); | |
- if (!resolveValue) { | |
- // Unset the previous cache item if it was set. | |
- if (this.minimalMode) this.previousCacheItem = undefined; | |
- return null; | |
- } | |
- // For on-demand revalidate wait to resolve until cache is set. | |
- // Otherwise resolve now. | |
- if (!isOnDemandRevalidate && !resolved) { | |
- resolve(resolveValue); | |
- resolved = true; | |
- } | |
- // We want to persist the result only if it has a revalidate value | |
- // defined. | |
- if (typeof resolveValue.revalidate !== 'undefined') { | |
- if (this.minimalMode) { | |
- this.previousCacheItem = { | |
- key: cacheKey, | |
- entry: resolveValue, | |
- expiresAt: Date.now() + 1000 | |
- }; | |
- } else { | |
- await incrementalCache.set(key, resolveValue.value, { | |
- revalidate: resolveValue.revalidate, | |
- isRoutePPREnabled, | |
- isFallback | |
- }); | |
- } | |
+ | |
+ // Race between actual responseGenerator and fallback | |
+ const cacheEntryPromise = Promise.race([ | |
+ responseGenerator({ | |
+ hasResolved: true, | |
+ previousCacheEntry: cachedResponse, | |
+ isRevalidating: true, | |
+ }), | |
+ // Use a timeout to ensure we don't wait too long | |
+ new Promise((resolve) => | |
+ setTimeout(() => { | |
+ resolve(null); | |
+ }, 2000) | |
+ ), | |
+ ]); | |
+ | |
+ // Ensure the cache set operation runs with waitUntil | |
+ const waitUntilAvailable = | |
+ !!globalThis.__openNextAls?.getStore()?.waitUntil; | |
+ if (waitUntilAvailable) { | |
+ globalThis.__openNextAls.getStore().waitUntil( | |
+ (async () => { | |
+ try { | |
+ const cacheEntry = await cacheEntryPromise; | |
+ | |
+ // If no entry was generated, use fallback | |
+ const entryToUse = | |
+ cacheEntry || (await fallbackPromise); | |
+ | |
+ if (entryToUse) { | |
+ const resolveValue = await (0, | |
+ _utils.fromResponseCacheEntry)({ | |
+ ...entryToUse, | |
+ isMiss: false, | |
+ }); | |
+ | |
+ if ( | |
+ resolveValue && | |
+ typeof resolveValue.revalidate !== "undefined" | |
+ ) { | |
+ await incrementalCache.set( | |
+ key, | |
+ resolveValue.value, | |
+ { | |
+ revalidate: resolveValue.revalidate, | |
+ isRoutePPREnabled, | |
+ isFallback, | |
+ } | |
+ ); | |
+ } | |
+ } | |
+ } catch (error) { | |
+ console.error( | |
+ "[CACHE-DEBUG] Error in waitUntil revalidation:", | |
+ error | |
+ ); | |
+ } | |
+ })() | |
+ ); | |
} | |
- return resolveValue; | |
- } catch (err) { | |
- // When a getStaticProps path is erroring we automatically re-set the | |
- // existing cache under a new expiration to prevent non-stop retrying. | |
- if (cachedResponse) { | |
- await incrementalCache.set(key, cachedResponse.value, { | |
- revalidate: Math.min(Math.max(cachedResponse.revalidate || 3, 3), 30), | |
- isRoutePPREnabled, | |
- isFallback | |
- }); | |
+ } catch (err) { | |
+ console.error( | |
+ "[CACHE-DEBUG] Error during forced revalidation:", | |
+ err | |
+ ); | |
+ } | |
+ } | |
+ | |
+ if (!cachedResponse.isStale || context.isPrefetch) { | |
+ // The cached value is still valid, so we don't need | |
+ // to update it yet. | |
+ return null; | |
+ } | |
+ | |
+ // CLOUDFLARE PATCH: Already handled stale cache above, return null | |
+ return null; | |
+ } | |
+ | |
+ const cacheEntry = await responseGenerator({ | |
+ hasResolved: resolved, | |
+ previousCacheEntry: cachedResponse, | |
+ isRevalidating: true, | |
+ }); | |
+ | |
+ // If the cache entry couldn't be generated, we don't want to cache | |
+ // the result. | |
+ if (!cacheEntry) { | |
+ // Unset the previous cache item if it was set. | |
+ if (this.minimalMode) this.previousCacheItem = undefined; | |
+ return null; | |
+ } | |
+ | |
+ const resolveValue = await (0, _utils.fromResponseCacheEntry)({ | |
+ ...cacheEntry, | |
+ isMiss: !cachedResponse, | |
+ }); | |
+ | |
+ if (!resolveValue) { | |
+ // Unset the previous cache item if it was set. | |
+ if (this.minimalMode) this.previousCacheItem = undefined; | |
+ return null; | |
+ } | |
+ | |
+ // For on-demand revalidate wait to resolve until cache is set. | |
+ // Otherwise resolve now. | |
+ if (!isOnDemandRevalidate && !resolved) { | |
+ resolve(resolveValue); | |
+ resolved = true; | |
+ } | |
+ | |
+ // We want to persist the result only if it has a revalidate value | |
+ // defined. | |
+ if (typeof resolveValue.revalidate !== "undefined") { | |
+ if (this.minimalMode) { | |
+ this.previousCacheItem = { | |
+ key: cacheKey, | |
+ entry: resolveValue, | |
+ expiresAt: Date.now() + 1000, | |
+ }; | |
+ } else { | |
+ try { | |
+ // CLOUDFLARE PATCH: Wrap incrementalCache.set with waitUntil | |
+ const setCachePromise = incrementalCache.set( | |
+ key, | |
+ resolveValue.value, | |
+ { | |
+ revalidate: resolveValue.revalidate, | |
+ isRoutePPREnabled, | |
+ isFallback, | |
+ } | |
+ ); | |
+ | |
+ // Use waitUntil if available in the Cloudflare Workers environment | |
+ const waitUntilAvailable = | |
+ !!globalThis.__openNextAls?.getStore()?.waitUntil; | |
+ | |
+ if (waitUntilAvailable) { | |
+ globalThis.__openNextAls.getStore().waitUntil( | |
+ setCachePromise.then( | |
+ () => {}, | |
+ (err) => | |
+ console.error( | |
+ "[CACHE-DEBUG] incrementalCache.set failed:", | |
+ err | |
+ ) | |
+ ) | |
+ ); | |
+ } else { | |
+ await setCachePromise; | |
} | |
- // While revalidating in the background we can't reject as we already | |
- // resolved the cache entry so log the error here. | |
- if (resolved) { | |
- console.error(err); | |
- return null; | |
+ } catch (err) { | |
+ console.error( | |
+ "[CACHE-DEBUG] Error during incrementalCache.set:", | |
+ err | |
+ ); | |
+ } | |
+ } | |
+ } | |
+ | |
+ return resolveValue; | |
+ } catch (err) { | |
+ console.error("[CACHE-DEBUG] Error in batch callback:", err); | |
+ | |
+ // When a getStaticProps path is erroring we automatically re-set the | |
+ // existing cache under a new expiration to prevent non-stop retrying. | |
+ if (cachedResponse) { | |
+ try { | |
+ // CLOUDFLARE PATCH: Wrap incrementalCache.set with waitUntil | |
+ const setCachePromise = incrementalCache.set( | |
+ key, | |
+ cachedResponse.value, | |
+ { | |
+ revalidate: Math.min( | |
+ Math.max(cachedResponse.revalidate || 3, 3), | |
+ 30 | |
+ ), | |
+ isRoutePPREnabled, | |
+ isFallback, | |
} | |
- // We haven't resolved yet, so let's throw to indicate an error. | |
- throw err; | |
+ ); | |
+ | |
+ // Use waitUntil if available in the Cloudflare Workers environment | |
+ const waitUntilAvailable = | |
+ !!globalThis.__openNextAls?.getStore()?.waitUntil; | |
+ | |
+ if (waitUntilAvailable) { | |
+ globalThis.__openNextAls.getStore().waitUntil( | |
+ setCachePromise.then( | |
+ () => {}, | |
+ (err) => | |
+ console.error( | |
+ "[CACHE-DEBUG] incrementalCache.set failed (error path):", | |
+ err | |
+ ) | |
+ ) | |
+ ); | |
+ } else { | |
+ await setCachePromise; | |
+ } | |
+ } catch (setCacheErr) { | |
+ console.error( | |
+ "[CACHE-DEBUG] Error during incrementalCache.set (error path):", | |
+ setCacheErr | |
+ ); | |
} | |
- }); | |
- return (0, _utils.toResponseCacheEntry)(response); | |
- } | |
+ } | |
+ | |
+ // While revalidating in the background we can't reject as we already | |
+ // resolved the cache entry so log the error here. | |
+ if (resolved) { | |
+ console.error( | |
+ "[CACHE-DEBUG] Already resolved, logging error:", | |
+ err | |
+ ); | |
+ return null; | |
+ } | |
+ | |
+ // We haven't resolved yet, so let's throw to indicate an error. | |
+ console.error("[CACHE-DEBUG] Not resolved yet, rethrowing error"); | |
+ throw err; | |
+ } | |
+ } | |
+ ); | |
+ | |
+ return (0, _utils.toResponseCacheEntry)(response); | |
+ } | |
} | |
//# sourceMappingURL=index.js.map |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
writing by ai
Fixing ISR in Cloudflare Workers with OpenNext(v0.5.8)
Problem
When deploying a Next.js application with Incremental Static Regeneration (ISR) to Cloudflare Workers using the OpenNext adapter, ISR doesn't work in production environments. Stale content is never revalidated, despite working correctly in development environments.
Root Cause
Two main issues prevent ISR from working properly in Cloudflare Workers:
Missing
waitUntil
for cache operations:waitUntil
incrementalCache.set
operations never complete in productionPrefetch detection prevents revalidation:
isPrefetch: true
) in productionisPrefetch
is true, Next.js skips revalidation for stale contentSolution
The solution involves modifying the Next.js
ResponseCache
implementation to:isPrefetch
flagincrementalCache.set
operations with Cloudflare'swaitUntil
Key part of the fix:
How to Apply
Locate the Next.js ResponseCache implementation in your build output
Apply the patch to handle stale cache revalidation regardless of isPrefetch
Wrap all incrementalCache.set calls with waitUntil when available
This solution has been tested and works correctly in both development and production Cloudflare Workers environments.
Technical Details
The patch forces immediate revalidation within the same request when stale content is detected
It uses Cloudflare's waitUntil API to ensure cache operations complete even after the response is sent
The modification is compatible with Next.js 14+
Feel free to use this patch until an official fix is available in the OpenNext Cloudflare adapter.