@rangojs/router 0.0.0-experimental.1b930379 → 0.0.0-experimental.1c0bdfad

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 (400) hide show
  1. package/AGENTS.md +12 -0
  2. package/README.md +245 -49
  3. package/dist/bin/rango.js +441 -134
  4. package/dist/testing/vitest.js +82 -0
  5. package/dist/vite/index.js +3453 -1240
  6. package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  7. package/package.json +76 -21
  8. package/skills/api-client/SKILL.md +211 -0
  9. package/skills/breadcrumbs/SKILL.md +64 -2
  10. package/skills/bundle-analysis/SKILL.md +159 -0
  11. package/skills/cache-guide/SKILL.md +247 -23
  12. package/skills/caching/SKILL.md +318 -15
  13. package/skills/composability/SKILL.md +27 -2
  14. package/skills/css/SKILL.md +76 -0
  15. package/skills/debug-manifest/SKILL.md +4 -2
  16. package/skills/document-cache/SKILL.md +78 -55
  17. package/skills/handler-use/SKILL.md +364 -0
  18. package/skills/hooks/SKILL.md +250 -30
  19. package/skills/host-router/SKILL.md +83 -23
  20. package/skills/i18n/SKILL.md +276 -0
  21. package/skills/intercept/SKILL.md +50 -6
  22. package/skills/layout/SKILL.md +35 -9
  23. package/skills/links/SKILL.md +249 -17
  24. package/skills/loader/SKILL.md +279 -53
  25. package/skills/middleware/SKILL.md +52 -13
  26. package/skills/migrate-nextjs/SKILL.md +584 -0
  27. package/skills/migrate-react-router/SKILL.md +771 -0
  28. package/skills/mime-routes/SKILL.md +28 -1
  29. package/skills/observability/SKILL.md +172 -0
  30. package/skills/parallel/SKILL.md +203 -7
  31. package/skills/prerender/SKILL.md +153 -109
  32. package/skills/rango/SKILL.md +251 -22
  33. package/skills/react-compiler/SKILL.md +168 -0
  34. package/skills/response-routes/SKILL.md +123 -48
  35. package/skills/route/SKILL.md +101 -5
  36. package/skills/router-setup/SKILL.md +116 -8
  37. package/skills/scripts/SKILL.md +179 -0
  38. package/skills/server-actions/SKILL.md +775 -0
  39. package/skills/streams-and-websockets/SKILL.md +283 -0
  40. package/skills/tailwind/SKILL.md +27 -3
  41. package/skills/testing/SKILL.md +129 -0
  42. package/skills/testing/bindings.md +103 -0
  43. package/skills/testing/cache-prerender.md +127 -0
  44. package/skills/testing/client-components.md +124 -0
  45. package/skills/testing/e2e-parity.md +125 -0
  46. package/skills/testing/flight.md +91 -0
  47. package/skills/testing/handles.md +129 -0
  48. package/skills/testing/loader.md +128 -0
  49. package/skills/testing/middleware.md +99 -0
  50. package/skills/testing/render-handler.md +121 -0
  51. package/skills/testing/response-routes.md +95 -0
  52. package/skills/testing/reverse-and-types.md +84 -0
  53. package/skills/testing/server-actions.md +107 -0
  54. package/skills/testing/server-tree.md +128 -0
  55. package/skills/testing/setup.md +123 -0
  56. package/skills/typesafety/SKILL.md +332 -29
  57. package/skills/use-cache/SKILL.md +46 -14
  58. package/skills/view-transitions/SKILL.md +294 -0
  59. package/src/__augment-tests__/augment.ts +81 -0
  60. package/src/__augment-tests__/augmented.check.ts +116 -0
  61. package/src/__internal.ts +1 -66
  62. package/src/browser/action-coordinator.ts +53 -36
  63. package/src/browser/action-fence.ts +47 -0
  64. package/src/browser/app-shell.ts +39 -0
  65. package/src/browser/app-version.ts +14 -0
  66. package/src/browser/connection-warmup.ts +134 -0
  67. package/src/browser/cookie-name.ts +140 -0
  68. package/src/browser/event-controller.ts +197 -150
  69. package/src/browser/history-state.ts +21 -0
  70. package/src/browser/index.ts +3 -3
  71. package/src/browser/invalidate-client-cache.ts +52 -0
  72. package/src/browser/navigation-bridge.ts +111 -31
  73. package/src/browser/navigation-client.ts +201 -67
  74. package/src/browser/navigation-store-handle.ts +38 -0
  75. package/src/browser/navigation-store.ts +76 -67
  76. package/src/browser/navigation-transaction.ts +18 -66
  77. package/src/browser/network-error-handler.ts +34 -7
  78. package/src/browser/partial-update.ts +187 -112
  79. package/src/browser/prefetch/cache.ts +230 -35
  80. package/src/browser/prefetch/fetch.ts +338 -39
  81. package/src/browser/prefetch/queue.ts +126 -20
  82. package/src/browser/prefetch/resource-ready.ts +77 -0
  83. package/src/browser/rango-state.ts +158 -76
  84. package/src/browser/react/Link.tsx +111 -16
  85. package/src/browser/react/NavigationProvider.tsx +135 -120
  86. package/src/browser/react/ScrollRestoration.tsx +10 -6
  87. package/src/browser/react/context.ts +7 -2
  88. package/src/browser/react/filter-segment-order.ts +66 -7
  89. package/src/browser/react/index.ts +0 -48
  90. package/src/browser/react/location-state-shared.ts +178 -8
  91. package/src/browser/react/location-state.ts +39 -14
  92. package/src/browser/react/use-action.ts +6 -15
  93. package/src/browser/react/use-handle.ts +23 -69
  94. package/src/browser/react/use-href.tsx +8 -1
  95. package/src/browser/react/use-link-status.ts +33 -8
  96. package/src/browser/react/use-navigation.ts +32 -7
  97. package/src/browser/react/use-params.ts +20 -10
  98. package/src/browser/react/use-reverse.ts +106 -0
  99. package/src/browser/react/use-router.ts +46 -11
  100. package/src/browser/react/use-search-params.ts +0 -5
  101. package/src/browser/react/use-segments.ts +11 -21
  102. package/src/browser/response-adapter.ts +80 -5
  103. package/src/browser/rsc-router.tsx +218 -76
  104. package/src/browser/scroll-restoration.ts +54 -42
  105. package/src/browser/segment-reconciler.ts +36 -9
  106. package/src/browser/segment-structure-assert.ts +2 -2
  107. package/src/browser/server-action-bridge.ts +222 -61
  108. package/src/browser/types.ts +91 -11
  109. package/src/browser/validate-redirect-origin.ts +43 -16
  110. package/src/build/collect-fallback-refs.ts +107 -0
  111. package/src/build/generate-manifest.ts +65 -40
  112. package/src/build/generate-route-types.ts +5 -1
  113. package/src/build/index.ts +8 -2
  114. package/src/build/prefix-tree-utils.ts +123 -0
  115. package/src/build/route-trie.ts +165 -36
  116. package/src/build/route-types/ast-route-extraction.ts +15 -8
  117. package/src/build/route-types/codegen.ts +16 -5
  118. package/src/build/route-types/include-resolution.ts +125 -24
  119. package/src/build/route-types/param-extraction.ts +6 -3
  120. package/src/build/route-types/per-module-writer.ts +22 -6
  121. package/src/build/route-types/router-processing.ts +272 -96
  122. package/src/build/route-types/scan-filter.ts +9 -2
  123. package/src/build/route-types/source-scan.ts +216 -0
  124. package/src/build/runtime-discovery.ts +9 -20
  125. package/src/cache/cache-error.ts +104 -0
  126. package/src/cache/cache-key-utils.ts +29 -13
  127. package/src/cache/cache-policy.ts +108 -34
  128. package/src/cache/cache-runtime.ts +214 -48
  129. package/src/cache/cache-scope.ts +236 -89
  130. package/src/cache/cache-tag.ts +103 -0
  131. package/src/cache/cf/cf-base64.ts +33 -0
  132. package/src/cache/cf/cf-cache-constants.ts +127 -0
  133. package/src/cache/cf/cf-cache-store.ts +2224 -171
  134. package/src/cache/cf/cf-cache-types.ts +349 -0
  135. package/src/cache/cf/cf-kv-utils.ts +46 -0
  136. package/src/cache/cf/cf-tag-marker-memo.ts +105 -0
  137. package/src/cache/cf/index.ts +11 -17
  138. package/src/cache/document-cache.ts +89 -27
  139. package/src/cache/handle-snapshot.ts +70 -0
  140. package/src/cache/index.ts +11 -20
  141. package/src/cache/memory-segment-store.ts +136 -37
  142. package/src/cache/profile-registry.ts +31 -31
  143. package/src/cache/read-through-swr.ts +41 -11
  144. package/src/cache/segment-codec.ts +9 -17
  145. package/src/cache/tag-invalidation.ts +230 -0
  146. package/src/cache/taint.ts +55 -0
  147. package/src/cache/types.ts +37 -100
  148. package/src/client.rsc.tsx +44 -21
  149. package/src/client.tsx +119 -290
  150. package/src/cloudflare/index.ts +11 -0
  151. package/src/cloudflare/tracing.ts +109 -0
  152. package/src/component-utils.ts +19 -0
  153. package/src/components/DefaultDocument.tsx +8 -2
  154. package/src/context-var.ts +84 -2
  155. package/src/debug.ts +2 -2
  156. package/src/decode-loader-results.ts +52 -0
  157. package/src/defer.ts +196 -0
  158. package/src/deps/ssr.ts +0 -1
  159. package/src/encode-kv.ts +49 -0
  160. package/src/errors.ts +30 -4
  161. package/src/escape-script.ts +52 -0
  162. package/src/handle.ts +70 -22
  163. package/src/handles/MetaTags.tsx +56 -19
  164. package/src/handles/Scripts.tsx +183 -0
  165. package/src/handles/breadcrumbs.ts +37 -8
  166. package/src/handles/is-thenable.ts +19 -0
  167. package/src/handles/meta.ts +51 -40
  168. package/src/handles/script.ts +244 -0
  169. package/src/host/cookie-handler.ts +9 -60
  170. package/src/host/errors.ts +0 -24
  171. package/src/host/index.ts +8 -2
  172. package/src/host/pattern-matcher.ts +23 -52
  173. package/src/host/router.ts +107 -99
  174. package/src/host/testing.ts +40 -27
  175. package/src/host/types.ts +37 -4
  176. package/src/host/utils.ts +1 -1
  177. package/src/href-client.ts +137 -22
  178. package/src/index.rsc.ts +93 -12
  179. package/src/index.ts +133 -15
  180. package/src/internal-debug.ts +11 -10
  181. package/src/loader-store.ts +500 -0
  182. package/src/loader.rsc.ts +20 -13
  183. package/src/loader.ts +12 -11
  184. package/src/missing-id-error.ts +68 -0
  185. package/src/outlet-context.ts +1 -1
  186. package/src/outlet-provider.tsx +1 -5
  187. package/src/prerender/param-hash.ts +16 -16
  188. package/src/prerender/store.ts +37 -41
  189. package/src/prerender.ts +198 -82
  190. package/src/redirect-origin.ts +100 -0
  191. package/src/regex-escape.ts +8 -0
  192. package/src/render-error-thrower.tsx +20 -0
  193. package/src/response-utils.ts +62 -0
  194. package/src/reverse.ts +65 -15
  195. package/src/root-error-boundary.tsx +1 -19
  196. package/src/route-content-wrapper.tsx +7 -72
  197. package/src/route-definition/dsl-helpers.ts +469 -276
  198. package/src/route-definition/helper-factories.ts +29 -139
  199. package/src/route-definition/helpers-types.ts +113 -37
  200. package/src/route-definition/index.ts +3 -0
  201. package/src/route-definition/redirect.ts +53 -12
  202. package/src/route-definition/resolve-handler-use.ts +161 -0
  203. package/src/route-definition/use-item-types.ts +32 -0
  204. package/src/route-map-builder.ts +7 -17
  205. package/src/route-types.ts +37 -41
  206. package/src/router/basename.ts +14 -0
  207. package/src/router/content-negotiation.ts +164 -17
  208. package/src/router/error-handling.ts +45 -18
  209. package/src/router/find-match.ts +45 -22
  210. package/src/router/handler-context.ts +83 -39
  211. package/src/router/instrument.ts +350 -0
  212. package/src/router/intercept-resolution.ts +50 -24
  213. package/src/router/lazy-includes.ts +19 -53
  214. package/src/router/loader-resolution.ts +274 -56
  215. package/src/router/logging.ts +5 -8
  216. package/src/router/manifest.ts +49 -45
  217. package/src/router/match-api.ts +120 -204
  218. package/src/router/match-context.ts +0 -22
  219. package/src/router/match-handlers.ts +58 -58
  220. package/src/router/match-middleware/background-revalidation.ts +33 -6
  221. package/src/router/match-middleware/cache-lookup.ts +214 -263
  222. package/src/router/match-middleware/cache-store.ts +73 -33
  223. package/src/router/match-middleware/intercept-resolution.ts +8 -28
  224. package/src/router/match-middleware/segment-resolution.ts +52 -18
  225. package/src/router/match-pipelines.ts +1 -42
  226. package/src/router/match-result.ts +104 -40
  227. package/src/router/metrics.ts +5 -34
  228. package/src/router/middleware-types.ts +13 -142
  229. package/src/router/middleware.ts +270 -172
  230. package/src/router/navigation-snapshot.ts +131 -0
  231. package/src/router/params-util.ts +23 -0
  232. package/src/router/pattern-matching.ts +132 -90
  233. package/src/router/prefetch-cache-ttl.ts +51 -0
  234. package/src/router/prerender-match.ts +195 -56
  235. package/src/router/preview-match.ts +32 -102
  236. package/src/router/request-classification.ts +276 -0
  237. package/src/router/revalidation.ts +123 -73
  238. package/src/router/route-snapshot.ts +244 -0
  239. package/src/router/router-context.ts +8 -28
  240. package/src/router/router-interfaces.ts +115 -35
  241. package/src/router/router-options.ts +172 -15
  242. package/src/router/router-registry.ts +2 -5
  243. package/src/router/segment-resolution/fresh.ts +264 -77
  244. package/src/router/segment-resolution/helpers.ts +115 -30
  245. package/src/router/segment-resolution/loader-cache.ts +63 -37
  246. package/src/router/segment-resolution/revalidation.ts +474 -385
  247. package/src/router/segment-resolution/static-store.ts +19 -5
  248. package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
  249. package/src/router/segment-resolution/view-transition-default.ts +36 -0
  250. package/src/router/segment-resolution.ts +5 -1
  251. package/src/router/segment-wrappers.ts +8 -5
  252. package/src/router/state-cookie-name.ts +33 -0
  253. package/src/router/substitute-pattern-params.ts +56 -0
  254. package/src/router/telemetry-otel.ts +161 -199
  255. package/src/router/telemetry.ts +96 -19
  256. package/src/router/timeout.ts +0 -20
  257. package/src/router/tracing.ts +206 -0
  258. package/src/router/trie-matching.ts +163 -59
  259. package/src/router/types.ts +10 -63
  260. package/src/router/url-params.ts +44 -0
  261. package/src/router.ts +163 -55
  262. package/src/rsc/handler-context.ts +3 -2
  263. package/src/rsc/handler.ts +658 -511
  264. package/src/rsc/helpers.ts +168 -46
  265. package/src/rsc/index.ts +2 -5
  266. package/src/rsc/json-route-result.ts +38 -0
  267. package/src/rsc/loader-fetch.ts +127 -31
  268. package/src/rsc/manifest-init.ts +33 -42
  269. package/src/rsc/origin-guard.ts +39 -25
  270. package/src/rsc/progressive-enhancement.ts +77 -11
  271. package/src/rsc/redirect-guard.ts +99 -0
  272. package/src/rsc/response-cache-serve.ts +238 -0
  273. package/src/rsc/response-error.ts +79 -12
  274. package/src/rsc/response-route-handler.ts +99 -189
  275. package/src/rsc/rsc-rendering.ts +105 -72
  276. package/src/rsc/runtime-warnings.ts +23 -10
  277. package/src/rsc/server-action.ts +263 -112
  278. package/src/rsc/ssr-setup.ts +18 -2
  279. package/src/rsc/types.ts +32 -6
  280. package/src/runtime-env.ts +18 -0
  281. package/src/search-params.ts +35 -30
  282. package/src/segment-content-promise.ts +67 -0
  283. package/src/segment-loader-promise.ts +149 -0
  284. package/src/segment-system.tsx +281 -129
  285. package/src/serialize.ts +243 -0
  286. package/src/server/context.ts +309 -61
  287. package/src/server/cookie-parse.ts +32 -0
  288. package/src/server/cookie-store.ts +80 -5
  289. package/src/server/handle-store.ts +40 -38
  290. package/src/server/loader-registry.ts +26 -46
  291. package/src/server/request-context.ts +398 -172
  292. package/src/ssr/index.tsx +25 -16
  293. package/src/static-handler.ts +27 -18
  294. package/src/testing/cache-status.ts +162 -0
  295. package/src/testing/collect-handle.ts +40 -0
  296. package/src/testing/dispatch.ts +701 -0
  297. package/src/testing/dom.entry.ts +22 -0
  298. package/src/testing/e2e/fixture.ts +188 -0
  299. package/src/testing/e2e/index.ts +128 -0
  300. package/src/testing/e2e/matchers.ts +35 -0
  301. package/src/testing/e2e/page-helpers.ts +272 -0
  302. package/src/testing/e2e/parity.ts +387 -0
  303. package/src/testing/e2e/server.ts +195 -0
  304. package/src/testing/flight-matchers.ts +97 -0
  305. package/src/testing/flight-normalize.ts +11 -0
  306. package/src/testing/flight-runtime.d.ts +57 -0
  307. package/src/testing/flight-tree.ts +682 -0
  308. package/src/testing/flight.entry.ts +52 -0
  309. package/src/testing/flight.ts +257 -0
  310. package/src/testing/generated-routes.ts +183 -0
  311. package/src/testing/index.ts +99 -0
  312. package/src/testing/internal/context.ts +371 -0
  313. package/src/testing/internal/flight-client-globals.ts +30 -0
  314. package/src/testing/internal/seed-vars.ts +54 -0
  315. package/src/testing/render-handler.ts +343 -0
  316. package/src/testing/render-route.tsx +581 -0
  317. package/src/testing/run-loader.ts +385 -0
  318. package/src/testing/run-middleware.ts +205 -0
  319. package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
  320. package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
  321. package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
  322. package/src/testing/vitest-stubs/version.ts +5 -0
  323. package/src/testing/vitest.ts +305 -0
  324. package/src/theme/ThemeProvider.tsx +20 -58
  325. package/src/theme/ThemeScript.tsx +7 -9
  326. package/src/theme/constants.ts +52 -13
  327. package/src/theme/index.ts +0 -7
  328. package/src/theme/theme-context.ts +1 -5
  329. package/src/theme/theme-script.ts +22 -21
  330. package/src/theme/use-theme.ts +0 -3
  331. package/src/types/boundaries.ts +0 -35
  332. package/src/types/cache-types.ts +17 -8
  333. package/src/types/error-types.ts +30 -90
  334. package/src/types/global-namespace.ts +54 -41
  335. package/src/types/handler-context.ts +233 -81
  336. package/src/types/index.ts +1 -10
  337. package/src/types/loader-types.ts +44 -15
  338. package/src/types/request-scope.ts +112 -0
  339. package/src/types/route-config.ts +6 -50
  340. package/src/types/route-entry.ts +19 -7
  341. package/src/types/segments.ts +37 -14
  342. package/src/urls/include-helper.ts +33 -70
  343. package/src/urls/index.ts +1 -11
  344. package/src/urls/path-helper-types.ts +58 -11
  345. package/src/urls/path-helper.ts +57 -111
  346. package/src/urls/pattern-types.ts +48 -19
  347. package/src/urls/response-types.ts +25 -22
  348. package/src/urls/type-extraction.ts +58 -139
  349. package/src/urls/urls-function.ts +1 -18
  350. package/src/use-loader.tsx +346 -89
  351. package/src/vite/debug.ts +185 -0
  352. package/src/vite/discovery/bundle-postprocess.ts +36 -38
  353. package/src/vite/discovery/discover-routers.ts +130 -85
  354. package/src/vite/discovery/discovery-errors.ts +194 -0
  355. package/src/vite/discovery/gate-state.ts +171 -0
  356. package/src/vite/discovery/prerender-collection.ts +214 -132
  357. package/src/vite/discovery/route-types-writer.ts +40 -84
  358. package/src/vite/discovery/self-gen-tracking.ts +27 -1
  359. package/src/vite/discovery/state.ts +57 -6
  360. package/src/vite/discovery/virtual-module-codegen.ts +14 -34
  361. package/src/vite/index.ts +6 -0
  362. package/src/vite/inject-client-debug.ts +36 -0
  363. package/src/vite/plugin-types.ts +155 -65
  364. package/src/vite/plugins/cjs-to-esm.ts +16 -19
  365. package/src/vite/plugins/client-ref-dedup.ts +16 -11
  366. package/src/vite/plugins/client-ref-hashing.ts +28 -15
  367. package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
  368. package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  369. package/src/vite/plugins/cloudflare-protocol-stub.ts +194 -0
  370. package/src/vite/plugins/expose-action-id.ts +49 -98
  371. package/src/vite/plugins/expose-id-utils.ts +96 -51
  372. package/src/vite/plugins/expose-ids/export-analysis.ts +101 -34
  373. package/src/vite/plugins/expose-ids/handler-transform.ts +15 -64
  374. package/src/vite/plugins/expose-ids/loader-transform.ts +14 -24
  375. package/src/vite/plugins/expose-ids/router-transform.ts +118 -29
  376. package/src/vite/plugins/expose-internal-ids.ts +553 -317
  377. package/src/vite/plugins/performance-tracks.ts +89 -0
  378. package/src/vite/plugins/refresh-cmd.ts +89 -27
  379. package/src/vite/plugins/use-cache-transform.ts +73 -83
  380. package/src/vite/plugins/version-injector.ts +21 -25
  381. package/src/vite/plugins/version-plugin.ts +46 -37
  382. package/src/vite/plugins/virtual-entries.ts +13 -18
  383. package/src/vite/rango.ts +238 -295
  384. package/src/vite/router-discovery.ts +940 -149
  385. package/src/vite/utils/ast-handler-extract.ts +26 -35
  386. package/src/vite/utils/banner.ts +4 -4
  387. package/src/vite/utils/bundle-analysis.ts +10 -15
  388. package/src/vite/utils/client-chunks.ts +184 -0
  389. package/src/vite/utils/directive-prologue.ts +40 -0
  390. package/src/vite/utils/forward-user-plugins.ts +171 -0
  391. package/src/vite/utils/manifest-utils.ts +4 -59
  392. package/src/vite/utils/package-resolution.ts +20 -52
  393. package/src/vite/utils/prerender-utils.ts +81 -34
  394. package/src/vite/utils/shared-utils.ts +92 -42
  395. package/src/browser/action-response-classifier.ts +0 -99
  396. package/src/browser/react/use-client-cache.ts +0 -58
  397. package/src/browser/shallow.ts +0 -40
  398. package/src/handles/index.ts +0 -7
  399. package/src/network-error-thrower.tsx +0 -23
  400. package/src/router/middleware-cookies.ts +0 -55
