@rangojs/router 0.0.0-experimental.20 → 0.0.0-experimental.20dbba0c

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 (189) hide show
  1. package/AGENTS.md +4 -0
  2. package/README.md +172 -50
  3. package/dist/bin/rango.js +138 -50
  4. package/dist/vite/index.js +1160 -508
  5. package/dist/vite/index.js.bak +5448 -0
  6. package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  7. package/package.json +17 -16
  8. package/skills/breadcrumbs/SKILL.md +252 -0
  9. package/skills/cache-guide/SKILL.md +32 -0
  10. package/skills/caching/SKILL.md +49 -8
  11. package/skills/document-cache/SKILL.md +2 -2
  12. package/skills/handler-use/SKILL.md +362 -0
  13. package/skills/hooks/SKILL.md +61 -51
  14. package/skills/host-router/SKILL.md +218 -0
  15. package/skills/intercept/SKILL.md +20 -0
  16. package/skills/layout/SKILL.md +22 -0
  17. package/skills/links/SKILL.md +91 -17
  18. package/skills/loader/SKILL.md +107 -24
  19. package/skills/middleware/SKILL.md +34 -3
  20. package/skills/migrate-nextjs/SKILL.md +560 -0
  21. package/skills/migrate-react-router/SKILL.md +765 -0
  22. package/skills/parallel/SKILL.md +185 -0
  23. package/skills/prerender/SKILL.md +112 -70
  24. package/skills/rango/SKILL.md +24 -23
  25. package/skills/response-routes/SKILL.md +8 -0
  26. package/skills/route/SKILL.md +58 -4
  27. package/skills/router-setup/SKILL.md +95 -5
  28. package/skills/streams-and-websockets/SKILL.md +283 -0
  29. package/skills/typesafety/SKILL.md +38 -24
  30. package/src/__internal.ts +92 -0
  31. package/src/browser/app-shell.ts +52 -0
  32. package/src/browser/app-version.ts +14 -0
  33. package/src/browser/event-controller.ts +5 -0
  34. package/src/browser/link-interceptor.ts +4 -0
  35. package/src/browser/navigation-bridge.ts +175 -17
  36. package/src/browser/navigation-client.ts +177 -44
  37. package/src/browser/navigation-store.ts +68 -9
  38. package/src/browser/navigation-transaction.ts +11 -9
  39. package/src/browser/partial-update.ts +113 -17
  40. package/src/browser/prefetch/cache.ts +275 -28
  41. package/src/browser/prefetch/fetch.ts +191 -46
  42. package/src/browser/prefetch/policy.ts +6 -0
  43. package/src/browser/prefetch/queue.ts +123 -20
  44. package/src/browser/prefetch/resource-ready.ts +77 -0
  45. package/src/browser/rango-state.ts +53 -13
  46. package/src/browser/react/Link.tsx +98 -14
  47. package/src/browser/react/NavigationProvider.tsx +89 -14
  48. package/src/browser/react/context.ts +7 -2
  49. package/src/browser/react/use-handle.ts +9 -58
  50. package/src/browser/react/use-navigation.ts +22 -2
  51. package/src/browser/react/use-params.ts +11 -1
  52. package/src/browser/react/use-router.ts +29 -9
  53. package/src/browser/rsc-router.tsx +177 -66
  54. package/src/browser/scroll-restoration.ts +41 -42
  55. package/src/browser/segment-reconciler.ts +36 -9
  56. package/src/browser/server-action-bridge.ts +8 -6
  57. package/src/browser/types.ts +73 -5
  58. package/src/build/generate-manifest.ts +6 -6
  59. package/src/build/generate-route-types.ts +3 -0
  60. package/src/build/route-trie.ts +67 -25
  61. package/src/build/route-types/include-resolution.ts +8 -1
  62. package/src/build/route-types/router-processing.ts +223 -74
  63. package/src/build/route-types/scan-filter.ts +8 -1
  64. package/src/cache/cache-runtime.ts +15 -11
  65. package/src/cache/cache-scope.ts +48 -7
  66. package/src/cache/cf/cf-cache-store.ts +455 -15
  67. package/src/cache/cf/index.ts +5 -1
  68. package/src/cache/document-cache.ts +17 -7
  69. package/src/cache/index.ts +1 -0
  70. package/src/cache/taint.ts +55 -0
  71. package/src/client.rsc.tsx +2 -1
  72. package/src/client.tsx +85 -276
  73. package/src/context-var.ts +72 -2
  74. package/src/debug.ts +2 -2
  75. package/src/handle.ts +40 -0
  76. package/src/handles/breadcrumbs.ts +66 -0
  77. package/src/handles/index.ts +1 -0
  78. package/src/host/index.ts +0 -3
  79. package/src/index.rsc.ts +9 -36
  80. package/src/index.ts +79 -70
  81. package/src/outlet-context.ts +1 -1
  82. package/src/prerender/store.ts +57 -15
  83. package/src/prerender.ts +138 -77
  84. package/src/response-utils.ts +28 -0
  85. package/src/reverse.ts +27 -2
  86. package/src/route-definition/dsl-helpers.ts +240 -40
  87. package/src/route-definition/helpers-types.ts +67 -19
  88. package/src/route-definition/index.ts +3 -3
  89. package/src/route-definition/redirect.ts +11 -3
  90. package/src/route-definition/resolve-handler-use.ts +155 -0
  91. package/src/route-map-builder.ts +7 -1
  92. package/src/route-types.ts +18 -0
  93. package/src/router/content-negotiation.ts +100 -1
  94. package/src/router/find-match.ts +4 -2
  95. package/src/router/handler-context.ts +129 -26
  96. package/src/router/intercept-resolution.ts +11 -4
  97. package/src/router/lazy-includes.ts +10 -7
  98. package/src/router/loader-resolution.ts +160 -22
  99. package/src/router/logging.ts +5 -2
  100. package/src/router/manifest.ts +31 -16
  101. package/src/router/match-api.ts +128 -193
  102. package/src/router/match-middleware/background-revalidation.ts +30 -2
  103. package/src/router/match-middleware/cache-lookup.ts +94 -17
  104. package/src/router/match-middleware/cache-store.ts +53 -10
  105. package/src/router/match-middleware/intercept-resolution.ts +9 -7
  106. package/src/router/match-middleware/segment-resolution.ts +61 -5
  107. package/src/router/match-result.ts +103 -18
  108. package/src/router/metrics.ts +238 -13
  109. package/src/router/middleware-types.ts +48 -27
  110. package/src/router/middleware.ts +201 -86
  111. package/src/router/navigation-snapshot.ts +182 -0
  112. package/src/router/pattern-matching.ts +77 -11
  113. package/src/router/prerender-match.ts +114 -10
  114. package/src/router/preview-match.ts +30 -102
  115. package/src/router/request-classification.ts +310 -0
  116. package/src/router/revalidation.ts +27 -7
  117. package/src/router/route-snapshot.ts +245 -0
  118. package/src/router/router-context.ts +6 -1
  119. package/src/router/router-interfaces.ts +50 -5
  120. package/src/router/router-options.ts +50 -19
  121. package/src/router/segment-resolution/fresh.ts +215 -19
  122. package/src/router/segment-resolution/helpers.ts +30 -25
  123. package/src/router/segment-resolution/loader-cache.ts +1 -0
  124. package/src/router/segment-resolution/revalidation.ts +454 -301
  125. package/src/router/segment-wrappers.ts +2 -0
  126. package/src/router/trie-matching.ts +30 -6
  127. package/src/router/types.ts +1 -0
  128. package/src/router/url-params.ts +49 -0
  129. package/src/router.ts +89 -17
  130. package/src/rsc/handler.ts +563 -364
  131. package/src/rsc/helpers.ts +69 -41
  132. package/src/rsc/index.ts +0 -20
  133. package/src/rsc/loader-fetch.ts +23 -3
  134. package/src/rsc/manifest-init.ts +5 -1
  135. package/src/rsc/progressive-enhancement.ts +37 -10
  136. package/src/rsc/response-route-handler.ts +14 -1
  137. package/src/rsc/rsc-rendering.ts +47 -44
  138. package/src/rsc/server-action.ts +24 -10
  139. package/src/rsc/ssr-setup.ts +128 -0
  140. package/src/rsc/types.ts +11 -1
  141. package/src/search-params.ts +16 -13
  142. package/src/segment-content-promise.ts +67 -0
  143. package/src/segment-loader-promise.ts +122 -0
  144. package/src/segment-system.tsx +109 -23
  145. package/src/server/context.ts +174 -19
  146. package/src/server/handle-store.ts +19 -0
  147. package/src/server/loader-registry.ts +9 -8
  148. package/src/server/request-context.ts +218 -65
  149. package/src/server.ts +6 -0
  150. package/src/ssr/index.tsx +4 -0
  151. package/src/static-handler.ts +18 -6
  152. package/src/theme/index.ts +4 -13
  153. package/src/types/cache-types.ts +4 -4
  154. package/src/types/handler-context.ts +140 -72
  155. package/src/types/loader-types.ts +41 -15
  156. package/src/types/request-scope.ts +126 -0
  157. package/src/types/route-config.ts +17 -8
  158. package/src/types/route-entry.ts +19 -1
  159. package/src/types/segments.ts +2 -5
  160. package/src/urls/include-helper.ts +24 -14
  161. package/src/urls/path-helper-types.ts +39 -6
  162. package/src/urls/path-helper.ts +48 -13
  163. package/src/urls/pattern-types.ts +12 -0
  164. package/src/urls/response-types.ts +18 -16
  165. package/src/use-loader.tsx +77 -5
  166. package/src/vite/discovery/bundle-postprocess.ts +61 -89
  167. package/src/vite/discovery/discover-routers.ts +7 -4
  168. package/src/vite/discovery/prerender-collection.ts +162 -88
  169. package/src/vite/discovery/state.ts +17 -13
  170. package/src/vite/index.ts +8 -3
  171. package/src/vite/plugin-types.ts +51 -79
  172. package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
  173. package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  174. package/src/vite/plugins/cloudflare-protocol-stub.ts +214 -0
  175. package/src/vite/plugins/expose-action-id.ts +1 -3
  176. package/src/vite/plugins/expose-id-utils.ts +12 -0
  177. package/src/vite/plugins/expose-ids/handler-transform.ts +30 -0
  178. package/src/vite/plugins/expose-internal-ids.ts +257 -40
  179. package/src/vite/plugins/performance-tracks.ts +88 -0
  180. package/src/vite/plugins/refresh-cmd.ts +127 -0
  181. package/src/vite/plugins/version-plugin.ts +13 -1
  182. package/src/vite/rango.ts +190 -217
  183. package/src/vite/router-discovery.ts +241 -45
  184. package/src/vite/utils/banner.ts +4 -4
  185. package/src/vite/utils/package-resolution.ts +34 -1
  186. package/src/vite/utils/prerender-utils.ts +97 -5
  187. package/src/vite/utils/shared-utils.ts +3 -2
  188. package/skills/testing/SKILL.md +0 -226
  189. package/src/route-definition/route-function.ts +0 -119
