@rangojs/router 0.0.0-experimental.cb54cbba → 0.0.0-experimental.debug-cache-2383ca26

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