@rangojs/router 0.26.6 → 0.28.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 (234) 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-coordinator.d.ts +0 -4
  4. package/dist/types/browser/action-fence.d.ts +11 -3
  5. package/dist/types/browser/event-controller.d.ts +20 -1
  6. package/dist/types/browser/history-state.d.ts +7 -9
  7. package/dist/types/browser/intercept-utils.d.ts +23 -0
  8. package/dist/types/browser/navigation-store.d.ts +8 -6
  9. package/dist/types/browser/react/filter-segment-order.d.ts +2 -2
  10. package/dist/types/browser/rsc-router.d.ts +1 -1
  11. package/dist/types/browser/segment-reconciler.d.ts +6 -4
  12. package/dist/types/browser/types.d.ts +2 -1
  13. package/dist/types/browser/wire-protocol.d.ts +25 -0
  14. package/dist/types/cache/cache-key-utils.d.ts +11 -5
  15. package/dist/types/cache/cache-scope.d.ts +34 -22
  16. package/dist/types/cache/cf/cf-cache-constants.d.ts +2 -2
  17. package/dist/types/cache/cf/cf-cache-store.d.ts +47 -65
  18. package/dist/types/cache/cf/cf-kv-utils.d.ts +1 -1
  19. package/dist/types/cache/handle-snapshot.d.ts +9 -4
  20. package/dist/types/cache/segment-codec.d.ts +14 -0
  21. package/dist/types/cache/shell-snapshot.d.ts +1 -1
  22. package/dist/types/cache/tag-invalidation.d.ts +10 -0
  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 +4 -6
  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 +4 -4
  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 +12 -11
  35. package/dist/types/router/match-handlers.d.ts +2 -6
  36. package/dist/types/router/match-middleware/background-revalidation.d.ts +10 -20
  37. package/dist/types/router/match-middleware/cache-lookup.d.ts +35 -87
  38. package/dist/types/router/match-middleware/cache-store.d.ts +2 -2
  39. package/dist/types/router/match-middleware/segment-resolution.d.ts +8 -6
  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/revalidation.d.ts +6 -1
  44. package/dist/types/router/router-context.d.ts +9 -63
  45. package/dist/types/router/router-interfaces.d.ts +8 -14
  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 +93 -0
  50. package/dist/types/router/segment-resolution/revalidation.d.ts +82 -31
  51. package/dist/types/router/segment-resolution.d.ts +1 -2
  52. package/dist/types/router/segment-wrappers.d.ts +11 -48
  53. package/dist/types/router/telemetry.d.ts +2 -0
  54. package/dist/types/router/types.d.ts +10 -0
  55. package/dist/types/rsc/full-payload.d.ts +2 -2
  56. package/dist/types/rsc/handler-context.d.ts +6 -6
  57. package/dist/types/rsc/json-route-result.d.ts +5 -9
  58. package/dist/types/rsc/request-stages.d.ts +203 -0
  59. package/dist/types/rsc/response-cache-serve.d.ts +7 -12
  60. package/dist/types/rsc/response-route-handler.d.ts +2 -2
  61. package/dist/types/rsc/routine-plan.d.ts +1 -1
  62. package/dist/types/rsc/server-action.d.ts +2 -2
  63. package/dist/types/rsc/shell-capture-constants.d.ts +2 -2
  64. package/dist/types/rsc/shell-capture.d.ts +132 -15
  65. package/dist/types/rsc/shell-serve.d.ts +37 -12
  66. package/dist/types/server/context.d.ts +9 -8
  67. package/dist/types/server/handle-store.d.ts +16 -0
  68. package/dist/types/server/render-pass.d.ts +72 -0
  69. package/dist/types/server/request-context.d.ts +24 -104
  70. package/dist/types/server/rewrite-trail.d.ts +2 -2
  71. package/dist/types/testing/dispatch.d.ts +35 -18
  72. package/dist/types/testing/serve-shell-request.d.ts +7 -0
  73. package/dist/types/types/cache-types.d.ts +6 -3
  74. package/dist/types/types/error-types.d.ts +9 -1
  75. package/dist/types/types/handler-context.d.ts +18 -7
  76. package/dist/types/types/segments.d.ts +1 -1
  77. package/dist/types/vite/discovery/build-discovery-phase.d.ts +40 -0
  78. package/dist/types/vite/discovery/build-env.d.ts +22 -0
  79. package/dist/types/vite/discovery/bundle-scan-phase.d.ts +12 -0
  80. package/dist/types/vite/discovery/dev-prerender-cache.d.ts +3 -2
  81. package/dist/types/vite/discovery/dev-server-phase.d.ts +26 -0
  82. package/dist/types/vite/discovery/discovery-errors.d.ts +1 -1
  83. package/dist/types/vite/discovery/state.d.ts +12 -0
  84. package/dist/types/vite/plugins/version-plugin.d.ts +1 -1
  85. package/dist/types/vite/plugins/virtual-entries.d.ts +1 -1
  86. package/dist/types/vite/router-discovery.d.ts +8 -47
  87. package/dist/types/vite/temp-rsc-server.d.ts +39 -0
  88. package/dist/types/vite/utils/prerender-utils.d.ts +9 -0
  89. package/dist/types/wire-params.d.ts +34 -0
  90. package/dist/vite/index.js +3809 -3710
  91. package/package.json +1 -1
  92. package/skills/cache-guide/SKILL.md +8 -5
  93. package/skills/caching/SKILL.md +37 -18
  94. package/skills/client-urls/SKILL.md +2 -1
  95. package/skills/document-cache/SKILL.md +18 -1
  96. package/skills/handler-use/SKILL.md +1 -1
  97. package/skills/intercept/SKILL.md +53 -13
  98. package/skills/layout/SKILL.md +11 -7
  99. package/skills/loader/SKILL.md +30 -13
  100. package/skills/middleware/SKILL.md +19 -1
  101. package/skills/migrate-nextjs/SKILL.md +5 -4
  102. package/skills/parallel/SKILL.md +5 -4
  103. package/skills/ppr/SKILL.md +19 -3
  104. package/skills/prerender/SKILL.md +39 -15
  105. package/skills/rango/SKILL.md +23 -11
  106. package/skills/server-actions/SKILL.md +51 -6
  107. package/skills/testing/predicates.md +1 -1
  108. package/skills/testing/response-routes.md +6 -4
  109. package/skills/testing/serve-shell-request.md +7 -7
  110. package/skills/use-cache/SKILL.md +46 -8
  111. package/skills/ux-architecture/SKILL.md +3 -2
  112. package/src/__internal.ts +7 -1
  113. package/src/browser/action-coordinator.ts +2 -13
  114. package/src/browser/action-fence.ts +11 -3
  115. package/src/browser/connection-warmup.ts +3 -1
  116. package/src/browser/event-controller.ts +42 -3
  117. package/src/browser/history-state.ts +15 -13
  118. package/src/browser/intercept-utils.ts +51 -0
  119. package/src/browser/navigation-bridge.ts +34 -30
  120. package/src/browser/navigation-client.ts +21 -33
  121. package/src/browser/navigation-store.ts +25 -27
  122. package/src/browser/navigation-transaction.ts +7 -11
  123. package/src/browser/partial-update.ts +8 -0
  124. package/src/browser/prefetch/cache.ts +2 -1
  125. package/src/browser/prefetch/fetch.ts +8 -16
  126. package/src/browser/prefetch/queue.ts +4 -4
  127. package/src/browser/react/NavigationProvider.tsx +2 -1
  128. package/src/browser/react/filter-segment-order.ts +2 -2
  129. package/src/browser/rsc-router.tsx +17 -2
  130. package/src/browser/segment-reconciler.ts +66 -53
  131. package/src/browser/server-action-bridge.ts +265 -127
  132. package/src/browser/types.ts +2 -1
  133. package/src/browser/wire-protocol.ts +41 -0
  134. package/src/cache/cache-key-utils.ts +11 -5
  135. package/src/cache/cache-runtime.ts +110 -50
  136. package/src/cache/cache-scope.ts +110 -64
  137. package/src/cache/cf/cf-cache-constants.ts +2 -2
  138. package/src/cache/cf/cf-cache-store.ts +486 -874
  139. package/src/cache/cf/cf-kv-utils.ts +1 -1
  140. package/src/cache/document-cache.ts +42 -15
  141. package/src/cache/handle-snapshot.ts +19 -7
  142. package/src/cache/segment-codec.ts +55 -0
  143. package/src/cache/shell-snapshot.ts +2 -2
  144. package/src/cache/tag-invalidation.ts +21 -2
  145. package/src/cache/taint.ts +5 -3
  146. package/src/cache/types.ts +7 -3
  147. package/src/cache/vercel/vercel-cache-store.ts +243 -310
  148. package/src/handles/pushed-by.ts +1 -1
  149. package/src/prefetch-markers.ts +4 -7
  150. package/src/prerender/store.ts +76 -0
  151. package/src/prerender/warm-request.ts +8 -4
  152. package/src/prerender.ts +2 -2
  153. package/src/route-name.ts +8 -0
  154. package/src/router/error-handling.ts +4 -4
  155. package/src/router/instrument.ts +4 -3
  156. package/src/router/intercept-resolution.ts +77 -75
  157. package/src/router/loader-resolution.ts +5 -4
  158. package/src/router/match-api.ts +4 -5
  159. package/src/router/match-context.ts +33 -12
  160. package/src/router/match-handlers.ts +8 -20
  161. package/src/router/match-middleware/background-revalidation.ts +41 -53
  162. package/src/router/match-middleware/cache-lookup.ts +680 -720
  163. package/src/router/match-middleware/cache-store.ts +14 -12
  164. package/src/router/match-middleware/intercept-resolution.ts +3 -23
  165. package/src/router/match-middleware/segment-resolution.ts +34 -61
  166. package/src/router/match-result.ts +1 -20
  167. package/src/router/middleware.ts +69 -11
  168. package/src/router/navigation-snapshot.ts +4 -3
  169. package/src/router/prerender-match.ts +100 -44
  170. package/src/router/preview-match.ts +6 -41
  171. package/src/router/request-classification.ts +13 -7
  172. package/src/router/revalidation.ts +33 -7
  173. package/src/router/route-snapshot.ts +2 -5
  174. package/src/router/router-context.ts +14 -117
  175. package/src/router/router-interfaces.ts +8 -14
  176. package/src/router/segment-resolution/loader-cache.ts +14 -9
  177. package/src/router/segment-resolution/loader-mask.ts +5 -5
  178. package/src/router/segment-resolution/prefetch-deferral.ts +30 -27
  179. package/src/router/segment-resolution/resolution-pass.ts +137 -0
  180. package/src/router/segment-resolution/revalidation.ts +897 -776
  181. package/src/router/segment-resolution.ts +4 -13
  182. package/src/router/segment-wrappers.ts +51 -269
  183. package/src/router/telemetry.ts +5 -0
  184. package/src/router/types.ts +10 -0
  185. package/src/router.ts +12 -13
  186. package/src/rsc/full-payload.ts +6 -5
  187. package/src/rsc/handler-context.ts +8 -14
  188. package/src/rsc/handler.ts +126 -507
  189. package/src/rsc/json-route-result.ts +5 -9
  190. package/src/rsc/loader-fetch.ts +6 -5
  191. package/src/rsc/progressive-enhancement.ts +5 -0
  192. package/src/rsc/request-stages.ts +757 -0
  193. package/src/rsc/response-cache-serve.ts +10 -14
  194. package/src/rsc/response-route-handler.ts +13 -5
  195. package/src/rsc/rewrite-passes.ts +2 -1
  196. package/src/rsc/routine-plan.ts +1 -1
  197. package/src/rsc/rsc-rendering.ts +172 -349
  198. package/src/rsc/server-action.ts +87 -113
  199. package/src/rsc/shell-capture-constants.ts +3 -1
  200. package/src/rsc/shell-capture.ts +212 -169
  201. package/src/rsc/shell-serve.ts +121 -18
  202. package/src/rsc/ssr-setup.ts +5 -4
  203. package/src/segment-system.tsx +3 -3
  204. package/src/server/context.ts +22 -25
  205. package/src/server/handle-store.ts +55 -0
  206. package/src/server/render-pass.ts +291 -0
  207. package/src/server/request-context.ts +26 -115
  208. package/src/server/rewrite-trail.ts +6 -4
  209. package/src/ssr/index.tsx +1 -1
  210. package/src/ssr/ssr-root.tsx +1 -0
  211. package/src/testing/dispatch.ts +223 -710
  212. package/src/testing/e2e/page-helpers.ts +2 -2
  213. package/src/testing/serve-shell-request.ts +37 -32
  214. package/src/types/cache-types.ts +6 -3
  215. package/src/types/error-types.ts +9 -1
  216. package/src/types/handler-context.ts +21 -7
  217. package/src/types/segments.ts +1 -1
  218. package/src/vite/discovery/build-discovery-phase.ts +204 -0
  219. package/src/vite/discovery/build-env.ts +148 -0
  220. package/src/vite/discovery/bundle-scan-phase.ts +128 -0
  221. package/src/vite/discovery/dev-prerender-cache.ts +8 -3
  222. package/src/vite/discovery/dev-server-phase.ts +1776 -0
  223. package/src/vite/discovery/discover-routers.ts +1 -1
  224. package/src/vite/discovery/discovery-errors.ts +1 -1
  225. package/src/vite/discovery/prerender-collection.ts +24 -39
  226. package/src/vite/discovery/state.ts +15 -0
  227. package/src/vite/plugins/version-plugin.ts +1 -1
  228. package/src/vite/plugins/virtual-entries.ts +1 -1
  229. package/src/vite/router-discovery.ts +29 -2428
  230. package/src/vite/temp-rsc-server.ts +223 -0
  231. package/src/vite/utils/prerender-utils.ts +47 -0
  232. package/src/wire-params.ts +35 -0
  233. package/dist/types/router/segment-resolution/fresh.d.ts +0 -72
  234. package/src/router/segment-resolution/fresh.ts +0 -945
