@timber-js/app 0.2.0-alpha.146 → 0.2.0-alpha.148

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 (297) hide show
  1. package/dist/_chunks/{actions-OIZ8siQu.js → actions-a0RczDnB.js} +8 -34
  2. package/dist/_chunks/{actions-OIZ8siQu.js.map → actions-a0RczDnB.js.map} +1 -1
  3. package/dist/_chunks/cache-api-Co-yI-gP.js +424 -0
  4. package/dist/_chunks/cache-api-Co-yI-gP.js.map +1 -0
  5. package/dist/_chunks/{metadata-routes-BjepjefT.js → canonicalize-P41GR6tY.js} +65 -2
  6. package/dist/_chunks/canonicalize-P41GR6tY.js.map +1 -0
  7. package/dist/_chunks/{cli-schema-sync-D2IpXicg.js → cli-schema-sync-DJDdyEae.js} +157 -313
  8. package/dist/_chunks/cli-schema-sync-DJDdyEae.js.map +1 -0
  9. package/dist/_chunks/cli-schema-sync-D_IhHEUW.js +2 -0
  10. package/dist/_chunks/{define-DMVhsipD.js → define-Bssfp6ot.js} +3 -33
  11. package/dist/_chunks/define-Bssfp6ot.js.map +1 -0
  12. package/dist/_chunks/{define-C3RvHQPK.js → define-CFmvb4Bt.js} +15 -7
  13. package/dist/_chunks/define-CFmvb4Bt.js.map +1 -0
  14. package/dist/_chunks/{href-validation-DyZIzfMJ.js → href-validation-FiA_rToW.js} +2 -1
  15. package/dist/_chunks/{href-validation-DyZIzfMJ.js.map → href-validation-FiA_rToW.js.map} +1 -1
  16. package/dist/_chunks/{tracing-M_zKubhk.js → logger-CM0clugc.js} +394 -107
  17. package/dist/_chunks/logger-CM0clugc.js.map +1 -0
  18. package/dist/_chunks/navigation-root-bUDZLw74.js.map +1 -1
  19. package/dist/_chunks/plugin-context-DurqFBuY.js +2 -0
  20. package/dist/_chunks/plugin-context-GfDWsdd2.js +268 -0
  21. package/dist/_chunks/plugin-context-GfDWsdd2.js.map +1 -0
  22. package/dist/_chunks/{resolve-schema-Dj8tQloi.js → resolve-schema-3iUvBV5T.js} +3 -30
  23. package/dist/_chunks/resolve-schema-3iUvBV5T.js.map +1 -0
  24. package/dist/_chunks/{schema-bridge-DK_89h8O.js → schema-bridge-BY3QLBL7.js} +35 -2
  25. package/dist/_chunks/schema-bridge-BY3QLBL7.js.map +1 -0
  26. package/dist/_chunks/tree-match-D2l830j2.js +102 -0
  27. package/dist/_chunks/tree-match-D2l830j2.js.map +1 -0
  28. package/dist/_chunks/use-segment-params-D1CzlgBo.js.map +1 -1
  29. package/dist/_chunks/{walkers-7FBPM-hX.js → walkers-DvWDj453.js} +154 -88
  30. package/dist/_chunks/walkers-DvWDj453.js.map +1 -0
  31. package/dist/adapters/cloudflare-kv-cache.d.ts.map +1 -1
  32. package/dist/adapters/cloudflare-kv-cache.js +36 -14
  33. package/dist/adapters/cloudflare-kv-cache.js.map +1 -1
  34. package/dist/adapters/compress-module.d.ts +5 -4
  35. package/dist/adapters/compress-module.d.ts.map +1 -1
  36. package/dist/adapters/nitro.d.ts +1 -3
  37. package/dist/adapters/nitro.d.ts.map +1 -1
  38. package/dist/adapters/nitro.js +14 -134
  39. package/dist/adapters/nitro.js.map +1 -1
  40. package/dist/cache/cache-api.d.ts.map +1 -1
  41. package/dist/cache/index.d.ts +3 -1
  42. package/dist/cache/index.d.ts.map +1 -1
  43. package/dist/cache/index.js +20 -235
  44. package/dist/cache/index.js.map +1 -1
  45. package/dist/cache/invalidation-epoch.d.ts +43 -0
  46. package/dist/cache/invalidation-epoch.d.ts.map +1 -0
  47. package/dist/cache/redis-handler.d.ts +13 -0
  48. package/dist/cache/redis-handler.d.ts.map +1 -1
  49. package/dist/cache/timber-cache.d.ts +17 -9
  50. package/dist/cache/timber-cache.d.ts.map +1 -1
  51. package/dist/cli-schema-sync.d.ts +21 -1
  52. package/dist/cli-schema-sync.d.ts.map +1 -1
  53. package/dist/cli.d.ts +12 -0
  54. package/dist/cli.d.ts.map +1 -1
  55. package/dist/cli.js +26 -31
  56. package/dist/cli.js.map +1 -1
  57. package/dist/client/browser-entry/hmr.d.ts.map +1 -1
  58. package/dist/client/browser-entry/hydrate.d.ts.map +1 -1
  59. package/dist/client/browser-entry/post-hydration.d.ts.map +1 -1
  60. package/dist/client/browser-entry/router-init.d.ts.map +1 -1
  61. package/dist/client/error-boundary.d.ts.map +1 -1
  62. package/dist/client/error-boundary.js +13 -4
  63. package/dist/client/error-boundary.js.map +1 -1
  64. package/dist/client/error-reconstituter.d.ts +8 -1
  65. package/dist/client/error-reconstituter.d.ts.map +1 -1
  66. package/dist/client/history.d.ts +10 -17
  67. package/dist/client/history.d.ts.map +1 -1
  68. package/dist/client/index.js +1 -1
  69. package/dist/client/index.js.map +1 -1
  70. package/dist/client/internal.js +30 -33
  71. package/dist/client/internal.js.map +1 -1
  72. package/dist/client/navigation-api.d.ts +9 -2
  73. package/dist/client/navigation-api.d.ts.map +1 -1
  74. package/dist/client/navigation-context.d.ts +0 -11
  75. package/dist/client/navigation-context.d.ts.map +1 -1
  76. package/dist/client/navigation-root.d.ts +0 -5
  77. package/dist/client/navigation-root.d.ts.map +1 -1
  78. package/dist/client/router.d.ts.map +1 -1
  79. package/dist/client/rsc-fetch.d.ts.map +1 -1
  80. package/dist/client/use-search-params.d.ts +10 -5
  81. package/dist/client/use-search-params.d.ts.map +1 -1
  82. package/dist/codec.js +1 -1
  83. package/dist/config-validation.d.ts.map +1 -1
  84. package/dist/cookies/define-cookie.d.ts.map +1 -1
  85. package/dist/cookies/index.js +2 -4
  86. package/dist/cookies/index.js.map +1 -1
  87. package/dist/dev-tools/dev-page-shell.d.ts +1 -0
  88. package/dist/dev-tools/dev-page-shell.d.ts.map +1 -1
  89. package/dist/dev-tools/hydration-overlay.d.ts +11 -0
  90. package/dist/dev-tools/hydration-overlay.d.ts.map +1 -0
  91. package/dist/dev-tools/index.d.ts +1 -2
  92. package/dist/dev-tools/index.d.ts.map +1 -1
  93. package/dist/fonts/ast-walk.d.ts +68 -0
  94. package/dist/fonts/ast-walk.d.ts.map +1 -0
  95. package/dist/fonts/ast.d.ts +23 -11
  96. package/dist/fonts/ast.d.ts.map +1 -1
  97. package/dist/fonts/bundle.d.ts +3 -1
  98. package/dist/fonts/bundle.d.ts.map +1 -1
  99. package/dist/fonts/dev-middleware.d.ts +1 -1
  100. package/dist/fonts/dev-middleware.d.ts.map +1 -1
  101. package/dist/fonts/fallbacks.d.ts +0 -7
  102. package/dist/fonts/fallbacks.d.ts.map +1 -1
  103. package/dist/fonts/google.d.ts +4 -2
  104. package/dist/fonts/google.d.ts.map +1 -1
  105. package/dist/fonts/local.d.ts +3 -3
  106. package/dist/fonts/local.d.ts.map +1 -1
  107. package/dist/fonts/paths.d.ts +43 -0
  108. package/dist/fonts/paths.d.ts.map +1 -0
  109. package/dist/fonts/pipeline.d.ts +7 -21
  110. package/dist/fonts/pipeline.d.ts.map +1 -1
  111. package/dist/fonts/transform.d.ts.map +1 -1
  112. package/dist/fonts/virtual-modules.d.ts +1 -18
  113. package/dist/fonts/virtual-modules.d.ts.map +1 -1
  114. package/dist/index.d.ts.map +1 -1
  115. package/dist/index.js +609 -406
  116. package/dist/index.js.map +1 -1
  117. package/dist/params/index.js +1 -1
  118. package/dist/params/resolve-schema.d.ts.map +1 -1
  119. package/dist/plugin-context.d.ts +28 -5
  120. package/dist/plugin-context.d.ts.map +1 -1
  121. package/dist/plugins/cache.d.ts +13 -5
  122. package/dist/plugins/cache.d.ts.map +1 -1
  123. package/dist/plugins/dev-server.d.ts.map +1 -1
  124. package/dist/plugins/fonts.d.ts +1 -2
  125. package/dist/plugins/fonts.d.ts.map +1 -1
  126. package/dist/plugins/routing.d.ts.map +1 -1
  127. package/dist/routing/codegen-types.d.ts +4 -0
  128. package/dist/routing/codegen-types.d.ts.map +1 -1
  129. package/dist/routing/codegen.d.ts.map +1 -1
  130. package/dist/routing/convention-lint.d.ts.map +1 -1
  131. package/dist/routing/file-cache.d.ts +8 -0
  132. package/dist/routing/file-cache.d.ts.map +1 -0
  133. package/dist/routing/index.js +2 -2
  134. package/dist/routing/interception.d.ts +0 -5
  135. package/dist/routing/interception.d.ts.map +1 -1
  136. package/dist/routing/manifest-codegen.d.ts +32 -0
  137. package/dist/routing/manifest-codegen.d.ts.map +1 -0
  138. package/dist/routing/scanner.d.ts.map +1 -1
  139. package/dist/schema-bridge.d.ts +15 -0
  140. package/dist/schema-bridge.d.ts.map +1 -1
  141. package/dist/search-params/define.d.ts.map +1 -1
  142. package/dist/search-params/index.js +7 -3
  143. package/dist/search-params/index.js.map +1 -1
  144. package/dist/search-params/wrappers.d.ts.map +1 -1
  145. package/dist/segment-params/define.d.ts.map +1 -1
  146. package/dist/segment-params/index.js +1 -1
  147. package/dist/server/access-gate.d.ts.map +1 -1
  148. package/dist/server/actions.d.ts +0 -14
  149. package/dist/server/actions.d.ts.map +1 -1
  150. package/dist/server/build-manifest.d.ts +0 -29
  151. package/dist/server/build-manifest.d.ts.map +1 -1
  152. package/dist/server/canonicalize.d.ts +10 -0
  153. package/dist/server/canonicalize.d.ts.map +1 -1
  154. package/dist/server/deny-boundary.d.ts +27 -2
  155. package/dist/server/deny-boundary.d.ts.map +1 -1
  156. package/dist/server/deny-renderer.d.ts +1 -1
  157. package/dist/server/deny-renderer.d.ts.map +1 -1
  158. package/dist/server/error-boundary-wrapper.d.ts.map +1 -1
  159. package/dist/server/html-injector-core.d.ts +8 -0
  160. package/dist/server/html-injector-core.d.ts.map +1 -1
  161. package/dist/server/html-injectors.d.ts.map +1 -1
  162. package/dist/server/index.js +2 -2
  163. package/dist/server/internal.d.ts +1 -5
  164. package/dist/server/internal.d.ts.map +1 -1
  165. package/dist/server/internal.js +238 -349
  166. package/dist/server/internal.js.map +1 -1
  167. package/dist/server/node-stream-transforms.d.ts +0 -11
  168. package/dist/server/node-stream-transforms.d.ts.map +1 -1
  169. package/dist/server/render-utils.d.ts +25 -0
  170. package/dist/server/render-utils.d.ts.map +1 -0
  171. package/dist/server/route-element-builder.d.ts +11 -25
  172. package/dist/server/route-element-builder.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 +2 -18
  175. package/dist/server/rsc-entry/helpers.d.ts.map +1 -1
  176. package/dist/server/rsc-entry/index.d.ts.map +1 -1
  177. package/dist/server/rsc-entry/render-route.d.ts.map +1 -1
  178. package/dist/server/rsc-entry/rsc-payload.d.ts.map +1 -1
  179. package/dist/server/ssr-render.d.ts.map +1 -1
  180. package/dist/server/status-code-resolver.d.ts +6 -0
  181. package/dist/server/status-code-resolver.d.ts.map +1 -1
  182. package/dist/server/utils/mdx-file.d.ts +15 -0
  183. package/dist/server/utils/mdx-file.d.ts.map +1 -0
  184. package/dist/shared/href-validation.d.ts.map +1 -1
  185. package/dist/shims/image.d.ts +15 -15
  186. package/package.json +3 -3
  187. package/src/adapters/cloudflare-kv-cache.ts +56 -19
  188. package/src/adapters/compress-module.ts +27 -118
  189. package/src/adapters/nitro.ts +3 -41
  190. package/src/adapters/raw-imports.d.ts +5 -0
  191. package/src/cache/cache-api.ts +9 -1
  192. package/src/cache/index.ts +5 -2
  193. package/src/cache/invalidation-epoch.ts +93 -0
  194. package/src/cache/redis-handler.ts +33 -2
  195. package/src/cache/timber-cache.ts +109 -53
  196. package/src/cli-schema-sync.ts +123 -30
  197. package/src/cli.ts +33 -37
  198. package/src/client/browser-entry/hmr.ts +0 -11
  199. package/src/client/browser-entry/hydrate.ts +39 -5
  200. package/src/client/browser-entry/post-hydration.ts +6 -15
  201. package/src/client/browser-entry/router-init.ts +98 -3
  202. package/src/client/error-boundary.tsx +14 -0
  203. package/src/client/error-reconstituter.tsx +15 -1
  204. package/src/client/history.ts +10 -24
  205. package/src/client/navigation-api.ts +14 -9
  206. package/src/client/navigation-context.ts +1 -21
  207. package/src/client/navigation-root.tsx +0 -8
  208. package/src/client/router.ts +18 -8
  209. package/src/client/rsc-fetch.ts +7 -2
  210. package/src/client/use-search-params.ts +10 -5
  211. package/src/config-validation.ts +33 -25
  212. package/src/cookies/define-cookie.ts +2 -18
  213. package/src/dev-tools/dev-page-shell.ts +6 -2
  214. package/src/dev-tools/hydration-overlay.ts +158 -0
  215. package/src/dev-tools/index.ts +3 -16
  216. package/src/fonts/ast-walk.ts +235 -0
  217. package/src/fonts/ast.ts +91 -249
  218. package/src/fonts/bundle.ts +16 -9
  219. package/src/fonts/dev-middleware.ts +16 -5
  220. package/src/fonts/fallbacks.ts +0 -23
  221. package/src/fonts/google.ts +7 -3
  222. package/src/fonts/local.ts +7 -6
  223. package/src/fonts/paths.ts +58 -0
  224. package/src/fonts/pipeline.ts +27 -48
  225. package/src/fonts/transform.ts +27 -0
  226. package/src/fonts/virtual-modules.ts +1 -57
  227. package/src/index.ts +5 -11
  228. package/src/params/resolve-schema.ts +2 -34
  229. package/src/plugin-context.ts +95 -17
  230. package/src/plugins/cache.ts +64 -17
  231. package/src/plugins/dev-server.ts +61 -9
  232. package/src/plugins/fonts.ts +21 -6
  233. package/src/plugins/routing.ts +5 -348
  234. package/src/routing/codegen-types.ts +4 -0
  235. package/src/routing/codegen.ts +66 -8
  236. package/src/routing/convention-lint.ts +4 -3
  237. package/src/routing/file-cache.ts +23 -0
  238. package/src/routing/interception.ts +2 -21
  239. package/src/routing/manifest-codegen.ts +290 -0
  240. package/src/routing/scanner.ts +85 -80
  241. package/src/routing/schema-validation.ts +2 -2
  242. package/src/schema-bridge.ts +63 -2
  243. package/src/search-params/define.ts +34 -2
  244. package/src/search-params/wrappers.ts +21 -3
  245. package/src/segment-params/define.ts +2 -62
  246. package/src/server/access-gate.tsx +8 -0
  247. package/src/server/actions.ts +8 -31
  248. package/src/server/build-manifest.ts +0 -56
  249. package/src/server/canonicalize.ts +6 -3
  250. package/src/server/deny-boundary.ts +106 -13
  251. package/src/server/deny-renderer.ts +46 -10
  252. package/src/server/error-boundary-wrapper.ts +1 -14
  253. package/src/server/html-injector-core.ts +19 -0
  254. package/src/server/html-injectors.ts +14 -4
  255. package/src/server/internal.ts +1 -18
  256. package/src/server/node-stream-transforms.ts +5 -72
  257. package/src/server/render-utils.ts +31 -0
  258. package/src/server/route-element-builder.ts +77 -47
  259. package/src/server/route-matcher.ts +22 -6
  260. package/src/server/rsc-entry/error-renderer.ts +2 -12
  261. package/src/server/rsc-entry/helpers.ts +6 -34
  262. package/src/server/rsc-entry/index.ts +11 -0
  263. package/src/server/rsc-entry/render-route.ts +7 -9
  264. package/src/server/rsc-entry/rsc-payload.ts +2 -1
  265. package/src/server/rsc-entry/ssr-renderer.ts +2 -2
  266. package/src/server/ssr-render.ts +1 -11
  267. package/src/server/status-code-resolver.ts +3 -2
  268. package/src/server/tree-match.ts +15 -5
  269. package/src/server/utils/mdx-file.ts +22 -0
  270. package/src/shared/href-validation.ts +8 -0
  271. package/dist/_chunks/cli-schema-sync-D2IpXicg.js.map +0 -1
  272. package/dist/_chunks/cli-schema-sync-DH-M8Poy.js +0 -2
  273. package/dist/_chunks/define-C3RvHQPK.js.map +0 -1
  274. package/dist/_chunks/define-DMVhsipD.js.map +0 -1
  275. package/dist/_chunks/handler-store-BVDgyTh7.js +0 -78
  276. package/dist/_chunks/handler-store-BVDgyTh7.js.map +0 -1
  277. package/dist/_chunks/logger-Dt-jQaBO.js +0 -291
  278. package/dist/_chunks/logger-Dt-jQaBO.js.map +0 -1
  279. package/dist/_chunks/metadata-routes-BjepjefT.js.map +0 -1
  280. package/dist/_chunks/resolve-schema-Dj8tQloi.js.map +0 -1
  281. package/dist/_chunks/schema-bridge-DK_89h8O.js.map +0 -1
  282. package/dist/_chunks/tracing-M_zKubhk.js.map +0 -1
  283. package/dist/_chunks/walkers-7FBPM-hX.js.map +0 -1
  284. package/dist/_chunks/warnings-CNPXaypM.js +0 -125
  285. package/dist/_chunks/warnings-CNPXaypM.js.map +0 -1
  286. package/dist/client/link-pending-store.d.ts +0 -65
  287. package/dist/client/link-pending-store.d.ts.map +0 -1
  288. package/dist/client/nav-link-store.d.ts +0 -27
  289. package/dist/client/nav-link-store.d.ts.map +0 -1
  290. package/dist/dev-tools/warnings.d.ts +0 -109
  291. package/dist/dev-tools/warnings.d.ts.map +0 -1
  292. package/dist/server/flush.d.ts +0 -74
  293. package/dist/server/flush.d.ts.map +0 -1
  294. package/src/client/link-pending-store.ts +0 -116
  295. package/src/client/nav-link-store.ts +0 -37
  296. package/src/dev-tools/warnings.ts +0 -238
  297. package/src/server/flush.ts +0 -183
