@rangojs/router 0.0.0-experimental.c873df95 → 0.0.0-experimental.c9471f9b

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 (448) hide show
  1. package/AGENTS.md +8 -4
  2. package/README.md +303 -741
  3. package/dist/bin/rango.js +724 -183
  4. package/dist/testing/vitest.js +82 -0
  5. package/dist/vite/index.js +4276 -1206
  6. package/dist/vite/index.js.bak +5448 -0
  7. package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  8. package/package.json +86 -15
  9. package/skills/api-client/SKILL.md +211 -0
  10. package/skills/breadcrumbs/SKILL.md +85 -6
  11. package/skills/bundle-analysis/SKILL.md +159 -0
  12. package/skills/cache-guide/SKILL.md +251 -24
  13. package/skills/caching/SKILL.md +338 -13
  14. package/skills/catalog.json +271 -0
  15. package/skills/comparison/SKILL.md +50 -0
  16. package/skills/comparison/agents/openai.yaml +4 -0
  17. package/skills/comparison/references/framework-comparison.md +837 -0
  18. package/skills/composability/SKILL.md +110 -4
  19. package/skills/css/SKILL.md +76 -0
  20. package/skills/debug-manifest/SKILL.md +5 -3
  21. package/skills/defer-hydration/SKILL.md +235 -0
  22. package/skills/document-cache/SKILL.md +87 -56
  23. package/skills/fonts/SKILL.md +1 -1
  24. package/skills/handler-use/SKILL.md +364 -0
  25. package/skills/hooks/SKILL.md +73 -691
  26. package/skills/hooks/data.md +273 -0
  27. package/skills/hooks/handle-and-actions.md +103 -0
  28. package/skills/hooks/navigation.md +110 -0
  29. package/skills/hooks/outlets.md +41 -0
  30. package/skills/hooks/state.md +228 -0
  31. package/skills/hooks/urls.md +135 -0
  32. package/skills/host-router/SKILL.md +129 -27
  33. package/skills/i18n/SKILL.md +276 -0
  34. package/skills/intercept/SKILL.md +94 -18
  35. package/skills/layout/SKILL.md +62 -19
  36. package/skills/links/SKILL.md +249 -17
  37. package/skills/loader/SKILL.md +302 -54
  38. package/skills/middleware/SKILL.md +59 -16
  39. package/skills/migrate-nextjs/SKILL.md +745 -0
  40. package/skills/migrate-react-router/SKILL.md +153 -0
  41. package/skills/migrate-react-router/cloudflare-workers.md +129 -0
  42. package/skills/migrate-react-router/component-migration.md +196 -0
  43. package/skills/migrate-react-router/data-and-actions.md +225 -0
  44. package/skills/migrate-react-router/route-mapping.md +271 -0
  45. package/skills/mime-routes/SKILL.md +29 -2
  46. package/skills/observability/SKILL.md +202 -0
  47. package/skills/parallel/SKILL.md +166 -10
  48. package/skills/ppr/SKILL.md +622 -0
  49. package/skills/prerender/SKILL.md +178 -124
  50. package/skills/rango/SKILL.md +318 -24
  51. package/skills/react-compiler/SKILL.md +168 -0
  52. package/skills/response-routes/SKILL.md +138 -49
  53. package/skills/route/SKILL.md +172 -9
  54. package/skills/router-setup/SKILL.md +131 -11
  55. package/skills/scripts/SKILL.md +179 -0
  56. package/skills/server-actions/SKILL.md +776 -0
  57. package/skills/shell-manifest/SKILL.md +185 -0
  58. package/skills/streams-and-websockets/SKILL.md +283 -0
  59. package/skills/tailwind/SKILL.md +28 -4
  60. package/skills/testing/SKILL.md +130 -0
  61. package/skills/testing/bindings.md +103 -0
  62. package/skills/testing/cache-prerender.md +127 -0
  63. package/skills/testing/client-components.md +124 -0
  64. package/skills/testing/e2e-parity.md +125 -0
  65. package/skills/testing/flight.md +91 -0
  66. package/skills/testing/handles.md +131 -0
  67. package/skills/testing/loader.md +128 -0
  68. package/skills/testing/middleware.md +99 -0
  69. package/skills/testing/render-handler.md +122 -0
  70. package/skills/testing/response-routes.md +95 -0
  71. package/skills/testing/reverse-and-types.md +85 -0
  72. package/skills/testing/server-actions.md +107 -0
  73. package/skills/testing/server-tree.md +128 -0
  74. package/skills/testing/setup.md +123 -0
  75. package/skills/theme/SKILL.md +1 -1
  76. package/skills/typesafety/SKILL.md +45 -616
  77. package/skills/typesafety/env-and-bindings.md +254 -0
  78. package/skills/typesafety/generated-files-and-cli.md +335 -0
  79. package/skills/typesafety/params-and-search.md +153 -0
  80. package/skills/typesafety/route-types.md +209 -0
  81. package/skills/use-cache/SKILL.md +74 -15
  82. package/skills/vercel/SKILL.md +128 -0
  83. package/skills/view-transitions/SKILL.md +337 -0
  84. package/src/__augment-tests__/augment.ts +81 -0
  85. package/src/__augment-tests__/augmented.check.ts +116 -0
  86. package/src/__internal.ts +1 -66
  87. package/src/browser/action-coordinator.ts +53 -36
  88. package/src/browser/action-fence.ts +47 -0
  89. package/src/browser/app-shell.ts +39 -0
  90. package/src/browser/app-version.ts +14 -0
  91. package/src/browser/connection-warmup.ts +134 -0
  92. package/src/browser/cookie-name.ts +140 -0
  93. package/src/browser/event-controller.ts +252 -158
  94. package/src/browser/history-state.ts +21 -0
  95. package/src/browser/index.ts +3 -3
  96. package/src/browser/invalidate-client-cache.ts +52 -0
  97. package/src/browser/logging.ts +28 -0
  98. package/src/browser/merge-segment-loaders.ts +6 -4
  99. package/src/browser/navigation-bridge.ts +131 -30
  100. package/src/browser/navigation-client.ts +204 -87
  101. package/src/browser/navigation-store-handle.ts +38 -0
  102. package/src/browser/navigation-store.ts +203 -80
  103. package/src/browser/navigation-transaction.ts +9 -59
  104. package/src/browser/network-error-handler.ts +34 -7
  105. package/src/browser/partial-update.ts +198 -125
  106. package/src/browser/prefetch/cache.ts +246 -71
  107. package/src/browser/prefetch/fetch.ts +358 -40
  108. package/src/browser/prefetch/queue.ts +113 -32
  109. package/src/browser/prefetch/resource-ready.ts +77 -0
  110. package/src/browser/rango-state.ts +158 -76
  111. package/src/browser/react/Link.tsx +112 -15
  112. package/src/browser/react/NavigationProvider.tsx +213 -122
  113. package/src/browser/react/ScrollRestoration.tsx +10 -6
  114. package/src/browser/react/context.ts +7 -2
  115. package/src/browser/react/filter-segment-order.ts +66 -7
  116. package/src/browser/react/index.ts +0 -48
  117. package/src/browser/react/location-state-shared.ts +178 -8
  118. package/src/browser/react/location-state.ts +39 -14
  119. package/src/browser/react/use-action.ts +6 -15
  120. package/src/browser/react/use-handle.ts +23 -69
  121. package/src/browser/react/use-href.tsx +8 -1
  122. package/src/browser/react/use-link-status.ts +33 -8
  123. package/src/browser/react/use-navigation.ts +32 -7
  124. package/src/browser/react/use-params.ts +20 -10
  125. package/src/browser/react/use-reverse.ts +106 -0
  126. package/src/browser/react/use-router.ts +46 -11
  127. package/src/browser/react/use-search-params.ts +0 -5
  128. package/src/browser/react/use-segments.ts +11 -21
  129. package/src/browser/response-adapter.ts +99 -8
  130. package/src/browser/rsc-router.tsx +168 -28
  131. package/src/browser/scroll-restoration.ts +44 -27
  132. package/src/browser/segment-reconciler.ts +44 -12
  133. package/src/browser/segment-structure-assert.ts +2 -2
  134. package/src/browser/server-action-bridge.ts +244 -71
  135. package/src/browser/types.ts +127 -12
  136. package/src/browser/validate-redirect-origin.ts +43 -16
  137. package/src/build/collect-fallback-refs.ts +107 -0
  138. package/src/build/generate-manifest.ts +207 -158
  139. package/src/build/generate-route-types.ts +6 -1
  140. package/src/build/index.ts +11 -3
  141. package/src/build/prefix-tree-utils.ts +123 -0
  142. package/src/build/route-trie.ts +198 -41
  143. package/src/build/route-types/ast-route-extraction.ts +15 -8
  144. package/src/build/route-types/codegen.ts +16 -5
  145. package/src/build/route-types/include-resolution.ts +464 -63
  146. package/src/build/route-types/param-extraction.ts +6 -3
  147. package/src/build/route-types/per-module-writer.ts +22 -6
  148. package/src/build/route-types/router-processing.ts +333 -117
  149. package/src/build/route-types/scan-filter.ts +9 -2
  150. package/src/build/route-types/source-scan.ts +216 -0
  151. package/src/build/runtime-discovery.ts +13 -21
  152. package/src/cache/cache-error.ts +104 -0
  153. package/src/cache/cache-key-utils.ts +58 -13
  154. package/src/cache/cache-policy.ts +108 -34
  155. package/src/cache/cache-runtime.ts +454 -101
  156. package/src/cache/cache-scope.ts +233 -101
  157. package/src/cache/cache-tag.ts +149 -0
  158. package/src/cache/cf/cf-base64.ts +33 -0
  159. package/src/cache/cf/cf-cache-constants.ts +127 -0
  160. package/src/cache/cf/cf-cache-store.ts +2206 -372
  161. package/src/cache/cf/cf-cache-types.ts +349 -0
  162. package/src/cache/cf/cf-kv-utils.ts +46 -0
  163. package/src/cache/cf/cf-tag-marker-memo.ts +105 -0
  164. package/src/cache/cf/index.ts +6 -16
  165. package/src/cache/document-cache.ts +126 -41
  166. package/src/cache/handle-snapshot.ts +70 -0
  167. package/src/cache/index.ts +23 -20
  168. package/src/cache/memory-segment-store.ts +243 -37
  169. package/src/cache/profile-registry.ts +46 -31
  170. package/src/cache/read-through-swr.ts +56 -12
  171. package/src/cache/segment-codec.ts +13 -21
  172. package/src/cache/shell-snapshot.ts +417 -0
  173. package/src/cache/tag-invalidation.ts +230 -0
  174. package/src/cache/taint.ts +55 -0
  175. package/src/cache/types.ts +194 -99
  176. package/src/cache/vercel/index.ts +11 -0
  177. package/src/cache/vercel/vercel-cache-store.ts +1132 -0
  178. package/src/client.rsc.tsx +41 -21
  179. package/src/client.tsx +116 -290
  180. package/src/cloudflare/index.ts +11 -0
  181. package/src/cloudflare/tracing.ts +108 -0
  182. package/src/component-utils.ts +19 -0
  183. package/src/components/DefaultDocument.tsx +8 -2
  184. package/src/context-var.ts +84 -2
  185. package/src/decode-loader-results.ts +52 -0
  186. package/src/defer.ts +185 -0
  187. package/src/deps/ssr.ts +0 -1
  188. package/src/encode-kv.ts +49 -0
  189. package/src/errors.ts +30 -4
  190. package/src/escape-script.ts +52 -0
  191. package/src/handle.ts +104 -34
  192. package/src/handles/MetaTags.tsx +24 -53
  193. package/src/handles/Scripts.tsx +183 -0
  194. package/src/handles/breadcrumbs.ts +35 -8
  195. package/src/handles/deferred-resolution.ts +127 -0
  196. package/src/handles/is-thenable.ts +18 -0
  197. package/src/handles/meta.ts +14 -40
  198. package/src/handles/script.ts +244 -0
  199. package/src/host/cookie-handler.ts +9 -60
  200. package/src/host/errors.ts +13 -22
  201. package/src/host/index.ts +9 -2
  202. package/src/host/pattern-matcher.ts +23 -52
  203. package/src/host/router.ts +107 -99
  204. package/src/host/testing.ts +40 -27
  205. package/src/host/types.ts +37 -4
  206. package/src/host/utils.ts +1 -1
  207. package/src/href-client.ts +137 -22
  208. package/src/index.rsc.ts +100 -13
  209. package/src/index.ts +143 -19
  210. package/src/internal-debug.ts +11 -10
  211. package/src/loader-store.ts +500 -0
  212. package/src/loader.rsc.ts +20 -13
  213. package/src/loader.ts +12 -11
  214. package/src/missing-id-error.ts +68 -0
  215. package/src/outlet-context.ts +1 -1
  216. package/src/outlet-provider.tsx +1 -5
  217. package/src/prerender/param-hash.ts +16 -16
  218. package/src/prerender/store.ts +37 -41
  219. package/src/prerender.ts +215 -86
  220. package/src/redirect-origin.ts +114 -0
  221. package/src/regex-escape.ts +8 -0
  222. package/src/render-error-thrower.tsx +20 -0
  223. package/src/response-utils.ts +62 -0
  224. package/src/reverse.ts +65 -15
  225. package/src/root-error-boundary.tsx +1 -19
  226. package/src/route-content-wrapper.tsx +19 -77
  227. package/src/route-definition/dsl-helpers.ts +461 -304
  228. package/src/route-definition/helper-factories.ts +28 -140
  229. package/src/route-definition/helpers-types.ts +153 -77
  230. package/src/route-definition/index.ts +4 -2
  231. package/src/route-definition/redirect.ts +51 -10
  232. package/src/route-definition/resolve-handler-use.ts +160 -0
  233. package/src/route-definition/use-item-types.ts +29 -0
  234. package/src/route-map-builder.ts +41 -20
  235. package/src/route-types.ts +37 -46
  236. package/src/router/basename.ts +14 -0
  237. package/src/router/content-negotiation.ts +164 -17
  238. package/src/router/error-handling.ts +45 -18
  239. package/src/router/find-match.ts +129 -30
  240. package/src/router/handler-context.ts +83 -39
  241. package/src/router/instrument.ts +355 -0
  242. package/src/router/intercept-resolution.ts +48 -24
  243. package/src/router/lazy-includes.ts +85 -62
  244. package/src/router/loader-resolution.ts +286 -56
  245. package/src/router/logging.ts +0 -6
  246. package/src/router/manifest.ts +96 -53
  247. package/src/router/match-api.ts +178 -218
  248. package/src/router/match-context.ts +0 -22
  249. package/src/router/match-handlers.ts +211 -165
  250. package/src/router/match-middleware/background-revalidation.ts +48 -21
  251. package/src/router/match-middleware/cache-lookup.ts +178 -277
  252. package/src/router/match-middleware/cache-store.ts +74 -45
  253. package/src/router/match-middleware/intercept-resolution.ts +0 -22
  254. package/src/router/match-middleware/segment-resolution.ts +45 -14
  255. package/src/router/match-pipelines.ts +1 -42
  256. package/src/router/match-result.ts +124 -50
  257. package/src/router/metrics.ts +0 -34
  258. package/src/router/middleware-types.ts +13 -142
  259. package/src/router/middleware.ts +300 -177
  260. package/src/router/navigation-snapshot.ts +133 -0
  261. package/src/router/params-util.ts +23 -0
  262. package/src/router/parse-pattern.ts +115 -0
  263. package/src/router/pattern-matching.ts +181 -150
  264. package/src/router/prefetch-cache-ttl.ts +51 -0
  265. package/src/router/prefetch-limits.ts +37 -0
  266. package/src/router/prerender-match.ts +203 -58
  267. package/src/router/preview-match.ts +35 -103
  268. package/src/router/request-classification.ts +291 -0
  269. package/src/router/revalidation.ts +123 -73
  270. package/src/router/route-snapshot.ts +256 -0
  271. package/src/router/router-context.ts +6 -28
  272. package/src/router/router-interfaces.ts +146 -35
  273. package/src/router/router-options.ts +202 -15
  274. package/src/router/router-registry.ts +2 -5
  275. package/src/router/segment-resolution/fresh.ts +204 -88
  276. package/src/router/segment-resolution/helpers.ts +115 -30
  277. package/src/router/segment-resolution/loader-cache.ts +155 -39
  278. package/src/router/segment-resolution/loader-mask.ts +60 -0
  279. package/src/router/segment-resolution/loader-snapshot.ts +259 -0
  280. package/src/router/segment-resolution/mask-nested.ts +83 -0
  281. package/src/router/segment-resolution/revalidation.ts +354 -319
  282. package/src/router/segment-resolution/static-store.ts +19 -5
  283. package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
  284. package/src/router/segment-resolution/view-transition-default.ts +56 -0
  285. package/src/router/segment-resolution.ts +5 -1
  286. package/src/router/segment-wrappers.ts +6 -5
  287. package/src/router/state-cookie-name.ts +33 -0
  288. package/src/router/substitute-pattern-params.ts +75 -0
  289. package/src/router/telemetry-otel.ts +160 -200
  290. package/src/router/telemetry.ts +105 -20
  291. package/src/router/timeout.ts +0 -20
  292. package/src/router/tracing.ts +215 -0
  293. package/src/router/trie-matching.ts +171 -59
  294. package/src/router/types.ts +10 -63
  295. package/src/router/url-params.ts +57 -0
  296. package/src/router.ts +205 -70
  297. package/src/rsc/full-payload.ts +70 -0
  298. package/src/rsc/handler-context.ts +3 -2
  299. package/src/rsc/handler.ts +682 -508
  300. package/src/rsc/helpers.ts +168 -46
  301. package/src/rsc/index.ts +2 -5
  302. package/src/rsc/json-route-result.ts +38 -0
  303. package/src/rsc/loader-fetch.ts +127 -31
  304. package/src/rsc/manifest-init.ts +33 -42
  305. package/src/rsc/nonce.ts +10 -1
  306. package/src/rsc/origin-guard.ts +39 -25
  307. package/src/rsc/progressive-enhancement.ts +138 -15
  308. package/src/rsc/redirect-guard.ts +100 -0
  309. package/src/rsc/response-cache-serve.ts +238 -0
  310. package/src/rsc/response-error.ts +79 -12
  311. package/src/rsc/response-route-handler.ts +99 -189
  312. package/src/rsc/rsc-rendering.ts +509 -73
  313. package/src/rsc/runtime-warnings.ts +23 -10
  314. package/src/rsc/server-action.ts +287 -113
  315. package/src/rsc/shell-capture.ts +1190 -0
  316. package/src/rsc/shell-serve.ts +181 -0
  317. package/src/rsc/ssr-setup.ts +18 -2
  318. package/src/rsc/transition-gate.ts +89 -0
  319. package/src/rsc/types.ts +62 -6
  320. package/src/runtime-env.ts +18 -0
  321. package/src/search-params.ts +35 -30
  322. package/src/segment-content-promise.ts +67 -0
  323. package/src/segment-loader-promise.ts +167 -0
  324. package/src/segment-system.tsx +386 -205
  325. package/src/serialize.ts +243 -0
  326. package/src/server/context.ts +280 -51
  327. package/src/server/cookie-parse.ts +32 -0
  328. package/src/server/cookie-store.ts +152 -5
  329. package/src/server/handle-store.ts +40 -38
  330. package/src/server/loader-registry.ts +38 -46
  331. package/src/server/request-context.ts +550 -171
  332. package/src/ssr/index.tsx +467 -175
  333. package/src/ssr/inject-rsc-eager.ts +167 -0
  334. package/src/ssr/ssr-root.tsx +228 -0
  335. package/src/static-handler.ts +27 -18
  336. package/src/testing/cache-status.ts +162 -0
  337. package/src/testing/collect-handle.ts +46 -0
  338. package/src/testing/dispatch.ts +813 -0
  339. package/src/testing/dom.entry.ts +22 -0
  340. package/src/testing/e2e/fixture.ts +188 -0
  341. package/src/testing/e2e/index.ts +128 -0
  342. package/src/testing/e2e/matchers.ts +35 -0
  343. package/src/testing/e2e/page-helpers.ts +272 -0
  344. package/src/testing/e2e/parity.ts +387 -0
  345. package/src/testing/e2e/server.ts +195 -0
  346. package/src/testing/flight-matchers.ts +97 -0
  347. package/src/testing/flight-normalize.ts +11 -0
  348. package/src/testing/flight-runtime.d.ts +57 -0
  349. package/src/testing/flight-tree.ts +682 -0
  350. package/src/testing/flight.entry.ts +52 -0
  351. package/src/testing/flight.ts +257 -0
  352. package/src/testing/generated-routes.ts +199 -0
  353. package/src/testing/index.ts +105 -0
  354. package/src/testing/internal/context.ts +371 -0
  355. package/src/testing/internal/flight-client-globals.ts +30 -0
  356. package/src/testing/internal/seed-vars.ts +54 -0
  357. package/src/testing/render-handler.ts +357 -0
  358. package/src/testing/render-route.tsx +584 -0
  359. package/src/testing/run-loader.ts +385 -0
  360. package/src/testing/run-middleware.ts +205 -0
  361. package/src/testing/run-transition-when.ts +164 -0
  362. package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
  363. package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
  364. package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
  365. package/src/testing/vitest-stubs/version.ts +5 -0
  366. package/src/testing/vitest.ts +305 -0
  367. package/src/theme/ThemeProvider.tsx +56 -84
  368. package/src/theme/ThemeScript.tsx +7 -9
  369. package/src/theme/constants.ts +52 -13
  370. package/src/theme/index.ts +0 -7
  371. package/src/theme/theme-context.ts +1 -5
  372. package/src/theme/theme-script.ts +22 -21
  373. package/src/theme/use-theme.ts +0 -3
  374. package/src/types/boundaries.ts +0 -35
  375. package/src/types/cache-types.ts +17 -8
  376. package/src/types/error-types.ts +30 -90
  377. package/src/types/global-namespace.ts +54 -41
  378. package/src/types/handler-context.ts +145 -79
  379. package/src/types/index.ts +3 -10
  380. package/src/types/loader-types.ts +44 -15
  381. package/src/types/request-scope.ts +112 -0
  382. package/src/types/route-config.ts +20 -52
  383. package/src/types/route-entry.ts +12 -7
  384. package/src/types/segments.ts +136 -15
  385. package/src/urls/include-helper.ts +40 -75
  386. package/src/urls/include-provider.ts +71 -0
  387. package/src/urls/index.ts +2 -11
  388. package/src/urls/path-helper-types.ts +102 -23
  389. package/src/urls/path-helper.ts +62 -111
  390. package/src/urls/pattern-types.ts +84 -19
  391. package/src/urls/response-types.ts +25 -22
  392. package/src/urls/type-extraction.ts +98 -154
  393. package/src/urls/urls-function.ts +1 -19
  394. package/src/use-loader.tsx +346 -89
  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 +36 -38
  399. package/src/vite/discovery/dev-prerender-cache.ts +117 -0
  400. package/src/vite/discovery/discover-routers.ts +130 -85
  401. package/src/vite/discovery/discovery-errors.ts +255 -0
  402. package/src/vite/discovery/gate-state.ts +171 -0
  403. package/src/vite/discovery/prerender-collection.ts +214 -132
  404. package/src/vite/discovery/route-types-writer.ts +40 -84
  405. package/src/vite/discovery/self-gen-tracking.ts +27 -1
  406. package/src/vite/discovery/state.ts +57 -4
  407. package/src/vite/discovery/virtual-module-codegen.ts +14 -34
  408. package/src/vite/index.ts +15 -0
  409. package/src/vite/inject-client-debug.ts +88 -0
  410. package/src/vite/plugin-types.ts +261 -6
  411. package/src/vite/plugins/cjs-to-esm.ts +16 -19
  412. package/src/vite/plugins/client-ref-dedup.ts +16 -11
  413. package/src/vite/plugins/client-ref-hashing.ts +28 -15
  414. package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
  415. package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  416. package/src/vite/plugins/cloudflare-protocol-stub.ts +194 -0
  417. package/src/vite/plugins/expose-action-id.ts +48 -95
  418. package/src/vite/plugins/expose-id-utils.ts +96 -51
  419. package/src/vite/plugins/expose-ids/export-analysis.ts +101 -34
  420. package/src/vite/plugins/expose-ids/handler-transform.ts +15 -64
  421. package/src/vite/plugins/expose-ids/loader-transform.ts +14 -24
  422. package/src/vite/plugins/expose-ids/router-transform.ts +118 -29
  423. package/src/vite/plugins/expose-internal-ids.ts +553 -317
  424. package/src/vite/plugins/performance-tracks.ts +89 -0
  425. package/src/vite/plugins/refresh-cmd.ts +89 -27
  426. package/src/vite/plugins/use-cache-transform.ts +73 -83
  427. package/src/vite/plugins/vercel-output.ts +384 -0
  428. package/src/vite/plugins/version-injector.ts +40 -29
  429. package/src/vite/plugins/version-plugin.ts +37 -40
  430. package/src/vite/plugins/virtual-entries.ts +138 -27
  431. package/src/vite/rango.ts +247 -132
  432. package/src/vite/router-discovery.ts +1091 -159
  433. package/src/vite/utils/ast-handler-extract.ts +26 -35
  434. package/src/vite/utils/banner.ts +1 -1
  435. package/src/vite/utils/bundle-analysis.ts +10 -15
  436. package/src/vite/utils/client-chunks.ts +184 -0
  437. package/src/vite/utils/directive-prologue.ts +40 -0
  438. package/src/vite/utils/forward-user-plugins.ts +171 -0
  439. package/src/vite/utils/manifest-utils.ts +4 -59
  440. package/src/vite/utils/package-resolution.ts +20 -52
  441. package/src/vite/utils/prerender-utils.ts +98 -38
  442. package/src/vite/utils/shared-utils.ts +144 -44
  443. package/src/browser/action-response-classifier.ts +0 -99
  444. package/src/browser/react/use-client-cache.ts +0 -58
  445. package/src/browser/shallow.ts +0 -40
  446. package/src/handles/index.ts +0 -7
  447. package/src/network-error-thrower.tsx +0 -23
  448. 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") {
