@rangojs/router 0.0.0-experimental.8 → 0.0.0-experimental.81

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.
Files changed (316) hide show
  1. package/AGENTS.md +9 -0
  2. package/README.md +942 -4
  3. package/dist/bin/rango.js +1689 -0
  4. package/dist/vite/index.js +5091 -941
  5. package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  6. package/package.json +61 -52
  7. package/skills/breadcrumbs/SKILL.md +250 -0
  8. package/skills/cache-guide/SKILL.md +294 -0
  9. package/skills/caching/SKILL.md +93 -23
  10. package/skills/composability/SKILL.md +172 -0
  11. package/skills/debug-manifest/SKILL.md +12 -8
  12. package/skills/document-cache/SKILL.md +18 -16
  13. package/skills/fonts/SKILL.md +167 -0
  14. package/skills/handler-use/SKILL.md +362 -0
  15. package/skills/hooks/SKILL.md +340 -72
  16. package/skills/host-router/SKILL.md +218 -0
  17. package/skills/intercept/SKILL.md +151 -8
  18. package/skills/layout/SKILL.md +122 -3
  19. package/skills/links/SKILL.md +92 -31
  20. package/skills/loader/SKILL.md +404 -44
  21. package/skills/middleware/SKILL.md +205 -37
  22. package/skills/migrate-nextjs/SKILL.md +560 -0
  23. package/skills/migrate-react-router/SKILL.md +765 -0
  24. package/skills/mime-routes/SKILL.md +128 -0
  25. package/skills/parallel/SKILL.md +263 -1
  26. package/skills/prerender/SKILL.md +685 -0
  27. package/skills/rango/SKILL.md +87 -16
  28. package/skills/response-routes/SKILL.md +411 -0
  29. package/skills/route/SKILL.md +281 -14
  30. package/skills/router-setup/SKILL.md +210 -32
  31. package/skills/tailwind/SKILL.md +129 -0
  32. package/skills/theme/SKILL.md +9 -8
  33. package/skills/typesafety/SKILL.md +328 -89
  34. package/skills/use-cache/SKILL.md +324 -0
  35. package/src/__internal.ts +102 -4
  36. package/src/bin/rango.ts +321 -0
  37. package/src/browser/action-coordinator.ts +97 -0
  38. package/src/browser/action-response-classifier.ts +99 -0
  39. package/src/browser/app-version.ts +14 -0
  40. package/src/browser/event-controller.ts +92 -64
  41. package/src/browser/history-state.ts +80 -0
  42. package/src/browser/intercept-utils.ts +52 -0
  43. package/src/browser/link-interceptor.ts +24 -4
  44. package/src/browser/logging.ts +55 -0
  45. package/src/browser/merge-segment-loaders.ts +20 -12
  46. package/src/browser/navigation-bridge.ts +317 -560
  47. package/src/browser/navigation-client.ts +206 -68
  48. package/src/browser/navigation-store.ts +73 -55
  49. package/src/browser/navigation-transaction.ts +297 -0
  50. package/src/browser/network-error-handler.ts +61 -0
  51. package/src/browser/partial-update.ts +343 -316
  52. package/src/browser/prefetch/cache.ts +216 -0
  53. package/src/browser/prefetch/fetch.ts +206 -0
  54. package/src/browser/prefetch/observer.ts +65 -0
  55. package/src/browser/prefetch/policy.ts +48 -0
  56. package/src/browser/prefetch/queue.ts +160 -0
  57. package/src/browser/prefetch/resource-ready.ts +77 -0
  58. package/src/browser/rango-state.ts +112 -0
  59. package/src/browser/react/Link.tsx +253 -74
  60. package/src/browser/react/NavigationProvider.tsx +91 -11
  61. package/src/browser/react/context.ts +11 -0
  62. package/src/browser/react/filter-segment-order.ts +11 -0
  63. package/src/browser/react/index.ts +12 -12
  64. package/src/browser/react/location-state-shared.ts +95 -53
  65. package/src/browser/react/location-state.ts +60 -15
  66. package/src/browser/react/mount-context.ts +6 -1
  67. package/src/browser/react/nonce-context.ts +23 -0
  68. package/src/browser/react/shallow-equal.ts +27 -0
  69. package/src/browser/react/use-action.ts +29 -51
  70. package/src/browser/react/use-client-cache.ts +5 -3
  71. package/src/browser/react/use-handle.ts +30 -126
  72. package/src/browser/react/use-href.tsx +2 -2
  73. package/src/browser/react/use-link-status.ts +6 -5
  74. package/src/browser/react/use-navigation.ts +44 -65
  75. package/src/browser/react/use-params.ts +75 -0
  76. package/src/browser/react/use-pathname.ts +47 -0
  77. package/src/browser/react/use-router.ts +76 -0
  78. package/src/browser/react/use-search-params.ts +56 -0
  79. package/src/browser/react/use-segments.ts +80 -97
  80. package/src/browser/response-adapter.ts +73 -0
  81. package/src/browser/rsc-router.tsx +214 -58
  82. package/src/browser/scroll-restoration.ts +127 -52
  83. package/src/browser/segment-reconciler.ts +243 -0
  84. package/src/browser/segment-structure-assert.ts +16 -0
  85. package/src/browser/server-action-bridge.ts +510 -603
  86. package/src/browser/shallow.ts +6 -1
  87. package/src/browser/types.ts +141 -48
  88. package/src/browser/validate-redirect-origin.ts +29 -0
  89. package/src/build/generate-manifest.ts +235 -24
  90. package/src/build/generate-route-types.ts +39 -0
  91. package/src/build/index.ts +13 -0
  92. package/src/build/route-trie.ts +291 -0
  93. package/src/build/route-types/ast-helpers.ts +25 -0
  94. package/src/build/route-types/ast-route-extraction.ts +98 -0
  95. package/src/build/route-types/codegen.ts +102 -0
  96. package/src/build/route-types/include-resolution.ts +418 -0
  97. package/src/build/route-types/param-extraction.ts +48 -0
  98. package/src/build/route-types/per-module-writer.ts +128 -0
  99. package/src/build/route-types/router-processing.ts +618 -0
  100. package/src/build/route-types/scan-filter.ts +85 -0
  101. package/src/build/runtime-discovery.ts +231 -0
  102. package/src/cache/background-task.ts +34 -0
  103. package/src/cache/cache-key-utils.ts +44 -0
  104. package/src/cache/cache-policy.ts +125 -0
  105. package/src/cache/cache-runtime.ts +342 -0
  106. package/src/cache/cache-scope.ts +167 -309
  107. package/src/cache/cf/cf-cache-store.ts +571 -17
  108. package/src/cache/cf/index.ts +13 -3
  109. package/src/cache/document-cache.ts +116 -77
  110. package/src/cache/handle-capture.ts +81 -0
  111. package/src/cache/handle-snapshot.ts +41 -0
  112. package/src/cache/index.ts +1 -15
  113. package/src/cache/memory-segment-store.ts +191 -13
  114. package/src/cache/profile-registry.ts +73 -0
  115. package/src/cache/read-through-swr.ts +134 -0
  116. package/src/cache/segment-codec.ts +256 -0
  117. package/src/cache/taint.ts +153 -0
  118. package/src/cache/types.ts +72 -122
  119. package/src/client.rsc.tsx +3 -1
  120. package/src/client.tsx +135 -301
  121. package/src/component-utils.ts +4 -4
  122. package/src/components/DefaultDocument.tsx +5 -1
  123. package/src/context-var.ts +156 -0
  124. package/src/debug.ts +19 -9
  125. package/src/errors.ts +108 -2
  126. package/src/handle.ts +55 -29
  127. package/src/handles/MetaTags.tsx +73 -20
  128. package/src/handles/breadcrumbs.ts +66 -0
  129. package/src/handles/index.ts +1 -0
  130. package/src/handles/meta.ts +30 -13
  131. package/src/host/cookie-handler.ts +21 -15
  132. package/src/host/errors.ts +8 -8
  133. package/src/host/index.ts +4 -7
  134. package/src/host/pattern-matcher.ts +27 -27
  135. package/src/host/router.ts +61 -39
  136. package/src/host/testing.ts +8 -8
  137. package/src/host/types.ts +15 -7
  138. package/src/host/utils.ts +1 -1
  139. package/src/href-client.ts +119 -29
  140. package/src/index.rsc.ts +155 -19
  141. package/src/index.ts +251 -30
  142. package/src/internal-debug.ts +11 -0
  143. package/src/loader.rsc.ts +26 -157
  144. package/src/loader.ts +27 -10
  145. package/src/network-error-thrower.tsx +3 -1
  146. package/src/outlet-provider.tsx +45 -0
  147. package/src/prerender/param-hash.ts +37 -0
  148. package/src/prerender/store.ts +186 -0
  149. package/src/prerender.ts +524 -0
  150. package/src/reverse.ts +354 -0
  151. package/src/root-error-boundary.tsx +41 -29
  152. package/src/route-content-wrapper.tsx +7 -4
  153. package/src/route-definition/dsl-helpers.ts +1121 -0
  154. package/src/route-definition/helper-factories.ts +200 -0
  155. package/src/route-definition/helpers-types.ts +478 -0
  156. package/src/route-definition/index.ts +55 -0
  157. package/src/route-definition/redirect.ts +101 -0
  158. package/src/route-definition/resolve-handler-use.ts +149 -0
  159. package/src/route-definition.ts +1 -1428
  160. package/src/route-map-builder.ts +217 -123
  161. package/src/route-name.ts +53 -0
  162. package/src/route-types.ts +77 -8
  163. package/src/router/content-negotiation.ts +215 -0
  164. package/src/router/debug-manifest.ts +72 -0
  165. package/src/router/error-handling.ts +9 -9
  166. package/src/router/find-match.ts +160 -0
  167. package/src/router/handler-context.ts +438 -86
  168. package/src/router/intercept-resolution.ts +402 -0
  169. package/src/router/lazy-includes.ts +237 -0
  170. package/src/router/loader-resolution.ts +356 -128
  171. package/src/router/logging.ts +251 -0
  172. package/src/router/manifest.ts +163 -35
  173. package/src/router/match-api.ts +555 -0
  174. package/src/router/match-context.ts +5 -3
  175. package/src/router/match-handlers.ts +440 -0
  176. package/src/router/match-middleware/background-revalidation.ts +108 -93
  177. package/src/router/match-middleware/cache-lookup.ts +460 -10
  178. package/src/router/match-middleware/cache-store.ts +98 -26
  179. package/src/router/match-middleware/intercept-resolution.ts +57 -17
  180. package/src/router/match-middleware/segment-resolution.ts +80 -6
  181. package/src/router/match-pipelines.ts +10 -45
  182. package/src/router/match-result.ts +135 -35
  183. package/src/router/metrics.ts +240 -15
  184. package/src/router/middleware-cookies.ts +55 -0
  185. package/src/router/middleware-types.ts +220 -0
  186. package/src/router/middleware.ts +324 -369
  187. package/src/router/navigation-snapshot.ts +182 -0
  188. package/src/router/pattern-matching.ts +211 -43
  189. package/src/router/prerender-match.ts +502 -0
  190. package/src/router/preview-match.ts +98 -0
  191. package/src/router/request-classification.ts +310 -0
  192. package/src/router/revalidation.ts +137 -38
  193. package/src/router/route-snapshot.ts +245 -0
  194. package/src/router/router-context.ts +41 -21
  195. package/src/router/router-interfaces.ts +484 -0
  196. package/src/router/router-options.ts +618 -0
  197. package/src/router/router-registry.ts +24 -0
  198. package/src/router/segment-resolution/fresh.ts +748 -0
  199. package/src/router/segment-resolution/helpers.ts +268 -0
  200. package/src/router/segment-resolution/loader-cache.ts +199 -0
  201. package/src/router/segment-resolution/revalidation.ts +1379 -0
  202. package/src/router/segment-resolution/static-store.ts +67 -0
  203. package/src/router/segment-resolution.ts +21 -0
  204. package/src/router/segment-wrappers.ts +291 -0
  205. package/src/router/telemetry-otel.ts +299 -0
  206. package/src/router/telemetry.ts +300 -0
  207. package/src/router/timeout.ts +148 -0
  208. package/src/router/trie-matching.ts +239 -0
  209. package/src/router/types.ts +78 -3
  210. package/src/router.ts +740 -4252
  211. package/src/rsc/handler-context.ts +45 -0
  212. package/src/rsc/handler.ts +907 -797
  213. package/src/rsc/helpers.ts +140 -6
  214. package/src/rsc/index.ts +0 -20
  215. package/src/rsc/loader-fetch.ts +229 -0
  216. package/src/rsc/manifest-init.ts +90 -0
  217. package/src/rsc/nonce.ts +14 -0
  218. package/src/rsc/origin-guard.ts +141 -0
  219. package/src/rsc/progressive-enhancement.ts +393 -0
  220. package/src/rsc/response-error.ts +37 -0
  221. package/src/rsc/response-route-handler.ts +347 -0
  222. package/src/rsc/rsc-rendering.ts +246 -0
  223. package/src/rsc/runtime-warnings.ts +42 -0
  224. package/src/rsc/server-action.ts +358 -0
  225. package/src/rsc/ssr-setup.ts +128 -0
  226. package/src/rsc/types.ts +46 -11
  227. package/src/search-params.ts +230 -0
  228. package/src/segment-content-promise.ts +67 -0
  229. package/src/segment-loader-promise.ts +122 -0
  230. package/src/segment-system.tsx +134 -36
  231. package/src/server/context.ts +341 -61
  232. package/src/server/cookie-store.ts +190 -0
  233. package/src/server/fetchable-loader-store.ts +37 -0
  234. package/src/server/handle-store.ts +113 -15
  235. package/src/server/loader-registry.ts +24 -64
  236. package/src/server/request-context.ts +607 -81
  237. package/src/server.ts +35 -130
  238. package/src/ssr/index.tsx +103 -30
  239. package/src/static-handler.ts +126 -0
  240. package/src/theme/ThemeProvider.tsx +21 -15
  241. package/src/theme/ThemeScript.tsx +5 -5
  242. package/src/theme/constants.ts +5 -2
  243. package/src/theme/index.ts +4 -14
  244. package/src/theme/theme-context.ts +4 -30
  245. package/src/theme/theme-script.ts +21 -18
  246. package/src/types/boundaries.ts +158 -0
  247. package/src/types/cache-types.ts +198 -0
  248. package/src/types/error-types.ts +192 -0
  249. package/src/types/global-namespace.ts +100 -0
  250. package/src/types/handler-context.ts +791 -0
  251. package/src/types/index.ts +88 -0
  252. package/src/types/loader-types.ts +210 -0
  253. package/src/types/route-config.ts +170 -0
  254. package/src/types/route-entry.ts +120 -0
  255. package/src/types/segments.ts +150 -0
  256. package/src/types.ts +1 -1623
  257. package/src/urls/include-helper.ts +207 -0
  258. package/src/urls/index.ts +53 -0
  259. package/src/urls/path-helper-types.ts +372 -0
  260. package/src/urls/path-helper.ts +364 -0
  261. package/src/urls/pattern-types.ts +107 -0
  262. package/src/urls/response-types.ts +116 -0
  263. package/src/urls/type-extraction.ts +372 -0
  264. package/src/urls/urls-function.ts +98 -0
  265. package/src/urls.ts +1 -802
  266. package/src/use-loader.tsx +161 -81
  267. package/src/vite/discovery/bundle-postprocess.ts +181 -0
  268. package/src/vite/discovery/discover-routers.ts +348 -0
  269. package/src/vite/discovery/prerender-collection.ts +439 -0
  270. package/src/vite/discovery/route-types-writer.ts +258 -0
  271. package/src/vite/discovery/self-gen-tracking.ts +47 -0
  272. package/src/vite/discovery/state.ts +117 -0
  273. package/src/vite/discovery/virtual-module-codegen.ts +203 -0
  274. package/src/vite/index.ts +15 -1133
  275. package/src/vite/plugin-types.ts +103 -0
  276. package/src/vite/plugins/cjs-to-esm.ts +93 -0
  277. package/src/vite/plugins/client-ref-dedup.ts +115 -0
  278. package/src/vite/plugins/client-ref-hashing.ts +105 -0
  279. package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
  280. package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  281. package/src/vite/plugins/cloudflare-protocol-stub.ts +214 -0
  282. package/src/vite/{expose-action-id.ts → plugins/expose-action-id.ts} +72 -53
  283. package/src/vite/plugins/expose-id-utils.ts +299 -0
  284. package/src/vite/plugins/expose-ids/export-analysis.ts +296 -0
  285. package/src/vite/plugins/expose-ids/handler-transform.ts +209 -0
  286. package/src/vite/plugins/expose-ids/loader-transform.ts +74 -0
  287. package/src/vite/plugins/expose-ids/router-transform.ts +110 -0
  288. package/src/vite/plugins/expose-ids/types.ts +45 -0
  289. package/src/vite/plugins/expose-internal-ids.ts +786 -0
  290. package/src/vite/plugins/performance-tracks.ts +88 -0
  291. package/src/vite/plugins/refresh-cmd.ts +127 -0
  292. package/src/vite/plugins/use-cache-transform.ts +323 -0
  293. package/src/vite/plugins/version-injector.ts +83 -0
  294. package/src/vite/plugins/version-plugin.ts +266 -0
  295. package/src/vite/{virtual-entries.ts → plugins/virtual-entries.ts} +23 -14
  296. package/src/vite/plugins/virtual-stub-plugin.ts +29 -0
  297. package/src/vite/rango.ts +462 -0
  298. package/src/vite/router-discovery.ts +977 -0
  299. package/src/vite/utils/ast-handler-extract.ts +517 -0
  300. package/src/vite/utils/banner.ts +36 -0
  301. package/src/vite/utils/bundle-analysis.ts +137 -0
  302. package/src/vite/utils/manifest-utils.ts +70 -0
  303. package/src/vite/{package-resolution.ts → utils/package-resolution.ts} +25 -29
  304. package/src/vite/utils/prerender-utils.ts +221 -0
  305. package/src/vite/utils/shared-utils.ts +170 -0
  306. package/CLAUDE.md +0 -43
  307. package/src/browser/lru-cache.ts +0 -69
  308. package/src/browser/request-controller.ts +0 -164
  309. package/src/cache/memory-store.ts +0 -253
  310. package/src/href-context.ts +0 -33
  311. package/src/href.ts +0 -255
  312. package/src/server/route-manifest-cache.ts +0 -173
  313. package/src/vite/expose-handle-id.ts +0 -209
  314. package/src/vite/expose-loader-id.ts +0 -426
  315. package/src/vite/expose-location-state-id.ts +0 -177
  316. /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 segments to consolidate (only valid when this is the last action) */
169
- getConsolidationSegments(): string[] | null;
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(url: string, options?: NavigateOptions): NavigationHandle;
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(subscriptionId: string, entryActionId: string): boolean {
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 state =
372
- currentNavigation !== null || hasActiveActions ? "loading" : "idle";
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
- pendingUrl: currentNavigation?.phase === "fetching" ? currentNavigation.url : null,
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((a) => matchesActionId(actionId, a.actionId) && a.phase !== "settling")
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((a) => matchesActionId(actionId, a.actionId) && a.phase === "settling")
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
- getConsolidationSegments(): string[] | null {
654
- // Only consolidate if all actions have at least received their response
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.values()) {
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
- for (const actionId of actionListeners.keys()) {
714
- notifyAction(actionId);
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
+ }