@rangojs/router 0.0.0-experimental.18 → 0.0.0-experimental.19

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 (177) hide show
  1. package/README.md +46 -8
  2. package/dist/bin/rango.js +105 -18
  3. package/dist/vite/index.js +227 -93
  4. package/package.json +15 -14
  5. package/skills/hooks/SKILL.md +1 -1
  6. package/skills/intercept/SKILL.md +79 -0
  7. package/skills/layout/SKILL.md +62 -2
  8. package/skills/loader/SKILL.md +94 -1
  9. package/skills/middleware/SKILL.md +81 -0
  10. package/skills/parallel/SKILL.md +57 -2
  11. package/skills/prerender/SKILL.md +187 -17
  12. package/skills/route/SKILL.md +42 -1
  13. package/skills/router-setup/SKILL.md +77 -0
  14. package/src/__internal.ts +1 -1
  15. package/src/bin/rango.ts +38 -19
  16. package/src/browser/action-coordinator.ts +97 -0
  17. package/src/browser/event-controller.ts +25 -27
  18. package/src/browser/history-state.ts +80 -0
  19. package/src/browser/intercept-utils.ts +1 -1
  20. package/src/browser/link-interceptor.ts +0 -3
  21. package/src/browser/merge-segment-loaders.ts +9 -2
  22. package/src/browser/navigation-bridge.ts +46 -13
  23. package/src/browser/navigation-client.ts +32 -61
  24. package/src/browser/navigation-store.ts +1 -31
  25. package/src/browser/navigation-transaction.ts +46 -207
  26. package/src/browser/partial-update.ts +102 -150
  27. package/src/browser/{prefetch-cache.ts → prefetch/cache.ts} +23 -4
  28. package/src/browser/{prefetch-fetch.ts → prefetch/fetch.ts} +36 -8
  29. package/src/browser/prefetch/policy.ts +42 -0
  30. package/src/browser/{prefetch-queue.ts → prefetch/queue.ts} +10 -3
  31. package/src/browser/react/Link.tsx +28 -23
  32. package/src/browser/react/NavigationProvider.tsx +9 -1
  33. package/src/browser/react/index.ts +2 -6
  34. package/src/browser/react/location-state-shared.ts +1 -1
  35. package/src/browser/react/location-state.ts +2 -0
  36. package/src/browser/react/nonce-context.ts +23 -0
  37. package/src/browser/react/use-action.ts +9 -1
  38. package/src/browser/react/use-handle.ts +3 -25
  39. package/src/browser/react/use-params.ts +2 -4
  40. package/src/browser/react/use-pathname.ts +2 -3
  41. package/src/browser/react/use-router.ts +1 -1
  42. package/src/browser/react/use-search-params.ts +2 -1
  43. package/src/browser/react/use-segments.ts +7 -60
  44. package/src/browser/response-adapter.ts +73 -0
  45. package/src/browser/rsc-router.tsx +29 -23
  46. package/src/browser/scroll-restoration.ts +10 -7
  47. package/src/browser/server-action-bridge.ts +115 -96
  48. package/src/browser/types.ts +1 -31
  49. package/src/browser/validate-redirect-origin.ts +29 -0
  50. package/src/build/generate-manifest.ts +5 -0
  51. package/src/build/generate-route-types.ts +2 -0
  52. package/src/build/route-types/codegen.ts +13 -4
  53. package/src/build/route-types/include-resolution.ts +13 -0
  54. package/src/build/route-types/per-module-writer.ts +15 -3
  55. package/src/build/route-types/router-processing.ts +45 -3
  56. package/src/build/runtime-discovery.ts +13 -1
  57. package/src/cache/background-task.ts +34 -0
  58. package/src/cache/cache-key-utils.ts +44 -0
  59. package/src/cache/cache-policy.ts +125 -0
  60. package/src/cache/cache-runtime.ts +132 -96
  61. package/src/cache/cache-scope.ts +71 -73
  62. package/src/cache/cf/cf-cache-store.ts +9 -4
  63. package/src/cache/document-cache.ts +72 -47
  64. package/src/cache/handle-capture.ts +81 -0
  65. package/src/cache/memory-segment-store.ts +18 -7
  66. package/src/cache/profile-registry.ts +43 -8
  67. package/src/cache/read-through-swr.ts +134 -0
  68. package/src/cache/segment-codec.ts +101 -112
  69. package/src/cache/taint.ts +26 -0
  70. package/src/client.tsx +53 -30
  71. package/src/errors.ts +6 -1
  72. package/src/handle.ts +1 -1
  73. package/src/handles/MetaTags.tsx +5 -2
  74. package/src/host/cookie-handler.ts +8 -3
  75. package/src/host/router.ts +14 -1
  76. package/src/href-client.ts +3 -1
  77. package/src/index.rsc.ts +33 -1
  78. package/src/index.ts +27 -0
  79. package/src/loader.rsc.ts +12 -4
  80. package/src/loader.ts +8 -0
  81. package/src/prerender/store.ts +4 -3
  82. package/src/prerender.ts +76 -18
  83. package/src/reverse.ts +11 -7
  84. package/src/root-error-boundary.tsx +30 -26
  85. package/src/route-definition/dsl-helpers.ts +9 -6
  86. package/src/route-definition/redirect.ts +15 -3
  87. package/src/route-map-builder.ts +38 -2
  88. package/src/route-name.ts +53 -0
  89. package/src/route-types.ts +7 -0
  90. package/src/router/content-negotiation.ts +1 -1
  91. package/src/router/debug-manifest.ts +16 -3
  92. package/src/router/handler-context.ts +94 -15
  93. package/src/router/intercept-resolution.ts +6 -4
  94. package/src/router/lazy-includes.ts +4 -0
  95. package/src/router/loader-resolution.ts +1 -0
  96. package/src/router/logging.ts +100 -3
  97. package/src/router/manifest.ts +32 -3
  98. package/src/router/match-api.ts +61 -7
  99. package/src/router/match-context.ts +3 -0
  100. package/src/router/match-handlers.ts +185 -11
  101. package/src/router/match-middleware/background-revalidation.ts +65 -85
  102. package/src/router/match-middleware/cache-lookup.ts +69 -4
  103. package/src/router/match-middleware/cache-store.ts +2 -0
  104. package/src/router/match-pipelines.ts +8 -43
  105. package/src/router/middleware-types.ts +7 -0
  106. package/src/router/middleware.ts +93 -8
  107. package/src/router/pattern-matching.ts +41 -5
  108. package/src/router/prerender-match.ts +34 -6
  109. package/src/router/preview-match.ts +7 -1
  110. package/src/router/revalidation.ts +61 -2
  111. package/src/router/router-context.ts +15 -0
  112. package/src/router/router-interfaces.ts +34 -0
  113. package/src/router/router-options.ts +200 -0
  114. package/src/router/segment-resolution/fresh.ts +123 -30
  115. package/src/router/segment-resolution/helpers.ts +19 -0
  116. package/src/router/segment-resolution/loader-cache.ts +37 -146
  117. package/src/router/segment-resolution/revalidation.ts +358 -94
  118. package/src/router/segment-wrappers.ts +3 -0
  119. package/src/router/telemetry-otel.ts +299 -0
  120. package/src/router/telemetry.ts +300 -0
  121. package/src/router/timeout.ts +148 -0
  122. package/src/router/types.ts +7 -1
  123. package/src/router.ts +155 -11
  124. package/src/rsc/handler-context.ts +11 -0
  125. package/src/rsc/handler.ts +380 -88
  126. package/src/rsc/helpers.ts +25 -16
  127. package/src/rsc/loader-fetch.ts +84 -42
  128. package/src/rsc/origin-guard.ts +141 -0
  129. package/src/rsc/progressive-enhancement.ts +232 -19
  130. package/src/rsc/response-route-handler.ts +37 -26
  131. package/src/rsc/rsc-rendering.ts +12 -5
  132. package/src/rsc/runtime-warnings.ts +42 -0
  133. package/src/rsc/server-action.ts +134 -58
  134. package/src/rsc/types.ts +8 -0
  135. package/src/search-params.ts +22 -10
  136. package/src/server/context.ts +53 -5
  137. package/src/server/fetchable-loader-store.ts +11 -6
  138. package/src/server/handle-store.ts +66 -9
  139. package/src/server/loader-registry.ts +11 -46
  140. package/src/server/request-context.ts +90 -9
  141. package/src/ssr/index.tsx +63 -27
  142. package/src/static-handler.ts +7 -0
  143. package/src/theme/ThemeProvider.tsx +6 -1
  144. package/src/theme/index.ts +1 -6
  145. package/src/theme/theme-context.ts +1 -28
  146. package/src/theme/theme-script.ts +2 -1
  147. package/src/types/cache-types.ts +5 -0
  148. package/src/types/error-types.ts +3 -0
  149. package/src/types/global-namespace.ts +9 -0
  150. package/src/types/handler-context.ts +35 -13
  151. package/src/types/loader-types.ts +7 -0
  152. package/src/types/route-entry.ts +28 -0
  153. package/src/urls/include-helper.ts +49 -8
  154. package/src/urls/index.ts +1 -0
  155. package/src/urls/path-helper-types.ts +30 -12
  156. package/src/urls/path-helper.ts +17 -2
  157. package/src/urls/pattern-types.ts +21 -1
  158. package/src/urls/response-types.ts +27 -2
  159. package/src/urls/type-extraction.ts +23 -15
  160. package/src/use-loader.tsx +12 -4
  161. package/src/vite/discovery/bundle-postprocess.ts +12 -7
  162. package/src/vite/discovery/discover-routers.ts +30 -18
  163. package/src/vite/discovery/prerender-collection.ts +24 -27
  164. package/src/vite/discovery/route-types-writer.ts +7 -7
  165. package/src/vite/discovery/virtual-module-codegen.ts +5 -2
  166. package/src/vite/plugins/client-ref-hashing.ts +3 -3
  167. package/src/vite/plugins/use-cache-transform.ts +91 -3
  168. package/src/vite/rango.ts +3 -3
  169. package/src/vite/router-discovery.ts +99 -36
  170. package/src/vite/utils/prerender-utils.ts +21 -0
  171. package/src/vite/utils/shared-utils.ts +3 -1
  172. package/src/browser/request-controller.ts +0 -164
  173. package/src/href-context.ts +0 -33
  174. package/src/router.gen.ts +0 -6
  175. package/src/static-handler.gen.ts +0 -5
  176. package/src/urls.gen.ts +0 -8
  177. /package/src/browser/{prefetch-observer.ts → prefetch/observer.ts} +0 -0
