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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (333) hide show
  1. package/LICENSE +8 -0
  2. package/dist/_chunks/{als-registry-B7DbZ2hS.js → als-registry-Ba7URUIn.js} +1 -1
  3. package/dist/_chunks/als-registry-Ba7URUIn.js.map +1 -0
  4. package/dist/_chunks/chunk-DYhsFzuS.js +33 -0
  5. package/dist/_chunks/{debug-gwlJkDuf.js → debug-ECi_61pb.js} +2 -2
  6. package/dist/_chunks/debug-ECi_61pb.js.map +1 -0
  7. package/dist/_chunks/define-TK8C1M3x.js +279 -0
  8. package/dist/_chunks/define-TK8C1M3x.js.map +1 -0
  9. package/dist/_chunks/define-cookie-k9btcEfI.js +93 -0
  10. package/dist/_chunks/define-cookie-k9btcEfI.js.map +1 -0
  11. package/dist/_chunks/error-boundary-B9vT_YK_.js +211 -0
  12. package/dist/_chunks/error-boundary-B9vT_YK_.js.map +1 -0
  13. package/dist/_chunks/{format-DviM89f0.js → format-cX7wzEp2.js} +2 -2
  14. package/dist/_chunks/{format-DviM89f0.js.map → format-cX7wzEp2.js.map} +1 -1
  15. package/dist/_chunks/{interception-BOoWmLUA.js → interception-D2djYaIm.js} +112 -77
  16. package/dist/_chunks/interception-D2djYaIm.js.map +1 -0
  17. package/dist/_chunks/{metadata-routes-Cjmvi3rQ.js → metadata-routes-BU684ls2.js} +1 -1
  18. package/dist/_chunks/{metadata-routes-Cjmvi3rQ.js.map → metadata-routes-BU684ls2.js.map} +1 -1
  19. package/dist/_chunks/{request-context-DIkVh_jG.js → request-context-0h-6Voad.js} +95 -69
  20. package/dist/_chunks/request-context-0h-6Voad.js.map +1 -0
  21. package/dist/_chunks/segment-context-DBn-nrMN.js +69 -0
  22. package/dist/_chunks/segment-context-DBn-nrMN.js.map +1 -0
  23. package/dist/_chunks/stale-reload-4L-_skC7.js +47 -0
  24. package/dist/_chunks/stale-reload-4L-_skC7.js.map +1 -0
  25. package/dist/_chunks/{tracing-Cwn7697K.js → tracing-JI4cYUdz.js} +17 -3
  26. package/dist/_chunks/{tracing-Cwn7697K.js.map → tracing-JI4cYUdz.js.map} +1 -1
  27. package/dist/_chunks/{use-query-states-D5KaffOK.js → use-query-states-wEXY2JQB.js} +1 -1
  28. package/dist/_chunks/{use-query-states-D5KaffOK.js.map → use-query-states-wEXY2JQB.js.map} +1 -1
  29. package/dist/_chunks/wrappers-C9XPg7-U.js +63 -0
  30. package/dist/_chunks/wrappers-C9XPg7-U.js.map +1 -0
  31. package/dist/adapters/compress-module.d.ts.map +1 -1
  32. package/dist/adapters/nitro.d.ts +17 -1
  33. package/dist/adapters/nitro.d.ts.map +1 -1
  34. package/dist/adapters/nitro.js +56 -13
  35. package/dist/adapters/nitro.js.map +1 -1
  36. package/dist/cache/fast-hash.d.ts +22 -0
  37. package/dist/cache/fast-hash.d.ts.map +1 -0
  38. package/dist/cache/index.d.ts +5 -2
  39. package/dist/cache/index.d.ts.map +1 -1
  40. package/dist/cache/index.js +90 -20
  41. package/dist/cache/index.js.map +1 -1
  42. package/dist/cache/register-cached-function.d.ts.map +1 -1
  43. package/dist/cache/singleflight.d.ts +18 -1
  44. package/dist/cache/singleflight.d.ts.map +1 -1
  45. package/dist/cache/timber-cache.d.ts +1 -1
  46. package/dist/cache/timber-cache.d.ts.map +1 -1
  47. package/dist/client/error-boundary.d.ts +10 -1
  48. package/dist/client/error-boundary.d.ts.map +1 -1
  49. package/dist/client/error-boundary.js +1 -125
  50. package/dist/client/index.d.ts +3 -2
  51. package/dist/client/index.d.ts.map +1 -1
  52. package/dist/client/index.js +213 -93
  53. package/dist/client/index.js.map +1 -1
  54. package/dist/client/link.d.ts +22 -8
  55. package/dist/client/link.d.ts.map +1 -1
  56. package/dist/client/navigation-context.d.ts +2 -2
  57. package/dist/client/router.d.ts +25 -3
  58. package/dist/client/router.d.ts.map +1 -1
  59. package/dist/client/rsc-fetch.d.ts +23 -2
  60. package/dist/client/rsc-fetch.d.ts.map +1 -1
  61. package/dist/client/segment-cache.d.ts +1 -1
  62. package/dist/client/segment-cache.d.ts.map +1 -1
  63. package/dist/client/segment-context.d.ts +1 -1
  64. package/dist/client/segment-context.d.ts.map +1 -1
  65. package/dist/client/segment-merger.d.ts.map +1 -1
  66. package/dist/client/stale-reload.d.ts +15 -0
  67. package/dist/client/stale-reload.d.ts.map +1 -1
  68. package/dist/client/top-loader.d.ts +1 -1
  69. package/dist/client/top-loader.d.ts.map +1 -1
  70. package/dist/client/transition-root.d.ts +1 -1
  71. package/dist/client/transition-root.d.ts.map +1 -1
  72. package/dist/client/use-params.d.ts +2 -2
  73. package/dist/client/use-params.d.ts.map +1 -1
  74. package/dist/client/use-query-states.d.ts +1 -1
  75. package/dist/codec.d.ts +21 -0
  76. package/dist/codec.d.ts.map +1 -0
  77. package/dist/cookies/define-cookie.d.ts +33 -12
  78. package/dist/cookies/define-cookie.d.ts.map +1 -1
  79. package/dist/cookies/index.js +1 -83
  80. package/dist/fonts/css.d.ts +1 -0
  81. package/dist/fonts/css.d.ts.map +1 -1
  82. package/dist/index.d.ts +112 -35
  83. package/dist/index.d.ts.map +1 -1
  84. package/dist/index.js +467 -246
  85. package/dist/index.js.map +1 -1
  86. package/dist/params/define.d.ts +76 -0
  87. package/dist/params/define.d.ts.map +1 -0
  88. package/dist/params/index.d.ts +8 -0
  89. package/dist/params/index.d.ts.map +1 -0
  90. package/dist/params/index.js +105 -0
  91. package/dist/params/index.js.map +1 -0
  92. package/dist/plugins/adapter-build.d.ts.map +1 -1
  93. package/dist/plugins/build-manifest.d.ts.map +1 -1
  94. package/dist/plugins/client-chunks.d.ts +32 -0
  95. package/dist/plugins/client-chunks.d.ts.map +1 -0
  96. package/dist/plugins/dev-error-overlay.d.ts +26 -1
  97. package/dist/plugins/dev-error-overlay.d.ts.map +1 -1
  98. package/dist/plugins/entries.d.ts.map +1 -1
  99. package/dist/plugins/fonts.d.ts +7 -0
  100. package/dist/plugins/fonts.d.ts.map +1 -1
  101. package/dist/plugins/routing.d.ts.map +1 -1
  102. package/dist/plugins/server-bundle.d.ts.map +1 -1
  103. package/dist/plugins/static-build.d.ts.map +1 -1
  104. package/dist/routing/codegen.d.ts +2 -2
  105. package/dist/routing/codegen.d.ts.map +1 -1
  106. package/dist/routing/index.js +1 -1
  107. package/dist/routing/scanner.d.ts.map +1 -1
  108. package/dist/routing/status-file-lint.d.ts +2 -1
  109. package/dist/routing/status-file-lint.d.ts.map +1 -1
  110. package/dist/routing/types.d.ts +6 -4
  111. package/dist/routing/types.d.ts.map +1 -1
  112. package/dist/rsc-runtime/rsc.d.ts +1 -1
  113. package/dist/rsc-runtime/rsc.d.ts.map +1 -1
  114. package/dist/rsc-runtime/ssr.d.ts +12 -0
  115. package/dist/rsc-runtime/ssr.d.ts.map +1 -1
  116. package/dist/search-params/codecs.d.ts +1 -1
  117. package/dist/search-params/define.d.ts +159 -0
  118. package/dist/search-params/define.d.ts.map +1 -0
  119. package/dist/search-params/index.d.ts +4 -5
  120. package/dist/search-params/index.d.ts.map +1 -1
  121. package/dist/search-params/index.js +4 -474
  122. package/dist/search-params/registry.d.ts +1 -1
  123. package/dist/search-params/wrappers.d.ts +53 -0
  124. package/dist/search-params/wrappers.d.ts.map +1 -0
  125. package/dist/server/access-gate.d.ts +4 -0
  126. package/dist/server/access-gate.d.ts.map +1 -1
  127. package/dist/server/action-client.d.ts.map +1 -1
  128. package/dist/server/action-encryption.d.ts +76 -0
  129. package/dist/server/action-encryption.d.ts.map +1 -0
  130. package/dist/server/action-handler.d.ts.map +1 -1
  131. package/dist/server/als-registry.d.ts +18 -4
  132. package/dist/server/als-registry.d.ts.map +1 -1
  133. package/dist/server/build-manifest.d.ts +2 -2
  134. package/dist/server/debug.d.ts +1 -1
  135. package/dist/server/default-logger.d.ts +22 -0
  136. package/dist/server/default-logger.d.ts.map +1 -0
  137. package/dist/server/deny-renderer.d.ts.map +1 -1
  138. package/dist/server/early-hints.d.ts +13 -5
  139. package/dist/server/early-hints.d.ts.map +1 -1
  140. package/dist/server/error-boundary-wrapper.d.ts +4 -0
  141. package/dist/server/error-boundary-wrapper.d.ts.map +1 -1
  142. package/dist/server/flight-injection-state.d.ts +66 -0
  143. package/dist/server/flight-injection-state.d.ts.map +1 -0
  144. package/dist/server/flight-scripts.d.ts +39 -0
  145. package/dist/server/flight-scripts.d.ts.map +1 -0
  146. package/dist/server/flush.d.ts.map +1 -1
  147. package/dist/server/form-data.d.ts +29 -0
  148. package/dist/server/form-data.d.ts.map +1 -1
  149. package/dist/server/html-injectors.d.ts +51 -11
  150. package/dist/server/html-injectors.d.ts.map +1 -1
  151. package/dist/server/index.d.ts +4 -2
  152. package/dist/server/index.d.ts.map +1 -1
  153. package/dist/server/index.js +1974 -1648
  154. package/dist/server/index.js.map +1 -1
  155. package/dist/server/logger.d.ts +24 -7
  156. package/dist/server/logger.d.ts.map +1 -1
  157. package/dist/server/node-stream-transforms.d.ts +113 -0
  158. package/dist/server/node-stream-transforms.d.ts.map +1 -0
  159. package/dist/server/pipeline.d.ts +7 -4
  160. package/dist/server/pipeline.d.ts.map +1 -1
  161. package/dist/server/primitives.d.ts +30 -3
  162. package/dist/server/primitives.d.ts.map +1 -1
  163. package/dist/server/render-timeout.d.ts +51 -0
  164. package/dist/server/render-timeout.d.ts.map +1 -0
  165. package/dist/server/request-context.d.ts +65 -38
  166. package/dist/server/request-context.d.ts.map +1 -1
  167. package/dist/server/route-element-builder.d.ts +7 -0
  168. package/dist/server/route-element-builder.d.ts.map +1 -1
  169. package/dist/server/route-handler.d.ts.map +1 -1
  170. package/dist/server/route-matcher.d.ts +2 -2
  171. package/dist/server/route-matcher.d.ts.map +1 -1
  172. package/dist/server/rsc-entry/error-renderer.d.ts.map +1 -1
  173. package/dist/server/rsc-entry/helpers.d.ts +46 -3
  174. package/dist/server/rsc-entry/helpers.d.ts.map +1 -1
  175. package/dist/server/rsc-entry/index.d.ts +6 -1
  176. package/dist/server/rsc-entry/index.d.ts.map +1 -1
  177. package/dist/server/rsc-entry/rsc-payload.d.ts.map +1 -1
  178. package/dist/server/rsc-entry/rsc-stream.d.ts +9 -0
  179. package/dist/server/rsc-entry/rsc-stream.d.ts.map +1 -1
  180. package/dist/server/rsc-entry/ssr-renderer.d.ts.map +1 -1
  181. package/dist/server/slot-resolver.d.ts +1 -1
  182. package/dist/server/slot-resolver.d.ts.map +1 -1
  183. package/dist/server/ssr-entry.d.ts +22 -0
  184. package/dist/server/ssr-entry.d.ts.map +1 -1
  185. package/dist/server/ssr-render.d.ts +39 -21
  186. package/dist/server/ssr-render.d.ts.map +1 -1
  187. package/dist/server/ssr-wrappers.d.ts +50 -0
  188. package/dist/server/ssr-wrappers.d.ts.map +1 -0
  189. package/dist/server/tracing.d.ts +10 -0
  190. package/dist/server/tracing.d.ts.map +1 -1
  191. package/dist/server/tree-builder.d.ts +19 -12
  192. package/dist/server/tree-builder.d.ts.map +1 -1
  193. package/dist/server/types.d.ts +1 -3
  194. package/dist/server/types.d.ts.map +1 -1
  195. package/dist/server/version-skew.d.ts +61 -0
  196. package/dist/server/version-skew.d.ts.map +1 -0
  197. package/dist/server/waituntil-bridge.d.ts.map +1 -1
  198. package/dist/shared/merge-search-params.d.ts +22 -0
  199. package/dist/shared/merge-search-params.d.ts.map +1 -0
  200. package/dist/shims/navigation-client.d.ts +1 -1
  201. package/dist/shims/navigation-client.d.ts.map +1 -1
  202. package/dist/shims/navigation.d.ts +1 -1
  203. package/dist/shims/navigation.d.ts.map +1 -1
  204. package/dist/utils/state-machine.d.ts +80 -0
  205. package/dist/utils/state-machine.d.ts.map +1 -0
  206. package/package.json +17 -14
  207. package/src/adapters/compress-module.ts +24 -4
  208. package/src/adapters/nitro.ts +58 -9
  209. package/src/cache/fast-hash.ts +34 -0
  210. package/src/cache/index.ts +5 -2
  211. package/src/cache/register-cached-function.ts +7 -3
  212. package/src/cache/singleflight.ts +62 -4
  213. package/src/cache/timber-cache.ts +40 -29
  214. package/src/cli.ts +0 -0
  215. package/src/client/browser-entry.ts +133 -93
  216. package/src/client/error-boundary.tsx +18 -1
  217. package/src/client/index.ts +10 -1
  218. package/src/client/link.tsx +78 -19
  219. package/src/client/navigation-context.ts +2 -2
  220. package/src/client/router.ts +105 -60
  221. package/src/client/rsc-fetch.ts +63 -2
  222. package/src/client/segment-cache.ts +1 -1
  223. package/src/client/segment-context.ts +6 -1
  224. package/src/client/segment-merger.ts +2 -8
  225. package/src/client/stale-reload.ts +32 -6
  226. package/src/client/top-loader.tsx +10 -9
  227. package/src/client/transition-root.tsx +7 -1
  228. package/src/client/use-params.ts +3 -3
  229. package/src/client/use-query-states.ts +1 -1
  230. package/src/codec.ts +21 -0
  231. package/src/cookies/define-cookie.ts +69 -18
  232. package/src/fonts/css.ts +2 -1
  233. package/src/index.ts +280 -85
  234. package/src/params/define.ts +260 -0
  235. package/src/params/index.ts +28 -0
  236. package/src/plugins/adapter-build.ts +6 -0
  237. package/src/plugins/build-manifest.ts +11 -0
  238. package/src/plugins/client-chunks.ts +65 -0
  239. package/src/plugins/dev-error-overlay.ts +70 -1
  240. package/src/plugins/dev-server.ts +38 -4
  241. package/src/plugins/entries.ts +5 -7
  242. package/src/plugins/fonts.ts +93 -42
  243. package/src/plugins/routing.ts +40 -14
  244. package/src/plugins/server-bundle.ts +32 -1
  245. package/src/plugins/shims.ts +1 -1
  246. package/src/plugins/static-build.ts +8 -4
  247. package/src/routing/codegen.ts +109 -88
  248. package/src/routing/scanner.ts +55 -6
  249. package/src/routing/status-file-lint.ts +2 -1
  250. package/src/routing/types.ts +7 -4
  251. package/src/rsc-runtime/rsc.ts +2 -0
  252. package/src/rsc-runtime/ssr.ts +50 -0
  253. package/src/rsc-runtime/vendor-types.d.ts +7 -0
  254. package/src/search-params/codecs.ts +1 -1
  255. package/src/search-params/define.ts +518 -0
  256. package/src/search-params/index.ts +12 -18
  257. package/src/search-params/registry.ts +1 -1
  258. package/src/search-params/wrappers.ts +85 -0
  259. package/src/server/access-gate.tsx +40 -9
  260. package/src/server/action-client.ts +7 -1
  261. package/src/server/action-encryption.ts +144 -0
  262. package/src/server/action-handler.ts +19 -2
  263. package/src/server/als-registry.ts +18 -4
  264. package/src/server/build-manifest.ts +4 -4
  265. package/src/server/compress.ts +25 -7
  266. package/src/server/debug.ts +1 -1
  267. package/src/server/default-logger.ts +98 -0
  268. package/src/server/deny-renderer.ts +2 -1
  269. package/src/server/early-hints.ts +36 -15
  270. package/src/server/error-boundary-wrapper.ts +57 -14
  271. package/src/server/flight-injection-state.ts +113 -0
  272. package/src/server/flight-scripts.ts +59 -0
  273. package/src/server/flush.ts +2 -1
  274. package/src/server/form-data.ts +76 -0
  275. package/src/server/html-injectors.ts +261 -117
  276. package/src/server/index.ts +9 -4
  277. package/src/server/logger.ts +38 -35
  278. package/src/server/node-stream-transforms.ts +504 -0
  279. package/src/server/pipeline.ts +131 -39
  280. package/src/server/primitives.ts +47 -5
  281. package/src/server/render-timeout.ts +108 -0
  282. package/src/server/request-context.ts +119 -119
  283. package/src/server/route-element-builder.ts +106 -114
  284. package/src/server/route-handler.ts +2 -1
  285. package/src/server/route-matcher.ts +2 -2
  286. package/src/server/rsc-entry/error-renderer.ts +5 -3
  287. package/src/server/rsc-entry/helpers.ts +122 -3
  288. package/src/server/rsc-entry/index.ts +108 -43
  289. package/src/server/rsc-entry/rsc-payload.ts +52 -12
  290. package/src/server/rsc-entry/rsc-stream.ts +49 -12
  291. package/src/server/rsc-entry/ssr-renderer.ts +40 -13
  292. package/src/server/slot-resolver.ts +222 -217
  293. package/src/server/ssr-entry.ts +209 -30
  294. package/src/server/ssr-render.ts +289 -67
  295. package/src/server/ssr-wrappers.tsx +139 -0
  296. package/src/server/tracing.ts +23 -0
  297. package/src/server/tree-builder.ts +91 -57
  298. package/src/server/types.ts +1 -3
  299. package/src/server/version-skew.ts +104 -0
  300. package/src/server/waituntil-bridge.ts +4 -1
  301. package/src/shared/merge-search-params.ts +48 -0
  302. package/src/shims/navigation-client.ts +1 -1
  303. package/src/shims/navigation.ts +1 -1
  304. package/src/utils/state-machine.ts +111 -0
  305. package/dist/_chunks/als-registry-B7DbZ2hS.js.map +0 -1
  306. package/dist/_chunks/debug-gwlJkDuf.js.map +0 -1
  307. package/dist/_chunks/interception-BOoWmLUA.js.map +0 -1
  308. package/dist/_chunks/request-context-DIkVh_jG.js.map +0 -1
  309. package/dist/_chunks/ssr-data-MjmprTmO.js +0 -88
  310. package/dist/_chunks/ssr-data-MjmprTmO.js.map +0 -1
  311. package/dist/_chunks/use-cookie-DX-l1_5E.js +0 -91
  312. package/dist/_chunks/use-cookie-DX-l1_5E.js.map +0 -1
  313. package/dist/client/error-boundary.js.map +0 -1
  314. package/dist/cookies/index.js.map +0 -1
  315. package/dist/plugins/dynamic-transform.d.ts +0 -72
  316. package/dist/plugins/dynamic-transform.d.ts.map +0 -1
  317. package/dist/search-params/analyze.d.ts +0 -54
  318. package/dist/search-params/analyze.d.ts.map +0 -1
  319. package/dist/search-params/builtin-codecs.d.ts +0 -105
  320. package/dist/search-params/builtin-codecs.d.ts.map +0 -1
  321. package/dist/search-params/create.d.ts +0 -106
  322. package/dist/search-params/create.d.ts.map +0 -1
  323. package/dist/search-params/index.js.map +0 -1
  324. package/dist/server/prerender.d.ts +0 -77
  325. package/dist/server/prerender.d.ts.map +0 -1
  326. package/dist/server/response-cache.d.ts +0 -53
  327. package/dist/server/response-cache.d.ts.map +0 -1
  328. package/src/plugins/dynamic-transform.ts +0 -161
  329. package/src/search-params/analyze.ts +0 -192
  330. package/src/search-params/builtin-codecs.ts +0 -228
  331. package/src/search-params/create.ts +0 -321
  332. package/src/server/prerender.ts +0 -139
  333. package/src/server/response-cache.ts +0 -277
