@timber-js/app 0.2.0-alpha.184 → 0.2.0-alpha.186

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 (441) hide show
  1. package/dist/_chunks/{actions-O_LsyCE4.js → actions-C-Rw9vPc.js} +3 -3
  2. package/dist/_chunks/{actions-O_LsyCE4.js.map → actions-C-Rw9vPc.js.map} +1 -1
  3. package/dist/_chunks/als-slots-mFweg276.js +27 -0
  4. package/dist/_chunks/als-slots-mFweg276.js.map +1 -0
  5. package/dist/_chunks/base-path-DaQrzbez.js +80 -0
  6. package/dist/_chunks/base-path-DaQrzbez.js.map +1 -0
  7. package/dist/_chunks/build-manifest-DWppEdLB.js +86 -0
  8. package/dist/_chunks/build-manifest-DWppEdLB.js.map +1 -0
  9. package/dist/_chunks/build-output-helper-C3DrfzZR.js +369 -0
  10. package/dist/_chunks/build-output-helper-C3DrfzZR.js.map +1 -0
  11. package/dist/_chunks/{cache-api-B-lhk9p4.js → cache-api-Cd0VZ_Pd.js} +3 -3
  12. package/dist/_chunks/{cache-api-B-lhk9p4.js.map → cache-api-Cd0VZ_Pd.js.map} +1 -1
  13. package/dist/_chunks/cli-schema-sync-CKgHC2MB.js +1857 -0
  14. package/dist/_chunks/cli-schema-sync-CKgHC2MB.js.map +1 -0
  15. package/dist/_chunks/{cloudflare-CnT5Lr7U.js → cloudflare-Cs0uZXea.js} +89 -12
  16. package/dist/_chunks/{cloudflare-CnT5Lr7U.js.map → cloudflare-Cs0uZXea.js.map} +1 -1
  17. package/dist/_chunks/error-boundary-DpYRI_I1.js +330 -0
  18. package/dist/_chunks/error-boundary-DpYRI_I1.js.map +1 -0
  19. package/dist/_chunks/{fast-hash-D6hIVt1Y.js → fast-hash-C-tVYrLb.js} +6 -1
  20. package/dist/_chunks/fast-hash-C-tVYrLb.js.map +1 -0
  21. package/dist/_chunks/fs-identity-D7vb6GdI.js +208 -0
  22. package/dist/_chunks/fs-identity-D7vb6GdI.js.map +1 -0
  23. package/dist/_chunks/{logger-AWfuX-KJ.js → logger-N7e5auP0.js} +9 -13
  24. package/dist/_chunks/logger-N7e5auP0.js.map +1 -0
  25. package/dist/_chunks/navigation-root-B29qg0_T.js +274 -0
  26. package/dist/_chunks/navigation-root-B29qg0_T.js.map +1 -0
  27. package/dist/_chunks/param-value-C8TNYchQ.js +66 -0
  28. package/dist/_chunks/param-value-C8TNYchQ.js.map +1 -0
  29. package/dist/_chunks/{plugin-context---kTF5v8.js → plugin-context-DEGLSJs3.js} +2 -1
  30. package/dist/_chunks/plugin-context-DEGLSJs3.js.map +1 -0
  31. package/dist/_chunks/{router-ref-CtmF-aPv.js → router-ref-DuYuV_0Q.js} +2 -2
  32. package/dist/_chunks/router-ref-DuYuV_0Q.js.map +1 -0
  33. package/dist/_chunks/rsc-cache-key-DD0fl_-s.js +243 -0
  34. package/dist/_chunks/rsc-cache-key-DD0fl_-s.js.map +1 -0
  35. package/dist/_chunks/rsc-error-envelope-tT5PJs4q.js +40 -0
  36. package/dist/_chunks/rsc-error-envelope-tT5PJs4q.js.map +1 -0
  37. package/dist/_chunks/rsc-payload-path-B_LBodc2.js +48 -0
  38. package/dist/_chunks/rsc-payload-path-B_LBodc2.js.map +1 -0
  39. package/dist/_chunks/{segment-classify-Byy425ng.js → segment-classify-C539Pa2O.js} +23 -2
  40. package/dist/_chunks/segment-classify-C539Pa2O.js.map +1 -0
  41. package/dist/_chunks/{canonicalize-DQHyFClh.js → segment-keys-BawYuNFO.js} +61 -2
  42. package/dist/_chunks/segment-keys-BawYuNFO.js.map +1 -0
  43. package/dist/_chunks/slot-params-BCTmZkQB.js +76 -0
  44. package/dist/_chunks/slot-params-BCTmZkQB.js.map +1 -0
  45. package/dist/_chunks/{ssr-data-BOWsq18U.js → ssr-data-14MXm7Pj.js} +17 -2
  46. package/dist/_chunks/ssr-data-14MXm7Pj.js.map +1 -0
  47. package/dist/_chunks/use-segment-params-ClyUNq4d.js +128 -0
  48. package/dist/_chunks/use-segment-params-ClyUNq4d.js.map +1 -0
  49. package/dist/_chunks/{walkers-BtTlKviE.js → walkers-BhhwI9TD.js} +21 -87
  50. package/dist/_chunks/walkers-BhhwI9TD.js.map +1 -0
  51. package/dist/adapters/build-output-helper.d.ts +37 -0
  52. package/dist/adapters/build-output-helper.d.ts.map +1 -1
  53. package/dist/adapters/cloudflare-dev.js +1 -1
  54. package/dist/adapters/cloudflare-kv-cache.js +1 -1
  55. package/dist/adapters/cloudflare.d.ts.map +1 -1
  56. package/dist/adapters/cloudflare.js +1 -1
  57. package/dist/adapters/fs-identity.d.ts +64 -0
  58. package/dist/adapters/fs-identity.d.ts.map +1 -0
  59. package/dist/adapters/nitro.d.ts.map +1 -1
  60. package/dist/adapters/nitro.js +2 -3
  61. package/dist/adapters/nitro.js.map +1 -1
  62. package/dist/adapters/shared.d.ts +57 -1
  63. package/dist/adapters/shared.d.ts.map +1 -1
  64. package/dist/adapters/types.d.ts +13 -1
  65. package/dist/adapters/types.d.ts.map +1 -1
  66. package/dist/cache/fast-hash.d.ts +5 -0
  67. package/dist/cache/fast-hash.d.ts.map +1 -1
  68. package/dist/cache/index.js +1 -1
  69. package/dist/cli.js +2 -2
  70. package/dist/client/browser-entry/action-dispatch.d.ts.map +1 -1
  71. package/dist/client/browser-entry/hydrate.d.ts +20 -9
  72. package/dist/client/browser-entry/hydrate.d.ts.map +1 -1
  73. package/dist/client/browser-entry/post-hydration.d.ts +4 -1
  74. package/dist/client/browser-entry/post-hydration.d.ts.map +1 -1
  75. package/dist/client/browser-entry/router-init.d.ts +3 -0
  76. package/dist/client/browser-entry/router-init.d.ts.map +1 -1
  77. package/dist/client/browser-entry/rsc-stream.d.ts +8 -0
  78. package/dist/client/browser-entry/rsc-stream.d.ts.map +1 -1
  79. package/dist/client/error-boundary.d.ts +1 -0
  80. package/dist/client/error-boundary.d.ts.map +1 -1
  81. package/dist/client/error-boundary.js +1 -162
  82. package/dist/client/history.d.ts +13 -6
  83. package/dist/client/history.d.ts.map +1 -1
  84. package/dist/client/index.js +18 -11
  85. package/dist/client/index.js.map +1 -1
  86. package/dist/client/internal.d.ts +4 -3
  87. package/dist/client/internal.d.ts.map +1 -1
  88. package/dist/client/internal.js +872 -305
  89. package/dist/client/internal.js.map +1 -1
  90. package/dist/client/link.d.ts.map +1 -1
  91. package/dist/client/navigation-api.d.ts +2 -2
  92. package/dist/client/navigation-commit.d.ts +71 -0
  93. package/dist/client/navigation-commit.d.ts.map +1 -0
  94. package/dist/client/navigation-context.d.ts +37 -20
  95. package/dist/client/navigation-context.d.ts.map +1 -1
  96. package/dist/client/navigation-root.d.ts +63 -23
  97. package/dist/client/navigation-root.d.ts.map +1 -1
  98. package/dist/client/params-context.d.ts +81 -0
  99. package/dist/client/params-context.d.ts.map +1 -0
  100. package/dist/client/router-effects.d.ts +129 -0
  101. package/dist/client/router-effects.d.ts.map +1 -0
  102. package/dist/client/router-lifecycle.d.ts +55 -0
  103. package/dist/client/router-lifecycle.d.ts.map +1 -0
  104. package/dist/client/router-pipeline.d.ts +84 -0
  105. package/dist/client/router-pipeline.d.ts.map +1 -0
  106. package/dist/client/router-ref.d.ts +1 -1
  107. package/dist/client/router-ref.d.ts.map +1 -1
  108. package/dist/client/router-skew.d.ts +21 -0
  109. package/dist/client/router-skew.d.ts.map +1 -0
  110. package/dist/client/router-types.d.ts +207 -0
  111. package/dist/client/router-types.d.ts.map +1 -0
  112. package/dist/client/router.d.ts +1 -182
  113. package/dist/client/router.d.ts.map +1 -1
  114. package/dist/client/rsc-fetch.d.ts +39 -16
  115. package/dist/client/rsc-fetch.d.ts.map +1 -1
  116. package/dist/client/segment-cache.d.ts +79 -34
  117. package/dist/client/segment-cache.d.ts.map +1 -1
  118. package/dist/client/ssr-data.d.ts +6 -0
  119. package/dist/client/ssr-data.d.ts.map +1 -1
  120. package/dist/client/stale-client.d.ts +69 -0
  121. package/dist/client/stale-client.d.ts.map +1 -0
  122. package/dist/client/state.d.ts +10 -1
  123. package/dist/client/state.d.ts.map +1 -1
  124. package/dist/client/top-loader.d.ts +2 -1
  125. package/dist/client/top-loader.d.ts.map +1 -1
  126. package/dist/client/unload-guard.d.ts +5 -0
  127. package/dist/client/unload-guard.d.ts.map +1 -1
  128. package/dist/client/use-cookie.d.ts.map +1 -1
  129. package/dist/client/use-router.d.ts +1 -1
  130. package/dist/client/use-segment-params.d.ts +18 -8
  131. package/dist/client/use-segment-params.d.ts.map +1 -1
  132. package/dist/cookies/index.js +3 -3
  133. package/dist/cookies/index.js.map +1 -1
  134. package/dist/index.d.ts.map +1 -1
  135. package/dist/index.js +915 -320
  136. package/dist/index.js.map +1 -1
  137. package/dist/plugin-context.d.ts +33 -1
  138. package/dist/plugin-context.d.ts.map +1 -1
  139. package/dist/plugins/adapter-build.d.ts.map +1 -1
  140. package/dist/plugins/build-manifest.d.ts +1 -1
  141. package/dist/plugins/build-manifest.d.ts.map +1 -1
  142. package/dist/plugins/entries.d.ts.map +1 -1
  143. package/dist/plugins/fonts.d.ts.map +1 -1
  144. package/dist/plugins/request-dep/analysis.d.ts +106 -0
  145. package/dist/plugins/request-dep/analysis.d.ts.map +1 -0
  146. package/dist/plugins/request-dep/index.d.ts +75 -0
  147. package/dist/plugins/request-dep/index.d.ts.map +1 -0
  148. package/dist/plugins/request-dep/origin.d.ts +61 -0
  149. package/dist/plugins/request-dep/origin.d.ts.map +1 -0
  150. package/dist/plugins/routing.d.ts +9 -0
  151. package/dist/plugins/routing.d.ts.map +1 -1
  152. package/dist/plugins/static-build.d.ts +149 -8
  153. package/dist/plugins/static-build.d.ts.map +1 -1
  154. package/dist/routing/codegen-types.d.ts +18 -0
  155. package/dist/routing/codegen-types.d.ts.map +1 -1
  156. package/dist/routing/codegen.d.ts.map +1 -1
  157. package/dist/routing/collision-probe.d.ts +104 -0
  158. package/dist/routing/collision-probe.d.ts.map +1 -0
  159. package/dist/routing/collision-spaces.d.ts +26 -0
  160. package/dist/routing/collision-spaces.d.ts.map +1 -0
  161. package/dist/routing/index.js +3 -3
  162. package/dist/routing/interception.d.ts +235 -6
  163. package/dist/routing/interception.d.ts.map +1 -1
  164. package/dist/routing/manifest-codegen.d.ts.map +1 -1
  165. package/dist/routing/scanner.d.ts.map +1 -1
  166. package/dist/routing/segment-classify.d.ts +33 -0
  167. package/dist/routing/segment-classify.d.ts.map +1 -1
  168. package/dist/routing/segment-keys.d.ts +95 -0
  169. package/dist/routing/segment-keys.d.ts.map +1 -0
  170. package/dist/routing/slot-placement.d.ts +48 -0
  171. package/dist/routing/slot-placement.d.ts.map +1 -0
  172. package/dist/routing/types.d.ts +1 -3
  173. package/dist/routing/types.d.ts.map +1 -1
  174. package/dist/routing/walkers.d.ts +6 -0
  175. package/dist/routing/walkers.d.ts.map +1 -1
  176. package/dist/search-params/define.d.ts +0 -6
  177. package/dist/search-params/define.d.ts.map +1 -1
  178. package/dist/search-params/index.js +176 -2
  179. package/dist/search-params/index.js.map +1 -1
  180. package/dist/segment-params/define.d.ts +0 -6
  181. package/dist/segment-params/define.d.ts.map +1 -1
  182. package/dist/segment-params/index.js +69 -1
  183. package/dist/segment-params/index.js.map +1 -0
  184. package/dist/server/actions.d.ts +6 -3
  185. package/dist/server/actions.d.ts.map +1 -1
  186. package/dist/server/als-registry.d.ts +2 -1
  187. package/dist/server/als-registry.d.ts.map +1 -1
  188. package/dist/server/build-manifest.d.ts +13 -1
  189. package/dist/server/build-manifest.d.ts.map +1 -1
  190. package/dist/server/chain-url-parts.d.ts +82 -0
  191. package/dist/server/chain-url-parts.d.ts.map +1 -0
  192. package/dist/server/children-interception.d.ts +49 -0
  193. package/dist/server/children-interception.d.ts.map +1 -0
  194. package/dist/server/default-status-page.d.ts +45 -0
  195. package/dist/server/default-status-page.d.ts.map +1 -0
  196. package/dist/server/deny-boundary.d.ts +4 -7
  197. package/dist/server/deny-boundary.d.ts.map +1 -1
  198. package/dist/server/deny-renderer.d.ts +14 -3
  199. package/dist/server/deny-renderer.d.ts.map +1 -1
  200. package/dist/server/head-response.d.ts +35 -0
  201. package/dist/server/head-response.d.ts.map +1 -0
  202. package/dist/server/index.js +2 -2
  203. package/dist/server/internal.js +528 -373
  204. package/dist/server/internal.js.map +1 -1
  205. package/dist/server/param-coercion.d.ts +10 -1
  206. package/dist/server/param-coercion.d.ts.map +1 -1
  207. package/dist/server/pipeline-helpers.d.ts +0 -18
  208. package/dist/server/pipeline-helpers.d.ts.map +1 -1
  209. package/dist/server/pipeline-interception.d.ts +59 -9
  210. package/dist/server/pipeline-interception.d.ts.map +1 -1
  211. package/dist/server/pipeline-outcome.d.ts.map +1 -1
  212. package/dist/server/pipeline-phases.d.ts.map +1 -1
  213. package/dist/server/pipeline.d.ts +11 -0
  214. package/dist/server/pipeline.d.ts.map +1 -1
  215. package/dist/server/prebuilt-builder.d.ts.map +1 -1
  216. package/dist/server/primitives.d.ts +0 -12
  217. package/dist/server/primitives.d.ts.map +1 -1
  218. package/dist/server/publish-params.d.ts +33 -0
  219. package/dist/server/publish-params.d.ts.map +1 -0
  220. package/dist/server/request-context.d.ts +48 -0
  221. package/dist/server/request-context.d.ts.map +1 -1
  222. package/dist/server/route-element-builder.d.ts +32 -2
  223. package/dist/server/route-element-builder.d.ts.map +1 -1
  224. package/dist/server/route-handler.d.ts.map +1 -1
  225. package/dist/server/route-matcher.d.ts +13 -0
  226. package/dist/server/route-matcher.d.ts.map +1 -1
  227. package/dist/server/rsc-cache-key-guard.d.ts +78 -0
  228. package/dist/server/rsc-cache-key-guard.d.ts.map +1 -0
  229. package/dist/server/rsc-entry/api-handler.d.ts.map +1 -1
  230. package/dist/server/rsc-entry/error-renderer.d.ts +14 -2
  231. package/dist/server/rsc-entry/error-renderer.d.ts.map +1 -1
  232. package/dist/server/rsc-entry/helpers.d.ts +2 -10
  233. package/dist/server/rsc-entry/helpers.d.ts.map +1 -1
  234. package/dist/server/rsc-entry/index.d.ts.map +1 -1
  235. package/dist/server/rsc-entry/render-route.d.ts.map +1 -1
  236. package/dist/server/rsc-entry/rsc-payload.d.ts +3 -2
  237. package/dist/server/rsc-entry/rsc-payload.d.ts.map +1 -1
  238. package/dist/server/rsc-entry/rsc-stream.d.ts +3 -2
  239. package/dist/server/rsc-entry/rsc-stream.d.ts.map +1 -1
  240. package/dist/server/rsc-entry/ssr-renderer.d.ts +2 -0
  241. package/dist/server/rsc-entry/ssr-renderer.d.ts.map +1 -1
  242. package/dist/server/sitemap-generator.d.ts.map +1 -1
  243. package/dist/server/skippable-prefix.d.ts +23 -0
  244. package/dist/server/skippable-prefix.d.ts.map +1 -0
  245. package/dist/server/slot-interception.d.ts +80 -0
  246. package/dist/server/slot-interception.d.ts.map +1 -0
  247. package/dist/server/slot-resolver.d.ts +70 -8
  248. package/dist/server/slot-resolver.d.ts.map +1 -1
  249. package/dist/server/ssr-bridge-types.d.ts +6 -0
  250. package/dist/server/ssr-bridge-types.d.ts.map +1 -1
  251. package/dist/server/ssr-entry.d.ts.map +1 -1
  252. package/dist/server/ssr-wrappers.d.ts +24 -13
  253. package/dist/server/ssr-wrappers.d.ts.map +1 -1
  254. package/dist/server/state-tree-diff.d.ts +6 -27
  255. package/dist/server/state-tree-diff.d.ts.map +1 -1
  256. package/dist/server/static-generator.d.ts +6 -0
  257. package/dist/server/static-generator.d.ts.map +1 -1
  258. package/dist/server/static-not-found.d.ts +52 -0
  259. package/dist/server/static-not-found.d.ts.map +1 -0
  260. package/dist/server/status-code-resolver.d.ts +3 -10
  261. package/dist/server/status-code-resolver.d.ts.map +1 -1
  262. package/dist/shared/als-slots.d.ts +41 -0
  263. package/dist/shared/als-slots.d.ts.map +1 -0
  264. package/dist/shared/base-path.d.ts +56 -0
  265. package/dist/shared/base-path.d.ts.map +1 -0
  266. package/dist/shared/param-value.d.ts +80 -0
  267. package/dist/shared/param-value.d.ts.map +1 -0
  268. package/dist/shared/payload-root.d.ts +108 -0
  269. package/dist/shared/payload-root.d.ts.map +1 -0
  270. package/dist/shared/rsc-cache-key.d.ts +163 -0
  271. package/dist/shared/rsc-cache-key.d.ts.map +1 -0
  272. package/dist/shared/rsc-error-envelope.d.ts +25 -0
  273. package/dist/shared/rsc-error-envelope.d.ts.map +1 -0
  274. package/dist/shared/rsc-manifest.d.ts +28 -0
  275. package/dist/shared/rsc-manifest.d.ts.map +1 -0
  276. package/dist/shared/rsc-payload-path.d.ts +42 -0
  277. package/dist/shared/rsc-payload-path.d.ts.map +1 -0
  278. package/dist/shared/segment-info.d.ts +59 -0
  279. package/dist/shared/segment-info.d.ts.map +1 -0
  280. package/dist/shared/slot-params.d.ts +51 -0
  281. package/dist/shared/slot-params.d.ts.map +1 -0
  282. package/dist/shared/static-platform-files.d.ts +71 -0
  283. package/dist/shared/static-platform-files.d.ts.map +1 -0
  284. package/dist/shims/navigation-client.d.ts +8 -19
  285. package/dist/shims/navigation-client.d.ts.map +1 -1
  286. package/dist/shims/navigation.d.ts +0 -1
  287. package/dist/shims/navigation.d.ts.map +1 -1
  288. package/docs/api/31-api-client.mdx +4 -2
  289. package/docs/learn/02-pages-and-layouts.mdx +1 -1
  290. package/docs/learn/11-error-handling.mdx +4 -3
  291. package/docs/learn/12-client-navigation.mdx +2 -2
  292. package/docs/learn/14-deploying.mdx +23 -0
  293. package/docs/more/01-advanced-routing.mdx +172 -2
  294. package/docs/more/04-metadata-and-fonts.mdx +1 -1
  295. package/package.json +2 -2
  296. package/src/adapters/build-output-helper.ts +116 -3
  297. package/src/adapters/cloudflare.ts +86 -11
  298. package/src/adapters/fs-identity.ts +156 -0
  299. package/src/adapters/nitro.ts +6 -6
  300. package/src/adapters/shared.ts +196 -21
  301. package/src/adapters/types.ts +13 -1
  302. package/src/cache/fast-hash.ts +5 -0
  303. package/src/client/browser-entry/action-dispatch.ts +9 -4
  304. package/src/client/browser-entry/hydrate.ts +40 -29
  305. package/src/client/browser-entry/index.ts +15 -1
  306. package/src/client/browser-entry/post-hydration.ts +10 -9
  307. package/src/client/browser-entry/router-init.ts +104 -48
  308. package/src/client/browser-entry/rsc-stream.ts +11 -2
  309. package/src/client/error-boundary.tsx +20 -15
  310. package/src/client/history.ts +13 -6
  311. package/src/client/internal.ts +4 -3
  312. package/src/client/link.tsx +55 -8
  313. package/src/client/navigation-api.ts +3 -3
  314. package/src/client/navigation-commit.ts +174 -0
  315. package/src/client/navigation-context.ts +40 -67
  316. package/src/client/navigation-root.tsx +326 -101
  317. package/src/client/params-context.ts +192 -0
  318. package/src/client/router-effects.ts +231 -0
  319. package/src/client/router-lifecycle.ts +288 -0
  320. package/src/client/router-pipeline.ts +389 -0
  321. package/src/client/router-ref.ts +1 -1
  322. package/src/client/router-skew.ts +30 -0
  323. package/src/client/router-types.ts +230 -0
  324. package/src/client/router.ts +242 -690
  325. package/src/client/rsc-fetch.ts +180 -122
  326. package/src/client/segment-cache.ts +121 -44
  327. package/src/client/ssr-data.ts +6 -0
  328. package/src/client/stale-client.ts +191 -0
  329. package/src/client/state.ts +14 -1
  330. package/src/client/top-loader.tsx +2 -1
  331. package/src/client/unload-guard.ts +34 -2
  332. package/src/client/use-cookie.ts +9 -2
  333. package/src/client/use-router.ts +1 -1
  334. package/src/client/use-segment-params.ts +41 -18
  335. package/src/index.ts +5 -1
  336. package/src/plugin-context.ts +34 -1
  337. package/src/plugins/adapter-build.ts +4 -0
  338. package/src/plugins/build-manifest.ts +34 -13
  339. package/src/plugins/entries.ts +4 -0
  340. package/src/plugins/fonts.ts +11 -8
  341. package/src/plugins/request-dep/analysis.ts +447 -0
  342. package/src/plugins/request-dep/index.ts +376 -0
  343. package/src/plugins/request-dep/origin.ts +310 -0
  344. package/src/plugins/routing.ts +60 -3
  345. package/src/plugins/static-build.ts +333 -63
  346. package/src/routing/codegen-types.ts +18 -0
  347. package/src/routing/codegen.ts +50 -10
  348. package/src/routing/collision-probe.ts +438 -0
  349. package/src/routing/collision-spaces.ts +281 -0
  350. package/src/routing/interception.ts +896 -50
  351. package/src/routing/manifest-codegen.ts +6 -9
  352. package/src/routing/scanner.ts +114 -113
  353. package/src/routing/segment-classify.ts +57 -0
  354. package/src/routing/segment-keys.ts +146 -0
  355. package/src/routing/slot-placement.ts +97 -0
  356. package/src/routing/types.ts +1 -3
  357. package/src/routing/walkers.ts +8 -0
  358. package/src/search-params/define.ts +6 -29
  359. package/src/segment-params/define.ts +4 -27
  360. package/src/server/action-handler.ts +3 -1
  361. package/src/server/actions.ts +6 -3
  362. package/src/server/als-registry.ts +2 -1
  363. package/src/server/build-manifest.ts +14 -1
  364. package/src/server/chain-url-parts.ts +163 -0
  365. package/src/server/children-interception.ts +115 -0
  366. package/src/server/default-status-page.ts +127 -0
  367. package/src/server/deny-boundary.ts +5 -31
  368. package/src/server/deny-renderer.ts +83 -22
  369. package/src/server/head-response.ts +54 -0
  370. package/src/server/param-coercion.ts +48 -18
  371. package/src/server/pipeline-helpers.ts +0 -45
  372. package/src/server/pipeline-interception.ts +81 -32
  373. package/src/server/pipeline-outcome.ts +3 -5
  374. package/src/server/pipeline-phases.ts +68 -16
  375. package/src/server/pipeline.ts +57 -10
  376. package/src/server/prebuilt-builder.ts +1 -7
  377. package/src/server/prebuilt-runtime.ts +4 -4
  378. package/src/server/primitives.ts +0 -18
  379. package/src/server/publish-params.ts +42 -0
  380. package/src/server/request-context.ts +97 -18
  381. package/src/server/route-element-builder.ts +89 -98
  382. package/src/server/route-handler.ts +8 -8
  383. package/src/server/route-matcher.ts +21 -9
  384. package/src/server/rsc-cache-key-guard.ts +170 -0
  385. package/src/server/rsc-entry/api-handler.ts +6 -8
  386. package/src/server/rsc-entry/error-renderer.ts +64 -25
  387. package/src/server/rsc-entry/helpers.ts +31 -16
  388. package/src/server/rsc-entry/index.ts +36 -28
  389. package/src/server/rsc-entry/render-route.ts +12 -7
  390. package/src/server/rsc-entry/revalidate-renderer.ts +1 -1
  391. package/src/server/rsc-entry/rsc-payload.ts +14 -25
  392. package/src/server/rsc-entry/rsc-stream.ts +4 -3
  393. package/src/server/rsc-entry/ssr-renderer.ts +13 -9
  394. package/src/server/sitemap-generator.ts +1 -7
  395. package/src/server/skippable-prefix.ts +92 -0
  396. package/src/server/slot-interception.ts +168 -0
  397. package/src/server/slot-resolver.ts +253 -156
  398. package/src/server/ssr-bridge-types.ts +6 -0
  399. package/src/server/ssr-entry.ts +12 -4
  400. package/src/server/ssr-wrappers.tsx +33 -32
  401. package/src/server/state-tree-diff.ts +14 -55
  402. package/src/server/static-generator.ts +67 -45
  403. package/src/server/static-not-found.ts +134 -0
  404. package/src/server/status-code-resolver.ts +4 -47
  405. package/src/shared/als-slots.ts +72 -0
  406. package/src/shared/base-path.ts +80 -0
  407. package/src/shared/param-value.ts +244 -0
  408. package/src/shared/payload-root.ts +151 -0
  409. package/src/shared/rsc-cache-key.ts +269 -0
  410. package/src/shared/rsc-error-envelope.ts +31 -0
  411. package/src/shared/rsc-manifest.ts +29 -0
  412. package/src/shared/rsc-payload-path.ts +45 -0
  413. package/src/shared/segment-info.ts +60 -0
  414. package/src/shared/slot-params.ts +111 -0
  415. package/src/shared/static-platform-files.ts +75 -0
  416. package/src/shims/navigation-client.ts +8 -26
  417. package/src/shims/navigation.ts +0 -4
  418. package/dist/_chunks/build-output-helper-DOGYFb_X.js +0 -196
  419. package/dist/_chunks/build-output-helper-DOGYFb_X.js.map +0 -1
  420. package/dist/_chunks/canonicalize-DQHyFClh.js.map +0 -1
  421. package/dist/_chunks/cli-schema-sync-D3naS3eH.js +0 -688
  422. package/dist/_chunks/cli-schema-sync-D3naS3eH.js.map +0 -1
  423. package/dist/_chunks/define-COtkxMRT.js +0 -191
  424. package/dist/_chunks/define-COtkxMRT.js.map +0 -1
  425. package/dist/_chunks/define-c4au4I9R.js +0 -82
  426. package/dist/_chunks/define-c4au4I9R.js.map +0 -1
  427. package/dist/_chunks/fast-hash-D6hIVt1Y.js.map +0 -1
  428. package/dist/_chunks/logger-AWfuX-KJ.js.map +0 -1
  429. package/dist/_chunks/navigation-root-bUDZLw74.js +0 -124
  430. package/dist/_chunks/navigation-root-bUDZLw74.js.map +0 -1
  431. package/dist/_chunks/plugin-context---kTF5v8.js.map +0 -1
  432. package/dist/_chunks/router-ref-CtmF-aPv.js.map +0 -1
  433. package/dist/_chunks/segment-classify-Byy425ng.js.map +0 -1
  434. package/dist/_chunks/ssr-data-BOWsq18U.js.map +0 -1
  435. package/dist/_chunks/use-segment-params-D1CzlgBo.js +0 -151
  436. package/dist/_chunks/use-segment-params-D1CzlgBo.js.map +0 -1
  437. package/dist/_chunks/walkers-BtTlKviE.js.map +0 -1
  438. package/dist/client/error-boundary.js.map +0 -1
  439. package/dist/plugins/request-dep.d.ts +0 -32
  440. package/dist/plugins/request-dep.d.ts.map +0 -1
  441. package/src/plugins/request-dep.ts +0 -499