@@ -18,6 +18,7 @@ import type {
18
18
  ShouldRevalidateFn,
19
19
  } from "../types.js";
20
20
  import type { RouteMatchResult } from "./pattern-matching.js";
21
+ import type { TelemetrySink } from "./telemetry.js";
21
22
 
22
23
  /**
23
24
  * Revalidation context passed to segment resolution
@@ -79,6 +80,7 @@ export interface RouterContext<TEnv = any> {
79
80
  routeMap?: Record<string, string>,
80
81
  routeName?: string,
81
82
  responseType?: string,
83
+ isPassthroughRoute?: boolean,
82
84
  ) => HandlerContext<any, TEnv>;
83
85
 
84
86
  // Loader setup
@@ -181,6 +183,12 @@ export interface RouterContext<TEnv = any> {
181
183
  context: HandlerContext<any, TEnv>;
182
184
  actionContext?: any;
183
185
  stale?: boolean;
186
+ traceSource?:
187
+ | "segment-resolution"
188
+ | "cache-hit"
189
+ | "loader"
190
+ | "parallel"
191
+ | "orphan-layout";
184
192
  }) => Promise<boolean>;
185
193
 
186
194
  // Request context
@@ -234,6 +242,7 @@ export interface RouterContext<TEnv = any> {
234
242
  nextUrl: URL,
235
243
  routeKey: string,
236
244
  actionContext?: any,
245
+ stale?: boolean,
237
246
  ) => Promise<{ segments: ResolvedSegment[]; matchedIds: string[] }>;
238
247
 
239
248
  // Entry revalidation map
@@ -241,6 +250,12 @@ export interface RouterContext<TEnv = any> {
241
250
  entries: EntryData[],
242
251
  ) => Map<string, { revalidate: ShouldRevalidateFn[] }>;
243
252
 
253
+ // Telemetry sink (optional, no-op when undefined)
254
+ telemetry?: TelemetrySink;
255
+
256
+ // Request ID for telemetry span correlation (set per-request in match handlers)
257
+ requestId?: string;
258
+
244
259
  // Intercept loaders only (for cache hit + intercept scenarios)
245
260
  resolveInterceptLoadersOnly?: (
246
261
  intercept: InterceptEntry,
@@ -14,6 +14,7 @@ import type { MiddlewareEntry, MiddlewareFn } from "./middleware.js";
14
14
  import { RSC_ROUTER_BRAND } from "./router-registry.js";
15
15
  import type { RSCRouterOptions, RootLayoutProps } from "./router-options.js";
16
16
  import type { DefaultVars } from "../types/global-namespace.js";
17
+ import type { ResolvedTimeouts, OnTimeoutCallback } from "./timeout.js";
17
18
 
18
19
  /**
19
20
  * Options passed to router.fetch(), router.match(), and other request entrypoints.
@@ -246,6 +247,15 @@ export interface RSCRouterInternal<
246
247
  */
