@timber-js/app 0.2.0-alpha.185 → 0.2.0-alpha.187

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 (570) hide show
  1. package/dist/_chunks/{actions-O_LsyCE4.js → actions-35jnMdeJ.js} +4 -3
  2. package/dist/_chunks/{actions-O_LsyCE4.js.map → actions-35jnMdeJ.js.map} +1 -1
  3. package/dist/_chunks/als-registry-C6kcfprT.js +41 -0
  4. package/dist/_chunks/als-registry-C6kcfprT.js.map +1 -0
  5. package/dist/_chunks/als-slots-BEEIPKYm.js +26 -0
  6. package/dist/_chunks/als-slots-BEEIPKYm.js.map +1 -0
  7. package/dist/_chunks/base-path-DaQrzbez.js +80 -0
  8. package/dist/_chunks/base-path-DaQrzbez.js.map +1 -0
  9. package/dist/_chunks/build-manifest-DWppEdLB.js +86 -0
  10. package/dist/_chunks/build-manifest-DWppEdLB.js.map +1 -0
  11. package/dist/_chunks/build-output-helper-C3DrfzZR.js +369 -0
  12. package/dist/_chunks/build-output-helper-C3DrfzZR.js.map +1 -0
  13. package/dist/_chunks/{cache-api-B-lhk9p4.js → cache-api-DjNrIWRR.js} +8 -14
  14. package/dist/_chunks/cache-api-DjNrIWRR.js.map +1 -0
  15. package/dist/_chunks/cli-check-CpmN7Nh-.js +256 -0
  16. package/dist/_chunks/cli-check-CpmN7Nh-.js.map +1 -0
  17. package/dist/_chunks/cli-schema-sync-CGMp_Psg.js +298 -0
  18. package/dist/_chunks/cli-schema-sync-CGMp_Psg.js.map +1 -0
  19. package/dist/_chunks/{cloudflare-CnT5Lr7U.js → cloudflare-CGP6BZKO.js} +92 -14
  20. package/dist/_chunks/{cloudflare-CnT5Lr7U.js.map → cloudflare-CGP6BZKO.js.map} +1 -1
  21. package/dist/_chunks/convention-lint-kXsgc_-7.js +784 -0
  22. package/dist/_chunks/convention-lint-kXsgc_-7.js.map +1 -0
  23. package/dist/_chunks/error-boundary-D-ODYX41.js +353 -0
  24. package/dist/_chunks/error-boundary-D-ODYX41.js.map +1 -0
  25. package/dist/_chunks/{fast-hash-D6hIVt1Y.js → fast-hash-C-tVYrLb.js} +6 -1
  26. package/dist/_chunks/fast-hash-C-tVYrLb.js.map +1 -0
  27. package/dist/_chunks/file-cache-DmX7OqZP.js +454 -0
  28. package/dist/_chunks/file-cache-DmX7OqZP.js.map +1 -0
  29. package/dist/_chunks/fs-identity-D7vb6GdI.js +208 -0
  30. package/dist/_chunks/fs-identity-D7vb6GdI.js.map +1 -0
  31. package/dist/_chunks/{logger-AWfuX-KJ.js → logger-D8xJZXIN.js} +34 -51
  32. package/dist/_chunks/logger-D8xJZXIN.js.map +1 -0
  33. package/dist/_chunks/mdx-file-C005ay-P.js +25 -0
  34. package/dist/_chunks/mdx-file-C005ay-P.js.map +1 -0
  35. package/dist/_chunks/{plugin-context---kTF5v8.js → plugin-context-rCinWLiE.js} +8 -2
  36. package/dist/_chunks/plugin-context-rCinWLiE.js.map +1 -0
  37. package/dist/_chunks/purge-store-Byr8XOjU.js +14 -0
  38. package/dist/_chunks/purge-store-Byr8XOjU.js.map +1 -0
  39. package/dist/_chunks/{resolve-schema-Dz3fcFUo.js → resolve-schema-5ma5pp1b.js} +2 -2
  40. package/dist/_chunks/{resolve-schema-Dz3fcFUo.js.map → resolve-schema-5ma5pp1b.js.map} +1 -1
  41. package/dist/_chunks/{router-ref-CtmF-aPv.js → router-ref-DuYuV_0Q.js} +2 -2
  42. package/dist/_chunks/router-ref-DuYuV_0Q.js.map +1 -0
  43. package/dist/_chunks/rsc-cache-key-DD0fl_-s.js +243 -0
  44. package/dist/_chunks/rsc-cache-key-DD0fl_-s.js.map +1 -0
  45. package/dist/_chunks/rsc-error-envelope-tT5PJs4q.js +40 -0
  46. package/dist/_chunks/rsc-error-envelope-tT5PJs4q.js.map +1 -0
  47. package/dist/_chunks/rsc-media-type-DRqE_lD_.js +46 -0
  48. package/dist/_chunks/rsc-media-type-DRqE_lD_.js.map +1 -0
  49. package/dist/_chunks/rsc-payload-path-B_LBodc2.js +48 -0
  50. package/dist/_chunks/rsc-payload-path-B_LBodc2.js.map +1 -0
  51. package/dist/_chunks/scanner-C8b0Gcw3.js +1564 -0
  52. package/dist/_chunks/scanner-C8b0Gcw3.js.map +1 -0
  53. package/dist/_chunks/schema-bridge-Cc2Gngu1.js +199 -0
  54. package/dist/_chunks/schema-bridge-Cc2Gngu1.js.map +1 -0
  55. package/dist/_chunks/{segment-classify-Byy425ng.js → segment-classify-C539Pa2O.js} +23 -2
  56. package/dist/_chunks/segment-classify-C539Pa2O.js.map +1 -0
  57. package/dist/_chunks/segment-context-CjOlyB8Y.js +97 -0
  58. package/dist/_chunks/segment-context-CjOlyB8Y.js.map +1 -0
  59. package/dist/_chunks/{canonicalize-DQHyFClh.js → segment-keys-BawYuNFO.js} +61 -2
  60. package/dist/_chunks/segment-keys-BawYuNFO.js.map +1 -0
  61. package/dist/_chunks/slot-params-BCTmZkQB.js +76 -0
  62. package/dist/_chunks/slot-params-BCTmZkQB.js.map +1 -0
  63. package/dist/_chunks/{ssr-data-BOWsq18U.js → ssr-data-14MXm7Pj.js} +17 -2
  64. package/dist/_chunks/ssr-data-14MXm7Pj.js.map +1 -0
  65. package/dist/_chunks/{use-query-states-DFvWd-EA.js → use-query-states-BbU5Ge1V.js} +74 -26
  66. package/dist/_chunks/use-query-states-BbU5Ge1V.js.map +1 -0
  67. package/dist/_chunks/use-segment-params-C4r4BD9T.js +398 -0
  68. package/dist/_chunks/use-segment-params-C4r4BD9T.js.map +1 -0
  69. package/dist/_chunks/walkers-RzN6AFjr.js +141 -0
  70. package/dist/_chunks/walkers-RzN6AFjr.js.map +1 -0
  71. package/dist/adapters/build-output-helper.d.ts +37 -0
  72. package/dist/adapters/build-output-helper.d.ts.map +1 -1
  73. package/dist/adapters/cloudflare-dev.js +1 -1
  74. package/dist/adapters/cloudflare-kv-cache.js +1 -1
  75. package/dist/adapters/cloudflare.d.ts.map +1 -1
  76. package/dist/adapters/cloudflare.js +1 -1
  77. package/dist/adapters/compress-module.d.ts +12 -0
  78. package/dist/adapters/compress-module.d.ts.map +1 -1
  79. package/dist/adapters/fs-identity.d.ts +64 -0
  80. package/dist/adapters/fs-identity.d.ts.map +1 -0
  81. package/dist/adapters/nitro.d.ts.map +1 -1
  82. package/dist/adapters/nitro.js +56 -5
  83. package/dist/adapters/nitro.js.map +1 -1
  84. package/dist/adapters/shared.d.ts +57 -1
  85. package/dist/adapters/shared.d.ts.map +1 -1
  86. package/dist/adapters/types.d.ts +13 -1
  87. package/dist/adapters/types.d.ts.map +1 -1
  88. package/dist/cache/fast-hash.d.ts +5 -0
  89. package/dist/cache/fast-hash.d.ts.map +1 -1
  90. package/dist/cache/index.js +1 -1
  91. package/dist/cdn/cloudflare-purge.js +30 -0
  92. package/dist/cdn/cloudflare-purge.js.map +1 -0
  93. package/dist/cdn/fastly-purge.js +33 -0
  94. package/dist/cdn/fastly-purge.js.map +1 -0
  95. package/dist/cdn/index.js +94 -0
  96. package/dist/cdn/index.js.map +1 -0
  97. package/dist/cdn/workers-cache-purge.js +35 -0
  98. package/dist/cdn/workers-cache-purge.js.map +1 -0
  99. package/dist/cli-check.d.ts +153 -0
  100. package/dist/cli-check.d.ts.map +1 -0
  101. package/dist/cli.d.ts +34 -8
  102. package/dist/cli.d.ts.map +1 -1
  103. package/dist/cli.js +46 -21
  104. package/dist/cli.js.map +1 -1
  105. package/dist/client/browser-entry/action-dispatch.d.ts.map +1 -1
  106. package/dist/client/browser-entry/hydrate.d.ts +20 -9
  107. package/dist/client/browser-entry/hydrate.d.ts.map +1 -1
  108. package/dist/client/browser-entry/index.d.ts +1 -1
  109. package/dist/client/browser-entry/index.d.ts.map +1 -1
  110. package/dist/client/browser-entry/post-hydration.d.ts +4 -1
  111. package/dist/client/browser-entry/post-hydration.d.ts.map +1 -1
  112. package/dist/client/browser-entry/router-init.d.ts +3 -0
  113. package/dist/client/browser-entry/router-init.d.ts.map +1 -1
  114. package/dist/client/browser-entry/rsc-stream.d.ts +8 -0
  115. package/dist/client/browser-entry/rsc-stream.d.ts.map +1 -1
  116. package/dist/client/error-boundary.d.ts +7 -0
  117. package/dist/client/error-boundary.d.ts.map +1 -1
  118. package/dist/client/error-boundary.js +1 -162
  119. package/dist/client/history.d.ts +13 -6
  120. package/dist/client/history.d.ts.map +1 -1
  121. package/dist/client/index.d.ts +1 -0
  122. package/dist/client/index.d.ts.map +1 -1
  123. package/dist/client/index.js +42 -41
  124. package/dist/client/index.js.map +1 -1
  125. package/dist/client/internal.d.ts +4 -3
  126. package/dist/client/internal.d.ts.map +1 -1
  127. package/dist/client/internal.js +877 -311
  128. package/dist/client/internal.js.map +1 -1
  129. package/dist/client/link.d.ts +1 -7
  130. package/dist/client/link.d.ts.map +1 -1
  131. package/dist/client/navigation-api.d.ts +2 -2
  132. package/dist/client/navigation-commit.d.ts +87 -0
  133. package/dist/client/navigation-commit.d.ts.map +1 -0
  134. package/dist/client/navigation-context.d.ts +37 -20
  135. package/dist/client/navigation-context.d.ts.map +1 -1
  136. package/dist/client/navigation-root.d.ts +63 -23
  137. package/dist/client/navigation-root.d.ts.map +1 -1
  138. package/dist/client/params-context.d.ts +81 -0
  139. package/dist/client/params-context.d.ts.map +1 -0
  140. package/dist/client/router-effects.d.ts +129 -0
  141. package/dist/client/router-effects.d.ts.map +1 -0
  142. package/dist/client/router-lifecycle.d.ts +55 -0
  143. package/dist/client/router-lifecycle.d.ts.map +1 -0
  144. package/dist/client/router-pipeline.d.ts +84 -0
  145. package/dist/client/router-pipeline.d.ts.map +1 -0
  146. package/dist/client/router-ref.d.ts +1 -1
  147. package/dist/client/router-ref.d.ts.map +1 -1
  148. package/dist/client/router-skew.d.ts +21 -0
  149. package/dist/client/router-skew.d.ts.map +1 -0
  150. package/dist/client/router-types.d.ts +207 -0
  151. package/dist/client/router-types.d.ts.map +1 -0
  152. package/dist/client/router.d.ts +1 -182
  153. package/dist/client/router.d.ts.map +1 -1
  154. package/dist/client/rsc-fetch.d.ts +47 -26
  155. package/dist/client/rsc-fetch.d.ts.map +1 -1
  156. package/dist/client/segment-cache.d.ts +87 -34
  157. package/dist/client/segment-cache.d.ts.map +1 -1
  158. package/dist/client/ssr-data.d.ts +6 -0
  159. package/dist/client/ssr-data.d.ts.map +1 -1
  160. package/dist/client/stale-client.d.ts +69 -0
  161. package/dist/client/stale-client.d.ts.map +1 -0
  162. package/dist/client/state.d.ts +10 -1
  163. package/dist/client/state.d.ts.map +1 -1
  164. package/dist/client/top-loader.d.ts +2 -1
  165. package/dist/client/top-loader.d.ts.map +1 -1
  166. package/dist/client/unload-guard.d.ts +5 -0
  167. package/dist/client/unload-guard.d.ts.map +1 -1
  168. package/dist/client/use-cookie.d.ts.map +1 -1
  169. package/dist/client/use-query-states.d.ts +9 -3
  170. package/dist/client/use-query-states.d.ts.map +1 -1
  171. package/dist/client/use-router.d.ts +1 -1
  172. package/dist/client/use-segment-params.d.ts +18 -8
  173. package/dist/client/use-segment-params.d.ts.map +1 -1
  174. package/dist/codec.js +1 -1
  175. package/dist/cookies/define-cookie.d.ts.map +1 -1
  176. package/dist/cookies/index.js +5 -5
  177. package/dist/cookies/index.js.map +1 -1
  178. package/dist/index.d.ts +4 -1
  179. package/dist/index.d.ts.map +1 -1
  180. package/dist/index.js +1009 -759
  181. package/dist/index.js.map +1 -1
  182. package/dist/params/index.js +1 -1
  183. package/dist/plugin-context.d.ts +46 -0
  184. package/dist/plugin-context.d.ts.map +1 -1
  185. package/dist/plugins/adapter-build.d.ts.map +1 -1
  186. package/dist/plugins/build-manifest.d.ts +1 -1
  187. package/dist/plugins/build-manifest.d.ts.map +1 -1
  188. package/dist/plugins/entries.d.ts.map +1 -1
  189. package/dist/plugins/fonts.d.ts.map +1 -1
  190. package/dist/plugins/request-dep/analysis.d.ts +106 -0
  191. package/dist/plugins/request-dep/analysis.d.ts.map +1 -0
  192. package/dist/plugins/request-dep/index.d.ts +75 -0
  193. package/dist/plugins/request-dep/index.d.ts.map +1 -0
  194. package/dist/plugins/request-dep/origin.d.ts +61 -0
  195. package/dist/plugins/request-dep/origin.d.ts.map +1 -0
  196. package/dist/plugins/routing.d.ts.map +1 -1
  197. package/dist/plugins/shims.d.ts.map +1 -1
  198. package/dist/plugins/static-build.d.ts +160 -8
  199. package/dist/plugins/static-build.d.ts.map +1 -1
  200. package/dist/routing/codegen-shared.d.ts +3 -44
  201. package/dist/routing/codegen-shared.d.ts.map +1 -1
  202. package/dist/routing/codegen-types.d.ts +28 -31
  203. package/dist/routing/codegen-types.d.ts.map +1 -1
  204. package/dist/routing/codegen-write.d.ts +51 -0
  205. package/dist/routing/codegen-write.d.ts.map +1 -0
  206. package/dist/routing/codegen.d.ts.map +1 -1
  207. package/dist/routing/collision-probe.d.ts +104 -0
  208. package/dist/routing/collision-probe.d.ts.map +1 -0
  209. package/dist/routing/collision-spaces.d.ts +26 -0
  210. package/dist/routing/collision-spaces.d.ts.map +1 -0
  211. package/dist/routing/convention-lint.d.ts +18 -4
  212. package/dist/routing/convention-lint.d.ts.map +1 -1
  213. package/dist/routing/export-detect.d.ts +16 -0
  214. package/dist/routing/export-detect.d.ts.map +1 -1
  215. package/dist/routing/index.js +4 -3
  216. package/dist/routing/interception.d.ts +235 -6
  217. package/dist/routing/interception.d.ts.map +1 -1
  218. package/dist/routing/link-codegen.d.ts +19 -4
  219. package/dist/routing/link-codegen.d.ts.map +1 -1
  220. package/dist/routing/manifest-codegen.d.ts +1 -7
  221. package/dist/routing/manifest-codegen.d.ts.map +1 -1
  222. package/dist/routing/scanner.d.ts.map +1 -1
  223. package/dist/routing/segment-classify.d.ts +33 -0
  224. package/dist/routing/segment-classify.d.ts.map +1 -1
  225. package/dist/routing/segment-keys.d.ts +95 -0
  226. package/dist/routing/segment-keys.d.ts.map +1 -0
  227. package/dist/routing/slot-placement.d.ts +48 -0
  228. package/dist/routing/slot-placement.d.ts.map +1 -0
  229. package/dist/routing/types.d.ts +0 -6
  230. package/dist/routing/types.d.ts.map +1 -1
  231. package/dist/routing/walkers.d.ts +6 -0
  232. package/dist/routing/walkers.d.ts.map +1 -1
  233. package/dist/schema-bridge.d.ts +60 -9
  234. package/dist/schema-bridge.d.ts.map +1 -1
  235. package/dist/search-params/define.d.ts +62 -14
  236. package/dist/search-params/define.d.ts.map +1 -1
  237. package/dist/search-params/index.d.ts +0 -1
  238. package/dist/search-params/index.d.ts.map +1 -1
  239. package/dist/search-params/index.js +221 -10
  240. package/dist/search-params/index.js.map +1 -1
  241. package/dist/search-params/parse-total.d.ts +70 -0
  242. package/dist/search-params/parse-total.d.ts.map +1 -0
  243. package/dist/search-params/wrappers.d.ts +26 -3
  244. package/dist/search-params/wrappers.d.ts.map +1 -1
  245. package/dist/server/access-gate.d.ts +19 -8
  246. package/dist/server/access-gate.d.ts.map +1 -1
  247. package/dist/server/action-handler.d.ts.map +1 -1
  248. package/dist/server/actions.d.ts +6 -3
  249. package/dist/server/actions.d.ts.map +1 -1
  250. package/dist/server/als-registry.d.ts +18 -1
  251. package/dist/server/als-registry.d.ts.map +1 -1
  252. package/dist/server/build-manifest.d.ts +13 -1
  253. package/dist/server/build-manifest.d.ts.map +1 -1
  254. package/dist/server/chain-url-parts.d.ts +82 -0
  255. package/dist/server/chain-url-parts.d.ts.map +1 -0
  256. package/dist/server/children-interception.d.ts +49 -0
  257. package/dist/server/children-interception.d.ts.map +1 -0
  258. package/dist/server/compress.d.ts.map +1 -1
  259. package/dist/server/default-status-page.d.ts +45 -0
  260. package/dist/server/default-status-page.d.ts.map +1 -0
  261. package/dist/server/deny-boundary.d.ts +148 -15
  262. package/dist/server/deny-boundary.d.ts.map +1 -1
  263. package/dist/server/deny-renderer.d.ts +15 -4
  264. package/dist/server/deny-renderer.d.ts.map +1 -1
  265. package/dist/server/error-boundary-wrapper.d.ts +85 -15
  266. package/dist/server/error-boundary-wrapper.d.ts.map +1 -1
  267. package/dist/server/head-response.d.ts +35 -0
  268. package/dist/server/head-response.d.ts.map +1 -0
  269. package/dist/server/index.d.ts +0 -1
  270. package/dist/server/index.d.ts.map +1 -1
  271. package/dist/server/index.js +3 -2
  272. package/dist/server/index.js.map +1 -1
  273. package/dist/server/internal.d.ts +3 -1
  274. package/dist/server/internal.d.ts.map +1 -1
  275. package/dist/server/internal.js +712 -418
  276. package/dist/server/internal.js.map +1 -1
  277. package/dist/server/metadata-collector.d.ts +52 -0
  278. package/dist/server/metadata-collector.d.ts.map +1 -0
  279. package/dist/server/param-coercion.d.ts +22 -6
  280. package/dist/server/param-coercion.d.ts.map +1 -1
  281. package/dist/server/pipeline-helpers.d.ts +0 -18
  282. package/dist/server/pipeline-helpers.d.ts.map +1 -1
  283. package/dist/server/pipeline-interception.d.ts +59 -9
  284. package/dist/server/pipeline-interception.d.ts.map +1 -1
  285. package/dist/server/pipeline-outcome.d.ts.map +1 -1
  286. package/dist/server/pipeline-phases.d.ts.map +1 -1
  287. package/dist/server/pipeline.d.ts +11 -0
  288. package/dist/server/pipeline.d.ts.map +1 -1
  289. package/dist/server/primitives.d.ts +23 -0
  290. package/dist/server/primitives.d.ts.map +1 -1
  291. package/dist/server/publish-params.d.ts +33 -0
  292. package/dist/server/publish-params.d.ts.map +1 -0
  293. package/dist/server/request-context.d.ts +48 -0
  294. package/dist/server/request-context.d.ts.map +1 -1
  295. package/dist/server/route-element-builder.d.ts +22 -3
  296. package/dist/server/route-element-builder.d.ts.map +1 -1
  297. package/dist/server/route-handler.d.ts.map +1 -1
  298. package/dist/server/route-matcher.d.ts +13 -0
  299. package/dist/server/route-matcher.d.ts.map +1 -1
  300. package/dist/server/rsc-cache-key-guard.d.ts +78 -0
  301. package/dist/server/rsc-cache-key-guard.d.ts.map +1 -0
  302. package/dist/server/rsc-entry/api-handler.d.ts.map +1 -1
  303. package/dist/server/rsc-entry/deny-fallback.d.ts.map +1 -1
  304. package/dist/server/rsc-entry/error-renderer.d.ts +15 -3
  305. package/dist/server/rsc-entry/error-renderer.d.ts.map +1 -1
  306. package/dist/server/rsc-entry/helpers.d.ts +2 -17
  307. package/dist/server/rsc-entry/helpers.d.ts.map +1 -1
  308. package/dist/server/rsc-entry/index.d.ts +0 -1
  309. package/dist/server/rsc-entry/index.d.ts.map +1 -1
  310. package/dist/server/rsc-entry/render-route.d.ts.map +1 -1
  311. package/dist/server/rsc-entry/revalidate-renderer.d.ts.map +1 -1
  312. package/dist/server/rsc-entry/rsc-payload.d.ts +1 -2
  313. package/dist/server/rsc-entry/rsc-payload.d.ts.map +1 -1
  314. package/dist/server/rsc-entry/rsc-stream.d.ts +15 -2
  315. package/dist/server/rsc-entry/rsc-stream.d.ts.map +1 -1
  316. package/dist/server/rsc-entry/ssr-renderer.d.ts.map +1 -1
  317. package/dist/server/skippable-prefix.d.ts +23 -0
  318. package/dist/server/skippable-prefix.d.ts.map +1 -0
  319. package/dist/server/slot-interception.d.ts +80 -0
  320. package/dist/server/slot-interception.d.ts.map +1 -0
  321. package/dist/server/slot-resolver.d.ts +70 -8
  322. package/dist/server/slot-resolver.d.ts.map +1 -1
  323. package/dist/server/ssr-bridge-types.d.ts +17 -0
  324. package/dist/server/ssr-bridge-types.d.ts.map +1 -1
  325. package/dist/server/ssr-entry.d.ts +0 -1
  326. package/dist/server/ssr-entry.d.ts.map +1 -1
  327. package/dist/server/ssr-wrappers.d.ts +24 -13
  328. package/dist/server/ssr-wrappers.d.ts.map +1 -1
  329. package/dist/server/state-tree-diff.d.ts +6 -27
  330. package/dist/server/state-tree-diff.d.ts.map +1 -1
  331. package/dist/server/static-generator.d.ts +6 -0
  332. package/dist/server/static-generator.d.ts.map +1 -1
  333. package/dist/server/static-not-found.d.ts +52 -0
  334. package/dist/server/static-not-found.d.ts.map +1 -0
  335. package/dist/server/status-code-resolver.d.ts +8 -1
  336. package/dist/server/status-code-resolver.d.ts.map +1 -1
  337. package/dist/server/tree-builder.d.ts +28 -36
  338. package/dist/server/tree-builder.d.ts.map +1 -1
  339. package/dist/server/types.d.ts +12 -7
  340. package/dist/server/types.d.ts.map +1 -1
  341. package/dist/server/utils/element-type.d.ts +40 -0
  342. package/dist/server/utils/element-type.d.ts.map +1 -0
  343. package/dist/shared/als-slots.d.ts +41 -0
  344. package/dist/shared/als-slots.d.ts.map +1 -0
  345. package/dist/shared/base-path.d.ts +56 -0
  346. package/dist/shared/base-path.d.ts.map +1 -0
  347. package/dist/shared/param-value.d.ts +80 -0
  348. package/dist/shared/param-value.d.ts.map +1 -0
  349. package/dist/shared/payload-root.d.ts +108 -0
  350. package/dist/shared/payload-root.d.ts.map +1 -0
  351. package/dist/shared/rsc-cache-key.d.ts +163 -0
  352. package/dist/shared/rsc-cache-key.d.ts.map +1 -0
  353. package/dist/shared/rsc-error-envelope.d.ts +25 -0
  354. package/dist/shared/rsc-error-envelope.d.ts.map +1 -0
  355. package/dist/shared/rsc-manifest.d.ts +28 -0
  356. package/dist/shared/rsc-manifest.d.ts.map +1 -0
  357. package/dist/shared/rsc-media-type.d.ts +40 -0
  358. package/dist/shared/rsc-media-type.d.ts.map +1 -0
  359. package/dist/shared/rsc-payload-path.d.ts +42 -0
  360. package/dist/shared/rsc-payload-path.d.ts.map +1 -0
  361. package/dist/shared/segment-info.d.ts +59 -0
  362. package/dist/shared/segment-info.d.ts.map +1 -0
  363. package/dist/shared/slot-params.d.ts +51 -0
  364. package/dist/shared/slot-params.d.ts.map +1 -0
  365. package/dist/shared/static-platform-files.d.ts +71 -0
  366. package/dist/shared/static-platform-files.d.ts.map +1 -0
  367. package/docs/api/30-api-server.mdx +1 -1
  368. package/docs/api/31-api-client.mdx +4 -2
  369. package/docs/api/33-api-search-params.mdx +38 -16
  370. package/docs/api/35-api-typescript.mdx +3 -3
  371. package/docs/api/36-cli.mdx +34 -7
  372. package/docs/learn/00-introduction.mdx +1 -1
  373. package/docs/learn/02-pages-and-layouts.mdx +2 -2
  374. package/docs/learn/05-typed-params.mdx +8 -8
  375. package/docs/learn/07-typed-routes.mdx +12 -7
  376. package/docs/learn/11-error-handling.mdx +18 -0
  377. package/docs/learn/12-client-navigation.mdx +2 -2
  378. package/docs/learn/14-deploying.mdx +23 -0
  379. package/docs/more/01-advanced-routing.mdx +173 -3
  380. package/docs/more/03-coming-from-nextjs.mdx +2 -2
  381. package/docs/more/04-metadata-and-fonts.mdx +1 -1
  382. package/docs/more/50-ai-agent-instructions.mdx +6 -4
  383. package/package.json +9 -6
  384. package/src/adapters/build-output-helper.ts +116 -3
  385. package/src/adapters/cloudflare.ts +90 -12
  386. package/src/adapters/compress-module.ts +79 -1
  387. package/src/adapters/fs-identity.ts +156 -0
  388. package/src/adapters/nitro.ts +6 -6
  389. package/src/adapters/shared.ts +196 -21
  390. package/src/adapters/types.ts +13 -1
  391. package/src/cache/fast-hash.ts +5 -0
  392. package/src/cli-check.ts +458 -0
  393. package/src/cli.ts +59 -24
  394. package/src/client/browser-entry/action-dispatch.ts +11 -5
  395. package/src/client/browser-entry/hydrate.ts +40 -29
  396. package/src/client/browser-entry/index.ts +15 -6
  397. package/src/client/browser-entry/post-hydration.ts +10 -9
  398. package/src/client/browser-entry/router-init.ts +104 -48
  399. package/src/client/browser-entry/rsc-stream.ts +11 -2
  400. package/src/client/error-boundary.tsx +85 -16
  401. package/src/client/history.ts +13 -6
  402. package/src/client/index.ts +14 -3
  403. package/src/client/internal.ts +4 -3
  404. package/src/client/link.tsx +120 -72
  405. package/src/client/navigation-api.ts +3 -3
  406. package/src/client/navigation-commit.ts +197 -0
  407. package/src/client/navigation-context.ts +40 -67
  408. package/src/client/navigation-root.tsx +326 -101
  409. package/src/client/params-context.ts +192 -0
  410. package/src/client/router-effects.ts +231 -0
  411. package/src/client/router-lifecycle.ts +288 -0
  412. package/src/client/router-pipeline.ts +393 -0
  413. package/src/client/router-ref.ts +1 -1
  414. package/src/client/router-skew.ts +30 -0
  415. package/src/client/router-types.ts +230 -0
  416. package/src/client/router.ts +243 -707
  417. package/src/client/rsc-fetch.ts +183 -106
  418. package/src/client/segment-cache.ts +129 -44
  419. package/src/client/ssr-data.ts +6 -0
  420. package/src/client/stale-client.ts +191 -0
  421. package/src/client/state.ts +14 -1
  422. package/src/client/top-loader.tsx +2 -1
  423. package/src/client/unload-guard.ts +34 -2
  424. package/src/client/use-cookie.ts +9 -2
  425. package/src/client/use-query-states.ts +102 -39
  426. package/src/client/use-router.ts +1 -1
  427. package/src/client/use-segment-params.ts +41 -18
  428. package/src/cookies/define-cookie.ts +6 -1
  429. package/src/index.ts +25 -4
  430. package/src/plugin-context.ts +58 -0
  431. package/src/plugins/adapter-build.ts +4 -0
  432. package/src/plugins/build-manifest.ts +34 -13
  433. package/src/plugins/entries.ts +4 -0
  434. package/src/plugins/fonts.ts +11 -8
  435. package/src/plugins/request-dep/analysis.ts +447 -0
  436. package/src/plugins/request-dep/index.ts +376 -0
  437. package/src/plugins/request-dep/origin.ts +310 -0
  438. package/src/plugins/routing.ts +97 -102
  439. package/src/plugins/shims.ts +0 -1
  440. package/src/plugins/static-build.ts +391 -95
  441. package/src/routing/codegen-shared.ts +3 -79
  442. package/src/routing/codegen-types.ts +28 -31
  443. package/src/routing/codegen-write.ts +139 -0
  444. package/src/routing/codegen.ts +86 -172
  445. package/src/routing/collision-probe.ts +438 -0
  446. package/src/routing/collision-spaces.ts +281 -0
  447. package/src/routing/convention-lint.ts +139 -40
  448. package/src/routing/export-detect.ts +151 -7
  449. package/src/routing/interception.ts +896 -50
  450. package/src/routing/link-codegen.ts +32 -65
  451. package/src/routing/manifest-codegen.ts +6 -62
  452. package/src/routing/scanner.ts +117 -92
  453. package/src/routing/segment-classify.ts +57 -0
  454. package/src/routing/segment-keys.ts +146 -0
  455. package/src/routing/slot-placement.ts +97 -0
  456. package/src/routing/types.ts +0 -6
  457. package/src/routing/walkers.ts +8 -0
  458. package/src/schema-bridge.ts +180 -58
  459. package/src/search-params/define.ts +108 -66
  460. package/src/search-params/index.ts +0 -1
  461. package/src/search-params/parse-total.ts +78 -0
  462. package/src/search-params/wrappers.ts +60 -11
  463. package/src/server/access-gate.tsx +60 -40
  464. package/src/server/action-handler.ts +4 -5
  465. package/src/server/actions.ts +6 -3
  466. package/src/server/als-registry.ts +18 -1
  467. package/src/server/build-manifest.ts +14 -1
  468. package/src/server/chain-url-parts.ts +163 -0
  469. package/src/server/children-interception.ts +115 -0
  470. package/src/server/compress.ts +9 -1
  471. package/src/server/default-status-page.ts +127 -0
  472. package/src/server/deny-boundary.ts +269 -41
  473. package/src/server/deny-renderer.ts +111 -39
  474. package/src/server/error-boundary-wrapper.ts +166 -79
  475. package/src/server/head-response.ts +54 -0
  476. package/src/server/index.ts +1 -3
  477. package/src/server/internal.ts +2 -2
  478. package/src/server/metadata-collector.ts +115 -0
  479. package/src/server/param-coercion.ts +51 -69
  480. package/src/server/pipeline-helpers.ts +2 -47
  481. package/src/server/pipeline-interception.ts +81 -32
  482. package/src/server/pipeline-outcome.ts +4 -5
  483. package/src/server/pipeline-phases.ts +77 -25
  484. package/src/server/pipeline.ts +57 -10
  485. package/src/server/prebuilt-runtime.ts +4 -4
  486. package/src/server/primitives.ts +25 -0
  487. package/src/server/publish-params.ts +42 -0
  488. package/src/server/request-context.ts +97 -18
  489. package/src/server/route-element-builder.ts +241 -253
  490. package/src/server/route-handler.ts +8 -8
  491. package/src/server/route-matcher.ts +21 -9
  492. package/src/server/rsc-cache-key-guard.ts +164 -0
  493. package/src/server/rsc-entry/api-handler.ts +6 -8
  494. package/src/server/rsc-entry/deny-fallback.ts +3 -2
  495. package/src/server/rsc-entry/error-renderer.ts +94 -35
  496. package/src/server/rsc-entry/helpers.ts +33 -28
  497. package/src/server/rsc-entry/index.ts +36 -33
  498. package/src/server/rsc-entry/render-route.ts +18 -10
  499. package/src/server/rsc-entry/revalidate-renderer.ts +3 -2
  500. package/src/server/rsc-entry/rsc-payload.ts +113 -40
  501. package/src/server/rsc-entry/rsc-stream.ts +29 -5
  502. package/src/server/rsc-entry/ssr-renderer.ts +38 -22
  503. package/src/server/skippable-prefix.ts +92 -0
  504. package/src/server/slot-interception.ts +168 -0
  505. package/src/server/slot-resolver.ts +263 -158
  506. package/src/server/ssr-bridge-types.ts +18 -0
  507. package/src/server/ssr-entry.ts +20 -10
  508. package/src/server/ssr-wrappers.tsx +33 -32
  509. package/src/server/state-tree-diff.ts +14 -55
  510. package/src/server/static-generator.ts +68 -29
  511. package/src/server/static-not-found.ts +134 -0
  512. package/src/server/status-code-resolver.ts +28 -11
  513. package/src/server/tree-builder.ts +35 -218
  514. package/src/server/types.ts +12 -7
  515. package/src/server/utils/element-type.ts +72 -0
  516. package/src/shared/als-slots.ts +72 -0
  517. package/src/shared/base-path.ts +80 -0
  518. package/src/shared/param-value.ts +244 -0
  519. package/src/shared/payload-root.ts +151 -0
  520. package/src/shared/rsc-cache-key.ts +269 -0
  521. package/src/shared/rsc-error-envelope.ts +31 -0
  522. package/src/shared/rsc-manifest.ts +29 -0
  523. package/src/shared/rsc-media-type.ts +43 -0
  524. package/src/shared/rsc-payload-path.ts +45 -0
  525. package/src/shared/segment-info.ts +60 -0
  526. package/src/shared/slot-params.ts +111 -0
  527. package/src/shared/static-platform-files.ts +75 -0
  528. package/dist/_chunks/build-output-helper-DOGYFb_X.js +0 -196
  529. package/dist/_chunks/build-output-helper-DOGYFb_X.js.map +0 -1
  530. package/dist/_chunks/cache-api-B-lhk9p4.js.map +0 -1
  531. package/dist/_chunks/canonicalize-DQHyFClh.js.map +0 -1
  532. package/dist/_chunks/cli-schema-sync-BTWEKJXo.js +0 -668
  533. package/dist/_chunks/cli-schema-sync-BTWEKJXo.js.map +0 -1
  534. package/dist/_chunks/define-COtkxMRT.js +0 -191
  535. package/dist/_chunks/define-COtkxMRT.js.map +0 -1
  536. package/dist/_chunks/define-c4au4I9R.js +0 -82
  537. package/dist/_chunks/define-c4au4I9R.js.map +0 -1
  538. package/dist/_chunks/fast-hash-D6hIVt1Y.js.map +0 -1
  539. package/dist/_chunks/logger-AWfuX-KJ.js.map +0 -1
  540. package/dist/_chunks/navigation-root-bUDZLw74.js +0 -124
  541. package/dist/_chunks/navigation-root-bUDZLw74.js.map +0 -1
  542. package/dist/_chunks/plugin-context---kTF5v8.js.map +0 -1
  543. package/dist/_chunks/registry-DbJPKoBp.js +0 -20
  544. package/dist/_chunks/registry-DbJPKoBp.js.map +0 -1
  545. package/dist/_chunks/router-ref-CtmF-aPv.js.map +0 -1
  546. package/dist/_chunks/schema-bridge-DT_Tn0Xf.js +0 -119
  547. package/dist/_chunks/schema-bridge-DT_Tn0Xf.js.map +0 -1
  548. package/dist/_chunks/segment-classify-Byy425ng.js.map +0 -1
  549. package/dist/_chunks/segment-context-ZDnXDkbz.js +0 -34
  550. package/dist/_chunks/segment-context-ZDnXDkbz.js.map +0 -1
  551. package/dist/_chunks/ssr-data-BOWsq18U.js.map +0 -1
  552. package/dist/_chunks/use-query-states-DFvWd-EA.js.map +0 -1
  553. package/dist/_chunks/use-segment-params-D1CzlgBo.js +0 -151
  554. package/dist/_chunks/use-segment-params-D1CzlgBo.js.map +0 -1
  555. package/dist/_chunks/walkers-_6zKFlch.js +0 -1002
  556. package/dist/_chunks/walkers-_6zKFlch.js.map +0 -1
  557. package/dist/client/error-boundary.js.map +0 -1
  558. package/dist/plugins/request-dep.d.ts +0 -32
  559. package/dist/plugins/request-dep.d.ts.map +0 -1
  560. package/dist/search-params/registry.d.ts +0 -20
  561. package/dist/search-params/registry.d.ts.map +0 -1
  562. package/dist/segment-params/define.d.ts +0 -89
  563. package/dist/segment-params/define.d.ts.map +0 -1
  564. package/dist/segment-params/index.d.ts +0 -3
  565. package/dist/segment-params/index.d.ts.map +0 -1
  566. package/dist/segment-params/index.js +0 -2
  567. package/src/plugins/request-dep.ts +0 -499
  568. package/src/search-params/registry.ts +0 -31
  569. package/src/segment-params/define.ts +0 -249
  570. package/src/segment-params/index.ts +0 -9