@@ -9,6 +9,8 @@
9
9
 
10
10
  import { createElement } from 'react';
11
11
  import { createFromFetch } from '../../rsc-runtime/browser.js';
12
+ import { PayloadRoot } from '../params-context.js';
13
+ import { readPublishedParams, type ParamsSource } from '../../shared/payload-root.js';
12
14
  import { createRouter, setGlobalRouter } from '#client-internal';
13
15
  import type { RouterDeps, RouterInstance } from '#client-internal';
14
16
  import type { NavigationState } from '#client-internal';
@@ -29,6 +31,8 @@ import { getScrollY, scrollToHashTarget } from './scroll.js';
29
31
 
30
32
  export interface RouterInitOptions {
31
33
  initialElement?: unknown;
34
+ /** The params the hydration payload published, beside `initialElement`. */
35
+ initialParams?: ParamsSource;
32
36
  clientSegmentCache?: boolean;
33
37
  }
34
38
 
@@ -58,6 +62,63 @@ export function createTimberRouter(options?: RouterInitOptions): RouterInitResul
58
62
  // very first client navigation (before any wrapPayload call).
59
63
  let currentPayload: unknown = options?.initialElement ?? null;
60
64
 
65
+ // The params `currentPayload` is displayed with. Tracked beside it because
66
+ // the two render paths that do NOT receive a payload — a shallow search
67
+ // update and any re-wrap of the current tree — must republish the same
68
+ // record rather than dropping it. They are written together, in one place
69
+ // (`renderTree`), so they cannot come apart.
70
+ let currentParams: ParamsSource = options?.initialParams ?? readPublishedParams(undefined);
71
+
72
+ /**
73
+ * Build the client's wrapper chain around a tree. **The only place it is
74
+ * built.**
75
+ *
76
+ * Every render path goes through here — hydration builds the same chain in
77
+ * `hydrate.ts` against this shape, and a navigation, a shallow search
78
+ * update, a popstate replay and a revalidation all call this. That matters
79
+ * beyond tidiness: an element type present on one render and absent on the
80
+ * next changes the type at that position, so React unmounts and remounts
81
+ * everything below it, destroying the layout state partial navigation
82
+ * exists to preserve (caught by tests/e2e/segment-merge-navigation.test.ts
83
+ * when the params wrapper was conditional).
84
+ *
85
+ * `PayloadRoot` is *above* `currentPayload`, which on a partial navigation
86
+ * is the retained tree. That position is the whole design: it is the only
87
+ * provider a reader in a skipped layout can resolve to (TIM-1297).
88
+ *
89
+ * Pure — building the element does not make it the displayed one. Every
90
+ * caller pairs this with `publishTree` at the point the tree is actually
91
+ * handed to React, because a navigation builds its element before it is
92
+ * known to have won and a superseded one must leave `currentPayload`
93
+ * describing the route still on screen (TIM-1301).
94
+ */
95
+ function renderTree(
96
+ tree: unknown,
97
+ navState: NavigationState,
98
+ params: ParamsSource,
99
+ segmentUpdates?: Map<string, React.ReactNode>
100
+ ): React.ReactElement {
101
+ const withParams = createElement(PayloadRoot, { source: params }, tree as React.ReactNode);
102
+ const withNav = createElement(NavigationProvider, { value: navState }, withParams);
103
+ const withUpdates = createElement(
104
+ SegmentUpdateContext.Provider,
105
+ { value: segmentUpdates ?? EMPTY_SEGMENT_UPDATES },
106
+ withNav
107
+ );
108
+ return createElement(TimberNuqsAdapter, null, withUpdates);
109
+ }
110
+
111
+ /**
112
+ * Record the tree the client is now displaying, and the params published
113
+ * over it. Written together — the two paths that re-wrap without a fresh
114
+ * payload (a shallow search update, a partial navigation) read them back as
115
+ * a pair and would otherwise restore one route's tree with another's params.
116
+ */
117
+ function publishTree(tree: unknown, params: ParamsSource): void {
118
+ currentPayload = tree;
119
+ currentParams = params;
120
+ }
121
+
61
122
  // ─── History API Patch ──────────────────────────────────────────