247
248
  readonly themeConfig: import("../theme/types.js").ResolvedThemeConfig | null;
248
249
 
250
+ /**
251
+ * Cache profiles for "use cache" per-request resolution.
252
+ * Always includes at least the "default" profile.
253
+ */
254
+ readonly cacheProfiles: Record<
255
+ string,
256
+ import("../cache/profile-registry.js").CacheProfile
257
+ >;
258
+
249
259
  /**
250
260
  * Cache-Control header value for prefetch responses.
251
261
  * False means no browser caching of prefetch responses.
@@ -265,6 +275,16 @@ export interface RSCRouterInternal<
265
275
  */
266
276
  readonly allowDebugManifest: boolean;
267
277
 
278
+ /**
279
+ * Resolved timeout configuration (merged from shorthand + structured).
280
+ */
281
+ readonly timeouts: ResolvedTimeouts;
282
+
283
+ /**
284
+ * Custom timeout response handler.
285
+ */
286
+ readonly onTimeout?: OnTimeoutCallback<TEnv>;
287
+
268
288
  /**
269
289
  * App-level middleware entries
270
290
  * These wrap the entire request/response cycle
@@ -286,6 +306,18 @@ export interface RSCRouterInternal<
286
306
  */
287
307
  readonly urlpatterns?: UrlPatterns<TEnv, any>;