@@ -1,10 +1,79 @@
1
- import { t as bindUseQueryStates } from "../_chunks/use-query-states-DFvWd-EA.js";
2
- import { c as cachedSearchParams, i as _setCachedSearch, n as getSsrData, r as setSsrData, s as cachedSearch, t as clearSsrData } from "../_chunks/ssr-data-BOWsq18U.js";
3
- import { a as setNavigationState, i as getNavigationState, o as useNavigationContext, r as NavigationProvider, t as setCurrentParams } from "../_chunks/use-segment-params-D1CzlgBo.js";
4
- import { n as getRouterOrNull, r as setGlobalRouter, t as getRouter } from "../_chunks/router-ref-CtmF-aPv.js";
5
- import { TimberErrorBoundary } from "./error-boundary.js";
6
- import { n as useSegmentContext, t as SegmentProvider } from "../_chunks/segment-context-ZDnXDkbz.js";
7
- import { n as supersedeNavigationTransitions, t as setHardNavigating } from "../_chunks/navigation-root-bUDZLw74.js";
1
+ import { t as RSC_CONTENT_TYPE } from "../_chunks/rsc-media-type-DRqE_lD_.js";
2
+ import { a as randomRscCacheKey, n as RSC_KEY_PARAM, o as recordLookup, s as rscCacheKey } from "../_chunks/rsc-cache-key-DD0fl_-s.js";
3
+ import { i as withBasePath, r as stripBasePath } from "../_chunks/base-path-DaQrzbez.js";
4
+ import { t as rscPayloadPath } from "../_chunks/rsc-payload-path-B_LBodc2.js";
5
+ import { i as _setCachedSearch, l as cachedSearch, n as getSsrData, r as setSsrData, t as clearSsrData, u as cachedSearchParams } from "../_chunks/ssr-data-14MXm7Pj.js";
6
+ import { n as getRouterOrNull, r as setGlobalRouter, t as getRouter } from "../_chunks/router-ref-DuYuV_0Q.js";
7
+ import { i as markStaleFromError, n as isClientStale, r as markClientStale, t as TimberErrorBoundary } from "../_chunks/error-boundary-D-ODYX41.js";
8
+ import { n as useSegmentContext, t as SegmentProvider } from "../_chunks/segment-context-CjOlyB8Y.js";
9
+ import "../_chunks/rsc-error-envelope-tT5PJs4q.js";
10
+ import { a as supersedeNavigationTransitions, c as setNavigationState, i as setHardNavigating, l as useNavigationContext, n as setCurrentSlotParams, o as NavigationProvider, s as getNavigationState, t as setCurrentParams } from "../_chunks/use-segment-params-C4r4BD9T.js";
11
+ import { t as bindUseQueryStates } from "../_chunks/use-query-states-BbU5Ge1V.js";
12
+ //#region src/shared/payload-root.ts
13
+ /**
14
+ * What a reader gets when the value it was handed is not a payload root.
15
+ *
16
+ * In the browser this is unreachable for a real response: all four producers
17
+ * of a route payload go through `withPublishedParams`, and a client talking to
18
+ * a different build is answered with 204 at Stage 1c
19
+ * (`server/pipeline-phases.ts`) before any payload exists. It is reachable in
20
+ * the router's test/fallback path, where `decodeRsc` is absent and the
21
+ * "payload" is the raw response text.
22
+ */
23
+ var NO_PUBLISHED_PARAMS = {
24
+ params: {},
25
+ slotParams: null
26
+ };
27
+ /** True when `value` carries published params — a root, or a read of one. */
28
+ function hasPublishedParams(value) {
29
+ return typeof value === "object" && value !== null && "params" in value && typeof value.params === "object" && value.params !== null;
30
+ }
31
+ /**
32
+ * Read the params published beside a tree.
33
+ *
34
+ * Accepts a payload root *or* a `PublishedParams` already split off one, so
35
+ * the client can pre-resolve on the navigation path and still hand the same
36
+ * value to the same provider. Never throws.
37
+ */
38
+ function readPublishedParams(source) {
39
+ if (!hasPublishedParams(source)) return NO_PUBLISHED_PARAMS;
40
+ return {
41
+ params: source.params,
42
+ slotParams: source.slotParams ?? null
43
+ };
44
+ }
45
+ /**
46
+ * Read the renderable tree out of a payload root.
47
+ *
48
+ * Returns the value unchanged when it is not a root — the router's fallback
49
+ * path stores raw response text under the same name, and a test asserting on
50
+ * that text should see the text.
51
+ */
52
+ function readPayloadTree(root) {
53
+ if (typeof root === "object" && root !== null && "tree" in root && hasPublishedParams(root)) return root.tree;
54
+ return root;
55
+ }
56
+ /**
57
+ * Split a decoded payload root into its tree and its params.
58
+ *
59
+ * Accepts a settled root or a thenable of one, and preserves which it was: a
60
+ * settled root splits synchronously, so no code path gains a suspend point it
61
+ * did not already have.
62
+ */
63
+ function splitPayloadRoot(root) {
64
+ if (typeof root !== "object" || root === null || typeof root.then !== "function") return {
65
+ tree: readPayloadTree(root),
66
+ params: readPublishedParams(root)
67
+ };
68
+ const settled = root;
69
+ const tree = Promise.resolve(settled).then(readPayloadTree);
70
+ tree.catch(() => {});
71
+ return {
72
+ tree,
73
+ params: Promise.resolve(settled).then(readPublishedParams, () => NO_PUBLISHED_PARAMS)
74
+ };
75
+ }
76
+ //#endregion
8
77
  //#region src/client/segment-cache.ts
