@timber-js/app 0.2.0-alpha.198 → 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 (246) 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-dVDi1GQz.js → cli-check-D6VolrDV.js} +3 -3
  11. package/dist/_chunks/{cli-check-dVDi1GQz.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-Ph6luW4c.js → convention-lint-fRkwVwEH.js} +25 -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/index.js +2 -2
  92. package/dist/routing/interception.d.ts +2 -2
  93. package/dist/routing/slot-placement.d.ts +2 -2
  94. package/dist/server/access-gate.d.ts +73 -1
  95. package/dist/server/access-gate.d.ts.map +1 -1
  96. package/dist/server/action-handler.d.ts.map +1 -1
  97. package/dist/server/actions.d.ts +16 -1
  98. package/dist/server/actions.d.ts.map +1 -1
  99. package/dist/server/als-registry.d.ts +3 -9
  100. package/dist/server/als-registry.d.ts.map +1 -1
  101. package/dist/server/children-interception.d.ts +1 -1
  102. package/dist/server/default-status-page.d.ts +2 -2
  103. package/dist/server/default-status-page.d.ts.map +1 -1
  104. package/dist/server/deny-boundary.d.ts +15 -9
  105. package/dist/server/deny-boundary.d.ts.map +1 -1
  106. package/dist/server/deny-renderer.d.ts.map +1 -1
  107. package/dist/server/error-boundary-wrapper.d.ts +21 -4
  108. package/dist/server/error-boundary-wrapper.d.ts.map +1 -1
  109. package/dist/server/error-response-headers.d.ts +3 -0
  110. package/dist/server/error-response-headers.d.ts.map +1 -0
  111. package/dist/server/index.js +3 -3
  112. package/dist/server/index.js.map +1 -1
  113. package/dist/server/internal.d.ts +1 -2
  114. package/dist/server/internal.d.ts.map +1 -1
  115. package/dist/server/internal.js +2339 -2506
  116. package/dist/server/internal.js.map +1 -1
  117. package/dist/server/metadata-collector.d.ts +2 -5
  118. package/dist/server/metadata-collector.d.ts.map +1 -1
  119. package/dist/server/param-coercion.d.ts +10 -3
  120. package/dist/server/param-coercion.d.ts.map +1 -1
  121. package/dist/server/pipeline-outcome.d.ts.map +1 -1
  122. package/dist/server/pipeline-phases.d.ts +11 -0
  123. package/dist/server/pipeline-phases.d.ts.map +1 -1
  124. package/dist/server/port-resolution.d.ts +3 -89
  125. package/dist/server/port-resolution.d.ts.map +1 -1
  126. package/dist/server/primitives.d.ts +38 -10
  127. package/dist/server/primitives.d.ts.map +1 -1
  128. package/dist/server/response-cache-policy.d.ts +3 -0
  129. package/dist/server/response-cache-policy.d.ts.map +1 -0
  130. package/dist/server/route-element-builder.d.ts +11 -41
  131. package/dist/server/route-element-builder.d.ts.map +1 -1
  132. package/dist/server/route-element-helpers.d.ts +12 -0
  133. package/dist/server/route-element-helpers.d.ts.map +1 -0
  134. package/dist/server/route-module-loader.d.ts +37 -0
  135. package/dist/server/route-module-loader.d.ts.map +1 -0
  136. package/dist/server/rsc-cache-key-guard.d.ts.map +1 -1
  137. package/dist/server/rsc-entry/action-middleware-runner.d.ts.map +1 -1
  138. package/dist/server/rsc-entry/error-renderer.d.ts.map +1 -1
  139. package/dist/server/rsc-entry/render-route.d.ts.map +1 -1
  140. package/dist/server/rsc-entry/rsc-payload.d.ts +22 -1
  141. package/dist/server/rsc-entry/rsc-payload.d.ts.map +1 -1
  142. package/dist/server/rsc-entry/rsc-stream.d.ts +4 -11
  143. package/dist/server/rsc-entry/rsc-stream.d.ts.map +1 -1
  144. package/dist/server/rsc-entry/ssr-renderer.d.ts.map +1 -1
  145. package/dist/server/rsc-error-envelope.d.ts +11 -0
  146. package/dist/server/rsc-error-envelope.d.ts.map +1 -0
  147. package/dist/server/skippable-prefix.d.ts +18 -15
  148. package/dist/server/skippable-prefix.d.ts.map +1 -1
  149. package/dist/server/slot-resolver.d.ts.map +1 -1
  150. package/dist/server/slot-subtree-contain.d.ts +54 -0
  151. package/dist/server/slot-subtree-contain.d.ts.map +1 -0
  152. package/dist/server/stream-utils.d.ts.map +1 -1
  153. package/dist/server/utils/element-type.d.ts +10 -0
  154. package/dist/server/utils/element-type.d.ts.map +1 -1
  155. package/dist/shared/rsc-error-envelope.d.ts +0 -9
  156. package/dist/shared/rsc-error-envelope.d.ts.map +1 -1
  157. package/dist/shared/status-reason-phrase.d.ts +26 -0
  158. package/dist/shared/status-reason-phrase.d.ts.map +1 -0
  159. package/docs/api/30-api-server.mdx +4 -2
  160. package/docs/api/31-api-client.mdx +5 -1
  161. package/docs/api/36-cli.mdx +5 -3
  162. package/docs/learn/12-error-handling.mdx +5 -1
  163. package/package.json +10 -10
  164. package/src/client/browser-entry/action-dispatch.ts +166 -99
  165. package/src/client/browser-entry/action-queue.ts +90 -0
  166. package/src/client/browser-entry/router-init.ts +60 -35
  167. package/src/client/deny-last-resort.tsx +54 -0
  168. package/src/client/error-boundary.tsx +144 -42
  169. package/src/client/history.ts +52 -3
  170. package/src/client/internal.ts +1 -0
  171. package/src/client/link.tsx +70 -35
  172. package/src/client/navigation-commit.ts +79 -27
  173. package/src/client/navigation-transition.ts +176 -127
  174. package/src/client/router-effects.ts +14 -17
  175. package/src/client/router-lifecycle.ts +181 -115
  176. package/src/client/router-pipeline.ts +94 -71
  177. package/src/client/router-types.ts +61 -7
  178. package/src/client/router.ts +147 -74
  179. package/src/client/rsc-fetch.ts +0 -13
  180. package/src/client/segment-cache.ts +43 -10
  181. package/src/client/state.ts +26 -0
  182. package/src/client/status-page-marker.tsx +32 -0
  183. package/src/dev-tools/holding-server.ts +4 -17
  184. package/src/index.ts +18 -34
  185. package/src/plugins/dev-server.ts +2 -1
  186. package/src/routing/interception.ts +2 -2
  187. package/src/routing/slot-placement.ts +2 -2
  188. package/src/server/access-gate.tsx +89 -21
  189. package/src/server/action-client.ts +2 -2
  190. package/src/server/action-handler.ts +23 -10
  191. package/src/server/actions.ts +81 -34
  192. package/src/server/als-registry.ts +3 -9
  193. package/src/server/children-interception.ts +1 -1
  194. package/src/server/default-status-page.ts +7 -47
  195. package/src/server/deny-boundary.ts +45 -28
  196. package/src/server/deny-renderer.ts +6 -2
  197. package/src/server/error-boundary-wrapper.ts +23 -4
  198. package/src/server/error-response-headers.ts +18 -0
  199. package/src/server/internal.ts +2 -10
  200. package/src/server/metadata-collector.ts +3 -18
  201. package/src/server/param-coercion.ts +13 -4
  202. package/src/server/pipeline-outcome.ts +35 -13
  203. package/src/server/pipeline-phases.ts +22 -16
  204. package/src/server/port-resolution.ts +3 -165
  205. package/src/server/prebuilt-builder.ts +4 -4
  206. package/src/server/primitives.ts +75 -11
  207. package/src/server/response-cache-policy.ts +45 -0
  208. package/src/server/route-element-builder.ts +149 -412
  209. package/src/server/route-element-helpers.ts +37 -0
  210. package/src/server/route-handler.ts +2 -2
  211. package/src/server/route-module-loader.ts +161 -0
  212. package/src/server/rsc-cache-key-guard.ts +2 -42
  213. package/src/server/rsc-entry/action-middleware-runner.ts +4 -4
  214. package/src/server/rsc-entry/api-handler.ts +5 -5
  215. package/src/server/rsc-entry/error-renderer.ts +3 -4
  216. package/src/server/rsc-entry/helpers.ts +1 -1
  217. package/src/server/rsc-entry/index.ts +3 -3
  218. package/src/server/rsc-entry/render-route.ts +4 -8
  219. package/src/server/rsc-entry/rsc-payload.ts +59 -42
  220. package/src/server/rsc-entry/rsc-stream.ts +48 -27
  221. package/src/server/rsc-entry/ssr-renderer.ts +6 -10
  222. package/src/server/rsc-error-envelope.ts +18 -0
  223. package/src/server/skippable-prefix.ts +105 -7
  224. package/src/server/slot-resolver.ts +43 -12
  225. package/src/server/slot-subtree-contain.ts +255 -0
  226. package/src/server/stream-utils.ts +12 -8
  227. package/src/server/utils/element-type.ts +18 -2
  228. package/src/shared/rsc-error-envelope.ts +0 -15
  229. package/src/shared/status-reason-phrase.ts +61 -0
  230. package/dist/_chunks/actions-BS-m5SLv.js.map +0 -1
  231. package/dist/_chunks/convention-lint-Ph6luW4c.js.map +0 -1
  232. package/dist/_chunks/error-boundary-BvRCCmbN.js +0 -353
  233. package/dist/_chunks/error-boundary-BvRCCmbN.js.map +0 -1
  234. package/dist/_chunks/logger-DDirEsn7.js.map +0 -1
  235. package/dist/_chunks/mdx-file-CXyHGUpS.js +0 -25
  236. package/dist/_chunks/mdx-file-CXyHGUpS.js.map +0 -1
  237. package/dist/_chunks/router-ref-8gr8qsxN.js +0 -28
  238. package/dist/_chunks/router-ref-8gr8qsxN.js.map +0 -1
  239. package/dist/_chunks/rsc-error-envelope-tT5PJs4q.js +0 -40
  240. package/dist/_chunks/rsc-error-envelope-tT5PJs4q.js.map +0 -1
  241. package/dist/_chunks/scanner-tdFPvDYi.js.map +0 -1
  242. package/dist/_chunks/segment-keys-BhqoHiLc.js.map +0 -1
  243. package/dist/_chunks/ssr-data-BQGhTPAK.js.map +0 -1
  244. package/dist/server/tree-builder.d.ts +0 -150
  245. package/dist/server/tree-builder.d.ts.map +0 -1
  246. package/src/server/tree-builder.ts +0 -313
