@timber-js/app 0.2.0-alpha.195 → 0.2.0-alpha.197

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 (285) hide show
  1. package/dist/_chunks/{actions-CWYtq6ii.js → actions-BS-m5SLv.js} +3 -3
  2. package/dist/_chunks/{actions-CWYtq6ii.js.map → actions-BS-m5SLv.js.map} +1 -1
  3. package/dist/_chunks/als-registry-C6kcfprT.js.map +1 -1
  4. package/dist/_chunks/{build-manifest-DWppEdLB.js → build-manifest-DTmSGLRz.js} +51 -2
  5. package/dist/_chunks/build-manifest-DTmSGLRz.js.map +1 -0
  6. package/dist/_chunks/{cache-api-CQeYzA5g.js → cache-api-DqzgTEqk.js} +4 -49
  7. package/dist/_chunks/cache-api-DqzgTEqk.js.map +1 -0
  8. package/dist/_chunks/{chains-h7EO-u3n.js → chains-CZG7E5zg.js} +2 -2
  9. package/dist/_chunks/{chains-h7EO-u3n.js.map → chains-CZG7E5zg.js.map} +1 -1
  10. package/dist/_chunks/{cli-check-BVthpfLS.js → cli-check-bE3H5Bjr.js} +3 -3
  11. package/dist/_chunks/{cli-check-BVthpfLS.js.map → cli-check-bE3H5Bjr.js.map} +1 -1
  12. package/dist/_chunks/{cli-schema-sync-3Wutm8pH.js → cli-schema-sync-DTy_-Msq.js} +2 -2
  13. package/dist/_chunks/{cli-schema-sync-3Wutm8pH.js.map → cli-schema-sync-DTy_-Msq.js.map} +1 -1
  14. package/dist/_chunks/{cloudflare-BKJC3SC_.js → cloudflare-BFb__LYG.js} +2 -2
  15. package/dist/_chunks/{cloudflare-BKJC3SC_.js.map → cloudflare-BFb__LYG.js.map} +1 -1
  16. package/dist/_chunks/{convention-lint-DO10_pVl.js → convention-lint-n3RJLgww.js} +2 -2
  17. package/dist/_chunks/{convention-lint-DO10_pVl.js.map → convention-lint-n3RJLgww.js.map} +1 -1
  18. package/dist/_chunks/{error-boundary-D-lkwyaD.js → error-boundary-BvRCCmbN.js} +3 -3
  19. package/dist/_chunks/{error-boundary-D-lkwyaD.js.map → error-boundary-BvRCCmbN.js.map} +1 -1
  20. package/dist/_chunks/{href-validation-CMc5JRls.js → href-validation-BIrxavIy.js} +74 -2
  21. package/dist/_chunks/href-validation-BIrxavIy.js.map +1 -0
  22. package/dist/_chunks/{live-graph-Bx4HodF1.js → live-graph-BXDsdzBv.js} +3 -3
  23. package/dist/_chunks/{live-graph-Bx4HodF1.js.map → live-graph-BXDsdzBv.js.map} +1 -1
  24. package/dist/_chunks/{logger-pumCm3Il.js → logger-DDirEsn7.js} +3 -4
  25. package/dist/_chunks/{logger-pumCm3Il.js.map → logger-DDirEsn7.js.map} +1 -1
  26. package/dist/_chunks/navigation-root-B00jjGd5.js +233 -0
  27. package/dist/_chunks/navigation-root-B00jjGd5.js.map +1 -0
  28. package/dist/_chunks/{segment-context-CjOlyB8Y.js → param-value-C8TNYchQ.js} +2 -33
  29. package/dist/_chunks/param-value-C8TNYchQ.js.map +1 -0
  30. package/dist/_chunks/{poison-scan-BAxfTT5L.js → poison-scan-BoDLgbix.js} +2 -2
  31. package/dist/_chunks/{poison-scan-BAxfTT5L.js.map → poison-scan-BoDLgbix.js.map} +1 -1
  32. package/dist/_chunks/{router-ref-BzqbPwYC.js → router-ref-8gr8qsxN.js} +2 -2
  33. package/dist/_chunks/{router-ref-BzqbPwYC.js.map → router-ref-8gr8qsxN.js.map} +1 -1
  34. package/dist/_chunks/{rsc-cache-key-DD0fl_-s.js → rsc-cache-key-ClUiXQnK.js} +2 -2
  35. package/dist/_chunks/{rsc-cache-key-DD0fl_-s.js.map → rsc-cache-key-ClUiXQnK.js.map} +1 -1
  36. package/dist/_chunks/{scanner-BRIOmHE2.js → scanner-tdFPvDYi.js} +174 -7
  37. package/dist/_chunks/scanner-tdFPvDYi.js.map +1 -0
  38. package/dist/_chunks/segment-context-D9_89u34.js +34 -0
  39. package/dist/_chunks/segment-context-D9_89u34.js.map +1 -0
  40. package/dist/_chunks/singleflight-2lUWfcAk.js +54 -0
  41. package/dist/_chunks/singleflight-2lUWfcAk.js.map +1 -0
  42. package/dist/_chunks/{ssr-data-Ya2HJPFp.js → ssr-data-BQGhTPAK.js} +2 -17
  43. package/dist/_chunks/ssr-data-BQGhTPAK.js.map +1 -0
  44. package/dist/_chunks/{walkers-BU6z9xRV.js → walkers-DNX05dC0.js} +2 -2
  45. package/dist/_chunks/{walkers-BU6z9xRV.js.map → walkers-DNX05dC0.js.map} +1 -1
  46. package/dist/adapters/cloudflare-dev.js +1 -1
  47. package/dist/adapters/cloudflare-kv-cache.js +1 -1
  48. package/dist/adapters/cloudflare.js +1 -1
  49. package/dist/adapters/nitro.d.ts +1 -1
  50. package/dist/adapters/nitro.d.ts.map +1 -1
  51. package/dist/adapters/nitro.js.map +1 -1
  52. package/dist/analyze/crawl-entry.js +2 -2
  53. package/dist/analyze/graph-command.js +2 -2
  54. package/dist/cache/index.js +1 -1
  55. package/dist/cache/singleflight.d.ts +2 -0
  56. package/dist/cache/singleflight.d.ts.map +1 -1
  57. package/dist/cli.js +2 -2
  58. package/dist/client/browser-entry/hydrate.d.ts +21 -15
  59. package/dist/client/browser-entry/hydrate.d.ts.map +1 -1
  60. package/dist/client/browser-entry/index.d.ts +4 -3
  61. package/dist/client/browser-entry/index.d.ts.map +1 -1
  62. package/dist/client/browser-entry/post-hydration.d.ts.map +1 -1
  63. package/dist/client/browser-entry/router-init.d.ts +17 -1
  64. package/dist/client/browser-entry/router-init.d.ts.map +1 -1
  65. package/dist/client/error-boundary.js +1 -1
  66. package/dist/client/global-context.d.ts +15 -0
  67. package/dist/client/global-context.d.ts.map +1 -0
  68. package/dist/client/index.js +139 -36
  69. package/dist/client/index.js.map +1 -1
  70. package/dist/client/internal.d.ts +0 -1
  71. package/dist/client/internal.d.ts.map +1 -1
  72. package/dist/client/internal.js +206 -55
  73. package/dist/client/internal.js.map +1 -1
  74. package/dist/client/link.d.ts.map +1 -1
  75. package/dist/client/location-search.d.ts +12 -0
  76. package/dist/client/location-search.d.ts.map +1 -0
  77. package/dist/client/navigation-api.d.ts.map +1 -1
  78. package/dist/client/navigation-commit.d.ts +18 -0
  79. package/dist/client/navigation-commit.d.ts.map +1 -1
  80. package/dist/client/navigation-context.d.ts +13 -11
  81. package/dist/client/navigation-context.d.ts.map +1 -1
  82. package/dist/client/navigation-root.d.ts +47 -108
  83. package/dist/client/navigation-root.d.ts.map +1 -1
  84. package/dist/client/navigation-transition.d.ts +136 -0
  85. package/dist/client/navigation-transition.d.ts.map +1 -0
  86. package/dist/client/nuqs-adapter.d.ts.map +1 -1
  87. package/dist/client/params-context.d.ts +4 -5
  88. package/dist/client/params-context.d.ts.map +1 -1
  89. package/dist/client/react-root.d.ts +44 -0
  90. package/dist/client/react-root.d.ts.map +1 -0
  91. package/dist/client/router-pipeline.d.ts +2 -2
  92. package/dist/client/router-pipeline.d.ts.map +1 -1
  93. package/dist/client/router-types.d.ts +12 -2
  94. package/dist/client/router-types.d.ts.map +1 -1
  95. package/dist/client/router.d.ts.map +1 -1
  96. package/dist/client/segment-cache.d.ts +39 -0
  97. package/dist/client/segment-cache.d.ts.map +1 -1
  98. package/dist/client/segment-context.d.ts.map +1 -1
  99. package/dist/client/segment-outlet.d.ts +25 -14
  100. package/dist/client/segment-outlet.d.ts.map +1 -1
  101. package/dist/client/segment-update-context.d.ts +3 -9
  102. package/dist/client/segment-update-context.d.ts.map +1 -1
  103. package/dist/client/slot-content-cache-context.d.ts +35 -0
  104. package/dist/client/slot-content-cache-context.d.ts.map +1 -0
  105. package/dist/client/ssr-data.d.ts +8 -2
  106. package/dist/client/ssr-data.d.ts.map +1 -1
  107. package/dist/client/state.d.ts +0 -15
  108. package/dist/client/state.d.ts.map +1 -1
  109. package/dist/client/use-pathname.d.ts +13 -11
  110. package/dist/client/use-pathname.d.ts.map +1 -1
  111. package/dist/client/use-search-params.d.ts +13 -13
  112. package/dist/client/use-search-params.d.ts.map +1 -1
  113. package/dist/client/use-segment-params.d.ts +18 -68
  114. package/dist/client/use-segment-params.d.ts.map +1 -1
  115. package/dist/config-types.d.ts +17 -0
  116. package/dist/config-types.d.ts.map +1 -1
  117. package/dist/config-validation.d.ts.map +1 -1
  118. package/dist/cookies/index.js +1 -1
  119. package/dist/dev-tools/holding-server.d.ts +15 -10
  120. package/dist/dev-tools/holding-server.d.ts.map +1 -1
  121. package/dist/index.d.ts.map +1 -1
  122. package/dist/index.js +44 -43
  123. package/dist/index.js.map +1 -1
  124. package/dist/plugins/dev-server.d.ts.map +1 -1
  125. package/dist/plugins/entries.d.ts.map +1 -1
  126. package/dist/plugins/shims.d.ts.map +1 -1
  127. package/dist/plugins/static-build.d.ts +2 -2
  128. package/dist/plugins/static-build.d.ts.map +1 -1
  129. package/dist/routing/index.js +2 -2
  130. package/dist/routing/interception-overlap.d.ts +35 -0
  131. package/dist/routing/interception-overlap.d.ts.map +1 -0
  132. package/dist/routing/interception.d.ts.map +1 -1
  133. package/dist/rsc-runtime/ssr.d.ts +3 -1
  134. package/dist/rsc-runtime/ssr.d.ts.map +1 -1
  135. package/dist/server/als-registry.d.ts +6 -0
  136. package/dist/server/als-registry.d.ts.map +1 -1
  137. package/dist/server/csp-nonce.d.ts +45 -0
  138. package/dist/server/csp-nonce.d.ts.map +1 -0
  139. package/dist/server/default-status-page.d.ts.map +1 -1
  140. package/dist/server/deny-renderer.d.ts.map +1 -1
  141. package/dist/server/flight-scripts.d.ts +5 -2
  142. package/dist/server/flight-scripts.d.ts.map +1 -1
  143. package/dist/server/html-injector-core.d.ts +17 -2
  144. package/dist/server/html-injector-core.d.ts.map +1 -1
  145. package/dist/server/html-injectors.d.ts +3 -2
  146. package/dist/server/html-injectors.d.ts.map +1 -1
  147. package/dist/server/index.js +2 -2
  148. package/dist/server/internal.js +86 -37
  149. package/dist/server/internal.js.map +1 -1
  150. package/dist/server/metadata-render.d.ts.map +1 -1
  151. package/dist/server/node-stream-transforms.d.ts +3 -17
  152. package/dist/server/node-stream-transforms.d.ts.map +1 -1
  153. package/dist/server/nuqs-ssr-provider.d.ts +7 -3
  154. package/dist/server/nuqs-ssr-provider.d.ts.map +1 -1
  155. package/dist/server/pipeline-phases.d.ts.map +1 -1
  156. package/dist/server/prebuilt/key-discipline.d.ts +32 -3
  157. package/dist/server/prebuilt/key-discipline.d.ts.map +1 -1
  158. package/dist/server/primitives.d.ts.map +1 -1
  159. package/dist/server/render-utils.d.ts +4 -3
  160. package/dist/server/render-utils.d.ts.map +1 -1
  161. package/dist/server/rsc-entry/action-middleware-runner.d.ts.map +1 -1
  162. package/dist/server/rsc-entry/error-renderer.d.ts.map +1 -1
  163. package/dist/server/rsc-entry/index.d.ts.map +1 -1
  164. package/dist/server/rsc-entry/ssr-renderer.d.ts.map +1 -1
  165. package/dist/server/ssr-bridge-types.d.ts +22 -2
  166. package/dist/server/ssr-bridge-types.d.ts.map +1 -1
  167. package/dist/server/ssr-entry.d.ts.map +1 -1
  168. package/dist/server/ssr-render.d.ts +5 -1
  169. package/dist/server/ssr-render.d.ts.map +1 -1
  170. package/dist/server/ssr-wrappers.d.ts +59 -27
  171. package/dist/server/ssr-wrappers.d.ts.map +1 -1
  172. package/dist/server/types.d.ts +10 -0
  173. package/dist/server/types.d.ts.map +1 -1
  174. package/dist/shims/navigation-rsc.d.ts +21 -0
  175. package/dist/shims/navigation-rsc.d.ts.map +1 -0
  176. package/docs/api/30-api-server.mdx +1 -0
  177. package/docs/api/35-api-typescript.mdx +4 -83
  178. package/docs/learn/03-fetching-data.mdx +1 -1
  179. package/docs/learn/{03b-access-control.mdx → 04-access-control.mdx} +2 -17
  180. package/docs/learn/05-the-flush-point.mdx +175 -0
  181. package/docs/learn/{05-typed-params.mdx → 06-typed-params.mdx} +1 -1
  182. package/docs/learn/07-typed-routes.mdx +25 -49
  183. package/docs/learn/{08-streaming.mdx → 09-streaming.mdx} +1 -7
  184. package/docs/learn/{10-middleware.mdx → 11-middleware.mdx} +1 -0
  185. package/package.json +3 -3
  186. package/src/adapters/nitro.ts +7 -7
  187. package/src/cache/singleflight.ts +5 -0
  188. package/src/client/browser-entry/hydrate.ts +54 -104
  189. package/src/client/browser-entry/index.ts +16 -6
  190. package/src/client/browser-entry/post-hydration.ts +3 -2
  191. package/src/client/browser-entry/router-init.ts +84 -33
  192. package/src/client/global-context.ts +31 -0
  193. package/src/client/internal.ts +1 -2
  194. package/src/client/link.tsx +25 -19
  195. package/src/client/location-search.ts +15 -0
  196. package/src/client/navigation-api.ts +4 -2
  197. package/src/client/navigation-commit.ts +48 -2
  198. package/src/client/navigation-context.ts +25 -37
  199. package/src/client/navigation-root.tsx +55 -411
  200. package/src/client/navigation-transition.ts +278 -0
  201. package/src/client/nuqs-adapter.tsx +4 -5
  202. package/src/client/params-context.ts +13 -18
  203. package/src/client/react-root.ts +72 -0
  204. package/src/client/router-lifecycle.ts +1 -1
  205. package/src/client/router-pipeline.ts +96 -22
  206. package/src/client/router-types.ts +12 -2
  207. package/src/client/router.ts +48 -36
  208. package/src/client/segment-cache.ts +70 -2
  209. package/src/client/segment-context.ts +7 -4
  210. package/src/client/segment-outlet.tsx +41 -86
  211. package/src/client/segment-update-context.ts +7 -26
  212. package/src/client/slot-content-cache-context.ts +43 -0
  213. package/src/client/ssr-data.ts +8 -2
  214. package/src/client/state.ts +0 -26
  215. package/src/client/use-pathname.ts +21 -31
  216. package/src/client/use-search-params.ts +31 -29
  217. package/src/client/use-segment-params.ts +27 -126
  218. package/src/config-types.ts +17 -0
  219. package/src/config-validation.ts +17 -0
  220. package/src/dev-tools/holding-server.ts +23 -12
  221. package/src/index.ts +26 -11
  222. package/src/plugins/dev-server.ts +9 -12
  223. package/src/plugins/entries.ts +3 -0
  224. package/src/plugins/shims.ts +8 -7
  225. package/src/plugins/static-build.ts +9 -5
  226. package/src/routing/interception-overlap.ts +141 -0
  227. package/src/routing/interception.ts +118 -5
  228. package/src/rsc-runtime/ssr.ts +3 -2
  229. package/src/server/als-registry.ts +6 -0
  230. package/src/server/csp-nonce.ts +70 -0
  231. package/src/server/default-status-page.ts +1 -0
  232. package/src/server/deny-renderer.ts +7 -3
  233. package/src/server/flight-scripts.ts +9 -4
  234. package/src/server/html-injector-core.ts +26 -9
  235. package/src/server/html-injectors.ts +8 -8
  236. package/src/server/metadata-render.ts +26 -4
  237. package/src/server/node-stream-transforms.ts +7 -20
  238. package/src/server/nuqs-ssr-provider.tsx +8 -7
  239. package/src/server/pipeline-phases.ts +5 -0
  240. package/src/server/prebuilt/key-discipline.ts +82 -13
  241. package/src/server/prebuilt-runtime.ts +2 -2
  242. package/src/server/primitives.ts +4 -4
  243. package/src/server/render-utils.ts +8 -4
  244. package/src/server/rsc-entry/action-middleware-runner.ts +7 -0
  245. package/src/server/rsc-entry/error-renderer.ts +5 -2
  246. package/src/server/rsc-entry/index.ts +8 -0
  247. package/src/server/rsc-entry/ssr-renderer.ts +11 -4
  248. package/src/server/ssr-bridge-types.ts +22 -2
  249. package/src/server/ssr-entry.ts +35 -28
  250. package/src/server/ssr-render.ts +13 -4
  251. package/src/server/ssr-wrappers.tsx +81 -61
  252. package/src/server/types.ts +10 -0
  253. package/src/shared/slot-params.ts +3 -4
  254. package/src/shims/navigation-rsc.ts +47 -0
  255. package/dist/_chunks/build-manifest-DWppEdLB.js.map +0 -1
  256. package/dist/_chunks/cache-api-CQeYzA5g.js.map +0 -1
  257. package/dist/_chunks/href-validation-CMc5JRls.js.map +0 -1
  258. package/dist/_chunks/scanner-BRIOmHE2.js.map +0 -1
  259. package/dist/_chunks/segment-context-CjOlyB8Y.js.map +0 -1
  260. package/dist/_chunks/slot-params-BCTmZkQB.js +0 -76
  261. package/dist/_chunks/slot-params-BCTmZkQB.js.map +0 -1
  262. package/dist/_chunks/ssr-data-Ya2HJPFp.js.map +0 -1
  263. package/dist/_chunks/use-segment-params-DzTBpkvj.js +0 -398
  264. package/dist/_chunks/use-segment-params-DzTBpkvj.js.map +0 -1
  265. package/docs/learn/04-loading-states.mdx +0 -67
  266. package/docs/learn/04b-the-flush-point.mdx +0 -115
  267. package/docs/learn/12-client-navigation.mdx +0 -176
  268. package/docs/learn/13-configuration.mdx +0 -166
  269. package/docs/more/01-advanced-routing.mdx +0 -344
  270. package/docs/more/02-advanced-forms.mdx +0 -137
  271. package/docs/more/03-coming-from-nextjs.mdx +0 -186
  272. package/docs/more/04-metadata-and-fonts.mdx +0 -193
  273. package/docs/more/04b-mdx.mdx +0 -229
  274. package/docs/more/05-content-collections.mdx +0 -90
  275. package/docs/more/06-instrumentation.mdx +0 -214
  276. package/docs/more/07-security.mdx +0 -129
  277. package/docs/more/08-developer-experience.mdx +0 -134
  278. package/docs/more/40-why-timber.mdx +0 -50
  279. package/docs/more/41-timber-vs-nextjs.mdx +0 -81
  280. package/docs/more/42-timber-vs-others.mdx +0 -68
  281. package/docs/more/50-ai-agent-instructions.mdx +0 -171
  282. /package/docs/learn/{06-forms-and-actions.mdx → 08-forms-and-actions.mdx} +0 -0
  283. /package/docs/learn/{09-caching.mdx → 10-caching.mdx} +0 -0
  284. /package/docs/learn/{11-error-handling.mdx → 12-error-handling.mdx} +0 -0
  285. /package/docs/learn/{14-deploying.mdx → 13-deploying.mdx} +0 -0
