@rangojs/router 0.17.0 → 0.19.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 (293) hide show
  1. package/README.md +3 -2
  2. package/dist/types/browser/cookie-name.d.ts +6 -0
  3. package/dist/types/browser/event-controller.d.ts +3 -3
  4. package/dist/types/browser/history-state.d.ts +18 -0
  5. package/dist/types/browser/navigation-transaction.d.ts +20 -2
  6. package/dist/types/browser/partial-update.d.ts +16 -1
  7. package/dist/types/browser/prefetch/cache.d.ts +6 -6
  8. package/dist/types/browser/react/Link.d.ts +19 -9
  9. package/dist/types/browser/react/context.d.ts +2 -2
  10. package/dist/types/browser/react/location-state-shared.d.ts +95 -56
  11. package/dist/types/browser/react/location-state.d.ts +1 -1
  12. package/dist/types/browser/transition-when.d.ts +80 -0
  13. package/dist/types/browser/types.d.ts +104 -9
  14. package/dist/types/build/generate-manifest.d.ts +8 -0
  15. package/dist/types/cache/background-task.d.ts +24 -2
  16. package/dist/types/cache/cache-exec-scope.d.ts +31 -0
  17. package/dist/types/cache/cache-key-utils.d.ts +48 -2
  18. package/dist/types/cache/cache-policy.d.ts +18 -1
  19. package/dist/types/cache/cache-runtime.d.ts +7 -0
  20. package/dist/types/cache/cache-scope.d.ts +192 -26
  21. package/dist/types/cache/cache-tag.d.ts +116 -11
  22. package/dist/types/cache/cf/cf-base64.d.ts +6 -0
  23. package/dist/types/cache/cf/cf-cache-constants.d.ts +11 -0
  24. package/dist/types/cache/cf/cf-cache-store.d.ts +224 -72
  25. package/dist/types/cache/cf/cf-cache-types.d.ts +31 -4
  26. package/dist/types/cache/cf/cf-shell-frame.d.ts +95 -0
  27. package/dist/types/cache/cf/cf-tag-marker-memo.d.ts +19 -0
  28. package/dist/types/cache/document-cache.d.ts +10 -0
  29. package/dist/types/cache/handle-snapshot.d.ts +41 -8
  30. package/dist/types/cache/index.d.ts +1 -0
  31. package/dist/types/cache/invalidation-order.d.ts +23 -0
  32. package/dist/types/cache/isolate-tag-memo.d.ts +149 -0
  33. package/dist/types/cache/request-tag-mask.d.ts +54 -0
  34. package/dist/types/cache/shell-memo.d.ts +178 -0
  35. package/dist/types/cache/shell-snapshot.d.ts +141 -105
  36. package/dist/types/cache/tag-invalidation.d.ts +54 -3
  37. package/dist/types/cache/types.d.ts +267 -96
  38. package/dist/types/cache/vercel/vercel-cache-store.d.ts +123 -18
  39. package/dist/types/client-urls/navigation.d.ts +28 -2
  40. package/dist/types/client-urls/server-projection.d.ts +5 -2
  41. package/dist/types/client-urls/types.d.ts +5 -5
  42. package/dist/types/client.d.ts +1 -1
  43. package/dist/types/index.d.ts +1 -1
  44. package/dist/types/index.rsc.d.ts +1 -1
  45. package/dist/types/prerender/build-shell-capture.d.ts +11 -4
  46. package/dist/types/route-definition/helpers-types.d.ts +11 -11
  47. package/dist/types/route-name.d.ts +7 -0
  48. package/dist/types/router/error-handling.d.ts +1 -1
  49. package/dist/types/router/instrument.d.ts +8 -0
  50. package/dist/types/router/intercept-resolution.d.ts +2 -0
  51. package/dist/types/router/match-context.d.ts +5 -0
  52. package/dist/types/router/match-middleware/background-revalidation.d.ts +0 -4
  53. package/dist/types/router/middleware-types.d.ts +6 -1
  54. package/dist/types/router/router-options.d.ts +4 -1
  55. package/dist/types/router/segment-resolution/loader-cache.d.ts +58 -12
  56. package/dist/types/router/segment-resolution/loader-mask.d.ts +3 -4
  57. package/dist/types/router/segment-resolution/loader-snapshot.d.ts +33 -48
  58. package/dist/types/router/segment-resolution/mask-nested.d.ts +73 -24
  59. package/dist/types/router/segment-resolution/view-transition-default.d.ts +7 -5
  60. package/dist/types/router/transition-when.d.ts +24 -10
  61. package/dist/types/rsc/attach-transition-when.d.ts +18 -0
  62. package/dist/types/rsc/full-payload.d.ts +21 -0
  63. package/dist/types/rsc/handler.d.ts +1 -1
  64. package/dist/types/rsc/response-cache-serve.d.ts +15 -12
  65. package/dist/types/rsc/shell-build-manifest.d.ts +12 -3
  66. package/dist/types/rsc/shell-capture-constants.d.ts +116 -12
  67. package/dist/types/rsc/shell-capture.d.ts +202 -74
  68. package/dist/types/rsc/shell-serve.d.ts +144 -50
  69. package/dist/types/rsc/types.d.ts +9 -0
  70. package/dist/types/segment-system.d.ts +7 -0
  71. package/dist/types/server/context.d.ts +155 -33
  72. package/dist/types/server/cookie-store.d.ts +33 -0
  73. package/dist/types/server/handle-store.d.ts +52 -1
  74. package/dist/types/server/request-context.d.ts +210 -63
  75. package/dist/types/server/request-headers.d.ts +44 -0
  76. package/dist/types/server.d.ts +1 -0
  77. package/dist/types/ssr/index.d.ts +9 -0
  78. package/dist/types/ssr/ssr-root.d.ts +9 -8
  79. package/dist/types/testing/e2e/index.d.ts +1 -1
  80. package/dist/types/testing/flight.entry.d.ts +7 -0
  81. package/dist/types/testing/index.d.ts +3 -2
  82. package/dist/types/testing/internal/action-id.d.ts +7 -0
  83. package/dist/types/testing/location-state-key.d.ts +24 -0
  84. package/dist/types/testing/render-route.d.ts +17 -1
  85. package/dist/types/testing/run-loader.d.ts +18 -3
  86. package/dist/types/testing/run-transition-when.d.ts +44 -69
  87. package/dist/types/testing/serve-shell-request.d.ts +133 -0
  88. package/dist/types/testing/shell-status.d.ts +44 -19
  89. package/dist/types/transition-when-ref.d.ts +79 -0
  90. package/dist/types/types/cache-types.d.ts +15 -3
  91. package/dist/types/types/handler-context.d.ts +14 -1
  92. package/dist/types/types/index.d.ts +1 -1
  93. package/dist/types/types/segments.d.ts +86 -71
  94. package/dist/types/urls/path-helper-types.d.ts +3 -4
  95. package/dist/types/urls/pattern-types.d.ts +21 -18
  96. package/dist/types/vite/plugins/transition-when-hoist.d.ts +45 -0
  97. package/dist/types/vite/router-discovery.d.ts +29 -2
  98. package/dist/vite/index.js +1005 -105
  99. package/package.json +1 -1
  100. package/skills/breadcrumbs/SKILL.md +17 -7
  101. package/skills/cache-guide/SKILL.md +175 -76
  102. package/skills/caching/SKILL.md +361 -33
  103. package/skills/catalog.json +1 -1
  104. package/skills/client-urls/SKILL.md +12 -7
  105. package/skills/cloudflare/SKILL.md +15 -0
  106. package/skills/comparison/references/framework-comparison.md +52 -37
  107. package/skills/defer-hydration/SKILL.md +50 -30
  108. package/skills/deployment-caching/SKILL.md +5 -2
  109. package/skills/document-cache/SKILL.md +32 -2
  110. package/skills/handler-use/SKILL.md +2 -0
  111. package/skills/hooks/SKILL.md +3 -1
  112. package/skills/hooks/handle-and-actions.md +3 -1
  113. package/skills/hooks/navigation.md +23 -2
  114. package/skills/hooks/state.md +101 -7
  115. package/skills/intercept/SKILL.md +29 -12
  116. package/skills/loader/SKILL.md +197 -44
  117. package/skills/middleware/SKILL.md +34 -9
  118. package/skills/migrate-nextjs/SKILL.md +68 -37
  119. package/skills/migrate-react-router/SKILL.md +2 -1
  120. package/skills/migrate-react-router/component-migration.md +3 -1
  121. package/skills/migrate-react-router/data-and-actions.md +3 -1
  122. package/skills/migrate-react-router/route-mapping.md +7 -4
  123. package/skills/observability/SKILL.md +65 -0
  124. package/skills/parallel/SKILL.md +13 -8
  125. package/skills/ppr/SKILL.md +715 -371
  126. package/skills/prerender/SKILL.md +30 -17
  127. package/skills/rango/SKILL.md +72 -34
  128. package/skills/route/SKILL.md +14 -7
  129. package/skills/router-setup/SKILL.md +4 -2
  130. package/skills/scripts/SKILL.md +8 -1
  131. package/skills/server-actions/SKILL.md +11 -0
  132. package/skills/shell-manifest/SKILL.md +21 -12
  133. package/skills/testing/SKILL.md +49 -43
  134. package/skills/testing/cache-prerender.md +15 -14
  135. package/skills/testing/client-components.md +45 -3
  136. package/skills/testing/loader.md +19 -1
  137. package/skills/testing/middleware.md +1 -0
  138. package/skills/testing/predicates.md +49 -49
  139. package/skills/testing/render-handler.md +1 -0
  140. package/skills/testing/serve-shell-request.md +202 -0
  141. package/skills/testing/server-actions.md +9 -3
  142. package/skills/theme/SKILL.md +56 -4
  143. package/skills/typesafety/env-and-bindings.md +18 -2
  144. package/skills/use-cache/SKILL.md +91 -23
  145. package/skills/vercel/SKILL.md +14 -2
  146. package/skills/view-transitions/SKILL.md +150 -29
  147. package/src/__augment-tests__/augmented.check.ts +23 -0
  148. package/src/browser/cookie-name.ts +23 -0
  149. package/src/browser/event-controller.ts +4 -4
  150. package/src/browser/history-state.ts +75 -1
  151. package/src/browser/link-interceptor.ts +4 -0
  152. package/src/browser/navigation-bridge.ts +77 -18
  153. package/src/browser/navigation-client.ts +4 -3
  154. package/src/browser/navigation-store.ts +43 -1
  155. package/src/browser/navigation-transaction.ts +31 -11
  156. package/src/browser/partial-update.ts +146 -35
  157. package/src/browser/prefetch/cache.ts +6 -6
  158. package/src/browser/prefetch/fetch.ts +2 -2
  159. package/src/browser/react/Link.tsx +38 -33
  160. package/src/browser/react/NavigationProvider.tsx +2 -2
  161. package/src/browser/react/context.ts +2 -2
  162. package/src/browser/react/location-state-shared.ts +241 -49
  163. package/src/browser/react/location-state.ts +5 -2
  164. package/src/browser/rsc-router.tsx +9 -0
  165. package/src/browser/server-action-bridge.ts +84 -15
  166. package/src/browser/transition-when.ts +205 -0
  167. package/src/browser/types.ts +124 -10
  168. package/src/build/generate-manifest.ts +77 -1
  169. package/src/cache/background-task.ts +55 -2
  170. package/src/cache/cache-exec-scope.ts +48 -1
  171. package/src/cache/cache-key-utils.ts +62 -2
  172. package/src/cache/cache-policy.ts +31 -4
  173. package/src/cache/cache-runtime.ts +209 -129
  174. package/src/cache/cache-scope.ts +573 -69
  175. package/src/cache/cache-tag.ts +475 -19
  176. package/src/cache/cf/cf-base64.ts +36 -4
  177. package/src/cache/cf/cf-cache-constants.ts +12 -0
  178. package/src/cache/cf/cf-cache-store.ts +1094 -376
  179. package/src/cache/cf/cf-cache-types.ts +32 -4
  180. package/src/cache/cf/cf-shell-frame.ts +258 -0
  181. package/src/cache/cf/cf-tag-marker-memo.ts +55 -40
  182. package/src/cache/document-cache.ts +53 -7
  183. package/src/cache/handle-snapshot.ts +53 -9
  184. package/src/cache/index.ts +2 -0
  185. package/src/cache/invalidation-order.ts +84 -0
  186. package/src/cache/isolate-tag-memo.ts +308 -0
  187. package/src/cache/request-tag-mask.ts +147 -0
  188. package/src/cache/shell-memo.ts +341 -0
  189. package/src/cache/shell-snapshot.ts +249 -277
  190. package/src/cache/tag-invalidation.ts +109 -18
  191. package/src/cache/types.ts +274 -102
  192. package/src/cache/vercel/vercel-cache-store.ts +561 -74
  193. package/src/client-urls/client-root.tsx +8 -4
  194. package/src/client-urls/client-urls.ts +8 -6
  195. package/src/client-urls/navigation.ts +55 -4
  196. package/src/client-urls/server-projection.ts +31 -10
  197. package/src/client-urls/types.ts +12 -5
  198. package/src/client.tsx +2 -0
  199. package/src/host/cookie-handler.ts +2 -1
  200. package/src/index.rsc.ts +3 -0
  201. package/src/index.ts +3 -0
  202. package/src/loader-redirect.tsx +7 -3
  203. package/src/prerender/build-shell-capture.ts +173 -110
  204. package/src/route-definition/dsl-helpers.ts +6 -3
  205. package/src/route-definition/helpers-types.ts +11 -11
  206. package/src/route-name.ts +12 -0
  207. package/src/router/content-negotiation.ts +4 -1
  208. package/src/router/error-handling.ts +5 -2
  209. package/src/router/handler-context.ts +42 -5
  210. package/src/router/instrument.ts +54 -2
  211. package/src/router/intercept-resolution.ts +30 -8
  212. package/src/router/loader-resolution.ts +89 -33
  213. package/src/router/logging.ts +5 -3
  214. package/src/router/match-api.ts +89 -42
  215. package/src/router/match-context.ts +5 -0
  216. package/src/router/match-handlers.ts +14 -29
  217. package/src/router/match-middleware/background-revalidation.ts +3 -5
  218. package/src/router/match-middleware/cache-lookup.ts +52 -19
  219. package/src/router/match-middleware/cache-store.ts +36 -10
  220. package/src/router/match-result.ts +1 -0
  221. package/src/router/middleware-types.ts +6 -1
  222. package/src/router/middleware.ts +20 -4
  223. package/src/router/navigation-snapshot.ts +6 -5
  224. package/src/router/prerender-match.ts +10 -0
  225. package/src/router/request-classification.ts +7 -4
  226. package/src/router/router-options.ts +4 -1
  227. package/src/router/segment-resolution/fresh.ts +18 -13
  228. package/src/router/segment-resolution/helpers.ts +4 -2
  229. package/src/router/segment-resolution/loader-cache.ts +366 -70
  230. package/src/router/segment-resolution/loader-mask.ts +3 -4
  231. package/src/router/segment-resolution/loader-snapshot.ts +215 -126
  232. package/src/router/segment-resolution/mask-nested.ts +161 -42
  233. package/src/router/segment-resolution/revalidation.ts +27 -13
  234. package/src/router/segment-resolution/view-transition-default.ts +6 -21
  235. package/src/router/telemetry.ts +6 -3
  236. package/src/router/transition-when.ts +34 -52
  237. package/src/router.ts +2 -1
  238. package/src/rsc/attach-transition-when.ts +47 -0
  239. package/src/rsc/full-payload.ts +47 -3
  240. package/src/rsc/handler.ts +24 -10
  241. package/src/rsc/loader-fetch.ts +2 -1
  242. package/src/rsc/manifest-init.ts +4 -0
  243. package/src/rsc/origin-guard.ts +8 -5
  244. package/src/rsc/progressive-enhancement.ts +10 -32
  245. package/src/rsc/response-cache-serve.ts +69 -78
  246. package/src/rsc/response-route-handler.ts +2 -2
  247. package/src/rsc/rsc-rendering.ts +712 -338
  248. package/src/rsc/server-action.ts +8 -20
  249. package/src/rsc/shell-build-manifest.ts +49 -29
  250. package/src/rsc/shell-capture-constants.ts +182 -12
  251. package/src/rsc/shell-capture.ts +1126 -612
  252. package/src/rsc/shell-serve.ts +331 -85
  253. package/src/rsc/ssr-setup.ts +3 -2
  254. package/src/rsc/types.ts +9 -0
  255. package/src/segment-system.tsx +40 -7
  256. package/src/server/context.ts +328 -72
  257. package/src/server/cookie-store.ts +184 -133
  258. package/src/server/handle-store.ts +245 -27
  259. package/src/server/request-context.ts +364 -100
  260. package/src/server/request-headers.ts +134 -0
  261. package/src/server.ts +4 -0
  262. package/src/ssr/index.tsx +96 -37
  263. package/src/ssr/ssr-root.tsx +9 -8
  264. package/src/testing/dispatch.ts +4 -7
  265. package/src/testing/e2e/index.ts +1 -0
  266. package/src/testing/flight.entry.ts +14 -0
  267. package/src/testing/index.ts +5 -0
  268. package/src/testing/internal/action-id.ts +25 -0
  269. package/src/testing/internal/context.ts +1 -1
  270. package/src/testing/location-state-key.ts +45 -0
  271. package/src/testing/render-handler.ts +1 -1
  272. package/src/testing/render-route.tsx +109 -16
  273. package/src/testing/run-client-revalidate.ts +3 -19
  274. package/src/testing/run-loader.ts +67 -5
  275. package/src/testing/run-transition-when.ts +109 -172
  276. package/src/testing/serve-shell-request.ts +381 -0
  277. package/src/testing/shell-status.ts +73 -47
  278. package/src/theme/ThemeProvider.tsx +7 -6
  279. package/src/transition-when-ref.ts +220 -0
  280. package/src/types/cache-types.ts +15 -3
  281. package/src/types/handler-context.ts +14 -1
  282. package/src/types/index.ts +3 -0
  283. package/src/types/segments.ts +94 -91
  284. package/src/urls/path-helper-types.ts +3 -4
  285. package/src/urls/pattern-types.ts +21 -18
  286. package/src/vite/discovery/discover-routers.ts +3 -0
  287. package/src/vite/discovery/shell-prerender-phase.ts +9 -2
  288. package/src/vite/discovery/virtual-module-codegen.ts +16 -6
  289. package/src/vite/plugins/transition-when-hoist.ts +1064 -0
  290. package/src/vite/rango.ts +2 -0
  291. package/src/vite/router-discovery.ts +89 -9
  292. package/dist/types/rsc/transition-gate.d.ts +0 -23
  293. package/src/rsc/transition-gate.ts +0 -86
