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