@rangojs/router 0.0.0-experimental.e9c0b2f2 → 0.0.0-experimental.ea9f40f2

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 (222) hide show
  1. package/AGENTS.md +6 -10
  2. package/README.md +289 -938
  3. package/dist/bin/rango.js +271 -46
  4. package/dist/vite/index.js +673 -193
  5. package/package.json +10 -8
  6. package/skills/api-client/SKILL.md +1 -1
  7. package/skills/breadcrumbs/SKILL.md +31 -14
  8. package/skills/cache-guide/SKILL.md +5 -2
  9. package/skills/caching/SKILL.md +59 -4
  10. package/skills/catalog.json +271 -0
  11. package/skills/comparison/SKILL.md +50 -0
  12. package/skills/comparison/agents/openai.yaml +4 -0
  13. package/skills/comparison/references/framework-comparison.md +837 -0
  14. package/skills/composability/SKILL.md +83 -2
  15. package/skills/debug-manifest/SKILL.md +1 -1
  16. package/skills/defer-hydration/SKILL.md +235 -0
  17. package/skills/document-cache/SKILL.md +9 -1
  18. package/skills/fonts/SKILL.md +1 -1
  19. package/skills/handler-use/SKILL.md +8 -8
  20. package/skills/hooks/SKILL.md +54 -892
  21. package/skills/hooks/data.md +273 -0
  22. package/skills/hooks/handle-and-actions.md +103 -0
  23. package/skills/hooks/navigation.md +110 -0
  24. package/skills/hooks/outlets.md +41 -0
  25. package/skills/hooks/state.md +228 -0
  26. package/skills/hooks/urls.md +135 -0
  27. package/skills/host-router/SKILL.md +4 -4
  28. package/skills/i18n/SKILL.md +1 -1
  29. package/skills/intercept/SKILL.md +46 -14
  30. package/skills/layout/SKILL.md +27 -10
  31. package/skills/links/SKILL.md +1 -1
  32. package/skills/loader/SKILL.md +23 -1
  33. package/skills/middleware/SKILL.md +7 -3
  34. package/skills/migrate-nextjs/SKILL.md +167 -6
  35. package/skills/migrate-react-router/SKILL.md +59 -677
  36. package/skills/migrate-react-router/cloudflare-workers.md +129 -0
  37. package/skills/migrate-react-router/component-migration.md +196 -0
  38. package/skills/migrate-react-router/data-and-actions.md +225 -0
  39. package/skills/migrate-react-router/route-mapping.md +271 -0
  40. package/skills/mime-routes/SKILL.md +1 -1
  41. package/skills/observability/SKILL.md +9 -1
  42. package/skills/parallel/SKILL.md +23 -4
  43. package/skills/ppr/SKILL.md +622 -0
  44. package/skills/prerender/SKILL.md +28 -18
  45. package/skills/rango/SKILL.md +84 -25
  46. package/skills/response-routes/SKILL.md +15 -1
  47. package/skills/route/SKILL.md +71 -4
  48. package/skills/router-setup/SKILL.md +14 -3
  49. package/skills/scripts/SKILL.md +1 -1
  50. package/skills/server-actions/SKILL.md +3 -2
  51. package/skills/shell-manifest/SKILL.md +185 -0
  52. package/skills/streams-and-websockets/SKILL.md +1 -1
  53. package/skills/tailwind/SKILL.md +1 -1
  54. package/skills/testing/SKILL.md +2 -1
  55. package/skills/testing/handles.md +4 -2
  56. package/skills/testing/render-handler.md +15 -14
  57. package/skills/testing/reverse-and-types.md +8 -7
  58. package/skills/theme/SKILL.md +1 -1
  59. package/skills/typesafety/SKILL.md +45 -919
  60. package/skills/typesafety/env-and-bindings.md +254 -0
  61. package/skills/typesafety/generated-files-and-cli.md +335 -0
  62. package/skills/typesafety/params-and-search.md +153 -0
  63. package/skills/typesafety/route-types.md +209 -0
  64. package/skills/use-cache/SKILL.md +30 -3
  65. package/skills/vercel/SKILL.md +1 -1
  66. package/skills/view-transitions/SKILL.md +44 -1
  67. package/src/browser/event-controller.ts +62 -10
  68. package/src/browser/logging.ts +28 -0
  69. package/src/browser/merge-segment-loaders.ts +6 -4
  70. package/src/browser/navigation-bridge.ts +65 -16
  71. package/src/browser/navigation-client.ts +32 -2
  72. package/src/browser/navigation-store.ts +128 -14
  73. package/src/browser/network-error-handler.ts +34 -7
  74. package/src/browser/partial-update.ts +76 -17
  75. package/src/browser/prefetch/cache.ts +51 -11
  76. package/src/browser/prefetch/fetch.ts +59 -21
  77. package/src/browser/prefetch/queue.ts +19 -4
  78. package/src/browser/react/Link.tsx +13 -3
  79. package/src/browser/react/NavigationProvider.tsx +108 -4
  80. package/src/browser/response-adapter.ts +38 -9
  81. package/src/browser/rsc-router.tsx +54 -4
  82. package/src/browser/scroll-restoration.ts +7 -5
  83. package/src/browser/segment-reconciler.ts +31 -21
  84. package/src/browser/server-action-bridge.ts +22 -10
  85. package/src/browser/types.ts +54 -1
  86. package/src/build/generate-manifest.ts +155 -131
  87. package/src/build/index.ts +3 -1
  88. package/src/build/route-trie.ts +35 -7
  89. package/src/build/route-types/include-resolution.ts +347 -47
  90. package/src/build/runtime-discovery.ts +4 -1
  91. package/src/cache/cache-key-utils.ts +29 -0
  92. package/src/cache/cache-runtime.ts +262 -71
  93. package/src/cache/cache-scope.ts +2 -17
  94. package/src/cache/cache-tag.ts +60 -14
  95. package/src/cache/cf/cf-cache-store.ts +243 -20
  96. package/src/cache/document-cache.ts +54 -21
  97. package/src/cache/index.ts +1 -0
  98. package/src/cache/memory-segment-store.ts +110 -3
  99. package/src/cache/profile-registry.ts +15 -0
  100. package/src/cache/read-through-swr.ts +15 -1
  101. package/src/cache/segment-codec.ts +4 -4
  102. package/src/cache/shell-snapshot.ts +417 -0
  103. package/src/cache/types.ts +158 -0
  104. package/src/cache/vercel/vercel-cache-store.ts +401 -124
  105. package/src/client.rsc.tsx +0 -3
  106. package/src/client.tsx +0 -3
  107. package/src/cloudflare/tracing.ts +7 -8
  108. package/src/defer.ts +11 -22
  109. package/src/handle.ts +37 -15
  110. package/src/handles/MetaTags.tsx +16 -82
  111. package/src/handles/breadcrumbs.ts +12 -14
  112. package/src/handles/deferred-resolution.ts +127 -0
  113. package/src/handles/is-thenable.ts +7 -8
  114. package/src/handles/meta.ts +7 -44
  115. package/src/host/errors.ts +15 -0
  116. package/src/host/index.ts +1 -0
  117. package/src/index.rsc.ts +8 -2
  118. package/src/index.ts +19 -13
  119. package/src/internal-debug.ts +11 -8
  120. package/src/prerender.ts +17 -4
  121. package/src/redirect-origin.ts +14 -0
  122. package/src/render-error-thrower.tsx +20 -0
  123. package/src/route-content-wrapper.tsx +12 -5
  124. package/src/route-definition/dsl-helpers.ts +21 -32
  125. package/src/route-definition/helper-factories.ts +0 -2
  126. package/src/route-definition/helpers-types.ts +43 -43
  127. package/src/route-definition/index.ts +1 -2
  128. package/src/route-definition/resolve-handler-use.ts +0 -1
  129. package/src/route-definition/use-item-types.ts +3 -6
  130. package/src/route-map-builder.ts +41 -4
  131. package/src/route-types.ts +0 -5
  132. package/src/router/find-match.ts +86 -8
  133. package/src/router/instrument.ts +9 -4
  134. package/src/router/lazy-includes.ts +72 -12
  135. package/src/router/loader-resolution.ts +14 -2
  136. package/src/router/manifest.ts +56 -11
  137. package/src/router/match-api.ts +76 -32
  138. package/src/router/match-handlers.ts +181 -135
  139. package/src/router/match-middleware/background-revalidation.ts +40 -23
  140. package/src/router/match-middleware/cache-store.ts +39 -24
  141. package/src/router/match-result.ts +35 -15
  142. package/src/router/middleware.ts +64 -38
  143. package/src/router/navigation-snapshot.ts +7 -5
  144. package/src/router/parse-pattern.ts +115 -0
  145. package/src/router/pattern-matching.ts +53 -64
  146. package/src/router/prefetch-limits.ts +37 -0
  147. package/src/router/prerender-match.ts +11 -5
  148. package/src/router/preview-match.ts +3 -1
  149. package/src/router/request-classification.ts +23 -8
  150. package/src/router/route-snapshot.ts +14 -2
  151. package/src/router/router-context.ts +3 -1
  152. package/src/router/router-interfaces.ts +32 -1
  153. package/src/router/router-options.ts +30 -0
  154. package/src/router/segment-resolution/fresh.ts +39 -3
  155. package/src/router/segment-resolution/loader-cache.ts +93 -2
  156. package/src/router/segment-resolution/loader-mask.ts +60 -0
  157. package/src/router/segment-resolution/loader-snapshot.ts +259 -0
  158. package/src/router/segment-resolution/mask-nested.ts +83 -0
  159. package/src/router/segment-resolution/revalidation.ts +3 -0
  160. package/src/router/segment-resolution/view-transition-default.ts +35 -15
  161. package/src/router/substitute-pattern-params.ts +54 -35
  162. package/src/router/telemetry-otel.ts +6 -8
  163. package/src/router/telemetry.ts +9 -1
  164. package/src/router/tracing.ts +14 -5
  165. package/src/router/trie-matching.ts +19 -11
  166. package/src/router/url-params.ts +13 -0
  167. package/src/router.ts +47 -16
  168. package/src/rsc/full-payload.ts +70 -0
  169. package/src/rsc/handler.ts +60 -33
  170. package/src/rsc/manifest-init.ts +1 -1
  171. package/src/rsc/nonce.ts +10 -1
  172. package/src/rsc/progressive-enhancement.ts +61 -4
  173. package/src/rsc/redirect-guard.ts +2 -1
  174. package/src/rsc/rsc-rendering.ts +429 -37
  175. package/src/rsc/server-action.ts +25 -2
  176. package/src/rsc/shell-capture.ts +1190 -0
  177. package/src/rsc/shell-serve.ts +181 -0
  178. package/src/rsc/transition-gate.ts +89 -0
  179. package/src/rsc/types.ts +30 -0
  180. package/src/segment-loader-promise.ts +18 -0
  181. package/src/segment-system.tsx +149 -14
  182. package/src/server/context.ts +67 -9
  183. package/src/server/cookie-store.ts +73 -1
  184. package/src/server/loader-registry.ts +13 -1
  185. package/src/server/request-context.ts +169 -10
  186. package/src/ssr/index.tsx +462 -178
  187. package/src/ssr/inject-rsc-eager.ts +167 -0
  188. package/src/ssr/ssr-root.tsx +228 -0
  189. package/src/testing/collect-handle.ts +14 -8
  190. package/src/testing/dispatch.ts +152 -40
  191. package/src/testing/generated-routes.ts +27 -11
  192. package/src/testing/index.ts +6 -0
  193. package/src/testing/render-handler.ts +14 -0
  194. package/src/testing/render-route.tsx +13 -10
  195. package/src/testing/run-transition-when.ts +164 -0
  196. package/src/theme/ThemeProvider.tsx +36 -26
  197. package/src/types/handler-context.ts +1 -1
  198. package/src/types/index.ts +2 -0
  199. package/src/types/route-config.ts +19 -7
  200. package/src/types/segments.ts +100 -0
  201. package/src/urls/include-helper.ts +10 -8
  202. package/src/urls/include-provider.ts +71 -0
  203. package/src/urls/index.ts +1 -0
  204. package/src/urls/path-helper-types.ts +44 -12
  205. package/src/urls/path-helper.ts +5 -0
  206. package/src/urls/pattern-types.ts +36 -0
  207. package/src/urls/type-extraction.ts +43 -18
  208. package/src/urls/urls-function.ts +0 -1
  209. package/src/vercel/tracing.ts +7 -7
  210. package/src/vite/discovery/dev-prerender-cache.ts +117 -0
  211. package/src/vite/discovery/discover-routers.ts +1 -1
  212. package/src/vite/discovery/discovery-errors.ts +61 -0
  213. package/src/vite/index.ts +7 -0
  214. package/src/vite/inject-client-debug.ts +88 -0
  215. package/src/vite/plugins/vercel-output.ts +114 -25
  216. package/src/vite/plugins/version-injector.ts +22 -7
  217. package/src/vite/plugins/virtual-entries.ts +80 -22
  218. package/src/vite/rango.ts +29 -19
  219. package/src/vite/router-discovery.ts +171 -43
  220. package/src/vite/utils/prerender-utils.ts +17 -4
  221. package/src/vite/utils/shared-utils.ts +47 -0
  222. package/src/network-error-thrower.tsx +0 -18