@@ -8,20 +8,7 @@
8
8
  import { TimberErrorBoundary } from '../client/error-boundary.js';
9
9
  import type { ManifestSegmentNode } from './route-matcher.js';
10
10
  import { loadModule } from './safe-load.js';
11
-
12
- /** MDX/markdown extensions — server components that cannot be passed as function props. */
13
- const MDX_EXTENSIONS = new Set(['.mdx', '.md']);
14
-
15
- /**
16
- * Check if a manifest file path ends with an MDX/markdown extension.
17
- * MDX components are server components and cannot cross the RSC→client
18
- * boundary as function props to TimberErrorBoundary.
19
- */
20
- function isMdxFilePath(filePath: string): boolean {
21
- const dotIndex = filePath.lastIndexOf('.');
22
- if (dotIndex === -1) return false;
23
- return MDX_EXTENSIONS.has(filePath.slice(dotIndex));
24
- }
11
+ import { isMdxFilePath } from './utils/mdx-file.js';
25
12
 
26
13
  /**
27
14
  * Wrap an element in error boundaries defined by a route segment.
@@ -302,6 +302,7 @@ export class FlightInjectorCore {
302
302
  private readonly errorSignal?: AbortSignal;
303
303
  private readonly decoder = new TextDecoder('utf-8', { fatal: true });
304
304
  private pullPromise: Promise<void> | null = null;
305
+ private _aborted = false;
305
306
 
306
307
  constructor(
307
308
  rscStream: ReadableStream<Uint8Array>,
@@ -363,6 +364,22 @@ export class FlightInjectorCore {
363
364
  return this.machine.state.phase === 'error' ? this.machine.state.error : null;
364
365
  }
365
366
 
367
+ /**
368
+ * Abort the pull loop and release the RSC reader. Called by stream
369
+ * wrappers when the HTML pipeline is torn down (Node `destroy()`,
370
+ * Web `cancel()`). Safe to call multiple times.
371
+ */
372
+ abort(reason?: unknown): void {
373
+ if (this._aborted) return;
374
+ this._aborted = true;
375
+ this.rscReader.cancel(reason).catch(() => {});
376
+ this.pending.length = 0;
377
+ }
378
+
379
+ get isAborted(): boolean {
380
+ return this._aborted;
381
+ }
382
+
366
383
  private async runPullLoop(onYieldDrain: () => void): Promise<void> {
367
384
  // Yield once so the first HTML shell chunk flows through the wrapper
368
385
  // before we start reading RSC data.
@@ -370,6 +387,8 @@ export class FlightInjectorCore {
370
387
 
371
388
  try {
372
389
  for (;;) {
390
+ if (this._aborted) return;
391
+
373
392
  // Guard each read with a timeout so a permanently hung RSC stream
374
393
  // eventually aborts. See design/02 §"Streaming Constraints".
375
394
  const readPromise = this.rscReader.read();
@@ -271,8 +271,11 @@ function createFlightInjectionTransform(
271
271
  if (_controller) drain(_controller);
272
272
  };
273
273
 
274
- return new TransformStream<Uint8Array, Uint8Array>({
275
- transform(chunk, controller) {
274
+ // The cast is needed because TypeScript's lib types don't include the
275
+ // `cancel` method on Transformer, even though it's part of the WHATWG
276
+ // Streams spec and supported in all target runtimes.
277
+ const transformer = {
278
+ transform(chunk: Uint8Array, controller: TransformStreamDefaultController<Uint8Array>) {
276
279
  _controller = controller;
277
280
 
278
281
  if (core.isInit) {
@@ -287,7 +290,7 @@ function createFlightInjectionTransform(
287
290
  controller.enqueue(chunk);
288
291
  if (core.pending.length > 0) drain(controller);
289
292
  },
290
- flush(controller) {
293
+ flush(controller: TransformStreamDefaultController<Uint8Array>) {
291
294
  // All HTML chunks emitted. Pull loop stops yielding.
292
295
  core.notifyHtmlDone();
293
296
 
@@ -299,7 +302,14 @@ function createFlightInjectionTransform(
299
302
  }
300
303
  return core.ensurePullLoop(onYieldDrain).then(finish);
301
304
  },
302
- });
305
+ cancel(reason: unknown) {
306
+ core.abort(reason ?? 'transform cancelled');
307
+ },
308
+ };
309
+
310
+ return new TransformStream<Uint8Array, Uint8Array>(
311
+ transformer as Transformer<Uint8Array, Uint8Array>
312
+ );
303
313
  }
304
314
 
305
315
  /**
@@ -86,10 +86,6 @@ export type {
86
86
  SlotDeniedKind,
87
87
  } from './status-code-resolver.js';
88
88
 
89
- // ── Flush Controller ─────────────────────────────────────────────────────
90
- export { flushResponse } from './flush.js';
91
- export type { FlushOptions, FlushResult, RenderFn, RenderResult } from './flush.js';
92
-
93
89
  // ── CSRF Protection ──────────────────────────────────────────────────────
94
90
  export { validateCsrf } from './csrf.js';
95
91
  export type { CsrfConfig, CsrfResult } from './csrf.js';
@@ -123,7 +119,7 @@ export type {
123
119
  } from './metadata-routes.js';
124
120
 
125
121
  // ── Server Actions (pipeline internals) ──────────────────────────────────
126
- export { executeAction, buildNoJsResponse, isRscActionRequest } from './actions.js';
122
+ export { executeAction } from './actions.js';
127
123
  export type {
128
124
  RevalidateRenderer,
129
125
  RevalidationState,
@@ -160,19 +156,6 @@ export type {
160
156
  InstrumentationErrorContext,
161
157
  } from './instrumentation.js';
162
158
 
163
- // ── Dev Warnings ─────────────────────────────────────────────────────────
164
- export {
165
- warnSuspenseWrappingChildren,
166
- warnDenyInSuspense,
167
- warnRedirectInSuspense,
168
- warnRedirectInAccess,
169
- warnStaticRequestApi,
170
- warnSlowSlotWithoutSuspense,
171
- setViteServer,
172
- WarningId,
173
- } from '../dev-tools/warnings.js';
174
- export type { DevWarningConfig } from '../dev-tools/warnings.js';
175
-
176
159
  // ── Route Handler ────────────────────────────────────────────────────────
177
160
  export { handleRouteRequest, resolveAllowedMethods } from './route-handler.js';
178
161
  export type { RouteModule, RouteHandler, HttpMethod } from './route-handler.js';
@@ -20,7 +20,6 @@
20
20
 
21
21
  import { PassThrough, Transform, type Readable } from 'node:stream';
22
22
  import { pipeline } from 'node:stream/promises';
23
- import { createGzip, constants } from 'node:zlib';
24
23
 
25
24
  import {
26
25
  BufferAggregator,
@@ -35,6 +34,7 @@ import {
35
34
  } from './html-injector-core.js';
36
35
  import { logStreamingError } from './logger.js';
37
36
  import { createTickCoalescer } from './coalesce-tick.js';
37
+ import { isAbortError, NOINDEX_SCRIPT } from './render-utils.js';
38
38
 
39
39
  // ─── Buffered Transform ──────────────────────────────────────────────────────
40
40
 
@@ -229,6 +229,10 @@ export function createNodeFlightInjector(
229
229
  if (wasInit) core.ensurePullLoop(drain);
230
230
  callback();
231
231
  },
232
+ destroy(err, callback) {
233
+ core.abort(err ?? 'pipeline destroyed');
234
+ callback(err);
235
+ },
232
236
  flush(callback) {
233
237
  // All HTML chunks have been emitted. Pull loop stops yielding.
234
238
  core.notifyHtmlDone();
@@ -263,15 +267,6 @@ export function createNodeFlightInjector(
263
267
 
264
268
  // ─── Error Handling ──────────────────────────────────────────────────────────
265
269
 
266
- const NOINDEX_SCRIPT =
267
- '<script>document.head.appendChild(Object.assign(document.createElement("meta"),{name:"robots",content:"noindex"}))</script>';
268
-
269
- function isAbortError(error: unknown): boolean {
270
- if (error instanceof DOMException && error.name === 'AbortError') return true;
271
- if (error instanceof Error && error.name === 'AbortError') return true;
272
- return false;
273
- }
274
-
275
270
  /**
276
271
  * Run the Node SSR post-processing pipeline with post-shell error handling.
277
272
  *
@@ -334,65 +329,3 @@ export function runNodePipelineWithErrorHandling(
334
329
 
335
330
  return output;
336
331
  }
337
-
338
- // ─── Compression ─────────────────────────────────────────────────────────────
339
-
340
- const COMPRESSIBLE_TYPES = new Set([
341
- 'text/html',
342
- 'text/css',
343
- 'text/plain',
344
- 'text/xml',
345
- 'text/javascript',
346
- 'text/x-component',
347
- 'application/json',
348
- 'application/javascript',
349
- 'application/xml',
350
- 'application/xhtml+xml',
351
- 'application/rss+xml',
352
- 'image/svg+xml',
353
- ]);
354
-
355
- /**
356
- * Create a Node.js gzip Transform using native node:zlib.
357
- *
358
- * Uses `createGzip()` which is backed by C++ zlib — significantly faster
359
- * than the Web Streams `CompressionStream` API (which is a JS wrapper
360
- * around the same zlib but with per-chunk Promise overhead).
361
- *
362
- * Returns null if the response shouldn't be compressed (wrong content type,
363
- * client doesn't accept gzip, already encoded, etc.).
364
- */
365
- export function createNodeGzipCompressor(
366
- requestHeaders: Headers,
367
- responseHeaders: Headers
368
- ): Transform | null {
369
- // Check Accept-Encoding
370
- const acceptEncoding = requestHeaders.get('accept-encoding') || '';
371
- if (!acceptEncoding.includes('gzip')) return null;
372
-
373
- // Check content type is compressible
374
- const contentType = responseHeaders.get('content-type') || '';
375
- const mimeType = contentType.split(';')[0].trim().toLowerCase();
376
- if (!COMPRESSIBLE_TYPES.has(mimeType)) return null;
377
-
378
- // Don't double-compress
379
- if (responseHeaders.has('content-encoding')) return null;
380
-
381
- // Set response headers for gzip
382
- responseHeaders.set('content-encoding', 'gzip');
383
- responseHeaders.delete('content-length');
384
- const existingVary = responseHeaders.get('vary');
385
- if (existingVary) {
386
- if (!existingVary.toLowerCase().includes('accept-encoding')) {
387
- responseHeaders.set('vary', existingVary + ', Accept-Encoding');
388
- }
389
- } else {
390
- responseHeaders.set('vary', 'Accept-Encoding');
391
- }
392
-
393
- // Z_SYNC_FLUSH ensures each chunk is flushed to the output immediately.
394
- // Without it, gzip buffers internally and the browser doesn't receive
395
- // the HTML shell until the gzip stream closes — breaking streaming.
396
- // ~2–5% size overhead vs Z_NO_FLUSH but preserves correct streaming.
397
- return createGzip({ flush: constants.Z_SYNC_FLUSH });
398
- }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Shared render-phase utilities — used by both the Node.js and Web
3
+ * stream wrappers. Pure functions only; no stream-shape dependencies.
4
+ *
5
+ * Consolidated from duplicated copies in node-stream-transforms.ts,
6
+ * ssr-render.ts, and rsc-entry/helpers.ts. See TIM-1071 item 1.
7
+ */
8
+
9
+ /**
10
+ * Check if an error is an abort error (connection closed by client).
11
+ *
12
+ * When the browser aborts a request (page refresh, navigation away),
13
+ * the AbortSignal fires and React/streams throw an AbortError. This
14
+ * is not an application error — suppress it from error boundaries and logs.
15
+ */
16
+ export function isAbortError(error: unknown): boolean {
17
+ if (error instanceof DOMException && error.name === 'AbortError') return true;
18
+ if (error instanceof Error && error.name === 'AbortError') return true;
19
+ return false;
20
+ }
21
+
22
+ /**
23
+ * Inline script that adds `<meta name="robots" content="noindex">` to the
24
+ * document head. Injected when a render-phase error replaces the shell so
25
+ * search engines don't index error pages.
26
+ *
27
+ * MUST be byte-identical across all stream wrappers (Node and Web) to
28
+ * ensure consistent Content-Length headers.
29
+ */
30
+ export const NOINDEX_SCRIPT =
31
+ '<script>document.head.appendChild(Object.assign(document.createElement("meta"),{name:"robots",content:"noindex"}))</script>';
@@ -6,12 +6,11 @@
6
6
  * rsc-entry.ts under the 500-line limit.
7
7
  *
8
8
  * This module handles:
9
- * 1. Loading page/layout components from the segment chain
10
- * 2. Collecting access.ts checks (executed inside render by AccessPreRunner)
11
- * 3. Resolving metadata (static object or async function, both exported as `metadata`)
9
+ * 1. Running access.ts checks eagerly to gate metadata resolution (TIM-1027)
10
+ * 2. Loading page/layout components from the segment chain
11
+ * 3. Resolving metadata (skipped for denied segments to prevent side effects)
12
12
  * 4. Building the React element tree (page → error boundaries → access gates → layouts)
13
13
  * 5. Resolving parallel slots
14
- * 6. Wrapping the tree with AccessPreRunner for React.cache-scoped access checks
15
14
  *
16
15
  * See design/02-rendering-pipeline.md, design/04-authorization.md
17
16
  */
@@ -154,24 +153,6 @@ export interface RouteElementResult {
154
153
  skippedSegments: string[];
155
154
  }
156
155
 
157
- /**
158
- * Wraps a DenySignal or RedirectSignal with the layout components loaded
159
- * so far, enabling the caller to render deny pages inside the layout shell.
160
- *
161
- * @deprecated No longer thrown by buildRouteElement since TIM-662. Access
162
- * checks now run inside AccessPreRunner during renderToReadableStream, and
163
- * signals are caught by onError. Kept for backward compat with external code.
164
- */
165
- export class RouteSignalWithContext extends Error {
166
- constructor(
167
- public readonly signal: DenySignal | RedirectSignal,
168
- public readonly layoutComponents: LayoutComponentEntry[],
169
- public readonly segments: ManifestSegmentNode[]
170
- ) {
171
- super(signal.message);
172
- }
173
- }
174
-
175
156
  // ─── Module Processing Helpers ─────────────────────────────────────────────
176
157
 
177
158
  /**
@@ -235,16 +216,18 @@ function extractDeferSuspenseFor(mod: Record<string, unknown>, current: number):
235
216
  /**
236
217
  * Build a React element tree from a matched route.
237
218
  *
238
- * Loads modules, runs access checks, resolves metadata, and constructs
239
- * the element tree. DenySignal and RedirectSignal propagate to the caller
240
- * for HTTP-level handling.
219
+ * Runs access checks eagerly to gate metadata resolution, then loads
220
+ * modules, resolves metadata (skipping denied segments), and constructs
221
+ * the element tree. DenySignal and RedirectSignal from access are stored
222
+ * as AccessGate verdicts for synchronous replay during render.
241
223
  *
242
224
  * Does NOT serialize to RSC Flight — the caller decides whether to render
243
225
  * to a stream or use the element directly (e.g., for action revalidation).
244
226
  *
245
- * Access checks are collected but NOT executed here. They run inside
246
- * AccessPreRunner during renderToReadableStream so that access.ts and
247
- * render components share the same React.cache scope (TIM-662).
227
+ * For passing segments, AccessGate re-runs access during render so that
228
+ * React.cache is populated for layout dedup (TIM-662). For denied
229
+ * segments, the pre-computed verdict is replayed — no re-execution,
230
+ * no metadata side effects, no head leak (TIM-1027).
248
231
  */
249
232
  export async function buildRouteElement(
250
233
  req: Request,
@@ -255,7 +238,46 @@ export async function buildRouteElement(
255
238
  ): Promise<RouteElementResult> {
256
239
  const segments = match.segments;
257
240
 
258
- // Load all modules along the segment chain
241
+ // ── Access pre-pass ──────────────────────────────────────────────────
242
+ // Run access checks eagerly to determine which segments are denied.
243
+ // This gates metadata() resolution: denied segments' metadata functions
244
+ // are never called, preventing unauthorized side effects (DB queries)
245
+ // and metadata content leaking into the <head> of denied responses.
246
+ //
247
+ // Verdicts are passed to AccessGate for synchronous replay (DenySignal
248
+ // renders the deny page in-tree; RedirectSignal propagates). For PASSING
249
+ // segments, no verdict is passed — AccessGate re-runs access during
250
+ // renderToReadableStream to populate React.cache for layout dedup.
251
+ //
252
+ // See TIM-1027, design/04-authorization.md, design/13-security.md.
253
+ const accessVerdicts = new Map<number, DenySignal | RedirectSignal>();
254
+ let firstDeniedIndex = Infinity;
255
+
256
+ for (let i = 0; i < segments.length; i++) {
257
+ const segment = segments[i];
258
+ if (!segment.access || i >= firstDeniedIndex) continue;
259
+
260
+ try {
261
+ const accessMod = await loadModule(segment.access);
262
+ const accessFn = accessMod.default as (() => Promise<void>) | undefined;
263
+ if (accessFn) {
264
+ await withSpan(
265
+ 'timber.access.pre',
266
+ { 'timber.segment': segment.segmentName ?? 'unknown' },
267
+ () => accessFn()
268
+ );
269
+ }
270
+ } catch (error: unknown) {
271
+ if (error instanceof DenySignal || error instanceof RedirectSignal) {
272
+ accessVerdicts.set(i, error);
273
+ firstDeniedIndex = i;
274
+ } else {
275
+ throw error;
276
+ }
277
+ }
278
+ }
279
+
280
+ // ── Module loading + metadata resolution ─────────────────────────────
259
281
  const metadataEntries: Array<{ metadata: Metadata; isPage: boolean }> = [];
260
282
  const layoutComponents: LayoutComponentEntry[] = [];
261
283
  let PageComponent: ((...args: unknown[]) => unknown) | null = null;
@@ -264,6 +286,7 @@ export async function buildRouteElement(
264
286
  for (let i = 0; i < segments.length; i++) {
265
287
  const segment = segments[i];
266
288
  const isLeaf = i === segments.length - 1;
289
+ const isDenied = i >= firstDeniedIndex;
267
290
 
268
291
  // Load layout
269
292
  if (segment.layout) {
@@ -279,9 +302,11 @@ export async function buildRouteElement(
279
302
  // middleware and rendering. See coerceSegmentParams() in pipeline.ts.
280
303
 
281
304
  rejectLegacyGenerateMetadata(mod, segment.layout.filePath ?? segment.urlPath);
282
- const layoutMetadata = await extractMetadata(mod, segment);
283
- if (layoutMetadata) {
284
- metadataEntries.push({ metadata: layoutMetadata, isPage: false });
305
+ if (!isDenied) {
306
+ const layoutMetadata = await extractMetadata(mod, segment);
307
+ if (layoutMetadata) {
308
+ metadataEntries.push({ metadata: layoutMetadata, isPage: false });
309
+ }
285
310
  }
286
311
  deferSuspenseFor = extractDeferSuspenseFor(mod, deferSuspenseFor);
287
312
  }
@@ -297,9 +322,11 @@ export async function buildRouteElement(
297
322
  PageComponent = mod.default as (...args: unknown[]) => unknown;
298
323
  }
299
324
  rejectLegacyGenerateMetadata(mod, segment.page.filePath ?? segment.urlPath);
300
- const pageMetadata = await extractMetadata(mod, segment);
301
- if (pageMetadata) {
302
- metadataEntries.push({ metadata: pageMetadata, isPage: true });
325
+ if (!isDenied) {
326
+ const pageMetadata = await extractMetadata(mod, segment);
327
+ if (pageMetadata) {
328
+ metadataEntries.push({ metadata: pageMetadata, isPage: true });
329
+ }
303
330
  }
304
331
  deferSuspenseFor = extractDeferSuspenseFor(mod, deferSuspenseFor);
305
332
  }
@@ -317,16 +344,6 @@ export async function buildRouteElement(
317
344
  );
318
345
  }
319
346
 
320
- // Access checks are NOT run here. AccessGate components in the element
321
- // tree call accessFn directly during renderToReadableStream, sharing the
322
- // same React.cache scope as layout/page components. A requireUser() call
323
- // in access.ts populates React.cache; the same call in a layout is a hit.
324
- //
325
- // Previously (before TIM-662), access checks ran eagerly in a pre-render
326
- // loop OUTSIDE renderToReadableStream, breaking React.cache dedup.
327
- //
328
- // See design/04-authorization.md §"Pre-Render Pass and Verdict Replay"
329
-
330
347
  // Build deny page fallback chains for each segment position.
331
348
  // When AccessGate or PageDenyBoundary catches a DenySignal, they render
332
349
  // the matching deny page in-tree instead of throwing into React Flight.
@@ -359,8 +376,11 @@ export async function buildRouteElement(
359
376
  ? new URL(requestUrl.origin)
360
377
  : resolvedMetadata.metadataBase;
361
378
 
362
- for (const segment of segments) {
379
+ for (let si = 0; si < segments.length; si++) {
380
+ const segment = segments[si];
363
381
  if (!segment.metadataRoutes) continue;
382
+ // Skip auto-linking for denied segments — same gate as metadata().
383
+ if (si >= firstDeniedIndex) continue;
364
384
  for (const baseName of Object.keys(segment.metadataRoutes)) {
365
385
  const convention = METADATA_ROUTE_CONVENTIONS[baseName];
366
386
  if (!convention) continue;
@@ -461,7 +481,7 @@ export async function buildRouteElement(
461
481
  //
462
482
  // When clientStateTree is provided (from X-Timber-State-Tree header on
463
483
  // client navigation), sync layouts the client already has are skipped.
464
- // Access.ts already ran for ALL segments in the pre-render loop above.
484
+ // Access.ts was pre-checked eagerly above for metadata gating (TIM-1027).
465
485
  // See design/19-client-navigation.md §"X-Timber-State-Tree Header"
466
486
  //
467
487
  // hasRenderedLayoutBelow tracks whether a non-skipped layout has been
@@ -517,10 +537,15 @@ export async function buildRouteElement(
517
537
  const accessMod = await loadModule(segment.access);
518
538
  const accessFn = accessMod.default as (() => unknown) | undefined;
519
539
  if (accessFn) {
540
+ // Pass verdict for denied/redirected segments so AccessGate replays
541
+ // without re-execution. Passing segments omit verdict so AccessGate
542
+ // re-runs access during render for React.cache population.
543
+ const verdict = accessVerdicts.get(i);
520
544
  element = h(AccessGate, {
521
545
  accessFn,
522
546
  segmentName: segment.segmentName,
523
547
  denyPages: denyPageChains.get(i),
548
+ ...(verdict ? { verdict } : {}),
524
549
  children: element,
525
550
  });
526
551
  }
@@ -661,10 +686,15 @@ export async function buildRouteElement(
661
686
  const accessMod = await loadModule(segment.access);
662
687
  const accessFn = accessMod.default as (() => unknown) | undefined;
663
688
  if (accessFn) {
689
+ // Pass verdict for denied/redirected segments so AccessGate replays
690
+ // without re-execution. Passing segments omit verdict so AccessGate
691
+ // re-runs access during render for React.cache population.
692
+ const verdict = accessVerdicts.get(i);
664
693
  element = h(AccessGate, {
665
694
  accessFn,
666
695
  segmentName: segment.segmentName,
667
696
  denyPages: denyPageChains.get(i),
697
+ ...(verdict ? { verdict } : {}),
668
698
  children: element,
669
699
  });
670
700
  }
@@ -185,7 +185,7 @@ function matchMetadataPathname(
185
185
  // Non-nestable routes (robots, manifest, favicon) only serve from root
186
186
  if (!convention.nestable && segment !== root) return null;
187
187
 
188
- const file = findMetadataRouteInSegment(segment, baseName);
188
+ const file = findMetadataRouteInSegment(segment, baseName, convention.nestable);
189
189
  if (!file) return null;
190
190
 
191
191
  const ext = file.filePath.slice(file.filePath.lastIndexOf('.') + 1);
@@ -205,19 +205,35 @@ function matchMetadataPathname(
205
205
  }
206
206
 
207
207
  /**
208
- * Find a metadata route file in a segment, including transparent group children.
209
- * Groups don't add URL depth, so their metadata routes are reachable at the parent's path.
208
+ * Find a metadata route file in a segment, including children that match
209
+ * zero URL segments at the segment's own path:
210
+ *
211
+ * - Group children are transparent — they don't add URL depth.
212
+ * - Optional catch-all children ([[...slug]]) match zero segments, so
213
+ * `app/docs/[[...path]]/icon.tsx` serves `/docs/icon`. The non-zero
214
+ * case is handled by the tree walker (requireTerminal:false), but the
215
+ * zero-segment case never reaches the walker — the parent node accepts
216
+ * the fully-consumed prefix first, so the fallback lives here.
217
+ *
218
+ * Optional catch-all descent applies to NESTABLE conventions only. A
219
+ * non-nestable file (robots, manifest, favicon) inside `[[...slug]]` is
220
+ * not at the root — serving it there would bypass the root-only rule.
221
+ * Group descent is unconditional: groups are transparent and a group
222
+ * child of the root IS at the root.
223
+ *
224
+ * The segment's own metadata file wins over a zero-segment child's.
210
225
  */
211
226
  function findMetadataRouteInSegment(
212
227
  node: ManifestSegmentNode,
213
- baseName: string
228
+ baseName: string,
229
+ nestable: boolean
214
230
  ): ManifestFile | null {
215
231
  if (node.metadataRoutes?.[baseName]) {
216
232
  return node.metadataRoutes[baseName];
217
233
  }
218
234
  for (const child of node.children) {
219
- if (child.segmentType === 'group') {
220
- const found = findMetadataRouteInSegment(child, baseName);
235
+ if (child.segmentType === 'group' || (nestable && child.segmentType === 'optional-catch-all')) {
236
+ const found = findMetadataRouteInSegment(child, baseName, nestable);
221
237
  if (found) return found;
222
238
  }
223
239
  }
@@ -33,6 +33,7 @@ import { ErrorReconstituter } from '../../client/error-reconstituter.js';
33
33
  import type { SerializableError } from '../../client/error-reconstituter.js';
34
34
  import { loadModule } from '../safe-load.js';
35
35
  import { sourceMapError } from '../dev-source-map.js';
36
+ import { isMdxFilePath as isMdxFile } from '../utils/mdx-file.js';
36
37
 
37
38
  /**
38
39
  * A manifest file reference with lazy import and path.
@@ -44,18 +45,6 @@ export interface GlobalErrorFile {
44
45
  filePath: string;
45
46
  }
46
47
 
47
- /** MDX/markdown extensions — server components that need RSC rendering. */
48
- const MDX_EXTENSIONS = new Set(['.mdx', '.md']);
49
-
50
- /**
51
- * Check if a file path ends with an MDX/markdown extension.
52
- */
53
- function isMdxFile(filePath: string): boolean {
54
- const dotIndex = filePath.lastIndexOf('.');
55
- if (dotIndex === -1) return false;
56
- return MDX_EXTENSIONS.has(filePath.slice(dotIndex));
57
- }
58
-
59
48
  /**
60
49
  * Result of walking the segment chain for an error file.
61
50
  */
@@ -385,6 +374,7 @@ export async function renderNoMatchPage(
385
374
  deny,
386
375
  segments,
387
376
  layoutComponents,
377
+ req,
388
378
  responseHeaders,
389
379
  createDebugChannelSink
390
380
  );
@@ -5,7 +5,6 @@
5
5
  */
6
6
 
7
7
  import type { ManifestSegmentNode } from '../route-matcher.js';
8
- import { RedirectSignal } from '../primitives.js';
9
8
  import { swallow } from '../logger.js';
10
9
 
11
10
  /** RSC content type for client navigation payload requests. */
@@ -221,39 +220,12 @@ export function isRscPayloadRequest(req: Request): boolean {
221
220
  return accept.includes(RSC_CONTENT_TYPE);
222
221
  }
223
222
 
224
- /**
225
- * Build a redirect Response. For RSC payload requests (client navigation),
226
- * return 204 + X-Timber-Redirect header instead of a raw 302. The browser's
227
- * fetch with redirect: "manual" turns a 302 into an opaque redirect (status 0,
228
- * null body, inaccessible headers), which crashes createFromFetch when it
229
- * tries to call .body.getReader(). The X-Timber-Redirect header lets the
230
- * client detect the redirect and perform a soft SPA navigation.
231
- */
232
- export function buildRedirectResponse(
233
- req: Request,
234
- signal: RedirectSignal,
235
- responseHeaders: Headers
236
- ): Response {
237
- if (isRscPayloadRequest(req)) {
238
- responseHeaders.set('X-Timber-Redirect', signal.location);
239
- return new Response(null, { status: 204, headers: responseHeaders });
240
- }
241
- responseHeaders.set('Location', signal.location);
242
- return new Response(null, { status: signal.status, headers: responseHeaders });
243
- }
244
-
245
- /**
246
- * Check if an error is an abort error (connection closed by client).
247
- *
248
- * When the browser aborts a request (page refresh, navigation away),
249
- * the AbortSignal fires and React/streams throw an AbortError. This
250
- * is not an application error — suppress it from error boundaries and logs.
251
- */
252
- export function isAbortError(error: unknown): boolean {
253
- if (error instanceof DOMException && error.name === 'AbortError') return true;
254
- if (error instanceof Error && error.name === 'AbortError') return true;
255
- return false;
256
- }
223
+ // Re-export consolidated helpers from their single source of truth.
224
+ // Callers within rsc-entry/ import from this file; the implementations
225
+ // live in pipeline-helpers.ts (buildRedirectResponse) and render-utils.ts
226
+ // (isAbortError). See TIM-1071 item 1.
227
+ export { buildRedirectResponse } from '../pipeline-helpers.js';
228
+ export { isAbortError } from '../render-utils.js';
257
229
 
258
230
  // escapeHtml is now imported from server/utils/escape-html.ts
259
231
  export { escapeHtml } from '../utils/escape-html.js';
@@ -130,6 +130,16 @@ async function createRequestHandler(manifest: typeof routeManifest, runtimeConfi
130
130
  setCacheHandler(userCacheHandler as Parameters<typeof setCacheHandler>[0]);
131
131
  }
132
132
 
133
+ // Propagate renderTimeoutMs into the default singleflight so cache calls
134
+ // without a per-call timeoutMs still get deadlock protection (TIM-1032).
135
+ const renderTimeoutMs = (runtimeConfig as Record<string, unknown>).renderTimeoutMs as
136
+ | number
137
+ | undefined;
138
+ if (renderTimeoutMs) {
139
+ const { setDefaultSingleflightTimeout } = await import('../../cache/timber-cache.js');
140
+ setDefaultSingleflightTimeout(renderTimeoutMs);
141
+ }
142
+
133
143
  const matchRoute = createRouteMatcher(manifest);
134
144
  const matchMetadataRoute = createMetadataRouteMatcher(manifest);
135
145
 
@@ -369,6 +379,7 @@ async function createRequestHandler(manifest: typeof routeManifest, runtimeConfi
369
379
  deny,
370
380
  chain,
371
381
  layoutComponents,
382
+ req,
372
383
  responseHeaders,
373
384
  createDebugChannelSink
374
385
  );