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

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 (334) 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/build-manifest.d.ts.map +1 -1
  135. package/dist/server/debug.d.ts +1 -1
  136. package/dist/server/default-logger.d.ts +22 -0
  137. package/dist/server/default-logger.d.ts.map +1 -0
  138. package/dist/server/deny-renderer.d.ts.map +1 -1
  139. package/dist/server/early-hints.d.ts +13 -5
  140. package/dist/server/early-hints.d.ts.map +1 -1
  141. package/dist/server/error-boundary-wrapper.d.ts +4 -0
  142. package/dist/server/error-boundary-wrapper.d.ts.map +1 -1
  143. package/dist/server/flight-injection-state.d.ts +66 -0
  144. package/dist/server/flight-injection-state.d.ts.map +1 -0
  145. package/dist/server/flight-scripts.d.ts +39 -0
  146. package/dist/server/flight-scripts.d.ts.map +1 -0
  147. package/dist/server/flush.d.ts.map +1 -1
  148. package/dist/server/form-data.d.ts +29 -0
  149. package/dist/server/form-data.d.ts.map +1 -1
  150. package/dist/server/html-injectors.d.ts +51 -11
  151. package/dist/server/html-injectors.d.ts.map +1 -1
  152. package/dist/server/index.d.ts +4 -2
  153. package/dist/server/index.d.ts.map +1 -1
  154. package/dist/server/index.js +1974 -1648
  155. package/dist/server/index.js.map +1 -1
  156. package/dist/server/logger.d.ts +24 -7
  157. package/dist/server/logger.d.ts.map +1 -1
  158. package/dist/server/node-stream-transforms.d.ts +113 -0
  159. package/dist/server/node-stream-transforms.d.ts.map +1 -0
  160. package/dist/server/pipeline.d.ts +7 -4
  161. package/dist/server/pipeline.d.ts.map +1 -1
  162. package/dist/server/primitives.d.ts +30 -3
  163. package/dist/server/primitives.d.ts.map +1 -1
  164. package/dist/server/render-timeout.d.ts +51 -0
  165. package/dist/server/render-timeout.d.ts.map +1 -0
  166. package/dist/server/request-context.d.ts +65 -38
  167. package/dist/server/request-context.d.ts.map +1 -1
  168. package/dist/server/route-element-builder.d.ts +7 -0
  169. package/dist/server/route-element-builder.d.ts.map +1 -1
  170. package/dist/server/route-handler.d.ts.map +1 -1
  171. package/dist/server/route-matcher.d.ts +2 -2
  172. package/dist/server/route-matcher.d.ts.map +1 -1
  173. package/dist/server/rsc-entry/error-renderer.d.ts.map +1 -1
  174. package/dist/server/rsc-entry/helpers.d.ts +46 -3
  175. package/dist/server/rsc-entry/helpers.d.ts.map +1 -1
  176. package/dist/server/rsc-entry/index.d.ts +6 -1
  177. package/dist/server/rsc-entry/index.d.ts.map +1 -1
  178. package/dist/server/rsc-entry/rsc-payload.d.ts.map +1 -1
  179. package/dist/server/rsc-entry/rsc-stream.d.ts +9 -0
  180. package/dist/server/rsc-entry/rsc-stream.d.ts.map +1 -1
  181. package/dist/server/rsc-entry/ssr-renderer.d.ts.map +1 -1
  182. package/dist/server/slot-resolver.d.ts +1 -1
  183. package/dist/server/slot-resolver.d.ts.map +1 -1
  184. package/dist/server/ssr-entry.d.ts +22 -0
  185. package/dist/server/ssr-entry.d.ts.map +1 -1
  186. package/dist/server/ssr-render.d.ts +39 -21
  187. package/dist/server/ssr-render.d.ts.map +1 -1
  188. package/dist/server/ssr-wrappers.d.ts +50 -0
  189. package/dist/server/ssr-wrappers.d.ts.map +1 -0
  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 +40 -29
  215. package/src/cli.ts +0 -0
  216. package/src/client/browser-entry.ts +133 -93
  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/index.ts +280 -85
  235. package/src/params/define.ts +260 -0
  236. package/src/params/index.ts +28 -0
  237. package/src/plugins/adapter-build.ts +6 -0
  238. package/src/plugins/build-manifest.ts +11 -0
  239. package/src/plugins/client-chunks.ts +65 -0
  240. package/src/plugins/dev-error-overlay.ts +70 -1
  241. package/src/plugins/dev-server.ts +38 -4
  242. package/src/plugins/entries.ts +5 -7
  243. package/src/plugins/fonts.ts +93 -42
  244. package/src/plugins/routing.ts +40 -14
  245. package/src/plugins/server-bundle.ts +32 -1
  246. package/src/plugins/shims.ts +1 -1
  247. package/src/plugins/static-build.ts +8 -4
  248. package/src/routing/codegen.ts +109 -88
  249. package/src/routing/scanner.ts +55 -6
  250. package/src/routing/status-file-lint.ts +2 -1
  251. package/src/routing/types.ts +7 -4
  252. package/src/rsc-runtime/rsc.ts +2 -0
  253. package/src/rsc-runtime/ssr.ts +50 -0
  254. package/src/rsc-runtime/vendor-types.d.ts +7 -0
  255. package/src/search-params/codecs.ts +1 -1
  256. package/src/search-params/define.ts +518 -0
  257. package/src/search-params/index.ts +12 -18
  258. package/src/search-params/registry.ts +1 -1
  259. package/src/search-params/wrappers.ts +85 -0
  260. package/src/server/access-gate.tsx +40 -9
  261. package/src/server/action-client.ts +7 -1
  262. package/src/server/action-encryption.ts +144 -0
  263. package/src/server/action-handler.ts +19 -2
  264. package/src/server/als-registry.ts +18 -4
  265. package/src/server/build-manifest.ts +16 -5
  266. package/src/server/compress.ts +25 -7
  267. package/src/server/debug.ts +1 -1
  268. package/src/server/default-logger.ts +98 -0
  269. package/src/server/deny-renderer.ts +2 -1
  270. package/src/server/early-hints.ts +36 -15
  271. package/src/server/error-boundary-wrapper.ts +57 -14
  272. package/src/server/flight-injection-state.ts +113 -0
  273. package/src/server/flight-scripts.ts +59 -0
  274. package/src/server/flush.ts +2 -1
  275. package/src/server/form-data.ts +76 -0
  276. package/src/server/html-injectors.ts +261 -117
  277. package/src/server/index.ts +9 -4
  278. package/src/server/logger.ts +38 -35
  279. package/src/server/node-stream-transforms.ts +504 -0
  280. package/src/server/pipeline.ts +131 -39
  281. package/src/server/primitives.ts +47 -5
  282. package/src/server/render-timeout.ts +108 -0
  283. package/src/server/request-context.ts +119 -119
  284. package/src/server/route-element-builder.ts +106 -114
  285. package/src/server/route-handler.ts +2 -1
  286. package/src/server/route-matcher.ts +2 -2
  287. package/src/server/rsc-entry/error-renderer.ts +5 -3
  288. package/src/server/rsc-entry/helpers.ts +122 -3
  289. package/src/server/rsc-entry/index.ts +108 -43
  290. package/src/server/rsc-entry/rsc-payload.ts +52 -12
  291. package/src/server/rsc-entry/rsc-stream.ts +49 -12
  292. package/src/server/rsc-entry/ssr-renderer.ts +40 -13
  293. package/src/server/slot-resolver.ts +222 -217
  294. package/src/server/ssr-entry.ts +209 -30
  295. package/src/server/ssr-render.ts +289 -67
  296. package/src/server/ssr-wrappers.tsx +139 -0
  297. package/src/server/tracing.ts +23 -0
  298. package/src/server/tree-builder.ts +91 -57
  299. package/src/server/types.ts +1 -3
  300. package/src/server/version-skew.ts +104 -0
  301. package/src/server/waituntil-bridge.ts +4 -1
  302. package/src/shared/merge-search-params.ts +48 -0
  303. package/src/shims/navigation-client.ts +1 -1
  304. package/src/shims/navigation.ts +1 -1
  305. package/src/utils/state-machine.ts +111 -0
  306. package/dist/_chunks/als-registry-B7DbZ2hS.js.map +0 -1
  307. package/dist/_chunks/debug-gwlJkDuf.js.map +0 -1
  308. package/dist/_chunks/interception-BOoWmLUA.js.map +0 -1
  309. package/dist/_chunks/request-context-DIkVh_jG.js.map +0 -1
  310. package/dist/_chunks/ssr-data-MjmprTmO.js +0 -88
  311. package/dist/_chunks/ssr-data-MjmprTmO.js.map +0 -1
  312. package/dist/_chunks/use-cookie-DX-l1_5E.js +0 -91
  313. package/dist/_chunks/use-cookie-DX-l1_5E.js.map +0 -1
  314. package/dist/client/error-boundary.js.map +0 -1
  315. package/dist/cookies/index.js.map +0 -1
  316. package/dist/plugins/dynamic-transform.d.ts +0 -72
  317. package/dist/plugins/dynamic-transform.d.ts.map +0 -1
  318. package/dist/search-params/analyze.d.ts +0 -54
  319. package/dist/search-params/analyze.d.ts.map +0 -1
  320. package/dist/search-params/builtin-codecs.d.ts +0 -105
  321. package/dist/search-params/builtin-codecs.d.ts.map +0 -1
  322. package/dist/search-params/create.d.ts +0 -106
  323. package/dist/search-params/create.d.ts.map +0 -1
  324. package/dist/search-params/index.js.map +0 -1
  325. package/dist/server/prerender.d.ts +0 -77
  326. package/dist/server/prerender.d.ts.map +0 -1
  327. package/dist/server/response-cache.d.ts +0 -53
  328. package/dist/server/response-cache.d.ts.map +0 -1
  329. package/src/plugins/dynamic-transform.ts +0 -161
  330. package/src/search-params/analyze.ts +0 -192
  331. package/src/search-params/builtin-codecs.ts +0 -228
  332. package/src/search-params/create.ts +0 -321
  333. package/src/server/prerender.ts +0 -139
  334. package/src/server/response-cache.ts +0 -277
