@rangojs/router 0.31.0 → 0.33.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/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 -10143
- package/dist/bin/runtime-EPUDEHO4.js +596 -0
- package/dist/bin/runtime-discovery-CZ2QHT4Q.js +352 -0
- package/dist/bin/state-ACD2MGKJ.js +16 -0
- package/dist/bin/temp-rsc-server-QRR6HD6T.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/history-state.d.ts +13 -2
- 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 +19 -13
- package/dist/types/browser/react/location-state-shared.d.ts +2 -1
- 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/route-definition/redirect.d.ts +5 -0
- 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/middleware-types.d.ts +6 -0
- 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 +8 -3
- package/dist/types/rsc/prefetch-render-error.d.ts +7 -2
- package/dist/types/rsc/request-stages.d.ts +4 -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 +59 -0
- package/dist/types/server/handle-store.d.ts +2 -0
- package/dist/types/server/request-context.d.ts +65 -12
- package/dist/types/testing/e2e/server.d.ts +16 -1
- package/dist/types/testing/render-route.d.ts +83 -2
- package/dist/types/testing/serve-shell-request.d.ts +15 -3
- package/dist/types/types/handler-context.d.ts +21 -4
- package/dist/types/types/loader-types.d.ts +7 -4
- package/dist/types/types/segments.d.ts +18 -1
- package/dist/types/urls/path-helper-types.d.ts +7 -5
- package/dist/types/vite/temp-rsc-server.d.ts +2 -2
- package/dist/types/vite/utils/forward-user-plugins.d.ts +32 -2
- package/dist/vite/index.js +451 -191
- package/package.json +2 -2
- package/skills/app-structure/SKILL.md +74 -0
- package/skills/app-structure/e-commerce.md +10 -9
- package/skills/app-structure/large-app.md +15 -2
- package/skills/caching/SKILL.md +10 -5
- package/skills/client-urls/SKILL.md +18 -11
- package/skills/comparison/references/framework-comparison.md +16 -10
- package/skills/document-cache/SKILL.md +13 -0
- package/skills/hooks/navigation.md +8 -8
- package/skills/hooks/state.md +132 -12
- package/skills/intercept/SKILL.md +33 -11
- package/skills/layout/SKILL.md +11 -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 +38 -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 +49 -8
- package/skills/testing/client-components.md +73 -32
- package/skills/testing/serve-shell-request.md +45 -20
- package/skills/testing/server-actions.md +8 -0
- package/skills/ux-architecture/SKILL.md +67 -15
- 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/history-state.ts +24 -2
- 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 +24 -16
- package/src/browser/react/location-state-shared.ts +8 -1
- package/src/browser/react/use-navigation.ts +1 -1
- package/src/browser/rsc-router.tsx +13 -3
- package/src/browser/server-action-bridge.ts +3 -3
- 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/build/runtime-discovery.ts +46 -8
- 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 +37 -1
- package/src/cache/document-cache.ts +12 -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/route-definition/redirect.ts +5 -0
- 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 +39 -14
- package/src/router/match-result.ts +45 -85
- package/src/router/middleware-types.ts +6 -0
- package/src/router/navigation-snapshot.ts +18 -3
- package/src/router/prefetch-cache-ttl.ts +15 -3
- package/src/router/revalidation.ts +63 -13
- 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 +20 -6
- package/src/rsc/prefetch-render-error.ts +7 -2
- package/src/rsc/progressive-enhancement.ts +8 -2
- package/src/rsc/request-stages.ts +87 -1
- package/src/rsc/rsc-rendering.ts +78 -32
- 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 +334 -0
- package/src/server/handle-store.ts +2 -0
- package/src/server/request-context.ts +137 -33
- package/src/testing/dispatch.ts +6 -0
- package/src/testing/e2e/fixture.ts +4 -12
- package/src/testing/e2e/server.ts +91 -4
- package/src/testing/render-route.tsx +309 -10
- package/src/testing/run-revalidate.ts +11 -1
- package/src/testing/serve-shell-request.ts +22 -3
- package/src/types/handler-context.ts +21 -4
- package/src/types/loader-types.ts +7 -4
- package/src/types/segments.ts +18 -1
- package/src/urls/path-helper-types.ts +7 -5
- package/src/vite/temp-rsc-server.ts +2 -2
- package/src/vite/utils/forward-user-plugins.ts +13 -5
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
startBrowserTransaction,
|
|
14
14
|
} from "./logging.js";
|
|
15
15
|
import { getRangoState } from "./rango-state.js";
|
|
16
|
+
import { stateEpoch } from "./cookie-name.js";
|
|
16
17
|
import { isActionFenceActive } from "./action-fence.js";
|
|
17
18
|
import {
|
|
18
19
|
expandPayloadFragments,
|
|
@@ -35,11 +36,54 @@ import {
|
|
|
35
36
|
consumePrefetch,
|
|
36
37
|
disableFragmentPassthrough,
|
|
37
38
|
isFragmentPassthroughEnabled,
|
|
39
|
+
isPrefetchStale,
|
|
40
|
+
pageMatchesParamsLabel,
|
|
41
|
+
paramsCandidateKeys,
|
|
38
42
|
type DecodedPrefetch,
|
|
39
43
|
} from "./prefetch/cache.js";
|
|
44
|
+
import { getCommittedPage } from "./navigation-store-handle.js";
|
|
45
|
+
import type { SourcePage } from "../prefetch-source-scope.js";
|
|
40
46
|
import { cancelAllPrefetches } from "./prefetch/loader.js";
|
|
41
47
|
import { writePartialParams } from "./wire-protocol.js";
|
|
42
48
|
|
|
49
|
+
/**
|
|
50
|
+
* The completed prefetch a navigation adopts: the one stored under its
|
|
51
|
+
* source key, else one stored for a group of source pages the page is in
|
|
52
|
+
* (`paramsKeys`, paramsCandidateKeys), else the one every source page
|
|
53
|
+
* shares. None when the navigation cannot use a prefetch. A stale one only
|
|
54
|
+
* when `acceptStale` (FetchPartialOptions.acceptStalePrefetch).
|
|
55
|
+
*/
|
|
56
|
+
export function consumeStoredPrefetch(
|
|
57
|
+
canUsePrefetch: boolean,
|
|
58
|
+
sourceKey: string,
|
|
59
|
+
paramsKeys: readonly string[],
|
|
60
|
+
wildcardKey: string,
|
|
61
|
+
acceptStale = false,
|
|
62
|
+
): { entry: DecodedPrefetch | null; hitKey: string | null } {
|
|
63
|
+
if (!canUsePrefetch) return { entry: null, hitKey: null };
|
|
64
|
+
for (const key of [sourceKey, ...paramsKeys, wildcardKey]) {
|
|
65
|
+
const entry = consumePrefetch(key, { acceptStale });
|
|
66
|
+
if (entry) return { entry, hitKey: key };
|
|
67
|
+
}
|
|
68
|
+
return { entry: null, hitKey: null };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Whether a navigation from `page` may serve an in-flight prefetch it
|
|
73
|
+
* adopted through the wildcard key: not one the server scoped to its own
|
|
74
|
+
* source page, nor one scoped to a group of source pages `page` is not in.
|
|
75
|
+
*/
|
|
76
|
+
export function wildcardAdoptionFits(
|
|
77
|
+
entry: DecodedPrefetch,
|
|
78
|
+
page: SourcePage | null,
|
|
79
|
+
): boolean {
|
|
80
|
+
if (entry.scope === "source") return false;
|
|
81
|
+
if (entry.scope === "params") {
|
|
82
|
+
return pageMatchesParamsLabel(entry.paramsLabel ?? "", page);
|
|
83
|
+
}
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
|
|
43
87
|
/**
|
|
44
88
|
* Create a navigation client for fetching RSC payloads
|
|
45
89
|
*
|
|
@@ -88,8 +132,14 @@ export function createNavigationClient(
|
|
|
88
132
|
routerId,
|
|
89
133
|
hmr,
|
|
90
134
|
fill,
|
|
135
|
+
acceptStalePrefetch = false,
|
|
91
136
|
} = options;
|
|
92
137
|
|
|
138
|
+
// The page left, absolute: the router store keeps a committed URL as
|
|
139
|
+
// navigate() was given it, often a path. The header and the source
|
|
140
|
+
// prefetch key name it as prefetch/fetch.ts does, so they agree.
|
|
141
|
+
const sourceHref = new URL(previousUrl, window.location.origin).href;
|
|
142
|
+
|
|
93
143
|
const debugEnabled = isBrowserDebugEnabled();
|
|
94
144
|
const tx = debugEnabled
|
|
95
145
|
? startBrowserTransaction(staleRevalidation ? "revalidate" : "navigate")
|
|
@@ -135,8 +185,13 @@ export function createNavigationClient(
|
|
|
135
185
|
// intercept targets, or when a Link opted in with
|
|
136
186
|
// `prefetchKey=":source"`), then fall back to the wildcard slot shared
|
|
137
187
|
// across source pages.
|
|
138
|
-
// Both keys embed the Rango state
|
|
139
|
-
//
|
|
188
|
+
// Both keys embed the Rango state's epoch (`{version}:{timestamp}`,
|
|
189
|
+
// cookie-name.ts stateEpoch), so state rotation (deploy or
|
|
190
|
+
// invalidateClientCache()) auto-invalidates both scopes. A server
|
|
191
|
+
// action bumps only the write part, which the keys leave out: it marks
|
|
192
|
+
// entries stale instead, and one is still served to a click
|
|
193
|
+
// (acceptStalePrefetch), whose bridge revalidates its page after it
|
|
194
|
+
// settled (stalePrefetch); for any other caller it is a miss.
|
|
140
195
|
// Skip cache for stale revalidation (needs fresh data), HMR (needs
|
|
141
196
|
// fresh modules), and intercept contexts (source-dependent responses).
|
|
142
197
|
// A prefetch served during an action's flight holds pre-write data:
|
|
@@ -144,20 +199,22 @@ export function createNavigationClient(
|
|
|
144
199
|
const canUsePrefetch =
|
|
145
200
|
!staleRevalidation && !hmr && !fill && !interceptSourceUrl;
|
|
146
201
|
const rangoState = getRangoState();
|
|
147
|
-
const
|
|
148
|
-
const
|
|
202
|
+
const stateKey = stateEpoch(rangoState);
|
|
203
|
+
const wildcardKey = buildPrefetchKey(stateKey, fetchUrl);
|
|
204
|
+
const cacheKey = buildSourceKey(stateKey, sourceHref, fetchUrl);
|
|
205
|
+
// The page being left, as the router store committed it: the group a
|
|
206
|
+
// params-scoped prefetch must have been answered for.
|
|
207
|
+
const sourcePage = canUsePrefetch ? getCommittedPage() : null;
|
|
149
208
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
}
|
|
160
|
-
}
|
|
209
|
+
const stored = consumeStoredPrefetch(
|
|
210
|
+
canUsePrefetch,
|
|
211
|
+
cacheKey,
|
|
212
|
+
paramsCandidateKeys(stateKey, fetchUrl, sourcePage),
|
|
213
|
+
wildcardKey,
|
|
214
|
+
acceptStalePrefetch,
|
|
215
|
+
);
|
|
216
|
+
const cachedEntry = stored.entry;
|
|
217
|
+
let hitKey = stored.hitKey;
|
|
161
218
|
|
|
162
219
|
let inflightEntryPromise: Promise<DecodedPrefetch | null> | null = null;
|
|
163
220
|
if (canUsePrefetch && !cachedEntry) {
|
|
@@ -262,7 +319,7 @@ export function createNavigationClient(
|
|
|
262
319
|
cache: "no-store" as RequestCache,
|
|
263
320
|
}),
|
|
264
321
|
headers: {
|
|
265
|
-
"X-RSC-Router-Client-Path":
|
|
322
|
+
"X-RSC-Router-Client-Path": sourceHref,
|
|
266
323
|
// Reuse the single per-operation read (see rangoState above): the
|
|
267
324
|
// cache-key lookup and this header must agree on one value, and the
|
|
268
325
|
// cookie read has side effects (external-rotation notify) we do not
|
|
@@ -312,6 +369,9 @@ export function createNavigationClient(
|
|
|
312
369
|
// (complete === true). A still-streaming hit and the fresh path stay false,
|
|
313
370
|
// so only a fully-warmed prefetch commits in a transition (no fallback flash).
|
|
314
371
|
let fullyPrefetched = false;
|
|
372
|
+
// A prefetch an action marked stale after it was fetched: committed
|
|
373
|
+
// like any other, then revalidated (FetchPartialResult.stalePrefetch).
|
|
374
|
+
let stalePrefetch = false;
|
|
315
375
|
|
|
316
376
|
// The prefetch entry this navigation serves.
|
|
317
377
|
let adopted = cachedEntry;
|
|
@@ -335,15 +395,27 @@ export function createNavigationClient(
|
|
|
335
395
|
if (tx) {
|
|
336
396
|
browserDebugLog(tx, "inflight prefetch unavailable, refetching");
|
|
337
397
|
}
|
|
338
|
-
} else if (
|
|
339
|
-
|
|
340
|
-
|
|
398
|
+
} else if (
|
|
399
|
+
adoptedViaWildcard &&
|
|
400
|
+
!wildcardAdoptionFits(entry, sourcePage)
|
|
401
|
+
) {
|
|
402
|
+
// A wildcard-adopted inflight that turned out source-scoped, or
|
|
403
|
+
// params-scoped to a group this page is not in, was built for a
|
|
404
|
+
// different source page. Discard and refetch.
|
|
341
405
|
if (tx) {
|
|
342
406
|
browserDebugLog(
|
|
343
407
|
tx,
|
|
344
|
-
|
|
408
|
+
`wildcard inflight turned out ${entry.scope}-scoped, refetching`,
|
|
345
409
|
);
|
|
346
410
|
}
|
|
411
|
+
} else if (!acceptStalePrefetch && isPrefetchStale(entry)) {
|
|
412
|
+
// An action marked it stale, and this navigation never revalidates
|
|
413
|
+
// (not a click): drop it and fetch fresh. Its reserve is released;
|
|
414
|
+
// the decode it started is never read.
|
|
415
|
+
entry.dispose?.();
|
|
416
|
+
if (tx) {
|
|
417
|
+
browserDebugLog(tx, "inflight prefetch is stale, refetching");
|
|
418
|
+
}
|
|
347
419
|
} else {
|
|
348
420
|
adopted = entry;
|
|
349
421
|
}
|
|
@@ -357,6 +429,7 @@ export function createNavigationClient(
|
|
|
357
429
|
// prefetched"; a still-streaming one keeps streaming its fallbacks like
|
|
358
430
|
// a cold load.
|
|
359
431
|
fullyPrefetched = adopted.complete;
|
|
432
|
+
stalePrefetch = isPrefetchStale(adopted);
|
|
360
433
|
} else {
|
|
361
434
|
({ payload: payloadPromise, streamComplete: streamCompletePromise } =
|
|
362
435
|
freshResult());
|
|
@@ -393,6 +466,7 @@ export function createNavigationClient(
|
|
|
393
466
|
const recovery = freshResult(false, true);
|
|
394
467
|
streamCompletePromise = recovery.streamComplete;
|
|
395
468
|
fullyPrefetched = false;
|
|
469
|
+
stalePrefetch = false;
|
|
396
470
|
payload = await recovery.payload;
|
|
397
471
|
await expandPayloadFragments(payload, deps.createFromReadableStream);
|
|
398
472
|
}
|
|
@@ -414,6 +488,7 @@ export function createNavigationClient(
|
|
|
414
488
|
payload,
|
|
415
489
|
streamComplete: streamCompletePromise,
|
|
416
490
|
fullyPrefetched,
|
|
491
|
+
stalePrefetch,
|
|
417
492
|
};
|
|
418
493
|
} catch (error) {
|
|
419
494
|
// Convert network-level errors to NetworkError for proper handling
|
|
@@ -7,13 +7,23 @@
|
|
|
7
7
|
* receive it by argument: the jar-divergence observer (below) and the client
|
|
8
8
|
* seat of `invalidateClientCache()` (added later).
|
|
9
9
|
*
|
|
10
|
-
* Dependency-light on purpose: it imports only
|
|
11
|
-
* store type, so pulling it into the default
|
|
12
|
-
* navigation store into bundles that previously
|
|
10
|
+
* Dependency-light on purpose: it imports only the action fence, the
|
|
11
|
+
* rango-state observers and the store type, so pulling it into the default
|
|
12
|
+
* root entry does not drag the navigation store into bundles that previously
|
|
13
|
+
* lacked it.
|
|
13
14
|
*/
|
|
14
15
|
|
|
15
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
noteStateRotation,
|
|
18
|
+
noteStateWrite,
|
|
19
|
+
setUnlandedWriteHandler,
|
|
20
|
+
} from "./action-fence.js";
|
|
21
|
+
import {
|
|
22
|
+
setRangoStateObserver,
|
|
23
|
+
setRangoStateWriteObserver,
|
|
24
|
+
} from "./rango-state.js";
|
|
16
25
|
import type { NavigationStore } from "./types.js";
|
|
26
|
+
import type { SourcePage } from "../prefetch-source-scope.js";
|
|
17
27
|
|
|
18
28
|
let registeredStore: NavigationStore | null = null;
|
|
19
29
|
|
|
@@ -23,16 +33,68 @@ let registeredStore: NavigationStore | null = null;
|
|
|
23
33
|
* sibling tab, a server `Set-Cookie`, or a cookie clear), mark this tab's
|
|
24
34
|
* history cache stale. The history cache is not state-keyed, so the value
|
|
25
35
|
* rotation alone does not reach it. No broadcast, no prefetch clear, no
|
|
26
|
-
* re-rotation — the value already changed externally.
|
|
36
|
+
* re-rotation — the value already changed externally. A rotation seen while
|
|
37
|
+
* an action is in flight is recorded on the fence: the action's landing then
|
|
38
|
+
* sends the full broadcast (navigation-store.ts markCacheAsStaleAndBroadcast).
|
|
39
|
+
*
|
|
40
|
+
* A read that sees only the write part change (a server action's response,
|
|
41
|
+
* in this tab or another) marks this tab's history entries and prefetches
|
|
42
|
+
* stale, as a soft broadcast's receiver does: a tab the broadcast does not
|
|
43
|
+
* reach would otherwise serve a prefetch made before the write as fresh.
|
|
44
|
+
* While an action is in flight the write is recorded on the fence instead:
|
|
45
|
+
* the action's landing marks every cache stale once (navigation-store.ts
|
|
46
|
+
* markStaleSoftAndBroadcast), and when no landing does, the fence's drop
|
|
47
|
+
* marks them.
|
|
27
48
|
*/
|
|
28
49
|
export function registerNavigationStore(store: NavigationStore): void {
|
|
29
50
|
registeredStore = store;
|
|
30
51
|
setRangoStateObserver(() => {
|
|
52
|
+
noteStateRotation();
|
|
31
53
|
registeredStore?.markHistoryCacheStale();
|
|
32
54
|
});
|
|
55
|
+
const markCachesStale = () => registeredStore?.markCachesStaleAfterWrite();
|
|
56
|
+
setRangoStateWriteObserver(() => {
|
|
57
|
+
if (!noteStateWrite()) markCachesStale();
|
|
58
|
+
});
|
|
59
|
+
setUnlandedWriteHandler(markCachesStale);
|
|
33
60
|
}
|
|
34
61
|
|
|
35
62
|
/** The active navigation store, or null before boot has registered it. */
|
|
36
63
|
export function getRegisteredStore(): NavigationStore | null {
|
|
37
64
|
return registeredStore;
|
|
38
65
|
}
|
|
66
|
+
|
|
67
|
+
let readCommittedPage: (() => SourcePage) | null = null;
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Register the reader of the committed page's params and route name: the
|
|
71
|
+
* router store's own state, which useParams reads (the event controller's
|
|
72
|
+
* params, the displayed history entry's route name). Read by the prefetch
|
|
73
|
+
* cache outside render, at request time, to look up an answer shared by the
|
|
74
|
+
* pages with equal values of what a revalidate() rule read
|
|
75
|
+
* (prefetch-source-scope.ts). A reader, not a copy: there is no second
|
|
76
|
+
* source of truth for the params.
|
|
77
|
+
*/
|
|
78
|
+
export function registerCommittedPageReader(read: () => SourcePage): void {
|
|
79
|
+
readCommittedPage = read;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* The boot reader (rsc-router.tsx): the event controller's params, which
|
|
84
|
+
* useParams reads and only a commit sets (an optimistic clientUrls() frame
|
|
85
|
+
* sets React context, not them), and the displayed entry's route name.
|
|
86
|
+
*/
|
|
87
|
+
export function committedPageOf(
|
|
88
|
+
controller: { getParams(): Record<string, string> },
|
|
89
|
+
store: Pick<NavigationStore, "getHistoryEntryMemory">,
|
|
90
|
+
): () => SourcePage {
|
|
91
|
+
return () => ({
|
|
92
|
+
params: controller.getParams(),
|
|
93
|
+
routeName: store.getHistoryEntryMemory()?.routeName,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** The committed page, or null before boot has registered its reader. */
|
|
98
|
+
export function getCommittedPage(): SourcePage | null {
|
|
99
|
+
return readCommittedPage ? readCommittedPage() : null;
|
|
100
|
+
}
|
|
@@ -10,7 +10,11 @@ import type {
|
|
|
10
10
|
ShownEntry,
|
|
11
11
|
} from "./types.js";
|
|
12
12
|
import type { EventController } from "./event-controller.js";
|
|
13
|
-
import { clearPrefetchCache } from "./prefetch/cache.js";
|
|
13
|
+
import { clearPrefetchCache, markPrefetchesStale } from "./prefetch/cache.js";
|
|
14
|
+
import {
|
|
15
|
+
takeStateRotationInActionFence,
|
|
16
|
+
takeStateWriteInActionFence,
|
|
17
|
+
} from "./action-fence.js";
|
|
14
18
|
import { LoaderStore } from "../loader-store.js";
|
|
15
19
|
import {
|
|
16
20
|
adoptRangoState,
|
|
@@ -172,8 +176,8 @@ export interface NavigationStoreConfig {
|
|
|
172
176
|
|
|
173
177
|
/**
|
|
174
178
|
* Enable cross-tab cache invalidation via BroadcastChannel (default: true)
|
|
175
|
-
*
|
|
176
|
-
*
|
|
179
|
+
* After a server action other tabs that share a segment mark their caches
|
|
180
|
+
* stale; after invalidateClientCache() they adopt its state and flush
|
|
177
181
|
*/
|
|
178
182
|
crossTabSync?: boolean;
|
|
179
183
|
|
|
@@ -332,22 +336,57 @@ export function createNavigationStore(
|
|
|
332
336
|
*/
|
|
333
337
|
function clearCacheAndBroadcast(): void {
|
|
334
338
|
clearCacheInternal();
|
|
335
|
-
broadcastInvalidation();
|
|
339
|
+
broadcastInvalidation(false);
|
|
336
340
|
}
|
|
337
341
|
|
|
338
342
|
/**
|
|
339
|
-
* Mark cache as stale
|
|
343
|
+
* Mark cache as stale, flush the prefetch caches (rotating the state), and
|
|
344
|
+
* broadcast to other tabs: the client seat of `invalidateClientCache()`.
|
|
340
345
|
*/
|
|
341
346
|
function markStaleAndBroadcast(): void {
|
|
342
347
|
markCacheAsStaleInternal();
|
|
343
|
-
broadcastInvalidation();
|
|
348
|
+
broadcastInvalidation(false);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* What a server action without a directive does once its response arrived
|
|
353
|
+
* (server-action-bridge.ts finalizeAction). History entries and prefetches
|
|
354
|
+
* are marked stale, nothing is flushed or rotated, and other tabs get a
|
|
355
|
+
* `soft` broadcast: they mark theirs stale and refresh.
|
|
356
|
+
*
|
|
357
|
+
* When the state rotated during the action (the action called the server
|
|
358
|
+
* seat of `invalidateClientCache()`, whose `Set-Cookie` reached the jar),
|
|
359
|
+
* it does what every action did before #1329: flush the prefetch map,
|
|
360
|
+
* rotate the state once more and send the full broadcast. The second
|
|
361
|
+
* rotation is what lets other tabs adopt: one that already read the
|
|
362
|
+
* server's value from the shared jar would refuse an equal one and rotate
|
|
363
|
+
* on its own, stranding this tab's new prefetches.
|
|
364
|
+
*/
|
|
365
|
+
function markStaleSoftAndBroadcast(): void {
|
|
366
|
+
markHistoryStale();
|
|
367
|
+
// The read reports an external rotation or write the jar holds to the
|
|
368
|
+
// fence. Either branch below marks every cache stale, so it covers a
|
|
369
|
+
// write seen so far.
|
|
370
|
+
const rangoState = getRangoState();
|
|
371
|
+
takeStateWriteInActionFence();
|
|
372
|
+
if (takeStateRotationInActionFence()) {
|
|
373
|
+
clearPrefetchCache();
|
|
374
|
+
broadcastInvalidation(false);
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
markPrefetchesStale();
|
|
378
|
+
broadcastInvalidation(true, rangoState);
|
|
344
379
|
}
|
|
345
380
|
|
|
346
381
|
/**
|
|
347
|
-
* Broadcast cache invalidation to other tabs
|
|
348
|
-
*
|
|
382
|
+
* Broadcast cache invalidation to other tabs. `soft` marks a server action
|
|
383
|
+
* that rotated nothing: a receiver that cannot adopt a newer state marks its
|
|
384
|
+
* caches stale instead of flushing them and rotating.
|
|
349
385
|
*/
|
|
350
|
-
function broadcastInvalidation(
|
|
386
|
+
function broadcastInvalidation(
|
|
387
|
+
soft: boolean,
|
|
388
|
+
rangoState: string = getRangoState(),
|
|
389
|
+
): void {
|
|
351
390
|
// Only broadcast if cross-tab sync is enabled
|
|
352
391
|
if (!crossTabSync) return;
|
|
353
392
|
|
|
@@ -360,8 +399,9 @@ export function createNavigationStore(
|
|
|
360
399
|
type: "invalidate",
|
|
361
400
|
path: currentPath,
|
|
362
401
|
segmentIds: currentSegmentIds,
|
|
363
|
-
rangoState
|
|
402
|
+
rangoState,
|
|
364
403
|
stateCookieName: getRangoStateCookieName(),
|
|
404
|
+
...(soft && { soft: true }),
|
|
365
405
|
});
|
|
366
406
|
}
|
|
367
407
|
}
|
|
@@ -394,12 +434,20 @@ export function createNavigationStore(
|
|
|
394
434
|
typeof stateCookieName === "string"
|
|
395
435
|
) {
|
|
396
436
|
if (stateCookieName !== getRangoStateCookieName()) return;
|
|
397
|
-
// The sender already rotated the same-origin cookie. Adopt that
|
|
398
|
-
// value before clearing locally so tabs cannot ping-pong rotations
|
|
399
|
-
// and obsolete each other's post-invalidation prefetches.
|
|
400
|
-
const adopted = adoptRangoState(rangoState);
|
|
401
437
|
markHistoryStale();
|
|
402
|
-
|
|
438
|
+
if (adoptRangoState(rangoState)) {
|
|
439
|
+
// The sender carries a newer state (it rotated the shared
|
|
440
|
+
// cookie: invalidateClientCache() on either seat). Adopt it
|
|
441
|
+
// before flushing locally, whether or not the message is soft,
|
|
442
|
+
// so tabs cannot ping-pong rotations and obsolete each other's
|
|
443
|
+
// post-invalidation prefetches.
|
|
444
|
+
clearPrefetchCache(false);
|
|
445
|
+
} else if (event.data.soft === true) {
|
|
446
|
+
// A server action that rotated nothing: mark, never flush.
|
|
447
|
+
markPrefetchesStale();
|
|
448
|
+
} else {
|
|
449
|
+
clearPrefetchCache(true);
|
|
450
|
+
}
|
|
403
451
|
} else {
|
|
404
452
|
// Compatibility with an already-open tab running an older sender.
|
|
405
453
|
markCacheAsStaleInternal();
|
|
@@ -679,6 +727,18 @@ export function createNavigationStore(
|
|
|
679
727
|
markHistoryStale();
|
|
680
728
|
},
|
|
681
729
|
|
|
730
|
+
/**
|
|
731
|
+
* Mark history entries and prefetches stale, telling no other tab: a
|
|
732
|
+
* cookie read saw a server action's write part change (rango-state.ts
|
|
733
|
+
* write observer). What a soft broadcast's receiver does, for a tab the
|
|
734
|
+
* broadcast does not reach: one whose page shares no segment with the
|
|
735
|
+
* acting page, or one with crossTabSync off.
|
|
736
|
+
*/
|
|
737
|
+
markCachesStaleAfterWrite(): void {
|
|
738
|
+
markHistoryStale();
|
|
739
|
+
markPrefetchesStale();
|
|
740
|
+
},
|
|
741
|
+
|
|
682
742
|
/**
|
|
683
743
|
* Clear the history cache and broadcast to other tabs
|
|
684
744
|
* Use this for hard invalidation when data is definitely stale
|
|
@@ -688,10 +748,19 @@ export function createNavigationStore(
|
|
|
688
748
|
},
|
|
689
749
|
|
|
690
750
|
/**
|
|
691
|
-
* Mark
|
|
692
|
-
*
|
|
751
|
+
* Mark history entries and prefetches stale and tell other tabs. Called
|
|
752
|
+
* after a server action without a directive: Back and a click on a
|
|
753
|
+
* prefetch made before it show the cached page and revalidate it.
|
|
693
754
|
*/
|
|
694
755
|
markCacheAsStaleAndBroadcast(): void {
|
|
756
|
+
markStaleSoftAndBroadcast();
|
|
757
|
+
},
|
|
758
|
+
|
|
759
|
+
/**
|
|
760
|
+
* Mark history stale, flush the prefetch caches, rotate the state and
|
|
761
|
+
* tell other tabs: the client seat of invalidateClientCache().
|
|
762
|
+
*/
|
|
763
|
+
invalidateCacheAndBroadcast(): void {
|
|
695
764
|
markStaleAndBroadcast();
|
|
696
765
|
},
|
|
697
766
|
|
|
@@ -114,7 +114,6 @@ export interface CommitResult {
|
|
|
114
114
|
* Bound transaction with pre-configured commit options (without segmentIds/segments)
|
|
115
115
|
*/
|
|
116
116
|
export interface BoundTransaction {
|
|
117
|
-
readonly currentUrl: string;
|
|
118
117
|
/** Bound `replace` option (history replace instead of push). */
|
|
119
118
|
readonly replace?: boolean;
|
|
120
119
|
/** Bound `traversal` option: a back/forward navigation. */
|
|
@@ -156,8 +155,6 @@ export function createNavigationTransaction(
|
|
|
156
155
|
url: string,
|
|
157
156
|
options?: NavigateOptionsInternal & { skipLoadingState?: boolean },
|
|
158
157
|
): NavigationTransaction {
|
|
159
|
-
const currentUrl = window.location.href;
|
|
160
|
-
|
|
161
158
|
const handle = eventController.startNavigation(url, options);
|
|
162
159
|
|
|
163
160
|
/**
|
|
@@ -260,9 +257,6 @@ export function createNavigationTransaction(
|
|
|
260
257
|
opts: Omit<CommitOptions, "segmentIds" | "segments">,
|
|
261
258
|
): BoundTransaction {
|
|
262
259
|
return {
|
|
263
|
-
get currentUrl() {
|
|
264
|
-
return currentUrl;
|
|
265
|
-
},
|
|
266
260
|
replace: opts.replace,
|
|
267
261
|
traversal: opts.traversal,
|
|
268
262
|
state: opts.state,
|
|
@@ -238,6 +238,19 @@ export type UpdateMode =
|
|
|
238
238
|
refresh?: boolean;
|
|
239
239
|
/** The cross-tab refresh's held boundary (see `action`). */
|
|
240
240
|
boundary?: string;
|
|
241
|
+
/**
|
|
242
|
+
* A click's: told of each response this navigation fetches, true when
|
|
243
|
+
* it is a prefetch a server action marked stale
|
|
244
|
+
* (FetchPartialResult.stalePrefetch), false otherwise, so a fresh
|
|
245
|
+
* refetch after it (a fill's start-over, an HMR retry) clears it. Its
|
|
246
|
+
* presence is what lets a stale prefetch answer
|
|
247
|
+
* (FetchPartialOptions.acceptStalePrefetch): only a navigation that
|
|
248
|
+
* revalidates afterwards takes one. The click commits it as it commits
|
|
249
|
+
* any prefetch; the bridge revalidates the page once the navigation
|
|
250
|
+
* settled (navigation-bridge.ts navigate). Nothing here changes how or
|
|
251
|
+
* when this commit lands.
|
|
252
|
+
*/
|
|
253
|
+
onStalePrefetch?: (stale: boolean) => void;
|
|
241
254
|
}
|
|
242
255
|
| { type: "leave-intercept"; interceptSourceUrl?: string; boundary?: never }
|
|
243
256
|
| {
|
|
@@ -601,16 +614,18 @@ export function createPartialUpdater(
|
|
|
601
614
|
segments = segmentIds ?? segmentState.currentSegmentIds;
|
|
602
615
|
}
|
|
603
616
|
|
|
604
|
-
// The page the client is on, which the server diffs the target against
|
|
605
|
-
//
|
|
606
|
-
//
|
|
607
|
-
//
|
|
608
|
-
//
|
|
609
|
-
//
|
|
617
|
+
// The page the client is on, which the server diffs the target against:
|
|
618
|
+
// the committed entry (the store's URL until tx.commit()), whose segment
|
|
619
|
+
// ids are sent above, or the intercept's source. Never window.location:
|
|
620
|
+
// a popstate has already moved it to the popped entry before that
|
|
621
|
+
// entry's fetch commits. Sent from there, a back/forward was told the
|
|
622
|
+
// target is also the source and kept the page being left (#1030), and a
|
|
623
|
+
// click during a back/forward that missed the history cache was judged
|
|
624
|
+
// against the popped entry while holding the page on screen's copies, so
|
|
625
|
+
// a layout kept for one page showed the other's.
|
|
610
626
|
const previousUrl =
|
|
611
|
-
mode.type
|
|
612
|
-
|
|
613
|
-
: interceptSourceUrl || tx.currentUrl || segmentState.currentUrl;
|
|
627
|
+
(mode.type !== "leave-intercept" && interceptSourceUrl) ||
|
|
628
|
+
segmentState.currentUrl;
|
|
614
629
|
|
|
615
630
|
if (IS_BROWSER_DEBUG) {
|
|
616
631
|
debugLog(`\n[Browser] >>> NAVIGATION`);
|
|
@@ -625,8 +640,7 @@ export function createPartialUpdater(
|
|
|
625
640
|
// transition({ when }). Every decision below runs before tx.commit()
|
|
626
641
|
// moves the store, so `from` is still the location being left:
|
|
627
642
|
// segmentState.currentUrl is the committed URL even on a popstate cache
|
|
628
|
-
// miss, where
|
|
629
|
-
// target.
|
|
643
|
+
// miss, where window.location already points at the target.
|
|
630
644
|
const whenKind: TransitionWhenKind =
|
|
631
645
|
mode.type === "action"
|
|
632
646
|
? "action"
|
|
@@ -707,6 +721,8 @@ export function createPartialUpdater(
|
|
|
707
721
|
boundary,
|
|
708
722
|
version: getVersion(),
|
|
709
723
|
routerId: store.getRouterId?.(),
|
|
724
|
+
acceptStalePrefetch:
|
|
725
|
+
mode.type === "navigate" && mode.onStalePrefetch !== undefined,
|
|
710
726
|
});
|
|
711
727
|
const streamingToken = tx.startStreaming();
|
|
712
728
|
const {
|
|
@@ -714,6 +730,9 @@ export function createPartialUpdater(
|
|
|
714
730
|
streamComplete: rawStreamComplete,
|
|
715
731
|
fullyPrefetched,
|
|
716
732
|
} = fetchResult;
|
|
733
|
+
if (mode.type === "navigate") {
|
|
734
|
+
mode.onStalePrefetch?.(fetchResult.stalePrefetch === true);
|
|
735
|
+
}
|
|
717
736
|
debugLog("payload.metadata", payload.metadata);
|
|
718
737
|
|
|
719
738
|
// Side effect only: end the streaming token once the stream settles.
|
|
@@ -1093,7 +1112,7 @@ export function createPartialUpdater(
|
|
|
1093
1112
|
|
|
1094
1113
|
// A background refresh keeps the stale page on screen when a loader it
|
|
1095
1114
|
// re-ran threw (its entries settled in the build): nothing is committed,
|
|
1096
|
-
// and navigation-bridge logs the rejection
|
|
1115
|
+
// and navigation-bridge logs the rejection. Every other lane
|
|
1097
1116
|
// throws it at the read. Before the abort check: the await yields.
|
|
1098
1117
|
if (mode.type === "stale-revalidation") {
|
|
1099
1118
|
const failed = await revalidationErrorOf(serverSegments);
|