@rangojs/router 0.32.0 → 0.33.1
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/README.md +3 -2
- package/dist/bin/browser-7KIJ5HSV.js +15 -0
- package/dist/bin/chunk-3B7CKBRO.js +463 -0
- package/dist/bin/chunk-524VACCG.js +71 -0
- package/dist/bin/chunk-LD7UBHT4.js +196 -0
- package/dist/bin/chunk-TO6CRWLN.js +1429 -0
- package/dist/bin/chunk-TTXC2U3G.js +2477 -0
- package/dist/bin/client-urls-projection-DQLWPDAT.js +28 -0
- package/dist/bin/forward-user-plugins-URBZKJP4.js +62 -0
- package/dist/bin/rango.js +42 -10173
- package/dist/bin/runtime-EPUDEHO4.js +596 -0
- package/dist/bin/runtime-discovery-3HH6BP3K.js +352 -0
- package/dist/bin/state-ACD2MGKJ.js +16 -0
- package/dist/bin/temp-rsc-server-JALTIWLT.js +3190 -0
- package/dist/types/bin/rango.d.ts +1 -0
- package/dist/types/browser/action-fence.d.ts +20 -0
- package/dist/types/browser/cookie-name.d.ts +51 -10
- package/dist/types/browser/invalidate-client-cache.d.ts +8 -6
- package/dist/types/browser/navigation-bridge.d.ts +36 -1
- package/dist/types/browser/navigation-client.d.ts +19 -0
- package/dist/types/browser/navigation-store-handle.d.ts +37 -4
- package/dist/types/browser/navigation-store.d.ts +2 -2
- package/dist/types/browser/navigation-transaction.d.ts +0 -1
- package/dist/types/browser/partial-update.d.ts +13 -0
- package/dist/types/browser/prefetch/cache.d.ts +96 -8
- package/dist/types/browser/prefetch/fetch.d.ts +26 -2
- package/dist/types/browser/prefetch/queue.d.ts +3 -2
- package/dist/types/browser/rango-state.d.ts +34 -5
- package/dist/types/browser/react/Link.d.ts +15 -6
- package/dist/types/browser/types.d.ts +25 -0
- package/dist/types/build/generate-route-types.d.ts +1 -1
- package/dist/types/build/route-types/include-resolution.d.ts +9 -1
- package/dist/types/build/route-types/router-processing.d.ts +3 -1
- package/dist/types/cache/cache-key-utils.d.ts +12 -8
- package/dist/types/cache/cache-scope.d.ts +4 -2
- package/dist/types/cache/cf/cf-cache-types.d.ts +9 -5
- package/dist/types/cache/document-cache-policy.d.ts +9 -1
- package/dist/types/cache/memory-segment-store.d.ts +3 -2
- package/dist/types/cache/segment-codec.d.ts +2 -2
- package/dist/types/cache/shell-snapshot.d.ts +1 -1
- package/dist/types/cache/types.d.ts +12 -7
- package/dist/types/cache/vercel/vercel-cache-store.d.ts +2 -1
- package/dist/types/client-urls/types.d.ts +7 -5
- package/dist/types/prefetch-markers.d.ts +9 -0
- package/dist/types/prefetch-source-scope.d.ts +66 -0
- package/dist/types/route-definition/helpers-types.d.ts +8 -5
- package/dist/types/router/intercept-resolution.d.ts +25 -11
- package/dist/types/router/match-context.d.ts +1 -0
- package/dist/types/router/match-handlers.d.ts +1 -1
- package/dist/types/router/prefetch-cache-ttl.d.ts +11 -3
- package/dist/types/router/router-context.d.ts +1 -1
- package/dist/types/router/router-options.d.ts +6 -3
- package/dist/types/router/segment-resolution/loader-cache.d.ts +8 -0
- package/dist/types/router/segment-resolution/prefetch-deferral.d.ts +23 -7
- package/dist/types/router/segment-resolution/revalidation.d.ts +19 -0
- package/dist/types/router/source-reads.d.ts +53 -0
- package/dist/types/router/types.d.ts +1 -1
- package/dist/types/rsc/helpers.d.ts +5 -2
- package/dist/types/rsc/prefetch-render-error.d.ts +7 -2
- package/dist/types/rsc/request-stages.d.ts +2 -0
- package/dist/types/rsc/shell-serve.d.ts +6 -1
- package/dist/types/server/context.d.ts +23 -6
- package/dist/types/server/cookie-store.d.ts +15 -1
- package/dist/types/server/document-location-state.d.ts +5 -2
- package/dist/types/server/handle-store.d.ts +2 -0
- package/dist/types/server/request-context.d.ts +43 -7
- package/dist/types/testing/e2e/server.d.ts +16 -1
- package/dist/types/testing/render-route.d.ts +66 -2
- package/dist/types/testing/serve-shell-request.d.ts +6 -3
- package/dist/types/types/handler-context.d.ts +7 -5
- package/dist/types/types/loader-types.d.ts +7 -4
- package/dist/types/types/segments.d.ts +15 -4
- package/dist/types/urls/path-helper-types.d.ts +7 -5
- package/dist/types/vite/temp-rsc-server.d.ts +2 -2
- package/dist/vite/index.js +449 -189
- package/package.json +2 -2
- package/skills/app-structure/SKILL.md +81 -0
- package/skills/app-structure/e-commerce.md +10 -9
- package/skills/app-structure/large-app.md +18 -2
- package/skills/caching/SKILL.md +10 -5
- package/skills/client-urls/SKILL.md +29 -11
- package/skills/comparison/references/framework-comparison.md +13 -8
- package/skills/document-cache/SKILL.md +8 -0
- package/skills/hooks/navigation.md +8 -8
- package/skills/hooks/outlets.md +4 -0
- package/skills/hooks/state.md +24 -8
- package/skills/intercept/SKILL.md +33 -12
- package/skills/layout/SKILL.md +60 -0
- package/skills/links/SKILL.md +101 -4
- package/skills/loader/SKILL.md +49 -22
- package/skills/migrate-nextjs/SKILL.md +8 -2
- package/skills/migrate-react-router/data-and-actions.md +5 -0
- package/skills/ppr/SKILL.md +26 -5
- package/skills/prerender/SKILL.md +4 -1
- package/skills/route/SKILL.md +6 -6
- package/skills/router-setup/SKILL.md +2 -1
- package/skills/server-actions/SKILL.md +41 -8
- package/skills/testing/client-components.md +29 -13
- package/skills/testing/serve-shell-request.md +7 -7
- package/skills/testing/server-actions.md +8 -0
- package/skills/ux-architecture/SKILL.md +70 -19
- package/src/bin/rango.ts +39 -13
- package/src/browser/action-fence.ts +72 -0
- package/src/browser/cookie-name.ts +82 -14
- package/src/browser/invalidate-client-cache.ts +9 -7
- package/src/browser/navigation-bridge.ts +164 -32
- package/src/browser/navigation-client.ts +95 -20
- package/src/browser/navigation-store-handle.ts +67 -5
- package/src/browser/navigation-store.ts +86 -17
- package/src/browser/navigation-transaction.ts +0 -6
- package/src/browser/partial-update.ts +31 -12
- package/src/browser/prefetch/cache.ts +188 -11
- package/src/browser/prefetch/fetch.ts +211 -51
- package/src/browser/prefetch/queue.ts +3 -2
- package/src/browser/rango-state.ts +50 -8
- package/src/browser/react/Link.tsx +20 -9
- package/src/browser/react/use-navigation.ts +1 -1
- package/src/browser/rsc-router.tsx +8 -1
- package/src/browser/server-action-bridge.ts +16 -11
- package/src/browser/types.ts +25 -0
- package/src/build/generate-route-types.ts +1 -0
- package/src/build/route-types/include-resolution.ts +40 -3
- package/src/build/route-types/router-processing.ts +170 -25
- package/src/build/route-types/scan-filter.ts +1 -0
- package/src/cache/cache-key-utils.ts +12 -8
- package/src/cache/cache-runtime.ts +5 -1
- package/src/cache/cache-scope.ts +28 -7
- package/src/cache/cf/cf-cache-types.ts +9 -5
- package/src/cache/document-cache-policy.ts +27 -0
- package/src/cache/document-cache.ts +5 -0
- package/src/cache/memory-segment-store.ts +3 -2
- package/src/cache/segment-codec.ts +2 -2
- package/src/cache/shell-snapshot.ts +1 -1
- package/src/cache/types.ts +12 -7
- package/src/cache/vercel/vercel-cache-store.ts +2 -1
- package/src/client-urls/types.ts +7 -5
- package/src/prefetch-markers.ts +10 -0
- package/src/prefetch-source-scope.ts +137 -0
- package/src/route-definition/helpers-types.ts +8 -5
- package/src/router/intercept-resolution.ts +94 -50
- package/src/router/loader-resolution.ts +8 -6
- package/src/router/match-api.ts +33 -29
- package/src/router/match-context.ts +5 -0
- package/src/router/match-handlers.ts +2 -1
- package/src/router/match-middleware/cache-lookup.ts +21 -13
- package/src/router/match-result.ts +39 -87
- package/src/router/navigation-snapshot.ts +18 -3
- package/src/router/prefetch-cache-ttl.ts +15 -3
- package/src/router/revalidation.ts +53 -23
- package/src/router/router-context.ts +2 -1
- package/src/router/router-options.ts +6 -3
- package/src/router/segment-resolution/loader-cache.ts +21 -3
- package/src/router/segment-resolution/prefetch-deferral.ts +38 -31
- package/src/router/segment-resolution/revalidation.ts +40 -12
- package/src/router/source-reads.ts +122 -0
- package/src/router/types.ts +2 -1
- package/src/router.ts +11 -12
- package/src/rsc/helpers.ts +8 -3
- package/src/rsc/prefetch-render-error.ts +7 -2
- package/src/rsc/progressive-enhancement.ts +3 -2
- package/src/rsc/request-stages.ts +59 -0
- package/src/rsc/rsc-rendering.ts +49 -33
- package/src/rsc/server-action.ts +2 -1
- package/src/rsc/shell-capture.ts +4 -5
- package/src/rsc/shell-serve.ts +12 -1
- package/src/segment-system.tsx +1 -1
- package/src/server/context.ts +37 -6
- package/src/server/cookie-store.ts +22 -1
- package/src/server/document-location-state.ts +23 -2
- package/src/server/handle-store.ts +2 -0
- package/src/server/request-context.ts +113 -28
- package/src/testing/dispatch.ts +3 -0
- package/src/testing/e2e/fixture.ts +4 -12
- package/src/testing/e2e/server.ts +91 -4
- package/src/testing/render-route.tsx +269 -4
- package/src/testing/serve-shell-request.ts +6 -3
- package/src/types/handler-context.ts +7 -5
- package/src/types/loader-types.ts +7 -4
- package/src/types/segments.ts +15 -4
- package/src/urls/path-helper-types.ts +7 -5
- package/src/vite/temp-rsc-server.ts +2 -2
|
@@ -16,28 +16,39 @@
|
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
18
|
import {
|
|
19
|
+
buildParamsKey,
|
|
19
20
|
buildPrefetchKey,
|
|
20
21
|
buildSourceKey,
|
|
21
22
|
hasPrefetch,
|
|
23
|
+
paramsCandidateKeys,
|
|
24
|
+
rememberParamsShape,
|
|
22
25
|
markPrefetchInflight,
|
|
23
26
|
setInflightPromiseWithAliases,
|
|
24
27
|
storePrefetch,
|
|
25
28
|
removePrefetch,
|
|
26
29
|
clearPrefetchInflight,
|
|
27
30
|
currentGeneration,
|
|
31
|
+
currentPrefetchEpoch,
|
|
28
32
|
isFragmentPassthroughEnabled,
|
|
33
|
+
isPrefetchStale,
|
|
29
34
|
type DecodedPrefetch,
|
|
35
|
+
type PrefetchScope,
|
|
30
36
|
} from "./cache.js";
|
|
37
|
+
import { paramsScopeLabel } from "../../prefetch-source-scope.js";
|
|
31
38
|
import { getRangoState } from "../rango-state.js";
|
|
39
|
+
import { stateEpoch } from "../cookie-name.js";
|
|
32
40
|
import { isActionFenceActive } from "../action-fence.js";
|
|
33
41
|
import { enqueuePrefetch } from "./queue.js";
|
|
34
42
|
import { shouldPrefetch } from "./policy.js";
|
|
35
43
|
import { debugLog, IS_BROWSER_DEBUG } from "../logging.js";
|
|
36
44
|
import { teeWithCompletion, isForeignRouterId } from "../response-adapter.js";
|
|
37
45
|
import { SEGMENT_FRAGMENT_CAPABILITY_HEADER } from "../../segment-fragments.js";
|
|
38
|
-
import { PREFETCH_HEADER } from "../../prefetch-markers.js";
|
|
46
|
+
import { PREFETCH_HEADER, PREFETCH_INTENT } from "../../prefetch-markers.js";
|
|
39
47
|
import type { RscPayload } from "../types.js";
|
|
40
|
-
import {
|
|
48
|
+
import {
|
|
49
|
+
getCommittedPage,
|
|
50
|
+
getRegisteredStore,
|
|
51
|
+
} from "../navigation-store-handle.js";
|
|
41
52
|
import { writePartialParams } from "../wire-protocol.js";
|
|
42
53
|
|
|
43
54
|
/**
|
|
@@ -87,7 +98,8 @@ export function setPrefetchDecoder(fn: PrefetchDecoder): void {
|
|
|
87
98
|
function makeRespawn(
|
|
88
99
|
storageKey: string,
|
|
89
100
|
reserve: ReadableStream<Uint8Array>,
|
|
90
|
-
|
|
101
|
+
slot: PrefetchSlot,
|
|
102
|
+
epoch: number,
|
|
91
103
|
): () => DecodedPrefetch {
|
|
92
104
|
return () => {
|
|
93
105
|
const [body, nextReserve] = reserve.tee();
|
|
@@ -98,7 +110,9 @@ function makeRespawn(
|
|
|
98
110
|
const entry: DecodedPrefetch = {
|
|
99
111
|
payload,
|
|
100
112
|
streamComplete: Promise.resolve(),
|
|
101
|
-
scope,
|
|
113
|
+
scope: slot.scope,
|
|
114
|
+
paramsLabel: slot.paramsLabel,
|
|
115
|
+
epoch,
|
|
102
116
|
complete: true,
|
|
103
117
|
dispose: () => {
|
|
104
118
|
// Rejects (caught) instead of throwing if the branch is already
|
|
@@ -107,7 +121,7 @@ function makeRespawn(
|
|
|
107
121
|
nextReserve.cancel().catch(() => {});
|
|
108
122
|
},
|
|
109
123
|
};
|
|
110
|
-
entry.respawn = makeRespawn(storageKey, nextReserve,
|
|
124
|
+
entry.respawn = makeRespawn(storageKey, nextReserve, slot, epoch);
|
|
111
125
|
// Parity with the original path: a rejected decode must not stay
|
|
112
126
|
// consumable. Clean bytes should never fail to decode, but if they do,
|
|
113
127
|
// evict (identity-guarded) so navigation refetches instead.
|
|
@@ -117,17 +131,36 @@ function makeRespawn(
|
|
|
117
131
|
}
|
|
118
132
|
|
|
119
133
|
/**
|
|
120
|
-
*
|
|
134
|
+
* The page a prefetch is sent from: the URL the router store committed, the
|
|
135
|
+
* page whose segment ids every caller sends. Not window.location: a popstate
|
|
136
|
+
* moves it to the popped entry before that entry's fetch commits, and a
|
|
137
|
+
* prefetch sent from there with the page on screen's ids was judged against
|
|
138
|
+
* the wrong page, so a layout kept for one page showed the other's copy.
|
|
139
|
+
* Absolute, as navigation-client.ts makes the URL navigation sends
|
|
140
|
+
* (partial-update.ts previousUrl), so the source keys agree.
|
|
141
|
+
*/
|
|
142
|
+
function committedHref(): string {
|
|
143
|
+
const committed = getRegisteredStore()?.getSegmentState().currentUrl;
|
|
144
|
+
return committed
|
|
145
|
+
? new URL(committed, window.location.origin).href
|
|
146
|
+
: window.location.href;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function withoutHash(href: string): string {
|
|
150
|
+
const hash = href.indexOf("#");
|
|
151
|
+
return hash === -1 ? href : href.slice(0, hash);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Check if a URL resolves to the committed page (same pathname + search).
|
|
121
156
|
* Used to prevent same-page prefetching, which produces a trivial diff
|
|
122
157
|
* that would corrupt the (default wildcard) prefetch cache entry.
|
|
123
158
|
*/
|
|
124
|
-
function isSamePage(url: string): boolean {
|
|
159
|
+
function isSamePage(url: string, sourceHref: string): boolean {
|
|
125
160
|
try {
|
|
126
161
|
const target = new URL(url, window.location.origin);
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
window.location.pathname + window.location.search
|
|
130
|
-
);
|
|
162
|
+
const source = new URL(sourceHref, window.location.origin);
|
|
163
|
+
return target.pathname + target.search === source.pathname + source.search;
|
|
131
164
|
} catch {
|
|
132
165
|
return false;
|
|
133
166
|
}
|
|
@@ -165,6 +198,41 @@ function buildPrefetchUrl(
|
|
|
165
198
|
});
|
|
166
199
|
}
|
|
167
200
|
|
|
201
|
+
/** Where a completed prefetch is stored, and the label of a params scope. */
|
|
202
|
+
export interface PrefetchSlot {
|
|
203
|
+
scope: PrefetchScope;
|
|
204
|
+
key: string;
|
|
205
|
+
paramsLabel?: string;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Where a completed prefetch is stored: under the source key when the Link
|
|
210
|
+
* forced it or the response says `X-RSC-Prefetch-Scope: source`; under the
|
|
211
|
+
* label's key when it says `params; <label>`, for the pages with equal values
|
|
212
|
+
* of what a revalidate() rule read; else under the key every source page
|
|
213
|
+
* shares (#1331). `stateKey` is the state's epoch the other keys were built
|
|
214
|
+
* with (cookie-name.ts stateEpoch).
|
|
215
|
+
*/
|
|
216
|
+
export function prefetchStorageSlot(
|
|
217
|
+
forceSourceScope: boolean,
|
|
218
|
+
scopeHeader: string | null,
|
|
219
|
+
sourceKey: string,
|
|
220
|
+
wildcardKey: string,
|
|
221
|
+
stateKey: string,
|
|
222
|
+
targetUrl: URL,
|
|
223
|
+
): PrefetchSlot {
|
|
224
|
+
if (forceSourceScope || scopeHeader === "source") {
|
|
225
|
+
return { scope: "source", key: sourceKey };
|
|
226
|
+
}
|
|
227
|
+
const label = paramsScopeLabel(scopeHeader);
|
|
228
|
+
if (label === undefined) return { scope: "wildcard", key: wildcardKey };
|
|
229
|
+
return {
|
|
230
|
+
scope: "params",
|
|
231
|
+
key: buildParamsKey(stateKey, label, targetUrl),
|
|
232
|
+
paramsLabel: label,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
168
236
|
/**
|
|
169
237
|
* Core prefetch fetch logic. Fetches the response, eagerly decodes it, and
|
|
170
238
|
* stores the decoded payload in the in-memory cache. The returned Promise
|
|
@@ -191,21 +259,38 @@ function buildPrefetchUrl(
|
|
|
191
259
|
* `sourceKey` so same-source navigations adopt directly via their own
|
|
192
260
|
* source key. Storage key is chosen at response time from the
|
|
193
261
|
* `X-RSC-Prefetch-Scope` header — `"source"` → `sourceKey` (a route an
|
|
194
|
-
* intercept targets),
|
|
195
|
-
*
|
|
196
|
-
*
|
|
262
|
+
* intercept targets), `"params; <label>"` → the label's key, anything else
|
|
263
|
+
* → `wildcardKey`. The entry records its scope (and label) so cross-source
|
|
264
|
+
* navigations that adopted via `wildcardKey` can bail out in
|
|
265
|
+
* `navigation-client.ts` when the adopted entry turns out source-scoped,
|
|
266
|
+
* or params-scoped to a group their page is not in.
|
|
197
267
|
*/
|
|
198
268
|
function executePrefetchFetch(
|
|
199
269
|
wildcardKey: string,
|
|
200
270
|
sourceKey: string,
|
|
201
|
-
|
|
271
|
+
/** The committed page sent as the source, the one `sourceKey` names. */
|
|
272
|
+
sourceHref: string,
|
|
273
|
+
targetUrl: URL,
|
|
202
274
|
forceSourceScope: boolean,
|
|
203
|
-
/**
|
|
275
|
+
/**
|
|
276
|
+
* Rango state captured once by the caller (keys + header share one read).
|
|
277
|
+
* The header sends it whole; the keys hold its epoch.
|
|
278
|
+
*/
|
|
204
279
|
rangoState: string,
|
|
280
|
+
/**
|
|
281
|
+
* An intent prefetch (prefetchDirect: hover, `prefetch()`): sent as
|
|
282
|
+
* PREFETCH_INTENT so the server defers nothing, as in the navigation it
|
|
283
|
+
* starts early. A speculative one (prefetchQueued) sends `1` and lets the
|
|
284
|
+
* server defer `prefetch: false` work (docs/design/prefetch-false.md, R16).
|
|
285
|
+
*/
|
|
286
|
+
intent: boolean,
|
|
205
287
|
expectedRouterId?: string,
|
|
206
288
|
signal?: AbortSignal,
|
|
207
289
|
): Promise<DecodedPrefetch | null> {
|
|
208
290
|
const gen = currentGeneration();
|
|
291
|
+
// An action that lands while this fetch is out marks it stale without
|
|
292
|
+
// aborting it: it finishes and is stored stale (cache.ts isPrefetchStale).
|
|
293
|
+
const epoch = currentPrefetchEpoch();
|
|
209
294
|
const inflightKeys = forceSourceScope
|
|
210
295
|
? [sourceKey]
|
|
211
296
|
: [wildcardKey, sourceKey];
|
|
@@ -253,7 +338,7 @@ function executePrefetchFetch(
|
|
|
253
338
|
});
|
|
254
339
|
}
|
|
255
340
|
|
|
256
|
-
const promise: Promise<DecodedPrefetch | null> = fetch(
|
|
341
|
+
const promise: Promise<DecodedPrefetch | null> = fetch(targetUrl.toString(), {
|
|
257
342
|
priority: "low" as RequestPriority,
|
|
258
343
|
// During an action's flight the state is not rotated, so the old
|
|
259
344
|
// X-Rango-State still matches the Vary-keyed HTTP-cache entry; bypass it so
|
|
@@ -264,8 +349,8 @@ function executePrefetchFetch(
|
|
|
264
349
|
signal: effectiveSignal,
|
|
265
350
|
headers: {
|
|
266
351
|
"X-Rango-State": rangoState,
|
|
267
|
-
"X-RSC-Router-Client-Path":
|
|
268
|
-
[PREFETCH_HEADER]: "1",
|
|
352
|
+
"X-RSC-Router-Client-Path": sourceHref,
|
|
353
|
+
[PREFETCH_HEADER]: intent ? PREFETCH_INTENT : "1",
|
|
269
354
|
...(isFragmentPassthroughEnabled() && {
|
|
270
355
|
[SEGMENT_FRAGMENT_CAPABILITY_HEADER]: "1",
|
|
271
356
|
}),
|
|
@@ -288,12 +373,18 @@ function executePrefetchFetch(
|
|
|
288
373
|
return null;
|
|
289
374
|
}
|
|
290
375
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
376
|
+
// The one place the storage key is decided: from the header, with the
|
|
377
|
+
// values the server read. A key computed when the fetch was scheduled
|
|
378
|
+
// and written again later would undo it (vercel/next.js#91488).
|
|
379
|
+
const slot = prefetchStorageSlot(
|
|
380
|
+
forceSourceScope,
|
|
381
|
+
response.headers.get("x-rsc-prefetch-scope"),
|
|
382
|
+
sourceKey,
|
|
383
|
+
wildcardKey,
|
|
384
|
+
stateEpoch(rangoState),
|
|
385
|
+
targetUrl,
|
|
386
|
+
);
|
|
387
|
+
const storageKey = slot.key;
|
|
297
388
|
|
|
298
389
|
// Track stream completion off a tee so navigation's scroll/revalidation
|
|
299
390
|
// gating matches the fresh-fetch path; decode the other branch. The
|
|
@@ -340,7 +431,9 @@ function executePrefetchFetch(
|
|
|
340
431
|
const entry: DecodedPrefetch = {
|
|
341
432
|
payload,
|
|
342
433
|
streamComplete,
|
|
343
|
-
scope,
|
|
434
|
+
scope: slot.scope,
|
|
435
|
+
paramsLabel: slot.paramsLabel,
|
|
436
|
+
epoch,
|
|
344
437
|
complete: false,
|
|
345
438
|
stallTimer: timeoutId,
|
|
346
439
|
...(reserve && {
|
|
@@ -349,7 +442,10 @@ function executePrefetchFetch(
|
|
|
349
442
|
},
|
|
350
443
|
}),
|
|
351
444
|
};
|
|
352
|
-
storePrefetch(storageKey, entry, gen);
|
|
445
|
+
storePrefetch(storageKey, entry, gen, inflightKeys[0]);
|
|
446
|
+
if (slot.paramsLabel !== undefined) {
|
|
447
|
+
rememberParamsShape(targetUrl, slot.paramsLabel);
|
|
448
|
+
}
|
|
353
449
|
// The stall timeout now owns the body stream: arm eviction (publishedKey)
|
|
354
450
|
// and clear the timer once the stream completes. The tee's finally resolves
|
|
355
451
|
// streamComplete on normal completion AND on abort, so a healthy body pays
|
|
@@ -375,7 +471,7 @@ function executePrefetchFetch(
|
|
|
375
471
|
if (decode.status === "fulfilled" && endedCleanly) {
|
|
376
472
|
entry.complete = true;
|
|
377
473
|
if (reserve) {
|
|
378
|
-
entry.respawn = makeRespawn(storageKey, reserve,
|
|
474
|
+
entry.respawn = makeRespawn(storageKey, reserve, slot, epoch);
|
|
379
475
|
// Refill after an early adoption. A click that lands before this
|
|
380
476
|
// point adopts the entry with respawn unarmed and leaves the slot
|
|
381
477
|
// empty — via either path: pre-headers inflight adoption makes
|
|
@@ -386,7 +482,11 @@ function executePrefetchFetch(
|
|
|
386
482
|
// live again (our own un-adopted entry, a fresh entry, or a new
|
|
387
483
|
// in-flight prefetch), and storePrefetch's generation check drops
|
|
388
484
|
// the sibling if the cache was invalidated since the fetch began.
|
|
389
|
-
|
|
485
|
+
// A stale entry is served at most once (cache.ts consumePrefetch),
|
|
486
|
+
// so an adopted stale one is not refilled: the next click fetches
|
|
487
|
+
// fresh. The isPrefetchStale check also keeps hasPrefetch from
|
|
488
|
+
// dropping (and disposing) our own un-adopted entry once stale.
|
|
489
|
+
if (!isPrefetchStale(entry) && !hasPrefetch(storageKey)) {
|
|
390
490
|
storePrefetch(storageKey, entry.respawn(), gen);
|
|
391
491
|
}
|
|
392
492
|
}
|
|
@@ -409,7 +509,8 @@ function executePrefetchFetch(
|
|
|
409
509
|
}
|
|
410
510
|
|
|
411
511
|
/**
|
|
412
|
-
* Dedup check for prefetch entry presence
|
|
512
|
+
* Dedup check for prefetch entry presence: the wildcard key, the source key,
|
|
513
|
+
* or a params-scoped answer whose group the page on screen is in.
|
|
413
514
|
*
|
|
414
515
|
* Forced `:source` must NOT dedupe against a pre-existing wildcard entry —
|
|
415
516
|
* otherwise the source slot would stay unpopulated and navigation from
|
|
@@ -420,15 +521,29 @@ function hasPrefetchHit(
|
|
|
420
521
|
forceSourceScope: boolean,
|
|
421
522
|
wildcardKey: string,
|
|
422
523
|
sourceKey: string,
|
|
524
|
+
stateKey: string,
|
|
525
|
+
targetUrl: URL,
|
|
423
526
|
): boolean {
|
|
424
|
-
return
|
|
425
|
-
|
|
426
|
-
|
|
527
|
+
if (forceSourceScope) return hasPrefetch(sourceKey);
|
|
528
|
+
return (
|
|
529
|
+
hasPrefetch(wildcardKey) ||
|
|
530
|
+
hasPrefetch(sourceKey) ||
|
|
531
|
+
paramsCandidateKeys(stateKey, targetUrl, getCommittedPage()).some(
|
|
532
|
+
hasPrefetch,
|
|
533
|
+
)
|
|
534
|
+
);
|
|
427
535
|
}
|
|
428
536
|
|
|
429
537
|
/**
|
|
430
538
|
* Prefetch (direct): fetch with low priority and store in in-memory cache.
|
|
431
|
-
* Used by hover
|
|
539
|
+
* Used by hover (on hover and viewport links, and delegated anchors) and
|
|
540
|
+
* useRouter().prefetch() -- fires immediately without queueing.
|
|
541
|
+
*
|
|
542
|
+
* An intent prefetch: the visitor is about to click, so it asks for the full
|
|
543
|
+
* response the navigation would get, `prefetch: false` loaders and flagged
|
|
544
|
+
* `loading()` work included. It runs only on a miss: a cached prefetch or one
|
|
545
|
+
* in flight, even a speculative one that deferred work, is used as it is,
|
|
546
|
+
* and its click sends the fill as before (docs/design/prefetch-false.md, R16).
|
|
432
547
|
*
|
|
433
548
|
* By default the wildcard key (Rango-state-keyed) is used for inflight
|
|
434
549
|
* dedup and for responses that are not source-sensitive; source-scoped
|
|
@@ -450,18 +565,27 @@ export function prefetchDirect(
|
|
|
450
565
|
const targetUrl = buildPrefetchUrl(url, segmentIds, version, routerId);
|
|
451
566
|
if (!targetUrl) return;
|
|
452
567
|
const forceSourceScope = prefetchKey === ":source";
|
|
568
|
+
const sourceHref = committedHref();
|
|
453
569
|
// Skip same-page prefetch — a same-page diff is trivial and would corrupt
|
|
454
570
|
// the wildcard cache entry used for cross-page navigation.
|
|
455
571
|
// When `:source` is forced the entry is source-scoped (single-aliased to
|
|
456
572
|
// itself), so it cannot poison any shared slot — allow it.
|
|
457
|
-
if (!forceSourceScope && isSamePage(url)) {
|
|
573
|
+
if (!forceSourceScope && isSamePage(url, sourceHref)) {
|
|
458
574
|
return;
|
|
459
575
|
}
|
|
460
|
-
const sourceHref = window.location.href;
|
|
461
576
|
const rangoState = getRangoState();
|
|
462
|
-
const
|
|
463
|
-
const
|
|
464
|
-
|
|
577
|
+
const stateKey = stateEpoch(rangoState);
|
|
578
|
+
const wildcardKey = buildPrefetchKey(stateKey, targetUrl);
|
|
579
|
+
const sourceKey = buildSourceKey(stateKey, sourceHref, targetUrl);
|
|
580
|
+
if (
|
|
581
|
+
hasPrefetchHit(
|
|
582
|
+
forceSourceScope,
|
|
583
|
+
wildcardKey,
|
|
584
|
+
sourceKey,
|
|
585
|
+
stateKey,
|
|
586
|
+
targetUrl,
|
|
587
|
+
)
|
|
588
|
+
) {
|
|
465
589
|
if (IS_BROWSER_DEBUG) {
|
|
466
590
|
debugLog("[prefetch] direct dedup (key already exists)", {
|
|
467
591
|
url,
|
|
@@ -484,16 +608,19 @@ export function prefetchDirect(
|
|
|
484
608
|
executePrefetchFetch(
|
|
485
609
|
wildcardKey,
|
|
486
610
|
sourceKey,
|
|
487
|
-
|
|
611
|
+
sourceHref,
|
|
612
|
+
targetUrl,
|
|
488
613
|
forceSourceScope,
|
|
489
614
|
rangoState,
|
|
615
|
+
true,
|
|
490
616
|
routerId,
|
|
491
617
|
);
|
|
492
618
|
}
|
|
493
619
|
|
|
494
620
|
/**
|
|
495
621
|
* Prefetch (queued): goes through the concurrency-limited queue.
|
|
496
|
-
* Used by viewport/render strategies to avoid flooding the server.
|
|
622
|
+
* Used by viewport/render strategies to avoid flooding the server. A
|
|
623
|
+
* speculative prefetch: the server defers `prefetch: false` work in it.
|
|
497
624
|
* Returns the inflight key (wildcard by default, source-scoped when
|
|
498
625
|
* `prefetchKey=":source"` is passed).
|
|
499
626
|
*/
|
|
@@ -508,15 +635,24 @@ export function prefetchQueued(
|
|
|
508
635
|
const targetUrl = buildPrefetchUrl(url, segmentIds, version, routerId);
|
|
509
636
|
if (!targetUrl) return "";
|
|
510
637
|
const forceSourceScope = prefetchKey === ":source";
|
|
511
|
-
|
|
638
|
+
const sourceHref = committedHref();
|
|
639
|
+
if (!forceSourceScope && isSamePage(url, sourceHref)) {
|
|
512
640
|
return "";
|
|
513
641
|
}
|
|
514
|
-
const sourceHref = window.location.href;
|
|
515
642
|
const rangoState = getRangoState();
|
|
516
|
-
const
|
|
517
|
-
const
|
|
643
|
+
const stateKey = stateEpoch(rangoState);
|
|
644
|
+
const wildcardKey = buildPrefetchKey(stateKey, targetUrl);
|
|
645
|
+
const sourceKey = buildSourceKey(stateKey, sourceHref, targetUrl);
|
|
518
646
|
const queueKey = forceSourceScope ? sourceKey : wildcardKey;
|
|
519
|
-
if (
|
|
647
|
+
if (
|
|
648
|
+
hasPrefetchHit(
|
|
649
|
+
forceSourceScope,
|
|
650
|
+
wildcardKey,
|
|
651
|
+
sourceKey,
|
|
652
|
+
stateKey,
|
|
653
|
+
targetUrl,
|
|
654
|
+
)
|
|
655
|
+
) {
|
|
520
656
|
if (IS_BROWSER_DEBUG) {
|
|
521
657
|
debugLog("[prefetch] queued dedup (key already exists)", {
|
|
522
658
|
url,
|
|
@@ -527,22 +663,46 @@ export function prefetchQueued(
|
|
|
527
663
|
}
|
|
528
664
|
return queueKey;
|
|
529
665
|
}
|
|
530
|
-
const fetchUrlStr = targetUrl.toString();
|
|
531
666
|
enqueuePrefetch(queueKey, (signal) => {
|
|
667
|
+
// The header sends the state as it is now: a server action that landed
|
|
668
|
+
// while this sat in the queue bumped the write part, and the value read
|
|
669
|
+
// when it was queued would be answered by the browser HTTP cache with
|
|
670
|
+
// pre-write bytes. A rotation since then made the keys obsolete. Read
|
|
671
|
+
// before the re-check below: a read that sees another tab's write marks
|
|
672
|
+
// the entries from before it stale, so they no longer count as a hit.
|
|
673
|
+
const liveState = getRangoState();
|
|
674
|
+
if (stateEpoch(liveState) !== stateKey) return Promise.resolve();
|
|
532
675
|
// Re-check at execution time: a hover-triggered prefetchDirect may
|
|
533
676
|
// have started or completed this key while the item sat in the queue.
|
|
534
|
-
if (
|
|
677
|
+
if (
|
|
678
|
+
hasPrefetchHit(
|
|
679
|
+
forceSourceScope,
|
|
680
|
+
wildcardKey,
|
|
681
|
+
sourceKey,
|
|
682
|
+
stateKey,
|
|
683
|
+
targetUrl,
|
|
684
|
+
)
|
|
685
|
+
) {
|
|
535
686
|
return Promise.resolve();
|
|
536
687
|
}
|
|
537
|
-
|
|
688
|
+
// The segment ids and the source were read when it was queued: once the
|
|
689
|
+
// committed page has moved, they name a page no longer on screen.
|
|
690
|
+
// A hash-only change commits the same page, so the hash is not compared.
|
|
691
|
+
const committed = committedHref();
|
|
692
|
+
if (
|
|
693
|
+
withoutHash(committed) !== withoutHash(sourceHref) ||
|
|
694
|
+
(!forceSourceScope && isSamePage(url, committed))
|
|
695
|
+
) {
|
|
538
696
|
return Promise.resolve();
|
|
539
697
|
}
|
|
540
698
|
return executePrefetchFetch(
|
|
541
699
|
wildcardKey,
|
|
542
700
|
sourceKey,
|
|
543
|
-
|
|
701
|
+
sourceHref,
|
|
702
|
+
targetUrl,
|
|
544
703
|
forceSourceScope,
|
|
545
|
-
|
|
704
|
+
liveState,
|
|
705
|
+
false,
|
|
546
706
|
routerId,
|
|
547
707
|
signal,
|
|
548
708
|
).then(() => {});
|
|
@@ -188,8 +188,9 @@ export function cancelAllPrefetches(keepUrl?: string | null): void {
|
|
|
188
188
|
|
|
189
189
|
/**
|
|
190
190
|
* Hard-cancel everything including in-flight prefetches.
|
|
191
|
-
* Used by clearPrefetchCache (
|
|
192
|
-
* in-flight responses would be stale.
|
|
191
|
+
* Used by clearPrefetchCache (invalidateClientCache(), a deploy) where
|
|
192
|
+
* in-flight responses would be stale. A server action never calls it: it
|
|
193
|
+
* marks prefetches stale and lets in-flight ones finish (cache.ts).
|
|
193
194
|
*/
|
|
194
195
|
export function abortAllPrefetches(): void {
|
|
195
196
|
for (const ac of abortControllers.values()) {
|
|
@@ -6,11 +6,25 @@
|
|
|
6
6
|
* requests; the server responds with `Vary: X-Rango-State`, so the browser HTTP
|
|
7
7
|
* cache keys responses by (URL, X-Rango-State value).
|
|
8
8
|
*
|
|
9
|
-
* Value format: `{buildVersion}:{invalidationTimestamp}`
|
|
9
|
+
* Value format: `{buildVersion}:{invalidationTimestamp}[:{write}]`
|
|
10
10
|
* - Build version: the router's document version. It changes when a deploy
|
|
11
11
|
* changes the router's server code or the client assets, busting all cached
|
|
12
12
|
* prefetches at boot; a deploy that changes neither keeps them.
|
|
13
|
-
* - Timestamp rotates on invalidation (
|
|
13
|
+
* - Timestamp rotates on invalidation (invalidateClientCache, either seat).
|
|
14
|
+
* - Write: bumped by the response of a server action without a directive (its
|
|
15
|
+
* `Set-Cookie`, server side, so a no-JS action bumps it too); left out while
|
|
16
|
+
* 0. It changes the `X-Rango-State` every tab and every reload sends, so
|
|
17
|
+
* responses the browser HTTP cache holds from before the write miss.
|
|
18
|
+
*
|
|
19
|
+
* The client's in-memory comparisons read only `{buildVersion}:{timestamp}`
|
|
20
|
+
* (`stateEpoch`): the prefetch cache keys, the action fence's rotation check,
|
|
21
|
+
* the external-rotation observer below and the cross-tab adopt. A write-only
|
|
22
|
+
* change is soft: prefetches and history entries are marked stale and nothing
|
|
23
|
+
* is flushed. The tab that ran the action marks its own when it lands
|
|
24
|
+
* (navigation-store.ts markStaleSoftAndBroadcast) and tells the tabs that
|
|
25
|
+
* share a segment with it. Any other tab marks its own when a read sees the
|
|
26
|
+
* write part change (the write observer below), so a tab that ignores the
|
|
27
|
+
* broadcast or has crossTabSync off does not serve a pre-write prefetch fresh.
|
|
14
28
|
*
|
|
15
29
|
* Storage is a session cookie named by the server-resolved name passed to
|
|
16
30
|
* initRangoState (`{prefix}_{routerId}`, default prefix `rango-state`). The
|
|
@@ -32,6 +46,7 @@ import {
|
|
|
32
46
|
getRawCookieValue,
|
|
33
47
|
mintStateValue,
|
|
34
48
|
serializeStateCookie,
|
|
49
|
+
stateEpoch,
|
|
35
50
|
} from "./cookie-name.js";
|
|
36
51
|
|
|
37
52
|
let cookieName: string = DEFAULT_STATE_COOKIE_PREFIX;
|
|
@@ -42,10 +57,13 @@ let mirror: string | null = null;
|
|
|
42
57
|
let cookieBacked = false;
|
|
43
58
|
|
|
44
59
|
let externalRotationObserver: ((value: string) => void) | null = null;
|
|
60
|
+
let externalWriteObserver: (() => void) | null = null;
|
|
45
61
|
|
|
46
62
|
/**
|
|
47
63
|
* Register the observer invoked when a read detects an EXTERNAL rotation (a
|
|
48
|
-
* sibling tab, a server `Set-Cookie`, or a cookie clear)
|
|
64
|
+
* sibling tab, a server `Set-Cookie`, or a cookie clear) of the
|
|
65
|
+
* `{version}:{timestamp}` part. A change to the write part alone (a server
|
|
66
|
+
* action's write) goes to the write observer instead. Self-rotations
|
|
49
67
|
* (invalidateRangoState) update the mirror synchronously and never fire it.
|
|
50
68
|
*/
|
|
51
69
|
export function setRangoStateObserver(
|
|
@@ -54,6 +72,19 @@ export function setRangoStateObserver(
|
|
|
54
72
|
externalRotationObserver = observer;
|
|
55
73
|
}
|
|
56
74
|
|
|
75
|
+
/**
|
|
76
|
+
* Register the observer invoked when a read sees the write part change while
|
|
77
|
+
* the `{version}:{timestamp}` part stays: a server action's response, in this
|
|
78
|
+
* tab or another, bumped it in the shared jar since the last read. Every read
|
|
79
|
+
* updates the mirror, and a prefetch reads the state before it starts, so a
|
|
80
|
+
* prefetch sent with the new value is never one this observer sees as old.
|
|
81
|
+
*/
|
|
82
|
+
export function setRangoStateWriteObserver(
|
|
83
|
+
observer: (() => void) | null,
|
|
84
|
+
): void {
|
|
85
|
+
externalWriteObserver = observer;
|
|
86
|
+
}
|
|
87
|
+
|
|
57
88
|
/** Return the resolved cookie name used to namespace cross-tab messages. */
|
|
58
89
|
export function getRangoStateCookieName(): string {
|
|
59
90
|
return cookieName;
|
|
@@ -146,11 +177,17 @@ export function getRangoState(): string {
|
|
|
146
177
|
|
|
147
178
|
if (read.value !== null) {
|
|
148
179
|
if (read.value !== mirror) {
|
|
149
|
-
// External
|
|
150
|
-
// mirror update makes this idempotent across a burst of reads.
|
|
180
|
+
// External change (sibling tab / server Set-Cookie): adopt it. The
|
|
181
|
+
// mirror update makes this idempotent across a burst of reads. A
|
|
182
|
+
// rotation notifies the rotation observer; a write-only bump (a server
|
|
183
|
+
// action's response, or a sibling tab's) keeps the caches the epoch
|
|
184
|
+
// keys and notifies the write observer, which marks them stale.
|
|
185
|
+
const rotated =
|
|
186
|
+
mirror === null || stateEpoch(mirror) !== stateEpoch(read.value);
|
|
151
187
|
mirror = read.value;
|
|
152
188
|
cookieBacked = true;
|
|
153
|
-
notifyExternalRotation(read.value);
|
|
189
|
+
if (rotated) notifyExternalRotation(read.value);
|
|
190
|
+
else externalWriteObserver?.();
|
|
154
191
|
} else {
|
|
155
192
|
cookieBacked = true;
|
|
156
193
|
}
|
|
@@ -176,7 +213,7 @@ export function getRangoState(): string {
|
|
|
176
213
|
|
|
177
214
|
/**
|
|
178
215
|
* Invalidate the Rango state (self-rotation). Called when the client clears its
|
|
179
|
-
* prefetch caches (e.g.
|
|
216
|
+
* prefetch caches (e.g. invalidateClientCache()). Rotates the timestamp,
|
|
180
217
|
* keeps the version, writes the cookie, and updates the mirror synchronously so
|
|
181
218
|
* the external-rotation observer is NOT triggered by our own write.
|
|
182
219
|
*/
|
|
@@ -186,7 +223,12 @@ export function invalidateRangoState(): void {
|
|
|
186
223
|
writeCookie(cookieName, mirror);
|
|
187
224
|
}
|
|
188
225
|
|
|
189
|
-
/**
|
|
226
|
+
/**
|
|
227
|
+
* Adopt the state carried by a same-origin cache-invalidation broadcast when
|
|
228
|
+
* its timestamp is newer (a rotation). The write part is not compared: a soft
|
|
229
|
+
* broadcast's newer write is already in the shared jar, and a value that
|
|
230
|
+
* differs only there is no rotation to adopt.
|
|
231
|
+
*/
|
|
190
232
|
export function adoptRangoState(value: string): boolean {
|
|
191
233
|
const incoming = decodeStateValue(value);
|
|
192
234
|
if (!incoming || incoming.version !== currentVersion) return false;
|
|
@@ -115,12 +115,21 @@ export interface LinkProps extends Omit<
|
|
|
115
115
|
* server cannot see.
|
|
116
116
|
*
|
|
117
117
|
* The server scopes a prefetch to its source automatically
|
|
118
|
-
* (`x-rsc-prefetch-scope: source`) when an intercept targets the route
|
|
119
|
-
* whether or not it applies
|
|
120
|
-
* are both source-specific),
|
|
121
|
-
*
|
|
122
|
-
*
|
|
123
|
-
*
|
|
118
|
+
* (`x-rsc-prefetch-scope: source`) when an intercept targets the route or
|
|
119
|
+
* the source page, whether or not it applies (the modal and the full page
|
|
120
|
+
* are both source-specific), when the answer defers a `prefetch: false`
|
|
121
|
+
* unit the source does not hold, when a `revalidate()` of the target read
|
|
122
|
+
* `currentUrl` and kept the segment, and when a kept answer sends no
|
|
123
|
+
* segment. A `revalidate()` that read `currentParams` or `fromRouteName`
|
|
124
|
+
* and kept the segment, or a prerendered target that keeps segments on
|
|
125
|
+
* equal params (`storedEntrySends`), shares the answer between the pages
|
|
126
|
+
* with equal values of what was read (`x-rsc-prefetch-scope: params`,
|
|
127
|
+
* #1331). Every other response stays shared. Use the key
|
|
128
|
+
* for what the server does not track: `context` in a `revalidate()`, or
|
|
129
|
+
* a `Referer` read.
|
|
130
|
+
*
|
|
131
|
+
* A source-scoped entry is keyed by the source page's full URL, query
|
|
132
|
+
* and hash included, so every page prefetches the link again.
|
|
124
133
|
*
|
|
125
134
|
* @example
|
|
126
135
|
* ```tsx
|
|
@@ -361,9 +370,11 @@ export const Link: ForwardRefExoticComponent<
|
|
|
361
370
|
(!isHashOnlyNavigation(element) && !isPrefetchScopeDisabled(element)))
|
|
362
371
|
) {
|
|
363
372
|
// For "hover", this is the primary prefetch trigger.
|
|
364
|
-
// For "viewport", this
|
|
365
|
-
//
|
|
366
|
-
//
|
|
373
|
+
// For "viewport", this prioritizes a potentially queued prefetch:
|
|
374
|
+
// prefetchDirect bypasses the queue, and its dedupe skips it when the
|
|
375
|
+
// viewport prefetch is cached or in flight. Either way it is an intent
|
|
376
|
+
// prefetch, which defers no `prefetch: false` work
|
|
377
|
+
// (docs/design/prefetch-false.md, R16).
|
|
367
378
|
const segmentState = ctx.store.getSegmentState();
|
|
368
379
|
prefetchDirect(
|
|
369
380
|
resolvedTo,
|
|
@@ -27,7 +27,7 @@ interface Snapshot<T> {
|
|
|
27
27
|
/**
|
|
28
28
|
* A notification while React holds a page: work that starts shows at once
|
|
29
29
|
* (loading, streaming, a newer click's URL), what settles waits for the
|
|
30
|
-
* page's commit
|
|
30
|
+
* page's commit, and location moves only with the notification
|
|
31
31
|
* that moves it (the page on screen until the next one shows).
|
|
32
32
|
*/
|
|
33
33
|
function hold(
|
|
@@ -27,7 +27,11 @@ import type { EventController } from "./event-controller.js";
|
|
|
27
27
|
import type { ResolvedThemeConfig, Theme } from "../theme/types.js";
|
|
28
28
|
import { expandPayloadFragments } from "../segment-fragments.js";
|
|
29
29
|
import { initRangoState } from "./rango-state.js";
|
|
30
|
-
import {
|
|
30
|
+
import {
|
|
31
|
+
committedPageOf,
|
|
32
|
+
registerCommittedPageReader,
|
|
33
|
+
registerNavigationStore,
|
|
34
|
+
} from "./navigation-store-handle.js";
|
|
31
35
|
import { initPrefetchCache } from "./prefetch/cache.js";
|
|
32
36
|
import {
|
|
33
37
|
auditTreeCause,
|
|
@@ -286,6 +290,9 @@ export async function initBrowserApp(
|
|
|
286
290
|
|
|
287
291
|
// Initialize route params
|
|
288
292
|
eventController.setParams(initialPayload.metadata?.params ?? {});
|
|
293
|
+
// The page a prefetch is sent from, read by the prefetch cache at request
|
|
294
|
+
// time (browser/prefetch/cache.ts paramsCandidateKeys).
|
|
295
|
+
registerCommittedPageReader(committedPageOf(eventController, store));
|
|
289
296
|
|
|
290
297
|
// Initialize handle data from initial payload BEFORE hydration
|
|
291
298
|
// This ensures useHandle returns correct data during hydration to avoid mismatch
|