@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
@@ -22,7 +22,8 @@ import {
22
22
  } from "../deps/rsc-client.js";
23
23
  import {
24
24
  getRequestContext,
25
- runWithRequestContext,
25
+ runAsBackgroundWork,
26
+ type RequestContext,
26
27
  } from "../server/request-context.js";
27
28
  import { isUnderTestRunner } from "../runtime-env.js";
28
29
  import {
@@ -34,7 +35,12 @@ import {
34
35
  } from "./taint.js";
35
36
 
36
37
  export { isCachedFunction };
37
- import { serializeResult, deserializeResult } from "./segment-codec.js";
38
+ import {
39
+ serializeResult,
40
+ deserializeResult,
41
+ isUnsupportedValueError,
42
+ teeResult,
43
+ } from "./segment-codec.js";
38
44
  import {
39
45
  appendHandles,
40
46
  encodeHandles,
@@ -384,6 +390,23 @@ function refreshView(arg: unknown): unknown {
384
390
  });
385
391
  }
386
392
 
393
+ /**
394
+ * Run `fn` inside a "use cache" execution's scopes: its exec scope
395
+ * (cookies()/headers() reads and ctx writes refuse) and its tag scope
396
+ * (cacheTag() and nested calls tag the entry, #980), `recorded` when given.
397
+ * The body runs here, and so does the value's encode: it renders the server
398
+ * components in the value, which are part of the entry. Scar: the encode ran
399
+ * in the tag scope only, so a returned component's cookies() read went
400
+ * through and the entry served the first visitor's value to the next.
401
+ */
402
+ function inExecution<T>(
403
+ scope: CacheExecScope,
404
+ fn: () => T,
405
+ recorded?: Set<string>,
406
+ ): { result: T; tags: Set<string> } {
407
+ return runWithCacheTagScope(() => runWithCacheExecScope(fn, scope), recorded);
408
+ }
409
+
387
410
  // ============================================================================
388
411
  // Core: registerCachedFunction
389
412
  // ============================================================================
@@ -459,14 +482,6 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
459
482
  const entryTags = (recorded: Set<string>): string[] => [
460
483
  ...new Set(normalizeTags([...(profile.tags ?? []), ...recorded])),
461
484
  ];
462
- // The value's Flight encode runs back inside its execution's tag scope:
463
- // it renders the server components in the value, and a "use cache" call
464
- // or cacheTag() among them belongs to this entry (#980).
465
- const inTagScope = <T>(
466
- recorded: Set<string>,
467
- encode: () => Promise<T>,
468
- ): Promise<T> => runWithCacheTagScope(encode, recorded).result;
469
-
470
485
  // Separate request-scoped args (ctx, a Request, the request's env) from
471
486
  // key-generating args. For those that carry route context (ctx params,
472
487
  // pathname, searchParams; a Request's URL), extract serializable values
@@ -712,30 +727,29 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
712
727
  // user's value into the shared entry — with zero visibility to the
713
728
  // concurrent foreground.
714
729
 