@@ -28,7 +28,14 @@
28
28
  * is a direct call — no wrapper, no timestamp, no allocation.
29
29
  */
30
30
 
31
- import { _getRequestContext } from "../server/request-context.js";
31
+ import {
32
+ _getRequestContext,
33
+ type RequestContext,
34
+ } from "../server/request-context.js";
35
+ import {
36
+ recordTagOwnersArmed,
37
+ runInSegmentTagScope,
38
+ } from "../cache/cache-tag.js";
32
39
  import { isAutoGeneratedRouteName } from "../route-name.js";
33
40
  import { getRouterContext } from "./router-context.js";
34
41
  import { resolveSink, safeEmit, type TelemetryEvent } from "./telemetry.js";
@@ -352,11 +359,56 @@ export function observeHandler<C, R>(
352
359
  handler: (ctx: C) => R,
353
360
  ctx: C,
354
361
  ): R {
355
- const tracing = _getRequestContext()?._tracing;
362
+ return observeHandlerIn(_getRequestContext(), id, handler, ctx);
363
+ }
364
+
365
+ function observeHandlerIn<C, R>(
366
+ reqCtx: RequestContext | undefined,
367
+ id: string,
368
+ handler: (ctx: C) => R,
369
+ ctx: C,
370
+ ): R {
371
+ const tracing = reqCtx?._tracing;
356
372
  if (!tracing || tracing.phases.handler === false) return handler(ctx);
357
373
  return observePhase(PHASES.handler(id), () => handler(ctx));
358
374
  }
