@rangojs/router 0.0.0-experimental.20 → 0.0.0-experimental.20dbba0c
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/AGENTS.md +4 -0
- package/README.md +172 -50
- package/dist/bin/rango.js +138 -50
- package/dist/vite/index.js +1160 -508
- package/dist/vite/index.js.bak +5448 -0
- package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/package.json +17 -16
- package/skills/breadcrumbs/SKILL.md +252 -0
- package/skills/cache-guide/SKILL.md +32 -0
- package/skills/caching/SKILL.md +49 -8
- package/skills/document-cache/SKILL.md +2 -2
- package/skills/handler-use/SKILL.md +362 -0
- package/skills/hooks/SKILL.md +61 -51
- package/skills/host-router/SKILL.md +218 -0
- package/skills/intercept/SKILL.md +20 -0
- package/skills/layout/SKILL.md +22 -0
- package/skills/links/SKILL.md +91 -17
- package/skills/loader/SKILL.md +107 -24
- package/skills/middleware/SKILL.md +34 -3
- package/skills/migrate-nextjs/SKILL.md +560 -0
- package/skills/migrate-react-router/SKILL.md +765 -0
- package/skills/parallel/SKILL.md +185 -0
- package/skills/prerender/SKILL.md +112 -70
- package/skills/rango/SKILL.md +24 -23
- package/skills/response-routes/SKILL.md +8 -0
- package/skills/route/SKILL.md +58 -4
- package/skills/router-setup/SKILL.md +95 -5
- package/skills/streams-and-websockets/SKILL.md +283 -0
- package/skills/typesafety/SKILL.md +38 -24
- package/src/__internal.ts +92 -0
- package/src/browser/app-shell.ts +52 -0
- package/src/browser/app-version.ts +14 -0
- package/src/browser/event-controller.ts +5 -0
- package/src/browser/link-interceptor.ts +4 -0
- package/src/browser/navigation-bridge.ts +175 -17
- package/src/browser/navigation-client.ts +177 -44
- package/src/browser/navigation-store.ts +68 -9
- package/src/browser/navigation-transaction.ts +11 -9
- package/src/browser/partial-update.ts +113 -17
- package/src/browser/prefetch/cache.ts +275 -28
- package/src/browser/prefetch/fetch.ts +191 -46
- package/src/browser/prefetch/policy.ts +6 -0
- package/src/browser/prefetch/queue.ts +123 -20
- package/src/browser/prefetch/resource-ready.ts +77 -0
- package/src/browser/rango-state.ts +53 -13
- package/src/browser/react/Link.tsx +98 -14
- package/src/browser/react/NavigationProvider.tsx +89 -14
- package/src/browser/react/context.ts +7 -2
- package/src/browser/react/use-handle.ts +9 -58
- package/src/browser/react/use-navigation.ts +22 -2
- package/src/browser/react/use-params.ts +11 -1
- package/src/browser/react/use-router.ts +29 -9
- package/src/browser/rsc-router.tsx +177 -66
- package/src/browser/scroll-restoration.ts +41 -42
- package/src/browser/segment-reconciler.ts +36 -9
- package/src/browser/server-action-bridge.ts +8 -6
- package/src/browser/types.ts +73 -5
- package/src/build/generate-manifest.ts +6 -6
- package/src/build/generate-route-types.ts +3 -0
- package/src/build/route-trie.ts +67 -25
- package/src/build/route-types/include-resolution.ts +8 -1
- package/src/build/route-types/router-processing.ts +223 -74
- package/src/build/route-types/scan-filter.ts +8 -1
- package/src/cache/cache-runtime.ts +15 -11
- package/src/cache/cache-scope.ts +48 -7
- package/src/cache/cf/cf-cache-store.ts +455 -15
- package/src/cache/cf/index.ts +5 -1
- package/src/cache/document-cache.ts +17 -7
- package/src/cache/index.ts +1 -0
- package/src/cache/taint.ts +55 -0
- package/src/client.rsc.tsx +2 -1
- package/src/client.tsx +85 -276
- package/src/context-var.ts +72 -2
- package/src/debug.ts +2 -2
- package/src/handle.ts +40 -0
- package/src/handles/breadcrumbs.ts +66 -0
- package/src/handles/index.ts +1 -0
- package/src/host/index.ts +0 -3
- package/src/index.rsc.ts +9 -36
- package/src/index.ts +79 -70
- package/src/outlet-context.ts +1 -1
- package/src/prerender/store.ts +57 -15
- package/src/prerender.ts +138 -77
- package/src/response-utils.ts +28 -0
- package/src/reverse.ts +27 -2
- package/src/route-definition/dsl-helpers.ts +240 -40
- package/src/route-definition/helpers-types.ts +67 -19
- package/src/route-definition/index.ts +3 -3
- package/src/route-definition/redirect.ts +11 -3
- package/src/route-definition/resolve-handler-use.ts +155 -0
- package/src/route-map-builder.ts +7 -1
- package/src/route-types.ts +18 -0
- package/src/router/content-negotiation.ts +100 -1
- package/src/router/find-match.ts +4 -2
- package/src/router/handler-context.ts +129 -26
- package/src/router/intercept-resolution.ts +11 -4
- package/src/router/lazy-includes.ts +10 -7
- package/src/router/loader-resolution.ts +160 -22
- package/src/router/logging.ts +5 -2
- package/src/router/manifest.ts +31 -16
- package/src/router/match-api.ts +128 -193
- package/src/router/match-middleware/background-revalidation.ts +30 -2
- package/src/router/match-middleware/cache-lookup.ts +94 -17
- package/src/router/match-middleware/cache-store.ts +53 -10
- package/src/router/match-middleware/intercept-resolution.ts +9 -7
- package/src/router/match-middleware/segment-resolution.ts +61 -5
- package/src/router/match-result.ts +103 -18
- package/src/router/metrics.ts +238 -13
- package/src/router/middleware-types.ts +48 -27
- package/src/router/middleware.ts +201 -86
- package/src/router/navigation-snapshot.ts +182 -0
- package/src/router/pattern-matching.ts +77 -11
- package/src/router/prerender-match.ts +114 -10
- package/src/router/preview-match.ts +30 -102
- package/src/router/request-classification.ts +310 -0
- package/src/router/revalidation.ts +27 -7
- package/src/router/route-snapshot.ts +245 -0
- package/src/router/router-context.ts +6 -1
- package/src/router/router-interfaces.ts +50 -5
- package/src/router/router-options.ts +50 -19
- package/src/router/segment-resolution/fresh.ts +215 -19
- package/src/router/segment-resolution/helpers.ts +30 -25
- package/src/router/segment-resolution/loader-cache.ts +1 -0
- package/src/router/segment-resolution/revalidation.ts +454 -301
- package/src/router/segment-wrappers.ts +2 -0
- package/src/router/trie-matching.ts +30 -6
- package/src/router/types.ts +1 -0
- package/src/router/url-params.ts +49 -0
- package/src/router.ts +89 -17
- package/src/rsc/handler.ts +563 -364
- package/src/rsc/helpers.ts +69 -41
- package/src/rsc/index.ts +0 -20
- package/src/rsc/loader-fetch.ts +23 -3
- package/src/rsc/manifest-init.ts +5 -1
- package/src/rsc/progressive-enhancement.ts +37 -10
- package/src/rsc/response-route-handler.ts +14 -1
- package/src/rsc/rsc-rendering.ts +47 -44
- package/src/rsc/server-action.ts +24 -10
- package/src/rsc/ssr-setup.ts +128 -0
- package/src/rsc/types.ts +11 -1
- package/src/search-params.ts +16 -13
- package/src/segment-content-promise.ts +67 -0
- package/src/segment-loader-promise.ts +122 -0
- package/src/segment-system.tsx +109 -23
- package/src/server/context.ts +174 -19
- package/src/server/handle-store.ts +19 -0
- package/src/server/loader-registry.ts +9 -8
- package/src/server/request-context.ts +218 -65
- package/src/server.ts +6 -0
- package/src/ssr/index.tsx +4 -0
- package/src/static-handler.ts +18 -6
- package/src/theme/index.ts +4 -13
- package/src/types/cache-types.ts +4 -4
- package/src/types/handler-context.ts +140 -72
- package/src/types/loader-types.ts +41 -15
- package/src/types/request-scope.ts +126 -0
- package/src/types/route-config.ts +17 -8
- package/src/types/route-entry.ts +19 -1
- package/src/types/segments.ts +2 -5
- package/src/urls/include-helper.ts +24 -14
- package/src/urls/path-helper-types.ts +39 -6
- package/src/urls/path-helper.ts +48 -13
- package/src/urls/pattern-types.ts +12 -0
- package/src/urls/response-types.ts +18 -16
- package/src/use-loader.tsx +77 -5
- package/src/vite/discovery/bundle-postprocess.ts +61 -89
- package/src/vite/discovery/discover-routers.ts +7 -4
- package/src/vite/discovery/prerender-collection.ts +162 -88
- package/src/vite/discovery/state.ts +17 -13
- package/src/vite/index.ts +8 -3
- package/src/vite/plugin-types.ts +51 -79
- package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
- package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/src/vite/plugins/cloudflare-protocol-stub.ts +214 -0
- package/src/vite/plugins/expose-action-id.ts +1 -3
- package/src/vite/plugins/expose-id-utils.ts +12 -0
- package/src/vite/plugins/expose-ids/handler-transform.ts +30 -0
- package/src/vite/plugins/expose-internal-ids.ts +257 -40
- package/src/vite/plugins/performance-tracks.ts +88 -0
- package/src/vite/plugins/refresh-cmd.ts +127 -0
- package/src/vite/plugins/version-plugin.ts +13 -1
- package/src/vite/rango.ts +190 -217
- package/src/vite/router-discovery.ts +241 -45
- package/src/vite/utils/banner.ts +4 -4
- package/src/vite/utils/package-resolution.ts +34 -1
- package/src/vite/utils/prerender-utils.ts +97 -5
- package/src/vite/utils/shared-utils.ts +3 -2
- package/skills/testing/SKILL.md +0 -226
- package/src/route-definition/route-function.ts +0 -119
|
@@ -17,6 +17,12 @@ import {
|
|
|
17
17
|
emptyResponse,
|
|
18
18
|
teeWithCompletion,
|
|
19
19
|
} from "./response-adapter.js";
|
|
20
|
+
import {
|
|
21
|
+
buildPrefetchKey,
|
|
22
|
+
buildSourceKey,
|
|
23
|
+
consumeInflightPrefetch,
|
|
24
|
+
consumePrefetch,
|
|
25
|
+
} from "./prefetch/cache.js";
|
|
20
26
|
|
|
21
27
|
/**
|
|
22
28
|
* Create a navigation client for fetching RSC payloads
|
|
@@ -24,21 +30,14 @@ import {
|
|
|
24
30
|
* The client handles building URLs with RSC parameters and
|
|
25
31
|
* deserializing the response using the RSC runtime.
|
|
26
32
|
*
|
|
33
|
+
* Checks the in-memory prefetch cache before making a network request.
|
|
34
|
+
* Tries the source-scoped key first (populated when the server tagged
|
|
35
|
+
* the response as source-sensitive via `X-RSC-Prefetch-Scope: source`)
|
|
36
|
+
* and falls back to the Rango-state-keyed wildcard slot used for the
|
|
37
|
+
* common source-agnostic case.
|
|
38
|
+
*
|
|
27
39
|
* @param deps - RSC browser dependencies (createFromFetch)
|
|
28
40
|
* @returns NavigationClient instance
|
|
29
|
-
*
|
|
30
|
-
* @example
|
|
31
|
-
* ```typescript
|
|
32
|
-
* import { createFromFetch } from "@vitejs/plugin-rsc/browser";
|
|
33
|
-
*
|
|
34
|
-
* const client = createNavigationClient({ createFromFetch });
|
|
35
|
-
*
|
|
36
|
-
* const payload = await client.fetchPartial({
|
|
37
|
-
* targetUrl: "/shop/products",
|
|
38
|
-
* segmentIds: ["root", "shop"],
|
|
39
|
-
* previousUrl: "/",
|
|
40
|
-
* });
|
|
41
|
-
* ```
|
|
42
41
|
*/
|
|
43
42
|
export function createNavigationClient(
|
|
44
43
|
deps: Pick<RscBrowserDependencies, "createFromFetch">,
|
|
@@ -47,8 +46,9 @@ export function createNavigationClient(
|
|
|
47
46
|
/**
|
|
48
47
|
* Fetch a partial RSC payload for navigation
|
|
49
48
|
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
49
|
+
* First checks the in-memory prefetch cache for a matching entry.
|
|
50
|
+
* If found, uses the cached response instantly. Otherwise sends
|
|
51
|
+
* current segment IDs to the server for diff-based rendering.
|
|
52
52
|
*
|
|
53
53
|
* @param options - Fetch options
|
|
54
54
|
* @returns RSC payload with segments and metadata, plus stream completion promise
|
|
@@ -64,6 +64,7 @@ export function createNavigationClient(
|
|
|
64
64
|
staleRevalidation,
|
|
65
65
|
interceptSourceUrl,
|
|
66
66
|
version,
|
|
67
|
+
routerId,
|
|
67
68
|
hmr,
|
|
68
69
|
} = options;
|
|
69
70
|
|
|
@@ -80,7 +81,8 @@ export function createNavigationClient(
|
|
|
80
81
|
});
|
|
81
82
|
}
|
|
82
83
|
|
|
83
|
-
// Build fetch URL with partial rendering params
|
|
84
|
+
// Build fetch URL with partial rendering params (used for both
|
|
85
|
+
// cache key lookup and actual fetch if cache misses)
|
|
84
86
|
const fetchUrl = new URL(targetUrl, window.location.origin);
|
|
85
87
|
fetchUrl.searchParams.set("_rsc_partial", "true");
|
|
86
88
|
fetchUrl.searchParams.set("_rsc_segments", segmentIds.join(","));
|
|
@@ -90,32 +92,62 @@ export function createNavigationClient(
|
|
|
90
92
|
if (version) {
|
|
91
93
|
fetchUrl.searchParams.set("_rsc_v", version);
|
|
92
94
|
}
|
|
93
|
-
if (
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
95
|
+
if (routerId) {
|
|
96
|
+
fetchUrl.searchParams.set("_rsc_rid", routerId);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Check completed in-memory prefetch cache before making a network
|
|
100
|
+
// request. Try the source-scoped key first (populated when the server
|
|
101
|
+
// tagged the prefetch response as source-sensitive, e.g. intercepts,
|
|
102
|
+
// or when a Link opted in with `prefetchKey=":source"`), then fall
|
|
103
|
+
// back to the wildcard slot shared across source pages.
|
|
104
|
+
// Both keys embed the Rango state, so state rotation (deploy or
|
|
105
|
+
// server-action invalidation) auto-invalidates both scopes.
|
|
106
|
+
// Skip cache for stale revalidation (needs fresh data), HMR (needs
|
|
107
|
+
// fresh modules), and intercept contexts (source-dependent responses).
|
|
108
|
+
const canUsePrefetch = !staleRevalidation && !hmr && !interceptSourceUrl;
|
|
109
|
+
const rangoState = getRangoState();
|
|
110
|
+
const wildcardKey = buildPrefetchKey(rangoState, fetchUrl);
|
|
111
|
+
const cacheKey = buildSourceKey(rangoState, previousUrl, fetchUrl);
|
|
112
|
+
|
|
113
|
+
let cachedResponse: Response | null = null;
|
|
114
|
+
let hitKey: string | null = null;
|
|
115
|
+
if (canUsePrefetch) {
|
|
116
|
+
cachedResponse = consumePrefetch(cacheKey);
|
|
117
|
+
if (cachedResponse) {
|
|
118
|
+
hitKey = cacheKey;
|
|
119
|
+
} else {
|
|
120
|
+
cachedResponse = consumePrefetch(wildcardKey);
|
|
121
|
+
if (cachedResponse) hitKey = wildcardKey;
|
|
122
|
+
}
|
|
97
123
|
}
|
|
98
124
|
|
|
125
|
+
let inflightResponsePromise: Promise<Response | null> | null = null;
|
|
126
|
+
if (canUsePrefetch && !cachedResponse) {
|
|
127
|
+
inflightResponsePromise = consumeInflightPrefetch(cacheKey);
|
|
128
|
+
if (inflightResponsePromise) {
|
|
129
|
+
hitKey = cacheKey;
|
|
130
|
+
} else {
|
|
131
|
+
inflightResponsePromise = consumeInflightPrefetch(wildcardKey);
|
|
132
|
+
if (inflightResponsePromise) hitKey = wildcardKey;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
99
135
|
// Track when the stream completes
|
|
100
136
|
let resolveStreamComplete: () => void;
|
|
101
137
|
const streamComplete = new Promise<void>((resolve) => {
|
|
102
138
|
resolveStreamComplete = resolve;
|
|
103
139
|
});
|
|
104
140
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
},
|
|
116
|
-
signal,
|
|
117
|
-
}).then((response) => {
|
|
118
|
-
// Check for version mismatch - server wants us to reload
|
|
141
|
+
/**
|
|
142
|
+
* Validate RSC control headers on any response (fresh, cached, or
|
|
143
|
+
* in-flight). Handles version-mismatch reloads and server redirects.
|
|
144
|
+
* Returns the response unchanged when no control header is present.
|
|
145
|
+
*/
|
|
146
|
+
const validateRscHeaders = (
|
|
147
|
+
response: Response,
|
|
148
|
+
source: string,
|
|
149
|
+
): Response | Promise<Response> => {
|
|
150
|
+
// Version mismatch — server wants a full page reload
|
|
119
151
|
const reload = extractRscHeaderUrl(response, "X-RSC-Reload");
|
|
120
152
|
if (reload === "blocked") {
|
|
121
153
|
resolveStreamComplete();
|
|
@@ -123,11 +155,12 @@ export function createNavigationClient(
|
|
|
123
155
|
}
|
|
124
156
|
if (reload) {
|
|
125
157
|
if (tx) {
|
|
126
|
-
browserDebugLog(tx,
|
|
158
|
+
browserDebugLog(tx, `version mismatch, reloading (${source})`, {
|
|
127
159
|
reloadUrl: reload.url,
|
|
128
160
|
});
|
|
129
161
|
}
|
|
130
162
|
window.location.href = reload.url;
|
|
163
|
+
// Block further processing — page is reloading
|
|
131
164
|
return new Promise<Response>(() => {});
|
|
132
165
|
}
|
|
133
166
|
|
|
@@ -142,7 +175,7 @@ export function createNavigationClient(
|
|
|
142
175
|
}
|
|
143
176
|
if (redirect) {
|
|
144
177
|
if (tx) {
|
|
145
|
-
browserDebugLog(tx,
|
|
178
|
+
browserDebugLog(tx, `server redirect (${source})`, {
|
|
146
179
|
redirectUrl: redirect.url,
|
|
147
180
|
});
|
|
148
181
|
}
|
|
@@ -150,19 +183,119 @@ export function createNavigationClient(
|
|
|
150
183
|
throw new ServerRedirect(redirect.url, undefined);
|
|
151
184
|
}
|
|
152
185
|
|
|
153
|
-
return
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
186
|
+
return response;
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
/** Start a fresh navigation fetch (no cache / inflight hit). */
|
|
190
|
+
const doFreshFetch = (): Promise<Response> => {
|
|
191
|
+
if (tx) {
|
|
192
|
+
browserDebugLog(tx, "fetching", {
|
|
193
|
+
path: `${fetchUrl.pathname}${fetchUrl.search}`,
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return fetch(fetchUrl, {
|
|
198
|
+
headers: {
|
|
199
|
+
"X-RSC-Router-Client-Path": previousUrl,
|
|
200
|
+
"X-Rango-State": getRangoState(),
|
|
201
|
+
...(tx && { "X-RSC-Router-Request-Id": tx.requestId }),
|
|
202
|
+
...(interceptSourceUrl && {
|
|
203
|
+
"X-RSC-Router-Intercept-Source": interceptSourceUrl,
|
|
204
|
+
}),
|
|
205
|
+
...(hmr && { "X-RSC-HMR": "1" }),
|
|
158
206
|
},
|
|
159
207
|
signal,
|
|
160
|
-
)
|
|
161
|
-
|
|
208
|
+
}).then((response) => {
|
|
209
|
+
const validated = validateRscHeaders(response, "fetch");
|
|
210
|
+
if (validated instanceof Promise) return validated;
|
|
211
|
+
|
|
212
|
+
return teeWithCompletion(
|
|
213
|
+
validated,
|
|
214
|
+
() => {
|
|
215
|
+
if (tx) browserDebugLog(tx, "stream complete");
|
|
216
|
+
resolveStreamComplete();
|
|
217
|
+
},
|
|
218
|
+
signal,
|
|
219
|
+
);
|
|
220
|
+
});
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
let responsePromise: Promise<Response>;
|
|
224
|
+
|
|
225
|
+
if (cachedResponse) {
|
|
226
|
+
if (tx) {
|
|
227
|
+
browserDebugLog(tx, "prefetch cache hit", {
|
|
228
|
+
key: hitKey,
|
|
229
|
+
wildcard: hitKey === wildcardKey,
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
responsePromise = Promise.resolve(cachedResponse).then((response) => {
|
|
233
|
+
const validated = validateRscHeaders(response, "prefetch cache");
|
|
234
|
+
if (validated instanceof Promise) return validated;
|
|
235
|
+
|
|
236
|
+
return teeWithCompletion(
|
|
237
|
+
validated,
|
|
238
|
+
() => {
|
|
239
|
+
if (tx) browserDebugLog(tx, "stream complete (from cache)");
|
|
240
|
+
resolveStreamComplete();
|
|
241
|
+
},
|
|
242
|
+
signal,
|
|
243
|
+
);
|
|
244
|
+
});
|
|
245
|
+
} else if (inflightResponsePromise) {
|
|
246
|
+
if (tx) {
|
|
247
|
+
browserDebugLog(tx, "reusing inflight prefetch", {
|
|
248
|
+
key: hitKey,
|
|
249
|
+
wildcard: hitKey === wildcardKey,
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
const adoptedViaWildcard = hitKey === wildcardKey;
|
|
253
|
+
responsePromise = inflightResponsePromise.then(async (response) => {
|
|
254
|
+
if (!response) {
|
|
255
|
+
if (tx) {
|
|
256
|
+
browserDebugLog(tx, "inflight prefetch unavailable, refetching");
|
|
257
|
+
}
|
|
258
|
+
return doFreshFetch();
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Cross-source safety: an inflight promise adopted via the
|
|
262
|
+
// wildcard key may turn out to be source-scoped (server emitted
|
|
263
|
+
// `X-RSC-Prefetch-Scope: source`), which means it was built for
|
|
264
|
+
// a different source page. Discard and refetch.
|
|
265
|
+
if (
|
|
266
|
+
adoptedViaWildcard &&
|
|
267
|
+
response.headers.get("x-rsc-prefetch-scope") === "source"
|
|
268
|
+
) {
|
|
269
|
+
if (tx) {
|
|
270
|
+
browserDebugLog(
|
|
271
|
+
tx,
|
|
272
|
+
"wildcard inflight turned out source-scoped, refetching",
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
return doFreshFetch();
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const validated = validateRscHeaders(response, "inflight prefetch");
|
|
279
|
+
if (validated instanceof Promise) return validated;
|
|
280
|
+
|
|
281
|
+
return teeWithCompletion(
|
|
282
|
+
validated,
|
|
283
|
+
() => {
|
|
284
|
+
if (tx) {
|
|
285
|
+
browserDebugLog(tx, "stream complete (from inflight prefetch)");
|
|
286
|
+
}
|
|
287
|
+
resolveStreamComplete();
|
|
288
|
+
},
|
|
289
|
+
signal,
|
|
290
|
+
);
|
|
291
|
+
});
|
|
292
|
+
} else {
|
|
293
|
+
responsePromise = doFreshFetch();
|
|
294
|
+
}
|
|
162
295
|
|
|
163
296
|
try {
|
|
164
|
-
// Deserialize RSC payload
|
|
165
297
|
const payload = await deps.createFromFetch<RscPayload>(responsePromise);
|
|
298
|
+
|
|
166
299
|
if (tx) {
|
|
167
300
|
browserDebugLog(tx, "response received", {
|
|
168
301
|
isPartial: payload.metadata?.isPartial,
|
|
@@ -12,7 +12,10 @@ import type {
|
|
|
12
12
|
ActionStateListener,
|
|
13
13
|
HandleData,
|
|
14
14
|
} from "./types.js";
|
|
15
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
clearPrefetchCache,
|
|
17
|
+
clearPrefetchCacheLocal,
|
|
18
|
+
} from "./prefetch/cache.js";
|
|
16
19
|
|
|
17
20
|
/**
|
|
18
21
|
* Default action state (idle with no payload)
|
|
@@ -28,9 +31,15 @@ const DEFAULT_ACTION_STATE: TrackedActionState = {
|
|
|
28
31
|
// Maximum number of history entries to cache (URLs visited)
|
|
29
32
|
const HISTORY_CACHE_SIZE = 20;
|
|
30
33
|
|
|
31
|
-
// Cache entry: [url-key, segments, stale, handleData?]
|
|
34
|
+
// Cache entry: [url-key, segments, stale, handleData?, routerId?]
|
|
32
35
|
// stale=true means the data may be outdated and should be revalidated on access
|
|
33
|
-
type HistoryCacheEntry = [
|
|
36
|
+
type HistoryCacheEntry = [
|
|
37
|
+
string,
|
|
38
|
+
ResolvedSegment[],
|
|
39
|
+
boolean,
|
|
40
|
+
HandleData?,
|
|
41
|
+
string?,
|
|
42
|
+
];
|
|
34
43
|
|
|
35
44
|
/**
|
|
36
45
|
* Shallow clone handleData to avoid reference sharing between cache entries.
|
|
@@ -258,6 +267,11 @@ export function createNavigationStore(
|
|
|
258
267
|
// Used to maintain intercept context during action revalidation
|
|
259
268
|
let interceptSourceUrl: string | null = null;
|
|
260
269
|
|
|
270
|
+
// Router identity - tracks which router is currently active.
|
|
271
|
+
// When this changes on a partial response, the client forces a full
|
|
272
|
+
// tree replacement instead of reconciling with stale segments.
|
|
273
|
+
let currentRouterId: string | undefined;
|
|
274
|
+
|
|
261
275
|
// Action state tracking (for useAction hook)
|
|
262
276
|
// Maps action function ID to its tracked state
|
|
263
277
|
const actionStates = new Map<string, TrackedActionState>();
|
|
@@ -324,6 +338,18 @@ export function createNavigationStore(
|
|
|
324
338
|
clearPrefetchCache();
|
|
325
339
|
}
|
|
326
340
|
|
|
341
|
+
/**
|
|
342
|
+
* Drop this tab's navigation + prefetch caches without broadcasting or
|
|
343
|
+
* rotating shared state. Used when the local session changes in a way that
|
|
344
|
+
* doesn't affect other tabs — e.g. this tab crosses into a different app
|
|
345
|
+
* via a cross-router navigation. Other tabs in the old app keep their
|
|
346
|
+
* caches and their X-Rango-State token.
|
|
347
|
+
*/
|
|
348
|
+
function clearCacheInternalLocal(): void {
|
|
349
|
+
historyCache.length = 0;
|
|
350
|
+
clearPrefetchCacheLocal();
|
|
351
|
+
}
|
|
352
|
+
|
|
327
353
|
/**
|
|
328
354
|
* Mark all cache entries as stale (internal - does not broadcast)
|
|
329
355
|
*/
|
|
@@ -571,10 +597,17 @@ export function createNavigationStore(
|
|
|
571
597
|
segments,
|
|
572
598
|
false,
|
|
573
599
|
clonedHandleData,
|
|
600
|
+
currentRouterId,
|
|
574
601
|
];
|
|
575
602
|
} else {
|
|
576
603
|
// Add new entry at the end (not stale)
|
|
577
|
-
historyCache.push([
|
|
604
|
+
historyCache.push([
|
|
605
|
+
historyKey,
|
|
606
|
+
segments,
|
|
607
|
+
false,
|
|
608
|
+
clonedHandleData,
|
|
609
|
+
currentRouterId,
|
|
610
|
+
]);
|
|
578
611
|
// Remove oldest entries if over limit
|
|
579
612
|
while (historyCache.length > cacheSize) {
|
|
580
613
|
historyCache.shift();
|
|
@@ -586,14 +619,22 @@ export function createNavigationStore(
|
|
|
586
619
|
* Get cached segments for a history entry
|
|
587
620
|
* Returns { segments, stale, handleData } or undefined if not cached
|
|
588
621
|
*/
|
|
589
|
-
getCachedSegments(
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
622
|
+
getCachedSegments(historyKey: string):
|
|
623
|
+
| {
|
|
624
|
+
segments: ResolvedSegment[];
|
|
625
|
+
stale: boolean;
|
|
626
|
+
handleData?: HandleData;
|
|
627
|
+
routerId?: string;
|
|
628
|
+
}
|
|
593
629
|
| undefined {
|
|
594
630
|
const entry = historyCache.find(([key]) => key === historyKey);
|
|
595
631
|
if (!entry) return undefined;
|
|
596
|
-
return {
|
|
632
|
+
return {
|
|
633
|
+
segments: entry[1],
|
|
634
|
+
stale: entry[2],
|
|
635
|
+
handleData: entry[3],
|
|
636
|
+
routerId: entry[4],
|
|
637
|
+
};
|
|
597
638
|
},
|
|
598
639
|
|
|
599
640
|
/**
|
|
@@ -621,6 +662,7 @@ export function createNavigationStore(
|
|
|
621
662
|
entry[1],
|
|
622
663
|
entry[2],
|
|
623
664
|
clonedHandleData,
|
|
665
|
+
entry[4], // preserve routerId
|
|
624
666
|
];
|
|
625
667
|
}
|
|
626
668
|
},
|
|
@@ -641,6 +683,15 @@ export function createNavigationStore(
|
|
|
641
683
|
clearCacheAndBroadcast();
|
|
642
684
|
},
|
|
643
685
|
|
|
686
|
+
/**
|
|
687
|
+
* Drop this tab's navigation + prefetch caches locally without
|
|
688
|
+
* broadcasting or rotating shared state. Intended for cross-app
|
|
689
|
+
* transitions where the session state diverges for this tab only.
|
|
690
|
+
*/
|
|
691
|
+
clearHistoryCacheLocal(): void {
|
|
692
|
+
clearCacheInternalLocal();
|
|
693
|
+
},
|
|
694
|
+
|
|
644
695
|
/**
|
|
645
696
|
* Mark cache as stale and broadcast to other tabs
|
|
646
697
|
* Called after server actions - allows SWR pattern for popstate
|
|
@@ -687,6 +738,14 @@ export function createNavigationStore(
|
|
|
687
738
|
interceptSourceUrl = url;
|
|
688
739
|
},
|
|
689
740
|
|
|
741
|
+
getRouterId(): string | undefined {
|
|
742
|
+
return currentRouterId;
|
|
743
|
+
},
|
|
744
|
+
|
|
745
|
+
setRouterId(id: string): void {
|
|
746
|
+
currentRouterId = id;
|
|
747
|
+
},
|
|
748
|
+
|
|
690
749
|
// ========================================================================
|
|
691
750
|
// UI Update Notifications
|
|
692
751
|
// ========================================================================
|
|
@@ -7,7 +7,6 @@ import type {
|
|
|
7
7
|
import { generateHistoryKey } from "./navigation-store.js";
|
|
8
8
|
import {
|
|
9
9
|
handleNavigationStart,
|
|
10
|
-
handleNavigationEnd,
|
|
11
10
|
ensureHistoryKey,
|
|
12
11
|
} from "./scroll-restoration.js";
|
|
13
12
|
import type { EventController, NavigationHandle } from "./event-controller.js";
|
|
@@ -81,11 +80,12 @@ export interface BoundTransaction {
|
|
|
81
80
|
readonly currentUrl: string;
|
|
82
81
|
/** Start streaming and get a token to end it when the stream completes */
|
|
83
82
|
startStreaming(): StreamingToken;
|
|
83
|
+
/** Commit the navigation. Returns the effective scroll option for the caller to handle. */
|
|
84
84
|
commit(
|
|
85
85
|
segmentIds: string[],
|
|
86
86
|
segments: ResolvedSegment[],
|
|
87
87
|
overrides?: BoundCommitOverrides,
|
|
88
|
-
):
|
|
88
|
+
): { scroll?: boolean };
|
|
89
89
|
}
|
|
90
90
|
|
|
91
91
|
/**
|
|
@@ -93,7 +93,7 @@ export interface BoundTransaction {
|
|
|
93
93
|
* Uses the event controller handle for lifecycle management
|
|
94
94
|
*/
|
|
95
95
|
interface NavigationTransaction extends Disposable {
|
|
96
|
-
commit(options: CommitOptions):
|
|
96
|
+
commit(options: CommitOptions): { scroll?: boolean };
|
|
97
97
|
with(
|
|
98
98
|
options: Omit<CommitOptions, "segmentIds" | "segments">,
|
|
99
99
|
): BoundTransaction;
|
|
@@ -120,7 +120,7 @@ export function createNavigationTransaction(
|
|
|
120
120
|
/**
|
|
121
121
|
* Commit the navigation - updates store and URL atomically
|
|
122
122
|
*/
|
|
123
|
-
function commit(opts: CommitOptions):
|
|
123
|
+
function commit(opts: CommitOptions): { scroll?: boolean } {
|
|
124
124
|
committed = true;
|
|
125
125
|
|
|
126
126
|
const {
|
|
@@ -150,7 +150,7 @@ export function createNavigationTransaction(
|
|
|
150
150
|
// Without this, the entry lingers and weakens state-machine invariants.
|
|
151
151
|
handle.complete(parsedUrl);
|
|
152
152
|
debugLog("[Browser] Cache-only commit, historyKey:", historyKey);
|
|
153
|
-
return;
|
|
153
|
+
return { scroll: false };
|
|
154
154
|
}
|
|
155
155
|
|
|
156
156
|
// Save current scroll position before navigating
|
|
@@ -172,7 +172,7 @@ export function createNavigationTransaction(
|
|
|
172
172
|
debugLog("[Browser] Store updated (action)");
|
|
173
173
|
// Complete navigation to clear loading state
|
|
174
174
|
handle.complete(parsedUrl);
|
|
175
|
-
return;
|
|
175
|
+
return { scroll: false };
|
|
176
176
|
}
|
|
177
177
|
|
|
178
178
|
// Build history state - include user state, intercept info, and server-set state
|
|
@@ -205,14 +205,16 @@ export function createNavigationTransaction(
|
|
|
205
205
|
// Complete the navigation in event controller (sets idle state, updates location)
|
|
206
206
|
handle.complete(parsedUrl);
|
|
207
207
|
|
|
208
|
-
//
|
|
209
|
-
|
|
208
|
+
// NOTE: Scroll is NOT handled here. The caller (partial-update.ts) handles
|
|
209
|
+
// scroll AFTER onUpdate() so React has the new content before we scroll.
|
|
210
210
|
|
|
211
211
|
debugLog(
|
|
212
212
|
"[Browser] Navigation committed, historyKey:",
|
|
213
213
|
historyKey,
|
|
214
214
|
intercept ? "(intercept)" : "",
|
|
215
215
|
);
|
|
216
|
+
|
|
217
|
+
return { scroll };
|
|
216
218
|
}
|
|
217
219
|
|
|
218
220
|
return {
|
|
@@ -263,7 +265,7 @@ export function createNavigationTransaction(
|
|
|
263
265
|
overrides?.state !== undefined ? overrides.state : opts.state;
|
|
264
266
|
// Server-set location state: only from overrides (set by partial-update)
|
|
265
267
|
const serverState = overrides?.serverState;
|
|
266
|
-
commit({
|
|
268
|
+
return commit({
|
|
267
269
|
...opts,
|
|
268
270
|
segmentIds,
|
|
269
271
|
segments,
|