@timber-js/app 0.2.0-alpha.5 → 0.2.0-alpha.50

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 (333) hide show
  1. package/LICENSE +8 -0
  2. package/dist/_chunks/{als-registry-B7DbZ2hS.js → als-registry-Ba7URUIn.js} +1 -1
  3. package/dist/_chunks/als-registry-Ba7URUIn.js.map +1 -0
  4. package/dist/_chunks/chunk-DYhsFzuS.js +33 -0
  5. package/dist/_chunks/{debug-gwlJkDuf.js → debug-ECi_61pb.js} +2 -2
  6. package/dist/_chunks/debug-ECi_61pb.js.map +1 -0
  7. package/dist/_chunks/define-TK8C1M3x.js +279 -0
  8. package/dist/_chunks/define-TK8C1M3x.js.map +1 -0
  9. package/dist/_chunks/define-cookie-k9btcEfI.js +93 -0
  10. package/dist/_chunks/define-cookie-k9btcEfI.js.map +1 -0
  11. package/dist/_chunks/error-boundary-B9vT_YK_.js +211 -0
  12. package/dist/_chunks/error-boundary-B9vT_YK_.js.map +1 -0
  13. package/dist/_chunks/{format-DviM89f0.js → format-cX7wzEp2.js} +2 -2
  14. package/dist/_chunks/{format-DviM89f0.js.map → format-cX7wzEp2.js.map} +1 -1
  15. package/dist/_chunks/{interception-BOoWmLUA.js → interception-D2djYaIm.js} +112 -77
  16. package/dist/_chunks/interception-D2djYaIm.js.map +1 -0
  17. package/dist/_chunks/{metadata-routes-Cjmvi3rQ.js → metadata-routes-BU684ls2.js} +1 -1
  18. package/dist/_chunks/{metadata-routes-Cjmvi3rQ.js.map → metadata-routes-BU684ls2.js.map} +1 -1
  19. package/dist/_chunks/{request-context-DIkVh_jG.js → request-context-0h-6Voad.js} +95 -69
  20. package/dist/_chunks/request-context-0h-6Voad.js.map +1 -0
  21. package/dist/_chunks/segment-context-DBn-nrMN.js +69 -0
  22. package/dist/_chunks/segment-context-DBn-nrMN.js.map +1 -0
  23. package/dist/_chunks/stale-reload-4L-_skC7.js +47 -0
  24. package/dist/_chunks/stale-reload-4L-_skC7.js.map +1 -0
  25. package/dist/_chunks/{tracing-Cwn7697K.js → tracing-JI4cYUdz.js} +17 -3
  26. package/dist/_chunks/{tracing-Cwn7697K.js.map → tracing-JI4cYUdz.js.map} +1 -1
  27. package/dist/_chunks/{use-query-states-D5KaffOK.js → use-query-states-wEXY2JQB.js} +1 -1
  28. package/dist/_chunks/{use-query-states-D5KaffOK.js.map → use-query-states-wEXY2JQB.js.map} +1 -1
  29. package/dist/_chunks/wrappers-C9XPg7-U.js +63 -0
  30. package/dist/_chunks/wrappers-C9XPg7-U.js.map +1 -0
  31. package/dist/adapters/compress-module.d.ts.map +1 -1
  32. package/dist/adapters/nitro.d.ts +17 -1
  33. package/dist/adapters/nitro.d.ts.map +1 -1
  34. package/dist/adapters/nitro.js +56 -13
  35. package/dist/adapters/nitro.js.map +1 -1
  36. package/dist/cache/fast-hash.d.ts +22 -0
  37. package/dist/cache/fast-hash.d.ts.map +1 -0
  38. package/dist/cache/index.d.ts +5 -2
  39. package/dist/cache/index.d.ts.map +1 -1
  40. package/dist/cache/index.js +90 -20
  41. package/dist/cache/index.js.map +1 -1
  42. package/dist/cache/register-cached-function.d.ts.map +1 -1
  43. package/dist/cache/singleflight.d.ts +18 -1
  44. package/dist/cache/singleflight.d.ts.map +1 -1
  45. package/dist/cache/timber-cache.d.ts +1 -1
  46. package/dist/cache/timber-cache.d.ts.map +1 -1
  47. package/dist/client/error-boundary.d.ts +10 -1
  48. package/dist/client/error-boundary.d.ts.map +1 -1
  49. package/dist/client/error-boundary.js +1 -125
  50. package/dist/client/index.d.ts +3 -2
  51. package/dist/client/index.d.ts.map +1 -1
  52. package/dist/client/index.js +213 -93
  53. package/dist/client/index.js.map +1 -1
  54. package/dist/client/link.d.ts +22 -8
  55. package/dist/client/link.d.ts.map +1 -1
  56. package/dist/client/navigation-context.d.ts +2 -2
  57. package/dist/client/router.d.ts +25 -3
  58. package/dist/client/router.d.ts.map +1 -1
  59. package/dist/client/rsc-fetch.d.ts +23 -2
  60. package/dist/client/rsc-fetch.d.ts.map +1 -1
  61. package/dist/client/segment-cache.d.ts +1 -1
  62. package/dist/client/segment-cache.d.ts.map +1 -1
  63. package/dist/client/segment-context.d.ts +1 -1
  64. package/dist/client/segment-context.d.ts.map +1 -1
  65. package/dist/client/segment-merger.d.ts.map +1 -1
  66. package/dist/client/stale-reload.d.ts +15 -0
  67. package/dist/client/stale-reload.d.ts.map +1 -1
  68. package/dist/client/top-loader.d.ts +1 -1
  69. package/dist/client/top-loader.d.ts.map +1 -1
  70. package/dist/client/transition-root.d.ts +1 -1
  71. package/dist/client/transition-root.d.ts.map +1 -1
  72. package/dist/client/use-params.d.ts +2 -2
  73. package/dist/client/use-params.d.ts.map +1 -1
  74. package/dist/client/use-query-states.d.ts +1 -1
  75. package/dist/codec.d.ts +21 -0
  76. package/dist/codec.d.ts.map +1 -0
  77. package/dist/cookies/define-cookie.d.ts +33 -12
  78. package/dist/cookies/define-cookie.d.ts.map +1 -1
  79. package/dist/cookies/index.js +1 -83
  80. package/dist/fonts/css.d.ts +1 -0
  81. package/dist/fonts/css.d.ts.map +1 -1
  82. package/dist/index.d.ts +112 -35
  83. package/dist/index.d.ts.map +1 -1
  84. package/dist/index.js +467 -246
  85. package/dist/index.js.map +1 -1
  86. package/dist/params/define.d.ts +76 -0
  87. package/dist/params/define.d.ts.map +1 -0
  88. package/dist/params/index.d.ts +8 -0
  89. package/dist/params/index.d.ts.map +1 -0
  90. package/dist/params/index.js +105 -0
  91. package/dist/params/index.js.map +1 -0
  92. package/dist/plugins/adapter-build.d.ts.map +1 -1
  93. package/dist/plugins/build-manifest.d.ts.map +1 -1
  94. package/dist/plugins/client-chunks.d.ts +32 -0
  95. package/dist/plugins/client-chunks.d.ts.map +1 -0
  96. package/dist/plugins/dev-error-overlay.d.ts +26 -1
  97. package/dist/plugins/dev-error-overlay.d.ts.map +1 -1
  98. package/dist/plugins/entries.d.ts.map +1 -1
  99. package/dist/plugins/fonts.d.ts +7 -0
  100. package/dist/plugins/fonts.d.ts.map +1 -1
  101. package/dist/plugins/routing.d.ts.map +1 -1
  102. package/dist/plugins/server-bundle.d.ts.map +1 -1
  103. package/dist/plugins/static-build.d.ts.map +1 -1
  104. package/dist/routing/codegen.d.ts +2 -2
  105. package/dist/routing/codegen.d.ts.map +1 -1
  106. package/dist/routing/index.js +1 -1
  107. package/dist/routing/scanner.d.ts.map +1 -1
  108. package/dist/routing/status-file-lint.d.ts +2 -1
  109. package/dist/routing/status-file-lint.d.ts.map +1 -1
  110. package/dist/routing/types.d.ts +6 -4
  111. package/dist/routing/types.d.ts.map +1 -1
  112. package/dist/rsc-runtime/rsc.d.ts +1 -1
  113. package/dist/rsc-runtime/rsc.d.ts.map +1 -1
  114. package/dist/rsc-runtime/ssr.d.ts +12 -0
  115. package/dist/rsc-runtime/ssr.d.ts.map +1 -1
  116. package/dist/search-params/codecs.d.ts +1 -1
  117. package/dist/search-params/define.d.ts +159 -0
  118. package/dist/search-params/define.d.ts.map +1 -0
  119. package/dist/search-params/index.d.ts +4 -5
  120. package/dist/search-params/index.d.ts.map +1 -1
  121. package/dist/search-params/index.js +4 -474
  122. package/dist/search-params/registry.d.ts +1 -1
  123. package/dist/search-params/wrappers.d.ts +53 -0
  124. package/dist/search-params/wrappers.d.ts.map +1 -0
  125. package/dist/server/access-gate.d.ts +4 -0
  126. package/dist/server/access-gate.d.ts.map +1 -1
  127. package/dist/server/action-client.d.ts.map +1 -1
  128. package/dist/server/action-encryption.d.ts +76 -0
  129. package/dist/server/action-encryption.d.ts.map +1 -0
  130. package/dist/server/action-handler.d.ts.map +1 -1
  131. package/dist/server/als-registry.d.ts +18 -4
  132. package/dist/server/als-registry.d.ts.map +1 -1
  133. package/dist/server/build-manifest.d.ts +2 -2
  134. package/dist/server/debug.d.ts +1 -1
  135. package/dist/server/default-logger.d.ts +22 -0
  136. package/dist/server/default-logger.d.ts.map +1 -0
  137. package/dist/server/deny-renderer.d.ts.map +1 -1
  138. package/dist/server/early-hints.d.ts +13 -5
  139. package/dist/server/early-hints.d.ts.map +1 -1
  140. package/dist/server/error-boundary-wrapper.d.ts +4 -0
  141. package/dist/server/error-boundary-wrapper.d.ts.map +1 -1
  142. package/dist/server/flight-injection-state.d.ts +66 -0
  143. package/dist/server/flight-injection-state.d.ts.map +1 -0
  144. package/dist/server/flight-scripts.d.ts +39 -0
  145. package/dist/server/flight-scripts.d.ts.map +1 -0
  146. package/dist/server/flush.d.ts.map +1 -1
  147. package/dist/server/form-data.d.ts +29 -0
  148. package/dist/server/form-data.d.ts.map +1 -1
  149. package/dist/server/html-injectors.d.ts +51 -11
  150. package/dist/server/html-injectors.d.ts.map +1 -1
  151. package/dist/server/index.d.ts +4 -2
  152. package/dist/server/index.d.ts.map +1 -1
  153. package/dist/server/index.js +1974 -1648
  154. package/dist/server/index.js.map +1 -1
  155. package/dist/server/logger.d.ts +24 -7
  156. package/dist/server/logger.d.ts.map +1 -1
  157. package/dist/server/node-stream-transforms.d.ts +113 -0
  158. package/dist/server/node-stream-transforms.d.ts.map +1 -0
  159. package/dist/server/pipeline.d.ts +7 -4
  160. package/dist/server/pipeline.d.ts.map +1 -1
  161. package/dist/server/primitives.d.ts +30 -3
  162. package/dist/server/primitives.d.ts.map +1 -1
  163. package/dist/server/render-timeout.d.ts +51 -0
  164. package/dist/server/render-timeout.d.ts.map +1 -0
  165. package/dist/server/request-context.d.ts +65 -38
  166. package/dist/server/request-context.d.ts.map +1 -1
  167. package/dist/server/route-element-builder.d.ts +7 -0
  168. package/dist/server/route-element-builder.d.ts.map +1 -1
  169. package/dist/server/route-handler.d.ts.map +1 -1
  170. package/dist/server/route-matcher.d.ts +2 -2
  171. package/dist/server/route-matcher.d.ts.map +1 -1
  172. package/dist/server/rsc-entry/error-renderer.d.ts.map +1 -1
  173. package/dist/server/rsc-entry/helpers.d.ts +46 -3
  174. package/dist/server/rsc-entry/helpers.d.ts.map +1 -1
  175. package/dist/server/rsc-entry/index.d.ts +6 -1
  176. package/dist/server/rsc-entry/index.d.ts.map +1 -1
  177. package/dist/server/rsc-entry/rsc-payload.d.ts.map +1 -1
  178. package/dist/server/rsc-entry/rsc-stream.d.ts +9 -0
  179. package/dist/server/rsc-entry/rsc-stream.d.ts.map +1 -1
  180. package/dist/server/rsc-entry/ssr-renderer.d.ts.map +1 -1
  181. package/dist/server/slot-resolver.d.ts +1 -1
  182. package/dist/server/slot-resolver.d.ts.map +1 -1
  183. package/dist/server/ssr-entry.d.ts +22 -0
  184. package/dist/server/ssr-entry.d.ts.map +1 -1
  185. package/dist/server/ssr-render.d.ts +39 -21
  186. package/dist/server/ssr-render.d.ts.map +1 -1
  187. package/dist/server/ssr-wrappers.d.ts +50 -0
  188. package/dist/server/ssr-wrappers.d.ts.map +1 -0
  189. package/dist/server/tracing.d.ts +10 -0
  190. package/dist/server/tracing.d.ts.map +1 -1
  191. package/dist/server/tree-builder.d.ts +19 -12
  192. package/dist/server/tree-builder.d.ts.map +1 -1
  193. package/dist/server/types.d.ts +1 -3
  194. package/dist/server/types.d.ts.map +1 -1
  195. package/dist/server/version-skew.d.ts +61 -0
  196. package/dist/server/version-skew.d.ts.map +1 -0
  197. package/dist/server/waituntil-bridge.d.ts.map +1 -1
  198. package/dist/shared/merge-search-params.d.ts +22 -0
  199. package/dist/shared/merge-search-params.d.ts.map +1 -0
  200. package/dist/shims/navigation-client.d.ts +1 -1
  201. package/dist/shims/navigation-client.d.ts.map +1 -1
  202. package/dist/shims/navigation.d.ts +1 -1
  203. package/dist/shims/navigation.d.ts.map +1 -1
  204. package/dist/utils/state-machine.d.ts +80 -0
  205. package/dist/utils/state-machine.d.ts.map +1 -0
  206. package/package.json +17 -14
  207. package/src/adapters/compress-module.ts +24 -4
  208. package/src/adapters/nitro.ts +58 -9
  209. package/src/cache/fast-hash.ts +34 -0
  210. package/src/cache/index.ts +5 -2
  211. package/src/cache/register-cached-function.ts +7 -3
  212. package/src/cache/singleflight.ts +62 -4
  213. package/src/cache/timber-cache.ts +40 -29
  214. package/src/cli.ts +0 -0
  215. package/src/client/browser-entry.ts +133 -93
  216. package/src/client/error-boundary.tsx +18 -1
  217. package/src/client/index.ts +10 -1
  218. package/src/client/link.tsx +78 -19
  219. package/src/client/navigation-context.ts +2 -2
  220. package/src/client/router.ts +105 -60
  221. package/src/client/rsc-fetch.ts +63 -2
  222. package/src/client/segment-cache.ts +1 -1
  223. package/src/client/segment-context.ts +6 -1
  224. package/src/client/segment-merger.ts +2 -8
  225. package/src/client/stale-reload.ts +32 -6
  226. package/src/client/top-loader.tsx +10 -9
  227. package/src/client/transition-root.tsx +7 -1
  228. package/src/client/use-params.ts +3 -3
  229. package/src/client/use-query-states.ts +1 -1
  230. package/src/codec.ts +21 -0
  231. package/src/cookies/define-cookie.ts +69 -18
  232. package/src/fonts/css.ts +2 -1
  233. package/src/index.ts +280 -85
  234. package/src/params/define.ts +260 -0
  235. package/src/params/index.ts +28 -0
  236. package/src/plugins/adapter-build.ts +6 -0
  237. package/src/plugins/build-manifest.ts +11 -0
  238. package/src/plugins/client-chunks.ts +65 -0
  239. package/src/plugins/dev-error-overlay.ts +70 -1
  240. package/src/plugins/dev-server.ts +38 -4
  241. package/src/plugins/entries.ts +5 -7
  242. package/src/plugins/fonts.ts +93 -42
  243. package/src/plugins/routing.ts +40 -14
  244. package/src/plugins/server-bundle.ts +32 -1
  245. package/src/plugins/shims.ts +1 -1
  246. package/src/plugins/static-build.ts +8 -4
  247. package/src/routing/codegen.ts +109 -88
  248. package/src/routing/scanner.ts +55 -6
  249. package/src/routing/status-file-lint.ts +2 -1
  250. package/src/routing/types.ts +7 -4
  251. package/src/rsc-runtime/rsc.ts +2 -0
  252. package/src/rsc-runtime/ssr.ts +50 -0
  253. package/src/rsc-runtime/vendor-types.d.ts +7 -0
  254. package/src/search-params/codecs.ts +1 -1
  255. package/src/search-params/define.ts +518 -0
  256. package/src/search-params/index.ts +12 -18
  257. package/src/search-params/registry.ts +1 -1
  258. package/src/search-params/wrappers.ts +85 -0
  259. package/src/server/access-gate.tsx +40 -9
  260. package/src/server/action-client.ts +7 -1
  261. package/src/server/action-encryption.ts +144 -0
  262. package/src/server/action-handler.ts +19 -2
  263. package/src/server/als-registry.ts +18 -4
  264. package/src/server/build-manifest.ts +4 -4
  265. package/src/server/compress.ts +25 -7
  266. package/src/server/debug.ts +1 -1
  267. package/src/server/default-logger.ts +98 -0
  268. package/src/server/deny-renderer.ts +2 -1
  269. package/src/server/early-hints.ts +36 -15
  270. package/src/server/error-boundary-wrapper.ts +57 -14
  271. package/src/server/flight-injection-state.ts +113 -0
  272. package/src/server/flight-scripts.ts +59 -0
  273. package/src/server/flush.ts +2 -1
  274. package/src/server/form-data.ts +76 -0
  275. package/src/server/html-injectors.ts +261 -117
  276. package/src/server/index.ts +9 -4
  277. package/src/server/logger.ts +38 -35
  278. package/src/server/node-stream-transforms.ts +504 -0
  279. package/src/server/pipeline.ts +131 -39
  280. package/src/server/primitives.ts +47 -5
  281. package/src/server/render-timeout.ts +108 -0
  282. package/src/server/request-context.ts +119 -119
  283. package/src/server/route-element-builder.ts +106 -114
  284. package/src/server/route-handler.ts +2 -1
  285. package/src/server/route-matcher.ts +2 -2
  286. package/src/server/rsc-entry/error-renderer.ts +5 -3
  287. package/src/server/rsc-entry/helpers.ts +122 -3
  288. package/src/server/rsc-entry/index.ts +108 -43
  289. package/src/server/rsc-entry/rsc-payload.ts +52 -12
  290. package/src/server/rsc-entry/rsc-stream.ts +49 -12
  291. package/src/server/rsc-entry/ssr-renderer.ts +40 -13
  292. package/src/server/slot-resolver.ts +222 -217
  293. package/src/server/ssr-entry.ts +209 -30
  294. package/src/server/ssr-render.ts +289 -67
  295. package/src/server/ssr-wrappers.tsx +139 -0
  296. package/src/server/tracing.ts +23 -0
  297. package/src/server/tree-builder.ts +91 -57
  298. package/src/server/types.ts +1 -3
  299. package/src/server/version-skew.ts +104 -0
  300. package/src/server/waituntil-bridge.ts +4 -1
  301. package/src/shared/merge-search-params.ts +48 -0
  302. package/src/shims/navigation-client.ts +1 -1
  303. package/src/shims/navigation.ts +1 -1
  304. package/src/utils/state-machine.ts +111 -0
  305. package/dist/_chunks/als-registry-B7DbZ2hS.js.map +0 -1
  306. package/dist/_chunks/debug-gwlJkDuf.js.map +0 -1
  307. package/dist/_chunks/interception-BOoWmLUA.js.map +0 -1
  308. package/dist/_chunks/request-context-DIkVh_jG.js.map +0 -1
  309. package/dist/_chunks/ssr-data-MjmprTmO.js +0 -88
  310. package/dist/_chunks/ssr-data-MjmprTmO.js.map +0 -1
  311. package/dist/_chunks/use-cookie-DX-l1_5E.js +0 -91
  312. package/dist/_chunks/use-cookie-DX-l1_5E.js.map +0 -1
  313. package/dist/client/error-boundary.js.map +0 -1
  314. package/dist/cookies/index.js.map +0 -1
  315. package/dist/plugins/dynamic-transform.d.ts +0 -72
  316. package/dist/plugins/dynamic-transform.d.ts.map +0 -1
  317. package/dist/search-params/analyze.d.ts +0 -54
  318. package/dist/search-params/analyze.d.ts.map +0 -1
  319. package/dist/search-params/builtin-codecs.d.ts +0 -105
  320. package/dist/search-params/builtin-codecs.d.ts.map +0 -1
  321. package/dist/search-params/create.d.ts +0 -106
  322. package/dist/search-params/create.d.ts.map +0 -1
  323. package/dist/search-params/index.js.map +0 -1
  324. package/dist/server/prerender.d.ts +0 -77
  325. package/dist/server/prerender.d.ts.map +0 -1
  326. package/dist/server/response-cache.d.ts +0 -53
  327. package/dist/server/response-cache.d.ts.map +0 -1
  328. package/src/plugins/dynamic-transform.ts +0 -161
  329. package/src/search-params/analyze.ts +0 -192
  330. package/src/search-params/builtin-codecs.ts +0 -228
  331. package/src/search-params/create.ts +0 -321
  332. package/src/server/prerender.ts +0 -139
  333. package/src/server/response-cache.ts +0 -277
