@timber-js/app 0.2.0-alpha.197 → 0.2.0-alpha.199

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 (249) hide show
  1. package/dist/_chunks/{actions-BS-m5SLv.js → actions-d1hCqnU3.js} +35 -8
  2. package/dist/_chunks/actions-d1hCqnU3.js.map +1 -0
  3. package/dist/_chunks/als-registry-C6kcfprT.js.map +1 -1
  4. package/dist/_chunks/{cache-api-DqzgTEqk.js → cache-api-ByagcC-J.js} +2 -2
  5. package/dist/_chunks/{cache-api-DqzgTEqk.js.map → cache-api-ByagcC-J.js.map} +1 -1
  6. package/dist/_chunks/canonicalize-CgHoscYO.js +66 -0
  7. package/dist/_chunks/canonicalize-CgHoscYO.js.map +1 -0
  8. package/dist/_chunks/{chains-CZG7E5zg.js → chains-Bpb0W4ax.js} +3 -3
  9. package/dist/_chunks/{chains-CZG7E5zg.js.map → chains-Bpb0W4ax.js.map} +1 -1
  10. package/dist/_chunks/{cli-check-bE3H5Bjr.js → cli-check-D6VolrDV.js} +3 -3
  11. package/dist/_chunks/{cli-check-bE3H5Bjr.js.map → cli-check-D6VolrDV.js.map} +1 -1
  12. package/dist/_chunks/{cli-schema-sync-DTy_-Msq.js → cli-schema-sync-D6rO-VcS.js} +2 -2
  13. package/dist/_chunks/{cli-schema-sync-DTy_-Msq.js.map → cli-schema-sync-D6rO-VcS.js.map} +1 -1
  14. package/dist/_chunks/{convention-lint-n3RJLgww.js → convention-lint-fRkwVwEH.js} +27 -4
  15. package/dist/_chunks/convention-lint-fRkwVwEH.js.map +1 -0
  16. package/dist/_chunks/error-boundary-BfPHZjm0.js +1050 -0
  17. package/dist/_chunks/error-boundary-BfPHZjm0.js.map +1 -0
  18. package/dist/_chunks/{live-graph-BXDsdzBv.js → live-graph-D_2D32Ad.js} +3 -3
  19. package/dist/_chunks/{live-graph-BXDsdzBv.js.map → live-graph-D_2D32Ad.js.map} +1 -1
  20. package/dist/_chunks/{logger-DDirEsn7.js → logger-uLBuGKDI.js} +471 -440
  21. package/dist/_chunks/logger-uLBuGKDI.js.map +1 -0
  22. package/dist/_chunks/{navigation-root-B00jjGd5.js → navigation-context-D0TU0Jog.js} +3 -101
  23. package/dist/_chunks/navigation-context-D0TU0Jog.js.map +1 -0
  24. package/dist/_chunks/navigation-root-mHSK9psY.js +126 -0
  25. package/dist/_chunks/{navigation-root-B00jjGd5.js.map → navigation-root-mHSK9psY.js.map} +1 -1
  26. package/dist/_chunks/{poison-scan-BoDLgbix.js → poison-scan-Bm9Yyqk9.js} +2 -2
  27. package/dist/_chunks/{poison-scan-BoDLgbix.js.map → poison-scan-Bm9Yyqk9.js.map} +1 -1
  28. package/dist/_chunks/{scanner-tdFPvDYi.js → scanner-AiazgH_f.js} +6 -5
  29. package/dist/_chunks/scanner-AiazgH_f.js.map +1 -0
  30. package/dist/_chunks/{segment-keys-BhqoHiLc.js → segment-keys-lqtdookO.js} +2 -65
  31. package/dist/_chunks/segment-keys-lqtdookO.js.map +1 -0
  32. package/dist/_chunks/{ssr-data-BQGhTPAK.js → ssr-data-D6T6Y3ef.js} +4 -26
  33. package/dist/_chunks/ssr-data-D6T6Y3ef.js.map +1 -0
  34. package/dist/_chunks/state-FippDgxN.js +52 -0
  35. package/dist/_chunks/state-FippDgxN.js.map +1 -0
  36. package/dist/_chunks/status-page-marker-DwQBrLBz.js +496 -0
  37. package/dist/_chunks/status-page-marker-DwQBrLBz.js.map +1 -0
  38. package/dist/_chunks/{walkers-DNX05dC0.js → walkers-B6XUtmqK.js} +2 -2
  39. package/dist/_chunks/{walkers-DNX05dC0.js.map → walkers-B6XUtmqK.js.map} +1 -1
  40. package/dist/analyze/crawl-entry.js +2 -2
  41. package/dist/analyze/graph-command.js +2 -2
  42. package/dist/cache/index.js +1 -1
  43. package/dist/cli.js +2 -2
  44. package/dist/client/browser-entry/action-dispatch.d.ts +6 -4
  45. package/dist/client/browser-entry/action-dispatch.d.ts.map +1 -1
  46. package/dist/client/browser-entry/action-queue.d.ts +44 -0
  47. package/dist/client/browser-entry/action-queue.d.ts.map +1 -0
  48. package/dist/client/browser-entry/router-init.d.ts.map +1 -1
  49. package/dist/client/deny-last-resort.d.ts +29 -0
  50. package/dist/client/deny-last-resort.d.ts.map +1 -0
  51. package/dist/client/error-boundary.d.ts +48 -2
  52. package/dist/client/error-boundary.d.ts.map +1 -1
  53. package/dist/client/error-boundary.js +2 -2
  54. package/dist/client/history.d.ts +21 -2
  55. package/dist/client/history.d.ts.map +1 -1
  56. package/dist/client/index.js +34 -14
  57. package/dist/client/index.js.map +1 -1
  58. package/dist/client/internal.d.ts +1 -0
  59. package/dist/client/internal.d.ts.map +1 -1
  60. package/dist/client/internal.js +272 -1225
  61. package/dist/client/internal.js.map +1 -1
  62. package/dist/client/link.d.ts.map +1 -1
  63. package/dist/client/navigation-commit.d.ts +12 -19
  64. package/dist/client/navigation-commit.d.ts.map +1 -1
  65. package/dist/client/navigation-transition.d.ts +62 -11
  66. package/dist/client/navigation-transition.d.ts.map +1 -1
  67. package/dist/client/router-effects.d.ts +9 -8
  68. package/dist/client/router-effects.d.ts.map +1 -1
  69. package/dist/client/router-lifecycle.d.ts +60 -17
  70. package/dist/client/router-lifecycle.d.ts.map +1 -1
  71. package/dist/client/router-pipeline.d.ts +7 -4
  72. package/dist/client/router-pipeline.d.ts.map +1 -1
  73. package/dist/client/router-types.d.ts +63 -8
  74. package/dist/client/router-types.d.ts.map +1 -1
  75. package/dist/client/router.d.ts.map +1 -1
  76. package/dist/client/rsc-fetch.d.ts +0 -9
  77. package/dist/client/rsc-fetch.d.ts.map +1 -1
  78. package/dist/client/segment-cache.d.ts +23 -8
  79. package/dist/client/segment-cache.d.ts.map +1 -1
  80. package/dist/client/state.d.ts +16 -0
  81. package/dist/client/state.d.ts.map +1 -1
  82. package/dist/client/status-page-marker.d.ts +25 -0
  83. package/dist/client/status-page-marker.d.ts.map +1 -0
  84. package/dist/cookies/index.js +1 -1
  85. package/dist/dev-tools/holding-server.d.ts +4 -17
  86. package/dist/dev-tools/holding-server.d.ts.map +1 -1
  87. package/dist/index.d.ts.map +1 -1
  88. package/dist/index.js +74 -175
  89. package/dist/index.js.map +1 -1
  90. package/dist/plugins/dev-server.d.ts.map +1 -1
  91. package/dist/routing/codegen-write.d.ts.map +1 -1
  92. package/dist/routing/index.js +2 -2
  93. package/dist/routing/interception.d.ts +2 -2
  94. package/dist/routing/slot-placement.d.ts +2 -2
  95. package/dist/server/access-gate.d.ts +73 -1
  96. package/dist/server/access-gate.d.ts.map +1 -1
  97. package/dist/server/action-handler.d.ts.map +1 -1
  98. package/dist/server/actions.d.ts +16 -1
  99. package/dist/server/actions.d.ts.map +1 -1
  100. package/dist/server/als-registry.d.ts +3 -9
  101. package/dist/server/als-registry.d.ts.map +1 -1
  102. package/dist/server/children-interception.d.ts +1 -1
  103. package/dist/server/default-status-page.d.ts +2 -2
  104. package/dist/server/default-status-page.d.ts.map +1 -1
  105. package/dist/server/deny-boundary.d.ts +15 -9
  106. package/dist/server/deny-boundary.d.ts.map +1 -1
  107. package/dist/server/deny-renderer.d.ts.map +1 -1
  108. package/dist/server/error-boundary-wrapper.d.ts +21 -4
  109. package/dist/server/error-boundary-wrapper.d.ts.map +1 -1
  110. package/dist/server/error-response-headers.d.ts +3 -0
  111. package/dist/server/error-response-headers.d.ts.map +1 -0
  112. package/dist/server/index.js +3 -3
  113. package/dist/server/index.js.map +1 -1
  114. package/dist/server/internal.d.ts +1 -2
  115. package/dist/server/internal.d.ts.map +1 -1
  116. package/dist/server/internal.js +2339 -2506
  117. package/dist/server/internal.js.map +1 -1
  118. package/dist/server/metadata-collector.d.ts +2 -5
  119. package/dist/server/metadata-collector.d.ts.map +1 -1
  120. package/dist/server/param-coercion.d.ts +10 -3
  121. package/dist/server/param-coercion.d.ts.map +1 -1
  122. package/dist/server/pipeline-outcome.d.ts.map +1 -1
  123. package/dist/server/pipeline-phases.d.ts +11 -0
  124. package/dist/server/pipeline-phases.d.ts.map +1 -1
  125. package/dist/server/port-resolution.d.ts +3 -89
  126. package/dist/server/port-resolution.d.ts.map +1 -1
  127. package/dist/server/primitives.d.ts +38 -10
  128. package/dist/server/primitives.d.ts.map +1 -1
  129. package/dist/server/response-cache-policy.d.ts +3 -0
  130. package/dist/server/response-cache-policy.d.ts.map +1 -0
  131. package/dist/server/route-element-builder.d.ts +11 -41
  132. package/dist/server/route-element-builder.d.ts.map +1 -1
  133. package/dist/server/route-element-helpers.d.ts +12 -0
  134. package/dist/server/route-element-helpers.d.ts.map +1 -0
  135. package/dist/server/route-module-loader.d.ts +37 -0
  136. package/dist/server/route-module-loader.d.ts.map +1 -0
  137. package/dist/server/rsc-cache-key-guard.d.ts.map +1 -1
  138. package/dist/server/rsc-entry/action-middleware-runner.d.ts.map +1 -1
  139. package/dist/server/rsc-entry/error-renderer.d.ts.map +1 -1
  140. package/dist/server/rsc-entry/render-route.d.ts.map +1 -1
  141. package/dist/server/rsc-entry/rsc-payload.d.ts +22 -1
  142. package/dist/server/rsc-entry/rsc-payload.d.ts.map +1 -1
  143. package/dist/server/rsc-entry/rsc-stream.d.ts +4 -11
  144. package/dist/server/rsc-entry/rsc-stream.d.ts.map +1 -1
  145. package/dist/server/rsc-entry/ssr-renderer.d.ts.map +1 -1
  146. package/dist/server/rsc-error-envelope.d.ts +11 -0
  147. package/dist/server/rsc-error-envelope.d.ts.map +1 -0
  148. package/dist/server/skippable-prefix.d.ts +18 -15
  149. package/dist/server/skippable-prefix.d.ts.map +1 -1
  150. package/dist/server/slot-resolver.d.ts.map +1 -1
  151. package/dist/server/slot-subtree-contain.d.ts +54 -0
  152. package/dist/server/slot-subtree-contain.d.ts.map +1 -0
  153. package/dist/server/stream-utils.d.ts.map +1 -1
  154. package/dist/server/utils/element-type.d.ts +10 -0
  155. package/dist/server/utils/element-type.d.ts.map +1 -1
  156. package/dist/shared/rsc-error-envelope.d.ts +0 -9
  157. package/dist/shared/rsc-error-envelope.d.ts.map +1 -1
  158. package/dist/shared/status-reason-phrase.d.ts +26 -0
  159. package/dist/shared/status-reason-phrase.d.ts.map +1 -0
  160. package/docs/api/30-api-server.mdx +4 -2
  161. package/docs/api/31-api-client.mdx +5 -1
  162. package/docs/api/36-cli.mdx +5 -3
  163. package/docs/learn/12-error-handling.mdx +5 -1
  164. package/package.json +10 -10
  165. package/src/client/browser-entry/action-dispatch.ts +166 -99
  166. package/src/client/browser-entry/action-queue.ts +90 -0
  167. package/src/client/browser-entry/router-init.ts +60 -35
  168. package/src/client/deny-last-resort.tsx +54 -0
  169. package/src/client/error-boundary.tsx +144 -42
  170. package/src/client/history.ts +52 -3
  171. package/src/client/internal.ts +1 -0
  172. package/src/client/link.tsx +70 -35
  173. package/src/client/navigation-commit.ts +79 -27
  174. package/src/client/navigation-transition.ts +176 -127
  175. package/src/client/router-effects.ts +14 -17
  176. package/src/client/router-lifecycle.ts +181 -115
  177. package/src/client/router-pipeline.ts +94 -71
  178. package/src/client/router-types.ts +61 -7
  179. package/src/client/router.ts +147 -74
  180. package/src/client/rsc-fetch.ts +0 -13
  181. package/src/client/segment-cache.ts +43 -10
  182. package/src/client/state.ts +26 -0
  183. package/src/client/status-page-marker.tsx +32 -0
  184. package/src/dev-tools/holding-server.ts +4 -17
  185. package/src/index.ts +18 -34
  186. package/src/plugins/dev-server.ts +2 -1
  187. package/src/react-canary.d.ts +2 -0
  188. package/src/routing/codegen-write.ts +2 -0
  189. package/src/routing/interception.ts +2 -2
  190. package/src/routing/slot-placement.ts +2 -2
  191. package/src/server/access-gate.tsx +89 -21
  192. package/src/server/action-client.ts +2 -2
  193. package/src/server/action-handler.ts +23 -10
  194. package/src/server/actions.ts +81 -34
  195. package/src/server/als-registry.ts +3 -9
  196. package/src/server/children-interception.ts +1 -1
  197. package/src/server/default-status-page.ts +7 -47
  198. package/src/server/deny-boundary.ts +45 -28
  199. package/src/server/deny-renderer.ts +6 -2
  200. package/src/server/error-boundary-wrapper.ts +23 -4
  201. package/src/server/error-response-headers.ts +18 -0
  202. package/src/server/internal.ts +2 -10
  203. package/src/server/metadata-collector.ts +3 -18
  204. package/src/server/param-coercion.ts +13 -4
  205. package/src/server/pipeline-outcome.ts +35 -13
  206. package/src/server/pipeline-phases.ts +22 -16
  207. package/src/server/port-resolution.ts +3 -165
  208. package/src/server/prebuilt-builder.ts +4 -4
  209. package/src/server/primitives.ts +75 -11
  210. package/src/server/response-cache-policy.ts +45 -0
  211. package/src/server/route-element-builder.ts +149 -412
  212. package/src/server/route-element-helpers.ts +37 -0
  213. package/src/server/route-handler.ts +2 -2
  214. package/src/server/route-module-loader.ts +161 -0
  215. package/src/server/rsc-cache-key-guard.ts +2 -42
  216. package/src/server/rsc-entry/action-middleware-runner.ts +4 -4
  217. package/src/server/rsc-entry/api-handler.ts +5 -5
  218. package/src/server/rsc-entry/error-renderer.ts +3 -4
  219. package/src/server/rsc-entry/helpers.ts +1 -1
  220. package/src/server/rsc-entry/index.ts +3 -3
  221. package/src/server/rsc-entry/render-route.ts +4 -8
  222. package/src/server/rsc-entry/rsc-payload.ts +59 -42
  223. package/src/server/rsc-entry/rsc-stream.ts +48 -27
  224. package/src/server/rsc-entry/ssr-renderer.ts +6 -10
  225. package/src/server/rsc-error-envelope.ts +18 -0
  226. package/src/server/skippable-prefix.ts +105 -7
  227. package/src/server/slot-resolver.ts +43 -12
  228. package/src/server/slot-subtree-contain.ts +255 -0
  229. package/src/server/stream-utils.ts +12 -8
  230. package/src/server/utils/element-type.ts +18 -2
  231. package/src/shared/rsc-error-envelope.ts +0 -15
  232. package/src/shared/status-reason-phrase.ts +61 -0
  233. package/dist/_chunks/actions-BS-m5SLv.js.map +0 -1
  234. package/dist/_chunks/convention-lint-n3RJLgww.js.map +0 -1
  235. package/dist/_chunks/error-boundary-BvRCCmbN.js +0 -353
  236. package/dist/_chunks/error-boundary-BvRCCmbN.js.map +0 -1
  237. package/dist/_chunks/logger-DDirEsn7.js.map +0 -1
  238. package/dist/_chunks/mdx-file-CXyHGUpS.js +0 -25
  239. package/dist/_chunks/mdx-file-CXyHGUpS.js.map +0 -1
  240. package/dist/_chunks/router-ref-8gr8qsxN.js +0 -28
  241. package/dist/_chunks/router-ref-8gr8qsxN.js.map +0 -1
  242. package/dist/_chunks/rsc-error-envelope-tT5PJs4q.js +0 -40
  243. package/dist/_chunks/rsc-error-envelope-tT5PJs4q.js.map +0 -1
  244. package/dist/_chunks/scanner-tdFPvDYi.js.map +0 -1
  245. package/dist/_chunks/segment-keys-BhqoHiLc.js.map +0 -1
  246. package/dist/_chunks/ssr-data-BQGhTPAK.js.map +0 -1
  247. package/dist/server/tree-builder.d.ts +0 -150
  248. package/dist/server/tree-builder.d.ts.map +0 -1
  249. package/src/server/tree-builder.ts +0 -313
