@timber-js/app 0.2.0-alpha.4 → 0.2.0-alpha.41

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 (336) 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-ECi_61pb.js +108 -0
  6. package/dist/_chunks/debug-ECi_61pb.js.map +1 -0
  7. package/dist/_chunks/define-cookie-BmKbSyp0.js +93 -0
  8. package/dist/_chunks/define-cookie-BmKbSyp0.js.map +1 -0
  9. package/dist/_chunks/error-boundary-BAN3751q.js +211 -0
  10. package/dist/_chunks/error-boundary-BAN3751q.js.map +1 -0
  11. package/dist/_chunks/{format-CwdaB0_2.js → format-cX7wzEp2.js} +2 -2
  12. package/dist/_chunks/{format-CwdaB0_2.js.map → format-cX7wzEp2.js.map} +1 -1
  13. package/dist/_chunks/{interception-BOoWmLUA.js → interception-D2djYaIm.js} +112 -77
  14. package/dist/_chunks/interception-D2djYaIm.js.map +1 -0
  15. package/dist/_chunks/{metadata-routes-Cjmvi3rQ.js → metadata-routes-BU684ls2.js} +1 -1
  16. package/dist/_chunks/{metadata-routes-Cjmvi3rQ.js.map → metadata-routes-BU684ls2.js.map} +1 -1
  17. package/dist/_chunks/{request-context-CZJi4CuK.js → request-context-BxYIJM24.js} +93 -69
  18. package/dist/_chunks/request-context-BxYIJM24.js.map +1 -0
  19. package/dist/_chunks/segment-context-C6byCyZU.js +69 -0
  20. package/dist/_chunks/segment-context-C6byCyZU.js.map +1 -0
  21. package/dist/_chunks/stale-reload-C0ValzG7.js +47 -0
  22. package/dist/_chunks/stale-reload-C0ValzG7.js.map +1 -0
  23. package/dist/_chunks/{tracing-Cwn7697K.js → tracing-CuXiCP5p.js} +17 -3
  24. package/dist/_chunks/{tracing-Cwn7697K.js.map → tracing-CuXiCP5p.js.map} +1 -1
  25. package/dist/_chunks/{use-query-states-D5KaffOK.js → use-query-states-BvW0TKDn.js} +1 -1
  26. package/dist/_chunks/{use-query-states-D5KaffOK.js.map → use-query-states-BvW0TKDn.js.map} +1 -1
  27. package/dist/_chunks/wrappers-C6J0nNji.js +331 -0
  28. package/dist/_chunks/wrappers-C6J0nNji.js.map +1 -0
  29. package/dist/adapters/compress-module.d.ts.map +1 -1
  30. package/dist/adapters/nitro.d.ts +17 -1
  31. package/dist/adapters/nitro.d.ts.map +1 -1
  32. package/dist/adapters/nitro.js +56 -13
  33. package/dist/adapters/nitro.js.map +1 -1
  34. package/dist/cache/fast-hash.d.ts +22 -0
  35. package/dist/cache/fast-hash.d.ts.map +1 -0
  36. package/dist/cache/index.d.ts +5 -2
  37. package/dist/cache/index.d.ts.map +1 -1
  38. package/dist/cache/index.js +88 -18
  39. package/dist/cache/index.js.map +1 -1
  40. package/dist/cache/register-cached-function.d.ts.map +1 -1
  41. package/dist/cache/singleflight.d.ts +18 -1
  42. package/dist/cache/singleflight.d.ts.map +1 -1
  43. package/dist/cache/timber-cache.d.ts.map +1 -1
  44. package/dist/client/error-boundary.d.ts +10 -1
  45. package/dist/client/error-boundary.d.ts.map +1 -1
  46. package/dist/client/error-boundary.js +1 -125
  47. package/dist/client/index.d.ts +3 -2
  48. package/dist/client/index.d.ts.map +1 -1
  49. package/dist/client/index.js +213 -93
  50. package/dist/client/index.js.map +1 -1
  51. package/dist/client/link.d.ts +22 -8
  52. package/dist/client/link.d.ts.map +1 -1
  53. package/dist/client/navigation-context.d.ts +2 -2
  54. package/dist/client/router.d.ts +25 -3
  55. package/dist/client/router.d.ts.map +1 -1
  56. package/dist/client/rsc-fetch.d.ts +23 -2
  57. package/dist/client/rsc-fetch.d.ts.map +1 -1
  58. package/dist/client/segment-cache.d.ts +1 -1
  59. package/dist/client/segment-cache.d.ts.map +1 -1
  60. package/dist/client/segment-context.d.ts +1 -1
  61. package/dist/client/segment-context.d.ts.map +1 -1
  62. package/dist/client/segment-merger.d.ts.map +1 -1
  63. package/dist/client/stale-reload.d.ts +15 -0
  64. package/dist/client/stale-reload.d.ts.map +1 -1
  65. package/dist/client/top-loader.d.ts +1 -1
  66. package/dist/client/top-loader.d.ts.map +1 -1
  67. package/dist/client/transition-root.d.ts +1 -1
  68. package/dist/client/transition-root.d.ts.map +1 -1
  69. package/dist/client/use-params.d.ts +2 -2
  70. package/dist/client/use-params.d.ts.map +1 -1
  71. package/dist/client/use-query-states.d.ts +1 -1
  72. package/dist/codec.d.ts +21 -0
  73. package/dist/codec.d.ts.map +1 -0
  74. package/dist/cookies/define-cookie.d.ts +33 -12
  75. package/dist/cookies/define-cookie.d.ts.map +1 -1
  76. package/dist/cookies/index.js +1 -83
  77. package/dist/fonts/css.d.ts +1 -0
  78. package/dist/fonts/css.d.ts.map +1 -1
  79. package/dist/fonts/local.d.ts +4 -2
  80. package/dist/fonts/local.d.ts.map +1 -1
  81. package/dist/index.d.ts +112 -35
  82. package/dist/index.d.ts.map +1 -1
  83. package/dist/index.js +635 -233
  84. package/dist/index.js.map +1 -1
  85. package/dist/params/define.d.ts +76 -0
  86. package/dist/params/define.d.ts.map +1 -0
  87. package/dist/params/index.d.ts +8 -0
  88. package/dist/params/index.d.ts.map +1 -0
  89. package/dist/params/index.js +104 -0
  90. package/dist/params/index.js.map +1 -0
  91. package/dist/plugins/adapter-build.d.ts.map +1 -1
  92. package/dist/plugins/build-manifest.d.ts.map +1 -1
  93. package/dist/plugins/client-chunks.d.ts +32 -0
  94. package/dist/plugins/client-chunks.d.ts.map +1 -0
  95. package/dist/plugins/dev-error-overlay.d.ts +26 -1
  96. package/dist/plugins/dev-error-overlay.d.ts.map +1 -1
  97. package/dist/plugins/entries.d.ts +7 -0
  98. package/dist/plugins/entries.d.ts.map +1 -1
  99. package/dist/plugins/fonts.d.ts +9 -1
  100. package/dist/plugins/fonts.d.ts.map +1 -1
  101. package/dist/plugins/mdx.d.ts +6 -0
  102. package/dist/plugins/mdx.d.ts.map +1 -1
  103. package/dist/plugins/routing.d.ts.map +1 -1
  104. package/dist/plugins/server-bundle.d.ts.map +1 -1
  105. package/dist/plugins/static-build.d.ts.map +1 -1
  106. package/dist/routing/codegen.d.ts +2 -2
  107. package/dist/routing/codegen.d.ts.map +1 -1
  108. package/dist/routing/index.js +1 -1
  109. package/dist/routing/scanner.d.ts.map +1 -1
  110. package/dist/routing/status-file-lint.d.ts +2 -1
  111. package/dist/routing/status-file-lint.d.ts.map +1 -1
  112. package/dist/routing/types.d.ts +6 -4
  113. package/dist/routing/types.d.ts.map +1 -1
  114. package/dist/rsc-runtime/rsc.d.ts +1 -1
  115. package/dist/rsc-runtime/rsc.d.ts.map +1 -1
  116. package/dist/rsc-runtime/ssr.d.ts +12 -0
  117. package/dist/rsc-runtime/ssr.d.ts.map +1 -1
  118. package/dist/search-params/codecs.d.ts +1 -1
  119. package/dist/search-params/define.d.ts +153 -0
  120. package/dist/search-params/define.d.ts.map +1 -0
  121. package/dist/search-params/index.d.ts +4 -5
  122. package/dist/search-params/index.d.ts.map +1 -1
  123. package/dist/search-params/index.js +3 -474
  124. package/dist/search-params/registry.d.ts +1 -1
  125. package/dist/search-params/wrappers.d.ts +53 -0
  126. package/dist/search-params/wrappers.d.ts.map +1 -0
  127. package/dist/server/access-gate.d.ts +4 -0
  128. package/dist/server/access-gate.d.ts.map +1 -1
  129. package/dist/server/action-client.d.ts.map +1 -1
  130. package/dist/server/action-encryption.d.ts +76 -0
  131. package/dist/server/action-encryption.d.ts.map +1 -0
  132. package/dist/server/action-handler.d.ts.map +1 -1
  133. package/dist/server/als-registry.d.ts +18 -4
  134. package/dist/server/als-registry.d.ts.map +1 -1
  135. package/dist/server/build-manifest.d.ts +2 -2
  136. package/dist/server/debug.d.ts +46 -15
  137. package/dist/server/debug.d.ts.map +1 -1
  138. package/dist/server/default-logger.d.ts +22 -0
  139. package/dist/server/default-logger.d.ts.map +1 -0
  140. package/dist/server/deny-renderer.d.ts.map +1 -1
  141. package/dist/server/early-hints.d.ts +13 -5
  142. package/dist/server/early-hints.d.ts.map +1 -1
  143. package/dist/server/error-boundary-wrapper.d.ts +4 -0
  144. package/dist/server/error-boundary-wrapper.d.ts.map +1 -1
  145. package/dist/server/flight-injection-state.d.ts +78 -0
  146. package/dist/server/flight-injection-state.d.ts.map +1 -0
  147. package/dist/server/flight-scripts.d.ts +39 -0
  148. package/dist/server/flight-scripts.d.ts.map +1 -0
  149. package/dist/server/flush.d.ts.map +1 -1
  150. package/dist/server/form-data.d.ts +29 -0
  151. package/dist/server/form-data.d.ts.map +1 -1
  152. package/dist/server/html-injectors.d.ts +5 -11
  153. package/dist/server/html-injectors.d.ts.map +1 -1
  154. package/dist/server/index.d.ts +4 -2
  155. package/dist/server/index.d.ts.map +1 -1
  156. package/dist/server/index.js +1975 -1649
  157. package/dist/server/index.js.map +1 -1
  158. package/dist/server/logger.d.ts +24 -7
  159. package/dist/server/logger.d.ts.map +1 -1
  160. package/dist/server/node-stream-transforms.d.ts +77 -0
  161. package/dist/server/node-stream-transforms.d.ts.map +1 -0
  162. package/dist/server/pipeline.d.ts +7 -4
  163. package/dist/server/pipeline.d.ts.map +1 -1
  164. package/dist/server/primitives.d.ts +30 -3
  165. package/dist/server/primitives.d.ts.map +1 -1
  166. package/dist/server/render-timeout.d.ts +51 -0
  167. package/dist/server/render-timeout.d.ts.map +1 -0
  168. package/dist/server/request-context.d.ts +65 -38
  169. package/dist/server/request-context.d.ts.map +1 -1
  170. package/dist/server/route-element-builder.d.ts +7 -0
  171. package/dist/server/route-element-builder.d.ts.map +1 -1
  172. package/dist/server/route-handler.d.ts.map +1 -1
  173. package/dist/server/route-matcher.d.ts +2 -2
  174. package/dist/server/route-matcher.d.ts.map +1 -1
  175. package/dist/server/rsc-entry/error-renderer.d.ts.map +1 -1
  176. package/dist/server/rsc-entry/helpers.d.ts +46 -3
  177. package/dist/server/rsc-entry/helpers.d.ts.map +1 -1
  178. package/dist/server/rsc-entry/index.d.ts +6 -1
  179. package/dist/server/rsc-entry/index.d.ts.map +1 -1
  180. package/dist/server/rsc-entry/rsc-payload.d.ts.map +1 -1
  181. package/dist/server/rsc-entry/rsc-stream.d.ts +9 -0
  182. package/dist/server/rsc-entry/rsc-stream.d.ts.map +1 -1
  183. package/dist/server/rsc-entry/ssr-renderer.d.ts.map +1 -1
  184. package/dist/server/slot-resolver.d.ts +1 -1
  185. package/dist/server/slot-resolver.d.ts.map +1 -1
  186. package/dist/server/ssr-entry.d.ts +22 -0
  187. package/dist/server/ssr-entry.d.ts.map +1 -1
  188. package/dist/server/ssr-render.d.ts +39 -21
  189. package/dist/server/ssr-render.d.ts.map +1 -1
  190. package/dist/server/tracing.d.ts +10 -0
  191. package/dist/server/tracing.d.ts.map +1 -1
  192. package/dist/server/tree-builder.d.ts +19 -12
  193. package/dist/server/tree-builder.d.ts.map +1 -1
  194. package/dist/server/types.d.ts +1 -3
  195. package/dist/server/types.d.ts.map +1 -1
  196. package/dist/server/version-skew.d.ts +61 -0
  197. package/dist/server/version-skew.d.ts.map +1 -0
  198. package/dist/server/waituntil-bridge.d.ts.map +1 -1
  199. package/dist/shared/merge-search-params.d.ts +22 -0
  200. package/dist/shared/merge-search-params.d.ts.map +1 -0
  201. package/dist/shims/navigation-client.d.ts +1 -1
  202. package/dist/shims/navigation-client.d.ts.map +1 -1
  203. package/dist/shims/navigation.d.ts +1 -1
  204. package/dist/shims/navigation.d.ts.map +1 -1
  205. package/dist/utils/state-machine.d.ts +80 -0
  206. package/dist/utils/state-machine.d.ts.map +1 -0
  207. package/package.json +17 -14
  208. package/src/adapters/compress-module.ts +24 -4
  209. package/src/adapters/nitro.ts +58 -9
  210. package/src/cache/fast-hash.ts +34 -0
  211. package/src/cache/index.ts +5 -2
  212. package/src/cache/register-cached-function.ts +7 -3
  213. package/src/cache/singleflight.ts +62 -4
  214. package/src/cache/timber-cache.ts +34 -26
  215. package/src/cli.ts +0 -0
  216. package/src/client/browser-entry.ts +94 -90
  217. package/src/client/error-boundary.tsx +18 -1
  218. package/src/client/index.ts +10 -1
  219. package/src/client/link.tsx +78 -19
  220. package/src/client/navigation-context.ts +2 -2
  221. package/src/client/router.ts +105 -60
  222. package/src/client/rsc-fetch.ts +63 -2
  223. package/src/client/segment-cache.ts +1 -1
  224. package/src/client/segment-context.ts +6 -1
  225. package/src/client/segment-merger.ts +2 -8
  226. package/src/client/stale-reload.ts +32 -6
  227. package/src/client/top-loader.tsx +10 -9
  228. package/src/client/transition-root.tsx +7 -1
  229. package/src/client/use-params.ts +3 -3
  230. package/src/client/use-query-states.ts +1 -1
  231. package/src/codec.ts +21 -0
  232. package/src/cookies/define-cookie.ts +69 -18
  233. package/src/fonts/css.ts +2 -1
  234. package/src/fonts/local.ts +7 -3
  235. package/src/index.ts +280 -85
  236. package/src/params/define.ts +260 -0
  237. package/src/params/index.ts +28 -0
  238. package/src/plugins/adapter-build.ts +6 -0
  239. package/src/plugins/build-manifest.ts +11 -0
  240. package/src/plugins/client-chunks.ts +65 -0
  241. package/src/plugins/dev-error-overlay.ts +70 -1
  242. package/src/plugins/dev-server.ts +38 -4
  243. package/src/plugins/entries.ts +12 -11
  244. package/src/plugins/fonts.ts +171 -19
  245. package/src/plugins/mdx.ts +9 -5
  246. package/src/plugins/routing.ts +40 -14
  247. package/src/plugins/server-bundle.ts +32 -1
  248. package/src/plugins/shims.ts +1 -1
  249. package/src/plugins/static-build.ts +8 -4
  250. package/src/routing/codegen.ts +109 -88
  251. package/src/routing/scanner.ts +55 -6
  252. package/src/routing/status-file-lint.ts +2 -1
  253. package/src/routing/types.ts +7 -4
  254. package/src/rsc-runtime/rsc.ts +2 -0
  255. package/src/rsc-runtime/ssr.ts +50 -0
  256. package/src/rsc-runtime/vendor-types.d.ts +7 -0
  257. package/src/search-params/codecs.ts +1 -1
  258. package/src/search-params/define.ts +504 -0
  259. package/src/search-params/index.ts +12 -18
  260. package/src/search-params/registry.ts +1 -1
  261. package/src/search-params/wrappers.ts +85 -0
  262. package/src/server/access-gate.tsx +40 -9
  263. package/src/server/action-client.ts +14 -5
  264. package/src/server/action-encryption.ts +144 -0
  265. package/src/server/action-handler.ts +19 -2
  266. package/src/server/als-registry.ts +18 -4
  267. package/src/server/build-manifest.ts +4 -4
  268. package/src/server/compress.ts +25 -7
  269. package/src/server/debug.ts +55 -17
  270. package/src/server/default-logger.ts +98 -0
  271. package/src/server/deny-renderer.ts +2 -1
  272. package/src/server/early-hints.ts +36 -15
  273. package/src/server/error-boundary-wrapper.ts +57 -14
  274. package/src/server/flight-injection-state.ts +152 -0
  275. package/src/server/flight-scripts.ts +59 -0
  276. package/src/server/flush.ts +2 -1
  277. package/src/server/form-data.ts +76 -0
  278. package/src/server/html-injectors.ts +103 -66
  279. package/src/server/index.ts +9 -4
  280. package/src/server/logger.ts +38 -35
  281. package/src/server/node-stream-transforms.ts +381 -0
  282. package/src/server/pipeline.ts +131 -39
  283. package/src/server/primitives.ts +47 -5
  284. package/src/server/render-timeout.ts +108 -0
  285. package/src/server/request-context.ts +112 -119
  286. package/src/server/route-element-builder.ts +106 -114
  287. package/src/server/route-handler.ts +2 -1
  288. package/src/server/route-matcher.ts +2 -2
  289. package/src/server/rsc-entry/error-renderer.ts +5 -3
  290. package/src/server/rsc-entry/helpers.ts +122 -3
  291. package/src/server/rsc-entry/index.ts +125 -49
  292. package/src/server/rsc-entry/rsc-payload.ts +52 -12
  293. package/src/server/rsc-entry/rsc-stream.ts +33 -8
  294. package/src/server/rsc-entry/ssr-renderer.ts +40 -13
  295. package/src/server/slot-resolver.ts +199 -210
  296. package/src/server/ssr-entry.ts +168 -22
  297. package/src/server/ssr-render.ts +289 -67
  298. package/src/server/tracing.ts +23 -0
  299. package/src/server/tree-builder.ts +91 -57
  300. package/src/server/types.ts +1 -3
  301. package/src/server/version-skew.ts +104 -0
  302. package/src/server/waituntil-bridge.ts +4 -1
  303. package/src/shared/merge-search-params.ts +48 -0
  304. package/src/shims/navigation-client.ts +1 -1
  305. package/src/shims/navigation.ts +1 -1
  306. package/src/utils/state-machine.ts +111 -0
  307. package/dist/_chunks/als-registry-B7DbZ2hS.js.map +0 -1
  308. package/dist/_chunks/debug-B4WUeqJ-.js +0 -75
  309. package/dist/_chunks/debug-B4WUeqJ-.js.map +0 -1
  310. package/dist/_chunks/interception-BOoWmLUA.js.map +0 -1
  311. package/dist/_chunks/request-context-CZJi4CuK.js.map +0 -1
  312. package/dist/_chunks/ssr-data-MjmprTmO.js +0 -88
  313. package/dist/_chunks/ssr-data-MjmprTmO.js.map +0 -1
  314. package/dist/_chunks/use-cookie-DX-l1_5E.js +0 -91
  315. package/dist/_chunks/use-cookie-DX-l1_5E.js.map +0 -1
  316. package/dist/client/error-boundary.js.map +0 -1
  317. package/dist/cookies/index.js.map +0 -1
  318. package/dist/plugins/dynamic-transform.d.ts +0 -72
  319. package/dist/plugins/dynamic-transform.d.ts.map +0 -1
  320. package/dist/search-params/analyze.d.ts +0 -54
  321. package/dist/search-params/analyze.d.ts.map +0 -1
  322. package/dist/search-params/builtin-codecs.d.ts +0 -105
  323. package/dist/search-params/builtin-codecs.d.ts.map +0 -1
  324. package/dist/search-params/create.d.ts +0 -106
  325. package/dist/search-params/create.d.ts.map +0 -1
  326. package/dist/search-params/index.js.map +0 -1
  327. package/dist/server/prerender.d.ts +0 -77
  328. package/dist/server/prerender.d.ts.map +0 -1
  329. package/dist/server/response-cache.d.ts +0 -53
  330. package/dist/server/response-cache.d.ts.map +0 -1
  331. package/src/plugins/dynamic-transform.ts +0 -161
  332. package/src/search-params/analyze.ts +0 -192
  333. package/src/search-params/builtin-codecs.ts +0 -228
  334. package/src/search-params/create.ts +0 -321
  335. package/src/server/prerender.ts +0 -139
  336. package/src/server/response-cache.ts +0 -277
