@rangojs/router 0.0.0-experimental.39 → 0.0.0-experimental.3b1deca8

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 (89) hide show
  1. package/dist/bin/rango.js +8 -3
  2. package/dist/vite/index.js +292 -204
  3. package/package.json +1 -1
  4. package/skills/cache-guide/SKILL.md +32 -0
  5. package/skills/caching/SKILL.md +45 -4
  6. package/skills/loader/SKILL.md +53 -43
  7. package/skills/parallel/SKILL.md +126 -0
  8. package/skills/route/SKILL.md +31 -0
  9. package/skills/router-setup/SKILL.md +52 -2
  10. package/skills/typesafety/SKILL.md +10 -0
  11. package/src/browser/debug-channel.ts +93 -0
  12. package/src/browser/event-controller.ts +5 -0
  13. package/src/browser/navigation-bridge.ts +1 -5
  14. package/src/browser/navigation-client.ts +84 -27
  15. package/src/browser/navigation-transaction.ts +11 -9
  16. package/src/browser/partial-update.ts +50 -9
  17. package/src/browser/prefetch/cache.ts +57 -5
  18. package/src/browser/prefetch/fetch.ts +30 -21
  19. package/src/browser/prefetch/queue.ts +92 -20
  20. package/src/browser/prefetch/resource-ready.ts +77 -0
  21. package/src/browser/react/Link.tsx +9 -1
  22. package/src/browser/react/NavigationProvider.tsx +32 -3
  23. package/src/browser/rsc-router.tsx +109 -57
  24. package/src/browser/scroll-restoration.ts +31 -34
  25. package/src/browser/segment-reconciler.ts +6 -1
  26. package/src/browser/server-action-bridge.ts +12 -0
  27. package/src/browser/types.ts +17 -1
  28. package/src/build/route-types/router-processing.ts +12 -2
  29. package/src/cache/cache-runtime.ts +15 -11
  30. package/src/cache/cache-scope.ts +48 -7
  31. package/src/cache/cf/cf-cache-store.ts +453 -11
  32. package/src/cache/cf/index.ts +5 -1
  33. package/src/cache/document-cache.ts +17 -7
  34. package/src/cache/index.ts +1 -0
  35. package/src/cache/taint.ts +55 -0
  36. package/src/context-var.ts +72 -2
  37. package/src/debug.ts +2 -2
  38. package/src/deps/browser.ts +1 -0
  39. package/src/route-definition/dsl-helpers.ts +32 -7
  40. package/src/route-definition/helpers-types.ts +6 -5
  41. package/src/route-definition/redirect.ts +2 -2
  42. package/src/route-map-builder.ts +7 -1
  43. package/src/router/find-match.ts +4 -2
  44. package/src/router/handler-context.ts +31 -8
  45. package/src/router/intercept-resolution.ts +2 -0
  46. package/src/router/lazy-includes.ts +4 -1
  47. package/src/router/loader-resolution.ts +7 -1
  48. package/src/router/logging.ts +5 -2
  49. package/src/router/manifest.ts +9 -3
  50. package/src/router/match-middleware/background-revalidation.ts +30 -2
  51. package/src/router/match-middleware/cache-lookup.ts +66 -9
  52. package/src/router/match-middleware/cache-store.ts +53 -10
  53. package/src/router/match-middleware/intercept-resolution.ts +9 -7
  54. package/src/router/match-middleware/segment-resolution.ts +8 -5
  55. package/src/router/match-result.ts +22 -6
  56. package/src/router/metrics.ts +6 -1
  57. package/src/router/middleware-types.ts +6 -2
  58. package/src/router/middleware.ts +4 -3
  59. package/src/router/router-context.ts +6 -1
  60. package/src/router/segment-resolution/fresh.ts +130 -17
  61. package/src/router/segment-resolution/helpers.ts +29 -24
  62. package/src/router/segment-resolution/loader-cache.ts +1 -0
  63. package/src/router/segment-resolution/revalidation.ts +352 -290
  64. package/src/router/segment-wrappers.ts +2 -0
  65. package/src/router/types.ts +1 -0
  66. package/src/router.ts +6 -1
  67. package/src/rsc/handler.ts +28 -2
  68. package/src/rsc/loader-fetch.ts +7 -2
  69. package/src/rsc/progressive-enhancement.ts +4 -1
  70. package/src/rsc/rsc-rendering.ts +4 -1
  71. package/src/rsc/server-action.ts +2 -0
  72. package/src/rsc/types.ts +7 -1
  73. package/src/segment-system.tsx +140 -4
  74. package/src/server/context.ts +102 -13
  75. package/src/server/request-context.ts +59 -12
  76. package/src/ssr/index.tsx +1 -0
  77. package/src/types/handler-context.ts +120 -22
  78. package/src/types/loader-types.ts +4 -4
  79. package/src/types/route-entry.ts +7 -0
  80. package/src/types/segments.ts +2 -0
  81. package/src/urls/path-helper.ts +1 -1
  82. package/src/vite/discovery/state.ts +0 -2
  83. package/src/vite/plugin-types.ts +0 -83
  84. package/src/vite/plugins/expose-action-id.ts +1 -3
  85. package/src/vite/plugins/performance-tracks.ts +235 -0
  86. package/src/vite/plugins/version-plugin.ts +13 -1
  87. package/src/vite/rango.ts +148 -209
  88. package/src/vite/router-discovery.ts +0 -8
  89. package/src/vite/utils/banner.ts +3 -3
