@rangojs/router 0.27.0 → 0.28.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/rango.js +1 -1
- package/dist/types/__internal.d.ts +7 -1
- package/dist/types/browser/event-controller.d.ts +2 -1
- package/dist/types/browser/history-state.d.ts +3 -9
- package/dist/types/browser/navigation-store.d.ts +8 -6
- package/dist/types/browser/react/filter-segment-order.d.ts +2 -2
- package/dist/types/browser/segment-reconciler.d.ts +6 -4
- package/dist/types/browser/types.d.ts +2 -1
- package/dist/types/browser/wire-protocol.d.ts +25 -0
- package/dist/types/cache/cache-key-utils.d.ts +11 -5
- package/dist/types/cache/cache-scope.d.ts +34 -22
- package/dist/types/cache/cf/cf-cache-constants.d.ts +2 -2
- package/dist/types/cache/cf/cf-cache-store.d.ts +47 -65
- package/dist/types/cache/cf/cf-kv-utils.d.ts +1 -1
- package/dist/types/cache/handle-snapshot.d.ts +9 -4
- package/dist/types/cache/segment-codec.d.ts +14 -0
- package/dist/types/cache/shell-snapshot.d.ts +1 -1
- package/dist/types/cache/tag-invalidation.d.ts +10 -0
- package/dist/types/cache/taint.d.ts +5 -3
- package/dist/types/cache/types.d.ts +7 -3
- package/dist/types/cache/vercel/vercel-cache-store.d.ts +8 -6
- package/dist/types/prefetch-markers.d.ts +5 -14
- package/dist/types/prerender/store.d.ts +31 -0
- package/dist/types/prerender/warm-request.d.ts +3 -3
- package/dist/types/prerender.d.ts +2 -2
- package/dist/types/route-name.d.ts +5 -0
- package/dist/types/router/error-handling.d.ts +4 -4
- package/dist/types/router/instrument.d.ts +4 -3
- package/dist/types/router/intercept-resolution.d.ts +25 -9
- package/dist/types/router/match-context.d.ts +4 -11
- package/dist/types/router/match-handlers.d.ts +2 -6
- package/dist/types/router/match-middleware/background-revalidation.d.ts +10 -20
- package/dist/types/router/match-middleware/cache-lookup.d.ts +34 -86
- package/dist/types/router/match-middleware/segment-resolution.d.ts +8 -6
- package/dist/types/router/prerender-match.d.ts +21 -6
- package/dist/types/router/preview-match.d.ts +2 -7
- package/dist/types/router/request-classification.d.ts +5 -0
- package/dist/types/router/router-context.d.ts +7 -46
- package/dist/types/router/router-interfaces.d.ts +8 -14
- package/dist/types/router/segment-resolution/loader-cache.d.ts +3 -3
- package/dist/types/router/segment-resolution/loader-mask.d.ts +4 -4
- package/dist/types/router/segment-resolution/prefetch-deferral.d.ts +20 -18
- package/dist/types/router/segment-resolution/resolution-pass.d.ts +93 -0
- package/dist/types/router/segment-resolution/revalidation.d.ts +82 -31
- package/dist/types/router/segment-resolution.d.ts +1 -2
- package/dist/types/router/segment-wrappers.d.ts +11 -48
- package/dist/types/router/telemetry.d.ts +2 -0
- package/dist/types/router/types.d.ts +10 -0
- package/dist/types/rsc/full-payload.d.ts +2 -2
- package/dist/types/rsc/handler-context.d.ts +6 -6
- package/dist/types/rsc/json-route-result.d.ts +5 -9
- package/dist/types/rsc/request-stages.d.ts +203 -0
- package/dist/types/rsc/response-cache-serve.d.ts +7 -12
- package/dist/types/rsc/response-route-handler.d.ts +2 -2
- package/dist/types/rsc/routine-plan.d.ts +1 -1
- package/dist/types/rsc/server-action.d.ts +2 -2
- package/dist/types/rsc/shell-capture-constants.d.ts +1 -1
- package/dist/types/rsc/shell-capture.d.ts +132 -15
- package/dist/types/rsc/shell-serve.d.ts +37 -12
- package/dist/types/server/context.d.ts +9 -8
- package/dist/types/server/handle-store.d.ts +16 -0
- package/dist/types/server/render-pass.d.ts +72 -0
- package/dist/types/server/request-context.d.ts +24 -104
- package/dist/types/server/rewrite-trail.d.ts +2 -2
- package/dist/types/testing/dispatch.d.ts +35 -18
- package/dist/types/types/cache-types.d.ts +6 -3
- package/dist/types/types/error-types.d.ts +9 -1
- package/dist/types/types/handler-context.d.ts +9 -1
- package/dist/types/types/segments.d.ts +1 -1
- package/dist/types/vite/discovery/build-discovery-phase.d.ts +40 -0
- package/dist/types/vite/discovery/build-env.d.ts +22 -0
- package/dist/types/vite/discovery/bundle-scan-phase.d.ts +12 -0
- package/dist/types/vite/discovery/dev-prerender-cache.d.ts +3 -2
- package/dist/types/vite/discovery/dev-server-phase.d.ts +26 -0
- package/dist/types/vite/discovery/discovery-errors.d.ts +1 -1
- package/dist/types/vite/discovery/state.d.ts +12 -0
- package/dist/types/vite/plugins/version-plugin.d.ts +1 -1
- package/dist/types/vite/plugins/virtual-entries.d.ts +1 -1
- package/dist/types/vite/router-discovery.d.ts +8 -47
- package/dist/types/vite/temp-rsc-server.d.ts +39 -0
- package/dist/types/vite/utils/prerender-utils.d.ts +9 -0
- package/dist/types/wire-params.d.ts +34 -0
- package/dist/vite/index.js +3809 -3710
- package/package.json +1 -1
- package/skills/cache-guide/SKILL.md +8 -5
- package/skills/caching/SKILL.md +37 -18
- package/skills/document-cache/SKILL.md +13 -1
- package/skills/handler-use/SKILL.md +1 -1
- package/skills/intercept/SKILL.md +17 -7
- package/skills/middleware/SKILL.md +18 -0
- package/skills/ppr/SKILL.md +18 -3
- package/skills/prerender/SKILL.md +39 -15
- package/skills/rango/SKILL.md +5 -1
- package/skills/server-actions/SKILL.md +8 -1
- package/skills/testing/response-routes.md +6 -4
- package/skills/use-cache/SKILL.md +46 -8
- package/src/__internal.ts +7 -1
- package/src/browser/connection-warmup.ts +3 -1
- package/src/browser/event-controller.ts +5 -3
- package/src/browser/history-state.ts +6 -12
- package/src/browser/navigation-bridge.ts +14 -13
- package/src/browser/navigation-client.ts +18 -25
- package/src/browser/navigation-store.ts +25 -27
- package/src/browser/navigation-transaction.ts +4 -1
- package/src/browser/prefetch/cache.ts +2 -1
- package/src/browser/prefetch/fetch.ts +8 -16
- package/src/browser/prefetch/queue.ts +4 -4
- package/src/browser/react/NavigationProvider.tsx +2 -1
- package/src/browser/react/filter-segment-order.ts +2 -2
- package/src/browser/rsc-router.tsx +1 -0
- package/src/browser/segment-reconciler.ts +66 -53
- package/src/browser/server-action-bridge.ts +50 -57
- package/src/browser/types.ts +2 -1
- package/src/browser/wire-protocol.ts +41 -0
- package/src/cache/cache-key-utils.ts +11 -5
- package/src/cache/cache-runtime.ts +110 -50
- package/src/cache/cache-scope.ts +110 -64
- package/src/cache/cf/cf-cache-constants.ts +2 -2
- package/src/cache/cf/cf-cache-store.ts +486 -874
- package/src/cache/cf/cf-kv-utils.ts +1 -1
- package/src/cache/document-cache.ts +34 -13
- package/src/cache/handle-snapshot.ts +19 -7
- package/src/cache/segment-codec.ts +55 -0
- package/src/cache/shell-snapshot.ts +2 -2
- package/src/cache/tag-invalidation.ts +21 -2
- package/src/cache/taint.ts +5 -3
- package/src/cache/types.ts +7 -3
- package/src/cache/vercel/vercel-cache-store.ts +243 -310
- package/src/handles/pushed-by.ts +1 -1
- package/src/prefetch-markers.ts +5 -16
- package/src/prerender/store.ts +76 -0
- package/src/prerender/warm-request.ts +8 -4
- package/src/prerender.ts +2 -2
- package/src/route-name.ts +8 -0
- package/src/router/error-handling.ts +4 -4
- package/src/router/instrument.ts +4 -3
- package/src/router/intercept-resolution.ts +77 -75
- package/src/router/loader-resolution.ts +5 -4
- package/src/router/match-api.ts +4 -5
- package/src/router/match-context.ts +20 -12
- package/src/router/match-handlers.ts +8 -20
- package/src/router/match-middleware/background-revalidation.ts +41 -53
- package/src/router/match-middleware/cache-lookup.ts +675 -726
- package/src/router/match-middleware/cache-store.ts +4 -8
- package/src/router/match-middleware/intercept-resolution.ts +3 -23
- package/src/router/match-middleware/segment-resolution.ts +34 -61
- package/src/router/middleware.ts +69 -11
- package/src/router/navigation-snapshot.ts +4 -4
- package/src/router/prerender-match.ts +100 -44
- package/src/router/preview-match.ts +6 -41
- package/src/router/request-classification.ts +13 -7
- package/src/router/route-snapshot.ts +2 -5
- package/src/router/router-context.ts +10 -95
- package/src/router/router-interfaces.ts +8 -14
- package/src/router/segment-resolution/loader-cache.ts +14 -9
- package/src/router/segment-resolution/loader-mask.ts +5 -5
- package/src/router/segment-resolution/prefetch-deferral.ts +30 -27
- package/src/router/segment-resolution/resolution-pass.ts +137 -0
- package/src/router/segment-resolution/revalidation.ts +897 -776
- package/src/router/segment-resolution.ts +4 -13
- package/src/router/segment-wrappers.ts +51 -269
- package/src/router/telemetry.ts +5 -0
- package/src/router/types.ts +10 -0
- package/src/router.ts +12 -13
- package/src/rsc/full-payload.ts +6 -5
- package/src/rsc/handler-context.ts +8 -14
- package/src/rsc/handler.ts +126 -507
- package/src/rsc/json-route-result.ts +5 -9
- package/src/rsc/loader-fetch.ts +6 -5
- package/src/rsc/progressive-enhancement.ts +5 -0
- package/src/rsc/request-stages.ts +757 -0
- package/src/rsc/response-cache-serve.ts +10 -14
- package/src/rsc/response-route-handler.ts +13 -5
- package/src/rsc/rewrite-passes.ts +2 -1
- package/src/rsc/routine-plan.ts +1 -1
- package/src/rsc/rsc-rendering.ts +164 -349
- package/src/rsc/server-action.ts +87 -113
- package/src/rsc/shell-capture-constants.ts +1 -1
- package/src/rsc/shell-capture.ts +212 -169
- package/src/rsc/shell-serve.ts +121 -18
- package/src/rsc/ssr-setup.ts +5 -4
- package/src/segment-system.tsx +3 -3
- package/src/server/context.ts +22 -25
- package/src/server/handle-store.ts +55 -0
- package/src/server/render-pass.ts +291 -0
- package/src/server/request-context.ts +26 -115
- package/src/server/rewrite-trail.ts +6 -4
- package/src/ssr/index.tsx +1 -1
- package/src/testing/dispatch.ts +223 -710
- package/src/testing/e2e/page-helpers.ts +2 -2
- package/src/testing/serve-shell-request.ts +23 -36
- package/src/types/cache-types.ts +6 -3
- package/src/types/error-types.ts +9 -1
- package/src/types/handler-context.ts +12 -1
- package/src/types/segments.ts +1 -1
- package/src/vite/discovery/build-discovery-phase.ts +204 -0
- package/src/vite/discovery/build-env.ts +148 -0
- package/src/vite/discovery/bundle-scan-phase.ts +128 -0
- package/src/vite/discovery/dev-prerender-cache.ts +8 -3
- package/src/vite/discovery/dev-server-phase.ts +1776 -0
- package/src/vite/discovery/discover-routers.ts +1 -1
- package/src/vite/discovery/discovery-errors.ts +1 -1
- package/src/vite/discovery/prerender-collection.ts +24 -39
- package/src/vite/discovery/state.ts +15 -0
- package/src/vite/plugins/version-plugin.ts +1 -1
- package/src/vite/plugins/virtual-entries.ts +1 -1
- package/src/vite/router-discovery.ts +29 -2428
- package/src/vite/temp-rsc-server.ts +223 -0
- package/src/vite/utils/prerender-utils.ts +47 -0
- package/src/wire-params.ts +35 -0
- package/dist/types/router/segment-resolution/fresh.d.ts +0 -72
- package/src/router/segment-resolution/fresh.ts +0 -945
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
resolveLocationStateEntries,
|
|
10
10
|
stampLocationState,
|
|
11
11
|
} from "./react/location-state-shared.js";
|
|
12
|
+
import { WIRE } from "../wire-params.js";
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
15
|
* Check if state is from typed LocationStateEntry[] (has __rsc_ls_ keys)
|
|
@@ -137,16 +138,9 @@ export function pushHistoryWithIdx(
|
|
|
137
138
|
}
|
|
138
139
|
|
|
139
140
|
/**
|
|
140
|
-
*
|
|
141
|
-
*
|
|
142
|
-
*
|
|
143
|
-
* cache miss.
|
|
144
|
-
*/
|
|
145
|
-
export const SHELL_MISS_MARKER: string = "_rsc_shell";
|
|
146
|
-
|
|
147
|
-
/**
|
|
148
|
-
* Drop the forced-MISS marker from the address bar at boot, keeping the
|
|
149
|
-
* history entry's state (initBrowserApp calls it before any location read).
|
|
141
|
+
* Drop the forced-MISS marker (`WIRE.shellMiss`, rsc/shell-serve.ts) from the
|
|
142
|
+
* address bar at boot, keeping the history entry's state (initBrowserApp
|
|
143
|
+
* calls it before any location read).
|
|
150
144
|
* Left in place, a refresh or a shared link would stay off the shell and the
|
|
151
145
|
* app's search params would carry it. The server strips the same marker from
|
|
152
146
|
* the request before rendering (shell-serve.ts withoutShellMissMarker), so
|
|
@@ -154,8 +148,8 @@ export const SHELL_MISS_MARKER: string = "_rsc_shell";
|
|
|
154
148
|
*/
|
|
155
149
|
export function stripShellMissMarker(): void {
|
|
156
150
|
const url = new URL(window.location.href);
|
|
157
|
-
if (!url.searchParams.has(
|
|
158
|
-
url.searchParams.delete(
|
|
151
|
+
if (!url.searchParams.has(WIRE.shellMiss)) return;
|
|
152
|
+
url.searchParams.delete(WIRE.shellMiss);
|
|
159
153
|
window.history.replaceState(window.history.state, "", url.href);
|
|
160
154
|
}
|
|
161
155
|
|
|
@@ -273,15 +273,18 @@ export function createNavigationBridge(
|
|
|
273
273
|
targetUrl.pathname === new URL(window.location.href).pathname
|
|
274
274
|
) {
|
|
275
275
|
// Preserve intercept context from the current history entry so that
|
|
276
|
-
// popstate uses the
|
|
277
|
-
//
|
|
276
|
+
// popstate uses the same cache key and restores the right full-page
|
|
277
|
+
// vs intercept semantics.
|
|
278
278
|
const currentHistoryState = window.history.state;
|
|
279
279
|
const isIntercept = currentHistoryState?.intercept === true;
|
|
280
280
|
const interceptSourceUrl = isIntercept
|
|
281
281
|
? currentHistoryState?.sourceUrl
|
|
282
282
|
: undefined;
|
|
283
283
|
|
|
284
|
-
const historyKey = generateHistoryKey(url, {
|
|
284
|
+
const historyKey = generateHistoryKey(url, {
|
|
285
|
+
intercept: isIntercept,
|
|
286
|
+
interceptSource: interceptSourceUrl,
|
|
287
|
+
});
|
|
285
288
|
|
|
286
289
|
// Copy current segments to the new history key so back/forward restores instantly.
|
|
287
290
|
// The array itself, not a clone: getPageHistoryKeys reads a shared
|
|
@@ -405,12 +408,6 @@ export function createNavigationBridge(
|
|
|
405
408
|
});
|
|
406
409
|
}
|
|
407
410
|
|
|
408
|
-
// Also check if there's an intercept cache entry for this URL
|
|
409
|
-
// If so, this URL CAN be intercepted, and we shouldn't use the non-intercept cache
|
|
410
|
-
// because the navigation might result in an intercept (depending on source URL)
|
|
411
|
-
const interceptHistoryKey = generateHistoryKey(url, { intercept: true });
|
|
412
|
-
const hasInterceptCache = store.hasHistoryCache(interceptHistoryKey);
|
|
413
|
-
|
|
414
411
|
// Skip cached SWR for:
|
|
415
412
|
// 1. intercept caches - interception depends on source page context
|
|
416
413
|
// 2. routes that CAN be intercepted - we don't know if this navigation will intercept
|
|
@@ -420,10 +417,12 @@ export function createNavigationBridge(
|
|
|
420
417
|
const hasUsableCache =
|
|
421
418
|
isRestorable(cachedSegments) &&
|
|
422
419
|
!isInterceptOnlyCache(cachedSegments) &&
|
|
423
|
-
!hasInterceptCache &&
|
|
424
420
|
!isLeavingIntercept &&
|
|
425
421
|
!cached?.stale &&
|
|
426
|
-
!options?._skipCache
|
|
422
|
+
!options?._skipCache &&
|
|
423
|
+
// A cached intercept entry for this URL (from any source page) means
|
|
424
|
+
// the navigation might intercept, depending on the source URL.
|
|
425
|
+
!store.hasInterceptCache(historyKey);
|
|
427
426
|
|
|
428
427
|
// Forward navigations always await fetchPartialUpdate before rendering,
|
|
429
428
|
// so useNavigation should always report "loading". skipLoadingState is
|
|
@@ -664,8 +663,10 @@ export function createNavigationBridge(
|
|
|
664
663
|
const isLeavingIntercept =
|
|
665
664
|
!isIntercept && currentInterceptSource !== null;
|
|
666
665
|
|
|
667
|
-
|
|
668
|
-
|
|
666
|
+
const historyKey = generateHistoryKey(url, {
|
|
667
|
+
intercept: isIntercept,
|
|
668
|
+
interceptSource: interceptSourceUrl,
|
|
669
|
+
});
|
|
669
670
|
|
|
670
671
|
debugLog(
|
|
671
672
|
"[Browser] Popstate -",
|
|
@@ -38,7 +38,7 @@ import {
|
|
|
38
38
|
type DecodedPrefetch,
|
|
39
39
|
} from "./prefetch/cache.js";
|
|
40
40
|
import { cancelAllPrefetches } from "./prefetch/loader.js";
|
|
41
|
-
import {
|
|
41
|
+
import { writePartialParams } from "./wire-protocol.js";
|
|
42
42
|
|
|
43
43
|
/**
|
|
44
44
|
* Create a navigation client for fetching RSC payloads
|
|
@@ -103,30 +103,23 @@ export function createNavigationClient(
|
|
|
103
103
|
}
|
|
104
104
|
|
|
105
105
|
// Build fetch URL with partial rendering params (used for both
|
|
106
|
-
// cache key lookup and actual fetch if cache misses)
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
}
|
|
124
|
-
// A param, not a header: the document cache decides from the raw URL
|
|
125
|
-
// (cache/document-cache.ts), and it keeps the fill out of every
|
|
126
|
-
// prefetch key below.
|
|
127
|
-
if (fill) {
|
|
128
|
-
fetchUrl.searchParams.set(FILL_PARAM, "1");
|
|
129
|
-
}
|
|
106
|
+
// cache key lookup and actual fetch if cache misses). The page left's
|
|
107
|
+
// rewrite target (#1144) is in both prefetch keys, like the params
|
|
108
|
+
// around it. The fill is a param, not a header: the document cache
|
|
109
|
+
// decides from the raw URL (cache/document-cache.ts), and it keeps the
|
|
110
|
+
// fill out of every prefetch key below.
|
|
111
|
+
const fetchUrl = writePartialParams(
|
|
112
|
+
new URL(targetUrl, window.location.origin),
|
|
113
|
+
{
|
|
114
|
+
segmentIds,
|
|
115
|
+
emptySegments: "send",
|
|
116
|
+
stale: staleRevalidation,
|
|
117
|
+
version,
|
|
118
|
+
routerId,
|
|
119
|
+
rewriteTarget,
|
|
120
|
+
fill,
|
|
121
|
+
},
|
|
122
|
+
);
|
|
130
123
|
|
|
131
124
|
// If the lazy prefetch runtime has not loaded yet, prevent a deferred
|
|
132
125
|
// speculative request from starting after this navigation misses the
|
|
@@ -34,7 +34,7 @@ const HISTORY_ENTRY_MEMORY_SIZE = 200;
|
|
|
34
34
|
// stale=true means the data may be outdated and should be revalidated on access.
|
|
35
35
|
// navInstance is the monotonic nav-instance token (see navInstance below): it
|
|
36
36
|
// identifies the per-commit visit that owns this entry. generateHistoryKey is
|
|
37
|
-
// URL
|
|
37
|
+
// the URL (plus the source page for an intercept), so A->B->A reuses the key; the token lets a late async
|
|
38
38
|
// resolution tell its own visit's entry apart from a newer same-URL visit's, so
|
|
39
39
|
// a stale nav can never clobber a fresher one.
|
|
40
40
|
// handlesPending=true means the entry's handle data is INCOMPLETE (a deferred
|
|
@@ -124,36 +124,35 @@ function getCacheInvalidationChannel(): BroadcastChannel | null {
|
|
|
124
124
|
* Options for generating a history key
|
|
125
125
|
*/
|
|
126
126
|
export interface HistoryKeyOptions {
|
|
127
|
-
/**
|
|
127
|
+
/** Key an intercept entry (apart from the plain entry for the URL). */
|
|
128
128
|
intercept?: boolean;
|
|
129
|
+
/** The page the intercept was opened over (history.state.sourceUrl). */
|
|
130
|
+
interceptSource?: string;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function urlKey(url: string): string {
|
|
134
|
+
const parsed = new URL(url, "http://localhost");
|
|
135
|
+
return parsed.pathname + parsed.search;
|
|
129
136
|
}
|
|
130
137
|
|
|
131
138
|
/**
|
|
132
|
-
* Generate a cache key from a URL.
|
|
133
|
-
*
|
|
134
|
-
* Hash fragments (#) are excluded since they don't affect server data.
|
|
139
|
+
* Generate a cache key from a URL: pathname + search. Hash fragments (#) are
|
|
140
|
+
* excluded since they don't affect server data.
|
|
135
141
|
*
|
|
136
|
-
*
|
|
137
|
-
*
|
|
142
|
+
* An intercept entry is keyed `<url>:intercept:<source>`: its cached tree
|
|
143
|
+
* includes the page under the intercept. hasInterceptCache relies on the
|
|
144
|
+
* `:intercept:` separator.
|
|
138
145
|
*/
|
|
139
146
|
export function generateHistoryKey(
|
|
140
147
|
url?: string,
|
|
141
148
|
options?: HistoryKeyOptions,
|
|
142
149
|
): string {
|
|
143
|
-
|
|
144
|
-
url
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
let key = parsed.pathname + parsed.search;
|
|
150
|
-
|
|
151
|
-
// Append intercept suffix for separate caching
|
|
152
|
-
if (options?.intercept) {
|
|
153
|
-
key += ":intercept";
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
return key;
|
|
150
|
+
const key = urlKey(
|
|
151
|
+
url || (typeof window !== "undefined" ? window.location.href : "/"),
|
|
152
|
+
);
|
|
153
|
+
if (!options?.intercept) return key;
|
|
154
|
+
const source = options.interceptSource;
|
|
155
|
+
return `${key}:intercept:${source ? urlKey(source) : ""}`;
|
|
157
156
|
}
|
|
158
157
|
|
|
159
158
|
/**
|
|
@@ -258,7 +257,8 @@ export function createNavigationStore(
|
|
|
258
257
|
|
|
259
258
|
// Monotonic nav-instance token. Bumped each time a cache entry is created or
|
|
260
259
|
// replaced in cacheSegmentsForHistory (i.e. once per commit). Because
|
|
261
|
-
// generateHistoryKey is URL
|
|
260
|
+
// generateHistoryKey is the URL (plus the source page for an intercept), two
|
|
261
|
+
// visits to the same URL share a key; this
|
|
262
262
|
// token gives each visit a distinct identity so a late async handle resolution
|
|
263
263
|
// can tell whether it still owns the live page / the target cache entry, and
|
|
264
264
|
// never overwrite a newer same-URL visit's state.
|
|
@@ -580,11 +580,9 @@ export function createNavigationStore(
|
|
|
580
580
|
};
|
|
581
581
|
},
|
|
582
582
|
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
hasHistoryCache(historyKey: string): boolean {
|
|
587
|
-
return historyCache.some(([key]) => key === historyKey);
|
|
583
|
+
hasInterceptCache(historyKey: string): boolean {
|
|
584
|
+
const prefix = `${historyKey}:intercept:`;
|
|
585
|
+
return historyCache.some(([key]) => key.startsWith(prefix));
|
|
588
586
|
},
|
|
589
587
|
|
|
590
588
|
/**
|
|
@@ -180,7 +180,10 @@ export function createNavigationTransaction(
|
|
|
180
180
|
|
|
181
181
|
const parsedUrl = new URL(url, window.location.origin);
|
|
182
182
|
|
|
183
|
-
const historyKey = generateHistoryKey(url, {
|
|
183
|
+
const historyKey = generateHistoryKey(url, {
|
|
184
|
+
intercept,
|
|
185
|
+
interceptSource: interceptSourceUrl,
|
|
186
|
+
});
|
|
184
187
|
|
|
185
188
|
if (cacheOnly) {
|
|
186
189
|
const currentHandleData = eventController.getHandleState().data;
|
|
@@ -43,6 +43,7 @@ import { abortAllPrefetches } from "./loader.js";
|
|
|
43
43
|
import { notifyPrefetchCacheInvalidated } from "./invalidation.js";
|
|
44
44
|
import { invalidateRangoState } from "../rango-state.js";
|
|
45
45
|
import type { RscPayload } from "../types.js";
|
|
46
|
+
import { WIRE } from "../../wire-params.js";
|
|
46
47
|
|
|
47
48
|
/**
|
|
48
49
|
* A prefetch that has been fetched AND eagerly decoded. Storing the decoded
|
|
@@ -190,7 +191,7 @@ let generation = 0;
|
|
|
190
191
|
*/
|
|
191
192
|
export function buildPrefetchKey(prefix: string, targetUrl: URL): string {
|
|
192
193
|
const normalizedTarget = new URL(targetUrl);
|
|
193
|
-
normalizedTarget.searchParams.delete(
|
|
194
|
+
normalizedTarget.searchParams.delete(WIRE.segments);
|
|
194
195
|
return prefix + "\0" + normalizedTarget.pathname + normalizedTarget.search;
|
|
195
196
|
}
|
|
196
197
|
|
|
@@ -38,6 +38,7 @@ import { SEGMENT_FRAGMENT_CAPABILITY_HEADER } from "../../segment-fragments.js";
|
|
|
38
38
|
import { PREFETCH_HEADER } from "../../prefetch-markers.js";
|
|
39
39
|
import type { RscPayload } from "../types.js";
|
|
40
40
|
import { getRegisteredStore } from "../navigation-store-handle.js";
|
|
41
|
+
import { writePartialParams } from "../wire-protocol.js";
|
|
41
42
|
|
|
42
43
|
/**
|
|
43
44
|
* Decoder injected at app startup (see setPrefetchDecoder). This is
|
|
@@ -153,24 +154,15 @@ function buildPrefetchUrl(
|
|
|
153
154
|
if (targetUrl.origin !== window.location.origin) {
|
|
154
155
|
return null;
|
|
155
156
|
}
|
|
156
|
-
targetUrl.searchParams.set("_rsc_partial", "true");
|
|
157
|
-
if (segmentIds.length > 0) {
|
|
158
|
-
targetUrl.searchParams.set("_rsc_segments", segmentIds.join(","));
|
|
159
|
-
}
|
|
160
|
-
if (version) {
|
|
161
|
-
targetUrl.searchParams.set("_rsc_v", version);
|
|
162
|
-
}
|
|
163
|
-
if (routerId) {
|
|
164
|
-
targetUrl.searchParams.set("_rsc_rid", routerId);
|
|
165
|
-
}
|
|
166
157
|
// The page's rewrite target, so the server diffs against the route that
|
|
167
158
|
// rendered it, as for the navigation.
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
159
|
+
return writePartialParams(targetUrl, {
|
|
160
|
+
segmentIds,
|
|
161
|
+
emptySegments: "omit",
|
|
162
|
+
version,
|
|
163
|
+
routerId,
|
|
164
|
+
rewriteTarget: getRegisteredStore()?.getHistoryEntryMemory()?.rewriteTarget,
|
|
165
|
+
});
|
|
174
166
|
}
|
|
175
167
|
|
|
176
168
|
/**
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
import { wait, waitForIdle, waitForViewportImages } from "./resource-ready.js";
|
|
18
|
+
import { WIRE_PREFIX } from "../../wire-params.js";
|
|
18
19
|
|
|
19
20
|
// Max prefetches executing at once. Mirrors DEFAULT_PREFETCH_CONCURRENCY
|
|
20
21
|
// (router/prefetch-limits.ts); kept as a local literal so the client bundle
|
|
@@ -128,16 +129,15 @@ export function enqueuePrefetch(
|
|
|
128
129
|
|
|
129
130
|
/**
|
|
130
131
|
* Normalize a URL-like string for keep-alive matching: parse against a
|
|
131
|
-
* placeholder origin and strip
|
|
132
|
+
* placeholder origin and strip every `WIRE_PREFIX` query param. Returns
|
|
132
133
|
* `pathname + search` so comparisons ignore hash and the internal params
|
|
133
|
-
* that prefetch appends to targets
|
|
134
|
-
* `_rsc_v`, `_rsc_rid`, `_rsc_stale`).
|
|
134
|
+
* that prefetch appends to targets.
|
|
135
135
|
*/
|
|
136
136
|
function normalizeForMatch(urlish: string): string {
|
|
137
137
|
try {
|
|
138
138
|
const u = new URL(urlish, "http://placeholder");
|
|
139
139
|
for (const k of [...u.searchParams.keys()]) {
|
|
140
|
-
if (k.startsWith(
|
|
140
|
+
if (k.startsWith(WIRE_PREFIX)) u.searchParams.delete(k);
|
|
141
141
|
}
|
|
142
142
|
return u.pathname + u.search;
|
|
143
143
|
} catch {
|
|
@@ -78,7 +78,8 @@ async function processHandles(
|
|
|
78
78
|
|
|
79
79
|
// This nav's instance token, captured before any await — processHandles runs
|
|
80
80
|
// right after its own commit, so this is that commit's token. generateHistoryKey
|
|
81
|
-
// is URL
|
|
81
|
+
// is the URL (plus the source page for an intercept), so an A->B->A revisit
|
|
82
|
+
// reuses the key; the token lets a late
|
|
82
83
|
// resolution tell its own visit apart from a newer same-URL visit, so a stale
|
|
83
84
|
// nav can never clobber a fresher one's live state or cache (P1).
|
|
84
85
|
const myInstance = store.getNavInstance();
|
|
@@ -33,8 +33,8 @@ export function loaderParentId(id: string): string {
|
|
|
33
33
|
* 2. Place each parallel slot id (contains ".@") immediately after its
|
|
34
34
|
* parent layout/route id. Raw segment-resolution emission order does NOT
|
|
35
35
|
* guarantee this: route-mounted parallels are resolved/pushed BEFORE the
|
|
36
|
-
* route handler's segment is appended (see
|
|
37
|
-
*
|
|
36
|
+
* route handler's segment is appended (see resolveSegment in
|
|
37
|
+
* segment-resolution/revalidation.ts), so matched can read
|
|
38
38
|
* `[..., R0.@panel, R0]`. collectHandleData consumes segmentOrder verbatim
|
|
39
39
|
* with later-wins semantics, so without normalization the route handler's
|
|
40
40
|
* Meta would override the slot's more-specific Meta — backwards.
|
|
@@ -614,6 +614,7 @@ export async function initBrowserApp(
|
|
|
614
614
|
|
|
615
615
|
const historyKey = generateHistoryKey(window.location.href, {
|
|
616
616
|
intercept: responseIsIntercept,
|
|
617
|
+
interceptSource: interceptSourceUrl ?? undefined,
|
|
617
618
|
});
|
|
618
619
|
store.setHistoryKey(historyKey);
|
|
619
620
|
const currentHandleData = liveHandleDataForCache(
|
|
@@ -46,6 +46,60 @@ export interface ReconcileResult {
|
|
|
46
46
|
interceptSegments: ResolvedSegment[];
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
/**
|
|
50
|
+
* A segment the server sent over its cached copy, keeping the cached
|
|
51
|
+
* structural properties so the React tree keeps its shape. Changing them
|
|
52
|
+
* between renders alters the element nesting (with/without
|
|
53
|
+
* RouteContentWrapper, MountContextProvider, etc.), which remounts components
|
|
54
|
+
* and destroys useActionState.
|
|
55
|
+
*/
|
|
56
|
+
function mergeCached(
|
|
57
|
+
fromServer: ResolvedSegment,
|
|
58
|
+
fromCache: ResolvedSegment,
|
|
59
|
+
actor: ReconcileActor,
|
|
60
|
+
): ResolvedSegment {
|
|
61
|
+
let merged = fromServer;
|
|
62
|
+
|
|
63
|
+
// When server returns component: null for a layout segment, it means
|
|
64
|
+
// "this segment doesn't need re-rendering" - preserve the cached component
|
|
65
|
+
// to maintain the outlet chain and prevent React tree changes
|
|
66
|
+
if (
|
|
67
|
+
fromServer.component === null &&
|
|
68
|
+
fromServer.type === "layout" &&
|
|
69
|
+
fromCache.component != null
|
|
70
|
+
) {
|
|
71
|
+
merged = { ...merged, component: fromCache.component };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Loading preservation is actor-aware:
|
|
75
|
+
// - action/stale-revalidation: always preserve cached value to prevent
|
|
76
|
+
// tree remount (even when cached is undefined, to avoid adding a
|
|
77
|
+
// Suspense boundary that wasn't there before)
|
|
78
|
+
// - navigation: only when cached is defined (building a new tree)
|
|
79
|
+
if (
|
|
80
|
+
fromServer.loading !== fromCache.loading &&
|
|
81
|
+
(actor !== "navigation" || fromCache.loading !== undefined)
|
|
82
|
+
) {
|
|
83
|
+
merged = { ...merged, loading: fromCache.loading };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// mountPath: SSR segments may lack mountPath while revalidated segments
|
|
87
|
+
// include it. The conditional MountContextProvider wrapper changes tree depth.
|
|
88
|
+
if (fromServer.mountPath !== fromCache.mountPath) {
|
|
89
|
+
merged = { ...merged, mountPath: fromCache.mountPath };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Dev-mode assertion: warn if the merged result still differs from cache
|
|
93
|
+
// in tree-structural properties. This catches bugs where the merge code
|
|
94
|
+
// above fails to preserve a value it should have.
|
|
95
|
+
assertSegmentStructure(
|
|
96
|
+
fromCache,
|
|
97
|
+
merged,
|
|
98
|
+
actor === "action" ? "action-bridge" : "partial-update",
|
|
99
|
+
);
|
|
100
|
+
return merged;
|
|
101
|
+
}
|
|
102
|
+
|
|
49
103
|
/**
|
|
50
104
|
* Single source of truth for merging server segments with cached segments.
|
|
51
105
|
*
|
|
@@ -70,7 +124,6 @@ export interface ReconcileResult {
|
|
|
70
124
|
*/
|
|
71
125
|
export function reconcileSegments(input: ReconcileInput): ReconcileResult {
|
|
72
126
|
const { actor, matched, diff, insertMissingDiff } = input;
|
|
73
|
-
const context = actor === "action" ? "action-bridge" : "partial-update";
|
|
74
127
|
|
|
75
128
|
// Build lookup maps from arrays
|
|
76
129
|
const serverSegments = new Map<string, ResolvedSegment>();
|
|
@@ -111,59 +164,13 @@ export function reconcileSegments(input: ReconcileInput): ReconcileResult {
|
|
|
111
164
|
|
|
112
165
|
if (fromServer) {
|
|
113
166
|
const inDiff = diffSet.has(segId);
|
|
114
|
-
// Preserve cached structural properties to maintain consistent React tree.
|
|
115
|
-
// Changing these between renders alters the element nesting
|
|
116
|
-
// (with/without RouteContentWrapper, MountContextProvider, etc.),
|
|
117
|
-
// causing React to remount components and destroy useActionState.
|
|
118
167
|
if (fromCache) {
|
|
119
|
-
let merged = fromServer;
|
|
120
|
-
|
|
121
|
-
// When server returns component: null for a layout segment, it means
|
|
122
|
-
// "this segment doesn't need re-rendering" - preserve the cached component
|
|
123
|
-
// to maintain the outlet chain and prevent React tree changes
|
|
124
|
-
if (
|
|
125
|
-
fromServer.component === null &&
|
|
126
|
-
fromServer.type === "layout" &&
|
|
127
|
-
fromCache.component != null
|
|
128
|
-
) {
|
|
129
|
-
merged = { ...merged, component: fromCache.component };
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
// Loading preservation is actor-aware:
|
|
133
|
-
// - action/stale-revalidation: always preserve cached value to prevent
|
|
134
|
-
// tree remount (even when cached is undefined, to avoid adding a
|
|
135
|
-
// Suspense boundary that wasn't there before)
|
|
136
|
-
// - navigation: only when cached is defined (building a new tree)
|
|
137
|
-
if (actor !== "navigation") {
|
|
138
|
-
if (fromServer.loading !== fromCache.loading) {
|
|
139
|
-
merged = { ...merged, loading: fromCache.loading };
|
|
140
|
-
}
|
|
141
|
-
} else {
|
|
142
|
-
if (
|
|
143
|
-
fromCache.loading !== undefined &&
|
|
144
|
-
fromServer.loading !== fromCache.loading
|
|
145
|
-
) {
|
|
146
|
-
merged = { ...merged, loading: fromCache.loading };
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
// mountPath: SSR segments may lack mountPath while revalidated segments
|
|
151
|
-
// include it. The conditional MountContextProvider wrapper changes tree depth.
|
|
152
|
-
if (fromServer.mountPath !== fromCache.mountPath) {
|
|
153
|
-
merged = { ...merged, mountPath: fromCache.mountPath };
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
// Dev-mode assertion: warn if the merged result still differs from cache
|
|
157
|
-
// in tree-structural properties. This catches bugs where the merge code
|
|
158
|
-
// above fails to preserve a value it should have.
|
|
159
|
-
assertSegmentStructure(fromCache, merged, context);
|
|
160
|
-
|
|
161
168
|
if (IS_BROWSER_DEBUG) {
|
|
162
169
|
debugLog(
|
|
163
170
|
`[reconcile] ${segId}: SERVER+CACHE merge (${inDiff ? "in diff" : "not in diff"}, type=${fromServer.type}, component=${fromServer.component === null ? "null→cached" : "server"})`,
|
|
164
171
|
);
|
|
165
172
|
}
|
|
166
|
-
return
|
|
173
|
+
return mergeCached(fromServer, fromCache, actor);
|
|
167
174
|
}
|
|
168
175
|
if (IS_BROWSER_DEBUG) {
|
|
169
176
|
debugLog(
|
|
@@ -221,11 +228,13 @@ export function reconcileSegments(input: ReconcileInput): ReconcileResult {
|
|
|
221
228
|
}
|
|
222
229
|
|
|
223
230
|
/**
|
|
224
|
-
* Reconcile error
|
|
231
|
+
* Reconcile an error-boundary response with cached segments.
|
|
225
232
|
*
|
|
226
|
-
*
|
|
227
|
-
*
|
|
228
|
-
* preserving sibling layouts that aren't in
|
|
233
|
+
* The server returns the error boundary segment and, above it, what the
|
|
234
|
+
* action's revalidation re-ran (rsc/server-action.ts). This function overlays
|
|
235
|
+
* them onto the full cached tree, preserving sibling layouts that aren't in
|
|
236
|
+
* the error parent chain. The error segment replaces its cached copy; every
|
|
237
|
+
* other segment merges as an action's does (mergeCached).
|
|
229
238
|
*/
|
|
230
239
|
export function reconcileErrorSegments(
|
|
231
240
|
cachedSegments: ResolvedSegment[],
|
|
@@ -236,7 +245,11 @@ export function reconcileErrorSegments(
|
|
|
236
245
|
|
|
237
246
|
const segments = cachedSegments.map((cached) => {
|
|
238
247
|
const fromServer = errorMap.get(cached.id);
|
|
239
|
-
return fromServer
|
|
248
|
+
return !fromServer
|
|
249
|
+
? cached
|
|
250
|
+
: fromServer.type === "error"
|
|
251
|
+
? fromServer
|
|
252
|
+
: mergeCached(fromServer, cached, "action");
|
|
240
253
|
});
|
|
241
254
|
|
|
242
255
|
const { main, intercept } = splitInterceptSegments(segments);
|