9
78
  /**
10
79
  * Maintains the client-side segment tree representing currently mounted
@@ -32,18 +101,28 @@ var SegmentCache = class {
32
101
  * client can actually merge (i.e., segments whose cached element tree
33
102
  * contains an inner SegmentProvider the merger can splice into).
34
103
  *
104
+ * `treePaths` is collected UNFILTERED, unlike `segments` and `slots`. Those
105
+ * two answer "what may the server skip re-rendering?", so every reason a
106
+ * segment cannot be reused is a reason to leave it out. `treePaths` answers
107
+ * "what is mounted right now?" for interception scoping — a request-dependent
108
+ * layout is still on screen, and dropping it would silently narrow the scope
109
+ * the server sees and cancel a modal that should open (TIM-1282).
110
+ *
35
111
  * This is a performance optimization only, NOT a security boundary.
36
112
  * The server always runs all access.ts files regardless of the state tree.
37
113
  */
38
114
  serializeStateTree(mergeableFilter) {
39
115
  const segments = [];
40
116
  const slots = [];
117
+ const treePaths = [];
41
118
  if (this.root) {
42
119
  collectSyncSegments(this.root, segments, mergeableFilter);
43
120
  collectSyncSlots(this.root, slots);
121
+ collectTreePaths(this.root, treePaths);
44
122
  }
45
123
  const tree = { segments };
46
124
  if (slots.length > 0) tree.slots = slots;
125
+ if (treePaths.length > 0) tree.treePaths = treePaths;
47
126
  return tree;
48
127
  }
