@rangojs/router 0.27.0 → 0.29.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (243) hide show
  1. package/dist/bin/rango.js +1 -1
  2. package/dist/types/__internal.d.ts +7 -1
  3. package/dist/types/browser/action-fence.d.ts +2 -1
  4. package/dist/types/browser/event-controller.d.ts +8 -2
  5. package/dist/types/browser/history-state.d.ts +3 -9
  6. package/dist/types/browser/navigation-store.d.ts +8 -6
  7. package/dist/types/browser/partial-update.d.ts +11 -0
  8. package/dist/types/browser/react/filter-segment-order.d.ts +2 -2
  9. package/dist/types/browser/react/use-action.d.ts +7 -2
  10. package/dist/types/browser/react/use-handle.d.ts +19 -16
  11. package/dist/types/browser/react/use-link-status.d.ts +5 -2
  12. package/dist/types/browser/segment-reconciler.d.ts +32 -4
  13. package/dist/types/browser/types.d.ts +24 -5
  14. package/dist/types/browser/wire-protocol.d.ts +26 -0
  15. package/dist/types/cache/cache-key-utils.d.ts +11 -5
  16. package/dist/types/cache/cache-scope.d.ts +34 -22
  17. package/dist/types/cache/cf/cf-cache-constants.d.ts +2 -2
  18. package/dist/types/cache/cf/cf-cache-store.d.ts +47 -65
  19. package/dist/types/cache/cf/cf-kv-utils.d.ts +1 -1
  20. package/dist/types/cache/handle-snapshot.d.ts +9 -4
  21. package/dist/types/cache/segment-codec.d.ts +14 -0
  22. package/dist/types/cache/shell-snapshot.d.ts +1 -1
  23. package/dist/types/cache/taint.d.ts +5 -3
  24. package/dist/types/cache/types.d.ts +7 -3
  25. package/dist/types/cache/vercel/vercel-cache-store.d.ts +8 -6
  26. package/dist/types/prefetch-markers.d.ts +5 -14
  27. package/dist/types/prerender/store.d.ts +31 -0
  28. package/dist/types/prerender/warm-request.d.ts +3 -3
  29. package/dist/types/prerender.d.ts +2 -2
  30. package/dist/types/route-name.d.ts +5 -0
  31. package/dist/types/router/error-handling.d.ts +11 -5
  32. package/dist/types/router/instrument.d.ts +4 -3
  33. package/dist/types/router/intercept-resolution.d.ts +25 -9
  34. package/dist/types/router/match-context.d.ts +6 -11
  35. package/dist/types/router/match-handlers.d.ts +2 -6
  36. package/dist/types/router/match-middleware/background-revalidation.d.ts +13 -22
  37. package/dist/types/router/match-middleware/cache-lookup.d.ts +34 -86
  38. package/dist/types/router/match-middleware/segment-resolution.d.ts +8 -6
  39. package/dist/types/router/navigation-snapshot.d.ts +7 -0
  40. package/dist/types/router/prerender-match.d.ts +21 -6
  41. package/dist/types/router/preview-match.d.ts +2 -7
  42. package/dist/types/router/request-classification.d.ts +5 -0
  43. package/dist/types/router/router-context.d.ts +7 -46
  44. package/dist/types/router/router-interfaces.d.ts +8 -14
  45. package/dist/types/router/segment-resolution/helpers.d.ts +19 -3
  46. package/dist/types/router/segment-resolution/loader-cache.d.ts +3 -3
  47. package/dist/types/router/segment-resolution/loader-mask.d.ts +4 -4
  48. package/dist/types/router/segment-resolution/prefetch-deferral.d.ts +20 -18
  49. package/dist/types/router/segment-resolution/resolution-pass.d.ts +95 -0
  50. package/dist/types/router/segment-resolution/revalidation.d.ts +82 -31
  51. package/dist/types/router/segment-resolution/streamed-handler-boundary.d.ts +2 -0
  52. package/dist/types/router/segment-resolution.d.ts +1 -2
  53. package/dist/types/router/segment-wrappers.d.ts +11 -48
  54. package/dist/types/router/telemetry.d.ts +2 -0
  55. package/dist/types/router/types.d.ts +11 -1
  56. package/dist/types/rsc/full-payload.d.ts +2 -2
  57. package/dist/types/rsc/handler-context.d.ts +6 -6
  58. package/dist/types/rsc/json-route-result.d.ts +5 -9
  59. package/dist/types/rsc/request-stages.d.ts +203 -0
  60. package/dist/types/rsc/response-cache-serve.d.ts +7 -12
  61. package/dist/types/rsc/response-route-handler.d.ts +2 -2
  62. package/dist/types/rsc/routine-plan.d.ts +1 -1
  63. package/dist/types/rsc/server-action.d.ts +2 -2
  64. package/dist/types/rsc/shell-capture-constants.d.ts +1 -1
  65. package/dist/types/rsc/shell-capture.d.ts +132 -15
  66. package/dist/types/rsc/shell-serve.d.ts +37 -12
  67. package/dist/types/server/context.d.ts +9 -8
  68. package/dist/types/server/handle-store.d.ts +16 -0
  69. package/dist/types/server/render-pass.d.ts +72 -0
  70. package/dist/types/server/request-context.d.ts +53 -104
  71. package/dist/types/server/rewrite-trail.d.ts +2 -2
  72. package/dist/types/testing/dispatch.d.ts +35 -18
  73. package/dist/types/testing/render-route.d.ts +43 -8
  74. package/dist/types/testing/serve-shell-request.d.ts +8 -1
  75. package/dist/types/types/cache-types.d.ts +6 -3
  76. package/dist/types/types/error-types.d.ts +30 -3
  77. package/dist/types/types/handler-context.d.ts +9 -1
  78. package/dist/types/types/segments.d.ts +1 -1
  79. package/dist/types/vite/discovery/build-discovery-phase.d.ts +40 -0
  80. package/dist/types/vite/discovery/build-env.d.ts +22 -0
  81. package/dist/types/vite/discovery/bundle-scan-phase.d.ts +12 -0
  82. package/dist/types/vite/discovery/dev-prerender-cache.d.ts +3 -2
  83. package/dist/types/vite/discovery/dev-server-phase.d.ts +26 -0
  84. package/dist/types/vite/discovery/discovery-errors.d.ts +1 -1
  85. package/dist/types/vite/discovery/state.d.ts +12 -0
  86. package/dist/types/vite/plugins/version-plugin.d.ts +1 -1
  87. package/dist/types/vite/plugins/virtual-entries.d.ts +1 -1
  88. package/dist/types/vite/router-discovery.d.ts +8 -47
  89. package/dist/types/vite/temp-rsc-server.d.ts +39 -0
  90. package/dist/types/vite/utils/prerender-utils.d.ts +9 -0
  91. package/dist/types/wire-params.d.ts +39 -0
  92. package/dist/vite/index.js +3822 -3727
  93. package/package.json +1 -1
  94. package/skills/cache-guide/SKILL.md +8 -5
  95. package/skills/caching/SKILL.md +37 -18
  96. package/skills/client-urls/SKILL.md +4 -2
  97. package/skills/document-cache/SKILL.md +13 -1
  98. package/skills/handler-use/SKILL.md +1 -1
  99. package/skills/hooks/SKILL.md +1 -1
  100. package/skills/hooks/data.md +8 -1
  101. package/skills/hooks/handle-and-actions.md +33 -2
  102. package/skills/hooks/navigation.md +121 -15
  103. package/skills/hooks/state.md +6 -4
  104. package/skills/hooks/urls.md +16 -5
  105. package/skills/intercept/SKILL.md +23 -7
  106. package/skills/middleware/SKILL.md +18 -0
  107. package/skills/observability/SKILL.md +30 -0
  108. package/skills/ppr/SKILL.md +18 -3
  109. package/skills/prerender/SKILL.md +39 -15
  110. package/skills/rango/SKILL.md +5 -1
  111. package/skills/server-actions/SKILL.md +15 -1
  112. package/skills/testing/client-components.md +25 -18
  113. package/skills/testing/response-routes.md +6 -4
  114. package/skills/testing/serve-shell-request.md +7 -7
  115. package/skills/use-cache/SKILL.md +46 -8
  116. package/skills/view-transitions/SKILL.md +1 -1
  117. package/src/__internal.ts +7 -1
  118. package/src/browser/action-fence.ts +2 -1
  119. package/src/browser/connection-warmup.ts +3 -1
  120. package/src/browser/event-controller.ts +37 -13
  121. package/src/browser/history-state.ts +6 -12
  122. package/src/browser/navigation-bridge.ts +59 -26
  123. package/src/browser/navigation-client.ts +20 -25
  124. package/src/browser/navigation-store.ts +25 -27
  125. package/src/browser/navigation-transaction.ts +4 -1
  126. package/src/browser/network-error-handler.ts +4 -0
  127. package/src/browser/partial-update.ts +33 -6
  128. package/src/browser/prefetch/cache.ts +2 -1
  129. package/src/browser/prefetch/fetch.ts +8 -16
  130. package/src/browser/prefetch/queue.ts +4 -4
  131. package/src/browser/react/NavigationProvider.tsx +2 -1
  132. package/src/browser/react/filter-segment-order.ts +2 -2
  133. package/src/browser/react/use-action.ts +24 -48
  134. package/src/browser/react/use-handle.ts +22 -18
  135. package/src/browser/react/use-link-status.ts +48 -60
  136. package/src/browser/react/use-navigation.ts +92 -41
  137. package/src/browser/rsc-router.tsx +1 -0
  138. package/src/browser/segment-reconciler.ts +109 -53
  139. package/src/browser/server-action-bridge.ts +81 -61
  140. package/src/browser/types.ts +25 -5
  141. package/src/browser/wire-protocol.ts +43 -0
  142. package/src/cache/cache-key-utils.ts +11 -5
  143. package/src/cache/cache-runtime.ts +132 -75
  144. package/src/cache/cache-scope.ts +110 -64
  145. package/src/cache/cf/cf-cache-constants.ts +2 -2
  146. package/src/cache/cf/cf-cache-store.ts +486 -874
  147. package/src/cache/cf/cf-kv-utils.ts +1 -1
  148. package/src/cache/document-cache.ts +64 -46
  149. package/src/cache/handle-snapshot.ts +19 -7
  150. package/src/cache/read-through-swr.ts +20 -18
  151. package/src/cache/segment-codec.ts +55 -0
  152. package/src/cache/shell-snapshot.ts +2 -2
  153. package/src/cache/tag-invalidation.ts +6 -2
  154. package/src/cache/taint.ts +5 -3
  155. package/src/cache/types.ts +7 -3
  156. package/src/cache/vercel/vercel-cache-store.ts +243 -310
  157. package/src/handles/pushed-by.ts +1 -1
  158. package/src/prefetch-markers.ts +5 -16
  159. package/src/prerender/store.ts +76 -0
  160. package/src/prerender/warm-request.ts +8 -4
  161. package/src/prerender.ts +2 -2
  162. package/src/route-name.ts +8 -0
  163. package/src/router/error-handling.ts +22 -10
  164. package/src/router/instrument.ts +4 -3
  165. package/src/router/intercept-resolution.ts +82 -75
  166. package/src/router/loader-resolution.ts +5 -4
  167. package/src/router/match-api.ts +5 -5
  168. package/src/router/match-context.ts +23 -12
  169. package/src/router/match-handlers.ts +8 -20
  170. package/src/router/match-middleware/background-revalidation.ts +46 -57
  171. package/src/router/match-middleware/cache-lookup.ts +677 -726
  172. package/src/router/match-middleware/cache-store.ts +4 -8
  173. package/src/router/match-middleware/intercept-resolution.ts +3 -23
  174. package/src/router/match-middleware/segment-resolution.ts +34 -61
  175. package/src/router/middleware.ts +69 -11
  176. package/src/router/navigation-snapshot.ts +14 -4
  177. package/src/router/prerender-match.ts +100 -44
  178. package/src/router/preview-match.ts +6 -41
  179. package/src/router/request-classification.ts +13 -7
  180. package/src/router/route-snapshot.ts +2 -5
  181. package/src/router/router-context.ts +10 -95
  182. package/src/router/router-interfaces.ts +8 -14
  183. package/src/router/segment-resolution/helpers.ts +49 -13
  184. package/src/router/segment-resolution/loader-cache.ts +15 -9
  185. package/src/router/segment-resolution/loader-mask.ts +5 -5
  186. package/src/router/segment-resolution/prefetch-deferral.ts +30 -27
  187. package/src/router/segment-resolution/resolution-pass.ts +139 -0
  188. package/src/router/segment-resolution/revalidation.ts +939 -789
  189. package/src/router/segment-resolution/streamed-handler-boundary.ts +3 -0
  190. package/src/router/segment-resolution.ts +4 -13
  191. package/src/router/segment-wrappers.ts +51 -269
  192. package/src/router/telemetry.ts +5 -0
  193. package/src/router/types.ts +11 -1
  194. package/src/router.ts +19 -17
  195. package/src/rsc/full-payload.ts +6 -5
  196. package/src/rsc/handler-context.ts +8 -14
  197. package/src/rsc/handler.ts +126 -507
  198. package/src/rsc/json-route-result.ts +5 -9
  199. package/src/rsc/loader-fetch.ts +6 -5
  200. package/src/rsc/progressive-enhancement.ts +5 -0
  201. package/src/rsc/request-stages.ts +759 -0
  202. package/src/rsc/response-cache-serve.ts +10 -14
  203. package/src/rsc/response-route-handler.ts +13 -5
  204. package/src/rsc/rewrite-passes.ts +2 -1
  205. package/src/rsc/routine-plan.ts +1 -1
  206. package/src/rsc/rsc-rendering.ts +164 -349
  207. package/src/rsc/server-action.ts +87 -113
  208. package/src/rsc/shell-capture-constants.ts +1 -1
  209. package/src/rsc/shell-capture.ts +275 -227
  210. package/src/rsc/shell-serve.ts +121 -18
  211. package/src/rsc/ssr-setup.ts +5 -4
  212. package/src/segment-system.tsx +3 -3
  213. package/src/server/context.ts +22 -25
  214. package/src/server/handle-store.ts +55 -0
  215. package/src/server/render-pass.ts +291 -0
  216. package/src/server/request-context.ts +75 -115
  217. package/src/server/rewrite-trail.ts +6 -4
  218. package/src/ssr/index.tsx +1 -1
  219. package/src/testing/dispatch.ts +226 -710
  220. package/src/testing/e2e/page-helpers.ts +2 -2
  221. package/src/testing/render-route.tsx +199 -13
  222. package/src/testing/serve-shell-request.ts +36 -40
  223. package/src/types/cache-types.ts +6 -3
  224. package/src/types/error-types.ts +30 -3
  225. package/src/types/handler-context.ts +12 -1
  226. package/src/types/segments.ts +1 -1
  227. package/src/vite/discovery/build-discovery-phase.ts +204 -0
  228. package/src/vite/discovery/build-env.ts +148 -0
  229. package/src/vite/discovery/bundle-scan-phase.ts +128 -0
  230. package/src/vite/discovery/dev-prerender-cache.ts +8 -3
  231. package/src/vite/discovery/dev-server-phase.ts +1776 -0
  232. package/src/vite/discovery/discover-routers.ts +1 -1
  233. package/src/vite/discovery/discovery-errors.ts +1 -1
  234. package/src/vite/discovery/prerender-collection.ts +24 -39
  235. package/src/vite/discovery/state.ts +15 -0
  236. package/src/vite/plugins/version-plugin.ts +1 -1
  237. package/src/vite/plugins/virtual-entries.ts +1 -1
  238. package/src/vite/router-discovery.ts +29 -2428
  239. package/src/vite/temp-rsc-server.ts +223 -0
  240. package/src/vite/utils/prerender-utils.ts +47 -0
  241. package/src/wire-params.ts +40 -0
  242. package/dist/types/router/segment-resolution/fresh.d.ts +0 -72
  243. package/src/router/segment-resolution/fresh.ts +0 -945
