@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.
Files changed (59) hide show
  1. package/README.md +174 -37
  2. package/dist/cache/cdn-adapter-config.d.ts +2 -1
  3. package/dist/cache/cdn-adapter-config.js +13 -5
  4. package/dist/cache/cdn-adapter.d.ts +5 -56
  5. package/dist/cache/cdn-adapter.js +3 -61
  6. package/dist/cache/cdn-adapter.runtime.d.ts +1 -0
  7. package/dist/cache/cdn-adapter.runtime.js +17 -3
  8. package/dist/cache/cdn-adapter.worker.d.ts +1 -0
  9. package/dist/cache/cdn-adapter.worker.js +6 -4
  10. package/dist/cache/cdn-build-id.d.ts +1 -1
  11. package/dist/cache/cdn-build-id.js +1 -1
  12. package/dist/cache/config.d.ts +137 -0
  13. package/dist/cache/config.js +87 -0
  14. package/dist/cache/kv-data-adapter.d.ts +5 -1
  15. package/dist/cache/kv-data-adapter.js +6 -2
  16. package/dist/cache/kv-data-adapter.runtime.js +13 -9
  17. package/dist/cache/kv-key.d.ts +7 -4
  18. package/dist/cache/kv-key.js +16 -5
  19. package/dist/cache/response-store-adapter.d.ts +36 -0
  20. package/dist/cache/response-store-adapter.js +26 -1
  21. package/dist/cache/response-store-adapter.self-contained.worker.d.ts +1 -0
  22. package/dist/cache/response-store-adapter.self-contained.worker.js +3 -1
  23. package/dist/cache/response-store-adapter.service-binding.worker.d.ts +1 -0
  24. package/dist/cache/response-store-adapter.service-binding.worker.js +3 -1
  25. package/dist/cache/response-store-adapter.worker.d.ts +1 -1
  26. package/dist/cache/response-store-adapter.worker.js +148 -81
  27. package/dist/cache/response-store-cdn.runtime.js +1 -0
  28. package/dist/cache/response-store-data.runtime.js +53 -31
  29. package/dist/cache/static-assets-adapter.build.d.ts +6 -0
  30. package/dist/cache/static-assets-adapter.build.js +65 -0
  31. package/dist/cache/static-assets-adapter.d.ts +27 -0
  32. package/dist/cache/static-assets-adapter.js +26 -0
  33. package/dist/cache/static-assets-adapter.runtime.d.ts +26 -0
  34. package/dist/cache/static-assets-adapter.runtime.js +77 -0
  35. package/dist/cache/static-assets-adapter.shared.d.ts +12 -0
  36. package/dist/cache/static-assets-adapter.shared.js +4 -0
  37. package/dist/cache/workers-cache-cdn-adapter.d.ts +58 -0
  38. package/dist/cache/workers-cache-cdn-adapter.js +65 -0
  39. package/dist/cacheability-artifact.d.ts +7 -1
  40. package/dist/cacheability-artifact.js +60 -21
  41. package/dist/cacheability-probe.d.ts +2 -0
  42. package/dist/cacheability-probe.js +107 -13
  43. package/dist/cdn-warm.d.ts +2 -1
  44. package/dist/cdn-warm.js +25 -8
  45. package/dist/deploy-config.d.ts +3 -8
  46. package/dist/deploy-config.js +5 -23
  47. package/dist/deploy-help.js +58 -47
  48. package/dist/deploy.d.ts +31 -21
  49. package/dist/deploy.js +501 -286
  50. package/dist/tpr.d.ts +29 -72
  51. package/dist/tpr.js +52 -601
  52. package/dist/version-deploy.d.ts +27 -0
  53. package/dist/version-deploy.js +125 -5
  54. package/dist/wrangler-config.d.ts +19 -0
  55. package/dist/wrangler-config.js +294 -0
  56. package/dist/wrangler-version-metadata.js +1 -1
  57. package/package.json +16 -9
  58. package/dist/prerender-kv-populate.d.ts +0 -24
  59. 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 { VINEXT_RSC_CONTENT_TYPE, VINEXT_RSC_VARY_HEADER, applyRscCompatibilityIdHeader, applyRscDeploymentIdHeader, createCanonicalRscRequestHeaders, createCanonicalRscRequestUrl } from "vinext/internal/server/app-rsc-cache-busting";
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 = "__vinext_response_store";
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 serializeInvocation(request, props) {
44
- return JSON.stringify({
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 serialized = serializeInvocation(request, props);
74
- return runWithResponseStoreInvocation(serialized, isReplayableInvocation(request, props), () => handleResponseStage(request, env, context, props, dispatchRequestStage, { cache }), capture);
75
- }
76
- function createVinextResponseStoreOptions() {
77
- return { async regenerate(input, { env, ctx }) {
78
- if (input.id === ROUTE_REVALIDATOR_ID) {
79
- const invocation = parseInvocation(input.args.at(-1));
80
- const response = await invokeResponseStage(restoreRequest(invocation), invocation.props, env, ctx, "shared");
81
- if (!isCacheable(response)) {
82
- await response.body?.cancel().catch(() => {});
83
- throw new Error("Vinext response-stage regeneration was not cacheable");
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
- return response;
86
- }
87
- if (input.id === "vinext:cache-function" && typeof input.args[0] === "string" && typeof input.args[1] === "string") {
88
- const invocation = JSON.parse(input.args[1]);
89
- 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");
90
- return captureResponseStoreDataRegeneration(input.args[0], async () => {
91
- const responseStage = await loadVinextResponseStage();
92
- if (!responseStage.invokeCacheFunction) throw new Error("The vinext response stage cannot invoke cache functions");
93
- await responseStage.invokeCacheFunction(invocation, env, stageContext(ctx, env), (request) => invokeRequestStage(request, env, ctx));
94
- });
95
- }
96
- if (input.id === "vinext:data" && typeof input.args[0] === "string") {
97
- const invocation = parseInvocation(input.args.at(-1));
98
- return captureResponseStoreDataRegeneration(input.args[0], async () => {
99
- await (await invokeResponseStage(restoreRequest(invocation), invocation.props, env, ctx, "bypass")).body?.pipeTo(new WritableStream());
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
- throw new Error(`Unknown vinext response-store revalidator ${input.id}`);
103
- } };
120
+ };
104
121
  }
105
- async function cacheRequest(request, props) {
122
+ async function cacheRequest(invocation) {
106
123
  const identity = JSON.stringify([
107
- request.method,
108
- request.url,
109
- safeProps(props),
110
- replayHeaders(request)
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, { method: "GET" });
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 isResponseStoreMiss(response) {
122
- return response.status === 404 && response.headers.get("X-Workers-Response-Store") === "MISS";
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
- const invocation = serializeInvocation(stageRequest, props);
164
- if (options.cache === "bypass" || stageRequest.method !== "GET" && stageRequest.method !== "HEAD") return publicResponse(await invokeResponseStage(stageRequest, props, env, ctx, "bypass"), "BYPASS");
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
- ...props,
218
+ ...identityProps,
169
219
  isRscRequest: true,
170
220
  renderMode: "navigation"
171
221
  },
172
- request: new Request(new URL(createCanonicalRscRequestUrl(stageRequest.url), stageRequest.url), { headers: createCanonicalRscRequestHeaders() })
222
+ request: new Request(new URL(createCanonicalRscRequestUrl(identityRequest.url), identityRequest.url), { headers: createCanonicalRscRequestHeaders() })
173
223
  } : void 0;
174
- const rscKey = rscSeed ? await cacheRequest(rscSeed.request, rscSeed.props) : void 0;
175
- const key = await cacheRequest(stageRequest, props);
176
- const stored = await responseStore.fetch(key);
177
- if (!isResponseStoreMiss(stored)) {
178
- if (!rscKey) return publicResponse(stored, "HIT");
179
- const storedRsc = await responseStore.fetch(rscKey);
180
- if (!isResponseStoreMiss(storedRsc)) {
181
- await storedRsc.body?.cancel();
182
- return publicResponse(stored, "HIT");
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
- await Promise.all([stored.body?.cancel(), storedRsc.body?.cancel()]);
236
+ stored.body?.cancel().catch(() => {});
185
237
  }
186
238
  const capture = rscSeed ? { captureRscData: true } : isWarmup ? {} : { streamResponse: true };
187
- const rendered = await invokeResponseStage(stageRequest, props, env, ctx, "shared", capture);
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: [invocation]
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
- await responseStore.put(key, cacheResponse, {
220
- coalesce: true,
221
- revalidator: {
222
- id: ROUTE_REVALIDATOR_ID,
223
- args: [invocation]
224
- }
225
- });
226
- if (rscSeed && rscKey && capture?.rscData) {
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 rscInvocation = serializeInvocation(rscSeed.request, rscSeed.props);
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: [rscInvocation]
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);
@@ -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 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