@rangojs/router 0.32.0 → 0.33.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 (180) hide show
  1. package/README.md +3 -2
  2. package/dist/bin/browser-7KIJ5HSV.js +15 -0
  3. package/dist/bin/chunk-3B7CKBRO.js +463 -0
  4. package/dist/bin/chunk-524VACCG.js +71 -0
  5. package/dist/bin/chunk-LD7UBHT4.js +196 -0
  6. package/dist/bin/chunk-TO6CRWLN.js +1429 -0
  7. package/dist/bin/chunk-TTXC2U3G.js +2477 -0
  8. package/dist/bin/client-urls-projection-DQLWPDAT.js +28 -0
  9. package/dist/bin/forward-user-plugins-URBZKJP4.js +62 -0
  10. package/dist/bin/rango.js +42 -10173
  11. package/dist/bin/runtime-EPUDEHO4.js +596 -0
  12. package/dist/bin/runtime-discovery-CZ2QHT4Q.js +352 -0
  13. package/dist/bin/state-ACD2MGKJ.js +16 -0
  14. package/dist/bin/temp-rsc-server-QRR6HD6T.js +3190 -0
  15. package/dist/types/bin/rango.d.ts +1 -0
  16. package/dist/types/browser/action-fence.d.ts +20 -0
  17. package/dist/types/browser/cookie-name.d.ts +51 -10
  18. package/dist/types/browser/invalidate-client-cache.d.ts +8 -6
  19. package/dist/types/browser/navigation-bridge.d.ts +36 -1
  20. package/dist/types/browser/navigation-client.d.ts +19 -0
  21. package/dist/types/browser/navigation-store-handle.d.ts +37 -4
  22. package/dist/types/browser/navigation-store.d.ts +2 -2
  23. package/dist/types/browser/navigation-transaction.d.ts +0 -1
  24. package/dist/types/browser/partial-update.d.ts +13 -0
  25. package/dist/types/browser/prefetch/cache.d.ts +96 -8
  26. package/dist/types/browser/prefetch/fetch.d.ts +26 -2
  27. package/dist/types/browser/prefetch/queue.d.ts +3 -2
  28. package/dist/types/browser/rango-state.d.ts +34 -5
  29. package/dist/types/browser/react/Link.d.ts +15 -6
  30. package/dist/types/browser/types.d.ts +25 -0
  31. package/dist/types/build/generate-route-types.d.ts +1 -1
  32. package/dist/types/build/route-types/include-resolution.d.ts +9 -1
  33. package/dist/types/build/route-types/router-processing.d.ts +3 -1
  34. package/dist/types/cache/cache-key-utils.d.ts +12 -8
  35. package/dist/types/cache/cache-scope.d.ts +4 -2
  36. package/dist/types/cache/cf/cf-cache-types.d.ts +9 -5
  37. package/dist/types/cache/document-cache-policy.d.ts +9 -1
  38. package/dist/types/cache/memory-segment-store.d.ts +3 -2
  39. package/dist/types/cache/segment-codec.d.ts +2 -2
  40. package/dist/types/cache/shell-snapshot.d.ts +1 -1
  41. package/dist/types/cache/types.d.ts +12 -7
  42. package/dist/types/cache/vercel/vercel-cache-store.d.ts +2 -1
  43. package/dist/types/client-urls/types.d.ts +7 -5
  44. package/dist/types/prefetch-markers.d.ts +9 -0
  45. package/dist/types/prefetch-source-scope.d.ts +66 -0
  46. package/dist/types/route-definition/helpers-types.d.ts +8 -5
  47. package/dist/types/router/intercept-resolution.d.ts +25 -11
  48. package/dist/types/router/match-context.d.ts +1 -0
  49. package/dist/types/router/match-handlers.d.ts +1 -1
  50. package/dist/types/router/prefetch-cache-ttl.d.ts +11 -3
  51. package/dist/types/router/router-context.d.ts +1 -1
  52. package/dist/types/router/router-options.d.ts +6 -3
  53. package/dist/types/router/segment-resolution/loader-cache.d.ts +8 -0
  54. package/dist/types/router/segment-resolution/prefetch-deferral.d.ts +23 -7
  55. package/dist/types/router/segment-resolution/revalidation.d.ts +19 -0
  56. package/dist/types/router/source-reads.d.ts +53 -0
  57. package/dist/types/router/types.d.ts +1 -1
  58. package/dist/types/rsc/helpers.d.ts +5 -2
  59. package/dist/types/rsc/prefetch-render-error.d.ts +7 -2
  60. package/dist/types/rsc/request-stages.d.ts +2 -0
  61. package/dist/types/rsc/shell-serve.d.ts +6 -1
  62. package/dist/types/server/context.d.ts +23 -6
  63. package/dist/types/server/cookie-store.d.ts +15 -1
  64. package/dist/types/server/document-location-state.d.ts +5 -2
  65. package/dist/types/server/handle-store.d.ts +2 -0
  66. package/dist/types/server/request-context.d.ts +43 -7
  67. package/dist/types/testing/e2e/server.d.ts +16 -1
  68. package/dist/types/testing/render-route.d.ts +66 -2
  69. package/dist/types/testing/serve-shell-request.d.ts +6 -3
  70. package/dist/types/types/handler-context.d.ts +7 -5
  71. package/dist/types/types/loader-types.d.ts +7 -4
  72. package/dist/types/types/segments.d.ts +15 -4
  73. package/dist/types/urls/path-helper-types.d.ts +7 -5
  74. package/dist/types/vite/temp-rsc-server.d.ts +2 -2
  75. package/dist/vite/index.js +449 -189
  76. package/package.json +2 -2
  77. package/skills/app-structure/SKILL.md +74 -0
  78. package/skills/app-structure/e-commerce.md +10 -9
  79. package/skills/app-structure/large-app.md +15 -2
  80. package/skills/caching/SKILL.md +10 -5
  81. package/skills/client-urls/SKILL.md +18 -11
  82. package/skills/comparison/references/framework-comparison.md +13 -8
  83. package/skills/document-cache/SKILL.md +8 -0
  84. package/skills/hooks/navigation.md +8 -8
  85. package/skills/hooks/state.md +24 -8
  86. package/skills/intercept/SKILL.md +33 -12
  87. package/skills/layout/SKILL.md +11 -0
  88. package/skills/links/SKILL.md +101 -4
  89. package/skills/loader/SKILL.md +49 -22
  90. package/skills/migrate-nextjs/SKILL.md +8 -2
  91. package/skills/migrate-react-router/data-and-actions.md +5 -0
  92. package/skills/ppr/SKILL.md +26 -5
  93. package/skills/prerender/SKILL.md +4 -1
  94. package/skills/route/SKILL.md +6 -6
  95. package/skills/router-setup/SKILL.md +2 -1
  96. package/skills/server-actions/SKILL.md +41 -8
  97. package/skills/testing/client-components.md +29 -13
  98. package/skills/testing/serve-shell-request.md +7 -7
  99. package/skills/testing/server-actions.md +8 -0
  100. package/skills/ux-architecture/SKILL.md +67 -18
  101. package/src/bin/rango.ts +39 -13
  102. package/src/browser/action-fence.ts +72 -0
  103. package/src/browser/cookie-name.ts +82 -14
  104. package/src/browser/invalidate-client-cache.ts +9 -7
  105. package/src/browser/navigation-bridge.ts +164 -32
  106. package/src/browser/navigation-client.ts +95 -20
  107. package/src/browser/navigation-store-handle.ts +67 -5
  108. package/src/browser/navigation-store.ts +86 -17
  109. package/src/browser/navigation-transaction.ts +0 -6
  110. package/src/browser/partial-update.ts +31 -12
  111. package/src/browser/prefetch/cache.ts +188 -11
  112. package/src/browser/prefetch/fetch.ts +211 -51
  113. package/src/browser/prefetch/queue.ts +3 -2
  114. package/src/browser/rango-state.ts +50 -8
  115. package/src/browser/react/Link.tsx +20 -9
  116. package/src/browser/react/use-navigation.ts +1 -1
  117. package/src/browser/rsc-router.tsx +8 -1
  118. package/src/browser/server-action-bridge.ts +3 -3
  119. package/src/browser/types.ts +25 -0
  120. package/src/build/generate-route-types.ts +1 -0
  121. package/src/build/route-types/include-resolution.ts +40 -3
  122. package/src/build/route-types/router-processing.ts +170 -25
  123. package/src/build/route-types/scan-filter.ts +1 -0
  124. package/src/cache/cache-key-utils.ts +12 -8
  125. package/src/cache/cache-runtime.ts +5 -1
  126. package/src/cache/cache-scope.ts +28 -7
  127. package/src/cache/cf/cf-cache-types.ts +9 -5
  128. package/src/cache/document-cache-policy.ts +27 -0
  129. package/src/cache/document-cache.ts +5 -0
  130. package/src/cache/memory-segment-store.ts +3 -2
  131. package/src/cache/segment-codec.ts +2 -2
  132. package/src/cache/shell-snapshot.ts +1 -1
  133. package/src/cache/types.ts +12 -7
  134. package/src/cache/vercel/vercel-cache-store.ts +2 -1
  135. package/src/client-urls/types.ts +7 -5
  136. package/src/prefetch-markers.ts +10 -0
  137. package/src/prefetch-source-scope.ts +137 -0
  138. package/src/route-definition/helpers-types.ts +8 -5
  139. package/src/router/intercept-resolution.ts +94 -50
  140. package/src/router/loader-resolution.ts +8 -6
  141. package/src/router/match-api.ts +33 -29
  142. package/src/router/match-context.ts +5 -0
  143. package/src/router/match-handlers.ts +2 -1
  144. package/src/router/match-middleware/cache-lookup.ts +21 -13
  145. package/src/router/match-result.ts +39 -87
  146. package/src/router/navigation-snapshot.ts +18 -3
  147. package/src/router/prefetch-cache-ttl.ts +15 -3
  148. package/src/router/revalidation.ts +53 -23
  149. package/src/router/router-context.ts +2 -1
  150. package/src/router/router-options.ts +6 -3
  151. package/src/router/segment-resolution/loader-cache.ts +21 -3
  152. package/src/router/segment-resolution/prefetch-deferral.ts +38 -31
  153. package/src/router/segment-resolution/revalidation.ts +40 -12
  154. package/src/router/source-reads.ts +122 -0
  155. package/src/router/types.ts +2 -1
  156. package/src/router.ts +11 -12
  157. package/src/rsc/helpers.ts +8 -3
  158. package/src/rsc/prefetch-render-error.ts +7 -2
  159. package/src/rsc/progressive-enhancement.ts +3 -2
  160. package/src/rsc/request-stages.ts +59 -0
  161. package/src/rsc/rsc-rendering.ts +49 -33
  162. package/src/rsc/server-action.ts +2 -1
  163. package/src/rsc/shell-capture.ts +4 -5
  164. package/src/rsc/shell-serve.ts +12 -1
  165. package/src/segment-system.tsx +1 -1
  166. package/src/server/context.ts +37 -6
  167. package/src/server/cookie-store.ts +22 -1
  168. package/src/server/document-location-state.ts +23 -2
  169. package/src/server/handle-store.ts +2 -0
  170. package/src/server/request-context.ts +113 -28
  171. package/src/testing/dispatch.ts +3 -0
  172. package/src/testing/e2e/fixture.ts +4 -12
  173. package/src/testing/e2e/server.ts +91 -4
  174. package/src/testing/render-route.tsx +269 -4
  175. package/src/testing/serve-shell-request.ts +6 -3
  176. package/src/types/handler-context.ts +7 -5
  177. package/src/types/loader-types.ts +7 -4
  178. package/src/types/segments.ts +15 -4
  179. package/src/urls/path-helper-types.ts +7 -5
  180. package/src/vite/temp-rsc-server.ts +2 -2