@@ -7,6 +7,179 @@
7
7
  * Design docs: 02-rendering-pipeline.md, 18-build-system.md §"Entry Files"
8
8
  */
9
9
 
10
+ // ─── Buffered Transform ──────────────────────────────────────────────────────
11
+
12
+ /**
13
+ * Options for the buffered transform stream.
14
+ */
15
+ export interface BufferedTransformOptions {
16
+ /**
17
+ * Flush synchronously once the buffer reaches this many bytes.
18
+ * Prevents unbounded memory growth for very large Fizz flushes.
19
+ * Default: Infinity (no byte limit — flush only on tick boundary).
20
+ */
21
+ readonly maxBufferByteLength?: number;
22
+ }
23
+
24
+ /**
25
+ * Buffer incoming chunks and coalesce them within a single event loop tick.
26
+ *
27
+ * React Fizz may emit multiple micro-chunks within a single flush (e.g.,
28
+ * opening tags, attributes, closing tags as separate writes). Without
29
+ * buffering, downstream transforms (especially flight injection) could
30
+ * see chunk boundaries in the middle of HTML tags or attribute values.
31
+ *
32
+ * This transform collects all chunks that arrive in the same tick and
33
+ * emits them as a single concatenated chunk on the next `setImmediate`.
34
+ * This ensures each output chunk represents a coherent HTML fragment
35
+ * from a single Fizz flush — safe for downstream script injection at
36
+ * chunk boundaries.
37
+ *
38
+ * **Not a polling loop.** Uses a single-shot `setImmediate` per flush
39
+ * cycle — no recursive scheduling, no busy-wait. See design/02 §"No Polling".
40
+ *
41
+ * Inspired by Next.js `createBufferedTransformStream`.
42
+ */
43
+ export function createBufferedTransformStream(
44
+ options: BufferedTransformOptions = {}
45
+ ): TransformStream<Uint8Array, Uint8Array> {
46
+ const { maxBufferByteLength = Infinity } = options;
47
+
48
+ let bufferedChunks: Uint8Array[] = [];
49
+ let bufferByteLength = 0;
50
+ let pendingFlush: Promise<void> | undefined;
51
+
52
+ const flush = (controller: TransformStreamDefaultController<Uint8Array>) => {
53
+ if (bufferedChunks.length === 0) return;
54
+
55
+ // Concatenate all buffered chunks into a single output chunk
56
+ const merged = new Uint8Array(bufferByteLength);
57
+ let offset = 0;
58
+ for (const chunk of bufferedChunks) {
59
+ merged.set(chunk, offset);
60
+ offset += chunk.byteLength;
61
+ }
62
+
63
+ bufferedChunks = [];
64
+ bufferByteLength = 0;
65
+
66
+ try {
67
+ controller.enqueue(merged);
68
+ } catch {
69
+ // Controller may be errored (e.g., stream cancelled) — ignore
70
+ }
71
+ };
72
+
73
+ const scheduleFlush = (controller: TransformStreamDefaultController<Uint8Array>) => {
74
+ if (pendingFlush) return;
75
+
76
+ // Single-shot setImmediate — fires once at the end of the current
77
+ // event loop iteration (check phase), then the promise resolves.
78
+ // NOT a recursive loop — no CPU spin risk.
79
+ pendingFlush = new Promise<void>((resolve) => {
80
+ setImmediate(() => {
81
+ try {
82
+ flush(controller);
83
+ } finally {
84
+ pendingFlush = undefined;
85
+ resolve();
86
+ }
87
+ });
88
+ });
89
+ };
90
+
91
+ return new TransformStream<Uint8Array, Uint8Array>({
92
+ transform(chunk, controller) {
93
+ bufferedChunks.push(chunk);
94
+ bufferByteLength += chunk.byteLength;
95
+
96
+ if (bufferByteLength >= maxBufferByteLength) {
97
+ // Synchronous flush — buffer is too large to hold
98
+ flush(controller);
99
+ } else {
100
+ // Schedule a deferred flush for end of this tick
101
+ scheduleFlush(controller);
102
+ }
103
+ },
104
+ flush() {
105
+ // Wait for any pending scheduled flush to complete
106
+ return pendingFlush;
107
+ },
108
+ });
109
+ }
110
+
111
+ // ─── Move Suffix Transform ───────────────────────────────────────────────────
112
+
113
+ const SUFFIX = '</body></html>';
114
+
115
+ /**
116
+ * Move `</body></html>` to the end of the stream.
117
+ *
118
+ * React's renderToReadableStream emits `</body></html>` as part of the
119
+ * shell chunk. Content that arrives after the shell (Suspense resolutions,
120
+ * RSC script tags) would appear after the closing tags, producing invalid
121
+ * HTML like `</body></html><script>...</script>`.
122
+ *
123
+ * This transform strips the suffix when first encountered and re-emits
124
+ * it in `flush()` — after all content has passed through. If no suffix
125
+ * is found, it's appended anyway to ensure well-formed HTML.
126
+ *
127
+ * Equivalent to Next.js's `createMoveSuffixStream`.
128
+ */
129
+ export function createMoveSuffixStream(): TransformStream<Uint8Array, Uint8Array> {
130
+ const encoder = new TextEncoder();
131
+ const suffixBytes = encoder.encode(SUFFIX);
132
+ let foundSuffix = false;
133
+
134
+ return new TransformStream<Uint8Array, Uint8Array>({
135
+ transform(chunk, controller) {
136
+ if (foundSuffix) {
137
+ controller.enqueue(chunk);
138
+ return;
139
+ }
140
+
141
+ // Search for the suffix in this chunk
142
+ const text = new TextDecoder().decode(chunk, { stream: true });
143
+ const idx = text.indexOf(SUFFIX);
144
+ if (idx === -1) {
145
+ controller.enqueue(chunk);
146
+ return;
147
+ }
148
+
149
+ foundSuffix = true;
150
+
151
+ // If the entire chunk is exactly the suffix, skip it
152
+ if (chunk.byteLength === suffixBytes.byteLength) return;
153
+
154
+ // Emit content before the suffix
155
+ const before = text.slice(0, idx);
156
+ const after = text.slice(idx + SUFFIX.length);
157
+ if (before) controller.enqueue(encoder.encode(before));
158
+ // Emit content after the suffix (shouldn't normally exist,
159
+ // but handle it for robustness)
160
+ if (after) controller.enqueue(encoder.encode(after));
161
+ },
162
+ flush(controller) {
163
+ // Always emit the suffix at the very end — even if we didn't
164
+ // find it in the input (ensures well-formed HTML).
165
+ controller.enqueue(suffixBytes);
166
+ },
167
+ });
168
+ }
169
+
170
+ // ─── Injection Transforms ────────────────────────────────────────────────────
171
+
172
+ import { createMachine } from '../utils/state-machine.js';
173
+ import { flightChunkScript } from './flight-scripts.js';
174
+ import {
175
+ flightInjectionTransitions,
176
+ isHtmlDone,
177
+ isPullDone,
178
+ type FlightInjectionState,
179
+ type FlightInjectionEvent,
180
+ } from './flight-injection-state.js';
181
+ import { withTimeout, RenderTimeoutError } from './render-timeout.js';
182
+
10
183
  /**
11
184
  * Inject HTML content before a closing tag in the stream.
12
185
  *
@@ -95,22 +268,6 @@ export function injectScripts(
95
268
  return createInjector(stream, scriptsHtml, '</body>');
96
269
  }
97
270
 
98
- /**
99
- * Escape a string for safe embedding inside a `<script>` tag within
100
- * a JSON-encoded value.
101
- *
102
- * Only needs to prevent `</script>` from closing the tag early and
103
- * handle U+2028/U+2029 (line/paragraph separators valid in JSON but
104
- * historically problematic in JS). Since we use JSON.stringify for the
105
- * outer encoding, we only escape `<` and the line separators.
106
- */
107
- function htmlEscapeJsonString(str: string): string {
108
- return str
109
- .replace(/</g, '\\u003c')
110
- .replace(/\u2028/g, '\\u2028')
111
- .replace(/\u2029/g, '\\u2029');
112
- }
113
-
114
271
  /**
115
272
  * Transform an RSC Flight stream into a stream of inline `<script>` tags.
116
273
  *
@@ -118,42 +275,42 @@ function htmlEscapeJsonString(str: string): string {
118
275
  * transform) drives reads from the RSC stream on demand. No background
119
276
  * reader, no shared mutable arrays, no race conditions.
120
277
  *
121
- * Each RSC chunk becomes:
122
- * <script>(self.__timber_f=self.__timber_f||[]).push([1,"escaped_chunk"])</script>
123
- *
124
- * The first chunk emitted is the bootstrap signal [0] which the client
125
- * uses to initialize its buffer.
126
- *
127
- * Uses JSON-encoded typed tuples matching the pattern from Next.js:
128
- * [0] — bootstrap signal
129
- * [1, data] — RSC Flight data chunk (UTF-8 string)
278
+ * Each RSC chunk becomes a `<script>self.__timber_f.push([1,"data"])</script>`.
279
+ * The init script (which creates __timber_f) is in `<head>` via
280
+ * flightInitScript() — see flight-scripts.ts.
130
281
  */