@@ -9,7 +9,11 @@
9
9
 
10
10
  import type { CookieOptions } from "../router/middleware-types.js";
11
11
  import { getRequestContext, _getRequestContext } from "./request-context.js";
12
- import { isInsideCacheScope } from "./context.js";
12
+ import {
13
+ isInsideCacheScope,
14
+ getCurrentLoaderBodyId,
15
+ isInsideHandlerInvokedLoaderBody,
16
+ } from "./context.js";
13
17
  import { INSIDE_CACHE_EXEC } from "../cache/taint.js";
14
18
 
15
19
  /**
@@ -62,6 +66,7 @@ export interface CookieStore {
62
66
  export function cookies(): CookieStore {
63
67
  const ctx = getRequestContext();
64
68
  assertNotInsideCacheContext(ctx, "cookies");
69
+ assertNotInsideShellCapture(ctx, "cookies");
65
70
  return createCookieStore(ctx);
66
71
  }
67
72
 
@@ -132,6 +137,72 @@ function assertNotInsideCacheContext(ctx: unknown, fnName: string): void {
132
137
  }
133
138
  }
134
139
 
140
+ /**
141
+ * Throw if called during the ACTIVE background shell-capture render
142
+ * (`_shellCaptureRun` true on the derived request context built by
143
+ * shell-capture.ts). The captured shell prelude is shared across every user
144
+ * hitting the URL, so a request-scoped read here would bake one user's
145
+ * cookies/headers into markup served to others — same hazard as the cache
146
+ * scopes above, at the document tier. DSL segment loaders need no exemption:
147
+ * the live lane is masked (never executed) during capture, and the bake lane
148
+ * is exactly what this guard exists for.
149
+ *
150
+ * HANDLER-INVOKED loader bodies (`await ctx.use(Loader)` from a handler) are
151
+ * EXEMPT — the consumption-lane rule: handler consumption yields a BAKED
152
+ * shared copy in every artifact tier, and the cache-purity guards above
153
+ * already permit identity reads there (cache()/"use cache" bake the same
154
+ * reads today). Guarding only the PPR tier made the same code legal under
155
+ * cache() but capture-refusing under ppr (issue #672 / #674). The trade is
156
+ * documented: an identity read in a handler-consumed loader bakes the CAPTURE
157
+ * request's value into the shared shell; client-side consumption (useLoader)
158
+ * is the live lane.
159
+ *
160
+ * Keys off `_shellCaptureRun`, NOT the `_shellCapture` descriptor: the descriptor
161
+ * is also present during the FOREGROUND render (it means "a capture is wanted"),
162
+ * and the foreground must read cookies/headers normally to serve the real user.
163
+ * Only the derived capture context sets `_shellCaptureRun`.
164
+ *
165
+ * Applies only to the READ surfaces (cookies(), headers()) whose values
166
+ * become markup. Response directives (invalidateClientCache(),
167
+ * keepClientCache()) stay callable: during capture they are header effects on
168
+ * a discarded response, and on the live HIT path the full pipeline runs so their
169
+ * headers flow to the client normally.
170
+ *
171
+ * The throw makes such a route PPR-ineligible by construction: the capture
172
+ * render errors, nothing is stored, and every request keeps getting the
173
+ * normal axis-1 render.
174
+ */
175
+ function assertNotInsideShellCapture(ctx: unknown, fnName: string): void {
176
+ if (
177
+ ctx !== null &&
178
+ typeof ctx === "object" &&
179
+ (ctx as { _shellCaptureRun?: unknown })._shellCaptureRun === true
180
+ ) {
181
+ if (isInsideHandlerInvokedLoaderBody()) return;
182
+ // Flag the capture context BEFORE throwing: inside an executing bake-lane
183
+ // loader this throw is swallowed by wrapLoaderPromise into per-loader error
184
+ // UI, which would bake silently into the shared shell. The capture checks
185
+ // the flag after the render and refuses (shell-capture.ts). Also record
186
+ // WHICH loader body (if any) made the read, so the refusal warning can
187
+ // name the real source instead of hardcoding a lane — the read may come
188
+ // from a bake-lane loader OR from handler/render code (issue #672).
189
+ (ctx as { _shellCaptureGuardTripped?: string })._shellCaptureGuardTripped =
190
+ fnName;
191
+ (
192
+ ctx as { _shellCaptureGuardTrippedLoaderId?: string }
193
+ )._shellCaptureGuardTrippedLoaderId = getCurrentLoaderBodyId();
194
+ throw new Error(
195
+ `${fnName}() cannot be called while capturing a shared shell ` +
196
+ `(shell-cache middleware). The captured shell is served to every user ` +
197
+ `of this URL, so request-scoped data read here would leak one user's ` +
198
+ `${fnName === "cookies" ? "cookies" : "headers"} to others. Read it ` +
199
+ `inside a loader instead — loaders are never captured and always run ` +
200
+ `fresh per request:\n\n` +
201
+ ` loader("user", () => getUser(cookies().get("session")?.value));`,
202
+ );
203
+ }
204
+ }
205
+
135
206
  const HEADERS_MUTATION_METHODS = new Set(["set", "append", "delete"]);
