@rangojs/router 0.0.0-experimental.7 → 0.0.0-experimental.71
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +9 -0
- package/README.md +942 -4
- package/dist/bin/rango.js +1689 -0
- package/dist/vite/index.js +4951 -930
- package/package.json +70 -60
- package/skills/breadcrumbs/SKILL.md +250 -0
- package/skills/cache-guide/SKILL.md +294 -0
- package/skills/caching/SKILL.md +93 -23
- package/skills/composability/SKILL.md +172 -0
- package/skills/debug-manifest/SKILL.md +12 -8
- package/skills/document-cache/SKILL.md +18 -16
- package/skills/fonts/SKILL.md +167 -0
- package/skills/hooks/SKILL.md +334 -72
- package/skills/host-router/SKILL.md +218 -0
- package/skills/intercept/SKILL.md +131 -8
- package/skills/layout/SKILL.md +100 -3
- package/skills/links/SKILL.md +92 -31
- package/skills/loader/SKILL.md +404 -44
- package/skills/middleware/SKILL.md +173 -34
- package/skills/mime-routes/SKILL.md +128 -0
- package/skills/parallel/SKILL.md +204 -1
- package/skills/prerender/SKILL.md +685 -0
- package/skills/rango/SKILL.md +85 -16
- package/skills/response-routes/SKILL.md +411 -0
- package/skills/route/SKILL.md +257 -14
- package/skills/router-setup/SKILL.md +210 -32
- package/skills/tailwind/SKILL.md +129 -0
- package/skills/theme/SKILL.md +9 -8
- package/skills/typesafety/SKILL.md +328 -89
- package/skills/use-cache/SKILL.md +324 -0
- package/src/__internal.ts +102 -4
- package/src/bin/rango.ts +321 -0
- package/src/browser/action-coordinator.ts +97 -0
- package/src/browser/action-response-classifier.ts +99 -0
- package/src/browser/app-version.ts +14 -0
- package/src/browser/event-controller.ts +92 -64
- package/src/browser/history-state.ts +80 -0
- package/src/browser/intercept-utils.ts +52 -0
- package/src/browser/link-interceptor.ts +24 -4
- package/src/browser/logging.ts +55 -0
- package/src/browser/merge-segment-loaders.ts +20 -12
- package/src/browser/navigation-bridge.ts +296 -558
- package/src/browser/navigation-client.ts +179 -69
- package/src/browser/navigation-store.ts +73 -55
- package/src/browser/navigation-transaction.ts +297 -0
- package/src/browser/network-error-handler.ts +61 -0
- package/src/browser/partial-update.ts +328 -313
- package/src/browser/prefetch/cache.ts +206 -0
- package/src/browser/prefetch/fetch.ts +150 -0
- package/src/browser/prefetch/observer.ts +65 -0
- package/src/browser/prefetch/policy.ts +48 -0
- package/src/browser/prefetch/queue.ts +160 -0
- package/src/browser/prefetch/resource-ready.ts +77 -0
- package/src/browser/rango-state.ts +112 -0
- package/src/browser/react/Link.tsx +230 -74
- package/src/browser/react/NavigationProvider.tsx +87 -11
- package/src/browser/react/context.ts +11 -0
- package/src/browser/react/filter-segment-order.ts +11 -0
- package/src/browser/react/index.ts +12 -12
- package/src/browser/react/location-state-shared.ts +95 -53
- package/src/browser/react/location-state.ts +60 -15
- package/src/browser/react/mount-context.ts +6 -1
- package/src/browser/react/nonce-context.ts +23 -0
- package/src/browser/react/shallow-equal.ts +27 -0
- package/src/browser/react/use-action.ts +29 -51
- package/src/browser/react/use-client-cache.ts +5 -3
- package/src/browser/react/use-handle.ts +30 -126
- package/src/browser/react/use-href.tsx +2 -2
- package/src/browser/react/use-link-status.ts +6 -5
- package/src/browser/react/use-navigation.ts +22 -63
- package/src/browser/react/use-params.ts +65 -0
- package/src/browser/react/use-pathname.ts +47 -0
- package/src/browser/react/use-router.ts +76 -0
- package/src/browser/react/use-search-params.ts +56 -0
- package/src/browser/react/use-segments.ts +80 -97
- package/src/browser/response-adapter.ts +73 -0
- package/src/browser/rsc-router.tsx +214 -58
- package/src/browser/scroll-restoration.ts +127 -52
- package/src/browser/segment-reconciler.ts +221 -0
- package/src/browser/segment-structure-assert.ts +16 -0
- package/src/browser/server-action-bridge.ts +510 -603
- package/src/browser/shallow.ts +6 -1
- package/src/browser/types.ts +141 -48
- package/src/browser/validate-redirect-origin.ts +29 -0
- package/src/build/generate-manifest.ts +235 -24
- package/src/build/generate-route-types.ts +39 -0
- package/src/build/index.ts +13 -0
- package/src/build/route-trie.ts +265 -0
- package/src/build/route-types/ast-helpers.ts +25 -0
- package/src/build/route-types/ast-route-extraction.ts +98 -0
- package/src/build/route-types/codegen.ts +102 -0
- package/src/build/route-types/include-resolution.ts +418 -0
- package/src/build/route-types/param-extraction.ts +48 -0
- package/src/build/route-types/per-module-writer.ts +128 -0
- package/src/build/route-types/router-processing.ts +618 -0
- package/src/build/route-types/scan-filter.ts +85 -0
- package/src/build/runtime-discovery.ts +231 -0
- package/src/cache/background-task.ts +34 -0
- package/src/cache/cache-key-utils.ts +44 -0
- package/src/cache/cache-policy.ts +125 -0
- package/src/cache/cache-runtime.ts +342 -0
- package/src/cache/cache-scope.ts +167 -309
- package/src/cache/cf/cf-cache-store.ts +571 -17
- package/src/cache/cf/index.ts +13 -3
- package/src/cache/document-cache.ts +116 -77
- package/src/cache/handle-capture.ts +81 -0
- package/src/cache/handle-snapshot.ts +41 -0
- package/src/cache/index.ts +1 -15
- package/src/cache/memory-segment-store.ts +191 -13
- package/src/cache/profile-registry.ts +73 -0
- package/src/cache/read-through-swr.ts +134 -0
- package/src/cache/segment-codec.ts +256 -0
- package/src/cache/taint.ts +153 -0
- package/src/cache/types.ts +72 -122
- package/src/client.rsc.tsx +3 -1
- package/src/client.tsx +105 -179
- package/src/component-utils.ts +4 -4
- package/src/components/DefaultDocument.tsx +5 -1
- package/src/context-var.ts +156 -0
- package/src/debug.ts +19 -9
- package/src/errors.ts +108 -2
- package/src/handle.ts +55 -29
- package/src/handles/MetaTags.tsx +73 -20
- package/src/handles/breadcrumbs.ts +66 -0
- package/src/handles/index.ts +1 -0
- package/src/handles/meta.ts +30 -13
- package/src/host/cookie-handler.ts +21 -15
- package/src/host/errors.ts +8 -8
- package/src/host/index.ts +4 -7
- package/src/host/pattern-matcher.ts +27 -27
- package/src/host/router.ts +61 -39
- package/src/host/testing.ts +8 -8
- package/src/host/types.ts +15 -7
- package/src/host/utils.ts +1 -1
- package/src/href-client.ts +119 -29
- package/src/index.rsc.ts +155 -19
- package/src/index.ts +223 -30
- package/src/internal-debug.ts +11 -0
- package/src/loader.rsc.ts +26 -157
- package/src/loader.ts +27 -10
- package/src/network-error-thrower.tsx +3 -1
- package/src/outlet-provider.tsx +45 -0
- package/src/prerender/param-hash.ts +37 -0
- package/src/prerender/store.ts +186 -0
- package/src/prerender.ts +524 -0
- package/src/reverse.ts +351 -0
- package/src/root-error-boundary.tsx +41 -29
- package/src/route-content-wrapper.tsx +7 -4
- package/src/route-definition/dsl-helpers.ts +982 -0
- package/src/route-definition/helper-factories.ts +200 -0
- package/src/route-definition/helpers-types.ts +434 -0
- package/src/route-definition/index.ts +55 -0
- package/src/route-definition/redirect.ts +101 -0
- package/src/route-definition/resolve-handler-use.ts +149 -0
- package/src/route-definition.ts +1 -1428
- package/src/route-map-builder.ts +217 -123
- package/src/route-name.ts +53 -0
- package/src/route-types.ts +70 -8
- package/src/router/content-negotiation.ts +215 -0
- package/src/router/debug-manifest.ts +72 -0
- package/src/router/error-handling.ts +9 -9
- package/src/router/find-match.ts +160 -0
- package/src/router/handler-context.ts +435 -86
- package/src/router/intercept-resolution.ts +402 -0
- package/src/router/lazy-includes.ts +237 -0
- package/src/router/loader-resolution.ts +356 -128
- package/src/router/logging.ts +251 -0
- package/src/router/manifest.ts +154 -35
- package/src/router/match-api.ts +555 -0
- package/src/router/match-context.ts +5 -3
- package/src/router/match-handlers.ts +440 -0
- package/src/router/match-middleware/background-revalidation.ts +108 -93
- package/src/router/match-middleware/cache-lookup.ts +459 -10
- package/src/router/match-middleware/cache-store.ts +98 -26
- package/src/router/match-middleware/intercept-resolution.ts +57 -17
- package/src/router/match-middleware/segment-resolution.ts +80 -6
- package/src/router/match-pipelines.ts +10 -45
- package/src/router/match-result.ts +135 -35
- package/src/router/metrics.ts +240 -15
- package/src/router/middleware-cookies.ts +55 -0
- package/src/router/middleware-types.ts +220 -0
- package/src/router/middleware.ts +324 -369
- package/src/router/navigation-snapshot.ts +182 -0
- package/src/router/pattern-matching.ts +211 -43
- package/src/router/prerender-match.ts +502 -0
- package/src/router/preview-match.ts +98 -0
- package/src/router/request-classification.ts +310 -0
- package/src/router/revalidation.ts +137 -38
- package/src/router/route-snapshot.ts +245 -0
- package/src/router/router-context.ts +41 -21
- package/src/router/router-interfaces.ts +484 -0
- package/src/router/router-options.ts +618 -0
- package/src/router/router-registry.ts +24 -0
- package/src/router/segment-resolution/fresh.ts +748 -0
- package/src/router/segment-resolution/helpers.ts +268 -0
- package/src/router/segment-resolution/loader-cache.ts +199 -0
- package/src/router/segment-resolution/revalidation.ts +1379 -0
- package/src/router/segment-resolution/static-store.ts +67 -0
- package/src/router/segment-resolution.ts +21 -0
- package/src/router/segment-wrappers.ts +291 -0
- package/src/router/telemetry-otel.ts +299 -0
- package/src/router/telemetry.ts +300 -0
- package/src/router/timeout.ts +148 -0
- package/src/router/trie-matching.ts +239 -0
- package/src/router/types.ts +78 -3
- package/src/router.ts +740 -4252
- package/src/rsc/handler-context.ts +45 -0
- package/src/rsc/handler.ts +907 -797
- package/src/rsc/helpers.ts +140 -6
- package/src/rsc/index.ts +0 -20
- package/src/rsc/loader-fetch.ts +229 -0
- package/src/rsc/manifest-init.ts +90 -0
- package/src/rsc/nonce.ts +14 -0
- package/src/rsc/origin-guard.ts +141 -0
- package/src/rsc/progressive-enhancement.ts +391 -0
- package/src/rsc/response-error.ts +37 -0
- package/src/rsc/response-route-handler.ts +347 -0
- package/src/rsc/rsc-rendering.ts +246 -0
- package/src/rsc/runtime-warnings.ts +42 -0
- package/src/rsc/server-action.ts +356 -0
- package/src/rsc/ssr-setup.ts +128 -0
- package/src/rsc/types.ts +46 -11
- package/src/search-params.ts +230 -0
- package/src/segment-system.tsx +165 -17
- package/src/server/context.ts +315 -58
- package/src/server/cookie-store.ts +190 -0
- package/src/server/fetchable-loader-store.ts +37 -0
- package/src/server/handle-store.ts +113 -15
- package/src/server/loader-registry.ts +24 -64
- package/src/server/request-context.ts +607 -81
- package/src/server.ts +35 -130
- package/src/ssr/index.tsx +103 -30
- package/src/static-handler.ts +126 -0
- package/src/theme/ThemeProvider.tsx +21 -15
- package/src/theme/ThemeScript.tsx +5 -5
- package/src/theme/constants.ts +5 -2
- package/src/theme/index.ts +4 -14
- package/src/theme/theme-context.ts +4 -30
- package/src/theme/theme-script.ts +21 -18
- package/src/types/boundaries.ts +158 -0
- package/src/types/cache-types.ts +198 -0
- package/src/types/error-types.ts +192 -0
- package/src/types/global-namespace.ts +100 -0
- package/src/types/handler-context.ts +791 -0
- package/src/types/index.ts +88 -0
- package/src/types/loader-types.ts +210 -0
- package/src/types/route-config.ts +170 -0
- package/src/types/route-entry.ts +109 -0
- package/src/types/segments.ts +151 -0
- package/src/types.ts +1 -1623
- package/src/urls/include-helper.ts +197 -0
- package/src/urls/index.ts +53 -0
- package/src/urls/path-helper-types.ts +346 -0
- package/src/urls/path-helper.ts +364 -0
- package/src/urls/pattern-types.ts +107 -0
- package/src/urls/response-types.ts +116 -0
- package/src/urls/type-extraction.ts +372 -0
- package/src/urls/urls-function.ts +98 -0
- package/src/urls.ts +1 -802
- package/src/use-loader.tsx +161 -81
- package/src/vite/discovery/bundle-postprocess.ts +181 -0
- package/src/vite/discovery/discover-routers.ts +348 -0
- package/src/vite/discovery/prerender-collection.ts +439 -0
- package/src/vite/discovery/route-types-writer.ts +258 -0
- package/src/vite/discovery/self-gen-tracking.ts +47 -0
- package/src/vite/discovery/state.ts +117 -0
- package/src/vite/discovery/virtual-module-codegen.ts +203 -0
- package/src/vite/index.ts +15 -1129
- package/src/vite/plugin-types.ts +103 -0
- package/src/vite/plugins/cjs-to-esm.ts +93 -0
- package/src/vite/plugins/client-ref-dedup.ts +115 -0
- package/src/vite/plugins/client-ref-hashing.ts +105 -0
- package/src/vite/{expose-action-id.ts → plugins/expose-action-id.ts} +72 -53
- package/src/vite/plugins/expose-id-utils.ts +299 -0
- package/src/vite/plugins/expose-ids/export-analysis.ts +296 -0
- package/src/vite/plugins/expose-ids/handler-transform.ts +209 -0
- package/src/vite/plugins/expose-ids/loader-transform.ts +74 -0
- package/src/vite/plugins/expose-ids/router-transform.ts +110 -0
- package/src/vite/plugins/expose-ids/types.ts +45 -0
- package/src/vite/plugins/expose-internal-ids.ts +786 -0
- package/src/vite/plugins/performance-tracks.ts +88 -0
- package/src/vite/plugins/refresh-cmd.ts +127 -0
- package/src/vite/plugins/use-cache-transform.ts +323 -0
- package/src/vite/plugins/version-injector.ts +83 -0
- package/src/vite/plugins/version-plugin.ts +266 -0
- package/src/vite/{virtual-entries.ts → plugins/virtual-entries.ts} +23 -14
- package/src/vite/plugins/virtual-stub-plugin.ts +29 -0
- package/src/vite/rango.ts +462 -0
- package/src/vite/router-discovery.ts +918 -0
- package/src/vite/utils/ast-handler-extract.ts +517 -0
- package/src/vite/utils/banner.ts +36 -0
- package/src/vite/utils/bundle-analysis.ts +137 -0
- package/src/vite/utils/manifest-utils.ts +70 -0
- package/src/vite/{package-resolution.ts → utils/package-resolution.ts} +25 -29
- package/src/vite/utils/prerender-utils.ts +207 -0
- package/src/vite/utils/shared-utils.ts +170 -0
- package/CLAUDE.md +0 -43
- package/src/browser/lru-cache.ts +0 -69
- package/src/browser/request-controller.ts +0 -164
- package/src/cache/memory-store.ts +0 -253
- package/src/href-context.ts +0 -33
- package/src/href.ts +0 -255
- package/src/server/route-manifest-cache.ts +0 -173
- package/src/vite/expose-handle-id.ts +0 -209
- package/src/vite/expose-loader-id.ts +0 -426
- package/src/vite/expose-location-state-id.ts +0 -177
- /package/src/vite/{version.d.ts → plugins/version.d.ts} +0 -0
|
@@ -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) */
|
|
@@ -116,15 +120,6 @@ export interface HandleState {
|
|
|
116
120
|
segmentOrder: string[];
|
|
117
121
|
}
|
|
118
122
|
|
|
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;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
123
|
/**
|
|
129
124
|
* Result from starting a navigation
|
|
130
125
|
* Implements Disposable for use with `using` keyword
|
|
@@ -165,8 +160,8 @@ export interface ActionHandle extends Disposable {
|
|
|
165
160
|
readonly settled: boolean;
|
|
166
161
|
/** Check if any concurrent actions were started */
|
|
167
162
|
hadConcurrentActions: boolean;
|
|
168
|
-
/** Get
|
|
169
|
-
|
|
163
|
+
/** Get raw set of segments revalidated by concurrent actions */
|
|
164
|
+
getRevalidatedSegments(): Set<string>;
|
|
170
165
|
/** Clear consolidation tracking */
|
|
171
166
|
clearConsolidation(): void;
|
|
172
167
|
}
|
|
@@ -176,7 +171,10 @@ export interface ActionHandle extends Disposable {
|
|
|
176
171
|
*/
|
|
177
172
|
export interface EventController {
|
|
178
173
|
// Navigation operations
|
|
179
|
-
startNavigation(
|
|
174
|
+
startNavigation(
|
|
175
|
+
url: string,
|
|
176
|
+
options?: NavigateOptions & { skipLoadingState?: boolean },
|
|
177
|
+
): NavigationHandle;
|
|
180
178
|
abortNavigation(): void;
|
|
181
179
|
|
|
182
180
|
// Action operations
|
|
@@ -186,6 +184,7 @@ export interface EventController {
|
|
|
186
184
|
// State access
|
|
187
185
|
getState(): DerivedNavigationState;
|
|
188
186
|
getActionState(actionId: string): TrackedActionState;
|
|
187
|
+
getLocation(): NavigationLocation;
|
|
189
188
|
|
|
190
189
|
// Location updates (for popstate where navigation doesn't go through startNavigation)
|
|
191
190
|
setLocation(location: NavigationLocation): void;
|
|
@@ -194,7 +193,7 @@ export interface EventController {
|
|
|
194
193
|
subscribe(listener: StateListener): () => void;
|
|
195
194
|
subscribeToAction(
|
|
196
195
|
actionId: string,
|
|
197
|
-
listener: ActionStateListener
|
|
196
|
+
listener: ActionStateListener,
|
|
198
197
|
): () => void;
|
|
199
198
|
subscribeToHandles(listener: HandleListener): () => void;
|
|
200
199
|
|
|
@@ -202,13 +201,19 @@ export interface EventController {
|
|
|
202
201
|
setHandleData(
|
|
203
202
|
data: HandleData,
|
|
204
203
|
matched?: string[],
|
|
205
|
-
isPartial?: boolean
|
|
204
|
+
isPartial?: boolean,
|
|
206
205
|
): void;
|
|
207
206
|
getHandleState(): HandleState;
|
|
208
207
|
|
|
208
|
+
// Params operations
|
|
209
|
+
setParams(params: Record<string, string>): void;
|
|
210
|
+
getParams(): Record<string, string>;
|
|
211
|
+
|
|
209
212
|
// Direct state access for advanced use
|
|
210
213
|
getCurrentNavigation(): NavigationEntry | null;
|
|
211
214
|
getInflightActions(): Map<string, ActionEntry>;
|
|
215
|
+
/** Whether any concurrent actions have occurred (shared across all handles) */
|
|
216
|
+
hadAnyConcurrentActions(): boolean;
|
|
212
217
|
}
|
|
213
218
|
|
|
214
219
|
// ============================================================================
|
|
@@ -230,7 +235,10 @@ const DEFAULT_ACTION_STATE: TrackedActionState = {
|
|
|
230
235
|
* When subscriptionId has no '#', it's just an action name and matches by suffix.
|
|
231
236
|
* This allows useAction("addToCart") to match "hash#addToCart" or "src/file.ts#addToCart".
|
|
232
237
|
*/
|
|
233
|
-
function matchesActionId(
|
|
238
|
+
function matchesActionId(
|
|
239
|
+
subscriptionId: string,
|
|
240
|
+
entryActionId: string,
|
|
241
|
+
): boolean {
|
|
234
242
|
if (subscriptionId.includes("#")) {
|
|
235
243
|
// Full ID: exact match
|
|
236
244
|
return subscriptionId === entryActionId;
|
|
@@ -261,7 +269,7 @@ export interface EventControllerConfig {
|
|
|
261
269
|
* Actions use mergeMap semantics (all run concurrently, consolidate at end).
|
|
262
270
|
*/
|
|
263
271
|
export function createEventController(
|
|
264
|
-
config?: EventControllerConfig
|
|
272
|
+
config?: EventControllerConfig,
|
|
265
273
|
): EventController {
|
|
266
274
|
// ========================================================================
|
|
267
275
|
// Source of Truth
|
|
@@ -293,6 +301,9 @@ export function createEventController(
|
|
|
293
301
|
let handleData: HandleData = {};
|
|
294
302
|
let handleSegmentOrder: string[] = [];
|
|
295
303
|
|
|
304
|
+
// Merged route params from current match
|
|
305
|
+
let routeParams: Record<string, string> = {};
|
|
306
|
+
|
|
296
307
|
// ========================================================================
|
|
297
308
|
// Listeners
|
|
298
309
|
// ========================================================================
|
|
@@ -334,7 +345,7 @@ export function createEventController(
|
|
|
334
345
|
listeners.forEach((listener) => listener(state));
|
|
335
346
|
}
|
|
336
347
|
}
|
|
337
|
-
}, 0)
|
|
348
|
+
}, 0),
|
|
338
349
|
);
|
|
339
350
|
}
|
|
340
351
|
|
|
@@ -367,9 +378,12 @@ export function createEventController(
|
|
|
367
378
|
}));
|
|
368
379
|
|
|
369
380
|
// State: loading if navigation OR actions are in progress
|
|
381
|
+
// Background revalidations (skipLoadingState) don't affect visible state
|
|
370
382
|
const hasActiveActions = inflightActionsList.length > 0;
|
|
371
|
-
const
|
|
372
|
-
currentNavigation !== null
|
|
383
|
+
const isVisibleNavigation =
|
|
384
|
+
currentNavigation !== null &&
|
|
385
|
+
!currentNavigation.options?.skipLoadingState;
|
|
386
|
+
const state = isVisibleNavigation || hasActiveActions ? "loading" : "idle";
|
|
373
387
|
|
|
374
388
|
// Streaming: true if any active streams (navigation or action) or loading
|
|
375
389
|
const isStreaming = activeStreamCount > 0 || state === "loading";
|
|
@@ -377,9 +391,17 @@ export function createEventController(
|
|
|
377
391
|
return {
|
|
378
392
|
state,
|
|
379
393
|
isStreaming,
|
|
394
|
+
// True when a navigation is active (fetching or streaming, before
|
|
395
|
+
// commit). Broader than pendingUrl which clears during streaming.
|
|
396
|
+
isNavigating: currentNavigation !== null,
|
|
380
397
|
location,
|
|
381
|
-
// pendingUrl only during fetching phase - once streaming starts (URL changed), not pending
|
|
382
|
-
|
|
398
|
+
// pendingUrl only during fetching phase - once streaming starts (URL changed), not pending.
|
|
399
|
+
// Background revalidations (skipLoadingState) don't expose a pending URL.
|
|
400
|
+
pendingUrl:
|
|
401
|
+
currentNavigation?.phase === "fetching" &&
|
|
402
|
+
!currentNavigation.options?.skipLoadingState
|
|
403
|
+
? currentNavigation.url
|
|
404
|
+
: null,
|
|
383
405
|
inflightActions: inflightActionsList,
|
|
384
406
|
};
|
|
385
407
|
}
|
|
@@ -388,12 +410,16 @@ export function createEventController(
|
|
|
388
410
|
// Find the most recent action with this ID that's not settling
|
|
389
411
|
// Uses suffix matching when actionId is just a name (no #)
|
|
390
412
|
const activeEntry = [...inflightActions.values()]
|
|
391
|
-
.filter(
|
|
413
|
+
.filter(
|
|
414
|
+
(a) => matchesActionId(actionId, a.actionId) && a.phase !== "settling",
|
|
415
|
+
)
|
|
392
416
|
.sort((a, b) => b.startedAt - a.startedAt)[0];
|
|
393
417
|
|
|
394
418
|
// Also check for settling entries to get result/error
|
|
395
419
|
const settlingEntry = [...inflightActions.values()]
|
|
396
|
-
.filter(
|
|
420
|
+
.filter(
|
|
421
|
+
(a) => matchesActionId(actionId, a.actionId) && a.phase === "settling",
|
|
422
|
+
)
|
|
397
423
|
.sort((a, b) => b.startedAt - a.startedAt)[0];
|
|
398
424
|
|
|
399
425
|
const entry = activeEntry || settlingEntry;
|
|
@@ -431,7 +457,7 @@ export function createEventController(
|
|
|
431
457
|
|
|
432
458
|
function startNavigation(
|
|
433
459
|
url: string,
|
|
434
|
-
options?: NavigateOptions
|
|
460
|
+
options?: NavigateOptions & { skipLoadingState?: boolean },
|
|
435
461
|
): NavigationHandle {
|
|
436
462
|
// Cancel existing navigation (switchMap semantics)
|
|
437
463
|
if (currentNavigation) {
|
|
@@ -463,6 +489,7 @@ export function createEventController(
|
|
|
463
489
|
|
|
464
490
|
startStreaming(): StreamingToken {
|
|
465
491
|
let ended = false;
|
|
492
|
+
entry.phase = "streaming";
|
|
466
493
|
activeStreamCount++;
|
|
467
494
|
notify();
|
|
468
495
|
|
|
@@ -650,24 +677,8 @@ export function createEventController(
|
|
|
650
677
|
// If streaming is in progress, tryFinalize() will be called when streaming ends
|
|
651
678
|
},
|
|
652
679
|
|
|
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);
|
|
680
|
+
getRevalidatedSegments(): Set<string> {
|
|
681
|
+
return concurrentRevalidatedSegments;
|
|
671
682
|
},
|
|
672
683
|
|
|
673
684
|
clearConsolidation() {
|
|
@@ -702,16 +713,26 @@ export function createEventController(
|
|
|
702
713
|
}
|
|
703
714
|
|
|
704
715
|
function abortAllActions() {
|
|
705
|
-
for (const entry of inflightActions
|
|
716
|
+
for (const [id, entry] of inflightActions) {
|
|
717
|
+
// Preserve settling entries — they have already been handled by
|
|
718
|
+
// fail()/complete() and will self-cleanup via the settlement timeout.
|
|
719
|
+
// Clearing them here would prevent debounced notifications from
|
|
720
|
+
// delivering the error/result state to subscribers.
|
|
721
|
+
if (entry.phase === "settling") continue;
|
|
706
722
|
entry.abort.abort();
|
|
723
|
+
inflightActions.delete(id);
|
|
707
724
|
}
|
|
708
|
-
inflightActions.clear();
|
|
709
725
|
hadAnyConcurrentActions = false;
|
|
710
726
|
concurrentRevalidatedSegments.clear();
|
|
711
727
|
notify();
|
|
712
|
-
// Notify all action listeners
|
|
713
|
-
|
|
714
|
-
|
|
728
|
+
// Notify all action listeners directly by subscription ID.
|
|
729
|
+
// actionListeners keys are subscription IDs (possibly short names like
|
|
730
|
+
// "addToCart"), not full entry actionIds. Passing them to notifyAction
|
|
731
|
+
// would fail the suffix matcher — instead, notify each subscriber with
|
|
732
|
+
// its own state.
|
|
733
|
+
for (const [subscriptionId, listeners] of actionListeners) {
|
|
734
|
+
const state = getActionState(subscriptionId);
|
|
735
|
+
listeners.forEach((listener) => listener(state));
|
|
715
736
|
}
|
|
716
737
|
}
|
|
717
738
|
|
|
@@ -719,22 +740,10 @@ export function createEventController(
|
|
|
719
740
|
// Handle Operations
|
|
720
741
|
// ========================================================================
|
|
721
742
|
|
|
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
743
|
function setHandleData(
|
|
735
744
|
data: HandleData,
|
|
736
745
|
matched?: string[],
|
|
737
|
-
isPartial?: boolean
|
|
746
|
+
isPartial?: boolean,
|
|
738
747
|
): void {
|
|
739
748
|
const newSegmentOrder = filterSegmentOrder(matched ?? []);
|
|
740
749
|
|
|
@@ -783,7 +792,7 @@ export function createEventController(
|
|
|
783
792
|
|
|
784
793
|
function subscribeToAction(
|
|
785
794
|
actionId: string,
|
|
786
|
-
listener: ActionStateListener
|
|
795
|
+
listener: ActionStateListener,
|
|
787
796
|
): () => void {
|
|
788
797
|
let listeners = actionListeners.get(actionId);
|
|
789
798
|
if (!listeners) {
|
|
@@ -805,6 +814,19 @@ export function createEventController(
|
|
|
805
814
|
return () => handleListeners.delete(listener);
|
|
806
815
|
}
|
|
807
816
|
|
|
817
|
+
// ========================================================================
|
|
818
|
+
// Params Operations
|
|
819
|
+
// ========================================================================
|
|
820
|
+
|
|
821
|
+
function setParams(params: Record<string, string>): void {
|
|
822
|
+
routeParams = params;
|
|
823
|
+
notify();
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
function getParams(): Record<string, string> {
|
|
827
|
+
return routeParams;
|
|
828
|
+
}
|
|
829
|
+
|
|
808
830
|
// ========================================================================
|
|
809
831
|
// Return Controller
|
|
810
832
|
// ========================================================================
|
|
@@ -821,12 +843,17 @@ export function createEventController(
|
|
|
821
843
|
// State
|
|
822
844
|
getState,
|
|
823
845
|
getActionState,
|
|
846
|
+
getLocation: () => location,
|
|
824
847
|
setLocation,
|
|
825
848
|
|
|
826
849
|
// Handles
|
|
827
850
|
setHandleData,
|
|
828
851
|
getHandleState,
|
|
829
852
|
|
|
853
|
+
// Params
|
|
854
|
+
setParams,
|
|
855
|
+
getParams,
|
|
856
|
+
|
|
830
857
|
// Subscriptions
|
|
831
858
|
subscribe,
|
|
832
859
|
subscribeToAction,
|
|
@@ -835,6 +862,7 @@ export function createEventController(
|
|
|
835
862
|
// Direct access
|
|
836
863
|
getCurrentNavigation: () => currentNavigation,
|
|
837
864
|
getInflightActions: () => inflightActions,
|
|
865
|
+
hadAnyConcurrentActions: () => hadAnyConcurrentActions,
|
|
838
866
|
};
|
|
839
867
|
}
|
|
840
868
|
|
|
@@ -848,7 +876,7 @@ let controllerInstance: EventController | null = null;
|
|
|
848
876
|
* Initialize the global event controller
|
|
849
877
|
*/
|
|
850
878
|
export function initEventController(
|
|
851
|
-
config?: EventControllerConfig
|
|
879
|
+
config?: EventControllerConfig,
|
|
852
880
|
): EventController {
|
|
853
881
|
if (!controllerInstance) {
|
|
854
882
|
controllerInstance = createEventController(config);
|
|
@@ -862,7 +890,7 @@ export function initEventController(
|
|
|
862
890
|
export function getEventController(): EventController {
|
|
863
891
|
if (!controllerInstance) {
|
|
864
892
|
throw new Error(
|
|
865
|
-
"Event controller not initialized. Call initEventController first."
|
|
893
|
+
"Event controller not initialized. Call initEventController first.",
|
|
866
894
|
);
|
|
867
895
|
}
|
|
868
896
|
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
|
+
}
|