@@ -6,13 +6,13 @@ import type { SegmentInfo } from './segment-cache';
6
6
  import { HistoryStack } from './history';
7
7
  import type { HeadElement } from './head';
8
8
  import { setCurrentParams } from './use-params.js';
9
- import { setNavigationState } from './navigation-context.js';
10
9
  import {
11
- SegmentElementCache,
12
- cacheSegmentElements,
13
- mergeSegmentTree,
14
- } from './segment-merger.js';
15
- import { fetchRscPayload, RedirectError } from './rsc-fetch.js';
10
+ setNavigationState,
11
+ getNavigationState,
12
+ type NavigationState,
13
+ } from './navigation-context.js';
14
+ import { SegmentElementCache, cacheSegmentElements, mergeSegmentTree } from './segment-merger.js';
15
+ import { fetchRscPayload, RedirectError, VersionSkewError } from './rsc-fetch.js';
16
16
  import type { FetchResult } from './rsc-fetch.js';
17
17
 
18
18
  // ─── Types ───────────────────────────────────────────────────────
@@ -35,8 +35,12 @@ export type RscDecoder = (fetchPromise: Promise<Response>) => unknown;
35
35
  * Function that renders a decoded RSC element tree into the DOM.
36
36
  * In production: reactRoot.render(element).
