@rangojs/router 0.0.0-experimental.a769fbe7 → 0.0.0-experimental.ac99d918

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 (374) hide show
  1. package/README.md +211 -43
  2. package/dist/bin/rango.js +279 -102
  3. package/dist/testing/vitest.js +82 -0
  4. package/dist/vite/index.js +3313 -1160
  5. package/dist/vite/index.js.bak +5448 -0
  6. package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  7. package/package.json +62 -11
  8. package/skills/api-client/SKILL.md +211 -0
  9. package/skills/breadcrumbs/SKILL.md +63 -1
  10. package/skills/bundle-analysis/SKILL.md +159 -0
  11. package/skills/cache-guide/SKILL.md +222 -30
  12. package/skills/caching/SKILL.md +263 -8
  13. package/skills/composability/SKILL.md +27 -2
  14. package/skills/css/SKILL.md +76 -0
  15. package/skills/document-cache/SKILL.md +78 -55
  16. package/skills/handler-use/SKILL.md +364 -0
  17. package/skills/hooks/SKILL.md +250 -30
  18. package/skills/host-router/SKILL.md +124 -22
  19. package/skills/i18n/SKILL.md +276 -0
  20. package/skills/intercept/SKILL.md +49 -5
  21. package/skills/layout/SKILL.md +35 -9
  22. package/skills/links/SKILL.md +249 -17
  23. package/skills/loader/SKILL.md +223 -9
  24. package/skills/middleware/SKILL.md +52 -13
  25. package/skills/migrate-nextjs/SKILL.md +584 -0
  26. package/skills/migrate-react-router/SKILL.md +769 -0
  27. package/skills/mime-routes/SKILL.md +27 -0
  28. package/skills/observability/SKILL.md +137 -0
  29. package/skills/parallel/SKILL.md +77 -7
  30. package/skills/prerender/SKILL.md +123 -100
  31. package/skills/rango/SKILL.md +250 -22
  32. package/skills/react-compiler/SKILL.md +168 -0
  33. package/skills/response-routes/SKILL.md +122 -47
  34. package/skills/route/SKILL.md +66 -5
  35. package/skills/router-setup/SKILL.md +38 -3
  36. package/skills/server-actions/SKILL.md +775 -0
  37. package/skills/streams-and-websockets/SKILL.md +283 -0
  38. package/skills/tailwind/SKILL.md +27 -3
  39. package/skills/testing/SKILL.md +129 -0
  40. package/skills/testing/bindings.md +89 -0
  41. package/skills/testing/cache-prerender.md +124 -0
  42. package/skills/testing/client-components.md +122 -0
  43. package/skills/testing/e2e-parity.md +125 -0
  44. package/skills/testing/flight.md +92 -0
  45. package/skills/testing/handles.md +129 -0
  46. package/skills/testing/loader.md +128 -0
  47. package/skills/testing/middleware.md +99 -0
  48. package/skills/testing/render-handler.md +121 -0
  49. package/skills/testing/response-routes.md +95 -0
  50. package/skills/testing/reverse-and-types.md +84 -0
  51. package/skills/testing/server-actions.md +107 -0
  52. package/skills/testing/server-tree.md +128 -0
  53. package/skills/testing/setup.md +120 -0
  54. package/skills/typesafety/SKILL.md +319 -27
  55. package/skills/use-cache/SKILL.md +36 -5
  56. package/skills/vercel/SKILL.md +107 -0
  57. package/skills/view-transitions/SKILL.md +294 -0
  58. package/src/__augment-tests__/augment.ts +81 -0
  59. package/src/__augment-tests__/augmented.check.ts +116 -0
  60. package/src/__internal.ts +1 -66
  61. package/src/browser/action-coordinator.ts +53 -36
  62. package/src/browser/action-fence.ts +47 -0
  63. package/src/browser/app-shell.ts +39 -0
  64. package/src/browser/app-version.ts +14 -0
  65. package/src/browser/cookie-name.ts +140 -0
  66. package/src/browser/event-controller.ts +81 -147
  67. package/src/browser/history-state.ts +21 -0
  68. package/src/browser/index.ts +3 -3
  69. package/src/browser/invalidate-client-cache.ts +52 -0
  70. package/src/browser/navigation-bridge.ts +66 -14
  71. package/src/browser/navigation-client.ts +172 -109
  72. package/src/browser/navigation-store-handle.ts +38 -0
  73. package/src/browser/navigation-store.ts +76 -67
  74. package/src/browser/navigation-transaction.ts +9 -59
  75. package/src/browser/partial-update.ts +79 -93
  76. package/src/browser/prefetch/cache.ts +180 -62
  77. package/src/browser/prefetch/fetch.ts +252 -39
  78. package/src/browser/prefetch/queue.ts +42 -8
  79. package/src/browser/rango-state.ts +158 -76
  80. package/src/browser/react/Link.tsx +72 -10
  81. package/src/browser/react/NavigationProvider.tsx +83 -31
  82. package/src/browser/react/ScrollRestoration.tsx +10 -6
  83. package/src/browser/react/context.ts +7 -2
  84. package/src/browser/react/filter-segment-order.ts +49 -7
  85. package/src/browser/react/index.ts +0 -48
  86. package/src/browser/react/location-state-shared.ts +166 -8
  87. package/src/browser/react/location-state.ts +39 -14
  88. package/src/browser/react/use-action.ts +6 -15
  89. package/src/browser/react/use-handle.ts +23 -69
  90. package/src/browser/react/use-link-status.ts +0 -4
  91. package/src/browser/react/use-navigation.ts +22 -5
  92. package/src/browser/react/use-params.ts +20 -10
  93. package/src/browser/react/use-reverse.ts +106 -0
  94. package/src/browser/react/use-router.ts +46 -11
  95. package/src/browser/react/use-search-params.ts +0 -5
  96. package/src/browser/react/use-segments.ts +11 -21
  97. package/src/browser/response-adapter.ts +52 -1
  98. package/src/browser/rsc-router.tsx +111 -24
  99. package/src/browser/scroll-restoration.ts +29 -19
  100. package/src/browser/segment-reconciler.ts +36 -14
  101. package/src/browser/segment-structure-assert.ts +2 -2
  102. package/src/browser/server-action-bridge.ts +176 -62
  103. package/src/browser/types.ts +60 -11
  104. package/src/browser/validate-redirect-origin.ts +43 -16
  105. package/src/build/collect-fallback-refs.ts +107 -0
  106. package/src/build/generate-manifest.ts +65 -40
  107. package/src/build/generate-route-types.ts +6 -0
  108. package/src/build/index.ts +8 -2
  109. package/src/build/prefix-tree-utils.ts +123 -0
  110. package/src/build/route-trie.ts +137 -32
  111. package/src/build/route-types/codegen.ts +4 -4
  112. package/src/build/route-types/include-resolution.ts +9 -2
  113. package/src/build/route-types/param-extraction.ts +6 -3
  114. package/src/build/route-types/per-module-writer.ts +7 -4
  115. package/src/build/route-types/router-processing.ts +333 -94
  116. package/src/build/route-types/scan-filter.ts +9 -2
  117. package/src/build/route-types/source-scan.ts +118 -0
  118. package/src/build/runtime-discovery.ts +9 -20
  119. package/src/cache/cache-error.ts +104 -0
  120. package/src/cache/cache-policy.ts +68 -28
  121. package/src/cache/cache-runtime.ts +134 -32
  122. package/src/cache/cache-scope.ts +100 -74
  123. package/src/cache/cache-tag.ts +98 -0
  124. package/src/cache/cf/cf-cache-store.ts +2256 -241
  125. package/src/cache/cf/index.ts +6 -16
  126. package/src/cache/document-cache.ts +61 -20
  127. package/src/cache/handle-snapshot.ts +63 -0
  128. package/src/cache/index.ts +22 -20
  129. package/src/cache/memory-segment-store.ts +136 -37
  130. package/src/cache/profile-registry.ts +6 -30
  131. package/src/cache/read-through-swr.ts +41 -11
  132. package/src/cache/segment-codec.ts +0 -16
  133. package/src/cache/tag-invalidation.ts +230 -0
  134. package/src/cache/types.ts +33 -100
  135. package/src/cache/vercel/index.ts +11 -0
  136. package/src/cache/vercel/vercel-cache-store.ts +799 -0
  137. package/src/client.rsc.tsx +6 -21
  138. package/src/client.tsx +108 -290
  139. package/src/component-utils.ts +19 -0
  140. package/src/context-var.ts +17 -5
  141. package/src/decode-loader-results.ts +36 -0
  142. package/src/defer.ts +196 -0
  143. package/src/deps/browser.ts +0 -1
  144. package/src/deps/ssr.ts +0 -1
  145. package/src/errors.ts +30 -4
  146. package/src/handle.ts +70 -22
  147. package/src/handles/MetaTags.tsx +0 -14
  148. package/src/handles/breadcrumbs.ts +16 -5
  149. package/src/handles/meta.ts +0 -39
  150. package/src/host/cookie-handler.ts +0 -36
  151. package/src/host/errors.ts +0 -24
  152. package/src/host/index.ts +8 -2
  153. package/src/host/pattern-matcher.ts +7 -50
  154. package/src/host/router.ts +107 -99
  155. package/src/host/testing.ts +40 -27
  156. package/src/host/types.ts +37 -4
  157. package/src/host/utils.ts +1 -1
  158. package/src/href-client.ts +137 -22
  159. package/src/index.rsc.ts +69 -10
  160. package/src/index.ts +112 -14
  161. package/src/internal-debug.ts +2 -4
  162. package/src/loader-store.ts +500 -0
  163. package/src/loader.rsc.ts +20 -13
  164. package/src/loader.ts +12 -11
  165. package/src/missing-id-error.ts +68 -0
  166. package/src/network-error-thrower.tsx +1 -6
  167. package/src/outlet-context.ts +1 -1
  168. package/src/outlet-provider.tsx +1 -5
  169. package/src/prerender/param-hash.ts +10 -11
  170. package/src/prerender/store.ts +37 -41
  171. package/src/prerender.ts +198 -82
  172. package/src/redirect-origin.ts +100 -0
  173. package/src/response-utils.ts +37 -0
  174. package/src/reverse.ts +65 -15
  175. package/src/root-error-boundary.tsx +1 -19
  176. package/src/route-content-wrapper.tsx +7 -72
  177. package/src/route-definition/dsl-helpers.ts +413 -275
  178. package/src/route-definition/helper-factories.ts +29 -139
  179. package/src/route-definition/helpers-types.ts +107 -32
  180. package/src/route-definition/index.ts +3 -0
  181. package/src/route-definition/redirect.ts +50 -8
  182. package/src/route-definition/resolve-handler-use.ts +161 -0
  183. package/src/route-definition/use-item-types.ts +32 -0
  184. package/src/route-map-builder.ts +0 -16
  185. package/src/route-types.ts +37 -41
  186. package/src/router/basename.ts +14 -0
  187. package/src/router/content-negotiation.ts +108 -9
  188. package/src/router/error-handling.ts +13 -17
  189. package/src/router/find-match.ts +44 -23
  190. package/src/router/handler-context.ts +46 -30
  191. package/src/router/intercept-resolution.ts +23 -23
  192. package/src/router/lazy-includes.ts +15 -52
  193. package/src/router/loader-resolution.ts +207 -30
  194. package/src/router/logging.ts +0 -6
  195. package/src/router/manifest.ts +40 -42
  196. package/src/router/match-api.ts +120 -204
  197. package/src/router/match-context.ts +0 -22
  198. package/src/router/match-handlers.ts +58 -58
  199. package/src/router/match-middleware/background-revalidation.ts +0 -7
  200. package/src/router/match-middleware/cache-lookup.ts +161 -262
  201. package/src/router/match-middleware/cache-store.ts +3 -33
  202. package/src/router/match-middleware/intercept-resolution.ts +0 -22
  203. package/src/router/match-middleware/segment-resolution.ts +45 -14
  204. package/src/router/match-pipelines.ts +1 -42
  205. package/src/router/match-result.ts +87 -39
  206. package/src/router/metrics.ts +0 -34
  207. package/src/router/middleware-types.ts +7 -140
  208. package/src/router/middleware.ts +169 -140
  209. package/src/router/navigation-snapshot.ts +131 -0
  210. package/src/router/params-util.ts +23 -0
  211. package/src/router/pattern-matching.ts +109 -63
  212. package/src/router/prerender-match.ts +190 -54
  213. package/src/router/preview-match.ts +32 -102
  214. package/src/router/request-classification.ts +276 -0
  215. package/src/router/revalidation.ts +63 -55
  216. package/src/router/route-snapshot.ts +244 -0
  217. package/src/router/router-context.ts +0 -27
  218. package/src/router/router-interfaces.ts +100 -35
  219. package/src/router/router-options.ts +91 -11
  220. package/src/router/router-registry.ts +2 -5
  221. package/src/router/segment-resolution/fresh.ts +119 -65
  222. package/src/router/segment-resolution/helpers.ts +34 -0
  223. package/src/router/segment-resolution/loader-cache.ts +40 -37
  224. package/src/router/segment-resolution/revalidation.ts +329 -305
  225. package/src/router/segment-resolution/static-store.ts +19 -5
  226. package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
  227. package/src/router/segment-resolution/view-transition-default.ts +36 -0
  228. package/src/router/segment-resolution.ts +4 -1
  229. package/src/router/segment-wrappers.ts +0 -3
  230. package/src/router/state-cookie-name.ts +33 -0
  231. package/src/router/substitute-pattern-params.ts +56 -0
  232. package/src/router/telemetry-otel.ts +0 -20
  233. package/src/router/telemetry.ts +96 -19
  234. package/src/router/timeout.ts +0 -20
  235. package/src/router/trie-matching.ts +91 -46
  236. package/src/router/types.ts +9 -63
  237. package/src/router/url-params.ts +44 -0
  238. package/src/router.ts +128 -42
  239. package/src/rsc/handler-context.ts +3 -2
  240. package/src/rsc/handler.ts +492 -409
  241. package/src/rsc/helpers.ts +162 -46
  242. package/src/rsc/index.ts +1 -1
  243. package/src/rsc/json-route-result.ts +38 -0
  244. package/src/rsc/loader-fetch.ts +18 -3
  245. package/src/rsc/manifest-init.ts +33 -42
  246. package/src/rsc/origin-guard.ts +39 -25
  247. package/src/rsc/progressive-enhancement.ts +28 -4
  248. package/src/rsc/redirect-guard.ts +99 -0
  249. package/src/rsc/response-error.ts +79 -12
  250. package/src/rsc/response-route-handler.ts +90 -63
  251. package/src/rsc/rsc-rendering.ts +53 -56
  252. package/src/rsc/runtime-warnings.ts +23 -10
  253. package/src/rsc/server-action.ts +74 -69
  254. package/src/rsc/ssr-setup.ts +18 -2
  255. package/src/rsc/types.ts +22 -9
  256. package/src/runtime-env.ts +18 -0
  257. package/src/search-params.ts +4 -20
  258. package/src/segment-content-promise.ts +67 -0
  259. package/src/segment-loader-promise.ts +134 -0
  260. package/src/segment-system.tsx +208 -201
  261. package/src/serialize.ts +243 -0
  262. package/src/server/context.ts +211 -52
  263. package/src/server/cookie-store.ts +80 -5
  264. package/src/server/handle-store.ts +26 -24
  265. package/src/server/loader-registry.ts +10 -28
  266. package/src/server/request-context.ts +289 -124
  267. package/src/ssr/index.tsx +22 -15
  268. package/src/static-handler.ts +27 -18
  269. package/src/testing/cache-status.ts +162 -0
  270. package/src/testing/collect-handle.ts +40 -0
  271. package/src/testing/dispatch.ts +618 -0
  272. package/src/testing/dom.entry.ts +22 -0
  273. package/src/testing/e2e/fixture.ts +188 -0
  274. package/src/testing/e2e/index.ts +128 -0
  275. package/src/testing/e2e/matchers.ts +35 -0
  276. package/src/testing/e2e/page-helpers.ts +272 -0
  277. package/src/testing/e2e/parity.ts +387 -0
  278. package/src/testing/e2e/server.ts +195 -0
  279. package/src/testing/flight-matchers.ts +97 -0
  280. package/src/testing/flight-normalize.ts +11 -0
  281. package/src/testing/flight-runtime.d.ts +57 -0
  282. package/src/testing/flight-tree.ts +682 -0
  283. package/src/testing/flight.entry.ts +52 -0
  284. package/src/testing/flight.ts +232 -0
  285. package/src/testing/generated-routes.ts +183 -0
  286. package/src/testing/index.ts +99 -0
  287. package/src/testing/internal/context.ts +348 -0
  288. package/src/testing/internal/flight-client-globals.ts +30 -0
  289. package/src/testing/internal/seed-vars.ts +54 -0
  290. package/src/testing/render-handler.ts +330 -0
  291. package/src/testing/render-route.tsx +566 -0
  292. package/src/testing/run-loader.ts +378 -0
  293. package/src/testing/run-middleware.ts +205 -0
  294. package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
  295. package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
  296. package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
  297. package/src/testing/vitest-stubs/version.ts +5 -0
  298. package/src/testing/vitest.ts +305 -0
  299. package/src/theme/ThemeProvider.tsx +0 -52
  300. package/src/theme/ThemeScript.tsx +0 -6
  301. package/src/theme/constants.ts +0 -12
  302. package/src/theme/index.ts +0 -7
  303. package/src/theme/theme-context.ts +1 -5
  304. package/src/theme/theme-script.ts +0 -14
  305. package/src/theme/use-theme.ts +0 -3
  306. package/src/types/boundaries.ts +0 -35
  307. package/src/types/cache-types.ts +17 -8
  308. package/src/types/error-types.ts +30 -90
  309. package/src/types/global-namespace.ts +54 -41
  310. package/src/types/handler-context.ts +124 -70
  311. package/src/types/index.ts +1 -10
  312. package/src/types/loader-types.ts +40 -11
  313. package/src/types/request-scope.ts +107 -0
  314. package/src/types/route-config.ts +6 -50
  315. package/src/types/route-entry.ts +12 -7
  316. package/src/types/segments.ts +36 -15
  317. package/src/urls/include-helper.ts +33 -70
  318. package/src/urls/index.ts +1 -11
  319. package/src/urls/path-helper-types.ts +58 -11
  320. package/src/urls/path-helper.ts +57 -111
  321. package/src/urls/pattern-types.ts +48 -19
  322. package/src/urls/response-types.ts +25 -22
  323. package/src/urls/type-extraction.ts +58 -139
  324. package/src/urls/urls-function.ts +1 -18
  325. package/src/use-loader.tsx +346 -89
  326. package/src/vite/debug.ts +185 -0
  327. package/src/vite/discovery/bundle-postprocess.ts +36 -38
  328. package/src/vite/discovery/discover-routers.ts +130 -85
  329. package/src/vite/discovery/discovery-errors.ts +194 -0
  330. package/src/vite/discovery/gate-state.ts +171 -0
  331. package/src/vite/discovery/prerender-collection.ts +192 -99
  332. package/src/vite/discovery/route-types-writer.ts +40 -84
  333. package/src/vite/discovery/self-gen-tracking.ts +27 -1
  334. package/src/vite/discovery/state.ts +51 -4
  335. package/src/vite/discovery/virtual-module-codegen.ts +14 -34
  336. package/src/vite/index.ts +8 -0
  337. package/src/vite/plugin-types.ts +236 -6
  338. package/src/vite/plugins/cjs-to-esm.ts +8 -18
  339. package/src/vite/plugins/client-ref-dedup.ts +16 -11
  340. package/src/vite/plugins/client-ref-hashing.ts +28 -15
  341. package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
  342. package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  343. package/src/vite/plugins/cloudflare-protocol-stub.ts +194 -0
  344. package/src/vite/plugins/expose-action-id.ts +48 -95
  345. package/src/vite/plugins/expose-id-utils.ts +11 -50
  346. package/src/vite/plugins/expose-ids/export-analysis.ts +76 -34
  347. package/src/vite/plugins/expose-ids/handler-transform.ts +10 -48
  348. package/src/vite/plugins/expose-ids/loader-transform.ts +3 -20
  349. package/src/vite/plugins/expose-ids/router-transform.ts +20 -16
  350. package/src/vite/plugins/expose-internal-ids.ts +554 -317
  351. package/src/vite/plugins/performance-tracks.ts +65 -207
  352. package/src/vite/plugins/refresh-cmd.ts +89 -27
  353. package/src/vite/plugins/use-cache-transform.ts +73 -83
  354. package/src/vite/plugins/vercel-output.ts +258 -0
  355. package/src/vite/plugins/version-injector.ts +21 -25
  356. package/src/vite/plugins/version-plugin.ts +32 -23
  357. package/src/vite/plugins/virtual-entries.ts +46 -17
  358. package/src/vite/rango.ts +207 -125
  359. package/src/vite/router-discovery.ts +931 -133
  360. package/src/vite/utils/ast-handler-extract.ts +15 -31
  361. package/src/vite/utils/banner.ts +1 -1
  362. package/src/vite/utils/bundle-analysis.ts +10 -15
  363. package/src/vite/utils/client-chunks.ts +184 -0
  364. package/src/vite/utils/forward-user-plugins.ts +171 -0
  365. package/src/vite/utils/manifest-utils.ts +4 -59
  366. package/src/vite/utils/package-resolution.ts +20 -52
  367. package/src/vite/utils/prerender-utils.ts +27 -29
  368. package/src/vite/utils/shared-utils.ts +97 -44
  369. package/src/browser/action-response-classifier.ts +0 -99
  370. package/src/browser/debug-channel.ts +0 -93
  371. package/src/browser/react/use-client-cache.ts +0 -58
  372. package/src/browser/shallow.ts +0 -40
  373. package/src/handles/index.ts +0 -7
  374. package/src/router/middleware-cookies.ts +0 -55
