@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
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Canonical inbound-Cookie-header parser.
3
+ *
4
+ * Kept as a dependency-free leaf so any consumer (request-context, the host
5
+ * dispatcher, tests) can share one implementation without pulling a heavier
6
+ * module's graph. A duplicate copy in middleware-cookies.ts was removed; the
7
+ * host copy in cookie-handler.ts was collapsed onto this one. Not part of the
8
+ * public export surface.
9
+ */
10
+ export function parseCookiesFromHeader(
11
+ cookieHeader: string | null,
12
+ ): Record<string, string> {
13
+ if (!cookieHeader) return {};
14
+
15
+ const cookies: Record<string, string> = {};
16
+ const pairs = cookieHeader.split(";");
17
+
18
+ for (const pair of pairs) {
19
+ const [name, ...rest] = pair.trim().split("=");
20
+ if (name) {
21
+ const raw = rest.join("=");
22
+ try {
23
+ cookies[name] = decodeURIComponent(raw);
24
+ } catch {
25
+ // Malformed percent-encoding: fall back to raw value
26
+ cookies[name] = raw;
27
+ }
28
+ }
29
+ }
30
+
31
+ return cookies;
32
+ }
@@ -0,0 +1,310 @@
1
+ /**
2
+ * Cookie Store — Next.js-style cookie facade backed by the response-derived model.
3
+ *
4
+ * `cookies()` returns a CookieStore scoped to the current request.
5
+ * Reads merge the original Cookie header with Set-Cookie mutations
6
+ * already queued on the response stub (last-write-wins).
7
+ * Writes append Set-Cookie to the response stub.
8
+ */
9
+
10
+ import type { CookieOptions } from "../router/middleware-types.js";
11
+ import { getRequestContext, _getRequestContext } from "./request-context.js";
12
+ import { isInsideCacheScope } from "./context.js";
13
+ import { INSIDE_CACHE_EXEC } from "../cache/taint.js";
14
+
15
+ /**
16
+ * A single cookie entry returned by get() and getAll().
17
+ */
18
+ export interface Cookie {
19
+ name: string;
20
+ value: string;
21
+ }
22
+
23
+ /**
24
+ * Request-scoped cookie store.
25
+ *
26
+ * Reads see the effective merged view (original request + same-request mutations).
27
+ * Writes append Set-Cookie headers to the shared response stub.
28
+ */
29
+ export interface CookieStore {
30
+ /** Get a single cookie by name. Returns undefined if not set or deleted. */
31
+ get(name: string): Cookie | undefined;
32
+
33
+ /** Get all effective cookies, or all cookies with a given name. */
34
+ getAll(name?: string): Cookie[];
35
+
36
+ /** Check whether a cookie exists in the effective view. */
37
+ has(name: string): boolean;
38
+
39
+ /** Set a cookie (appends Set-Cookie to the response stub). */
40
+ set(name: string, value: string, options?: CookieOptions): void;
41
+
42
+ /** Delete a cookie (appends Set-Cookie with maxAge=0 to the response stub). */
43
+ delete(name: string, options?: Pick<CookieOptions, "domain" | "path">): void;
44
+ }
45
+
46
+ /**
47
+ * Get the request-scoped cookie store.
48
+ *
49
+ * Must be called inside a request context (middleware, handler, loader, action).
50
+ * Throws if called outside request scope.
51
+ *
52
+ * @example
53
+ * ```typescript
54
+ * import { cookies } from "@rangojs/router";
55
+ *
56
+ * // In a handler, loader, or action:
57
+ * const session = cookies().get("session")?.value;
58
+ * cookies().set("session", "new-token", { httpOnly: true });
59
+ * cookies().delete("session");
60
+ * ```
61
+ */
62
+ export function cookies(): CookieStore {
63
+ const ctx = getRequestContext();
64
+ assertNotInsideCacheContext(ctx, "cookies");
65
+ assertNotInsideShellCapture(ctx, "cookies");
66
+ return createCookieStore(ctx);
67
+ }
68
+
69
+ /**
70
+ * Read-only view of HTTP headers.
71
+ * Exposes only the read methods of the Headers API.
72
+ */
73
+ export interface ReadonlyHeaders {
74
+ get(name: string): string | null;
75
+ has(name: string): boolean;
76
+ entries(): HeadersIterator<[string, string]>;
77
+ keys(): HeadersIterator<string>;
78
+ values(): HeadersIterator<string>;
79
+ forEach(
80
+ callback: (value: string, name: string, parent: ReadonlyHeaders) => void,
81
+ ): void;
82
+ [Symbol.iterator](): HeadersIterator<[string, string]>;
83
+ }
84
+
85
+ // Minimal iterator interface (avoids pulling IterableIterator from lib.dom)
86
+ type HeadersIterator<T> = IterableIterator<T>;
87
+
88
+ /**
89
+ * Throw if called inside a cache boundary — either a "use cache" function
90
+ * (`INSIDE_CACHE_EXEC` stamped on ctx by the cache runtime) or a `cache()`
91
+ * DSL boundary (`isInsideCacheScope()` — the render-store flag set while
92
+ * resolving a `type: "cache"` route entry).
93
+ *
94
+ * Reading request-scoped data (cookies, headers) inside a cached scope
95
+ * produces per-request values that are NOT reflected in the cache key, so
96
+ * they would be frozen into the shared cache entry and served to the wrong
97
+ * users. This is the same hazard for both scopes: a `cache()` boundary caches
98
+ * everything except loaders (it is the document-level "PPR shell"), so a read
99
+ * here is baked into the shell exactly like a `"use cache"` return value is
100
+ * baked into its cache entry.
101
+ *
102
+ * `isInsideCacheScope()` returns false inside loaders (loaders always run
103
+ * fresh on every request, even on a cache hit), so reading cookies()/headers()
104
+ * from a loader is allowed — loaders are the dynamic "holes" of a cached
105
+ * document.
106
+ */
107
+ function assertNotInsideCacheContext(ctx: unknown, fnName: string): void {
108
+ if (
109
+ ctx !== null &&
110
+ ctx !== undefined &&
111
+ typeof ctx === "object" &&
112
+ (INSIDE_CACHE_EXEC as symbol) in (ctx as Record<symbol, unknown>)
113
+ ) {
114
+ throw new Error(
115
+ `${fnName}() cannot be called inside a "use cache" function. ` +
116
+ `Request-scoped data (cookies, headers) varies per request but is not ` +
117
+ `reflected in the cache key, so cached results would be served to the ` +
118
+ `wrong users. Extract the value before the cached function and pass it ` +
119
+ `as an argument:\n\n` +
120
+ ` const locale = cookies().get("locale")?.value ?? "en";\n` +
121
+ ` const data = await getCachedData(locale); // locale is now in the cache key`,
122
+ );
123
+ }
124
+ if (isInsideCacheScope()) {
125
+ throw new Error(
126
+ `${fnName}() cannot be called inside a cache() boundary. ` +
127
+ `A cache() scope caches everything except loaders, so request-scoped ` +
128
+ `data (cookies, headers) read here would be frozen into the shared ` +
129
+ `cached shell and served to other users. Read it inside a loader ` +
130
+ `instead — loaders always run fresh on every request, even on a cache hit:\n\n` +
131
+ ` loader("user", () => getUser(cookies().get("session")?.value));`,
132
+ );
133
+ }
134
+ }
135
+
136
+ /**
137
+ * Throw if called during the ACTIVE background shell-capture render
138
+ * (`_shellCaptureRun` true on the derived request context built by
139
+ * shell-capture.ts). The captured shell prelude is shared across every user
140
+ * hitting the URL, so a request-scoped read here would bake one user's
141
+ * cookies/headers into markup served to others — same hazard as the cache
142
+ * scopes above, at the document tier. Loaders need no exemption: they are
143
+ * masked (never executed) during capture and remain the per-request holes of
144
+ * the shell.
145
+ *
146
+ * Keys off `_shellCaptureRun`, NOT the `_shellCapture` descriptor: the descriptor
147
+ * is also present during the FOREGROUND render (it means "a capture is wanted"),
148
+ * and the foreground must read cookies/headers normally to serve the real user.
149
+ * Only the derived capture context sets `_shellCaptureRun`.
150
+ *
151
+ * Applies only to the READ surfaces (cookies(), headers()) whose values
152
+ * become markup. Response directives (invalidateClientCache(),
153
+ * keepClientCache()) stay callable: during capture they are header effects on
154
+ * a discarded response, and on the live HIT path the full pipeline runs so their
155
+ * headers flow to the client normally.
156
+ *
157
+ * The throw makes such a route PPR-ineligible by construction: the capture
158
+ * render errors, nothing is stored, and every request keeps getting the
159
+ * normal axis-1 render.
160
+ */
161
+ function assertNotInsideShellCapture(ctx: unknown, fnName: string): void {
162
+ if (
163
+ ctx !== null &&
164
+ typeof ctx === "object" &&
165
+ (ctx as { _shellCaptureRun?: unknown })._shellCaptureRun === true
166
+ ) {
167
+ throw new Error(
168
+ `${fnName}() cannot be called while capturing a shared shell ` +
169
+ `(shell-cache middleware). The captured shell is served to every user ` +
170
+ `of this URL, so request-scoped data read here would leak one user's ` +
171
+ `${fnName === "cookies" ? "cookies" : "headers"} to others. Read it ` +
172
+ `inside a loader instead — loaders are never captured and always run ` +
173
+ `fresh per request:\n\n` +
174
+ ` loader("user", () => getUser(cookies().get("session")?.value));`,
175
+ );
176
+ }
177
+ }
178
+
179
+ const HEADERS_MUTATION_METHODS = new Set(["set", "append", "delete"]);
180
+
181
+ /**
182
+ * Get the original request headers (read-only).
183
+ *
184
+ * Must be called inside a request context.
185
+ * Returns a read-only view of the incoming request's headers.
186
+ * Mutation methods (set, append, delete) throw at runtime.
187
+ *
188
+ * @example
189
+ * ```typescript
190
+ * import { headers } from "@rangojs/router";
191
+ *
192
+ * const auth = headers().get("authorization");
193
+ * const contentType = headers().get("content-type");
194
+ * ```
195
+ */
196
+ export function headers(): ReadonlyHeaders {
197
+ const ctx = getRequestContext();
198
+ assertNotInsideCacheContext(ctx, "headers");
199
+ assertNotInsideShellCapture(ctx, "headers");
200
+ return new Proxy(ctx.request.headers, {
201
+ get(target, prop, receiver) {
202
+ if (typeof prop === "string" && HEADERS_MUTATION_METHODS.has(prop)) {
203
+ return () => {
204
+ throw new Error(
205
+ `headers().${prop}() is not allowed. headers() returns a read-only view of request headers. ` +
206
+ `Use ctx.header() to set response headers.`,
207
+ );
208
+ };
209
+ }
210
+ const value = Reflect.get(target, prop, receiver);
211
+ return typeof value === "function" ? value.bind(target) : value;
212
+ },
213
+ }) as unknown as ReadonlyHeaders;
214
+ }
215
+
216
+ /**
217
+ * Force the calling client's caches to miss from now on, from the server seat:
218
+ * write a rotated `Set-Cookie` for the rango state. The responding client
219
+ * applies it on receipt, and its history cache is marked stale by the
220
+ * jar-divergence observer at its next read. Per-client and lazy — it rotates
221
+ * only the client that receives this response, not every client.
222
+ *
223
+ * Idempotent within a request (one `Set-Cookie`). Inert (a dev warning) when
224
+ * called outside a request context. Like `cookies()`, it throws inside a
225
+ * `"use cache"` / `cache()` boundary, but is allowed from a loader (loaders are
226
+ * the dynamic holes of a cached document).
227
+ */
228
+ export function invalidateClientCache(): void {
229
+ const ctx = _getRequestContext();
230
+ if (!ctx) {
231
+ if (process.env.NODE_ENV !== "production") {
232
+ console.warn(
233
+ "[rango] invalidateClientCache() was called outside a request context; ignored.",
234
+ );
235
+ }
236
+ return;
237
+ }
238
+ assertNotInsideCacheContext(ctx, "invalidateClientCache");
239
+ ctx._rotateStateCookie();
240
+ }
241
+
242
+ /**
243
+ * Suppress a server action's automatic client-cache invalidation: tell the
244
+ * action bridge this action changed nothing a route renders, so it should leave
245
+ * the client's state and caches alone (no rotation, no prefetch wipe, no
246
+ * broadcast, no revalidation refetch). Per-response, not per-action-definition —
247
+ * only the execution knows whether anything changed.
248
+ *
249
+ * Sets an internal response header the bridge reads. Idempotent within a
250
+ * request. Inert (a dev warning) outside a request context — there is no
251
+ * automatic invalidation to suppress.
252
+ */
253
+ export function keepClientCache(): void {
254
+ const ctx = _getRequestContext();
255
+ if (!ctx) {
256
+ if (process.env.NODE_ENV !== "production") {
257
+ console.warn(
258
+ "[rango] keepClientCache() was called outside a request context; ignored.",
259
+ );
260
+ }
261
+ return;
262
+ }
263
+ assertNotInsideCacheContext(ctx, "keepClientCache");
264
+ ctx._setKeepCacheDirective();
265
+ }
266
+
267
+ /**
268
+ * Create a CookieStore backed by a RequestContext.
269
+ * @internal Shared between cookies() shorthand and context methods.
270
+ */
271
+ function createCookieStore(ctx: {
272
+ cookie(name: string): string | undefined;
273
+ cookies(): Record<string, string>;
274
+ setCookie(name: string, value: string, options?: CookieOptions): void;
275
+ deleteCookie(
276
+ name: string,
277
+ options?: Pick<CookieOptions, "domain" | "path">,
278
+ ): void;
279
+ }): CookieStore {
280
+ return {
281
+ get(name: string): Cookie | undefined {
282
+ const value = ctx.cookie(name);
283
+ return value !== undefined ? { name, value } : undefined;
284
+ },
285
+
286
+ getAll(name?: string): Cookie[] {
287
+ const all = ctx.cookies();
288
+ if (name !== undefined) {
289
+ const value = all[name];
290
+ return value !== undefined ? [{ name, value }] : [];
291
+ }
292
+ return Object.entries(all).map(([n, v]) => ({ name: n, value: v }));
293
+ },
294
+
295
+ has(name: string): boolean {
296
+ return ctx.cookie(name) !== undefined;
297
+ },
298
+
299
+ set(name: string, value: string, options?: CookieOptions): void {
300
+ ctx.setCookie(name, value, options);
301
+ },
302
+
303
+ delete(
304
+ name: string,
305
+ options?: Pick<CookieOptions, "domain" | "path">,
306
+ ): void {
307
+ ctx.deleteCookie(name, options);
308
+ },
309
+ };
310
+ }
@@ -12,21 +12,26 @@
12
12
  import type { LoaderFn } from "../types.js";
