@rangojs/router 0.22.1 → 0.24.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 +10 -4
- 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/merge-segment-loaders.d.ts +5 -2
- package/dist/types/browser/navigation-transaction.d.ts +21 -7
- package/dist/types/browser/partial-update.d.ts +39 -8
- package/dist/types/browser/react/NavigationProvider.d.ts +19 -5
- package/dist/types/browser/react/context.d.ts +3 -2
- 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 +24 -10
- 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 +22 -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/cache/document-cache-policy.d.ts +24 -0
- package/dist/types/cache/document-cache.d.ts +2 -10
- 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/context-var.d.ts +12 -0
- package/dist/types/errors.d.ts +15 -0
- package/dist/types/handles/deferred-resolution.d.ts +2 -1
- package/dist/types/handles/pushed-by.d.ts +31 -0
- package/dist/types/index.d.ts +4 -0
- package/dist/types/index.rsc.d.ts +2 -0
- package/dist/types/loader-store.d.ts +8 -0
- package/dist/types/prefetch-markers.d.ts +11 -0
- package/dist/types/response-utils.d.ts +7 -0
- package/dist/types/reverse.d.ts +8 -8
- package/dist/types/route-definition/helpers-types.d.ts +27 -0
- package/dist/types/route-definition/rewrite.d.ts +56 -0
- package/dist/types/route-map-builder.d.ts +7 -2
- package/dist/types/router/basename.d.ts +7 -0
- package/dist/types/router/handler-context.d.ts +1 -5
- package/dist/types/router/internal-params.d.ts +8 -0
- package/dist/types/router/is-action.d.ts +2 -1
- package/dist/types/router/middleware-types.d.ts +15 -0
- package/dist/types/router/revalidation.d.ts +1 -2
- package/dist/types/router/route-trie-builder.d.ts +10 -0
- package/dist/types/router/segment-resolution/loader-mask.d.ts +3 -2
- package/dist/types/router/segment-resolution/mask-nested.d.ts +4 -2
- package/dist/types/router/segment-resolution/prefetch-deferral.d.ts +264 -0
- package/dist/types/rsc/full-payload.d.ts +11 -0
- package/dist/types/rsc/handler.d.ts +33 -0
- package/dist/types/rsc/helpers.d.ts +8 -0
- package/dist/types/rsc/loader-fetch.d.ts +16 -0
- package/dist/types/rsc/rewrite-passes.d.ts +74 -0
- package/dist/types/rsc/ssr-setup.d.ts +3 -1
- 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 +52 -2
- package/dist/types/server/rewrite-trail.d.ts +49 -0
- package/dist/types/server/slot-accessors.d.ts +29 -0
- 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 +4 -4
- 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/render-route.d.ts +8 -0
- package/dist/types/testing/run-loader.d.ts +2 -1
- package/dist/types/testing/run-middleware.d.ts +20 -0
- 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 +40 -1
- package/dist/types/testing/vitest.d.ts +11 -0
- package/dist/types/thenable-status.d.ts +6 -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/request-scope.d.ts +7 -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 +14 -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 +974 -379
- package/package.json +1 -1
- package/skills/caching/SKILL.md +54 -5
- package/skills/catalog.json +3 -3
- package/skills/client-urls/SKILL.md +197 -24
- package/skills/cloudflare/SKILL.md +14 -9
- package/skills/hooks/SKILL.md +16 -3
- package/skills/hooks/data.md +3 -1
- package/skills/links/SKILL.md +24 -0
- package/skills/loader/SKILL.md +199 -2
- package/skills/middleware/SKILL.md +183 -2
- 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 +22 -0
- package/skills/prerender/SKILL.md +22 -8
- package/skills/rango/SKILL.md +13 -7
- package/skills/route/SKILL.md +139 -0
- package/skills/server-actions/SKILL.md +27 -0
- package/skills/testing/SKILL.md +3 -2
- package/skills/testing/client-components.md +2 -1
- package/skills/testing/middleware.md +31 -10
- package/skills/testing/predicates.md +67 -32
- package/skills/testing/response-routes.md +1 -0
- package/skills/testing/serve-shell-request.md +107 -19
- package/skills/testing/setup.md +2 -2
- package/skills/typesafety/route-types.md +2 -2
- package/skills/use-cache/SKILL.md +4 -2
- package/skills/view-transitions/SKILL.md +26 -0
- package/src/__augment-tests__/augmented.check.ts +20 -0
- package/src/browser/defer.ts +82 -0
- package/src/browser/document-revealed.ts +52 -0
- package/src/browser/event-controller.ts +46 -32
- package/src/browser/merge-segment-loaders.ts +31 -18
- package/src/browser/navigation-bridge.ts +82 -16
- 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 +554 -146
- package/src/browser/prefetch/fetch.ts +2 -1
- package/src/browser/react/Link.tsx +7 -9
- package/src/browser/react/NavigationProvider.tsx +88 -49
- package/src/browser/react/context.ts +3 -2
- package/src/browser/react/filter-segment-order.ts +13 -2
- package/src/browser/react/use-router.ts +2 -4
- package/src/browser/response-adapter.ts +20 -0
- package/src/browser/rsc-router.tsx +17 -45
- package/src/browser/server-action-bridge.ts +25 -63
- package/src/browser/types.ts +24 -10
- 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 +45 -11
- 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-policy.ts +131 -0
- package/src/cache/document-cache.ts +21 -107
- 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/context-var.ts +32 -2
- package/src/errors.ts +34 -0
- package/src/handles/deferred-resolution.ts +28 -5
- package/src/handles/pushed-by.ts +148 -0
- package/src/index.rsc.ts +8 -0
- package/src/index.ts +7 -0
- package/src/loader-store.ts +18 -7
- package/src/prefetch-markers.ts +13 -0
- package/src/response-utils.ts +19 -0
- package/src/reverse.ts +17 -14
- 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/route-definition/redirect.ts +2 -15
- package/src/route-definition/rewrite.ts +146 -0
- package/src/route-map-builder.ts +14 -11
- package/src/router/basename.ts +20 -0
- package/src/router/handler-context.ts +3 -13
- package/src/router/intercept-resolution.ts +35 -16
- package/src/router/internal-params.ts +16 -0
- package/src/router/is-action.ts +2 -1
- package/src/router/loader-resolution.ts +85 -4
- package/src/router/match-api.ts +36 -1
- package/src/router/match-middleware/background-revalidation.ts +16 -4
- package/src/router/match-middleware/cache-lookup.ts +35 -7
- package/src/router/match-middleware/cache-store.ts +15 -1
- package/src/router/match-middleware/intercept-resolution.ts +4 -2
- package/src/router/match-result.ts +14 -2
- package/src/router/middleware-types.ts +16 -0
- package/src/router/middleware.ts +127 -49
- package/src/router/navigation-snapshot.ts +2 -1
- package/src/router/revalidation.ts +1 -1
- package/src/router/route-trie-builder.ts +28 -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/loader-snapshot.ts +18 -4
- package/src/router/segment-resolution/mask-nested.ts +8 -4
- package/src/router/segment-resolution/prefetch-deferral.ts +583 -0
- package/src/router/segment-resolution/revalidation.ts +215 -82
- package/src/rsc/full-payload.ts +37 -5
- package/src/rsc/handler.ts +82 -55
- package/src/rsc/helpers.ts +24 -0
- package/src/rsc/loader-fetch.ts +79 -24
- package/src/rsc/response-cache-serve.ts +18 -2
- package/src/rsc/response-route-handler.ts +4 -7
- package/src/rsc/rewrite-passes.ts +203 -0
- package/src/rsc/rsc-rendering.ts +54 -9
- package/src/rsc/shell-capture.ts +2 -1
- package/src/rsc/ssr-setup.ts +11 -5
- 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/cookie-parse.ts +36 -12
- package/src/server/cookie-store.ts +7 -3
- package/src/server/handle-store.ts +152 -25
- package/src/server/request-context.ts +175 -64
- package/src/server/request-headers.ts +4 -14
- package/src/server/rewrite-trail.ts +190 -0
- package/src/server/slot-accessors.ts +57 -0
- package/src/server.ts +4 -0
- package/src/ssr/index.tsx +118 -16
- package/src/testing/action-form.ts +2 -11
- package/src/testing/dispatch.ts +138 -67
- package/src/testing/e2e/page-helpers.ts +3 -1
- package/src/testing/flight.entry.ts +1 -0
- package/src/testing/index.ts +7 -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/render-route.tsx +16 -3
- package/src/testing/run-loader.ts +4 -1
- package/src/testing/run-middleware.ts +56 -11
- package/src/testing/run-revalidate.ts +142 -0
- package/src/testing/run-transition-when.ts +7 -8
- package/src/testing/serve-shell-request.ts +131 -8
- package/src/testing/vitest.ts +67 -1
- package/src/thenable-status.ts +11 -0
- 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/request-scope.ts +8 -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 +28 -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/expose-internal-ids.ts +2 -3
- 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 +116 -53
- 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
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
import type { ResolvedSegment } from "./types.js";
|
|
2
2
|
import { debugLog, IS_BROWSER_DEBUG } from "./logging.js";
|
|
3
|
+
import { settledValue } from "../thenable-status.js";
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Merge partial loader data from server with cached loader data.
|
|
6
7
|
*
|
|
7
8
|
* During partial revalidation (stale or action), the server may return only
|
|
8
|
-
* some loaders that pass the revalidation check
|
|
9
|
-
* all loader data, so we merge fresh data with
|
|
9
|
+
* some loaders that pass the revalidation check, none when every one is held.
|
|
10
|
+
* The component still needs all loader data, so we merge fresh data with
|
|
11
|
+
* cached data. Two settled aggregates merge into the array itself: a fresh
|
|
12
|
+
* Promise.all over settled parts is a promise React has not read, and content
|
|
13
|
+
* on screen handed one suspends (docs/internal/suspense-contract.md, rule 2).
|
|
10
14
|
*
|
|
11
15
|
* @param fromServer - Segment returned from server with partial loaders
|
|
12
16
|
* @param fromCache - Cached segment with full loader data
|
|
@@ -25,24 +29,33 @@ export function mergeSegmentLoaders(
|
|
|
25
29
|
);
|
|
26
30
|
}
|
|
27
31
|
|
|
32
|
+
// Fresh data for the loaders the server re-ran, cached data for the rest.
|
|
33
|
+
const merge = (newData: any[], cachedData: any[]): any[] =>
|
|
34
|
+
cachedLoaderIds.map((id: string, i: number) => {
|
|
35
|
+
const newIndex = serverLoaderIds.indexOf(id);
|
|
36
|
+
return newIndex !== -1 ? newData[newIndex] : cachedData[i];
|
|
37
|
+
});
|
|
38
|
+
const settledNew = settledValue(fromServer.loaderDataPromise!);
|
|
39
|
+
const settledCached = settledValue(fromCache.loaderDataPromise!);
|
|
40
|
+
|
|
28
41
|
return {
|
|
29
42
|
...fromCache,
|
|
30
|
-
//
|
|
31
|
-
|
|
32
|
-
//
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
43
|
+
// The server re-ran the slot's handler and layout: their render applies,
|
|
44
|
+
// as it does when every loader re-runs. Only the loaders it held keep
|
|
45
|
+
// their cached data.
|
|
46
|
+
component: fromServer.component,
|
|
47
|
+
layout: fromServer.layout,
|
|
48
|
+
// Every loader held: the cached aggregate itself, so a boundary still
|
|
49
|
+
// waiting on it keeps its one thenable (suspense-contract.md, rule 1).
|
|
50
|
+
loaderDataPromise:
|
|
51
|
+
serverLoaderIds.length === 0
|
|
52
|
+
? fromCache.loaderDataPromise
|
|
53
|
+
: settledNew && settledCached
|
|
54
|
+
? merge(settledNew, settledCached)
|
|
55
|
+
: Promise.all([
|
|
56
|
+
fromServer.loaderDataPromise!,
|
|
57
|
+
fromCache.loaderDataPromise!,
|
|
58
|
+
]).then(([newData, cachedData]) => merge(newData, cachedData)),
|
|
46
59
|
// Keep all loader IDs from cache
|
|
47
60
|
loaderIds: fromCache.loaderIds,
|
|
48
61
|
// The rebuilt aggregate is the source of truth. Cached per-loader
|
|
@@ -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,13 @@ 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";
|
|
24
|
+
import { withSegmentHandles } from "../handles/pushed-by.js";
|
|
18
25
|
import {
|
|
19
26
|
handleNavigationStart,
|
|
20
27
|
handleNavigationEnd,
|
|
@@ -120,6 +127,21 @@ export function setupNavigationBridgeDelegatedPrefetch(
|
|
|
120
127
|
);
|
|
121
128
|
}
|
|
122
129
|
|
|
130
|
+
/**
|
|
131
|
+
* A history entry's segments, when they can be shown. An entry left before
|
|
132
|
+
* its fill landed (`prefetch: false`) holds placeholders, not segments:
|
|
133
|
+
* restoring it would suspend on holes nobody resolves, so it is fetched.
|
|
134
|
+
*/
|
|
135
|
+
function isRestorable(
|
|
136
|
+
segments: ResolvedSegment[] | undefined,
|
|
137
|
+
): segments is ResolvedSegment[] {
|
|
138
|
+
return (
|
|
139
|
+
segments !== undefined &&
|
|
140
|
+
segments.length > 0 &&
|
|
141
|
+
!segments.some(isPlaceholder)
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
123
145
|
/**
|
|
124
146
|
* Create a navigation bridge for handling client-side navigation
|
|
125
147
|
*
|
|
@@ -154,6 +176,20 @@ export function createNavigationBridge(
|
|
|
154
176
|
onUpdate,
|
|
155
177
|
renderSegments,
|
|
156
178
|
getVersion: () => version,
|
|
179
|
+
fill: {
|
|
180
|
+
// A fill follows a redirect as a navigation (`bridge` is below).
|
|
181
|
+
redirect: (url, state) =>
|
|
182
|
+
void bridge.navigate(url, { state, replace: true, _skipCache: true }),
|
|
183
|
+
// No payload follows (`treeless`): the entry's readers are told now.
|
|
184
|
+
locationState: (state) => {
|
|
185
|
+
eventController.commitLocationState(mergeLocationState(state), true);
|
|
186
|
+
eventController.flushRouteState();
|
|
187
|
+
},
|
|
188
|
+
handles: (metadata) => {
|
|
189
|
+
if (metadata.handles) consumeHandles(metadata, eventController, store);
|
|
190
|
+
},
|
|
191
|
+
flush: eventController.flushRouteState,
|
|
192
|
+
},
|
|
157
193
|
});
|
|
158
194
|
|
|
159
195
|
/**
|
|
@@ -195,7 +231,7 @@ export function createNavigationBridge(
|
|
|
195
231
|
});
|
|
196
232
|
}
|
|
197
233
|
|
|
198
|
-
|
|
234
|
+
const bridge: NavigationBridge = {
|
|
199
235
|
/**
|
|
200
236
|
* Navigate to a URL
|
|
201
237
|
* Uses cached segments for SWR revalidation when available
|
|
@@ -247,7 +283,9 @@ export function createNavigationBridge(
|
|
|
247
283
|
|
|
248
284
|
const historyKey = generateHistoryKey(url, { intercept: isIntercept });
|
|
249
285
|
|
|
250
|
-
// Copy current segments to the new history key so back/forward restores instantly
|
|
286
|
+
// Copy current segments to the new history key so back/forward restores instantly.
|
|
287
|
+
// The array itself, not a clone: getPageHistoryKeys reads a shared
|
|
288
|
+
// array as the same page (the document's late handle pushes).
|
|
251
289
|
const currentKey = store.getHistoryKey();
|
|
252
290
|
const currentCache = store.getCachedSegments(currentKey);
|
|
253
291
|
if (currentCache?.segments) {
|
|
@@ -349,15 +387,21 @@ export function createNavigationBridge(
|
|
|
349
387
|
// since it may have fresher data after an action revalidation.
|
|
350
388
|
// This avoids unnecessary server round-trips for shared layout loaders.
|
|
351
389
|
let cachedSegments = cached?.segments;
|
|
352
|
-
|
|
390
|
+
// The segments taken from the page on screen: their pushes come from
|
|
391
|
+
// it too (targetCacheHandleData below).
|
|
392
|
+
const fromSource = new Set<string>();
|
|
353
393
|
if (cachedSegments && sourceCached?.segments) {
|
|
354
394
|
const sourceSegmentMap = new Map(
|
|
355
395
|
sourceCached.segments.map((s) => [s.id, s]),
|
|
356
396
|
);
|
|
357
397
|
cachedSegments = cachedSegments.map((targetSeg) => {
|
|
358
398
|
const sourceSeg = sourceSegmentMap.get(targetSeg.id);
|
|
359
|
-
// Use source (current page) version for shared segments - it's
|
|
360
|
-
|
|
399
|
+
// Use source (current page) version for shared segments - it's
|
|
400
|
+
// fresher. A placeholder waiting for its fill (prefetch: false) is
|
|
401
|
+
// not a copy of the segment: the target keeps its own.
|
|
402
|
+
if (!sourceSeg || isPlaceholder(sourceSeg)) return targetSeg;
|
|
403
|
+
fromSource.add(targetSeg.id);
|
|
404
|
+
return sourceSeg;
|
|
361
405
|
});
|
|
362
406
|
}
|
|
363
407
|
|
|
@@ -374,8 +418,7 @@ export function createNavigationBridge(
|
|
|
374
418
|
// 4. redirect-with-state - force re-render so hooks read fresh state
|
|
375
419
|
// 5. stale cache - server action invalidated it, need fresh data with loading state
|
|
376
420
|
const hasUsableCache =
|
|
377
|
-
cachedSegments &&
|
|
378
|
-
cachedSegments.length > 0 &&
|
|
421
|
+
isRestorable(cachedSegments) &&
|
|
379
422
|
!isInterceptOnlyCache(cachedSegments) &&
|
|
380
423
|
!hasInterceptCache &&
|
|
381
424
|
!isLeavingIntercept &&
|
|
@@ -460,10 +503,23 @@ export function createNavigationBridge(
|
|
|
460
503
|
...(hasUsableCache
|
|
461
504
|
? {
|
|
462
505
|
targetCacheSegments: cachedSegments,
|
|
463
|
-
|
|
506
|
+
// Read at commit: a push the page on screen gets while
|
|
507
|
+
// the request is in flight is in it.
|
|
508
|
+
targetCacheHandleData: () =>
|
|
509
|
+
withSegmentHandles(
|
|
510
|
+
cached?.handleData ?? {},
|
|
511
|
+
liveHandleDataForCache(
|
|
512
|
+
eventController,
|
|
513
|
+
store,
|
|
514
|
+
sourceHistoryKey,
|
|
515
|
+
),
|
|
516
|
+
fromSource,
|
|
517
|
+
),
|
|
464
518
|
}
|
|
465
519
|
: {}),
|
|
466
|
-
|
|
520
|
+
...(clientUrlPresentation && {
|
|
521
|
+
presentation: clientUrlPresentation,
|
|
522
|
+
}),
|
|
467
523
|
},
|
|
468
524
|
);
|
|
469
525
|
} catch (error) {
|
|
@@ -580,7 +636,9 @@ export function createNavigationBridge(
|
|
|
580
636
|
* Uses cached segments when available for instant restoration
|
|
581
637
|
*/
|
|
582
638
|
async handlePopstate(): Promise<void> {
|
|
583
|
-
// Abort any pending navigation to prevent race conditions
|
|
639
|
+
// Abort any pending navigation to prevent race conditions, and the
|
|
640
|
+
// fill of the page being left (prefetch: false), which its committed
|
|
641
|
+
// navigation still owns.
|
|
584
642
|
eventController.abortNavigation();
|
|
585
643
|
handleTraversalStart();
|
|
586
644
|
|
|
@@ -648,7 +706,7 @@ export function createNavigationBridge(
|
|
|
648
706
|
// revalidates (SWR) instead of serving it as fresh.
|
|
649
707
|
const isStale = (cached?.stale ?? false) || isActionFenceActive();
|
|
650
708
|
|
|
651
|
-
if (cachedSegments
|
|
709
|
+
if (isRestorable(cachedSegments)) {
|
|
652
710
|
// transition({ when }) decides the restore (kind "pop") against the
|
|
653
711
|
// entry being left, so before the store moves to the restored entry.
|
|
654
712
|
// The decision the entry was committed with is not reused: it was
|
|
@@ -818,8 +876,12 @@ export function createNavigationBridge(
|
|
|
818
876
|
? { type: "leave-intercept" }
|
|
819
877
|
: undefined,
|
|
820
878
|
);
|
|
821
|
-
// Restore scroll position after fetch completes
|
|
822
|
-
|
|
879
|
+
// Restore scroll position after fetch completes. A traversal that
|
|
880
|
+
// was left returns here too, its response ignored: it restores
|
|
881
|
+
// nothing.
|
|
882
|
+
if (!tx.handle.signal.aborted) {
|
|
883
|
+
handleNavigationEnd({ restore: true, isStreaming });
|
|
884
|
+
}
|
|
823
885
|
} catch (error) {
|
|
824
886
|
// Aborted or superseded by a newer navigation: bail without clobbering
|
|
825
887
|
// content with a boundary (see navigate()).
|
|
@@ -888,13 +950,16 @@ export function createNavigationBridge(
|
|
|
888
950
|
// 3. The user presses back and the browser restores from bfcache
|
|
889
951
|
// At that point, currentNavigation is still set from step 1, so
|
|
890
952
|
// getState() returns "loading" and the progress bar shows.
|
|
891
|
-
// Abort the stale navigation to reset state to idle.
|
|
953
|
+
// Abort the stale navigation to reset state to idle. Only that one: the
|
|
954
|
+
// page restored is not being left, so a committed navigation's
|
|
955
|
+
// prefetch: false fill keeps its signal, and fails as a navigation if
|
|
956
|
+
// the browser dropped its request while the page was frozen.
|
|
892
957
|
const handlePageShow = (event: PageTransitionEvent) => {
|
|
893
958
|
if (event.persisted) {
|
|
894
959
|
debugLog(
|
|
895
960
|
"[Browser] Page restored from bfcache, resetting navigation state",
|
|
896
961
|
);
|
|
897
|
-
eventController.abortNavigation();
|
|
962
|
+
eventController.abortNavigation(true);
|
|
898
963
|
}
|
|
899
964
|
};
|
|
900
965
|
|
|
@@ -933,6 +998,7 @@ export function createNavigationBridge(
|
|
|
933
998
|
store.clearHistoryCache();
|
|
934
999
|
},
|
|
935
1000
|
};
|
|
1001
|
+
return bridge;
|
|
936
1002
|
}
|
|
937
1003
|
|
|
938
1004
|
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 {
|