359
375
 
376
+ /**
377
+ * observeHandler, inside segment `segmentId`'s tag scope, so the tags the
378
+ * handler records reach the route's cache() record (#957, cache-tag.ts
379
+ * runInSegmentTagScope). `spanId` is observeHandler's `id`. A request that
380
+ * cannot write a record takes observeHandler's path as is, with the same one
381
+ * request-context read: no scope, no closure.
382
+ */
383
+ export function observeSegmentHandler<C, R>(
384
+ segmentId: string,
385
+ spanId: string,
386
+ handler: (ctx: C) => R,
387
+ ctx: C,
388
+ ): R {
389
+ const reqCtx = _getRequestContext();
390
+ if (!recordTagOwnersArmed(reqCtx)) {
391
+ return observeHandlerIn(reqCtx, spanId, handler, ctx);
392
+ }
393
+ return observeInSegmentScope(segmentId, reqCtx, spanId, handler, ctx);
394
+ }
395
+
396
+ // Separate from observeSegmentHandler: a closure there would make V8
397
+ // allocate its captured-variable context on every call, armed or not.
398
+ function observeInSegmentScope<C, R>(
399
+ segmentId: string,
400
+ reqCtx: RequestContext | undefined,
401
+ spanId: string,
402
+ handler: (ctx: C) => R,
403
+ ctx: C,
404
+ ): R {
405
+ return runInSegmentTagScope(
406
+ segmentId,
407
+ (c) => observeHandlerIn(reqCtx, spanId, handler, c),
408
+ ctx,
409
+ );
410
+ }
411
+
360
412
  /**
361
413
  * Emit one discrete telemetry event (the event-shaped counterpart to
362
414
  * observePhase). Resolves the sink from the active router context and stamps the
@@ -32,6 +32,7 @@ import { resolveLoaderData } from "./segment-resolution/loader-cache.js";
32
32
  import type { SegmentResolutionDeps } from "./types.js";
33
33
  import { debugLog } from "./logging.js";
34
34
  import { getRouterContext } from "./router-context.js";
35
+ import { runInSegmentTagScope } from "../cache/cache-tag.js";
35
36
  import {
36
37
  RangoContext,
37
38
  latchPprHeaderScopeForEntries,
@@ -63,6 +64,8 @@ function latchPprHeaderScopeForInterceptTarget(
63
64
  * Check if an intercept's when conditions are satisfied.
64
65
  * All when() functions must return true for the intercept to activate.
65
66
  * If no when() conditions are defined, the intercept always activates.
67
+ * A selector that throws yields the conservative default, no intercept (the
68
+ * full page renders), and is logged; it never fails the request.
66
69
  *
67
70
  * During action revalidation, when() is NOT evaluated.
68
71
  */
