@rangojs/router 0.27.0 → 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 (212) hide show
  1. package/dist/bin/rango.js +1 -1
  2. package/dist/types/__internal.d.ts +7 -1
  3. package/dist/types/browser/event-controller.d.ts +2 -1
  4. package/dist/types/browser/history-state.d.ts +3 -9
  5. package/dist/types/browser/navigation-store.d.ts +8 -6
  6. package/dist/types/browser/react/filter-segment-order.d.ts +2 -2
  7. package/dist/types/browser/segment-reconciler.d.ts +6 -4
  8. package/dist/types/browser/types.d.ts +2 -1
  9. package/dist/types/browser/wire-protocol.d.ts +25 -0
  10. package/dist/types/cache/cache-key-utils.d.ts +11 -5
  11. package/dist/types/cache/cache-scope.d.ts +34 -22
  12. package/dist/types/cache/cf/cf-cache-constants.d.ts +2 -2
  13. package/dist/types/cache/cf/cf-cache-store.d.ts +47 -65
  14. package/dist/types/cache/cf/cf-kv-utils.d.ts +1 -1
  15. package/dist/types/cache/handle-snapshot.d.ts +9 -4
  16. package/dist/types/cache/segment-codec.d.ts +14 -0
  17. package/dist/types/cache/shell-snapshot.d.ts +1 -1
  18. package/dist/types/cache/tag-invalidation.d.ts +10 -0
  19. package/dist/types/cache/taint.d.ts +5 -3
  20. package/dist/types/cache/types.d.ts +7 -3
  21. package/dist/types/cache/vercel/vercel-cache-store.d.ts +8 -6
  22. package/dist/types/prefetch-markers.d.ts +5 -14
  23. package/dist/types/prerender/store.d.ts +31 -0
  24. package/dist/types/prerender/warm-request.d.ts +3 -3
  25. package/dist/types/prerender.d.ts +2 -2
  26. package/dist/types/route-name.d.ts +5 -0
  27. package/dist/types/router/error-handling.d.ts +4 -4
  28. package/dist/types/router/instrument.d.ts +4 -3
  29. package/dist/types/router/intercept-resolution.d.ts +25 -9
  30. package/dist/types/router/match-context.d.ts +4 -11
  31. package/dist/types/router/match-handlers.d.ts +2 -6
  32. package/dist/types/router/match-middleware/background-revalidation.d.ts +10 -20
  33. package/dist/types/router/match-middleware/cache-lookup.d.ts +34 -86
  34. package/dist/types/router/match-middleware/segment-resolution.d.ts +8 -6
  35. package/dist/types/router/prerender-match.d.ts +21 -6
  36. package/dist/types/router/preview-match.d.ts +2 -7
  37. package/dist/types/router/request-classification.d.ts +5 -0
  38. package/dist/types/router/router-context.d.ts +7 -46
  39. package/dist/types/router/router-interfaces.d.ts +8 -14
  40. package/dist/types/router/segment-resolution/loader-cache.d.ts +3 -3
  41. package/dist/types/router/segment-resolution/loader-mask.d.ts +4 -4
  42. package/dist/types/router/segment-resolution/prefetch-deferral.d.ts +20 -18
  43. package/dist/types/router/segment-resolution/resolution-pass.d.ts +93 -0
  44. package/dist/types/router/segment-resolution/revalidation.d.ts +82 -31
  45. package/dist/types/router/segment-resolution.d.ts +1 -2
  46. package/dist/types/router/segment-wrappers.d.ts +11 -48
  47. package/dist/types/router/telemetry.d.ts +2 -0
  48. package/dist/types/router/types.d.ts +10 -0
  49. package/dist/types/rsc/full-payload.d.ts +2 -2
  50. package/dist/types/rsc/handler-context.d.ts +6 -6
  51. package/dist/types/rsc/json-route-result.d.ts +5 -9
  52. package/dist/types/rsc/request-stages.d.ts +203 -0
  53. package/dist/types/rsc/response-cache-serve.d.ts +7 -12
  54. package/dist/types/rsc/response-route-handler.d.ts +2 -2
  55. package/dist/types/rsc/routine-plan.d.ts +1 -1
  56. package/dist/types/rsc/server-action.d.ts +2 -2
  57. package/dist/types/rsc/shell-capture-constants.d.ts +1 -1
  58. package/dist/types/rsc/shell-capture.d.ts +132 -15
  59. package/dist/types/rsc/shell-serve.d.ts +37 -12
  60. package/dist/types/server/context.d.ts +9 -8
  61. package/dist/types/server/handle-store.d.ts +16 -0
  62. package/dist/types/server/render-pass.d.ts +72 -0
  63. package/dist/types/server/request-context.d.ts +24 -104
  64. package/dist/types/server/rewrite-trail.d.ts +2 -2
  65. package/dist/types/testing/dispatch.d.ts +35 -18
  66. package/dist/types/types/cache-types.d.ts +6 -3
  67. package/dist/types/types/error-types.d.ts +9 -1
  68. package/dist/types/types/handler-context.d.ts +9 -1
  69. package/dist/types/types/segments.d.ts +1 -1
  70. package/dist/types/vite/discovery/build-discovery-phase.d.ts +40 -0
  71. package/dist/types/vite/discovery/build-env.d.ts +22 -0
  72. package/dist/types/vite/discovery/bundle-scan-phase.d.ts +12 -0
  73. package/dist/types/vite/discovery/dev-prerender-cache.d.ts +3 -2
  74. package/dist/types/vite/discovery/dev-server-phase.d.ts +26 -0
  75. package/dist/types/vite/discovery/discovery-errors.d.ts +1 -1
  76. package/dist/types/vite/discovery/state.d.ts +12 -0
  77. package/dist/types/vite/plugins/version-plugin.d.ts +1 -1
  78. package/dist/types/vite/plugins/virtual-entries.d.ts +1 -1
  79. package/dist/types/vite/router-discovery.d.ts +8 -47
  80. package/dist/types/vite/temp-rsc-server.d.ts +39 -0
  81. package/dist/types/vite/utils/prerender-utils.d.ts +9 -0
  82. package/dist/types/wire-params.d.ts +34 -0
  83. package/dist/vite/index.js +3809 -3710
  84. package/package.json +1 -1
  85. package/skills/cache-guide/SKILL.md +8 -5
  86. package/skills/caching/SKILL.md +37 -18
  87. package/skills/document-cache/SKILL.md +13 -1
  88. package/skills/handler-use/SKILL.md +1 -1
  89. package/skills/intercept/SKILL.md +17 -7
  90. package/skills/middleware/SKILL.md +18 -0
  91. package/skills/ppr/SKILL.md +18 -3
  92. package/skills/prerender/SKILL.md +39 -15
  93. package/skills/rango/SKILL.md +5 -1
  94. package/skills/server-actions/SKILL.md +8 -1
  95. package/skills/testing/response-routes.md +6 -4
  96. package/skills/use-cache/SKILL.md +46 -8
  97. package/src/__internal.ts +7 -1
  98. package/src/browser/connection-warmup.ts +3 -1
  99. package/src/browser/event-controller.ts +5 -3
  100. package/src/browser/history-state.ts +6 -12
  101. package/src/browser/navigation-bridge.ts +14 -13
  102. package/src/browser/navigation-client.ts +18 -25
  103. package/src/browser/navigation-store.ts +25 -27
  104. package/src/browser/navigation-transaction.ts +4 -1
  105. package/src/browser/prefetch/cache.ts +2 -1
  106. package/src/browser/prefetch/fetch.ts +8 -16
  107. package/src/browser/prefetch/queue.ts +4 -4
  108. package/src/browser/react/NavigationProvider.tsx +2 -1
  109. package/src/browser/react/filter-segment-order.ts +2 -2
  110. package/src/browser/rsc-router.tsx +1 -0
  111. package/src/browser/segment-reconciler.ts +66 -53
  112. package/src/browser/server-action-bridge.ts +50 -57
  113. package/src/browser/types.ts +2 -1
  114. package/src/browser/wire-protocol.ts +41 -0
  115. package/src/cache/cache-key-utils.ts +11 -5
  116. package/src/cache/cache-runtime.ts +110 -50
  117. package/src/cache/cache-scope.ts +110 -64
  118. package/src/cache/cf/cf-cache-constants.ts +2 -2
  119. package/src/cache/cf/cf-cache-store.ts +486 -874
  120. package/src/cache/cf/cf-kv-utils.ts +1 -1
  121. package/src/cache/document-cache.ts +34 -13
  122. package/src/cache/handle-snapshot.ts +19 -7
  123. package/src/cache/segment-codec.ts +55 -0
  124. package/src/cache/shell-snapshot.ts +2 -2
  125. package/src/cache/tag-invalidation.ts +21 -2
  126. package/src/cache/taint.ts +5 -3
  127. package/src/cache/types.ts +7 -3
  128. package/src/cache/vercel/vercel-cache-store.ts +243 -310
  129. package/src/handles/pushed-by.ts +1 -1
  130. package/src/prefetch-markers.ts +5 -16
  131. package/src/prerender/store.ts +76 -0
  132. package/src/prerender/warm-request.ts +8 -4
  133. package/src/prerender.ts +2 -2
  134. package/src/route-name.ts +8 -0
  135. package/src/router/error-handling.ts +4 -4
  136. package/src/router/instrument.ts +4 -3
  137. package/src/router/intercept-resolution.ts +77 -75
  138. package/src/router/loader-resolution.ts +5 -4
  139. package/src/router/match-api.ts +4 -5
  140. package/src/router/match-context.ts +20 -12
  141. package/src/router/match-handlers.ts +8 -20
  142. package/src/router/match-middleware/background-revalidation.ts +41 -53
  143. package/src/router/match-middleware/cache-lookup.ts +675 -726
  144. package/src/router/match-middleware/cache-store.ts +4 -8
  145. package/src/router/match-middleware/intercept-resolution.ts +3 -23
  146. package/src/router/match-middleware/segment-resolution.ts +34 -61
  147. package/src/router/middleware.ts +69 -11
  148. package/src/router/navigation-snapshot.ts +4 -4
  149. package/src/router/prerender-match.ts +100 -44
  150. package/src/router/preview-match.ts +6 -41
  151. package/src/router/request-classification.ts +13 -7
  152. package/src/router/route-snapshot.ts +2 -5
  153. package/src/router/router-context.ts +10 -95
  154. package/src/router/router-interfaces.ts +8 -14
  155. package/src/router/segment-resolution/loader-cache.ts +14 -9
  156. package/src/router/segment-resolution/loader-mask.ts +5 -5
  157. package/src/router/segment-resolution/prefetch-deferral.ts +30 -27
  158. package/src/router/segment-resolution/resolution-pass.ts +137 -0
  159. package/src/router/segment-resolution/revalidation.ts +897 -776
  160. package/src/router/segment-resolution.ts +4 -13
  161. package/src/router/segment-wrappers.ts +51 -269
  162. package/src/router/telemetry.ts +5 -0
  163. package/src/router/types.ts +10 -0
  164. package/src/router.ts +12 -13
  165. package/src/rsc/full-payload.ts +6 -5
  166. package/src/rsc/handler-context.ts +8 -14
  167. package/src/rsc/handler.ts +126 -507
  168. package/src/rsc/json-route-result.ts +5 -9
  169. package/src/rsc/loader-fetch.ts +6 -5
  170. package/src/rsc/progressive-enhancement.ts +5 -0
  171. package/src/rsc/request-stages.ts +757 -0
  172. package/src/rsc/response-cache-serve.ts +10 -14
  173. package/src/rsc/response-route-handler.ts +13 -5
  174. package/src/rsc/rewrite-passes.ts +2 -1
  175. package/src/rsc/routine-plan.ts +1 -1
  176. package/src/rsc/rsc-rendering.ts +164 -349
  177. package/src/rsc/server-action.ts +87 -113
  178. package/src/rsc/shell-capture-constants.ts +1 -1
  179. package/src/rsc/shell-capture.ts +212 -169
  180. package/src/rsc/shell-serve.ts +121 -18
  181. package/src/rsc/ssr-setup.ts +5 -4
  182. package/src/segment-system.tsx +3 -3
  183. package/src/server/context.ts +22 -25
  184. package/src/server/handle-store.ts +55 -0
  185. package/src/server/render-pass.ts +291 -0
  186. package/src/server/request-context.ts +26 -115
  187. package/src/server/rewrite-trail.ts +6 -4
  188. package/src/ssr/index.tsx +1 -1
  189. package/src/testing/dispatch.ts +223 -710
  190. package/src/testing/e2e/page-helpers.ts +2 -2
  191. package/src/testing/serve-shell-request.ts +23 -36
  192. package/src/types/cache-types.ts +6 -3
  193. package/src/types/error-types.ts +9 -1
  194. package/src/types/handler-context.ts +12 -1
  195. package/src/types/segments.ts +1 -1
  196. package/src/vite/discovery/build-discovery-phase.ts +204 -0
  197. package/src/vite/discovery/build-env.ts +148 -0
  198. package/src/vite/discovery/bundle-scan-phase.ts +128 -0
  199. package/src/vite/discovery/dev-prerender-cache.ts +8 -3
  200. package/src/vite/discovery/dev-server-phase.ts +1776 -0
  201. package/src/vite/discovery/discover-routers.ts +1 -1
  202. package/src/vite/discovery/discovery-errors.ts +1 -1
  203. package/src/vite/discovery/prerender-collection.ts +24 -39
  204. package/src/vite/discovery/state.ts +15 -0
  205. package/src/vite/plugins/version-plugin.ts +1 -1
  206. package/src/vite/plugins/virtual-entries.ts +1 -1
  207. package/src/vite/router-discovery.ts +29 -2428
  208. package/src/vite/temp-rsc-server.ts +223 -0
  209. package/src/vite/utils/prerender-utils.ts +47 -0
  210. package/src/wire-params.ts +35 -0
  211. package/dist/types/router/segment-resolution/fresh.d.ts +0 -72
  212. package/src/router/segment-resolution/fresh.ts +0 -945
