@rangojs/router 0.0.0-experimental.52ff0316 → 0.0.0-experimental.54a3dc6a

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 (56) hide show
  1. package/dist/bin/rango.js +8 -3
  2. package/dist/vite/index.js +139 -200
  3. package/package.json +1 -1
  4. package/skills/caching/SKILL.md +37 -4
  5. package/skills/parallel/SKILL.md +59 -0
  6. package/src/browser/event-controller.ts +5 -0
  7. package/src/browser/navigation-bridge.ts +1 -7
  8. package/src/browser/navigation-client.ts +60 -27
  9. package/src/browser/navigation-transaction.ts +11 -9
  10. package/src/browser/partial-update.ts +39 -9
  11. package/src/browser/prefetch/cache.ts +57 -5
  12. package/src/browser/prefetch/fetch.ts +30 -21
  13. package/src/browser/prefetch/queue.ts +53 -13
  14. package/src/browser/react/Link.tsx +9 -1
  15. package/src/browser/react/NavigationProvider.tsx +27 -0
  16. package/src/browser/rsc-router.tsx +109 -57
  17. package/src/browser/scroll-restoration.ts +20 -11
  18. package/src/browser/segment-reconciler.ts +6 -1
  19. package/src/browser/types.ts +9 -0
  20. package/src/build/route-types/router-processing.ts +12 -2
  21. package/src/cache/cache-scope.ts +2 -2
  22. package/src/cache/cf/cf-cache-store.ts +453 -11
  23. package/src/cache/cf/index.ts +5 -1
  24. package/src/cache/document-cache.ts +17 -7
  25. package/src/cache/index.ts +1 -0
  26. package/src/debug.ts +2 -2
  27. package/src/route-definition/dsl-helpers.ts +32 -7
  28. package/src/route-definition/redirect.ts +2 -2
  29. package/src/router/lazy-includes.ts +2 -1
  30. package/src/router/logging.ts +1 -1
  31. package/src/router/manifest.ts +6 -2
  32. package/src/router/match-middleware/background-revalidation.ts +18 -1
  33. package/src/router/match-middleware/cache-lookup.ts +32 -9
  34. package/src/router/match-middleware/cache-store.ts +32 -6
  35. package/src/router/match-middleware/intercept-resolution.ts +9 -7
  36. package/src/router/match-middleware/segment-resolution.ts +7 -5
  37. package/src/router/match-result.ts +11 -1
  38. package/src/router/metrics.ts +6 -1
  39. package/src/router/middleware.ts +2 -1
  40. package/src/router/segment-resolution/fresh.ts +121 -14
  41. package/src/router/segment-resolution/loader-cache.ts +1 -0
  42. package/src/router/segment-resolution/revalidation.ts +339 -287
  43. package/src/router.ts +1 -1
  44. package/src/segment-system.tsx +140 -4
  45. package/src/server/context.ts +90 -13
  46. package/src/server/request-context.ts +10 -4
  47. package/src/ssr/index.tsx +1 -0
  48. package/src/types/segments.ts +2 -0
  49. package/src/urls/path-helper.ts +1 -1
  50. package/src/vite/discovery/state.ts +0 -2
  51. package/src/vite/plugin-types.ts +0 -83
  52. package/src/vite/plugins/expose-action-id.ts +1 -3
  53. package/src/vite/plugins/version-plugin.ts +13 -1
  54. package/src/vite/rango.ts +144 -209
  55. package/src/vite/router-discovery.ts +0 -8
  56. 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 };
@@ -997,143 +1041,72 @@ export async function resolveOrphanLayoutWithRevalidation<TEnv>(
997
1041
  ...(orphan.mountPath ? { mountPath: orphan.mountPath } : {}),
998
1042
  });
999
1043
 