715
- try {
716
- // Re-establish the request-context ALS so a "use cache" body that
717
- // reads the ambient getRequestContext() (e.g.
718
- // getRequestContext().env.ApiKey) resolves during the background
719
- // revalidation instead of throwing "called outside of a request
720
- // context". runWithRequestContext sets the store for fn's
721
- // synchronous kickoff; its async continuations inherit it.
722
- //
723
- // The span opens inside the request ALS and wraps the WHOLE task —
724
- // fn kickoff through the store write — so its platform spans nest
725
- // under rango.background and a slow or failing setItem is part of
726
- // the traced revalidation, not an untraced tail.
727
- await runWithRequestContext(requestCtx, () =>
728
- observePhase(
730
+ // Re-establish the request-context ALS so a "use cache" body that
731
+ // reads the ambient getRequestContext() (e.g.
732
+ // getRequestContext().env.ApiKey) resolves during the background
733
+ // revalidation instead of throwing "called outside of a request
734
+ // context". runAsBackgroundWork sets the store for fn's synchronous
735
+ // kickoff; its async continuations inherit it. No visitor made the
736
+ // invalidations it runs (no fresh-reads cookie), and onError marks
737
+ // its errors `background`, the failure caught below included.
738
+ //
739
+ // The span opens inside the request ALS and wraps the WHOLE task —
740
+ // fn kickoff through the store write — so its platform spans nest
741
+ // under rango.background and a slow or failing setItem is part of
742
+ // the traced revalidation, not an untraced tail.
743
+ await runAsBackgroundWork(requestCtx, async () => {
744
+ try {
745
+ await observePhase(
729
746
  PHASES.background("use-cache-revalidation"),
730
747
  async () => {
731
748
  const refreshArgs = args.map(refreshView);
732
749
  // The refresh reads its data from here on (#973, #977).
733
750
  const start = executionStart();
734
- const scoped = runWithCacheTagScope(() =>
735
- runWithCacheExecScope(
736
- () => fn.apply(this, refreshArgs),
737
- bgScope,
738
- ),
751
+ const scoped = inExecution(bgScope, () =>
752
+ fn.apply(this, refreshArgs),
739
753
  );
740
754
  const freshResult = await scoped.result;
741
755
  bgCapture?.stop();
@@ -744,17 +758,22 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
744
758
  const onFlightError = (error: unknown): void => {
745
759
  flightErrors.push(error);
746
760
  };
747
- const serialized = await inTagScope(scoped.tags, () =>
748
- serializeResult(freshResult, onFlightError),
749
- );
761
+ const serialized = await inExecution(
762
+ bgScope,
763
+ () => serializeResult(freshResult, onFlightError),
764
+ scoped.tags,
765
+ ).result;
750
766
  if (serialized !== null) {
751
767
  const encodedHandles = bgCapture
752
- ? await inTagScope(scoped.tags, () =>
753
- encodeHandles(
754
- bgCapture.capture.data,
755
- onFlightError,
756
- ),
757
- )
768
+ ? await inExecution(
769
+ bgScope,
770
+ () =>
771
+ encodeHandles(
772
+ bgCapture.capture.data,
773
+ onFlightError,
774
+ ),
775
+ scoped.tags,
776
+ ).result
758
777
  : undefined;
759
778
  // An error row would replace the stale entry; the catch
760
779
  // below reports it and the stale entry keeps serving.
@@ -777,20 +796,17 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
777
796
  noteWarmWrite(requestCtx, "item");
778
797
  }
779
798
  },
780
- ),
781
- );
782
- } catch (bgError) {
783
- bgCapture?.stop();
784
- // Pass requestCtx explicitly: this runs in a detached background
785
- // task where the ALS context is gone, so onError can only fire if
786
- // we hand it the context captured up front.
787
- reportCacheError(
788
- bgError,
789
- "stale-revalidation",
790
- "[use cache] background revalidation failed",
791
- requestCtx,
792
- );
793
- }
799
+ );
800
+ } catch (bgError) {
801
+ bgCapture?.stop();
802
+ reportCacheError(
803
+ bgError,
804
+ "stale-revalidation",
805
+ "[use cache] background revalidation failed",
806
+ requestCtx,
807
+ );
808
+ }
809
+ });
794
810
  }),
795
811
  );
796
812
  return result;
@@ -921,21 +937,24 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
921
937
  // deliberately NOT stamped: a property on that SHARED object made every
922
938
  // parallel read on the request throw for the cached body's whole
923
939
  // execution window (a 2s cached fetch poisoned a sibling loader's
924
- // cookies() — same hazard class as issue #684, plan 010).
940
+ // cookies() — same hazard class as issue #684, plan 010). Not as an
941
+ // argument either (getRequestContext(), any pass: a derived pass inherits
942
+ // `_requestRoot`): a derived pass's writers that it does not route to a
943
+ // sink check the request's own context (server/render-pass.ts), so the
944
+ // stamp failed a shell capture's or a background re-render's writes while
945
+ // a foreground body held it.
925
946
  const taintedArgs: unknown[] = [];