136
207
 
137
208
  /**
@@ -152,6 +223,7 @@ const HEADERS_MUTATION_METHODS = new Set(["set", "append", "delete"]);
152
223
  export function headers(): ReadonlyHeaders {
153
224
  const ctx = getRequestContext();
154
225
  assertNotInsideCacheContext(ctx, "headers");
226
+ assertNotInsideShellCapture(ctx, "headers");
155
227
  return new Proxy(ctx.request.headers, {
156
228
  get(target, prop, receiver) {
157
229
  if (typeof prop === "string" && HEADERS_MUTATION_METHODS.has(prop)) {
@@ -67,7 +67,19 @@ export async function getLoaderLazy(
67
67
  }
68
68
  }
69
69
 
70
- // Dev fallback: parse ID (format: "src/path/to/file.ts#ExportName") and import
70
+ // The remaining dev fallback (parse the id as "src/path/file.ts#ExportName"
71
+ // and import it by path) only makes sense in dev, where ids ARE file paths
72
+ // and the dev loader manifest is intentionally empty. In production ids are
73
+ // hashed ("<hash>#ExportName") and every resolvable loader is reached above
74
+ // via the in-memory registry or the lazy import manifest. The hash is not a
75
+ // path, so a production fall-through would run import("/<hash>") and throw a
76
+ // misleading "No such module <hash>" 500 instead of reporting the loader as
77
+ // unregistered. Return undefined in production so a genuinely unknown loader
78
+ // is a clean 404 "not found in registry" from handleLoaderFetch.
79
+ if (process.env.NODE_ENV === "production") {
80
+ return undefined;
81
+ }
82
+
71
83
  const hashIndex = id.indexOf("#");
72
84
  if (hashIndex !== -1) {
73
85
  const filePath = id.slice(0, hashIndex);
@@ -48,8 +48,8 @@ import { getFetchableLoader } from "./fetchable-loader-store.js";
48
48
  import type { SegmentCacheStore } from "../cache/types.js";
49
49
  import type { Theme, ResolvedThemeConfig } from "../theme/types.js";
50
50
  import type { ExecutionContext, RequestScope } from "../types/request-scope.js";
51
+ import type { TransitionWhenFn } from "../types/segments.js";
51
52
  import type { ResolvedTracing } from "../router/tracing.js";
52
- import { fireAndForgetWaitUntil } from "../types/request-scope.js";
53
53
  import {
54
54
  THEME_COOKIE,
55
55
  isValidTheme,
@@ -161,9 +161,75 @@ export interface RequestContext<
161
161
  /** @internal Handle store for tracking handle data across segments */
