@rangojs/router 0.0.0-experimental.14 → 0.0.0-experimental.140

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 (460) hide show
  1. package/AGENTS.md +17 -0
  2. package/README.md +432 -7
  3. package/dist/bin/rango.js +2073 -213
  4. package/dist/testing/vitest.js +82 -0
  5. package/dist/vite/index.js +7258 -2714
  6. package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  7. package/package.json +140 -67
  8. package/skills/api-client/SKILL.md +211 -0
  9. package/skills/breadcrumbs/SKILL.md +329 -0
  10. package/skills/bundle-analysis/SKILL.md +159 -0
  11. package/skills/cache-guide/SKILL.md +487 -0
  12. package/skills/caching/SKILL.md +357 -25
  13. package/skills/comparison/SKILL.md +50 -0
  14. package/skills/comparison/agents/openai.yaml +4 -0
  15. package/skills/comparison/references/framework-comparison.md +837 -0
  16. package/skills/composability/SKILL.md +246 -0
  17. package/skills/css/SKILL.md +76 -0
  18. package/skills/debug-manifest/SKILL.md +16 -10
  19. package/skills/document-cache/SKILL.md +87 -62
  20. package/skills/fonts/SKILL.md +6 -4
  21. package/skills/handler-use/SKILL.md +364 -0
  22. package/skills/hooks/SKILL.md +557 -79
  23. package/skills/host-router/SKILL.md +320 -0
  24. package/skills/i18n/SKILL.md +276 -0
  25. package/skills/intercept/SKILL.md +207 -15
  26. package/skills/layout/SKILL.md +146 -6
  27. package/skills/links/SKILL.md +304 -25
  28. package/skills/loader/SKILL.md +616 -54
  29. package/skills/middleware/SKILL.md +217 -37
  30. package/skills/migrate-nextjs/SKILL.md +611 -0
  31. package/skills/migrate-react-router/SKILL.md +927 -0
  32. package/skills/mime-routes/SKILL.md +42 -11
  33. package/skills/observability/SKILL.md +194 -0
  34. package/skills/parallel/SKILL.md +284 -3
  35. package/skills/ppr/SKILL.md +426 -0
  36. package/skills/prerender/SKILL.md +437 -52
  37. package/skills/rango/SKILL.md +369 -22
  38. package/skills/react-compiler/SKILL.md +168 -0
  39. package/skills/response-routes/SKILL.md +263 -121
  40. package/skills/route/SKILL.md +350 -21
  41. package/skills/router-setup/SKILL.md +246 -33
  42. package/skills/scripts/SKILL.md +179 -0
  43. package/skills/server-actions/SKILL.md +775 -0
  44. package/skills/shell-manifest/SKILL.md +185 -0
  45. package/skills/streams-and-websockets/SKILL.md +283 -0
  46. package/skills/tailwind/SKILL.md +27 -3
  47. package/skills/testing/SKILL.md +126 -222
  48. package/skills/testing/bindings.md +103 -0
  49. package/skills/testing/cache-prerender.md +127 -0
  50. package/skills/testing/client-components.md +124 -0
  51. package/skills/testing/e2e-parity.md +125 -0
  52. package/skills/testing/flight.md +91 -0
  53. package/skills/testing/handles.md +131 -0
  54. package/skills/testing/loader.md +128 -0
  55. package/skills/testing/middleware.md +99 -0
  56. package/skills/testing/render-handler.md +122 -0
  57. package/skills/testing/response-routes.md +95 -0
  58. package/skills/testing/reverse-and-types.md +85 -0
  59. package/skills/testing/server-actions.md +107 -0
  60. package/skills/testing/server-tree.md +128 -0
  61. package/skills/testing/setup.md +123 -0
  62. package/skills/theme/SKILL.md +9 -8
  63. package/skills/typesafety/SKILL.md +532 -103
  64. package/skills/use-cache/SKILL.md +367 -0
  65. package/skills/vercel/SKILL.md +128 -0
  66. package/skills/view-transitions/SKILL.md +337 -0
  67. package/src/__augment-tests__/augment.ts +81 -0
  68. package/src/__augment-tests__/augmented.check.ts +116 -0
  69. package/src/__internal.ts +77 -44
  70. package/src/bin/rango.ts +312 -15
  71. package/src/browser/action-coordinator.ts +114 -0
  72. package/src/browser/action-fence.ts +47 -0
  73. package/src/browser/app-shell.ts +39 -0
  74. package/src/browser/app-version.ts +14 -0
  75. package/src/browser/connection-warmup.ts +134 -0
  76. package/src/browser/cookie-name.ts +140 -0
  77. package/src/browser/event-controller.ts +293 -202
  78. package/src/browser/history-state.ts +101 -0
  79. package/src/browser/index.ts +3 -3
  80. package/src/browser/intercept-utils.ts +52 -0
  81. package/src/browser/invalidate-client-cache.ts +52 -0
  82. package/src/browser/link-interceptor.ts +24 -4
  83. package/src/browser/logging.ts +11 -0
  84. package/src/browser/merge-segment-loaders.ts +20 -12
  85. package/src/browser/navigation-bridge.ts +385 -576
  86. package/src/browser/navigation-client.ts +245 -75
  87. package/src/browser/navigation-store-handle.ts +38 -0
  88. package/src/browser/navigation-store.ts +184 -118
  89. package/src/browser/navigation-transaction.ts +247 -0
  90. package/src/browser/network-error-handler.ts +88 -0
  91. package/src/browser/partial-update.ts +412 -364
  92. package/src/browser/prefetch/cache.ts +359 -0
  93. package/src/browser/prefetch/fetch.ts +452 -0
  94. package/src/browser/prefetch/observer.ts +65 -0
  95. package/src/browser/prefetch/policy.ts +48 -0
  96. package/src/browser/prefetch/queue.ts +209 -0
  97. package/src/browser/prefetch/resource-ready.ts +77 -0
  98. package/src/browser/rango-state.ts +194 -0
  99. package/src/browser/react/Link.tsx +275 -68
  100. package/src/browser/react/NavigationProvider.tsx +265 -109
  101. package/src/browser/react/ScrollRestoration.tsx +10 -6
  102. package/src/browser/react/context.ts +11 -0
  103. package/src/browser/react/filter-segment-order.ts +70 -0
  104. package/src/browser/react/index.ts +0 -48
  105. package/src/browser/react/location-state-shared.ts +272 -60
  106. package/src/browser/react/location-state.ts +90 -20
  107. package/src/browser/react/mount-context.ts +6 -1
  108. package/src/browser/react/nonce-context.ts +23 -0
  109. package/src/browser/react/shallow-equal.ts +27 -0
  110. package/src/browser/react/use-action.ts +35 -66
  111. package/src/browser/react/use-handle.ts +39 -126
  112. package/src/browser/react/use-href.tsx +8 -1
  113. package/src/browser/react/use-link-status.ts +39 -13
  114. package/src/browser/react/use-navigation.ts +53 -69
  115. package/src/browser/react/use-params.ts +75 -0
  116. package/src/browser/react/use-pathname.ts +47 -0
  117. package/src/browser/react/use-reverse.ts +106 -0
  118. package/src/browser/react/use-router.ts +98 -0
  119. package/src/browser/react/use-search-params.ts +51 -0
  120. package/src/browser/react/use-segments.ts +72 -99
  121. package/src/browser/response-adapter.ts +164 -0
  122. package/src/browser/rsc-router.tsx +300 -72
  123. package/src/browser/scroll-restoration.ts +138 -50
  124. package/src/browser/segment-reconciler.ts +243 -0
  125. package/src/browser/segment-structure-assert.ts +17 -1
  126. package/src/browser/server-action-bridge.ts +668 -613
  127. package/src/browser/types.ts +223 -51
  128. package/src/browser/validate-redirect-origin.ts +56 -0
  129. package/src/build/collect-fallback-refs.ts +107 -0
  130. package/src/build/generate-manifest.ts +252 -161
  131. package/src/build/generate-route-types.ts +41 -1038
  132. package/src/build/index.ts +12 -7
  133. package/src/build/prefix-tree-utils.ts +123 -0
  134. package/src/build/route-trie.ts +225 -42
  135. package/src/build/route-types/ast-helpers.ts +25 -0
  136. package/src/build/route-types/ast-route-extraction.ts +105 -0
  137. package/src/build/route-types/codegen.ts +113 -0
  138. package/src/build/route-types/include-resolution.ts +812 -0
  139. package/src/build/route-types/param-extraction.ts +51 -0
  140. package/src/build/route-types/per-module-writer.ts +144 -0
  141. package/src/build/route-types/router-processing.ts +695 -0
  142. package/src/build/route-types/scan-filter.ts +85 -0
  143. package/src/build/route-types/source-scan.ts +216 -0
  144. package/src/build/runtime-discovery.ts +223 -0
  145. package/src/cache/background-task.ts +34 -0
  146. package/src/cache/cache-error.ts +104 -0
  147. package/src/cache/cache-key-utils.ts +60 -0
  148. package/src/cache/cache-policy.ts +199 -0
  149. package/src/cache/cache-runtime.ts +525 -0
  150. package/src/cache/cache-scope.ts +298 -332
  151. package/src/cache/cache-tag.ts +103 -0
  152. package/src/cache/cf/cf-base64.ts +33 -0
  153. package/src/cache/cf/cf-cache-constants.ts +127 -0
  154. package/src/cache/cf/cf-cache-store.ts +2500 -158
  155. package/src/cache/cf/cf-cache-types.ts +349 -0
  156. package/src/cache/cf/cf-kv-utils.ts +46 -0
  157. package/src/cache/cf/cf-tag-marker-memo.ts +105 -0
  158. package/src/cache/cf/index.ts +17 -17
  159. package/src/cache/document-cache.ts +199 -92
  160. package/src/cache/handle-capture.ts +81 -0
  161. package/src/cache/handle-snapshot.ts +111 -0
  162. package/src/cache/index.ts +29 -35
  163. package/src/cache/memory-segment-store.ts +363 -30
  164. package/src/cache/profile-registry.ts +88 -0
  165. package/src/cache/read-through-swr.ts +178 -0
  166. package/src/cache/segment-codec.ts +248 -0
  167. package/src/cache/shell-cache.ts +386 -0
  168. package/src/cache/tag-invalidation.ts +230 -0
  169. package/src/cache/taint.ts +153 -0
  170. package/src/cache/types.ts +156 -211
  171. package/src/cache/vercel/index.ts +11 -0
  172. package/src/cache/vercel/vercel-cache-store.ts +1102 -0
  173. package/src/client.rsc.tsx +43 -21
  174. package/src/client.tsx +131 -347
  175. package/src/cloudflare/index.ts +11 -0
  176. package/src/cloudflare/tracing.ts +109 -0
  177. package/src/component-utils.ts +23 -4
  178. package/src/components/DefaultDocument.tsx +13 -3
  179. package/src/context-var.ts +168 -0
  180. package/src/debug.ts +19 -9
  181. package/src/decode-loader-results.ts +52 -0
  182. package/src/defer.ts +185 -0
  183. package/src/deps/ssr.ts +0 -1
  184. package/src/encode-kv.ts +49 -0
  185. package/src/errors.ts +106 -10
  186. package/src/escape-script.ts +52 -0
  187. package/src/handle.ts +110 -35
  188. package/src/handles/MetaTags.tsx +83 -59
  189. package/src/handles/Scripts.tsx +183 -0
  190. package/src/handles/breadcrumbs.ts +93 -0
  191. package/src/handles/deferred-resolution.ts +127 -0
  192. package/src/handles/is-thenable.ts +18 -0
  193. package/src/handles/meta.ts +44 -53
  194. package/src/handles/script.ts +244 -0
  195. package/src/host/cookie-handler.ts +20 -65
  196. package/src/host/errors.ts +21 -30
  197. package/src/host/index.ts +13 -9
  198. package/src/host/pattern-matcher.ts +50 -79
  199. package/src/host/router.ts +151 -121
  200. package/src/host/testing.ts +45 -32
  201. package/src/host/types.ts +52 -11
  202. package/src/host/utils.ts +2 -2
  203. package/src/href-client.ts +192 -57
  204. package/src/index.rsc.ts +177 -35
  205. package/src/index.ts +255 -71
  206. package/src/internal-debug.ts +9 -2
  207. package/src/loader-store.ts +500 -0
  208. package/src/loader.rsc.ts +31 -99
  209. package/src/loader.ts +30 -12
  210. package/src/missing-id-error.ts +68 -0
  211. package/src/outlet-context.ts +1 -1
  212. package/src/outlet-provider.tsx +41 -0
  213. package/src/prerender/param-hash.ts +16 -14
  214. package/src/prerender/store.ts +121 -21
  215. package/src/prerender.ts +460 -26
  216. package/src/redirect-origin.ts +100 -0
  217. package/src/regex-escape.ts +8 -0
  218. package/src/render-error-thrower.tsx +20 -0
  219. package/src/response-utils.ts +62 -0
  220. package/src/reverse.ts +198 -128
  221. package/src/root-error-boundary.tsx +42 -48
  222. package/src/route-content-wrapper.tsx +22 -77
  223. package/src/route-definition/dsl-helpers.ts +1116 -0
  224. package/src/route-definition/helper-factories.ts +88 -0
  225. package/src/route-definition/helpers-types.ts +505 -0
  226. package/src/route-definition/index.ts +54 -0
  227. package/src/route-definition/redirect.ts +134 -0
  228. package/src/route-definition/resolve-handler-use.ts +160 -0
  229. package/src/route-definition/use-item-types.ts +29 -0
  230. package/src/route-definition.ts +1 -1481
  231. package/src/route-map-builder.ts +82 -144
  232. package/src/route-name.ts +53 -0
  233. package/src/route-types.ts +71 -45
  234. package/src/router/basename.ts +14 -0
  235. package/src/router/content-negotiation.ts +263 -0
  236. package/src/router/debug-manifest.ts +72 -0
  237. package/src/router/error-handling.ts +54 -27
  238. package/src/router/find-match.ts +245 -0
  239. package/src/router/handler-context.ts +377 -125
  240. package/src/router/instrument.ts +350 -0
  241. package/src/router/intercept-resolution.ts +59 -28
  242. package/src/router/lazy-includes.ts +254 -0
  243. package/src/router/loader-resolution.ts +421 -157
  244. package/src/router/logging.ts +106 -6
  245. package/src/router/manifest.ts +131 -57
  246. package/src/router/match-api.ts +167 -246
  247. package/src/router/match-context.ts +4 -24
  248. package/src/router/match-handlers.ts +440 -0
  249. package/src/router/match-middleware/background-revalidation.ts +117 -93
  250. package/src/router/match-middleware/cache-lookup.ts +297 -150
  251. package/src/router/match-middleware/cache-store.ts +123 -51
  252. package/src/router/match-middleware/intercept-resolution.ts +44 -43
  253. package/src/router/match-middleware/segment-resolution.ts +64 -22
  254. package/src/router/match-pipelines.ts +11 -87
  255. package/src/router/match-result.ts +121 -50
  256. package/src/router/metrics.ts +219 -28
  257. package/src/router/middleware-types.ts +93 -0
  258. package/src/router/middleware.ts +505 -441
  259. package/src/router/navigation-snapshot.ts +133 -0
  260. package/src/router/params-util.ts +23 -0
  261. package/src/router/parse-pattern.ts +115 -0
  262. package/src/router/pattern-matching.ts +311 -142
  263. package/src/router/prefetch-cache-ttl.ts +51 -0
  264. package/src/router/prefetch-limits.ts +37 -0
  265. package/src/router/prerender-match.ts +547 -0
  266. package/src/router/preview-match.ts +102 -0
  267. package/src/router/request-classification.ts +278 -0
  268. package/src/router/revalidation.ts +203 -62
  269. package/src/router/route-snapshot.ts +246 -0
  270. package/src/router/router-context.ts +45 -48
  271. package/src/router/router-interfaces.ts +554 -0
  272. package/src/router/router-options.ts +779 -0
  273. package/src/router/router-registry.ts +21 -0
  274. package/src/router/segment-resolution/fresh.ts +772 -0
  275. package/src/router/segment-resolution/helpers.ts +348 -0
  276. package/src/router/segment-resolution/loader-cache.ts +250 -0
  277. package/src/router/segment-resolution/loader-mask.ts +44 -0
  278. package/src/router/segment-resolution/revalidation.ts +1331 -0
  279. package/src/router/segment-resolution/static-store.ts +81 -0
  280. package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
  281. package/src/router/segment-resolution/view-transition-default.ts +56 -0
  282. package/src/router/segment-resolution.ts +25 -1354
  283. package/src/router/segment-wrappers.ts +292 -0
  284. package/src/router/state-cookie-name.ts +33 -0
  285. package/src/router/substitute-pattern-params.ts +75 -0
  286. package/src/router/telemetry-otel.ts +261 -0
  287. package/src/router/telemetry.ts +377 -0
  288. package/src/router/timeout.ts +128 -0
  289. package/src/router/tracing.ts +206 -0
  290. package/src/router/trie-matching.ts +240 -61
  291. package/src/router/types.ts +23 -70
  292. package/src/router/url-params.ts +57 -0
  293. package/src/router.ts +781 -2378
  294. package/src/rsc/full-payload.ts +70 -0
  295. package/src/rsc/handler-context.ts +46 -0
  296. package/src/rsc/handler.ts +905 -1142
  297. package/src/rsc/helpers.ts +275 -19
  298. package/src/rsc/index.ts +2 -25
  299. package/src/rsc/json-route-result.ts +38 -0
  300. package/src/rsc/loader-fetch.ts +305 -0
  301. package/src/rsc/manifest-init.ts +77 -0
  302. package/src/rsc/nonce.ts +14 -0
  303. package/src/rsc/origin-guard.ts +155 -0
  304. package/src/rsc/progressive-enhancement.ts +502 -0
  305. package/src/rsc/redirect-guard.ts +99 -0
  306. package/src/rsc/response-cache-serve.ts +238 -0
  307. package/src/rsc/response-error.ts +104 -0
  308. package/src/rsc/response-route-handler.ts +257 -0
  309. package/src/rsc/rsc-rendering.ts +337 -0
  310. package/src/rsc/runtime-warnings.ts +55 -0
  311. package/src/rsc/server-action.ts +522 -0
  312. package/src/rsc/shell-capture.ts +439 -0
  313. package/src/rsc/ssr-setup.ts +144 -0
  314. package/src/rsc/transition-gate.ts +89 -0
  315. package/src/rsc/types.ts +95 -12
  316. package/src/runtime-env.ts +18 -0
  317. package/src/search-params.ts +99 -82
  318. package/src/segment-content-promise.ts +67 -0
  319. package/src/segment-loader-promise.ts +149 -0
  320. package/src/segment-system.tsx +349 -134
  321. package/src/serialize.ts +243 -0
  322. package/src/server/context.ts +452 -85
  323. package/src/server/cookie-parse.ts +32 -0
  324. package/src/server/cookie-store.ts +310 -0
  325. package/src/server/fetchable-loader-store.ts +11 -6
  326. package/src/server/handle-store.ts +123 -42
  327. package/src/server/live.ts +130 -0
  328. package/src/server/loader-registry.ts +51 -100
  329. package/src/server/request-context.ts +842 -157
  330. package/src/server.ts +15 -8
  331. package/src/ssr/index.tsx +412 -136
  332. package/src/ssr/ssr-root.tsx +228 -0
  333. package/src/static-handler.ts +45 -18
  334. package/src/testing/cache-status.ts +162 -0
  335. package/src/testing/collect-handle.ts +46 -0
  336. package/src/testing/dispatch.ts +701 -0
  337. package/src/testing/dom.entry.ts +22 -0
  338. package/src/testing/e2e/fixture.ts +188 -0
  339. package/src/testing/e2e/index.ts +128 -0
  340. package/src/testing/e2e/matchers.ts +35 -0
  341. package/src/testing/e2e/page-helpers.ts +272 -0
  342. package/src/testing/e2e/parity.ts +387 -0
  343. package/src/testing/e2e/server.ts +195 -0
  344. package/src/testing/flight-matchers.ts +97 -0
  345. package/src/testing/flight-normalize.ts +11 -0
  346. package/src/testing/flight-runtime.d.ts +57 -0
  347. package/src/testing/flight-tree.ts +682 -0
  348. package/src/testing/flight.entry.ts +52 -0
  349. package/src/testing/flight.ts +257 -0
  350. package/src/testing/generated-routes.ts +199 -0
  351. package/src/testing/index.ts +105 -0
  352. package/src/testing/internal/context.ts +371 -0
  353. package/src/testing/internal/flight-client-globals.ts +30 -0
  354. package/src/testing/internal/seed-vars.ts +54 -0
  355. package/src/testing/render-handler.ts +357 -0
  356. package/src/testing/render-route.tsx +584 -0
  357. package/src/testing/run-loader.ts +385 -0
  358. package/src/testing/run-middleware.ts +205 -0
  359. package/src/testing/run-transition-when.ts +164 -0
  360. package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
  361. package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
  362. package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
  363. package/src/testing/vitest-stubs/version.ts +5 -0
  364. package/src/testing/vitest.ts +305 -0
  365. package/src/theme/ThemeProvider.tsx +40 -72
  366. package/src/theme/ThemeScript.tsx +12 -14
  367. package/src/theme/constants.ts +57 -15
  368. package/src/theme/index.ts +3 -20
  369. package/src/theme/theme-context.ts +5 -35
  370. package/src/theme/theme-script.ts +43 -39
  371. package/src/theme/use-theme.ts +0 -3
  372. package/src/types/boundaries.ts +123 -0
  373. package/src/types/cache-types.ts +207 -0
  374. package/src/types/error-types.ts +132 -0
  375. package/src/types/global-namespace.ts +113 -0
  376. package/src/types/handler-context.ts +839 -0
  377. package/src/types/index.ts +81 -0
  378. package/src/types/loader-types.ts +212 -0
  379. package/src/types/request-scope.ts +112 -0
  380. package/src/types/route-config.ts +138 -0
  381. package/src/types/route-entry.ts +114 -0
  382. package/src/types/segments.ts +271 -0
  383. package/src/types.ts +1 -1795
  384. package/src/urls/include-helper.ts +162 -0
  385. package/src/urls/include-provider.ts +71 -0
  386. package/src/urls/index.ts +43 -0
  387. package/src/urls/path-helper-types.ts +413 -0
  388. package/src/urls/path-helper.ts +275 -0
  389. package/src/urls/pattern-types.ts +124 -0
  390. package/src/urls/response-types.ts +109 -0
  391. package/src/urls/type-extraction.ts +316 -0
  392. package/src/urls/urls-function.ts +80 -0
  393. package/src/urls.ts +1 -1341
  394. package/src/use-loader.tsx +406 -141
  395. package/src/vercel/index.ts +11 -0
  396. package/src/vercel/tracing.ts +88 -0
  397. package/src/vite/debug.ts +185 -0
  398. package/src/vite/discovery/bundle-postprocess.ts +182 -0
  399. package/src/vite/discovery/discover-routers.ts +389 -0
  400. package/src/vite/discovery/discovery-errors.ts +255 -0
  401. package/src/vite/discovery/gate-state.ts +171 -0
  402. package/src/vite/discovery/prerender-collection.ts +467 -0
  403. package/src/vite/discovery/route-types-writer.ts +214 -0
  404. package/src/vite/discovery/self-gen-tracking.ts +73 -0
  405. package/src/vite/discovery/state.ts +161 -0
  406. package/src/vite/discovery/virtual-module-codegen.ts +183 -0
  407. package/src/vite/index.ts +23 -2255
  408. package/src/vite/inject-client-debug.ts +36 -0
  409. package/src/vite/plugin-types.ts +303 -0
  410. package/src/vite/plugins/cjs-to-esm.ts +90 -0
  411. package/src/vite/plugins/client-ref-dedup.ts +120 -0
  412. package/src/vite/plugins/client-ref-hashing.ts +118 -0
  413. package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
  414. package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  415. package/src/vite/plugins/cloudflare-protocol-stub.ts +194 -0
  416. package/src/vite/{expose-action-id.ts → plugins/expose-action-id.ts} +88 -110
  417. package/src/vite/{expose-id-utils.ts → plugins/expose-id-utils.ts} +89 -79
  418. package/src/vite/plugins/expose-ids/export-analysis.ts +363 -0
  419. package/src/vite/plugins/expose-ids/handler-transform.ts +130 -0
  420. package/src/vite/plugins/expose-ids/loader-transform.ts +64 -0
  421. package/src/vite/plugins/expose-ids/router-transform.ts +199 -0
  422. package/src/vite/plugins/expose-ids/types.ts +45 -0
  423. package/src/vite/plugins/expose-internal-ids.ts +805 -0
  424. package/src/vite/plugins/performance-tracks.ts +89 -0
  425. package/src/vite/plugins/refresh-cmd.ts +127 -0
  426. package/src/vite/plugins/use-cache-transform.ts +313 -0
  427. package/src/vite/plugins/vercel-output.ts +384 -0
  428. package/src/vite/plugins/version-injector.ts +94 -0
  429. package/src/vite/plugins/version-plugin.ts +263 -0
  430. package/src/vite/plugins/virtual-entries.ts +234 -0
  431. package/src/vite/plugins/virtual-stub-plugin.ts +29 -0
  432. package/src/vite/rango.ts +560 -0
  433. package/src/vite/router-discovery.ts +1638 -0
  434. package/src/vite/{ast-handler-extract.ts → utils/ast-handler-extract.ts} +200 -37
  435. package/src/vite/utils/banner.ts +36 -0
  436. package/src/vite/utils/bundle-analysis.ts +132 -0
  437. package/src/vite/utils/client-chunks.ts +184 -0
  438. package/src/vite/utils/directive-prologue.ts +40 -0
  439. package/src/vite/utils/forward-user-plugins.ts +171 -0
  440. package/src/vite/utils/manifest-utils.ts +15 -0
  441. package/src/vite/utils/package-resolution.ts +89 -0
  442. package/src/vite/utils/prerender-utils.ts +249 -0
  443. package/src/vite/utils/shared-utils.ts +269 -0
  444. package/CLAUDE.md +0 -43
  445. package/dist/vite/index.named-routes.gen.ts +0 -103
  446. package/src/browser/lru-cache.ts +0 -69
  447. package/src/browser/react/use-client-cache.ts +0 -56
  448. package/src/browser/request-controller.ts +0 -164
  449. package/src/browser/shallow.ts +0 -35
  450. package/src/cache/memory-store.ts +0 -253
  451. package/src/handles/index.ts +0 -6
  452. package/src/href-context.ts +0 -33
  453. package/src/network-error-thrower.tsx +0 -21
  454. package/src/router.gen.ts +0 -6
  455. package/src/static-handler.gen.ts +0 -5
  456. package/src/urls.gen.ts +0 -8
  457. package/src/vite/expose-internal-ids.ts +0 -1167
  458. package/src/vite/package-resolution.ts +0 -125
  459. package/src/vite/virtual-entries.ts +0 -114
  460. /package/src/vite/{version.d.ts → plugins/version.d.ts} +0 -0
