@rangojs/router 0.22.1 → 0.23.0
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/bin/rango.js +8 -3
- package/dist/testing/vitest.js +31 -1
- package/dist/types/browser/defer.d.ts +48 -0
- package/dist/types/browser/document-revealed.d.ts +17 -0
- package/dist/types/browser/event-controller.d.ts +7 -1
- package/dist/types/browser/navigation-transaction.d.ts +21 -7
- package/dist/types/browser/partial-update.d.ts +34 -5
- package/dist/types/browser/react/NavigationProvider.d.ts +13 -3
- package/dist/types/browser/react/filter-segment-order.d.ts +7 -0
- package/dist/types/browser/response-adapter.d.ts +7 -0
- package/dist/types/browser/types.d.ts +19 -9
- package/dist/types/browser/validate-redirect-origin.d.ts +11 -0
- package/dist/types/build/generate-route-types.d.ts +1 -1
- package/dist/types/build/route-types/router-processing.d.ts +1 -1
- package/dist/types/build/route-types/scan-filter.d.ts +16 -2
- package/dist/types/cache/cf/cf-cache-store.d.ts +16 -1
- package/dist/types/cache/cf/cf-cache-types.d.ts +23 -2
- package/dist/types/client-urls/client-urls.d.ts +1 -1
- package/dist/types/client-urls/hoisted-revalidate.d.ts +13 -0
- package/dist/types/client-urls/navigation.d.ts +0 -37
- package/dist/types/client-urls/revalidate-registry.d.ts +4 -0
- package/dist/types/client-urls/server-projection.d.ts +29 -1
- package/dist/types/client-urls/types.d.ts +34 -69
- package/dist/types/client.d.ts +0 -1
- package/dist/types/errors.d.ts +5 -0
- package/dist/types/handles/deferred-resolution.d.ts +2 -1
- package/dist/types/handles/pushed-by.d.ts +23 -0
- package/dist/types/loader-store.d.ts +8 -0
- package/dist/types/prefetch-markers.d.ts +11 -0
- package/dist/types/route-definition/helpers-types.d.ts +27 -0
- package/dist/types/router/is-action.d.ts +2 -1
- package/dist/types/router/segment-resolution/loader-mask.d.ts +3 -2
- package/dist/types/router/segment-resolution/prefetch-deferral.d.ts +241 -0
- package/dist/types/server/context.d.ts +21 -1
- package/dist/types/server/handle-store.d.ts +9 -1
- package/dist/types/server/request-context.d.ts +14 -1
- package/dist/types/server.d.ts +1 -0
- package/dist/types/testing/flight.entry.d.ts +1 -1
- package/dist/types/testing/index.d.ts +3 -3
- package/dist/types/testing/internal/action-id.d.ts +10 -1
- package/dist/types/testing/internal/client-urls-projection.d.ts +16 -0
- package/dist/types/testing/internal/test-action.d.ts +8 -0
- package/dist/types/testing/run-loader.d.ts +2 -1
- package/dist/types/testing/run-revalidate.d.ts +74 -0
- package/dist/types/testing/run-transition-when.d.ts +2 -3
- package/dist/types/testing/serve-shell-request.d.ts +38 -0
- package/dist/types/testing/vitest.d.ts +11 -0
- package/dist/types/types/boundaries.d.ts +9 -0
- package/dist/types/types/handler-context.d.ts +14 -0
- package/dist/types/types/loader-types.d.ts +30 -0
- package/dist/types/types/segments.d.ts +21 -4
- package/dist/types/urls/path-helper-types.d.ts +25 -4
- package/dist/types/urls/type-extraction.d.ts +6 -2
- package/dist/types/vite/discovery/discover-routers.d.ts +9 -0
- package/dist/types/vite/discovery/state.d.ts +11 -4
- package/dist/types/vite/plugins/client-urls-revalidate-hoist.d.ts +54 -0
- package/dist/types/vite/plugins/hoist/literal.d.ts +83 -0
- package/dist/types/vite/plugins/hoist/scope.d.ts +40 -0
- package/dist/types/vite/plugins/hoist/source.d.ts +41 -0
- package/dist/types/vite/plugins/transition-when-hoist.d.ts +8 -11
- package/dist/types/vite/router-discovery.d.ts +10 -0
- package/dist/vite/index.js +842 -308
- package/package.json +1 -1
- package/skills/caching/SKILL.md +42 -5
- package/skills/catalog.json +2 -2
- package/skills/client-urls/SKILL.md +113 -24
- package/skills/cloudflare/SKILL.md +14 -9
- package/skills/hooks/SKILL.md +11 -3
- package/skills/links/SKILL.md +24 -0
- package/skills/loader/SKILL.md +192 -1
- package/skills/migrate-nextjs/SKILL.md +2 -2
- package/skills/migrate-react-router/SKILL.md +10 -7
- package/skills/migrate-react-router/data-and-actions.md +1 -1
- package/skills/ppr/SKILL.md +13 -0
- package/skills/prerender/SKILL.md +22 -8
- package/skills/rango/SKILL.md +12 -6
- package/skills/route/SKILL.md +139 -0
- package/skills/testing/SKILL.md +3 -2
- package/skills/testing/client-components.md +1 -1
- package/skills/testing/predicates.md +67 -32
- package/skills/testing/serve-shell-request.md +88 -18
- package/skills/testing/setup.md +2 -2
- package/skills/typesafety/route-types.md +2 -2
- package/skills/view-transitions/SKILL.md +26 -0
- package/src/browser/defer.ts +82 -0
- package/src/browser/document-revealed.ts +52 -0
- package/src/browser/event-controller.ts +43 -32
- package/src/browser/navigation-bridge.ts +64 -14
- package/src/browser/navigation-client.ts +48 -36
- package/src/browser/navigation-store.ts +13 -0
- package/src/browser/navigation-transaction.ts +25 -8
- package/src/browser/partial-update.ts +528 -135
- package/src/browser/prefetch/fetch.ts +2 -1
- package/src/browser/react/NavigationProvider.tsx +47 -28
- package/src/browser/react/filter-segment-order.ts +13 -2
- package/src/browser/response-adapter.ts +20 -0
- package/src/browser/rsc-router.tsx +17 -45
- package/src/browser/server-action-bridge.ts +19 -63
- package/src/browser/types.ts +19 -9
- package/src/browser/validate-redirect-origin.ts +16 -0
- package/src/build/generate-route-types.ts +1 -0
- package/src/build/route-types/router-processing.ts +3 -6
- package/src/build/route-types/scan-filter.ts +37 -10
- package/src/cache/cf/cf-cache-store.ts +59 -21
- package/src/cache/cf/cf-cache-types.ts +24 -2
- package/src/cache/document-cache.ts +22 -1
- package/src/client-urls/client-urls.ts +22 -6
- package/src/client-urls/hoisted-revalidate.ts +25 -0
- package/src/client-urls/navigation.ts +0 -96
- package/src/client-urls/revalidate-registry.ts +30 -0
- package/src/client-urls/server-projection.ts +151 -65
- package/src/client-urls/types.ts +39 -71
- package/src/client.tsx +9 -5
- package/src/errors.ts +15 -0
- package/src/handles/deferred-resolution.ts +28 -5
- package/src/handles/pushed-by.ts +122 -0
- package/src/loader-store.ts +18 -7
- package/src/prefetch-markers.ts +13 -0
- package/src/route-content-wrapper.tsx +18 -4
- package/src/route-definition/dsl-helpers.ts +27 -4
- package/src/route-definition/helpers-types.ts +27 -1
- package/src/router/intercept-resolution.ts +6 -4
- package/src/router/is-action.ts +2 -1
- package/src/router/loader-resolution.ts +83 -4
- package/src/router/match-api.ts +36 -1
- package/src/router/match-middleware/background-revalidation.ts +11 -1
- package/src/router/match-middleware/cache-lookup.ts +29 -6
- package/src/router/match-middleware/cache-store.ts +17 -1
- package/src/router/match-result.ts +14 -2
- package/src/router/navigation-snapshot.ts +2 -1
- package/src/router/segment-resolution/fresh.ts +22 -18
- package/src/router/segment-resolution/loader-cache.ts +2 -1
- package/src/router/segment-resolution/loader-mask.ts +5 -3
- package/src/router/segment-resolution/prefetch-deferral.ts +543 -0
- package/src/router/segment-resolution/revalidation.ts +169 -60
- package/src/rsc/rsc-rendering.ts +50 -7
- package/src/rsc/shell-capture.ts +2 -1
- package/src/segment-loader-promise.ts +23 -20
- package/src/segment-system.tsx +60 -26
- package/src/server/context.ts +39 -5
- package/src/server/handle-store.ts +104 -8
- package/src/server/request-context.ts +21 -1
- package/src/server.ts +4 -0
- package/src/testing/action-form.ts +2 -11
- package/src/testing/e2e/page-helpers.ts +3 -1
- package/src/testing/flight.entry.ts +1 -0
- package/src/testing/index.ts +6 -3
- package/src/testing/internal/action-id.ts +16 -1
- package/src/testing/internal/client-urls-projection.ts +70 -0
- package/src/testing/internal/test-action.ts +25 -0
- package/src/testing/run-loader.ts +2 -1
- package/src/testing/run-revalidate.ts +142 -0
- package/src/testing/run-transition-when.ts +7 -8
- package/src/testing/serve-shell-request.ts +116 -6
- package/src/testing/vitest.ts +67 -1
- package/src/types/boundaries.ts +12 -0
- package/src/types/handler-context.ts +14 -0
- package/src/types/loader-types.ts +30 -0
- package/src/types/segments.ts +26 -4
- package/src/urls/path-helper-types.ts +27 -5
- package/src/urls/type-extraction.ts +30 -10
- package/src/use-loader.tsx +18 -13
- package/src/vite/discovery/client-urls-projection.ts +20 -21
- package/src/vite/discovery/discover-routers.ts +94 -26
- package/src/vite/discovery/state.ts +20 -4
- package/src/vite/discovery/virtual-module-codegen.ts +22 -17
- package/src/vite/plugins/client-urls-revalidate-hoist.ts +564 -0
- package/src/vite/plugins/hoist/literal.ts +417 -0
- package/src/vite/plugins/hoist/scope.ts +370 -0
- package/src/vite/plugins/hoist/source.ts +84 -0
- package/src/vite/plugins/transition-when-hoist.ts +113 -779
- package/src/vite/rango.ts +2 -0
- package/src/vite/router-discovery.ts +93 -32
- package/dist/types/client-urls/revalidate-chain.d.ts +0 -33
- package/dist/types/client-urls/revalidation-protocol.d.ts +0 -25
- package/dist/types/testing/run-client-revalidate.d.ts +0 -43
- package/src/client-urls/revalidate-chain.ts +0 -83
- package/src/client-urls/revalidation-protocol.ts +0 -56
- package/src/testing/run-client-revalidate.ts +0 -92
|
@@ -5,6 +5,7 @@ import type {
|
|
|
5
5
|
NavigateOptionsInternal,
|
|
6
6
|
ResolvedSegment,
|
|
7
7
|
} from "./types.js";
|
|
8
|
+
import { isPlaceholder } from "../types/segments.js";
|
|
8
9
|
import { setAppVersion } from "./app-version.js";
|
|
9
10
|
import { isActionFenceActive } from "./action-fence.js";
|
|
10
11
|
import { getRangoState } from "./rango-state.js";
|
|
@@ -14,7 +15,12 @@ import {
|
|
|
14
15
|
createNavigationTransaction,
|
|
15
16
|
resolveNavigationState,
|
|
16
17
|
} from "./navigation-transaction.js";
|
|
17
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
buildHistoryState,
|
|
20
|
+
mergeLocationState,
|
|
21
|
+
pushHistoryWithIdx,
|
|
22
|
+
} from "./history-state.js";
|
|
23
|
+
import { consumeHandles } from "./react/NavigationProvider.js";
|
|
18
24
|
import {
|
|
19
25
|
handleNavigationStart,
|
|
20
26
|
handleNavigationEnd,
|
|
@@ -120,6 +126,21 @@ export function setupNavigationBridgeDelegatedPrefetch(
|
|
|
120
126
|
);
|
|
121
127
|
}
|
|
122
128
|
|
|
129
|
+
/**
|
|
130
|
+
* A history entry's segments, when they can be shown. An entry left before
|
|
131
|
+
* its fill landed (`prefetch: false`) holds placeholders, not segments:
|
|
132
|
+
* restoring it would suspend on holes nobody resolves, so it is fetched.
|
|
133
|
+
*/
|
|
134
|
+
function isRestorable(
|
|
135
|
+
segments: ResolvedSegment[] | undefined,
|
|
136
|
+
): segments is ResolvedSegment[] {
|
|
137
|
+
return (
|
|
138
|
+
segments !== undefined &&
|
|
139
|
+
segments.length > 0 &&
|
|
140
|
+
!segments.some(isPlaceholder)
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
123
144
|
/**
|
|
124
145
|
* Create a navigation bridge for handling client-side navigation
|
|
125
146
|
*
|
|
@@ -154,6 +175,20 @@ export function createNavigationBridge(
|
|
|
154
175
|
onUpdate,
|
|
155
176
|
renderSegments,
|
|
156
177
|
getVersion: () => version,
|
|
178
|
+
fill: {
|
|
179
|
+
// A fill follows a redirect as a navigation (`bridge` is below).
|
|
180
|
+
redirect: (url, state) =>
|
|
181
|
+
void bridge.navigate(url, { state, replace: true, _skipCache: true }),
|
|
182
|
+
// No payload follows (`treeless`): the entry's readers are told now.
|
|
183
|
+
locationState: (state) => {
|
|
184
|
+
eventController.commitLocationState(mergeLocationState(state), true);
|
|
185
|
+
eventController.flushRouteState();
|
|
186
|
+
},
|
|
187
|
+
handles: (metadata) => {
|
|
188
|
+
if (metadata.handles) consumeHandles(metadata, eventController, store);
|
|
189
|
+
},
|
|
190
|
+
flush: eventController.flushRouteState,
|
|
191
|
+
},
|
|
157
192
|
});
|
|
158
193
|
|
|
159
194
|
/**
|
|
@@ -195,7 +230,7 @@ export function createNavigationBridge(
|
|
|
195
230
|
});
|
|
196
231
|
}
|
|
197
232
|
|
|
198
|
-
|
|
233
|
+
const bridge: NavigationBridge = {
|
|
199
234
|
/**
|
|
200
235
|
* Navigate to a URL
|
|
201
236
|
* Uses cached segments for SWR revalidation when available
|
|
@@ -247,7 +282,9 @@ export function createNavigationBridge(
|
|
|
247
282
|
|
|
248
283
|
const historyKey = generateHistoryKey(url, { intercept: isIntercept });
|
|
249
284
|
|
|
250
|
-
// Copy current segments to the new history key so back/forward restores instantly
|
|
285
|
+
// Copy current segments to the new history key so back/forward restores instantly.
|
|
286
|
+
// The array itself, not a clone: getPageHistoryKeys reads a shared
|
|
287
|
+
// array as the same page (the document's late handle pushes).
|
|
251
288
|
const currentKey = store.getHistoryKey();
|
|
252
289
|
const currentCache = store.getCachedSegments(currentKey);
|
|
253
290
|
if (currentCache?.segments) {
|
|
@@ -356,8 +393,10 @@ export function createNavigationBridge(
|
|
|
356
393
|
);
|
|
357
394
|
cachedSegments = cachedSegments.map((targetSeg) => {
|
|
358
395
|
const sourceSeg = sourceSegmentMap.get(targetSeg.id);
|
|
359
|
-
// Use source (current page) version for shared segments - it's
|
|
360
|
-
|
|
396
|
+
// Use source (current page) version for shared segments - it's
|
|
397
|
+
// fresher. A placeholder waiting for its fill (prefetch: false) is
|
|
398
|
+
// not a copy of the segment: the target keeps its own.
|
|
399
|
+
return sourceSeg && !isPlaceholder(sourceSeg) ? sourceSeg : targetSeg;
|
|
361
400
|
});
|
|
362
401
|
}
|
|
363
402
|
|
|
@@ -374,8 +413,7 @@ export function createNavigationBridge(
|
|
|
374
413
|
// 4. redirect-with-state - force re-render so hooks read fresh state
|
|
375
414
|
// 5. stale cache - server action invalidated it, need fresh data with loading state
|
|
376
415
|
const hasUsableCache =
|
|
377
|
-
cachedSegments &&
|
|
378
|
-
cachedSegments.length > 0 &&
|
|
416
|
+
isRestorable(cachedSegments) &&
|
|
379
417
|
!isInterceptOnlyCache(cachedSegments) &&
|
|
380
418
|
!hasInterceptCache &&
|
|
381
419
|
!isLeavingIntercept &&
|
|
@@ -463,7 +501,9 @@ export function createNavigationBridge(
|
|
|
463
501
|
targetCacheHandleData: cachedHandleData,
|
|
464
502
|
}
|
|
465
503
|
: {}),
|
|
466
|
-
|
|
504
|
+
...(clientUrlPresentation && {
|
|
505
|
+
presentation: clientUrlPresentation,
|
|
506
|
+
}),
|
|
467
507
|
},
|
|
468
508
|
);
|
|
469
509
|
} catch (error) {
|
|
@@ -580,7 +620,9 @@ export function createNavigationBridge(
|
|
|
580
620
|
* Uses cached segments when available for instant restoration
|
|
581
621
|
*/
|
|
582
622
|
async handlePopstate(): Promise<void> {
|
|
583
|
-
// Abort any pending navigation to prevent race conditions
|
|
623
|
+
// Abort any pending navigation to prevent race conditions, and the
|
|
624
|
+
// fill of the page being left (prefetch: false), which its committed
|
|
625
|
+
// navigation still owns.
|
|
584
626
|
eventController.abortNavigation();
|
|
585
627
|
handleTraversalStart();
|
|
586
628
|
|
|
@@ -648,7 +690,7 @@ export function createNavigationBridge(
|
|
|
648
690
|
// revalidates (SWR) instead of serving it as fresh.
|
|
649
691
|
const isStale = (cached?.stale ?? false) || isActionFenceActive();
|
|
650
692
|
|
|
651
|
-
if (cachedSegments
|
|
693
|
+
if (isRestorable(cachedSegments)) {
|
|
652
694
|
// transition({ when }) decides the restore (kind "pop") against the
|
|
653
695
|
// entry being left, so before the store moves to the restored entry.
|
|
654
696
|
// The decision the entry was committed with is not reused: it was
|
|
@@ -818,8 +860,12 @@ export function createNavigationBridge(
|
|
|
818
860
|
? { type: "leave-intercept" }
|
|
819
861
|
: undefined,
|
|
820
862
|
);
|
|
821
|
-
// Restore scroll position after fetch completes
|
|
822
|
-
|
|
863
|
+
// Restore scroll position after fetch completes. A traversal that
|
|
864
|
+
// was left returns here too, its response ignored: it restores
|
|
865
|
+
// nothing.
|
|
866
|
+
if (!tx.handle.signal.aborted) {
|
|
867
|
+
handleNavigationEnd({ restore: true, isStreaming });
|
|
868
|
+
}
|
|
823
869
|
} catch (error) {
|
|
824
870
|
// Aborted or superseded by a newer navigation: bail without clobbering
|
|
825
871
|
// content with a boundary (see navigate()).
|
|
@@ -888,13 +934,16 @@ export function createNavigationBridge(
|
|
|
888
934
|
// 3. The user presses back and the browser restores from bfcache
|
|
889
935
|
// At that point, currentNavigation is still set from step 1, so
|
|
890
936
|
// getState() returns "loading" and the progress bar shows.
|
|
891
|
-
// Abort the stale navigation to reset state to idle.
|
|
937
|
+
// Abort the stale navigation to reset state to idle. Only that one: the
|
|
938
|
+
// page restored is not being left, so a committed navigation's
|
|
939
|
+
// prefetch: false fill keeps its signal, and fails as a navigation if
|
|
940
|
+
// the browser dropped its request while the page was frozen.
|
|
892
941
|
const handlePageShow = (event: PageTransitionEvent) => {
|
|
893
942
|
if (event.persisted) {
|
|
894
943
|
debugLog(
|
|
895
944
|
"[Browser] Page restored from bfcache, resetting navigation state",
|
|
896
945
|
);
|
|
897
|
-
eventController.abortNavigation();
|
|
946
|
+
eventController.abortNavigation(true);
|
|
898
947
|
}
|
|
899
948
|
};
|
|
900
949
|
|
|
@@ -933,6 +982,7 @@ export function createNavigationBridge(
|
|
|
933
982
|
store.clearHistoryCache();
|
|
934
983
|
},
|
|
935
984
|
};
|
|
985
|
+
return bridge;
|
|
936
986
|
}
|
|
937
987
|
|
|
938
988
|
export { createNavigationBridge as default };
|
|
@@ -14,7 +14,6 @@ import {
|
|
|
14
14
|
} from "./logging.js";
|
|
15
15
|
import { getRangoState } from "./rango-state.js";
|
|
16
16
|
import { isActionFenceActive } from "./action-fence.js";
|
|
17
|
-
import { CLIENT_REVALIDATION_HEADER } from "../client-urls/revalidation-protocol.js";
|
|
18
17
|
import {
|
|
19
18
|
expandPayloadFragments,
|
|
20
19
|
SEGMENT_FRAGMENT_CAPABILITY_HEADER,
|
|
@@ -22,6 +21,7 @@ import {
|
|
|
22
21
|
SegmentFragmentDecodeError,
|
|
23
22
|
} from "../segment-fragments.js";
|
|
24
23
|
import {
|
|
24
|
+
abortUntilResponse,
|
|
25
25
|
extractRscHeaderUrl,
|
|
26
26
|
emptyResponse,
|
|
27
27
|
handleReloadHeader,
|
|
@@ -38,6 +38,7 @@ import {
|
|
|
38
38
|
type DecodedPrefetch,
|
|
39
39
|
} from "./prefetch/cache.js";
|
|
40
40
|
import { cancelAllPrefetches } from "./prefetch/loader.js";
|
|
41
|
+
import { FILL_PARAM } from "../prefetch-markers.js";
|
|
41
42
|
|
|
42
43
|
/**
|
|
43
44
|
* Create a navigation client for fetching RSC payloads
|
|
@@ -80,11 +81,11 @@ export function createNavigationClient(
|
|
|
80
81
|
previousUrl,
|
|
81
82
|
signal,
|
|
82
83
|
staleRevalidation,
|
|
83
|
-
clientRevalidation,
|
|
84
84
|
interceptSourceUrl,
|
|
85
85
|
version,
|
|
86
86
|
routerId,
|
|
87
87
|
hmr,
|
|
88
|
+
fill,
|
|
88
89
|
} = options;
|
|
89
90
|
|
|
90
91
|
const debugEnabled = isBrowserDebugEnabled();
|
|
@@ -114,11 +115,18 @@ export function createNavigationClient(
|
|
|
114
115
|
if (routerId) {
|
|
115
116
|
fetchUrl.searchParams.set("_rsc_rid", routerId);
|
|
116
117
|
}
|
|
118
|
+
// A param, not a header: the document cache decides from the raw URL
|
|
119
|
+
// (cache/document-cache.ts), and it keeps the fill out of every
|
|
120
|
+
// prefetch key below.
|
|
121
|
+
if (fill) {
|
|
122
|
+
fetchUrl.searchParams.set(FILL_PARAM, "1");
|
|
123
|
+
}
|
|
117
124
|
|
|
118
125
|
// If the lazy prefetch runtime has not loaded yet, prevent a deferred
|
|
119
126
|
// speculative request from starting after this navigation misses the
|
|
120
|
-
// synchronous cache lookup and begins its own fetch.
|
|
121
|
-
|
|
127
|
+
// synchronous cache lookup and begins its own fetch. A fill is not a
|
|
128
|
+
// navigation: the links on the page it completes stay prefetched.
|
|
129
|
+
if (!fill) cancelAllPrefetches(targetUrl);
|
|
122
130
|
|
|
123
131
|
// Check completed in-memory prefetch cache before making a network
|
|
124
132
|
// request. Try the source-scoped key first (populated when the server
|
|
@@ -136,6 +144,7 @@ export function createNavigationClient(
|
|
|
136
144
|
const canUsePrefetch =
|
|
137
145
|
!staleRevalidation &&
|
|
138
146
|
!hmr &&
|
|
147
|
+
!fill &&
|
|
139
148
|
!interceptSourceUrl &&
|
|
140
149
|
!isActionFenceActive();
|
|
141
150
|
const rangoState = getRangoState();
|
|
@@ -242,39 +251,42 @@ export function createNavigationClient(
|
|
|
242
251
|
});
|
|
243
252
|
}
|
|
244
253
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
// cookie read has side effects (external-rotation notify) we do not
|
|
259
|
-
// want to fire twice per navigation.
|
|
260
|
-
"X-Rango-State": rangoState,
|
|
261
|
-
...(fragmentPassthrough && {
|
|
262
|
-
[SEGMENT_FRAGMENT_CAPABILITY_HEADER]: "1",
|
|
263
|
-
}),
|
|
264
|
-
...(fragmentRecovery && {
|
|
265
|
-
[SEGMENT_FRAGMENT_RECOVERY_HEADER]: "1",
|
|
266
|
-
}),
|
|
267
|
-
...(clientRevalidation && {
|
|
268
|
-
[CLIENT_REVALIDATION_HEADER]: clientRevalidation,
|
|
254
|
+
// `signal` cancels the request until its response arrives
|
|
255
|
+
// (abortUntilResponse). A prefetch: false fill passes its
|
|
256
|
+
// navigation's signal, which stays abortable after the commit
|
|
257
|
+
// (event-controller.ts streamingNavigation).
|
|
258
|
+
const responsePromise = abortUntilResponse(signal, (requestSignal) =>
|
|
259
|
+
fetch(fetchUrl, {
|
|
260
|
+
// During an action's flight the state is not rotated, so the old
|
|
261
|
+
// X-Rango-State still matches the Vary-keyed HTTP-cache entry; bypass
|
|
262
|
+
// it so a genuine mid-action navigation fetches fresh instead of being
|
|
263
|
+
// served the stale prefetched bytes. Fragment recovery also bypasses
|
|
264
|
+
// HTTP caches so it reaches the server's decode-and-evict path.
|
|
265
|
+
...((isActionFenceActive() || fragmentRecovery || fill) && {
|
|
266
|
+
cache: "no-store" as RequestCache,
|
|
269
267
|
}),
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
268
|
+
headers: {
|
|
269
|
+
"X-RSC-Router-Client-Path": previousUrl,
|
|
270
|
+
// Reuse the single per-operation read (see rangoState above): the
|
|
271
|
+
// cache-key lookup and this header must agree on one value, and the
|
|
272
|
+
// cookie read has side effects (external-rotation notify) we do not
|
|
273
|
+
// want to fire twice per navigation.
|
|
274
|
+
"X-Rango-State": rangoState,
|
|
275
|
+
...(fragmentPassthrough && {
|
|
276
|
+
[SEGMENT_FRAGMENT_CAPABILITY_HEADER]: "1",
|
|
277
|
+
}),
|
|
278
|
+
...(fragmentRecovery && {
|
|
279
|
+
[SEGMENT_FRAGMENT_RECOVERY_HEADER]: "1",
|
|
280
|
+
}),
|
|
281
|
+
...(tx && { "X-RSC-Router-Request-Id": tx.requestId }),
|
|
282
|
+
...(interceptSourceUrl && {
|
|
283
|
+
"X-RSC-Router-Intercept-Source": interceptSourceUrl,
|
|
284
|
+
}),
|
|
285
|
+
...(hmr && { "X-RSC-HMR": "1" }),
|
|
286
|
+
},
|
|
287
|
+
signal: requestSignal,
|
|
288
|
+
}),
|
|
289
|
+
).then((response) => {
|
|
278
290
|
const validated = validateRscHeaders(
|
|
279
291
|
response,
|
|
280
292
|
"fetch",
|
|
@@ -656,6 +656,19 @@ export function createNavigationStore(
|
|
|
656
656
|
];
|
|
657
657
|
},
|
|
658
658
|
|
|
659
|
+
/**
|
|
660
|
+
* Entries that show the same page as `historyKey`'s. A shallow navigation
|
|
661
|
+
* (navigation-bridge.ts) caches the page's segments array under the new
|
|
662
|
+
* key; every other commit caches a new array, so identity is the page.
|
|
663
|
+
*/
|
|
664
|
+
getPageHistoryKeys(historyKey: string): string[] {
|
|
665
|
+
const segments = historyCache.find(([key]) => key === historyKey)?.[1];
|
|
666
|
+
if (!segments) return [historyKey];
|
|
667
|
+
return historyCache
|
|
668
|
+
.filter((entry) => entry[1] === segments)
|
|
669
|
+
.map(([key]) => key);
|
|
670
|
+
},
|
|
671
|
+
|
|
659
672
|
/**
|
|
660
673
|
* Mark every history entry stale WITHOUT clearing the prefetch caches or
|
|
661
674
|
* rotating the rango state. The jar-divergence observer calls this after an
|
|
@@ -92,6 +92,23 @@ interface BoundCommitOverrides {
|
|
|
92
92
|
treeless?: boolean;
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
+
/**
|
|
96
|
+
* What a commit decided about scroll, in the shape the update that follows
|
|
97
|
+
* carries it (NavigationUpdate.scroll). A navigation always decides, and "do
|
|
98
|
+
* not scroll" is a decision (`<Link scroll={false}>`, an intercept): it
|
|
99
|
+
* replaces a pending one. A commit that is not a navigation decides nothing:
|
|
100
|
+
* an action refetch into the entry on screen (`storeOnly`) and a cache-only
|
|
101
|
+
* commit return no `scroll`, and their update leaves a pending decision alone
|
|
102
|
+
* (react/NavigationProvider.tsx).
|
|
103
|
+
*
|
|
104
|
+
* Scar tissue: those two used to answer `scroll: false`. An action refetch
|
|
105
|
+
* that landed before React committed a navigation then replaced the
|
|
106
|
+
* navigation's pending scroll with "do not scroll".
|
|
107
|
+
*/
|
|
108
|
+
export interface CommitResult {
|
|
109
|
+
scroll?: { enabled: boolean | undefined };
|
|
110
|
+
}
|
|
111
|
+
|
|
95
112
|
/**
|
|
96
113
|
* Bound transaction with pre-configured commit options (without segmentIds/segments)
|
|
97
114
|
*/
|
|
@@ -107,12 +124,12 @@ export interface BoundTransaction {
|
|
|
107
124
|
readonly transitionGatedOff?: boolean;
|
|
108
125
|
/** Start streaming and get a token to end it when the stream completes */
|
|
109
126
|
startStreaming(): StreamingToken;
|
|
110
|
-
/** Commit the navigation. Returns
|
|
127
|
+
/** Commit the navigation. Returns its scroll decision, for the update. */
|
|
111
128
|
commit(
|
|
112
129
|
segmentIds: string[],
|
|
113
130
|
segments: ResolvedSegment[],
|
|
114
131
|
overrides?: BoundCommitOverrides,
|
|
115
|
-
):
|
|
132
|
+
): CommitResult;
|
|
116
133
|
}
|
|
117
134
|
|
|
118
135
|
/**
|
|
@@ -120,7 +137,7 @@ export interface BoundTransaction {
|
|
|
120
137
|
* Uses the event controller handle for lifecycle management
|
|
121
138
|
*/
|
|
122
139
|
interface NavigationTransaction extends Disposable {
|
|
123
|
-
commit(options: CommitOptions):
|
|
140
|
+
commit(options: CommitOptions): CommitResult;
|
|
124
141
|
with(
|
|
125
142
|
options: Omit<CommitOptions, "segmentIds" | "segments">,
|
|
126
143
|
): BoundTransaction;
|
|
@@ -146,7 +163,7 @@ export function createNavigationTransaction(
|
|
|
146
163
|
/**
|
|
147
164
|
* Commit the navigation - updates store and URL atomically
|
|
148
165
|
*/
|
|
149
|
-
function commit(opts: CommitOptions):
|
|
166
|
+
function commit(opts: CommitOptions): CommitResult {
|
|
150
167
|
committed = true;
|
|
151
168
|
|
|
152
169
|
const {
|
|
@@ -172,7 +189,7 @@ export function createNavigationTransaction(
|
|
|
172
189
|
store.cacheSegmentsForHistory(historyKey, segments, currentHandleData);
|
|
173
190
|
handle.complete(parsedUrl);
|
|
174
191
|
debugLog("[Browser] Cache-only commit, historyKey:", historyKey);
|
|
175
|
-
return {
|
|
192
|
+
return {};
|
|
176
193
|
}
|
|
177
194
|
|
|
178
195
|
if (!traversal) handleNavigationStart();
|
|
@@ -196,7 +213,7 @@ export function createNavigationTransaction(
|
|
|
196
213
|
store.rememberDisplayedEntry(opts.routeName);
|
|
197
214
|
debugLog("[Browser] Store updated (action)");
|
|
198
215
|
handle.complete(parsedUrl);
|
|
199
|
-
return {
|
|
216
|
+
return {};
|
|
200
217
|
}
|
|
201
218
|
|
|
202
219
|
if (traversal) {
|
|
@@ -209,7 +226,7 @@ export function createNavigationTransaction(
|
|
|
209
226
|
eventController.commitLocationState(entryState, opts.treeless);
|
|
210
227
|
handle.complete(parsedUrl);
|
|
211
228
|
debugLog("[Browser] Traversal committed, historyKey:", historyKey);
|
|
212
|
-
return { scroll };
|
|
229
|
+
return { scroll: { enabled: scroll } };
|
|
213
230
|
}
|
|
214
231
|
|
|
215
232
|
const historyState = buildHistoryState(
|
|
@@ -231,7 +248,7 @@ export function createNavigationTransaction(
|
|
|
231
248
|
intercept ? "(intercept)" : "",
|
|
232
249
|
);
|
|
233
250
|
|
|
234
|
-
return { scroll };
|
|
251
|
+
return { scroll: { enabled: scroll } };
|
|
235
252
|
}
|
|
236
253
|
|
|
237
254
|
return {
|