@vinext/cloudflare 1.0.0 → 1.1.0
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/dist/cache/browser-cache-policy.d.ts +7 -0
- package/dist/cache/browser-cache-policy.js +39 -0
- package/dist/cache/cdn-adapter.runtime.d.ts +6 -2
- package/dist/cache/cdn-adapter.runtime.js +30 -20
- package/dist/cache/cdn-adapter.worker.d.ts +2 -1
- package/dist/cache/cdn-adapter.worker.js +69 -48
- package/dist/cache/kv-data-adapter.d.ts +5 -1
- package/dist/cache/kv-data-adapter.runtime.js +5 -7
- package/dist/cache/kv-key.js +1 -1
- package/dist/cache/response-store-adapter.worker.js +42 -16
- package/dist/cache/response-store-data.runtime.d.ts +1 -0
- package/dist/cache/response-store-data.runtime.js +29 -9
- package/dist/cache/static-assets-adapter.build.d.ts +1 -1
- package/dist/cache/static-assets-adapter.build.js +45 -7
- package/dist/cache/static-assets-adapter.d.ts +1 -1
- package/dist/cache/static-assets-adapter.js +1 -1
- package/dist/cache/static-assets-adapter.runtime.d.ts +2 -1
- package/dist/cache/static-assets-adapter.runtime.js +22 -5
- package/dist/cache/static-assets-adapter.shared.d.ts +1 -1
- package/dist/cache/workers-cache-cdn-adapter.d.ts +4 -4
- package/dist/cache/workers-cache-cdn-adapter.js +4 -4
- package/dist/cache/workers-cache-invalidation.d.ts +30 -0
- package/dist/cache/workers-cache-invalidation.js +27 -0
- package/dist/cdn-warm.js +3 -3
- package/package.json +7 -7
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
//#region src/cache/browser-cache-policy.d.ts
|
|
2
|
+
export type SharedResponseStage = {
|
|
3
|
+
headers: Headers;
|
|
4
|
+
};
|
|
5
|
+
export declare const SHARED_RESPONSE_STAGE_HEADER = "x-vinext-cloudflare-shared-response-stage";
|
|
6
|
+
export declare function finalizeGatewayResponse(response: Response, sharedResponses: Map<string, SharedResponseStage>, cacheStatusHeader?: string): Response;
|
|
7
|
+
//#endregion
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { NEXTJS_CACHE_HEADER, VINEXT_CACHE_HEADER } from "vinext/internal/server/headers";
|
|
2
|
+
//#region src/cache/browser-cache-policy.ts
|
|
3
|
+
const CLOUDFLARE_EDGE_POLICY_HEADER = "Cloudflare-CDN-Cache-Control";
|
|
4
|
+
const SHARED_RESPONSE_STAGE_HEADER = "x-vinext-cloudflare-shared-response-stage";
|
|
5
|
+
const PRIVATE_RESPONSE_HEADERS = /* @__PURE__ */ new Set([
|
|
6
|
+
SHARED_RESPONSE_STAGE_HEADER,
|
|
7
|
+
CLOUDFLARE_EDGE_POLICY_HEADER.toLowerCase(),
|
|
8
|
+
"cdn-cache-control",
|
|
9
|
+
"cache-tag",
|
|
10
|
+
VINEXT_CACHE_HEADER.toLowerCase(),
|
|
11
|
+
NEXTJS_CACHE_HEADER.toLowerCase()
|
|
12
|
+
]);
|
|
13
|
+
function finalizeGatewayResponse(response, sharedResponses, cacheStatusHeader = "CF-Cache-Status") {
|
|
14
|
+
const provenance = response.headers.get(SHARED_RESPONSE_STAGE_HEADER);
|
|
15
|
+
const sharedResponse = provenance === null ? void 0 : sharedResponses.get(provenance);
|
|
16
|
+
const usedSharedResponseStage = sharedResponse !== void 0;
|
|
17
|
+
const sharedResponseStageCollision = provenance !== null && !usedSharedResponseStage;
|
|
18
|
+
if (!response.headers.has(CLOUDFLARE_EDGE_POLICY_HEADER) && !usedSharedResponseStage && !sharedResponseStageCollision) return response;
|
|
19
|
+
const headers = new Headers(response.headers);
|
|
20
|
+
headers.delete(SHARED_RESPONSE_STAGE_HEADER);
|
|
21
|
+
headers.delete(CLOUDFLARE_EDGE_POLICY_HEADER);
|
|
22
|
+
if (usedSharedResponseStage || sharedResponseStageCollision) {
|
|
23
|
+
for (const name of PRIVATE_RESPONSE_HEADERS) headers.delete(name);
|
|
24
|
+
if (sharedResponseStageCollision) headers.set("Cache-Control", "no-store");
|
|
25
|
+
else if (!headers.has("Cache-Control")) headers.set("Cache-Control", "private, max-age=0, must-revalidate");
|
|
26
|
+
}
|
|
27
|
+
const cacheStatus = sharedResponse?.headers.get(cacheStatusHeader);
|
|
28
|
+
if (cacheStatus) {
|
|
29
|
+
headers.set(VINEXT_CACHE_HEADER, cacheStatus);
|
|
30
|
+
headers.set(NEXTJS_CACHE_HEADER, cacheStatus);
|
|
31
|
+
}
|
|
32
|
+
return new Response(response.body, {
|
|
33
|
+
headers,
|
|
34
|
+
status: response.status,
|
|
35
|
+
statusText: response.statusText
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
//#endregion
|
|
39
|
+
export { SHARED_RESPONSE_STAGE_HEADER, finalizeGatewayResponse };
|
|
@@ -32,8 +32,12 @@ export declare class CloudflareCdnCacheAdapter implements CdnCacheAdapter {
|
|
|
32
32
|
set(_key: string, _data: IncrementalCacheValue | null, _ctx?: Record<string, unknown>): Promise<void>;
|
|
33
33
|
buildResponseIdentityHeaders(): CdnResponseHeaders;
|
|
34
34
|
buildResponseHeaders(input: CdnCacheableHeaderInput): CdnResponseHeaders;
|
|
35
|
-
/**
|
|
36
|
-
|
|
35
|
+
/**
|
|
36
|
+
* Invalidate edge-cached responses by tag via the request context's cache.
|
|
37
|
+
* Stale tags map to `cache.invalidate`, so the edge serves the stale response
|
|
38
|
+
* while it refetches; expired tags map to `cache.purge`.
|
|
39
|
+
*/
|
|
40
|
+
revalidateTag(tags: string | string[], durations?: {
|
|
37
41
|
expire?: number;
|
|
38
42
|
}): Promise<void>;
|
|
39
43
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { VINEXT_CDN_BUILD_ID_HEADER, getVinextCdnBuildIdentity } from "./cdn-build-id.js";
|
|
2
2
|
import { VINEXT_EXPECTED_WORKER_VERSION_HEADER } from "../version-headers.js";
|
|
3
|
-
import {
|
|
3
|
+
import { invalidateOrPurge, isStaleTagInvalidation } from "./workers-cache-invalidation.js";
|
|
4
|
+
import { isNonCacheableCacheControl, splitCacheControlDirectives } from "vinext/shims/cdn-cache";
|
|
4
5
|
import { getRequestExecutionContext } from "vinext/shims/request-context";
|
|
5
6
|
import { fnv1a64 } from "vinext/internal/utils/hash";
|
|
6
7
|
//#region src/cache/cdn-adapter.runtime.ts
|
|
@@ -19,17 +20,18 @@ import { fnv1a64 } from "vinext/internal/utils/hash";
|
|
|
19
20
|
* cache headers, so there is nothing to persist at the origin.
|
|
20
21
|
* - `buildResponseHeaders` emits the SWR policy as `Cloudflare-CDN-Cache-Control`
|
|
21
22
|
* (`public, max-age=…, stale-while-revalidate=…`) so the edge caches and
|
|
22
|
-
* revalidates.
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
* `private, max-age=0, must-revalidate` before public egress so personalized
|
|
26
|
-
* request-stage headers cannot enter another shared cache. A `Cache-Tag`
|
|
23
|
+
* revalidates. Explicit endpoint Cache-Control is preserved for clients,
|
|
24
|
+
* independently of shared admission. Framework-managed responses default to
|
|
25
|
+
* `private, max-age=0, must-revalidate`. A `Cache-Tag`
|
|
27
26
|
* header lets entries be purged by tag. Note the edge directive uses `max-age`
|
|
28
27
|
* (not `s-maxage`):
|
|
29
28
|
* the framework computes the policy with `s-maxage` for shared caches, but
|
|
30
29
|
* `Cloudflare-CDN-Cache-Control` is already CDN-scoped so `max-age` is the correct knob
|
|
31
30
|
* for the edge to honor max-age + stale-while-revalidate.
|
|
32
31
|
* - `revalidateTag` purges the edge via the request context's `cache.purge({ tags })`.
|
|
32
|
+
* A stale-while-revalidate invalidation (`revalidateTag` with durations
|
|
33
|
+
* other than `expire: 0`) uses `cache.invalidate({ tags })` instead, so the
|
|
34
|
+
* edge keeps serving the stale response while it refetches in the background.
|
|
33
35
|
*
|
|
34
36
|
* Tags use fixed-size lowercase digests before emission and purge because
|
|
35
37
|
* Workers Cache tags are case-insensitive printable ASCII, while Next.js tags
|
|
@@ -54,13 +56,13 @@ function getBuildIdentityResponseHeader() {
|
|
|
54
56
|
const buildId = getVinextCdnBuildIdentity();
|
|
55
57
|
return buildId ? { [VINEXT_CDN_BUILD_ID_HEADER]: buildId } : {};
|
|
56
58
|
}
|
|
57
|
-
/**
|
|
58
|
-
function clearCloudflareCdnResponseHeaders(cacheControl) {
|
|
59
|
+
/** Disable shared caching while preserving an explicit client policy. */
|
|
60
|
+
function clearCloudflareCdnResponseHeaders(cacheControl, browserCacheControl) {
|
|
59
61
|
return {
|
|
60
62
|
...getBuildIdentityResponseHeader(),
|
|
61
|
-
"Cache-Control": cacheControl,
|
|
63
|
+
"Cache-Control": browserCacheControl ?? cacheControl,
|
|
62
64
|
"CDN-Cache-Control": null,
|
|
63
|
-
"Cloudflare-CDN-Cache-Control": null,
|
|
65
|
+
"Cloudflare-CDN-Cache-Control": browserCacheControl !== void 0 && !isNonCacheableCacheControl(browserCacheControl) ? "no-store" : null,
|
|
64
66
|
"Cache-Tag": null
|
|
65
67
|
};
|
|
66
68
|
}
|
|
@@ -99,7 +101,7 @@ const NO_STORE = "no-store";
|
|
|
99
101
|
* revalidate (against the edge) rather than serving a stored copy — so the user
|
|
100
102
|
* always sees edge-fresh content while still permitting conditional 304s.
|
|
101
103
|
*/
|
|
102
|
-
const BROWSER_REVALIDATE = "
|
|
104
|
+
const BROWSER_REVALIDATE = "private, max-age=0, must-revalidate";
|
|
103
105
|
/**
|
|
104
106
|
* A concrete stale window (1 year) substituted for a value-less
|
|
105
107
|
* `stale-while-revalidate`. The framework emits a bare `stale-while-revalidate`
|
|
@@ -119,8 +121,10 @@ const UNBOUNDED_SWR_SECONDS = 31536e3;
|
|
|
119
121
|
* `public`.
|
|
120
122
|
*/
|
|
121
123
|
function toEdgeCacheControl(cacheControl) {
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
+
const directives = splitCacheControlDirectives(cacheControl);
|
|
125
|
+
const hasSharedMaxAge = directives.some((directive) => /^s-maxage\s*=/i.test(directive));
|
|
126
|
+
const withMaxAge = directives.filter((directive) => !hasSharedMaxAge || !/^max-age\s*=/i.test(directive)).map((directive) => directive.replace(/^s-maxage\s*=/i, "max-age=").replace(/^stale-while-revalidate$/i, `stale-while-revalidate=${UNBOUNDED_SWR_SECONDS}`)).join(", ");
|
|
127
|
+
return directives.some((directive) => /^public$/i.test(directive)) ? withMaxAge : `public, ${withMaxAge}`;
|
|
124
128
|
}
|
|
125
129
|
/**
|
|
126
130
|
* Cloudflare's `Cache-Tag` header budget is 16 KB total. Fixed-size digests let
|
|
@@ -197,27 +201,33 @@ var CloudflareCdnCacheAdapter = class {
|
|
|
197
201
|
buildResponseHeaders(input) {
|
|
198
202
|
if (input.pendingDynamicCheck) return clearCloudflareCdnResponseHeaders(NO_STORE);
|
|
199
203
|
if (!input.cacheControl) return clearCloudflareCdnResponseHeaders(NO_STORE);
|
|
200
|
-
if (isNonCacheableCacheControl(input.cacheControl)) return clearCloudflareCdnResponseHeaders(input.cacheControl);
|
|
204
|
+
if (isNonCacheableCacheControl(input.cacheControl)) return clearCloudflareCdnResponseHeaders(input.cacheControl, input.browserCacheControl);
|
|
201
205
|
const cacheTag = input.tags?.length ? formatCacheTag(input.tags) : null;
|
|
202
|
-
if (input.tags?.length && !cacheTag) return clearCloudflareCdnResponseHeaders(NO_STORE);
|
|
206
|
+
if (input.tags?.length && !cacheTag) return clearCloudflareCdnResponseHeaders(NO_STORE, input.browserCacheControl);
|
|
203
207
|
return {
|
|
204
208
|
...getBuildIdentityResponseHeader(),
|
|
205
|
-
"Cache-Control": BROWSER_REVALIDATE,
|
|
209
|
+
"Cache-Control": input.browserCacheControl ?? BROWSER_REVALIDATE,
|
|
206
210
|
"CDN-Cache-Control": null,
|
|
207
211
|
"Cloudflare-CDN-Cache-Control": toEdgeCacheControl(input.cacheControl),
|
|
208
212
|
"Cache-Tag": cacheTag
|
|
209
213
|
};
|
|
210
214
|
}
|
|
211
|
-
/**
|
|
212
|
-
|
|
215
|
+
/**
|
|
216
|
+
* Invalidate edge-cached responses by tag via the request context's cache.
|
|
217
|
+
* Stale tags map to `cache.invalidate`, so the edge serves the stale response
|
|
218
|
+
* while it refetches; expired tags map to `cache.purge`.
|
|
219
|
+
*/
|
|
220
|
+
async revalidateTag(tags, durations) {
|
|
213
221
|
const cache = getWorkersCache();
|
|
214
222
|
if (!cache) return;
|
|
215
223
|
const tagList = (Array.isArray(tags) ? tags : [tags]).filter((t) => typeof t === "string");
|
|
216
224
|
if (tagList.length === 0) return;
|
|
217
|
-
const
|
|
225
|
+
const options = { tags: tagList.map(encodeCloudflareCacheTag) };
|
|
226
|
+
const operation = isStaleTagInvalidation(durations) ? "invalidate" : "purge";
|
|
227
|
+
const result = await (operation === "invalidate" ? invalidateOrPurge(cache, options) : cache.purge(options));
|
|
218
228
|
if (result?.success === false) {
|
|
219
229
|
const errors = result.errors.map(({ code, message }) => `${code}: ${message}`).join(", ");
|
|
220
|
-
throw new Error(`[vinext] Workers Cache
|
|
230
|
+
throw new Error(`[vinext] Workers Cache ${operation} failed${errors ? `: ${errors}` : ""}`);
|
|
221
231
|
}
|
|
222
232
|
}
|
|
223
233
|
};
|
|
@@ -18,8 +18,9 @@ type CachePurgeOptions = {
|
|
|
18
18
|
export declare class VinextCachedResponse extends WorkerEntrypoint<unknown, unknown> {
|
|
19
19
|
fetch(request: Request): Promise<Response>;
|
|
20
20
|
purge(options: CachePurgeOptions): Promise<unknown>;
|
|
21
|
+
invalidate(options: CachePurgeOptions): Promise<unknown>;
|
|
21
22
|
}
|
|
22
|
-
/** Uncached response entrypoint
|
|
23
|
+
/** Uncached response entrypoint retained for deployment readiness probes. */
|
|
23
24
|
export declare class VinextUncachedResponse extends WorkerEntrypoint<unknown, unknown> {
|
|
24
25
|
fetch(request: Request): Promise<Response>;
|
|
25
26
|
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { VINEXT_CDN_BUILD_ID_HEADER, getVinextCdnBuildIdentity } from "./cdn-build-id.js";
|
|
2
|
-
import {
|
|
2
|
+
import { SHARED_RESPONSE_STAGE_HEADER, finalizeGatewayResponse } from "./browser-cache-policy.js";
|
|
3
|
+
import { hasCacheMethod, invalidateOrPurge } from "./workers-cache-invalidation.js";
|
|
4
|
+
import { VINEXT_PRERENDER_READINESS_PATH, VINEXT_RSC_VARY_HEADER } from "vinext/internal/server/headers";
|
|
3
5
|
import { isNonCacheableCacheControl } from "vinext/shims/cdn-cache";
|
|
4
6
|
import { WorkerEntrypoint } from "cloudflare:workers";
|
|
5
7
|
import "vinext/internal/server/cloudflare-workers-tracing";
|
|
@@ -13,8 +15,7 @@ const AUTHORIZATION_TRANSPORT_HEADER = "x-vinext-internal-authorization";
|
|
|
13
15
|
const REQUEST_CACHE_CONTROL_TRANSPORT_HEADER = "x-vinext-internal-request-cache-control";
|
|
14
16
|
const REQUEST_CF_TRANSPORT_HEADER = "x-vinext-internal-request-cf";
|
|
15
17
|
const REQUEST_PRAGMA_TRANSPORT_HEADER = "x-vinext-internal-request-pragma";
|
|
16
|
-
const
|
|
17
|
-
const SHARED_RESPONSE_STAGE_HEADER = "x-vinext-cloudflare-shared-response-stage";
|
|
18
|
+
const WARMUP_USER_AGENT = "vinext-cloudflare-cdn-warm";
|
|
18
19
|
const RESPONSE_STAGE_WIRE_CACHE = {
|
|
19
20
|
bypass: "vinext-cloudflare-v1:bypass",
|
|
20
21
|
shared: "vinext-cloudflare-v1:shared"
|
|
@@ -93,9 +94,6 @@ function withWorkerHostRuntime(context, env) {
|
|
|
93
94
|
function hasFetch(value) {
|
|
94
95
|
return value !== null && (typeof value === "object" || typeof value === "function") && "fetch" in value && typeof value.fetch === "function";
|
|
95
96
|
}
|
|
96
|
-
function hasPurge(value) {
|
|
97
|
-
return value !== null && (typeof value === "object" || typeof value === "function") && "purge" in value && typeof value.purge === "function";
|
|
98
|
-
}
|
|
99
97
|
function getResponseStageBinding(context, exportName, serializedInvocation) {
|
|
100
98
|
const binding = context.exports?.[exportName];
|
|
101
99
|
if (typeof binding !== "function") return null;
|
|
@@ -181,6 +179,31 @@ function restoreResponseStageRequest(request, requestUrl, requestMethod) {
|
|
|
181
179
|
request: restored
|
|
182
180
|
};
|
|
183
181
|
}
|
|
182
|
+
function isAppPageDocumentInvocation(invocation) {
|
|
183
|
+
const props = invocation.props;
|
|
184
|
+
return props !== null && typeof props === "object" && Reflect.get(props, "kind") === "app-page" && Reflect.get(props, "isRscRequest") === false;
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* No client waits on a deployment warm-up. Workers Cache revalidates an entry
|
|
188
|
+
* that has a validator with a conditional request, as it does the Response
|
|
189
|
+
* Store's, and serves the stale entry meanwhile or fills an expired one.
|
|
190
|
+
*/
|
|
191
|
+
function rendersWholeDocument(request) {
|
|
192
|
+
return request.headers.get("user-agent") === WARMUP_USER_AGENT || request.headers.has("If-None-Match") || request.headers.has("If-Modified-Since");
|
|
193
|
+
}
|
|
194
|
+
/** Give a cacheable document a validator, so Workers Cache revalidates it conditionally. */
|
|
195
|
+
function withRevalidationValidator(response) {
|
|
196
|
+
if (response.headers.has("ETag") || response.headers.has("Last-Modified")) return response;
|
|
197
|
+
const policy = response.headers.get("Cloudflare-CDN-Cache-Control") ?? response.headers.get("CDN-Cache-Control") ?? response.headers.get("Cache-Control");
|
|
198
|
+
if (policy === null || isNonCacheableCacheControl(policy)) return response;
|
|
199
|
+
const headers = new Headers(response.headers);
|
|
200
|
+
headers.set("Last-Modified", (/* @__PURE__ */ new Date()).toUTCString());
|
|
201
|
+
return new Response(response.body, {
|
|
202
|
+
headers,
|
|
203
|
+
status: response.status,
|
|
204
|
+
statusText: response.statusText
|
|
205
|
+
});
|
|
206
|
+
}
|
|
184
207
|
function preventResponseCaching(response) {
|
|
185
208
|
const headers = new Headers(response.headers);
|
|
186
209
|
headers.set("Cache-Control", "no-store");
|
|
@@ -209,48 +232,32 @@ function markSharedResponseStage(response, provenanceToken, responseStageProps,
|
|
|
209
232
|
statusText: response.statusText
|
|
210
233
|
}), cacheStatus, responseStageProps);
|
|
211
234
|
}
|
|
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
235
|
function hasTaggedCustomVary(response) {
|
|
240
236
|
if (!response.headers.get("Cache-Tag")) return false;
|
|
241
237
|
return (response.headers.get("Vary") ?? "").split(",").map((name) => name.trim().toLowerCase()).filter(Boolean).some((name) => !FRAMEWORK_RESPONSE_VARY_FIELDS.has(name));
|
|
242
238
|
}
|
|
243
|
-
|
|
239
|
+
/**
|
|
240
|
+
* Workers Cache purges and invalidations only reach the calling entrypoint's
|
|
241
|
+
* cache, so route both through the cache-bearing response entrypoint.
|
|
242
|
+
*/
|
|
243
|
+
function withResponseStageCache(context) {
|
|
244
244
|
const factory = context.exports?.[CACHED_RESPONSE_STAGE_EXPORT];
|
|
245
245
|
if (typeof factory !== "function") return context;
|
|
246
246
|
const fallback = context.cache;
|
|
247
247
|
return {
|
|
248
248
|
...context,
|
|
249
|
-
cache: {
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
249
|
+
cache: {
|
|
250
|
+
purge(options) {
|
|
251
|
+
const target = factory({ props: {} });
|
|
252
|
+
if (hasCacheMethod(target, "purge")) return target.purge(options);
|
|
253
|
+
return hasCacheMethod(fallback, "purge") ? fallback.purge(options) : void 0;
|
|
254
|
+
},
|
|
255
|
+
invalidate(options) {
|
|
256
|
+
const target = factory({ props: {} });
|
|
257
|
+
const targetCanUpdate = hasCacheMethod(target, "invalidate") || hasCacheMethod(target, "purge");
|
|
258
|
+
return invalidateOrPurge(targetCanUpdate ? target : fallback, options);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
254
261
|
};
|
|
255
262
|
}
|
|
256
263
|
function getResponseStageInvocation(value, expectedCache) {
|
|
@@ -311,19 +318,29 @@ var VinextCachedResponse = class extends WorkerEntrypoint {
|
|
|
311
318
|
}));
|
|
312
319
|
if (invocation.expectedResponseStageBuildIdentity !== void 0 && invocation.expectedResponseStageBuildIdentity !== getVinextCdnBuildIdentity()) return stampResponseStageBuildIdentity(responseStageUnavailable());
|
|
313
320
|
const restored = restoreResponseStageRequest(request, invocation.requestUrl, invocation.requestMethod);
|
|
314
|
-
const
|
|
315
|
-
|
|
321
|
+
const isAppPageDocument = isAppPageDocumentInvocation(invocation);
|
|
322
|
+
const response = await invokeResponseStage(restored.request, this.env, context, isAppPageDocument && rendersWholeDocument(request) ? {
|
|
323
|
+
...invocation,
|
|
324
|
+
options: {
|
|
325
|
+
...invocation.options,
|
|
326
|
+
renderWholeDocument: true
|
|
327
|
+
}
|
|
328
|
+
} : invocation);
|
|
329
|
+
return stampResponseStageBuildIdentity(restored.didAccessRequestCf() || hasTaggedCustomVary(response) ? preventResponseCaching(response) : isAppPageDocument ? withRevalidationValidator(response) : response);
|
|
316
330
|
}
|
|
317
331
|
async purge(options) {
|
|
318
332
|
const cache = Reflect.get(this.ctx, "cache");
|
|
319
|
-
if (!
|
|
333
|
+
if (!hasCacheMethod(cache, "purge")) return void 0;
|
|
320
334
|
return cache.purge(options);
|
|
321
335
|
}
|
|
336
|
+
async invalidate(options) {
|
|
337
|
+
return invalidateOrPurge(Reflect.get(this.ctx, "cache"), options);
|
|
338
|
+
}
|
|
322
339
|
};
|
|
323
|
-
/** Uncached response entrypoint
|
|
340
|
+
/** Uncached response entrypoint retained for deployment readiness probes. */
|
|
324
341
|
var VinextUncachedResponse = class extends WorkerEntrypoint {
|
|
325
342
|
async fetch(request) {
|
|
326
|
-
const context =
|
|
343
|
+
const context = withResponseStageCache(withWorkerHostRuntime(this.ctx, this.env));
|
|
327
344
|
const invocation = getResponseStageInvocation(context.props, "bypass");
|
|
328
345
|
if (!invocation) return stampResponseStageBuildIdentity(new Response("Invalid vinext response-stage invocation", {
|
|
329
346
|
status: 400,
|
|
@@ -336,8 +353,8 @@ var VinextUncachedResponse = class extends WorkerEntrypoint {
|
|
|
336
353
|
/** Uncached gateway: request routing and middleware always execute here. */
|
|
337
354
|
var cdn_adapter_worker_default = { async fetch(request, env, context) {
|
|
338
355
|
request = stripUntrustedTransportHeaders(request);
|
|
339
|
-
const
|
|
340
|
-
const stageContext =
|
|
356
|
+
const sharedResponses = /* @__PURE__ */ new Map();
|
|
357
|
+
const stageContext = withResponseStageCache(withWorkerHostRuntime(context, env));
|
|
341
358
|
const dispatchResponseStage = async (stageRequest, props, options) => {
|
|
342
359
|
const expectedResponseStageBuildIdentity = getVinextCdnBuildIdentity();
|
|
343
360
|
const invocation = {
|
|
@@ -349,6 +366,7 @@ var cdn_adapter_worker_default = { async fetch(request, env, context) {
|
|
|
349
366
|
};
|
|
350
367
|
const usesSharedCache = options.cache === "shared";
|
|
351
368
|
try {
|
|
369
|
+
if (!usesSharedCache && !isResponseStageReadinessRequest(stageRequest)) return stampResponseStageBuildIdentity(await invokeResponseStage(stageRequest, env, stageContext, invocation));
|
|
352
370
|
const serializedInvocation = JSON.stringify({
|
|
353
371
|
...invocation,
|
|
354
372
|
options: expectedResponseStageBuildIdentity === null ? options : {
|
|
@@ -360,14 +378,17 @@ var cdn_adapter_worker_default = { async fetch(request, env, context) {
|
|
|
360
378
|
if (!binding) return responseStageUnavailable();
|
|
361
379
|
const entrypointRequest = usesSharedCache ? await createCacheFacingRequest(stageRequest, serializedInvocation) : stageRequest;
|
|
362
380
|
const response = validateResponseStageBuildIdentity(await binding.fetch(entrypointRequest));
|
|
363
|
-
|
|
381
|
+
if (!usesSharedCache) return response;
|
|
382
|
+
const shared = markSharedResponseStage(response, crypto.randomUUID(), props, true);
|
|
383
|
+
sharedResponses.set(shared.headers.get(SHARED_RESPONSE_STAGE_HEADER), { headers: new Headers(shared.headers) });
|
|
384
|
+
return shared;
|
|
364
385
|
} catch (error) {
|
|
365
386
|
if (isResponseStageReadinessRequest(stageRequest)) return responseStageUnavailable();
|
|
366
387
|
throw error;
|
|
367
388
|
}
|
|
368
389
|
};
|
|
369
390
|
const { handleRequestStage } = await loadVinextRequestStage();
|
|
370
|
-
return finalizeGatewayResponse(await handleRequestStage(request, env, stageContext, dispatchResponseStage),
|
|
391
|
+
return finalizeGatewayResponse(await handleRequestStage(request, env, stageContext, dispatchResponseStage), sharedResponses);
|
|
371
392
|
} };
|
|
372
393
|
//#endregion
|
|
373
394
|
export { VinextCachedResponse, VinextUncachedResponse, cdn_adapter_worker_default as default };
|
|
@@ -6,7 +6,11 @@ export type KvDataAdapterOptions = {
|
|
|
6
6
|
binding?: string;
|
|
7
7
|
/** Namespace prefix for cache keys (isolates multiple apps in one namespace). */
|
|
8
8
|
appPrefix?: string;
|
|
9
|
-
/**
|
|
9
|
+
/**
|
|
10
|
+
* Default KV `expirationTtl` in seconds. An entry whose `expire` is longer,
|
|
11
|
+
* such as one cached with `cacheLife("max")`, keeps it for its `expire`.
|
|
12
|
+
* @default 2592000 (30 days)
|
|
13
|
+
*/
|
|
10
14
|
ttlSeconds?: number;
|
|
11
15
|
/** TTL in milliseconds for the in-memory tag-invalidation cache. @default 5000 */
|
|
12
16
|
tagCacheTtlMs?: number;
|
|
@@ -26,8 +26,6 @@ import { getRequestExecutionContext } from "vinext/shims/request-context";
|
|
|
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) {
|
|
@@ -295,7 +293,7 @@ var KVCacheHandler = class {
|
|
|
295
293
|
expireAt,
|
|
296
294
|
cacheControl
|
|
297
295
|
};
|
|
298
|
-
const expirationTtl = this.ttlSeconds;
|
|
296
|
+
const expirationTtl = typeof effectiveExpire === "number" && Number.isFinite(effectiveExpire) && effectiveExpire > this.ttlSeconds ? resolveKvExpirationTtlSeconds(Math.ceil(effectiveExpire)) : 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,
|
package/dist/cache/kv-key.js
CHANGED
|
@@ -38,7 +38,7 @@ function normalizeAppPrefix(appPrefix) {
|
|
|
38
38
|
}
|
|
39
39
|
function buildStorageKey(prefix, categoryPrefix, logicalKey) {
|
|
40
40
|
const key = `${prefix}${categoryPrefix}${logicalKey}`;
|
|
41
|
-
if (kvKeyByteLength(key) <= KV_KEY_MAX_BYTES) return key;
|
|
41
|
+
if (!(categoryPrefix === TAG_PREFIX && logicalKey.includes(":")) && kvKeyByteLength(key) <= KV_KEY_MAX_BYTES) return key;
|
|
42
42
|
return `${prefix}${categoryPrefix}${HASHED_KEY_PREFIX}${fnv1a64(logicalKey)}`;
|
|
43
43
|
}
|
|
44
44
|
/** Create the deterministic key namespace for runtime cache operations. */
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { SHARED_RESPONSE_STAGE_HEADER, finalizeGatewayResponse } from "./browser-cache-policy.js";
|
|
1
2
|
import { captureResponseStoreDataRegeneration, runWithResponseStoreInvocation, setResponseStore } from "./response-store-data.runtime.js";
|
|
2
|
-
import { VINEXT_PARAMS_HEADER, VINEXT_RENDERED_PATH_AND_SEARCH_HEADER } from "vinext/internal/server/headers";
|
|
3
|
+
import { VINEXT_PARAMS_HEADER, VINEXT_RENDERED_PATH_AND_SEARCH_HEADER, VINEXT_SPECIAL_ERROR_STATUS_HEADER } from "vinext/internal/server/headers";
|
|
3
4
|
import { VINEXT_RSC_CONTENT_TYPE, VINEXT_RSC_VARY_HEADER as VINEXT_RSC_VARY_HEADER$1, applyRscCompatibilityIdHeader, applyRscDeploymentIdHeader, createCanonicalRscRequestHeaders, createCanonicalRscRequestUrl } from "vinext/internal/server/app-rsc-cache-busting";
|
|
4
5
|
import { isNonCacheableCacheControl } from "vinext/shims/cdn-cache";
|
|
5
6
|
import { loadVinextRequestStage } from "vinext/server/request-stage";
|
|
@@ -70,9 +71,9 @@ function restoreRequest(invocation) {
|
|
|
70
71
|
}
|
|
71
72
|
async function invokeRequestStage(request, env, ctx) {
|
|
72
73
|
const { handleRequestStage } = await loadVinextRequestStage();
|
|
73
|
-
return handleRequestStage(request, env, stageContext(ctx, env), (request, props, options) => invokeResponseStage(request, props, env, ctx, options.cache));
|
|
74
|
+
return handleRequestStage(request, env, stageContext(ctx, env), (request, props, options) => invokeResponseStage(request, props, env, ctx, { cache: options.cache }));
|
|
74
75
|
}
|
|
75
|
-
async function invokeResponseStage(request, props, env, ctx,
|
|
76
|
+
async function invokeResponseStage(request, props, env, ctx, stageOptions, capture, invocation) {
|
|
76
77
|
const context = stageContext(ctx, env);
|
|
77
78
|
const dispatchRequestStage = (request) => invokeRequestStage(request, env, ctx);
|
|
78
79
|
const { handleResponseStage } = await loadVinextResponseStage();
|
|
@@ -80,7 +81,7 @@ async function invokeResponseStage(request, props, env, ctx, cache, capture, inv
|
|
|
80
81
|
replayable: isReplayableInvocation(request, props),
|
|
81
82
|
serialized: serializeInvocation(request, props)
|
|
82
83
|
};
|
|
83
|
-
return runWithResponseStoreInvocation(storedInvocation.serialized, storedInvocation.replayable, () => handleResponseStage(request, env, context, props, dispatchRequestStage,
|
|
84
|
+
return runWithResponseStoreInvocation(storedInvocation.serialized, storedInvocation.replayable, () => handleResponseStage(request, env, context, props, dispatchRequestStage, stageOptions), capture);
|
|
84
85
|
}
|
|
85
86
|
function createVinextResponseStoreOptions(configuration) {
|
|
86
87
|
const locationHint = configuration?.locationHint;
|
|
@@ -93,7 +94,10 @@ function createVinextResponseStoreOptions(configuration) {
|
|
|
93
94
|
async regenerate(input, { env, ctx }) {
|
|
94
95
|
if (input.id === ROUTE_REVALIDATOR_ID) {
|
|
95
96
|
const invocation = parseInvocation(input.args.at(-1));
|
|
96
|
-
const response = await invokeResponseStage(restoreRequest(invocation), invocation.props, env, ctx,
|
|
97
|
+
const response = await invokeResponseStage(restoreRequest(invocation), invocation.props, env, ctx, {
|
|
98
|
+
cache: "shared",
|
|
99
|
+
renderWholeDocument: true
|
|
100
|
+
});
|
|
97
101
|
if (!isCacheable(response)) {
|
|
98
102
|
await response.body?.cancel().catch(() => {});
|
|
99
103
|
throw new Error("Vinext response-stage regeneration was not cacheable");
|
|
@@ -112,7 +116,7 @@ function createVinextResponseStoreOptions(configuration) {
|
|
|
112
116
|
if (input.id === "vinext:data" && typeof input.args[0] === "string") {
|
|
113
117
|
const invocation = parseInvocation(input.args.at(-1));
|
|
114
118
|
return captureResponseStoreDataRegeneration(input.args[0], async () => {
|
|
115
|
-
await (await invokeResponseStage(restoreRequest(invocation), invocation.props, env, ctx, "bypass")).body?.pipeTo(new WritableStream());
|
|
119
|
+
await (await invokeResponseStage(restoreRequest(invocation), invocation.props, env, ctx, { cache: "bypass" })).body?.pipeTo(new WritableStream());
|
|
116
120
|
});
|
|
117
121
|
}
|
|
118
122
|
throw new Error(`Unknown vinext response-store revalidator ${input.id}`);
|
|
@@ -151,16 +155,20 @@ function withoutRequestScopedHeaders(response, responseStageProps) {
|
|
|
151
155
|
statusText: response.statusText
|
|
152
156
|
});
|
|
153
157
|
}
|
|
158
|
+
function isStoredErrorStatus(status) {
|
|
159
|
+
return status === 401 || status === 403 || status === 404;
|
|
160
|
+
}
|
|
154
161
|
function isCacheable(response) {
|
|
155
162
|
const policy = response.headers.get("Cloudflare-CDN-Cache-Control") ?? response.headers.get("CDN-Cache-Control") ?? response.headers.get("Cache-Control");
|
|
156
|
-
return response.status >= 200 && response.status < 400 && policy !== null && !isNonCacheableCacheControl(policy);
|
|
163
|
+
return response.status >= 200 && (response.status < 400 || isStoredErrorStatus(response.status)) && policy !== null && !isNonCacheableCacheControl(policy);
|
|
157
164
|
}
|
|
158
165
|
async function readStoredResponse(key) {
|
|
159
166
|
try {
|
|
160
167
|
const response = await responseStore.fetch(key);
|
|
161
|
-
|
|
168
|
+
const storeStatus = response.headers.get("X-Workers-Response-Store");
|
|
169
|
+
if (response.status >= 200 && response.status < 400 || isStoredErrorStatus(response.status) && (storeStatus === "BLOB-FRESH" || storeStatus === "BLOB-STALE")) return response;
|
|
162
170
|
response.body?.cancel().catch(() => {});
|
|
163
|
-
if (response.status === 404 &&
|
|
171
|
+
if (response.status === 404 && storeStatus === "MISS") return null;
|
|
164
172
|
throw new Error(`Workers Response Store returned ${response.status}`);
|
|
165
173
|
} catch (error) {
|
|
166
174
|
console.error(JSON.stringify({
|
|
@@ -170,7 +178,7 @@ async function readStoredResponse(key) {
|
|
|
170
178
|
return null;
|
|
171
179
|
}
|
|
172
180
|
}
|
|
173
|
-
function publicResponse(response, cacheStatus, responseStageProps) {
|
|
181
|
+
function publicResponse(response, cacheStatus, responseStageProps, pendingAdmission = false) {
|
|
174
182
|
const headers = new Headers(response.headers);
|
|
175
183
|
const publicCacheStatus = cacheStatus === "HIT" && headers.get("CF-Cache-Status") === "UPDATING" ? "UPDATING" : cacheStatus;
|
|
176
184
|
const ageBasis = /^(\d+):(\d+)$/.exec(headers.get(AGE_BASIS_HEADER) ?? "");
|
|
@@ -193,7 +201,7 @@ function publicResponse(response, cacheStatus, responseStageProps) {
|
|
|
193
201
|
headers.set("X-Vinext-Cache", publicCacheStatus);
|
|
194
202
|
}
|
|
195
203
|
const cacheControl = headers.get("Cache-Control");
|
|
196
|
-
if (!cacheControl || !isNonCacheableCacheControl(cacheControl)) headers.set("Cache-Control", "
|
|
204
|
+
if (pendingAdmission && (!cacheControl || !isNonCacheableCacheControl(cacheControl, "browser"))) headers.set("Cache-Control", "no-store, must-revalidate");
|
|
197
205
|
return traceCachedResponseStart(new Response(response.body, {
|
|
198
206
|
headers,
|
|
199
207
|
status: response.status,
|
|
@@ -209,7 +217,7 @@ function createVinextResponseStoreHandler(store) {
|
|
|
209
217
|
const handler = { async fetch(request, env, ctx) {
|
|
210
218
|
const context = stageContext(ctx, env);
|
|
211
219
|
const dispatchResponseStage = async (stageRequest, props, options) => {
|
|
212
|
-
if (options.cache === "bypass" || stageRequest.method !== "GET" && stageRequest.method !== "HEAD") return publicResponse(await invokeResponseStage(stageRequest, props, env, ctx, "bypass"), "BYPASS", props);
|
|
220
|
+
if (options.cache === "bypass" || stageRequest.method !== "GET" && stageRequest.method !== "HEAD") return publicResponse(await invokeResponseStage(stageRequest, props, env, ctx, { cache: "bypass" }), "BYPASS", props);
|
|
213
221
|
const identityRequest = options.cacheIdentity?.request ?? stageRequest;
|
|
214
222
|
const identityProps = options.cacheIdentity?.props ?? props;
|
|
215
223
|
const isWarmup = request.headers.get("user-agent") === WARMUP_USER_AGENT;
|
|
@@ -237,7 +245,10 @@ const handler = { async fetch(request, env, ctx) {
|
|
|
237
245
|
}
|
|
238
246
|
const capture = rscSeed ? { captureRscData: true } : isWarmup ? {} : { streamResponse: true };
|
|
239
247
|
const serializedInvocation = JSON.stringify(invocation);
|
|
240
|
-
const rendered = await invokeResponseStage(stageRequest, props, env, ctx,
|
|
248
|
+
const rendered = await invokeResponseStage(stageRequest, props, env, ctx, isWarmup ? {
|
|
249
|
+
cache: "shared",
|
|
250
|
+
renderWholeDocument: true
|
|
251
|
+
} : { cache: "shared" }, capture, {
|
|
241
252
|
replayable: isReplayableInvocation(stageRequest, props),
|
|
242
253
|
serialized: options.cacheIdentity ? serializeInvocation(stageRequest, props) : serializedInvocation
|
|
243
254
|
});
|
|
@@ -260,7 +271,7 @@ const handler = { async fetch(request, env, ctx) {
|
|
|
260
271
|
error: error instanceof Error ? error.message : String(error)
|
|
261
272
|
}));
|
|
262
273
|
}));
|
|
263
|
-
return publicResponse(rendered, "MISS", props);
|
|
274
|
+
return publicResponse(rendered, "MISS", props, true);
|
|
264
275
|
}
|
|
265
276
|
if (!isCacheable(rendered)) {
|
|
266
277
|
capture?.rscData?.catch(() => {});
|
|
@@ -300,10 +311,11 @@ const handler = { async fetch(request, env, ctx) {
|
|
|
300
311
|
rscHeaders.set("Vary", VINEXT_RSC_VARY_HEADER$1);
|
|
301
312
|
applyRscCompatibilityIdHeader(rscHeaders);
|
|
302
313
|
applyRscDeploymentIdHeader(rscHeaders);
|
|
314
|
+
if (isStoredErrorStatus(rendered.status)) rscHeaders.set(VINEXT_SPECIAL_ERROR_STATUS_HEADER, String(rendered.status));
|
|
303
315
|
const serializedRscInvocation = JSON.stringify(rscInvocation);
|
|
304
316
|
await responseStore.put(rscKey, new Response(rscData, {
|
|
305
317
|
headers: rscHeaders,
|
|
306
|
-
status: 200
|
|
318
|
+
status: rendered.status >= 300 && rendered.status < 400 ? 200 : rendered.status
|
|
307
319
|
}), {
|
|
308
320
|
coalesce: true,
|
|
309
321
|
revalidator: {
|
|
@@ -315,7 +327,21 @@ const handler = { async fetch(request, env, ctx) {
|
|
|
315
327
|
return publicResponse(new Response(foreground, rendered), "MISS", props);
|
|
316
328
|
};
|
|
317
329
|
const { handleRequestStage } = await loadVinextRequestStage();
|
|
318
|
-
|
|
330
|
+
const sharedResponses = /* @__PURE__ */ new Map();
|
|
331
|
+
const dispatch = async (stageRequest, props, options) => {
|
|
332
|
+
const response = await dispatchResponseStage(stageRequest, props, options);
|
|
333
|
+
if (options.cache !== "shared") return response;
|
|
334
|
+
const headers = new Headers(response.headers);
|
|
335
|
+
const token = crypto.randomUUID();
|
|
336
|
+
headers.set(SHARED_RESPONSE_STAGE_HEADER, token);
|
|
337
|
+
sharedResponses.set(token, { headers: new Headers(headers) });
|
|
338
|
+
return new Response(response.body, {
|
|
339
|
+
headers,
|
|
340
|
+
status: response.status,
|
|
341
|
+
statusText: response.statusText
|
|
342
|
+
});
|
|
343
|
+
};
|
|
344
|
+
return finalizeGatewayResponse(await handleRequestStage(request, env, context, dispatch), sharedResponses, "X-Vinext-Cache");
|
|
319
345
|
} };
|
|
320
346
|
//#endregion
|
|
321
347
|
export { createVinextResponseStoreHandler, createVinextResponseStoreOptions };
|
|
@@ -25,6 +25,7 @@ export declare class WorkersResponseStoreCacheHandler implements CacheHandler {
|
|
|
25
25
|
constructor(store?: WorkersResponseStore);
|
|
26
26
|
get(key: string, context?: Record<string, unknown>): Promise<CacheHandlerValue | null>;
|
|
27
27
|
set(key: string, value: IncrementalCacheValue | null, context?: Record<string, unknown>): Promise<void>;
|
|
28
|
+
private write;
|
|
28
29
|
revalidateTag(tags: string | string[], durations?: {
|
|
29
30
|
expire?: number;
|
|
30
31
|
}): Promise<void>;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isStaleTagInvalidation } from "./workers-cache-invalidation.js";
|
|
1
2
|
import { encodeCloudflareCacheTag } from "./cdn-adapter.runtime.js";
|
|
2
3
|
import { Buffer } from "node:buffer";
|
|
3
4
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
@@ -136,8 +137,15 @@ function deferResponseStoreAdmission(response, complete) {
|
|
|
136
137
|
}
|
|
137
138
|
/** Re-render an invocation and return the exact rewritten data-cache entry. */
|
|
138
139
|
async function captureResponseStoreDataRegeneration(key, callback) {
|
|
139
|
-
const scope = {
|
|
140
|
-
|
|
140
|
+
const scope = {
|
|
141
|
+
sideWrites: [],
|
|
142
|
+
targetKey: key
|
|
143
|
+
};
|
|
144
|
+
try {
|
|
145
|
+
await regenerationStorage.run(scope, callback);
|
|
146
|
+
} finally {
|
|
147
|
+
while (scope.sideWrites.length) await Promise.allSettled(scope.sideWrites.splice(0));
|
|
148
|
+
}
|
|
141
149
|
if (!scope.captured) throw new Error(`vinext response-store regeneration did not rewrite data key ${key}`);
|
|
142
150
|
return scope.captured;
|
|
143
151
|
}
|
|
@@ -180,7 +188,8 @@ function deserialize(value) {
|
|
|
180
188
|
}
|
|
181
189
|
function cachePolicy(revalidate, expire) {
|
|
182
190
|
if (revalidate === false || revalidate === void 0) return `public, max-age=${CACHE_MAX_AGE_SECONDS}`;
|
|
183
|
-
|
|
191
|
+
const staleSeconds = expire === void 0 ? CACHE_MAX_AGE_SECONDS : Math.max(0, expire - revalidate);
|
|
192
|
+
return `public, max-age=${Math.max(0, revalidate)}, stale-while-revalidate=${staleSeconds}`;
|
|
184
193
|
}
|
|
185
194
|
var WorkersResponseStoreCacheHandler = class {
|
|
186
195
|
store;
|
|
@@ -231,9 +240,9 @@ var WorkersResponseStoreCacheHandler = class {
|
|
|
231
240
|
const requestedStale = typeof requestedRevalidate === "number" && requestedRevalidate > 0 && age > requestedRevalidate * 1e3;
|
|
232
241
|
let cacheState;
|
|
233
242
|
if (response.headers.get(REPLAYABLE_HEADER) === "1") {
|
|
234
|
-
if (requestedStale) cacheState = "stale";
|
|
243
|
+
if (requestedStale && storeStatus !== "BLOB-STALE") cacheState = "stale";
|
|
235
244
|
} else if (typeof entry.cacheControl?.expire === "number" && age > entry.cacheControl.expire * 1e3) cacheState = "expired";
|
|
236
|
-
else if (requestedStale || typeof entry.cacheControl?.revalidate === "number" && entry.cacheControl.revalidate > 0 && age > entry.cacheControl.revalidate * 1e3) cacheState = "stale";
|
|
245
|
+
else if (requestedStale || storeStatus === "BLOB-STALE" || typeof entry.cacheControl?.revalidate === "number" && entry.cacheControl.revalidate > 0 && age > entry.cacheControl.revalidate * 1e3) cacheState = "stale";
|
|
237
246
|
return {
|
|
238
247
|
lastModified: entry.lastModified,
|
|
239
248
|
value: entry.value,
|
|
@@ -248,7 +257,13 @@ var WorkersResponseStoreCacheHandler = class {
|
|
|
248
257
|
return null;
|
|
249
258
|
}
|
|
250
259
|
}
|
|
251
|
-
|
|
260
|
+
set(key, value, context) {
|
|
261
|
+
const write = this.write(key, value, context);
|
|
262
|
+
const regeneration = regenerationStorage.getStore();
|
|
263
|
+
if (regeneration && regeneration.targetKey !== key) regeneration.sideWrites.push(write);
|
|
264
|
+
return write;
|
|
265
|
+
}
|
|
266
|
+
async write(key, value, context) {
|
|
252
267
|
let revalidate = readCacheControlField(context, "revalidate");
|
|
253
268
|
if (value && "revalidate" in value) revalidate = value.revalidate;
|
|
254
269
|
if (revalidate === 0) return;
|
|
@@ -287,12 +302,14 @@ var WorkersResponseStoreCacheHandler = class {
|
|
|
287
302
|
...revalidator ? { [REPLAYABLE_HEADER]: "1" } : {}
|
|
288
303
|
} });
|
|
289
304
|
const regeneration = regenerationStorage.getStore();
|
|
290
|
-
if (regeneration) {
|
|
291
|
-
|
|
305
|
+
if (regeneration?.targetKey === key) {
|
|
306
|
+
regeneration.captured = response;
|
|
292
307
|
return;
|
|
293
308
|
}
|
|
309
|
+
if (regeneration && !revalidator) return;
|
|
294
310
|
await this.store.put(await cacheRequest(key), response, {
|
|
295
311
|
...revalidator ? { revalidator } : {},
|
|
312
|
+
...revalidator?.id !== "vinext:cache-function" ? { expiryBehavior: "miss" } : {},
|
|
296
313
|
coalesce: true,
|
|
297
314
|
purgeExisting: true
|
|
298
315
|
});
|
|
@@ -300,7 +317,10 @@ var WorkersResponseStoreCacheHandler = class {
|
|
|
300
317
|
async revalidateTag(tags, durations) {
|
|
301
318
|
const encodedTags = (Array.isArray(tags) ? tags : [tags]).map(encodeCloudflareCacheTag);
|
|
302
319
|
if (!encodedTags.length) return;
|
|
303
|
-
if (durations
|
|
320
|
+
if (isStaleTagInvalidation(durations)) await this.store.invalidate({
|
|
321
|
+
tags: encodedTags,
|
|
322
|
+
...durations?.expire === void 0 ? {} : { expire: durations.expire }
|
|
323
|
+
});
|
|
304
324
|
else await this.store.purge({ tags: encodedTags });
|
|
305
325
|
}
|
|
306
326
|
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
//#region src/cache/static-assets-adapter.build.d.ts
|
|
2
|
-
/** Package
|
|
2
|
+
/** Package prerender output from both routers as immutable Workers Static Assets. */
|
|
3
3
|
export declare function finalizeStaticAssetsPrerenderOutput(root: string, options?: {
|
|
4
4
|
clientOutDir?: string;
|
|
5
5
|
}): number;
|
|
@@ -3,13 +3,23 @@ import fs from "node:fs";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { createHash } from "node:crypto";
|
|
5
5
|
import { readPrerenderManifest } from "vinext/internal/server/prerender-manifest";
|
|
6
|
-
import { appIsrCacheKey } from "vinext/internal/server/isr-cache";
|
|
6
|
+
import { appIsrCacheKey, pagesIsrCacheKey } from "vinext/internal/server/isr-cache";
|
|
7
|
+
import { extractVinextNextDataJson } from "vinext/internal/client/vinext-next-data";
|
|
7
8
|
import { normalizePregeneratedPathname } from "vinext/internal/server/pregenerated-concrete-paths";
|
|
8
9
|
import { getAppRouteOutputPath, getOutputPath, getRscOutputPath } from "vinext/internal/utils/prerender-output-paths";
|
|
9
10
|
//#region src/cache/static-assets-adapter.build.ts
|
|
10
11
|
function cacheAssetId(key) {
|
|
11
12
|
return createHash("sha256").update(key).digest("hex");
|
|
12
13
|
}
|
|
14
|
+
function pagesCacheKey(pathname, locale, buildId) {
|
|
15
|
+
let pagesPathname = new URL(`https://vinext.invalid${pathname}`).pathname;
|
|
16
|
+
if (locale) {
|
|
17
|
+
const prefix = `/${locale}`;
|
|
18
|
+
if (pagesPathname === prefix) pagesPathname = "/";
|
|
19
|
+
else if (pagesPathname.startsWith(`${prefix}/`)) pagesPathname = pagesPathname.slice(prefix.length);
|
|
20
|
+
}
|
|
21
|
+
return pagesIsrCacheKey(pagesPathname, buildId, locale ? `locale:${locale}` : void 0);
|
|
22
|
+
}
|
|
13
23
|
function cacheControl(route) {
|
|
14
24
|
if (route.revalidate === void 0) return void 0;
|
|
15
25
|
return {
|
|
@@ -18,24 +28,27 @@ function cacheControl(route) {
|
|
|
18
28
|
...route.stale === void 0 ? {} : { stale: route.stale }
|
|
19
29
|
};
|
|
20
30
|
}
|
|
21
|
-
function writeCacheAsset(outputDir, index, key, kind, sourcePath, route) {
|
|
31
|
+
function writeCacheAsset(outputDir, index, key, kind, sourcePath, route, contents) {
|
|
22
32
|
if (!fs.existsSync(sourcePath)) return false;
|
|
23
33
|
const id = cacheAssetId(key);
|
|
24
|
-
const extension = kind
|
|
34
|
+
const extension = kind;
|
|
25
35
|
const policy = cacheControl(route);
|
|
36
|
+
const headers = kind !== "rsc" ? route.headers : route.responseStatus !== void 0 && route.headers?.location !== void 0 ? { location: route.headers.location } : void 0;
|
|
26
37
|
const metadata = {
|
|
27
38
|
kind,
|
|
28
39
|
lastModified: fs.statSync(sourcePath).mtimeMs,
|
|
29
40
|
...policy ? { cacheControl: policy } : {},
|
|
30
|
-
...
|
|
31
|
-
...
|
|
41
|
+
...headers ? { headers } : {},
|
|
42
|
+
...route.responseStatus !== void 0 ? { status: route.responseStatus } : {}
|
|
32
43
|
};
|
|
33
44
|
fs.mkdirSync(outputDir, { recursive: true });
|
|
34
|
-
|
|
45
|
+
const outputPath = path.join(outputDir, `${id}.${extension}`);
|
|
46
|
+
if (contents === void 0) fs.copyFileSync(sourcePath, outputPath);
|
|
47
|
+
else fs.writeFileSync(outputPath, contents);
|
|
35
48
|
index[id] = metadata;
|
|
36
49
|
return true;
|
|
37
50
|
}
|
|
38
|
-
/** Package
|
|
51
|
+
/** Package prerender output from both routers as immutable Workers Static Assets. */
|
|
39
52
|
function finalizeStaticAssetsPrerenderOutput(root, options = {}) {
|
|
40
53
|
const serverDir = path.join(root, "dist", "server");
|
|
41
54
|
const prerenderDir = path.join(serverDir, "prerendered-routes");
|
|
@@ -56,6 +69,31 @@ function finalizeStaticAssetsPrerenderOutput(root, options = {}) {
|
|
|
56
69
|
if (route.router === "app") {
|
|
57
70
|
count += Number(writeCacheAsset(outputDir, index, appIsrCacheKey(cachePathname, "html", manifest.buildId), "html", path.join(prerenderDir, getOutputPath(pathname, manifest.trailingSlash ?? false)), route));
|
|
58
71
|
count += Number(writeCacheAsset(outputDir, index, appIsrCacheKey(cachePathname, "rsc", manifest.buildId), "rsc", path.join(prerenderDir, getRscOutputPath(pathname)), route));
|
|
72
|
+
} else if (route.router === "pages") {
|
|
73
|
+
const sourcePath = path.join(prerenderDir, getOutputPath(pathname, manifest.trailingSlash ?? false));
|
|
74
|
+
if (!fs.existsSync(sourcePath)) continue;
|
|
75
|
+
const html = fs.readFileSync(sourcePath, "utf8");
|
|
76
|
+
const key = pagesCacheKey(pathname, route.locale, manifest.buildId);
|
|
77
|
+
if (route.notFound) {
|
|
78
|
+
count += Number(writeCacheAsset(outputDir, index, key, "not-found", sourcePath, route, "null"));
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
if (route.redirectProps) {
|
|
82
|
+
count += Number(writeCacheAsset(outputDir, index, key, "redirect", sourcePath, route, JSON.stringify(route.redirectProps)));
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
const json = extractVinextNextDataJson(html);
|
|
86
|
+
if (json === null) continue;
|
|
87
|
+
const nextData = JSON.parse(json);
|
|
88
|
+
if (nextData.gsp !== true && nextData.autoExport !== true) continue;
|
|
89
|
+
if (!nextData.props || typeof nextData.props !== "object") throw new Error(`[vinext] Missing prerendered Pages props for ${pathname}`);
|
|
90
|
+
count += Number(writeCacheAsset(outputDir, index, pagesCacheKey(pathname, nextData.locale ?? route.locale, manifest.buildId), "pages", sourcePath, {
|
|
91
|
+
...route,
|
|
92
|
+
responseStatus: route.route === "/404" ? 404 : route.route === "/500" ? 500 : route.responseStatus ?? 200
|
|
93
|
+
}, JSON.stringify({
|
|
94
|
+
html,
|
|
95
|
+
pageData: nextData.props
|
|
96
|
+
})));
|
|
59
97
|
} else if (route.router === "metadata") count += Number(writeCacheAsset(outputDir, index, appIsrCacheKey(cachePathname, "route", manifest.buildId), "route", path.join(prerenderDir, getAppRouteOutputPath(pathname)), route));
|
|
60
98
|
}
|
|
61
99
|
if (count > 0) fs.writeFileSync(path.join(outputDir, "index.json"), JSON.stringify(index));
|
|
@@ -6,7 +6,7 @@ export type StaticAssetsAdapterOptions = {
|
|
|
6
6
|
/**
|
|
7
7
|
* Read-only page cache backed by Workers Static Assets.
|
|
8
8
|
*
|
|
9
|
-
* Locally prerendered App
|
|
9
|
+
* Locally prerendered Pages HTML/data and App HTML, RSC, and metadata are copied
|
|
10
10
|
* into `dist/client` after the build. Runtime cache writes and invalidations are
|
|
11
11
|
* intentionally no-ops; a new deployment replaces the cache contents.
|
|
12
12
|
*/
|
|
@@ -4,7 +4,7 @@ import { fileURLToPath } from "node:url";
|
|
|
4
4
|
/**
|
|
5
5
|
* Read-only page cache backed by Workers Static Assets.
|
|
6
6
|
*
|
|
7
|
-
* Locally prerendered App
|
|
7
|
+
* Locally prerendered Pages HTML/data and App HTML, RSC, and metadata are copied
|
|
8
8
|
* into `dist/client` after the build. Runtime cache writes and invalidations are
|
|
9
9
|
* intentionally no-ops; a new deployment replaces the cache contents.
|
|
10
10
|
*/
|
|
@@ -6,7 +6,8 @@ type AssetFetcher = {
|
|
|
6
6
|
fetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response>;
|
|
7
7
|
};
|
|
8
8
|
export declare class StaticAssetsCacheAdapter implements CdnCacheAdapter {
|
|
9
|
-
|
|
9
|
+
readonly assets: AssetFetcher;
|
|
10
|
+
readonly hasPrerenderedPages = true;
|
|
10
11
|
readonly ownsBackgroundRevalidation = false;
|
|
11
12
|
private indexPromise;
|
|
12
13
|
constructor(assets: AssetFetcher);
|
|
@@ -6,10 +6,11 @@ async function cacheAssetId(key) {
|
|
|
6
6
|
return Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
7
7
|
}
|
|
8
8
|
function isMetadata(value) {
|
|
9
|
-
return value !== null && typeof value === "object" && "kind" in value && (value.kind === "html" || value.kind === "rsc" || value.kind === "route") && "lastModified" in value && typeof value.lastModified === "number";
|
|
9
|
+
return value !== null && typeof value === "object" && "kind" in value && (value.kind === "html" || value.kind === "rsc" || value.kind === "route" || value.kind === "pages" || value.kind === "redirect" || value.kind === "not-found") && "lastModified" in value && typeof value.lastModified === "number";
|
|
10
10
|
}
|
|
11
11
|
var StaticAssetsCacheAdapter = class {
|
|
12
12
|
assets;
|
|
13
|
+
hasPrerenderedPages = true;
|
|
13
14
|
ownsBackgroundRevalidation = false;
|
|
14
15
|
indexPromise;
|
|
15
16
|
constructor(assets) {
|
|
@@ -22,11 +23,27 @@ var StaticAssetsCacheAdapter = class {
|
|
|
22
23
|
const id = await cacheAssetId(key);
|
|
23
24
|
const metadata = (await this.loadIndex())?.[id];
|
|
24
25
|
if (!isMetadata(metadata)) return null;
|
|
25
|
-
const extension = metadata.kind
|
|
26
|
+
const extension = metadata.kind;
|
|
26
27
|
const bodyResponse = await this.assets.fetch(`https://vinext.invalid${STATIC_ASSET_CACHE_PATH}/${id}.${extension}`);
|
|
27
28
|
if (!bodyResponse.ok) return null;
|
|
28
29
|
let value;
|
|
29
|
-
if (metadata.kind === "
|
|
30
|
+
if (metadata.kind === "not-found") {
|
|
31
|
+
await bodyResponse.body?.cancel();
|
|
32
|
+
value = null;
|
|
33
|
+
} else if (metadata.kind === "redirect") value = {
|
|
34
|
+
kind: "REDIRECT",
|
|
35
|
+
props: await bodyResponse.json()
|
|
36
|
+
};
|
|
37
|
+
else if (metadata.kind === "pages") {
|
|
38
|
+
const { html, pageData } = await bodyResponse.json();
|
|
39
|
+
value = {
|
|
40
|
+
kind: "PAGES",
|
|
41
|
+
html,
|
|
42
|
+
pageData,
|
|
43
|
+
headers: metadata.headers,
|
|
44
|
+
status: metadata.status
|
|
45
|
+
};
|
|
46
|
+
} else if (metadata.kind === "html") value = {
|
|
30
47
|
kind: "APP_PAGE",
|
|
31
48
|
html: await bodyResponse.text(),
|
|
32
49
|
rscData: void 0,
|
|
@@ -39,10 +56,10 @@ var StaticAssetsCacheAdapter = class {
|
|
|
39
56
|
kind: "APP_PAGE",
|
|
40
57
|
html: "",
|
|
41
58
|
rscData: await bodyResponse.arrayBuffer(),
|
|
42
|
-
headers:
|
|
59
|
+
headers: metadata.headers,
|
|
43
60
|
postponed: void 0,
|
|
44
61
|
prerendered: true,
|
|
45
|
-
status:
|
|
62
|
+
status: metadata.status
|
|
46
63
|
};
|
|
47
64
|
else value = {
|
|
48
65
|
kind: "APP_ROUTE",
|
|
@@ -2,7 +2,7 @@ import { CacheControlMetadata } from "vinext/shims/cache-handler";
|
|
|
2
2
|
//#region src/cache/static-assets-adapter.shared.d.ts
|
|
3
3
|
export declare const STATIC_ASSET_CACHE_PATH = "/_vinext/static-cache";
|
|
4
4
|
export type StaticAssetCacheMetadata = {
|
|
5
|
-
kind: "html" | "rsc" | "route";
|
|
5
|
+
kind: "html" | "rsc" | "route" | "pages" | "redirect" | "not-found";
|
|
6
6
|
lastModified: number;
|
|
7
7
|
cacheControl?: CacheControlMetadata;
|
|
8
8
|
headers?: Record<string, string | string[]>;
|
|
@@ -12,15 +12,15 @@ export type CdnAdapterOptions = {
|
|
|
12
12
|
* Unlike the data adapter (which stores cache entries in a durable store and
|
|
13
13
|
* serves HIT/STALE itself), this adapter delegates serving to Workers Cache on
|
|
14
14
|
* a named Worker entrypoint. The default entrypoint always runs middleware and
|
|
15
|
-
* request-time routing
|
|
16
|
-
*
|
|
15
|
+
* request-time routing, then renders bypass requests inline or dispatches shared
|
|
16
|
+
* requests to the cached response-stage entrypoint.
|
|
17
17
|
*
|
|
18
18
|
* The deployment configuration enables Workers Cache only for that response-stage
|
|
19
19
|
* export, so cache hits do not start the application stage. Legacy Cloudflare
|
|
20
20
|
* builds receive this policy in their generated Wrangler config. Cloudflare Vite
|
|
21
21
|
* plugin v2 builds declare the same entrypoint policy in `cloudflare.config.ts`.
|
|
22
|
-
*
|
|
23
|
-
*
|
|
22
|
+
* Deployment readiness probes still use the uncached response entrypoint to
|
|
23
|
+
* verify that named response entrypoints are available.
|
|
24
24
|
*
|
|
25
25
|
* The adapter adds a transport-only URL digest so distinct response-stage
|
|
26
26
|
* identities cannot collide. Workers Cache owns this key independently of
|
|
@@ -10,15 +10,15 @@ const CLOUDFLARE_WORKER_ENTRY_ID = "virtual:cloudflare/worker-entry";
|
|
|
10
10
|
* Unlike the data adapter (which stores cache entries in a durable store and
|
|
11
11
|
* serves HIT/STALE itself), this adapter delegates serving to Workers Cache on
|
|
12
12
|
* a named Worker entrypoint. The default entrypoint always runs middleware and
|
|
13
|
-
* request-time routing
|
|
14
|
-
*
|
|
13
|
+
* request-time routing, then renders bypass requests inline or dispatches shared
|
|
14
|
+
* requests to the cached response-stage entrypoint.
|
|
15
15
|
*
|
|
16
16
|
* The deployment configuration enables Workers Cache only for that response-stage
|
|
17
17
|
* export, so cache hits do not start the application stage. Legacy Cloudflare
|
|
18
18
|
* builds receive this policy in their generated Wrangler config. Cloudflare Vite
|
|
19
19
|
* plugin v2 builds declare the same entrypoint policy in `cloudflare.config.ts`.
|
|
20
|
-
*
|
|
21
|
-
*
|
|
20
|
+
* Deployment readiness probes still use the uncached response entrypoint to
|
|
21
|
+
* verify that named response entrypoints are available.
|
|
22
22
|
*
|
|
23
23
|
* The adapter adds a transport-only URL digest so distinct response-stage
|
|
24
24
|
* identities cannot collide. Workers Cache owns this key independently of
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
//#region src/cache/workers-cache-invalidation.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Workers Cache tag invalidation shared by the CDN adapter, its worker
|
|
4
|
+
* entrypoints, and the Response Store data adapter.
|
|
5
|
+
*/
|
|
6
|
+
type CacheInvalidationOptions = {
|
|
7
|
+
tags: string[];
|
|
8
|
+
};
|
|
9
|
+
type CacheMethodName = "invalidate" | "purge";
|
|
10
|
+
type CacheMethods<Name extends CacheMethodName> = Record<Name, (options: CacheInvalidationOptions) => unknown>;
|
|
11
|
+
/**
|
|
12
|
+
* Whether `revalidateTag` marks its tags stale rather than expiring them.
|
|
13
|
+
*
|
|
14
|
+
* Next.js marks a tag stale whenever durations are passed, and expires it
|
|
15
|
+
* immediately when they are absent (`updateTag`, `revalidatePath`, the legacy
|
|
16
|
+
* single-argument `revalidateTag`) or `expire` is 0.
|
|
17
|
+
* https://github.com/vercel/next.js/blob/canary/packages/next/src/server/lib/incremental-cache/file-system-cache.ts
|
|
18
|
+
*/
|
|
19
|
+
export declare function isStaleTagInvalidation(durations?: {
|
|
20
|
+
expire?: number;
|
|
21
|
+
}): boolean;
|
|
22
|
+
export declare function hasCacheMethod<Name extends CacheMethodName>(value: unknown, name: Name): value is CacheMethods<Name>;
|
|
23
|
+
/**
|
|
24
|
+
* Mark matching Workers Cache responses stale so the edge keeps serving them
|
|
25
|
+
* while it refetches in the background. Local Miniflare does not implement
|
|
26
|
+
* `invalidate()` yet, so it falls back to a hard purge there. Returns
|
|
27
|
+
* `undefined` when the cache supports neither.
|
|
28
|
+
*/
|
|
29
|
+
export declare function invalidateOrPurge(cache: unknown, options: CacheInvalidationOptions): unknown;
|
|
30
|
+
//#endregion
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
//#region src/cache/workers-cache-invalidation.ts
|
|
2
|
+
/**
|
|
3
|
+
* Whether `revalidateTag` marks its tags stale rather than expiring them.
|
|
4
|
+
*
|
|
5
|
+
* Next.js marks a tag stale whenever durations are passed, and expires it
|
|
6
|
+
* immediately when they are absent (`updateTag`, `revalidatePath`, the legacy
|
|
7
|
+
* single-argument `revalidateTag`) or `expire` is 0.
|
|
8
|
+
* https://github.com/vercel/next.js/blob/canary/packages/next/src/server/lib/incremental-cache/file-system-cache.ts
|
|
9
|
+
*/
|
|
10
|
+
function isStaleTagInvalidation(durations) {
|
|
11
|
+
return durations !== void 0 && !(typeof durations.expire === "number" && durations.expire <= 0);
|
|
12
|
+
}
|
|
13
|
+
function hasCacheMethod(value, name) {
|
|
14
|
+
return value !== null && (typeof value === "object" || typeof value === "function") && name in value && typeof Reflect.get(value, name) === "function";
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Mark matching Workers Cache responses stale so the edge keeps serving them
|
|
18
|
+
* while it refetches in the background. Local Miniflare does not implement
|
|
19
|
+
* `invalidate()` yet, so it falls back to a hard purge there. Returns
|
|
20
|
+
* `undefined` when the cache supports neither.
|
|
21
|
+
*/
|
|
22
|
+
function invalidateOrPurge(cache, options) {
|
|
23
|
+
if (hasCacheMethod(cache, "invalidate")) return cache.invalidate(options);
|
|
24
|
+
return hasCacheMethod(cache, "purge") ? cache.purge(options) : void 0;
|
|
25
|
+
}
|
|
26
|
+
//#endregion
|
|
27
|
+
export { hasCacheMethod, invalidateOrPurge, isStaleTagInvalidation };
|
package/dist/cdn-warm.js
CHANGED
|
@@ -373,7 +373,7 @@ const CDN_CACHE_POLICY_HEADERS = [
|
|
|
373
373
|
"CDN-Cache-Control",
|
|
374
374
|
"Cache-Control"
|
|
375
375
|
];
|
|
376
|
-
function splitCacheControlDirectives(value) {
|
|
376
|
+
function splitCacheControlDirectives$1(value) {
|
|
377
377
|
const directives = [];
|
|
378
378
|
let start = 0;
|
|
379
379
|
let quoted = false;
|
|
@@ -392,7 +392,7 @@ function splitCacheControlDirectives(value) {
|
|
|
392
392
|
return directives;
|
|
393
393
|
}
|
|
394
394
|
function hasFieldQualifiedSetCookie(value) {
|
|
395
|
-
return splitCacheControlDirectives(value).some((directive) => {
|
|
395
|
+
return splitCacheControlDirectives$1(value).some((directive) => {
|
|
396
396
|
const separator = directive.indexOf("=");
|
|
397
397
|
if (separator === -1) return false;
|
|
398
398
|
const name = directive.slice(0, separator).trim().toLowerCase();
|
|
@@ -482,7 +482,7 @@ function validateBuildIdentity(response, expectedBuildId) {
|
|
|
482
482
|
return null;
|
|
483
483
|
}
|
|
484
484
|
function isExpectedTerminalStatus(status) {
|
|
485
|
-
return status >= 300 && status < 400 || status === 404;
|
|
485
|
+
return status >= 300 && status < 400 || status === 401 || status === 403 || status === 404;
|
|
486
486
|
}
|
|
487
487
|
function validateRscWarmResponse(response, expectedBuildId, expectedRscBuildId, requireCacheHit = false, statusSource = "cloudflare") {
|
|
488
488
|
const buildIdentityValidation = validateBuildIdentity(response, expectedBuildId);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vinext/cloudflare",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Cloudflare cache and image adapters for vinext.",
|
|
5
5
|
"homepage": "https://vinext.dev/",
|
|
6
6
|
"license": "MIT",
|
|
@@ -48,21 +48,21 @@
|
|
|
48
48
|
}
|
|
49
49
|
},
|
|
50
50
|
"dependencies": {
|
|
51
|
-
"@cloudflare/workers-response-store": "1.
|
|
51
|
+
"@cloudflare/workers-response-store": "1.1.0"
|
|
52
52
|
},
|
|
53
53
|
"devDependencies": {
|
|
54
54
|
"@types/node": "^25.9.2",
|
|
55
55
|
"cf": "1.0.0-beta.1",
|
|
56
56
|
"miniflare": "5.20260730.0-alpha",
|
|
57
|
-
"vite": "npm:@voidzero-dev/vite-plus-core@
|
|
58
|
-
"vite-plus": "
|
|
59
|
-
"vitest": "
|
|
60
|
-
"vinext": "1.
|
|
57
|
+
"vite": "npm:@voidzero-dev/vite-plus-core@1.1.0",
|
|
58
|
+
"vite-plus": "1.1.0",
|
|
59
|
+
"vitest": "5.0.3",
|
|
60
|
+
"vinext": "1.1.0"
|
|
61
61
|
},
|
|
62
62
|
"peerDependencies": {
|
|
63
63
|
"@cloudflare/vite-plugin": "*",
|
|
64
64
|
"cf": "*",
|
|
65
|
-
"vinext": "^1.
|
|
65
|
+
"vinext": "^1.1.0"
|
|
66
66
|
},
|
|
67
67
|
"peerDependenciesMeta": {
|
|
68
68
|
"@cloudflare/vite-plugin": {
|