@@ -20,11 +20,11 @@ import { createElement, Fragment } from 'react';
20
20
  import { withSpan } from './tracing.ts';
21
21
  import type { RouteMatch } from './pipeline.ts';
22
22
  import type { ManifestSegmentNode } from './route-matcher.ts';
23
- import type { Metadata } from './types.ts';
24
- import { DenySignal, RedirectSignal } from './primitives.ts';
23
+ import { isDenySignal, DenySignal } from './primitives.ts';
24
+ import { isClientReference } from './utils/element-type.ts';
25
+ import { loadRouteModules, type MetadataSource, type AccessFn } from './route-module-loader.ts';
25
26
  import { MetadataCollector, MetadataHead } from './metadata-collector.ts';
26
27
  import { AccessGate } from './access-gate.tsx';
27
- import { requestContextAls } from './als-registry.ts';
28
28
  import { PageDenyBoundary, buildDenyPageChain, handleCaughtDeny } from './deny-boundary.ts';
29
29
  import type { DenyPageEntry } from './deny-boundary.ts';
30
30
  import {
@@ -42,115 +42,13 @@ import { SegmentOutlet } from '../client/segment-outlet.tsx';
42
42
  import { TimberErrorBoundary } from '../client/error-boundary.tsx';
43
43
  import { wrapSegmentWithErrorBoundaries } from './error-boundary-wrapper.ts';
44
44
  import type { InterceptionContext } from './pipeline.ts';
45
- import { shouldSkipSegment, type ClientStateTree } from './state-tree-diff.ts';
46
- import { truncateSkippableForChangedSlots } from './skippable-prefix.ts';
47
- import {
48
- computeSegmentKeys,
49
- computeSegmentTreePaths,
50
- computeSlotKey,
51
- } from '../routing/segment-keys.ts';
45
+ import type { ClientStateTree } from './state-tree-diff.ts';
46
+ import { computeSkippableSet } from './skippable-prefix.ts';
47
+ import { computeSegmentKeys, computeSegmentTreePaths } from '../routing/segment-keys.ts';
52
48
  import { setFirstDeniedSegmentIndex } from './deny-boundary.ts';
53
49
  import { loadModule } from './safe-load.ts';
50
+ import { replaceOutermostSegmentOutlet } from './route-element-helpers.ts';
54
51
 
55
- /**
56
- * Replace the outermost SegmentOutlet in a partial payload with its
57
- * SegmentProvider child. Walks through AccessGate wrappers (which sit
58
- * outside SegmentOutlet) to find the outlet.
59
- */
60
- function replaceOutermostSegmentOutlet(
61
- element: React.ReactElement,
62
- replacement: React.ReactElement
63
- ): React.ReactElement {
64
- if (element.type === SegmentOutlet) {
65
- return replacement;
66
- }
67
- // AccessGate and MetadataCollector wrap outside SegmentOutlet — recurse through them.
68
- const props = element.props as Record<string, unknown>;
69
- if ((element.type === AccessGate || element.type === MetadataCollector) && props.children) {
70
- const inner = replaceOutermostSegmentOutlet(props.children as React.ReactElement, replacement);
71
- /* eslint-disable react/no-children-prop -- createElement API */
72
- return createElement(element.type as React.FunctionComponent<Record<string, unknown>>, {
73
- ...props,
74
- children: inner,
75
- });
76
- /* eslint-enable react/no-children-prop */
77
- }
78
- return element;
79
- }
80
-
81
- /**
82
- * Whether a layout's returned tree still contains its children boundary.
83
- *
84
- * Layouts may forward `children` through a non-children prop or clone it with
85
- * React.Children/cloneElement. Checking only `props.children` or object
86
- * identity would incorrectly classify those layouts as suppressing their
87
- * descendants and let shell settlement complete before an async child deny.
88
- */
89
- export function containsLayoutChildren(
90
- node: unknown,
91
- boundaryType: React.ElementType,
92
- seen = new WeakSet<object>()
93
- ): boolean {
94
- if (node == null || typeof node !== 'object') return false;
95
- if (seen.has(node)) return false;
96
- seen.add(node);
97
- try {
98
- if (Array.isArray(node)) {
99
- return node.some((child) => containsLayoutChildren(child, boundaryType, seen));
100
- }
101
- if ('$$typeof' in node && 'type' in node && 'props' in node) {
102
- if (node.type === boundaryType) return true;
103
- // React's development element also exposes `_owner`; following it can
104
- // walk back into the currently rendering layout's pending props and
105
- // falsely "find" a child that the returned tree actually suppressed.
106
- return containsLayoutChildren(node.props, boundaryType, seen);
107
- }
108
- const descriptors = Object.getOwnPropertyDescriptors(node);
109
- return Object.values(descriptors).some(
110
- (descriptor) =>
111
- 'value' in descriptor &&
112
- descriptor.enumerable === true &&
113
- containsLayoutChildren(descriptor.value, boundaryType, seen)
114
- );
115
- } catch {
116
- // An opaque return value cannot safely prove that it retained the
117
- // boundary. Treat it as suppression so an uninspectable Proxy cannot
118
- // strand every descendant settlement promise and hang the response.
119
- return false;
120
- }
121
- }
122
-
123
- export function descendantSettleCallbacks(
124
- callbacks: Array<() => void>,
125
- layoutSettled: (() => void) | undefined
126
- ): Array<() => void> {
127
- // A tracked layout appended its own callback last; descendants are every
128
- // callback before it. An untracked layout appended nothing, so all current
129
- // callbacks belong to descendants. `slice(0, undefined)` is not equivalent
130
- // to `slice()` — it returns an empty array.
131
- return layoutSettled ? callbacks.slice(0, -1) : callbacks.slice();
132
- }
133
-
134
- // ─── Client Reference Detection ──────────────────────────────────────────
135
-
136
- /**
137
- * Symbol used by React Flight to mark client references.
138
- * Client references are proxy objects created by @vitejs/plugin-rsc for
139
- * 'use client' modules in the RSC environment. They must be passed to
140
- * createElement() — calling them as functions throws:
141
- * "Unexpectedly client reference export 'default' is called on server"
142
- */
143
- const CLIENT_REFERENCE_TAG = Symbol.for('react.client.reference');
144
-
145
- /**
146
- * Detect whether a component is a React client reference.
147
- * Client references have $$typeof set to Symbol.for('react.client.reference')
148
- * by registerClientReference() in the React Flight server runtime.
149
- *
150
- * Used to skip OTEL tracing wrappers that would call the component as a
151
- * function. Client components must go through createElement only — they are
152
- * serialized as references in the RSC Flight stream, not executed on the server.
153
- */
154
52
  /**
155
53
  * The `segments` array handed to a layout's `SegmentProvider` — the value
156
54
  * `useSelectedLayoutSegment(s)` reads.
@@ -173,27 +71,6 @@ export function computeLayoutSegmentPath(
173
71
  return parts.length === 0 ? [''] : ['', ...parts];
174
72
  }
175
73
 
176
- export function isClientReference(component: unknown): boolean {
177
- return (
178
- component != null &&
179
- typeof component === 'function' &&
180
- (component as unknown as Record<string, unknown>).$$typeof === CLIENT_REFERENCE_TAG
181
- );
182
- }
183
-
184
- // ─── Typed Errors ────────────────────────────────────────────────────────
185
-
186
- /**
187
- * Thrown when an app/schema.ts segment-param codec's parse() fails.
188
- * The pipeline catches this and responds with 404.
189
- */
190
- export class ParamCoercionError extends Error {
191
- constructor(message: string) {
192
- super(message);
193
- this.name = 'ParamCoercionError';
194
- }
195
- }
196
-
197
74
  /**
198
75
  * Thrown when a matched route has no page component in its leaf segment.
199
76
  * The pipeline catches this and renders the custom 404 page.
@@ -234,51 +111,73 @@ export interface RouteElementResult {
234
111
  * See design/19-client-navigation.md §"X-Timber-State-Tree Header"
235
112
  */
236
113
  skippedSegments: string[];
237
- /**
238
- * Resolves when every deny-capable shell component (PageDenyBoundary and
239
- * each TracedLayout with a deny page chain) has finished executing, OR
240
- * immediately when any of them catches a DenySignal in-tree (the HTTP
241
- * status is known at that point; inner components may never run).
242
- * Undefined when the route has no deny-capable server components.
243
- * See TIM-1045 (pages), TIM-1208 (layouts).
244
- */
245
- shellSettled?: Promise<void>;
246
114
  /**
247
115
  * Slot metadata for the X-Timber-Segments header. Contains info about
248
116
  * all parallel route slots encountered during element tree construction,
249
117
  * enabling the client to populate its segment cache with slot entries.
250
118
  */
251
119
  slotSkipInfo: SlotSkipEntry[];
120
+ /**
121
+ * The deny page chain built from the leaf segment outward to root.
122
+ * Threaded into `renderRscStream` for late addressing of nested-component
123
+ * denies in Flight's `onError`. See TIM-1450.
124
+ */
125
+ leafDenyChain?: DenyPageEntry[];
252
126
  }