@@ -22,7 +22,7 @@ import {
22
22
  } from "../deps/rsc-client.js";
23
23
  import {
24
24
  getRequestContext,
25
- runWithRequestContext,
25
+ type RequestContext,
26
26
  } from "../server/request-context.js";
27
27
  import { isUnderTestRunner } from "../runtime-env.js";
28
28
  import {
@@ -34,7 +34,12 @@ import {
34
34
  } from "./taint.js";
35
35
 
36
36
  export { isCachedFunction };
37
- import { serializeResult, deserializeResult } from "./segment-codec.js";
37
+ import {
38
+ serializeResult,
39
+ deserializeResult,
40
+ isUnsupportedValueError,
41
+ teeResult,
42
+ } from "./segment-codec.js";
38
43
  import {
39
44
  appendHandles,
40
45
  encodeHandles,
@@ -70,6 +75,7 @@ import {
70
75
  executionStart,
71
76
  invalidatedSince,
72
77
  predatesInvalidation,
78
+ runAsBackgroundRefresh,
73
79
  type ExecutionStart,
74
80
  } from "./tag-invalidation.js";
75
81
  import type { CacheItemResult } from "./types.js";
@@ -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
@@ -717,25 +732,24 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
717
732
  // reads the ambient getRequestContext() (e.g.
718
733
  // getRequestContext().env.ApiKey) resolves during the background
719
734
  // 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.
735
+ // context". runAsBackgroundRefresh sets the store for fn's
736
+ // synchronous kickoff; its async continuations inherit it. No
737
+ // visitor made the invalidations it runs: they set no
738
+ // fresh-reads cookie.
722
739
  //
723
740
  // The span opens inside the request ALS and wraps the WHOLE task —
724
741
  // fn kickoff through the store write — so its platform spans nest
725
742
  // under rango.background and a slow or failing setItem is part of
726
743
  // the traced revalidation, not an untraced tail.
727
- await runWithRequestContext(requestCtx, () =>
744
+ await runAsBackgroundRefresh(requestCtx, () =>
728
745
  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.
@@ -921,21 +940,24 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
921
940
  // deliberately NOT stamped: a property on that SHARED object made every
922
941
  // parallel read on the request throw for the cached body's whole
923
942
  // execution window (a 2s cached fetch poisoned a sibling loader's
924
- // cookies() — same hazard class as issue #684, plan 010).
943
+ // cookies() — same hazard class as issue #684, plan 010). Not as an
944
+ // argument either (getRequestContext(), any pass: a derived pass inherits
945
+ // `_requestRoot`): a derived pass's writers that it does not route to a
946
+ // sink check the request's own context (server/render-pass.ts), so the
947
+ // stamp failed a shell capture's or a background re-render's writes while
948
+ // a foreground body held it.
925
949
  const taintedArgs: unknown[] = [];
926
950
  for (const arg of args) {
927
- if (isTainted(arg)) {
951
+ if (isTainted(arg) && !(arg as RequestContext)._requestRoot) {
928
952
  stampCacheExec(arg as object);
929
953
  taintedArgs.push(arg);
930
954
  }
931
955
  }
932
956
 
933
957
  let result: any;
934
- let scoped: ReturnType<typeof runWithCacheTagScope>;
958
+ let scoped: ReturnType<typeof inExecution>;
935
959
  try {
936
- scoped = runWithCacheTagScope(() =>
937
- runWithCacheExecScope(() => fn.apply(this, args), execScope),
938
- );
960
+ scoped = inExecution(execScope, () => fn.apply(this, args));
939
961
  result = await scoped.result;
940
962
  } catch (execError) {
941
963
  // The function threw: drop the in-flight entry and reject any waiters so
@@ -957,25 +979,39 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
957
979
  const recorded = scoped!.tags;
958
980
  recordRequestTags(entryTags(recorded), requestCtx);
959
981
 
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.
982
+ // Encode the value ONCE, inside the execution's scopes, and hand the
983
+ // caller a decoded copy of it, as a hit does. The caller rendering
984
+ // `result` itself would share its lazy state (a React.lazy, a memoizing
985
+ // thenable) with the encode: whichever render reached it first decided
986
+ // its value for both, so a read the caller's render made outside the
987
+ // scope was stored for the next visitor. Then resolve the in-flight
988
+ // envelope so concurrent followers serve a synthetic hit, and persist.
989
+ // When waitUntil is available the leader waits for the copy's root row
990
+ // only, not for the rest of the encode or the store write.
991
+ const flightErrors: unknown[] = [];
992
+ const onFlightError = (error: unknown): void => {
993
+ flightErrors.push(error);
994
+ };
995
+ const encoded = inExecution(
996
+ execScope,
997
+ () => teeResult(result, onFlightError),
998
+ recorded,
999
+ ).result;
964
1000
  const finalizeAndWrite = async (): Promise<void> => {
965
1001
  let serialized: string | null;
966
1002
  let encodedHandles: string | undefined;
967
- const flightErrors: unknown[] = [];
968
- const onFlightError = (error: unknown): void => {
969
- flightErrors.push(error);
970
- };
971
1003
  try {
972
- serialized = await inTagScope(recorded, () =>
973
- serializeResult(result, onFlightError),
974
- );
1004
+ serialized = encoded ? await encoded.serialized : null;
1005
+ // The returned tree renders only in the encode, so its cacheTag()
1006
+ // calls tag the entry alone: record the entry's tags onto the
1007
+ // request, as a hit does.
1008
+ recordRequestTags(entryTags(recorded), requestCtx);
975
1009
  encodedHandles = capture
976
- ? await inTagScope(recorded, () =>
977
- encodeHandles(capture.capture.data, onFlightError),
978
- )
1010
+ ? await inExecution(
1011
+ execScope,
1012
+ () => encodeHandles(capture.capture.data, onFlightError),
1013
+ recorded,
1014
+ ).result
979
1015
  : undefined;
980
1016
  // Flight encodes an async component that throws, or a rejected
981
1017
  // promise in the result or a handle value, as an error row and
@@ -986,10 +1022,17 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
986
1022
  assertLoaderReadsClean(execScope);
987
1023
  } catch (buildError) {
988
1024
  // Serialize/handle-encode failed: no envelope for followers (they run
989
- // fresh) and nothing to write.
1025
+ // fresh) and nothing to write. Logged too: a read the guard refused
1026
+ // in the value is otherwise silent with no onError. An error the call
1027
+ // also throws reaches onError once (the request dedups by identity).
990
1028
  clearSelf();
991
1029
  rejectEnvelope(buildError);
992
- requestCtx?._reportBackgroundError?.(buildError, "cache-write");
1030
+ reportCacheError(
1031
+ buildError,
1032
+ "cache-write",
1033
+ `[use cache] "${id}" write`,
1034
+ requestCtx,
1035
+ );
993
1036
  return;
994
1037
  }
995
1038
  if (serialized === null) {
@@ -1039,7 +1082,24 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
1039
1082
 
1040
1083
  await runBackground(requestCtx, finalizeAndWrite, true);
1041
1084
 
1042
- return result;
1085
+ // The encode could not start: nothing is stored, the value renders live.
1086
+ if (!encoded) return result;
1087
+ try {
1088
+ return await encoded.decoded;
1089
+ } catch (decodeError) {
1090
+ // The root row failed. A value Flight cannot encode (a function, a
1091
+ // class instance) is not cached and renders live, as it always has;
1092
+ // anything else, a refused read above all, fails the call as the same
1093
+ // read in the body does, with the encode's own error (production
1094
+ // Flight redacts the decoded one).
1095
+ if (
1096
+ flightErrors.length > 0 &&
1097
+ flightErrors.every(isUnsupportedValueError)
1098
+ ) {
1099
+ return result;
1100
+ }
1101
+ throw flightErrors[0] ?? decodeError;
1102
+ }
1043
1103
  };
1044
1104
 
1045
1105
  // 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
@@ -55,14 +55,14 @@ export const CACHE_REVALIDATING_AT_HEADER = "x-edge-cache-revalidating-at";
55
55
  * re-arm re-puts the full window and restarts CF's retention clock, pinning a
56
56
  * perpetually-stale entry (one whose background revalidation keeps failing) past
57
57
  * its intended hard-expiry indefinitely. Mirrors the KVSegmentEnvelope `e`
58
- * field and the remaining-ttl math in promoteSegmentToL1/promoteItemToL1.
58
+ * field and the remaining-ttl math in promoteToL1.
59
59
  * @internal
60
60
  */
61
61
  export const CACHE_EXPIRES_AT_HEADER = "x-edge-cache-expires-at";
62
62
 
63
63
  /**
64
64
  * Header stashing the route author's original Cache-Control on L1 document
65
- * entries. putResponse/promoteResponseToL1 overwrite Cache-Control with a long
65
+ * entries. putResponse/promoteToL1 overwrite Cache-Control with a long
66
66
  * `max-age` so the CF Cache API retains the entry across the whole SWR window;
67
67
  * getResponse restores this original value before serving so the client and any
68
68
  * upstream CDN see the author's intended directive, not the internal edge TTL.