@rangojs/router 0.0.0-experimental.7 → 0.0.0-experimental.71
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 +9 -0
- package/README.md +942 -4
- package/dist/bin/rango.js +1689 -0
- package/dist/vite/index.js +4951 -930
- package/package.json +70 -60
- package/skills/breadcrumbs/SKILL.md +250 -0
- package/skills/cache-guide/SKILL.md +294 -0
- package/skills/caching/SKILL.md +93 -23
- package/skills/composability/SKILL.md +172 -0
- package/skills/debug-manifest/SKILL.md +12 -8
- package/skills/document-cache/SKILL.md +18 -16
- package/skills/fonts/SKILL.md +167 -0
- package/skills/hooks/SKILL.md +334 -72
- package/skills/host-router/SKILL.md +218 -0
- package/skills/intercept/SKILL.md +131 -8
- package/skills/layout/SKILL.md +100 -3
- package/skills/links/SKILL.md +92 -31
- package/skills/loader/SKILL.md +404 -44
- package/skills/middleware/SKILL.md +173 -34
- package/skills/mime-routes/SKILL.md +128 -0
- package/skills/parallel/SKILL.md +204 -1
- package/skills/prerender/SKILL.md +685 -0
- package/skills/rango/SKILL.md +85 -16
- package/skills/response-routes/SKILL.md +411 -0
- package/skills/route/SKILL.md +257 -14
- package/skills/router-setup/SKILL.md +210 -32
- package/skills/tailwind/SKILL.md +129 -0
- package/skills/theme/SKILL.md +9 -8
- package/skills/typesafety/SKILL.md +328 -89
- package/skills/use-cache/SKILL.md +324 -0
- package/src/__internal.ts +102 -4
- package/src/bin/rango.ts +321 -0
- package/src/browser/action-coordinator.ts +97 -0
- package/src/browser/action-response-classifier.ts +99 -0
- package/src/browser/app-version.ts +14 -0
- package/src/browser/event-controller.ts +92 -64
- package/src/browser/history-state.ts +80 -0
- package/src/browser/intercept-utils.ts +52 -0
- package/src/browser/link-interceptor.ts +24 -4
- package/src/browser/logging.ts +55 -0
- package/src/browser/merge-segment-loaders.ts +20 -12
- package/src/browser/navigation-bridge.ts +296 -558
- package/src/browser/navigation-client.ts +179 -69
- package/src/browser/navigation-store.ts +73 -55
- package/src/browser/navigation-transaction.ts +297 -0
- package/src/browser/network-error-handler.ts +61 -0
- package/src/browser/partial-update.ts +328 -313
- package/src/browser/prefetch/cache.ts +206 -0
- package/src/browser/prefetch/fetch.ts +150 -0
- package/src/browser/prefetch/observer.ts +65 -0
- package/src/browser/prefetch/policy.ts +48 -0
- package/src/browser/prefetch/queue.ts +160 -0
- package/src/browser/prefetch/resource-ready.ts +77 -0
- package/src/browser/rango-state.ts +112 -0
- package/src/browser/react/Link.tsx +230 -74
- package/src/browser/react/NavigationProvider.tsx +87 -11
- package/src/browser/react/context.ts +11 -0
- package/src/browser/react/filter-segment-order.ts +11 -0
- package/src/browser/react/index.ts +12 -12
- package/src/browser/react/location-state-shared.ts +95 -53
- package/src/browser/react/location-state.ts +60 -15
- package/src/browser/react/mount-context.ts +6 -1
- package/src/browser/react/nonce-context.ts +23 -0
- package/src/browser/react/shallow-equal.ts +27 -0
- package/src/browser/react/use-action.ts +29 -51
- package/src/browser/react/use-client-cache.ts +5 -3
- package/src/browser/react/use-handle.ts +30 -126
- package/src/browser/react/use-href.tsx +2 -2
- package/src/browser/react/use-link-status.ts +6 -5
- package/src/browser/react/use-navigation.ts +22 -63
- package/src/browser/react/use-params.ts +65 -0
- package/src/browser/react/use-pathname.ts +47 -0
- package/src/browser/react/use-router.ts +76 -0
- package/src/browser/react/use-search-params.ts +56 -0
- package/src/browser/react/use-segments.ts +80 -97
- package/src/browser/response-adapter.ts +73 -0
- package/src/browser/rsc-router.tsx +214 -58
- package/src/browser/scroll-restoration.ts +127 -52
- package/src/browser/segment-reconciler.ts +221 -0
- package/src/browser/segment-structure-assert.ts +16 -0
- package/src/browser/server-action-bridge.ts +510 -603
- package/src/browser/shallow.ts +6 -1
- package/src/browser/types.ts +141 -48
- package/src/browser/validate-redirect-origin.ts +29 -0
- package/src/build/generate-manifest.ts +235 -24
- package/src/build/generate-route-types.ts +39 -0
- package/src/build/index.ts +13 -0
- package/src/build/route-trie.ts +265 -0
- package/src/build/route-types/ast-helpers.ts +25 -0
- package/src/build/route-types/ast-route-extraction.ts +98 -0
- package/src/build/route-types/codegen.ts +102 -0
- package/src/build/route-types/include-resolution.ts +418 -0
- package/src/build/route-types/param-extraction.ts +48 -0
- package/src/build/route-types/per-module-writer.ts +128 -0
- package/src/build/route-types/router-processing.ts +618 -0
- package/src/build/route-types/scan-filter.ts +85 -0
- package/src/build/runtime-discovery.ts +231 -0
- package/src/cache/background-task.ts +34 -0
- package/src/cache/cache-key-utils.ts +44 -0
- package/src/cache/cache-policy.ts +125 -0
- package/src/cache/cache-runtime.ts +342 -0
- package/src/cache/cache-scope.ts +167 -309
- package/src/cache/cf/cf-cache-store.ts +571 -17
- package/src/cache/cf/index.ts +13 -3
- package/src/cache/document-cache.ts +116 -77
- package/src/cache/handle-capture.ts +81 -0
- package/src/cache/handle-snapshot.ts +41 -0
- package/src/cache/index.ts +1 -15
- package/src/cache/memory-segment-store.ts +191 -13
- package/src/cache/profile-registry.ts +73 -0
- package/src/cache/read-through-swr.ts +134 -0
- package/src/cache/segment-codec.ts +256 -0
- package/src/cache/taint.ts +153 -0
- package/src/cache/types.ts +72 -122
- package/src/client.rsc.tsx +3 -1
- package/src/client.tsx +105 -179
- package/src/component-utils.ts +4 -4
- package/src/components/DefaultDocument.tsx +5 -1
- package/src/context-var.ts +156 -0
- package/src/debug.ts +19 -9
- package/src/errors.ts +108 -2
- package/src/handle.ts +55 -29
- package/src/handles/MetaTags.tsx +73 -20
- package/src/handles/breadcrumbs.ts +66 -0
- package/src/handles/index.ts +1 -0
- package/src/handles/meta.ts +30 -13
- package/src/host/cookie-handler.ts +21 -15
- package/src/host/errors.ts +8 -8
- package/src/host/index.ts +4 -7
- package/src/host/pattern-matcher.ts +27 -27
- package/src/host/router.ts +61 -39
- package/src/host/testing.ts +8 -8
- package/src/host/types.ts +15 -7
- package/src/host/utils.ts +1 -1
- package/src/href-client.ts +119 -29
- package/src/index.rsc.ts +155 -19
- package/src/index.ts +223 -30
- package/src/internal-debug.ts +11 -0
- package/src/loader.rsc.ts +26 -157
- package/src/loader.ts +27 -10
- package/src/network-error-thrower.tsx +3 -1
- package/src/outlet-provider.tsx +45 -0
- package/src/prerender/param-hash.ts +37 -0
- package/src/prerender/store.ts +186 -0
- package/src/prerender.ts +524 -0
- package/src/reverse.ts +351 -0
- package/src/root-error-boundary.tsx +41 -29
- package/src/route-content-wrapper.tsx +7 -4
- package/src/route-definition/dsl-helpers.ts +982 -0
- package/src/route-definition/helper-factories.ts +200 -0
- package/src/route-definition/helpers-types.ts +434 -0
- package/src/route-definition/index.ts +55 -0
- package/src/route-definition/redirect.ts +101 -0
- package/src/route-definition/resolve-handler-use.ts +149 -0
- package/src/route-definition.ts +1 -1428
- package/src/route-map-builder.ts +217 -123
- package/src/route-name.ts +53 -0
- package/src/route-types.ts +70 -8
- package/src/router/content-negotiation.ts +215 -0
- package/src/router/debug-manifest.ts +72 -0
- package/src/router/error-handling.ts +9 -9
- package/src/router/find-match.ts +160 -0
- package/src/router/handler-context.ts +435 -86
- package/src/router/intercept-resolution.ts +402 -0
- package/src/router/lazy-includes.ts +237 -0
- package/src/router/loader-resolution.ts +356 -128
- package/src/router/logging.ts +251 -0
- package/src/router/manifest.ts +154 -35
- package/src/router/match-api.ts +555 -0
- package/src/router/match-context.ts +5 -3
- package/src/router/match-handlers.ts +440 -0
- package/src/router/match-middleware/background-revalidation.ts +108 -93
- package/src/router/match-middleware/cache-lookup.ts +459 -10
- package/src/router/match-middleware/cache-store.ts +98 -26
- package/src/router/match-middleware/intercept-resolution.ts +57 -17
- package/src/router/match-middleware/segment-resolution.ts +80 -6
- package/src/router/match-pipelines.ts +10 -45
- package/src/router/match-result.ts +135 -35
- package/src/router/metrics.ts +240 -15
- package/src/router/middleware-cookies.ts +55 -0
- package/src/router/middleware-types.ts +220 -0
- package/src/router/middleware.ts +324 -369
- package/src/router/navigation-snapshot.ts +182 -0
- package/src/router/pattern-matching.ts +211 -43
- package/src/router/prerender-match.ts +502 -0
- package/src/router/preview-match.ts +98 -0
- package/src/router/request-classification.ts +310 -0
- package/src/router/revalidation.ts +137 -38
- package/src/router/route-snapshot.ts +245 -0
- package/src/router/router-context.ts +41 -21
- package/src/router/router-interfaces.ts +484 -0
- package/src/router/router-options.ts +618 -0
- package/src/router/router-registry.ts +24 -0
- package/src/router/segment-resolution/fresh.ts +748 -0
- package/src/router/segment-resolution/helpers.ts +268 -0
- package/src/router/segment-resolution/loader-cache.ts +199 -0
- package/src/router/segment-resolution/revalidation.ts +1379 -0
- package/src/router/segment-resolution/static-store.ts +67 -0
- package/src/router/segment-resolution.ts +21 -0
- package/src/router/segment-wrappers.ts +291 -0
- package/src/router/telemetry-otel.ts +299 -0
- package/src/router/telemetry.ts +300 -0
- package/src/router/timeout.ts +148 -0
- package/src/router/trie-matching.ts +239 -0
- package/src/router/types.ts +78 -3
- package/src/router.ts +740 -4252
- package/src/rsc/handler-context.ts +45 -0
- package/src/rsc/handler.ts +907 -797
- package/src/rsc/helpers.ts +140 -6
- package/src/rsc/index.ts +0 -20
- package/src/rsc/loader-fetch.ts +229 -0
- package/src/rsc/manifest-init.ts +90 -0
- package/src/rsc/nonce.ts +14 -0
- package/src/rsc/origin-guard.ts +141 -0
- package/src/rsc/progressive-enhancement.ts +391 -0
- package/src/rsc/response-error.ts +37 -0
- package/src/rsc/response-route-handler.ts +347 -0
- package/src/rsc/rsc-rendering.ts +246 -0
- package/src/rsc/runtime-warnings.ts +42 -0
- package/src/rsc/server-action.ts +356 -0
- package/src/rsc/ssr-setup.ts +128 -0
- package/src/rsc/types.ts +46 -11
- package/src/search-params.ts +230 -0
- package/src/segment-system.tsx +165 -17
- package/src/server/context.ts +315 -58
- package/src/server/cookie-store.ts +190 -0
- package/src/server/fetchable-loader-store.ts +37 -0
- package/src/server/handle-store.ts +113 -15
- package/src/server/loader-registry.ts +24 -64
- package/src/server/request-context.ts +607 -81
- package/src/server.ts +35 -130
- package/src/ssr/index.tsx +103 -30
- package/src/static-handler.ts +126 -0
- package/src/theme/ThemeProvider.tsx +21 -15
- package/src/theme/ThemeScript.tsx +5 -5
- package/src/theme/constants.ts +5 -2
- package/src/theme/index.ts +4 -14
- package/src/theme/theme-context.ts +4 -30
- package/src/theme/theme-script.ts +21 -18
- package/src/types/boundaries.ts +158 -0
- package/src/types/cache-types.ts +198 -0
- package/src/types/error-types.ts +192 -0
- package/src/types/global-namespace.ts +100 -0
- package/src/types/handler-context.ts +791 -0
- package/src/types/index.ts +88 -0
- package/src/types/loader-types.ts +210 -0
- package/src/types/route-config.ts +170 -0
- package/src/types/route-entry.ts +109 -0
- package/src/types/segments.ts +151 -0
- package/src/types.ts +1 -1623
- package/src/urls/include-helper.ts +197 -0
- package/src/urls/index.ts +53 -0
- package/src/urls/path-helper-types.ts +346 -0
- package/src/urls/path-helper.ts +364 -0
- package/src/urls/pattern-types.ts +107 -0
- package/src/urls/response-types.ts +116 -0
- package/src/urls/type-extraction.ts +372 -0
- package/src/urls/urls-function.ts +98 -0
- package/src/urls.ts +1 -802
- package/src/use-loader.tsx +161 -81
- package/src/vite/discovery/bundle-postprocess.ts +181 -0
- package/src/vite/discovery/discover-routers.ts +348 -0
- package/src/vite/discovery/prerender-collection.ts +439 -0
- package/src/vite/discovery/route-types-writer.ts +258 -0
- package/src/vite/discovery/self-gen-tracking.ts +47 -0
- package/src/vite/discovery/state.ts +117 -0
- package/src/vite/discovery/virtual-module-codegen.ts +203 -0
- package/src/vite/index.ts +15 -1129
- package/src/vite/plugin-types.ts +103 -0
- package/src/vite/plugins/cjs-to-esm.ts +93 -0
- package/src/vite/plugins/client-ref-dedup.ts +115 -0
- package/src/vite/plugins/client-ref-hashing.ts +105 -0
- package/src/vite/{expose-action-id.ts → plugins/expose-action-id.ts} +72 -53
- package/src/vite/plugins/expose-id-utils.ts +299 -0
- package/src/vite/plugins/expose-ids/export-analysis.ts +296 -0
- package/src/vite/plugins/expose-ids/handler-transform.ts +209 -0
- package/src/vite/plugins/expose-ids/loader-transform.ts +74 -0
- package/src/vite/plugins/expose-ids/router-transform.ts +110 -0
- package/src/vite/plugins/expose-ids/types.ts +45 -0
- package/src/vite/plugins/expose-internal-ids.ts +786 -0
- package/src/vite/plugins/performance-tracks.ts +88 -0
- package/src/vite/plugins/refresh-cmd.ts +127 -0
- package/src/vite/plugins/use-cache-transform.ts +323 -0
- package/src/vite/plugins/version-injector.ts +83 -0
- package/src/vite/plugins/version-plugin.ts +266 -0
- package/src/vite/{virtual-entries.ts → plugins/virtual-entries.ts} +23 -14
- package/src/vite/plugins/virtual-stub-plugin.ts +29 -0
- package/src/vite/rango.ts +462 -0
- package/src/vite/router-discovery.ts +918 -0
- package/src/vite/utils/ast-handler-extract.ts +517 -0
- package/src/vite/utils/banner.ts +36 -0
- package/src/vite/utils/bundle-analysis.ts +137 -0
- package/src/vite/utils/manifest-utils.ts +70 -0
- package/src/vite/{package-resolution.ts → utils/package-resolution.ts} +25 -29
- package/src/vite/utils/prerender-utils.ts +207 -0
- package/src/vite/utils/shared-utils.ts +170 -0
- package/CLAUDE.md +0 -43
- package/src/browser/lru-cache.ts +0 -69
- package/src/browser/request-controller.ts +0 -164
- package/src/cache/memory-store.ts +0 -253
- package/src/href-context.ts +0 -33
- package/src/href.ts +0 -255
- package/src/server/route-manifest-cache.ts +0 -173
- package/src/vite/expose-handle-id.ts +0 -209
- package/src/vite/expose-loader-id.ts +0 -426
- package/src/vite/expose-location-state-id.ts +0 -177
- /package/src/vite/{version.d.ts → plugins/version.d.ts} +0 -0
|
@@ -5,7 +5,23 @@ import type {
|
|
|
5
5
|
RscPayload,
|
|
6
6
|
RscBrowserDependencies,
|
|
7
7
|
} from "./types.js";
|
|
8
|
-
import { NetworkError, isNetworkError } from "../errors.js";
|
|
8
|
+
import { NetworkError, ServerRedirect, isNetworkError } from "../errors.js";
|
|
9
|
+
import {
|
|
10
|
+
browserDebugLog,
|
|
11
|
+
isBrowserDebugEnabled,
|
|
12
|
+
startBrowserTransaction,
|
|
13
|
+
} from "./logging.js";
|
|
14
|
+
import { getRangoState } from "./rango-state.js";
|
|
15
|
+
import {
|
|
16
|
+
extractRscHeaderUrl,
|
|
17
|
+
emptyResponse,
|
|
18
|
+
teeWithCompletion,
|
|
19
|
+
} from "./response-adapter.js";
|
|
20
|
+
import {
|
|
21
|
+
buildPrefetchKey,
|
|
22
|
+
consumeInflightPrefetch,
|
|
23
|
+
consumePrefetch,
|
|
24
|
+
} from "./prefetch/cache.js";
|
|
9
25
|
|
|
10
26
|
/**
|
|
11
27
|
* Create a navigation client for fetching RSC payloads
|
|
@@ -13,21 +29,12 @@ import { NetworkError, isNetworkError } from "../errors.js";
|
|
|
13
29
|
* The client handles building URLs with RSC parameters and
|
|
14
30
|
* deserializing the response using the RSC runtime.
|
|
15
31
|
*
|
|
32
|
+
* Checks the in-memory prefetch cache before making a network request.
|
|
33
|
+
* The cache key is source-dependent (includes the previous URL) so
|
|
34
|
+
* prefetch responses match the exact diff the server would produce.
|
|
35
|
+
*
|
|
16
36
|
* @param deps - RSC browser dependencies (createFromFetch)
|
|
17
37
|
* @returns NavigationClient instance
|
|
18
|
-
*
|
|
19
|
-
* @example
|
|
20
|
-
* ```typescript
|
|
21
|
-
* import { createFromFetch } from "@vitejs/plugin-rsc/browser";
|
|
22
|
-
*
|
|
23
|
-
* const client = createNavigationClient({ createFromFetch });
|
|
24
|
-
*
|
|
25
|
-
* const payload = await client.fetchPartial({
|
|
26
|
-
* targetUrl: "/shop/products",
|
|
27
|
-
* segmentIds: ["root", "shop"],
|
|
28
|
-
* previousUrl: "/",
|
|
29
|
-
* });
|
|
30
|
-
* ```
|
|
31
38
|
*/
|
|
32
39
|
export function createNavigationClient(
|
|
33
40
|
deps: Pick<RscBrowserDependencies, "createFromFetch">,
|
|
@@ -36,8 +43,9 @@ export function createNavigationClient(
|
|
|
36
43
|
/**
|
|
37
44
|
* Fetch a partial RSC payload for navigation
|
|
38
45
|
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
46
|
+
* First checks the in-memory prefetch cache for a matching entry.
|
|
47
|
+
* If found, uses the cached response instantly. Otherwise sends
|
|
48
|
+
* current segment IDs to the server for diff-based rendering.
|
|
41
49
|
*
|
|
42
50
|
* @param options - Fetch options
|
|
43
51
|
* @returns RSC payload with segments and metadata, plus stream completion promise
|
|
@@ -53,17 +61,25 @@ export function createNavigationClient(
|
|
|
53
61
|
staleRevalidation,
|
|
54
62
|
interceptSourceUrl,
|
|
55
63
|
version,
|
|
64
|
+
routerId,
|
|
65
|
+
hmr,
|
|
56
66
|
} = options;
|
|
57
67
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
if (
|
|
63
|
-
|
|
68
|
+
const debugEnabled = isBrowserDebugEnabled();
|
|
69
|
+
const tx = debugEnabled
|
|
70
|
+
? startBrowserTransaction(staleRevalidation ? "revalidate" : "navigate")
|
|
71
|
+
: null;
|
|
72
|
+
if (tx) {
|
|
73
|
+
browserDebugLog(tx, "request start", {
|
|
74
|
+
from: previousUrl,
|
|
75
|
+
to: targetUrl,
|
|
76
|
+
segments: segmentIds,
|
|
77
|
+
staleRevalidation: !!staleRevalidation,
|
|
78
|
+
});
|
|
64
79
|
}
|
|
65
80
|
|
|
66
|
-
// Build fetch URL with partial rendering params
|
|
81
|
+
// Build fetch URL with partial rendering params (used for both
|
|
82
|
+
// cache key lookup and actual fetch if cache misses)
|
|
67
83
|
const fetchUrl = new URL(targetUrl, window.location.origin);
|
|
68
84
|
fetchUrl.searchParams.set("_rsc_partial", "true");
|
|
69
85
|
fetchUrl.searchParams.set("_rsc_segments", segmentIds.join(","));
|
|
@@ -73,75 +89,169 @@ export function createNavigationClient(
|
|
|
73
89
|
if (version) {
|
|
74
90
|
fetchUrl.searchParams.set("_rsc_v", version);
|
|
75
91
|
}
|
|
92
|
+
if (routerId) {
|
|
93
|
+
fetchUrl.searchParams.set("_rsc_rid", routerId);
|
|
94
|
+
}
|
|
76
95
|
|
|
77
|
-
|
|
78
|
-
|
|
96
|
+
// Check completed in-memory prefetch cache before making a network request.
|
|
97
|
+
// The cache key includes the source URL (previousUrl) because the
|
|
98
|
+
// server's diff response depends on the source page context.
|
|
99
|
+
// Skip cache for stale revalidation (needs fresh data), HMR (needs
|
|
100
|
+
// fresh modules), and intercept contexts (source-dependent responses).
|
|
101
|
+
//
|
|
102
|
+
const canUsePrefetch = !staleRevalidation && !hmr && !interceptSourceUrl;
|
|
103
|
+
const cacheKey = buildPrefetchKey(previousUrl, fetchUrl);
|
|
104
|
+
const cachedResponse = canUsePrefetch ? consumePrefetch(cacheKey) : null;
|
|
105
|
+
const inflightResponsePromise = canUsePrefetch
|
|
106
|
+
? consumeInflightPrefetch(cacheKey)
|
|
107
|
+
: null;
|
|
79
108
|
// Track when the stream completes
|
|
80
109
|
let resolveStreamComplete: () => void;
|
|
81
110
|
const streamComplete = new Promise<void>((resolve) => {
|
|
82
111
|
resolveStreamComplete = resolve;
|
|
83
112
|
});
|
|
84
113
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
114
|
+
/**
|
|
115
|
+
* Validate RSC control headers on any response (fresh, cached, or
|
|
116
|
+
* in-flight). Handles version-mismatch reloads and server redirects.
|
|
117
|
+
* Returns the response unchanged when no control header is present.
|
|
118
|
+
*/
|
|
119
|
+
const validateRscHeaders = (
|
|
120
|
+
response: Response,
|
|
121
|
+
source: string,
|
|
122
|
+
): Response | Promise<Response> => {
|
|
123
|
+
// Version mismatch — server wants a full page reload
|
|
124
|
+
const reload = extractRscHeaderUrl(response, "X-RSC-Reload");
|
|
125
|
+
if (reload === "blocked") {
|
|
126
|
+
resolveStreamComplete();
|
|
127
|
+
return emptyResponse();
|
|
128
|
+
}
|
|
129
|
+
if (reload) {
|
|
130
|
+
if (tx) {
|
|
131
|
+
browserDebugLog(tx, `version mismatch, reloading (${source})`, {
|
|
132
|
+
reloadUrl: reload.url,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
window.location.href = reload.url;
|
|
136
|
+
// Block further processing — page is reloading
|
|
101
137
|
return new Promise<Response>(() => {});
|
|
102
138
|
}
|
|
103
139
|
|
|
104
|
-
|
|
105
|
-
|
|
140
|
+
// Server-side redirect without state: the server returned 204 with
|
|
141
|
+
// X-RSC-Redirect instead of a 3xx (which fetch would auto-follow
|
|
142
|
+
// to a URL rendering full HTML). Throw ServerRedirect so the
|
|
143
|
+
// navigation bridge catches it and re-navigates with _skipCache.
|
|
144
|
+
const redirect = extractRscHeaderUrl(response, "X-RSC-Redirect");
|
|
145
|
+
if (redirect === "blocked") {
|
|
146
|
+
resolveStreamComplete();
|
|
147
|
+
return emptyResponse();
|
|
148
|
+
}
|
|
149
|
+
if (redirect) {
|
|
150
|
+
if (tx) {
|
|
151
|
+
browserDebugLog(tx, `server redirect (${source})`, {
|
|
152
|
+
redirectUrl: redirect.url,
|
|
153
|
+
});
|
|
154
|
+
}
|
|
106
155
|
resolveStreamComplete();
|
|
107
|
-
|
|
156
|
+
throw new ServerRedirect(redirect.url, undefined);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return response;
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
/** Start a fresh navigation fetch (no cache / inflight hit). */
|
|
163
|
+
const doFreshFetch = (): Promise<Response> => {
|
|
164
|
+
if (tx) {
|
|
165
|
+
browserDebugLog(tx, "fetching", {
|
|
166
|
+
path: `${fetchUrl.pathname}${fetchUrl.search}`,
|
|
167
|
+
});
|
|
108
168
|
}
|
|
109
169
|
|
|
110
|
-
|
|
111
|
-
|
|
170
|
+
return fetch(fetchUrl, {
|
|
171
|
+
headers: {
|
|
172
|
+
"X-RSC-Router-Client-Path": previousUrl,
|
|
173
|
+
"X-Rango-State": getRangoState(),
|
|
174
|
+
...(tx && { "X-RSC-Router-Request-Id": tx.requestId }),
|
|
175
|
+
...(interceptSourceUrl && {
|
|
176
|
+
"X-RSC-Router-Intercept-Source": interceptSourceUrl,
|
|
177
|
+
}),
|
|
178
|
+
...(hmr && { "X-RSC-HMR": "1" }),
|
|
179
|
+
},
|
|
180
|
+
signal,
|
|
181
|
+
}).then((response) => {
|
|
182
|
+
const validated = validateRscHeaders(response, "fetch");
|
|
183
|
+
if (validated instanceof Promise) return validated;
|
|
112
184
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
185
|
+
return teeWithCompletion(
|
|
186
|
+
validated,
|
|
187
|
+
() => {
|
|
188
|
+
if (tx) browserDebugLog(tx, "stream complete");
|
|
189
|
+
resolveStreamComplete();
|
|
190
|
+
},
|
|
191
|
+
signal,
|
|
192
|
+
);
|
|
193
|
+
});
|
|
194
|
+
};
|
|
116
195
|
|
|
117
|
-
|
|
118
|
-
const onAbort = reader.cancel.bind(reader);
|
|
119
|
-
signal?.addEventListener("abort", onAbort, { once: true });
|
|
196
|
+
let responsePromise: Promise<Response>;
|
|
120
197
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
198
|
+
if (cachedResponse) {
|
|
199
|
+
if (tx) {
|
|
200
|
+
browserDebugLog(tx, "prefetch cache hit", { key: cacheKey });
|
|
201
|
+
}
|
|
202
|
+
responsePromise = Promise.resolve(cachedResponse).then((response) => {
|
|
203
|
+
const validated = validateRscHeaders(response, "prefetch cache");
|
|
204
|
+
if (validated instanceof Promise) return validated;
|
|
205
|
+
|
|
206
|
+
return teeWithCompletion(
|
|
207
|
+
validated,
|
|
208
|
+
() => {
|
|
209
|
+
if (tx) browserDebugLog(tx, "stream complete (from cache)");
|
|
210
|
+
resolveStreamComplete();
|
|
211
|
+
},
|
|
212
|
+
signal,
|
|
213
|
+
);
|
|
214
|
+
});
|
|
215
|
+
} else if (inflightResponsePromise) {
|
|
216
|
+
if (tx) {
|
|
217
|
+
browserDebugLog(tx, "reusing inflight prefetch", { key: cacheKey });
|
|
218
|
+
}
|
|
219
|
+
responsePromise = inflightResponsePromise.then(async (response) => {
|
|
220
|
+
if (!response) {
|
|
221
|
+
if (tx) {
|
|
222
|
+
browserDebugLog(tx, "inflight prefetch unavailable, refetching");
|
|
125
223
|
}
|
|
126
|
-
|
|
127
|
-
signal?.removeEventListener("abort", onAbort);
|
|
128
|
-
reader.releaseLock();
|
|
129
|
-
console.log("[STREAMING] RSC stream complete");
|
|
130
|
-
resolveStreamComplete();
|
|
224
|
+
return doFreshFetch();
|
|
131
225
|
}
|
|
132
|
-
})();
|
|
133
226
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
227
|
+
const validated = validateRscHeaders(response, "inflight prefetch");
|
|
228
|
+
if (validated instanceof Promise) return validated;
|
|
229
|
+
|
|
230
|
+
return teeWithCompletion(
|
|
231
|
+
validated,
|
|
232
|
+
() => {
|
|
233
|
+
if (tx) {
|
|
234
|
+
browserDebugLog(tx, "stream complete (from inflight prefetch)");
|
|
235
|
+
}
|
|
236
|
+
resolveStreamComplete();
|
|
237
|
+
},
|
|
238
|
+
signal,
|
|
239
|
+
);
|
|
139
240
|
});
|
|
140
|
-
}
|
|
241
|
+
} else {
|
|
242
|
+
responsePromise = doFreshFetch();
|
|
243
|
+
}
|
|
141
244
|
|
|
142
245
|
try {
|
|
143
|
-
// Deserialize RSC payload
|
|
144
246
|
const payload = await deps.createFromFetch<RscPayload>(responsePromise);
|
|
247
|
+
|
|
248
|
+
if (tx) {
|
|
249
|
+
browserDebugLog(tx, "response received", {
|
|
250
|
+
isPartial: payload.metadata?.isPartial,
|
|
251
|
+
matchedCount: payload.metadata?.matched?.length ?? 0,
|
|
252
|
+
diffCount: payload.metadata?.diff?.length ?? 0,
|
|
253
|
+
});
|
|
254
|
+
}
|
|
145
255
|
return { payload, streamComplete };
|
|
146
256
|
} catch (error) {
|
|
147
257
|
// Convert network-level errors to NetworkError for proper handling
|
|
@@ -12,6 +12,7 @@ import type {
|
|
|
12
12
|
ActionStateListener,
|
|
13
13
|
HandleData,
|
|
14
14
|
} from "./types.js";
|
|
15
|
+
import { clearPrefetchCache } from "./prefetch/cache.js";
|
|
15
16
|
|
|
16
17
|
/**
|
|
17
18
|
* Default action state (idle with no payload)
|
|
@@ -27,9 +28,15 @@ const DEFAULT_ACTION_STATE: TrackedActionState = {
|
|
|
27
28
|
// Maximum number of history entries to cache (URLs visited)
|
|
28
29
|
const HISTORY_CACHE_SIZE = 20;
|
|
29
30
|
|
|
30
|
-
// Cache entry: [url-key, segments, stale, handleData?]
|
|
31
|
+
// Cache entry: [url-key, segments, stale, handleData?, routerId?]
|
|
31
32
|
// stale=true means the data may be outdated and should be revalidated on access
|
|
32
|
-
type HistoryCacheEntry = [
|
|
33
|
+
type HistoryCacheEntry = [
|
|
34
|
+
string,
|
|
35
|
+
ResolvedSegment[],
|
|
36
|
+
boolean,
|
|
37
|
+
HandleData?,
|
|
38
|
+
string?,
|
|
39
|
+
];
|
|
33
40
|
|
|
34
41
|
/**
|
|
35
42
|
* Shallow clone handleData to avoid reference sharing between cache entries.
|
|
@@ -88,7 +95,7 @@ export interface HistoryKeyOptions {
|
|
|
88
95
|
*/
|
|
89
96
|
export function generateHistoryKey(
|
|
90
97
|
url?: string,
|
|
91
|
-
options?: HistoryKeyOptions
|
|
98
|
+
options?: HistoryKeyOptions,
|
|
92
99
|
): string {
|
|
93
100
|
if (!url) {
|
|
94
101
|
url = typeof window !== "undefined" ? window.location.href : "/";
|
|
@@ -182,7 +189,7 @@ function createLocation(loc: { href: string }): NavigationLocation {
|
|
|
182
189
|
* ```
|
|
183
190
|
*/
|
|
184
191
|
export function createNavigationStore(
|
|
185
|
-
config?: NavigationStoreConfig
|
|
192
|
+
config?: NavigationStoreConfig,
|
|
186
193
|
): NavigationStore {
|
|
187
194
|
// Default location from window or config
|
|
188
195
|
const defaultLocation: NavigationLocation =
|
|
@@ -257,6 +264,11 @@ export function createNavigationStore(
|
|
|
257
264
|
// Used to maintain intercept context during action revalidation
|
|
258
265
|
let interceptSourceUrl: string | null = null;
|
|
259
266
|
|
|
267
|
+
// Router identity - tracks which router is currently active.
|
|
268
|
+
// When this changes on a partial response, the client forces a full
|
|
269
|
+
// tree replacement instead of reconciling with stale segments.
|
|
270
|
+
let currentRouterId: string | undefined;
|
|
271
|
+
|
|
260
272
|
// Action state tracking (for useAction hook)
|
|
261
273
|
// Maps action function ID to its tracked state
|
|
262
274
|
const actionStates = new Map<string, TrackedActionState>();
|
|
@@ -270,7 +282,7 @@ export function createNavigationStore(
|
|
|
270
282
|
*/
|
|
271
283
|
function createDebouncedNotifier<T extends (...args: any[]) => void>(
|
|
272
284
|
fn: T,
|
|
273
|
-
ms: number = 20
|
|
285
|
+
ms: number = 20,
|
|
274
286
|
): T {
|
|
275
287
|
let timeout: ReturnType<typeof setTimeout> | null = null;
|
|
276
288
|
return ((...args: Parameters<T>) => {
|
|
@@ -297,7 +309,7 @@ export function createNavigationStore(
|
|
|
297
309
|
setTimeout(() => {
|
|
298
310
|
timeouts.delete(key);
|
|
299
311
|
fn(key, ...args);
|
|
300
|
-
}, ms)
|
|
312
|
+
}, ms),
|
|
301
313
|
);
|
|
302
314
|
}) as T;
|
|
303
315
|
}
|
|
@@ -312,7 +324,7 @@ export function createNavigationStore(
|
|
|
312
324
|
if (listeners) {
|
|
313
325
|
listeners.forEach((listener) => listener(state));
|
|
314
326
|
}
|
|
315
|
-
}
|
|
327
|
+
},
|
|
316
328
|
);
|
|
317
329
|
|
|
318
330
|
/**
|
|
@@ -320,6 +332,7 @@ export function createNavigationStore(
|
|
|
320
332
|
*/
|
|
321
333
|
function clearCacheInternal(): void {
|
|
322
334
|
historyCache.length = 0;
|
|
335
|
+
clearPrefetchCache();
|
|
323
336
|
}
|
|
324
337
|
|
|
325
338
|
/**
|
|
@@ -329,13 +342,13 @@ export function createNavigationStore(
|
|
|
329
342
|
for (let i = 0; i < historyCache.length; i++) {
|
|
330
343
|
historyCache[i][2] = true;
|
|
331
344
|
}
|
|
345
|
+
clearPrefetchCache();
|
|
332
346
|
}
|
|
333
347
|
|
|
334
348
|
/**
|
|
335
349
|
* Clear the history cache and broadcast to other tabs
|
|
336
350
|
*/
|
|
337
351
|
function clearCacheAndBroadcast(): void {
|
|
338
|
-
console.log("[Browser] Clearing cache and broadcasting to other tabs");
|
|
339
352
|
clearCacheInternal();
|
|
340
353
|
broadcastInvalidation();
|
|
341
354
|
}
|
|
@@ -344,9 +357,6 @@ export function createNavigationStore(
|
|
|
344
357
|
* Mark cache as stale and broadcast to other tabs
|
|
345
358
|
*/
|
|
346
359
|
function markStaleAndBroadcast(): void {
|
|
347
|
-
console.log(
|
|
348
|
-
"[Browser] Marking cache as stale and broadcasting to other tabs"
|
|
349
|
-
);
|
|
350
360
|
markCacheAsStaleInternal();
|
|
351
361
|
broadcastInvalidation();
|
|
352
362
|
}
|
|
@@ -369,14 +379,6 @@ export function createNavigationStore(
|
|
|
369
379
|
path: currentPath,
|
|
370
380
|
segmentIds: currentSegmentIds,
|
|
371
381
|
});
|
|
372
|
-
console.log(
|
|
373
|
-
"[Browser] Broadcast sent for path:",
|
|
374
|
-
currentPath,
|
|
375
|
-
"segments:",
|
|
376
|
-
currentSegmentIds.join(", ")
|
|
377
|
-
);
|
|
378
|
-
} else {
|
|
379
|
-
console.warn("[Browser] No BroadcastChannel available");
|
|
380
382
|
}
|
|
381
383
|
}
|
|
382
384
|
|
|
@@ -393,7 +395,7 @@ export function createNavigationStore(
|
|
|
393
395
|
// Check for shared segments between tabs
|
|
394
396
|
// Routes sharing any segment (layout, loader, etc.) should invalidate together
|
|
395
397
|
const hasSharedSegment = mutatedSegmentIds.some((id) =>
|
|
396
|
-
currentSegmentIds.includes(id)
|
|
398
|
+
currentSegmentIds.includes(id),
|
|
397
399
|
);
|
|
398
400
|
|
|
399
401
|
if (!hasSharedSegment) {
|
|
@@ -401,34 +403,21 @@ export function createNavigationStore(
|
|
|
401
403
|
return;
|
|
402
404
|
}
|
|
403
405
|
|
|
404
|
-
console.log(
|
|
405
|
-
"[Browser] Cache marked stale by another tab, shared segments:",
|
|
406
|
-
mutatedSegmentIds
|
|
407
|
-
.filter((id) => currentSegmentIds.includes(id))
|
|
408
|
-
.join(", ")
|
|
409
|
-
);
|
|
410
406
|
markCacheAsStaleInternal();
|
|
411
407
|
|
|
412
408
|
// Auto-refresh if enabled and callback is registered
|
|
413
409
|
if (crossTabAutoRefresh && crossTabRefreshCallback) {
|
|
414
410
|
// If idle, refresh immediately. If loading, wait for idle then refresh.
|
|
415
411
|
if (navState.state === "idle") {
|
|
416
|
-
console.log("[Browser] Cross-tab refresh triggered (idle)");
|
|
417
412
|
crossTabRefreshCallback();
|
|
418
413
|
} else if (!pendingCrossTabRefresh) {
|
|
419
414
|
// Only queue one refresh, ignore subsequent events while loading
|
|
420
415
|
pendingCrossTabRefresh = true;
|
|
421
|
-
console.log(
|
|
422
|
-
"[Browser] Navigation in progress, deferring cross-tab refresh"
|
|
423
|
-
);
|
|
424
416
|
// Subscribe to state changes, refresh when idle
|
|
425
417
|
const listener: StateListener = () => {
|
|
426
418
|
if (navState.state === "idle") {
|
|
427
419
|
stateListeners.delete(listener);
|
|
428
420
|
pendingCrossTabRefresh = false;
|
|
429
|
-
console.log(
|
|
430
|
-
"[Browser] Cross-tab refresh triggered (deferred)"
|
|
431
|
-
);
|
|
432
421
|
crossTabRefreshCallback?.();
|
|
433
422
|
}
|
|
434
423
|
};
|
|
@@ -574,7 +563,7 @@ export function createNavigationStore(
|
|
|
574
563
|
cacheSegmentsForHistory(
|
|
575
564
|
historyKey: string,
|
|
576
565
|
segments: ResolvedSegment[],
|
|
577
|
-
handleData?: HandleData
|
|
566
|
+
handleData?: HandleData,
|
|
578
567
|
): void {
|
|
579
568
|
// Shallow clone handleData arrays to avoid reference sharing between cache entries
|
|
580
569
|
// We only clone the structure (objects and arrays), not the data items themselves,
|
|
@@ -585,13 +574,25 @@ export function createNavigationStore(
|
|
|
585
574
|
|
|
586
575
|
// Check if entry already exists and update it
|
|
587
576
|
const existingIndex = historyCache.findIndex(
|
|
588
|
-
([key]) => key === historyKey
|
|
577
|
+
([key]) => key === historyKey,
|
|
589
578
|
);
|
|
590
579
|
if (existingIndex !== -1) {
|
|
591
|
-
historyCache[existingIndex] = [
|
|
580
|
+
historyCache[existingIndex] = [
|
|
581
|
+
historyKey,
|
|
582
|
+
segments,
|
|
583
|
+
false,
|
|
584
|
+
clonedHandleData,
|
|
585
|
+
currentRouterId,
|
|
586
|
+
];
|
|
592
587
|
} else {
|
|
593
588
|
// Add new entry at the end (not stale)
|
|
594
|
-
historyCache.push([
|
|
589
|
+
historyCache.push([
|
|
590
|
+
historyKey,
|
|
591
|
+
segments,
|
|
592
|
+
false,
|
|
593
|
+
clonedHandleData,
|
|
594
|
+
currentRouterId,
|
|
595
|
+
]);
|
|
595
596
|
// Remove oldest entries if over limit
|
|
596
597
|
while (historyCache.length > cacheSize) {
|
|
597
598
|
historyCache.shift();
|
|
@@ -603,12 +604,22 @@ export function createNavigationStore(
|
|
|
603
604
|
* Get cached segments for a history entry
|
|
604
605
|
* Returns { segments, stale, handleData } or undefined if not cached
|
|
605
606
|
*/
|
|
606
|
-
getCachedSegments(
|
|
607
|
-
|
|
608
|
-
|
|
607
|
+
getCachedSegments(historyKey: string):
|
|
608
|
+
| {
|
|
609
|
+
segments: ResolvedSegment[];
|
|
610
|
+
stale: boolean;
|
|
611
|
+
handleData?: HandleData;
|
|
612
|
+
routerId?: string;
|
|
613
|
+
}
|
|
614
|
+
| undefined {
|
|
609
615
|
const entry = historyCache.find(([key]) => key === historyKey);
|
|
610
616
|
if (!entry) return undefined;
|
|
611
|
-
return {
|
|
617
|
+
return {
|
|
618
|
+
segments: entry[1],
|
|
619
|
+
stale: entry[2],
|
|
620
|
+
handleData: entry[3],
|
|
621
|
+
routerId: entry[4],
|
|
622
|
+
};
|
|
612
623
|
},
|
|
613
624
|
|
|
614
625
|
/**
|
|
@@ -625,13 +636,19 @@ export function createNavigationStore(
|
|
|
625
636
|
*/
|
|
626
637
|
updateCacheHandleData(historyKey: string, handleData: HandleData): void {
|
|
627
638
|
const existingIndex = historyCache.findIndex(
|
|
628
|
-
([key]) => key === historyKey
|
|
639
|
+
([key]) => key === historyKey,
|
|
629
640
|
);
|
|
630
641
|
if (existingIndex !== -1) {
|
|
631
642
|
const entry = historyCache[existingIndex];
|
|
632
643
|
// Shallow clone handleData arrays to avoid reference sharing
|
|
633
644
|
const clonedHandleData = cloneHandleData(handleData);
|
|
634
|
-
historyCache[existingIndex] = [
|
|
645
|
+
historyCache[existingIndex] = [
|
|
646
|
+
entry[0],
|
|
647
|
+
entry[1],
|
|
648
|
+
entry[2],
|
|
649
|
+
clonedHandleData,
|
|
650
|
+
entry[4], // preserve routerId
|
|
651
|
+
];
|
|
635
652
|
}
|
|
636
653
|
},
|
|
637
654
|
|
|
@@ -640,14 +657,7 @@ export function createNavigationStore(
|
|
|
640
657
|
* Called after server actions to indicate data may be outdated
|
|
641
658
|
*/
|
|
642
659
|
markCacheAsStale(): void {
|
|
643
|
-
|
|
644
|
-
historyCache[i][2] = true;
|
|
645
|
-
}
|
|
646
|
-
console.log(
|
|
647
|
-
"[Browser] Marked",
|
|
648
|
-
historyCache.length,
|
|
649
|
-
"cache entries as stale"
|
|
650
|
-
);
|
|
660
|
+
markCacheAsStaleInternal();
|
|
651
661
|
},
|
|
652
662
|
|
|
653
663
|
/**
|
|
@@ -704,6 +714,14 @@ export function createNavigationStore(
|
|
|
704
714
|
interceptSourceUrl = url;
|
|
705
715
|
},
|
|
706
716
|
|
|
717
|
+
getRouterId(): string | undefined {
|
|
718
|
+
return currentRouterId;
|
|
719
|
+
},
|
|
720
|
+
|
|
721
|
+
setRouterId(id: string): void {
|
|
722
|
+
currentRouterId = id;
|
|
723
|
+
},
|
|
724
|
+
|
|
707
725
|
// ========================================================================
|
|
708
726
|
// UI Update Notifications
|
|
709
727
|
// ========================================================================
|
|
@@ -745,7 +763,7 @@ export function createNavigationStore(
|
|
|
745
763
|
*/
|
|
746
764
|
setActionState(
|
|
747
765
|
actionId: string,
|
|
748
|
-
partial: Partial<TrackedActionState
|
|
766
|
+
partial: Partial<TrackedActionState>,
|
|
749
767
|
): void {
|
|
750
768
|
const current = actionStates.get(actionId) ?? { ...DEFAULT_ACTION_STATE };
|
|
751
769
|
const updated: TrackedActionState = {
|
|
@@ -763,7 +781,7 @@ export function createNavigationStore(
|
|
|
763
781
|
*/
|
|
764
782
|
subscribeToAction(
|
|
765
783
|
actionId: string,
|
|
766
|
-
listener: ActionStateListener
|
|
784
|
+
listener: ActionStateListener,
|
|
767
785
|
): () => void {
|
|
768
786
|
let listeners = actionListeners.get(actionId);
|
|
769
787
|
if (!listeners) {
|
|
@@ -793,7 +811,7 @@ let storeInstance: NavigationStore | null = null;
|
|
|
793
811
|
* Subsequent calls return the existing instance.
|
|
794
812
|
*/
|
|
795
813
|
export function initNavigationStore(
|
|
796
|
-
config?: NavigationStoreConfig
|
|
814
|
+
config?: NavigationStoreConfig,
|
|
797
815
|
): NavigationStore {
|
|
798
816
|
if (!storeInstance) {
|
|
799
817
|
storeInstance = createNavigationStore(config);
|
|
@@ -809,7 +827,7 @@ export function initNavigationStore(
|
|
|
809
827
|
export function getNavigationStore(): NavigationStore {
|
|
810
828
|
if (!storeInstance) {
|
|
811
829
|
throw new Error(
|
|
812
|
-
"Navigation store not initialized. Call initNavigationStore first."
|
|
830
|
+
"Navigation store not initialized. Call initNavigationStore first.",
|
|
813
831
|
);
|
|
814
832
|
}
|
|
815
833
|
return storeInstance;
|