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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (333) hide show
  1. package/LICENSE +8 -0
  2. package/dist/_chunks/{als-registry-B7DbZ2hS.js → als-registry-Ba7URUIn.js} +1 -1
  3. package/dist/_chunks/als-registry-Ba7URUIn.js.map +1 -0
  4. package/dist/_chunks/chunk-DYhsFzuS.js +33 -0
  5. package/dist/_chunks/{debug-gwlJkDuf.js → debug-ECi_61pb.js} +2 -2
  6. package/dist/_chunks/debug-ECi_61pb.js.map +1 -0
  7. package/dist/_chunks/define-TK8C1M3x.js +279 -0
  8. package/dist/_chunks/define-TK8C1M3x.js.map +1 -0
  9. package/dist/_chunks/define-cookie-k9btcEfI.js +93 -0
  10. package/dist/_chunks/define-cookie-k9btcEfI.js.map +1 -0
  11. package/dist/_chunks/error-boundary-B9vT_YK_.js +211 -0
  12. package/dist/_chunks/error-boundary-B9vT_YK_.js.map +1 -0
  13. package/dist/_chunks/{format-DviM89f0.js → format-cX7wzEp2.js} +2 -2
  14. package/dist/_chunks/{format-DviM89f0.js.map → format-cX7wzEp2.js.map} +1 -1
  15. package/dist/_chunks/{interception-BOoWmLUA.js → interception-D2djYaIm.js} +112 -77
  16. package/dist/_chunks/interception-D2djYaIm.js.map +1 -0
  17. package/dist/_chunks/{metadata-routes-Cjmvi3rQ.js → metadata-routes-BU684ls2.js} +1 -1
  18. package/dist/_chunks/{metadata-routes-Cjmvi3rQ.js.map → metadata-routes-BU684ls2.js.map} +1 -1
  19. package/dist/_chunks/{request-context-DIkVh_jG.js → request-context-0h-6Voad.js} +95 -69
  20. package/dist/_chunks/request-context-0h-6Voad.js.map +1 -0
  21. package/dist/_chunks/segment-context-DBn-nrMN.js +69 -0
  22. package/dist/_chunks/segment-context-DBn-nrMN.js.map +1 -0
  23. package/dist/_chunks/stale-reload-4L-_skC7.js +47 -0
  24. package/dist/_chunks/stale-reload-4L-_skC7.js.map +1 -0
  25. package/dist/_chunks/{tracing-Cwn7697K.js → tracing-JI4cYUdz.js} +17 -3
  26. package/dist/_chunks/{tracing-Cwn7697K.js.map → tracing-JI4cYUdz.js.map} +1 -1
  27. package/dist/_chunks/{use-query-states-D5KaffOK.js → use-query-states-wEXY2JQB.js} +1 -1
  28. package/dist/_chunks/{use-query-states-D5KaffOK.js.map → use-query-states-wEXY2JQB.js.map} +1 -1
  29. package/dist/_chunks/wrappers-C9XPg7-U.js +63 -0
  30. package/dist/_chunks/wrappers-C9XPg7-U.js.map +1 -0
  31. package/dist/adapters/compress-module.d.ts.map +1 -1
  32. package/dist/adapters/nitro.d.ts +17 -1
  33. package/dist/adapters/nitro.d.ts.map +1 -1
  34. package/dist/adapters/nitro.js +56 -13
  35. package/dist/adapters/nitro.js.map +1 -1
  36. package/dist/cache/fast-hash.d.ts +22 -0
  37. package/dist/cache/fast-hash.d.ts.map +1 -0
  38. package/dist/cache/index.d.ts +5 -2
  39. package/dist/cache/index.d.ts.map +1 -1
  40. package/dist/cache/index.js +90 -20
  41. package/dist/cache/index.js.map +1 -1
  42. package/dist/cache/register-cached-function.d.ts.map +1 -1
  43. package/dist/cache/singleflight.d.ts +18 -1
  44. package/dist/cache/singleflight.d.ts.map +1 -1
  45. package/dist/cache/timber-cache.d.ts +1 -1
  46. package/dist/cache/timber-cache.d.ts.map +1 -1
  47. package/dist/client/error-boundary.d.ts +10 -1
  48. package/dist/client/error-boundary.d.ts.map +1 -1
  49. package/dist/client/error-boundary.js +1 -125
  50. package/dist/client/index.d.ts +3 -2
  51. package/dist/client/index.d.ts.map +1 -1
  52. package/dist/client/index.js +213 -93
  53. package/dist/client/index.js.map +1 -1
  54. package/dist/client/link.d.ts +22 -8
  55. package/dist/client/link.d.ts.map +1 -1
  56. package/dist/client/navigation-context.d.ts +2 -2
  57. package/dist/client/router.d.ts +25 -3
  58. package/dist/client/router.d.ts.map +1 -1
  59. package/dist/client/rsc-fetch.d.ts +23 -2
  60. package/dist/client/rsc-fetch.d.ts.map +1 -1
  61. package/dist/client/segment-cache.d.ts +1 -1
  62. package/dist/client/segment-cache.d.ts.map +1 -1
  63. package/dist/client/segment-context.d.ts +1 -1
  64. package/dist/client/segment-context.d.ts.map +1 -1
  65. package/dist/client/segment-merger.d.ts.map +1 -1
  66. package/dist/client/stale-reload.d.ts +15 -0
  67. package/dist/client/stale-reload.d.ts.map +1 -1
  68. package/dist/client/top-loader.d.ts +1 -1
  69. package/dist/client/top-loader.d.ts.map +1 -1
  70. package/dist/client/transition-root.d.ts +1 -1
  71. package/dist/client/transition-root.d.ts.map +1 -1
  72. package/dist/client/use-params.d.ts +2 -2
  73. package/dist/client/use-params.d.ts.map +1 -1
  74. package/dist/client/use-query-states.d.ts +1 -1
  75. package/dist/codec.d.ts +21 -0
  76. package/dist/codec.d.ts.map +1 -0
  77. package/dist/cookies/define-cookie.d.ts +33 -12
  78. package/dist/cookies/define-cookie.d.ts.map +1 -1
  79. package/dist/cookies/index.js +1 -83
  80. package/dist/fonts/css.d.ts +1 -0
  81. package/dist/fonts/css.d.ts.map +1 -1
  82. package/dist/index.d.ts +112 -35
  83. package/dist/index.d.ts.map +1 -1
  84. package/dist/index.js +467 -246
  85. package/dist/index.js.map +1 -1
  86. package/dist/params/define.d.ts +76 -0
  87. package/dist/params/define.d.ts.map +1 -0
  88. package/dist/params/index.d.ts +8 -0
  89. package/dist/params/index.d.ts.map +1 -0
  90. package/dist/params/index.js +105 -0
  91. package/dist/params/index.js.map +1 -0
  92. package/dist/plugins/adapter-build.d.ts.map +1 -1
  93. package/dist/plugins/build-manifest.d.ts.map +1 -1
  94. package/dist/plugins/client-chunks.d.ts +32 -0
  95. package/dist/plugins/client-chunks.d.ts.map +1 -0
  96. package/dist/plugins/dev-error-overlay.d.ts +26 -1
  97. package/dist/plugins/dev-error-overlay.d.ts.map +1 -1
  98. package/dist/plugins/entries.d.ts.map +1 -1
  99. package/dist/plugins/fonts.d.ts +7 -0
  100. package/dist/plugins/fonts.d.ts.map +1 -1
  101. package/dist/plugins/routing.d.ts.map +1 -1
  102. package/dist/plugins/server-bundle.d.ts.map +1 -1
  103. package/dist/plugins/static-build.d.ts.map +1 -1
  104. package/dist/routing/codegen.d.ts +2 -2
  105. package/dist/routing/codegen.d.ts.map +1 -1
  106. package/dist/routing/index.js +1 -1
  107. package/dist/routing/scanner.d.ts.map +1 -1
  108. package/dist/routing/status-file-lint.d.ts +2 -1
  109. package/dist/routing/status-file-lint.d.ts.map +1 -1
  110. package/dist/routing/types.d.ts +6 -4
  111. package/dist/routing/types.d.ts.map +1 -1
  112. package/dist/rsc-runtime/rsc.d.ts +1 -1
  113. package/dist/rsc-runtime/rsc.d.ts.map +1 -1
  114. package/dist/rsc-runtime/ssr.d.ts +12 -0
  115. package/dist/rsc-runtime/ssr.d.ts.map +1 -1
  116. package/dist/search-params/codecs.d.ts +1 -1
  117. package/dist/search-params/define.d.ts +159 -0
  118. package/dist/search-params/define.d.ts.map +1 -0
  119. package/dist/search-params/index.d.ts +4 -5
  120. package/dist/search-params/index.d.ts.map +1 -1
  121. package/dist/search-params/index.js +4 -474
  122. package/dist/search-params/registry.d.ts +1 -1
  123. package/dist/search-params/wrappers.d.ts +53 -0
  124. package/dist/search-params/wrappers.d.ts.map +1 -0
  125. package/dist/server/access-gate.d.ts +4 -0
  126. package/dist/server/access-gate.d.ts.map +1 -1
  127. package/dist/server/action-client.d.ts.map +1 -1
  128. package/dist/server/action-encryption.d.ts +76 -0
  129. package/dist/server/action-encryption.d.ts.map +1 -0
  130. package/dist/server/action-handler.d.ts.map +1 -1
  131. package/dist/server/als-registry.d.ts +18 -4
  132. package/dist/server/als-registry.d.ts.map +1 -1
  133. package/dist/server/build-manifest.d.ts +2 -2
  134. package/dist/server/debug.d.ts +1 -1
  135. package/dist/server/default-logger.d.ts +22 -0
  136. package/dist/server/default-logger.d.ts.map +1 -0
  137. package/dist/server/deny-renderer.d.ts.map +1 -1
  138. package/dist/server/early-hints.d.ts +13 -5
  139. package/dist/server/early-hints.d.ts.map +1 -1
  140. package/dist/server/error-boundary-wrapper.d.ts +4 -0
  141. package/dist/server/error-boundary-wrapper.d.ts.map +1 -1
  142. package/dist/server/flight-injection-state.d.ts +66 -0
  143. package/dist/server/flight-injection-state.d.ts.map +1 -0
  144. package/dist/server/flight-scripts.d.ts +39 -0
  145. package/dist/server/flight-scripts.d.ts.map +1 -0
  146. package/dist/server/flush.d.ts.map +1 -1
  147. package/dist/server/form-data.d.ts +29 -0
  148. package/dist/server/form-data.d.ts.map +1 -1
  149. package/dist/server/html-injectors.d.ts +51 -11
  150. package/dist/server/html-injectors.d.ts.map +1 -1
  151. package/dist/server/index.d.ts +4 -2
  152. package/dist/server/index.d.ts.map +1 -1
  153. package/dist/server/index.js +1974 -1648
  154. package/dist/server/index.js.map +1 -1
  155. package/dist/server/logger.d.ts +24 -7
  156. package/dist/server/logger.d.ts.map +1 -1
  157. package/dist/server/node-stream-transforms.d.ts +113 -0
  158. package/dist/server/node-stream-transforms.d.ts.map +1 -0
  159. package/dist/server/pipeline.d.ts +7 -4
  160. package/dist/server/pipeline.d.ts.map +1 -1
  161. package/dist/server/primitives.d.ts +30 -3
  162. package/dist/server/primitives.d.ts.map +1 -1
  163. package/dist/server/render-timeout.d.ts +51 -0
  164. package/dist/server/render-timeout.d.ts.map +1 -0
  165. package/dist/server/request-context.d.ts +65 -38
  166. package/dist/server/request-context.d.ts.map +1 -1
  167. package/dist/server/route-element-builder.d.ts +7 -0
  168. package/dist/server/route-element-builder.d.ts.map +1 -1
  169. package/dist/server/route-handler.d.ts.map +1 -1
  170. package/dist/server/route-matcher.d.ts +2 -2
  171. package/dist/server/route-matcher.d.ts.map +1 -1
  172. package/dist/server/rsc-entry/error-renderer.d.ts.map +1 -1
  173. package/dist/server/rsc-entry/helpers.d.ts +46 -3
  174. package/dist/server/rsc-entry/helpers.d.ts.map +1 -1
  175. package/dist/server/rsc-entry/index.d.ts +6 -1
  176. package/dist/server/rsc-entry/index.d.ts.map +1 -1
  177. package/dist/server/rsc-entry/rsc-payload.d.ts.map +1 -1
  178. package/dist/server/rsc-entry/rsc-stream.d.ts +9 -0
  179. package/dist/server/rsc-entry/rsc-stream.d.ts.map +1 -1
  180. package/dist/server/rsc-entry/ssr-renderer.d.ts.map +1 -1
  181. package/dist/server/slot-resolver.d.ts +1 -1
  182. package/dist/server/slot-resolver.d.ts.map +1 -1
  183. package/dist/server/ssr-entry.d.ts +22 -0
  184. package/dist/server/ssr-entry.d.ts.map +1 -1
  185. package/dist/server/ssr-render.d.ts +39 -21
  186. package/dist/server/ssr-render.d.ts.map +1 -1
  187. package/dist/server/ssr-wrappers.d.ts +50 -0
  188. package/dist/server/ssr-wrappers.d.ts.map +1 -0
  189. package/dist/server/tracing.d.ts +10 -0
  190. package/dist/server/tracing.d.ts.map +1 -1
  191. package/dist/server/tree-builder.d.ts +19 -12
  192. package/dist/server/tree-builder.d.ts.map +1 -1
  193. package/dist/server/types.d.ts +1 -3
  194. package/dist/server/types.d.ts.map +1 -1
  195. package/dist/server/version-skew.d.ts +61 -0
  196. package/dist/server/version-skew.d.ts.map +1 -0
  197. package/dist/server/waituntil-bridge.d.ts.map +1 -1
  198. package/dist/shared/merge-search-params.d.ts +22 -0
  199. package/dist/shared/merge-search-params.d.ts.map +1 -0
  200. package/dist/shims/navigation-client.d.ts +1 -1
  201. package/dist/shims/navigation-client.d.ts.map +1 -1
  202. package/dist/shims/navigation.d.ts +1 -1
  203. package/dist/shims/navigation.d.ts.map +1 -1
  204. package/dist/utils/state-machine.d.ts +80 -0
  205. package/dist/utils/state-machine.d.ts.map +1 -0
  206. package/package.json +17 -14
  207. package/src/adapters/compress-module.ts +24 -4
  208. package/src/adapters/nitro.ts +58 -9
  209. package/src/cache/fast-hash.ts +34 -0
  210. package/src/cache/index.ts +5 -2
  211. package/src/cache/register-cached-function.ts +7 -3
  212. package/src/cache/singleflight.ts +62 -4
  213. package/src/cache/timber-cache.ts +40 -29
  214. package/src/cli.ts +0 -0
  215. package/src/client/browser-entry.ts +133 -93
  216. package/src/client/error-boundary.tsx +18 -1
  217. package/src/client/index.ts +10 -1
  218. package/src/client/link.tsx +78 -19
  219. package/src/client/navigation-context.ts +2 -2
  220. package/src/client/router.ts +105 -60
  221. package/src/client/rsc-fetch.ts +63 -2
  222. package/src/client/segment-cache.ts +1 -1
  223. package/src/client/segment-context.ts +6 -1
  224. package/src/client/segment-merger.ts +2 -8
  225. package/src/client/stale-reload.ts +32 -6
  226. package/src/client/top-loader.tsx +10 -9
  227. package/src/client/transition-root.tsx +7 -1
  228. package/src/client/use-params.ts +3 -3
  229. package/src/client/use-query-states.ts +1 -1
  230. package/src/codec.ts +21 -0
  231. package/src/cookies/define-cookie.ts +69 -18
  232. package/src/fonts/css.ts +2 -1
  233. package/src/index.ts +280 -85
  234. package/src/params/define.ts +260 -0
  235. package/src/params/index.ts +28 -0
  236. package/src/plugins/adapter-build.ts +6 -0
  237. package/src/plugins/build-manifest.ts +11 -0
  238. package/src/plugins/client-chunks.ts +65 -0
  239. package/src/plugins/dev-error-overlay.ts +70 -1
  240. package/src/plugins/dev-server.ts +38 -4
  241. package/src/plugins/entries.ts +5 -7
  242. package/src/plugins/fonts.ts +93 -42
  243. package/src/plugins/routing.ts +40 -14
  244. package/src/plugins/server-bundle.ts +32 -1
  245. package/src/plugins/shims.ts +1 -1
  246. package/src/plugins/static-build.ts +8 -4
  247. package/src/routing/codegen.ts +109 -88
  248. package/src/routing/scanner.ts +55 -6
  249. package/src/routing/status-file-lint.ts +2 -1
  250. package/src/routing/types.ts +7 -4
  251. package/src/rsc-runtime/rsc.ts +2 -0
  252. package/src/rsc-runtime/ssr.ts +50 -0
  253. package/src/rsc-runtime/vendor-types.d.ts +7 -0
  254. package/src/search-params/codecs.ts +1 -1
  255. package/src/search-params/define.ts +518 -0
  256. package/src/search-params/index.ts +12 -18
  257. package/src/search-params/registry.ts +1 -1
  258. package/src/search-params/wrappers.ts +85 -0
  259. package/src/server/access-gate.tsx +40 -9
  260. package/src/server/action-client.ts +7 -1
  261. package/src/server/action-encryption.ts +144 -0
  262. package/src/server/action-handler.ts +19 -2
  263. package/src/server/als-registry.ts +18 -4
  264. package/src/server/build-manifest.ts +4 -4
  265. package/src/server/compress.ts +25 -7
  266. package/src/server/debug.ts +1 -1
  267. package/src/server/default-logger.ts +98 -0
  268. package/src/server/deny-renderer.ts +2 -1
  269. package/src/server/early-hints.ts +36 -15
  270. package/src/server/error-boundary-wrapper.ts +57 -14
  271. package/src/server/flight-injection-state.ts +113 -0
  272. package/src/server/flight-scripts.ts +59 -0
  273. package/src/server/flush.ts +2 -1
  274. package/src/server/form-data.ts +76 -0
  275. package/src/server/html-injectors.ts +261 -117
  276. package/src/server/index.ts +9 -4
  277. package/src/server/logger.ts +38 -35
  278. package/src/server/node-stream-transforms.ts +504 -0
  279. package/src/server/pipeline.ts +131 -39
  280. package/src/server/primitives.ts +47 -5
  281. package/src/server/render-timeout.ts +108 -0
  282. package/src/server/request-context.ts +119 -119
  283. package/src/server/route-element-builder.ts +106 -114
  284. package/src/server/route-handler.ts +2 -1
  285. package/src/server/route-matcher.ts +2 -2
  286. package/src/server/rsc-entry/error-renderer.ts +5 -3
  287. package/src/server/rsc-entry/helpers.ts +122 -3
  288. package/src/server/rsc-entry/index.ts +108 -43
  289. package/src/server/rsc-entry/rsc-payload.ts +52 -12
  290. package/src/server/rsc-entry/rsc-stream.ts +49 -12
  291. package/src/server/rsc-entry/ssr-renderer.ts +40 -13
  292. package/src/server/slot-resolver.ts +222 -217
  293. package/src/server/ssr-entry.ts +209 -30
  294. package/src/server/ssr-render.ts +289 -67
  295. package/src/server/ssr-wrappers.tsx +139 -0
  296. package/src/server/tracing.ts +23 -0
  297. package/src/server/tree-builder.ts +91 -57
  298. package/src/server/types.ts +1 -3
  299. package/src/server/version-skew.ts +104 -0
  300. package/src/server/waituntil-bridge.ts +4 -1
  301. package/src/shared/merge-search-params.ts +48 -0
  302. package/src/shims/navigation-client.ts +1 -1
  303. package/src/shims/navigation.ts +1 -1
  304. package/src/utils/state-machine.ts +111 -0
  305. package/dist/_chunks/als-registry-B7DbZ2hS.js.map +0 -1
  306. package/dist/_chunks/debug-gwlJkDuf.js.map +0 -1
  307. package/dist/_chunks/interception-BOoWmLUA.js.map +0 -1
  308. package/dist/_chunks/request-context-DIkVh_jG.js.map +0 -1
  309. package/dist/_chunks/ssr-data-MjmprTmO.js +0 -88
  310. package/dist/_chunks/ssr-data-MjmprTmO.js.map +0 -1
  311. package/dist/_chunks/use-cookie-DX-l1_5E.js +0 -91
  312. package/dist/_chunks/use-cookie-DX-l1_5E.js.map +0 -1
  313. package/dist/client/error-boundary.js.map +0 -1
  314. package/dist/cookies/index.js.map +0 -1
  315. package/dist/plugins/dynamic-transform.d.ts +0 -72
  316. package/dist/plugins/dynamic-transform.d.ts.map +0 -1
  317. package/dist/search-params/analyze.d.ts +0 -54
  318. package/dist/search-params/analyze.d.ts.map +0 -1
  319. package/dist/search-params/builtin-codecs.d.ts +0 -105
  320. package/dist/search-params/builtin-codecs.d.ts.map +0 -1
  321. package/dist/search-params/create.d.ts +0 -106
  322. package/dist/search-params/create.d.ts.map +0 -1
  323. package/dist/search-params/index.js.map +0 -1
  324. package/dist/server/prerender.d.ts +0 -77
  325. package/dist/server/prerender.d.ts.map +0 -1
  326. package/dist/server/response-cache.d.ts +0 -53
  327. package/dist/server/response-cache.d.ts.map +0 -1
  328. package/src/plugins/dynamic-transform.ts +0 -161
  329. package/src/search-params/analyze.ts +0 -192
  330. package/src/search-params/builtin-codecs.ts +0 -228
  331. package/src/search-params/create.ts +0 -321
  332. package/src/server/prerender.ts +0 -139
  333. package/src/server/response-cache.ts +0 -277