162
162
  _handleStore: HandleStore;
163
163
 
164
+ /**
165
+ * @internal transition({ when }) predicates for segments matched this request,
166
+ * keyed by segment id. Collected during resolution (the function is stripped
167
+ * from the serialized segment config), then evaluated post-handler in
168
+ * rsc-rendering — outside any cache scope — to drop the transition of any
169
+ * segment whose predicate returns false.
170
+ */
171
+ _transitionWhen?: Array<{ id: string; when: TransitionWhenFn }>;
172
+
164
173
  /** @internal Cache store for segment caching (optional, used by CacheScope) */
165
174
  _cacheStore?: SegmentCacheStore;
166
175
 
176
+ /**
177
+ * @internal PPR shell-capture ACTIVE marker. True ONLY inside the background
178
+ * capture task's derived request context (built by shell-capture.ts). This is
179
+ * the switch every capture-specific behavior reads: loader masking
180
+ * (loader-mask.ts isShellCaptureActive / fresh.ts emitStreaming) and the
181
+ * cookies()/headers() capture guard (cookie-store.ts
182
+ * assertNotInsideShellCapture). The foreground render never sets it, so the
183
+ * served response is byte-identical to axis 1. The capture descriptor itself
184
+ * (key/ttl/swr/tags/store) is NOT threaded through the request context — the
185
+ * integrated PPR serve path (rsc/shell-serve.ts + rsc-rendering.ts) builds it
186
+ * locally and passes it to scheduleShellCapture directly.
187
+ */
188
+ _shellCaptureRun?: boolean;
189
+
190
+ /**
191
+ * @internal Bake-lane loader containers collected DURING a shell capture:
192
+ * segment-key -> the loader's (pre-wrap) result promise. Populated by
193
+ * resolveLoaderData for loaders on entries with no renderable loading() (the
194
+ * bake lane — they execute at capture instead of being masked; see
195
+ * docs/design/loader-container-bake.md). Drained by captureAndStoreShell
196
+ * after the shell quiesces: settled containers are promise-elided,
197
+ * Flight-serialized, and pinned into the snapshot's loader family; a
198
+ * REJECTED container refuses the capture (error UI must never bake into the
199
+ * shared shell). Own property of the capture's derived context only.
200
+ */
201
+ _shellCaptureLoaderRecords?: Map<string, Promise<unknown>>;
202
+
203
+ /**
204
+ * @internal Loader-family snapshot seed for a shell HIT's tail render:
205
+ * segment-key -> the capture's elided container (already Flight-deserialized
206
+ * by serveShellHit). resolveLoaderData overlays it onto the fresh run's
207
+ * container (recorded paths pinned, hole-marker paths keep the fresh nested
208
+ * promises) so the payload's baked bytes match the frozen prelude. Own
209
+ * property of the HIT tail's derived context only.
210
+ */
211
+ _shellLoaderSeed?: Map<string, unknown>;
212
+
213
+ /**
214
+ * @internal Set (to the offending fn name) by the cookies()/headers()
215
+ * capture guard when it throws DURING a capture render. Load-bearing for the
216
+ * bake lane: a guard throw inside an executing loader is swallowed by
217
+ * wrapLoaderPromise into per-loader error UI, which would otherwise bake
218
+ * silently into the shared shell — the capture checks this flag after the
219
+ * render and refuses instead. Deterministic, so the capture does not retry.
220
+ */
221
+ _shellCaptureGuardTripped?: string;
222
+
223
+ /**
224
+ * @internal The loader $$id whose BODY was executing when the capture guard
225
+ * tripped (read off the loader-body ALS scope at trip time), or undefined
226
+ * when the read came from handler/render code. Only used to make the
227
+ * once-per-key refusal warning name the real source — the old text
228
+ * hardcoded "a bake-lane loader", which misattributed handler-land reads
229
+ * and sent users debugging the wrong lane (issue #672, secondary).
230
+ */
231
+ _shellCaptureGuardTrippedLoaderId?: string;
232
+
167
233
  /**
168
234
  * @internal Handler-owned registry of explicit per-scope stores from
169
235
  * cache({ store }). Created once per createRSCHandler() and threaded into
@@ -211,6 +277,19 @@ export interface RequestContext<
211
277
  /** @internal Registered onResponse callbacks */
