@vinext/cloudflare 1.0.0-beta.0 → 1.0.0-beta.10
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 +94 -1
- package/dist/cache/cdn-adapter-config.d.ts +30 -0
- package/dist/cache/cdn-adapter-config.js +87 -0
- package/dist/cache/cdn-adapter.d.ts +47 -15
- package/dist/cache/cdn-adapter.js +49 -13
- package/dist/cache/cdn-adapter.runtime.d.ts +26 -4
- package/dist/cache/cdn-adapter.runtime.js +150 -37
- package/dist/cache/cdn-adapter.worker.d.ts +31 -0
- package/dist/cache/cdn-adapter.worker.js +373 -0
- package/dist/cache/cdn-build-id.d.ts +6 -0
- package/dist/cache/cdn-build-id.js +9 -0
- package/dist/cache/kv-data-adapter.d.ts +32 -8
- package/dist/cache/kv-data-adapter.js +5 -1
- package/dist/cache/kv-data-adapter.runtime.d.ts +45 -13
- package/dist/cache/kv-data-adapter.runtime.js +133 -53
- package/dist/cache/kv-key.d.ts +15 -0
- package/dist/cache/kv-key.js +43 -0
- package/dist/cache/response-store-adapter.d.ts +73 -0
- package/dist/cache/response-store-adapter.js +64 -0
- package/dist/cache/response-store-adapter.self-contained.worker.d.ts +18 -0
- package/dist/cache/response-store-adapter.self-contained.worker.js +12 -0
- package/dist/cache/response-store-adapter.service-binding.worker.d.ts +16 -0
- package/dist/cache/response-store-adapter.service-binding.worker.js +11 -0
- package/dist/cache/response-store-adapter.worker.d.ts +14 -0
- package/dist/cache/response-store-adapter.worker.js +278 -0
- package/dist/cache/response-store-cdn.runtime.d.ts +5 -0
- package/dist/cache/response-store-cdn.runtime.js +41 -0
- package/dist/cache/response-store-data.runtime.d.ts +34 -0
- package/dist/cache/response-store-data.runtime.js +300 -0
- package/dist/cacheability-artifact.d.ts +9 -0
- package/dist/cacheability-artifact.js +191 -0
- package/dist/cacheability-manifest-limits.d.ts +4 -0
- package/dist/cacheability-manifest-limits.js +7 -0
- package/dist/cacheability-probe.d.ts +49 -0
- package/dist/cacheability-probe.js +660 -0
- package/dist/cdn-warm.d.ts +110 -12
- package/dist/cdn-warm.js +880 -82
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +15 -1
- package/dist/deploy-config.d.ts +13 -11
- package/dist/deploy-config.js +21 -3
- package/dist/deploy-help.d.ts +3 -4
- package/dist/deploy-help.js +61 -18
- package/dist/deploy.d.ts +121 -41
- package/dist/deploy.js +787 -121
- package/dist/images/images-optimizer.d.ts +5 -5
- package/dist/images/images-optimizer.runtime.d.ts +1 -5
- package/dist/prerender-kv-populate.d.ts +3 -4
- package/dist/prerender-kv-populate.js +51 -18
- package/dist/tpr.d.ts +32 -70
- package/dist/tpr.js +45 -602
- package/dist/utils/cache-control-metadata.d.ts +3 -4
- package/dist/version-deploy.d.ts +19 -17
- package/dist/version-deploy.js +32 -13
- package/dist/version-headers.d.ts +2 -0
- package/dist/version-headers.js +2 -0
- package/dist/worker-deployment-url.d.ts +9 -0
- package/dist/worker-deployment-url.js +45 -0
- package/dist/workers-dev-url.d.ts +2 -3
- package/dist/wrangler-config.d.ts +19 -0
- package/dist/wrangler-config.js +294 -0
- package/dist/wrangler-version-metadata.d.ts +7 -0
- package/dist/wrangler-version-metadata.js +9 -0
- package/package.json +14 -10
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ENTRY_PREFIX, createKvKeySpace } from "./kv-key.js";
|
|
1
2
|
import { isUnknownRecord, readCacheControlNumberField } from "../utils/cache-control-metadata.js";
|
|
2
3
|
import { Buffer } from "node:buffer";
|
|
3
4
|
import { getRequestExecutionContext } from "vinext/shims/request-context";
|
|
@@ -23,14 +24,14 @@ import { getRequestExecutionContext } from "vinext/shims/request-context";
|
|
|
23
24
|
*/
|
|
24
25
|
/** Default KV namespace binding name read from the Worker `env`. */
|
|
25
26
|
const DEFAULT_BINDING = "VINEXT_KV_CACHE";
|
|
26
|
-
/** Key prefix for tag invalidation timestamps. */
|
|
27
|
-
const TAG_PREFIX = "__tag:";
|
|
28
|
-
/** Key prefix for cache entries. */
|
|
29
|
-
const ENTRY_PREFIX = "cache:";
|
|
30
27
|
/** Prefix used by revalidatePath for path-based tags. */
|
|
31
28
|
const PATH_TAG_PREFIX = "_N_T_";
|
|
32
29
|
/** Max tag length to prevent KV key abuse. */
|
|
33
30
|
const MAX_TAG_LENGTH = 256;
|
|
31
|
+
/** The runtime rejects a lower `cacheTtl` with "Cache TTL must be at least 30". */
|
|
32
|
+
const MIN_KV_CACHE_TTL_SECONDS = 30;
|
|
33
|
+
/** Cloudflare caps a bulk `get()` at 100 keys per call. */
|
|
34
|
+
const KV_BULK_GET_LIMIT = 100;
|
|
34
35
|
/** Matches a valid base64 string (standard alphabet with optional padding). */
|
|
35
36
|
const BASE64_RE = /^[A-Za-z0-9+/]*={0,2}$/;
|
|
36
37
|
/**
|
|
@@ -96,53 +97,92 @@ function isPathChildOf(path, prefix) {
|
|
|
96
97
|
*/
|
|
97
98
|
var KVCacheHandler = class {
|
|
98
99
|
kv;
|
|
99
|
-
|
|
100
|
+
keySpace;
|
|
100
101
|
ctx;
|
|
101
102
|
ttlSeconds;
|
|
102
103
|
/** Local in-memory cache for tag invalidation timestamps. Avoids redundant KV reads. */
|
|
103
104
|
_tagCache = /* @__PURE__ */ new Map();
|
|
105
|
+
/** Monotonic ordering for concurrent tag-cache fills and local invalidations. */
|
|
106
|
+
_tagCacheOrder = 0;
|
|
104
107
|
/** TTL (ms) for local tag cache entries. After this, re-fetch from KV. */
|
|
105
108
|
_tagCacheTtl;
|
|
109
|
+
/** Read options for entry keys only. `undefined` keeps the KV default cacheTtl. */
|
|
110
|
+
_entryReadOptions;
|
|
106
111
|
constructor(kvNamespace, options) {
|
|
107
112
|
this.kv = kvNamespace;
|
|
108
|
-
this.
|
|
113
|
+
this.keySpace = createKvKeySpace(options?.appPrefix);
|
|
109
114
|
this.ctx = options?.ctx;
|
|
110
|
-
this.ttlSeconds = options?.ttlSeconds ??
|
|
115
|
+
this.ttlSeconds = options?.ttlSeconds ?? 2592e3;
|
|
111
116
|
this._tagCacheTtl = options?.tagCacheTtlMs ?? 5e3;
|
|
117
|
+
const entryCacheTtl = options?.entryCacheTtlSeconds;
|
|
118
|
+
this._entryReadOptions = typeof entryCacheTtl === "number" && Number.isFinite(entryCacheTtl) ? { cacheTtl: Math.max(MIN_KV_CACHE_TTL_SECONDS, Math.floor(entryCacheTtl)) } : void 0;
|
|
119
|
+
}
|
|
120
|
+
_entryKey(key) {
|
|
121
|
+
return this.keySpace.entryKey(key);
|
|
122
|
+
}
|
|
123
|
+
_tagKey(tag) {
|
|
124
|
+
return this.keySpace.tagKey(tag);
|
|
112
125
|
}
|
|
113
126
|
async get(key, _ctx) {
|
|
114
|
-
const kvKey = this.
|
|
115
|
-
const
|
|
127
|
+
const kvKey = this._entryKey(key);
|
|
128
|
+
const softTags = validUniqueTags(readStringArrayField(_ctx, "softTags"));
|
|
129
|
+
const entryRead = this._entryReadOptions ? this.kv.get(kvKey, this._entryReadOptions) : this.kv.get(kvKey);
|
|
130
|
+
let softTagCache = this._tagCache;
|
|
131
|
+
const softTagPrime = this._primeTagCache(softTags);
|
|
132
|
+
if (softTags.length > 0) {
|
|
133
|
+
const backgroundPrime = softTagPrime.catch(() => {});
|
|
134
|
+
const ctx = getRequestExecutionContext() ?? this.ctx;
|
|
135
|
+
if (ctx) ctx.waitUntil(backgroundPrime);
|
|
136
|
+
}
|
|
137
|
+
const raw = await entryRead;
|
|
116
138
|
if (!raw) return null;
|
|
117
139
|
let parsed;
|
|
118
140
|
try {
|
|
119
141
|
parsed = JSON.parse(raw);
|
|
120
142
|
} catch {
|
|
121
|
-
this.
|
|
143
|
+
this._deleteEntryReadInBackground(kvKey);
|
|
122
144
|
return null;
|
|
123
145
|
}
|
|
124
146
|
const entry = validateCacheEntry(parsed);
|
|
125
147
|
if (!entry) {
|
|
126
148
|
console.error("[vinext] Invalid cache entry shape for key:", key);
|
|
127
|
-
this.
|
|
149
|
+
this._deleteEntryReadInBackground(kvKey);
|
|
128
150
|
return null;
|
|
129
151
|
}
|
|
130
152
|
let restoredValue = null;
|
|
131
153
|
if (entry.value) {
|
|
132
154
|
restoredValue = restoreArrayBuffers(entry.value);
|
|
133
155
|
if (!restoredValue) {
|
|
134
|
-
this.
|
|
156
|
+
this._deleteEntryReadInBackground(kvKey);
|
|
135
157
|
return null;
|
|
136
158
|
}
|
|
137
159
|
}
|
|
138
|
-
|
|
139
|
-
|
|
160
|
+
const entryTags = validUniqueTags(entry.tags);
|
|
161
|
+
if (this._hasRevalidatedTag(entryTags, entry.lastModified, true)) {
|
|
162
|
+
this._deleteEntryReadInBackground(kvKey);
|
|
140
163
|
return null;
|
|
141
164
|
}
|
|
142
|
-
|
|
143
|
-
|
|
165
|
+
await softTagPrime;
|
|
166
|
+
while (softTagCache !== this._tagCache) {
|
|
167
|
+
softTagCache = this._tagCache;
|
|
168
|
+
await this._primeTagCache(softTags);
|
|
169
|
+
}
|
|
170
|
+
let invalidated = this._hasRevalidatedTag(entryTags, entry.lastModified, true);
|
|
171
|
+
if (!invalidated) {
|
|
172
|
+
await this._primeTagCache(entryTags);
|
|
173
|
+
while (softTagCache !== this._tagCache) {
|
|
174
|
+
softTagCache = this._tagCache;
|
|
175
|
+
await this._primeTagCache([.../* @__PURE__ */ new Set([...softTags, ...entryTags])]);
|
|
176
|
+
}
|
|
177
|
+
invalidated = this._hasRevalidatedTag(entryTags, entry.lastModified);
|
|
178
|
+
}
|
|
179
|
+
if (invalidated) {
|
|
180
|
+
this._deleteEntryReadInBackground(kvKey);
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
if (this._hasRevalidatedTag(softTags, entry.lastModified)) return null;
|
|
144
184
|
if (entry.expireAt !== void 0 && entry.expireAt !== null && Date.now() > entry.expireAt) {
|
|
145
|
-
this.
|
|
185
|
+
this._deleteEntryReadInBackground(kvKey);
|
|
146
186
|
return null;
|
|
147
187
|
}
|
|
148
188
|
const now = Date.now();
|
|
@@ -161,37 +201,63 @@ var KVCacheHandler = class {
|
|
|
161
201
|
};
|
|
162
202
|
}
|
|
163
203
|
/**
|
|
164
|
-
*
|
|
165
|
-
*
|
|
204
|
+
* Load the invalidation markers these tags still need into the local cache.
|
|
205
|
+
* Tags a recent read already cached cost nothing, so the entry-tag batch only
|
|
206
|
+
* pays for what the soft-tag batch did not already fetch.
|
|
166
207
|
*/
|
|
167
|
-
async
|
|
168
|
-
if (tags.length === 0) return
|
|
208
|
+
async _primeTagCache(tags) {
|
|
209
|
+
if (tags.length === 0) return;
|
|
210
|
+
const tagCache = this._tagCache;
|
|
169
211
|
const now = Date.now();
|
|
170
|
-
const
|
|
212
|
+
const missing = tags.filter((tag) => {
|
|
213
|
+
const cached = tagCache.get(tag);
|
|
214
|
+
if (cached && now - cached.fetchedAt < this._tagCacheTtl) return false;
|
|
215
|
+
if (cached) tagCache.delete(tag);
|
|
216
|
+
return true;
|
|
217
|
+
});
|
|
218
|
+
if (missing.length === 0) return;
|
|
219
|
+
const order = ++this._tagCacheOrder;
|
|
220
|
+
const markers = await this._readTagMarkers(missing.map((tag) => this._tagKey(tag)));
|
|
221
|
+
for (const tag of missing) {
|
|
222
|
+
const current = tagCache.get(tag);
|
|
223
|
+
if (current && current.order >= order) continue;
|
|
224
|
+
const marker = markers.get(this._tagKey(tag));
|
|
225
|
+
tagCache.set(tag, {
|
|
226
|
+
timestamp: marker ? Number(marker) : 0,
|
|
227
|
+
fetchedAt: now,
|
|
228
|
+
order
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Read every marker key, one round trip per 100-key chunk.
|
|
234
|
+
*
|
|
235
|
+
* Markers never take the entry `cacheTtl`: `revalidateTag()` writes them, so
|
|
236
|
+
* a longer colo cache would hide a publish from that colo for its span. They
|
|
237
|
+
* keep KV's own default cacheTtl of 60 s.
|
|
238
|
+
*/
|
|
239
|
+
async _readTagMarkers(keys) {
|
|
240
|
+
if (keys.length === 1) return /* @__PURE__ */ new Map([[keys[0], await this.kv.get(keys[0])]]);
|
|
241
|
+
const chunks = [];
|
|
242
|
+
for (let i = 0; i < keys.length; i += KV_BULK_GET_LIMIT) chunks.push(keys.slice(i, i + KV_BULK_GET_LIMIT));
|
|
243
|
+
const results = await Promise.all(chunks.map((chunk) => this.kv.get(chunk)));
|
|
244
|
+
const markers = /* @__PURE__ */ new Map();
|
|
245
|
+
for (const result of results) for (const [key, value] of result) markers.set(key, value);
|
|
246
|
+
return markers;
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Report whether any tag has an invalidation marker at or after `lastModified`.
|
|
250
|
+
* Call `_primeTagCache` for the same tags first — a tag with no cache entry
|
|
251
|
+
* counts as never invalidated. Pass `requireFresh` to call it before a prime,
|
|
252
|
+
* so an entry past `tagCacheTtlMs` does not answer for a tag it never re-read.
|
|
253
|
+
*/
|
|
254
|
+
_hasRevalidatedTag(tags, lastModified, requireFresh = false) {
|
|
255
|
+
const now = requireFresh ? Date.now() : 0;
|
|
171
256
|
for (const tag of tags) {
|
|
172
257
|
const cached = this._tagCache.get(tag);
|
|
173
|
-
if (cached
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
if (cached) this._tagCache.delete(tag);
|
|
177
|
-
uncachedTags.push(tag);
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
if (uncachedTags.length > 0) {
|
|
181
|
-
const tagResults = await Promise.all(uncachedTags.map((tag) => this.kv.get(this.prefix + TAG_PREFIX + tag)));
|
|
182
|
-
for (let i = 0; i < uncachedTags.length; i++) {
|
|
183
|
-
const tagTime = tagResults[i];
|
|
184
|
-
const tagTimestamp = tagTime ? Number(tagTime) : 0;
|
|
185
|
-
this._tagCache.set(uncachedTags[i], {
|
|
186
|
-
timestamp: tagTimestamp,
|
|
187
|
-
fetchedAt: now
|
|
188
|
-
});
|
|
189
|
-
}
|
|
190
|
-
for (const tag of uncachedTags) {
|
|
191
|
-
const cached = this._tagCache.get(tag);
|
|
192
|
-
if (!cached || cached.timestamp === 0) continue;
|
|
193
|
-
if (Number.isNaN(cached.timestamp) || cached.timestamp >= lastModified) return true;
|
|
194
|
-
}
|
|
258
|
+
if (!cached || cached.timestamp === 0) continue;
|
|
259
|
+
if (requireFresh && now - cached.fetchedAt >= this._tagCacheTtl) continue;
|
|
260
|
+
if (Number.isNaN(cached.timestamp) || cached.timestamp >= lastModified) return true;
|
|
195
261
|
}
|
|
196
262
|
return false;
|
|
197
263
|
}
|
|
@@ -210,14 +276,16 @@ var KVCacheHandler = class {
|
|
|
210
276
|
let effectiveExpire;
|
|
211
277
|
effectiveRevalidate = readCacheControlNumberField(ctx, "revalidate");
|
|
212
278
|
effectiveExpire = readCacheControlNumberField(ctx, "expire");
|
|
279
|
+
const effectiveStale = readCacheControlNumberField(ctx, "stale");
|
|
213
280
|
if (data && "revalidate" in data && typeof data.revalidate === "number") effectiveRevalidate = data.revalidate;
|
|
214
281
|
if (effectiveRevalidate === 0) return Promise.resolve();
|
|
215
282
|
const now = Date.now();
|
|
216
283
|
const revalidateAt = typeof effectiveRevalidate === "number" && effectiveRevalidate > 0 ? now + effectiveRevalidate * 1e3 : null;
|
|
217
284
|
const expireAt = typeof effectiveExpire === "number" && effectiveExpire > 0 ? now + effectiveExpire * 1e3 : null;
|
|
218
|
-
const cacheControl = typeof effectiveRevalidate === "number" ?
|
|
285
|
+
const cacheControl = typeof effectiveRevalidate === "number" ? {
|
|
219
286
|
revalidate: effectiveRevalidate,
|
|
220
|
-
expire: effectiveExpire
|
|
287
|
+
...effectiveExpire === void 0 ? {} : { expire: effectiveExpire },
|
|
288
|
+
...effectiveStale === void 0 ? {} : { stale: effectiveStale }
|
|
221
289
|
} : void 0;
|
|
222
290
|
const entry = {
|
|
223
291
|
value: data ? serializeForJSON(data) : null,
|
|
@@ -229,7 +297,7 @@ var KVCacheHandler = class {
|
|
|
229
297
|
};
|
|
230
298
|
const expirationTtl = revalidateAt !== null ? this.ttlSeconds : void 0;
|
|
231
299
|
const metadata = JSON.stringify({ tags }).length <= 1024 ? { tags } : void 0;
|
|
232
|
-
return this._put(this.
|
|
300
|
+
return this._put(this._entryKey(key), JSON.stringify(entry), {
|
|
233
301
|
expirationTtl,
|
|
234
302
|
metadata
|
|
235
303
|
});
|
|
@@ -238,10 +306,12 @@ var KVCacheHandler = class {
|
|
|
238
306
|
const tagList = Array.isArray(tags) ? tags : [tags];
|
|
239
307
|
const now = Date.now();
|
|
240
308
|
const validTags = tagList.filter((t) => validateTag(t) !== null);
|
|
241
|
-
await Promise.all(validTags.map((tag) => this.kv.put(this.
|
|
309
|
+
await Promise.all(validTags.map((tag) => this.kv.put(this._tagKey(tag), String(now), { expirationTtl: 2592e3 })));
|
|
310
|
+
const order = ++this._tagCacheOrder;
|
|
242
311
|
for (const tag of validTags) this._tagCache.set(tag, {
|
|
243
312
|
timestamp: now,
|
|
244
|
-
fetchedAt: now
|
|
313
|
+
fetchedAt: now,
|
|
314
|
+
order
|
|
245
315
|
});
|
|
246
316
|
}
|
|
247
317
|
/**
|
|
@@ -263,7 +333,7 @@ var KVCacheHandler = class {
|
|
|
263
333
|
async revalidateByPathPrefix(pathPrefix) {
|
|
264
334
|
const tagsToInvalidate = /* @__PURE__ */ new Set();
|
|
265
335
|
let cursor;
|
|
266
|
-
const listPrefix = this.
|
|
336
|
+
const listPrefix = this.keySpace.entryPrefix;
|
|
267
337
|
do {
|
|
268
338
|
const page = await this.kv.list({
|
|
269
339
|
prefix: listPrefix,
|
|
@@ -297,7 +367,15 @@ var KVCacheHandler = class {
|
|
|
297
367
|
* fresh KVCacheHandler per request or invoke this method explicitly.
|
|
298
368
|
*/
|
|
299
369
|
resetRequestCache() {
|
|
300
|
-
this._tagCache
|
|
370
|
+
this._tagCache = /* @__PURE__ */ new Map();
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* Clean up a bad entry only when this read cannot be an explicitly cached
|
|
374
|
+
* older version of the shared KV value. KV has no conditional delete, so a
|
|
375
|
+
* cached read must remain a non-destructive miss.
|
|
376
|
+
*/
|
|
377
|
+
_deleteEntryReadInBackground(kvKey) {
|
|
378
|
+
if (!this._entryReadOptions) this._deleteInBackground(kvKey);
|
|
301
379
|
}
|
|
302
380
|
/**
|
|
303
381
|
* Fire a KV delete in the background.
|
|
@@ -324,7 +402,7 @@ var KVCacheHandler = class {
|
|
|
324
402
|
return promise;
|
|
325
403
|
}
|
|
326
404
|
};
|
|
327
|
-
const VALID_KINDS = new Set([
|
|
405
|
+
const VALID_KINDS = /* @__PURE__ */ new Set([
|
|
328
406
|
"FETCH",
|
|
329
407
|
"APP_PAGE",
|
|
330
408
|
"PAGES",
|
|
@@ -347,6 +425,7 @@ function validateCacheEntry(raw) {
|
|
|
347
425
|
if (!isUnknownRecord(obj.cacheControl)) return null;
|
|
348
426
|
if (typeof obj.cacheControl.revalidate !== "number") return null;
|
|
349
427
|
if (obj.cacheControl.expire !== void 0 && typeof obj.cacheControl.expire !== "number") return null;
|
|
428
|
+
if (obj.cacheControl.stale !== void 0 && typeof obj.cacheControl.stale !== "number") return null;
|
|
350
429
|
}
|
|
351
430
|
if (obj.value !== null) {
|
|
352
431
|
if (!obj.value || typeof obj.value !== "object") return null;
|
|
@@ -447,7 +526,8 @@ const createKvDataCacheAdapter = ({ env, options }) => {
|
|
|
447
526
|
return new KVCacheHandler(namespace, {
|
|
448
527
|
appPrefix: options?.appPrefix,
|
|
449
528
|
ttlSeconds: options?.ttlSeconds,
|
|
450
|
-
tagCacheTtlMs: options?.tagCacheTtlMs
|
|
529
|
+
tagCacheTtlMs: options?.tagCacheTtlMs,
|
|
530
|
+
entryCacheTtlSeconds: options?.entryCacheTtlSeconds
|
|
451
531
|
});
|
|
452
532
|
};
|
|
453
533
|
//#endregion
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
//#region src/cache/kv-key.d.ts
|
|
2
|
+
/** Key prefix for cache entries. */
|
|
3
|
+
export declare const ENTRY_PREFIX = "cache:";
|
|
4
|
+
export type KvKeySpace = {
|
|
5
|
+
/** Prefix shared by every cache entry, including entries with hashed logical keys. */
|
|
6
|
+
entryPrefix: string;
|
|
7
|
+
entryKey(logicalKey: string): string;
|
|
8
|
+
tagKey(tag: string): string;
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Create the deterministic key namespace shared by runtime cache operations
|
|
12
|
+
* and deploy-time prerender population.
|
|
13
|
+
*/
|
|
14
|
+
export declare function createKvKeySpace(appPrefix: string | undefined): KvKeySpace;
|
|
15
|
+
//#endregion
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { fnv1a64 } from "vinext/internal/utils/hash";
|
|
2
|
+
//#region src/cache/kv-key.ts
|
|
3
|
+
/** Key prefix for tag invalidation timestamps. */
|
|
4
|
+
const TAG_PREFIX = "__tag:";
|
|
5
|
+
/** Key prefix for cache entries. */
|
|
6
|
+
const ENTRY_PREFIX = "cache:";
|
|
7
|
+
/** Cloudflare KV's maximum UTF-8 encoded key length. */
|
|
8
|
+
const KV_KEY_MAX_BYTES = 512;
|
|
9
|
+
/** Marker for logical keys that must be hashed to fit in KV. */
|
|
10
|
+
const HASHED_KEY_PREFIX = "__hash:";
|
|
11
|
+
const KV_KEY_ENCODER = new TextEncoder();
|
|
12
|
+
function kvKeyByteLength(key) {
|
|
13
|
+
return KV_KEY_ENCODER.encode(key).length;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Keep short app prefixes readable, but bound the namespace portion so a
|
|
17
|
+
* hashed entry or tag key is always able to fit within Cloudflare KV's limit.
|
|
18
|
+
*/
|
|
19
|
+
function normalizeAppPrefix(appPrefix) {
|
|
20
|
+
if (!appPrefix) return "";
|
|
21
|
+
const prefix = `${appPrefix}:`;
|
|
22
|
+
if (kvKeyByteLength(`${prefix}cache:${HASHED_KEY_PREFIX}${fnv1a64("")}`) <= KV_KEY_MAX_BYTES) return prefix;
|
|
23
|
+
return `__app:${fnv1a64(appPrefix)}:`;
|
|
24
|
+
}
|
|
25
|
+
function buildStorageKey(prefix, categoryPrefix, logicalKey) {
|
|
26
|
+
const key = `${prefix}${categoryPrefix}${logicalKey}`;
|
|
27
|
+
if (kvKeyByteLength(key) <= KV_KEY_MAX_BYTES) return key;
|
|
28
|
+
return `${prefix}${categoryPrefix}${HASHED_KEY_PREFIX}${fnv1a64(logicalKey)}`;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Create the deterministic key namespace shared by runtime cache operations
|
|
32
|
+
* and deploy-time prerender population.
|
|
33
|
+
*/
|
|
34
|
+
function createKvKeySpace(appPrefix) {
|
|
35
|
+
const prefix = normalizeAppPrefix(appPrefix);
|
|
36
|
+
return {
|
|
37
|
+
entryPrefix: `${prefix}${ENTRY_PREFIX}`,
|
|
38
|
+
entryKey: (logicalKey) => buildStorageKey(prefix, ENTRY_PREFIX, logicalKey),
|
|
39
|
+
tagKey: (tag) => buildStorageKey(prefix, TAG_PREFIX, tag)
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
//#endregion
|
|
43
|
+
export { ENTRY_PREFIX, createKvKeySpace };
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { ResponseStoreLocationHint } from "@cloudflare/workers-response-store";
|
|
2
|
+
//#region src/cache/response-store-adapter.d.ts
|
|
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;
|
|
10
|
+
mode?: "self-contained" | "service-binding";
|
|
11
|
+
/** Split version-scoped metadata across this many Durable Objects. */
|
|
12
|
+
shards?: number;
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Use Workers Response Store for both vinext response-stage and data caching.
|
|
16
|
+
* Service-binding mode keeps storage in a separate cache Worker. Self-contained
|
|
17
|
+
* mode keeps the same API and loopback in the application Worker.
|
|
18
|
+
*/
|
|
19
|
+
export declare function responseStoreAdapter(options?: ResponseStoreAdapterOptions): {
|
|
20
|
+
cdn: {
|
|
21
|
+
options?: undefined;
|
|
22
|
+
adapter: string;
|
|
23
|
+
output: {
|
|
24
|
+
entry: string;
|
|
25
|
+
matchesBuild({ plugins }: {
|
|
26
|
+
plugins: readonly {
|
|
27
|
+
name?: string;
|
|
28
|
+
}[];
|
|
29
|
+
}): boolean;
|
|
30
|
+
transformHostEntry({ code, id }: {
|
|
31
|
+
code: string;
|
|
32
|
+
id: string;
|
|
33
|
+
}): string | null;
|
|
34
|
+
type: "multi-stage";
|
|
35
|
+
};
|
|
36
|
+
capabilities: {
|
|
37
|
+
buildIdentity: "response-header";
|
|
38
|
+
isResponsePolicyHeader: (name: string) => boolean;
|
|
39
|
+
requestRouting: "uncached-stage";
|
|
40
|
+
warmup: "response-store";
|
|
41
|
+
};
|
|
42
|
+
} | {
|
|
43
|
+
options: Pick<ResponseStoreAdapterOptions, "locationHint" | "shards">;
|
|
44
|
+
adapter: string;
|
|
45
|
+
output: {
|
|
46
|
+
entry: string;
|
|
47
|
+
matchesBuild({ plugins }: {
|
|
48
|
+
plugins: readonly {
|
|
49
|
+
name?: string;
|
|
50
|
+
}[];
|
|
51
|
+
}): boolean;
|
|
52
|
+
transformHostEntry({ code, id }: {
|
|
53
|
+
code: string;
|
|
54
|
+
id: string;
|
|
55
|
+
}): string | null;
|
|
56
|
+
type: "multi-stage";
|
|
57
|
+
};
|
|
58
|
+
capabilities: {
|
|
59
|
+
buildIdentity: "response-header";
|
|
60
|
+
isResponsePolicyHeader: (name: string) => boolean;
|
|
61
|
+
requestRouting: "uncached-stage";
|
|
62
|
+
warmup: "response-store";
|
|
63
|
+
};
|
|
64
|
+
};
|
|
65
|
+
data: {
|
|
66
|
+
options?: undefined;
|
|
67
|
+
adapter: string;
|
|
68
|
+
} | {
|
|
69
|
+
options: Pick<ResponseStoreAdapterOptions, "locationHint" | "shards">;
|
|
70
|
+
adapter: string;
|
|
71
|
+
};
|
|
72
|
+
};
|
|
73
|
+
//#endregion
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { fileURLToPath } from "node:url";
|
|
2
|
+
//#region src/cache/response-store-adapter.ts
|
|
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
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Use Workers Response Store for both vinext response-stage and data caching.
|
|
19
|
+
* Service-binding mode keeps storage in a separate cache Worker. Self-contained
|
|
20
|
+
* mode keeps the same API and loopback in the application Worker.
|
|
21
|
+
*/
|
|
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");
|
|
25
|
+
const mode = options.mode ?? "service-binding";
|
|
26
|
+
if (mode !== "service-binding" && mode !== "self-contained") throw new Error(`Unknown Workers Response Store mode: ${String(mode)}`);
|
|
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");
|
|
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 };
|
|
33
|
+
const workerEntry = fileURLToPath(import.meta.resolve(mode === "self-contained" ? "./response-store-adapter.self-contained.worker.js" : "./response-store-adapter.service-binding.worker.js"));
|
|
34
|
+
const entrypoints = mode === "self-contained" ? "CacheMetadata, ResponseStoreBinding, ResponseStoreRevalidator" : "ResponseStoreClient, ResponseStoreRevalidator";
|
|
35
|
+
return {
|
|
36
|
+
cdn: {
|
|
37
|
+
adapter: fileURLToPath(import.meta.resolve("./response-store-cdn.runtime.js")),
|
|
38
|
+
...runtimeOptions,
|
|
39
|
+
output: {
|
|
40
|
+
entry: workerEntry,
|
|
41
|
+
matchesBuild({ plugins }) {
|
|
42
|
+
return plugins.some(({ name }) => name === "vite-plugin-cloudflare" || name?.startsWith("vite-plugin-cloudflare:"));
|
|
43
|
+
},
|
|
44
|
+
transformHostEntry({ code, id }) {
|
|
45
|
+
if ((id.charCodeAt(0) === 0 ? id.slice(1) : id) !== CLOUDFLARE_WORKER_ENTRY_ID) return null;
|
|
46
|
+
return `${code}\nexport { ${entrypoints} } from ${JSON.stringify(workerEntry)};\n`;
|
|
47
|
+
},
|
|
48
|
+
type: "multi-stage"
|
|
49
|
+
},
|
|
50
|
+
capabilities: {
|
|
51
|
+
buildIdentity: "response-header",
|
|
52
|
+
isResponsePolicyHeader: (name) => name.toLowerCase() === "cdn-cache-control" || name.toLowerCase() === "cloudflare-cdn-cache-control",
|
|
53
|
+
requestRouting: "uncached-stage",
|
|
54
|
+
warmup: "response-store"
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
data: {
|
|
58
|
+
adapter: fileURLToPath(import.meta.resolve("./response-store-data.runtime.js")),
|
|
59
|
+
...runtimeOptions
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
//#endregion
|
|
64
|
+
export { responseStoreAdapter };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { VinextResponseStoreEnv } from "./response-store-adapter.worker.js";
|
|
2
|
+
import "vinext/internal/server/cloudflare-workers-tracing";
|
|
3
|
+
import { ResponseStoreRevalidatorEntrypoint, WorkersResponseStoreEnv } from "@cloudflare/workers-response-store";
|
|
4
|
+
import { CacheMetadata as CacheMetadata$1, ResponseStoreBinding as ResponseStoreBinding$1 } from "@cloudflare/workers-response-store/service";
|
|
5
|
+
//#region src/cache/response-store-adapter.self-contained.worker.d.ts
|
|
6
|
+
export declare const CacheMetadata: typeof CacheMetadata$1;
|
|
7
|
+
export declare const ResponseStoreBinding: typeof ResponseStoreBinding$1;
|
|
8
|
+
export declare const ResponseStoreRevalidator: ResponseStoreRevalidatorEntrypoint<WorkersResponseStoreEnv>;
|
|
9
|
+
declare const _default: {
|
|
10
|
+
fetch(request: Request, env: VinextResponseStoreEnv, ctx: {
|
|
11
|
+
exports?: Record<string, unknown>;
|
|
12
|
+
passThroughOnException(): void;
|
|
13
|
+
props?: unknown;
|
|
14
|
+
waitUntil(promise: Promise<unknown>): void;
|
|
15
|
+
}): Promise<Response>;
|
|
16
|
+
};
|
|
17
|
+
//#endregion
|
|
18
|
+
export { _default as default };
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { createVinextResponseStoreHandler, createVinextResponseStoreOptions } from "./response-store-adapter.worker.js";
|
|
2
|
+
import "vinext/internal/server/cloudflare-workers-tracing";
|
|
3
|
+
import { createWorkersResponseStore } from "@cloudflare/workers-response-store";
|
|
4
|
+
import { configuredCdnCacheAdapterOptions } from "virtual:vinext-cdn-cache-adapter";
|
|
5
|
+
//#region src/cache/response-store-adapter.self-contained.worker.ts
|
|
6
|
+
const responseStore = createWorkersResponseStore(createVinextResponseStoreOptions(configuredCdnCacheAdapterOptions));
|
|
7
|
+
const CacheMetadata = responseStore.entrypoints.CacheMetadata;
|
|
8
|
+
const ResponseStoreBinding = responseStore.entrypoints.ResponseStoreBinding;
|
|
9
|
+
const ResponseStoreRevalidator = responseStore.entrypoints.ResponseStoreRevalidator;
|
|
10
|
+
var response_store_adapter_self_contained_worker_default = createVinextResponseStoreHandler(responseStore);
|
|
11
|
+
//#endregion
|
|
12
|
+
export { CacheMetadata, ResponseStoreBinding, ResponseStoreRevalidator, response_store_adapter_self_contained_worker_default as default };
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { VinextResponseStoreEnv } from "./response-store-adapter.worker.js";
|
|
2
|
+
import "vinext/internal/server/cloudflare-workers-tracing";
|
|
3
|
+
import { ResponseStoreClientEntrypoint, ResponseStoreRevalidatorEntrypoint, WorkersResponseStoreClientEnv } from "@cloudflare/workers-response-store";
|
|
4
|
+
//#region src/cache/response-store-adapter.service-binding.worker.d.ts
|
|
5
|
+
export declare const ResponseStoreClient: ResponseStoreClientEntrypoint;
|
|
6
|
+
export declare const ResponseStoreRevalidator: ResponseStoreRevalidatorEntrypoint<WorkersResponseStoreClientEnv>;
|
|
7
|
+
declare const _default: {
|
|
8
|
+
fetch(request: Request, env: VinextResponseStoreEnv, ctx: {
|
|
9
|
+
exports?: Record<string, unknown>;
|
|
10
|
+
passThroughOnException(): void;
|
|
11
|
+
props?: unknown;
|
|
12
|
+
waitUntil(promise: Promise<unknown>): void;
|
|
13
|
+
}): Promise<Response>;
|
|
14
|
+
};
|
|
15
|
+
//#endregion
|
|
16
|
+
export { _default as default };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { createVinextResponseStoreHandler, createVinextResponseStoreOptions } from "./response-store-adapter.worker.js";
|
|
2
|
+
import "vinext/internal/server/cloudflare-workers-tracing";
|
|
3
|
+
import { createWorkersResponseStoreClient } from "@cloudflare/workers-response-store";
|
|
4
|
+
import { configuredCdnCacheAdapterOptions } from "virtual:vinext-cdn-cache-adapter";
|
|
5
|
+
//#region src/cache/response-store-adapter.service-binding.worker.ts
|
|
6
|
+
const responseStore = createWorkersResponseStoreClient(createVinextResponseStoreOptions(configuredCdnCacheAdapterOptions));
|
|
7
|
+
const ResponseStoreClient = responseStore.entrypoints.ResponseStoreClient;
|
|
8
|
+
const ResponseStoreRevalidator = responseStore.entrypoints.ResponseStoreRevalidator;
|
|
9
|
+
var response_store_adapter_service_binding_worker_default = createVinextResponseStoreHandler(responseStore);
|
|
10
|
+
//#endregion
|
|
11
|
+
export { ResponseStoreClient, ResponseStoreRevalidator, response_store_adapter_service_binding_worker_default as default };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { WorkersResponseStore, WorkersResponseStoreClientEnv, WorkersResponseStoreEnv, WorkersResponseStoreOptions } from "@cloudflare/workers-response-store";
|
|
2
|
+
//#region src/cache/response-store-adapter.worker.d.ts
|
|
3
|
+
type WorkerExecutionContext = {
|
|
4
|
+
exports?: Record<string, unknown>;
|
|
5
|
+
passThroughOnException(): void;
|
|
6
|
+
props?: unknown;
|
|
7
|
+
waitUntil(promise: Promise<unknown>): void;
|
|
8
|
+
};
|
|
9
|
+
export type VinextResponseStoreEnv = WorkersResponseStoreClientEnv | WorkersResponseStoreEnv;
|
|
10
|
+
export declare function createVinextResponseStoreOptions<Env extends VinextResponseStoreEnv>(configuration?: Record<string, unknown>): WorkersResponseStoreOptions<Env>;
|
|
11
|
+
export declare function createVinextResponseStoreHandler(store: WorkersResponseStore): {
|
|
12
|
+
fetch(request: Request, env: VinextResponseStoreEnv, ctx: WorkerExecutionContext): Promise<Response>;
|
|
13
|
+
};
|
|
14
|
+
//#endregion
|