@timber-js/app 0.2.0-alpha.71 → 0.2.0-alpha.72

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 (248) hide show
  1. package/dist/_chunks/actions-Dg-ANYHb.js +421 -0
  2. package/dist/_chunks/actions-Dg-ANYHb.js.map +1 -0
  3. package/dist/_chunks/{als-registry-BJARkOcu.js → als-registry-HS0LGUl2.js} +1 -1
  4. package/dist/_chunks/als-registry-HS0LGUl2.js.map +1 -0
  5. package/dist/_chunks/{define-Dz1bqwaS.js → define-C77ScO0m.js} +14 -14
  6. package/dist/_chunks/define-C77ScO0m.js.map +1 -0
  7. package/dist/_chunks/{define-CGuYoRHU.js → define-CZqDwhSu.js} +15 -15
  8. package/dist/_chunks/define-CZqDwhSu.js.map +1 -0
  9. package/dist/_chunks/{define-cookie-B5mewxwM.js → define-cookie-C2IkoFGN.js} +9 -8
  10. package/dist/_chunks/{define-cookie-B5mewxwM.js.map → define-cookie-C2IkoFGN.js.map} +1 -1
  11. package/dist/_chunks/{format-Rn922VH2.js → dev-warnings-DpGRGoDi.js} +4 -26
  12. package/dist/_chunks/dev-warnings-DpGRGoDi.js.map +1 -0
  13. package/dist/_chunks/format-CYBGxKtc.js +14 -0
  14. package/dist/_chunks/format-CYBGxKtc.js.map +1 -0
  15. package/dist/_chunks/{interception-CEdHHviP.js → interception-Dpn_UfAD.js} +2 -2
  16. package/dist/_chunks/{interception-CEdHHviP.js.map → interception-Dpn_UfAD.js.map} +1 -1
  17. package/dist/_chunks/{segment-context-hzuJ048X.js → merge-search-params-Cm_KIWDX.js} +2 -33
  18. package/dist/_chunks/merge-search-params-Cm_KIWDX.js.map +1 -0
  19. package/dist/_chunks/{request-context-CywiO4jV.js → request-context-qMsWgy9C.js} +72 -36
  20. package/dist/_chunks/request-context-qMsWgy9C.js.map +1 -0
  21. package/dist/_chunks/{schema-bridge-C4SwjCQD.js → schema-bridge-C3xl_vfb.js} +1 -1
  22. package/dist/_chunks/{schema-bridge-C4SwjCQD.js.map → schema-bridge-C3xl_vfb.js.map} +1 -1
  23. package/dist/_chunks/segment-context-fHFLF1PE.js +34 -0
  24. package/dist/_chunks/segment-context-fHFLF1PE.js.map +1 -0
  25. package/dist/_chunks/ssr-data-DzuI0bIV.js +88 -0
  26. package/dist/_chunks/ssr-data-DzuI0bIV.js.map +1 -0
  27. package/dist/_chunks/{stale-reload-BLUC_Pl_.js → stale-reload-C2plcNtG.js} +1 -1
  28. package/dist/_chunks/{stale-reload-BLUC_Pl_.js.map → stale-reload-C2plcNtG.js.map} +1 -1
  29. package/dist/_chunks/{handler-store-BVePM7hp.js → tracing-CCYbKn5n.js} +60 -60
  30. package/dist/_chunks/tracing-CCYbKn5n.js.map +1 -0
  31. package/dist/_chunks/use-params-B1AuhI1p.js +307 -0
  32. package/dist/_chunks/use-params-B1AuhI1p.js.map +1 -0
  33. package/dist/_chunks/{use-query-states-DAhgj8Gx.js → use-query-states-Lo_s_pw2.js} +4 -4
  34. package/dist/_chunks/use-query-states-Lo_s_pw2.js.map +1 -0
  35. package/dist/_chunks/{wrappers-LZbghvn0.js → wrappers-_DTmImGt.js} +1 -1
  36. package/dist/_chunks/{wrappers-LZbghvn0.js.map → wrappers-_DTmImGt.js.map} +1 -1
  37. package/dist/adapters/cloudflare-kv-cache.d.ts +64 -0
  38. package/dist/adapters/cloudflare-kv-cache.d.ts.map +1 -0
  39. package/dist/adapters/cloudflare-kv-cache.js +95 -0
  40. package/dist/adapters/cloudflare-kv-cache.js.map +1 -0
  41. package/dist/cache/index.d.ts +18 -4
  42. package/dist/cache/index.d.ts.map +1 -1
  43. package/dist/cache/index.js +78 -12
  44. package/dist/cache/index.js.map +1 -1
  45. package/dist/cache/sizeof.d.ts +22 -0
  46. package/dist/cache/sizeof.d.ts.map +1 -0
  47. package/dist/cli.d.ts +6 -1
  48. package/dist/cli.d.ts.map +1 -1
  49. package/dist/cli.js +6 -1
  50. package/dist/cli.js.map +1 -1
  51. package/dist/client/browser-dev.d.ts +27 -1
  52. package/dist/client/browser-dev.d.ts.map +1 -1
  53. package/dist/client/browser-entry/action-dispatch.d.ts +17 -0
  54. package/dist/client/browser-entry/action-dispatch.d.ts.map +1 -0
  55. package/dist/client/browser-entry/hmr.d.ts +21 -0
  56. package/dist/client/browser-entry/hmr.d.ts.map +1 -0
  57. package/dist/client/browser-entry/hydrate.d.ts +46 -0
  58. package/dist/client/browser-entry/hydrate.d.ts.map +1 -0
  59. package/dist/client/browser-entry/index.d.ts +30 -0
  60. package/dist/client/browser-entry/index.d.ts.map +1 -0
  61. package/dist/client/browser-entry/post-hydration.d.ts +26 -0
  62. package/dist/client/browser-entry/post-hydration.d.ts.map +1 -0
  63. package/dist/client/browser-entry/router-init.d.ts +23 -0
  64. package/dist/client/browser-entry/router-init.d.ts.map +1 -0
  65. package/dist/client/browser-entry/rsc-stream.d.ts +24 -0
  66. package/dist/client/browser-entry/rsc-stream.d.ts.map +1 -0
  67. package/dist/client/browser-entry/scroll.d.ts +19 -0
  68. package/dist/client/browser-entry/scroll.d.ts.map +1 -0
  69. package/dist/client/error-boundary.js +131 -1
  70. package/dist/client/error-boundary.js.map +1 -0
  71. package/dist/client/index.d.ts +4 -19
  72. package/dist/client/index.d.ts.map +1 -1
  73. package/dist/client/index.js +14 -1191
  74. package/dist/client/index.js.map +1 -1
  75. package/dist/client/internal.d.ts +18 -0
  76. package/dist/client/internal.d.ts.map +1 -0
  77. package/dist/client/internal.js +890 -0
  78. package/dist/client/internal.js.map +1 -0
  79. package/dist/client/navigation-context.d.ts.map +1 -1
  80. package/dist/client/router-ref.d.ts +1 -1
  81. package/dist/client/top-loader.d.ts +2 -2
  82. package/dist/client/use-link-status.d.ts +1 -1
  83. package/dist/client/{use-navigation-pending.d.ts → use-pending-navigation.d.ts} +4 -4
  84. package/dist/client/use-pending-navigation.d.ts.map +1 -0
  85. package/dist/client/use-router.d.ts +1 -1
  86. package/dist/codec.d.ts +10 -0
  87. package/dist/codec.d.ts.map +1 -1
  88. package/dist/codec.js +1 -1
  89. package/dist/config-types.d.ts +210 -0
  90. package/dist/config-types.d.ts.map +1 -0
  91. package/dist/content/index.d.ts +1 -10
  92. package/dist/content/index.d.ts.map +1 -1
  93. package/dist/content/index.js +0 -2
  94. package/dist/cookies/define-cookie.d.ts.map +1 -1
  95. package/dist/cookies/index.d.ts +0 -2
  96. package/dist/cookies/index.d.ts.map +1 -1
  97. package/dist/cookies/index.js +2 -3
  98. package/dist/index.d.ts +25 -288
  99. package/dist/index.d.ts.map +1 -1
  100. package/dist/index.js +261 -43
  101. package/dist/index.js.map +1 -1
  102. package/dist/plugin-context.d.ts +84 -0
  103. package/dist/plugin-context.d.ts.map +1 -0
  104. package/dist/plugins/adapter-build.d.ts +1 -1
  105. package/dist/plugins/adapter-build.d.ts.map +1 -1
  106. package/dist/plugins/build-manifest.d.ts +1 -1
  107. package/dist/plugins/build-manifest.d.ts.map +1 -1
  108. package/dist/plugins/build-report.d.ts +1 -1
  109. package/dist/plugins/build-report.d.ts.map +1 -1
  110. package/dist/plugins/content.d.ts +1 -1
  111. package/dist/plugins/content.d.ts.map +1 -1
  112. package/dist/plugins/dev-browser-logs.d.ts +1 -1
  113. package/dist/plugins/dev-browser-logs.d.ts.map +1 -1
  114. package/dist/plugins/dev-logs.d.ts +1 -1
  115. package/dist/plugins/dev-logs.d.ts.map +1 -1
  116. package/dist/plugins/dev-server.d.ts +1 -1
  117. package/dist/plugins/dev-server.d.ts.map +1 -1
  118. package/dist/plugins/entries.d.ts +1 -1
  119. package/dist/plugins/entries.d.ts.map +1 -1
  120. package/dist/plugins/fonts.d.ts +1 -1
  121. package/dist/plugins/fonts.d.ts.map +1 -1
  122. package/dist/plugins/mdx.d.ts +1 -1
  123. package/dist/plugins/mdx.d.ts.map +1 -1
  124. package/dist/plugins/routing.d.ts +1 -1
  125. package/dist/plugins/routing.d.ts.map +1 -1
  126. package/dist/plugins/shims.d.ts +1 -1
  127. package/dist/plugins/shims.d.ts.map +1 -1
  128. package/dist/plugins/static-build.d.ts +4 -4
  129. package/dist/plugins/static-build.d.ts.map +1 -1
  130. package/dist/routing/index.js +1 -1
  131. package/dist/search-params/define.d.ts +6 -6
  132. package/dist/search-params/define.d.ts.map +1 -1
  133. package/dist/search-params/index.d.ts +1 -2
  134. package/dist/search-params/index.d.ts.map +1 -1
  135. package/dist/search-params/index.js +4 -4
  136. package/dist/search-params/registry.d.ts +1 -1
  137. package/dist/search-params/registry.d.ts.map +1 -1
  138. package/dist/segment-params/define.d.ts +6 -6
  139. package/dist/segment-params/define.d.ts.map +1 -1
  140. package/dist/segment-params/index.d.ts +0 -1
  141. package/dist/segment-params/index.d.ts.map +1 -1
  142. package/dist/segment-params/index.js +3 -3
  143. package/dist/server/als-registry.d.ts +1 -1
  144. package/dist/server/dev-holding-server.d.ts +52 -0
  145. package/dist/server/dev-holding-server.d.ts.map +1 -0
  146. package/dist/server/dev-warnings.d.ts +1 -7
  147. package/dist/server/dev-warnings.d.ts.map +1 -1
  148. package/dist/server/index.d.ts +6 -45
  149. package/dist/server/index.d.ts.map +1 -1
  150. package/dist/server/index.js +7 -3272
  151. package/dist/server/index.js.map +1 -1
  152. package/dist/server/internal.d.ts +46 -0
  153. package/dist/server/internal.d.ts.map +1 -0
  154. package/dist/server/internal.js +2865 -0
  155. package/dist/server/internal.js.map +1 -0
  156. package/dist/server/pipeline.d.ts.map +1 -1
  157. package/dist/server/primitives.d.ts +41 -17
  158. package/dist/server/primitives.d.ts.map +1 -1
  159. package/dist/server/request-context.d.ts +45 -15
  160. package/dist/server/request-context.d.ts.map +1 -1
  161. package/dist/server/tracing.d.ts +4 -4
  162. package/dist/server/tracing.d.ts.map +1 -1
  163. package/dist/shims/headers.d.ts +2 -1
  164. package/dist/shims/headers.d.ts.map +1 -1
  165. package/dist/shims/navigation.d.ts +2 -1
  166. package/dist/shims/navigation.d.ts.map +1 -1
  167. package/package.json +19 -13
  168. package/src/adapters/cloudflare-kv-cache.ts +142 -0
  169. package/src/cache/handler-store.ts +2 -2
  170. package/src/cache/index.ts +74 -15
  171. package/src/cache/sizeof.ts +31 -0
  172. package/src/cli.ts +6 -1
  173. package/src/client/browser-dev.ts +128 -1
  174. package/src/client/browser-entry/action-dispatch.ts +116 -0
  175. package/src/client/browser-entry/hmr.ts +81 -0
  176. package/src/client/browser-entry/hydrate.ts +145 -0
  177. package/src/client/browser-entry/index.ts +138 -0
  178. package/src/client/browser-entry/post-hydration.ts +119 -0
  179. package/src/client/browser-entry/router-init.ts +184 -0
  180. package/src/client/browser-entry/rsc-stream.ts +157 -0
  181. package/src/client/browser-entry/scroll.ts +27 -0
  182. package/src/client/index.ts +10 -38
  183. package/src/client/internal.ts +57 -0
  184. package/src/client/navigation-context.ts +6 -2
  185. package/src/client/navigation-root.tsx +1 -1
  186. package/src/client/router-ref.ts +1 -1
  187. package/src/client/top-loader.tsx +2 -2
  188. package/src/client/use-link-status.ts +1 -1
  189. package/src/client/{use-navigation-pending.ts → use-pending-navigation.ts} +5 -5
  190. package/src/client/use-query-states.ts +2 -2
  191. package/src/client/use-router.ts +1 -1
  192. package/src/codec.ts +15 -0
  193. package/src/config-types.ts +208 -0
  194. package/src/content/index.ts +5 -13
  195. package/src/cookies/define-cookie.ts +9 -7
  196. package/src/cookies/index.ts +6 -5
  197. package/src/index.ts +84 -416
  198. package/src/plugin-context.ts +200 -0
  199. package/src/plugins/adapter-build.ts +1 -1
  200. package/src/plugins/build-manifest.ts +1 -1
  201. package/src/plugins/build-report.ts +1 -1
  202. package/src/plugins/content.ts +1 -1
  203. package/src/plugins/dev-browser-logs.ts +1 -1
  204. package/src/plugins/dev-logs.ts +1 -1
  205. package/src/plugins/dev-server.ts +16 -1
  206. package/src/plugins/entries.ts +2 -2
  207. package/src/plugins/fonts.ts +4 -3
  208. package/src/plugins/mdx.ts +1 -1
  209. package/src/plugins/routing.ts +1 -1
  210. package/src/plugins/shims.ts +53 -5
  211. package/src/plugins/static-build.ts +8 -6
  212. package/src/search-params/define.ts +22 -22
  213. package/src/search-params/index.ts +3 -3
  214. package/src/search-params/registry.ts +1 -1
  215. package/src/segment-params/define.ts +18 -18
  216. package/src/segment-params/index.ts +2 -1
  217. package/src/server/action-handler.ts +1 -1
  218. package/src/server/als-registry.ts +3 -3
  219. package/src/server/dev-holding-server.ts +185 -0
  220. package/src/server/dev-warnings.ts +2 -21
  221. package/src/server/html-injectors.ts +3 -3
  222. package/src/server/index.ts +25 -180
  223. package/src/server/internal.ts +169 -0
  224. package/src/server/pipeline.ts +12 -7
  225. package/src/server/primitives.ts +71 -30
  226. package/src/server/request-context.ts +77 -39
  227. package/src/server/route-element-builder.ts +1 -1
  228. package/src/server/rsc-entry/index.ts +2 -2
  229. package/src/server/rsc-entry/ssr-renderer.ts +1 -1
  230. package/src/server/slot-resolver.ts +1 -1
  231. package/src/server/tracing.ts +6 -6
  232. package/src/server/tree-builder.ts +1 -1
  233. package/src/shims/headers.ts +5 -1
  234. package/src/shims/navigation.ts +5 -1
  235. package/dist/_chunks/als-registry-BJARkOcu.js.map +0 -1
  236. package/dist/_chunks/define-CGuYoRHU.js.map +0 -1
  237. package/dist/_chunks/define-Dz1bqwaS.js.map +0 -1
  238. package/dist/_chunks/error-boundary-D9hzsveV.js +0 -216
  239. package/dist/_chunks/error-boundary-D9hzsveV.js.map +0 -1
  240. package/dist/_chunks/format-Rn922VH2.js.map +0 -1
  241. package/dist/_chunks/handler-store-BVePM7hp.js.map +0 -1
  242. package/dist/_chunks/request-context-CywiO4jV.js.map +0 -1
  243. package/dist/_chunks/segment-context-hzuJ048X.js.map +0 -1
  244. package/dist/_chunks/use-query-states-DAhgj8Gx.js.map +0 -1
  245. package/dist/client/browser-entry.d.ts +0 -21
  246. package/dist/client/browser-entry.d.ts.map +0 -1
  247. package/dist/client/use-navigation-pending.d.ts.map +0 -1
  248. package/src/client/browser-entry.ts +0 -846
