@vinext/cloudflare 1.0.0-beta.6 → 1.0.0-beta.7
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 +17 -0
- package/dist/cache/cdn-adapter-config.d.ts +22 -0
- package/dist/cache/cdn-adapter-config.js +47 -0
- package/dist/cache/cdn-adapter.d.ts +34 -3
- package/dist/cache/cdn-adapter.js +34 -2
- package/dist/cache/cdn-adapter.runtime.d.ts +17 -1
- package/dist/cache/cdn-adapter.runtime.js +80 -5
- package/dist/cache/cdn-build-id.d.ts +5 -0
- package/dist/cache/cdn-build-id.js +5 -0
- package/dist/cache/kv-data-adapter.runtime.js +1 -1
- package/dist/cacheability-artifact.d.ts +10 -0
- package/dist/cacheability-artifact.js +165 -0
- package/dist/cacheability-manifest-limits.d.ts +5 -0
- package/dist/cacheability-manifest-limits.js +7 -0
- package/dist/cacheability-probe.d.ts +50 -0
- package/dist/cacheability-probe.js +496 -0
- package/dist/cdn-warm.d.ts +95 -2
- package/dist/cdn-warm.js +704 -80
- package/dist/cli.js +15 -1
- package/dist/deploy-config.d.ts +5 -1
- package/dist/deploy-config.js +15 -2
- package/dist/deploy-help.js +43 -6
- package/dist/deploy.d.ts +61 -6
- package/dist/deploy.js +631 -82
- package/dist/prerender-kv-populate.js +1 -1
- package/dist/tpr.js +2 -2
- package/dist/version-deploy.d.ts +12 -9
- package/dist/version-deploy.js +24 -10
- package/dist/version-headers.d.ts +2 -0
- package/dist/version-headers.js +2 -0
- package/dist/worker-deployment-url.d.ts +7 -1
- package/dist/worker-deployment-url.js +23 -1
- package/dist/wrangler-version-metadata.d.ts +8 -0
- package/dist/wrangler-version-metadata.js +9 -0
- package/package.json +4 -3
|
@@ -0,0 +1,496 @@
|
|
|
1
|
+
import { MAX_CACHEABILITY_MANIFEST_BYTES, cacheabilityManifestByteLimitError } from "./cacheability-manifest-limits.js";
|
|
2
|
+
import "./cache/cdn-build-id.js";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { Buffer } from "node:buffer";
|
|
6
|
+
import { cacheabilityManifestRouteKey, cacheabilityRequestIdentity, cacheabilityRoutePathname, normalizeCacheabilityRoutePathname } from "vinext/internal/server/cacheability-manifest";
|
|
7
|
+
import { randomUUID } from "node:crypto";
|
|
8
|
+
import { setTimeout as setTimeout$1 } from "node:timers/promises";
|
|
9
|
+
import { VINEXT_CACHEABILITY_PROBE_HEADER, VINEXT_CACHEABILITY_PROBE_QUERY_PARAM, VINEXT_PRERENDER_SECRET_HEADER } from "vinext/internal/server/headers";
|
|
10
|
+
//#region src/cacheability-probe.ts
|
|
11
|
+
const DEFAULT_CACHEABILITY_PROBE_PHASE_TIMEOUT_MS = 12e4;
|
|
12
|
+
const DEFAULT_CACHEABILITY_PROBE_RETRIES = 2;
|
|
13
|
+
const DEFAULT_CACHEABILITY_PROBE_RETRY_DELAY_MS = 1e3;
|
|
14
|
+
const DEFAULT_CACHEABILITY_PROBE_REQUEST_TIMEOUT_MS = 3e4;
|
|
15
|
+
const MAX_CACHEABILITY_PROBE_ENVELOPE_BYTES = 64 * 1024;
|
|
16
|
+
function sharedPathPrefix(pathnames) {
|
|
17
|
+
if (pathnames.length === 0) return null;
|
|
18
|
+
let prefix = pathnames[0];
|
|
19
|
+
for (const pathname of pathnames.slice(1)) {
|
|
20
|
+
const limit = Math.min(prefix.length, pathname.length);
|
|
21
|
+
let length = 0;
|
|
22
|
+
while (length < limit && prefix.charCodeAt(length) === pathname.charCodeAt(length)) length++;
|
|
23
|
+
prefix = prefix.slice(0, length);
|
|
24
|
+
}
|
|
25
|
+
while (prefix.length > 1 && pathnames.some((pathname) => pathname !== prefix && (prefix.endsWith("/") ? !pathname.startsWith(prefix) : !pathname.startsWith(`${prefix}/`)))) {
|
|
26
|
+
const slash = prefix.lastIndexOf("/");
|
|
27
|
+
prefix = slash <= 0 ? "/" : prefix.slice(0, slash + 1);
|
|
28
|
+
}
|
|
29
|
+
return prefix || null;
|
|
30
|
+
}
|
|
31
|
+
/** Remove a shared pathname prefix only when it makes the serialized route smaller. */
|
|
32
|
+
function compactManifestRoutePaths(route) {
|
|
33
|
+
const pathPrefix = sharedPathPrefix([...route.runtimePaths ?? [], ...Object.values(route.staticPaths ?? {}).flatMap((paths) => paths ?? [])]);
|
|
34
|
+
if (!pathPrefix) return route;
|
|
35
|
+
const compacted = {
|
|
36
|
+
...route,
|
|
37
|
+
pathPrefix,
|
|
38
|
+
...route.runtimePaths ? { runtimePaths: route.runtimePaths.map((pathname) => pathname.slice(pathPrefix.length)) } : {},
|
|
39
|
+
...route.staticPaths ? { staticPaths: Object.fromEntries(Object.entries(route.staticPaths).map(([representation, paths]) => [representation, paths.map((pathname) => pathname.slice(pathPrefix.length))])) } : {}
|
|
40
|
+
};
|
|
41
|
+
return Buffer.byteLength(JSON.stringify(compacted)) < Buffer.byteLength(JSON.stringify(route)) ? compacted : route;
|
|
42
|
+
}
|
|
43
|
+
function isProbeRouteState(value) {
|
|
44
|
+
return value === "static-candidate" || value === "dynamic" || value === "probe-failed";
|
|
45
|
+
}
|
|
46
|
+
function readPrerenderSecret(root) {
|
|
47
|
+
const manifestPath = path.join(root, "dist", "server", "vinext-server.json");
|
|
48
|
+
const parsed = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
|
|
49
|
+
const secret = parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed.prerenderSecret : void 0;
|
|
50
|
+
if (typeof secret !== "string" || secret.length === 0) throw new Error("Cannot probe staged Worker cacheability because dist/server/vinext-server.json does not contain a prerender secret. Rebuild the app before deploying.");
|
|
51
|
+
return secret;
|
|
52
|
+
}
|
|
53
|
+
async function readProbeEnvelope(response) {
|
|
54
|
+
if (!response.body) return {
|
|
55
|
+
reason: "probe returned invalid JSON",
|
|
56
|
+
state: "probe-failed",
|
|
57
|
+
version: 1
|
|
58
|
+
};
|
|
59
|
+
const reader = response.body.getReader();
|
|
60
|
+
const chunks = [];
|
|
61
|
+
let bytes = 0;
|
|
62
|
+
try {
|
|
63
|
+
while (true) {
|
|
64
|
+
const result = await reader.read();
|
|
65
|
+
if (result.done) break;
|
|
66
|
+
bytes += result.value.byteLength;
|
|
67
|
+
if (bytes > MAX_CACHEABILITY_PROBE_ENVELOPE_BYTES) {
|
|
68
|
+
await reader.cancel().catch(() => {});
|
|
69
|
+
return {
|
|
70
|
+
reason: `probe response exceeded ${MAX_CACHEABILITY_PROBE_ENVELOPE_BYTES} bytes`,
|
|
71
|
+
state: "probe-failed",
|
|
72
|
+
version: 1
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
chunks.push(result.value);
|
|
76
|
+
}
|
|
77
|
+
} catch (error) {
|
|
78
|
+
return {
|
|
79
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
80
|
+
state: "probe-failed",
|
|
81
|
+
version: 1
|
|
82
|
+
};
|
|
83
|
+
} finally {
|
|
84
|
+
reader.releaseLock();
|
|
85
|
+
}
|
|
86
|
+
try {
|
|
87
|
+
return JSON.parse(Buffer.concat(chunks, bytes).toString("utf8"));
|
|
88
|
+
} catch {
|
|
89
|
+
return {
|
|
90
|
+
reason: "probe returned invalid JSON",
|
|
91
|
+
state: "probe-failed",
|
|
92
|
+
version: 1
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
async function probeTarget(options) {
|
|
97
|
+
const headers = new Headers(options.headers);
|
|
98
|
+
for (const [name, value] of new Headers(options.target.headers)) headers.set(name, value);
|
|
99
|
+
headers.set("Cache-Control", "no-cache");
|
|
100
|
+
headers.set(VINEXT_CACHEABILITY_PROBE_HEADER, "1");
|
|
101
|
+
headers.set(VINEXT_PRERENDER_SECRET_HEADER, options.secret);
|
|
102
|
+
let reason = "probe failed";
|
|
103
|
+
const probeId = randomUUID();
|
|
104
|
+
const phaseTimeoutPayload = () => ({
|
|
105
|
+
phaseTimedOut: true,
|
|
106
|
+
reason: `cacheability probing made no progress for ${options.phaseTimeoutMs}ms`,
|
|
107
|
+
state: "probe-failed",
|
|
108
|
+
version: 1
|
|
109
|
+
});
|
|
110
|
+
for (let attempt = 0; attempt <= options.retries; attempt++) {
|
|
111
|
+
if (options.getDeadlineAt() - Date.now() <= 0) return phaseTimeoutPayload();
|
|
112
|
+
const controller = new AbortController();
|
|
113
|
+
const requestDeadlineAt = Date.now() + options.timeoutMs;
|
|
114
|
+
let timeout;
|
|
115
|
+
let timedOutBy = null;
|
|
116
|
+
let retryable = true;
|
|
117
|
+
try {
|
|
118
|
+
const url = new URL(options.target.pathname, options.targetUrl);
|
|
119
|
+
url.searchParams.set(VINEXT_CACHEABILITY_PROBE_QUERY_PARAM, `${probeId}-${attempt}`);
|
|
120
|
+
const request = (async () => {
|
|
121
|
+
const response = await options.fetchImpl(url, {
|
|
122
|
+
headers,
|
|
123
|
+
redirect: "manual",
|
|
124
|
+
signal: controller.signal
|
|
125
|
+
});
|
|
126
|
+
if (options.expectedBuildId !== void 0 && response.headers.get("X-Vinext-Build-Id") !== options.expectedBuildId) {
|
|
127
|
+
response.body?.cancel().catch(() => {});
|
|
128
|
+
return {
|
|
129
|
+
kind: "retry",
|
|
130
|
+
reason: "probe reached an unexpected Worker build",
|
|
131
|
+
retryable: true
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
if (!response.ok) {
|
|
135
|
+
response.body?.cancel().catch(() => {});
|
|
136
|
+
return {
|
|
137
|
+
kind: "retry",
|
|
138
|
+
reason: `probe returned HTTP ${response.status}`,
|
|
139
|
+
retryable: response.status === 404 || response.status === 503
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
return {
|
|
143
|
+
kind: "complete",
|
|
144
|
+
payload: await readProbeEnvelope(response)
|
|
145
|
+
};
|
|
146
|
+
})();
|
|
147
|
+
const timedOut = new Promise((_resolve, reject) => {
|
|
148
|
+
const checkDeadline = () => {
|
|
149
|
+
const phaseDeadlineAt = options.getDeadlineAt();
|
|
150
|
+
const remainingMs = Math.min(requestDeadlineAt, phaseDeadlineAt) - Date.now();
|
|
151
|
+
if (remainingMs > 0) {
|
|
152
|
+
timeout = setTimeout(checkDeadline, remainingMs);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
timedOutBy = requestDeadlineAt <= phaseDeadlineAt ? "request" : "phase";
|
|
156
|
+
controller.abort();
|
|
157
|
+
reject(new DOMException("Probe deadline exceeded", "AbortError"));
|
|
158
|
+
};
|
|
159
|
+
checkDeadline();
|
|
160
|
+
});
|
|
161
|
+
const result = await Promise.race([request, timedOut]);
|
|
162
|
+
if (Date.now() >= options.getDeadlineAt()) return phaseTimeoutPayload();
|
|
163
|
+
if (result.kind === "complete") return result.payload;
|
|
164
|
+
reason = result.reason;
|
|
165
|
+
retryable = result.retryable;
|
|
166
|
+
} catch (error) {
|
|
167
|
+
if (timedOutBy === "phase" || Date.now() >= options.getDeadlineAt()) return phaseTimeoutPayload();
|
|
168
|
+
reason = error instanceof Error && error.name === "AbortError" ? `probe timed out after ${options.timeoutMs}ms` : error instanceof Error ? error.message : String(error);
|
|
169
|
+
} finally {
|
|
170
|
+
if (timeout !== void 0) clearTimeout(timeout);
|
|
171
|
+
}
|
|
172
|
+
if (!retryable || attempt === options.retries) break;
|
|
173
|
+
if (options.retryDelayMs > 0) {
|
|
174
|
+
const delayMs = Math.min(options.retryDelayMs, Math.max(0, options.getDeadlineAt() - Date.now()));
|
|
175
|
+
if (delayMs <= 0) return phaseTimeoutPayload();
|
|
176
|
+
await setTimeout$1(delayMs);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return {
|
|
180
|
+
reason,
|
|
181
|
+
state: "probe-failed",
|
|
182
|
+
version: 1
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
async function probeStagedWorkerCacheability(options) {
|
|
186
|
+
const secret = readPrerenderSecret(options.root);
|
|
187
|
+
const concurrency = Math.max(1, options.concurrency ?? 25);
|
|
188
|
+
const retries = Math.max(0, options.retries ?? 2);
|
|
189
|
+
const retryDelayMs = Math.max(0, options.retryDelayMs ?? 1e3);
|
|
190
|
+
const timeoutMs = Math.max(1, options.timeoutMs ?? DEFAULT_CACHEABILITY_PROBE_REQUEST_TIMEOUT_MS);
|
|
191
|
+
const phaseTimeoutMs = Math.max(1, options.phaseTimeoutMs ?? 12e4);
|
|
192
|
+
let lastProgressAt = Date.now();
|
|
193
|
+
const getDeadlineAt = () => lastProgressAt + phaseTimeoutMs;
|
|
194
|
+
const routes = {};
|
|
195
|
+
const cacheableTargets = [];
|
|
196
|
+
const speculativeTargets = [];
|
|
197
|
+
const failures = [];
|
|
198
|
+
const maxManifestBytes = Math.min(options.manifestLimits?.maxBytes ?? 2097152, MAX_CACHEABILITY_MANIFEST_BYTES);
|
|
199
|
+
const emptyManifest = {
|
|
200
|
+
buildId: options.buildId,
|
|
201
|
+
routes: {},
|
|
202
|
+
version: 1
|
|
203
|
+
};
|
|
204
|
+
let manifestBytes = Buffer.byteLength(JSON.stringify(emptyManifest));
|
|
205
|
+
const routeEntryBytes = /* @__PURE__ */ new Map();
|
|
206
|
+
let limitFailure = null;
|
|
207
|
+
let phaseTimedOut = false;
|
|
208
|
+
let probed = 0;
|
|
209
|
+
let completedPathCount = 0;
|
|
210
|
+
let skippedPathCount = 0;
|
|
211
|
+
let staticPathCount = 0;
|
|
212
|
+
let dynamicPathCount = 0;
|
|
213
|
+
const targetPreference = (target) => {
|
|
214
|
+
if (target.route?.kind === "app-route") return target.kind === "app-route" ? 0 : 1;
|
|
215
|
+
return target.kind === "html" ? 0 : target.kind === "rsc-full" ? 1 : 2;
|
|
216
|
+
};
|
|
217
|
+
const patterns = /* @__PURE__ */ new Map();
|
|
218
|
+
const targetsByConcretePath = /* @__PURE__ */ new Map();
|
|
219
|
+
let missingRouteMetadata = 0;
|
|
220
|
+
for (const target of options.targets) {
|
|
221
|
+
if (!target.route) {
|
|
222
|
+
missingRouteMetadata++;
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
const key = cacheabilityManifestRouteKey(target.route.kind, target.route.pattern);
|
|
226
|
+
const pattern = patterns.get(key) ?? {
|
|
227
|
+
canPrune: true,
|
|
228
|
+
groups: [],
|
|
229
|
+
key,
|
|
230
|
+
pathnames: /* @__PURE__ */ new Set(),
|
|
231
|
+
pruned: false,
|
|
232
|
+
results: /* @__PURE__ */ new Map(),
|
|
233
|
+
route: target.route
|
|
234
|
+
};
|
|
235
|
+
pattern.canPrune &&= target.route.cacheabilityProbe?.canPrunePattern === true;
|
|
236
|
+
patterns.set(key, pattern);
|
|
237
|
+
const routePathname = target.route.cacheabilityProbe?.concretePathname ?? cacheabilityRoutePathname(target.pathname, target.kind);
|
|
238
|
+
pattern.pathnames.add(routePathname);
|
|
239
|
+
const concreteKey = `${key}\0${routePathname}`;
|
|
240
|
+
const groupTargets = targetsByConcretePath.get(concreteKey) ?? [];
|
|
241
|
+
groupTargets.push(target);
|
|
242
|
+
targetsByConcretePath.set(concreteKey, groupTargets);
|
|
243
|
+
}
|
|
244
|
+
if (missingRouteMetadata > 0) failures.push(`${missingRouteMetadata} warm target${missingRouteMetadata === 1 ? " is" : "s are"} missing route-pattern metadata`);
|
|
245
|
+
const groups = Array.from(targetsByConcretePath, ([concreteKey, groupTargets]) => {
|
|
246
|
+
const route = groupTargets[0].route;
|
|
247
|
+
const key = cacheabilityManifestRouteKey(route.kind, route.pattern);
|
|
248
|
+
const pattern = patterns.get(key);
|
|
249
|
+
const routePathname = concreteKey.slice(key.length + 1);
|
|
250
|
+
groupTargets.sort((first, second) => {
|
|
251
|
+
return targetPreference(first) - targetPreference(second) || first.sourcePathname.localeCompare(second.sourcePathname);
|
|
252
|
+
});
|
|
253
|
+
const group = {
|
|
254
|
+
pattern,
|
|
255
|
+
primary: groupTargets[0],
|
|
256
|
+
routePathname,
|
|
257
|
+
targets: groupTargets
|
|
258
|
+
};
|
|
259
|
+
pattern.groups.push(group);
|
|
260
|
+
return group;
|
|
261
|
+
});
|
|
262
|
+
const reportProgress = () => {
|
|
263
|
+
options.onProgress?.({
|
|
264
|
+
completed: completedPathCount + (missingRouteMetadata > 0 ? 1 : 0),
|
|
265
|
+
dynamic: dynamicPathCount,
|
|
266
|
+
failed: failures.length,
|
|
267
|
+
probed,
|
|
268
|
+
skipped: skippedPathCount,
|
|
269
|
+
static: staticPathCount,
|
|
270
|
+
total: groups.length + (missingRouteMetadata > 0 ? 1 : 0)
|
|
271
|
+
});
|
|
272
|
+
};
|
|
273
|
+
const addRouteWithinManifestLimits = (key, route) => {
|
|
274
|
+
const previousBytes = routeEntryBytes.get(key);
|
|
275
|
+
const nextEntryBytes = Buffer.byteLength(`${JSON.stringify(key)}:${JSON.stringify(route)}`);
|
|
276
|
+
const nextManifestBytes = previousBytes === void 0 ? manifestBytes + nextEntryBytes + (routeEntryBytes.size > 0 ? 1 : 0) : manifestBytes - previousBytes + nextEntryBytes;
|
|
277
|
+
if (nextManifestBytes > maxManifestBytes) {
|
|
278
|
+
limitFailure = cacheabilityManifestByteLimitError(nextManifestBytes, maxManifestBytes);
|
|
279
|
+
return false;
|
|
280
|
+
}
|
|
281
|
+
routes[key] = route;
|
|
282
|
+
routeEntryBytes.set(key, nextEntryBytes);
|
|
283
|
+
manifestBytes = nextManifestBytes;
|
|
284
|
+
return true;
|
|
285
|
+
};
|
|
286
|
+
const classifyConcretePath = async (group) => {
|
|
287
|
+
if (group.pattern.pruned) {
|
|
288
|
+
skippedPathCount += 1;
|
|
289
|
+
completedPathCount += 1;
|
|
290
|
+
reportProgress();
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
const target = group.primary;
|
|
294
|
+
const identity = cacheabilityRequestIdentity(new Request(new URL(target.pathname, options.targetUrl), { headers: target.headers }));
|
|
295
|
+
if (!identity || identity.representation !== target.kind) {
|
|
296
|
+
failures.push(`${target.label}: warm request does not have a cacheable request identity`);
|
|
297
|
+
completedPathCount += 1;
|
|
298
|
+
reportProgress();
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
const result = await probeTarget({
|
|
302
|
+
expectedBuildId: options.expectedResponseBuildId,
|
|
303
|
+
fetchImpl: options.fetchImpl ?? fetch,
|
|
304
|
+
getDeadlineAt,
|
|
305
|
+
headers: options.headers,
|
|
306
|
+
retries,
|
|
307
|
+
retryDelayMs,
|
|
308
|
+
phaseTimeoutMs,
|
|
309
|
+
secret,
|
|
310
|
+
target,
|
|
311
|
+
targetUrl: options.targetUrl,
|
|
312
|
+
timeoutMs
|
|
313
|
+
});
|
|
314
|
+
probed += 1;
|
|
315
|
+
lastProgressAt = Date.now();
|
|
316
|
+
if (limitFailure) return;
|
|
317
|
+
if (result.phaseTimedOut) {
|
|
318
|
+
phaseTimedOut = true;
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
if (result.version !== 1 || result.kind !== "app-page" && result.kind !== "app-route" && result.kind !== "pages-page" || typeof result.pattern !== "string" || !result.pattern.startsWith("/") || !isProbeRouteState(result.state) || result.scope !== void 0 && result.scope !== "identity" && result.scope !== "pattern" || result.scope === "pattern" && result.state !== "dynamic" || result.rendererStatic !== void 0 && typeof result.rendererStatic !== "boolean" || !Number.isInteger(result.status) || result.status < 100 || result.status > 599) {
|
|
322
|
+
failures.push(`${target.label}: ${result.reason ?? "probe returned an invalid envelope"}`);
|
|
323
|
+
completedPathCount += 1;
|
|
324
|
+
reportProgress();
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
if (result.state === "probe-failed") {
|
|
328
|
+
failures.push(`${target.label}: ${result.reason ?? "probe failed"}`);
|
|
329
|
+
completedPathCount += 1;
|
|
330
|
+
reportProgress();
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
if (!target.route || result.kind !== target.route.kind || result.pattern !== target.route.pattern) {
|
|
334
|
+
failures.push(`${target.label}: probe resolved to unexpected route ${result.pattern ?? ""}`);
|
|
335
|
+
completedPathCount += 1;
|
|
336
|
+
reportProgress();
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
if (result.state === "dynamic" && result.scope === "pattern" && group.pattern.canPrune) {
|
|
340
|
+
group.pattern.pruned = true;
|
|
341
|
+
dynamicPathCount += 1;
|
|
342
|
+
completedPathCount += 1;
|
|
343
|
+
reportProgress();
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
group.pattern.results.set(group.routePathname, {
|
|
347
|
+
rendererStatic: result.rendererStatic === true,
|
|
348
|
+
representation: target.kind,
|
|
349
|
+
state: result.state
|
|
350
|
+
});
|
|
351
|
+
if (result.state === "static-candidate") staticPathCount += 1;
|
|
352
|
+
else dynamicPathCount += 1;
|
|
353
|
+
completedPathCount += 1;
|
|
354
|
+
reportProgress();
|
|
355
|
+
};
|
|
356
|
+
const runGroups = async (scheduledGroups) => {
|
|
357
|
+
let nextIndex = 0;
|
|
358
|
+
const worker = async () => {
|
|
359
|
+
while (!limitFailure && !phaseTimedOut && nextIndex < scheduledGroups.length) await classifyConcretePath(scheduledGroups[nextIndex++]);
|
|
360
|
+
};
|
|
361
|
+
await Promise.all(Array.from({ length: Math.min(concurrency, scheduledGroups.length) }, () => worker()));
|
|
362
|
+
};
|
|
363
|
+
reportProgress();
|
|
364
|
+
const representativeGroups = [];
|
|
365
|
+
const siblingGroups = [];
|
|
366
|
+
const scheduledPatterns = /* @__PURE__ */ new Set();
|
|
367
|
+
for (const group of groups) if (scheduledPatterns.has(group.pattern.key)) siblingGroups.push(group);
|
|
368
|
+
else {
|
|
369
|
+
scheduledPatterns.add(group.pattern.key);
|
|
370
|
+
representativeGroups.push(group);
|
|
371
|
+
}
|
|
372
|
+
await runGroups(representativeGroups);
|
|
373
|
+
if (!limitFailure && !phaseTimedOut) await runGroups(siblingGroups);
|
|
374
|
+
if (limitFailure) throw limitFailure;
|
|
375
|
+
if (phaseTimedOut || Date.now() >= getDeadlineAt()) throw new Error(`cacheability probing made no progress for ${phaseTimeoutMs}ms`);
|
|
376
|
+
let classified = 0;
|
|
377
|
+
let dynamic = 0;
|
|
378
|
+
let skipped = 0;
|
|
379
|
+
const fallbackRoutes = /* @__PURE__ */ new Map();
|
|
380
|
+
for (const fallbackRoute of options.fallbackRoutePatterns ?? []) {
|
|
381
|
+
const key = cacheabilityManifestRouteKey(fallbackRoute.kind, fallbackRoute.pattern);
|
|
382
|
+
fallbackRoutes.set(key, fallbackRoute);
|
|
383
|
+
}
|
|
384
|
+
for (const [key, fallbackRoute] of fallbackRoutes) {
|
|
385
|
+
if (patterns.has(key)) continue;
|
|
386
|
+
if (!addRouteWithinManifestLimits(key, {
|
|
387
|
+
kind: fallbackRoute.kind,
|
|
388
|
+
pattern: fallbackRoute.pattern,
|
|
389
|
+
state: "static-candidate"
|
|
390
|
+
})) break;
|
|
391
|
+
classified += 1;
|
|
392
|
+
}
|
|
393
|
+
if (limitFailure) throw limitFailure;
|
|
394
|
+
for (const pattern of patterns.values()) {
|
|
395
|
+
if (pattern.pruned) {
|
|
396
|
+
classified += 1;
|
|
397
|
+
dynamic += 1;
|
|
398
|
+
skipped += 1;
|
|
399
|
+
const loadingShellTargets = pattern.groups.flatMap((group) => group.targets.filter((target) => target.kind === "rsc-loading-shell"));
|
|
400
|
+
if (loadingShellTargets.length > 0) {
|
|
401
|
+
cacheableTargets.push(...loadingShellTargets);
|
|
402
|
+
speculativeTargets.push(...loadingShellTargets);
|
|
403
|
+
const route = {
|
|
404
|
+
kind: pattern.route.kind,
|
|
405
|
+
pattern: pattern.route.pattern,
|
|
406
|
+
runtimeRepresentation: "rsc-loading-shell",
|
|
407
|
+
state: "runtime-check"
|
|
408
|
+
};
|
|
409
|
+
if (!addRouteWithinManifestLimits(pattern.key, route)) break;
|
|
410
|
+
}
|
|
411
|
+
continue;
|
|
412
|
+
}
|
|
413
|
+
if (pattern.results.size === 0) continue;
|
|
414
|
+
classified += 1;
|
|
415
|
+
if (Array.from(pattern.results.values()).some((result) => result.state === "dynamic")) dynamic += 1;
|
|
416
|
+
const staticPaths = {};
|
|
417
|
+
const runtimePathSet = /* @__PURE__ */ new Set();
|
|
418
|
+
for (const group of pattern.groups) {
|
|
419
|
+
const result = pattern.results.get(group.routePathname);
|
|
420
|
+
if (result?.state === "static-candidate") {
|
|
421
|
+
if (result.rendererStatic) {
|
|
422
|
+
const paths = staticPaths[result.representation] ?? [];
|
|
423
|
+
paths.push(group.routePathname);
|
|
424
|
+
staticPaths[result.representation] = paths;
|
|
425
|
+
} else runtimePathSet.add(group.routePathname);
|
|
426
|
+
cacheableTargets.push(...group.targets);
|
|
427
|
+
speculativeTargets.push(...group.targets.filter((target) => target !== group.primary));
|
|
428
|
+
continue;
|
|
429
|
+
}
|
|
430
|
+
runtimePathSet.add(group.routePathname);
|
|
431
|
+
if (!pattern.pruned) {
|
|
432
|
+
const pairedTargets = group.targets.filter((target) => target !== group.primary);
|
|
433
|
+
cacheableTargets.push(...pairedTargets);
|
|
434
|
+
speculativeTargets.push(...pairedTargets);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
for (const paths of Object.values(staticPaths)) paths?.sort();
|
|
438
|
+
const allObservedPathsStaticallyGenerated = pattern.results.size === pattern.pathnames.size && runtimePathSet.size === 0 && Array.from(pattern.results.values()).every((result) => result.rendererStatic);
|
|
439
|
+
const hasStaticFallback = fallbackRoutes.has(pattern.key);
|
|
440
|
+
const soleGroup = pattern.groups.length === 1 ? pattern.groups[0] : null;
|
|
441
|
+
const literalPatternNamesSolePath = soleGroup !== null && !/(^|\/):/.test(pattern.route.pattern) && normalizeCacheabilityRoutePathname(pattern.route.pattern) === soleGroup.routePathname;
|
|
442
|
+
let route;
|
|
443
|
+
if (literalPatternNamesSolePath) {
|
|
444
|
+
const result = pattern.results.get(soleGroup.routePathname);
|
|
445
|
+
route = result?.state === "static-candidate" ? pattern.route.kind === "app-route" ? {
|
|
446
|
+
kind: pattern.route.kind,
|
|
447
|
+
pattern: pattern.route.pattern,
|
|
448
|
+
state: "static-candidate"
|
|
449
|
+
} : result.rendererStatic ? {
|
|
450
|
+
kind: pattern.route.kind,
|
|
451
|
+
pattern: pattern.route.pattern,
|
|
452
|
+
state: "runtime-check",
|
|
453
|
+
staticRepresentation: result.representation
|
|
454
|
+
} : {
|
|
455
|
+
kind: pattern.route.kind,
|
|
456
|
+
pattern: pattern.route.pattern,
|
|
457
|
+
state: "runtime-check"
|
|
458
|
+
} : {
|
|
459
|
+
kind: pattern.route.kind,
|
|
460
|
+
pattern: pattern.route.pattern,
|
|
461
|
+
state: "runtime-check"
|
|
462
|
+
};
|
|
463
|
+
} else route = compactManifestRoutePaths({
|
|
464
|
+
kind: pattern.route.kind,
|
|
465
|
+
pattern: pattern.route.pattern,
|
|
466
|
+
state: "runtime-check",
|
|
467
|
+
...hasStaticFallback || allObservedPathsStaticallyGenerated ? {
|
|
468
|
+
allowUnknown: true,
|
|
469
|
+
unknownState: "static-candidate"
|
|
470
|
+
} : {},
|
|
471
|
+
...runtimePathSet.size > 0 ? { runtimePaths: Array.from(runtimePathSet).sort() } : {},
|
|
472
|
+
...Object.keys(staticPaths).length > 0 ? { staticPaths } : {}
|
|
473
|
+
});
|
|
474
|
+
if (!addRouteWithinManifestLimits(pattern.key, route)) break;
|
|
475
|
+
}
|
|
476
|
+
if (limitFailure) throw limitFailure;
|
|
477
|
+
const sortedRoutes = Object.fromEntries(Object.entries(routes).sort(([first], [second]) => first.localeCompare(second)));
|
|
478
|
+
cacheableTargets.sort((first, second) => `${first.kind}\0${first.sourcePathname}`.localeCompare(`${second.kind}\0${second.sourcePathname}`));
|
|
479
|
+
speculativeTargets.sort((first, second) => `${first.kind}\0${first.sourcePathname}`.localeCompare(`${second.kind}\0${second.sourcePathname}`));
|
|
480
|
+
return {
|
|
481
|
+
cacheableTargets,
|
|
482
|
+
classified,
|
|
483
|
+
dynamic,
|
|
484
|
+
failures,
|
|
485
|
+
manifest: {
|
|
486
|
+
buildId: options.buildId,
|
|
487
|
+
routes: sortedRoutes,
|
|
488
|
+
version: 1
|
|
489
|
+
},
|
|
490
|
+
probed,
|
|
491
|
+
skipped,
|
|
492
|
+
speculativeTargets
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
//#endregion
|
|
496
|
+
export { DEFAULT_CACHEABILITY_PROBE_PHASE_TIMEOUT_MS, DEFAULT_CACHEABILITY_PROBE_RETRIES, DEFAULT_CACHEABILITY_PROBE_RETRY_DELAY_MS, probeStagedWorkerCacheability, readPrerenderSecret };
|
package/dist/cdn-warm.d.ts
CHANGED
|
@@ -1,36 +1,129 @@
|
|
|
1
|
+
import { PrerenderRoutePattern } from "vinext/internal/build/prerender-paths";
|
|
1
2
|
import { PrerenderManifest, PrerenderedPathSelectionOptions } from "vinext/internal/server/prerender-manifest";
|
|
2
3
|
//#region src/cdn-warm.d.ts
|
|
3
4
|
type CdnWarmOptions = {
|
|
4
5
|
targetUrl: string;
|
|
5
6
|
paths: readonly string[];
|
|
7
|
+
/** Pages Router JSON data identities used by client navigation. */
|
|
8
|
+
pagesDataPaths?: readonly string[];
|
|
9
|
+
/** Statically eligible App Route Handler request identities. */
|
|
10
|
+
routeHandlerPaths?: readonly string[];
|
|
11
|
+
routePatterns?: Readonly<Record<string, PrerenderRoutePattern>>;
|
|
12
|
+
/** App Router ISR paths whose definitive client-navigation payload is warmed. */
|
|
13
|
+
rscPaths?: readonly string[];
|
|
14
|
+
/** App Router paths whose deterministic loading-boundary payload is warmed. */
|
|
15
|
+
loadingShellPaths?: readonly string[];
|
|
16
|
+
/** Build identity that the warmed RSC response must have been rendered by. */
|
|
17
|
+
expectedRscBuildId?: string;
|
|
18
|
+
/** Application build identity stamped by the configured CDN adapter. */
|
|
19
|
+
expectedBuildId?: string;
|
|
20
|
+
deploymentId?: string;
|
|
6
21
|
headers?: HeadersInit;
|
|
7
22
|
concurrency?: number;
|
|
8
23
|
timeoutMs?: number;
|
|
9
24
|
retries?: number;
|
|
25
|
+
retryDelayMs?: number;
|
|
26
|
+
/** Bound the whole warm phase, including queued targets and retries. */
|
|
27
|
+
phaseTimeoutMs?: number;
|
|
28
|
+
/** Retry a newly staged version or preview alias until its routing has propagated. */
|
|
29
|
+
propagatingTarget?: boolean;
|
|
30
|
+
/** Require the response to come from a reusable cache entry, not merely an eligible MISS. */
|
|
31
|
+
requireCacheHit?: boolean;
|
|
10
32
|
strict?: boolean;
|
|
11
33
|
fetchImpl?: typeof fetch;
|
|
12
34
|
};
|
|
13
|
-
declare const
|
|
35
|
+
declare const DEFAULT_CDN_WARM_CONCURRENCY = 25;
|
|
36
|
+
declare const DEFAULT_CDN_WARM_TIMEOUT_MS = 10000;
|
|
37
|
+
declare const DEFAULT_STAGED_READINESS_RETRIES = 60;
|
|
38
|
+
declare const DEFAULT_STAGED_READINESS_INTERVAL_MS = 1000;
|
|
39
|
+
declare const DEFAULT_STAGED_READINESS_PHASE_TIMEOUT_MS = 120000;
|
|
14
40
|
type PrerenderCdnWarmOptions = Omit<CdnWarmOptions, "paths"> & {
|
|
15
41
|
root: string;
|
|
16
42
|
includeFallbackShells?: boolean;
|
|
43
|
+
/** Use the manifest build identity unless the configured adapter cannot expose it. */
|
|
44
|
+
validateBuildIdentity?: boolean;
|
|
17
45
|
};
|
|
18
46
|
type CdnWarmResult = {
|
|
19
47
|
total: number;
|
|
20
48
|
warmed: number;
|
|
49
|
+
skipped: number;
|
|
21
50
|
failed: number;
|
|
22
51
|
failures: Array<{
|
|
23
52
|
path: string;
|
|
24
53
|
error: string;
|
|
25
54
|
}>;
|
|
55
|
+
skippedTargets: CdnWarmTarget[];
|
|
56
|
+
warmedPlan: CdnWarmRequestPlan;
|
|
57
|
+
retryPlan: CdnWarmRequestPlan;
|
|
26
58
|
};
|
|
59
|
+
type CdnWarmRequestPlan = {
|
|
60
|
+
loadingShellPaths: string[];
|
|
61
|
+
pagesDataPaths: string[];
|
|
62
|
+
paths: string[];
|
|
63
|
+
rscPaths: string[];
|
|
64
|
+
routeHandlerPaths?: string[];
|
|
65
|
+
routePatterns?: Record<string, PrerenderRoutePattern>;
|
|
66
|
+
};
|
|
67
|
+
type CdnWarmReadinessResult = {
|
|
68
|
+
ready: true;
|
|
69
|
+
} | {
|
|
70
|
+
error: string;
|
|
71
|
+
ready: false;
|
|
72
|
+
};
|
|
73
|
+
type PrerenderWarmPlan = {
|
|
74
|
+
appPaths?: string[];
|
|
75
|
+
buildId?: string;
|
|
76
|
+
buildIdentity?: string;
|
|
77
|
+
deploymentId?: string;
|
|
78
|
+
fallbackRoutePatterns?: PrerenderRoutePattern[];
|
|
79
|
+
loadingShellPaths: string[];
|
|
80
|
+
pagesDataPaths?: string[];
|
|
81
|
+
pagesPaths?: string[];
|
|
82
|
+
paths: string[];
|
|
83
|
+
routeHandlerPaths?: string[];
|
|
84
|
+
routePatterns?: Record<string, PrerenderRoutePattern>;
|
|
85
|
+
rscBuildId?: string;
|
|
86
|
+
rscPaths: string[];
|
|
87
|
+
};
|
|
88
|
+
declare function readPrerenderWarmPlan(root: string, options?: {
|
|
89
|
+
includeFallbackShells?: boolean;
|
|
90
|
+
strict?: boolean;
|
|
91
|
+
}): PrerenderWarmPlan;
|
|
27
92
|
declare function readPrerenderWarmPaths(root: string, options?: {
|
|
28
93
|
includeFallbackShells?: boolean;
|
|
29
94
|
strict?: boolean;
|
|
30
95
|
}): string[];
|
|
31
96
|
declare function getWarmPathsFromPrerenderManifest(manifest: PrerenderManifest, options?: PrerenderedPathSelectionOptions): string[];
|
|
32
97
|
declare function buildWarmupUrl(targetUrl: string, pathname: string): URL;
|
|
98
|
+
type CdnWarmTarget = {
|
|
99
|
+
headers?: HeadersInit;
|
|
100
|
+
kind: "app-route" | "html" | "pages-data" | "rsc-full" | "rsc-loading-shell";
|
|
101
|
+
label: string;
|
|
102
|
+
pathname: string;
|
|
103
|
+
sourcePathname: string;
|
|
104
|
+
route?: PrerenderRoutePattern;
|
|
105
|
+
};
|
|
106
|
+
declare function createCdnWarmTargets(options: Pick<CdnWarmOptions, "deploymentId" | "headers" | "loadingShellPaths" | "pagesDataPaths" | "paths" | "routeHandlerPaths" | "routePatterns" | "rscPaths">): Promise<CdnWarmTarget[]>;
|
|
107
|
+
declare class CdnOperationProgress {
|
|
108
|
+
private readonly isTTY;
|
|
109
|
+
private lastLineLength;
|
|
110
|
+
update(completed: number, total: number, label: string, phase?: string): void;
|
|
111
|
+
finish(): void;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Wait until version-override requests consistently reach the uploaded build
|
|
115
|
+
* before any real cache key is filled. Every probe has a unique query key, so
|
|
116
|
+
* an early response cannot make a later readiness attempt pass from cache.
|
|
117
|
+
*/
|
|
118
|
+
declare function waitForCdnWarmTargetReadiness(options: Pick<CdnWarmOptions, "deploymentId" | "expectedBuildId" | "expectedRscBuildId" | "fetchImpl" | "headers" | "retries" | "targetUrl" | "timeoutMs"> & {
|
|
119
|
+
plan: CdnWarmRequestPlan;
|
|
120
|
+
maxAttempts?: number;
|
|
121
|
+
phaseTimeoutMs?: number;
|
|
122
|
+
prerenderSecret?: string;
|
|
123
|
+
probeIntervalMs?: number;
|
|
124
|
+
requiredConsecutiveSuccesses?: number;
|
|
125
|
+
}): Promise<CdnWarmReadinessResult>;
|
|
33
126
|
declare function warmCdnCache(options: CdnWarmOptions): Promise<CdnWarmResult>;
|
|
34
127
|
declare function warmCdnCacheFromPrerender(options: PrerenderCdnWarmOptions): Promise<CdnWarmResult>;
|
|
35
128
|
//#endregion
|
|
36
|
-
export { CdnWarmOptions, CdnWarmResult, DEFAULT_CDN_WARM_TIMEOUT_MS, PrerenderCdnWarmOptions, buildWarmupUrl, getWarmPathsFromPrerenderManifest, readPrerenderWarmPaths, warmCdnCache, warmCdnCacheFromPrerender };
|
|
129
|
+
export { CdnOperationProgress, CdnWarmOptions, CdnWarmReadinessResult, CdnWarmRequestPlan, CdnWarmResult, CdnWarmTarget, DEFAULT_CDN_WARM_CONCURRENCY, DEFAULT_CDN_WARM_TIMEOUT_MS, DEFAULT_STAGED_READINESS_INTERVAL_MS, DEFAULT_STAGED_READINESS_PHASE_TIMEOUT_MS, DEFAULT_STAGED_READINESS_RETRIES, PrerenderCdnWarmOptions, PrerenderWarmPlan, buildWarmupUrl, createCdnWarmTargets, getWarmPathsFromPrerenderManifest, readPrerenderWarmPaths, readPrerenderWarmPlan, waitForCdnWarmTargetReadiness, warmCdnCache, warmCdnCacheFromPrerender };
|