@@ -10,7 +10,7 @@ import type {
10
10
  ShouldRevalidateFn,
11
11
  TransitionConfig,
12
12
  } from "../types";
13
- import { invariant } from "../errors";
13
+ import { invariant, DslContextError } from "../errors";
14
14
  import type { DefaultRouteName } from "../types/global-namespace.js";
15
15
 
16
16
  // ============================================================================
@@ -40,7 +40,7 @@ export interface MetricsStore {
40
40
  metrics: PerformanceMetric[];
41
41
  }
42
42
  // ============================================================================
43
- // RSC Router Context
43
+ // Rango Context
44
44
  // ============================================================================
45
45
 
46
46
  /**
@@ -71,6 +71,10 @@ export type EntryPropCommon = {
71
71
  };
72
72
 
73
73
  /**
74
+ * Attachments resolved by walking the parent chain, not owned by the entry:
75
+ * middleware composes downward; revalidate and the error/notFound boundaries are
76
+ * resolved by nearest-ancestor lookup. Inherited, not a single execution chain.
77
+ *
74
78
  * @internal This type is an implementation detail and may change without notice.
75
79
  */
76
80
  export type EntryPropDatas = {
@@ -80,6 +84,16 @@ export type EntryPropDatas = {
80
84
  notFoundBoundary: (ReactNode | NotFoundBoundaryHandler)[];
81
85
  };
82
86
 
87
+ /**
88
+ * Render-time presentation fields shared by every entry variant.
89
+ *
90
+ * @internal This type is an implementation detail and may change without notice.
91
+ */
92
+ export type EntryPropRender = {
93
+ loading?: ReactNode | false;
94
+ transition?: TransitionConfig;
95
+ };
96
+
83
97
  /**
84
98
  * Loader entry stored in EntryData
85
99
  * Contains the loader definition and its revalidation rules
@@ -158,11 +172,9 @@ export type InterceptEntry = {
158
172
  };
159
173
 
160
174
  export interface ParallelEntryData
161
- extends EntryPropCommon, EntryPropDatas, EntryPropSegments {
175
+ extends EntryPropCommon, EntryPropDatas, EntryPropSegments, EntryPropRender {
162
176
  type: "parallel";
163
177
  handler: Record<`@${string}`, Handler<any, any, any> | ReactNode>;
164
- loading?: ReactNode | false;
165
- transition?: TransitionConfig;
166
178
  /** Set when any parallel slot is a Static definition */
167
179
  isStaticPrerender?: true;
168
180
  /** Per-slot static handler $$ids for build-time store lookup */
@@ -171,6 +183,13 @@ export interface ParallelEntryData
171
183
 
172
184
  export type ParallelEntries = Partial<Record<`@${string}`, ParallelEntryData>>;
173
185
 
186
+ /**
187
+ * This entry's own structural children plus its owned loaders. `loader` lives
188
+ * here (not in EntryPropDatas) because loaders are owned by the entry, not
189
+ * inherited from ancestors.
190
+ *
191
+ * @internal This type is an implementation detail and may change without notice.
192
+ */
174
193
  export type EntryPropSegments = {
175
194
  loader: LoaderEntry[];
176
195
  layout: EntryData[];
@@ -182,8 +201,6 @@ export type EntryData =
182
201
  | ({
183
202
  type: "route";
184
203
  handler: Handler<any, any, any>;
185
- loading?: ReactNode | false;
186
- transition?: TransitionConfig;
187
204
  /** URL pattern for this route (used by path() in urls()) */
188
205
  pattern?: string;
189
206
  /** Set when handler is a Prerender definition */
@@ -191,8 +208,12 @@ export type EntryData =
191
208
  /** Original PrerenderHandlerDefinition (for build-time getParams access) */
192
209
  prerenderDef?: {
193
210
  getParams?: (ctx: any) => Promise<any[]> | any[];
194
- options?: { passthrough?: boolean };
211
+ options?: { concurrency?: number };
195
212
  };
213
+ /** Set when route is wrapped with Passthrough() — has a separate live handler */
214
+ isPassthrough?: true;
215
+ /** Live handler for runtime fallback (only set on Passthrough routes) */
216
+ liveHandler?: Handler<any, any, any>;
196
217
  /** Set when handler is a Static definition (build-time only) */
197
218
  isStaticPrerender?: true;
198
219
  /** Static handler $$id for build-time store lookup */
@@ -201,29 +222,28 @@ export type EntryData =
201
222
  responseType?: string;
202
223
  } & EntryPropCommon &
203
224
  EntryPropDatas &
204
- EntryPropSegments)
225
+ EntryPropSegments &
226
+ EntryPropRender)
205
227
  | ({
206
228
  type: "layout";
207
229
  handler: ReactNode | Handler<any, any, any>;
208
- loading?: ReactNode | false;
209
- transition?: TransitionConfig;
210
230
  /** Set when handler is a Static definition (build-time only) */
211
231
  isStaticPrerender?: true;
212
232
  /** Static handler $$id for build-time store lookup */
213
233
  staticHandlerId?: string;
214
234
  } & EntryPropCommon &
215
235
  EntryPropDatas &
216
- EntryPropSegments)
236
+ EntryPropSegments &
237
+ EntryPropRender)
217
238
  | ParallelEntryData