@@ -10,7 +10,11 @@ import type { ReactNode } from "react";
10
10
  import { invariant } from "../../errors";
11
11
  import { revalidate } from "../loader-resolution.js";
12
12
  import { evaluateRevalidation } from "../revalidation.js";
13
- import type { EntryData } from "../../server/context";
13
+ import {
14
+ getParallelEntries,
15
+ getParallelSlotEntries,
16
+ type EntryData,
17
+ } from "../../server/context";
14
18
  import type {
15
19
  HandlerContext,
16
20
  InternalHandlerContext,
@@ -38,6 +42,7 @@ import {
38
42
  import { getRouterContext } from "../router-context.js";
39
43
  import { resolveSink, safeEmit } from "../telemetry.js";
40
44
  import { track } from "../../server/context.js";
45
+ import { RSCRouterContext } from "../../server/context.js";
41
46
 
42
47
  // ---------------------------------------------------------------------------
43
48
  // Telemetry helpers
@@ -258,26 +263,62 @@ export async function resolveLoadersOnlyWithRevalidation<TEnv>(
258
263
  ): Promise<{ segments: ResolvedSegment[]; matchedIds: string[] }> {
259
264
  const allLoaderSegments: ResolvedSegment[] = [];
260
265
  const allMatchedIds: string[] = [];
266
+ const seenIds = new Set<string>();
267
+
268
+ async function collectEntryLoaders(
269
+ entry: EntryData,
270
+ belongsToRoute: boolean,
271
+ shortCodeOverride?: string,
272
+ ): Promise<void> {
273
+ // Skip if all loaders from this entry have already been resolved
274
+ // via a parent (e.g., cache boundary wrapping a layout with shared loaders).
275
+ const loaderEntries = entry.loader ?? [];
276
+ const sc = shortCodeOverride ?? entry.shortCode;
277
+ const allAlreadySeen =
278
+ loaderEntries.length > 0 &&
279
+ loaderEntries.every((le, i) =>
280
+ seenIds.has(`${sc}D${i}.${le.loader.$$id}`),
281
+ );
282
+ if (!allAlreadySeen) {
283
+ const { segments, matchedIds } = await resolveLoadersWithRevalidation(
284
+ entry,
285
+ context,
286
+ belongsToRoute,
287
+ clientSegmentIds,
288
+ prevParams,
289
+ request,
290
+ prevUrl,
291
+ nextUrl,
292
+ routeKey,
293
+ deps,
294
+ actionContext,
295
+ shortCodeOverride,
296
+ stale,
297
+ );
298
+ for (const seg of segments) {
299
+ if (!seenIds.has(seg.id)) {
300
+ seenIds.add(seg.id);
301
+ allLoaderSegments.push(seg);
302
+ }
303
+ }
304
+ allMatchedIds.push(...matchedIds);
305
+ }
306
+
307
+ const seenParallelEntryIds = new Set<string>();
308
+ for (const parallelEntry of getParallelEntries(entry.parallel)) {
309
+ if (seenParallelEntryIds.has(parallelEntry.id)) continue;
310
+ seenParallelEntryIds.add(parallelEntry.id);
311
+ await collectEntryLoaders(parallelEntry, belongsToRoute, entry.shortCode);
312
+ }
313
+
314
+ const childBelongsToRoute = belongsToRoute || entry.type === "route";
315
+ for (const layoutEntry of entry.layout) {
316
+ await collectEntryLoaders(layoutEntry, childBelongsToRoute);
317
+ }
318
+ }
261
319
 
262
320
  for (const entry of entries) {
263
- const belongsToRoute = entry.type === "route";
264
- const { segments, matchedIds } = await resolveLoadersWithRevalidation(
265
- entry,
266
- context,
267
- belongsToRoute,
268
- clientSegmentIds,
269
- prevParams,
270
- request,
271
- prevUrl,
272
- nextUrl,
273
- routeKey,
274
- deps,
275
- actionContext,
276
- undefined, // shortCodeOverride
277
- stale,
278
- );
279
- allLoaderSegments.push(...segments);
280
- allMatchedIds.push(...matchedIds);
321
+ await collectEntryLoaders(entry, entry.type === "route");
281
322
  }
282
323
 
283
324
  return { segments: allLoaderSegments, matchedIds: allMatchedIds };
@@ -301,22 +342,20 @@ export function buildEntryRevalidateMap(
301
342
  map.set(entry.shortCode, { entry, revalidate: entry.revalidate });
302
343
 
303
344
  if (entry.type !== "parallel") {
304
- for (const parallelEntry of entry.parallel) {
305
- if (parallelEntry.type === "parallel") {
306
- const slots = Object.keys(parallelEntry.handler) as `@${string}`[];
307
- for (const slot of slots) {
308
- const parallelId = `${parallelEntry.shortCode}.${slot}`;
309
- map.set(parallelId, {
310
- entry: parallelEntry,
311
- revalidate: parallelEntry.revalidate,
312
- });
313
- }
314
- }
345
+ for (const { slot, entry: parallelEntry } of getParallelSlotEntries(
346
+ entry.parallel,
347
+ )) {
348
+ const parallelParentShortCode = parentShortCode ?? entry.shortCode;
349
+ const parallelId = `${parallelParentShortCode}.${slot}`;
350
+ map.set(parallelId, {
351
+ entry: parallelEntry,
352
+ revalidate: parallelEntry.revalidate,
353
+ });
315
354
  }
316
355
  }
317
356
 
318
357
  for (const layoutEntry of entry.layout) {
319
- processEntry(layoutEntry);
358
+ processEntry(layoutEntry, entry.shortCode);
320
359
  }
321
360
  }
322
361
 
@@ -348,7 +387,10 @@ export async function resolveParallelSegmentsWithRevalidation<TEnv>(
348
387
  const segments: ResolvedSegment[] = [];
349
388
  const matchedIds: string[] = [];
350
389
 
351
- for (const parallelEntry of entry.parallel) {
390
+ const resolvedParallelEntries = new Set<string>();
391
+ for (const { slot, entry: parallelEntry } of getParallelSlotEntries(
392
+ entry.parallel,
393
+ )) {
352
394
  invariant(
353
395
  parallelEntry.type === "parallel",
354
396
  `Expected parallel entry, got: ${parallelEntry.type}`,
@@ -359,141 +401,61 @@ export async function resolveParallelSegmentsWithRevalidation<TEnv>(
359
401
  | ((ctx: HandlerContext<any, TEnv>) => ReactNode | Promise<ReactNode>)
360
402
  | ReactNode
361
403
  >;
404
+ // In production, static handler bodies are evicted and the slot value
405
+ // may be undefined. The static store holds the pre-rendered component.
406
+ // We defer the handler check until after tryStaticSlot.
407
+ const handler = slots[slot];
408
+
409
+ const parallelId = `${entry.shortCode}.${slot}`;
410
+
411
+ const isFullRefetch = clientSegmentIds.size === 0;
412
+ const isNewParent = !clientSegmentIds.has(entry.shortCode);
413
+ if (
414
+ isFullRefetch ||
415
+ clientSegmentIds.has(parallelId) ||
416
+ belongsToRoute ||
417
+ isNewParent
418
+ ) {
419
+ matchedIds.push(parallelId);
420
+ }
362
421
 
363
- for (const [slot, handler] of Object.entries(slots)) {
364
- const parallelId = `${entry.shortCode}.${slot}`;
365
-
366
- const isFullRefetch = clientSegmentIds.size === 0;
367
- // When the parent layout is new (not in client's segment set),
368
- // all its parallel children must be resolved and tracked.
369
- // Without this, navigating to a new layout with parallels
370
- // (e.g., BlogLayout with @sidebar) from a different route
371
- // would silently drop those parallel segments.
372
- const isNewParent = !clientSegmentIds.has(entry.shortCode);
373
- if (
374
- isFullRefetch ||
375
- clientSegmentIds.has(parallelId) ||
376
- belongsToRoute ||
377
- isNewParent
378
- ) {
379
- matchedIds.push(parallelId);
380
- }
381
-
382
- const shouldResolve = await (async () => {
383
- if (isFullRefetch) {
384
- if (isTraceActive()) {
385
- pushRevalidationTraceEntry({
386
- segmentId: parallelId,
387
- segmentType: "parallel",
388
- belongsToRoute,
389
- source: "parallel",
390
- defaultShouldRevalidate: true,
391
- finalShouldRevalidate: true,
392
- reason: "full-refetch",
393
- });
394
- }
395
- return true;
396
- }
397
- if (!clientSegmentIds.has(parallelId)) {
398
- const result = belongsToRoute || isNewParent;
399
- if (isTraceActive()) {
400
- pushRevalidationTraceEntry({
401
- segmentId: parallelId,
402
- segmentType: "parallel",
403
- belongsToRoute,
404
- source: "parallel",
405
- defaultShouldRevalidate: result,
406
- finalShouldRevalidate: result,
407
- reason: result ? "new-segment" : "skip-parent-chain",
408
- });
409
- }
410
- return result;
422
+ const shouldResolve = await (async () => {
423
+ if (isFullRefetch) {
424
+ if (isTraceActive()) {
425
+ pushRevalidationTraceEntry({
426
+ segmentId: parallelId,
427
+ segmentType: "parallel",
428
+ belongsToRoute,
429
+ source: "parallel",
430
+ defaultShouldRevalidate: true,
431
+ finalShouldRevalidate: true,
432
+ reason: "full-refetch",
433
+ });
411
434
  }
412
-
413
- const dummySegment: ResolvedSegment = {
414
- id: parallelId,
415
- namespace: parallelEntry.id,
416
- type: "parallel",
417
- index: 0,
418
- component: null as any,
419
- params,
420
- slot,
421
- belongsToRoute,
422
- parallelName: `${parallelEntry.id}.${slot}`,
423
- ...(parallelEntry.mountPath
424
- ? { mountPath: parallelEntry.mountPath }
425
- : {}),
426
- };
427
-
428
- return await evaluateRevalidation({
429
- segment: dummySegment,
430
- prevParams,
431
- getPrevSegment: null,
432
- request,
433
- prevUrl,
434
- nextUrl,
435
- revalidations: parallelEntry.revalidate.map((fn, i) => ({
436
- name: `revalidate${i}`,
437
- fn,
438
- })),
439
- routeKey,
440
- context,
441
- actionContext,
442
- stale,
443
- traceSource: "parallel",
444
- });
445
- })();
446
- emitRevalidationDecision(
447
- parallelId,
448
- context.pathname,
449
- routeKey,
450
- shouldResolve,
451
- );
452
-
453
- let component: ReactNode | undefined;
454
- if (shouldResolve) {
455
- component = await tryStaticSlot(parallelEntry, slot, parallelId);
435
+ return true;
456
436
  }
457
- if (component === undefined) {
458
- const hasLoadingFallback =
459
- parallelEntry.loading !== undefined &&
460
- parallelEntry.loading !== false;
461
- if (!shouldResolve) {
462
- component = null;
463
- } else if (hasLoadingFallback) {
464
- const result =
465
- typeof handler === "function" ? handler(context) : handler;
466
- if (result instanceof Promise) {
467
- const tracked = deps.trackHandler(result, {
468
- segmentId: parallelId,
469
- segmentType: "parallel",
470
- });
471
- observeStreamedHandler(
472
- tracked,
473
- parallelId,
474
- "parallel",
475
- context.pathname,
476
- routeKey,
477
- params,
478
- );
479
- component = tracked as ReactNode;
480
- } else {
481
- component = result as ReactNode;
482
- }
483
- } else {
484
- component =
485
- typeof handler === "function" ? await handler(context) : handler;
437
+ if (!clientSegmentIds.has(parallelId)) {
438
+ const result = belongsToRoute || isNewParent;
439
+ if (isTraceActive()) {
440
+ pushRevalidationTraceEntry({
441
+ segmentId: parallelId,
442
+ segmentType: "parallel",
443
+ belongsToRoute,
444
+ source: "parallel",
445
+ defaultShouldRevalidate: result,
446
+ finalShouldRevalidate: result,
447
+ reason: result ? "new-segment" : "skip-parent-chain",
448
+ });
486
449
  }
450
+ return result;
487
451
  }
488
452
 
489
- segments.push({
453
+ const dummySegment: ResolvedSegment = {
490
454
  id: parallelId,
491
455
  namespace: parallelEntry.id,
492
456
  type: "parallel",
493
457
  index: 0,
494
- component,
495
- loading: parallelEntry.loading === false ? null : parallelEntry.loading,
496
- transition: parallelEntry.transition,
458
+ component: null as any,
497
459
  params,
498
460
  slot,
499
461
  belongsToRoute,
@@ -501,28 +463,111 @@ export async function resolveParallelSegmentsWithRevalidation<TEnv>(
501
463
  ...(parallelEntry.mountPath
502
464
  ? { mountPath: parallelEntry.mountPath }
503
465
  : {}),
504
- });
505
- }
466
+ };
506
467
 
507
- if (!parallelEntry.loading) {
508
- const loaderResult = await resolveLoadersWithRevalidation(
509
- parallelEntry,
510
- context,
511
- belongsToRoute,
512
- clientSegmentIds,
468
+ return await evaluateRevalidation({
469
+ segment: dummySegment,
513
470
  prevParams,
471
+ getPrevSegment: null,
514
472
  request,
515
473
  prevUrl,
516
474
  nextUrl,
475
+ revalidations: parallelEntry.revalidate.map((fn, i) => ({
476
+ name: `revalidate${i}`,
477
+ fn,
478
+ })),
517
479
  routeKey,
518
- deps,
480
+ context,
519
481
  actionContext,
520
- entry.shortCode,
521
482
  stale,
522
- );
523
- segments.push(...loaderResult.segments);
524
- matchedIds.push(...loaderResult.matchedIds);
483
+ traceSource: "parallel",
484
+ });
485
+ })();
486
+ emitRevalidationDecision(
487
+ parallelId,
488
+ context.pathname,
489
+ routeKey,
490
+ shouldResolve,
491
+ );
492
+
493
+ let component: ReactNode | undefined;
494
+ if (shouldResolve) {
495
+ component = await tryStaticSlot(parallelEntry, slot, parallelId);
525
496
  }
497
+ if (component === undefined) {
498
+ const hasLoadingFallback =
499
+ parallelEntry.loading !== undefined && parallelEntry.loading !== false;
500
+ if (!shouldResolve) {
501
+ component = null;
502
+ } else if (handler === undefined) {
503
+ // Handler evicted (production static slot) but static lookup missed.
504
+ // Nothing to render — use null so the client keeps its cached version.
505
+ component = null;
506
+ } else if (hasLoadingFallback) {
507
+ const result =
508
+ typeof handler === "function" ? handler(context) : handler;
509
+ if (result instanceof Promise) {
510
+ const tracked = deps.trackHandler(result, {
511
+ segmentId: parallelId,
512
+ segmentType: "parallel",
513
+ });
514
+ observeStreamedHandler(
515
+ tracked,
516
+ parallelId,
517
+ "parallel",
518
+ context.pathname,
519
+ routeKey,
520
+ params,
521
+ );
522
+ component = tracked as ReactNode;
523
+ } else {
524
+ component = result as ReactNode;
525
+ }
526
+ } else {
527
+ component =
528
+ typeof handler === "function" ? await handler(context) : handler;
529
+ }
530
+ }
531
+
532
+ segments.push({
533
+ id: parallelId,
534
+ namespace: parallelEntry.id,
535
+ type: "parallel",
536
+ index: 0,
537
+ component,
538
+ loading: parallelEntry.loading === false ? null : parallelEntry.loading,
539
+ transition: parallelEntry.transition,
540
+ params,
541
+ slot,
542
+ belongsToRoute,
543
+ parallelName: `${parallelEntry.id}.${slot}`,
544
+ ...(parallelEntry.mountPath
545
+ ? { mountPath: parallelEntry.mountPath }
546
+ : {}),
547
+ });
548
+
549
+ if (resolvedParallelEntries.has(parallelEntry.id)) {
550
+ continue;
551
+ }
552
+
553
+ const loaderResult = await resolveLoadersWithRevalidation(
554
+ parallelEntry,
555
+ context,
556
+ belongsToRoute,
557
+ clientSegmentIds,
558
+ prevParams,
559
+ request,
560
+ prevUrl,
561
+ nextUrl,
562
+ routeKey,
563
+ deps,
564
+ actionContext,
565
+ entry.shortCode,
566
+ stale,
567
+ );
568
+ segments.push(...loaderResult.segments);
569
+ matchedIds.push(...loaderResult.matchedIds);
570
+ resolvedParallelEntries.add(parallelEntry.id);
526
571
  }
527
572
 
528
573
  return { segments, matchedIds };
@@ -608,6 +653,8 @@ export async function resolveEntryHandlerWithRevalidation<TEnv>(
608
653
  context,
609
654
  actionContext,
610
655
  stale,
656
+ traceSource:
657
+ entry.type === "route" ? "route-handler" : "layout-handler",
611
658
  });
612
659
  emitRevalidationDecision(
613
660
  entry.shortCode,
@@ -676,10 +723,12 @@ export async function resolveEntryHandlerWithRevalidation<TEnv>(
676
723
  () => null,
677
724
  );
678
725
 
726
+ // Normalize void handlers (undefined) to null so the reconciler's
727
+ // component === null checks work consistently for both void and explicit null.
679
728
  const resolvedComponent =
680
729
  component && typeof component === "object" && "content" in component
681
- ? (component as { content: ReactNode }).content
682
- : component;
730
+ ? ((component as { content: ReactNode }).content ?? null)
731
+ : (component ?? null);
683
732
 
684
733
  const segment: ResolvedSegment = {
685
734
  id: entry.shortCode,
@@ -995,143 +1044,72 @@ export async function resolveOrphanLayoutWithRevalidation<TEnv>(
995
1044
  ...(orphan.mountPath ? { mountPath: orphan.mountPath } : {}),
996
1045
  });
997
1046
 
998
- for (const parallelEntry of orphan.parallel) {
1047
+ const resolvedParallelEntries = new Set<string>();
1048
+ for (const { slot, entry: parallelEntry } of getParallelSlotEntries(
1049
+ orphan.parallel,
1050
+ )) {
999
1051
  invariant(
1000
1052
  parallelEntry.type === "parallel",
1001
1053
  `Expected parallel entry, got: ${parallelEntry.type}`,
1002
1054
  );
1003
1055
 
1004
- const loaderResult = await resolveLoadersWithRevalidation(
1005
- parallelEntry,
1006
- context,
1007
- belongsToRoute,
1008
- clientSegmentIds,
1009
- prevParams,
1010
- request,
1011
- prevUrl,
1012
- nextUrl,
1013
- routeKey,
1014
- deps,
1015
- actionContext,
1016
- undefined,
1017
- stale,
1018
- );
1019
- segments.push(...loaderResult.segments);
1020
- matchedIds.push(...loaderResult.matchedIds);
1056
+ if (!resolvedParallelEntries.has(parallelEntry.id)) {
1057
+ const loaderResult = await resolveLoadersWithRevalidation(
1058
+ parallelEntry,
1059
+ context,
1060
+ belongsToRoute,
1061
+ clientSegmentIds,
1062
+ prevParams,
1063
+ request,
1064
+ prevUrl,
1065
+ nextUrl,
1066
+ routeKey,
1067
+ deps,
1068
+ actionContext,
1069
+ undefined,
1070
+ stale,
1071
+ );
1072
+ segments.push(...loaderResult.segments);
1073
+ matchedIds.push(...loaderResult.matchedIds);
1074
+ resolvedParallelEntries.add(parallelEntry.id);
1075
+ }
1021
1076
 
1022
1077
  const slots = parallelEntry.handler as Record<
1023
1078
  `@${string}`,
1024
1079
  | ((ctx: HandlerContext<any, TEnv>) => ReactNode | Promise<ReactNode>)
1025
1080
  | ReactNode
1026
1081
  >;
1082
+ // Handler may be undefined in production after static handler eviction.
1083
+ const handler = slots[slot];
1027
1084
 
1028
- for (const [slot, handler] of Object.entries(slots)) {
1029
- // Use orphan.shortCode (the parent layout) to match the SSR path
1030
- // (resolveParallelEntry receives parentShortCode = orphan.shortCode).
1031
- // Using parallelEntry.shortCode would generate IDs the client doesn't know about.
1032
- const parallelId = `${orphan.shortCode}.${slot}`;
1033
- matchedIds.push(parallelId);
1085
+ // Use orphan.shortCode (the parent layout) to match the SSR path
1086
+ // (resolveParallelEntry receives parentShortCode = orphan.shortCode).
1087
+ // Using parallelEntry.shortCode would generate IDs the client doesn't know about.
1088
+ const parallelId = `${orphan.shortCode}.${slot}`;
1089
+ matchedIds.push(parallelId);
1034
1090
 
1035
- const shouldResolve = await (async () => {
1036
- if (!clientSegmentIds.has(parallelId)) {
1037
- if (isTraceActive()) {
1038
- pushRevalidationTraceEntry({
1039
- segmentId: parallelId,
1040
- segmentType: "parallel",
1041
- belongsToRoute,
1042
- source: "parallel",
1043
- defaultShouldRevalidate: true,
1044
- finalShouldRevalidate: true,
1045
- reason: "new-segment",
1046
- });
1047
- }
1048
- return true;
1049
- }
1050
-
1051
- const dummySegment: ResolvedSegment = {
1052
- id: parallelId,
1053
- namespace: parallelEntry.id,
1054
- type: "parallel",
1055
- index: 0,
1056
- component: null as any,
1057
- params,
1058
- slot,
1059
- belongsToRoute,
1060
- parallelName: `${parallelEntry.id}.${slot}`,
1061
- ...(parallelEntry.mountPath
1062
- ? { mountPath: parallelEntry.mountPath }
1063
- : {}),
1064
- };
1065
-
1066
- return await evaluateRevalidation({
1067
- segment: dummySegment,
1068
- prevParams,
1069
- getPrevSegment: null,
1070
- request,
1071
- prevUrl,
1072
- nextUrl,
1073
- revalidations: parallelEntry.revalidate.map((fn, i) => ({
1074
- name: `revalidate${i}`,
1075
- fn,
1076
- })),
1077
- routeKey,
1078
- context,
1079
- actionContext,
1080
- stale,
1081
- traceSource: "parallel",
1082
- });
1083
- })();
1084
- emitRevalidationDecision(
1085
- parallelId,
1086
- context.pathname,
1087
- routeKey,
1088
- shouldResolve,
1089
- );
1090
-
1091
- let component: ReactNode | undefined;
1092
- if (shouldResolve) {
1093
- component = await tryStaticSlot(parallelEntry, slot, parallelId);
1094
- }
1095
- if (component === undefined) {
1096
- const hasLoadingFallback =
1097
- parallelEntry.loading !== undefined &&
1098
- parallelEntry.loading !== false;
1099
- if (!shouldResolve) {
1100
- component = null;
1101
- } else if (hasLoadingFallback) {
1102
- const result =
1103
- typeof handler === "function" ? handler(context) : handler;
1104
- if (result instanceof Promise) {
1105
- const tracked = deps.trackHandler(result, {
1106
- segmentId: parallelId,
1107
- segmentType: "parallel",
1108
- });
1109
- observeStreamedHandler(
1110
- tracked,
1111
- parallelId,
1112
- "parallel",
1113
- context.pathname,
1114
- routeKey,
1115
- params,
1116
- );
1117
- component = tracked as ReactNode;
1118
- } else {
1119
- component = result as ReactNode;
1120
- }
1121
- } else {
1122
- component =
1123
- typeof handler === "function" ? await handler(context) : handler;
1091
+ const shouldResolve = await (async () => {
1092
+ if (!clientSegmentIds.has(parallelId)) {
1093
+ if (isTraceActive()) {
1094
+ pushRevalidationTraceEntry({
1095
+ segmentId: parallelId,
1096
+ segmentType: "parallel",
1097
+ belongsToRoute,
1098
+ source: "parallel",
1099
+ defaultShouldRevalidate: true,
1100
+ finalShouldRevalidate: true,
1101
+ reason: "new-segment",
1102
+ });
1124
1103
  }
1104
+ return true;
1125
1105
  }
1126
1106
 
1127
- segments.push({
1107
+ const dummySegment: ResolvedSegment = {
1128
1108
  id: parallelId,
1129
1109
  namespace: parallelEntry.id,
1130
1110
  type: "parallel",
1131
1111
  index: 0,
1132
- component,
1133
- loading: parallelEntry.loading === false ? null : parallelEntry.loading,
1134
- transition: parallelEntry.transition,
1112
+ component: null as any,
1135
1113
  params,
1136
1114
  slot,
1137
1115
  belongsToRoute,
@@ -1139,8 +1117,87 @@ export async function resolveOrphanLayoutWithRevalidation<TEnv>(
1139
1117
  ...(parallelEntry.mountPath
1140
1118
  ? { mountPath: parallelEntry.mountPath }
1141
1119
  : {}),
1120
+ };
1121
+
1122
+ return await evaluateRevalidation({
1123
+ segment: dummySegment,
1124
+ prevParams,
1125
+ getPrevSegment: null,
1126
+ request,
1127
+ prevUrl,
1128
+ nextUrl,
1129
+ revalidations: parallelEntry.revalidate.map((fn, i) => ({
1130
+ name: `revalidate${i}`,
1131
+ fn,
1132
+ })),
1133
+ routeKey,
1134
+ context,
1135
+ actionContext,
1136
+ stale,
1137
+ traceSource: "parallel",
1142
1138
  });
1139
+ })();
1140
+ emitRevalidationDecision(
1141
+ parallelId,
1142
+ context.pathname,
1143
+ routeKey,
1144
+ shouldResolve,
1145
+ );
1146
+
1147
+ let component: ReactNode | undefined;
1148
+ if (shouldResolve) {
1149
+ component = await tryStaticSlot(parallelEntry, slot, parallelId);
1150
+ }
1151
+ if (component === undefined) {
1152
+ const hasLoadingFallback =
1153
+ parallelEntry.loading !== undefined && parallelEntry.loading !== false;
1154
+ if (!shouldResolve) {
1155
+ component = null;
1156
+ } else if (handler === undefined) {
1157
+ // Handler evicted (production static slot) but static lookup missed.
1158
+ component = null;
1159
+ } else if (hasLoadingFallback) {
1160
+ const result =
1161
+ typeof handler === "function" ? handler(context) : handler;
1162
+ if (result instanceof Promise) {
1163
+ const tracked = deps.trackHandler(result, {
1164
+ segmentId: parallelId,
1165
+ segmentType: "parallel",
1166
+ });
1167
+ observeStreamedHandler(
1168
+ tracked,
1169
+ parallelId,
1170
+ "parallel",
1171
+ context.pathname,
1172
+ routeKey,
1173
+ params,
1174
+ );
1175
+ component = tracked as ReactNode;
1176
+ } else {
1177
+ component = result as ReactNode;
1178
+ }
1179
+ } else {
1180
+ component =
1181
+ typeof handler === "function" ? await handler(context) : handler;
1182
+ }
1143
1183
  }
1184
+
1185
+ segments.push({
1186
+ id: parallelId,
1187
+ namespace: parallelEntry.id,
1188
+ type: "parallel",
1189
+ index: 0,
1190
+ component,
1191
+ loading: parallelEntry.loading === false ? null : parallelEntry.loading,
1192
+ transition: parallelEntry.transition,
1193
+ params,
1194
+ slot,
1195
+ belongsToRoute,
1196
+ parallelName: `${parallelEntry.id}.${slot}`,
1197
+ ...(parallelEntry.mountPath
1198
+ ? { mountPath: parallelEntry.mountPath }
1199
+ : {}),
1200
+ });
1144
1201
  }
1145
1202
 
1146
1203
  return { segments, matchedIds };
@@ -1165,6 +1222,7 @@ export async function resolveAllSegmentsWithRevalidation<TEnv>(
1165
1222
  localRouteName: string,
1166
1223
  pathname: string,
1167
1224
  deps: SegmentResolutionDeps<TEnv>,
1225
+ stale?: boolean,
1168
1226
  ): Promise<{ segments: ResolvedSegment[]; matchedIds: string[] }> {
1169
1227
  const allSegments: ResolvedSegment[] = [];
1170
1228
  const matchedIds: string[] = [];
@@ -1191,6 +1249,10 @@ export async function resolveAllSegmentsWithRevalidation<TEnv>(
1191
1249
  }
1192
1250
 
1193
1251
  const nonParallelEntry = entry as Exclude<EntryData, { type: "parallel" }>;
1252
+ if (entry.type === "cache") {
1253
+ const store = RSCRouterContext.getStore();
1254
+ if (store) store.insideCacheScope = true;
1255
+ }
1194
1256
  const doneEntry = track(`segment:${entry.id}`, 1);
1195
1257
  const resolved = await resolveWithErrorBoundary(
1196
1258
  nonParallelEntry,
@@ -1209,7 +1271,7 @@ export async function resolveAllSegmentsWithRevalidation<TEnv>(
1209
1271
  loaderPromises,
1210
1272
  deps,
1211
1273
  actionContext,
1212
- false,
1274
+ stale,
1213
1275
  ),
1214
1276
  (seg) => ({ segments: [seg], matchedIds: [seg.id] }),
1215
1277
  deps,