@@ -20,6 +20,7 @@
20
20
  // - searchParams and inline query string are mutually exclusive
21
21
 
22
22
  import {
23
+ startTransition,
23
24
  useRef,
24
25
  useState,
25
26
  type AnchorHTMLAttributes,
@@ -37,6 +38,8 @@ import { getRouterOrNull } from './router-ref.ts';
37
38
  import { getSsrData } from './ssr-data.ts';
38
39
  import { mergePreservedSearchParams } from '../shared/merge-search-params.ts';
39
40
  import { getLinkCodec } from '../params/codec-registry.ts';
41
+ import { locationSearch } from './location-search.ts';
42
+ import { stripRscCacheKey } from '../shared/rsc-cache-key.ts';
40
43
  import type { LinkStatus } from './use-link-status.ts';
41
44
 
42
45
  const LINK_PENDING: LinkStatus = { isPending: true };
@@ -46,16 +49,15 @@ const LINK_IDLE: LinkStatus = { isPending: false };
46
49
 
47
50
  /**
48
51
  * Read the current URL's search string without requiring a React hook.
49
- * On the client, reads window.location.search. During SSR, reads from
50
- * the request context (getSsrData). Returns empty string if unavailable.
52
+ * On the client, reads window.location.search. During SSR, reads the raw
53
+ * query string from the request context (getSsrData) — the same `''` or
54
+ * `?…` shape, preserving repeated keys (`?tag=a&tag=b`) so the
55
+ * server-rendered href matches what the hydrated client rebuilds from the
56
+ * address bar (TIM-1428). Returns empty string if unavailable.
51
57
  */
52
58
  function getCurrentSearch(): string {
53
- if (typeof window !== 'undefined') return window.location.search;
54
- const data = getSsrData();
55
- if (!data) return '';
56
- const sp = new URLSearchParams(data.searchParams);
57
- const str = sp.toString();
58
- return str ? `?${str}` : '';
59
+ if (typeof window !== 'undefined') return locationSearch();
60
+ return getSsrData()?.search ?? '';
59
61
  }
60
62
 
61
63
  // ─── Types ───────────────────────────────────────────────────────
@@ -482,7 +484,7 @@ export const Link: LinkFunction = function LinkImpl(props: any) {
482
484
  // a thenable from `startTransition` hands it to `ReactSharedInternals.S`,
483
485
  // which calls react-dom's `entangleAsyncAction`. That opens an action scope
484
486
  // whose `currentEntangledLane` collects every transition update scheduled
485
- // while it is open — including `NavigationRoot`'s `setRendered`, which is a
487
+ // while it is open — including the router's `root.render` of `NavigationRoot`, which is a
486
488
  // fully synchronous `startTransition` in a different component — and
487
489
  // rendering that lane suspends on `currentEntangledActionThenable` until the
488
490
  // action settles. `navigateTransition` awaits `decodePromise`, so a Link
@@ -496,7 +498,12 @@ export const Link: LinkFunction = function LinkImpl(props: any) {
496
498
  //
497
499
  // `isPending` runs from the click until `router.navigate()` settles, which
498
500
  // is after `decodePromise` — the same lifecycle the router's pending store
499
- // and the TopLoader use.
501
+ // and the TopLoader use. The clear wraps in `startTransition` so it lands
502
+ // on the same React lane as the router's `root.render` — without it,
503
+ // the clear is an urgent update that paints before the transition commits,
504
+ // and destinations with pending Suspense boundaries flash idle for one frame
505
+ // while the old page is still showing (TIM-1418). The callback is
506
+ // synchronous, so no action scope is opened (TIM-1306/1307 safe).
500
507
  //
501
508
  // `clickSeq` exists because a superseded navigation RESOLVES rather than
502
509
  // rejecting: `runNavigation` swallows AbortErrors. So clicking the same link
@@ -509,11 +516,10 @@ export const Link: LinkFunction = function LinkImpl(props: any) {
509
516
  const linkStatus = isPending ? LINK_PENDING : LINK_IDLE;
510
517
 
511
518
  // Preserve search params from the current URL when requested.
512
- // useSearchParams() works during both SSR (reads from request context)
513
- // and on the client (reads from window.location, reactive to URL changes).
514
- // We read current search params directly to avoid unconditional hook calls.
515
- // On the client, window.location.search is always current; during SSR,
516
- // getSsrData() provides the request's search params.
519
+ // Read via getCurrentSearch() rather than a hook, to avoid an
520
+ // unconditional hook call for a prop most links don't pass. On the
521
+ // client, window.location.search is always current; during SSR,
522
+ // getSsrData() provides the request's raw query string.
517
523
  const internal = isInternalHref(baseHref);
518
524
 
519
525
  // Only preserve search params for internal links — leaking current
@@ -559,7 +565,7 @@ export const Link: LinkFunction = function LinkImpl(props: any) {
559
565
 
560
566
  if (
561
567
  resolved.pathname === window.location.pathname &&
562
- resolved.search === window.location.search &&
568
+ stripRscCacheKey(resolved.search) === locationSearch() &&
563
569
  resolved.hash
564
570
  ) {
565
571
  return;
@@ -571,11 +577,11 @@ export const Link: LinkFunction = function LinkImpl(props: any) {
571
577
  // Keep the #fragment — the router commits it to the address bar and
572
578
  // scrolls to the matching element after render. The hash is stripped
573
579
  // from the RSC fetch URL inside the router (TIM-1035).
574
- const absoluteHref = resolved.pathname + resolved.search + resolved.hash;
580
+ const absoluteHref = resolved.pathname + stripRscCacheKey(resolved.search) + resolved.hash;
575
581
 
576
582
  const seq = ++clickSeq.current;
577
583
  const settle = () => {
578
- if (clickSeq.current === seq) setIsPending(false);
584
+ if (clickSeq.current === seq) startTransition(() => setIsPending(false));
579
585
  };
580
586
  setIsPending(true);
581
587
  router.navigate(absoluteHref, { scroll: shouldScroll }).then(settle, (error: unknown) => {
@@ -609,7 +615,7 @@ export const Link: LinkFunction = function LinkImpl(props: any) {
609
615
  ? mergePreservedSearchParams(baseHref, getCurrentSearch(), preserveSearchParams)
610
616
  : resolvedHref;
611
617
  const resolved = new URL(prefetchHref, window.location.href);
612
- router.prefetch(resolved.pathname + resolved.search);
618
+ router.prefetch(resolved.pathname + stripRscCacheKey(resolved.search));
613
619
  }
614
620
  }
615
621
  : userOnMouseEnter;
@@ -0,0 +1,15 @@
1
+ import { stripRscCacheKey } from '../shared/rsc-cache-key.ts';
2
+
3
+ /**
4
+ * The app-visible search string from the address bar.
5
+ *
6
+ * Mirrors what `appVisibleSearch()` does on the server: strips key-shaped
7
+ * `_rsc` values so the client and server agree on search state regardless
8
+ * of whether the document URL itself carries a payload cache key (a user
9
+ * pasting/sharing an RSC payload URL). Non-key-shaped `_rsc` values — an
10
+ * application legitimately owning that name — survive, using the same
11
+ * `isRscCacheKeyShape` predicate the server uses.
12
+ */
13
+ export function locationSearch(): string {
14
+ return stripRscCacheKey(window.location.search);
15
+ }
@@ -18,6 +18,7 @@
18
18
  */
19
19
 
20
20
  import { isHardNavigating } from './navigation-root.tsx';
21
+ import { stripRscCacheKey } from '../shared/rsc-cache-key.ts';
21
22
 
22
23
  // ─── Feature Detection ───────────────────────────────────────────
23
24
 
@@ -220,7 +221,7 @@ export function setupNavigationApi(callbacks: NavigationApiCallbacks): Navigatio
220
221
  // Skip reload navigations — let the browser handle full page reload
221
222
  if (event.navigationType === 'reload') return;
222
223
 
223
- const url = destUrl.pathname + destUrl.search;
224
+ const url = destUrl.pathname + stripRscCacheKey(destUrl.search);
224
225
 
225
226
  if (event.navigationType === 'traverse') {
226
227
  // Back/forward button — intercept and delegate to router.
@@ -265,7 +266,8 @@ export function setupNavigationApi(callbacks: NavigationApiCallbacks): Navigatio
265
266
  // destination. Once intercept() is called, currentEntry switches and
266
267
  // getCurrentUrl() returns the destination (TIM-1232).
267
268
  const departingUrl = nav.currentEntry?.url
268
- ? new URL(nav.currentEntry.url).pathname + new URL(nav.currentEntry.url).search
269
+ ? new URL(nav.currentEntry.url).pathname +
270
+ stripRscCacheKey(new URL(nav.currentEntry.url).search)
269
271
  : undefined;
270
272
 
271
273
  event.intercept({
@@ -54,6 +54,51 @@ export interface NavigationCommitInput {
54
54
  * See TIM-1356.
55
55
  */
56
56
  status?: number;
57
+ /**
58
+ * Layouts the server skipped on this response (`X-Timber-Skipped-Segments`),
59
+ * when it came from a fetch. Omitted by replays and revalidation.
60
+ *
61
+ * Together with `skipped` slot entries in `segmentInfo`, this decides
62
+ * whether the payload can be *replayed*. A partial payload only renders
63
+ * merged over the tree on screen, and a `skip=true` outlet resolves against
64
+ * the slot content cache at render time — so a stored copy of either,
65
+ * replayed later, shows whatever the client holds *then*, not what was on
66
+ * screen when the entry committed. Such entries store a null payload and
67
+ * traversals to them fetch instead. Decided here, not at the fetch call
68
+ * sites, for the same reason as `status`: `performNavigationFetch` had the
69
+ * check and `fetchCommitAndRender` did not, so a back/forward fetch that
70
+ * was answered with skips stored the holes (TIM-1432).
71
+ */
72
+ skippedSegments?: string[] | null;
73
+ }
74
+
75
+ /** Whether a response skipped layouts — its payload merges over the tree on screen. */
76
+ export function isPartialNavigation(skippedSegments: string[] | null | undefined): boolean {
77
+ return skippedSegments != null && skippedSegments.length > 0;
78
+ }
79
+
80
+ /**
81
+ * Whether a payload can stand on its own when replayed from the history
82
+ * stack. See `NavigationCommitInput.skippedSegments`.
83
+ */
84
+ function isReplayable(opts: NavigationCommitInput): boolean {
85
+ return !isPartialNavigation(opts.skippedSegments) && !hasSkippedSlot(opts.segmentInfo);
86
+ }
87
+
88
+ function hasSkippedSlot(segmentInfo: SegmentInfo[] | null | undefined): boolean {
89
+ return segmentInfo?.some((s) => s.slot && s.skipped) ?? false;
90
+ }
91
+
92
+ /**
93
+ * The metadata a history entry keeps. `skipped` describes the *response*
94
+ * — "this slot's content was omitted" — and the entry stores no such
95
+ * payload (it stores null). Left on the entry, the flag outlives the
96
+ * response: `applyRevalidation()` reuses the entry's metadata beside a full
97
+ * re-render and would have that judged non-replayable too.
98
+ */
99
+ function storedSegmentInfo(segmentInfo: SegmentInfo[] | null | undefined) {
100
+ if (!hasSkippedSlot(segmentInfo)) return segmentInfo;
101
+ return segmentInfo!.map(({ skipped: _skipped, ...rest }) => rest);
57
102
  }
58
103
 
59
104
  /**
@@ -173,6 +218,7 @@ export function createNavigationCommitter(deps: {
173
218
  // traversal back to this URL would replay the metadata this drops.
174
219
  const cacheable = opts.status === undefined || opts.status < 400;
175
220
  const segmentInfo = cacheable ? opts.segmentInfo : [];
221
+ const payload = isReplayable(opts) ? opts.payload : null;
176
222
 
177
223
  return {
178
224
  navState,
@@ -186,9 +232,9 @@ export function createNavigationCommitter(deps: {
186
232
  setNavigationState(navState);
187
233
 
188
234
  historyStack.push(url, {
189
- payload: opts.payload,
235
+ payload,
190
236
  params: opts.params,
191
- segmentInfo,
237
+ segmentInfo: storedSegmentInfo(segmentInfo),
192
238
  });
193
239
  },
194
240
  };
@@ -18,14 +18,15 @@
18
18
  * All consumers (usePathname, useSearchParams) see the new values in the
19
19
  * same render pass as the new tree.
20
20
  *
21
- * During SSR, no NavigationProvider is mounted. Hooks fall back to
22
- * the ALS-backed getSsrData() for per-request isolation.
21
+ * During SSR, the wrapper chain mounts this same provider with the
22
+ * request's pathname and raw search string (TIM-1424), so the hooks
23
+ * resolve through the identical code path on both sides. Per-request
24
+ * isolation is by construction — the value is part of the request's
25
+ * element tree.
23
26
  *
24
- * IMPORTANT: createContext and useContext are NOT available in the RSC
25
- * environment (React Server Components use a stripped-down React).
26
- * The context is lazily initialized on first access, and all functions
27
- * that depend on these APIs are safe to call from any environment —
28
- * they return null or no-op when the APIs aren't available.
27
+ * This module is `'use client'` and is never evaluated in the RSC
28
+ * environment. The shims plugin resolves next/navigation to
29
+ * navigation-rsc.ts in RSC, which has throwing stubs (TIM-1420).
29
30
  *
30
31
  * SINGLETON GUARANTEE: All shared mutable state uses globalThis via
31
32
  * Symbol.for keys. The RSC client bundler can duplicate this module
@@ -41,7 +42,7 @@
41
42
  * See design/19-client-navigation.md §"NavigationContext"
42
43
  */
43
44
 
44
- import React, { createElement, type ReactNode } from 'react';
45
+ import { createContext, useContext, createElement, type ReactNode } from 'react';
45
46
  import type { SlotParamsRecord } from '../shared/slot-params.ts';
46
47
 
47
48
  // ---------------------------------------------------------------------------
@@ -68,10 +69,6 @@ export interface NavigationState {
68
69
  // ---------------------------------------------------------------------------
69
70
 
70
71
  /**
71
- * The context is created lazily to avoid calling createContext at module
72
- * level. In the RSC environment, React.createContext doesn't exist —
73
- * calling it at import time would crash the server.
74
- *
75
72
  * Context instances are stored on globalThis (NOT in module-level
76
73
  * variables) because the ESM bundler can duplicate this module across
77
74
  * chunks. Module-level variables would create separate instances per
@@ -79,38 +76,34 @@ export interface NavigationState {
79
76
  * context A while the consumer in usePendingNavigation (shared chunk)
80
77
  * reads from context B. globalThis guarantees a single instance.
81
78
  *
79
+ * This module is `'use client'` so it is never evaluated in the RSC
80
+ * environment — `createContext` and `useContext` are always available.
81
+ * The shims plugin resolves next/navigation to navigation-rsc.ts in RSC,
82
+ * which has throwing stubs for the hooks (TIM-1420).
83
+ *
82
84
  * See design/27-chunking-strategy.md §"Singleton Safety"
83
85
  */
84
86
 
85
- // Symbol keys for globalThis storage — prevents collisions with user code
86
87
  const NAV_CTX_KEY = Symbol.for('__timber_nav_ctx');
87
88
 
88
- function getOrCreateContext(): React.Context<NavigationState | null> | undefined {
89
+ function getOrCreateContext(): import('react').Context<NavigationState | null> {
89
90
  const existing = (globalThis as Record<symbol, unknown>)[NAV_CTX_KEY] as
90
- | React.Context<NavigationState | null>
91
+ | import('react').Context<NavigationState | null>
91
92
  | undefined;
92
93
  if (existing !== undefined) return existing;
93
- // createContext may not exist in the RSC environment
94
- if (typeof React.createContext === 'function') {
95
- const ctx = React.createContext<NavigationState | null>(null);
96
- (globalThis as Record<symbol, unknown>)[NAV_CTX_KEY] = ctx;
97
- return ctx;
98
- }
99
- return undefined;
94
+ const ctx = createContext<NavigationState | null>(null);
95
+ (globalThis as Record<symbol, unknown>)[NAV_CTX_KEY] = ctx;
96
+ return ctx;
100
97
  }
101
98
 
102
99
  /**
103
- * Read the navigation context. Returns null during SSR (no provider)
104
- * or in the RSC environment (no context available).
100
+ * Read the navigation context. Returns null only when no provider is above
101
+ * the caller — a component rendered outside the timber app.
102
+ *
105
103
  * Internal — used by usePathname() and useSearchParams().
106
104
  */
107
105
  export function useNavigationContext(): NavigationState | null {
108
- const ctx = getOrCreateContext();
109
- if (!ctx) return null;
110
- // useContext may not exist in the RSC environment — caller wraps in try/catch
111
- if (typeof React.useContext !== 'function') return null;
112
- // eslint-disable-next-line rules-of-hooks -- conditional on environment, not render path
113
- return React.useContext(ctx);
106
+ return useContext(getOrCreateContext());
114
107
  }
115
108
 
116
109
  // ---------------------------------------------------------------------------
@@ -131,13 +124,8 @@ export interface NavigationProviderProps {
131
124
  export function NavigationProvider({
132
125
  value,
133
126
  children,
134
- }: NavigationProviderProps): React.ReactElement {
135
- const ctx = getOrCreateContext();
136
- if (!ctx) {
137
- // RSC environment — no context available. Return children as-is.
138
- return children as React.ReactElement;
139
- }
140
- return createElement(ctx.Provider, { value }, children);
127
+ }: NavigationProviderProps): import('react').ReactElement {
128
+ return createElement(getOrCreateContext().Provider, { value }, children);
141
129
  }
142
130
 
143
131
  // ---------------------------------------------------------------------------