@@ -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) {
@@ -22,10 +22,123 @@ import { SlotAccessGate } from './access-gate.js';
22
22
  import { wrapSegmentWithErrorBoundaries } from './error-boundary-wrapper.js';
23
23
  import type { InterceptionContext, RouteMatch } from './pipeline.js';
24
24
  import { DenySignal } from './primitives.js';
25
+ import { logRenderError } from './logger.js';
25
26
  import type { ManifestSegmentNode } from './route-matcher.js';
26
27
 
27
28
  type CreateElementFn = (...args: unknown[]) => React.ReactElement;
28
29
 
30
+ // ─── Module Loading Helpers ─────────────────────────────────────────────────
31
+
32
+ /**
33
+ * Load a module and extract its default export as a component function.
34
+ * Returns undefined if no default export exists.
35
+ */
36
+ async function loadComponent(loader: {
37
+ load: () => Promise<unknown>;
38
+ }): Promise<((...args: unknown[]) => unknown) | undefined> {
39
+ const mod = (await loader.load()) as Record<string, unknown>;
40
+ return mod.default as ((...args: unknown[]) => unknown) | undefined;
41
+ }
42
+
43
+ /**
44
+ * Load and render the default.tsx fallback for a slot node.
45
+ * Returns null if the slot has no default.tsx or it has no default export.
46
+ */
47
+ async function renderDefaultFallback(
48
+ slotNode: ManifestSegmentNode,
49
+ h: CreateElementFn
50
+ ): Promise<React.ReactElement | null> {
51
+ if (!slotNode.default) return null;
52
+ const DefaultComp = await loadComponent(slotNode.default);
53
+ if (!DefaultComp) return null;
54
+ return h(DefaultComp, {});
55
+ }
56
+
57
+ // ─── Segment Tree Matching ──────────────────────────────────────────────────
58
+
59
+ /**
60
+ * Find a matching child node for a URL segment name.
61
+ *
62
+ * Tries matches in priority order:
63
+ * 1. Static segment (exact name match)
64
+ * 2. Dynamic segment ([param])
65
+ * 3. Catch-all or optional-catch-all ([...param] / [[...param]])
66
+ * 4. Group children (transparent wrappers)
67
+ *
68
+ * Returns `{ node, consumesRest }` where `consumesRest` is true for catch-all
69
+ * segments that consume all remaining URL parts.
70
+ */
71
+ function findMatchingChild(
72
+ children: ManifestSegmentNode[],
73
+ segmentName: string
74
+ ): { node: ManifestSegmentNode; consumesRest: boolean } | null {
75
+ // 1. Static match
76
+ for (const child of children) {
77
+ if (child.segmentType === 'static' && child.segmentName === segmentName) {
78
+ return { node: child, consumesRest: false };
79
+ }
80
+ }
81
+
82
+ // 2. Dynamic match
83
+ for (const child of children) {
84
+ if (child.segmentType === 'dynamic') {
85
+ return { node: child, consumesRest: false };
86
+ }
87
+ }
88
+
89
+ // 3. Catch-all match — consumes all remaining segments
90
+ for (const child of children) {
91
+ if (child.segmentType === 'catch-all' || child.segmentType === 'optional-catch-all') {
92
+ return { node: child, consumesRest: true };
93
+ }
94
+ }
95
+
96
+ // 4. Group children (transparent)
97
+ for (const child of children) {
98
+ if (child.segmentType === 'group') {
99
+ for (const groupChild of child.children ?? []) {
100
+ if (groupChild.segmentName === segmentName) {
101
+ return { node: groupChild, consumesRest: false };
102
+ }
103
+ }
104
+ }
105
+ }
106
+
107
+ return null;
108
+ }
109
+
110
+ /**
111
+ * Walk a segment tree from `startNode`, matching each part in `parts` against
112
+ * child nodes. Returns the chain of matched nodes (including startNode) and the
113
+ * final node, or null if any part fails to match.
114
+ */
115
+ function walkSegmentTree(
116
+ startNode: ManifestSegmentNode,
117
+ parts: { segmentName: string }[] | string[],
118
+ initialChain: ManifestSegmentNode[] = [startNode]
119
+ ): { chain: ManifestSegmentNode[]; leaf: ManifestSegmentNode } | null {
120
+ const chain = [...initialChain];
121
+ let currentNode = startNode;
122
+
123
+ for (const part of parts) {
124
+ const segName = typeof part === 'string' ? part : part.segmentName;
125
+ const directChildren = currentNode.children ?? [];
126
+ const match = findMatchingChild(directChildren, segName);
127
+
128
+ if (!match) return null;
129
+
130
+ chain.push(match.node);
131
+ currentNode = match.node;
132
+
133
+ // Catch-all segments consume all remaining parts
134
+ if (match.consumesRest) break;
135
+ }
136
+
137
+ return { chain, leaf: currentNode };
138
+ }
139
+
140
+ // ─── Slot Element Resolution ────────────────────────────────────────────────
141
+
29
142
  /**
30
143
  * Resolve the element for a parallel slot.
31
144
  *
@@ -40,7 +153,6 @@ type CreateElementFn = (...args: unknown[]) => React.ReactElement;
40
153
  export async function resolveSlotElement(
41
154
  slotNode: ManifestSegmentNode,
42
155
  match: RouteMatch,
43
- paramsPromise: Promise<Record<string, string | string[]>>,
44
156
  h: CreateElementFn,
45
157
  interception?: InterceptionContext
46
158
  ): Promise<React.ReactElement | null> {
@@ -54,125 +166,61 @@ export async function resolveSlotElement(
54
166
  : findSlotMatch(slotNode, match);
55
167
 
56
168
  if (slotMatch) {
57
- const mod = (await slotMatch.page.load()) as Record<string, unknown>;
58
- if (mod.default) {
59
- const SlotPage = mod.default as (...args: unknown[]) => unknown;
60
-
169
+ const SlotPage = await loadComponent(slotMatch.page);
170
+ if (SlotPage) {
61
171
  // Load default.tsx fallback for notFound() handling in the slot page.
62
172
  // When a slot page calls notFound() or deny(), it should gracefully
63
173
  // degrade to default.tsx or null — not crash the page. This matches
64
174
  // Next.js behavior. See design/02-rendering-pipeline.md
65
175
  // §"Slot Access Failure = Graceful Degradation"
66
- let denyFallback: React.ReactElement | null = null;
67
- if (slotNode.default) {
68
- const defaultMod = (await slotNode.default.load()) as Record<string, unknown>;
69
- const DefaultComp = defaultMod.default as ((...args: unknown[]) => unknown) | undefined;
70
- if (DefaultComp) {
71
- denyFallback = h(DefaultComp, { params: paramsPromise, searchParams: {} });
72
- }
73
- }
74
-
75
- // Wrap the slot page to catch DenySignal (from notFound() or deny())
76
- // at the component level. This prevents the signal from reaching the
77
- // RSC onError callback and being tracked as a page-level denial, which
78
- // would cause the pipeline to replace the entire successful SSR response
79
- // with a deny page. Instead, the slot gracefully degrades.
80
- const denyFallbackCapture = denyFallback;
176
+ const denyFallback = await renderDefaultFallback(slotNode, h);
177
+
178
+ // Wrap the slot page to catch ALL errors at the component level.
179
+ // This prevents errors from leaving unresolved Flight rows in the
180
+ // RSC stream — when a slot component throws and the error propagates
181
+ // to React's Flight renderer, it may not emit a resolution row for
182
+ // the slot's lazy reference. The client's createFromReadableStream
183
+ // then throws "Connection closed" when the stream ends with pending
184
+ // references. By catching all errors here and returning a fallback,
185
+ // React sees a resolved component and emits a proper Flight row.
186
+ //
187
+ // DenySignal (from notFound() or deny()) returns the deny fallback.
188
+ // All other errors return the deny fallback or null the slot
189
+ // gracefully degrades rather than breaking the entire page.
190
+ // See TIM-524.
81
191
  const SafeSlotPage = async (props: Record<string, unknown>) => {
82
192
  try {
83
193
  return await (SlotPage as (props: Record<string, unknown>) => unknown)(props);
84
194
  } catch (error) {
85
195
  if (error instanceof DenySignal) {
86
- return denyFallbackCapture;
196
+ return denyFallback;
87
197
  }
88
- throw error;
198
+ // Log the error but don't re-throw — returning fallback ensures
199
+ // the Flight row is resolved and the page hydrates correctly.
200
+ logRenderError({
201
+ method: '',
202
+ path: '',
203
+ error,
204
+ });
205
+ return denyFallback;
89
206
  }
90
207
  };
91
208
 
92
- let element: React.ReactElement = h(SafeSlotPage, {
93
- params: paramsPromise,
94
- searchParams: {},
95
- });
209
+ let element: React.ReactElement = h(SafeSlotPage, {});
96
210
 
97
211
  // Wrap with error boundaries and layouts from intermediate slot segments
98
212
  // (everything between slot root and leaf). Process innermost-first, same
99
213
  // order as route-element-builder.ts handles main segments. The slot root
100
214
  // (index 0) is handled separately after the access gate below.
101
- for (let i = slotMatch.chain.length - 1; i > 0; i--) {
102
- const seg = slotMatch.chain[i];
103
-
104
- // Error boundaries from this segment
105
- element = await wrapSegmentWithErrorBoundaries(seg, element, h);
106
-
107
- // Layout from this segment
108
- if (seg.layout) {
109
- const layoutMod = (await seg.layout.load()) as Record<string, unknown>;
110
- if (layoutMod.default) {
111
- const Layout = layoutMod.default as (...args: unknown[]) => unknown;
112
- element = h(Layout, {
113
- params: paramsPromise,
114
- searchParams: {},
115
- children: element,
116
- });
117
- }
118
- }
119
- }
215
+ element = await wrapWithIntermediateSegments(slotMatch.chain, element, h);
120
216
 
121
217
  // Wrap in SlotAccessGate if slot root has access.ts.
122
218
  // On denial: denied.tsx → default.tsx → null (graceful degradation).
123
219
  // See design/04-authorization.md §"Slot-Level Auth".
124
- if (slotNode.access) {
125
- const accessMod = (await slotNode.access.load()) as Record<string, unknown>;
126
- const accessFn = accessMod.default as
127
- | ((ctx: { params: Record<string, string | string[]>; searchParams: unknown }) => unknown)
128
- | undefined;
129
- if (accessFn) {
130
- // Load denied.tsx fallback
131
- let deniedFallback: React.ReactElement | null = null;
132
- if (slotNode.denied) {
133
- const deniedMod = (await slotNode.denied.load()) as Record<string, unknown>;
134
- const DeniedComponent = deniedMod.default as
135
- | ((...args: unknown[]) => unknown)
136
- | undefined;
137
- if (DeniedComponent) {
138
- deniedFallback = h(DeniedComponent, {});
139
- }
140
- }
141
-
142
- // Load default.tsx fallback
143
- let defaultFallback: React.ReactElement | null = null;
144
- if (slotNode.default) {
145
- const defaultMod = (await slotNode.default.load()) as Record<string, unknown>;
146
- const DefaultComp = defaultMod.default as ((...args: unknown[]) => unknown) | undefined;
147
- if (DefaultComp) {
148
- defaultFallback = h(DefaultComp, { params: paramsPromise, searchParams: {} });
149
- }
150
- }
151
-
152
- const params = await paramsPromise;
153
- element = h(SlotAccessGate, {
154
- accessFn,
155
- params,
156
- searchParams: {},
157
- deniedFallback,
158
- defaultFallback,
159
- children: element,
160
- });
161
- }
162
- }
220
+ element = await wrapWithAccessGate(slotNode, element, h);
163
221
 
164
222
  // Wrap with slot root's layout (outermost, outside access gate)
165
- if (slotNode.layout) {
166
- const layoutMod = (await slotNode.layout.load()) as Record<string, unknown>;
167
- if (layoutMod.default) {
168
- const Layout = layoutMod.default as (...args: unknown[]) => unknown;
169
- element = h(Layout, {
170
- params: paramsPromise,
171
- searchParams: {},
172
- children: element,
173
- });
174
- }
175
- }
223
+ element = await wrapWithLayout(slotNode, element, h);
176
224
 
177
225
  // Wrap with slot root's error boundaries (outermost)
178
226
  element = await wrapSegmentWithErrorBoundaries(slotNode, element, h);
@@ -195,18 +243,81 @@ export async function resolveSlotElement(
195
243
  }
196
244
 
197
245
  // No matching page — render default.tsx fallback
198
- if (slotNode.default) {
199
- const mod = (await slotNode.default.load()) as Record<string, unknown>;
200
- if (mod.default) {
201
- const DefaultComponent = mod.default as (...args: unknown[]) => unknown;
202
- return h(DefaultComponent, { params: paramsPromise, searchParams: {} });
203
- }
246
+ return renderDefaultFallback(slotNode, h);
247
+ }
248
+
249
+ // ─── Element Wrapping Helpers ───────────────────────────────────────────────
250
+
251
+ /**
252
+ * Wrap an element with error boundaries and layouts from intermediate
253
+ * slot segments (indices 1..n, skipping the slot root at index 0).
254
+ * Processes innermost-first to match route-element-builder.ts ordering.
255
+ */
256
+ async function wrapWithIntermediateSegments(
257
+ chain: ManifestSegmentNode[],
258
+ element: React.ReactElement,
259
+ h: CreateElementFn
260
+ ): Promise<React.ReactElement> {
261
+ for (let i = chain.length - 1; i > 0; i--) {
262
+ const seg = chain[i];
263
+ element = await wrapSegmentWithErrorBoundaries(seg, element, h);
264
+ element = await wrapWithLayout(seg, element, h);
204
265
  }
266
+ return element;
267
+ }
205
268
 
206
- // No page and no default — slot renders nothing
207
- return null;
269
+ /**
270
+ * Wrap an element with a segment's layout component, if present.
271
+ */
272
+ async function wrapWithLayout(
273
+ node: ManifestSegmentNode,
274
+ element: React.ReactElement,
275
+ h: CreateElementFn
276
+ ): Promise<React.ReactElement> {
277
+ if (!node.layout) return element;
278
+ const Layout = await loadComponent(node.layout);
279
+ if (!Layout) return element;
280
+ return h(Layout, { children: element });
208
281
  }
209
282
 
283
+ /**
284
+ * Wrap an element with a SlotAccessGate if the node has access.ts.
285
+ * On denial: denied.tsx → default.tsx → null (graceful degradation).
286
+ */
287
+ async function wrapWithAccessGate(
288
+ slotNode: ManifestSegmentNode,
289
+ element: React.ReactElement,
290
+ h: CreateElementFn
291
+ ): Promise<React.ReactElement> {
292
+ if (!slotNode.access) return element;
293
+
294
+ const accessFn = await loadComponent(slotNode.access);
295
+ if (!accessFn) return element;
296
+
297
+ // Pass the component (not pre-built element) so SlotAccessGate can
298
+ // forward DenySignal.data as dangerouslyPassData dynamically. See TIM-488.
299
+ let DeniedComponent: ((...args: unknown[]) => unknown) | null = null;
300
+ if (slotNode.denied) {
301
+ DeniedComponent = (await loadComponent(slotNode.denied)) ?? null;
302
+ }
303
+
304
+ // Extract slot name from the directory name (strip @ prefix)
305
+ const slotName = slotNode.segmentName?.replace(/^@/, '') ?? '';
306
+
307
+ const defaultFallback = await renderDefaultFallback(slotNode, h);
308
+
309
+ return h(SlotAccessGate, {
310
+ accessFn,
311
+ DeniedComponent,
312
+ slotName,
313
+ createElement: h,
314
+ defaultFallback,
315
+ children: element,
316
+ });
317
+ }
318
+
319
+ // ─── Slot Matching ──────────────────────────────────────────────────────────
320
+
210
321
  /** Result of matching a slot's sub-tree against the current route. */
211
322
  interface SlotMatchResult {
212
323
  /** The page file at the matched leaf. */
@@ -267,74 +378,10 @@ function findSlotMatch(slotNode: ManifestSegmentNode, match: RouteMatch): SlotMa
267
378
  return null;
268
379
  }
269
380
 
270
- // Walk the slot's children to match remaining URL segments.
271
- // Track the chain so we can apply error boundaries and layouts.
272
- const chain: ManifestSegmentNode[] = [slotNode];
273
- let currentNode = slotNode;
274
- for (const seg of remainingSegments) {
275
- const childName = seg.segmentName;
276
- const directChildren = currentNode.children ?? [];
277
-
278
- let found: ManifestSegmentNode | null = null;
279
- for (const child of directChildren) {
280
- // Exact static match
281
- if (child.segmentType === 'static' && child.segmentName === childName) {
282
- found = child;
283
- break;
284
- }
285
- }
286
-
287
- // Try dynamic segments if no static match
288
- if (!found) {
289
- for (const child of directChildren) {
290
- if (child.segmentType === 'dynamic') {
291
- found = child;
292
- break;
293
- }
294
- }
295
- }
296
-
297
- // Try catch-all segments — these consume ALL remaining URL segments,
298
- // so we break out of the outer loop immediately.
299
- if (!found) {
300
- for (const child of directChildren) {
301
- if (child.segmentType === 'catch-all' || child.segmentType === 'optional-catch-all') {
302
- found = child;
303
- break;
304
- }
305
- }
306
- if (found) {
307
- chain.push(found);
308
- currentNode = found;
309
- break;
310
- }
311
- }
312
-
313
- // Try group children (transparent)
314
- if (!found) {
315
- for (const child of directChildren) {
316
- if (child.segmentType === 'group') {
317
- for (const groupChild of child.children ?? []) {
318
- if (groupChild.segmentName === childName) {
319
- found = groupChild;
320
- break;
321
- }
322
- }
323
- if (found) break;
324
- }
325
- }
326
- }
327
-
328
- if (!found) {
329
- // No matching child in slot tree — slot doesn't match this URL
330
- return null;
331
- }
332
- chain.push(found);
333
- currentNode = found;
334
- }
335
-
336
- if (currentNode.page) {
337
- return { page: currentNode.page, chain };
381
+ // Walk the slot's children to match remaining URL segments
382
+ const result = walkSegmentTree(slotNode, remainingSegments);
383
+ if (result && result.leaf.page) {
384
+ return { page: result.leaf.page, chain: result.chain };
338
385
  }
339
386
  return null;
340
387
  }
@@ -374,59 +421,17 @@ function findInterceptingMatch(
374
421
 
375
422
  // Walk the intercepting child's sub-tree to match remaining target parts
376
423
  const remaining = targetParts.slice(matchIdx + 1);
377
- const chain: ManifestSegmentNode[] = [slotNode, child];
378
424
 
379
425
  if (remaining.length === 0) {
380
426
  if (child.page) {
381
- return { page: child.page, chain };
427
+ return { page: child.page, chain: [slotNode, child] };
382
428
  }
383
429
  continue;
384
430
  }
385
431
 
386
- let currentNode = child;
387
- let matched = true;
388
- for (const part of remaining) {
389
- const children = currentNode.children ?? [];
390
- let found: ManifestSegmentNode | null = null;
391
-
392
- // Static match
393
- for (const c of children) {
394
- if (c.segmentType === 'static' && c.segmentName === part) {
395
- found = c;
396
- break;
397
- }
398
- }
399
-
400
- // Dynamic match
401
- if (!found) {
402
- for (const c of children) {
403
- if (c.segmentType === 'dynamic') {
404
- found = c;
405
- break;
406
- }
407
- }
408
- }
409
-
410
- // Catch-all match
411
- if (!found) {
412
- for (const c of children) {
413
- if (c.segmentType === 'catch-all' || c.segmentType === 'optional-catch-all') {
414
- found = c;
415
- break;
416
- }
417
- }
418
- }
419
-
420
- if (!found) {
421
- matched = false;
422
- break;
423
- }
424
- chain.push(found);
425
- currentNode = found;
426
- }
427
-
428
- if (matched && currentNode.page) {
429
- return { page: currentNode.page, chain };
432
+ const result = walkSegmentTree(child, remaining, [slotNode, child]);
433
+ if (result && result.leaf.page) {
434
+ return { page: result.leaf.page, chain: result.chain };
430
435
  }
431
436
  }
432
437