@rangojs/router 0.0.0-experimental.32 → 0.0.0-experimental.3232cd17

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 (376) hide show
  1. package/AGENTS.md +4 -0
  2. package/README.md +198 -44
  3. package/dist/bin/rango.js +287 -105
  4. package/dist/testing/vitest.js +82 -0
  5. package/dist/vite/index.js +3248 -1117
  6. package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  7. package/package.json +73 -21
  8. package/skills/api-client/SKILL.md +211 -0
  9. package/skills/breadcrumbs/SKILL.md +107 -1
  10. package/skills/bundle-analysis/SKILL.md +159 -0
  11. package/skills/cache-guide/SKILL.md +245 -21
  12. package/skills/caching/SKILL.md +302 -6
  13. package/skills/composability/SKILL.md +27 -2
  14. package/skills/css/SKILL.md +76 -0
  15. package/skills/document-cache/SKILL.md +78 -55
  16. package/skills/handler-use/SKILL.md +364 -0
  17. package/skills/hooks/SKILL.md +270 -30
  18. package/skills/host-router/SKILL.md +82 -22
  19. package/skills/i18n/SKILL.md +276 -0
  20. package/skills/intercept/SKILL.md +49 -5
  21. package/skills/layout/SKILL.md +35 -9
  22. package/skills/links/SKILL.md +249 -17
  23. package/skills/loader/SKILL.md +294 -30
  24. package/skills/middleware/SKILL.md +52 -13
  25. package/skills/migrate-nextjs/SKILL.md +584 -0
  26. package/skills/migrate-react-router/SKILL.md +769 -0
  27. package/skills/mime-routes/SKILL.md +27 -0
  28. package/skills/observability/SKILL.md +137 -0
  29. package/skills/parallel/SKILL.md +203 -7
  30. package/skills/prerender/SKILL.md +123 -100
  31. package/skills/rango/SKILL.md +250 -22
  32. package/skills/react-compiler/SKILL.md +168 -0
  33. package/skills/response-routes/SKILL.md +122 -47
  34. package/skills/route/SKILL.md +97 -5
  35. package/skills/router-setup/SKILL.md +90 -5
  36. package/skills/server-actions/SKILL.md +775 -0
  37. package/skills/streams-and-websockets/SKILL.md +283 -0
  38. package/skills/tailwind/SKILL.md +27 -3
  39. package/skills/testing/SKILL.md +129 -0
  40. package/skills/testing/bindings.md +89 -0
  41. package/skills/testing/cache-prerender.md +124 -0
  42. package/skills/testing/client-components.md +122 -0
  43. package/skills/testing/e2e-parity.md +125 -0
  44. package/skills/testing/flight.md +92 -0
  45. package/skills/testing/handles.md +129 -0
  46. package/skills/testing/loader.md +128 -0
  47. package/skills/testing/middleware.md +99 -0
  48. package/skills/testing/render-handler.md +121 -0
  49. package/skills/testing/response-routes.md +95 -0
  50. package/skills/testing/reverse-and-types.md +84 -0
  51. package/skills/testing/server-actions.md +107 -0
  52. package/skills/testing/server-tree.md +128 -0
  53. package/skills/testing/setup.md +120 -0
  54. package/skills/typesafety/SKILL.md +329 -27
  55. package/skills/use-cache/SKILL.md +36 -5
  56. package/skills/view-transitions/SKILL.md +294 -0
  57. package/src/__augment-tests__/augment.ts +81 -0
  58. package/src/__augment-tests__/augmented.check.ts +116 -0
  59. package/src/__internal.ts +67 -40
  60. package/src/browser/action-coordinator.ts +53 -36
  61. package/src/browser/action-fence.ts +47 -0
  62. package/src/browser/app-shell.ts +39 -0
  63. package/src/browser/app-version.ts +14 -0
  64. package/src/browser/cookie-name.ts +140 -0
  65. package/src/browser/event-controller.ts +86 -147
  66. package/src/browser/history-state.ts +21 -0
  67. package/src/browser/index.ts +3 -3
  68. package/src/browser/invalidate-client-cache.ts +52 -0
  69. package/src/browser/link-interceptor.ts +4 -0
  70. package/src/browser/navigation-bridge.ts +148 -19
  71. package/src/browser/navigation-client.ts +187 -67
  72. package/src/browser/navigation-store-handle.ts +38 -0
  73. package/src/browser/navigation-store.ts +76 -67
  74. package/src/browser/navigation-transaction.ts +18 -66
  75. package/src/browser/partial-update.ts +123 -94
  76. package/src/browser/prefetch/cache.ts +214 -36
  77. package/src/browser/prefetch/fetch.ts +260 -38
  78. package/src/browser/prefetch/policy.ts +6 -0
  79. package/src/browser/prefetch/queue.ts +126 -20
  80. package/src/browser/prefetch/resource-ready.ts +77 -0
  81. package/src/browser/rango-state.ts +158 -76
  82. package/src/browser/react/Link.tsx +93 -11
  83. package/src/browser/react/NavigationProvider.tsx +115 -34
  84. package/src/browser/react/ScrollRestoration.tsx +10 -6
  85. package/src/browser/react/context.ts +7 -2
  86. package/src/browser/react/filter-segment-order.ts +49 -7
  87. package/src/browser/react/index.ts +0 -48
  88. package/src/browser/react/location-state-shared.ts +166 -8
  89. package/src/browser/react/location-state.ts +39 -14
  90. package/src/browser/react/use-action.ts +6 -15
  91. package/src/browser/react/use-handle.ts +23 -69
  92. package/src/browser/react/use-link-status.ts +0 -4
  93. package/src/browser/react/use-navigation.ts +22 -5
  94. package/src/browser/react/use-params.ts +20 -10
  95. package/src/browser/react/use-reverse.ts +106 -0
  96. package/src/browser/react/use-router.ts +46 -11
  97. package/src/browser/react/use-search-params.ts +0 -5
  98. package/src/browser/react/use-segments.ts +11 -21
  99. package/src/browser/response-adapter.ts +52 -1
  100. package/src/browser/rsc-router.tsx +215 -76
  101. package/src/browser/scroll-restoration.ts +46 -39
  102. package/src/browser/segment-reconciler.ts +36 -9
  103. package/src/browser/segment-structure-assert.ts +2 -2
  104. package/src/browser/server-action-bridge.ts +176 -50
  105. package/src/browser/types.ts +95 -11
  106. package/src/browser/validate-redirect-origin.ts +43 -16
  107. package/src/build/collect-fallback-refs.ts +107 -0
  108. package/src/build/generate-manifest.ts +65 -40
  109. package/src/build/generate-route-types.ts +5 -0
  110. package/src/build/index.ts +8 -2
  111. package/src/build/prefix-tree-utils.ts +123 -0
  112. package/src/build/route-trie.ts +137 -32
  113. package/src/build/route-types/codegen.ts +4 -4
  114. package/src/build/route-types/include-resolution.ts +9 -2
  115. package/src/build/route-types/param-extraction.ts +6 -3
  116. package/src/build/route-types/per-module-writer.ts +7 -4
  117. package/src/build/route-types/router-processing.ts +278 -96
  118. package/src/build/route-types/scan-filter.ts +9 -2
  119. package/src/build/route-types/source-scan.ts +118 -0
  120. package/src/build/runtime-discovery.ts +9 -20
  121. package/src/cache/cache-error.ts +104 -0
  122. package/src/cache/cache-policy.ts +68 -28
  123. package/src/cache/cache-runtime.ts +149 -43
  124. package/src/cache/cache-scope.ts +148 -81
  125. package/src/cache/cache-tag.ts +98 -0
  126. package/src/cache/cf/cf-cache-store.ts +2550 -93
  127. package/src/cache/cf/index.ts +11 -17
  128. package/src/cache/document-cache.ts +78 -27
  129. package/src/cache/handle-snapshot.ts +63 -0
  130. package/src/cache/index.ts +23 -20
  131. package/src/cache/memory-segment-store.ts +136 -37
  132. package/src/cache/profile-registry.ts +6 -30
  133. package/src/cache/read-through-swr.ts +41 -11
  134. package/src/cache/segment-codec.ts +0 -16
  135. package/src/cache/tag-invalidation.ts +230 -0
  136. package/src/cache/taint.ts +55 -0
  137. package/src/cache/types.ts +33 -100
  138. package/src/cache/vercel/index.ts +11 -0
  139. package/src/cache/vercel/vercel-cache-store.ts +799 -0
  140. package/src/client.rsc.tsx +6 -21
  141. package/src/client.tsx +108 -290
  142. package/src/component-utils.ts +19 -0
  143. package/src/context-var.ts +84 -2
  144. package/src/debug.ts +2 -2
  145. package/src/decode-loader-results.ts +36 -0
  146. package/src/defer.ts +196 -0
  147. package/src/deps/ssr.ts +0 -1
  148. package/src/errors.ts +30 -4
  149. package/src/handle.ts +70 -22
  150. package/src/handles/MetaTags.tsx +0 -14
  151. package/src/handles/breadcrumbs.ts +16 -5
  152. package/src/handles/meta.ts +0 -39
  153. package/src/host/cookie-handler.ts +0 -36
  154. package/src/host/errors.ts +0 -24
  155. package/src/host/index.ts +8 -2
  156. package/src/host/pattern-matcher.ts +7 -50
  157. package/src/host/router.ts +107 -99
  158. package/src/host/testing.ts +40 -27
  159. package/src/host/types.ts +37 -4
  160. package/src/host/utils.ts +1 -1
  161. package/src/href-client.ts +137 -22
  162. package/src/index.rsc.ts +52 -26
  163. package/src/index.ts +100 -38
  164. package/src/internal-debug.ts +2 -4
  165. package/src/loader-store.ts +500 -0
  166. package/src/loader.rsc.ts +20 -13
  167. package/src/loader.ts +12 -11
  168. package/src/missing-id-error.ts +68 -0
  169. package/src/network-error-thrower.tsx +1 -6
  170. package/src/outlet-context.ts +1 -1
  171. package/src/outlet-provider.tsx +1 -5
  172. package/src/prerender/param-hash.ts +10 -11
  173. package/src/prerender/store.ts +37 -41
  174. package/src/prerender.ts +198 -82
  175. package/src/redirect-origin.ts +100 -0
  176. package/src/response-utils.ts +37 -0
  177. package/src/reverse.ts +65 -15
  178. package/src/root-error-boundary.tsx +1 -19
  179. package/src/route-content-wrapper.tsx +7 -72
  180. package/src/route-definition/dsl-helpers.ts +437 -274
  181. package/src/route-definition/helper-factories.ts +29 -139
  182. package/src/route-definition/helpers-types.ts +113 -37
  183. package/src/route-definition/index.ts +3 -0
  184. package/src/route-definition/redirect.ts +52 -10
  185. package/src/route-definition/resolve-handler-use.ts +161 -0
  186. package/src/route-definition/use-item-types.ts +32 -0
  187. package/src/route-map-builder.ts +7 -17
  188. package/src/route-types.ts +37 -41
  189. package/src/router/basename.ts +14 -0
  190. package/src/router/content-negotiation.ts +108 -9
  191. package/src/router/error-handling.ts +13 -17
  192. package/src/router/find-match.ts +45 -22
  193. package/src/router/handler-context.ts +83 -41
  194. package/src/router/intercept-resolution.ts +25 -23
  195. package/src/router/lazy-includes.ts +19 -53
  196. package/src/router/loader-resolution.ts +213 -30
  197. package/src/router/logging.ts +5 -8
  198. package/src/router/manifest.ts +49 -45
  199. package/src/router/match-api.ts +120 -204
  200. package/src/router/match-context.ts +0 -22
  201. package/src/router/match-handlers.ts +58 -58
  202. package/src/router/match-middleware/background-revalidation.ts +27 -6
  203. package/src/router/match-middleware/cache-lookup.ts +205 -249
  204. package/src/router/match-middleware/cache-store.ts +45 -32
  205. package/src/router/match-middleware/intercept-resolution.ts +8 -28
  206. package/src/router/match-middleware/segment-resolution.ts +52 -18
  207. package/src/router/match-pipelines.ts +1 -42
  208. package/src/router/match-result.ts +104 -40
  209. package/src/router/metrics.ts +5 -34
  210. package/src/router/middleware-types.ts +13 -142
  211. package/src/router/middleware.ts +173 -143
  212. package/src/router/navigation-snapshot.ts +131 -0
  213. package/src/router/params-util.ts +23 -0
  214. package/src/router/pattern-matching.ts +109 -63
  215. package/src/router/prerender-match.ts +190 -54
  216. package/src/router/preview-match.ts +32 -102
  217. package/src/router/request-classification.ts +276 -0
  218. package/src/router/revalidation.ts +63 -55
  219. package/src/router/route-snapshot.ts +244 -0
  220. package/src/router/router-context.ts +6 -28
  221. package/src/router/router-interfaces.ts +100 -35
  222. package/src/router/router-options.ts +91 -11
  223. package/src/router/router-registry.ts +2 -5
  224. package/src/router/segment-resolution/fresh.ts +242 -75
  225. package/src/router/segment-resolution/helpers.ts +63 -24
  226. package/src/router/segment-resolution/loader-cache.ts +41 -37
  227. package/src/router/segment-resolution/revalidation.ts +456 -372
  228. package/src/router/segment-resolution/static-store.ts +19 -5
  229. package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
  230. package/src/router/segment-resolution/view-transition-default.ts +36 -0
  231. package/src/router/segment-resolution.ts +4 -1
  232. package/src/router/segment-wrappers.ts +2 -3
  233. package/src/router/state-cookie-name.ts +33 -0
  234. package/src/router/substitute-pattern-params.ts +56 -0
  235. package/src/router/telemetry-otel.ts +0 -20
  236. package/src/router/telemetry.ts +96 -19
  237. package/src/router/timeout.ts +0 -20
  238. package/src/router/trie-matching.ts +91 -46
  239. package/src/router/types.ts +10 -63
  240. package/src/router/url-params.ts +44 -0
  241. package/src/router.ts +134 -43
  242. package/src/rsc/handler-context.ts +3 -2
  243. package/src/rsc/handler.ts +492 -383
  244. package/src/rsc/helpers.ts +162 -46
  245. package/src/rsc/index.ts +1 -1
  246. package/src/rsc/json-route-result.ts +38 -0
  247. package/src/rsc/loader-fetch.ts +23 -3
  248. package/src/rsc/manifest-init.ts +33 -42
  249. package/src/rsc/origin-guard.ts +39 -25
  250. package/src/rsc/progressive-enhancement.ts +30 -3
  251. package/src/rsc/redirect-guard.ts +99 -0
  252. package/src/rsc/response-error.ts +79 -12
  253. package/src/rsc/response-route-handler.ts +90 -63
  254. package/src/rsc/rsc-rendering.ts +56 -54
  255. package/src/rsc/runtime-warnings.ts +23 -10
  256. package/src/rsc/server-action.ts +74 -67
  257. package/src/rsc/ssr-setup.ts +18 -2
  258. package/src/rsc/types.ts +25 -6
  259. package/src/runtime-env.ts +18 -0
  260. package/src/search-params.ts +4 -20
  261. package/src/segment-content-promise.ts +67 -0
  262. package/src/segment-loader-promise.ts +134 -0
  263. package/src/segment-system.tsx +272 -129
  264. package/src/serialize.ts +243 -0
  265. package/src/server/context.ts +309 -61
  266. package/src/server/cookie-store.ts +80 -5
  267. package/src/server/handle-store.ts +26 -24
  268. package/src/server/loader-registry.ts +10 -28
  269. package/src/server/request-context.ts +338 -126
  270. package/src/ssr/index.tsx +23 -15
  271. package/src/static-handler.ts +27 -18
  272. package/src/testing/cache-status.ts +162 -0
  273. package/src/testing/collect-handle.ts +40 -0
  274. package/src/testing/dispatch.ts +618 -0
  275. package/src/testing/dom.entry.ts +22 -0
  276. package/src/testing/e2e/fixture.ts +188 -0
  277. package/src/testing/e2e/index.ts +128 -0
  278. package/src/testing/e2e/matchers.ts +35 -0
  279. package/src/testing/e2e/page-helpers.ts +272 -0
  280. package/src/testing/e2e/parity.ts +387 -0
  281. package/src/testing/e2e/server.ts +195 -0
  282. package/src/testing/flight-matchers.ts +97 -0
  283. package/src/testing/flight-normalize.ts +11 -0
  284. package/src/testing/flight-runtime.d.ts +57 -0
  285. package/src/testing/flight-tree.ts +682 -0
  286. package/src/testing/flight.entry.ts +52 -0
  287. package/src/testing/flight.ts +232 -0
  288. package/src/testing/generated-routes.ts +183 -0
  289. package/src/testing/index.ts +99 -0
  290. package/src/testing/internal/context.ts +348 -0
  291. package/src/testing/internal/flight-client-globals.ts +30 -0
  292. package/src/testing/internal/seed-vars.ts +54 -0
  293. package/src/testing/render-handler.ts +330 -0
  294. package/src/testing/render-route.tsx +566 -0
  295. package/src/testing/run-loader.ts +378 -0
  296. package/src/testing/run-middleware.ts +205 -0
  297. package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
  298. package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
  299. package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
  300. package/src/testing/vitest-stubs/version.ts +5 -0
  301. package/src/testing/vitest.ts +305 -0
  302. package/src/theme/ThemeProvider.tsx +0 -52
  303. package/src/theme/ThemeScript.tsx +0 -6
  304. package/src/theme/constants.ts +0 -12
  305. package/src/theme/index.ts +0 -7
  306. package/src/theme/theme-context.ts +1 -5
  307. package/src/theme/theme-script.ts +0 -14
  308. package/src/theme/use-theme.ts +0 -3
  309. package/src/types/boundaries.ts +0 -35
  310. package/src/types/cache-types.ts +17 -8
  311. package/src/types/error-types.ts +30 -90
  312. package/src/types/global-namespace.ts +54 -41
  313. package/src/types/handler-context.ts +233 -81
  314. package/src/types/index.ts +1 -10
  315. package/src/types/loader-types.ts +44 -15
  316. package/src/types/request-scope.ts +107 -0
  317. package/src/types/route-config.ts +6 -50
  318. package/src/types/route-entry.ts +19 -7
  319. package/src/types/segments.ts +37 -14
  320. package/src/urls/include-helper.ts +33 -70
  321. package/src/urls/index.ts +1 -11
  322. package/src/urls/path-helper-types.ts +58 -11
  323. package/src/urls/path-helper.ts +57 -111
  324. package/src/urls/pattern-types.ts +48 -19
  325. package/src/urls/response-types.ts +25 -22
  326. package/src/urls/type-extraction.ts +58 -139
  327. package/src/urls/urls-function.ts +1 -18
  328. package/src/use-loader.tsx +346 -89
  329. package/src/vite/debug.ts +185 -0
  330. package/src/vite/discovery/bundle-postprocess.ts +36 -38
  331. package/src/vite/discovery/discover-routers.ts +130 -85
  332. package/src/vite/discovery/discovery-errors.ts +194 -0
  333. package/src/vite/discovery/gate-state.ts +171 -0
  334. package/src/vite/discovery/prerender-collection.ts +192 -99
  335. package/src/vite/discovery/route-types-writer.ts +40 -84
  336. package/src/vite/discovery/self-gen-tracking.ts +27 -1
  337. package/src/vite/discovery/state.ts +51 -6
  338. package/src/vite/discovery/virtual-module-codegen.ts +14 -34
  339. package/src/vite/index.ts +8 -0
  340. package/src/vite/plugin-types.ts +187 -69
  341. package/src/vite/plugins/cjs-to-esm.ts +8 -18
  342. package/src/vite/plugins/client-ref-dedup.ts +16 -11
  343. package/src/vite/plugins/client-ref-hashing.ts +28 -15
  344. package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
  345. package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  346. package/src/vite/plugins/cloudflare-protocol-stub.ts +194 -0
  347. package/src/vite/plugins/expose-action-id.ts +49 -98
  348. package/src/vite/plugins/expose-id-utils.ts +11 -50
  349. package/src/vite/plugins/expose-ids/export-analysis.ts +76 -34
  350. package/src/vite/plugins/expose-ids/handler-transform.ts +10 -48
  351. package/src/vite/plugins/expose-ids/loader-transform.ts +3 -20
  352. package/src/vite/plugins/expose-ids/router-transform.ts +20 -16
  353. package/src/vite/plugins/expose-internal-ids.ts +554 -317
  354. package/src/vite/plugins/performance-tracks.ts +89 -0
  355. package/src/vite/plugins/refresh-cmd.ts +89 -27
  356. package/src/vite/plugins/use-cache-transform.ts +73 -83
  357. package/src/vite/plugins/vercel-output.ts +258 -0
  358. package/src/vite/plugins/version-injector.ts +21 -25
  359. package/src/vite/plugins/version-plugin.ts +41 -20
  360. package/src/vite/plugins/virtual-entries.ts +2 -17
  361. package/src/vite/rango.ts +257 -289
  362. package/src/vite/router-discovery.ts +930 -140
  363. package/src/vite/utils/ast-handler-extract.ts +15 -31
  364. package/src/vite/utils/banner.ts +4 -4
  365. package/src/vite/utils/bundle-analysis.ts +10 -15
  366. package/src/vite/utils/client-chunks.ts +184 -0
  367. package/src/vite/utils/forward-user-plugins.ts +171 -0
  368. package/src/vite/utils/manifest-utils.ts +4 -59
  369. package/src/vite/utils/package-resolution.ts +20 -52
  370. package/src/vite/utils/prerender-utils.ts +27 -29
  371. package/src/vite/utils/shared-utils.ts +92 -42
  372. package/src/browser/action-response-classifier.ts +0 -99
  373. package/src/browser/react/use-client-cache.ts +0 -58
  374. package/src/browser/shallow.ts +0 -40
  375. package/src/handles/index.ts +0 -7
  376. package/src/router/middleware-cookies.ts +0 -55