@@ -445,6 +445,85 @@ function isRecord(value: unknown): value is Record<string, unknown> {
445
445
  return typeof value === "object" && value !== null;
446
446
  }
447
447
 
448
+ function asSegmentEnvelope(raw: unknown): VercelSegmentEnvelope | null {
449
+ if (!isRecord(raw)) return null;
450
+ const { d, s, e, ta } = raw;
451
+ if (!isRecord(d) || !Array.isArray((d as Record<string, unknown>).segments)) {
452
+ return null;
453
+ }
454
+ if (typeof s !== "number" || typeof e !== "number") return null;
455
+ return {
456
+ d: d as unknown as CachedEntryData,
457
+ s,
458
+ e,
459
+ ta: typeof ta === "number" ? ta : undefined,
460
+ };
461
+ }
462
+
463
+ function asItemEnvelope(raw: unknown): VercelItemEnvelope | null {
464
+ if (!isRecord(raw)) return null;
465
+ const { v, h, s, e, t, ta } = raw;
466
+ if (typeof v !== "string") return null;
467
+ if (typeof s !== "number" || typeof e !== "number") return null;
468
+ return {
469
+ v,
470
+ h: typeof h === "string" ? h : undefined,
471
+ s,
472
+ e,
473
+ ta: typeof ta === "number" ? ta : undefined,
474
+ t: Array.isArray(t) ? (t as string[]) : undefined,
475
+ };
476
+ }
477
+
478
+ function asShellEnvelope(raw: unknown): VercelShellEnvelope | null {
479
+ if (!isRecord(raw)) return null;
480
+ const { p, po, rv, bv, c, s, e, t, i, lp, sn, dk, pr, no } = raw;
481
+ if (typeof rv !== "string" || typeof bv !== "string") return null;
482
+ // Document half: required unless navigationOnly (`no`), which stores
483
+ // neither field. Tolerate a legacy navigationOnly envelope that still
484
+ // carries them.
485
+ if (p === undefined ? no !== true : typeof p !== "string") return null;
486
+ if (po === undefined ? no !== true : po !== null && typeof po !== "string") {
487
+ return null;
488
+ }
489
+ if (typeof c !== "number") return null;
490
+ if (typeof s !== "number" || typeof e !== "number") return null;
491
+ return {
492
+ ...(typeof p === "string" ? { p } : {}),
493
+ ...(po !== undefined ? { po: po as string | null } : {}),
494
+ rv,
495
+ bv,
496
+ c,
497
+ s,
498
+ e,
499
+ t: Array.isArray(t) ? (t as string[]) : undefined,
500
+ i: typeof i === "string" ? i : undefined,
501
+ lp: typeof lp === "string" ? lp : undefined,
502
+ // A non-array field reads as no records.
503
+ sn: Array.isArray(sn) ? (sn as ShellSnapshotRecord[]) : [],
504
+ dk: typeof dk === "string" ? dk : undefined,
505
+ pr: typeof pr === "string" ? pr : undefined,
506
+ no: no === true ? true : undefined,
507
+ };
508
+ }
509
+
510
+ function asResponseEnvelope(raw: unknown): VercelResponseEnvelope | null {
511
+ if (!isRecord(raw)) return null;
512
+ const { b, st, hd, s, e, t, ta } = raw;
513
+ if (typeof b !== "string" || typeof st !== "number") return null;
514
+ if (!Array.isArray(hd)) return null;
515
+ if (typeof s !== "number" || typeof e !== "number") return null;
516
+ return {
517
+ b,
518
+ st,
519
+ hd: hd as [string, string][],
520
+ s,
521
+ e,
522
+ t: Array.isArray(t) ? (t as string[]) : undefined,
523
+ ta: typeof ta === "number" ? ta : undefined,
524
+ };
525
+ }
526
+
448
527
  /**
449
528
  * The envelope's `ta` stamp, on tagged entries only: the producing execution's
450
529
  * start when the writer passed it, else write time (#1068). Only the request
@@ -456,6 +535,83 @@ function tagStamp(tags: string[], startedAt?: number): { ta?: number } {
456
535
  return ta === undefined ? {} : { ta };
457
536
  }
458
537
 
538
+ /**
539
+ * A family read of VercelCacheStore (`get`, `getItem`, `getResponse`,
540
+ * through readRecord): its keys, its envelope check, the tags the request
541
+ * mask reads and the hit it serves.
542
+ */
543
+ interface VercelReadFamily<
544
+ E extends { s: number; e: number; ta?: number },
545
+ V extends { shouldRevalidate: boolean },
546
+ Err extends CacheReadError | null,
547
+ > {
548
+ op: "get" | "getItem" | "getResponse";
549
+ family: "s" | "i" | "r";
550
+ /** Names the envelope in the corrupt-entry report. */
551
+ name: "segment" | "item" | "response";
552
+ readError: Err;
553
+ decode(raw: unknown): E | null;
554
+ /** The tags the request mask checks, with the envelope's `ta`. */
555
+ tags(env: E): string[] | undefined;
556
+ /**
557
+ * The hit, with `shouldRevalidate: false` until the stale claim sets it;
558
+ * throws when the envelope does not rebuild.
559
+ */
560
+ hit(env: E): V;
561
+ }
562
+
563
+ const SEGMENT_READ: VercelReadFamily<
564
+ VercelSegmentEnvelope,
565
+ CacheGetResult,
566
+ CacheReadError
567
+ > = {
568
+ op: "get",
569
+ family: "s",
570
+ name: "segment",
571
+ // Distinct from a miss so the PPR replay composition renders uncached
572
+ // instead of substituting the seeded doc record.
573
+ readError: CACHE_READ_ERROR,
574
+ decode: asSegmentEnvelope,
575
+ tags: (env) => env.d.tags,
576
+ hit: (env) => ({ data: env.d, shouldRevalidate: false }),
577
+ };
578
+
579
+ const ITEM_READ: VercelReadFamily<VercelItemEnvelope, CacheItemResult, null> = {
580
+ op: "getItem",
581
+ family: "i",
582
+ name: "item",
583
+ readError: null,
584
+ decode: asItemEnvelope,
585
+ tags: (env) => env.t,
586
+ hit: (env) => ({
587
+ value: env.v,
588
+ handles: env.h,
589
+ shouldRevalidate: false,
590
+ tags: env.t,
591
+ }),
592
+ };
593
+
594
+ const RESPONSE_READ: VercelReadFamily<
595
+ VercelResponseEnvelope,
596
+ { response: Response; shouldRevalidate: boolean },
597
+ null
598
+ > = {
599
+ op: "getResponse",
600
+ family: "r",
601
+ name: "response",
602
+ readError: null,
603
+ decode: asResponseEnvelope,
604
+ tags: (env) => env.t,
605
+ // Invalid base64, bad header entries or an out-of-range status throw.
606
+ hit: (env) => ({
607
+ response: new Response(base64ToBuffer(env.b), {
608
+ status: env.st,
609
+ headers: new Headers(env.hd),
610
+ }),
611
+ shouldRevalidate: false,
612
+ }),
613
+ };
614
+
459
615
  /**
460
616
  * Vercel Runtime Cache-backed segment cache store.
461
617
  *
@@ -562,68 +718,8 @@ export class VercelCacheStore<
562
718
 
563
719
  // --- Segment family (get/set/delete) ---
564
720
 
565
- async get(key: string): Promise<CacheGetResult | null | CacheReadError> {
566
- const storeKey = this.toStoreKey(key, "s");
567
- const started = Date.now();
568
- let raw: unknown;
569
- try {
570
- raw = await this.cache.get(storeKey);
571
- } catch (error) {
572
- reportCacheError(error, "cache-read", "[VercelCacheStore] get");
573
- this.emitDebug({ op: "get", key, outcome: "error" });
574
- // Distinct from a miss so the PPR replay composition renders uncached
575
- // instead of substituting the seeded doc record (CACHE_READ_ERROR).
576
- return CACHE_READ_ERROR;
577
- }
578
- const readMs = Date.now() - started;
579
-
580
- if (raw == null) {
581
- this.emitDebug({ op: "get", key, outcome: "miss", readMs });
582
- return null;
583
- }
584
- const env = this.asSegmentEnvelope(this.decodeRaw(raw));
585
- if (!env) {
586
- reportCacheError(
587
- new Error("malformed segment envelope"),
588
- "cache-corrupt",
589
- "[VercelCacheStore] get",
590
- );
591
- void this.safeDelete(storeKey);
592
- this.emitDebug({ op: "get", key, outcome: "corrupt", readMs });
593
- return null;
594
- }
595
-
596
- const now = Date.now();
597
- if (now > env.e) {
598
- void this.safeDelete(storeKey);
599
- this.emitDebug({
600
- op: "get",
601
- key,
602
- outcome: "expired",
603
- staleAt: env.s,
604
- expiresAt: env.e,
605
- readMs,
606
- });
607
- return null;
608
- }
609
- if (this.maskedMiss("get", key, { t: env.d.tags, ta: env.ta }, readMs)) {
610
- return null;
611
- }
612
-
613
- const isStale = env.s > 0 && now > env.s;
614
- const shouldRevalidate = isStale
615
- ? await this.claimRevalidation(storeKey, env.e, "[VercelCacheStore] get")
616
- : false;
617
- this.emitDebug({
618
- op: "get",
619
- key,
620
- outcome: shouldRevalidate ? "stale-revalidate" : "fresh",
621
- shouldRevalidate,
622
- staleAt: env.s,
623
- expiresAt: env.e,
624
- readMs,
625
- });
626
- return { data: env.d, shouldRevalidate };
721
+ get(key: string): Promise<CacheGetResult | null | CacheReadError> {
722
+ return this.readRecord(key, SEGMENT_READ);
627
723
  }
628
724
 
629
725
  async set(
@@ -677,92 +773,10 @@ export class VercelCacheStore<
677
773
 
678
774
  // --- Response family (getResponse/putResponse) ---
679
775
 
680
- async getResponse(
776
+ getResponse(
681
777
  key: string,
682
778
  ): Promise<{ response: Response; shouldRevalidate: boolean } | null> {
683
- const storeKey = this.toStoreKey(key, "r");
684
- const started = Date.now();
685
- let raw: unknown;
686
- try {
687
- raw = await this.cache.get(storeKey);
688
- } catch (error) {
689
- reportCacheError(error, "cache-read", "[VercelCacheStore] getResponse");
690
- this.emitDebug({ op: "getResponse", key, outcome: "error" });
691
- return null;
692
- }
693
- const readMs = Date.now() - started;
694
-
695
- if (raw == null) {
696
- this.emitDebug({ op: "getResponse", key, outcome: "miss", readMs });
697
- return null;
698
- }
699
- const env = this.asResponseEnvelope(this.decodeRaw(raw));
700
- if (!env) {
701
- reportCacheError(
702
- new Error("malformed response envelope"),
703
- "cache-corrupt",
704
- "[VercelCacheStore] getResponse",
705
- );
706
- void this.safeDelete(storeKey);
707
- this.emitDebug({ op: "getResponse", key, outcome: "corrupt", readMs });
708
- return null;
709
- }
710
-
711
- const now = Date.now();
712
- if (now > env.e) {
713
- void this.safeDelete(storeKey);
714
- this.emitDebug({
715
- op: "getResponse",
716
- key,
717
- outcome: "expired",
718
- staleAt: env.s,
719
- expiresAt: env.e,
720
- readMs,
721
- });
722
- return null;
723
- }
724
- if (this.maskedMiss("getResponse", key, env, readMs)) return null;
725
-
726
- // Reconstruct the Response BEFORE claiming revalidation. A corrupt body
727
- // (e.g. invalid base64 from base64ToBuffer, or bad header entries) would
728
- // otherwise throw out of getResponse — breaking the fail-open contract — and
729
- // a stale read would have written a lock for an entry it then evicts. Treat a
730
- // reconstruction failure as a corrupt entry: report, evict, and miss.
731
- let response: Response;
732
- try {
733
- response = new Response(base64ToBuffer(env.b), {
734
- status: env.st,
735
- headers: new Headers(env.hd),
736
- });
737
- } catch (error) {
738
- reportCacheError(
739
- error,
740
- "cache-corrupt",
741
- "[VercelCacheStore] getResponse",
742
- );
743
- void this.safeDelete(storeKey);
744
- this.emitDebug({ op: "getResponse", key, outcome: "corrupt", readMs });
745
- return null;
746
- }
747
-
748
- const isStale = env.s > 0 && now > env.s;
749
- const shouldRevalidate = isStale
750
- ? await this.claimRevalidation(
751
- storeKey,
752
- env.e,
753
- "[VercelCacheStore] getResponse",
754
- )
755
- : false;
756
- this.emitDebug({
757
- op: "getResponse",
758
- key,
759
- outcome: shouldRevalidate ? "stale-revalidate" : "fresh",
760
- shouldRevalidate,
761
- staleAt: env.s,
762
- expiresAt: env.e,
763
- readMs,
764
- });
765
- return { response, shouldRevalidate };
779
+ return this.readRecord(key, RESPONSE_READ);
766
780
  }
767
781
 
768
782
  async putResponse(
@@ -813,73 +827,8 @@ export class VercelCacheStore<
813
827
 
814
828
  // --- Item family ("use cache" - getItem/setItem) ---
815
829
 
816
- async getItem(key: string): Promise<CacheItemResult | null> {
817
- const storeKey = this.toStoreKey(key, "i");
818
- const started = Date.now();
819
- let raw: unknown;
820
- try {
821
- raw = await this.cache.get(storeKey);
822
- } catch (error) {
823
- reportCacheError(error, "cache-read", "[VercelCacheStore] getItem");
824
- this.emitDebug({ op: "getItem", key, outcome: "error" });
825
- return null;
826
- }
827
- const readMs = Date.now() - started;
828
-
829
- if (raw == null) {
830
- this.emitDebug({ op: "getItem", key, outcome: "miss", readMs });
831
- return null;
832
- }
833
- const env = this.asItemEnvelope(this.decodeRaw(raw));
834
- if (!env) {
835
- reportCacheError(
836
- new Error("malformed item envelope"),
837
- "cache-corrupt",
838
- "[VercelCacheStore] getItem",
839
- );
840
- void this.safeDelete(storeKey);
841
- this.emitDebug({ op: "getItem", key, outcome: "corrupt", readMs });
842
- return null;
843
- }
844
-
845
- const now = Date.now();
846
- if (now > env.e) {
847
- void this.safeDelete(storeKey);
848
- this.emitDebug({
849
- op: "getItem",
850
- key,
851
- outcome: "expired",
852
- staleAt: env.s,
853
- expiresAt: env.e,
854
- readMs,
855
- });
856
- return null;
857
- }
858
- if (this.maskedMiss("getItem", key, env, readMs)) return null;
859
-
860
- const isStale = env.s > 0 && now > env.s;
861
- const shouldRevalidate = isStale
862
- ? await this.claimRevalidation(
863
- storeKey,
864
- env.e,
865
- "[VercelCacheStore] getItem",
866
- )
867
- : false;
868
- this.emitDebug({
869
- op: "getItem",
870
- key,
871
- outcome: shouldRevalidate ? "stale-revalidate" : "fresh",
872
- shouldRevalidate,
873
- staleAt: env.s,
874
- expiresAt: env.e,
875
- readMs,
876
- });
877
- return {
878
- value: env.v,
879
- handles: env.h,
880
- shouldRevalidate,
881
- tags: env.t,
882
- };
830
+ getItem(key: string): Promise<CacheItemResult | null> {
831
+ return this.readRecord(key, ITEM_READ);
883
832
  }
884
833
 
885
834
  async setItem(
@@ -1071,7 +1020,7 @@ export class VercelCacheStore<
1071
1020
  this.emitDebug({ op: "getShell", key, outcome: "miss", readMs });
1072
1021
  return null;
1073
1022
  }
1074
- const env = this.asShellEnvelope(this.decodeRaw(raw));
1023
+ const env = asShellEnvelope(this.decodeRaw(raw));
1075
1024
  if (stats) {
1076
1025
  const readAt = performance.now();
1077
1026
  stats.matchMs = readAt - readStartedAt;
@@ -1479,20 +1428,88 @@ export class VercelCacheStore<
1479
1428
  return maskedForRequest(_getRequestContext(), this, tags, taggedAt ?? 0);
1480
1429
  }
1481
1430
 
1482
- /** A data read's mask check: reports the miss and returns true when masked. */
1483
- private maskedMiss(
1484
- op: "get" | "getItem" | "getResponse",
1485
- key: string,
1486
- env: { t?: string[]; ta?: number },
1487
- readMs: number,
1488
- ): boolean {
1489
- if (!this.maskedForRequest(env.t, env.ta)) return false;
1490
- this.emitDebug({ op, key, outcome: "miss", readMs });
1491
- return true;
1492
- }
1493
-
1494
1431
  // --- Internals ---