212
278
  _onResponseCallbacks: Array<(response: Response) => Response>;
213
279
 
280
+ /**
281
+ * @internal Promises of the background tasks scheduled via this context's
282
+ * waitUntil (deferred cache writes, revalidations, consumer tasks). The PPR
283
+ * shell capture drains this list BEFORE its match/render as an ORDERING EDGE:
284
+ * every foreground deferred cache write is scheduled here before the capture
285
+ * task is, so settling the list first guarantees the capture's cache reads
286
+ * observe the foreground's generation instead of racing it (see
287
+ * shell-capture.ts). Tasks whose scheduling fn carries
288
+ * UNTRACKED_BACKGROUND_TASK are not tracked (the capture task itself —
289
+ * tracking it would make that drain await its own promise).
290
+ */
291
+ _pendingBackgroundTasks?: Array<Promise<unknown>>;
292
+
214
293
  /**
215
294
  * Current theme setting (only available when theme is enabled in router config)
216
295
  *
@@ -292,6 +371,30 @@ export interface RequestContext<
292
371
  /** @internal Previous route key (from the navigation source), used for revalidation */
293
372
  _prevRouteKey?: string;
294
373
 
374
+ /**
375
+ * @internal Navigation/action source data the transition({ when }) gate reads
376
+ * to build its ShouldRevalidateFn-shaped predicate context. currentUrl/Params
377
+ * come from the navigation snapshot (set at match time); action* are stashed
378
+ * at the action-bearing gate call sites. All undefined when there is no source
379
+ * (initial full load) or no action (plain navigation).
380
+ */
381
+ _gateCurrentUrl?: URL;
382
+ _gateCurrentParams?: Record<string, string>;
383
+ _gateActionId?: string;
384
+ _gateActionUrl?: URL;
385
+ _gateActionResult?: unknown;
386
+ _gateFormData?: FormData;
387
+
388
+ /**
389
+ * @internal True while the post-action revalidation render is running (set by
390
+ * revalidateAfterAction). The "use cache" runtime reads this to prefer
391
+ * freshness over a fast stale response during an action: a stale entry
392
+ * re-executes in the foreground (so the action response reflects the refreshed
393
+ * value) with only the store write deferred, instead of serving stale and
394
+ * revalidating in the background. A plain navigation (flag unset) keeps SWR.
395
+ */
396
+ _inActionRevalidation?: boolean;
397
+
295
398
  /**
296
399
  * @internal Render barrier for experimental `rendered()` API.
297
400
  * Resolves when all non-loader segments have settled and handle data
@@ -375,6 +478,15 @@ export interface RequestContext<
375
478
  /** @internal Request-scoped performance metrics store */