@@ -7,7 +7,11 @@
7
7
 
8
8
  import type { ReactNode } from "react";
9
9
  import { invariant } from "../../errors";
10
- import type { EntryData } from "../../server/context";
10
+ import {
11
+ getParallelEntries,
12
+ getParallelSlotEntries,
13
+ type EntryData,
14
+ } from "../../server/context";
11
15
  import type {
12
16
  HandlerContext,
13
17
  InternalHandlerContext,
@@ -15,6 +19,8 @@ import type {
15
19
  } from "../../types";
16
20
  import type { SegmentResolutionDeps } from "../types.js";
17
21
  import { resolveLoaderData } from "./loader-cache.js";
22
+ import { _getRequestContext } from "../../server/request-context.js";
23
+ import { appendMetric } from "../metrics.js";
18
24
  import {
19
25
  handleHandlerResult,
20
26
  tryStaticHandler,
@@ -24,6 +30,11 @@ import {
24
30
  } from "./helpers.js";
25
31
  import { getRouterContext } from "../router-context.js";
26
32
  import { resolveSink, safeEmit } from "../telemetry.js";
33
+ import {
34
+ track,
35
+ RSCRouterContext,
36
+ runInsideLoaderScope,
37
+ } from "../../server/context.js";
27
38
 
28
39
  // ---------------------------------------------------------------------------
29
40
  // Streamed handler telemetry
@@ -89,9 +100,11 @@ export async function resolveLoaders<TEnv>(
89
100
  const shortCode = shortCodeOverride ?? entry.shortCode;
90
101
  const hasLoading = "loading" in entry && entry.loading !== undefined;
91
102
  const loadingDisabled = hasLoading && entry.loading === false;
103
+ const ms = _getRequestContext()?._metricsStore;
92
104
 
93
105
  if (!loadingDisabled) {
94
- return loaderEntries.map((loaderEntry, i) => {
106
+ // Streaming loaders: promises kick off now, settle during RSC serialization.
107
+ const segments = loaderEntries.map((loaderEntry, i) => {
95
108
  const { loader } = loaderEntry;
96
109
  const segmentId = `${shortCode}D${i}.${loader.$$id}`;
97
110
  return {
@@ -103,7 +116,9 @@ export async function resolveLoaders<TEnv>(
103
116
  params: ctx.params,
104
117
  loaderId: loader.$$id,
105
118
  loaderData: deps.wrapLoaderPromise(
106
- resolveLoaderData(loaderEntry, ctx, ctx.pathname),
119
+ runInsideLoaderScope(() =>
120
+ resolveLoaderData(loaderEntry, ctx, ctx.pathname),
121
+ ),
107
122
  entry,
108
123
  segmentId,
109
124
  ctx.pathname,
@@ -111,18 +126,38 @@ export async function resolveLoaders<TEnv>(
111
126
  belongsToRoute,
112
127
  };
113
128
  });
129
+
130
+ return segments;
114
131
  }
115
132
 
116
133
  // Loading disabled: still start all loaders in parallel, but only emit
117
134
  // settled promises so handlers don't stream loading placeholders.
118
- const pendingLoaderData = loaderEntries.map((loaderEntry) =>
119
- resolveLoaderData(loaderEntry, ctx, ctx.pathname),
120
- );
121
- await Promise.all(pendingLoaderData);
135
+ const pendingLoaderData = loaderEntries.map((loaderEntry) => {
136
+ const start = performance.now();
137
+ const promise = runInsideLoaderScope(() =>
138
+ resolveLoaderData(loaderEntry, ctx, ctx.pathname),
139
+ );
140
+ return { promise, start, loaderId: loaderEntry.loader.$$id };
141
+ });
142
+ await Promise.all(pendingLoaderData.map((p) => p.promise));
122
143
 
123
144
  return loaderEntries.map((loaderEntry, i) => {
124
145
  const { loader } = loaderEntry;
125
146
  const segmentId = `${shortCode}D${i}.${loader.$$id}`;
147
+ const pending = pendingLoaderData[i]!;
148
+ if (ms && !ms.metrics.some((m) => m.label === `loader:${loader.$$id}`)) {
149
+ // All loaders ran in parallel via Promise.all — each span covers
150
+ // from its own kickoff to the batch settlement, giving a ceiling
151
+ // on that loader's contribution to the overall wait.
152
+ const batchEnd = performance.now();
153
+ appendMetric(
154
+ ms,
155
+ `loader:${loader.$$id}`,
156
+ pending.start,
157
+ batchEnd - pending.start,
158
+ 2,
159
+ );
160
+ }
126
161
  return {
127
162
  id: segmentId,
128
163
  namespace: entry.id,
@@ -132,7 +167,7 @@ export async function resolveLoaders<TEnv>(
132
167
  params: ctx.params,
133
168
  loaderId: loader.$$id,
134
169
  loaderData: deps.wrapLoaderPromise(
135
- pendingLoaderData[i]!,
170
+ pending.promise,
136
171
  entry,
137
172
  segmentId,
138
173
  ctx.pathname,
@@ -178,7 +213,9 @@ export async function resolveSegment<TEnv>(
178
213
  (context as InternalHandlerContext<any, TEnv>)._currentSegmentId =
179
214
  entry.shortCode;
180
215
 
216
+ const doneLayoutHandler = track(`handler:${entry.id}`, 2);
181
217
  const component = await resolveLayoutComponent(entry, context);
218
+ doneLayoutHandler();
182
219
 
183
220
  segments.push({
184
221
  id: entry.shortCode,
@@ -194,7 +231,10 @@ export async function resolveSegment<TEnv>(
194
231
  ...(entry.mountPath ? { mountPath: entry.mountPath } : {}),
195
232
  });
196
233
 
197
- for (const parallelEntry of entry.parallel) {
234
+ const resolvedParallelEntries = new Set<string>();
235
+ for (const { slot, entry: parallelEntry } of getParallelSlotEntries(
236
+ entry.parallel,
237
+ )) {
198
238
  const parallelSegments = await resolveParallelEntry(
199
239
  parallelEntry,
200
240
  params,
@@ -204,8 +244,11 @@ export async function resolveSegment<TEnv>(
204
244
  deps,
205
245
  options,
206
246
  routeKey,
247
+ [slot],
248
+ !resolvedParallelEntries.has(parallelEntry.id),
207
249
  );
208
250
  segments.push(...parallelSegments);
251
+ resolvedParallelEntries.add(parallelEntry.id);
209
252
  }
210
253
 
211
254
  for (const orphan of entry.layout) {
@@ -241,9 +284,16 @@ export async function resolveSegment<TEnv>(
241
284
  entry.shortCode,
242
285
  );
243
286
  if (component === undefined) {
287
+ // For Passthrough routes at runtime, use the live handler instead of
288
+ // the build handler. At build time (context.build === true), always
289
+ // use the build handler from entry.handler.
290
+ const handler =
291
+ !context.build && entry.liveHandler ? entry.liveHandler : entry.handler;
292
+ const doneRouteHandler = track(`handler:${entry.id}`, 2);
244
293
  if (entry.loading) {
245
- const result = handleHandlerResult(entry.handler(context));
294
+ const result = handleHandlerResult(handler(context));
246
295
  if (result instanceof Promise) {
296
+ result.finally(doneRouteHandler).catch(() => {});
247
297
  const tracked = deps.trackHandler(result, {
248
298
  segmentId: entry.shortCode,
249
299
  segmentType: entry.type,
@@ -258,10 +308,12 @@ export async function resolveSegment<TEnv>(
258
308
  );
259
309
  component = tracked;
260
310
  } else {
311
+ doneRouteHandler();
261
312
  component = result;
262
313
  }
263
314
  } else {
264
- component = handleHandlerResult(await entry.handler(context));
315
+ component = handleHandlerResult(await handler(context));
316
+ doneRouteHandler();
265
317
  }
266
318
  }
267
319
 
@@ -275,11 +327,15 @@ export async function resolveSegment<TEnv>(
275
327
  deps,
276
328
  options,
277
329
  routeKey,
330
+ entry,
278
331
  );
279
332
  segments.push(...orphanSegments);
280
333
  }
281
334
 
282
- for (const parallelEntry of entry.parallel) {
335
+ const resolvedParallelEntries = new Set<string>();
336
+ for (const { slot, entry: parallelEntry } of getParallelSlotEntries(
337
+ entry.parallel,
338
+ )) {
283
339
  const parallelSegments = await resolveParallelEntry(
284
340
  parallelEntry,
285
341
  params,
@@ -289,8 +345,11 @@ export async function resolveSegment<TEnv>(
289
345
  deps,
290
346
  options,
291
347
  routeKey,
348
+ [slot],
349
+ !resolvedParallelEntries.has(parallelEntry.id),
292
350
  );
293
351
  segments.push(...parallelSegments);
352
+ resolvedParallelEntries.add(parallelEntry.id);
294
353
  }
295
354
 
296
355
  segments.push({
@@ -298,7 +357,7 @@ export async function resolveSegment<TEnv>(
298
357
  namespace: entry.id,
299
358
  type: "route",
300
359
  index: 0,
301
- component,
360
+ component: component ?? null,
302
361
  loading: entry.loading === false ? null : entry.loading,
303
362
  transition: entry.transition,
304
363
  params,
@@ -324,6 +383,9 @@ export async function resolveOrphanLayout<TEnv>(
324
383
  deps: SegmentResolutionDeps<TEnv>,
325
384
  options?: ResolveSegmentOptions,
326
385
  routeKey?: string,
386
+ /** Parent route entry — its loaders are inherited by the layout so
387
+ * parallel slots inside this layout can access them via useLoader(). */
388
+ parentRouteEntry?: EntryData,
327
389
  ): Promise<ResolvedSegment[]> {
328
390
  invariant(
329
391
  orphan.type === "layout" || orphan.type === "cache",
@@ -339,11 +401,37 @@ export async function resolveOrphanLayout<TEnv>(
339
401
  deps,
340
402
  );
341
403
  segments.push(...loaderSegments);
404
+
405
+ // Inherit parent route's loaders so parallel slots inside this layout
406
+ // can access them via useLoader(). Without this, the route's loaders
407
+ // are only in the route's OutletProvider (rendered as <Outlet /> content),
408
+ // which is a child — not a parent — of the layout's context.
409
+ if (
410
+ parentRouteEntry &&
411
+ parentRouteEntry.loader &&
412
+ parentRouteEntry.loader.length > 0 &&
413
+ Object.keys(orphan.parallel).length > 0
414
+ ) {
415
+ const inheritedLoaders = await resolveLoaders(
416
+ parentRouteEntry,
417
+ context,
418
+ belongsToRoute,
419
+ deps,
420
+ orphan.shortCode,
421
+ );
422
+ // Tag as inherited so buildMatchResult can deduplicate when safe
423
+ for (const s of inheritedLoaders) {
424
+ s._inherited = true;
425
+ }
426
+ segments.push(...inheritedLoaders);
427
+ }
342
428
  }
343
429
 
344
430
  // Handler-first: orphan layout handler executes before its parallels
345
431
  // so that ctx.set() values are visible to parallel children.
432
+ const doneOrphanHandler = track(`handler:${orphan.id}`, 2);
346
433
  const component = await resolveLayoutComponent(orphan, context);
434
+ doneOrphanHandler();
347
435
 
348
436
  segments.push({
349
437
  id: orphan.shortCode,
@@ -359,7 +447,10 @@ export async function resolveOrphanLayout<TEnv>(
359
447
  ...(orphan.mountPath ? { mountPath: orphan.mountPath } : {}),
360
448
  });
361
449
 
362
- for (const parallelEntry of orphan.parallel) {
450
+ const resolvedParallelEntries = new Set<string>();
451
+ for (const { slot, entry: parallelEntry } of getParallelSlotEntries(
452
+ orphan.parallel,
453
+ )) {
363
454
  const parallelSegments = await resolveParallelEntry(
364
455
  parallelEntry,
365
456
  params,
@@ -369,8 +460,11 @@ export async function resolveOrphanLayout<TEnv>(
369
460
  deps,
370
461
  options,
371
462
  routeKey,
463
+ [slot],
464
+ !resolvedParallelEntries.has(parallelEntry.id),
372
465
  );
373
466
  segments.push(...parallelSegments);
467
+ resolvedParallelEntries.add(parallelEntry.id);
374
468
  }
375
469
 
376
470
  return segments;
@@ -388,6 +482,8 @@ export async function resolveParallelEntry<TEnv>(
388
482
  deps: SegmentResolutionDeps<TEnv>,
389
483
  options?: ResolveSegmentOptions,
390
484
  routeKey?: string,
485
+ slotNames?: `@${string}`[],
486
+ includeLoaders: boolean = true,
391
487
  ): Promise<ResolvedSegment[]> {
392
488
  invariant(
393
489
  parallelEntry.type === "parallel",
@@ -402,7 +498,12 @@ export async function resolveParallelEntry<TEnv>(
402
498
  | ReactNode
403
499
  >;
404
500
 
405
- for (const [slot, handler] of Object.entries(slots)) {
501
+ const slotsToResolve = slotNames ?? (Object.keys(slots) as `@${string}`[]);
502
+
503
+ for (const slot of slotsToResolve) {
504
+ // Try static lookup first — in production, handler bodies are evicted
505
+ // and replaced with stubs that have no .handler property (undefined).
506
+ // The static store holds the pre-rendered component for these slots.
406
507
  let component: ReactNode | undefined = await tryStaticSlot(
407
508
  parallelEntry,
408
509
  slot,
@@ -410,12 +511,21 @@ export async function resolveParallelEntry<TEnv>(
410
511
  );
411
512
 
412
513
  if (component === undefined) {
514
+ const handler = slots[slot];
515
+ if (handler === undefined) {
516
+ continue;
517
+ }
518
+ const doneParallelHandler = track(
519
+ `handler:${parallelEntry.id}.${slot}`,
520
+ 2,
521
+ );
413
522
  const hasLoadingFallback =
414
523
  parallelEntry.loading !== undefined && parallelEntry.loading !== false;
415
524
  if (hasLoadingFallback) {
416
525
  const result =
417
526
  typeof handler === "function" ? handler(context) : handler;
418
527
  if (result instanceof Promise) {
528
+ result.finally(doneParallelHandler).catch(() => {});
419
529
  const tracked = deps.trackHandler(result, {
420
530
  segmentId: `${parentShortCode}.${slot}`,
421
531
  segmentType: "parallel",
@@ -430,11 +540,13 @@ export async function resolveParallelEntry<TEnv>(
430
540
  );
431
541
  component = tracked as ReactNode;
432
542
  } else {
543
+ doneParallelHandler();
433
544
  component = result as ReactNode;
434
545
  }
435
546
  } else {
436
547
  component =
437
548
  typeof handler === "function" ? await handler(context) : handler;
549
+ doneParallelHandler();
438
550
  }
439
551
  }
440
552
 
@@ -456,7 +568,7 @@ export async function resolveParallelEntry<TEnv>(
456
568
  });
457
569
  }
458
570
 
459
- if (!parallelEntry.loading && !options?.skipLoaders) {
571
+ if (!options?.skipLoaders && includeLoaders) {
460
572
  const loaderSegments = await resolveLoaders(
461
573
  parallelEntry,
462
574
  context,
@@ -464,6 +576,15 @@ export async function resolveParallelEntry<TEnv>(
464
576
  deps,
465
577
  parentShortCode,
466
578
  );
579
+ // Tag parallel-owned loaders so renderSegments can stream them
580
+ // using the parallel's loading() instead of awaiting on the layout
581
+ const parallelLoading =
582
+ parallelEntry.loading === false ? undefined : parallelEntry.loading;
583
+ if (parallelLoading) {
584
+ for (const seg of loaderSegments) {
585
+ seg.parallelLoading = parallelLoading;
586
+ }
587
+ }
467
588
  segments.push(...loaderSegments);
468
589
  }
469
590
 
@@ -499,6 +620,14 @@ export async function resolveAllSegments<TEnv>(
499
620
  } catch {}
500
621
 
501
622
  for (const entry of entries) {
623
+ // Set ALS flag when entering a cache() boundary so that ctx.get()
624
+ // can guard non-cacheable variable reads. Also guards response-level
625
+ // side effects (headers.set). Persists for all descendant entries.
626
+ if (entry.type === "cache") {
627
+ const store = RSCRouterContext.getStore();
628
+ if (store) store.insideCacheScope = true;
629
+ }
630
+ const doneEntry = track(`segment:${entry.id}`, 1);
502
631
  const resolvedSegments = await resolveWithErrorBoundary(
503
632
  entry,
504
633
  params,
@@ -518,6 +647,7 @@ export async function resolveAllSegments<TEnv>(
518
647
  { request: safeRequest, url: context.url, routeKey, telemetry },
519
648
  context.pathname,
520
649
  );
650
+ doneEntry();
521
651
  // Deduplicate by segment ID. include() scopes can produce entries that
522
652
  // resolve the same shared layout/loader segment. Duplicates in the segment
523
653
  // array propagate to the client's matched[] and change the React tree depth.
@@ -541,11 +671,77 @@ export async function resolveLoadersOnly<TEnv>(
541
671
  deps: SegmentResolutionDeps<TEnv>,
542
672
  ): Promise<ResolvedSegment[]> {
543
673
  const loaderSegments: ResolvedSegment[] = [];
674
+ const seenIds = new Set<string>();
675
+
676
+ async function collectEntryLoaders(
677
+ entry: EntryData,
678
+ belongsToRoute: boolean,
679
+ shortCodeOverride?: string,
680
+ ): Promise<void> {
681
+ // Skip if all loaders from this entry have already been resolved
682
+ // via a parent (e.g., cache boundary wrapping a layout with shared loaders).
683
+ const entryLoaders = entry.loader ?? [];
684
+ const sc = shortCodeOverride ?? entry.shortCode;
685
+ const allAlreadySeen =
686
+ entryLoaders.length > 0 &&
687
+ entryLoaders.every((le, i) =>
688
+ seenIds.has(`${sc}D${i}.${le.loader.$$id}`),
689
+ );
690
+ if (!allAlreadySeen) {
691
+ const segments = await resolveLoaders(
692
+ entry,
693
+ context,
694
+ belongsToRoute,
695
+ deps,
696
+ shortCodeOverride,
697
+ );
698
+ for (const seg of segments) {
699
+ if (!seenIds.has(seg.id)) {
700
+ seenIds.add(seg.id);
701
+ loaderSegments.push(seg);
702
+ }
703
+ }
704
+ }
705
+
706
+ const seenParallelEntryIds = new Set<string>();
707
+ for (const parallelEntry of getParallelEntries(entry.parallel)) {
708
+ if (seenParallelEntryIds.has(parallelEntry.id)) continue;
709
+ seenParallelEntryIds.add(parallelEntry.id);
710
+ await collectEntryLoaders(parallelEntry, belongsToRoute, entry.shortCode);
711
+ }
712
+
713
+ const childBelongsToRoute = belongsToRoute || entry.type === "route";
714
+ for (const layoutEntry of entry.layout) {
715
+ await collectEntryLoaders(layoutEntry, childBelongsToRoute);
716
+ // Inherit route loaders for orphan layouts with parallels.
717
+ // Resolve directly — do NOT re-enter collectEntryLoaders with the
718
+ // route entry, as that would re-iterate route.layout and loop.
719
+ if (
720
+ entry.type === "route" &&
721
+ entry.loader &&
722
+ entry.loader.length > 0 &&
723
+ Object.keys(layoutEntry.parallel).length > 0
724
+ ) {
725
+ const inherited = await resolveLoaders(
726
+ entry,
727
+ context,
728
+ childBelongsToRoute,
729
+ deps,
730
+ layoutEntry.shortCode,
731
+ );
732
+ for (const seg of inherited) {
733
+ if (!seenIds.has(seg.id)) {
734
+ seenIds.add(seg.id);
735
+ seg._inherited = true;
736
+ loaderSegments.push(seg);
737
+ }
738
+ }
739
+ }
740
+ }
741
+ }
544
742
 
545
743
  for (const entry of entries) {
546
- const belongsToRoute = entry.type === "route";
547
- const segments = await resolveLoaders(entry, context, belongsToRoute, deps);
548
- loaderSegments.push(...segments);
744
+ await collectEntryLoaders(entry, entry.type === "route");
549
745
  }
550
746
 
551
747
  return loaderSegments;
@@ -8,7 +8,7 @@
8
8
  * - Error boundary segment creation
9
9
  */
10
10
 
11
- import type { ReactNode } from "react";
11
+ import { createElement, type ReactNode } from "react";
12
12
  import { DataNotFoundError } from "../../errors";
13
13
  import {
14
14
  createErrorInfo,
@@ -174,40 +174,45 @@ export function catchSegmentError<TEnv>(
174
174
  const setResponseStatus = (status: number) => {
175
175
  const reqCtx = getRequestContext();
176
176
  if (reqCtx) {
177
- reqCtx.setStatus(status);
177
+ reqCtx._setStatus(status);
178
178
  }
179
179
  };
180
180
 
181
181
  if (error instanceof DataNotFoundError) {
182
182
  const notFoundFallback = deps.findNearestNotFoundBoundary(entry);
183
+ // Fall back to router's notFound component, then a plain default
184
+ const notFoundOption = deps.notFoundComponent;
185
+ const defaultFallback =
186
+ typeof notFoundOption === "function"
187
+ ? notFoundOption({ pathname: pathname ?? "" })
188
+ : (notFoundOption ?? createElement("h1", null, "Not Found"));
189
+ const effectiveNotFoundFallback = notFoundFallback ?? defaultFallback;
183
190
 
184
- if (notFoundFallback) {
185
- const notFoundInfo = createNotFoundInfo(
186
- error,
187
- entry.shortCode,
188
- entry.type,
189
- pathname,
190
- );
191
+ const notFoundInfo = createNotFoundInfo(
192
+ error,
193
+ entry.shortCode,
194
+ entry.type,
195
+ pathname,
196
+ );
191
197
 
192
- reportError(true, {
193
- notFound: true,
194
- message: notFoundInfo.message,
195
- });
198
+ reportError(true, {
199
+ notFound: true,
200
+ message: notFoundInfo.message,
201
+ });
196
202
 
197
- debugLog("segment", "notFound boundary handled error", {
198
- segmentId: entry.shortCode,
199
- message: notFoundInfo.message,
200
- });
203
+ debugLog("segment", "notFound boundary handled error", {
204
+ segmentId: entry.shortCode,
205
+ message: notFoundInfo.message,
206
+ });
201
207
 
202
- setResponseStatus(404);
208
+ setResponseStatus(404);
203
209
 
204
- return createNotFoundSegment(
205
- notFoundInfo,
206
- notFoundFallback,
207
- entry,
208
- params,
209
- );
210
- }
210
+ return createNotFoundSegment(
211
+ notFoundInfo,
212
+ effectiveNotFoundFallback,
213
+ entry,
214
+ params,
215
+ );
211
216
  }
212
217
 
213
218
  const fallback = deps.findNearestErrorBoundary(entry);
@@ -147,6 +147,7 @@ export function resolveLoaderData<TEnv>(
147
147
  }
148
148
 
149
149
  const loaderId = loaderEntry.loader.$$id;
150
+
150
151
  const ttl = resolveTtl(options.ttl, store.defaults, DEFAULT_ROUTE_TTL);
151
152
  const swrWindow = resolveSwrWindow(options.swr, store.defaults);
152
153
  const swr = swrWindow || undefined;