@rangojs/router 0.0.0-experimental.124 → 0.0.0-experimental.126

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 (235) hide show
  1. package/README.md +6 -4
  2. package/dist/bin/rango.js +3 -4
  3. package/dist/vite/index.js +315 -68
  4. package/package.json +19 -18
  5. package/skills/breadcrumbs/SKILL.md +60 -0
  6. package/skills/hooks/SKILL.md +2 -2
  7. package/skills/route/SKILL.md +6 -0
  8. package/skills/server-actions/SKILL.md +25 -1
  9. package/skills/testing/SKILL.md +17 -17
  10. package/skills/testing/cache-prerender.md +29 -3
  11. package/skills/testing/flight.md +13 -10
  12. package/skills/testing/render-handler.md +3 -0
  13. package/skills/testing/server-tree.md +1 -1
  14. package/skills/testing/setup.md +1 -1
  15. package/src/__internal.ts +0 -65
  16. package/src/browser/action-coordinator.ts +1 -1
  17. package/src/browser/action-fence.ts +10 -0
  18. package/src/browser/event-controller.ts +1 -83
  19. package/src/browser/navigation-store-handle.ts +3 -4
  20. package/src/browser/navigation-store.ts +0 -39
  21. package/src/browser/navigation-transaction.ts +0 -32
  22. package/src/browser/partial-update.ts +23 -84
  23. package/src/browser/prefetch/cache.ts +6 -45
  24. package/src/browser/prefetch/queue.ts +6 -3
  25. package/src/browser/rango-state.ts +2 -23
  26. package/src/browser/react/Link.tsx +0 -2
  27. package/src/browser/react/NavigationProvider.tsx +2 -1
  28. package/src/browser/react/ScrollRestoration.tsx +10 -6
  29. package/src/browser/react/filter-segment-order.ts +0 -2
  30. package/src/browser/react/index.ts +0 -45
  31. package/src/browser/react/location-state-shared.ts +0 -13
  32. package/src/browser/react/location-state.ts +0 -1
  33. package/src/browser/react/use-action.ts +6 -15
  34. package/src/browser/react/use-handle.ts +0 -5
  35. package/src/browser/react/use-link-status.ts +0 -4
  36. package/src/browser/react/use-navigation.ts +0 -3
  37. package/src/browser/react/use-params.ts +0 -2
  38. package/src/browser/react/use-router.ts +2 -1
  39. package/src/browser/react/use-search-params.ts +0 -5
  40. package/src/browser/react/use-segments.ts +0 -13
  41. package/src/browser/rsc-router.tsx +10 -3
  42. package/src/browser/server-action-bridge.ts +51 -3
  43. package/src/browser/types.ts +23 -5
  44. package/src/browser/validate-redirect-origin.ts +43 -16
  45. package/src/build/index.ts +8 -9
  46. package/src/build/route-trie.ts +46 -11
  47. package/src/build/route-types/param-extraction.ts +6 -3
  48. package/src/build/route-types/router-processing.ts +0 -8
  49. package/src/cache/cache-policy.ts +0 -54
  50. package/src/cache/cache-runtime.ts +48 -24
  51. package/src/cache/cache-scope.ts +0 -27
  52. package/src/cache/cache-tag.ts +0 -37
  53. package/src/cache/cf/cf-cache-store.ts +72 -45
  54. package/src/cache/cf/index.ts +0 -24
  55. package/src/cache/document-cache.ts +10 -36
  56. package/src/cache/handle-snapshot.ts +0 -40
  57. package/src/cache/index.ts +0 -27
  58. package/src/cache/memory-segment-store.ts +0 -52
  59. package/src/cache/profile-registry.ts +6 -30
  60. package/src/cache/read-through-swr.ts +41 -11
  61. package/src/cache/segment-codec.ts +0 -16
  62. package/src/cache/types.ts +0 -98
  63. package/src/client.rsc.tsx +4 -22
  64. package/src/client.tsx +19 -32
  65. package/src/context-var.ts +12 -0
  66. package/src/defer.ts +196 -0
  67. package/src/deps/ssr.ts +0 -1
  68. package/src/handle.ts +2 -12
  69. package/src/handles/MetaTags.tsx +0 -14
  70. package/src/handles/breadcrumbs.ts +16 -5
  71. package/src/handles/meta.ts +0 -39
  72. package/src/host/cookie-handler.ts +0 -36
  73. package/src/host/errors.ts +0 -24
  74. package/src/host/index.ts +6 -0
  75. package/src/host/pattern-matcher.ts +7 -50
  76. package/src/host/router.ts +1 -65
  77. package/src/host/testing.ts +0 -16
  78. package/src/host/types.ts +6 -2
  79. package/src/href-client.ts +0 -4
  80. package/src/index.rsc.ts +27 -2
  81. package/src/index.ts +7 -0
  82. package/src/internal-debug.ts +2 -4
  83. package/src/loader.rsc.ts +4 -15
  84. package/src/loader.ts +3 -9
  85. package/src/network-error-thrower.tsx +1 -6
  86. package/src/outlet-provider.tsx +1 -5
  87. package/src/prerender/param-hash.ts +10 -11
  88. package/src/prerender/store.ts +23 -30
  89. package/src/prerender.ts +34 -0
  90. package/src/redirect-origin.ts +100 -0
  91. package/src/root-error-boundary.tsx +1 -19
  92. package/src/route-content-wrapper.tsx +1 -44
  93. package/src/route-definition/dsl-helpers.ts +7 -19
  94. package/src/route-definition/helpers-types.ts +3 -3
  95. package/src/route-definition/redirect.ts +43 -9
  96. package/src/route-definition/resolve-handler-use.ts +6 -0
  97. package/src/route-map-builder.ts +0 -16
  98. package/src/router/content-negotiation.ts +0 -13
  99. package/src/router/error-handling.ts +12 -16
  100. package/src/router/find-match.ts +4 -31
  101. package/src/router/intercept-resolution.ts +10 -1
  102. package/src/router/lazy-includes.ts +1 -57
  103. package/src/router/loader-resolution.ts +25 -23
  104. package/src/router/logging.ts +0 -6
  105. package/src/router/manifest.ts +1 -25
  106. package/src/router/match-api.ts +0 -20
  107. package/src/router/match-context.ts +0 -22
  108. package/src/router/match-handlers.ts +0 -43
  109. package/src/router/match-middleware/background-revalidation.ts +0 -7
  110. package/src/router/match-middleware/cache-lookup.ts +96 -179
  111. package/src/router/match-middleware/cache-store.ts +0 -31
  112. package/src/router/match-middleware/intercept-resolution.ts +0 -22
  113. package/src/router/match-middleware/segment-resolution.ts +0 -22
  114. package/src/router/match-pipelines.ts +1 -42
  115. package/src/router/match-result.ts +1 -52
  116. package/src/router/metrics.ts +0 -34
  117. package/src/router/middleware-types.ts +0 -116
  118. package/src/router/middleware.ts +77 -60
  119. package/src/router/navigation-snapshot.ts +0 -51
  120. package/src/router/params-util.ts +23 -0
  121. package/src/router/pattern-matching.ts +5 -56
  122. package/src/router/prerender-match.ts +56 -51
  123. package/src/router/request-classification.ts +1 -38
  124. package/src/router/revalidation.ts +14 -62
  125. package/src/router/route-snapshot.ts +0 -1
  126. package/src/router/router-context.ts +0 -27
  127. package/src/router/router-interfaces.ts +10 -0
  128. package/src/router/segment-resolution/fresh.ts +25 -57
  129. package/src/router/segment-resolution/helpers.ts +34 -0
  130. package/src/router/segment-resolution/loader-cache.ts +35 -23
  131. package/src/router/segment-resolution/revalidation.ts +188 -283
  132. package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
  133. package/src/router/segment-resolution.ts +4 -1
  134. package/src/router/segment-wrappers.ts +0 -3
  135. package/src/router/telemetry-otel.ts +0 -20
  136. package/src/router/telemetry.ts +0 -22
  137. package/src/router/timeout.ts +0 -20
  138. package/src/router/trie-matching.ts +66 -45
  139. package/src/router/types.ts +1 -63
  140. package/src/router/url-params.ts +0 -5
  141. package/src/router.ts +8 -11
  142. package/src/rsc/handler-context.ts +1 -0
  143. package/src/rsc/handler.ts +20 -4
  144. package/src/rsc/helpers.ts +71 -3
  145. package/src/rsc/json-route-result.ts +38 -0
  146. package/src/rsc/origin-guard.ts +9 -15
  147. package/src/rsc/progressive-enhancement.ts +10 -1
  148. package/src/rsc/redirect-guard.ts +99 -0
  149. package/src/rsc/response-route-handler.ts +23 -18
  150. package/src/rsc/rsc-rendering.ts +2 -7
  151. package/src/rsc/runtime-warnings.ts +14 -0
  152. package/src/rsc/server-action.ts +34 -29
  153. package/src/rsc/types.ts +6 -3
  154. package/src/search-params.ts +0 -16
  155. package/src/segment-loader-promise.ts +14 -2
  156. package/src/segment-system.tsx +79 -88
  157. package/src/server/handle-store.ts +7 -24
  158. package/src/server/loader-registry.ts +5 -24
  159. package/src/server/request-context.ts +29 -92
  160. package/src/ssr/index.tsx +14 -14
  161. package/src/static-handler.ts +2 -27
  162. package/src/testing/cache-status.ts +44 -48
  163. package/src/testing/collect-handle.ts +1 -24
  164. package/src/testing/dispatch.ts +43 -6
  165. package/src/testing/e2e/index.ts +1 -22
  166. package/src/testing/e2e/matchers.ts +0 -16
  167. package/src/testing/flight-matchers.ts +0 -13
  168. package/src/testing/flight-normalize.ts +3 -30
  169. package/src/testing/flight.ts +46 -48
  170. package/src/testing/generated-routes.ts +1 -41
  171. package/src/testing/index.ts +1 -21
  172. package/src/testing/internal/context.ts +3 -45
  173. package/src/testing/internal/seed-vars.ts +0 -26
  174. package/src/testing/render-handler.ts +31 -61
  175. package/src/testing/render-route.tsx +75 -103
  176. package/src/testing/run-loader.ts +0 -96
  177. package/src/testing/run-middleware.ts +0 -26
  178. package/src/theme/ThemeProvider.tsx +0 -52
  179. package/src/theme/ThemeScript.tsx +0 -6
  180. package/src/theme/constants.ts +0 -12
  181. package/src/theme/index.ts +0 -7
  182. package/src/theme/theme-context.ts +1 -5
  183. package/src/theme/theme-script.ts +0 -14
  184. package/src/theme/use-theme.ts +0 -3
  185. package/src/types/boundaries.ts +0 -35
  186. package/src/types/error-types.ts +25 -89
  187. package/src/types/global-namespace.ts +4 -14
  188. package/src/types/handler-context.ts +28 -9
  189. package/src/types/index.ts +0 -10
  190. package/src/types/request-scope.ts +0 -19
  191. package/src/types/route-config.ts +6 -50
  192. package/src/types/route-entry.ts +0 -6
  193. package/src/types/segments.ts +0 -13
  194. package/src/urls/include-helper.ts +0 -4
  195. package/src/urls/index.ts +0 -6
  196. package/src/urls/path-helper-types.ts +2 -2
  197. package/src/urls/path-helper.ts +0 -54
  198. package/src/urls/urls-function.ts +0 -13
  199. package/src/use-loader.tsx +0 -186
  200. package/src/vite/discovery/bundle-postprocess.ts +2 -1
  201. package/src/vite/discovery/discover-routers.ts +28 -18
  202. package/src/vite/discovery/prerender-collection.ts +2 -4
  203. package/src/vite/discovery/state.ts +5 -0
  204. package/src/vite/discovery/virtual-module-codegen.ts +1 -11
  205. package/src/vite/plugin-types.ts +35 -9
  206. package/src/vite/plugins/cjs-to-esm.ts +0 -11
  207. package/src/vite/plugins/client-ref-dedup.ts +0 -11
  208. package/src/vite/plugins/client-ref-hashing.ts +0 -10
  209. package/src/vite/plugins/cloudflare-protocol-stub.ts +0 -20
  210. package/src/vite/plugins/expose-action-id.ts +2 -73
  211. package/src/vite/plugins/expose-id-utils.ts +0 -55
  212. package/src/vite/plugins/expose-ids/export-analysis.ts +0 -38
  213. package/src/vite/plugins/expose-ids/handler-transform.ts +0 -15
  214. package/src/vite/plugins/expose-ids/loader-transform.ts +0 -15
  215. package/src/vite/plugins/expose-ids/router-transform.ts +0 -13
  216. package/src/vite/plugins/expose-internal-ids.ts +10 -0
  217. package/src/vite/plugins/performance-tracks.ts +0 -3
  218. package/src/vite/plugins/refresh-cmd.ts +1 -1
  219. package/src/vite/plugins/use-cache-transform.ts +21 -46
  220. package/src/vite/plugins/version-injector.ts +0 -20
  221. package/src/vite/plugins/version-plugin.ts +1 -49
  222. package/src/vite/plugins/virtual-entries.ts +0 -15
  223. package/src/vite/rango.ts +2 -108
  224. package/src/vite/router-discovery.ts +9 -1
  225. package/src/vite/utils/ast-handler-extract.ts +0 -16
  226. package/src/vite/utils/bundle-analysis.ts +6 -13
  227. package/src/vite/utils/client-chunks.ts +0 -6
  228. package/src/vite/utils/forward-user-plugins.ts +0 -22
  229. package/src/vite/utils/manifest-utils.ts +0 -4
  230. package/src/vite/utils/package-resolution.ts +1 -73
  231. package/src/vite/utils/prerender-utils.ts +0 -35
  232. package/src/vite/utils/shared-utils.ts +3 -35
  233. package/src/browser/shallow.ts +0 -40
  234. package/src/handles/index.ts +0 -7
  235. package/src/router/middleware-cookies.ts +0 -55