288
308
 
309
+ /**
310
+ * SSR configuration. resolveStreaming determines stream vs allReady
311
+ * per HTML request (undefined = always stream).
312
+ */
313
+ readonly ssr?: import("./router-options.js").SSROptions<TEnv>;
314
+
315
+ /**
316
+ * Cross-origin request protection configuration.
317
+ * Default: true (enabled).
318
+ */
319
+ readonly originCheck: import("../rsc/origin-guard.js").OriginCheckConfig<TEnv>;
320
+
289
321
  /**
290
322
  * Source file path where createRouter() was called.
291
323
  * Set via Error.stack parsing at construction time.
@@ -307,6 +339,7 @@ export interface RSCRouterInternal<
307
339
  pathname: string,
308
340
  params: Record<string, string>,
309
341
  buildVars?: Record<string, any>,
342
+ isPassthroughRoute?: boolean,
310
343
  ): Promise<{
311
344
  segments: SerializedSegmentData[];
312
345
  handles: Record<string, SegmentHandleData>;
@@ -314,6 +347,7 @@ export interface RSCRouterInternal<
314
347
  params: Record<string, string>;
315
348
  interceptSegments?: SerializedSegmentData[];
316
349
  interceptHandles?: Record<string, SegmentHandleData>;
350
+ passthrough?: true;
317
351
  } | null>;
318
352
 
319
353
  /**
@@ -9,6 +9,58 @@ import type { NonceProvider } from "../rsc/types.js";
9
9
  import type { ExecutionContext } from "../server/request-context.js";
10
10
  import type { UrlPatterns } from "../urls.js";
11
11
  import type { NamedRouteEntry } from "./content-negotiation.js";
12
+ import type { TelemetrySink } from "./telemetry.js";
13
+ import type { RouterTimeouts, OnTimeoutCallback } from "./timeout.js";
14
+
15
+ /**
16
+ * SSR stream mode returned by resolveStreaming.
17
+ *
18
+ * - `"stream"` — start flushing HTML as soon as the shell is ready
19
+ * (default React SSR behavior via `renderToReadableStream`).
20
+ * - `"allReady"` — wait for every Suspense boundary to resolve before
21
+ * sending any bytes (equivalent to awaiting `stream.allReady`).
22
+ */
23
+ export type SSRStreamMode = "stream" | "allReady";
24
+
25
+ /**
26
+ * Context passed to the resolveStreaming callback.
27
+ */
28
+ export interface ResolveStreamingContext<TEnv = unknown> {
29
+ request: Request;
30
+ env: TEnv;
31
+ url: URL;
32
+ }
33
+
34
+ /**
35
+ * SSR configuration options.
36
+ */
37
+ export interface SSROptions<TEnv = unknown> {
38
+ /**
39
+ * Determine whether an HTML response should stream progressively or
40
+ * wait for full readiness before flushing.
41
+ *
42
+ * Called once per HTML request, before the HTML response is produced.
43
+ * Does NOT apply to RSC responses (`__rsc`, partial navigation, prefetch).
44
+ *
45
+ * Return `"stream"` (default) for progressive streaming or `"allReady"`
46
+ * to buffer the complete HTML before sending.
47
+ *
48
+ * @example Bot detection
49
+ * ```ts
50
+ * createRouter({
51
+ * ssr: {
52
+ * resolveStreaming: async ({ request, env }) => {
53
+ * const bot = await detectBot(request, env);
54
+ * return bot.isBot && !bot.supportsStreaming ? "allReady" : "stream";
55
+ * },
56
+ * },
57
+ * });
58
+ * ```
59
+ */
60
+ resolveStreaming?: (
61
+ context: ResolveStreamingContext<TEnv>,
62
+ ) => SSRStreamMode | Promise<SSRStreamMode>;
63
+ }
12
64
 