@@ -102,6 +102,7 @@ import { contextGet } from "../context-var.js";
102
102
  import { rewriteShellAudience, rewriteTrail } from "../server/rewrite-trail.js";
103
103
  import { visitorShellKey } from "./shell-capture-constants.js";
104
104
  import { PREFETCH_HEADER } from "../prefetch-markers.js";
105
+ import { isPrefetchKind } from "../router/segment-resolution/prefetch-deferral.js";
105
106
  import { WIRE } from "../wire-params.js";
106
107
  import {
107
108
  isWarmReplace,
@@ -140,6 +141,7 @@ import {
140
141
  SEGMENT_FRAGMENT_CAPABILITY_HEADER,
141
142
  SEGMENT_FRAGMENT_RECOVERY_HEADER,
142
143
  } from "../segment-fragments.js";
144
+ import { prefetchScopeOf } from "../router/source-reads.js";
143
145
 
144
146
  type PprReplayStatus =
145
147
  | { outcome: "HIT"; freshness: "fresh" | "stale" }
@@ -361,10 +363,10 @@ type PreparedRender =
361
363
  kind: "payload";
362
364
  payload: RscPayload;
363
365
  /**
364
- * The response depends on the source page: an intercept targets the
365
- * route, or it carries deferred units (`prefetch: false`).
366
+ * `x-rsc-prefetch-scope`, when the response depends on the source
367
+ * page (prefetchScopeOf).
366
368
  */
367
- sourceScoped: boolean;
369
+ prefetchScope: string | undefined;
368
370
  pprReplayStatus?: PprReplayStatus;
369
371
  /** The key a navigation-only heal capture stores under, when one is needed. */
370
372
  healKey?: string;
@@ -857,7 +859,7 @@ function* preparePayloadPlan<TEnv>(
857
859
  return {
858
860
  kind: "payload",
859
861
  payload: buildFullPayload(match, ctx, url, reqCtx, handleStore),
860
- sourceScoped: false,
862
+ prefetchScope: undefined,
861
863
  pprReplayStatus,
862
864
  healKey,
863
865
  };
@@ -869,6 +871,7 @@ function* preparePayloadPlan<TEnv>(
869
871
  // showed at once, and ran held work again.
870
872
  const deferring = result.segments.some((s) => s.deferred);
871
873
  if (deferring) reqCtx._payloadDeferred = true;
874
+ if (result.fromStorage) reqCtx._servedFromStorage = true;
872
875
  return {
873
876
  kind: "payload",
874
877
  payload: {
@@ -898,10 +901,11 @@ function* preparePayloadPlan<TEnv>(
898
901
  stateCookieName: ctx.router.resolvedStateCookieName,
899
902
  },
900
903
  },
901
- sourceScoped:
902
- result.interceptTargeted === true ||
903
- result.sourceHeld === true ||
904
- deferring,
904
+ prefetchScope: prefetchScopeOf(
905
+ result.interceptTargeted === true || deferring,
906
+ result.sourceHeld,
907
+ result.diff.length === 0,
908
+ ),
905
909
  pprReplayStatus,
906
910
  healKey,
907
911
  };
@@ -925,7 +929,7 @@ function* preparePayloadPlan<TEnv>(
925
929
  return {
926
930
  kind: "payload",
927
931
  payload: buildFullPayload(match, ctx, url, reqCtx, handleStore),
928
- sourceScoped: false,
932
+ prefetchScope: undefined,
929
933
  };
930
934
  }
931
935
 
@@ -1111,7 +1115,7 @@ function renderPreparedRscResponse<TEnv>(
1111
1115
  ): Promise<Response> {
1112
1116
  const { ctx, request, env, url, isPartial, nonce, reqCtx, renderSpan } =
1113
1117
  input;
1114
- const { payload, pprReplayStatus, sourceScoped } = prepared;
1118
+ const { payload, pprReplayStatus, prefetchScope } = prepared;
1115
1119
  const metricsStore = reqCtx._metricsStore;
1116
1120
 
1117
1121
  const rscHeaders: Record<string, string> = {
@@ -1128,24 +1132,24 @@ function renderPreparedRscResponse<TEnv>(
1128
1132
  serializePprReplayStatus(pprReplayStatus);
1129
1133
  }
1130
1134
  // Tell the client's prefetch cache to scope this response to its source
1131
- // URL (instead of the default source-agnostic wildcard). A route an
1132
- // intercept targets renders the modal or the full page depending on the
1133
- // source, so neither response may be reused from another source: a full
1134
- // page prefetched where the intercept does not apply would otherwise
1135
- // serve a later click where it does (#1007). So is a diff a revalidate()
1136
- // decided from the source page: another page adopting it would keep its
1137
- // own copy of the segment the answer held (#1331).
1138
- if (sourceScoped) {
1139
- rscHeaders["x-rsc-prefetch-scope"] = "source";
1135
+ // (instead of the default source-agnostic wildcard). See prefetchScopeOf.
1136
+ if (prefetchScope !== undefined) {
1137
+ rscHeaders["x-rsc-prefetch-scope"] = prefetchScope;
1138
+ // A shared cache keys a partial by URL and the ids the client holds, not
1139
+ // by its source page (document-cache.ts): stored, a scoped answer would
1140
+ // serve its diff to another page that holds the same ids.
1141
+ reqCtx._payloadSourceScoped = true;
1140
1142
  }
1141
- // prefetch: false (docs/design/prefetch-false.md, R7). A response that
1142
- // carries deferred units answers one page's prefetch, and a fill is the
1143
- // second half of one adoption: neither is reused by a shared cache or the
1144
- // document cache (which also refuses a payload marked `_payloadDeferred`),
1145
- // whatever Cache-Control the route set; so is a stale partial render, which
1146
- // a shared cache keyed by URL must not store or serve. A route with no flag
1147
- // keeps its headers.
1143
+ // Not reused by a shared cache or the document cache (which also refuses a
1144
+ // payload marked `_payloadDeferred` or `_payloadSourceScoped`), whatever
1145
+ // Cache-Control the route set: a partial scoped to its source page (above);
1146
+ // a response that carries deferred units, which answers one page's prefetch,
1147
+ // and a fill, the second half of one adoption (prefetch: false,
1148
+ // docs/design/prefetch-false.md, R7); and a stale partial render, which a
1149
+ // shared cache keyed by URL must not store or serve. Any other route keeps
1150
+ // its headers.
1148
1151
  if (
1152
+ reqCtx._payloadSourceScoped ||
1149
1153
  reqCtx._payloadDeferred ||
1150
1154
  (isPartial &&
1151
1155
  (reqCtx._requestKind === "fill" ||
@@ -1195,7 +1199,7 @@ function renderPreparedRscResponse<TEnv>(
1195
1199
  },
1196
1200
  );
1197
1201
  // A prefetch whose render reported an error is not kept (#1170).
1198
- if (reqCtx._requestKind !== "prefetch" || !isPartial) return rendered;
1202
+ if (!isPrefetchKind(reqCtx._requestKind) || !isPartial) return rendered;
1199
1203
  return rendered.then((response) =>
1200
1204
  markPrefetchOnRenderError(response, reqCtx._renderErrors ?? []),
1201
1205
  );
@@ -1735,8 +1739,14 @@ function serveShellHit(
1735
1739
  // request's own context and before the response leaves (the tail runs on a
1736
1740
  // derived context, after an await), so the response goes
1737
1741
  // `private, no-store` and stays out of the document cache
1738
- // (server/document-location-state.ts).
1739
- if ((reqCtx._locationState?.length ?? 0) > 0) {
1742
+ // (server/document-location-state.ts). The tail carries exactly this
1743
+ // state: a write during the tail (a loader's, through getRequestContext())
1744
+ // comes after these headers chose the response's caching, so it is not
1745
+ // sent, as the skill documents (skills/hooks/state.md).
1746
+ // Never undefined: attachLocationStateIfPresent would read the tail's
1747
+ // context instead.
1748
+ const tailLocationState = reqCtx._locationState ?? [];
1749
+ if (tailLocationState.length > 0) {
1740
1750
  reqCtx._documentLocationState = true;
1741
1751
  }
1742
1752
  const metricsStore = reqCtx._metricsStore;
@@ -1766,7 +1776,7 @@ function serveShellHit(
1766
1776
  reportCacheError(error, "cache-write", "[ShellServe] retire");
1767
1777
  }
1768
1778
  };
1769
- /** The first loader pin of the entry that failed to decode (dc-t89). */
1779
+ /** The first loader pin of the entry that failed to decode. */
1770
1780
  let undecodablePin: string | undefined;
1771
1781
 
1772
1782
  const renderTail = async (
@@ -1797,7 +1807,7 @@ function serveShellHit(
1797
1807
  payload.metadata.pathname = entry.pathname ?? url.pathname;
1798
1808
  // The visitor's location state, as a MISS document's payload carries
1799
1809
  // it. The request was marked per visitor above.
1800
- attachLocationStateIfPresent(payload);
1810
+ attachLocationStateIfPresent(payload, false, tailLocationState);
1801
1811
  }
1802
1812
  // Full Flight render per request: hydration needs the whole payload (there
1803
1813
  // is no Flight-side resume — a React limitation, not ours).
@@ -2061,7 +2071,13 @@ function serveShellHit(
2061
2071
  "the doc record could not supply the handler layer",
2062
2072
  );
2063
2073
  }
2064
- controller.enqueue(new TextEncoder().encode(shellReloadScript()));
2074
+ // A document redirect's state cookie: this response's headers
2075
+ // cleared it, and the reload must read it again.
2076
+ controller.enqueue(
2077
+ new TextEncoder().encode(
2078
+ shellReloadScript(reqCtx._carriedLocationStateCookie),
2079
+ ),
2080
+ );
2065
2081
  if (tailTiming) {
2066
2082
  tailTiming.outcome = "error";
2067
2083
  tailTiming.completeMs = Math.round(performance.now() - tailT0);
@@ -2087,7 +2103,7 @@ function serveShellHit(
2087
2103
  }
2088
2104
  // A pin that failed to decode left its loader to this request's run:
2089
2105
  // this response hydrated once over a prelude rendered from the
2090
- // capture's value (dc-t89). Retire the entry once the response is
2106
+ // capture's value. Retire the entry once the response is
2091
2107
  // closed, and back the key off so a pin that keeps failing costs one
2092
2108
  // mismatch per window. No reload: that would cost a second page load.
2093
2109
  if (undecodablePin !== undefined) {
@@ -22,6 +22,7 @@ import {
22
22
  setRequestContextParams,
23
23
  } from "../server/request-context.js";
24
24
  import { printServerError } from "../server/print-server-error.js";
25
+ import { runActionBody } from "../server/cookie-store.js";
25
26
  import { observePhase, PHASES } from "../router/instrument.js";
26
27
  import { resolveActionRefId } from "../router/is-action.js";
27
28
  import { getInterceptSourceHeader } from "../router/navigation-snapshot.js";
@@ -203,7 +204,7 @@ export async function executeServerAction<TEnv>(
203
204
 
204
205
  try {
205
206
  loadedAction = await ctx.loadServerAction(actionId);
206
- let data = await loadedAction!.apply(null, args);
207
+ let data = await runActionBody(() => loadedAction!.apply(null, args));
207
208
 
208
209
  // Intercept redirect Responses: serializing one as the action returnValue
209
210
  // would fail, and revalidation would run needlessly.
@@ -552,7 +552,7 @@ const SNAPSHOT_BYTE_ENCODER = new TextEncoder();
552
552
 
553
553
  /**
554
554
  * Warn once per key that the capture's loader pins exceeded the route's
555
- * `maxSnapshotBytes` cap, so no document shell was stored (dc-t89). Once per
555
+ * `maxSnapshotBytes` cap, so no document shell was stored. Once per
556
556
  * key: the route recaptures every backoff window.
557
557
  */
558
558
  function warnSnapshotOverCapOnce(
@@ -2603,8 +2603,7 @@ async function captureAndStoreShell(
2603
2603
  throw new Error(`[rango] no shell pin for "${segmentKey}"`);
2604
2604
  }
2605
2605
  } catch (error) {
2606
- // Unpinned, the loader would run on every HIT over this prelude
2607
- // (dc-t89): refuse like an encode error.
2606
+ // Unpinned, the loader would run on every HIT over this prelude: refuse like an encode error.
2608
2607
  reqCtx._renderErrors?.push(error);
2609
2608
  }
2610
2609
  }
@@ -2650,7 +2649,7 @@ async function captureAndStoreShell(
2650
2649
  }
2651
2650
 
2652
2651
  // Snapshot size guard: `maxSnapshotBytes` bounds the loader pins, and a
2653
- // document HIT needs every pin its prelude baked (dc-t89). Over the cap
2652
+ // document HIT needs every pin its prelude baked. Over the cap
2654
2653
  // the entry keeps the navigation half only (the doc record, or nothing:
2655
2654
  // a tombstone that also shadows a build shell) and the attempt refuses.
2656
2655
  // The whole entry is bounded separately below, by the store's limit.
@@ -2897,7 +2896,7 @@ export {
2897
2896
 
2898
2897
  // Exported for unit tests that pin the refused-capture backoff policy directly
2899
2898
  // (dev cap vs production exponential growth, stored-clears, cold-start re-probe),
2900
- // and markCaptureBackoff for serveShellHit (a pin that failed to decode, dc-t89).
2899
+ // and markCaptureBackoff for serveShellHit (a pin that failed to decode).
2901
2900
  // These are the same module-level functions the schedule path uses; a test that
2902
2901
  // drove them through a real capture round-trip could not assert the exact window
2903
2902
  // arithmetic without a full cold render.
@@ -38,6 +38,7 @@ import {
38
38
  import { routerKeyPrefix } from "../cache/cache-key-utils.js";
39
39
  import { reportCacheError } from "../cache/cache-error.js";
40
40
  import { appendMetric } from "../router/metrics.js";
41
+ import { escapeJsonForScript } from "../escape-script.js";
41
42
  import type { MetricsStore } from "../server/context.js";
42
43
  import { WIRE } from "../wire-params.js";
43
44
 
@@ -162,12 +163,22 @@ export function withoutShellMissMarker(request: Request): Request | undefined {
162
163
  * the page. window.stop() runs only when it reloads: it keeps the half-sent
163
164
  * Flight stream from being closed by DOMContentLoaded, which would throw
164
165
  * React's "Connection closed" (#412) before the reload lands.
166
+ *
167
+ * `restoreCookie` is the request's `_carriedLocationStateCookie`: the HIT's
168
+ * headers already cleared the location state cookie a document redirect left
169
+ * (server/document-location-state.ts), so the script sets it again before it
170
+ * reloads, and the reload carries the state.
165
171
  */
166
- export function shellReloadScript(): string {
172
+ export function shellReloadScript(restoreCookie?: string): string {
173
+ const restore =
174
+ restoreCookie === undefined
175
+ ? ""
176
+ : `document.cookie=${escapeJsonForScript(JSON.stringify(restoreCookie))};`;
167
177
  return inlineShellScript(
168
178
  "(function(){var u=new URL(location.href);" +
169
179
  `if(u.searchParams.has(${JSON.stringify(WIRE.shellMiss)}))return;` +
170
180
  `u.searchParams.set(${JSON.stringify(WIRE.shellMiss)},"miss");` +
181
+ restore +
171
182
  "try{window.stop()}catch(e){}location.replace(u.href)})()",
172
183
  );
173
184
  }
@@ -318,7 +318,7 @@ function wrapDefaultOutletContent(
318
318
  * is settled like a flagged one, so the commit shows no fallback. A loader
319
319
  * error is left to its read site there, its errorBoundary() fallback riding
320
320
  * the throw, as on a navigation: the nearest boundary to the read catches it
321
- * (#1129, dc-t65). notFound()/redirect() keep their build-time node.
321
+ * (#1129). notFound()/redirect() keep their build-time node.
322
322
  */
323
323
  async function buildLoaderStreams(
324
324
  loaders: ResolvedSegment[],
@@ -126,9 +126,10 @@ export type EntryPropRender = {
126
126
  loading?: ReactNode | false;
127
127
  /**
128
128
  * loading(fallback, { prefetch: false }) with a renderable fallback: a
129
- * prefetch defers what the fallback covers while the entry's segment is
130
- * new to the client (router/segment-resolution/prefetch-deferral.ts). Set
131
- * on both DSL evaluations, whatever `ssr` did to `loading`.
129
+ * speculative prefetch defers what the fallback covers while the entry's
130
+ * segment is new to the client
131
+ * (router/segment-resolution/prefetch-deferral.ts). Set on both DSL
132
+ * evaluations, whatever `ssr` did to `loading`.
132
133
  */
133
134
  loadingPrefetch?: false;
134
135
  /**
@@ -171,8 +172,8 @@ export type LoaderEntry = {
171
172
  */
172
173
  bake?: true;
173
174
  /**
174
- * loader(Def, { prefetch: false }): a prefetch defers this registration
175
- * while its segment is new to the client
175
+ * loader(Def, { prefetch: false }): a speculative prefetch defers this
176
+ * registration while its segment is new to the client
176
177
  * (router/segment-resolution/prefetch-deferral.ts).
177
178
  */
178
179
  prefetch?: false;
@@ -211,7 +212,10 @@ export type InterceptSegmentsState = {
211
212
  * @internal This type is an implementation detail and may change without notice.
212
213
  */
213
214
  export type InterceptSelectorContext<TEnv = any> = {
214
- /** Where the navigation comes from: the intercept source while one is open. */
215
+ /**
216
+ * Where the navigation comes from: the intercept source while one is open.
217
+ * Its `url` never carries a #fragment.
218
+ */
215
219
  from: ServerRouteLocation;
216
220
  /** The navigation target. */
217
221
  to: ServerRouteLocation;
@@ -1719,6 +1723,33 @@ export function isInsideLoaderBody(loaderId: string): boolean {
1719
1723
  return false;
1720
1724
  }
1721
1725
 
1726
+ // The segment of the streamed handler or loader body running here; the
1727
+ // walker moves the shared `_currentSegmentId` on without awaiting either.
1728
+ const segmentPinALS: AsyncLocalStorage<string | undefined> = ((
1729
+ globalThis as any
1730
+ )[Symbol.for("rangojs-router:segment-pin")] ??= new AsyncLocalStorage<
1731
+ string | undefined
1732
+ >());
1733
+
1734
+ /**
1735
+ * Run `fn` as segment `segmentId`'s code: a handle push, a loader kickoff or
1736
+ * a "use cache" call it makes, before or after an await, files under
1737
+ * `segmentId` (segmentPinOf).
1738
+ */
1739
+ export function runAsSegment<R>(segmentId: string | undefined, fn: () => R): R {
1740
+ return segmentPinALS.run(segmentId, fn);
1741
+ }
1742
+
1743
+ /**
1744
+ * The segment code here runs for: the innermost runAsSegment scope, else
1745
+ * `ctx`'s pin (a handler the walker awaits).
1746
+ */
1747
+ export function segmentPinOf(ctx: {
1748
+ _currentSegmentId?: string;
1749
+ }): string | undefined {
1750
+ return segmentPinALS.getStore() ?? ctx._currentSegmentId;
1751
+ }
1752
+
1722
1753
  // Scope for handle PUSH CALLBACKS (push(() => ...), including async ones).
1723
1754
  // A push callback's value is stored as-is; if it is a promise it is NOT tracked
1724
1755
  // by handleStore.settled and does not block segment resolution, so a
@@ -330,7 +330,8 @@ export function invalidateClientCache(): void {
330
330
  *
331
331
  * Sets an internal response header the bridge reads. Idempotent within a
332
332
  * request. Inert (a dev warning) outside a request context — there is no
333
- * automatic invalidation to suppress.
333
+ * automatic invalidation to suppress. It also keeps the state's write part
334
+ * (runActionBody).
334
335
  */
335
336
  export function keepClientCache(): void {
336
337
  const ctx = _getRequestContext();
@@ -347,6 +348,26 @@ export function keepClientCache(): void {
347
348
  ctx._setKeepCacheDirective();
348
349
  }
349
350
 
351
+ /**
352
+ * Run a server action's body (the JS call or a no-JS form post), then bump the
353
+ * rango state's write part for the client that called it: the response's
354
+ * state `Set-Cookie` keeps the client's timestamp and carries a new write part
355
+ * (browser/cookie-name.ts). Every later request of every tab and reload sends
356
+ * the new `X-Rango-State`, so a prefetch or navigation the browser HTTP cache
357
+ * holds from before the write (a response's own `Cache-Control: max-age`)
358
+ * misses, while the client's in-memory caches only go stale, as the action
359
+ * bridge marks them. Bumped however the body ends (returned, threw, or
360
+ * redirected), since the write may have committed either way, and not after
361
+ * keepClientCache() or invalidateClientCache() (whose rotation covers it).
362
+ */
363
+ export async function runActionBody<T>(body: () => T | Promise<T>): Promise<T> {
364
+ try {
365
+ return await body();
366
+ } finally {
367
+ _getRequestContext()?._rotateStateCookie("write");
368
+ }
369
+ }
370
+
350
371
  /**
351
372
  * Create a CookieStore backed by a RequestContext. Reads go through the
352
373
  * context's unguarded `_readCookie()` / `_readCookies()`: cookies() ran the
@@ -14,7 +14,10 @@
14
14
  * reads it on the next GET document request that navigates to the target
15
15
  * pathname (`Sec-Fetch-Dest` a browsing context, where sent), seeds
16
16
  * `_locationState` with its entries (so the render attaches them to the
17
- * bootstrap payload, rsc/rsc-rendering.ts) and clears it.
17
+ * bootstrap payload, rsc/rsc-rendering.ts) and clears it. A shell HIT that
18
+ * degrades into a reload after its headers cleared the cookie sets it
19
+ * again from script first (`_carriedLocationStateCookie`), so the reload
20
+ * carries the state.
18
21
  *
19
22
  * A document that carries location state is per visitor: it is marked
20
23
  * `_documentLocationState`, never stored by the document cache or as a PPR
@@ -136,7 +139,10 @@ function isNavigationRequest(request: Request): boolean {
136
139
  * slots are taken from it (the value comes from the browser).
137
140
  */
138
141
  export function consumeDocumentLocationState(
139
- reqCtx: Pick<RequestContext, "_locationState" | "setCookie">,
142
+ reqCtx: Pick<
143
+ RequestContext,
144
+ "_locationState" | "_carriedLocationStateCookie" | "setCookie"
145
+ >,
140
146
  request: Request,
141
147
  url: URL,
142
148
  stateCookieName: string,
@@ -159,6 +165,21 @@ export function consumeDocumentLocationState(
159
165
  }
160
166
  if (entries.length === 0) return;
161
167
  reqCtx._locationState = entries;
168
+ reqCtx._carriedLocationStateCookie = restoreCookieLine(name, raw, secure);
169
+ }
170
+
171
+ /**
172
+ * The `document.cookie` line that sets the consumed cookie again, for a
173
+ * shell HIT that reloads after its headers cleared it (rsc/rsc-rendering.ts
174
+ * serveShellHit). Script cannot write an `HttpOnly` cookie, so this one is
175
+ * readable by the page's script for its short life; it carries nothing the
176
+ * page's payload does not already hold.
177
+ */
178
+ function restoreCookieLine(name: string, raw: string, secure: boolean): string {
179
+ return (
180
+ `${name}=${raw}; Path=/; Max-Age=${DOCUMENT_LOCATION_STATE_MAX_AGE}; ` +
181
+ `SameSite=Lax${secure ? "; Secure" : ""}`
182
+ );
162
183
  }
163
184
 
164
185
  /** JSON keeps it as it is: what the cookie can carry without change. */
@@ -302,6 +302,8 @@ export interface HandleStore {
302
302
  * so a loader that throws or skips its push shows none rather than the
303
303
  * record's. A hole's replays go with them. Any other loader's replays stay
304
304
  * (a dependency another entry's HIT replayed), and so do restored copies.
305
+ * A prefetch that defers the loader calls it too (revalidation.ts
306
+ * resolveOwnerLoaders): no run is made, so none of its placeholders stand.
305
307
  */
306
308
  settleLoaderRun(loaderId: string): void;
307
309