376
479
  _metricsStore?: MetricsStore;
377
480
 
481
+ /**
482
+ * @internal True request entry timestamp (performance.now() at handler entry).
483
+ * Set once at request-context creation (rsc/handler.ts) so a metrics store
484
+ * created MID-request — ctx.debugPerformance() or the getMetricsStore wrapper —
485
+ * anchors its timeline to the real request start instead of the opt-in moment,
486
+ * keeping phases that began before the opt-in at their true (non-negative) offset.
487
+ */
488
+ _handlerStart?: number;
489
+
378
490
  /** @internal Resolved platform phase-span tracing for this request (Cloudflare or OTel) */
379
491
  _tracing?: ResolvedTracing;
380
492
 
@@ -413,7 +525,10 @@ export type PublicRequestContext<
413
525
  | "setCookie"
414
526
  | "deleteCookie"
415
527
  | "_handleStore"
528
+ | "_transitionWhen"
416
529
  | "_cacheStore"
530
+ | "_shellCaptureRun"
531
+ | "_shellCaptureGuardTrippedLoaderId"
417
532
  | "_explicitTaggedStores"
418
533
  | "_requestTags"
419
534
  | "_cacheProfiles"
@@ -422,6 +537,13 @@ export type PublicRequestContext<
422
537
  | "_locationState"