926
947
  for (const arg of args) {
927
- if (isTainted(arg)) {
948
+ if (isTainted(arg) && !(arg as RequestContext)._requestRoot) {
928
949
  stampCacheExec(arg as object);
929
950
  taintedArgs.push(arg);
930
951
  }
931
952
  }
932
953
 
933
954
  let result: any;
934
- let scoped: ReturnType<typeof runWithCacheTagScope>;
955
+ let scoped: ReturnType<typeof inExecution>;
935
956
  try {
936
- scoped = runWithCacheTagScope(() =>
937
- runWithCacheExecScope(() => fn.apply(this, args), execScope),
938
- );
957
+ scoped = inExecution(execScope, () => fn.apply(this, args));
939
958
  result = await scoped.result;
940
959
  } catch (execError) {
941
960
  // The function threw: drop the in-flight entry and reject any waiters so
@@ -957,25 +976,39 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
957
976
  const recorded = scoped!.tags;
958
977
  recordRequestTags(entryTags(recorded), requestCtx);
959
978
 
960
- // Serialize + encode handles ONCE, resolve the in-flight envelope so any
961
- // concurrent followers can serve a synthetic hit, then persist to the store.
962
- // Fully non-blocking when waitUntil is available — the leader's response
963
- // waits on neither serialization nor the store write.
979
+ // Encode the value ONCE, inside the execution's scopes, and hand the
980
+ // caller a decoded copy of it, as a hit does. The caller rendering
981
+ // `result` itself would share its lazy state (a React.lazy, a memoizing
982
+ // thenable) with the encode: whichever render reached it first decided
983
+ // its value for both, so a read the caller's render made outside the
984
+ // scope was stored for the next visitor. Then resolve the in-flight
985
+ // envelope so concurrent followers serve a synthetic hit, and persist.
986
+ // When waitUntil is available the leader waits for the copy's root row
987
+ // only, not for the rest of the encode or the store write.
988
+ const flightErrors: unknown[] = [];
989
+ const onFlightError = (error: unknown): void => {
990
+ flightErrors.push(error);
991
+ };
992
+ const encoded = inExecution(
993
+ execScope,
994
+ () => teeResult(result, onFlightError),
995
+ recorded,
996
+ ).result;
964
997
  const finalizeAndWrite = async (): Promise<void> => {
965
998
  let serialized: string | null;
966
999
  let encodedHandles: string | undefined;
967
- const flightErrors: unknown[] = [];
968
- const onFlightError = (error: unknown): void => {
969
- flightErrors.push(error);
970
- };
971
1000
  try {
972
- serialized = await inTagScope(recorded, () =>
973
- serializeResult(result, onFlightError),
974
- );
1001
+ serialized = encoded ? await encoded.serialized : null;
1002
+ // The returned tree renders only in the encode, so its cacheTag()
1003
+ // calls tag the entry alone: record the entry's tags onto the
1004
+ // request, as a hit does.
1005
+ recordRequestTags(entryTags(recorded), requestCtx);
975
1006
  encodedHandles = capture
976
- ? await inTagScope(recorded, () =>
977
- encodeHandles(capture.capture.data, onFlightError),
978
- )
1007
+ ? await inExecution(
1008
+ execScope,
1009
+ () => encodeHandles(capture.capture.data, onFlightError),
1010
+ recorded,
1011
+ ).result
979
1012
  : undefined;
980
1013
  // Flight encodes an async component that throws, or a rejected
981
1014
  // promise in the result or a handle value, as an error row and
@@ -986,10 +1019,17 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
986
1019
  assertLoaderReadsClean(execScope);
987
1020
  } catch (buildError) {
988
1021
  // Serialize/handle-encode failed: no envelope for followers (they run
989
- // fresh) and nothing to write.
1022
+ // fresh) and nothing to write. Logged too: a read the guard refused
1023
+ // in the value is otherwise silent with no onError. An error the call
1024
+ // also throws reaches onError once (the request dedups by identity).
990
1025
  clearSelf();
991
1026
  rejectEnvelope(buildError);
992
- requestCtx?._reportBackgroundError?.(buildError, "cache-write");
1027
+ reportCacheError(
1028
+ buildError,
1029
+ "cache-write",
1030
+ `[use cache] "${id}" write`,
1031
+ requestCtx,
1032
+ );
993
1033
  return;
994
1034
  }
995
1035
  if (serialized === null) {
@@ -1039,7 +1079,24 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
1039
1079
 
1040
1080
  await runBackground(requestCtx, finalizeAndWrite, true);
1041
1081
 
1042
- return result;
1082
+ // The encode could not start: nothing is stored, the value renders live.
1083
+ if (!encoded) return result;
1084
+ try {
1085
+ return await encoded.decoded;
1086
+ } catch (decodeError) {
1087
+ // The root row failed. A value Flight cannot encode (a function, a
1088
+ // class instance) is not cached and renders live, as it always has;
1089
+ // anything else, a refused read above all, fails the call as the same
1090
+ // read in the body does, with the encode's own error (production
1091
+ // Flight redacts the decoded one).
1092
+ if (
1093
+ flightErrors.length > 0 &&
1094
+ flightErrors.every(isUnsupportedValueError)
1095
+ ) {
1096
+ return result;
1097
+ }
1098
+ throw flightErrors[0] ?? decodeError;
1099
+ }
1043
1100
  };
1044
1101
 
1045
1102
  // Brand the wrapper so it can be detected at runtime (e.g., to prevent
@@ -33,8 +33,9 @@ import {
33
33
  // pulls @vitejs/plugin-rsc (a virtual: module the plain node/vitest runner cannot
34
34
  // resolve). It is imported LAZILY at the two call sites below (deserializeSegments
35
35
  // in lookupRoute, serializeSegments in cacheRoute) so that requiring cache-scope —
36
- // e.g. dispatch's lazy `import("../cache/cache-scope.js")` for the response-route
37
- // cache path — does not crash a consumer test that never mocks plugin-rsc. Behavior
36
+ // e.g. through the request stages dispatch loads for a response route
37
+ // (rsc/request-stages.ts) — does not crash a consumer test that never mocks
38
+ // plugin-rsc. Behavior
38
39
  // is unchanged: both methods are async and already awaited the codec.
39
40
  import {
40
41
  captureHandles,
@@ -54,14 +55,16 @@ import {
54
55
  import type { RequestContext } from "../server/request-context.js";
55
56
  import { runIdentityExempt } from "./cache-exec-scope.js";
56
57
  import { isWarmReplace, noteWarmWrite } from "../prerender/warm-request.js";
58
+ import { WIRE } from "../wire-params.js";
57
59
 
58
60
  /**
59
- * Narrow the request's route-record window (RequestContext._routeRecordWindow)
60
- * to a record this request read or wrote: fresh until `freshUntil` (the
61
- * record's `expiresAt`), then stale for `swr` seconds. A shell capture starts
62
- * with no window of its own (shell-capture.ts deriveShellCaptureContext sets
63
- * it undefined), so it narrows only to the records the capture itself read
64
- * or wrote.
61
+ * Narrow a shell capture's route-record window
62
+ * (ShellCaptureState.routeRecordWindow) to a record the capture read or
63
+ * wrote: fresh until `freshUntil` (the record's `expiresAt`), then stale for
64
+ * `swr` seconds. A capture starts with no window of its own
65
+ * (RENDER_PASS_FIELDS["shell-capture"] creates it undefined), so it narrows
66
+ * only to the records the capture itself read or wrote. Only a capture reads
67
+ * the window (shell-capture.ts capShellWindow): another render notes nothing.
65
68
  */
66
69
  function noteRouteRecordWindow(
67
70
  requestCtx: RequestContext,
@@ -70,6 +73,8 @@ function noteRouteRecordWindow(
70
73
  swr: number,
71
74
  written: boolean,
72
75
  ): void {
76
+ const captureState = requestCtx._shellCaptureState;
77
+ if (!captureState) return;
73
78
  const next = {
74
79
  freshUntil,
75
80
  staleUntil: freshUntil + swr * 1000,
@@ -77,9 +82,9 @@ function noteRouteRecordWindow(
77
82
  swr,
78
83
  written,
79
84
  };
80
- const current = requestCtx._routeRecordWindow;
85
+ const current = captureState.routeRecordWindow;
81
86
  // The record that runs out first names the config; freshness is the least.
82
- requestCtx._routeRecordWindow = current
87
+ captureState.routeRecordWindow = current
83
88
  ? {
84
89
  ...(current.staleUntil <= next.staleUntil ? current : next),
85
90
  freshUntil: Math.min(current.freshUntil, freshUntil),
@@ -123,23 +128,36 @@ function validatedSwr(value: number | undefined): number | undefined {
123
128
  return isValidCacheSeconds(value, "swr") ? value : undefined;
124
129
  }
125
130
 
131
+ /**
132
+ * The request kind a route record answers: `doc` (a document request),
133
+ * `partial` (a navigation) or `intercept:<intercept>`, where `intercept` is
134
+ * the intercept an intercept navigation selected (interceptRecordKey,
135
+ * router/intercept-resolution.ts). The kinds' records hold different
136
+ * segments (an intercept's holds its slot and no route page), so every record
137
+ * key names its kind, a `key()`'s included (CacheScope.resolveKeyFrom).
138
+ * Keyed by the URL alone, a record written under one intercept answered a
139
+ * navigation that selected another; keyed by a `key()` alone, a document GET
140
+ * rendered an intercept's record and dropped the page.
141
+ */
142
+ function routeRecordKind(intercept?: string, prefixOverride?: "doc"): string {
143
+ if (intercept !== undefined) return `intercept:${intercept}`;
144
+ if (prefixOverride !== undefined) return prefixOverride;
145
+ const isPartial =
146
+ getRequestContext()?.originalUrl?.searchParams.has(WIRE.partial) ?? false;
147
+ return isPartial ? "partial" : "doc";
148
+ }
149
+
150
+ /** `{kind}:` + the request's key base (routeRecordKind, requestKeyBase). */
126
151
  function getDefaultRouteCacheKey(
127
152
  pathname: string,
128
- params?: Record<string, string>,
129
- isIntercept?: boolean,
130
- prefixOverride?: "doc",
153
+ params: Record<string, string> | undefined,
154
+ kind: string,
131
155
  ): string {
132
156
  const ctx = getRequestContext();
133
- const isPartial = ctx?.originalUrl?.searchParams.has("_rsc_partial") ?? false;
134
157
  const searchParams = ctx?.url.searchParams;
135
158
  const host = ctx?.url.host ?? "localhost";
136
159
 
137
- // Intercept navigations get their own cache namespace
138
- const prefix = isIntercept
139
- ? "intercept"
140
- : (prefixOverride ?? (isPartial ? "partial" : "doc"));
141
-
142
- return `${prefix}:${requestKeyBase(ctx, host, pathname, searchParams, params, ctx?._searchParamsFilter)}`;
160
+ return `${kind}:${requestKeyBase(ctx, host, pathname, searchParams, params, ctx?._searchParamsFilter)}`;
143
161
  }
144
162
 
145
163
  // ============================================================================
@@ -236,15 +254,23 @@ function memoizedKey(
236
254
  return key;
237
255
  }
238
256
 
257
+ /**
258
+ * A route record that answered. `restoreHandles` is set when the lookup left
259
+ * the handle restore to its caller (lookupRouteDetailed `deferHandles`),
260
+ * which names the segments it sends (handle-snapshot.ts restoreHandles).
261
+ */
262
+ export interface CachedRouteHit {
263
+ segments: ResolvedSegment[];
264
+ shouldRevalidate: boolean;
265
+ restoreHandles?: (sentIds: ReadonlySet<string>) => Promise<void>;
266
+ }
267
+
239
268
  /**
240
269
  * Discriminated outcome of a route cache lookup — see
241
270
  * {@link CacheScope.lookupRouteDetailed} for what each status licenses.
242
271
  */
243
272
  export type CacheRouteLookupOutcome =
244
- | {
245
- status: "hit";
246
- result: { segments: ResolvedSegment[]; shouldRevalidate: boolean };
247
- }
273
+ | { status: "hit"; result: CachedRouteHit }
248
274
  | { status: "miss" | "bypass" | "error" };
249
275
 
250
276
  /**
@@ -455,15 +481,13 @@ export class CacheScope {
455
481
  private resolveKey(
456
482
  pathname: string,
457
483
  params: Record<string, string>,
458
- isIntercept?: boolean,
484
+ intercept?: string,
459
485
  ): Promise<string> {
486
+ const kind = routeRecordKind(intercept, this.defaultKeyPrefix);
460
487
  return this.resolveKeyFrom(
461
- getDefaultRouteCacheKey(
462
- pathname,
463
- params,
464
- isIntercept,
465
- this.defaultKeyPrefix,
466
- ),
488
+ getDefaultRouteCacheKey(pathname, params, kind),
489
+ "",
490
+ kind,
467
491
  );
468
492
  }
469
493
 
@@ -500,14 +524,18 @@ export class CacheScope {
500
524
  * enclosing partition (#970, #974). A scope whose own config sets no
501
525
  * `key()` appends its own default key (resolveDefaultKey). Reusing the
502
526
  * outer `key()` result alone would give every route under the inner
503
- * cache() one record: a `key()` replaces the whole default key, so an
504
- * outer key that names no route (`tier:gold`) would make `/a` HIT `/b`'s
505
- * record. A scope with its own `key()` adds no default key: a `key()` is a
506
- * full override, and its store's keyGenerator does not run. With no
507
- * partition part it is resolveDefaultKey alone. With a `key()` on the
508
- * chain but no request context it rejects (keyWithoutContextError).
509
- * `partitionedPrefix` prefixes a key built from partition parts only (the
510
- * response family's `response:`).
527
+ * cache() one record: a `key()` replaces the default key's URL part, so
528
+ * an outer key that names no route (`tier:gold`) would make `/a` HIT
529
+ * `/b`'s record. A scope with its own `key()` adds no default key: its `key()`
530
+ * replaces the URL part, and its store's keyGenerator does not run. It
531
+ * appends `kind` instead (routeRecordKind): the `key()` decides which
532
+ * requests of one kind share a record, never that a document, a
533
+ * navigation and an intercept navigation do. With no partition part it is
534
+ * resolveDefaultKey alone. With a `key()` on the chain but no request
535
+ * context it rejects (keyWithoutContextError). `partitionedPrefix`
536
+ * prefixes a key built from partition parts only (the response family's
537
+ * `response:`, which passes no `kind`: a response route answers every
538
+ * request alike).
511
539
  *
512
540
  * Each `key()` result lives on the request context (`_resolvedCacheKeys`),
513
541
  * by function: it runs once per request whatever the default key
@@ -518,7 +546,11 @@ export class CacheScope {
518
546
  * itself: a `key()` calling `cookies()` would trip the capture guard there,
519
547
  * and a capture must store the partition its request resolved.
520
548
  */
521
- resolveKeyFrom(defaultKey: string, partitionedPrefix = ""): Promise<string> {
549
+ resolveKeyFrom(
550
+ defaultKey: string,
551
+ partitionedPrefix = "",
552
+ kind?: string,
553
+ ): Promise<string> {
522
554
  if (this.fixedKey !== undefined) return Promise.resolve(this.fixedKey);
523
555
  const requestCtx = _getRequestContext();
524
556
  if (!requestCtx || !this.mayPartition) {
@@ -534,7 +566,8 @@ export class CacheScope {
534
566
  ]).then(([[keys, storeParts], ownDefault]) => {
535
567
  const generated = positionalParts(storeParts);
536
568
  if (keys.length === 0 && generated.length === 0) return ownDefault!;
537
- if (ownDefault !== undefined) generated.push(ownDefault);
569
+ const last = ownDefault ?? kind;
570
+ if (last !== undefined) generated.push(last);
538
571
  return partitionedPrefix + composeCacheKeys(keys, generated);
539
572
  });
540
573
  }
@@ -558,7 +591,7 @@ export class CacheScope {
558
591
  ? undefined
559
592
  : this.getStore()?.keyGenerator;
560
593
  if (!this.mayPartition && !keyGenerator) return undefined;
561
- const defaultKey = getDefaultRouteCacheKey(pathname, params, false, "doc");
594
+ const defaultKey = getDefaultRouteCacheKey(pathname, params, "doc");
562
595
  const requestCtx = _getRequestContext();
563
596
  if (!requestCtx) {
564
597
  return this.keyFns.length > 0
@@ -735,24 +768,26 @@ export class CacheScope {
735
768
  *
736
769
  * @param pathname - URL pathname for cache key generation
737
770
  * @param params - Route params for cache key generation
738
- * @param isIntercept - Whether this is an intercept navigation (uses different cache key)
771
+ * @param intercept - The intercept an intercept navigation selected
772
+ * (interceptRecordKey): its record has its own key
739
773
  * @param ownedPushes - What the record is to each loader's handle pushes
740
774
  * (withCacheLookup: loader-cache.ts loaderPins), called on a hit
775
+ * @param deferHandles - Restore no handle value here: the caller calls the
776
+ * hit's `restoreHandles` once it decided which segments it sends
741
777
  */
742
778
  async lookupRoute(
743
779
  pathname: string,
744
780
  params: Record<string, string>,
745
- isIntercept?: boolean,
781
+ intercept?: string,
746
782
  ownedPushes?: () => OwnedPushDelivery,
747
- ): Promise<{
748
- segments: ResolvedSegment[];
749
- shouldRevalidate: boolean;
750
- } | null> {
783
+ deferHandles?: boolean,
784
+ ): Promise<CachedRouteHit | null> {
751
785
  const outcome = await this.lookupRouteDetailed(
752
786
  pathname,
753
787
  params,
754
- isIntercept,
788
+ intercept,
755
789
  ownedPushes,
790
+ deferHandles,
756
791
  );
757
792
  return outcome.status === "hit" ? outcome.result : null;
758
793
  }
@@ -778,8 +813,9 @@ export class CacheScope {
778
813
  async lookupRouteDetailed(
779
814
  pathname: string,
780
815
  params: Record<string, string>,
781
- isIntercept?: boolean,
816
+ intercept?: string,
782
817
  ownedPushes?: () => OwnedPushDelivery,
818
+ deferHandles?: boolean,
783
819
  ): Promise<CacheRouteLookupOutcome> {
784
820
  if (!this.enabled) return { status: "bypass" };
785
821
  if (!this.conditionAllows("read")) return { status: "bypass" };
@@ -801,7 +837,7 @@ export class CacheScope {
801
837
  // happens here, where a miss is a safe outcome.
802
838
  let key: string | undefined;
803
839
  try {
804
- key = await this.resolveKey(pathname, params, isIntercept);
840
+ key = await this.resolveKey(pathname, params, intercept);
805
841
 
806
842
  const result = await store.get(key);
807
843
 
@@ -882,17 +918,23 @@ export class CacheScope {
882
918
  await this.onHit?.();
883
919
 
884
920
  const handleStore = _getRequestContext()?._handleStore;
921
+ let restoreHandles: CachedRouteHit["restoreHandles"];
885
922
  // An empty handles string means the route pushed none (the common
886
923
  // case); a decode failure skips the restore but keeps the segments.
887
924
  if (handleStore) {
888
- await restoreRecordHandles(
889
- handleStore,
890
- cached,
891
- ownedPushes?.(),
925
+ const owned = ownedPushes?.();
926
+ const keepIds =
892
927
  this.boundary !== undefined
893
928
  ? new Set(segments.map((s) => s.id))
894
- : undefined,
895
- );
929
+ : undefined;
930
+ if (deferHandles) {
931
+ // The authority holds from here, as with the restore below.
932
+ if (owned) handleStore.setRecordAuthority(owned);
933
+ restoreHandles = (sent) =>
934
+ restoreRecordHandles(handleStore, cached, owned, keepIds, sent);
935
+ } else {
936
+ await restoreRecordHandles(handleStore, cached, owned, keepIds);
937
+ }
896
938
  }
897
939
 
898
940
  if (INTERNAL_RANGO_DEBUG) {
@@ -907,7 +949,10 @@ export class CacheScope {
907
949
  if (ambientContext)
908
950
  this.noteRecordWindow(ambientContext, cached.expiresAt, false);
909
951
 
910
- return { status: "hit", result: { segments, shouldRevalidate } };
952
+ return {
953
+ status: "hit",
954
+ result: { segments, shouldRevalidate, restoreHandles },
955
+ };
911
956
  } catch (error) {
912
957
  // Covers a store.get() failure AND a throwing consumer key()/keyGenerator
913
958
  // (resolveKey). Either way degrade to an uncached render — reported as
@@ -969,7 +1014,8 @@ export class CacheScope {
969
1014
  * @param pathname - URL pathname for cache key generation
970
1015
  * @param params - Route params for cache key generation
971
1016
  * @param segments - All resolved segments to cache
972
- * @param isIntercept - Whether this is an intercept navigation (uses different cache key)
1017
+ * @param intercept - The intercept an intercept navigation selected
1018
+ * (interceptRecordKey): its record has its own key
973
1019
  * @param start - Where the render of `segments` started: the record is not
974
1020
  * written when one of its tags was invalidated since (#977,
975
1021
  * predatesInvalidation). A shell capture's doc record passes none: it
@@ -979,7 +1025,7 @@ export class CacheScope {
979
1025
  pathname: string,
980
1026
  params: Record<string, string>,
981
1027
  segments: ResolvedSegment[],
982
- isIntercept?: boolean,
1028
+ intercept?: string,
983
1029
  start?: ExecutionStart,
984
1030
  ): Promise<void> {
985
1031
  if (!this.enabled || segments.length === 0) return;
@@ -1006,7 +1052,7 @@ export class CacheScope {
1006
1052
  this.noteRecordWindow(requestCtx, Date.now() + ttl * 1000, true);
1007
1053
 
1008
1054
  // Resolve cache key early (while request context is available)
1009
- const key = await this.resolveKey(pathname, params, isIntercept);
1055
+ const key = await this.resolveKey(pathname, params, intercept);
1010
1056
 
1011
1057
  // Doc-namespaced scopes (the shell implicit scope and the capture's
1012
1058
  // composed doc scope) publish the canonical document segment key so
@@ -1022,7 +1068,7 @@ export class CacheScope {
1022
1068
  recordRequestTags(tags, requestCtx);
1023
1069
 
1024
1070
  // Check if this is a partial request (navigation) vs document request
1025
- const isPartial = requestCtx.originalUrl.searchParams.has("_rsc_partial");
1071
+ const isPartial = requestCtx.originalUrl.searchParams.has(WIRE.partial);
1026
1072
 
1027
1073
  if (INTERNAL_RANGO_DEBUG) {
1028
1074
  debugCacheLog(
@@ -1204,7 +1250,7 @@ export function resolveShellPartition(
1204
1250
  ): Promise<string | null> | undefined {
1205
1251
  if (routeScope?.enabled) return routeScope.resolvePartition(pathname, params);
1206
1252
  if (!appStore?.keyGenerator) return undefined;
1207
- const defaultKey = getDefaultRouteCacheKey(pathname, params, false, "doc");
1253
+ const defaultKey = getDefaultRouteCacheKey(pathname, params, "doc");
1208
1254
  return resolveCacheKey(undefined, appStore, defaultKey).then((resolved) =>
1209
1255
  resolved === defaultKey ? null : resolved,
1210
1256
  );
@@ -1259,7 +1305,7 @@ export function createShellImplicitDocScope(
1259
1305
  * normal partial navigation replay), substitute an implicit doc-level scope
1260
1306
  * so withCacheLookup/withCacheStore treat the WHOLE matched route as a
1261
1307
  * cache() boundary — the shell entry IS a cache() of the handler layer, with
1262
- * loaders as the live carve-outs (resolveFreshLoadersAndYield).
1308
+ * loaders as the live carve-outs (serveStoredSegments).
1263
1309
  *
1264
1310
  * A document HIT tail (`docTail`) always gets the implicit scope: it replays
1265
1311
  * the shell's own record and never consults a route-derived scope, whose