@@ -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
- import { isDebug, setDebugFromConfig } from '#/server/debug.js';
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);
@@ -127,9 +162,6 @@ async function createRequestHandler(manifest: typeof routeManifest, runtimeConfi
127
162
  buildManifest: buildManifest as BuildManifest,
128
163
  });
129
164
 
130
- // Dev logging — initialize OTEL-based dev tracing once at handler creation.
131
- // In production, isDev is false — no tracing, no overhead.
132
- // The DevSpanProcessor handles all formatting and stderr output.
133
165
  // Initialize debug flag from config before anything else.
134
166
  // This allows timber.config.ts `debug: true` to enable debug logging
135
167
  // in production without the TIMBER_DEBUG env var.
@@ -137,10 +169,24 @@ async function createRequestHandler(manifest: typeof routeManifest, runtimeConfi
137
169
  setDebugFromConfig(true);
138
170
  }
139
171
 
140
- const isDev = isDebug();
172
+ // Two separate flags for two different security levels:
173
+ //
174
+ // isDev (isDevMode) — gates client-visible behavior: dev error pages with
175
+ // stack traces, detailed Server-Timing headers, error messages in action
176
+ // payloads. Statically replaced in production → tree-shaken to zero.
177
+ // TIMBER_DEBUG cannot enable this.
178
+ //
179
+ // debugEnabled (isDebug) — gates server-side logging only: stderr warnings,
180
+ // OTEL dev tracing, console.error fallbacks. TIMBER_DEBUG enables this.
181
+ // Never affects what clients see.
182
+ const isDev = isDevMode();
183
+ const debugEnabled = isDebug();
141
184
  const slowPhaseMs = (runtimeConfig as Record<string, unknown>).slowPhaseMs as number | undefined;