@@ -113,11 +116,24 @@ export type ActionStateListener = (state: TrackedActionState) => void;
113
116
  export type HandleListener = () => void;
114
117
 
115
118
  /**
116
- * 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.
117
132
  */
118
133
  export interface HandleState {
119
134
  data: HandleData;
120
135
  segmentOrder: string[];
136
+ routeSegmentIds: string[];
121
137
  }
122
138
 
123
139
  /**
@@ -152,6 +168,22 @@ export interface ActionHandle extends Disposable {
152
168
  startStreaming(): StreamingToken;
153
169
  /** Record segments that were revalidated */
154
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>;
155
187
  /** Complete the action with result */
156
188
  complete(result?: unknown): void;
157
189
  /** Fail the action with error */
@@ -178,7 +210,12 @@ export interface EventController {
178
210
  abortNavigation(): void;
179
211
 
180
212
  // Action operations
181
- 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;
182
219
  abortAllActions(): void;
183
220
 
184
221
  // State access
@@ -202,8 +239,24 @@ export interface EventController {
202
239
  data: HandleData,
203
240
  matched?: string[],
204
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[],
205
250
  ): void;
206
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;
207
260
 
208
261
  // Params operations
209
262
  setParams(params: Record<string, string>): void;
@@ -216,10 +269,6 @@ export interface EventController {
216
269
  hadAnyConcurrentActions(): boolean;
217
270
  }
218
271
 
219
- // ============================================================================
220
- // Default States
221
- // ============================================================================
222
-
223
272
  const DEFAULT_ACTION_STATE: TrackedActionState = {
224
273
  state: "idle",
225
274
  actionId: null,
@@ -228,6 +277,13 @@ const DEFAULT_ACTION_STATE: TrackedActionState = {
228
277
  result: null,
229
278
  };
230
279
 
280
+ // Shared empty inflight-actions list. getState() hands back this exact reference
281
+ // whenever no action is inflight (the overwhelmingly common case), so the derived
282
+ // snapshot's `inflightActions` is referentially stable across notifies instead of
283
+ // a fresh [] each call. Read-only by contract (consumers only read length/spread),
284
+ // same as the shared DEFAULT_ACTION_STATE.
285
+ const EMPTY_INFLIGHT_ACTIONS: InflightAction[] = [];
286
+
231
287
  /**
232
288
  * Check if a subscription ID matches an action's full ID.
233
289
  *
@@ -240,20 +296,23 @@ function matchesActionId(
240
296
  entryActionId: string,
241
297
  ): boolean {
242
298
  if (subscriptionId.includes("#")) {
243
- // Full ID: exact match
244
299
  return subscriptionId === entryActionId;
245
300
  }
246
- // Action name only: suffix match (matches "anything#actionName")
247
301
  return entryActionId.endsWith(`#${subscriptionId}`);
248
302
  }
249
303
 
250
- // ============================================================================
251
- // Implementation
252
- // ============================================================================
304
+ // Batch rapid notifications into one microtask to prevent render storms
305
+ function makeDebouncedNotifier(listeners: Set<() => void>): () => void {
306
+ let timeout: ReturnType<typeof setTimeout> | null = null;
307
+ return () => {
308
+ if (timeout !== null) clearTimeout(timeout);
309
+ timeout = setTimeout(() => {
310
+ timeout = null;
311
+ listeners.forEach((listener) => listener());
312
+ }, 0);
313
+ };
314
+ }
253
315
 
254
- /**
255
- * Configuration for creating an event controller
256
- */
257
316
  export interface EventControllerConfig {
258
317
  initialLocation?: NavigationLocation;
259
318
  }
@@ -271,61 +330,68 @@ export interface EventControllerConfig {
271
330
  export function createEventController(
272
331
  config?: EventControllerConfig,
273
332
  ): EventController {
274
- // ========================================================================
275
- // Source of Truth
276
- // ========================================================================
277
-
278
- // Current navigation in progress (null = idle)
279
333
  let currentNavigation: NavigationEntry | null = null;
280
334
 
281
- // All in-flight actions (keyed by unique instance ID)
282
335
  const inflightActions = new Map<string, ActionEntry>();
283
336
 
284
- // Committed location (updated when navigation completes)
285
337
  let location: NavigationLocation =
286
338
  config?.initialLocation ??
287
339
  (typeof window !== "undefined"
288
340
  ? new URL(window.location.href)
289
341
  : new URL("/", "http://localhost"));
290
342
 
291
- // Track if any concurrent actions occurred (for consolidation)
292
343
  let hadAnyConcurrentActions = false;
293
344
 
294
- // Track segments revalidated by concurrent actions
295
345
  const concurrentRevalidatedSegments = new Set<string>();
296
346
 
297
- // Active streaming count (independent of navigation/action lifecycle)
347
+ // Monotonic dispatch counter: every startAction() takes the next value
348
+ // (private), so a larger sequence means the action was initiated later.
349
+ let actionDispatchSeq = 0;
350
+
351
+ // Concurrent location-state arbitration, scoped PER COHORT (history entry).
352
+ // Each cohort owns a slotKey->winningDispatchSeq map plus a refcount of its
353
+ // inflight actions; claimLocationState() consults its own cohort's map so
354
+ // same-key writes within one entry resolve to the last-initiated action
355
+ // regardless of settle order, while actions on different entries never
356
+ // compete. A cohort's map is freed once its LAST action's cleanup runs — the
357
+ // same brief post-settle grace (doSettle's 100ms timer) as other action
358
+ // teardown, not when every action everywhere settles — so a long-running
359
+ // action in one cohort can never retain arbitration keys from other cohorts
360
+ // that have since drained. It is never cleared in clearConsolidation, which
361
+ // fires
362
+ // per-action on divert/error and would let a later-settling earlier action
363
+ // wrongly reclaim a key a sibling already won.
364
+ const cohortArbitration = new Map<
365
+ string,
366
+ { keySeq: Map<string, number>; inflight: number }
367
+ >();
368
+
298
369
  let activeStreamCount = 0;
299
370
 
300
- // Handle data from RSC payload
301
371
  let handleData: HandleData = {};
302
372
  let handleSegmentOrder: string[] = [];
373
+ let routeSegmentIds: string[] = [];
303
374
 
304
- // Merged route params from current match
305
375
  let routeParams: Record<string, string> = {};
306
376
 
307
- // ========================================================================
308
- // Listeners
309
- // ========================================================================
310
-
311
377
  const stateListeners = new Set<StateListener>();
312
378
  const actionListeners = new Map<string, Set<ActionStateListener>>();
313
379
  const handleListeners = new Set<HandleListener>();
314
380
 
315
- // Debounce state notifications to batch rapid updates
316
- let notifyTimeout: ReturnType<typeof setTimeout> | null = null;
381
+ const notifyStateListeners = makeDebouncedNotifier(stateListeners);
317
382
 
318
- function notify() {
319
- if (notifyTimeout !== null) {
320
- clearTimeout(notifyTimeout);
321
- }
322
- notifyTimeout = setTimeout(() => {
323
- notifyTimeout = null;
324
- stateListeners.forEach((listener) => listener());
325
- }, 0);
383
+ // Memoized derived snapshot. Every state mutation already funnels through
384
+ // notify(), so invalidating here (synchronously, before the debounced fire)
385
+ // means a getState() call between two mutations reuses the same object — an
386
+ // unchanged state returns the SAME reference — while any real change recomputes
387
+ // on the next read. Kept null when dirty.
388
+ let cachedDerivedState: DerivedNavigationState | null = null;
389
+
390
+ function notify(): void {
391
+ cachedDerivedState = null;
392
+ notifyStateListeners();
326
393
  }
327
394
 
328
- // Debounce per-action notifications
329
395
  const actionNotifyTimeouts = new Map<string, ReturnType<typeof setTimeout>>();
330
396
 
331
397
  function notifyAction(actionId: string) {
@@ -337,8 +403,6 @@ export function createEventController(
337
403
  actionId,
338
404
  setTimeout(() => {
339
405
  actionNotifyTimeouts.delete(actionId);
340
- // Notify all listeners whose subscription ID matches this action
341
- // This includes exact matches and suffix matches (e.g., "addToCart" matches "hash#addToCart")
342
406
  for (const [subscriptionId, listeners] of actionListeners) {
343
407
  if (matchesActionId(subscriptionId, actionId)) {
344
408
  const state = getActionState(subscriptionId);
@@ -349,46 +413,34 @@ export function createEventController(
349
413
  );
350
414
  }
351
415
 
352
- // Debounce handle notifications
353
- let handleNotifyTimeout: ReturnType<typeof setTimeout> | null = null;
354
-
355
- function notifyHandles() {
356
- if (handleNotifyTimeout !== null) {
357
- clearTimeout(handleNotifyTimeout);
358
- }
359
- handleNotifyTimeout = setTimeout(() => {
360
- handleNotifyTimeout = null;
361
- handleListeners.forEach((listener) => listener());
362
- }, 0);
363
- }
364
-
365
- // ========================================================================
366
- // Derived State
367
- // ========================================================================
416
+ const notifyHandles = makeDebouncedNotifier(handleListeners);
368
417
 
369
418
  function getState(): DerivedNavigationState {
370
- // Build inflight actions list (for compatibility with existing API)
371
- const inflightActionsList: InflightAction[] = [...inflightActions.values()]
372
- .filter((a) => a.phase !== "settling")
373
- .map((a) => ({
374
- id: a.id,
375
- actionId: a.actionId,
376
- payload: a.payload,
377
- startedAt: a.startedAt,
378
- }));
379
-
380
- // State: loading if navigation OR actions are in progress
381
- // Background revalidations (skipLoadingState) don't affect visible state
419
+ if (cachedDerivedState) return cachedDerivedState;
420
+
421
+ // Skip the spread/filter/map entirely when idle — the common case — and hand
422
+ // back the shared frozen empty list for referential stability.
423
+ const inflightActionsList: InflightAction[] =
424
+ inflightActions.size === 0
425
+ ? EMPTY_INFLIGHT_ACTIONS
426
+ : [...inflightActions.values()]
427
+ .filter((a) => a.phase !== "settling")
428
+ .map((a) => ({
429
+ id: a.id,
430
+ actionId: a.actionId,
431
+ payload: a.payload,
432
+ startedAt: a.startedAt,
433
+ }));
434
+
382
435
  const hasActiveActions = inflightActionsList.length > 0;
383
436
  const isVisibleNavigation =
384
437
  currentNavigation !== null &&
385
438
  !currentNavigation.options?.skipLoadingState;
386
439
  const state = isVisibleNavigation || hasActiveActions ? "loading" : "idle";
387
440
 
388
- // Streaming: true if any active streams (navigation or action) or loading
389
441
  const isStreaming = activeStreamCount > 0 || state === "loading";
390
442
 
391
- return {
443
+ cachedDerivedState = {
392
444
  state,
393
445
  isStreaming,
394
446
  // True when a navigation is active (fetching or streaming, before
@@ -404,31 +456,28 @@ export function createEventController(
404
456
  : null,
405
457
  inflightActions: inflightActionsList,
406
458
  };
459
+ return cachedDerivedState;
407
460
  }
408
461
 
409
462
  function getActionState(actionId: string): TrackedActionState {
410
- // Find the most recent action with this ID that's not settling
411
- // Uses suffix matching when actionId is just a name (no #)
412
- const activeEntry = [...inflightActions.values()]
413
- .filter(
414
- (a) => matchesActionId(actionId, a.actionId) && a.phase !== "settling",
415
- )
416
- .sort((a, b) => b.startedAt - a.startedAt)[0];
417
-
418
- // Also check for settling entries to get result/error
419
- const settlingEntry = [...inflightActions.values()]
420
- .filter(
421
- (a) => matchesActionId(actionId, a.actionId) && a.phase === "settling",
422
- )
423
- .sort((a, b) => b.startedAt - a.startedAt)[0];
424
-
425
- const entry = activeEntry || settlingEntry;
463
+ // Nothing inflight skip building/scanning the list and return the shared
464
+ // idle snapshot (the same reference use-action falls back to).
465
+ if (inflightActions.size === 0) return DEFAULT_ACTION_STATE;
466
+
467
+ const entry = [...inflightActions.values()]
468
+ .filter((a) => matchesActionId(actionId, a.actionId))
469
+ .reduce<ActionEntry | undefined>((best, a) => {
470
+ if (!best) return a;
471
+ const aActive = a.phase !== "settling";
472
+ const bActive = best.phase !== "settling";
473
+ if (aActive !== bActive) return aActive ? a : best;
474
+ return a.startedAt > best.startedAt ? a : best;
475
+ }, undefined);
426
476
 
427
477
  if (!entry) {
428
478
  return { ...DEFAULT_ACTION_STATE };
429
479
  }
430
480
 
431
- // Derive state from phase
432
481
  let state: ActionLifecycleState;
433
482
  switch (entry.phase) {
434
483
  case "fetching":
@@ -543,10 +592,29 @@ export function createEventController(
543
592
  // Action Operations
544
593
  // ========================================================================
545
594
 
546
- function startAction(actionId: string, args: unknown[]): ActionHandle {
595
+ function startAction(
596
+ actionId: string,
597
+ args: unknown[],
598
+ cohort?: string,
599
+ ): ActionHandle {
547
600
  const id = `${actionId}-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
601
+ // Private to this handle: never exposed on the returned ActionHandle.
602
+ const dispatchSeq = actionDispatchSeq++;
548
603
  const abort = new AbortController();
549
604
 
605
+ // Register this action under its cohort (originating history entry). The
606
+ // cohort's arbitration is created on first use and freed when its last
607
+ // action settles (see doSettle). Keyless entries share the "" cohort.
608
+ const cohortId = cohort ?? "";
609
+ let arb = cohortArbitration.get(cohortId);
610
+ if (!arb) {
611
+ arb = { keySeq: new Map<string, number>(), inflight: 0 };
612
+ cohortArbitration.set(cohortId, arb);
613
+ }
614
+ // const so the captured reference stays non-undefined inside doSettle.
615
+ const arbitration = arb;
616
+ arbitration.inflight++;
617
+
550
618
  // Track if this action started while others were pending (concurrent)
551
619
  const hadConcurrent = inflightActions.size > 0;
552
620
  if (hadConcurrent) {
@@ -570,11 +638,28 @@ export function createEventController(
570
638
  let settled = false;
571
639
  let streamingEnded = false;
572
640
  let actionCompleted = false;
641
+ let cohortReleased = false;
573
642
  let pendingResult:
574
643
  | { type: "success"; value?: unknown }
575
644
  | { type: "error"; value: unknown }
576
645
  | null = null;
577
646
 
647
+ // Release this action's hold on its cohort arbitration exactly once: drop
648
+ // the refcount and, only if the map still points at THIS arbitration object,
649
+ // delete it. A newer generation may have replaced it (e.g. abortAllActions
650
+ // cleared the map and a fresh action recreated the same cohort id), so a
651
+ // stale settlement must never delete the newer one by id.
652
+ function releaseCohort() {
653
+ if (cohortReleased) return;
654
+ cohortReleased = true;
655
+ if (
656
+ --arbitration.inflight <= 0 &&
657
+ cohortArbitration.get(cohortId) === arbitration
658
+ ) {
659
+ cohortArbitration.delete(cohortId);
660
+ }
661
+ }
662
+
578
663
  function doSettle() {
579
664
  if (settled) return;
580
665
  settled = true;
@@ -582,6 +667,9 @@ export function createEventController(
582
667
  // Cleanup after brief delay (allow useAction to read result)
583
668
  setTimeout(() => {
584
669
  inflightActions.delete(id);
670
+ // Free this cohort's arbitration once its last action has settled, so
671
+ // a long-running action elsewhere cannot pin keys from a drained entry.
672
+ releaseCohort();
585
673
  // Check for consolidation
586
674
  if (inflightActions.size === 0) {
587
675
  // All actions done - reset tracking
@@ -610,6 +698,19 @@ export function createEventController(
610
698
  doSettle();
611
699
  }
612
700
 
701
+ // streamingEnded is forced here for the "streaming never started" case so
702
+ // tryFinalize can run; otherwise the streaming token's end() finalizes.
703
+ function settleWith(result: NonNullable<typeof pendingResult>) {
704
+ if (!inflightActions.has(id) || settled) return;
705
+ actionCompleted = true;
706
+ entry.completed = true;
707
+ pendingResult = result;
708
+ if (entry.phase === "fetching" || streamingEnded) {
709
+ streamingEnded = true;
710
+ tryFinalize();
711
+ }
712
+ }
713
+
613
714
  return {
614
715
  id,
615
716
  abort,
@@ -645,36 +746,32 @@ export function createEventController(
645
746
  segmentIds.forEach((id) => concurrentRevalidatedSegments.add(id));
646
747
  },
647
748
 
648
- complete(result?: unknown) {
649
- if (!inflightActions.has(id) || settled) return;
650
-
651
- actionCompleted = true;
652
- entry.completed = true;
653
- pendingResult = { type: "success", value: result };
654
-
655
- // If streaming never started or already ended, finalize immediately
656
- // Otherwise wait for streaming to end
657
- if (entry.phase === "fetching" || streamingEnded) {
658
- streamingEnded = true; // Mark as ended if never started
659
- tryFinalize();
749
+ claimLocationState(state: Record<string, unknown>) {
750
+ const winning: Record<string, unknown> = {};
751
+ // Arbitrate against this action's OWN captured arbitration object, not a
752
+ // live map lookup: a concurrent map clear/replace (abortAllActions, a
753
+ // stale settlement) must not make this action silently stop recording
754
+ // and accept every key.
755
+ const keySeq = arbitration.keySeq;
756
+ for (const key of Object.keys(state)) {
757
+ const prevSeq = keySeq.get(key);
758
+ // Strictly-greater: a later-initiated action wins a key over an
759
+ // earlier one in the same cohort regardless of arrival order. Equal
760
+ // cannot happen (dispatchSeq is unique per action).
761
+ if (prevSeq === undefined || dispatchSeq > prevSeq) {
762
+ keySeq.set(key, dispatchSeq);
763
+ winning[key] = state[key];
764
+ }
660
765
  }
661
- // If streaming is in progress, tryFinalize() will be called when streaming ends
766
+ return winning;
662
767
  },
663
768
 
664
- fail(error: unknown) {
665
- if (!inflightActions.has(id) || settled) return;
666
-
667
- actionCompleted = true;
668
- entry.completed = true;
669
- pendingResult = { type: "error", value: error };
769
+ complete(result?: unknown) {
770
+ settleWith({ type: "success", value: result });
771
+ },
670
772
 
671
- // If streaming never started or already ended, finalize immediately
672
- // Otherwise wait for streaming to end
673
- if (entry.phase === "fetching" || streamingEnded) {
674
- streamingEnded = true; // Mark as ended if never started
675
- tryFinalize();
676
- }
677
- // If streaming is in progress, tryFinalize() will be called when streaming ends
773
+ fail(error: unknown) {
774
+ settleWith({ type: "error", value: error });
678
775
  },
679
776
 
680
777
  getRevalidatedSegments(): Set<string> {
@@ -690,6 +787,10 @@ export function createEventController(
690
787
  [Symbol.dispose]() {
691
788
  // If aborted, another navigation/error took over - don't touch state
692
789
  if (abort.signal.aborted) {
790
+ // Aborted actions skip doSettle, so release the cohort hold here to
791
+ // keep the per-cohort refcount balanced (no leak when an action is
792
+ // aborted individually rather than via abortAllActions).
793
+ releaseCohort();
693
794
  inflightActions.delete(id);
694
795
  notify();
695
796
  notifyAction(actionId);
@@ -724,6 +825,7 @@ export function createEventController(
724
825
  }
725
826
  hadAnyConcurrentActions = false;
726
827
  concurrentRevalidatedSegments.clear();
828
+ cohortArbitration.clear();
727
829
  notify();
728
830
  // Notify all action listeners directly by subscription ID.
729
831
  // actionListeners keys are subscription IDs (possibly short names like
@@ -744,8 +846,13 @@ export function createEventController(
744
846
  data: HandleData,
745
847
  matched?: string[],
746
848
  isPartial?: boolean,
849
+ resolvedIds?: string[],
747
850
  ): void {
748
- const newSegmentOrder = filterSegmentOrder(matched ?? []);
851
+ const rawMatched = matched ?? [];
852
+ const newSegmentOrder = filterSegmentOrder(rawMatched);
853
+ // Separate list for useSegments(): "layouts and routes only" — strip
854
+ // parallels (".@") and loader sub-ids (D digit) without reordering.
855
+ const newRouteSegmentIds = filterRouteSegmentIds(rawMatched);
749
856
 
750
857
  if (isPartial && newSegmentOrder.length > 0) {
751
858
  // Partial update: merge new data with existing
@@ -757,10 +864,19 @@ export function createEventController(
757
864
  handleData[handleName][segmentId] = data[handleName][segmentId];
758
865
  }
759
866
  }
760
- // Clean up data from segments no longer in the matched list
867
+ const resolvedIdSet =
868
+ resolvedIds && resolvedIds.length > 0 ? new Set(resolvedIds) : null;
869
+ // Cleanup pass:
870
+ // a) segment dropped from the match list — delete its bucket.
871
+ // b) segment was re-resolved this request but pushed nothing for
872
+ // this handle — its previous bucket is stale.
873
+ // (a) is the existing behavior; (b) requires resolvedIds.
761
874
  for (const handleName of Object.keys(handleData)) {
762
875
  for (const segmentId of Object.keys(handleData[handleName])) {
763
- if (!newSegmentOrder.includes(segmentId)) {
876
+ const droppedFromMatch = !newSegmentOrder.includes(segmentId);
877
+ const reresolvedWithoutPush =
878
+ resolvedIdSet?.has(segmentId) && !data[handleName]?.[segmentId];
879
+ if (droppedFromMatch || reresolvedWithoutPush) {
764
880
  delete handleData[handleName][segmentId];
765
881
  }
766
882
  }
@@ -770,6 +886,7 @@ export function createEventController(
770
886
  handleData = data;
771
887
  }
772
888
  handleSegmentOrder = newSegmentOrder;
889
+ routeSegmentIds = newRouteSegmentIds;
773
890
 
774
891
  notifyHandles();
775
892
  }
@@ -778,9 +895,22 @@ export function createEventController(
778
895
  return {
779
896
  data: handleData,
780
897
  segmentOrder: handleSegmentOrder,
898
+ routeSegmentIds,
781
899
  };
782
900
  }
783
901
 
902
+ function setRouteSegmentIds(matched: string[]): void {
903
+ const next = filterRouteSegmentIds(matched);
904
+ if (
905
+ next.length === routeSegmentIds.length &&
906
+ next.every((id, i) => id === routeSegmentIds[i])
907
+ ) {
908
+ return;
909
+ }
910
+ routeSegmentIds = next;
911
+ notifyHandles();
912
+ }
913
+
784
914
  // ========================================================================
785
915
  // Subscriptions
786
916
  // ========================================================================
@@ -849,6 +979,7 @@ export function createEventController(
849
979
  // Handles
850
980
  setHandleData,
851
981
  getHandleState,
982
+ setRouteSegmentIds,
852
983
 
853
984
  // Params
854
985
  setParams,
@@ -865,40 +996,3 @@ export function createEventController(
865
996
  hadAnyConcurrentActions: () => hadAnyConcurrentActions,
866
997
  };
867
998
  }
868
-
869
- // ============================================================================
870
- // Singleton
871
- // ============================================================================
872
-
873
- let controllerInstance: EventController | null = null;
874
-
875
- /**
876
- * Initialize the global event controller
877
- */
878
- export function initEventController(
879
- config?: EventControllerConfig,
880
- ): EventController {
881
- if (!controllerInstance) {
882
- controllerInstance = createEventController(config);
883
- }
884
- return controllerInstance;
885
- }
886
-
887
- /**
888
- * Get the global event controller
889
- */
890
- export function getEventController(): EventController {
891
- if (!controllerInstance) {
892
- throw new Error(
893
- "Event controller not initialized. Call initEventController first.",
894
- );
895
- }
896
- return controllerInstance;
897
- }
898
-
899
- /**
900
- * Reset the controller instance (for testing)
901
- */
902
- export function resetEventController(): void {
903
- controllerInstance = null;
904
- }
@@ -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