@vinext/cloudflare 1.0.0-beta.8 → 1.0.0-beta.9
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 +9 -0
- package/dist/cache/cdn-adapter.worker.d.ts +1 -0
- package/dist/cache/cdn-adapter.worker.js +6 -4
- package/dist/cache/kv-data-adapter.d.ts +4 -0
- package/dist/cache/kv-data-adapter.js +5 -1
- package/dist/cache/response-store-adapter.d.ts +33 -0
- package/dist/cache/response-store-adapter.js +7 -1
- package/dist/cache/response-store-adapter.self-contained.worker.d.ts +1 -0
- package/dist/cache/response-store-adapter.self-contained.worker.js +3 -1
- package/dist/cache/response-store-adapter.service-binding.worker.d.ts +1 -0
- package/dist/cache/response-store-adapter.service-binding.worker.js +3 -1
- package/dist/cache/response-store-adapter.worker.d.ts +1 -1
- package/dist/cache/response-store-adapter.worker.js +48 -38
- package/dist/cdn-warm.d.ts +1 -1
- package/dist/cdn-warm.js +23 -7
- package/dist/deploy-help.js +20 -14
- package/dist/deploy.d.ts +13 -5
- package/dist/deploy.js +175 -99
- package/dist/tpr.d.ts +25 -72
- package/dist/tpr.js +45 -603
- package/dist/version-deploy.js +4 -2
- package/dist/wrangler-config.d.ts +19 -0
- package/dist/wrangler-config.js +294 -0
- package/package.json +4 -8
package/README.md
CHANGED
|
@@ -91,6 +91,15 @@ npx @vinext/cloudflare deploy
|
|
|
91
91
|
`vinext-cloudflare deploy` never creates, rewrites, or deploys the Response
|
|
92
92
|
Store Worker.
|
|
93
93
|
|
|
94
|
+
Metadata sharding is opt-in and works in either deployment mode:
|
|
95
|
+
|
|
96
|
+
```ts
|
|
97
|
+
vinext({ cache: responseStoreAdapter({ shards: 16 }) });
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Keys remain pinned to one shard while tag/path mutations fan out across every
|
|
101
|
+
shard. Omit `shards` to retain the original single metadata Durable Object.
|
|
102
|
+
|
|
94
103
|
To deploy storage and cache entrypoints with the application instead, select
|
|
95
104
|
self-contained mode:
|
|
96
105
|
|
|
@@ -2,8 +2,10 @@ import { VINEXT_CDN_BUILD_ID_HEADER, getVinextCdnBuildIdentity } from "./cdn-bui
|
|
|
2
2
|
import { NEXTJS_CACHE_HEADER, VINEXT_CACHE_HEADER, VINEXT_PRERENDER_READINESS_PATH, VINEXT_RSC_VARY_HEADER } from "vinext/internal/server/headers";
|
|
3
3
|
import { isNonCacheableCacheControl } from "vinext/shims/cdn-cache";
|
|
4
4
|
import { WorkerEntrypoint } from "cloudflare:workers";
|
|
5
|
+
import "vinext/internal/server/cloudflare-workers-tracing";
|
|
5
6
|
import { loadVinextRequestStage } from "vinext/server/request-stage";
|
|
6
7
|
import { loadVinextResponseStage } from "vinext/server/response-stage";
|
|
8
|
+
import { traceCachedResponseStart } from "vinext/internal/server/response-start-tracing";
|
|
7
9
|
//#region src/cache/cdn-adapter.worker.ts
|
|
8
10
|
const CACHED_RESPONSE_STAGE_EXPORT = "VinextCachedResponse";
|
|
9
11
|
const UNCACHED_RESPONSE_STAGE_EXPORT = "VinextUncachedResponse";
|
|
@@ -197,15 +199,15 @@ function preventResponseCaching(response) {
|
|
|
197
199
|
* the outer gateway never forwards an inner shared-cache policy after adding
|
|
198
200
|
* request-specific middleware or routing headers.
|
|
199
201
|
*/
|
|
200
|
-
function markSharedResponseStage(response, provenanceToken, exposeEntrypointCacheStatus = false) {
|
|
202
|
+
function markSharedResponseStage(response, provenanceToken, responseStageProps, exposeEntrypointCacheStatus = false) {
|
|
201
203
|
const headers = new Headers(response.headers);
|
|
202
204
|
const cacheStatus = exposeEntrypointCacheStatus ? headers.get("CF-Cache-Status") : null;
|
|
203
205
|
headers.set(SHARED_RESPONSE_STAGE_HEADER, cacheStatus ? `${provenanceToken}:${encodeURIComponent(cacheStatus)}` : provenanceToken);
|
|
204
|
-
return new Response(response.body, {
|
|
206
|
+
return traceCachedResponseStart(new Response(response.body, {
|
|
205
207
|
headers,
|
|
206
208
|
status: response.status,
|
|
207
209
|
statusText: response.statusText
|
|
208
|
-
});
|
|
210
|
+
}), cacheStatus, responseStageProps);
|
|
209
211
|
}
|
|
210
212
|
function finalizeGatewayResponse(response, provenanceToken) {
|
|
211
213
|
const provenance = response.headers.get(SHARED_RESPONSE_STAGE_HEADER);
|
|
@@ -358,7 +360,7 @@ var cdn_adapter_worker_default = { async fetch(request, env, context) {
|
|
|
358
360
|
if (!binding) return responseStageUnavailable();
|
|
359
361
|
const entrypointRequest = usesSharedCache ? await createCacheFacingRequest(stageRequest, serializedInvocation) : stageRequest;
|
|
360
362
|
const response = validateResponseStageBuildIdentity(await binding.fetch(entrypointRequest));
|
|
361
|
-
return usesSharedCache ? markSharedResponseStage(response, sharedResponseStageProvenance, true) : response;
|
|
363
|
+
return usesSharedCache ? markSharedResponseStage(response, sharedResponseStageProvenance, props, true) : response;
|
|
362
364
|
} catch (error) {
|
|
363
365
|
if (isResponseStageReadinessRequest(stageRequest)) return responseStageUnavailable();
|
|
364
366
|
throw error;
|
|
@@ -44,5 +44,9 @@ export type KvDataAdapterOptions = {
|
|
|
44
44
|
export declare function kvDataAdapter(options?: KvDataAdapterOptions): {
|
|
45
45
|
adapter: string;
|
|
46
46
|
options: KvDataAdapterOptions | undefined;
|
|
47
|
+
capabilities: {
|
|
48
|
+
buildIdentity: "response-header";
|
|
49
|
+
warmup: "data-cache";
|
|
50
|
+
};
|
|
47
51
|
};
|
|
48
52
|
//#endregion
|
|
@@ -18,7 +18,11 @@ function kvDataAdapter(options) {
|
|
|
18
18
|
if (options?.binding !== void 0 && typeof options.binding !== "string") throw new TypeError("[vinext] kvDataAdapter({ binding }) must be a string KV binding name.");
|
|
19
19
|
return {
|
|
20
20
|
adapter: fileURLToPath(import.meta.resolve("./kv-data-adapter.runtime.js")),
|
|
21
|
-
options
|
|
21
|
+
options,
|
|
22
|
+
capabilities: {
|
|
23
|
+
buildIdentity: "response-header",
|
|
24
|
+
warmup: "data-cache"
|
|
25
|
+
}
|
|
22
26
|
};
|
|
23
27
|
}
|
|
24
28
|
//#endregion
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
//#region src/cache/response-store-adapter.d.ts
|
|
2
2
|
export type ResponseStoreAdapterOptions = {
|
|
3
3
|
mode?: "self-contained" | "service-binding";
|
|
4
|
+
/** Split version-scoped metadata across this many Durable Objects. */
|
|
5
|
+
shards?: number;
|
|
4
6
|
};
|
|
5
7
|
/**
|
|
6
8
|
* Use Workers Response Store for both vinext response-stage and data caching.
|
|
@@ -9,6 +11,31 @@ export type ResponseStoreAdapterOptions = {
|
|
|
9
11
|
*/
|
|
10
12
|
export declare function responseStoreAdapter(options?: ResponseStoreAdapterOptions): {
|
|
11
13
|
cdn: {
|
|
14
|
+
options?: undefined;
|
|
15
|
+
adapter: string;
|
|
16
|
+
output: {
|
|
17
|
+
entry: string;
|
|
18
|
+
matchesBuild({ plugins }: {
|
|
19
|
+
plugins: readonly {
|
|
20
|
+
name?: string;
|
|
21
|
+
}[];
|
|
22
|
+
}): boolean;
|
|
23
|
+
transformHostEntry({ code, id }: {
|
|
24
|
+
code: string;
|
|
25
|
+
id: string;
|
|
26
|
+
}): string | null;
|
|
27
|
+
type: "multi-stage";
|
|
28
|
+
};
|
|
29
|
+
capabilities: {
|
|
30
|
+
buildIdentity: "response-header";
|
|
31
|
+
isResponsePolicyHeader: (name: string) => boolean;
|
|
32
|
+
requestRouting: "uncached-stage";
|
|
33
|
+
warmup: "response-store";
|
|
34
|
+
};
|
|
35
|
+
} | {
|
|
36
|
+
options: {
|
|
37
|
+
shards: number;
|
|
38
|
+
};
|
|
12
39
|
adapter: string;
|
|
13
40
|
output: {
|
|
14
41
|
entry: string;
|
|
@@ -31,6 +58,12 @@ export declare function responseStoreAdapter(options?: ResponseStoreAdapterOptio
|
|
|
31
58
|
};
|
|
32
59
|
};
|
|
33
60
|
data: {
|
|
61
|
+
options?: undefined;
|
|
62
|
+
adapter: string;
|
|
63
|
+
} | {
|
|
64
|
+
options: {
|
|
65
|
+
shards: number;
|
|
66
|
+
};
|
|
34
67
|
adapter: string;
|
|
35
68
|
};
|
|
36
69
|
};
|
|
@@ -9,11 +9,14 @@ const CLOUDFLARE_WORKER_ENTRY_ID = "virtual:cloudflare/worker-entry";
|
|
|
9
9
|
function responseStoreAdapter(options = {}) {
|
|
10
10
|
const mode = options.mode ?? "service-binding";
|
|
11
11
|
if (mode !== "service-binding" && mode !== "self-contained") throw new Error(`Unknown Workers Response Store mode: ${String(mode)}`);
|
|
12
|
+
if (options.shards !== void 0 && (!Number.isSafeInteger(options.shards) || options.shards <= 1)) throw new TypeError("Workers Response Store shards must be an integer greater than 1");
|
|
13
|
+
const runtimeOptions = options.shards === void 0 ? {} : { options: { shards: options.shards } };
|
|
12
14
|
const workerEntry = fileURLToPath(import.meta.resolve(mode === "self-contained" ? "./response-store-adapter.self-contained.worker.js" : "./response-store-adapter.service-binding.worker.js"));
|
|
13
15
|
const entrypoints = mode === "self-contained" ? "CacheMetadata, ResponseStoreBinding, ResponseStoreRevalidator" : "ResponseStoreClient, ResponseStoreRevalidator";
|
|
14
16
|
return {
|
|
15
17
|
cdn: {
|
|
16
18
|
adapter: fileURLToPath(import.meta.resolve("./response-store-cdn.runtime.js")),
|
|
19
|
+
...runtimeOptions,
|
|
17
20
|
output: {
|
|
18
21
|
entry: workerEntry,
|
|
19
22
|
matchesBuild({ plugins }) {
|
|
@@ -32,7 +35,10 @@ function responseStoreAdapter(options = {}) {
|
|
|
32
35
|
warmup: "response-store"
|
|
33
36
|
}
|
|
34
37
|
},
|
|
35
|
-
data: {
|
|
38
|
+
data: {
|
|
39
|
+
adapter: fileURLToPath(import.meta.resolve("./response-store-data.runtime.js")),
|
|
40
|
+
...runtimeOptions
|
|
41
|
+
}
|
|
36
42
|
};
|
|
37
43
|
}
|
|
38
44
|
//#endregion
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { VinextResponseStoreEnv } from "./response-store-adapter.worker.js";
|
|
2
|
+
import "vinext/internal/server/cloudflare-workers-tracing";
|
|
2
3
|
import { ResponseStoreRevalidatorEntrypoint, WorkersResponseStoreEnv } from "@cloudflare/workers-response-store";
|
|
3
4
|
import { CacheMetadata as CacheMetadata$1, ResponseStoreBinding as ResponseStoreBinding$1 } from "@cloudflare/workers-response-store/service";
|
|
4
5
|
//#region src/cache/response-store-adapter.self-contained.worker.d.ts
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { createVinextResponseStoreHandler, createVinextResponseStoreOptions } from "./response-store-adapter.worker.js";
|
|
2
|
+
import "vinext/internal/server/cloudflare-workers-tracing";
|
|
2
3
|
import { createWorkersResponseStore } from "@cloudflare/workers-response-store";
|
|
4
|
+
import { configuredCdnCacheAdapterOptions } from "virtual:vinext-cdn-cache-adapter";
|
|
3
5
|
//#region src/cache/response-store-adapter.self-contained.worker.ts
|
|
4
|
-
const responseStore = createWorkersResponseStore(createVinextResponseStoreOptions());
|
|
6
|
+
const responseStore = createWorkersResponseStore(createVinextResponseStoreOptions(configuredCdnCacheAdapterOptions));
|
|
5
7
|
const CacheMetadata = responseStore.entrypoints.CacheMetadata;
|
|
6
8
|
const ResponseStoreBinding = responseStore.entrypoints.ResponseStoreBinding;
|
|
7
9
|
const ResponseStoreRevalidator = responseStore.entrypoints.ResponseStoreRevalidator;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { VinextResponseStoreEnv } from "./response-store-adapter.worker.js";
|
|
2
|
+
import "vinext/internal/server/cloudflare-workers-tracing";
|
|
2
3
|
import { ResponseStoreClientEntrypoint, ResponseStoreRevalidatorEntrypoint, WorkersResponseStoreClientEnv } from "@cloudflare/workers-response-store";
|
|
3
4
|
//#region src/cache/response-store-adapter.service-binding.worker.d.ts
|
|
4
5
|
export declare const ResponseStoreClient: ResponseStoreClientEntrypoint;
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { createVinextResponseStoreHandler, createVinextResponseStoreOptions } from "./response-store-adapter.worker.js";
|
|
2
|
+
import "vinext/internal/server/cloudflare-workers-tracing";
|
|
2
3
|
import { createWorkersResponseStoreClient } from "@cloudflare/workers-response-store";
|
|
4
|
+
import { configuredCdnCacheAdapterOptions } from "virtual:vinext-cdn-cache-adapter";
|
|
3
5
|
//#region src/cache/response-store-adapter.service-binding.worker.ts
|
|
4
|
-
const responseStore = createWorkersResponseStoreClient(createVinextResponseStoreOptions());
|
|
6
|
+
const responseStore = createWorkersResponseStoreClient(createVinextResponseStoreOptions(configuredCdnCacheAdapterOptions));
|
|
5
7
|
const ResponseStoreClient = responseStore.entrypoints.ResponseStoreClient;
|
|
6
8
|
const ResponseStoreRevalidator = responseStore.entrypoints.ResponseStoreRevalidator;
|
|
7
9
|
var response_store_adapter_service_binding_worker_default = createVinextResponseStoreHandler(responseStore);
|
|
@@ -7,7 +7,7 @@ type WorkerExecutionContext = {
|
|
|
7
7
|
waitUntil(promise: Promise<unknown>): void;
|
|
8
8
|
};
|
|
9
9
|
export type VinextResponseStoreEnv = WorkersResponseStoreClientEnv | WorkersResponseStoreEnv;
|
|
10
|
-
export declare function createVinextResponseStoreOptions<Env extends VinextResponseStoreEnv>(): WorkersResponseStoreOptions<Env>;
|
|
10
|
+
export declare function createVinextResponseStoreOptions<Env extends VinextResponseStoreEnv>(configuration?: Record<string, unknown>): WorkersResponseStoreOptions<Env>;
|
|
11
11
|
export declare function createVinextResponseStoreHandler(store: WorkersResponseStore): {
|
|
12
12
|
fetch(request: Request, env: VinextResponseStoreEnv, ctx: WorkerExecutionContext): Promise<Response>;
|
|
13
13
|
};
|
|
@@ -3,12 +3,14 @@ import { VINEXT_RSC_CONTENT_TYPE, VINEXT_RSC_VARY_HEADER, applyRscCompatibilityI
|
|
|
3
3
|
import { isNonCacheableCacheControl } from "vinext/shims/cdn-cache";
|
|
4
4
|
import { loadVinextRequestStage } from "vinext/server/request-stage";
|
|
5
5
|
import { loadVinextResponseStage } from "vinext/server/response-stage";
|
|
6
|
+
import { traceCachedResponseStart } from "vinext/internal/server/response-start-tracing";
|
|
6
7
|
//#region src/cache/response-store-adapter.worker.ts
|
|
7
8
|
const ROUTE_REVALIDATOR_ID = "vinext:response";
|
|
8
|
-
const RESPONSE_STORE_KEY_PARAM = "
|
|
9
|
+
const RESPONSE_STORE_KEY_PARAM = "__workers_response_store";
|
|
9
10
|
const AGE_BASIS_HEADER = "X-Workers-Response-Store-Age-Basis";
|
|
10
11
|
const WARMUP_USER_AGENT = "vinext-cloudflare-cdn-warm";
|
|
11
12
|
const REPLAY_REQUEST_HEADERS = VINEXT_RSC_VARY_HEADER.split(",").map((name) => name.trim().toLowerCase());
|
|
13
|
+
const CACHE_REQUEST_VARY_HEADERS = REPLAY_REQUEST_HEADERS.map((name) => [name, "vinext-keyed"]);
|
|
12
14
|
function stageContext(ctx, env) {
|
|
13
15
|
const assets = Reflect.get(env, "ASSETS");
|
|
14
16
|
return Object.assign(Object.create(Object.getPrototypeOf(ctx)), ctx, {
|
|
@@ -73,34 +75,39 @@ async function invokeResponseStage(request, props, env, ctx, cache, capture) {
|
|
|
73
75
|
const serialized = serializeInvocation(request, props);
|
|
74
76
|
return runWithResponseStoreInvocation(serialized, isReplayableInvocation(request, props), () => handleResponseStage(request, env, context, props, dispatchRequestStage, { cache }), capture);
|
|
75
77
|
}
|
|
76
|
-
function createVinextResponseStoreOptions() {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
78
|
+
function createVinextResponseStoreOptions(configuration) {
|
|
79
|
+
const shards = configuration?.shards;
|
|
80
|
+
if (shards !== void 0 && typeof shards !== "number") throw new TypeError("Workers Response Store shards must be a number");
|
|
81
|
+
return {
|
|
82
|
+
...shards === void 0 ? {} : { shards },
|
|
83
|
+
async regenerate(input, { env, ctx }) {
|
|
84
|
+
if (input.id === ROUTE_REVALIDATOR_ID) {
|
|
85
|
+
const invocation = parseInvocation(input.args.at(-1));
|
|
86
|
+
const response = await invokeResponseStage(restoreRequest(invocation), invocation.props, env, ctx, "shared");
|
|
87
|
+
if (!isCacheable(response)) {
|
|
88
|
+
await response.body?.cancel().catch(() => {});
|
|
89
|
+
throw new Error("Vinext response-stage regeneration was not cacheable");
|
|
90
|
+
}
|
|
91
|
+
return response;
|
|
84
92
|
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
});
|
|
93
|
+
if (input.id === "vinext:cache-function" && typeof input.args[0] === "string" && typeof input.args[1] === "string") {
|
|
94
|
+
const invocation = JSON.parse(input.args[1]);
|
|
95
|
+
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");
|
|
96
|
+
return captureResponseStoreDataRegeneration(input.args[0], async () => {
|
|
97
|
+
const responseStage = await loadVinextResponseStage();
|
|
98
|
+
if (!responseStage.invokeCacheFunction) throw new Error("The vinext response stage cannot invoke cache functions");
|
|
99
|
+
await responseStage.invokeCacheFunction(invocation, env, stageContext(ctx, env), (request) => invokeRequestStage(request, env, ctx));
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
if (input.id === "vinext:data" && typeof input.args[0] === "string") {
|
|
103
|
+
const invocation = parseInvocation(input.args.at(-1));
|
|
104
|
+
return captureResponseStoreDataRegeneration(input.args[0], async () => {
|
|
105
|
+
await (await invokeResponseStage(restoreRequest(invocation), invocation.props, env, ctx, "bypass")).body?.pipeTo(new WritableStream());
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
throw new Error(`Unknown vinext response-store revalidator ${input.id}`);
|
|
101
109
|
}
|
|
102
|
-
|
|
103
|
-
} };
|
|
110
|
+
};
|
|
104
111
|
}
|
|
105
112
|
async function cacheRequest(request, props) {
|
|
106
113
|
const identity = JSON.stringify([
|
|
@@ -111,8 +118,11 @@ async function cacheRequest(request, props) {
|
|
|
111
118
|
]);
|
|
112
119
|
const digest = new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(identity)));
|
|
113
120
|
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, {
|
|
121
|
+
url.searchParams.set(RESPONSE_STORE_KEY_PARAM, `v1.${[...digest].map((byte) => byte.toString(16).padStart(2, "0")).join("")}`);
|
|
122
|
+
return new Request(url, {
|
|
123
|
+
headers: CACHE_REQUEST_VARY_HEADERS,
|
|
124
|
+
method: "GET"
|
|
125
|
+
});
|
|
116
126
|
}
|
|
117
127
|
function isCacheable(response) {
|
|
118
128
|
const policy = response.headers.get("Cloudflare-CDN-Cache-Control") ?? response.headers.get("CDN-Cache-Control") ?? response.headers.get("Cache-Control");
|
|
@@ -121,7 +131,7 @@ function isCacheable(response) {
|
|
|
121
131
|
function isResponseStoreMiss(response) {
|
|
122
132
|
return response.status === 404 && response.headers.get("X-Workers-Response-Store") === "MISS";
|
|
123
133
|
}
|
|
124
|
-
function publicResponse(response, cacheStatus) {
|
|
134
|
+
function publicResponse(response, cacheStatus, responseStageProps) {
|
|
125
135
|
const headers = new Headers(response.headers);
|
|
126
136
|
const publicCacheStatus = cacheStatus === "HIT" && headers.get("CF-Cache-Status") === "UPDATING" ? "UPDATING" : cacheStatus;
|
|
127
137
|
const ageBasis = /^(\d+):(\d+)$/.exec(headers.get(AGE_BASIS_HEADER) ?? "");
|
|
@@ -145,11 +155,11 @@ function publicResponse(response, cacheStatus) {
|
|
|
145
155
|
}
|
|
146
156
|
const cacheControl = headers.get("Cache-Control");
|
|
147
157
|
if (!cacheControl || !isNonCacheableCacheControl(cacheControl)) headers.set("Cache-Control", "private, max-age=0, must-revalidate");
|
|
148
|
-
return new Response(response.body, {
|
|
158
|
+
return traceCachedResponseStart(new Response(response.body, {
|
|
149
159
|
headers,
|
|
150
160
|
status: response.status,
|
|
151
161
|
statusText: response.statusText
|
|
152
|
-
});
|
|
162
|
+
}), publicCacheStatus ?? null, responseStageProps);
|
|
153
163
|
}
|
|
154
164
|
let responseStore;
|
|
155
165
|
function createVinextResponseStoreHandler(store) {
|
|
@@ -161,7 +171,7 @@ const handler = { async fetch(request, env, ctx) {
|
|
|
161
171
|
const context = stageContext(ctx, env);
|
|
162
172
|
const dispatchResponseStage = async (stageRequest, props, options) => {
|
|
163
173
|
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");
|
|
174
|
+
if (options.cache === "bypass" || stageRequest.method !== "GET" && stageRequest.method !== "HEAD") return publicResponse(await invokeResponseStage(stageRequest, props, env, ctx, "bypass"), "BYPASS", props);
|
|
165
175
|
const isWarmup = request.headers.get("user-agent") === WARMUP_USER_AGENT;
|
|
166
176
|
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
177
|
props: {
|
|
@@ -175,11 +185,11 @@ const handler = { async fetch(request, env, ctx) {
|
|
|
175
185
|
const key = await cacheRequest(stageRequest, props);
|
|
176
186
|
const stored = await responseStore.fetch(key);
|
|
177
187
|
if (!isResponseStoreMiss(stored)) {
|
|
178
|
-
if (!rscKey) return publicResponse(stored, "HIT");
|
|
188
|
+
if (!rscKey) return publicResponse(stored, "HIT", props);
|
|
179
189
|
const storedRsc = await responseStore.fetch(rscKey);
|
|
180
190
|
if (!isResponseStoreMiss(storedRsc)) {
|
|
181
191
|
await storedRsc.body?.cancel();
|
|
182
|
-
return publicResponse(stored, "HIT");
|
|
192
|
+
return publicResponse(stored, "HIT", props);
|
|
183
193
|
}
|
|
184
194
|
await Promise.all([stored.body?.cancel(), storedRsc.body?.cancel()]);
|
|
185
195
|
}
|
|
@@ -204,11 +214,11 @@ const handler = { async fetch(request, env, ctx) {
|
|
|
204
214
|
error: error instanceof Error ? error.message : String(error)
|
|
205
215
|
}));
|
|
206
216
|
}));
|
|
207
|
-
return publicResponse(rendered, "MISS");
|
|
217
|
+
return publicResponse(rendered, "MISS", props);
|
|
208
218
|
}
|
|
209
219
|
if (!isCacheable(rendered)) {
|
|
210
220
|
capture?.rscData?.catch(() => {});
|
|
211
|
-
return publicResponse(rendered, "BYPASS");
|
|
221
|
+
return publicResponse(rendered, "BYPASS", props);
|
|
212
222
|
}
|
|
213
223
|
if (rscSeed && !capture?.rscData) {
|
|
214
224
|
await rendered.body?.cancel();
|
|
@@ -245,7 +255,7 @@ const handler = { async fetch(request, env, ctx) {
|
|
|
245
255
|
}
|
|
246
256
|
});
|
|
247
257
|
}
|
|
248
|
-
return publicResponse(new Response(foreground, rendered), "MISS");
|
|
258
|
+
return publicResponse(new Response(foreground, rendered), "MISS", props);
|
|
249
259
|
};
|
|
250
260
|
const { handleRequestStage } = await loadVinextRequestStage();
|
|
251
261
|
return handleRequestStage(request, env, context, dispatchResponseStage);
|
package/dist/cdn-warm.d.ts
CHANGED
|
@@ -33,7 +33,7 @@ export type CdnWarmOptions = {
|
|
|
33
33
|
requireCacheHit?: boolean;
|
|
34
34
|
strict?: boolean;
|
|
35
35
|
/** Cache-admission signal exposed by the deployed adapter. */
|
|
36
|
-
statusSource?: "cloudflare" | "vinext";
|
|
36
|
+
statusSource?: "cloudflare" | "data-cache" | "vinext";
|
|
37
37
|
fetchImpl?: typeof fetch;
|
|
38
38
|
};
|
|
39
39
|
export declare const DEFAULT_CDN_WARM_CONCURRENCY = 25;
|
package/dist/cdn-warm.js
CHANGED
|
@@ -365,6 +365,7 @@ const REUSABLE_CF_CACHE_STATUSES = /* @__PURE__ */ new Set([
|
|
|
365
365
|
"REVALIDATED",
|
|
366
366
|
"UPDATING"
|
|
367
367
|
]);
|
|
368
|
+
const TRANSITIONAL_VINEXT_CACHE_STATUSES = /* @__PURE__ */ new Set(["MISS", "UPDATING"]);
|
|
368
369
|
const NON_CACHEABLE_CF_CACHE_STATUSES = /* @__PURE__ */ new Set(["BYPASS"]);
|
|
369
370
|
const CDN_CACHE_POLICY_HEADERS = [
|
|
370
371
|
"Cloudflare-CDN-Cache-Control",
|
|
@@ -445,20 +446,29 @@ function validateCachePolicy(response, requireCacheStatus, requireCacheHit = fal
|
|
|
445
446
|
error: `CF-Cache-Status is ${cacheStatus}`
|
|
446
447
|
};
|
|
447
448
|
}
|
|
448
|
-
function validateVinextCacheStatus(response) {
|
|
449
|
+
function validateVinextCacheStatus(response, missingStatus = "failed", requireCacheHit = false) {
|
|
449
450
|
const status = response.headers.get(VINEXT_CACHE_HEADER)?.trim().toUpperCase();
|
|
451
|
+
if (requireCacheHit && status !== "HIT") return {
|
|
452
|
+
outcome: "failed",
|
|
453
|
+
error: `${VINEXT_CACHE_HEADER} is ${status ?? "missing"}; the cache fill is not reusable`
|
|
454
|
+
};
|
|
450
455
|
if (status === "MISS" || status === "HIT" || status === "UPDATING") return { outcome: "warmed" };
|
|
451
456
|
if (status === "BYPASS" || status === "DYNAMIC") return {
|
|
452
457
|
outcome: "skipped",
|
|
453
458
|
reason: `${VINEXT_CACHE_HEADER} is ${status}`
|
|
454
459
|
};
|
|
455
|
-
return {
|
|
460
|
+
return missingStatus === "skipped" ? {
|
|
461
|
+
outcome: "skipped",
|
|
462
|
+
reason: `response is missing ${VINEXT_CACHE_HEADER}`
|
|
463
|
+
} : {
|
|
456
464
|
outcome: "failed",
|
|
457
465
|
error: `response is missing ${VINEXT_CACHE_HEADER}`
|
|
458
466
|
};
|
|
459
467
|
}
|
|
460
468
|
function validateWarmAdmission(response, statusSource, requireCacheHit) {
|
|
461
|
-
|
|
469
|
+
if (statusSource === "vinext") return validateVinextCacheStatus(response, "failed", requireCacheHit);
|
|
470
|
+
if (statusSource === "data-cache") return validateVinextCacheStatus(response, "skipped", requireCacheHit);
|
|
471
|
+
return validateCachePolicy(response, true, requireCacheHit);
|
|
462
472
|
}
|
|
463
473
|
function validateBuildIdentity(response, expectedBuildId) {
|
|
464
474
|
if (expectedBuildId !== void 0 && response.headers.get("X-Vinext-Build-Id") !== expectedBuildId) {
|
|
@@ -645,6 +655,14 @@ function shouldRetryValidationFailure(response, target, options) {
|
|
|
645
655
|
if (expectedIdentities.some((matches) => !matches)) return true;
|
|
646
656
|
return isRetryableStatus(response.status, options.retryNotFound);
|
|
647
657
|
}
|
|
658
|
+
function isRetryableCertificationFailure(response, statusSource, retryPropagationFailures) {
|
|
659
|
+
if (statusSource === "cloudflare") {
|
|
660
|
+
const status = response.headers.get("CF-Cache-Status")?.trim().toUpperCase();
|
|
661
|
+
return !REUSABLE_CF_CACHE_STATUSES.has(status ?? "") && (retryPropagationFailures || ADMITTED_CF_CACHE_STATUSES.has(status ?? ""));
|
|
662
|
+
}
|
|
663
|
+
const status = response.headers.get(VINEXT_CACHE_HEADER)?.trim().toUpperCase();
|
|
664
|
+
return TRANSITIONAL_VINEXT_CACHE_STATUSES.has(status ?? "");
|
|
665
|
+
}
|
|
648
666
|
async function warmOnePath(target, options) {
|
|
649
667
|
const url = buildWarmupUrl(options.targetUrl, target.pathname);
|
|
650
668
|
let lastError = "request failed before the first attempt";
|
|
@@ -707,8 +725,7 @@ async function warmOnePath(target, options) {
|
|
|
707
725
|
}
|
|
708
726
|
lastSkippedReason = null;
|
|
709
727
|
lastError = validation.error;
|
|
710
|
-
|
|
711
|
-
lastRetryable = options.requireCacheHit && ADMITTED_CF_CACHE_STATUSES.has(cacheStatus ?? "") && !REUSABLE_CF_CACHE_STATUSES.has(cacheStatus ?? "") || shouldRetryValidationFailure(response, target, options);
|
|
728
|
+
lastRetryable = options.requireCacheHit && isRetryableCertificationFailure(response, options.statusSource, options.retryPropagationFailures) || shouldRetryValidationFailure(response, target, options);
|
|
712
729
|
if (!lastRetryable) break;
|
|
713
730
|
if (!canRetry(attempt)) break;
|
|
714
731
|
if (!await waitBeforeRetry()) return {
|
|
@@ -745,8 +762,7 @@ async function warmOnePath(target, options) {
|
|
|
745
762
|
}
|
|
746
763
|
lastSkippedReason = null;
|
|
747
764
|
lastError = validation.error;
|
|
748
|
-
|
|
749
|
-
lastRetryable = options.requireCacheHit && ADMITTED_CF_CACHE_STATUSES.has(cacheStatus ?? "") && !REUSABLE_CF_CACHE_STATUSES.has(cacheStatus ?? "") || shouldRetryValidationFailure(response, target, options);
|
|
765
|
+
lastRetryable = options.requireCacheHit && isRetryableCertificationFailure(response, options.statusSource, options.retryPropagationFailures) || shouldRetryValidationFailure(response, target, options);
|
|
750
766
|
if (!lastRetryable) break;
|
|
751
767
|
} catch (error) {
|
|
752
768
|
lastSkippedReason = null;
|
package/dist/deploy-help.js
CHANGED
|
@@ -19,6 +19,8 @@ function formatDeployHelp() {
|
|
|
19
19
|
--skip-build Skip the build step (use existing dist/)
|
|
20
20
|
--dry-run Validate setup without building or deploying
|
|
21
21
|
--verbose Print raw output from internal Wrangler commands
|
|
22
|
+
--no-promote Do not promote the uploaded Worker version to 100%
|
|
23
|
+
traffic
|
|
22
24
|
--prerender-all Pre-render discovered routes after building (future
|
|
23
25
|
releases will auto-populate the remote cache)
|
|
24
26
|
--prerender-concurrency <count>
|
|
@@ -60,8 +62,6 @@ function formatDeployHelp() {
|
|
|
60
62
|
--dangerously-promote-on-cdn-warm-error
|
|
61
63
|
Promote even when ordinary staged warmup cannot be
|
|
62
64
|
verified (never bypasses --warm-cdn-certify)
|
|
63
|
-
--warm-cdn-no-promote Leave the warmed Worker version staged at 0% traffic;
|
|
64
|
-
production triggers are still applied before warming
|
|
65
65
|
--warm-cdn-promotion-delay <ms>
|
|
66
66
|
Delay before promotion after warmup (default: 15000)
|
|
67
67
|
--warm-cdn-include-fallbacks
|
|
@@ -69,16 +69,18 @@ function formatDeployHelp() {
|
|
|
69
69
|
-h, --help Show this help
|
|
70
70
|
|
|
71
71
|
Experimental:
|
|
72
|
-
--experimental-
|
|
73
|
-
|
|
74
|
-
--
|
|
75
|
-
--
|
|
72
|
+
--experimental-traffic-aware-warm-cache
|
|
73
|
+
Select CDN pre-warm routes from traffic
|
|
74
|
+
--traffic-aware-coverage <pct> Traffic coverage target, 0-100 (default: 90)
|
|
75
|
+
--traffic-aware-limit <count> Hard cap on selected routes (default: 1000)
|
|
76
|
+
--traffic-aware-window <hours> Analytics lookback window in hours (default: 24)
|
|
76
77
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
78
|
+
Traffic-aware warming uses Cloudflare zone analytics to select the
|
|
79
|
+
highest-traffic routes, then feeds those routes into the same staged CDN
|
|
80
|
+
pre-warming flow used by --experimental-warm-cdn-cache. It requires a custom
|
|
81
|
+
domain and a CLOUDFLARE_API_TOKEN with Zone Analytics read permission.
|
|
82
|
+
|
|
83
|
+
Legacy --experimental-tpr and --tpr-* aliases remain supported.
|
|
82
84
|
|
|
83
85
|
Workers Cache automatically uses tiered caching. Warmed entries can therefore
|
|
84
86
|
be reused outside the data center reached by the warmup request after cache
|
|
@@ -93,12 +95,16 @@ function formatDeployHelp() {
|
|
|
93
95
|
vinext-cloudflare deploy --config dist/server/wrangler.json Deploy using a generated Wrangler config
|
|
94
96
|
vinext-cloudflare deploy --dry-run Validate setup without building or deploying
|
|
95
97
|
vinext-cloudflare deploy --name my-app Deploy with a custom Worker name
|
|
98
|
+
vinext-cloudflare deploy --no-promote Upload a version without changing deployment traffic
|
|
96
99
|
vinext-cloudflare deploy --experimental-warm-cdn-cache Warm build-discovered paths during version deploy (experimental)
|
|
97
100
|
vinext-cloudflare deploy --experimental-warm-cdn-cache --warm-cdn-target https://example.com
|
|
98
101
|
Warm an explicit production origin
|
|
99
|
-
vinext-cloudflare deploy --experimental-
|
|
100
|
-
|
|
101
|
-
vinext-cloudflare deploy --experimental-
|
|
102
|
+
vinext-cloudflare deploy --experimental-traffic-aware-warm-cache
|
|
103
|
+
Enable traffic-aware warming
|
|
104
|
+
vinext-cloudflare deploy --experimental-traffic-aware-warm-cache --traffic-aware-coverage 95
|
|
105
|
+
Cover 95% of traffic
|
|
106
|
+
vinext-cloudflare deploy --experimental-traffic-aware-warm-cache --traffic-aware-limit 500
|
|
107
|
+
Cap at 500 routes
|
|
102
108
|
`;
|
|
103
109
|
}
|
|
104
110
|
function printDeployHelp() {
|
package/dist/deploy.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { CdnWarmOptions, CdnWarmRequestPlan, PrerenderWarmPlan } from "./cdn-warm.js";
|
|
2
|
-
import { parseWranglerConfig } from "./
|
|
2
|
+
import { parseWranglerConfig } from "./wrangler-config.js";
|
|
3
|
+
import { TrafficEntry } from "./tpr.js";
|
|
3
4
|
import { WranglerDeploymentStatus, WranglerVersionTraffic } from "./version-deploy.js";
|
|
4
5
|
import { KVBulkPair } from "./prerender-kv-populate.js";
|
|
5
6
|
import { spawn } from "node:child_process";
|
|
@@ -58,17 +59,17 @@ export type DeployOptions = {
|
|
|
58
59
|
warmCdnReadinessProbeDelay?: number;
|
|
59
60
|
/** Promote even when staged CDN warmup cannot be completed */
|
|
60
61
|
dangerouslyPromoteOnCdnWarmError?: boolean;
|
|
61
|
-
/** Promote the
|
|
62
|
+
/** Promote the uploaded Worker version to 100% traffic (default: true) */
|
|
62
63
|
warmCdnPromote?: boolean;
|
|
63
64
|
/** Delay between successful warmup and promotion in milliseconds */
|
|
64
65
|
warmCdnPromotionDelay?: number;
|
|
65
66
|
/** Include PPR fallback-shell placeholder paths during CDN warmup */
|
|
66
67
|
warmCdnIncludeFallbacks?: boolean;
|
|
67
|
-
/**
|
|
68
|
+
/** Select CDN pre-warm routes using traffic analytics */
|
|
68
69
|
experimentalTPR?: boolean;
|
|
69
70
|
/** TPR: traffic coverage percentage target (0–100, default: 90) */
|
|
70
71
|
tprCoverage?: number;
|
|
71
|
-
/** TPR: hard cap on
|
|
72
|
+
/** TPR: hard cap on selected routes (default: 1000) */
|
|
72
73
|
tprLimit?: number;
|
|
73
74
|
/** TPR: analytics lookback window in hours (default: 24) */
|
|
74
75
|
tprWindow?: number;
|
|
@@ -157,18 +158,25 @@ export declare function runWranglerKVBulkPut(root: string, options: {
|
|
|
157
158
|
pairs: KVBulkPair[];
|
|
158
159
|
tempDir?: string;
|
|
159
160
|
}, execute?: typeof spawn, nodeExecutable?: string): Promise<void>;
|
|
160
|
-
export declare function runWranglerDeploy(root: string, options: Pick<DeployOptions, "preview" | "env" | "name" | "config"
|
|
161
|
+
export declare function runWranglerDeploy(root: string, options: Pick<DeployOptions, "preview" | "env" | "name" | "config" | "verbose"> & {
|
|
162
|
+
promote?: boolean;
|
|
163
|
+
}, execute?: typeof spawn): Promise<string>;
|
|
161
164
|
export declare function hasCdnWarmRequests(plan: Omit<CdnWarmRequestPlan, "pagesDataPaths"> & {
|
|
162
165
|
pagesDataPaths?: readonly string[];
|
|
163
166
|
}): boolean;
|
|
167
|
+
export declare function selectTPRWarmPlan(plan: PrerenderWarmPlan, traffic: readonly TrafficEntry[], coverage: number, limit: number): PrerenderWarmPlan;
|
|
164
168
|
export declare function projectRequiresRouteCacheabilityProbeManifest(project: Pick<ProjectInfo, "isAppRouter" | "isPagesRouter">, cacheConfig: VinextCacheConfig | null): boolean;
|
|
165
169
|
type CdnWarmDeployOptions = Pick<DeployOptions, "preview" | "env" | "name" | "config" | "verbose" | "warmCdnTarget" | "warmCdnConcurrency" | "warmCdnTimeout" | "warmCdnRetries" | "warmCdnDiscoveryTimeout" | "warmCdnDiscoveryRetries" | "warmCdnProbeTimeout" | "warmCdnProbeRetries" | "warmCdnCertify" | "warmCdnReadinessTimeout" | "warmCdnReadinessRetries" | "warmCdnReadinessProbes" | "warmCdnReadinessProbeDelay" | "dangerouslyPromoteOnCdnWarmError" | "warmCdnPromote" | "warmCdnPromotionDelay"> & Pick<CdnWarmOptions, "deploymentId" | "expectedBuildId" | "expectedRscBuildId" | "loadingShellPaths" | "pagesDataPaths" | "routeHandlerPaths" | "routePatterns" | "rscPaths" | "statusSource"> & {
|
|
170
|
+
/** Allow optional route selection to discover no warmable requests. */
|
|
171
|
+
allowEmptyWarmPlan?: boolean;
|
|
166
172
|
/** Probe a staged Worker and upload the resulting manifest as a second version. */
|
|
167
173
|
cacheabilityProbe?: boolean;
|
|
168
174
|
discoverWarmPlan?: (target: {
|
|
169
175
|
headers?: HeadersInit;
|
|
170
176
|
targetUrl: string;
|
|
171
177
|
}) => Promise<PrerenderWarmPlan>;
|
|
178
|
+
/** Narrow the final warm requests without changing discovery or probe metadata. */
|
|
179
|
+
selectWarmPlan?: (plan: PrerenderWarmPlan) => PrerenderWarmPlan;
|
|
172
180
|
};
|
|
173
181
|
export declare function deployWithCdnWarmup(root: string, paths: readonly string[], options: CdnWarmDeployOptions): Promise<string>;
|
|
174
182
|
export declare function resolveCdnWarmupTargetUrl(root: string, deployedUrl: string | null): string | null;
|