@@ -52,6 +52,7 @@ import { stampLocationState } from "./react/location-state-shared.js";
52
52
  import { classifyActionOutcome } from "./action-coordinator.js";
53
53
  import { revalidationErrorOf } from "../decode-loader-results.js";
54
54
  import { getAppVersion } from "./app-version.js";
55
+ import { writePartialParams } from "./wire-protocol.js";
55
56
  import {
56
57
  decideCommitGatedOff,
57
58
  findActionFormData,
@@ -487,28 +488,16 @@ export function createServerActionBridge(
487
488
  // Create temporary references for serialization
488
489
  const temporaryReferences = deps.createTemporaryReferenceSet();
489
490
 
490
- // Build action request URL with current segments
491
- const url = new URL(window.location.href);
492
- url.searchParams.set("_rsc_action", id);
493
- url.searchParams.set(
494
- "_rsc_segments",
495
- segmentState.currentSegmentIds.join(","),
496
- );
497
- // Add version param for version mismatch detection
498
- const version = getAppVersion();
499
- if (version) {
500
- url.searchParams.set("_rsc_v", version);
501
- }
502
- // Add router ID for app switch detection
503
- const rid = store.getRouterId?.();
504
- if (rid) {
505
- url.searchParams.set("_rsc_rid", rid);
506
- }
507
- // The page's rewrite target: the server diffs the re-render against it.
508
- const rewriteTarget = store.getHistoryEntryMemory()?.rewriteTarget;
509
- if (rewriteTarget) {
510
- url.searchParams.set("_rsc_rt", rewriteTarget);
511
- }
491
+ // Build action request URL with current segments; the server diffs the
492
+ // re-render against the page's rewrite target.
493
+ const url = writePartialParams(new URL(window.location.href), {
494
+ action: id,
495
+ segmentIds: segmentState.currentSegmentIds,
496
+ emptySegments: "send",
497
+ version: getAppVersion(),
498
+ routerId: store.getRouterId?.(),
499
+ rewriteTarget: store.getHistoryEntryMemory()?.rewriteTarget,
500
+ });
512
501
 
513
502
  // Encode arguments
514
503
  const encodedBody = await deps.encodeReply(args, { temporaryReferences });
@@ -740,36 +729,50 @@ export function createServerActionBridge(
740
729
  }
741
730
 
742
731
  // Handle error responses with error boundary UI
743
- if (isError && isPartial && segments && diff) {
732
+ if (isError && isPartial && segments) {
744
733
  log("processing error boundary response");
745
734
 
746
- // Fail current handle BEFORE aborting all actions so the event controller
747
- // records the error state (abortAllActions clears inflight entries)
735
+ // transition({ when }) sees action.error on the boundary commit and on
736
+ // a dropped one's follow-up.
737
+ whenAction = {
738
+ ...whenAction,
739
+ error: returnValue && !returnValue.ok ? returnValue.data : undefined,
740
+ };
741
+
742
+ // No consolidation: the boundary replaces the page, or the page left.
743
+ handle.clearConsolidation();
744
+
745
+ const currentKey = store.getHistoryKey();
746
+ const pageLeft = () =>
747
+ navigatedSince() || store.getHistoryKey() !== currentKey;
748
+ // The action's page left the screen (during the flight or the error
749
+ // render): drop the boundary; the call rejects and the exit owes the
750
+ // follow-up.
751
+ const dropBoundary = () => {
752
+ log("user navigated during error render, skipping");
753
+ if (returnValue && !returnValue.ok) throw returnValue.data;
754
+ handle.complete(undefined);
755
+ return undefined;
756
+ };
757
+ if (pageLeft()) return dropBoundary();
758
+
759
+ // fail() settles once the response's stream ends; the abort below
760
+ // spares this action, so its entry keeps the error for useAction().
748
761
  if (returnValue && !returnValue.ok) {
749
762
  handle.fail(returnValue.data);
750
763
  }
751
764
 
752
- // Abort all other pending action requests - error takes precedence
753
- // This prevents other actions from completing and overwriting the error UI
754
- eventController.abortAllActions();
755
-
756
- // Clear concurrent action tracking - no consolidation needed when showing error
757
- handle.clearConsolidation();
765
+ // Abort the other actions in flight: the error takes precedence, so
766
+ // none completes over the boundary.
767
+ eventController.abortAllActions(handle.id);
758
768
 
759
769
  // Get current page's cached segments
760
- const currentKey = store.getHistoryKey();
761
770
  const cached = store.getCachedSegments(currentKey);
762
771
  const cachedSegments = cached?.segments || [];
763
772
 
764
773
  // Reconcile error segments with cached tree
765
774
  const errorResult = reconcileErrorSegments(cachedSegments, segments);
766
775
 
767
- // transition({ when }) decides the error-boundary commit too, with
768
- // `action.error` set.
769
- whenAction = {
770
- ...whenAction,
771
- error: returnValue && !returnValue.ok ? returnValue.data : undefined,
772
- };
773
776
  const errorGatedOff = decideActionGatedOff(
774
777
  errorResult.segments,
775
778
  metadata,
@@ -786,28 +789,18 @@ export function createServerActionBridge(
786
789
  : undefined,
787
790
  });
788
791
 
789
- // The page the action was sent from left the screen, during the
790
- // flight or while the error tree rendered: the boundary is not shown,
791
- // and the action's exit owes the page on screen the follow-up.
792
- if (navigatedSince() || store.getHistoryKey() !== currentKey) {
793
- log("user navigated during error render, skipping");
794
- if (returnValue && !returnValue.ok) {
795
- throw returnValue.data;
796
- }
797
- handle.complete(undefined);
798
- return undefined;
799
- }
800
-
801
- // Update segment tracking to exclude error segment IDs
802
- const errorSegmentIds = new Set(diff);
803
- const segmentIdsAfterError = segmentState.currentSegmentIds.filter(
804
- (id) => !errorSegmentIds.has(id),
805
- );
792
+ if (pageLeft()) return dropBoundary();
806
793
 
807
794
  // Cache (and bump the nav instance) BEFORE the UI update so a deferred
808
795
  // handle pushed by the error-boundary render still applies — see the
809
- // "normal" case below for why caching after onUpdate dropped it.
810
- store.setSegmentIds(segmentIdsAfterError);
796
+ // "normal" case below for why caching after onUpdate dropped it. The
797
+ // page stops claiming the error segments only: the loaders the
798
+ // response re-ran above the boundary stay held.
799
+ store.setSegmentIds(
800
+ segmentState.currentSegmentIds.filter(
801
+ (id) => !segments.some((s) => s.id === id && s.type === "error"),
802
+ ),
803
+ );
811
804
  const currentHandleData = liveHandleDataForCache(
812
805
  eventController,
813
806
  store,
@@ -649,7 +649,8 @@ export interface NavigationStore {
649
649
  handlesPending?: boolean;
650
650
  }
651
651
  | undefined;
652
- hasHistoryCache(historyKey: string): boolean;
652
+ /** Whether an intercept entry is cached for the plain `historyKey`, from any source page. */
653
+ hasInterceptCache(historyKey: string): boolean;
653
654
  /**
654
655
  * Update only the handleData (and optionally the stale / handlesPending flags)
655
656
  * of an existing cache entry. When a flag is omitted the entry's current value
@@ -0,0 +1,41 @@
1
+ import { WIRE } from "../wire-params.js";
2
+
3
+ /** The parameters of a partial request: a navigation, a prefetch or a JS server action. */
4
+ export interface PartialRequestInit {
5
+ /** A JS server action's id: written as `_rsc_action` in place of `_rsc_partial`. */
6
+ action?: string;
7
+ /** The mounted segment ids the server diffs against. */
8
+ segmentIds: readonly string[];
9
+ /**
10
+ * An empty `segmentIds`: "send" writes `_rsc_segments=` (navigation,
11
+ * action), "omit" leaves it out (prefetch). The prefetch cache keys carry
12
+ * the difference.
13
+ */
14
+ emptySegments: "send" | "omit";
15
+ stale?: boolean;
16
+ version?: string;
17
+ routerId?: string;
18
+ rewriteTarget?: string;
19
+ fill?: boolean;
20
+ }
21
+
22
+ /**
23
+ * Writes the partial-request parameters onto `url` in the one order every
24
+ * sender uses. Navigation and prefetch cache keys are built from the result
25
+ * (prefetch/cache.ts), so a navigation adopts a prefetch only while both
26
+ * write identically: browser/__tests__/wire-urls.test.ts pins the strings.
27
+ */
28
+ export function writePartialParams(url: URL, init: PartialRequestInit): URL {
29
+ const params = url.searchParams;
30
+ if (init.action !== undefined) params.set(WIRE.action, init.action);
31
+ else params.set(WIRE.partial, "true");
32
+ if (init.segmentIds.length > 0 || init.emptySegments === "send") {
33
+ params.set(WIRE.segments, init.segmentIds.join(","));
34
+ }
35
+ if (init.stale) params.set(WIRE.stale, "true");
36
+ if (init.version) params.set(WIRE.version, init.version);
37
+ if (init.routerId) params.set(WIRE.routerId, init.routerId);
38
+ if (init.rewriteTarget) params.set(WIRE.rewriteTarget, init.rewriteTarget);
39
+ if (init.fill) params.set(WIRE.fill, "1");
40
+ return url;
41
+ }
@@ -187,8 +187,10 @@ export const KEY_PART_PREFIX = "key:";
187
187
  * (CacheScope.resolveKeyFrom, CacheScope.resolvePartition): the `key()`
188
188
  * results of the scope's cache() chain, outermost first, then its default
189
189
  * parts: the keyGenerator results of enclosing scopes on another store
190
- * (#974) and, for a record key under a scope without its own `key()`, its
191
- * default key (or its store's keyGenerator result, #970).
190
+ * (#974) and, for a route record key, the scope's own part: without a
191
+ * `key()` of its own, its default key (or its store's keyGenerator result,
192
+ * #970); with one, the default key's request kind (`doc`, `partial` or
193
+ * `intercept:<intercept>`, routeRecordKind in cache-scope.ts).
192
194
  *
193
195
  * Key scheme invariant (#975). A `key()` result is often request input,
194
196
  * and a record key has no route or scope discriminator (the store reads
@@ -214,9 +216,13 @@ export const KEY_PART_PREFIX = "key:";
214
216
  * A namespaced `key()` result starts with `key:` and holds no `|`. A
215
217
  * composed key holds a `|` and either starts with `key:` or holds no `:`.
216
218
  * Among composed keys, splitting on `|` recovers each part, a part's kind
217
- * (a `key:` prefix, or no `:` at all) and its value. Pinned by the collision
218
- * probe in src/cache/__tests__/cache-scope-chain.test.ts. A keyGenerator
219
- * result is the store's own namespace, as before.
219
+ * (a `key:` prefix, or no `:` at all) and its value. A request kind part is
220
+ * a default key's prefix without the URL after it, so it never equals a
221
+ * default key part; a keyGenerator part can equal it (`doc`) only when the
222
+ * keyGenerator returns a constant, which already gives every URL one record.
223
+ * Pinned by the collision probe in
224
+ * src/cache/__tests__/cache-scope-chain.test.ts. A keyGenerator result is
225
+ * the store's own namespace, as before.
220
226
  *
221
227
  * Shared with the testing helper `shellCacheKey`, so a test builds the
222
228
  * production partition.
@@ -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