@rangojs/router 0.0.0-experimental.b9cb8739 → 0.0.0-experimental.bd6e11bc
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 +196 -43
- package/dist/bin/rango.js +277 -99
- package/dist/testing/vitest.js +48 -0
- package/dist/vite/index.js +2779 -1064
- package/dist/vite/index.js.bak +5448 -0
- package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/package.json +57 -11
- package/skills/breadcrumbs/SKILL.md +3 -1
- package/skills/bundle-analysis/SKILL.md +159 -0
- package/skills/cache-guide/SKILL.md +243 -21
- package/skills/caching/SKILL.md +155 -6
- package/skills/composability/SKILL.md +27 -2
- package/skills/document-cache/SKILL.md +78 -55
- package/skills/handler-use/SKILL.md +364 -0
- package/skills/hooks/SKILL.md +229 -20
- package/skills/host-router/SKILL.md +45 -20
- package/skills/i18n/SKILL.md +276 -0
- package/skills/intercept/SKILL.md +46 -4
- package/skills/layout/SKILL.md +28 -7
- package/skills/links/SKILL.md +249 -17
- package/skills/loader/SKILL.md +273 -53
- package/skills/middleware/SKILL.md +49 -12
- package/skills/migrate-nextjs/SKILL.md +562 -0
- package/skills/migrate-react-router/SKILL.md +769 -0
- package/skills/mime-routes/SKILL.md +27 -0
- package/skills/observability/SKILL.md +137 -0
- package/skills/parallel/SKILL.md +197 -6
- package/skills/prerender/SKILL.md +123 -100
- package/skills/rango/SKILL.md +242 -22
- package/skills/react-compiler/SKILL.md +168 -0
- package/skills/response-routes/SKILL.md +66 -9
- package/skills/route/SKILL.md +88 -4
- package/skills/router-setup/SKILL.md +90 -5
- package/skills/server-actions/SKILL.md +751 -0
- package/skills/streams-and-websockets/SKILL.md +283 -0
- package/skills/testing/SKILL.md +716 -0
- package/skills/typesafety/SKILL.md +329 -27
- package/skills/use-cache/SKILL.md +34 -5
- package/skills/view-transitions/SKILL.md +294 -0
- package/src/__augment-tests__/augment.ts +81 -0
- package/src/__augment-tests__/augmented.check.ts +117 -0
- package/src/__internal.ts +1 -1
- package/src/browser/action-coordinator.ts +53 -36
- package/src/browser/app-shell.ts +52 -0
- package/src/browser/app-version.ts +14 -0
- package/src/browser/event-controller.ts +91 -70
- package/src/browser/history-state.ts +21 -0
- package/src/browser/index.ts +3 -3
- package/src/browser/navigation-bridge.ts +102 -16
- package/src/browser/navigation-client.ts +164 -59
- package/src/browser/navigation-store.ts +75 -17
- package/src/browser/navigation-transaction.ts +21 -37
- package/src/browser/partial-update.ts +139 -38
- package/src/browser/prefetch/cache.ts +175 -15
- package/src/browser/prefetch/fetch.ts +180 -33
- 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 +81 -9
- package/src/browser/react/NavigationProvider.tsx +110 -33
- package/src/browser/react/context.ts +7 -2
- package/src/browser/react/filter-segment-order.ts +51 -7
- package/src/browser/react/index.ts +3 -0
- package/src/browser/react/location-state-shared.ts +175 -4
- package/src/browser/react/location-state.ts +39 -13
- package/src/browser/react/use-handle.ts +23 -64
- package/src/browser/react/use-navigation.ts +22 -2
- package/src/browser/react/use-params.ts +20 -8
- package/src/browser/react/use-reverse.ts +106 -0
- package/src/browser/react/use-router.ts +43 -10
- package/src/browser/react/use-segments.ts +11 -8
- package/src/browser/response-adapter.ts +25 -0
- package/src/browser/rsc-router.tsx +191 -74
- package/src/browser/scroll-restoration.ts +41 -14
- package/src/browser/segment-reconciler.ts +36 -9
- package/src/browser/segment-structure-assert.ts +2 -2
- package/src/browser/server-action-bridge.ts +31 -36
- package/src/browser/types.ts +57 -5
- package/src/build/collect-fallback-refs.ts +107 -0
- package/src/build/generate-manifest.ts +65 -40
- package/src/build/generate-route-types.ts +5 -0
- package/src/build/index.ts +2 -0
- package/src/build/route-trie.ts +52 -25
- package/src/build/route-types/codegen.ts +4 -4
- package/src/build/route-types/include-resolution.ts +9 -2
- package/src/build/route-types/per-module-writer.ts +7 -4
- package/src/build/route-types/router-processing.ts +278 -88
- package/src/build/route-types/scan-filter.ts +9 -2
- package/src/build/route-types/source-scan.ts +118 -0
- package/src/build/runtime-discovery.ts +9 -20
- package/src/cache/cache-runtime.ts +15 -11
- package/src/cache/cache-scope.ts +76 -49
- package/src/cache/cf/cf-cache-store.ts +501 -18
- 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 +3 -0
- package/src/client.tsx +94 -238
- package/src/context-var.ts +72 -2
- package/src/debug.ts +2 -2
- package/src/decode-loader-results.ts +36 -0
- package/src/errors.ts +30 -1
- package/src/handle.ts +65 -12
- package/src/host/index.ts +2 -2
- package/src/host/router.ts +129 -57
- package/src/host/types.ts +31 -2
- package/src/host/utils.ts +1 -1
- package/src/href-client.ts +140 -20
- package/src/index.rsc.ts +12 -5
- package/src/index.ts +61 -11
- package/src/loader-store.ts +500 -0
- package/src/loader.rsc.ts +2 -5
- package/src/loader.ts +3 -10
- package/src/missing-id-error.ts +68 -0
- package/src/outlet-context.ts +1 -1
- package/src/prerender/store.ts +5 -4
- package/src/prerender.ts +141 -80
- package/src/response-utils.ts +37 -0
- package/src/reverse.ts +65 -15
- package/src/route-content-wrapper.tsx +6 -28
- package/src/route-definition/dsl-helpers.ts +435 -260
- package/src/route-definition/helper-factories.ts +29 -139
- package/src/route-definition/helpers-types.ts +110 -34
- package/src/route-definition/index.ts +3 -0
- package/src/route-definition/redirect.ts +11 -3
- package/src/route-definition/resolve-handler-use.ts +155 -0
- package/src/route-definition/use-item-types.ts +32 -0
- package/src/route-map-builder.ts +7 -1
- package/src/route-types.ts +37 -41
- package/src/router/basename.ts +14 -0
- package/src/router/content-negotiation.ts +113 -1
- package/src/router/error-handling.ts +1 -1
- package/src/router/find-match.ts +4 -2
- package/src/router/handler-context.ts +77 -38
- package/src/router/intercept-resolution.ts +15 -22
- package/src/router/lazy-includes.ts +12 -9
- package/src/router/loader-resolution.ts +174 -22
- package/src/router/logging.ts +5 -2
- package/src/router/manifest.ts +31 -16
- package/src/router/match-api.ts +128 -192
- package/src/router/match-handlers.ts +63 -20
- package/src/router/match-middleware/background-revalidation.ts +30 -2
- package/src/router/match-middleware/cache-lookup.ts +136 -106
- package/src/router/match-middleware/cache-store.ts +54 -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 +125 -10
- package/src/router/metrics.ts +7 -2
- package/src/router/middleware-types.ts +21 -34
- package/src/router/middleware.ts +103 -90
- package/src/router/navigation-snapshot.ts +182 -0
- package/src/router/pattern-matching.ts +101 -17
- package/src/router/prerender-match.ts +110 -10
- package/src/router/preview-match.ts +32 -102
- package/src/router/request-classification.ts +286 -0
- package/src/router/revalidation.ts +58 -2
- package/src/router/route-snapshot.ts +245 -0
- package/src/router/router-context.ts +6 -1
- package/src/router/router-interfaces.ts +77 -28
- package/src/router/router-options.ts +76 -11
- package/src/router/router-registry.ts +2 -5
- package/src/router/segment-resolution/fresh.ts +223 -24
- package/src/router/segment-resolution/helpers.ts +29 -24
- package/src/router/segment-resolution/loader-cache.ts +1 -0
- package/src/router/segment-resolution/revalidation.ts +466 -285
- package/src/router/segment-resolution/view-transition-default.ts +36 -0
- package/src/router/segment-wrappers.ts +2 -0
- package/src/router/substitute-pattern-params.ts +56 -0
- package/src/router/telemetry.ts +99 -0
- package/src/router/trie-matching.ts +18 -13
- package/src/router/types.ts +9 -0
- package/src/router/url-params.ts +49 -0
- package/src/router.ts +91 -23
- package/src/rsc/handler-context.ts +2 -2
- package/src/rsc/handler.ts +440 -381
- package/src/rsc/helpers.ts +91 -43
- package/src/rsc/index.ts +1 -1
- package/src/rsc/loader-fetch.ts +23 -3
- package/src/rsc/manifest-init.ts +5 -1
- package/src/rsc/origin-guard.ts +28 -10
- package/src/rsc/progressive-enhancement.ts +18 -2
- package/src/rsc/response-route-handler.ts +46 -53
- package/src/rsc/rsc-rendering.ts +41 -48
- package/src/rsc/runtime-warnings.ts +9 -10
- package/src/rsc/server-action.ts +25 -37
- package/src/rsc/ssr-setup.ts +18 -2
- package/src/rsc/types.ts +17 -3
- package/src/search-params.ts +4 -4
- package/src/segment-content-promise.ts +67 -0
- package/src/segment-loader-promise.ts +122 -0
- package/src/segment-system.tsx +219 -67
- package/src/serialize.ts +243 -0
- package/src/server/context.ts +277 -61
- package/src/server/cookie-store.ts +28 -4
- package/src/server/handle-store.ts +19 -0
- package/src/server/loader-registry.ts +9 -8
- package/src/server/request-context.ts +204 -60
- package/src/ssr/index.tsx +9 -1
- package/src/static-handler.ts +19 -7
- package/src/testing/cache-status.ts +166 -0
- package/src/testing/collect-handle.ts +63 -0
- package/src/testing/dispatch.ts +440 -0
- package/src/testing/dom.entry.ts +22 -0
- package/src/testing/e2e/fixture.ts +154 -0
- package/src/testing/e2e/index.ts +149 -0
- package/src/testing/e2e/matchers.ts +51 -0
- package/src/testing/e2e/page-helpers.ts +272 -0
- package/src/testing/e2e/parity.ts +306 -0
- package/src/testing/e2e/server.ts +183 -0
- package/src/testing/flight-matchers.ts +104 -0
- package/src/testing/flight-runtime.d.ts +21 -0
- package/src/testing/flight.entry.ts +22 -0
- package/src/testing/flight.ts +182 -0
- package/src/testing/generated-routes.ts +223 -0
- package/src/testing/index.ts +106 -0
- package/src/testing/internal/context.ts +255 -0
- package/src/testing/render-route.tsx +565 -0
- package/src/testing/run-loader.ts +296 -0
- package/src/testing/run-middleware.ts +179 -0
- package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
- package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
- package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
- package/src/testing/vitest-stubs/version.ts +5 -0
- package/src/testing/vitest.ts +183 -0
- package/src/types/cache-types.ts +4 -4
- package/src/types/global-namespace.ts +39 -26
- package/src/types/handler-context.ts +194 -72
- package/src/types/index.ts +1 -0
- package/src/types/loader-types.ts +41 -15
- package/src/types/request-scope.ts +126 -0
- package/src/types/route-entry.ts +19 -1
- package/src/types/segments.ts +37 -1
- package/src/urls/include-helper.ts +34 -67
- package/src/urls/index.ts +0 -3
- package/src/urls/path-helper-types.ts +50 -9
- package/src/urls/path-helper.ts +63 -63
- package/src/urls/pattern-types.ts +48 -19
- package/src/urls/response-types.ts +25 -22
- package/src/urls/type-extraction.ts +26 -116
- package/src/urls/urls-function.ts +1 -5
- package/src/use-loader.tsx +487 -44
- package/src/vite/debug.ts +185 -0
- package/src/vite/discovery/bundle-postprocess.ts +34 -37
- package/src/vite/discovery/discover-routers.ts +105 -51
- package/src/vite/discovery/discovery-errors.ts +194 -0
- package/src/vite/discovery/gate-state.ts +171 -0
- package/src/vite/discovery/prerender-collection.ts +188 -93
- package/src/vite/discovery/route-types-writer.ts +40 -84
- package/src/vite/discovery/self-gen-tracking.ts +27 -1
- package/src/vite/discovery/state.ts +46 -6
- package/src/vite/discovery/virtual-module-codegen.ts +13 -23
- package/src/vite/index.ts +6 -0
- package/src/vite/plugin-types.ts +111 -72
- package/src/vite/plugins/cjs-to-esm.ts +8 -7
- package/src/vite/plugins/client-ref-dedup.ts +16 -0
- package/src/vite/plugins/client-ref-hashing.ts +28 -5
- 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 +55 -33
- package/src/vite/plugins/expose-id-utils.ts +24 -8
- package/src/vite/plugins/expose-ids/export-analysis.ts +100 -20
- package/src/vite/plugins/expose-ids/handler-transform.ts +12 -35
- package/src/vite/plugins/expose-ids/loader-transform.ts +3 -5
- package/src/vite/plugins/expose-ids/router-transform.ts +20 -3
- package/src/vite/plugins/expose-internal-ids.ts +544 -317
- package/src/vite/plugins/performance-tracks.ts +92 -0
- package/src/vite/plugins/refresh-cmd.ts +88 -26
- package/src/vite/plugins/use-cache-transform.ts +65 -50
- package/src/vite/plugins/version-injector.ts +39 -23
- package/src/vite/plugins/version-plugin.ts +72 -3
- package/src/vite/plugins/virtual-entries.ts +2 -2
- package/src/vite/rango.ts +265 -226
- package/src/vite/router-discovery.ts +920 -137
- package/src/vite/utils/ast-handler-extract.ts +15 -15
- package/src/vite/utils/banner.ts +4 -4
- package/src/vite/utils/bundle-analysis.ts +4 -2
- package/src/vite/utils/client-chunks.ts +190 -0
- package/src/vite/utils/forward-user-plugins.ts +193 -0
- package/src/vite/utils/manifest-utils.ts +21 -5
- package/src/vite/utils/package-resolution.ts +41 -1
- package/src/vite/utils/prerender-utils.ts +38 -5
- package/src/vite/utils/shared-utils.ts +109 -27
- package/src/browser/action-response-classifier.ts +0 -99
|
@@ -15,9 +15,15 @@ import { getRangoState } from "./rango-state.js";
|
|
|
15
15
|
import {
|
|
16
16
|
extractRscHeaderUrl,
|
|
17
17
|
emptyResponse,
|
|
18
|
+
handleReloadHeader,
|
|
18
19
|
teeWithCompletion,
|
|
19
20
|
} from "./response-adapter.js";
|
|
20
|
-
import {
|
|
21
|
+
import {
|
|
22
|
+
buildPrefetchKey,
|
|
23
|
+
buildSourceKey,
|
|
24
|
+
consumeInflightPrefetch,
|
|
25
|
+
consumePrefetch,
|
|
26
|
+
} from "./prefetch/cache.js";
|
|
21
27
|
|
|
22
28
|
/**
|
|
23
29
|
* Create a navigation client for fetching RSC payloads
|
|
@@ -26,8 +32,10 @@ import { buildPrefetchKey, consumePrefetch } from "./prefetch/cache.js";
|
|
|
26
32
|
* deserializing the response using the RSC runtime.
|
|
27
33
|
*
|
|
28
34
|
* Checks the in-memory prefetch cache before making a network request.
|
|
29
|
-
*
|
|
30
|
-
*
|
|
35
|
+
* Tries the source-scoped key first (populated when the server tagged
|
|
36
|
+
* the response as source-sensitive via `X-RSC-Prefetch-Scope: source`)
|
|
37
|
+
* and falls back to the Rango-state-keyed wildcard slot used for the
|
|
38
|
+
* common source-agnostic case.
|
|
31
39
|
*
|
|
32
40
|
* @param deps - RSC browser dependencies (createFromFetch)
|
|
33
41
|
* @returns NavigationClient instance
|
|
@@ -57,6 +65,7 @@ export function createNavigationClient(
|
|
|
57
65
|
staleRevalidation,
|
|
58
66
|
interceptSourceUrl,
|
|
59
67
|
version,
|
|
68
|
+
routerId,
|
|
60
69
|
hmr,
|
|
61
70
|
} = options;
|
|
62
71
|
|
|
@@ -84,50 +93,105 @@ export function createNavigationClient(
|
|
|
84
93
|
if (version) {
|
|
85
94
|
fetchUrl.searchParams.set("_rsc_v", version);
|
|
86
95
|
}
|
|
96
|
+
if (routerId) {
|
|
97
|
+
fetchUrl.searchParams.set("_rsc_rid", routerId);
|
|
98
|
+
}
|
|
87
99
|
|
|
88
|
-
// Check in-memory prefetch cache before making a network
|
|
89
|
-
//
|
|
90
|
-
//
|
|
100
|
+
// Check completed in-memory prefetch cache before making a network
|
|
101
|
+
// request. Try the source-scoped key first (populated when the server
|
|
102
|
+
// tagged the prefetch response as source-sensitive, e.g. intercepts,
|
|
103
|
+
// or when a Link opted in with `prefetchKey=":source"`), then fall
|
|
104
|
+
// back to the wildcard slot shared across source pages.
|
|
105
|
+
// Both keys embed the Rango state, so state rotation (deploy or
|
|
106
|
+
// server-action invalidation) auto-invalidates both scopes.
|
|
91
107
|
// Skip cache for stale revalidation (needs fresh data), HMR (needs
|
|
92
108
|
// fresh modules), and intercept contexts (source-dependent responses).
|
|
93
|
-
const
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
109
|
+
const canUsePrefetch = !staleRevalidation && !hmr && !interceptSourceUrl;
|
|
110
|
+
const rangoState = getRangoState();
|
|
111
|
+
const wildcardKey = buildPrefetchKey(rangoState, fetchUrl);
|
|
112
|
+
const cacheKey = buildSourceKey(rangoState, previousUrl, fetchUrl);
|
|
113
|
+
|
|
114
|
+
let cachedResponse: Response | null = null;
|
|
115
|
+
let hitKey: string | null = null;
|
|
116
|
+
if (canUsePrefetch) {
|
|
117
|
+
cachedResponse = consumePrefetch(cacheKey);
|
|
118
|
+
if (cachedResponse) {
|
|
119
|
+
hitKey = cacheKey;
|
|
120
|
+
} else {
|
|
121
|
+
cachedResponse = consumePrefetch(wildcardKey);
|
|
122
|
+
if (cachedResponse) hitKey = wildcardKey;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
98
125
|
|
|
126
|
+
let inflightResponsePromise: Promise<Response | null> | null = null;
|
|
127
|
+
if (canUsePrefetch && !cachedResponse) {
|
|
128
|
+
inflightResponsePromise = consumeInflightPrefetch(cacheKey);
|
|
129
|
+
if (inflightResponsePromise) {
|
|
130
|
+
hitKey = cacheKey;
|
|
131
|
+
} else {
|
|
132
|
+
inflightResponsePromise = consumeInflightPrefetch(wildcardKey);
|
|
133
|
+
if (inflightResponsePromise) hitKey = wildcardKey;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
99
136
|
// Track when the stream completes
|
|
100
137
|
let resolveStreamComplete: () => void;
|
|
101
138
|
const streamComplete = new Promise<void>((resolve) => {
|
|
102
139
|
resolveStreamComplete = resolve;
|
|
103
140
|
});
|
|
104
141
|
|
|
105
|
-
|
|
142
|
+
/**
|
|
143
|
+
* Validate RSC control headers on any response (fresh, cached, or
|
|
144
|
+
* in-flight). Handles version-mismatch reloads and server redirects.
|
|
145
|
+
* Returns the response unchanged when no control header is present.
|
|
146
|
+
*/
|
|
147
|
+
const validateRscHeaders = (
|
|
148
|
+
response: Response,
|
|
149
|
+
source: string,
|
|
150
|
+
): Response | Promise<Response> => {
|
|
151
|
+
// Version mismatch — server wants a full page reload
|
|
152
|
+
const reloadResult = handleReloadHeader(response, {
|
|
153
|
+
onBlocked: resolveStreamComplete,
|
|
154
|
+
onReload: (url) => {
|
|
155
|
+
if (tx) {
|
|
156
|
+
browserDebugLog(tx, `version mismatch, reloading (${source})`, {
|
|
157
|
+
reloadUrl: url,
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
},
|
|
161
|
+
});
|
|
162
|
+
if (reloadResult) return reloadResult;
|
|
106
163
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
164
|
+
// Server-side redirect without state: the server returned 204 with
|
|
165
|
+
// X-RSC-Redirect instead of a 3xx (which fetch would auto-follow
|
|
166
|
+
// to a URL rendering full HTML). Throw ServerRedirect so the
|
|
167
|
+
// navigation bridge catches it and re-navigates with _skipCache.
|
|
168
|
+
const redirect = extractRscHeaderUrl(response, "X-RSC-Redirect");
|
|
169
|
+
if (redirect === "blocked") {
|
|
170
|
+
resolveStreamComplete();
|
|
171
|
+
return emptyResponse();
|
|
110
172
|
}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
173
|
+
if (redirect) {
|
|
174
|
+
if (tx) {
|
|
175
|
+
browserDebugLog(tx, `server redirect (${source})`, {
|
|
176
|
+
redirectUrl: redirect.url,
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
resolveStreamComplete();
|
|
180
|
+
throw new ServerRedirect(redirect.url, undefined);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return response;
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
/** Start a fresh navigation fetch (no cache / inflight hit). */
|
|
187
|
+
const doFreshFetch = (): Promise<Response> => {
|
|
124
188
|
if (tx) {
|
|
125
189
|
browserDebugLog(tx, "fetching", {
|
|
126
190
|
path: `${fetchUrl.pathname}${fetchUrl.search}`,
|
|
127
191
|
});
|
|
128
192
|
}
|
|
129
193
|
|
|
130
|
-
|
|
194
|
+
return fetch(fetchUrl, {
|
|
131
195
|
headers: {
|
|
132
196
|
"X-RSC-Router-Client-Path": previousUrl,
|
|
133
197
|
"X-Rango-State": getRangoState(),
|
|
@@ -139,55 +203,96 @@ export function createNavigationClient(
|
|
|
139
203
|
},
|
|
140
204
|
signal,
|
|
141
205
|
}).then((response) => {
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
206
|
+
const validated = validateRscHeaders(response, "fetch");
|
|
207
|
+
if (validated instanceof Promise) return validated;
|
|
208
|
+
|
|
209
|
+
return teeWithCompletion(
|
|
210
|
+
validated,
|
|
211
|
+
() => {
|
|
212
|
+
if (tx) browserDebugLog(tx, "stream complete");
|
|
213
|
+
resolveStreamComplete();
|
|
214
|
+
},
|
|
215
|
+
signal,
|
|
216
|
+
);
|
|
217
|
+
});
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
let responsePromise: Promise<Response>;
|
|
221
|
+
|
|
222
|
+
if (cachedResponse) {
|
|
223
|
+
if (tx) {
|
|
224
|
+
browserDebugLog(tx, "prefetch cache hit", {
|
|
225
|
+
key: hitKey,
|
|
226
|
+
wildcard: hitKey === wildcardKey,
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
responsePromise = Promise.resolve(cachedResponse).then((response) => {
|
|
230
|
+
const validated = validateRscHeaders(response, "prefetch cache");
|
|
231
|
+
if (validated instanceof Promise) return validated;
|
|
232
|
+
|
|
233
|
+
return teeWithCompletion(
|
|
234
|
+
validated,
|
|
235
|
+
() => {
|
|
236
|
+
if (tx) browserDebugLog(tx, "stream complete (from cache)");
|
|
237
|
+
resolveStreamComplete();
|
|
238
|
+
},
|
|
239
|
+
signal,
|
|
240
|
+
);
|
|
241
|
+
});
|
|
242
|
+
} else if (inflightResponsePromise) {
|
|
243
|
+
if (tx) {
|
|
244
|
+
browserDebugLog(tx, "reusing inflight prefetch", {
|
|
245
|
+
key: hitKey,
|
|
246
|
+
wildcard: hitKey === wildcardKey,
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
const adoptedViaWildcard = hitKey === wildcardKey;
|
|
250
|
+
responsePromise = inflightResponsePromise.then(async (response) => {
|
|
251
|
+
if (!response) {
|
|
149
252
|
if (tx) {
|
|
150
|
-
browserDebugLog(tx, "
|
|
151
|
-
reloadUrl: reload.url,
|
|
152
|
-
});
|
|
253
|
+
browserDebugLog(tx, "inflight prefetch unavailable, refetching");
|
|
153
254
|
}
|
|
154
|
-
|
|
155
|
-
return new Promise<Response>(() => {});
|
|
255
|
+
return doFreshFetch();
|
|
156
256
|
}
|
|
157
257
|
|
|
158
|
-
//
|
|
159
|
-
//
|
|
160
|
-
//
|
|
161
|
-
//
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
}
|
|
167
|
-
if (redirect) {
|
|
258
|
+
// Cross-source safety: an inflight promise adopted via the
|
|
259
|
+
// wildcard key may turn out to be source-scoped (server emitted
|
|
260
|
+
// `X-RSC-Prefetch-Scope: source`), which means it was built for
|
|
261
|
+
// a different source page. Discard and refetch.
|
|
262
|
+
if (
|
|
263
|
+
adoptedViaWildcard &&
|
|
264
|
+
response.headers.get("x-rsc-prefetch-scope") === "source"
|
|
265
|
+
) {
|
|
168
266
|
if (tx) {
|
|
169
|
-
browserDebugLog(
|
|
170
|
-
|
|
171
|
-
|
|
267
|
+
browserDebugLog(
|
|
268
|
+
tx,
|
|
269
|
+
"wildcard inflight turned out source-scoped, refetching",
|
|
270
|
+
);
|
|
172
271
|
}
|
|
173
|
-
|
|
174
|
-
throw new ServerRedirect(redirect.url, undefined);
|
|
272
|
+
return doFreshFetch();
|
|
175
273
|
}
|
|
176
274
|
|
|
275
|
+
const validated = validateRscHeaders(response, "inflight prefetch");
|
|
276
|
+
if (validated instanceof Promise) return validated;
|
|
277
|
+
|
|
177
278
|
return teeWithCompletion(
|
|
178
|
-
|
|
279
|
+
validated,
|
|
179
280
|
() => {
|
|
180
|
-
if (tx)
|
|
281
|
+
if (tx) {
|
|
282
|
+
browserDebugLog(tx, "stream complete (from inflight prefetch)");
|
|
283
|
+
}
|
|
181
284
|
resolveStreamComplete();
|
|
182
285
|
},
|
|
183
286
|
signal,
|
|
184
287
|
);
|
|
185
288
|
});
|
|
289
|
+
} else {
|
|
290
|
+
responsePromise = doFreshFetch();
|
|
186
291
|
}
|
|
187
292
|
|
|
188
293
|
try {
|
|
189
|
-
// Deserialize RSC payload
|
|
190
294
|
const payload = await deps.createFromFetch<RscPayload>(responsePromise);
|
|
295
|
+
|
|
191
296
|
if (tx) {
|
|
192
297
|
browserDebugLog(tx, "response received", {
|
|
193
298
|
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>();
|
|
@@ -269,18 +283,17 @@ export function createNavigationStore(
|
|
|
269
283
|
/**
|
|
270
284
|
* Create a debounced function that batches rapid calls
|
|
271
285
|
*/
|
|
286
|
+
// A non-keyed notifier is the keyed one restricted to a single constant key;
|
|
287
|
+
// its own keyed instance means the "" key never collides with action keys.
|
|
272
288
|
function createDebouncedNotifier<T extends (...args: any[]) => void>(
|
|
273
289
|
fn: T,
|
|
274
290
|
ms: number = 20,
|
|
275
291
|
): T {
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
fn(...args);
|
|
282
|
-
}, ms);
|
|
283
|
-
}) as T;
|
|
292
|
+
const keyed = createKeyedDebouncedNotifier(
|
|
293
|
+
(_key: string, ...args: any[]) => fn(...args),
|
|
294
|
+
ms,
|
|
295
|
+
);
|
|
296
|
+
return ((...args: Parameters<T>) => keyed("", ...args)) as T;
|
|
284
297
|
}
|
|
285
298
|
|
|
286
299
|
/**
|
|
@@ -324,6 +337,18 @@ export function createNavigationStore(
|
|
|
324
337
|
clearPrefetchCache();
|
|
325
338
|
}
|
|
326
339
|
|
|
340
|
+
/**
|
|
341
|
+
* Drop this tab's navigation + prefetch caches without broadcasting or
|
|
342
|
+
* rotating shared state. Used when the local session changes in a way that
|
|
343
|
+
* doesn't affect other tabs — e.g. this tab crosses into a different app
|
|
344
|
+
* via a cross-router navigation. Other tabs in the old app keep their
|
|
345
|
+
* caches and their X-Rango-State token.
|
|
346
|
+
*/
|
|
347
|
+
function clearCacheInternalLocal(): void {
|
|
348
|
+
historyCache.length = 0;
|
|
349
|
+
clearPrefetchCacheLocal();
|
|
350
|
+
}
|
|
351
|
+
|
|
327
352
|
/**
|
|
328
353
|
* Mark all cache entries as stale (internal - does not broadcast)
|
|
329
354
|
*/
|
|
@@ -571,10 +596,17 @@ export function createNavigationStore(
|
|
|
571
596
|
segments,
|
|
572
597
|
false,
|
|
573
598
|
clonedHandleData,
|
|
599
|
+
currentRouterId,
|
|
574
600
|
];
|
|
575
601
|
} else {
|
|
576
602
|
// Add new entry at the end (not stale)
|
|
577
|
-
historyCache.push([
|
|
603
|
+
historyCache.push([
|
|
604
|
+
historyKey,
|
|
605
|
+
segments,
|
|
606
|
+
false,
|
|
607
|
+
clonedHandleData,
|
|
608
|
+
currentRouterId,
|
|
609
|
+
]);
|
|
578
610
|
// Remove oldest entries if over limit
|
|
579
611
|
while (historyCache.length > cacheSize) {
|
|
580
612
|
historyCache.shift();
|
|
@@ -586,14 +618,22 @@ export function createNavigationStore(
|
|
|
586
618
|
* Get cached segments for a history entry
|
|
587
619
|
* Returns { segments, stale, handleData } or undefined if not cached
|
|
588
620
|
*/
|
|
589
|
-
getCachedSegments(
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
621
|
+
getCachedSegments(historyKey: string):
|
|
622
|
+
| {
|
|
623
|
+
segments: ResolvedSegment[];
|
|
624
|
+
stale: boolean;
|
|
625
|
+
handleData?: HandleData;
|
|
626
|
+
routerId?: string;
|
|
627
|
+
}
|
|
593
628
|
| undefined {
|
|
594
629
|
const entry = historyCache.find(([key]) => key === historyKey);
|
|
595
630
|
if (!entry) return undefined;
|
|
596
|
-
return {
|
|
631
|
+
return {
|
|
632
|
+
segments: entry[1],
|
|
633
|
+
stale: entry[2],
|
|
634
|
+
handleData: entry[3],
|
|
635
|
+
routerId: entry[4],
|
|
636
|
+
};
|
|
597
637
|
},
|
|
598
638
|
|
|
599
639
|
/**
|
|
@@ -621,6 +661,7 @@ export function createNavigationStore(
|
|
|
621
661
|
entry[1],
|
|
622
662
|
entry[2],
|
|
623
663
|
clonedHandleData,
|
|
664
|
+
entry[4], // preserve routerId
|
|
624
665
|
];
|
|
625
666
|
}
|
|
626
667
|
},
|
|
@@ -641,6 +682,15 @@ export function createNavigationStore(
|
|
|
641
682
|
clearCacheAndBroadcast();
|
|
642
683
|
},
|
|
643
684
|
|
|
685
|
+
/**
|
|
686
|
+
* Drop this tab's navigation + prefetch caches locally without
|
|
687
|
+
* broadcasting or rotating shared state. Intended for cross-app
|
|
688
|
+
* transitions where the session state diverges for this tab only.
|
|
689
|
+
*/
|
|
690
|
+
clearHistoryCacheLocal(): void {
|
|
691
|
+
clearCacheInternalLocal();
|
|
692
|
+
},
|
|
693
|
+
|
|
644
694
|
/**
|
|
645
695
|
* Mark cache as stale and broadcast to other tabs
|
|
646
696
|
* Called after server actions - allows SWR pattern for popstate
|
|
@@ -687,6 +737,14 @@ export function createNavigationStore(
|
|
|
687
737
|
interceptSourceUrl = url;
|
|
688
738
|
},
|
|
689
739
|
|
|
740
|
+
getRouterId(): string | undefined {
|
|
741
|
+
return currentRouterId;
|
|
742
|
+
},
|
|
743
|
+
|
|
744
|
+
setRouterId(id: string): void {
|
|
745
|
+
currentRouterId = id;
|
|
746
|
+
},
|
|
747
|
+
|
|
690
748
|
// ========================================================================
|
|
691
749
|
// UI Update Notifications
|
|
692
750
|
// ========================================================================
|
|
@@ -7,12 +7,11 @@ 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";
|
|
14
13
|
import { debugLog } from "./logging.js";
|
|
15
|
-
import { buildHistoryState } from "./history-state.js";
|
|
14
|
+
import { buildHistoryState, pushHistoryWithIdx } from "./history-state.js";
|
|
16
15
|
|
|
17
16
|
// Re-export for consumers that import from navigation-transaction
|
|
18
17
|
export { resolveNavigationState } from "./history-state.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
|
|
@@ -186,12 +186,8 @@ export function createNavigationTransaction(
|
|
|
186
186
|
// Used to detect when location state is being cleared.
|
|
187
187
|
const oldState = window.history.state;
|
|
188
188
|
|
|
189
|
-
// Update browser URL
|
|
190
|
-
|
|
191
|
-
window.history.replaceState(historyState, "", url);
|
|
192
|
-
} else {
|
|
193
|
-
window.history.pushState(historyState, "", url);
|
|
194
|
-
}
|
|
189
|
+
// Update browser URL (stamps history.state.idx for back() first-entry detection)
|
|
190
|
+
pushHistoryWithIdx(historyState, url, replace ?? false);
|
|
195
191
|
// Ensure new history entry has a scroll restoration key
|
|
196
192
|
ensureHistoryKey();
|
|
197
193
|
|
|
@@ -205,14 +201,16 @@ export function createNavigationTransaction(
|
|
|
205
201
|
// Complete the navigation in event controller (sets idle state, updates location)
|
|
206
202
|
handle.complete(parsedUrl);
|
|
207
203
|
|
|
208
|
-
//
|
|
209
|
-
|
|
204
|
+
// NOTE: Scroll is NOT handled here. The caller (partial-update.ts) handles
|
|
205
|
+
// scroll AFTER onUpdate() so React has the new content before we scroll.
|
|
210
206
|
|
|
211
207
|
debugLog(
|
|
212
208
|
"[Browser] Navigation committed, historyKey:",
|
|
213
209
|
historyKey,
|
|
214
210
|
intercept ? "(intercept)" : "",
|
|
215
211
|
);
|
|
212
|
+
|
|
213
|
+
return { scroll };
|
|
216
214
|
}
|
|
217
215
|
|
|
218
216
|
return {
|
|
@@ -238,32 +236,18 @@ export function createNavigationTransaction(
|
|
|
238
236
|
segments: ResolvedSegment[],
|
|
239
237
|
overrides?: BoundCommitOverrides,
|
|
240
238
|
) => {
|
|
241
|
-
|
|
242
|
-
const
|
|
243
|
-
|
|
244
|
-
// Allow overrides to force replace (e.g., for intercepts)
|
|
245
|
-
const finalReplace =
|
|
246
|
-
overrides?.replace !== undefined ? overrides.replace : opts.replace;
|
|
247
|
-
// Intercept info: overrides take precedence, fallback to opts
|
|
248
|
-
const intercept =
|
|
249
|
-
overrides?.intercept !== undefined
|
|
250
|
-
? overrides.intercept
|
|
251
|
-
: opts.intercept;
|
|
239
|
+
const finalScroll = overrides?.scroll ?? opts.scroll;
|
|
240
|
+
const finalReplace = overrides?.replace ?? opts.replace;
|
|
241
|
+
const intercept = overrides?.intercept ?? opts.intercept;
|
|
252
242
|
const interceptSourceUrl =
|
|
253
|
-
overrides?.interceptSourceUrl
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
//
|
|
257
|
-
const cacheOnly =
|
|
258
|
-
overrides?.cacheOnly !== undefined
|
|
259
|
-
? overrides.cacheOnly
|
|
260
|
-
: opts.cacheOnly;
|
|
261
|
-
// User state: overrides take precedence, fallback to opts
|
|
243
|
+
overrides?.interceptSourceUrl ?? opts.interceptSourceUrl;
|
|
244
|
+
const cacheOnly = overrides?.cacheOnly ?? opts.cacheOnly;
|
|
245
|
+
// state is `unknown` (null is meaningful) so `??` would wrongly drop a
|
|
246
|
+
// null override; serverState always comes from overrides, never opts.
|
|
262
247
|
const state =
|
|
263
248
|
overrides?.state !== undefined ? overrides.state : opts.state;
|
|
264
|
-
// Server-set location state: only from overrides (set by partial-update)
|
|
265
249
|
const serverState = overrides?.serverState;
|
|
266
|
-
commit({
|
|
250
|
+
return commit({
|
|
267
251
|
...opts,
|
|
268
252
|
segmentIds,
|
|
269
253
|
segments,
|