62
123
  // Patch pushState/replaceState to detect external URL changes (nuqs
63
124
  // shallow updates, replaceUrl, third-party libraries). When the router
@@ -93,25 +154,20 @@ export function createTimberRouter(options?: RouterInitOptions): RouterInitResul
93
154
  if (currentSearch === navState.search) return;
94
155
  if (!currentPayload) return;
95
156
 
157
+ // A shallow search update does not re-render the tree from the server, so
158
+ // the params are unchanged — but they are the client's to publish now, and
159
+ // this path rebuilds the whole chain. Republishing `currentParams` is what
160
+ // keeps a `?q=` change from silently clearing the route's params
161
+ // (TIM-1285's failure mode, on a path that used to be exempt from it
162
+ // because the record lived inside the payload).
96
163
  const newNavState: NavigationState = {
97
- params: navState.params,
98
164
  pathname: navState.pathname,
99
165
  search: currentSearch,
100
166
  };
101
167
  setNavigationState(newNavState);
102
168
 
103
- const withNav = createElement(
104
- NavigationProvider,
105
- { value: newNavState },
106
- currentPayload as React.ReactNode
107
- );
108
- const withUpdates = createElement(
109
- SegmentUpdateContext.Provider,
110
- { value: EMPTY_SEGMENT_UPDATES },
111
- withNav
112
- );
113
- const wrapped = createElement(TimberNuqsAdapter, null, withUpdates);
114
- transitionRender(wrapped);
169
+ publishTree(currentPayload, currentParams);
170
+ transitionRender(renderTree(currentPayload, newNavState, currentParams));
115
171
  }