1495
1432
 
1433
+ /**
1434
+ * The read of a data family (get, getItem, getResponse): a failed read is
1435
+ * the family's `readError`; a missing, corrupt, expired or masked entry is a
1436
+ * miss, and a corrupt or expired one is evicted. The hit is built before the
1437
+ * stale claim, so an entry that does not rebuild is evicted as corrupt with
1438
+ * no revalidation lock written for it.
1439
+ */
1440
+ private async readRecord<
1441
+ E extends { s: number; e: number; ta?: number },
1442
+ V extends { shouldRevalidate: boolean },
1443
+ Err extends CacheReadError | null,
1444
+ >(key: string, f: VercelReadFamily<E, V, Err>): Promise<V | null | Err> {
1445
+ const { op } = f;
1446
+ const storeKey = this.toStoreKey(key, f.family);
1447
+ const label = `[VercelCacheStore] ${op}`;
1448
+ const started = Date.now();
1449
+ let raw: unknown;
1450
+ try {
1451
+ raw = await this.cache.get(storeKey);
1452
+ } catch (error) {
1453
+ reportCacheError(error, "cache-read", label);
1454
+ this.emitDebug({ op, key, outcome: "error" });
1455
+ return f.readError;
1456
+ }
1457
+ const readMs = Date.now() - started;
1458
+
1459
+ if (raw == null) {
1460
+ this.emitDebug({ op, key, outcome: "miss", readMs });
1461
+ return null;
1462
+ }
1463
+ const corrupt = (error: unknown): null => {
1464
+ reportCacheError(error, "cache-corrupt", label);
1465
+ void this.safeDelete(storeKey);
1466
+ this.emitDebug({ op, key, outcome: "corrupt", readMs });
1467
+ return null;
1468
+ };
1469
+ const env = f.decode(this.decodeRaw(raw));
1470
+ if (!env) return corrupt(new Error(`malformed ${f.name} envelope`));
1471
+
1472
+ const now = Date.now();
1473
+ if (now > env.e) {
1474
+ void this.safeDelete(storeKey);
1475
+ this.emitDebug({
1476
+ op,
1477
+ key,
1478
+ outcome: "expired",
1479
+ staleAt: env.s,
1480
+ expiresAt: env.e,
1481
+ readMs,
1482
+ });
1483
+ return null;
1484
+ }
1485
+ if (this.maskedForRequest(f.tags(env), env.ta)) {
1486
+ this.emitDebug({ op, key, outcome: "miss", readMs });
1487
+ return null;
1488
+ }
1489
+ let hit: V;
1490
+ try {
1491
+ hit = f.hit(env);
1492
+ } catch (error) {
1493
+ return corrupt(error);
1494
+ }
1495
+
1496
+ const isStale = env.s > 0 && now > env.s;
1497
+ const shouldRevalidate = isStale
1498
+ ? await this.claimRevalidation(storeKey, env.e, label)
1499
+ : false;
1500
+ this.emitDebug({
1501
+ op,
1502
+ key,
1503
+ outcome: shouldRevalidate ? "stale-revalidate" : "fresh",
1504
+ shouldRevalidate,
1505
+ staleAt: env.s,
1506
+ expiresAt: env.e,
1507
+ readMs,
1508
+ });
1509
+ hit.shouldRevalidate = shouldRevalidate;
1510
+ return hit;
1511
+ }
1512
+
1496
1513
  // The `rg:{family}:` prefix keeps the three value tiers in disjoint
