@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.
Files changed (54) hide show
  1. package/README.md +165 -37
  2. package/dist/cache/browser-cache-policy.d.ts +7 -0
  3. package/dist/cache/browser-cache-policy.js +39 -0
  4. package/dist/cache/cdn-adapter-config.d.ts +2 -1
  5. package/dist/cache/cdn-adapter-config.js +13 -5
  6. package/dist/cache/cdn-adapter.d.ts +5 -56
  7. package/dist/cache/cdn-adapter.js +3 -61
  8. package/dist/cache/cdn-adapter.runtime.d.ts +1 -0
  9. package/dist/cache/cdn-adapter.runtime.js +33 -19
  10. package/dist/cache/cdn-adapter.worker.d.ts +1 -1
  11. package/dist/cache/cdn-adapter.worker.js +10 -35
  12. package/dist/cache/cdn-build-id.d.ts +1 -1
  13. package/dist/cache/cdn-build-id.js +1 -1
  14. package/dist/cache/config.d.ts +137 -0
  15. package/dist/cache/config.js +87 -0
  16. package/dist/cache/kv-data-adapter.d.ts +1 -1
  17. package/dist/cache/kv-data-adapter.js +1 -1
  18. package/dist/cache/kv-data-adapter.runtime.js +17 -15
  19. package/dist/cache/kv-key.d.ts +7 -4
  20. package/dist/cache/kv-key.js +17 -6
  21. package/dist/cache/response-store-adapter.d.ts +9 -6
  22. package/dist/cache/response-store-adapter.js +20 -1
  23. package/dist/cache/response-store-adapter.worker.js +121 -48
  24. package/dist/cache/response-store-cdn.runtime.js +1 -0
  25. package/dist/cache/response-store-data.runtime.js +53 -31
  26. package/dist/cache/static-assets-adapter.build.d.ts +6 -0
  27. package/dist/cache/static-assets-adapter.build.js +102 -0
  28. package/dist/cache/static-assets-adapter.d.ts +27 -0
  29. package/dist/cache/static-assets-adapter.js +26 -0
  30. package/dist/cache/static-assets-adapter.runtime.d.ts +27 -0
  31. package/dist/cache/static-assets-adapter.runtime.js +94 -0
  32. package/dist/cache/static-assets-adapter.shared.d.ts +12 -0
  33. package/dist/cache/static-assets-adapter.shared.js +4 -0
  34. package/dist/cache/workers-cache-cdn-adapter.d.ts +58 -0
  35. package/dist/cache/workers-cache-cdn-adapter.js +65 -0
  36. package/dist/cacheability-artifact.d.ts +7 -1
  37. package/dist/cacheability-artifact.js +60 -21
  38. package/dist/cacheability-probe.d.ts +2 -0
  39. package/dist/cacheability-probe.js +107 -13
  40. package/dist/cdn-warm.d.ts +1 -0
  41. package/dist/cdn-warm.js +4 -3
  42. package/dist/deploy-config.d.ts +3 -8
  43. package/dist/deploy-config.js +5 -23
  44. package/dist/deploy-help.js +47 -42
  45. package/dist/deploy.d.ts +18 -16
  46. package/dist/deploy.js +355 -216
  47. package/dist/tpr.d.ts +4 -0
  48. package/dist/tpr.js +16 -7
  49. package/dist/version-deploy.d.ts +27 -0
  50. package/dist/version-deploy.js +121 -3
  51. package/dist/wrangler-version-metadata.js +1 -1
  52. package/package.json +16 -5
  53. package/dist/prerender-kv-populate.d.ts +0 -24
  54. 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 { VINEXT_RSC_CONTENT_TYPE, VINEXT_RSC_VARY_HEADER, applyRscCompatibilityIdHeader, applyRscDeploymentIdHeader, createCanonicalRscRequestHeaders, createCanonicalRscRequestUrl } from "vinext/internal/server/app-rsc-cache-busting";
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 serializeInvocation(request, props) {
46
- return JSON.stringify({
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 serialized = serializeInvocation(request, props);
76
- return runWithResponseStoreInvocation(serialized, isReplayableInvocation(request, props), () => handleResponseStage(request, env, context, props, dispatchRequestStage, { cache }), capture);
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(request, props) {
123
+ async function cacheRequest(invocation) {
113
124
  const identity = JSON.stringify([
114
- request.method,
115
- request.url,
116
- safeProps(props),
117
- replayHeaders(request)
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 isResponseStoreMiss(response) {
132
- return response.status === 404 && response.headers.get("X-Workers-Response-Store") === "MISS";
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
- ...props,
220
+ ...identityProps,
179
221
  isRscRequest: true,
180
222
  renderMode: "navigation"
181
223
  },
182
- request: new Request(new URL(createCanonicalRscRequestUrl(stageRequest.url), stageRequest.url), { headers: createCanonicalRscRequestHeaders() })
224
+ request: new Request(new URL(createCanonicalRscRequestUrl(identityRequest.url), identityRequest.url), { headers: createCanonicalRscRequestHeaders() })
183
225
  } : void 0;
184
- const rscKey = rscSeed ? await cacheRequest(rscSeed.request, rscSeed.props) : void 0;
185
- const key = await cacheRequest(stageRequest, props);
186
- const stored = await responseStore.fetch(key);
187
- if (!isResponseStoreMiss(stored)) {
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 responseStore.fetch(rscKey);
190
- if (!isResponseStoreMiss(storedRsc)) {
191
- await storedRsc.body?.cancel();
233
+ const storedRsc = await readStoredResponse(rscKey);
234
+ if (storedRsc) {
235
+ storedRsc.body?.cancel().catch(() => {});
192
236
  return publicResponse(stored, "HIT", props);
193
237
  }
194
- await Promise.all([stored.body?.cancel(), storedRsc.body?.cancel()]);
238
+ stored.body?.cancel().catch(() => {});
195
239
  }
196
240
  const capture = rscSeed ? { captureRscData: true } : isWarmup ? {} : { streamResponse: true };
197
- const rendered = await invokeResponseStage(stageRequest, props, env, ctx, "shared", capture);
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: [invocation]
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
- await responseStore.put(key, cacheResponse, {
230
- coalesce: true,
231
- revalidator: {
232
- id: ROUTE_REVALIDATOR_ID,
233
- args: [invocation]
234
- }
235
- });
236
- if (rscSeed && rscKey && capture?.rscData) {
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 rscInvocation = serializeInvocation(rscSeed.request, rscSeed.props);
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: [rscInvocation]
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
- return handleRequestStage(request, env, context, dispatchResponseStage);
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 };
@@ -6,6 +6,7 @@ var ResponseStoreCdnCacheAdapter = class {
6
6
  headers;
7
7
  ownsBackgroundRevalidation = false;
8
8
  requiresCompletedResponseAdmission = true;
9
+ responseStageCacheIdentity = "query-free";
9
10
  responsePolicy;
10
11
  constructor(headers) {
11
12
  this.headers = headers;
@@ -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
- const response = await this.store.fetch(request);
196
- if (response.status === 404 && response.headers.get("X-Workers-Response-Store") === "MISS") return null;
197
- if (!response.ok) throw new Error(`Workers Response Store returned ${response.status}`);
198
- const entry = deserialize(await response.text());
199
- if (!entry) {
200
- await this.store.purge({ pathPrefixes: [new URL(request.url).pathname] });
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
- if (await expiration >= entry.lastModified) return null;
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