@rangojs/router 0.0.0-experimental.259 → 0.0.0-experimental.26

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 (225) hide show
  1. package/README.md +294 -28
  2. package/dist/bin/rango.js +355 -47
  3. package/dist/vite/index.js +1658 -1239
  4. package/package.json +3 -3
  5. package/skills/cache-guide/SKILL.md +9 -5
  6. package/skills/caching/SKILL.md +4 -4
  7. package/skills/document-cache/SKILL.md +2 -2
  8. package/skills/hooks/SKILL.md +40 -29
  9. package/skills/host-router/SKILL.md +218 -0
  10. package/skills/intercept/SKILL.md +79 -0
  11. package/skills/layout/SKILL.md +62 -2
  12. package/skills/loader/SKILL.md +229 -15
  13. package/skills/middleware/SKILL.md +109 -30
  14. package/skills/parallel/SKILL.md +57 -2
  15. package/skills/prerender/SKILL.md +189 -19
  16. package/skills/rango/SKILL.md +1 -2
  17. package/skills/response-routes/SKILL.md +3 -3
  18. package/skills/route/SKILL.md +44 -3
  19. package/skills/router-setup/SKILL.md +80 -3
  20. package/skills/theme/SKILL.md +5 -4
  21. package/skills/typesafety/SKILL.md +59 -16
  22. package/skills/use-cache/SKILL.md +16 -2
  23. package/src/__internal.ts +1 -1
  24. package/src/bin/rango.ts +56 -19
  25. package/src/browser/action-coordinator.ts +97 -0
  26. package/src/browser/event-controller.ts +29 -48
  27. package/src/browser/history-state.ts +80 -0
  28. package/src/browser/intercept-utils.ts +1 -1
  29. package/src/browser/link-interceptor.ts +19 -3
  30. package/src/browser/merge-segment-loaders.ts +9 -2
  31. package/src/browser/navigation-bridge.ts +66 -443
  32. package/src/browser/navigation-client.ts +34 -62
  33. package/src/browser/navigation-store.ts +4 -33
  34. package/src/browser/navigation-transaction.ts +295 -0
  35. package/src/browser/partial-update.ts +103 -151
  36. package/src/browser/prefetch/cache.ts +67 -0
  37. package/src/browser/prefetch/fetch.ts +137 -0
  38. package/src/browser/prefetch/observer.ts +65 -0
  39. package/src/browser/prefetch/policy.ts +42 -0
  40. package/src/browser/prefetch/queue.ts +88 -0
  41. package/src/browser/rango-state.ts +112 -0
  42. package/src/browser/react/Link.tsx +154 -44
  43. package/src/browser/react/NavigationProvider.tsx +32 -0
  44. package/src/browser/react/context.ts +6 -0
  45. package/src/browser/react/filter-segment-order.ts +11 -0
  46. package/src/browser/react/index.ts +2 -6
  47. package/src/browser/react/location-state-shared.ts +29 -11
  48. package/src/browser/react/location-state.ts +6 -4
  49. package/src/browser/react/nonce-context.ts +23 -0
  50. package/src/browser/react/shallow-equal.ts +27 -0
  51. package/src/browser/react/use-action.ts +23 -45
  52. package/src/browser/react/use-client-cache.ts +5 -3
  53. package/src/browser/react/use-handle.ts +21 -64
  54. package/src/browser/react/use-navigation.ts +7 -32
  55. package/src/browser/react/use-params.ts +5 -34
  56. package/src/browser/react/use-pathname.ts +2 -3
  57. package/src/browser/react/use-router.ts +3 -6
  58. package/src/browser/react/use-search-params.ts +2 -1
  59. package/src/browser/react/use-segments.ts +75 -114
  60. package/src/browser/response-adapter.ts +73 -0
  61. package/src/browser/rsc-router.tsx +46 -22
  62. package/src/browser/scroll-restoration.ts +10 -7
  63. package/src/browser/server-action-bridge.ts +458 -405
  64. package/src/browser/types.ts +21 -35
  65. package/src/browser/validate-redirect-origin.ts +29 -0
  66. package/src/build/generate-manifest.ts +38 -13
  67. package/src/build/generate-route-types.ts +4 -0
  68. package/src/build/index.ts +1 -0
  69. package/src/build/route-trie.ts +19 -3
  70. package/src/build/route-types/codegen.ts +13 -4
  71. package/src/build/route-types/include-resolution.ts +13 -0
  72. package/src/build/route-types/per-module-writer.ts +15 -3
  73. package/src/build/route-types/router-processing.ts +170 -18
  74. package/src/build/runtime-discovery.ts +13 -1
  75. package/src/cache/background-task.ts +34 -0
  76. package/src/cache/cache-key-utils.ts +44 -0
  77. package/src/cache/cache-policy.ts +125 -0
  78. package/src/cache/cache-runtime.ts +136 -123
  79. package/src/cache/cache-scope.ts +76 -83
  80. package/src/cache/cf/cf-cache-store.ts +12 -7
  81. package/src/cache/document-cache.ts +93 -69
  82. package/src/cache/handle-capture.ts +81 -0
  83. package/src/cache/index.ts +0 -15
  84. package/src/cache/memory-segment-store.ts +43 -69
  85. package/src/cache/profile-registry.ts +43 -8
  86. package/src/cache/read-through-swr.ts +134 -0
  87. package/src/cache/segment-codec.ts +140 -117
  88. package/src/cache/taint.ts +30 -3
  89. package/src/cache/types.ts +1 -115
  90. package/src/client.rsc.tsx +0 -1
  91. package/src/client.tsx +53 -76
  92. package/src/errors.ts +6 -1
  93. package/src/handle.ts +1 -1
  94. package/src/handles/MetaTags.tsx +5 -2
  95. package/src/host/cookie-handler.ts +8 -3
  96. package/src/host/index.ts +0 -3
  97. package/src/host/router.ts +14 -1
  98. package/src/href-client.ts +3 -1
  99. package/src/index.rsc.ts +53 -10
  100. package/src/index.ts +73 -43
  101. package/src/loader.rsc.ts +12 -4
  102. package/src/loader.ts +8 -0
  103. package/src/prerender/store.ts +60 -18
  104. package/src/prerender.ts +76 -18
  105. package/src/reverse.ts +11 -7
  106. package/src/root-error-boundary.tsx +30 -26
  107. package/src/route-definition/dsl-helpers.ts +9 -6
  108. package/src/route-definition/index.ts +0 -3
  109. package/src/route-definition/redirect.ts +15 -3
  110. package/src/route-map-builder.ts +38 -2
  111. package/src/route-name.ts +53 -0
  112. package/src/route-types.ts +7 -0
  113. package/src/router/content-negotiation.ts +1 -1
  114. package/src/router/debug-manifest.ts +16 -3
  115. package/src/router/handler-context.ts +96 -17
  116. package/src/router/intercept-resolution.ts +6 -4
  117. package/src/router/lazy-includes.ts +4 -0
  118. package/src/router/loader-resolution.ts +6 -11
  119. package/src/router/logging.ts +100 -3
  120. package/src/router/manifest.ts +32 -3
  121. package/src/router/match-api.ts +62 -54
  122. package/src/router/match-context.ts +3 -0
  123. package/src/router/match-handlers.ts +185 -11
  124. package/src/router/match-middleware/background-revalidation.ts +65 -85
  125. package/src/router/match-middleware/cache-lookup.ts +78 -10
  126. package/src/router/match-middleware/cache-store.ts +2 -0
  127. package/src/router/match-pipelines.ts +8 -43
  128. package/src/router/match-result.ts +0 -9
  129. package/src/router/metrics.ts +233 -13
  130. package/src/router/middleware-types.ts +34 -39
  131. package/src/router/middleware.ts +290 -130
  132. package/src/router/pattern-matching.ts +61 -10
  133. package/src/router/prerender-match.ts +36 -6
  134. package/src/router/preview-match.ts +7 -1
  135. package/src/router/revalidation.ts +61 -2
  136. package/src/router/router-context.ts +15 -0
  137. package/src/router/router-interfaces.ts +158 -40
  138. package/src/router/router-options.ts +223 -1
  139. package/src/router/router-registry.ts +5 -2
  140. package/src/router/segment-resolution/fresh.ts +165 -242
  141. package/src/router/segment-resolution/helpers.ts +263 -0
  142. package/src/router/segment-resolution/loader-cache.ts +102 -98
  143. package/src/router/segment-resolution/revalidation.ts +394 -272
  144. package/src/router/segment-resolution/static-store.ts +2 -2
  145. package/src/router/segment-resolution.ts +1 -3
  146. package/src/router/segment-wrappers.ts +3 -0
  147. package/src/router/telemetry-otel.ts +299 -0
  148. package/src/router/telemetry.ts +300 -0
  149. package/src/router/timeout.ts +148 -0
  150. package/src/router/trie-matching.ts +20 -2
  151. package/src/router/types.ts +7 -1
  152. package/src/router.ts +203 -18
  153. package/src/rsc/handler-context.ts +13 -2
  154. package/src/rsc/handler.ts +489 -438
  155. package/src/rsc/helpers.ts +125 -5
  156. package/src/rsc/index.ts +0 -20
  157. package/src/rsc/loader-fetch.ts +84 -42
  158. package/src/rsc/manifest-init.ts +3 -2
  159. package/src/rsc/origin-guard.ts +141 -0
  160. package/src/rsc/progressive-enhancement.ts +245 -19
  161. package/src/rsc/response-route-handler.ts +347 -0
  162. package/src/rsc/rsc-rendering.ts +47 -43
  163. package/src/rsc/runtime-warnings.ts +42 -0
  164. package/src/rsc/server-action.ts +166 -66
  165. package/src/rsc/ssr-setup.ts +128 -0
  166. package/src/rsc/types.ts +20 -2
  167. package/src/search-params.ts +38 -23
  168. package/src/server/context.ts +61 -7
  169. package/src/server/cookie-store.ts +190 -0
  170. package/src/server/fetchable-loader-store.ts +11 -6
  171. package/src/server/handle-store.ts +84 -12
  172. package/src/server/loader-registry.ts +11 -46
  173. package/src/server/request-context.ts +275 -49
  174. package/src/server.ts +6 -0
  175. package/src/ssr/index.tsx +67 -28
  176. package/src/static-handler.ts +7 -0
  177. package/src/theme/ThemeProvider.tsx +6 -1
  178. package/src/theme/index.ts +4 -18
  179. package/src/theme/theme-context.ts +1 -28
  180. package/src/theme/theme-script.ts +2 -1
  181. package/src/types/cache-types.ts +6 -1
  182. package/src/types/error-types.ts +3 -0
  183. package/src/types/global-namespace.ts +22 -0
  184. package/src/types/handler-context.ts +103 -16
  185. package/src/types/index.ts +1 -1
  186. package/src/types/loader-types.ts +9 -6
  187. package/src/types/route-config.ts +17 -26
  188. package/src/types/route-entry.ts +28 -0
  189. package/src/types/segments.ts +0 -5
  190. package/src/urls/include-helper.ts +49 -8
  191. package/src/urls/index.ts +1 -0
  192. package/src/urls/path-helper-types.ts +30 -12
  193. package/src/urls/path-helper.ts +17 -2
  194. package/src/urls/pattern-types.ts +21 -1
  195. package/src/urls/response-types.ts +29 -7
  196. package/src/urls/type-extraction.ts +23 -15
  197. package/src/use-loader.tsx +27 -9
  198. package/src/vite/discovery/bundle-postprocess.ts +32 -52
  199. package/src/vite/discovery/discover-routers.ts +52 -26
  200. package/src/vite/discovery/prerender-collection.ts +58 -41
  201. package/src/vite/discovery/route-types-writer.ts +7 -7
  202. package/src/vite/discovery/state.ts +7 -7
  203. package/src/vite/discovery/virtual-module-codegen.ts +5 -2
  204. package/src/vite/index.ts +10 -51
  205. package/src/vite/plugins/client-ref-dedup.ts +115 -0
  206. package/src/vite/plugins/client-ref-hashing.ts +3 -3
  207. package/src/vite/plugins/expose-internal-ids.ts +4 -3
  208. package/src/vite/plugins/refresh-cmd.ts +65 -0
  209. package/src/vite/plugins/use-cache-transform.ts +91 -3
  210. package/src/vite/plugins/version-plugin.ts +188 -18
  211. package/src/vite/rango.ts +61 -36
  212. package/src/vite/router-discovery.ts +173 -100
  213. package/src/vite/utils/prerender-utils.ts +81 -0
  214. package/src/vite/utils/shared-utils.ts +19 -9
  215. package/skills/testing/SKILL.md +0 -226
  216. package/src/browser/lru-cache.ts +0 -61
  217. package/src/browser/react/prefetch.ts +0 -27
  218. package/src/browser/request-controller.ts +0 -164
  219. package/src/cache/memory-store.ts +0 -253
  220. package/src/href-context.ts +0 -33
  221. package/src/route-definition/route-function.ts +0 -119
  222. package/src/router.gen.ts +0 -6
  223. package/src/static-handler.gen.ts +0 -5
  224. package/src/urls.gen.ts +0 -8
  225. /package/{CLAUDE.md → AGENTS.md} +0 -0