37
37
  * In tests: a no-op or mock.
38
+ *
39
+ * Receives the current NavigationState explicitly — no temporal
40
+ * coupling with setNavigationState/getNavigationState. The renderer
41
+ * wraps the element in NavigationProvider with this state.
38
42
  */
39
- export type RootRenderer = (element: unknown) => void;
43
+ export type RootRenderer = (element: unknown, navState: NavigationState) => void;
40
44
 
41
45
  /**
42
46
  * Platform dependencies injected for testability. In production these
@@ -68,13 +72,17 @@ export interface RouterDeps {
68
72
  *
69
73
  * The `perform` callback receives a `wrapPayload` function to wrap the
70
74
  * decoded RSC payload with NavigationProvider + NuqsAdapter before
71
- * TransitionRoot sets it as the new element.
75
+ * TransitionRoot sets it as the new element. The `wrapPayload` function
76
+ * receives the NavigationState explicitly — no temporal coupling with
77
+ * getNavigationState().
72
78
  *
73
79
  * If not provided (tests), the router falls back to renderRoot.
74
80
  */
75
81
  navigateTransition?: (
76
82
  pendingUrl: string,
77
- perform: (wrapPayload: (payload: unknown) => unknown) => Promise<unknown>
83
+ perform: (
84
+ wrapPayload: (payload: unknown, navState: NavigationState) => unknown
85
+ ) => Promise<unknown>
78
86
  ) => Promise<void>;