218
239
  | ({
219
240
  type: "cache";
220
241
  /** Cache entries create cache boundaries and render like layouts (with Outlet) */
221
242
  handler: ReactNode | Handler<any, any, any>;
222
- loading?: ReactNode | false;
223
- transition?: TransitionConfig;
224
243
  } & EntryPropCommon &
225
244
  EntryPropDatas &
226
- EntryPropSegments);
245
+ EntryPropSegments &
246
+ EntryPropRender);
227
247
 
228
248
  /**
229
249
  * Tracked include info for build-time manifest generation
@@ -276,6 +296,22 @@ interface HelperContext {
276
296
  /** True when resolving handlers inside a cache() DSL boundary.
277
297
  * Read by ctx.get() to guard non-cacheable variable reads. */
278
298
  insideCacheScope?: boolean;
299
+ /**
300
+ * Include scope string applied to direct-descendant shortCodes.
301
+ *
302
+ * Each `include(...)` call allocates a sibling-positional token like `I0`,
303
+ * `I1` from its parent's include counter and stores the composed scope
304
+ * (`${parentScope}I${idx}`) in its lazyContext. When the include's handler
305
+ * evaluates lazily, the store's `includeScope` is set from that context so
306
+ * every direct-descendant shortCode is generated as
307
+ * `${parent.shortCode}${includeScope}${prefix}${index}` — preventing
308
+ * collisions with siblings declared outside the include.
309
+ *
310
+ * The scope is NOT propagated through `store.run(...)`, so layouts /
311
+ * parallels / caches inside the include absorb the scope into their own
312
+ * shortCodes and their children start fresh.
313
+ */
314
+ includeScope?: string;
279
315
  }