@@ -1,11 +1,13 @@
1
1
  "use client";
2
2
  import { n as __exportAll } from "../_chunks/chunk-DYhsFzuS.js";
3
3
  import { t as classifyUrlSegment } from "../_chunks/segment-classify-BDNn6EzD.js";
4
- import { n as useQueryStates, t as bindUseQueryStates } from "../_chunks/use-query-states-DAhgj8Gx.js";
5
- import { n as useSegmentContext, r as mergePreservedSearchParams, t as SegmentProvider } from "../_chunks/segment-context-hzuJ048X.js";
6
- import { a as _setCachedSearch, c as cachedSearch, d as globalRouter, i as setSsrData, l as cachedSearchParams, n as clearSsrData, o as _setCurrentParams, r as getSsrData, s as _setGlobalRouter, t as TimberErrorBoundary, u as currentParams } from "../_chunks/error-boundary-D9hzsveV.js";
7
- import { t as _registerUseCookieModule } from "../_chunks/define-cookie-B5mewxwM.js";
8
- import React, { cloneElement, createContext, createElement, isValidElement, useActionState as useActionState$1, useContext, useEffect, useRef, useState, useSyncExternalStore, useTransition } from "react";
4
+ import { n as useQueryStates } from "../_chunks/use-query-states-Lo_s_pw2.js";
5
+ import { t as mergePreservedSearchParams } from "../_chunks/merge-search-params-Cm_KIWDX.js";
6
+ import { c as cachedSearchParams, i as _setCachedSearch, n as getSsrData, s as cachedSearch } from "../_chunks/ssr-data-DzuI0bIV.js";
7
+ import { n as useSegmentContext } from "../_chunks/segment-context-fHFLF1PE.js";
8
+ import { c as usePendingNavigationUrl, d as setLinkForCurrentNavigation, f as unmountLinkForCurrentNavigation, l as IDLE_LINK_STATUS, m as getRouterOrNull, n as useSegmentParams, s as useNavigationContext, u as PENDING_LINK_STATUS } from "../_chunks/use-params-B1AuhI1p.js";
9
+ import { t as _registerUseCookieModule } from "../_chunks/define-cookie-C2IkoFGN.js";
10
+ import { createContext, useActionState as useActionState$1, useContext, useEffect, useRef, useState, useSyncExternalStore, useTransition } from "react";
9
11
  import { jsx } from "react/jsx-runtime";