@@ -8,7 +8,12 @@ import type {
8
8
  ResolvedSegment,
9
9
  RscMetadata,
10
10
  HandleData,
11
+ StreamingToken,
11
12
  } from "./types.js";
13
+ import {
14
+ filterSegmentOrder,
15
+ filterRouteSegmentIds,
16
+ } from "./react/filter-segment-order.js";
12
17
 
13
18
  // Polyfill Symbol.dispose for Safari and older browsers
14
19
  if (typeof Symbol.dispose === "undefined") {
@@ -40,7 +45,7 @@ export interface NavigationEntry {
40
45
  abort: AbortController;
41
46
  phase: NavigationPhase;
42
47
  startedAt: number;
43
- options?: NavigateOptions;
48
+ options?: NavigateOptions & { skipLoadingState?: boolean };
44
49
  }
45
50
 
46
51
  /**
@@ -77,6 +82,8 @@ export interface DerivedNavigationState {
77
82
  state: "idle" | "loading";
78
83
  /** Whether any operation is streaming */
79
84
  isStreaming: boolean;
85
+ /** Whether a navigation is active (fetching or streaming, before commit) */
86
+ isNavigating: boolean;
80
87
  /** Current committed location */
81
88
  location: NavigationLocation;
82
89
  /** URL being navigated to (null if idle) */
@@ -109,20 +116,24 @@ export type ActionStateListener = (state: TrackedActionState) => void;
109
116
  export type HandleListener = () => void;
110
117
 
111
118
  /**
112
- * 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.
113
132
  */
114
133
  export interface HandleState {
115
134
  data: HandleData;
116
135
  segmentOrder: string[];
117
- }
118
-
119
- /**
120
- * Token for tracking an active stream
121
- * Call end() when the stream completes
122
- */
123
- export interface StreamingToken {
124
- /** End this streaming operation */
125
- end(): void;
136
+ routeSegmentIds: string[];
126
137
  }
127
138
 
128
139
  /**
@@ -157,6 +168,22 @@ export interface ActionHandle extends Disposable {
157
168
  startStreaming(): StreamingToken;
158
169
  /** Record segments that were revalidated */
159
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>;
160
187
  /** Complete the action with result */
161
188
  complete(result?: unknown): void;
162
189
  /** Fail the action with error */
@@ -165,8 +192,8 @@ export interface ActionHandle extends Disposable {
165
192
  readonly settled: boolean;
166
193
  /** Check if any concurrent actions were started */
167
194
  hadConcurrentActions: boolean;
168
- /** Get segments to consolidate (only valid when this is the last action) */
169
- getConsolidationSegments(): string[] | null;
195
+ /** Get raw set of segments revalidated by concurrent actions */
196
+ getRevalidatedSegments(): Set<string>;
170
197
  /** Clear consolidation tracking */
171
198
  clearConsolidation(): void;
172
199
  }
@@ -176,16 +203,25 @@ export interface ActionHandle extends Disposable {
176
203
  */
177
204
  export interface EventController {
178
205
  // Navigation operations
179
- startNavigation(url: string, options?: NavigateOptions): NavigationHandle;
206
+ startNavigation(
207
+ url: string,
208
+ options?: NavigateOptions & { skipLoadingState?: boolean },
209
+ ): NavigationHandle;
180
210
  abortNavigation(): void;
181
211
 
182
212
  // Action operations
183
- 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;
184
219
  abortAllActions(): void;
185
220
 
186
221
  // State access
187
222
  getState(): DerivedNavigationState;
188
223
  getActionState(actionId: string): TrackedActionState;
224
+ getLocation(): NavigationLocation;
189
225
 
190
226
  // Location updates (for popstate where navigation doesn't go through startNavigation)
191
227
  setLocation(location: NavigationLocation): void;
@@ -194,7 +230,7 @@ export interface EventController {
194
230
  subscribe(listener: StateListener): () => void;
195
231
  subscribeToAction(
196
232
  actionId: string,
197
- listener: ActionStateListener
233
+ listener: ActionStateListener,
198
234
  ): () => void;
199
235
  subscribeToHandles(listener: HandleListener): () => void;
200
236
 
@@ -202,19 +238,37 @@ export interface EventController {
202
238
  setHandleData(
203
239
  data: HandleData,
204
240
  matched?: string[],
205
- isPartial?: boolean
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[],
206
250
  ): void;
207
251
  getHandleState(): HandleState;
252
+ /**
253
+ * Update ONLY `routeSegmentIds` (what `useSegments` reads) from `matched`,
254
+ * leaving `data` and `segmentOrder` (what `useHandle` collects over) untouched.
255
+ * Used while a deferred handle is resolving: the route has changed (so
256
+ * `useSegments` must reflect the new segment ids) but `useHandle` still holds
257
+ * its previous value until the deferred snapshot is applied.
258
+ */
259
+ setRouteSegmentIds(matched: string[]): void;
260
+
261
+ // Params operations
262
+ setParams(params: Record<string, string>): void;
263
+ getParams(): Record<string, string>;
208
264
 
209
265
  // Direct state access for advanced use
210
266
  getCurrentNavigation(): NavigationEntry | null;
211
267
  getInflightActions(): Map<string, ActionEntry>;
268
+ /** Whether any concurrent actions have occurred (shared across all handles) */
269
+ hadAnyConcurrentActions(): boolean;
212
270
  }
213
271
 
214
- // ============================================================================
215
- // Default States
216
- // ============================================================================
217
-
218
272
  const DEFAULT_ACTION_STATE: TrackedActionState = {
219
273
  state: "idle",
220
274
  actionId: null,
@@ -230,22 +284,28 @@ const DEFAULT_ACTION_STATE: TrackedActionState = {
230
284
  * When subscriptionId has no '#', it's just an action name and matches by suffix.
231
285
  * This allows useAction("addToCart") to match "hash#addToCart" or "src/file.ts#addToCart".
232
286
  */
233
- function matchesActionId(subscriptionId: string, entryActionId: string): boolean {
287
+ function matchesActionId(
288
+ subscriptionId: string,
289
+ entryActionId: string,
290
+ ): boolean {
234
291
  if (subscriptionId.includes("#")) {
235
- // Full ID: exact match
236
292
  return subscriptionId === entryActionId;
237
293
  }
238
- // Action name only: suffix match (matches "anything#actionName")
239
294
  return entryActionId.endsWith(`#${subscriptionId}`);
240
295
  }
241
296
 
242
- // ============================================================================
243
- // Implementation
244
- // ============================================================================
297
+ // Batch rapid notifications into one microtask to prevent render storms
298
+ function makeDebouncedNotifier(listeners: Set<() => void>): () => void {
299
+ let timeout: ReturnType<typeof setTimeout> | null = null;
300
+ return () => {
301
+ if (timeout !== null) clearTimeout(timeout);
302
+ timeout = setTimeout(() => {
303
+ timeout = null;
304
+ listeners.forEach((listener) => listener());
305
+ }, 0);
306
+ };
307
+ }
245
308
 
246
- /**
247
- * Configuration for creating an event controller
248
- */
249
309
  export interface EventControllerConfig {
250
310
  initialLocation?: NavigationLocation;
251
311
  }
@@ -261,60 +321,58 @@ export interface EventControllerConfig {
261
321
  * Actions use mergeMap semantics (all run concurrently, consolidate at end).
262
322
  */
263
323
  export function createEventController(
264
- config?: EventControllerConfig
324
+ config?: EventControllerConfig,
265
325
  ): EventController {
266
- // ========================================================================
267
- // Source of Truth
268
- // ========================================================================
269
-
270
- // Current navigation in progress (null = idle)
271
326
  let currentNavigation: NavigationEntry | null = null;
272
327
 
273
- // All in-flight actions (keyed by unique instance ID)
274
328
  const inflightActions = new Map<string, ActionEntry>();
275
329
 
276
- // Committed location (updated when navigation completes)
277
330
  let location: NavigationLocation =
278
331
  config?.initialLocation ??
279
332
  (typeof window !== "undefined"
280
333
  ? new URL(window.location.href)
281
334
  : new URL("/", "http://localhost"));
282
335
 
283
- // Track if any concurrent actions occurred (for consolidation)
284
336
  let hadAnyConcurrentActions = false;
285
337
 
286
- // Track segments revalidated by concurrent actions
287
338
  const concurrentRevalidatedSegments = new Set<string>();
288
339
 
289
- // Active streaming count (independent of navigation/action lifecycle)
340
+ // Monotonic dispatch counter: every startAction() takes the next value
341
+ // (private), so a larger sequence means the action was initiated later.
342
+ let actionDispatchSeq = 0;
343
+
344
+ // Concurrent location-state arbitration, scoped PER COHORT (history entry).
345
+ // Each cohort owns a slotKey->winningDispatchSeq map plus a refcount of its
346
+ // inflight actions; claimLocationState() consults its own cohort's map so
347
+ // same-key writes within one entry resolve to the last-initiated action
348
+ // regardless of settle order, while actions on different entries never
349
+ // compete. A cohort's map is freed once its LAST action's cleanup runs — the
350
+ // same brief post-settle grace (doSettle's 100ms timer) as other action
351
+ // teardown, not when every action everywhere settles — so a long-running
352
+ // action in one cohort can never retain arbitration keys from other cohorts
353
+ // that have since drained. It is never cleared in clearConsolidation, which
354
+ // fires
355
+ // per-action on divert/error and would let a later-settling earlier action
356
+ // wrongly reclaim a key a sibling already won.
357
+ const cohortArbitration = new Map<
358
+ string,
359
+ { keySeq: Map<string, number>; inflight: number }
360
+ >();
361
+
290
362
  let activeStreamCount = 0;
291
363
 
292
- // Handle data from RSC payload
293
364
  let handleData: HandleData = {};
294
365
  let handleSegmentOrder: string[] = [];
366
+ let routeSegmentIds: string[] = [];
295
367
 
296
- // ========================================================================
297
- // Listeners
298
- // ========================================================================
368
+ let routeParams: Record<string, string> = {};
299
369
 
300
370
  const stateListeners = new Set<StateListener>();
301
371
  const actionListeners = new Map<string, Set<ActionStateListener>>();
302
372
  const handleListeners = new Set<HandleListener>();
303
373
 
304
- // Debounce state notifications to batch rapid updates
305
- let notifyTimeout: ReturnType<typeof setTimeout> | null = null;
306
-
307
- function notify() {
308
- if (notifyTimeout !== null) {
309
- clearTimeout(notifyTimeout);
310
- }
311
- notifyTimeout = setTimeout(() => {
312
- notifyTimeout = null;
313
- stateListeners.forEach((listener) => listener());
314
- }, 0);
315
- }
374
+ const notify = makeDebouncedNotifier(stateListeners);
316
375
 
317
- // Debounce per-action notifications
318
376
  const actionNotifyTimeouts = new Map<string, ReturnType<typeof setTimeout>>();
319
377
 
320
378
  function notifyAction(actionId: string) {
@@ -326,37 +384,19 @@ export function createEventController(
326
384
  actionId,
327
385
  setTimeout(() => {
328
386
  actionNotifyTimeouts.delete(actionId);
329
- // Notify all listeners whose subscription ID matches this action
330
- // This includes exact matches and suffix matches (e.g., "addToCart" matches "hash#addToCart")
331
387
  for (const [subscriptionId, listeners] of actionListeners) {
332
388
  if (matchesActionId(subscriptionId, actionId)) {
333
389
  const state = getActionState(subscriptionId);
334
390
  listeners.forEach((listener) => listener(state));
335
391
  }
336
392
  }
337
- }, 0)
393
+ }, 0),
338
394
  );
339
395
  }
340
396
 
341
- // Debounce handle notifications
342
- let handleNotifyTimeout: ReturnType<typeof setTimeout> | null = null;
343
-
344
- function notifyHandles() {
345
- if (handleNotifyTimeout !== null) {
346
- clearTimeout(handleNotifyTimeout);
347
- }
348
- handleNotifyTimeout = setTimeout(() => {
349
- handleNotifyTimeout = null;
350
- handleListeners.forEach((listener) => listener());
351
- }, 0);
352
- }
353
-
354
- // ========================================================================
355
- // Derived State
356
- // ========================================================================
397
+ const notifyHandles = makeDebouncedNotifier(handleListeners);
357
398
 
358
399
  function getState(): DerivedNavigationState {
359
- // Build inflight actions list (for compatibility with existing API)
360
400
  const inflightActionsList: InflightAction[] = [...inflightActions.values()]
361
401
  .filter((a) => a.phase !== "settling")
362
402
  .map((a) => ({
@@ -366,43 +406,47 @@ export function createEventController(
366
406
  startedAt: a.startedAt,
367
407
  }));
368
408
 
369
- // State: loading if navigation OR actions are in progress
370
409
  const hasActiveActions = inflightActionsList.length > 0;
371
- const state =
372
- currentNavigation !== null || hasActiveActions ? "loading" : "idle";
410
+ const isVisibleNavigation =
411
+ currentNavigation !== null &&
412
+ !currentNavigation.options?.skipLoadingState;
413
+ const state = isVisibleNavigation || hasActiveActions ? "loading" : "idle";
373
414
 
374
- // Streaming: true if any active streams (navigation or action) or loading
375
415
  const isStreaming = activeStreamCount > 0 || state === "loading";
376
416
 
377
417
  return {
378
418
  state,
379
419
  isStreaming,
420
+ // True when a navigation is active (fetching or streaming, before
421
+ // commit). Broader than pendingUrl which clears during streaming.
422
+ isNavigating: currentNavigation !== null,
380
423
  location,
381
- // pendingUrl only during fetching phase - once streaming starts (URL changed), not pending
382
- pendingUrl: currentNavigation?.phase === "fetching" ? currentNavigation.url : null,
424
+ // pendingUrl only during fetching phase - once streaming starts (URL changed), not pending.
425
+ // Background revalidations (skipLoadingState) don't expose a pending URL.
426
+ pendingUrl:
427
+ currentNavigation?.phase === "fetching" &&
428
+ !currentNavigation.options?.skipLoadingState
429
+ ? currentNavigation.url
430
+ : null,
383
431
  inflightActions: inflightActionsList,
384
432
  };
385
433
  }
386
434
 
387
435
  function getActionState(actionId: string): TrackedActionState {
388
- // Find the most recent action with this ID that's not settling
389
- // Uses suffix matching when actionId is just a name (no #)
390
- const activeEntry = [...inflightActions.values()]
391
- .filter((a) => matchesActionId(actionId, a.actionId) && a.phase !== "settling")
392
- .sort((a, b) => b.startedAt - a.startedAt)[0];
393
-
394
- // Also check for settling entries to get result/error
395
- const settlingEntry = [...inflightActions.values()]
396
- .filter((a) => matchesActionId(actionId, a.actionId) && a.phase === "settling")
397
- .sort((a, b) => b.startedAt - a.startedAt)[0];
398
-
399
- const entry = activeEntry || settlingEntry;
436
+ const entry = [...inflightActions.values()]
437
+ .filter((a) => matchesActionId(actionId, a.actionId))
438
+ .reduce<ActionEntry | undefined>((best, a) => {
439
+ if (!best) return a;
440
+ const aActive = a.phase !== "settling";
441
+ const bActive = best.phase !== "settling";
442
+ if (aActive !== bActive) return aActive ? a : best;
443
+ return a.startedAt > best.startedAt ? a : best;
444
+ }, undefined);
400
445
 
401
446
  if (!entry) {
402
447
  return { ...DEFAULT_ACTION_STATE };
403
448
  }
404
449
 
405
- // Derive state from phase
406
450
  let state: ActionLifecycleState;
407
451
  switch (entry.phase) {
408
452
  case "fetching":
@@ -431,7 +475,7 @@ export function createEventController(
431
475
 
432
476
  function startNavigation(
433
477
  url: string,
434
- options?: NavigateOptions
478
+ options?: NavigateOptions & { skipLoadingState?: boolean },
435
479
  ): NavigationHandle {
436
480
  // Cancel existing navigation (switchMap semantics)
437
481
  if (currentNavigation) {
@@ -463,6 +507,7 @@ export function createEventController(
463
507
 
464
508
  startStreaming(): StreamingToken {
465
509
  let ended = false;
510
+ entry.phase = "streaming";
466
511
  activeStreamCount++;
467
512
  notify();
468
513
 
@@ -516,10 +561,29 @@ export function createEventController(
516
561
  // Action Operations
517
562
  // ========================================================================
518
563
 
519
- function startAction(actionId: string, args: unknown[]): ActionHandle {
564
+ function startAction(
565
+ actionId: string,
566
+ args: unknown[],
567
+ cohort?: string,
568
+ ): ActionHandle {
520
569
  const id = `${actionId}-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
570
+ // Private to this handle: never exposed on the returned ActionHandle.
571
+ const dispatchSeq = actionDispatchSeq++;
521
572
  const abort = new AbortController();
522
573
 
574
+ // Register this action under its cohort (originating history entry). The
575
+ // cohort's arbitration is created on first use and freed when its last
576
+ // action settles (see doSettle). Keyless entries share the "" cohort.
577
+ const cohortId = cohort ?? "";
578
+ let arb = cohortArbitration.get(cohortId);
579
+ if (!arb) {
580
+ arb = { keySeq: new Map<string, number>(), inflight: 0 };
581
+ cohortArbitration.set(cohortId, arb);
582
+ }
583
+ // const so the captured reference stays non-undefined inside doSettle.
584
+ const arbitration = arb;
585
+ arbitration.inflight++;
586
+
523
587
  // Track if this action started while others were pending (concurrent)
524
588
  const hadConcurrent = inflightActions.size > 0;
525
589
  if (hadConcurrent) {
@@ -543,11 +607,28 @@ export function createEventController(
543
607
  let settled = false;
544
608
  let streamingEnded = false;
545
609
  let actionCompleted = false;
610
+ let cohortReleased = false;
546
611
  let pendingResult:
547
612
  | { type: "success"; value?: unknown }
548
613
  | { type: "error"; value: unknown }
549
614
  | null = null;
550
615
 
616
+ // Release this action's hold on its cohort arbitration exactly once: drop
617
+ // the refcount and, only if the map still points at THIS arbitration object,
618
+ // delete it. A newer generation may have replaced it (e.g. abortAllActions
619
+ // cleared the map and a fresh action recreated the same cohort id), so a
620
+ // stale settlement must never delete the newer one by id.
621
+ function releaseCohort() {
622
+ if (cohortReleased) return;
623
+ cohortReleased = true;
624
+ if (
625
+ --arbitration.inflight <= 0 &&
626
+ cohortArbitration.get(cohortId) === arbitration
627
+ ) {
628
+ cohortArbitration.delete(cohortId);
629
+ }
630
+ }
631
+
551
632
  function doSettle() {
552
633
  if (settled) return;
553
634
  settled = true;
@@ -555,6 +636,9 @@ export function createEventController(
555
636
  // Cleanup after brief delay (allow useAction to read result)
556
637
  setTimeout(() => {
557
638
  inflightActions.delete(id);
639
+ // Free this cohort's arbitration once its last action has settled, so
640
+ // a long-running action elsewhere cannot pin keys from a drained entry.
641
+ releaseCohort();
558
642
  // Check for consolidation
559
643
  if (inflightActions.size === 0) {
560
644
  // All actions done - reset tracking
@@ -583,6 +667,19 @@ export function createEventController(
583
667
  doSettle();
584
668
  }
585
669
 
670
+ // streamingEnded is forced here for the "streaming never started" case so
671
+ // tryFinalize can run; otherwise the streaming token's end() finalizes.
672
+ function settleWith(result: NonNullable<typeof pendingResult>) {
673
+ if (!inflightActions.has(id) || settled) return;
674
+ actionCompleted = true;
675
+ entry.completed = true;
676
+ pendingResult = result;
677
+ if (entry.phase === "fetching" || streamingEnded) {
678
+ streamingEnded = true;
679
+ tryFinalize();
680
+ }
681
+ }
682
+
586
683
  return {
587
684
  id,
588
685
  abort,
@@ -618,56 +715,36 @@ export function createEventController(
618
715
  segmentIds.forEach((id) => concurrentRevalidatedSegments.add(id));
619
716
  },
620
717
 
621
- complete(result?: unknown) {
622
- if (!inflightActions.has(id) || settled) return;
623
-
624
- actionCompleted = true;
625
- entry.completed = true;
626
- pendingResult = { type: "success", value: result };
627
-
628
- // If streaming never started or already ended, finalize immediately
629
- // Otherwise wait for streaming to end
630
- if (entry.phase === "fetching" || streamingEnded) {
631
- streamingEnded = true; // Mark as ended if never started
632
- tryFinalize();
718
+ claimLocationState(state: Record<string, unknown>) {
719
+ const winning: Record<string, unknown> = {};
720
+ // Arbitrate against this action's OWN captured arbitration object, not a
721
+ // live map lookup: a concurrent map clear/replace (abortAllActions, a
722
+ // stale settlement) must not make this action silently stop recording
723
+ // and accept every key.
724
+ const keySeq = arbitration.keySeq;
725
+ for (const key of Object.keys(state)) {
726
+ const prevSeq = keySeq.get(key);
727
+ // Strictly-greater: a later-initiated action wins a key over an
728
+ // earlier one in the same cohort regardless of arrival order. Equal
729
+ // cannot happen (dispatchSeq is unique per action).
730
+ if (prevSeq === undefined || dispatchSeq > prevSeq) {
731
+ keySeq.set(key, dispatchSeq);
732
+ winning[key] = state[key];
733
+ }
633
734
  }
634
- // If streaming is in progress, tryFinalize() will be called when streaming ends
735
+ return winning;
635
736
  },
636
737
 
637
- fail(error: unknown) {
638
- if (!inflightActions.has(id) || settled) return;
639
-
640
- actionCompleted = true;
641
- entry.completed = true;
642
- pendingResult = { type: "error", value: error };
643
-
644
- // If streaming never started or already ended, finalize immediately
645
- // Otherwise wait for streaming to end
646
- if (entry.phase === "fetching" || streamingEnded) {
647
- streamingEnded = true; // Mark as ended if never started
648
- tryFinalize();
649
- }
650
- // If streaming is in progress, tryFinalize() will be called when streaming ends
738
+ complete(result?: unknown) {
739
+ settleWith({ type: "success", value: result });
651
740
  },
652
741
 
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;
742
+ fail(error: unknown) {
743
+ settleWith({ type: "error", value: error });
744
+ },
660
745
 
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);
746
+ getRevalidatedSegments(): Set<string> {
747
+ return concurrentRevalidatedSegments;
671
748
  },
672
749
 
673
750
  clearConsolidation() {
@@ -679,6 +756,10 @@ export function createEventController(
679
756
  [Symbol.dispose]() {
680
757
  // If aborted, another navigation/error took over - don't touch state
681
758
  if (abort.signal.aborted) {
759
+ // Aborted actions skip doSettle, so release the cohort hold here to
760
+ // keep the per-cohort refcount balanced (no leak when an action is
761
+ // aborted individually rather than via abortAllActions).
762
+ releaseCohort();
682
763
  inflightActions.delete(id);
683
764
  notify();
684
765
  notifyAction(actionId);
@@ -702,16 +783,27 @@ export function createEventController(
702
783
  }
703
784
 
704
785
  function abortAllActions() {
705
- for (const entry of inflightActions.values()) {
786
+ for (const [id, entry] of inflightActions) {
787
+ // Preserve settling entries — they have already been handled by
788
+ // fail()/complete() and will self-cleanup via the settlement timeout.
789
+ // Clearing them here would prevent debounced notifications from
790
+ // delivering the error/result state to subscribers.
791
+ if (entry.phase === "settling") continue;
706
792
  entry.abort.abort();
793
+ inflightActions.delete(id);
707
794
  }
708
- inflightActions.clear();
709
795
  hadAnyConcurrentActions = false;
710
796
  concurrentRevalidatedSegments.clear();
797
+ cohortArbitration.clear();
711
798
  notify();
712
- // Notify all action listeners
713
- for (const actionId of actionListeners.keys()) {
714
- notifyAction(actionId);
799
+ // Notify all action listeners directly by subscription ID.
800
+ // actionListeners keys are subscription IDs (possibly short names like
801
+ // "addToCart"), not full entry actionIds. Passing them to notifyAction
802
+ // would fail the suffix matcher — instead, notify each subscriber with
803
+ // its own state.
804
+ for (const [subscriptionId, listeners] of actionListeners) {
805
+ const state = getActionState(subscriptionId);
806
+ listeners.forEach((listener) => listener(state));
715
807
  }
716
808
  }
717
809
 
@@ -719,24 +811,17 @@ export function createEventController(
719
811
  // Handle Operations
720
812
  // ========================================================================
721
813
 
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
814
  function setHandleData(
735
815
  data: HandleData,
736
816
  matched?: string[],
737
- isPartial?: boolean
817
+ isPartial?: boolean,
818
+ resolvedIds?: string[],
738
819
  ): void {
739
- const newSegmentOrder = filterSegmentOrder(matched ?? []);
820
+ const rawMatched = matched ?? [];
821
+ const newSegmentOrder = filterSegmentOrder(rawMatched);
822
+ // Separate list for useSegments(): "layouts and routes only" — strip
823
+ // parallels (".@") and loader sub-ids (D digit) without reordering.
824
+ const newRouteSegmentIds = filterRouteSegmentIds(rawMatched);
740
825
 
741
826
  if (isPartial && newSegmentOrder.length > 0) {
742
827
  // Partial update: merge new data with existing
@@ -748,10 +833,19 @@ export function createEventController(
748
833
  handleData[handleName][segmentId] = data[handleName][segmentId];
749
834
  }
750
835
  }
751
- // Clean up data from segments no longer in the matched list
836
+ const resolvedIdSet =
837
+ resolvedIds && resolvedIds.length > 0 ? new Set(resolvedIds) : null;
838
+ // Cleanup pass:
839
+ // a) segment dropped from the match list — delete its bucket.
840
+ // b) segment was re-resolved this request but pushed nothing for
841
+ // this handle — its previous bucket is stale.
842
+ // (a) is the existing behavior; (b) requires resolvedIds.
752
843
  for (const handleName of Object.keys(handleData)) {
753
844
  for (const segmentId of Object.keys(handleData[handleName])) {
754
- if (!newSegmentOrder.includes(segmentId)) {
845
+ const droppedFromMatch = !newSegmentOrder.includes(segmentId);
846
+ const reresolvedWithoutPush =
847
+ resolvedIdSet?.has(segmentId) && !data[handleName]?.[segmentId];
848
+ if (droppedFromMatch || reresolvedWithoutPush) {
755
849
  delete handleData[handleName][segmentId];
756
850
  }
757
851
  }
@@ -761,6 +855,7 @@ export function createEventController(
761
855
  handleData = data;
762
856
  }
763
857
  handleSegmentOrder = newSegmentOrder;
858
+ routeSegmentIds = newRouteSegmentIds;
764
859
 
765
860
  notifyHandles();
766
861
  }
@@ -769,9 +864,22 @@ export function createEventController(
769
864
  return {
770
865
  data: handleData,
771
866
  segmentOrder: handleSegmentOrder,
867
+ routeSegmentIds,
772
868
  };
773
869
  }
774
870
 
871
+ function setRouteSegmentIds(matched: string[]): void {
872
+ const next = filterRouteSegmentIds(matched);
873
+ if (
874
+ next.length === routeSegmentIds.length &&
875
+ next.every((id, i) => id === routeSegmentIds[i])
876
+ ) {
877
+ return;
878
+ }
879
+ routeSegmentIds = next;
880
+ notifyHandles();
881
+ }
882
+
775
883
  // ========================================================================
776
884
  // Subscriptions
777
885
  // ========================================================================
@@ -783,7 +891,7 @@ export function createEventController(
783
891
 
784
892
  function subscribeToAction(
785
893
  actionId: string,
786
- listener: ActionStateListener
894
+ listener: ActionStateListener,
787
895
  ): () => void {
788
896
  let listeners = actionListeners.get(actionId);
789
897
  if (!listeners) {
@@ -805,6 +913,19 @@ export function createEventController(
805
913
  return () => handleListeners.delete(listener);
806
914
  }
807
915
 
916
+ // ========================================================================
917
+ // Params Operations
918
+ // ========================================================================
919
+
920
+ function setParams(params: Record<string, string>): void {
921
+ routeParams = params;
922
+ notify();
923
+ }
924
+
925
+ function getParams(): Record<string, string> {
926
+ return routeParams;
927
+ }
928
+
808
929
  // ========================================================================
809
930
  // Return Controller
810
931
  // ========================================================================
@@ -821,11 +942,17 @@ export function createEventController(
821
942
  // State
822
943
  getState,
823
944
  getActionState,
945
+ getLocation: () => location,
824
946
  setLocation,
825
947
 
826
948
  // Handles
827
949
  setHandleData,
828
950
  getHandleState,
951
+ setRouteSegmentIds,
952
+
953
+ // Params
954
+ setParams,
955
+ getParams,
829
956
 
830
957
  // Subscriptions
831
958
  subscribe,
@@ -835,42 +962,6 @@ export function createEventController(
835
962
  // Direct access
836
963
  getCurrentNavigation: () => currentNavigation,
837
964
  getInflightActions: () => inflightActions,
965
+ hadAnyConcurrentActions: () => hadAnyConcurrentActions,
838
966
  };
839
967
  }
840
-
841
- // ============================================================================
842
- // Singleton
843
- // ============================================================================
844
-
845
- let controllerInstance: EventController | null = null;
846
-
847
- /**
848
- * Initialize the global event controller
849
- */
850
- export function initEventController(
851
- config?: EventControllerConfig
852
- ): EventController {
853
- if (!controllerInstance) {
854
- controllerInstance = createEventController(config);
855
- }
856
- return controllerInstance;
857
- }
858
-
859
- /**
860
- * Get the global event controller
861
- */
862
- export function getEventController(): EventController {
863
- if (!controllerInstance) {
864
- throw new Error(
865
- "Event controller not initialized. Call initEventController first."
866
- );
867
- }
868
- return controllerInstance;
869
- }
870
-
871
- /**
872
- * Reset the controller instance (for testing)
873
- */
874
- export function resetEventController(): void {
875
- controllerInstance = null;
876
- }