@@ -79,6 +79,8 @@ export interface DerivedNavigationState {
79
79
  state: "idle" | "loading";
80
80
  /** Whether any operation is streaming */
81
81
  isStreaming: boolean;
82
+ /** Whether a navigation is active (fetching or streaming, before commit) */
83
+ isNavigating: boolean;
82
84
  /** Current committed location */
83
85
  location: NavigationLocation;
84
86
  /** URL being navigated to (null if idle) */
@@ -111,11 +113,24 @@ export type ActionStateListener = (state: TrackedActionState) => void;
111
113
  export type HandleListener = () => void;
112
114
 
113
115
  /**
114
- * 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.
115
129
  */
116
130
  export interface HandleState {
117
131
  data: HandleData;
118
132
  segmentOrder: string[];
133
+ routeSegmentIds: string[];
119
134
  }
120
135
 
121
136
  /**
@@ -200,6 +215,14 @@ export interface EventController {
200
215
  data: HandleData,
201
216
  matched?: string[],
202
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[],
203
226
  ): void;
204
227
  getHandleState(): HandleState;
205
228
 
@@ -214,10 +237,6 @@ export interface EventController {
214
237
  hadAnyConcurrentActions(): boolean;
215
238
  }
216
239
 
217
- // ============================================================================
218
- // Default States
219
- // ============================================================================
220
-
221
240
  const DEFAULT_ACTION_STATE: TrackedActionState = {
222
241
  state: "idle",
223
242
  actionId: null,
@@ -238,20 +257,23 @@ function matchesActionId(
238
257
  entryActionId: string,
239
258
  ): boolean {
240
259
  if (subscriptionId.includes("#")) {
241
- // Full ID: exact match
242
260
  return subscriptionId === entryActionId;
243
261
  }
244
- // Action name only: suffix match (matches "anything#actionName")
245
262
  return entryActionId.endsWith(`#${subscriptionId}`);
246
263
  }
247
264
 
248
- // ============================================================================
249
- // Implementation
250
- // ============================================================================
265
+ // Batch rapid notifications into one microtask to prevent render storms
266
+ function makeDebouncedNotifier(listeners: Set<() => void>): () => void {
267
+ let timeout: ReturnType<typeof setTimeout> | null = null;
268
+ return () => {
269
+ if (timeout !== null) clearTimeout(timeout);
270
+ timeout = setTimeout(() => {
271
+ timeout = null;
272
+ listeners.forEach((listener) => listener());
273
+ }, 0);
274
+ };
275
+ }
251
276
 
252
- /**
253
- * Configuration for creating an event controller
254
- */
255
277
  export interface EventControllerConfig {
256
278
  initialLocation?: NavigationLocation;
257
279
  }
@@ -269,61 +291,34 @@ export interface EventControllerConfig {
269
291
  export function createEventController(
270
292
  config?: EventControllerConfig,
271
293
  ): EventController {
272
- // ========================================================================
273
- // Source of Truth
274
- // ========================================================================
275
-
276
- // Current navigation in progress (null = idle)
277
294
  let currentNavigation: NavigationEntry | null = null;
278
295
 
279
- // All in-flight actions (keyed by unique instance ID)
280
296
  const inflightActions = new Map<string, ActionEntry>();
281
297
 
282
- // Committed location (updated when navigation completes)
283
298
  let location: NavigationLocation =
284
299
  config?.initialLocation ??
285
300
  (typeof window !== "undefined"
286
301
  ? new URL(window.location.href)
287
302
  : new URL("/", "http://localhost"));
288
303
 
289
- // Track if any concurrent actions occurred (for consolidation)
290
304
  let hadAnyConcurrentActions = false;
291
305
 
292
- // Track segments revalidated by concurrent actions
293
306
  const concurrentRevalidatedSegments = new Set<string>();
294
307
 
295
- // Active streaming count (independent of navigation/action lifecycle)
296
308
  let activeStreamCount = 0;
297
309
 
298
- // Handle data from RSC payload
299
310
  let handleData: HandleData = {};
300
311
  let handleSegmentOrder: string[] = [];
312
+ let routeSegmentIds: string[] = [];
301
313
 
302
- // Merged route params from current match
303
314
  let routeParams: Record<string, string> = {};
304
315
 
305
- // ========================================================================
306
- // Listeners
307
- // ========================================================================
308
-
309
316
  const stateListeners = new Set<StateListener>();
310
317
  const actionListeners = new Map<string, Set<ActionStateListener>>();
311
318
  const handleListeners = new Set<HandleListener>();
312
319
 
313
- // Debounce state notifications to batch rapid updates
314
- let notifyTimeout: ReturnType<typeof setTimeout> | null = null;
315
-
316
- function notify() {
317
- if (notifyTimeout !== null) {
318
- clearTimeout(notifyTimeout);
319
- }
320
- notifyTimeout = setTimeout(() => {
321
- notifyTimeout = null;
322
- stateListeners.forEach((listener) => listener());
323
- }, 0);
324
- }
320
+ const notify = makeDebouncedNotifier(stateListeners);
325
321
 
326
- // Debounce per-action notifications
327
322
  const actionNotifyTimeouts = new Map<string, ReturnType<typeof setTimeout>>();
328
323
 
329
324
  function notifyAction(actionId: string) {
@@ -335,8 +330,6 @@ export function createEventController(
335
330
  actionId,
336
331
  setTimeout(() => {
337
332
  actionNotifyTimeouts.delete(actionId);
338
- // Notify all listeners whose subscription ID matches this action
339
- // This includes exact matches and suffix matches (e.g., "addToCart" matches "hash#addToCart")
340
333
  for (const [subscriptionId, listeners] of actionListeners) {
341
334
  if (matchesActionId(subscriptionId, actionId)) {
342
335
  const state = getActionState(subscriptionId);
@@ -347,25 +340,9 @@ export function createEventController(
347
340
  );
348
341
  }
349
342
 
350
- // Debounce handle notifications
351
- let handleNotifyTimeout: ReturnType<typeof setTimeout> | null = null;
352
-
353
- function notifyHandles() {
354
- if (handleNotifyTimeout !== null) {
355
- clearTimeout(handleNotifyTimeout);
356
- }
357
- handleNotifyTimeout = setTimeout(() => {
358
- handleNotifyTimeout = null;
359
- handleListeners.forEach((listener) => listener());
360
- }, 0);
361
- }
362
-
363
- // ========================================================================
364
- // Derived State
365
- // ========================================================================
343
+ const notifyHandles = makeDebouncedNotifier(handleListeners);
366
344
 
367
345
  function getState(): DerivedNavigationState {
368
- // Build inflight actions list (for compatibility with existing API)
369
346
  const inflightActionsList: InflightAction[] = [...inflightActions.values()]
370
347
  .filter((a) => a.phase !== "settling")
371
348
  .map((a) => ({
@@ -375,20 +352,20 @@ export function createEventController(
375
352
  startedAt: a.startedAt,
376
353
  }));
377
354
 
378
- // State: loading if navigation OR actions are in progress
379
- // Background revalidations (skipLoadingState) don't affect visible state
380
355
  const hasActiveActions = inflightActionsList.length > 0;
381
356
  const isVisibleNavigation =
382
357
  currentNavigation !== null &&
383
358
  !currentNavigation.options?.skipLoadingState;
384
359
  const state = isVisibleNavigation || hasActiveActions ? "loading" : "idle";
385
360
 
386
- // Streaming: true if any active streams (navigation or action) or loading
387
361
  const isStreaming = activeStreamCount > 0 || state === "loading";
388
362
 
389
363
  return {
390
364
  state,
391
365
  isStreaming,
366
+ // True when a navigation is active (fetching or streaming, before
367
+ // commit). Broader than pendingUrl which clears during streaming.
368
+ isNavigating: currentNavigation !== null,
392
369
  location,
393
370
  // pendingUrl only during fetching phase - once streaming starts (URL changed), not pending.
394
371
  // Background revalidations (skipLoadingState) don't expose a pending URL.
@@ -402,28 +379,20 @@ export function createEventController(
402
379
  }
403
380
 
404
381
  function getActionState(actionId: string): TrackedActionState {
405
- // Find the most recent action with this ID that's not settling
406
- // Uses suffix matching when actionId is just a name (no #)
407
- const activeEntry = [...inflightActions.values()]
408
- .filter(
409
- (a) => matchesActionId(actionId, a.actionId) && a.phase !== "settling",
410
- )
411
- .sort((a, b) => b.startedAt - a.startedAt)[0];
412
-
413
- // Also check for settling entries to get result/error
414
- const settlingEntry = [...inflightActions.values()]
415
- .filter(
416
- (a) => matchesActionId(actionId, a.actionId) && a.phase === "settling",
417
- )
418
- .sort((a, b) => b.startedAt - a.startedAt)[0];
419
-
420
- const entry = activeEntry || settlingEntry;
382
+ const entry = [...inflightActions.values()]
383
+ .filter((a) => matchesActionId(actionId, a.actionId))
384
+ .reduce<ActionEntry | undefined>((best, a) => {
385
+ if (!best) return a;
386
+ const aActive = a.phase !== "settling";
387
+ const bActive = best.phase !== "settling";
388
+ if (aActive !== bActive) return aActive ? a : best;
389
+ return a.startedAt > best.startedAt ? a : best;
390
+ }, undefined);
421
391
 
422
392
  if (!entry) {
423
393
  return { ...DEFAULT_ACTION_STATE };
424
394
  }
425
395
 
426
- // Derive state from phase
427
396
  let state: ActionLifecycleState;
428
397
  switch (entry.phase) {
429
398
  case "fetching":
@@ -605,6 +574,19 @@ export function createEventController(
605
574
  doSettle();
606
575
  }
607
576
 
577
+ // streamingEnded is forced here for the "streaming never started" case so
578
+ // tryFinalize can run; otherwise the streaming token's end() finalizes.
579
+ function settleWith(result: NonNullable<typeof pendingResult>) {
580
+ if (!inflightActions.has(id) || settled) return;
581
+ actionCompleted = true;
582
+ entry.completed = true;
583
+ pendingResult = result;
584
+ if (entry.phase === "fetching" || streamingEnded) {
585
+ streamingEnded = true;
586
+ tryFinalize();
587
+ }
588
+ }
589
+
608
590
  return {
609
591
  id,
610
592
  abort,
@@ -641,35 +623,11 @@ export function createEventController(
641
623
  },
642
624
 
643
625
  complete(result?: unknown) {
644
- if (!inflightActions.has(id) || settled) return;
645
-
646
- actionCompleted = true;
647
- entry.completed = true;
648
- pendingResult = { type: "success", value: result };
649
-
650
- // If streaming never started or already ended, finalize immediately
651
- // Otherwise wait for streaming to end
652
- if (entry.phase === "fetching" || streamingEnded) {
653
- streamingEnded = true; // Mark as ended if never started
654
- tryFinalize();
655
- }
656
- // If streaming is in progress, tryFinalize() will be called when streaming ends
626
+ settleWith({ type: "success", value: result });
657
627
  },
658
628
 
659
629
  fail(error: unknown) {
660
- if (!inflightActions.has(id) || settled) return;
661
-
662
- actionCompleted = true;
663
- entry.completed = true;
664
- pendingResult = { type: "error", value: error };
665
-
666
- // If streaming never started or already ended, finalize immediately
667
- // Otherwise wait for streaming to end
668
- if (entry.phase === "fetching" || streamingEnded) {
669
- streamingEnded = true; // Mark as ended if never started
670
- tryFinalize();
671
- }
672
- // If streaming is in progress, tryFinalize() will be called when streaming ends
630
+ settleWith({ type: "error", value: error });
673
631
  },
674
632
 
675
633
  getRevalidatedSegments(): Set<string> {
@@ -739,8 +697,15 @@ export function createEventController(
739
697
  data: HandleData,
740
698
  matched?: string[],
741
699
  isPartial?: boolean,
700
+ resolvedIds?: string[],
742
701
  ): void {
743
- const newSegmentOrder = filterSegmentOrder(matched ?? []);
702
+ const rawMatched = matched ?? [];
703
+ const newSegmentOrder = filterSegmentOrder(rawMatched);
704
+ // Separate list for useSegments(): "layouts and routes only" — strip
705
+ // parallels (".@") and loader sub-ids (D digit) without reordering.
706
+ const newRouteSegmentIds = rawMatched.filter(
707
+ (id) => !id.includes(".@") && !/D\d+\./.test(id),
708
+ );
744
709
 
745
710
  if (isPartial && newSegmentOrder.length > 0) {
746
711
  // Partial update: merge new data with existing
@@ -752,10 +717,19 @@ export function createEventController(
752
717
  handleData[handleName][segmentId] = data[handleName][segmentId];
753
718
  }
754
719
  }
755
- // Clean up data from segments no longer in the matched list
720
+ const resolvedIdSet =
721
+ resolvedIds && resolvedIds.length > 0 ? new Set(resolvedIds) : null;
722
+ // Cleanup pass:
723
+ // a) segment dropped from the match list — delete its bucket.
724
+ // b) segment was re-resolved this request but pushed nothing for
725
+ // this handle — its previous bucket is stale.
726
+ // (a) is the existing behavior; (b) requires resolvedIds.
756
727
  for (const handleName of Object.keys(handleData)) {
757
728
  for (const segmentId of Object.keys(handleData[handleName])) {
758
- if (!newSegmentOrder.includes(segmentId)) {
729
+ const droppedFromMatch = !newSegmentOrder.includes(segmentId);
730
+ const reresolvedWithoutPush =
731
+ resolvedIdSet?.has(segmentId) && !data[handleName]?.[segmentId];
732
+ if (droppedFromMatch || reresolvedWithoutPush) {
759
733
  delete handleData[handleName][segmentId];
760
734
  }
761
735
  }
@@ -765,6 +739,7 @@ export function createEventController(
765
739
  handleData = data;
766
740
  }
767
741
  handleSegmentOrder = newSegmentOrder;
742
+ routeSegmentIds = newRouteSegmentIds;
768
743
 
769
744
  notifyHandles();
770
745
  }
@@ -773,6 +748,7 @@ export function createEventController(
773
748
  return {
774
749
  data: handleData,
775
750
  segmentOrder: handleSegmentOrder,
751
+ routeSegmentIds,
776
752
  };
777
753
  }
778
754
 
@@ -860,40 +836,3 @@ export function createEventController(
860
836
  hadAnyConcurrentActions: () => hadAnyConcurrentActions,
861
837
  };
862
838
  }
863
-
864
- // ============================================================================
865
- // Singleton
866
- // ============================================================================
867
-
868
- let controllerInstance: EventController | null = null;
869
-
870
- /**
871
- * Initialize the global event controller
872
- */
873
- export function initEventController(
874
- config?: EventControllerConfig,
875
- ): EventController {
876
- if (!controllerInstance) {
877
- controllerInstance = createEventController(config);
878
- }
879
- return controllerInstance;
880
- }
881
-
882
- /**
883
- * Get the global event controller
884
- */
885
- export function getEventController(): EventController {
886
- if (!controllerInstance) {
887
- throw new Error(
888
- "Event controller not initialized. Call initEventController first.",
889
- );
890
- }
891
- return controllerInstance;
892
- }
893
-
894
- /**
895
- * Reset the controller instance (for testing)
896
- */
897
- export function resetEventController(): void {
898
- controllerInstance = null;
899
- }
@@ -61,6 +61,27 @@ export function buildHistoryState(
61
61
  return Object.keys(result).length > 0 ? result : null;
62
62
  }
63
63
 
64
+ /**
65
+ * Stamp an `idx` on the next history entry's state and call push/replaceState.
66
+ * Push increments the current idx; replace keeps it. Initial entry idx is 0.
67
+ * Used by useRouter().back() to detect "first entry in this session" without
68
+ * relying on the Navigation API.
69
+ */
70
+ export function pushHistoryWithIdx(
71
+ state: Record<string, unknown> | null,
72
+ url: string,
73
+ replace: boolean,
74
+ ): void {
75
+ const oldIdx = (window.history.state as { idx?: number } | null)?.idx ?? 0;
76
+ const newIdx = replace ? oldIdx : oldIdx + 1;
77
+ const finalState = { ...(state ?? {}), idx: newIdx };
78
+ if (replace) {
79
+ window.history.replaceState(finalState, "", url);
80
+ } else {
81
+ window.history.pushState(finalState, "", url);
82
+ }
83
+ }
84
+
64
85
  /**
65
86
  * Merge server-set location state into the current history entry.
66
87
  * Replaces the current history state and dispatches notification event
@@ -1,9 +1,9 @@
1
1
  // ============================================================================
2
- // Browser Module - Browser entry point for RSC Router
2
+ // Browser Module - Browser entry point for Rango
3
3
  // ============================================================================
4
4
  //
5
5
  // Usage:
6
- // import { initBrowserApp, RSCRouter } from "rsc-router/browser";
6
+ // import { initBrowserApp, Rango } from "rsc-router/browser";
7
7
  //
8
8
  // For React components (Link, useNavigation, etc.):
9
9
  // import { Link, useNavigation, useAction, href } from "rsc-router/client";
@@ -13,6 +13,6 @@
13
13
  // Browser app initialization
14
14
  export {
15
15
  initBrowserApp,
16
- RSCRouter,
16
+ Rango,
17
17
  type InitBrowserAppOptions,
18
18
  } from "./rsc-router.js";
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Client seat of `invalidateClientCache()` (the `default` export condition).
3
+ *
4
+ * Makes the current client behave as if a server action had just completed:
5
+ * the history cache is marked stale (SWR), the prefetch map is flushed, the
6
+ * state rotates, and sibling tabs are broadcast to — the same
7
+ * `markCacheAsStaleAndBroadcast()` path the server-action bridge uses. This is
8
+ * the gentler mark-stale (not hard-clear) behavior, so Back renders the cached
9
+ * entry instantly and revalidates.
10
+ */
11
+
12
+ import { getRegisteredStore } from "./navigation-store-handle.js";
13
+ import { clearPrefetchCache } from "./prefetch/cache.js";
14
+
15
+ export function invalidateClientCache(): void {
16
+ if (typeof document === "undefined") {
17
+ // SSR pass of a client component also resolves the default condition. A
18
+ // render-time call must not take down the page; no-op with a dev warning.
19
+ if (process.env.NODE_ENV !== "production") {
20
+ console.warn(
21
+ "[rango] invalidateClientCache() was called during a server render; " +
22
+ "it is a no-op outside the browser.",
23
+ );
24
+ }
25
+ return;
26
+ }
27
+
28
+ const store = getRegisteredStore();
29
+ if (store) {
30
+ store.markCacheAsStaleAndBroadcast();
31
+ return;
32
+ }
33
+
34
+ // Pre-boot: no store registered yet. clearPrefetchCache() (which rotates the
35
+ // state) is complete at this point — there is no history cache to mark and no
36
+ // sibling state worth broadcasting.
37
+ clearPrefetchCache();
38
+ }
39
+
40
+ /**
41
+ * Client no-op for `keepClientCache()`. It is a server action directive (the
42
+ * `react-server` condition sets a response header the action bridge reads);
43
+ * there is nothing to suppress from the client side.
44
+ */
45
+ export function keepClientCache(): void {
46
+ if (process.env.NODE_ENV !== "production") {
47
+ console.warn(
48
+ "[rango] keepClientCache() has no effect on the client; it is a server " +
49
+ "action directive. Call it from inside a server action.",
50
+ );
51
+ }
52
+ }
@@ -117,6 +117,7 @@ export function setupLinkInterception(
117
117
  // Read navigation options from data attributes (set by Link component)
118
118
  const scrollAttr = link.getAttribute("data-scroll");
119
119
  const replaceAttr = link.getAttribute("data-replace");
120
+ const revalidateAttr = link.getAttribute("data-revalidate");
120
121
 
121
122
  const navigateOptions: NavigateOptions = {};
122
123
  if (scrollAttr === "false") {
@@ -125,6 +126,9 @@ export function setupLinkInterception(
125
126
  if (replaceAttr === "true") {
126
127
  navigateOptions.replace = true;
127
128
  }
129
+ if (revalidateAttr === "false") {
130
+ navigateOptions.revalidate = false;
131
+ }
128
132
 
129
133
  onNavigate(href, navigateOptions);
130
134
  };