423
538
  | "_routeName"
424
539
  | "_prevRouteKey"
540
+ | "_gateCurrentUrl"
541
+ | "_gateCurrentParams"
542
+ | "_gateActionId"
543
+ | "_gateActionUrl"
544
+ | "_gateActionResult"
545
+ | "_gateFormData"
546
+ | "_inActionRevalidation"
425
547
  | "_reportedErrors"
426
548
  | "_renderBarrier"
427
549
  | "_resolveRenderBarrier"
@@ -434,6 +556,7 @@ export type PublicRequestContext<
434
556
  | "_reportBackgroundError"
435
557
  | "_debugPerformance"
436
558
  | "_metricsStore"
559
+ | "_handlerStart"
437
560
  | "_basename"
438
561
  | "_setStatus"
439
562
  | "_rotateStateCookie"
@@ -444,6 +567,17 @@ export type PublicRequestContext<
444
567
  | "res"
445
568
  >;
446
569
 
570
+ /**
571
+ * Marker for a waitUntil-scheduled fn whose task promise must NOT enter
572
+ * _pendingBackgroundTasks. Used by the PPR shell capture for its own task:
573
+ * the capture's pre-render write barrier settles that list, so tracking the
574
+ * capture itself would make the drain wait on its own (still-running) promise.
575
+ * @internal
576
+ */
577
+ export const UNTRACKED_BACKGROUND_TASK: unique symbol = Symbol.for(
578
+ "rango.untrackedBackgroundTask",
579
+ );
580
+
447
581
  // AsyncLocalStorage instance for request context