253
127
 
254
- // ─── Module Processing Helpers ─────────────────────────────────────────────
128
+ // ─── Wrapping Helpers ────────────────────────────────────────────────────
255
129
 
256
130
  /**
257
- * Reject the legacy `generateMetadata` export with a helpful migration message.
258
- * Throws if the module exports `generateMetadata` instead of `metadata`.
131
+ * Wrap an element in MetadataCollector(s) for one segment's metadata sources.
132
+ *
133
+ * The loop wraps innermost-to-outermost, so the last-wrapped collector
134
+ * renders first. `.reverse()` ensures layout metadata is pushed before
135
+ * page metadata, preserving the root→page merge order required by
136
+ * resolveMetadata (page-authoritative). See TIM-1363.
259
137
  */
260
- function rejectLegacyGenerateMetadata(mod: Record<string, unknown>, filePath: string): void {
261
- if ('generateMetadata' in mod) {
262
- throw new Error(
263
- `${filePath}: "generateMetadata" is not a valid export. ` +
264
- `Export an async function named "metadata" instead.\n\n` +
265
- ` // Before\n` +
266
- ` export async function generateMetadata({ params }) { ... }\n\n` +
267
- ` // After\n` +
268
- ` export async function metadata() { ... }`
269
- );
138
+ function wrapMetadataCollectors(
139
+ element: React.ReactElement,
140
+ sources: MetadataSource[],
141
+ h: (...args: unknown[]) => React.ReactElement
142
+ ): React.ReactElement {
143
+ for (const meta of sources) {
144
+ element = h(MetadataCollector, {
145
+ metadataExport: meta.metadataExport,
146
+ segmentName: meta.segmentName,
147
+ isPage: meta.isPage,
148
+ children: element,
149
+ });
270
150
  }
151
+ return element;
271
152
  }
272
153
 
273
154
  /**
274
- * Extract `deferSuspenseFor` from a module and return the maximum
275
- * of the current value and the module's value.
155
+ * Wrap an element in an AccessGate for one segment.
156
+ *
157
+ * Skipped segments pass `skippedSegment: true` and omit `denyPages`.
158
+ * Rendered segments pass `denyPages` and omit `skippedSegment`.
276
159
  */
277
- function extractDeferSuspenseFor(mod: Record<string, unknown>, current: number): number {
278
- if (typeof mod.deferSuspenseFor === 'number' && mod.deferSuspenseFor > current) {
279
- return mod.deferSuspenseFor;
280
- }
281
- return current;
160
+ function wrapAccessGate(
161
+ element: React.ReactElement,
162
+ opts: {
163
+ accessFn: () => unknown;
164
+ segment: ManifestSegmentNode;
165
+ segmentKey: string;
166
+ segmentIndex: number;
167
+ denyPages?: DenyPageEntry[];
168
+ skippedSegment?: boolean;
169
+ },
170
+ h: (...args: unknown[]) => React.ReactElement
171
+ ): React.ReactElement {
172
+ return h(AccessGate, {
173
+ accessFn: opts.accessFn,
174
+ segmentName: opts.segment.segmentName,
175
+ segmentKey: opts.segmentKey,
176
+ segmentIndex: opts.segmentIndex,
177
+ ...(opts.skippedSegment ? { skippedSegment: true } : {}),
178
+ ...(opts.denyPages ? { denyPages: opts.denyPages } : {}),
179
+ children: element,
180
+ });
282
181
  }
283
182
 
284
183
  // ─── Builder ──────────────────────────────────────────────────────────────
@@ -286,10 +185,9 @@ function extractDeferSuspenseFor(mod: Record<string, unknown>, current: number):
286
185
  /**
287
186
  * Build a React element tree from a matched route.
288
187
  *
289
- * Runs access checks eagerly to gate metadata resolution, then loads
290
- * modules, resolves metadata (skipping denied segments), and constructs
291
- * the element tree. DenySignal and RedirectSignal from access are stored
292
- * as AccessGate verdicts for synchronous replay during render.
188
+ * Loads route modules, resolves metadata (skipping denied segments via
189
+ * AccessGate), and constructs the element tree. Access checks run during
190
+ * render inside AccessGate — denied segments' metadata never executes.
293
191
  *
294
192
  * Does NOT serialize to RSC Flight — the caller decides whether to render
295
193
  * to a stream or use the element directly (e.g., for action revalidation).
@@ -314,74 +212,8 @@ export async function buildRouteElement(
314
212
  // placed below each AccessGate — denied segments' metadata() never
315
213
  // executes because AccessGate prevents MetadataCollector from rendering.
316
214
  // No pre-render access pass needed. See TIM-1363.
317
- interface MetadataSource {
318
- metadataExport: Metadata | (() => Promise<Metadata>);
319
- segmentName: string;
320
- isPage: boolean;
321
- segmentIndex: number;
322
- }
323
- const metadataSources: MetadataSource[] = [];
324
- const layoutComponents: LayoutComponentEntry[] = [];
325
- let PageComponent: ((...args: unknown[]) => unknown) | null = null;
326
- let deferSuspenseFor = 0;
327
-
328
- for (let i = 0; i < segments.length; i++) {
329
- const segment = segments[i];
330
- const isLeaf = i === segments.length - 1;
331
-
332
- // Load layout
333
- if (segment.layout) {
334
- const mod = await loadModule(segment.layout);
335
- if (mod.default) {
336
- layoutComponents.push({
337
- component: mod.default as (...args: unknown[]) => unknown,
338
- segment,
339
- });
340
- }
341
-
342
- rejectLegacyGenerateMetadata(mod, segment.layout.filePath ?? segment.urlPath);
343
- if (mod.metadata) {
344
- metadataSources.push({
345
- metadataExport: mod.metadata as Metadata | (() => Promise<Metadata>),
346
- segmentName: segment.segmentName ?? segment.urlPath,
347
- isPage: false,
348
- segmentIndex: i,
349
- });
350
- }
351
- deferSuspenseFor = extractDeferSuspenseFor(mod, deferSuspenseFor);
352
- }
353
-
354
- // Load page (leaf segment only)
355
- if (isLeaf && segment.page) {
356
- const mod = await loadModule(segment.page);
357
-
358
- if (mod.default) {
359
- PageComponent = mod.default as (...args: unknown[]) => unknown;
360
- }
361
- rejectLegacyGenerateMetadata(mod, segment.page.filePath ?? segment.urlPath);
362
- if (mod.metadata) {
363
- metadataSources.push({
364
- metadataExport: mod.metadata as Metadata | (() => Promise<Metadata>),
365
- segmentName: segment.segmentName ?? segment.urlPath,
366
- isPage: true,
367
- segmentIndex: i,
368
- });
369
- }
370
- deferSuspenseFor = extractDeferSuspenseFor(mod, deferSuspenseFor);
371
- }
372
- }
373
-
374
- if (!PageComponent) {
375
- const segmentInfo = segments
376
- .map(
377
- (s, i) =>
378
- ` [${i}] ${s.segmentName} (page: ${s.page ? 'yes' : 'no'}, layout: ${s.layout ? 'yes' : 'no'}, children: ${s.children?.length ?? 0})${i === segments.length - 1 ? ' ← leaf' : ''}`
379
- )
380
- .join('\n');
381
- throw new NoPageComponentError(
382
- `No page component found for route: ${new URL(req.url).pathname}\nMatched segments:\n${segmentInfo}`
383
- );
384
- }
215
+ const { metadataSources, layoutComponents, PageComponent, deferSuspenseFor, accessFns } =
216
+ await loadRouteModules(segments, req.url);
385
217
 
386
218
  // Compute state-tree keys for all segments. Route groups accumulate
387
219
  // ancestor group names for globally unique keys (e.g., "/(a)/(shared)"
@@ -414,31 +246,8 @@ export async function buildRouteElement(
414
246
  }
415
247
 
416
248
  // Build element tree: page wrapped in layouts (innermost to outermost)
417
- const h = createElement as (...args: unknown[]) => React.ReactElement;
418
-
419
- // Shell settlement tracking (TIM-1045 pages, TIM-1208 layouts).
420
- // Multiple deny-capable components can suspend independently. We need all
421
- // of them to settle before committing the HTTP status.
422
- //
423
- // Each tracked component gets its own deferred. shellSettled resolves
424
- // when ALL settle (Promise.all) OR when any in-tree deny fires
425
- // (short-circuit via onDenyStatus — covers AccessGate, TracedLayout,
426
- // and PageDenyBoundary without threading callbacks into each).
427
- const shellPromises: Promise<void>[] = [];
428
- const shellSettleCallbacks: Array<() => void> = [];
429
- let resolveShortCircuit: (() => void) | undefined;
430
- const trackShellComponent = (): (() => void) => {
431
- let resolve!: () => void;
432
- shellPromises.push(new Promise<void>((r) => (resolve = r)));
433
- let settled = false;
434
- const settle = () => {
435
- if (settled) return;
436
- settled = true;
437
- resolve();
438
- };
439
- shellSettleCallbacks.push(settle);
440
- return settle;
441
- };
249
+ type CreateElementFn = (...args: unknown[]) => React.ReactElement;
250
+ const h: CreateElementFn = createElement as CreateElementFn;
442
251
 
443
252
  // Build the page element.
444
253
  // Server component pages are wrapped in PageDenyBoundary which calls
@@ -454,13 +263,11 @@ export async function buildRouteElement(
454
263
  if (isClientReference(PageComponent)) {
455
264
  element = h(PageComponent, {});
456
265
  } else if (leafDenyPages && leafDenyPages.length > 0) {
457
- const onSettled = trackShellComponent();
458
266
  element = h(PageDenyBoundary, {
459
267
  Page: PageComponent,
460
268
  route: match.segments[leafIndex]?.urlPath ?? '/',
461
269
  denyPages: leafDenyPages,
462
270
  segmentKey: segmentKeys[leafIndex],
463
- onSettled,
464
271
  });
465
272
  } else {
466
273
  // Server component page WITHOUT deny page chain — trace only
@@ -503,86 +310,15 @@ export async function buildRouteElement(
503
310
  const skippedSegments: string[] = [];
504
311
 
505
312
  // Pre-compute the contiguous skippable prefix (top-down).
506
- //
507
- // Skipped segments must form a contiguous prefix from root to avoid
508
- // non-contiguous skips that break the client merge. Walk from root
509
- // toward leaf, marking segments as skippable. Stop at the first
510
- // segment that can't be skipped (async, not in client tree, leaf).
511
- // Layoutless segments are transparent (no outlet, no layout to skip).
512
- //
513
- // After finding the prefix, validate: the first non-skipped layout
514
- // must exist in the client's state tree (the client needs a mounted
515
- // outlet there to receive the partial payload).
516
- const skippableSet = new Set<number>();
517
- if (clientStateTree) {
518
- for (let i = 0; i < segments.length; i++) {
519
- const isLeaf = i === segments.length - 1;
520
- const layoutComponent = layoutBySegment.get(segments[i]);
521
-
522
- if (shouldSkipSegment(segmentKeys[i], layoutComponent, isLeaf, clientStateTree)) {
523
- skippableSet.add(i);
524
- } else if (layoutComponent) {
525
- break;
526
- }
527
- }
528
-
529
- // Ensure there's a merge point: a non-skipped layout whose key is
530
- // in the client's state tree (so the client has a mounted outlet).
531
- // If all layouts are in the prefix, keep the innermost as merge point.
532
- if (skippableSet.size > 0) {
533
- let mergePointIndex = -1;
534
- for (let i = 0; i < segments.length; i++) {
535
- if (skippableSet.has(i)) continue;
536
- if (layoutBySegment.has(segments[i])) {
537
- mergePointIndex = i;
538
- break;
539
- }
540
- }
541
-
542
- if (mergePointIndex === -1) {
543
- // All layouts are skippable — keep the innermost as merge point
544
- for (let i = segments.length - 1; i >= 0; i--) {
545
- if (skippableSet.has(i) && layoutBySegment.has(segments[i])) {
546
- skippableSet.delete(i);
547
- mergePointIndex = i;
548
- break;
549
- }
550
- }
551
- }
552
-
553
- // Cross-section guard: at least one rendered layout below the
554
- // prefix must exist in the client's state tree (the client needs
555
- // a mounted outlet to receive the partial payload). Check ALL
556
- // non-skipped layouts, not just the merge point — async layouts
557
- // are excluded from the client state tree but may sit above a
558
- // sync layout that IS in the tree.
559
- let hasClientOutlet = false;
560
- for (let j = 0; j < segments.length; j++) {
561
- if (skippableSet.has(j)) continue;
562
- if (layoutBySegment.has(segments[j]) && clientStateTree.has(segmentKeys[j])) {
563
- hasClientOutlet = true;
564
- break;
565
- }
566
- }
567
- if (!hasClientOutlet) {
568
- skippableSet.clear();
569
- }
570
- }
571
- }
572
-
573
- // Slot-change guard (TIM-1232): if a skippable layout has parallel
574
- // slots whose content changed, the layout cannot be skipped — it must
575
- // render so its slots are included in the payload.
576
- if (skippableSet.size > 0 && clientStateTree) {
577
- truncateSkippableForChangedSlots(
578
- skippableSet,
579
- segments,
580
- segmentKeys,
581
- match,
582
- clientStateTree,
583
- req
584
- );
585
- }
313
+ // See skippable-prefix.ts for the three sub-rules and slot-change guard.
314
+ const skippableSet = computeSkippableSet(
315
+ segments,
316
+ segmentKeys,
317
+ layoutBySegment,
318
+ clientStateTree,
319
+ match,
320
+ req
321
+ );
586
322
 
587
323
  let outermostSegmentProvider: React.ReactElement | null = null;
588
324
 
@@ -630,16 +366,7 @@ export async function buildRouteElement(
630
366
  // root layout title templates). MetadataCollector wraps the element
631
367
  // inside AccessGate — if access denies, metadata is excluded.
632
368
  const skippedMeta = metadataSources.filter((s) => s.segmentIndex === i).reverse();
633
- for (const meta of skippedMeta) {
634
- const isDynamic = typeof meta.metadataExport === 'function';
635
- element = h(MetadataCollector, {
636
- metadataExport: meta.metadataExport,
637
- segmentName: meta.segmentName,
638
- isPage: meta.isPage,
639
- ...(isDynamic ? { onSettled: trackShellComponent() } : {}),
640
- children: element,
641
- });
642
- }
369
+ element = wrapMetadataCollectors(element, skippedMeta, h);
643
370
 
644
371
  // SECURITY: Even though the layout is skipped, AccessGate MUST still
645
372
  // wrap the element tree. access.ts runs on every navigation regardless
@@ -648,25 +375,28 @@ export async function buildRouteElement(
648
375
  //
649
376
  // Skipped segments omit denyPages and set skippedSegment=true. If
650
377
  // access denies at a skipped segment, AccessGate records the deny in
651
- // ALS and rethrows — the response path returns rscErrorEnvelope so the
652
- // client hard-navigates for a clean full render with correct deny page
653
- // placement. Rendering the deny page in-tree here would appear inside
654
- // the client's cached layout chrome (TIM-1074). See TIM-1363.
655
- if (segment.access) {
656
- const accessMod = await loadModule(segment.access);
657
- const accessFn = accessMod.default as (() => unknown) | undefined;
658
- if (accessFn) {
659
- const onGateSettled = trackShellComponent();
660
- element = h(AccessGate, {
378
+ // ALS and rethrows. Rendering the deny page in-tree here would appear
379
+ // inside the client's cached layout chrome (TIM-1074), so the throw
380
+ // goes to Flight's onError instead: addressed there against the leaf
381
+ // chain, its row is claimed by the boundary the client still has
382
+ // mounted at the owner (TIM-1450); with no addressable file it stays
383
+ // unplaced, and the response path answers with rscErrorEnvelope so the
384
+ // client hard-navigates for a clean full render — when the deny lands
385
+ // inside Flight's opening window. After it, the unaddressed row reaches
386
+ // the terminal boundary the last full render left mounted. See TIM-1363.
387
+ const accessFn = accessFns[i];
388
+ if (accessFn) {
389
+ element = wrapAccessGate(
390
+ element,
391
+ {
661
392
  accessFn,
662
- segmentName: segment.segmentName,
393
+ segment,
663
394
  segmentKey: segmentKeys[i],
664
395
  segmentIndex: i,
665
396
  skippedSegment: true,
666
- onSettled: onGateSettled,
667
- children: element,
668
- });
669
- }
397
+ },
398
+ h
399
+ );
670
400
  }
671
401
 
672
402
  continue;
@@ -682,16 +412,7 @@ export async function buildRouteElement(
682
412
  // before page metadata, preserving the root→page merge order required
683
413
  // by resolveMetadata (page-authoritative). See TIM-1363.
684
414
  const segmentMetadata = metadataSources.filter((s) => s.segmentIndex === i).reverse();
685
- for (const meta of segmentMetadata) {
686
- const isDynamic = typeof meta.metadataExport === 'function';
687
- element = h(MetadataCollector, {
688
- metadataExport: meta.metadataExport,
689
- segmentName: meta.segmentName,
690
- isPage: meta.isPage,
691
- ...(isDynamic ? { onSettled: trackShellComponent() } : {}),
692
- children: element,
693
- });
694
- }
415
+ element = wrapMetadataCollectors(element, segmentMetadata, h);
695
416
 
696
417
  // Wrap with error boundaries from this segment (inside layout).
697
418
  // Keep ALL error boundaries (including 4xx) — they're the safety net for
@@ -729,7 +450,7 @@ export async function buildRouteElement(
729
450
  interception,
730
451
  parentTreePath,
731
452
  destinationUrl,
732
- clientStateTree: clientStateTree as ClientStateTree | null,
453
+ clientStateTree: clientStateTree ?? null,
733
454
  slotSkipInfo,
734
455
  });
735
456
 
@@ -759,41 +480,48 @@ export async function buildRouteElement(
759
480
  // fallback path. See TIM-668, design/04-authorization.md.
760
481
  const layoutComponentRef = layoutComponent;
761
482
  const layoutDenyPages = denyPageChains.get(i);
762
- const hasLayoutDenyChain = !!layoutDenyPages && layoutDenyPages.length > 0;
763
- const onLayoutSettled = hasLayoutDenyChain ? trackShellComponent() : undefined;
764
- const descendantSettlers = descendantSettleCallbacks(shellSettleCallbacks, onLayoutSettled);
765
- const LayoutChildrenBoundary = ({ children }: { children: React.ReactNode }) => children;
766
483
  const TracedLayout = async (props: Record<string, unknown>) => {
767
484
  try {
768
- const rendered = await withSpan('timber.layout', { 'timber.segment': segmentId }, () =>
485
+ return await withSpan('timber.layout', { 'timber.segment': segmentId }, () =>
769
486
  (layoutComponentRef as (props: Record<string, unknown>) => unknown)(props)
770
487
  );
771
- // A layout is allowed to suppress children. Descendant deny-capable
772
- // components will never execute in that case, so settle their
773
- // bookkeeping rather than leaving the RSC response waiting forever.
774
- if (!containsLayoutChildren(rendered, LayoutChildrenBoundary)) {
775
- for (const settle of descendantSettlers) settle();
776
- }
777
- return rendered;
778
488
  } catch (error: unknown) {
779
489
  // A layout that denied never renders its own chrome — it threw
780
490
  // instead of returning it — so an entry this segment owns is
781
491
  // already correctly placed here. Anything owned further up is
782
492
  // hoisted (handleCaughtDeny throws). See TIM-1356.
783
- if (error instanceof DenySignal) {
493
+ if (isDenySignal(error)) {
784
494
  setFirstDeniedSegmentIndex(i);
785
495
  if (layoutDenyPages) {
786
496
  return handleCaughtDeny(layoutDenyPages, error, segmentId);
787
497
  }
788
498
  }
789
499
  throw error;
790
- } finally {
791
- onLayoutSettled?.();
792
500
  }
793
501
  };
794
502
  layoutElement = h(TracedLayout, {
795
503
  ...slotProps,
796
- children: h(LayoutChildrenBoundary, { children: element }),
504
+ children: element,
505
+ });
506
+
507
+ // A second, deny-only copy of this segment's boundaries OUTSIDE its
508
+ // layout. The copy above (inside the layout) is the one a page error
509
+ // reaches; a server component this layout renders in its own chrome
510
+ // is a sibling of `children`, so its throw never passes that copy —
511
+ // it passes this one. Claiming is by the same key, so the segment
512
+ // answers a nested deny in its chrome the way `TracedLayout` answers
513
+ // a deny in the layout body: with its own file, replacing the layout.
514
+ // A hoisted deny addressed further up passes through unclaimed.
515
+ //
516
+ // Inside SegmentProvider so a partial navigation, which splices the
517
+ // provider into the client's mounted outlet, carries it along.
518
+ // See TIM-1450, design/04-authorization.md §"Where a Deny Page Renders".
519
+ layoutElement = await wrapSegmentWithErrorBoundaries(segment, layoutElement, {
520
+ loadModule,
521
+ createElement: h,
522
+ errorBoundaryComponent: TimberErrorBoundary,
523
+ segmentKey: segmentId,
524
+ denyOnly: true,
797
525
  });
798
526
  }
799
527
 
@@ -822,21 +550,19 @@ export async function buildRouteElement(
822
550
  // Parent layouts (from outer iterations) form the shell. Access runs
823
551
  // exactly once, during render, inside React.cache scope — layouts
824
552
  // share the cache. See TIM-1363, design/04-authorization.md.
825
- if (segment.access) {
826
- const accessMod = await loadModule(segment.access);
827
- const accessFn = accessMod.default as (() => unknown) | undefined;
828
- if (accessFn) {
829
- const onGateSettled = trackShellComponent();
830
- element = h(AccessGate, {
553
+ const accessFn = accessFns[i];
554
+ if (accessFn) {
555
+ element = wrapAccessGate(
556
+ element,
557
+ {
831
558
  accessFn,
832
- segmentName: segment.segmentName,
559
+ segment,
833
560
  segmentKey: segmentKeys[i],
834
561
  segmentIndex: i,
835
562
  denyPages: denyPageChains.get(i),
836
- onSettled: onGateSettled,
837
- children: element,
838
- });
839
- }
563
+ },
564
+ h
565
+ );
840
566
  }
841
567
  }
842
568
 
@@ -849,16 +575,27 @@ export async function buildRouteElement(
849
575
  element = replaceOutermostSegmentOutlet(element, outermostSegmentProvider);
850
576
  }
851
577
 
852
- // Compose shellSettled: resolves when ALL deny-capable components settle
853
- // (Promise.all) OR immediately when any component calls setDenyStatus()
854
- // (short-circuit via onDenyStatus — covers AccessGate, TracedLayout, and
855
- // PageDenyBoundary). See TIM-1045, TIM-1208.
856
- let shellSettled: Promise<void> | undefined;
857
- if (shellPromises.length > 0) {
858
- const shortCircuit = new Promise<void>((r) => (resolveShortCircuit = r));
859
- const store = requestContextAls.getStore();
860
- if (store) store.onDenyStatus = () => resolveShortCircuit?.();
861
- shellSettled = Promise.race([Promise.all(shellPromises).then(() => {}), shortCircuit]);
578
+ // The deny chain's terminal, for a deny that streamed as an error row and
579
+ // that no segment boundary — inner or outer — could claim. With the outer
580
+ // copies above, that is one shape only: a server component in a layout's
581
+ // chrome denied and no status file exists at or above that layout. Files
582
+ // below it are not candidates (rendering them re-runs the layout that
583
+ // denied), so this renders the framework default page, as the document
584
+ // path does for a deny with no file. A non-deny row nothing claimed ends
585
+ // here too: the terminal leaves the SPA so the document request renders
586
+ // the real error page (see TimberErrorBoundary). Outside every segment
587
+ // boundary, inside the payload root so hydration agrees.
588
+ //
589
+ // Skipped on partial navigations: the terminal from the last full render
590
+ // is still mounted above the merge point, and a copy here would sit below
591
+ // the skipped ancestors' boundaries and intercept denies addressed to them.
592
+ // See TIM-1450.
593
+ if (skippedSegments.length === 0) {
594
+ element = h(TimberErrorBoundary, {
595
+ lastResort: true,
596
+ fallbackComponent: (await import('../client/deny-last-resort.tsx')).DenyLastResort,
597
+ children: element,
598
+ });
862
599
  }
863
600
 
864
601
  // Publish the request's params beside the finished tree, as the payload's
@@ -872,7 +609,7 @@ export async function buildRouteElement(
872
609
  segments,
873
610
  deferSuspenseFor,
874
611
  skippedSegments,
875
- shellSettled,
876
612
  slotSkipInfo,
613
+ leafDenyChain: leafDenyPages,
877
614
  };
878
615
  }