@@ -50,14 +50,14 @@ export interface ManifestSegmentNode {
50
50
  middleware?: ManifestFile;
51
51
  access?: ManifestFile;
52
52
  route?: ManifestFile;
53
+ /** params.ts — isomorphic convention file for segmentParams + searchParams definitions. */
54
+ params?: ManifestFile;
53
55
  error?: ManifestFile;
54
56
  default?: ManifestFile;
55
57
  denied?: ManifestFile;
56
- searchParams?: ManifestFile;
57
58
  statusFiles?: Record<string, ManifestFile>;
58
59
  jsonStatusFiles?: Record<string, ManifestFile>;
59
60
  legacyStatusFiles?: Record<string, ManifestFile>;
60
- prerender?: ManifestFile;
61
61
  /** Metadata route files (sitemap.ts, robots.ts, icon.tsx, etc.) keyed by base name */
62
62
  metadataRoutes?: Record<string, ManifestFile>;
63
63
 
@@ -12,10 +12,12 @@ import { logRenderError } from '#/server/logger.js';
12
12
  import type { ManifestSegmentNode } from '#/server/route-matcher.js';
13
13
  import { DenySignal, RenderError } from '#/server/primitives.js';
14
14
  import type { ClientBootstrapConfig } from '#/server/html-injectors.js';
15
+ import { flightInitScript } from '#/server/flight-scripts.js';
15
16
  import { renderDenyPage } from '#/server/deny-renderer.js';
16
17
  import type { LayoutEntry } from '#/server/deny-renderer.js';
17
18
  import type { NavContext } from '#/server/ssr-entry.js';
18
- import { createDebugChannelSink, parseCookiesFromHeader } from './helpers.js';
19
+ import { createDebugChannelSink } from './helpers.js';
20
+ import { getCookiesForSsr } from '#/server/request-context.js';
19
21
  import { callSsr } from './ssr-bridge.js';
20
22
 
21
23
  /**
@@ -124,10 +126,10 @@ export async function renderErrorPage(
124
126
  searchParams: Object.fromEntries(new URL(req.url).searchParams),
125
127
  statusCode: status,
126
128
  responseHeaders,
127
- headHtml: '',
129
+ headHtml: flightInitScript(),
128
130
  bootstrapScriptContent: clientBootstrap.bootstrapScriptContent,
129
131
  rscStream: inlineStream,
130
- cookies: parseCookiesFromHeader(req.headers.get('cookie') ?? ''),
132
+ cookies: getCookiesForSsr(),
131
133
  };
132
134
 
133
135
  return callSsr(ssrStream, navContext);
@@ -20,9 +20,6 @@ export const RSC_CONTENT_TYPE = 'text/x-component';
20
20
  * stream that we drain and discard.
21
21
  *
22
22
  * See design/13-security.md §"Server component source leak"
23
- *
24
- * TODO: In the future, expose this debug data to the browser in dev mode
25
- * for inline error overlays (e.g. component stack traces).
26
23
  */
27
24
  export function createDebugChannelSink(): { readable: ReadableStream; writable: WritableStream } {
28
25
  const sink = new TransformStream();
@@ -34,6 +31,128 @@ export function createDebugChannelSink(): { readable: ReadableStream; writable:
34
31
  };
35
32
  }
36
33
 
34
+ // ─── Debug Channel Collector (dev mode only) ────────────────────────────
35
+
36
+ /**
37
+ * Parsed component debug info extracted from the Flight debug channel.
38
+ *
39
+ * Contains only component names, environment labels, and stack frames —
40
+ * never source code or props. See design/13-security.md §"Server source
41
+ * never reaches the client".
42
+ */
43
+ export interface DebugComponentEntry {
44
+ name: string;
45
+ env: string | null;
46
+ key: string | null;
47
+ stack: unknown[] | null;
48
+ }
49
+
50
+ /**
51
+ * A debug channel that collects Flight debug rows instead of discarding them.
52
+ *
53
+ * Used in dev mode to capture server component tree information for the
54
+ * Vite error overlay. The collector provides the same `{ readable, writable }`
55
+ * shape as the discard sink, plus methods to retrieve collected data.
56
+ *
57
+ * Security: only component names, environments, and stack frames are
58
+ * extracted — props and source code are stripped. In production builds,
59
+ * use `createDebugChannelSink()` instead (this function is never called).
60
+ */
61
+ export interface DebugChannelCollector {
62
+ readable: ReadableStream;
63
+ writable: WritableStream;
64
+ /** Get the raw collected text from the debug channel. */
65
+ getCollectedText(): string;
66
+ /** Get parsed component entries (names, stacks — no props or source). */
67
+ getComponents(): DebugComponentEntry[];
68
+ }
69
+
70
+ export function createDebugChannelCollector(): DebugChannelCollector {
71
+ const chunks: string[] = [];
72
+ const decoder = new TextDecoder();
73
+
74
+ const sink = new TransformStream();
75
+
76
+ // Collect chunks from the readable side instead of discarding them.
77
+ sink.readable
78
+ .pipeTo(
79
+ new WritableStream({
80
+ write(chunk: Uint8Array) {
81
+ chunks.push(decoder.decode(chunk, { stream: true }));
82
+ },
83
+ close() {
84
+ // Flush any remaining bytes in the decoder
85
+ const remaining = decoder.decode();
86
+ if (remaining) chunks.push(remaining);
87
+ },
88
+ })
89
+ )
90
+ .catch(() => {
91
+ // Stream abort — request cancelled. Not an error.
92
+ });
93
+
94
+ return {
95
+ readable: new ReadableStream(), // no commands to send to Flight
96
+ writable: sink.writable,
97
+ getCollectedText() {
98
+ return chunks.join('');
99
+ },
100
+ getComponents() {
101
+ return parseDebugRows(chunks.join(''));
102
+ },
103
+ };
104
+ }
105
+
106
+ /**
107
+ * Parse React Flight debug rows into component entries.
108
+ *
109
+ * The Flight debug channel writes rows in `hexId:json\n` format. Each row
110
+ * with a JSON object containing a `name` field is a component debug info
111
+ * entry. Rows without `name` (timing rows, reference rows like `D"$id"`)
112
+ * are skipped.
113
+ *
114
+ * Security: `props` are explicitly stripped from parsed entries — they may
115
+ * contain rendered output or user data. Only `name`, `env`, `key`, and
116
+ * `stack` are retained.
117
+ */
118
+ export function parseDebugRows(text: string): DebugComponentEntry[] {
119
+ if (!text) return [];
120
+
121
+ const entries: DebugComponentEntry[] = [];
122
+ const lines = text.split('\n');
123
+
124
+ for (const line of lines) {
125
+ if (!line) continue;
126
+
127
+ // Flight row format: hexId:payload
128
+ const colonIdx = line.indexOf(':');
129
+ if (colonIdx === -1) continue;
130
+
131
+ const payload = line.slice(colonIdx + 1);
132
+ // Skip non-JSON payloads (e.g., D"$a" reference rows)
133
+ if (!payload.startsWith('{')) continue;
134
+
135
+ try {
136
+ const parsed = JSON.parse(payload);
137
+ if (typeof parsed !== 'object' || parsed === null) continue;
138
+ if (typeof parsed.name !== 'string') continue;
139
+
140
+ // Strip props — may contain source-derived data or user data.
141
+ // Only retain: name, env, key, stack.
142
+ entries.push({
143
+ name: parsed.name,
144
+ env: parsed.env ?? null,
145
+ key: parsed.key ?? null,
146
+ stack: Array.isArray(parsed.stack) ? parsed.stack : null,
147
+ });
148
+ } catch {
149
+ // Malformed JSON — skip this row
150
+ }
151
+ }
152
+
153
+ return entries;
154
+ }
155
+
37
156
  /**
38
157
  * Build segment metadata for the X-Timber-Segments response header.
39
158
  * Describes the rendered segment chain with async status, enabling
@@ -47,7 +47,11 @@ import { buildClientScripts } from '#/server/html-injectors.js';
47
47
  import type { InterceptionContext, PipelineConfig, RouteMatch } from '#/server/pipeline.js';
48
48
  import { createPipeline } from '#/server/pipeline.js';
49
49
  import { DenySignal, RedirectSignal } from '#/server/primitives.js';
50
- import { buildRouteElement, RouteSignalWithContext } from '#/server/route-element-builder.js';
50
+ import {
51
+ buildRouteElement,
52
+ RouteSignalWithContext,
53
+ ParamCoercionError,
54
+ } from '#/server/route-element-builder.js';
51
55
  import type { ManifestSegmentNode } from '#/server/route-matcher.js';
52
56
  import { createMetadataRouteMatcher, createRouteMatcher } from '#/server/route-matcher.js';
53
57
  import { initDevTracing } from '#/server/tracing.js';
@@ -61,33 +65,62 @@ import {
61
65
  createDebugChannelSink,
62
66
  escapeHtml,
63
67
  isRscPayloadRequest,
68
+ type DebugComponentEntry,
64
69
  } from './helpers.js';
65
70
  import { parseClientStateTree } from '#/server/state-tree-diff.js';
66
- import {
67
- createResponseCache,
68
- resolveResponseCacheConfig,
69
- type ResponseCache,
70
- } from '#/server/response-cache.js';
71
71
  import { buildRscPayloadResponse } from './rsc-payload.js';
72
72
  import { renderRscStream } from './rsc-stream.js';
73
73
  import { renderSsrResponse } from './ssr-renderer.js';
74
74
  import { callSsr } from './ssr-bridge.js';
75
75
  import { isDebug, isDevMode, setDebugFromConfig } from '#/server/debug.js';
76
+ import { recordTiming } from '#/server/server-timing.js';
77
+
78
+ /**
79
+ * Resolve the Server-Timing mode from timber.config.ts.
80
+ *
81
+ * If the user set `serverTiming` explicitly, use that value.
82
+ * Otherwise: `'detailed'` in dev, `'total'` in production.
83
+ */
84
+ function resolveServerTimingMode(
85
+ config: Record<string, unknown>,
86
+ isDev: boolean
87
+ ): 'detailed' | 'total' | false {
88
+ const userValue = config.serverTiming as 'detailed' | 'total' | false | undefined;
89
+ if (userValue !== undefined) return userValue;
90
+ return isDev ? 'detailed' : 'total';
91
+ }
76
92
 
77
93
  // Dev-only pipeline error handler, set by the dev server after import.
78
94
  // In production this is always undefined — no overhead.
79
- let _devPipelineErrorHandler: ((error: Error, phase: string) => void) | undefined;
95
+ // The third argument provides RSC debug component data (from the Flight
96
+ // debug channel) when available — used by the error overlay to show the
97
+ // server component tree context for render errors.
98
+ let _devPipelineErrorHandler:
99
+ | ((error: Error, phase: string, debugComponents?: DebugComponentEntry[]) => void)
100
+ | undefined;
80
101
 
81
102
  /**
82
103
  * Set the dev pipeline error handler.
83
104
  *
84
105
  * Called by the dev server after importing this module to wire pipeline
85
106
  * errors into the Vite browser error overlay. No-op in production.
107
+ *
108
+ * The handler receives an optional third argument with RSC debug component
109
+ * info — component names, environments, and stack frames from the Flight
110
+ * debug channel. This is only populated for render-phase errors.
86
111
  */
87
- export function setDevPipelineErrorHandler(handler: (error: Error, phase: string) => void): void {
112
+ export function setDevPipelineErrorHandler(
113
+ handler: (error: Error, phase: string, debugComponents?: DebugComponentEntry[]) => void
114
+ ): void {
88
115
  _devPipelineErrorHandler = handler;
89
116
  }
90
117
 
118
+ // Dev-only: holds a getter for the current request's RSC debug components.
119
+ // Updated on each renderRscStream call so the onPipelineError callback can
120
+ // include component tree context for render-phase errors. This is request-
121
+ // scoped by convention — each renderRoute call sets it before returning.
122
+ let _lastDebugComponentsGetter: (() => DebugComponentEntry[]) | undefined;
123
+
91
124
  /**
92
125
  * Create the RSC request handler from the route manifest.
93
126
  *
@@ -101,13 +134,15 @@ async function createRequestHandler(manifest: typeof routeManifest, runtimeConfi
101
134
  // See design/17-logging.md §"register() — Server Startup"
102
135
  await loadInstrumentation(loadUserInstrumentation);
103
136
 
104
- // Initialize cookie signing secrets from config (design/29-cookies.md §"Signed Cookies")
105
- const cookieSecrets = (runtimeConfig as Record<string, unknown>).cookieSecrets as
106
- | string[]
137
+ // Initialize deployment ID for version skew detection (TIM-446).
138
+ // The manifest init module sets globalThis.__TIMBER_DEPLOYMENT_ID__ at startup.
139
+ // In dev mode this is undefined — skew checks are skipped.
140
+ const deploymentId = (globalThis as Record<string, unknown>).__TIMBER_DEPLOYMENT_ID__ as
141
+ | string
107
142
  | undefined;
108
- if (cookieSecrets?.length) {
109
- const { setCookieSecrets } = await import('#/server/request-context.js');
110
- setCookieSecrets(cookieSecrets);
143
+ if (deploymentId) {
144
+ const { setDeploymentId } = await import('#/server/version-skew.js');
145
+ setDeploymentId(deploymentId);
111
146
  }
112
147
 
113
148
  const matchRoute = createRouteMatcher(manifest);
@@ -164,17 +199,6 @@ async function createRequestHandler(manifest: typeof routeManifest, runtimeConfi
164
199
 
165
200
  const typedBuildManifest = buildManifest as BuildManifest;
166
201
 
167
- // Initialize response-level caching and singleflight deduplication.
168
- // See design/31-benchmarking.md for performance motivation.
169
- const responseCacheRaw = (runtimeConfig as Record<string, unknown>).responseCache as
170
- | { maxSize?: number; ttlMs?: number; publicOnly?: boolean }
171
- | false
172
- | undefined;
173
- const responseCacheConfig = resolveResponseCacheConfig(responseCacheRaw);
174
- const responseCache: ResponseCache | null = responseCacheConfig
175
- ? createResponseCache(responseCacheConfig)
176
- : null;
177
-
178
202
  const pipelineConfig: PipelineConfig = {
179
203
  proxyLoader: manifest.proxy?.load,
180
204
  matchRoute,
@@ -205,17 +229,15 @@ async function createRequestHandler(manifest: typeof routeManifest, runtimeConfi
205
229
  _requestHeaderOverlay: Headers,
206
230
  interception?: InterceptionContext
207
231
  ) => {
208
- const doRender = () =>
209
- renderRoute(req, match, responseHeaders, clientBootstrap, clientJsDisabled, interception);
210
-
211
- // Response cache wraps the render with singleflight + LRU.
212
- // Interception requests (modals) are excluded — they depend on
213
- // X-Timber-URL which makes caching semantics ambiguous.
214
- if (responseCache && !interception) {
215
- const isRsc = (req.headers.get('Accept') ?? '').includes('text/x-component');
216
- return responseCache.getOrRender(req, isRsc, doRender);
217
- }
218
- return doRender();
232
+ return renderRoute(
233
+ req,
234
+ match,
235
+ responseHeaders,
236
+ clientBootstrap,
237
+ clientJsDisabled,
238
+ interception,
239
+ manifest.root
240
+ );
219
241
  },
220
242
  renderNoMatch: async (req: Request, responseHeaders: Headers) => {
221
243
  return renderNoMatchPage(req, manifest.root, responseHeaders, clientBootstrap);
@@ -224,10 +246,18 @@ async function createRequestHandler(manifest: typeof routeManifest, runtimeConfi
224
246
  // Slow request threshold from timber.config.ts. Default 3000ms, 0 to disable.
225
247
  // See design/17-logging.md §"slowRequestMs"
226
248
  slowRequestMs: (runtimeConfig as Record<string, unknown>).slowRequestMs as number | undefined,
227
- enableServerTiming: isDev,
249
+ serverTiming: resolveServerTimingMode(runtimeConfig, isDev),
228
250
  onPipelineError: isDev
229
251
  ? (error: Error, phase: string) => {
230
- if (_devPipelineErrorHandler) _devPipelineErrorHandler(error, phase);
252
+ if (_devPipelineErrorHandler) {
253
+ // For render-phase errors, include RSC debug component data
254
+ // from the Flight debug channel (if available from the current request).
255
+ const debugComponents =
256
+ phase === 'render' && _lastDebugComponentsGetter
257
+ ? _lastDebugComponentsGetter()
258
+ : undefined;
259
+ _devPipelineErrorHandler(error, phase, debugComponents);
260
+ }
231
261
  }
232
262
  : undefined,
233
263
  renderFallbackError: (error, req, responseHeaders) =>
@@ -307,7 +337,8 @@ async function renderRoute(
307
337
  responseHeaders: Headers,
308
338
  clientBootstrap: ClientBootstrapConfig,
309
339
  clientJsDisabled: boolean,
310
- interception?: InterceptionContext
340
+ interception?: InterceptionContext,
341
+ rootSegment?: ManifestSegmentNode
311
342
  ): Promise<Response> {
312
343
  const segments = match.segments as unknown as ManifestSegmentNode[];
313
344
  const leaf = segments[segments.length - 1];
@@ -329,6 +360,7 @@ async function renderRoute(
329
360
  // Build the React element tree — loads modules, runs access checks,
330
361
  // resolves metadata. DenySignal/RedirectSignal propagate for HTTP handling.
331
362
  let routeResult;
363
+ const _buildStart = performance.now();
332
364
  try {
333
365
  routeResult = await buildRouteElement(_req, match, interception, clientStateTree);
334
366
  } catch (error) {
@@ -361,14 +393,29 @@ async function renderRoute(
361
393
  return buildRedirectResponse(_req, signal, responseHeaders);
362
394
  }
363
395
  }
364
- // No PageComponent found
396
+ // Param coercion failed — render the custom 404 page (status files / not-found).
397
+ // Previously returned a bare Response(null, { status: 404 }) which bypassed
398
+ // custom not-found pages. Now routes through renderNoMatchPage so apps with
399
+ // 404.tsx / not-found status files render their custom page.
400
+ if (error instanceof ParamCoercionError) {
401
+ return renderNoMatchPage(_req, rootSegment!, responseHeaders, clientBootstrap);
402
+ }
403
+ // No PageComponent found — same treatment as param coercion: render custom 404.
365
404
  if (error instanceof Error && error.message.startsWith('No page component')) {
366
- return new Response(null, { status: 404 });
405
+ return renderNoMatchPage(_req, rootSegment!, responseHeaders, clientBootstrap);
367
406
  }
368
407
  throw error;
369
408
  }
370
409
 
371
- const { element, headElements, layoutComponents, deferSuspenseFor, skippedSegments } = routeResult;
410
+ const _buildEnd = performance.now();
411
+ recordTiming({
412
+ name: 'build',
413
+ dur: Math.round(_buildEnd - _buildStart),
414
+ desc: 'build element tree',
415
+ });
416
+
417
+ const { element, headElements, layoutComponents, deferSuspenseFor, skippedSegments } =
418
+ routeResult;
372
419
 
373
420
  // Build head HTML for injection into the SSR output.
374
421
  // Collects CSS, fonts, and modulepreload from the build manifest for matched segments.
@@ -385,6 +432,14 @@ async function renderRoute(
385
432
  headHtml += buildCssLinkTags(cssUrls);
386
433
  }
387
434
 
435
+ // Inline font CSS as a <style> tag — @font-face rules and scoped classes.
436
+ // The font CSS is set on globalThis by the transformed font file's
437
+ // side-effect import of virtual:timber-font-css-register.
438
+ const fontCss = (globalThis as Record<string, unknown>).__timber_font_css as string | undefined;
439
+ if (fontCss) {
440
+ headHtml += `<style data-timber-fonts>${fontCss}</style>`;
441
+ }
442
+
388
443
  const fontEntries = collectRouteFonts(segments, typedManifest);
389
444
  if (fontEntries.length > 0) {
390
445
  headHtml += buildFontPreloadTags(fontEntries);
@@ -411,7 +466,17 @@ async function renderRoute(
411
466
  }
412
467
 
413
468
  // Render to RSC Flight stream with signal tracking.
414
- const { rscStream, signals } = renderRscStream(element, _req);
469
+ const _rscStart = performance.now();
470
+ const { rscStream, signals, getDebugComponents } = renderRscStream(element, _req);
471
+
472
+ // Store the debug components getter so onPipelineError can include
473
+ // component tree context for render-phase errors (dev mode only).
474
+ _lastDebugComponentsGetter = getDebugComponents;
475
+ recordTiming({
476
+ name: 'rsc-init',
477
+ dur: Math.round(performance.now() - _rscStart),
478
+ desc: 'RSC stream init',
479
+ });
415
480
 
416
481
  // Synchronous redirect — redirect() in access.ts or a non-async component
417
482
  // throws during renderToReadableStream creation. Return HTTP redirect.
@@ -45,18 +45,45 @@ export async function buildRscPayloadResponse(
45
45
  skippedSegments?: string[]
46
46
  ): Promise<Response> {
47
47
  // Read the first chunk from the RSC stream before committing headers.
48
+ // Race the first read against signal detection — if an async component
49
+ // throws a RedirectSignal or DenySignal, the onError callback fires
50
+ // signals.onSignal() and we can react immediately without waiting for
51
+ // the full macrotask queue.
52
+ //
53
+ // The rejection chain for an async-wrapped page component:
54
+ // 1. PageComponent throws RedirectSignal
55
+ // 2. withSpan catches and re-throws (microtask 1)
56
+ // 3. TracedPage promise rejects (microtask 2)
57
+ // 4. React Flight rejection handler → onError (microtask 3+)
58
+ //
59
+ // Promise.race reacts the instant onError fires, eliminating the
60
+ // per-request setTimeout(0) macrotask delay for the common case
61
+ // (no signal). A 50ms ceiling timeout guards against edge cases
62
+ // where onError never fires.
48
63
  const reader = rscStream.getReader();
49
- const firstRead = await reader.read();
64
+ const signalDetected = new Promise<void>((resolve) => {
65
+ signals.onSignal = resolve;
66
+ });
50
67
 
51
- // Yield to the microtask queue so that async component rejections
52
- // (e.g. an async-wrapped page component that throws redirect())
53
- // propagate to the onError callback before we check the signals.
54
- // The rejected Promise from an async component resolves in the next
55
- // microtask after read(), so we need at least one tick.
56
- //
57
- // Uses queueMicrotask instead of setTimeout(0) to stay within the
58
- // same tick — no full event loop round-trip needed.
59
- await new Promise<void>((r) => queueMicrotask(r));
68
+ type RaceResult =
69
+ | { type: 'data'; chunk: ReadableStreamReadResult<Uint8Array> }
70
+ | { type: 'signal' };
71
+
72
+ const first: RaceResult = await Promise.race([
73
+ reader.read().then((chunk) => ({ type: 'data' as const, chunk })),
74
+ signalDetected.then(() => ({ type: 'signal' as const })),
75
+ ]);
76
+
77
+ // If data arrived first, still check signals — they may have fired
78
+ // concurrently. Also do a final ceiling timeout check for edge cases
79
+ // where the signal fires just after the first read resolves.
80
+ if (first.type === 'data' && !signals.redirectSignal && !signals.denySignal) {
81
+ // Brief yield to let any in-flight microtask rejections complete.
82
+ await new Promise<void>((r) => setTimeout(r, 0));
83
+ }
84
+
85
+ // Detach the callback — no longer needed after this point.
86
+ signals.onSignal = undefined;
60
87
 
61
88
  // Check for redirect/deny signals detected during initial rendering
62
89
  const trackedRedirect = signals.redirectSignal as RedirectSignal | null;
@@ -75,6 +102,19 @@ export async function buildRscPayloadResponse(
75
102
  );
76
103
  }
77
104
 
105
+ // Extract the first chunk from the race result.
106
+ // If the signal won the race but neither redirect nor deny was detected
107
+ // (edge case), cancel the reader immediately rather than issuing a bare
108
+ // read() that could hang forever if the RSC stream has stalled.
109
+ // See TIM-519.
110
+ let firstRead: ReadableStreamReadResult<Uint8Array>;
111
+ if (first.type === 'data') {
112
+ firstRead = first.chunk;
113
+ } else {
114
+ await reader.cancel();
115
+ firstRead = { done: true, value: undefined };
116
+ }
117
+
78
118
  // Reconstruct the stream: prepend the buffered first chunk,
79
119
  // then continue piping from the original reader.
80
120
  const patchedStream = new ReadableStream<Uint8Array>({
@@ -123,8 +163,8 @@ export async function buildRscPayloadResponse(
123
163
  responseHeaders.set('X-Timber-Skipped-Segments', JSON.stringify(skippedSegments));
124
164
  }
125
165
 
126
- // Send route params so the client can populate useParams() after
127
- // SPA navigation. Without this, useParams() returns {}.
166
+ // Send route params so the client can populate useSegmentParams() after
167
+ // SPA navigation. Without this, useSegmentParams() returns {}.
128
168
  if (Object.keys(match.params).length > 0) {
129
169
  responseHeaders.set('X-Timber-Params', JSON.stringify(match.params));
130
170
  }
@@ -16,24 +16,38 @@ import { logRenderError } from '#/server/logger.js';
16
16
  import { DenySignal, RedirectSignal, RenderError } from '#/server/primitives.js';
17
17
  import { checkAndWarnRscPropError } from '#/server/rsc-prop-warnings.js';
18
18
 
19
- import { createDebugChannelSink, isAbortError } from './helpers.js';
19
+ import {
20
+ createDebugChannelSink,
21
+ createDebugChannelCollector,
22
+ isAbortError,
23
+ type DebugComponentEntry,
24
+ } from './helpers.js';
20
25
  import { isDebug } from '#/server/debug.js';
26
+ import { isDevMode } from '#/server/debug.js';
21
27
 
22
28
  /**
23
29
  * Mutable signal state captured during RSC rendering.
24
30
  *
25
31
  * Signals fire asynchronously via `onError` during stream consumption.
26
32
  * The first signal of each type wins — subsequent signals are ignored.
33
+ *
34
+ * `onSignal` is an optional callback fired when a DenySignal or
35
+ * RedirectSignal is captured. Consumers use it with Promise.race to
36
+ * react immediately instead of polling with setTimeout/queueMicrotask.
27
37
  */
28
38
  export interface RenderSignals {
29
39
  denySignal: DenySignal | null;
30
40
  redirectSignal: RedirectSignal | null;
31
41
  renderError: { error: unknown; status: number } | null;
42
+ /** Callback fired when a redirect or deny signal is captured in onError. */
43
+ onSignal?: () => void;
32
44
  }
33
45
 
34
46
  export interface RscStreamResult {
35
47
  rscStream: ReadableStream<Uint8Array> | undefined;
36
48
  signals: RenderSignals;
49
+ /** Dev-only: server component debug info from the Flight debug channel. */
50
+ getDebugComponents?: () => DebugComponentEntry[];
37
51
  }
38
52
 
39
53
  /**
@@ -56,6 +70,10 @@ export function renderRscStream(element: React.ReactElement, req: Request): RscS
56
70
 
57
71
  let rscStream: ReadableStream<Uint8Array> | undefined;
58
72
 
73
+ // In dev mode, collect debug channel data for the error overlay.
74
+ // In production, use the discard sink (no overhead).
75
+ const debugChannel = isDevMode() ? createDebugChannelCollector() : createDebugChannelSink();
76
+
59
77
  try {
60
78
  rscStream = renderToReadableStream(
61
79
  element,
@@ -67,11 +85,13 @@ export function renderRscStream(element: React.ReactElement, req: Request): RscS
67
85
  if (isAbortError(error) || req.signal?.aborted) return;
68
86
  if (error instanceof DenySignal) {
69
87
  signals.denySignal = error;
88
+ signals.onSignal?.();
70
89
  // Return structured digest for client-side error boundaries
71
90
  return JSON.stringify({ type: 'deny', status: error.status, data: error.data });
72
91
  }
73
92
  if (error instanceof RedirectSignal) {
74
93
  signals.redirectSignal = error;
94
+ signals.onSignal?.();
75
95
  return JSON.stringify({
76
96
  type: 'redirect',
77
97
  location: error.location,
@@ -98,11 +118,7 @@ export function renderRscStream(element: React.ReactElement, req: Request): RscS
98
118
  // directive isn't at the very top of the file, or the component is
99
119
  // re-exported through a barrel file without 'use client'.
100
120
  // See LOCAL-297.
101
- if (
102
- isDebug() &&
103
- error instanceof Error &&
104
- error.message.includes('Invalid hook call')
105
- ) {
121
+ if (isDebug() && error instanceof Error && error.message.includes('Invalid hook call')) {
106
122
  console.error(
107
123
  '[timber] A React hook was called during RSC rendering. This usually means a ' +
108
124
  "'use client' component is being executed as a server component instead of " +
@@ -122,13 +138,25 @@ export function renderRscStream(element: React.ReactElement, req: Request): RscS
122
138
  checkAndWarnRscPropError(error, new URL(req.url).pathname);
123
139
  }
124
140
 
125
- // Track unhandled errors for pre-flush handling (500 status)
126
- if (!signals.renderError) {
127
- signals.renderError = { error, status: 500 };
128
- }
141
+ // Log the error but do NOT track it as a page-level render error.
142
+ // If this error is inside a <Suspense> boundary, React will emit
143
+ // an error row in the Flight stream and the Suspense fallback will
144
+ // render on the client. Tracking it as signals.renderError would
145
+ // cause the pipeline to treat the entire page as a 500, even though
146
+ // the shell rendered successfully. See TIM-524.
147
+ //
148
+ // Only track as renderError if no Suspense boundary contains it —
149
+ // React will call onShellError for truly unrecoverable errors.
129
150
  logRenderError({ method: req.method, path: new URL(req.url).pathname, error });
151
+
152
+ // Return a digest so React emits a per-row error in the Flight
153
+ // stream instead of leaving the lazy reference unresolved.
154
+ return JSON.stringify({
155
+ type: 'error',
156
+ message: error instanceof Error ? error.message : String(error),
157
+ });
130
158
  },
131
- debugChannel: createDebugChannelSink(),
159
+ debugChannel,
132
160
  },
133
161
  {
134
162
  onClientReference(info: { id: string; name: string; deps: unknown }) {
@@ -156,5 +184,14 @@ export function renderRscStream(element: React.ReactElement, req: Request): RscS
156
184
  }
157
185
  }
158
186
 
159
- return { rscStream, signals };
187
+ return {
188
+ rscStream,
189
+ signals,
190
+ // Expose the debug channel collector's getComponents in dev mode.
191
+ // The caller can retrieve component tree info when handling errors.
192
+ getDebugComponents:
193
+ 'getComponents' in debugChannel
194
+ ? (debugChannel as { getComponents: () => DebugComponentEntry[] }).getComponents
195
+ : undefined,
196
+ };
160
197
  }