@@ -7,23 +7,14 @@
7
7
  */
8
8
 
9
9
  import type { ReactNode } from "react";
10
- import { DataNotFoundError, invariant } from "../../errors";
11
- import {
12
- createErrorInfo,
13
- createErrorSegment,
14
- createNotFoundInfo,
15
- createNotFoundSegment,
16
- } from "../error-handling.js";
10
+ import { invariant } from "../../errors";
17
11
  import { revalidate } from "../loader-resolution.js";
18
12
  import { evaluateRevalidation } from "../revalidation.js";
19
- import { getRequestContext } from "../../server/request-context.js";
20
- import { DefaultErrorFallback } from "../../default-error-boundary.js";
21
13
  import type { EntryData } from "../../server/context";
22
14
  import type {
23
15
  HandlerContext,
24
16
  InternalHandlerContext,
25
17
  ResolvedSegment,
26
- ErrorInfo,
27
18
  ShouldRevalidateFn,
28
19
  } from "../../types";
29
20
  import type {
@@ -31,10 +22,98 @@ import type {
31
22
  SegmentRevalidationResult,
32
23
  ActionContext,
33
24
  } from "../types.js";
34
- import { debugLog } from "../logging.js";
35
- import { tryStaticLookup } from "./static-store.js";
36
- import { handleHandlerResult } from "./fresh.js";
25
+ import {
26
+ debugLog,
27
+ pushRevalidationTraceEntry,
28
+ isTraceActive,
29
+ } from "../logging.js";
37
30
  import { resolveLoaderData } from "./loader-cache.js";
31
+ import {
32
+ handleHandlerResult,
33
+ tryStaticHandler,
34
+ tryStaticSlot,
35
+ resolveLayoutComponent,
36
+ resolveWithErrorBoundary,
37
+ } from "./helpers.js";
38
+ import { getRouterContext } from "../router-context.js";
39
+ import { resolveSink, safeEmit } from "../telemetry.js";
40
+ import { track } from "../../server/context.js";
41
+
42
+ // ---------------------------------------------------------------------------
43
+ // Telemetry helpers
44
+ // ---------------------------------------------------------------------------
45
+
46
+ /**
47
+ * Attach a fire-and-forget rejection observer to a streamed handler promise.
48
+ * Silently no-ops when called outside RouterContext (e.g. in unit tests).
49
+ */
50
+ function observeStreamedHandler(
51
+ promise: Promise<ReactNode>,
52
+ segmentId: string,
53
+ segmentType: string,
54
+ pathname?: string,
55
+ routeKey?: string,
56
+ params?: Record<string, string>,
57
+ ): void {
58
+ let routerCtx;
59
+ try {
60
+ routerCtx = getRouterContext();
61
+ } catch {
62
+ return;
63
+ }
64
+ if (!routerCtx?.telemetry) return;
65
+ const sink = resolveSink(routerCtx.telemetry);
66
+ const reqId = routerCtx.requestId;
67
+ promise.catch((err: unknown) => {
68
+ const errorObj = err instanceof Error ? err : new Error(String(err));
69
+ safeEmit(sink, {
70
+ type: "handler.error",
71
+ timestamp: performance.now(),
72
+ requestId: reqId,
73
+ segmentId,
74
+ segmentType,
75
+ error: errorObj,
76
+ handledByBoundary: true,
77
+ pathname,
78
+ routeKey,
79
+ params,
80
+ });
81
+ });
82
+ }
83
+
84
+ // ---------------------------------------------------------------------------
85
+ // Revalidation telemetry helper
86
+ // ---------------------------------------------------------------------------
87
+
88
+ /**
89
+ * Emit revalidation.decision telemetry for a segment if a sink is configured.
90
+ * Called after evaluateRevalidation returns to capture the decision.
91
+ * Silently no-ops when called outside RouterContext (e.g. in unit tests).
92
+ */
93
+ function emitRevalidationDecision(
94
+ segmentId: string,
95
+ pathname: string,
96
+ routeKey: string,
97
+ shouldRevalidate: boolean,
98
+ ): void {
99
+ let routerCtx;
100
+ try {
101
+ routerCtx = getRouterContext();
102
+ } catch {
103
+ return;
104
+ }
105
+ if (routerCtx?.telemetry) {
106
+ safeEmit(resolveSink(routerCtx.telemetry), {
107
+ type: "revalidation.decision",
108
+ timestamp: performance.now(),
109
+ requestId: routerCtx.requestId,
110
+ segmentId,
111
+ pathname,
112
+ routeKey,
113
+ shouldRevalidate,
114
+ });
115
+ }
116
+ }
38
117
 
39
118
  // ---------------------------------------------------------------------------
40
119
  // Revalidation path (partial match)
@@ -85,7 +164,20 @@ export async function resolveLoadersWithRevalidation<TEnv>(
85
164
  }) => {
86
165
  const shouldRun = await revalidate(
87
166
  async () => {
88
- if (!clientSegmentIds.has(segmentId)) return true;
167
+ if (!clientSegmentIds.has(segmentId)) {
168
+ if (isTraceActive()) {
169
+ pushRevalidationTraceEntry({
170
+ segmentId,
171
+ segmentType: "loader",
172
+ belongsToRoute,
173
+ source: "loader",
174
+ defaultShouldRevalidate: true,
175
+ finalShouldRevalidate: true,
176
+ reason: "new-segment",
177
+ });
178
+ }
179
+ return true;
180
+ }
89
181
 
90
182
  const dummySegment: ResolvedSegment = {
91
183
  id: segmentId,
@@ -113,11 +205,13 @@ export async function resolveLoadersWithRevalidation<TEnv>(
113
205
  context: ctx,
114
206
  actionContext,
115
207
  stale,
208
+ traceSource: "loader",
116
209
  });
117
210
  },
118
211
  async () => true,
119
212
  () => false,
120
213
  );
214
+ emitRevalidationDecision(segmentId, ctx.pathname, routeKey, shouldRun);
121
215
  return { shouldRun, loaderEntry, loader, segmentId, index };
122
216
  },
