@vinext/cloudflare 1.0.0-beta.6 → 1.0.0-beta.8
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 +62 -12
- 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 +131 -33
- package/dist/cache/cdn-adapter.worker.d.ts +30 -0
- package/dist/cache/cdn-adapter.worker.js +371 -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 +21 -5
- package/dist/cache/kv-data-adapter.runtime.d.ts +38 -4
- package/dist/cache/kv-data-adapter.runtime.js +116 -42
- package/dist/cache/kv-key.d.ts +4 -5
- package/dist/cache/response-store-adapter.d.ts +37 -0
- package/dist/cache/response-store-adapter.js +39 -0
- package/dist/cache/response-store-adapter.self-contained.worker.d.ts +17 -0
- package/dist/cache/response-store-adapter.self-contained.worker.js +10 -0
- package/dist/cache/response-store-adapter.service-binding.worker.d.ts +15 -0
- package/dist/cache/response-store-adapter.service-binding.worker.js +9 -0
- package/dist/cache/response-store-adapter.worker.d.ts +14 -0
- package/dist/cache/response-store-adapter.worker.js +254 -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 +289 -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 -11
- package/dist/cdn-warm.js +864 -82
- package/dist/cli.js +15 -1
- package/dist/deploy-config.d.ts +13 -10
- package/dist/deploy-config.js +21 -3
- package/dist/deploy-help.d.ts +3 -4
- package/dist/deploy-help.js +43 -6
- package/dist/deploy.d.ts +77 -23
- package/dist/deploy.js +666 -84
- package/dist/images/images-optimizer.d.ts +3 -4
- package/dist/prerender-kv-populate.d.ts +3 -4
- package/dist/prerender-kv-populate.js +4 -3
- package/dist/tpr.d.ts +9 -10
- package/dist/tpr.js +3 -3
- package/dist/utils/cache-control-metadata.d.ts +3 -4
- package/dist/version-deploy.d.ts +19 -16
- package/dist/version-deploy.js +29 -12
- package/dist/version-headers.d.ts +2 -0
- package/dist/version-headers.js +2 -0
- package/dist/worker-deployment-url.d.ts +8 -3
- package/dist/worker-deployment-url.js +23 -1
- package/dist/workers-dev-url.d.ts +2 -3
- package/dist/wrangler-version-metadata.d.ts +7 -0
- package/dist/wrangler-version-metadata.js +9 -0
- package/package.json +14 -7
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
import { VINEXT_CDN_BUILD_ID_HEADER, getVinextCdnBuildIdentity } from "./cdn-build-id.js";
|
|
2
|
+
import { NEXTJS_CACHE_HEADER, VINEXT_CACHE_HEADER, VINEXT_PRERENDER_READINESS_PATH, VINEXT_RSC_VARY_HEADER } from "vinext/internal/server/headers";
|
|
3
|
+
import { isNonCacheableCacheControl } from "vinext/shims/cdn-cache";
|
|
4
|
+
import { WorkerEntrypoint } from "cloudflare:workers";
|
|
5
|
+
import { loadVinextRequestStage } from "vinext/server/request-stage";
|
|
6
|
+
import { loadVinextResponseStage } from "vinext/server/response-stage";
|
|
7
|
+
//#region src/cache/cdn-adapter.worker.ts
|
|
8
|
+
const CACHED_RESPONSE_STAGE_EXPORT = "VinextCachedResponse";
|
|
9
|
+
const UNCACHED_RESPONSE_STAGE_EXPORT = "VinextUncachedResponse";
|
|
10
|
+
const AUTHORIZATION_TRANSPORT_HEADER = "x-vinext-internal-authorization";
|
|
11
|
+
const REQUEST_CACHE_CONTROL_TRANSPORT_HEADER = "x-vinext-internal-request-cache-control";
|
|
12
|
+
const REQUEST_CF_TRANSPORT_HEADER = "x-vinext-internal-request-cf";
|
|
13
|
+
const REQUEST_PRAGMA_TRANSPORT_HEADER = "x-vinext-internal-request-pragma";
|
|
14
|
+
const CLOUDFLARE_EDGE_POLICY_HEADER = "Cloudflare-CDN-Cache-Control";
|
|
15
|
+
const SHARED_RESPONSE_STAGE_HEADER = "x-vinext-cloudflare-shared-response-stage";
|
|
16
|
+
const RESPONSE_STAGE_WIRE_CACHE = {
|
|
17
|
+
bypass: "vinext-cloudflare-v1:bypass",
|
|
18
|
+
shared: "vinext-cloudflare-v1:shared"
|
|
19
|
+
};
|
|
20
|
+
const FRAMEWORK_RESPONSE_VARY_FIELDS = new Set(VINEXT_RSC_VARY_HEADER.split(",").map((name) => name.trim().toLowerCase()));
|
|
21
|
+
function isResponseStageReadinessRequest(request) {
|
|
22
|
+
return request.url.includes(VINEXT_PRERENDER_READINESS_PATH) && new URL(request.url).pathname === VINEXT_PRERENDER_READINESS_PATH;
|
|
23
|
+
}
|
|
24
|
+
function responseStageUnavailable() {
|
|
25
|
+
return new Response(null, {
|
|
26
|
+
status: 503,
|
|
27
|
+
headers: { "Cache-Control": "no-store" }
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
/** Stamp the entrypoint that actually produced a response, before Workers Cache stores it. */
|
|
31
|
+
function stampResponseStageBuildIdentity(response) {
|
|
32
|
+
const buildIdentity = getVinextCdnBuildIdentity();
|
|
33
|
+
if (!buildIdentity) return response;
|
|
34
|
+
try {
|
|
35
|
+
response.headers.set(VINEXT_CDN_BUILD_ID_HEADER, buildIdentity);
|
|
36
|
+
return response;
|
|
37
|
+
} catch {
|
|
38
|
+
const headers = new Headers(response.headers);
|
|
39
|
+
headers.set(VINEXT_CDN_BUILD_ID_HEADER, buildIdentity);
|
|
40
|
+
const webSocket = response.webSocket;
|
|
41
|
+
if (!webSocket && (response.status < 200 || response.status > 599)) {
|
|
42
|
+
const unavailable = responseStageUnavailable();
|
|
43
|
+
unavailable.headers.set(VINEXT_CDN_BUILD_ID_HEADER, buildIdentity);
|
|
44
|
+
return unavailable;
|
|
45
|
+
}
|
|
46
|
+
const init = {
|
|
47
|
+
headers,
|
|
48
|
+
status: response.status,
|
|
49
|
+
statusText: response.statusText
|
|
50
|
+
};
|
|
51
|
+
if (webSocket) init.webSocket = webSocket;
|
|
52
|
+
return new Response(response.body, init);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/** Reject a response routed to an entrypoint from another propagating build. */
|
|
56
|
+
function validateResponseStageBuildIdentity(response) {
|
|
57
|
+
const expectedBuildIdentity = getVinextCdnBuildIdentity();
|
|
58
|
+
if (!expectedBuildIdentity || response.headers.get("X-Vinext-Build-Id") === expectedBuildIdentity) return response;
|
|
59
|
+
response.body?.cancel().catch(() => {});
|
|
60
|
+
return responseStageUnavailable();
|
|
61
|
+
}
|
|
62
|
+
function stripUntrustedTransportHeaders(request) {
|
|
63
|
+
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;
|
|
64
|
+
const headers = new Headers(request.headers);
|
|
65
|
+
headers.delete(AUTHORIZATION_TRANSPORT_HEADER);
|
|
66
|
+
headers.delete(REQUEST_CACHE_CONTROL_TRANSPORT_HEADER);
|
|
67
|
+
headers.delete(REQUEST_CF_TRANSPORT_HEADER);
|
|
68
|
+
headers.delete(REQUEST_PRAGMA_TRANSPORT_HEADER);
|
|
69
|
+
const sanitized = new Request(request, { headers });
|
|
70
|
+
const requestCf = Reflect.get(request, "cf");
|
|
71
|
+
if (requestCf !== void 0) Object.defineProperty(sanitized, "cf", {
|
|
72
|
+
configurable: true,
|
|
73
|
+
enumerable: true,
|
|
74
|
+
value: requestCf
|
|
75
|
+
});
|
|
76
|
+
return sanitized;
|
|
77
|
+
}
|
|
78
|
+
function withWorkerHostRuntime(context, env) {
|
|
79
|
+
const candidateAssets = env && typeof env === "object" && Reflect.has(env, "ASSETS") ? Reflect.get(env, "ASSETS") : void 0;
|
|
80
|
+
const assets = candidateAssets && (typeof candidateAssets === "object" || typeof candidateAssets === "function") && Reflect.has(candidateAssets, "fetch") && typeof Reflect.get(candidateAssets, "fetch") === "function" ? candidateAssets : context?.assets;
|
|
81
|
+
return {
|
|
82
|
+
...assets === void 0 ? {} : { assets },
|
|
83
|
+
...context?.cache === void 0 ? {} : { cache: context.cache },
|
|
84
|
+
...context?.exports === void 0 ? {} : { exports: context.exports },
|
|
85
|
+
hostRuntime: "worker",
|
|
86
|
+
...typeof context?.passThroughOnException === "function" ? { passThroughOnException: () => context.passThroughOnException?.() } : {},
|
|
87
|
+
...context?.props === void 0 ? {} : { props: context.props },
|
|
88
|
+
...typeof context?.waitUntil === "function" ? { waitUntil: (promise) => context.waitUntil?.(promise) } : {}
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
function hasFetch(value) {
|
|
92
|
+
return value !== null && (typeof value === "object" || typeof value === "function") && "fetch" in value && typeof value.fetch === "function";
|
|
93
|
+
}
|
|
94
|
+
function hasPurge(value) {
|
|
95
|
+
return value !== null && (typeof value === "object" || typeof value === "function") && "purge" in value && typeof value.purge === "function";
|
|
96
|
+
}
|
|
97
|
+
function getResponseStageBinding(context, exportName, serializedInvocation) {
|
|
98
|
+
const binding = context.exports?.[exportName];
|
|
99
|
+
if (typeof binding !== "function") return null;
|
|
100
|
+
const target = binding({ props: JSON.parse(serializedInvocation) });
|
|
101
|
+
return hasFetch(target) ? target : null;
|
|
102
|
+
}
|
|
103
|
+
async function createCacheFacingRequest(request, serializedInvocation) {
|
|
104
|
+
const authorization = request.headers.get("Authorization");
|
|
105
|
+
let serializedRequestCf = null;
|
|
106
|
+
const requestCf = Reflect.get(request, "cf");
|
|
107
|
+
if (requestCf !== void 0) try {
|
|
108
|
+
const json = JSON.stringify(requestCf);
|
|
109
|
+
if (json !== void 0) serializedRequestCf = encodeURIComponent(json);
|
|
110
|
+
} catch {}
|
|
111
|
+
const authorizationIdentity = authorization === null ? "absent" : `present:${authorization.length}:${authorization}`;
|
|
112
|
+
const frameworkVaryIdentity = JSON.stringify([...FRAMEWORK_RESPONSE_VARY_FIELDS].map((name) => [name, request.headers.get(name)]));
|
|
113
|
+
const responseStageBuildIdentity = getVinextCdnBuildIdentity() ?? "";
|
|
114
|
+
const bytes = new TextEncoder().encode(`${request.url}\0${serializedInvocation}\0${authorizationIdentity}\0${frameworkVaryIdentity}\0${responseStageBuildIdentity}`);
|
|
115
|
+
const key = [...new Uint8Array(await crypto.subtle.digest("SHA-256", bytes))].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
116
|
+
const url = new URL(request.url);
|
|
117
|
+
url.searchParams.set("__vinext_cache_key", key);
|
|
118
|
+
const headers = new Headers(request.headers);
|
|
119
|
+
const requestCacheControl = headers.get("Cache-Control");
|
|
120
|
+
const requestPragma = headers.get("Pragma");
|
|
121
|
+
headers.delete("Cache-Control");
|
|
122
|
+
headers.delete("Pragma");
|
|
123
|
+
headers.delete("Authorization");
|
|
124
|
+
headers.delete(AUTHORIZATION_TRANSPORT_HEADER);
|
|
125
|
+
headers.delete(REQUEST_CACHE_CONTROL_TRANSPORT_HEADER);
|
|
126
|
+
headers.delete(REQUEST_CF_TRANSPORT_HEADER);
|
|
127
|
+
headers.delete(REQUEST_PRAGMA_TRANSPORT_HEADER);
|
|
128
|
+
if (authorization !== null) headers.set(AUTHORIZATION_TRANSPORT_HEADER, encodeURIComponent(authorization));
|
|
129
|
+
if (serializedRequestCf !== null) headers.set(REQUEST_CF_TRANSPORT_HEADER, serializedRequestCf);
|
|
130
|
+
if (requestCacheControl !== null) headers.set(REQUEST_CACHE_CONTROL_TRANSPORT_HEADER, encodeURIComponent(requestCacheControl));
|
|
131
|
+
if (requestPragma !== null) headers.set(REQUEST_PRAGMA_TRANSPORT_HEADER, encodeURIComponent(requestPragma));
|
|
132
|
+
return new Request(url, {
|
|
133
|
+
cf: { vary: { default: { action: "passthrough" } } },
|
|
134
|
+
headers,
|
|
135
|
+
method: request.method
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
function restoreResponseStageRequest(request, requestUrl, requestMethod) {
|
|
139
|
+
const headers = new Headers(request.headers);
|
|
140
|
+
const serializedAuthorization = headers.get(AUTHORIZATION_TRANSPORT_HEADER);
|
|
141
|
+
const serializedRequestCacheControl = headers.get(REQUEST_CACHE_CONTROL_TRANSPORT_HEADER);
|
|
142
|
+
const serializedRequestCf = headers.get(REQUEST_CF_TRANSPORT_HEADER);
|
|
143
|
+
const serializedRequestPragma = headers.get(REQUEST_PRAGMA_TRANSPORT_HEADER);
|
|
144
|
+
headers.delete("Cache-Control");
|
|
145
|
+
headers.delete("Pragma");
|
|
146
|
+
headers.delete("Authorization");
|
|
147
|
+
headers.delete(AUTHORIZATION_TRANSPORT_HEADER);
|
|
148
|
+
headers.delete(REQUEST_CACHE_CONTROL_TRANSPORT_HEADER);
|
|
149
|
+
headers.delete(REQUEST_CF_TRANSPORT_HEADER);
|
|
150
|
+
headers.delete(REQUEST_PRAGMA_TRANSPORT_HEADER);
|
|
151
|
+
if (serializedAuthorization !== null) try {
|
|
152
|
+
headers.set("Authorization", decodeURIComponent(serializedAuthorization));
|
|
153
|
+
} catch {}
|
|
154
|
+
let requestCf;
|
|
155
|
+
if (serializedRequestCf !== null) try {
|
|
156
|
+
requestCf = JSON.parse(decodeURIComponent(serializedRequestCf));
|
|
157
|
+
} catch {}
|
|
158
|
+
if (serializedRequestCacheControl !== null) try {
|
|
159
|
+
headers.set("Cache-Control", decodeURIComponent(serializedRequestCacheControl));
|
|
160
|
+
} catch {}
|
|
161
|
+
if (serializedRequestPragma !== null) try {
|
|
162
|
+
headers.set("Pragma", decodeURIComponent(serializedRequestPragma));
|
|
163
|
+
} catch {}
|
|
164
|
+
const restored = new Request(new Request(requestUrl, request), {
|
|
165
|
+
headers,
|
|
166
|
+
method: requestMethod
|
|
167
|
+
});
|
|
168
|
+
let didAccessRequestCf = false;
|
|
169
|
+
if (requestCf !== void 0) Object.defineProperty(restored, "cf", {
|
|
170
|
+
configurable: true,
|
|
171
|
+
enumerable: true,
|
|
172
|
+
get() {
|
|
173
|
+
didAccessRequestCf = true;
|
|
174
|
+
return requestCf;
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
return {
|
|
178
|
+
didAccessRequestCf: () => didAccessRequestCf,
|
|
179
|
+
request: restored
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
function preventResponseCaching(response) {
|
|
183
|
+
const headers = new Headers(response.headers);
|
|
184
|
+
headers.set("Cache-Control", "no-store");
|
|
185
|
+
headers.delete("CDN-Cache-Control");
|
|
186
|
+
headers.delete("Cloudflare-CDN-Cache-Control");
|
|
187
|
+
headers.delete("Cache-Tag");
|
|
188
|
+
return new Response(response.body, {
|
|
189
|
+
headers,
|
|
190
|
+
status: response.status,
|
|
191
|
+
statusText: response.statusText
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Cloudflare consumes its private cache policy before returning a cached
|
|
196
|
+
* entrypoint response. Strip it here as well for uncached/local fallbacks so
|
|
197
|
+
* the outer gateway never forwards an inner shared-cache policy after adding
|
|
198
|
+
* request-specific middleware or routing headers.
|
|
199
|
+
*/
|
|
200
|
+
function markSharedResponseStage(response, provenanceToken, exposeEntrypointCacheStatus = false) {
|
|
201
|
+
const headers = new Headers(response.headers);
|
|
202
|
+
const cacheStatus = exposeEntrypointCacheStatus ? headers.get("CF-Cache-Status") : null;
|
|
203
|
+
headers.set(SHARED_RESPONSE_STAGE_HEADER, cacheStatus ? `${provenanceToken}:${encodeURIComponent(cacheStatus)}` : provenanceToken);
|
|
204
|
+
return new Response(response.body, {
|
|
205
|
+
headers,
|
|
206
|
+
status: response.status,
|
|
207
|
+
statusText: response.statusText
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
function finalizeGatewayResponse(response, provenanceToken) {
|
|
211
|
+
const provenance = response.headers.get(SHARED_RESPONSE_STAGE_HEADER);
|
|
212
|
+
const usedSharedResponseStage = provenance === provenanceToken || provenance?.startsWith(`${provenanceToken}:`) === true;
|
|
213
|
+
const sharedResponseStageCollision = provenance !== null && !usedSharedResponseStage;
|
|
214
|
+
const cacheControl = response.headers.get("Cache-Control");
|
|
215
|
+
if (!response.headers.has(CLOUDFLARE_EDGE_POLICY_HEADER) && !usedSharedResponseStage && !sharedResponseStageCollision) return response;
|
|
216
|
+
const headers = new Headers(response.headers);
|
|
217
|
+
headers.delete(SHARED_RESPONSE_STAGE_HEADER);
|
|
218
|
+
headers.delete(CLOUDFLARE_EDGE_POLICY_HEADER);
|
|
219
|
+
if (usedSharedResponseStage || sharedResponseStageCollision) {
|
|
220
|
+
headers.delete("CDN-Cache-Control");
|
|
221
|
+
headers.delete("Cache-Tag");
|
|
222
|
+
headers.delete(VINEXT_CACHE_HEADER);
|
|
223
|
+
headers.delete(NEXTJS_CACHE_HEADER);
|
|
224
|
+
if (!cacheControl || !isNonCacheableCacheControl(cacheControl)) headers.set("Cache-Control", "private, max-age=0, must-revalidate");
|
|
225
|
+
}
|
|
226
|
+
if (usedSharedResponseStage && provenance?.startsWith(`${provenanceToken}:`)) {
|
|
227
|
+
const cacheStatus = decodeURIComponent(provenance.slice(provenanceToken.length + 1));
|
|
228
|
+
headers.set(VINEXT_CACHE_HEADER, cacheStatus);
|
|
229
|
+
headers.set(NEXTJS_CACHE_HEADER, cacheStatus);
|
|
230
|
+
}
|
|
231
|
+
return new Response(response.body, {
|
|
232
|
+
headers,
|
|
233
|
+
status: response.status,
|
|
234
|
+
statusText: response.statusText
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
function hasTaggedCustomVary(response) {
|
|
238
|
+
if (!response.headers.get("Cache-Tag")) return false;
|
|
239
|
+
return (response.headers.get("Vary") ?? "").split(",").map((name) => name.trim().toLowerCase()).filter(Boolean).some((name) => !FRAMEWORK_RESPONSE_VARY_FIELDS.has(name));
|
|
240
|
+
}
|
|
241
|
+
function withResponseStagePurge(context) {
|
|
242
|
+
const factory = context.exports?.[CACHED_RESPONSE_STAGE_EXPORT];
|
|
243
|
+
if (typeof factory !== "function") return context;
|
|
244
|
+
const fallback = context.cache;
|
|
245
|
+
return {
|
|
246
|
+
...context,
|
|
247
|
+
cache: { purge(options) {
|
|
248
|
+
const target = factory({ props: {} });
|
|
249
|
+
if (hasPurge(target)) return target.purge(options);
|
|
250
|
+
return hasPurge(fallback) ? fallback.purge(options) : void 0;
|
|
251
|
+
} }
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
function getResponseStageInvocation(value, expectedCache) {
|
|
255
|
+
if (!value || typeof value !== "object") return null;
|
|
256
|
+
const expectedResponseStageBuildIdentity = Reflect.get(value, "expectedResponseStageBuildIdentity");
|
|
257
|
+
if (expectedResponseStageBuildIdentity !== void 0 && typeof expectedResponseStageBuildIdentity !== "string") return null;
|
|
258
|
+
const options = Reflect.get(value, "options");
|
|
259
|
+
if (!options || typeof options !== "object") return null;
|
|
260
|
+
const wireCache = Reflect.get(options, "cache");
|
|
261
|
+
let cache;
|
|
262
|
+
if (expectedResponseStageBuildIdentity !== void 0 && wireCache === RESPONSE_STAGE_WIRE_CACHE.shared) cache = "shared";
|
|
263
|
+
else if (expectedResponseStageBuildIdentity !== void 0 && wireCache === RESPONSE_STAGE_WIRE_CACHE.bypass) cache = "bypass";
|
|
264
|
+
else if (expectedResponseStageBuildIdentity === void 0 && getVinextCdnBuildIdentity() === null && (wireCache === "shared" || wireCache === "bypass")) cache = wireCache;
|
|
265
|
+
else return null;
|
|
266
|
+
if (expectedCache !== void 0 && cache !== expectedCache) return null;
|
|
267
|
+
const requestUrl = Reflect.get(value, "requestUrl");
|
|
268
|
+
if (typeof requestUrl !== "string") return null;
|
|
269
|
+
const requestMethod = Reflect.get(value, "requestMethod");
|
|
270
|
+
if (typeof requestMethod !== "string" || requestMethod.length === 0) return null;
|
|
271
|
+
try {
|
|
272
|
+
new URL(requestUrl);
|
|
273
|
+
} catch {
|
|
274
|
+
return null;
|
|
275
|
+
}
|
|
276
|
+
return {
|
|
277
|
+
...typeof expectedResponseStageBuildIdentity === "string" ? { expectedResponseStageBuildIdentity } : {},
|
|
278
|
+
options: {
|
|
279
|
+
...options,
|
|
280
|
+
cache
|
|
281
|
+
},
|
|
282
|
+
props: Reflect.get(value, "props"),
|
|
283
|
+
requestMethod,
|
|
284
|
+
requestUrl
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
async function invokeResponseStage(request, env, context, invocation) {
|
|
288
|
+
const dispatchResponseStage = (stageRequest, props, options) => invokeResponseStage(stageRequest, env, context, {
|
|
289
|
+
options,
|
|
290
|
+
props,
|
|
291
|
+
requestMethod: stageRequest.method,
|
|
292
|
+
requestUrl: stageRequest.url
|
|
293
|
+
});
|
|
294
|
+
const dispatchRequestStage = async (stageRequest) => {
|
|
295
|
+
const { handleRequestStage } = await loadVinextRequestStage();
|
|
296
|
+
return handleRequestStage(stageRequest, env, context, dispatchResponseStage);
|
|
297
|
+
};
|
|
298
|
+
const { handleResponseStage } = await loadVinextResponseStage();
|
|
299
|
+
return handleResponseStage(request, env, context, invocation.props, dispatchRequestStage, invocation.options);
|
|
300
|
+
}
|
|
301
|
+
/** Cache-bearing entrypoint. Workers Cache HITs bypass this class entirely. */
|
|
302
|
+
var VinextCachedResponse = class extends WorkerEntrypoint {
|
|
303
|
+
async fetch(request) {
|
|
304
|
+
const context = withWorkerHostRuntime(this.ctx, this.env);
|
|
305
|
+
const invocation = getResponseStageInvocation(context.props, "shared");
|
|
306
|
+
if (!invocation) return stampResponseStageBuildIdentity(new Response("Invalid vinext response-stage invocation", {
|
|
307
|
+
status: 400,
|
|
308
|
+
headers: { "Cache-Control": "no-store" }
|
|
309
|
+
}));
|
|
310
|
+
if (invocation.expectedResponseStageBuildIdentity !== void 0 && invocation.expectedResponseStageBuildIdentity !== getVinextCdnBuildIdentity()) return stampResponseStageBuildIdentity(responseStageUnavailable());
|
|
311
|
+
const restored = restoreResponseStageRequest(request, invocation.requestUrl, invocation.requestMethod);
|
|
312
|
+
const response = await invokeResponseStage(restored.request, this.env, context, invocation);
|
|
313
|
+
return stampResponseStageBuildIdentity(restored.didAccessRequestCf() || hasTaggedCustomVary(response) ? preventResponseCaching(response) : response);
|
|
314
|
+
}
|
|
315
|
+
async purge(options) {
|
|
316
|
+
const cache = Reflect.get(this.ctx, "cache");
|
|
317
|
+
if (!hasPurge(cache)) return void 0;
|
|
318
|
+
return cache.purge(options);
|
|
319
|
+
}
|
|
320
|
+
};
|
|
321
|
+
/** Uncached response entrypoint. Bypass and probe renders execute only here. */
|
|
322
|
+
var VinextUncachedResponse = class extends WorkerEntrypoint {
|
|
323
|
+
async fetch(request) {
|
|
324
|
+
const context = withResponseStagePurge(withWorkerHostRuntime(this.ctx, this.env));
|
|
325
|
+
const invocation = getResponseStageInvocation(context.props, "bypass");
|
|
326
|
+
if (!invocation) return stampResponseStageBuildIdentity(new Response("Invalid vinext response-stage invocation", {
|
|
327
|
+
status: 400,
|
|
328
|
+
headers: { "Cache-Control": "no-store" }
|
|
329
|
+
}));
|
|
330
|
+
if (invocation.expectedResponseStageBuildIdentity !== void 0 && invocation.expectedResponseStageBuildIdentity !== getVinextCdnBuildIdentity()) return stampResponseStageBuildIdentity(responseStageUnavailable());
|
|
331
|
+
return stampResponseStageBuildIdentity(await invokeResponseStage(request, this.env, context, invocation));
|
|
332
|
+
}
|
|
333
|
+
};
|
|
334
|
+
/** Uncached gateway: request routing and middleware always execute here. */
|
|
335
|
+
var cdn_adapter_worker_default = { async fetch(request, env, context) {
|
|
336
|
+
request = stripUntrustedTransportHeaders(request);
|
|
337
|
+
const sharedResponseStageProvenance = crypto.randomUUID();
|
|
338
|
+
const stageContext = withResponseStagePurge(withWorkerHostRuntime(context, env));
|
|
339
|
+
const dispatchResponseStage = async (stageRequest, props, options) => {
|
|
340
|
+
const expectedResponseStageBuildIdentity = getVinextCdnBuildIdentity();
|
|
341
|
+
const invocation = {
|
|
342
|
+
...expectedResponseStageBuildIdentity === null ? {} : { expectedResponseStageBuildIdentity },
|
|
343
|
+
options,
|
|
344
|
+
props,
|
|
345
|
+
requestMethod: stageRequest.method,
|
|
346
|
+
requestUrl: stageRequest.url
|
|
347
|
+
};
|
|
348
|
+
const usesSharedCache = options.cache === "shared";
|
|
349
|
+
try {
|
|
350
|
+
const serializedInvocation = JSON.stringify({
|
|
351
|
+
...invocation,
|
|
352
|
+
options: expectedResponseStageBuildIdentity === null ? options : {
|
|
353
|
+
...options,
|
|
354
|
+
cache: RESPONSE_STAGE_WIRE_CACHE[options.cache]
|
|
355
|
+
}
|
|
356
|
+
});
|
|
357
|
+
const binding = getResponseStageBinding(stageContext, usesSharedCache ? CACHED_RESPONSE_STAGE_EXPORT : UNCACHED_RESPONSE_STAGE_EXPORT, serializedInvocation);
|
|
358
|
+
if (!binding) return responseStageUnavailable();
|
|
359
|
+
const entrypointRequest = usesSharedCache ? await createCacheFacingRequest(stageRequest, serializedInvocation) : stageRequest;
|
|
360
|
+
const response = validateResponseStageBuildIdentity(await binding.fetch(entrypointRequest));
|
|
361
|
+
return usesSharedCache ? markSharedResponseStage(response, sharedResponseStageProvenance, true) : response;
|
|
362
|
+
} catch (error) {
|
|
363
|
+
if (isResponseStageReadinessRequest(stageRequest)) return responseStageUnavailable();
|
|
364
|
+
throw error;
|
|
365
|
+
}
|
|
366
|
+
};
|
|
367
|
+
const { handleRequestStage } = await loadVinextRequestStage();
|
|
368
|
+
return finalizeGatewayResponse(await handleRequestStage(request, env, stageContext, dispatchResponseStage), sharedResponseStageProvenance);
|
|
369
|
+
} };
|
|
370
|
+
//#endregion
|
|
371
|
+
export { VinextCachedResponse, VinextUncachedResponse, cdn_adapter_worker_default as default };
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
//#region src/cache/cdn-build-id.d.ts
|
|
2
|
+
/** Build identity stamped by the Cloudflare CDN adapter on page responses. */
|
|
3
|
+
export declare const VINEXT_CDN_BUILD_ID_HEADER = "X-Vinext-Build-Id";
|
|
4
|
+
/** Opaque identity shared by every server entry emitted by one vinext build. */
|
|
5
|
+
export declare function getVinextCdnBuildIdentity(): string | null;
|
|
6
|
+
//#endregion
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
//#region src/cache/cdn-build-id.ts
|
|
2
|
+
/** Build identity stamped by the Cloudflare CDN adapter on page responses. */
|
|
3
|
+
const VINEXT_CDN_BUILD_ID_HEADER = "X-Vinext-Build-Id";
|
|
4
|
+
/** Opaque identity shared by every server entry emitted by one vinext build. */
|
|
5
|
+
function getVinextCdnBuildIdentity() {
|
|
6
|
+
return process.env.__VINEXT_RSC_BUILD_IDENTITY || process.env.__VINEXT_BUILD_ID || null;
|
|
7
|
+
}
|
|
8
|
+
//#endregion
|
|
9
|
+
export { VINEXT_CDN_BUILD_ID_HEADER, getVinextCdnBuildIdentity };
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
//#region src/cache/kv-data-adapter.d.ts
|
|
2
|
-
declare const DEFAULT_KV_DATA_CACHE_BINDING = "VINEXT_KV_CACHE";
|
|
2
|
+
export declare const DEFAULT_KV_DATA_CACHE_BINDING = "VINEXT_KV_CACHE";
|
|
3
3
|
/** Options accepted by {@link kvDataAdapter}, forwarded to the runtime factory. */
|
|
4
|
-
type KvDataAdapterOptions = {
|
|
4
|
+
export type KvDataAdapterOptions = {
|
|
5
5
|
/** KV namespace binding name on the Worker `env`. @default "VINEXT_KV_CACHE" */
|
|
6
6
|
binding?: string;
|
|
7
7
|
/** Namespace prefix for cache keys (isolates multiple apps in one namespace). */
|
|
@@ -10,6 +10,23 @@ type KvDataAdapterOptions = {
|
|
|
10
10
|
ttlSeconds?: number;
|
|
11
11
|
/** TTL in milliseconds for the in-memory tag-invalidation cache. @default 5000 */
|
|
12
12
|
tagCacheTtlMs?: number;
|
|
13
|
+
/**
|
|
14
|
+
* KV `cacheTtl` in seconds for entry reads, letting a colo answer a repeat
|
|
15
|
+
* read from its own cache instead of the central store. The runtime rejects
|
|
16
|
+
* a value below 30, so lower values are raised to 30.
|
|
17
|
+
*
|
|
18
|
+
* Trade-off: after a `set()`, a colo that already cached the key can serve
|
|
19
|
+
* the superseded value for up to this long.
|
|
20
|
+
*
|
|
21
|
+
* It applies to entry reads only. Tag markers, which `revalidateTag()` and
|
|
22
|
+
* `revalidatePath()` write, keep KV's own default cacheTtl of 60 s rather
|
|
23
|
+
* than this longer one, so this option never widens the window in which a
|
|
24
|
+
* colo can miss a publish. That window stays `tagCacheTtlMs` plus whatever
|
|
25
|
+
* the KV default cache holds, with or without this option.
|
|
26
|
+
*
|
|
27
|
+
* @default undefined (entry reads keep KV's default cacheTtl of 60 s)
|
|
28
|
+
*/
|
|
29
|
+
entryCacheTtlSeconds?: number;
|
|
13
30
|
};
|
|
14
31
|
/**
|
|
15
32
|
* Cloudflare KV data cache.
|
|
@@ -24,9 +41,8 @@ type KvDataAdapterOptions = {
|
|
|
24
41
|
* }
|
|
25
42
|
* ```
|
|
26
43
|
*/
|
|
27
|
-
declare function kvDataAdapter(options?: KvDataAdapterOptions): {
|
|
44
|
+
export declare function kvDataAdapter(options?: KvDataAdapterOptions): {
|
|
28
45
|
adapter: string;
|
|
29
46
|
options: KvDataAdapterOptions | undefined;
|
|
30
47
|
};
|
|
31
|
-
//#endregion
|
|
32
|
-
export { DEFAULT_KV_DATA_CACHE_BINDING, KvDataAdapterOptions, kvDataAdapter };
|
|
48
|
+
//#endregion
|
|
@@ -6,10 +6,16 @@ import { CacheHandler, CacheHandlerValue, IncrementalCacheValue } from "vinext/s
|
|
|
6
6
|
type KVNamespace = {
|
|
7
7
|
get(key: string, options?: {
|
|
8
8
|
type?: string;
|
|
9
|
+
cacheTtl?: number;
|
|
9
10
|
}): Promise<string | null>;
|
|
10
11
|
get(key: string, options: {
|
|
11
12
|
type: "arrayBuffer";
|
|
12
13
|
}): Promise<ArrayBuffer | null>;
|
|
14
|
+
/** Bulk read, capped at 100 keys per call. */
|
|
15
|
+
get(keys: string[], options?: {
|
|
16
|
+
type?: string;
|
|
17
|
+
cacheTtl?: number;
|
|
18
|
+
}): Promise<Map<string, string | null>>;
|
|
13
19
|
put(key: string, value: string | ArrayBuffer | ReadableStream, options?: {
|
|
14
20
|
expirationTtl?: number;
|
|
15
21
|
metadata?: Record<string, unknown>;
|
|
@@ -49,28 +55,50 @@ type KVNamespace = {
|
|
|
49
55
|
* the first request. This class stays exported for the runtime factory and for
|
|
50
56
|
* backwards compatibility, but is not the recommended consumer API.
|
|
51
57
|
*/
|
|
52
|
-
declare class KVCacheHandler implements CacheHandler {
|
|
58
|
+
export declare class KVCacheHandler implements CacheHandler {
|
|
53
59
|
private kv;
|
|
54
60
|
private keySpace;
|
|
55
61
|
private ctx;
|
|
56
62
|
private ttlSeconds;
|
|
57
63
|
/** Local in-memory cache for tag invalidation timestamps. Avoids redundant KV reads. */
|
|
58
64
|
private _tagCache;
|
|
65
|
+
/** Monotonic ordering for concurrent tag-cache fills and local invalidations. */
|
|
66
|
+
private _tagCacheOrder;
|
|
59
67
|
/** TTL (ms) for local tag cache entries. After this, re-fetch from KV. */
|
|
60
68
|
private _tagCacheTtl;
|
|
69
|
+
/** Read options for entry keys only. `undefined` keeps the KV default cacheTtl. */
|
|
70
|
+
private _entryReadOptions;
|
|
61
71
|
constructor(kvNamespace: KVNamespace, options?: {
|
|
62
72
|
appPrefix?: string;
|
|
63
73
|
ctx?: ExecutionContextLike;
|
|
64
74
|
ttlSeconds?: number;
|
|
65
75
|
/** TTL in milliseconds for the local tag cache. Defaults to 5000ms. */
|
|
66
76
|
tagCacheTtlMs?: number;
|
|
77
|
+
/** KV `cacheTtl` in seconds for entry reads. Off by default, never used for tag markers. */
|
|
78
|
+
entryCacheTtlSeconds?: number;
|
|
67
79
|
});
|
|
68
80
|
private _entryKey;
|
|
69
81
|
private _tagKey;
|
|
70
82
|
get(key: string, _ctx?: Record<string, unknown>): Promise<CacheHandlerValue | null>;
|
|
71
83
|
/**
|
|
72
|
-
*
|
|
73
|
-
*
|
|
84
|
+
* Load the invalidation markers these tags still need into the local cache.
|
|
85
|
+
* Tags a recent read already cached cost nothing, so the entry-tag batch only
|
|
86
|
+
* pays for what the soft-tag batch did not already fetch.
|
|
87
|
+
*/
|
|
88
|
+
private _primeTagCache;
|
|
89
|
+
/**
|
|
90
|
+
* Read every marker key, one round trip per 100-key chunk.
|
|
91
|
+
*
|
|
92
|
+
* Markers never take the entry `cacheTtl`: `revalidateTag()` writes them, so
|
|
93
|
+
* a longer colo cache would hide a publish from that colo for its span. They
|
|
94
|
+
* keep KV's own default cacheTtl of 60 s.
|
|
95
|
+
*/
|
|
96
|
+
private _readTagMarkers;
|
|
97
|
+
/**
|
|
98
|
+
* Report whether any tag has an invalidation marker at or after `lastModified`.
|
|
99
|
+
* Call `_primeTagCache` for the same tags first — a tag with no cache entry
|
|
100
|
+
* counts as never invalidated. Pass `requireFresh` to call it before a prime,
|
|
101
|
+
* so an entry past `tagCacheTtlMs` does not answer for a tag it never re-read.
|
|
74
102
|
*/
|
|
75
103
|
private _hasRevalidatedTag;
|
|
76
104
|
set(key: string, data: IncrementalCacheValue | null, ctx?: Record<string, unknown>): Promise<void>;
|
|
@@ -109,6 +137,12 @@ declare class KVCacheHandler implements CacheHandler {
|
|
|
109
137
|
* fresh KVCacheHandler per request or invoke this method explicitly.
|
|
110
138
|
*/
|
|
111
139
|
resetRequestCache(): void;
|
|
140
|
+
/**
|
|
141
|
+
* Clean up a bad entry only when this read cannot be an explicitly cached
|
|
142
|
+
* older version of the shared KV value. KV has no conditional delete, so a
|
|
143
|
+
* cached read must remain a non-destructive miss.
|
|
144
|
+
*/
|
|
145
|
+
private _deleteEntryReadInBackground;
|
|
112
146
|
/**
|
|
113
147
|
* Fire a KV delete in the background.
|
|
114
148
|
* Prefers the per-request ExecutionContext from ALS (set by
|
|
@@ -130,4 +164,4 @@ declare const createKvDataCacheAdapter: ({ env, options }: {
|
|
|
130
164
|
options?: KvDataAdapterOptions;
|
|
131
165
|
}) => CacheHandler;
|
|
132
166
|
//#endregion
|
|
133
|
-
export { ENTRY_PREFIX,
|
|
167
|
+
export { ENTRY_PREFIX, createKvDataCacheAdapter as default };
|