@rangojs/router 0.0.0-experimental.10 → 0.0.0-experimental.100
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 +1037 -4
- package/dist/bin/rango.js +1619 -157
- package/dist/vite/index.js +5762 -2301
- package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/package.json +71 -63
- package/skills/breadcrumbs/SKILL.md +252 -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 +6 -4
- package/skills/handler-use/SKILL.md +364 -0
- package/skills/hooks/SKILL.md +367 -71
- package/skills/host-router/SKILL.md +218 -0
- package/skills/i18n/SKILL.md +276 -0
- package/skills/intercept/SKILL.md +176 -8
- package/skills/layout/SKILL.md +124 -3
- package/skills/links/SKILL.md +304 -25
- package/skills/loader/SKILL.md +474 -47
- package/skills/middleware/SKILL.md +207 -37
- package/skills/migrate-nextjs/SKILL.md +562 -0
- package/skills/migrate-react-router/SKILL.md +769 -0
- package/skills/mime-routes/SKILL.md +15 -11
- package/skills/parallel/SKILL.md +272 -1
- package/skills/prerender/SKILL.md +467 -65
- package/skills/rango/SKILL.md +89 -21
- package/skills/response-routes/SKILL.md +152 -91
- package/skills/route/SKILL.md +305 -14
- package/skills/router-setup/SKILL.md +210 -32
- package/skills/server-actions/SKILL.md +739 -0
- package/skills/streams-and-websockets/SKILL.md +283 -0
- package/skills/theme/SKILL.md +9 -8
- package/skills/typesafety/SKILL.md +333 -86
- package/skills/use-cache/SKILL.md +324 -0
- package/skills/view-transitions/SKILL.md +212 -0
- package/src/__internal.ts +102 -4
- package/src/bin/rango.ts +312 -15
- package/src/browser/action-coordinator.ts +97 -0
- package/src/browser/action-response-classifier.ts +99 -0
- package/src/browser/app-shell.ts +52 -0
- package/src/browser/app-version.ts +14 -0
- package/src/browser/event-controller.ts +136 -68
- 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 +374 -561
- package/src/browser/navigation-client.ts +228 -70
- package/src/browser/navigation-store.ts +97 -55
- package/src/browser/navigation-transaction.ts +297 -0
- package/src/browser/network-error-handler.ts +61 -0
- package/src/browser/partial-update.ts +376 -315
- package/src/browser/prefetch/cache.ts +314 -0
- package/src/browser/prefetch/fetch.ts +282 -0
- package/src/browser/prefetch/observer.ts +65 -0
- package/src/browser/prefetch/policy.ts +48 -0
- package/src/browser/prefetch/queue.ts +191 -0
- package/src/browser/prefetch/resource-ready.ts +77 -0
- package/src/browser/rango-state.ts +152 -0
- package/src/browser/react/Link.tsx +255 -71
- package/src/browser/react/NavigationProvider.tsx +152 -24
- package/src/browser/react/context.ts +11 -0
- package/src/browser/react/filter-segment-order.ts +55 -0
- package/src/browser/react/index.ts +15 -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 -120
- package/src/browser/react/use-link-status.ts +6 -5
- package/src/browser/react/use-navigation.ts +44 -65
- package/src/browser/react/use-params.ts +78 -0
- package/src/browser/react/use-pathname.ts +47 -0
- package/src/browser/react/use-reverse.ts +99 -0
- package/src/browser/react/use-router.ts +83 -0
- package/src/browser/react/use-search-params.ts +56 -0
- package/src/browser/react/use-segments.ts +85 -99
- package/src/browser/response-adapter.ts +73 -0
- package/src/browser/rsc-router.tsx +246 -64
- package/src/browser/scroll-restoration.ts +127 -52
- package/src/browser/segment-reconciler.ts +243 -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 +158 -48
- package/src/browser/validate-redirect-origin.ts +29 -0
- package/src/build/generate-manifest.ts +84 -23
- package/src/build/generate-route-types.ts +39 -828
- package/src/build/index.ts +4 -5
- package/src/build/route-trie.ts +85 -32
- 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 -307
- package/src/cache/cf/cf-cache-store.ts +573 -21
- 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 +6 -1
- package/src/client.tsx +118 -302
- 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 +77 -7
- package/src/handle.ts +55 -10
- 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 +65 -45
- package/src/index.rsc.ts +138 -21
- package/src/index.ts +206 -51
- package/src/internal-debug.ts +11 -0
- package/src/loader.rsc.ts +25 -143
- package/src/loader.ts +27 -10
- package/src/network-error-thrower.tsx +3 -1
- package/src/outlet-context.ts +1 -1
- package/src/outlet-provider.tsx +45 -0
- package/src/prerender/param-hash.ts +4 -2
- package/src/prerender/store.ts +159 -13
- package/src/prerender.ts +397 -29
- package/src/response-utils.ts +28 -0
- package/src/reverse.ts +231 -121
- package/src/root-error-boundary.tsx +41 -29
- package/src/route-content-wrapper.tsx +7 -4
- package/src/route-definition/dsl-helpers.ts +1134 -0
- package/src/route-definition/helper-factories.ts +200 -0
- package/src/route-definition/helpers-types.ts +483 -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 +155 -0
- package/src/route-definition.ts +1 -1431
- package/src/route-map-builder.ts +162 -123
- package/src/route-name.ts +53 -0
- package/src/route-types.ts +66 -9
- 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 +418 -86
- package/src/router/intercept-resolution.ts +35 -20
- package/src/router/lazy-includes.ts +237 -0
- package/src/router/loader-resolution.ts +359 -128
- package/src/router/logging.ts +251 -0
- package/src/router/manifest.ts +98 -32
- package/src/router/match-api.ts +196 -261
- package/src/router/match-context.ts +4 -2
- package/src/router/match-handlers.ts +441 -0
- package/src/router/match-middleware/background-revalidation.ts +108 -93
- package/src/router/match-middleware/cache-lookup.ts +415 -86
- package/src/router/match-middleware/cache-store.ts +91 -29
- package/src/router/match-middleware/intercept-resolution.ts +48 -21
- package/src/router/match-middleware/segment-resolution.ts +73 -9
- package/src/router/match-pipelines.ts +10 -45
- package/src/router/match-result.ts +154 -35
- package/src/router/metrics.ts +240 -15
- package/src/router/middleware-cookies.ts +55 -0
- package/src/router/middleware-types.ts +209 -0
- package/src/router/middleware.ts +373 -371
- package/src/router/navigation-snapshot.ts +182 -0
- package/src/router/pattern-matching.ts +292 -52
- 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 +152 -39
- 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 +756 -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 +1407 -0
- package/src/router/segment-resolution/static-store.ts +67 -0
- package/src/router/segment-resolution.ts +21 -1315
- package/src/router/segment-wrappers.ts +291 -0
- package/src/router/substitute-pattern-params.ts +56 -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 +111 -39
- package/src/router/types.ts +17 -9
- package/src/router/url-params.ts +49 -0
- package/src/router.ts +642 -2011
- package/src/rsc/handler-context.ts +45 -0
- package/src/rsc/handler.ts +864 -1114
- package/src/rsc/helpers.ts +181 -19
- 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 +395 -0
- package/src/rsc/response-error.ts +37 -0
- package/src/rsc/response-route-handler.ts +360 -0
- package/src/rsc/rsc-rendering.ts +256 -0
- package/src/rsc/runtime-warnings.ts +42 -0
- package/src/rsc/server-action.ts +360 -0
- package/src/rsc/ssr-setup.ts +128 -0
- package/src/rsc/types.ts +52 -11
- package/src/search-params.ts +230 -0
- package/src/segment-content-promise.ts +67 -0
- package/src/segment-loader-promise.ts +122 -0
- package/src/segment-system.tsx +187 -38
- package/src/server/context.ts +333 -59
- 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 +603 -109
- package/src/server.ts +35 -155
- package/src/ssr/index.tsx +107 -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 +764 -0
- package/src/types/index.ts +88 -0
- package/src/types/loader-types.ts +209 -0
- package/src/types/request-scope.ts +126 -0
- package/src/types/route-config.ts +170 -0
- package/src/types/route-entry.ts +120 -0
- package/src/types/segments.ts +167 -0
- package/src/types.ts +1 -1757
- package/src/urls/include-helper.ts +207 -0
- package/src/urls/index.ts +53 -0
- package/src/urls/path-helper-types.ts +372 -0
- package/src/urls/path-helper.ts +364 -0
- package/src/urls/pattern-types.ts +107 -0
- package/src/urls/response-types.ts +108 -0
- package/src/urls/type-extraction.ts +372 -0
- package/src/urls/urls-function.ts +98 -0
- package/src/urls.ts +1 -1282
- package/src/use-loader.tsx +161 -81
- package/src/vite/debug.ts +184 -0
- package/src/vite/discovery/bundle-postprocess.ts +181 -0
- package/src/vite/discovery/discover-routers.ts +376 -0
- package/src/vite/discovery/gate-state.ts +171 -0
- package/src/vite/discovery/prerender-collection.ts +486 -0
- package/src/vite/discovery/route-types-writer.ts +258 -0
- package/src/vite/discovery/self-gen-tracking.ts +73 -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 -2063
- package/src/vite/plugin-types.ts +103 -0
- package/src/vite/plugins/cjs-to-esm.ts +98 -0
- package/src/vite/plugins/client-ref-dedup.ts +131 -0
- package/src/vite/plugins/client-ref-hashing.ts +117 -0
- package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
- package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/src/vite/plugins/cloudflare-protocol-stub.ts +214 -0
- package/src/vite/{expose-action-id.ts → plugins/expose-action-id.ts} +107 -64
- 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 +127 -0
- package/src/vite/plugins/expose-ids/types.ts +45 -0
- package/src/vite/plugins/expose-internal-ids.ts +816 -0
- package/src/vite/plugins/performance-tracks.ts +96 -0
- package/src/vite/plugins/refresh-cmd.ts +127 -0
- package/src/vite/plugins/use-cache-transform.ts +336 -0
- package/src/vite/plugins/version-injector.ts +109 -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 +497 -0
- package/src/vite/router-discovery.ts +1423 -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/utils/package-resolution.ts +161 -0
- package/src/vite/utils/prerender-utils.ts +222 -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/router.gen.ts +0 -6
- package/src/urls.gen.ts +0 -8
- 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/expose-prerender-handler-id.ts +0 -429
- package/src/vite/package-resolution.ts +0 -125
- /package/src/vite/{version.d.ts → plugins/version.d.ts} +0 -0
|
@@ -8,7 +8,9 @@ import type {
|
|
|
8
8
|
ResolvedSegment,
|
|
9
9
|
RscMetadata,
|
|
10
10
|
HandleData,
|
|
11
|
+
StreamingToken,
|
|
11
12
|
} from "./types.js";
|
|
13
|
+
import { filterSegmentOrder } from "./react/filter-segment-order.js";
|
|
12
14
|
|
|
13
15
|
// Polyfill Symbol.dispose for Safari and older browsers
|
|
14
16
|
if (typeof Symbol.dispose === "undefined") {
|
|
@@ -40,7 +42,7 @@ export interface NavigationEntry {
|
|
|
40
42
|
abort: AbortController;
|
|
41
43
|
phase: NavigationPhase;
|
|
42
44
|
startedAt: number;
|
|
43
|
-
options?: NavigateOptions;
|
|
45
|
+
options?: NavigateOptions & { skipLoadingState?: boolean };
|
|
44
46
|
}
|
|
45
47
|
|
|
46
48
|
/**
|
|
@@ -77,6 +79,8 @@ export interface DerivedNavigationState {
|
|
|
77
79
|
state: "idle" | "loading";
|
|
78
80
|
/** Whether any operation is streaming */
|
|
79
81
|
isStreaming: boolean;
|
|
82
|
+
/** Whether a navigation is active (fetching or streaming, before commit) */
|
|
83
|
+
isNavigating: boolean;
|
|
80
84
|
/** Current committed location */
|
|
81
85
|
location: NavigationLocation;
|
|
82
86
|
/** URL being navigated to (null if idle) */
|
|
@@ -109,20 +113,24 @@ export type ActionStateListener = (state: TrackedActionState) => void;
|
|
|
109
113
|
export type HandleListener = () => void;
|
|
110
114
|
|
|
111
115
|
/**
|
|
112
|
-
* Internal handle state stored in controller
|
|
116
|
+
* Internal handle state stored in controller.
|
|
117
|
+
*
|
|
118
|
+
* Two segment lists are exposed because they serve different consumers:
|
|
119
|
+
*
|
|
120
|
+
* - `segmentOrder` drives handle collection (collectHandleData). Includes
|
|
121
|
+
* parallel slot ids and reorders them after their parent so later-wins
|
|
122
|
+
* collect functions (e.g. Meta) get the right precedence.
|
|
123
|
+
* - `routeSegmentIds` is the layouts-and-routes-only list documented by
|
|
124
|
+
* `useSegments().segmentIds`. Parallels and loader sub-ids are stripped;
|
|
125
|
+
* raw matched order is preserved.
|
|
126
|
+
*
|
|
127
|
+
* Both are derived from the same `matched` input on each setHandleData call
|
|
128
|
+
* so they stay in sync.
|
|
113
129
|
*/
|
|
114
130
|
export interface HandleState {
|
|
115
131
|
data: HandleData;
|
|
116
132
|
segmentOrder: string[];
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
/**
|
|
120
|
-
* Token for tracking an active stream
|
|
121
|
-
* Call end() when the stream completes
|
|
122
|
-
*/
|
|
123
|
-
export interface StreamingToken {
|
|
124
|
-
/** End this streaming operation */
|
|
125
|
-
end(): void;
|
|
133
|
+
routeSegmentIds: string[];
|
|
126
134
|
}
|
|
127
135
|
|
|
128
136
|
/**
|
|
@@ -165,8 +173,8 @@ export interface ActionHandle extends Disposable {
|
|
|
165
173
|
readonly settled: boolean;
|
|
166
174
|
/** Check if any concurrent actions were started */
|
|
167
175
|
hadConcurrentActions: boolean;
|
|
168
|
-
/** Get
|
|
169
|
-
|
|
176
|
+
/** Get raw set of segments revalidated by concurrent actions */
|
|
177
|
+
getRevalidatedSegments(): Set<string>;
|
|
170
178
|
/** Clear consolidation tracking */
|
|
171
179
|
clearConsolidation(): void;
|
|
172
180
|
}
|
|
@@ -176,7 +184,10 @@ export interface ActionHandle extends Disposable {
|
|
|
176
184
|
*/
|
|
177
185
|
export interface EventController {
|
|
178
186
|
// Navigation operations
|
|
179
|
-
startNavigation(
|
|
187
|
+
startNavigation(
|
|
188
|
+
url: string,
|
|
189
|
+
options?: NavigateOptions & { skipLoadingState?: boolean },
|
|
190
|
+
): NavigationHandle;
|
|
180
191
|
abortNavigation(): void;
|
|
181
192
|
|
|
182
193
|
// Action operations
|
|
@@ -186,6 +197,7 @@ export interface EventController {
|
|
|
186
197
|
// State access
|
|
187
198
|
getState(): DerivedNavigationState;
|
|
188
199
|
getActionState(actionId: string): TrackedActionState;
|
|
200
|
+
getLocation(): NavigationLocation;
|
|
189
201
|
|
|
190
202
|
// Location updates (for popstate where navigation doesn't go through startNavigation)
|
|
191
203
|
setLocation(location: NavigationLocation): void;
|
|
@@ -194,7 +206,7 @@ export interface EventController {
|
|
|
194
206
|
subscribe(listener: StateListener): () => void;
|
|
195
207
|
subscribeToAction(
|
|
196
208
|
actionId: string,
|
|
197
|
-
listener: ActionStateListener
|
|
209
|
+
listener: ActionStateListener,
|
|
198
210
|
): () => void;
|
|
199
211
|
subscribeToHandles(listener: HandleListener): () => void;
|
|
200
212
|
|
|
@@ -202,13 +214,27 @@ export interface EventController {
|
|
|
202
214
|
setHandleData(
|
|
203
215
|
data: HandleData,
|
|
204
216
|
matched?: string[],
|
|
205
|
-
isPartial?: boolean
|
|
217
|
+
isPartial?: boolean,
|
|
218
|
+
/**
|
|
219
|
+
* Segment ids that were re-resolved on the server this request (the
|
|
220
|
+
* partial response's `diff`). On a partial update, any existing bucket
|
|
221
|
+
* keyed under one of these ids that has no incoming entry is treated as
|
|
222
|
+
* stale and cleared. Without this, a parallel slot that revalidates but
|
|
223
|
+
* pushes nothing leaves its previous bucket in place forever.
|
|
224
|
+
*/
|
|
225
|
+
resolvedIds?: string[],
|
|
206
226
|
): void;
|
|
207
227
|
getHandleState(): HandleState;
|
|
208
228
|
|
|
229
|
+
// Params operations
|
|
230
|
+
setParams(params: Record<string, string>): void;
|
|
231
|
+
getParams(): Record<string, string>;
|
|
232
|
+
|
|
209
233
|
// Direct state access for advanced use
|
|
210
234
|
getCurrentNavigation(): NavigationEntry | null;
|
|
211
235
|
getInflightActions(): Map<string, ActionEntry>;
|
|
236
|
+
/** Whether any concurrent actions have occurred (shared across all handles) */
|
|
237
|
+
hadAnyConcurrentActions(): boolean;
|
|
212
238
|
}
|
|
213
239
|
|
|
214
240
|
// ============================================================================
|
|
@@ -230,7 +256,10 @@ const DEFAULT_ACTION_STATE: TrackedActionState = {
|
|
|
230
256
|
* When subscriptionId has no '#', it's just an action name and matches by suffix.
|
|
231
257
|
* This allows useAction("addToCart") to match "hash#addToCart" or "src/file.ts#addToCart".
|
|
232
258
|
*/
|
|
233
|
-
function matchesActionId(
|
|
259
|
+
function matchesActionId(
|
|
260
|
+
subscriptionId: string,
|
|
261
|
+
entryActionId: string,
|
|
262
|
+
): boolean {
|
|
234
263
|
if (subscriptionId.includes("#")) {
|
|
235
264
|
// Full ID: exact match
|
|
236
265
|
return subscriptionId === entryActionId;
|
|
@@ -261,7 +290,7 @@ export interface EventControllerConfig {
|
|
|
261
290
|
* Actions use mergeMap semantics (all run concurrently, consolidate at end).
|
|
262
291
|
*/
|
|
263
292
|
export function createEventController(
|
|
264
|
-
config?: EventControllerConfig
|
|
293
|
+
config?: EventControllerConfig,
|
|
265
294
|
): EventController {
|
|
266
295
|
// ========================================================================
|
|
267
296
|
// Source of Truth
|
|
@@ -292,6 +321,10 @@ export function createEventController(
|
|
|
292
321
|
// Handle data from RSC payload
|
|
293
322
|
let handleData: HandleData = {};
|
|
294
323
|
let handleSegmentOrder: string[] = [];
|
|
324
|
+
let routeSegmentIds: string[] = [];
|
|
325
|
+
|
|
326
|
+
// Merged route params from current match
|
|
327
|
+
let routeParams: Record<string, string> = {};
|
|
295
328
|
|
|
296
329
|
// ========================================================================
|
|
297
330
|
// Listeners
|
|
@@ -334,7 +367,7 @@ export function createEventController(
|
|
|
334
367
|
listeners.forEach((listener) => listener(state));
|
|
335
368
|
}
|
|
336
369
|
}
|
|
337
|
-
}, 0)
|
|
370
|
+
}, 0),
|
|
338
371
|
);
|
|
339
372
|
}
|
|
340
373
|
|
|
@@ -367,9 +400,12 @@ export function createEventController(
|
|
|
367
400
|
}));
|
|
368
401
|
|
|
369
402
|
// State: loading if navigation OR actions are in progress
|
|
403
|
+
// Background revalidations (skipLoadingState) don't affect visible state
|
|
370
404
|
const hasActiveActions = inflightActionsList.length > 0;
|
|
371
|
-
const
|
|
372
|
-
currentNavigation !== null
|
|
405
|
+
const isVisibleNavigation =
|
|
406
|
+
currentNavigation !== null &&
|
|
407
|
+
!currentNavigation.options?.skipLoadingState;
|
|
408
|
+
const state = isVisibleNavigation || hasActiveActions ? "loading" : "idle";
|
|
373
409
|
|
|
374
410
|
// Streaming: true if any active streams (navigation or action) or loading
|
|
375
411
|
const isStreaming = activeStreamCount > 0 || state === "loading";
|
|
@@ -377,9 +413,17 @@ export function createEventController(
|
|
|
377
413
|
return {
|
|
378
414
|
state,
|
|
379
415
|
isStreaming,
|
|
416
|
+
// True when a navigation is active (fetching or streaming, before
|
|
417
|
+
// commit). Broader than pendingUrl which clears during streaming.
|
|
418
|
+
isNavigating: currentNavigation !== null,
|
|
380
419
|
location,
|
|
381
|
-
// pendingUrl only during fetching phase - once streaming starts (URL changed), not pending
|
|
382
|
-
|
|
420
|
+
// pendingUrl only during fetching phase - once streaming starts (URL changed), not pending.
|
|
421
|
+
// Background revalidations (skipLoadingState) don't expose a pending URL.
|
|
422
|
+
pendingUrl:
|
|
423
|
+
currentNavigation?.phase === "fetching" &&
|
|
424
|
+
!currentNavigation.options?.skipLoadingState
|
|
425
|
+
? currentNavigation.url
|
|
426
|
+
: null,
|
|
383
427
|
inflightActions: inflightActionsList,
|
|
384
428
|
};
|
|
385
429
|
}
|
|
@@ -388,12 +432,16 @@ export function createEventController(
|
|
|
388
432
|
// Find the most recent action with this ID that's not settling
|
|
389
433
|
// Uses suffix matching when actionId is just a name (no #)
|
|
390
434
|
const activeEntry = [...inflightActions.values()]
|
|
391
|
-
.filter(
|
|
435
|
+
.filter(
|
|
436
|
+
(a) => matchesActionId(actionId, a.actionId) && a.phase !== "settling",
|
|
437
|
+
)
|
|
392
438
|
.sort((a, b) => b.startedAt - a.startedAt)[0];
|
|
393
439
|
|
|
394
440
|
// Also check for settling entries to get result/error
|
|
395
441
|
const settlingEntry = [...inflightActions.values()]
|
|
396
|
-
.filter(
|
|
442
|
+
.filter(
|
|
443
|
+
(a) => matchesActionId(actionId, a.actionId) && a.phase === "settling",
|
|
444
|
+
)
|
|
397
445
|
.sort((a, b) => b.startedAt - a.startedAt)[0];
|
|
398
446
|
|
|
399
447
|
const entry = activeEntry || settlingEntry;
|
|
@@ -431,7 +479,7 @@ export function createEventController(
|
|
|
431
479
|
|
|
432
480
|
function startNavigation(
|
|
433
481
|
url: string,
|
|
434
|
-
options?: NavigateOptions
|
|
482
|
+
options?: NavigateOptions & { skipLoadingState?: boolean },
|
|
435
483
|
): NavigationHandle {
|
|
436
484
|
// Cancel existing navigation (switchMap semantics)
|
|
437
485
|
if (currentNavigation) {
|
|
@@ -463,6 +511,7 @@ export function createEventController(
|
|
|
463
511
|
|
|
464
512
|
startStreaming(): StreamingToken {
|
|
465
513
|
let ended = false;
|
|
514
|
+
entry.phase = "streaming";
|
|
466
515
|
activeStreamCount++;
|
|
467
516
|
notify();
|
|
468
517
|
|
|
@@ -650,24 +699,8 @@ export function createEventController(
|
|
|
650
699
|
// If streaming is in progress, tryFinalize() will be called when streaming ends
|
|
651
700
|
},
|
|
652
701
|
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
// We don't need to wait for streaming to complete since we're refetching anyway
|
|
656
|
-
// Count actions that are still fetching (waiting for server response)
|
|
657
|
-
const stillFetchingCount = [...inflightActions.values()].filter(
|
|
658
|
-
(a) => a.phase === "fetching"
|
|
659
|
-
).length;
|
|
660
|
-
|
|
661
|
-
if (stillFetchingCount > 0) {
|
|
662
|
-
return null; // Some actions still waiting for server response
|
|
663
|
-
}
|
|
664
|
-
if (!hadAnyConcurrentActions) {
|
|
665
|
-
return null; // No concurrent actions occurred
|
|
666
|
-
}
|
|
667
|
-
if (concurrentRevalidatedSegments.size === 0) {
|
|
668
|
-
return null; // No segments to consolidate
|
|
669
|
-
}
|
|
670
|
-
return Array.from(concurrentRevalidatedSegments);
|
|
702
|
+
getRevalidatedSegments(): Set<string> {
|
|
703
|
+
return concurrentRevalidatedSegments;
|
|
671
704
|
},
|
|
672
705
|
|
|
673
706
|
clearConsolidation() {
|
|
@@ -702,16 +735,26 @@ export function createEventController(
|
|
|
702
735
|
}
|
|
703
736
|
|
|
704
737
|
function abortAllActions() {
|
|
705
|
-
for (const entry of inflightActions
|
|
738
|
+
for (const [id, entry] of inflightActions) {
|
|
739
|
+
// Preserve settling entries — they have already been handled by
|
|
740
|
+
// fail()/complete() and will self-cleanup via the settlement timeout.
|
|
741
|
+
// Clearing them here would prevent debounced notifications from
|
|
742
|
+
// delivering the error/result state to subscribers.
|
|
743
|
+
if (entry.phase === "settling") continue;
|
|
706
744
|
entry.abort.abort();
|
|
745
|
+
inflightActions.delete(id);
|
|
707
746
|
}
|
|
708
|
-
inflightActions.clear();
|
|
709
747
|
hadAnyConcurrentActions = false;
|
|
710
748
|
concurrentRevalidatedSegments.clear();
|
|
711
749
|
notify();
|
|
712
|
-
// Notify all action listeners
|
|
713
|
-
|
|
714
|
-
|
|
750
|
+
// Notify all action listeners directly by subscription ID.
|
|
751
|
+
// actionListeners keys are subscription IDs (possibly short names like
|
|
752
|
+
// "addToCart"), not full entry actionIds. Passing them to notifyAction
|
|
753
|
+
// would fail the suffix matcher — instead, notify each subscriber with
|
|
754
|
+
// its own state.
|
|
755
|
+
for (const [subscriptionId, listeners] of actionListeners) {
|
|
756
|
+
const state = getActionState(subscriptionId);
|
|
757
|
+
listeners.forEach((listener) => listener(state));
|
|
715
758
|
}
|
|
716
759
|
}
|
|
717
760
|
|
|
@@ -719,24 +762,19 @@ export function createEventController(
|
|
|
719
762
|
// Handle Operations
|
|
720
763
|
// ========================================================================
|
|
721
764
|
|
|
722
|
-
/**
|
|
723
|
-
* Filter segment IDs to only include routes and layouts.
|
|
724
|
-
* Excludes parallels (contain .@) and loaders (contain D followed by digit).
|
|
725
|
-
*/
|
|
726
|
-
function filterSegmentOrder(matched: string[]): string[] {
|
|
727
|
-
return matched.filter((id) => {
|
|
728
|
-
if (id.includes(".@")) return false;
|
|
729
|
-
if (/D\d+\./.test(id)) return false;
|
|
730
|
-
return true;
|
|
731
|
-
});
|
|
732
|
-
}
|
|
733
|
-
|
|
734
765
|
function setHandleData(
|
|
735
766
|
data: HandleData,
|
|
736
767
|
matched?: string[],
|
|
737
|
-
isPartial?: boolean
|
|
768
|
+
isPartial?: boolean,
|
|
769
|
+
resolvedIds?: string[],
|
|
738
770
|
): void {
|
|
739
|
-
const
|
|
771
|
+
const rawMatched = matched ?? [];
|
|
772
|
+
const newSegmentOrder = filterSegmentOrder(rawMatched);
|
|
773
|
+
// Separate list for useSegments(): "layouts and routes only" — strip
|
|
774
|
+
// parallels (".@") and loader sub-ids (D digit) without reordering.
|
|
775
|
+
const newRouteSegmentIds = rawMatched.filter(
|
|
776
|
+
(id) => !id.includes(".@") && !/D\d+\./.test(id),
|
|
777
|
+
);
|
|
740
778
|
|
|
741
779
|
if (isPartial && newSegmentOrder.length > 0) {
|
|
742
780
|
// Partial update: merge new data with existing
|
|
@@ -748,10 +786,19 @@ export function createEventController(
|
|
|
748
786
|
handleData[handleName][segmentId] = data[handleName][segmentId];
|
|
749
787
|
}
|
|
750
788
|
}
|
|
751
|
-
|
|
789
|
+
const resolvedIdSet =
|
|
790
|
+
resolvedIds && resolvedIds.length > 0 ? new Set(resolvedIds) : null;
|
|
791
|
+
// Cleanup pass:
|
|
792
|
+
// a) segment dropped from the match list — delete its bucket.
|
|
793
|
+
// b) segment was re-resolved this request but pushed nothing for
|
|
794
|
+
// this handle — its previous bucket is stale.
|
|
795
|
+
// (a) is the existing behavior; (b) requires resolvedIds.
|
|
752
796
|
for (const handleName of Object.keys(handleData)) {
|
|
753
797
|
for (const segmentId of Object.keys(handleData[handleName])) {
|
|
754
|
-
|
|
798
|
+
const droppedFromMatch = !newSegmentOrder.includes(segmentId);
|
|
799
|
+
const reresolvedWithoutPush =
|
|
800
|
+
resolvedIdSet?.has(segmentId) && !data[handleName]?.[segmentId];
|
|
801
|
+
if (droppedFromMatch || reresolvedWithoutPush) {
|
|
755
802
|
delete handleData[handleName][segmentId];
|
|
756
803
|
}
|
|
757
804
|
}
|
|
@@ -761,6 +808,7 @@ export function createEventController(
|
|
|
761
808
|
handleData = data;
|
|
762
809
|
}
|
|
763
810
|
handleSegmentOrder = newSegmentOrder;
|
|
811
|
+
routeSegmentIds = newRouteSegmentIds;
|
|
764
812
|
|
|
765
813
|
notifyHandles();
|
|
766
814
|
}
|
|
@@ -769,6 +817,7 @@ export function createEventController(
|
|
|
769
817
|
return {
|
|
770
818
|
data: handleData,
|
|
771
819
|
segmentOrder: handleSegmentOrder,
|
|
820
|
+
routeSegmentIds,
|
|
772
821
|
};
|
|
773
822
|
}
|
|
774
823
|
|
|
@@ -783,7 +832,7 @@ export function createEventController(
|
|
|
783
832
|
|
|
784
833
|
function subscribeToAction(
|
|
785
834
|
actionId: string,
|
|
786
|
-
listener: ActionStateListener
|
|
835
|
+
listener: ActionStateListener,
|
|
787
836
|
): () => void {
|
|
788
837
|
let listeners = actionListeners.get(actionId);
|
|
789
838
|
if (!listeners) {
|
|
@@ -805,6 +854,19 @@ export function createEventController(
|
|
|
805
854
|
return () => handleListeners.delete(listener);
|
|
806
855
|
}
|
|
807
856
|
|
|
857
|
+
// ========================================================================
|
|
858
|
+
// Params Operations
|
|
859
|
+
// ========================================================================
|
|
860
|
+
|
|
861
|
+
function setParams(params: Record<string, string>): void {
|
|
862
|
+
routeParams = params;
|
|
863
|
+
notify();
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
function getParams(): Record<string, string> {
|
|
867
|
+
return routeParams;
|
|
868
|
+
}
|
|
869
|
+
|
|
808
870
|
// ========================================================================
|
|
809
871
|
// Return Controller
|
|
810
872
|
// ========================================================================
|
|
@@ -821,12 +883,17 @@ export function createEventController(
|
|
|
821
883
|
// State
|
|
822
884
|
getState,
|
|
823
885
|
getActionState,
|
|
886
|
+
getLocation: () => location,
|
|
824
887
|
setLocation,
|
|
825
888
|
|
|
826
889
|
// Handles
|
|
827
890
|
setHandleData,
|
|
828
891
|
getHandleState,
|
|
829
892
|
|
|
893
|
+
// Params
|
|
894
|
+
setParams,
|
|
895
|
+
getParams,
|
|
896
|
+
|
|
830
897
|
// Subscriptions
|
|
831
898
|
subscribe,
|
|
832
899
|
subscribeToAction,
|
|
@@ -835,6 +902,7 @@ export function createEventController(
|
|
|
835
902
|
// Direct access
|
|
836
903
|
getCurrentNavigation: () => currentNavigation,
|
|
837
904
|
getInflightActions: () => inflightActions,
|
|
905
|
+
hadAnyConcurrentActions: () => hadAnyConcurrentActions,
|
|
838
906
|
};
|
|
839
907
|
}
|
|
840
908
|
|
|
@@ -848,7 +916,7 @@ let controllerInstance: EventController | null = null;
|
|
|
848
916
|
* Initialize the global event controller
|
|
849
917
|
*/
|
|
850
918
|
export function initEventController(
|
|
851
|
-
config?: EventControllerConfig
|
|
919
|
+
config?: EventControllerConfig,
|
|
852
920
|
): EventController {
|
|
853
921
|
if (!controllerInstance) {
|
|
854
922
|
controllerInstance = createEventController(config);
|
|
@@ -862,7 +930,7 @@ export function initEventController(
|
|
|
862
930
|
export function getEventController(): EventController {
|
|
863
931
|
if (!controllerInstance) {
|
|
864
932
|
throw new Error(
|
|
865
|
-
"Event controller not initialized. Call initEventController first."
|
|
933
|
+
"Event controller not initialized. Call initEventController first.",
|
|
866
934
|
);
|
|
867
935
|
}
|
|
868
936
|
return controllerInstance;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import {
|
|
2
|
+
isLocationStateEntry,
|
|
3
|
+
resolveLocationStateEntries,
|
|
4
|
+
} from "./react/location-state-shared.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Check if state is from typed LocationStateEntry[] (has __rsc_ls_ keys)
|
|
8
|
+
*/
|
|
9
|
+
function isTypedLocationState(
|
|
10
|
+
state: unknown,
|
|
11
|
+
): state is Record<string, unknown> {
|
|
12
|
+
if (state === null || typeof state !== "object") return false;
|
|
13
|
+
return Object.keys(state).some((key) => key.startsWith("__rsc_ls_"));
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Resolve navigation state - handles both LocationStateEntry[] and plain formats
|
|
18
|
+
*/
|
|
19
|
+
export function resolveNavigationState(state: unknown): unknown {
|
|
20
|
+
if (
|
|
21
|
+
Array.isArray(state) &&
|
|
22
|
+
state.length > 0 &&
|
|
23
|
+
isLocationStateEntry(state[0])
|
|
24
|
+
) {
|
|
25
|
+
return resolveLocationStateEntries(state);
|
|
26
|
+
}
|
|
27
|
+
return state;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Build history state object from user state
|
|
32
|
+
* - Typed state: spread directly into history.state
|
|
33
|
+
* - Plain state: store in history.state.state
|
|
34
|
+
*/
|
|
35
|
+
export function buildHistoryState(
|
|
36
|
+
userState: unknown,
|
|
37
|
+
routerState?: { intercept?: boolean; sourceUrl?: string },
|
|
38
|
+
serverState?: Record<string, unknown>,
|
|
39
|
+
): Record<string, unknown> | null {
|
|
40
|
+
const result: Record<string, unknown> = {};
|
|
41
|
+
|
|
42
|
+
if (routerState?.intercept) {
|
|
43
|
+
result.intercept = true;
|
|
44
|
+
if (routerState.sourceUrl) {
|
|
45
|
+
result.sourceUrl = routerState.sourceUrl;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (userState !== undefined) {
|
|
50
|
+
if (isTypedLocationState(userState)) {
|
|
51
|
+
Object.assign(result, userState);
|
|
52
|
+
} else {
|
|
53
|
+
result.state = userState;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (serverState) {
|
|
58
|
+
Object.assign(result, serverState);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return Object.keys(result).length > 0 ? result : null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Merge server-set location state into the current history entry.
|
|
66
|
+
* Replaces the current history state and dispatches notification event
|
|
67
|
+
* so useLocationState hooks re-read from history.state.
|
|
68
|
+
*/
|
|
69
|
+
export function mergeLocationState(
|
|
70
|
+
locationState: Record<string, unknown>,
|
|
71
|
+
): void {
|
|
72
|
+
const merged = {
|
|
73
|
+
...window.history.state,
|
|
74
|
+
...locationState,
|
|
75
|
+
};
|
|
76
|
+
window.history.replaceState(merged, "", window.location.href);
|
|
77
|
+
if (Object.keys(locationState).some((k) => k.startsWith("__rsc_ls_"))) {
|
|
78
|
+
window.dispatchEvent(new Event("__rsc_locationstate"));
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { ResolvedSegment } from "./types.js";
|
|
2
|
+
import type { SlotState } from "../types.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Check if a segment is an intercept segment.
|
|
6
|
+
* Intercept segments have namespace starting with "intercept:" — both the
|
|
7
|
+
* parallel container (@modal) and its content children receive this namespace
|
|
8
|
+
* from intercept-resolution.ts. Regular parallel segments like @sidebar do not.
|
|
9
|
+
*/
|
|
10
|
+
export function isInterceptSegment(s: ResolvedSegment): boolean {
|
|
11
|
+
return s.namespace?.startsWith("intercept:") === true;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Split an array of segments into main and intercept groups.
|
|
16
|
+
* Intercept segments are separated for explicit injection into the render tree
|
|
17
|
+
* via the interceptSegments render option.
|
|
18
|
+
*/
|
|
19
|
+
export function splitInterceptSegments(segments: ResolvedSegment[]): {
|
|
20
|
+
main: ResolvedSegment[];
|
|
21
|
+
intercept: ResolvedSegment[];
|
|
22
|
+
} {
|
|
23
|
+
const main: ResolvedSegment[] = [];
|
|
24
|
+
const intercept: ResolvedSegment[] = [];
|
|
25
|
+
for (const s of segments) {
|
|
26
|
+
if (isInterceptSegment(s)) {
|
|
27
|
+
intercept.push(s);
|
|
28
|
+
} else {
|
|
29
|
+
main.push(s);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return { main, intercept };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Check if any slot is currently active (has content to render).
|
|
37
|
+
* Active slots indicate an intercept response where a parallel segment
|
|
38
|
+
* (e.g., @modal) has matched and should be rendered.
|
|
39
|
+
*/
|
|
40
|
+
export function hasActiveIntercept(slots?: Record<string, SlotState>): boolean {
|
|
41
|
+
if (!slots) return false;
|
|
42
|
+
return Object.values(slots).some((slot) => slot.active);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Check if cached segments contain any intercept segments.
|
|
47
|
+
* Intercept caches shouldn't be used for cached SWR rendering since
|
|
48
|
+
* whether interception happens depends on the current page context.
|
|
49
|
+
*/
|
|
50
|
+
export function isInterceptOnlyCache(segments: ResolvedSegment[]): boolean {
|
|
51
|
+
return segments.some(isInterceptSegment);
|
|
52
|
+
}
|
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
import type { LinkInterceptorOptions, NavigateOptions } from "./types.js";
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Check if an anchor points to the same page with only a hash change.
|
|
5
|
+
* Used by both Link component and link-interceptor to let the browser
|
|
6
|
+
* handle anchor scrolling natively.
|
|
7
|
+
*/
|
|
8
|
+
export function isHashOnlyNavigation(anchor: HTMLAnchorElement): boolean {
|
|
9
|
+
return (
|
|
10
|
+
anchor.pathname === window.location.pathname &&
|
|
11
|
+
anchor.search === window.location.search &&
|
|
12
|
+
!!anchor.hash
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
|
|
3
16
|
/**
|
|
4
17
|
* Default link interception predicate
|
|
5
18
|
*
|
|
@@ -44,6 +57,12 @@ export function defaultShouldIntercept(link: HTMLAnchorElement): boolean {
|
|
|
44
57
|
return false;
|
|
45
58
|
}
|
|
46
59
|
|
|
60
|
+
// Don't intercept hash-only navigation (same path, only fragment changes).
|
|
61
|
+
// Let the browser handle anchor scrolling natively.
|
|
62
|
+
if (isHashOnlyNavigation(link)) {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
|
|
47
66
|
return true;
|
|
48
67
|
}
|
|
49
68
|
|
|
@@ -70,7 +89,7 @@ export function defaultShouldIntercept(link: HTMLAnchorElement): boolean {
|
|
|
70
89
|
*/
|
|
71
90
|
export function setupLinkInterception(
|
|
72
91
|
onNavigate: (url: string, options?: NavigateOptions) => void,
|
|
73
|
-
options?: LinkInterceptorOptions
|
|
92
|
+
options?: LinkInterceptorOptions,
|
|
74
93
|
): () => void {
|
|
75
94
|
const shouldIntercept = options?.shouldIntercept ?? defaultShouldIntercept;
|
|
76
95
|
|
|
@@ -98,6 +117,7 @@ export function setupLinkInterception(
|
|
|
98
117
|
// Read navigation options from data attributes (set by Link component)
|
|
99
118
|
const scrollAttr = link.getAttribute("data-scroll");
|
|
100
119
|
const replaceAttr = link.getAttribute("data-replace");
|
|
120
|
+
const revalidateAttr = link.getAttribute("data-revalidate");
|
|
101
121
|
|
|
102
122
|
const navigateOptions: NavigateOptions = {};
|
|
103
123
|
if (scrollAttr === "false") {
|
|
@@ -106,16 +126,16 @@ export function setupLinkInterception(
|
|
|
106
126
|
if (replaceAttr === "true") {
|
|
107
127
|
navigateOptions.replace = true;
|
|
108
128
|
}
|
|
129
|
+
if (revalidateAttr === "false") {
|
|
130
|
+
navigateOptions.revalidate = false;
|
|
131
|
+
}
|
|
109
132
|
|
|
110
133
|
onNavigate(href, navigateOptions);
|
|
111
134
|
};
|
|
112
135
|
|
|
113
136
|
document.addEventListener("click", handleClick);
|
|
114
137
|
|
|
115
|
-
console.log("[Browser] Link interception enabled");
|
|
116
|
-
|
|
117
138
|
return () => {
|
|
118
139
|
document.removeEventListener("click", handleClick);
|
|
119
|
-
console.log("[Browser] Link interception disabled");
|
|
120
140
|
};
|
|
121
141
|
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { INTERNAL_RANGO_DEBUG } from "../internal-debug.js";
|
|
2
|
+
|
|
3
|
+
interface BrowserLogContext {
|
|
4
|
+
requestId: string;
|
|
5
|
+
txId: string;
|
|
6
|
+
operation: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
let txCounter = 0;
|
|
10
|
+
let requestCounter = 0;
|
|
11
|
+
|
|
12
|
+
export function isBrowserDebugEnabled(): boolean {
|
|
13
|
+
return INTERNAL_RANGO_DEBUG;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function nextId(prefix: string, counter: number): string {
|
|
17
|
+
return `${prefix}${counter.toString(36)}`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function startBrowserTransaction(operation: string): BrowserLogContext {
|
|
21
|
+
txCounter += 1;
|
|
22
|
+
requestCounter += 1;
|
|
23
|
+
return {
|
|
24
|
+
operation,
|
|
25
|
+
txId: nextId("ctx-", txCounter),
|
|
26
|
+
requestId: nextId("creq-", requestCounter),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function browserDebugLog(
|
|
31
|
+
ctx: BrowserLogContext,
|
|
32
|
+
message: string,
|
|
33
|
+
details?: Record<string, unknown>,
|
|
34
|
+
): void {
|
|
35
|
+
if (!INTERNAL_RANGO_DEBUG) return;
|
|
36
|
+
|
|
37
|
+
const prefix = `[Browser][req:${ctx.requestId}][tx:${ctx.operation}-${ctx.txId}]`;
|
|
38
|
+
if (details) {
|
|
39
|
+
console.log(`${prefix} ${message}`, details);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
console.log(`${prefix} ${message}`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Simple gated console.log for browser-side debug output.
|
|
48
|
+
* Unlike browserDebugLog, this doesn't require a transaction context -
|
|
49
|
+
* use it for standalone debug messages in partial-update, navigation-bridge, etc.
|
|
50
|
+
*/
|
|
51
|
+
export function debugLog(msg: string, ...args: unknown[]): void {
|
|
52
|
+
if (INTERNAL_RANGO_DEBUG) {
|
|
53
|
+
console.log(msg, ...args);
|
|
54
|
+
}
|
|
55
|
+
}
|