@vinext/cloudflare 1.0.0-beta.8 → 1.0.0-beta.9

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 CHANGED
@@ -91,6 +91,15 @@ npx @vinext/cloudflare deploy
91
91
  `vinext-cloudflare deploy` never creates, rewrites, or deploys the Response
92
92
  Store Worker.
93
93
 
94
+ Metadata sharding is opt-in and works in either deployment mode:
95
+
96
+ ```ts
97
+ vinext({ cache: responseStoreAdapter({ shards: 16 }) });
98
+ ```
99
+
100
+ Keys remain pinned to one shard while tag/path mutations fan out across every
101
+ shard. Omit `shards` to retain the original single metadata Durable Object.
102
+
94
103
  To deploy storage and cache entrypoints with the application instead, select
95
104
  self-contained mode:
96
105
 
@@ -1,4 +1,5 @@
1
1
  import { WorkerEntrypoint } from "cloudflare:workers";
2
+ import "vinext/internal/server/cloudflare-workers-tracing";
2
3
  import { VinextAssetFetcher } from "vinext/server/multi-stage";
3
4
  //#region src/cache/cdn-adapter.worker.d.ts
4
5
  type CloudflareStageContext = {
@@ -2,8 +2,10 @@ import { VINEXT_CDN_BUILD_ID_HEADER, getVinextCdnBuildIdentity } from "./cdn-bui
2
2
  import { NEXTJS_CACHE_HEADER, VINEXT_CACHE_HEADER, VINEXT_PRERENDER_READINESS_PATH, VINEXT_RSC_VARY_HEADER } from "vinext/internal/server/headers";
3
3
  import { isNonCacheableCacheControl } from "vinext/shims/cdn-cache";
4
4
  import { WorkerEntrypoint } from "cloudflare:workers";
5
+ import "vinext/internal/server/cloudflare-workers-tracing";
5
6
  import { loadVinextRequestStage } from "vinext/server/request-stage";
6
7
  import { loadVinextResponseStage } from "vinext/server/response-stage";
8
+ import { traceCachedResponseStart } from "vinext/internal/server/response-start-tracing";
7
9
  //#region src/cache/cdn-adapter.worker.ts
8
10
  const CACHED_RESPONSE_STAGE_EXPORT = "VinextCachedResponse";
9
11
  const UNCACHED_RESPONSE_STAGE_EXPORT = "VinextUncachedResponse";
@@ -197,15 +199,15 @@ function preventResponseCaching(response) {
197
199
  * the outer gateway never forwards an inner shared-cache policy after adding
198
200
  * request-specific middleware or routing headers.
199
201
  */
200
- function markSharedResponseStage(response, provenanceToken, exposeEntrypointCacheStatus = false) {
202
+ function markSharedResponseStage(response, provenanceToken, responseStageProps, exposeEntrypointCacheStatus = false) {
201
203
  const headers = new Headers(response.headers);
202
204
  const cacheStatus = exposeEntrypointCacheStatus ? headers.get("CF-Cache-Status") : null;
203
205
  headers.set(SHARED_RESPONSE_STAGE_HEADER, cacheStatus ? `${provenanceToken}:${encodeURIComponent(cacheStatus)}` : provenanceToken);
204
- return new Response(response.body, {
206
+ return traceCachedResponseStart(new Response(response.body, {
205
207
  headers,
206
208
  status: response.status,
207
209
  statusText: response.statusText
208
- });
210
+ }), cacheStatus, responseStageProps);
209
211
  }
210
212
  function finalizeGatewayResponse(response, provenanceToken) {
211
213
  const provenance = response.headers.get(SHARED_RESPONSE_STAGE_HEADER);
@@ -358,7 +360,7 @@ var cdn_adapter_worker_default = { async fetch(request, env, context) {
358
360
  if (!binding) return responseStageUnavailable();
359
361
  const entrypointRequest = usesSharedCache ? await createCacheFacingRequest(stageRequest, serializedInvocation) : stageRequest;
360
362
  const response = validateResponseStageBuildIdentity(await binding.fetch(entrypointRequest));
361
- return usesSharedCache ? markSharedResponseStage(response, sharedResponseStageProvenance, true) : response;
363
+ return usesSharedCache ? markSharedResponseStage(response, sharedResponseStageProvenance, props, true) : response;
362
364
  } catch (error) {
363
365
  if (isResponseStageReadinessRequest(stageRequest)) return responseStageUnavailable();
364
366
  throw error;
@@ -44,5 +44,9 @@ export type KvDataAdapterOptions = {
44
44
  export declare function kvDataAdapter(options?: KvDataAdapterOptions): {
45
45
  adapter: string;
46
46
  options: KvDataAdapterOptions | undefined;
47
+ capabilities: {
48
+ buildIdentity: "response-header";
49
+ warmup: "data-cache";
50
+ };
47
51
  };
48
52
  //#endregion
@@ -18,7 +18,11 @@ function kvDataAdapter(options) {
18
18
  if (options?.binding !== void 0 && typeof options.binding !== "string") throw new TypeError("[vinext] kvDataAdapter({ binding }) must be a string KV binding name.");
19
19
  return {
20
20
  adapter: fileURLToPath(import.meta.resolve("./kv-data-adapter.runtime.js")),
21
- options
21
+ options,
22
+ capabilities: {
23
+ buildIdentity: "response-header",
24
+ warmup: "data-cache"
25
+ }
22
26
  };
23
27
  }
24
28
  //#endregion
@@ -1,6 +1,8 @@
1
1
  //#region src/cache/response-store-adapter.d.ts
2
2
  export type ResponseStoreAdapterOptions = {
3
3
  mode?: "self-contained" | "service-binding";
4
+ /** Split version-scoped metadata across this many Durable Objects. */
5
+ shards?: number;
4
6
  };
5
7
  /**
6
8
  * Use Workers Response Store for both vinext response-stage and data caching.
@@ -9,6 +11,31 @@ export type ResponseStoreAdapterOptions = {
9
11
  */
10
12
  export declare function responseStoreAdapter(options?: ResponseStoreAdapterOptions): {
11
13
  cdn: {
14
+ options?: undefined;
15
+ adapter: string;
16
+ output: {
17
+ entry: string;
18
+ matchesBuild({ plugins }: {
19
+ plugins: readonly {
20
+ name?: string;
21
+ }[];
22
+ }): boolean;
23
+ transformHostEntry({ code, id }: {
24
+ code: string;
25
+ id: string;
26
+ }): string | null;
27
+ type: "multi-stage";
28
+ };
29
+ capabilities: {
30
+ buildIdentity: "response-header";
31
+ isResponsePolicyHeader: (name: string) => boolean;
32
+ requestRouting: "uncached-stage";
33
+ warmup: "response-store";
34
+ };
35
+ } | {
36
+ options: {
37
+ shards: number;
38
+ };
12
39
  adapter: string;
13
40
  output: {
14
41
  entry: string;
@@ -31,6 +58,12 @@ export declare function responseStoreAdapter(options?: ResponseStoreAdapterOptio
31
58
  };
32
59
  };
33
60
  data: {
61
+ options?: undefined;
62
+ adapter: string;
63
+ } | {
64
+ options: {
65
+ shards: number;
66
+ };
34
67
  adapter: string;
35
68
  };
36
69
  };
@@ -9,11 +9,14 @@ const CLOUDFLARE_WORKER_ENTRY_ID = "virtual:cloudflare/worker-entry";
9
9
  function responseStoreAdapter(options = {}) {
10
10
  const mode = options.mode ?? "service-binding";
11
11
  if (mode !== "service-binding" && mode !== "self-contained") throw new Error(`Unknown Workers Response Store mode: ${String(mode)}`);
12
+ if (options.shards !== void 0 && (!Number.isSafeInteger(options.shards) || options.shards <= 1)) throw new TypeError("Workers Response Store shards must be an integer greater than 1");
13
+ const runtimeOptions = options.shards === void 0 ? {} : { options: { shards: options.shards } };
12
14
  const workerEntry = fileURLToPath(import.meta.resolve(mode === "self-contained" ? "./response-store-adapter.self-contained.worker.js" : "./response-store-adapter.service-binding.worker.js"));
13
15
  const entrypoints = mode === "self-contained" ? "CacheMetadata, ResponseStoreBinding, ResponseStoreRevalidator" : "ResponseStoreClient, ResponseStoreRevalidator";
14
16
  return {
15
17
  cdn: {
16
18
  adapter: fileURLToPath(import.meta.resolve("./response-store-cdn.runtime.js")),
19
+ ...runtimeOptions,
17
20
  output: {
18
21
  entry: workerEntry,
19
22
  matchesBuild({ plugins }) {
@@ -32,7 +35,10 @@ function responseStoreAdapter(options = {}) {
32
35
  warmup: "response-store"
33
36
  }
34
37
  },
35
- data: { adapter: fileURLToPath(import.meta.resolve("./response-store-data.runtime.js")) }
38
+ data: {
39
+ adapter: fileURLToPath(import.meta.resolve("./response-store-data.runtime.js")),
40
+ ...runtimeOptions
41
+ }
36
42
  };
37
43
  }
38
44
  //#endregion
@@ -1,4 +1,5 @@
1
1
  import { VinextResponseStoreEnv } from "./response-store-adapter.worker.js";
2
+ import "vinext/internal/server/cloudflare-workers-tracing";
2
3
  import { ResponseStoreRevalidatorEntrypoint, WorkersResponseStoreEnv } from "@cloudflare/workers-response-store";
3
4
  import { CacheMetadata as CacheMetadata$1, ResponseStoreBinding as ResponseStoreBinding$1 } from "@cloudflare/workers-response-store/service";
4
5
  //#region src/cache/response-store-adapter.self-contained.worker.d.ts
@@ -1,7 +1,9 @@
1
1
  import { createVinextResponseStoreHandler, createVinextResponseStoreOptions } from "./response-store-adapter.worker.js";
2
+ import "vinext/internal/server/cloudflare-workers-tracing";
2
3
  import { createWorkersResponseStore } from "@cloudflare/workers-response-store";
4
+ import { configuredCdnCacheAdapterOptions } from "virtual:vinext-cdn-cache-adapter";
3
5
  //#region src/cache/response-store-adapter.self-contained.worker.ts
4
- const responseStore = createWorkersResponseStore(createVinextResponseStoreOptions());
6
+ const responseStore = createWorkersResponseStore(createVinextResponseStoreOptions(configuredCdnCacheAdapterOptions));
5
7
  const CacheMetadata = responseStore.entrypoints.CacheMetadata;
6
8
  const ResponseStoreBinding = responseStore.entrypoints.ResponseStoreBinding;
7
9
  const ResponseStoreRevalidator = responseStore.entrypoints.ResponseStoreRevalidator;
@@ -1,4 +1,5 @@
1
1
  import { VinextResponseStoreEnv } from "./response-store-adapter.worker.js";
2
+ import "vinext/internal/server/cloudflare-workers-tracing";
2
3
  import { ResponseStoreClientEntrypoint, ResponseStoreRevalidatorEntrypoint, WorkersResponseStoreClientEnv } from "@cloudflare/workers-response-store";
3
4
  //#region src/cache/response-store-adapter.service-binding.worker.d.ts
4
5
  export declare const ResponseStoreClient: ResponseStoreClientEntrypoint;
@@ -1,7 +1,9 @@
1
1
  import { createVinextResponseStoreHandler, createVinextResponseStoreOptions } from "./response-store-adapter.worker.js";
2
+ import "vinext/internal/server/cloudflare-workers-tracing";
2
3
  import { createWorkersResponseStoreClient } from "@cloudflare/workers-response-store";
4
+ import { configuredCdnCacheAdapterOptions } from "virtual:vinext-cdn-cache-adapter";
3
5
  //#region src/cache/response-store-adapter.service-binding.worker.ts
4
- const responseStore = createWorkersResponseStoreClient(createVinextResponseStoreOptions());
6
+ const responseStore = createWorkersResponseStoreClient(createVinextResponseStoreOptions(configuredCdnCacheAdapterOptions));
5
7
  const ResponseStoreClient = responseStore.entrypoints.ResponseStoreClient;
6
8
  const ResponseStoreRevalidator = responseStore.entrypoints.ResponseStoreRevalidator;
7
9
  var response_store_adapter_service_binding_worker_default = createVinextResponseStoreHandler(responseStore);
@@ -7,7 +7,7 @@ type WorkerExecutionContext = {
7
7
  waitUntil(promise: Promise<unknown>): void;
8
8
  };
9
9
  export type VinextResponseStoreEnv = WorkersResponseStoreClientEnv | WorkersResponseStoreEnv;
10
- export declare function createVinextResponseStoreOptions<Env extends VinextResponseStoreEnv>(): WorkersResponseStoreOptions<Env>;
10
+ export declare function createVinextResponseStoreOptions<Env extends VinextResponseStoreEnv>(configuration?: Record<string, unknown>): WorkersResponseStoreOptions<Env>;
11
11
  export declare function createVinextResponseStoreHandler(store: WorkersResponseStore): {
12
12
  fetch(request: Request, env: VinextResponseStoreEnv, ctx: WorkerExecutionContext): Promise<Response>;
13
13
  };
@@ -3,12 +3,14 @@ import { VINEXT_RSC_CONTENT_TYPE, VINEXT_RSC_VARY_HEADER, applyRscCompatibilityI
3
3
  import { isNonCacheableCacheControl } from "vinext/shims/cdn-cache";
4
4
  import { loadVinextRequestStage } from "vinext/server/request-stage";
5
5
  import { loadVinextResponseStage } from "vinext/server/response-stage";
6
+ import { traceCachedResponseStart } from "vinext/internal/server/response-start-tracing";
6
7
  //#region src/cache/response-store-adapter.worker.ts
7
8
  const ROUTE_REVALIDATOR_ID = "vinext:response";
8
- const RESPONSE_STORE_KEY_PARAM = "__vinext_response_store";
9
+ const RESPONSE_STORE_KEY_PARAM = "__workers_response_store";
9
10
  const AGE_BASIS_HEADER = "X-Workers-Response-Store-Age-Basis";
10
11
  const WARMUP_USER_AGENT = "vinext-cloudflare-cdn-warm";
11
12
  const REPLAY_REQUEST_HEADERS = VINEXT_RSC_VARY_HEADER.split(",").map((name) => name.trim().toLowerCase());
13
+ const CACHE_REQUEST_VARY_HEADERS = REPLAY_REQUEST_HEADERS.map((name) => [name, "vinext-keyed"]);
12
14
  function stageContext(ctx, env) {
13
15
  const assets = Reflect.get(env, "ASSETS");
14
16
  return Object.assign(Object.create(Object.getPrototypeOf(ctx)), ctx, {
@@ -73,34 +75,39 @@ async function invokeResponseStage(request, props, env, ctx, cache, capture) {
73
75
  const serialized = serializeInvocation(request, props);
74
76
  return runWithResponseStoreInvocation(serialized, isReplayableInvocation(request, props), () => handleResponseStage(request, env, context, props, dispatchRequestStage, { cache }), capture);
75
77
  }
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");
78
+ function createVinextResponseStoreOptions(configuration) {
79
+ const shards = configuration?.shards;
80
+ if (shards !== void 0 && typeof shards !== "number") throw new TypeError("Workers Response Store shards must be a number");
81
+ return {
82
+ ...shards === void 0 ? {} : { shards },
83
+ async regenerate(input, { env, ctx }) {
84
+ if (input.id === ROUTE_REVALIDATOR_ID) {
85
+ const invocation = parseInvocation(input.args.at(-1));
86
+ const response = await invokeResponseStage(restoreRequest(invocation), invocation.props, env, ctx, "shared");
87
+ if (!isCacheable(response)) {
88
+ await response.body?.cancel().catch(() => {});
89
+ throw new Error("Vinext response-stage regeneration was not cacheable");
90
+ }
91
+ return response;
84
92
  }
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
- });
93
+ if (input.id === "vinext:cache-function" && typeof input.args[0] === "string" && typeof input.args[1] === "string") {
94
+ const invocation = JSON.parse(input.args[1]);
95
+ 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");
96
+ return captureResponseStoreDataRegeneration(input.args[0], async () => {
97
+ const responseStage = await loadVinextResponseStage();
98
+ if (!responseStage.invokeCacheFunction) throw new Error("The vinext response stage cannot invoke cache functions");
99
+ await responseStage.invokeCacheFunction(invocation, env, stageContext(ctx, env), (request) => invokeRequestStage(request, env, ctx));
100
+ });
101
+ }
102
+ if (input.id === "vinext:data" && typeof input.args[0] === "string") {
103
+ const invocation = parseInvocation(input.args.at(-1));
104
+ return captureResponseStoreDataRegeneration(input.args[0], async () => {
105
+ await (await invokeResponseStage(restoreRequest(invocation), invocation.props, env, ctx, "bypass")).body?.pipeTo(new WritableStream());
106
+ });
107
+ }
108
+ throw new Error(`Unknown vinext response-store revalidator ${input.id}`);
101
109
  }
102
- throw new Error(`Unknown vinext response-store revalidator ${input.id}`);
103
- } };
110
+ };
104
111
  }
105
112
  async function cacheRequest(request, props) {
106
113
  const identity = JSON.stringify([
@@ -111,8 +118,11 @@ async function cacheRequest(request, props) {
111
118
  ]);
112
119
  const digest = new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(identity)));
113
120
  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" });
121
+ url.searchParams.set(RESPONSE_STORE_KEY_PARAM, `v1.${[...digest].map((byte) => byte.toString(16).padStart(2, "0")).join("")}`);
122
+ return new Request(url, {
123
+ headers: CACHE_REQUEST_VARY_HEADERS,
124
+ method: "GET"
125
+ });
116
126
  }
117
127
  function isCacheable(response) {
118
128
  const policy = response.headers.get("Cloudflare-CDN-Cache-Control") ?? response.headers.get("CDN-Cache-Control") ?? response.headers.get("Cache-Control");
@@ -121,7 +131,7 @@ function isCacheable(response) {
121
131
  function isResponseStoreMiss(response) {
122
132
  return response.status === 404 && response.headers.get("X-Workers-Response-Store") === "MISS";
123
133
  }
124
- function publicResponse(response, cacheStatus) {
134
+ function publicResponse(response, cacheStatus, responseStageProps) {
125
135
  const headers = new Headers(response.headers);
126
136
  const publicCacheStatus = cacheStatus === "HIT" && headers.get("CF-Cache-Status") === "UPDATING" ? "UPDATING" : cacheStatus;
127
137
  const ageBasis = /^(\d+):(\d+)$/.exec(headers.get(AGE_BASIS_HEADER) ?? "");
@@ -145,11 +155,11 @@ function publicResponse(response, cacheStatus) {
145
155
  }
146
156
  const cacheControl = headers.get("Cache-Control");
147
157
  if (!cacheControl || !isNonCacheableCacheControl(cacheControl)) headers.set("Cache-Control", "private, max-age=0, must-revalidate");
148
- return new Response(response.body, {
158
+ return traceCachedResponseStart(new Response(response.body, {
149
159
  headers,
150
160
  status: response.status,
151
161
  statusText: response.statusText
152
- });
162
+ }), publicCacheStatus ?? null, responseStageProps);
153
163
  }
154
164
  let responseStore;
155
165
  function createVinextResponseStoreHandler(store) {
@@ -161,7 +171,7 @@ const handler = { async fetch(request, env, ctx) {
161
171
  const context = stageContext(ctx, env);
162
172
  const dispatchResponseStage = async (stageRequest, props, options) => {
163
173
  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");
174
+ if (options.cache === "bypass" || stageRequest.method !== "GET" && stageRequest.method !== "HEAD") return publicResponse(await invokeResponseStage(stageRequest, props, env, ctx, "bypass"), "BYPASS", props);
165
175
  const isWarmup = request.headers.get("user-agent") === WARMUP_USER_AGENT;
166
176
  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
177
  props: {
@@ -175,11 +185,11 @@ const handler = { async fetch(request, env, ctx) {
175
185
  const key = await cacheRequest(stageRequest, props);
176
186
  const stored = await responseStore.fetch(key);
177
187
  if (!isResponseStoreMiss(stored)) {
178
- if (!rscKey) return publicResponse(stored, "HIT");
188
+ if (!rscKey) return publicResponse(stored, "HIT", props);
179
189
  const storedRsc = await responseStore.fetch(rscKey);
180
190
  if (!isResponseStoreMiss(storedRsc)) {
181
191
  await storedRsc.body?.cancel();
182
- return publicResponse(stored, "HIT");
192
+ return publicResponse(stored, "HIT", props);
183
193
  }
184
194
  await Promise.all([stored.body?.cancel(), storedRsc.body?.cancel()]);
185
195
  }
@@ -204,11 +214,11 @@ const handler = { async fetch(request, env, ctx) {
204
214
  error: error instanceof Error ? error.message : String(error)
205
215
  }));
206
216
  }));
207
- return publicResponse(rendered, "MISS");
217
+ return publicResponse(rendered, "MISS", props);
208
218
  }
209
219
  if (!isCacheable(rendered)) {
210
220
  capture?.rscData?.catch(() => {});
211
- return publicResponse(rendered, "BYPASS");
221
+ return publicResponse(rendered, "BYPASS", props);
212
222
  }
213
223
  if (rscSeed && !capture?.rscData) {
214
224
  await rendered.body?.cancel();
@@ -245,7 +255,7 @@ const handler = { async fetch(request, env, ctx) {
245
255
  }
246
256
  });
247
257
  }
248
- return publicResponse(new Response(foreground, rendered), "MISS");
258
+ return publicResponse(new Response(foreground, rendered), "MISS", props);
249
259
  };
250
260
  const { handleRequestStage } = await loadVinextRequestStage();
251
261
  return handleRequestStage(request, env, context, dispatchResponseStage);
@@ -33,7 +33,7 @@ export type CdnWarmOptions = {
33
33
  requireCacheHit?: boolean;
34
34
  strict?: boolean;
35
35
  /** Cache-admission signal exposed by the deployed adapter. */
36
- statusSource?: "cloudflare" | "vinext";
36
+ statusSource?: "cloudflare" | "data-cache" | "vinext";
37
37
  fetchImpl?: typeof fetch;
38
38
  };
39
39
  export declare const DEFAULT_CDN_WARM_CONCURRENCY = 25;
package/dist/cdn-warm.js CHANGED
@@ -365,6 +365,7 @@ const REUSABLE_CF_CACHE_STATUSES = /* @__PURE__ */ new Set([
365
365
  "REVALIDATED",
366
366
  "UPDATING"
367
367
  ]);
368
+ const TRANSITIONAL_VINEXT_CACHE_STATUSES = /* @__PURE__ */ new Set(["MISS", "UPDATING"]);
368
369
  const NON_CACHEABLE_CF_CACHE_STATUSES = /* @__PURE__ */ new Set(["BYPASS"]);
369
370
  const CDN_CACHE_POLICY_HEADERS = [
370
371
  "Cloudflare-CDN-Cache-Control",
@@ -445,20 +446,29 @@ function validateCachePolicy(response, requireCacheStatus, requireCacheHit = fal
445
446
  error: `CF-Cache-Status is ${cacheStatus}`
446
447
  };
447
448
  }
448
- function validateVinextCacheStatus(response) {
449
+ function validateVinextCacheStatus(response, missingStatus = "failed", requireCacheHit = false) {
449
450
  const status = response.headers.get(VINEXT_CACHE_HEADER)?.trim().toUpperCase();
451
+ if (requireCacheHit && status !== "HIT") return {
452
+ outcome: "failed",
453
+ error: `${VINEXT_CACHE_HEADER} is ${status ?? "missing"}; the cache fill is not reusable`
454
+ };
450
455
  if (status === "MISS" || status === "HIT" || status === "UPDATING") return { outcome: "warmed" };
451
456
  if (status === "BYPASS" || status === "DYNAMIC") return {
452
457
  outcome: "skipped",
453
458
  reason: `${VINEXT_CACHE_HEADER} is ${status}`
454
459
  };
455
- return {
460
+ return missingStatus === "skipped" ? {
461
+ outcome: "skipped",
462
+ reason: `response is missing ${VINEXT_CACHE_HEADER}`
463
+ } : {
456
464
  outcome: "failed",
457
465
  error: `response is missing ${VINEXT_CACHE_HEADER}`
458
466
  };
459
467
  }
460
468
  function validateWarmAdmission(response, statusSource, requireCacheHit) {
461
- return statusSource === "vinext" ? validateVinextCacheStatus(response) : validateCachePolicy(response, true, requireCacheHit);
469
+ if (statusSource === "vinext") return validateVinextCacheStatus(response, "failed", requireCacheHit);
470
+ if (statusSource === "data-cache") return validateVinextCacheStatus(response, "skipped", requireCacheHit);
471
+ return validateCachePolicy(response, true, requireCacheHit);
462
472
  }
463
473
  function validateBuildIdentity(response, expectedBuildId) {
464
474
  if (expectedBuildId !== void 0 && response.headers.get("X-Vinext-Build-Id") !== expectedBuildId) {
@@ -645,6 +655,14 @@ function shouldRetryValidationFailure(response, target, options) {
645
655
  if (expectedIdentities.some((matches) => !matches)) return true;
646
656
  return isRetryableStatus(response.status, options.retryNotFound);
647
657
  }
658
+ function isRetryableCertificationFailure(response, statusSource, retryPropagationFailures) {
659
+ if (statusSource === "cloudflare") {
660
+ const status = response.headers.get("CF-Cache-Status")?.trim().toUpperCase();
661
+ return !REUSABLE_CF_CACHE_STATUSES.has(status ?? "") && (retryPropagationFailures || ADMITTED_CF_CACHE_STATUSES.has(status ?? ""));
662
+ }
663
+ const status = response.headers.get(VINEXT_CACHE_HEADER)?.trim().toUpperCase();
664
+ return TRANSITIONAL_VINEXT_CACHE_STATUSES.has(status ?? "");
665
+ }
648
666
  async function warmOnePath(target, options) {
649
667
  const url = buildWarmupUrl(options.targetUrl, target.pathname);
650
668
  let lastError = "request failed before the first attempt";
@@ -707,8 +725,7 @@ async function warmOnePath(target, options) {
707
725
  }
708
726
  lastSkippedReason = null;
709
727
  lastError = validation.error;
710
- const cacheStatus = response.headers.get("CF-Cache-Status")?.trim().toUpperCase();
711
- lastRetryable = options.requireCacheHit && ADMITTED_CF_CACHE_STATUSES.has(cacheStatus ?? "") && !REUSABLE_CF_CACHE_STATUSES.has(cacheStatus ?? "") || shouldRetryValidationFailure(response, target, options);
728
+ lastRetryable = options.requireCacheHit && isRetryableCertificationFailure(response, options.statusSource, options.retryPropagationFailures) || shouldRetryValidationFailure(response, target, options);
712
729
  if (!lastRetryable) break;
713
730
  if (!canRetry(attempt)) break;
714
731
  if (!await waitBeforeRetry()) return {
@@ -745,8 +762,7 @@ async function warmOnePath(target, options) {
745
762
  }
746
763
  lastSkippedReason = null;
747
764
  lastError = validation.error;
748
- const cacheStatus = response.headers.get("CF-Cache-Status")?.trim().toUpperCase();
749
- lastRetryable = options.requireCacheHit && ADMITTED_CF_CACHE_STATUSES.has(cacheStatus ?? "") && !REUSABLE_CF_CACHE_STATUSES.has(cacheStatus ?? "") || shouldRetryValidationFailure(response, target, options);
765
+ lastRetryable = options.requireCacheHit && isRetryableCertificationFailure(response, options.statusSource, options.retryPropagationFailures) || shouldRetryValidationFailure(response, target, options);
750
766
  if (!lastRetryable) break;
751
767
  } catch (error) {
752
768
  lastSkippedReason = null;
@@ -19,6 +19,8 @@ function formatDeployHelp() {
19
19
  --skip-build Skip the build step (use existing dist/)
20
20
  --dry-run Validate setup without building or deploying
21
21
  --verbose Print raw output from internal Wrangler commands
22
+ --no-promote Do not promote the uploaded Worker version to 100%
23
+ traffic
22
24
  --prerender-all Pre-render discovered routes after building (future
23
25
  releases will auto-populate the remote cache)
24
26
  --prerender-concurrency <count>
@@ -60,8 +62,6 @@ function formatDeployHelp() {
60
62
  --dangerously-promote-on-cdn-warm-error
61
63
  Promote even when ordinary staged warmup cannot be
62
64
  verified (never bypasses --warm-cdn-certify)
63
- --warm-cdn-no-promote Leave the warmed Worker version staged at 0% traffic;
64
- production triggers are still applied before warming
65
65
  --warm-cdn-promotion-delay <ms>
66
66
  Delay before promotion after warmup (default: 15000)
67
67
  --warm-cdn-include-fallbacks
@@ -69,16 +69,18 @@ function formatDeployHelp() {
69
69
  -h, --help Show this help
70
70
 
71
71
  Experimental:
72
- --experimental-tpr Enable Traffic-aware Pre-Rendering
73
- --tpr-coverage <pct> Traffic coverage target, 0-100 (default: 90)
74
- --tpr-limit <count> Hard cap on pages to pre-render (default: 1000)
75
- --tpr-window <hours> Analytics lookback window in hours (default: 24)
72
+ --experimental-traffic-aware-warm-cache
73
+ Select CDN pre-warm routes from traffic
74
+ --traffic-aware-coverage <pct> Traffic coverage target, 0-100 (default: 90)
75
+ --traffic-aware-limit <count> Hard cap on selected routes (default: 1000)
76
+ --traffic-aware-window <hours> Analytics lookback window in hours (default: 24)
76
77
 
77
- TPR (Traffic-aware Pre-Rendering) uses Cloudflare zone analytics to determine
78
- which pages get the most traffic and pre-renders them into KV cache during
79
- deploy. This feature is experimental and must be explicitly enabled. Requires
80
- a custom domain (zone analytics are unavailable on *.workers.dev) and the
81
- CLOUDFLARE_API_TOKEN environment variable with Zone.Analytics read permission.
78
+ Traffic-aware warming uses Cloudflare zone analytics to select the
79
+ highest-traffic routes, then feeds those routes into the same staged CDN
80
+ pre-warming flow used by --experimental-warm-cdn-cache. It requires a custom
81
+ domain and a CLOUDFLARE_API_TOKEN with Zone Analytics read permission.
82
+
83
+ Legacy --experimental-tpr and --tpr-* aliases remain supported.
82
84
 
83
85
  Workers Cache automatically uses tiered caching. Warmed entries can therefore
84
86
  be reused outside the data center reached by the warmup request after cache
@@ -93,12 +95,16 @@ function formatDeployHelp() {
93
95
  vinext-cloudflare deploy --config dist/server/wrangler.json Deploy using a generated Wrangler config
94
96
  vinext-cloudflare deploy --dry-run Validate setup without building or deploying
95
97
  vinext-cloudflare deploy --name my-app Deploy with a custom Worker name
98
+ vinext-cloudflare deploy --no-promote Upload a version without changing deployment traffic
96
99
  vinext-cloudflare deploy --experimental-warm-cdn-cache Warm build-discovered paths during version deploy (experimental)
97
100
  vinext-cloudflare deploy --experimental-warm-cdn-cache --warm-cdn-target https://example.com
98
101
  Warm an explicit production origin
99
- vinext-cloudflare deploy --experimental-tpr Enable TPR during deploy
100
- vinext-cloudflare deploy --experimental-tpr --tpr-coverage 95 Cover 95% of traffic
101
- vinext-cloudflare deploy --experimental-tpr --tpr-limit 500 Cap at 500 pages
102
+ vinext-cloudflare deploy --experimental-traffic-aware-warm-cache
103
+ Enable traffic-aware warming
104
+ vinext-cloudflare deploy --experimental-traffic-aware-warm-cache --traffic-aware-coverage 95
105
+ Cover 95% of traffic
106
+ vinext-cloudflare deploy --experimental-traffic-aware-warm-cache --traffic-aware-limit 500
107
+ Cap at 500 routes
102
108
  `;
103
109
  }
104
110
  function printDeployHelp() {
package/dist/deploy.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { CdnWarmOptions, CdnWarmRequestPlan, PrerenderWarmPlan } from "./cdn-warm.js";
2
- import { parseWranglerConfig } from "./tpr.js";
2
+ import { parseWranglerConfig } from "./wrangler-config.js";
3
+ import { TrafficEntry } from "./tpr.js";
3
4
  import { WranglerDeploymentStatus, WranglerVersionTraffic } from "./version-deploy.js";
4
5
  import { KVBulkPair } from "./prerender-kv-populate.js";
5
6
  import { spawn } from "node:child_process";
@@ -58,17 +59,17 @@ export type DeployOptions = {
58
59
  warmCdnReadinessProbeDelay?: number;
59
60
  /** Promote even when staged CDN warmup cannot be completed */
60
61
  dangerouslyPromoteOnCdnWarmError?: boolean;
61
- /** Promote the warmed Worker version to 100% traffic (default: true) */
62
+ /** Promote the uploaded Worker version to 100% traffic (default: true) */
62
63
  warmCdnPromote?: boolean;
63
64
  /** Delay between successful warmup and promotion in milliseconds */
64
65
  warmCdnPromotionDelay?: number;
65
66
  /** Include PPR fallback-shell placeholder paths during CDN warmup */
66
67
  warmCdnIncludeFallbacks?: boolean;
67
- /** Enable experimental TPR (Traffic-aware Pre-Rendering) */
68
+ /** Select CDN pre-warm routes using traffic analytics */
68
69
  experimentalTPR?: boolean;
69
70
  /** TPR: traffic coverage percentage target (0–100, default: 90) */
70
71
  tprCoverage?: number;
71
- /** TPR: hard cap on number of pages to pre-render (default: 1000) */
72
+ /** TPR: hard cap on selected routes (default: 1000) */
72
73
  tprLimit?: number;
73
74
  /** TPR: analytics lookback window in hours (default: 24) */
74
75
  tprWindow?: number;
@@ -157,18 +158,25 @@ export declare function runWranglerKVBulkPut(root: string, options: {
157
158
  pairs: KVBulkPair[];
158
159
  tempDir?: string;
159
160
  }, execute?: typeof spawn, nodeExecutable?: string): Promise<void>;
160
- export declare function runWranglerDeploy(root: string, options: Pick<DeployOptions, "preview" | "env" | "name" | "config">, execute?: typeof spawn): Promise<string>;
161
+ export declare function runWranglerDeploy(root: string, options: Pick<DeployOptions, "preview" | "env" | "name" | "config" | "verbose"> & {
162
+ promote?: boolean;
163
+ }, execute?: typeof spawn): Promise<string>;
161
164
  export declare function hasCdnWarmRequests(plan: Omit<CdnWarmRequestPlan, "pagesDataPaths"> & {
162
165
  pagesDataPaths?: readonly string[];
163
166
  }): boolean;
167
+ export declare function selectTPRWarmPlan(plan: PrerenderWarmPlan, traffic: readonly TrafficEntry[], coverage: number, limit: number): PrerenderWarmPlan;
164
168
  export declare function projectRequiresRouteCacheabilityProbeManifest(project: Pick<ProjectInfo, "isAppRouter" | "isPagesRouter">, cacheConfig: VinextCacheConfig | null): boolean;
165
169
  type CdnWarmDeployOptions = Pick<DeployOptions, "preview" | "env" | "name" | "config" | "verbose" | "warmCdnTarget" | "warmCdnConcurrency" | "warmCdnTimeout" | "warmCdnRetries" | "warmCdnDiscoveryTimeout" | "warmCdnDiscoveryRetries" | "warmCdnProbeTimeout" | "warmCdnProbeRetries" | "warmCdnCertify" | "warmCdnReadinessTimeout" | "warmCdnReadinessRetries" | "warmCdnReadinessProbes" | "warmCdnReadinessProbeDelay" | "dangerouslyPromoteOnCdnWarmError" | "warmCdnPromote" | "warmCdnPromotionDelay"> & Pick<CdnWarmOptions, "deploymentId" | "expectedBuildId" | "expectedRscBuildId" | "loadingShellPaths" | "pagesDataPaths" | "routeHandlerPaths" | "routePatterns" | "rscPaths" | "statusSource"> & {
170
+ /** Allow optional route selection to discover no warmable requests. */
171
+ allowEmptyWarmPlan?: boolean;
166
172
  /** Probe a staged Worker and upload the resulting manifest as a second version. */
167
173
  cacheabilityProbe?: boolean;
168
174
  discoverWarmPlan?: (target: {
169
175
  headers?: HeadersInit;
170
176
  targetUrl: string;
171
177
  }) => Promise<PrerenderWarmPlan>;
178
+ /** Narrow the final warm requests without changing discovery or probe metadata. */
179
+ selectWarmPlan?: (plan: PrerenderWarmPlan) => PrerenderWarmPlan;
172
180
  };
173
181
  export declare function deployWithCdnWarmup(root: string, paths: readonly string[], options: CdnWarmDeployOptions): Promise<string>;
174
182
  export declare function resolveCdnWarmupTargetUrl(root: string, deployedUrl: string | null): string | null;