@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.
- package/AGENTS.md +4 -0
- package/dist/bin/rango.js +8 -3
- package/dist/vite/index.js +139 -200
- package/package.json +15 -14
- package/skills/caching/SKILL.md +37 -4
- package/skills/parallel/SKILL.md +126 -0
- package/src/browser/event-controller.ts +5 -0
- package/src/browser/navigation-bridge.ts +1 -3
- package/src/browser/navigation-client.ts +60 -27
- package/src/browser/navigation-transaction.ts +11 -9
- package/src/browser/partial-update.ts +50 -9
- package/src/browser/prefetch/cache.ts +57 -5
- package/src/browser/prefetch/fetch.ts +30 -21
- package/src/browser/prefetch/queue.ts +53 -13
- package/src/browser/react/Link.tsx +9 -1
- package/src/browser/react/NavigationProvider.tsx +27 -0
- package/src/browser/rsc-router.tsx +109 -57
- package/src/browser/scroll-restoration.ts +31 -34
- package/src/browser/segment-reconciler.ts +6 -1
- package/src/browser/types.ts +9 -0
- package/src/build/route-types/router-processing.ts +12 -2
- package/src/cache/cache-runtime.ts +15 -11
- package/src/cache/cache-scope.ts +43 -3
- package/src/cache/cf/cf-cache-store.ts +453 -11
- package/src/cache/cf/index.ts +5 -1
- package/src/cache/document-cache.ts +17 -7
- package/src/cache/index.ts +1 -0
- package/src/debug.ts +2 -2
- package/src/route-definition/dsl-helpers.ts +32 -7
- package/src/route-definition/redirect.ts +2 -2
- package/src/route-map-builder.ts +7 -1
- package/src/router/find-match.ts +4 -2
- package/src/router/intercept-resolution.ts +2 -0
- package/src/router/lazy-includes.ts +4 -1
- package/src/router/logging.ts +5 -2
- package/src/router/manifest.ts +9 -3
- package/src/router/match-middleware/background-revalidation.ts +30 -2
- package/src/router/match-middleware/cache-lookup.ts +66 -9
- package/src/router/match-middleware/cache-store.ts +53 -10
- package/src/router/match-middleware/intercept-resolution.ts +9 -7
- package/src/router/match-middleware/segment-resolution.ts +8 -5
- package/src/router/match-result.ts +22 -6
- package/src/router/metrics.ts +6 -1
- package/src/router/middleware.ts +2 -1
- package/src/router/router-context.ts +6 -1
- package/src/router/segment-resolution/fresh.ts +122 -15
- package/src/router/segment-resolution/loader-cache.ts +1 -0
- package/src/router/segment-resolution/revalidation.ts +347 -290
- package/src/router/segment-wrappers.ts +2 -0
- package/src/router.ts +5 -1
- package/src/segment-system.tsx +140 -4
- package/src/server/context.ts +90 -13
- package/src/server/request-context.ts +10 -4
- package/src/ssr/index.tsx +1 -0
- package/src/types/handler-context.ts +103 -17
- package/src/types/route-entry.ts +7 -0
- package/src/types/segments.ts +2 -0
- package/src/urls/path-helper.ts +1 -1
- package/src/vite/discovery/state.ts +0 -2
- package/src/vite/plugin-types.ts +0 -83
- package/src/vite/plugins/expose-action-id.ts +1 -3
- package/src/vite/plugins/version-plugin.ts +13 -1
- package/src/vite/rango.ts +144 -209
- package/src/vite/router-discovery.ts +0 -8
- 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
|
|
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
|
-
|
|
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
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
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
|
-
|
|
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
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
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 (
|
|
458
|
-
const
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
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
|
-
|
|
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
|
-
|
|
508
|
-
|
|
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
|
-
|
|
479
|
+
context,
|
|
519
480
|
actionContext,
|
|
520
|
-
entry.shortCode,
|
|
521
481
|
stale,
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
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
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
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
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1269
|
+
stale,
|
|
1213
1270
|
),
|
|
1214
1271
|
(seg) => ({ segments: [seg], matchedIds: [seg.id] }),
|
|
1215
1272
|
deps,
|