13
65
  /**
14
66
  * Props passed to the root layout component
@@ -384,4 +436,152 @@ export interface RSCRouterOptions<TEnv = any> {
384
436
  * @default true
385
437
  */
386
438
  warmup?: boolean;
439
+
440
+ /**
441
+ * Shorthand timeout (ms) applied to both action execution and render start.
442
+ * Does NOT apply to streamIdleMs.
443
+ * Overridden by individual values in `timeouts`.
444
+ *
445
+ * @example
446
+ * ```typescript
447
+ * createRouter({ timeout: 10_000 });
448
+ * ```
449
+ */
450
+ timeout?: number;
451
+
452
+ /**
453
+ * Structured timeout configuration per phase.
454
+ * Values here override the `timeout` shorthand.
455
+ *
456
+ * @example
457
+ * ```typescript
458
+ * createRouter({
459
+ * timeouts: {
460
+ * actionMs: 10_000,
461
+ * renderStartMs: 8_000,
462
+ * },
463
+ * });
464
+ * ```
465
+ */
466
+ timeouts?: RouterTimeouts;
467
+
468
+ /**
469
+ * Custom handler invoked when a timeout occurs.
470
+ * Receives context about which phase timed out and must return a Response.
471
+ * If not provided, returns a plain 504 with "Request timed out" body
472
+ * and X-Rango-Timeout-Phase header.
473
+ *
474
+ * If the callback throws, the default 504 response is used as fallback.
475
+ *
476
+ * @example
477
+ * ```typescript
478
+ * createRouter({
479
+ * timeout: 10_000,
480
+ * onTimeout: (ctx) => {
481
+ * return new Response(
482
+ * JSON.stringify({ error: "timeout", phase: ctx.phase }),
483
+ * { status: 504, headers: { "Content-Type": "application/json" } },
484
+ * );
485
+ * },
486
+ * });
487
+ * ```
488
+ */
489
+ onTimeout?: OnTimeoutCallback<TEnv>;
490
+
491
+ /**
492
+ * Telemetry sink for structured lifecycle events.
493
+ *
494
+ * When provided, the router emits events for request start/end,
495
+ * loader start/end/error, handler errors, cache decisions, and
496
+ * revalidation decisions.
497
+ *
498
+ * No-op when not configured (zero overhead).
499
+ *
500
+ * @example Console logging
501
+ * ```typescript
502
+ * import { createConsoleSink } from "@rangojs/router";
503
+ *
504
+ * const router = createRouter({
505
+ * telemetry: createConsoleSink(),
506
+ * });
507
+ * ```
508
+ *
509
+ * @example Custom sink
510
+ * ```typescript
511
+ * const router = createRouter({
512
+ * telemetry: {
513
+ * emit(event) {
514
+ * myTracer.record(event);
515
+ * },
516
+ * },
517
+ * });
518
+ * ```
519
+ */
520
+ telemetry?: TelemetrySink;
521
+
522
+ /**
523
+ * SSR configuration options.
524
+ *
525
+ * @example
526
+ * ```typescript
527
+ * createRouter({
528
+ * ssr: {
529
+ * resolveStreaming: async ({ request, env }) => {
530
+ * const bot = await detectBot(request, env);
531
+ * return bot.isBot ? "allReady" : "stream";
532
+ * },
533
+ * },
534
+ * });
535
+ * ```
536
+ */
537
+ ssr?: SSROptions<TEnv>;
538
+
539
+ /**
540
+ * Cross-origin request protection for server actions, loader fetches,
541
+ * and progressive enhancement form submissions.
542
+ *
543
+ * When enabled, the router validates that the request's Origin header
544
+ * (or Referer fallback) matches the Host before executing actions,
545
+ * loaders, or PE submissions. Requests without Origin/Referer are
546
+ * allowed (same-origin navigations, non-browser clients).
547
+ *
548
+ * The built-in check compares Origin against the Host header and
549
+ * url.protocol. It does NOT trust X-Forwarded-Host/Proto headers
550
+ * (they are client-controllable without a trusted proxy). On standard
551
+ * deployments (Cloudflare Workers, Node behind nginx/caddy) the Host
552
+ * header is already set to the public-facing host by the platform or
553
+ * proxy. For non-standard proxy setups where Host differs from the
554
+ * public origin, use a custom function that reads the appropriate
555
+ * forwarded headers from your trusted proxy.
556
+ *
557
+ * - `true` (default) -- enable built-in origin validation
558
+ * - `false` -- disable
559
+ * - function -- full custom control with access to env, phase,
560
+ * and the built-in check via `ctx.defaultCheck()`
561
+ *
562
+ * The callback receives `OriginCheckContext` with `request`, `url`,
563
+ * `env`, `routerId`, `phase` ("action" | "loader" | "pe-form"),
564
+ * and `defaultCheck()`. Return `true` to allow, `false` for default
565
+ * 403 rejection, or a `Response` for custom rejection.
566
+ *
567
+ * @default true
568
+ *
569
+ * @example Trusted proxy with X-Forwarded-Host
570
+ * ```ts
571
+ * createRouter({
572
+ * originCheck({ request, url, env, defaultCheck }) {
573
+ * if (env.TRUST_PROXY) {
574
+ * const origin = request.headers.get("origin");
575
+ * if (!origin) return true;
576
+ * if (origin === "null") return false;
577
+ * const host = request.headers.get("x-forwarded-host")
578
+ * ?? request.headers.get("host") ?? url.host;
579
+ * return origin.toLowerCase() === `${url.protocol}//${host}`.toLowerCase();
580
+ * }
581
+ * return defaultCheck();
582
+ * },
583
+ * });
584
+ * ```
585
+ */
586
+ originCheck?: import("../rsc/origin-guard.js").OriginCheckConfig<TEnv>;
387
587
  }