131
282
  export function createInlinedRscStream(
132
- rscStream: ReadableStream<Uint8Array>
283
+ rscStream: ReadableStream<Uint8Array>,
284
+ renderTimeoutMs?: number
133
285
  ): ReadableStream<Uint8Array> {
134
286
  const encoder = new TextEncoder();
135
287
  const rscReader = rscStream.getReader();
136
288
  const decoder = new TextDecoder('utf-8', { fatal: true });
289
+ const timeoutMs = renderTimeoutMs ?? 30_000;
137
290
 
138
291
  return new ReadableStream<Uint8Array>({
139
- start(controller) {
140
- // Emit bootstrap signal tells the client that __timber_f is active
141
- const bootstrap = `<script>(self.__timber_f=self.__timber_f||[]).push(${htmlEscapeJsonString(JSON.stringify([0]))})</script>`;
142
- controller.enqueue(encoder.encode(bootstrap));
143
- },
292
+ // No bootstrap signal here — the init script is in <head> via
293
+ // flightInitScript() (see flight-scripts.ts). This ensures the
294
+ // __timber_f array exists before any chunk scripts execute.
144
295
  async pull(controller) {
145
296
  try {
146
- const { done, value } = await rscReader.read();
297
+ const readPromise = rscReader.read();
298
+ const { done, value } =
299
+ timeoutMs > 0
300
+ ? await withTimeout(readPromise, timeoutMs, 'RSC stream read timed out')
301
+ : await readPromise;
147
302
  if (done) {
148
303
  controller.close();
149
304
  return;
150
305
  }
151
306
  if (value) {
152
307
  const decoded = decoder.decode(value, { stream: true });
153
- const escaped = htmlEscapeJsonString(JSON.stringify([1, decoded]));
154
- controller.enqueue(encoder.encode(`<script>self.__timber_f.push(${escaped})</script>`));
308
+ controller.enqueue(encoder.encode(flightChunkScript(decoded)));
155
309
  }
156
310
  } catch (error) {
311
+ if (error instanceof RenderTimeoutError) {
312
+ rscReader.cancel(error).catch(() => {});
313
+ }
157
314
  controller.error(error);
158
315
  }
159
316
  },
@@ -162,67 +319,70 @@ export function createInlinedRscStream(
162
319
 
163
320
  /**
164
321
  * Merge RSC script stream into the HTML stream, injecting scripts
165
- * only as direct children of `<body>`.
322
+ * between HTML chunks at safe boundaries.
166
323
  *
167
- * This single transform replaces the previous two-stage pipeline
168
- * (createFlightInjectionTransform + createMoveSuffixStream). It:
324
+ * Suffix stripping (</body></html>) is handled upstream by
325
+ * createMoveSuffixStream. This transform only interleaves RSC
326
+ * scripts — no suffix tracking needed.
169
327
  *
170
- * 1. Strips `</body></html>` from the shell chunk so all subsequent
171
- * content is at the `<body>` level.
172
- * 2. Buffers RSC `<script>` tags and drains them after the suffix
173
- * has been stripped — guaranteeing body-level injection.
174
- * 3. Re-emits `</body></html>` at the very end after all RSC scripts.
328
+ * RSC scripts are buffered in pending[] by pullLoop and only
329
+ * drained from transform() (after a complete HTML chunk) or
330
+ * flush(). Never pushed directly from pullLoop to avoid mid-tag
331
+ * injection (TIM-527).
175
332
  *
176
- * Because the suffix is stripped before any scripts are injected,
177
- * scripts are always direct children of `<body>` regardless of how
178
- * React's renderToReadableStream chunks the HTML. No HTML structure
179
- * scanning or depth tracking needed — the suffix removal is the
180
- * structural guarantee.
333
+ * State machine phases:
334
+ * init streaming flushing done
335
+ * (any) error
181
336
  *
182
337
  * Inspired by Next.js createFlightDataInjectionTransformStream.
183
338
  */
184
339
  function createFlightInjectionTransform(
185
- rscScriptStream: ReadableStream<Uint8Array>
340
+ rscScriptStream: ReadableStream<Uint8Array>,
341
+ renderTimeoutMs?: number
186
342
  ): TransformStream<Uint8Array, Uint8Array> {
187
- const encoder = new TextEncoder();
188
- const decoder = new TextDecoder();
189
- const suffix = '</body></html>';
190
- const suffixBytes = encoder.encode(suffix);
191
-
192
343
  const rscReader = rscScriptStream.getReader();
344
+ const timeoutMs = renderTimeoutMs ?? 30_000;
345
+
346
+ const machine = createMachine<FlightInjectionState, FlightInjectionEvent>({
347
+ initial: { phase: 'init' },
348
+ transitions: flightInjectionTransitions,
349
+ });
350
+
193
351
  let pullPromise: Promise<void> | null = null;
194
- let donePulling = false;
195
- let pullError: unknown = null;
196
- // Once the suffix is stripped, all content is body-level and
197
- // scripts can safely be drained after any HTML chunk.
198
- let foundSuffix = false;
199
352
 
200
- // RSC script chunks waiting to be injected at the body level.
353
+ // RSC script chunks waiting to be drained at a safe boundary.
201
354
  const pending: Uint8Array[] = [];
202
355
 
203
356
  async function pullLoop(): Promise<void> {
204
- // Wait one macrotask so the HTML shell chunk flows through
205
- // transform() first. The browser needs the shell HTML before
206
- // RSC data script tags arrive.
207
- await new Promise<void>((r) => setTimeout(r, 0));
357
+ // Yield once so the first HTML shell chunk flows through
358
+ // transform() before we start reading RSC data.
359
+ await new Promise<void>((r) => setImmediate(r));
208
360
 
209
361
  try {
210
362
  for (;;) {
211
- const { done, value } = await rscReader.read();
363
+ // Guard each RSC read with a timeout.
364
+ // See design/02 §"Streaming Constraints".
365
+ const readPromise = rscReader.read();
366
+ const { done, value } =
367
+ timeoutMs > 0
368
+ ? await withTimeout(readPromise, timeoutMs, 'RSC stream read timed out')
369
+ : await readPromise;
212
370
  if (done) {
213
- donePulling = true;
371
+ machine.send({ type: 'PULL_DONE' });
214
372
  return;
215
373
  }
216
374
  pending.push(value);
217
- // Yield between reads so HTML chunks get a chance to flow
218
- // through transform() first. RSC and HTML are driven by the
219
- // same source — each RSC chunk typically produces a
220
- // corresponding HTML chunk from SSR.
221
- await new Promise<void>((r) => setTimeout(r, 0));
375
+ // Yield between reads so HTML chunks get priority.
376
+ // Once flush() fires, drain without yielding.
377
+ if (!isHtmlDone(machine.state)) {
378
+ await new Promise<void>((r) => setImmediate(r));
379
+ }
222
380
  }
223
381
  } catch (err) {
224
- pullError = err;
225
- donePulling = true;
382
+ if (err instanceof RenderTimeoutError) {
383
+ rscReader.cancel(err).catch(() => {});
384
+ }
385
+ machine.send({ type: 'PULL_ERROR', error: err });
226
386
  }
227
387
  }
228
388
 
@@ -231,59 +391,34 @@ function createFlightInjectionTransform(
231
391
  while (pending.length > 0) {
232
392
  controller.enqueue(pending.shift()!);
233
393
  }
234
- if (pullError) {
235
- const err = pullError;
236
- pullError = null;
237
- controller.error(err);
394
+ if (machine.state.phase === 'error') {
395
+ controller.error(machine.state.error);
238
396
  }
239
397
  }
240
398
 
241
399
  return new TransformStream<Uint8Array, Uint8Array>({
242
400
  transform(chunk, controller) {
243
- // Start pulling RSC scripts into the buffer (if not started)
244
- if (!pullPromise) {
401
+ if (machine.state.phase === 'init') {
402
+ machine.send({ type: 'FIRST_CHUNK' });
245
403
  pullPromise = pullLoop();
246
404
  }
247
405
 
248
- if (foundSuffix) {
249
- // Post-suffix: everything is body-level (Suspense chunks).
250
- // Emit HTML, then drain any buffered scripts.
251
- controller.enqueue(chunk);
252
- if (pending.length > 0) drainPending(controller);
253
- return;
254
- }
255
-
256
- // Look for </body></html> in the shell chunk.
257
- const text = decoder.decode(chunk, { stream: true });
258
- const idx = text.indexOf(suffix);
259
- if (idx !== -1) {
260
- foundSuffix = true;
261
- // Emit everything before the suffix (still inside <body>'s
262
- // child elements — don't inject scripts here).
263
- const before = text.slice(0, idx);
264
- const after = text.slice(idx + suffix.length);
265
- if (before) controller.enqueue(encoder.encode(before));
266
- // Now we're at body level — drain buffered scripts
267
- if (pending.length > 0) drainPending(controller);
268
- // Emit any content after the suffix (shouldn't normally exist)
269
- if (after) controller.enqueue(encoder.encode(after));
270
- } else {
271
- // Pre-suffix: inside nested elements. Pass through, don't
272
- // inject scripts (they'd become children of nested elements).
273
- controller.enqueue(chunk);
274
- }
406
+ // Emit the HTML chunk, then drain any buffered RSC scripts.
407
+ // Scripts always come AFTER a complete HTML chunk — never mid-tag.
408
+ // The buffered transform upstream (TIM-528) ensures coherent chunks.
409
+ // Suffix stripping is upstream via createMoveSuffixStream (TIM-530).
410
+ controller.enqueue(chunk);
411
+ if (pending.length > 0) drainPending(controller);
275
412
  },
276
413
  flush(controller) {
277
- // HTML stream is done drain remaining RSC chunks at body level
414
+ // All HTML chunks emitted. Pull loop stops yielding.
415
+ machine.send({ type: 'HTML_DONE' });
416
+
278
417
  const finish = () => {
279
418
  drainPending(controller);
280
- // Re-emit the suffix at the very end so HTML is well-formed
281
- if (foundSuffix) {
282
- controller.enqueue(suffixBytes);
283
- }
284
419
  };
285
420
 
286
- if (donePulling) {
421
+ if (isPullDone(machine.state)) {
287
422
  finish();
288
423
  return;
289
424
  }
@@ -314,16 +449,25 @@ function createFlightInjectionTransform(
314
449
  */
315
450
  export function injectRscPayload(
316
451
  htmlStream: ReadableStream<Uint8Array>,
317
- rscStream: ReadableStream<Uint8Array> | undefined
452
+ rscStream: ReadableStream<Uint8Array> | undefined,
453
+ renderTimeoutMs?: number
318
454
  ): ReadableStream<Uint8Array> {
319
455
  if (!rscStream) return htmlStream;
320
456
 
321
457
  // Transform RSC binary stream → stream of <script> tags
322
- const rscScriptStream = createInlinedRscStream(rscStream);
323
-
324
- // Single transform: strip </body></html>, inject RSC scripts at
325
- // body level, re-emit suffix at the very end.
326
- return htmlStream.pipeThrough(createFlightInjectionTransform(rscScriptStream));
458
+ const rscScriptStream = createInlinedRscStream(rscStream, renderTimeoutMs);
459
+
460
+ // Pipeline: flightInjection moveSuffix
461
+ //
462
+ // 1. flightInjection interleaves RSC scripts between HTML chunks
463
+ // 2. moveSuffix strips </body></html> and re-emits at end
464
+ //
465
+ // The flight injector is upstream and interleaves scripts between
466
+ // HTML chunks. The moveSuffix transform then ensures </body></html>
467
+ // appears after all injected scripts, producing well-formed HTML.
468
+ return htmlStream
469
+ .pipeThrough(createFlightInjectionTransform(rscScriptStream, renderTimeoutMs))
470
+ .pipeThrough(createMoveSuffixStream());
327
471
  }
328
472
 
329
473
  /**
@@ -6,19 +6,19 @@ export type { MiddlewareContext } from './types';
6
6
  export type { RouteContext } from './types';
7
7
  export type { Metadata, MetadataRoute } from './types';
8
8
 
9
- // Request Context — ALS-backed headers(), cookies(), and searchParams()
9
+ // Request Context — ALS-backed headers(), cookies(), and rawSearchParams()
10
10
  // Design doc: design/04-authorization.md §"AccessContext does not include cookies or headers"
11
11
  // Design doc: design/23-search-params.md §"Server Integration"
12
12
  export {
13
13
  headers,
14
14
  cookies,
15
- searchParams,
16
- setParsedSearchParams,
15
+ rawSearchParams,
16
+ rawSegmentParams,
17
+ setSegmentParams,
17
18
  runWithRequestContext,
18
19
  setMutableCookieContext,
19
20
  markResponseFlushed,
20
21
  getSetCookieHeaders,
21
- setCookieSecrets,
22
22
  } from './request-context';
23
23
  export type { ReadonlyHeaders, RequestCookies, CookieOptions } from './request-context';
24
24
 
@@ -34,6 +34,7 @@ export {
34
34
  waitUntil,
35
35
  DenySignal,
36
36
  RedirectSignal,
37
+ type RedirectOptions,
37
38
  } from './primitives';
38
39
  export type { RenderErrorDigest, WaitUntilAdapter } from './primitives';
39
40
  export type { JsonSerializable } from './types';
@@ -221,3 +222,7 @@ export type { DevWarningConfig } from './dev-warnings';
221
222
  // Design doc: design/07-routing.md §"route.ts — API Endpoints"
222
223
  export { handleRouteRequest, resolveAllowedMethods } from './route-handler';
223
224
  export type { RouteModule, RouteHandler, HttpMethod } from './route-handler';
225
+
226
+ // Render timeout — design doc: 02-rendering-pipeline.md §"Streaming Constraints"
227
+ export { RenderTimeoutError } from './render-timeout';
228
+ export type { RenderTimeout } from './render-timeout';
@@ -1,16 +1,15 @@
1
1
  /**
2
2
  * Logger — structured logging with environment-aware formatting.
3
3
  *
4
- * timber.js does not ship a logger. Users export any object with
5
- * info/warn/error/debug methods from instrumentation.ts and the framework
6
- * picks it up. Silent if no logger export is present.
4
+ * timber.js ships a DefaultLogger that writes human-readable lines to stderr
5
+ * in production. Users can export a custom logger from instrumentation.ts to
6
+ * replace it with pino, winston, or any TimberLogger-compatible object.
7
7
  *
8
8
  * See design/17-logging.md §"Production Logging"
9
9
  */
10
10
 
11
11
  import { getTraceStore } from './tracing.js';
12
- import { formatSsrError } from './error-formatter.js';
13
- import { isDebug } from './debug.js';
12
+ import { createDefaultLogger } from './default-logger.js';
14
13
 
15
14
  // ─── Logger Interface ─────────────────────────────────────────────────────
16
15
 
@@ -24,21 +23,24 @@ export interface TimberLogger {
24
23
 
25
24
  // ─── Logger Registry ──────────────────────────────────────────────────────
26
25
 
27
- let _logger: TimberLogger | null = null;
26
+ // Initialize with DefaultLogger so production errors are never silent.
27
+ // Replaced when setLogger() is called from instrumentation.ts.
28
+ let _logger: TimberLogger = createDefaultLogger();
28
29
 
29
30
  /**
30
31
  * Set the user-provided logger. Called by the instrumentation loader
31
- * when it finds a `logger` export in instrumentation.ts.
32
+ * when it finds a `logger` export in instrumentation.ts. Replaces
33
+ * the DefaultLogger entirely.
32
34
  */
33
35
  export function setLogger(logger: TimberLogger): void {
34
36
  _logger = logger;
35
37
  }
36
38
 
37
39
  /**
38
- * Get the current logger, or null if none configured.
39
- * Framework-internal used at framework event points to emit structured logs.
40
+ * Get the current logger. Always non-null returns DefaultLogger when
41
+ * no custom logger is configured.
40
42
  */
41
- export function getLogger(): TimberLogger | null {
43
+ export function getLogger(): TimberLogger {
42
44
  return _logger;
43
45
  }
44
46
 
@@ -71,12 +73,12 @@ export function logRequestCompleted(data: {
71
73
  /** Number of concurrent in-flight requests (including this one) at completion time. */
72
74
  concurrency?: number;
73
75
  }): void {
74
- _logger?.info('request completed', withTraceContext(data));
76
+ _logger.info('request completed', withTraceContext(data));
75
77
  }
76
78
 
77
79
  /** Log request received. Level: debug. */
78
80
  export function logRequestReceived(data: { method: string; path: string }): void {
79
- _logger?.debug('request received', withTraceContext(data));
81
+ _logger.debug('request received', withTraceContext(data));
80
82
  }
81
83
 
82
84
  /** Log a slow request warning. Level: warn. */
@@ -88,7 +90,7 @@ export function logSlowRequest(data: {
88
90
  /** Number of concurrent in-flight requests at the time the slow request completed. */
89
91
  concurrency?: number;
90
92
  }): void {
91
- _logger?.warn('slow request exceeded threshold', withTraceContext(data));
93
+ _logger.warn('slow request exceeded threshold', withTraceContext(data));
92
94
  }
93
95
 
94
96
  /** Log middleware short-circuit. Level: debug. */
@@ -97,54 +99,55 @@ export function logMiddlewareShortCircuit(data: {
97
99
  path: string;
98
100
  status: number;
99
101
  }): void {
100
- _logger?.debug('middleware short-circuited', withTraceContext(data));
102
+ _logger.debug('middleware short-circuited', withTraceContext(data));
101
103
  }
102
104
 
103
105
  /** Log unhandled error in middleware phase. Level: error. */
104
106
  export function logMiddlewareError(data: { method: string; path: string; error: unknown }): void {
105
- if (_logger) {
106
- _logger.error('unhandled error in middleware phase', withTraceContext(data));
107
- } else if (isDebug()) {
108
- console.error('[timber] middleware error', data.error);
109
- }
107
+ _logger.error('unhandled error in middleware phase', withTraceContext(data));
110
108
  }
111
109
 
112
110
  /** Log unhandled render-phase error. Level: error. */
113
111
  export function logRenderError(data: { method: string; path: string; error: unknown }): void {
114
- if (_logger) {
115
- _logger.error('unhandled render-phase error', withTraceContext(data));
116
- } else if (isDebug()) {
117
- // No logger configured — fall back to console.error in dev with
118
- // cleaned-up error messages (vendor paths rewritten, hints added).
119
- console.error('[timber] render error:', formatSsrError(data.error));
120
- }
112
+ _logger.error('unhandled render-phase error', withTraceContext(data));
121
113
  }
122
114
 
123
115
  /** Log proxy.ts uncaught error. Level: error. */
124
116
  export function logProxyError(data: { error: unknown }): void {
125
- if (_logger) {
126
- _logger.error('proxy.ts threw uncaught error', withTraceContext(data));
127
- } else if (isDebug()) {
128
- console.error('[timber] proxy error', data.error);
129
- }
117
+ _logger.error('proxy.ts threw uncaught error', withTraceContext(data));
118
+ }
119
+
120
+ /** Log unhandled error in server action. Level: error. */
121
+ export function logActionError(data: { method: string; path: string; error: unknown }): void {
122
+ _logger.error('unhandled server action error', withTraceContext(data));
123
+ }
124
+
125
+ /** Log unhandled error in route handler. Level: error. */
126
+ export function logRouteError(data: { method: string; path: string; error: unknown }): void {
127
+ _logger.error('unhandled route handler error', withTraceContext(data));
128
+ }
129
+
130
+ /** Log SSR streaming error (post-shell). Level: error. */
131
+ export function logStreamingError(data: { error: unknown }): void {
132
+ _logger.error('SSR streaming error (post-shell)', withTraceContext(data));
130
133
  }
131
134
 
132
135
  /** Log waitUntil() adapter missing (once at startup). Level: warn. */
133
136
  export function logWaitUntilUnsupported(): void {
134
- _logger?.warn('adapter does not support waitUntil()');
137
+ _logger.warn('adapter does not support waitUntil()');
135
138
  }
136
139
 
137
140
  /** Log waitUntil() promise rejection. Level: warn. */
138
141
  export function logWaitUntilRejected(data: { error: unknown }): void {
139
- _logger?.warn('waitUntil() promise rejected', withTraceContext(data));
142
+ _logger.warn('waitUntil() promise rejected', withTraceContext(data));
140
143
  }
141
144
 
142
145
  /** Log staleWhileRevalidate refetch failure. Level: warn. */
143
146
  export function logSwrRefetchFailed(data: { cacheKey: string; error: unknown }): void {
144
- _logger?.warn('staleWhileRevalidate refetch failed', withTraceContext(data));
147
+ _logger.warn('staleWhileRevalidate refetch failed', withTraceContext(data));
145
148
  }
146
149
 
147
150
  /** Log cache miss. Level: debug. */
148
151
  export function logCacheMiss(data: { cacheKey: string }): void {
149
- _logger?.debug('timber.cache MISS', withTraceContext(data));
152
+ _logger.debug('timber.cache MISS', withTraceContext(data));
150
153
  }