13
13
  import type { MiddlewareFn } from "../router/middleware.js";
14
14
 
15
- const fetchableLoaderRegistry = new Map<
16
- string,
17
- { fn: LoaderFn<any, any, any>; middleware: MiddlewareFn[] }
18
- >();
15
+ export interface LoaderRegistryEntry {
16
+ fn: LoaderFn<any, any, any>;
17
+ middleware: MiddlewareFn[];
18
+ /** Whether this loader is fetchable via the _rsc_loader endpoint. */
19
+ fetchable: boolean;
20
+ }
21
+
22
+ const fetchableLoaderRegistry = new Map<string, LoaderRegistryEntry>();
19
23
 
20
24
  export function registerFetchableLoader(
21
25
  id: string,
22
26
  fn: LoaderFn<any, any, any>,
23
27
  middleware: MiddlewareFn[],
28
+ fetchable: boolean,
24
29
  ): void {
25
- fetchableLoaderRegistry.set(id, { fn, middleware });
30
+ fetchableLoaderRegistry.set(id, { fn, middleware, fetchable });
26
31
  }
27
32
 
28
33
  export function getFetchableLoader(
29
34
  id: string,
30
- ): { fn: LoaderFn<any, any, any>; middleware: MiddlewareFn[] } | undefined {
35
+ ): LoaderRegistryEntry | undefined {
31
36
  return fetchableLoaderRegistry.get(id);
32
37
  }