79
87
  }
80
88
 
@@ -134,21 +142,40 @@ function isAbortError(error: unknown): boolean {
134
142
  * Create a router instance. In production, called once at app hydration
135
143
  * with real browser APIs. In tests, called with mock dependencies.
136
144
  */
145
+ /**
146
+ * Router navigation phase — discriminated union replacing scattered
147
+ * `pending` + `pendingUrl` boolean flags.
148
+ *
149
+ * - `idle`: No navigation in flight. The committed params/pathname
150
+ * are current.
151
+ * - `navigating`: A fetch or render is in progress. `targetUrl` is
152
+ * the destination being navigated to.
153
+ */
154
+ export type RouterPhase = { phase: 'idle' } | { phase: 'navigating'; targetUrl: string };
155
+
137
156
  export function createRouter(deps: RouterDeps): RouterInstance {
138
157
  const segmentCache = new SegmentCache();
139
158
  const prefetchCache = new PrefetchCache();
140
159
  const historyStack = new HistoryStack();
141
160
  const segmentElementCache = new SegmentElementCache();
142
161
 
143
- let pending = false;
144
- let pendingUrl: string | null = null;
162
+ let routerPhase: RouterPhase = { phase: 'idle' };
145
163
  const pendingListeners = new Set<(pending: boolean) => void>();
146
164
 
147
165
  function setPending(value: boolean, url?: string): void {
148
- const newPendingUrl = value && url ? url : null;
149
- if (pending === value && pendingUrl === newPendingUrl) return;
150
- pending = value;
151
- pendingUrl = newPendingUrl;
166
+ const next: RouterPhase =
167
+ value && url ? { phase: 'navigating', targetUrl: url } : { phase: 'idle' };
168
+ // Skip no-op updates
169
+ if (
170
+ routerPhase.phase === next.phase &&
171
+ (routerPhase.phase === 'idle' ||
172
+ (routerPhase.phase === 'navigating' &&
173
+ next.phase === 'navigating' &&
174
+ routerPhase.targetUrl === next.targetUrl))
175
+ ) {
176
+ return;
177
+ }
178
+ routerPhase = next;
152
179
  // Notify external store listeners (non-React consumers).
153
180
  // React-facing pending state is handled by useOptimistic in
154
181
  // TransitionRoot via navigateTransition — not this function.
@@ -167,9 +194,9 @@ export function createRouter(deps: RouterDeps): RouterInstance {
167
194
  }
168
195
 
169
196
  /** Render a decoded RSC payload into the DOM if a renderer is available. */
170
- function renderPayload(payload: unknown): void {
197
+ function renderPayload(payload: unknown, navState: NavigationState): void {
171
198
  if (deps.renderRoot) {
172
- deps.renderRoot(payload);
199
+ deps.renderRoot(payload, navState);
173
200
  }
174
201
  }
175
202
 
@@ -198,32 +225,34 @@ export function createRouter(deps: RouterDeps): RouterInstance {
198
225
  /**
199
226
  * Update navigation state (params + pathname) for the next render.
200
227
  *
201
- * Sets both the module-level fallback (for tests and SSR) and the
202
- * navigation context state (read by renderRoot to wrap the element
203
- * in NavigationProvider). The context update is atomic with the tree
204
- * render both are passed to reactRoot.render() in the same call.
228
+ * Sets the module-level fallback (for tests and SSR) and the
229
+ * globalThis bridge, then returns the NavigationState so callers
230
+ * can pass it explicitly to renderRoot/wrapPayload eliminating
231
+ * temporal coupling with getNavigationState().
205
232
  */
206
233
  function updateNavigationState(
207
234
  params: Record<string, string | string[]> | null | undefined,
208
235
  url: string
209
- ): void {
236
+ ): NavigationState {
210
237
  const resolvedParams = params ?? {};
211
238
  // Module-level fallback for tests (no NavigationProvider) and SSR
212
239
  setCurrentParams(resolvedParams);
213
- // Navigation contextread by renderRoot to wrap the RSC element
240
+ // globalThis bridgekept for backward compat
214
241
  const pathname = url.startsWith('http') ? new URL(url).pathname : url.split('?')[0] || '/';
215
- setNavigationState({ params: resolvedParams, pathname });
242
+ const navState: NavigationState = { params: resolvedParams, pathname };
243
+ setNavigationState(navState);
244
+ return navState;
216
245
  }
217
246
 
218
247
  /**
219
248
  * Render a payload via navigateTransition (production) or renderRoot (tests).
220
- * The perform callback should fetch data, update state, and return the payload.
221
- * In production, the entire callback runs inside a React transition with
222
- * useOptimistic for the pending URL. In tests, the payload is rendered directly.
249
+ * The perform callback should fetch data, update state, and return the
250
+ * FetchResult plus the NavigationState (so it can be passed explicitly
251
+ * to wrapPayload/renderRoot without temporal coupling).
223
252
  */
224
253
  async function renderViaTransition(
225
254
  url: string,
226
- perform: () => Promise<FetchResult>
255
+ perform: () => Promise<FetchResult & { navState: NavigationState }>
227
256
  ): Promise<HeadElement[] | null> {
228
257
  if (deps.navigateTransition) {
229
258
  let headElements: HeadElement[] | null = null;
@@ -239,7 +268,9 @@ export function createRouter(deps: RouterDeps): RouterInstance {
239
268
  headElements: result.headElements,
240
269
  params: result.params,
241
270
  });
242
- return wrapPayload(merged);
271
+ // Pass navState explicitly — wrapPayload wraps element in
272
+ // NavigationProvider with this state, no getNavigationState() needed.
273
+ return wrapPayload(merged, result.navState);
243
274
  });
244
275
  return headElements;
245
276
  }
@@ -253,7 +284,7 @@ export function createRouter(deps: RouterDeps): RouterInstance {
253
284
  headElements: result.headElements,
254
285
  params: result.params,
255
286
  });
256
- renderPayload(merged);
287
+ renderPayload(merged, result.navState);
257
288
  return result.headElements;
258
289
  }
259
290
 
@@ -273,6 +304,17 @@ export function createRouter(deps: RouterDeps): RouterInstance {
273
304
  }
274
305
  }
