diff --git a/packages/vinext/src/entries/pages-server-entry.ts b/packages/vinext/src/entries/pages-server-entry.ts index 498f4cb4f..651086167 100644 --- a/packages/vinext/src/entries/pages-server-entry.ts +++ b/packages/vinext/src/entries/pages-server-entry.ts @@ -45,6 +45,10 @@ const _pagesNodeCompatPath = fileURLToPath( const _pagesApiRoutePath = fileURLToPath( new URL("../server/pages-api-route.js", import.meta.url), ).replace(/\\/g, "/"); +const _isrCachePath = fileURLToPath(new URL("../server/isr-cache.js", import.meta.url)).replace( + /\\/g, + "/", +); /** * Generate the virtual SSR server entry module. @@ -275,7 +279,7 @@ import { renderToReadableStream } from "react-dom/server.edge"; import { resetSSRHead, getSSRHeadHTML } from "next/head"; import { flushPreloads } from "next/dynamic"; import { setSSRContext, wrapWithRouterContext } from "next/router"; -import { getCacheHandler, _runWithCacheState } from "next/cache"; +import { _runWithCacheState } from "next/cache"; import { runWithPrivateCache } from "vinext/cache-runtime"; import { ensureFetchPatch, runWithFetchCache } from "vinext/fetch-cache"; import { runWithRequestContext as _runWithUnifiedCtx, createRequestContext as _createUnifiedCtx } from "vinext/unified-request-context"; @@ -294,6 +298,12 @@ import { reportRequestError as _reportRequestError } from "vinext/instrumentatio import { resolvePagesI18nRequest } from ${JSON.stringify(_pagesI18nPath)}; import { createPagesReqRes as __createPagesReqRes } from ${JSON.stringify(_pagesNodeCompatPath)}; import { handlePagesApiRoute as __handlePagesApiRoute } from ${JSON.stringify(_pagesApiRoutePath)}; +import { + isrGet as __sharedIsrGet, + isrSet as __sharedIsrSet, + isrCacheKey as __sharedIsrCacheKey, + triggerBackgroundRegeneration as __sharedTriggerBackgroundRegeneration, +} from ${JSON.stringify(_isrCachePath)}; import { resolvePagesPageData as __resolvePagesPageData } from ${JSON.stringify(_pagesPageDataPath)}; import { renderPagesPageResponse as __renderPagesPageResponse } from ${JSON.stringify(_pagesPageResponsePath)}; ${instrumentationImportCode} @@ -310,51 +320,17 @@ const buildId = ${buildIdJson}; // Full resolved config for production server (embedded at build time) export const vinextConfig = ${vinextConfigJson}; -// ISR cache helpers (inlined for the server entry) -async function isrGet(key) { - const handler = getCacheHandler(); - const result = await handler.get(key); - if (!result || !result.value) return null; - return { value: result, isStale: result.cacheState === "stale" }; +function isrGet(key) { + return __sharedIsrGet(key); } -async function isrSet(key, data, revalidateSeconds, tags) { - const handler = getCacheHandler(); - await handler.set(key, data, { revalidate: revalidateSeconds, tags: tags || [] }); +function isrSet(key, data, revalidateSeconds, tags) { + return __sharedIsrSet(key, data, revalidateSeconds, tags); } -const pendingRegenerations = new Map(); function triggerBackgroundRegeneration(key, renderFn) { - if (pendingRegenerations.has(key)) return; - const promise = renderFn() - .catch((err) => console.error("[vinext] ISR regen failed for " + key + ":", err)) - .finally(() => pendingRegenerations.delete(key)); - pendingRegenerations.set(key, promise); - // Register with the Workers ExecutionContext so the isolate is kept alive - // until the regeneration finishes, even after the Response has been sent. - const ctx = _getRequestExecutionContext(); - if (ctx && typeof ctx.waitUntil === "function") ctx.waitUntil(promise); -} - -function fnv1a64(input) { - let h1 = 0x811c9dc5; - for (let i = 0; i < input.length; i++) { - h1 ^= input.charCodeAt(i); - h1 = (h1 * 0x01000193) >>> 0; - } - let h2 = 0x050c5d1f; - for (let i = 0; i < input.length; i++) { - h2 ^= input.charCodeAt(i); - h2 = (h2 * 0x01000193) >>> 0; - } - return h1.toString(36) + h2.toString(36); + return __sharedTriggerBackgroundRegeneration(key, renderFn); } -// Keep prefix construction and hashing logic in sync with isrCacheKey() in server/isr-cache.ts. -// buildId is a top-level const in the generated entry (see "const buildId = ..." above). function isrCacheKey(router, pathname) { - const normalized = pathname === "/" ? "/" : pathname.replace(/\\/$/, ""); - const prefix = buildId ? router + ":" + buildId : router; - const key = prefix + ":" + normalized; - if (key.length <= 200) return key; - return prefix + ":__hash:" + fnv1a64(normalized); + return __sharedIsrCacheKey(router, pathname, buildId || undefined); } async function renderToStringAsync(element) { diff --git a/tests/__snapshots__/entry-templates.test.ts.snap b/tests/__snapshots__/entry-templates.test.ts.snap index 0d2b831ec..b830f13ee 100644 --- a/tests/__snapshots__/entry-templates.test.ts.snap +++ b/tests/__snapshots__/entry-templates.test.ts.snap @@ -13687,7 +13687,7 @@ import { renderToReadableStream } from "react-dom/server.edge"; import { resetSSRHead, getSSRHeadHTML } from "next/head"; import { flushPreloads } from "next/dynamic"; import { setSSRContext, wrapWithRouterContext } from "next/router"; -import { getCacheHandler, _runWithCacheState } from "next/cache"; +import { _runWithCacheState } from "next/cache"; import { runWithPrivateCache } from "vinext/cache-runtime"; import { ensureFetchPatch, runWithFetchCache } from "vinext/fetch-cache"; import { runWithRequestContext as _runWithUnifiedCtx, createRequestContext as _createUnifiedCtx } from "vinext/unified-request-context"; @@ -13706,6 +13706,12 @@ import { reportRequestError as _reportRequestError } from "vinext/instrumentatio import { resolvePagesI18nRequest } from "/packages/vinext/src/server/pages-i18n.js"; import { createPagesReqRes as __createPagesReqRes } from "/packages/vinext/src/server/pages-node-compat.js"; import { handlePagesApiRoute as __handlePagesApiRoute } from "/packages/vinext/src/server/pages-api-route.js"; +import { + isrGet as __sharedIsrGet, + isrSet as __sharedIsrSet, + isrCacheKey as __sharedIsrCacheKey, + triggerBackgroundRegeneration as __sharedTriggerBackgroundRegeneration, +} from "/packages/vinext/src/server/isr-cache.js"; import { resolvePagesPageData as __resolvePagesPageData } from "/packages/vinext/src/server/pages-page-data.js"; import { renderPagesPageResponse as __renderPagesPageResponse } from "/packages/vinext/src/server/pages-page-response.js"; import * as _instrumentation from "/tests/fixtures/pages-basic/instrumentation.ts"; @@ -13733,51 +13739,17 @@ const buildId = "test-build-id"; // Full resolved config for production server (embedded at build time) export const vinextConfig = {"basePath":"","trailingSlash":false,"redirects":[{"source":"/old-about","destination":"/about","permanent":true},{"source":"/repeat-redirect/:id","destination":"/docs/:id/:id","permanent":false},{"source":"/redirect-before-middleware-rewrite","destination":"/about","permanent":false},{"source":"/redirect-before-middleware-response","destination":"/about","permanent":false}],"rewrites":{"beforeFiles":[{"source":"/before-rewrite","destination":"/about"},{"source":"/repeat-rewrite/:id","destination":"/docs/:id/:id"},{"source":"/mw-gated-before","has":[{"type":"cookie","key":"mw-before-user"}],"destination":"/about"}],"afterFiles":[{"source":"/after-rewrite","destination":"/about"},{"source":"/mw-gated-rewrite","has":[{"type":"cookie","key":"mw-user"}],"destination":"/about"}],"fallback":[{"source":"/fallback-rewrite","destination":"/about"}]},"headers":[{"source":"/api/(.*)","headers":[{"key":"X-Custom-Header","value":"vinext"}]},{"source":"/about","has":[{"type":"cookie","key":"logged-in"}],"headers":[{"key":"X-Auth-Only-Header","value":"1"}]},{"source":"/about","missing":[{"type":"cookie","key":"logged-in"}],"headers":[{"key":"X-Guest-Only-Header","value":"1"}]},{"source":"/ssr","headers":[{"key":"Vary","value":"Accept-Language"}]},{"source":"/headers-before-middleware-rewrite","headers":[{"key":"X-Rewrite-Source-Header","value":"1"}]}],"i18n":null,"images":{}}; -// ISR cache helpers (inlined for the server entry) -async function isrGet(key) { - const handler = getCacheHandler(); - const result = await handler.get(key); - if (!result || !result.value) return null; - return { value: result, isStale: result.cacheState === "stale" }; +function isrGet(key) { + return __sharedIsrGet(key); } -async function isrSet(key, data, revalidateSeconds, tags) { - const handler = getCacheHandler(); - await handler.set(key, data, { revalidate: revalidateSeconds, tags: tags || [] }); +function isrSet(key, data, revalidateSeconds, tags) { + return __sharedIsrSet(key, data, revalidateSeconds, tags); } -const pendingRegenerations = new Map(); function triggerBackgroundRegeneration(key, renderFn) { - if (pendingRegenerations.has(key)) return; - const promise = renderFn() - .catch((err) => console.error("[vinext] ISR regen failed for " + key + ":", err)) - .finally(() => pendingRegenerations.delete(key)); - pendingRegenerations.set(key, promise); - // Register with the Workers ExecutionContext so the isolate is kept alive - // until the regeneration finishes, even after the Response has been sent. - const ctx = _getRequestExecutionContext(); - if (ctx && typeof ctx.waitUntil === "function") ctx.waitUntil(promise); + return __sharedTriggerBackgroundRegeneration(key, renderFn); } - -function fnv1a64(input) { - let h1 = 0x811c9dc5; - for (let i = 0; i < input.length; i++) { - h1 ^= input.charCodeAt(i); - h1 = (h1 * 0x01000193) >>> 0; - } - let h2 = 0x050c5d1f; - for (let i = 0; i < input.length; i++) { - h2 ^= input.charCodeAt(i); - h2 = (h2 * 0x01000193) >>> 0; - } - return h1.toString(36) + h2.toString(36); -} -// Keep prefix construction and hashing logic in sync with isrCacheKey() in server/isr-cache.ts. -// buildId is a top-level const in the generated entry (see "const buildId = ..." above). function isrCacheKey(router, pathname) { - const normalized = pathname === "/" ? "/" : pathname.replace(/\\/$/, ""); - const prefix = buildId ? router + ":" + buildId : router; - const key = prefix + ":" + normalized; - if (key.length <= 200) return key; - return prefix + ":__hash:" + fnv1a64(normalized); + return __sharedIsrCacheKey(router, pathname, buildId || undefined); } async function renderToStringAsync(element) { diff --git a/tests/entry-templates.test.ts b/tests/entry-templates.test.ts index ef81e4dae..3e1fd6e37 100644 --- a/tests/entry-templates.test.ts +++ b/tests/entry-templates.test.ts @@ -283,13 +283,16 @@ describe("Pages Router entry templates", () => { expect(stabilize(code)).toContain("trieMatch"); }); - it("server entry eagerly starts ISR regeneration before waitUntil registration", async () => { + it("server entry delegates Pages ISR cache plumbing to shared helpers", async () => { const code = await getVirtualModuleCode("virtual:vinext-server-entry"); - const renderFnCall = code.indexOf("const promise = renderFn()"); - const waitUntilCall = code.indexOf("ctx.waitUntil(promise)"); - - expect(renderFnCall).toBeGreaterThan(-1); - expect(waitUntilCall).toBeGreaterThan(renderFnCall); + const stableCode = stabilize(code); + + expect(stableCode).toContain('from "/packages/vinext/src/server/isr-cache.js";'); + expect(code).toContain("function isrGet(key) {"); + expect(code).toContain("return __sharedIsrGet(key);"); + expect(code).toContain("return __sharedTriggerBackgroundRegeneration(key, renderFn);"); + expect(code).not.toContain("const promise = renderFn()"); + expect(code).not.toContain("ctx.waitUntil(promise)"); }); it("server entry seeds the main Pages Router unified context with executionContext", async () => { @@ -331,8 +334,18 @@ describe("Pages Router entry templates", () => { const code = await getVirtualModuleCode("virtual:vinext-server-entry"); expect(code).toContain("resolvePagesPageData as __resolvePagesPageData"); + expect(code).toContain("isrGet as __sharedIsrGet"); + expect(code).toContain("isrSet as __sharedIsrSet"); + expect(code).toContain("isrCacheKey as __sharedIsrCacheKey"); + expect(code).toContain( + "triggerBackgroundRegeneration as __sharedTriggerBackgroundRegeneration", + ); expect(code).toContain("const pageDataResult = await __resolvePagesPageData({"); - expect(code).not.toContain("triggerBackgroundRegeneration(cacheKey, async function()"); + expect(code).toContain("return __sharedTriggerBackgroundRegeneration(key, renderFn);"); + expect(code).not.toContain("async function isrGet(key)"); + expect(code).not.toContain("async function isrSet(key, data, revalidateSeconds, tags)"); + expect(code).not.toContain("const pendingRegenerations = new Map();"); + expect(code).not.toContain("function fnv1a64(input)"); expect(code).not.toContain("const result = await pageModule.getServerSideProps(ctx);"); expect(code).not.toContain("const result = await pageModule.getStaticProps(ctx);"); });