@vinext/cloudflare 1.0.0-beta.9 → 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 +165 -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-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 +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 +9 -6
- package/dist/cache/response-store-adapter.js +20 -1
- package/dist/cache/response-store-adapter.worker.js +100 -43
- 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 +1 -0
- package/dist/cdn-warm.js +2 -1
- 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,6 @@
|
|
|
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";
|
|
@@ -9,7 +10,7 @@ const ROUTE_REVALIDATOR_ID = "vinext:response";
|
|
|
9
10
|
const RESPONSE_STORE_KEY_PARAM = "__workers_response_store";
|
|
10
11
|
const AGE_BASIS_HEADER = "X-Workers-Response-Store-Age-Basis";
|
|
11
12
|
const WARMUP_USER_AGENT = "vinext-cloudflare-cdn-warm";
|
|
12
|
-
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());
|
|
13
14
|
const CACHE_REQUEST_VARY_HEADERS = REPLAY_REQUEST_HEADERS.map((name) => [name, "vinext-keyed"]);
|
|
14
15
|
function stageContext(ctx, env) {
|
|
15
16
|
const assets = Reflect.get(env, "ASSETS");
|
|
@@ -42,15 +43,18 @@ function isReplayableInvocation(request, props) {
|
|
|
42
43
|
if (request.headers.has("authorization") || request.headers.has("cookie")) return false;
|
|
43
44
|
return !(props && typeof props === "object" && ["draftModeCookie", "middlewareCookieOverlay"].some((name) => name in props && Reflect.get(props, name) !== null));
|
|
44
45
|
}
|
|
45
|
-
function
|
|
46
|
-
return
|
|
46
|
+
function prepareInvocation(request, props) {
|
|
47
|
+
return {
|
|
47
48
|
props: safeProps(props),
|
|
48
49
|
request: {
|
|
49
50
|
headers: replayHeaders(request),
|
|
50
51
|
method: request.method,
|
|
51
52
|
url: request.url
|
|
52
53
|
}
|
|
53
|
-
}
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
function serializeInvocation(request, props) {
|
|
57
|
+
return JSON.stringify(prepareInvocation(request, props));
|
|
54
58
|
}
|
|
55
59
|
function parseInvocation(value) {
|
|
56
60
|
if (typeof value !== "string") throw new TypeError("Invalid vinext response-store invocation");
|
|
@@ -68,17 +72,23 @@ async function invokeRequestStage(request, env, ctx) {
|
|
|
68
72
|
const { handleRequestStage } = await loadVinextRequestStage();
|
|
69
73
|
return handleRequestStage(request, env, stageContext(ctx, env), (request, props, options) => invokeResponseStage(request, props, env, ctx, options.cache));
|
|
70
74
|
}
|
|
71
|
-
async function invokeResponseStage(request, props, env, ctx, cache, capture) {
|
|
75
|
+
async function invokeResponseStage(request, props, env, ctx, cache, capture, invocation) {
|
|
72
76
|
const context = stageContext(ctx, env);
|
|
73
77
|
const dispatchRequestStage = (request) => invokeRequestStage(request, env, ctx);
|
|
74
78
|
const { handleResponseStage } = await loadVinextResponseStage();
|
|
75
|
-
const
|
|
76
|
-
|
|
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);
|
|
77
84
|
}
|
|
78
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");
|
|
79
88
|
const shards = configuration?.shards;
|
|
80
89
|
if (shards !== void 0 && typeof shards !== "number") throw new TypeError("Workers Response Store shards must be a number");
|
|
81
90
|
return {
|
|
91
|
+
...locationHint === void 0 ? {} : { locationHint },
|
|
82
92
|
...shards === void 0 ? {} : { shards },
|
|
83
93
|
async regenerate(input, { env, ctx }) {
|
|
84
94
|
if (input.id === ROUTE_REVALIDATOR_ID) {
|
|
@@ -88,7 +98,7 @@ function createVinextResponseStoreOptions(configuration) {
|
|
|
88
98
|
await response.body?.cancel().catch(() => {});
|
|
89
99
|
throw new Error("Vinext response-stage regeneration was not cacheable");
|
|
90
100
|
}
|
|
91
|
-
return response;
|
|
101
|
+
return withoutRequestScopedHeaders(response, invocation.props);
|
|
92
102
|
}
|
|
93
103
|
if (input.id === "vinext:cache-function" && typeof input.args[0] === "string" && typeof input.args[1] === "string") {
|
|
94
104
|
const invocation = JSON.parse(input.args[1]);
|
|
@@ -109,27 +119,56 @@ function createVinextResponseStoreOptions(configuration) {
|
|
|
109
119
|
}
|
|
110
120
|
};
|
|
111
121
|
}
|
|
112
|
-
async function cacheRequest(
|
|
122
|
+
async function cacheRequest(invocation) {
|
|
113
123
|
const identity = JSON.stringify([
|
|
114
|
-
request.method,
|
|
115
|
-
request.url,
|
|
116
|
-
|
|
117
|
-
|
|
124
|
+
invocation.request.method,
|
|
125
|
+
invocation.request.url,
|
|
126
|
+
invocation.props,
|
|
127
|
+
invocation.request.headers
|
|
118
128
|
]);
|
|
119
129
|
const digest = new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(identity)));
|
|
120
|
-
const url = new URL(request.url);
|
|
130
|
+
const url = new URL(invocation.request.url);
|
|
121
131
|
url.searchParams.set(RESPONSE_STORE_KEY_PARAM, `v1.${[...digest].map((byte) => byte.toString(16).padStart(2, "0")).join("")}`);
|
|
122
132
|
return new Request(url, {
|
|
123
133
|
headers: CACHE_REQUEST_VARY_HEADERS,
|
|
124
134
|
method: "GET"
|
|
125
135
|
});
|
|
126
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
|
+
});
|
|
153
|
+
}
|
|
127
154
|
function isCacheable(response) {
|
|
128
155
|
const policy = response.headers.get("Cloudflare-CDN-Cache-Control") ?? response.headers.get("CDN-Cache-Control") ?? response.headers.get("Cache-Control");
|
|
129
156
|
return response.status >= 200 && response.status < 400 && policy !== null && !isNonCacheableCacheControl(policy);
|
|
130
157
|
}
|
|
131
|
-
function
|
|
132
|
-
|
|
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
|
+
}
|
|
133
172
|
}
|
|
134
173
|
function publicResponse(response, cacheStatus, responseStageProps) {
|
|
135
174
|
const headers = new Headers(response.headers);
|
|
@@ -170,42 +209,49 @@ function createVinextResponseStoreHandler(store) {
|
|
|
170
209
|
const handler = { async fetch(request, env, ctx) {
|
|
171
210
|
const context = stageContext(ctx, env);
|
|
172
211
|
const dispatchResponseStage = async (stageRequest, props, options) => {
|
|
173
|
-
const invocation = serializeInvocation(stageRequest, props);
|
|
174
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;
|
|
175
215
|
const isWarmup = request.headers.get("user-agent") === WARMUP_USER_AGENT;
|
|
176
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 ? {
|
|
177
217
|
props: {
|
|
178
|
-
...
|
|
218
|
+
...identityProps,
|
|
179
219
|
isRscRequest: true,
|
|
180
220
|
renderMode: "navigation"
|
|
181
221
|
},
|
|
182
|
-
request: new Request(new URL(createCanonicalRscRequestUrl(
|
|
222
|
+
request: new Request(new URL(createCanonicalRscRequestUrl(identityRequest.url), identityRequest.url), { headers: createCanonicalRscRequestHeaders() })
|
|
183
223
|
} : void 0;
|
|
184
|
-
const
|
|
185
|
-
const
|
|
186
|
-
const
|
|
187
|
-
|
|
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) {
|
|
188
230
|
if (!rscKey) return publicResponse(stored, "HIT", props);
|
|
189
|
-
const storedRsc = await
|
|
190
|
-
if (
|
|
191
|
-
|
|
231
|
+
const storedRsc = await readStoredResponse(rscKey);
|
|
232
|
+
if (storedRsc) {
|
|
233
|
+
storedRsc.body?.cancel().catch(() => {});
|
|
192
234
|
return publicResponse(stored, "HIT", props);
|
|
193
235
|
}
|
|
194
|
-
|
|
236
|
+
stored.body?.cancel().catch(() => {});
|
|
195
237
|
}
|
|
196
238
|
const capture = rscSeed ? { captureRscData: true } : isWarmup ? {} : { streamResponse: true };
|
|
197
|
-
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
|
+
});
|
|
198
244
|
if (capture.admittedResponse) {
|
|
199
245
|
ctx.waitUntil(capture.admittedResponse.then(async (admitted) => {
|
|
200
246
|
if (!isCacheable(admitted)) {
|
|
201
247
|
await admitted.body?.cancel().catch(() => {});
|
|
202
248
|
return;
|
|
203
249
|
}
|
|
204
|
-
await responseStore.put(key, admitted, {
|
|
250
|
+
await responseStore.put(key, withoutRequestScopedHeaders(admitted, props), {
|
|
205
251
|
coalesce: true,
|
|
206
252
|
revalidator: {
|
|
207
253
|
id: ROUTE_REVALIDATOR_ID,
|
|
208
|
-
args: [
|
|
254
|
+
args: [serializedInvocation]
|
|
209
255
|
}
|
|
210
256
|
});
|
|
211
257
|
}).catch((error) => {
|
|
@@ -225,25 +271,36 @@ const handler = { async fetch(request, env, ctx) {
|
|
|
225
271
|
throw new Error("Vinext response-store warmup did not capture the App page RSC payload");
|
|
226
272
|
}
|
|
227
273
|
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
|
-
|
|
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) {
|
|
237
292
|
const rscData = await capture.rscData;
|
|
238
293
|
const rscHeaders = new Headers(rendered.headers);
|
|
239
294
|
rscHeaders.delete("Content-Length");
|
|
240
295
|
rscHeaders.delete("Link");
|
|
241
296
|
rscHeaders.delete("X-Vinext-Response-Store-Replayable");
|
|
297
|
+
rscHeaders.delete(VINEXT_PARAMS_HEADER);
|
|
298
|
+
rscHeaders.delete(VINEXT_RENDERED_PATH_AND_SEARCH_HEADER);
|
|
242
299
|
rscHeaders.set("Content-Type", VINEXT_RSC_CONTENT_TYPE);
|
|
243
|
-
rscHeaders.set("Vary", VINEXT_RSC_VARY_HEADER);
|
|
300
|
+
rscHeaders.set("Vary", VINEXT_RSC_VARY_HEADER$1);
|
|
244
301
|
applyRscCompatibilityIdHeader(rscHeaders);
|
|
245
302
|
applyRscDeploymentIdHeader(rscHeaders);
|
|
246
|
-
const
|
|
303
|
+
const serializedRscInvocation = JSON.stringify(rscInvocation);
|
|
247
304
|
await responseStore.put(rscKey, new Response(rscData, {
|
|
248
305
|
headers: rscHeaders,
|
|
249
306
|
status: 200
|
|
@@ -251,7 +308,7 @@ const handler = { async fetch(request, env, ctx) {
|
|
|
251
308
|
coalesce: true,
|
|
252
309
|
revalidator: {
|
|
253
310
|
id: ROUTE_REVALIDATOR_ID,
|
|
254
|
-
args: [
|
|
311
|
+
args: [serializedRscInvocation]
|
|
255
312
|
}
|
|
256
313
|
});
|
|
257
314
|
}
|
|
@@ -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
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { STATIC_ASSET_CACHE_PATH } from "./static-assets-adapter.shared.js";
|
|
2
|
+
import { DefaultCdnCacheAdapter } from "vinext/shims/cdn-cache";
|
|
3
|
+
//#region src/cache/static-assets-adapter.runtime.ts
|
|
4
|
+
async function cacheAssetId(key) {
|
|
5
|
+
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(key));
|
|
6
|
+
return Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
7
|
+
}
|
|
8
|
+
function isMetadata(value) {
|
|
9
|
+
return value !== null && typeof value === "object" && "kind" in value && (value.kind === "html" || value.kind === "rsc" || value.kind === "route") && "lastModified" in value && typeof value.lastModified === "number";
|
|
10
|
+
}
|
|
11
|
+
var StaticAssetsCacheAdapter = class {
|
|
12
|
+
assets;
|
|
13
|
+
ownsBackgroundRevalidation = false;
|
|
14
|
+
indexPromise;
|
|
15
|
+
constructor(assets) {
|
|
16
|
+
this.assets = assets;
|
|
17
|
+
}
|
|
18
|
+
loadIndex() {
|
|
19
|
+
return this.indexPromise ??= this.assets.fetch(`https://vinext.invalid${STATIC_ASSET_CACHE_PATH}/index.json`).then(async (response) => response.ok ? await response.json() : null).then((value) => value !== null && typeof value === "object" ? value : null);
|
|
20
|
+
}
|
|
21
|
+
async get(key) {
|
|
22
|
+
const id = await cacheAssetId(key);
|
|
23
|
+
const metadata = (await this.loadIndex())?.[id];
|
|
24
|
+
if (!isMetadata(metadata)) return null;
|
|
25
|
+
const extension = metadata.kind === "html" ? "html" : metadata.kind === "rsc" ? "rsc" : "route";
|
|
26
|
+
const bodyResponse = await this.assets.fetch(`https://vinext.invalid${STATIC_ASSET_CACHE_PATH}/${id}.${extension}`);
|
|
27
|
+
if (!bodyResponse.ok) return null;
|
|
28
|
+
let value;
|
|
29
|
+
if (metadata.kind === "html") value = {
|
|
30
|
+
kind: "APP_PAGE",
|
|
31
|
+
html: await bodyResponse.text(),
|
|
32
|
+
rscData: void 0,
|
|
33
|
+
headers: metadata.headers,
|
|
34
|
+
postponed: void 0,
|
|
35
|
+
prerendered: true,
|
|
36
|
+
status: metadata.status
|
|
37
|
+
};
|
|
38
|
+
else if (metadata.kind === "rsc") value = {
|
|
39
|
+
kind: "APP_PAGE",
|
|
40
|
+
html: "",
|
|
41
|
+
rscData: await bodyResponse.arrayBuffer(),
|
|
42
|
+
headers: void 0,
|
|
43
|
+
postponed: void 0,
|
|
44
|
+
prerendered: true,
|
|
45
|
+
status: void 0
|
|
46
|
+
};
|
|
47
|
+
else value = {
|
|
48
|
+
kind: "APP_ROUTE",
|
|
49
|
+
body: await bodyResponse.arrayBuffer(),
|
|
50
|
+
headers: metadata.headers ?? {},
|
|
51
|
+
status: metadata.status ?? 200
|
|
52
|
+
};
|
|
53
|
+
return {
|
|
54
|
+
lastModified: metadata.lastModified,
|
|
55
|
+
...metadata.cacheControl ? { cacheControl: metadata.cacheControl } : {},
|
|
56
|
+
value
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
async set(_key, _data, _ctx) {}
|
|
60
|
+
buildResponseHeaders(input) {
|
|
61
|
+
return { "Cache-Control": input.pendingDynamicCheck ? "no-store, must-revalidate" : input.cacheControl };
|
|
62
|
+
}
|
|
63
|
+
buildResponseIdentityHeaders() {
|
|
64
|
+
const buildId = process.env.__VINEXT_RSC_BUILD_IDENTITY || process.env.__VINEXT_BUILD_ID;
|
|
65
|
+
return buildId ? { "X-Vinext-Build-Id": buildId } : {};
|
|
66
|
+
}
|
|
67
|
+
async revalidateTag(_tags, _durations) {}
|
|
68
|
+
};
|
|
69
|
+
function createStaticAssetsCacheAdapter({ env, options } = {}) {
|
|
70
|
+
if (process.env.VINEXT_PRERENDER === "1") return new DefaultCdnCacheAdapter(process.env.__VINEXT_RSC_BUILD_IDENTITY || process.env.__VINEXT_BUILD_ID);
|
|
71
|
+
const binding = options?.binding ?? "ASSETS";
|
|
72
|
+
const assets = env?.[binding];
|
|
73
|
+
if (!assets || typeof assets !== "object" || !("fetch" in assets) || typeof assets.fetch !== "function") throw new Error(`[vinext] Static Assets binding \`${binding}\` is not configured.`);
|
|
74
|
+
return new StaticAssetsCacheAdapter(assets);
|
|
75
|
+
}
|
|
76
|
+
//#endregion
|
|
77
|
+
export { StaticAssetsCacheAdapter, createStaticAssetsCacheAdapter as default };
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { CacheControlMetadata } from "vinext/shims/cache-handler";
|
|
2
|
+
//#region src/cache/static-assets-adapter.shared.d.ts
|
|
3
|
+
export declare const STATIC_ASSET_CACHE_PATH = "/_vinext/static-cache";
|
|
4
|
+
export type StaticAssetCacheMetadata = {
|
|
5
|
+
kind: "html" | "rsc" | "route";
|
|
6
|
+
lastModified: number;
|
|
7
|
+
cacheControl?: CacheControlMetadata;
|
|
8
|
+
headers?: Record<string, string | string[]>;
|
|
9
|
+
status?: number;
|
|
10
|
+
};
|
|
11
|
+
export type StaticAssetCacheIndex = Record<string, StaticAssetCacheMetadata>;
|
|
12
|
+
//#endregion
|