10
12
  //#region src/client/use-link-status.ts
11
13
  /**
@@ -17,7 +19,7 @@ var LinkStatusContext = createContext({ pending: false });
17
19
  * Returns `{ pending: true }` while the nearest parent `<Link>` component's
18
20
  * navigation is in flight. Must be used inside a `<Link>` component's children.
19
21
  *
20
- * Unlike `useNavigationPending()` which is global, this hook is scoped to
22
+ * Unlike `usePendingNavigation()` which is global, this hook is scoped to
21
23
  * the nearest parent `<Link>` — only the link the user clicked shows pending.
22
24
  *
23
25
  * ```tsx
@@ -41,72 +43,6 @@ var LinkStatusContext = createContext({ pending: false });
41
43
  function useLinkStatus() {
42
44
  return useContext(LinkStatusContext);
43
45
  }
44
- //#endregion
45
- //#region src/client/router-ref.ts
46
- /**
47
- * Set the global router instance. Called once during bootstrap.
48
- */
49
- function setGlobalRouter(router) {
50
- _setGlobalRouter(router);
51
- }
52
- /**
53
- * Get the global router instance. Throws if called before bootstrap.
54
- * Used by client-side hooks (useNavigationPending, etc.)
55
- */
56
- function getRouter() {
57
- if (!globalRouter) throw new Error("[timber] Router not initialized. getRouter() was called before bootstrap().");
58
- return globalRouter;
59
- }
60
- /**
61
- * Get the global router instance or null if not yet initialized.
62
- * Used by useRouter() methods to avoid silent failures — callers
63
- * can log a meaningful warning instead of silently no-oping.
64
- */
65
- function getRouterOrNull() {
66
- return globalRouter;
67
- }
68
- //#endregion
69
- //#region src/client/link-pending-store.ts
70
- var LINK_PENDING_KEY = Symbol.for("__timber_link_pending");
71
- /** Status object indicating link is pending — shared reference */
72
- var PENDING_LINK_STATUS = { pending: true };
73
- /** Status object indicating link is idle — shared reference */
74
- var IDLE_LINK_STATUS = { pending: false };
75
- function getStore() {
76
- const g = globalThis;
77
- if (!g[LINK_PENDING_KEY]) g[LINK_PENDING_KEY] = {
78
- current: null,
79
- navId: 0
80
- };
81
- return g[LINK_PENDING_KEY];
82
- }
83
- /**
84
- * Register the link instance that initiated the current navigation.
85
- *
86
- * Called from <Link>'s click handler before router.navigate().
87
- * - Resets the previous pending link to IDLE (urgent update, immediate)
88
- * - Does NOT set the new link to PENDING here — the Link's click handler
89
- * calls setLinkStatus(PENDING) directly for the eager show
90
- * - Increments the navId counter for stale-clear protection
91
- *
92
- * Pass `null` to clear (e.g., for programmatic navigations).
93
- */
94
- function setLinkForCurrentNavigation(link) {
95
- const store = getStore();
96
- const prev = store.current;
97
- if (prev && prev !== link) prev.setLinkStatus(IDLE_LINK_STATUS);
98
- store.current = link;
99
- store.navId++;
100
- }
101
- /**
102
- * Unmount a link instance from navigation tracking. Called when a Link
103
- * component unmounts while it is the current navigation link. Prevents
104
- * calling setState on an unmounted component.
105
- */
106
- function unmountLinkForCurrentNavigation(link) {
107
- const store = getStore();
108
- if (store.current === link) store.current = null;
109
- }
110
46
  /**
111
47
  * Store metadata from Link's onClick for the next navigate event.
112
48
  * Called synchronously in the click handler — the navigate event
@@ -114,211 +50,6 @@ function unmountLinkForCurrentNavigation(link) {
114
50
  */
115
51
  function setNavLinkMetadata(metadata) {}
116
52
  //#endregion