1497
1514
  // keyspaces, but the isolation is only as strong as the cache's
1498
1515
  // keyHashFunction: djb2 (getCache's default) folds this to 32 bits, so at a
@@ -1695,88 +1712,4 @@ export class VercelCacheStore<
1695
1712
  // The debug sink must never break the cache path.
1696
1713
  }
1697
1714
  }
1698
-
1699
- private asSegmentEnvelope(raw: unknown): VercelSegmentEnvelope | null {
1700
- if (!isRecord(raw)) return null;
1701
- const { d, s, e, ta } = raw;
1702
- if (
1703
- !isRecord(d) ||
1704
- !Array.isArray((d as Record<string, unknown>).segments)
1705
- ) {
1706
- return null;
1707
- }
1708
- if (typeof s !== "number" || typeof e !== "number") return null;
1709
- return {
1710
- d: d as unknown as CachedEntryData,
1711
- s,
1712
- e,
1713
- ta: typeof ta === "number" ? ta : undefined,
1714
- };
1715
- }
1716
-
1717
- private asItemEnvelope(raw: unknown): VercelItemEnvelope | null {
1718
- if (!isRecord(raw)) return null;
1719
- const { v, h, s, e, t, ta } = raw;
1720
- if (typeof v !== "string") return null;
1721
- if (typeof s !== "number" || typeof e !== "number") return null;
1722
- return {
1723
- v,
1724
- h: typeof h === "string" ? h : undefined,
1725
- s,
1726
- e,
1727
- ta: typeof ta === "number" ? ta : undefined,
1728
- t: Array.isArray(t) ? (t as string[]) : undefined,
1729
- };
1730
- }
1731
-
1732
- private asShellEnvelope(raw: unknown): VercelShellEnvelope | null {
1733
- if (!isRecord(raw)) return null;
1734
- const { p, po, rv, bv, c, s, e, t, i, lp, sn, dk, pr, no } = raw;
1735
- if (typeof rv !== "string" || typeof bv !== "string") return null;
1736
- // Document half: required unless navigationOnly (`no`), which stores
1737
- // neither field. Tolerate a legacy navigationOnly envelope that still
1738
- // carries them.
1739
- if (p === undefined ? no !== true : typeof p !== "string") return null;
1740
- if (
1741
- po === undefined ? no !== true : po !== null && typeof po !== "string"
1742
- ) {
1743
- return null;
1744
- }
1745
- if (typeof c !== "number") return null;
1746
- if (typeof s !== "number" || typeof e !== "number") return null;
1747
- return {
1748
- ...(typeof p === "string" ? { p } : {}),
1749
- ...(po !== undefined ? { po: po as string | null } : {}),
1750
- rv,
1751
- bv,
1752
- c,
1753
- s,
1754
- e,
1755
- t: Array.isArray(t) ? (t as string[]) : undefined,
1756
- i: typeof i === "string" ? i : undefined,
1757
- lp: typeof lp === "string" ? lp : undefined,
1758
- // A non-array field reads as no records.
1759
- sn: Array.isArray(sn) ? (sn as ShellSnapshotRecord[]) : [],
1760
- dk: typeof dk === "string" ? dk : undefined,
1761
- pr: typeof pr === "string" ? pr : undefined,
1762
- no: no === true ? true : undefined,
1763
- };
1764
- }
1765
-
1766
- private asResponseEnvelope(raw: unknown): VercelResponseEnvelope | null {
1767
- if (!isRecord(raw)) return null;
1768
- const { b, st, hd, s, e, t, ta } = raw;
1769
- if (typeof b !== "string" || typeof st !== "number") return null;
1770
- if (!Array.isArray(hd)) return null;
1771
- if (typeof s !== "number" || typeof e !== "number") return null;
1772
- return {
1773
- b,
1774
- st,
1775
- hd: hd as [string, string][],
1776
- s,
1777
- e,
1778
- t: Array.isArray(t) ? (t as string[]) : undefined,
1779
- ta: typeof ta === "number" ? ta : undefined,
1780
- };
1781
- }
1782
1715
  }
