@vinext/cloudflare 1.0.0-beta.7 → 1.0.0-beta.8
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 +55 -22
- package/dist/cache/cdn-adapter-config.d.ts +16 -8
- package/dist/cache/cdn-adapter-config.js +57 -17
- package/dist/cache/cdn-adapter.d.ts +25 -24
- package/dist/cache/cdn-adapter.js +24 -20
- package/dist/cache/cdn-adapter.runtime.d.ts +9 -3
- package/dist/cache/cdn-adapter.runtime.js +63 -40
- package/dist/cache/cdn-adapter.worker.d.ts +30 -0
- package/dist/cache/cdn-adapter.worker.js +371 -0
- package/dist/cache/cdn-build-id.d.ts +4 -3
- package/dist/cache/cdn-build-id.js +5 -1
- package/dist/cache/kv-data-adapter.d.ts +21 -5
- package/dist/cache/kv-data-adapter.runtime.d.ts +38 -4
- package/dist/cache/kv-data-adapter.runtime.js +115 -41
- package/dist/cache/kv-key.d.ts +4 -5
- package/dist/cache/response-store-adapter.d.ts +37 -0
- package/dist/cache/response-store-adapter.js +39 -0
- package/dist/cache/response-store-adapter.self-contained.worker.d.ts +17 -0
- package/dist/cache/response-store-adapter.self-contained.worker.js +10 -0
- package/dist/cache/response-store-adapter.service-binding.worker.d.ts +15 -0
- package/dist/cache/response-store-adapter.service-binding.worker.js +9 -0
- package/dist/cache/response-store-adapter.worker.d.ts +14 -0
- package/dist/cache/response-store-adapter.worker.js +254 -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 +289 -0
- package/dist/cacheability-artifact.d.ts +2 -3
- package/dist/cacheability-artifact.js +27 -1
- package/dist/cacheability-manifest-limits.d.ts +3 -4
- package/dist/cacheability-manifest-limits.js +1 -1
- package/dist/cacheability-probe.d.ts +8 -9
- package/dist/cacheability-probe.js +255 -91
- package/dist/cdn-warm.d.ts +31 -25
- package/dist/cdn-warm.js +208 -50
- package/dist/deploy-config.d.ts +11 -12
- package/dist/deploy-config.js +8 -3
- package/dist/deploy-help.d.ts +3 -4
- package/dist/deploy.d.ts +23 -24
- package/dist/deploy.js +44 -11
- package/dist/images/images-optimizer.d.ts +3 -4
- package/dist/prerender-kv-populate.d.ts +3 -4
- package/dist/prerender-kv-populate.js +3 -2
- package/dist/tpr.d.ts +9 -10
- package/dist/tpr.js +1 -1
- package/dist/utils/cache-control-metadata.d.ts +3 -4
- package/dist/version-deploy.d.ts +17 -17
- package/dist/version-deploy.js +6 -3
- package/dist/worker-deployment-url.d.ts +3 -4
- package/dist/workers-dev-url.d.ts +2 -3
- package/dist/wrangler-version-metadata.d.ts +2 -3
- package/package.json +13 -7
|
@@ -28,6 +28,10 @@ const DEFAULT_BINDING = "VINEXT_KV_CACHE";
|
|
|
28
28
|
const PATH_TAG_PREFIX = "_N_T_";
|
|
29
29
|
/** Max tag length to prevent KV key abuse. */
|
|
30
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;
|
|
31
35
|
/** Matches a valid base64 string (standard alphabet with optional padding). */
|
|
32
36
|
const BASE64_RE = /^[A-Za-z0-9+/]*={0,2}$/;
|
|
33
37
|
/**
|
|
@@ -98,14 +102,20 @@ var KVCacheHandler = class {
|
|
|
98
102
|
ttlSeconds;
|
|
99
103
|
/** Local in-memory cache for tag invalidation timestamps. Avoids redundant KV reads. */
|
|
100
104
|
_tagCache = /* @__PURE__ */ new Map();
|
|
105
|
+
/** Monotonic ordering for concurrent tag-cache fills and local invalidations. */
|
|
106
|
+
_tagCacheOrder = 0;
|
|
101
107
|
/** TTL (ms) for local tag cache entries. After this, re-fetch from KV. */
|
|
102
108
|
_tagCacheTtl;
|
|
109
|
+
/** Read options for entry keys only. `undefined` keeps the KV default cacheTtl. */
|
|
110
|
+
_entryReadOptions;
|
|
103
111
|
constructor(kvNamespace, options) {
|
|
104
112
|
this.kv = kvNamespace;
|
|
105
113
|
this.keySpace = createKvKeySpace(options?.appPrefix);
|
|
106
114
|
this.ctx = options?.ctx;
|
|
107
|
-
this.ttlSeconds = options?.ttlSeconds ??
|
|
115
|
+
this.ttlSeconds = options?.ttlSeconds ?? 2592e3;
|
|
108
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;
|
|
109
119
|
}
|
|
110
120
|
_entryKey(key) {
|
|
111
121
|
return this.keySpace.entryKey(key);
|
|
@@ -115,37 +125,64 @@ var KVCacheHandler = class {
|
|
|
115
125
|
}
|
|
116
126
|
async get(key, _ctx) {
|
|
117
127
|
const kvKey = this._entryKey(key);
|
|
118
|
-
const
|
|
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;
|
|
119
138
|
if (!raw) return null;
|
|
120
139
|
let parsed;
|
|
121
140
|
try {
|
|
122
141
|
parsed = JSON.parse(raw);
|
|
123
142
|
} catch {
|
|
124
|
-
this.
|
|
143
|
+
this._deleteEntryReadInBackground(kvKey);
|
|
125
144
|
return null;
|
|
126
145
|
}
|
|
127
146
|
const entry = validateCacheEntry(parsed);
|
|
128
147
|
if (!entry) {
|
|
129
148
|
console.error("[vinext] Invalid cache entry shape for key:", key);
|
|
130
|
-
this.
|
|
149
|
+
this._deleteEntryReadInBackground(kvKey);
|
|
131
150
|
return null;
|
|
132
151
|
}
|
|
133
152
|
let restoredValue = null;
|
|
134
153
|
if (entry.value) {
|
|
135
154
|
restoredValue = restoreArrayBuffers(entry.value);
|
|
136
155
|
if (!restoredValue) {
|
|
137
|
-
this.
|
|
156
|
+
this._deleteEntryReadInBackground(kvKey);
|
|
138
157
|
return null;
|
|
139
158
|
}
|
|
140
159
|
}
|
|
141
|
-
|
|
142
|
-
|
|
160
|
+
const entryTags = validUniqueTags(entry.tags);
|
|
161
|
+
if (this._hasRevalidatedTag(entryTags, entry.lastModified, true)) {
|
|
162
|
+
this._deleteEntryReadInBackground(kvKey);
|
|
143
163
|
return null;
|
|
144
164
|
}
|
|
145
|
-
|
|
146
|
-
|
|
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;
|
|
147
184
|
if (entry.expireAt !== void 0 && entry.expireAt !== null && Date.now() > entry.expireAt) {
|
|
148
|
-
this.
|
|
185
|
+
this._deleteEntryReadInBackground(kvKey);
|
|
149
186
|
return null;
|
|
150
187
|
}
|
|
151
188
|
const now = Date.now();
|
|
@@ -164,37 +201,63 @@ var KVCacheHandler = class {
|
|
|
164
201
|
};
|
|
165
202
|
}
|
|
166
203
|
/**
|
|
167
|
-
*
|
|
168
|
-
*
|
|
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.
|
|
169
207
|
*/
|
|
170
|
-
async
|
|
171
|
-
if (tags.length === 0) return
|
|
208
|
+
async _primeTagCache(tags) {
|
|
209
|
+
if (tags.length === 0) return;
|
|
210
|
+
const tagCache = this._tagCache;
|
|
172
211
|
const now = Date.now();
|
|
173
|
-
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;
|
|
174
256
|
for (const tag of tags) {
|
|
175
257
|
const cached = this._tagCache.get(tag);
|
|
176
|
-
if (cached
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
if (cached) this._tagCache.delete(tag);
|
|
180
|
-
uncachedTags.push(tag);
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
if (uncachedTags.length > 0) {
|
|
184
|
-
const tagResults = await Promise.all(uncachedTags.map((tag) => this.kv.get(this._tagKey(tag))));
|
|
185
|
-
for (let i = 0; i < uncachedTags.length; i++) {
|
|
186
|
-
const tagTime = tagResults[i];
|
|
187
|
-
const tagTimestamp = tagTime ? Number(tagTime) : 0;
|
|
188
|
-
this._tagCache.set(uncachedTags[i], {
|
|
189
|
-
timestamp: tagTimestamp,
|
|
190
|
-
fetchedAt: now
|
|
191
|
-
});
|
|
192
|
-
}
|
|
193
|
-
for (const tag of uncachedTags) {
|
|
194
|
-
const cached = this._tagCache.get(tag);
|
|
195
|
-
if (!cached || cached.timestamp === 0) continue;
|
|
196
|
-
if (Number.isNaN(cached.timestamp) || cached.timestamp >= lastModified) return true;
|
|
197
|
-
}
|
|
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;
|
|
198
261
|
}
|
|
199
262
|
return false;
|
|
200
263
|
}
|
|
@@ -243,10 +306,12 @@ var KVCacheHandler = class {
|
|
|
243
306
|
const tagList = Array.isArray(tags) ? tags : [tags];
|
|
244
307
|
const now = Date.now();
|
|
245
308
|
const validTags = tagList.filter((t) => validateTag(t) !== null);
|
|
246
|
-
await Promise.all(validTags.map((tag) => this.kv.put(this._tagKey(tag), String(now), { expirationTtl:
|
|
309
|
+
await Promise.all(validTags.map((tag) => this.kv.put(this._tagKey(tag), String(now), { expirationTtl: 2592e3 })));
|
|
310
|
+
const order = ++this._tagCacheOrder;
|
|
247
311
|
for (const tag of validTags) this._tagCache.set(tag, {
|
|
248
312
|
timestamp: now,
|
|
249
|
-
fetchedAt: now
|
|
313
|
+
fetchedAt: now,
|
|
314
|
+
order
|
|
250
315
|
});
|
|
251
316
|
}
|
|
252
317
|
/**
|
|
@@ -302,7 +367,15 @@ var KVCacheHandler = class {
|
|
|
302
367
|
* fresh KVCacheHandler per request or invoke this method explicitly.
|
|
303
368
|
*/
|
|
304
369
|
resetRequestCache() {
|
|
305
|
-
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);
|
|
306
379
|
}
|
|
307
380
|
/**
|
|
308
381
|
* Fire a KV delete in the background.
|
|
@@ -453,7 +526,8 @@ const createKvDataCacheAdapter = ({ env, options }) => {
|
|
|
453
526
|
return new KVCacheHandler(namespace, {
|
|
454
527
|
appPrefix: options?.appPrefix,
|
|
455
528
|
ttlSeconds: options?.ttlSeconds,
|
|
456
|
-
tagCacheTtlMs: options?.tagCacheTtlMs
|
|
529
|
+
tagCacheTtlMs: options?.tagCacheTtlMs,
|
|
530
|
+
entryCacheTtlSeconds: options?.entryCacheTtlSeconds
|
|
457
531
|
});
|
|
458
532
|
};
|
|
459
533
|
//#endregion
|
package/dist/cache/kv-key.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
//#region src/cache/kv-key.d.ts
|
|
2
2
|
/** Key prefix for cache entries. */
|
|
3
|
-
declare const ENTRY_PREFIX = "cache:";
|
|
4
|
-
type KvKeySpace = {
|
|
3
|
+
export declare const ENTRY_PREFIX = "cache:";
|
|
4
|
+
export type KvKeySpace = {
|
|
5
5
|
/** Prefix shared by every cache entry, including entries with hashed logical keys. */
|
|
6
6
|
entryPrefix: string;
|
|
7
7
|
entryKey(logicalKey: string): string;
|
|
@@ -11,6 +11,5 @@ type KvKeySpace = {
|
|
|
11
11
|
* Create the deterministic key namespace shared by runtime cache operations
|
|
12
12
|
* and deploy-time prerender population.
|
|
13
13
|
*/
|
|
14
|
-
declare function createKvKeySpace(appPrefix: string | undefined): KvKeySpace;
|
|
15
|
-
//#endregion
|
|
16
|
-
export { ENTRY_PREFIX, KvKeySpace, createKvKeySpace };
|
|
14
|
+
export declare function createKvKeySpace(appPrefix: string | undefined): KvKeySpace;
|
|
15
|
+
//#endregion
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
//#region src/cache/response-store-adapter.d.ts
|
|
2
|
+
export type ResponseStoreAdapterOptions = {
|
|
3
|
+
mode?: "self-contained" | "service-binding";
|
|
4
|
+
};
|
|
5
|
+
/**
|
|
6
|
+
* Use Workers Response Store for both vinext response-stage and data caching.
|
|
7
|
+
* Service-binding mode keeps storage in a separate cache Worker. Self-contained
|
|
8
|
+
* mode keeps the same API and loopback in the application Worker.
|
|
9
|
+
*/
|
|
10
|
+
export declare function responseStoreAdapter(options?: ResponseStoreAdapterOptions): {
|
|
11
|
+
cdn: {
|
|
12
|
+
adapter: string;
|
|
13
|
+
output: {
|
|
14
|
+
entry: string;
|
|
15
|
+
matchesBuild({ plugins }: {
|
|
16
|
+
plugins: readonly {
|
|
17
|
+
name?: string;
|
|
18
|
+
}[];
|
|
19
|
+
}): boolean;
|
|
20
|
+
transformHostEntry({ code, id }: {
|
|
21
|
+
code: string;
|
|
22
|
+
id: string;
|
|
23
|
+
}): string | null;
|
|
24
|
+
type: "multi-stage";
|
|
25
|
+
};
|
|
26
|
+
capabilities: {
|
|
27
|
+
buildIdentity: "response-header";
|
|
28
|
+
isResponsePolicyHeader: (name: string) => boolean;
|
|
29
|
+
requestRouting: "uncached-stage";
|
|
30
|
+
warmup: "response-store";
|
|
31
|
+
};
|
|
32
|
+
};
|
|
33
|
+
data: {
|
|
34
|
+
adapter: string;
|
|
35
|
+
};
|
|
36
|
+
};
|
|
37
|
+
//#endregion
|
|
@@ -0,0 +1,39 @@
|
|
|
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
|
+
/**
|
|
5
|
+
* Use Workers Response Store for both vinext response-stage and data caching.
|
|
6
|
+
* Service-binding mode keeps storage in a separate cache Worker. Self-contained
|
|
7
|
+
* mode keeps the same API and loopback in the application Worker.
|
|
8
|
+
*/
|
|
9
|
+
function responseStoreAdapter(options = {}) {
|
|
10
|
+
const mode = options.mode ?? "service-binding";
|
|
11
|
+
if (mode !== "service-binding" && mode !== "self-contained") throw new Error(`Unknown Workers Response Store mode: ${String(mode)}`);
|
|
12
|
+
const workerEntry = fileURLToPath(import.meta.resolve(mode === "self-contained" ? "./response-store-adapter.self-contained.worker.js" : "./response-store-adapter.service-binding.worker.js"));
|
|
13
|
+
const entrypoints = mode === "self-contained" ? "CacheMetadata, ResponseStoreBinding, ResponseStoreRevalidator" : "ResponseStoreClient, ResponseStoreRevalidator";
|
|
14
|
+
return {
|
|
15
|
+
cdn: {
|
|
16
|
+
adapter: fileURLToPath(import.meta.resolve("./response-store-cdn.runtime.js")),
|
|
17
|
+
output: {
|
|
18
|
+
entry: workerEntry,
|
|
19
|
+
matchesBuild({ plugins }) {
|
|
20
|
+
return plugins.some(({ name }) => name === "vite-plugin-cloudflare" || name?.startsWith("vite-plugin-cloudflare:"));
|
|
21
|
+
},
|
|
22
|
+
transformHostEntry({ code, id }) {
|
|
23
|
+
if ((id.charCodeAt(0) === 0 ? id.slice(1) : id) !== CLOUDFLARE_WORKER_ENTRY_ID) return null;
|
|
24
|
+
return `${code}\nexport { ${entrypoints} } from ${JSON.stringify(workerEntry)};\n`;
|
|
25
|
+
},
|
|
26
|
+
type: "multi-stage"
|
|
27
|
+
},
|
|
28
|
+
capabilities: {
|
|
29
|
+
buildIdentity: "response-header",
|
|
30
|
+
isResponsePolicyHeader: (name) => name.toLowerCase() === "cdn-cache-control" || name.toLowerCase() === "cloudflare-cdn-cache-control",
|
|
31
|
+
requestRouting: "uncached-stage",
|
|
32
|
+
warmup: "response-store"
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
data: { adapter: fileURLToPath(import.meta.resolve("./response-store-data.runtime.js")) }
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
//#endregion
|
|
39
|
+
export { responseStoreAdapter };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { VinextResponseStoreEnv } from "./response-store-adapter.worker.js";
|
|
2
|
+
import { ResponseStoreRevalidatorEntrypoint, WorkersResponseStoreEnv } from "@cloudflare/workers-response-store";
|
|
3
|
+
import { CacheMetadata as CacheMetadata$1, ResponseStoreBinding as ResponseStoreBinding$1 } from "@cloudflare/workers-response-store/service";
|
|
4
|
+
//#region src/cache/response-store-adapter.self-contained.worker.d.ts
|
|
5
|
+
export declare const CacheMetadata: typeof CacheMetadata$1;
|
|
6
|
+
export declare const ResponseStoreBinding: typeof ResponseStoreBinding$1;
|
|
7
|
+
export declare const ResponseStoreRevalidator: ResponseStoreRevalidatorEntrypoint<WorkersResponseStoreEnv>;
|
|
8
|
+
declare const _default: {
|
|
9
|
+
fetch(request: Request, env: VinextResponseStoreEnv, ctx: {
|
|
10
|
+
exports?: Record<string, unknown>;
|
|
11
|
+
passThroughOnException(): void;
|
|
12
|
+
props?: unknown;
|
|
13
|
+
waitUntil(promise: Promise<unknown>): void;
|
|
14
|
+
}): Promise<Response>;
|
|
15
|
+
};
|
|
16
|
+
//#endregion
|
|
17
|
+
export { _default as default };
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { createVinextResponseStoreHandler, createVinextResponseStoreOptions } from "./response-store-adapter.worker.js";
|
|
2
|
+
import { createWorkersResponseStore } from "@cloudflare/workers-response-store";
|
|
3
|
+
//#region src/cache/response-store-adapter.self-contained.worker.ts
|
|
4
|
+
const responseStore = createWorkersResponseStore(createVinextResponseStoreOptions());
|
|
5
|
+
const CacheMetadata = responseStore.entrypoints.CacheMetadata;
|
|
6
|
+
const ResponseStoreBinding = responseStore.entrypoints.ResponseStoreBinding;
|
|
7
|
+
const ResponseStoreRevalidator = responseStore.entrypoints.ResponseStoreRevalidator;
|
|
8
|
+
var response_store_adapter_self_contained_worker_default = createVinextResponseStoreHandler(responseStore);
|
|
9
|
+
//#endregion
|
|
10
|
+
export { CacheMetadata, ResponseStoreBinding, ResponseStoreRevalidator, response_store_adapter_self_contained_worker_default as default };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { VinextResponseStoreEnv } from "./response-store-adapter.worker.js";
|
|
2
|
+
import { ResponseStoreClientEntrypoint, ResponseStoreRevalidatorEntrypoint, WorkersResponseStoreClientEnv } from "@cloudflare/workers-response-store";
|
|
3
|
+
//#region src/cache/response-store-adapter.service-binding.worker.d.ts
|
|
4
|
+
export declare const ResponseStoreClient: ResponseStoreClientEntrypoint;
|
|
5
|
+
export declare const ResponseStoreRevalidator: ResponseStoreRevalidatorEntrypoint<WorkersResponseStoreClientEnv>;
|
|
6
|
+
declare const _default: {
|
|
7
|
+
fetch(request: Request, env: VinextResponseStoreEnv, ctx: {
|
|
8
|
+
exports?: Record<string, unknown>;
|
|
9
|
+
passThroughOnException(): void;
|
|
10
|
+
props?: unknown;
|
|
11
|
+
waitUntil(promise: Promise<unknown>): void;
|
|
12
|
+
}): Promise<Response>;
|
|
13
|
+
};
|
|
14
|
+
//#endregion
|
|
15
|
+
export { _default as default };
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { createVinextResponseStoreHandler, createVinextResponseStoreOptions } from "./response-store-adapter.worker.js";
|
|
2
|
+
import { createWorkersResponseStoreClient } from "@cloudflare/workers-response-store";
|
|
3
|
+
//#region src/cache/response-store-adapter.service-binding.worker.ts
|
|
4
|
+
const responseStore = createWorkersResponseStoreClient(createVinextResponseStoreOptions());
|
|
5
|
+
const ResponseStoreClient = responseStore.entrypoints.ResponseStoreClient;
|
|
6
|
+
const ResponseStoreRevalidator = responseStore.entrypoints.ResponseStoreRevalidator;
|
|
7
|
+
var response_store_adapter_service_binding_worker_default = createVinextResponseStoreHandler(responseStore);
|
|
8
|
+
//#endregion
|
|
9
|
+
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>(): WorkersResponseStoreOptions<Env>;
|
|
11
|
+
export declare function createVinextResponseStoreHandler(store: WorkersResponseStore): {
|
|
12
|
+
fetch(request: Request, env: VinextResponseStoreEnv, ctx: WorkerExecutionContext): Promise<Response>;
|
|
13
|
+
};
|
|
14
|
+
//#endregion
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import { captureResponseStoreDataRegeneration, runWithResponseStoreInvocation, setResponseStore } from "./response-store-data.runtime.js";
|
|
2
|
+
import { VINEXT_RSC_CONTENT_TYPE, VINEXT_RSC_VARY_HEADER, applyRscCompatibilityIdHeader, applyRscDeploymentIdHeader, createCanonicalRscRequestHeaders, createCanonicalRscRequestUrl } from "vinext/internal/server/app-rsc-cache-busting";
|
|
3
|
+
import { isNonCacheableCacheControl } from "vinext/shims/cdn-cache";
|
|
4
|
+
import { loadVinextRequestStage } from "vinext/server/request-stage";
|
|
5
|
+
import { loadVinextResponseStage } from "vinext/server/response-stage";
|
|
6
|
+
//#region src/cache/response-store-adapter.worker.ts
|
|
7
|
+
const ROUTE_REVALIDATOR_ID = "vinext:response";
|
|
8
|
+
const RESPONSE_STORE_KEY_PARAM = "__vinext_response_store";
|
|
9
|
+
const AGE_BASIS_HEADER = "X-Workers-Response-Store-Age-Basis";
|
|
10
|
+
const WARMUP_USER_AGENT = "vinext-cloudflare-cdn-warm";
|
|
11
|
+
const REPLAY_REQUEST_HEADERS = VINEXT_RSC_VARY_HEADER.split(",").map((name) => name.trim().toLowerCase());
|
|
12
|
+
function stageContext(ctx, env) {
|
|
13
|
+
const assets = Reflect.get(env, "ASSETS");
|
|
14
|
+
return Object.assign(Object.create(Object.getPrototypeOf(ctx)), ctx, {
|
|
15
|
+
...assets && typeof assets === "object" && typeof Reflect.get(assets, "fetch") === "function" ? { assets } : {},
|
|
16
|
+
...ctx.exports ? { exports: ctx.exports } : {},
|
|
17
|
+
hostRuntime: "worker",
|
|
18
|
+
passThroughOnException: () => ctx.passThroughOnException(),
|
|
19
|
+
...ctx.props === void 0 ? {} : { props: ctx.props },
|
|
20
|
+
waitUntil: (promise) => ctx.waitUntil(promise)
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
function safeProps(props) {
|
|
24
|
+
const copy = JSON.parse(JSON.stringify(props));
|
|
25
|
+
if (copy && typeof copy === "object") {
|
|
26
|
+
if ("draftModeCookie" in copy) copy.draftModeCookie = null;
|
|
27
|
+
if ("middlewareCookieOverlay" in copy) copy.middlewareCookieOverlay = null;
|
|
28
|
+
}
|
|
29
|
+
return copy;
|
|
30
|
+
}
|
|
31
|
+
function replayHeaders(request) {
|
|
32
|
+
return REPLAY_REQUEST_HEADERS.flatMap((name) => {
|
|
33
|
+
const value = request.headers.get(name);
|
|
34
|
+
return value === null ? [] : [[name, value]];
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
function isReplayableInvocation(request, props) {
|
|
38
|
+
const method = request.method.toUpperCase();
|
|
39
|
+
if (method !== "GET" && method !== "HEAD") return false;
|
|
40
|
+
if (request.headers.has("authorization") || request.headers.has("cookie")) return false;
|
|
41
|
+
return !(props && typeof props === "object" && ["draftModeCookie", "middlewareCookieOverlay"].some((name) => name in props && Reflect.get(props, name) !== null));
|
|
42
|
+
}
|
|
43
|
+
function serializeInvocation(request, props) {
|
|
44
|
+
return JSON.stringify({
|
|
45
|
+
props: safeProps(props),
|
|
46
|
+
request: {
|
|
47
|
+
headers: replayHeaders(request),
|
|
48
|
+
method: request.method,
|
|
49
|
+
url: request.url
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
function parseInvocation(value) {
|
|
54
|
+
if (typeof value !== "string") throw new TypeError("Invalid vinext response-store invocation");
|
|
55
|
+
const invocation = JSON.parse(value);
|
|
56
|
+
if (!invocation || typeof invocation !== "object" || !invocation.request || typeof invocation.request.url !== "string" || typeof invocation.request.method !== "string" || !Array.isArray(invocation.request.headers)) throw new TypeError("Invalid vinext response-store invocation");
|
|
57
|
+
return invocation;
|
|
58
|
+
}
|
|
59
|
+
function restoreRequest(invocation) {
|
|
60
|
+
return new Request(invocation.request.url, {
|
|
61
|
+
headers: invocation.request.headers,
|
|
62
|
+
method: invocation.request.method
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
async function invokeRequestStage(request, env, ctx) {
|
|
66
|
+
const { handleRequestStage } = await loadVinextRequestStage();
|
|
67
|
+
return handleRequestStage(request, env, stageContext(ctx, env), (request, props, options) => invokeResponseStage(request, props, env, ctx, options.cache));
|
|
68
|
+
}
|
|
69
|
+
async function invokeResponseStage(request, props, env, ctx, cache, capture) {
|
|
70
|
+
const context = stageContext(ctx, env);
|
|
71
|
+
const dispatchRequestStage = (request) => invokeRequestStage(request, env, ctx);
|
|
72
|
+
const { handleResponseStage } = await loadVinextResponseStage();
|
|
73
|
+
const serialized = serializeInvocation(request, props);
|
|
74
|
+
return runWithResponseStoreInvocation(serialized, isReplayableInvocation(request, props), () => handleResponseStage(request, env, context, props, dispatchRequestStage, { cache }), capture);
|
|
75
|
+
}
|
|
76
|
+
function createVinextResponseStoreOptions() {
|
|
77
|
+
return { async regenerate(input, { env, ctx }) {
|
|
78
|
+
if (input.id === ROUTE_REVALIDATOR_ID) {
|
|
79
|
+
const invocation = parseInvocation(input.args.at(-1));
|
|
80
|
+
const response = await invokeResponseStage(restoreRequest(invocation), invocation.props, env, ctx, "shared");
|
|
81
|
+
if (!isCacheable(response)) {
|
|
82
|
+
await response.body?.cancel().catch(() => {});
|
|
83
|
+
throw new Error("Vinext response-stage regeneration was not cacheable");
|
|
84
|
+
}
|
|
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
|
+
});
|
|
101
|
+
}
|
|
102
|
+
throw new Error(`Unknown vinext response-store revalidator ${input.id}`);
|
|
103
|
+
} };
|
|
104
|
+
}
|
|
105
|
+
async function cacheRequest(request, props) {
|
|
106
|
+
const identity = JSON.stringify([
|
|
107
|
+
request.method,
|
|
108
|
+
request.url,
|
|
109
|
+
safeProps(props),
|
|
110
|
+
replayHeaders(request)
|
|
111
|
+
]);
|
|
112
|
+
const digest = new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(identity)));
|
|
113
|
+
const url = new URL(request.url);
|
|
114
|
+
url.searchParams.set(RESPONSE_STORE_KEY_PARAM, [...digest].map((byte) => byte.toString(16).padStart(2, "0")).join(""));
|
|
115
|
+
return new Request(url, { method: "GET" });
|
|
116
|
+
}
|
|
117
|
+
function isCacheable(response) {
|
|
118
|
+
const policy = response.headers.get("Cloudflare-CDN-Cache-Control") ?? response.headers.get("CDN-Cache-Control") ?? response.headers.get("Cache-Control");
|
|
119
|
+
return response.status >= 200 && response.status < 400 && policy !== null && !isNonCacheableCacheControl(policy);
|
|
120
|
+
}
|
|
121
|
+
function isResponseStoreMiss(response) {
|
|
122
|
+
return response.status === 404 && response.headers.get("X-Workers-Response-Store") === "MISS";
|
|
123
|
+
}
|
|
124
|
+
function publicResponse(response, cacheStatus) {
|
|
125
|
+
const headers = new Headers(response.headers);
|
|
126
|
+
const publicCacheStatus = cacheStatus === "HIT" && headers.get("CF-Cache-Status") === "UPDATING" ? "UPDATING" : cacheStatus;
|
|
127
|
+
const ageBasis = /^(\d+):(\d+)$/.exec(headers.get(AGE_BASIS_HEADER) ?? "");
|
|
128
|
+
if (cacheStatus === "HIT" && ageBasis) {
|
|
129
|
+
const elapsed = (BigInt(Date.now()) - BigInt(ageBasis[1])) / 1000n;
|
|
130
|
+
headers.set("Age", String(BigInt(ageBasis[2]) + (elapsed > 0n ? elapsed : 0n)));
|
|
131
|
+
}
|
|
132
|
+
for (const name of [
|
|
133
|
+
"Cache-Tag",
|
|
134
|
+
"CDN-Cache-Control",
|
|
135
|
+
"CF-Cache-Status",
|
|
136
|
+
"Cloudflare-CDN-Cache-Control",
|
|
137
|
+
"X-Workers-Response-Store",
|
|
138
|
+
AGE_BASIS_HEADER,
|
|
139
|
+
"X-Workers-Response-Store-Binding-Invocation",
|
|
140
|
+
"X-Workers-Response-Store-Revision"
|
|
141
|
+
]) headers.delete(name);
|
|
142
|
+
if (publicCacheStatus) {
|
|
143
|
+
headers.set("X-Nextjs-Cache", publicCacheStatus);
|
|
144
|
+
headers.set("X-Vinext-Cache", publicCacheStatus);
|
|
145
|
+
}
|
|
146
|
+
const cacheControl = headers.get("Cache-Control");
|
|
147
|
+
if (!cacheControl || !isNonCacheableCacheControl(cacheControl)) headers.set("Cache-Control", "private, max-age=0, must-revalidate");
|
|
148
|
+
return new Response(response.body, {
|
|
149
|
+
headers,
|
|
150
|
+
status: response.status,
|
|
151
|
+
statusText: response.statusText
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
let responseStore;
|
|
155
|
+
function createVinextResponseStoreHandler(store) {
|
|
156
|
+
responseStore = store;
|
|
157
|
+
setResponseStore(store);
|
|
158
|
+
return handler;
|
|
159
|
+
}
|
|
160
|
+
const handler = { async fetch(request, env, ctx) {
|
|
161
|
+
const context = stageContext(ctx, env);
|
|
162
|
+
const dispatchResponseStage = async (stageRequest, props, options) => {
|
|
163
|
+
const invocation = serializeInvocation(stageRequest, props);
|
|
164
|
+
if (options.cache === "bypass" || stageRequest.method !== "GET" && stageRequest.method !== "HEAD") return publicResponse(await invokeResponseStage(stageRequest, props, env, ctx, "bypass"), "BYPASS");
|
|
165
|
+
const isWarmup = request.headers.get("user-agent") === WARMUP_USER_AGENT;
|
|
166
|
+
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
|
+
props: {
|
|
168
|
+
...props,
|
|
169
|
+
isRscRequest: true,
|
|
170
|
+
renderMode: "navigation"
|
|
171
|
+
},
|
|
172
|
+
request: new Request(new URL(createCanonicalRscRequestUrl(stageRequest.url), stageRequest.url), { headers: createCanonicalRscRequestHeaders() })
|
|
173
|
+
} : void 0;
|
|
174
|
+
const rscKey = rscSeed ? await cacheRequest(rscSeed.request, rscSeed.props) : void 0;
|
|
175
|
+
const key = await cacheRequest(stageRequest, props);
|
|
176
|
+
const stored = await responseStore.fetch(key);
|
|
177
|
+
if (!isResponseStoreMiss(stored)) {
|
|
178
|
+
if (!rscKey) return publicResponse(stored, "HIT");
|
|
179
|
+
const storedRsc = await responseStore.fetch(rscKey);
|
|
180
|
+
if (!isResponseStoreMiss(storedRsc)) {
|
|
181
|
+
await storedRsc.body?.cancel();
|
|
182
|
+
return publicResponse(stored, "HIT");
|
|
183
|
+
}
|
|
184
|
+
await Promise.all([stored.body?.cancel(), storedRsc.body?.cancel()]);
|
|
185
|
+
}
|
|
186
|
+
const capture = rscSeed ? { captureRscData: true } : isWarmup ? {} : { streamResponse: true };
|
|
187
|
+
const rendered = await invokeResponseStage(stageRequest, props, env, ctx, "shared", capture);
|
|
188
|
+
if (capture.admittedResponse) {
|
|
189
|
+
ctx.waitUntil(capture.admittedResponse.then(async (admitted) => {
|
|
190
|
+
if (!isCacheable(admitted)) {
|
|
191
|
+
await admitted.body?.cancel().catch(() => {});
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
await responseStore.put(key, admitted, {
|
|
195
|
+
coalesce: true,
|
|
196
|
+
revalidator: {
|
|
197
|
+
id: ROUTE_REVALIDATOR_ID,
|
|
198
|
+
args: [invocation]
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
}).catch((error) => {
|
|
202
|
+
console.error(JSON.stringify({
|
|
203
|
+
message: "Vinext response-store admission failed",
|
|
204
|
+
error: error instanceof Error ? error.message : String(error)
|
|
205
|
+
}));
|
|
206
|
+
}));
|
|
207
|
+
return publicResponse(rendered, "MISS");
|
|
208
|
+
}
|
|
209
|
+
if (!isCacheable(rendered)) {
|
|
210
|
+
capture?.rscData?.catch(() => {});
|
|
211
|
+
return publicResponse(rendered, "BYPASS");
|
|
212
|
+
}
|
|
213
|
+
if (rscSeed && !capture?.rscData) {
|
|
214
|
+
await rendered.body?.cancel();
|
|
215
|
+
throw new Error("Vinext response-store warmup did not capture the App page RSC payload");
|
|
216
|
+
}
|
|
217
|
+
const [foreground, cacheBody] = rendered.body ? rendered.body.tee() : [null, null];
|
|
218
|
+
const cacheResponse = new Response(cacheBody, rendered);
|
|
219
|
+
await responseStore.put(key, cacheResponse, {
|
|
220
|
+
coalesce: true,
|
|
221
|
+
revalidator: {
|
|
222
|
+
id: ROUTE_REVALIDATOR_ID,
|
|
223
|
+
args: [invocation]
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
if (rscSeed && rscKey && capture?.rscData) {
|
|
227
|
+
const rscData = await capture.rscData;
|
|
228
|
+
const rscHeaders = new Headers(rendered.headers);
|
|
229
|
+
rscHeaders.delete("Content-Length");
|
|
230
|
+
rscHeaders.delete("Link");
|
|
231
|
+
rscHeaders.delete("X-Vinext-Response-Store-Replayable");
|
|
232
|
+
rscHeaders.set("Content-Type", VINEXT_RSC_CONTENT_TYPE);
|
|
233
|
+
rscHeaders.set("Vary", VINEXT_RSC_VARY_HEADER);
|
|
234
|
+
applyRscCompatibilityIdHeader(rscHeaders);
|
|
235
|
+
applyRscDeploymentIdHeader(rscHeaders);
|
|
236
|
+
const rscInvocation = serializeInvocation(rscSeed.request, rscSeed.props);
|
|
237
|
+
await responseStore.put(rscKey, new Response(rscData, {
|
|
238
|
+
headers: rscHeaders,
|
|
239
|
+
status: 200
|
|
240
|
+
}), {
|
|
241
|
+
coalesce: true,
|
|
242
|
+
revalidator: {
|
|
243
|
+
id: ROUTE_REVALIDATOR_ID,
|
|
244
|
+
args: [rscInvocation]
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
return publicResponse(new Response(foreground, rendered), "MISS");
|
|
249
|
+
};
|
|
250
|
+
const { handleRequestStage } = await loadVinextRequestStage();
|
|
251
|
+
return handleRequestStage(request, env, context, dispatchResponseStage);
|
|
252
|
+
} };
|
|
253
|
+
//#endregion
|
|
254
|
+
export { createVinextResponseStoreHandler, createVinextResponseStoreOptions };
|