@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.
- package/README.md +165 -37
- package/dist/cache/browser-cache-policy.d.ts +7 -0
- package/dist/cache/browser-cache-policy.js +39 -0
- package/dist/cache/cdn-adapter-config.d.ts +2 -1
- package/dist/cache/cdn-adapter-config.js +13 -5
- package/dist/cache/cdn-adapter.d.ts +5 -56
- package/dist/cache/cdn-adapter.js +3 -61
- package/dist/cache/cdn-adapter.runtime.d.ts +1 -0
- package/dist/cache/cdn-adapter.runtime.js +33 -19
- package/dist/cache/cdn-adapter.worker.d.ts +1 -1
- package/dist/cache/cdn-adapter.worker.js +10 -35
- package/dist/cache/cdn-build-id.d.ts +1 -1
- package/dist/cache/cdn-build-id.js +1 -1
- package/dist/cache/config.d.ts +137 -0
- package/dist/cache/config.js +87 -0
- package/dist/cache/kv-data-adapter.d.ts +1 -1
- package/dist/cache/kv-data-adapter.js +1 -1
- package/dist/cache/kv-data-adapter.runtime.js +17 -15
- package/dist/cache/kv-key.d.ts +7 -4
- package/dist/cache/kv-key.js +17 -6
- package/dist/cache/response-store-adapter.d.ts +9 -6
- package/dist/cache/response-store-adapter.js +20 -1
- package/dist/cache/response-store-adapter.worker.js +121 -48
- package/dist/cache/response-store-cdn.runtime.js +1 -0
- package/dist/cache/response-store-data.runtime.js +53 -31
- package/dist/cache/static-assets-adapter.build.d.ts +6 -0
- package/dist/cache/static-assets-adapter.build.js +102 -0
- package/dist/cache/static-assets-adapter.d.ts +27 -0
- package/dist/cache/static-assets-adapter.js +26 -0
- package/dist/cache/static-assets-adapter.runtime.d.ts +27 -0
- package/dist/cache/static-assets-adapter.runtime.js +94 -0
- package/dist/cache/static-assets-adapter.shared.d.ts +12 -0
- package/dist/cache/static-assets-adapter.shared.js +4 -0
- package/dist/cache/workers-cache-cdn-adapter.d.ts +58 -0
- package/dist/cache/workers-cache-cdn-adapter.js +65 -0
- package/dist/cacheability-artifact.d.ts +7 -1
- package/dist/cacheability-artifact.js +60 -21
- package/dist/cacheability-probe.d.ts +2 -0
- package/dist/cacheability-probe.js +107 -13
- package/dist/cdn-warm.d.ts +1 -0
- package/dist/cdn-warm.js +4 -3
- package/dist/deploy-config.d.ts +3 -8
- package/dist/deploy-config.js +5 -23
- package/dist/deploy-help.js +47 -42
- package/dist/deploy.d.ts +18 -16
- package/dist/deploy.js +355 -216
- package/dist/tpr.d.ts +4 -0
- package/dist/tpr.js +16 -7
- package/dist/version-deploy.d.ts +27 -0
- package/dist/version-deploy.js +121 -3
- package/dist/wrangler-version-metadata.js +1 -1
- package/package.json +16 -5
- package/dist/prerender-kv-populate.d.ts +0 -24
- 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 {
|
|
3
|
-
import {
|
|
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
|
|
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
|
|
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
|
-
|
|
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),
|
|
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
|
|
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
|
|
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 };
|
|
@@ -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"
|
|
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
|
-
*
|
|
40
|
-
*
|
|
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
|
|
44
|
-
if (/[\x00-\x1f
|
|
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
|
|
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 =
|
|
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)
|
|
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}"
|
|
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,
|
package/dist/cache/kv-key.d.ts
CHANGED
|
@@ -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
|
package/dist/cache/kv-key.js
CHANGED
|
@@ -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
|
|
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 {
|