@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.
Files changed (141) hide show
  1. package/AGENTS.md +8 -0
  2. package/README.md +43 -2
  3. package/dist/bin/rango.js +92 -16
  4. package/dist/vite/index.js +166 -70
  5. package/package.json +19 -18
  6. package/skills/breadcrumbs/SKILL.md +1 -1
  7. package/skills/bundle-analysis/SKILL.md +2 -2
  8. package/skills/cache-guide/SKILL.md +2 -2
  9. package/skills/caching/SKILL.md +16 -9
  10. package/skills/debug-manifest/SKILL.md +4 -2
  11. package/skills/document-cache/SKILL.md +2 -2
  12. package/skills/handler-use/SKILL.md +1 -1
  13. package/skills/hooks/SKILL.md +2 -2
  14. package/skills/host-router/SKILL.md +1 -1
  15. package/skills/intercept/SKILL.md +1 -1
  16. package/skills/loader/SKILL.md +2 -0
  17. package/skills/migrate-react-router/SKILL.md +4 -2
  18. package/skills/mime-routes/SKILL.md +1 -1
  19. package/skills/prerender/SKILL.md +2 -0
  20. package/skills/rango/SKILL.md +12 -11
  21. package/skills/response-routes/SKILL.md +2 -2
  22. package/skills/route/SKILL.md +4 -0
  23. package/skills/router-setup/SKILL.md +3 -0
  24. package/skills/scripts/SKILL.md +179 -0
  25. package/skills/testing/SKILL.md +1 -1
  26. package/skills/testing/bindings.md +20 -6
  27. package/skills/testing/cache-prerender.md +5 -2
  28. package/skills/testing/client-components.md +2 -0
  29. package/skills/testing/e2e-parity.md +1 -1
  30. package/skills/testing/flight.md +8 -9
  31. package/skills/testing/render-handler.md +1 -1
  32. package/skills/testing/response-routes.md +1 -1
  33. package/skills/testing/server-actions.md +11 -11
  34. package/skills/testing/setup.md +3 -0
  35. package/skills/typesafety/SKILL.md +3 -2
  36. package/skills/use-cache/SKILL.md +10 -9
  37. package/src/browser/event-controller.ts +109 -2
  38. package/src/browser/partial-update.ts +12 -0
  39. package/src/browser/prefetch/cache.ts +17 -0
  40. package/src/browser/prefetch/fetch.ts +69 -2
  41. package/src/browser/react/Link.tsx +30 -5
  42. package/src/browser/react/NavigationProvider.tsx +12 -2
  43. package/src/browser/react/location-state-shared.ts +14 -2
  44. package/src/browser/react/use-href.tsx +8 -1
  45. package/src/browser/react/use-link-status.ts +23 -2
  46. package/src/browser/response-adapter.ts +14 -3
  47. package/src/browser/rsc-router.tsx +3 -0
  48. package/src/browser/scroll-restoration.ts +8 -3
  49. package/src/browser/server-action-bridge.ts +46 -11
  50. package/src/browser/types.ts +6 -0
  51. package/src/build/generate-route-types.ts +0 -1
  52. package/src/build/route-trie.ts +33 -9
  53. package/src/build/route-types/include-resolution.ts +7 -1
  54. package/src/build/route-types/router-processing.ts +0 -6
  55. package/src/build/route-types/source-scan.ts +105 -7
  56. package/src/cache/cache-policy.ts +42 -8
  57. package/src/cache/cache-runtime.ts +65 -5
  58. package/src/cache/cache-scope.ts +71 -11
  59. package/src/cache/cache-tag.ts +7 -2
  60. package/src/cache/cf/cf-base64.ts +33 -0
  61. package/src/cache/cf/cf-cache-constants.ts +127 -0
  62. package/src/cache/cf/cf-cache-store.ts +85 -613
  63. package/src/cache/cf/cf-cache-types.ts +349 -0
  64. package/src/cache/cf/cf-kv-utils.ts +46 -0
  65. package/src/cache/cf/cf-tag-marker-memo.ts +105 -0
  66. package/src/cache/document-cache.ts +11 -0
  67. package/src/cache/handle-snapshot.ts +8 -1
  68. package/src/cache/profile-registry.ts +25 -1
  69. package/src/cache/segment-codec.ts +9 -1
  70. package/src/cache/types.ts +4 -0
  71. package/src/client.rsc.tsx +38 -0
  72. package/src/client.tsx +11 -0
  73. package/src/components/DefaultDocument.tsx +8 -2
  74. package/src/context-var.ts +1 -1
  75. package/src/decode-loader-results.ts +7 -1
  76. package/src/escape-script.ts +52 -0
  77. package/src/handles/MetaTags.tsx +56 -5
  78. package/src/handles/Scripts.tsx +183 -0
  79. package/src/handles/breadcrumbs.ts +29 -11
  80. package/src/handles/is-thenable.ts +19 -0
  81. package/src/handles/meta.ts +46 -0
  82. package/src/handles/script.ts +244 -0
  83. package/src/host/cookie-handler.ts +7 -3
  84. package/src/host/pattern-matcher.ts +16 -2
  85. package/src/index.rsc.ts +5 -0
  86. package/src/index.ts +5 -0
  87. package/src/response-utils.ts +25 -0
  88. package/src/route-definition/dsl-helpers.ts +7 -0
  89. package/src/route-definition/redirect.ts +1 -2
  90. package/src/router/content-negotiation.ts +58 -10
  91. package/src/router/intercept-resolution.ts +9 -0
  92. package/src/router/match-middleware/cache-store.ts +10 -1
  93. package/src/router/middleware.ts +10 -3
  94. package/src/router/pattern-matching.ts +25 -23
  95. package/src/router/prefetch-cache-ttl.ts +51 -0
  96. package/src/router/router-interfaces.ts +7 -0
  97. package/src/router/router-options.ts +23 -0
  98. package/src/router/segment-resolution/fresh.ts +10 -0
  99. package/src/router/segment-resolution/helpers.ts +35 -1
  100. package/src/router/segment-resolution/loader-cache.ts +10 -6
  101. package/src/router/segment-resolution/revalidation.ts +6 -0
  102. package/src/router/segment-resolution.ts +1 -0
  103. package/src/router/trie-matching.ts +14 -9
  104. package/src/router.ts +18 -10
  105. package/src/rsc/handler.ts +52 -13
  106. package/src/rsc/helpers.ts +7 -1
  107. package/src/rsc/index.ts +1 -4
  108. package/src/rsc/loader-fetch.ts +107 -37
  109. package/src/rsc/progressive-enhancement.ts +18 -6
  110. package/src/rsc/response-cache-serve.ts +238 -0
  111. package/src/rsc/response-route-handler.ts +16 -133
  112. package/src/rsc/rsc-rendering.ts +13 -4
  113. package/src/rsc/server-action.ts +52 -6
  114. package/src/rsc/types.ts +7 -0
  115. package/src/search-params.ts +24 -5
  116. package/src/segment-loader-promise.ts +17 -2
  117. package/src/server/loader-registry.ts +16 -18
  118. package/src/server/request-context.ts +47 -20
  119. package/src/testing/dispatch.ts +108 -25
  120. package/src/testing/flight.ts +25 -0
  121. package/src/testing/internal/context.ts +25 -2
  122. package/src/testing/render-handler.ts +3 -1
  123. package/src/testing/render-route.tsx +15 -0
  124. package/src/testing/run-loader.ts +10 -3
  125. package/src/theme/ThemeProvider.tsx +20 -6
  126. package/src/theme/ThemeScript.tsx +7 -3
  127. package/src/theme/constants.ts +54 -3
  128. package/src/theme/theme-script.ts +22 -7
  129. package/src/types/request-scope.ts +8 -3
  130. package/src/vite/plugins/cjs-to-esm.ts +8 -1
  131. package/src/vite/plugins/expose-id-utils.ts +10 -1
  132. package/src/vite/plugins/expose-ids/handler-transform.ts +5 -16
  133. package/src/vite/plugins/expose-ids/loader-transform.ts +12 -5
  134. package/src/vite/plugins/expose-ids/router-transform.ts +6 -1
  135. package/src/vite/plugins/expose-internal-ids.ts +0 -1
  136. package/src/vite/plugins/version-plugin.ts +5 -17
  137. package/src/vite/plugins/virtual-entries.ts +12 -2
  138. package/src/vite/rango.ts +15 -6
  139. package/src/vite/utils/ast-handler-extract.ts +11 -4
  140. package/src/vite/utils/directive-prologue.ts +40 -0
  141. package/src/vite/utils/prerender-utils.ts +17 -2