117
- //#region src/client/navigation-context.ts
118
- /**
119
- * NavigationContext — React context for navigation state.
120
- *
121
- * Holds the current route params and pathname, updated atomically
122
- * with the RSC tree on each navigation. This replaces the previous
123
- * useSyncExternalStore approach for useSegmentParams() and usePathname(),
124
- * which suffered from a timing gap: the new tree could commit before
125
- * the external store re-renders fired, causing a frame where both
126
- * old and new active states were visible simultaneously.
127
- *
128
- * By wrapping the RSC payload element in NavigationProvider inside
129
- * renderRoot(), the context value and the element tree are passed to
130
- * reactRoot.render() in the same call — atomic by construction.
131
- * All consumers (useParams, usePathname) see the new values in the
132
- * same render pass as the new tree.
133
- *
134
- * During SSR, no NavigationProvider is mounted. Hooks fall back to
135
- * the ALS-backed getSsrData() for per-request isolation.
136
- *
137
- * IMPORTANT: createContext and useContext are NOT available in the RSC
138
- * environment (React Server Components use a stripped-down React).
139
- * The context is lazily initialized on first access, and all functions
140
- * that depend on these APIs are safe to call from any environment —
141
- * they return null or no-op when the APIs aren't available.
142
- *
143
- * SINGLETON GUARANTEE: All shared mutable state uses globalThis via
144
- * Symbol.for keys. The RSC client bundler can duplicate this module
145
- * across chunks (browser-entry graph + client-reference graph). With
146
- * ESM output, each chunk gets its own module scope — module-level
147
- * variables would create separate singleton instances per chunk.
148
- * globalThis guarantees a single instance regardless of duplication.
149
- *
150
- * This workaround will be removed when Rolldown ships `format: 'app'`
151
- * (module registry format that deduplicates like webpack/Turbopack).
152
- * See design/27-chunking-strategy.md.
153
- *
154
- * See design/19-client-navigation.md §"NavigationContext"
155
- */
156
- /**
157
- * The context is created lazily to avoid calling createContext at module
158
- * level. In the RSC environment, React.createContext doesn't exist —
159
- * calling it at import time would crash the server.
160
- *
161
- * Context instances are stored on globalThis (NOT in module-level
162
- * variables) because the ESM bundler can duplicate this module across
163
- * chunks. Module-level variables would create separate instances per
164
- * chunk — the provider in NavigationRoot (index chunk) would use
165
- * context A while the consumer in useNavigationPending (shared chunk)
166
- * reads from context B. globalThis guarantees a single instance.
167
- *
168
- * See design/27-chunking-strategy.md §"Singleton Safety"
169
- */
170
- var NAV_CTX_KEY = Symbol.for("__timber_nav_ctx");
171
- var PENDING_CTX_KEY = Symbol.for("__timber_pending_nav_ctx");
172
- function getOrCreateContext() {
173
- const existing = globalThis[NAV_CTX_KEY];
174
- if (existing !== void 0) return existing;
175
- if (typeof React.createContext === "function") {
176
- const ctx = React.createContext(null);
177
- globalThis[NAV_CTX_KEY] = ctx;
178
- return ctx;
179
- }
180
- }
181
- /**
182
- * Read the navigation context. Returns null during SSR (no provider)
183
- * or in the RSC environment (no context available).
184
- * Internal — used by useSegmentParams() and usePathname().
185
- */
186
- function useNavigationContext() {
187
- const ctx = getOrCreateContext();
188
- if (!ctx) return null;
189
- if (typeof React.useContext !== "function") return null;
190
- return React.useContext(ctx);
191
- }
192
- /**
193
- * Wraps children with NavigationContext.Provider.
194
- *
195
- * Used in browser-entry.ts renderRoot to wrap the RSC payload element
196
- * so that navigation state updates atomically with the tree render.
197
- */
198
- function NavigationProvider({ value, children }) {
199
- const ctx = getOrCreateContext();
200
- if (!ctx) return children;
201
- return createElement(ctx.Provider, { value }, children);
202
- }
203
- /**
204
- * Navigation state communicated between the router and renderRoot.
205
- *
206
- * The router calls setNavigationState() before renderRoot(). The
207
- * renderRoot callback reads via getNavigationState() to create the
208
- * NavigationProvider with the correct params/pathname.
209
- *
210
- * This is NOT used by hooks directly — hooks read from React context.
211
- *
212
- * Stored on globalThis (like the context instances above) because the
213
- * router lives in one chunk while renderRoot lives in another. Module-
214
- * level variables would be separate per chunk.
215
- */
216
- var NAV_STATE_KEY = Symbol.for("__timber_nav_state");
217
- function _getNavStateStore() {
218
- const g = globalThis;
219
- if (!g[NAV_STATE_KEY]) g[NAV_STATE_KEY] = { current: {
220
- params: {},
221
- pathname: "/"
222
- } };
223
- return g[NAV_STATE_KEY];
224
- }
225
- function setNavigationState(state) {
226
- _getNavStateStore().current = state;
227
- }
228
- function getNavigationState() {
229
- return _getNavStateStore().current;
230
- }
231
- /**
232
- * Separate context for the in-flight navigation URL. Provided by
233
- * NavigationRoot (urgent useState), consumed by useNavigationPending
234
- * and TopLoader. Per-link pending state uses useOptimistic instead
235
- * (see link-pending-store.ts).
236
- *
237
- * Uses globalThis via Symbol.for for the same reason as NavigationContext
238
- * above — the bundler may duplicate this module across chunks, and module-
239
- * level variables would create separate context instances.
240
- */
241
- function getOrCreatePendingContext() {
242
- const existing = globalThis[PENDING_CTX_KEY];
243
- if (existing !== void 0) return existing;
244
- if (typeof React.createContext === "function") {
245
- const ctx = React.createContext(null);
246
- globalThis[PENDING_CTX_KEY] = ctx;
247
- return ctx;
248
- }
249
- }
250
- /**
251
- * Read the pending navigation URL from context.
252
- * Returns null during SSR (no provider) or in the RSC environment.
253
- */
254
- function usePendingNavigationUrl() {
255
- const ctx = getOrCreatePendingContext();
256
- if (!ctx) return null;
257
- if (typeof React.useContext !== "function") return null;
258
- return React.useContext(ctx);
259
- }
260
- //#endregion
261
- //#region src/client/top-loader.tsx
262
- /**
263
- * TopLoader — Built-in progress bar for client navigations.
264
- *
265
- * Shows an animated progress bar at the top of the viewport while an RSC
266
- * navigation is in flight. Injected automatically by the framework into
267
- * NavigationRoot — users never render this component directly.
268
- *
269
- * Configuration is via timber.config.ts `topLoader` key. Enabled by default.
270
- * Users who want a fully custom progress indicator disable the built-in one
271
- * (`topLoader: { enabled: false }`) and use `useNavigationPending()` directly.
272
- *
273
- * Animation approach: pure CSS @keyframes. The bar crawls from 0% to ~90%
274
- * width over ~30s using ease-out timing. When navigation completes, the bar
275
- * snaps to 100% and fades out over 200ms. No JS animation loops (RAF, setInterval).
276
- *
277
- * Phase transitions are derived synchronously during render (React's
278
- * getDerivedStateFromProps pattern) — no useEffect needed for state tracking.
279
- * The finishing → hidden cleanup uses onTransitionEnd from the CSS transition.
280
- *
281
- * When delay > 0, CSS animation-delay + a visibility keyframe ensure the bar
282
- * stays invisible during the delay period. If navigation finishes before the
283
- * delay, the bar was never visible so the finish transition is also invisible.
284
- *
285
- * See design/19-client-navigation.md §"useNavigationPending()"
286
- * See LOCAL-336 for design decisions.
287
- */
288
- //#endregion
289
- //#region src/client/navigation-root.tsx
290
- /**
291
- * Module-level flag indicating a hard (MPA) navigation is in progress.
292
- *
293
- * When true:
294
- * - NavigationRoot throws an unresolved thenable to suspend forever,
295
- * preventing React from rendering children during page teardown
296
- * (avoids "Rendered more hooks" crashes).
297
- * - The Navigation API handler skips interception, letting the browser
298
- * perform a full page load (prevents infinite loops where
299
- * window.location.href → navigate event → router.navigate → 500 →
300
- * window.location.href → ...).
301
- *
302
- * Uses globalThis for singleton guarantee across chunks (same pattern
303
- * as NavigationContext). See design/19-client-navigation.md §"Singleton
304
- * Guarantee via globalThis".
305
- */
306
- var HARD_NAV_KEY = Symbol.for("__timber_hard_navigating");
307
- function getHardNavStore() {
308
- const g = globalThis;
309
- if (!g[HARD_NAV_KEY]) g[HARD_NAV_KEY] = { value: false };
310
- return g[HARD_NAV_KEY];
311
- }
312
- /**
313
- * Set the hard-navigating flag. Call this BEFORE setting
314
- * window.location.href or window.location.reload() to prevent:
315
- * 1. React from rendering children during page teardown
316
- * 2. Navigation API from intercepting the hard navigation
317
- */
318
- function setHardNavigating(value) {
319
- getHardNavStore().value = value;
320
- }
321
- //#endregion
322
53
  //#region src/client/navigation-api.ts
