@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,6 +1,6 @@
1
1
  import { VINEXT_CDN_BUILD_ID_HEADER, getVinextCdnBuildIdentity } from "./cdn-build-id.js";
2
- import { NEXTJS_CACHE_HEADER, VINEXT_CACHE_HEADER, VINEXT_PRERENDER_READINESS_PATH, VINEXT_RSC_VARY_HEADER } from "vinext/internal/server/headers";
3
- import { isNonCacheableCacheControl } from "vinext/shims/cdn-cache";
2
+ import { SHARED_RESPONSE_STAGE_HEADER, finalizeGatewayResponse } from "./browser-cache-policy.js";
3
+ import { VINEXT_PRERENDER_READINESS_PATH, VINEXT_RSC_VARY_HEADER } from "vinext/internal/server/headers";
4
4
  import { WorkerEntrypoint } from "cloudflare:workers";
5
5
  import "vinext/internal/server/cloudflare-workers-tracing";
6
6
  import { loadVinextRequestStage } from "vinext/server/request-stage";
@@ -13,8 +13,6 @@ const AUTHORIZATION_TRANSPORT_HEADER = "x-vinext-internal-authorization";
13
13
  const REQUEST_CACHE_CONTROL_TRANSPORT_HEADER = "x-vinext-internal-request-cache-control";
14
14
  const REQUEST_CF_TRANSPORT_HEADER = "x-vinext-internal-request-cf";
15
15
  const REQUEST_PRAGMA_TRANSPORT_HEADER = "x-vinext-internal-request-pragma";
16
- const CLOUDFLARE_EDGE_POLICY_HEADER = "Cloudflare-CDN-Cache-Control";
17
- const SHARED_RESPONSE_STAGE_HEADER = "x-vinext-cloudflare-shared-response-stage";
18
16
  const RESPONSE_STAGE_WIRE_CACHE = {
19
17
  bypass: "vinext-cloudflare-v1:bypass",
20
18
  shared: "vinext-cloudflare-v1:shared"
@@ -209,33 +207,6 @@ function markSharedResponseStage(response, provenanceToken, responseStageProps,
209
207
  statusText: response.statusText
210
208
  }), cacheStatus, responseStageProps);
211
209
  }
212
- function finalizeGatewayResponse(response, provenanceToken) {
213
- const provenance = response.headers.get(SHARED_RESPONSE_STAGE_HEADER);
214
- const usedSharedResponseStage = provenance === provenanceToken || provenance?.startsWith(`${provenanceToken}:`) === true;
215
- const sharedResponseStageCollision = provenance !== null && !usedSharedResponseStage;
216
- const cacheControl = response.headers.get("Cache-Control");
217
- if (!response.headers.has(CLOUDFLARE_EDGE_POLICY_HEADER) && !usedSharedResponseStage && !sharedResponseStageCollision) return response;
218
- const headers = new Headers(response.headers);
219
- headers.delete(SHARED_RESPONSE_STAGE_HEADER);
220
- headers.delete(CLOUDFLARE_EDGE_POLICY_HEADER);
221
- if (usedSharedResponseStage || sharedResponseStageCollision) {
222
- headers.delete("CDN-Cache-Control");
223
- headers.delete("Cache-Tag");
224
- headers.delete(VINEXT_CACHE_HEADER);
225
- headers.delete(NEXTJS_CACHE_HEADER);
226
- if (!cacheControl || !isNonCacheableCacheControl(cacheControl)) headers.set("Cache-Control", "private, max-age=0, must-revalidate");
227
- }
228
- if (usedSharedResponseStage && provenance?.startsWith(`${provenanceToken}:`)) {
229
- const cacheStatus = decodeURIComponent(provenance.slice(provenanceToken.length + 1));
230
- headers.set(VINEXT_CACHE_HEADER, cacheStatus);
231
- headers.set(NEXTJS_CACHE_HEADER, cacheStatus);
232
- }
233
- return new Response(response.body, {
234
- headers,
235
- status: response.status,
236
- statusText: response.statusText
237
- });
238
- }
239
210
  function hasTaggedCustomVary(response) {
240
211
  if (!response.headers.get("Cache-Tag")) return false;
241
212
  return (response.headers.get("Vary") ?? "").split(",").map((name) => name.trim().toLowerCase()).filter(Boolean).some((name) => !FRAMEWORK_RESPONSE_VARY_FIELDS.has(name));
@@ -320,7 +291,7 @@ var VinextCachedResponse = class extends WorkerEntrypoint {
320
291
  return cache.purge(options);
321
292
  }
322
293
  };
