@rangojs/router 0.32.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 -10173
- 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/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 +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 +13 -8
- package/skills/document-cache/SKILL.md +8 -0
- package/skills/hooks/navigation.md +8 -8
- package/skills/hooks/state.md +24 -8
- package/skills/intercept/SKILL.md +33 -12
- 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 +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 +67 -18
- 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 +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/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
|
@@ -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);
|
|
@@ -7,11 +7,14 @@
|
|
|
7
7
|
* stream resolves the route's client references, so the route's JS chunks are
|
|
8
8
|
* imported during prefetch rather than on click. The decoded payload is reused
|
|
9
9
|
* verbatim by navigation, so a prefetched click loads no new code. Two key
|
|
10
|
-
* scopes are in play
|
|
10
|
+
* scopes are in play. Wherever a key below says `rangoState`, callers pass the
|
|
11
|
+
* state's `{version}:{timestamp}` part (cookie-name.ts stateEpoch), not the
|
|
12
|
+
* whole value: a server action's write bump leaves the keys as they were.
|
|
11
13
|
* - Wildcard (default): built by `buildPrefetchKey(rangoState, target)` —
|
|
12
14
|
* shape `rangoState\0/target?...`. Shared across all source pages and
|
|
13
15
|
* source segment trees; `_rsc_segments` is omitted from this key. Invalidated
|
|
14
|
-
* automatically when Rango state bumps (deploy or
|
|
16
|
+
* automatically when Rango state bumps (deploy or invalidateClientCache()).
|
|
17
|
+
* A server action marks entries stale instead (the stale epoch, below).
|
|
15
18
|
* - Source-scoped: built by `buildSourceKey(rangoState, sourceHref, target)`
|
|
16
19
|
* — shape `rangoState\0sourceHref\0/target?...`. Embeds the Rango state
|
|
17
20
|
* (so rotation invalidates source-scoped entries too), the source href, and
|
|
@@ -20,6 +23,14 @@
|
|
|
20
23
|
* targets, matched or not), OR when a Link opts in with
|
|
21
24
|
* `prefetchKey=":source"` — in both cases so source-sensitive responses
|
|
22
25
|
* cannot bleed into navigations from other pages.
|
|
26
|
+
* - Params-scoped: built by `buildParamsKey(rangoState, label, target)` —
|
|
27
|
+
* shape `rangoState\0p:<label>\0/target?...`. Populated when the server
|
|
28
|
+
* tags a response with `X-RSC-Prefetch-Scope: params; <label>`: a
|
|
29
|
+
* revalidate() rule read the source's params or route name and kept a
|
|
30
|
+
* segment, so the answer is right for every page with equal values of
|
|
31
|
+
* what it read (prefetch-source-scope.ts). `paramsShapes` remembers, per
|
|
32
|
+
* target, what the stored labels read, so a lookup rebuilds the label of
|
|
33
|
+
* the page on screen (paramsCandidateKeys).
|
|
23
34
|
*
|
|
24
35
|
* Also tracks in-flight prefetch promises. Each promise resolves to the
|
|
25
36
|
* decoded prefetch entry (or null), letting navigation adopt a
|
|
@@ -44,6 +55,13 @@ import { notifyPrefetchCacheInvalidated } from "./invalidation.js";
|
|
|
44
55
|
import { invalidateRangoState } from "../rango-state.js";
|
|
45
56
|
import type { RscPayload } from "../types.js";
|
|
46
57
|
import { WIRE } from "../../wire-params.js";
|
|
58
|
+
import {
|
|
59
|
+
formatSourceLabel,
|
|
60
|
+
parseSourceShape,
|
|
61
|
+
sourceShapeId,
|
|
62
|
+
type SourcePage,
|
|
63
|
+
type SourceShape,
|
|
64
|
+
} from "../../prefetch-source-scope.js";
|
|
47
65
|
|
|
48
66
|
/**
|
|
49
67
|
* A prefetch that has been fetched AND eagerly decoded. Storing the decoded
|
|
@@ -64,9 +82,23 @@ export interface DecodedPrefetch {
|
|
|
64
82
|
* Prefetch scope as tagged by the server via `X-RSC-Prefetch-Scope`.
|
|
65
83
|
* `"source"` means the response is source-page-sensitive and must not be
|
|
66
84
|
* reused by a navigation from a different page — navigation enforces this
|
|
67
|
-
* when it adopted an inflight entry through the wildcard key.
|
|
85
|
+
* when it adopted an inflight entry through the wildcard key. `"params"`
|
|
86
|
+
* means it serves the pages whose label equals `paramsLabel`.
|
|
68
87
|
*/
|
|
69
|
-
scope:
|
|
88
|
+
scope: PrefetchScope;
|
|
89
|
+
/** The label of a `"params"` entry (prefetch-source-scope.ts). */
|
|
90
|
+
paramsLabel?: string;
|
|
91
|
+
/**
|
|
92
|
+
* The stale epoch (`currentPrefetchEpoch()`) the prefetch's fetch started
|
|
93
|
+
* under. A server action without a directive marks every prefetch stale
|
|
94
|
+
* (`markPrefetchesStale()` bumps the epoch) instead of flushing it: an entry
|
|
95
|
+
* from an older epoch is still served, and the navigation that adopts it
|
|
96
|
+
* revalidates its page once it settled (`isPrefetchStale`). A stale entry
|
|
97
|
+
* is served at most once, and only to a click (`consumePrefetch`'s
|
|
98
|
+
* `acceptStale`). A respawn keeps the original fetch's epoch, as it keeps
|
|
99
|
+
* its timestamp.
|
|
100
|
+
*/
|
|
101
|
+
epoch: number;
|
|
70
102
|
/**
|
|
71
103
|
* Synchronously-readable flag, flipped to true when `streamComplete` resolves
|
|
72
104
|
* (the entire RSC stream has drained). Navigation reads this at click time to
|
|
@@ -108,6 +140,9 @@ export interface DecodedPrefetch {
|
|
|
108
140
|
dispose?: () => void;
|
|
109
141
|
}
|
|
110
142
|
|
|
143
|
+
/** Where a prefetch is stored: see the module comment. */
|
|
144
|
+
export type PrefetchScope = "source" | "params" | "wildcard";
|
|
145
|
+
|
|
111
146
|
let cacheTTL = 300_000;
|
|
112
147
|
let fragmentPassthroughEnabled = true;
|
|
113
148
|
|
|
@@ -163,8 +198,25 @@ const inflightAliases = new Map<string, string[]>();
|
|
|
163
198
|
|
|
164
199
|
const adoptedKeys = new Set<string>();
|
|
165
200
|
|
|
201
|
+
/**
|
|
202
|
+
* What the stored params-scoped labels of a target read, by
|
|
203
|
+
* `buildPrefetchKey("", target)`: the shapes paramsCandidateKeys rebuilds
|
|
204
|
+
* labels from. Bounded like the cache; cleared with it.
|
|
205
|
+
*/
|
|
206
|
+
const paramsShapes = new Map<string, Map<string, SourceShape>>();
|
|
207
|
+
|
|
166
208
|
let generation = 0;
|
|
167
209
|
|
|
210
|
+
/**
|
|
211
|
+
* The stale epoch. `markPrefetchesStale()` bumps it; an entry whose fetch
|
|
212
|
+
* started under an older one is stale (`isPrefetchStale`). Separate from
|
|
213
|
+
* `generation`: a stale mark keeps every entry and every in-flight fetch,
|
|
214
|
+
* where a generation bump drops them. So a Flight response that has already
|
|
215
|
+
* arrived is never aborted by an action (aborting it mid-stream errors
|
|
216
|
+
* the stream): an in-flight prefetch finishes and lands stale.
|
|
217
|
+
*/
|
|
218
|
+
let staleEpoch = 0;
|
|
219
|
+
|
|
168
220
|
/**
|
|
169
221
|
* Build a cache key by combining a scope prefix with the target URL.
|
|
170
222
|
*
|
|
@@ -172,7 +224,7 @@ let generation = 0;
|
|
|
172
224
|
* one of:
|
|
173
225
|
* - Wildcard (source-agnostic): prefix is the Rango state value from
|
|
174
226
|
* `getRangoState()`. Shared across all source pages. Invalidated
|
|
175
|
-
* automatically when Rango state bumps (deploy or
|
|
227
|
+
* automatically when Rango state bumps (deploy or invalidateClientCache()).
|
|
176
228
|
* `_rsc_segments` is omitted so sibling payloads prefetched from one page
|
|
177
229
|
* remain reusable after another prefetched navigation commits. Key shape:
|
|
178
230
|
* `rangoState\0/target?...`.
|
|
@@ -218,6 +270,76 @@ export function buildSourceKey(
|
|
|
218
270
|
);
|
|
219
271
|
}
|
|
220
272
|
|
|
273
|
+
/**
|
|
274
|
+
* Build a params-scoped cache key. Key shape:
|
|
275
|
+
* `rangoState\0p:<label>\0/target?...`, the target normalized like the
|
|
276
|
+
* wildcard key's: the answer serves every page with this label, whatever
|
|
277
|
+
* segments it holds, as a wildcard answer serves every page.
|
|
278
|
+
*/
|
|
279
|
+
export function buildParamsKey(
|
|
280
|
+
rangoState: string,
|
|
281
|
+
label: string,
|
|
282
|
+
targetUrl: URL,
|
|
283
|
+
): string {
|
|
284
|
+
return buildPrefetchKey(rangoState + "\0p:" + label, targetUrl);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Remember that a params-scoped answer for `targetUrl` is stored under a
|
|
289
|
+
* label of this shape. Called where the response header is read, with the
|
|
290
|
+
* store (fetch.ts executePrefetchFetch).
|
|
291
|
+
*/
|
|
292
|
+
export function rememberParamsShape(targetUrl: URL, label: string): void {
|
|
293
|
+
const target = buildPrefetchKey("", targetUrl);
|
|
294
|
+
const shape = parseSourceShape(label);
|
|
295
|
+
let shapes = paramsShapes.get(target);
|
|
296
|
+
if (!shapes) {
|
|
297
|
+
if (paramsShapes.size >= maxPrefetchCacheSize) {
|
|
298
|
+
const oldest = paramsShapes.keys().next().value;
|
|
299
|
+
if (oldest !== undefined) paramsShapes.delete(oldest);
|
|
300
|
+
}
|
|
301
|
+
shapes = new Map();
|
|
302
|
+
paramsShapes.set(target, shapes);
|
|
303
|
+
}
|
|
304
|
+
shapes.set(sourceShapeId(shape), shape);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* The params-scoped keys a request for `targetUrl` from `page` may adopt:
|
|
309
|
+
* the page's label under each shape stored for the target, the shapes that
|
|
310
|
+
* read the most first. Empty without a page (before boot).
|
|
311
|
+
*/
|
|
312
|
+
export function paramsCandidateKeys(
|
|
313
|
+
rangoState: string,
|
|
314
|
+
targetUrl: URL,
|
|
315
|
+
page: SourcePage | null,
|
|
316
|
+
): string[] {
|
|
317
|
+
if (!page) return [];
|
|
318
|
+
const shapes = paramsShapes.get(buildPrefetchKey("", targetUrl));
|
|
319
|
+
if (!shapes) return [];
|
|
320
|
+
return [...shapes.values()]
|
|
321
|
+
.sort((a, b) => weightOf(b) - weightOf(a))
|
|
322
|
+
.map((shape) =>
|
|
323
|
+
buildParamsKey(rangoState, formatSourceLabel(shape, page), targetUrl),
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const weightOf = (shape: SourceShape): number =>
|
|
328
|
+
shape.names.length + (shape.route ? 1 : 0) + (shape.keys ? 1 : 0);
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Whether `page` is in the group a params-scoped entry serves: its label
|
|
332
|
+
* under the entry's shape equals the entry's.
|
|
333
|
+
*/
|
|
334
|
+
export function pageMatchesParamsLabel(
|
|
335
|
+
label: string,
|
|
336
|
+
page: SourcePage | null,
|
|
337
|
+
): boolean {
|
|
338
|
+
return (
|
|
339
|
+
page !== null && formatSourceLabel(parseSourceShape(label), page) === label
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
|
|
221
343
|
/**
|
|
222
344
|
* Walk an inflight key plus any sibling aliases registered via
|
|
223
345
|
* `setInflightPromiseWithAliases`, invoking `fn` for each.
|
|
@@ -233,13 +355,19 @@ function forEachAlias(key: string, fn: (k: string) => void): void {
|
|
|
233
355
|
|
|
234
356
|
/**
|
|
235
357
|
* Check if a prefetch is already cached, in-flight, or queued for the given key.
|
|
358
|
+
*
|
|
359
|
+
* A stale stored entry (`isPrefetchStale`) counts as absent and is dropped, so
|
|
360
|
+
* a new hover or viewport trigger replaces it with a fresh prefetch, and a
|
|
361
|
+
* click made while that one is in flight adopts it rather than the stale one.
|
|
362
|
+
* An in-flight fetch counts whatever its epoch: it is never aborted, and a
|
|
363
|
+
* duplicate request for its key would race it for the slot.
|
|
236
364
|
*/
|
|
237
365
|
export function hasPrefetch(key: string): boolean {
|
|
238
366
|
if (inflight.has(key)) return true;
|
|
239
367
|
if (cacheTTL <= 0) return false;
|
|
240
368
|
const entry = cache.get(key);
|
|
241
369
|
if (!entry) return false;
|
|
242
|
-
if (Date.now() - entry.timestamp > cacheTTL) {
|
|
370
|
+
if (Date.now() - entry.timestamp > cacheTTL || isPrefetchStale(entry.entry)) {
|
|
243
371
|
entry.entry.dispose?.();
|
|
244
372
|
cache.delete(key);
|
|
245
373
|
return false;
|
|
@@ -258,10 +386,20 @@ export function hasPrefetch(key: string): boolean {
|
|
|
258
386
|
* TTL bounds the age of the data, not of the latest adoption. Without
|
|
259
387
|
* `respawn` the entry is deleted (legacy one-shot behavior).
|
|
260
388
|
*
|
|
389
|
+
* A stale entry (`isPrefetchStale`) is served only to a caller that passes
|
|
390
|
+
* `acceptStale` (a click, which revalidates its page once it settled,
|
|
391
|
+
* navigation-bridge.ts): for any other caller it is a miss and stays in its
|
|
392
|
+
* slot. It is served at most once: its slot is not re-armed, so a second
|
|
393
|
+
* click on the same link fetches fresh rather than showing pre-write data
|
|
394
|
+
* older than what the first click's revalidation showed.
|
|
395
|
+
*
|
|
261
396
|
* Does NOT check in-flight prefetches — use consumeInflightPrefetch()
|
|
262
397
|
* for that (returns a Promise instead of a resolved entry).
|
|
263
398
|
*/
|
|
264
|
-
export function consumePrefetch(
|
|
399
|
+
export function consumePrefetch(
|
|
400
|
+
key: string,
|
|
401
|
+
options?: { acceptStale?: boolean },
|
|
402
|
+
): DecodedPrefetch | null {
|
|
265
403
|
if (cacheTTL <= 0) return null;
|
|
266
404
|
const cached = cache.get(key);
|
|
267
405
|
if (!cached) return null;
|
|
@@ -271,10 +409,15 @@ export function consumePrefetch(key: string): DecodedPrefetch | null {
|
|
|
271
409
|
return null;
|
|
272
410
|
}
|
|
273
411
|
const entry = cached.entry;
|
|
274
|
-
|
|
412
|
+
const stale = isPrefetchStale(entry);
|
|
413
|
+
if (stale && !options?.acceptStale) return null;
|
|
414
|
+
if (entry.respawn && !stale) {
|
|
275
415
|
cached.entry = entry.respawn();
|
|
276
416
|
} else {
|
|
277
417
|
cache.delete(key);
|
|
418
|
+
// The buffered reserve (fetch.ts) is never replayed: release it. It is a
|
|
419
|
+
// separate tee branch, so the payload the caller adopts keeps streaming.
|
|
420
|
+
if (stale) entry.dispose?.();
|
|
278
421
|
}
|
|
279
422
|
return entry;
|
|
280
423
|
}
|
|
@@ -318,12 +461,18 @@ export function consumeInflightPrefetch(
|
|
|
318
461
|
* Store an eagerly-decoded prefetch in the in-memory cache.
|
|
319
462
|
*
|
|
320
463
|
* Skips storage if the generation has changed since the fetch started
|
|
321
|
-
* (
|
|
464
|
+
* (the cache was flushed mid-flight). An entry from an older stale epoch (an
|
|
465
|
+
* action marked prefetches stale mid-flight) is stored, and stays stale.
|
|
466
|
+
*
|
|
467
|
+
* `inflightKey` is the key the prefetch was in flight under, when the
|
|
468
|
+
* storage key is not one of its aliases (a params-scoped answer): a
|
|
469
|
+
* navigation that adopted it marked the aliases.
|
|
322
470
|
*/
|
|
323
471
|
export function storePrefetch(
|
|
324
472
|
key: string,
|
|
325
473
|
entry: DecodedPrefetch,
|
|
326
474
|
fetchGeneration: number,
|
|
475
|
+
inflightKey: string = key,
|
|
327
476
|
): void {
|
|
328
477
|
if (cacheTTL <= 0) return;
|
|
329
478
|
if (fetchGeneration !== generation) return;
|
|
@@ -333,8 +482,8 @@ export function storePrefetch(
|
|
|
333
482
|
// publish it to the cache map, or a later navigation would be served the
|
|
334
483
|
// exhausted entry and lose that route's handles. Clear the marker (under all
|
|
335
484
|
// aliases) now that the decision is made.
|
|
336
|
-
if (adoptedKeys.has(
|
|
337
|
-
forEachAlias(
|
|
485
|
+
if (adoptedKeys.has(inflightKey)) {
|
|
486
|
+
forEachAlias(inflightKey, (k) => adoptedKeys.delete(k));
|
|
338
487
|
return;
|
|
339
488
|
}
|
|
340
489
|
|
|
@@ -398,6 +547,33 @@ export function currentGeneration(): number {
|
|
|
398
547
|
return generation;
|
|
399
548
|
}
|
|
400
549
|
|
|
550
|
+
/**
|
|
551
|
+
* Capture the current stale epoch. A prefetch fetch stamps its entry with the
|
|
552
|
+
* value read when it started (`DecodedPrefetch.epoch`).
|
|
553
|
+
*/
|
|
554
|
+
export function currentPrefetchEpoch(): number {
|
|
555
|
+
return staleEpoch;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
/** Whether an action marked prefetches stale after this entry's fetch began. */
|
|
559
|
+
export function isPrefetchStale(
|
|
560
|
+
entry: Pick<DecodedPrefetch, "epoch">,
|
|
561
|
+
): boolean {
|
|
562
|
+
return entry.epoch < staleEpoch;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
/**
|
|
566
|
+
* Mark every stored and in-flight prefetch stale: what a server action
|
|
567
|
+
* without a directive does (navigation-store.ts markCacheAsStaleAndBroadcast).
|
|
568
|
+
* Only bumps the epoch. Nothing is dropped, aborted or rotated, and visible
|
|
569
|
+
* links are not told (no `notifyPrefetchCacheInvalidated()`), so they do not
|
|
570
|
+
* prefetch again. A click on a stale entry commits it at once; the navigation
|
|
571
|
+
* then revalidates its page (navigation-bridge.ts navigate).
|
|
572
|
+
*/
|
|
573
|
+
export function markPrefetchesStale(): void {
|
|
574
|
+
staleEpoch++;
|
|
575
|
+
}
|
|
576
|
+
|
|
401
577
|
export function markPrefetchInflight(key: string): void {
|
|
402
578
|
inflight.add(key);
|
|
403
579
|
}
|
|
@@ -457,6 +633,7 @@ export function clearPrefetchCache(rotateRangoState = true): void {
|
|
|
457
633
|
inflightPromises.clear();
|
|
458
634
|
inflightAliases.clear();
|
|
459
635
|
adoptedKeys.clear();
|
|
636
|
+
paramsShapes.clear();
|
|
460
637
|
for (const cached of cache.values()) cached.entry.dispose?.();
|
|
461
638
|
cache.clear();
|
|
462
639
|
earliestTimestamp = Infinity;
|