@@ -1,353 +0,0 @@
1
- import { n as getSsrData, o as _setUnloading, u as unloading } from "./ssr-data-BQGhTPAK.js";
2
- import { n as getRouterOrNull } from "./router-ref-8gr8qsxN.js";
3
- import { Component, createElement } from "react";
4
- //#region src/client/unload-guard.ts
5
- /**
6
- * Page unload detection — suppresses spurious errors during page refresh/navigation.
7
- *
8
- * When the user refreshes the page or navigates away while React is still
9
- * streaming Suspense content, the aborted connection causes streaming errors.
10
- * These are not application errors — they're a side effect of the browser
11
- * tearing down the connection. This module tracks whether the page is being
12
- * unloaded so error boundaries and error handlers can suppress abort-related
13
- * errors during the unload window.
14
- *
15
- * This is the single owner of "is this document going away". Anything that
16
- * needs the answer imports `isPageUnloading()` — an unload flag maintained a
17
- * second time somewhere else is a flag that will drift out of agreement with
18
- * this one (TIM-1260).
19
- *
20
- * Mutable state is delegated to client/state.ts for singleton guarantees.
21
- * See design/18-build-system.md §"Singleton State Registry"
22
- *
23
- * See design/10-error-handling.md §"Known limitation: deny() inside Suspense and hydration"
24
- */
25
- /**
26
- * Departure is *committed*: `pagehide` has fired, or this document has called
27
- * reload() on itself. `beforeunload` alone doesn't qualify — it is
28
- * speculative, and the user can still cancel out of it.
29
- */
30
- var committed = false;
31
- if (typeof window !== "undefined") {
32
- window.addEventListener("beforeunload", () => {
33
- _setUnloading(true);
34
- setTimeout(() => {
35
- if (!committed) _setUnloading(false);
36
- }, 0);
37
- });
38
- window.addEventListener("pagehide", () => {
39
- committed = true;
40
- _setUnloading(true);
41
- });
42
- window.addEventListener("pageshow", (event) => {
43
- if (event.persisted) {
44
- committed = false;
45
- _setUnloading(false);
46
- }
47
- });
48
- }
49
- /**
50
- * Returns true if the page is currently being unloaded (user refreshed
51
- * or navigated away). Error boundaries should suppress errors in this state.
52
- */
53
- function isPageUnloading() {
54
- return unloading;
55
- }
56
- //#endregion
57
- //#region src/client/stale-client.ts
58
- /**
59
- * Stale Client — knowing when this bundle has been superseded.
60
- *
61
- * Layer 1 of version skew handling (see design/33-version-skew.md) is
62
- * proactive: the server compares `X-Timber-Deployment-Id` against the current
63
- * build and answers a stale client with `X-Timber-Reload: 1`. That covers
64
- * every RSC navigation and every server action — as long as there is a server
65
- * in the request path.
66
- *
67
- * This module is the reactive half, plus the state both halves feed. It
68
- * recognizes the two error shapes a stale client produces on its own:
69
- *
70
- * 1. **Stale client references** — React's Flight client throws
71
- * "Could not find the module …" when an RSC payload names a client module
72
- * ID that isn't in this bundle's manifest.
73
- * 2. **Chunk load failures** — a content-hashed chunk that no longer exists on
74
- * the CDN fails to `import()`. Every engine words this differently.
75
- *
76
- * It matters most for `output: 'static'`, where there is no server to send the
77
- * reload header and this is the *only* skew detection that can run.
78
- *
79
- * **Nothing here reloads the page.** A stale client is recorded, not acted on;
80
- * the router turns the next user-initiated navigation into a full document
81
- * load. See design/33-version-skew.md §"Recovery on the next navigation" for
82
- * why — a self-issued `location.reload()` cannot tell a recovery from a bounce
83
- * between two live deployments, and mid-rollout it produces exactly that loop.
84
- *
85
- * See TIM-1260, TIM-1275.
86
- */
87
- /**
88
- * Substrings that identify a failed dynamic import, lowercased for
89
- * case-insensitive matching. Engines and bundlers each word this
90
- * differently and none of them expose a stable error code.
91
- */
92
- var CHUNK_LOAD_PATTERNS = [
93
- "failed to fetch dynamically imported module",
94
- "error loading dynamically imported module",
95
- "importing a module script failed",
96
- "unable to preload css for"
97
- ];
98
- /** Substring React's Flight client uses when a client module ID is unknown. */
99
- var STALE_REFERENCE_PATTERN = "could not find the module";
100
- /**
101
- * Pull a comparable message out of an unknown thrown value. Rejections cross
102
- * enough boundaries (event handlers, `unhandledrejection`, Vite's preload
103
- * event) that the value is not reliably an `Error`.
104
- */
105
- function messageOf(error) {
106
- if (typeof error === "string") return error.toLowerCase();
107
- if (error instanceof Error) return error.message.toLowerCase();
108
- if (error && typeof error === "object" && "message" in error) {
109
- const message = error.message;
110
- if (typeof message === "string") return message.toLowerCase();
111
- }
112
- return "";
113
- }
114
- /**
115
- * True when the error is React's "client reference not in this manifest"
116
- * failure — the payload was produced by a build this bundle doesn't match.
117
- */
118
- function isStaleClientReference(error) {
119
- return messageOf(error).includes(STALE_REFERENCE_PATTERN);
120
- }
121
- /**
122
- * True when the error is a failed dynamic import or CSS preload — the
123
- * content-hashed chunk this bundle asks for is gone from the CDN.
124
- */
125
- function isChunkLoadError(error) {
126
- if (error instanceof Error && error.name === "ChunkLoadError") return true;
127
- const message = messageOf(error);
128
- return CHUNK_LOAD_PATTERNS.some((pattern) => message.includes(pattern));
129
- }
130
- /**
131
- * True once this client is known to be running against a superseded
132
- * deployment. One-way: a document that has gone stale cannot come back, since
133
- * the only way to get fresh code is to load a new document.
134
- *
135
- * In-memory on purpose. Persisting it would be wrong — the next document gets
136
- * whatever build the server hands it, and if that one is stale too it will
137
- * find out the same way.
138
- */
139
- var stale = false;
140
- /**
141
- * True once {@link initStaleClient} has seen a deployment ID — i.e. this is a
142
- * production build. False in dev, where nothing is ever stale: HMR owns code
143
- * updates, and a failed dynamic import is a real error the developer needs to
144
- * see rather than something a page load fixes.
145
- */
146
- var deployed = false;
147
- /** Whether this client has been superseded by a newer deployment. */
148
- function isClientStale() {
149
- return stale;
150
- }
151
- /**
152
- * Record that the origin has told us this client is stale — the `X-Timber-Reload`
153
- * header on an RSC or action response. Authoritative, so it is not gated on
154
- * anything: a dev server never sends the header in the first place.
155
- */
156
- function markClientStale() {
157
- stale = true;
158
- }
159
- /**
160
- * Record staleness if `error` is one of the shapes a superseded bundle
161
- * produces. Returns whether it did, so a caller mid-navigation can decide to
162
- * hard-navigate instead of surfacing the error.
163
- *
164
- * Inference, unlike {@link markClientStale}, so it is gated on being a
165
- * production build. This is the single place that gate lives — call sites
166
- * cannot see each other, and "remember to gate the next one too" is not a
167
- * guarantee.
168
- */
169
- function markStaleFromError(error) {
170
- if (!deployed) return false;
171
- if (isPageUnloading()) return false;
172
- if (!isStaleClientReference(error) && !isChunkLoadError(error)) return false;
173
- stale = true;
174
- return true;
175
- }
176
- //#endregion
177
- //#region src/client/error-boundary.tsx
178
- /**
179
- * Framework-injected React error boundary.
180
- *
181
- * Catches errors thrown by children and renders a fallback component
182
- * with the appropriate props based on error type:
183
- * - DenySignal (4xx) → { status, dangerouslyPassData }
184
- * - RenderError (5xx) → { error, digest, reset }
185
- * - Unhandled error → { error, digest: null, reset }
186
- *
187
- * The `status` prop controls which errors this boundary catches:
188
- * - Specific code (e.g. 403) → only that status
189
- * - Category (400) → any 4xx
190
- * - Category (500) → any 5xx
191
- * - Omitted → catches everything (error.tsx behavior)
192
- *
193
- * See design/10-error-handling.md §"Status-Code Files"
194
- */
195
- var _handledRedirects = /* @__PURE__ */ new WeakSet();
196
- var _pendingRedirectLocation = null;
197
- var TimberErrorBoundary = class extends Component {
198
- constructor(props) {
199
- super(props);
200
- this.state = {
201
- hasError: false,
202
- error: null
203
- };
204
- }
205
- static getDerivedStateFromError(error) {
206
- if (isPageUnloading()) return {
207
- hasError: false,
208
- error: null
209
- };
210
- return {
211
- hasError: true,
212
- error
213
- };
214
- }
215
- componentDidCatch(error) {
216
- markStaleFromError(error);
217
- }
218
- componentDidUpdate(prevProps) {
219
- if (this.state.hasError && prevProps.children !== this.props.children) this.setState({
220
- hasError: false,
221
- error: null
222
- });
223
- }
224
- /** Reset the error state so children re-render. */
225
- reset = () => {
226
- this.setState({
227
- hasError: false,
228
- error: null
229
- });
230
- };
231
- render() {
232
- if (!this.state.hasError || !this.state.error) return this.props.children;
233
- const error = this.state.error;
234
- const parsed = parseDigest(error);
235
- if (parsed?.type === "redirect") {
236
- if (typeof window === "undefined") throw error;
237
- if (!(_handledRedirects.has(error) || _pendingRedirectLocation === parsed.location)) {
238
- _handledRedirects.add(error);
239
- const router = getRouterOrNull();
240
- if (router) {
241
- _pendingRedirectLocation = parsed.location;
242
- queueMicrotask(() => {
243
- router.navigate(parsed.location, { replace: true }).catch(() => {
244
- window.location.href = parsed.location;
245
- }).finally(() => {
246
- _pendingRedirectLocation = null;
247
- });
248
- });
249
- } else window.location.href = parsed.location;
250
- }
251
- return null;
252
- }
253
- if (parsed?.type === "deny" && typeof parsed.ownerKey === "string" && parsed.ownerKey !== this.props.segmentKey) throw error;
254
- if (this.props.status != null) {
255
- const errorStatus = getErrorStatus(parsed, error);
256
- if (errorStatus == null || !statusMatches(this.props.status, errorStatus)) throw error;
257
- }
258
- if (parsed?.type === "deny") {
259
- if (this.props.fallbackElement == null || this.props.isSlotBoundary) {
260
- const ssrData = getSsrData();
261
- if (ssrData?._navContext) {
262
- ssrData._navContext._denyHandledByBoundary = true;
263
- if (!this.props.isSlotBoundary) ssrData._navContext.statusCode = parsed.status;
264
- }
265
- }
266
- }
267
- if (this.props.fallbackElement != null) return this.props.fallbackElement;
268
- if (parsed?.type === "deny") {
269
- if (this.props.status != null) return createElement(this.props.fallbackComponent, {
270
- status: parsed.status,
271
- dangerouslyPassData: parsed.data
272
- });
273
- return createElement(this.props.fallbackComponent, {
274
- error: denyError(parsed.status, error),
275
- digest: null,
276
- reset: this.reset,
277
- status: parsed.status,
278
- dangerouslyPassData: parsed.data
279
- });
280
- }
281
- const digest = parsed?.type === "render-error" ? {
282
- code: parsed.code,
283
- data: parsed.data
284
- } : null;
285
- return createElement(this.props.fallbackComponent, {
286
- error,
287
- digest,
288
- reset: this.reset
289
- });
290
- }
291
- };
292
- /**
293
- * Parse the structured digest from the error.
294
- * React sets `error.digest` from the string returned by RSC's onError.
295
- */
296
- function parseDigest(error) {
297
- const raw = error.digest;
298
- if (typeof raw !== "string") return null;
299
- try {
300
- const parsed = JSON.parse(raw);
301
- if (parsed && typeof parsed === "object" && typeof parsed.type === "string") return parsed;
302
- } catch (e) {
303
- console.debug("[timber] error digest is not valid JSON:", e instanceof Error ? e.message : e);
304
- }
305
- return null;
306
- }
307
- /**
308
- * The Error an `error.tsx` serving a deny should receive.
309
- *
310
- * The error that actually arrives here is React's, not the framework's: a
311
- * server-thrown error crossing Flight has its message and name replaced with
312
- * a generic redacted string in production, by design. An `error.tsx` written
313
- * per the docs branches on `error.message` — so handing it the redacted error
314
- * gives it nothing to branch on, while the in-tree path hands the same file a
315
- * `DenySignal` named error reading `Access denied with status <N>` (see
316
- * `renderDenyEntry`). This reconstructs that shape from the digest, which is
317
- * where the status already is.
318
- *
319
- * Derived from the status alone — no stack, no server data — so it is safe on
320
- * the client for the same reason the digest is. See design/13-security.md
321
- * §"Errors don't leak", TIM-1081.
322
- */
323
- function denyError(status, original) {
324
- if (original.name === "DenySignal") return original;
325
- const error = /* @__PURE__ */ new Error(`Access denied with status ${status}`);
326
- error.name = "DenySignal";
327
- return error;
328
- }
329
- /**
330
- * Extract the HTTP status code from a parsed digest or error message.
331
- * Falls back to message pattern matching for errors without a digest.
332
- */
333
- function getErrorStatus(parsed, error) {
334
- if (parsed?.type === "deny") return parsed.status;
335
- if (parsed?.type === "render-error") return parsed.status;
336
- if (parsed?.type === "redirect") return parsed.status;
337
- const match = error.message.match(/^Access denied with status (\d+)$/);
338
- if (match) return parseInt(match[1], 10);
339
- return 500;
340
- }
341
- /**
342
- * Check whether an error's status matches the boundary's status filter.
343
- * Category markers (400, 500) match any status in that range.
344
- */
345
- function statusMatches(boundaryStatus, errorStatus) {
346
- if (boundaryStatus === 400) return errorStatus >= 400 && errorStatus <= 499;
347
- if (boundaryStatus === 500) return errorStatus >= 500 && errorStatus <= 599;
348
- return boundaryStatus === errorStatus;
349
- }
350
- //#endregion
351
- export { markStaleFromError as i, isClientStale as n, markClientStale as r, TimberErrorBoundary as t };
352
-
353
- //# sourceMappingURL=error-boundary-BvRCCmbN.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"error-boundary-BvRCCmbN.js","names":[],"sources":["../../src/client/unload-guard.ts","../../src/client/stale-client.ts","../../src/client/error-boundary.tsx"],"sourcesContent":["/**\n * Page unload detection — suppresses spurious errors during page refresh/navigation.\n *\n * When the user refreshes the page or navigates away while React is still\n * streaming Suspense content, the aborted connection causes streaming errors.\n * These are not application errors — they're a side effect of the browser\n * tearing down the connection. This module tracks whether the page is being\n * unloaded so error boundaries and error handlers can suppress abort-related\n * errors during the unload window.\n *\n * This is the single owner of \"is this document going away\". Anything that\n * needs the answer imports `isPageUnloading()` — an unload flag maintained a\n * second time somewhere else is a flag that will drift out of agreement with\n * this one (TIM-1260).\n *\n * Mutable state is delegated to client/state.ts for singleton guarantees.\n * See design/18-build-system.md §\"Singleton State Registry\"\n *\n * See design/10-error-handling.md §\"Known limitation: deny() inside Suspense and hydration\"\n */\n\nimport { unloading, _setUnloading } from './state.ts';\n\n/**\n * Departure is *committed*: `pagehide` has fired, or this document has called\n * reload() on itself. `beforeunload` alone doesn't qualify — it is\n * speculative, and the user can still cancel out of it.\n */\nlet committed = false;\n\nif (typeof window !== 'undefined') {\n window.addEventListener('beforeunload', () => {\n _setUnloading(true);\n\n // An app with an unsaved-changes prompt can have this cancelled. Nothing\n // fires to say so, so the only signal is that the page is still running\n // afterwards — and timers are blocked while the modal is up, so this task\n // cannot run until the user has chosen. If they left, `pagehide` has\n // already committed and this is a no-op.\n setTimeout(() => {\n if (!committed) _setUnloading(false);\n }, 0);\n });\n\n // pagehide is the committed-departure event, and fires for both navigations\n // and bfcache entry. Safari in particular may skip beforeunload entirely.\n window.addEventListener('pagehide', () => {\n committed = true;\n _setUnloading(true);\n });\n\n window.addEventListener('pageshow', (event) => {\n // A bfcache restore un-does the teardown `pagehide` announced: the same\n // document is alive again, with this module's state intact. Without the\n // reset, every consumer would go on suppressing errors on a live page.\n if (event.persisted) {\n committed = false;\n _setUnloading(false);\n }\n });\n}\n\n/**\n * Returns true if the page is currently being unloaded (user refreshed\n * or navigated away). Error boundaries should suppress errors in this state.\n */\nexport function isPageUnloading(): boolean {\n return unloading;\n}\n","/**\n * Stale Client — knowing when this bundle has been superseded.\n *\n * Layer 1 of version skew handling (see design/33-version-skew.md) is\n * proactive: the server compares `X-Timber-Deployment-Id` against the current\n * build and answers a stale client with `X-Timber-Reload: 1`. That covers\n * every RSC navigation and every server action — as long as there is a server\n * in the request path.\n *\n * This module is the reactive half, plus the state both halves feed. It\n * recognizes the two error shapes a stale client produces on its own:\n *\n * 1. **Stale client references** — React's Flight client throws\n * \"Could not find the module …\" when an RSC payload names a client module\n * ID that isn't in this bundle's manifest.\n * 2. **Chunk load failures** — a content-hashed chunk that no longer exists on\n * the CDN fails to `import()`. Every engine words this differently.\n *\n * It matters most for `output: 'static'`, where there is no server to send the\n * reload header and this is the *only* skew detection that can run.\n *\n * **Nothing here reloads the page.** A stale client is recorded, not acted on;\n * the router turns the next user-initiated navigation into a full document\n * load. See design/33-version-skew.md §\"Recovery on the next navigation\" for\n * why — a self-issued `location.reload()` cannot tell a recovery from a bounce\n * between two live deployments, and mid-rollout it produces exactly that loop.\n *\n * See TIM-1260, TIM-1275.\n */\n\n// The only import: the shared unload state. Kept minimal because this module\n// is reached from the error boundary, a `'use client'` component evaluated in\n// the SSR graph too — a dependency on the client fetch layer would drag that\n// graph along with it.\nimport { isPageUnloading } from './unload-guard.ts';\n\n// ─── Error Shapes ────────────────────────────────────────────────\n\n/**\n * Substrings that identify a failed dynamic import, lowercased for\n * case-insensitive matching. Engines and bundlers each word this\n * differently and none of them expose a stable error code.\n */\nconst CHUNK_LOAD_PATTERNS = [\n // Chromium\n 'failed to fetch dynamically imported module',\n // Firefox\n 'error loading dynamically imported module',\n // Safari\n 'importing a module script failed',\n // Vite's preload helper, for the stylesheet of a lazily-imported chunk\n 'unable to preload css for',\n];\n\n/** Substring React's Flight client uses when a client module ID is unknown. */\nconst STALE_REFERENCE_PATTERN = 'could not find the module';\n\n/**\n * Pull a comparable message out of an unknown thrown value. Rejections cross\n * enough boundaries (event handlers, `unhandledrejection`, Vite's preload\n * event) that the value is not reliably an `Error`.\n */\nfunction messageOf(error: unknown): string {\n if (typeof error === 'string') return error.toLowerCase();\n if (error instanceof Error) return error.message.toLowerCase();\n if (error && typeof error === 'object' && 'message' in error) {\n const message = (error as { message: unknown }).message;\n if (typeof message === 'string') return message.toLowerCase();\n }\n return '';\n}\n\n/**\n * True when the error is React's \"client reference not in this manifest\"\n * failure — the payload was produced by a build this bundle doesn't match.\n */\nexport function isStaleClientReference(error: unknown): boolean {\n return messageOf(error).includes(STALE_REFERENCE_PATTERN);\n}\n\n/**\n * True when the error is a failed dynamic import or CSS preload — the\n * content-hashed chunk this bundle asks for is gone from the CDN.\n */\nexport function isChunkLoadError(error: unknown): boolean {\n if (error instanceof Error && error.name === 'ChunkLoadError') return true;\n const message = messageOf(error);\n return CHUNK_LOAD_PATTERNS.some((pattern) => message.includes(pattern));\n}\n\n// ─── Staleness ───────────────────────────────────────────────────\n\n/**\n * True once this client is known to be running against a superseded\n * deployment. One-way: a document that has gone stale cannot come back, since\n * the only way to get fresh code is to load a new document.\n *\n * In-memory on purpose. Persisting it would be wrong — the next document gets\n * whatever build the server hands it, and if that one is stale too it will\n * find out the same way.\n */\nlet stale = false;\n\n/**\n * True once {@link initStaleClient} has seen a deployment ID — i.e. this is a\n * production build. False in dev, where nothing is ever stale: HMR owns code\n * updates, and a failed dynamic import is a real error the developer needs to\n * see rather than something a page load fixes.\n */\nlet deployed = false;\n\n/** Whether this client has been superseded by a newer deployment. */\nexport function isClientStale(): boolean {\n return stale;\n}\n\n/**\n * Record that the origin has told us this client is stale — the `X-Timber-Reload`\n * header on an RSC or action response. Authoritative, so it is not gated on\n * anything: a dev server never sends the header in the first place.\n */\nexport function markClientStale(): void {\n stale = true;\n}\n\n/**\n * Record staleness if `error` is one of the shapes a superseded bundle\n * produces. Returns whether it did, so a caller mid-navigation can decide to\n * hard-navigate instead of surfacing the error.\n *\n * Inference, unlike {@link markClientStale}, so it is gated on being a\n * production build. This is the single place that gate lives — call sites\n * cannot see each other, and \"remember to gate the next one too\" is not a\n * guarantee.\n */\nexport function markStaleFromError(error: unknown): boolean {\n if (!deployed) return false;\n\n // A document being torn down aborts its in-flight dynamic imports, and an\n // aborted import reads exactly like a chunk the CDN has dropped. That is not\n // evidence of staleness, and the bit outlives the teardown that produced it:\n // a document entering the back-forward cache is frozen, not destroyed, so a\n // false positive recorded on the way out would cost the *restored* page its\n // client-side navigation for good. `unload-guard.ts` clears its own flag on\n // a persisted `pageshow`; this one is one-way by design, so it has to not be\n // set in the first place.\n if (isPageUnloading()) return false;\n\n if (!isStaleClientReference(error) && !isChunkLoadError(error)) return false;\n stale = true;\n return true;\n}\n\n// ─── Initialization ──────────────────────────────────────────────\n\n/**\n * The underlying error Vite attaches to a `vite:preloadError` event.\n *\n * Narrowed at runtime rather than cast: the property is Vite's, not ours, and\n * a shape change should fall back to matching the event itself rather than\n * silently reading `undefined` forever.\n */\nfunction preloadErrorPayload(event: Event): unknown {\n return 'payload' in event ? (event as Event & { payload: unknown }).payload : event;\n}\n\n/**\n * Arm the module, and register the listeners that catch stale-client errors\n * never passing through the router: a lazily imported client component, a\n * preloaded stylesheet, anything that rejects outside a navigation.\n *\n * No-op when `deploymentId` is null — that is dev. Called from the browser\n * entry before hydration, so it precedes any render or navigation that could\n * throw.\n */\nexport function initStaleClient(deploymentId: string | null): void {\n if (!deploymentId) return;\n deployed = true;\n\n // Vite dispatches this for failed chunk/CSS preloads. Note what is *not*\n // here: `preventDefault()`. Recording staleness is not recovering from this\n // error — the page is still broken until the user navigates — so Vite must\n // go on rethrowing it rather than have it silently swallowed.\n window.addEventListener('vite:preloadError', (event) => {\n markStaleFromError(preloadErrorPayload(event));\n });\n\n window.addEventListener('unhandledrejection', (event: PromiseRejectionEvent) => {\n markStaleFromError(event.reason);\n });\n}\n","'use client';\n\n/**\n * Framework-injected React error boundary.\n *\n * Catches errors thrown by children and renders a fallback component\n * with the appropriate props based on error type:\n * - DenySignal (4xx) → { status, dangerouslyPassData }\n * - RenderError (5xx) → { error, digest, reset }\n * - Unhandled error → { error, digest: null, reset }\n *\n * The `status` prop controls which errors this boundary catches:\n * - Specific code (e.g. 403) → only that status\n * - Category (400) → any 4xx\n * - Category (500) → any 5xx\n * - Omitted → catches everything (error.tsx behavior)\n *\n * See design/10-error-handling.md §\"Status-Code Files\"\n */\n\nimport { Component, createElement, type ReactNode } from 'react';\nimport { getSsrData } from './ssr-data.ts';\nimport { getRouterOrNull } from './router-ref.ts';\nimport { markStaleFromError } from './stale-client.ts';\n// Page unload detection: when the page is being torn down, error boundaries\n// suppress activation — the error is from the aborted connection, not the\n// application. unload-guard.ts is the single owner of that state; this module\n// used to keep its own copy, which drifted (TIM-1260).\nimport { isPageUnloading } from './unload-guard.ts';\n\n// ─── Digest Types ────────────────────────────────────────────────────────────\n\n/** Structured digest returned by RSC onError for DenySignal. */\ninterface DenyDigest {\n type: 'deny';\n status: number;\n data: unknown;\n /**\n * Segment key of the segment that owns the matched deny page, present when\n * an in-tree catch site matched a file it may not render at its own\n * position and re-threw to reach the owner. See TIM-1356.\n */\n ownerKey?: string;\n}\n\n/** Structured digest returned by RSC onError for RenderError. */\ninterface RenderErrorDigest {\n type: 'render-error';\n code: string;\n data: unknown;\n status: number;\n}\n\n/** Structured digest returned by RSC onError for RedirectSignal. */\ninterface RedirectDigest {\n type: 'redirect';\n location: string;\n status: number;\n}\n\ntype ParsedDigest = DenyDigest | RenderErrorDigest | RedirectDigest;\n\n// ─── Props & State ───────────────────────────────────────────────────────────\n\nexport interface TimberErrorBoundaryProps {\n /** The component to render when an error is caught. */\n fallbackComponent?: (...args: unknown[]) => ReactNode;\n /**\n * Pre-rendered fallback element. Used for MDX status files which are server\n * components and cannot be passed as function props across the RSC→client\n * boundary. When set, rendered directly instead of calling fallbackComponent.\n *\n * See design/10-error-handling.md §\"Status-Code File Variants\" — MDX status\n * files are server components by default (zero client JS).\n */\n fallbackElement?: ReactNode;\n /**\n * Status code filter. If set, only catches errors matching this status.\n * 400 = any 4xx, 500 = any 5xx, specific number = exact match.\n */\n status?: number;\n /**\n * When true, this boundary wraps a parallel slot. Slot denials are\n * graceful degradation — they must NOT change the HTTP status code.\n */\n isSlotBoundary?: boolean;\n /**\n * Segment key of the segment that declared this boundary. Only meaningful\n * against a deny digest carrying an `ownerKey`. See the decline rule in\n * `render()` and TIM-1356.\n */\n segmentKey?: string;\n children: ReactNode;\n}\n\ninterface TimberErrorBoundaryState {\n hasError: boolean;\n error: Error | null;\n}\n\n// Module-level guard: the first boundary to catch a given redirect digest\n// schedules the navigation and marks it handled here. Subsequent renders\n// (React retries the error-boundary render path multiple times in dev, and\n// outer boundaries that also see the error) become no-ops, preventing\n// duplicate router.navigate() calls.\nconst _handledRedirects = new WeakSet<object>();\n\n// Companion guard keyed by location: client-render redirect() (the\n// next/navigation shim) throws a FRESH error instance on every render\n// attempt, and React replays the errored render — so the WeakSet alone\n// would schedule the navigation twice. While a navigation to a location\n// is pending, further redirect errors for the same location are no-ops.\nlet _pendingRedirectLocation: string | null = null;\n\n// ─── Component ───────────────────────────────────────────────────────────────\n\nexport class TimberErrorBoundary extends Component<\n TimberErrorBoundaryProps,\n TimberErrorBoundaryState\n> {\n constructor(props: TimberErrorBoundaryProps) {\n super(props);\n this.state = { hasError: false, error: null };\n }\n\n static getDerivedStateFromError(error: Error): TimberErrorBoundaryState {\n // Suppress error boundaries during page unload (refresh/navigate away).\n // The aborted connection causes React's streaming hydration to error,\n // but the page is about to be replaced — showing an error boundary\n // would be a jarring flash for the user.\n if (isPageUnloading()) {\n return { hasError: false, error: null };\n }\n\n return { hasError: true, error };\n }\n\n componentDidCatch(error: Error): void {\n // A stale bundle most often surfaces here rather than as a rejected\n // navigation: a client reference missing from this build's manifest, or\n // a lazily-imported chunk the CDN no longer has, thrown during render.\n //\n // Recording it is all that happens — this boundary still renders its\n // fallback, and the recovery is the user's next navigation, which the\n // router turns into a full document load. Reloading from here would be a\n // page load the user did not ask for, on a page that may be mid-rollout\n // and would come back just as stale (TIM-1275).\n markStaleFromError(error);\n }\n\n componentDidUpdate(prevProps: TimberErrorBoundaryProps): void {\n // Reset error state when children change (e.g. client-side navigation).\n // Without this, navigating from one error page to another keeps the\n // stale error — getDerivedStateFromError doesn't re-fire for new children.\n if (this.state.hasError && prevProps.children !== this.props.children) {\n this.setState({ hasError: false, error: null });\n }\n }\n\n /** Reset the error state so children re-render. */\n private reset = () => {\n this.setState({ hasError: false, error: null });\n };\n\n render(): ReactNode {\n if (!this.state.hasError || !this.state.error) {\n return this.props.children;\n }\n\n const error = this.state.error;\n const parsed = parseDigest(error);\n\n // RedirectSignal handling splits by environment:\n //\n // - SSR (no window): re-throw so the Fizz shell fails and the server\n // pipeline catch block can produce a proper HTTP 3xx response. See\n // design/04-authorization.md.\n //\n // - Client (window exists): a RedirectSignal that leaked into the RSC\n // Flight payload during client navigation would otherwise bubble past\n // every error boundary to `window.onerror`, which in dev triggers the\n // Vite error overlay (false positive — navigation itself works).\n // Instead, ask the router to perform an SPA navigation to the target\n // and render nothing while it takes over. The router's own catch path\n // handles cross-origin / hard navigations. See TIM-838.\n if (parsed?.type === 'redirect') {\n if (typeof window === 'undefined') {\n throw error;\n }\n // Dedupe: React may render the error-boundary fallback path more\n // than once for a single caught error (dev-mode double render, and\n // additional boundaries above us in the tree). Only schedule the\n // navigation on the first render that sees this error instance.\n const alreadyHandled =\n _handledRedirects.has(error) || _pendingRedirectLocation === parsed.location;\n if (!alreadyHandled) {\n _handledRedirects.add(error);\n const router = getRouterOrNull();\n if (router) {\n _pendingRedirectLocation = parsed.location;\n // Schedule the navigation outside the render phase to avoid\n // setState-during-render warnings from router state updates.\n queueMicrotask(() => {\n router\n .navigate(parsed.location, { replace: true })\n .catch(() => {\n // Fall back to a hard navigation if the soft navigation fails.\n window.location.href = parsed.location;\n })\n .finally(() => {\n _pendingRedirectLocation = null;\n });\n });\n } else {\n // No router available (shouldn't happen post-bootstrap) — fall\n // back to a hard navigation so the user still ends up at the\n // target.\n window.location.href = parsed.location;\n }\n }\n return null;\n }\n\n // Owner-addressed denies: an in-tree catch site already resolved the\n // fallback chain and matched a file it may not render at its own\n // position, so it re-threw to reach the segment that owns that file.\n // Every boundary but that one declines — otherwise the nearest boundary\n // whose status filter happens to match catches first and renders the\n // deny page at its own depth, wrapped in the layouts the hoist existed\n // to skip, and from a different file than the chain chose. (Boundary\n // nesting is per-segment `{status} → category → error`; the 4xx chain\n // runs status files across ALL segments before any error.tsx, so a\n // leaf error.tsx would otherwise beat a root 404.tsx.)\n //\n // Declining is the default: an unkeyed boundary — a slot boundary, or\n // one from a construction site that builds no deny chains — never\n // claims a deny addressed to a segment. See TIM-1356.\n // `typeof === 'string'`, not a null check: the digest is untrusted JSON —\n // `parseDigest` validates only `type`, and any component can set\n // `error.digest` itself. A non-string `ownerKey` is unequal to every\n // boundary's key, so a truthiness test would let one error decline its\n // way past the entire tree and blank the page, where it would otherwise\n // have hit the nearest matching boundary.\n if (\n parsed?.type === 'deny' &&\n typeof parsed.ownerKey === 'string' &&\n parsed.ownerKey !== this.props.segmentKey\n ) {\n throw error;\n }\n\n // If this boundary has a status filter, check whether the error matches.\n // Non-matching errors re-throw so an outer boundary can catch them.\n if (this.props.status != null) {\n const errorStatus = getErrorStatus(parsed, error);\n if (errorStatus == null || !statusMatches(this.props.status, errorStatus)) {\n // Re-throw: this boundary doesn't handle this error.\n throw error;\n }\n }\n\n // Report DenySignal handling so the pipeline skips the redundant\n // renderDenyPage() re-render — but ONLY when this boundary can fully\n // handle the deny in-tree:\n //\n // ✅ TSX boundaries (fallbackComponent) — can receive runtime props\n // (status, dangerouslyPassData) dynamically in render()\n // ❌ MDX boundaries (fallbackElement) — pre-rendered at tree-build time,\n // cannot receive runtime deny props. Must fall through to re-render.\n //\n // For qualifying segment boundaries: also set the HTTP status code from\n // the DenySignal so the Response has the correct 4xx status. This runs\n // synchronously during Fizz rendering, BEFORE onShellReady.\n //\n // For slot boundaries: set _denyHandledByBoundary but do NOT change\n // statusCode — slot denials are graceful degradation.\n //\n // See TIM-664, LOCAL-298.\n if (parsed?.type === 'deny') {\n const canHandleInTree = this.props.fallbackElement == null;\n\n if (canHandleInTree || this.props.isSlotBoundary) {\n const ssrData = getSsrData();\n if (ssrData?._navContext) {\n ssrData._navContext._denyHandledByBoundary = true;\n if (!this.props.isSlotBoundary) {\n ssrData._navContext.statusCode = parsed.status;\n }\n }\n }\n }\n\n // Pre-rendered fallback element (MDX status files) — render directly.\n // MDX components are server components that cannot be passed as function\n // props across the RSC→client boundary. Instead, they are pre-rendered\n // as elements in the RSC environment and passed here as fallbackElement.\n if (this.props.fallbackElement != null) {\n return this.props.fallbackElement;\n }\n\n // Render the fallback component with the right props shape.\n if (parsed?.type === 'deny') {\n // Status-code file boundaries (404.tsx, 4xx.tsx, …) get the documented\n // 4xx props. error.tsx boundaries (no status filter) get their\n // documented { error, digest, reset } contract — plus the deny extras\n // for dual-shape implementations. Without the error prop, an error.tsx\n // reading error.message crashes on deny fall-through. See TIM-1081.\n if (this.props.status != null) {\n return createElement(this.props.fallbackComponent as never, {\n status: parsed.status,\n dangerouslyPassData: parsed.data,\n });\n }\n return createElement(this.props.fallbackComponent as never, {\n error: denyError(parsed.status, error),\n digest: null,\n reset: this.reset,\n status: parsed.status,\n dangerouslyPassData: parsed.data,\n });\n }\n\n // 5xx / RenderError / unhandled error\n const digest =\n parsed?.type === 'render-error' ? { code: parsed.code, data: parsed.data } : null;\n\n return createElement(this.props.fallbackComponent as never, {\n error,\n digest,\n reset: this.reset,\n });\n }\n}\n\n// ─── Helpers ─────────────────────────────────────────────────────────────────\n\n/**\n * Parse the structured digest from the error.\n * React sets `error.digest` from the string returned by RSC's onError.\n */\nfunction parseDigest(error: Error): ParsedDigest | null {\n const raw = (error as { digest?: string }).digest;\n if (typeof raw !== 'string') return null;\n try {\n const parsed = JSON.parse(raw);\n if (parsed && typeof parsed === 'object' && typeof parsed.type === 'string') {\n return parsed as ParsedDigest;\n }\n } catch (e) {\n console.debug('[timber] error digest is not valid JSON:', e instanceof Error ? e.message : e);\n }\n return null;\n}\n\n/**\n * The Error an `error.tsx` serving a deny should receive.\n *\n * The error that actually arrives here is React's, not the framework's: a\n * server-thrown error crossing Flight has its message and name replaced with\n * a generic redacted string in production, by design. An `error.tsx` written\n * per the docs branches on `error.message` — so handing it the redacted error\n * gives it nothing to branch on, while the in-tree path hands the same file a\n * `DenySignal` named error reading `Access denied with status <N>` (see\n * `renderDenyEntry`). This reconstructs that shape from the digest, which is\n * where the status already is.\n *\n * Derived from the status alone — no stack, no server data — so it is safe on\n * the client for the same reason the digest is. See design/13-security.md\n * §\"Errors don't leak\", TIM-1081.\n */\nfunction denyError(status: number, original: Error): Error {\n // A dev build may have preserved something more useful than the redaction.\n if (original.name === 'DenySignal') return original;\n const error = new Error(`Access denied with status ${status}`);\n error.name = 'DenySignal';\n return error;\n}\n\n/**\n * Extract the HTTP status code from a parsed digest or error message.\n * Falls back to message pattern matching for errors without a digest.\n */\nfunction getErrorStatus(parsed: ParsedDigest | null, error: Error): number | null {\n if (parsed?.type === 'deny') return parsed.status;\n if (parsed?.type === 'render-error') return parsed.status;\n if (parsed?.type === 'redirect') return parsed.status;\n\n // Fallback: parse DenySignal message pattern for errors that lost their digest\n const match = error.message.match(/^Access denied with status (\\d+)$/);\n if (match) return parseInt(match[1], 10);\n\n // Unhandled errors are implicitly 500\n return 500;\n}\n\n/**\n * Check whether an error's status matches the boundary's status filter.\n * Category markers (400, 500) match any status in that range.\n */\nfunction statusMatches(boundaryStatus: number, errorStatus: number): boolean {\n // Category catch-all: 400 matches any 4xx, 500 matches any 5xx\n if (boundaryStatus === 400) return errorStatus >= 400 && errorStatus <= 499;\n if (boundaryStatus === 500) return errorStatus >= 500 && errorStatus <= 599;\n // Exact match\n return boundaryStatus === errorStatus;\n}\n"],"mappings":"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"}