275
306
 
307
+ /**
308
+ * Schedule scroll restoration after the next paint and fire the
309
+ * scroll-restored event. Used by navigate, popstate, and refresh.
310
+ */
311
+ function restoreScrollAfterPaint(scrollY: number): void {
312
+ afterPaint(() => {
313
+ deps.scrollTo(0, scrollY);
314
+ window.dispatchEvent(new Event('timber:scroll-restored'));
315
+ });
316
+ }
317
+
276
318
  /**
277
319
  * Core navigation logic shared between the transition and fallback paths.
278
320
  * Fetches the RSC payload, updates all state, and returns the result.
@@ -280,7 +322,7 @@ export function createRouter(deps: RouterDeps): RouterInstance {
280
322
  async function performNavigationFetch(
281
323
  url: string,
282
324
  options: { replace: boolean }
283
- ): Promise<FetchResult> {
325
+ ): Promise<FetchResult & { navState: NavigationState }> {
284
326
  // Check prefetch cache first. PrefetchResult has optional segmentInfo/params
285
327
  // fields — normalize to null for FetchResult compatibility.
286
328
  const prefetched = prefetchCache.consume(url);
@@ -320,10 +362,10 @@ export function createRouter(deps: RouterDeps): RouterInstance {
320
362
  // Update the segment cache with the new route's segment tree.
321
363
  updateSegmentCache(result.segmentInfo);
322
364
 
323
- // Update navigation state (params + pathname) before rendering.
324
- updateNavigationState(result.params, url);
365
+ // Update navigation state and capture it for explicit passing.
366
+ const navState = updateNavigationState(result.params, url);
325
367
 
326
- return result;
368
+ return { ...result, navState };
327
369
  }
328
370
 
329
371
  async function navigate(url: string, options: NavigationOptions = {}): Promise<void> {
@@ -354,15 +396,18 @@ export function createRouter(deps: RouterDeps): RouterInstance {
354
396
  // Scroll-to-top on forward navigation, or restore captured position
355
397
  // for scroll={false}. React's render() on the document root can reset
356
398
  // scroll during DOM reconciliation, so all scroll must be actively managed.
357
- afterPaint(() => {
358
- if (scroll) {
359
- deps.scrollTo(0, 0);
360
- } else {
361
- deps.scrollTo(0, currentScrollY);
362
- }
363
- window.dispatchEvent(new Event('timber:scroll-restored'));
364
- });
399
+ restoreScrollAfterPaint(scroll ? 0 : currentScrollY);
365
400
  } catch (error) {
401
+ // Version skew — server has been redeployed. Trigger full page reload
402
+ // so the browser fetches the new bundle. See TIM-446.
403
+ if (error instanceof VersionSkewError) {
404
+ // Import triggerStaleReload dynamically to avoid circular deps
405
+ // and keep the reload logic centralized with its loop guard.
406
+ const { triggerStaleReload } = await import('./stale-reload.js');
407
+ triggerStaleReload();
408
+ // Return a never-resolving promise — page is reloading.
409
+ return new Promise(() => {}) as never;
410
+ }
366
411
  // Server-side redirect during RSC fetch → soft router navigation.
367
412
  if (error instanceof RedirectError) {
368
413
  setPending(false);
@@ -388,8 +433,8 @@ export function createRouter(deps: RouterDeps): RouterInstance {
388
433
  const result = await fetchRscPayload(currentUrl, deps);
389
434
  // History push handled by renderViaTransition (stores merged payload)
390
435
  updateSegmentCache(result.segmentInfo);
391
- updateNavigationState(result.params, currentUrl);
392
- return result;
436
+ const navState = updateNavigationState(result.params, currentUrl);
437
+ return { ...result, navState };
393
438
  });
394
439
 
395
440
  applyHead(headElements);
@@ -406,13 +451,10 @@ export function createRouter(deps: RouterDeps): RouterInstance {
406
451
 
407
452
  if (entry && entry.payload !== null) {
408
453
  // Replay cached payload — no server roundtrip
409
- updateNavigationState(entry.params, url);
410
- renderPayload(entry.payload);
454
+ const navState = updateNavigationState(entry.params, url);
455
+ renderPayload(entry.payload, navState);
411
456
  applyHead(entry.headElements);
412
- afterPaint(() => {
413
- deps.scrollTo(0, scrollY);
414
- window.dispatchEvent(new Event('timber:scroll-restored'));
415
- });
457
+ restoreScrollAfterPaint(scrollY);
416
458
  } else {
417
459
  // No cached payload — fetch from server.
418
460
  // This happens when navigating back to the initial SSR'd page
@@ -421,19 +463,18 @@ export function createRouter(deps: RouterDeps): RouterInstance {
421
463
  setPending(true, url);
422
464
  try {
423
465
  const headElements = await renderViaTransition(url, async () => {
424
- const stateTree = segmentCache.serializeStateTree(segmentElementCache.getMergeablePaths());
466
+ const stateTree = segmentCache.serializeStateTree(
467
+ segmentElementCache.getMergeablePaths()
468
+ );
425
469
  const result = await fetchRscPayload(url, deps, stateTree);
426
470
  updateSegmentCache(result.segmentInfo);
427
- updateNavigationState(result.params, url);
471
+ const navState = updateNavigationState(result.params, url);
428
472
  // History push handled by renderViaTransition (stores merged payload)
429
- return result;
473
+ return { ...result, navState };
430
474
  });
431
475
 
432
476
  applyHead(headElements);
433
- afterPaint(() => {
434
- deps.scrollTo(0, scrollY);
435
- window.dispatchEvent(new Event('timber:scroll-restored'));
436
- });
477
+ restoreScrollAfterPaint(scrollY);
437
478
  } finally {
438
479
  setPending(false);
439
480
  }
@@ -465,8 +506,8 @@ export function createRouter(deps: RouterDeps): RouterInstance {
465
506
  navigate,
466
507
  refresh,
467
508
  handlePopState,
468
- isPending: () => pending,
469
- getPendingUrl: () => pendingUrl,
509
+ isPending: () => routerPhase.phase === 'navigating',
510
+ getPendingUrl: () => (routerPhase.phase === 'navigating' ? routerPhase.targetUrl : null),
470
511
  onPendingChange(listener) {
471
512
  pendingListeners.add(listener);
472
513
  return () => pendingListeners.delete(listener);
@@ -483,7 +524,11 @@ export function createRouter(deps: RouterDeps): RouterInstance {
483
524
  payload: merged,
484
525
  headElements,
485
526
  });
486
- renderPayload(merged);
527
+ // Revalidation doesn't change params/pathname — preserve current state.
528
+ // DO NOT call updateNavigationState(null, ...) here: that normalizes
529
+ // params to {}, clearing dynamic route params on every action response.
530
+ const navState = getNavigationState();
531
+ renderPayload(merged, navState);
487
532
  applyHead(headElements);
488
533
  },
489
534
  initSegmentCache: (segments: SegmentInfo[]) => updateSegmentCache(segments),
@@ -23,7 +23,7 @@ export interface FetchResult {
23
23
  headElements: HeadElement[] | null;
24
24
  /** Segment metadata from X-Timber-Segments header for populating the segment cache. */
