@rangojs/router 0.0.0-experimental.3 → 0.0.0-experimental.30
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 +5 -0
- package/README.md +883 -4
- package/dist/bin/rango.js +1601 -0
- package/dist/vite/index.js +4655 -747
- package/package.json +78 -50
- package/skills/cache-guide/SKILL.md +262 -0
- package/skills/caching/SKILL.md +54 -25
- package/skills/composability/SKILL.md +172 -0
- package/skills/debug-manifest/SKILL.md +12 -8
- package/skills/document-cache/SKILL.md +23 -21
- package/skills/fonts/SKILL.md +167 -0
- package/skills/hooks/SKILL.md +390 -63
- package/skills/host-router/SKILL.md +218 -0
- package/skills/intercept/SKILL.md +133 -10
- package/skills/layout/SKILL.md +102 -5
- package/skills/links/SKILL.md +239 -0
- package/skills/loader/SKILL.md +366 -29
- package/skills/middleware/SKILL.md +173 -36
- package/skills/mime-routes/SKILL.md +128 -0
- package/skills/parallel/SKILL.md +80 -3
- package/skills/prerender/SKILL.md +643 -0
- package/skills/rango/SKILL.md +86 -16
- package/skills/response-routes/SKILL.md +411 -0
- package/skills/route/SKILL.md +227 -14
- package/skills/router-setup/SKILL.md +225 -32
- package/skills/tailwind/SKILL.md +129 -0
- package/skills/theme/SKILL.md +12 -11
- package/skills/typesafety/SKILL.md +401 -75
- package/skills/use-cache/SKILL.md +324 -0
- package/src/__internal.ts +10 -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/event-controller.ts +87 -64
- package/src/browser/history-state.ts +80 -0
- package/src/browser/intercept-utils.ts +52 -0
- package/src/browser/link-interceptor.ts +20 -4
- package/src/browser/logging.ts +55 -0
- package/src/browser/merge-segment-loaders.ts +20 -12
- package/src/browser/navigation-bridge.ts +201 -553
- package/src/browser/navigation-client.ts +124 -71
- package/src/browser/navigation-store.ts +33 -50
- package/src/browser/navigation-transaction.ts +295 -0
- package/src/browser/network-error-handler.ts +61 -0
- package/src/browser/partial-update.ts +267 -317
- package/src/browser/prefetch/cache.ts +146 -0
- package/src/browser/prefetch/fetch.ts +135 -0
- package/src/browser/prefetch/observer.ts +65 -0
- package/src/browser/prefetch/policy.ts +42 -0
- package/src/browser/prefetch/queue.ts +88 -0
- package/src/browser/rango-state.ts +112 -0
- package/src/browser/react/Link.tsx +173 -73
- package/src/browser/react/NavigationProvider.tsx +138 -27
- package/src/browser/react/context.ts +6 -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 +37 -0
- 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 +49 -65
- package/src/browser/react/use-href.tsx +20 -188
- package/src/browser/react/use-link-status.ts +6 -5
- package/src/browser/react/use-mount.ts +31 -0
- package/src/browser/react/use-navigation.ts +27 -78
- 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 +63 -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 +111 -26
- package/src/browser/scroll-restoration.ts +92 -16
- package/src/browser/segment-reconciler.ts +216 -0
- package/src/browser/segment-structure-assert.ts +83 -0
- package/src/browser/server-action-bridge.ts +504 -584
- package/src/browser/shallow.ts +6 -1
- package/src/browser/types.ts +92 -57
- package/src/browser/validate-redirect-origin.ts +29 -0
- package/src/build/generate-manifest.ts +438 -0
- package/src/build/generate-route-types.ts +36 -0
- package/src/build/index.ts +35 -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 +411 -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 +469 -0
- package/src/build/route-types/scan-filter.ts +78 -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 +338 -0
- package/src/cache/cache-scope.ts +120 -303
- package/src/cache/cf/cf-cache-store.ts +119 -7
- package/src/cache/cf/index.ts +8 -2
- package/src/cache/document-cache.ts +101 -72
- package/src/cache/handle-capture.ts +81 -0
- package/src/cache/handle-snapshot.ts +41 -0
- package/src/cache/index.ts +0 -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 +98 -0
- package/src/cache/types.ts +72 -122
- package/src/client.rsc.tsx +10 -15
- package/src/client.tsx +114 -135
- package/src/component-utils.ts +4 -4
- package/src/components/DefaultDocument.tsx +5 -1
- package/src/context-var.ts +86 -0
- package/src/debug.ts +17 -7
- package/src/errors.ts +108 -2
- package/src/handle.ts +34 -19
- package/src/handles/MetaTags.tsx +73 -20
- package/src/handles/meta.ts +30 -13
- package/src/host/cookie-handler.ts +165 -0
- package/src/host/errors.ts +97 -0
- package/src/host/index.ts +53 -0
- package/src/host/pattern-matcher.ts +214 -0
- package/src/host/router.ts +352 -0
- package/src/host/testing.ts +79 -0
- package/src/host/types.ts +146 -0
- package/src/host/utils.ts +25 -0
- package/src/href-client.ts +135 -49
- package/src/index.rsc.ts +182 -17
- package/src/index.ts +238 -24
- package/src/internal-debug.ts +11 -0
- package/src/loader.rsc.ts +27 -142
- 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 +185 -0
- package/src/prerender.ts +463 -0
- package/src/reverse.ts +330 -0
- package/src/root-error-boundary.tsx +41 -29
- package/src/route-content-wrapper.tsx +9 -11
- package/src/route-definition/dsl-helpers.ts +934 -0
- package/src/route-definition/helper-factories.ts +200 -0
- package/src/route-definition/helpers-types.ts +430 -0
- package/src/route-definition/index.ts +52 -0
- package/src/route-definition/redirect.ts +93 -0
- package/src/route-definition.ts +1 -1388
- package/src/route-map-builder.ts +241 -112
- package/src/route-name.ts +53 -0
- package/src/route-types.ts +70 -9
- package/src/router/content-negotiation.ts +116 -0
- package/src/router/debug-manifest.ts +72 -0
- package/src/router/error-handling.ts +9 -9
- package/src/router/find-match.ts +158 -0
- package/src/router/handler-context.ts +371 -81
- package/src/router/intercept-resolution.ts +395 -0
- package/src/router/lazy-includes.ts +234 -0
- package/src/router/loader-resolution.ts +215 -122
- package/src/router/logging.ts +248 -0
- package/src/router/manifest.ts +155 -32
- package/src/router/match-api.ts +620 -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 +80 -93
- package/src/router/match-middleware/cache-lookup.ts +382 -9
- package/src/router/match-middleware/cache-store.ts +51 -22
- package/src/router/match-middleware/intercept-resolution.ts +55 -17
- package/src/router/match-middleware/segment-resolution.ts +24 -6
- package/src/router/match-pipelines.ts +10 -45
- package/src/router/match-result.ts +34 -29
- package/src/router/metrics.ts +235 -15
- package/src/router/middleware-cookies.ts +55 -0
- package/src/router/middleware-types.ts +222 -0
- package/src/router/middleware.ts +324 -367
- package/src/router/pattern-matching.ts +321 -30
- package/src/router/prerender-match.ts +400 -0
- package/src/router/preview-match.ts +170 -0
- package/src/router/revalidation.ts +137 -38
- package/src/router/router-context.ts +36 -21
- package/src/router/router-interfaces.ts +452 -0
- package/src/router/router-options.ts +592 -0
- package/src/router/router-registry.ts +24 -0
- package/src/router/segment-resolution/fresh.ts +570 -0
- package/src/router/segment-resolution/helpers.ts +263 -0
- package/src/router/segment-resolution/loader-cache.ts +198 -0
- package/src/router/segment-resolution/revalidation.ts +1241 -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 +289 -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 +77 -3
- package/src/router.ts +688 -3656
- package/src/rsc/handler-context.ts +45 -0
- package/src/rsc/handler.ts +786 -760
- package/src/rsc/helpers.ts +140 -6
- package/src/rsc/index.ts +5 -25
- package/src/rsc/loader-fetch.ts +209 -0
- package/src/rsc/manifest-init.ts +86 -0
- package/src/rsc/nonce.ts +14 -0
- package/src/rsc/origin-guard.ts +141 -0
- package/src/rsc/progressive-enhancement.ts +379 -0
- package/src/rsc/response-error.ts +37 -0
- package/src/rsc/response-route-handler.ts +347 -0
- package/src/rsc/rsc-rendering.ts +235 -0
- package/src/rsc/runtime-warnings.ts +42 -0
- package/src/rsc/server-action.ts +348 -0
- package/src/rsc/ssr-setup.ts +128 -0
- package/src/rsc/types.ts +40 -14
- package/src/search-params.ts +230 -0
- package/src/segment-system.tsx +57 -61
- package/src/server/context.ts +202 -51
- package/src/server/cookie-store.ts +190 -0
- package/src/server/fetchable-loader-store.ts +37 -0
- package/src/server/handle-store.ts +94 -15
- package/src/server/loader-registry.ts +15 -56
- package/src/server/request-context.ts +422 -70
- package/src/server.ts +36 -120
- package/src/ssr/index.tsx +157 -26
- package/src/static-handler.ts +114 -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 +687 -0
- package/src/types/index.ts +88 -0
- package/src/types/loader-types.ts +183 -0
- package/src/types/route-config.ts +170 -0
- package/src/types/route-entry.ts +102 -0
- package/src/types/segments.ts +148 -0
- package/src/types.ts +1 -1577
- package/src/urls/include-helper.ts +197 -0
- package/src/urls/index.ts +53 -0
- package/src/urls/path-helper-types.ts +339 -0
- package/src/urls/path-helper.ts +329 -0
- package/src/urls/pattern-types.ts +95 -0
- package/src/urls/response-types.ts +106 -0
- package/src/urls/type-extraction.ts +372 -0
- package/src/urls/urls-function.ts +98 -0
- package/src/urls.ts +1 -726
- package/src/use-loader.tsx +85 -77
- package/src/vite/discovery/bundle-postprocess.ts +184 -0
- package/src/vite/discovery/discover-routers.ts +344 -0
- package/src/vite/discovery/prerender-collection.ts +385 -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 +110 -0
- package/src/vite/discovery/virtual-module-codegen.ts +203 -0
- package/src/vite/index.ts +11 -782
- package/src/vite/plugin-types.ts +131 -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 -51
- package/src/vite/plugins/expose-id-utils.ts +287 -0
- package/src/vite/plugins/expose-ids/export-analysis.ts +296 -0
- package/src/vite/plugins/expose-ids/handler-transform.ts +179 -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 +569 -0
- package/src/vite/plugins/refresh-cmd.ts +65 -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 +254 -0
- package/src/vite/{virtual-entries.ts → plugins/virtual-entries.ts} +29 -15
- package/src/vite/plugins/virtual-stub-plugin.ts +29 -0
- package/src/vite/rango.ts +510 -0
- package/src/vite/router-discovery.ts +785 -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 +189 -0
- package/src/vite/utils/shared-utils.ts +169 -0
- package/CLAUDE.md +0 -3
- 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/vite/expose-handle-id.ts +0 -209
- package/src/vite/expose-loader-id.ts +0 -357
- package/src/vite/expose-location-state-id.ts +0 -177
- /package/src/vite/{version.d.ts → plugins/version.d.ts} +0 -0
|
@@ -1,442 +1,45 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
NavigationBridge,
|
|
3
3
|
NavigationBridgeConfig,
|
|
4
|
-
|
|
5
|
-
NavigationStore,
|
|
4
|
+
NavigateOptionsInternal,
|
|
6
5
|
ResolvedSegment,
|
|
7
6
|
} from "./types.js";
|
|
7
|
+
import * as React from "react";
|
|
8
|
+
import { startTransition } from "react";
|
|
8
9
|
import {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
} from "./
|
|
10
|
+
createNavigationTransaction,
|
|
11
|
+
resolveNavigationState,
|
|
12
|
+
} from "./navigation-transaction.js";
|
|
12
13
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
function isTypedLocationState(
|
|
17
|
-
state: unknown
|
|
18
|
-
): state is Record<string, unknown> {
|
|
19
|
-
if (state === null || typeof state !== "object") return false;
|
|
20
|
-
return Object.keys(state).some((key) => key.startsWith("__rsc_ls_"));
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Resolve navigation state - handles both LocationStateEntry[] and legacy formats
|
|
25
|
-
*/
|
|
26
|
-
function resolveNavigationState(state: unknown): unknown {
|
|
27
|
-
// Check if it's an array of LocationStateEntry
|
|
28
|
-
if (
|
|
29
|
-
Array.isArray(state) &&
|
|
30
|
-
state.length > 0 &&
|
|
31
|
-
isLocationStateEntry(state[0])
|
|
32
|
-
) {
|
|
33
|
-
return resolveLocationStateEntries(state);
|
|
34
|
-
}
|
|
35
|
-
// Return as-is for legacy formats
|
|
36
|
-
return state;
|
|
37
|
-
}
|
|
14
|
+
// addTransitionType is only available in React experimental
|
|
15
|
+
const addTransitionType: ((type: string) => void) | undefined =
|
|
16
|
+
"addTransitionType" in React ? (React as any).addTransitionType : undefined;
|
|
38
17
|
|
|
39
|
-
/**
|
|
40
|
-
* Build history state object from user state
|
|
41
|
-
* - Typed state: spread directly into history.state
|
|
42
|
-
* - Legacy state: store in history.state.state
|
|
43
|
-
*/
|
|
44
|
-
function buildHistoryState(
|
|
45
|
-
userState: unknown,
|
|
46
|
-
routerState?: { intercept?: boolean; sourceUrl?: string }
|
|
47
|
-
): Record<string, unknown> | null {
|
|
48
|
-
const result: Record<string, unknown> = {};
|
|
49
|
-
|
|
50
|
-
// Add router internal state
|
|
51
|
-
if (routerState?.intercept) {
|
|
52
|
-
result.intercept = true;
|
|
53
|
-
if (routerState.sourceUrl) {
|
|
54
|
-
result.sourceUrl = routerState.sourceUrl;
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// Add user state
|
|
59
|
-
if (userState !== undefined) {
|
|
60
|
-
if (isTypedLocationState(userState)) {
|
|
61
|
-
// Typed state: spread directly
|
|
62
|
-
Object.assign(result, userState);
|
|
63
|
-
} else {
|
|
64
|
-
// Legacy state: store in .state
|
|
65
|
-
result.state = userState;
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
return Object.keys(result).length > 0 ? result : null;
|
|
70
|
-
}
|
|
71
18
|
import { setupLinkInterception } from "./link-interceptor.js";
|
|
72
19
|
import { createPartialUpdater } from "./partial-update.js";
|
|
73
20
|
import { generateHistoryKey } from "./navigation-store.js";
|
|
21
|
+
import { handleNavigationEnd } from "./scroll-restoration.js";
|
|
22
|
+
import type { EventController } from "./event-controller.js";
|
|
23
|
+
import { isInterceptOnlyCache } from "./intercept-utils.js";
|
|
74
24
|
import {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
} from "./
|
|
79
|
-
import
|
|
80
|
-
import {
|
|
81
|
-
import {
|
|
82
|
-
import { createElement, startTransition } from "react";
|
|
25
|
+
toNetworkError,
|
|
26
|
+
emitNetworkError,
|
|
27
|
+
isBackgroundSuppressible,
|
|
28
|
+
} from "./network-error-handler.js";
|
|
29
|
+
import { debugLog } from "./logging.js";
|
|
30
|
+
import { ServerRedirect } from "../errors.js";
|
|
31
|
+
import { validateRedirectOrigin } from "./validate-redirect-origin.js";
|
|
83
32
|
|
|
84
33
|
// Polyfill Symbol.dispose for Safari and older browsers
|
|
85
34
|
if (typeof Symbol.dispose === "undefined") {
|
|
86
35
|
(Symbol as any).dispose = Symbol("Symbol.dispose");
|
|
87
36
|
}
|
|
88
37
|
|
|
89
|
-
/**
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
*/
|
|
93
|
-
function isInterceptSegment(s: ResolvedSegment): boolean {
|
|
94
|
-
return (
|
|
95
|
-
s.namespace?.startsWith("intercept:") ||
|
|
96
|
-
(s.type === "parallel" && s.id.includes(".@"))
|
|
97
|
-
);
|
|
38
|
+
/** Get IDs of non-loader segments (layouts, routes, parallels). */
|
|
39
|
+
function getNonLoaderSegmentIds(segments: ResolvedSegment[]): string[] {
|
|
40
|
+
return segments.filter((s) => s.type !== "loader").map((s) => s.id);
|
|
98
41
|
}
|
|
99
42
|
|
|
100
|
-
/**
|
|
101
|
-
* Check if cached segments are intercept-only (no main route segments)
|
|
102
|
-
* Intercept responses shouldn't be used for optimistic rendering since
|
|
103
|
-
* whether interception happens depends on the current page context
|
|
104
|
-
*/
|
|
105
|
-
function isInterceptOnlyCache(segments: ResolvedSegment[]): boolean {
|
|
106
|
-
return segments.some(isInterceptSegment);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
/**
|
|
110
|
-
* Options for committing a navigation transaction
|
|
111
|
-
*/
|
|
112
|
-
interface CommitOptions {
|
|
113
|
-
url: string;
|
|
114
|
-
segmentIds: string[];
|
|
115
|
-
segments: ResolvedSegment[];
|
|
116
|
-
replace?: boolean;
|
|
117
|
-
scroll?: boolean;
|
|
118
|
-
/** User-provided state to store in history.state */
|
|
119
|
-
state?: unknown;
|
|
120
|
-
/** If true, only update store without changing URL/history (for server actions) */
|
|
121
|
-
storeOnly?: boolean;
|
|
122
|
-
/** If true, this is an intercept route - store in history.state for popstate handling */
|
|
123
|
-
intercept?: boolean;
|
|
124
|
-
/** Source URL where the intercept was triggered from (stored in history.state) */
|
|
125
|
-
interceptSourceUrl?: string;
|
|
126
|
-
/** If true, only update cache without touching store or history (for background stale revalidation) */
|
|
127
|
-
cacheOnly?: boolean;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
/**
|
|
131
|
-
* Options that can override the pre-configured commit settings
|
|
132
|
-
*/
|
|
133
|
-
interface BoundCommitOverrides {
|
|
134
|
-
/** Override scroll behavior (e.g., disable for intercepts) */
|
|
135
|
-
scroll?: boolean;
|
|
136
|
-
/** Override replace behavior (e.g., force replace for intercepts) */
|
|
137
|
-
replace?: boolean;
|
|
138
|
-
/** Override user-provided state */
|
|
139
|
-
state?: unknown;
|
|
140
|
-
/** Mark this as an intercept route */
|
|
141
|
-
intercept?: boolean;
|
|
142
|
-
/** Source URL where intercept was triggered from */
|
|
143
|
-
interceptSourceUrl?: string;
|
|
144
|
-
/** If true, only update cache (for stale revalidation) */
|
|
145
|
-
cacheOnly?: boolean;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
/**
|
|
149
|
-
* Token for tracking an active stream - call end() when stream completes
|
|
150
|
-
*/
|
|
151
|
-
export interface StreamingToken {
|
|
152
|
-
end(): void;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
/**
|
|
156
|
-
* Bound transaction with pre-configured commit options (without segmentIds/segments)
|
|
157
|
-
*/
|
|
158
|
-
export interface BoundTransaction {
|
|
159
|
-
readonly currentUrl: string;
|
|
160
|
-
/** Start streaming and get a token to end it when the stream completes */
|
|
161
|
-
startStreaming(): StreamingToken;
|
|
162
|
-
commit(
|
|
163
|
-
segmentIds: string[],
|
|
164
|
-
segments: ResolvedSegment[],
|
|
165
|
-
overrides?: BoundCommitOverrides
|
|
166
|
-
): void;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
/**
|
|
170
|
-
* Navigation transaction for managing state during navigation
|
|
171
|
-
* Uses the event controller handle for lifecycle management
|
|
172
|
-
*/
|
|
173
|
-
interface NavigationTransaction extends Disposable {
|
|
174
|
-
/** Optimistically commit from cache - instant render before revalidation */
|
|
175
|
-
optimisticCommit(options: CommitOptions): void;
|
|
176
|
-
/** Final commit with server data (or reconciliation after optimistic) */
|
|
177
|
-
commit(options: CommitOptions): void;
|
|
178
|
-
with(
|
|
179
|
-
options: Omit<CommitOptions, "segmentIds" | "segments">
|
|
180
|
-
): BoundTransaction;
|
|
181
|
-
/** The navigation handle from the event controller */
|
|
182
|
-
handle: NavigationHandle;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
/**
|
|
186
|
-
* Creates a navigation transaction that coordinates with the event controller.
|
|
187
|
-
* Handles loading state transitions and cleanup on completion/abort.
|
|
188
|
-
*
|
|
189
|
-
* Supports optimistic navigation: render from cache immediately,
|
|
190
|
-
* then revalidate in background and reconcile if data changed.
|
|
191
|
-
*/
|
|
192
|
-
function createNavigationTransaction(
|
|
193
|
-
store: NavigationStore,
|
|
194
|
-
eventController: EventController,
|
|
195
|
-
url: string,
|
|
196
|
-
options?: NavigateOptions & { skipLoadingState?: boolean }
|
|
197
|
-
): NavigationTransaction {
|
|
198
|
-
let committed = false;
|
|
199
|
-
let optimisticallyCommitted = false;
|
|
200
|
-
let earlyStatePushed = false;
|
|
201
|
-
const currentUrl = window.location.href;
|
|
202
|
-
|
|
203
|
-
// Start navigation in event controller (this sets loading state)
|
|
204
|
-
const handle = eventController.startNavigation(url, options);
|
|
205
|
-
|
|
206
|
-
// If state is provided, push it to history immediately so loading UI can access it
|
|
207
|
-
// This enables "optimistic state" - showing product names in skeletons etc.
|
|
208
|
-
if (options?.state !== undefined && !options?.replace) {
|
|
209
|
-
const earlyHistoryState = buildHistoryState(options.state);
|
|
210
|
-
window.history.pushState(earlyHistoryState, "", url);
|
|
211
|
-
earlyStatePushed = true;
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
/**
|
|
215
|
-
* Optimistically commit from cache - renders immediately before revalidation
|
|
216
|
-
* Sets optimisticallyCommitted flag so final commit() knows to reconcile
|
|
217
|
-
*/
|
|
218
|
-
function optimisticCommit(opts: CommitOptions): void {
|
|
219
|
-
optimisticallyCommitted = true;
|
|
220
|
-
|
|
221
|
-
const { url, segmentIds, segments, replace, scroll } = opts;
|
|
222
|
-
const parsedUrl = new URL(url, window.location.origin);
|
|
223
|
-
|
|
224
|
-
// Save current scroll position before navigating
|
|
225
|
-
handleNavigationStart();
|
|
226
|
-
|
|
227
|
-
// Update segment state
|
|
228
|
-
store.setSegmentIds(segmentIds);
|
|
229
|
-
store.setCurrentUrl(url);
|
|
230
|
-
store.setPath(parsedUrl.pathname);
|
|
231
|
-
|
|
232
|
-
// Generate history key from URL
|
|
233
|
-
const historyKey = generateHistoryKey(url);
|
|
234
|
-
store.setHistoryKey(historyKey);
|
|
235
|
-
|
|
236
|
-
// Cache segments with current handleData (will be overwritten by fresh data on final commit)
|
|
237
|
-
const currentHandleData = eventController.getHandleState().data;
|
|
238
|
-
store.cacheSegmentsForHistory(historyKey, segments, currentHandleData);
|
|
239
|
-
|
|
240
|
-
// Build history state with user state if provided
|
|
241
|
-
const historyState = buildHistoryState(opts.state);
|
|
242
|
-
|
|
243
|
-
// Update browser URL
|
|
244
|
-
// Use replaceState if we already pushed early (for optimistic state access)
|
|
245
|
-
if (replace || earlyStatePushed) {
|
|
246
|
-
window.history.replaceState(historyState, "", url);
|
|
247
|
-
} else {
|
|
248
|
-
window.history.pushState(historyState, "", url);
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
// Ensure new history entry has a scroll restoration key
|
|
252
|
-
ensureHistoryKey();
|
|
253
|
-
|
|
254
|
-
// Complete the navigation in event controller (sets idle state)
|
|
255
|
-
handle.complete(parsedUrl);
|
|
256
|
-
|
|
257
|
-
// Handle scroll after navigation
|
|
258
|
-
handleNavigationEnd({ scroll });
|
|
259
|
-
|
|
260
|
-
console.log(
|
|
261
|
-
"[Browser] Optimistic commit from cache, historyKey:",
|
|
262
|
-
historyKey
|
|
263
|
-
);
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
/**
|
|
267
|
-
* Commit the navigation - updates store and URL atomically
|
|
268
|
-
* If optimisticCommit was called, this becomes a reconciliation
|
|
269
|
-
*/
|
|
270
|
-
function commit(opts: CommitOptions): void {
|
|
271
|
-
committed = true;
|
|
272
|
-
|
|
273
|
-
// If optimistic commit already done, adjust options for reconciliation
|
|
274
|
-
const isReconciliation = optimisticallyCommitted;
|
|
275
|
-
const {
|
|
276
|
-
url,
|
|
277
|
-
segmentIds,
|
|
278
|
-
segments,
|
|
279
|
-
storeOnly,
|
|
280
|
-
intercept,
|
|
281
|
-
interceptSourceUrl,
|
|
282
|
-
cacheOnly,
|
|
283
|
-
} = opts;
|
|
284
|
-
// For reconciliation: always replace (URL already pushed), no scroll
|
|
285
|
-
const replace = isReconciliation ? true : opts.replace;
|
|
286
|
-
const scroll = isReconciliation ? false : opts.scroll;
|
|
287
|
-
|
|
288
|
-
const parsedUrl = new URL(url, window.location.origin);
|
|
289
|
-
|
|
290
|
-
// Generate history key from URL (with intercept suffix for separate caching)
|
|
291
|
-
const historyKey = generateHistoryKey(url, { intercept });
|
|
292
|
-
|
|
293
|
-
// For cache-only commits (stale revalidation), only update cache and return
|
|
294
|
-
// Don't touch store state or history - user may have navigated elsewhere
|
|
295
|
-
if (cacheOnly) {
|
|
296
|
-
const currentHandleData = eventController.getHandleState().data;
|
|
297
|
-
store.cacheSegmentsForHistory(historyKey, segments, currentHandleData);
|
|
298
|
-
console.log("[Browser] Cache-only commit, historyKey:", historyKey);
|
|
299
|
-
return;
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
// Save current scroll position before navigating (only for non-reconciliation)
|
|
303
|
-
if (!isReconciliation) {
|
|
304
|
-
handleNavigationStart();
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
// Update segment state atomically
|
|
308
|
-
store.setSegmentIds(segmentIds);
|
|
309
|
-
store.setCurrentUrl(url);
|
|
310
|
-
store.setPath(parsedUrl.pathname);
|
|
311
|
-
|
|
312
|
-
store.setHistoryKey(historyKey);
|
|
313
|
-
|
|
314
|
-
// Cache segments with current handleData for this history entry (fresh data overwrites optimistic)
|
|
315
|
-
const currentHandleData = eventController.getHandleState().data;
|
|
316
|
-
store.cacheSegmentsForHistory(historyKey, segments, currentHandleData);
|
|
317
|
-
|
|
318
|
-
// For server actions, skip URL/history updates but still complete navigation
|
|
319
|
-
if (storeOnly) {
|
|
320
|
-
console.log("[Browser] Store updated (action)");
|
|
321
|
-
// Complete navigation to clear loading state
|
|
322
|
-
handle.complete(parsedUrl);
|
|
323
|
-
return;
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
// Build history state - include user state and intercept info for popstate handling
|
|
327
|
-
const historyState = buildHistoryState(opts.state, {
|
|
328
|
-
intercept,
|
|
329
|
-
sourceUrl: interceptSourceUrl,
|
|
330
|
-
});
|
|
331
|
-
|
|
332
|
-
// Update browser URL (skip if reconciliation - already done in optimisticCommit)
|
|
333
|
-
if (!isReconciliation) {
|
|
334
|
-
// Use replaceState if we already pushed early (for optimistic state access) or replace requested
|
|
335
|
-
if (replace || earlyStatePushed) {
|
|
336
|
-
window.history.replaceState(historyState, "", url);
|
|
337
|
-
} else {
|
|
338
|
-
window.history.pushState(historyState, "", url);
|
|
339
|
-
}
|
|
340
|
-
// Ensure new history entry has a scroll restoration key
|
|
341
|
-
ensureHistoryKey();
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
// Complete the navigation in event controller (sets idle state, updates location)
|
|
345
|
-
handle.complete(parsedUrl);
|
|
346
|
-
|
|
347
|
-
// Handle scroll after navigation (skip if reconciliation)
|
|
348
|
-
if (!isReconciliation) {
|
|
349
|
-
handleNavigationEnd({ scroll });
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
if (isReconciliation) {
|
|
353
|
-
console.log("[Browser] Reconciliation commit, historyKey:", historyKey);
|
|
354
|
-
} else {
|
|
355
|
-
console.log(
|
|
356
|
-
"[Browser] Navigation committed, historyKey:",
|
|
357
|
-
historyKey,
|
|
358
|
-
intercept ? "(intercept)" : ""
|
|
359
|
-
);
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
return {
|
|
364
|
-
handle,
|
|
365
|
-
optimisticCommit,
|
|
366
|
-
commit,
|
|
367
|
-
|
|
368
|
-
/**
|
|
369
|
-
* Create a bound transaction with pre-configured URL options
|
|
370
|
-
* segmentIds and segments provided at commit time (after they're resolved)
|
|
371
|
-
*/
|
|
372
|
-
with(
|
|
373
|
-
opts: Omit<CommitOptions, "segmentIds" | "segments">
|
|
374
|
-
): BoundTransaction {
|
|
375
|
-
return {
|
|
376
|
-
get currentUrl() {
|
|
377
|
-
return currentUrl;
|
|
378
|
-
},
|
|
379
|
-
startStreaming() {
|
|
380
|
-
return handle.startStreaming();
|
|
381
|
-
},
|
|
382
|
-
commit: (
|
|
383
|
-
segmentIds: string[],
|
|
384
|
-
segments: ResolvedSegment[],
|
|
385
|
-
overrides?: BoundCommitOverrides
|
|
386
|
-
) => {
|
|
387
|
-
// Allow overrides to disable scroll (e.g., for intercepts)
|
|
388
|
-
const finalScroll =
|
|
389
|
-
overrides?.scroll !== undefined ? overrides.scroll : opts.scroll;
|
|
390
|
-
// Allow overrides to force replace (e.g., for intercepts)
|
|
391
|
-
const finalReplace =
|
|
392
|
-
overrides?.replace !== undefined ? overrides.replace : opts.replace;
|
|
393
|
-
// Intercept info: overrides take precedence, fallback to opts
|
|
394
|
-
const intercept =
|
|
395
|
-
overrides?.intercept !== undefined
|
|
396
|
-
? overrides.intercept
|
|
397
|
-
: opts.intercept;
|
|
398
|
-
const interceptSourceUrl =
|
|
399
|
-
overrides?.interceptSourceUrl !== undefined
|
|
400
|
-
? overrides.interceptSourceUrl
|
|
401
|
-
: opts.interceptSourceUrl;
|
|
402
|
-
// Cache-only mode: overrides take precedence, fallback to opts
|
|
403
|
-
const cacheOnly =
|
|
404
|
-
overrides?.cacheOnly !== undefined
|
|
405
|
-
? overrides.cacheOnly
|
|
406
|
-
: opts.cacheOnly;
|
|
407
|
-
// User state: overrides take precedence, fallback to opts
|
|
408
|
-
const state =
|
|
409
|
-
overrides?.state !== undefined ? overrides.state : opts.state;
|
|
410
|
-
commit({
|
|
411
|
-
...opts,
|
|
412
|
-
segmentIds,
|
|
413
|
-
segments,
|
|
414
|
-
scroll: finalScroll,
|
|
415
|
-
replace: finalReplace,
|
|
416
|
-
state,
|
|
417
|
-
intercept,
|
|
418
|
-
interceptSourceUrl,
|
|
419
|
-
cacheOnly,
|
|
420
|
-
});
|
|
421
|
-
},
|
|
422
|
-
};
|
|
423
|
-
},
|
|
424
|
-
|
|
425
|
-
[Symbol.dispose]() {
|
|
426
|
-
// If aborted, another navigation took over - don't touch state
|
|
427
|
-
if (handle.signal.aborted) return;
|
|
428
|
-
|
|
429
|
-
// If not committed (and not optimistically committed), the handle's dispose
|
|
430
|
-
// will reset state to idle via the event controller
|
|
431
|
-
if (!committed && !optimisticallyCommitted) {
|
|
432
|
-
handle[Symbol.dispose]();
|
|
433
|
-
// The NavigationHandle's [Symbol.dispose] handles this
|
|
434
|
-
}
|
|
435
|
-
},
|
|
436
|
-
};
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
// Export for use by server-action-bridge
|
|
440
43
|
export { createNavigationTransaction };
|
|
441
44
|
|
|
442
45
|
/**
|
|
@@ -462,9 +65,10 @@ export interface NavigationBridgeConfigWithController extends NavigationBridgeCo
|
|
|
462
65
|
* @returns NavigationBridge instance
|
|
463
66
|
*/
|
|
464
67
|
export function createNavigationBridge(
|
|
465
|
-
config: NavigationBridgeConfigWithController
|
|
68
|
+
config: NavigationBridgeConfigWithController,
|
|
466
69
|
): NavigationBridge {
|
|
467
|
-
const { store, client, eventController, onUpdate, renderSegments, version } =
|
|
70
|
+
const { store, client, eventController, onUpdate, renderSegments, version } =
|
|
71
|
+
config;
|
|
468
72
|
|
|
469
73
|
// Create shared partial updater
|
|
470
74
|
const fetchPartialUpdate = createPartialUpdater({
|
|
@@ -478,19 +82,42 @@ export function createNavigationBridge(
|
|
|
478
82
|
return {
|
|
479
83
|
/**
|
|
480
84
|
* Navigate to a URL
|
|
481
|
-
* Uses
|
|
85
|
+
* Uses cached segments for SWR revalidation when available
|
|
482
86
|
*/
|
|
483
|
-
async navigate(
|
|
87
|
+
async navigate(
|
|
88
|
+
url: string,
|
|
89
|
+
options?: NavigateOptionsInternal,
|
|
90
|
+
): Promise<void> {
|
|
484
91
|
// Resolve LocationStateEntry[] to flat object if needed
|
|
485
92
|
const resolvedState =
|
|
486
93
|
options?.state !== undefined
|
|
487
94
|
? resolveNavigationState(options.state)
|
|
488
95
|
: undefined;
|
|
489
96
|
|
|
97
|
+
// Cross-origin URLs are not handled by SPA navigation.
|
|
98
|
+
// Fall back to a full browser navigation for http/https only.
|
|
99
|
+
let targetUrl: URL;
|
|
100
|
+
try {
|
|
101
|
+
targetUrl = new URL(url, window.location.origin);
|
|
102
|
+
} catch {
|
|
103
|
+
console.warn(`[rango] navigate() ignored: malformed URL "${url}"`);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
if (targetUrl.origin !== window.location.origin) {
|
|
107
|
+
if (targetUrl.protocol !== "http:" && targetUrl.protocol !== "https:") {
|
|
108
|
+
console.error(
|
|
109
|
+
`[rango] navigate() blocked: unsupported scheme "${targetUrl.protocol}"`,
|
|
110
|
+
);
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
window.location.href = targetUrl.href;
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
490
117
|
// Only abort pending requests when navigating to a different route
|
|
491
118
|
// Same-route navigation (e.g., /todos -> /todos) should not cancel in-flight actions
|
|
492
119
|
const currentPath = new URL(window.location.href).pathname;
|
|
493
|
-
const targetPath =
|
|
120
|
+
const targetPath = targetUrl.pathname;
|
|
494
121
|
if (currentPath !== targetPath) {
|
|
495
122
|
eventController.abortNavigation();
|
|
496
123
|
}
|
|
@@ -503,7 +130,9 @@ export function createNavigationBridge(
|
|
|
503
130
|
const isLeavingIntercept = isCurrentlyIntercept && isSamePathNavigation;
|
|
504
131
|
|
|
505
132
|
if (isLeavingIntercept) {
|
|
506
|
-
|
|
133
|
+
debugLog(
|
|
134
|
+
"[Browser] Leaving intercept - same URL navigation from intercept",
|
|
135
|
+
);
|
|
507
136
|
// Clear intercept source URL to ensure server doesn't treat this as intercept
|
|
508
137
|
store.setInterceptSourceUrl(null);
|
|
509
138
|
}
|
|
@@ -517,7 +146,7 @@ export function createNavigationBridge(
|
|
|
517
146
|
store.cacheSegmentsForHistory(
|
|
518
147
|
sourceHistoryKey,
|
|
519
148
|
sourceCached.segments,
|
|
520
|
-
currentHandleData
|
|
149
|
+
currentHandleData,
|
|
521
150
|
);
|
|
522
151
|
}
|
|
523
152
|
|
|
@@ -532,7 +161,7 @@ export function createNavigationBridge(
|
|
|
532
161
|
const cachedHandleData = cached?.handleData;
|
|
533
162
|
if (cachedSegments && sourceCached?.segments) {
|
|
534
163
|
const sourceSegmentMap = new Map(
|
|
535
|
-
sourceCached.segments.map((s) => [s.id, s])
|
|
164
|
+
sourceCached.segments.map((s) => [s.id, s]),
|
|
536
165
|
);
|
|
537
166
|
cachedSegments = cachedSegments.map((targetSeg) => {
|
|
538
167
|
const sourceSeg = sourceSegmentMap.get(targetSeg.id);
|
|
@@ -547,18 +176,20 @@ export function createNavigationBridge(
|
|
|
547
176
|
const interceptHistoryKey = generateHistoryKey(url, { intercept: true });
|
|
548
177
|
const hasInterceptCache = store.hasHistoryCache(interceptHistoryKey);
|
|
549
178
|
|
|
550
|
-
// Skip
|
|
179
|
+
// Skip cached SWR for:
|
|
551
180
|
// 1. intercept caches - interception depends on source page context
|
|
552
181
|
// 2. routes that CAN be intercepted - we don't know if this navigation will intercept
|
|
553
182
|
// 3. when leaving intercept - we need fresh non-intercept segments from server
|
|
183
|
+
// 4. redirect-with-state - force re-render so hooks read fresh state
|
|
554
184
|
const hasUsableCache =
|
|
555
185
|
cachedSegments &&
|
|
556
186
|
cachedSegments.length > 0 &&
|
|
557
187
|
!isInterceptOnlyCache(cachedSegments) &&
|
|
558
188
|
!hasInterceptCache &&
|
|
559
|
-
!isLeavingIntercept
|
|
189
|
+
!isLeavingIntercept &&
|
|
190
|
+
!options?._skipCache;
|
|
560
191
|
|
|
561
|
-
|
|
192
|
+
const tx = createNavigationTransaction(store, eventController, url, {
|
|
562
193
|
...options,
|
|
563
194
|
state: resolvedState,
|
|
564
195
|
skipLoadingState: hasUsableCache,
|
|
@@ -568,7 +199,11 @@ export function createNavigationBridge(
|
|
|
568
199
|
try {
|
|
569
200
|
await fetchPartialUpdate(
|
|
570
201
|
url,
|
|
571
|
-
hasUsableCache
|
|
202
|
+
hasUsableCache
|
|
203
|
+
? getNonLoaderSegmentIds(cachedSegments!)
|
|
204
|
+
: options?._skipCache
|
|
205
|
+
? [] // Action redirect: send no segments so server renders everything fresh
|
|
206
|
+
: undefined,
|
|
572
207
|
false,
|
|
573
208
|
tx.handle.signal,
|
|
574
209
|
tx.with({
|
|
@@ -577,55 +212,56 @@ export function createNavigationBridge(
|
|
|
577
212
|
scroll: options?.scroll,
|
|
578
213
|
state: resolvedState,
|
|
579
214
|
}),
|
|
580
|
-
// Pass cached segments (merged with current page's fresh segments for shared IDs)
|
|
581
|
-
// so the segment map is consistent with what we tell the server we have.
|
|
582
|
-
// Server decides what needs revalidation based on route matching and custom functions.
|
|
583
|
-
// No need for staleRevalidation flag - we're sending the freshest segments we have.
|
|
584
|
-
// Also pass cached handle data for restoring breadcrumbs when server returns empty diff.
|
|
585
|
-
// When leaving intercept, pass the flag so fetchPartialUpdate knows to filter segments.
|
|
586
215
|
hasUsableCache
|
|
587
|
-
? {
|
|
216
|
+
? {
|
|
217
|
+
type: "navigate" as const,
|
|
218
|
+
targetCacheSegments: cachedSegments,
|
|
219
|
+
targetCacheHandleData: cachedHandleData,
|
|
220
|
+
}
|
|
588
221
|
: isLeavingIntercept
|
|
589
|
-
? {
|
|
590
|
-
: undefined
|
|
222
|
+
? { type: "leave-intercept" as const }
|
|
223
|
+
: undefined,
|
|
591
224
|
);
|
|
592
225
|
} catch (error) {
|
|
593
|
-
//
|
|
226
|
+
// Server-side redirect with location state: the current transaction's
|
|
227
|
+
// cleanup resets loading state. Re-navigate to the redirect
|
|
228
|
+
// target carrying the server-set state into history.pushState.
|
|
229
|
+
if (error instanceof ServerRedirect) {
|
|
230
|
+
const redirectUrl = validateRedirectOrigin(
|
|
231
|
+
error.url,
|
|
232
|
+
window.location.origin,
|
|
233
|
+
);
|
|
234
|
+
if (!redirectUrl) {
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
return this.navigate(redirectUrl, {
|
|
238
|
+
state: error.state,
|
|
239
|
+
replace: options?.replace,
|
|
240
|
+
_skipCache: true,
|
|
241
|
+
} as NavigateOptionsInternal);
|
|
242
|
+
}
|
|
243
|
+
|
|
594
244
|
if (error instanceof DOMException && error.name === "AbortError") {
|
|
595
|
-
|
|
245
|
+
debugLog("[Browser] Navigation aborted by newer navigation");
|
|
596
246
|
return;
|
|
597
247
|
}
|
|
598
248
|
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
: new NetworkError(
|
|
605
|
-
"Unable to connect to server. Please check your connection.",
|
|
606
|
-
{ cause: error, url, operation: "navigation" }
|
|
607
|
-
);
|
|
608
|
-
|
|
249
|
+
const networkError = toNetworkError(error, {
|
|
250
|
+
url,
|
|
251
|
+
operation: "navigation",
|
|
252
|
+
});
|
|
253
|
+
if (networkError) {
|
|
609
254
|
console.error(
|
|
610
255
|
"[Browser] Network error during navigation:",
|
|
611
|
-
networkError
|
|
256
|
+
networkError,
|
|
612
257
|
);
|
|
613
|
-
|
|
614
|
-
// Emit update with NetworkErrorThrower to trigger root error boundary
|
|
615
|
-
startTransition(() => {
|
|
616
|
-
onUpdate({
|
|
617
|
-
root: createElement(NetworkErrorThrower, { error: networkError }),
|
|
618
|
-
metadata: {
|
|
619
|
-
pathname: url,
|
|
620
|
-
segments: [],
|
|
621
|
-
isError: true,
|
|
622
|
-
},
|
|
623
|
-
});
|
|
624
|
-
});
|
|
258
|
+
emitNetworkError(onUpdate, networkError, url);
|
|
625
259
|
return;
|
|
626
260
|
}
|
|
627
261
|
|
|
628
262
|
throw error;
|
|
263
|
+
} finally {
|
|
264
|
+
tx[Symbol.dispose]();
|
|
629
265
|
}
|
|
630
266
|
},
|
|
631
267
|
|
|
@@ -635,11 +271,11 @@ export function createNavigationBridge(
|
|
|
635
271
|
async refresh(): Promise<void> {
|
|
636
272
|
eventController.abortNavigation();
|
|
637
273
|
|
|
638
|
-
|
|
274
|
+
const tx = createNavigationTransaction(
|
|
639
275
|
store,
|
|
640
276
|
eventController,
|
|
641
277
|
window.location.href,
|
|
642
|
-
{ replace: true }
|
|
278
|
+
{ replace: true },
|
|
643
279
|
);
|
|
644
280
|
|
|
645
281
|
try {
|
|
@@ -649,41 +285,24 @@ export function createNavigationBridge(
|
|
|
649
285
|
[],
|
|
650
286
|
false,
|
|
651
287
|
tx.handle.signal,
|
|
652
|
-
tx.with({ url: window.location.href, replace: true, scroll: false })
|
|
288
|
+
tx.with({ url: window.location.href, replace: true, scroll: false }),
|
|
653
289
|
);
|
|
654
290
|
} catch (error) {
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
: new NetworkError(
|
|
661
|
-
"Unable to connect to server. Please check your connection.",
|
|
662
|
-
{
|
|
663
|
-
cause: error,
|
|
664
|
-
url: window.location.href,
|
|
665
|
-
operation: "revalidation",
|
|
666
|
-
}
|
|
667
|
-
);
|
|
668
|
-
|
|
291
|
+
const networkError = toNetworkError(error, {
|
|
292
|
+
url: window.location.href,
|
|
293
|
+
operation: "revalidation",
|
|
294
|
+
});
|
|
295
|
+
if (networkError) {
|
|
669
296
|
console.error(
|
|
670
297
|
"[Browser] Network error during refresh:",
|
|
671
|
-
networkError
|
|
298
|
+
networkError,
|
|
672
299
|
);
|
|
673
|
-
|
|
674
|
-
startTransition(() => {
|
|
675
|
-
onUpdate({
|
|
676
|
-
root: createElement(NetworkErrorThrower, { error: networkError }),
|
|
677
|
-
metadata: {
|
|
678
|
-
pathname: window.location.href,
|
|
679
|
-
segments: [],
|
|
680
|
-
isError: true,
|
|
681
|
-
},
|
|
682
|
-
});
|
|
683
|
-
});
|
|
300
|
+
emitNetworkError(onUpdate, networkError, window.location.href);
|
|
684
301
|
return;
|
|
685
302
|
}
|
|
686
303
|
throw error;
|
|
304
|
+
} finally {
|
|
305
|
+
tx[Symbol.dispose]();
|
|
687
306
|
}
|
|
688
307
|
},
|
|
689
308
|
|
|
@@ -707,8 +326,8 @@ export function createNavigationBridge(
|
|
|
707
326
|
const currentInterceptSource = store.getInterceptSourceUrl();
|
|
708
327
|
const newInterceptSource = interceptSourceUrl ?? null;
|
|
709
328
|
if (currentInterceptSource !== newInterceptSource) {
|
|
710
|
-
|
|
711
|
-
`[Browser] Intercept context changing (${currentInterceptSource} -> ${newInterceptSource}), aborting in-flight actions
|
|
329
|
+
debugLog(
|
|
330
|
+
`[Browser] Intercept context changing (${currentInterceptSource} -> ${newInterceptSource}), aborting in-flight actions`,
|
|
712
331
|
);
|
|
713
332
|
eventController.abortAllActions();
|
|
714
333
|
}
|
|
@@ -716,11 +335,11 @@ export function createNavigationBridge(
|
|
|
716
335
|
// Compute history key from URL (with intercept suffix if applicable)
|
|
717
336
|
const historyKey = generateHistoryKey(url, { intercept: isIntercept });
|
|
718
337
|
|
|
719
|
-
|
|
338
|
+
debugLog(
|
|
720
339
|
"[Browser] Popstate -",
|
|
721
340
|
isIntercept ? "intercept" : "normal",
|
|
722
341
|
"key:",
|
|
723
|
-
historyKey
|
|
342
|
+
historyKey,
|
|
724
343
|
);
|
|
725
344
|
|
|
726
345
|
// Update location in event controller
|
|
@@ -751,10 +370,19 @@ export function createNavigationBridge(
|
|
|
751
370
|
|
|
752
371
|
// Render from cache - force await to skip loading fallbacks
|
|
753
372
|
try {
|
|
754
|
-
const root = renderSegments(cachedSegments, {
|
|
373
|
+
const root = await renderSegments(cachedSegments, {
|
|
755
374
|
forceAwait: true,
|
|
756
375
|
});
|
|
757
|
-
|
|
376
|
+
// Merge params from cached segments for useParams restoration.
|
|
377
|
+
// Set params on event controller before onUpdate so both location
|
|
378
|
+
// and params are current when the debounced notify() fires.
|
|
379
|
+
const cachedParams: Record<string, string> = {};
|
|
380
|
+
for (const s of cachedSegments) {
|
|
381
|
+
if (s.params) Object.assign(cachedParams, s.params);
|
|
382
|
+
}
|
|
383
|
+
eventController.setParams(cachedParams);
|
|
384
|
+
|
|
385
|
+
const popstateUpdate = {
|
|
758
386
|
root,
|
|
759
387
|
metadata: {
|
|
760
388
|
pathname: new URL(url).pathname,
|
|
@@ -763,23 +391,35 @@ export function createNavigationBridge(
|
|
|
763
391
|
matched: cachedSegments.map((s) => s.id),
|
|
764
392
|
diff: [],
|
|
765
393
|
cachedHandleData,
|
|
394
|
+
params: cachedParams,
|
|
766
395
|
},
|
|
767
|
-
}
|
|
396
|
+
};
|
|
397
|
+
const hasTransition = cachedSegments.some((s) => s.transition);
|
|
398
|
+
if (hasTransition) {
|
|
399
|
+
startTransition(() => {
|
|
400
|
+
if (addTransitionType) {
|
|
401
|
+
addTransitionType("navigation-back");
|
|
402
|
+
}
|
|
403
|
+
onUpdate(popstateUpdate);
|
|
404
|
+
});
|
|
405
|
+
} else {
|
|
406
|
+
onUpdate(popstateUpdate);
|
|
407
|
+
}
|
|
768
408
|
|
|
769
409
|
// Restore scroll position for back/forward navigation
|
|
770
410
|
handleNavigationEnd({ restore: true, isStreaming });
|
|
771
411
|
|
|
772
412
|
// SWR: If stale, trigger background revalidation
|
|
773
413
|
if (isStale) {
|
|
774
|
-
|
|
414
|
+
debugLog("[Browser] Cache is stale, background revalidating...");
|
|
775
415
|
// Background revalidation - don't await, just fire and forget
|
|
776
|
-
const segmentIds = cachedSegments
|
|
416
|
+
const segmentIds = getNonLoaderSegmentIds(cachedSegments);
|
|
777
417
|
|
|
778
|
-
|
|
418
|
+
const tx = createNavigationTransaction(
|
|
779
419
|
store,
|
|
780
420
|
eventController,
|
|
781
421
|
url,
|
|
782
|
-
{ skipLoadingState: true, replace: true }
|
|
422
|
+
{ skipLoadingState: true, replace: true },
|
|
783
423
|
);
|
|
784
424
|
|
|
785
425
|
fetchPartialUpdate(
|
|
@@ -795,41 +435,33 @@ export function createNavigationBridge(
|
|
|
795
435
|
interceptSourceUrl,
|
|
796
436
|
cacheOnly: true,
|
|
797
437
|
}),
|
|
798
|
-
{
|
|
799
|
-
)
|
|
800
|
-
|
|
801
|
-
error
|
|
802
|
-
error
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
return;
|
|
806
|
-
}
|
|
807
|
-
// For background revalidation, network errors are logged but don't trigger error boundary
|
|
808
|
-
// since the user is already seeing cached content
|
|
809
|
-
if (error instanceof NetworkError || isNetworkError(error)) {
|
|
810
|
-
console.warn(
|
|
811
|
-
"[Browser] Background revalidation network error (cached content preserved):",
|
|
812
|
-
error.message
|
|
438
|
+
{ type: "stale-revalidation", interceptSourceUrl },
|
|
439
|
+
)
|
|
440
|
+
.catch((error) => {
|
|
441
|
+
if (isBackgroundSuppressible(error)) return;
|
|
442
|
+
console.error(
|
|
443
|
+
"[Browser] Background revalidation failed:",
|
|
444
|
+
error,
|
|
813
445
|
);
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
446
|
+
})
|
|
447
|
+
.finally(() => {
|
|
448
|
+
tx[Symbol.dispose]();
|
|
449
|
+
});
|
|
818
450
|
}
|
|
819
451
|
return;
|
|
820
452
|
} catch (error) {
|
|
821
453
|
console.warn(
|
|
822
454
|
"[Browser] Failed to render from cache, fetching:",
|
|
823
|
-
error
|
|
455
|
+
error,
|
|
824
456
|
);
|
|
825
457
|
// Fall through to fetch
|
|
826
458
|
}
|
|
827
459
|
} else {
|
|
828
|
-
|
|
460
|
+
debugLog("[Browser] History cache miss for key:", historyKey);
|
|
829
461
|
}
|
|
830
462
|
|
|
831
463
|
// Fetch if not cached
|
|
832
|
-
|
|
464
|
+
const tx = createNavigationTransaction(store, eventController, url, {
|
|
833
465
|
replace: true,
|
|
834
466
|
});
|
|
835
467
|
|
|
@@ -839,45 +471,39 @@ export function createNavigationBridge(
|
|
|
839
471
|
undefined,
|
|
840
472
|
false,
|
|
841
473
|
tx.handle.signal,
|
|
842
|
-
tx.with({
|
|
474
|
+
tx.with({
|
|
475
|
+
url,
|
|
476
|
+
replace: true,
|
|
477
|
+
scroll: false,
|
|
478
|
+
intercept: isIntercept,
|
|
479
|
+
interceptSourceUrl,
|
|
480
|
+
}),
|
|
481
|
+
isIntercept ? { type: "navigate", interceptSourceUrl } : undefined,
|
|
843
482
|
);
|
|
844
483
|
// Restore scroll position after fetch completes
|
|
845
484
|
handleNavigationEnd({ restore: true, isStreaming });
|
|
846
485
|
} catch (error) {
|
|
847
486
|
if (error instanceof DOMException && error.name === "AbortError") {
|
|
848
|
-
|
|
487
|
+
debugLog("[Browser] Popstate navigation aborted");
|
|
849
488
|
return;
|
|
850
489
|
}
|
|
851
490
|
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
: new NetworkError(
|
|
858
|
-
"Unable to connect to server. Please check your connection.",
|
|
859
|
-
{ cause: error, url, operation: "navigation" }
|
|
860
|
-
);
|
|
861
|
-
|
|
491
|
+
const networkError = toNetworkError(error, {
|
|
492
|
+
url,
|
|
493
|
+
operation: "navigation",
|
|
494
|
+
});
|
|
495
|
+
if (networkError) {
|
|
862
496
|
console.error(
|
|
863
|
-
"[Browser] Network error during popstate
|
|
864
|
-
networkError
|
|
497
|
+
"[Browser] Network error during popstate:",
|
|
498
|
+
networkError,
|
|
865
499
|
);
|
|
866
|
-
|
|
867
|
-
startTransition(() => {
|
|
868
|
-
onUpdate({
|
|
869
|
-
root: createElement(NetworkErrorThrower, { error: networkError }),
|
|
870
|
-
metadata: {
|
|
871
|
-
pathname: url,
|
|
872
|
-
segments: [],
|
|
873
|
-
isError: true,
|
|
874
|
-
},
|
|
875
|
-
});
|
|
876
|
-
});
|
|
500
|
+
emitNetworkError(onUpdate, networkError, url);
|
|
877
501
|
return;
|
|
878
502
|
}
|
|
879
503
|
|
|
880
504
|
throw error;
|
|
505
|
+
} finally {
|
|
506
|
+
tx[Symbol.dispose]();
|
|
881
507
|
}
|
|
882
508
|
},
|
|
883
509
|
|
|
@@ -894,17 +520,39 @@ export function createNavigationBridge(
|
|
|
894
520
|
this.handlePopstate();
|
|
895
521
|
};
|
|
896
522
|
|
|
523
|
+
// When the browser restores a page from bfcache (back-forward cache),
|
|
524
|
+
// any in-flight navigation state is stale. This happens when:
|
|
525
|
+
// 1. A navigation triggers X-RSC-Reload (e.g., response route hit via SPA)
|
|
526
|
+
// 2. window.location.href does a hard navigation
|
|
527
|
+
// 3. The user presses back and the browser restores from bfcache
|
|
528
|
+
// At that point, currentNavigation is still set from step 1, so
|
|
529
|
+
// getState() returns "loading" and the progress bar shows.
|
|
530
|
+
// Abort the stale navigation to reset state to idle.
|
|
531
|
+
const handlePageShow = (event: PageTransitionEvent) => {
|
|
532
|
+
if (event.persisted) {
|
|
533
|
+
debugLog(
|
|
534
|
+
"[Browser] Page restored from bfcache, resetting navigation state",
|
|
535
|
+
);
|
|
536
|
+
eventController.abortNavigation();
|
|
537
|
+
// pagehide flips scrollRestoration to "auto" for bfcache compat;
|
|
538
|
+
// restore "manual" so the router controls scroll on SPA navigations.
|
|
539
|
+
window.history.scrollRestoration = "manual";
|
|
540
|
+
}
|
|
541
|
+
};
|
|
542
|
+
|
|
897
543
|
// Register cross-tab refresh callback with the store
|
|
898
544
|
store.setCrossTabRefreshCallback(() => {
|
|
899
545
|
this.refresh();
|
|
900
546
|
});
|
|
901
547
|
|
|
902
548
|
window.addEventListener("popstate", handlePopstate);
|
|
903
|
-
|
|
549
|
+
window.addEventListener("pageshow", handlePageShow);
|
|
550
|
+
debugLog("[Browser] Navigation bridge ready");
|
|
904
551
|
|
|
905
552
|
return () => {
|
|
906
553
|
cleanupLinks();
|
|
907
554
|
window.removeEventListener("popstate", handlePopstate);
|
|
555
|
+
window.removeEventListener("pageshow", handlePageShow);
|
|
908
556
|
};
|
|
909
557
|
},
|
|
910
558
|
};
|