1000
- for (const parallelEntry of orphan.parallel) {
1044
+ const resolvedParallelEntries = new Set<string>();
1045
+ for (const { slot, entry: parallelEntry } of getParallelSlotEntries(
1046
+ orphan.parallel,
1047
+ )) {
1001
1048
  invariant(
1002
1049
  parallelEntry.type === "parallel",
1003
1050
  `Expected parallel entry, got: ${parallelEntry.type}`,
1004
1051
  );
1005
1052
 
1006
- const loaderResult = await resolveLoadersWithRevalidation(
1007
- parallelEntry,
1008
- context,
1009
- belongsToRoute,
1010
- clientSegmentIds,
1011
- prevParams,
1012
- request,
1013
- prevUrl,
1014
- nextUrl,
1015
- routeKey,
1016
- deps,
1017
- actionContext,
1018
- undefined,
1019
- stale,
1020
- );
1021
- segments.push(...loaderResult.segments);
1022
- matchedIds.push(...loaderResult.matchedIds);
1053
+ if (!resolvedParallelEntries.has(parallelEntry.id)) {
1054
+ const loaderResult = await resolveLoadersWithRevalidation(
1055
+ parallelEntry,
1056
+ context,
1057
+ belongsToRoute,
1058
+ clientSegmentIds,
1059
+ prevParams,
1060
+ request,
1061
+ prevUrl,
1062
+ nextUrl,
1063
+ routeKey,
1064
+ deps,
1065
+ actionContext,
1066
+ undefined,
1067
+ stale,
1068
+ );
1069
+ segments.push(...loaderResult.segments);
1070
+ matchedIds.push(...loaderResult.matchedIds);
1071
+ resolvedParallelEntries.add(parallelEntry.id);
1072
+ }
1023
1073
 
1024
1074
  const slots = parallelEntry.handler as Record<
1025
1075
  `@${string}`,
1026
1076
  | ((ctx: HandlerContext<any, TEnv>) => ReactNode | Promise<ReactNode>)
1027
1077
  | ReactNode
1028
1078
  >;
1079
+ // Handler may be undefined in production after static handler eviction.
1080
+ const handler = slots[slot];
1029
1081
 
1030
- for (const [slot, handler] of Object.entries(slots)) {
1031
- // Use orphan.shortCode (the parent layout) to match the SSR path
1032
- // (resolveParallelEntry receives parentShortCode = orphan.shortCode).
1033
- // Using parallelEntry.shortCode would generate IDs the client doesn't know about.
1034
- const parallelId = `${orphan.shortCode}.${slot}`;
1035
- matchedIds.push(parallelId);
1036
-
1037
- const shouldResolve = await (async () => {
1038
- if (!clientSegmentIds.has(parallelId)) {
1039
- if (isTraceActive()) {
1040
- pushRevalidationTraceEntry({
1041
- segmentId: parallelId,
1042
- segmentType: "parallel",
1043
- belongsToRoute,
1044
- source: "parallel",
1045
- defaultShouldRevalidate: true,
1046
- finalShouldRevalidate: true,
1047
- reason: "new-segment",
1048
- });
1049
- }
1050
- return true;
1051
- }
1052
-
1053
- const dummySegment: ResolvedSegment = {
1054
- id: parallelId,
1055
- namespace: parallelEntry.id,
1056
- type: "parallel",
1057
- index: 0,
1058
- component: null as any,
1059
- params,
1060
- slot,
1061
- belongsToRoute,
1062
- parallelName: `${parallelEntry.id}.${slot}`,
1063
- ...(parallelEntry.mountPath
1064
- ? { mountPath: parallelEntry.mountPath }
1065
- : {}),
1066
- };
1067
-
1068
- return await evaluateRevalidation({
1069
- segment: dummySegment,
1070
- prevParams,
1071
- getPrevSegment: null,
1072
- request,
1073
- prevUrl,
1074
- nextUrl,
1075
- revalidations: parallelEntry.revalidate.map((fn, i) => ({
1076
- name: `revalidate${i}`,
1077
- fn,
1078
- })),
1079
- routeKey,
1080
- context,
1081
- actionContext,
1082
- stale,
1083
- traceSource: "parallel",
1084
- });
1085
- })();
1086
- emitRevalidationDecision(
1087
- parallelId,
1088
- context.pathname,
1089
- routeKey,
1090
- shouldResolve,
1091
- );
1082
+ // Use orphan.shortCode (the parent layout) to match the SSR path
1083
+ // (resolveParallelEntry receives parentShortCode = orphan.shortCode).
1084
+ // Using parallelEntry.shortCode would generate IDs the client doesn't know about.
1085
+ const parallelId = `${orphan.shortCode}.${slot}`;
1086
+ matchedIds.push(parallelId);
1092
1087
 
1093
- let component: ReactNode | undefined;
1094
- if (shouldResolve) {
1095
- component = await tryStaticSlot(parallelEntry, slot, parallelId);
1096
- }
1097
- if (component === undefined) {
1098
- const hasLoadingFallback =
1099
- parallelEntry.loading !== undefined &&
1100
- parallelEntry.loading !== false;
1101
- if (!shouldResolve) {
1102
- component = null;
1103
- } else if (hasLoadingFallback) {
1104
- const result =
1105
- typeof handler === "function" ? handler(context) : handler;
1106
- if (result instanceof Promise) {
1107
- const tracked = deps.trackHandler(result, {
1108
- segmentId: parallelId,
1109
- segmentType: "parallel",
1110
- });
1111
- observeStreamedHandler(
1112
- tracked,
1113
- parallelId,
1114
- "parallel",
1115
- context.pathname,
1116
- routeKey,
1117
- params,
1118
- );
1119
- component = tracked as ReactNode;
1120
- } else {
1121
- component = result as ReactNode;
1122
- }
1123
- } else {
1124
- component =
1125
- typeof handler === "function" ? await handler(context) : handler;
1088
+ const shouldResolve = await (async () => {
1089
+ if (!clientSegmentIds.has(parallelId)) {
1090
+ if (isTraceActive()) {
1091
+ pushRevalidationTraceEntry({
1092
+ segmentId: parallelId,
1093
+ segmentType: "parallel",
1094
+ belongsToRoute,
1095
+ source: "parallel",
1096
+ defaultShouldRevalidate: true,
1097
+ finalShouldRevalidate: true,
1098
+ reason: "new-segment",
1099
+ });
1126
1100
  }
1101
+ return true;
1127
1102
  }
1128
1103
 
1129
- segments.push({
1104
+ const dummySegment: ResolvedSegment = {
1130
1105
  id: parallelId,
1131
1106
  namespace: parallelEntry.id,
1132
1107
  type: "parallel",
1133
1108
  index: 0,
1134
- component,
1135
- loading: parallelEntry.loading === false ? null : parallelEntry.loading,
1136
- transition: parallelEntry.transition,
1109
+ component: null as any,
1137
1110
  params,
1138
1111
  slot,
1139
1112
  belongsToRoute,
@@ -1141,8 +1114,87 @@ export async function resolveOrphanLayoutWithRevalidation<TEnv>(
1141
1114
  ...(parallelEntry.mountPath
1142
1115
  ? { mountPath: parallelEntry.mountPath }
1143
1116
  : {}),
1117
+ };
1118
+
1119
+ return await evaluateRevalidation({
1120
+ segment: dummySegment,
1121
+ prevParams,
1122
+ getPrevSegment: null,
1123
+ request,
1124
+ prevUrl,
1125
+ nextUrl,
1126
+ revalidations: parallelEntry.revalidate.map((fn, i) => ({
1127
+ name: `revalidate${i}`,
1128
+ fn,
1129
+ })),
1130
+ routeKey,
1131
+ context,
1132
+ actionContext,
1133
+ stale,
1134
+ traceSource: "parallel",
1144
1135
  });
1136
+ })();
1137
+ emitRevalidationDecision(
1138
+ parallelId,
1139
+ context.pathname,
1140
+ routeKey,
1141
+ shouldResolve,
1142
+ );
1143
+
1144
+ let component: ReactNode | undefined;
1145
+ if (shouldResolve) {
1146
+ component = await tryStaticSlot(parallelEntry, slot, parallelId);
1147
+ }
1148
+ if (component === undefined) {
1149
+ const hasLoadingFallback =
1150
+ parallelEntry.loading !== undefined && parallelEntry.loading !== false;
1151
+ if (!shouldResolve) {
1152
+ component = null;
1153
+ } else if (handler === undefined) {
1154
+ // Handler evicted (production static slot) but static lookup missed.
1155
+ component = null;
1156
+ } else if (hasLoadingFallback) {
1157
+ const result =
1158
+ typeof handler === "function" ? handler(context) : handler;
1159
+ if (result instanceof Promise) {
1160
+ const tracked = deps.trackHandler(result, {
1161
+ segmentId: parallelId,
1162
+ segmentType: "parallel",
1163
+ });
1164
+ observeStreamedHandler(
1165
+ tracked,
1166
+ parallelId,
1167
+ "parallel",
1168
+ context.pathname,
1169
+ routeKey,
1170
+ params,
1171
+ );
1172
+ component = tracked as ReactNode;
1173
+ } else {
1174
+ component = result as ReactNode;
1175
+ }
1176
+ } else {
1177
+ component =
1178
+ typeof handler === "function" ? await handler(context) : handler;
1179
+ }
1145
1180
  }
1181
+
1182
+ segments.push({
1183
+ id: parallelId,
1184
+ namespace: parallelEntry.id,
1185
+ type: "parallel",
1186
+ index: 0,
1187
+ component,
1188
+ loading: parallelEntry.loading === false ? null : parallelEntry.loading,
1189
+ transition: parallelEntry.transition,
1190
+ params,
1191
+ slot,
1192
+ belongsToRoute,
1193
+ parallelName: `${parallelEntry.id}.${slot}`,
1194
+ ...(parallelEntry.mountPath
1195
+ ? { mountPath: parallelEntry.mountPath }
1196
+ : {}),
1197
+ });
1146
1198
  }
1147
1199
 
1148
1200
  return { segments, matchedIds };