@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
|
@@ -43,7 +43,7 @@ import {
|
|
|
43
43
|
generateHistoryKey,
|
|
44
44
|
liveHandleDataForCache,
|
|
45
45
|
} from "./navigation-store.js";
|
|
46
|
-
import type { EventController } from "./event-controller.js";
|
|
46
|
+
import type { EventController, NavigationEntry } from "./event-controller.js";
|
|
47
47
|
import { isInterceptOnlyCache } from "./intercept-utils.js";
|
|
48
48
|
import { actionBoundaryAt, idsAboveBoundary } from "./segment-reconciler.js";
|
|
49
49
|
import {
|
|
@@ -140,6 +140,82 @@ function isRestorable(
|
|
|
140
140
|
);
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
+
/**
|
|
144
|
+
* The request a stale-prefetch revalidation makes (scheduleStalePrefetch-
|
|
145
|
+
* Revalidation): the ids it holds, and the intercept and action error
|
|
146
|
+
* boundary on screen.
|
|
147
|
+
*/
|
|
148
|
+
export interface StalePrefetchRevalidation {
|
|
149
|
+
segmentIds: string[];
|
|
150
|
+
intercept: { isIntercept: boolean; sourceUrl?: string };
|
|
151
|
+
boundary: string | undefined;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* A click that adopted a prefetch a server action marked stale
|
|
156
|
+
* (prefetch/cache.ts isPrefetchStale) committed the pre-write payload, as any
|
|
157
|
+
* prefetched click commits. Once that navigation settled
|
|
158
|
+
* (NavigationEntry.settled: its commit, any `prefetch: false` fill and its
|
|
159
|
+
* stream, so this never supersedes the click nor cuts a response
|
|
160
|
+
* that arrived short), `revalidate` runs the page's request in
|
|
161
|
+
* Back's lane (the bridge's revalidateInBackground), with the ids on screen
|
|
162
|
+
* and the action error boundary it shows held, as Back onto a stale entry
|
|
163
|
+
* does.
|
|
164
|
+
*
|
|
165
|
+
* The entry is marked stale first, so a Back onto it revalidates when this
|
|
166
|
+
* one never ran or was left. Nothing runs when the click did not commit, or
|
|
167
|
+
* when the page moved since (a newer navigation aborts the click's entry, a
|
|
168
|
+
* shallow one moves the history key) or another navigation is open. An
|
|
169
|
+
* action in flight does not hold it back, as it does not hold Back's: its
|
|
170
|
+
* follow-up, when it owes one, starts a navigation that supersedes this one.
|
|
171
|
+
*
|
|
172
|
+
* Exported so renderRoute (testing/render-route.tsx) drives these guards and
|
|
173
|
+
* this plan rather than a copy of them.
|
|
174
|
+
*/
|
|
175
|
+
export function scheduleStalePrefetchRevalidation(
|
|
176
|
+
store: Pick<
|
|
177
|
+
NavigationStore,
|
|
178
|
+
"getCachedSegments" | "getHistoryKey" | "updateCacheHandleData"
|
|
179
|
+
>,
|
|
180
|
+
eventController: Pick<EventController, "getUnsettledNavigation">,
|
|
181
|
+
entry: Pick<NavigationEntry, "settled">,
|
|
182
|
+
historyKey: string,
|
|
183
|
+
revalidate: (request: StalePrefetchRevalidation) => void,
|
|
184
|
+
): void {
|
|
185
|
+
void entry.settled.then((committed) => {
|
|
186
|
+
if (!committed) return;
|
|
187
|
+
const cached = store.getCachedSegments(historyKey);
|
|
188
|
+
if (!cached) return;
|
|
189
|
+
store.updateCacheHandleData(historyKey, cached.handleData ?? {}, true);
|
|
190
|
+
if (
|
|
191
|
+
store.getHistoryKey() !== historyKey ||
|
|
192
|
+
eventController.getUnsettledNavigation() ||
|
|
193
|
+
!isRestorable(cached.segments)
|
|
194
|
+
) {
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
const historyState = window.history.state;
|
|
198
|
+
const isIntercept = historyState?.intercept === true;
|
|
199
|
+
const boundaryAt = isIntercept ? -1 : actionBoundaryAt(cached.segments);
|
|
200
|
+
if (boundaryAt === 0) return;
|
|
201
|
+
const boundary = cached.segments[boundaryAt]?.id;
|
|
202
|
+
debugLog("[Browser] Stale prefetch committed, background revalidating");
|
|
203
|
+
// A deferred Meta still pending when its handles were cached: a whole
|
|
204
|
+
// render re-streams them (handlePopstate, handlesPending).
|
|
205
|
+
const segmentIds = cached.handlesPending
|
|
206
|
+
? []
|
|
207
|
+
: idsAboveBoundary(
|
|
208
|
+
cached.segments.map((s) => s.id),
|
|
209
|
+
boundary,
|
|
210
|
+
);
|
|
211
|
+
revalidate({
|
|
212
|
+
segmentIds,
|
|
213
|
+
intercept: { isIntercept, sourceUrl: historyState?.sourceUrl },
|
|
214
|
+
boundary,
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
|
|
143
219
|
/**
|
|
144
220
|
* Create a navigation bridge for handling client-side navigation
|
|
145
221
|
*
|
|
@@ -236,6 +312,53 @@ export function createNavigationBridge(
|
|
|
236
312
|
});
|
|
237
313
|
}
|
|
238
314
|
|
|
315
|
+
/**
|
|
316
|
+
* Back's stale-revalidation lane: a background request for the page on
|
|
317
|
+
* screen (skipLoadingState: useNavigation() stays idle, useLinkStatus shows
|
|
318
|
+
* nothing) that holds `segmentIds` and an action error boundary on screen,
|
|
319
|
+
* re-caches the entry (cacheOnly) and commits as kind "revalidate". Used by
|
|
320
|
+
* a Back/Forward restore of a stale entry (handlePopstate) and after a click
|
|
321
|
+
* on a stale prefetch (scheduleStalePrefetchRevalidation).
|
|
322
|
+
*/
|
|
323
|
+
function revalidateInBackground(
|
|
324
|
+
url: string,
|
|
325
|
+
segmentIds: string[],
|
|
326
|
+
intercept: { isIntercept: boolean; sourceUrl?: string },
|
|
327
|
+
boundary: string | undefined,
|
|
328
|
+
): void {
|
|
329
|
+
const tx = createNavigationTransaction(store, eventController, url, {
|
|
330
|
+
skipLoadingState: true,
|
|
331
|
+
replace: true,
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
fetchPartialUpdate(
|
|
335
|
+
url,
|
|
336
|
+
segmentIds,
|
|
337
|
+
false,
|
|
338
|
+
tx.handle.signal,
|
|
339
|
+
tx.with({
|
|
340
|
+
url,
|
|
341
|
+
replace: true,
|
|
342
|
+
scroll: false,
|
|
343
|
+
intercept: intercept.isIntercept,
|
|
344
|
+
interceptSourceUrl: intercept.sourceUrl,
|
|
345
|
+
cacheOnly: true,
|
|
346
|
+
}),
|
|
347
|
+
{
|
|
348
|
+
type: "stale-revalidation",
|
|
349
|
+
interceptSourceUrl: intercept.sourceUrl,
|
|
350
|
+
boundary,
|
|
351
|
+
},
|
|
352
|
+
)
|
|
353
|
+
.catch((error) => {
|
|
354
|
+
if (isBackgroundSuppressible(error)) return;
|
|
355
|
+
console.error("[Browser] Background revalidation failed:", error);
|
|
356
|
+
})
|
|
357
|
+
.finally(() => {
|
|
358
|
+
tx[Symbol.dispose]();
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
|
|
239
362
|
const bridge: NavigationBridge = {
|
|
240
363
|
/**
|
|
241
364
|
* Navigate to a URL
|
|
@@ -387,6 +510,17 @@ export function createNavigationBridge(
|
|
|
387
510
|
store.updateCacheHandleData(sourceHistoryKey, currentHandleData);
|
|
388
511
|
}
|
|
389
512
|
|
|
513
|
+
// Read the rango state BEFORE the target entry's stale bit, as
|
|
514
|
+
// handlePopstate does. Another tab's write reaches this tab only through
|
|
515
|
+
// a read (rango-state.ts write observer) when its broadcast does not:
|
|
516
|
+
// the pages share no segment, or crossTabSync is off. The first read
|
|
517
|
+
// otherwise comes from the navigation client, after the held ids below
|
|
518
|
+
// are chosen, and the server keeps what the client says it holds when
|
|
519
|
+
// the default allows (router/revalidation.ts: a route whose params and
|
|
520
|
+
// search are the entry's, a layout loader with no revalidate()), so the
|
|
521
|
+
// entry's pre-write copy would commit as fresh.
|
|
522
|
+
getRangoState();
|
|
523
|
+
|
|
390
524
|
// Check if we have cached segments for target URL
|
|
391
525
|
const historyKey = generateHistoryKey(url);
|
|
392
526
|
const cached = store.getCachedSegments(historyKey);
|
|
@@ -441,6 +575,9 @@ export function createNavigationBridge(
|
|
|
441
575
|
state: resolvedState,
|
|
442
576
|
skipLoadingState: false,
|
|
443
577
|
});
|
|
578
|
+
// This navigation's entry when its response was a prefetch an action
|
|
579
|
+
// marked stale (scheduleStalePrefetchRevalidation).
|
|
580
|
+
let stalePrefetchEntry: NavigationEntry | null = null;
|
|
444
581
|
// A cross-route clientUrls() navigation presents its destination
|
|
445
582
|
// optimistically at once: transition({ when }) decides THERE (the first
|
|
446
583
|
// presentation), over the destination route's predicate and those of
|
|
@@ -520,8 +657,30 @@ export function createNavigationBridge(
|
|
|
520
657
|
...(clientUrlPresentation && {
|
|
521
658
|
presentation: clientUrlPresentation,
|
|
522
659
|
}),
|
|
660
|
+
// The last response decides: a fresh refetch after a stale
|
|
661
|
+
// adoption (a fill's start-over) clears it.
|
|
662
|
+
onStalePrefetch: (stale: boolean) => {
|
|
663
|
+
const entry = eventController.getUnsettledNavigation();
|
|
664
|
+
stalePrefetchEntry =
|
|
665
|
+
stale && entry?.abort === tx.handle.abort ? entry : null;
|
|
666
|
+
},
|
|
523
667
|
},
|
|
524
668
|
);
|
|
669
|
+
if (stalePrefetchEntry && !tx.handle.signal.aborted) {
|
|
670
|
+
scheduleStalePrefetchRevalidation(
|
|
671
|
+
store,
|
|
672
|
+
eventController,
|
|
673
|
+
stalePrefetchEntry,
|
|
674
|
+
store.getHistoryKey(),
|
|
675
|
+
({ segmentIds, intercept, boundary }) =>
|
|
676
|
+
revalidateInBackground(
|
|
677
|
+
window.location.href,
|
|
678
|
+
segmentIds,
|
|
679
|
+
intercept,
|
|
680
|
+
boundary,
|
|
681
|
+
),
|
|
682
|
+
);
|
|
683
|
+
}
|
|
525
684
|
} catch (error) {
|
|
526
685
|
// Aborted, or superseded by a newer navigation. A superseded nav may
|
|
527
686
|
// reject with a non-AbortError (e.g. a Flight decode that fails after its
|
|
@@ -846,39 +1005,12 @@ export function createNavigationBridge(
|
|
|
846
1005
|
// the deferred-Meta-aborted case, so action/cross-tab SWR keeps the
|
|
847
1006
|
// cheap diff path.
|
|
848
1007
|
const segmentIds = cached?.handlesPending ? [] : heldIds;
|
|
849
|
-
|
|
850
|
-
const tx = createNavigationTransaction(
|
|
851
|
-
store,
|
|
852
|
-
eventController,
|
|
853
|
-
url,
|
|
854
|
-
{ skipLoadingState: true, replace: true },
|
|
855
|
-
);
|
|
856
|
-
|
|
857
|
-
fetchPartialUpdate(
|
|
1008
|
+
revalidateInBackground(
|
|
858
1009
|
url,
|
|
859
1010
|
segmentIds,
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
url,
|
|
864
|
-
replace: true,
|
|
865
|
-
scroll: false,
|
|
866
|
-
intercept: isIntercept,
|
|
867
|
-
interceptSourceUrl,
|
|
868
|
-
cacheOnly: true,
|
|
869
|
-
}),
|
|
870
|
-
{ type: "stale-revalidation", interceptSourceUrl, boundary },
|
|
871
|
-
)
|
|
872
|
-
.catch((error) => {
|
|
873
|
-
if (isBackgroundSuppressible(error)) return;
|
|
874
|
-
console.error(
|
|
875
|
-
"[Browser] Background revalidation failed:",
|
|
876
|
-
error,
|
|
877
|
-
);
|
|
878
|
-
})
|
|
879
|
-
.finally(() => {
|
|
880
|
-
tx[Symbol.dispose]();
|
|
881
|
-
});
|
|
1011
|
+
{ isIntercept, sourceUrl: interceptSourceUrl },
|
|
1012
|
+
boundary,
|
|
1013
|
+
);
|
|
882
1014
|
}
|
|
883
1015
|
};
|
|
884
1016
|
// React commits a transition started while window.event is the
|
|
@@ -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
|
+
}
|