323
54
  /**
324
55
  * Returns true if the Navigation API is available in the current environment.
@@ -545,915 +276,7 @@ var Link = function LinkImpl(props) {
545
276
  });
546
277
  };
547
278
  //#endregion
548
- //#region src/client/segment-cache.ts
549
- /**
550
- * Maintains the client-side segment tree representing currently mounted
551
- * layouts and pages. Used for navigation reconciliation — the router diffs
552
- * new routes against this tree to determine which segments to re-fetch.
553
- */
554
- var SegmentCache = class {
555
- root;
556
- get(segment) {
557
- if (segment === "/" || segment === this.root?.segment) return this.root;
558
- }
559
- set(segment, node) {
560
- if (segment === "/" || !this.root) this.root = node;
561
- }
562
- clear() {
563
- this.root = void 0;
564
- }
565
- /**
566
- * Serialize the mounted segment tree for the X-Timber-State-Tree header.
567
- * Only includes sync segments — async segments are excluded because the
568
- * server must always re-render them (they may depend on request context).
569
- *
570
- * When mergeableFilter is provided, only segments whose paths are in the
571
- * set are included. This ensures the server only skips segments that the
572
- * client can actually merge (i.e., segments whose cached element tree
573
- * contains an inner SegmentProvider the merger can splice into).
574
- *
575
- * This is a performance optimization only, NOT a security boundary.
576
- * The server always runs all access.ts files regardless of the state tree.
577
- */
578
- serializeStateTree(mergeableFilter) {
579
- const segments = [];
580
- if (this.root) collectSyncSegments(this.root, segments, mergeableFilter);
581
- return { segments };
582
- }
583
- };
584
- /** Recursively collect sync segment paths from the tree */
585
- function collectSyncSegments(node, out, mergeableFilter) {
586
- if (!node.isAsync && (!mergeableFilter || mergeableFilter.has(node.segment))) out.push(node.segment);
587
- for (const child of node.children.values()) collectSyncSegments(child, out, mergeableFilter);
588
- }
589
- /**
590
- * Build a SegmentNode tree from flat segment metadata.
591
- *
592
- * Takes an ordered list of segment descriptors (root → leaf) from the
593
- * server's X-Timber-Segments header and constructs the hierarchical
594
- * tree structure that SegmentCache expects.
595
- *
596
- * Each segment is nested as a child of the previous one, forming a
597
- * linear chain from root to leaf. The leaf segment (page) is excluded
598
- * from the tree — pages are never cached across navigations.
599
- */
600
- function buildSegmentTree(segments) {
601
- if (segments.length === 0) return void 0;
602
- const layouts = segments.length > 1 ? segments.slice(0, -1) : segments;
603
- let root;
604
- let parent;
605
- for (const info of layouts) {
606
- const node = {
607
- segment: info.path,
608
- payload: null,
609
- isAsync: info.isAsync,
610
- children: /* @__PURE__ */ new Map()
611
- };
612
- if (!root) root = node;
613
- if (parent) parent.children.set(info.path, node);
614
- parent = node;
615
- }
616
- return root;
617
- }
618
- /**
619
- * Short-lived cache for hover-triggered prefetches. Entries expire after
620
- * 30 seconds. When a link is clicked, the prefetched payload is consumed
621
- * (moved to the history stack) and removed from this cache.
622
- *
623
- * timber.js does NOT prefetch on viewport intersection — only explicit
624
- * hover on <Link prefetch> triggers a prefetch.
625
- */
626
- var PrefetchCache = class PrefetchCache {
627
- static TTL_MS = 3e4;
628
- entries = /* @__PURE__ */ new Map();
629
- set(url, result) {
630
- this.entries.set(url, {
631
- result,
632
- expiresAt: Date.now() + PrefetchCache.TTL_MS
633
- });
634
- }
635
- get(url) {
636
- const entry = this.entries.get(url);
637
- if (!entry) return void 0;
638
- if (Date.now() >= entry.expiresAt) {
639
- this.entries.delete(url);
640
- return;
641
- }
642
- return entry.result;
643
- }
644
- /** Get and remove the entry (used when navigation consumes a prefetch) */
645
- consume(url) {
646
- const result = this.get(url);
647
- if (result !== void 0) this.entries.delete(url);
648
- return result;
649
- }
650
- };
651
- //#endregion
652
- //#region src/client/history.ts
653
- /**
654
- * Session-lived history stack keyed by URL. Enables instant back/forward
655
- * navigation without a server roundtrip.
656
- *
657
- * On forward navigation, the new page's payload is pushed onto the stack.
658
- * On popstate, the cached payload is replayed instantly.
659
- *
660
- * Supports two keying modes:
661
- * - **URL-keyed** (default): entries keyed by pathname + search.
662
- * Used with the History API fallback.
663
- * - **Entry-key + URL**: when the Navigation API is available,
664
- * entries can also be stored by Navigation entry key for
665
- * disambiguation of duplicate URLs in the history stack.
666
- * Falls back to URL lookup when entry key is not found.
667
- *
668
- * Scroll positions are stored in history.state or Navigation API entry
669
- * state, not in this stack — see design/19-client-navigation.md §Scroll Restoration.
670
- *
671
- * Entries persist for the session duration (no expiry) and are cleared
672
- * when the tab is closed — matching browser back-button behavior.
673
- */
674
- var HistoryStack = class {
675
- entries = /* @__PURE__ */ new Map();
676
- /** Entries keyed by Navigation API entry key for duplicate URL disambiguation. */
677
- entryKeyMap = /* @__PURE__ */ new Map();
678
- push(url, entry, entryKey) {
679
- this.entries.set(url, entry);
680
- if (entryKey) this.entryKeyMap.set(entryKey, entry);
681
- }
682
- /**
683
- * Get an entry. When an entry key is provided (Navigation API),
684
- * tries the entry-key map first for accurate disambiguation of
685
- * duplicate URLs, then falls back to URL lookup.
686
- */
687
- get(url, entryKey) {
688
- if (entryKey) {
689
- const byKey = this.entryKeyMap.get(entryKey);
690
- if (byKey) return byKey;
691
- }
692
- return this.entries.get(url);
693
- }
694
- has(url) {
695
- return this.entries.has(url);
696
- }
697
- };
698
- //#endregion
699
- //#region src/client/use-params.ts
700
- /**
701
- * Set the current route params in the module-level store.
702
- *
703
- * Called by the router on each navigation. This updates the fallback
704
- * snapshot used by tests and by the hook when called outside a React
705
- * component (no NavigationContext available).
706
- *
707
- * On the client, the primary reactivity path is NavigationContext —
708
- * the router calls setNavigationState() then renderRoot() which wraps
709
- * the element in NavigationProvider. setCurrentParams is still called
710
- * for the module-level fallback.
711
- *
712
- * During SSR, params are also available via getSsrData().params
713
- * (ALS-backed).
714
- */
715
- function setCurrentParams(params) {
716
- _setCurrentParams(params);
717
- }
718
- function useSegmentParams(_route) {
719
- try {
720
- const navContext = useNavigationContext();
721
- if (navContext !== null) return navContext.params;
722
- } catch {}
723
- return getSsrData()?.params ?? currentParams;
724
- }
725
- //#endregion
726
- //#region src/client/segment-merger.ts
727
- /**
728
- * Segment Merger — client-side tree merging for partial RSC payloads.
729
- *
730
- * When the server skips rendering sync layouts (because the client already
731
- * has them cached), the RSC payload is missing outer segment wrappers.
732
- * This module reconstructs the full element tree by splicing the partial
733
- * payload into cached segment subtrees.
734
- *
735
- * The approach:
736
- * 1. After each full RSC payload render, walk the decoded element tree
737
- * and cache each segment's subtree (identified by SegmentProvider boundaries)
738
- * 2. When a partial payload arrives, wrap it with cached segment elements
739
- * using React.cloneElement to preserve component identity
740
- *
741
- * React.cloneElement preserves the element's `type` — React sees the same
742
- * component at the same tree position and reconciles (preserving state)
743
- * rather than remounting. This is how layout state survives navigations.
744
- *
745
- * Design docs: 19-client-navigation.md §"Navigation Reconciliation"
746
- * Security: access.ts runs on the server regardless of skipping — this
747
- * is a performance optimization only. See 13-security.md.
748
- */
749
- /**
750
- * Cache of React element subtrees per segment path.
751
- * Updated after each navigation with the full decoded RSC element tree.
752
- */
753
- var SegmentElementCache = class {
754
- entries = /* @__PURE__ */ new Map();
755
- get(segmentPath) {
756
- return this.entries.get(segmentPath);
757
- }
758
- set(segmentPath, entry) {
759
- this.entries.set(segmentPath, entry);
760
- }
761
- has(segmentPath) {
762
- return this.entries.has(segmentPath);
763
- }
764
- clear() {
765
- this.entries.clear();
766
- }
767
- get size() {
768
- return this.entries.size;
769
- }
770
- /**
771
- * Get the set of segment paths that are safe for the server to skip.
772
- * Only segments with an inner SegmentProvider (hasMergeableChild) are
773
- * included — the merger can only replace inner SegmentProviders, not
774
- * pages embedded in layout output. Used to filter the state tree.
775
- *
776
- * Returns an empty set if the element cache is empty (no elements
777
- * cached yet). This is the safe default — an empty set means no
778
- * segments pass the filter, so the state tree is empty and the server
779
- * does a full render. The element cache is populated lazily after the
780
- * first SPA navigation (RSC-decoded elements from hydration are
781
- * thenables that can't be walked until React resolves them).
782
- */
783
- getMergeablePaths() {
784
- const paths = /* @__PURE__ */ new Set();
785
- for (const [, entry] of this.entries) if (entry.hasMergeableChild) paths.add(entry.segmentPath);
786
- return paths;
787
- }
788
- };
789
- /**
790
- * Check if a React element is a SegmentProvider by looking for the
791
- * `segments` prop (an array of path segments). This is the only
792
- * component that receives this prop shape.
793
- */
794
- function isSegmentProvider(element) {
795
- if (!isValidElement(element)) return false;
796
- const props = element.props;
797
- return Array.isArray(props.segments);
798
- }
799
- /**
800
- * Extract the segment path from a SegmentProvider element.
801
- *
802
- * Uses the `segmentId` prop if available (set by the server for route groups
803
- * to distinguish siblings that share the same urlPath). Falls back to
804
- * reconstructing from the `segments` array prop.
805
- */
806
- function getSegmentPath(element) {
807
- const props = element.props;
808
- if (props.segmentId) return props.segmentId;
809
- const filtered = props.segments.filter(Boolean);
810
- return filtered.length === 0 ? "/" : "/" + filtered.join("/");
811
- }
812
- /**
813
- * Walk a React element tree and extract all SegmentProvider boundaries.
814
- * Returns an ordered list of segment entries from outermost to innermost.
815
- *
816
- * This only finds SegmentProviders along the main children path — it does
817
- * not descend into parallel routes/slots (those are separate subtrees).
818
- */
819
- function extractSegments(element) {
820
- const segments = [];
821
- walkForSegments(element, segments);
822
- for (let i = 0; i < segments.length; i++) segments[i].hasMergeableChild = i < segments.length - 1;
823
- return segments;
824
- }
825
- function walkForSegments(node, out) {
826
- if (!isValidElement(node)) return;
827
- const el = node;
828
- const props = el.props;
829
- if (isSegmentProvider(node)) {
830
- out.push({
831
- segmentPath: getSegmentPath(el),
832
- element: el,
833
- hasMergeableChild: false
834
- });
835
- walkChildren(props.children, out);
836
- return;
837
- }
838
- walkChildren(props.children, out);
839
- }
840
- function walkChildren(children, out) {
841
- if (children == null) return;
842
- if (Array.isArray(children)) for (const child of children) walkForSegments(child, out);
843
- else walkForSegments(children, out);
844
- }
845
- /**
846
- * Cache all segment subtrees from a fully-rendered RSC element tree.
847
- * Call this after every full RSC payload render (navigate, refresh, hydration).
848
- */
849
- function cacheSegmentElements(element, cache) {
850
- const segments = extractSegments(element);
851
- for (const entry of segments) cache.set(entry.segmentPath, entry);
852
- }
853
- function findSegmentProviderPath(node, targetPath) {
854
- const children = node.props.children;
855
- if (children == null) return null;
856
- if (Array.isArray(children)) for (let i = 0; i < children.length; i++) {
857
- const child = children[i];
858
- if (!isValidElement(child)) continue;
859
- if (isSegmentProvider(child)) {
860
- if (!targetPath || getSegmentPath(child) === targetPath) return [{
861
- element: node,
862
- childIndex: i
863
- }];
864
- }
865
- const deeper = findSegmentProviderPath(child, targetPath);
866
- if (deeper) return [{
867
- element: node,
868
- childIndex: i
869
- }, ...deeper];
870
- }
871
- else if (isValidElement(children)) {
872
- if (isSegmentProvider(children)) {
873
- if (!targetPath || getSegmentPath(children) === targetPath) return [{
874
- element: node,
875
- childIndex: -1
876
- }];
877
- }
878
- const deeper = findSegmentProviderPath(children, targetPath);
879
- if (deeper) return [{
880
- element: node,
881
- childIndex: -1
882
- }, ...deeper];
883
- }
884
- return null;
885
- }
886
- /**
887
- * Replace a nested SegmentProvider within a cached element tree with
888
- * new content. Uses cloneElement along the path to produce a new tree
889
- * with preserved component identity at every level except the replaced node.
890
- *
891
- * @param cachedElement The cached SegmentProvider element for this segment
892
- * @param newInnerContent The new React element to splice in at the inner segment position
893
- * @param innerSegmentPath The path of the inner segment to replace (optional — replaces first found)
894
- * @returns New element tree with the inner segment replaced
895
- */
896
- function replaceInnerSegment(cachedElement, newInnerContent, innerSegmentPath) {
897
- const path = findSegmentProviderPath(cachedElement, innerSegmentPath);
898
- if (!path || path.length === 0) return cachedElement;
899
- let replacement = newInnerContent;
900
- for (let i = path.length - 1; i >= 0; i--) {
901
- const { element, childIndex } = path[i];
902
- if (childIndex === -1) replacement = cloneElement(element, {}, replacement);
903
- else {
904
- const newChildren = [...element.props.children];
905
- newChildren[childIndex] = replacement;
906
- replacement = cloneElement(element, {}, ...newChildren);
907
- }
908
- }
909
- return replacement;
910
- }
911
- /**
912
- * Merge a partial RSC payload with cached segment elements.
913
- *
914
- * When the server skips segments, the partial payload starts from the
915
- * first non-skipped segment. This function wraps it with cached elements
916
- * for the skipped segments, producing a full tree that React can
917
- * reconcile with the mounted tree (preserving layout state).
918
- *
919
- * @param partialPayload The RSC payload element (may be partial)
920
- * @param skippedSegments Ordered list of segment paths that were skipped (outermost first)
921
- * @param cache The segment element cache
922
- * @returns The merged full element tree, or the partial payload if merging isn't possible
923
- */
924
- function mergeSegmentTree(partialPayload, skippedSegments, cache) {
925
- if (!isValidElement(partialPayload)) return partialPayload;
926
- if (skippedSegments.length === 0) return partialPayload;
927
- let result = partialPayload;
928
- for (let i = skippedSegments.length - 1; i >= 0; i--) {
929
- const segmentPath = skippedSegments[i];
930
- const cached = cache.get(segmentPath);
931
- if (!cached) return partialPayload;
932
- result = replaceInnerSegment(cached.element, result);
933
- }
934
- return result;
935
- }
936
- //#endregion
937
- //#region src/client/rsc-fetch.ts
938
- var RSC_CONTENT_TYPE = "text/x-component";
939
- /**
940
- * Generate a short random cache-busting ID (5 chars, a-z0-9).
941
- * Matches the format Next.js uses for _rsc params.
942
- */
943
- function generateCacheBustId() {
944
- const chars = "abcdefghijklmnopqrstuvwxyz0123456789";
945
- let id = "";
946
- for (let i = 0; i < 5; i++) id += chars[Math.random() * 36 | 0];
947
- return id;
948
- }
949
- /**
950
- * Append a `_rsc=<id>` query parameter to the URL.
951
- * Follows Next.js's pattern — prevents CDN/browser from serving cached HTML
952
- * for RSC navigation requests and signals that this is an RSC fetch.
953
- */
954
- function appendRscParam(url) {
955
- return `${url}${url.includes("?") ? "&" : "?"}_rsc=${generateCacheBustId()}`;
956
- }
957
- /**
958
- * The client's deployment ID, set at bootstrap from the runtime config.
959
- * Sent with every RSC/action request for version skew detection.
960
- * Null in dev mode. See TIM-446.
961
- */
962
- var clientDeploymentId = null;
963
- /** Header name used by the server to signal a version skew reload. */
964
- var RELOAD_HEADER = "X-Timber-Reload";
965
- /** Header name for the client's deployment ID. */
966
- var DEPLOYMENT_ID_HEADER = "X-Timber-Deployment-Id";
967
- /**
968
- * Check if a response signals a version skew reload.
969
- * Triggers a full page reload if the server indicates the client is stale.
970
- */
971
- function checkReloadSignal(response) {
972
- return response.headers.get(RELOAD_HEADER) === "1";
973
- }
974
- function buildRscHeaders(stateTree, currentUrl) {
975
- const headers = { Accept: RSC_CONTENT_TYPE };
976
- if (stateTree) headers["X-Timber-State-Tree"] = JSON.stringify(stateTree);
977
- if (currentUrl) headers["X-Timber-URL"] = currentUrl;
978
- if (clientDeploymentId) headers[DEPLOYMENT_ID_HEADER] = clientDeploymentId;
979
- return headers;
980
- }
981
- /**
982
- * Extract head elements from the X-Timber-Head response header.
983
- * Returns null if the header is missing or malformed.
984
- */
985
- function extractHeadElements(response) {
986
- const header = response.headers.get("X-Timber-Head");
987
- if (!header) return null;
988
- try {
989
- return JSON.parse(decodeURIComponent(header));
990
- } catch {
991
- return null;
992
- }
993
- }
994
- /**
995
- * Extract segment metadata from the X-Timber-Segments response header.
996
- * Returns null if the header is missing or malformed.
997
- *
998
- * Format: JSON array of {path, isAsync} objects describing the rendered
999
- * segment chain from root to leaf. Used to populate the client-side
1000
- * segment cache for state tree diffing on subsequent navigations.
1001
- */
1002
- function extractSegmentInfo(response) {
1003
- const header = response.headers.get("X-Timber-Segments");
1004
- if (!header) return null;
1005
- try {
1006
- return JSON.parse(header);
1007
- } catch {
1008
- return null;
1009
- }
1010
- }
1011
- /**
1012
- * Extract skipped segment paths from the X-Timber-Skipped-Segments header.
1013
- * Returns null if the header is missing or malformed.
1014
- *
1015
- * When the server skips sync layouts the client already has cached,
1016
- * it sends this header listing the skipped segment paths (outermost first).
1017
- * The client uses this to merge the partial payload with cached segments.
1018
- */
1019
- function extractSkippedSegments(response) {
1020
- const header = response.headers.get("X-Timber-Skipped-Segments");
1021
- if (!header) return null;
1022
- try {
1023
- const parsed = JSON.parse(header);
1024
- return Array.isArray(parsed) ? parsed : null;
1025
- } catch {
1026
- return null;
1027
- }
1028
- }
1029
- /**
1030
- * Extract route params from the X-Timber-Params response header.
1031
- * Returns null if the header is missing or malformed.
1032
- *
1033
- * Used to populate useSegmentParams() after client-side navigation.
1034
- */
1035
- function extractParams(response) {
1036
- const header = response.headers.get("X-Timber-Params");
1037
- if (!header) return null;
1038
- try {
1039
- return JSON.parse(header);
1040
- } catch {
1041
- return null;
1042
- }
1043
- }
1044
- /**
1045
- * Thrown when an RSC payload response contains X-Timber-Redirect header.
1046
- * Caught in navigate() to trigger a soft router navigation to the redirect target.
1047
- */
1048
- var RedirectError = class extends Error {
1049
- redirectUrl;
1050
- constructor(url) {
1051
- super(`Server redirect to ${url}`);
1052
- this.redirectUrl = url;
1053
- }
1054
- };
1055
- /**
1056
- * Thrown when the server signals a version skew (X-Timber-Reload header).
1057
- * Caught in navigate() to trigger a full page reload via triggerStaleReload().
1058
- * See TIM-446.
1059
- */
1060
- var VersionSkewError = class extends Error {
1061
- constructor() {
1062
- super("Version skew detected — server has been redeployed");
1063
- }
1064
- };
1065
- /**
1066
- * Thrown when the server returns an error for an RSC payload request.
1067
- * The server sends X-Timber-Error header and a JSON body instead of a
1068
- * broken RSC stream for any RenderError (4xx or 5xx). Caught in
1069
- * navigate() to trigger a hard navigation so the server can render
1070
- * the error page as HTML.
1071
- *
1072
- * See design/10-error-handling.md §"Error Page Rendering for Client Navigation"
1073
- */
1074
- var ServerErrorResponse = class extends Error {
1075
- status;
1076
- url;
1077
- constructor(status, url) {
1078
- super(`Server error ${status} during navigation to ${url}`);
1079
- this.status = status;
1080
- this.url = url;
1081
- }
1082
- };
1083
- /**
1084
- * Fetch an RSC payload from the server. If a decodeRsc function is provided,
1085
- * the response is decoded into a React element tree via createFromFetch.
1086
- * Otherwise, the raw response text is returned (test mode).
1087
- *
1088
- * Also extracts head elements from the X-Timber-Head response header
1089
- * so the client can update document.title and <meta> tags after navigation.
1090
- */
1091
- async function fetchRscPayload(url, deps, stateTree, currentUrl, signal) {
1092
- const rscUrl = appendRscParam(url);
1093
- const headers = buildRscHeaders(stateTree, currentUrl);
1094
- if (deps.decodeRsc) {
1095
- const fetchPromise = deps.fetch(rscUrl, {
1096
- headers,
1097
- redirect: "manual",
1098
- signal
1099
- });
1100
- let headElements = null;
1101
- let segmentInfo = null;
1102
- let params = null;
1103
- let skippedSegments = null;
1104
- const wrappedPromise = fetchPromise.then((response) => {
1105
- if (checkReloadSignal(response)) throw new VersionSkewError();
1106
- const redirectLocation = response.headers.get("X-Timber-Redirect") || (response.status >= 300 && response.status < 400 ? response.headers.get("Location") : null);
1107
- if (redirectLocation) throw new RedirectError(redirectLocation);
1108
- if (response.headers.get("X-Timber-Error") === "1") throw new ServerErrorResponse(response.status, url);
1109
- headElements = extractHeadElements(response);
1110
- segmentInfo = extractSegmentInfo(response);
1111
- params = extractParams(response);
1112
- skippedSegments = extractSkippedSegments(response);
1113
- return response;
1114
- });
1115
- await wrappedPromise;
1116
- return {
1117
- payload: await deps.decodeRsc(wrappedPromise),
1118
- headElements,
1119
- segmentInfo,
1120
- params,
1121
- skippedSegments
1122
- };
1123
- }
1124
- const response = await deps.fetch(rscUrl, {
1125
- headers,
1126
- redirect: "manual",
1127
- signal
1128
- });
1129
- if (response.status >= 300 && response.status < 400) {
1130
- const location = response.headers.get("Location");
1131
- if (location) throw new RedirectError(location);
1132
- }
1133
- return {
1134
- payload: await response.text(),
1135
- headElements: extractHeadElements(response),
1136
- segmentInfo: extractSegmentInfo(response),
1137
- params: extractParams(response),
1138
- skippedSegments: extractSkippedSegments(response)
1139
- };
1140
- }
1141
- //#endregion
1142
- //#region src/client/router.ts
1143
- /**
1144
- * Check if an error is an abort error (connection closed / fetch aborted).
1145
- * Browsers throw DOMException with name 'AbortError' when a fetch is aborted.
1146
- */
1147
- function isAbortError(error) {
1148
- if (error instanceof DOMException && error.name === "AbortError") return true;
1149
- if (error instanceof Error && error.name === "AbortError") return true;
1150
- return false;
1151
- }
1152
- function createRouter(deps) {
1153
- const segmentCache = new SegmentCache();
1154
- const prefetchCache = new PrefetchCache();
1155
- const historyStack = new HistoryStack();
1156
- const segmentElementCache = new SegmentElementCache();
1157
- let routerPhase = { phase: "idle" };
1158
- const pendingListeners = /* @__PURE__ */ new Set();
1159
- let currentNavAbort = null;
1160
- /**
1161
- * Create a new AbortController for a navigation, aborting any
1162
- * previous in-flight navigation. Optionally links to an external
1163
- * signal (e.g., from the Navigation API's NavigateEvent.signal).
1164
- */
1165
- function createNavAbort(externalSignal) {
1166
- currentNavAbort?.abort();
1167
- const controller = new AbortController();
1168
- currentNavAbort = controller;
1169
- if (externalSignal) if (externalSignal.aborted) controller.abort();
1170
- else externalSignal.addEventListener("abort", () => controller.abort(), { once: true });
1171
- return controller;
1172
- }
1173
- function setPending(value, url) {
1174
- const next = value && url ? {
1175
- phase: "navigating",
1176
- targetUrl: url
1177
- } : { phase: "idle" };
1178
- if (routerPhase.phase === next.phase && (routerPhase.phase === "idle" || routerPhase.phase === "navigating" && next.phase === "navigating" && routerPhase.targetUrl === next.targetUrl)) return;
1179
- routerPhase = next;
1180
- for (const listener of pendingListeners) listener(value);
1181
- }
1182
- /** Update the segment cache from server-provided segment metadata. */
1183
- function updateSegmentCache(segmentInfo) {
1184
- if (!segmentInfo || segmentInfo.length === 0) return;
1185
- const tree = buildSegmentTree(segmentInfo);
1186
- if (tree) segmentCache.set("/", tree);
1187
- }
1188
- /** Render a decoded RSC payload into the DOM if a renderer is available. */
1189
- function renderPayload(payload, navState) {
1190
- if (deps.renderRoot) deps.renderRoot(payload, navState);
1191
- }
1192
- /**
1193
- * Merge a partial RSC payload with cached segment elements if segments
1194
- * were skipped, then cache segments from the (merged) payload.
1195
- * Returns the merged payload ready for rendering.
1196
- */
1197
- function mergeAndCachePayload(payload, skippedSegments) {
1198
- let merged = payload;
1199
- if (skippedSegments && skippedSegments.length > 0) merged = mergeSegmentTree(payload, skippedSegments, segmentElementCache);
1200
- cacheSegmentElements(merged, segmentElementCache);
1201
- return merged;
1202
- }
1203
- /**
1204
- * Update navigation state (params + pathname) for the next render.
1205
- *
1206
- * Sets the module-level fallback (for tests and SSR) and the
1207
- * globalThis bridge, then returns the NavigationState so callers
1208
- * can pass it explicitly to renderRoot/wrapPayload — eliminating
1209
- * temporal coupling with getNavigationState().
1210
- */
1211
- function updateNavigationState(params, url) {
1212
- const resolvedParams = params ?? {};
1213
- setCurrentParams(resolvedParams);
1214
- const navState = {
1215
- params: resolvedParams,
1216
- pathname: url.startsWith("http") ? new URL(url).pathname : url.split("?")[0] || "/"
1217
- };
1218
- setNavigationState(navState);
1219
- return navState;
1220
- }
1221
- /**
1222
- * Render a payload via navigateTransition (production) or renderRoot (tests).
1223
- * The perform callback should fetch data, update state, and return the
1224
- * FetchResult plus the NavigationState (so it can be passed explicitly
1225
- * to wrapPayload/renderRoot without temporal coupling).
1226
- */
1227
- async function renderViaTransition(url, perform) {
1228
- if (deps.navigateTransition) {
1229
- let headElements = null;
1230
- await deps.navigateTransition(url, async (wrapPayload) => {
1231
- const result = await perform();
1232
- headElements = result.headElements;
1233
- const merged = mergeAndCachePayload(result.payload, result.skippedSegments);
1234
- historyStack.push(url, {
1235
- payload: merged,
1236
- headElements: result.headElements,
1237
- params: result.params
1238
- });
1239
- return wrapPayload(merged, result.navState);
1240
- });
1241
- return headElements;
1242
- }
1243
- const result = await perform();
1244
- const merged = mergeAndCachePayload(result.payload, result.skippedSegments);
1245
- historyStack.push(url, {
1246
- payload: merged,
1247
- headElements: result.headElements,
1248
- params: result.params
1249
- });
1250
- renderPayload(merged, result.navState);
1251
- return result.headElements;
1252
- }
1253
- /** Apply head elements (title, meta tags) to the DOM if available. */
1254
- function applyHead(elements) {
1255
- if (elements && deps.applyHead) deps.applyHead(elements);
1256
- }
1257
- /** Run a callback after the next paint (after React commit). */
1258
- function afterPaint(callback) {
1259
- if (deps.afterPaint) deps.afterPaint(callback);
1260
- else callback();
1261
- }
1262
- /**
1263
- * Schedule scroll restoration after the next paint and fire the
1264
- * scroll-restored event. Used by navigate, popstate, and refresh.
1265
- */
1266
- function restoreScrollAfterPaint(scrollY) {
1267
- afterPaint(() => {
1268
- deps.scrollTo(0, scrollY);
1269
- window.dispatchEvent(new Event("timber:scroll-restored"));
1270
- });
1271
- }
1272
- /**
1273
- * Core navigation logic shared between the transition and fallback paths.
1274
- * Fetches the RSC payload, updates all state, and returns the result.
1275
- */
1276
- async function performNavigationFetch(url, options) {
1277
- const prefetched = prefetchCache.consume(url);
1278
- let result = prefetched ? {
1279
- payload: prefetched.payload,
1280
- headElements: prefetched.headElements,
1281
- segmentInfo: prefetched.segmentInfo ?? null,
1282
- params: prefetched.params ?? null,
1283
- skippedSegments: prefetched.skippedSegments ?? null
1284
- } : void 0;
1285
- if (result === void 0) {
1286
- const stateTree = segmentCache.serializeStateTree(segmentElementCache.getMergeablePaths());
1287
- const rawCurrentUrl = deps.getCurrentUrl();
1288
- result = await fetchRscPayload(url, deps, stateTree, rawCurrentUrl.startsWith("http") ? new URL(rawCurrentUrl).pathname : new URL(rawCurrentUrl, "http://localhost").pathname, options.signal);
1289
- }
1290
- if (!options.skipHistory) {
1291
- deps.setRouterNavigating?.(true);
1292
- if (options.replace) deps.replaceState({
1293
- timber: true,
1294
- scrollY: 0
1295
- }, "", url);
1296
- else deps.pushState({
1297
- timber: true,
1298
- scrollY: 0
1299
- }, "", url);
1300
- deps.setRouterNavigating?.(false);
1301
- }
1302
- updateSegmentCache(result.segmentInfo);
1303
- const navState = updateNavigationState(result.params, url);
1304
- return {
1305
- ...result,
1306
- navState
1307
- };
1308
- }
1309
- async function navigate(url, options = {}) {
1310
- const scroll = options.scroll !== false;
1311
- const replace = options.replace === true;
1312
- const externalSignal = options._signal;
1313
- const skipHistory = options._skipHistory === true;
1314
- const navAbort = createNavAbort(externalSignal);
1315
- const currentScrollY = deps.getScrollY();
1316
- if (deps.saveNavigationEntryScroll) deps.saveNavigationEntryScroll(currentScrollY);
1317
- else deps.replaceState({
1318
- timber: true,
1319
- scrollY: currentScrollY
1320
- }, "", deps.getCurrentUrl());
1321
- let effectiveSkipHistory = skipHistory;
1322
- if (!skipHistory && deps.navigationNavigate) {
1323
- deps.setRouterNavigating?.(true);
1324
- deps.navigationNavigate(url, replace);
1325
- deps.setRouterNavigating?.(false);
1326
- effectiveSkipHistory = true;
1327
- }
1328
- setPending(true, url);
1329
- try {
1330
- applyHead(await renderViaTransition(url, () => performNavigationFetch(url, {
1331
- replace,
1332
- signal: navAbort.signal,
1333
- skipHistory: effectiveSkipHistory
1334
- })));
1335
- window.dispatchEvent(new Event("timber:navigation-end"));
1336
- restoreScrollAfterPaint(scroll ? 0 : currentScrollY);
1337
- } catch (error) {
1338
- if (error instanceof VersionSkewError) {
1339
- setHardNavigating(true);
1340
- const { triggerStaleReload } = await import("../_chunks/stale-reload-BLUC_Pl_.js");
1341
- triggerStaleReload();
1342
- return new Promise(() => {});
1343
- }
1344
- if (error instanceof RedirectError) {
1345
- setPending(false);
1346
- deps.completeRouterNavigation?.();
1347
- await navigate(error.redirectUrl, { replace: true });
1348
- return;
1349
- }
1350
- if (error instanceof ServerErrorResponse) {
1351
- setHardNavigating(true);
1352
- window.location.href = error.url;
1353
- return new Promise(() => {});
1354
- }
1355
- if (isAbortError(error)) return;
1356
- throw error;
1357
- } finally {
1358
- if (currentNavAbort === navAbort) currentNavAbort = null;
1359
- setPending(false);
1360
- deps.completeRouterNavigation?.();
1361
- }
1362
- }
1363
- async function refresh() {
1364
- const currentUrl = deps.getCurrentUrl();
1365
- const navAbort = createNavAbort();
1366
- setPending(true, currentUrl);
1367
- try {
1368
- applyHead(await renderViaTransition(currentUrl, async () => {
1369
- const result = await fetchRscPayload(currentUrl, deps, void 0, void 0, navAbort.signal);
1370
- updateSegmentCache(result.segmentInfo);
1371
- const navState = updateNavigationState(result.params, currentUrl);
1372
- return {
1373
- ...result,
1374
- navState
1375
- };
1376
- }));
1377
- } catch (error) {
1378
- if (isAbortError(error)) return;
1379
- throw error;
1380
- } finally {
1381
- if (currentNavAbort === navAbort) currentNavAbort = null;
1382
- setPending(false);
1383
- deps.completeRouterNavigation?.();
1384
- }
1385
- }
1386
- async function handlePopState(url, scrollY = 0, externalSignal) {
1387
- const entry = historyStack.get(url);
1388
- if (entry && entry.payload !== null) {
1389
- const navState = updateNavigationState(entry.params, url);
1390
- renderPayload(entry.payload, navState);
1391
- applyHead(entry.headElements);
1392
- restoreScrollAfterPaint(scrollY);
1393
- } else {
1394
- const navAbort = createNavAbort(externalSignal);
1395
- setPending(true, url);
1396
- try {
1397
- applyHead(await renderViaTransition(url, async () => {
1398
- const result = await fetchRscPayload(url, deps, segmentCache.serializeStateTree(segmentElementCache.getMergeablePaths()), void 0, navAbort.signal);
1399
- updateSegmentCache(result.segmentInfo);
1400
- const navState = updateNavigationState(result.params, url);
1401
- return {
1402
- ...result,
1403
- navState
1404
- };
1405
- }));
1406
- restoreScrollAfterPaint(scrollY);
1407
- } catch (error) {
1408
- if (isAbortError(error)) return;
1409
- throw error;
1410
- } finally {
1411
- if (currentNavAbort === navAbort) currentNavAbort = null;
1412
- setPending(false);
1413
- }
1414
- }
1415
- }
1416
- /**
1417
- * Prefetch an RSC payload for a URL and store it in the prefetch cache.
1418
- * Called on hover of <Link prefetch> elements.
1419
- */
1420
- function prefetch(url) {
1421
- if (prefetchCache.get(url) !== void 0) return;
1422
- if (historyStack.has(url)) return;
1423
- fetchRscPayload(url, deps, segmentCache.serializeStateTree(segmentElementCache.getMergeablePaths())).then((result) => {
1424
- prefetchCache.set(url, result);
1425
- }, () => {});
1426
- }
1427
- return {
1428
- navigate,
1429
- refresh,
1430
- handlePopState,
1431
- isPending: () => routerPhase.phase === "navigating",
1432
- getPendingUrl: () => routerPhase.phase === "navigating" ? routerPhase.targetUrl : null,
1433
- onPendingChange(listener) {
1434
- pendingListeners.add(listener);
1435
- return () => pendingListeners.delete(listener);
1436
- },
1437
- prefetch,
1438
- applyRevalidation(element, headElements) {
1439
- const currentUrl = deps.getCurrentUrl();
1440
- const merged = mergeAndCachePayload(element, null);
1441
- historyStack.push(currentUrl, {
1442
- payload: merged,
1443
- headElements
1444
- });
1445
- renderPayload(merged, getNavigationState());
1446
- applyHead(headElements);
1447
- },
1448
- initSegmentCache: (segments) => updateSegmentCache(segments),
1449
- cacheElementTree: (element) => cacheSegmentElements(element, segmentElementCache),
1450
- segmentCache,
1451
- prefetchCache,
1452
- historyStack
1453
- };
1454
- }
1455
- //#endregion
1456
- //#region src/client/use-navigation-pending.ts
279
+ //#region src/client/use-pending-navigation.ts
1457
280
  /**
1458
281
  * Returns true while an RSC navigation is in flight.
1459
282
  *
@@ -1466,10 +289,10 @@ function createRouter(deps) {
1466
289
  *
1467
290
  * ```tsx
1468
291
  * 'use client'
1469
- * import { useNavigationPending } from '@timber-js/app/client'
292
+ * import { usePendingNavigation } from '@timber-js/app/client'
1470
293
  *
1471
294
  * export function NavBar() {
1472
- * const isPending = useNavigationPending()
295
+ * const isPending = usePendingNavigation()
1473
296
  * return (
1474
297
  * <nav className={isPending ? 'opacity-50' : ''}>
1475
298
  * <Link href="/dashboard">Dashboard</Link>
@@ -1478,7 +301,7 @@ function createRouter(deps) {
1478
301
  * }
1479
302
  * ```
1480
303
  */