49
128
  };
@@ -52,6 +131,17 @@ function collectSyncSegments(node, out, mergeableFilter) {
52
131
  if (!node.isRequestDependent && (!mergeableFilter || mergeableFilter.has(node.segment))) out.push(node.segment);
53
132
  for (const child of node.children.values()) collectSyncSegments(child, out, mergeableFilter);
54
133
  }
134
+ /**
135
+ * Recursively collect the `app/` directory path of every mounted segment.
136
+ *
137
+ * Slots are not walked: a slot's own sub-tree can never own an intercepting
138
+ * slot's scope, because scopes are always ordinary segments (`interception.ts`
139
+ * derives one from the slot owner's ancestor chain).
140
+ */
141
+ function collectTreePaths(node, out) {
142
+ if (node.treePath) out.push(node.treePath);
143
+ for (const child of node.children.values()) collectTreePaths(child, out);
144
+ }
55
145
  /** Recursively collect cacheable slot paths from the tree */
56
146
  function collectSyncSlots(node, out) {
57
147
  if (node.slots) {
@@ -83,6 +173,7 @@ function buildSegmentTree(segments) {
83
173
  const id = info.segmentId ?? info.path;
84
174
  const node = {
85
175
  segment: id,
176
+ treePath: info.treePath,
86
177
  payload: null,
87
178
  isRequestDependent: info.isRequestDependent,
88
179
  children: /* @__PURE__ */ new Map()
@@ -112,6 +203,22 @@ function buildSegmentTree(segments) {
112
203
  /** Sentinel value for negative cache entries (URL is not a route). */
113
204
  var NEGATIVE_ENTRY = Object.freeze({ payload: null });
114
205
  /**
206
+ * Compose the map key. Length-prefixed rather than delimiter-joined for the
207
+ * same reason `shared/rsc-cache-key.ts` is: any delimiter is forgeable by a
208
+ * value containing it, and a URL may contain any character a delimiter could.
209
+ */
210
+ function prefetchMapKey(key) {
211
+ return `${key.from.length}:${key.from}:${key.scope.length}:${key.scope}:${key.url}`;
212
+ }
213
+ /**
214
+ * The `scope` half of a `PrefetchKey`, derived from the state tree that will
215
+ * actually be sent. Taking it from the header value rather than re-walking the
216
+ * cache is the point: the key varies by exactly what the request varies by.
217
+ */
218
+ function prefetchScopeOf(stateTree) {
219
+ return stateTree?.treePaths?.join("\0") ?? "";
220
+ }
221
+ /**
115
222
  * Short-lived cache for hover-triggered prefetches. Entries expire after
116
223
  * 30 seconds. When a link is clicked, the prefetched payload is consumed
117
224
  * (moved to the history stack) and removed from this cache.
@@ -122,34 +229,42 @@ var NEGATIVE_ENTRY = Object.freeze({ payload: null });
122
229
  var PrefetchCache = class PrefetchCache {
123
230
  static TTL_MS = 3e4;
124
231
  entries = /* @__PURE__ */ new Map();
125
- set(url, result) {
126
- this.entries.set(url, {
232
+ set(key, result) {
233
+ this.entries.set(prefetchMapKey(key), {
127
234
  result,
128
235
  expiresAt: Date.now() + PrefetchCache.TTL_MS
129
236
  });
130
237
  }
131
- get(url) {
132
- const entry = this.entries.get(url);
238
+ get(key) {
239
+ const mapKey = prefetchMapKey(key);
240
+ const entry = this.entries.get(mapKey);
133
241
  if (!entry) return void 0;
134
242
  if (Date.now() >= entry.expiresAt) {
135
- this.entries.delete(url);
243
+ this.entries.delete(mapKey);
136
244
  return;
137
245
  }
138
246
  return entry.result;
139
247
  }
140
248
  /** Get and remove the entry (used when navigation consumes a prefetch) */
141
- consume(url) {
142
- const result = this.get(url);
143
- if (result !== void 0) this.entries.delete(url);
249
+ consume(key) {
250
+ const result = this.get(key);
251
+ if (result !== void 0) this.entries.delete(prefetchMapKey(key));
144
252
  return result;
145
253
  }
146
- /** Store a negative entry — the URL is not a route (non-RSC Content-Type). */
147
- setNegative(url) {
148
- this.set(url, NEGATIVE_ENTRY);
254
+ /**
255
+ * Store a negative entry — the URL is not a route (non-RSC Content-Type).
256
+ *
257
+ * Keyed like every other entry even though "not a route" does not actually
258
+ * vary by departing URL: a source-independent negative would be a second
259
+ * keying rule to keep correct, and the only cost of the uniform one is a
260
+ * repeated fetch for a link hovered from a second page.
261
+ */
262
+ setNegative(key) {
263
+ this.set(key, NEGATIVE_ENTRY);
149
264
  }
150
265
  /** Check if the entry is a negative cache entry (URL is not a route). */
151
- isNegative(url) {
152
- return this.get(url) === NEGATIVE_ENTRY;
266
+ isNegative(key) {
267
+ return this.get(key) === NEGATIVE_ENTRY;
153
268
  }
154
269
  };
155
270
  //#endregion
@@ -183,30 +298,164 @@ var HistoryStack = class {
183
298
  }
184
299
  };
185
300
  //#endregion
186
- //#region src/client/rsc-fetch.ts
187
- var RSC_CONTENT_TYPE = "text/x-component";
301
+ //#region src/client/navigation-commit.ts
302
+ /**
303
+ * What it means for a page to be "current" on the client.
304
+ *
305
+ * One module owns every write that makes a navigation's destination the
306
+ * current page, and the read that describes the current page back to the
307
+ * server. Keeping them together is the point: `X-Timber-State-Tree` is
308
+ * derived from the segment cache, so the thing that publishes and the thing
309
+ * that reports have to agree about when a page becomes current.
310
+ *
311
+ * See design/19-client-navigation.md §"State Update Invariants".
312
+ */
188
313
  /**
189
- * Generate a short random cache-busting ID (5 chars, a-z0-9).
190
- * Matches the format Next.js uses for _rsc params.
314
+ * Derive the navigation state (pathname + search) a URL renders with.
315
+ *
316
+ * Pure: publishing it — the module-level fallback for tests and SSR, and the
317
+ * globalThis bridge — is `prepareNavigation`'s commit, which runs only once
318
+ * the render is known to have won (TIM-1301). Callers pass the returned value
319
+ * explicitly to renderRoot/wrapPayload, so a render never depends on the
320
+ * publish having happened first.
191
321
  */
192
- function generateCacheBustId() {
193
- const chars = "abcdefghijklmnopqrstuvwxyz0123456789";
194
- let id = "";
195
- for (let i = 0; i < 5; i++) id += chars[Math.random() * 36 | 0];
196
- return id;
322
+ function deriveNavigationState(url) {
323
+ const parsed = new URL(url, "http://localhost");
324
+ return {
325
+ pathname: parsed.pathname || "/",
326
+ search: parsed.search
327
+ };
197
328
  }
329
+ function createNavigationCommitter(deps) {
330
+ const { segmentCache, historyStack } = deps;
331
+ /**
332
+ * Update the segment cache from server-provided segment metadata.
333
+ *
334
+ * Runs whether or not `clientSegmentCache` is enabled: the tree is also
335
+ * where the mounted tree paths come from, and those cross on every
336
+ * navigation. What the flag gates is which parts get serialized — see
337
+ * `currentStateTree`.
338
+ */
339
+ function updateSegmentCache(segmentInfo) {
340
+ if (!segmentInfo || segmentInfo.length === 0) return;
341
+ const tree = buildSegmentTree(segmentInfo);
342
+ if (tree) segmentCache.set("/", tree);
343
+ }
344
+ return {
345
+ updateSegmentCache,
346
+ /**
347
+ * The X-Timber-State-Tree to send with the next RSC request.
348
+ *
349
+ * Two different things travel on one header. `segments`/`slots` are the
350
+ * caching opt-in and are omitted unless `clientSegmentCache` is on.
351
+ * `treePaths` is how the server learns which route is actually mounted,
352
+ * which stops being derivable from the address bar as soon as a modal is
353
+ * open — so it crosses regardless (TIM-1282). Undefined when there is
354
+ * nothing to say, so a no-op header never enters the `_rsc` cache key.
355
+ *
356
+ * Reads what the last *committed* navigation published, so a superseded
357
+ * navigation's destination can never end up describing the mounted tree
358
+ * (TIM-1301).
359
+ */
360
+ currentStateTree() {
361
+ const tree = segmentCache.serializeStateTree();
362
+ if (deps.clientSegmentCache()) return tree;
363
+ return tree.treePaths ? {
364
+ segments: [],
365
+ treePaths: tree.treePaths
366
+ } : void 0;
367
+ },
368
+ /**
369
+ * Prepare all navigation-owned state for a new page — without publishing
370
+ * any of it. Every code path that changes the "current page" must go
371
+ * through this function, which is what makes "forgot a field" impossible
372
+ * by construction.
373
+ *
374
+ * Returns the destination's `NavigationState`, which is a pure function of
375
+ * the URL and is what the incoming tree is *rendered* with, plus a
376
+ * `commit` thunk that performs the writes that make a page current:
377
+ * 1. Segment cache — update from server-provided segment metadata
378
+ * 2. Navigation state — pathname/search for usePathname/useSearchParams
379
+ * 3. History stack — store the payload for instant back/forward replay
380
+ *
381
+ * Nothing is written until `commit()` runs, and on a transitioned
382
+ * navigation it runs only once the transition is known to have won — see
383
+ * `renderViaTransition` and `NavigationRoot`. A navigation superseded
384
+ * while its payload was in flight never calls it, so the cache, the
385
+ * pathname the client reports and the history stack all keep describing
386
+ * the route still on screen (TIM-1301).
387
+ *
388
+ * Callers with state of their own to publish — the address bar, the
389
+ * client's record of the mounted tree — wrap this thunk rather than
390
+ * commit beside it, so there stays exactly one moment at which a page
391
+ * becomes current.
392
+ */
393
+ prepareNavigation(url, opts) {
394
+ const navState = deriveNavigationState(url);
395
+ const segmentInfo = opts.status === void 0 || opts.status < 400 ? opts.segmentInfo : [];
396
+ return {
397
+ navState,
398
+ commit() {
399
+ if (segmentInfo && segmentInfo.length > 0) updateSegmentCache(segmentInfo);
400
+ else if (segmentInfo?.length === 0 || opts.clearSegmentCacheOnEmpty) segmentCache.clear();
401
+ setNavigationState(navState);
402
+ historyStack.push(url, {
403
+ payload: opts.payload,
404
+ params: opts.params,
405
+ segmentInfo
406
+ });
407
+ }
408
+ };
409
+ }
410
+ };
411
+ }
412
+ //#endregion
413
+ //#region src/client/rsc-fetch.ts
198
414
  /**
199
- * Append a `_rsc=<id>` query parameter to the URL.
200
- * Follows Next.js's pattern — prevents CDN/browser from serving cached HTML
201
- * for RSC navigation requests and signals that this is an RSC fetch.
415
+ * RSC Fetch — handles fetching and parsing RSC Flight payloads.
416
+ *
417
+ * Extracted from router.ts to keep both files under the 500-line limit.
418
+ * This module handles:
419
+ * - Cache-busting URL generation for RSC requests
420
+ * - Building RSC request headers (Accept, X-Timber-State-Tree)
421
+ * - Extracting metadata from RSC response headers
422
+ * - Fetching and decoding RSC payloads
423
+ *
424
+ * See design/19-client-navigation.md §"RSC Payload Handling"
425
+ */
426
+ /**
427
+ * Append a `_rsc=<key>` query parameter to the URL (TIM-1268).
428
+ *
429
+ * The key is a deterministic hash of the RSC request headers, so identical
430
+ * requests share a URL and the payload becomes cacheable. It also keeps the
431
+ * document and the payload on different URLs, so a shared cache cannot
432
+ * return HTML for an RSC request (or vice versa) even where `Vary` is
433
+ * opt-in configuration rather than default behaviour — Cloudflare and
434
+ * CloudFront both. See GHSA-wfc6-r584-vfw7, design/13-security.md.
435
+ *
436
+ * The origin recomputes this key from the received headers and refuses to
437
+ * let a mismatched response be shared-cached, so a caller cannot claim
438
+ * another client's key while sending its own headers.
439
+ *
440
+ * Falls back to a random value when no key can be derived — `crypto.subtle`
441
+ * is absent in non-secure browsing contexts (plain `http://` on a LAN
442
+ * address). Dropping the parameter instead would put the payload back on
443
+ * the document URL, where a query-keyed cache that ignores `Vary` could
444
+ * store a Flight response under `/about` and serve it to an HTML
445
+ * navigation. The random parameter this feature replaced did separate those
446
+ * representations, and the fallback must not be worse than what it
447
+ * replaced. The origin cannot verify a random key, so it refuses to
448
+ * share-cache the response — which is exactly the old behaviour, where a
449
+ * unique URL was never a cache hit anyway.
202
450
  *
203
451
  * Strips any #fragment before appending — fragments are client-only and
204
452
  * fetch() discards them, so _rsc would land inside the hash and be lost.
205
453
  */
206
- function appendRscParam(url) {
454
+ async function appendRscParam(url, headers) {
207
455
  const hashIndex = url.indexOf("#");
208
456
  const urlWithoutHash = hashIndex === -1 ? url : url.slice(0, hashIndex);
209
- return `${urlWithoutHash}${urlWithoutHash.includes("?") ? "&" : "?"}_rsc=${generateCacheBustId()}`;
457
+ const key = await rscCacheKey(recordLookup(headers)) ?? randomRscCacheKey();
458
+ return `${urlWithoutHash}${urlWithoutHash.includes("?") ? "&" : "?"}${RSC_KEY_PARAM}=${key}`;
210
459
  }
211
460
  /**
212
461
  * The client's deployment ID, set at bootstrap from the runtime config.
@@ -215,6 +464,17 @@ function appendRscParam(url) {
215
464
  */
216
465
  var clientDeploymentId = null;
217
466
  /**
467
+ * The deployment base path (Vite's resolved `base`), normalized.
468
+ * Set at bootstrap from `virtual:timber-config`; `'/'` unless the app is
469
+ * deployed under a sub-path. See design/11-platform.md, TIM-1261.
470
+ *
471
+ * **The base is applied in exactly one place: at fetch time**
472
+ * (`toStaticRscUrl`). The static build writes the RSC manifest with
473
+ * root-relative keys *and* root-relative URLs, so the generated site stays
474
+ * relocatable and there is no second copy of the base to drift from this one.
475
+ */
476
+ var basePath = "/";
477
+ /**
218
478
  * When true, RSC fetches use _rsc/*.rsc file URLs instead of
219
479
  * the route URL with Accept headers. Static hosts ignore Accept
220
480
  * headers, so the client must fetch the pre-generated .rsc files
@@ -222,9 +482,10 @@ var clientDeploymentId = null;
222
482
  */
223
483
  var staticMode = false;
224
484
  /**
225
- * RSC manifest mapping unhashed → hashed URLs + params. Populated from
226
- * `window.__TIMBER_RSC_MANIFEST__` (injected into HTML during
227
- * static generation). See TIM-1254, TIM-1255.
485
+ * RSC manifest mapping unhashed → content-hashed `.rsc` URLs. Populated from
486
+ * `window.__TIMBER_RSC_MANIFEST__` (injected into HTML during static
487
+ * generation). Shape shared with the static build that writes it — see
488
+ * `shared/rsc-manifest.ts`. See TIM-1254.
228
489
  */
229
490
  var rscManifest = null;
230
491
  function getRscManifest() {
@@ -233,22 +494,35 @@ function getRscManifest() {
233
494
  return rscManifest;
234
495
  }
235
496
  /**
236
- * Convert a route URL to the corresponding _rsc/*.rsc file path.
237
- * Mirrors the naming in plugins/static-build.ts staticOutputPath.
497
+ * Resolve a route URL to the `_rsc/*.rsc` file to fetch. The naming rule is
498
+ * `shared/rsc-payload-path.ts`, shared with the build that writes the files.
238
499
  *
239
500
  * When an RSC manifest is available (hashed filenames from TIM-1254),
240
501
  * the manifest is consulted to resolve to the hashed path.
241
502
  *
242
503
  * / → /_rsc/index.rsc (or /_rsc/index-B7YxEKdN.rsc with manifest)
243
- * /about → /_rsc/about.rsc (or /_rsc/about-C8ZzFLfO.rsc with manifest)
244
- * /blog/hello → /_rsc/blog/hello.rsc
504
+ * /about → /_rsc/about/index.rsc (or /_rsc/about/index-C8ZzFLfO.rsc)
505
+ * /blog/hello → /_rsc/blog/hello/index.rsc
506
+ *
507
+ * The manifest is written and read in route space (no base prefix), so the
508
+ * base is applied here — once, on the way out. See client/base-path.ts.
245
509
  */
246
510
  function toStaticRscUrl(url) {
247
511
  const unhashed = toUnhashedRscUrl(url);
248
- return manifestLookup(unhashed) ?? unhashed;
512
+ const hashedUrl = manifestLookup(unhashed);
513
+ return {
514
+ url: withBasePath(basePath, hashedUrl ?? unhashed),
515
+ hashed: hashedUrl !== null
516
+ };
249
517
  }
250
518
  /**
251
- * Compute the unhashed _rsc/*.rsc URL for a route path.
519
+ * Compute the unhashed `_rsc/*.rsc` path for a route URL.
520
+ *
521
+ * Returns a **route-space** path: the deployment base is stripped from the
522
+ * incoming URL and is NOT re-applied, because this value doubles as the RSC
523
+ * manifest key, and the manifest is base-less. `toStaticRscUrl` applies the
524
+ * base to whatever is actually fetched.
525
+ *
252
526
  * @internal Exported for testing.
253
527
  */
254
528
  function toUnhashedRscUrl(url) {
@@ -257,9 +531,9 @@ function toUnhashedRscUrl(url) {
257
531
  const hashEnd = hashIndex === -1 ? url.length : hashIndex;
258
532
  const queryEnd = queryIndex === -1 ? url.length : queryIndex;
259
533
  const end = Math.min(hashEnd, queryEnd);
260
- let pathname = url.slice(0, end);
534
+ let pathname = stripBasePath(basePath, url.slice(0, end));
261
535
  if (pathname.length > 1 && pathname.endsWith("/")) pathname = pathname.slice(0, -1);
262
- return `/_rsc${pathname === "/" ? "/index" : pathname}.rsc`;
536
+ return rscPayloadPath(pathname);
263
537
  }
264
538
  /**
265
539
  * Look up a key in the RSC manifest, falling back to percent-decoded
@@ -281,12 +555,6 @@ function manifestEntry(key) {
281
555
  function manifestLookup(key) {
282
556
  return manifestEntry(key)?.url ?? null;
283
557
  }
284
- /**
285
- * Look up inlined route params for a route from the manifest. TIM-1255.
286
- */
287
- function manifestParams(url) {
288
- return manifestEntry(toUnhashedRscUrl(url))?.params ?? null;
289
- }
290
558
  /** Header name used by the server to signal a version skew reload. */
291
559
  var RELOAD_HEADER = "X-Timber-Reload";
292
560
  /** Header name for the client's deployment ID. */
@@ -298,6 +566,17 @@ var DEPLOYMENT_ID_HEADER = "X-Timber-Deployment-Id";
298
566
  function checkReloadSignal(response) {
299
567
  return response.headers.get(RELOAD_HEADER) === "1";
300
568
  }
569
+ /**
570
+ * Build the headers for an RSC payload request.
571
+ *
572
+ * **Every header added here is hashed into the payload URL** as the `_rsc`
573
+ * cache key (see `rscCacheKey`), and URLs are logged by proxies, CDNs, and
574
+ * origin access logs. So: no credentials, no session identifiers, no
575
+ * user-supplied content. `fnv1aHash` is a cache-key hash, not a KDF — a
576
+ * low-entropy secret would be recoverable from the URL by brute force.
577
+ * Authentication already travels on cookies, which are not part of this
578
+ * object and never enter the key.
579
+ */
301
580
  function buildRscHeaders(stateTree, currentUrl) {
302
581
  const headers = { Accept: RSC_CONTENT_TYPE };
303
582
  if (stateTree) headers["X-Timber-State-Tree"] = JSON.stringify(stateTree);
@@ -350,22 +629,6 @@ function extractSkippedSegments(response) {
350
629
  }
351
630
  }
352
631
  /**
353
- * Extract route params from the X-Timber-Params response header.
354
- * Returns null if the header is missing or malformed.
355
- *
356
- * Used to populate useSegmentParams() after client-side navigation.
357
- */
358
- function extractParams(response) {
359
- const header = response.headers.get("X-Timber-Params");
360
- if (!header) return null;
361
- try {
362
- return JSON.parse(header);
363
- } catch {
364
- warnMalformedHeader("X-Timber-Params", header);
365
- return null;
366
- }
367
- }
368
- /**
369
632
  * Thrown when an RSC payload response contains X-Timber-Redirect header.
370
633
  * Caught in navigate() to trigger a soft router navigation to the redirect target.
371
634
  */
@@ -427,9 +690,10 @@ var NonRscResponse = class extends Error {
427
690
  * a `done` promise that resolves when the last chunk is read (or
428
691
  * rejects if the stream errors).
429
692
  *
430
- * Used to keep React transitions open for the full RSC stream
431
- * duration — createFromFetch's thenable resolves on shell arrival,
432
- * but we need stream completion for useOptimistic pending state.
693
+ * createFromFetch's thenable resolves on shell arrival, but callers need to
694
+ * know when the stream is fully decoded: that is what `navigateTransition`'s
695
+ * returned promise means, and what the router's pending store, the Navigation
696
+ * API deferred and `<Link>`'s `isPending` are all timed against.
433
697
  */
434
698
  function trackStreamCompletion(body) {
435
699
  let resolveDone;
@@ -467,9 +731,11 @@ function trackStreamCompletion(body) {
467
731
  * Otherwise, the raw response text is returned (test mode).
468
732
  */
469
733
  async function fetchRscPayload(url, deps, stateTree, currentUrl, signal) {
470
- const fetchTarget = staticMode ? toStaticRscUrl(url) : url;
471
- const rscUrl = staticMode && fetchTarget !== toUnhashedRscUrl(url) ? fetchTarget : appendRscParam(fetchTarget);
472
- const headers = buildRscHeaders(staticMode ? void 0 : stateTree, currentUrl);
734
+ const staticTarget = staticMode ? toStaticRscUrl(url) : null;
735
+ const fetchTarget = staticTarget ? staticTarget.url : url;
736
+ const headers = staticMode ? buildRscHeaders(void 0, void 0) : buildRscHeaders(stateTree, currentUrl);
737
+ const rscUrl = staticTarget?.hashed ? fetchTarget : await appendRscParam(fetchTarget, headers);
738
+ signal?.throwIfAborted();
473
739
  if (deps.decodeRsc) {
474
740
  const fetchPromise = deps.fetch(rscUrl, {
475
741
  headers,
@@ -477,8 +743,8 @@ async function fetchRscPayload(url, deps, stateTree, currentUrl, signal) {
477
743
  signal
478
744
  });
479
745
  let segmentInfo = null;
480
- let params = null;
481
746
  let skippedSegments = null;
747
+ let status = 200;
482
748
  let streamDone = Promise.resolve();
483
749
  const wrappedPromise = fetchPromise.then((response) => {
484
750
  if (checkReloadSignal(response)) throw new VersionSkewError();
@@ -499,8 +765,8 @@ async function fetchRscPayload(url, deps, stateTree, currentUrl, signal) {
499
765
  }
500
766
  }
501
767
  segmentInfo = extractSegmentInfo(response);
502
- params = extractParams(response);
503
768
  skippedSegments = extractSkippedSegments(response);
769
+ status = response.status;
504
770
  if (response.body) {
505
771
  const tracked = trackStreamCompletion(response.body);
506
772
  streamDone = tracked.done;
@@ -513,18 +779,21 @@ async function fetchRscPayload(url, deps, stateTree, currentUrl, signal) {
513
779
  return response;
514
780
  });
515
781
  await wrappedPromise;
516
- if (staticMode && !params) params = manifestParams(url);
517
- const payload = deps.decodeRsc(wrappedPromise);
782
+ const root = deps.decodeRsc(wrappedPromise);
783
+ const { tree: payload, params } = splitPayloadRoot(root);
518
784
  const payloadError = new Promise((_, reject) => {
519
- Promise.resolve(payload).then(() => {}, reject);
785
+ Promise.resolve(root).then(() => {}, reject);
520
786
  });
521
787
  payloadError.catch(() => {});
788
+ const decodePromise = Promise.race([streamDone, payloadError]);
789
+ decodePromise.catch(() => {});
522
790
  return {
523
791
  payload,
524
- decodePromise: Promise.race([streamDone, payloadError]),
525
- segmentInfo,
526
792
  params,
527
- skippedSegments
793
+ decodePromise,
794
+ segmentInfo,
795
+ skippedSegments,
796
+ status
528
797
  };
529
798
  }
530
799
  const response = await deps.fetch(rscUrl, {
@@ -550,18 +819,192 @@ async function fetchRscPayload(url, deps, stateTree, currentUrl, signal) {
550
819
  throw new NonRscResponse(url);
551
820
  }
552
821
  }
553
- let fallbackParams = extractParams(response);
554
- if (staticMode && !fallbackParams) fallbackParams = manifestParams(url);
555
822
  return {
556
823
  payload: await response.text(),
824
+ params: readPublishedParams(void 0),
557
825
  decodePromise: null,
558
826
  segmentInfo: extractSegmentInfo(response),
559
- params: fallbackParams,
560
- skippedSegments: extractSkippedSegments(response)
827
+ skippedSegments: extractSkippedSegments(response),
828
+ status: response.status
561
829
  };
562
830
  }
563
831
  //#endregion
564
- //#region src/client/router.ts
832
+ //#region src/client/router-skew.ts
833
+ /**
834
+ * Router Skew Recording — turning a rejection into "this bundle is superseded".
835
+ *
836
+ * Detection lives in `stale-client.ts`; this is the one place the router (and
837
+ * anything else holding an RSC rejection) turns an error into that verdict.
838
+ * Recovery is not here: every path that gives up on client-side navigation —
839
+ * skew or not — leaves through `createSpaExits()` in `router-effects.ts`.
840
+ *
841
+ * See design/33-version-skew.md §"Recovery on the next navigation"
842
+ */
843
+ /**
844
+ * Record a version skew if `error` is one, and report whether it was.
845
+ *
846
+ * `VersionSkewError` is the origin saying so outright; the reactive shapes are
847
+ * inferred from the error text and are a no-op in dev.
848
+ *
849
+ * Closes over nothing — anywhere an RSC payload can fail needs it, including
850
+ * places that have no router instance to hand.
851
+ */
852
+ function recordSkew(error) {
853
+ if (error instanceof VersionSkewError) {
854
+ markClientStale();
855
+ return true;
856
+ }
857
+ return markStaleFromError(error);
858
+ }
859
+ //#endregion
860
+ //#region src/client/router-effects.ts
861
+ /**
862
+ * Router Effects — full-page navigation and post-paint scroll.
863
+ *
864
+ * The two things the router does *to the document* rather than to its own
865
+ * state: leaving the SPA entirely, and moving the scroll position once React
866
+ * has committed. Extracted from `router.ts` to keep that file focused on
867
+ * navigation state (see design/18-build-system.md §"No file >500 lines").
868
+ *
869
+ * See design/19-client-navigation.md §"Scroll Restoration"
870
+ */
871
+ function createScrollEffects(deps) {
872
+ /** Run a callback after the next paint (after React commit). */
873
+ function afterPaint(callback) {
874
+ if (deps.afterPaint) deps.afterPaint(callback);
875
+ else callback();
876
+ }
877
+ function restoreScrollAfterPaint(scrollY) {
878
+ afterPaint(() => {
879
+ deps.scrollTo(0, scrollY);
880
+ window.dispatchEvent(new Event("timber:scroll-restored"));
881
+ });
882
+ }
883
+ function scrollToHashAfterPaint(hash) {
884
+ afterPaint(() => {
885
+ if (deps.scrollToHash?.(hash) !== true) deps.scrollTo(0, 0);
886
+ window.dispatchEvent(new Event("timber:scroll-restored"));
887
+ });
888
+ }
889
+ return {
890
+ restoreScrollAfterPaint,
891
+ scrollToHashAfterPaint
892
+ };
893
+ }
894
+ /**
895
+ * Leave the SPA and never come back.
896
+ *
897
+ * Every router path that gives up on client-side navigation does the same
898
+ * three things: flag the hard navigation so the Navigation API and React stop
899
+ * acting on a dying document, perform the document load, then block forever so
900
+ * the caller cannot carry on rendering into it. The returned promise is
901
+ * deliberately unresolvable — `location` assignment does not stop this turn of
902
+ * the event loop.
903
+ *
904
+ * Callers: server error, non-RSC response, and every version skew path
905
+ * ([33-version-skew.md](../../../../design/33-version-skew.md)).
906
+ */
907
+ function leaveSpa(url, fromUrl) {
908
+ setHardNavigating(true);
909
+ hardNavigate(url, fromUrl);
910
+ return new Promise(() => {});
911
+ }
912
+ /**
913
+ * The two ownership-aware ways out of the SPA.
914
+ *
915
+ * Both wrap {@link leaveSpa} with the question "is this navigation still the
916
+ * one the user is waiting for?" — asked before leaving on a failure, answered
917
+ * by force on a path that decided before any navigation began. They live
918
+ * together because every caller of `leaveSpa()` needs one or the other, and
919
+ * an unguarded call is the bug (TIM-1275, TIM-1276).
920
+ */
921
+ function createSpaExits({ currentNavAbort, supersede }) {
922
+ async function leaveSpaIfOwned(navAbort, url, fromUrl) {
923
+ if (currentNavAbort() !== navAbort) return;
924
+ await leaveSpa(url, fromUrl);
925
+ }
926
+ function leaveSpaSuperseding(url, fromUrl) {
927
+ supersede();
928
+ return leaveSpa(url, fromUrl);
929
+ }
930
+ return {
931
+ leaveSpaIfOwned,
932
+ leaveSpaSuperseding
933
+ };
934
+ }
935
+ /**
936
+ * Answer the failures an RSC fetch can end on, or report that this one is not
937
+ * ours to answer.
938
+ *
939
+ * Returns `true` when the error was handled; the caller rethrows on `false`.
940
+ * Handling a failure by leaving the SPA never resolves at all — the document
941
+ * is going away and the caller must not carry on rendering into it.
942
+ *
943
+ * There is exactly one family of these and one response to it, so it lives in
944
+ * one function rather than once per fetch path. `navigate()`, `refresh()` and
945
+ * an uncached traversal all reach it: the latter two used to rethrow instead,
946
+ * and since nobody awaits a traversal that meant pressing Back on a 500 left
947
+ * the user on the old document with an unhandled rejection (TIM-1277).
948
+ *
949
+ * Order matters in exactly one way: `recordSkew()` ends in error-*text*
950
+ * matching, so it goes last and can never pre-empt a framework control-flow
951
+ * signal whose message happens to read like a stale bundle. The class checks
952
+ * above it are mutually exclusive, so their order is free.
953
+ *
954
+ * Must be called from inside the `runNavigation()` callback — see
955
+ * {@link SpaExits.leaveSpaIfOwned}.
956
+ */
957
+ function createNavigationRecovery({ currentNavAbort, leaveSpaIfOwned, navigate }) {
958
+ return async function recoverFromNavigationError(error, navAbort, url, fromUrl) {
959
+ if (error instanceof RedirectError) {
960
+ if (currentNavAbort() !== navAbort) return true;
961
+ await navigate(error.redirectUrl);
962
+ return true;
963
+ }
964
+ if (error instanceof ServerErrorResponse || error instanceof NonRscResponse || recordSkew(error)) {
965
+ await leaveSpaIfOwned(navAbort, url, fromUrl);
966
+ return true;
967
+ }
968
+ return false;
969
+ };
970
+ }
971
+ /**
972
+ * Perform the full document load.
973
+ *
974
+ * `fromUrl` is the URL the navigation departed from, passed explicitly
975
+ * because the address bar may have already been updated.
976
+ *
977
+ * When the target differs from the departure point only by #fragment,
978
+ * assigning `href` is a hash change, not a load — so assign first (to update
979
+ * the address bar) and then reload (TIM-1235).
980
+ */
981
+ function hardNavigate(url, fromUrl) {
982
+ const current = new URL(fromUrl, window.location.origin);
983
+ const target = new URL(url, window.location.origin);
984
+ if (target.pathname === current.pathname && target.search === current.search) {
985
+ window.location.href = url;
986
+ window.location.reload();
987
+ } else window.location.href = url;
988
+ }
989
+ //#endregion
990
+ //#region src/client/router-lifecycle.ts
991
+ /**
992
+ * Navigation Lifecycle — who owns the router, and when a fetch may be cut.
993
+ *
994
+ * One navigation at a time owns the router. This module holds that ownership
995
+ * (`currentNavAbort`), the rule for taking it (`createNavAbort` supersedes),
996
+ * the wrapper every navigation runs inside (`runNavigation`), the set of
997
+ * navigations whose trees are on screen and therefore unabortable
998
+ * (`handedOffNavAborts`), and the pending store that `TopLoader` and
999
+ * `usePendingNavigation()` subscribe to.
1000
+ *
1001
+ * These belong together because every one of them is a read or a write of
1002
+ * "which navigation is the user waiting for?" — the question `router.ts` used
1003
+ * to answer in four places. Split out of `router.ts` for the same reason
1004
+ * `router-effects.ts` was (design/18-build-system.md §"No file >500 lines").
1005
+ *
1006
+ * See design/19-client-navigation.md §"How Pending State Works".
1007
+ */
565
1008
  /**
566
1009
  * Check if an error is an abort error (connection closed / fetch aborted).
567
1010
  * Browsers throw DOMException with name 'AbortError' when a fetch is aborted.
@@ -571,22 +1014,82 @@ function isAbortError(error) {
571
1014
  if (error instanceof Error && error.name === "AbortError") return true;
572
1015
  return false;
573
1016
  }
574
- function createRouter(deps) {
575
- const segmentCache = new SegmentCache();
576
- const prefetchCache = new PrefetchCache();
577
- const historyStack = new HistoryStack();
1017
+ function createNavigationLifecycle(deps) {
1018
+ let currentNavAbort = null;
578
1019
  let routerPhase = { phase: "idle" };
579
1020
  const pendingListeners = /* @__PURE__ */ new Set();
580
- let currentNavAbort = null;
1021
+ /**
1022
+ * The controllers of navigations whose trees have been HANDED TO REACT.
1023
+ *
1024
+ * Its response may still be streaming: a destination reveals as soon as
1025
+ * React can render it, so the tree on screen routinely has Suspense
1026
+ * boundaries still waiting on later Flight rows. Aborting that response
1027
+ * rejects those rows, and the rejection surfaces through the tree into
1028
+ * whatever error boundary the app has — replacing the page the user is
1029
+ * looking at with an error state, while the successor navigation is still
1030
+ * in flight (codex on #1004).
1031
+ *
1032
+ * So such a navigation's stream is allowed to finish. It is finite and
1033
+ * already in flight; the alternative is a visible error on the page being
1034
+ * departed from. Cancelling is still correct for a navigation whose payload
1035
+ * never reached React at all, which is the case the abort was written for.
1036
+ *
1037
+ * **Handed over, not committed.** This is set when the tree is given to
1038
+ * React, not when React commits it. Marking on commit is a whole React
1039
+ * commit phase too late: the notification would be `NavigationRoot`'s
1040
+ * layout effect, and React runs *descendant* layout effects first — so a
1041
+ * destination that navigates from its own mount layout effect (a redirect
1042
+ * guard) runs before the mark and its just-committed stream gets torn out
1043
+ * from under it. `tests/navigation-supersede.test.ts` already pins that
1044
+ * ordering, and there is no earlier hook short of an extra sibling fiber,
1045
+ * which would shift every `useId` in the payload (see `server/ssr-wrappers`).
1046
+ *
1047
+ * Handing over is the right moment on its own terms, not merely a safe
1048
+ * over-approximation: from the instant React holds the tree it may commit
1049
+ * it without asking, so there is no later point at which "not on screen"
1050
+ * is still knowable from out here. The residue is that a navigation
1051
+ * superseded in the window between handover and commit keeps streaming a
1052
+ * payload nobody sees — bandwidth on a finite response, against a visible
1053
+ * error page the other way (codex on #1004, second round).
1054
+ *
1055
+ * A SET, not a single slot. Between a successor's handover and its commit
1056
+ * React is still showing the previous tree — that is what the transition
1057
+ * buys — so the previous navigation's stream is still feeding the screen and
1058
+ * must stay unabortable. Entries are dropped when a later tree actually
1059
+ * commits (`forgetOlderHandoffs`), which is the moment the trees they fed
1060
+ * are gone. A single slot made a legitimate handover steal protection from
1061
+ * a stream still on screen, and let a superseded navigation overwrite the
1062
+ * winner's entry outright.
1063
+ */
1064
+ const handedOffNavAborts = /* @__PURE__ */ new Set();
1065
+ /**
1066
+ * Cancel a navigation's RSC fetch — unless its tree is the one on screen.
1067
+ *
1068
+ * THE only place a navigation controller is aborted. Every path that gives
1069
+ * up on a navigation calls this, so the "is this tree displayed?" question
1070
+ * is asked once rather than at each site (the same reason `createSpaExits`
1071
+ * exists for the SPA exits). A new abort path is a call to this, not a copy
1072
+ * of `controller.abort()`.
1073
+ *
1074
+ * See `handedOffNavAborts` for why such a navigation keeps its stream.
1075
+ */
1076
+ function abortUnlessHandedOff(controller) {
1077
+ if (handedOffNavAborts.has(controller)) return;
1078
+ controller.abort();
1079
+ }
581
1080
  /**
582
1081
  * Create a new AbortController for a navigation, superseding any
583
1082
  * previous in-flight navigation. Optionally links to an external
584
1083
  * signal (e.g., from the Navigation API's NavigateEvent.signal).
585
1084
  *
586
1085
  * Superseding is one operation with three parts:
587
- * 1. Abort the previous navigation's fetch.
1086
+ * 1. Abort the previous navigation's fetch — UNLESS its tree is the one on
1087
+ * screen, in which case tearing the stream down would error the page the
1088
+ * user is currently looking at. See `handedOffNavAborts`.
588
1089
  * 2. Invalidate its render transition so a response that already
589
1090
  * arrived can't commit a stale tree (NavigationRoot's transId guard).
1091
+ * This happens either way, so skipping the abort cannot let a stale tree
1092
+ * displace the successor.
590
1093
  * 3. Resolve its Navigation API deferred — the superseded navigation's
591
1094
  * finally block is staleness-guarded (see TIM-1034) and no longer
592
1095
  * cleans up after itself, so the browser's native loading state for
@@ -594,14 +1097,14 @@ function createRouter(deps) {
594
1097
  */
595
1098
  function createNavAbort(externalSignal) {
596
1099
  if (currentNavAbort) {
597
- currentNavAbort.abort();
1100
+ abortUnlessHandedOff(currentNavAbort);
598
1101
  supersedeNavigationTransitions();
599
1102
  deps.completeRouterNavigation?.();
600
1103
  }
601
1104
  const controller = new AbortController();
602
1105
  currentNavAbort = controller;
603
- if (externalSignal) if (externalSignal.aborted) controller.abort();
604
- else externalSignal.addEventListener("abort", () => controller.abort(), { once: true });
1106
+ if (externalSignal) if (externalSignal.aborted) abortUnlessHandedOff(controller);
1107
+ else externalSignal.addEventListener("abort", () => abortUnlessHandedOff(controller), { once: true });
605
1108
  return controller;
606
1109
  }
607
1110
  function setPending(value, url) {
@@ -613,38 +1116,6 @@ function createRouter(deps) {
613
1116
  routerPhase = next;
614
1117
  for (const listener of pendingListeners) listener(value);
615
1118
  }
616
- /** Update the segment cache from server-provided segment metadata. */
617
- function updateSegmentCache(segmentInfo) {
618
- if (!deps.clientSegmentCache) return;
619
- if (!segmentInfo || segmentInfo.length === 0) return;
620
- const tree = buildSegmentTree(segmentInfo);
621
- if (tree) segmentCache.set("/", tree);
622
- }
623
- /** Render a decoded RSC payload into the DOM if a renderer is available. */
624
- function renderPayload(payload, navState) {
625
- if (deps.renderRoot) deps.renderRoot(payload, navState);
626
- }
627
- /**
628
- * Atomically update all navigation-owned state for a new page. Every
629
- * code path that changes the "current page" must go through this
630
- * function — making "forgot a field" impossible by construction.
631
- *
632
- * The three operations:
633
- * 1. Segment cache — update from server-provided segment metadata
634
- * 2. Navigation state — params + pathname for useSegmentParams/usePathname
635
- * 3. History stack — store the payload for instant back/forward replay
636
- */
637
- function commitNavigation(url, opts) {
638
- if (opts.segmentInfo && opts.segmentInfo.length > 0) updateSegmentCache(opts.segmentInfo);
639
- else if (opts.clearSegmentCacheOnEmpty) segmentCache.clear();
640
- const navState = updateNavigationState(opts.params, url);
641
- historyStack.push(url, {
642
- payload: opts.payload,
643
- params: navState.params,
644
- segmentInfo: opts.segmentInfo
645
- });
646
- return navState;
647
- }
648
1119
  /**
649
1120
  * Wrap a navigation in the standard abort/pending/cleanup lifecycle.
650
1121
  * Consolidates the createNavAbort + setPending + staleness-guarded
@@ -668,6 +1139,117 @@ function createRouter(deps) {
668
1139
  }
669
1140
  }
670
1141
  }
1142
+ return {
1143
+ currentNavAbort: () => currentNavAbort,
1144
+ createNavAbort,
1145
+ runNavigation,
1146
+ markHandedOff(owner) {
1147
+ if (currentNavAbort !== owner) return;
1148
+ handedOffNavAborts.add(owner);
1149
+ },
1150
+ forgetOlderHandoffs(owner) {
1151
+ for (const controller of handedOffNavAborts) if (controller !== owner) handedOffNavAborts.delete(controller);
1152
+ },
1153
+ isPending: () => routerPhase.phase === "navigating",
1154
+ getPendingUrl: () => routerPhase.phase === "navigating" ? routerPhase.targetUrl : null,
1155
+ onPendingChange(listener) {
1156
+ pendingListeners.add(listener);
1157
+ return () => pendingListeners.delete(listener);
1158
+ }
1159
+ };
1160
+ }
1161
+ //#endregion
1162
+ //#region src/client/router-pipeline.ts
1163
+ /**
1164
+ * Navigation Pipeline — URL in, tree in React's hands.
1165
+ *
1166
+ * Everything between "the user asked for this URL" and "React is holding the
1167
+ * destination's tree": consult the prefetch cache or fetch, prepare the commit
1168
+ * that makes the page current, and hand the result over inside a transition.
1169
+ *
1170
+ * `router.ts` keeps the *operations* — `navigate`, `refresh`, `handlePopState`,
1171
+ * `prefetch`, `applyRevalidation` — and each of them is a call into here. The
1172
+ * split is the same one `router-effects.ts` and `router-lifecycle.ts` made
1173
+ * (design/18-build-system.md §"No file >500 lines").
1174
+ *
1175
+ * See design/19-client-navigation.md §"Partial Rendering" and §"Prefetching".
1176
+ */
1177
+ /**
1178
+ * Normalize a departing URL to the pathname the server is told about.
1179
+ *
1180
+ * This is the value sent as `X-Timber-URL` *and* the source half of the
1181
+ * prefetch cache key — one function so a prefetch and the click that consumes
1182
+ * it can never disagree about what "where we came from" means (TIM-1265). The
1183
+ * query is dropped deliberately: it would land in the `_rsc` digest, which is
1184
+ * logged (TIM-1268), and nothing downstream reads it — slot-skip resolution
1185
+ * and interception scope both compare pathnames.
1186
+ *
1187
+ * Absolute inputs parse against the base and ignore it, so one call handles
1188
+ * both `location.href` and a bare pathname.
1189
+ */
1190
+ function departingPathname(raw) {
1191
+ return new URL(raw, "http://localhost").pathname;
1192
+ }
1193
+ /**
1194
+ * The prefetch cache key for a destination.
1195
+ *
1196
+ * Both halves of the cache are keyed through here — the hover that writes an
1197
+ * entry (`router.ts`'s `prefetch()`) and the click that consumes it
1198
+ * (`performNavigationFetch` below) — so the two cannot disagree about what
1199
+ * identifies a payload. That is the whole point of the key: `from` scopes it
1200
+ * to the page the link was on (TIM-1265) and `scope` to the tree that was
1201
+ * mounted (TIM-1282), and an entry written under one reading of either would
1202
+ * simply never be found under the other, silently costing every prefetch.
1203
+ *
1204
+ * `departingPathname` is applied here rather than by callers for the same
1205
+ * reason: it is also what goes out as `X-Timber-URL`, so the key and the
1206
+ * request describe the same departure point.
1207
+ */
1208
+ function prefetchKeyFor(url, departingUrl, stateTree) {
1209
+ return {
1210
+ url,
1211
+ from: departingPathname(departingUrl),
1212
+ scope: prefetchScopeOf(stateTree)
1213
+ };
1214
+ }
1215
+ function isPartialNavigation(skippedSegments) {
1216
+ return skippedSegments != null && skippedSegments.length > 0;
1217
+ }
1218
+ /**
1219
+ * Build a segment updates map for partial navigation. Identifies the
1220
+ * first non-skipped segment and maps it to the payload content.
1221
+ */
1222
+ function buildSegmentUpdates(result) {
1223
+ const skipped = new Set(result.skippedSegments);
1224
+ const segmentInfo = result.segmentInfo;
1225
+ const updates = /* @__PURE__ */ new Map();
1226
+ if (segmentInfo) {
1227
+ for (const info of segmentInfo) if (!skipped.has(info.segmentId ?? info.path)) {
1228
+ updates.set(info.segmentId ?? info.path, result.payload);
1229
+ break;
1230
+ }
1231
+ }
1232
+ return updates;
1233
+ }
1234
+ /**
1235
+ * Inspect a decode rejection for version skew before handing the promise on,
1236
+ * without changing what the consumer sees.
1237
+ *
1238
+ * The decode rejects independently of the fetch, and not every consumer
1239
+ * awaits it — NavigationRoot drops it outright when the transition has been
1240
+ * superseded. A stale client reference arriving that way is still evidence
1241
+ * the bundle is superseded, so record it here where every consumer is fed
1242
+ * from, rather than at each place that might drop it (TIM-1275).
1243
+ */
1244
+ function observeSkew(decodePromise) {
1245
+ decodePromise?.catch((error) => void recordSkew(error));
1246
+ return decodePromise;
1247
+ }
1248
+ function createNavigationPipeline({ deps, prefetchCache, currentStateTree, prepareNavigation, markHandedOff, forgetOlderHandoffs }) {
1249
+ /** Render a decoded RSC payload into the DOM if a renderer is available. */
1250
+ function renderPayload(payload, navState, params) {
1251
+ if (deps.renderRoot) deps.renderRoot(payload, navState, params);
1252
+ }
671
1253
  /**
672
1254
  * Resolve thenable payloads in the test/fallback path (no navigateTransition).
673
1255
  * In production, React handles thenables from createFromFetch directly via
@@ -677,141 +1259,76 @@ function createRouter(deps) {
677
1259
  if (!deps.navigateTransition && payload != null && typeof payload === "object" && "then" in payload) return await payload;
678
1260
  return payload;
679
1261
  }
680
- function isPartialNavigation(skippedSegments) {
681
- return skippedSegments != null && skippedSegments.length > 0;
682
- }
683
- /**
684
- * Build a segment updates map for partial navigation. Identifies the
685
- * first non-skipped segment and maps it to the payload content.
686
- */
687
- function buildSegmentUpdates(result) {
688
- const skipped = new Set(result.skippedSegments);
689
- const segmentInfo = result.segmentInfo;
690
- const updates = /* @__PURE__ */ new Map();
691
- if (segmentInfo) {
692
- for (const info of segmentInfo) if (!skipped.has(info.segmentId ?? info.path)) {
693
- updates.set(info.segmentId ?? info.path, result.payload);
694
- break;
695
- }
696
- }
697
- return updates;
698
- }
699
- /**
700
- * Update navigation state (params + pathname) for the next render.
701
- *
702
- * Sets the module-level fallback (for tests and SSR) and the
703
- * globalThis bridge, then returns the NavigationState so callers
704
- * can pass it explicitly to renderRoot/wrapPayload — eliminating
705
- * temporal coupling with getNavigationState().
706
- */
707
- function updateNavigationState(params, url) {
708
- const resolvedParams = params ?? {};
709
- setCurrentParams(resolvedParams);
710
- const parsed = new URL(url, "http://localhost");
711
- const navState = {
712
- params: resolvedParams,
713
- pathname: parsed.pathname || "/",
714
- search: parsed.search
715
- };
716
- setNavigationState(navState);
717
- return navState;
718
- }
719
1262
  /**
720
1263
  * Render a payload via navigateTransition (production) or renderRoot (tests).
721
- * The perform callback should fetch data, call commitNavigation, and return
722
- * the FetchResult plus the NavigationState.
1264
+ * The perform callback should fetch data, prepare the commit, and return the
1265
+ * FetchResult plus the NavigationState and the commit thunk.
723
1266
  *
724
- * State management (segmentCache, navState, historyStack) is handled by
725
- * commitNavigation inside perform — this function only handles rendering.
1267
+ * The thunk is handed *through* to NavigationRoot rather than called here:
1268
+ * state management (segmentCache, navState, historyStack) has to happen on
1269
+ * the far side of the supersession check, which only NavigationRoot can
1270
+ * make (TIM-1301). The fallback path below has no transition to be
1271
+ * superseded by, so it commits directly.
726
1272
  */
727
- async function renderViaTransition(url, perform) {
1273
+ async function renderViaTransition(url, owner, perform) {
1274
+ const handOff = () => markHandedOff(owner);
1275
+ const commitAndForget = (commit) => () => {
1276
+ forgetOlderHandoffs(owner);
1277
+ commit();
1278
+ };
728
1279
  if (deps.navigateTransition) {
729
1280
  await deps.navigateTransition(url, async (wrapPayload) => {
730
1281
  const result = await perform();
1282
+ const params = await result.params;
731
1283
  if (isPartialNavigation(result.skippedSegments)) {
732
1284
  const segmentUpdates = buildSegmentUpdates(result);
1285
+ const element = wrapPayload(deps._getCurrentPayload?.() ?? result.payload, result.navState, params, segmentUpdates);
1286
+ handOff();
733
1287
  return {
734
- element: wrapPayload(deps._getCurrentPayload?.() ?? result.payload, result.navState, segmentUpdates),
735
- decodePromise: result.decodePromise
1288
+ element,
1289
+ decodePromise: observeSkew(result.decodePromise),
1290
+ commit: commitAndForget(result.commit)
736
1291
  };
737
1292
  }
1293
+ const element = wrapPayload(result.payload, result.navState, params);
1294
+ handOff();
738
1295
  return {
739
- element: wrapPayload(result.payload, result.navState),
740
- decodePromise: result.decodePromise
1296
+ element,
1297
+ decodePromise: observeSkew(result.decodePromise),
1298
+ commit: commitAndForget(result.commit)
741
1299
  };
742
1300
  });
743
1301
  return;
744
1302
  }
745
1303
  const result = await perform();
746
- if (!isPartialNavigation(result.skippedSegments)) renderPayload(result.payload, result.navState);
747
- }
748
- /**
749
- * Perform a hard navigation to a URL. When the target is same-document
750
- * (same pathname+search, different hash), `location.href = url` is a
751
- * hash change — no reload. Use `location.reload()` instead (TIM-1235).
752
- *
753
- * `fromUrl` is the departing URL captured before any pushState or
754
- * Navigation API commit — deps.getCurrentUrl() is unreliable here
755
- * because the URL may have already been updated.
756
- */
757
- function hardNavigate(url, fromUrl) {
758
- const current = new URL(fromUrl, window.location.origin);
759
- const target = new URL(url, window.location.origin);
760
- if (target.pathname === current.pathname && target.search === current.search) {
761
- window.location.href = url;
762
- window.location.reload();
763
- } else window.location.href = url;
764
- }
765
- /** Run a callback after the next paint (after React commit). */
766
- function afterPaint(callback) {
767
- if (deps.afterPaint) deps.afterPaint(callback);
768
- else callback();
769
- }
770
- /**
771
- * Schedule scroll restoration after the next paint and fire the
772
- * scroll-restored event. Used by navigate, popstate, and refresh.
773
- */
774
- function restoreScrollAfterPaint(scrollY) {
775
- afterPaint(() => {
776
- deps.scrollTo(0, scrollY);
777
- window.dispatchEvent(new Event("timber:scroll-restored"));
778
- });
779
- }
780
- /**
781
- * Scroll to the element matching the URL #fragment after paint, falling
782
- * back to scroll-to-top when no element matches (matching full-page-load
783
- * behavior for an unknown fragment). Used by forward navigation to a
784
- * hash-bearing URL (TIM-1035).
785
- */
786
- function scrollToHashAfterPaint(hash) {
787
- afterPaint(() => {
788
- if (deps.scrollToHash?.(hash) !== true) deps.scrollTo(0, 0);
789
- window.dispatchEvent(new Event("timber:scroll-restored"));
790
- });
1304
+ handOff();
1305
+ commitAndForget(result.commit)();
1306
+ if (!isPartialNavigation(result.skippedSegments)) renderPayload(result.payload, result.navState, await result.params);
791
1307
  }
792
1308
  /**
793
1309
  * Core navigation logic shared between the transition and fallback paths.
794
1310
  * Fetches the RSC payload, updates all state, and returns the result.
795
1311
  */
796
1312
  async function performNavigationFetch(url, options) {
797
- if (prefetchCache.isNegative(url)) {
798
- prefetchCache.consume(url);
1313
+ const stateTree = currentStateTree();
1314
+ const cacheKey = prefetchKeyFor(url, options.departingUrl ?? deps.getCurrentUrl(), stateTree);
1315
+ const currentUrl = cacheKey.from;
1316
+ if (prefetchCache.isNegative(cacheKey)) {
1317
+ prefetchCache.consume(cacheKey);
799
1318
  throw new NonRscResponse(url);
800
1319
  }
801
- const prefetched = prefetchCache.consume(url);
1320
+ const prefetched = prefetchCache.consume(cacheKey);
802
1321
  let result = prefetched ? {
803
1322
  payload: prefetched.payload,
804
- decodePromise: null,
1323
+ params: prefetched.params ?? readPublishedParams(void 0),
1324
+ decodePromise: prefetched.decodePromise ?? null,
805
1325
  segmentInfo: prefetched.segmentInfo ?? null,
806
- params: prefetched.params ?? null,
807
- skippedSegments: prefetched.skippedSegments ?? null
1326
+ skippedSegments: prefetched.skippedSegments ?? null,
1327
+ status: prefetched.status ?? 200
808
1328
  } : void 0;
809
- if (result === void 0) {
810
- const stateTree = deps.clientSegmentCache ? segmentCache.serializeStateTree() : void 0;
811
- const rawDepartingUrl = options.departingUrl ?? deps.getCurrentUrl();
812
- result = await fetchRscPayload(url, deps, stateTree, rawDepartingUrl.startsWith("http") ? new URL(rawDepartingUrl).pathname : new URL(rawDepartingUrl, "http://localhost").pathname, options.signal);
813
- }
814
- if (!options.skipHistory) {
1329
+ if (result === void 0) result = await fetchRscPayload(url, deps, stateTree, currentUrl, options.signal);
1330
+ const commitHistoryUrl = () => {
1331
+ if (options.skipHistory) return;
815
1332
  const commitUrl = options.commitUrl ?? url;
816
1333
  deps.setRouterNavigating?.(true);
817
1334
  if (options.replace) deps.replaceState({
@@ -823,21 +1340,70 @@ function createRouter(deps) {
823
1340
  scrollY: 0
824
1341
  }, "", commitUrl);
825
1342
  deps.setRouterNavigating?.(false);
826
- }
1343
+ };
827
1344
  const payload = await resolveForFallback(result.payload);
1345
+ const params = await result.params;
828
1346
  const isPartial = isPartialNavigation(result.skippedSegments);
829
1347
  const hasSkippedSlots = result.segmentInfo?.some((s) => s.slot && s.skipped) ?? false;
830
- const navState = commitNavigation(url, {
1348
+ const { navState, commit } = prepareNavigation(url, {
831
1349
  payload: isPartial || hasSkippedSlots ? null : payload,
832
- params: result.params,
833
- segmentInfo: result.segmentInfo
1350
+ params,
1351
+ segmentInfo: result.segmentInfo,
1352
+ status: result.status
834
1353
  });
835
1354
  return {
836
1355
  ...result,
837
1356
  payload,
838
- navState
1357
+ params,
1358
+ navState,
1359
+ commit: () => {
1360
+ commitHistoryUrl();
1361
+ commit();
1362
+ window.dispatchEvent(new Event("timber:navigation-end"));
1363
+ }
839
1364
  };
840
1365
  }
1366
+ return {
1367
+ performNavigationFetch,
1368
+ renderViaTransition,
1369
+ renderPayload,
1370
+ resolveForFallback
1371
+ };
1372
+ }
1373
+ //#endregion
1374
+ //#region src/client/router.ts
1375
+ /**
1376
+ * Create a router instance. In production, called once at app hydration
1377
+ * with real browser APIs. In tests, called with mock dependencies.
1378
+ */
1379
+ function createRouter(deps) {
1380
+ const segmentCache = new SegmentCache();
1381
+ const prefetchCache = new PrefetchCache();
1382
+ const historyStack = new HistoryStack();
1383
+ const { prepareNavigation, currentStateTree, updateSegmentCache } = createNavigationCommitter({
1384
+ segmentCache,
1385
+ historyStack,
1386
+ clientSegmentCache: () => deps.clientSegmentCache
1387
+ });
1388
+ const { currentNavAbort, createNavAbort, runNavigation, markHandedOff, forgetOlderHandoffs, isPending, getPendingUrl, onPendingChange } = createNavigationLifecycle(deps);
1389
+ const { performNavigationFetch, renderViaTransition, renderPayload, resolveForFallback } = createNavigationPipeline({
1390
+ deps,
1391
+ prefetchCache,
1392
+ currentStateTree,
1393
+ prepareNavigation,
1394
+ markHandedOff,
1395
+ forgetOlderHandoffs
1396
+ });
1397
+ const { restoreScrollAfterPaint, scrollToHashAfterPaint } = createScrollEffects(deps);
1398
+ const { leaveSpaIfOwned, leaveSpaSuperseding } = createSpaExits({
1399
+ currentNavAbort,
1400
+ supersede: () => void createNavAbort()
1401
+ });
1402
+ const recoverFromNavigationError = createNavigationRecovery({
1403
+ currentNavAbort,
1404
+ leaveSpaIfOwned,
1405
+ navigate: (url) => navigate(url, { replace: true })
1406
+ });
841
1407
  async function navigate(url, options = {}) {
842
1408
  const scroll = options.scroll !== false;
843
1409
  const replace = options.replace === true;
@@ -853,6 +1419,7 @@ function createRouter(deps) {
853
1419
  timber: true,
854
1420
  scrollY: currentScrollY
855
1421
  }, "", deps.getCurrentUrl());
1422
+ if (isClientStale()) await leaveSpaSuperseding(url, departingUrl);
856
1423
  let effectiveSkipHistory = skipHistory;
857
1424
  await runNavigation(url, async (navAbort) => {
858
1425
  if (!effectiveSkipHistory && deps.navigationNavigate) {
@@ -862,128 +1429,127 @@ function createRouter(deps) {
862
1429
  effectiveSkipHistory = true;
863
1430
  }
864
1431
  try {
865
- await renderViaTransition(fetchUrl, () => performNavigationFetch(fetchUrl, {
1432
+ await renderViaTransition(fetchUrl, navAbort, () => performNavigationFetch(fetchUrl, {
866
1433
  replace,
867
1434
  commitUrl: url,
868
1435
  signal: navAbort.signal,
869
1436
  skipHistory: effectiveSkipHistory,
870
1437
  departingUrl
871
1438
  }));
872
- window.dispatchEvent(new Event("timber:navigation-end"));
873
1439
  if (scroll && hash) scrollToHashAfterPaint(hash);
874
1440
  else restoreScrollAfterPaint(scroll ? 0 : currentScrollY);
875
1441
  } catch (error) {
876
- if (error instanceof VersionSkewError) {
877
- setHardNavigating(true);
878
- window.location.reload();
879
- await new Promise(() => {});
880
- }
881
- if (error instanceof RedirectError) {
882
- if (currentNavAbort !== navAbort) return;
883
- await navigate(error.redirectUrl, { replace: true });
884
- return;
885
- }
886
- if (error instanceof ServerErrorResponse) {
887
- setHardNavigating(true);
888
- hardNavigate(url, departingUrl);
889
- await new Promise(() => {});
890
- }
891
- if (error instanceof NonRscResponse) {
892
- setHardNavigating(true);
893
- hardNavigate(url, departingUrl);
894
- await new Promise(() => {});
895
- }
1442
+ if (await recoverFromNavigationError(error, navAbort, url, departingUrl)) return;
896
1443
  throw error;
897
1444
  }
898
1445
  }, externalSignal);
899
1446
  }
1447
+ /**
1448
+ * Fetch a URL's RSC payload, commit it, and render — the pipeline shared by
1449
+ * refresh() and an uncached back/forward traversal. Both can be the path
1450
+ * that discovers a version skew, and both recover the same way (TIM-1275).
1451
+ *
1452
+ * Not `performNavigationFetch`: neither caller consults the prefetch cache
1453
+ * (there is no link to have hovered) and neither moves the address bar (the
1454
+ * browser is already where it is going), so the fetch is a plain one.
1455
+ */
1456
+ async function fetchCommitAndRender(url, opts = {}) {
1457
+ await runNavigation(url, async (navAbort) => {
1458
+ try {
1459
+ await renderViaTransition(url, navAbort, async () => {
1460
+ const result = await fetchRscPayload(url, deps, opts.stateTree, void 0, navAbort.signal);
1461
+ const payload = await resolveForFallback(result.payload);
1462
+ const params = await result.params;
1463
+ const { navState, commit } = prepareNavigation(url, {
1464
+ payload,
1465
+ params,
1466
+ segmentInfo: result.segmentInfo,
1467
+ status: result.status
1468
+ });
1469
+ return {
1470
+ ...result,
1471
+ payload,
1472
+ params,
1473
+ navState,
1474
+ commit
1475
+ };
1476
+ });
1477
+ } catch (error) {
1478
+ if (await recoverFromNavigationError(error, navAbort, url, url)) return;
1479
+ throw error;
1480
+ }
1481
+ if (opts.scrollY !== void 0) restoreScrollAfterPaint(opts.scrollY);
1482
+ }, opts.externalSignal);
1483
+ }
900
1484
  async function refresh() {
901
1485
  const currentUrl = deps.getCurrentUrl();
902
- await runNavigation(currentUrl, async (navAbort) => {
903
- await renderViaTransition(currentUrl, async () => {
904
- const result = await fetchRscPayload(currentUrl, deps, void 0, void 0, navAbort.signal);
905
- const payload = await resolveForFallback(result.payload);
906
- const navState = commitNavigation(currentUrl, {
907
- payload,
908
- params: result.params,
909
- segmentInfo: result.segmentInfo
910
- });
911
- return {
912
- ...result,
913
- payload,
914
- navState
915
- };
916
- });
917
- });
1486
+ if (isClientStale()) await leaveSpaSuperseding(currentUrl, currentUrl);
1487
+ await fetchCommitAndRender(currentUrl);
918
1488
  }
919
1489
  async function handlePopState(url, scrollY = 0, externalSignal) {
1490
+ if (isClientStale()) await leaveSpaSuperseding(url, url);
920
1491
  const entry = historyStack.get(url);
921
1492
  if (entry && entry.payload !== null) await runNavigation(url, async () => {
922
- const navState = commitNavigation(url, {
1493
+ const { navState, commit } = prepareNavigation(url, {
923
1494
  payload: entry.payload,
924
1495
  params: entry.params,
925
1496
  segmentInfo: entry.segmentInfo,
926
1497
  clearSegmentCacheOnEmpty: true
927
1498
  });
928
- renderPayload(entry.payload, navState);
929
- restoreScrollAfterPaint(scrollY);
930
- }, externalSignal);
931
- else await runNavigation(url, async (navAbort) => {
932
- await renderViaTransition(url, async () => {
933
- const result = await fetchRscPayload(url, deps, deps.clientSegmentCache ? segmentCache.serializeStateTree() : void 0, void 0, navAbort.signal);
934
- const payload = await resolveForFallback(result.payload);
935
- const navState = commitNavigation(url, {
936
- payload,
937
- params: result.params,
938
- segmentInfo: result.segmentInfo
939
- });
940
- return {
941
- ...result,
942
- payload,
943
- navState
944
- };
945
- });
1499
+ commit();
1500
+ renderPayload(entry.payload, navState, entry.params);
946
1501
  restoreScrollAfterPaint(scrollY);
947
1502
  }, externalSignal);
1503
+ else await fetchCommitAndRender(url, {
1504
+ stateTree: currentStateTree(),
1505
+ scrollY,
1506
+ externalSignal
1507
+ });
948
1508
  }
949
1509
  /**
950
1510
  * Prefetch an RSC payload for a URL and store it in the prefetch cache.
951
1511
  * Called on hover of <Link prefetch> elements.
952
1512
  */
953
1513
  function prefetch(url) {
1514
+ if (isClientStale()) return;
954
1515
  const hashIndex = url.indexOf("#");
955
1516
  const fetchUrl = hashIndex === -1 ? url : url.slice(0, hashIndex);
956
- if (prefetchCache.get(fetchUrl) !== void 0) return;
1517
+ const stateTree = currentStateTree();
1518
+ const cacheKey = prefetchKeyFor(fetchUrl, deps.getCurrentUrl(), stateTree);
1519
+ const from = cacheKey.from;
1520
+ if (prefetchCache.get(cacheKey) !== void 0) return;
957
1521
  if (historyStack.has(fetchUrl)) return;
958
- fetchRscPayload(fetchUrl, deps, deps.clientSegmentCache ? segmentCache.serializeStateTree() : void 0).then((result) => {
959
- result.decodePromise?.catch(() => {});
960
- prefetchCache.set(fetchUrl, result);
1522
+ fetchRscPayload(fetchUrl, deps, stateTree, from).then((result) => {
1523
+ result.decodePromise?.catch((error) => void recordSkew(error));
1524
+ prefetchCache.set(cacheKey, result);
961
1525
  }, (error) => {
962
1526
  if (error instanceof NonRscResponse) {
963
- prefetchCache.setNegative(fetchUrl);
1527
+ prefetchCache.setNegative(cacheKey);
964
1528
  return;
965
1529
  }
1530
+ recordSkew(error);
966
1531
  });
967
1532
  }
968
1533
  return {
969
1534
  navigate,
970
1535
  refresh,
971
1536
  handlePopState,
972
- isPending: () => routerPhase.phase === "navigating",
973
- getPendingUrl: () => routerPhase.phase === "navigating" ? routerPhase.targetUrl : null,
974
- onPendingChange(listener) {
975
- pendingListeners.add(listener);
976
- return () => pendingListeners.delete(listener);
977
- },
1537
+ isPending,
1538
+ getPendingUrl,
1539
+ onPendingChange,
978
1540
  prefetch,
979
- applyRevalidation(element) {
1541
+ applyRevalidation(payloadRoot) {
980
1542
  const currentUrl = deps.getCurrentUrl();
1543
+ const tree = readPayloadTree(payloadRoot);
1544
+ const params = readPublishedParams(payloadRoot);
981
1545
  const existingEntry = historyStack.get(currentUrl);
982
- renderPayload(element, commitNavigation(currentUrl, {
983
- payload: element,
984
- params: getNavigationState().params,
1546
+ const { navState, commit } = prepareNavigation(currentUrl, {
1547
+ payload: tree,
1548
+ params,
985
1549
  segmentInfo: existingEntry?.segmentInfo
986
- }));
1550
+ });
1551
+ commit();
1552
+ renderPayload(tree, navState, params);
987
1553
  },
988
1554
  initSegmentCache: (segments) => updateSegmentCache(segments),
989
1555
  segmentCache,
@@ -1044,6 +1610,6 @@ function useSearchParams() {
1044
1610
  return new URLSearchParams();
1045
1611
  }
1046
1612
  //#endregion
1047
- export { HistoryStack, NavigationProvider, PrefetchCache, SegmentCache, SegmentProvider, TimberErrorBoundary, bindUseQueryStates, clearSsrData, createRouter, getNavigationState, getRouter, getRouterOrNull, getSsrData, setCurrentParams, setGlobalRouter, setNavigationState, setSsrData, useSearchParams, useSegmentContext };
1613
+ export { HistoryStack, NavigationProvider, PrefetchCache, SegmentCache, SegmentProvider, TimberErrorBoundary, bindUseQueryStates, clearSsrData, createRouter, getNavigationState, getRouter, getRouterOrNull, getSsrData, setCurrentParams, setCurrentSlotParams, setGlobalRouter, setNavigationState, setSsrData, useSearchParams, useSegmentContext };
1048
1614
 
1049
1615
  //# sourceMappingURL=internal.js.map