@@ -14,6 +14,7 @@ import {
14
14
  getParallelEntries,
15
15
  getParallelSlotEntries,
16
16
  type EntryData,
17
+ type ParallelEntryData,
17
18
  } from "../../server/context";
18
19
  import type {
19
20
  HandlerContext,
@@ -34,6 +35,7 @@ import {
34
35
  import { resolveLoaderData } from "./loader-cache.js";
35
36
  import {
36
37
  handleHandlerResult,
38
+ warnOnStreamedResponse,
37
39
  tryStaticHandler,
38
40
  tryStaticSlot,
39
41
  resolveLayoutComponent,
@@ -42,54 +44,13 @@ import {
42
44
  import { applyViewTransitionDefault } from "./view-transition-default.js";
43
45
  import { getRouterContext } from "../router-context.js";
44
46
  import { resolveSink, safeEmit } from "../telemetry.js";
47
+ import { observeStreamedHandler } from "./streamed-handler-telemetry.js";
45
48
  import {
46
49
  track,
47
50
  RangoContext,
48
51
  runInsideLoaderScope,
49
52
  } from "../../server/context.js";
50
53
 
51
- // ---------------------------------------------------------------------------
52
- // Telemetry helpers
53
- // ---------------------------------------------------------------------------
54
-
55
- /**
56
- * Attach a fire-and-forget rejection observer to a streamed handler promise.
57
- * Silently no-ops when called outside RouterContext (e.g. in unit tests).
58
- */
59
- function observeStreamedHandler(
60
- promise: Promise<ReactNode>,
61
- segmentId: string,
62
- segmentType: string,
63
- pathname?: string,
64
- routeKey?: string,
65
- params?: Record<string, string>,
66
- ): void {
67
- let routerCtx;
68
- try {
69
- routerCtx = getRouterContext();
70
- } catch {
71
- return;
72
- }
73
- if (!routerCtx?.telemetry) return;
74
- const sink = resolveSink(routerCtx.telemetry);
75
- const reqId = routerCtx.requestId;
76
- promise.catch((err: unknown) => {
77
- const errorObj = err instanceof Error ? err : new Error(String(err));
78
- safeEmit(sink, {
79
- type: "handler.error",
80
- timestamp: performance.now(),
81
- requestId: reqId,
82
- segmentId,
83
- segmentType,
84
- error: errorObj,
85
- handledByBoundary: true,
86
- pathname,
87
- routeKey,
88
- params,
89
- });
90
- });
91
- }
92
-
93
54
  /**
94
55
  * Trace a parallel slot that's being force-rendered on a full refetch (client
95
56
  * has no cached state). User revalidate fns are bypassed in this case — see
@@ -426,6 +387,97 @@ export function buildEntryRevalidateMap(
426
387
  return map;
427
388
  }
428
389
 
390
+ /**
391
+ * Resolve the component for a single parallel slot on the revalidation path.
392
+ * Pure component resolution shared verbatim by
393
+ * resolveParallelSegmentsWithRevalidation and the orphan-inlined loop in
394
+ * resolveOrphanLayoutWithRevalidation: try the static slot cache, else run the
395
+ * slot handler (pinning _currentSegmentId to the slot id so handle pushes land
396
+ * in the slot's own bucket, and wrapping a streamed handler). Returns the
397
+ * resolved component and whether the handler actually ran. Does NOT touch the
398
+ * revalidate-default policy (the caller decides shouldResolve, including the
399
+ * orphan-vs-main defaultOverride divergence) or loader-resolution ordering.
400
+ */
401
+ async function resolveParallelSlotComponent<TEnv>(args: {
402
+ shouldResolve: boolean;
403
+ parallelEntry: ParallelEntryData;
404
+ slot: string;
405
+ parallelId: string;
406
+ handler:
407
+ | ((ctx: HandlerContext<any, TEnv>) => ReactNode | Promise<ReactNode>)
408
+ | ReactNode
409
+ | undefined;
410
+ context: HandlerContext<any, TEnv>;
411
+ deps: SegmentResolutionDeps<TEnv>;
412
+ routeKey: string;
413
+ params: Record<string, string>;
414
+ }): Promise<{ component: ReactNode | undefined; handlerRan: boolean }> {
415
+ const {
416
+ shouldResolve,
417
+ parallelEntry,
418
+ slot,
419
+ parallelId,
420
+ handler,
421
+ context,
422
+ deps,
423
+ routeKey,
424
+ params,
425
+ } = args;
426
+
427
+ let component: ReactNode | undefined;
428
+ let handlerRan = false;
429
+ if (shouldResolve) {
430
+ component = await tryStaticSlot(parallelEntry, slot, parallelId);
431
+ // tryStaticSlot returning a value means the static cache supplied the
432
+ // component — handler did NOT run. handlerRan stays false.
433
+ }
434
+ if (component === undefined) {
435
+ const hasLoadingFallback =
436
+ parallelEntry.loading !== undefined && parallelEntry.loading !== false;
437
+ if (!shouldResolve) {
438
+ component = null;
439
+ } else if (handler === undefined) {
440
+ // Handler evicted (production static slot) but static lookup missed.
441
+ // Nothing to render — use null so the client keeps its cached version.
442
+ component = null;
443
+ } else {
444
+ // Slot-keyed pushes — slot owns its own bucket, parent layout owns its
445
+ // own. On slot-only revalidations the partial merge updates only the
446
+ // slot's bucket; the parent's bucket stays intact.
447
+ (context as InternalHandlerContext<any, TEnv>)._currentSegmentId =
448
+ parallelId;
449
+ handlerRan = true;
450
+ if (hasLoadingFallback) {
451
+ const result =
452
+ typeof handler === "function" ? handler(context) : handler;
453
+ if (result instanceof Promise) {
454
+ warnOnStreamedResponse(result, parallelId);
455
+ const tracked = deps.trackHandler(result, {
456
+ segmentId: parallelId,
457
+ segmentType: "parallel",
458
+ });
459
+ observeStreamedHandler(
460
+ tracked,
461
+ parallelId,
462
+ "parallel",
463
+ context.pathname,
464
+ routeKey,
465
+ params,
466
+ );
467
+ component = tracked as ReactNode;
468
+ } else {
469
+ component = result as ReactNode;
470
+ }
471
+ } else {
472
+ component =
473
+ typeof handler === "function" ? await handler(context) : handler;
474
+ }
475
+ }
476
+ }
477
+
478
+ return { component, handlerRan };
479
+ }
480
+
429
481
  /**
430
482
  * Resolve parallel segments with revalidation.
431
483
  */
@@ -443,9 +495,30 @@ export async function resolveParallelSegmentsWithRevalidation<TEnv>(
443
495
  deps: SegmentResolutionDeps<TEnv>,
444
496
  actionContext?: ActionContext,
445
497
  stale?: boolean,
498
+ options?: {
499
+ /**
500
+ * Seed for an unknown parent-chain slot (slot not in clientSegmentIds) when
501
+ * there are no deciding revalidate fns. "type-derived" (default, main path):
502
+ * `belongsToRoute || isNewParent`. "force-render" (orphan path): always
503
+ * `true` — orphan parallels always belong to the route and must render
504
+ * unless the user opts out via revalidate(); the #482 blank-parent-chain-
505
+ * slot guard.
506
+ */
507
+ parentChainDefault?: "type-derived" | "force-render";
508
+ /**
509
+ * When a slot's loaders are resolved relative to the slot segment push.
510
+ * "after" (default, main path) pushes the slot segment first; "before"
511
+ * (orphan path) resolves loaders first. This only changes the
512
+ * segments/matchedIds emission ORDER (the client reconciler is insensitive
513
+ * to it: loader sub-ids are filtered out and slots are re-grouped by parent).
514
+ */
515
+ loaderOrder?: "after" | "before";
516
+ },
446
517
  ): Promise<SegmentRevalidationResult> {
447
518
  const segments: ResolvedSegment[] = [];
448
519
  const matchedIds: string[] = [];
520
+ const parentChainDefault = options?.parentChainDefault ?? "type-derived";
521
+ const loaderOrder = options?.loaderOrder ?? "after";
449
522
 
450
523
  const resolvedParallelEntries = new Set<string>();
451
524
  for (const { slot, entry: parallelEntry } of getParallelSlotEntries(
@@ -470,6 +543,34 @@ export async function resolveParallelSegmentsWithRevalidation<TEnv>(
470
543
 
471
544
  const isFullRefetch = clientSegmentIds.size === 0;
472
545
  const isNewParent = !clientSegmentIds.has(entry.shortCode);
546
+
547
+ // A slot's loaders (never cached) are deduped per parallel entry and
548
+ // emitted either before or after the slot segment per loaderOrder.
549
+ const resolveSlotLoaders = async () => {
550
+ if (resolvedParallelEntries.has(parallelEntry.id)) return;
551
+ const loaderResult = await resolveLoadersWithRevalidation(
552
+ parallelEntry,
553
+ context,
554
+ belongsToRoute,
555
+ clientSegmentIds,
556
+ prevParams,
557
+ request,
558
+ prevUrl,
559
+ nextUrl,
560
+ routeKey,
561
+ deps,
562
+ actionContext,
563
+ entry.shortCode,
564
+ stale,
565
+ );
566
+ segments.push(...loaderResult.segments);
567
+ matchedIds.push(...loaderResult.matchedIds);
568
+ resolvedParallelEntries.add(parallelEntry.id);
569
+ };
570
+
571
+ if (loaderOrder === "before") {
572
+ await resolveSlotLoaders();
573
+ }
473
574
  // Always announce the slot in matchedIds — it's unconditionally appended
474
575
  // to `segments` below, and a segment present in segments but missing from
475
576
  // matched lets the client prune it (then it's missing from clientSegmentIds
@@ -489,7 +590,10 @@ export async function resolveParallelSegmentsWithRevalidation<TEnv>(
489
590
  // soft chain seeds with the right "new segment" / "parent-chain" value.
490
591
  let defaultOverride: { value: boolean; reason: string } | undefined;
491
592
  if (!clientSegmentIds.has(parallelId)) {
492
- const value = belongsToRoute || isNewParent;
593
+ const value =
594
+ parentChainDefault === "force-render"
595
+ ? true
596
+ : belongsToRoute || isNewParent;
493
597
  defaultOverride = {
494
598
  value,
495
599
  reason: value ? "new-segment" : "skip-parent-chain",
@@ -537,55 +641,17 @@ export async function resolveParallelSegmentsWithRevalidation<TEnv>(
537
641
  shouldResolve,
538
642
  );
539
643
 
540
- let component: ReactNode | undefined;
541
- let handlerRan = false;
542
- if (shouldResolve) {
543
- component = await tryStaticSlot(parallelEntry, slot, parallelId);
544
- // tryStaticSlot returning a value means the static cache supplied the
545
- // component — handler did NOT run. handlerRan stays false.
546
- }
547
- if (component === undefined) {
548
- const hasLoadingFallback =
549
- parallelEntry.loading !== undefined && parallelEntry.loading !== false;
550
- if (!shouldResolve) {
551
- component = null;
552
- } else if (handler === undefined) {
553
- // Handler evicted (production static slot) but static lookup missed.
554
- // Nothing to render — use null so the client keeps its cached version.
555
- component = null;
556
- } else {
557
- // Slot-keyed pushes — slot owns its own bucket, parent layout owns
558
- // its own. On slot-only revalidations the partial merge updates only
559
- // the slot's bucket; the parent's bucket stays intact.
560
- (context as InternalHandlerContext<any, TEnv>)._currentSegmentId =
561
- parallelId;
562
- handlerRan = true;
563
- if (hasLoadingFallback) {
564
- const result =
565
- typeof handler === "function" ? handler(context) : handler;
566
- if (result instanceof Promise) {
567
- const tracked = deps.trackHandler(result, {
568
- segmentId: parallelId,
569
- segmentType: "parallel",
570
- });
571
- observeStreamedHandler(
572
- tracked,
573
- parallelId,
574
- "parallel",
575
- context.pathname,
576
- routeKey,
577
- params,
578
- );
579
- component = tracked as ReactNode;
580
- } else {
581
- component = result as ReactNode;
582
- }
583
- } else {
584
- component =
585
- typeof handler === "function" ? await handler(context) : handler;
586
- }
587
- }
588
- }
644
+ const { component, handlerRan } = await resolveParallelSlotComponent({
645
+ shouldResolve,
646
+ parallelEntry,
647
+ slot,
648
+ parallelId,
649
+ handler,
650
+ context,
651
+ deps,
652
+ routeKey,
653
+ params,
654
+ });
589
655
 
590
656
  segments.push({
591
657
  id: parallelId,
@@ -608,28 +674,9 @@ export async function resolveParallelSegmentsWithRevalidation<TEnv>(
608
674
  : {}),
609
675
  });
610
676
 
611
- if (resolvedParallelEntries.has(parallelEntry.id)) {
612
- continue;
677
+ if (loaderOrder === "after") {
678
+ await resolveSlotLoaders();
613
679
  }
614
-
615
- const loaderResult = await resolveLoadersWithRevalidation(
616
- parallelEntry,
617
- context,
618
- belongsToRoute,
619
- clientSegmentIds,
620
- prevParams,
621
- request,
622
- prevUrl,
623
- nextUrl,
624
- routeKey,
625
- deps,
626
- actionContext,
627
- entry.shortCode,
628
- stale,
629
- );
630
- segments.push(...loaderResult.segments);
631
- matchedIds.push(...loaderResult.matchedIds);
632
- resolvedParallelEntries.add(parallelEntry.id);
633
680
  }
634
681
 
635
682
  return { segments, matchedIds };
@@ -762,6 +809,7 @@ export async function resolveEntryHandlerWithRevalidation<TEnv>(
762
809
  if (!actionContext) {
763
810
  const result = handleHandlerResult(handler(context));
764
811
  if (result instanceof Promise) {
812
+ warnOnStreamedResponse(result, routeEntry.id);
765
813
  result.finally(doneHandler).catch(() => {});
766
814
  const tracked = deps.trackHandler(result, {
767
815
  segmentId: entry.shortCode,
@@ -836,7 +884,6 @@ export async function resolveSegmentWithRevalidation<TEnv>(
836
884
  request: Request,
837
885
  prevUrl: URL,
838
886
  nextUrl: URL,
839
- loaderPromises: Map<string, Promise<any>>,
840
887
  deps: SegmentResolutionDeps<TEnv>,
841
888
  actionContext?: ActionContext,
842
889
  stale?: boolean,
@@ -1151,173 +1198,33 @@ export async function resolveOrphanLayoutWithRevalidation<TEnv>(
1151
1198
  ...(orphan.mountPath ? { mountPath: orphan.mountPath } : {}),
1152
1199
  });
1153
1200
 
1154
- const resolvedParallelEntries = new Set<string>();
1155
- for (const { slot, entry: parallelEntry } of getParallelSlotEntries(
1156
- orphan.parallel,
1157
- )) {
1158
- invariant(
1159
- parallelEntry.type === "parallel",
1160
- `Expected parallel entry, got: ${parallelEntry.type}`,
1161
- );
1162
-
1163
- if (!resolvedParallelEntries.has(parallelEntry.id)) {
1164
- // shortCodeOverride must match the parent layout, not the parallel entry.
1165
- const loaderResult = await resolveLoadersWithRevalidation(
1166
- parallelEntry,
1167
- context,
1168
- belongsToRoute,
1169
- clientSegmentIds,
1170
- prevParams,
1171
- request,
1172
- prevUrl,
1173
- nextUrl,
1174
- routeKey,
1175
- deps,
1176
- actionContext,
1177
- orphan.shortCode,
1178
- stale,
1179
- );
1180
- segments.push(...loaderResult.segments);
1181
- matchedIds.push(...loaderResult.matchedIds);
1182
- resolvedParallelEntries.add(parallelEntry.id);
1183
- }
1184
-
1185
- const slots = parallelEntry.handler as Record<
1186
- `@${string}`,
1187
- | ((ctx: HandlerContext<any, TEnv>) => ReactNode | Promise<ReactNode>)
1188
- | ReactNode
1189
- >;
1190
- // Handler may be undefined in production after static handler eviction.
1191
- const handler = slots[slot];
1192
-
1193
- // Use orphan.shortCode (the parent layout) to match the SSR path
1194
- // (resolveParallelEntry receives parentShortCode = orphan.shortCode).
1195
- // Using parallelEntry.shortCode would generate IDs the client doesn't know about.
1196
- const parallelId = `${orphan.shortCode}.${slot}`;
1197
- matchedIds.push(parallelId);
1198
-
1199
- const isFullRefetch = clientSegmentIds.size === 0;
1200
- let shouldResolve: boolean;
1201
- if (isFullRefetch) {
1202
- // Same load-bearing rationale as the main parallel path: full refetch
1203
- // means the client has nothing to fall back to, so the slot must render.
1204
- traceFullRefetchedParallelSlot(parallelId, belongsToRoute);
1205
- shouldResolve = true;
1206
- } else {
1207
- // When slot is unknown to the client, seed the soft chain with `true`
1208
- // (orphan parallels always belong to the route — we want them rendered
1209
- // unless the user explicitly opts out via revalidate()).
1210
- const defaultOverride = clientSegmentIds.has(parallelId)
1211
- ? undefined
1212
- : { value: true, reason: "new-segment" };
1213
-
1214
- const dummySegment: ResolvedSegment = {
1215
- id: parallelId,
1216
- namespace: parallelEntry.id,
1217
- type: "parallel",
1218
- index: 0,
1219
- component: null as any,
1220
- params,
1221
- slot,
1222
- belongsToRoute,
1223
- parallelName: `${parallelEntry.id}.${slot}`,
1224
- ...(parallelEntry.mountPath
1225
- ? { mountPath: parallelEntry.mountPath }
1226
- : {}),
1227
- };
1228
-
1229
- shouldResolve = await evaluateRevalidation({
1230
- segment: dummySegment,
1231
- prevParams,
1232
- getPrevSegment: null,
1233
- request,
1234
- prevUrl,
1235
- nextUrl,
1236
- revalidations: parallelEntry.revalidate.map((fn, i) => ({
1237
- name: `revalidate${i}`,
1238
- fn,
1239
- })),
1240
- routeKey,
1241
- context,
1242
- actionContext,
1243
- stale,
1244
- traceSource: "parallel",
1245
- defaultOverride,
1246
- });
1247
- }
1248
- emitRevalidationDecision(
1249
- parallelId,
1250
- context.pathname,
1251
- routeKey,
1252
- shouldResolve,
1253
- );
1254
-
1255
- let component: ReactNode | undefined;
1256
- let handlerRan = false;
1257
- if (shouldResolve) {
1258
- component = await tryStaticSlot(parallelEntry, slot, parallelId);
1259
- }
1260
- if (component === undefined) {
1261
- const hasLoadingFallback =
1262
- parallelEntry.loading !== undefined && parallelEntry.loading !== false;
1263
- if (!shouldResolve) {
1264
- component = null;
1265
- } else if (handler === undefined) {
1266
- // Handler evicted (production static slot) but static lookup missed.
1267
- component = null;
1268
- } else {
1269
- // Slot-keyed pushes — see resolveParallelSegmentsWithRevalidation.
1270
- (context as InternalHandlerContext<any, TEnv>)._currentSegmentId =
1271
- parallelId;
1272
- handlerRan = true;
1273
- if (hasLoadingFallback) {
1274
- const result =
1275
- typeof handler === "function" ? handler(context) : handler;
1276
- if (result instanceof Promise) {
1277
- const tracked = deps.trackHandler(result, {
1278
- segmentId: parallelId,
1279
- segmentType: "parallel",
1280
- });
1281
- observeStreamedHandler(
1282
- tracked,
1283
- parallelId,
1284
- "parallel",
1285
- context.pathname,
1286
- routeKey,
1287
- params,
1288
- );
1289
- component = tracked as ReactNode;
1290
- } else {
1291
- component = result as ReactNode;
1292
- }
1293
- } else {
1294
- component =
1295
- typeof handler === "function" ? await handler(context) : handler;
1296
- }
1297
- }
1298
- }
1299
-
1300
- segments.push({
1301
- id: parallelId,
1302
- namespace: parallelEntry.id,
1303
- type: "parallel",
1304
- index: 0,
1305
- component,
1306
- loading: parallelEntry.loading === false ? null : parallelEntry.loading,
1307
- transition: applyViewTransitionDefault(
1308
- parallelEntry.transition,
1309
- deps.viewTransitionDefault,
1310
- ),
1311
- params,
1312
- slot,
1313
- _handlerRan: handlerRan,
1314
- belongsToRoute,
1315
- parallelName: `${parallelEntry.id}.${slot}`,
1316
- ...(parallelEntry.mountPath
1317
- ? { mountPath: parallelEntry.mountPath }
1318
- : {}),
1319
- });
1320
- }
1201
+ // Resolve the orphan layout's parallel slots through the shared main-path
1202
+ // helper. The orphan policy is carried by explicit args, byte-for-byte:
1203
+ // - parentChainDefault "force-render": an unknown parent-chain slot seeds
1204
+ // `true` (orphan parallels always belong to the route — the #482 guard),
1205
+ // where the main path would seed `belongsToRoute || isNewParent`.
1206
+ // - loaderOrder "before": a slot's loaders are emitted before the slot
1207
+ // segment, matching the prior inlined order.
1208
+ // `entry.shortCode` inside the helper is `orphan.shortCode` (orphan is passed
1209
+ // as `entry`), so the parallel ids + loader shortCodeOverride are unchanged.
1210
+ const parallelResult = await resolveParallelSegmentsWithRevalidation(
1211
+ orphan,
1212
+ params,
1213
+ context,
1214
+ belongsToRoute,
1215
+ clientSegmentIds,
1216
+ prevParams,
1217
+ request,
1218
+ prevUrl,
1219
+ nextUrl,
1220
+ routeKey,
1221
+ deps,
1222
+ actionContext,
1223
+ stale,
1224
+ { parentChainDefault: "force-render", loaderOrder: "before" },
1225
+ );
1226
+ segments.push(...parallelResult.segments);
1227
+ matchedIds.push(...parallelResult.matchedIds);
1321
1228
 
1322
1229
  return { segments, matchedIds };
1323
1230
  }
@@ -1335,7 +1242,6 @@ export async function resolveAllSegmentsWithRevalidation<TEnv>(
1335
1242
  request: Request,
1336
1243
  prevUrl: URL,
1337
1244
  nextUrl: URL,
1338
- loaderPromises: Map<string, Promise<any>>,
1339
1245
  actionContext: ActionContext | undefined,
1340
1246
  interceptResult: { intercept: any; entry: EntryData } | null,
1341
1247
  localRouteName: string,
@@ -1387,7 +1293,6 @@ export async function resolveAllSegmentsWithRevalidation<TEnv>(
1387
1293
  request,
1388
1294
  prevUrl,
1389
1295
  nextUrl,
1390
- loaderPromises,
1391
1296
  deps,
1392
1297
  actionContext,
1393
1298
  stale,
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Streamed Handler Telemetry
3
+ *
4
+ * Shared fire-and-forget rejection observer for streamed handler promises,
5
+ * used by both the fresh and revalidation segment-resolution paths. Lives in
6
+ * its own module (never mocked) so the resolution unit tests that mock
7
+ * helpers.js / telemetry.js with explicit export lists do not resolve it to
8
+ * undefined.
9
+ */
10
+
11
+ import type { ReactNode } from "react";
12
+ import { getRouterContext } from "../router-context.js";
13
+ import { resolveSink, safeEmit } from "../telemetry.js";
14
+
15
+ /**
16
+ * Attach a fire-and-forget rejection observer to a streamed handler promise.
17
+ * React catches the actual error via its error boundary; this only emits
18
+ * the handler.error telemetry event.
19
+ */
20
+ export function observeStreamedHandler(
21
+ promise: Promise<ReactNode>,
22
+ segmentId: string,
23
+ segmentType: string,
24
+ pathname?: string,
25
+ routeKey?: string,
26
+ params?: Record<string, string>,
27
+ ): void {
28
+ let routerCtx;
29
+ try {
30
+ routerCtx = getRouterContext();
31
+ } catch {
32
+ return;
33
+ }
34
+ if (!routerCtx?.telemetry) return;
35
+ const sink = resolveSink(routerCtx.telemetry);
36
+ const reqId = routerCtx.requestId;
37
+ promise.catch((err: unknown) => {
38
+ const errorObj = err instanceof Error ? err : new Error(String(err));
39
+ safeEmit(sink, {
40
+ type: "handler.error",
41
+ timestamp: performance.now(),
42
+ requestId: reqId,
43
+ segmentId,
44
+ segmentType,
45
+ error: errorObj,
46
+ handledByBoundary: true,
47
+ pathname,
48
+ routeKey,
49
+ params,
50
+ });
51
+ });
52
+ }
@@ -1,5 +1,8 @@
1
1
  // Barrel re-export -- see segment-resolution/ for implementations.
2
- export { handleHandlerResult } from "./segment-resolution/helpers.js";
2
+ export {
3
+ handleHandlerResult,
4
+ warnOnStreamedResponse,
5
+ } from "./segment-resolution/helpers.js";
3
6
  export {
4
7
  resolveLoaders,
5
8
  type ResolveSegmentOptions,
@@ -68,7 +68,6 @@ export interface SegmentWrappers<TEnv = any> {
68
68
  request: Request,
69
69
  prevUrl: URL,
70
70
  nextUrl: URL,
71
- loaderPromises: Map<string, Promise<any>>,
72
71
  actionContext:
73
72
  | {
74
73
  actionId?: string;
@@ -192,7 +191,6 @@ export function createSegmentWrappers<TEnv = any>(
192
191
  request: Request,
193
192
  prevUrl: URL,
194
193
  nextUrl: URL,
195
- loaderPromises: Map<string, Promise<any>>,
196
194
  actionContext:
197
195
  | {
198
196
  actionId?: string;
@@ -216,7 +214,6 @@ export function createSegmentWrappers<TEnv = any>(
216
214
  request,
217
215
  prevUrl,
218
216
  nextUrl,
219
- loaderPromises,
220
217
  actionContext,
221
218
  interceptResult,
222
219
  localRouteName,