@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
@@ -1,34 +1,8 @@
1
- /**
2
- * Port resolution for the dev server, Vite preview, and the Node
3
- * production preview server.
4
- *
5
- * Behavior (see TIM-842):
6
- *
7
- * 1. Default port is **3000** for both dev and prod.
8
- * 2. If the user did NOT set an explicit port, auto-bump from 3000
9
- * until a free port is found (3000 → 3001 → 3002 → …).
10
- * 3. If the user DID set an explicit port (via `--port`, `PORT` env
11
- * var, or `vite.config.ts` `server.port`), use it as-is and let
12
- * the bind fail loudly on conflict (`strictPort: true`).
13
- *
14
- * The port-bump probe is performed by binding the **actual** server
15
- * (e.g. the dev holding server) — not a throwaway probe — so there is
16
- * no time-of-check / time-of-use race between probing and listening.
17
- *
18
- * Design doc: 21-dev-server.md §"Default Port and Auto-Bump".
19
- */
1
+ /** Port input precedence for dev and preview. Vite owns binding and restart reuse. */
20
2
 
21
3
  /** Default port used by `timber dev` and `timber preview`. */
22
4
  export const DEFAULT_PORT = 3000;
23
5
 
24
- /**
25
- * A function that attempts to bind to a given port.
26
- *
27
- * Resolves with the bound port on success. Rejects with an error
28
- * (typically with `code === 'EADDRINUSE'`) on failure.
29
- */
30
- export type ListenFn = (port: number) => Promise<number>;
31
-
32
6
  // ── Pure resolution ────────────────────────────────────────────────────
33
7
 
