@vinext/cloudflare 1.0.0-beta.1 → 1.0.0-beta.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +94 -1
- package/dist/cache/cdn-adapter-config.d.ts +30 -0
- package/dist/cache/cdn-adapter-config.js +87 -0
- package/dist/cache/cdn-adapter.d.ts +47 -15
- package/dist/cache/cdn-adapter.js +49 -13
- package/dist/cache/cdn-adapter.runtime.d.ts +26 -4
- package/dist/cache/cdn-adapter.runtime.js +150 -37
- package/dist/cache/cdn-adapter.worker.d.ts +31 -0
- package/dist/cache/cdn-adapter.worker.js +373 -0
- package/dist/cache/cdn-build-id.d.ts +6 -0
- package/dist/cache/cdn-build-id.js +9 -0
- package/dist/cache/kv-data-adapter.d.ts +32 -8
- package/dist/cache/kv-data-adapter.js +5 -1
- package/dist/cache/kv-data-adapter.runtime.d.ts +45 -13
- package/dist/cache/kv-data-adapter.runtime.js +132 -52
- package/dist/cache/kv-key.d.ts +15 -0
- package/dist/cache/kv-key.js +43 -0
- package/dist/cache/response-store-adapter.d.ts +73 -0
- package/dist/cache/response-store-adapter.js +64 -0
- package/dist/cache/response-store-adapter.self-contained.worker.d.ts +18 -0
- package/dist/cache/response-store-adapter.self-contained.worker.js +12 -0
- package/dist/cache/response-store-adapter.service-binding.worker.d.ts +16 -0
- package/dist/cache/response-store-adapter.service-binding.worker.js +11 -0
- package/dist/cache/response-store-adapter.worker.d.ts +14 -0
- package/dist/cache/response-store-adapter.worker.js +278 -0
- package/dist/cache/response-store-cdn.runtime.d.ts +5 -0
- package/dist/cache/response-store-cdn.runtime.js +41 -0
- package/dist/cache/response-store-data.runtime.d.ts +34 -0
- package/dist/cache/response-store-data.runtime.js +300 -0
- package/dist/cacheability-artifact.d.ts +9 -0
- package/dist/cacheability-artifact.js +191 -0
- package/dist/cacheability-manifest-limits.d.ts +4 -0
- package/dist/cacheability-manifest-limits.js +7 -0
- package/dist/cacheability-probe.d.ts +49 -0
- package/dist/cacheability-probe.js +660 -0
- package/dist/cdn-warm.d.ts +110 -12
- package/dist/cdn-warm.js +880 -82
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +15 -1
- package/dist/deploy-config.d.ts +13 -11
- package/dist/deploy-config.js +21 -3
- package/dist/deploy-help.d.ts +3 -4
- package/dist/deploy-help.js +61 -18
- package/dist/deploy.d.ts +121 -41
- package/dist/deploy.js +762 -104
- package/dist/images/images-optimizer.d.ts +5 -5
- package/dist/images/images-optimizer.runtime.d.ts +1 -5
- package/dist/prerender-kv-populate.d.ts +3 -4
- package/dist/prerender-kv-populate.js +51 -18
- package/dist/tpr.d.ts +32 -70
- package/dist/tpr.js +45 -602
- package/dist/utils/cache-control-metadata.d.ts +3 -4
- package/dist/version-deploy.d.ts +19 -17
- package/dist/version-deploy.js +32 -13
- package/dist/version-headers.d.ts +2 -0
- package/dist/version-headers.js +2 -0
- package/dist/worker-deployment-url.d.ts +9 -0
- package/dist/worker-deployment-url.js +45 -0
- package/dist/workers-dev-url.d.ts +2 -3
- package/dist/wrangler-config.d.ts +19 -0
- package/dist/wrangler-config.js +294 -0
- package/dist/wrangler-version-metadata.d.ts +7 -0
- package/dist/wrangler-version-metadata.js +9 -0
- package/package.json +14 -11
package/dist/cdn-warm.js
CHANGED
|
@@ -1,9 +1,28 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { VINEXT_CDN_BUILD_ID_HEADER } from "./cache/cdn-build-id.js";
|
|
2
2
|
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { cacheabilityRoutePathname } from "vinext/internal/server/cacheability-manifest";
|
|
5
|
+
import { randomUUID } from "node:crypto";
|
|
6
|
+
import { setTimeout as setTimeout$1 } from "node:timers/promises";
|
|
7
|
+
import { VINEXT_CACHE_HEADER, VINEXT_PRERENDER_READINESS_HEADER, VINEXT_PRERENDER_READINESS_PATH, VINEXT_PRERENDER_SECRET_HEADER } from "vinext/internal/server/headers";
|
|
3
8
|
import { PRERENDER_PATHS_MANIFEST } from "vinext/internal/build/prerender-paths";
|
|
4
9
|
import { getPrerenderedConcretePaths, readPrerenderManifest } from "vinext/internal/server/prerender-manifest";
|
|
10
|
+
import { VINEXT_RSC_BUILD_ID_HEADER, VINEXT_RSC_CONTENT_TYPE, VINEXT_RSC_VARY_HEADER as VINEXT_RSC_VARY_HEADER$1, createCanonicalLoadingShellRscRequestHeaders, createCanonicalRscRequestHeaders, createCanonicalRscRequestUrl, createRscRequestUrl } from "vinext/internal/server/app-rsc-cache-busting";
|
|
11
|
+
import { isNonCacheableCacheControl } from "vinext/shims/cdn-cache";
|
|
12
|
+
import { normalizePathTrailingSlash } from "vinext/shims/url-utils";
|
|
5
13
|
//#region src/cdn-warm.ts
|
|
6
|
-
const
|
|
14
|
+
const DEFAULT_CDN_WARM_CONCURRENCY = 25;
|
|
15
|
+
const DEFAULT_CDN_WARM_TIMEOUT_MS = 1e4;
|
|
16
|
+
const DEFAULT_STAGED_READINESS_RETRIES = 60;
|
|
17
|
+
const DEFAULT_STAGED_READINESS_INTERVAL_MS = 1e3;
|
|
18
|
+
const DEFAULT_STAGED_READINESS_PHASE_TIMEOUT_MS = 12e4;
|
|
19
|
+
const DEFAULT_STAGED_READINESS_SUCCESSES = 6;
|
|
20
|
+
const STAGED_READINESS_QUERY_PARAM = "__vinext_cdn_warm_readiness";
|
|
21
|
+
function applyWarmPathConfig(pathname, config) {
|
|
22
|
+
const withBasePath = config.basePath ? pathname === "/" ? config.basePath : `${config.basePath}${pathname}` : pathname;
|
|
23
|
+
const normalized = normalizePathTrailingSlash(withBasePath, config.trailingSlash === true);
|
|
24
|
+
return new URL(normalized, "http://vinext.local").pathname;
|
|
25
|
+
}
|
|
7
26
|
function readBuiltBuildId(root) {
|
|
8
27
|
try {
|
|
9
28
|
const buildId = fs.readFileSync(path.join(root, "dist", "server", "BUILD_ID"), "utf-8").trim();
|
|
@@ -19,52 +38,83 @@ function readPrerenderPathManifest(manifestPath) {
|
|
|
19
38
|
const parsed = JSON.parse(fs.readFileSync(manifestPath, "utf-8"));
|
|
20
39
|
if (!parsed || typeof parsed !== "object") return null;
|
|
21
40
|
const manifest = parsed;
|
|
22
|
-
if (!Array.isArray(manifest.paths)) return null;
|
|
41
|
+
if (!Array.isArray(manifest.paths) || !manifest.paths.every((pathname) => typeof pathname === "string") || manifest.appPaths !== void 0 && (!Array.isArray(manifest.appPaths) || !manifest.appPaths.every((pathname) => typeof pathname === "string")) || manifest.pagesPaths !== void 0 && (!Array.isArray(manifest.pagesPaths) || !manifest.pagesPaths.every((pathname) => typeof pathname === "string")) || manifest.pagesDataPaths !== void 0 && (!Array.isArray(manifest.pagesDataPaths) || !manifest.pagesDataPaths.every((pathname) => typeof pathname === "string")) || manifest.excludedWarmPaths !== void 0 && (!Array.isArray(manifest.excludedWarmPaths) || !manifest.excludedWarmPaths.every((pathname) => typeof pathname === "string")) || manifest.fallbackRoutePatterns !== void 0 && (!Array.isArray(manifest.fallbackRoutePatterns) || !manifest.fallbackRoutePatterns.every((route) => route !== null && typeof route === "object" && !Array.isArray(route) && (route.kind === "app-page" || route.kind === "app-route" || route.kind === "pages-page") && typeof route.pattern === "string" && route.pattern.startsWith("/"))) || manifest.rscPaths !== void 0 && (!Array.isArray(manifest.rscPaths) || !manifest.rscPaths.every((pathname) => typeof pathname === "string")) || manifest.routeHandlerPaths !== void 0 && (!Array.isArray(manifest.routeHandlerPaths) || !manifest.routeHandlerPaths.every((pathname) => typeof pathname === "string")) || manifest.routePatterns !== void 0 && (!manifest.routePatterns || typeof manifest.routePatterns !== "object" || Array.isArray(manifest.routePatterns) || !Object.entries(manifest.routePatterns).every(([pathname, route]) => pathname.startsWith("/") && route !== null && typeof route === "object" && !Array.isArray(route) && (route.kind === "app-page" || route.kind === "app-route" || route.kind === "pages-page") && typeof route.pattern === "string" && route.pattern.startsWith("/") && (route.cacheabilityProbe === void 0 || route.cacheabilityProbe !== null && typeof route.cacheabilityProbe === "object" && !Array.isArray(route.cacheabilityProbe) && typeof route.cacheabilityProbe.canPrunePattern === "boolean" && (route.cacheabilityProbe.concretePathname === void 0 || typeof route.cacheabilityProbe.concretePathname === "string" && route.cacheabilityProbe.concretePathname.startsWith("/")) && (route.cacheabilityProbe.routeMayResolve === void 0 || typeof route.cacheabilityProbe.routeMayResolve === "boolean") && (route.cacheabilityProbe.requestStageMayTerminate === void 0 || typeof route.cacheabilityProbe.requestStageMayTerminate === "boolean")))) || manifest.loadingShellPaths !== void 0 && (!Array.isArray(manifest.loadingShellPaths) || !manifest.loadingShellPaths.every((pathname) => typeof pathname === "string")) || manifest.basePath !== void 0 && typeof manifest.basePath !== "string" || manifest.buildIdentity !== void 0 && typeof manifest.buildIdentity !== "string" || manifest.deploymentId !== void 0 && typeof manifest.deploymentId !== "string" || manifest.rscBuildId !== void 0 && typeof manifest.rscBuildId !== "string" || manifest.trailingSlash !== void 0 && typeof manifest.trailingSlash !== "boolean" || manifest.responseVary !== void 0 && manifest.responseVary !== "verbatim") return null;
|
|
23
42
|
return manifest;
|
|
24
43
|
} catch (error) {
|
|
25
44
|
console.warn(`[vinext] Failed to read prerender path manifest at ${manifestPath}:`, error);
|
|
26
45
|
return null;
|
|
27
46
|
}
|
|
28
47
|
}
|
|
29
|
-
function
|
|
30
|
-
const manifest = readPrerenderPathManifest(path.join(root, "dist", "server", PRERENDER_PATHS_MANIFEST));
|
|
31
|
-
if (!manifest) return null;
|
|
48
|
+
function createPrerenderPathWarmPaths(root, manifest, options) {
|
|
32
49
|
const builtBuildId = readBuiltBuildId(root);
|
|
33
50
|
if (!manifest.buildId || !builtBuildId || manifest.buildId !== builtBuildId) {
|
|
34
51
|
const message = "[vinext] CDN warmup skipped: prerender path manifest buildId does not match dist/server/BUILD_ID.";
|
|
35
52
|
if (options?.strict) throw new Error(message);
|
|
36
53
|
console.warn(message);
|
|
37
|
-
return
|
|
54
|
+
return null;
|
|
38
55
|
}
|
|
39
|
-
return
|
|
56
|
+
return {
|
|
57
|
+
manifest,
|
|
58
|
+
pagesPaths: (manifest.pagesPaths ?? []).filter((pathname) => pathname.startsWith("/")).filter((pathname) => !manifest.excludedWarmPaths?.includes(pathname)).map((pathname) => applyWarmPathConfig(pathname, manifest)),
|
|
59
|
+
paths: manifest.paths.filter((pathname) => pathname.startsWith("/")).filter((pathname) => !manifest.excludedWarmPaths?.includes(pathname)).map((pathname) => applyWarmPathConfig(pathname, manifest))
|
|
60
|
+
};
|
|
40
61
|
}
|
|
41
|
-
function
|
|
42
|
-
const
|
|
43
|
-
if (!
|
|
44
|
-
const
|
|
45
|
-
if (pathManifestPaths !== null) return pathManifestPaths;
|
|
46
|
-
}
|
|
47
|
-
const manifest = readPrerenderManifest(path.join(root, "dist", "server", "vinext-prerender.json"));
|
|
48
|
-
if (!manifest) {
|
|
49
|
-
if (shouldPreferPrerenderManifest) {
|
|
50
|
-
const pathManifestPaths = readPrerenderPathWarmPaths(root, options);
|
|
51
|
-
if (pathManifestPaths !== null) {
|
|
52
|
-
console.warn("[vinext] CDN warmup fallback shells requested, but prerender manifest not found; warming build-discovered paths only.");
|
|
53
|
-
return pathManifestPaths;
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
const message = "[vinext] CDN warmup skipped: prerender manifest not found.";
|
|
62
|
+
function createPrerenderWarmPlan(root, manifest, options) {
|
|
63
|
+
const pathPlan = createPrerenderPathWarmPaths(root, manifest, options);
|
|
64
|
+
if (!pathPlan) {
|
|
65
|
+
const message = "[vinext] CDN warmup skipped: prerender path discovery is invalid.";
|
|
57
66
|
if (options?.strict) throw new Error(message);
|
|
58
|
-
return
|
|
67
|
+
return {
|
|
68
|
+
loadingShellPaths: [],
|
|
69
|
+
paths: [],
|
|
70
|
+
rscPaths: []
|
|
71
|
+
};
|
|
59
72
|
}
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
73
|
+
const supportsCanonicalRsc = (manifest.responseVary === "verbatim" || options?.includeCanonicalRsc === true) && manifest.rscPaths !== void 0 && manifest.rscBuildId !== void 0;
|
|
74
|
+
const applyConfig = (pathname) => applyWarmPathConfig(pathname, manifest);
|
|
75
|
+
const routePatterns = manifest.routePatterns ? Object.fromEntries(Object.entries(manifest.routePatterns).map(([pathname, route]) => [pathname.includes("/_next/data/") ? pathname : applyConfig(pathname), route])) : void 0;
|
|
76
|
+
let htmlPaths = pathPlan.paths;
|
|
77
|
+
if (options?.includeFallbackShells === true) {
|
|
78
|
+
const prerenderManifest = readPrerenderManifest(path.join(root, "dist", "server", "vinext-prerender.json"));
|
|
79
|
+
if (!prerenderManifest) console.warn("[vinext] CDN warmup fallback shells requested, but prerender manifest not found; warming build-discovered paths only.");
|
|
80
|
+
else if (!prerenderManifest.buildId || prerenderManifest.buildId !== manifest.buildId) {
|
|
81
|
+
const message = "[vinext] CDN warmup skipped: prerender manifest buildId does not match dist/server/BUILD_ID.";
|
|
82
|
+
if (options.strict) throw new Error(message);
|
|
83
|
+
console.warn(message);
|
|
84
|
+
htmlPaths = [];
|
|
85
|
+
} else htmlPaths = getPrerenderedConcretePaths(prerenderManifest, { includeFallbackShells: true }).filter((pathname) => pathname.startsWith("/")).filter((pathname) => !manifest.excludedWarmPaths?.includes(pathname)).map(applyConfig);
|
|
86
|
+
}
|
|
87
|
+
return {
|
|
88
|
+
...manifest.appPaths ? { appPaths: manifest.appPaths.map(applyConfig) } : {},
|
|
89
|
+
buildId: manifest.buildId,
|
|
90
|
+
...manifest.buildIdentity ? { buildIdentity: manifest.buildIdentity } : {},
|
|
91
|
+
...manifest.deploymentId ? { deploymentId: manifest.deploymentId } : {},
|
|
92
|
+
...manifest.fallbackRoutePatterns ? { fallbackRoutePatterns: manifest.fallbackRoutePatterns } : {},
|
|
93
|
+
loadingShellPaths: supportsCanonicalRsc ? (manifest.loadingShellPaths ?? []).map(applyConfig) : [],
|
|
94
|
+
...manifest.pagesDataPaths ? { pagesDataPaths: manifest.pagesDataPaths } : {},
|
|
95
|
+
...manifest.pagesPaths ? { pagesPaths: manifest.pagesPaths.map(applyConfig) } : {},
|
|
96
|
+
paths: htmlPaths,
|
|
97
|
+
...supportsCanonicalRsc ? { rscBuildId: manifest.rscBuildId } : {},
|
|
98
|
+
rscPaths: supportsCanonicalRsc ? manifest.rscPaths.map(applyConfig) : [],
|
|
99
|
+
...manifest.routeHandlerPaths ? { routeHandlerPaths: manifest.routeHandlerPaths.map(applyConfig) } : {},
|
|
100
|
+
...routePatterns ? { routePatterns } : {}
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
function readPrerenderWarmPlan(root, options) {
|
|
104
|
+
const manifest = readPrerenderPathManifest(path.join(root, "dist", "server", PRERENDER_PATHS_MANIFEST));
|
|
105
|
+
if (!manifest) {
|
|
106
|
+
const message = "[vinext] CDN warmup skipped: prerender path manifest not found.";
|
|
63
107
|
if (options?.strict) throw new Error(message);
|
|
64
|
-
|
|
65
|
-
|
|
108
|
+
return {
|
|
109
|
+
loadingShellPaths: [],
|
|
110
|
+
paths: [],
|
|
111
|
+
rscPaths: []
|
|
112
|
+
};
|
|
66
113
|
}
|
|
67
|
-
return
|
|
114
|
+
return createPrerenderWarmPlan(root, manifest, options);
|
|
115
|
+
}
|
|
116
|
+
function readPrerenderWarmPaths(root, options) {
|
|
117
|
+
return readPrerenderWarmPlan(root, options).paths;
|
|
68
118
|
}
|
|
69
119
|
function getWarmPathsFromPrerenderManifest(manifest, options) {
|
|
70
120
|
return getPrerenderedConcretePaths(manifest, options);
|
|
@@ -75,45 +125,676 @@ function normalizeWarmPath(pathname) {
|
|
|
75
125
|
function buildWarmupUrl(targetUrl, pathname) {
|
|
76
126
|
return new URL(normalizeWarmPath(pathname), targetUrl.endsWith("/") ? targetUrl : `${targetUrl}/`);
|
|
77
127
|
}
|
|
78
|
-
function isRetryableStatus(status) {
|
|
79
|
-
return status === 408 || status === 429 || status >= 500;
|
|
128
|
+
function isRetryableStatus(status, retryNotFound) {
|
|
129
|
+
return status === 408 || status === 429 || status >= 500 || retryNotFound && status === 404;
|
|
130
|
+
}
|
|
131
|
+
const WORKER_VERSION_OVERRIDE_HEADER = "Cloudflare-Workers-Version-Overrides";
|
|
132
|
+
async function fetchWithTimeout(fetchImpl, url, timeoutMs, headers, redirect) {
|
|
133
|
+
const controller = new AbortController();
|
|
134
|
+
let response;
|
|
135
|
+
let timer;
|
|
136
|
+
const requestHeaders = new Headers(headers);
|
|
137
|
+
requestHeaders.set("User-Agent", "vinext-cloudflare-cdn-warm");
|
|
138
|
+
try {
|
|
139
|
+
const timedOut = new Promise((_resolve, reject) => {
|
|
140
|
+
timer = setTimeout(() => {
|
|
141
|
+
controller.abort();
|
|
142
|
+
reject(new DOMException(`Timed out after ${timeoutMs}ms`, "AbortError"));
|
|
143
|
+
}, timeoutMs);
|
|
144
|
+
});
|
|
145
|
+
return await Promise.race([(async () => {
|
|
146
|
+
response = await fetchImpl(url, {
|
|
147
|
+
method: "GET",
|
|
148
|
+
redirect,
|
|
149
|
+
headers: requestHeaders,
|
|
150
|
+
signal: controller.signal
|
|
151
|
+
});
|
|
152
|
+
return {
|
|
153
|
+
body: await response.arrayBuffer(),
|
|
154
|
+
response
|
|
155
|
+
};
|
|
156
|
+
})(), timedOut]);
|
|
157
|
+
} finally {
|
|
158
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
159
|
+
if (controller.signal.aborted && response?.body) response.body.cancel().catch(() => {});
|
|
160
|
+
}
|
|
80
161
|
}
|
|
81
|
-
async function
|
|
162
|
+
async function fetchHeadersWithTimeout(fetchImpl, url, timeoutMs, headers, method = "GET") {
|
|
82
163
|
const controller = new AbortController();
|
|
83
|
-
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
84
164
|
const requestHeaders = new Headers(headers);
|
|
85
165
|
requestHeaders.set("User-Agent", "vinext-cloudflare-cdn-warm");
|
|
166
|
+
let timer;
|
|
86
167
|
try {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
168
|
+
const timedOut = new Promise((_resolve, reject) => {
|
|
169
|
+
timer = setTimeout(() => {
|
|
170
|
+
controller.abort();
|
|
171
|
+
reject(new DOMException(`Timed out after ${timeoutMs}ms`, "AbortError"));
|
|
172
|
+
}, timeoutMs);
|
|
173
|
+
});
|
|
174
|
+
return await Promise.race([fetchImpl(url, {
|
|
175
|
+
method,
|
|
176
|
+
redirect: "manual",
|
|
90
177
|
headers: requestHeaders,
|
|
91
178
|
signal: controller.signal
|
|
92
|
-
});
|
|
179
|
+
}), timedOut]);
|
|
93
180
|
} finally {
|
|
94
|
-
clearTimeout(timer);
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
181
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
const MAX_CDN_WARM_REPORT_ROUTES = 10;
|
|
185
|
+
const MAX_CDN_WARM_REPORT_PATTERN_WIDTH = 48;
|
|
186
|
+
const CDN_WARM_ROUTE_KIND_LABELS = {
|
|
187
|
+
"app-page": "App page",
|
|
188
|
+
"app-route": "Route Handler",
|
|
189
|
+
"pages-page": "Pages page"
|
|
190
|
+
};
|
|
191
|
+
function formatCount(count, singular, plural = `${singular}s`) {
|
|
192
|
+
return `${count.toLocaleString("en-US")} ${count === 1 ? singular : plural}`;
|
|
193
|
+
}
|
|
194
|
+
function formatNumber(count) {
|
|
195
|
+
return count.toLocaleString("en-US");
|
|
196
|
+
}
|
|
197
|
+
function truncateMiddle(value, maxLength) {
|
|
198
|
+
if (value.length <= maxLength) return value;
|
|
199
|
+
const remaining = maxLength - 1;
|
|
200
|
+
const startLength = Math.ceil(remaining / 2);
|
|
201
|
+
return `${value.slice(0, startLength)}…${value.slice(-(remaining - startLength))}`;
|
|
202
|
+
}
|
|
203
|
+
function printCdnWarmTable(headings, rows, rightAlignedFrom) {
|
|
204
|
+
const widths = headings.map((heading, index) => Math.max(heading.length, ...rows.map((row) => row[index]?.length ?? 0)));
|
|
205
|
+
for (const row of [headings, ...rows]) console.log(` ${row.map((value, index) => index >= rightAlignedFrom ? value.padStart(widths[index]) : value.padEnd(widths[index])).join(" ")}`);
|
|
206
|
+
}
|
|
207
|
+
function createCdnWarmRouteReport(targets, outcomes) {
|
|
208
|
+
const routes = /* @__PURE__ */ new Map();
|
|
209
|
+
for (const [index, target] of targets.entries()) {
|
|
210
|
+
const key = target.route ? `${target.route.kind}\0${target.route.pattern}` : "unmatched";
|
|
211
|
+
const route = routes.get(key) ?? {
|
|
212
|
+
failed: 0,
|
|
213
|
+
kind: target.route ? CDN_WARM_ROUTE_KIND_LABELS[target.route.kind] : "Other",
|
|
214
|
+
key,
|
|
215
|
+
pattern: target.route?.pattern ?? "Other discovered requests",
|
|
216
|
+
paths: /* @__PURE__ */ new Set(),
|
|
217
|
+
skipped: 0,
|
|
218
|
+
total: 0,
|
|
219
|
+
warmed: 0
|
|
106
220
|
};
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
if (
|
|
111
|
-
|
|
221
|
+
route.paths.add(target.route?.cacheabilityProbe?.concretePathname ?? cacheabilityRoutePathname(target.pathname, target.kind));
|
|
222
|
+
route.total++;
|
|
223
|
+
const outcome = outcomes?.[index];
|
|
224
|
+
if (outcome) route[outcome]++;
|
|
225
|
+
routes.set(key, route);
|
|
226
|
+
}
|
|
227
|
+
return [...routes.values()].sort((left, right) => right.total - left.total || left.key.localeCompare(right.key));
|
|
228
|
+
}
|
|
229
|
+
function printCdnWarmRouteReport(targets, outcomes) {
|
|
230
|
+
if (!targets.some((target) => target.route)) return;
|
|
231
|
+
const routes = createCdnWarmRouteReport(targets, outcomes);
|
|
232
|
+
if (routes.length === 0) return;
|
|
233
|
+
console.log(` CDN warmup ${outcomes ? "result" : "plan"} by route:`);
|
|
234
|
+
const visible = routes.slice(0, MAX_CDN_WARM_REPORT_ROUTES);
|
|
235
|
+
const routeColumns = (route) => [truncateMiddle(route.pattern, MAX_CDN_WARM_REPORT_PATTERN_WIDTH), route.kind];
|
|
236
|
+
if (outcomes) printCdnWarmTable([
|
|
237
|
+
"Route pattern",
|
|
238
|
+
"Kind",
|
|
239
|
+
"Warmed",
|
|
240
|
+
"Skipped",
|
|
241
|
+
"Failed"
|
|
242
|
+
], visible.map((route) => [
|
|
243
|
+
...routeColumns(route),
|
|
244
|
+
`${formatNumber(route.warmed)}/${formatNumber(route.total)}`,
|
|
245
|
+
formatNumber(route.skipped),
|
|
246
|
+
formatNumber(route.failed)
|
|
247
|
+
]), 2);
|
|
248
|
+
else printCdnWarmTable([
|
|
249
|
+
"Route pattern",
|
|
250
|
+
"Kind",
|
|
251
|
+
"Paths",
|
|
252
|
+
"Entries"
|
|
253
|
+
], visible.map((route) => [
|
|
254
|
+
...routeColumns(route),
|
|
255
|
+
formatNumber(route.paths.size),
|
|
256
|
+
formatNumber(route.total)
|
|
257
|
+
]), 2);
|
|
258
|
+
const omitted = routes.slice(MAX_CDN_WARM_REPORT_ROUTES);
|
|
259
|
+
if (omitted.length > 0) {
|
|
260
|
+
const omittedEntries = omitted.reduce((total, route) => total + route.total, 0);
|
|
261
|
+
console.log(` ... ${formatCount(omitted.length, "additional route pattern")} omitted (${formatCount(omittedEntries, "cache entry", "cache entries")})`);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
function cdnWarmTargetKey(target) {
|
|
265
|
+
return `${target.kind}\0${target.sourcePathname}`;
|
|
266
|
+
}
|
|
267
|
+
async function createCdnWarmTargets(options) {
|
|
268
|
+
const requests = [];
|
|
269
|
+
const fullRscPaths = new Set(options.rscPaths ?? []);
|
|
270
|
+
const loadingShellPaths = new Set(options.loadingShellPaths ?? []);
|
|
271
|
+
const commonHeaders = new Headers(options.headers);
|
|
272
|
+
for (const pathname of /* @__PURE__ */ new Set([...fullRscPaths, ...loadingShellPaths])) {
|
|
273
|
+
if (fullRscPaths.has(pathname)) {
|
|
274
|
+
const rscHeaders = new Headers(commonHeaders);
|
|
275
|
+
for (const [name, value] of createCanonicalRscRequestHeaders(options.deploymentId)) rscHeaders.set(name, value);
|
|
276
|
+
requests.push({
|
|
277
|
+
headers: rscHeaders,
|
|
278
|
+
kind: "rsc-full",
|
|
279
|
+
label: `${pathname} (RSC full)`,
|
|
280
|
+
pathname: createCanonicalRscRequestUrl(pathname),
|
|
281
|
+
sourcePathname: pathname,
|
|
282
|
+
route: options.routePatterns?.[pathname]
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
if (loadingShellPaths.has(pathname)) {
|
|
286
|
+
const loadingHeaders = new Headers(commonHeaders);
|
|
287
|
+
for (const [name, value] of createCanonicalLoadingShellRscRequestHeaders(options.deploymentId)) loadingHeaders.set(name, value);
|
|
288
|
+
requests.push({
|
|
289
|
+
headers: loadingHeaders,
|
|
290
|
+
kind: "rsc-loading-shell",
|
|
291
|
+
label: `${pathname} (RSC loading shell)`,
|
|
292
|
+
pathname: await createRscRequestUrl(pathname, loadingHeaders),
|
|
293
|
+
sourcePathname: pathname,
|
|
294
|
+
route: options.routePatterns?.[pathname]
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
for (const pathname of new Set(options.paths)) {
|
|
299
|
+
const htmlHeaders = new Headers(commonHeaders);
|
|
300
|
+
htmlHeaders.set("Accept", "text/html");
|
|
301
|
+
requests.push({
|
|
302
|
+
headers: htmlHeaders,
|
|
303
|
+
kind: "html",
|
|
304
|
+
label: pathname,
|
|
305
|
+
pathname,
|
|
306
|
+
sourcePathname: pathname,
|
|
307
|
+
route: options.routePatterns?.[pathname]
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
for (const pathname of new Set(options.pagesDataPaths ?? [])) {
|
|
311
|
+
const dataHeaders = new Headers(commonHeaders);
|
|
312
|
+
dataHeaders.set("Accept", "application/json");
|
|
313
|
+
requests.push({
|
|
314
|
+
headers: dataHeaders,
|
|
315
|
+
kind: "pages-data",
|
|
316
|
+
label: `${pathname} (Pages data)`,
|
|
317
|
+
pathname,
|
|
318
|
+
sourcePathname: pathname,
|
|
319
|
+
route: options.routePatterns?.[pathname]
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
for (const pathname of new Set(options.routeHandlerPaths ?? [])) {
|
|
323
|
+
const routeHeaders = new Headers(commonHeaders);
|
|
324
|
+
routeHeaders.set("Accept", "*/*");
|
|
325
|
+
requests.push({
|
|
326
|
+
headers: routeHeaders,
|
|
327
|
+
kind: "app-route",
|
|
328
|
+
label: `${pathname} (Route Handler)`,
|
|
329
|
+
pathname,
|
|
330
|
+
sourcePathname: pathname,
|
|
331
|
+
route: options.routePatterns?.[pathname]
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
return requests;
|
|
335
|
+
}
|
|
336
|
+
var CdnOperationProgress = class {
|
|
337
|
+
isTTY = process.stderr.isTTY;
|
|
338
|
+
lastLineLength = 0;
|
|
339
|
+
update(completed, total, label, phase = "Warming CDN cache") {
|
|
340
|
+
if (!this.isTTY) return;
|
|
341
|
+
const percent = total > 0 ? Math.floor(completed / total * 100) : 0;
|
|
342
|
+
const filled = Math.floor(percent / 5);
|
|
343
|
+
const bar = `[${"█".repeat(filled)}${" ".repeat(20 - filled)}]`;
|
|
344
|
+
const shortLabel = label.length > 40 ? `…${label.slice(-39)}` : label;
|
|
345
|
+
const line = `${phase}... ${bar} ${String(completed).padStart(String(total).length)}/${total} ${shortLabel}`;
|
|
346
|
+
process.stderr.write(`\r${line.padEnd(this.lastLineLength)}`);
|
|
347
|
+
this.lastLineLength = line.length;
|
|
348
|
+
}
|
|
349
|
+
finish() {
|
|
350
|
+
if (!this.isTTY || this.lastLineLength === 0) return;
|
|
351
|
+
process.stderr.write(`\r${" ".repeat(this.lastLineLength)}\r`);
|
|
352
|
+
this.lastLineLength = 0;
|
|
353
|
+
}
|
|
354
|
+
};
|
|
355
|
+
const REQUIRED_RSC_VARY_HEADERS = VINEXT_RSC_VARY_HEADER$1.split(",").map((name) => name.trim().toLowerCase());
|
|
356
|
+
const ADMITTED_CF_CACHE_STATUSES = /* @__PURE__ */ new Set([
|
|
357
|
+
"HIT",
|
|
358
|
+
"MISS",
|
|
359
|
+
"EXPIRED",
|
|
360
|
+
"REVALIDATED",
|
|
361
|
+
"UPDATING"
|
|
362
|
+
]);
|
|
363
|
+
const REUSABLE_CF_CACHE_STATUSES = /* @__PURE__ */ new Set([
|
|
364
|
+
"HIT",
|
|
365
|
+
"REVALIDATED",
|
|
366
|
+
"UPDATING"
|
|
367
|
+
]);
|
|
368
|
+
const TRANSITIONAL_VINEXT_CACHE_STATUSES = /* @__PURE__ */ new Set(["MISS", "UPDATING"]);
|
|
369
|
+
const NON_CACHEABLE_CF_CACHE_STATUSES = /* @__PURE__ */ new Set(["BYPASS"]);
|
|
370
|
+
const CDN_CACHE_POLICY_HEADERS = [
|
|
371
|
+
"Cloudflare-CDN-Cache-Control",
|
|
372
|
+
"CDN-Cache-Control",
|
|
373
|
+
"Cache-Control"
|
|
374
|
+
];
|
|
375
|
+
function splitCacheControlDirectives(value) {
|
|
376
|
+
const directives = [];
|
|
377
|
+
let start = 0;
|
|
378
|
+
let quoted = false;
|
|
379
|
+
let escaped = false;
|
|
380
|
+
for (let index = 0; index < value.length; index++) {
|
|
381
|
+
const character = value[index];
|
|
382
|
+
if (escaped) escaped = false;
|
|
383
|
+
else if (quoted && character === "\\") escaped = true;
|
|
384
|
+
else if (character === "\"") quoted = !quoted;
|
|
385
|
+
else if (character === "," && !quoted) {
|
|
386
|
+
directives.push(value.slice(start, index));
|
|
387
|
+
start = index + 1;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
directives.push(value.slice(start));
|
|
391
|
+
return directives;
|
|
392
|
+
}
|
|
393
|
+
function hasFieldQualifiedSetCookie(value) {
|
|
394
|
+
return splitCacheControlDirectives(value).some((directive) => {
|
|
395
|
+
const separator = directive.indexOf("=");
|
|
396
|
+
if (separator === -1) return false;
|
|
397
|
+
const name = directive.slice(0, separator).trim().toLowerCase();
|
|
398
|
+
if (name !== "private" && name !== "no-cache") return false;
|
|
399
|
+
const rawFields = directive.slice(separator + 1).trim();
|
|
400
|
+
return (rawFields.startsWith("\"") && rawFields.endsWith("\"") ? rawFields.slice(1, -1) : rawFields).split(",").some((field) => field.trim().toLowerCase() === "set-cookie");
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
function validateCachePolicy(response, requireCacheStatus, requireCacheHit = false) {
|
|
404
|
+
const effectivePolicy = CDN_CACHE_POLICY_HEADERS.map((name) => ({
|
|
405
|
+
name,
|
|
406
|
+
value: response.headers.get(name)
|
|
407
|
+
})).find((entry) => entry.value !== null);
|
|
408
|
+
const nonCacheableHeaders = effectivePolicy && isNonCacheableCacheControl(effectivePolicy.value) ? [effectivePolicy.name] : [];
|
|
409
|
+
const hasSetCookie = response.headers.has("Set-Cookie");
|
|
410
|
+
const cacheStatus = response.headers.get("CF-Cache-Status")?.trim().toUpperCase();
|
|
411
|
+
if (requireCacheHit && !REUSABLE_CF_CACHE_STATUSES.has(cacheStatus ?? "")) return {
|
|
412
|
+
outcome: "failed",
|
|
413
|
+
error: `CF-Cache-Status is ${cacheStatus ?? "missing"}; the cache fill is not reusable`
|
|
414
|
+
};
|
|
415
|
+
if (cacheStatus && ADMITTED_CF_CACHE_STATUSES.has(cacheStatus)) {
|
|
416
|
+
if (hasSetCookie && (!effectivePolicy || !hasFieldQualifiedSetCookie(effectivePolicy.value))) return {
|
|
417
|
+
outcome: "failed",
|
|
418
|
+
error: "response sets a cookie without an observable field-qualified cache policy"
|
|
419
|
+
};
|
|
420
|
+
return { outcome: "warmed" };
|
|
421
|
+
}
|
|
422
|
+
if (cacheStatus && NON_CACHEABLE_CF_CACHE_STATUSES.has(cacheStatus)) return {
|
|
423
|
+
outcome: "skipped",
|
|
424
|
+
reason: nonCacheableHeaders.length > 0 ? `${nonCacheableHeaders.join(", ")} opts out of caching` : hasSetCookie ? "response sets a cookie" : `CF-Cache-Status is ${cacheStatus}`
|
|
425
|
+
};
|
|
426
|
+
if (cacheStatus === "DYNAMIC") {
|
|
427
|
+
if (nonCacheableHeaders.length > 0 || hasSetCookie) return {
|
|
428
|
+
outcome: "skipped",
|
|
429
|
+
reason: nonCacheableHeaders.length > 0 ? `${nonCacheableHeaders.join(", ")} opts out of caching` : "response sets a cookie"
|
|
430
|
+
};
|
|
431
|
+
return {
|
|
432
|
+
outcome: "failed",
|
|
433
|
+
error: "CF-Cache-Status is DYNAMIC"
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
if (nonCacheableHeaders.length > 0 || hasSetCookie) return {
|
|
437
|
+
outcome: "failed",
|
|
438
|
+
error: `${nonCacheableHeaders.length > 0 ? `${nonCacheableHeaders.join(", ")} opts out of caching` : "response sets a cookie"}, but CF-Cache-Status is ${cacheStatus ?? "missing"}`
|
|
439
|
+
};
|
|
440
|
+
if (!cacheStatus) return requireCacheStatus ? {
|
|
441
|
+
outcome: "failed",
|
|
442
|
+
error: "response is missing CF-Cache-Status"
|
|
443
|
+
} : { outcome: "warmed" };
|
|
444
|
+
return {
|
|
445
|
+
outcome: "failed",
|
|
446
|
+
error: `CF-Cache-Status is ${cacheStatus}`
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
function validateVinextCacheStatus(response, missingStatus = "failed", requireCacheHit = false) {
|
|
450
|
+
const status = response.headers.get(VINEXT_CACHE_HEADER)?.trim().toUpperCase();
|
|
451
|
+
if (requireCacheHit && status !== "HIT") return {
|
|
452
|
+
outcome: "failed",
|
|
453
|
+
error: `${VINEXT_CACHE_HEADER} is ${status ?? "missing"}; the cache fill is not reusable`
|
|
454
|
+
};
|
|
455
|
+
if (status === "MISS" || status === "HIT" || status === "UPDATING") return { outcome: "warmed" };
|
|
456
|
+
if (status === "BYPASS" || status === "DYNAMIC") return {
|
|
457
|
+
outcome: "skipped",
|
|
458
|
+
reason: `${VINEXT_CACHE_HEADER} is ${status}`
|
|
459
|
+
};
|
|
460
|
+
return missingStatus === "skipped" ? {
|
|
461
|
+
outcome: "skipped",
|
|
462
|
+
reason: `response is missing ${VINEXT_CACHE_HEADER}`
|
|
463
|
+
} : {
|
|
464
|
+
outcome: "failed",
|
|
465
|
+
error: `response is missing ${VINEXT_CACHE_HEADER}`
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
function validateWarmAdmission(response, statusSource, requireCacheHit) {
|
|
469
|
+
if (statusSource === "vinext") return validateVinextCacheStatus(response, "failed", requireCacheHit);
|
|
470
|
+
if (statusSource === "data-cache") return validateVinextCacheStatus(response, "skipped", requireCacheHit);
|
|
471
|
+
return validateCachePolicy(response, true, requireCacheHit);
|
|
472
|
+
}
|
|
473
|
+
function validateBuildIdentity(response, expectedBuildId) {
|
|
474
|
+
if (expectedBuildId !== void 0 && response.headers.get("X-Vinext-Build-Id") !== expectedBuildId) {
|
|
475
|
+
const actualBuildId = response.headers.get("X-Vinext-Build-Id") ?? "missing";
|
|
476
|
+
return {
|
|
477
|
+
outcome: "failed",
|
|
478
|
+
error: `response ${VINEXT_CDN_BUILD_ID_HEADER} does not match build ${expectedBuildId} (received ${actualBuildId})`
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
return null;
|
|
482
|
+
}
|
|
483
|
+
function isExpectedTerminalStatus(status) {
|
|
484
|
+
return status >= 300 && status < 400 || status === 404;
|
|
485
|
+
}
|
|
486
|
+
function validateRscWarmResponse(response, expectedBuildId, expectedRscBuildId, requireCacheHit = false, statusSource = "cloudflare") {
|
|
487
|
+
const buildIdentityValidation = validateBuildIdentity(response, expectedBuildId);
|
|
488
|
+
if (buildIdentityValidation) return buildIdentityValidation;
|
|
489
|
+
if (expectedRscBuildId !== void 0 && response.headers.get(VINEXT_RSC_BUILD_ID_HEADER) !== expectedRscBuildId) return {
|
|
490
|
+
outcome: "failed",
|
|
491
|
+
error: `response ${VINEXT_RSC_BUILD_ID_HEADER} does not match build ${expectedRscBuildId}`
|
|
492
|
+
};
|
|
493
|
+
if (response.redirected) return {
|
|
494
|
+
outcome: "failed",
|
|
495
|
+
error: "redirected response"
|
|
496
|
+
};
|
|
497
|
+
const terminalResponse = response.status < 200 || response.status >= 300;
|
|
498
|
+
if (terminalResponse) {
|
|
499
|
+
if (!isExpectedTerminalStatus(response.status) || expectedBuildId === void 0 && expectedRscBuildId === void 0) return {
|
|
500
|
+
outcome: "failed",
|
|
501
|
+
error: `HTTP ${response.status}`
|
|
502
|
+
};
|
|
503
|
+
} else if (!response.headers.get("Content-Type")?.toLowerCase().startsWith(VINEXT_RSC_CONTENT_TYPE)) return {
|
|
504
|
+
outcome: "failed",
|
|
505
|
+
error: `expected ${VINEXT_RSC_CONTENT_TYPE} response`
|
|
506
|
+
};
|
|
507
|
+
const cachePolicyValidation = validateWarmAdmission(response, statusSource, requireCacheHit);
|
|
508
|
+
if (cachePolicyValidation.outcome !== "warmed") return cachePolicyValidation;
|
|
509
|
+
if (terminalResponse && !response.headers.get("Content-Type")?.toLowerCase().startsWith(VINEXT_RSC_CONTENT_TYPE)) return {
|
|
510
|
+
outcome: "failed",
|
|
511
|
+
error: `expected ${VINEXT_RSC_CONTENT_TYPE} response`
|
|
512
|
+
};
|
|
513
|
+
const vary = new Set((response.headers.get("Vary") ?? "").split(",").map((name) => name.trim().toLowerCase()).filter(Boolean));
|
|
514
|
+
const missingVary = REQUIRED_RSC_VARY_HEADERS.find((name) => !vary.has(name));
|
|
515
|
+
if (missingVary) return {
|
|
516
|
+
outcome: "failed",
|
|
517
|
+
error: `response Vary is missing ${missingVary}`
|
|
518
|
+
};
|
|
519
|
+
return { outcome: "warmed" };
|
|
520
|
+
}
|
|
521
|
+
function validateHtmlWarmResponse(response, expectedBuildId, requireCacheHit = false, statusSource = "cloudflare") {
|
|
522
|
+
const buildIdentityValidation = validateBuildIdentity(response, expectedBuildId);
|
|
523
|
+
if (buildIdentityValidation) return buildIdentityValidation;
|
|
524
|
+
if (response.redirected) return {
|
|
525
|
+
outcome: "failed",
|
|
526
|
+
error: "redirected response"
|
|
527
|
+
};
|
|
528
|
+
if (response.status < 200 || response.status >= 300) {
|
|
529
|
+
if (!isExpectedTerminalStatus(response.status) || expectedBuildId === void 0) return {
|
|
530
|
+
outcome: "failed",
|
|
531
|
+
error: `HTTP ${response.status}`
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
const cachePolicyValidation = validateWarmAdmission(response, statusSource, requireCacheHit);
|
|
535
|
+
if (cachePolicyValidation.outcome !== "warmed") return cachePolicyValidation;
|
|
536
|
+
return { outcome: "warmed" };
|
|
537
|
+
}
|
|
538
|
+
function validatePagesDataWarmResponse(response, expectedBuildId, requireCacheHit = false, statusSource = "cloudflare") {
|
|
539
|
+
const validation = validateHtmlWarmResponse(response, expectedBuildId, requireCacheHit, statusSource);
|
|
540
|
+
if (validation.outcome !== "warmed") return validation;
|
|
541
|
+
if (!response.headers.get("Content-Type")?.toLowerCase().startsWith("application/json")) return {
|
|
542
|
+
outcome: "failed",
|
|
543
|
+
error: "expected application/json response"
|
|
544
|
+
};
|
|
545
|
+
return validation;
|
|
546
|
+
}
|
|
547
|
+
function validateReadinessResponse(response, kind, expectedBuildId, expectedRscBuildId) {
|
|
548
|
+
const buildIdentityValidation = validateBuildIdentity(response, expectedBuildId);
|
|
549
|
+
if (buildIdentityValidation?.outcome === "failed") return buildIdentityValidation.error;
|
|
550
|
+
if (kind === "rsc" && expectedRscBuildId !== void 0 && response.headers.get(VINEXT_RSC_BUILD_ID_HEADER) !== expectedRscBuildId) return `response ${VINEXT_RSC_BUILD_ID_HEADER} does not match build ${expectedRscBuildId}`;
|
|
551
|
+
if (response.redirected) return "redirected response";
|
|
552
|
+
if (response.status >= 500) return `HTTP ${response.status}`;
|
|
553
|
+
if (response.status >= 200 && response.status < 300) {
|
|
554
|
+
const contentType = response.headers.get("Content-Type")?.toLowerCase();
|
|
555
|
+
if (kind === "rsc" && !contentType?.startsWith(VINEXT_RSC_CONTENT_TYPE)) return `expected ${VINEXT_RSC_CONTENT_TYPE} response`;
|
|
556
|
+
if (kind === "pages-data" && !contentType?.startsWith("application/json")) return "expected application/json response";
|
|
557
|
+
}
|
|
558
|
+
return null;
|
|
559
|
+
}
|
|
560
|
+
function validatePrerenderReadinessResponse(response, expectedBuildId) {
|
|
561
|
+
const buildIdentityValidation = validateBuildIdentity(response, expectedBuildId);
|
|
562
|
+
if (buildIdentityValidation?.outcome === "failed") return buildIdentityValidation.error;
|
|
563
|
+
if (response.redirected) return "redirected response";
|
|
564
|
+
if (response.status !== 204) return `expected readiness HTTP 204, received ${response.status}`;
|
|
565
|
+
if (response.headers.get(VINEXT_PRERENDER_READINESS_HEADER) !== "1") return `response is missing ${VINEXT_PRERENDER_READINESS_HEADER}: 1`;
|
|
566
|
+
const cacheControl = response.headers.get("Cache-Control");
|
|
567
|
+
if (!cacheControl || !/(?:^|,)\s*no-store\s*(?:,|$)/i.test(cacheControl)) return "readiness response is missing Cache-Control: no-store";
|
|
568
|
+
return null;
|
|
569
|
+
}
|
|
570
|
+
/**
|
|
571
|
+
* Wait until version-override requests consistently reach the uploaded build
|
|
572
|
+
* before any real cache key is filled. Every probe has a unique query key, so
|
|
573
|
+
* an early response cannot make a later readiness attempt pass from cache.
|
|
574
|
+
*/
|
|
575
|
+
async function waitForCdnWarmTargetReadiness(options) {
|
|
576
|
+
const rscPath = options.plan.rscPaths[0] ?? options.plan.loadingShellPaths[0];
|
|
577
|
+
const htmlPath = options.plan.paths[0];
|
|
578
|
+
const pagesDataPath = options.plan.pagesDataPaths[0];
|
|
579
|
+
const routeHandlerPath = options.plan.routeHandlerPaths?.[0];
|
|
580
|
+
const kind = rscPath ? "rsc" : htmlPath ? "html" : pagesDataPath ? "pages-data" : "app-route";
|
|
581
|
+
const pathname = rscPath ?? htmlPath ?? pagesDataPath ?? routeHandlerPath;
|
|
582
|
+
if (!pathname) return { ready: true };
|
|
583
|
+
if (options.expectedBuildId === void 0 && options.expectedRscBuildId === void 0) return {
|
|
584
|
+
error: "no response build identity is available for a staged-version readiness probe",
|
|
585
|
+
ready: false
|
|
586
|
+
};
|
|
587
|
+
const headers = new Headers(options.headers);
|
|
588
|
+
const readinessSecret = options.prerenderSecret;
|
|
589
|
+
const useReadinessEndpoint = Boolean(readinessSecret);
|
|
590
|
+
const probePath = useReadinessEndpoint ? VINEXT_PRERENDER_READINESS_PATH : kind === "rsc" ? createCanonicalRscRequestUrl(pathname) : pathname;
|
|
591
|
+
if (readinessSecret) {
|
|
592
|
+
headers.set("Accept", "text/html");
|
|
593
|
+
headers.set(VINEXT_PRERENDER_SECRET_HEADER, readinessSecret);
|
|
594
|
+
} else if (kind === "rsc") for (const [name, value] of createCanonicalRscRequestHeaders(options.deploymentId)) headers.set(name, value);
|
|
595
|
+
else if (kind === "html") headers.set("Accept", "text/html");
|
|
596
|
+
else if (kind === "pages-data") headers.set("Accept", "application/json");
|
|
597
|
+
else headers.set("Accept", "*/*");
|
|
598
|
+
headers.set("Cache-Control", "no-cache");
|
|
599
|
+
headers.set("Pragma", "no-cache");
|
|
600
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
601
|
+
const timeoutMs = Math.max(1, options.timeoutMs ?? 1e4);
|
|
602
|
+
const probeIntervalMs = Math.max(0, options.probeIntervalMs ?? 1e3);
|
|
603
|
+
const requiredConsecutiveSuccesses = Math.max(1, options.requiredConsecutiveSuccesses ?? DEFAULT_STAGED_READINESS_SUCCESSES);
|
|
604
|
+
const readinessRetries = Math.max(0, options.retries ?? 60);
|
|
605
|
+
const maxAttempts = Math.max(requiredConsecutiveSuccesses, options.maxAttempts ?? requiredConsecutiveSuccesses + readinessRetries);
|
|
606
|
+
const phaseTimeoutMs = Math.max(1, options.phaseTimeoutMs ?? 12e4);
|
|
607
|
+
const deadlineAt = Date.now() + phaseTimeoutMs;
|
|
608
|
+
const probeId = randomUUID();
|
|
609
|
+
let consecutiveSuccesses = 0;
|
|
610
|
+
let lastError = "readiness probe did not run";
|
|
611
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
612
|
+
const remainingMs = deadlineAt - Date.now();
|
|
613
|
+
if (remainingMs <= 0) {
|
|
614
|
+
lastError = `staged readiness exceeded its ${phaseTimeoutMs}ms phase deadline`;
|
|
615
|
+
break;
|
|
616
|
+
}
|
|
617
|
+
const url = buildWarmupUrl(options.targetUrl, probePath);
|
|
618
|
+
url.searchParams.set(STAGED_READINESS_QUERY_PARAM, `${probeId}-${attempt}`);
|
|
619
|
+
let response;
|
|
620
|
+
try {
|
|
621
|
+
response = await fetchHeadersWithTimeout(fetchImpl, url, Math.min(timeoutMs, remainingMs), headers, useReadinessEndpoint ? "POST" : "GET");
|
|
622
|
+
const validationError = useReadinessEndpoint ? validatePrerenderReadinessResponse(response, options.expectedBuildId) : validateReadinessResponse(response, kind, options.expectedBuildId, options.expectedRscBuildId);
|
|
623
|
+
if (process.env.VINEXT_CDN_WARM_DEBUG === "1") console.log(` CDN warm readiness attempt ${attempt + 1}: ${url.pathname}${url.search} HTTP ${response.status} build=${response.headers.get(VINEXT_RSC_BUILD_ID_HEADER) ?? response.headers.get("X-Vinext-Build-Id") ?? "missing"}`);
|
|
624
|
+
if (validationError === null) {
|
|
625
|
+
consecutiveSuccesses++;
|
|
626
|
+
if (consecutiveSuccesses >= requiredConsecutiveSuccesses) return { ready: true };
|
|
627
|
+
} else {
|
|
628
|
+
consecutiveSuccesses = 0;
|
|
629
|
+
lastError = validationError;
|
|
630
|
+
}
|
|
631
|
+
} catch (error) {
|
|
632
|
+
consecutiveSuccesses = 0;
|
|
633
|
+
lastError = Date.now() >= deadlineAt ? `staged readiness exceeded its ${phaseTimeoutMs}ms phase deadline` : error instanceof DOMException && error.name === "AbortError" ? `timed out after ${Math.min(timeoutMs, remainingMs)}ms` : error instanceof Error ? error.message : String(error);
|
|
634
|
+
} finally {
|
|
635
|
+
if (response?.body) response.body.cancel().catch(() => {});
|
|
636
|
+
}
|
|
637
|
+
if (attempt + 1 < maxAttempts && probeIntervalMs > 0) {
|
|
638
|
+
const delayMs = Math.min(probeIntervalMs, Math.max(0, deadlineAt - Date.now()));
|
|
639
|
+
if (delayMs <= 0) {
|
|
640
|
+
lastError = `staged readiness exceeded its ${phaseTimeoutMs}ms phase deadline`;
|
|
641
|
+
break;
|
|
642
|
+
}
|
|
643
|
+
await setTimeout$1(delayMs);
|
|
644
|
+
}
|
|
112
645
|
}
|
|
113
646
|
return {
|
|
114
|
-
|
|
647
|
+
error: `${lastError}; uploaded build was not stable for ${requiredConsecutiveSuccesses} consecutive probe(s)`,
|
|
648
|
+
ready: false
|
|
649
|
+
};
|
|
650
|
+
}
|
|
651
|
+
function shouldRetryValidationFailure(response, target, options) {
|
|
652
|
+
if (!options.retryPropagationFailures) return isRetryableStatus(response.status, options.retryNotFound);
|
|
653
|
+
const expectedIdentities = [options.expectedBuildId === void 0 ? null : response.headers.get(VINEXT_CDN_BUILD_ID_HEADER) === options.expectedBuildId, !target.kind.startsWith("rsc-") || options.expectedRscBuildId === void 0 ? null : response.headers.get(VINEXT_RSC_BUILD_ID_HEADER) === options.expectedRscBuildId].filter((matches) => matches !== null);
|
|
654
|
+
if (expectedIdentities.some((matches) => matches)) return isRetryableStatus(response.status, false);
|
|
655
|
+
if (expectedIdentities.some((matches) => !matches)) return true;
|
|
656
|
+
return isRetryableStatus(response.status, options.retryNotFound);
|
|
657
|
+
}
|
|
658
|
+
function isRetryableCertificationFailure(response, statusSource, retryPropagationFailures) {
|
|
659
|
+
if (statusSource === "cloudflare") {
|
|
660
|
+
const status = response.headers.get("CF-Cache-Status")?.trim().toUpperCase();
|
|
661
|
+
return !REUSABLE_CF_CACHE_STATUSES.has(status ?? "") && (retryPropagationFailures || ADMITTED_CF_CACHE_STATUSES.has(status ?? ""));
|
|
662
|
+
}
|
|
663
|
+
const status = response.headers.get(VINEXT_CACHE_HEADER)?.trim().toUpperCase();
|
|
664
|
+
return TRANSITIONAL_VINEXT_CACHE_STATUSES.has(status ?? "");
|
|
665
|
+
}
|
|
666
|
+
async function warmOnePath(target, options) {
|
|
667
|
+
const url = buildWarmupUrl(options.targetUrl, target.pathname);
|
|
668
|
+
let lastError = "request failed before the first attempt";
|
|
669
|
+
let lastRetryable = true;
|
|
670
|
+
let lastSkippedReason = null;
|
|
671
|
+
const phaseDeadlineError = () => `CDN warmup exceeded its ${options.phaseTimeoutMs}ms phase deadline`;
|
|
672
|
+
const remainingPhaseMs = () => options.deadlineAt === void 0 ? options.timeoutMs : options.deadlineAt - Date.now();
|
|
673
|
+
const canRetry = (attempt) => attempt < options.retries;
|
|
674
|
+
const waitBeforeRetry = async () => {
|
|
675
|
+
if (options.retryDelayMs <= 0) return remainingPhaseMs() > 0;
|
|
676
|
+
const delayMs = Math.min(options.retryDelayMs, Math.max(0, remainingPhaseMs()));
|
|
677
|
+
if (delayMs <= 0) return false;
|
|
678
|
+
await setTimeout$1(delayMs);
|
|
679
|
+
return remainingPhaseMs() > 0;
|
|
680
|
+
};
|
|
681
|
+
for (let attempt = 0; attempt <= options.retries; attempt++) {
|
|
682
|
+
const remainingMs = remainingPhaseMs();
|
|
683
|
+
if (remainingMs <= 0) return {
|
|
684
|
+
path: target.label,
|
|
685
|
+
ok: false,
|
|
686
|
+
error: phaseDeadlineError(),
|
|
687
|
+
retryable: false
|
|
688
|
+
};
|
|
689
|
+
const attemptTimeoutMs = Math.min(options.timeoutMs, remainingMs);
|
|
690
|
+
const phaseLimitedAttempt = options.deadlineAt !== void 0 && remainingMs <= options.timeoutMs;
|
|
691
|
+
try {
|
|
692
|
+
const response = options.requireCacheHit ? await fetchHeadersWithTimeout(options.fetchImpl, url, attemptTimeoutMs, target.headers ?? options.headers) : (await fetchWithTimeout(options.fetchImpl, url, attemptTimeoutMs, target.headers ?? options.headers, "manual")).response;
|
|
693
|
+
if (options.deadlineAt !== void 0 && Date.now() >= options.deadlineAt) return {
|
|
694
|
+
path: target.label,
|
|
695
|
+
ok: false,
|
|
696
|
+
error: phaseDeadlineError(),
|
|
697
|
+
retryable: false
|
|
698
|
+
};
|
|
699
|
+
if (options.requireCacheHit) response.body?.cancel().catch(() => {});
|
|
700
|
+
if (process.env.VINEXT_CDN_WARM_DEBUG === "1") console.log(` CDN warm debug ${target.label} attempt ${attempt + 1}: ${url.pathname}${url.search} HTTP ${response.status} cache=${response.headers.get("CF-Cache-Status") ?? "missing"} ray=${response.headers.get("CF-Ray") ?? "missing"} encoding=${response.headers.get("Content-Encoding") ?? "identity"} rscBuild=${response.headers.get(VINEXT_RSC_BUILD_ID_HEADER) ?? "missing"}`);
|
|
701
|
+
if (target.kind.startsWith("rsc-")) {
|
|
702
|
+
const validation = validateRscWarmResponse(response, options.expectedBuildId, options.expectedRscBuildId, options.requireCacheHit, options.statusSource);
|
|
703
|
+
if (validation.outcome === "warmed") return {
|
|
704
|
+
path: target.label,
|
|
705
|
+
ok: true,
|
|
706
|
+
skipped: false
|
|
707
|
+
};
|
|
708
|
+
if (validation.outcome === "skipped") {
|
|
709
|
+
if (!options.retrySkipped) return {
|
|
710
|
+
path: target.label,
|
|
711
|
+
ok: true,
|
|
712
|
+
skipped: true,
|
|
713
|
+
reason: validation.reason
|
|
714
|
+
};
|
|
715
|
+
lastSkippedReason = validation.reason;
|
|
716
|
+
lastRetryable = true;
|
|
717
|
+
if (!canRetry(attempt)) break;
|
|
718
|
+
if (!await waitBeforeRetry()) return {
|
|
719
|
+
path: target.label,
|
|
720
|
+
ok: false,
|
|
721
|
+
error: phaseDeadlineError(),
|
|
722
|
+
retryable: false
|
|
723
|
+
};
|
|
724
|
+
continue;
|
|
725
|
+
}
|
|
726
|
+
lastSkippedReason = null;
|
|
727
|
+
lastError = validation.error;
|
|
728
|
+
lastRetryable = options.requireCacheHit && isRetryableCertificationFailure(response, options.statusSource, options.retryPropagationFailures) || shouldRetryValidationFailure(response, target, options);
|
|
729
|
+
if (!lastRetryable) break;
|
|
730
|
+
if (!canRetry(attempt)) break;
|
|
731
|
+
if (!await waitBeforeRetry()) return {
|
|
732
|
+
path: target.label,
|
|
733
|
+
ok: false,
|
|
734
|
+
error: phaseDeadlineError(),
|
|
735
|
+
retryable: false
|
|
736
|
+
};
|
|
737
|
+
continue;
|
|
738
|
+
}
|
|
739
|
+
const validation = target.kind === "pages-data" ? validatePagesDataWarmResponse(response, options.expectedBuildId, options.requireCacheHit, options.statusSource) : validateHtmlWarmResponse(response, options.expectedBuildId, options.requireCacheHit, options.statusSource);
|
|
740
|
+
if (validation.outcome === "warmed") return {
|
|
741
|
+
path: target.label,
|
|
742
|
+
ok: true,
|
|
743
|
+
skipped: false
|
|
744
|
+
};
|
|
745
|
+
if (validation.outcome === "skipped") {
|
|
746
|
+
if (!options.retrySkipped) return {
|
|
747
|
+
path: target.label,
|
|
748
|
+
ok: true,
|
|
749
|
+
skipped: true,
|
|
750
|
+
reason: validation.reason
|
|
751
|
+
};
|
|
752
|
+
lastSkippedReason = validation.reason;
|
|
753
|
+
lastRetryable = true;
|
|
754
|
+
if (!canRetry(attempt)) break;
|
|
755
|
+
if (!await waitBeforeRetry()) return {
|
|
756
|
+
path: target.label,
|
|
757
|
+
ok: false,
|
|
758
|
+
error: phaseDeadlineError(),
|
|
759
|
+
retryable: false
|
|
760
|
+
};
|
|
761
|
+
continue;
|
|
762
|
+
}
|
|
763
|
+
lastSkippedReason = null;
|
|
764
|
+
lastError = validation.error;
|
|
765
|
+
lastRetryable = options.requireCacheHit && isRetryableCertificationFailure(response, options.statusSource, options.retryPropagationFailures) || shouldRetryValidationFailure(response, target, options);
|
|
766
|
+
if (!lastRetryable) break;
|
|
767
|
+
} catch (error) {
|
|
768
|
+
lastSkippedReason = null;
|
|
769
|
+
lastRetryable = true;
|
|
770
|
+
if (error instanceof DOMException && error.name === "AbortError") {
|
|
771
|
+
if (phaseLimitedAttempt) return {
|
|
772
|
+
path: target.label,
|
|
773
|
+
ok: false,
|
|
774
|
+
error: phaseDeadlineError(),
|
|
775
|
+
retryable: false
|
|
776
|
+
};
|
|
777
|
+
lastError = `timed out after ${attemptTimeoutMs}ms`;
|
|
778
|
+
} else lastError = error instanceof Error ? error.message : String(error);
|
|
779
|
+
}
|
|
780
|
+
if (!canRetry(attempt)) break;
|
|
781
|
+
if (!await waitBeforeRetry()) return {
|
|
782
|
+
path: target.label,
|
|
783
|
+
ok: false,
|
|
784
|
+
error: phaseDeadlineError(),
|
|
785
|
+
retryable: false
|
|
786
|
+
};
|
|
787
|
+
}
|
|
788
|
+
return lastSkippedReason === null ? {
|
|
789
|
+
path: target.label,
|
|
115
790
|
ok: false,
|
|
116
|
-
error: lastError
|
|
791
|
+
error: lastError,
|
|
792
|
+
retryable: lastRetryable
|
|
793
|
+
} : {
|
|
794
|
+
path: target.label,
|
|
795
|
+
ok: true,
|
|
796
|
+
skipped: true,
|
|
797
|
+
reason: lastSkippedReason
|
|
117
798
|
};
|
|
118
799
|
}
|
|
119
800
|
async function runWithConcurrency(items, concurrency, fn) {
|
|
@@ -131,53 +812,170 @@ async function runWithConcurrency(items, concurrency, fn) {
|
|
|
131
812
|
return results;
|
|
132
813
|
}
|
|
133
814
|
async function warmCdnCache(options) {
|
|
134
|
-
const
|
|
135
|
-
const
|
|
136
|
-
const
|
|
137
|
-
|
|
815
|
+
const targets = await createCdnWarmTargets(options);
|
|
816
|
+
const requests = targets.filter((target) => target.kind !== "html");
|
|
817
|
+
const htmlRequests = targets.filter((target) => target.kind === "html");
|
|
818
|
+
requests.push(...htmlRequests);
|
|
819
|
+
const timeoutMs = Math.max(1, options.timeoutMs ?? 1e4);
|
|
820
|
+
const hasVersionOverride = new Headers(options.headers).has(WORKER_VERSION_OVERRIDE_HEADER);
|
|
821
|
+
const propagatingTarget = options.propagatingTarget ?? hasVersionOverride;
|
|
822
|
+
const concurrency = Math.max(1, options.concurrency ?? 25);
|
|
823
|
+
const normalRetries = Math.max(0, options.retries ?? 1);
|
|
824
|
+
const propagationRetries = Math.max(0, options.retries ?? 60);
|
|
825
|
+
const normalRetryDelayMs = Math.max(0, options.retryDelayMs ?? 0);
|
|
826
|
+
const propagationRetryDelayMs = Math.max(0, options.retryDelayMs ?? 1e3);
|
|
138
827
|
const fetchImpl = options.fetchImpl ?? fetch;
|
|
139
|
-
|
|
828
|
+
const phaseTimeoutMs = options.phaseTimeoutMs === void 0 ? void 0 : Math.max(1, options.phaseTimeoutMs);
|
|
829
|
+
const deadlineAt = phaseTimeoutMs === void 0 ? void 0 : Date.now() + phaseTimeoutMs;
|
|
830
|
+
if (requests.length === 0) return {
|
|
140
831
|
total: 0,
|
|
141
832
|
warmed: 0,
|
|
833
|
+
skipped: 0,
|
|
142
834
|
failed: 0,
|
|
143
|
-
failures: []
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
835
|
+
failures: [],
|
|
836
|
+
skippedTargets: [],
|
|
837
|
+
warmedPlan: {
|
|
838
|
+
loadingShellPaths: [],
|
|
839
|
+
pagesDataPaths: [],
|
|
840
|
+
paths: [],
|
|
841
|
+
rscPaths: []
|
|
842
|
+
},
|
|
843
|
+
retryPlan: {
|
|
844
|
+
loadingShellPaths: [],
|
|
845
|
+
pagesDataPaths: [],
|
|
846
|
+
paths: [],
|
|
847
|
+
rscPaths: []
|
|
848
|
+
}
|
|
849
|
+
};
|
|
850
|
+
console.log(`\n Warming ${formatCount(requests.length, "CDN cache entry", "CDN cache entries")}...`);
|
|
851
|
+
printCdnWarmRouteReport(requests);
|
|
852
|
+
const progress = new CdnOperationProgress();
|
|
853
|
+
let completedRequests = 0;
|
|
854
|
+
progress.update(0, requests.length, "starting warmup");
|
|
855
|
+
const warmTarget = (target, retryMode = "normal") => {
|
|
856
|
+
const isPropagationRequest = retryMode !== "normal";
|
|
857
|
+
const retries = retryMode === "propagation-retry" ? Math.max(0, propagationRetries - 1) : retryMode === "propagation-pass" ? 0 : normalRetries;
|
|
858
|
+
return warmOnePath(target, {
|
|
859
|
+
targetUrl: options.targetUrl,
|
|
860
|
+
timeoutMs,
|
|
861
|
+
retries,
|
|
862
|
+
deadlineAt,
|
|
863
|
+
fetchImpl,
|
|
864
|
+
headers: options.headers,
|
|
865
|
+
expectedBuildId: options.expectedBuildId,
|
|
866
|
+
expectedRscBuildId: options.expectedRscBuildId,
|
|
867
|
+
retryPropagationFailures: isPropagationRequest,
|
|
868
|
+
retryDelayMs: isPropagationRequest ? propagationRetryDelayMs : normalRetryDelayMs,
|
|
869
|
+
retryNotFound: isPropagationRequest,
|
|
870
|
+
phaseTimeoutMs,
|
|
871
|
+
requireCacheHit: options.requireCacheHit === true,
|
|
872
|
+
statusSource: options.statusSource ?? "cloudflare",
|
|
873
|
+
retrySkipped: isPropagationRequest && options.retrySkippedTargetKeys?.has(cdnWarmTargetKey(target)) === true
|
|
874
|
+
});
|
|
875
|
+
};
|
|
876
|
+
const warmRequest = async (target, retryMode = "normal") => {
|
|
877
|
+
const result = await warmTarget(target, retryMode);
|
|
878
|
+
completedRequests++;
|
|
879
|
+
progress.update(completedRequests, requests.length, target.label);
|
|
880
|
+
return result;
|
|
881
|
+
};
|
|
882
|
+
const warmPropagatingPass = async (targets) => {
|
|
883
|
+
const results = await runWithConcurrency(targets, concurrency, (target) => warmRequest(target, "propagation-pass"));
|
|
884
|
+
const retryable = targets.map((target, index) => ({
|
|
885
|
+
index,
|
|
886
|
+
result: results[index],
|
|
887
|
+
target
|
|
888
|
+
})).filter(({ result, target }) => result.ok ? result.skipped && options.retrySkippedTargetKeys?.has(cdnWarmTargetKey(target)) === true : result.retryable);
|
|
889
|
+
if (retryable.length === 0 || propagationRetries === 0) return results;
|
|
890
|
+
progress.finish();
|
|
891
|
+
console.log(` CDN warmup: retrying ${retryable.length} unresolved request(s) after completing the initial pass...`);
|
|
892
|
+
let completedRetries = 0;
|
|
893
|
+
progress.update(0, retryable.length, "starting retry pass", "Retrying CDN cache");
|
|
894
|
+
const retried = await runWithConcurrency(retryable.map(({ target }) => target), concurrency, async (target) => {
|
|
895
|
+
const result = await warmTarget(target, "propagation-retry");
|
|
896
|
+
completedRetries++;
|
|
897
|
+
progress.update(completedRetries, retryable.length, target.label, "Retrying CDN cache");
|
|
898
|
+
return result;
|
|
899
|
+
});
|
|
900
|
+
progress.finish();
|
|
901
|
+
for (const [retryIndex, { index }] of retryable.entries()) results[index] = retried[retryIndex];
|
|
902
|
+
return results;
|
|
903
|
+
};
|
|
904
|
+
let results;
|
|
905
|
+
if (propagatingTarget) results = await warmPropagatingPass(requests);
|
|
906
|
+
else results = await runWithConcurrency(requests, concurrency, (target) => warmRequest(target));
|
|
907
|
+
progress.finish();
|
|
908
|
+
const failedRequests = requests.map((target, index) => ({
|
|
909
|
+
result: results[index],
|
|
910
|
+
target
|
|
911
|
+
})).filter((entry) => !entry.result.ok);
|
|
912
|
+
const failures = failedRequests.map(({ result: { path, error } }) => ({
|
|
154
913
|
path,
|
|
155
914
|
error
|
|
156
915
|
}));
|
|
157
|
-
const
|
|
158
|
-
|
|
916
|
+
const skippedResults = results.filter((result) => result.ok && result.skipped);
|
|
917
|
+
const skippedTargets = requests.filter((_target, index) => {
|
|
918
|
+
const result = results[index];
|
|
919
|
+
return result.ok && result.skipped;
|
|
920
|
+
});
|
|
921
|
+
const warmedRequests = requests.filter((_target, index) => {
|
|
922
|
+
const result = results[index];
|
|
923
|
+
return result.ok && !result.skipped;
|
|
924
|
+
});
|
|
925
|
+
const warmed = results.length - failures.length - skippedResults.length;
|
|
926
|
+
const warmedRouteHandlerPaths = warmedRequests.filter((target) => target.kind === "app-route").map((target) => target.sourcePathname);
|
|
927
|
+
const failedRouteHandlerPaths = failedRequests.filter(({ target }) => target.kind === "app-route").map(({ target }) => target.sourcePathname);
|
|
928
|
+
printCdnWarmRouteReport(requests, results.map((result) => !result.ok ? "failed" : result.skipped ? "skipped" : "warmed"));
|
|
929
|
+
console.log(` CDN warmup: ${warmed} warmed, ${skippedResults.length} skipped, ${failures.length} failed.`);
|
|
930
|
+
for (const skipped of skippedResults.slice(0, 5)) console.log(` CDN warmup skipped ${skipped.path}: ${skipped.reason}`);
|
|
159
931
|
if (failures.length > 0) {
|
|
160
932
|
for (const failure of failures.slice(0, 5)) console.warn(` CDN warmup failed for ${failure.path}: ${failure.error}`);
|
|
161
933
|
if (failures.length > 5) console.warn(` CDN warmup: ${failures.length - 5} additional failure(s) omitted.`);
|
|
162
934
|
}
|
|
163
935
|
const result = {
|
|
164
|
-
total:
|
|
936
|
+
total: requests.length,
|
|
165
937
|
warmed,
|
|
938
|
+
skipped: skippedResults.length,
|
|
166
939
|
failed: failures.length,
|
|
167
|
-
failures
|
|
940
|
+
failures,
|
|
941
|
+
skippedTargets,
|
|
942
|
+
warmedPlan: {
|
|
943
|
+
loadingShellPaths: warmedRequests.filter((target) => target.kind === "rsc-loading-shell").map((target) => target.sourcePathname),
|
|
944
|
+
pagesDataPaths: warmedRequests.filter((target) => target.kind === "pages-data").map((target) => target.sourcePathname),
|
|
945
|
+
paths: warmedRequests.filter((target) => target.kind === "html").map((target) => target.sourcePathname),
|
|
946
|
+
rscPaths: warmedRequests.filter((target) => target.kind === "rsc-full").map((target) => target.sourcePathname),
|
|
947
|
+
...warmedRouteHandlerPaths.length > 0 ? { routeHandlerPaths: warmedRouteHandlerPaths } : {}
|
|
948
|
+
},
|
|
949
|
+
retryPlan: {
|
|
950
|
+
loadingShellPaths: failedRequests.filter(({ target }) => target.kind === "rsc-loading-shell").map(({ target }) => target.sourcePathname),
|
|
951
|
+
pagesDataPaths: failedRequests.filter(({ target }) => target.kind === "pages-data").map(({ target }) => target.sourcePathname),
|
|
952
|
+
paths: failedRequests.filter(({ target }) => target.kind === "html").map(({ target }) => target.sourcePathname),
|
|
953
|
+
rscPaths: failedRequests.filter(({ target }) => target.kind === "rsc-full").map(({ target }) => target.sourcePathname),
|
|
954
|
+
...failedRouteHandlerPaths.length > 0 ? { routeHandlerPaths: failedRouteHandlerPaths } : {}
|
|
955
|
+
}
|
|
168
956
|
};
|
|
169
|
-
if (options.strict && failures.length > 0) throw new Error(`CDN warmup failed for ${failures.length}/${
|
|
957
|
+
if (options.strict && failures.length > 0) throw new Error(`CDN warmup failed for ${failures.length}/${requests.length} request(s). First failure: ${failures[0].path}: ${failures[0].error}`);
|
|
170
958
|
return result;
|
|
171
959
|
}
|
|
172
960
|
async function warmCdnCacheFromPrerender(options) {
|
|
173
|
-
const
|
|
961
|
+
const plan = readPrerenderWarmPlan(options.root, {
|
|
174
962
|
includeFallbackShells: options.includeFallbackShells,
|
|
175
963
|
strict: options.strict
|
|
176
964
|
});
|
|
965
|
+
const warmPlan = {
|
|
966
|
+
deploymentId: plan.deploymentId,
|
|
967
|
+
loadingShellPaths: plan.loadingShellPaths,
|
|
968
|
+
pagesDataPaths: plan.pagesDataPaths,
|
|
969
|
+
paths: plan.paths,
|
|
970
|
+
routeHandlerPaths: plan.routeHandlerPaths,
|
|
971
|
+
rscPaths: plan.rscPaths
|
|
972
|
+
};
|
|
177
973
|
return warmCdnCache({
|
|
178
974
|
...options,
|
|
179
|
-
|
|
975
|
+
...warmPlan,
|
|
976
|
+
expectedBuildId: options.expectedBuildId ?? (options.validateBuildIdentity === false ? void 0 : plan.buildIdentity),
|
|
977
|
+
expectedRscBuildId: options.expectedRscBuildId ?? plan.rscBuildId
|
|
180
978
|
});
|
|
181
979
|
}
|
|
182
980
|
//#endregion
|
|
183
|
-
export { DEFAULT_CDN_WARM_TIMEOUT_MS, buildWarmupUrl, getWarmPathsFromPrerenderManifest, readPrerenderWarmPaths, warmCdnCache, warmCdnCacheFromPrerender };
|
|
981
|
+
export { CdnOperationProgress, DEFAULT_CDN_WARM_CONCURRENCY, DEFAULT_CDN_WARM_TIMEOUT_MS, DEFAULT_STAGED_READINESS_INTERVAL_MS, DEFAULT_STAGED_READINESS_PHASE_TIMEOUT_MS, DEFAULT_STAGED_READINESS_RETRIES, buildWarmupUrl, createCdnWarmTargets, createPrerenderWarmPlan, getWarmPathsFromPrerenderManifest, readPrerenderWarmPaths, readPrerenderWarmPlan, waitForCdnWarmTargetReadiness, warmCdnCache, warmCdnCacheFromPrerender };
|