323
- /** Uncached response entrypoint. Bypass and probe renders execute only here. */
294
+ /** Uncached response entrypoint retained for deployment readiness probes. */
324
295
  var VinextUncachedResponse = class extends WorkerEntrypoint {
325
296
  async fetch(request) {
326
297
  const context = withResponseStagePurge(withWorkerHostRuntime(this.ctx, this.env));
@@ -336,7 +307,7 @@ var VinextUncachedResponse = class extends WorkerEntrypoint {
336
307
  /** Uncached gateway: request routing and middleware always execute here. */
337
308
  var cdn_adapter_worker_default = { async fetch(request, env, context) {
338
309
  request = stripUntrustedTransportHeaders(request);
339
- const sharedResponseStageProvenance = crypto.randomUUID();
310
+ const sharedResponses = /* @__PURE__ */ new Map();
340
311
  const stageContext = withResponseStagePurge(withWorkerHostRuntime(context, env));
341
312
  const dispatchResponseStage = async (stageRequest, props, options) => {
342
313
  const expectedResponseStageBuildIdentity = getVinextCdnBuildIdentity();
@@ -349,6 +320,7 @@ var cdn_adapter_worker_default = { async fetch(request, env, context) {
349
320
  };
350
321
  const usesSharedCache = options.cache === "shared";
351
322
  try {
323
+ if (!usesSharedCache && !isResponseStageReadinessRequest(stageRequest)) return stampResponseStageBuildIdentity(await invokeResponseStage(stageRequest, env, stageContext, invocation));
352
324
  const serializedInvocation = JSON.stringify({
353
325
  ...invocation,
354
326
  options: expectedResponseStageBuildIdentity === null ? options : {
@@ -360,14 +332,17 @@ var cdn_adapter_worker_default = { async fetch(request, env, context) {
360
332
  if (!binding) return responseStageUnavailable();
361
333
  const entrypointRequest = usesSharedCache ? await createCacheFacingRequest(stageRequest, serializedInvocation) : stageRequest;
362
334
  const response = validateResponseStageBuildIdentity(await binding.fetch(entrypointRequest));
363
- return usesSharedCache ? markSharedResponseStage(response, sharedResponseStageProvenance, props, true) : response;
335
+ if (!usesSharedCache) return response;
336
+ const shared = markSharedResponseStage(response, crypto.randomUUID(), props, true);
337
+ sharedResponses.set(shared.headers.get(SHARED_RESPONSE_STAGE_HEADER), { headers: new Headers(shared.headers) });
338
+ return shared;
364
339
  } catch (error) {
365
340
  if (isResponseStageReadinessRequest(stageRequest)) return responseStageUnavailable();
366
341
  throw error;
367
342
  }
368
343
  };
369
344
  const { handleRequestStage } = await loadVinextRequestStage();
370
- return finalizeGatewayResponse(await handleRequestStage(request, env, stageContext, dispatchResponseStage), sharedResponseStageProvenance);
345
+ return finalizeGatewayResponse(await handleRequestStage(request, env, stageContext, dispatchResponseStage), sharedResponses);
371
346
  } };
372
347
  //#endregion
373
348
  export { VinextCachedResponse, VinextUncachedResponse, cdn_adapter_worker_default as default };
@@ -1,6 +1,6 @@
1
1
  //#region src/cache/cdn-build-id.d.ts
2
2
  /** Build identity stamped by the Cloudflare CDN adapter on page responses. */
3
3
  export declare const VINEXT_CDN_BUILD_ID_HEADER = "X-Vinext-Build-Id";
4
- /** Opaque identity shared by every server entry emitted by one vinext build. */
4
+ /** Opaque identity shared by every server entry emitted by one vite build. */
5
5
  export declare function getVinextCdnBuildIdentity(): string | null;
6
6
  //#endregion
@@ -1,7 +1,7 @@
1
1
  //#region src/cache/cdn-build-id.ts
2
2
  /** Build identity stamped by the Cloudflare CDN adapter on page responses. */
3
3
  const VINEXT_CDN_BUILD_ID_HEADER = "X-Vinext-Build-Id";
4
- /** Opaque identity shared by every server entry emitted by one vinext build. */
4
+ /** Opaque identity shared by every server entry emitted by one vite build. */
5
5
  function getVinextCdnBuildIdentity() {
6
6
  return process.env.__VINEXT_RSC_BUILD_IDENTITY || process.env.__VINEXT_BUILD_ID || null;
7
7
  }
@@ -0,0 +1,137 @@
1
+ import { CloudflareConfig, WorkerConfig } from "cf/config";
2
+ //#region src/cache/config.d.ts
3
+ type ResponseStoreStorageOptions = {
4
+ worker: string;
5
+ bucket: string;
6
+ };
7
+ export declare function createWorkersResponseStoreSelfContainedConfig(options: ResponseStoreStorageOptions): Promise<{
8
+ cache: {
9
+ enabled: true;
10
+ };
11
+ exports: {
12
+ CacheMetadata: {
13
+ state?: "created";
14
+ type: "durable-object";
15
+ storage: "sqlite";
16
+ container: undefined;
17
+ } | {
18
+ state?: "created";
19
+ type: "durable-object";
20
+ storage: "sqlite";
21
+ container: undefined;
22
+ };
23
+ ResponseStoreBinding: import("cf/config").WorkerEntrypointExport;
24
+ };
25
+ env: {
26
+ CACHE_BODIES: import("cf/config").R2Binding;
27
+ CACHE_METADATA: {
28
+ type: "durable-object";
29
+ worker: string;
30
+ exportName: string;
31
+ };
32
+ CF_VERSION_METADATA: import("cf/config").VersionMetadataBinding;
33
+ };
34
+ }>;
35
+ export declare function createWorkersResponseStoreServiceBindingConfig(options: {
36
+ accountId?: CloudflareConfig["accountId"];
37
+ worker: {
38
+ name: string;
39
+ compatibilityDate: string;
40
+ compatibilityFlags?: string[];
41
+ observability?: WorkerConfig["observability"];
42
+ };
43
+ bucket: string;
44
+ }): Promise<{
45
+ accountId: string | undefined;
46
+ serviceBindingWorker: {
47
+ cache: {
48
+ enabled: true;
49
+ };
50
+ exports: {
51
+ CacheMetadata: {
52
+ state?: "created";
53
+ type: "durable-object";
54
+ storage: "sqlite";
55
+ container: undefined;
56
+ } | {
57
+ state?: "created";
58
+ type: "durable-object";
59
+ storage: "sqlite";
60
+ container: undefined;
61
+ };
62
+ ResponseStoreBinding: import("cf/config").WorkerEntrypointExport;
63
+ };
64
+ env: {
65
+ CACHE_BODIES: import("cf/config").R2Binding;
66
+ CACHE_METADATA: {
67
+ type: "durable-object";
68
+ worker: string;
69
+ exportName: string;
70
+ };
71
+ };
72
+ name: string;
73
+ compatibilityDate: string;
74
+ compatibilityFlags?: string[];
75
+ observability?: WorkerConfig["observability"];
76
+ entrypoint: string;
77
+ workersDev: false;
78
+ previewUrls: false;
79
+ };
80
+ applicationWorker: {
81
+ cache: {
82
+ enabled: false;
83
+ };
84
+ env: {
85
+ RESPONSE_STORE: import("cf/config").WorkerBinding<{
86
+ cache: {
87
+ enabled: true;
88
+ };
89
+ exports: {
90
+ CacheMetadata: {
91
+ state?: "created";
92
+ type: "durable-object";
93
+ storage: "sqlite";
94
+ container: undefined;
95
+ } | {
96
+ state?: "created";
97
+ type: "durable-object";
98
+ storage: "sqlite";
99
+ container: undefined;
100
+ };
101
+ ResponseStoreBinding: import("cf/config").WorkerEntrypointExport;
102
+ };
103
+ env: {
104
+ CACHE_BODIES: import("cf/config").R2Binding;
105
+ CACHE_METADATA: {
106
+ type: "durable-object";
107
+ worker: string;
108
+ exportName: string;
109
+ };
110
+ };
111
+ name: string;
112
+ compatibilityDate: string;
113
+ compatibilityFlags?: string[];
114
+ observability?: WorkerConfig["observability"];
115
+ entrypoint: string;
116
+ workersDev: false;
117
+ previewUrls: false;
118
+ }, "ResponseStoreService">;
119
+ CF_VERSION_METADATA: import("cf/config").VersionMetadataBinding;
120
+ };
121
+ };
122
+ }>;
123
+ export declare function createWorkersCacheConfig({ versionMetadataBinding }?: {
124
+ versionMetadataBinding?: string;
125
+ }): Promise<{
126
+ cache: {
127
+ enabled: false;
128
+ };
129
+ env: {
130
+ [versionMetadataBinding]: import("cf/config").VersionMetadataBinding;
131
+ };
132
+ exports: {
133
+ VinextCachedResponse: import("cf/config").WorkerEntrypointExport;
134
+ VinextUncachedResponse: import("cf/config").WorkerEntrypointExport;
135
+ };
136
+ }>;
137
+ //#endregion
@@ -0,0 +1,87 @@
1
+ //#region src/cache/config.ts
2
+ const RESPONSE_STORE_BINDING = "RESPONSE_STORE";
3
+ const RESPONSE_STORE_ENTRYPOINT = "ResponseStoreService";
4
+ const DEFAULT_VERSION_METADATA_BINDING = "CF_VERSION_METADATA";
5
+ const CACHE_BODIES_BINDING = "CACHE_BODIES";
6
+ const CACHE_METADATA_BINDING = "CACHE_METADATA";
7
+ const CACHE_METADATA_CLASS = "CacheMetadata";
8
+ const RESPONSE_STORE_CACHE_ENTRYPOINT = "ResponseStoreBinding";
9
+ const RESPONSE_STORE_SERVICE_ENTRYPOINT = "@cloudflare/workers-response-store/service";
10
+ async function createResponseStoreWorkerConfig(options) {
11
+ const { bindings, exports } = await import("cf/config");
12
+ return {
13
+ cache: { enabled: true },
14
+ exports: {
15
+ [CACHE_METADATA_CLASS]: {
16
+ ...exports.durableObject({ storage: "sqlite" }),
17
+ storage: "sqlite",
18
+ container: void 0
19
+ },
20
+ [RESPONSE_STORE_CACHE_ENTRYPOINT]: exports.worker({ cache: { enabled: true } })
21
+ },
22
+ env: {
23
+ [CACHE_BODIES_BINDING]: bindings.r2({ name: options.bucket }),
24
+ [CACHE_METADATA_BINDING]: {
25
+ ...bindings.durableObject({
26
+ worker: options.worker,
27
+ exportName: CACHE_METADATA_CLASS
28
+ }),
29
+ worker: options.worker,
30
+ exportName: CACHE_METADATA_CLASS
31
+ }
32
+ }
33
+ };
34
+ }
35
+ async function createWorkersResponseStoreSelfContainedConfig(options) {
36
+ const { bindings } = await import("cf/config");
37
+ const storage = await createResponseStoreWorkerConfig(options);
38
+ return {
39
+ ...storage,
40
+ env: {
41
+ ...storage.env,
42
+ [DEFAULT_VERSION_METADATA_BINDING]: bindings.versionMetadata()
43
+ }
44
+ };
45
+ }
46
+ async function createWorkersResponseStoreServiceBindingConfig(options) {
47
+ const { bindings } = await import("cf/config");
48
+ const storage = await createResponseStoreWorkerConfig({
49
+ worker: options.worker.name,
50
+ bucket: options.bucket
51
+ });
52
+ const serviceBindingWorker = {
53
+ ...options.worker,
54
+ entrypoint: RESPONSE_STORE_SERVICE_ENTRYPOINT,
55
+ workersDev: false,
56
+ previewUrls: false,
57
+ ...storage
58
+ };
59
+ return {
60
+ accountId: options.accountId,
61
+ serviceBindingWorker,
62
+ applicationWorker: {
63
+ cache: { enabled: false },
64
+ env: {
65
+ [RESPONSE_STORE_BINDING]: bindings.worker({
66
+ worker: serviceBindingWorker,
67
+ exportName: RESPONSE_STORE_ENTRYPOINT
68
+ }),
69
+ [DEFAULT_VERSION_METADATA_BINDING]: bindings.versionMetadata()
70
+ }
71
+ }
72
+ };
73
+ }
74
+ async function createWorkersCacheConfig({ versionMetadataBinding = DEFAULT_VERSION_METADATA_BINDING } = {}) {
75
+ if (versionMetadataBinding.length === 0) throw new TypeError("versionMetadataBinding must be a non-empty string");
76
+ const { bindings, exports } = await import("cf/config");
77
+ return {
78
+ cache: { enabled: false },
79
+ env: { [versionMetadataBinding]: bindings.versionMetadata() },
80
+ exports: {
81
+ VinextCachedResponse: exports.worker({ cache: { enabled: true } }),
82
+ VinextUncachedResponse: exports.worker({ cache: { enabled: false } })
83
+ }
84
+ };
85
+ }
86
+ //#endregion
87
+ export { createWorkersCacheConfig, createWorkersResponseStoreSelfContainedConfig, createWorkersResponseStoreServiceBindingConfig };
@@ -36,7 +36,7 @@ export type KvDataAdapterOptions = {
36
36
  * // wrangler.jsonc
37
37
  * {
38
38
  * "kv_namespaces": [
39
- * { "binding": "VINEXT_KV_CACHE", "id": "<your-kv-namespace-id>" }
39
+ * { "binding": "VINEXT_KV_CACHE" }
40
40
  * ]
41
41
  * }
42
42
  * ```
@@ -9,7 +9,7 @@ const DEFAULT_KV_DATA_CACHE_BINDING = "VINEXT_KV_CACHE";
9
9
  * // wrangler.jsonc
10
10
  * {
11
11
  * "kv_namespaces": [
12
- * { "binding": "VINEXT_KV_CACHE", "id": "<your-kv-namespace-id>" }
12
+ * { "binding": "VINEXT_KV_CACHE" }
13
13
  * ]
14
14
  * }
15
15
  * ```
@@ -1,5 +1,5 @@
1
- import { ENTRY_PREFIX, createKvKeySpace } from "./kv-key.js";
2
1
  import { isUnknownRecord, readCacheControlNumberField } from "../utils/cache-control-metadata.js";
2
+ import { ENTRY_PREFIX, createKvKeySpace, resolveKvExpirationTtlSeconds } from "./kv-key.js";
3
3
  import { Buffer } from "node:buffer";
4
4
  import { getRequestExecutionContext } from "vinext/shims/request-context";
5
5
  //#region src/cache/kv-data-adapter.runtime.ts
@@ -20,14 +20,12 @@ import { getRequestExecutionContext } from "vinext/shims/request-context";
20
20
  *
21
21
  * Wrangler config (wrangler.jsonc):
22
22
  *
23
- * { "kv_namespaces": [{ "binding": "VINEXT_KV_CACHE", "id": "<your-kv-namespace-id>" }] }
23
+ * { "kv_namespaces": [{ "binding": "VINEXT_KV_CACHE" }] }
24
24
  */
25
25
  /** Default KV namespace binding name read from the Worker `env`. */
26
26
  const DEFAULT_BINDING = "VINEXT_KV_CACHE";
27
27
  /** Prefix used by revalidatePath for path-based tags. */
28
28
  const PATH_TAG_PREFIX = "_N_T_";
29
- /** Max tag length to prevent KV key abuse. */
30
- const MAX_TAG_LENGTH = 256;
31
29
  /** The runtime rejects a lower `cacheTtl` with "Cache TTL must be at least 30". */
32
30
  const MIN_KV_CACHE_TTL_SECONDS = 30;
33
31
  /** Cloudflare caps a bulk `get()` at 100 keys per call. */
@@ -36,12 +34,12 @@ const KV_BULK_GET_LIMIT = 100;
36
34
  const BASE64_RE = /^[A-Za-z0-9+/]*={0,2}$/;
37
35
  /**
38
36
  * Validate a cache tag. Returns null if invalid.
39
- * Note: `:` is rejected because TAG_PREFIX and ENTRY_PREFIX use `:` as a
40
- * separator — allowing `:` in user tags could cause ambiguous key lookups.
37
+ * Tags may already be header-encoded, so their length is not the raw API tag
38
+ * length. The key builder bounds KV keys and safely handles colon tags.
41
39
  */
42
40
  function validateTag(tag) {
43
- if (typeof tag !== "string" || tag.length === 0 || tag.length > MAX_TAG_LENGTH) return null;
44
- if (/[\x00-\x1f\\:]/.test(tag)) return null;
41
+ if (typeof tag !== "string" || tag.length === 0) return null;
42
+ if (/[\x00-\x1f\\]/.test(tag)) return null;
45
43
  return tag;
46
44
  }
47
45
  function readStringArrayField(ctx, field) {
@@ -112,7 +110,7 @@ var KVCacheHandler = class {
112
110
  this.kv = kvNamespace;
113
111
  this.keySpace = createKvKeySpace(options?.appPrefix);
114
112
  this.ctx = options?.ctx;
115
- this.ttlSeconds = options?.ttlSeconds ?? 2592e3;
113
+ this.ttlSeconds = resolveKvExpirationTtlSeconds(options?.ttlSeconds);
116
114
  this._tagCacheTtl = options?.tagCacheTtlMs ?? 5e3;
117
115
  const entryCacheTtl = options?.entryCacheTtlSeconds;
118
116
  this._entryReadOptions = typeof entryCacheTtl === "number" && Number.isFinite(entryCacheTtl) ? { cacheTtl: Math.max(MIN_KV_CACHE_TTL_SECONDS, Math.floor(entryCacheTtl)) } : void 0;
@@ -280,10 +278,10 @@ var KVCacheHandler = class {
280
278
  if (data && "revalidate" in data && typeof data.revalidate === "number") effectiveRevalidate = data.revalidate;
281
279
  if (effectiveRevalidate === 0) return Promise.resolve();
282
280
  const now = Date.now();
283
- const revalidateAt = typeof effectiveRevalidate === "number" && effectiveRevalidate > 0 ? now + effectiveRevalidate * 1e3 : null;
281
+ const revalidateAt = typeof effectiveRevalidate === "number" && effectiveRevalidate > 0 && Number.isFinite(effectiveRevalidate) ? now + effectiveRevalidate * 1e3 : null;
284
282
  const expireAt = typeof effectiveExpire === "number" && effectiveExpire > 0 ? now + effectiveExpire * 1e3 : null;
285
283
  const cacheControl = typeof effectiveRevalidate === "number" ? {
286
- revalidate: effectiveRevalidate,
284
+ revalidate: Number.isFinite(effectiveRevalidate) ? effectiveRevalidate : false,
287
285
  ...effectiveExpire === void 0 ? {} : { expire: effectiveExpire },
288
286
  ...effectiveStale === void 0 ? {} : { stale: effectiveStale }
289
287
  } : void 0;
@@ -295,7 +293,7 @@ var KVCacheHandler = class {
295
293
  expireAt,
296
294
  cacheControl
297
295
  };
298
- const expirationTtl = revalidateAt !== null ? this.ttlSeconds : void 0;
296
+ const expirationTtl = this.ttlSeconds;
299
297
  const metadata = JSON.stringify({ tags }).length <= 1024 ? { tags } : void 0;
300
298
  return this._put(this._entryKey(key), JSON.stringify(entry), {
301
299
  expirationTtl,
@@ -306,7 +304,7 @@ var KVCacheHandler = class {
306
304
  const tagList = Array.isArray(tags) ? tags : [tags];
307
305
  const now = Date.now();
308
306
  const validTags = tagList.filter((t) => validateTag(t) !== null);
309
- await Promise.all(validTags.map((tag) => this.kv.put(this._tagKey(tag), String(now), { expirationTtl: 2592e3 })));
307
+ await Promise.all(validTags.map((tag) => this.kv.put(this._tagKey(tag), String(now))));
310
308
  const order = ++this._tagCacheOrder;
311
309
  for (const tag of validTags) this._tagCache.set(tag, {
312
310
  timestamp: now,
@@ -423,7 +421,7 @@ function validateCacheEntry(raw) {
423
421
  if (obj.expireAt !== void 0 && obj.expireAt !== null && typeof obj.expireAt !== "number") return null;
424
422
  if (obj.cacheControl !== void 0) {
425
423
  if (!isUnknownRecord(obj.cacheControl)) return null;
426
- if (typeof obj.cacheControl.revalidate !== "number") return null;
424
+ if (typeof obj.cacheControl.revalidate !== "number" && obj.cacheControl.revalidate !== false) return null;
427
425
  if (obj.cacheControl.expire !== void 0 && typeof obj.cacheControl.expire !== "number") return null;
428
426
  if (obj.cacheControl.stale !== void 0 && typeof obj.cacheControl.stale !== "number") return null;
429
427
  }
@@ -432,6 +430,10 @@ function validateCacheEntry(raw) {
432
430
  const value = obj.value;
433
431
  if (typeof value.kind !== "string" || !VALID_KINDS.has(value.kind)) return null;
434
432
  }
433
+ if (obj.cacheControl?.revalidate === false) obj.cacheControl = {
434
+ ...obj.cacheControl,
435
+ revalidate: Infinity
436
+ };
435
437
  return raw;
436
438
  }
437
439
  /**
@@ -522,7 +524,7 @@ function safeBase64ToArrayBuffer(base64) {
522
524
  const createKvDataCacheAdapter = ({ env, options }) => {
523
525
  const binding = options?.binding ?? DEFAULT_BINDING;
524
526
  const namespace = env?.[binding];
525
- if (!namespace) throw new Error(`[vinext] The KV data cache adapter requires a \`${binding}\` KV namespace binding.\n Add it to wrangler.jsonc:\n "kv_namespaces": [{ "binding": "${binding}", "id": "<your-kv-namespace-id>" }]`);
527
+ if (!namespace) throw new Error(`[vinext] The KV data cache adapter requires a \`${binding}\` KV namespace binding.\n Add it to wrangler.jsonc:\n "kv_namespaces": [{ "binding": "${binding}" }]`);
526
528
  return new KVCacheHandler(namespace, {
527
529
  appPrefix: options?.appPrefix,
528
530
  ttlSeconds: options?.ttlSeconds,
@@ -1,15 +1,18 @@
1
1
  //#region src/cache/kv-key.d.ts
2
2
  /** Key prefix for cache entries. */
3
3
  export declare const ENTRY_PREFIX = "cache:";
4
+ /**
5
+ * The KV TTL for a configured `ttlSeconds`. A missing, non-finite or
6
+ * nonpositive value falls back to 30 days. Any other value is truncated to
7
+ * whole seconds, as the binding does, and kept within the range KV accepts.
8
+ */
9
+ export declare function resolveKvExpirationTtlSeconds(ttlSeconds: number | undefined): number;
4
10
  export type KvKeySpace = {
5
11
  /** Prefix shared by every cache entry, including entries with hashed logical keys. */
6
12
  entryPrefix: string;
7
13
  entryKey(logicalKey: string): string;
8
14
  tagKey(tag: string): string;
9
15
  };
10
- /**
11
- * Create the deterministic key namespace shared by runtime cache operations
12
- * and deploy-time prerender population.
13
- */
16
+ /** Create the deterministic key namespace for runtime cache operations. */
14
17
  export declare function createKvKeySpace(appPrefix: string | undefined): KvKeySpace;
15
18
  //#endregion
@@ -9,6 +9,20 @@ const KV_KEY_MAX_BYTES = 512;
9
9
  /** Marker for logical keys that must be hashed to fit in KV. */
10
10
  const HASHED_KEY_PREFIX = "__hash:";
11
11
  const KV_KEY_ENCODER = new TextEncoder();
12
+ /** KV rejects an expiration TTL below 60 seconds. */
13
+ const MIN_KV_EXPIRATION_TTL_SECONDS = 60;
14
+ /** The Workers KV binding types `expirationTtl` as a signed 32-bit integer. */
15
+ const MAX_KV_EXPIRATION_TTL_SECONDS = 2147483647;
16
+ /** Default KV TTL for cache entries. */
17
+ const DEFAULT_KV_EXPIRATION_TTL_SECONDS = 2592e3;
18
+ /**
19
+ * The KV TTL for a configured `ttlSeconds`. A missing, non-finite or
20
+ * nonpositive value falls back to 30 days. Any other value is truncated to
21
+ * whole seconds, as the binding does, and kept within the range KV accepts.
22
+ */
23
+ function resolveKvExpirationTtlSeconds(ttlSeconds) {
24
+ return Math.min(MAX_KV_EXPIRATION_TTL_SECONDS, Math.max(MIN_KV_EXPIRATION_TTL_SECONDS, Math.trunc(typeof ttlSeconds === "number" && Number.isFinite(ttlSeconds) && ttlSeconds > 0 ? ttlSeconds : DEFAULT_KV_EXPIRATION_TTL_SECONDS)));
25
+ }
12
26
  function kvKeyByteLength(key) {
13
27
  return KV_KEY_ENCODER.encode(key).length;
14
28
  }
@@ -24,13 +38,10 @@ function normalizeAppPrefix(appPrefix) {
24
38
  }
25
39
  function buildStorageKey(prefix, categoryPrefix, logicalKey) {
26
40
  const key = `${prefix}${categoryPrefix}${logicalKey}`;
27
- if (kvKeyByteLength(key) <= KV_KEY_MAX_BYTES) return key;
41
+ if (!(categoryPrefix === TAG_PREFIX && logicalKey.includes(":")) && kvKeyByteLength(key) <= KV_KEY_MAX_BYTES) return key;
28
42
  return `${prefix}${categoryPrefix}${HASHED_KEY_PREFIX}${fnv1a64(logicalKey)}`;
29
43
  }
30
- /**
31
- * Create the deterministic key namespace shared by runtime cache operations
32
- * and deploy-time prerender population.
33
- */
44
+ /** Create the deterministic key namespace for runtime cache operations. */
34
45
  function createKvKeySpace(appPrefix) {
35
46
  const prefix = normalizeAppPrefix(appPrefix);
36
47
  return {
@@ -40,4 +51,4 @@ function createKvKeySpace(appPrefix) {
40
51
  };
41
52
  }
42
53
  //#endregion
43
- export { ENTRY_PREFIX, createKvKeySpace };
54
+ export { ENTRY_PREFIX, createKvKeySpace, resolveKvExpirationTtlSeconds };
@@ -1,5 +1,12 @@
1
+ import { ResponseStoreLocationHint } from "@cloudflare/workers-response-store";
1
2
  //#region src/cache/response-store-adapter.d.ts
2
3
  export type ResponseStoreAdapterOptions = {
4
+ /**
5
+ * Best-effort location for metadata Durable Objects on first creation.
6
+ * Changing this does not move existing objects and should be treated as a
7
+ * cache-cold deployment change.
8
+ */
9
+ locationHint?: ResponseStoreLocationHint;
3
10
  mode?: "self-contained" | "service-binding";
4
11
  /** Split version-scoped metadata across this many Durable Objects. */
5
12
  shards?: number;
@@ -33,9 +40,7 @@ export declare function responseStoreAdapter(options?: ResponseStoreAdapterOptio
33
40
  warmup: "response-store";
34
41
  };
35
42
  } | {
36
- options: {
37
- shards: number;
38
- };
43
+ options: Pick<ResponseStoreAdapterOptions, "locationHint" | "shards">;
39
44
  adapter: string;
40
45
  output: {
41
46
  entry: string;
@@ -61,9 +66,7 @@ export declare function responseStoreAdapter(options?: ResponseStoreAdapterOptio
61
66
  options?: undefined;
62
67
  adapter: string;
63
68
  } | {
64
- options: {
65
- shards: number;
66
- };
69
+ options: Pick<ResponseStoreAdapterOptions, "locationHint" | "shards">;
67
70
  adapter: string;
68
71
  };
69
72
  };
@@ -1,16 +1,35 @@
1
1
  import { fileURLToPath } from "node:url";
2
2
  //#region src/cache/response-store-adapter.ts
3
3
  const CLOUDFLARE_WORKER_ENTRY_ID = "virtual:cloudflare/worker-entry";
4
+ const RESPONSE_STORE_LOCATION_HINTS = {
5
+ afr: true,
6
+ apac: true,
7
+ "apac-ne": true,
8
+ "apac-se": true,
9
+ eeur: true,
10
+ enam: true,
11
+ me: true,
12
+ oc: true,
13
+ sam: true,
14
+ weur: true,
15
+ wnam: true
16
+ };
4
17
  /**
5
18
  * Use Workers Response Store for both vinext response-stage and data caching.
6
19
  * Service-binding mode keeps storage in a separate cache Worker. Self-contained
7
20
  * mode keeps the same API and loopback in the application Worker.
8
21
  */
9
22
  function responseStoreAdapter(options = {}) {
23
+ const locationHint = options.locationHint;
24
+ if (locationHint !== void 0 && (typeof locationHint !== "string" || !Object.hasOwn(RESPONSE_STORE_LOCATION_HINTS, locationHint))) throw new TypeError("Workers Response Store locationHint is not supported by Cloudflare");
10
25
  const mode = options.mode ?? "service-binding";
11
26
  if (mode !== "service-binding" && mode !== "self-contained") throw new Error(`Unknown Workers Response Store mode: ${String(mode)}`);
12
27
  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 } };
28
+ const configuredOptions = {
29
+ ...locationHint === void 0 ? {} : { locationHint },
30
+ ...options.shards === void 0 ? {} : { shards: options.shards }
31
+ };
32
+ const runtimeOptions = Object.keys(configuredOptions).length === 0 ? {} : { options: configuredOptions };
14
33
  const workerEntry = fileURLToPath(import.meta.resolve(mode === "self-contained" ? "./response-store-adapter.self-contained.worker.js" : "./response-store-adapter.service-binding.worker.js"));
15
34
  const entrypoints = mode === "self-contained" ? "CacheMetadata, ResponseStoreBinding, ResponseStoreRevalidator" : "ResponseStoreClient, ResponseStoreRevalidator";
16
35
  return {