@@ -83,7 +86,15 @@ export function evaluateInterceptWhen(
83
86
  return false;
84
87
  }
85
88
 
86
- return intercept.when.every((fn) => fn(selectorContext));
89
+ try {
90
+ return intercept.when.every((fn) => fn(selectorContext));
91
+ } catch (error) {
92
+ console.error(
93
+ `[rango] intercept({ when }) for route "${selectorContext.to.routeName ?? intercept.routeName}" (${intercept.slotName}) threw; rendering the full page without the intercept.`,
94
+ error,
95
+ );
96
+ return false;
97
+ }
87
98
  }
88
99
 
89
100
  /**
@@ -332,12 +343,18 @@ export async function resolveInterceptEntry<TEnv>(
332
343
  );
333
344
  }
334
345
 
346
+ // The intercept segment's id, and the tag scope its handler and layout
347
+ // record in (#957, cache-tag.ts runInSegmentTagScope).
348
+ const slotSegmentId = `${parentEntry.shortCode}.${interceptEntry.slotName}`;
335
349
  let handlerResult: ReactNode;
336
350
  try {
351
+ const handler = interceptEntry.handler;
337
352
  handlerResult =
338
- typeof interceptEntry.handler === "function"
339
- ? handleHandlerResult(interceptEntry.handler(context))
340
- : interceptEntry.handler;
353
+ typeof handler === "function"
354
+ ? handleHandlerResult(
355
+ runInSegmentTagScope(slotSegmentId, handler, context),
356
+ )
357
+ : handler;
341
358
  } catch (error) {
342
359
  handlerResult = renderInterceptHandlerError(
343
360
  error,
@@ -355,14 +372,19 @@ export async function resolveInterceptEntry<TEnv>(
355
372
 
356
373
  let layoutElement: ReactNode | undefined;
357
374
  if (interceptEntry.layout) {
358
- if (typeof interceptEntry.layout === "function") {
359
- const layoutResult = await interceptEntry.layout(context);
375
+ const layout = interceptEntry.layout;
376
+ if (typeof layout === "function") {
377
+ const layoutResult = await runInSegmentTagScope(
378
+ slotSegmentId,
379
+ layout,
380
+ context,
381
+ );
360
382
  if (layoutResult instanceof Response) {
361
383
  throw layoutResult;
362
384
  }
363
385
  layoutElement = layoutResult;
364
386
  } else {
365
- layoutElement = interceptEntry.layout;
387
+ layoutElement = layout;
366
388
  }
367
389
  }
368
390
 
@@ -413,7 +435,7 @@ export async function resolveInterceptEntry<TEnv>(
413
435
  }
414
436
 
415
437
  const interceptSegment = {
416
- id: `${parentEntry.shortCode}.${interceptEntry.slotName}`,
438
+ id: slotSegmentId,
417
439
  namespace: `intercept:${interceptEntry.routeName}`,
418
440
  type: "parallel" as const,
419
441
  index: 0,
@@ -9,6 +9,16 @@ import type { EntryData } from "../server/context";
9
9
  import { observePhase, PHASES } from "./instrument.js";
10
10
  import { contextGet, isNonCacheable } from "../context-var.js";
11
11
  import { NOCACHE_SYMBOL } from "../cache/taint.js";
12
+ import {
13
+ linkLoaderTags,
14
+ linkRecordedTags,
15
+ linkValueTags,
16
+ loaderTagSetsArmed,
17
+ markTagSetStart,
18
+ readValueTags,
19
+ tagLoaderValue,
20
+ } from "../cache/cache-tag.js";
21
+ import { executionStart } from "../cache/tag-invalidation.js";
12
22
  import type {
13
23
  ResolvedSegment,
14
24
  HandlerContext,
@@ -388,6 +398,18 @@ function createLoaderExecutor<TEnv>(
388
398
  * payload's handle-stream lifetime.
389
399
  */
