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

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions packages/cloudflare/src/cacheability-probe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -314,7 +314,7 @@ export async function probeStagedWorkerCacheability(options: {
}
if (
result.version !== 1 ||
result.kind !== "app-page" ||
(result.kind !== "app-page" && result.kind !== "pages-page") ||
typeof result.pattern !== "string" ||
!result.pattern.startsWith("/") ||
!isProbeRouteState(result.state) ||
Expand All @@ -335,7 +335,7 @@ export async function probeStagedWorkerCacheability(options: {
if (result.state !== "static-candidate") continue;

const route: CacheabilityManifestRoute = {
kind: "app-page",
kind: result.kind,
pattern: result.pattern,
representation: identity.representation,
requestKey: identity.requestKey,
Expand Down
92 changes: 70 additions & 22 deletions packages/cloudflare/src/cdn-warm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ import { VINEXT_CDN_BUILD_ID_HEADER } from "./cache/cdn-build-id.js";
export type CdnWarmOptions = {
targetUrl: string;
paths: readonly string[];
/** Pages Router JSON data identities used by client navigation. */
pagesDataPaths?: readonly string[];
/** App Router ISR paths whose definitive client-navigation payload is warmed. */
rscPaths?: readonly string[];
/** App Router paths whose deterministic loading-boundary payload is warmed. */
Expand Down Expand Up @@ -79,6 +81,7 @@ export type CdnWarmResult = {

export type CdnWarmRequestPlan = {
loadingShellPaths: string[];
pagesDataPaths: string[];
paths: string[];
rscPaths: string[];
};
Expand All @@ -91,6 +94,8 @@ export type PrerenderWarmPlan = {
buildIdentity?: string;
deploymentId?: string;
loadingShellPaths: string[];
pagesDataPaths?: string[];
pagesPaths?: string[];
paths: string[];
rscBuildId?: string;
rscPaths: string[];
Expand Down Expand Up @@ -136,6 +141,9 @@ function readPrerenderPathManifest(manifestPath: string): PrerenderPathManifest
(manifest.pagesPaths !== undefined &&
(!Array.isArray(manifest.pagesPaths) ||
!manifest.pagesPaths.every((pathname) => typeof pathname === "string"))) ||
(manifest.pagesDataPaths !== undefined &&
(!Array.isArray(manifest.pagesDataPaths) ||
!manifest.pagesDataPaths.every((pathname) => typeof pathname === "string"))) ||
(manifest.excludedWarmPaths !== undefined &&
(!Array.isArray(manifest.excludedWarmPaths) ||
!manifest.excludedWarmPaths.every((pathname) => typeof pathname === "string"))) ||
Expand Down Expand Up @@ -241,6 +249,8 @@ export function readPrerenderWarmPlan(
loadingShellPaths: supportsCanonicalRsc
? (manifest.loadingShellPaths ?? []).map(applyConfig)
: [],
...(manifest.pagesDataPaths ? { pagesDataPaths: manifest.pagesDataPaths } : {}),
...(manifest.pagesPaths ? { pagesPaths: manifest.pagesPaths.map(applyConfig) } : {}),
paths: htmlPaths,
...(supportsCanonicalRsc ? { rscBuildId: manifest.rscBuildId } : {}),
rscPaths: supportsCanonicalRsc ? manifest.rscPaths!.map(applyConfig) : [],
Expand Down Expand Up @@ -354,7 +364,7 @@ async function fetchHeadersWithTimeout(

export type CdnWarmTarget = {
headers?: HeadersInit;
kind: "html" | "rsc-full" | "rsc-loading-shell";
kind: "html" | "pages-data" | "rsc-full" | "rsc-loading-shell";
label: string;
pathname: string;
sourcePathname: string;
Expand All @@ -363,7 +373,7 @@ export type CdnWarmTarget = {
export async function createCdnWarmTargets(
options: Pick<
CdnWarmOptions,
"deploymentId" | "headers" | "loadingShellPaths" | "paths" | "rscPaths"
"deploymentId" | "headers" | "loadingShellPaths" | "pagesDataPaths" | "paths" | "rscPaths"
>,
): Promise<CdnWarmTarget[]> {
const requests: CdnWarmTarget[] = [];
Expand Down Expand Up @@ -413,6 +423,17 @@ export async function createCdnWarmTargets(
sourcePathname: pathname,
});
}
for (const pathname of new Set(options.pagesDataPaths ?? [])) {
const dataHeaders = new Headers(commonHeaders);
dataHeaders.set("Accept", "application/json");
requests.push({
headers: dataHeaders,
kind: "pages-data",
label: `${pathname} (Pages data)`,
pathname,
sourcePathname: pathname,
});
}
return requests;
}

Expand Down Expand Up @@ -681,9 +702,22 @@ function validateHtmlWarmResponse(
return { outcome: "warmed" };
}

function validatePagesDataWarmResponse(
response: Response,
expectedBuildId?: string,
requireCacheHit = false,
): WarmValidation {
const validation = validateHtmlWarmResponse(response, expectedBuildId, requireCacheHit);
if (validation.outcome !== "warmed") return validation;
if (!response.headers.get("Content-Type")?.toLowerCase().startsWith("application/json")) {
return { outcome: "failed", error: "expected application/json response" };
}
return validation;
}

function validateReadinessResponse(
response: Response,
kind: "html" | "rsc",
kind: "html" | "pages-data" | "rsc",
expectedBuildId?: string,
expectedRscBuildId?: string,
): string | null {
Expand All @@ -698,13 +732,14 @@ function validateReadinessResponse(
}
if (response.redirected) return "redirected response";
if (response.status >= 500) return `HTTP ${response.status}`;
if (
response.status >= 200 &&
response.status < 300 &&
kind === "rsc" &&
!response.headers.get("Content-Type")?.toLowerCase().startsWith(VINEXT_RSC_CONTENT_TYPE)
) {
return `expected ${VINEXT_RSC_CONTENT_TYPE} response`;
if (response.status >= 200 && response.status < 300) {
const contentType = response.headers.get("Content-Type")?.toLowerCase();
if (kind === "rsc" && !contentType?.startsWith(VINEXT_RSC_CONTENT_TYPE)) {
return `expected ${VINEXT_RSC_CONTENT_TYPE} response`;
}
if (kind === "pages-data" && !contentType?.startsWith("application/json")) {
return "expected application/json response";
}
}
// Readiness proves only that version overrides consistently reach the
// uploaded build. The real warm pass validates status, representation, and
Expand Down Expand Up @@ -738,8 +773,9 @@ export async function waitForCdnWarmTargetReadiness(
): Promise<CdnWarmReadinessResult> {
const rscPath = options.plan.rscPaths[0] ?? options.plan.loadingShellPaths[0];
const htmlPath = options.plan.paths[0];
const kind = rscPath ? "rsc" : "html";
const pathname = rscPath ?? htmlPath;
const pagesDataPath = options.plan.pagesDataPaths[0];
const kind = rscPath ? "rsc" : htmlPath ? "html" : "pages-data";
const pathname = rscPath ?? htmlPath ?? pagesDataPath;
if (!pathname) return { ready: true };
if (options.expectedBuildId === undefined && options.expectedRscBuildId === undefined) {
return {
Expand All @@ -754,8 +790,10 @@ export async function waitForCdnWarmTargetReadiness(
for (const [name, value] of createCanonicalRscRequestHeaders(options.deploymentId)) {
headers.set(name, value);
}
} else {
} else if (kind === "html") {
headers.set("Accept", "text/html");
} else {
headers.set("Accept", "application/json");
}
headers.set("Cache-Control", "no-cache");
headers.set("Pragma", "no-cache");
Expand Down Expand Up @@ -869,7 +907,7 @@ function shouldRetryValidationFailure(
options.expectedBuildId === undefined
? null
: response.headers.get(VINEXT_CDN_BUILD_ID_HEADER) === options.expectedBuildId,
target.kind === "html" || options.expectedRscBuildId === undefined
!target.kind.startsWith("rsc-") || options.expectedRscBuildId === undefined
? null
: response.headers.get(VINEXT_RSC_BUILD_ID_HEADER) === options.expectedRscBuildId,
].filter((matches): matches is boolean => matches !== null);
Expand Down Expand Up @@ -964,7 +1002,7 @@ async function warmOnePath(
);
}

if (target.kind !== "html") {
if (target.kind.startsWith("rsc-")) {
const validation = validateRscWarmResponse(
response,
options.expectedBuildId,
Expand Down Expand Up @@ -992,11 +1030,14 @@ async function warmOnePath(
continue;
}

const validation = validateHtmlWarmResponse(
response,
options.expectedBuildId,
options.requireCacheHit,
);
const validation =
target.kind === "pages-data"
? validatePagesDataWarmResponse(
response,
options.expectedBuildId,
options.requireCacheHit,
)
: validateHtmlWarmResponse(response, options.expectedBuildId, options.requireCacheHit);
if (validation.outcome === "warmed") {
return { path: target.label, ok: true, skipped: false };
}
Expand Down Expand Up @@ -1080,8 +1121,8 @@ export async function warmCdnCache(options: CdnWarmOptions): Promise<CdnWarmResu
skipped: 0,
failed: 0,
failures: [],
warmedPlan: { loadingShellPaths: [], paths: [], rscPaths: [] },
retryPlan: { loadingShellPaths: [], paths: [], rscPaths: [] },
warmedPlan: { loadingShellPaths: [], pagesDataPaths: [], paths: [], rscPaths: [] },
retryPlan: { loadingShellPaths: [], pagesDataPaths: [], paths: [], rscPaths: [] },
};
}

Expand Down Expand Up @@ -1227,6 +1268,9 @@ export async function warmCdnCache(options: CdnWarmOptions): Promise<CdnWarmResu
loadingShellPaths: warmedRequests
.filter((target) => target.kind === "rsc-loading-shell")
.map((target) => target.sourcePathname),
pagesDataPaths: warmedRequests
.filter((target) => target.kind === "pages-data")
.map((target) => target.sourcePathname),
paths: warmedRequests
.filter((target) => target.kind === "html")
.map((target) => target.sourcePathname),
Expand All @@ -1238,6 +1282,9 @@ export async function warmCdnCache(options: CdnWarmOptions): Promise<CdnWarmResu
loadingShellPaths: failedRequests
.filter(({ target }) => target.kind === "rsc-loading-shell")
.map(({ target }) => target.sourcePathname),
pagesDataPaths: failedRequests
.filter(({ target }) => target.kind === "pages-data")
.map(({ target }) => target.sourcePathname),
paths: failedRequests
.filter(({ target }) => target.kind === "html")
.map(({ target }) => target.sourcePathname),
Expand Down Expand Up @@ -1267,6 +1314,7 @@ export async function warmCdnCacheFromPrerender(
const warmPlan = {
deploymentId: plan.deploymentId,
loadingShellPaths: plan.loadingShellPaths,
pagesDataPaths: plan.pagesDataPaths,
paths: plan.paths,
rscPaths: plan.rscPaths,
};
Expand Down
Loading
Loading