@vinext/cloudflare 1.0.0-beta.9 → 1.0.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/README.md +165 -37
- package/dist/cache/browser-cache-policy.d.ts +7 -0
- package/dist/cache/browser-cache-policy.js +39 -0
- package/dist/cache/cdn-adapter-config.d.ts +2 -1
- package/dist/cache/cdn-adapter-config.js +13 -5
- package/dist/cache/cdn-adapter.d.ts +5 -56
- package/dist/cache/cdn-adapter.js +3 -61
- package/dist/cache/cdn-adapter.runtime.d.ts +1 -0
- package/dist/cache/cdn-adapter.runtime.js +33 -19
- package/dist/cache/cdn-adapter.worker.d.ts +1 -1
- package/dist/cache/cdn-adapter.worker.js +10 -35
- package/dist/cache/cdn-build-id.d.ts +1 -1
- package/dist/cache/cdn-build-id.js +1 -1
- package/dist/cache/config.d.ts +137 -0
- package/dist/cache/config.js +87 -0
- package/dist/cache/kv-data-adapter.d.ts +1 -1
- package/dist/cache/kv-data-adapter.js +1 -1
- package/dist/cache/kv-data-adapter.runtime.js +17 -15
- package/dist/cache/kv-key.d.ts +7 -4
- package/dist/cache/kv-key.js +17 -6
- package/dist/cache/response-store-adapter.d.ts +9 -6
- package/dist/cache/response-store-adapter.js +20 -1
- package/dist/cache/response-store-adapter.worker.js +121 -48
- package/dist/cache/response-store-cdn.runtime.js +1 -0
- package/dist/cache/response-store-data.runtime.js +53 -31
- package/dist/cache/static-assets-adapter.build.d.ts +6 -0
- package/dist/cache/static-assets-adapter.build.js +102 -0
- package/dist/cache/static-assets-adapter.d.ts +27 -0
- package/dist/cache/static-assets-adapter.js +26 -0
- package/dist/cache/static-assets-adapter.runtime.d.ts +27 -0
- package/dist/cache/static-assets-adapter.runtime.js +94 -0
- package/dist/cache/static-assets-adapter.shared.d.ts +12 -0
- package/dist/cache/static-assets-adapter.shared.js +4 -0
- package/dist/cache/workers-cache-cdn-adapter.d.ts +58 -0
- package/dist/cache/workers-cache-cdn-adapter.js +65 -0
- package/dist/cacheability-artifact.d.ts +7 -1
- package/dist/cacheability-artifact.js +60 -21
- package/dist/cacheability-probe.d.ts +2 -0
- package/dist/cacheability-probe.js +107 -13
- package/dist/cdn-warm.d.ts +1 -0
- package/dist/cdn-warm.js +4 -3
- package/dist/deploy-config.d.ts +3 -8
- package/dist/deploy-config.js +5 -23
- package/dist/deploy-help.js +47 -42
- package/dist/deploy.d.ts +18 -16
- package/dist/deploy.js +355 -216
- package/dist/tpr.d.ts +4 -0
- package/dist/tpr.js +16 -7
- package/dist/version-deploy.d.ts +27 -0
- package/dist/version-deploy.js +121 -3
- package/dist/wrangler-version-metadata.js +1 -1
- package/package.json +16 -5
- package/dist/prerender-kv-populate.d.ts +0 -24
- package/dist/prerender-kv-populate.js +0 -153
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { SHARED_RESPONSE_STAGE_HEADER, finalizeGatewayResponse } from "./browser-cache-policy.js";
|
|
1
2
|
import { captureResponseStoreDataRegeneration, runWithResponseStoreInvocation, setResponseStore } from "./response-store-data.runtime.js";
|
|
2
|
-
import {
|
|
3
|
+
import { VINEXT_PARAMS_HEADER, VINEXT_RENDERED_PATH_AND_SEARCH_HEADER } from "vinext/internal/server/headers";
|
|
4
|
+
import { VINEXT_RSC_CONTENT_TYPE, VINEXT_RSC_VARY_HEADER as VINEXT_RSC_VARY_HEADER$1, applyRscCompatibilityIdHeader, applyRscDeploymentIdHeader, createCanonicalRscRequestHeaders, createCanonicalRscRequestUrl } from "vinext/internal/server/app-rsc-cache-busting";
|
|
3
5
|
import { isNonCacheableCacheControl } from "vinext/shims/cdn-cache";
|
|
4
6
|
import { loadVinextRequestStage } from "vinext/server/request-stage";
|
|
5
7
|
import { loadVinextResponseStage } from "vinext/server/response-stage";
|
|
@@ -9,7 +11,7 @@ const ROUTE_REVALIDATOR_ID = "vinext:response";
|
|
|
9
11
|
const RESPONSE_STORE_KEY_PARAM = "__workers_response_store";
|
|
10
12
|
const AGE_BASIS_HEADER = "X-Workers-Response-Store-Age-Basis";
|
|
11
13
|
const WARMUP_USER_AGENT = "vinext-cloudflare-cdn-warm";
|
|
12
|
-
const REPLAY_REQUEST_HEADERS = VINEXT_RSC_VARY_HEADER.split(",").map((name) => name.trim().toLowerCase());
|
|
14
|
+
const REPLAY_REQUEST_HEADERS = VINEXT_RSC_VARY_HEADER$1.split(",").map((name) => name.trim().toLowerCase());
|
|
13
15
|
const CACHE_REQUEST_VARY_HEADERS = REPLAY_REQUEST_HEADERS.map((name) => [name, "vinext-keyed"]);
|
|
14
16
|
function stageContext(ctx, env) {
|
|
15
17
|
const assets = Reflect.get(env, "ASSETS");
|
|
@@ -42,15 +44,18 @@ function isReplayableInvocation(request, props) {
|
|
|
42
44
|
if (request.headers.has("authorization") || request.headers.has("cookie")) return false;
|
|
43
45
|
return !(props && typeof props === "object" && ["draftModeCookie", "middlewareCookieOverlay"].some((name) => name in props && Reflect.get(props, name) !== null));
|
|
44
46
|
}
|
|
45
|
-
function
|
|
46
|
-
return
|
|
47
|
+
function prepareInvocation(request, props) {
|
|
48
|
+
return {
|
|
47
49
|
props: safeProps(props),
|
|
48
50
|
request: {
|
|
49
51
|
headers: replayHeaders(request),
|
|
50
52
|
method: request.method,
|
|
51
53
|
url: request.url
|
|
52
54
|
}
|
|
53
|
-
}
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
function serializeInvocation(request, props) {
|
|
58
|
+
return JSON.stringify(prepareInvocation(request, props));
|
|
54
59
|
}
|
|
55
60
|
function parseInvocation(value) {
|
|
56
61
|
if (typeof value !== "string") throw new TypeError("Invalid vinext response-store invocation");
|
|
@@ -68,17 +73,23 @@ async function invokeRequestStage(request, env, ctx) {
|
|
|
68
73
|
const { handleRequestStage } = await loadVinextRequestStage();
|
|
69
74
|
return handleRequestStage(request, env, stageContext(ctx, env), (request, props, options) => invokeResponseStage(request, props, env, ctx, options.cache));
|
|
70
75
|
}
|
|
71
|
-
async function invokeResponseStage(request, props, env, ctx, cache, capture) {
|
|
76
|
+
async function invokeResponseStage(request, props, env, ctx, cache, capture, invocation) {
|
|
72
77
|
const context = stageContext(ctx, env);
|
|
73
78
|
const dispatchRequestStage = (request) => invokeRequestStage(request, env, ctx);
|
|
74
79
|
const { handleResponseStage } = await loadVinextResponseStage();
|
|
75
|
-
const
|
|
76
|
-
|
|
80
|
+
const storedInvocation = invocation ?? {
|
|
81
|
+
replayable: isReplayableInvocation(request, props),
|
|
82
|
+
serialized: serializeInvocation(request, props)
|
|
83
|
+
};
|
|
84
|
+
return runWithResponseStoreInvocation(storedInvocation.serialized, storedInvocation.replayable, () => handleResponseStage(request, env, context, props, dispatchRequestStage, { cache }), capture);
|
|
77
85
|
}
|
|
78
86
|
function createVinextResponseStoreOptions(configuration) {
|
|
87
|
+
const locationHint = configuration?.locationHint;
|
|
88
|
+
if (locationHint !== void 0 && typeof locationHint !== "string") throw new TypeError("Workers Response Store locationHint must be a string");
|
|
79
89
|
const shards = configuration?.shards;
|
|
80
90
|
if (shards !== void 0 && typeof shards !== "number") throw new TypeError("Workers Response Store shards must be a number");
|
|
81
91
|
return {
|
|
92
|
+
...locationHint === void 0 ? {} : { locationHint },
|
|
82
93
|
...shards === void 0 ? {} : { shards },
|
|
83
94
|
async regenerate(input, { env, ctx }) {
|
|
84
95
|
if (input.id === ROUTE_REVALIDATOR_ID) {
|
|
@@ -88,7 +99,7 @@ function createVinextResponseStoreOptions(configuration) {
|
|
|
88
99
|
await response.body?.cancel().catch(() => {});
|
|
89
100
|
throw new Error("Vinext response-stage regeneration was not cacheable");
|
|
90
101
|
}
|
|
91
|
-
return response;
|
|
102
|
+
return withoutRequestScopedHeaders(response, invocation.props);
|
|
92
103
|
}
|
|
93
104
|
if (input.id === "vinext:cache-function" && typeof input.args[0] === "string" && typeof input.args[1] === "string") {
|
|
94
105
|
const invocation = JSON.parse(input.args[1]);
|
|
@@ -109,29 +120,59 @@ function createVinextResponseStoreOptions(configuration) {
|
|
|
109
120
|
}
|
|
110
121
|
};
|
|
111
122
|
}
|
|
112
|
-
async function cacheRequest(
|
|
123
|
+
async function cacheRequest(invocation) {
|
|
113
124
|
const identity = JSON.stringify([
|
|
114
|
-
request.method,
|
|
115
|
-
request.url,
|
|
116
|
-
|
|
117
|
-
|
|
125
|
+
invocation.request.method,
|
|
126
|
+
invocation.request.url,
|
|
127
|
+
invocation.props,
|
|
128
|
+
invocation.request.headers
|
|
118
129
|
]);
|
|
119
130
|
const digest = new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(identity)));
|
|
120
|
-
const url = new URL(request.url);
|
|
131
|
+
const url = new URL(invocation.request.url);
|
|
121
132
|
url.searchParams.set(RESPONSE_STORE_KEY_PARAM, `v1.${[...digest].map((byte) => byte.toString(16).padStart(2, "0")).join("")}`);
|
|
122
133
|
return new Request(url, {
|
|
123
134
|
headers: CACHE_REQUEST_VARY_HEADERS,
|
|
124
135
|
method: "GET"
|
|
125
136
|
});
|
|
126
137
|
}
|
|
138
|
+
/**
|
|
139
|
+
* The request stage recomposes the routed params and path on every App page
|
|
140
|
+
* RSC response, HITs included, so such an entry shared across queries must not
|
|
141
|
+
* carry the values of the request that filled it. Every other response kind
|
|
142
|
+
* (route handlers, metadata routes, HTML, Pages) keeps its headers as rendered.
|
|
143
|
+
*/
|
|
144
|
+
function withoutRequestScopedHeaders(response, responseStageProps) {
|
|
145
|
+
if (responseStageProps === null || typeof responseStageProps !== "object" || Reflect.get(responseStageProps, "kind") !== "app-page" || Reflect.get(responseStageProps, "isRscRequest") !== true) return response;
|
|
146
|
+
const headers = new Headers(response.headers);
|
|
147
|
+
headers.delete(VINEXT_PARAMS_HEADER);
|
|
148
|
+
headers.delete(VINEXT_RENDERED_PATH_AND_SEARCH_HEADER);
|
|
149
|
+
return new Response(response.body, {
|
|
150
|
+
headers,
|
|
151
|
+
status: response.status,
|
|
152
|
+
statusText: response.statusText
|
|
153
|
+
});
|
|
154
|
+
}
|
|
127
155
|
function isCacheable(response) {
|
|
128
156
|
const policy = response.headers.get("Cloudflare-CDN-Cache-Control") ?? response.headers.get("CDN-Cache-Control") ?? response.headers.get("Cache-Control");
|
|
129
|
-
return response.status >= 200 && response.status < 400 && policy !== null && !isNonCacheableCacheControl(policy);
|
|
157
|
+
return response.status >= 200 && (response.status < 400 || response.status === 404) && policy !== null && !isNonCacheableCacheControl(policy);
|
|
130
158
|
}
|
|
131
|
-
function
|
|
132
|
-
|
|
159
|
+
async function readStoredResponse(key) {
|
|
160
|
+
try {
|
|
161
|
+
const response = await responseStore.fetch(key);
|
|
162
|
+
const storeStatus = response.headers.get("X-Workers-Response-Store");
|
|
163
|
+
if (response.status >= 200 && response.status < 400 || response.status === 404 && (storeStatus === "BLOB-FRESH" || storeStatus === "BLOB-STALE")) return response;
|
|
164
|
+
response.body?.cancel().catch(() => {});
|
|
165
|
+
if (response.status === 404 && storeStatus === "MISS") return null;
|
|
166
|
+
throw new Error(`Workers Response Store returned ${response.status}`);
|
|
167
|
+
} catch (error) {
|
|
168
|
+
console.error(JSON.stringify({
|
|
169
|
+
message: "Vinext response-store response lookup failed; treating as a cache miss",
|
|
170
|
+
error: error instanceof Error ? error.message : String(error)
|
|
171
|
+
}));
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
133
174
|
}
|
|
134
|
-
function publicResponse(response, cacheStatus, responseStageProps) {
|
|
175
|
+
function publicResponse(response, cacheStatus, responseStageProps, pendingAdmission = false) {
|
|
135
176
|
const headers = new Headers(response.headers);
|
|
136
177
|
const publicCacheStatus = cacheStatus === "HIT" && headers.get("CF-Cache-Status") === "UPDATING" ? "UPDATING" : cacheStatus;
|
|
137
178
|
const ageBasis = /^(\d+):(\d+)$/.exec(headers.get(AGE_BASIS_HEADER) ?? "");
|
|
@@ -154,7 +195,7 @@ function publicResponse(response, cacheStatus, responseStageProps) {
|
|
|
154
195
|
headers.set("X-Vinext-Cache", publicCacheStatus);
|
|
155
196
|
}
|
|
156
197
|
const cacheControl = headers.get("Cache-Control");
|
|
157
|
-
if (!cacheControl || !isNonCacheableCacheControl(cacheControl)) headers.set("Cache-Control", "private, max-age=0, must-revalidate");
|
|
198
|
+
if (pendingAdmission && (!cacheControl || !isNonCacheableCacheControl(cacheControl, "browser"))) headers.set("Cache-Control", "private, max-age=0, must-revalidate");
|
|
158
199
|
return traceCachedResponseStart(new Response(response.body, {
|
|
159
200
|
headers,
|
|
160
201
|
status: response.status,
|
|
@@ -170,42 +211,49 @@ function createVinextResponseStoreHandler(store) {
|
|
|
170
211
|
const handler = { async fetch(request, env, ctx) {
|
|
171
212
|
const context = stageContext(ctx, env);
|
|
172
213
|
const dispatchResponseStage = async (stageRequest, props, options) => {
|
|
173
|
-
const invocation = serializeInvocation(stageRequest, props);
|
|
174
214
|
if (options.cache === "bypass" || stageRequest.method !== "GET" && stageRequest.method !== "HEAD") return publicResponse(await invokeResponseStage(stageRequest, props, env, ctx, "bypass"), "BYPASS", props);
|
|
215
|
+
const identityRequest = options.cacheIdentity?.request ?? stageRequest;
|
|
216
|
+
const identityProps = options.cacheIdentity?.props ?? props;
|
|
175
217
|
const isWarmup = request.headers.get("user-agent") === WARMUP_USER_AGENT;
|
|
176
218
|
const rscSeed = isWarmup && props !== null && typeof props === "object" && Reflect.get(props, "kind") === "app-page" && Reflect.get(props, "isRscRequest") === false && Reflect.get(props, "matchKind") === "request" && Reflect.get(props, "interceptionContext") === null && Reflect.get(props, "interceptionId") === null && Reflect.get(props, "mountedSlotsHeader") === null ? {
|
|
177
219
|
props: {
|
|
178
|
-
...
|
|
220
|
+
...identityProps,
|
|
179
221
|
isRscRequest: true,
|
|
180
222
|
renderMode: "navigation"
|
|
181
223
|
},
|
|
182
|
-
request: new Request(new URL(createCanonicalRscRequestUrl(
|
|
224
|
+
request: new Request(new URL(createCanonicalRscRequestUrl(identityRequest.url), identityRequest.url), { headers: createCanonicalRscRequestHeaders() })
|
|
183
225
|
} : void 0;
|
|
184
|
-
const
|
|
185
|
-
const
|
|
186
|
-
const
|
|
187
|
-
|
|
226
|
+
const invocation = prepareInvocation(identityRequest, identityProps);
|
|
227
|
+
const rscInvocation = rscSeed ? prepareInvocation(rscSeed.request, rscSeed.props) : void 0;
|
|
228
|
+
const rscKey = rscInvocation ? await cacheRequest(rscInvocation) : void 0;
|
|
229
|
+
const key = await cacheRequest(invocation);
|
|
230
|
+
const stored = await readStoredResponse(key);
|
|
231
|
+
if (stored) {
|
|
188
232
|
if (!rscKey) return publicResponse(stored, "HIT", props);
|
|
189
|
-
const storedRsc = await
|
|
190
|
-
if (
|
|
191
|
-
|
|
233
|
+
const storedRsc = await readStoredResponse(rscKey);
|
|
234
|
+
if (storedRsc) {
|
|
235
|
+
storedRsc.body?.cancel().catch(() => {});
|
|
192
236
|
return publicResponse(stored, "HIT", props);
|
|
193
237
|
}
|
|
194
|
-
|
|
238
|
+
stored.body?.cancel().catch(() => {});
|
|
195
239
|
}
|
|
196
240
|
const capture = rscSeed ? { captureRscData: true } : isWarmup ? {} : { streamResponse: true };
|
|
197
|
-
const
|
|
241
|
+
const serializedInvocation = JSON.stringify(invocation);
|
|
242
|
+
const rendered = await invokeResponseStage(stageRequest, props, env, ctx, "shared", capture, {
|
|
243
|
+
replayable: isReplayableInvocation(stageRequest, props),
|
|
244
|
+
serialized: options.cacheIdentity ? serializeInvocation(stageRequest, props) : serializedInvocation
|
|
245
|
+
});
|
|
198
246
|
if (capture.admittedResponse) {
|
|
199
247
|
ctx.waitUntil(capture.admittedResponse.then(async (admitted) => {
|
|
200
248
|
if (!isCacheable(admitted)) {
|
|
201
249
|
await admitted.body?.cancel().catch(() => {});
|
|
202
250
|
return;
|
|
203
251
|
}
|
|
204
|
-
await responseStore.put(key, admitted, {
|
|
252
|
+
await responseStore.put(key, withoutRequestScopedHeaders(admitted, props), {
|
|
205
253
|
coalesce: true,
|
|
206
254
|
revalidator: {
|
|
207
255
|
id: ROUTE_REVALIDATOR_ID,
|
|
208
|
-
args: [
|
|
256
|
+
args: [serializedInvocation]
|
|
209
257
|
}
|
|
210
258
|
});
|
|
211
259
|
}).catch((error) => {
|
|
@@ -214,7 +262,7 @@ const handler = { async fetch(request, env, ctx) {
|
|
|
214
262
|
error: error instanceof Error ? error.message : String(error)
|
|
215
263
|
}));
|
|
216
264
|
}));
|
|
217
|
-
return publicResponse(rendered, "MISS", props);
|
|
265
|
+
return publicResponse(rendered, "MISS", props, true);
|
|
218
266
|
}
|
|
219
267
|
if (!isCacheable(rendered)) {
|
|
220
268
|
capture?.rscData?.catch(() => {});
|
|
@@ -225,25 +273,36 @@ const handler = { async fetch(request, env, ctx) {
|
|
|
225
273
|
throw new Error("Vinext response-store warmup did not capture the App page RSC payload");
|
|
226
274
|
}
|
|
227
275
|
const [foreground, cacheBody] = rendered.body ? rendered.body.tee() : [null, null];
|
|
228
|
-
const cacheResponse = new Response(cacheBody, rendered);
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
276
|
+
const cacheResponse = withoutRequestScopedHeaders(new Response(cacheBody, rendered), props);
|
|
277
|
+
try {
|
|
278
|
+
await responseStore.put(key, cacheResponse, {
|
|
279
|
+
coalesce: true,
|
|
280
|
+
revalidator: {
|
|
281
|
+
id: ROUTE_REVALIDATOR_ID,
|
|
282
|
+
args: [serializedInvocation]
|
|
283
|
+
}
|
|
284
|
+
});
|
|
285
|
+
} catch (error) {
|
|
286
|
+
if (isWarmup) throw error;
|
|
287
|
+
cacheResponse.body?.cancel().catch(() => {});
|
|
288
|
+
console.error(JSON.stringify({
|
|
289
|
+
message: "Vinext response-store response fill failed; serving the rendered response",
|
|
290
|
+
error: error instanceof Error ? error.message : String(error)
|
|
291
|
+
}));
|
|
292
|
+
}
|
|
293
|
+
if (rscSeed && rscInvocation && rscKey && capture?.rscData) {
|
|
237
294
|
const rscData = await capture.rscData;
|
|
238
295
|
const rscHeaders = new Headers(rendered.headers);
|
|
239
296
|
rscHeaders.delete("Content-Length");
|
|
240
297
|
rscHeaders.delete("Link");
|
|
241
298
|
rscHeaders.delete("X-Vinext-Response-Store-Replayable");
|
|
299
|
+
rscHeaders.delete(VINEXT_PARAMS_HEADER);
|
|
300
|
+
rscHeaders.delete(VINEXT_RENDERED_PATH_AND_SEARCH_HEADER);
|
|
242
301
|
rscHeaders.set("Content-Type", VINEXT_RSC_CONTENT_TYPE);
|
|
243
|
-
rscHeaders.set("Vary", VINEXT_RSC_VARY_HEADER);
|
|
302
|
+
rscHeaders.set("Vary", VINEXT_RSC_VARY_HEADER$1);
|
|
244
303
|
applyRscCompatibilityIdHeader(rscHeaders);
|
|
245
304
|
applyRscDeploymentIdHeader(rscHeaders);
|
|
246
|
-
const
|
|
305
|
+
const serializedRscInvocation = JSON.stringify(rscInvocation);
|
|
247
306
|
await responseStore.put(rscKey, new Response(rscData, {
|
|
248
307
|
headers: rscHeaders,
|
|
249
308
|
status: 200
|
|
@@ -251,14 +310,28 @@ const handler = { async fetch(request, env, ctx) {
|
|
|
251
310
|
coalesce: true,
|
|
252
311
|
revalidator: {
|
|
253
312
|
id: ROUTE_REVALIDATOR_ID,
|
|
254
|
-
args: [
|
|
313
|
+
args: [serializedRscInvocation]
|
|
255
314
|
}
|
|
256
315
|
});
|
|
257
316
|
}
|
|
258
317
|
return publicResponse(new Response(foreground, rendered), "MISS", props);
|
|
259
318
|
};
|
|
260
319
|
const { handleRequestStage } = await loadVinextRequestStage();
|
|
261
|
-
|
|
320
|
+
const sharedResponses = /* @__PURE__ */ new Map();
|
|
321
|
+
const dispatch = async (stageRequest, props, options) => {
|
|
322
|
+
const response = await dispatchResponseStage(stageRequest, props, options);
|
|
323
|
+
if (options.cache !== "shared") return response;
|
|
324
|
+
const headers = new Headers(response.headers);
|
|
325
|
+
const token = crypto.randomUUID();
|
|
326
|
+
headers.set(SHARED_RESPONSE_STAGE_HEADER, token);
|
|
327
|
+
sharedResponses.set(token, { headers: new Headers(headers) });
|
|
328
|
+
return new Response(response.body, {
|
|
329
|
+
headers,
|
|
330
|
+
status: response.status,
|
|
331
|
+
statusText: response.statusText
|
|
332
|
+
});
|
|
333
|
+
};
|
|
334
|
+
return finalizeGatewayResponse(await handleRequestStage(request, env, context, dispatch), sharedResponses, "X-Vinext-Cache");
|
|
262
335
|
} };
|
|
263
336
|
//#endregion
|
|
264
337
|
export { createVinextResponseStoreHandler, createVinextResponseStoreOptions };
|
|
@@ -192,39 +192,61 @@ var WorkersResponseStoreCacheHandler = class {
|
|
|
192
192
|
async get(key, context) {
|
|
193
193
|
if (regenerationStorage.getStore()?.targetKey === key) return null;
|
|
194
194
|
const request = await cacheRequest(key);
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
return null;
|
|
202
|
-
}
|
|
203
|
-
const softTags = [...new Set(readStringArrayField(context, "softTags").map(encodeCloudflareCacheTag))].sort();
|
|
204
|
-
if (softTags.length) {
|
|
205
|
-
const key = softTags.join(",");
|
|
206
|
-
const expirations = this.tagExpirations();
|
|
207
|
-
let expiration = expirations.get(key);
|
|
208
|
-
if (!expiration) {
|
|
209
|
-
expiration = this.store.getTagExpiration(softTags);
|
|
210
|
-
expirations.set(key, expiration);
|
|
195
|
+
try {
|
|
196
|
+
const response = await this.store.fetch(request);
|
|
197
|
+
if (!response.ok) {
|
|
198
|
+
response.body?.cancel().catch(() => {});
|
|
199
|
+
if (response.status === 404 && response.headers.get("X-Workers-Response-Store") === "MISS") return null;
|
|
200
|
+
throw new Error(`Workers Response Store returned ${response.status}`);
|
|
211
201
|
}
|
|
212
|
-
|
|
202
|
+
const softTags = [...new Set(readStringArrayField(context, "softTags").map(encodeCloudflareCacheTag))].sort();
|
|
203
|
+
const getTagExpiration = () => {
|
|
204
|
+
const key = softTags.join(",");
|
|
205
|
+
const expirations = this.tagExpirations();
|
|
206
|
+
let expiration = expirations.get(key);
|
|
207
|
+
if (!expiration) {
|
|
208
|
+
expiration = Promise.resolve().then(() => this.store.getTagExpiration(softTags));
|
|
209
|
+
expirations.set(key, expiration);
|
|
210
|
+
expiration.catch(() => {
|
|
211
|
+
if (expirations.get(key) === expiration) expirations.delete(key);
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
return expiration;
|
|
215
|
+
};
|
|
216
|
+
const storeStatus = response.headers.get("X-Workers-Response-Store");
|
|
217
|
+
const body = response.text();
|
|
218
|
+
const eagerExpiration = softTags.length && (storeStatus === "BLOB-FRESH" || storeStatus === "BLOB-STALE") ? getTagExpiration().then((value) => ({ value }), (error) => ({ error })) : void 0;
|
|
219
|
+
const entry = deserialize(await body);
|
|
220
|
+
if (!entry) {
|
|
221
|
+
await this.store.purge({ pathPrefixes: [new URL(request.url).pathname] });
|
|
222
|
+
return null;
|
|
223
|
+
}
|
|
224
|
+
if (softTags.length) {
|
|
225
|
+
const expiration = eagerExpiration ? await eagerExpiration : { value: await getTagExpiration() };
|
|
226
|
+
if ("error" in expiration) throw expiration.error;
|
|
227
|
+
if (expiration.value >= entry.lastModified) return null;
|
|
228
|
+
}
|
|
229
|
+
const age = Date.now() - entry.lastModified;
|
|
230
|
+
const requestedRevalidate = readCacheControlField(context, "revalidate");
|
|
231
|
+
const requestedStale = typeof requestedRevalidate === "number" && requestedRevalidate > 0 && age > requestedRevalidate * 1e3;
|
|
232
|
+
let cacheState;
|
|
233
|
+
if (response.headers.get(REPLAYABLE_HEADER) === "1") {
|
|
234
|
+
if (requestedStale) cacheState = "stale";
|
|
235
|
+
} else if (typeof entry.cacheControl?.expire === "number" && age > entry.cacheControl.expire * 1e3) cacheState = "expired";
|
|
236
|
+
else if (requestedStale || typeof entry.cacheControl?.revalidate === "number" && entry.cacheControl.revalidate > 0 && age > entry.cacheControl.revalidate * 1e3) cacheState = "stale";
|
|
237
|
+
return {
|
|
238
|
+
lastModified: entry.lastModified,
|
|
239
|
+
value: entry.value,
|
|
240
|
+
...cacheState ? { cacheState } : {},
|
|
241
|
+
...entry.cacheControl ? { cacheControl: entry.cacheControl } : {}
|
|
242
|
+
};
|
|
243
|
+
} catch (error) {
|
|
244
|
+
console.error(JSON.stringify({
|
|
245
|
+
message: "Vinext response-store data lookup failed; treating as a cache miss",
|
|
246
|
+
error: error instanceof Error ? error.message : String(error)
|
|
247
|
+
}));
|
|
248
|
+
return null;
|
|
213
249
|
}
|
|
214
|
-
const age = Date.now() - entry.lastModified;
|
|
215
|
-
const requestedRevalidate = readCacheControlField(context, "revalidate");
|
|
216
|
-
const requestedStale = typeof requestedRevalidate === "number" && requestedRevalidate > 0 && age > requestedRevalidate * 1e3;
|
|
217
|
-
let cacheState;
|
|
218
|
-
if (response.headers.get(REPLAYABLE_HEADER) === "1") {
|
|
219
|
-
if (requestedStale) cacheState = "stale";
|
|
220
|
-
} else if (typeof entry.cacheControl?.expire === "number" && age > entry.cacheControl.expire * 1e3) cacheState = "expired";
|
|
221
|
-
else if (requestedStale || typeof entry.cacheControl?.revalidate === "number" && entry.cacheControl.revalidate > 0 && age > entry.cacheControl.revalidate * 1e3) cacheState = "stale";
|
|
222
|
-
return {
|
|
223
|
-
lastModified: entry.lastModified,
|
|
224
|
-
value: entry.value,
|
|
225
|
-
...cacheState ? { cacheState } : {},
|
|
226
|
-
...entry.cacheControl ? { cacheControl: entry.cacheControl } : {}
|
|
227
|
-
};
|
|
228
250
|
}
|
|
229
251
|
async set(key, value, context) {
|
|
230
252
|
let revalidate = readCacheControlField(context, "revalidate");
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
//#region src/cache/static-assets-adapter.build.d.ts
|
|
2
|
+
/** Package prerender output from both routers as immutable Workers Static Assets. */
|
|
3
|
+
export declare function finalizeStaticAssetsPrerenderOutput(root: string, options?: {
|
|
4
|
+
clientOutDir?: string;
|
|
5
|
+
}): number;
|
|
6
|
+
//#endregion
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { STATIC_ASSET_CACHE_PATH } from "./static-assets-adapter.shared.js";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { createHash } from "node:crypto";
|
|
5
|
+
import { readPrerenderManifest } from "vinext/internal/server/prerender-manifest";
|
|
6
|
+
import { appIsrCacheKey, pagesIsrCacheKey } from "vinext/internal/server/isr-cache";
|
|
7
|
+
import { extractVinextNextDataJson } from "vinext/internal/client/vinext-next-data";
|
|
8
|
+
import { normalizePregeneratedPathname } from "vinext/internal/server/pregenerated-concrete-paths";
|
|
9
|
+
import { getAppRouteOutputPath, getOutputPath, getRscOutputPath } from "vinext/internal/utils/prerender-output-paths";
|
|
10
|
+
//#region src/cache/static-assets-adapter.build.ts
|
|
11
|
+
function cacheAssetId(key) {
|
|
12
|
+
return createHash("sha256").update(key).digest("hex");
|
|
13
|
+
}
|
|
14
|
+
function pagesCacheKey(pathname, locale, buildId) {
|
|
15
|
+
let pagesPathname = new URL(`https://vinext.invalid${pathname}`).pathname;
|
|
16
|
+
if (locale) {
|
|
17
|
+
const prefix = `/${locale}`;
|
|
18
|
+
if (pagesPathname === prefix) pagesPathname = "/";
|
|
19
|
+
else if (pagesPathname.startsWith(`${prefix}/`)) pagesPathname = pagesPathname.slice(prefix.length);
|
|
20
|
+
}
|
|
21
|
+
return pagesIsrCacheKey(pagesPathname, buildId, locale ? `locale:${locale}` : void 0);
|
|
22
|
+
}
|
|
23
|
+
function cacheControl(route) {
|
|
24
|
+
if (route.revalidate === void 0) return void 0;
|
|
25
|
+
return {
|
|
26
|
+
revalidate: route.revalidate,
|
|
27
|
+
...route.expire === void 0 ? {} : { expire: route.expire },
|
|
28
|
+
...route.stale === void 0 ? {} : { stale: route.stale }
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
function writeCacheAsset(outputDir, index, key, kind, sourcePath, route, contents) {
|
|
32
|
+
if (!fs.existsSync(sourcePath)) return false;
|
|
33
|
+
const id = cacheAssetId(key);
|
|
34
|
+
const extension = kind;
|
|
35
|
+
const policy = cacheControl(route);
|
|
36
|
+
const metadata = {
|
|
37
|
+
kind,
|
|
38
|
+
lastModified: fs.statSync(sourcePath).mtimeMs,
|
|
39
|
+
...policy ? { cacheControl: policy } : {},
|
|
40
|
+
...kind !== "rsc" && route.headers ? { headers: route.headers } : {},
|
|
41
|
+
...kind !== "rsc" && route.responseStatus !== void 0 ? { status: route.responseStatus } : {}
|
|
42
|
+
};
|
|
43
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
44
|
+
const outputPath = path.join(outputDir, `${id}.${extension}`);
|
|
45
|
+
if (contents === void 0) fs.copyFileSync(sourcePath, outputPath);
|
|
46
|
+
else fs.writeFileSync(outputPath, contents);
|
|
47
|
+
index[id] = metadata;
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
/** Package prerender output from both routers as immutable Workers Static Assets. */
|
|
51
|
+
function finalizeStaticAssetsPrerenderOutput(root, options = {}) {
|
|
52
|
+
const serverDir = path.join(root, "dist", "server");
|
|
53
|
+
const prerenderDir = path.join(serverDir, "prerendered-routes");
|
|
54
|
+
const outputDir = path.join(options.clientOutDir ?? path.join(root, "dist", "client"), STATIC_ASSET_CACHE_PATH.replace(/^\//, ""));
|
|
55
|
+
fs.rmSync(outputDir, {
|
|
56
|
+
recursive: true,
|
|
57
|
+
force: true
|
|
58
|
+
});
|
|
59
|
+
const manifest = readPrerenderManifest(path.join(serverDir, "vinext-prerender.json"));
|
|
60
|
+
if (!manifest?.buildId || !manifest.routes || !fs.existsSync(prerenderDir)) return 0;
|
|
61
|
+
const index = {};
|
|
62
|
+
let count = 0;
|
|
63
|
+
for (const route of manifest.routes) {
|
|
64
|
+
if (route.status !== "rendered") continue;
|
|
65
|
+
if (typeof route.revalidate === "number" && route.revalidate <= 0) continue;
|
|
66
|
+
const pathname = route.path ?? route.route;
|
|
67
|
+
const cachePathname = normalizePregeneratedPathname(pathname);
|
|
68
|
+
if (route.router === "app") {
|
|
69
|
+
count += Number(writeCacheAsset(outputDir, index, appIsrCacheKey(cachePathname, "html", manifest.buildId), "html", path.join(prerenderDir, getOutputPath(pathname, manifest.trailingSlash ?? false)), route));
|
|
70
|
+
count += Number(writeCacheAsset(outputDir, index, appIsrCacheKey(cachePathname, "rsc", manifest.buildId), "rsc", path.join(prerenderDir, getRscOutputPath(pathname)), route));
|
|
71
|
+
} else if (route.router === "pages") {
|
|
72
|
+
const sourcePath = path.join(prerenderDir, getOutputPath(pathname, manifest.trailingSlash ?? false));
|
|
73
|
+
if (!fs.existsSync(sourcePath)) continue;
|
|
74
|
+
const html = fs.readFileSync(sourcePath, "utf8");
|
|
75
|
+
const key = pagesCacheKey(pathname, route.locale, manifest.buildId);
|
|
76
|
+
if (route.notFound) {
|
|
77
|
+
count += Number(writeCacheAsset(outputDir, index, key, "not-found", sourcePath, route, "null"));
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
if (route.redirectProps) {
|
|
81
|
+
count += Number(writeCacheAsset(outputDir, index, key, "redirect", sourcePath, route, JSON.stringify(route.redirectProps)));
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
const json = extractVinextNextDataJson(html);
|
|
85
|
+
if (json === null) continue;
|
|
86
|
+
const nextData = JSON.parse(json);
|
|
87
|
+
if (nextData.gsp !== true && nextData.autoExport !== true) continue;
|
|
88
|
+
if (!nextData.props || typeof nextData.props !== "object") throw new Error(`[vinext] Missing prerendered Pages props for ${pathname}`);
|
|
89
|
+
count += Number(writeCacheAsset(outputDir, index, pagesCacheKey(pathname, nextData.locale ?? route.locale, manifest.buildId), "pages", sourcePath, {
|
|
90
|
+
...route,
|
|
91
|
+
responseStatus: route.route === "/404" ? 404 : route.route === "/500" ? 500 : route.responseStatus ?? 200
|
|
92
|
+
}, JSON.stringify({
|
|
93
|
+
html,
|
|
94
|
+
pageData: nextData.props
|
|
95
|
+
})));
|
|
96
|
+
} else if (route.router === "metadata") count += Number(writeCacheAsset(outputDir, index, appIsrCacheKey(cachePathname, "route", manifest.buildId), "route", path.join(prerenderDir, getAppRouteOutputPath(pathname)), route));
|
|
97
|
+
}
|
|
98
|
+
if (count > 0) fs.writeFileSync(path.join(outputDir, "index.json"), JSON.stringify(index));
|
|
99
|
+
return count;
|
|
100
|
+
}
|
|
101
|
+
//#endregion
|
|
102
|
+
export { finalizeStaticAssetsPrerenderOutput };
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
//#region src/cache/static-assets-adapter.d.ts
|
|
2
|
+
export type StaticAssetsAdapterOptions = {
|
|
3
|
+
/** Workers Static Assets binding name. @default "ASSETS" */
|
|
4
|
+
binding?: string;
|
|
5
|
+
};
|
|
6
|
+
/**
|
|
7
|
+
* Read-only page cache backed by Workers Static Assets.
|
|
8
|
+
*
|
|
9
|
+
* Locally prerendered Pages HTML/data and App HTML, RSC, and metadata are copied
|
|
10
|
+
* into `dist/client` after the build. Runtime cache writes and invalidations are
|
|
11
|
+
* intentionally no-ops; a new deployment replaces the cache contents.
|
|
12
|
+
*/
|
|
13
|
+
export declare function staticAssetsAdapter(options?: StaticAssetsAdapterOptions): {
|
|
14
|
+
adapter: string;
|
|
15
|
+
options: StaticAssetsAdapterOptions | undefined;
|
|
16
|
+
output: {
|
|
17
|
+
finalizePrerenderOutput({ root, clientOutDir }: {
|
|
18
|
+
root: string;
|
|
19
|
+
clientOutDir: string;
|
|
20
|
+
}): void;
|
|
21
|
+
};
|
|
22
|
+
capabilities: {
|
|
23
|
+
buildIdentity: "response-header";
|
|
24
|
+
warmup: "data-cache";
|
|
25
|
+
};
|
|
26
|
+
};
|
|
27
|
+
//#endregion
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { finalizeStaticAssetsPrerenderOutput } from "./static-assets-adapter.build.js";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
//#region src/cache/static-assets-adapter.ts
|
|
4
|
+
/**
|
|
5
|
+
* Read-only page cache backed by Workers Static Assets.
|
|
6
|
+
*
|
|
7
|
+
* Locally prerendered Pages HTML/data and App HTML, RSC, and metadata are copied
|
|
8
|
+
* into `dist/client` after the build. Runtime cache writes and invalidations are
|
|
9
|
+
* intentionally no-ops; a new deployment replaces the cache contents.
|
|
10
|
+
*/
|
|
11
|
+
function staticAssetsAdapter(options) {
|
|
12
|
+
if (options?.binding !== void 0 && (typeof options.binding !== "string" || options.binding.length === 0)) throw new TypeError("[vinext] staticAssetsAdapter({ binding }) must be a non-empty string binding name.");
|
|
13
|
+
return {
|
|
14
|
+
adapter: fileURLToPath(import.meta.resolve("./static-assets-adapter.runtime.js")),
|
|
15
|
+
options,
|
|
16
|
+
output: { finalizePrerenderOutput({ root, clientOutDir }) {
|
|
17
|
+
finalizeStaticAssetsPrerenderOutput(root, { clientOutDir });
|
|
18
|
+
} },
|
|
19
|
+
capabilities: {
|
|
20
|
+
buildIdentity: "response-header",
|
|
21
|
+
warmup: "data-cache"
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
//#endregion
|
|
26
|
+
export { staticAssetsAdapter };
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { StaticAssetsAdapterOptions } from "./static-assets-adapter.js";
|
|
2
|
+
import { CdnCacheAdapter, CdnCacheableHeaderInput, CdnResponseHeaders } from "vinext/shims/cdn-cache";
|
|
3
|
+
import { CacheHandlerValue, IncrementalCacheValue } from "vinext/shims/cache-handler";
|
|
4
|
+
//#region src/cache/static-assets-adapter.runtime.d.ts
|
|
5
|
+
type AssetFetcher = {
|
|
6
|
+
fetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response>;
|
|
7
|
+
};
|
|
8
|
+
export declare class StaticAssetsCacheAdapter implements CdnCacheAdapter {
|
|
9
|
+
readonly assets: AssetFetcher;
|
|
10
|
+
readonly hasPrerenderedPages = true;
|
|
11
|
+
readonly ownsBackgroundRevalidation = false;
|
|
12
|
+
private indexPromise;
|
|
13
|
+
constructor(assets: AssetFetcher);
|
|
14
|
+
private loadIndex;
|
|
15
|
+
get(key: string): Promise<CacheHandlerValue | null>;
|
|
16
|
+
set(_key: string, _data: IncrementalCacheValue | null, _ctx?: Record<string, unknown>): Promise<void>;
|
|
17
|
+
buildResponseHeaders(input: CdnCacheableHeaderInput): CdnResponseHeaders;
|
|
18
|
+
buildResponseIdentityHeaders(): CdnResponseHeaders;
|
|
19
|
+
revalidateTag(_tags: string | string[], _durations?: {
|
|
20
|
+
expire?: number;
|
|
21
|
+
}): Promise<void>;
|
|
22
|
+
}
|
|
23
|
+
export default function createStaticAssetsCacheAdapter({ env, options }?: {
|
|
24
|
+
env?: Record<string, unknown>;
|
|
25
|
+
options?: StaticAssetsAdapterOptions;
|
|
26
|
+
}): CdnCacheAdapter;
|
|
27
|
+
//#endregion
|