390
400
  silentHandles?: boolean;
401
+ /**
402
+ * A stale refresh's executor (setupLoaderAccess _runLoaderIsolated): its
403
+ * runs' pushes are diverted from the page's store, so a run's end says
404
+ * nothing about the page's replayed values (HandleStore.settleLoaderRun).
405
+ */
406
+ detached?: boolean;
407
+ /**
408
+ * loaderId -> the cache() tags of that loader's binding in this request
409
+ * (setupLoaderAccess _bindLoaderCacheTags): an execution of it outside
410
+ * the binding answers for them too (#964).
411
+ */
412
+ bindingTags?: ReadonlyMap<string, Set<string>>;
391
413
  },
392
414
  ): (
393
415
  loader: LoaderDefinition<any, any>,
@@ -396,6 +418,8 @@ function createLoaderExecutor<TEnv>(
396
418
  // Capture RequestContext eagerly for cookie access (ALS protection on Cloudflare)
397
419
  const reqCtxRef = _getRequestContext();
398
420
  const silentHandles = executorOptions?.silentHandles === true;
421
+ // A run's end reports to the page's store only when its pushes reach it.
422
+ const reportsRunEnd = !silentHandles && executorOptions?.detached !== true;
399
423
  const internal = ctx as InternalHandlerContext<any, TEnv>;
400
424
 
401
425
  // Dependency graph: loaderId -> set of loader IDs it directly depends on.
@@ -438,8 +462,24 @@ function createLoaderExecutor<TEnv>(
438
462
  // loader's cache() read-through lives in loader-cache.ts
439
463
  // (executeLoaderData), whose ctx.use override serves a later handler read
440
464
  // the binding's result.
441
- if (loaderPromises.has(loader.$$id)) {
442
- return loaderPromises.get(loader.$$id)!;
465
+ //
466
+ // A read is a consumption: the value bakes into the reader's output, so
467
+ // the loader's tags follow the reader onto a cache() record (#957) and
468
+ // into the reader's own loader-cache entry (#964, readValueTags; armed
469
+ // only where a loader cache() can read them).
470
+ const reqCtx = reqCtxRef ?? _getRequestContext();
471
+ linkLoaderTags(loader.$$id, reqCtx);
472
+ const recordTags = loaderTagSetsArmed(reqCtx);
473
+ // A loader body reads a cache()-bound loader through its binding too, so
474
+ // every reader in the request gets the one value a HIT served. The
475
+ // binding's own MISS (callerLoaderId null) runs the loader below.
476
+ const bound =
477
+ callerLoaderId !== null &&
478
+ internal._loaderCacheOverrides?.get(loader.$$id);
479
+ const memo = bound || loaderPromises.get(loader.$$id);
480
+ if (memo) {
481
+ if (recordTags) readValueTags(memo);
482
+ return memo;
443
483
  }
444
484
 
445
485
  // Get loader function - either from loader object or fetchable registry
@@ -534,7 +574,7 @@ function createLoaderExecutor<TEnv>(
534
574
  return collectHandleData(keyOrVar, snapshot, segmentOrder);
535
575
  }
536
576
  if (isNonCacheable(variables, keyOrVar)) {
537
- assertNonCacheableReadAllowed(keyOrVar);
577
+ assertNonCacheableReadAllowed(keyOrVar, _getRequestContext());
538
578
  }
539
579
  return contextGet(variables, keyOrVar);
540
580
  }) as typeof ctx.get,
@@ -680,42 +720,39 @@ function createLoaderExecutor<TEnv>(
680
720
  // does not grant rendered() to handler-invoked loaders. Uses a body-only
681
721
  // scope, so isInsideLoaderScope() / barrier / deadlock gating is unchanged.
682
722
  //
683
- // `handlerInvoked` (!isDslLoader) rides on the scope for the CONSUMPTION-
684
- // LANE RULE: a handler-consumed loader's value is a BAKED copy in every
685
- // shared artifact (cache(), "use cache", the PPR shell), so its identity
686
- // reads are exempt from the shell-capture guard — same allowance the
687
- // cache-purity guards give it. DSL segment loaders keep their lane
688
- // machinery (live = masked at capture, bake = guarded). A DSL loader's
689
- // nested deps inherit isDslLoader=false only when the CHAIN started in a
690
- // handler; a chain started by the segment funnel stays DSL (the loader
691
- // scope ALS survives the body's awaits).
692
- // Shell fast path eligibility: a HANDLER-invoked loader executing during a
693
- // capture is handler-layer dynamism — on a handler-free (replayed) HIT it
694
- // would never re-run, freezing its consumption-lane value (#672's "fresh
695
- // per serve" slot shape). Mark the capture; the entry declines the fast
696
- // path and keeps the full tail. DSL loaders re-run on every HIT and never
697
- // set this.
698
- if (!isDslLoader) {
699
- const captureCtx = _getRequestContext();
700
- if (
701
- captureCtx?._shellCaptureRun &&
702
- captureCtx._shellCaptureHandleLiveness
703
- ) {
704
- captureCtx._shellCaptureHandleLiveness.handlerInvokedLoader = true;
705
- }
706
- }
707
-
723
+ // The CONSUMPTION-LANE RULE: a handler-consumed loader's value is a
724
+ // BAKED copy in every shared artifact (cache(), "use cache", the PPR
725
+ // shell). The cache() guard permits its identity reads; a "use cache"
726
+ // body it was entered in and a PPR capture do not (server/context.ts
727
+ // guardIdentityRead).
728
+
729
+ const recordedTags = recordTags ? new Set<string>() : undefined;
730
+ // A loader-cache write that stores or reads this value gates on the
731
+ // earliest start behind it (#977, earliestRecordedStart).
732
+ if (recordedTags) markTagSetStart(recordedTags, executionStart());
733
+ const runEndStore = reportsRunEnd
734
+ ? (reqCtxRef ?? _getRequestContext())?._handleStore
735
+ : undefined;
708
736
  const promise = observePhase(PHASES.loader(loader.$$id), () =>
709
737
  Promise.resolve(
710
738
  runInsideLoaderBodyScope(
711
739
  () => loaderFn(loaderCtx as LoaderContext<any, TEnv>),
712
740
  loader.$$id,
713
- !isDslLoader,
741
+ recordedTags,
714
742
  ),
715
743
  ).finally(() => {
716
744
  pendingLoaders.delete(loader.$$id);
745
+ // Its pushes are made: values replayed for it that none replaced
746
+ // go (a hole that throws or skips its push shows none).
747
+ runEndStore?.settleLoaderRun(loader.$$id);
717
748
  }),
718
749
  );
750
+ if (recordedTags) {
751
+ tagLoaderValue(promise, recordedTags);
752
+ readValueTags(promise);
753
+ const bindingTags = executorOptions?.bindingTags?.get(loader.$$id);
754
+ if (bindingTags) linkRecordedTags(recordedTags, bindingTags);
755
+ }
719
756
 
720
757
  // Auxiliary-lane tracking: keeps the handle store open for this body's
721
758
  // ctx.handle() pushes without joining `settled` (the handler barrier that
@@ -759,8 +796,6 @@ export function setupLoaderAccess<TEnv>(
759
796
  const reqCtxRef = _getRequestContext();
760
797
  const handleStoreRef = reqCtxRef?._handleStore;
761
798
 
762
- const useLoader = createLoaderExecutor(ctx, loaderPromises);
763
-
764
799
  // Loader-cache HIT replay (loader-cache.ts replayLoaderHandles): a cached
765
800
  // entry records its ctx.use dependencies' pushes too, and a dependency is
766
801
  // memoized here and shared with live readers. A loader that already ran
@@ -773,12 +808,33 @@ export function setupLoaderAccess<TEnv>(
773
808
  claimed.add(loaderId);
774
809
  return true;
775
810
  };
811
+ // A loader cache() binding's tags reach every execution of that loader
812
+ // outside the binding: one a reader started before the binding (linked
813
+ // here), a later one, or a refresh's (bindingTags).
814
+ const executor: { bindingTags?: Map<string, Set<string>> } = {};
815
+ internal._bindLoaderCacheTags = (loaderId, tags) => {
816
+ (executor.bindingTags ??= new Map()).set(loaderId, tags);
817
+ const started = loaderPromises.get(loaderId);
818
+ if (started) linkValueTags(started, tags);
819
+ };
820
+ const useLoader = createLoaderExecutor(ctx, loaderPromises, executor);
776
821
  // A refresh's pushes are diverted, so its loader ctxs never claim
777
- // (cache-runtime.ts refreshView).
822
+ // (cache-runtime.ts refreshView). It reads each loader fresh, not through
823
+ // the page's bindings: a stale refresh must not rebuild its entry from
824
+ // another stale entry.
778
825
  internal._runLoaderIsolated = (loader) =>
779
826
  createLoaderExecutor(
780
- Object.create(ctx, { _claimLoaderPushes: { value: undefined } }),
827
+ Object.create(ctx, {
828
+ _claimLoaderPushes: { value: undefined },
829
+ _loaderCacheOverrides: { value: undefined },
830
+ }),
781
831
  new Map(),
832
+ {
833
+ get bindingTags() {
834
+ return executor.bindingTags;
835
+ },
836
+ detached: true,
837
+ },
782
838
  )(loader, null);
783
839
 
784
840
  ctx.use = ((item: LoaderDefinition<any, any> | Handle<any, any>) => {
@@ -1,3 +1,4 @@
1
+ import { requestHeaders } from "../server/request-headers.js";
1
2
  import { AsyncLocalStorage } from "node:async_hooks";
2
3
  import { INTERNAL_RANGO_DEBUG } from "../internal-debug.js";
3
4
 
@@ -61,10 +62,11 @@ function nextId(prefix: string, counter: number): string {
61
62
  }
62
63
 
63
64
  function getHeaderRequestId(request: Request): string | null {
65
+ const headers = requestHeaders(request);
64
66
  const candidate =
65
- request.headers.get("x-rsc-router-request-id") ??
66
- request.headers.get("x-request-id") ??
67
- request.headers.get("cf-ray");
67
+ headers.get("x-rsc-router-request-id") ??
68
+ headers.get("x-request-id") ??
69
+ headers.get("cf-ray");
68
70
  if (!candidate) return null;
69
71
  const trimmed = candidate.trim();
70
72
  return trimmed.length > 0 ? trimmed : null;
@@ -1,8 +1,14 @@
1
+ import { requestHeaders } from "../server/request-headers.js";
1
2
  import {
2
3
  CacheScope,
3
4
  createCacheScope,
4
5
  resolveShellImplicitCacheScope,
5
6
  } from "../cache/cache-scope.js";
7
+ import { armLoaderTagSets, armRecordTagOwners } from "../cache/cache-tag.js";
8
+ import {
9
+ bindsLoaderCache,
10
+ routeLoaderLanes,
11
+ } from "./segment-resolution/loader-cache.js";
6
12
  import { RouteNotFoundError } from "../errors";
7
13
  import {
8
14
  createErrorInfo,
@@ -35,8 +41,7 @@ import {
35
41
  _getRequestContext,
36
42
  setRequestContextPrevRouteKey,
37
43
  } from "../server/request-context.js";
38
- import { isAutoGeneratedRouteName } from "../route-name.js";
39
- import type { DefaultRouteName } from "../types/global-namespace.js";
44
+ import { publicRouteName } from "../route-name.js";
40
45
  import { debugLog, debugWarn } from "./logging.js";
41
46
  import {
42
47
  resolveRoute,
@@ -60,7 +65,7 @@ export async function createMatchContextForFull<TEnv>(
60
65
 
61
66
  const metricsStore = deps.getMetricsStore();
62
67
 
63
- const isHmr = !!request.headers.get("X-RSC-HMR");
68
+ const isHmr = !!requestHeaders(request).get("X-RSC-HMR");
64
69
  // HMR: clear the manifest cache so stale handler references are discarded.
65
70
  if (isHmr) {
66
71
  clearManifestCache();
@@ -150,6 +155,26 @@ export async function createMatchContextForFull<TEnv>(
150
155
  Store.metrics = metricsStore;
151
156
  }
152
157
 
158
+ // A shell capture's implicit doc scope, or a document HIT tail's (which
159
+ // replaces any route-derived scope: the tail replays the shell's doc
160
+ // record), or else the route's own scope.
161
+ const cacheScope = resolveShellImplicitCacheScope(snapshot.cacheScope);
162
+ // A scope means a route cache() record may be written: arm its tag owners
163
+ // before any handler runs (#957). A document HIT tail writes no record and
164
+ // runs no handler, so it arms nothing. A loader bound with its own cache()
165
+ // needs the per-execution loader tag sets before any loader runs (#964):
166
+ // loaders still run on a HIT.
167
+ if (cacheScope && !_getRequestContext()?._shellImplicitCache?.docTail) {
168
+ armRecordTagOwners();
169
+ }
170
+ if (bindsLoaderCache(snapshot.entries)) armLoaderTagSets();
171
+ // A shell capture credits a push under a live-lane loader's body to that
172
+ // loader (shell-capture.ts deriveShellCaptureContext). The lanes are set
173
+ // before any loader runs: a bake-lane loader can start a later one.
174
+ if (reqCtx?._shellCaptureRun) {
175
+ reqCtx._shellCaptureLoaderLanes = routeLoaderLanes(snapshot.entries);
176
+ }
177
+
153
178
  return {
154
179
  request,
155
180
  url: cleanUrl,
@@ -172,23 +197,23 @@ export async function createMatchContextForFull<TEnv>(
172
197
  metricsStore,
173
198
  Store,
174
199
  interceptContextMatch: null,
175
- interceptSelectorContext: {
176
- from: cleanUrl,
177
- to: cleanUrl,
178
- params: matched.params,
179
- request,
180
- env,
181
- segments: { path: [], ids: [] },
182
- toRouteName:
183
- matched.routeKey && !isAutoGeneratedRouteName(matched.routeKey)
184
- ? (matched.routeKey as DefaultRouteName)
185
- : undefined,
186
- },
200
+ interceptSelectorContext: (() => {
201
+ const here = {
202
+ url: cleanUrl,
203
+ params: matched.params,
204
+ routeName: publicRouteName(matched.routeKey),
205
+ };
206
+ return {
207
+ from: here,
208
+ to: here,
209
+ request,
210
+ env,
211
+ segments: { path: [], ids: [] },
212
+ };
213
+ })(),
187
214
  isSameRouteNavigation: false,
188
215
  interceptResult: null,
189
- // Shell fast path: a capture or an eligible document/navigation HIT may
190
- // substitute an implicit doc-level scope (a route-derived scope wins).
191
- cacheScope: resolveShellImplicitCacheScope(snapshot.cacheScope),
216
+ cacheScope,
192
217
  isIntercept: false,
193
218
  actionContext: undefined,
194
219
  isAction: false,
@@ -217,7 +242,7 @@ export async function createMatchContextForPartial<TEnv>(
217
242
 
218
243
  const metricsStore = deps.getMetricsStore();
219
244
 
220
- const isHmr = !!request.headers.get("X-RSC-HMR");
245
+ const isHmr = !!requestHeaders(request).get("X-RSC-HMR");
221
246
 
222
247
  // HMR: clear manifest cache so stale handler references are discarded
223
248
  if (isHmr) {
@@ -335,31 +360,27 @@ export async function createMatchContextForPartial<TEnv>(
335
360
  });
336
361
  }
337
362
 
338
- setRequestContextPrevRouteKey(
339
- nav.effectiveFromMatch?.routeKey,
340
- nav.effectiveFromUrl,
341
- nav.effectiveFromMatch?.params ?? nav.prevParams,
342
- );
363
+ setRequestContextPrevRouteKey(nav.effectiveFromMatch?.routeKey);
343
364
 
344
365
  const interceptSelectorContext: InterceptSelectorContext = {
345
- from: nav.effectiveFromUrl,
346
- to: cleanUrl,
347
- params: matched.params,
366
+ // The source's params come from matching its URL (the intercept source
367
+ // while one is open), like its route name.
368
+ from: {
369
+ url: nav.effectiveFromUrl,
370
+ params: nav.effectiveFromMatch?.params ?? {},
371
+ routeName: publicRouteName(nav.effectiveFromMatch?.routeKey),
372
+ },
373
+ to: {
374
+ url: cleanUrl,
375
+ params: matched.params,
376
+ routeName: publicRouteName(matched.routeKey),
377
+ },
348
378
  request,
349
379
  env,
350
380
  segments: {
351
381
  path: nav.effectiveFromUrl.pathname.split("/").filter(Boolean),
352
382
  ids: nav.filteredSegmentIds,
353
383
  },
354
- fromRouteName:
355
- nav.effectiveFromMatch?.routeKey &&
356
- !isAutoGeneratedRouteName(nav.effectiveFromMatch.routeKey)
357
- ? (nav.effectiveFromMatch.routeKey as DefaultRouteName)
358
- : undefined,
359
- toRouteName:
360
- matched.routeKey && !isAutoGeneratedRouteName(matched.routeKey)
361
- ? (matched.routeKey as DefaultRouteName)
362
- : undefined,
363
384
  };
364
385
  const isAction = !!actionContext;
365
386
 
@@ -400,7 +421,37 @@ export async function createMatchContextForPartial<TEnv>(
400
421
  clientSegmentSet.delete(snapshot.manifestEntry.shortCode);
401
422
  }
402
423
 
424
+ // Whether an intercept targets this route at all: the same walk as above
425
+ // with isAction=true, which skips when(). If one does, the response depends
426
+ // on the source even when none applied (a when() that rejected it, a
427
+ // same-route navigation), so it must not share the source-agnostic prefetch
428
+ // slot (#1007). The walk is over the TARGET's chain, so a route no
429
+ // intercept targets never depends on the source this way.
430
+ const targetsIntercept = (routeKey: string): boolean =>
431
+ findInterceptForRoute(
432
+ routeKey,
433
+ snapshot.manifestEntry.parent,
434
+ null,
435
+ true,
436
+ ) !== null;
437
+ const interceptTargeted =
438
+ !!interceptResult ||
439
+ (!skipInterceptForAction &&
440
+ (targetsIntercept(matched.routeKey) ||
441
+ (snapshot.localRouteName !== matched.routeKey &&
442
+ targetsIntercept(snapshot.localRouteName))));
443
+
403
444
  const isIntercept = !!interceptResult;
445
+ // A normal-route navigation may replay a PPR shell's canonical segment
446
+ // snapshot. Intercepts remain source-dependent and always use their normal
447
+ // cache path.
448
+ const cacheScope = isIntercept
449
+ ? snapshot.cacheScope
450
+ : resolveShellImplicitCacheScope(snapshot.cacheScope);
451
+ // A scope means a route cache() record may be written: arm its tag owners
452
+ // before any handler runs (#957); see the full-match site for #964's arm.
453
+ if (cacheScope) armRecordTagOwners();
454
+ if (bindsLoaderCache(snapshot.entries)) armLoaderTagSets();
404
455
 
405
456
  return {
406
457
  request,
@@ -427,12 +478,8 @@ export async function createMatchContextForPartial<TEnv>(
427
478
  interceptSelectorContext,
428
479
  isSameRouteNavigation: nav.isSameRouteNavigation,
429
480
  interceptResult,
430
- // A normal-route navigation may replay a PPR shell's canonical segment
431
- // snapshot. Intercepts remain source-dependent and always use their normal
432
- // cache path.
433
- cacheScope: isIntercept
434
- ? snapshot.cacheScope
435
- : resolveShellImplicitCacheScope(snapshot.cacheScope),
481
+ interceptTargeted,
482
+ cacheScope,
436
483
  isIntercept,
437
484
  actionContext,
438
485
  isAction,
@@ -176,6 +176,11 @@ export interface MatchContext<TEnv = any> {
176
176
  interceptSelectorContext: InterceptSelectorContext;
177
177
  isSameRouteNavigation: boolean;
178
178
  interceptResult: InterceptResult | null;
179
+ /**
180
+ * An intercept targets this route, matched or not (a same-route navigation
181
+ * skips it), so the response depends on the source. Partial matches only.
182
+ */
183
+ interceptTargeted?: boolean;
179
184
 
180
185
  // Cache
181
186
  cacheScope: CacheScope | null;
@@ -9,7 +9,6 @@ import type {
9
9
  NotFoundBoundaryHandler,
10
10
  } from "../types";
11
11
  import {
12
- isPprEntry,
13
12
  type EntryData,
14
13
  type InterceptEntry,
15
14
  type InterceptSelectorContext,
@@ -47,7 +46,8 @@ import {
47
46
  buildCacheSignalSegments,
48
47
  } from "./telemetry.js";
49
48
  import { _getRequestContext } from "../server/request-context.js";
50
- import { evaluatePprTransitionWhen } from "./transition-when.js";
49
+ import { ShellRecordUnavailableError } from "../cache/shell-snapshot.js";
50
+ import { recordTransitionWhenRefs } from "./transition-when.js";
51
51
 
52
52
  /**
53
53
  * Per-call telemetry lifecycle emitter for match()/matchPartial(). Each method
@@ -234,33 +234,14 @@ export function createMatchHandlers<TEnv = any>(
234
234
  if (reqCtx) reqCtx._cacheSignal = segments;
235
235
  };
236
236
 
237
- const evaluatePprTransitionWhenForMatch = (
238
- ctx: MatchContext<TEnv>,
239
- isPartial: boolean,
240
- ): void => {
237
+ const recordTransitionWhenRefsForMatch = (ctx: MatchContext<TEnv>): void => {
241
238
  const reqCtx = _getRequestContext();
242
239
  if (!reqCtx) return;
243
- reqCtx._pprTransitionDecisions = undefined;
244
- if (!isPprEntry(ctx.manifestEntry)) return;
245
-
246
- evaluatePprTransitionWhen(
247
- ctx.entries,
248
- reqCtx,
249
- {
250
- params: ctx.matched.params,
251
- routeName: ctx.interceptSelectorContext.toRouteName,
252
- },
253
- (error, segmentId) =>
254
- callOnError(error, "rendering", {
255
- request: ctx.request,
256
- url: ctx.url,
257
- env: ctx.env,
258
- params: ctx.matched.params,
259
- segmentId,
260
- isPartial,
261
- handledByBoundary: false,
262
- }),
263
- );
240
+ const entry = ctx.manifestEntry;
241
+ recordTransitionWhenRefs(ctx.entries, reqCtx, {
242
+ routeName: ctx.routeKey,
243
+ pattern: entry?.type === "route" ? entry.pattern : undefined,
244
+ });
264
245
  };
265
246
 
266
247
  async function createMatchContextForFull(
@@ -349,7 +330,7 @@ export function createMatchHandlers<TEnv = any>(
349
330
  }
350
331
 
351
332
  const ctx = result as MatchContext<TEnv>;
352
- evaluatePprTransitionWhenForMatch(ctx, false);
333
+ recordTransitionWhenRefsForMatch(ctx);
353
334
 
354
335
  try {
355
336
  const state = createPipelineState();
@@ -380,6 +361,10 @@ export function createMatchHandlers<TEnv = any>(
380
361
  emitter.end(0, false, error.status);
381
362
  throw error;
382
363
  }
364
+ // A document shell HIT tail's doc record could not supply the
365
+ // handler layer: a serve-path signal serveShellHit degrades, not a
366
+ // routing error (the record's decode failure was already reported).
367
+ if (error instanceof ShellRecordUnavailableError) throw error;
383
368
  emitter.error(
384
369
  error instanceof Error ? error : new Error(String(error)),
385
370
  "routing",
@@ -461,7 +446,7 @@ export function createMatchHandlers<TEnv = any>(
461
446
  emitter.end(0, false);
462
447
  return null;
463
448
  }
464
- evaluatePprTransitionWhenForMatch(ctx, true);
449
+ recordTransitionWhenRefsForMatch(ctx);
465
450
 
466
451
  if (isRouterDebugEnabled()) {
467
452
  startRevalidationTrace({