@@ -7,7 +7,7 @@ import type { HandleData } from "../server/handle-store.js";
7
7
  *
8
8
  * A segment's handle bucket holds its handler's pushes and those of the
9
9
  * loaders registered on it: a loader pushes under its owning segment's id
10
- * (fresh.ts, revalidation.ts pin `_currentSegmentId`). A navigation or an
10
+ * (revalidation.ts pins `_currentSegmentId`). A navigation or an
11
11
  * action response decides each of them on its own: the handler re-runs and a
12
12
  * loader whose revalidate() answers false keeps its data on the client. The
13
13
  * response carries only what re-ran, so replacing the whole bucket with it
@@ -1,25 +1,14 @@
1
1
  /**
2
- * The wire markers of prefetches, of `prefetch: false`
3
- * (docs/design/prefetch-false.md) and of stale requests: shared by the browser
4
- * and the server (router/segment-resolution/prefetch-deferral.ts requestKind,
5
- * rsc/prefetch-render-error.ts, rsc/rsc-rendering.ts,
6
- * cache/document-cache.ts, router/navigation-snapshot.ts). Dependency free,
7
- * like segment-fragments.ts: the browser must not import the server modules
8
- * that use them.
2
+ * The wire markers of prefetches: shared by the browser and the server
3
+ * (router/segment-resolution/prefetch-deferral.ts requestKind,
4
+ * rsc/prefetch-render-error.ts, rsc/rsc-rendering.ts). Dependency free, like
5
+ * segment-fragments.ts: the browser must not import the server modules that
6
+ * use them.
9
7
  */
10
8
 
11
9
  /** Request header of a `<Link>` prefetch (browser/prefetch/fetch.ts). */
12
10
  export const PREFETCH_HEADER = "X-Rango-Prefetch";
13
11
 
14
- /** Query param of a fill request (browser/navigation-client.ts). */
15
- export const FILL_PARAM = "_rsc_fill";
16
-
17
- /**
18
- * Query param of a stale request (browser/navigation-client.ts): `revalidate()`
19
- * sees `stale: true` (router/navigation-snapshot.ts).
20
- */
21
- export const STALE_PARAM = "_rsc_stale";
22
-
23
12
  /**
24
13
  * The last row of a prefetch whose render reported an error
25
14
  * (rsc/prefetch-render-error.ts): a Flight hint with a code React does not