1481
- function useNavigationPending() {
304
+ function usePendingNavigation() {
1482
305
  return usePendingNavigationUrl() !== null;
1483
306
  }
1484
307
  //#endregion
@@ -1505,7 +328,7 @@ function useNavigationPending() {
1505
328
  *
1506
329
  * For loading UI during navigation, use:
1507
330
  * - useLinkStatus() — per-link pending indicator (inside <Link>)
1508
- * - useNavigationPending() — global navigation pending state
331
+ * - usePendingNavigation() — global navigation pending state
1509
332
  */
1510
333
  /**
1511
334
  * Get a router instance for programmatic navigation.
@@ -1960,6 +783,6 @@ function useCookie(name, defaultOptions) {
1960
783
  //#region src/client/index.ts
1961
784
  _registerUseCookieModule(use_cookie_exports);
1962
785
  //#endregion
1963
- export { HistoryStack, Link, LinkStatusContext, NavigationProvider, PrefetchCache, SegmentCache, SegmentProvider, TimberErrorBoundary, bindUseQueryStates, buildLinkProps, clearSsrData, createRouter, getNavigationState, getRouter, getRouterOrNull, getSsrData, interpolateParams, mergePreservedSearchParams, resolveHref, setCurrentParams, setGlobalRouter, setNavigationState, setSsrData, useActionState, useCookie, useFormAction, useFormErrors, useLinkStatus, useNavigationPending, usePathname, useQueryStates, useRouter, useSearchParams, useSegmentContext, useSegmentParams, useSelectedLayoutSegment, useSelectedLayoutSegments, validateLinkHref };
786
+ export { Link, LinkStatusContext, buildLinkProps, interpolateParams, mergePreservedSearchParams, resolveHref, useActionState, useCookie, useFormAction, useFormErrors, useLinkStatus, usePathname, usePendingNavigation, useQueryStates, useRouter, useSearchParams, useSegmentParams, useSelectedLayoutSegment, useSelectedLayoutSegments, validateLinkHref };
1964
787
 
1965
788
  //# sourceMappingURL=index.js.map