448
582
  const requestContextStorage = new AsyncLocalStorage<RequestContext<any>>();
449
583
 
@@ -527,11 +661,17 @@ export function setRequestContextParams(
527
661
  */
528
662
  export function setRequestContextPrevRouteKey(
529
663
  prevRouteKey: string | undefined,
664
+ currentUrl?: URL,
665
+ currentParams?: Record<string, string>,
530
666
  ): void {
531
667
  const ctx = requestContextStorage.getStore();
532
- if (ctx && prevRouteKey !== undefined) {
533
- ctx._prevRouteKey = prevRouteKey;
534
- }
668
+ if (!ctx) return;
669
+ if (prevRouteKey !== undefined) ctx._prevRouteKey = prevRouteKey;
670
+ // Source URL/params for the transition({ when }) gate (effectiveFromUrl /
671
+ // effectiveFromMatch.params from the navigation snapshot). Same write point as
672
+ // _prevRouteKey, which doubles as fromRouteName.
673
+ if (currentUrl !== undefined) ctx._gateCurrentUrl = currentUrl;
674
+ if (currentParams !== undefined) ctx._gateCurrentParams = currentParams;
535
675
  }
536
676
 
537
677
  /**
@@ -842,26 +982,44 @@ export function createRequestContext<TEnv>(
842
982
  method: request.method,
843
983
 
844
984
  _handleStore: handleStore,
985
+ _transitionWhen: [],
845
986
  _cacheStore: cacheStore,
846
987
  _explicitTaggedStores: explicitTaggedStores,
847
988
  _requestTags: new Set<string>(),
848
989
  _cacheProfiles: cacheProfiles,
849
990
 
850
991
  waitUntil(fn: () => Promise<void>): void {
992
+ // Wrap in Promise.resolve().then(fn) so a SYNCHRONOUS throw in a
993
+ // non-async callback becomes a rejected promise handed to the host's
994
+ // waitUntil (logged as a background failure), instead of escaping into
995
+ // the request flow. Mirrors fireAndForgetWaitUntil's deferral.
996
+ const task = Promise.resolve().then(fn);
997
+ // Track the task promise so the PPR shell capture can settle the
998
+ // foreground's deferred cache writes before its own match/render (the
999
+ // ordering edge; see _pendingBackgroundTasks). The capture task itself
1000
+ // opts out via the marker — the drain must never await its own promise.
1001
+ if (
1002
+ !(fn as { [UNTRACKED_BACKGROUND_TASK]?: boolean })[
1003
+ UNTRACKED_BACKGROUND_TASK
1004
+ ]
1005
+ ) {
1006
+ ctx._pendingBackgroundTasks?.push(task);
1007
+ }
851
1008
  if (executionContext?.waitUntil) {
852
- // Wrap in Promise.resolve().then(fn) so a SYNCHRONOUS throw in a
853
- // non-async callback becomes a rejected promise handed to the host's
854
- // waitUntil (logged as a background failure), instead of escaping into
855
- // the request flow. Mirrors fireAndForgetWaitUntil's deferral.
856
- executionContext.waitUntil(Promise.resolve().then(fn));
1009
+ executionContext.waitUntil(task);
857
1010
  } else {
858
- fireAndForgetWaitUntil(fn);
1011
+ // Node/dev fallback: fire-and-forget with error logging (the same
1012
+ // policy fireAndForgetWaitUntil applies).
1013
+ task.catch((err) =>
1014
+ console.error("[waitUntil] Background task failed:", err),
1015
+ );
859
1016
  }
860
1017
  },
861
1018
 
862
1019
  executionContext,
863
1020
 
864
1021
  _onResponseCallbacks: [],
1022
+ _pendingBackgroundTasks: [],
865
1023
 
866
1024
  onResponse(callback: (response: Response) => Response): void {
867
1025
  assertNotInsideCacheExec(ctx, "onResponse");
@@ -891,6 +1049,7 @@ export function createRequestContext<TEnv>(
891
1049
 
892
1050
  _reportedErrors: new WeakSet<object>(),
893
1051
  _metricsStore: undefined,
1052
+ _handlerStart: undefined,
894
1053
 
895
1054
  _renderBarrier: null as any,
896
1055
  _resolveRenderBarrier: null as any,