@vinext/cloudflare 1.2.1 → 1.3.1
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/dist/cache/app-page-parts.d.ts +7 -0
- package/dist/cache/app-page-parts.js +10 -0
- package/dist/cache/cdn-adapter.runtime.d.ts +1 -0
- package/dist/cache/cdn-adapter.runtime.js +1 -0
- package/dist/cache/cdn-adapter.worker.js +221 -34
- package/dist/cache/kv-data-adapter.runtime.js +24 -10
- package/dist/cache/response-store-adapter.worker.js +168 -55
- package/dist/cache/response-store-cdn.runtime.js +4 -1
- package/dist/cache/response-store-data.runtime.d.ts +14 -0
- package/dist/cache/response-store-data.runtime.js +44 -6
- package/dist/cli.js +2 -0
- package/dist/deploy-config.d.ts +4 -4
- package/dist/deploy-config.js +10 -7
- package/dist/deploy-help.js +5 -3
- package/dist/deploy.d.ts +3 -0
- package/dist/deploy.js +23 -10
- package/dist/previews-deploy.d.ts +1 -0
- package/dist/previews-deploy.js +4 -2
- package/dist/tpr.js +0 -1
- package/dist/utils/cache-control-metadata.d.ts +5 -0
- package/dist/utils/cache-control-metadata.js +9 -1
- package/package.json +4 -4
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
//#region src/cache/app-page-parts.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Whether the response-stage props select an App page's own document or RSC
|
|
4
|
+
* payload, the two parts Next.js keeps in one entry, rather than a variant.
|
|
5
|
+
*/
|
|
6
|
+
export declare function isCanonicalAppPage(responseStageProps: unknown, isRscRequest: boolean): boolean;
|
|
7
|
+
//#endregion
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
//#region src/cache/app-page-parts.ts
|
|
2
|
+
/**
|
|
3
|
+
* Whether the response-stage props select an App page's own document or RSC
|
|
4
|
+
* payload, the two parts Next.js keeps in one entry, rather than a variant.
|
|
5
|
+
*/
|
|
6
|
+
function isCanonicalAppPage(responseStageProps, isRscRequest) {
|
|
7
|
+
return responseStageProps !== null && typeof responseStageProps === "object" && Reflect.get(responseStageProps, "kind") === "app-page" && Reflect.get(responseStageProps, "isRscRequest") === isRscRequest && (Reflect.get(responseStageProps, "matchKind") === "request" || Reflect.get(responseStageProps, "matchKind") === "resolved") && (!isRscRequest || Reflect.get(responseStageProps, "renderMode") === "navigation") && Reflect.get(responseStageProps, "interceptionContext") === null && Reflect.get(responseStageProps, "interceptionId") === null && Reflect.get(responseStageProps, "mountedSlotsHeader") === null;
|
|
8
|
+
}
|
|
9
|
+
//#endregion
|
|
10
|
+
export { isCanonicalAppPage };
|
|
@@ -13,6 +13,7 @@ export declare class CloudflareCdnCacheAdapter implements CdnCacheAdapter {
|
|
|
13
13
|
private readonly versionMetadata?;
|
|
14
14
|
private readonly versionMetadataBinding;
|
|
15
15
|
readonly requiresCompletedResponseAdmission = true;
|
|
16
|
+
readonly responseStageCacheIdentity: "host-free";
|
|
16
17
|
readonly responseVary: "verbatim";
|
|
17
18
|
readonly responsePolicy: {
|
|
18
19
|
isHeader(name: string): boolean;
|
|
@@ -150,6 +150,7 @@ var CloudflareCdnCacheAdapter = class {
|
|
|
150
150
|
versionMetadata;
|
|
151
151
|
versionMetadataBinding;
|
|
152
152
|
requiresCompletedResponseAdmission = true;
|
|
153
|
+
responseStageCacheIdentity = "host-free";
|
|
153
154
|
responseVary = "verbatim";
|
|
154
155
|
responsePolicy = {
|
|
155
156
|
isHeader(name) {
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { VINEXT_CDN_BUILD_ID_HEADER, getVinextCdnBuildIdentity } from "./cdn-build-id.js";
|
|
2
|
+
import { isCanonicalAppPage } from "./app-page-parts.js";
|
|
2
3
|
import { SHARED_RESPONSE_STAGE_HEADER, finalizeGatewayResponse } from "./browser-cache-policy.js";
|
|
3
4
|
import { hasCacheMethod, invalidateOrPurge } from "./workers-cache-invalidation.js";
|
|
4
5
|
import { VINEXT_PRERENDER_READINESS_PATH, VINEXT_RSC_VARY_HEADER } from "vinext/internal/server/headers";
|
|
6
|
+
import { createCanonicalRscRequestHeaders, createCanonicalRscRequestUrl, stripRscCacheBustingSearchParam } from "vinext/internal/server/app-rsc-cache-busting";
|
|
5
7
|
import { isNonCacheableCacheControl, splitCacheControlDirectives } from "vinext/shims/cdn-cache";
|
|
6
8
|
import { WorkerEntrypoint } from "cloudflare:workers";
|
|
7
9
|
import "vinext/internal/server/cloudflare-workers-tracing";
|
|
@@ -15,11 +17,29 @@ const AUTHORIZATION_TRANSPORT_HEADER = "x-vinext-internal-authorization";
|
|
|
15
17
|
const REQUEST_CACHE_CONTROL_TRANSPORT_HEADER = "x-vinext-internal-request-cache-control";
|
|
16
18
|
const REQUEST_CF_TRANSPORT_HEADER = "x-vinext-internal-request-cf";
|
|
17
19
|
const REQUEST_PRAGMA_TRANSPORT_HEADER = "x-vinext-internal-request-pragma";
|
|
20
|
+
const RENDER_WHOLE_DOCUMENT_TRANSPORT_HEADER = "x-vinext-internal-render-whole-document";
|
|
21
|
+
const DISPATCH_TRANSPORT_HEADER = "x-vinext-internal-dispatch";
|
|
22
|
+
/** The stage props a host-free cache identity replaces. */
|
|
23
|
+
const HOST_BEARING_PROPS = /* @__PURE__ */ new Set([
|
|
24
|
+
"requestHost",
|
|
25
|
+
"requestOrigin",
|
|
26
|
+
"requestUrl"
|
|
27
|
+
]);
|
|
18
28
|
const WARMUP_USER_AGENT = "vinext-cloudflare-cdn-warm";
|
|
19
29
|
const RESPONSE_STAGE_WIRE_CACHE = {
|
|
20
30
|
bypass: "vinext-cloudflare-v1:bypass",
|
|
21
31
|
shared: "vinext-cloudflare-v1:shared"
|
|
22
32
|
};
|
|
33
|
+
/**
|
|
34
|
+
* The statuses of a part Workers Cache just rendered for its entry, inline or
|
|
35
|
+
* as a background refresh of a stale one.
|
|
36
|
+
* https://developers.cloudflare.com/workers/cache/debugging/
|
|
37
|
+
*/
|
|
38
|
+
const RENDERED_ENTRY_CACHE_STATUSES = /* @__PURE__ */ new Set([
|
|
39
|
+
"EXPIRED",
|
|
40
|
+
"MISS",
|
|
41
|
+
"UPDATING"
|
|
42
|
+
]);
|
|
23
43
|
const FRAMEWORK_RESPONSE_VARY_FIELDS = new Set(VINEXT_RSC_VARY_HEADER.split(",").map((name) => name.trim().toLowerCase()));
|
|
24
44
|
function isResponseStageReadinessRequest(request) {
|
|
25
45
|
return request.url.includes(VINEXT_PRERENDER_READINESS_PATH) && new URL(request.url).pathname === VINEXT_PRERENDER_READINESS_PATH;
|
|
@@ -63,20 +83,25 @@ function validateResponseStageBuildIdentity(response) {
|
|
|
63
83
|
return responseStageUnavailable();
|
|
64
84
|
}
|
|
65
85
|
function stripUntrustedTransportHeaders(request) {
|
|
66
|
-
if (!request.headers.has(AUTHORIZATION_TRANSPORT_HEADER) && !request.headers.has(REQUEST_CACHE_CONTROL_TRANSPORT_HEADER) && !request.headers.has(REQUEST_CF_TRANSPORT_HEADER) && !request.headers.has(REQUEST_PRAGMA_TRANSPORT_HEADER)) return request;
|
|
86
|
+
if (!request.headers.has(AUTHORIZATION_TRANSPORT_HEADER) && !request.headers.has(REQUEST_CACHE_CONTROL_TRANSPORT_HEADER) && !request.headers.has(REQUEST_CF_TRANSPORT_HEADER) && !request.headers.has(REQUEST_PRAGMA_TRANSPORT_HEADER) && !request.headers.has(RENDER_WHOLE_DOCUMENT_TRANSPORT_HEADER) && !request.headers.has(DISPATCH_TRANSPORT_HEADER)) return request;
|
|
67
87
|
const headers = new Headers(request.headers);
|
|
68
88
|
headers.delete(AUTHORIZATION_TRANSPORT_HEADER);
|
|
69
89
|
headers.delete(REQUEST_CACHE_CONTROL_TRANSPORT_HEADER);
|
|
70
90
|
headers.delete(REQUEST_CF_TRANSPORT_HEADER);
|
|
71
91
|
headers.delete(REQUEST_PRAGMA_TRANSPORT_HEADER);
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
92
|
+
headers.delete(RENDER_WHOLE_DOCUMENT_TRANSPORT_HEADER);
|
|
93
|
+
headers.delete(DISPATCH_TRANSPORT_HEADER);
|
|
94
|
+
return withRequestCf(new Request(request, { headers }), request);
|
|
95
|
+
}
|
|
96
|
+
/** Give a request built from another the platform's `request.cf` of that one. */
|
|
97
|
+
function withRequestCf(request, source) {
|
|
98
|
+
const requestCf = Reflect.get(source, "cf");
|
|
99
|
+
if (requestCf !== void 0) Object.defineProperty(request, "cf", {
|
|
75
100
|
configurable: true,
|
|
76
101
|
enumerable: true,
|
|
77
102
|
value: requestCf
|
|
78
103
|
});
|
|
79
|
-
return
|
|
104
|
+
return request;
|
|
80
105
|
}
|
|
81
106
|
function withWorkerHostRuntime(context, env) {
|
|
82
107
|
const candidateAssets = env && typeof env === "object" && Reflect.has(env, "ASSETS") ? Reflect.get(env, "ASSETS") : void 0;
|
|
@@ -100,7 +125,7 @@ function getResponseStageBinding(context, exportName, serializedInvocation) {
|
|
|
100
125
|
const target = binding({ props: JSON.parse(serializedInvocation) });
|
|
101
126
|
return hasFetch(target) ? target : null;
|
|
102
127
|
}
|
|
103
|
-
async function createCacheFacingRequest(request, serializedInvocation) {
|
|
128
|
+
async function createCacheFacingRequest(request, serializedInvocation, identityRequest, dispatchRestore) {
|
|
104
129
|
const authorization = request.headers.get("Authorization");
|
|
105
130
|
let serializedRequestCf = null;
|
|
106
131
|
const requestCf = Reflect.get(request, "cf");
|
|
@@ -109,9 +134,9 @@ async function createCacheFacingRequest(request, serializedInvocation) {
|
|
|
109
134
|
if (json !== void 0) serializedRequestCf = encodeURIComponent(json);
|
|
110
135
|
} catch {}
|
|
111
136
|
const authorizationIdentity = authorization === null ? "absent" : `present:${authorization.length}:${authorization}`;
|
|
112
|
-
const frameworkVaryIdentity = JSON.stringify([...FRAMEWORK_RESPONSE_VARY_FIELDS].map((name) => [name,
|
|
137
|
+
const frameworkVaryIdentity = JSON.stringify([...FRAMEWORK_RESPONSE_VARY_FIELDS].map((name) => [name, identityRequest.headers.get(name)]));
|
|
113
138
|
const responseStageBuildIdentity = getVinextCdnBuildIdentity() ?? "";
|
|
114
|
-
const bytes = new TextEncoder().encode(`${
|
|
139
|
+
const bytes = new TextEncoder().encode(`${identityRequest.url}\0${serializedInvocation}\0${authorizationIdentity}\0${frameworkVaryIdentity}\0${responseStageBuildIdentity}`);
|
|
115
140
|
const key = [...new Uint8Array(await crypto.subtle.digest("SHA-256", bytes))].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
116
141
|
const url = new URL(request.url);
|
|
117
142
|
url.searchParams.set("__vinext_cache_key", key);
|
|
@@ -125,7 +150,9 @@ async function createCacheFacingRequest(request, serializedInvocation) {
|
|
|
125
150
|
headers.delete(REQUEST_CACHE_CONTROL_TRANSPORT_HEADER);
|
|
126
151
|
headers.delete(REQUEST_CF_TRANSPORT_HEADER);
|
|
127
152
|
headers.delete(REQUEST_PRAGMA_TRANSPORT_HEADER);
|
|
153
|
+
headers.delete(DISPATCH_TRANSPORT_HEADER);
|
|
128
154
|
if (authorization !== null) headers.set(AUTHORIZATION_TRANSPORT_HEADER, encodeURIComponent(authorization));
|
|
155
|
+
if (dispatchRestore !== null) headers.set(DISPATCH_TRANSPORT_HEADER, encodeURIComponent(JSON.stringify(dispatchRestore)));
|
|
129
156
|
if (serializedRequestCf !== null) headers.set(REQUEST_CF_TRANSPORT_HEADER, serializedRequestCf);
|
|
130
157
|
if (requestCacheControl !== null) headers.set(REQUEST_CACHE_CONTROL_TRANSPORT_HEADER, encodeURIComponent(requestCacheControl));
|
|
131
158
|
if (requestPragma !== null) headers.set(REQUEST_PRAGMA_TRANSPORT_HEADER, encodeURIComponent(requestPragma));
|
|
@@ -149,6 +176,8 @@ function restoreResponseStageRequest(request, requestUrl, requestMethod) {
|
|
|
149
176
|
headers.delete(REQUEST_CACHE_CONTROL_TRANSPORT_HEADER);
|
|
150
177
|
headers.delete(REQUEST_CF_TRANSPORT_HEADER);
|
|
151
178
|
headers.delete(REQUEST_PRAGMA_TRANSPORT_HEADER);
|
|
179
|
+
headers.delete(RENDER_WHOLE_DOCUMENT_TRANSPORT_HEADER);
|
|
180
|
+
headers.delete(DISPATCH_TRANSPORT_HEADER);
|
|
152
181
|
if (serializedAuthorization !== null) try {
|
|
153
182
|
headers.set("Authorization", decodeURIComponent(serializedAuthorization));
|
|
154
183
|
} catch {}
|
|
@@ -199,11 +228,11 @@ function revalidatesStoredEntry(request) {
|
|
|
199
228
|
return request.headers.has("If-None-Match") || request.headers.has("If-Modified-Since");
|
|
200
229
|
}
|
|
201
230
|
/**
|
|
202
|
-
* No client waits on a deployment warm-up
|
|
203
|
-
* Response Store's.
|
|
231
|
+
* No client waits on a deployment warm-up, on a revalidation, as with the
|
|
232
|
+
* Response Store's, or on the gateway's fill of a page's document.
|
|
204
233
|
*/
|
|
205
234
|
function rendersWholeDocument(request) {
|
|
206
|
-
return request.headers.get("user-agent") === WARMUP_USER_AGENT || revalidatesStoredEntry(request);
|
|
235
|
+
return request.headers.get("user-agent") === WARMUP_USER_AGENT || request.headers.has(RENDER_WHOLE_DOCUMENT_TRANSPORT_HEADER) || revalidatesStoredEntry(request);
|
|
207
236
|
}
|
|
208
237
|
/**
|
|
209
238
|
* A conditional revalidation of a Pages page skips getStaticPaths(), so only a
|
|
@@ -328,6 +357,8 @@ function getResponseStageInvocation(value, expectedCache) {
|
|
|
328
357
|
} catch {
|
|
329
358
|
return null;
|
|
330
359
|
}
|
|
360
|
+
const restoresDispatch = Reflect.get(value, "restoresDispatch");
|
|
361
|
+
if (restoresDispatch !== void 0 && restoresDispatch !== true) return null;
|
|
331
362
|
return {
|
|
332
363
|
...typeof expectedResponseStageBuildIdentity === "string" ? { expectedResponseStageBuildIdentity } : {},
|
|
333
364
|
options: {
|
|
@@ -336,6 +367,116 @@ function getResponseStageInvocation(value, expectedCache) {
|
|
|
336
367
|
},
|
|
337
368
|
props: Reflect.get(value, "props"),
|
|
338
369
|
requestMethod,
|
|
370
|
+
requestUrl,
|
|
371
|
+
...restoresDispatch ? { restoresDispatch } : {}
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
/**
|
|
375
|
+
* The dispatch of the other part of the page a canonical App page part belongs
|
|
376
|
+
* to. Next.js keeps a page's document and RSC payload in one entry, so the two
|
|
377
|
+
* dispatches differ only in the RSC request selectors, the URL's `_rsc` and the
|
|
378
|
+
* request type in the props. The request keeps the client's other headers,
|
|
379
|
+
* minus those that would make the fetch conditional or partial. No client
|
|
380
|
+
* waits on a document fetched this way, so it is rendered whole, marked by a
|
|
381
|
+
* header that, unlike the props, does not key its entry.
|
|
382
|
+
*/
|
|
383
|
+
function otherPagePartDispatch(request, props) {
|
|
384
|
+
const isRscRequest = isCanonicalAppPage(props, true);
|
|
385
|
+
if (!isRscRequest && !isCanonicalAppPage(props, false)) return null;
|
|
386
|
+
const headers = new Headers(request.headers);
|
|
387
|
+
for (const name of FRAMEWORK_RESPONSE_VARY_FIELDS) headers.delete(name);
|
|
388
|
+
for (const name of [
|
|
389
|
+
"If-Match",
|
|
390
|
+
"If-Modified-Since",
|
|
391
|
+
"If-None-Match",
|
|
392
|
+
"If-Range",
|
|
393
|
+
"If-Unmodified-Since",
|
|
394
|
+
"Range"
|
|
395
|
+
]) headers.delete(name);
|
|
396
|
+
if (isRscRequest) {
|
|
397
|
+
const url = new URL(request.url);
|
|
398
|
+
stripRscCacheBustingSearchParam(url);
|
|
399
|
+
headers.delete("x-deployment-id");
|
|
400
|
+
headers.set("Accept", "text/html");
|
|
401
|
+
headers.set(RENDER_WHOLE_DOCUMENT_TRANSPORT_HEADER, "1");
|
|
402
|
+
return {
|
|
403
|
+
props: {
|
|
404
|
+
...props,
|
|
405
|
+
isRscRequest: false
|
|
406
|
+
},
|
|
407
|
+
request: withRequestCf(new Request(url, { headers }), request)
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
for (const [name, value] of createCanonicalRscRequestHeaders()) headers.set(name, value);
|
|
411
|
+
return {
|
|
412
|
+
props: {
|
|
413
|
+
...props,
|
|
414
|
+
isRscRequest: true,
|
|
415
|
+
renderMode: "navigation"
|
|
416
|
+
},
|
|
417
|
+
request: withRequestCf(new Request(new URL(createCanonicalRscRequestUrl(request.url), request.url), { headers }), request)
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
function isPlainObject(value) {
|
|
421
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
422
|
+
}
|
|
423
|
+
function pathAndSearch(url) {
|
|
424
|
+
return `${url.pathname}${url.search}`;
|
|
425
|
+
}
|
|
426
|
+
/**
|
|
427
|
+
* What a shared dispatch's host-free identity replaced: the request URL, and
|
|
428
|
+
* the top-level props that name the host. Null when the identity is anything
|
|
429
|
+
* else, so the dispatch keys itself, host and all.
|
|
430
|
+
*/
|
|
431
|
+
function getDispatchRestore(request, props, identity) {
|
|
432
|
+
if (identity.request.method !== request.method || pathAndSearch(new URL(identity.request.url)) !== pathAndSearch(new URL(request.url))) return null;
|
|
433
|
+
const dispatched = JSON.parse(JSON.stringify(props) ?? "null");
|
|
434
|
+
const keyed = JSON.parse(JSON.stringify(identity.props) ?? "null");
|
|
435
|
+
if (!isPlainObject(dispatched) || !isPlainObject(keyed)) return null;
|
|
436
|
+
if (Object.keys(keyed).some((name) => !Object.hasOwn(dispatched, name))) return null;
|
|
437
|
+
const replaced = {};
|
|
438
|
+
for (const [name, value] of Object.entries(dispatched)) {
|
|
439
|
+
if (JSON.stringify(value) === JSON.stringify(keyed[name])) continue;
|
|
440
|
+
if (!HOST_BEARING_PROPS.has(name)) return null;
|
|
441
|
+
replaced[name] = value;
|
|
442
|
+
}
|
|
443
|
+
return {
|
|
444
|
+
props: replaced,
|
|
445
|
+
requestUrl: request.url
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
/**
|
|
449
|
+
* Rebuild the dispatch a host-free invocation keys from the restore header
|
|
450
|
+
* the gateway sent. Null when an identity invocation has no valid restore.
|
|
451
|
+
*/
|
|
452
|
+
function restoreDispatchInvocation(request, invocation) {
|
|
453
|
+
if (!invocation.restoresDispatch) return invocation;
|
|
454
|
+
const serialized = request.headers.get(DISPATCH_TRANSPORT_HEADER);
|
|
455
|
+
if (serialized === null || !isPlainObject(invocation.props)) return null;
|
|
456
|
+
let restore;
|
|
457
|
+
try {
|
|
458
|
+
restore = JSON.parse(decodeURIComponent(serialized));
|
|
459
|
+
} catch {
|
|
460
|
+
return null;
|
|
461
|
+
}
|
|
462
|
+
if (!isPlainObject(restore) || !isPlainObject(restore.props)) return null;
|
|
463
|
+
if (Object.keys(restore.props).some((name) => !HOST_BEARING_PROPS.has(name))) return null;
|
|
464
|
+
const requestUrl = restore.requestUrl;
|
|
465
|
+
if (typeof requestUrl !== "string") return null;
|
|
466
|
+
let url;
|
|
467
|
+
try {
|
|
468
|
+
url = new URL(requestUrl);
|
|
469
|
+
} catch {
|
|
470
|
+
return null;
|
|
471
|
+
}
|
|
472
|
+
if (pathAndSearch(url) !== pathAndSearch(new URL(invocation.requestUrl))) return null;
|
|
473
|
+
const { restoresDispatch: _restoresDispatch, ...dispatch } = invocation;
|
|
474
|
+
return {
|
|
475
|
+
...dispatch,
|
|
476
|
+
props: {
|
|
477
|
+
...invocation.props,
|
|
478
|
+
...restore.props
|
|
479
|
+
},
|
|
339
480
|
requestUrl
|
|
340
481
|
};
|
|
341
482
|
}
|
|
@@ -363,22 +504,27 @@ var VinextCachedResponse = class extends WorkerEntrypoint {
|
|
|
363
504
|
headers: { "Cache-Control": "no-store" }
|
|
364
505
|
}));
|
|
365
506
|
if (invocation.expectedResponseStageBuildIdentity !== void 0 && invocation.expectedResponseStageBuildIdentity !== getVinextCdnBuildIdentity()) return stampResponseStageBuildIdentity(responseStageUnavailable());
|
|
366
|
-
const
|
|
367
|
-
|
|
368
|
-
|
|
507
|
+
const dispatch = restoreDispatchInvocation(request, invocation);
|
|
508
|
+
if (!dispatch) return stampResponseStageBuildIdentity(new Response("Invalid vinext response-stage invocation", {
|
|
509
|
+
status: 400,
|
|
510
|
+
headers: { "Cache-Control": "no-store" }
|
|
511
|
+
}));
|
|
512
|
+
const restored = restoreResponseStageRequest(request, dispatch.requestUrl, dispatch.requestMethod);
|
|
513
|
+
const isAppPageDocument = isAppPageDocumentInvocation(dispatch);
|
|
514
|
+
const isPagesPage = isPagesPageInvocation(dispatch);
|
|
369
515
|
const response = await invokeResponseStage(restored.request, this.env, context, isAppPageDocument && rendersWholeDocument(request) ? {
|
|
370
|
-
...
|
|
516
|
+
...dispatch,
|
|
371
517
|
options: {
|
|
372
|
-
...
|
|
518
|
+
...dispatch.options,
|
|
373
519
|
renderWholeDocument: true
|
|
374
520
|
}
|
|
375
521
|
} : isPagesPage && revalidatesStoredEntry(request) ? {
|
|
376
|
-
...
|
|
522
|
+
...dispatch,
|
|
377
523
|
options: {
|
|
378
|
-
...
|
|
524
|
+
...dispatch.options,
|
|
379
525
|
regeneratesStoredEntry: true
|
|
380
526
|
}
|
|
381
|
-
} :
|
|
527
|
+
} : dispatch);
|
|
382
528
|
return stampResponseStageBuildIdentity(restored.didAccessRequestCf() || hasTaggedCustomVary(response) ? preventResponseCaching(response) : isAppPageDocument ? withRevalidationValidator(response) : isPagesPage ? withPagesRevalidationValidator(response) : response);
|
|
383
529
|
}
|
|
384
530
|
async purge(options) {
|
|
@@ -408,30 +554,71 @@ var cdn_adapter_worker_default = { async fetch(request, env, context) {
|
|
|
408
554
|
request = stripUntrustedTransportHeaders(request);
|
|
409
555
|
const sharedResponses = /* @__PURE__ */ new Map();
|
|
410
556
|
const stageContext = withResponseStageCache(withWorkerHostRuntime(context, env));
|
|
411
|
-
const
|
|
557
|
+
const createInvocation = (stageRequest, props, options) => {
|
|
412
558
|
const expectedResponseStageBuildIdentity = getVinextCdnBuildIdentity();
|
|
413
|
-
const
|
|
559
|
+
const { cacheIdentity: _cacheIdentity, ...stageOptions } = options;
|
|
560
|
+
return {
|
|
414
561
|
...expectedResponseStageBuildIdentity === null ? {} : { expectedResponseStageBuildIdentity },
|
|
415
|
-
options,
|
|
562
|
+
options: stageOptions,
|
|
416
563
|
props,
|
|
417
564
|
requestMethod: stageRequest.method,
|
|
418
565
|
requestUrl: stageRequest.url
|
|
419
566
|
};
|
|
567
|
+
};
|
|
568
|
+
/** Fetch a response stage through its named entrypoint, the cached one for a shared dispatch. */
|
|
569
|
+
const fetchResponseStageEntrypoint = async (stageRequest, props, options) => {
|
|
570
|
+
const invocation = createInvocation(stageRequest, props, options);
|
|
571
|
+
const usesSharedCache = options.cache === "shared";
|
|
572
|
+
const { cacheIdentity } = options;
|
|
573
|
+
const dispatchRestore = usesSharedCache && cacheIdentity ? getDispatchRestore(stageRequest, props, cacheIdentity) : null;
|
|
574
|
+
const keyedRequest = dispatchRestore ? cacheIdentity.request : stageRequest;
|
|
575
|
+
const serializedInvocation = JSON.stringify({
|
|
576
|
+
...invocation,
|
|
577
|
+
...dispatchRestore ? {
|
|
578
|
+
props: cacheIdentity.props,
|
|
579
|
+
requestMethod: keyedRequest.method,
|
|
580
|
+
requestUrl: keyedRequest.url,
|
|
581
|
+
restoresDispatch: true
|
|
582
|
+
} : {},
|
|
583
|
+
options: invocation.expectedResponseStageBuildIdentity === void 0 ? invocation.options : {
|
|
584
|
+
...invocation.options,
|
|
585
|
+
cache: RESPONSE_STAGE_WIRE_CACHE[options.cache]
|
|
586
|
+
}
|
|
587
|
+
});
|
|
588
|
+
const binding = getResponseStageBinding(stageContext, usesSharedCache ? CACHED_RESPONSE_STAGE_EXPORT : UNCACHED_RESPONSE_STAGE_EXPORT, serializedInvocation);
|
|
589
|
+
if (!binding) return responseStageUnavailable();
|
|
590
|
+
const entrypointRequest = usesSharedCache ? await createCacheFacingRequest(stageRequest, serializedInvocation, keyedRequest, dispatchRestore) : stageRequest;
|
|
591
|
+
return validateResponseStageBuildIdentity(await binding.fetch(entrypointRequest));
|
|
592
|
+
};
|
|
593
|
+
/**
|
|
594
|
+
* Fetch the other part of a canonical App page part Workers Cache just
|
|
595
|
+
* rendered, so its entry is filled or refreshed too. Workers Cache stores
|
|
596
|
+
* only what its entrypoint returns, so the other part renders on its own.
|
|
597
|
+
* A deployment warm-up already requests both parts, and this fetch reaches
|
|
598
|
+
* the entrypoint directly, so it never fetches another part itself.
|
|
599
|
+
*/
|
|
600
|
+
const storeOtherPagePart = (stageRequest, props, options, cacheStatus) => {
|
|
601
|
+
if (cacheStatus === null || !RENDERED_ENTRY_CACHE_STATUSES.has(cacheStatus) || stageRequest.headers.get("user-agent") === WARMUP_USER_AGENT || typeof stageContext.waitUntil !== "function") return;
|
|
602
|
+
stageContext.waitUntil((async () => {
|
|
603
|
+
const otherPart = otherPagePartDispatch(stageRequest, props);
|
|
604
|
+
if (!otherPart) return;
|
|
605
|
+
const { cacheIdentity, ...otherOptions } = options;
|
|
606
|
+
const otherIdentity = cacheIdentity ? otherPagePartDispatch(cacheIdentity.request, cacheIdentity.props) : null;
|
|
607
|
+
await (await fetchResponseStageEntrypoint(otherPart.request, otherPart.props, otherIdentity ? {
|
|
608
|
+
...otherOptions,
|
|
609
|
+
cacheIdentity: otherIdentity
|
|
610
|
+
} : otherOptions)).body?.pipeTo(new WritableStream());
|
|
611
|
+
})().catch((error) => {
|
|
612
|
+
console.error("[vinext] Workers Cache could not store a page's other part:", error);
|
|
613
|
+
}));
|
|
614
|
+
};
|
|
615
|
+
const dispatchResponseStage = async (stageRequest, props, options) => {
|
|
420
616
|
const usesSharedCache = options.cache === "shared";
|
|
421
617
|
try {
|
|
422
|
-
if (!usesSharedCache && !isResponseStageReadinessRequest(stageRequest)) return stampResponseStageBuildIdentity(await invokeResponseStage(stageRequest, env, stageContext,
|
|
423
|
-
const
|
|
424
|
-
...invocation,
|
|
425
|
-
options: expectedResponseStageBuildIdentity === null ? options : {
|
|
426
|
-
...options,
|
|
427
|
-
cache: RESPONSE_STAGE_WIRE_CACHE[options.cache]
|
|
428
|
-
}
|
|
429
|
-
});
|
|
430
|
-
const binding = getResponseStageBinding(stageContext, usesSharedCache ? CACHED_RESPONSE_STAGE_EXPORT : UNCACHED_RESPONSE_STAGE_EXPORT, serializedInvocation);
|
|
431
|
-
if (!binding) return responseStageUnavailable();
|
|
432
|
-
const entrypointRequest = usesSharedCache ? await createCacheFacingRequest(stageRequest, serializedInvocation) : stageRequest;
|
|
433
|
-
const response = validateResponseStageBuildIdentity(await binding.fetch(entrypointRequest));
|
|
618
|
+
if (!usesSharedCache && !isResponseStageReadinessRequest(stageRequest)) return stampResponseStageBuildIdentity(await invokeResponseStage(stageRequest, env, stageContext, createInvocation(stageRequest, props, options)));
|
|
619
|
+
const response = await fetchResponseStageEntrypoint(stageRequest, props, options);
|
|
434
620
|
if (!usesSharedCache) return response;
|
|
621
|
+
storeOtherPagePart(stageRequest, props, options, response.headers.get("CF-Cache-Status"));
|
|
435
622
|
const shared = markSharedResponseStage(response, crypto.randomUUID(), props, true);
|
|
436
623
|
sharedResponses.set(shared.headers.get(SHARED_RESPONSE_STAGE_HEADER), { headers: new Headers(shared.headers) });
|
|
437
624
|
return shared;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { isStaleTagInvalidation } from "./workers-cache-invalidation.js";
|
|
2
|
-
import { isUnknownRecord, readCacheControlNumberField } from "../utils/cache-control-metadata.js";
|
|
2
|
+
import { isUnknownRecord, readCacheControlNumberField, readCacheTimestamp } from "../utils/cache-control-metadata.js";
|
|
3
3
|
import { ENTRY_PREFIX, createKvKeySpace, resolveKvExpirationTtlSeconds } from "./kv-key.js";
|
|
4
4
|
import { Buffer } from "node:buffer";
|
|
5
5
|
import { getRequestExecutionContext } from "vinext/shims/request-context";
|
|
@@ -25,6 +25,14 @@ import { getRequestExecutionContext } from "vinext/shims/request-context";
|
|
|
25
25
|
*/
|
|
26
26
|
/** Default KV namespace binding name read from the Worker `env`. */
|
|
27
27
|
const DEFAULT_BINDING = "VINEXT_KV_CACHE";
|
|
28
|
+
function toStoredLifetime(seconds) {
|
|
29
|
+
if (seconds === Infinity) return "Infinity";
|
|
30
|
+
if (seconds === -Infinity) return "-Infinity";
|
|
31
|
+
return seconds;
|
|
32
|
+
}
|
|
33
|
+
function isStoredLifetime(value) {
|
|
34
|
+
return typeof value === "number" || value === "Infinity" || value === "-Infinity";
|
|
35
|
+
}
|
|
28
36
|
function parseTagMarker(raw) {
|
|
29
37
|
if (raw === null || raw === void 0) return {};
|
|
30
38
|
if (!raw.startsWith("{")) return { expired: /^\d+$/.test(raw) ? Number(raw) : NaN };
|
|
@@ -308,13 +316,13 @@ var KVCacheHandler = class {
|
|
|
308
316
|
const effectiveStale = readCacheControlNumberField(ctx, "stale");
|
|
309
317
|
if (data && "revalidate" in data && typeof data.revalidate === "number") effectiveRevalidate = data.revalidate;
|
|
310
318
|
if (effectiveRevalidate === 0) return Promise.resolve();
|
|
311
|
-
const now = Date.now();
|
|
319
|
+
const now = readCacheTimestamp(ctx, Date.now());
|
|
312
320
|
const revalidateAt = typeof effectiveRevalidate === "number" && effectiveRevalidate > 0 && Number.isFinite(effectiveRevalidate) ? now + effectiveRevalidate * 1e3 : null;
|
|
313
321
|
const expireAt = typeof effectiveExpire === "number" && effectiveExpire > 0 ? now + effectiveExpire * 1e3 : null;
|
|
314
322
|
const cacheControl = typeof effectiveRevalidate === "number" ? {
|
|
315
323
|
revalidate: Number.isFinite(effectiveRevalidate) ? effectiveRevalidate : false,
|
|
316
|
-
...effectiveExpire === void 0 ? {} : { expire: effectiveExpire },
|
|
317
|
-
...effectiveStale === void 0 ? {} : { stale: effectiveStale }
|
|
324
|
+
...effectiveExpire === void 0 ? {} : { expire: toStoredLifetime(effectiveExpire) },
|
|
325
|
+
...effectiveStale === void 0 ? {} : { stale: toStoredLifetime(effectiveStale) }
|
|
318
326
|
} : void 0;
|
|
319
327
|
const entry = {
|
|
320
328
|
value: data ? serializeForJSON(data) : null,
|
|
@@ -469,18 +477,24 @@ function validateCacheEntry(raw) {
|
|
|
469
477
|
if (obj.cacheControl !== void 0) {
|
|
470
478
|
if (!isUnknownRecord(obj.cacheControl)) return null;
|
|
471
479
|
if (typeof obj.cacheControl.revalidate !== "number" && obj.cacheControl.revalidate !== false) return null;
|
|
472
|
-
|
|
473
|
-
|
|
480
|
+
for (const field of ["expire", "stale"]) {
|
|
481
|
+
const seconds = obj.cacheControl[field];
|
|
482
|
+
if (seconds !== void 0 && !isStoredLifetime(seconds)) return null;
|
|
483
|
+
}
|
|
474
484
|
}
|
|
475
485
|
if (obj.value !== null) {
|
|
476
486
|
if (!obj.value || typeof obj.value !== "object") return null;
|
|
477
487
|
const value = obj.value;
|
|
478
488
|
if (typeof value.kind !== "string" || !VALID_KINDS.has(value.kind)) return null;
|
|
479
489
|
}
|
|
480
|
-
if (obj.cacheControl
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
490
|
+
if (obj.cacheControl !== void 0) {
|
|
491
|
+
const { revalidate, expire, stale } = obj.cacheControl;
|
|
492
|
+
obj.cacheControl = {
|
|
493
|
+
revalidate: revalidate === false ? Infinity : revalidate,
|
|
494
|
+
...expire === void 0 ? {} : { expire: Number(expire) },
|
|
495
|
+
...stale === void 0 ? {} : { stale: Number(stale) }
|
|
496
|
+
};
|
|
497
|
+
}
|
|
484
498
|
return raw;
|
|
485
499
|
}
|
|
486
500
|
/**
|