@nifrajs/core 5.8.0 → 5.10.0

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 (239) hide show
  1. package/CHANGELOG.md +118 -0
  2. package/README.md +9 -4
  3. package/dist/assurance.d.ts +26 -1
  4. package/dist/assurance.d.ts.map +1 -1
  5. package/dist/assurance.js +43 -11
  6. package/dist/assurance.js.map +1 -1
  7. package/dist/canonical-json.d.ts +11 -0
  8. package/dist/canonical-json.d.ts.map +1 -0
  9. package/dist/canonical-json.js +46 -0
  10. package/dist/canonical-json.js.map +1 -0
  11. package/dist/capabilities.d.ts.map +1 -1
  12. package/dist/capabilities.js +1 -8
  13. package/dist/capabilities.js.map +1 -1
  14. package/dist/data.d.ts.map +1 -1
  15. package/dist/data.js +1 -8
  16. package/dist/data.js.map +1 -1
  17. package/dist/diff.d.ts.map +1 -1
  18. package/dist/diff.js +2 -12
  19. package/dist/diff.js.map +1 -1
  20. package/dist/durable-adapters.d.ts +2 -0
  21. package/dist/durable-adapters.d.ts.map +1 -1
  22. package/dist/durable-adapters.js +41 -49
  23. package/dist/durable-adapters.js.map +1 -1
  24. package/dist/durable-saga.d.ts.map +1 -1
  25. package/dist/durable-saga.js +14 -20
  26. package/dist/durable-saga.js.map +1 -1
  27. package/dist/evidence.d.ts.map +1 -1
  28. package/dist/evidence.js +14 -68
  29. package/dist/evidence.js.map +1 -1
  30. package/dist/idempotency.d.ts.map +1 -1
  31. package/dist/idempotency.js +60 -23
  32. package/dist/idempotency.js.map +1 -1
  33. package/dist/index.d.ts +3 -2
  34. package/dist/index.d.ts.map +1 -1
  35. package/dist/index.js +3 -2
  36. package/dist/index.js.map +1 -1
  37. package/dist/internal/control-character.d.ts +3 -0
  38. package/dist/internal/control-character.d.ts.map +1 -0
  39. package/dist/internal/control-character.js +10 -0
  40. package/dist/internal/control-character.js.map +1 -0
  41. package/dist/internal/durable-shared.d.ts.map +1 -1
  42. package/dist/internal/durable-shared.js +4 -1
  43. package/dist/internal/durable-shared.js.map +1 -1
  44. package/dist/internal/hex.d.ts +5 -0
  45. package/dist/internal/hex.d.ts.map +1 -0
  46. package/dist/internal/hex.js +14 -0
  47. package/dist/internal/hex.js.map +1 -0
  48. package/dist/internal/record.d.ts +3 -0
  49. package/dist/internal/record.d.ts.map +1 -0
  50. package/dist/internal/record.js +5 -0
  51. package/dist/internal/record.js.map +1 -0
  52. package/dist/internal/route-compiler.d.ts.map +1 -1
  53. package/dist/internal/route-compiler.js +2 -2
  54. package/dist/internal/route-compiler.js.map +1 -1
  55. package/dist/internal/route-execution.d.ts +4 -4
  56. package/dist/internal/route-execution.d.ts.map +1 -1
  57. package/dist/internal/route-execution.js +4 -0
  58. package/dist/internal/route-execution.js.map +1 -1
  59. package/dist/internal/route-lanes.d.ts +4 -3
  60. package/dist/internal/route-lanes.d.ts.map +1 -1
  61. package/dist/internal/route-lanes.js +16 -33
  62. package/dist/internal/route-lanes.js.map +1 -1
  63. package/dist/internal/schema-parts.d.ts +13 -0
  64. package/dist/internal/schema-parts.d.ts.map +1 -0
  65. package/dist/internal/schema-parts.js +15 -0
  66. package/dist/internal/schema-parts.js.map +1 -0
  67. package/dist/ledger.d.ts.map +1 -1
  68. package/dist/ledger.js +2 -14
  69. package/dist/ledger.js.map +1 -1
  70. package/dist/manifest.d.ts.map +1 -1
  71. package/dist/manifest.js +10 -37
  72. package/dist/manifest.js.map +1 -1
  73. package/dist/open-resolved.d.ts +18 -0
  74. package/dist/open-resolved.d.ts.map +1 -0
  75. package/dist/open-resolved.js +62 -0
  76. package/dist/open-resolved.js.map +1 -0
  77. package/dist/range.js +1 -1
  78. package/dist/range.js.map +1 -1
  79. package/dist/router/path-codec.d.ts +22 -0
  80. package/dist/router/path-codec.d.ts.map +1 -0
  81. package/dist/router/path-codec.js +87 -0
  82. package/dist/router/path-codec.js.map +1 -0
  83. package/dist/router/pattern.d.ts +21 -1
  84. package/dist/router/pattern.d.ts.map +1 -1
  85. package/dist/router/pattern.js +117 -0
  86. package/dist/router/pattern.js.map +1 -1
  87. package/dist/router/same-origin-path.d.ts +22 -0
  88. package/dist/router/same-origin-path.d.ts.map +1 -0
  89. package/dist/router/same-origin-path.js +37 -0
  90. package/dist/router/same-origin-path.js.map +1 -0
  91. package/dist/serve-static.d.ts +22 -0
  92. package/dist/serve-static.d.ts.map +1 -0
  93. package/dist/serve-static.js +187 -0
  94. package/dist/serve-static.js.map +1 -0
  95. package/dist/server/bare-error-lane.d.ts +2 -1
  96. package/dist/server/bare-error-lane.d.ts.map +1 -1
  97. package/dist/server/bare-error-lane.js +12 -3
  98. package/dist/server/bare-error-lane.js.map +1 -1
  99. package/dist/server/base64-answers.d.ts.map +1 -1
  100. package/dist/server/base64-answers.js +1 -4
  101. package/dist/server/base64-answers.js.map +1 -1
  102. package/dist/server/body-lane.d.ts.map +1 -1
  103. package/dist/server/body-lane.js +43 -38
  104. package/dist/server/body-lane.js.map +1 -1
  105. package/dist/server/body.d.ts.map +1 -1
  106. package/dist/server/body.js +4 -1
  107. package/dist/server/body.js.map +1 -1
  108. package/dist/server/bridge-markers.d.ts +1 -1
  109. package/dist/server/bridge-markers.js +1 -1
  110. package/dist/server/cookies.d.ts.map +1 -1
  111. package/dist/server/cookies.js +4 -1
  112. package/dist/server/cookies.js.map +1 -1
  113. package/dist/server/core-copies.js +1 -1
  114. package/dist/server/core-copies.js.map +1 -1
  115. package/dist/server/http.d.ts +6 -18
  116. package/dist/server/http.d.ts.map +1 -1
  117. package/dist/server/http.js +14 -33
  118. package/dist/server/http.js.map +1 -1
  119. package/dist/server/idempotency-lane.d.ts.map +1 -1
  120. package/dist/server/idempotency-lane.js +2 -0
  121. package/dist/server/idempotency-lane.js.map +1 -1
  122. package/dist/server/idle-timeout.d.ts +39 -0
  123. package/dist/server/idle-timeout.d.ts.map +1 -0
  124. package/dist/server/idle-timeout.js +95 -0
  125. package/dist/server/idle-timeout.js.map +1 -0
  126. package/dist/server/json-shared.d.ts +23 -0
  127. package/dist/server/json-shared.d.ts.map +1 -0
  128. package/dist/server/json-shared.js +33 -0
  129. package/dist/server/json-shared.js.map +1 -0
  130. package/dist/server/json-text.d.ts +1 -1
  131. package/dist/server/json-text.js +1 -1
  132. package/dist/server/node-outcome.js +1 -1
  133. package/dist/server/node-outcome.js.map +1 -1
  134. package/dist/server/request-context.d.ts +22 -1
  135. package/dist/server/request-context.d.ts.map +1 -1
  136. package/dist/server/request-context.js +44 -4
  137. package/dist/server/request-context.js.map +1 -1
  138. package/dist/server/request-local.d.ts +44 -0
  139. package/dist/server/request-local.d.ts.map +1 -0
  140. package/dist/server/request-local.js +91 -0
  141. package/dist/server/request-local.js.map +1 -0
  142. package/dist/server/respond.js +1 -1
  143. package/dist/server/respond.js.map +1 -1
  144. package/dist/server/response-contract-lane.d.ts +10 -2
  145. package/dist/server/response-contract-lane.d.ts.map +1 -1
  146. package/dist/server/response-contract-lane.js +93 -64
  147. package/dist/server/response-contract-lane.js.map +1 -1
  148. package/dist/server/runtime-core.d.ts +7 -4
  149. package/dist/server/runtime-core.d.ts.map +1 -1
  150. package/dist/server/runtime-core.js +9 -7
  151. package/dist/server/runtime-core.js.map +1 -1
  152. package/dist/server/server-types.d.ts +23 -8
  153. package/dist/server/server-types.d.ts.map +1 -1
  154. package/dist/server/server.d.ts +58 -59
  155. package/dist/server/server.d.ts.map +1 -1
  156. package/dist/server/server.js +860 -510
  157. package/dist/server/server.js.map +1 -1
  158. package/dist/server/transport-lane.d.ts.map +1 -1
  159. package/dist/server/transport-lane.js +2 -0
  160. package/dist/server/transport-lane.js.map +1 -1
  161. package/dist/server/webhook.js +1 -1
  162. package/dist/server/webhook.js.map +1 -1
  163. package/dist/server/websocket.d.ts +3 -1
  164. package/dist/server/websocket.d.ts.map +1 -1
  165. package/dist/server/websocket.js +14 -1
  166. package/dist/server/websocket.js.map +1 -1
  167. package/dist/server/ws-adapter-view.d.ts +54 -0
  168. package/dist/server/ws-adapter-view.d.ts.map +1 -0
  169. package/dist/server/ws-adapter-view.js +7 -0
  170. package/dist/server/ws-adapter-view.js.map +1 -0
  171. package/dist/server.d.ts +2 -1
  172. package/dist/server.d.ts.map +1 -1
  173. package/dist/server.js +2 -1
  174. package/dist/server.js.map +1 -1
  175. package/dist/single-copy.d.ts.map +1 -1
  176. package/dist/single-copy.js +1 -1
  177. package/dist/single-copy.js.map +1 -1
  178. package/dist/static-file.d.ts +156 -4
  179. package/dist/static-file.d.ts.map +1 -1
  180. package/dist/static-file.js +198 -4
  181. package/dist/static-file.js.map +1 -1
  182. package/dist/wire.d.ts.map +1 -1
  183. package/dist/wire.js +1 -3
  184. package/dist/wire.js.map +1 -1
  185. package/package.json +16 -1
  186. package/src/assurance.ts +66 -16
  187. package/src/canonical-json.ts +43 -0
  188. package/src/capabilities.ts +1 -8
  189. package/src/data.ts +2 -8
  190. package/src/diff.ts +2 -12
  191. package/src/durable-adapters.ts +53 -50
  192. package/src/durable-saga.ts +30 -23
  193. package/src/evidence.ts +25 -90
  194. package/src/idempotency.ts +62 -20
  195. package/src/index.ts +13 -2
  196. package/src/internal/control-character.ts +8 -0
  197. package/src/internal/durable-shared.ts +4 -1
  198. package/src/internal/hex.ts +14 -0
  199. package/src/internal/record.ts +4 -0
  200. package/src/internal/route-compiler.ts +2 -2
  201. package/src/internal/route-execution.ts +7 -20
  202. package/src/internal/route-lanes.ts +20 -44
  203. package/src/internal/schema-parts.ts +19 -0
  204. package/src/ledger.ts +2 -16
  205. package/src/manifest.ts +10 -38
  206. package/src/open-resolved.ts +64 -0
  207. package/src/range.ts +1 -1
  208. package/src/router/path-codec.ts +93 -0
  209. package/src/router/pattern.ts +127 -0
  210. package/src/router/same-origin-path.ts +36 -0
  211. package/src/serve-static.ts +227 -0
  212. package/src/server/bare-error-lane.ts +11 -2
  213. package/src/server/base64-answers.ts +2 -6
  214. package/src/server/body-lane.ts +58 -51
  215. package/src/server/body.ts +4 -1
  216. package/src/server/bridge-markers.ts +1 -1
  217. package/src/server/cookies.ts +4 -1
  218. package/src/server/core-copies.ts +1 -1
  219. package/src/server/http.ts +13 -33
  220. package/src/server/idempotency-lane.ts +2 -0
  221. package/src/server/idle-timeout.ts +107 -0
  222. package/src/server/json-shared.ts +38 -0
  223. package/src/server/json-text.ts +1 -1
  224. package/src/server/node-outcome.ts +1 -1
  225. package/src/server/request-context.ts +62 -3
  226. package/src/server/request-local.ts +109 -0
  227. package/src/server/respond.ts +1 -1
  228. package/src/server/response-contract-lane.ts +112 -65
  229. package/src/server/runtime-core.ts +10 -8
  230. package/src/server/server-types.ts +23 -8
  231. package/src/server/server.ts +1220 -798
  232. package/src/server/transport-lane.ts +2 -0
  233. package/src/server/webhook.ts +1 -1
  234. package/src/server/websocket.ts +18 -1
  235. package/src/server/ws-adapter-view.ts +63 -0
  236. package/src/server.ts +12 -1
  237. package/src/single-copy.ts +1 -3
  238. package/src/static-file.ts +350 -4
  239. package/src/wire.ts +2 -4
@@ -8,7 +8,7 @@ import { adoptRouteRuntimes, compileRouteOptions } from "../internal/route-compi
8
8
  import { compileRouteExecutionPlan, } from "../internal/route-execution.js";
9
9
  import { authenticateMount, compileRouteProgram, executeRouteProgram, } from "../internal/route-program.js";
10
10
  import { isSameOriginRequest } from "../internal/same-origin.js";
11
- import { decodeRouteParams, expandOptionalParams, } from "../router/pattern.js";
11
+ import { compileRoutePattern, decodeRouteParams, expandOptionalParams, } from "../router/pattern.js";
12
12
  import { EMPTY_PARAMS, METHODS, Router } from "../router/router.js";
13
13
  import { withAnswerSteps } from "./answer-step.js";
14
14
  import { isAuthenticatedRequest, markAuthenticatedRequest, } from "./auth.js";
@@ -22,8 +22,9 @@ import { declaredHeaders, headerObjectOf } from "./headers.js";
22
22
  import { isRoutableMethod, jsonError, pathnameOf, plainError, urlPartsOf, } from "./http.js";
23
23
  import { withStaticNodeHeaders } from "./node-outcome.js";
24
24
  import { queryObjectOf, searchOf } from "./query.js";