116
172
 
117
173
  window.history.pushState = function (
@@ -179,58 +235,58 @@ export function createTimberRouter(options?: RouterInitOptions): RouterInitResul
179
235
  // Wraps with NavigationProvider + TimberNuqsAdapter.
180
236
  //
181
237
  // For navigation renders (navigate, refresh, popstate-with-fetch),
182
- // navigateTransition is used instead — it wraps the entire navigation
183
- // in a React transition with useOptimistic for the pending URL.
238
+ // navigateTransition is used instead — it hands the destination tree to
239
+ // React in a transition so the departing page stays visible.
184
240
  //
185
241
  // navState is passed explicitly by the router — no temporal coupling
186
242
  // with getNavigationState().
187
- renderRoot: (element: unknown, navState: NavigationState) => {
188
- currentPayload = element;
189
- const withNav = createElement(
190
- NavigationProvider,
191
- { value: navState },
192
- element as React.ReactNode
193
- );
194
- const withUpdates = createElement(
195
- SegmentUpdateContext.Provider,
196
- { value: EMPTY_SEGMENT_UPDATES },
197
- withNav
198
- );
199
- const wrapped = createElement(TimberNuqsAdapter, null, withUpdates);
200
- transitionRender(wrapped);
243
+ renderRoot: (element: unknown, navState: NavigationState, params: ParamsSource) => {
244
+ // These paths render what is already decided (a replay, a
245
+ // revalidation), so publishing here is the commit.
246
+ publishTree(element, params);
247
+ transitionRender(renderTree(element, navState, params));
201
248
  },
202
249
 
203
- // Run a navigation inside a React transition with optimistic pending URL.
204
- // The entire fetch + state update runs inside startTransition. useOptimistic
205
- // shows the pending URL immediately and reverts to null when the transition
206
- // commits (atomic with the new tree + params).
250
+ // Hand a navigation's tree to React in a transition (navigation-root.tsx).
251
+ // The fetch runs outside any transition scope; only the state update that
252
+ // hands the tree over is wrapped, and in a synchronous `startTransition`.
253
+ // Do NOT make this callback's caller an async React action — see the
254
+ // entanglement note in `navigateTransition` (TIM-1306, TIM-1307).
207
255
  //
208
256
  // The perform callback receives a wrapPayload function that wraps the
209
257
  // decoded RSC payload with NavigationProvider + NuqsAdapter. navState
210
258
  // is passed explicitly by the router — no getNavigationState() needed.
211
- navigateTransition: (pendingUrl: string, perform) => {
212
- return navigateTransition(pendingUrl, async () => {
213
- const { element, decodePromise } = await perform(
259
+ navigateTransition: (url: string, perform) => {
260
+ return navigateTransition(url, async () => {
261
+ // What wrapPayload built, held until the transition wins. The client's
262
+ // record of the displayed tree is navigation state like any other, so
263
+ // it rides the same commit as the segment cache rather than being
264
+ // written as a side effect of building the element (TIM-1301).
265
+ let displayed: { tree: unknown; params: ParamsSource } | null = null;
266
+ const { element, decodePromise, commit } = await perform(
214
267
  (
215
268
  rawPayload: unknown,
216
269
  navState: NavigationState,
270
+ params: ParamsSource,
217
271
  segmentUpdates?: Map<string, unknown>
218
272
  ) => {
219
- currentPayload = rawPayload;
220
- const withNav = createElement(
221
- NavigationProvider,
222
- { value: navState },
223
- rawPayload as React.ReactNode
224
- );
225
- const withUpdates = createElement(
226
- SegmentUpdateContext.Provider,
227
- { value: (segmentUpdates ?? EMPTY_SEGMENT_UPDATES) as Map<string, React.ReactNode> },
228
- withNav
273
+ displayed = { tree: rawPayload, params };
274
+ return renderTree(
275
+ rawPayload,
276
+ navState,
277
+ params,
278
+ segmentUpdates as Map<string, React.ReactNode> | undefined
229
279
  );
230
- return createElement(TimberNuqsAdapter, null, withUpdates);
231
280
  }
232
281
  );
233
- return { element: element as React.ReactNode, decodePromise };
282
+ return {
283
+ element: element as React.ReactNode,
284
+ decodePromise,
285
+ commit: () => {
286
+ if (displayed) publishTree(displayed.tree, displayed.params);
287
+ commit();
288
+ },
289
+ };
234
290
  });
235
291
  },
236
292
 
@@ -13,12 +13,20 @@
13
13
 
14
14
  import { createFromReadableStream } from '../../rsc-runtime/browser.js';
15
15
  import { isPageUnloading } from '../unload-guard.js';
16
+ import { splitPayloadRoot, type PublishedParams } from '../../shared/payload-root.js';
16
17
 
17
18
  type FlightSegment = [isBootstrap: 0] | [isData: 1, data: string];
18
19
 
19
20
  export interface RscStreamResult {
20
21
  /** The decoded RSC element (thenable/lazy — resolved by React during render) */
21
22
  element: unknown;
23
+ /**
24
+ * The params the same payload published beside that tree. A promise: the
25
+ * inlined Flight stream is still arriving when this runs, and hydration must
26
+ * not wait for it. `PayloadRoot` suspends on it at the position React was
27
+ * going to suspend on `element` anyway (TIM-1297).
28
+ */
29
+ params: Promise<PublishedParams>;
22
30
  }
23
31
 
24
32
  /**
@@ -152,6 +160,7 @@ export function createRscPayloadStream(): RscStreamResult | null {
152
160
  queueMicrotask(onDOMContentLoaded);
153
161
  }
154
162
 
155
- const element = createFromReadableStream(rscPayload);
156
- return { element };
163
+ const { tree, params } = splitPayloadRoot(createFromReadableStream(rscPayload));
164
+ // The stream is a thenable, so the split is asynchronous on both halves.
165
+ return { element: tree, params: params as Promise<PublishedParams> };
157
166
  }
@@ -21,20 +21,12 @@
21
21
  import { Component, createElement, type ReactNode } from 'react';
22
22
  import { getSsrData } from './ssr-data.js';
23
23
  import { getRouterOrNull } from './router-ref.js';
24
-
25
- // ─── Page Unload Detection ───────────────────────────────────────────────────
26
- // Track whether the page is being unloaded (user refreshed or navigated away).
27
- // When this is true, error boundaries suppress activation — the error is from
28
- // the aborted connection, not an application error.
29
- let _isUnloading = false;
30
- if (typeof window !== 'undefined') {
31
- window.addEventListener('beforeunload', () => {
32
- _isUnloading = true;
33
- });
34
- window.addEventListener('pagehide', () => {
35
- _isUnloading = true;
36
- });
37
- }
24
+ import { markStaleFromError } from './stale-client.js';
25
+ // Page unload detection: when the page is being torn down, error boundaries
26
+ // suppress activation — the error is from the aborted connection, not the
27
+ // application. unload-guard.ts is the single owner of that state; this module
28
+ // used to keep its own copy, which drifted (TIM-1260).
29
+ import { isPageUnloading } from './unload-guard.js';
38
30
 
39
31
  // ─── Digest Types ────────────────────────────────────────────────────────────
40
32
 
@@ -124,13 +116,26 @@ export class TimberErrorBoundary extends Component<
124
116
  // The aborted connection causes React's streaming hydration to error,
125
117
  // but the page is about to be replaced — showing an error boundary
126
118
  // would be a jarring flash for the user.
127
- if (_isUnloading) {
119
+ if (isPageUnloading()) {
128
120
  return { hasError: false, error: null };
129
121
  }
130
122
 
131
123
  return { hasError: true, error };
132
124
  }
133
125
 
126
+ componentDidCatch(error: Error): void {
127
+ // A stale bundle most often surfaces here rather than as a rejected
128
+ // navigation: a client reference missing from this build's manifest, or
129
+ // a lazily-imported chunk the CDN no longer has, thrown during render.
130
+ //
131
+ // Recording it is all that happens — this boundary still renders its
132
+ // fallback, and the recovery is the user's next navigation, which the
133
+ // router turns into a full document load. Reloading from here would be a
134
+ // page load the user did not ask for, on a page that may be mid-rollout
135
+ // and would come back just as stale (TIM-1275).
136
+ markStaleFromError(error);
137
+ }
138
+
134
139
  componentDidUpdate(prevProps: TimberErrorBoundaryProps): void {
135
140
  // Reset error state when children change (e.g. client-side navigation).
136
141
  // Without this, navigating from one error page to another keeps the
@@ -1,7 +1,8 @@
1
1
  // History Stack — stores RSC payloads by URL for instant back/forward navigation
2
2
  // See design/19-client-navigation.md § History Stack
3
3
 
4
- import type { SegmentInfo } from './segment-cache';
4
+ import type { ParamsSource } from '../shared/payload-root.js';
5
+ import type { SegmentInfo } from '../shared/segment-info.js';
5
6
 
6
7
  // ─── Types ───────────────────────────────────────────────────────
7
8
 
@@ -9,12 +10,18 @@ export interface HistoryEntry {
9
10
  /** The complete segment tree payload at the time of navigation */
10
11
  payload: unknown;
11
12
  /**
12
- * Route params for this page (for useSegmentParams). Every entry that can
13
- * be replayed must carry them — the initial SSR entry gets them from the
14
- * server-embedded __timber_params, navigation entries from X-Timber-Params,
15
- * and revalidation overwrites preserve the current state (TIM-1037).
13
+ * The params that payload published. Replaying the tree republishes them,
14
+ * because the client owns the provider now and reads it from here
15
+ * (TIM-1297).
16
+ *
17
+ * Stored beside the tree but never *produced* beside it: both come out of
18
+ * one `splitPayloadRoot` on one decoded root and travel together through
19
+ * `prepareNavigation`, the only producer of an entry. That is the property
20
+ * that matters — when they were derived from separate channels, a replay
21
+ * could restore one route's tree with another route's params (TIM-1037,
22
+ * TIM-1285).
16
23
  */
17
- params?: Record<string, string | string[]> | null;
24
+ params: ParamsSource;
18
25
  /**
19
26
  * Segment metadata for this page's route. Restored into the segment cache
20
27
  * on popstate cached replay so the next forward navigation computes a
@@ -19,7 +19,7 @@ export type {
19
19
  RouterPhase,
20
20
  RscDecoder,
21
21
  RootRenderer,
22
- } from './router.js';
22
+ } from './router-types.js';
23
23
  export { getRouter, getRouterOrNull, setGlobalRouter } from './router-ref.js';
24
24
 
25
25
  // ── SSR bridge ───────────────────────────────────────────────────────────
@@ -32,14 +32,15 @@ export type { SegmentContextValue } from './segment-context.js';
32
32
 
33
33
  // ── Segment cache ────────────────────────────────────────────────────────
34
34
  export { SegmentCache, PrefetchCache } from './segment-cache.js';
35
- export type { SegmentNode, StateTree } from './segment-cache.js';
35
+ export type { SegmentNode, PrefetchKey } from './segment-cache.js';
36
+ export type { SegmentInfo, StateTree } from '../shared/segment-info.js';
36
37
 
37
38
  // ── History ──────────────────────────────────────────────────────────────
38
39
  export { HistoryStack } from './history.js';
39
40
  export type { HistoryEntry } from './history.js';
40
41
 
41
42
  // ── Params (internal setter + raw hooks) ─────────────────────────────────
42
- export { setCurrentParams } from './use-segment-params.js';
43
+ export { setCurrentParams, setCurrentSlotParams } from './use-segment-params.js';
43
44
  export { useSearchParams } from './use-search-params.js';
44
45
 
45
46
  // ── Navigation context ───────────────────────────────────────────────────
@@ -19,7 +19,8 @@
19
19
  // - searchParams and inline query string are mutually exclusive
20
20
 
21
21
  import {
22
- useTransition,
22
+ useRef,
23
+ useState,
23
24
  type AnchorHTMLAttributes,
24
25
  type ReactNode,
25
26
  type MouseEvent as ReactMouseEvent,
@@ -472,11 +473,38 @@ export const Link: LinkFunction = function LinkImpl(props: any) {
472
473
  const { href: baseHref } = buildLinkProps({ href, params: segmentParams, searchParams });
473
474
 
474
475
  // ─── Per-link pending state ─────────────────────────────────────────
475
- // Each Link owns a useTransition. On click, it wraps
476
- // router.navigate() in startTransition — only this Link re-renders
477
- // for isPending, zero siblings touched. The transition tracks the
478
- // full navigation lifecycle (fetch + render + commit).
479
- const [isPending, startTransition] = useTransition();
476
+ // Local `useState`, deliberately NOT `useTransition` (TIM-1307).
477
+ //
478
+ // Either shape keeps the re-render local — the state lives on this Link's
479
+ // own fiber, so no sibling link is touched. The difference is what wrapping
480
+ // `router.navigate()` in a transition does to the REST of the app: returning
481
+ // a thenable from `startTransition` hands it to `ReactSharedInternals.S`,
482
+ // which calls react-dom's `entangleAsyncAction`. That opens an action scope
483
+ // whose `currentEntangledLane` collects every transition update scheduled
484
+ // while it is open — including `NavigationRoot`'s `setRendered`, which is a
485
+ // fully synchronous `startTransition` in a different component — and
486
+ // rendering that lane suspends on `currentEntangledActionThenable` until the
487
+ // action settles. `navigateTransition` awaits `decodePromise`, so a Link
488
+ // navigation used to commit only after the whole Flight stream had decoded,
489
+ // while `useRouter().push()` committed as soon as React could render it.
490
+ //
491
+ // Two commit-timing regimes, and the slower one was the dominant path: no
492
+ // streaming reveal, and the TIM-1301 publish (address bar, segment cache,
493
+ // `timber:navigation-end`) waited for full decode. Not opening an action
494
+ // scope is what collapses them into one.
495
+ //
496
+ // `isPending` runs from the click until `router.navigate()` settles, which
497
+ // is after `decodePromise` — the same lifecycle the router's pending store
498
+ // and the TopLoader use.
499
+ //
500
+ // `clickSeq` exists because a superseded navigation RESOLVES rather than
501
+ // rejecting: `runNavigation` swallows AbortErrors. So clicking the same link
502
+ // twice in quick succession settles the first promise while the second is
503
+ // still in flight, and an unguarded clear would report idle mid-navigation.
504
+ // Only the newest click for this link may clear its flag. Written on click
505
+ // and nowhere else, so there is no reset for a settle handler to race.
506
+ const [isPending, setIsPending] = useState(false);
507
+ const clickSeq = useRef(0);
480
508
  const linkStatus = isPending ? LINK_PENDING : LINK_IDLE;
481
509
 
482
510
  // Preserve search params from the current URL when requested.
@@ -544,8 +572,27 @@ export const Link: LinkFunction = function LinkImpl(props: any) {
544
572
  // from the RSC fetch URL inside the router (TIM-1035).
545
573
  const absoluteHref = resolved.pathname + resolved.search + resolved.hash;
546
574
 
547
- startTransition(async () => {
548
- await router.navigate(absoluteHref, { scroll: shouldScroll });
575
+ const seq = ++clickSeq.current;
576
+ const settle = () => {
577
+ if (clickSeq.current === seq) setIsPending(false);
578
+ };
579
+ setIsPending(true);
580
+ router.navigate(absoluteHref, { scroll: shouldScroll }).then(settle, (error: unknown) => {
581
+ settle();
582
+ // Rethrow, so a navigation error that the router did not already
583
+ // recover from surfaces as an unhandled rejection — the same
584
+ // regime as `useRouter().push()`'s `void router.navigate(...)`.
585
+ //
586
+ // It used to reach the nearest error boundary instead: React
587
+ // re-throws a rejected async action during render (measured on
588
+ // 19.2.7). That replaced the departing page with the app's error
589
+ // UI, which contradicts what every other failure path here
590
+ // promises — a navigation that fails leaves the user on the page
591
+ // they were already looking at (TIM-1306). Recoverable failures
592
+ // never get here anyway; `runNavigation` swallows AbortErrors and
593
+ // `recoverFromNavigationError` turns a failed fetch into a full
594
+ // document load.
595
+ throw error;
549
596
  });
550
597
  }
551
598
  : userOnClick; // External links — just pass through user's onClick
@@ -97,8 +97,8 @@ export interface NavigationApiController {
97
97
  /**
98
98
  * Resolve the deferred promise created by setRouterNavigating(true),
99
99
  * clearing the browser's native loading state. Call this when the
100
- * navigation fully completes — aligned with when the TopLoader's
101
- * pendingUrl clears (same finally block in router.navigate).
100
+ * navigation fully completes — the same finally block in router.navigate
101
+ * that clears the router's pending store.
102
102
  */
103
103
  completeRouterNavigation: () => void;
104
104
 
@@ -147,7 +147,7 @@ export function setupNavigationApi(callbacks: NavigationApiCallbacks): Navigatio
147
147
  // setRouterNavigating(true) is called, resolved by completeRouterNavigation().
148
148
  // The navigate event handler intercepts with this promise so the browser's
149
149
  // native loading state (tab spinner) stays active until the navigation
150
- // completes — aligned with TopLoader's pendingUrl lifecycle.
150
+ // completes — the same lifecycle the TopLoader is driven by.
151
151
  let routerNavDeferred: { promise: Promise<void>; resolve: () => void } | null = null;
152
152
 
153
153
  function handleNavigate(event: NavigateEvent): void {
@@ -0,0 +1,174 @@
1
+ /**
2
+ * What it means for a page to be "current" on the client.
3
+ *
4
+ * One module owns every write that makes a navigation's destination the
5
+ * current page, and the read that describes the current page back to the
6
+ * server. Keeping them together is the point: `X-Timber-State-Tree` is
7
+ * derived from the segment cache, so the thing that publishes and the thing
8
+ * that reports have to agree about when a page becomes current.
9
+ *
10
+ * See design/19-client-navigation.md §"State Update Invariants".
11
+ */
12
+
13
+ import { buildSegmentTree, type SegmentCache } from './segment-cache.js';
14
+ import type { HistoryStack } from './history.js';
15
+ import type { SegmentInfo, StateTree } from '../shared/segment-info.js';
16
+ import type { ParamsSource } from '../shared/payload-root.js';
17
+ import { setNavigationState, type NavigationState } from './navigation-context.js';
18
+
19
+ /** Everything a commit needs to describe the page it makes current. */
20
+ export interface NavigationCommitInput {
21
+ payload: unknown;
22
+ /** The params that payload published — replayed with it on popstate. */
23
+ params: ParamsSource;
24
+ /**
25
+ * The rendered chain's metadata, or null/undefined when this commit
26
+ * carries none at all — a revalidation payload, or a history entry
27
+ * stored before the channel existed.
28
+ *
29
+ * An empty *array* is a different statement from an absent one: the
30
+ * server rendered a route with no layout-bearing segments (a layoutless
31
+ * app — the missing-root-layout check is a warning, not an error), so
32
+ * the correct tree is the empty one. Collapsing the two left the
33
+ * departing route's tree in place, which `treePaths` then reported as
34
+ * mounted — enough to select an intercepting rewrite whose slot is not
35
+ * on screen (TIM-1282).
36
+ */
37
+ segmentInfo?: SegmentInfo[] | null;
38
+ /** When true, clear the segment cache if segmentInfo is absent
39
+ * (popstate replay for entries without layout metadata). */
40
+ clearSegmentCacheOnEmpty?: boolean;
41
+ }
42
+
43
+ /**
44
+ * A navigation's destination, split into what a render needs and what a
45
+ * commit publishes.
46
+ */
47
+ export interface PreparedNavigation {
48
+ /** The state the incoming tree renders with. Pure — safe before winning. */
49
+ navState: NavigationState;
50
+ /** Publishes the destination. Call only once the render has won. */
51
+ commit: () => void;
52
+ }
53
+
54
+ export interface NavigationCommitter {
55
+ prepareNavigation(url: string, opts: NavigationCommitInput): PreparedNavigation;
56
+ currentStateTree(): StateTree | undefined;
57
+ updateSegmentCache(segmentInfo: SegmentInfo[] | null | undefined): void;
58
+ }
59
+
60
+ /**
61
+ * Derive the navigation state (pathname + search) a URL renders with.
62
+ *
63
+ * Pure: publishing it — the module-level fallback for tests and SSR, and the
64
+ * globalThis bridge — is `prepareNavigation`'s commit, which runs only once
65
+ * the render is known to have won (TIM-1301). Callers pass the returned value
66
+ * explicitly to renderRoot/wrapPayload, so a render never depends on the
67
+ * publish having happened first.
68
+ */
69
+ export function deriveNavigationState(url: string): NavigationState {
70
+ // Params are not part of it. They travel with the payload and are published
71
+ // by the one client-owned `ParamsProvider` as the new tree renders, which is
72
+ // also what clears the previous route's — a route with no params of its own
73
+ // still publishes an empty record (TIM-1294/TIM-1297, and the clearing
74
+ // requirement from TIM-1285).
75
+ const parsed = new URL(url, 'http://localhost');
76
+ return { pathname: parsed.pathname || '/', search: parsed.search };
77
+ }
78
+
79
+ export function createNavigationCommitter(deps: {
80
+ segmentCache: SegmentCache;
81
+ historyStack: HistoryStack;
82
+ /** Read at call time — the router's `clientSegmentCache` opt-in. */
83
+ clientSegmentCache: () => boolean | undefined;
84
+ }): NavigationCommitter {
85
+ const { segmentCache, historyStack } = deps;
86
+
87
+ /**
88
+ * Update the segment cache from server-provided segment metadata.
89
+ *
90
+ * Runs whether or not `clientSegmentCache` is enabled: the tree is also
91
+ * where the mounted tree paths come from, and those cross on every
92
+ * navigation. What the flag gates is which parts get serialized — see
93
+ * `currentStateTree`.
94
+ */
95
+ function updateSegmentCache(segmentInfo: SegmentInfo[] | null | undefined): void {
96
+ if (!segmentInfo || segmentInfo.length === 0) return;
97
+ const tree = buildSegmentTree(segmentInfo);
98
+ if (tree) {
99
+ segmentCache.set('/', tree);
100
+ }
101
+ }
102
+
103
+ return {
104
+ updateSegmentCache,
105
+
106
+ /**
107
+ * The X-Timber-State-Tree to send with the next RSC request.
108
+ *
109
+ * Two different things travel on one header. `segments`/`slots` are the
110
+ * caching opt-in and are omitted unless `clientSegmentCache` is on.
111
+ * `treePaths` is how the server learns which route is actually mounted,
112
+ * which stops being derivable from the address bar as soon as a modal is
113
+ * open — so it crosses regardless (TIM-1282). Undefined when there is
114
+ * nothing to say, so a no-op header never enters the `_rsc` cache key.
115
+ *
116
+ * Reads what the last *committed* navigation published, so a superseded
117
+ * navigation's destination can never end up describing the mounted tree
118
+ * (TIM-1301).
119
+ */
120
+ currentStateTree(): StateTree | undefined {
121
+ const tree = segmentCache.serializeStateTree();
122
+ if (deps.clientSegmentCache()) return tree;
123
+ return tree.treePaths ? { segments: [], treePaths: tree.treePaths } : undefined;
124
+ },
125
+
126
+ /**
127
+ * Prepare all navigation-owned state for a new page — without publishing
128
+ * any of it. Every code path that changes the "current page" must go
129
+ * through this function, which is what makes "forgot a field" impossible
130
+ * by construction.
131
+ *
132
+ * Returns the destination's `NavigationState`, which is a pure function of
133
+ * the URL and is what the incoming tree is *rendered* with, plus a
134
+ * `commit` thunk that performs the writes that make a page current:
135
+ * 1. Segment cache — update from server-provided segment metadata
136
+ * 2. Navigation state — pathname/search for usePathname/useSearchParams
137
+ * 3. History stack — store the payload for instant back/forward replay
138
+ *
139
+ * Nothing is written until `commit()` runs, and on a transitioned
140
+ * navigation it runs only once the transition is known to have won — see
141
+ * `renderViaTransition` and `NavigationRoot`. A navigation superseded
142
+ * while its payload was in flight never calls it, so the cache, the
143
+ * pathname the client reports and the history stack all keep describing
144
+ * the route still on screen (TIM-1301).
145
+ *
146
+ * Callers with state of their own to publish — the address bar, the
147
+ * client's record of the mounted tree — wrap this thunk rather than
148
+ * commit beside it, so there stays exactly one moment at which a page
149
+ * becomes current.
150
+ */
151
+ prepareNavigation(url: string, opts: NavigationCommitInput): PreparedNavigation {
152
+ const navState = deriveNavigationState(url);
153
+
154
+ return {
155
+ navState,
156
+ commit() {
157
+ if (opts.segmentInfo && opts.segmentInfo.length > 0) {
158
+ updateSegmentCache(opts.segmentInfo);
159
+ } else if (opts.segmentInfo?.length === 0 || opts.clearSegmentCacheOnEmpty) {
160
+ segmentCache.clear();
161
+ }
162
+
163
+ setNavigationState(navState);
164
+
165
+ historyStack.push(url, {
166
+ payload: opts.payload,
167
+ params: opts.params,
168
+ segmentInfo: opts.segmentInfo,
169
+ });
170
+ },
171
+ };
172
+ },
173
+ };
174
+ }