@@ -14,9 +14,37 @@
14
14
  export type HandleData = Record<string, Record<string, unknown[]>>;
15
15
 
16
16
  /**
17
- * Deep clone handle data to create a snapshot.
18
- * @internal
17
+ * Build a HandleData snapshot from a HandleStore using segment ordering.
18
+ * Reads data directly from the store for each segment in order.
19
19
  */
20
+ export function buildHandleSnapshot(
21
+ handleStore: HandleStore,
22
+ segmentOrder: string[],
23
+ ): HandleData {
24
+ const data: HandleData = {};
25
+ for (const segmentId of segmentOrder) {
26
+ const segData = handleStore.getDataForSegment(segmentId);
27
+ for (const handleName in segData) {
28
+ if (!data[handleName]) data[handleName] = {};
29
+ data[handleName][segmentId] = segData[handleName];
30
+ }
31
+ }
32
+ return data;
33
+ }
34
+
35
+ function createLateHandlePushError(
36
+ handleName: string,
37
+ segmentId: string,
38
+ ): Error {
39
+ const error = new Error(
40
+ `Handle "${handleName}" for segment "${segmentId}" was pushed after handle collection completed. ` +
41
+ `This usually means an async JSX subtree suspended and later tried to push a handle during streaming. ` +
42
+ `Push handles from the route/layout handler or during the initial synchronous JSX render instead.`,
43
+ );
44
+ error.name = "LateHandlePushError";
45
+ return error;
46
+ }
47
+
20
48
  function cloneHandleData(data: HandleData): HandleData {
21
49
  const clone: HandleData = {};
22
50
  for (const handleName in data) {
@@ -44,11 +72,26 @@ export interface HandleStore {
44
72
  track<T>(promise: Promise<T>): Promise<T>;
45
73
 
46
74
  /**
47
- * Promise that resolves when all tracked handlers have settled.
48
- * Does not reject - uses Promise.allSettled internally.
75
+ * Signal that no more track() calls will be made.
76
+ * settled will not resolve until seal() is called AND all tracked
77
+ * promises have settled. Calling stream() or getData() auto-seals.
78
+ */
79
+ seal(): void;
80
+
81
+ /**
82
+ * Promise that resolves when the store is sealed AND all tracked
83
+ * handlers have settled.
49
84
  */
50
85
  readonly settled: Promise<void>;
51
86
 
87
+ /**
88
+ * Optional error callback for late streaming-handle failures.
89
+ * Called when push() throws LateHandlePushError (handle pushed after
90
+ * stream completion). Allows the router to surface these errors
91
+ * to onError and telemetry.
92
+ */
93
+ onError?: (error: Error) => void;
94
+
52
95
  /**
53
96
  * Push handle data for a specific handle and segment.
54
97
  * Multiple pushes to the same handle/segment accumulate in an array.
@@ -58,9 +101,7 @@ export interface HandleStore {
58
101
 
59
102
  /**
60
103
  * Get all collected handle data after all handlers have settled.
61
- * Returns a promise that waits for `settled`, then returns the data.
62
- * The data may contain unresolved promises which RSC will stream.
63
- * @deprecated Use stream() for progressive updates
104
+ * Waits for `settled`, then returns the finalized data.
64
105
  */
65
106
  getData(): Promise<HandleData>;
66
107
 
@@ -81,7 +122,10 @@ export interface HandleStore {
81
122
  * Replay cached handle data back into the store (for cache hits).
82
123
  * Used to restore handle data when serving cached segments.
83
124
  */
84
- replaySegmentData(segmentId: string, segmentHandles: Record<string, unknown[]>): void;
125
+ replaySegmentData(
126
+ segmentId: string,
127
+ segmentHandles: Record<string, unknown[]>,
128
+ ): void;
85
129
  }
86
130
 
87
131
  /**
@@ -105,11 +149,35 @@ export interface HandleStore {
105
149
  * ```
106
150
  */
107
151
  export function createHandleStore(): HandleStore {
108
- const pending: Promise<unknown>[] = [];
109
152
  const data: HandleData = {};
110
153
 
111
- // Queue for pending emissions and resolver for waiting consumer
112
- let pendingEmissions: HandleData[] = [];
154
+ // Settlement barrier: resolved only when sealed AND inflight === 0.
155
+ // seal() signals "no more track() calls". Each track() increments
156
+ // inflightCount, each promise.finally() decrements. settled resolves
157
+ // once both conditions are met — even if tracks are added while
158
+ // earlier ones are still in flight.
159
+ let sealed = false;
160
+ let inflightCount = 0;
161
+ let drainWaiters: (() => void)[] = [];
162
+
163
+ function notifyDrain() {
164
+ if (sealed && inflightCount === 0 && drainWaiters.length > 0) {
165
+ const waiters = drainWaiters;
166
+ drainWaiters = [];
167
+ for (const resolve of waiters) resolve();
168
+ }
169
+ }
170
+
171
+ function sealInternal() {
172
+ if (sealed) return;
173
+ sealed = true;
174
+ notifyDrain();
175
+ }
176
+
177
+ // Dirty flag for pending emissions and resolver for waiting consumer.
178
+ // stream() only ever yields the latest full state, so we track a single
179
+ // dirty bit and clone `data` once at yield time instead of per push.
180
+ let hasPendingEmission = false;
113
181
  let emissionResolver: (() => void) | null = null;
114
182
  let completed = false;
115
183
 
@@ -124,7 +192,7 @@ export function createHandleStore(): HandleStore {
124
192
 
125
193
  // Wait for the next emission or completion
126
194
  function waitForEmission(): Promise<void> {
127
- if (pendingEmissions.length > 0 || completed) {
195
+ if (hasPendingEmission || completed) {
128
196
  return Promise.resolve();
129
197
  }
130
198
  return new Promise((resolve) => {
@@ -134,18 +202,36 @@ export function createHandleStore(): HandleStore {
134
202
 
135
203
  return {
136
204
  track<T>(promise: Promise<T>): Promise<T> {
137
- pending.push(promise);
205
+ inflightCount++;
206
+ // Use .then() instead of .finally() to avoid creating an unhandled rejection
207
+ // branch when the promise rejects. .finally() re-throws on a new branch that
208
+ // can crash the process if not caught.
209
+ const onSettle = () => {
210
+ inflightCount--;
211
+ notifyDrain();
212
+ };
213
+ promise.then(onSettle, onSettle);
138
214
  return promise;
139
215
  },
140
216
 
217
+ seal() {
218
+ sealInternal();
219
+ },
220
+
141
221
  get settled(): Promise<void> {
142
- if (pending.length === 0) {
143
- return Promise.resolve();
144
- }
145
- return Promise.allSettled(pending).then(() => {});
222
+ if (sealed && inflightCount === 0) return Promise.resolve();
223
+ return new Promise<void>((resolve) => {
224
+ drainWaiters.push(resolve);
225
+ });
146
226
  },
147
227
 
148
228
  push(handleName: string, segmentId: string, value: unknown): void {
229
+ if (completed) {
230
+ const error = createLateHandlePushError(handleName, segmentId);
231
+ if (this.onError) this.onError(error);
232
+ throw error;
233
+ }
234
+
149
235
  if (!data[handleName]) {
150
236
  data[handleName] = {};
151
237
  }
@@ -154,49 +240,42 @@ export function createHandleStore(): HandleStore {
154
240
  }
155
241
  data[handleName][segmentId].push(value);
156
242
 
157
- // Queue a snapshot for emission
158
- pendingEmissions.push(cloneHandleData(data));
243
+ // Mark dirty; the actual snapshot is cloned once at yield time.
244
+ hasPendingEmission = true;
159
245
  signalEmission();
160
246
  },
161
247
 
162
248
  getData(): Promise<HandleData> {
163
- return this.settled.then(() => data);
249
+ sealInternal();
250
+ return this.settled.then(() => cloneHandleData(data));
164
251
  },
165
252
 
166
253
  async *stream(): AsyncGenerator<HandleData, void, unknown> {
167
- // Set up completion handler
254
+ sealInternal();
255
+
168
256
  this.settled.then(() => {
169
257
  completed = true;
170
258
  signalEmission();
171
259
  });
172
260
 
173
- // Initial small delay to batch rapid synchronous pushes
174
- // This allows multiple handles pushing in quick succession to be batched
261
+ // Batch rapid synchronous pushes with initial delay
175
262
  await new Promise((resolve) => setTimeout(resolve, 0));
176
263
 
177
- // If we already have data, yield the accumulated state
178
264
  if (Object.keys(data).length > 0) {
179
- // Clear pending emissions since we're yielding current state
180
- pendingEmissions = [];
265
+ hasPendingEmission = false;
181
266
  yield cloneHandleData(data);
182
267
  }
183
268
 
184
- // Continue streaming on each push
185
269
  while (!completed) {
186
270
  await waitForEmission();
187
271
 
188
- // Yield all pending emissions (yield latest only)
189
- if (pendingEmissions.length > 0) {
190
- // Skip intermediate states, yield the latest
191
- const latest = pendingEmissions[pendingEmissions.length - 1];
192
- pendingEmissions = [];
193
- yield latest;
272
+ if (hasPendingEmission) {
273
+ hasPendingEmission = false;
274
+ yield cloneHandleData(data);
194
275
  }
195
276
  }
196
277
 
197
- // Final yield only if there are pending emissions that weren't yielded
198
- // (handles that pushed after our last yield but before completion)
199
- if (pendingEmissions.length > 0) {
278
+ if (hasPendingEmission) {
200
279
  yield cloneHandleData(data);
201
280
  }
202
281
  },
@@ -211,18 +290,20 @@ export function createHandleStore(): HandleStore {
211
290
  return result;
212
291
  },
213
292
 
214
- replaySegmentData(segmentId: string, segmentHandles: Record<string, unknown[]>): void {
293
+ replaySegmentData(
294
+ segmentId: string,
295
+ segmentHandles: Record<string, unknown[]>,
296
+ ): void {
215
297
  for (const handleName in segmentHandles) {
216
298
  if (!data[handleName]) {
217
299
  data[handleName] = {};
218
300
  }
219
- // Replace with replayed data (not append) to avoid handle bleeding between routes.
220
- // When a cached segment is restored, its handles should replace any existing data
221
- // for that segment, not accumulate on top of data from a different route.
301
+ // Replace (not append) to avoid handle bleeding between routes.
302
+ // Cached segment restoration should replace existing data for that
303
+ // segment, not accumulate on top of data from a different route.
222
304
  data[handleName][segmentId] = [...segmentHandles[handleName]];
223
305
  }
224
- // Trigger emission for streaming
225
- pendingEmissions.push(cloneHandleData(data));
306
+ hasPendingEmission = true;
226
307
  signalEmission();
227
308
  },
228
309
  };