@vinext/cloudflare 1.2.0 → 1.2.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.
@@ -28,4 +28,5 @@ export declare function finalizeCdnAdapterBuildOutput({ root, outDir, isPrimaryS
28
28
  binding: string;
29
29
  bindingIsExplicit: boolean;
30
30
  }): Promise<void>;
31
- //#endregion
31
+ //#endregion
32
+ export {};
@@ -134,4 +134,5 @@ export declare function createWorkersCacheConfig({ versionMetadataBinding }?: {
134
134
  VinextUncachedResponse: import("cf/config").WorkerEntrypointExport;
135
135
  };
136
136
  }>;
137
- //#endregion
137
+ //#endregion
138
+ export {};
@@ -60,7 +60,7 @@ export declare class KVCacheHandler implements CacheHandler {
60
60
  private keySpace;
61
61
  private ctx;
62
62
  private ttlSeconds;
63
- /** Local in-memory cache for tag invalidation timestamps. Avoids redundant KV reads. */
63
+ /** Local in-memory cache for tag invalidation markers. Avoids redundant KV reads. */
64
64
  private _tagCache;
65
65
  /** Monotonic ordering for concurrent tag-cache fills and local invalidations. */
66
66
  private _tagCacheOrder;
@@ -95,14 +95,18 @@ export declare class KVCacheHandler implements CacheHandler {
95
95
  */
96
96
  private _readTagMarkers;
97
97
  /**
98
- * Report whether any tag has an invalidation marker at or after `lastModified`.
98
+ * Report whether any tag expired an entry last modified at `lastModified`:
99
+ * its marker's expire time is at or after `lastModified` and, for a cacheLife
100
+ * profile's marker, has passed.
99
101
  * Call `_primeTagCache` for the same tags first — a tag with no cache entry
100
102
  * counts as never invalidated. Pass `requireFresh` to call it before a prime,
101
103
  * so an entry past `tagCacheTtlMs` does not answer for a tag it never re-read.
102
104
  */
103
105
  private _hasRevalidatedTag;
106
+ /** Report whether any primed tag was marked stale after `lastModified`. */
107
+ private _hasStaleTag;
104
108
  set(key: string, data: IncrementalCacheValue | null, ctx?: Record<string, unknown>): Promise<void>;
105
- revalidateTag(tags: string | string[], _durations?: {
109
+ revalidateTag(tags: string | string[], durations?: {
106
110
  expire?: number;
107
111
  }): Promise<void>;
108
112
  /**
@@ -1,3 +1,4 @@
1
+ import { isStaleTagInvalidation } from "./workers-cache-invalidation.js";
1
2
  import { isUnknownRecord, readCacheControlNumberField } from "../utils/cache-control-metadata.js";
2
3
  import { ENTRY_PREFIX, createKvKeySpace, resolveKvExpirationTtlSeconds } from "./kv-key.js";
3
4
  import { Buffer } from "node:buffer";
@@ -24,6 +25,23 @@ import { getRequestExecutionContext } from "vinext/shims/request-context";
24
25
  */
25
26
  /** Default KV namespace binding name read from the Worker `env`. */
26
27
  const DEFAULT_BINDING = "VINEXT_KV_CACHE";
28
+ function parseTagMarker(raw) {
29
+ if (raw === null || raw === void 0) return {};
30
+ if (!raw.startsWith("{")) return { expired: /^\d+$/.test(raw) ? Number(raw) : NaN };
31
+ let parsed;
32
+ try {
33
+ parsed = JSON.parse(raw);
34
+ } catch {
35
+ return { expired: NaN };
36
+ }
37
+ if (!isUnknownRecord(parsed)) return { expired: NaN };
38
+ const { stale, expired } = parsed;
39
+ if (stale === void 0 && expired === void 0 || stale !== void 0 && typeof stale !== "number" || expired !== void 0 && typeof expired !== "number") return { expired: NaN };
40
+ return {
41
+ ...stale === void 0 ? {} : { stale },
42
+ ...expired === void 0 ? {} : { expired }
43
+ };
44
+ }
27
45
  /** Prefix used by revalidatePath for path-based tags. */
28
46
  const PATH_TAG_PREFIX = "_N_T_";
29
47
  /** The runtime rejects a lower `cacheTtl` with "Cache TTL must be at least 30". */
@@ -98,7 +116,7 @@ var KVCacheHandler = class {
98
116
  keySpace;
99
117
  ctx;
100
118
  ttlSeconds;
101
- /** Local in-memory cache for tag invalidation timestamps. Avoids redundant KV reads. */
119
+ /** Local in-memory cache for tag invalidation markers. Avoids redundant KV reads. */
102
120
  _tagCache = /* @__PURE__ */ new Map();
103
121
  /** Monotonic ordering for concurrent tag-cache fills and local invalidations. */
104
122
  _tagCacheOrder = 0;
@@ -179,6 +197,7 @@ var KVCacheHandler = class {
179
197
  return null;
180
198
  }
181
199
  if (this._hasRevalidatedTag(softTags, entry.lastModified)) return null;
200
+ const hasStaleTag = this._hasStaleTag([...entryTags, ...softTags], entry.lastModified);
182
201
  if (entry.expireAt !== void 0 && entry.expireAt !== null && Date.now() > entry.expireAt) {
183
202
  this._deleteEntryReadInBackground(kvKey);
184
203
  return null;
@@ -186,7 +205,7 @@ var KVCacheHandler = class {
186
205
  const now = Date.now();
187
206
  const requestedRevalidate = readPositiveNumberField(_ctx, "revalidate");
188
207
  const requestedRevalidateAt = requestedRevalidate === void 0 ? null : entry.lastModified + requestedRevalidate * 1e3;
189
- if (entry.revalidateAt !== null && now > entry.revalidateAt || requestedRevalidateAt !== null && now > requestedRevalidateAt) return {
208
+ if (hasStaleTag || entry.revalidateAt !== null && now > entry.revalidateAt || requestedRevalidateAt !== null && now > requestedRevalidateAt) return {
190
209
  lastModified: entry.lastModified,
191
210
  value: restoredValue,
192
211
  cacheState: "stale",
@@ -221,7 +240,7 @@ var KVCacheHandler = class {
221
240
  if (current && current.order >= order) continue;
222
241
  const marker = markers.get(this._tagKey(tag));
223
242
  tagCache.set(tag, {
224
- timestamp: marker ? Number(marker) : 0,
243
+ marker: parseTagMarker(marker),
225
244
  fetchedAt: now,
226
245
  order
227
246
  });
@@ -244,18 +263,30 @@ var KVCacheHandler = class {
244
263
  return markers;
245
264
  }
246
265
  /**
247
- * Report whether any tag has an invalidation marker at or after `lastModified`.
266
+ * Report whether any tag expired an entry last modified at `lastModified`:
267
+ * its marker's expire time is at or after `lastModified` and, for a cacheLife
268
+ * profile's marker, has passed.
248
269
  * Call `_primeTagCache` for the same tags first — a tag with no cache entry
249
270
  * counts as never invalidated. Pass `requireFresh` to call it before a prime,
250
271
  * so an entry past `tagCacheTtlMs` does not answer for a tag it never re-read.
251
272
  */
252
273
  _hasRevalidatedTag(tags, lastModified, requireFresh = false) {
253
- const now = requireFresh ? Date.now() : 0;
274
+ const now = Date.now();
254
275
  for (const tag of tags) {
255
276
  const cached = this._tagCache.get(tag);
256
- if (!cached || cached.timestamp === 0) continue;
277
+ if (cached?.marker.expired === void 0) continue;
257
278
  if (requireFresh && now - cached.fetchedAt >= this._tagCacheTtl) continue;
258
- if (Number.isNaN(cached.timestamp) || cached.timestamp >= lastModified) return true;
279
+ const { stale, expired } = cached.marker;
280
+ const pending = stale !== void 0 && expired > now;
281
+ if (Number.isNaN(expired) || expired >= lastModified && !pending) return true;
282
+ }
283
+ return false;
284
+ }
285
+ /** Report whether any primed tag was marked stale after `lastModified`. */
286
+ _hasStaleTag(tags, lastModified) {
287
+ for (const tag of tags) {
288
+ const stale = this._tagCache.get(tag)?.marker.stale;
289
+ if (stale !== void 0 && stale > lastModified) return true;
259
290
  }
260
291
  return false;
261
292
  }
@@ -300,16 +331,32 @@ var KVCacheHandler = class {
300
331
  metadata
301
332
  });
302
333
  }
303
- async revalidateTag(tags, _durations) {
334
+ async revalidateTag(tags, durations) {
304
335
  const tagList = Array.isArray(tags) ? tags : [tags];
305
336
  const now = Date.now();
306
337
  const validTags = tagList.filter((t) => validateTag(t) !== null);
307
- await Promise.all(validTags.map((tag) => this.kv.put(this._tagKey(tag), String(now))));
338
+ const markers = await Promise.all(validTags.map(async (tag) => {
339
+ const tagKey = this._tagKey(tag);
340
+ if (!isStaleTagInvalidation(durations)) {
341
+ await this.kv.put(tagKey, String(now));
342
+ return { expired: now };
343
+ }
344
+ const expire = durations?.expire;
345
+ const expired = expire === void 0 ? parseTagMarker(await this.kv.get(tagKey)).expired : now + expire * 1e3;
346
+ const marker = {
347
+ stale: now,
348
+ ...expired !== void 0 && Number.isFinite(expired) ? { expired } : {}
349
+ };
350
+ await this.kv.put(tagKey, JSON.stringify(marker));
351
+ return marker;
352
+ }));
308
353
  const order = ++this._tagCacheOrder;
309
- for (const tag of validTags) this._tagCache.set(tag, {
310
- timestamp: now,
311
- fetchedAt: now,
312
- order
354
+ validTags.forEach((tag, index) => {
355
+ this._tagCache.set(tag, {
356
+ marker: markers[index],
357
+ fetchedAt: now,
358
+ order
359
+ });
313
360
  });
314
361
  }
315
362
  /**
@@ -11,4 +11,5 @@ export declare function createVinextResponseStoreOptions<Env extends VinextRespo
11
11
  export declare function createVinextResponseStoreHandler(store: WorkersResponseStore): {
12
12
  fetch(request: Request, env: VinextResponseStoreEnv, ctx: WorkerExecutionContext): Promise<Response>;
13
13
  };
14
- //#endregion
14
+ //#endregion
15
+ export {};
@@ -6,10 +6,11 @@ import { cacheForRequest } from "vinext/cache";
6
6
  //#region src/cache/response-store-data.runtime.ts
7
7
  const ARRAY_BUFFER_MARKER = "$vinextArrayBuffer";
8
8
  const CACHE_MAX_AGE_SECONDS = 31536e4;
9
- const CACHE_MAX_AGE = `public, max-age=${CACHE_MAX_AGE_SECONDS}`;
10
9
  const DATA_ENTRY_PATH = "__vinext_data";
11
10
  const REPLAYABLE_HEADER = "X-Vinext-Response-Store-Replayable";
12
11
  const MAX_CACHE_TAG_HEADER_BYTES = 16384;
12
+ /** How long past its revalidate a current Store's hit can still be marked fresh: write latency. */
13
+ const STALE_FRESH_HIT_GRACE_MS = 5e3;
13
14
  const DATA_REVALIDATOR_ID = "vinext:data";
14
15
  const CACHE_FUNCTION_REVALIDATOR_ID = "vinext:cache-function";
15
16
  const invocationStorage = new AsyncLocalStorage();
@@ -194,9 +195,9 @@ function deserialize(value) {
194
195
  return entry;
195
196
  }
196
197
  function cachePolicy(revalidate, expire) {
197
- if (revalidate === false || revalidate === void 0) return `public, max-age=${CACHE_MAX_AGE_SECONDS}`;
198
- const staleSeconds = expire === void 0 ? CACHE_MAX_AGE_SECONDS : Math.max(0, expire - revalidate);
199
- return `public, max-age=${Math.max(0, revalidate)}, stale-while-revalidate=${staleSeconds}`;
198
+ if (revalidate === false || revalidate === void 0) return `public, max-age=${expire === void 0 ? CACHE_MAX_AGE_SECONDS : Math.max(1, Math.ceil(expire))}`;
199
+ const maxAge = revalidate > 0 ? Math.max(1, Math.floor(revalidate)) : 0;
200
+ return `public, max-age=${maxAge}, stale-while-revalidate=${expire === void 0 ? CACHE_MAX_AGE_SECONDS : Math.max(0, Math.ceil(expire) - maxAge)}`;
200
201
  }
201
202
  var WorkersResponseStoreCacheHandler = class {
202
203
  store;
@@ -244,12 +245,13 @@ var WorkersResponseStoreCacheHandler = class {
244
245
  }
245
246
  const age = Date.now() - entry.lastModified;
246
247
  const requestedRevalidate = readCacheControlField(context, "revalidate");
248
+ const storedRevalidate = entry.cacheControl?.revalidate;
247
249
  const requestedStale = typeof requestedRevalidate === "number" && requestedRevalidate > 0 && age > requestedRevalidate * 1e3;
248
250
  let cacheState;
249
251
  if (response.headers.get(REPLAYABLE_HEADER) === "1") {
250
252
  if (requestedStale && storeStatus !== "BLOB-STALE") cacheState = "stale";
251
253
  } else if (typeof entry.cacheControl?.expire === "number" && age > entry.cacheControl.expire * 1e3) cacheState = "expired";
252
- else if (requestedStale || storeStatus === "BLOB-STALE" || typeof entry.cacheControl?.revalidate === "number" && entry.cacheControl.revalidate > 0 && age > entry.cacheControl.revalidate * 1e3) cacheState = "stale";
254
+ else if (storeStatus === "BLOB-STALE" || requestedStale && typeof requestedRevalidate === "number" && (typeof storedRevalidate !== "number" || requestedRevalidate < storedRevalidate) || typeof storedRevalidate === "number" && storedRevalidate > 0 && age > storedRevalidate * 1e3 + STALE_FRESH_HIT_GRACE_MS) cacheState = "stale";
253
255
  return {
254
256
  lastModified: entry.lastModified,
255
257
  value: entry.value,
@@ -274,9 +276,11 @@ var WorkersResponseStoreCacheHandler = class {
274
276
  let revalidate = readCacheControlField(context, "revalidate");
275
277
  if (value && "revalidate" in value) revalidate = value.revalidate;
276
278
  if (revalidate === 0) return;
279
+ if (typeof revalidate === "number" && !Number.isFinite(revalidate)) revalidate = false;
277
280
  const rawExpire = readCacheControlField(context, "expire");
278
- const expire = typeof rawExpire === "number" ? rawExpire : void 0;
279
- const stale = readCacheControlField(context, "stale");
281
+ const expire = typeof rawExpire === "number" && Number.isFinite(rawExpire) ? rawExpire : void 0;
282
+ const rawStale = readCacheControlField(context, "stale");
283
+ const stale = typeof rawStale === "number" && Number.isFinite(rawStale) ? rawStale : void 0;
280
284
  const cacheControl = typeof revalidate === "number" || revalidate === false ? {
281
285
  revalidate,
282
286
  ...typeof expire === "number" ? { expire } : {},
@@ -303,7 +307,7 @@ var WorkersResponseStoreCacheHandler = class {
303
307
  lastModified: Date.now(),
304
308
  value
305
309
  }), { headers: {
306
- "Cache-Control": revalidator ? cachePolicy(revalidate, expire) : CACHE_MAX_AGE,
310
+ "Cache-Control": cachePolicy(revalidate, expire),
307
311
  ...tagHeader ? { "Cache-Tag": tagHeader } : {},
308
312
  "Content-Type": "application/json",
309
313
  ...revalidator ? { [REPLAYABLE_HEADER]: "1" } : {}
@@ -26,4 +26,5 @@ export default function createStaticAssetsCacheAdapter({ env, options }?: {
26
26
  env?: Record<string, unknown>;
27
27
  options?: StaticAssetsAdapterOptions;
28
28
  }): CdnCacheAdapter;
29
- //#endregion
29
+ //#endregion
30
+ export {};
@@ -27,4 +27,5 @@ export declare function hasCacheMethod<Name extends CacheMethodName>(value: unkn
27
27
  * `undefined` when the cache supports neither.
28
28
  */
29
29
  export declare function invalidateOrPurge(cache: unknown, options: CacheInvalidationOptions): unknown;
30
- //#endregion
30
+ //#endregion
31
+ export {};
@@ -130,4 +130,5 @@ export declare function waitForCdnWarmTargetReadiness(options: Pick<CdnWarmOptio
130
130
  }): Promise<CdnWarmReadinessResult>;
131
131
  export declare function warmCdnCache(options: CdnWarmOptions): Promise<CdnWarmResult>;
132
132
  export declare function warmCdnCacheFromPrerender(options: PrerenderCdnWarmOptions): Promise<CdnWarmResult>;
133
- //#endregion
133
+ //#endregion
134
+ export {};
package/dist/deploy.d.ts CHANGED
@@ -192,4 +192,5 @@ export declare function resolveDeploymentControlPlaneOptions(options: Deployment
192
192
  export declare function resolveWorkerNameForVersionOverride(config: ParsedWranglerConfig | null, options: Pick<DeployOptions, "preview" | "env" | "name">): string | undefined;
193
193
  export declare function buildVersionOverrideHeaders(workerName: string | undefined, versionId: string): HeadersInit | undefined;
194
194
  export declare function deploy(options: DeployOptions): Promise<void>;
195
- //#endregion
195
+ //#endregion
196
+ export {};
package/dist/deploy.js CHANGED
@@ -1191,6 +1191,7 @@ async function deploy(options) {
1191
1191
  if (deploymentTool === "cf" && options.name) throw new Error("--name is not supported for cloudflare.config.ts projects. Set `name` in cloudflare.config.ts instead.");
1192
1192
  const viteMode = resolveViteBuildMode(deploymentTool, deployEnv);
1193
1193
  const wranglerFallbackEnv = deploymentTool === "cf" ? void 0 : deployEnv;
1194
+ if (!process.env.NODE_ENV) Reflect.set(process.env, "NODE_ENV", "production");
1194
1195
  loadDotenv({
1195
1196
  root,
1196
1197
  mode: viteMode
@@ -1,2 +1 @@
1
- import { VINEXT_EXPECTED_WORKER_VERSION_HEADER } from "vinext/internal/server/headers";
2
- export { VINEXT_EXPECTED_WORKER_VERSION_HEADER };
1
+ export { VINEXT_EXPECTED_WORKER_VERSION_HEADER } from "vinext/internal/server/headers";
@@ -16,4 +16,5 @@ type WranglerEnvironmentConfig = {
16
16
  * Parse the Wrangler fields used by TPR and staged CDN warming.
17
17
  */
18
18
  export declare function parseWranglerConfig(root: string, configPath?: string): WranglerConfig | null;
19
- //#endregion
19
+ //#endregion
20
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vinext/cloudflare",
3
- "version": "1.2.0",
3
+ "version": "1.2.1",
4
4
  "description": "Cloudflare cache and image adapters for vinext.",
5
5
  "homepage": "https://vinext.dev/",
6
6
  "license": "MIT",
@@ -48,21 +48,20 @@
48
48
  }
49
49
  },
50
50
  "dependencies": {
51
- "@cloudflare/workers-response-store": "1.1.2"
51
+ "@cloudflare/workers-response-store": "1.1.3"
52
52
  },
53
53
  "devDependencies": {
54
54
  "@types/node": "^25.9.2",
55
55
  "cf": "1.0.0-beta.1",
56
56
  "miniflare": "5.20260730.0-alpha",
57
- "vite": "npm:@voidzero-dev/vite-plus-core@1.1.0",
58
- "vite-plus": "1.1.0",
59
- "vitest": "5.0.3",
60
- "vinext": "1.3.0"
57
+ "vite": "npm:@voidzero-dev/vite-plus-core@1.3.0",
58
+ "vite-plus": "1.3.0",
59
+ "vinext": "1.3.1"
61
60
  },
62
61
  "peerDependencies": {
63
62
  "@cloudflare/vite-plugin": "*",
64
63
  "cf": "*",
65
- "vinext": "^1.3.0"
64
+ "vinext": "^1.3.1"
66
65
  },
67
66
  "peerDependenciesMeta": {
68
67
  "@cloudflare/vite-plugin": {
@@ -78,6 +77,6 @@
78
77
  "scripts": {
79
78
  "build": "vp pack",
80
79
  "dev": "vp pack --watch",
81
- "test": "vp run vinext#build && vp run @cloudflare/workers-response-store#build && vp run @vinext/cloudflare#build && VINEXT_RESPONSE_STORE_MODE=self-contained VINEXT_RESPONSE_STORE_LOCATION_HINT=weur pnpm --filter response-store-demo run build && VINEXT_RESPONSE_STORE_LOCATION_HINT=wnam pnpm --filter response-store-demo run build && vitest run"
80
+ "test": "vp run vinext#build && vp run @cloudflare/workers-response-store#build && vp run @vinext/cloudflare#build && VINEXT_RESPONSE_STORE_MODE=self-contained VINEXT_RESPONSE_STORE_LOCATION_HINT=weur pnpm --filter response-store-demo run build && VINEXT_RESPONSE_STORE_LOCATION_HINT=wnam pnpm --filter response-store-demo run build && vp test run"
82
81
  }
83
82
  }