25
- import { PLATFORM_PEER, RequestContext, readBodyFramed, } from "./request-context.js";
25
+ import { PLATFORM_IDLE_TIMEOUT, PLATFORM_PEER, PLATFORM_REQUEST, RequestContext, readBodyFramed, } from "./request-context.js";
26
26
  import { recordRequestReplacement } from "./request-lineage.js";
27
+ import { carryToCopy } from "./request-local.js";
27
28
  import { needsResolving, resolveDotSegments } from "./request-target.js";
28
29
  import { applyStaticResponseHeaders, buildStaticResponseHeaders, claimHandedOver, fusedRespond, fusedRespondNoSet, handedBackResponse, recordResidentTexts, toResponse, writableResponse, } from "./respond.js";
29
30
  import { CONTEXT_REQUEST, CONTEXT_SEARCH, CONTEXT_SET, EMPTY_RESPONSE_CONTROLS, getNeverAbortSignal, getUnboundedRequestBudget, headerOf, isResponseResult, requestOf, } from "./runtime-core.js";
@@ -39,24 +40,32 @@ import { jsonLogger } from "./logger.js";
39
40
  const BUN_NATIVE_REQUEST_SAFE = Symbol.for("@nifrajs/core/bun-native-request-safe");
40
41
  const functionToString = Function.prototype.toString;
41
42
  const CONTEXTLESS_ARROW = /^(?:async\s*)?\(\s*\)\s*(?::[\s\S]*?)?=>/;
42
- /** The socket peer Bun observed, as a `Platform` for the request lifecycle (`undefined` if unknown). */
43
- function bunPeerPlatform(server, req) {
43
+ /**
44
+ * The socket peer Bun observed (`undefined` if unknown) and its connection's idle timeout, as a
45
+ * `Platform` for one request. A class, not an object literal: JSC builds a literal holding a getter
46
+ * or a symbol key on a slow path, measured at about 5x the instructions of this instance.
47
+ */
48
+ class BunPeerPlatform {
49
+ #server;
50
+ #resolved = false;
51
+ #address;
52
+ constructor(server, req, setIdleTimeout) {
53
+ this.#server = server;
54
+ this[PLATFORM_IDLE_TIMEOUT] = setIdleTimeout;
55
+ this[PLATFORM_REQUEST] = req;
56
+ }
44
57
  // Bun's requestIP() costs ~0.5 us per call, about 7% of a bare GET's server time. Keep the
45
58
  // documented raw-peer c.clientIp behavior, but resolve it lazily: most routes never read c.clientIp,
46
59
  // so they should not pay for the socket lookup on every request. A getter also
47
60
  // preserves middleware that inspects the platform argument directly and trust-mode routes, which
48
61
  // resolve the value in deriveClientIp before the handler runs.
49
- let resolved = false;
50
- let address;
51
- return {
52
- get clientIp() {
53
- if (!resolved) {
54
- resolved = true;
55
- address = server.requestIP(req)?.address;
56
- }
57
- return address;
58
- },
59
- };
62
+ get clientIp() {
63
+ if (!this.#resolved) {
64
+ this.#resolved = true;
65
+ this.#address = this.#server.requestIP(this[PLATFORM_REQUEST])?.address;
66
+ }
67
+ return this.#address;
68
+ }
60
69
  }
61
70
  // Bun's route table has a slot for each of these; a QUERY entry is ignored and the request falls
62
71
  // through to the table's fetch, so QUERY routes stay on the portable dispatcher.
@@ -87,6 +96,20 @@ function outranks(other, route) {
87
96
  }
88
97
  return ahead && other.length === route.length;
89
98
  }
99
+ /** Whether one request can match both patterns: no two static segments differ before either ends
100
+ * in a wildcard, and the lengths agree otherwise. Errs toward `true`. */
101
+ function mayShareRequest(a, b) {
102
+ const shared = Math.min(a.length, b.length);
103
+ for (let i = 0; i < shared; i++) {
104
+ const left = a[i];
105
+ const right = b[i];
106
+ if (left.kind === "wildcard" || right.kind === "wildcard")
107
+ return true;
108
+ if (left.kind === "static" && right.kind === "static" && left.value !== right.value)
109
+ return false;
110
+ }
111
+ return a.length === b.length;
112
+ }
90
113
  /**
91
114
  * The key Bun's route table holds `path` under. Bun refuses one with a character outside printable
92
115
  * ASCII, so such a path is spelled as the portable router matches it: percent-encoded static
@@ -138,6 +161,8 @@ const BODY_ERROR = (error) => Promise.reject(error);
138
161
  const DEFAULT_DRAIN_MS = 10_000;
139
162
  const DRAIN_POLL_MS = 10;
140
163
  const STOP_HOOK_TIMEOUT_MS = 5_000;
164
+ /** Bun's own `idleTimeout` when `listen()` is given none. */
165
+ const BUN_IDLE_TIMEOUT_SEC = 10;
141
166
  /** Same-origin check for a WebSocket handshake (CSWSH default). {@link isSameOriginRequest} is the one
142
167
  * owner, shared with the server-function mount in `@nifrajs/web` - the two used to answer differently
143
168
  * for the same request, so a browser that could open a socket was told its POST was cross-origin. */
@@ -173,8 +198,8 @@ function hasUnroutedParam(params) {
173
198
  return false;
174
199
  }
175
200
  /**
176
- * Deno is the runtime that hands an app a `Request` whose `url` is the target as the client sent it;
177
- * Bun, workerd and every `new Request()` parse it first. Only there does `fetch` read the URL up front.
201
+ * Deno's server builds `Request.url` from the Host and target as received, without normalizing the
202
+ * target first. Bun, workerd, and `new Request()` parse it before core sees it.
178
203
  */
179
204
  const rawRequestTargets = typeof globalThis.Deno !== "undefined";
180
205
  /**
@@ -200,6 +225,7 @@ function withResolvedTarget(req, url) {
200
225
  // The same body, so the same framing: a runtime still delimited it.
201
226
  if (hasTrustedBodyFraming(req))
202
227
  markTrustedBodyFraming(rebuilt);
228
+ carryToCopy(req, rebuilt);
203
229
  return rebuilt;
204
230
  }
205
231
  /**
@@ -254,26 +280,35 @@ const invalidHost = (host) => {
254
280
  * Host (400), a URL replaces the target, `false` keeps it. Deno hands the target over as sent, so
255
281
  * its dot segments are resolved; so does Bun when HTTP/1.0 leaves the Host out, and its route table
256
282
  * matched `/files/..` raw. Such a request is served as `localhost`, as Node and Deno serve it.
257
- * `headerOf`, not `req.headers`: on Deno that reads the Host alone instead of building every
258
- * header. On Bun only a target that needs resolving is checked unless `validateHost` is set.
283
+ * `headerOf`, not `req.headers`: on Deno it reads Host only when `validateHost` is set, and dot
284
+ * segments are resolved only when `normalizeTarget` is. On Bun only a target that needs resolving
285
+ * is checked unless `validateHost` is set.
259
286
  */
