@rangojs/router 0.0.0-experimental.77 → 0.0.0-experimental.77ed8945
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +120 -25
- package/dist/bin/rango.js +147 -57
- package/dist/vite/index.js +2103 -861
- package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/package.json +13 -8
- package/skills/api-client/SKILL.md +211 -0
- package/skills/breadcrumbs/SKILL.md +3 -1
- package/skills/bundle-analysis/SKILL.md +159 -0
- package/skills/cache-guide/SKILL.md +220 -30
- package/skills/caching/SKILL.md +116 -8
- package/skills/composability/SKILL.md +27 -2
- package/skills/css/SKILL.md +76 -0
- package/skills/document-cache/SKILL.md +78 -55
- package/skills/handler-use/SKILL.md +3 -1
- package/skills/hooks/SKILL.md +229 -20
- package/skills/host-router/SKILL.md +66 -20
- package/skills/i18n/SKILL.md +276 -0
- package/skills/intercept/SKILL.md +26 -4
- package/skills/layout/SKILL.md +6 -7
- package/skills/links/SKILL.md +247 -17
- package/skills/loader/SKILL.md +219 -9
- package/skills/middleware/SKILL.md +47 -12
- package/skills/migrate-nextjs/SKILL.md +562 -0
- package/skills/migrate-react-router/SKILL.md +769 -0
- package/skills/mime-routes/SKILL.md +27 -0
- package/skills/observability/SKILL.md +137 -0
- package/skills/parallel/SKILL.md +12 -6
- package/skills/prerender/SKILL.md +14 -33
- package/skills/rango/SKILL.md +238 -22
- package/skills/react-compiler/SKILL.md +168 -0
- package/skills/response-routes/SKILL.md +122 -47
- package/skills/route/SKILL.md +33 -4
- package/skills/router-setup/SKILL.md +3 -3
- package/skills/server-actions/SKILL.md +751 -0
- package/skills/streams-and-websockets/SKILL.md +283 -0
- package/skills/tailwind/SKILL.md +27 -3
- package/skills/typesafety/SKILL.md +319 -27
- package/skills/use-cache/SKILL.md +34 -5
- package/skills/view-transitions/SKILL.md +294 -0
- package/src/__augment-tests__/augment.ts +81 -0
- package/src/__augment-tests__/augmented.check.ts +116 -0
- package/src/browser/action-coordinator.ts +53 -36
- package/src/browser/app-shell.ts +39 -0
- package/src/browser/event-controller.ts +86 -70
- package/src/browser/history-state.ts +21 -0
- package/src/browser/index.ts +3 -3
- package/src/browser/navigation-bridge.ts +29 -9
- package/src/browser/navigation-client.ts +99 -77
- package/src/browser/navigation-store.ts +7 -8
- package/src/browser/navigation-transaction.ts +10 -28
- package/src/browser/partial-update.ts +60 -40
- package/src/browser/prefetch/cache.ts +196 -49
- package/src/browser/prefetch/fetch.ts +203 -59
- package/src/browser/prefetch/queue.ts +36 -5
- package/src/browser/rango-state.ts +37 -13
- package/src/browser/react/Link.tsx +18 -13
- package/src/browser/react/NavigationProvider.tsx +75 -31
- package/src/browser/react/filter-segment-order.ts +51 -7
- package/src/browser/react/index.ts +3 -0
- package/src/browser/react/location-state-shared.ts +175 -4
- package/src/browser/react/location-state.ts +39 -13
- package/src/browser/react/use-handle.ts +17 -9
- package/src/browser/react/use-navigation.ts +22 -2
- package/src/browser/react/use-params.ts +20 -8
- package/src/browser/react/use-reverse.ts +106 -0
- package/src/browser/react/use-router.ts +23 -2
- package/src/browser/react/use-segments.ts +11 -8
- package/src/browser/response-adapter.ts +52 -1
- package/src/browser/rsc-router.tsx +71 -22
- package/src/browser/scroll-restoration.ts +22 -14
- package/src/browser/segment-reconciler.ts +10 -14
- package/src/browser/segment-structure-assert.ts +2 -2
- package/src/browser/server-action-bridge.ts +44 -30
- package/src/browser/types.ts +12 -2
- package/src/build/collect-fallback-refs.ts +107 -0
- package/src/build/generate-manifest.ts +60 -35
- package/src/build/generate-route-types.ts +2 -0
- package/src/build/index.ts +8 -1
- package/src/build/prefix-tree-utils.ts +123 -0
- package/src/build/route-trie.ts +45 -1
- package/src/build/route-types/codegen.ts +4 -4
- package/src/build/route-types/include-resolution.ts +1 -1
- package/src/build/route-types/per-module-writer.ts +7 -4
- package/src/build/route-types/router-processing.ts +55 -14
- package/src/build/route-types/scan-filter.ts +1 -1
- package/src/build/route-types/source-scan.ts +118 -0
- package/src/build/runtime-discovery.ts +9 -20
- package/src/cache/cache-runtime.ts +17 -5
- package/src/cache/cache-scope.ts +51 -49
- package/src/cache/cf/cf-cache-store.ts +502 -32
- package/src/cache/cf/index.ts +3 -0
- package/src/cache/handle-snapshot.ts +103 -0
- package/src/cache/index.ts +3 -0
- package/src/cache/memory-segment-store.ts +3 -2
- package/src/cache/types.ts +10 -6
- package/src/client.rsc.tsx +3 -0
- package/src/client.tsx +96 -205
- package/src/context-var.ts +5 -5
- package/src/decode-loader-results.ts +36 -0
- package/src/errors.ts +30 -4
- package/src/handle.ts +4 -6
- package/src/host/index.ts +2 -2
- package/src/host/router.ts +129 -57
- package/src/host/types.ts +31 -2
- package/src/host/utils.ts +1 -1
- package/src/href-client.ts +140 -21
- package/src/index.rsc.ts +10 -6
- package/src/index.ts +17 -8
- package/src/loader-store.ts +500 -0
- package/src/loader.rsc.ts +2 -5
- package/src/loader.ts +3 -10
- package/src/missing-id-error.ts +68 -0
- package/src/outlet-context.ts +1 -1
- package/src/prerender/store.ts +9 -7
- package/src/prerender.ts +4 -4
- package/src/response-utils.ts +37 -0
- package/src/reverse.ts +65 -39
- package/src/route-content-wrapper.tsx +6 -28
- package/src/route-definition/dsl-helpers.ts +253 -265
- package/src/route-definition/helper-factories.ts +29 -139
- package/src/route-definition/helpers-types.ts +43 -15
- package/src/route-definition/resolve-handler-use.ts +6 -0
- package/src/route-definition/use-item-types.ts +32 -0
- package/src/route-types.ts +26 -41
- package/src/router/content-negotiation.ts +15 -2
- package/src/router/error-handling.ts +1 -1
- package/src/router/find-match.ts +54 -6
- package/src/router/handler-context.ts +21 -41
- package/src/router/intercept-resolution.ts +4 -18
- package/src/router/lazy-includes.ts +41 -22
- package/src/router/loader-resolution.ts +82 -36
- package/src/router/manifest.ts +41 -19
- package/src/router/match-api.ts +4 -3
- package/src/router/match-handlers.ts +1 -0
- package/src/router/match-middleware/cache-lookup.ts +57 -95
- package/src/router/match-middleware/cache-store.ts +3 -2
- package/src/router/match-result.ts +53 -32
- package/src/router/metrics.ts +1 -1
- package/src/router/middleware-types.ts +15 -26
- package/src/router/middleware.ts +99 -84
- package/src/router/pattern-matching.ts +116 -19
- package/src/router/prerender-match.ts +40 -15
- package/src/router/preview-match.ts +3 -1
- package/src/router/request-classification.ts +40 -37
- package/src/router/revalidation.ts +58 -2
- package/src/router/router-interfaces.ts +51 -35
- package/src/router/router-options.ts +25 -1
- package/src/router/router-registry.ts +2 -5
- package/src/router/segment-resolution/fresh.ts +27 -6
- package/src/router/segment-resolution/revalidation.ts +147 -106
- package/src/router/segment-resolution/static-store.ts +19 -5
- package/src/router/segment-resolution/view-transition-default.ts +36 -0
- package/src/router/substitute-pattern-params.ts +56 -0
- package/src/router/trie-matching.ts +40 -16
- package/src/router/types.ts +8 -0
- package/src/router/url-params.ts +49 -0
- package/src/router.ts +37 -25
- package/src/rsc/handler-context.ts +2 -2
- package/src/rsc/handler.ts +58 -77
- package/src/rsc/helpers.ts +72 -43
- package/src/rsc/index.ts +1 -1
- package/src/rsc/manifest-init.ts +28 -41
- package/src/rsc/origin-guard.ts +30 -10
- package/src/rsc/progressive-enhancement.ts +4 -0
- package/src/rsc/response-error.ts +79 -12
- package/src/rsc/response-route-handler.ts +76 -61
- package/src/rsc/rsc-rendering.ts +45 -51
- package/src/rsc/runtime-warnings.ts +9 -10
- package/src/rsc/server-action.ts +33 -39
- package/src/rsc/ssr-setup.ts +16 -0
- package/src/rsc/types.ts +8 -2
- package/src/search-params.ts +4 -4
- package/src/segment-content-promise.ts +67 -0
- package/src/segment-loader-promise.ts +122 -0
- package/src/segment-system.tsx +132 -116
- package/src/serialize.ts +243 -0
- package/src/server/context.ts +175 -53
- package/src/server/cookie-store.ts +28 -4
- package/src/server/request-context.ts +57 -51
- package/src/ssr/index.tsx +5 -1
- package/src/static-handler.ts +1 -1
- package/src/types/global-namespace.ts +39 -26
- package/src/types/handler-context.ts +68 -50
- package/src/types/index.ts +1 -0
- package/src/types/loader-types.ts +11 -9
- package/src/types/request-scope.ts +126 -0
- package/src/types/route-entry.ts +11 -0
- package/src/types/segments.ts +35 -2
- package/src/urls/include-helper.ts +34 -67
- package/src/urls/index.ts +1 -5
- package/src/urls/path-helper-types.ts +17 -3
- package/src/urls/path-helper.ts +17 -52
- package/src/urls/pattern-types.ts +36 -19
- package/src/urls/response-types.ts +22 -29
- package/src/urls/type-extraction.ts +58 -139
- package/src/urls/urls-function.ts +1 -5
- package/src/use-loader.tsx +413 -42
- package/src/vite/debug.ts +185 -0
- package/src/vite/discovery/bundle-postprocess.ts +6 -6
- package/src/vite/discovery/discover-routers.ts +106 -75
- package/src/vite/discovery/discovery-errors.ts +194 -0
- package/src/vite/discovery/gate-state.ts +171 -0
- package/src/vite/discovery/prerender-collection.ts +72 -31
- package/src/vite/discovery/route-types-writer.ts +40 -84
- package/src/vite/discovery/self-gen-tracking.ts +27 -1
- package/src/vite/discovery/state.ts +33 -0
- package/src/vite/discovery/virtual-module-codegen.ts +13 -23
- package/src/vite/index.ts +2 -0
- package/src/vite/plugin-types.ts +67 -0
- package/src/vite/plugins/cjs-to-esm.ts +8 -7
- package/src/vite/plugins/client-ref-dedup.ts +16 -0
- package/src/vite/plugins/client-ref-hashing.ts +28 -5
- package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
- package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/src/vite/plugins/cloudflare-protocol-stub.ts +214 -0
- package/src/vite/plugins/expose-action-id.ts +54 -30
- package/src/vite/plugins/expose-id-utils.ts +12 -8
- package/src/vite/plugins/expose-ids/export-analysis.ts +100 -20
- package/src/vite/plugins/expose-ids/handler-transform.ts +8 -61
- package/src/vite/plugins/expose-ids/loader-transform.ts +3 -5
- package/src/vite/plugins/expose-ids/router-transform.ts +20 -3
- package/src/vite/plugins/expose-internal-ids.ts +496 -486
- package/src/vite/plugins/performance-tracks.ts +29 -25
- package/src/vite/plugins/use-cache-transform.ts +65 -50
- package/src/vite/plugins/version-injector.ts +39 -23
- package/src/vite/plugins/version-plugin.ts +59 -2
- package/src/vite/plugins/virtual-entries.ts +2 -2
- package/src/vite/rango.ts +116 -29
- package/src/vite/router-discovery.ts +753 -104
- package/src/vite/utils/ast-handler-extract.ts +15 -15
- package/src/vite/utils/banner.ts +1 -1
- package/src/vite/utils/bundle-analysis.ts +4 -2
- package/src/vite/utils/client-chunks.ts +190 -0
- package/src/vite/utils/forward-user-plugins.ts +193 -0
- package/src/vite/utils/manifest-utils.ts +8 -59
- package/src/vite/utils/package-resolution.ts +41 -1
- package/src/vite/utils/prerender-utils.ts +5 -4
- package/src/vite/utils/shared-utils.ts +107 -26
- package/src/browser/action-response-classifier.ts +0 -99
|
@@ -56,6 +56,28 @@ export const CACHE_STALE_AT_HEADER = "x-edge-cache-stale-at";
|
|
|
56
56
|
/** Header storing cache status: HIT | REVALIDATING */
|
|
57
57
|
export const CACHE_STATUS_HEADER = "x-edge-cache-status";
|
|
58
58
|
|
|
59
|
+
/**
|
|
60
|
+
* Header storing the epoch-ms timestamp when an entry was marked REVALIDATING.
|
|
61
|
+
* The SWR thundering-herd guard reads this to decide whether the in-flight
|
|
62
|
+
* revalidation is still recent. It replaces a prior reliance on the HTTP `Age`
|
|
63
|
+
* header: CF's Cache API does not populate `Age` reliably per-colo (and our own
|
|
64
|
+
* unit MockCache never set it), so an absent `Age` defaulted to 0 and made every
|
|
65
|
+
* REVALIDATING entry look "just revalidated" forever -- a dropped/never-finished
|
|
66
|
+
* background revalidation could then pin an entry stale until hard expiry. An
|
|
67
|
+
* explicit timestamp we write ourselves (same pattern as CACHE_STALE_AT_HEADER)
|
|
68
|
+
* is reliable and lets the MAX_REVALIDATION_INTERVAL re-arm actually fire.
|
|
69
|
+
*/
|
|
70
|
+
export const CACHE_REVALIDATING_AT_HEADER = "x-edge-cache-revalidating-at";
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Header stashing the route author's original Cache-Control on L1 document
|
|
74
|
+
* entries. putResponse/promoteResponseToL1 overwrite Cache-Control with a long
|
|
75
|
+
* `max-age` so the CF Cache API retains the entry across the whole SWR window;
|
|
76
|
+
* getResponse restores this original value before serving so the client and any
|
|
77
|
+
* upstream CDN see the author's intended directive, not the internal edge TTL.
|
|
78
|
+
*/
|
|
79
|
+
const CACHE_ORIG_CC_HEADER = "x-edge-cache-orig-cc";
|
|
80
|
+
|
|
59
81
|
/**
|
|
60
82
|
* Maximum age in seconds for REVALIDATING status before allowing new revalidation.
|
|
61
83
|
* After this period, a stale entry in REVALIDATING status will trigger revalidation again.
|
|
@@ -63,17 +85,44 @@ export const CACHE_STATUS_HEADER = "x-edge-cache-status";
|
|
|
63
85
|
*/
|
|
64
86
|
export const MAX_REVALIDATION_INTERVAL = 30;
|
|
65
87
|
|
|
88
|
+
/**
|
|
89
|
+
* Maximum time (ms) to wait for an L1 edge cache (CF Cache API) read before
|
|
90
|
+
* giving up and treating it as a miss. The Cache API is normally sub-millisecond
|
|
91
|
+
* per-colo, so a slow `match` signals a degraded colo; we don't want it adding
|
|
92
|
+
* latency to the request. On timeout the lookup is abandoned, a warning is
|
|
93
|
+
* logged, and the read falls through to its normal miss path (L2/KV or render).
|
|
94
|
+
*
|
|
95
|
+
* This is the default; override per store via
|
|
96
|
+
* `CFCacheStoreOptions.edgeLookupTimeoutMs` (<= 0 disables the budget).
|
|
97
|
+
*/
|
|
98
|
+
export const EDGE_LOOKUP_TIMEOUT_MS = 10;
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Maximum time (ms) to wait for the BODY of a matched L1 entry to be read
|
|
102
|
+
* (response.json()) before treating the read as a miss.
|
|
103
|
+
*
|
|
104
|
+
* This is separate from {@link EDGE_LOOKUP_TIMEOUT_MS} on purpose. CF's Cache
|
|
105
|
+
* API resolves `match()` with a lazily-streamed body, so a fast `match` can be
|
|
106
|
+
* followed by a multi-second stall while the body bytes are fetched -- the
|
|
107
|
+
* latency tail lives here, after the match budget has already passed. The body
|
|
108
|
+
* read also includes JSON parsing of a potentially large Flight payload, so a
|
|
109
|
+
* healthy read legitimately takes longer than a `match`; a 10ms budget here
|
|
110
|
+
* would false-miss large entries. The default is generous enough to clear a
|
|
111
|
+
* healthy fetch+parse yet still bound the seconds-long degraded tail.
|
|
112
|
+
*
|
|
113
|
+
* Override per store via `CFCacheStoreOptions.edgeReadTimeoutMs` (<= 0 disables).
|
|
114
|
+
*/
|
|
115
|
+
export const EDGE_READ_TIMEOUT_MS = 100;
|
|
116
|
+
|
|
66
117
|
// ============================================================================
|
|
67
118
|
// Types
|
|
68
119
|
// ============================================================================
|
|
69
120
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
export
|
|
74
|
-
|
|
75
|
-
passThroughOnException(): void;
|
|
76
|
-
}
|
|
121
|
+
// Re-exported from the canonical home so cf-cache-store consumers keep
|
|
122
|
+
// importing `ExecutionContext` from this module without a second interface
|
|
123
|
+
// drifting over time.
|
|
124
|
+
export type { ExecutionContext } from "../../types/request-scope.js";
|
|
125
|
+
import type { ExecutionContext } from "../../types/request-scope.js";
|
|
77
126
|
|
|
78
127
|
/**
|
|
79
128
|
* Minimal Cloudflare KV Namespace interface.
|
|
@@ -109,8 +158,8 @@ interface KVSegmentEnvelope {
|
|
|
109
158
|
interface KVItemEnvelope {
|
|
110
159
|
/** RSC-serialized return value */
|
|
111
160
|
v: string;
|
|
112
|
-
/**
|
|
113
|
-
h?:
|
|
161
|
+
/** RSC-encoded handle data (see handle-snapshot.ts encodeHandles) */
|
|
162
|
+
h?: string;
|
|
114
163
|
/** When entry becomes stale (ms epoch) */
|
|
115
164
|
s: number;
|
|
116
165
|
/** When entry hard-expires (ms epoch) */
|
|
@@ -136,6 +185,63 @@ interface KVResponseEnvelope {
|
|
|
136
185
|
e: number;
|
|
137
186
|
}
|
|
138
187
|
|
|
188
|
+
/**
|
|
189
|
+
* One L1 read decision, surfaced when `debug` is enabled. Lets an operator
|
|
190
|
+
* confirm on a real deployment (e.g. via `wrangler tail`) that the store's
|
|
191
|
+
* observed inputs match its decision: which tier answered, the entry's status,
|
|
192
|
+
* the stale/revalidating timestamps, the raw CF `Age` header (so its
|
|
193
|
+
* unreliability can be seen next to the explicit revalidating-at stamp), and
|
|
194
|
+
* the measured match/body-read durations (where the latency tail shows up).
|
|
195
|
+
*/
|
|
196
|
+
export interface CFCacheReadDebugEvent {
|
|
197
|
+
/** Which read method produced this event. */
|
|
198
|
+
op: "get" | "getItem" | "getResponse";
|
|
199
|
+
/** Cache key (without the internal fn:/doc: prefix or version path). */
|
|
200
|
+
key: string;
|
|
201
|
+
/**
|
|
202
|
+
* What the read resolved to:
|
|
203
|
+
* - l1-fresh / l1-stale-revalidate / l1-revalidating-guarded: L1 hit outcomes
|
|
204
|
+
* - match-timeout / body-timeout: the latency budgets fired
|
|
205
|
+
* - non-200: L1 returned a non-200 (treated as a miss)
|
|
206
|
+
* - l1-miss: no L1 entry
|
|
207
|
+
* - kv-fresh / kv-stale / kv-miss: L2 fallback outcomes
|
|
208
|
+
* - error: the read threw
|
|
209
|
+
*/
|
|
210
|
+
outcome:
|
|
211
|
+
| "l1-fresh"
|
|
212
|
+
| "l1-stale-revalidate"
|
|
213
|
+
| "l1-revalidating-guarded"
|
|
214
|
+
| "match-timeout"
|
|
215
|
+
| "body-timeout"
|
|
216
|
+
| "non-200"
|
|
217
|
+
| "l1-miss"
|
|
218
|
+
| "kv-fresh"
|
|
219
|
+
| "kv-stale"
|
|
220
|
+
| "kv-miss"
|
|
221
|
+
| "error";
|
|
222
|
+
/** HTTP status of the matched L1 response, when one was returned. */
|
|
223
|
+
status?: number;
|
|
224
|
+
/** Epoch-ms when the entry goes stale (from CACHE_STALE_AT_HEADER). */
|
|
225
|
+
staleAt?: number;
|
|
226
|
+
/** Epoch-ms the entry was marked REVALIDATING (from the explicit stamp). */
|
|
227
|
+
revalidatingAt?: number;
|
|
228
|
+
/** Raw CF `Age` header, for comparison against revalidatingAt (may be null). */
|
|
229
|
+
ageHeader?: string | null;
|
|
230
|
+
isStale?: boolean;
|
|
231
|
+
isRevalidating?: boolean;
|
|
232
|
+
shouldRevalidate?: boolean;
|
|
233
|
+
/** Wall-clock ms spent in cache.match (bounded by edgeLookupTimeoutMs). */
|
|
234
|
+
matchMs?: number;
|
|
235
|
+
/** Wall-clock ms spent reading the body (bounded by edgeReadTimeoutMs). */
|
|
236
|
+
bodyReadMs?: number;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Debug sink. `true` logs each {@link CFCacheReadDebugEvent} to console; a
|
|
241
|
+
* function receives the events for programmatic capture.
|
|
242
|
+
*/
|
|
243
|
+
export type CFCacheDebug = boolean | ((event: CFCacheReadDebugEvent) => void);
|
|
244
|
+
|
|
139
245
|
export interface CFCacheStoreOptions<TEnv = unknown> {
|
|
140
246
|
/**
|
|
141
247
|
* Cache namespace. If not provided, uses caches.default (recommended).
|
|
@@ -184,11 +290,43 @@ export interface CFCacheStoreOptions<TEnv = unknown> {
|
|
|
184
290
|
* Cache version string override. When this changes, all cached entries are
|
|
185
291
|
* effectively invalidated (new keys won't match old entries).
|
|
186
292
|
*
|
|
187
|
-
* Defaults to the auto-generated VERSION from
|
|
293
|
+
* Defaults to the auto-generated VERSION from the `@rangojs/router:version` virtual module.
|
|
188
294
|
* Only set this if you need a custom versioning strategy.
|
|
189
295
|
*/
|
|
190
296
|
version?: string;
|
|
191
297
|
|
|
298
|
+
/**
|
|
299
|
+
* Latency budget (ms) for an L1 edge cache (CF Cache API) read. A `match`
|
|
300
|
+
* slower than this is abandoned and treated as a miss, so a degraded colo
|
|
301
|
+
* cannot stall the request; the read then falls through to its normal miss
|
|
302
|
+
* path (L2/KV or render).
|
|
303
|
+
*
|
|
304
|
+
* Defaults to {@link EDGE_LOOKUP_TIMEOUT_MS} (10). Set to 0 (or any value
|
|
305
|
+
* <= 0) to disable the budget and always await `match`.
|
|
306
|
+
*/
|
|
307
|
+
edgeLookupTimeoutMs?: number;
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Latency budget (ms) for reading the BODY of a matched L1 entry
|
|
311
|
+
* (response.json()). CF streams the cache body lazily, so the multi-second
|
|
312
|
+
* tail can appear after `match` already resolved; this bounds it. On timeout
|
|
313
|
+
* the read is treated as a miss and falls through to L2/KV or render.
|
|
314
|
+
*
|
|
315
|
+
* Separate from {@link edgeLookupTimeoutMs} because a healthy body read
|
|
316
|
+
* (fetch + JSON parse of a potentially large Flight payload) legitimately
|
|
317
|
+
* takes longer than a `match`. Defaults to {@link EDGE_READ_TIMEOUT_MS} (100).
|
|
318
|
+
* Set to 0 (or any value <= 0) to disable and always await the body.
|
|
319
|
+
*/
|
|
320
|
+
edgeReadTimeoutMs?: number;
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Emit a {@link CFCacheReadDebugEvent} per L1 read. `true` logs to console
|
|
324
|
+
* (visible via `wrangler tail`); pass a function to capture events directly.
|
|
325
|
+
* Off by default. Intended for validating cache behavior on a real
|
|
326
|
+
* deployment before relying on it; not for steady-state production.
|
|
327
|
+
*/
|
|
328
|
+
debug?: CFCacheDebug;
|
|
329
|
+
|
|
192
330
|
/**
|
|
193
331
|
* Custom key generator applied to all cache operations.
|
|
194
332
|
* Receives the full RequestContext (including env) and the default-generated key.
|
|
@@ -242,9 +380,12 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
242
380
|
) => string | Promise<string>;
|
|
243
381
|
|
|
244
382
|
private readonly namespace?: string;
|
|
245
|
-
private readonly
|
|
383
|
+
private readonly explicitBaseUrl?: string;
|
|
246
384
|
private readonly waitUntil?: (fn: () => Promise<void>) => void;
|
|
247
385
|
private readonly version?: string;
|
|
386
|
+
private readonly edgeLookupTimeoutMs: number;
|
|
387
|
+
private readonly edgeReadTimeoutMs: number;
|
|
388
|
+
private readonly debug?: (event: CFCacheReadDebugEvent) => void;
|
|
248
389
|
private readonly kv?: KVNamespace;
|
|
249
390
|
|
|
250
391
|
constructor(options: CFCacheStoreOptions<TEnv>) {
|
|
@@ -257,21 +398,63 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
257
398
|
}
|
|
258
399
|
|
|
259
400
|
this.namespace = options.namespace;
|
|
260
|
-
|
|
401
|
+
// Base URL is resolved lazily per cache operation (see resolveBaseUrl).
|
|
402
|
+
// The store is constructed before the per-request context ALS is entered
|
|
403
|
+
// (the cache factory runs ahead of runWithRequestContext in the handler),
|
|
404
|
+
// so deriving the host here would always miss the request and fall back to
|
|
405
|
+
// the internal host. Only the explicit override can be captured eagerly.
|
|
406
|
+
this.explicitBaseUrl = options.baseUrl;
|
|
261
407
|
this.defaults = options.defaults;
|
|
262
408
|
this.version = options.version ?? VERSION;
|
|
409
|
+
this.edgeLookupTimeoutMs =
|
|
410
|
+
options.edgeLookupTimeoutMs ?? EDGE_LOOKUP_TIMEOUT_MS;
|
|
411
|
+
this.edgeReadTimeoutMs = options.edgeReadTimeoutMs ?? EDGE_READ_TIMEOUT_MS;
|
|
412
|
+
this.debug =
|
|
413
|
+
options.debug === true
|
|
414
|
+
? (event) =>
|
|
415
|
+
console.log(`[CFCacheStore:debug] ${JSON.stringify(event)}`)
|
|
416
|
+
: typeof options.debug === "function"
|
|
417
|
+
? options.debug
|
|
418
|
+
: undefined;
|
|
263
419
|
this.keyGenerator = options.keyGenerator;
|
|
264
420
|
this.waitUntil = (fn) => options.ctx.waitUntil(fn());
|
|
265
421
|
this.kv = options.kv;
|
|
266
422
|
}
|
|
267
423
|
|
|
424
|
+
/**
|
|
425
|
+
* Emit a debug event if `debug` is enabled. Swallows sink errors so a faulty
|
|
426
|
+
* debug callback can never break a cache read.
|
|
427
|
+
* @internal
|
|
428
|
+
*/
|
|
429
|
+
private emitDebug(event: CFCacheReadDebugEvent): void {
|
|
430
|
+
if (!this.debug) return;
|
|
431
|
+
try {
|
|
432
|
+
this.debug(event);
|
|
433
|
+
} catch {
|
|
434
|
+
// A broken debug sink must not affect the request.
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Resolve the cache-key base URL for the current cache operation.
|
|
440
|
+
* Prefers an explicit `baseUrl` option; otherwise derives it from the live
|
|
441
|
+
* request. Called per operation (from keyToRequest), which runs inside the
|
|
442
|
+
* request-context ALS, so deriveBaseUrl sees the request and can use the
|
|
443
|
+
* production host instead of the internal fallback.
|
|
444
|
+
* @internal
|
|
445
|
+
*/
|
|
446
|
+
private resolveBaseUrl(): string {
|
|
447
|
+
return this.explicitBaseUrl ?? this.deriveBaseUrl();
|
|
448
|
+
}
|
|
449
|
+
|
|
268
450
|
/**
|
|
269
451
|
* Derive base URL from request hostname via requestContext.
|
|
270
452
|
* Uses internal fallback for dev/preview environments and untrusted hostnames.
|
|
453
|
+
* Must run inside the request context (invoked lazily via resolveBaseUrl).
|
|
271
454
|
* @internal
|
|
272
455
|
*/
|
|
273
456
|
private deriveBaseUrl(): string {
|
|
274
|
-
const fallback = "https://rsc-
|
|
457
|
+
const fallback = "https://rsc-dummy-host-1.com/";
|
|
275
458
|
|
|
276
459
|
const ctx = _getRequestContext();
|
|
277
460
|
if (!ctx?.request) {
|
|
@@ -316,6 +499,92 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
316
499
|
return caches.default;
|
|
317
500
|
}
|
|
318
501
|
|
|
502
|
+
/**
|
|
503
|
+
* Read from the L1 edge cache with a latency budget. A `match` that takes
|
|
504
|
+
* longer than the configured budget (edgeLookupTimeoutMs, default
|
|
505
|
+
* EDGE_LOOKUP_TIMEOUT_MS) is abandoned and reported as a miss (undefined) so a
|
|
506
|
+
* degraded colo cannot stall the request; callers then fall through to their
|
|
507
|
+
* normal miss path (L2/KV or render). The slow `match` is left to settle in
|
|
508
|
+
* the background (errors swallowed) rather than aborted, since the Cache API
|
|
509
|
+
* exposes no cancellation. A budget <= 0 disables the timeout entirely and
|
|
510
|
+
* awaits `match` directly.
|
|
511
|
+
* @internal
|
|
512
|
+
*/
|
|
513
|
+
private async matchWithTimeout(
|
|
514
|
+
cache: Cache,
|
|
515
|
+
request: Request,
|
|
516
|
+
): Promise<Response | undefined> {
|
|
517
|
+
const budget = this.edgeLookupTimeoutMs;
|
|
518
|
+
if (budget <= 0) {
|
|
519
|
+
return cache.match(request);
|
|
520
|
+
}
|
|
521
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
522
|
+
const timeout = new Promise<{ timedOut: true }>((resolve) => {
|
|
523
|
+
timer = setTimeout(() => resolve({ timedOut: true }), budget);
|
|
524
|
+
});
|
|
525
|
+
try {
|
|
526
|
+
const matchPromise = cache.match(request);
|
|
527
|
+
// The losing branch keeps running; ensure a late rejection can't surface
|
|
528
|
+
// as an unhandled rejection once we've stopped awaiting it.
|
|
529
|
+
matchPromise.catch(() => {});
|
|
530
|
+
const result = await Promise.race([
|
|
531
|
+
matchPromise.then((response) => ({
|
|
532
|
+
timedOut: false as const,
|
|
533
|
+
response,
|
|
534
|
+
})),
|
|
535
|
+
timeout,
|
|
536
|
+
]);
|
|
537
|
+
if (result.timedOut) {
|
|
538
|
+
console.warn(
|
|
539
|
+
`[CFCacheStore] edge cache lookup exceeded ${budget}ms; treating as miss`,
|
|
540
|
+
);
|
|
541
|
+
return undefined;
|
|
542
|
+
}
|
|
543
|
+
return result.response;
|
|
544
|
+
} finally {
|
|
545
|
+
if (timer) clearTimeout(timer);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
/**
|
|
550
|
+
* Read and JSON-parse a matched L1 Response's body under the edgeReadTimeoutMs
|
|
551
|
+
* budget. CF resolves `match()` with a lazily-streamed body, so the latency
|
|
552
|
+
* tail surfaces here -- after matchWithTimeout has already passed -- not in the
|
|
553
|
+
* match itself. On timeout the read is abandoned (the dangling body read is
|
|
554
|
+
* left to settle, errors swallowed, since the stream exposes no cancellation)
|
|
555
|
+
* and `undefined` is returned so the caller falls through to L2/KV or render.
|
|
556
|
+
* A budget <= 0 disables the bound and awaits the body directly.
|
|
557
|
+
* @internal
|
|
558
|
+
*/
|
|
559
|
+
private async readJsonWithTimeout<T>(
|
|
560
|
+
response: Response,
|
|
561
|
+
): Promise<T | undefined> {
|
|
562
|
+
const budget = this.edgeReadTimeoutMs;
|
|
563
|
+
if (budget <= 0) return (await response.json()) as T;
|
|
564
|
+
|
|
565
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
566
|
+
const timeout = new Promise<{ timedOut: true }>((resolve) => {
|
|
567
|
+
timer = setTimeout(() => resolve({ timedOut: true }), budget);
|
|
568
|
+
});
|
|
569
|
+
try {
|
|
570
|
+
const readPromise = response.json() as Promise<T>;
|
|
571
|
+
readPromise.catch(() => {});
|
|
572
|
+
const result = await Promise.race([
|
|
573
|
+
readPromise.then((value) => ({ timedOut: false as const, value })),
|
|
574
|
+
timeout,
|
|
575
|
+
]);
|
|
576
|
+
if (result.timedOut) {
|
|
577
|
+
console.warn(
|
|
578
|
+
`[CFCacheStore] edge cache body read exceeded ${budget}ms; treating as miss`,
|
|
579
|
+
);
|
|
580
|
+
return undefined;
|
|
581
|
+
}
|
|
582
|
+
return result.value;
|
|
583
|
+
} finally {
|
|
584
|
+
if (timer) clearTimeout(timer);
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
319
588
|
// ============================================================================
|
|
320
589
|
// Segment Cache Methods
|
|
321
590
|
// ============================================================================
|
|
@@ -335,26 +604,84 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
335
604
|
try {
|
|
336
605
|
const cache = await this.getCache();
|
|
337
606
|
const request = this.keyToRequest(key);
|
|
338
|
-
const
|
|
607
|
+
const matchStart = Date.now();
|
|
608
|
+
const response = await this.matchWithTimeout(cache, request);
|
|
609
|
+
const matchMs = Date.now() - matchStart;
|
|
339
610
|
|
|
340
611
|
if (!response) {
|
|
612
|
+
// Genuine L1 miss, or matchWithTimeout abandoned a slow match (it warns).
|
|
613
|
+
this.emitDebug({ op: "get", key, outcome: "l1-miss", matchMs });
|
|
614
|
+
return this.kvGetSegment(key);
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
// A non-200 entry (a cached error response, or a foreign response that
|
|
618
|
+
// landed on this key) is not valid segment data; treat it as a miss
|
|
619
|
+
// rather than JSON-parsing garbage and serving it as a hit.
|
|
620
|
+
if (response.status !== 200) {
|
|
621
|
+
this.emitDebug({
|
|
622
|
+
op: "get",
|
|
623
|
+
key,
|
|
624
|
+
outcome: "non-200",
|
|
625
|
+
status: response.status,
|
|
626
|
+
matchMs,
|
|
627
|
+
});
|
|
341
628
|
return this.kvGetSegment(key);
|
|
342
629
|
}
|
|
343
630
|
|
|
344
631
|
// Read status headers
|
|
345
632
|
const status = response.headers.get(CACHE_STATUS_HEADER);
|
|
346
|
-
const
|
|
633
|
+
const ageHeader = response.headers.get("age");
|
|
347
634
|
const staleAt = Number(
|
|
348
635
|
response.headers.get(CACHE_STALE_AT_HEADER) ?? "0",
|
|
349
636
|
);
|
|
637
|
+
const revalidatingAt = Number(
|
|
638
|
+
response.headers.get(CACHE_REVALIDATING_AT_HEADER) ?? "0",
|
|
639
|
+
);
|
|
350
640
|
|
|
351
641
|
const isStale = staleAt > 0 && Date.now() > staleAt;
|
|
642
|
+
// Recency comes from our explicit revalidating-at stamp, not CF's `Age`
|
|
643
|
+
// header (see CACHE_REVALIDATING_AT_HEADER). An absent/zero stamp counts
|
|
644
|
+
// as "not recent" so a dropped revalidation re-arms instead of pinning.
|
|
352
645
|
const isRevalidating =
|
|
353
|
-
status === "REVALIDATING" &&
|
|
646
|
+
status === "REVALIDATING" &&
|
|
647
|
+
revalidatingAt > 0 &&
|
|
648
|
+
Date.now() - revalidatingAt < MAX_REVALIDATION_INTERVAL * 1000;
|
|
354
649
|
|
|
355
650
|
// Case 1: Fresh or already being revalidated - just return data
|
|
356
651
|
if (!isStale || isRevalidating) {
|
|
357
|
-
const
|
|
652
|
+
const bodyStart = Date.now();
|
|
653
|
+
const data = await this.readJsonWithTimeout<CachedEntryData>(response);
|
|
654
|
+
const bodyReadMs = Date.now() - bodyStart;
|
|
655
|
+
if (data === undefined) {
|
|
656
|
+
this.emitDebug({
|
|
657
|
+
op: "get",
|
|
658
|
+
key,
|
|
659
|
+
outcome: "body-timeout",
|
|
660
|
+
status: response.status,
|
|
661
|
+
staleAt,
|
|
662
|
+
revalidatingAt,
|
|
663
|
+
ageHeader,
|
|
664
|
+
isStale,
|
|
665
|
+
isRevalidating,
|
|
666
|
+
matchMs,
|
|
667
|
+
bodyReadMs,
|
|
668
|
+
});
|
|
669
|
+
return this.kvGetSegment(key);
|
|
670
|
+
}
|
|
671
|
+
this.emitDebug({
|
|
672
|
+
op: "get",
|
|
673
|
+
key,
|
|
674
|
+
outcome: isRevalidating ? "l1-revalidating-guarded" : "l1-fresh",
|
|
675
|
+
status: response.status,
|
|
676
|
+
staleAt,
|
|
677
|
+
revalidatingAt,
|
|
678
|
+
ageHeader,
|
|
679
|
+
isStale,
|
|
680
|
+
isRevalidating,
|
|
681
|
+
shouldRevalidate: false,
|
|
682
|
+
matchMs,
|
|
683
|
+
bodyReadMs,
|
|
684
|
+
});
|
|
358
685
|
return { data, shouldRevalidate: false };
|
|
359
686
|
}
|
|
360
687
|
|
|
@@ -363,6 +690,8 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
363
690
|
|
|
364
691
|
const headers = new Headers(response.headers);
|
|
365
692
|
headers.set(CACHE_STATUS_HEADER, "REVALIDATING");
|
|
693
|
+
// Stamp when we marked it so the herd guard / re-arm reads a reliable time.
|
|
694
|
+
headers.set(CACHE_REVALIDATING_AT_HEADER, String(Date.now()));
|
|
366
695
|
|
|
367
696
|
// Blocking write - must complete before returning to prevent race
|
|
368
697
|
await cache.put(
|
|
@@ -370,10 +699,45 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
370
699
|
new Response(b1, { status: response.status, headers }),
|
|
371
700
|
);
|
|
372
701
|
|
|
373
|
-
const
|
|
702
|
+
const bodyStart = Date.now();
|
|
703
|
+
const data = await this.readJsonWithTimeout<CachedEntryData>(
|
|
704
|
+
new Response(b2),
|
|
705
|
+
);
|
|
706
|
+
const bodyReadMs = Date.now() - bodyStart;
|
|
707
|
+
if (data === undefined) {
|
|
708
|
+
this.emitDebug({
|
|
709
|
+
op: "get",
|
|
710
|
+
key,
|
|
711
|
+
outcome: "body-timeout",
|
|
712
|
+
status: response.status,
|
|
713
|
+
staleAt,
|
|
714
|
+
revalidatingAt,
|
|
715
|
+
ageHeader,
|
|
716
|
+
isStale,
|
|
717
|
+
isRevalidating,
|
|
718
|
+
matchMs,
|
|
719
|
+
bodyReadMs,
|
|
720
|
+
});
|
|
721
|
+
return this.kvGetSegment(key);
|
|
722
|
+
}
|
|
723
|
+
this.emitDebug({
|
|
724
|
+
op: "get",
|
|
725
|
+
key,
|
|
726
|
+
outcome: "l1-stale-revalidate",
|
|
727
|
+
status: response.status,
|
|
728
|
+
staleAt,
|
|
729
|
+
revalidatingAt,
|
|
730
|
+
ageHeader,
|
|
731
|
+
isStale,
|
|
732
|
+
isRevalidating,
|
|
733
|
+
shouldRevalidate: true,
|
|
734
|
+
matchMs,
|
|
735
|
+
bodyReadMs,
|
|
736
|
+
});
|
|
374
737
|
return { data, shouldRevalidate: true };
|
|
375
738
|
} catch (error) {
|
|
376
739
|
console.error("[CFCacheStore] get failed:", error);
|
|
740
|
+
this.emitDebug({ op: "get", key, outcome: "error" });
|
|
377
741
|
return null;
|
|
378
742
|
}
|
|
379
743
|
}
|
|
@@ -421,7 +785,7 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
421
785
|
}
|
|
422
786
|
|
|
423
787
|
// L2: persist to KV
|
|
424
|
-
this.kvSetSegment(key, data, staleAt, totalTtl);
|
|
788
|
+
this.kvSetSegment(key, data, staleAt, totalTtl, swrWindow);
|
|
425
789
|
} catch (error) {
|
|
426
790
|
console.error("[CFCacheStore] set failed:", error);
|
|
427
791
|
}
|
|
@@ -469,7 +833,7 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
469
833
|
try {
|
|
470
834
|
const cache = await this.getCache();
|
|
471
835
|
const request = this.keyToRequest(`doc:${key}`);
|
|
472
|
-
const response = await
|
|
836
|
+
const response = await this.matchWithTimeout(cache, request);
|
|
473
837
|
|
|
474
838
|
if (!response || response.status !== 200) {
|
|
475
839
|
return this.kvGetResponse(key);
|
|
@@ -480,7 +844,7 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
480
844
|
const isStale = staleAt > 0 && Date.now() > staleAt;
|
|
481
845
|
|
|
482
846
|
return {
|
|
483
|
-
response,
|
|
847
|
+
response: this.toClientResponse(response),
|
|
484
848
|
shouldRevalidate: isStale,
|
|
485
849
|
};
|
|
486
850
|
} catch (error) {
|
|
@@ -489,6 +853,30 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
489
853
|
}
|
|
490
854
|
}
|
|
491
855
|
|
|
856
|
+
/**
|
|
857
|
+
* Strip internal edge headers and restore the author's Cache-Control before a
|
|
858
|
+
* cached document Response is served to a client. L1 entries carry the
|
|
859
|
+
* internal staleness/status headers and a rewritten Cache-Control; none of
|
|
860
|
+
* those should reach the browser or an upstream CDN.
|
|
861
|
+
*/
|
|
862
|
+
private toClientResponse(response: Response): Response {
|
|
863
|
+
const headers = new Headers(response.headers);
|
|
864
|
+
const originalCacheControl = headers.get(CACHE_ORIG_CC_HEADER);
|
|
865
|
+
if (originalCacheControl !== null) {
|
|
866
|
+
headers.set("Cache-Control", originalCacheControl);
|
|
867
|
+
} else {
|
|
868
|
+
headers.delete("Cache-Control");
|
|
869
|
+
}
|
|
870
|
+
headers.delete(CACHE_ORIG_CC_HEADER);
|
|
871
|
+
headers.delete(CACHE_STALE_AT_HEADER);
|
|
872
|
+
headers.delete(CACHE_STATUS_HEADER);
|
|
873
|
+
return new Response(response.body, {
|
|
874
|
+
status: response.status,
|
|
875
|
+
statusText: response.statusText,
|
|
876
|
+
headers,
|
|
877
|
+
});
|
|
878
|
+
}
|
|
879
|
+
|
|
492
880
|
/**
|
|
493
881
|
* Store a Response with TTL and optional SWR window (for document-level caching).
|
|
494
882
|
* When KV is configured, also persists to L2.
|
|
@@ -515,8 +903,14 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
515
903
|
: [null, null]
|
|
516
904
|
: [response.body, null];
|
|
517
905
|
|
|
518
|
-
// Clone and add cache headers
|
|
906
|
+
// Clone and add cache headers. The author's Cache-Control is stashed and
|
|
907
|
+
// replaced with a long max-age so the CF Cache API holds the entry across
|
|
908
|
+
// the SWR window; getResponse restores the original before serving.
|
|
519
909
|
const headers = new Headers(response.headers);
|
|
910
|
+
const originalCacheControl = response.headers.get("Cache-Control");
|
|
911
|
+
if (originalCacheControl !== null) {
|
|
912
|
+
headers.set(CACHE_ORIG_CC_HEADER, originalCacheControl);
|
|
913
|
+
}
|
|
520
914
|
headers.set("Cache-Control", `public, max-age=${totalTtl}`);
|
|
521
915
|
headers.set(CACHE_STALE_AT_HEADER, String(staleAt));
|
|
522
916
|
|
|
@@ -585,26 +979,81 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
585
979
|
try {
|
|
586
980
|
const cache = await this.getCache();
|
|
587
981
|
const request = this.keyToRequest(`fn:${key}`);
|
|
588
|
-
const
|
|
982
|
+
const matchStart = Date.now();
|
|
983
|
+
const response = await this.matchWithTimeout(cache, request);
|
|
984
|
+
const matchMs = Date.now() - matchStart;
|
|
985
|
+
|
|
986
|
+
if (!response) {
|
|
987
|
+
this.emitDebug({ op: "getItem", key, outcome: "l1-miss", matchMs });
|
|
988
|
+
return this.kvGetItem(key);
|
|
989
|
+
}
|
|
589
990
|
|
|
590
|
-
|
|
991
|
+
// Non-200 entry is not a valid cached function result; treat as a miss.
|
|
992
|
+
if (response.status !== 200) {
|
|
993
|
+
this.emitDebug({
|
|
994
|
+
op: "getItem",
|
|
995
|
+
key,
|
|
996
|
+
outcome: "non-200",
|
|
997
|
+
status: response.status,
|
|
998
|
+
matchMs,
|
|
999
|
+
});
|
|
1000
|
+
return this.kvGetItem(key);
|
|
1001
|
+
}
|
|
591
1002
|
|
|
592
1003
|
const staleAt = Number(
|
|
593
1004
|
response.headers.get(CACHE_STALE_AT_HEADER) ?? "0",
|
|
594
1005
|
);
|
|
595
1006
|
const status = response.headers.get(CACHE_STATUS_HEADER);
|
|
596
|
-
const
|
|
1007
|
+
const ageHeader = response.headers.get("age");
|
|
1008
|
+
const revalidatingAt = Number(
|
|
1009
|
+
response.headers.get(CACHE_REVALIDATING_AT_HEADER) ?? "0",
|
|
1010
|
+
);
|
|
597
1011
|
|
|
598
1012
|
const isStale = staleAt > 0 && Date.now() > staleAt;
|
|
1013
|
+
// Recency from our explicit stamp, not CF's `Age` header (see get()).
|
|
599
1014
|
const isRevalidating =
|
|
600
|
-
status === "REVALIDATING" &&
|
|
1015
|
+
status === "REVALIDATING" &&
|
|
1016
|
+
revalidatingAt > 0 &&
|
|
1017
|
+
Date.now() - revalidatingAt < MAX_REVALIDATION_INTERVAL * 1000;
|
|
601
1018
|
|
|
602
|
-
const
|
|
1019
|
+
const bodyStart = Date.now();
|
|
1020
|
+
const data = await this.readJsonWithTimeout<{
|
|
603
1021
|
value: string;
|
|
604
|
-
handles?:
|
|
605
|
-
};
|
|
1022
|
+
handles?: string;
|
|
1023
|
+
}>(response);
|
|
1024
|
+
const bodyReadMs = Date.now() - bodyStart;
|
|
1025
|
+
if (data === undefined) {
|
|
1026
|
+
this.emitDebug({
|
|
1027
|
+
op: "getItem",
|
|
1028
|
+
key,
|
|
1029
|
+
outcome: "body-timeout",
|
|
1030
|
+
status: response.status,
|
|
1031
|
+
staleAt,
|
|
1032
|
+
revalidatingAt,
|
|
1033
|
+
ageHeader,
|
|
1034
|
+
isStale,
|
|
1035
|
+
isRevalidating,
|
|
1036
|
+
matchMs,
|
|
1037
|
+
bodyReadMs,
|
|
1038
|
+
});
|
|
1039
|
+
return this.kvGetItem(key);
|
|
1040
|
+
}
|
|
606
1041
|
|
|
607
1042
|
if (!isStale || isRevalidating) {
|
|
1043
|
+
this.emitDebug({
|
|
1044
|
+
op: "getItem",
|
|
1045
|
+
key,
|
|
1046
|
+
outcome: isRevalidating ? "l1-revalidating-guarded" : "l1-fresh",
|
|
1047
|
+
status: response.status,
|
|
1048
|
+
staleAt,
|
|
1049
|
+
revalidatingAt,
|
|
1050
|
+
ageHeader,
|
|
1051
|
+
isStale,
|
|
1052
|
+
isRevalidating,
|
|
1053
|
+
shouldRevalidate: false,
|
|
1054
|
+
matchMs,
|
|
1055
|
+
bodyReadMs,
|
|
1056
|
+
});
|
|
608
1057
|
return {
|
|
609
1058
|
value: data.value,
|
|
610
1059
|
handles: data.handles,
|
|
@@ -615,11 +1064,27 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
615
1064
|
// Stale and needs revalidation — mark REVALIDATING atomically
|
|
616
1065
|
const headers = new Headers(response.headers);
|
|
617
1066
|
headers.set(CACHE_STATUS_HEADER, "REVALIDATING");
|
|
1067
|
+
// Stamp when we marked it so the herd guard / re-arm reads a reliable time.
|
|
1068
|
+
headers.set(CACHE_REVALIDATING_AT_HEADER, String(Date.now()));
|
|
618
1069
|
await cache.put(
|
|
619
1070
|
request,
|
|
620
1071
|
new Response(JSON.stringify(data), { status: 200, headers }),
|
|
621
1072
|
);
|
|
622
1073
|
|
|
1074
|
+
this.emitDebug({
|
|
1075
|
+
op: "getItem",
|
|
1076
|
+
key,
|
|
1077
|
+
outcome: "l1-stale-revalidate",
|
|
1078
|
+
status: response.status,
|
|
1079
|
+
staleAt,
|
|
1080
|
+
revalidatingAt,
|
|
1081
|
+
ageHeader,
|
|
1082
|
+
isStale,
|
|
1083
|
+
isRevalidating,
|
|
1084
|
+
shouldRevalidate: true,
|
|
1085
|
+
matchMs,
|
|
1086
|
+
bodyReadMs,
|
|
1087
|
+
});
|
|
623
1088
|
return {
|
|
624
1089
|
value: data.value,
|
|
625
1090
|
handles: data.handles,
|
|
@@ -627,6 +1092,7 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
627
1092
|
};
|
|
628
1093
|
} catch (error) {
|
|
629
1094
|
console.error("[CFCacheStore] getItem failed:", error);
|
|
1095
|
+
this.emitDebug({ op: "getItem", key, outcome: "error" });
|
|
630
1096
|
return null;
|
|
631
1097
|
}
|
|
632
1098
|
}
|
|
@@ -706,7 +1172,7 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
706
1172
|
const encodedKey = encodeURIComponent(key);
|
|
707
1173
|
// Include version in URL path to invalidate cache when version changes
|
|
708
1174
|
const versionPath = this.version ? `v/${this.version}/` : "";
|
|
709
|
-
return new Request(`${this.
|
|
1175
|
+
return new Request(`${this.resolveBaseUrl()}${versionPath}${encodedKey}`, {
|
|
710
1176
|
method: "GET",
|
|
711
1177
|
});
|
|
712
1178
|
}
|
|
@@ -766,13 +1232,13 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
766
1232
|
data: CachedEntryData,
|
|
767
1233
|
staleAt: number,
|
|
768
1234
|
totalTtl: number,
|
|
1235
|
+
swrWindow: number,
|
|
769
1236
|
): void {
|
|
770
1237
|
// KV requires expirationTtl >= 60s. Skip write for short-lived entries.
|
|
771
1238
|
if (!this.kv || !this.waitUntil || totalTtl < 60) return;
|
|
772
1239
|
|
|
773
1240
|
const kvKey = this.toKVKey(key);
|
|
774
|
-
const
|
|
775
|
-
const expiresAt = staleAt + swrWindow;
|
|
1241
|
+
const expiresAt = staleAt + swrWindow * 1000;
|
|
776
1242
|
|
|
777
1243
|
this.waitUntil(async () => {
|
|
778
1244
|
try {
|
|
@@ -939,6 +1405,10 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
939
1405
|
const request = this.keyToRequest(`doc:${key}`);
|
|
940
1406
|
|
|
941
1407
|
const headers = new Headers(envelope.hd);
|
|
1408
|
+
const originalCacheControl = headers.get("Cache-Control");
|
|
1409
|
+
if (originalCacheControl !== null) {
|
|
1410
|
+
headers.set(CACHE_ORIG_CC_HEADER, originalCacheControl);
|
|
1411
|
+
}
|
|
942
1412
|
headers.set("Cache-Control", `public, max-age=${remainingTtl}`);
|
|
943
1413
|
headers.set(CACHE_STALE_AT_HEADER, String(envelope.s));
|
|
944
1414
|
|