@@ -22,6 +22,51 @@ import {
22
22
  resolveLayoutComponent,
23
23
  resolveWithErrorBoundary,
24
24
  } from "./helpers.js";
25
+ import { getRouterContext } from "../router-context.js";
26
+ import { resolveSink, safeEmit } from "../telemetry.js";
27
+
28
+ // ---------------------------------------------------------------------------
29
+ // Streamed handler telemetry
30
+ // ---------------------------------------------------------------------------
31
+
32
+ /**
33
+ * Attach a fire-and-forget rejection observer to a streamed handler promise.
34
+ * React catches the actual error via its error boundary; this only emits
35
+ * the handler.error telemetry event.
36
+ */
37
+ function observeStreamedHandler(
38
+ promise: Promise<ReactNode>,
39
+ segmentId: string,
40
+ segmentType: string,
41
+ pathname?: string,
42
+ routeKey?: string,
43
+ params?: Record<string, string>,
44
+ ): void {
45
+ let routerCtx;
46
+ try {
47
+ routerCtx = getRouterContext();
48
+ } catch {
49
+ return;
50
+ }
51
+ if (!routerCtx?.telemetry) return;
52
+ const sink = resolveSink(routerCtx.telemetry);
53
+ const reqId = routerCtx.requestId;
54
+ promise.catch((err: unknown) => {
55
+ const errorObj = err instanceof Error ? err : new Error(String(err));
56
+ safeEmit(sink, {
57
+ type: "handler.error",
58
+ timestamp: performance.now(),
59
+ requestId: reqId,
60
+ segmentId,
61
+ segmentType,
62
+ error: errorObj,
63
+ handledByBoundary: true,
64
+ pathname,
65
+ routeKey,
66
+ params,
67
+ });
68
+ });
69
+ }
25
70
 
26
71
  // ---------------------------------------------------------------------------
27
72
  // Fresh path (full match, no revalidation)
@@ -128,19 +173,8 @@ export async function resolveSegment<TEnv>(
128
173
  segments.push(...loaderSegments);
129
174
  }
130
175
 