123
217
  ),
@@ -160,6 +254,7 @@ export async function resolveLoadersOnlyWithRevalidation<TEnv>(
160
254
  routeKey: string,
161
255
  deps: SegmentResolutionDeps<TEnv>,
162
256
  actionContext?: ActionContext,
257
+ stale?: boolean,
163
258
  ): Promise<{ segments: ResolvedSegment[]; matchedIds: string[] }> {
164
259
  const allLoaderSegments: ResolvedSegment[] = [];
165
260
  const allMatchedIds: string[] = [];
@@ -178,6 +273,8 @@ export async function resolveLoadersOnlyWithRevalidation<TEnv>(
178
273
  routeKey,
179
274
  deps,
180
275
  actionContext,
276
+ undefined, // shortCodeOverride
277
+ stale,
181
278
  );
182
279
  allLoaderSegments.push(...segments);
183
280
  allMatchedIds.push(...matchedIds);
@@ -283,9 +380,35 @@ export async function resolveParallelSegmentsWithRevalidation<TEnv>(
283
380
  }
284
381
 
285
382
  const shouldResolve = await (async () => {
286
- if (isFullRefetch) return true;
287
- if (!clientSegmentIds.has(parallelId))
288
- return belongsToRoute || isNewParent;
383
+ if (isFullRefetch) {
384
+ if (isTraceActive()) {
385
+ pushRevalidationTraceEntry({
386
+ segmentId: parallelId,
387
+ segmentType: "parallel",
388
+ belongsToRoute,
389
+ source: "parallel",
390
+ defaultShouldRevalidate: true,
391
+ finalShouldRevalidate: true,
392
+ reason: "full-refetch",
393
+ });
394
+ }
395
+ return true;
396
+ }
397
+ if (!clientSegmentIds.has(parallelId)) {
398
+ const result = belongsToRoute || isNewParent;
399
+ if (isTraceActive()) {
400
+ pushRevalidationTraceEntry({
401
+ segmentId: parallelId,
402
+ segmentType: "parallel",
403
+ belongsToRoute,
404
+ source: "parallel",
405
+ defaultShouldRevalidate: result,
406
+ finalShouldRevalidate: result,
407
+ reason: result ? "new-segment" : "skip-parent-chain",
408
+ });
409
+ }
410
+ return result;
411
+ }
289
412
 
290
413
  const dummySegment: ResolvedSegment = {
291
414
  id: parallelId,
@@ -317,14 +440,19 @@ export async function resolveParallelSegmentsWithRevalidation<TEnv>(
317
440
  context,
318
441
  actionContext,
319
442
  stale,
443
+ traceSource: "parallel",
320
444
  });
321
445
  })();
446
+ emitRevalidationDecision(
447
+ parallelId,
448
+ context.pathname,
449
+ routeKey,
450
+ shouldResolve,
451
+ );
322
452
 
323
453
  let component: ReactNode | undefined;
324
- // Static handler interception for individual parallel slots
325
- const slotStaticId = (parallelEntry as any).staticHandlerIds?.[slot];
326
- if (slotStaticId && shouldResolve) {
327
- component = await tryStaticLookup(slotStaticId, parallelId);
454
+ if (shouldResolve) {
455
+ component = await tryStaticSlot(parallelEntry, slot, parallelId);
328
456
  }
329
457
  if (component === undefined) {
330
458
  const hasLoadingFallback =
@@ -333,9 +461,25 @@ export async function resolveParallelSegmentsWithRevalidation<TEnv>(
333
461
  if (!shouldResolve) {
334
462
  component = null;
335
463
  } else if (hasLoadingFallback) {
336
- component = (
337
- typeof handler === "function" ? handler(context) : handler
338
- ) as ReactNode;
464
+ const result =
465
+ typeof handler === "function" ? handler(context) : handler;
466
+ if (result instanceof Promise) {
467
+ const tracked = deps.trackHandler(result, {
468
+ segmentId: parallelId,
469
+ segmentType: "parallel",
470
+ });
471
+ observeStreamedHandler(
472
+ tracked,
473
+ parallelId,
474
+ "parallel",
475
+ context.pathname,
476
+ routeKey,
477
+ params,
478
+ );
479
+ component = tracked as ReactNode;
480
+ } else {
481
+ component = result as ReactNode;
482
+ }
339
483
  } else {
340
484
  component =
341
485
  typeof handler === "function" ? await handler(context) : handler;
@@ -413,7 +557,24 @@ export async function resolveEntryHandlerWithRevalidation<TEnv>(
413
557
  clientHasSegment: hasSegment,
414
558
  belongsToRoute,
415
559
  });
416
- if (!hasSegment) return true;
560
+ if (!hasSegment) {
561
+ if (isTraceActive()) {
562
+ const segType =
563
+ entry.type === "cache"
564
+ ? "layout"
565
+ : (entry.type as "layout" | "route");
566
+ pushRevalidationTraceEntry({
567
+ segmentId: entry.shortCode,
568
+ segmentType: segType,
569
+ belongsToRoute,
570
+ source: "segment-resolution",
571
+ defaultShouldRevalidate: true,
572
+ finalShouldRevalidate: true,
573
+ reason: "new-segment",
574
+ });
575
+ }
576
+ return true;
577
+ }
417
578
 
418
579
  const dummySegment: ResolvedSegment = {
419
580
  id: entry.shortCode,
@@ -448,6 +609,12 @@ export async function resolveEntryHandlerWithRevalidation<TEnv>(
448
609
  actionContext,
449
610
  stale,
450
611
  });
612
+ emitRevalidationDecision(
613
+ entry.shortCode,
614
+ context.pathname,
615
+ routeKey,
616
+ shouldRevalidate,
617
+ );
451
618
  debugLog("segment.revalidate", "entry revalidation decision", {
452
619
  segmentId: entry.shortCode,
453
620
  shouldRevalidate,
@@ -455,40 +622,55 @@ export async function resolveEntryHandlerWithRevalidation<TEnv>(
455
622
  return shouldRevalidate;
456
623
  },
457
624
  async () => {
625
+ const doneHandler = track(`handler:${entry.id}`, 2);
458
626
  (context as InternalHandlerContext<any, TEnv>)._currentSegmentId =
459
627
  entry.shortCode;
460
- // Static handler interception: use pre-rendered component from build-time store
461
- const entryAny = entry as any;
462
- if (entryAny.isStaticPrerender && entryAny.staticHandlerId) {
463
- const staticComponent = await tryStaticLookup(
464
- entryAny.staticHandlerId,
465
- entry.shortCode,
466
- );
467
- if (staticComponent !== undefined) return staticComponent;
468
- }
469
628
  if (entry.type === "layout" || entry.type === "cache") {
470
- return typeof entry.handler === "function"
471
- ? handleHandlerResult(await entry.handler(context))
472
- : entry.handler;
629
+ const layoutComponent = await resolveLayoutComponent(entry, context);
630
+ doneHandler();
631
+ return layoutComponent;
632
+ }
633
+ const staticComponent = await tryStaticHandler(entry, entry.shortCode);
634
+ if (staticComponent !== undefined) {
635
+ doneHandler();
636
+ return staticComponent;
473
637
  }
474
638
  const routeEntry = entry as Extract<EntryData, { type: "route" }>;
475
639
  if (!routeEntry.loading) {
476
- return handleHandlerResult(await routeEntry.handler(context));
640
+ const result = handleHandlerResult(await routeEntry.handler(context));
641
+ doneHandler();
642
+ return result;
477
643
  }
478
644
  if (!actionContext) {
479
645
  const result = handleHandlerResult(routeEntry.handler(context));
480
- return {
481
- content:
482
- result instanceof Promise ? deps.trackHandler(result) : result,
483
- };
646
+ if (result instanceof Promise) {
647
+ result.finally(doneHandler).catch(() => {});
648
+ const tracked = deps.trackHandler(result, {
649
+ segmentId: entry.shortCode,
650
+ segmentType: entry.type,
651
+ });
652
+ observeStreamedHandler(
653
+ tracked,
654
+ entry.shortCode,
655
+ entry.type,
656
+ context.pathname,
657
+ routeKey,
658
+ params,
659
+ );
660
+ return { content: tracked };
661
+ }
662
+ doneHandler();
663
+ return { content: result };
484
664
  }
485
665
  debugLog("segment.action", "resolving action route with awaited value", {
486
666
  entryId: entry.id,
487
667
  });
668
+ const actionResult = handleHandlerResult(
669
+ await routeEntry.handler(context),
670
+ );
671
+ doneHandler();
488
672
  return {
489
- content: Promise.resolve(
490
- handleHandlerResult(await routeEntry.handler(context)),
491
- ),
673
+ content: Promise.resolve(actionResult),
492
674
  };
493
675
  },
494
676
  () => null,
@@ -605,31 +787,32 @@ export async function resolveSegmentWithRevalidation<TEnv>(
605
787
  }
606
788
  }
607
789
 
608
- const parallelResult = await resolveParallelSegmentsWithRevalidation(
609
- entry,
610
- params,
611
- context,
612
- belongsToRoute,
613
- clientSegmentIds,
614
- prevParams,
615
- request,
616
- prevUrl,
617
- nextUrl,
618
- routeKey,
619
- deps,
620
- actionContext,
621
- stale,
622
- );
623
- segments.push(...parallelResult.segments);
624
- matchedIds.push(...parallelResult.matchedIds);
625
-
626
- // Push handler BEFORE orphan layouts for layout/cache entries (matching SSR
627
- // order in resolveSegment). Route handler was already executed and is pushed
628
- // after children for tree composition.
629
790
  if (routeHandlerResult) {
791
+ // Route entry: handler already executed above; resolve parallels
792
+ // (handler data visible) then push handler segment last for tree order.
793
+ const parallelResult = await resolveParallelSegmentsWithRevalidation(
794
+ entry,
795
+ params,
796
+ context,
797
+ belongsToRoute,
798
+ clientSegmentIds,
799
+ prevParams,
800
+ request,
801
+ prevUrl,
802
+ nextUrl,
803
+ routeKey,
804
+ deps,
805
+ actionContext,
806
+ stale,
807
+ );
808
+ segments.push(...parallelResult.segments);
809
+ matchedIds.push(...parallelResult.matchedIds);
810
+
630
811
  segments.push(routeHandlerResult.segment);
631
812
  matchedIds.push(routeHandlerResult.matchedId);
632
813
  } else {
814
+ // Layout/cache entry: handler-first — resolve handler before parallels
815
+ // so ctx.set() values are visible to parallel children.
633
816
  const handlerResult = await resolveEntryHandlerWithRevalidation(
634
817
  entry,
635
818
  params,
@@ -647,9 +830,25 @@ export async function resolveSegmentWithRevalidation<TEnv>(
647
830
  );
648
831
  segments.push(handlerResult.segment);
649
832
  matchedIds.push(handlerResult.matchedId);
650
- }
651
833
 
652
- if (entry.type === "layout" || entry.type === "cache") {
834
+ const parallelResult = await resolveParallelSegmentsWithRevalidation(
835
+ entry,
836
+ params,
837
+ context,
838
+ belongsToRoute,
839
+ clientSegmentIds,
840
+ prevParams,
841
+ request,
842
+ prevUrl,
843
+ nextUrl,
844
+ routeKey,
845
+ deps,
846
+ actionContext,
847
+ stale,
848
+ );
849
+ segments.push(...parallelResult.segments);
850
+ matchedIds.push(...parallelResult.matchedIds);
851
+
653
852
  for (const orphan of entry.layout) {
654
853
  const orphanResult = await resolveOrphanLayoutWithRevalidation(
655
854
  orphan,
@@ -720,6 +919,82 @@ export async function resolveOrphanLayoutWithRevalidation<TEnv>(
720
919
  segments.push(...loaderResult.segments);
721
920
  matchedIds.push(...loaderResult.matchedIds);
722
921
 
922
+ // Handler-first: resolve orphan layout handler before its parallels
923
+ // so ctx.set() values are visible to parallel children.
924
+ matchedIds.push(orphan.shortCode);
925
+
926
+ const component = await revalidate(
927
+ async () => {
928
+ if (!clientSegmentIds.has(orphan.shortCode)) {
929
+ if (isTraceActive()) {
930
+ pushRevalidationTraceEntry({
931
+ segmentId: orphan.shortCode,
932
+ segmentType: "layout",
933
+ belongsToRoute,
934
+ source: "orphan-layout",
935
+ defaultShouldRevalidate: true,
936
+ finalShouldRevalidate: true,
937
+ reason: "new-segment",
938
+ });
939
+ }
940
+ return true;
941
+ }
942
+
943
+ const dummySegment: ResolvedSegment = {
944
+ id: orphan.shortCode,
945
+ namespace: orphan.id,
946
+ type: "layout",
947
+ index: 0,
948
+ component: null as any,
949
+ params,
950
+ belongsToRoute,
951
+ layoutName: orphan.id,
952
+ ...(orphan.mountPath ? { mountPath: orphan.mountPath } : {}),
953
+ };
954
+
955
+ const shouldRevalidate = await evaluateRevalidation({
956
+ segment: dummySegment,
957
+ prevParams,
958
+ getPrevSegment: null,
959
+ request,
960
+ prevUrl,
961
+ nextUrl,
962
+ revalidations: orphan.revalidate.map((fn, i) => ({
963
+ name: `revalidate${i}`,
964
+ fn,
965
+ })),
966
+ routeKey,
967
+ context,
968
+ actionContext,
969
+ stale,
970
+ traceSource: "orphan-layout",
971
+ });
972
+ emitRevalidationDecision(
973
+ orphan.shortCode,
974
+ context.pathname,
975
+ routeKey,
976
+ shouldRevalidate,
977
+ );
978
+ return shouldRevalidate;
979
+ },
980
+ async () => resolveLayoutComponent(orphan, context),
981
+ () => null,
982
+ );
983
+
984
+ segments.push({
985
+ id: orphan.shortCode,
986
+ namespace: orphan.id,
987
+ type: "layout",
988
+ index: 0,
989
+ component,
990
+ params,
991
+ belongsToRoute,
992
+ layoutName: orphan.id,
993
+ loading: orphan.loading === false ? null : orphan.loading,
994
+ transition: orphan.transition,
995
+ ...(orphan.mountPath ? { mountPath: orphan.mountPath } : {}),
996
+ });
997
+
723
998
  for (const parallelEntry of orphan.parallel) {
724
999
  invariant(
725
1000
  parallelEntry.type === "parallel",
@@ -758,7 +1033,20 @@ export async function resolveOrphanLayoutWithRevalidation<TEnv>(
758
1033
  matchedIds.push(parallelId);
759
1034
 
760
1035
  const shouldResolve = await (async () => {
761
- if (!clientSegmentIds.has(parallelId)) return true;
1036
+ if (!clientSegmentIds.has(parallelId)) {
1037
+ if (isTraceActive()) {
1038
+ pushRevalidationTraceEntry({
1039
+ segmentId: parallelId,
1040
+ segmentType: "parallel",
1041
+ belongsToRoute,
1042
+ source: "parallel",
1043
+ defaultShouldRevalidate: true,
1044
+ finalShouldRevalidate: true,
1045
+ reason: "new-segment",
1046
+ });
1047
+ }
1048
+ return true;
1049
+ }
762
1050
 
763
1051
  const dummySegment: ResolvedSegment = {
764
1052
  id: parallelId,
@@ -790,14 +1078,19 @@ export async function resolveOrphanLayoutWithRevalidation<TEnv>(
790
1078
  context,
791
1079
  actionContext,
792
1080
  stale,
1081
+ traceSource: "parallel",
793
1082
  });
794
1083
  })();
1084
+ emitRevalidationDecision(
1085
+ parallelId,
1086
+ context.pathname,
1087
+ routeKey,
1088
+ shouldResolve,
1089
+ );
795
1090
 
796
1091
  let component: ReactNode | undefined;
797
- // Static handler interception for individual parallel slots
798
- const slotStaticId = (parallelEntry as any).staticHandlerIds?.[slot];
799
- if (slotStaticId && shouldResolve) {
800
- component = await tryStaticLookup(slotStaticId, parallelId);
1092
+ if (shouldResolve) {
1093
+ component = await tryStaticSlot(parallelEntry, slot, parallelId);
801
1094
  }
802
1095
  if (component === undefined) {
803
1096
  const hasLoadingFallback =
@@ -806,9 +1099,25 @@ export async function resolveOrphanLayoutWithRevalidation<TEnv>(
806
1099
  if (!shouldResolve) {
807
1100
  component = null;
808
1101
  } else if (hasLoadingFallback) {
809
- component = (
810
- typeof handler === "function" ? handler(context) : handler
811
- ) as ReactNode;
1102
+ const result =
1103
+ typeof handler === "function" ? handler(context) : handler;
1104
+ if (result instanceof Promise) {
1105
+ const tracked = deps.trackHandler(result, {
1106
+ segmentId: parallelId,
1107
+ segmentType: "parallel",
1108
+ });
1109
+ observeStreamedHandler(
1110
+ tracked,
1111
+ parallelId,
1112
+ "parallel",
1113
+ context.pathname,
1114
+ routeKey,
1115
+ params,
1116
+ );
1117
+ component = tracked as ReactNode;
1118
+ } else {
1119
+ component = result as ReactNode;
1120
+ }
812
1121
  } else {
813
1122
  component =
814
1123
  typeof handler === "function" ? await handler(context) : handler;
@@ -834,204 +1143,9 @@ export async function resolveOrphanLayoutWithRevalidation<TEnv>(
834
1143
  }
835
1144
  }
836
1145
 
837
- matchedIds.push(orphan.shortCode);
838
-
839
- const component = await revalidate(
840
- async () => {
841
- if (!clientSegmentIds.has(orphan.shortCode)) return true;
842
-
843
- const dummySegment: ResolvedSegment = {
844
- id: orphan.shortCode,
845
- namespace: orphan.id,
846
- type: "layout",
847
- index: 0,
848
- component: null as any,
849
- params,
850
- belongsToRoute,
851
- layoutName: orphan.id,
852
- ...(orphan.mountPath ? { mountPath: orphan.mountPath } : {}),
853
- };
854
-
855
- return await evaluateRevalidation({
856
- segment: dummySegment,
857
- prevParams,
858
- getPrevSegment: null,
859
- request,
860
- prevUrl,
861
- nextUrl,
862
- revalidations: orphan.revalidate.map((fn, i) => ({
863
- name: `revalidate${i}`,
864
- fn,
865
- })),
866
- routeKey,
867
- context,
868
- actionContext,
869
- stale,
870
- });
871
- },
872
- async () => {
873
- // Static handler interception for orphan layouts
874
- const orphanAny = orphan as any;
875
- if (orphanAny.isStaticPrerender && orphanAny.staticHandlerId) {
876
- const staticComponent = await tryStaticLookup(
877
- orphanAny.staticHandlerId,
878
- orphan.shortCode,
879
- );
880
- if (staticComponent !== undefined) return staticComponent;
881
- }
882
- return typeof orphan.handler === "function"
883
- ? handleHandlerResult(await orphan.handler(context))
884
- : orphan.handler;
885
- },
886
- () => null,
887
- );
888
-
889
- segments.push({
890
- id: orphan.shortCode,
891
- namespace: orphan.id,
892
- type: "layout",
893
- index: 0,
894
- component,
895
- params,
896
- belongsToRoute,
897
- layoutName: orphan.id,
898
- loading: orphan.loading === false ? null : orphan.loading,
899
- transition: orphan.transition,
900
- ...(orphan.mountPath ? { mountPath: orphan.mountPath } : {}),
901
- });
902
-
903
1146
  return { segments, matchedIds };
904
1147
  }
905
1148
 
906
- /**
907
- * Wrapper for segment resolution with revalidation that adds error boundary handling.
908
- */
909
- export async function resolveWithRevalidationErrorHandling<TEnv>(
910
- entry: EntryData,
911
- params: Record<string, string>,
912
- resolveFn: () => Promise<SegmentRevalidationResult>,
913
- deps: SegmentResolutionDeps<TEnv>,
914
- pathname?: string,
915
- errorContext?: {
916
- request: Request;
917
- url: URL;
918
- routeKey?: string;
919
- env?: TEnv;
920
- isPartial?: boolean;
921
- requestStartTime?: number;
922
- },
923
- ): Promise<SegmentRevalidationResult> {
924
- try {
925
- return await resolveFn();
926
- } catch (error) {
927
- if (error instanceof Response) {
928
- throw error;
929
- }
930
-
931
- if (error instanceof DataNotFoundError) {
932
- const notFoundFallback = deps.findNearestNotFoundBoundary(entry);
933
-
934
- if (notFoundFallback) {
935
- const notFoundInfo = createNotFoundInfo(
936
- error,
937
- entry.shortCode,
938
- entry.type,
939
- pathname,
940
- );
941
-
942
- if (errorContext) {
943
- deps.callOnError(error, "handler", {
944
- request: errorContext.request,
945
- url: errorContext.url,
946
- routeKey: errorContext.routeKey,
947
- params,
948
- segmentId: entry.shortCode,
949
- segmentType: entry.type as any,
950
- env: errorContext.env,
951
- isPartial: errorContext.isPartial,
952
- handledByBoundary: true,
953
- metadata: { notFound: true, message: notFoundInfo.message },
954
- requestStartTime: errorContext.requestStartTime,
955
- });
956
- }
957
-
958
- debugLog("segment", "notFound boundary handled error", {
959
- segmentId: entry.shortCode,
960
- message: notFoundInfo.message,
961
- });
962
-
963
- const reqCtx = getRequestContext();
964
- if (reqCtx) {
965
- reqCtx.res = new Response(null, {
966
- status: 404,
967
- headers: reqCtx.res.headers,
968
- });
969
- }
970
-
971
- const notFoundSegment = createNotFoundSegment(
972
- notFoundInfo,
973
- notFoundFallback,
974
- entry,
975
- params,
976
- );
977
-
978
- return {
979
- segments: [notFoundSegment],
980
- matchedIds: [notFoundSegment.id],
981
- };
982
- }
983
- }
984
-
985
- const fallback = deps.findNearestErrorBoundary(entry);
986
- const segmentType: ErrorInfo["segmentType"] = entry.type;
987
- const errorInfo = createErrorInfo(error, entry.shortCode, segmentType);
988
- const effectiveFallback = fallback ?? DefaultErrorFallback;
989
-
990
- if (errorContext) {
991
- deps.callOnError(error, "handler", {
992
- request: errorContext.request,
993
- url: errorContext.url,
994
- routeKey: errorContext.routeKey,
995
- params,
996
- segmentId: entry.shortCode,
997
- segmentType: entry.type as any,
998
- env: errorContext.env,
999
- isPartial: errorContext.isPartial,
1000
- handledByBoundary: !!fallback,
1001
- requestStartTime: errorContext.requestStartTime,
1002
- });
1003
- }
1004
-
1005
- debugLog("segment", "error boundary handled error", {
1006
- segmentId: entry.shortCode,
1007
- boundary: fallback ? "custom" : "default",
1008
- message: errorInfo.message,
1009
- });
1010
-
1011
- {
1012
- const reqCtx = getRequestContext();
1013
- if (reqCtx) {
1014
- reqCtx.res = new Response(null, {
1015
- status: 500,
1016
- headers: reqCtx.res.headers,
1017
- });
1018
- }
1019
- }
1020
-
1021
- const errorSegment = createErrorSegment(
1022
- errorInfo,
1023
- effectiveFallback,
1024
- entry,
1025
- params,
1026
- );
1027
-
1028
- return {
1029
- segments: [errorSegment],
1030
- matchedIds: [errorSegment.id],
1031
- };
1032
- }
1033
- }
1034
-
1035
1149
  /**
1036
1150
  * Resolve all segments for a route with revalidation logic (for matchPartial).
1037
1151
  */
@@ -1057,6 +1171,8 @@ export async function resolveAllSegmentsWithRevalidation<TEnv>(
1057
1171
  const seenSegIds = new Set<string>();
1058
1172
  const seenMatchIds = new Set<string>();
1059
1173
 
1174
+ const telemetry = getRouterContext()?.telemetry;
1175
+
1060
1176
  for (const entry of entries) {
1061
1177
  if (entry.type === "route" && interceptResult) {
1062
1178
  debugLog(
@@ -1075,7 +1191,8 @@ export async function resolveAllSegmentsWithRevalidation<TEnv>(
1075
1191
  }
1076
1192
 
1077
1193
  const nonParallelEntry = entry as Exclude<EntryData, { type: "parallel" }>;
1078
- const resolved = await resolveWithRevalidationErrorHandling(
1194
+ const doneEntry = track(`segment:${entry.id}`, 1);
1195
+ const resolved = await resolveWithErrorBoundary(
1079
1196
  nonParallelEntry,
1080
1197
  params,
1081
1198
  () =>
@@ -1094,9 +1211,14 @@ export async function resolveAllSegmentsWithRevalidation<TEnv>(
1094
1211
  actionContext,
1095
1212
  false,
1096
1213
  ),
1214
+ (seg) => ({ segments: [seg], matchedIds: [seg.id] }),
1097
1215
  deps,
1216
+ telemetry
1217
+ ? { request, url: context.url, routeKey, isPartial: true, telemetry }
1218
+ : undefined,
1098
1219
  pathname,
1099
1220
  );
1221
+ doneEntry();
1100
1222
 
1101
1223
  // Deduplicate segments and matchedIds by ID, matching resolveAllSegments.
1102
1224
  // include() scopes can produce entries that resolve the same shared