@@ -18,7 +18,13 @@ import {
18
18
  } from "../server/request-context.js";
19
19
  import { recordRequestTags } from "./cache-tag.js";
20
20
  import { reportCacheError } from "./cache-error.js";
21
- import { serializeSegments, deserializeSegments } from "./segment-codec.js";
21
+ // segment-codec is the only module on cache-scope's import graph that eagerly
22
+ // pulls @vitejs/plugin-rsc (a virtual: module the plain node/vitest runner cannot
23
+ // resolve). It is imported LAZILY at the two call sites below (deserializeSegments
24
+ // in lookupRoute, serializeSegments in cacheRoute) so that requiring cache-scope —
25
+ // e.g. dispatch's lazy `import("../cache/cache-scope.js")` for the response-route
26
+ // cache path — does not crash a consumer test that never mocks plugin-rsc. Behavior
27
+ // is unchanged: both methods are async and already awaited the codec.
22
28
  import {
23
29
  captureHandles,
24
30
  restoreHandles,
@@ -28,6 +34,7 @@ import {
28
34
  import { sortedSearchString, sortedRouteParams } from "./cache-key-utils.js";
29
35
  import {
30
36
  DEFAULT_ROUTE_TTL,
37
+ isFiniteNonNegativeSeconds,
31
38
  resolveCacheKey,
32
39
  resolveCacheStore,
33
40
  resolveTagsOption,
@@ -48,6 +55,36 @@ function debugCacheLog(message: string): void {
48
55
  }
49
56
  }
50
57
 
58
+ /**
59
+ * A finite, non-negative seconds value? A NaN/Infinity ttl/swr (from a bad
60
+ * cache() option or store defaults) flows into computeExpiration ->
61
+ * staleAt/expiresAt = NaN, where every `now > NaN` is false so the entry never
62
+ * evicts and is served fresh forever; a negative value makes every read a miss.
63
+ * Mirror profile-registry.ts's Number.isFinite + >= 0 check, but the callers
64
+ * degrade to a default (warning in dev) rather than throw — this runs on the
65
+ * foreground render.
66
+ */
67
+ function isValidCacheSeconds(value: number, label: string): boolean {
68
+ if (isFiniteNonNegativeSeconds(value)) return true;
69
+ if (process.env.NODE_ENV !== "production") {
70
+ console.warn(
71
+ `[CacheScope] Invalid ${label} ${value}; falling back to default`,
72
+ );
73
+ }
74
+ return false;
75
+ }
76
+
77
+ /** Coerce a resolved ttl to a finite, non-negative number (default on invalid). */
78
+ function validatedTtl(value: number): number {
79
+ return isValidCacheSeconds(value, "ttl") ? value : DEFAULT_ROUTE_TTL;
80
+ }
81
+
82
+ /** Coerce a resolved swr to a finite, non-negative number, or undefined (no SWR window). */
83
+ function validatedSwr(value: number | undefined): number | undefined {
84
+ if (value === undefined) return undefined;
85
+ return isValidCacheSeconds(value, "swr") ? value : undefined;
86
+ }
87
+
51
88
  function getCacheKeyBase(
52
89
  host: string,
53
90
  pathname: string,
@@ -124,20 +161,26 @@ export class CacheScope {
124
161
  }
125
162
 
126
163
  /**
127
- * Get effective TTL from config or store defaults
164
+ * Get effective TTL from config or store defaults.
165
+ *
166
+ * Unlike profile-registry.ts (which fails fast at config time), the render
167
+ * path must DEGRADE: a non-finite/negative ttl (NaN/Infinity from a bad
168
+ * defaults config) would make computeExpiration produce NaN deadlines so the
169
+ * entry never evicts, or a guaranteed miss for a negative value. Fall back to
170
+ * DEFAULT_ROUTE_TTL instead of throwing in the foreground render.
128
171
  */
129
172
  get ttl(): number {
130
173
  if (this.config === false) return 0;
131
174
 
132
175
  // Explicit TTL in cache() options
133
176
  if (this.config.ttl !== undefined) {
134
- return this.config.ttl;
177
+ return validatedTtl(this.config.ttl);
135
178
  }
136
179
 
137
180
  // Fall back to store defaults (explicit store first, then app-level)
138
181
  const store = this.getStore();
139
182
  if (store?.defaults?.ttl !== undefined) {
140
- return store.defaults.ttl;
183
+ return validatedTtl(store.defaults.ttl);
141
184
  }
142
185
 
143
186
  // Hardcoded fallback
@@ -145,19 +188,22 @@ export class CacheScope {
145
188
  }
146
189
 
147
190
  /**
148
- * Get SWR window from config or store defaults
191
+ * Get SWR window from config or store defaults.
192
+ *
193
+ * A non-finite/negative swr is degraded to undefined (no SWR window) rather
194
+ * than fed into expiry math; see the ttl getter for the rationale.
149
195
  */
150
196
  get swr(): number | undefined {
151
197
  if (this.config === false) return undefined;
152
198
 
153
199
  // Explicit SWR in cache() options
154
200
  if (this.config.swr !== undefined) {
155
- return this.config.swr;
201
+ return validatedSwr(this.config.swr);
156
202
  }
157
203
 
158
204
  // Fall back to store defaults
159
205
  const store = this.getStore();
160
- return store?.defaults?.swr;
206
+ return validatedSwr(store?.defaults?.swr);
161
207
  }
162
208
 
163
209
  /**
@@ -231,10 +277,16 @@ export class CacheScope {
231
277
  const store = this.getStore();
232
278
  if (!store) return null;
233
279
 
234
- // Resolve cache key (may use custom key functions)
235
- const key = await this.resolveKey(pathname, params, isIntercept);
236
-
280
+ // Resolve cache key INSIDE the try so a throwing consumer key() (or a
281
+ // store.keyGenerator) degrades to a cache miss (return null -> render
282
+ // uncached) instead of crashing the foreground render. resolveCacheKey
283
+ // itself keeps its hard-fail/no-fallback-to-default contract (a throw must
284
+ // not silently collide onto the default slot); the graceful degradation
285
+ // happens here, where a miss is a safe outcome.
286
+ let key: string | undefined;
237
287
  try {
288
+ key = await this.resolveKey(pathname, params, isIntercept);
289
+
238
290
  const result = await store.get(key);
239
291
 
240
292
  if (!result) {
@@ -250,6 +302,7 @@ export class CacheScope {
250
302
  // error (handled by the outer catch).
251
303
  let segments: ResolvedSegment[];
252
304
  try {
305
+ const { deserializeSegments } = await import("./segment-codec.js");
253
306
  segments = await deserializeSegments(cached.segments);
254
307
  } catch (error) {
255
308
  reportCacheError(
@@ -294,7 +347,13 @@ export class CacheScope {
294
347
 
295
348
  return { segments, shouldRevalidate };
296
349
  } catch (error) {
297
- reportCacheError(error, "cache-read", `[CacheScope] lookup ${key}`);
350
+ // Covers a store.get() failure AND a throwing consumer key()/keyGenerator
351
+ // (resolveKey). Either way degrade to a cache miss so the render proceeds.
352
+ reportCacheError(
353
+ error,
354
+ "cache-read",
355
+ `[CacheScope] lookup ${key ?? "(key resolution failed)"}`,
356
+ );
298
357
  return null;
299
358
  }
300
359
  }
@@ -420,6 +479,7 @@ export class CacheScope {
420
479
  // Serialize segments and Flight-encode handles in parallel. Handles go
421
480
  // through the codec (not raw into the entry) so Promise/ReactNode handle
422
481
  // values survive a JSON-serializing store — see encodeHandles.
482
+ const { serializeSegments } = await import("./segment-codec.js");
423
483
  const [serializedSegments, encodedHandles] = await Promise.all([
424
484
  serializeSegments(nonLoaderSegments),
425
485
  encodeHandles(handles),
@@ -19,8 +19,13 @@ import {
19
19
  const cacheTagStorage = new AsyncLocalStorage<Set<string>>();
20
20
 
21
21
  export function normalizeTag(tag: string): string | null {
22
- if (!tag || !tag.trim()) return null;
23
- return tag;
22
+ // Trim and return the canonical (trimmed) form, not the raw tag. Both the
23
+ // write path (cacheTag) and the invalidate path (updateTag/revalidateTag)
24
+ // route through here, and matching is exact-string: returning the untrimmed
25
+ // tag made cacheTag(" products ") and updateTag("products") two different
26
+ // logical tags, a silent failure-to-invalidate (stale data served forever).
27
+ const trimmed = tag?.trim();
28
+ return trimmed ? trimmed : null;
24
29
  }
25
30
 
26
31
  export function normalizeTags(tags: Iterable<string>): string[] {
@@ -0,0 +1,33 @@
1
+ // ============================================================================
2
+ // Base64 Helpers (binary-safe response body encoding for KV)
3
+ // ============================================================================
4
+
5
+ // Chunk size for String.fromCharCode.apply: large enough to amortize the call
6
+ // overhead, small enough to stay well under the JS engine argument-count limit
7
+ // (~65k). 8192 turns a per-byte concat loop into O(n/8192) apply calls.
8
+ const FROM_CHARCODE_CHUNK = 8192;
9
+
10
+ /** Encode ArrayBuffer to base64 string. */
11
+ export function bufferToBase64(buffer: ArrayBuffer): string {
12
+ const bytes = new Uint8Array(buffer);
13
+ // Build the binary (latin1) string in fixed-size chunks instead of one
14
+ // String.fromCharCode per byte. Identical output to the per-byte loop (each
15
+ // byte maps to the same code unit); just far fewer string concatenations for
16
+ // large document payloads.
17
+ let binary = "";
18
+ for (let i = 0; i < bytes.length; i += FROM_CHARCODE_CHUNK) {
19
+ const chunk = bytes.subarray(i, i + FROM_CHARCODE_CHUNK);
20
+ binary += String.fromCharCode.apply(null, chunk as unknown as number[]);
21
+ }
22
+ return btoa(binary);
23
+ }
24
+
25
+ /** Decode base64 string to ArrayBuffer. */
26
+ export function base64ToBuffer(base64: string): ArrayBuffer {
27
+ const binary = atob(base64);
28
+ const bytes = new Uint8Array(binary.length);
29
+ for (let i = 0; i < binary.length; i++) {
30
+ bytes[i] = binary.charCodeAt(i);
31
+ }
32
+ return bytes.buffer;
33
+ }
@@ -0,0 +1,127 @@
1
+ // ============================================================================
2
+ // Constants
3
+ // ============================================================================
4
+ //
5
+ // Header names, KV prefixes, and timeout/interval defaults for the CF cache
6
+ // store. Extracted from cf-cache-store.ts so the constants can be shared by the
7
+ // store and its sibling collaborator modules without a circular import back to
8
+ // the class. The public ones (CACHE_*_HEADER, TAG_MARKER_PREFIX,
9
+ // MAX_REVALIDATION_INTERVAL, EDGE_*_TIMEOUT_MS, KV_READ_TIMEOUT_MS) are
10
+ // re-exported from cf-cache-store.ts so existing import paths still resolve.
11
+
12
+ /** Header storing timestamp when entry becomes stale */
13
+ export const CACHE_STALE_AT_HEADER = "x-edge-cache-stale-at";
14
+
15
+ /** Header storing cache status: HIT | REVALIDATING */
16
+ export const CACHE_STATUS_HEADER = "x-edge-cache-status";
17
+
18
+ /**
19
+ * Header storing this entry's cache tags as a JSON array. JSON-encoded (not the
20
+ * comma-delimited CF `Cache-Tag` format) so tags containing commas round-trip
21
+ * safely; the read paths parse this to run the tag-invalidation check.
22
+ */
23
+ export const CACHE_TAGS_HEADER = "x-edge-cache-tags";
24
+
25
+ /** Header storing the ms-epoch timestamp when this entry's tags were attached. */
26
+ export const CACHE_TAGGED_AT_HEADER = "x-edge-cache-tagged-at";
27
+
28
+ /**
29
+ * KV key prefix for tag-invalidation markers. A marker stores the ms-epoch
30
+ * timestamp of the most recent invalidation of a tag; reads treat any entry
31
+ * whose taggedAt is older than its tags' latest marker as invalidated. Markers
32
+ * live in the SAME KV namespace as the cached entries - there is no separate
33
+ * tag-invalidation store.
34
+ */
35
+ export const TAG_MARKER_PREFIX = "__tag__/";
36
+
37
+ /**
38
+ * Header storing the epoch-ms timestamp when an entry was marked REVALIDATING.
39
+ * The SWR thundering-herd guard reads this to decide whether the in-flight
40
+ * revalidation is still recent. It replaces a prior reliance on the HTTP `Age`
41
+ * header: CF's Cache API does not populate `Age` reliably per-colo (and our own
42
+ * unit MockCache never set it), so an absent `Age` defaulted to 0 and made every
43
+ * REVALIDATING entry look "just revalidated" forever -- a dropped/never-finished
44
+ * background revalidation could then pin an entry stale until hard expiry. An
45
+ * explicit timestamp we write ourselves (same pattern as CACHE_STALE_AT_HEADER)
46
+ * is reliable and lets the MAX_REVALIDATION_INTERVAL re-arm actually fire.
47
+ */
48
+ export const CACHE_REVALIDATING_AT_HEADER = "x-edge-cache-revalidating-at";
49
+
50
+ /**
51
+ * Header storing the absolute epoch-ms hard-expiry deadline (staleAt +
52
+ * swrWindow*1000) of an L1 entry. The stale-path REVALIDATING re-put reads this
53
+ * to recompute a SHRINKING Cache-Control max-age instead of copying set()'s
54
+ * original full-window max-age. Without it, every MAX_REVALIDATION_INTERVAL
55
+ * re-arm re-puts the full window and restarts CF's retention clock, pinning a
56
+ * perpetually-stale entry (one whose background revalidation keeps failing) past
57
+ * its intended hard-expiry indefinitely. Mirrors the KVSegmentEnvelope `e`
58
+ * field and the remaining-ttl math in promoteSegmentToL1/promoteItemToL1.
59
+ * @internal
60
+ */
61
+ export const CACHE_EXPIRES_AT_HEADER = "x-edge-cache-expires-at";
62
+
63
+ /**
64
+ * Header stashing the route author's original Cache-Control on L1 document
65
+ * entries. putResponse/promoteResponseToL1 overwrite Cache-Control with a long
66
+ * `max-age` so the CF Cache API retains the entry across the whole SWR window;
67
+ * getResponse restores this original value before serving so the client and any
68
+ * upstream CDN see the author's intended directive, not the internal edge TTL.
69
+ */
70
+ export const CACHE_ORIG_CC_HEADER = "x-edge-cache-orig-cc";
71
+
72
+ /**
73
+ * Maximum age in seconds for REVALIDATING status before allowing new revalidation.
74
+ * After this period, a stale entry in REVALIDATING status will trigger revalidation again.
75
+ * @internal
76
+ */
77
+ export const MAX_REVALIDATION_INTERVAL = 30;
78
+
79
+ /**
80
+ * Maximum time (ms) to wait for an L1 edge cache (CF Cache API) read before
81
+ * giving up and treating it as a miss. The Cache API is normally sub-millisecond
82
+ * per-colo, so a slow `match` signals a degraded colo; we don't want it adding
83
+ * latency to the request. On timeout the lookup is abandoned, a warning is
84
+ * logged, and the read falls through to its normal miss path (L2/KV or render).
85
+ *
86
+ * This is the default; override per store via
87
+ * `CFCacheStoreOptions.edgeLookupTimeoutMs` (<= 0 disables the budget).
88
+ */
89
+ export const EDGE_LOOKUP_TIMEOUT_MS = 10;
90
+
91
+ /**
92
+ * Maximum time (ms) to wait for the BODY of a matched L1 entry to be read
93
+ * (response.json()) before treating the read as a miss.
94
+ *
95
+ * This is separate from {@link EDGE_LOOKUP_TIMEOUT_MS} on purpose. CF's Cache
96
+ * API resolves `match()` with a lazily-streamed body, so a fast `match` can be
97
+ * followed by a multi-second stall while the body bytes are fetched -- the
98
+ * latency tail lives here, after the match budget has already passed. The
99
+ * default bounds that tail aggressively: a healthy per-colo body read (fetch +
100
+ * JSON parse) settles in low single-digit milliseconds, so 20ms clears a
101
+ * healthy read while still failing fast to L2/KV (or render) on a degraded colo
102
+ * instead of pinning the request behind a seconds-long read. Raise it per store
103
+ * if large Flight payloads legitimately need longer.
104
+ *
105
+ * Override per store via `CFCacheStoreOptions.edgeReadTimeoutMs` (<= 0 disables).
106
+ */
107
+ export const EDGE_READ_TIMEOUT_MS = 20;
108
+
109
+ /**
110
+ * Maximum time (ms) to wait for an L2 (KV) read (`kv.get(key, {type:"json"})`)
111
+ * before treating it as a miss. Unlike the L1 budgets, KV is a GLOBAL store: the
112
+ * file header documents ~50ms healthy reads, and a degraded namespace can tail
113
+ * to seconds. KV is the LAST cache tier before a full render, so an unbounded
114
+ * read here pins the whole request behind a degraded global lookup.
115
+ *
116
+ * The default (170ms) sits a few multiples above the documented ~50ms healthy
117
+ * read, leaving headroom for legitimate latency tails (larger payloads,
118
+ * far-from-colo regions) so a healthy-but-slow read does not false-miss into a
119
+ * render, while still abandoning a genuinely degraded namespace well before its
120
+ * multi-second tail can pin the request. A deployment with a tighter SLA can
121
+ * lower it, and one whose healthy p99 runs higher should raise it: measure the
122
+ * KV read p99 (Workers Analytics) and add margin. It is a degradation
123
+ * guard-rail, not a tuning lever for "slow KV is normal here".
124
+ *
125
+ * Override per store via `CFCacheStoreOptions.kvReadTimeoutMs` (<= 0 disables).
126
+ */
127
+ export const KV_READ_TIMEOUT_MS = 170;