260
- const requestTarget = (req, validateHost) => {
261
- if (!rawRequestTargets && !(unparsedBunTargets && (validateHost || needsResolving(req.url))))
287
+ const requestTarget = (req, validateHost, normalizeTarget) => {
288
+ if (rawRequestTargets) {
289
+ const host = validateHost ? headerOf(req, "host") : null;
290
+ if (host !== null && invalidHost(host))
291
+ return true;
292
+ if (!normalizeTarget || !needsResolving(req.url))
293
+ return false;
294
+ // Deno can preserve an unparseable absolute URL when Host is malformed. Do not route its
295
+ // unresolved dot segments through the raw-target fallback.
296
+ if (!URL.canParse(req.url, "http://localhost"))
297
+ return true;
298
+ return resolveDotSegments(req.url);
299
+ }
300
+ if (!(unparsedBunTargets && (validateHost || needsResolving(req.url))))
262
301
  return false;
263
302
  const host = headerOf(req, "host");
264
303
  if (host !== null && invalidHost(host)) {
265
- if (rawRequestTargets)
266
- return true;
267
304
  // Bun served it anyway: it could not read the Host, so it left the target unparsed, or it
268
305
  // built the URL on it - sent twice (joined with ", ", the URL on the first; RFC 9112 section
269
- // 3.2 answers 400), percent-encoded (`%61.example` routed as `a.example`), or with a
270
- // sub-delimiter (`a!b`). A request built in process keeps the URL its caller built, unless Bun
306
+ // 3.2 answers 400), percent-encoded (%61.example routed as a.example), or with a
307
+ // sub-delimiter (a!b). A request built in process keeps the URL its caller built, unless Bun
271
308
  // would have built it so.
272
309
  const routed = URL.parse(req.url)?.host;
273
310
  return routed === undefined || URL.parse(`//${host.split(", ")[0]}`, req.url)?.host === routed;
274
311
  }
275
- if (rawRequestTargets)
276
- return needsResolving(req.url) && resolveDotSegments(req.url);
277
312
  // A request built in process carries an absolute URL whatever its headers say.
278
313
  return (host === null &&
279
314
  req.url.charCodeAt(0) === 47 /* / */ &&
@@ -307,7 +342,8 @@ function mountPriorityOf(priority) {
307
342
  function compareMounts(a, b) {
308
343
  return b.priority - a.priority || b.path.length - a.path.length || a.order - b.order;
309
344
  }
310
- /** Rebase a mounted request's path without losing the authentication mark used by child caches. */
345
+ /** Rebase a mounted request's path without losing the authentication mark used by child caches, or
346
+ * its request locals. */
311
347
  function stripMountPrefix(request, prefix) {
312
348
  if (prefix === "/")
313
349
  return request;
@@ -317,11 +353,39 @@ function stripMountPrefix(request, prefix) {
317
353
  const childRequest = new Request(url.href, request);
318
354
  if (isAuthenticatedRequest(request))
319
355
  markAuthenticatedRequest(childRequest);
356
+ carryToCopy(request, childRequest);
320
357
  return childRequest;
321
358
  }
322
359
  function responseSet(ctx) {
323
360
  return ctx[CONTEXT_SET]() ?? EMPTY_RESPONSE_CONTROLS;
324
361
  }
362
+ /** The error hooks of the many fused routes that have none, shared so each does not allocate its own. */
363
+ const NO_ERROR_HOOKS = [];
364
+ const WEB_STAGES = new WeakMap();
365
+ const NODE_STAGES = new WeakMap();
366
+ const hookCount = (hooks) => hooks.length > 1 ? 2 : hooks.length;
367
+ /**
368
+ * The fused stage chain of a hook shape, built once per server and handed to every route with the same
369
+ * number of derive, before and after hooks (none, one or several of each). The chain reads a route's
370
+ * hooks and handler from the `FusedHooks` it is called with, so sharing it changes no answer.
371
+ */
372
+ function sharedStages(cache, owner, route, build) {
373
+ const derives = hookCount(route.derives);
374
+ const befores = hookCount(route.befores);
375
+ const afters = hookCount(route.afters);
376
+ const key = derives * 9 + befores * 3 + afters;
377
+ let chains = cache.get(owner);
378
+ if (chains === undefined) {
379
+ chains = new Map();
380
+ cache.set(owner, chains);
381
+ }
382
+ let stages = chains.get(key);
383
+ if (stages === undefined) {
384
+ stages = build(derives, befores, afters);
385
+ chains.set(key, stages);
386
+ }
387
+ return stages;
388
+ }
325
389
  function isContextlessNoArgArrow(handler) {
326
390
  if (handler.length !== 0)
327
391
  return false;
@@ -655,6 +719,7 @@ export class Server {
655
719
  /** `trustBodyFraming`: mark every `app.fetch` request as runtime-framed (see ServerOptions). */
656
720
  trustBodyFraming;
657
721
  validateHost;
722
+ normalizeTarget;
658
723
  wsMaxPayloadBytes;
659
724
  requestTimeoutMs;
660
725
  wsUpgradeTimeoutMs;
@@ -788,6 +853,7 @@ export class Server {
788
853
  this.formOrigins = options.formOrigins;
789
854
  this.trustBodyFraming = options.trustBodyFraming ?? false;
790
855
  this.validateHost = options.validateHost ?? false;
856
+ this.normalizeTarget = options.normalizeTarget ?? false;
791
857
  const wsMaxPayloadBytes = options.wsMaxPayloadBytes ?? maxBodyBytes;
792
858
  assertByteLimit(wsMaxPayloadBytes, "wsMaxPayloadBytes");
793
859
  this.maxBodyBytes = maxBodyBytes;
@@ -1642,39 +1708,42 @@ export class Server {
1642
1708
  handler = this.idempotencyRuntime.wrapHandler(handler);
1643
1709
  }
1644
1710
  const { bare, fusedQuery, fusedBody } = lanes;
1645
- // Fused lifecycle lanes: derive + before (with or without an after), and body + the same shape.
1646
- // The lane selectors in `selectRouteLanes` are exhaustive about the lifecycleHookLane /
1647
- // body-derive-before-after eligibility, so we only need to check the lane and the hook counts.
1648
- const lifecycleHookLane = lanes.lifecycleHookLane;
1649
- const isFusedLifecycle = lifecycleHookLane !== undefined && this.derives.length === 1;
1650
- const isFusedBodyLifecycle = lanes.fusedLane === "body-derive-before" || lanes.fusedLane === "body-derive-before-after";
1651
- // The generic runners below are the Node-direct twins of the fused Web lanes built further
1652
- // down: same single-frame lifecycle, finalizing through the caller's own `finalize` instead of
1653
- // building a `Response`, so the `!webFast` dispatcher prefers them over the generic program
1654
- // exactly as it prefers the body-only runner. Only the original body-only lane may feed the Web
1655
- // wrapper; every other runner here preserves its derive/before/after stages.
1711
+ // `selectRouteLanes` admits the fused hook lanes only for derive, before and after hooks, so the
1712
+ // three arrays below are the route's whole lifecycle.
1713
+ const fusedLifecycle = lanes.fusedLane === "hooks" || lanes.fusedLane === "body-hooks";
1714
+ // The fused chains keep the arrays they are built from, and `beforeHandle()` inserts into the live
1715
+ // ones ahead of `beforeHandler()` guards, so the entry and the fused chains share one snapshot of
1716
+ // this route's.
1717
+ const derivesSnapshot = [...this.derives];
1718
+ const beforesSnapshot = [...beforeHandle];
1719
+ const aftersSnapshot = [...afterHandle];
1720
+ const onErrorSnapshot = onError.length === 0 ? NO_ERROR_HOOKS : [...onError];
1721
+ const fusedHooks = fusedLifecycle
1722
+ ? {
1723
+ handler: handler,
1724
+ derives: derivesSnapshot,
1725
+ befores: beforesSnapshot,
1726
+ afters: aftersSnapshot,
1727
+ errors: onErrorSnapshot,
1728
+ decorations: hasDecorations ? routeDecorations : undefined,
1729
+ }
1730
+ : undefined;
1731
+ // The Node-direct dispatcher (`!webFast`) prefers these runners over the generic program. Only the
1732
+ // body-only runner also feeds the Web wrapper; the lifecycle lanes build their own Web closure.
1656
1733
  const fusedBodyRunner = fusedBody
1657
1734
  ? this.buildFusedBodyRunner(handler, schema?.body, hasDecorations ? routeDecorations : undefined, bodyLimit ?? UNLIMITED_BODY_BYTES)
1658
- : isFusedBodyLifecycle
1659
- ? this.buildFusedBodyDeriveBeforeAfterRunner(handler, this.derives[0], beforeHandle[0], afterHandle[0], schema?.body, bodyLimit ?? UNLIMITED_BODY_BYTES)
1660
- : isFusedLifecycle && lifecycleHookLane === "derive-before"
1661
- ? this.buildFusedDeriveBeforeRunner(handler, this.derives[0], beforeHandle[0], undefined, schema?.query, bodyLimit ?? UNLIMITED_BODY_BYTES)
1662
- : isFusedLifecycle && lifecycleHookLane === "derive-before-after"
1663
- ? this.buildFusedDeriveBeforeRunner(handler, this.derives[0], beforeHandle[0], afterHandle[0], schema?.query, bodyLimit ?? UNLIMITED_BODY_BYTES)
1664
- : undefined;
1735
+ : fusedHooks !== undefined
1736
+ ? this.buildFusedLifecycleRunner(fusedHooks, schema?.query, schema?.body, bodyLimit ?? UNLIMITED_BODY_BYTES)
1737
+ : undefined;
1665
1738
  const fusedWeb = bare && this.aroundHooks.length === 0
1666
1739
  ? this.buildFusedWeb(handler, hasDecorations ? routeDecorations : undefined, isContextlessNoArgArrow(handler), bodyLimit ?? UNLIMITED_BODY_BYTES)
1667
1740
  : fusedQuery
1668
1741
  ? this.buildFusedQueryWeb(handler, hasDecorations ? routeDecorations : undefined, schema?.query, bodyLimit ?? UNLIMITED_BODY_BYTES)
1669
1742
  : fusedBody
1670
1743
  ? this.buildFusedBodyWeb(fusedBodyRunner)
1671
- : isFusedBodyLifecycle
1672
- ? this.buildFusedBodyDeriveBeforeAfter(handler, this.derives[0], beforeHandle[0], afterHandle[0], schema?.body, bodyLimit ?? UNLIMITED_BODY_BYTES)
1673
- : isFusedLifecycle && lifecycleHookLane === "derive-before"
1674
- ? this.buildFusedDeriveBefore(handler, this.derives[0], beforeHandle[0], schema?.query, bodyLimit ?? UNLIMITED_BODY_BYTES)
1675
- : isFusedLifecycle && lifecycleHookLane === "derive-before-after"
1676
- ? this.buildFusedDeriveBeforeAfter(handler, this.derives[0], beforeHandle[0], afterHandle[0], schema?.query, bodyLimit ?? UNLIMITED_BODY_BYTES)
1677
- : undefined;
1744
+ : fusedHooks !== undefined
1745
+ ? this.buildFusedLifecycleWeb(fusedHooks, schema?.query, schema?.body, bodyLimit ?? UNLIMITED_BODY_BYTES)
1746
+ : undefined;
1678
1747
  const program = compileRouteProgram({
1679
1748
  schema,
1680
1749
  handler: handler,
@@ -1706,13 +1775,13 @@ export class Server {
1706
1775
  idempotent,
1707
1776
  ledgered,
1708
1777
  responseContract,
1709
- derives: [...this.derives],
1778
+ derives: derivesSnapshot,
1710
1779
  authStages: [...compiled.authStages],
1711
1780
  decorations: routeDecorations,
1712
1781
  hasDecorations,
1713
- beforeHandle: [...beforeHandle],
1714
- afterHandle: [...afterHandle],
1715
- onError: [...onError],
1782
+ beforeHandle: beforesSnapshot,
1783
+ afterHandle: aftersSnapshot,
1784
+ onError: onErrorSnapshot,
1716
1785
  lifecycleHookLane: lanes.lifecycleHookLane,
1717
1786
  around: [...this.aroundHooks],
1718
1787
  execution,
@@ -2113,49 +2182,29 @@ export class Server {
2113
2182
  const lane = entry.execution.fusedLane;
2114
2183
  // Type-erase the lane so each `===` branch sees the full union, not the narrowed remainder.
2115
2184
  const laneName = lane;
2116
- // Body+lifecycle routes have a fused Web renderer, and - unlike before - a generic fused
2117
- // runner too: it preserves derive/before/after (it IS the lifecycle, single-framed), so the
2118
- // Node-direct dispatcher's preference for the generic slot over the execution plan stays
2119
- // semantics-preserving. Only the original body-only lane may feed the Web wrapper below.
2120
- const derive = entry.derives[0];
2121
- const before = entry.beforeHandle[0];
2122
- const after = entry.afterHandle[0];
2185
+ const lifecycle = laneName === "hooks" || laneName === "body-hooks";
2186
+ const hooks = lifecycle
2187
+ ? {
2188
+ handler: entry.handler,
2189
+ derives: entry.derives,
2190
+ befores: entry.beforeHandle,
2191
+ afters: entry.afterHandle,
2192
+ errors: entry.onError,
2193
+ decorations: entry.hasDecorations ? entry.decorations : undefined,
2194
+ }
2195
+ : undefined;
2123
2196
  const fusedBody = laneName === "body"
2124
2197
  ? this.buildFusedBodyRunner(entry.handler, entry.schema?.body, entry.hasDecorations ? entry.decorations : undefined, entry.bodyLimit ?? UNLIMITED_BODY_BYTES)
2125
- : laneName === "body-derive-before" && derive !== undefined && before !== undefined
2126
- ? this.buildFusedBodyDeriveBeforeAfterRunner(entry.handler, derive, before, undefined, entry.schema?.body, entry.bodyLimit ?? UNLIMITED_BODY_BYTES)
2127
- : laneName === "body-derive-before-after" &&
2128
- derive !== undefined &&
2129
- before !== undefined &&
2130
- after !== undefined
2131
- ? this.buildFusedBodyDeriveBeforeAfterRunner(entry.handler, derive, before, after, entry.schema?.body, entry.bodyLimit ?? UNLIMITED_BODY_BYTES)
2132
- : laneName === "derive-before" && derive !== undefined && before !== undefined
2133
- ? this.buildFusedDeriveBeforeRunner(entry.handler, derive, before, undefined, entry.schema?.query, entry.bodyLimit ?? UNLIMITED_BODY_BYTES)
2134
- : laneName === "derive-before-after" &&
2135
- derive !== undefined &&
2136
- before !== undefined &&
2137
- after !== undefined
2138
- ? this.buildFusedDeriveBeforeRunner(entry.handler, derive, before, after, entry.schema?.query, entry.bodyLimit ?? UNLIMITED_BODY_BYTES)
2139
- : undefined;
2198
+ : hooks !== undefined
2199
+ ? this.buildFusedLifecycleRunner(hooks, entry.schema?.query, entry.schema?.body, entry.bodyLimit ?? UNLIMITED_BODY_BYTES)
2200
+ : undefined;
2140
2201
  const fusedWeb = laneName === "body"
2141
2202
  ? this.buildFusedBodyWeb(fusedBody)
2142
2203
  : laneName === "query"
2143
2204
  ? this.buildFusedQueryWeb(entry.handler, entry.hasDecorations ? entry.decorations : undefined, entry.schema?.query, entry.bodyLimit ?? UNLIMITED_BODY_BYTES)
2144
- : laneName === "body-derive-before" && derive !== undefined && before !== undefined
2145
- ? this.buildFusedBodyDeriveBeforeAfter(entry.handler, derive, before, undefined, entry.schema?.body, entry.bodyLimit ?? UNLIMITED_BODY_BYTES)
2146
- : laneName === "body-derive-before-after" &&
2147
- derive !== undefined &&
2148
- before !== undefined &&
2149
- after !== undefined
2150
- ? this.buildFusedBodyDeriveBeforeAfter(entry.handler, derive, before, after, entry.schema?.body, entry.bodyLimit ?? UNLIMITED_BODY_BYTES)
2151
- : laneName === "derive-before" && derive !== undefined && before !== undefined
2152
- ? this.buildFusedDeriveBefore(entry.handler, derive, before, entry.schema?.query, entry.bodyLimit ?? UNLIMITED_BODY_BYTES)
2153
- : laneName === "derive-before-after" &&
2154
- derive !== undefined &&
2155
- before !== undefined &&
2156
- after !== undefined
2157
- ? this.buildFusedDeriveBeforeAfter(entry.handler, derive, before, after, entry.schema?.query, entry.bodyLimit ?? UNLIMITED_BODY_BYTES)
2158
- : this.buildFusedWeb(entry.handler, entry.hasDecorations ? entry.decorations : undefined, isContextlessNoArgArrow(entry.handler), entry.bodyLimit ?? UNLIMITED_BODY_BYTES);
2205
+ : hooks !== undefined
2206
+ ? this.buildFusedLifecycleWeb(hooks, entry.schema?.query, entry.schema?.body, entry.bodyLimit ?? UNLIMITED_BODY_BYTES)
2207
+ : this.buildFusedWeb(entry.handler, entry.hasDecorations ? entry.decorations : undefined, isContextlessNoArgArrow(entry.handler), entry.bodyLimit ?? UNLIMITED_BODY_BYTES);
2159
2208
  return {
2160
2209
  ...route,
2161
2210
  entry: {
@@ -2188,7 +2237,7 @@ export class Server {
2188
2237
  fetch(req, platform) {
2189
2238
  // Deno's `Request.url` is the target as sent; Bun, workerd and every `new Request()` parse it.
2190
2239
  // Route what the parsed URL says, so `/users/../admin` is `/admin` on every runtime.
2191
- const target = requestTarget(req, this.validateHost);
2240
+ const target = requestTarget(req, this.validateHost, this.normalizeTarget);
2192
2241
  if (target === true)
2193
2242
  return jsonError(400, "invalid_host");
2194
2243
  if (target !== false) {
@@ -2238,6 +2287,23 @@ export class Server {
2238
2287
  }
2239
2288
  return this.admitGated(requestOf(source), () => this.fetchMatchedInner(source, entry, params, platform));
2240
2289
  }
2290
+ /** {@link fetchMatched} for an app whose `onRequest` hooks run before the route does: the hooks see
2291
+ * the request first, and the route Bun matched runs unless one of them replaced the request. */
2292
+ fetchMatchedHooked(source, entry, params, platform) {
2293
+ if (this.capacityGate === undefined) {
2294
+ return this.fetchMatchedHookedInner(source, entry, params, platform);
2295
+ }
2296
+ return this.admitGated(source, () => this.fetchMatchedHookedInner(source, entry, params, platform));
2297
+ }
2298
+ fetchMatchedHookedInner(source, entry, params, platform) {
2299
+ const outcome = this.runWithOnRequest(source, this.trustedPlatform(source, platform), this.finalizeResponse, this.wrapWebResponse, this.webResponseTimeout, true, { entry, params });
2300
+ if (this.onResponseHooks.length === 0 && this.onResponseFinalizedHooks.length === 0) {
2301
+ return outcome;
2302
+ }
2303
+ return outcome instanceof Promise
2304
+ ? outcome.then((response) => this.applyOnResponseAndFinalize(response, this.takeResponseRequest(source)))
2305
+ : this.applyOnResponseAndFinalize(outcome, this.takeResponseRequest(source));
2306
+ }
2241
2307
  fetchMatchedInner(source, entry, params, platform) {
2242
2308
  const outcome = this.runMatched(source, platform, entry, params, undefined, this.finalizeResponse, this.wrapWebResponse, this.webResponseTimeout, true);
2243
2309
  if (this.onResponseHooks.length === 0 && this.onResponseFinalizedHooks.length === 0) {
@@ -2597,7 +2663,7 @@ export class Server {
2597
2663
  // The adapter answers a pass by handing this same object to `fetch`.
2598
2664
  const received = req;
2599
2665
  // The handshake routes the path `fetch` would: Host checked, dot segments resolved (see `fetch`).
2600
- const target = requestTarget(req, this.validateHost);
2666
+ const target = requestTarget(req, this.validateHost, this.normalizeTarget);
2601
2667
  if (target === true) {
2602
2668
  return { kind: "reject", response: this.wrapWebResponse(jsonError(400, "invalid_host")) };
2603
2669
  }
@@ -2786,10 +2852,10 @@ export class Server {
2786
2852
  /** Bun `fetch` when WS routes exist: try a WS upgrade first, else run the normal HTTP lifecycle.
2787
2853
  * `undefined` ⇒ Bun owns the upgraded socket; a `Response` ⇒ a normal reply or a rejected upgrade.
2788
2854
  * (The socket dispatch itself lives in `ws-bun.ts`, loaded via `@nifrajs/core/ws`.) */
2789
- bunFetchWithWebSocket(req, server) {
2855
+ bunFetchWithWebSocket(req, server, setIdleTimeout) {
2790
2856
  const handle = (o) => {
2791
2857
  if (o.kind === "pass")
2792
- return this.fetch(req, bunPeerPlatform(server, req));
2858
+ return this.fetch(req, new BunPeerPlatform(server, req, setIdleTimeout));
2793
2859
  if (o.kind === "reject")
2794
2860
  return o.response;
2795
2861
  // A copy: the Bun socket keeps per-connection state on it, and a hand-built outcome may be shared.
@@ -2797,7 +2863,7 @@ export class Server {
2797
2863
  ? undefined
2798
2864
  : jsonError(426, "upgrade_required");
2799
2865
  };
2800
- const outcome = this.resolveWebSocketUpgrade(req, bunPeerPlatform(server, req));
2866
+ const outcome = this.resolveWebSocketUpgrade(req, new BunPeerPlatform(server, req, setIdleTimeout));
2801
2867
  return outcome instanceof Promise ? outcome.then(handle) : handle(outcome);
2802
2868
  }
2803
2869
  /**
@@ -2898,7 +2964,7 @@ export class Server {
2898
2964
  * at ~13% of a realistic request. The first hook that returns a Promise hands the REMAINING
2899
2965
  * hooks to the async continuation; behavior is identical.
2900
2966
  */
2901
- runWithOnRequest(source, platform, finalize, wrapResponse, onTimeout, webFast) {
2967
+ runWithOnRequest(source, platform, finalize, wrapResponse, onTimeout, webFast, matched) {
2902
2968
  const hooks = this.onRequestHooks;
2903
2969
  const originalRequest = requestOf(source);
2904
2970
  // A Web Request is already the exact object visible to the hooks and the response walk. Only
@@ -2917,7 +2983,7 @@ export class Server {
2917
2983
  return this.onRequestFailure(err, current, platform, finalize, wrapResponse);
2918
2984
  }
2919
2985
  if (outcome instanceof Promise) {
2920
- return outcome.then((early) => this.continueOnRequest(early, i + 1, originalRequest, current, platform, finalize, wrapResponse, onTimeout, webFast), (err) => this.onRequestFailure(err, current, platform, finalize, wrapResponse));
2986
+ return outcome.then((early) => this.continueOnRequest(early, i + 1, originalRequest, current, platform, finalize, wrapResponse, onTimeout, webFast, matched), (err) => this.onRequestFailure(err, current, platform, finalize, wrapResponse));
2921
2987
  }
2922
2988
  if (outcome instanceof Request) {
2923
2989
  if (outcome !== seen)
@@ -2930,11 +2996,11 @@ export class Server {
2930
2996
  if (outcome !== undefined)
2931
2997
  return earlyAnswer(outcome, wrapResponse);
2932
2998
  }
2933
- return this.routeAndRun(current, platform, finalize, wrapResponse, onTimeout, webFast);
2999
+ return this.routeAndRun(current, platform, finalize, wrapResponse, onTimeout, webFast, current === originalRequest ? matched : undefined);
2934
3000
  }
2935
3001
  /** Async tail of {@link runWithOnRequest}: applies the first awaited hook's outcome, then runs
2936
3002
  * the remaining hooks (awaiting freely - we're already async here). */
2937
- async continueOnRequest(first, nextIndex, originalRequest, sourceAtAwait, platform, finalize, wrapResponse, onTimeout, webFast) {
3003
+ async continueOnRequest(first, nextIndex, originalRequest, sourceAtAwait, platform, finalize, wrapResponse, onTimeout, webFast, matched) {
2938
3004
  let current = sourceAtAwait;
2939
3005
  let seen = requestOf(sourceAtAwait);
2940
3006
  let early = first;
@@ -2962,7 +3028,7 @@ export class Server {
2962
3028
  }
2963
3029
  index++;
2964
3030
  }
2965
- return this.routeAndRun(current, platform, finalize, wrapResponse, onTimeout, webFast);
3031
+ return this.routeAndRun(current, platform, finalize, wrapResponse, onTimeout, webFast, current === originalRequest ? matched : undefined);
2966
3032
  }
2967
3033
  takeResponseRequest(source) {
2968
3034
  const tracked = this.responseSources.get(source);
@@ -3031,7 +3097,12 @@ export class Server {
3031
3097
  * (selected by its compiled plan), so a sync handler produces its result with zero promise allocations;
3032
3098
  * routes with validation/hooks keep the full compiled route program, unchanged.
3033
3099
  */
3034
- routeAndRun(source, platform, finalize, wrapResponse, onTimeout, webFast) {
3100
+ routeAndRun(source, platform, finalize, wrapResponse, onTimeout, webFast,
3101
+ // The route Bun's table already matched for this very request, which routing would find again.
3102
+ matched) {
3103
+ if (matched !== undefined) {
3104
+ return this.runMatched(source, platform, matched.entry, matched.params, undefined, finalize, wrapResponse, onTimeout, webFast);
3105
+ }
3035
3106
  // Native delimiter searches keep path/query splitting out of a character-by-character JS loop.
3036
3107
  // Keep the slices in locals instead of allocating the public helper's pathname/search pair.
3037
3108
  let pathname;
@@ -3521,53 +3592,244 @@ export class Server {
3521
3592
  };
3522
3593
  }
3523
3594
  /**
3524
- * Compile a route's `derive + before` lifecycle to a single closure: the per-request walk through
3525
- * `runSync` → `runHooksSync` is a stack of 3 frames and 2 `Object.assign`s for what is by far the
3526
- * most common middleware shape (auth/trace/id derivation, an auth/policy `beforeHandle`). Folding
3527
- * the same steps into one closure hits the same monomorphic inline-cache site as `buildFusedWeb`,
3528
- * and `Object.assign(ctx, deriveResult)` becomes one extra property write on the context's already
3529
- * hot hidden class. Behavior is byte-identical to the compiled route program: the same thrown-value
3530
- * contract (`Response` is control flow, anything else logs + 500), the same `c.set` semantics, the
3531
- * same `fusedRespond` for the rendered body. The `LifecycleExecutionLane` guarantee
3532
- * (`schema.params === undefined && schema.headers === undefined && derives === 1 &&
3533
- * beforeHandle === 1 && afterHandle === 0 && onError === 0 && !hasResponseContract &&
3534
- * !hasDecorations`) is enforced by `selectRouteLanes`; this lane is selected ONLY when all of those
3535
- * hold. A `query` schema (the route declares `{ query }` alongside the hooks) is validated FIRST,
3536
- * mirroring the compiled route program's stage order; the fused builder runs validate → derive →
3537
- * before → handler in one frame.
3595
+ * The stage chain a hook shape runs - derive, before, handler, after, respond - for the Web lane,
3596
+ * built once per server and shared by every route with the same number of hooks in each phase (none,
3597
+ * one or several). The chain reads a route's handler and hooks from its `FusedHooks` per request, so
3598
+ * a route's own closure (see {@link buildFusedLifecycleWeb}) holds only that record and its schemas,
3599
+ * and memory grows with the number of distinct shapes, not routes or hook functions.
3538
3600
  */
3539
- buildFusedDeriveBefore(handler, derive, before, querySchema, maxBodyBytes) {
3540
- return this.buildFusedDeriveBeforeAfter(handler, derive, before, undefined, querySchema, maxBodyBytes);
3541
- }
3542
- // Both hook variants share one registered chain; request contexts stay in arguments.
3543
- buildFusedDeriveBeforeAfter(handler, derive, before, after, querySchema, maxBodyBytes) {
3544
- const logError = (err, ctx) => this.bareError(err, ctx, (result, set) => this.wrapWebResponse(toResponse(result, set)), this.wrapWebResponse);
3545
- // Continuations capture route state once; each request supplies its own context.
3546
- const runAfter = (result, ctx) => {
3547
- if (after === undefined)
3548
- return this.respondFused(result, ctx, this.onResponseHooks.length === 0);
3549
- let transformed;
3601
+ fusedWebStages(deriveCount, beforeCount, afterCount) {
3602
+ let runAfter;
3603
+ if (afterCount === 0) {
3604
+ runAfter = (result, ctx, route) => this.respondHooks(result, ctx, route, this.onResponseHooks.length === 0);
3605
+ }
3606
+ else if (afterCount === 1) {
3607
+ runAfter = (result, ctx, route) => {
3608
+ let transformed;
3609
+ try {
3610
+ transformed = route.afters[0](result, ctx);
3611
+ }
3612
+ catch (err) {
3613
+ return this.fusedWebRouteFail(err, ctx, route);
3614
+ }
3615
+ if (transformed instanceof Promise) {
3616
+ return transformed.then((value) => this.respondHooks(value, ctx, route, this.onResponseHooks.length === 0), (err) => this.fusedWebRouteFail(err, ctx, route));
3617
+ }
3618
+ return this.respondHooks(transformed, ctx, route, this.onResponseHooks.length === 0);
3619
+ };
3620
+ }
3621
+ else {
3622
+ runAfter = (result, ctx, route, index) => {
3623
+ const lastAfter = route.afters.length - 1;
3624
+ for (;;) {
3625
+ let transformed;
3626
+ try {
3627
+ transformed = route.afters[index](result, ctx);
3628
+ }
3629
+ catch (err) {
3630
+ return this.fusedWebRouteFail(err, ctx, route);
3631
+ }
3632
+ if (transformed instanceof Promise) {
3633
+ return transformed.then((value) => index < lastAfter
3634
+ ? runAfter(value, ctx, route, index + 1)
3635
+ : this.respondHooks(value, ctx, route, this.onResponseHooks.length === 0), (err) => this.fusedWebRouteFail(err, ctx, route));
3636
+ }
3637
+ if (index === lastAfter) {
3638
+ return this.respondHooks(transformed, ctx, route, this.onResponseHooks.length === 0);
3639
+ }
3640
+ result = transformed;
3641
+ index++;
3642
+ }
3643
+ };
3644
+ }
3645
+ const runHandler = (ctx, route) => {
3646
+ let result;
3550
3647
  try {
3551
- transformed = after(result, ctx);
3648
+ result = route.handler(ctx);
3552
3649
  }
3553
3650
  catch (err) {
3554
- return logError(err, ctx);
3651
+ return this.fusedWebRouteFail(err, ctx, route);
3555
3652
  }
3556
- if (transformed instanceof Promise) {
3557
- return transformed.then((value) => this.respondFused(value, ctx, this.onResponseHooks.length === 0), (err) => logError(err, ctx));
3653
+ if (result instanceof Promise) {
3654
+ return result.then((value) => runAfter(value, ctx, route, 0), (err) => this.fusedWebRouteFail(err, ctx, route));
3558
3655
  }
3559
- return this.respondFused(transformed, ctx, this.onResponseHooks.length === 0);
3656
+ return runAfter(result, ctx, route, 0);
3560
3657
  };
3658
+ let runBefore;
3659
+ if (beforeCount === 0) {
3660
+ runBefore = runHandler;
3661
+ }
3662
+ else if (beforeCount === 1) {
3663
+ runBefore = (ctx, route) => {
3664
+ let early;
3665
+ try {
3666
+ early = route.befores[0](ctx);
3667
+ }
3668
+ catch (err) {
3669
+ return this.fusedWebRouteFail(err, ctx, route);
3670
+ }
3671
+ if (early instanceof Promise) {
3672
+ return early.then((settled) => settled === undefined
3673
+ ? runHandler(ctx, route, 0)
3674
+ : this.respondHooks(settled, ctx, route), (err) => this.fusedWebRouteFail(err, ctx, route));
3675
+ }
3676
+ if (early === undefined)
3677
+ return runHandler(ctx, route, 0);
3678
+ return this.respondHooks(early, ctx, route);
3679
+ };
3680
+ }
3681
+ else {
3682
+ runBefore = (ctx, route, index) => {
3683
+ const lastBefore = route.befores.length - 1;
3684
+ for (;;) {
3685
+ let early;
3686
+ try {
3687
+ early = route.befores[index](ctx);
3688
+ }
3689
+ catch (err) {
3690
+ return this.fusedWebRouteFail(err, ctx, route);
3691
+ }
3692
+ if (early instanceof Promise) {
3693
+ return early.then((settled) => {
3694
+ if (settled !== undefined)
3695
+ return this.respondHooks(settled, ctx, route);
3696
+ return index < lastBefore
3697
+ ? runBefore(ctx, route, index + 1)
3698
+ : runHandler(ctx, route, 0);
3699
+ }, (err) => this.fusedWebRouteFail(err, ctx, route));
3700
+ }
3701
+ if (early !== undefined)
3702
+ return this.respondHooks(early, ctx, route);
3703
+ if (index === lastBefore)
3704
+ return runHandler(ctx, route, 0);
3705
+ index++;
3706
+ }
3707
+ };
3708
+ }
3709
+ let runDerive;
3710
+ if (deriveCount === 0) {
3711
+ runDerive = runBefore;
3712
+ }
3713
+ else if (deriveCount === 1) {
3714
+ runDerive = (ctx, route) => {
3715
+ let derived;
3716
+ try {
3717
+ derived = route.derives[0](ctx);
3718
+ }
3719
+ catch (err) {
3720
+ return this.fusedWebRouteFail(err, ctx, route);
3721
+ }
3722
+ if (derived instanceof Promise) {
3723
+ return derived.then((settled) => {
3724
+ if (isResponseResult(settled) || settled instanceof Response) {
3725
+ return this.respondHooks(settled, ctx, route);
3726
+ }
3727
+ try {
3728
+ Object.assign(ctx, settled);
3729
+ }
3730
+ catch (err) {
3731
+ return this.fusedWebRouteFail(err, ctx, route);
3732
+ }
3733
+ return runBefore(ctx, route, 0);
3734
+ }, (err) => this.fusedWebRouteFail(err, ctx, route));
3735
+ }
3736
+ if (isResponseResult(derived) || derived instanceof Response) {
3737
+ return this.respondHooks(derived, ctx, route);
3738
+ }
3739
+ try {
3740
+ Object.assign(ctx, derived);
3741
+ }
3742
+ catch (err) {
3743
+ return this.fusedWebRouteFail(err, ctx, route);
3744
+ }
3745
+ return runBefore(ctx, route, 0);
3746
+ };
3747
+ }
3748
+ else {
3749
+ runDerive = (ctx, route, index) => {
3750
+ const lastDerive = route.derives.length - 1;
3751
+ for (;;) {
3752
+ let derived;
3753
+ try {
3754
+ derived = route.derives[index](ctx);
3755
+ }
3756
+ catch (err) {
3757
+ return this.fusedWebRouteFail(err, ctx, route);
3758
+ }
3759
+ if (derived instanceof Promise) {
3760
+ return derived.then((settled) => {
3761
+ if (isResponseResult(settled) || settled instanceof Response) {
3762
+ return this.respondHooks(settled, ctx, route);
3763
+ }
3764
+ try {
3765
+ Object.assign(ctx, settled);
3766
+ }
3767
+ catch (err) {
3768
+ return this.fusedWebRouteFail(err, ctx, route);
3769
+ }
3770
+ return index < lastDerive
3771
+ ? runDerive(ctx, route, index + 1)
3772
+ : runBefore(ctx, route, 0);
3773
+ }, (err) => this.fusedWebRouteFail(err, ctx, route));
3774
+ }
3775
+ if (isResponseResult(derived) || derived instanceof Response) {
3776
+ return this.respondHooks(derived, ctx, route);
3777
+ }
3778
+ try {
3779
+ Object.assign(ctx, derived);
3780
+ }
3781
+ catch (err) {
3782
+ return this.fusedWebRouteFail(err, ctx, route);
3783
+ }
3784
+ if (index === lastDerive)
3785
+ return runBefore(ctx, route, 0);
3786
+ index++;
3787
+ }
3788
+ };
3789
+ }
3790
+ return runDerive;
3791
+ }
3792
+ /**
3793
+ * Compile a fused lifecycle route's entry closure: parse (body) → validate → the shared stage chain
3794
+ * of its hook shape (derive → before → handler → after → respond, see {@link fusedWebStages}) in a
3795
+ * single frame, where the generic program walks staged hook loops. `selectRouteLanes` admits only
3796
+ * derive, before and after hooks - no params or headers schema, onError, around, decorations,
3797
+ * response contract or recovery hook, and never a query schema beside a body schema - so the behavior
3798
+ * is the compiled program's: the same stage order, the same thrown-value contract (`Response` is
3799
+ * control flow, anything else logs and 500s), the same `c.set` semantics and `fusedRespond` render.
3800
+ * The route's own closure holds only its `FusedHooks` and schemas and takes the request's context as
3801
+ * an argument, so a request allocates no step closures.
3802
+ */
3803
+ /**
3804
+ * Keep the common one-derive/one-before shape bound to its hooks. Inline its synchronous path to
3805
+ * avoid stage calls; retain helper closures only for promise continuations.
3806
+ */
3807
+ buildFusedLifecycleWebSingle(handler, derive, before, after, maxBodyBytes) {
3808
+ const runAfter = after === undefined
3809
+ ? (result, ctx) => this.respondFused(result, ctx, this.onResponseHooks.length === 0)
3810
+ : (result, ctx) => {
3811
+ let transformed;
3812
+ try {
3813
+ transformed = after(result, ctx);
3814
+ }
3815
+ catch (err) {
3816
+ return this.fusedWebFail(err, ctx);
3817
+ }
3818
+ if (transformed instanceof Promise) {
3819
+ return transformed.then((value) => this.respondFused(value, ctx, this.onResponseHooks.length === 0), (err) => this.fusedWebFail(err, ctx));
3820
+ }
3821
+ return this.respondFused(transformed, ctx, this.onResponseHooks.length === 0);
3822
+ };
3561
3823
  const runHandler = (ctx) => {
3562
3824
  let result;
3563
3825
  try {
3564
3826
  result = handler(ctx);
3565
3827
  }
3566
3828
  catch (err) {
3567
- return logError(err, ctx);
3829
+ return this.fusedWebFail(err, ctx);
3568
3830
  }
3569
3831
  if (result instanceof Promise) {
3570
- return result.then((value) => runAfter(value, ctx), (err) => logError(err, ctx));
3832
+ return result.then((value) => runAfter(value, ctx), (err) => this.fusedWebFail(err, ctx));
3571
3833
  }
3572
3834
  return runAfter(result, ctx);
3573
3835
  };
@@ -3577,461 +3839,463 @@ export class Server {
3577
3839
  early = before(ctx);
3578
3840
  }
3579
3841
  catch (err) {
3580
- return logError(err, ctx);
3842
+ return this.fusedWebFail(err, ctx);
3581
3843
  }
3582
3844
  if (early instanceof Promise) {
3583
- return early.then((settled) => {
3584
- if (settled === undefined)
3585
- return runHandler(ctx);
3586
- return this.respondFused(settled, ctx);
3587
- }, (err) => logError(err, ctx));
3845
+ return early.then((settled) => (settled === undefined ? runHandler(ctx) : this.respondFused(settled, ctx)), (err) => this.fusedWebFail(err, ctx));
3588
3846
  }
3589
3847
  if (early === undefined)
3590
3848
  return runHandler(ctx);
3591
3849
  return this.respondFused(early, ctx);
3592
3850
  };
3593
- const runDerive = (ctx) => {
3851
+ return (source, params, search, signal, budget, platform, nativeContext) => {
3852
+ const ctx = nativeContext
3853
+ ? RequestContext.native(source, params, search, maxBodyBytes, platform, this.protoPoisoning)
3854
+ : new RequestContext(source, params, search, signal, budget, platform, maxBodyBytes, this.protoPoisoning);
3594
3855
  let derived;
3595
3856
  try {
3596
3857
  derived = derive(ctx);
3597
3858
  }
3598
3859
  catch (err) {
3599
- return logError(err, ctx);
3860
+ return this.fusedWebFail(err, ctx);
3600
3861
  }
3601
3862
  if (derived instanceof Promise) {
3602
3863
  return derived.then((settled) => {
3603
- if (isResponseResult(settled) || settled instanceof Response) {
3864
+ if (isResponseResult(settled) || settled instanceof Response)
3604
3865
  return this.respondFused(settled, ctx);
3866
+ try {
3867
+ Object.assign(ctx, settled);
3868
+ }
3869
+ catch (err) {
3870
+ return this.fusedWebFail(err, ctx);
3605
3871
  }
3606
- Object.assign(ctx, settled);
3607
3872
  return runBefore(ctx);
3608
- }, (err) => logError(err, ctx));
3873
+ }, (err) => this.fusedWebFail(err, ctx));
3609
3874
  }
3610
- if (isResponseResult(derived) || derived instanceof Response) {
3875
+ if (isResponseResult(derived) || derived instanceof Response)
3611
3876
  return this.respondFused(derived, ctx);
3877
+ let early;
3878
+ let result;
3879
+ let transformed;
3880
+ let handlerPromise;
3881
+ try {
3882
+ Object.assign(ctx, derived);
3883
+ early = before(ctx);
3884
+ if (early === undefined) {
3885
+ result = handler(ctx);
3886
+ if (result instanceof Promise)
3887
+ handlerPromise = result;
3888
+ else if (after !== undefined)
3889
+ transformed = after(result, ctx);
3890
+ }
3891
+ }
3892
+ catch (err) {
3893
+ return this.fusedWebFail(err, ctx);
3894
+ }
3895
+ if (early instanceof Promise) {
3896
+ return early.then((settled) => (settled === undefined ? runHandler(ctx) : this.respondFused(settled, ctx)), (err) => this.fusedWebFail(err, ctx));
3612
3897
  }
3613
- Object.assign(ctx, derived);
3614
- return runBefore(ctx);
3898
+ if (early !== undefined)
3899
+ return this.respondFused(early, ctx);
3900
+ if (handlerPromise !== undefined) {
3901
+ return handlerPromise.then((value) => after === undefined
3902
+ ? this.respondFused(value, ctx, this.onResponseHooks.length === 0)
3903
+ : runAfter(value, ctx), (err) => this.fusedWebFail(err, ctx));
3904
+ }
3905
+ if (after === undefined)
3906
+ return this.respondFused(result, ctx, this.onResponseHooks.length === 0);
3907
+ if (transformed instanceof Promise) {
3908
+ return transformed.then((value) => this.respondFused(value, ctx, this.onResponseHooks.length === 0), (err) => this.fusedWebFail(err, ctx));
3909
+ }
3910
+ return this.respondFused(transformed, ctx, this.onResponseHooks.length === 0);
3615
3911
  };
3912
+ }
3913
+ buildFusedLifecycleWeb(route, querySchema, bodySchema, maxBodyBytes) {
3914
+ if (querySchema === undefined &&
3915
+ bodySchema === undefined &&
3916
+ route.errors.length === 0 &&
3917
+ route.decorations === undefined &&
3918
+ route.derives.length === 1 &&
3919
+ route.befores.length === 1 &&
3920
+ route.afters.length <= 1) {
3921
+ const derive = route.derives[0];
3922
+ const before = route.befores[0];
3923
+ if (derive !== undefined && before !== undefined) {
3924
+ return this.buildFusedLifecycleWebSingle(route.handler, derive, before, route.afters[0], maxBodyBytes);
3925
+ }
3926
+ }
3927
+ const runDerive = sharedStages(WEB_STAGES, this, route, (d, b, a) => this.fusedWebStages(d, b, a));
3928
+ const decorations = route.decorations;
3929
+ if (bodySchema !== undefined) {
3930
+ const onParsed = (parsed, ctx) => {
3931
+ let validation;
3932
+ try {
3933
+ validation = bodySchema["~standard"].validate(parsed);
3934
+ }
3935
+ catch (err) {
3936
+ return this.fusedWebRouteFail(err, ctx, route);
3937
+ }
3938
+ if (validation instanceof Promise) {
3939
+ return validation.then((settled) => {
3940
+ if (settled.issues !== undefined)
3941
+ return this.wrapWebResponse(plainValidationError(settled.issues));
3942
+ ctx.body = settled.value;
3943
+ return runDerive(ctx, route, 0);
3944
+ }, (err) => this.fusedWebRouteFail(err, ctx, route));
3945
+ }
3946
+ if (validation.issues !== undefined)
3947
+ return this.wrapWebResponse(plainValidationError(validation.issues));
3948
+ ctx.body = validation.value;
3949
+ return runDerive(ctx, route, 0);
3950
+ };
3951
+ return (source, params, search, signal, budget, platform, nativeContext) => {
3952
+ const ctx = nativeContext
3953
+ ? RequestContext.native(source, params, search, maxBodyBytes, platform, this.protoPoisoning)
3954
+ : new RequestContext(source, params, search, signal, budget, platform, maxBodyBytes, this.protoPoisoning);
3955
+ if (decorations !== undefined)
3956
+ Object.assign(ctx, decorations);
3957
+ return readBodyFramed(source, maxBodyBytes, this.protoPoisoning, (parsed) => onParsed(parsed, ctx), this.wrapWebResponse, (err) => this.fusedWebRouteFail(err, ctx, route), bodySchema, this.formOrigins);
3958
+ };
3959
+ }
3616
3960
  return (source, params, search, signal, budget, platform, nativeContext) => {
3617
3961
  const ctx = nativeContext
3618
3962
  ? RequestContext.native(source, params, search, maxBodyBytes, platform, this.protoPoisoning)
3619
3963
  : new RequestContext(source, params, search, signal, budget, platform, maxBodyBytes, this.protoPoisoning);
3964
+ if (decorations !== undefined)
3965
+ Object.assign(ctx, decorations);
3620
3966
  if (querySchema === undefined)
3621
- return runDerive(ctx);
3967
+ return runDerive(ctx, route, 0);
3622
3968
  let validation;
3623
3969
  try {
3624
3970
  validation = querySchema["~standard"].validate(queryObjectOf(ctx[CONTEXT_SEARCH]));
3625
3971
  }
3626
3972
  catch (err) {
3627
- return logError(err, ctx);
3973
+ return this.fusedWebRouteFail(err, ctx, route);
3628
3974
  }
3629
3975
  if (validation instanceof Promise) {
3630
3976
  return validation.then((settled) => {
3631
3977
  if (settled.issues !== undefined)
3632
3978
  return this.wrapWebResponse(plainValidationError(settled.issues));
3633
3979
  ctx.query = settled.value;
3634
- return runDerive(ctx);
3635
- }, (err) => logError(err, ctx));
3980
+ return runDerive(ctx, route, 0);
3981
+ }, (err) => this.fusedWebRouteFail(err, ctx, route));
3636
3982
  }
3637
3983
  if (validation.issues !== undefined)
3638
3984
  return this.wrapWebResponse(plainValidationError(validation.issues));
3639
3985
  ctx.query = validation.value;
3640
- return runDerive(ctx);
3986
+ return runDerive(ctx, route, 0);
3641
3987
  };
3642
3988
  }
3643
3989
  /**
3644
- * Body + lifecycle hooks fused: parse → validate → derive → before → handler → (after) in one
3645
- * closure, the same win `buildFusedBodyRunner` gives for the no-hooks body shape. The shape is
3646
- * `lifecycleHookLane` PLUS a body schema with no other validation conflicts; the lane is selected
3647
- * ONLY when `schema.body !== undefined && schema.query/params/headers === undefined &&
3648
- * schema.onValidationError === undefined && !defaultOnValidationError && !hasIdempotency &&
3649
- * !hasLedger && derives === 1 && beforeHandle === 1 && afterHandle <= 1 && onError === 0 &&
3650
- * !hasResponseContract && !hasDecorations && around === 0`. The same single `querySchema === undefined`
3651
- * trick used in the no-body lane lets the build path stay simple: a query + body route falls back
3652
- * to the generic program (this is rare in practice - validated query + validated body is usually a
3653
- * "search results" route with a middleware chain on top, and the generic lane is fine there).
3990
+ * The Node-direct twin of {@link fusedWebStages}: the same chain, finalizing through the caller's
3991
+ * own `finalize`, shared by every route with the same hook shape. A terminal `finalize` throw (an
3992
+ * unstringifiable handler return on the Node lane) is caught as `finishLifecycleContract` catches it.
3654
3993
  */
3655
- buildFusedBodyDeriveBeforeAfter(handler, derive, before, after, bodySchema, maxBodyBytes) {
3656
- const logError = (err, ctx) => this.bareError(err, ctx, (result, set) => this.wrapWebResponse(toResponse(result, set)), this.wrapWebResponse);
3657
- return (source, params, search, signal, budget, platform, nativeContext) => {
3658
- const ctx = nativeContext
3659
- ? RequestContext.native(source, params, search, maxBodyBytes, platform, this.protoPoisoning)
3660
- : new RequestContext(source, params, search, signal, budget, platform, maxBodyBytes, this.protoPoisoning);
3661
- const runHandlerValidated = () => {
3662
- const runAfter = (result) => {
3663
- if (after === undefined) {
3664
- return this.respondFused(result, ctx, this.onResponseHooks.length === 0);
3665
- }
3994
+ fusedNodeStages(deriveCount, beforeCount, afterCount) {
3995
+ const finishResult = (result, ctx, route, finalize, wrapResponse) => {
3996
+ try {
3997
+ return finalize(result, responseSet(ctx), ctx);
3998
+ }
3999
+ catch (err) {
4000
+ return this.fusedFail(err, ctx, route, finalize, wrapResponse);
4001
+ }
4002
+ };
4003
+ let runAfter;
4004
+ if (afterCount === 0) {
4005
+ runAfter = (result, ctx, route, finalize, wrapResponse) => finishResult(result, ctx, route, finalize, wrapResponse);
4006
+ }
4007
+ else if (afterCount === 1) {
4008
+ runAfter = (result, ctx, route, finalize, wrapResponse) => {
4009
+ let transformed;
4010
+ try {
4011
+ transformed = route.afters[0](result, ctx);
4012
+ }
4013
+ catch (err) {
4014
+ return this.fusedFail(err, ctx, route, finalize, wrapResponse);
4015
+ }
4016
+ if (transformed instanceof Promise) {
4017
+ return transformed.then((value) => finishResult(value, ctx, route, finalize, wrapResponse), (err) => this.fusedFail(err, ctx, route, finalize, wrapResponse));
4018
+ }
4019
+ return finishResult(transformed, ctx, route, finalize, wrapResponse);
4020
+ };
4021
+ }
4022
+ else {
4023
+ runAfter = (result, ctx, route, finalize, wrapResponse, index) => {
4024
+ const lastAfter = route.afters.length - 1;
4025
+ for (;;) {
3666
4026
  let transformed;
3667
4027
  try {
3668
- transformed = after(result, ctx);
4028
+ transformed = route.afters[index](result, ctx);
3669
4029
  }
3670
4030
  catch (err) {
3671
- return logError(err, ctx);
4031
+ return this.fusedFail(err, ctx, route, finalize, wrapResponse);
3672
4032
  }
3673
4033
  if (transformed instanceof Promise) {
3674
- return transformed.then((value) => this.respondFused(value, ctx, this.onResponseHooks.length === 0), (err) => logError(err, ctx));
4034
+ return transformed.then((value) => index < lastAfter
4035
+ ? runAfter(value, ctx, route, finalize, wrapResponse, index + 1)
4036
+ : finishResult(value, ctx, route, finalize, wrapResponse), (err) => this.fusedFail(err, ctx, route, finalize, wrapResponse));
3675
4037
  }
3676
- return this.respondFused(transformed, ctx, this.onResponseHooks.length === 0);
3677
- };
3678
- const runHandler = () => {
3679
- let result;
3680
- try {
3681
- result = handler(ctx);
3682
- }
3683
- catch (err) {
3684
- return logError(err, ctx);
3685
- }
3686
- if (result instanceof Promise) {
3687
- return result.then((value) => runAfter(value), (err) => logError(err, ctx));
3688
- }
3689
- return runAfter(result);
3690
- };
3691
- const runBefore = () => {
4038
+ if (index === lastAfter)
4039
+ return finishResult(transformed, ctx, route, finalize, wrapResponse);
4040
+ result = transformed;
4041
+ index++;
4042
+ }
4043
+ };
4044
+ }
4045
+ const runHandler = (ctx, route, finalize, wrapResponse) => {
4046
+ let result;
4047
+ try {
4048
+ result = route.handler(ctx);
4049
+ }
4050
+ catch (err) {
4051
+ return this.fusedFail(err, ctx, route, finalize, wrapResponse);
4052
+ }
4053
+ if (result instanceof Promise) {
4054
+ return result.then((value) => runAfter(value, ctx, route, finalize, wrapResponse, 0), (err) => this.fusedFail(err, ctx, route, finalize, wrapResponse));
4055
+ }
4056
+ return runAfter(result, ctx, route, finalize, wrapResponse, 0);
4057
+ };
4058
+ let runBefore;
4059
+ if (beforeCount === 0) {
4060
+ runBefore = runHandler;
4061
+ }
4062
+ else if (beforeCount === 1) {
4063
+ runBefore = (ctx, route, finalize, wrapResponse) => {
4064
+ let early;
4065
+ try {
4066
+ early = route.befores[0](ctx);
4067
+ }
4068
+ catch (err) {
4069
+ return this.fusedFail(err, ctx, route, finalize, wrapResponse);
4070
+ }
4071
+ if (early instanceof Promise) {
4072
+ return early.then((settled) => settled === undefined
4073
+ ? runHandler(ctx, route, finalize, wrapResponse, 0)
4074
+ : finishResult(settled, ctx, route, finalize, wrapResponse), (err) => this.fusedFail(err, ctx, route, finalize, wrapResponse));
4075
+ }
4076
+ if (early === undefined)
4077
+ return runHandler(ctx, route, finalize, wrapResponse, 0);
4078
+ return finishResult(early, ctx, route, finalize, wrapResponse);
4079
+ };
4080
+ }
4081
+ else {
4082
+ runBefore = (ctx, route, finalize, wrapResponse, index) => {
4083
+ const lastBefore = route.befores.length - 1;
4084
+ for (;;) {
3692
4085
  let early;
3693
4086
  try {
3694
- early = before(ctx);
4087
+ early = route.befores[index](ctx);
3695
4088
  }
3696
4089
  catch (err) {
3697
- return logError(err, ctx);
4090
+ return this.fusedFail(err, ctx, route, finalize, wrapResponse);
3698
4091
  }
3699
4092
  if (early instanceof Promise) {
3700
4093
  return early.then((settled) => {
3701
- if (settled === undefined)
3702
- return runHandler();
3703
- return this.respondFused(settled, ctx);
3704
- }, (err) => logError(err, ctx));
4094
+ if (settled !== undefined)
4095
+ return finishResult(settled, ctx, route, finalize, wrapResponse);
4096
+ return index < lastBefore
4097
+ ? runBefore(ctx, route, finalize, wrapResponse, index + 1)
4098
+ : runHandler(ctx, route, finalize, wrapResponse, 0);
4099
+ }, (err) => this.fusedFail(err, ctx, route, finalize, wrapResponse));
3705
4100
  }
3706
- if (early === undefined)
3707
- return runHandler();
3708
- return this.respondFused(early, ctx);
3709
- };
4101
+ if (early !== undefined)
4102
+ return finishResult(early, ctx, route, finalize, wrapResponse);
4103
+ if (index === lastBefore)
4104
+ return runHandler(ctx, route, finalize, wrapResponse, 0);
4105
+ index++;
4106
+ }
4107
+ };
4108
+ }
4109
+ let runDerive;
4110
+ if (deriveCount === 0) {
4111
+ runDerive = runBefore;
4112
+ }
4113
+ else if (deriveCount === 1) {
4114
+ runDerive = (ctx, route, finalize, wrapResponse) => {
3710
4115
  let derived;
3711
4116
  try {
3712
- derived = derive(ctx);
4117
+ derived = route.derives[0](ctx);
3713
4118
  }
3714
4119
  catch (err) {
3715
- return logError(err, ctx);
4120
+ return this.fusedFail(err, ctx, route, finalize, wrapResponse);
3716
4121
  }
3717
4122
  if (derived instanceof Promise) {
3718
4123
  return derived.then((settled) => {
3719
4124
  if (isResponseResult(settled) || settled instanceof Response) {
3720
- return this.respondFused(settled, ctx);
4125
+ return finishResult(settled, ctx, route, finalize, wrapResponse);
3721
4126
  }
3722
- Object.assign(ctx, settled);
3723
- return runBefore();
3724
- }, (err) => logError(err, ctx));
4127
+ try {
4128
+ Object.assign(ctx, settled);
4129
+ }
4130
+ catch (err) {
4131
+ return this.fusedFail(err, ctx, route, finalize, wrapResponse);
4132
+ }
4133
+ return runBefore(ctx, route, finalize, wrapResponse, 0);
4134
+ }, (err) => this.fusedFail(err, ctx, route, finalize, wrapResponse));
3725
4135
  }
3726
4136
  if (isResponseResult(derived) || derived instanceof Response) {
3727
- return this.respondFused(derived, ctx);
4137
+ return finishResult(derived, ctx, route, finalize, wrapResponse);
3728
4138
  }
3729
- Object.assign(ctx, derived);
3730
- return runBefore();
4139
+ try {
4140
+ Object.assign(ctx, derived);
4141
+ }
4142
+ catch (err) {
4143
+ return this.fusedFail(err, ctx, route, finalize, wrapResponse);
4144
+ }
4145
+ return runBefore(ctx, route, finalize, wrapResponse, 0);
3731
4146
  };
3732
- const onParsed = (parsed) => {
4147
+ }
4148
+ else {
4149
+ runDerive = (ctx, route, finalize, wrapResponse, index) => {
4150
+ const lastDerive = route.derives.length - 1;
4151
+ for (;;) {
4152
+ let derived;
4153
+ try {
4154
+ derived = route.derives[index](ctx);
4155
+ }
4156
+ catch (err) {
4157
+ return this.fusedFail(err, ctx, route, finalize, wrapResponse);
4158
+ }
4159
+ if (derived instanceof Promise) {
4160
+ return derived.then((settled) => {
4161
+ if (isResponseResult(settled) || settled instanceof Response) {
4162
+ return finishResult(settled, ctx, route, finalize, wrapResponse);
4163
+ }
4164
+ try {
4165
+ Object.assign(ctx, settled);
4166
+ }
4167
+ catch (err) {
4168
+ return this.fusedFail(err, ctx, route, finalize, wrapResponse);
4169
+ }
4170
+ return index < lastDerive
4171
+ ? runDerive(ctx, route, finalize, wrapResponse, index + 1)
4172
+ : runBefore(ctx, route, finalize, wrapResponse, 0);
4173
+ }, (err) => this.fusedFail(err, ctx, route, finalize, wrapResponse));
4174
+ }
4175
+ if (isResponseResult(derived) || derived instanceof Response) {
4176
+ return finishResult(derived, ctx, route, finalize, wrapResponse);
4177
+ }
4178
+ try {
4179
+ Object.assign(ctx, derived);
4180
+ }
4181
+ catch (err) {
4182
+ return this.fusedFail(err, ctx, route, finalize, wrapResponse);
4183
+ }
4184
+ if (index === lastDerive)
4185
+ return runBefore(ctx, route, finalize, wrapResponse, 0);
4186
+ index++;
4187
+ }
4188
+ };
4189
+ }
4190
+ return runDerive;
4191
+ }
4192
+ /**
4193
+ * The Node-direct twin of {@link buildFusedLifecycleWeb}: the same steps, finalizing through the
4194
+ * caller's own `finalize` (the Node outcome on the direct lane) instead of building a `Response`, so
4195
+ * the `!webFast` dispatcher runs these routes in one frame rather than the generic program. A
4196
+ * terminal `finalize` throw (an unstringifiable handler return on the Node lane) is caught as
4197
+ * `finishLifecycleContract` catches it; the body's 415/400 renders take `wrapResponse` directly.
4198
+ */
4199
+ buildFusedLifecycleRunner(route, querySchema, bodySchema, maxBodyBytes) {
4200
+ const runDerive = sharedStages(NODE_STAGES, this, route, (d, b, a) => this.fusedNodeStages(d, b, a));
4201
+ const decorations = route.decorations;
4202
+ if (bodySchema !== undefined) {
4203
+ const onParsed = (parsed, ctx, finalize, wrapResponse) => {
3733
4204
  let validation;
3734
4205
  try {
3735
4206
  validation = bodySchema["~standard"].validate(parsed);
3736
4207
  }
3737
4208
  catch (err) {
3738
- return logError(err, ctx);
4209
+ return this.fusedFail(err, ctx, route, finalize, wrapResponse);
3739
4210
  }
3740
4211
  if (validation instanceof Promise) {
3741
4212
  return validation.then((settled) => {
3742
4213
  if (settled.issues !== undefined)
3743
- return this.wrapWebResponse(plainValidationError(settled.issues));
4214
+ return wrapResponse(plainValidationError(settled.issues));
3744
4215
  ctx.body = settled.value;
3745
- return runHandlerValidated();
3746
- }, (err) => logError(err, ctx));
4216
+ return runDerive(ctx, route, finalize, wrapResponse, 0);
4217
+ }, (err) => this.fusedFail(err, ctx, route, finalize, wrapResponse));
3747
4218
  }
3748
4219
  if (validation.issues !== undefined)
3749
- return this.wrapWebResponse(plainValidationError(validation.issues));
4220
+ return wrapResponse(plainValidationError(validation.issues));
3750
4221
  ctx.body = validation.value;
3751
- return runHandlerValidated();
4222
+ return runDerive(ctx, route, finalize, wrapResponse, 0);
3752
4223
  };
3753
- return readBodyFramed(source, maxBodyBytes, this.protoPoisoning, onParsed, (response) => this.wrapWebResponse(response), (err) => Promise.resolve(logError(err, ctx)), bodySchema, this.formOrigins);
3754
- };
3755
- }
3756
- /**
3757
- * Generic (finalize/wrapResponse-parameterized) fused runner for the derive-before(-after) lane:
3758
- * the Node-direct counterpart of `buildFusedDeriveBefore` / `buildFusedDeriveBeforeAfter`.
3759
- *
3760
- * The Web fused builders above return a `Response`, so the Node dispatcher (`webFast === false`)
3761
- * cannot use them and a derive+before route falls back to the generic route program there - two
3762
- * staged loops, per-stage dispatch, and the async-continuation machinery - while Bun rides the
3763
- * single closure. That asymmetry is the realistic-route Node deficit against Fastify: this runner
3764
- * closes it by running validate → derive → before → handler → (after) in one frame and finalizing
3765
- * through the caller's own `finalize` (the Node outcome on the direct lane, `toResponse` on Web),
3766
- * so the same registration feeds both runtimes with no per-lane copy of the lifecycle to drift.
3767
- *
3768
- * Semantics mirror the generic program stage-for-stage for the shapes this lane admits (exactly
3769
- * one derive, one beforeHandle, zero-or-one afterHandle, no onError/around/decorations/contracts/
3770
- * recovery hooks - enforced by `selectRouteLanes`): thrown `Response`/`status(...)` are control
3771
- * flow through `wrapResponse`/`finalize`, anything else logs + 500s via the shared `bareError`
3772
- * lane (identical to `handleLifecycleError` with no `onError` hooks), and a terminal `finalize`
3773
- * throw (e.g. an unstringifiable handler return on the Node lane) is caught exactly as
3774
- * `finishLifecycleContract` catches it. Step functions close over registration state only and take
3775
- * the request state as arguments, so the hot path allocates no per-request closures.
3776
- */
3777
- buildFusedDeriveBeforeRunner(handler, derive, before, after, querySchema, maxBodyBytes) {
3778
- const logError = (err, ctx, finalize, wrapResponse) => this.bareError(err, ctx, finalize, wrapResponse);
3779
- const runFinish = (result, ctx, finalize, wrapResponse) => {
3780
- if (after === undefined) {
3781
- try {
3782
- return finalize(result, responseSet(ctx), ctx);
3783
- }
3784
- catch (err) {
3785
- return logError(err, ctx, finalize, wrapResponse);
3786
- }
3787
- }
3788
- let transformed;
3789
- try {
3790
- transformed = after(result, ctx);
3791
- }
3792
- catch (err) {
3793
- return logError(err, ctx, finalize, wrapResponse);
3794
- }
3795
- if (transformed instanceof Promise) {
3796
- return transformed.then((value) => {
3797
- try {
3798
- return finalize(value, responseSet(ctx), ctx);
3799
- }
3800
- catch (err) {
3801
- return logError(err, ctx, finalize, wrapResponse);
3802
- }
3803
- }, (err) => logError(err, ctx, finalize, wrapResponse));
3804
- }
3805
- try {
3806
- return finalize(transformed, responseSet(ctx), ctx);
3807
- }
3808
- catch (err) {
3809
- return logError(err, ctx, finalize, wrapResponse);
3810
- }
3811
- };
3812
- const finalizeEarly = (result, ctx, finalize, wrapResponse) => {
3813
- try {
3814
- return finalize(result, responseSet(ctx), ctx);
3815
- }
3816
- catch (err) {
3817
- return logError(err, ctx, finalize, wrapResponse);
3818
- }
3819
- };
3820
- const runHandler = (ctx, finalize, wrapResponse) => {
3821
- let result;
3822
- try {
3823
- result = handler(ctx);
3824
- }
3825
- catch (err) {
3826
- return logError(err, ctx, finalize, wrapResponse);
3827
- }
3828
- if (result instanceof Promise) {
3829
- return result.then((value) => runFinish(value, ctx, finalize, wrapResponse), (err) => logError(err, ctx, finalize, wrapResponse));
3830
- }
3831
- return runFinish(result, ctx, finalize, wrapResponse);
3832
- };
3833
- const runBefore = (ctx, finalize, wrapResponse) => {
3834
- let early;
3835
- try {
3836
- early = before(ctx);
3837
- }
3838
- catch (err) {
3839
- return logError(err, ctx, finalize, wrapResponse);
3840
- }
3841
- if (early instanceof Promise) {
3842
- return early.then((settled) => settled === undefined
3843
- ? runHandler(ctx, finalize, wrapResponse)
3844
- : finalizeEarly(settled, ctx, finalize, wrapResponse), (err) => logError(err, ctx, finalize, wrapResponse));
3845
- }
3846
- if (early === undefined)
3847
- return runHandler(ctx, finalize, wrapResponse);
3848
- return finalizeEarly(early, ctx, finalize, wrapResponse);
3849
- };
3850
- const runDerive = (ctx, finalize, wrapResponse) => {
3851
- let derived;
3852
- try {
3853
- derived = derive(ctx);
3854
- }
3855
- catch (err) {
3856
- return logError(err, ctx, finalize, wrapResponse);
3857
- }
3858
- if (derived instanceof Promise) {
3859
- return derived.then((settled) => {
3860
- if (isResponseResult(settled) || settled instanceof Response) {
3861
- return finalizeEarly(settled, ctx, finalize, wrapResponse);
3862
- }
3863
- Object.assign(ctx, settled);
3864
- return runBefore(ctx, finalize, wrapResponse);
3865
- }, (err) => logError(err, ctx, finalize, wrapResponse));
3866
- }
3867
- if (isResponseResult(derived) || derived instanceof Response) {
3868
- return finalizeEarly(derived, ctx, finalize, wrapResponse);
3869
- }
3870
- Object.assign(ctx, derived);
3871
- return runBefore(ctx, finalize, wrapResponse);
3872
- };
4224
+ return (source, params, search, signal, budget, platform, nativeContext, finalize, wrapResponse) => {
4225
+ const ctx = nativeContext
4226
+ ? RequestContext.native(source, params, search, maxBodyBytes, platform, this.protoPoisoning)
4227
+ : new RequestContext(source, params, search, signal, budget, platform, maxBodyBytes, this.protoPoisoning);
4228
+ if (decorations !== undefined)
4229
+ Object.assign(ctx, decorations);
4230
+ return readBodyFramed(source, maxBodyBytes, this.protoPoisoning, (parsed) => onParsed(parsed, ctx, finalize, wrapResponse), wrapResponse, (err) => this.fusedFail(err, ctx, route, finalize, wrapResponse), bodySchema, this.formOrigins);
4231
+ };
4232
+ }
3873
4233
  const runValidated = (validation, ctx, finalize, wrapResponse) => {
3874
4234
  if (validation instanceof Promise) {
3875
4235
  return validation.then((settled) => {
3876
4236
  if (settled.issues !== undefined)
3877
4237
  return wrapResponse(plainValidationError(settled.issues));
3878
4238
  ctx.query = settled.value;
3879
- return runDerive(ctx, finalize, wrapResponse);
3880
- }, (err) => logError(err, ctx, finalize, wrapResponse));
4239
+ return runDerive(ctx, route, finalize, wrapResponse, 0);
4240
+ }, (err) => this.fusedFail(err, ctx, route, finalize, wrapResponse));
3881
4241
  }
3882
4242
  if (validation.issues !== undefined)
3883
4243
  return wrapResponse(plainValidationError(validation.issues));
3884
4244
  ctx.query = validation.value;
3885
- return runDerive(ctx, finalize, wrapResponse);
4245
+ return runDerive(ctx, route, finalize, wrapResponse, 0);
3886
4246
  };
3887
4247
  return (source, params, search, signal, budget, platform, nativeContext, finalize, wrapResponse) => {
3888
4248
  const ctx = nativeContext
3889
4249
  ? RequestContext.native(source, params, search, maxBodyBytes, platform, this.protoPoisoning)
3890
4250
  : new RequestContext(source, params, search, signal, budget, platform, maxBodyBytes, this.protoPoisoning);
4251
+ if (decorations !== undefined)
4252
+ Object.assign(ctx, decorations);
3891
4253
  if (querySchema === undefined)
3892
- return runDerive(ctx, finalize, wrapResponse);
4254
+ return runDerive(ctx, route, finalize, wrapResponse, 0);
3893
4255
  let validation;
3894
4256
  try {
3895
4257
  validation = querySchema["~standard"].validate(queryObjectOf(ctx[CONTEXT_SEARCH]));
3896
4258
  }
3897
4259
  catch (err) {
3898
- return logError(err, ctx, finalize, wrapResponse);
4260
+ return this.fusedFail(err, ctx, route, finalize, wrapResponse);
3899
4261
  }
3900
4262
  return runValidated(validation, ctx, finalize, wrapResponse);
3901
4263
  };
3902
4264
  }
3903
- /**
3904
- * Generic fused runner for the body+derive-before(-after) lane: the Node-direct counterpart of
3905
- * `buildFusedBodyDeriveBeforeAfter`. Same contract as `buildFusedDeriveBeforeRunner` above - one
3906
- * frame running parse → validate → derive → before → handler → (after) through the caller's own
3907
- * `finalize` - with the body read ahead of validation, exactly as the generic program orders its
3908
- * body stage before the derive/before stages. `readBodyFramed` keeps the shared bounded-read,
3909
- * content-type, and prototype-poisoning contracts; the 415/400 renders take `wrapResponse`
3910
- * directly, which is what the generic lane's wrap-after-identity amounts to.
3911
- */
3912
- buildFusedBodyDeriveBeforeAfterRunner(handler, derive, before, after, bodySchema, maxBodyBytes) {
3913
- const logError = (err, ctx, finalize, wrapResponse) => this.bareError(err, ctx, finalize, wrapResponse);
3914
- const runFinish = (result, ctx, finalize, wrapResponse) => {
3915
- if (after === undefined) {
3916
- try {
3917
- return finalize(result, responseSet(ctx), ctx);
3918
- }
3919
- catch (err) {
3920
- return logError(err, ctx, finalize, wrapResponse);
3921
- }
3922
- }
3923
- let transformed;
3924
- try {
3925
- transformed = after(result, ctx);
3926
- }
3927
- catch (err) {
3928
- return logError(err, ctx, finalize, wrapResponse);
3929
- }
3930
- if (transformed instanceof Promise) {
3931
- return transformed.then((value) => {
3932
- try {
3933
- return finalize(value, responseSet(ctx), ctx);
3934
- }
3935
- catch (err) {
3936
- return logError(err, ctx, finalize, wrapResponse);
3937
- }
3938
- }, (err) => logError(err, ctx, finalize, wrapResponse));
3939
- }
3940
- try {
3941
- return finalize(transformed, responseSet(ctx), ctx);
3942
- }
3943
- catch (err) {
3944
- return logError(err, ctx, finalize, wrapResponse);
3945
- }
3946
- };
3947
- const finalizeEarly = (result, ctx, finalize, wrapResponse) => {
3948
- try {
3949
- return finalize(result, responseSet(ctx), ctx);
3950
- }
3951
- catch (err) {
3952
- return logError(err, ctx, finalize, wrapResponse);
3953
- }
3954
- };
3955
- const runHandler = (ctx, finalize, wrapResponse) => {
3956
- let result;
3957
- try {
3958
- result = handler(ctx);
3959
- }
3960
- catch (err) {
3961
- return logError(err, ctx, finalize, wrapResponse);
3962
- }
3963
- if (result instanceof Promise) {
3964
- return result.then((value) => runFinish(value, ctx, finalize, wrapResponse), (err) => logError(err, ctx, finalize, wrapResponse));
3965
- }
3966
- return runFinish(result, ctx, finalize, wrapResponse);
3967
- };
3968
- const runBefore = (ctx, finalize, wrapResponse) => {
3969
- let early;
3970
- try {
3971
- early = before(ctx);
3972
- }
3973
- catch (err) {
3974
- return logError(err, ctx, finalize, wrapResponse);
3975
- }
3976
- if (early instanceof Promise) {
3977
- return early.then((settled) => settled === undefined
3978
- ? runHandler(ctx, finalize, wrapResponse)
3979
- : finalizeEarly(settled, ctx, finalize, wrapResponse), (err) => logError(err, ctx, finalize, wrapResponse));
3980
- }
3981
- if (early === undefined)
3982
- return runHandler(ctx, finalize, wrapResponse);
3983
- return finalizeEarly(early, ctx, finalize, wrapResponse);
3984
- };
3985
- const runDerive = (ctx, finalize, wrapResponse) => {
3986
- let derived;
3987
- try {
3988
- derived = derive(ctx);
3989
- }
3990
- catch (err) {
3991
- return logError(err, ctx, finalize, wrapResponse);
3992
- }
3993
- if (derived instanceof Promise) {
3994
- return derived.then((settled) => {
3995
- if (isResponseResult(settled) || settled instanceof Response) {
3996
- return finalizeEarly(settled, ctx, finalize, wrapResponse);
3997
- }
3998
- Object.assign(ctx, settled);
3999
- return runBefore(ctx, finalize, wrapResponse);
4000
- }, (err) => logError(err, ctx, finalize, wrapResponse));
4001
- }
4002
- if (isResponseResult(derived) || derived instanceof Response) {
4003
- return finalizeEarly(derived, ctx, finalize, wrapResponse);
4004
- }
4005
- Object.assign(ctx, derived);
4006
- return runBefore(ctx, finalize, wrapResponse);
4007
- };
4008
- const onParsed = (parsed, ctx, finalize, wrapResponse) => {
4009
- let validation;
4265
+ /** The thrown-value render of the Web fused lifecycle lanes for a route with no error hooks: a
4266
+ * `Response` is control flow, anything else is logged and answered with a plain 500. */
4267
+ fusedWebFail(err, ctx) {
4268
+ return this.bareError(err, ctx, this.finalizeResponse, this.wrapWebResponse);
4269
+ }
4270
+ /** {@link fusedWebFail} for a fused route that may have error hooks. */
4271
+ fusedWebRouteFail(err, ctx, route) {
4272
+ return this.fusedFail(err, ctx, route, this.finalizeResponse, this.wrapWebResponse);
4273
+ }
4274
+ /** The thrown-value render of a fused lifecycle route. A `Response` or `status(...)` is control flow
4275
+ * that no error hook sees, unless it cannot render, which is a fault like any other. Anything else
4276
+ * goes to the route's error hooks; with none it is logged and answered with a plain 500. */
4277
+ fusedFail(err, ctx, route, finalize, wrapResponse) {
4278
+ if (route.errors.length === 0)
4279
+ return this.bareError(err, ctx, finalize, wrapResponse);
4280
+ let fault = err;
4281
+ if (err instanceof Response || isResponseResult(err)) {
4010
4282
  try {
4011
- validation = bodySchema["~standard"].validate(parsed);
4012
- }
4013
- catch (err) {
4014
- return logError(err, ctx, finalize, wrapResponse);
4283
+ return finalize(err, responseSet(ctx), ctx);
4015
4284
  }
4016
- if (validation instanceof Promise) {
4017
- return validation.then((settled) => {
4018
- if (settled.issues !== undefined)
4019
- return wrapResponse(plainValidationError(settled.issues));
4020
- ctx.body = settled.value;
4021
- return runDerive(ctx, finalize, wrapResponse);
4022
- }, (err) => logError(err, ctx, finalize, wrapResponse));
4285
+ catch (renderError) {
4286
+ fault = renderError;
4023
4287
  }
4024
- if (validation.issues !== undefined)
4025
- return wrapResponse(plainValidationError(validation.issues));
4026
- ctx.body = validation.value;
4027
- return runDerive(ctx, finalize, wrapResponse);
4028
- };
4029
- return (source, params, search, signal, budget, platform, nativeContext, finalize, wrapResponse) => {
4030
- const ctx = nativeContext
4031
- ? RequestContext.native(source, params, search, maxBodyBytes, platform, this.protoPoisoning)
4032
- : new RequestContext(source, params, search, signal, budget, platform, maxBodyBytes, this.protoPoisoning);
4033
- return readBodyFramed(source, maxBodyBytes, this.protoPoisoning, (parsed) => onParsed(parsed, ctx, finalize, wrapResponse), wrapResponse, (err) => logError(err, ctx, finalize, wrapResponse), bodySchema, this.formOrigins);
4034
- };
4288
+ }
4289
+ return this.runErrorHooks(route.errors, undefined, fault, ctx, (result, set) => finalize(result, set, ctx), wrapResponse);
4290
+ }
4291
+ /** {@link respondFused} for a fused lifecycle route, whose failures reach its error hooks. */
4292
+ respondHooks(result, ctx, route, unstamped) {
4293
+ try {
4294
+ return fusedRespond(result, ctx, this.responseBodyTag, this.staticResponseHeaders, unstamped);
4295
+ }
4296
+ catch (err) {
4297
+ return this.fusedWebRouteFail(err, ctx, route);
4298
+ }
4035
4299
  }
4036
4300
  bareError(err, ctx, finalize, wrapResponse) {
4037
4301
  // The thrown-value contract lives in `bare-error-lane.ts`, one source of truth for both this
@@ -4557,11 +4821,12 @@ export class Server {
4557
4821
  }
4558
4822
  /** Adapt one route's compiled execution plan to Bun's already-matched request shape. Route
4559
4823
  * semantics remain in the plan; this closure only supplies native params and deadline fallback. */
4560
- compileBunNativeHandler(entry, paramNames, fused, signal, budget, peer, fallback) {
4824
+ compileBunNativeHandler(entry, method, paramNames, fused, signal, budget, peer, fallback, hookedPlatform) {
4561
4825
  // This callback is reached only from Bun's compiled native route table. Bun has already parsed
4562
4826
  // the HTTP framing, so the JSON lane may retain its native fused `json()` parse without weakening
4563
- // the portable/adapted source contract (those sources are never marked).
4564
- const markFramed = (request) => markTrustedBodyFraming(request);
4827
+ // the portable/adapted source contract (those sources are never marked). Bun hands a GET or
4828
+ // HEAD slot no body, so it skips the symbol write on every request.
4829
+ const markFramed = method === "GET" || method === "HEAD" ? () => undefined : markTrustedBodyFraming;
4565
4830
  // The direct native lanes bypass `runMatched`, so the route's transport byte cap must be
4566
4831
  // marked here too - otherwise a Bun `listen()` fused route would leave direct `c.req` body
4567
4832
  // reads uncapped. The fallback branches go through `fetchMatched` -> `runMatched`, which marks.
@@ -4575,7 +4840,9 @@ export class Server {
4575
4840
  (signal && budget
4576
4841
  ? (source, params, search, activeSignal, activeBudget, platform, nativeContext) => entry.execution.run(this, entry, source, params, search, activeSignal, activeBudget, platform, nativeContext, this.finalizeResponse, this.wrapWebResponse)
4577
4842
  : undefined);
4578
- const capped = inner === undefined || bodyLimit === undefined
4843
+ // The cap is never marked on a GET or HEAD, the only methods a GET or HEAD slot is handed, so those
4844
+ // slots skip the wrapper and its read of the method on every request.
4845
+ const capped = inner === undefined || bodyLimit === undefined || method === "GET" || method === "HEAD"
4579
4846
  ? inner
4580
4847
  : (source, params, search, fusedSignal, fusedBudget, platform, nativeContext) => {
4581
4848
  const method = source.method;
@@ -4603,6 +4870,12 @@ export class Server {
4603
4870
  : finishNative(request, outcome);
4604
4871
  };
4605
4872
  if (paramNames.length === 0) {
4873
+ if (hookedPlatform !== undefined) {
4874
+ return (request, server) => {
4875
+ markFramed(request);
4876
+ return this.fetchMatchedHooked(request, entry, EMPTY_PARAMS, hookedPlatform(request, server));
4877
+ };
4878
+ }
4606
4879
  if (capped === undefined) {
4607
4880
  return (request) => {
4608
4881
  markFramed(request);
@@ -4625,6 +4898,16 @@ export class Server {
4625
4898
  const malformed = paramNames.length === 1
4626
4899
  ? (params) => unroutedParam(params[paramNames[0]])
4627
4900
  : hasUnroutedParam;
4901
+ if (hookedPlatform !== undefined) {
4902
+ return (request, server) => {
4903
+ markFramed(request);
4904
+ // biome-ignore lint/plugin/requireSafetyCommentForTypeAssertion: Bun's route table hands each handler a Request carrying the `params` it matched.
4905
+ const params = request.params ?? EMPTY_PARAMS;
4906
+ if (malformed(params))
4907
+ return fallback(request, server);
4908
+ return this.fetchMatchedHooked(request, entry, params, hookedPlatform(request, server));
4909
+ };
4910
+ }
4628
4911
  if (capped === undefined) {
4629
4912
  return (request, server) => {
4630
4913
  markFramed(request);
@@ -4653,26 +4936,25 @@ export class Server {
4653
4936
  return runNative(request, params);
4654
4937
  };
4655
4938
  }
4656
- /** Compile portable route registrations into Bun's native route table. Apps with request-rewrite
4657
- * hooks or WebSockets retain the single portable dispatcher because those features must run before
4658
- * route selection/upgrade.
4939
+ /** Compile portable route registrations into Bun's native route table. Paths a composed mount or
4940
+ * a WebSocket handshake may reach stay with the portable dispatcher; provably disjoint HTTP routes
4941
+ * keep native matching. Request hooks run before the matched route and rewrites return to dispatch.
4659
4942
  *
4660
4943
  * Bun and the portable router agree on which of two static-and-`:param` paths wins a request, so
4661
4944
  * the table is kept to those paths. Where Bun would otherwise pass a request on to a less specific
4662
4945
  * path, the more specific one carries `fallback`, the portable dispatcher, under that method.
4663
4946
  * `peer` is the platform every request served from the table shares. */
4664
- buildBunNativeRoutes(fallback, peer) {
4665
- // A `clientIp` trust declaration must run the resolver in `dispatch`, which the fused native lane
4666
- // bypasses - so an app that declares trust routes through the fetch lane (where `c.clientIp`
4667
- // resolves) instead of Bun's native table. The allocation-free default keeps native fusion.
4668
- if ((this.onRequestHooks.length > 0 && !this.bunNativeRequestHooksSafe) ||
4669
- this.wsRoutes.length > 0 ||
4670
- this.preRouteMountCount > 0 ||
4671
- this.clientIpTrust !== undefined) {
4947
+ buildBunNativeRoutes(fallback, peer, platformFor) {
4948
+ const hooked = this.onRequestHooks.length > 0 && !this.bunNativeRequestHooksSafe;
4949
+ if ((hooked && platformFor === undefined) ||
4950
+ // A hook may rewrite a handshake onto a socket path, which only the dispatcher's upgrade check follows.
4951
+ (hooked && this.wsRoutes.length > 0) ||
4952
+ this.mountsTakeUpgrades()) {
4672
4953
  return undefined;
4673
4954
  }
4674
4955
  const routes = Object.create(null);
4675
- const mayUseFusedNative = this.requestTimeoutMs === 0 &&
4956
+ const mayUseFusedNative = !hooked &&
4957
+ this.requestTimeoutMs === 0 &&
4676
4958
  (this.onResponseHooks.length === 0 || this.bunNativeResponseHeadersOnly) &&
4677
4959
  this.onResponseFinalizedHooks.length === 0 &&
4678
4960
  // The capacity gate must wrap every request; the fused lane bypasses fetchMatched, so enabling
@@ -4682,28 +4964,49 @@ export class Server {
4682
4964
  const unboundedBudget = mayUseFusedNative ? getUnboundedRequestBudget() : undefined;
4683
4965
  let count = 0;
4684
4966
  const all = this.catalog.entries();
4967
+ const preRouteMounts = this.fetchMounts.filter((mount) => mount.beforeRoutes);
4685
4968
  // A path with a wildcard or a part-literal segment cannot go in the table: Bun reads `/:name.json`
4686
4969
  // as one parameter named `name.json`. These are filed under each run of static segments they
4687
4970
  // start with, which is where a table path they could outrank looks for them.
4688
4971
  const under = new Map();
4972
+ const fileUnder = (segments) => {
4973
+ let prefix = "";
4974
+ for (const segment of segments) {
4975
+ let bucket = under.get(prefix);
4976
+ if (bucket === undefined) {
4977
+ bucket = [];
4978
+ under.set(prefix, bucket);
4979
+ }
4980
+ bucket.push(segments);
4981
+ if (segment.kind !== "static")
4982
+ break;
4983
+ prefix += `/${segment.value}`;
4984
+ }
4985
+ };
4986
+ // A handshake is a GET the portable dispatcher gives to a socket route before any HTTP route, so a
4987
+ // socket path is outside the table like a wildcard path, and a GET route a request may share with
4988
+ // one stays with the dispatcher.
4989
+ const sockets = this.wsRoutes.map((route) => compileRoutePattern(route.path).segments);
4990
+ for (const segments of sockets)
4991
+ fileUnder(segments);
4689
4992
  const lead = all.map((route) => {
4690
4993
  const segments = route.pattern.segments;
4691
4994
  const plain = segments.every((segment) => segment.kind === "static" || segment.kind === "param");
4995
+ if (!plain)
4996
+ fileUnder(segments);
4692
4997
  let prefix = "";
4693
4998
  for (const segment of segments) {
4694
- if (!plain) {
4695
- let bucket = under.get(prefix);
4696
- if (bucket === undefined) {
4697
- bucket = [];
4698
- under.set(prefix, bucket);
4699
- }
4700
- bucket.push(route);
4701
- }
4702
4999
  if (segment.kind !== "static")
4703
5000
  break;
4704
5001
  prefix += `/${segment.value}`;
4705
5002
  }
4706
- return plain ? prefix : undefined;
5003
+ // Mounts take every method under their literal prefix before route matching. A parameter may
5004
+ // fill the rest of that prefix, but a static route ending above the mount cannot reach it.
5005
+ // Conservative by design: only the leading static run proves a dynamic route disjoint.
5006
+ const mayHitMount = preRouteMounts.some((mount) => underMountPrefix(prefix, mount.path) ||
5007
+ (route.pattern.paramNames.length > 0 &&
5008
+ (prefix === "" || underMountPrefix(mount.path, prefix))));
5009
+ return plain && !mayHitMount ? prefix : undefined;
4707
5010
  });
4708
5011
  // A preflight hook is intentionally handled by the fallback fetch path. Keeping OPTIONS out
4709
5012
  // of the native lane means Bun dispatches it through CORS's onRequest hook even when the
@@ -4729,17 +5032,20 @@ export class Server {
4729
5032
  entry.idempotent !== undefined ||
4730
5033
  !BUN_TABLE_METHODS.includes(method) ||
4731
5034
  (preflightOnFallback && method === "OPTIONS") ||
4732
- under.get(run)?.some((other) => outranks(other.pattern.segments, pattern.segments))) {
5035
+ under.get(run)?.some((other) => outranks(other, pattern.segments)) ||
5036
+ (method === "GET" && sockets.some((socket) => mayShareRequest(socket, pattern.segments)))) {
4733
5037
  continue;
4734
5038
  }
4735
5039
  const paramNames = pattern.paramNames;
4736
5040
  const fused = mayUseFusedNative ? entry.execution.fusedWeb : undefined;
4737
- const handler = this.compileBunNativeHandler(entry, paramNames, fused, unboundedSignal, unboundedBudget, peer, fallback);
5041
+ const handler = this.compileBunNativeHandler(entry, method, paramNames, fused, unboundedSignal, unboundedBudget, peer, fallback, hooked ? platformFor : undefined);
4738
5042
  // The table matched the target as sent; one `fetch` refuses or rewrites goes through it. Left
4739
5043
  // off, `validateHost` costs nothing here: the table holds no wildcard, a `..` parameter falls
4740
5044
  // back, and reading the Host on every request measured about 6% of a bare route.
4741
5045
  tableAt(path, pattern.segments)[method] = this.validateHost
4742
- ? (request, server) => requestTarget(request, true) ? fallback(request, server) : handler(request, server)
5046
+ ? (request, server) => requestTarget(request, true, this.normalizeTarget)
5047
+ ? fallback(request, server)
5048
+ : handler(request, server)
4743
5049
  : handler;
4744
5050
  count += 1;
4745
5051
  if (paramNames.length > 0) {
@@ -4794,11 +5100,17 @@ export class Server {
4794
5100
  * nothing - so any endpoint slower than 10s (a render, an export, a long upstream call) has its
4795
5101
  * connection cut mid-flight regardless of `requestTimeoutMs`. Apps with such endpoints must raise
4796
5102
  * this above their slowest expected response, which is why it is a first-class option and not a
4797
- * reason to drop down to `Bun.serve`. `0` disables the timeout entirely; max 255.
5103
+ * reason to drop down to `Bun.serve`, or hold those requests' connections alone with
5104
+ * `withIdleTimeout()` from `@nifrajs/core/idle-timeout`. `0` disables the timeout entirely; max 255.
4798
5105
  *
4799
5106
  * `tls` serves HTTPS directly: `app.listen(443, { tls: { cert, key } })` with PEM text or bytes.
4800
5107
  * Requests then arrive with `https:` URLs. Behind a proxy or a platform that terminates TLS, leave
4801
5108
  * it unset.
5109
+ *
5110
+ * `static` sees every request before the app does, the ones Bun's route table serves included, so
5111
+ * a file it answers wins over any route at the same path. It returns the file's response, or
5112
+ * `undefined` to let the app route the request. No hook of the app runs for a request it answers.
5113
+ * `@nifrajs/web`'s Bun build serves its client bundle and public files this way.
4802
5114
  */
4803
5115
  listen(port, options) {
4804
5116
  if (typeof Bun === "undefined") {
@@ -4851,6 +5163,10 @@ export class Server {
4851
5163
  return jsonError(500, "internal_error");
4852
5164
  },
4853
5165
  };
5166
+ const idleTimeoutSec = options?.idleTimeoutSec ?? BUN_IDLE_TIMEOUT_SEC;
5167
+ const setIdleTimeout = (request, seconds) =>
5168
+ // biome-ignore lint/plugin/noChainedTypeAssertions lint/plugin/requireSafetyCommentForTypeAssertion: `running` is the Bun `Server` this call started (`RunningServer` publishes a subset of it), and every source a Bun listener routes is a `Request`.
5169
+ running.timeout(request, seconds ?? idleTimeoutSec);
4854
5170
  const fallback = (req, server) => {
4855
5171
  // Bun has already framed and bounded this request body in its HTTP parser. Mark the
4856
5172
  // source before the portable fallback runs so body schemas use Bun's native `json()`
@@ -4858,25 +5174,59 @@ export class Server {
4858
5174
  // Requests. Native route handlers mark themselves in compileBunNativeHandler; this
4859
5175
  // covers every request the native lane does not serve.
4860
5176
  markTrustedBodyFraming(req);
4861
- return this.fetch(req, bunPeerPlatform(server, req));
5177
+ return this.fetch(req, new BunPeerPlatform(server, req, setIdleTimeout));
4862
5178
  };
4863
5179
  // Bun names the socket peer of a request when its server is asked. A request served from the
4864
5180
  // native table carries this one platform, which asks only when a handler reads `c.clientIp`.
5181
+ const socketPeer = (request) => running.requestIP(request)?.address;
5182
+ // A `clientIp` trust declaration resolves here too, from the request's headers, when a handler reads
5183
+ // the address; only a declaration of zero hops names the socket peer.
5184
+ const trust = this.clientIpTrust;
4865
5185
  const peer = {
4866
- [PLATFORM_PEER]: (request) => running.requestIP(request)?.address,
5186
+ [PLATFORM_PEER]: trust === undefined
5187
+ ? socketPeer
5188
+ : (request) => resolveClientIp(!("header" in trust) && trust.trustedHops === 0 ? socketPeer(request) : undefined,
5189
+ // biome-ignore lint/plugin/requireSafetyCommentForTypeAssertion: every source a Bun listener routes is a `Request`.
5190
+ request, trust),
5191
+ [PLATFORM_IDLE_TIMEOUT]: setIdleTimeout,
4867
5192
  };
4868
- const nativeRoutes = wsHandlers === undefined ? this.buildBunNativeRoutes(fallback, peer) : undefined;
5193
+ const nativeRoutes = this.buildBunNativeRoutes(fallback, peer, (request, server) =>
5194
+ // biome-ignore lint/plugin/requireSafetyCommentForTypeAssertion: `fallback` hands `fetch` the same platform, which holds no environment.
5195
+ new BunPeerPlatform(server, request, setIdleTimeout));
5196
+ const first = options?.static;
5197
+ const before = (inner) => first === undefined
5198
+ ? inner
5199
+ : (req, server) => {
5200
+ const answer = first(req);
5201
+ if (answer === undefined)
5202
+ return inner(req, server);
5203
+ return answer instanceof Promise
5204
+ ? answer.then((response) => response ?? inner(req, server))
5205
+ : answer;
5206
+ };
5207
+ // Bun runs a table handler without `fetch`, so each one gets the same front as the fallback.
5208
+ if (first !== undefined && nativeRoutes !== undefined) {
5209
+ for (const path in nativeRoutes) {
5210
+ const methods = nativeRoutes[path];
5211
+ for (const method of BUN_TABLE_METHODS) {
5212
+ const handler = methods[method];
5213
+ if (handler !== undefined)
5214
+ methods[method] = before(handler);
5215
+ }
5216
+ }
5217
+ }
4869
5218
  const sockets = new Set();
4870
5219
  this.bunSockets = sockets;
4871
5220
  const running = (wsHandlers === undefined
4872
5221
  ? Bun.serve({
4873
5222
  ...listening,
4874
5223
  ...(nativeRoutes === undefined ? {} : { routes: nativeRoutes }),
4875
- fetch: fallback,
5224
+ fetch: before(fallback),
4876
5225
  })
4877
5226
  : Bun.serve({
4878
5227
  ...listening,
4879
- fetch: (req, server) => this.bunFetchWithWebSocket(req, server),
5228
+ ...(nativeRoutes === undefined ? {} : { routes: nativeRoutes }),
5229
+ fetch: before((req, server) => this.bunFetchWithWebSocket(req, server, setIdleTimeout)),
4880
5230
  // Bun's `ServerWebSocket<BunWsData>` is runtime-compatible with the handlers' structural
4881
5231
  // `BunSocket` view (kept local so `Bun.*` types never leak into the published .d.ts); the
4882
5232
  // `unknown` params bridge a TS structural-variance quirk. Round-trip covered by websocket.test.ts.