280
316
  // Use a global symbol key so the AsyncLocalStorage instance survives HMR
281
317
  // module re-evaluation. Without this, Vite's RSC module runner may create
@@ -283,10 +319,28 @@ interface HelperContext {
283
319
  // hold references to the old instance — causing getStore() to return
284
320
  // undefined even inside a run() callback.
285
321
  const RSC_CONTEXT_KEY = Symbol.for("rangojs-router:rsc-context");
286
- export const RSCRouterContext: AsyncLocalStorage<HelperContext> = ((
322
+ export const RangoContext: AsyncLocalStorage<HelperContext> = ((
287
323
  globalThis as any
288
324
  )[RSC_CONTEXT_KEY] ??= new AsyncLocalStorage<HelperContext>());
289
325
 
326
+ /** shortCode prefix letter per entry type (e.g. "L0", "R2", "M1C0"). */
327
+ const SHORT_CODE_PREFIX: Record<
328
+ "layout" | "parallel" | "route" | "loader" | "cache",
329
+ string
330
+ > = {
331
+ layout: "L",
332
+ parallel: "P",
333
+ route: "R",
334
+ loader: "D",
335
+ cache: "C",
336
+ };
337
+
338
+ /** Post-increment a named per-store counter, returning the prior value. */
339
+ function bumpCounter(store: HelperContext, key: string): number {
340
+ store.counters[key] ??= 0;
341
+ return store.counters[key]++;
342
+ }
343
+
290
344
  export const getContext = (): {
291
345
  context: AsyncLocalStorage<HelperContext>;
292
346
  getStore: () => HelperContext;
@@ -310,12 +364,12 @@ export const getContext = (): {
310
364
  callback: (...args: any[]) => T,
311
365
  ) => T;
312
366
  } => {
313
- const context = RSCRouterContext;
367
+ const context = RangoContext;
314
368
 
315
369
  return {
316
370
  context,
317
371
  getOrCreateStore: (forRoute?: string): HelperContext => {
318
- let store = RSCRouterContext.getStore();
372
+ let store = RangoContext.getStore();
319
373
  if (!store) {
320
374
  store = {
321
375
  manifest: new Map<string, EntryData>(),
@@ -335,7 +389,7 @@ export const getContext = (): {
335
389
  const store = context.getStore();
336
390
  if (!store) {
337
391
  throw new Error(
338
- "RSC Router context store is not available. Make sure to run within RSC Router context.",
392
+ "Rango context store is not available. Make sure to run within Rango context.",
339
393
  );
340
394
  }
341
395
  return store;
@@ -352,48 +406,36 @@ export const getContext = (): {
352
406
  type: (string & {}) | "layout" | "parallel" | "middleware" | "revalidate",
353
407
  ) => {
354
408
  const store = context.getStore();
355
- invariant(store, "No context RSCRouterContext available");
356
- store.counters[type] ??= 0;
357
- const index = store.counters[type];
358
- store.counters[type] = index + 1;
359
- return `$${type}.${index}`;
409
+ invariant(store, "No context RangoContext available");
410
+ return `$${type}.${bumpCounter(store, type)}`;
360
411
  },
361
412
  getShortCode: (
362
413
  type: "layout" | "parallel" | "route" | "loader" | "cache",
363
414
  ) => {
364
415
  const store = context.getStore();
365
- invariant(store, "No context RSCRouterContext available");
416
+ invariant(store, "No context RangoContext available");
366
417
 
367
418
  const parent = store.parent;
368
- const prefix =
369
- type === "layout"
370
- ? "L"
371
- : type === "parallel"
372
- ? "P"
373
- : type === "loader"
374
- ? "D"
375
- : type === "cache"
376
- ? "C"
377
- : "R";
419
+ const prefix = SHORT_CODE_PREFIX[type];
378
420
  const mountPrefix =
379
421
  store.mountIndex !== undefined ? `M${store.mountIndex}` : "";
380
422
 
423
+ const includeScope = store.includeScope ?? "";
424
+
381
425
  if (!parent) {
382
426
  // Root entry: prefix with mount index and use mount-scoped counter
383
427
  const counterKey = mountPrefix
384
428
  ? `${mountPrefix}_root_${type}`
385
429
  : `root_${type}`;
386
- store.counters[counterKey] ??= 0;
387
- const index = store.counters[counterKey];
388
- store.counters[counterKey] = index + 1;
389
- return `${mountPrefix}${prefix}${index}`;
430
+ return `${mountPrefix}${prefix}${bumpCounter(store, counterKey)}`;
390
431
  } else {
391
- // Child entry: use parent-scoped counter (parent already has M prefix)
392
- const counterKey = `${parent.shortCode}_${type}`;
393
- store.counters[counterKey] ??= 0;
394
- const index = store.counters[counterKey];
395
- store.counters[counterKey] = index + 1;
396
- return `${parent.shortCode}${prefix}${index}`;
432
+ // Child entry: use parent-scoped counter with includeScope appended.
433
+ // When we're evaluating a lazy include's direct children, includeScope
434
+ // is a per-include token like "I0" / "I1I0" that partitions the
435
+ // parent's counter namespace so routes inside one include cannot
436
+ // collide with siblings declared outside it.
437
+ const counterKey = `${parent.shortCode}${includeScope}_${type}`;
438
+ return `${parent.shortCode}${includeScope}${prefix}${bumpCounter(store, counterKey)}`;
397
439
  }
398
440
  },
399
441
  runWithStore: <T>(
@@ -420,6 +462,7 @@ export const getContext = (): {
420
462
  rootScoped: store.rootScoped,
421
463
  trackedIncludes: store.trackedIncludes,
422
464
  cacheProfiles: store.cacheProfiles,
465
+ includeScope: store.includeScope,
423
466
  },
424
467
  callback,
425
468
  );
@@ -466,6 +509,31 @@ export const getContext = (): {
466
509
  };
467
510
  };
468
511
 
512
+ /**
513
+ * Acquire the active DSL build context, throwing `message` if a helper was
514
+ * called outside a urls()/map() builder. Returns the store API and the live
515
+ * HelperContext so callers avoid a second getContext() lookup.
516
+ */
517
+ export function requireDslContext(message: string): {
518
+ store: ReturnType<typeof getContext>;
519
+ ctx: HelperContext;
520
+ } {
521
+ const store = getContext();
522
+ const ctx = store.context.getStore();
523
+ if (!ctx) {
524
+ // The only reason the store is absent here is that a route-definition helper
525
+ // ran with no active RangoContext — i.e. outside a urls()/map() builder.
526
+ // Record that as the cause so the throw is self-explanatory, not a bare
527
+ // "must be called inside urls()" with no indication of the mechanism.
528
+ throw new DslContextError(message, {
529
+ cause:
530
+ "RangoContext store is undefined: a route-definition helper was called " +
531
+ "outside an active urls()/map() builder.",
532
+ });
533
+ }
534
+ return { store, ctx };
535
+ }
536
+
469
537
  /**
470
538
  * Run a callback with specific URL and name prefixes
471
539
  * Used by include() to apply prefixes to nested patterns
@@ -475,7 +543,7 @@ export function runWithPrefixes<T>(
475
543
  namePrefix: string | undefined,
476
544
  callback: () => T,
477
545
  ): T {
478
- const store = RSCRouterContext.getStore();
546
+ const store = RangoContext.getStore();
479
547
  if (!store) {
480
548
  throw new Error("runWithPrefixes must be called within router context");
481
549
  }
@@ -520,7 +588,7 @@ export function runWithPrefixes<T>(
520
588
  ? (store.rootScoped ?? false)
521
589
  : store.rootScoped;
522
590
 
523
- return RSCRouterContext.run(
591
+ return RangoContext.run(
524
592
  {
525
593
  ...store,
526
594
  urlPrefix: combinedUrlPrefix,
@@ -535,7 +603,7 @@ export function runWithPrefixes<T>(
535
603
  * Get current URL prefix from context
536
604
  */
537
605
  export function getUrlPrefix(): string {
538
- const store = RSCRouterContext.getStore();
606
+ const store = RangoContext.getStore();
539
607
  return store?.urlPrefix || "";
540
608
  }
541
609
 
@@ -543,7 +611,7 @@ export function getUrlPrefix(): string {
543
611
  * Get current name prefix from context
544
612
  */
545
613
  export function getNamePrefix(): string | undefined {
546
- const store = RSCRouterContext.getStore();
614
+ const store = RangoContext.getStore();
547
615
  return store?.namePrefix;
548
616
  }
549
617
 
@@ -552,7 +620,7 @@ export function getNamePrefix(): string | undefined {
552
620
  * Returns true at root or inside { name: "" } includes, false inside named includes.
553
621
  */
554
622
  export function getRootScoped(): boolean {
555
- const store = RSCRouterContext.getStore();
623
+ const store = RangoContext.getStore();
556
624
  return store?.rootScoped ?? true;
557
625
  }
558
626
 
@@ -649,7 +717,7 @@ export function getParallelSlotCount(
649
717
  * ```
650
718
  */
651
719
  export function track(label: string, depth?: number): () => void {
652
- const store = RSCRouterContext.getStore();
720
+ const store = RangoContext.getStore();
653
721
 
654
722
  // No-op if context unavailable or metrics not enabled
655
723
  if (!store?.metrics?.enabled) {
@@ -670,11 +738,102 @@ export function track(label: string, depth?: number): () => void {
670
738
  };
671
739
  }
672
740
 
741
+ /**
742
+ * Separate ALS for tracking loader execution scope.
743
+ * Uses a dedicated ALS (not RangoContext) to avoid issues with
744
+ * nested RangoContext.run() calls in Vite's module runner.
745
+ */
746
+ const LOADER_SCOPE_KEY = Symbol.for("rangojs-router:loader-scope");
747
+ const loaderScopeALS: AsyncLocalStorage<{ active: true }> = ((
748
+ globalThis as any
749
+ )[LOADER_SCOPE_KEY] ??= new AsyncLocalStorage<{ active: true }>());
750
+
751
+ // Purity-only scope: marks that a loader FUNCTION BODY is executing, regardless
752
+ // of how the loader was invoked (DSL via runInsideLoaderScope, or handler-
753
+ // invoked via ctx.use). Consulted ONLY by isInsideCacheScope() to exempt
754
+ // request-scoped reads. It deliberately does NOT affect isInsideLoaderScope(),
755
+ // so rendered()/barrier/deadlock gating (which must distinguish DSL from
756
+ // handler-invoked loaders) is unchanged.
757
+ const LOADER_BODY_SCOPE_KEY = Symbol.for("rangojs-router:loader-body-scope");
758
+ const loaderBodyScopeALS: AsyncLocalStorage<{ active: true }> = ((
759
+ globalThis as any
760
+ )[LOADER_BODY_SCOPE_KEY] ??= new AsyncLocalStorage<{ active: true }>());
761
+
673
762
  /**
674
763
  * Check if the current execution is inside a cache() DSL boundary.
675
764
  * Returns false inside loader execution — loaders are always fresh
676
765
  * (never cached), so non-cacheable reads are safe.
677
766
  */
678
767
  export function isInsideCacheScope(): boolean {
679
- return RSCRouterContext.getStore()?.insideCacheScope === true;
768
+ if (RangoContext.getStore()?.insideCacheScope !== true) return false;
769
+ // Loaders are always fresh — even inside a cache() boundary, the loader
770
+ // function re-executes on every request. Skip the guard when running
771
+ // inside a loader.
772
+ if (loaderScopeALS.getStore()?.active) return false;
773
+ // Also exempt handler-invoked loaders: their bodies run in a loader-body
774
+ // scope (not the DSL loader scope above), so request-scoped reads inside any
775
+ // loader — however invoked — are safe (loaders always re-run fresh).
776
+ if (loaderBodyScopeALS.getStore()?.active) return false;
777
+ return true;
778
+ }
779
+
780
+ /**
781
+ * Check if the current execution is inside a DSL loader scope
782
+ * (wrapped by runInsideLoaderScope). Used by rendered() barrier
783
+ * to distinguish DSL loaders from handler-invoked loaders.
784
+ */
785
+ export function isInsideLoaderScope(): boolean {
786
+ return loaderScopeALS.getStore()?.active === true;
787
+ }
788
+
789
+ /**
790
+ * Run `fn` inside a loader scope. While active, cache-scope guards
791
+ * are bypassed because loaders are always fresh (never cached) and
792
+ * their side effects (setCookie, header, etc.) are safe.
793
+ */
794
+ export function runInsideLoaderScope<T>(fn: () => T): T {
795
+ return loaderScopeALS.run({ active: true }, fn);
796
+ }
797
+
798
+ /**
799
+ * Run `fn` inside a loader BODY scope. Marks loader-function execution for the
800
+ * cache-purity guard only (isInsideCacheScope), WITHOUT affecting
801
+ * isInsideLoaderScope()/rendered() gating. Applied to every loader body (DSL
802
+ * and handler-invoked via ctx.use) so request-scoped reads inside a loader
803
+ * never trip the cache-scope guards — loaders always run fresh.
804
+ */
805
+ export function runInsideLoaderBodyScope<T>(fn: () => T): T {
806
+ return loaderBodyScopeALS.run({ active: true }, fn);
807
+ }
808
+
809
+ // Scope for handle PUSH CALLBACKS (push(() => ...), including async ones).
810
+ // A push callback's value is stored as-is; if it is a promise it is NOT tracked
811
+ // by handleStore.settled and does not block segment resolution, so a
812
+ // ctx.use(loader) made from inside such a callback can never form a rendered()
813
+ // deadlock. This is an ALS (not a plain boolean) so the exemption survives the
814
+ // callback's own awaits — an async push callback that resumes after `await`
815
+ // still reads as "inside a push callback" and stays out of the deadlock guard.
816
+ const PUSH_CALLBACK_SCOPE_KEY = Symbol.for(
817
+ "rangojs-router:push-callback-scope",
818
+ );
819
+ const pushCallbackScopeALS: AsyncLocalStorage<{ active: true }> = ((
820
+ globalThis as any
821
+ )[PUSH_CALLBACK_SCOPE_KEY] ??= new AsyncLocalStorage<{ active: true }>());
822
+
823
+ /**
824
+ * Check if the current execution is inside a handle push callback (sync or an
825
+ * async callback's continuation). Used by the handler-to-loader deadlock guard
826
+ * to exempt push-callback continuations.
827
+ */
828
+ export function isInsidePushCallbackScope(): boolean {
829
+ return pushCallbackScopeALS.getStore()?.active === true;
830
+ }
831
+
832
+ /**
833
+ * Run `fn` inside a push-callback scope. Wraps the invocation of a handle push
834
+ * callback so that any ctx.use(loader) it makes — including after one of its own
835
+ * awaits — is exempt from the deadlock guard.
836
+ */
837
+ export function runInsidePushCallbackScope<T>(fn: () => T): T {
838
+ return pushCallbackScopeALS.run({ active: true }, fn);
680
839
  }
@@ -8,7 +8,8 @@
8
8
  */
9
9
 
10
10
  import type { CookieOptions } from "../router/middleware-types.js";
11
- import { getRequestContext } from "./request-context.js";
11
+ import { getRequestContext, _getRequestContext } from "./request-context.js";
12
+ import { isInsideCacheScope } from "./context.js";
12
13
  import { INSIDE_CACHE_EXEC } from "../cache/taint.js";
13
14
 
14
15
  /**
@@ -84,10 +85,23 @@ export interface ReadonlyHeaders {
84
85
  type HeadersIterator<T> = IterableIterator<T>;
85
86
 
86
87
  /**
87
- * Throw if called inside a "use cache" function.
88
- * Reading request-scoped data (cookies, headers) inside a cached function
89
- * produces results that vary per request but the cache key does not include
90
- * those values, leading to one user's data being served to another.
88
+ * Throw if called inside a cache boundary — either a "use cache" function
89
+ * (`INSIDE_CACHE_EXEC` stamped on ctx by the cache runtime) or a `cache()`
90
+ * DSL boundary (`isInsideCacheScope()` the render-store flag set while
91
+ * resolving a `type: "cache"` route entry).
92
+ *
93
+ * Reading request-scoped data (cookies, headers) inside a cached scope
94
+ * produces per-request values that are NOT reflected in the cache key, so
95
+ * they would be frozen into the shared cache entry and served to the wrong
96
+ * users. This is the same hazard for both scopes: a `cache()` boundary caches
97
+ * everything except loaders (it is the document-level "PPR shell"), so a read
98
+ * here is baked into the shell exactly like a `"use cache"` return value is
99
+ * baked into its cache entry.
100
+ *
101
+ * `isInsideCacheScope()` returns false inside loaders (loaders always run
102
+ * fresh on every request, even on a cache hit), so reading cookies()/headers()
103
+ * from a loader is allowed — loaders are the dynamic "holes" of a cached
104
+ * document.
91
105
  */
92
106
  function assertNotInsideCacheContext(ctx: unknown, fnName: string): void {
93
107
  if (
@@ -106,6 +120,16 @@ function assertNotInsideCacheContext(ctx: unknown, fnName: string): void {
106
120
  ` const data = await getCachedData(locale); // locale is now in the cache key`,
107
121
  );
108
122
  }
123
+ if (isInsideCacheScope()) {
124
+ throw new Error(
125
+ `${fnName}() cannot be called inside a cache() boundary. ` +
126
+ `A cache() scope caches everything except loaders, so request-scoped ` +
127
+ `data (cookies, headers) read here would be frozen into the shared ` +
128
+ `cached shell and served to other users. Read it inside a loader ` +
129
+ `instead — loaders always run fresh on every request, even on a cache hit:\n\n` +
130
+ ` loader("user", () => getUser(cookies().get("session")?.value));`,
131
+ );
132
+ }
109
133
  }
110
134
 
111
135
  const HEADERS_MUTATION_METHODS = new Set(["set", "append", "delete"]);
@@ -144,6 +168,57 @@ export function headers(): ReadonlyHeaders {
144
168
  }) as unknown as ReadonlyHeaders;
145
169
  }
146
170
 
171
+ /**
172
+ * Force the calling client's caches to miss from now on, from the server seat:
173
+ * write a rotated `Set-Cookie` for the rango state. The responding client
174
+ * applies it on receipt, and its history cache is marked stale by the
175
+ * jar-divergence observer at its next read. Per-client and lazy — it rotates
176
+ * only the client that receives this response, not every client.
177
+ *
178
+ * Idempotent within a request (one `Set-Cookie`). Inert (a dev warning) when
179
+ * called outside a request context. Like `cookies()`, it throws inside a
180
+ * `"use cache"` / `cache()` boundary, but is allowed from a loader (loaders are
181
+ * the dynamic holes of a cached document).
182
+ */
183
+ export function invalidateClientCache(): void {
184
+ const ctx = _getRequestContext();
185
+ if (!ctx) {
186
+ if (process.env.NODE_ENV !== "production") {
187
+ console.warn(
188
+ "[rango] invalidateClientCache() was called outside a request context; ignored.",
189
+ );
190
+ }
191
+ return;
192
+ }
193
+ assertNotInsideCacheContext(ctx, "invalidateClientCache");
194
+ ctx._rotateStateCookie();
195
+ }
196
+
197
+ /**
198
+ * Suppress a server action's automatic client-cache invalidation: tell the
199
+ * action bridge this action changed nothing a route renders, so it should leave
200
+ * the client's state and caches alone (no rotation, no prefetch wipe, no
201
+ * broadcast, no revalidation refetch). Per-response, not per-action-definition —
202
+ * only the execution knows whether anything changed.
203
+ *
204
+ * Sets an internal response header the bridge reads. Idempotent within a
205
+ * request. Inert (a dev warning) outside a request context — there is no
206
+ * automatic invalidation to suppress.
207
+ */
208
+ export function keepClientCache(): void {
209
+ const ctx = _getRequestContext();
210
+ if (!ctx) {
211
+ if (process.env.NODE_ENV !== "production") {
212
+ console.warn(
213
+ "[rango] keepClientCache() was called outside a request context; ignored.",
214
+ );
215
+ }
216
+ return;
217
+ }
218
+ assertNotInsideCacheContext(ctx, "keepClientCache");
219
+ ctx._setKeepCacheDirective();
220
+ }
221
+
147
222
  /**
148
223
  * Create a CookieStore backed by a RequestContext.
149
224
  * @internal Shared between cookies() shorthand and context methods.
@@ -13,6 +13,25 @@
13
13
  */
14
14
  export type HandleData = Record<string, Record<string, unknown[]>>;
15
15
 
16
+ /**
17
+ * Build a HandleData snapshot from a HandleStore using segment ordering.
18
+ * Reads data directly from the store for each segment in order.
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
+
16
35
  function createLateHandlePushError(
17
36
  handleName: string,
18
37
  segmentId: string,
@@ -26,10 +45,6 @@ function createLateHandlePushError(
26
45
  return error;
27
46
  }
28
47
 
29
- /**
30
- * Deep clone handle data to create a snapshot.
31
- * @internal
32
- */
33
48
  function cloneHandleData(data: HandleData): HandleData {
34
49
  const clone: HandleData = {};
35
50
  for (const handleName in data) {
@@ -186,11 +201,9 @@ export function createHandleStore(): HandleStore {
186
201
  return {
187
202
  track<T>(promise: Promise<T>): Promise<T> {
188
203
  inflightCount++;
189
- // Use .then(onSettle, onSettle) instead of .finally() to avoid
190
- // creating an unhandled rejection branch when the tracked promise
191
- // rejects (e.g. error route handlers). .finally() re-throws the
192
- // rejection on a new branch that nobody catches, which can crash
193
- // the server process.
204
+ // Use .then() instead of .finally() to avoid creating an unhandled rejection
205
+ // branch when the promise rejects. .finally() re-throws on a new branch that
206
+ // can crash the process if not caught.
194
207
  const onSettle = () => {
195
208
  inflightCount--;
196
209
  notifyDrain();
@@ -236,42 +249,32 @@ export function createHandleStore(): HandleStore {
236
249
  },
237
250
 
238
251
  async *stream(): AsyncGenerator<HandleData, void, unknown> {
239
- // Auto-seal: stream() is called after all track() registrations.
240
252
  sealInternal();
241
253
 
242
- // Set up completion handler
243
254
  this.settled.then(() => {
244
255
  completed = true;
245
256
  signalEmission();
246
257
  });
247
258
 
248
- // Initial small delay to batch rapid synchronous pushes
249
- // This allows multiple handles pushing in quick succession to be batched
259
+ // Batch rapid synchronous pushes with initial delay
250
260
  await new Promise((resolve) => setTimeout(resolve, 0));
251
261
 
252
- // If we already have data, yield the accumulated state
253
262
  if (Object.keys(data).length > 0) {
254
- // Clear pending emissions since we're yielding current state
255
263
  pendingEmissions = [];
256
264
  const snapshot = cloneHandleData(data);
257
265
  yield snapshot;
258
266
  }
259
267
 
260
- // Continue streaming on each push
261
268
  while (!completed) {
262
269
  await waitForEmission();
263
270
 
264
- // Yield all pending emissions (yield latest only)
265
271
  if (pendingEmissions.length > 0) {
266
- // Skip intermediate states, yield the latest
267
272
  const latest = pendingEmissions[pendingEmissions.length - 1];
268
273
  pendingEmissions = [];
269
274
  yield latest;
270
275
  }
271
276
  }
272
277
 
273
- // Final yield only if there are pending emissions that weren't yielded
274
- // (handles that pushed after our last yield but before completion)
275
278
  if (pendingEmissions.length > 0) {
276
279
  yield cloneHandleData(data);
277
280
  }
@@ -295,12 +298,11 @@ export function createHandleStore(): HandleStore {
295
298
  if (!data[handleName]) {
296
299
  data[handleName] = {};
297
300
  }
298
- // Replace with replayed data (not append) to avoid handle bleeding between routes.
299
- // When a cached segment is restored, its handles should replace any existing data
300
- // 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.
301
304
  data[handleName][segmentId] = [...segmentHandles[handleName]];
302
305
  }
303
- // Trigger emission for streaming
304
306
  pendingEmissions.push(cloneHandleData(data));
305
307
  signalEmission();
306
308
  },