131
- for (const parallelEntry of entry.parallel) {
132
- const parallelSegments = await resolveParallelEntry(
133
- parallelEntry,
134
- params,
135
- context,
136
- false,
137
- entry.shortCode,
138
- deps,
139
- options,
140
- );
141
- segments.push(...parallelSegments);
142
- }
143
-
176
+ // Handler-first: layout handler executes before its parallels and orphan
177
+ // layouts so that ctx.set() values are visible to all children.
144
178
  (context as InternalHandlerContext<any, TEnv>)._currentSegmentId =
145
179
  entry.shortCode;
146
180
 
@@ -160,6 +194,20 @@ export async function resolveSegment<TEnv>(
160
194
  ...(entry.mountPath ? { mountPath: entry.mountPath } : {}),
161
195
  });
162
196
 
197
+ for (const parallelEntry of entry.parallel) {
198
+ const parallelSegments = await resolveParallelEntry(
199
+ parallelEntry,
200
+ params,
201
+ context,
202
+ false,
203
+ entry.shortCode,
204
+ deps,
205
+ options,
206
+ routeKey,
207
+ );
208
+ segments.push(...parallelSegments);
209
+ }
210
+
163
211
  for (const orphan of entry.layout) {
164
212
  const orphanSegments = await resolveOrphanLayout(
165
213
  orphan,
@@ -169,6 +217,7 @@ export async function resolveSegment<TEnv>(
169
217
  false,
170
218
  deps,
171
219
  options,
220
+ routeKey,
172
221
  );
173
222
  segments.push(...orphanSegments);
174
223
  }
@@ -194,8 +243,23 @@ export async function resolveSegment<TEnv>(
194
243
  if (component === undefined) {
195
244
  if (entry.loading) {
196
245
  const result = handleHandlerResult(entry.handler(context));
197
- component =
198
- result instanceof Promise ? deps.trackHandler(result) : result;
246
+ if (result instanceof Promise) {
247
+ const tracked = deps.trackHandler(result, {
248
+ segmentId: entry.shortCode,
249
+ segmentType: entry.type,
250
+ });
251
+ observeStreamedHandler(
252
+ tracked,
253
+ entry.shortCode,
254
+ entry.type,
255
+ context.pathname,
256
+ routeKey,
257
+ params,
258
+ );
259
+ component = tracked;
260
+ } else {
261
+ component = result;
262
+ }
199
263
  } else {
200
264
  component = handleHandlerResult(await entry.handler(context));
201
265
  }
@@ -210,6 +274,7 @@ export async function resolveSegment<TEnv>(
210
274
  true,
211
275
  deps,
212
276
  options,
277
+ routeKey,
213
278
  );
214
279
  segments.push(...orphanSegments);
215
280
  }
@@ -223,6 +288,7 @@ export async function resolveSegment<TEnv>(
223
288
  entry.shortCode,
224
289
  deps,
225
290
  options,
291
+ routeKey,
226
292
  );
227
293
  segments.push(...parallelSegments);
228
294
  }
@@ -257,6 +323,7 @@ export async function resolveOrphanLayout<TEnv>(
257
323
  belongsToRoute: boolean,
258
324
  deps: SegmentResolutionDeps<TEnv>,
259
325
  options?: ResolveSegmentOptions,
326
+ routeKey?: string,
260
327
  ): Promise<ResolvedSegment[]> {
261
328
  invariant(
262
329
  orphan.type === "layout" || orphan.type === "cache",
@@ -274,19 +341,8 @@ export async function resolveOrphanLayout<TEnv>(
274
341
  segments.push(...loaderSegments);
275
342
  }
276
343
 
277
- for (const parallelEntry of orphan.parallel) {
278
- const parallelSegments = await resolveParallelEntry(
279
- parallelEntry,
280
- params,
281
- context,
282
- belongsToRoute,
283
- orphan.shortCode,
284
- deps,
285
- options,
286
- );
287
- segments.push(...parallelSegments);
288
- }
289
-
344
+ // Handler-first: orphan layout handler executes before its parallels
345
+ // so that ctx.set() values are visible to parallel children.
290
346
  const component = await resolveLayoutComponent(orphan, context);
291
347
 
292
348
  segments.push({
@@ -303,6 +359,20 @@ export async function resolveOrphanLayout<TEnv>(
303
359
  ...(orphan.mountPath ? { mountPath: orphan.mountPath } : {}),
304
360
  });
305
361
 
362
+ for (const parallelEntry of orphan.parallel) {
363
+ const parallelSegments = await resolveParallelEntry(
364
+ parallelEntry,
365
+ params,
366
+ context,
367
+ belongsToRoute,
368
+ orphan.shortCode,
369
+ deps,
370
+ options,
371
+ routeKey,
372
+ );
373
+ segments.push(...parallelSegments);
374
+ }
375
+
306
376
  return segments;
307
377
  }
308
378
 
@@ -317,6 +387,7 @@ export async function resolveParallelEntry<TEnv>(
317
387
  parentShortCode: string,
318
388
  deps: SegmentResolutionDeps<TEnv>,
319
389
  options?: ResolveSegmentOptions,
390
+ routeKey?: string,
320
391
  ): Promise<ResolvedSegment[]> {
321
392
  invariant(
322
393
  parallelEntry.type === "parallel",
@@ -344,7 +415,23 @@ export async function resolveParallelEntry<TEnv>(
344
415
  if (hasLoadingFallback) {
345
416
  const result =
346
417
  typeof handler === "function" ? handler(context) : handler;
347
- component = result as ReactNode;
418
+ if (result instanceof Promise) {
419
+ const tracked = deps.trackHandler(result, {
420
+ segmentId: `${parentShortCode}.${slot}`,
421
+ segmentType: "parallel",
422
+ });
423
+ observeStreamedHandler(
424
+ tracked,
425
+ `${parentShortCode}.${slot}`,
426
+ "parallel",
427
+ context.pathname,
428
+ routeKey,
429
+ params,
430
+ );
431
+ component = tracked as ReactNode;
432
+ } else {
433
+ component = result as ReactNode;
434
+ }
348
435
  } else {
349
436
  component =
350
437
  typeof handler === "function" ? await handler(context) : handler;
@@ -405,6 +492,12 @@ export async function resolveAllSegments<TEnv>(
405
492
  safeRequest = context.request;
406
493
  } catch {}
407
494
 
495
+ // Get telemetry sink from RouterContext (may not exist during prerendering)
496
+ let telemetry;
497
+ try {
498
+ telemetry = getRouterContext()?.telemetry;
499
+ } catch {}
500
+
408
501
  for (const entry of entries) {
409
502
  const resolvedSegments = await resolveWithErrorBoundary(
410
503
  entry,
@@ -422,7 +515,7 @@ export async function resolveAllSegments<TEnv>(
422
515
  ),
423
516
  (seg) => [seg],
424
517
  deps,
425
- { request: safeRequest, url: context.url, routeKey },
518
+ { request: safeRequest, url: context.url, routeKey, telemetry },
426
519
  context.pathname,
427
520
  );
428
521
  // Deduplicate by segment ID. include() scopes can produce entries that
@@ -23,6 +23,8 @@ import type { ResolvedSegment, ErrorInfo, HandlerContext } from "../../types";
23
23
  import type { SegmentResolutionDeps } from "../types.js";
24
24
  import { debugLog } from "../logging.js";
25
25
  import { tryStaticLookup } from "./static-store.js";
26
+ import type { TelemetrySink } from "../telemetry.js";
27
+ import { resolveSink, safeEmit, getRequestId } from "../telemetry.js";
26
28
 
27
29
  // ---------------------------------------------------------------------------
28
30
  // Handler result processing
@@ -116,6 +118,7 @@ export interface ErrorReportContext {
116
118
  env?: any;
117
119
  isPartial?: boolean;
118
120
  requestStartTime?: number;
121
+ telemetry?: TelemetrySink;
119
122
  }
120
123
 
121
124
  /**
@@ -150,6 +153,22 @@ export function catchSegmentError<TEnv>(
150
153
  metadata,
151
154
  requestStartTime: report.requestStartTime,
152
155
  });
156
+ if (report.telemetry) {
157
+ const errorObj =
158
+ error instanceof Error ? error : new Error(String(error));
159
+ safeEmit(resolveSink(report.telemetry), {
160
+ type: "handler.error",
161
+ timestamp: performance.now(),
162
+ requestId: report.request ? getRequestId(report.request) : undefined,
163
+ segmentId: entry.shortCode,
164
+ segmentType: entry.type,
165
+ error: errorObj,
166
+ handledByBoundary,
167
+ pathname,
168
+ routeKey: report.routeKey,
169
+ params,
170
+ });
171
+ }
153
172
  };
154
173
 
155
174
  const setResponseStatus = (status: number) => {