142
185
 
143
- if (isDev) {
186
+ // Dev logging — initialize OTEL-based dev tracing once at handler creation.
187
+ // In production with TIMBER_DEBUG, this enables server-side tracing output
188
+ // without exposing anything to clients.
189
+ if (debugEnabled) {
144
190
  const devLogMode = resolveLogMode();
145
191
  if (devLogMode !== 'quiet') {
146
192
  await initDevTracing({ mode: devLogMode, slowPhaseMs });
@@ -153,17 +199,6 @@ async function createRequestHandler(manifest: typeof routeManifest, runtimeConfi
153
199
 
154
200
  const typedBuildManifest = buildManifest as BuildManifest;
155
201
 
156
- // Initialize response-level caching and singleflight deduplication.
157
- // See design/31-benchmarking.md for performance motivation.
158
- const responseCacheRaw = (runtimeConfig as Record<string, unknown>).responseCache as
159
- | { maxSize?: number; ttlMs?: number; publicOnly?: boolean }
160
- | false
161
- | undefined;
162
- const responseCacheConfig = resolveResponseCacheConfig(responseCacheRaw);
163
- const responseCache: ResponseCache | null = responseCacheConfig
164
- ? createResponseCache(responseCacheConfig)
165
- : null;
166
-
167
202
  const pipelineConfig: PipelineConfig = {
168
203
  proxyLoader: manifest.proxy?.load,
169
204
  matchRoute,
@@ -194,17 +229,15 @@ async function createRequestHandler(manifest: typeof routeManifest, runtimeConfi
194
229
  _requestHeaderOverlay: Headers,
195
230
  interception?: InterceptionContext
196
231
  ) => {
197
- const doRender = () =>
198
- renderRoute(req, match, responseHeaders, clientBootstrap, clientJsDisabled, interception);
199
-
200
- // Response cache wraps the render with singleflight + LRU.
201
- // Interception requests (modals) are excluded — they depend on
202
- // X-Timber-URL which makes caching semantics ambiguous.
203
- if (responseCache && !interception) {
204
- const isRsc = (req.headers.get('Accept') ?? '').includes('text/x-component');
205
- return responseCache.getOrRender(req, isRsc, doRender);
206
- }
207
- return doRender();
232
+ return renderRoute(
233
+ req,
234
+ match,
235
+ responseHeaders,
236
+ clientBootstrap,
237
+ clientJsDisabled,
238
+ interception,
239
+ manifest.root
240
+ );
208
241
  },
209
242
  renderNoMatch: async (req: Request, responseHeaders: Headers) => {
210
243
  return renderNoMatchPage(req, manifest.root, responseHeaders, clientBootstrap);
@@ -213,10 +246,18 @@ async function createRequestHandler(manifest: typeof routeManifest, runtimeConfi
213
246
  // Slow request threshold from timber.config.ts. Default 3000ms, 0 to disable.
214
247
  // See design/17-logging.md §"slowRequestMs"
215
248
  slowRequestMs: (runtimeConfig as Record<string, unknown>).slowRequestMs as number | undefined,
216
- enableServerTiming: isDev,
249
+ serverTiming: resolveServerTimingMode(runtimeConfig, isDev),
217
250
  onPipelineError: isDev
218
251
  ? (error: Error, phase: string) => {
219
- 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
+ }
220
261
  }
221
262
  : undefined,
222
263
  renderFallbackError: (error, req, responseHeaders) =>
@@ -296,7 +337,8 @@ async function renderRoute(
296
337
  responseHeaders: Headers,
297
338
  clientBootstrap: ClientBootstrapConfig,
298
339
  clientJsDisabled: boolean,
299
- interception?: InterceptionContext
340
+ interception?: InterceptionContext,
341
+ rootSegment?: ManifestSegmentNode
300
342
  ): Promise<Response> {
301
343
  const segments = match.segments as unknown as ManifestSegmentNode[];
302
344
  const leaf = segments[segments.length - 1];
@@ -318,6 +360,7 @@ async function renderRoute(
318
360
  // Build the React element tree — loads modules, runs access checks,
319
361
  // resolves metadata. DenySignal/RedirectSignal propagate for HTTP handling.
320
362
  let routeResult;
363
+ const _buildStart = performance.now();
321
364
  try {
322
365
  routeResult = await buildRouteElement(_req, match, interception, clientStateTree);
323
366
  } catch (error) {
@@ -350,14 +393,29 @@ async function renderRoute(
350
393
  return buildRedirectResponse(_req, signal, responseHeaders);
351
394
  }
352
395
  }
353
- // 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.
354
404
  if (error instanceof Error && error.message.startsWith('No page component')) {
355
- return new Response(null, { status: 404 });
405
+ return renderNoMatchPage(_req, rootSegment!, responseHeaders, clientBootstrap);
356
406
  }
357
407
  throw error;
358
408
  }
359
409
 
360
- 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;
361
419
 
362
420
  // Build head HTML for injection into the SSR output.
363
421
  // Collects CSS, fonts, and modulepreload from the build manifest for matched segments.
@@ -374,6 +432,14 @@ async function renderRoute(
374
432
  headHtml += buildCssLinkTags(cssUrls);
375
433
  }
376
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
+
377
443
  const fontEntries = collectRouteFonts(segments, typedManifest);
378
444
  if (fontEntries.length > 0) {
379
445
  headHtml += buildFontPreloadTags(fontEntries);
@@ -400,7 +466,17 @@ async function renderRoute(
400
466
  }
401
467
 
402
468
  // Render to RSC Flight stream with signal tracking.
403
- 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
+ });
404
480
 
405
481
  // Synchronous redirect — redirect() in access.ts or a non-async component
406
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 " +
@@ -128,7 +144,7 @@ export function renderRscStream(element: React.ReactElement, req: Request): RscS
128
144
  }
129
145
  logRenderError({ method: req.method, path: new URL(req.url).pathname, error });
130
146
  },
131
- debugChannel: createDebugChannelSink(),
147
+ debugChannel,
132
148
  },
133
149
  {
134
150
  onClientReference(info: { id: string; name: string; deps: unknown }) {
@@ -156,5 +172,14 @@ export function renderRscStream(element: React.ReactElement, req: Request): RscS
156
172
  }
157
173
  }
158
174
 
159
- return { rscStream, signals };
175
+ return {
176
+ rscStream,
177
+ signals,
178
+ // Expose the debug channel collector's getComponents in dev mode.
179
+ // The caller can retrieve component tree info when handling errors.
180
+ getDebugComponents:
181
+ 'getComponents' in debugChannel
182
+ ? (debugChannel as { getComponents: () => DebugComponentEntry[] }).getComponents
183
+ : undefined,
184
+ };
160
185
  }
@@ -13,6 +13,7 @@
13
13
  */
14
14
 
15
15
  import type { ClientBootstrapConfig } from '#/server/html-injectors.js';
16
+ import { flightInitScript } from '#/server/flight-scripts.js';
16
17
  import type { LayoutEntry } from '#/server/deny-renderer.js';
17
18
  import { renderDenyPage } from '#/server/deny-renderer.js';
18
19
  import type { RouteMatch } from '#/server/pipeline.js';
@@ -26,11 +27,12 @@ import {
26
27
  buildSegmentInfo,
27
28
  createDebugChannelSink,
28
29
  isAbortError,
29
- parseCookiesFromHeader,
30
30
  } from './helpers.js';
31
+ import { getCookiesForSsr } from '#/server/request-context.js';
31
32
  import { renderErrorPage } from './error-renderer.js';
32
33
  import { callSsr } from './ssr-bridge.js';
33
34
  import type { RenderSignals } from './rsc-stream.js';
35
+ import { recordTiming } from '#/server/server-timing.js';
34
36
 
35
37
  interface SsrRenderOptions {
36
38
  req: Request;
@@ -91,8 +93,8 @@ export async function renderSsrResponse(opts: SsrRenderOptions): Promise<Respons
91
93
  ? ''
92
94
  : `<script>self.__timber_segments=${JSON.stringify(buildSegmentInfo(segments, layoutComponents))}</script>`;
93
95
 
94
- // Embed route params in HTML so useParams() works on initial hydration.
95
- // Without this, useParams() returns {} until the first client navigation.
96
+ // Embed route params in HTML so useSegmentParams() works on initial hydration.
97
+ // Without this, useSegmentParams() returns {} until the first client navigation.
96
98
  const paramsScript =
97
99
  clientJsDisabled || Object.keys(match.params).length === 0
98
100
  ? ''
@@ -104,13 +106,20 @@ export async function renderSsrResponse(opts: SsrRenderOptions): Promise<Respons
104
106
  searchParams: Object.fromEntries(new URL(req.url).searchParams),
105
107
  statusCode: 200,
106
108
  responseHeaders,
107
- headHtml: headHtml + clientBootstrap.preloadLinks + segmentScript + paramsScript,
109
+ headHtml:
110
+ headHtml +
111
+ clientBootstrap.preloadLinks +
112
+ segmentScript +
113
+ paramsScript +
114
+ // Initialize __timber_f in <head> so it exists before any streaming
115
+ // chunk scripts arrive in <body>. See flight-scripts.ts, LOCAL-415.
116
+ (clientJsDisabled ? '' : flightInitScript()),
108
117
  bootstrapScriptContent: clientBootstrap.bootstrapScriptContent,
109
118
  // Skip RSC inline stream when client JS is disabled — no client to hydrate.
110
119
  rscStream: clientJsDisabled ? undefined : inlineStream,
111
120
  deferSuspenseFor: deferSuspenseFor > 0 ? deferSuspenseFor : undefined,
112
121
  signal: req.signal,
113
- cookies: parseCookiesFromHeader(req.headers.get('cookie') ?? ''),
122
+ cookies: getCookiesForSsr(),
114
123
  };
115
124
 
116
125
  // Helper: check if render-phase signals were captured and return the
@@ -156,16 +165,34 @@ export async function renderSsrResponse(opts: SsrRenderOptions): Promise<Respons
156
165
  try {
157
166
  const ssrResponse = await callSsr(ssrStream, navContext);
158
167
 
159
- // Signal promotion: yield one microtask so async component rejections
160
- // propagate to the RSC onError callback, then check if any signals
161
- // were captured during rendering inside Suspense boundaries.
162
- // The Response hasn't been sent yet — it's an unconsumed stream.
168
+ // Record SSR sub-phase timings for Server-Timing header (detailed mode).
169
+ // These are populated by handleSsr() in the SSR environment and passed
170
+ // back via navContext._ssrTimings across the RSC→SSR boundary.
171
+ if (navContext._ssrTimings) {
172
+ const t = navContext._ssrTimings;
173
+ recordTiming({ name: 'ssr-decode', dur: t.decodeMs, desc: 'RSC Flight decode' });
174
+ recordTiming({ name: 'ssr-shell', dur: t.shellMs, desc: 'Fizz onShellReady' });
175
+ recordTiming({ name: 'ssr-pipeline', dur: t.pipelineMs, desc: 'stream transforms' });
176
+ recordTiming({
177
+ name: 'ssr-total',
178
+ dur: t.totalMs,
179
+ desc: t.nodeStreams ? 'SSR (Node streams)' : 'SSR (Web Streams)',
180
+ });
181
+ }
182
+
183
+ // Signal promotion: check if any signals were captured during rendering
184
+ // inside Suspense boundaries. If no signals are present yet, yield one
185
+ // microtask so async component rejections propagate to the RSC onError
186
+ // callback before we commit the response.
163
187
  //
164
- // Uses queueMicrotask instead of setTimeout(0) to avoid yielding to
165
- // the full event loop (timers phase). Microtask resolution happens
166
- // within the same tick, eliminating per-request idle time under load.
188
+ // When signals are already captured (onSignal already fired), skip the
189
+ // yield entirely react immediately. Uses queueMicrotask instead of
190
+ // setTimeout(0) for the fallback to avoid yielding to the full event
191
+ // loop (timers phase).
167
192
  // See design/05-streaming.md §"deferSuspenseFor and the Hold Window"
168
- await new Promise<void>((r) => queueMicrotask(r));
193
+ if (!signals.redirectSignal && !signals.denySignal && !signals.renderError) {
194
+ await new Promise<void>((r) => queueMicrotask(r));
195
+ }
169
196
 
170
197
  const promoted = checkCapturedSignals(/* skipHandledDeny */ true);
171
198
  if (promoted) {