34
8
  export interface ResolvePortInput {
@@ -49,8 +23,7 @@ export interface ResolvedPortInput {
49
23
  port: number;
50
24
  /**
51
25
  * `true` if the port came from an explicit user override (config or
52
- * env). When `true`, callers must NOT auto-bump and must surface
53
- * `EADDRINUSE` to the user.
26
+ * env). Preview uses this to default to strict port binding.
54
27
  */
55
28
  explicit: boolean;
56
29
  }
@@ -58,7 +31,7 @@ export interface ResolvedPortInput {
58
31
  /**
59
32
  * Pure: compute the starting port from config / env / defaults.
60
33
  *
61
- * Performs no I/O — pair with {@link bindWithBump} to actually listen.
34
+ * Performs no I/O.
62
35
  */
63
36
  export function resolveStartPort(input: ResolvePortInput): ResolvedPortInput {
64
37
  const defaultPort = input.defaultPort ?? DEFAULT_PORT;
@@ -78,138 +51,3 @@ export function resolveStartPort(input: ResolvePortInput): ResolvedPortInput {
78
51
 
79
52
  return { port: defaultPort, explicit: false };
80
53
  }
81
-
82
- // ── Bind with auto-bump ────────────────────────────────────────────────
83
-
84
- export interface BindWithBumpOptions {
85
- /** First port to attempt. */
86
- startPort: number;
87
- /**
88
- * If `true`, increment the port and retry on `EADDRINUSE`. If
89
- * `false`, attempt once and let the error propagate.
90
- */
91
- autoBump: boolean;
92
- /** Maximum number of port attempts when `autoBump` is `true`. */
93
- maxAttempts?: number;
94
- }
95
-
96
- export interface BindWithBumpResult {
97
- /** Port that was actually bound. */
98
- port: number;
99
- /** `true` if the bound port differs from `startPort`. */
100
- bumped: boolean;
101
- }
102
-
103
- /**
104
- * Bind a server starting at `startPort`, optionally bumping the port
105
- * on `EADDRINUSE` until a free one is found.
106
- *
107
- * Use this with the actual server you intend to keep listening (e.g.
108
- * the dev holding server). Pairing the probe with the real listen
109
- * eliminates the TOCTOU race that a throwaway probe would introduce.
110
- */
111
- export async function bindWithBump(
112
- listen: ListenFn,
113
- options: BindWithBumpOptions
114
- ): Promise<BindWithBumpResult> {
115
- const maxAttempts = options.autoBump ? (options.maxAttempts ?? 100) : 1;
116
- let lastErr: unknown = null;
117
-
118
- for (let i = 0; i < maxAttempts; i++) {
119
- const port = options.startPort + i;
120
- try {
121
- const bound = await listen(port);
122
- return { port: bound, bumped: i > 0 };
123
- } catch (err) {
124
- lastErr = err;
125
- // Only retry on EADDRINUSE — other errors (EACCES, etc.) are
126
- // permanent and must surface immediately.
127
- if (!isAddrInUse(err)) throw err;
128
- }
129
- }
130
-
131
- throw lastErr ?? new Error(`Could not bind to a free port starting at ${options.startPort}`);
132
- }
133
-
134
- /** True if `err` is a Node `EADDRINUSE` error from `server.listen()`. */
135
- export function isAddrInUse(err: unknown): boolean {
136
- return (
137
- typeof err === 'object' && err !== null && (err as { code?: string }).code === 'EADDRINUSE'
138
- );
139
- }
140
-
141
- // ── High-level helper used by the rootSync plugin ────────────────────
142
-
143
- export interface StartDevServerPortInput {
144
- /** Resolved port from `userConfig.server?.port` (or `--port`), or undefined. */
145
- configPort: number | undefined;
146
- /** Raw `process.env.PORT` value. */
147
- envPort: string | undefined;
148
- /** ListenFn for the holding server (or any pre-bind probe target). */
149
- listen: ListenFn;
150
- /** Logger — defaults to `console`. Injected for tests. */
151
- log?: (msg: string) => void;
152
- warn?: (msg: string) => void;
153
- }
154
-
155
- export interface StartDevServerPortResult {
156
- /** Port chosen for both the holding server and Vite's dev server. */
157
- port: number;
158
- /** True if the user explicitly set the port (=> Vite must `strictPort: true`). */
159
- explicit: boolean;
160
- /** True if the holding server actually bound the port. */
161
- bound: boolean;
162
- }
163
-
164
- /**
165
- * Run the full dev-server port resolution + holding-server bind sequence.
166
- *
167
- * Resolves the port from config / env / default, attempts to bind the
168
- * holding server (auto-bumping when the port came from the default),
169
- * and logs the chosen URL. On a clean failure for an explicit port, it
170
- * warns and falls back to the requested port so Vite can surface the
171
- * conflict via `strictPort: true`.
172
- *
173
- * Extracted from `index.ts` so the rootSync `config()` hook stays
174
- * focused on plugin assembly.
175
- */
176
- export async function startDevServerPort(
177
- input: StartDevServerPortInput
178
- ): Promise<StartDevServerPortResult> {
179
- const log = input.log ?? ((msg: string) => console.log(msg));
180
- const warn = input.warn ?? ((msg: string) => console.warn(msg));
181
-
182
- const start = resolveStartPort({
183
- configPort: input.configPort,
184
- envPort: input.envPort,
185
- defaultPort: DEFAULT_PORT,
186
- });
187
-
188
- try {
189
- const result = await bindWithBump(input.listen, {
190
- startPort: start.port,
191
- autoBump: !start.explicit,
192
- });
193
- if (result.bumped) {
194
- log(`\n \x1b[33m[timber]\x1b[0m Port ${start.port} in use, using ${result.port}\n`);
195
- }
196
- log(
197
- `\n \x1b[2m\u{1FAB5} timber.js dev server starting at\x1b[0m ` +
198
- `\x1b[36mhttp://localhost:${result.port}\x1b[0m\n`
199
- );
200
- return { port: result.port, explicit: start.explicit, bound: true };
201
- } catch (err) {
202
- // Holding server failed to bind. For explicit ports we leave the
203
- // bound state false and let Vite fail loudly via strictPort: true.
204
- // For implicit ports this is essentially unreachable (auto-bump
205
- // tries 100 ports), but if we hit it we still want Vite to surface
206
- // the error.
207
- if (start.explicit && isAddrInUse(err)) {
208
- warn(
209
- `\n \x1b[33m[timber]\x1b[0m Port ${start.port} is already in use. ` +
210
- `Set PORT (or remove the override) to pick another port.\n`
211
- );
212
- }
213
- return { port: start.port, explicit: start.explicit, bound: false };
214
- }
215
- }
@@ -46,10 +46,10 @@ import { createElement } from 'react';
46
46
 
47
47
  import { requestContextAls } from './als-registry.ts';
48
48
  import type { CoercedParams } from '../shared/param-value.ts';
49
- import { ParamCoercionError } from './route-element-builder.ts';
49
+ import { ParamCoercionError } from './param-coercion.ts';
50
50
  import { SlotOutlet } from '../client/slot-outlet.tsx';
51
51
  import { coerceSegmentParams } from './param-coercion.ts';
52
- import { DenySignal, RedirectSignal } from './primitives.ts';
52
+ import { isDenySignal, isRedirectSignal } from './primitives.ts';
53
53
  import {
54
54
  beginCapture,
55
55
  endCapture,
@@ -395,8 +395,8 @@ async function renderEntry(
395
395
 
396
396
  /** Human-readable classification for a failed entry. */
397
397
  function describeFailure(error: unknown): string {
398
- if (error instanceof DenySignal) return `component called deny(${error.status})`;
399
- if (error instanceof RedirectSignal) return `component called redirect("${error.location}")`;
398
+ if (isDenySignal(error)) return `component called deny(${error.status})`;
399
+ if (isRedirectSignal(error)) return `component called redirect("${error.location}")`;
400
400
  if (error instanceof Error) return error.message;
401
401
  return String(error);
402
402
  }
@@ -103,31 +103,85 @@ function warnIfNotSerializable(data: unknown, callerName: string): void {
103
103
  }
104
104
  }
105
105
 
106
+ // ─── Signal branding ────────────────────────────────────────────────────────
107
+ //
108
+ // Catch sites must NOT use `instanceof` on these classes. In dev, an HMR
109
+ // program reload clears the RSC module cache; a request that is mid-render
110
+ // still holds the catch sites from the old evaluation of this module while
111
+ // user code loaded after the reload throws from the new one. The two
112
+ // evaluations are distinct classes, so `instanceof` fails and a correct
113
+ // deny() falls through every catch site as an unhandled 500 (TIM-1499).
114
+ //
115
+ // Each class stamps a `Symbol.for` brand on its instances in the constructor.
116
+ // `Symbol.for` keys live in the realm-wide symbol registry, so every
117
+ // evaluation of this module — and every module graph in the same process —
118
+ // agrees on the key. The `is*` predicates below check the brand and are the
119
+ // only supported way to detect a signal. The classes themselves are NOT
120
+ // anchored on globalThis: a re-evaluated module must hand out its own
121
+ // (possibly updated) class, not a stale one.
122
+ //
123
+ // See design/18-build-system.md §"Dev Mode: Signal Identity Across Program Reloads".
124
+
125
+ const DENY_BRAND: unique symbol = Symbol.for('timber:signal.deny');
126
+ const REDIRECT_BRAND: unique symbol = Symbol.for('timber:signal.redirect');
127
+ const RENDER_ERROR_BRAND: unique symbol = Symbol.for('timber:signal.render-error');
128
+ const SSR_STREAM_ERROR_BRAND: unique symbol = Symbol.for('timber:signal.ssr-stream-error');
129
+
130
+ function hasBrand(value: unknown, brand: symbol): boolean {
131
+ return typeof value === 'object' && value !== null && brand in value;
132
+ }
133
+
134
+ /** True for any `DenySignal`, regardless of which module evaluation created it. */
135
+ export function isDenySignal(error: unknown): error is DenySignal {
136
+ return hasBrand(error, DENY_BRAND);
137
+ }
138
+
139
+ /** True for any `RedirectSignal`, regardless of which module evaluation created it. */
140
+ export function isRedirectSignal(error: unknown): error is RedirectSignal {
141
+ return hasBrand(error, REDIRECT_BRAND);
142
+ }
143
+
144
+ /** True for any `RenderError`, regardless of which module evaluation created it. */
145
+ export function isRenderError(error: unknown): error is RenderError {
146
+ return hasBrand(error, RENDER_ERROR_BRAND);
147
+ }
148
+
149
+ /**
150
+ * True for any `SsrStreamError`. This one also crosses Vite environments:
151
+ * it is thrown in SSR and caught in RSC, which are separate module graphs in
152
+ * the same process — the brand is the only identity the two share.
153
+ */
154
+ export function isSsrStreamError(error: unknown): error is SsrStreamError {
155
+ return hasBrand(error, SSR_STREAM_ERROR_BRAND);
156
+ }
157
+
106
158
  // ─── DenySignal ─────────────────────────────────────────────────────────────
107
159
 
108
160
  /**
109
161
  * Render-phase signal thrown by `deny()`. Caught by the framework to produce
110
162
  * the correct HTTP status code (segment context) or graceful degradation (slot context).
163
+ *
164
+ * Detect with `isDenySignal()`, never `instanceof` — see "Signal branding" above.
111
165
  */
112
166
  export class DenySignal extends Error {
167
+ readonly [DENY_BRAND] = true;
113
168
  readonly status: number;
114
169
  readonly data: JsonSerializable | undefined;
115
170
 
116
171
  /**
117
- * Segment key of the segment that owns the deny page matched for this
118
- * signal — set by a framework catch site that matched an entry it may not
119
- * render at its own position, and re-threw so the boundary at the owning
120
- * segment renders it instead.
172
+ * Ordered list of segment keys that own matching deny pages for this
173
+ * signal, best match first. Present = addressed (a boundary should
174
+ * render it); absent = unplaced (the re-render fallback serves it).
121
175
  *
122
- * Written only by the three in-tree catch sites (`PageDenyBoundary`,
123
- * `TracedLayout`, `AccessGate`), never by user code. Two readers depend on
124
- * it: `TimberErrorBoundary`, which declines any deny whose `ownerKey` is
125
- * not its own `segmentKey`, and `buildRscPayloadResponse`, which keeps
126
- * streaming instead of re-rendering when it is set.
176
+ * In-tree hoists stamp a single-element list (`[entry.ownerKey]`) —
177
+ * identical to the pre-TIM-1450 `ownerKey`. Late addressing (TIM-1450)
178
+ * stamps the full candidate list so the client can find the first
179
+ * *reachable* owner via its ancestry context, eliminating the
180
+ * owner-below-throw-site escape.
127
181
  *
128
182
  * See design/04-authorization.md §"Where a Deny Page Renders", TIM-1356.
129
183
  */
130
- ownerKey?: string;
184
+ owners?: string[];
131
185
 
132
186
  /**
133
187
  * When true, the pipeline must respond with `rscErrorEnvelope` so the
@@ -235,8 +289,11 @@ export { RedirectType } from '../shared/redirect-type.ts';
235
289
  /**
236
290
  * Render-phase signal thrown by `redirect()` and `redirectExternal()`.
237
291
  * Caught by the framework to produce a 3xx response or client-side navigation.
292
+ *
293
+ * Detect with `isRedirectSignal()`, never `instanceof` — see "Signal branding" above.
238
294
  */
239
295
  export class RedirectSignal extends Error {
296
+ readonly [REDIRECT_BRAND] = true;
240
297
  readonly location: string;
241
298
  readonly status: number;
242
299
 
@@ -258,7 +315,7 @@ export class RedirectSignal extends Error {
258
315
  * See also: isFrameworkSignalError() in client/browser-dev.ts (client-only).
259
316
  */
260
317
  export function isControlFlowSignal(error: unknown): boolean {
261
- if (error instanceof DenySignal || error instanceof RedirectSignal) {
318
+ if (isDenySignal(error) || isRedirectSignal(error)) {
262
319
  return true;
263
320
  }
264
321
  if (error && typeof error === 'object') {
@@ -396,11 +453,14 @@ export interface RenderErrorDigest<
396
453
  * resourceId: params.id,
397
454
  * })
398
455
  * ```
456
+ *
457
+ * Detect with `isRenderError()`, never `instanceof` — see "Signal branding" above.
399
458
  */
400
459
  export class RenderError<
401
460
  TCode extends string = string,
402
461
  TData extends JsonSerializable = JsonSerializable,
403
462
  > extends Error {
463
+ readonly [RENDER_ERROR_BRAND] = true;
404
464
  readonly code: TCode;
405
465
  readonly digest: RenderErrorDigest<TCode, TData>;
406
466
  readonly status: number;
@@ -474,8 +534,12 @@ export function _resetWaitUntilWarning(): void {
474
534
  *
475
535
  * Defined in primitives.ts (not ssr-entry.ts) because ssr-entry.ts imports
476
536
  * react-dom/server which cannot be loaded in the RSC environment.
537
+ *
538
+ * Detect with `isSsrStreamError()` — it is thrown in the SSR environment and
539
+ * caught in RSC, so `instanceof` never matches. See "Signal branding" above.
477
540
  */
478
541
  export class SsrStreamError extends Error {
542
+ readonly [SSR_STREAM_ERROR_BRAND] = true;
479
543
  override readonly cause: unknown;
480
544
 
481
545
  constructor(message: string, cause: unknown) {
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Legacy shared-cache control fields that predate RFC 9213 and so do not
3
+ * follow the `<target>-Cache-Control` naming pattern — the one part of this
4
+ * rule that cannot be matched by shape. `X-Accel-Expires` is nginx
5
+ * `proxy_cache`, which gives it precedence over `Cache-Control`.
6
+ *
7
+ * A closed historical set, unlike the vendor `*-Cache-Control` names, which
8
+ * keep being minted and are handled by pattern below.
9
+ */
10
+ const LEGACY_CDN_CACHE_HEADERS = ['Surrogate-Control', 'Edge-Control', 'X-Accel-Expires'];
11
+
12
+ /**
13
+ * Whether a response header overrides `Cache-Control` at a shared cache.
14
+ *
15
+ * Matched by *shape*, not by vendor name. RFC 9213 defines targeted fields
16
+ * as `<target>-Cache-Control`, and every CDN spells its own: `CDN-`,
17
+ * `Cloudflare-CDN-`, `Netlify-CDN-`, `Vercel-CDN-`, and whichever ships
18
+ * next. A vendor list here is a denylist with an unbounded tail — it was
19
+ * extended twice during review, each time by one name, which is the signal
20
+ * that enumeration is the wrong shape. The suffix rule is total.
21
+ *
22
+ * `Cache-Control` itself does not match (no `-` prefix) and is replaced
23
+ * separately.
24
+ */
25
+ function isSharedCacheOverride(name: string): boolean {
26
+ const lowered = name.toLowerCase();
27
+ return (
28
+ lowered.endsWith('-cache-control') ||
29
+ LEGACY_CDN_CACHE_HEADERS.some((legacy) => legacy.toLowerCase() === lowered)
30
+ );
31
+ }
32
+
33
+ /** Disable caching, including CDN overrides that take precedence over Cache-Control. */
34
+ export function preventResponseCaching(headers: Headers): void {
35
+ // Collected before deleting — mutating Headers while iterating its live
36
+ // key iterator can skip entries.
37
+ const overrides: string[] = [];
38
+ for (const name of headers.keys()) {
39
+ if (isSharedCacheOverride(name)) overrides.push(name);
40
+ }
41
+ for (const name of overrides) {
42
+ headers.delete(name);
43
+ }
44
+ headers.set('Cache-Control', 'private, no-store');
45
+ }