@rangojs/router 0.0.0-experimental.132 → 0.0.0-experimental.133
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/AGENTS.md +8 -0
- package/README.md +43 -2
- package/dist/bin/rango.js +92 -16
- package/dist/vite/index.js +166 -70
- package/package.json +19 -18
- package/skills/breadcrumbs/SKILL.md +1 -1
- package/skills/bundle-analysis/SKILL.md +2 -2
- package/skills/cache-guide/SKILL.md +2 -2
- package/skills/caching/SKILL.md +16 -9
- package/skills/debug-manifest/SKILL.md +4 -2
- package/skills/document-cache/SKILL.md +2 -2
- package/skills/handler-use/SKILL.md +1 -1
- package/skills/hooks/SKILL.md +2 -2
- package/skills/host-router/SKILL.md +1 -1
- package/skills/intercept/SKILL.md +1 -1
- package/skills/loader/SKILL.md +2 -0
- package/skills/migrate-react-router/SKILL.md +4 -2
- package/skills/mime-routes/SKILL.md +1 -1
- package/skills/prerender/SKILL.md +2 -0
- package/skills/rango/SKILL.md +12 -11
- package/skills/response-routes/SKILL.md +2 -2
- package/skills/route/SKILL.md +4 -0
- package/skills/router-setup/SKILL.md +3 -0
- package/skills/scripts/SKILL.md +179 -0
- package/skills/testing/SKILL.md +1 -1
- package/skills/testing/bindings.md +20 -6
- package/skills/testing/cache-prerender.md +5 -2
- package/skills/testing/client-components.md +2 -0
- package/skills/testing/e2e-parity.md +1 -1
- package/skills/testing/flight.md +8 -9
- package/skills/testing/render-handler.md +1 -1
- package/skills/testing/response-routes.md +1 -1
- package/skills/testing/server-actions.md +11 -11
- package/skills/testing/setup.md +3 -0
- package/skills/typesafety/SKILL.md +3 -2
- package/skills/use-cache/SKILL.md +10 -9
- package/src/browser/event-controller.ts +109 -2
- package/src/browser/partial-update.ts +12 -0
- package/src/browser/prefetch/cache.ts +17 -0
- package/src/browser/prefetch/fetch.ts +69 -2
- package/src/browser/react/Link.tsx +30 -5
- package/src/browser/react/NavigationProvider.tsx +12 -2
- package/src/browser/react/location-state-shared.ts +14 -2
- package/src/browser/react/use-href.tsx +8 -1
- package/src/browser/react/use-link-status.ts +23 -2
- package/src/browser/response-adapter.ts +14 -3
- package/src/browser/rsc-router.tsx +3 -0
- package/src/browser/scroll-restoration.ts +8 -3
- package/src/browser/server-action-bridge.ts +46 -11
- package/src/browser/types.ts +6 -0
- package/src/build/generate-route-types.ts +0 -1
- package/src/build/route-trie.ts +33 -9
- package/src/build/route-types/include-resolution.ts +7 -1
- package/src/build/route-types/router-processing.ts +0 -6
- package/src/build/route-types/source-scan.ts +105 -7
- package/src/cache/cache-policy.ts +42 -8
- package/src/cache/cache-runtime.ts +65 -5
- package/src/cache/cache-scope.ts +71 -11
- package/src/cache/cache-tag.ts +7 -2
- package/src/cache/cf/cf-base64.ts +33 -0
- package/src/cache/cf/cf-cache-constants.ts +127 -0
- package/src/cache/cf/cf-cache-store.ts +85 -613
- package/src/cache/cf/cf-cache-types.ts +349 -0
- package/src/cache/cf/cf-kv-utils.ts +46 -0
- package/src/cache/cf/cf-tag-marker-memo.ts +105 -0
- package/src/cache/document-cache.ts +11 -0
- package/src/cache/handle-snapshot.ts +8 -1
- package/src/cache/profile-registry.ts +25 -1
- package/src/cache/segment-codec.ts +9 -1
- package/src/cache/types.ts +4 -0
- package/src/client.rsc.tsx +38 -0
- package/src/client.tsx +11 -0
- package/src/components/DefaultDocument.tsx +8 -2
- package/src/context-var.ts +1 -1
- package/src/decode-loader-results.ts +7 -1
- package/src/escape-script.ts +52 -0
- package/src/handles/MetaTags.tsx +56 -5
- package/src/handles/Scripts.tsx +183 -0
- package/src/handles/breadcrumbs.ts +29 -11
- package/src/handles/is-thenable.ts +19 -0
- package/src/handles/meta.ts +46 -0
- package/src/handles/script.ts +244 -0
- package/src/host/cookie-handler.ts +7 -3
- package/src/host/pattern-matcher.ts +16 -2
- package/src/index.rsc.ts +5 -0
- package/src/index.ts +5 -0
- package/src/response-utils.ts +25 -0
- package/src/route-definition/dsl-helpers.ts +7 -0
- package/src/route-definition/redirect.ts +1 -2
- package/src/router/content-negotiation.ts +58 -10
- package/src/router/intercept-resolution.ts +9 -0
- package/src/router/match-middleware/cache-store.ts +10 -1
- package/src/router/middleware.ts +10 -3
- package/src/router/pattern-matching.ts +25 -23
- package/src/router/prefetch-cache-ttl.ts +51 -0
- package/src/router/router-interfaces.ts +7 -0
- package/src/router/router-options.ts +23 -0
- package/src/router/segment-resolution/fresh.ts +10 -0
- package/src/router/segment-resolution/helpers.ts +35 -1
- package/src/router/segment-resolution/loader-cache.ts +10 -6
- package/src/router/segment-resolution/revalidation.ts +6 -0
- package/src/router/segment-resolution.ts +1 -0
- package/src/router/trie-matching.ts +14 -9
- package/src/router.ts +18 -10
- package/src/rsc/handler.ts +52 -13
- package/src/rsc/helpers.ts +7 -1
- package/src/rsc/index.ts +1 -4
- package/src/rsc/loader-fetch.ts +107 -37
- package/src/rsc/progressive-enhancement.ts +18 -6
- package/src/rsc/response-cache-serve.ts +238 -0
- package/src/rsc/response-route-handler.ts +16 -133
- package/src/rsc/rsc-rendering.ts +13 -4
- package/src/rsc/server-action.ts +52 -6
- package/src/rsc/types.ts +7 -0
- package/src/search-params.ts +24 -5
- package/src/segment-loader-promise.ts +17 -2
- package/src/server/loader-registry.ts +16 -18
- package/src/server/request-context.ts +47 -20
- package/src/testing/dispatch.ts +108 -25
- package/src/testing/flight.ts +25 -0
- package/src/testing/internal/context.ts +25 -2
- package/src/testing/render-handler.ts +3 -1
- package/src/testing/render-route.tsx +15 -0
- package/src/testing/run-loader.ts +10 -3
- package/src/theme/ThemeProvider.tsx +20 -6
- package/src/theme/ThemeScript.tsx +7 -3
- package/src/theme/constants.ts +54 -3
- package/src/theme/theme-script.ts +22 -7
- package/src/types/request-scope.ts +8 -3
- package/src/vite/plugins/cjs-to-esm.ts +8 -1
- package/src/vite/plugins/expose-id-utils.ts +10 -1
- package/src/vite/plugins/expose-ids/handler-transform.ts +5 -16
- package/src/vite/plugins/expose-ids/loader-transform.ts +12 -5
- package/src/vite/plugins/expose-ids/router-transform.ts +6 -1
- package/src/vite/plugins/expose-internal-ids.ts +0 -1
- package/src/vite/plugins/version-plugin.ts +5 -17
- package/src/vite/plugins/virtual-entries.ts +12 -2
- package/src/vite/rango.ts +15 -6
- package/src/vite/utils/ast-handler-extract.ts +11 -4
- package/src/vite/utils/directive-prologue.ts +40 -0
- package/src/vite/utils/prerender-utils.ts +17 -2
|
@@ -51,172 +51,60 @@ import {
|
|
|
51
51
|
} from "../cache-policy.js";
|
|
52
52
|
import { reportCacheError, reportingAsync } from "../cache-error.js";
|
|
53
53
|
import type { CacheErrorCategory } from "../cache-error.js";
|
|
54
|
+
import { bufferToBase64, base64ToBuffer } from "./cf-base64.js";
|
|
55
|
+
import {
|
|
56
|
+
KV_MAX_KEY_BYTES,
|
|
57
|
+
KV_MIN_EXPIRATION_TTL,
|
|
58
|
+
kvKeyByteLength,
|
|
59
|
+
remainingCacheControl,
|
|
60
|
+
} from "./cf-kv-utils.js";
|
|
61
|
+
import {
|
|
62
|
+
TAG_MARKER_CACHE_PREFIX,
|
|
63
|
+
TAG_MARKER_ABSENT,
|
|
64
|
+
getTagMarkerMemo,
|
|
65
|
+
getTagMarkerInflight,
|
|
66
|
+
} from "./cf-tag-marker-memo.js";
|
|
54
67
|
|
|
55
68
|
// ============================================================================
|
|
56
69
|
// Constants
|
|
57
70
|
// ============================================================================
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
* never "no marker" - absence is only ever represented by this cached sentinel.
|
|
96
|
-
*/
|
|
97
|
-
const TAG_MARKER_ABSENT = "none";
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* Header storing the epoch-ms timestamp when an entry was marked REVALIDATING.
|
|
101
|
-
* The SWR thundering-herd guard reads this to decide whether the in-flight
|
|
102
|
-
* revalidation is still recent. It replaces a prior reliance on the HTTP `Age`
|
|
103
|
-
* header: CF's Cache API does not populate `Age` reliably per-colo (and our own
|
|
104
|
-
* unit MockCache never set it), so an absent `Age` defaulted to 0 and made every
|
|
105
|
-
* REVALIDATING entry look "just revalidated" forever -- a dropped/never-finished
|
|
106
|
-
* background revalidation could then pin an entry stale until hard expiry. An
|
|
107
|
-
* explicit timestamp we write ourselves (same pattern as CACHE_STALE_AT_HEADER)
|
|
108
|
-
* is reliable and lets the MAX_REVALIDATION_INTERVAL re-arm actually fire.
|
|
109
|
-
*/
|
|
110
|
-
export const CACHE_REVALIDATING_AT_HEADER = "x-edge-cache-revalidating-at";
|
|
111
|
-
|
|
112
|
-
/**
|
|
113
|
-
* Header storing the absolute epoch-ms hard-expiry deadline (staleAt +
|
|
114
|
-
* swrWindow*1000) of an L1 entry. The stale-path REVALIDATING re-put reads this
|
|
115
|
-
* to recompute a SHRINKING Cache-Control max-age instead of copying set()'s
|
|
116
|
-
* original full-window max-age. Without it, every MAX_REVALIDATION_INTERVAL
|
|
117
|
-
* re-arm re-puts the full window and restarts CF's retention clock, pinning a
|
|
118
|
-
* perpetually-stale entry (one whose background revalidation keeps failing) past
|
|
119
|
-
* its intended hard-expiry indefinitely. Mirrors the KVSegmentEnvelope `e`
|
|
120
|
-
* field and the remaining-ttl math in promoteSegmentToL1/promoteItemToL1.
|
|
121
|
-
* @internal
|
|
122
|
-
*/
|
|
123
|
-
const CACHE_EXPIRES_AT_HEADER = "x-edge-cache-expires-at";
|
|
124
|
-
|
|
125
|
-
/**
|
|
126
|
-
* Header stashing the route author's original Cache-Control on L1 document
|
|
127
|
-
* entries. putResponse/promoteResponseToL1 overwrite Cache-Control with a long
|
|
128
|
-
* `max-age` so the CF Cache API retains the entry across the whole SWR window;
|
|
129
|
-
* getResponse restores this original value before serving so the client and any
|
|
130
|
-
* upstream CDN see the author's intended directive, not the internal edge TTL.
|
|
131
|
-
*/
|
|
132
|
-
const CACHE_ORIG_CC_HEADER = "x-edge-cache-orig-cc";
|
|
133
|
-
|
|
134
|
-
/**
|
|
135
|
-
* Maximum age in seconds for REVALIDATING status before allowing new revalidation.
|
|
136
|
-
* After this period, a stale entry in REVALIDATING status will trigger revalidation again.
|
|
137
|
-
* @internal
|
|
138
|
-
*/
|
|
139
|
-
export const MAX_REVALIDATION_INTERVAL = 30;
|
|
140
|
-
|
|
141
|
-
/**
|
|
142
|
-
* Per-request memo of tag-invalidation markers (tag -> latest invalidatedAt, or
|
|
143
|
-
* null when no marker exists). Keyed first by the request context object (so it
|
|
144
|
-
* is naturally request-scoped and garbage-collected with the request) and then
|
|
145
|
-
* by the store INSTANCE.
|
|
146
|
-
*
|
|
147
|
-
* The per-store nesting matters because a single request can run more than one
|
|
148
|
-
* CFCacheStore - the app-level store plus a route's `cache({ store })` override,
|
|
149
|
-
* which may point at a DIFFERENT KV binding or version. A module-level map keyed
|
|
150
|
-
* by request alone (the inner map keyed by the raw tag name) would let store B's
|
|
151
|
-
* memoized marker for a tag mask store A's own KV marker, so A could serve an
|
|
152
|
-
* entry A's own KV says is invalidated. Keying by the instance isolates them;
|
|
153
|
-
* two reads through the SAME store still share the memo. A read through one
|
|
154
|
-
* store never populates another's memo, so each store always consults its own KV
|
|
155
|
-
* binding. Markers are read only through isGloballyInvalidated(), which already
|
|
156
|
-
* short-circuits when a store has no KV, so a store without KV never allocates.
|
|
157
|
-
*
|
|
158
|
-
* Without the memo, isGloballyInvalidated() issues a KV read per tag on every
|
|
159
|
-
* tagged cache read, so a page composed of many segments/items sharing a tag
|
|
160
|
-
* pays that cost N times. The memo collapses it to one KV read per distinct tag
|
|
161
|
-
* per (request, store). invalidateTags() writes through so a same-request
|
|
162
|
-
* updateTag() stays read-your-own-writes consistent (the action's own re-render
|
|
163
|
-
* sees its own invalidation from the memo, without a re-read).
|
|
164
|
-
*
|
|
165
|
-
* It does NOT span requests, so a hot single-entry route still pays one KV read
|
|
166
|
-
* per request; that read hits Cloudflare KV's own edge read cache for hot keys.
|
|
167
|
-
*/
|
|
168
|
-
const tagMarkerMemo = new WeakMap<
|
|
169
|
-
object,
|
|
170
|
-
WeakMap<object, Map<string, number | null>>
|
|
171
|
-
>();
|
|
172
|
-
|
|
173
|
-
function getTagMarkerMemo(
|
|
174
|
-
ctx: object,
|
|
175
|
-
store: object,
|
|
176
|
-
): Map<string, number | null> {
|
|
177
|
-
let byStore = tagMarkerMemo.get(ctx);
|
|
178
|
-
if (!byStore) {
|
|
179
|
-
byStore = new WeakMap();
|
|
180
|
-
tagMarkerMemo.set(ctx, byStore);
|
|
181
|
-
}
|
|
182
|
-
let memo = byStore.get(store);
|
|
183
|
-
if (!memo) {
|
|
184
|
-
memo = new Map();
|
|
185
|
-
byStore.set(store, memo);
|
|
186
|
-
}
|
|
187
|
-
return memo;
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
/**
|
|
191
|
-
* Per-request map of IN-FLIGHT marker reads (tag -> the pending read promise).
|
|
192
|
-
* The resolved-value memo above only collapses SEQUENTIAL reads of a tag; the
|
|
193
|
-
* router resolves sibling segments in PARALLEL, so without this several
|
|
194
|
-
* concurrently-resolving segments sharing a tag would each issue their own KV
|
|
195
|
-
* read before any of them populates the memo. Sharing the in-flight promise
|
|
196
|
-
* collapses those to a single KV read. Entries are dropped once resolved (the
|
|
197
|
-
* value is then in the memo), so this only spans the concurrent read window.
|
|
198
|
-
*/
|
|
199
|
-
const tagMarkerInflight = new WeakMap<
|
|
200
|
-
object,
|
|
201
|
-
WeakMap<object, Map<string, Promise<number | null>>>
|
|
202
|
-
>();
|
|
203
|
-
|
|
204
|
-
function getTagMarkerInflight(
|
|
205
|
-
ctx: object,
|
|
206
|
-
store: object,
|
|
207
|
-
): Map<string, Promise<number | null>> {
|
|
208
|
-
let byStore = tagMarkerInflight.get(ctx);
|
|
209
|
-
if (!byStore) {
|
|
210
|
-
byStore = new WeakMap();
|
|
211
|
-
tagMarkerInflight.set(ctx, byStore);
|
|
212
|
-
}
|
|
213
|
-
let inflight = byStore.get(store);
|
|
214
|
-
if (!inflight) {
|
|
215
|
-
inflight = new Map();
|
|
216
|
-
byStore.set(store, inflight);
|
|
217
|
-
}
|
|
218
|
-
return inflight;
|
|
219
|
-
}
|
|
71
|
+
//
|
|
72
|
+
// Header names, KV prefixes, and timeout/interval defaults live in
|
|
73
|
+
// cf-cache-constants.ts so collaborator modules can share them without a
|
|
74
|
+
// circular import back to this class. They are re-exported below so existing
|
|
75
|
+
// import paths (`../cf-cache-store`, `./cf-cache-store.js`) still resolve.
|
|
76
|
+
import {
|
|
77
|
+
CACHE_STALE_AT_HEADER,
|
|
78
|
+
CACHE_STATUS_HEADER,
|
|
79
|
+
CACHE_TAGS_HEADER,
|
|
80
|
+
CACHE_TAGGED_AT_HEADER,
|
|
81
|
+
TAG_MARKER_PREFIX,
|
|
82
|
+
CACHE_REVALIDATING_AT_HEADER,
|
|
83
|
+
CACHE_EXPIRES_AT_HEADER,
|
|
84
|
+
CACHE_ORIG_CC_HEADER,
|
|
85
|
+
MAX_REVALIDATION_INTERVAL,
|
|
86
|
+
EDGE_LOOKUP_TIMEOUT_MS,
|
|
87
|
+
EDGE_READ_TIMEOUT_MS,
|
|
88
|
+
KV_READ_TIMEOUT_MS,
|
|
89
|
+
} from "./cf-cache-constants.js";
|
|
90
|
+
|
|
91
|
+
// Re-export the public constants so consumers/tests importing them from
|
|
92
|
+
// cf-cache-store keep working after the move.
|
|
93
|
+
export {
|
|
94
|
+
CACHE_STALE_AT_HEADER,
|
|
95
|
+
CACHE_STATUS_HEADER,
|
|
96
|
+
CACHE_TAGS_HEADER,
|
|
97
|
+
CACHE_TAGGED_AT_HEADER,
|
|
98
|
+
TAG_MARKER_PREFIX,
|
|
99
|
+
CACHE_REVALIDATING_AT_HEADER,
|
|
100
|
+
MAX_REVALIDATION_INTERVAL,
|
|
101
|
+
EDGE_LOOKUP_TIMEOUT_MS,
|
|
102
|
+
EDGE_READ_TIMEOUT_MS,
|
|
103
|
+
KV_READ_TIMEOUT_MS,
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
// The tag-marker prefix/sentinel and per-request memo helpers (with their
|
|
107
|
+
// module-singleton WeakMaps) live in cf-tag-marker-memo.ts; imported above.
|
|
220
108
|
|
|
221
109
|
/**
|
|
222
110
|
* Per-request memo of the derived cache-key base URL.
|
|
@@ -231,23 +119,8 @@ function getTagMarkerInflight(
|
|
|
231
119
|
*/
|
|
232
120
|
const derivedBaseUrlMemo = new WeakMap<object, string>();
|
|
233
121
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
/**
|
|
238
|
-
* Cloudflare KV's minimum `expirationTtl` (seconds). A `put` with a smaller
|
|
239
|
-
* expirationTtl is rejected outright. Tag-invalidation markers (the only writes
|
|
240
|
-
* that take a consumer-supplied TTL via tagInvalidationTtl) are floored to this
|
|
241
|
-
* so a too-small value cannot make EVERY updateTag/revalidateTag throw.
|
|
242
|
-
*/
|
|
243
|
-
const KV_MIN_EXPIRATION_TTL = 60;
|
|
244
|
-
|
|
245
|
-
const kvKeyEncoder = new TextEncoder();
|
|
246
|
-
|
|
247
|
-
/** UTF-8 byte length of a KV key (multibyte tags can exceed the char count). */
|
|
248
|
-
function kvKeyByteLength(key: string): number {
|
|
249
|
-
return kvKeyEncoder.encode(key).length;
|
|
250
|
-
}
|
|
122
|
+
// Pure KV helpers (key byte-length limits, expirationTtl floor, stale-path
|
|
123
|
+
// Cache-Control recompute) live in cf-kv-utils.ts; imported above.
|
|
251
124
|
|
|
252
125
|
/**
|
|
253
126
|
* Stores (by namespace) already warned about tag machinery configured without a
|
|
@@ -263,97 +136,26 @@ const warnedNoKvReadInvalidation = new Set<string>();
|
|
|
263
136
|
*/
|
|
264
137
|
const warnedTagInvalidationTtlFloor = new Set<string>();
|
|
265
138
|
|
|
266
|
-
/**
|
|
267
|
-
* Maximum time (ms) to wait for an L1 edge cache (CF Cache API) read before
|
|
268
|
-
* giving up and treating it as a miss. The Cache API is normally sub-millisecond
|
|
269
|
-
* per-colo, so a slow `match` signals a degraded colo; we don't want it adding
|
|
270
|
-
* latency to the request. On timeout the lookup is abandoned, a warning is
|
|
271
|
-
* logged, and the read falls through to its normal miss path (L2/KV or render).
|
|
272
|
-
*
|
|
273
|
-
* This is the default; override per store via
|
|
274
|
-
* `CFCacheStoreOptions.edgeLookupTimeoutMs` (<= 0 disables the budget).
|
|
275
|
-
*/
|
|
276
|
-
export const EDGE_LOOKUP_TIMEOUT_MS = 10;
|
|
277
|
-
|
|
278
|
-
/**
|
|
279
|
-
* Maximum time (ms) to wait for the BODY of a matched L1 entry to be read
|
|
280
|
-
* (response.json()) before treating the read as a miss.
|
|
281
|
-
*
|
|
282
|
-
* This is separate from {@link EDGE_LOOKUP_TIMEOUT_MS} on purpose. CF's Cache
|
|
283
|
-
* API resolves `match()` with a lazily-streamed body, so a fast `match` can be
|
|
284
|
-
* followed by a multi-second stall while the body bytes are fetched -- the
|
|
285
|
-
* latency tail lives here, after the match budget has already passed. The
|
|
286
|
-
* default bounds that tail aggressively: a healthy per-colo body read (fetch +
|
|
287
|
-
* JSON parse) settles in low single-digit milliseconds, so 20ms clears a
|
|
288
|
-
* healthy read while still failing fast to L2/KV (or render) on a degraded colo
|
|
289
|
-
* instead of pinning the request behind a seconds-long read. Raise it per store
|
|
290
|
-
* if large Flight payloads legitimately need longer.
|
|
291
|
-
*
|
|
292
|
-
* Override per store via `CFCacheStoreOptions.edgeReadTimeoutMs` (<= 0 disables).
|
|
293
|
-
*/
|
|
294
|
-
export const EDGE_READ_TIMEOUT_MS = 20;
|
|
295
|
-
|
|
296
|
-
/**
|
|
297
|
-
* Maximum time (ms) to wait for an L2 (KV) read (`kv.get(key, {type:"json"})`)
|
|
298
|
-
* before treating it as a miss. Unlike the L1 budgets, KV is a GLOBAL store: the
|
|
299
|
-
* file header documents ~50ms healthy reads, and a degraded namespace can tail
|
|
300
|
-
* to seconds. KV is the LAST cache tier before a full render, so an unbounded
|
|
301
|
-
* read here pins the whole request behind a degraded global lookup.
|
|
302
|
-
*
|
|
303
|
-
* The default (170ms) sits a few multiples above the documented ~50ms healthy
|
|
304
|
-
* read, leaving headroom for legitimate latency tails (larger payloads,
|
|
305
|
-
* far-from-colo regions) so a healthy-but-slow read does not false-miss into a
|
|
306
|
-
* render, while still abandoning a genuinely degraded namespace well before its
|
|
307
|
-
* multi-second tail can pin the request. A deployment with a tighter SLA can
|
|
308
|
-
* lower it, and one whose healthy p99 runs higher should raise it: measure the
|
|
309
|
-
* KV read p99 (Workers Analytics) and add margin. It is a degradation
|
|
310
|
-
* guard-rail, not a tuning lever for "slow KV is normal here".
|
|
311
|
-
*
|
|
312
|
-
* Override per store via `CFCacheStoreOptions.kvReadTimeoutMs` (<= 0 disables).
|
|
313
|
-
*/
|
|
314
|
-
export const KV_READ_TIMEOUT_MS = 170;
|
|
315
|
-
|
|
316
|
-
/**
|
|
317
|
-
* Compute the Cache-Control directive for a stale-path REVALIDATING re-put from
|
|
318
|
-
* the entry's stored hard-expiry deadline (CACHE_EXPIRES_AT_HEADER). Returns the
|
|
319
|
-
* REMAINING ttl so the re-put preserves the original retention deadline instead
|
|
320
|
-
* of restarting it -- copying set()'s original full-window max-age would reset
|
|
321
|
-
* CF's retention clock on every re-arm and pin a perpetually-stale entry forever.
|
|
322
|
-
* An entry lacking a valid deadline (legacy/tampered) floors to max-age=1, so it
|
|
323
|
-
* hard-expires in ~1s and self-heals via KV. Mirrors promoteSegmentToL1's math.
|
|
324
|
-
* @internal
|
|
325
|
-
*/
|
|
326
|
-
function remainingCacheControl(headers: Headers, now: number): string {
|
|
327
|
-
const expiresAt = Number(headers.get(CACHE_EXPIRES_AT_HEADER));
|
|
328
|
-
const remainingTtl =
|
|
329
|
-
Number.isFinite(expiresAt) && expiresAt > 0
|
|
330
|
-
? Math.max(1, Math.floor((expiresAt - now) / 1000))
|
|
331
|
-
: 1;
|
|
332
|
-
return `public, max-age=${remainingTtl}`;
|
|
333
|
-
}
|
|
334
|
-
|
|
335
139
|
// ============================================================================
|
|
336
140
|
// Types
|
|
337
141
|
// ============================================================================
|
|
338
|
-
|
|
339
|
-
//
|
|
340
|
-
//
|
|
341
|
-
//
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
delete(key: string): Promise<void>;
|
|
356
|
-
}
|
|
142
|
+
//
|
|
143
|
+
// The shared public types (KVNamespace, CFCacheReadDebugEvent, CFCacheDebug,
|
|
144
|
+
// CFCacheStoreOptions) live in cf-cache-types.ts; imported and re-exported below
|
|
145
|
+
// so existing import paths still resolve. The private KV envelope interfaces
|
|
146
|
+
// stay here with the methods that read/write them.
|
|
147
|
+
import type {
|
|
148
|
+
KVNamespace,
|
|
149
|
+
CFCacheReadDebugEvent,
|
|
150
|
+
CFCacheDebug,
|
|
151
|
+
CFCacheStoreOptions,
|
|
152
|
+
} from "./cf-cache-types.js";
|
|
153
|
+
export type {
|
|
154
|
+
KVNamespace,
|
|
155
|
+
CFCacheReadDebugEvent,
|
|
156
|
+
CFCacheDebug,
|
|
157
|
+
CFCacheStoreOptions,
|
|
158
|
+
};
|
|
357
159
|
|
|
358
160
|
/**
|
|
359
161
|
* KV envelope for segment cache entries.
|
|
@@ -410,325 +212,6 @@ interface KVResponseEnvelope {
|
|
|
410
212
|
ta?: number;
|
|
411
213
|
}
|
|
412
214
|
|
|
413
|
-
/**
|
|
414
|
-
* One L1 read decision, surfaced when `debug` is enabled. Lets an operator
|
|
415
|
-
* confirm on a real deployment (e.g. via `wrangler tail`) that the store's
|
|
416
|
-
* observed inputs match its decision: which tier answered, the entry's status,
|
|
417
|
-
* the stale/revalidating timestamps, the raw CF `Age` header (so its
|
|
418
|
-
* unreliability can be seen next to the explicit revalidating-at stamp), and
|
|
419
|
-
* the measured match/body-read durations (where the latency tail shows up).
|
|
420
|
-
*/
|
|
421
|
-
export interface CFCacheReadDebugEvent {
|
|
422
|
-
/**
|
|
423
|
-
* Which read method produced this event. Only the JSON read paths (segment
|
|
424
|
-
* `get` and function `getItem`) participate in debug; the document
|
|
425
|
-
* `getResponse` path streams its body and is intentionally out of scope.
|
|
426
|
-
*/
|
|
427
|
-
op: "get" | "getItem";
|
|
428
|
-
/** Cache key (without the internal fn:/doc: prefix or version path). */
|
|
429
|
-
key: string;
|
|
430
|
-
/**
|
|
431
|
-
* What the read resolved to:
|
|
432
|
-
* - l1-fresh / l1-stale-revalidate / l1-revalidating-guarded: L1 hit outcomes
|
|
433
|
-
* - match-timeout / body-timeout: the L1 latency budgets fired
|
|
434
|
-
* - match-error: the L1 match() itself rejected (a transient Cache API infra
|
|
435
|
-
* error) -- a miss that falls through to L2/KV and is reported cache-read,
|
|
436
|
-
* distinct from a genuine l1-miss (absence) so the two are separable
|
|
437
|
-
* - body-error: the L1 body read failed fast (corrupt/non-JSON body) -- a miss
|
|
438
|
-
* that falls through to L2/KV, distinct from a body-timeout
|
|
439
|
-
* - non-200: L1 returned a non-200 (treated as a miss)
|
|
440
|
-
* - l1-miss: no L1 entry
|
|
441
|
-
* - kv-fresh / kv-stale / kv-miss: L2 fallback outcomes
|
|
442
|
-
* - kv-stale-suppressed: a stale L2 hit served WITHOUT revalidation because
|
|
443
|
-
* the L1 fall-through was degraded (body-timeout / non-200) -- the herd
|
|
444
|
-
* mitigation, distinct from kv-stale so the suppression is visible
|
|
445
|
-
* - kv-timeout: the L2/KV read budget fired (read abandoned, NOT a genuine
|
|
446
|
-
* absence -- distinct from kv-miss so a degradation signal is separable)
|
|
447
|
-
* - tag-invalidated: a live L1/KV entry whose cache tags were invalidated
|
|
448
|
-
* after it was written -- treated as a miss so the next render re-populates
|
|
449
|
-
* it (the tag-invalidation read path, distinct from a plain miss)
|
|
450
|
-
* - error: the read threw
|
|
451
|
-
*/
|
|
452
|
-
outcome:
|
|
453
|
-
| "l1-fresh"
|
|
454
|
-
| "l1-stale-revalidate"
|
|
455
|
-
| "l1-revalidating-guarded"
|
|
456
|
-
| "match-timeout"
|
|
457
|
-
| "match-error"
|
|
458
|
-
| "body-timeout"
|
|
459
|
-
| "body-error"
|
|
460
|
-
| "non-200"
|
|
461
|
-
| "tag-invalidated"
|
|
462
|
-
| "l1-miss"
|
|
463
|
-
| "kv-fresh"
|
|
464
|
-
| "kv-stale"
|
|
465
|
-
| "kv-stale-suppressed"
|
|
466
|
-
| "kv-miss"
|
|
467
|
-
| "kv-timeout"
|
|
468
|
-
| "error";
|
|
469
|
-
/** HTTP status of the matched L1 response, when one was returned. */
|
|
470
|
-
status?: number;
|
|
471
|
-
/**
|
|
472
|
-
* Stored cache status header (CACHE_STATUS_HEADER): "HIT" or "REVALIDATING".
|
|
473
|
-
* Distinct from `isRevalidating`, which also factors in stamp recency -- this
|
|
474
|
-
* is the raw stored value, so a REVALIDATING entry whose stamp aged out (so
|
|
475
|
-
* `isRevalidating` is false) is still distinguishable from a plain HIT.
|
|
476
|
-
*/
|
|
477
|
-
cacheStatus?: string | null;
|
|
478
|
-
/** Epoch-ms when the entry goes stale (from CACHE_STALE_AT_HEADER). */
|
|
479
|
-
staleAt?: number;
|
|
480
|
-
/** Epoch-ms the entry was marked REVALIDATING (from the explicit stamp). */
|
|
481
|
-
revalidatingAt?: number;
|
|
482
|
-
/** Raw CF `Age` header, for comparison against revalidatingAt (may be null). */
|
|
483
|
-
ageHeader?: string | null;
|
|
484
|
-
isStale?: boolean;
|
|
485
|
-
isRevalidating?: boolean;
|
|
486
|
-
shouldRevalidate?: boolean;
|
|
487
|
-
/** Wall-clock ms spent in cache.match (bounded by edgeLookupTimeoutMs). */
|
|
488
|
-
matchMs?: number;
|
|
489
|
-
/**
|
|
490
|
-
* Wall-clock ms spent resolving the entry's tag-invalidation markers (the
|
|
491
|
-
* per-request memo -> optional per-colo L1 marker cache -> KV cascade), for a
|
|
492
|
-
* tagged entry. 0/absent for an untagged entry or a memo hit; a non-trivial
|
|
493
|
-
* value is the serial marker-read tail that sits between matchMs and
|
|
494
|
-
* bodyReadMs. Only measured when debug is enabled.
|
|
495
|
-
*/
|
|
496
|
-
markerMs?: number;
|
|
497
|
-
/** Wall-clock ms spent reading the body (bounded by edgeReadTimeoutMs). */
|
|
498
|
-
bodyReadMs?: number;
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
/**
|
|
502
|
-
* Debug sink. `true` logs each {@link CFCacheReadDebugEvent} to console; a
|
|
503
|
-
* function receives the events for programmatic capture.
|
|
504
|
-
*/
|
|
505
|
-
export type CFCacheDebug = boolean | ((event: CFCacheReadDebugEvent) => void);
|
|
506
|
-
|
|
507
|
-
export interface CFCacheStoreOptions<TEnv = unknown> {
|
|
508
|
-
/**
|
|
509
|
-
* Cache namespace. If not provided, uses caches.default (recommended).
|
|
510
|
-
* Only set this if you need isolated cache storage.
|
|
511
|
-
*/
|
|
512
|
-
namespace?: string;
|
|
513
|
-
|
|
514
|
-
/**
|
|
515
|
-
* Base URL for cache keys.
|
|
516
|
-
*
|
|
517
|
-
* If not provided, derives from request hostname via requestContext:
|
|
518
|
-
* - Production domains → uses `https://{hostname}/`
|
|
519
|
-
* - Dev/preview (localhost, workers.dev, pages.dev) → uses internal fallback URL
|
|
520
|
-
*/
|
|
521
|
-
baseUrl?: string;
|
|
522
|
-
|
|
523
|
-
/** Default cache options */
|
|
524
|
-
defaults?: CacheDefaults;
|
|
525
|
-
|
|
526
|
-
/**
|
|
527
|
-
* Cloudflare ExecutionContext for non-blocking cache writes.
|
|
528
|
-
* Pass the `ctx` from your worker's fetch handler.
|
|
529
|
-
*
|
|
530
|
-
* @example
|
|
531
|
-
* ```typescript
|
|
532
|
-
* new CFCacheStore({ ctx: env.ctx })
|
|
533
|
-
* ```
|
|
534
|
-
*/
|
|
535
|
-
ctx: ExecutionContext;
|
|
536
|
-
|
|
537
|
-
/**
|
|
538
|
-
* Optional KV namespace for L2 cache persistence.
|
|
539
|
-
*
|
|
540
|
-
* When provided, KV acts as a global fallback behind the per-colo Cache API.
|
|
541
|
-
* On L1 miss, KV is checked and hits are promoted back to L1.
|
|
542
|
-
* On writes, data is persisted to both L1 and KV.
|
|
543
|
-
*
|
|
544
|
-
* @example
|
|
545
|
-
* ```typescript
|
|
546
|
-
* new CFCacheStore({ ctx: env.ctx, kv: env.CACHE_KV })
|
|
547
|
-
* ```
|
|
548
|
-
*
|
|
549
|
-
* Tag-based invalidation (updateTag/revalidateTag) requires KV: the
|
|
550
|
-
* tag-invalidation markers are stored in this same namespace. There is no
|
|
551
|
-
* separate tag-invalidation store to configure.
|
|
552
|
-
*/
|
|
553
|
-
kv?: KVNamespace;
|
|
554
|
-
|
|
555
|
-
/**
|
|
556
|
-
* Optional eager-purge hook, called ONCE per updateTag()/revalidateTag() with
|
|
557
|
-
* the namespaced Cloudflare Cache-Tags to purge (one batched call for the
|
|
558
|
-
* whole invalidation, not one per tag). These exactly match the `Cache-Tag`
|
|
559
|
-
* header this store writes on its tag-lookup marker entries
|
|
560
|
-
* (`rg:{namespace}:lk:{encodedTag}`), so forwarding them to Cloudflare's
|
|
561
|
-
* purge-by-tag API evicts the cached lookups in every colo - making
|
|
562
|
-
* cross-colo invalidation prompt instead of waiting out `tagCacheTtl`.
|
|
563
|
-
*
|
|
564
|
-
* Only meaningful with `tagCacheTtl > 0` (otherwise there are no cached
|
|
565
|
-
* lookups to purge). The values are pre-encoded, so commas in tag names are
|
|
566
|
-
* safe to pass straight to the purge API.
|
|
567
|
-
*
|
|
568
|
-
* @example
|
|
569
|
-
* ```ts
|
|
570
|
-
* onRevalidateTag: async (cacheTags) => {
|
|
571
|
-
* await fetch(`https://api.cloudflare.com/client/v4/zones/${ZONE}/purge_cache`, {
|
|
572
|
-
* method: "POST",
|
|
573
|
-
* headers: { Authorization: `Bearer ${TOKEN}`, "Content-Type": "application/json" },
|
|
574
|
-
* body: JSON.stringify({ tags: cacheTags }),
|
|
575
|
-
* });
|
|
576
|
-
* }
|
|
577
|
-
* ```
|
|
578
|
-
*/
|
|
579
|
-
onRevalidateTag?: (cacheTags: string[]) => Promise<void>;
|
|
580
|
-
|
|
581
|
-
/**
|
|
582
|
-
* Optional expiration (seconds) for tag-invalidation markers in KV. A marker
|
|
583
|
-
* must outlive every entry tagged before the invalidation, so this MUST
|
|
584
|
-
* exceed your largest entry TTL+SWR. Defaults to no expiration (markers
|
|
585
|
-
* persist; they are tiny - one timestamp per distinct invalidated tag).
|
|
586
|
-
*
|
|
587
|
-
* Note the opposite sizing from `tagCacheTtl` below: `tagInvalidationTtl` must
|
|
588
|
-
* be LARGE (outlive data); `tagCacheTtl` should be SMALL (a staleness ceiling).
|
|
589
|
-
*
|
|
590
|
-
* Cardinality matters: each DISTINCT invalidated tag writes one permanent KV
|
|
591
|
-
* marker (with the no-expiry default). Keep tags LOW-cardinality and never
|
|
592
|
-
* derive an invalidation tag from untrusted input (e.g.
|
|
593
|
-
* `revalidateTag(req.query.tag)`) - an attacker could otherwise grow your KV
|
|
594
|
-
* namespace without bound. Set a `tagInvalidationTtl` only if your tags are
|
|
595
|
-
* unavoidably high-cardinality AND it can still safely exceed your max entry
|
|
596
|
-
* TTL+SWR.
|
|
597
|
-
*/
|
|
598
|
-
tagInvalidationTtl?: number;
|
|
599
|
-
|
|
600
|
-
/**
|
|
601
|
-
* Optional TTL (seconds) for caching tag-invalidation markers in the per-colo
|
|
602
|
-
* Cache API (L1), to avoid a KV marker read on every tagged cache read.
|
|
603
|
-
*
|
|
604
|
-
* Default `0` = disabled: the marker is read from KV on every tagged read
|
|
605
|
-
* (today's behavior), giving the strongest cross-colo invalidation latency
|
|
606
|
-
* (~KV consistency). A positive value caches each marker (including the
|
|
607
|
-
* "no marker yet" state) in L1 for that many seconds, so within the window a
|
|
608
|
-
* colo answers from L1 with no KV read.
|
|
609
|
-
*
|
|
610
|
-
* The colo that runs `updateTag`/`revalidateTag` writes the fresh marker
|
|
611
|
-
* straight into its own L1 (write-through), so the invalidating request and
|
|
612
|
-
* later reads in that colo observe the invalidation immediately. One caveat: a
|
|
613
|
-
* read already in flight when the invalidation lands (one that began its KV
|
|
614
|
-
* marker fetch first) can re-cache the PRIOR marker into L1 after the
|
|
615
|
-
* write-through, so a racing concurrent reader in the same colo may miss the
|
|
616
|
-
* invalidation for up to `tagCacheTtl` -- the Cache API exposes no
|
|
617
|
-
* compare-and-set to close this fully. `tagCacheTtl` is therefore a staleness
|
|
618
|
-
* CEILING, not a promise of zero same-colo latency; keep it small (or wire
|
|
619
|
-
* `onRevalidateTag`) when that matters. By default OTHER colos only converge
|
|
620
|
-
* when their cached marker expires, so `tagCacheTtl` is the MAXIMUM extra
|
|
621
|
-
* cross-colo invalidation latency for them. Recommended 30-60 for high-read,
|
|
622
|
-
* low-mutation tags; leave at 0 when prompt global invalidation matters and
|
|
623
|
-
* you cannot wire a purge.
|
|
624
|
-
*
|
|
625
|
-
* To make other colos prompt WITHOUT a short TTL, wire `onRevalidateTag` to a
|
|
626
|
-
* Cloudflare purge-by-tag call: each marker entry carries a namespaced
|
|
627
|
-
* `Cache-Tag`, and `onRevalidateTag` is handed exactly those tags to purge, so
|
|
628
|
-
* the cached lookups are evicted everywhere on invalidation. With a purge
|
|
629
|
-
* wired, `tagCacheTtl` becomes purely a read-cost reducer + fallback window
|
|
630
|
-
* (safe to set large) rather than the invalidation-latency ceiling.
|
|
631
|
-
*/
|
|
632
|
-
tagCacheTtl?: number;
|
|
633
|
-
|
|
634
|
-
/**
|
|
635
|
-
* Cache version string override. When this changes, all cached entries are
|
|
636
|
-
* effectively invalidated (new keys won't match old entries).
|
|
637
|
-
*
|
|
638
|
-
* Defaults to the auto-generated VERSION from the `@rangojs/router:version` virtual module.
|
|
639
|
-
* Only set this if you need a custom versioning strategy.
|
|
640
|
-
*/
|
|
641
|
-
version?: string;
|
|
642
|
-
|
|
643
|
-
/**
|
|
644
|
-
* Latency budget (ms) for an L1 edge cache (CF Cache API) read. A `match`
|
|
645
|
-
* slower than this is abandoned and treated as a miss, so a degraded colo
|
|
646
|
-
* cannot stall the request; the read then falls through to its normal miss
|
|
647
|
-
* path (L2/KV or render).
|
|
648
|
-
*
|
|
649
|
-
* Defaults to {@link EDGE_LOOKUP_TIMEOUT_MS} (10). Set to 0 (or any value
|
|
650
|
-
* <= 0) to disable the budget and always await `match`.
|
|
651
|
-
*/
|
|
652
|
-
edgeLookupTimeoutMs?: number;
|
|
653
|
-
|
|
654
|
-
/**
|
|
655
|
-
* Latency budget (ms) for reading the BODY of a matched L1 entry
|
|
656
|
-
* (response.json()). CF streams the cache body lazily, so the multi-second
|
|
657
|
-
* tail can appear after `match` already resolved; this bounds it. On timeout
|
|
658
|
-
* the read is treated as a miss and falls through to L2/KV or render.
|
|
659
|
-
*
|
|
660
|
-
* Separate from {@link edgeLookupTimeoutMs} because a healthy body read
|
|
661
|
-
* (fetch + JSON parse of a potentially large Flight payload) takes a little
|
|
662
|
-
* longer than a `match`. Defaults to {@link EDGE_READ_TIMEOUT_MS} (20), which
|
|
663
|
-
* clears a healthy per-colo read yet fails fast on a degraded one. Set to 0
|
|
664
|
-
* (or any value <= 0) to disable and always await the body.
|
|
665
|
-
*/
|
|
666
|
-
edgeReadTimeoutMs?: number;
|
|
667
|
-
|
|
668
|
-
/**
|
|
669
|
-
* Latency budget (ms) for an L2 (KV) read. KV is the last cache tier before a
|
|
670
|
-
* full render and is a global store (~50ms healthy, seconds when degraded);
|
|
671
|
-
* this bounds it so a slow namespace cannot pin the request. On timeout the
|
|
672
|
-
* read is treated as a miss (no L1 promote) and falls through to render.
|
|
673
|
-
*
|
|
674
|
-
* Defaults to {@link KV_READ_TIMEOUT_MS} (170) -- a few multiples above the
|
|
675
|
-
* ~50ms healthy read, with headroom for legitimate tails (large payloads / far
|
|
676
|
-
* regions) yet still well under a degraded namespace's multi-second tail.
|
|
677
|
-
* Lower it for a tighter SLA, raise it if your healthy KV p99 runs higher; it
|
|
678
|
-
* is a degradation guard-rail, not a tuning lever. Set to 0 (or any value
|
|
679
|
-
* <= 0) to disable and always await KV.
|
|
680
|
-
*/
|
|
681
|
-
kvReadTimeoutMs?: number;
|
|
682
|
-
|
|
683
|
-
/**
|
|
684
|
-
* Emit a {@link CFCacheReadDebugEvent} per L1 read. `true` logs to console
|
|
685
|
-
* (visible via `wrangler tail`); pass a function to capture events directly.
|
|
686
|
-
* Off by default. Intended for validating cache behavior on a real
|
|
687
|
-
* deployment before relying on it; not for steady-state production.
|
|
688
|
-
*/
|
|
689
|
-
debug?: CFCacheDebug;
|
|
690
|
-
|
|
691
|
-
/**
|
|
692
|
-
* Custom key generator applied to all cache operations.
|
|
693
|
-
* Receives the full RequestContext (including env) and the default-generated key.
|
|
694
|
-
* Return value becomes the final cache key (unless route overrides with `key` option).
|
|
695
|
-
*
|
|
696
|
-
* Reserved prefixes: tag-invalidation markers live in the SAME KV namespace as
|
|
697
|
-
* data, keyed `__tag__/<tag>` (and `__tagmarker__/<tag>` for the L1 cache). A
|
|
698
|
-
* returned key must NOT begin with `__tag__/` or `__tagmarker__/`, or it can
|
|
699
|
-
* collide with a tag marker and corrupt invalidation. The documented
|
|
700
|
-
* prepend-style generators below are safe.
|
|
701
|
-
*
|
|
702
|
-
* @example Using headers for user segmentation
|
|
703
|
-
* ```typescript
|
|
704
|
-
* keyGenerator: (ctx, defaultKey) => {
|
|
705
|
-
* const segment = ctx.request.headers.get('x-user-segment') || 'default';
|
|
706
|
-
* return `${segment}:${defaultKey}`;
|
|
707
|
-
* }
|
|
708
|
-
* ```
|
|
709
|
-
*
|
|
710
|
-
* @example Using env bindings for multi-region
|
|
711
|
-
* ```typescript
|
|
712
|
-
* keyGenerator: (ctx, defaultKey) => {
|
|
713
|
-
* const region = ctx.env.REGION || 'us';
|
|
714
|
-
* return `${region}:${defaultKey}`;
|
|
715
|
-
* }
|
|
716
|
-
* ```
|
|
717
|
-
*
|
|
718
|
-
* @example Using cookies for locale-aware caching
|
|
719
|
-
* ```typescript
|
|
720
|
-
* keyGenerator: (ctx, defaultKey) => {
|
|
721
|
-
* const locale = cookies().get('locale')?.value || 'en';
|
|
722
|
-
* return `${locale}:${defaultKey}`;
|
|
723
|
-
* }
|
|
724
|
-
* ```
|
|
725
|
-
*/
|
|
726
|
-
keyGenerator?: (
|
|
727
|
-
ctx: RequestContext<TEnv>,
|
|
728
|
-
defaultKey: string,
|
|
729
|
-
) => string | Promise<string>;
|
|
730
|
-
}
|
|
731
|
-
|
|
732
215
|
// ============================================================================
|
|
733
216
|
// CFCacheStore Implementation
|
|
734
217
|
// ============================================================================
|
|
@@ -2611,11 +2094,12 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
2611
2094
|
await Promise.all(
|
|
2612
2095
|
tags.map(async (tag) => {
|
|
2613
2096
|
const markerKey = this.tagMarkerKey(tag);
|
|
2614
|
-
|
|
2097
|
+
const markerKeyBytes = kvKeyByteLength(markerKey);
|
|
2098
|
+
if (markerKeyBytes > KV_MAX_KEY_BYTES) {
|
|
2615
2099
|
failedTags.add(tag);
|
|
2616
2100
|
errors.push(
|
|
2617
2101
|
new Error(
|
|
2618
|
-
`tag "${tag}" produces a ${
|
|
2102
|
+
`tag "${tag}" produces a ${markerKeyBytes}-byte KV ` +
|
|
2619
2103
|
`marker key, over the ${KV_MAX_KEY_BYTES}-byte limit`,
|
|
2620
2104
|
),
|
|
2621
2105
|
);
|
|
@@ -2990,7 +2474,19 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
2990
2474
|
typeof e.st === "number" &&
|
|
2991
2475
|
typeof e.e === "number" &&
|
|
2992
2476
|
typeof e.s === "number" &&
|
|
2993
|
-
|
|
2477
|
+
// stx is optional but, if present, must be a string (feeds Response).
|
|
2478
|
+
(e.stx === undefined || typeof e.stx === "string") &&
|
|
2479
|
+
// hd must be an array of [name, value] string tuples; a malformed
|
|
2480
|
+
// shape would otherwise throw in `new Headers(hd)`. Validate it here
|
|
2481
|
+
// so a faulty envelope is a fail-open MISS, never a thrown read.
|
|
2482
|
+
Array.isArray(e.hd) &&
|
|
2483
|
+
e.hd.every(
|
|
2484
|
+
(entry) =>
|
|
2485
|
+
Array.isArray(entry) &&
|
|
2486
|
+
entry.length === 2 &&
|
|
2487
|
+
typeof entry[0] === "string" &&
|
|
2488
|
+
typeof entry[1] === "string",
|
|
2489
|
+
),
|
|
2994
2490
|
"kvGetResponse",
|
|
2995
2491
|
);
|
|
2996
2492
|
if (!envelope) return null;
|
|
@@ -3095,27 +2591,3 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
3095
2591
|
);
|
|
3096
2592
|
}
|
|
3097
2593
|
}
|
|
3098
|
-
|
|
3099
|
-
// ============================================================================
|
|
3100
|
-
// Base64 Helpers (binary-safe response body encoding for KV)
|
|
3101
|
-
// ============================================================================
|
|
3102
|
-
|
|
3103
|
-
/** Encode ArrayBuffer to base64 string. */
|
|
3104
|
-
function bufferToBase64(buffer: ArrayBuffer): string {
|
|
3105
|
-
const bytes = new Uint8Array(buffer);
|
|
3106
|
-
let binary = "";
|
|
3107
|
-
for (let i = 0; i < bytes.length; i++) {
|
|
3108
|
-
binary += String.fromCharCode(bytes[i]!);
|
|
3109
|
-
}
|
|
3110
|
-
return btoa(binary);
|
|
3111
|
-
}
|
|
3112
|
-
|
|
3113
|
-
/** Decode base64 string to ArrayBuffer. */
|
|
3114
|
-
function base64ToBuffer(base64: string): ArrayBuffer {
|
|
3115
|
-
const binary = atob(base64);
|
|
3116
|
-
const bytes = new Uint8Array(binary.length);
|
|
3117
|
-
for (let i = 0; i < binary.length; i++) {
|
|
3118
|
-
bytes[i] = binary.charCodeAt(i);
|
|
3119
|
-
}
|
|
3120
|
-
return bytes.buffer;
|
|
3121
|
-
}
|