25
25
  segmentInfo: SegmentInfo[] | null;
26
- /** Route params from X-Timber-Params header for populating useParams(). */
26
+ /** Route params from X-Timber-Params header for populating useSegmentParams(). */
27
27
  params: Record<string, string | string[]> | null;
28
28
  /** Segment paths that were skipped by the server (for client-side merging). */
29
29
  skippedSegments: string[] | null;
@@ -58,6 +58,43 @@ function appendRscParam(url: string): string {
58
58
  return `${url}${separator}_rsc=${generateCacheBustId()}`;
59
59
  }
60
60
 
61
+ // ─── Deployment ID ───────────────────────────────────────────────
62
+
63
+ /**
64
+ * The client's deployment ID, set at bootstrap from the runtime config.
65
+ * Sent with every RSC/action request for version skew detection.
66
+ * Null in dev mode. See TIM-446.
67
+ */
68
+ let clientDeploymentId: string | null = null;
69
+
70
+ /** Set the client deployment ID. Called once at bootstrap. */
71
+ export function setClientDeploymentId(id: string | null): void {
72
+ clientDeploymentId = id;
73
+ }
74
+
75
+ /** Get the client deployment ID. */
76
+ export function getClientDeploymentId(): string | null {
77
+ return clientDeploymentId;
78
+ }
79
+
80
+ // ─── Reload Signal ───────────────────────────────────────────────
81
+
82
+ /** Header name used by the server to signal a version skew reload. */
83
+ export const RELOAD_HEADER = 'X-Timber-Reload';
84
+
85
+ /** Header name for the client's deployment ID. */
86
+ export const DEPLOYMENT_ID_HEADER = 'X-Timber-Deployment-Id';
87
+
88
+ /**
89
+ * Check if a response signals a version skew reload.
90
+ * Triggers a full page reload if the server indicates the client is stale.
91
+ */
92
+ export function checkReloadSignal(response: Response): boolean {
93
+ return response.headers.get(RELOAD_HEADER) === '1';
94
+ }
95
+
96
+ // ─── Header Builder ──────────────────────────────────────────────
97
+
61
98
  export function buildRscHeaders(
62
99
  stateTree: { segments: string[] } | undefined,
63
100
  currentUrl?: string
@@ -75,6 +112,13 @@ export function buildRscHeaders(
75
112
  if (currentUrl) {
76
113
  headers['X-Timber-URL'] = currentUrl;
77
114
  }
115
+ // Send deployment ID for version skew detection (TIM-446).
116
+ // The server compares this against the current build's ID.
117
+ // On mismatch, the server signals a reload instead of returning
118
+ // an RSC payload with mismatched module references.
119
+ if (clientDeploymentId) {
120
+ headers[DEPLOYMENT_ID_HEADER] = clientDeploymentId;
121
+ }
78
122
  return headers;
79
123
  }
80
124
 
@@ -135,7 +179,7 @@ export function extractSkippedSegments(response: Response): string[] | null {
135
179
  * Extract route params from the X-Timber-Params response header.
136
180
  * Returns null if the header is missing or malformed.
137
181
  *
138
- * Used to populate useParams() after client-side navigation.
182
+ * Used to populate useSegmentParams() after client-side navigation.
139
183
  */
140
184
  export function extractParams(response: Response): Record<string, string | string[]> | null {
141
185
  const header = response.headers.get('X-Timber-Params');
@@ -161,6 +205,17 @@ export class RedirectError extends Error {
161
205
  }
162
206
  }
163
207
 
208
+ /**
209
+ * Thrown when the server signals a version skew (X-Timber-Reload header).
210
+ * Caught in navigate() to trigger a full page reload via triggerStaleReload().
211
+ * See TIM-446.
212
+ */
213
+ export class VersionSkewError extends Error {
214
+ constructor() {
215
+ super('Version skew detected — server has been redeployed');
216
+ }
217
+ }
218
+
164
219
  // ─── Fetch ───────────────────────────────────────────────────────
165
220
 
166
221
  /**
@@ -192,6 +247,12 @@ export async function fetchRscPayload(
192
247
  let params: Record<string, string | string[]> | null = null;
193
248
  let skippedSegments: string[] | null = null;
194
249
  const wrappedPromise = fetchPromise.then((response) => {
250
+ // Version skew detection (TIM-446): if the server signals a reload,
251
+ // throw VersionSkewError so the caller (router navigate) can trigger
252
+ // a full page reload via triggerStaleReload().
253
+ if (checkReloadSignal(response)) {
254
+ throw new VersionSkewError();
255
+ }
195
256
  // Detect server-side redirects. The server returns 204 + X-Timber-Redirect
196
257
  // for RSC payload requests instead of a raw 302, because fetch with
197
258
  // redirect: "manual" turns 302s into opaque redirects (status 0, null body)
@@ -11,7 +11,7 @@ export interface PrefetchResult {
11
11
  headElements: HeadElement[] | null;
12
12
  /** Segment metadata from X-Timber-Segments header for populating the segment cache. */
13
13
  segmentInfo?: SegmentInfo[] | null;
14
- /** Route params from X-Timber-Params header for populating useParams(). */
14
+ /** Route params from X-Timber-Params header for populating useSegmentParams(). */
15
15
  params?: Record<string, string | string[]> | null;
16
16
  /** Segment paths skipped by the server (for client-side merging). */
17
17
  skippedSegments?: string[] | null;
@@ -52,7 +52,12 @@ interface SegmentProviderProps {
52
52
  * Wraps each layout to provide segment position context.
53
53
  * Injected by rsc-entry.ts during element tree construction.
54
54
  */
55
- export function SegmentProvider({ segments, segmentId: _segmentId, parallelRouteKeys, children }: SegmentProviderProps) {
55
+ export function SegmentProvider({
56
+ segments,
57
+ segmentId: _segmentId,
58
+ parallelRouteKeys,
59
+ children,
60
+ }: SegmentProviderProps) {
56
61
  const value = useMemo(
57
62
  () => ({ segments, parallelRouteKeys }),
58
63
  // segments and parallelRouteKeys are static per layout — they don't change
@@ -186,10 +186,7 @@ function walkChildren(children: ReactNode, out: CachedSegmentEntry[]): void {
186
186
  * Cache all segment subtrees from a fully-rendered RSC element tree.
187
187
  * Call this after every full RSC payload render (navigate, refresh, hydration).
188
188
  */
189
- export function cacheSegmentElements(
190
- element: unknown,
191
- cache: SegmentElementCache
192
- ): void {
189
+ export function cacheSegmentElements(element: unknown, cache: SegmentElementCache): void {
193
190
  const segments = extractSegments(element);
194
191
  for (const entry of segments) {
195
192
  cache.set(entry.segmentPath, entry);
@@ -208,10 +205,7 @@ export function cacheSegmentElements(
208
205
  */
209
206
  type TreePath = Array<{ element: ReactElement; childIndex: number }>;
210
207
 
211
- function findSegmentProviderPath(
212
- node: ReactElement,
213
- targetPath?: string
214
- ): TreePath | null {
208
+ function findSegmentProviderPath(node: ReactElement, targetPath?: string): TreePath | null {
215
209
  const children = (node.props as { children?: ReactNode }).children;
216
210
  if (children == null) return null;
217
211
 
@@ -32,6 +32,34 @@ export function isStaleClientReference(error: unknown): boolean {
32
32
  return msg.includes('Could not find the module') || msg.includes('client reference not found');
33
33
  }
34
34
 
35
+ /**
36
+ * Check if an error is a chunk load failure from a dynamic import.
37
+ *
38
+ * After a deployment, old chunk filenames no longer exist. When the client
39
+ * tries to dynamically import a chunk that's been replaced, the browser
40
+ * throws one of these errors:
41
+ *
42
+ * - Chromium: "Failed to fetch dynamically imported module: <url>"
43
+ * - Firefox: "error loading dynamically imported module: <url>"
44
+ * - Safari: "Importing a module script failed."
45
+ * - Vite/Rollup: "Unable to preload CSS for <url>"
46
+ *
47
+ * See TIM-446
48
+ */
49
+ export function isChunkLoadError(error: unknown): boolean {
50
+ if (!(error instanceof Error)) return false;
51
+ const msg = error.message.toLowerCase();
52
+ return (
53
+ msg.includes('failed to fetch dynamically imported module') ||
54
+ msg.includes('error loading dynamically imported module') ||
55
+ msg.includes('importing a module script failed') ||
56
+ msg.includes('unable to preload css') ||
57
+ // Webpack-style chunk load errors (unlikely in Vite but defensive)
58
+ msg.includes('loading chunk') ||
59
+ msg.includes('loading css chunk')
60
+ );
61
+ }
62
+
35
63
  /**
36
64
  * Trigger a full page reload to pick up new bundles.
37
65
  *
@@ -48,8 +76,8 @@ export function triggerStaleReload(): boolean {
48
76
  if (sessionStorage.getItem(RELOAD_FLAG_KEY)) {
49
77
  console.warn(
50
78
  '[timber] Stale client reference detected again after reload. ' +
51
- 'Not reloading to prevent infinite loop. ' +
52
- 'This may indicate a deployment issue — try a hard refresh.'
79
+ 'Not reloading to prevent infinite loop. ' +
80
+ 'This may indicate a deployment issue — try a hard refresh.'
53
81
  );
54
82
  return false;
55
83
  }
@@ -59,7 +87,7 @@ export function triggerStaleReload(): boolean {
59
87
 
60
88
  console.warn(
61
89
  '[timber] Stale client reference detected — the server has been ' +
62
- 'redeployed with new bundles. Reloading to pick up the new version.'
90
+ 'redeployed with new bundles. Reloading to pick up the new version.'
63
91
  );
64
92
 
65
93
  window.location.reload();
@@ -67,9 +95,7 @@ export function triggerStaleReload(): boolean {
67
95
  } catch {
68
96
  // sessionStorage may be unavailable (private browsing, storage full, etc.)
69
97
  // Fall back to reloading without loop protection
70
- console.warn(
71
- '[timber] Stale client reference detected. Reloading page.'
72
- );
98
+ console.warn('[timber] Stale client reference detected. Reloading page.');
73
99
  window.location.reload();
74
100
  return true;
75
101
  }
@@ -39,7 +39,7 @@ export interface TopLoaderConfig {
39
39
  color?: string;
40
40
  /** Bar height in pixels. Default: 3. */
41
41
  height?: number;
42
- /** Show subtle glow/shadow effect. Default: true. */
42
+ /** Show subtle glow/shadow effect. Default: false. */
43
43
  shadow?: boolean;
44
44
  /** Delay in ms before showing the bar. Default: 0. */
45
45
  delay?: number;
@@ -51,7 +51,7 @@ export interface TopLoaderConfig {
51
51
 
52
52
  const DEFAULT_COLOR = '#2299DD';
53
53
  const DEFAULT_HEIGHT = 3;
54
- const DEFAULT_SHADOW = true;
54
+ const DEFAULT_SHADOW = false;
55
55
  const DEFAULT_DELAY = 0;
56
56
  const DEFAULT_Z_INDEX = 1600;
57
57
 
@@ -183,18 +183,19 @@ export function TopLoader({ config }: { config?: TopLoaderConfig }): React.React
183
183
  };
184
184
 
185
185
  // Clean up the finishing phase when the finish animation completes.
186
- const handleAnimationEnd = phase === 'finishing'
187
- ? (e: React.AnimationEvent) => {
188
- if (e.animationName === FINISH_KEYFRAMES) {
189
- setPhase('hidden');
186
+ const handleAnimationEnd =
187
+ phase === 'finishing'
188
+ ? (e: React.AnimationEvent) => {
189
+ if (e.animationName === FINISH_KEYFRAMES) {
190
+ setPhase('hidden');
191
+ }
190
192
  }
191
- }
192
- : undefined;
193
+ : undefined;
193
194
 
194
195
  return createElement(
195
196
  'div',
196
197
  {
197
- style: containerStyle,
198
+ 'style': containerStyle,
198
199
  'aria-hidden': 'true',
199
200
  'data-timber-top-loader': '',
200
201
  },
@@ -62,7 +62,13 @@ let _navigateTransition:
62
62
  * Non-navigation renders:
63
63
  * transitionRender(newWrappedElement);
64
64
  */
65
- export function TransitionRoot({ initial, topLoaderConfig }: { initial: ReactNode; topLoaderConfig?: TopLoaderConfig }): ReactNode {
65
+ export function TransitionRoot({
66
+ initial,
67
+ topLoaderConfig,
68
+ }: {
69
+ initial: ReactNode;
70
+ topLoaderConfig?: TopLoaderConfig;
71
+ }): ReactNode {
66
72
  const [element, setElement] = useState<ReactNode>(initial);
67
73
  const [pendingUrl, setPendingUrl] = useState<string | null>(null);
68
74
  const [, startTransition] = useTransition();
@@ -119,9 +119,9 @@ export function notifyParamsListeners(): void {
119
119
  * exact params shape from the generated Routes interface.
120
120
  * @overload Fallback — returns the generic params record.
121
121
  */
122
- export function useParams<R extends keyof Routes>(route: R): Routes[R]['params'];
123
- export function useParams(route?: string): Record<string, string | string[]>;
124
- export function useParams(_route?: string): Record<string, string | string[]> {
122
+ export function useSegmentParams<R extends keyof Routes>(route: R): Routes[R]['params'];
123
+ export function useSegmentParams(route?: string): Record<string, string | string[]>;
124
+ export function useSegmentParams(_route?: string): Record<string, string | string[]> {
125
125
  // Try reading from NavigationContext (client-side, inside React tree).
126
126
  // During SSR, no NavigationProvider is mounted, so this returns null.
127
127
  // When called outside a React component, useContext throws — caught below.
@@ -17,7 +17,7 @@ import type {
17
17
  SearchParamsDefinition,
18
18
  SetParams,
19
19
  QueryStatesOptions,
20
- } from '#/search-params/create.js';
20
+ } from '#/search-params/define.js';
21
21
  import { getSearchParams } from '#/search-params/registry.js';
22
22
 
23
23
  // ─── Codec Bridge ─────────────────────────────────────────────────
package/src/codec.ts ADDED
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Shared codec protocol for parsing and serializing string values.
3
+ *
4
+ * Used by both search params and cookies. Any object with parse + serialize
5
+ * methods satisfies this interface. nuqs parsers are valid codecs natively.
6
+ *
7
+ * Design doc: design/23a-search-params-triage.md §"Unify Codec<T> type"
8
+ */
9
+
10
+ /**
11
+ * A codec that converts between string values and typed values.
12
+ *
13
+ * The canonical protocol shared across search params, cookies, and
14
+ * any future timber feature that needs string ↔ typed conversion.
15
+ */
16
+ export interface Codec<T> {
17
+ /** String → typed value. Receives undefined when the value is absent. */
18
+ parse(value: string | string[] | undefined): T;
19
+ /** Typed value → string. Return null to omit/clear. */
20
+ serialize(value: T): string | null;
21
+ }