@@ -10,7 +10,10 @@ import type {
10
10
  HandleData,
11
11
  StreamingToken,
12
12
  } from "./types.js";
13
- import { filterSegmentOrder } from "./react/filter-segment-order.js";
13
+ import {
14
+ filterSegmentOrder,
15
+ filterRouteSegmentIds,
16
+ } from "./react/filter-segment-order.js";
14
17
 
15
18
  // Polyfill Symbol.dispose for Safari and older browsers
16
19
  if (typeof Symbol.dispose === "undefined") {
@@ -79,6 +82,8 @@ export interface DerivedNavigationState {
79
82
  state: "idle" | "loading";
80
83
  /** Whether any operation is streaming */
81
84
  isStreaming: boolean;
85
+ /** Whether a navigation is active (fetching or streaming, before commit) */
86
+ isNavigating: boolean;
82
87
  /** Current committed location */
83
88
  location: NavigationLocation;
84
89
  /** URL being navigated to (null if idle) */
@@ -111,11 +116,24 @@ export type ActionStateListener = (state: TrackedActionState) => void;
111
116
  export type HandleListener = () => void;
112
117
 
113
118
  /**
114
- * Internal handle state stored in controller
119
+ * Internal handle state stored in controller.
120
+ *
121
+ * Two segment lists are exposed because they serve different consumers:
122
+ *
123
+ * - `segmentOrder` drives handle collection (collectHandleData). Includes
124
+ * parallel slot ids and reorders them after their parent so later-wins
125
+ * collect functions (e.g. Meta) get the right precedence.
126
+ * - `routeSegmentIds` is the layouts-and-routes-only list documented by
127
+ * `useSegments().segmentIds`. Parallels and loader sub-ids are stripped;
128
+ * raw matched order is preserved.
129
+ *
130
+ * Both are derived from the same `matched` input on each setHandleData call
131
+ * so they stay in sync.
115
132
  */
116
133
  export interface HandleState {
117
134
  data: HandleData;
118
135
  segmentOrder: string[];
136
+ routeSegmentIds: string[];
119
137
  }
120
138
 
121
139
  /**
@@ -150,6 +168,22 @@ export interface ActionHandle extends Disposable {
150
168
  startStreaming(): StreamingToken;
151
169
  /** Record segments that were revalidated */
152
170
  recordRevalidatedSegments(segmentIds: string[]): void;
171
+ /**
172
+ * Claim the subset of a location-state payload this action may write. A key
173
+ * is claimed only if no later-initiated action in the SAME cohort has already
174
+ * claimed it, so same-key concurrent writes resolve to the last-initiated
175
+ * action while distinct keys from every action survive. Recording the claim
176
+ * stops a later-settling earlier action from overwriting it. Arbitration is
177
+ * scoped to the action's cohort (its originating history entry, captured at
178
+ * startAction), so an action on one entry cannot suppress an action on
179
+ * another that happens to write the same slot.
180
+ *
181
+ * Contract: this guarantees the FINAL value is the last-initiated action's,
182
+ * not that the loser is never momentarily visible. When an earlier-initiated
183
+ * action settles first, its value is merged and observable until the
184
+ * later-initiated winner's response lands and overwrites it.
185
+ */
186
+ claimLocationState(state: Record<string, unknown>): Record<string, unknown>;
153
187
  /** Complete the action with result */
154
188
  complete(result?: unknown): void;
155
189
  /** Fail the action with error */
@@ -176,7 +210,12 @@ export interface EventController {
176
210
  abortNavigation(): void;
177
211
 
178
212
  // Action operations
179
- startAction(actionId: string, args: unknown[]): ActionHandle;
213
+ startAction(
214
+ actionId: string,
215
+ args: unknown[],
216
+ /** Originating history entry key; scopes location-state arbitration. */
217
+ cohort?: string,
218
+ ): ActionHandle;
180
219
  abortAllActions(): void;
181
220
 
182
221
  // State access
@@ -200,6 +239,14 @@ export interface EventController {
200
239
  data: HandleData,
201
240
  matched?: string[],
202
241
  isPartial?: boolean,
242
+ /**
243
+ * Segment ids that were re-resolved on the server this request (the
244
+ * partial response's `diff`). On a partial update, any existing bucket
245
+ * keyed under one of these ids that has no incoming entry is treated as
246
+ * stale and cleared. Without this, a parallel slot that revalidates but
247
+ * pushes nothing leaves its previous bucket in place forever.
248
+ */
249
+ resolvedIds?: string[],
203
250
  ): void;
204
251
  getHandleState(): HandleState;
205
252
 
@@ -214,10 +261,6 @@ export interface EventController {
214
261
  hadAnyConcurrentActions(): boolean;
215
262
  }
216
263
 
217
- // ============================================================================
218
- // Default States
219
- // ============================================================================
220
-
221
264
  const DEFAULT_ACTION_STATE: TrackedActionState = {
222
265
  state: "idle",
223
266
  actionId: null,
@@ -238,20 +281,23 @@ function matchesActionId(
238
281
  entryActionId: string,
239
282
  ): boolean {
240
283
  if (subscriptionId.includes("#")) {
241
- // Full ID: exact match
242
284
  return subscriptionId === entryActionId;
243
285
  }
244
- // Action name only: suffix match (matches "anything#actionName")
245
286
  return entryActionId.endsWith(`#${subscriptionId}`);
246
287
  }
247
288
 
248
- // ============================================================================
249
- // Implementation
250
- // ============================================================================
289
+ // Batch rapid notifications into one microtask to prevent render storms
290
+ function makeDebouncedNotifier(listeners: Set<() => void>): () => void {
291
+ let timeout: ReturnType<typeof setTimeout> | null = null;
292
+ return () => {
293
+ if (timeout !== null) clearTimeout(timeout);
294
+ timeout = setTimeout(() => {
295
+ timeout = null;
296
+ listeners.forEach((listener) => listener());
297
+ }, 0);
298
+ };
299
+ }
251
300
 
252
- /**
253
- * Configuration for creating an event controller
254
- */
255
301
  export interface EventControllerConfig {
256
302
  initialLocation?: NavigationLocation;
257
303
  }
@@ -269,61 +315,56 @@ export interface EventControllerConfig {
269
315
  export function createEventController(
270
316
  config?: EventControllerConfig,
271
317
  ): EventController {
272
- // ========================================================================
273
- // Source of Truth
274
- // ========================================================================
275
-
276
- // Current navigation in progress (null = idle)
277
318
  let currentNavigation: NavigationEntry | null = null;
278
319
 
279
- // All in-flight actions (keyed by unique instance ID)
280
320
  const inflightActions = new Map<string, ActionEntry>();
281
321
 
282
- // Committed location (updated when navigation completes)
283
322
  let location: NavigationLocation =
284
323
  config?.initialLocation ??
285
324
  (typeof window !== "undefined"
286
325
  ? new URL(window.location.href)
287
326
  : new URL("/", "http://localhost"));
288
327
 
289
- // Track if any concurrent actions occurred (for consolidation)
290
328
  let hadAnyConcurrentActions = false;
291
329
 
292
- // Track segments revalidated by concurrent actions
293
330
  const concurrentRevalidatedSegments = new Set<string>();
294
331
 
295
- // Active streaming count (independent of navigation/action lifecycle)
332
+ // Monotonic dispatch counter: every startAction() takes the next value
333
+ // (private), so a larger sequence means the action was initiated later.
334
+ let actionDispatchSeq = 0;
335
+
336
+ // Concurrent location-state arbitration, scoped PER COHORT (history entry).
337
+ // Each cohort owns a slotKey->winningDispatchSeq map plus a refcount of its
338
+ // inflight actions; claimLocationState() consults its own cohort's map so
339
+ // same-key writes within one entry resolve to the last-initiated action
340
+ // regardless of settle order, while actions on different entries never
341
+ // compete. A cohort's map is freed once its LAST action's cleanup runs — the
342
+ // same brief post-settle grace (doSettle's 100ms timer) as other action
343
+ // teardown, not when every action everywhere settles — so a long-running
344
+ // action in one cohort can never retain arbitration keys from other cohorts
345
+ // that have since drained. It is never cleared in clearConsolidation, which
346
+ // fires
347
+ // per-action on divert/error and would let a later-settling earlier action
348
+ // wrongly reclaim a key a sibling already won.
349
+ const cohortArbitration = new Map<
350
+ string,
351
+ { keySeq: Map<string, number>; inflight: number }
352
+ >();
353
+
296
354
  let activeStreamCount = 0;
297
355
 
298
- // Handle data from RSC payload
299
356
  let handleData: HandleData = {};
300
357
  let handleSegmentOrder: string[] = [];
358
+ let routeSegmentIds: string[] = [];
301
359
 
302
- // Merged route params from current match
303
360
  let routeParams: Record<string, string> = {};
304
361
 
305
- // ========================================================================
306
- // Listeners
307
- // ========================================================================
308
-
309
362
  const stateListeners = new Set<StateListener>();
310
363
  const actionListeners = new Map<string, Set<ActionStateListener>>();
311
364
  const handleListeners = new Set<HandleListener>();
312
365
 
313
- // Debounce state notifications to batch rapid updates
314
- let notifyTimeout: ReturnType<typeof setTimeout> | null = null;
366
+ const notify = makeDebouncedNotifier(stateListeners);
315
367
 
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
- }
325
-
326
- // Debounce per-action notifications
327
368
  const actionNotifyTimeouts = new Map<string, ReturnType<typeof setTimeout>>();
328
369
 
329
370
  function notifyAction(actionId: string) {
@@ -335,8 +376,6 @@ export function createEventController(
335
376
  actionId,
336
377
  setTimeout(() => {
337
378
  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
379
  for (const [subscriptionId, listeners] of actionListeners) {
341
380
  if (matchesActionId(subscriptionId, actionId)) {
342
381
  const state = getActionState(subscriptionId);
@@ -347,25 +386,9 @@ export function createEventController(
347
386
  );
348
387
  }
349
388
 
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
- // ========================================================================
389
+ const notifyHandles = makeDebouncedNotifier(handleListeners);
366
390
 
367
391
  function getState(): DerivedNavigationState {
368
- // Build inflight actions list (for compatibility with existing API)
369
392
  const inflightActionsList: InflightAction[] = [...inflightActions.values()]
370
393
  .filter((a) => a.phase !== "settling")
371
394
  .map((a) => ({
@@ -375,20 +398,20 @@ export function createEventController(
375
398
  startedAt: a.startedAt,
376
399
  }));
377
400
 
378
- // State: loading if navigation OR actions are in progress
379
- // Background revalidations (skipLoadingState) don't affect visible state
380
401
  const hasActiveActions = inflightActionsList.length > 0;
381
402
  const isVisibleNavigation =
382
403
  currentNavigation !== null &&
383
404
  !currentNavigation.options?.skipLoadingState;
384
405
  const state = isVisibleNavigation || hasActiveActions ? "loading" : "idle";
385
406
 
386
- // Streaming: true if any active streams (navigation or action) or loading
387
407
  const isStreaming = activeStreamCount > 0 || state === "loading";
388
408
 
389
409
  return {
390
410
  state,
391
411
  isStreaming,
412
+ // True when a navigation is active (fetching or streaming, before
413
+ // commit). Broader than pendingUrl which clears during streaming.
414
+ isNavigating: currentNavigation !== null,
392
415
  location,
393
416
  // pendingUrl only during fetching phase - once streaming starts (URL changed), not pending.
394
417
  // Background revalidations (skipLoadingState) don't expose a pending URL.
@@ -402,28 +425,20 @@ export function createEventController(
402
425
  }
403
426
 
404
427
  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;
428
+ const entry = [...inflightActions.values()]
429
+ .filter((a) => matchesActionId(actionId, a.actionId))
430
+ .reduce<ActionEntry | undefined>((best, a) => {
431
+ if (!best) return a;
432
+ const aActive = a.phase !== "settling";
433
+ const bActive = best.phase !== "settling";
434
+ if (aActive !== bActive) return aActive ? a : best;
435
+ return a.startedAt > best.startedAt ? a : best;
436
+ }, undefined);
421
437
 
422
438
  if (!entry) {
423
439
  return { ...DEFAULT_ACTION_STATE };
424
440
  }
425
441
 
426
- // Derive state from phase
427
442
  let state: ActionLifecycleState;
428
443
  switch (entry.phase) {
429
444
  case "fetching":
@@ -538,10 +553,29 @@ export function createEventController(
538
553
  // Action Operations
539
554
  // ========================================================================
540
555
 
541
- function startAction(actionId: string, args: unknown[]): ActionHandle {
556
+ function startAction(
557
+ actionId: string,
558
+ args: unknown[],
559
+ cohort?: string,
560
+ ): ActionHandle {
542
561
  const id = `${actionId}-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
562
+ // Private to this handle: never exposed on the returned ActionHandle.
563
+ const dispatchSeq = actionDispatchSeq++;
543
564
  const abort = new AbortController();
544
565
 
566
+ // Register this action under its cohort (originating history entry). The
567
+ // cohort's arbitration is created on first use and freed when its last
568
+ // action settles (see doSettle). Keyless entries share the "" cohort.
569
+ const cohortId = cohort ?? "";
570
+ let arb = cohortArbitration.get(cohortId);
571
+ if (!arb) {
572
+ arb = { keySeq: new Map<string, number>(), inflight: 0 };
573
+ cohortArbitration.set(cohortId, arb);
574
+ }
575
+ // const so the captured reference stays non-undefined inside doSettle.
576
+ const arbitration = arb;
577
+ arbitration.inflight++;
578
+
545
579
  // Track if this action started while others were pending (concurrent)
546
580
  const hadConcurrent = inflightActions.size > 0;
547
581
  if (hadConcurrent) {
@@ -565,11 +599,28 @@ export function createEventController(
565
599
  let settled = false;
566
600
  let streamingEnded = false;
567
601
  let actionCompleted = false;
602
+ let cohortReleased = false;
568
603
  let pendingResult:
569
604
  | { type: "success"; value?: unknown }
570
605
  | { type: "error"; value: unknown }
571
606
  | null = null;
572
607
 
608
+ // Release this action's hold on its cohort arbitration exactly once: drop
609
+ // the refcount and, only if the map still points at THIS arbitration object,
610
+ // delete it. A newer generation may have replaced it (e.g. abortAllActions
611
+ // cleared the map and a fresh action recreated the same cohort id), so a
612
+ // stale settlement must never delete the newer one by id.
613
+ function releaseCohort() {
614
+ if (cohortReleased) return;
615
+ cohortReleased = true;
616
+ if (
617
+ --arbitration.inflight <= 0 &&
618
+ cohortArbitration.get(cohortId) === arbitration
619
+ ) {
620
+ cohortArbitration.delete(cohortId);
621
+ }
622
+ }
623
+
573
624
  function doSettle() {
574
625
  if (settled) return;
575
626
  settled = true;
@@ -577,6 +628,9 @@ export function createEventController(
577
628
  // Cleanup after brief delay (allow useAction to read result)
578
629
  setTimeout(() => {
579
630
  inflightActions.delete(id);
631
+ // Free this cohort's arbitration once its last action has settled, so
632
+ // a long-running action elsewhere cannot pin keys from a drained entry.
633
+ releaseCohort();
580
634
  // Check for consolidation
581
635
  if (inflightActions.size === 0) {
582
636
  // All actions done - reset tracking
@@ -605,6 +659,19 @@ export function createEventController(
605
659
  doSettle();
606
660
  }
607
661
 
662
+ // streamingEnded is forced here for the "streaming never started" case so
663
+ // tryFinalize can run; otherwise the streaming token's end() finalizes.
664
+ function settleWith(result: NonNullable<typeof pendingResult>) {
665
+ if (!inflightActions.has(id) || settled) return;
666
+ actionCompleted = true;
667
+ entry.completed = true;
668
+ pendingResult = result;
669
+ if (entry.phase === "fetching" || streamingEnded) {
670
+ streamingEnded = true;
671
+ tryFinalize();
672
+ }
673
+ }
674
+
608
675
  return {
609
676
  id,
610
677
  abort,
@@ -640,36 +707,32 @@ export function createEventController(
640
707
  segmentIds.forEach((id) => concurrentRevalidatedSegments.add(id));
641
708
  },
642
709
 
643
- 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();
710
+ claimLocationState(state: Record<string, unknown>) {
711
+ const winning: Record<string, unknown> = {};
712
+ // Arbitrate against this action's OWN captured arbitration object, not a
713
+ // live map lookup: a concurrent map clear/replace (abortAllActions, a
714
+ // stale settlement) must not make this action silently stop recording
715
+ // and accept every key.
716
+ const keySeq = arbitration.keySeq;
717
+ for (const key of Object.keys(state)) {
718
+ const prevSeq = keySeq.get(key);
719
+ // Strictly-greater: a later-initiated action wins a key over an
720
+ // earlier one in the same cohort regardless of arrival order. Equal
721
+ // cannot happen (dispatchSeq is unique per action).
722
+ if (prevSeq === undefined || dispatchSeq > prevSeq) {
723
+ keySeq.set(key, dispatchSeq);
724
+ winning[key] = state[key];
725
+ }
655
726
  }
656
- // If streaming is in progress, tryFinalize() will be called when streaming ends
727
+ return winning;
657
728
  },
658
729
 
659
- fail(error: unknown) {
660
- if (!inflightActions.has(id) || settled) return;
661
-
662
- actionCompleted = true;
663
- entry.completed = true;
664
- pendingResult = { type: "error", value: error };
730
+ complete(result?: unknown) {
731
+ settleWith({ type: "success", value: result });
732
+ },
665
733
 
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
734
+ fail(error: unknown) {
735
+ settleWith({ type: "error", value: error });
673
736
  },
674
737
 
675
738
  getRevalidatedSegments(): Set<string> {
@@ -685,6 +748,10 @@ export function createEventController(
685
748
  [Symbol.dispose]() {
686
749
  // If aborted, another navigation/error took over - don't touch state
687
750
  if (abort.signal.aborted) {
751
+ // Aborted actions skip doSettle, so release the cohort hold here to
752
+ // keep the per-cohort refcount balanced (no leak when an action is
753
+ // aborted individually rather than via abortAllActions).
754
+ releaseCohort();
688
755
  inflightActions.delete(id);
689
756
  notify();
690
757
  notifyAction(actionId);
@@ -719,6 +786,7 @@ export function createEventController(
719
786
  }
720
787
  hadAnyConcurrentActions = false;
721
788
  concurrentRevalidatedSegments.clear();
789
+ cohortArbitration.clear();
722
790
  notify();
723
791
  // Notify all action listeners directly by subscription ID.
724
792
  // actionListeners keys are subscription IDs (possibly short names like
@@ -739,8 +807,13 @@ export function createEventController(
739
807
  data: HandleData,
740
808
  matched?: string[],
741
809
  isPartial?: boolean,
810
+ resolvedIds?: string[],
742
811
  ): void {
743
- const newSegmentOrder = filterSegmentOrder(matched ?? []);
812
+ const rawMatched = matched ?? [];
813
+ const newSegmentOrder = filterSegmentOrder(rawMatched);
814
+ // Separate list for useSegments(): "layouts and routes only" — strip
815
+ // parallels (".@") and loader sub-ids (D digit) without reordering.
816
+ const newRouteSegmentIds = filterRouteSegmentIds(rawMatched);
744
817
 
745
818
  if (isPartial && newSegmentOrder.length > 0) {
746
819
  // Partial update: merge new data with existing
@@ -752,10 +825,19 @@ export function createEventController(
752
825
  handleData[handleName][segmentId] = data[handleName][segmentId];
753
826
  }
754
827
  }
755
- // Clean up data from segments no longer in the matched list
828
+ const resolvedIdSet =
829
+ resolvedIds && resolvedIds.length > 0 ? new Set(resolvedIds) : null;
830
+ // Cleanup pass:
831
+ // a) segment dropped from the match list — delete its bucket.
832
+ // b) segment was re-resolved this request but pushed nothing for
833
+ // this handle — its previous bucket is stale.
834
+ // (a) is the existing behavior; (b) requires resolvedIds.
756
835
  for (const handleName of Object.keys(handleData)) {
757
836
  for (const segmentId of Object.keys(handleData[handleName])) {
758
- if (!newSegmentOrder.includes(segmentId)) {
837
+ const droppedFromMatch = !newSegmentOrder.includes(segmentId);
838
+ const reresolvedWithoutPush =
839
+ resolvedIdSet?.has(segmentId) && !data[handleName]?.[segmentId];
840
+ if (droppedFromMatch || reresolvedWithoutPush) {
759
841
  delete handleData[handleName][segmentId];
760
842
  }
761
843
  }
@@ -765,6 +847,7 @@ export function createEventController(
765
847
  handleData = data;
766
848
  }
767
849
  handleSegmentOrder = newSegmentOrder;
850
+ routeSegmentIds = newRouteSegmentIds;
768
851
 
769
852
  notifyHandles();
770
853
  }
@@ -773,6 +856,7 @@ export function createEventController(
773
856
  return {
774
857
  data: handleData,
775
858
  segmentOrder: handleSegmentOrder,
859
+ routeSegmentIds,
776
860
  };
777
861
  }
778
862
 
@@ -860,40 +944,3 @@ export function createEventController(
860
944
  hadAnyConcurrentActions: () => hadAnyConcurrentActions,
861
945
  };
862
946
  }
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
+ }