Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 17 additions & 41 deletions packages/vinext/src/entries/pages-server-entry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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";
Expand All @@ -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}
Expand All @@ -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) {
Expand Down
54 changes: 13 additions & 41 deletions tests/__snapshots__/entry-templates.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -13706,6 +13706,12 @@ import { reportRequestError as _reportRequestError } from "vinext/instrumentatio
import { resolvePagesI18nRequest } from "<ROOT>/packages/vinext/src/server/pages-i18n.js";
import { createPagesReqRes as __createPagesReqRes } from "<ROOT>/packages/vinext/src/server/pages-node-compat.js";
import { handlePagesApiRoute as __handlePagesApiRoute } from "<ROOT>/packages/vinext/src/server/pages-api-route.js";
import {
isrGet as __sharedIsrGet,
isrSet as __sharedIsrSet,
isrCacheKey as __sharedIsrCacheKey,
triggerBackgroundRegeneration as __sharedTriggerBackgroundRegeneration,
} from "<ROOT>/packages/vinext/src/server/isr-cache.js";
import { resolvePagesPageData as __resolvePagesPageData } from "<ROOT>/packages/vinext/src/server/pages-page-data.js";
import { renderPagesPageResponse as __renderPagesPageResponse } from "<ROOT>/packages/vinext/src/server/pages-page-response.js";
import * as _instrumentation from "<ROOT>/tests/fixtures/pages-basic/instrumentation.ts";
Expand Down Expand Up @@ -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) {
Expand Down
27 changes: 20 additions & 7 deletions tests/entry-templates.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 "<ROOT>/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 () => {
Expand Down Expand Up @@ -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);");
});
Expand Down