@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
@@ -0,0 +1,504 @@
1
+ /**
2
+ * Node.js native stream transforms for SSR HTML post-processing.
3
+ *
4
+ * These are Node.js Transform stream equivalents of the Web Stream
5
+ * transforms in html-injectors.ts. Used on Node.js/Bun where native
6
+ * streams (C++ backed) are faster than Web Streams (JS reimplementation).
7
+ *
8
+ * The transforms are pure string operations on HTML chunks — the same
9
+ * logic as the Web Stream versions, just wrapped in Node.js Transform
10
+ * instead of Web TransformStream.
11
+ *
12
+ * Architecture:
13
+ * renderToPipeableStream → pipe(errorHandler) → pipe(headInjector)
14
+ * → pipe(flightInjector) → Readable.toWeb() → Response
15
+ *
16
+ * All chunks stay in C++ Node.js stream buffers until the final
17
+ * Readable.toWeb() conversion for the Response body.
18
+ */
19
+
20
+ import { Transform } from 'node:stream';
21
+ import { createGzip, constants } from 'node:zlib';
22
+
23
+ // ─── Buffered Transform ──────────────────────────────────────────────────────
24
+
25
+ /**
26
+ * Options for the Node.js buffered transform.
27
+ */
28
+ export interface NodeBufferedTransformOptions {
29
+ /**
30
+ * Flush synchronously once the buffer reaches this many bytes.
31
+ * Prevents unbounded memory growth for very large Fizz flushes.
32
+ * Default: Infinity (no byte limit — flush only on tick boundary).
33
+ */
34
+ readonly maxBufferByteLength?: number;
35
+ }
36
+
37
+ /**
38
+ * Node.js Transform that buffers incoming chunks and coalesces them
39
+ * within a single event loop tick.
40
+ *
41
+ * Equivalent to createBufferedTransformStream() in html-injectors.ts.
42
+ * React Fizz may emit multiple micro-chunks within a single flush.
43
+ * Without buffering, downstream transforms (especially flight injection)
44
+ * could see chunk boundaries in the middle of HTML tags or attributes.
45
+ *
46
+ * This transform collects all chunks that arrive in the same tick and
47
+ * emits them as a single concatenated Buffer on the next `setImmediate`.
48
+ *
49
+ * **Not a polling loop.** Uses a single-shot `setImmediate` per flush
50
+ * cycle — no recursive scheduling, no busy-wait. See design/02 §"No Polling".
51
+ *
52
+ * Inspired by Next.js `createBufferedTransformStream`.
53
+ */
54
+ export function createNodeBufferedTransform(options: NodeBufferedTransformOptions = {}): Transform {
55
+ const { maxBufferByteLength = Infinity } = options;
56
+
57
+ let bufferedChunks: Buffer[] = [];
58
+ let bufferByteLength = 0;
59
+ let pendingImmediate: ReturnType<typeof setImmediate> | null = null;
60
+
61
+ const transform = new Transform({
62
+ transform(chunk: Buffer, _encoding, callback) {
63
+ bufferedChunks.push(chunk);
64
+ bufferByteLength += chunk.byteLength;
65
+
66
+ if (bufferByteLength >= maxBufferByteLength) {
67
+ // Synchronous flush — buffer is too large to hold
68
+ flushBuffer();
69
+ } else if (!pendingImmediate) {
70
+ // Schedule a deferred flush for end of this tick.
71
+ // Single-shot setImmediate — NOT a recursive loop.
72
+ // See design/02 §"No Polling".
73
+ pendingImmediate = setImmediate(() => {
74
+ pendingImmediate = null;
75
+ flushBuffer();
76
+ });
77
+ }
78
+
79
+ callback();
80
+ },
81
+ flush(callback) {
82
+ // Cancel any pending deferred flush and flush synchronously
83
+ if (pendingImmediate) {
84
+ clearImmediate(pendingImmediate);
85
+ pendingImmediate = null;
86
+ }
87
+ flushBuffer();
88
+ callback();
89
+ },
90
+ });
91
+
92
+ function flushBuffer() {
93
+ if (bufferedChunks.length === 0) return;
94
+
95
+ const merged = Buffer.concat(bufferedChunks, bufferByteLength);
96
+ bufferedChunks = [];
97
+ bufferByteLength = 0;
98
+ transform.push(merged);
99
+ }
100
+
101
+ return transform;
102
+ }
103
+
104
+ // ─── Injection Transforms ────────────────────────────────────────────────────
105
+
106
+ import { createMachine } from '../utils/state-machine.js';
107
+ import { flightChunkScript } from './flight-scripts.js';
108
+ import {
109
+ flightInjectionTransitions,
110
+ isHtmlDone,
111
+ isPullDone,
112
+ type FlightInjectionState,
113
+ type FlightInjectionEvent,
114
+ } from './flight-injection-state.js';
115
+ import { withTimeout, RenderTimeoutError } from './render-timeout.js';
116
+ import { logStreamingError } from './logger.js';
117
+
118
+ // ─── Move Suffix Transform ───────────────────────────────────────────────────
119
+
120
+ const SUFFIX = '</body></html>';
121
+ const SUFFIX_BUF = Buffer.from(SUFFIX, 'utf-8');
122
+
123
+ /**
124
+ * Node.js Transform that moves `</body></html>` to the end of the stream.
125
+ *
126
+ * Equivalent to createMoveSuffixStream() in html-injectors.ts.
127
+ * Strips the suffix when first encountered and re-emits it in flush().
128
+ * If no suffix is found, it's appended anyway for well-formed HTML.
129
+ */
130
+ export function createNodeMoveSuffixTransform(): Transform {
131
+ let foundSuffix = false;
132
+
133
+ return new Transform({
134
+ transform(chunk: Buffer, _encoding, callback) {
135
+ if (foundSuffix) {
136
+ this.push(chunk);
137
+ callback();
138
+ return;
139
+ }
140
+
141
+ const text = chunk.toString('utf-8');
142
+ const idx = text.indexOf(SUFFIX);
143
+ if (idx === -1) {
144
+ this.push(chunk);
145
+ callback();
146
+ return;
147
+ }
148
+
149
+ foundSuffix = true;
150
+
151
+ // If the entire chunk is exactly the suffix, skip it
152
+ if (chunk.byteLength === SUFFIX_BUF.byteLength) {
153
+ callback();
154
+ return;
155
+ }
156
+
157
+ // Emit content before the suffix
158
+ const before = text.slice(0, idx);
159
+ const after = text.slice(idx + SUFFIX.length);
160
+ if (before) this.push(Buffer.from(before, 'utf-8'));
161
+ if (after) this.push(Buffer.from(after, 'utf-8'));
162
+ callback();
163
+ },
164
+ flush(callback) {
165
+ // Always emit the suffix at the very end
166
+ this.push(SUFFIX_BUF);
167
+ callback();
168
+ },
169
+ });
170
+ }
171
+
172
+ // ─── Head Injection ──────────────────────────────────────────────────────────
173
+
174
+ /**
175
+ * Node.js Transform that injects HTML content before </head>.
176
+ *
177
+ * Equivalent to injectHead() in html-injectors.ts. Streams chunks
178
+ * through immediately, keeping only a small trailing buffer to handle
179
+ * </head> split across chunk boundaries.
180
+ */
181
+ export function createNodeHeadInjector(headHtml: string): Transform {
182
+ if (!headHtml) {
183
+ return new Transform({
184
+ transform(chunk, _enc, cb) {
185
+ cb(null, chunk);
186
+ },
187
+ });
188
+ }
189
+
190
+ const target = '</head>';
191
+ const tailLen = target.length - 1;
192
+ let injected = false;
193
+ let tail = '';
194
+
195
+ return new Transform({
196
+ transform(chunk: Buffer, _encoding, callback) {
197
+ if (injected) {
198
+ callback(null, chunk);
199
+ return;
200
+ }
201
+
202
+ const text = tail + chunk.toString('utf-8');
203
+ const tagIndex = text.indexOf(target);
204
+
205
+ if (tagIndex !== -1) {
206
+ const before = text.slice(0, tagIndex);
207
+ const after = text.slice(tagIndex);
208
+ this.push(Buffer.from(before + headHtml + after, 'utf-8'));
209
+ injected = true;
210
+ tail = '';
211
+ callback();
212
+ } else {
213
+ const safeEnd = Math.max(0, text.length - tailLen);
214
+ if (safeEnd > 0) {
215
+ this.push(Buffer.from(text.slice(0, safeEnd), 'utf-8'));
216
+ }
217
+ tail = text.slice(safeEnd);
218
+ callback();
219
+ }
220
+ },
221
+ flush(callback) {
222
+ if (!injected && tail) {
223
+ this.push(Buffer.from(tail, 'utf-8'));
224
+ }
225
+ callback();
226
+ },
227
+ });
228
+ }
229
+
230
+ // ─── RSC Flight Injection ────────────────────────────────────────────────────
231
+
232
+ /**
233
+ * Node.js Transform that merges RSC script tags into the HTML stream.
234
+ *
235
+ * Reads RSC chunks from the provided ReadableStream and injects them
236
+ * as `<script>` tags between HTML chunks. Scripts are buffered in
237
+ * pending[] and only drained from transform() (after a complete HTML
238
+ * chunk) or flush() — never pushed directly from the pull loop.
239
+ *
240
+ * Suffix stripping (</body></html>) is handled upstream by
241
+ * createNodeMoveSuffixTransform. This transform only interleaves
242
+ * RSC scripts at safe chunk boundaries.
243
+ *
244
+ * The RSC stream is a Web ReadableStream (from the tee'd RSC Flight
245
+ * stream). We read from it using the Web API — this is the one bridge
246
+ * point between Web Streams and Node.js streams in the pipeline.
247
+ */
248
+ /**
249
+ * Options for the Node.js flight injector.
250
+ */
251
+ export interface NodeFlightInjectorOptions {
252
+ /**
253
+ * Timeout in milliseconds for individual RSC stream reads.
254
+ * If a single `rscReader.read()` call does not resolve within
255
+ * this duration, the read is aborted and the stream errors with
256
+ * a RenderTimeoutError. Default: 30000 (30s).
257
+ */
258
+ renderTimeoutMs?: number;
259
+ }
260
+
261
+ export function createNodeFlightInjector(
262
+ rscStream: ReadableStream<Uint8Array> | undefined,
263
+ options?: NodeFlightInjectorOptions
264
+ ): Transform {
265
+ if (!rscStream) {
266
+ return new Transform({
267
+ transform(chunk, _enc, cb) {
268
+ cb(null, chunk);
269
+ },
270
+ });
271
+ }
272
+
273
+ const timeoutMs = options?.renderTimeoutMs ?? 30_000;
274
+ const rscReader = rscStream.getReader();
275
+ const decoder = new TextDecoder('utf-8', { fatal: true });
276
+
277
+ const machine = createMachine<FlightInjectionState, FlightInjectionEvent>({
278
+ initial: { phase: 'init' },
279
+ transitions: flightInjectionTransitions,
280
+ });
281
+
282
+ // Stored promise from pullLoop — awaited in flush() via .then()
283
+ // instead of polling. See design/02 §"No Polling".
284
+ let pullPromise: Promise<void> | null = null;
285
+
286
+ // RSC script chunks waiting to be drained at a safe boundary.
287
+ // pullLoop buffers here; transform() and flush() drain.
288
+ // Scripts are NEVER pushed directly from pullLoop — they are only
289
+ // emitted from transform() (after a complete HTML chunk) or flush().
290
+ // This guarantees scripts never land mid-tag. See TIM-527/TIM-529.
291
+ const pending: Buffer[] = [];
292
+
293
+ // pullLoop reads RSC chunks and buffers them as <script> tags in
294
+ // pending[]. It does NOT push directly to the transform output —
295
+ // that would cause scripts to interleave at arbitrary byte
296
+ // boundaries within HTML chunks (TIM-527). Pending scripts are
297
+ // drained only from transform() or flush().
298
+ async function pullLoop(): Promise<void> {
299
+ // Yield once so the first transform() call can process the shell
300
+ // HTML chunk before we start reading RSC data.
301
+ await new Promise<void>((r) => setImmediate(r));
302
+ try {
303
+ for (;;) {
304
+ // Guard each RSC read with a timeout so a permanently hung
305
+ // RSC stream eventually aborts instead of blocking forever.
306
+ // See design/02-rendering-pipeline.md §"Streaming Constraints".
307
+ const readPromise = rscReader.read();
308
+ const { done, value } =
309
+ timeoutMs > 0
310
+ ? await withTimeout(readPromise, timeoutMs, 'RSC stream read timed out')
311
+ : await readPromise;
312
+ if (done) {
313
+ machine.send({ type: 'PULL_DONE' });
314
+ return;
315
+ }
316
+ const decoded = decoder.decode(value, { stream: true });
317
+ const scriptBuf = Buffer.from(flightChunkScript(decoded), 'utf-8');
318
+ // Buffer the script — drained by the next transform() or flush().
319
+ pending.push(scriptBuf);
320
+ // Yield between reads so HTML chunks get a chance to flow
321
+ // through transform() first — but only while HTML is still
322
+ // streaming. Once flush() fires, drain without yielding.
323
+ if (!isHtmlDone(machine.state)) {
324
+ await new Promise<void>((r) => setImmediate(r));
325
+ }
326
+ }
327
+ } catch (err) {
328
+ // On timeout, cancel the RSC reader to release resources.
329
+ if (err instanceof RenderTimeoutError) {
330
+ rscReader.cancel(err).catch(() => {});
331
+ }
332
+ machine.send({ type: 'PULL_ERROR', error: err });
333
+ }
334
+ }
335
+
336
+ /** Drain all buffered RSC script chunks to the transform output. */
337
+ function drainPending(): void {
338
+ while (pending.length > 0) {
339
+ transform.push(pending.shift()!);
340
+ }
341
+ }
342
+
343
+ // No bootstrap script here — the init script is in <head> via
344
+ // flightInitScript() (see flight-scripts.ts). This ensures __timber_f
345
+ // exists before any chunk scripts execute.
346
+
347
+ const transform = new Transform({
348
+ transform(chunk: Buffer, _encoding, callback) {
349
+ const isFirst = machine.state.phase === 'init';
350
+ if (isFirst) {
351
+ machine.send({ type: 'FIRST_CHUNK' });
352
+ }
353
+
354
+ // Emit the HTML chunk, then drain any buffered RSC scripts.
355
+ // Scripts always come AFTER a complete HTML chunk — never mid-tag.
356
+ // The buffered transform upstream (TIM-528) ensures each chunk is
357
+ // a coherent HTML fragment. Suffix stripping is handled upstream
358
+ // by createNodeMoveSuffixTransform (TIM-530).
359
+ transform.push(chunk);
360
+ drainPending();
361
+
362
+ // Start the pull loop on the first HTML chunk.
363
+ if (isFirst) {
364
+ pullPromise = pullLoop();
365
+ }
366
+ callback();
367
+ },
368
+ flush(callback) {
369
+ // All HTML chunks have been emitted. Transition to flushing —
370
+ // the pull loop will stop yielding between RSC reads since
371
+ // isHtmlDone() now returns true.
372
+ machine.send({ type: 'HTML_DONE' });
373
+
374
+ const finish = () => {
375
+ // Drain any remaining buffered RSC scripts
376
+ drainPending();
377
+ if (machine.state.phase === 'error') {
378
+ const err = machine.state.error;
379
+ transform.destroy(err instanceof Error ? err : new Error(String(err)));
380
+ return;
381
+ }
382
+ callback();
383
+ };
384
+
385
+ if (isPullDone(machine.state)) {
386
+ finish();
387
+ return;
388
+ }
389
+ // Wait for the RSC pull loop promise to resolve instead of
390
+ // polling with setImmediate. No CPU spin, no busy-poll —
391
+ // just a Promise chain. See design/02 §"No Polling".
392
+ if (!pullPromise) {
393
+ pullPromise = pullLoop();
394
+ }
395
+ pullPromise.then(finish, (err) => {
396
+ machine.send({ type: 'PULL_ERROR', error: err });
397
+ finish();
398
+ });
399
+ },
400
+ });
401
+
402
+ return transform;
403
+ }
404
+
405
+ // ─── Error Handling ──────────────────────────────────────────────────────────
406
+
407
+ const NOINDEX_SCRIPT =
408
+ '<script>document.head.appendChild(Object.assign(document.createElement("meta"),{name:"robots",content:"noindex"}))</script>';
409
+
410
+ /**
411
+ * Node.js Transform that catches post-shell streaming errors.
412
+ *
413
+ * Equivalent to wrapStreamWithErrorHandling() in ssr-render.ts.
414
+ * Catches errors from React's streaming phase (deny/throw inside Suspense
415
+ * after the shell has flushed) and closes the stream cleanly.
416
+ */
417
+ export function createNodeErrorHandler(signal?: AbortSignal): Transform {
418
+ const transform = new Transform({
419
+ transform(chunk, _encoding, callback) {
420
+ callback(null, chunk);
421
+ },
422
+ });
423
+
424
+ transform.on('error', (error) => {
425
+ const isAbort =
426
+ (error instanceof DOMException && error.name === 'AbortError') ||
427
+ (error instanceof Error && error.name === 'AbortError') ||
428
+ signal?.aborted;
429
+
430
+ if (isAbort) {
431
+ transform.end();
432
+ return;
433
+ }
434
+
435
+ logStreamingError({ error });
436
+ transform.push(Buffer.from(NOINDEX_SCRIPT, 'utf-8'));
437
+ transform.end();
438
+ });
439
+
440
+ return transform;
441
+ }
442
+
443
+ // ─── Compression ─────────────────────────────────────────────────────────────
444
+
445
+ const COMPRESSIBLE_TYPES = new Set([
446
+ 'text/html',
447
+ 'text/css',
448
+ 'text/plain',
449
+ 'text/xml',
450
+ 'text/javascript',
451
+ 'text/x-component',
452
+ 'application/json',
453
+ 'application/javascript',
454
+ 'application/xml',
455
+ 'application/xhtml+xml',
456
+ 'application/rss+xml',
457
+ 'application/atom+xml',
458
+ 'image/svg+xml',
459
+ ]);
460
+
461
+ /**
462
+ * Create a Node.js gzip Transform using native node:zlib.
463
+ *
464
+ * Uses `createGzip()` which is backed by C++ zlib — significantly faster
465
+ * than the Web Streams `CompressionStream` API (which is a JS wrapper
466
+ * around the same zlib but with per-chunk Promise overhead).
467
+ *
468
+ * Returns null if the response shouldn't be compressed (wrong content type,
469
+ * client doesn't accept gzip, already encoded, etc.).
470
+ */
471
+ export function createNodeGzipCompressor(
472
+ requestHeaders: Headers,
473
+ responseHeaders: Headers
474
+ ): Transform | null {
475
+ // Check Accept-Encoding
476
+ const acceptEncoding = requestHeaders.get('accept-encoding') || '';
477
+ if (!acceptEncoding.includes('gzip')) return null;
478
+
479
+ // Check content type is compressible
480
+ const contentType = responseHeaders.get('content-type') || '';
481
+ const mimeType = contentType.split(';')[0].trim().toLowerCase();
482
+ if (!COMPRESSIBLE_TYPES.has(mimeType)) return null;
483
+
484
+ // Don't double-compress
485
+ if (responseHeaders.has('content-encoding')) return null;
486
+
487
+ // Set response headers for gzip
488
+ responseHeaders.set('content-encoding', 'gzip');
489
+ responseHeaders.delete('content-length');
490
+ const existingVary = responseHeaders.get('vary');
491
+ if (existingVary) {
492
+ if (!existingVary.toLowerCase().includes('accept-encoding')) {
493
+ responseHeaders.set('vary', existingVary + ', Accept-Encoding');
494
+ }
495
+ } else {
496
+ responseHeaders.set('vary', 'Accept-Encoding');
497
+ }
498
+
499
+ // Z_SYNC_FLUSH ensures each chunk is flushed to the output immediately.
500
+ // Without it, gzip buffers internally and the browser doesn't receive
501
+ // the HTML shell until the gzip stream closes — breaking streaming.
502
+ // ~2–5% size overhead vs Z_NO_FLUSH but preserves correct streaming.
503
+ return createGzip({ flush: constants.Z_SYNC_FLUSH });
504
+ }