@rangojs/router 0.0.0-experimental.93 → 0.0.0-experimental.95
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/dist/vite/index.js +1 -1
- package/package.json +1 -1
- package/src/browser/event-controller.ts +44 -4
- package/src/browser/react/NavigationProvider.tsx +20 -7
- package/src/browser/react/filter-segment-order.ts +51 -7
- package/src/browser/react/use-segments.ts +11 -8
- package/src/browser/types.ts +6 -0
- package/src/router/match-api.ts +1 -0
- package/src/router/match-handlers.ts +1 -0
- package/src/router/match-result.ts +21 -2
- package/src/router/segment-resolution/fresh.ts +8 -0
- package/src/router/segment-resolution/revalidation.ts +65 -42
- package/src/rsc/progressive-enhancement.ts +2 -0
- package/src/rsc/rsc-rendering.ts +3 -0
- package/src/rsc/server-action.ts +2 -0
- package/src/rsc/types.ts +6 -0
- package/src/ssr/index.tsx +5 -1
- package/src/types/segments.ts +17 -0
package/dist/vite/index.js
CHANGED
|
@@ -2040,7 +2040,7 @@ import { resolve } from "node:path";
|
|
|
2040
2040
|
// package.json
|
|
2041
2041
|
var package_default = {
|
|
2042
2042
|
name: "@rangojs/router",
|
|
2043
|
-
version: "0.0.0-experimental.
|
|
2043
|
+
version: "0.0.0-experimental.95",
|
|
2044
2044
|
description: "Django-inspired RSC router with composable URL patterns",
|
|
2045
2045
|
keywords: [
|
|
2046
2046
|
"react",
|
package/package.json
CHANGED
|
@@ -113,11 +113,24 @@ export type ActionStateListener = (state: TrackedActionState) => void;
|
|
|
113
113
|
export type HandleListener = () => void;
|
|
114
114
|
|
|
115
115
|
/**
|
|
116
|
-
* Internal handle state stored in controller
|
|
116
|
+
* Internal handle state stored in controller.
|
|
117
|
+
*
|
|
118
|
+
* Two segment lists are exposed because they serve different consumers:
|
|
119
|
+
*
|
|
120
|
+
* - `segmentOrder` drives handle collection (collectHandleData). Includes
|
|
121
|
+
* parallel slot ids and reorders them after their parent so later-wins
|
|
122
|
+
* collect functions (e.g. Meta) get the right precedence.
|
|
123
|
+
* - `routeSegmentIds` is the layouts-and-routes-only list documented by
|
|
124
|
+
* `useSegments().segmentIds`. Parallels and loader sub-ids are stripped;
|
|
125
|
+
* raw matched order is preserved.
|
|
126
|
+
*
|
|
127
|
+
* Both are derived from the same `matched` input on each setHandleData call
|
|
128
|
+
* so they stay in sync.
|
|
117
129
|
*/
|
|
118
130
|
export interface HandleState {
|
|
119
131
|
data: HandleData;
|
|
120
132
|
segmentOrder: string[];
|
|
133
|
+
routeSegmentIds: string[];
|
|
121
134
|
}
|
|
122
135
|
|
|
123
136
|
/**
|
|
@@ -202,6 +215,14 @@ export interface EventController {
|
|
|
202
215
|
data: HandleData,
|
|
203
216
|
matched?: string[],
|
|
204
217
|
isPartial?: boolean,
|
|
218
|
+
/**
|
|
219
|
+
* Segment ids that were re-resolved on the server this request (the
|
|
220
|
+
* partial response's `diff`). On a partial update, any existing bucket
|
|
221
|
+
* keyed under one of these ids that has no incoming entry is treated as
|
|
222
|
+
* stale and cleared. Without this, a parallel slot that revalidates but
|
|
223
|
+
* pushes nothing leaves its previous bucket in place forever.
|
|
224
|
+
*/
|
|
225
|
+
resolvedIds?: string[],
|
|
205
226
|
): void;
|
|
206
227
|
getHandleState(): HandleState;
|
|
207
228
|
|
|
@@ -300,6 +321,7 @@ export function createEventController(
|
|
|
300
321
|
// Handle data from RSC payload
|
|
301
322
|
let handleData: HandleData = {};
|
|
302
323
|
let handleSegmentOrder: string[] = [];
|
|
324
|
+
let routeSegmentIds: string[] = [];
|
|
303
325
|
|
|
304
326
|
// Merged route params from current match
|
|
305
327
|
let routeParams: Record<string, string> = {};
|
|
@@ -744,8 +766,15 @@ export function createEventController(
|
|
|
744
766
|
data: HandleData,
|
|
745
767
|
matched?: string[],
|
|
746
768
|
isPartial?: boolean,
|
|
769
|
+
resolvedIds?: string[],
|
|
747
770
|
): void {
|
|
748
|
-
const
|
|
771
|
+
const rawMatched = matched ?? [];
|
|
772
|
+
const newSegmentOrder = filterSegmentOrder(rawMatched);
|
|
773
|
+
// Separate list for useSegments(): "layouts and routes only" — strip
|
|
774
|
+
// parallels (".@") and loader sub-ids (D digit) without reordering.
|
|
775
|
+
const newRouteSegmentIds = rawMatched.filter(
|
|
776
|
+
(id) => !id.includes(".@") && !/D\d+\./.test(id),
|
|
777
|
+
);
|
|
749
778
|
|
|
750
779
|
if (isPartial && newSegmentOrder.length > 0) {
|
|
751
780
|
// Partial update: merge new data with existing
|
|
@@ -757,10 +786,19 @@ export function createEventController(
|
|
|
757
786
|
handleData[handleName][segmentId] = data[handleName][segmentId];
|
|
758
787
|
}
|
|
759
788
|
}
|
|
760
|
-
|
|
789
|
+
const resolvedIdSet =
|
|
790
|
+
resolvedIds && resolvedIds.length > 0 ? new Set(resolvedIds) : null;
|
|
791
|
+
// Cleanup pass:
|
|
792
|
+
// a) segment dropped from the match list — delete its bucket.
|
|
793
|
+
// b) segment was re-resolved this request but pushed nothing for
|
|
794
|
+
// this handle — its previous bucket is stale.
|
|
795
|
+
// (a) is the existing behavior; (b) requires resolvedIds.
|
|
761
796
|
for (const handleName of Object.keys(handleData)) {
|
|
762
797
|
for (const segmentId of Object.keys(handleData[handleName])) {
|
|
763
|
-
|
|
798
|
+
const droppedFromMatch = !newSegmentOrder.includes(segmentId);
|
|
799
|
+
const reresolvedWithoutPush =
|
|
800
|
+
resolvedIdSet?.has(segmentId) && !data[handleName]?.[segmentId];
|
|
801
|
+
if (droppedFromMatch || reresolvedWithoutPush) {
|
|
764
802
|
delete handleData[handleName][segmentId];
|
|
765
803
|
}
|
|
766
804
|
}
|
|
@@ -770,6 +808,7 @@ export function createEventController(
|
|
|
770
808
|
handleData = data;
|
|
771
809
|
}
|
|
772
810
|
handleSegmentOrder = newSegmentOrder;
|
|
811
|
+
routeSegmentIds = newRouteSegmentIds;
|
|
773
812
|
|
|
774
813
|
notifyHandles();
|
|
775
814
|
}
|
|
@@ -778,6 +817,7 @@ export function createEventController(
|
|
|
778
817
|
return {
|
|
779
818
|
data: handleData,
|
|
780
819
|
segmentOrder: handleSegmentOrder,
|
|
820
|
+
routeSegmentIds,
|
|
781
821
|
};
|
|
782
822
|
}
|
|
783
823
|
|
|
@@ -47,10 +47,22 @@ async function processHandles(
|
|
|
47
47
|
store: NavigationStore;
|
|
48
48
|
matched?: string[];
|
|
49
49
|
isPartial?: boolean;
|
|
50
|
+
/** Server's `resolvedIds`: every segment re-resolved this request,
|
|
51
|
+
* including null-component ones excluded from `diff`/`segments`.
|
|
52
|
+
* Drives cleanup of stale handle buckets when a re-resolved segment
|
|
53
|
+
* pushed nothing. */
|
|
54
|
+
resolvedIds?: string[];
|
|
50
55
|
historyKey: string;
|
|
51
56
|
},
|
|
52
57
|
): Promise<void> {
|
|
53
|
-
const {
|
|
58
|
+
const {
|
|
59
|
+
eventController,
|
|
60
|
+
store,
|
|
61
|
+
matched,
|
|
62
|
+
isPartial,
|
|
63
|
+
resolvedIds,
|
|
64
|
+
historyKey,
|
|
65
|
+
} = opts;
|
|
54
66
|
|
|
55
67
|
let yieldCount = 0;
|
|
56
68
|
for await (const handleData of handlesGenerator) {
|
|
@@ -65,7 +77,7 @@ async function processHandles(
|
|
|
65
77
|
}
|
|
66
78
|
|
|
67
79
|
yieldCount++;
|
|
68
|
-
eventController.setHandleData(handleData, matched, isPartial);
|
|
80
|
+
eventController.setHandleData(handleData, matched, isPartial, resolvedIds);
|
|
69
81
|
}
|
|
70
82
|
|
|
71
83
|
// Check again before final updates
|
|
@@ -73,12 +85,11 @@ async function processHandles(
|
|
|
73
85
|
return;
|
|
74
86
|
}
|
|
75
87
|
|
|
76
|
-
// For partial updates where the generator yielded nothing (
|
|
77
|
-
//
|
|
78
|
-
//
|
|
79
|
-
// route might not push any breadcrumbs, but we still need to remove the old ones.
|
|
88
|
+
// For partial updates where the generator yielded nothing (every
|
|
89
|
+
// re-resolved handler pushed nothing), still call setHandleData so the
|
|
90
|
+
// cleanup pass can clear out stale buckets for those segments.
|
|
80
91
|
if (yieldCount === 0 && matched) {
|
|
81
|
-
eventController.setHandleData({}, matched, true);
|
|
92
|
+
eventController.setHandleData({}, matched, true, resolvedIds);
|
|
82
93
|
}
|
|
83
94
|
|
|
84
95
|
// After handles processing completes, update the cache's handleData.
|
|
@@ -394,6 +405,7 @@ export function NavigationProvider({
|
|
|
394
405
|
store,
|
|
395
406
|
matched: update.metadata.matched,
|
|
396
407
|
isPartial: update.metadata.isPartial,
|
|
408
|
+
resolvedIds: update.metadata.resolvedIds,
|
|
397
409
|
historyKey,
|
|
398
410
|
}).catch((err) =>
|
|
399
411
|
console.error("[NavigationProvider] Error consuming handles:", err),
|
|
@@ -412,6 +424,7 @@ export function NavigationProvider({
|
|
|
412
424
|
{}, // Empty data - all existing data not in matched will be cleaned up
|
|
413
425
|
update.metadata.matched,
|
|
414
426
|
true, // partial update - will clean up segments not in matched
|
|
427
|
+
update.metadata.resolvedIds,
|
|
415
428
|
);
|
|
416
429
|
}
|
|
417
430
|
});
|
|
@@ -1,11 +1,55 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
2
|
+
* Build the handle-collection segment order from a raw `matched` list.
|
|
3
|
+
*
|
|
4
|
+
* Two responsibilities:
|
|
5
|
+
*
|
|
6
|
+
* 1. Drop loader sub-ids ("D" followed by a digit, e.g. "M0L0D1.user") —
|
|
7
|
+
* loaders never push handles.
|
|
8
|
+
*
|
|
9
|
+
* 2. Place each parallel slot id (contains ".@") immediately after its
|
|
10
|
+
* parent layout/route id. Raw segment-resolution emission order does NOT
|
|
11
|
+
* guarantee this: route-mounted parallels are resolved/pushed BEFORE the
|
|
12
|
+
* route handler's segment is appended (see fresh.ts:resolveSegment for
|
|
13
|
+
* routes, and revalidation.ts ~915-919), so matched can read
|
|
14
|
+
* `[..., R0.@panel, R0]`. collectHandleData consumes segmentOrder verbatim
|
|
15
|
+
* with later-wins semantics, so without normalization the route handler's
|
|
16
|
+
* Meta would override the slot's more-specific Meta — backwards.
|
|
17
|
+
*
|
|
18
|
+
* Slot-id format is `<parentShortCode>.@<slotName>`; `parentShortCode` never
|
|
19
|
+
* contains ".@", so splitting at the first ".@" reliably yields the parent.
|
|
4
20
|
*/
|
|
5
21
|
export function filterSegmentOrder(matched: string[]): string[] {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
22
|
+
const slotsByParent = new Map<string, string[]>();
|
|
23
|
+
const nonSlots: string[] = [];
|
|
24
|
+
const nonSlotSet = new Set<string>();
|
|
25
|
+
|
|
26
|
+
for (const id of matched) {
|
|
27
|
+
if (/D\d+\./.test(id)) continue;
|
|
28
|
+
const slotIdx = id.indexOf(".@");
|
|
29
|
+
if (slotIdx >= 0) {
|
|
30
|
+
const parent = id.slice(0, slotIdx);
|
|
31
|
+
const list = slotsByParent.get(parent);
|
|
32
|
+
if (list) {
|
|
33
|
+
list.push(id);
|
|
34
|
+
} else {
|
|
35
|
+
slotsByParent.set(parent, [id]);
|
|
36
|
+
}
|
|
37
|
+
} else {
|
|
38
|
+
nonSlots.push(id);
|
|
39
|
+
nonSlotSet.add(id);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const result: string[] = [];
|
|
44
|
+
for (const id of nonSlots) {
|
|
45
|
+
result.push(id);
|
|
46
|
+
const slots = slotsByParent.get(id);
|
|
47
|
+
if (slots) result.push(...slots);
|
|
48
|
+
}
|
|
49
|
+
// Defensive: any slot whose parent is missing from the filtered list still
|
|
50
|
+
// gets included rather than silently dropped. Shouldn't happen in practice.
|
|
51
|
+
for (const [parent, slots] of slotsByParent) {
|
|
52
|
+
if (!nonSlotSet.has(parent)) result.push(...slots);
|
|
53
|
+
}
|
|
54
|
+
return result;
|
|
11
55
|
}
|
|
@@ -25,15 +25,18 @@ function parsePathname(pathname: string): string[] {
|
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
/**
|
|
28
|
-
* Build segments state from event controller
|
|
28
|
+
* Build segments state from event controller. `segmentIds` is the
|
|
29
|
+
* route-only list (parallels and loaders stripped) — distinct from the
|
|
30
|
+
* controller's `segmentOrder` which drives handle collection and includes
|
|
31
|
+
* parallel slot ids.
|
|
29
32
|
*/
|
|
30
33
|
function buildSegmentsState(
|
|
31
34
|
location: URL,
|
|
32
|
-
|
|
35
|
+
routeSegmentIds: string[],
|
|
33
36
|
): SegmentsState {
|
|
34
37
|
return {
|
|
35
38
|
path: parsePathname(location.pathname),
|
|
36
|
-
segmentIds:
|
|
39
|
+
segmentIds: routeSegmentIds,
|
|
37
40
|
location,
|
|
38
41
|
};
|
|
39
42
|
}
|
|
@@ -74,7 +77,7 @@ export function useSegments<T>(
|
|
|
74
77
|
const handleState = ctx.eventController.getHandleState();
|
|
75
78
|
const segmentsState = buildSegmentsState(
|
|
76
79
|
location as URL,
|
|
77
|
-
handleState.
|
|
80
|
+
handleState.routeSegmentIds,
|
|
78
81
|
);
|
|
79
82
|
return selector ? selector(segmentsState) : segmentsState;
|
|
80
83
|
});
|
|
@@ -94,7 +97,7 @@ export function useSegments<T>(
|
|
|
94
97
|
// render-time setState calls.
|
|
95
98
|
const segmentsCache = useRef<{
|
|
96
99
|
location: URL;
|
|
97
|
-
|
|
100
|
+
routeSegmentIds: string[];
|
|
98
101
|
state: SegmentsState;
|
|
99
102
|
} | null>(null);
|
|
100
103
|
|
|
@@ -113,17 +116,17 @@ export function useSegments<T>(
|
|
|
113
116
|
if (
|
|
114
117
|
cache &&
|
|
115
118
|
cache.location === location &&
|
|
116
|
-
cache.
|
|
119
|
+
cache.routeSegmentIds === handleState.routeSegmentIds
|
|
117
120
|
) {
|
|
118
121
|
segmentsState = cache.state;
|
|
119
122
|
} else {
|
|
120
123
|
segmentsState = buildSegmentsState(
|
|
121
124
|
location as URL,
|
|
122
|
-
handleState.
|
|
125
|
+
handleState.routeSegmentIds,
|
|
123
126
|
);
|
|
124
127
|
segmentsCache.current = {
|
|
125
128
|
location: location as URL,
|
|
126
|
-
|
|
129
|
+
routeSegmentIds: handleState.routeSegmentIds,
|
|
127
130
|
state: segmentsState,
|
|
128
131
|
};
|
|
129
132
|
}
|
package/src/browser/types.ts
CHANGED
|
@@ -39,6 +39,12 @@ export interface RscMetadata {
|
|
|
39
39
|
isError?: boolean;
|
|
40
40
|
matched?: string[];
|
|
41
41
|
diff?: string[];
|
|
42
|
+
/**
|
|
43
|
+
* All segment ids re-resolved on the server, including null-component
|
|
44
|
+
* ones excluded from `segments`/`diff`. Drives client-side handle-bucket
|
|
45
|
+
* cleanup. Superset of `diff`. See MatchResult.resolvedIds.
|
|
46
|
+
*/
|
|
47
|
+
resolvedIds?: string[];
|
|
42
48
|
/** Merged route params from the matched route */
|
|
43
49
|
params?: Record<string, string>;
|
|
44
50
|
/**
|
package/src/router/match-api.ts
CHANGED
|
@@ -270,10 +270,29 @@ export function buildMatchResult<TEnv>(
|
|
|
270
270
|
const matchedIds =
|
|
271
271
|
removedIds.size > 0 ? allIds.filter((id) => !removedIds.has(id)) : allIds;
|
|
272
272
|
|
|
273
|
+
// resolvedIds: every segment whose handler actually ran this request.
|
|
274
|
+
// For full-match every segment is fresh; for partial-match we filter by
|
|
275
|
+
// the internal `_handlerRan` flag set in revalidation.ts. Drives the
|
|
276
|
+
// client's handle-bucket cleanup — a slot that re-resolved and pushed
|
|
277
|
+
// nothing must have its previous handle data cleared, but `diff` won't
|
|
278
|
+
// carry it because the segment payload skips null-component cached
|
|
279
|
+
// segments to save bytes.
|
|
280
|
+
const resolvedIds = ctx.isFullMatch
|
|
281
|
+
? allSegments.map((s) => s.id)
|
|
282
|
+
: allSegments.filter((s) => s._handlerRan).map((s) => s.id);
|
|
283
|
+
|
|
284
|
+
// Strip internal-only fields from the segments going on the wire.
|
|
285
|
+
const cleanedSegments = dedupedSegments.map((s) => {
|
|
286
|
+
if (s._handlerRan === undefined) return s;
|
|
287
|
+
const { _handlerRan: _drop, ...rest } = s;
|
|
288
|
+
return rest as ResolvedSegment;
|
|
289
|
+
});
|
|
290
|
+
|
|
273
291
|
return {
|
|
274
|
-
segments:
|
|
292
|
+
segments: cleanedSegments,
|
|
275
293
|
matched: matchedIds,
|
|
276
|
-
diff:
|
|
294
|
+
diff: cleanedSegments.map((s) => s.id),
|
|
295
|
+
resolvedIds,
|
|
277
296
|
params: ctx.matched.params,
|
|
278
297
|
routeName: ctx.routeKey,
|
|
279
298
|
slots: Object.keys(state.slots).length > 0 ? state.slots : undefined,
|
|
@@ -515,6 +515,14 @@ export async function resolveParallelEntry<TEnv>(
|
|
|
515
515
|
if (handler === undefined) {
|
|
516
516
|
continue;
|
|
517
517
|
}
|
|
518
|
+
// Pin `_currentSegmentId` to the slot's own id so handle pushes from
|
|
519
|
+
// inside the slot handler get their own bucket in the HandleStore.
|
|
520
|
+
// Parent-keying would collapse them into the parent layout's bucket;
|
|
521
|
+
// the partial-update merge then replaces the parent's bucket on a
|
|
522
|
+
// slot-only revalidation and drops layout-pushed Meta/Breadcrumbs.
|
|
523
|
+
// filterSegmentOrder() retains slot ids so the client preserves them.
|
|
524
|
+
(context as InternalHandlerContext<any, TEnv>)._currentSegmentId =
|
|
525
|
+
`${parentShortCode}.${slot}`;
|
|
518
526
|
const doneParallelHandler = track(
|
|
519
527
|
`handler:${parallelEntry.id}.${slot}`,
|
|
520
528
|
2,
|
|
@@ -537,8 +537,11 @@ export async function resolveParallelSegmentsWithRevalidation<TEnv>(
|
|
|
537
537
|
);
|
|
538
538
|
|
|
539
539
|
let component: ReactNode | undefined;
|
|
540
|
+
let handlerRan = false;
|
|
540
541
|
if (shouldResolve) {
|
|
541
542
|
component = await tryStaticSlot(parallelEntry, slot, parallelId);
|
|
543
|
+
// tryStaticSlot returning a value means the static cache supplied the
|
|
544
|
+
// component — handler did NOT run. handlerRan stays false.
|
|
542
545
|
}
|
|
543
546
|
if (component === undefined) {
|
|
544
547
|
const hasLoadingFallback =
|
|
@@ -549,29 +552,37 @@ export async function resolveParallelSegmentsWithRevalidation<TEnv>(
|
|
|
549
552
|
// Handler evicted (production static slot) but static lookup missed.
|
|
550
553
|
// Nothing to render — use null so the client keeps its cached version.
|
|
551
554
|
component = null;
|
|
552
|
-
} else
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
555
|
+
} else {
|
|
556
|
+
// Slot-keyed pushes — slot owns its own bucket, parent layout owns
|
|
557
|
+
// its own. On slot-only revalidations the partial merge updates only
|
|
558
|
+
// the slot's bucket; the parent's bucket stays intact.
|
|
559
|
+
(context as InternalHandlerContext<any, TEnv>)._currentSegmentId =
|
|
560
|
+
parallelId;
|
|
561
|
+
handlerRan = true;
|
|
562
|
+
if (hasLoadingFallback) {
|
|
563
|
+
const result =
|
|
564
|
+
typeof handler === "function" ? handler(context) : handler;
|
|
565
|
+
if (result instanceof Promise) {
|
|
566
|
+
const tracked = deps.trackHandler(result, {
|
|
567
|
+
segmentId: parallelId,
|
|
568
|
+
segmentType: "parallel",
|
|
569
|
+
});
|
|
570
|
+
observeStreamedHandler(
|
|
571
|
+
tracked,
|
|
572
|
+
parallelId,
|
|
573
|
+
"parallel",
|
|
574
|
+
context.pathname,
|
|
575
|
+
routeKey,
|
|
576
|
+
params,
|
|
577
|
+
);
|
|
578
|
+
component = tracked as ReactNode;
|
|
579
|
+
} else {
|
|
580
|
+
component = result as ReactNode;
|
|
581
|
+
}
|
|
569
582
|
} else {
|
|
570
|
-
component =
|
|
583
|
+
component =
|
|
584
|
+
typeof handler === "function" ? await handler(context) : handler;
|
|
571
585
|
}
|
|
572
|
-
} else {
|
|
573
|
-
component =
|
|
574
|
-
typeof handler === "function" ? await handler(context) : handler;
|
|
575
586
|
}
|
|
576
587
|
}
|
|
577
588
|
|
|
@@ -585,6 +596,7 @@ export async function resolveParallelSegmentsWithRevalidation<TEnv>(
|
|
|
585
596
|
transition: parallelEntry.transition,
|
|
586
597
|
params,
|
|
587
598
|
slot,
|
|
599
|
+
_handlerRan: handlerRan,
|
|
588
600
|
belongsToRoute,
|
|
589
601
|
parallelName: `${parallelEntry.id}.${slot}`,
|
|
590
602
|
...(parallelEntry.mountPath
|
|
@@ -639,6 +651,7 @@ export async function resolveEntryHandlerWithRevalidation<TEnv>(
|
|
|
639
651
|
): Promise<{ segment: ResolvedSegment; matchedId: string }> {
|
|
640
652
|
const matchedId = entry.shortCode;
|
|
641
653
|
|
|
654
|
+
let handlerRan = false;
|
|
642
655
|
const component = await revalidate(
|
|
643
656
|
async () => {
|
|
644
657
|
const hasSegment = clientSegmentIds.has(entry.shortCode);
|
|
@@ -715,6 +728,7 @@ export async function resolveEntryHandlerWithRevalidation<TEnv>(
|
|
|
715
728
|
return shouldRevalidate;
|
|
716
729
|
},
|
|
717
730
|
async () => {
|
|
731
|
+
handlerRan = true;
|
|
718
732
|
const doneHandler = track(`handler:${entry.id}`, 2);
|
|
719
733
|
(context as InternalHandlerContext<any, TEnv>)._currentSegmentId =
|
|
720
734
|
entry.shortCode;
|
|
@@ -796,6 +810,7 @@ export async function resolveEntryHandlerWithRevalidation<TEnv>(
|
|
|
796
810
|
? { layoutName: entry.id }
|
|
797
811
|
: {}),
|
|
798
812
|
...(entry.mountPath ? { mountPath: entry.mountPath } : {}),
|
|
813
|
+
_handlerRan: handlerRan,
|
|
799
814
|
};
|
|
800
815
|
|
|
801
816
|
return { segment, matchedId };
|
|
@@ -1228,6 +1243,7 @@ export async function resolveOrphanLayoutWithRevalidation<TEnv>(
|
|
|
1228
1243
|
);
|
|
1229
1244
|
|
|
1230
1245
|
let component: ReactNode | undefined;
|
|
1246
|
+
let handlerRan = false;
|
|
1231
1247
|
if (shouldResolve) {
|
|
1232
1248
|
component = await tryStaticSlot(parallelEntry, slot, parallelId);
|
|
1233
1249
|
}
|
|
@@ -1239,29 +1255,35 @@ export async function resolveOrphanLayoutWithRevalidation<TEnv>(
|
|
|
1239
1255
|
} else if (handler === undefined) {
|
|
1240
1256
|
// Handler evicted (production static slot) but static lookup missed.
|
|
1241
1257
|
component = null;
|
|
1242
|
-
} else
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
tracked,
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1258
|
+
} else {
|
|
1259
|
+
// Slot-keyed pushes — see resolveParallelSegmentsWithRevalidation.
|
|
1260
|
+
(context as InternalHandlerContext<any, TEnv>)._currentSegmentId =
|
|
1261
|
+
parallelId;
|
|
1262
|
+
handlerRan = true;
|
|
1263
|
+
if (hasLoadingFallback) {
|
|
1264
|
+
const result =
|
|
1265
|
+
typeof handler === "function" ? handler(context) : handler;
|
|
1266
|
+
if (result instanceof Promise) {
|
|
1267
|
+
const tracked = deps.trackHandler(result, {
|
|
1268
|
+
segmentId: parallelId,
|
|
1269
|
+
segmentType: "parallel",
|
|
1270
|
+
});
|
|
1271
|
+
observeStreamedHandler(
|
|
1272
|
+
tracked,
|
|
1273
|
+
parallelId,
|
|
1274
|
+
"parallel",
|
|
1275
|
+
context.pathname,
|
|
1276
|
+
routeKey,
|
|
1277
|
+
params,
|
|
1278
|
+
);
|
|
1279
|
+
component = tracked as ReactNode;
|
|
1280
|
+
} else {
|
|
1281
|
+
component = result as ReactNode;
|
|
1282
|
+
}
|
|
1259
1283
|
} else {
|
|
1260
|
-
component =
|
|
1284
|
+
component =
|
|
1285
|
+
typeof handler === "function" ? await handler(context) : handler;
|
|
1261
1286
|
}
|
|
1262
|
-
} else {
|
|
1263
|
-
component =
|
|
1264
|
-
typeof handler === "function" ? await handler(context) : handler;
|
|
1265
1287
|
}
|
|
1266
1288
|
}
|
|
1267
1289
|
|
|
@@ -1275,6 +1297,7 @@ export async function resolveOrphanLayoutWithRevalidation<TEnv>(
|
|
|
1275
1297
|
transition: parallelEntry.transition,
|
|
1276
1298
|
params,
|
|
1277
1299
|
slot,
|
|
1300
|
+
_handlerRan: handlerRan,
|
|
1278
1301
|
belongsToRoute,
|
|
1279
1302
|
parallelName: `${parallelEntry.id}.${slot}`,
|
|
1280
1303
|
...(parallelEntry.mountPath
|
|
@@ -248,6 +248,7 @@ export async function handleProgressiveEnhancement<TEnv>(
|
|
|
248
248
|
segments: match.segments,
|
|
249
249
|
matched: match.matched,
|
|
250
250
|
diff: match.diff,
|
|
251
|
+
resolvedIds: match.resolvedIds,
|
|
251
252
|
params: match.params,
|
|
252
253
|
isPartial: false,
|
|
253
254
|
rootLayout: ctx.router.rootLayout,
|
|
@@ -354,6 +355,7 @@ async function renderPeErrorBoundary<TEnv>(
|
|
|
354
355
|
segments: errorResult.segments,
|
|
355
356
|
matched: errorResult.matched,
|
|
356
357
|
diff: errorResult.diff,
|
|
358
|
+
resolvedIds: errorResult.resolvedIds,
|
|
357
359
|
params: errorResult.params,
|
|
358
360
|
isPartial: false,
|
|
359
361
|
isError: true,
|
package/src/rsc/rsc-rendering.ts
CHANGED
|
@@ -59,6 +59,7 @@ export async function handleRscRendering<TEnv>(
|
|
|
59
59
|
segments: match.segments,
|
|
60
60
|
matched: match.matched,
|
|
61
61
|
diff: match.diff,
|
|
62
|
+
resolvedIds: match.resolvedIds,
|
|
62
63
|
params: match.params,
|
|
63
64
|
isPartial: false,
|
|
64
65
|
rootLayout: ctx.router.rootLayout,
|
|
@@ -81,6 +82,7 @@ export async function handleRscRendering<TEnv>(
|
|
|
81
82
|
segments: result.segments,
|
|
82
83
|
matched: result.matched,
|
|
83
84
|
diff: result.diff,
|
|
85
|
+
resolvedIds: result.resolvedIds,
|
|
84
86
|
params: result.params,
|
|
85
87
|
isPartial: true,
|
|
86
88
|
slots: result.slots,
|
|
@@ -144,6 +146,7 @@ export async function handleRscRendering<TEnv>(
|
|
|
144
146
|
segments: match.segments,
|
|
145
147
|
matched: match.matched,
|
|
146
148
|
diff: match.diff,
|
|
149
|
+
resolvedIds: match.resolvedIds,
|
|
147
150
|
params: match.params,
|
|
148
151
|
isPartial: false,
|
|
149
152
|
rootLayout: ctx.router.rootLayout,
|
package/src/rsc/server-action.ts
CHANGED
|
@@ -213,6 +213,7 @@ export async function executeServerAction<TEnv>(
|
|
|
213
213
|
isPartial: true,
|
|
214
214
|
matched: errorResult.matched,
|
|
215
215
|
diff: errorResult.diff,
|
|
216
|
+
resolvedIds: errorResult.resolvedIds,
|
|
216
217
|
params: errorResult.params,
|
|
217
218
|
isError: true,
|
|
218
219
|
handles: handleStore.stream(),
|
|
@@ -324,6 +325,7 @@ export async function revalidateAfterAction<TEnv>(
|
|
|
324
325
|
isPartial: true,
|
|
325
326
|
matched: matchResult.matched,
|
|
326
327
|
diff: matchResult.diff,
|
|
328
|
+
resolvedIds: matchResult.resolvedIds,
|
|
327
329
|
params: matchResult.params,
|
|
328
330
|
slots: matchResult.slots,
|
|
329
331
|
handles: handleStore.stream(),
|
package/src/rsc/types.ts
CHANGED
|
@@ -26,6 +26,12 @@ export interface RscPayload {
|
|
|
26
26
|
isError?: boolean;
|
|
27
27
|
matched?: string[];
|
|
28
28
|
diff?: string[];
|
|
29
|
+
/**
|
|
30
|
+
* All segment ids re-resolved on the server, including null-component
|
|
31
|
+
* ones excluded from `segments`/`diff`. Drives client-side handle-bucket
|
|
32
|
+
* cleanup. Superset of `diff`. See MatchResult.resolvedIds.
|
|
33
|
+
*/
|
|
34
|
+
resolvedIds?: string[];
|
|
29
35
|
/** Merged route params from the matched route */
|
|
30
36
|
params?: Record<string, string>;
|
|
31
37
|
slots?: Record<string, SlotState>;
|
package/src/ssr/index.tsx
CHANGED
|
@@ -162,9 +162,13 @@ function createSsrEventController(opts: {
|
|
|
162
162
|
}): EventController {
|
|
163
163
|
const location = new URL(opts.pathname, "http://localhost");
|
|
164
164
|
let params = opts.params ?? {};
|
|
165
|
+
const rawMatched = opts.matched ?? [];
|
|
165
166
|
const handleState = {
|
|
166
167
|
data: opts.handleData ?? {},
|
|
167
|
-
segmentOrder: filterSegmentOrder(
|
|
168
|
+
segmentOrder: filterSegmentOrder(rawMatched),
|
|
169
|
+
routeSegmentIds: rawMatched.filter(
|
|
170
|
+
(id) => !id.includes(".@") && !/D\d+\./.test(id),
|
|
171
|
+
),
|
|
168
172
|
};
|
|
169
173
|
const state: DerivedNavigationState = {
|
|
170
174
|
state: "idle",
|
package/src/types/segments.ts
CHANGED
|
@@ -62,6 +62,14 @@ export interface ResolvedSegment {
|
|
|
62
62
|
notFoundInfo?: NotFoundInfo; // For notFound segments: the not found information
|
|
63
63
|
// Mount path from include() scope, used for MountContext.Provider wrapping
|
|
64
64
|
mountPath?: string;
|
|
65
|
+
/**
|
|
66
|
+
* @internal Server-side marker: true when the segment's handler actually ran
|
|
67
|
+
* this request (not skipped via the revalidate cache path). Used by
|
|
68
|
+
* match-result.ts to populate `MatchResult.resolvedIds` for client-side
|
|
69
|
+
* handle-bucket cleanup. Stripped from the wire payload before serialization
|
|
70
|
+
* — never reaches the client.
|
|
71
|
+
*/
|
|
72
|
+
_handlerRan?: boolean;
|
|
65
73
|
}
|
|
66
74
|
|
|
67
75
|
/**
|
|
@@ -116,6 +124,15 @@ export interface MatchResult {
|
|
|
116
124
|
segments: ResolvedSegment[];
|
|
117
125
|
matched: string[];
|
|
118
126
|
diff: string[];
|
|
127
|
+
/**
|
|
128
|
+
* Every segment id whose handler actually ran on the server this request,
|
|
129
|
+
* including ones with `component === null` that get filtered out of
|
|
130
|
+
* `segments`/`diff` to avoid wasted bytes. Drives the client's handle-
|
|
131
|
+
* cleanup pass — a slot that re-resolves and pushes nothing must clear
|
|
132
|
+
* its previous handle bucket, but `diff` doesn't carry it because the
|
|
133
|
+
* segment payload doesn't either. A superset of `diff`.
|
|
134
|
+
*/
|
|
135
|
+
resolvedIds: string[];
|
|
119
136
|
/**
|
|
120
137
|
* Merged route params from all matched segments
|
|
121
138
|
* Available for use by the handler after route matching
|