@nifrajs/core 3.1.0 → 3.3.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 (146) hide show
  1. package/README.md +21 -0
  2. package/dist/durable-adapters.d.ts.map +1 -1
  3. package/dist/durable-adapters.js +26 -3
  4. package/dist/durable-adapters.js.map +1 -1
  5. package/dist/durable-approval.d.ts +56 -0
  6. package/dist/durable-approval.d.ts.map +1 -0
  7. package/dist/durable-approval.js +233 -0
  8. package/dist/durable-approval.js.map +1 -0
  9. package/dist/durable-effect.d.ts +42 -0
  10. package/dist/durable-effect.d.ts.map +1 -0
  11. package/dist/durable-effect.js +234 -0
  12. package/dist/durable-effect.js.map +1 -0
  13. package/dist/durable-execution.d.ts +8 -346
  14. package/dist/durable-execution.d.ts.map +1 -1
  15. package/dist/durable-execution.js +7 -1150
  16. package/dist/durable-execution.js.map +1 -1
  17. package/dist/durable-saga.d.ts +94 -0
  18. package/dist/durable-saga.d.ts.map +1 -0
  19. package/dist/durable-saga.js +550 -0
  20. package/dist/durable-saga.js.map +1 -0
  21. package/dist/edge-kit.d.ts +2 -2
  22. package/dist/edge-kit.d.ts.map +1 -1
  23. package/dist/edge-kit.js +2 -2
  24. package/dist/edge-kit.js.map +1 -1
  25. package/dist/index.d.ts +23 -2
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js +18 -2
  28. package/dist/index.js.map +1 -1
  29. package/dist/internal/durable-approval-protocol.d.ts +1 -1
  30. package/dist/internal/durable-approval-protocol.d.ts.map +1 -1
  31. package/dist/internal/durable-effect-protocol.d.ts +1 -1
  32. package/dist/internal/durable-effect-protocol.d.ts.map +1 -1
  33. package/dist/internal/durable-saga-protocol.d.ts +1 -1
  34. package/dist/internal/durable-saga-protocol.d.ts.map +1 -1
  35. package/dist/internal/durable-shared.d.ts +26 -0
  36. package/dist/internal/durable-shared.d.ts.map +1 -0
  37. package/dist/internal/durable-shared.js +131 -0
  38. package/dist/internal/durable-shared.js.map +1 -0
  39. package/dist/internal/durable-types.d.ts +159 -0
  40. package/dist/internal/durable-types.d.ts.map +1 -0
  41. package/dist/internal/durable-types.js +2 -0
  42. package/dist/internal/durable-types.js.map +1 -0
  43. package/dist/internal/route-compiler.d.ts +61 -0
  44. package/dist/internal/route-compiler.d.ts.map +1 -0
  45. package/dist/internal/route-compiler.js +121 -0
  46. package/dist/internal/route-compiler.js.map +1 -0
  47. package/dist/internal/route-execution.d.ts +6 -4
  48. package/dist/internal/route-execution.d.ts.map +1 -1
  49. package/dist/internal/route-execution.js +2 -17
  50. package/dist/internal/route-execution.js.map +1 -1
  51. package/dist/internal/route-lanes.d.ts +35 -0
  52. package/dist/internal/route-lanes.d.ts.map +1 -0
  53. package/dist/internal/route-lanes.js +150 -0
  54. package/dist/internal/route-lanes.js.map +1 -0
  55. package/dist/internal/route-program.d.ts +67 -0
  56. package/dist/internal/route-program.d.ts.map +1 -0
  57. package/dist/internal/route-program.js +199 -0
  58. package/dist/internal/route-program.js.map +1 -0
  59. package/dist/server/bare-error-lane.d.ts +1 -1
  60. package/dist/server/bare-error-lane.d.ts.map +1 -1
  61. package/dist/server/bare-error-lane.js +3 -3
  62. package/dist/server/bare-error-lane.js.map +1 -1
  63. package/dist/server/body-lane.d.ts +13 -0
  64. package/dist/server/body-lane.d.ts.map +1 -0
  65. package/dist/server/body-lane.js +134 -0
  66. package/dist/server/body-lane.js.map +1 -0
  67. package/dist/server/contract.d.ts +40 -14
  68. package/dist/server/contract.d.ts.map +1 -1
  69. package/dist/server/contract.js +7 -0
  70. package/dist/server/contract.js.map +1 -1
  71. package/dist/server/cookies.d.ts.map +1 -1
  72. package/dist/server/cookies.js +16 -2
  73. package/dist/server/cookies.js.map +1 -1
  74. package/dist/server/plugin.d.ts +2 -2
  75. package/dist/server/plugin.d.ts.map +1 -1
  76. package/dist/server/plugin.js +6 -1
  77. package/dist/server/plugin.js.map +1 -1
  78. package/dist/server/registry.d.ts +40 -12
  79. package/dist/server/registry.d.ts.map +1 -1
  80. package/dist/server/request-context.d.ts +4 -28
  81. package/dist/server/request-context.d.ts.map +1 -1
  82. package/dist/server/request-context.js +10 -185
  83. package/dist/server/request-context.js.map +1 -1
  84. package/dist/server/respond.d.ts.map +1 -1
  85. package/dist/server/respond.js +39 -2
  86. package/dist/server/respond.js.map +1 -1
  87. package/dist/server/runtime-core.d.ts +18 -2
  88. package/dist/server/runtime-core.d.ts.map +1 -1
  89. package/dist/server/runtime-core.js +0 -23
  90. package/dist/server/runtime-core.js.map +1 -1
  91. package/dist/server/server-types.d.ts +3 -0
  92. package/dist/server/server-types.d.ts.map +1 -1
  93. package/dist/server/server.d.ts +113 -84
  94. package/dist/server/server.d.ts.map +1 -1
  95. package/dist/server/server.js +506 -639
  96. package/dist/server/server.js.map +1 -1
  97. package/dist/server/validation.d.ts +5 -0
  98. package/dist/server/validation.d.ts.map +1 -0
  99. package/dist/server/validation.js +13 -0
  100. package/dist/server/validation.js.map +1 -0
  101. package/dist/server/ws-bun.d.ts +11 -3
  102. package/dist/server/ws-bun.d.ts.map +1 -1
  103. package/dist/server/ws-bun.js +42 -12
  104. package/dist/server/ws-bun.js.map +1 -1
  105. package/dist/server/ws-hook.d.ts +3 -2
  106. package/dist/server/ws-hook.d.ts.map +1 -1
  107. package/dist/server.d.ts +2 -2
  108. package/dist/server.d.ts.map +1 -1
  109. package/dist/server.js +1 -1
  110. package/dist/server.js.map +1 -1
  111. package/dist/single-copy.d.ts.map +1 -1
  112. package/dist/single-copy.js +3 -1
  113. package/dist/single-copy.js.map +1 -1
  114. package/package.json +6 -3
  115. package/src/durable-adapters.ts +20 -0
  116. package/src/durable-approval.ts +281 -0
  117. package/src/durable-effect.ts +300 -0
  118. package/src/durable-execution.ts +8 -1627
  119. package/src/durable-saga.ts +744 -0
  120. package/src/edge-kit.ts +2 -2
  121. package/src/index.ts +129 -2
  122. package/src/internal/durable-approval-protocol.ts +1 -1
  123. package/src/internal/durable-effect-protocol.ts +1 -1
  124. package/src/internal/durable-saga-protocol.ts +1 -1
  125. package/src/internal/durable-shared.ts +157 -0
  126. package/src/internal/durable-types.ts +194 -0
  127. package/src/internal/route-compiler.ts +234 -0
  128. package/src/internal/route-execution.ts +34 -84
  129. package/src/internal/route-lanes.ts +221 -0
  130. package/src/internal/route-program.ts +404 -0
  131. package/src/server/bare-error-lane.ts +3 -2
  132. package/src/server/body-lane.ts +203 -0
  133. package/src/server/contract.ts +70 -20
  134. package/src/server/cookies.ts +16 -2
  135. package/src/server/plugin.ts +11 -5
  136. package/src/server/registry.ts +71 -16
  137. package/src/server/request-context.ts +7 -262
  138. package/src/server/respond.ts +45 -2
  139. package/src/server/runtime-core.ts +28 -2
  140. package/src/server/server-types.ts +3 -0
  141. package/src/server/server.ts +1152 -1260
  142. package/src/server/validation.ts +19 -0
  143. package/src/server/ws-bun.ts +44 -11
  144. package/src/server/ws-hook.ts +3 -2
  145. package/src/server.ts +7 -2
  146. package/src/single-copy.ts +3 -1
@@ -1,11 +1,13 @@
1
1
  import { admitDeadline, createRequestBudget, createUnboundedRequestBudget, NIFRA_DEADLINE_HEADER, } from "../budget.js";
2
2
  import { FrameworkError, RouteConfigError } from "../errors.js";
3
- import { CAPABILITY_GUARD, createCapabilityGuard, DEFAULT_CAPABILITY_INTERCEPTOR_TIMEOUT_MS, normalizeRouteCapabilities, } from "../internal/capability-runtime.js";
4
- import { assuranceDeclarationsOf, assuranceEvidenceFor, NIFRA_ASSURANCE_IDS, validEvidenceId, } from "../internal/route-assurance.js";
3
+ import { DEFAULT_CAPABILITY_INTERCEPTOR_TIMEOUT_MS, } from "../internal/capability-runtime.js";
4
+ import { assuranceDeclarationsOf, assuranceEvidenceFor, } from "../internal/route-assurance.js";
5
5
  import { RouteCatalog } from "../internal/route-catalog.js";
6
+ import { compileRouteOptions } from "../internal/route-compiler.js";
6
7
  import { compileRouteExecutionPlan, } from "../internal/route-execution.js";
8
+ import { compileRouteProgram, executeRouteProgram } from "../internal/route-program.js";
7
9
  import { isSameOriginRequest } from "../internal/same-origin.js";
8
- import { compileRoutePattern, decodeRouteParams } from "../router/pattern.js";
10
+ import { decodeRouteParams } from "../router/pattern.js";
9
11
  import { EMPTY_PARAMS, Router } from "../router/router.js";
10
12
  import { emitRequestErrorLog, renderBareError } from "./bare-error-lane.js";
11
13
  import { assertByteLimit, markTransportCap, markTrustedBodyFraming, UNLIMITED_BODY_BYTES, } from "./body.js";
@@ -14,11 +16,12 @@ import { hasLowercaseHeaderKeysMark, headerKeysAllLowercase, markLowercaseHeader
14
16
  import { headerObjectOf } from "./headers.js";
15
17
  import { jsonError, pathnameOf, plainError, urlPartsOf } from "./http.js";
16
18
  import { withStaticNodeHeaders } from "./node-outcome.js";
17
- import { isUrlEncodedForm, queryObjectOf, readBoundedForm, searchOf, } from "./query.js";
18
- import { RequestContext, readBodyFramed, readBoundedJsonSource } from "./request-context.js";
19
+ import { queryObjectOf, searchOf } from "./query.js";
20
+ import { RequestContext, readBodyFramed } from "./request-context.js";
19
21
  import { applyStaticResponseHeaders, buildStaticResponseHeaders, fusedRespond, fusedRespondNoSet, toResponse, } from "./respond.js";
20
- import { CONTEXT_SEARCH, CONTEXT_SET, EMPTY_RESPONSE_CONTROLS, getNeverAbortSignal, getUnboundedRequestBudget, headerOf, isResponseResult, requestOf, status, } from "./runtime-core.js";
22
+ import { CONTEXT_SEARCH, CONTEXT_SET, EMPTY_RESPONSE_CONTROLS, getNeverAbortSignal, getUnboundedRequestBudget, headerOf, isResponseResult, requestOf, } from "./runtime-core.js";
21
23
  import { normalizeStaticResponseHeaders } from "./static-headers.js";
24
+ import { plainValidationError } from "./validation.js";
22
25
  import { INSTALL_EFFECT_LEDGER, INSTALL_IDEMPOTENCY, INSTALL_MCP, INSTALL_NODE_DIRECT, INSTALL_RESPONSE_CONTRACT, INSTALL_RESPONSE_OBSERVER, INSTALL_SSE, INSTALL_WS, NODE_NATIVE_MOUNT, RESOLVE_NODE_MOUNT, } from "./install.js";
23
26
  import { jsonLogger } from "./logger.js";
24
27
  const functionToString = Function.prototype.toString;
@@ -74,22 +77,13 @@ const STOP_HOOK_TIMEOUT_MS = 5_000;
74
77
  * owner, shared with the server-function mount in `@nifrajs/web` - the two used to answer differently
75
78
  * for the same request, so a browser that could open a socket was told its POST was cross-origin. */
76
79
  const wsSameOrigin = isSameOriginRequest;
77
- function validationIssues(issues) {
78
- const serialized = issues.map((issue) => {
79
- const path = issue.path?.map((seg) => String(typeof seg === "object" ? seg.key : seg));
80
- return path !== undefined ? { message: issue.message, path } : { message: issue.message };
81
- });
82
- return { ok: false, error: "validation", issues: serialized };
83
- }
84
- /** The 422 as plain data - the shape every lane's response wrapper takes, so a rejected body costs
85
- * what an accepted one costs: on Node it is written straight to the socket with a `content-length`
86
- * instead of being built as a `Response` and drained back out. */
87
- export function plainValidationError(issues) {
88
- return status(422, validationIssues(issues));
89
- }
90
80
  // `jsonError`, `urlPartsOf`, `pathnameOf` moved to `./http.ts` (a dependency-free leaf shared with the
91
81
  // opt-in request lanes); re-exported so existing importers keep resolving from here.
92
82
  export { pathnameOf, urlPartsOf } from "./http.js";
83
+ /** The 422 as plain data - the shape every lane's response wrapper takes, so a rejected body costs
84
+ * what an accepted one costs: on Node it is written straight to the socket with a `content-length`
85
+ * instead of being built as a `Response` and drained back out. */
86
+ export { plainValidationError } from "./validation.js";
93
87
  // Query-string + urlencoded-form parsing now lives in `./query.ts`; re-exported so existing
94
88
  // importers keep resolving `searchOf`/`queryObjectOf`/`QueryValue` from here.
95
89
  export { queryObjectOf, searchOf };
@@ -128,20 +122,6 @@ function stripMountPrefix(request, prefix) {
128
122
  function responseSet(ctx) {
129
123
  return ctx[CONTEXT_SET]() ?? EMPTY_RESPONSE_CONTROLS;
130
124
  }
131
- /**
132
- * A `derive` that answered with a response rather than a context extension - `status(...)`, or a bare
133
- * `Response`. The request ends there, on the same lane a `beforeHandle`'s early return takes.
134
- *
135
- * `derive` is the one stage that could not exit early: `beforeHandle` says "done" by returning a
136
- * value, but a derive's return value IS the extension, so the only way out was `throw`ing a
137
- * `Response` - which measured as the single most expensive way to answer (the `Response` object
138
- * costs ~40% of the rejection lane; see `PlainRender`). Neither shape had a meaning here before:
139
- * `Object.assign(ctx, response)` copies nothing (a `Response`'s properties live on its prototype),
140
- * so a derive that returned one silently fell through to the handler it meant to prevent.
141
- */
142
- function isDeriveEarlyExit(extension) {
143
- return isResponseResult(extension) || extension instanceof Response;
144
- }
145
125
  function isContextlessNoArgArrow(handler) {
146
126
  if (handler.length !== 0)
147
127
  return false;
@@ -255,6 +235,13 @@ export class Server {
255
235
  * Created by the first `app.ws()` via the `@nifrajs/core/ws` runtime - `undefined` until then, so a
256
236
  * no-WebSocket app never constructs (or bundles) it. */
257
237
  topics;
238
+ /** True once any `app.ws()` route sets `validateSend: true` - the one case where a broadcast frame
239
+ * is validated (and possibly dropped) per socket, so `app.publish` must stay on the JS registry loop
240
+ * rather than delegate to Bun's native `server.publish`. */
241
+ wsHasValidatedSend = false;
242
+ /** Bun's native topic broadcast, bound by `listen()` in native-pubsub mode; `app.publish` uses it
243
+ * instead of the JS registry. `undefined` off Bun, before `listen()`, or with a validated-send route. */
244
+ nativePublish;
258
245
  maxBodyBytes;
259
246
  protoPoisoning;
260
247
  /** `trustBodyFraming`: mark every `app.fetch` request as runtime-framed (see ServerOptions). */
@@ -861,6 +848,10 @@ export class Server {
861
848
  if (handler.validateSend === true && handler.sendSchema === undefined) {
862
849
  throw new RouteConfigError("INVALID_WS_SEND_VALIDATION", `route WS ${path}: validateSend requires sendSchema`);
863
850
  }
851
+ // A validated-send route makes broadcast bytes route-dependent (drop-on-invalid per socket), so the
852
+ // whole app forgoes native pub/sub and keeps the JS registry loop that runs each send through the sender.
853
+ if (handler.validateSend === true)
854
+ this.wsHasValidatedSend = true;
864
855
  this.topics ??= runtime.createTopics();
865
856
  // A `messageSchema` wraps `message` with validation once, here - every adapter then dispatches
866
857
  // already-validated, typed messages (Bun/Deno/Node/Workers) with no per-adapter code.
@@ -876,6 +867,12 @@ export class Server {
876
867
  * an external fan-out (Redis, a Durable Object) to this. A no-op when nobody is subscribed.
877
868
  */
878
869
  publish(topic, data) {
870
+ // Native-pubsub mode (Bun, no validated-send route): one native broadcast to every subscriber, no
871
+ // per-connection JS loop. Bound only after `listen()`; before that (or off Bun) the registry runs.
872
+ if (this.nativePublish !== undefined) {
873
+ this.nativePublish(topic, data);
874
+ return;
875
+ }
879
876
  // No `app.ws()` yet ⇒ no registry and necessarily no subscribers - a publish is a no-op anyway.
880
877
  this.topics?.publish(topic, data);
881
878
  }
@@ -912,205 +909,88 @@ export class Server {
912
909
  this.catalog.addBatch(staged);
913
910
  }
914
911
  prepareRoute(method, path, schema, handler) {
915
- const pattern = compileRoutePattern(path);
916
- // Resolved here, once: `undefined` past this point means the route declared `"unlimited"`, NOT
917
- // "nothing declared". Readers fall back to UNLIMITED_BODY_BYTES accordingly.
918
- let bodyLimit = this.maxBodyBytes;
919
- const invalidBodyLimit = (message) => {
920
- throw new RouteConfigError("INVALID_BODY_LIMIT", `route ${method} ${path}: ${message}`);
921
- };
922
- if (schema?.bodyLimitReason !== undefined && schema.bodyLimit !== "unlimited") {
923
- invalidBodyLimit('bodyLimitReason is only valid with bodyLimit: "unlimited"');
924
- }
925
- if (schema?.bodyLimit === "unlimited") {
926
- if (typeof schema.bodyLimitReason !== "string" ||
927
- schema.bodyLimitReason.trim().length === 0) {
928
- invalidBodyLimit('bodyLimit: "unlimited" requires a non-empty bodyLimitReason');
929
- }
930
- if (schema.body !== undefined) {
931
- invalidBodyLimit('bodyLimit: "unlimited" cannot be used with a body schema; use a finite cap');
932
- }
933
- if (schema.idempotency !== undefined) {
934
- invalidBodyLimit('bodyLimit: "unlimited" cannot be used with idempotency');
935
- }
936
- bodyLimit = undefined;
937
- }
938
- else if (schema?.bodyLimit !== undefined) {
939
- try {
940
- assertByteLimit(schema.bodyLimit, "route bodyLimit");
941
- }
942
- catch {
943
- invalidBodyLimit('bodyLimit must be a non-negative safe integer or "unlimited"');
944
- }
945
- bodyLimit = schema.bodyLimit;
946
- }
947
- const capabilities = normalizeRouteCapabilities(schema?.capabilities);
948
- const handlerAssurance = assuranceDeclarationsOf(handler);
949
- const invalidHandlerScope = handlerAssurance.find((declaration) => declaration.scope !== "plugin");
950
- if (invalidHandlerScope !== undefined) {
951
- throw new RouteConfigError("INVALID_ASSURANCE", `route handler assurance must use plugin scope (received ${invalidHandlerScope.scope})`);
952
- }
953
- const authenticated = assuranceEvidenceFor([...this.activeAssurance, ...handlerAssurance, ...this.globalAssurance], method, path).some((evidence) => evidence.id === NIFRA_ASSURANCE_IDS.AUTHENTICATED);
954
- const routeDecorations = { ...this.decorations };
955
- if (capabilities.length > 0) {
956
- routeDecorations[CAPABILITY_GUARD] = createCapabilityGuard(capabilities, method, path, this.onCapabilityUse, this.idempotencyRuntime?.trackEffect, Object.freeze([...this.capabilityInterceptors]), Object.freeze([...this.capabilityObservers]));
957
- }
958
- const hasDecorations = Reflect.ownKeys(routeDecorations).length > 0;
959
- // An idempotency route runs a dedupe lane that must buffer the body and capture the response, so it
960
- // never takes the fused/native fast path - force it onto the portable matched lane (which routes
961
- // through `fetchMatched`, where the dedupe wrapper lives). Fail closed: a route may not declare
962
- // idempotency unless the idempotency runtime is installed, so the safety gate can never be silently
963
- // dropped by a missing plugin.
964
- if (schema?.idempotency !== undefined && this.idempotencyRuntime === undefined) {
965
- throw new RouteConfigError("INVALID_IDEMPOTENCY", "route declares idempotency but the idempotency plugin is not installed; add .use(idempotency())");
966
- }
967
- const idempotent = this.idempotencyRuntime?.resolve(schema, authenticated, this.maxBodyBytes);
968
- // A ledgered route (capabilities declared + `.use(effectLedger())`) needs a per-request
969
- // context to carry the ledger and a settle step to seal + sink it, so it too leaves the
970
- // fused/contextless fast path. Resolved per route, at registration - like the capability guard.
971
- const ledgered = this.effectLedgerRuntime?.resolve(capabilities, method, path);
972
- // A checked response schema needs the handler's VALUE before it becomes bytes, and the fused and
973
- // native lanes exist precisely to skip that step. So a contracted route leaves them - the same
974
- // structural trade an idempotent route makes above. The check itself is not the cost: a compiled
975
- // validator measures ~100ns/response, within benchmark noise on any route these lanes already
976
- // exclude (middleware, derives, lifecycle hooks).
977
- const runtime = this.responseContractRuntime;
978
- const contracted = runtime !== undefined && schema?.response !== undefined
979
- ? { runtime, schema: schema.response }
980
- : undefined;
981
- const bare = schema?.params === undefined &&
982
- schema?.headers === undefined &&
983
- schema?.body === undefined &&
984
- schema?.query === undefined &&
985
- idempotent === undefined &&
986
- ledgered === undefined &&
987
- contracted === undefined &&
988
- this.derives.length === 0 &&
989
- this.beforeHandleHooks.length === 0 &&
990
- this.afterHandleHooks.length === 0 &&
991
- this.onErrorHooks.length === 0;
992
- // A route whose ONLY lifecycle step is a query schema can fuse too: the parse + validate +
993
- // handler + respond collapse into one closure with no lifecycle promise on the sync path. The
994
- // guards mirror the `query` lane below PLUS everything the fused dispatch skips: around hooks,
995
- // the idempotency/ledger wrappers, and validation-error recovery (schema or server default) -
996
- // the fused invalid path is exactly `validationError(issues)`, so any recovery semantics keep
997
- // the generic lane.
998
- const fusedQuery = !bare &&
999
- contracted === undefined &&
1000
- schema?.query !== undefined &&
1001
- schema.body === undefined &&
1002
- schema.params === undefined &&
1003
- schema.headers === undefined &&
1004
- schema.onValidationError === undefined &&
1005
- this.defaultOnValidationError === undefined &&
1006
- idempotent === undefined &&
1007
- ledgered === undefined &&
1008
- this.derives.length === 0 &&
1009
- this.beforeHandleHooks.length === 0 &&
1010
- this.afterHandleHooks.length === 0 &&
1011
- this.onErrorHooks.length === 0 &&
1012
- this.aroundHooks.length === 0;
1013
- const bodyOnly = contracted === undefined &&
1014
- schema?.body !== undefined &&
1015
- schema.query === undefined &&
1016
- schema.params === undefined &&
1017
- schema.headers === undefined &&
1018
- this.derives.length === 0 &&
1019
- this.beforeHandleHooks.length === 0 &&
1020
- this.afterHandleHooks.length === 0 &&
1021
- this.onErrorHooks.length === 0;
1022
- const fusedBody = !bare &&
1023
- bodyOnly &&
1024
- schema.onValidationError === undefined &&
1025
- this.defaultOnValidationError === undefined &&
1026
- idempotent === undefined &&
1027
- ledgered === undefined &&
1028
- this.aroundHooks.length === 0;
1029
- // Body-only routes still need an async body read, but eligible routes can compile the validation +
1030
- // handler continuation once. The runner keeps the bounded parser and all error semantics while
1031
- // avoiding the generic entry/schema/lifecycle dispatch on Bun, Deno, and Node-direct.
912
+ const compiled = compileRouteOptions({
913
+ maxBodyBytes: this.maxBodyBytes,
914
+ activeAssurance: this.activeAssurance,
915
+ globalAssurance: this.globalAssurance,
916
+ decorations: this.decorations,
917
+ onCapabilityUse: this.onCapabilityUse,
918
+ capabilityInterceptors: this.capabilityInterceptors,
919
+ capabilityObservers: this.capabilityObservers,
920
+ effectLedgerRuntime: this.effectLedgerRuntime,
921
+ idempotencyRuntime: this.idempotencyRuntime,
922
+ responseContractRuntime: this.responseContractRuntime,
923
+ derives: this.derives,
924
+ beforeHandleHooks: this.beforeHandleHooks,
925
+ afterHandleHooks: this.afterHandleHooks,
926
+ onErrorHooks: this.onErrorHooks,
927
+ aroundHooks: this.aroundHooks,
928
+ defaultOnValidationError: this.defaultOnValidationError,
929
+ }, method, path, schema, handler);
930
+ const { pattern, bodyLimit, capabilities, routeDecorations, hasDecorations, idempotent, ledgered, responseContract, lanes, routeAssurance, } = compiled;
931
+ const { bare, fusedQuery, fusedBody } = lanes;
1032
932
  const fusedBodyRunner = fusedBody
1033
- ? this.buildFusedBodyRunner(handler, schema.body, hasDecorations ? routeDecorations : undefined, bodyLimit ?? UNLIMITED_BODY_BYTES)
933
+ ? this.buildFusedBodyRunner(handler, schema?.body, hasDecorations ? routeDecorations : undefined, bodyLimit ?? UNLIMITED_BODY_BYTES)
1034
934
  : undefined;
935
+ // Fused lifecycle lanes: derive + before (with or without an after), and body + the same shape.
936
+ // The lane selectors in `selectRouteLanes` are exhaustive about the lifecycleHookLane /
937
+ // body-derive-before-after eligibility, so we only need to check the lane and the hook counts.
938
+ const lifecycleHookLane = lanes.lifecycleHookLane;
939
+ const isFusedLifecycle = lifecycleHookLane !== undefined && this.derives.length === 1;
940
+ const isFusedBodyLifecycle = lanes.fusedLane === "body-derive-before" || lanes.fusedLane === "body-derive-before-after";
1035
941
  const fusedWeb = bare && this.aroundHooks.length === 0
1036
942
  ? this.buildFusedWeb(handler, hasDecorations ? routeDecorations : undefined, isContextlessNoArgArrow(handler), bodyLimit ?? UNLIMITED_BODY_BYTES)
1037
943
  : fusedQuery
1038
- ? this.buildFusedQueryWeb(handler, hasDecorations ? routeDecorations : undefined, schema.query, bodyLimit ?? UNLIMITED_BODY_BYTES)
944
+ ? this.buildFusedQueryWeb(handler, hasDecorations ? routeDecorations : undefined, schema?.query, bodyLimit ?? UNLIMITED_BODY_BYTES)
1039
945
  : fusedBody
1040
946
  ? this.buildFusedBodyWeb(fusedBodyRunner)
1041
- : undefined;
1042
- const contextless = bare && this.aroundHooks.length === 0 && isContextlessNoArgArrow(handler);
1043
- // The body and query lanes finalize the handler's result themselves, so a contracted route takes
1044
- // the general lifecycle lane instead - one place owns the check rather than three.
1045
- const lane = bare
1046
- ? "bare"
1047
- : bodyOnly
1048
- ? "body"
1049
- : contracted === undefined &&
1050
- schema?.body === undefined &&
1051
- schema?.query !== undefined &&
1052
- schema.params === undefined &&
1053
- schema.headers === undefined &&
1054
- this.derives.length === 0 &&
1055
- this.beforeHandleHooks.length === 0 &&
1056
- this.afterHandleHooks.length === 0 &&
1057
- this.onErrorHooks.length === 0
1058
- ? "query"
1059
- : "lifecycle";
1060
- // Lifecycle routes with no params schema are the common middleware shape: a derive/before hook
1061
- // plus an optional query or body schema. Select their complete validation stage at registration
1062
- // so the request path never re-checks params/body presence. Parameter-schema routes retain the
1063
- // generic lifecycle runner until their more involved recovery matrix is selected explicitly.
1064
- const lifecycleLane = lane !== "lifecycle" || schema?.params !== undefined || schema?.headers !== undefined
1065
- ? undefined
1066
- : schema?.body !== undefined
1067
- ? schema.query !== undefined
1068
- ? "body-query"
1069
- : "body"
1070
- : schema?.query !== undefined
1071
- ? "query"
1072
- : "hooks";
1073
- // The realistic middleware shape is commonly exactly one synchronous-or-async derive followed
1074
- // by one before hook. Keep the generic runner for every route that can observe decorations,
1075
- // after hooks, error hooks, or response contracts; this lane only removes the two per-request
1076
- // hook-loop dispatches and preserves the same async continuations and error handling.
1077
- const lifecycleHookLane = lane === "lifecycle" &&
1078
- contracted === undefined &&
1079
- !hasDecorations &&
1080
- this.derives.length === 1 &&
1081
- this.beforeHandleHooks.length === 1 &&
1082
- this.afterHandleHooks.length === 0 &&
1083
- this.onErrorHooks.length === 0
1084
- ? "derive-before"
1085
- : undefined;
947
+ : isFusedBodyLifecycle
948
+ ? this.buildFusedBodyDeriveBeforeAfter(handler, this.derives[0], this.beforeHandleHooks[0], this.afterHandleHooks[0], schema?.body, bodyLimit ?? UNLIMITED_BODY_BYTES)
949
+ : isFusedLifecycle && lifecycleHookLane === "derive-before"
950
+ ? this.buildFusedDeriveBefore(handler, this.derives[0], this.beforeHandleHooks[0], schema?.query, bodyLimit ?? UNLIMITED_BODY_BYTES)
951
+ : isFusedLifecycle && lifecycleHookLane === "derive-before-after"
952
+ ? this.buildFusedDeriveBeforeAfter(handler, this.derives[0], this.beforeHandleHooks[0], this.afterHandleHooks[0], schema?.query, bodyLimit ?? UNLIMITED_BODY_BYTES)
953
+ : undefined;
954
+ const program = compileRouteProgram({
955
+ schema,
956
+ handler: handler,
957
+ derives: this.derives,
958
+ beforeHandle: this.beforeHandleHooks,
959
+ afterHandle: this.afterHandleHooks,
960
+ onError: this.onErrorHooks,
961
+ decorations: routeDecorations,
962
+ hasDecorations,
963
+ bodySchema: schema?.body,
964
+ bodyLimit,
965
+ responseContract,
966
+ });
967
+ const contextless = lanes.bare && this.aroundHooks.length === 0 && isContextlessNoArgArrow(handler);
1086
968
  const execution = compileRouteExecutionPlan({
1087
- lane,
969
+ lane: lanes.lane,
1088
970
  contextless,
1089
971
  hasAround: this.aroundHooks.length > 0,
1090
972
  hasLedger: ledgered !== undefined,
1091
- lifecycleLane,
1092
973
  fusedWeb,
1093
974
  fusedBody: fusedBodyRunner,
1094
- fusedLane: fusedWeb === undefined ? undefined : fusedQuery ? "query" : fusedBody ? "body" : "bare",
975
+ fusedLane: lanes.fusedLane,
1095
976
  });
1096
977
  const registeredEntry = {
1097
- // (context: never) => unknown -> InternalHandler: the framework invokes it
1098
- // with the concrete RawContext the typed handler expects, so this is sound.
1099
978
  handler: handler,
1100
979
  schema,
1101
980
  bodyLimit,
1102
981
  idempotent,
1103
982
  ledgered,
1104
- responseContract: contracted,
983
+ responseContract,
1105
984
  derives: [...this.derives],
1106
985
  decorations: routeDecorations,
1107
986
  hasDecorations,
1108
987
  beforeHandle: [...this.beforeHandleHooks],
1109
988
  afterHandle: [...this.afterHandleHooks],
1110
989
  onError: [...this.onErrorHooks],
1111
- lifecycleHookLane,
990
+ lifecycleHookLane: lanes.lifecycleHookLane,
1112
991
  around: [...this.aroundHooks],
1113
992
  execution,
993
+ program,
1114
994
  };
1115
995
  const descriptor = {
1116
996
  method,
@@ -1119,64 +999,13 @@ export class Server {
1119
999
  ...(capabilities.length > 0 ? { capabilities } : {}),
1120
1000
  ...(schema?.family === true ? { family: true } : {}),
1121
1001
  };
1122
- const routeAssurance = [...this.activeAssurance, ...handlerAssurance];
1123
- if (contracted?.runtime.mode === "enforce" && schema?.response !== undefined) {
1124
- routeAssurance.push(Object.freeze({
1125
- id: NIFRA_ASSURANCE_IDS.RESPONSE_CONTRACT,
1126
- source: "response-contract",
1127
- scope: "plugin",
1128
- }));
1129
- }
1130
- // Inline `schema.assurance`: the route DECLARES its enforcement evidence adjacent to the handler, so an
1131
- // in-handler-guarded route satisfies a policy `require:` clause without a `withRouteAssurance` middleware
1132
- // rewrite. Each id becomes route-scoped `declared` evidence (invalid ids fail closed at registration).
1133
- for (const id of schema?.assurance ?? []) {
1134
- if (!validEvidenceId(id)) {
1135
- throw new Error(`route assurance: invalid evidence id ${JSON.stringify(id)} on ${method} ${path} (use lowercase dot/dash segments)`);
1136
- }
1137
- const declared = { id, source: "declared", scope: "plugin" };
1138
- Object.defineProperty(declared, "provenance", {
1139
- value: "declared",
1140
- enumerable: false,
1141
- configurable: false,
1142
- writable: false,
1143
- });
1144
- routeAssurance.push(Object.freeze(declared));
1145
- }
1146
- // Two ways a route proves its body is bounded, both enforced by the core at read time:
1147
- // `schema.body` (the validated read is bounded) and an explicit finite `schema.bodyLimit` (the
1148
- // transport cap shadows every reader, including a raw `c.req.body` stream that a Content-Length
1149
- // middleware cannot bound).
1150
- // `bodyLimit: "unlimited"` is rejected above for schema and idempotency routes. The remaining
1151
- // exemption is schema-less streaming/upload work, so it publishes NOTHING: there is no finite
1152
- // framework byte ceiling for assurance to attest and the application protocol owns that bound.
1153
- // The server-wide `maxBodyBytes` default is not evidence either. It applies to every route
1154
- // whether or not anyone thought about it, so publishing it would make the id pass everywhere and
1155
- // prove nothing; this evidence marks a bound the route chose.
1156
- const unlimitedBody = schema?.bodyLimit === "unlimited";
1157
- if (!unlimitedBody && (schema?.body !== undefined || typeof schema?.bodyLimit === "number")) {
1158
- routeAssurance.push(Object.freeze({
1159
- id: NIFRA_ASSURANCE_IDS.BODY_BOUNDED,
1160
- source: "route-schema",
1161
- scope: "plugin",
1162
- }));
1163
- }
1164
- // Declaring `schema.idempotency` is evidence for request replay only. It deliberately never proves
1165
- // durable command execution; that stronger evidence belongs to a command/outbox adapter.
1166
- if (schema?.idempotency !== undefined) {
1167
- routeAssurance.push(Object.freeze({
1168
- id: NIFRA_ASSURANCE_IDS.IDEMPOTENCY_KEY,
1169
- source: "route-schema",
1170
- scope: "plugin",
1171
- }));
1172
- }
1173
1002
  return {
1174
1003
  method,
1175
1004
  path,
1176
1005
  pattern,
1177
1006
  entry: registeredEntry,
1178
1007
  descriptor,
1179
- assurance: Object.freeze(routeAssurance),
1008
+ assurance: routeAssurance,
1180
1009
  };
1181
1010
  }
1182
1011
  /** The idempotency lane's bridge back into the normal matched lanes, resolved to a concrete Response
@@ -1341,14 +1170,37 @@ export class Server {
1341
1170
  return route;
1342
1171
  // Rebuild with the SAME builder that produced the closure - rebinding a query-fused route as
1343
1172
  // bare would silently drop its validation.
1344
- const fusedBody = entry.execution.fusedLane === "body"
1173
+ const lane = entry.execution.fusedLane;
1174
+ // Type-erase the lane so each `===` branch sees the full union, not the narrowed remainder.
1175
+ const laneName = lane;
1176
+ // Body+lifecycle routes have a fused Web renderer, but no body-only Node renderer: the latter
1177
+ // would bypass derive/before/after because the Node-direct dispatcher prefers `fusedBody` over
1178
+ // the generic execution plan. Only the original body-only lane may populate this slot.
1179
+ const fusedBody = laneName === "body"
1345
1180
  ? this.buildFusedBodyRunner(entry.handler, entry.schema?.body, entry.hasDecorations ? entry.decorations : undefined, entry.bodyLimit ?? UNLIMITED_BODY_BYTES)
1346
1181
  : undefined;
1347
- const fusedWeb = entry.execution.fusedLane === "body"
1182
+ const derive = entry.derives[0];
1183
+ const before = entry.beforeHandle[0];
1184
+ const after = entry.afterHandle[0];
1185
+ const fusedWeb = laneName === "body"
1348
1186
  ? this.buildFusedBodyWeb(fusedBody)
1349
- : entry.execution.fusedLane === "query"
1187
+ : laneName === "query"
1350
1188
  ? this.buildFusedQueryWeb(entry.handler, entry.hasDecorations ? entry.decorations : undefined, entry.schema?.query, entry.bodyLimit ?? UNLIMITED_BODY_BYTES)
1351
- : this.buildFusedWeb(entry.handler, entry.hasDecorations ? entry.decorations : undefined, isContextlessNoArgArrow(entry.handler), entry.bodyLimit ?? UNLIMITED_BODY_BYTES);
1189
+ : laneName === "body-derive-before" && derive !== undefined && before !== undefined
1190
+ ? this.buildFusedBodyDeriveBeforeAfter(entry.handler, derive, before, undefined, entry.schema?.body, entry.bodyLimit ?? UNLIMITED_BODY_BYTES)
1191
+ : laneName === "body-derive-before-after" &&
1192
+ derive !== undefined &&
1193
+ before !== undefined &&
1194
+ after !== undefined
1195
+ ? this.buildFusedBodyDeriveBeforeAfter(entry.handler, derive, before, after, entry.schema?.body, entry.bodyLimit ?? UNLIMITED_BODY_BYTES)
1196
+ : laneName === "derive-before" && derive !== undefined && before !== undefined
1197
+ ? this.buildFusedDeriveBefore(entry.handler, derive, before, entry.schema?.query, entry.bodyLimit ?? UNLIMITED_BODY_BYTES)
1198
+ : laneName === "derive-before-after" &&
1199
+ derive !== undefined &&
1200
+ before !== undefined &&
1201
+ after !== undefined
1202
+ ? this.buildFusedDeriveBeforeAfter(entry.handler, derive, before, after, entry.schema?.query, entry.bodyLimit ?? UNLIMITED_BODY_BYTES)
1203
+ : this.buildFusedWeb(entry.handler, entry.hasDecorations ? entry.decorations : undefined, isContextlessNoArgArrow(entry.handler), entry.bodyLimit ?? UNLIMITED_BODY_BYTES);
1352
1204
  return {
1353
1205
  ...route,
1354
1206
  entry: {
@@ -1987,7 +1839,7 @@ export class Server {
1987
1839
  /**
1988
1840
  * Route → build context → run. Synchronous through to the handler for a **bare** route
1989
1841
  * (selected by its compiled plan), so a sync handler produces its result with zero promise allocations;
1990
- * routes with validation/hooks keep the full async {@link runLifecycle}, unchanged.
1842
+ * routes with validation/hooks keep the full compiled route program, unchanged.
1991
1843
  */
1992
1844
  routeAndRun(source, platform, finalize, wrapResponse, onTimeout, webFast) {
1993
1845
  // Routing only needs the pathname. Scan the URL once but keep the two slices in locals instead
@@ -2152,7 +2004,6 @@ export class Server {
2152
2004
  /** The narrowest bare route: a syntactic `() => ...` handler cannot observe the context argument, so
2153
2005
  * successful requests can skip allocating `RequestContext`. Errors still allocate one for logging. */
2154
2006
  // @ts-expect-error TS6133 -- invoked structurally by compiled plans (internal/route-execution.ts)
2155
- // biome-ignore lint/correctness/noUnusedPrivateClassMembers: invoked structurally by compiled plans (internal/route-execution.ts)
2156
2007
  runContextlessBare(entry, source, params, search, signal, budget, platform, finalize, wrapResponse) {
2157
2008
  let result;
2158
2009
  try {
@@ -2175,18 +2026,17 @@ export class Server {
2175
2026
  return wrapResponse(err);
2176
2027
  const ctx = new RequestContext(source, params, search, signal, budget, platform, this.maxBodyBytes, this.protoPoisoning);
2177
2028
  this.logRequestError(err, ctx);
2178
- return wrapResponse(plainError(500, "internal_error"));
2029
+ return this.internalErrorResponse(wrapResponse);
2179
2030
  }
2180
2031
  /**
2181
2032
  * The synchronous fast path selected by a route's execution plan: apply static decorations, call the
2182
2033
  * handler, render the result - **no `await`** unless the handler itself returns a promise. It mirrors
2183
- * the bare slice of {@link runLifecycle} (which a bare route would otherwise no-op through) and shares
2034
+ * the same error boundary as the compiled route program (which a bare route would otherwise no-op through) and shares
2184
2035
  * {@link logRequestError}; a bare route has no `onError` hooks, so error handling is fully synchronous
2185
2036
  * (a thrown `Response` is control flow; anything else is a logged flat 500). This is where nifra skips
2186
2037
  * the per-request async-frame tax - the same win codegen routers get, but without `eval`.
2187
2038
  */
2188
2039
  // @ts-expect-error TS6133 -- invoked structurally by compiled plans (internal/route-execution.ts)
2189
- // biome-ignore lint/correctness/noUnusedPrivateClassMembers: invoked structurally by compiled plans (internal/route-execution.ts)
2190
2040
  runBare(entry, ctx, finalize, wrapResponse) {
2191
2041
  let result;
2192
2042
  try {
@@ -2203,7 +2053,7 @@ export class Server {
2203
2053
  }
2204
2054
  return finalize(result, responseSet(ctx));
2205
2055
  }
2206
- /** Bare-route error rendering - identical to {@link runLifecycle}'s catch minus the (absent) onError
2056
+ /** Bare-route error rendering - identical to the compiled route program's catch minus the (absent) onError
2207
2057
  * loop: a thrown `Response` is returned as deliberate control flow; anything else is logged + 500. */
2208
2058
  /**
2209
2059
  * Build a route's fused Web renderer. Composition happens once at
@@ -2383,14 +2233,338 @@ export class Server {
2383
2233
  return runHandler(ctx, validation.value);
2384
2234
  };
2385
2235
  }
2236
+ /**
2237
+ * Compile a route's `derive + before` lifecycle to a single closure: the per-request walk through
2238
+ * `runSync` → `runHooksSync` is a stack of 3 frames and 2 `Object.assign`s for what is by far the
2239
+ * most common middleware shape (auth/trace/id derivation, an auth/policy `beforeHandle`). Folding
2240
+ * the same steps into one closure hits the same monomorphic inline-cache site as `buildFusedWeb`,
2241
+ * and `Object.assign(ctx, deriveResult)` becomes one extra property write on the context's already
2242
+ * hot hidden class. Behavior is byte-identical to the compiled route program: the same thrown-value
2243
+ * contract (`Response` is control flow, anything else logs + 500), the same `c.set` semantics, the
2244
+ * same `fusedRespond` for the rendered body. The `LifecycleExecutionLane` guarantee
2245
+ * (`schema.params === undefined && schema.headers === undefined && derives === 1 &&
2246
+ * beforeHandle === 1 && afterHandle === 0 && onError === 0 && !hasResponseContract &&
2247
+ * !hasDecorations`) is enforced by `selectRouteLanes`; this lane is selected ONLY when all of those
2248
+ * hold. A `query` schema (the route declares `{ query }` alongside the hooks) is validated FIRST,
2249
+ * mirroring the compiled route program's stage order; the fused builder runs validate → derive →
2250
+ * before → handler in one frame.
2251
+ */
2252
+ buildFusedDeriveBefore(handler, derive, before, querySchema, maxBodyBytes) {
2253
+ const logError = (err, ctx) => this.bareError(err, ctx, (result, set) => this.wrapWebResponse(toResponse(result, set)), this.wrapWebResponse);
2254
+ return (source, params, search, signal, budget, platform, nativeContext) => {
2255
+ const ctx = nativeContext
2256
+ ? RequestContext.native(source, params, search, maxBodyBytes, platform, this.protoPoisoning)
2257
+ : new RequestContext(source, params, search, signal, budget, platform, maxBodyBytes, this.protoPoisoning);
2258
+ const runHandler = () => {
2259
+ let result;
2260
+ try {
2261
+ result = handler(ctx);
2262
+ }
2263
+ catch (err) {
2264
+ return logError(err, ctx);
2265
+ }
2266
+ if (result instanceof Promise) {
2267
+ return result.then((value) => fusedRespond(value, ctx, this.responseBodyTag, this.staticResponseHeaders, this.onResponseHooks.length === 0), (err) => logError(err, ctx));
2268
+ }
2269
+ return fusedRespond(result, ctx, this.responseBodyTag, this.staticResponseHeaders, this.onResponseHooks.length === 0);
2270
+ };
2271
+ const runBefore = () => {
2272
+ let early;
2273
+ try {
2274
+ early = before(ctx);
2275
+ }
2276
+ catch (err) {
2277
+ return logError(err, ctx);
2278
+ }
2279
+ if (early instanceof Promise) {
2280
+ return early.then((settled) => {
2281
+ if (settled === undefined)
2282
+ return runHandler();
2283
+ return fusedRespond(settled, ctx, this.responseBodyTag, this.staticResponseHeaders);
2284
+ }, (err) => logError(err, ctx));
2285
+ }
2286
+ if (early === undefined)
2287
+ return runHandler();
2288
+ return fusedRespond(early, ctx, this.responseBodyTag, this.staticResponseHeaders);
2289
+ };
2290
+ const runDerive = () => {
2291
+ let derived;
2292
+ try {
2293
+ derived = derive(ctx);
2294
+ }
2295
+ catch (err) {
2296
+ return logError(err, ctx);
2297
+ }
2298
+ if (derived instanceof Promise) {
2299
+ return derived.then((settled) => {
2300
+ if (isResponseResult(settled) || settled instanceof Response) {
2301
+ return fusedRespond(settled, ctx, this.responseBodyTag, this.staticResponseHeaders);
2302
+ }
2303
+ Object.assign(ctx, settled);
2304
+ return runBefore();
2305
+ }, (err) => logError(err, ctx));
2306
+ }
2307
+ if (isResponseResult(derived) || derived instanceof Response) {
2308
+ return fusedRespond(derived, ctx, this.responseBodyTag, this.staticResponseHeaders);
2309
+ }
2310
+ Object.assign(ctx, derived);
2311
+ return runBefore();
2312
+ };
2313
+ if (querySchema === undefined)
2314
+ return runDerive();
2315
+ let validation;
2316
+ try {
2317
+ validation = querySchema["~standard"].validate(queryObjectOf(ctx[CONTEXT_SEARCH]));
2318
+ }
2319
+ catch (err) {
2320
+ return logError(err, ctx);
2321
+ }
2322
+ if (validation instanceof Promise) {
2323
+ return validation.then((settled) => {
2324
+ if (settled.issues !== undefined)
2325
+ return this.wrapWebResponse(plainValidationError(settled.issues));
2326
+ ctx.query = settled.value;
2327
+ return runDerive();
2328
+ }, (err) => logError(err, ctx));
2329
+ }
2330
+ if (validation.issues !== undefined)
2331
+ return this.wrapWebResponse(plainValidationError(validation.issues));
2332
+ ctx.query = validation.value;
2333
+ return runDerive();
2334
+ };
2335
+ }
2336
+ /**
2337
+ * Same shape as `buildFusedDeriveBefore`, plus one `afterHandle` step. The closure runs (validate) →
2338
+ * derive → before → handler → after in one frame, the shape the realistic middleware route lands on
2339
+ * when it also wants a response transform (e.g. attaching a request id to a successful payload's
2340
+ * headers). `LifecycleExecutionLane` and `selectRouteLanes` guarantee `afterHandle === 1`, the
2341
+ * throw contract is the same as the generic program, and the `after` step is the final transform +
2342
+ * respond the generic program already does.
2343
+ */
2344
+ buildFusedDeriveBeforeAfter(handler, derive, before, after, querySchema, maxBodyBytes) {
2345
+ const logError = (err, ctx) => this.bareError(err, ctx, (result, set) => this.wrapWebResponse(toResponse(result, set)), this.wrapWebResponse);
2346
+ return (source, params, search, signal, budget, platform, nativeContext) => {
2347
+ const ctx = nativeContext
2348
+ ? RequestContext.native(source, params, search, maxBodyBytes, platform, this.protoPoisoning)
2349
+ : new RequestContext(source, params, search, signal, budget, platform, maxBodyBytes, this.protoPoisoning);
2350
+ const runAfter = (result) => {
2351
+ let transformed;
2352
+ try {
2353
+ transformed = after(result, ctx);
2354
+ }
2355
+ catch (err) {
2356
+ return logError(err, ctx);
2357
+ }
2358
+ if (transformed instanceof Promise) {
2359
+ return transformed.then((value) => fusedRespond(value, ctx, this.responseBodyTag, this.staticResponseHeaders, this.onResponseHooks.length === 0), (err) => logError(err, ctx));
2360
+ }
2361
+ return fusedRespond(transformed, ctx, this.responseBodyTag, this.staticResponseHeaders, this.onResponseHooks.length === 0);
2362
+ };
2363
+ const runHandler = () => {
2364
+ let result;
2365
+ try {
2366
+ result = handler(ctx);
2367
+ }
2368
+ catch (err) {
2369
+ return logError(err, ctx);
2370
+ }
2371
+ if (result instanceof Promise) {
2372
+ return result.then((value) => runAfter(value), (err) => logError(err, ctx));
2373
+ }
2374
+ return runAfter(result);
2375
+ };
2376
+ const runBefore = () => {
2377
+ let early;
2378
+ try {
2379
+ early = before(ctx);
2380
+ }
2381
+ catch (err) {
2382
+ return logError(err, ctx);
2383
+ }
2384
+ if (early instanceof Promise) {
2385
+ return early.then((settled) => {
2386
+ if (settled === undefined)
2387
+ return runHandler();
2388
+ return fusedRespond(settled, ctx, this.responseBodyTag, this.staticResponseHeaders);
2389
+ }, (err) => logError(err, ctx));
2390
+ }
2391
+ if (early === undefined)
2392
+ return runHandler();
2393
+ return fusedRespond(early, ctx, this.responseBodyTag, this.staticResponseHeaders);
2394
+ };
2395
+ const runDerive = () => {
2396
+ let derived;
2397
+ try {
2398
+ derived = derive(ctx);
2399
+ }
2400
+ catch (err) {
2401
+ return logError(err, ctx);
2402
+ }
2403
+ if (derived instanceof Promise) {
2404
+ return derived.then((settled) => {
2405
+ if (isResponseResult(settled) || settled instanceof Response) {
2406
+ return fusedRespond(settled, ctx, this.responseBodyTag, this.staticResponseHeaders);
2407
+ }
2408
+ Object.assign(ctx, settled);
2409
+ return runBefore();
2410
+ }, (err) => logError(err, ctx));
2411
+ }
2412
+ if (isResponseResult(derived) || derived instanceof Response) {
2413
+ return fusedRespond(derived, ctx, this.responseBodyTag, this.staticResponseHeaders);
2414
+ }
2415
+ Object.assign(ctx, derived);
2416
+ return runBefore();
2417
+ };
2418
+ if (querySchema === undefined)
2419
+ return runDerive();
2420
+ let validation;
2421
+ try {
2422
+ validation = querySchema["~standard"].validate(queryObjectOf(ctx[CONTEXT_SEARCH]));
2423
+ }
2424
+ catch (err) {
2425
+ return logError(err, ctx);
2426
+ }
2427
+ if (validation instanceof Promise) {
2428
+ return validation.then((settled) => {
2429
+ if (settled.issues !== undefined)
2430
+ return this.wrapWebResponse(plainValidationError(settled.issues));
2431
+ ctx.query = settled.value;
2432
+ return runDerive();
2433
+ }, (err) => logError(err, ctx));
2434
+ }
2435
+ if (validation.issues !== undefined)
2436
+ return this.wrapWebResponse(plainValidationError(validation.issues));
2437
+ ctx.query = validation.value;
2438
+ return runDerive();
2439
+ };
2440
+ }
2441
+ /**
2442
+ * Body + lifecycle hooks fused: parse → validate → derive → before → handler → (after) in one
2443
+ * closure, the same win `buildFusedBodyRunner` gives for the no-hooks body shape. The shape is
2444
+ * `lifecycleHookLane` PLUS a body schema with no other validation conflicts; the lane is selected
2445
+ * ONLY when `schema.body !== undefined && schema.query/params/headers === undefined &&
2446
+ * schema.onValidationError === undefined && !defaultOnValidationError && !hasIdempotency &&
2447
+ * !hasLedger && derives === 1 && beforeHandle === 1 && afterHandle <= 1 && onError === 0 &&
2448
+ * !hasResponseContract && !hasDecorations && around === 0`. The same single `querySchema === undefined`
2449
+ * trick used in the no-body lane lets the build path stay simple: a query + body route falls back
2450
+ * to the generic program (this is rare in practice - validated query + validated body is usually a
2451
+ * "search results" route with a middleware chain on top, and the generic lane is fine there).
2452
+ */
2453
+ buildFusedBodyDeriveBeforeAfter(handler, derive, before, after, bodySchema, maxBodyBytes) {
2454
+ const logError = (err, ctx) => this.bareError(err, ctx, (result, set) => this.wrapWebResponse(toResponse(result, set)), this.wrapWebResponse);
2455
+ return (source, params, search, signal, budget, platform, nativeContext) => {
2456
+ const ctx = nativeContext
2457
+ ? RequestContext.native(source, params, search, maxBodyBytes, platform, this.protoPoisoning)
2458
+ : new RequestContext(source, params, search, signal, budget, platform, maxBodyBytes, this.protoPoisoning);
2459
+ const runHandlerValidated = () => {
2460
+ const runAfter = (result) => {
2461
+ if (after === undefined) {
2462
+ return fusedRespond(result, ctx, this.responseBodyTag, this.staticResponseHeaders, this.onResponseHooks.length === 0);
2463
+ }
2464
+ let transformed;
2465
+ try {
2466
+ transformed = after(result, ctx);
2467
+ }
2468
+ catch (err) {
2469
+ return logError(err, ctx);
2470
+ }
2471
+ if (transformed instanceof Promise) {
2472
+ return transformed.then((value) => fusedRespond(value, ctx, this.responseBodyTag, this.staticResponseHeaders, this.onResponseHooks.length === 0), (err) => logError(err, ctx));
2473
+ }
2474
+ return fusedRespond(transformed, ctx, this.responseBodyTag, this.staticResponseHeaders, this.onResponseHooks.length === 0);
2475
+ };
2476
+ const runHandler = () => {
2477
+ let result;
2478
+ try {
2479
+ result = handler(ctx);
2480
+ }
2481
+ catch (err) {
2482
+ return logError(err, ctx);
2483
+ }
2484
+ if (result instanceof Promise) {
2485
+ return result.then((value) => runAfter(value), (err) => logError(err, ctx));
2486
+ }
2487
+ return runAfter(result);
2488
+ };
2489
+ const runBefore = () => {
2490
+ let early;
2491
+ try {
2492
+ early = before(ctx);
2493
+ }
2494
+ catch (err) {
2495
+ return logError(err, ctx);
2496
+ }
2497
+ if (early instanceof Promise) {
2498
+ return early.then((settled) => {
2499
+ if (settled === undefined)
2500
+ return runHandler();
2501
+ return fusedRespond(settled, ctx, this.responseBodyTag, this.staticResponseHeaders);
2502
+ }, (err) => logError(err, ctx));
2503
+ }
2504
+ if (early === undefined)
2505
+ return runHandler();
2506
+ return fusedRespond(early, ctx, this.responseBodyTag, this.staticResponseHeaders);
2507
+ };
2508
+ let derived;
2509
+ try {
2510
+ derived = derive(ctx);
2511
+ }
2512
+ catch (err) {
2513
+ return logError(err, ctx);
2514
+ }
2515
+ if (derived instanceof Promise) {
2516
+ return derived.then((settled) => {
2517
+ if (isResponseResult(settled) || settled instanceof Response) {
2518
+ return fusedRespond(settled, ctx, this.responseBodyTag, this.staticResponseHeaders);
2519
+ }
2520
+ Object.assign(ctx, settled);
2521
+ return runBefore();
2522
+ }, (err) => logError(err, ctx));
2523
+ }
2524
+ if (isResponseResult(derived) || derived instanceof Response) {
2525
+ return fusedRespond(derived, ctx, this.responseBodyTag, this.staticResponseHeaders);
2526
+ }
2527
+ Object.assign(ctx, derived);
2528
+ return runBefore();
2529
+ };
2530
+ const onParsed = (parsed) => {
2531
+ let validation;
2532
+ try {
2533
+ validation = bodySchema["~standard"].validate(parsed);
2534
+ }
2535
+ catch (err) {
2536
+ return logError(err, ctx);
2537
+ }
2538
+ if (validation instanceof Promise) {
2539
+ return validation.then((settled) => {
2540
+ if (settled.issues !== undefined)
2541
+ return this.wrapWebResponse(plainValidationError(settled.issues));
2542
+ ctx.body = settled.value;
2543
+ return runHandlerValidated();
2544
+ }, (err) => logError(err, ctx));
2545
+ }
2546
+ if (validation.issues !== undefined)
2547
+ return this.wrapWebResponse(plainValidationError(validation.issues));
2548
+ ctx.body = validation.value;
2549
+ return runHandlerValidated();
2550
+ };
2551
+ return readBodyFramed(source, maxBodyBytes, this.protoPoisoning, onParsed, (response) => this.wrapWebResponse(response), (err) => Promise.resolve(logError(err, ctx)));
2552
+ };
2553
+ }
2386
2554
  bareError(err, ctx, finalize, wrapResponse) {
2387
2555
  // The thrown-value contract lives in `bare-error-lane.ts`, one source of truth for both this
2388
2556
  // lane and the fused body runner. `responseSet` and the logger are injected because they reach
2389
2557
  // into class state; the closure is allocated only on the throw path, never on the hot lane.
2390
- return renderBareError(err, ctx, finalize, wrapResponse, responseSet, (e, c) => this.logRequestError(e, c));
2558
+ return renderBareError(err, ctx, finalize, wrapResponse, responseSet, (e, c) => this.logRequestError(e, c), () => this.internalErrorResponse(wrapResponse));
2559
+ }
2560
+ /**
2561
+ * Keep the ordinary thrown-error response independent from the caught value. Deliberate Response
2562
+ * control flow is handled by `renderBareError`; every other throw gets this fixed public envelope.
2563
+ */
2564
+ internalErrorResponse(wrapResponse) {
2565
+ return wrapResponse(plainError(500, "internal_error"));
2391
2566
  }
2392
2567
  // @ts-expect-error TS6133 -- invoked structurally by compiled plans (internal/route-execution.ts)
2393
- // biome-ignore lint/correctness/noUnusedPrivateClassMembers: invoked structurally by compiled plans (internal/route-execution.ts)
2394
2568
  runWithAround(entry, ctx, run, finalize, wrapResponse) {
2395
2569
  let outcome;
2396
2570
  try {
@@ -2403,6 +2577,45 @@ export class Server {
2403
2577
  ? outcome.catch((err) => this.handleLifecycleError(entry, err, ctx, finalize, wrapResponse))
2404
2578
  : outcome;
2405
2579
  }
2580
+ /** Execute the registration-compiled general stage program. */
2581
+ // @ts-expect-error TS6133 -- invoked structurally by compiled plans (internal/route-execution.ts)
2582
+ runProgram(entry, source, ctx, finalize, wrapResponse) {
2583
+ return executeRouteProgram(this, entry, entry.program, source, ctx, finalize, wrapResponse);
2584
+ }
2585
+ /** Bind a compiled validation stage to its stable schema and the existing recovery contract. */
2586
+ // @ts-expect-error TS6133 -- invoked structurally by the general route program
2587
+ validateProgramStage(entry, stage, source, ctx) {
2588
+ const input = stage.kind === "headers"
2589
+ ? headerObjectOf(source.headers)
2590
+ : stage.kind === "params"
2591
+ ? ctx.params
2592
+ : stage.kind === "query"
2593
+ ? queryObjectOf(ctx[CONTEXT_SEARCH])
2594
+ : undefined;
2595
+ if (stage.kind === "body")
2596
+ return this.readProgramBody(entry, source, ctx);
2597
+ const validation = stage.schema["~standard"].validate(input);
2598
+ return validation instanceof Promise
2599
+ ? validation.then((result) => this.applyLifecycleValidation(entry, result, ctx, stage.kind))
2600
+ : this.applyLifecycleValidation(entry, validation, ctx, stage.kind);
2601
+ }
2602
+ /** Preserve the existing bounded body reader and its fail-closed framing/prototype checks. */
2603
+ readProgramBody(entry, source, ctx) {
2604
+ const bodySchema = entry.program.bodySchema;
2605
+ if (bodySchema === undefined)
2606
+ return Promise.resolve(plainError(415, "unsupported_media_type"));
2607
+ return this.readAndValidateBody(source, entry, ctx, bodySchema, entry.program.bodyLimit);
2608
+ }
2609
+ /** Keep thrown Response/status and redacted 500 semantics in one existing error boundary. */
2610
+ // @ts-expect-error TS6133 -- invoked structurally by the general route program
2611
+ handleProgramError(entry, error, ctx, finalize, wrapResponse) {
2612
+ return this.handleLifecycleError(entry, error, ctx, finalize, wrapResponse);
2613
+ }
2614
+ /** Finish a successful program result through response-contract enforcement and finalization. */
2615
+ // @ts-expect-error TS6133 -- invoked structurally by the general route program
2616
+ finishProgramResult(entry, ctx, result, finalize, wrapResponse) {
2617
+ return this.finishLifecycleContract(entry, ctx, finalize, wrapResponse, result);
2618
+ }
2406
2619
  runAround(hooks, ctx, run) {
2407
2620
  const dispatch = (index) => {
2408
2621
  if (index >= hooks.length)
@@ -2419,7 +2632,6 @@ export class Server {
2419
2632
  return dispatch(0);
2420
2633
  }
2421
2634
  // @ts-expect-error TS6133 -- invoked structurally by compiled plans (internal/route-execution.ts)
2422
- // biome-ignore lint/correctness/noUnusedPrivateClassMembers: invoked structurally by compiled plans (internal/route-execution.ts)
2423
2635
  runBodyOnly(entry, source, ctx, finalize, wrapResponse) {
2424
2636
  return readBodyFramed(source, entry.bodyLimit ?? UNLIMITED_BODY_BYTES, this.protoPoisoning, (parsed) => this.finishBodyOnly(entry, parsed, ctx, finalize, wrapResponse), wrapResponse, (err) => this.handleLifecycleError(entry, err, ctx, finalize, wrapResponse));
2425
2637
  }
@@ -2520,7 +2732,6 @@ export class Server {
2520
2732
  return this.executeHandler(entry, ctx, finalize);
2521
2733
  }
2522
2734
  // @ts-expect-error TS6133 -- invoked structurally by compiled plans (internal/route-execution.ts)
2523
- // biome-ignore lint/correctness/noUnusedPrivateClassMembers: invoked structurally by compiled plans (internal/route-execution.ts)
2524
2735
  runQueryOnly(entry, ctx, finalize, wrapResponse) {
2525
2736
  try {
2526
2737
  // Call the validator directly for the raw StandardResult (read `.issues`/`.value`) - skip
@@ -2665,343 +2876,6 @@ export class Server {
2665
2876
  clearTimeout(timer);
2666
2877
  }
2667
2878
  }
2668
- /**
2669
- * The post-match lifecycle: validate → derive → beforeHandle → handler → afterHandle, with onError.
2670
- * Generic over the render: a success value goes through `finalize(result, set)` (a Web `Response`, or
2671
- * node-direct primitives); an early/error `Response` (validation, thrown, 500) through `wrapResponse`.
2672
- */
2673
- /** Synchronous until a validator, lifecycle hook, handler, or contract check actually returns a Promise. */
2674
- // @ts-expect-error TS6133 -- invoked structurally by compiled plans (internal/route-execution.ts)
2675
- // biome-ignore lint/correctness/noUnusedPrivateClassMembers: invoked structurally by compiled plans (internal/route-execution.ts)
2676
- runLifecycle(entry, source, ctx, finalize, wrapResponse) {
2677
- try {
2678
- const headersSchema = entry.schema?.headers;
2679
- if (headersSchema !== undefined) {
2680
- const validation = headersSchema["~standard"].validate(headerObjectOf(source.headers));
2681
- if (validation instanceof Promise) {
2682
- return validation.then((result) => this.runLifecycleAfterHeadersResult(entry, source, ctx, finalize, wrapResponse, result), (err) => this.handleLifecycleError(entry, err, ctx, finalize, wrapResponse));
2683
- }
2684
- return this.runLifecycleAfterHeadersResult(entry, source, ctx, finalize, wrapResponse, validation);
2685
- }
2686
- const paramsSchema = entry.schema?.params;
2687
- if (paramsSchema !== undefined) {
2688
- const validation = paramsSchema["~standard"].validate(ctx.params);
2689
- if (validation instanceof Promise) {
2690
- return validation.then((result) => this.runLifecycleAfterParamsResult(entry, source, ctx, finalize, wrapResponse, result), (err) => this.handleLifecycleError(entry, err, ctx, finalize, wrapResponse));
2691
- }
2692
- return this.runLifecycleAfterParamsResult(entry, source, ctx, finalize, wrapResponse, validation);
2693
- }
2694
- // Registration already knows whether this route has body/params/query validation. Most real
2695
- // read routes have no body or params schema, so skip the generic stage ladder and go straight to
2696
- // query validation + lifecycle hooks. The fallback below remains the complete path for routes
2697
- // with body/params combinations and validation recovery.
2698
- if (entry.schema?.body === undefined) {
2699
- return this.runQueryAndLifecycle(entry, ctx, finalize, wrapResponse);
2700
- }
2701
- return this.runLifecycleAfterParams(entry, source, ctx, finalize, wrapResponse, undefined);
2702
- }
2703
- catch (err) {
2704
- return this.handleLifecycleError(entry, err, ctx, finalize, wrapResponse);
2705
- }
2706
- }
2707
- runLifecycleAfterHeadersResult(entry, source, ctx, finalize, wrapResponse, result) {
2708
- try {
2709
- const headersError = this.applyLifecycleValidation(entry, result, ctx, "headers");
2710
- if (headersError instanceof Promise) {
2711
- return headersError.then((error) => error === undefined
2712
- ? this.runLifecycleAfterHeaders(entry, source, ctx, finalize, wrapResponse)
2713
- : wrapResponse(error), (err) => this.handleLifecycleError(entry, err, ctx, finalize, wrapResponse));
2714
- }
2715
- return headersError === undefined
2716
- ? this.runLifecycleAfterHeaders(entry, source, ctx, finalize, wrapResponse)
2717
- : wrapResponse(headersError);
2718
- }
2719
- catch (err) {
2720
- return this.handleLifecycleError(entry, err, ctx, finalize, wrapResponse);
2721
- }
2722
- }
2723
- runLifecycleAfterHeaders(entry, source, ctx, finalize, wrapResponse) {
2724
- const paramsSchema = entry.schema?.params;
2725
- if (paramsSchema === undefined) {
2726
- if (entry.schema?.body === undefined)
2727
- return this.runQueryAndLifecycle(entry, ctx, finalize, wrapResponse);
2728
- return this.runLifecycleAfterParams(entry, source, ctx, finalize, wrapResponse, undefined);
2729
- }
2730
- const validation = paramsSchema["~standard"].validate(ctx.params);
2731
- if (validation instanceof Promise) {
2732
- return validation.then((result) => this.runLifecycleAfterParamsResult(entry, source, ctx, finalize, wrapResponse, result), (err) => this.handleLifecycleError(entry, err, ctx, finalize, wrapResponse));
2733
- }
2734
- return this.runLifecycleAfterParamsResult(entry, source, ctx, finalize, wrapResponse, validation);
2735
- }
2736
- /** Registration-specialized query lifecycle: query validation (including recovery) → hooks. */
2737
- runLifecycleQuery(entry, ctx, finalize, wrapResponse) {
2738
- try {
2739
- // The execution plan only selects this runner when the route has a query schema. Keeping the
2740
- // non-null access here removes the per-request schema-presence branch from realistic GETs.
2741
- const validation = entry.schema.query["~standard"].validate(queryObjectOf(ctx[CONTEXT_SEARCH]));
2742
- if (validation instanceof Promise) {
2743
- return validation.then((result) => {
2744
- try {
2745
- return this.runQueryAndLifecycleResult(entry, ctx, finalize, wrapResponse, result);
2746
- }
2747
- catch (err) {
2748
- return this.handleLifecycleError(entry, err, ctx, finalize, wrapResponse);
2749
- }
2750
- }, (err) => this.handleLifecycleError(entry, err, ctx, finalize, wrapResponse));
2751
- }
2752
- return this.runQueryAndLifecycleResult(entry, ctx, finalize, wrapResponse, validation);
2753
- }
2754
- catch (err) {
2755
- return this.handleLifecycleError(entry, err, ctx, finalize, wrapResponse);
2756
- }
2757
- }
2758
- /** Registration-specialized body lifecycle: bounded body validation (including recovery) → hooks. */
2759
- // @ts-expect-error TS6133 -- invoked structurally by compiled plans (internal/route-execution.ts)
2760
- // biome-ignore lint/correctness/noUnusedPrivateClassMembers: invoked structurally by compiled plans (internal/route-execution.ts)
2761
- runLifecycleBody(entry, source, ctx, finalize, wrapResponse) {
2762
- return this.readAndValidateBody(source, entry, ctx).then((bodyError) => bodyError === undefined
2763
- ? this.runLifecycleHooks(entry, ctx, finalize, wrapResponse)
2764
- : wrapResponse(bodyError), (err) => this.handleLifecycleError(entry, err, ctx, finalize, wrapResponse));
2765
- }
2766
- /** Registration-specialized body + query lifecycle: bounded body validation → query validation → hooks. */
2767
- // @ts-expect-error TS6133 -- invoked structurally by compiled plans (internal/route-execution.ts)
2768
- // biome-ignore lint/correctness/noUnusedPrivateClassMembers: invoked structurally by compiled plans (internal/route-execution.ts)
2769
- runLifecycleBodyQuery(entry, source, ctx, finalize, wrapResponse) {
2770
- return this.readAndValidateBody(source, entry, ctx).then((bodyError) => bodyError === undefined
2771
- ? this.runLifecycleQuery(entry, ctx, finalize, wrapResponse)
2772
- : wrapResponse(bodyError), (err) => this.handleLifecycleError(entry, err, ctx, finalize, wrapResponse));
2773
- }
2774
- /** Registration-specialized no-body lifecycle: query validation (if present) → derives/hooks. */
2775
- runQueryAndLifecycle(entry, ctx, finalize, wrapResponse) {
2776
- const querySchema = entry.schema?.query;
2777
- return querySchema === undefined
2778
- ? this.runLifecycleHooks(entry, ctx, finalize, wrapResponse)
2779
- : this.runLifecycleQuery(entry, ctx, finalize, wrapResponse);
2780
- }
2781
- runQueryAndLifecycleResult(entry, ctx, finalize, wrapResponse, result) {
2782
- const queryError = this.applyLifecycleValidation(entry, result, ctx, "query");
2783
- if (queryError instanceof Promise) {
2784
- return queryError.then((error) => error === undefined
2785
- ? this.runLifecycleHooks(entry, ctx, finalize, wrapResponse)
2786
- : wrapResponse(error), (err) => this.handleLifecycleError(entry, err, ctx, finalize, wrapResponse));
2787
- }
2788
- if (queryError !== undefined)
2789
- return wrapResponse(queryError);
2790
- return this.runLifecycleHooks(entry, ctx, finalize, wrapResponse);
2791
- }
2792
- runLifecycleAfterParamsResult(entry, source, ctx, finalize, wrapResponse, result) {
2793
- try {
2794
- return this.runLifecycleAfterParams(entry, source, ctx, finalize, wrapResponse, this.applyLifecycleValidation(entry, result, ctx, "params"));
2795
- }
2796
- catch (err) {
2797
- return this.handleLifecycleError(entry, err, ctx, finalize, wrapResponse);
2798
- }
2799
- }
2800
- runLifecycleAfterParams(entry, source, ctx, finalize, wrapResponse, paramsError) {
2801
- if (paramsError instanceof Promise) {
2802
- return paramsError.then((error) => this.runLifecycleAfterParamsSettled(entry, source, ctx, finalize, wrapResponse, error), (err) => this.handleLifecycleError(entry, err, ctx, finalize, wrapResponse));
2803
- }
2804
- return this.runLifecycleAfterParamsSettled(entry, source, ctx, finalize, wrapResponse, paramsError);
2805
- }
2806
- runLifecycleAfterParamsSettled(entry, source, ctx, finalize, wrapResponse, paramsError) {
2807
- if (paramsError !== undefined)
2808
- return wrapResponse(paramsError);
2809
- if (entry.schema?.body !== undefined) {
2810
- return this.readAndValidateBody(source, entry, ctx).then((bodyError) => this.runLifecycleAfterBody(entry, ctx, finalize, wrapResponse, bodyError), (err) => this.handleLifecycleError(entry, err, ctx, finalize, wrapResponse));
2811
- }
2812
- return this.runLifecycleAfterBody(entry, ctx, finalize, wrapResponse, undefined);
2813
- }
2814
- runLifecycleAfterBody(entry, ctx, finalize, wrapResponse, bodyError) {
2815
- if (bodyError !== undefined)
2816
- return wrapResponse(bodyError);
2817
- const querySchema = entry.schema?.query;
2818
- if (querySchema === undefined)
2819
- return this.runLifecycleHooks(entry, ctx, finalize, wrapResponse);
2820
- const validation = querySchema["~standard"].validate(queryObjectOf(ctx[CONTEXT_SEARCH]));
2821
- if (validation instanceof Promise) {
2822
- return validation.then((result) => this.runLifecycleAfterQueryResult(entry, ctx, finalize, wrapResponse, result), (err) => this.handleLifecycleError(entry, err, ctx, finalize, wrapResponse));
2823
- }
2824
- return this.runLifecycleAfterQueryResult(entry, ctx, finalize, wrapResponse, validation);
2825
- }
2826
- runLifecycleAfterQueryResult(entry, ctx, finalize, wrapResponse, result) {
2827
- try {
2828
- const queryError = this.applyLifecycleValidation(entry, result, ctx, "query");
2829
- if (queryError instanceof Promise) {
2830
- return queryError.then((error) => this.runLifecycleAfterQuery(entry, ctx, finalize, wrapResponse, error), (err) => this.handleLifecycleError(entry, err, ctx, finalize, wrapResponse));
2831
- }
2832
- return this.runLifecycleAfterQuery(entry, ctx, finalize, wrapResponse, queryError);
2833
- }
2834
- catch (err) {
2835
- return this.handleLifecycleError(entry, err, ctx, finalize, wrapResponse);
2836
- }
2837
- }
2838
- runLifecycleAfterQuery(entry, ctx, finalize, wrapResponse, queryError) {
2839
- if (queryError !== undefined)
2840
- return wrapResponse(queryError);
2841
- return this.runLifecycleHooks(entry, ctx, finalize, wrapResponse);
2842
- }
2843
- runLifecycleHooks(entry, ctx, finalize, wrapResponse) {
2844
- if (entry.lifecycleHookLane === "derive-before") {
2845
- return this.runLifecycleDeriveBefore(entry, ctx, finalize, wrapResponse);
2846
- }
2847
- try {
2848
- if (entry.hasDecorations)
2849
- Object.assign(ctx, entry.decorations);
2850
- for (let i = 0; i < entry.derives.length; i++) {
2851
- const extension = entry.derives[i](ctx);
2852
- if (extension instanceof Promise) {
2853
- return this.continueLifecycleAfterDerive(entry, ctx, finalize, wrapResponse, i, extension);
2854
- }
2855
- if (isDeriveEarlyExit(extension))
2856
- return finalize(extension, responseSet(ctx));
2857
- Object.assign(ctx, extension);
2858
- }
2859
- for (let i = 0; i < entry.beforeHandle.length; i++) {
2860
- const outcome = entry.beforeHandle[i](ctx);
2861
- if (outcome instanceof Promise) {
2862
- return this.continueLifecycleAfterBefore(entry, ctx, finalize, wrapResponse, i, outcome);
2863
- }
2864
- if (outcome !== undefined)
2865
- return finalize(outcome, responseSet(ctx));
2866
- }
2867
- const result = entry.handler(ctx);
2868
- if (result instanceof Promise) {
2869
- return result.then((value) => this.finishLifecycleResult(entry, ctx, finalize, wrapResponse, value), (err) => this.handleLifecycleError(entry, err, ctx, finalize, wrapResponse));
2870
- }
2871
- return this.finishLifecycleResult(entry, ctx, finalize, wrapResponse, result);
2872
- }
2873
- catch (err) {
2874
- return this.handleLifecycleError(entry, err, ctx, finalize, wrapResponse);
2875
- }
2876
- }
2877
- /** Registration-specialized derive → before → handler lifecycle. */
2878
- runLifecycleDeriveBefore(entry, ctx, finalize, wrapResponse) {
2879
- try {
2880
- const extension = entry.derives[0](ctx);
2881
- if (extension instanceof Promise) {
2882
- return this.continueLifecycleAfterDerive(entry, ctx, finalize, wrapResponse, 0, extension);
2883
- }
2884
- if (isDeriveEarlyExit(extension))
2885
- return finalize(extension, responseSet(ctx));
2886
- Object.assign(ctx, extension);
2887
- const outcome = entry.beforeHandle[0](ctx);
2888
- if (outcome instanceof Promise) {
2889
- return this.continueLifecycleAfterBefore(entry, ctx, finalize, wrapResponse, 0, outcome);
2890
- }
2891
- if (outcome !== undefined)
2892
- return this.finishSimpleLifecycleResult(entry, ctx, finalize, wrapResponse, outcome);
2893
- const result = entry.handler(ctx);
2894
- if (result instanceof Promise) {
2895
- return result.then((value) => this.finishSimpleLifecycleResult(entry, ctx, finalize, wrapResponse, value), (err) => this.handleLifecycleError(entry, err, ctx, finalize, wrapResponse));
2896
- }
2897
- return this.finishSimpleLifecycleResult(entry, ctx, finalize, wrapResponse, result);
2898
- }
2899
- catch (err) {
2900
- return this.handleLifecycleError(entry, err, ctx, finalize, wrapResponse);
2901
- }
2902
- }
2903
- /** Finalize the specialized lane without rechecking its registration-proven invariants. */
2904
- finishSimpleLifecycleResult(entry, ctx, finalize, wrapResponse, result) {
2905
- try {
2906
- return finalize(result, responseSet(ctx));
2907
- }
2908
- catch (err) {
2909
- return this.handleLifecycleError(entry, err, ctx, finalize, wrapResponse);
2910
- }
2911
- }
2912
- async continueLifecycleAfterDerive(entry, ctx, finalize, wrapResponse, firstIndex, first) {
2913
- try {
2914
- const settled = await first;
2915
- if (isDeriveEarlyExit(settled))
2916
- return finalize(settled, responseSet(ctx));
2917
- Object.assign(ctx, settled);
2918
- for (let i = firstIndex + 1; i < entry.derives.length; i++) {
2919
- const raw = entry.derives[i](ctx);
2920
- const extension = raw instanceof Promise ? await raw : raw;
2921
- if (isDeriveEarlyExit(extension))
2922
- return finalize(extension, responseSet(ctx));
2923
- Object.assign(ctx, extension);
2924
- }
2925
- return await this.runLifecycleHooksAsync(entry, ctx, finalize, wrapResponse);
2926
- }
2927
- catch (err) {
2928
- return this.handleLifecycleError(entry, err, ctx, finalize, wrapResponse);
2929
- }
2930
- }
2931
- async runLifecycleHooksAsync(entry, ctx, finalize, wrapResponse) {
2932
- try {
2933
- // Indexed, not `for...of`. An array iterator declared inside an async function stays live
2934
- // across the `await` below, so neither engine can sink it - unlike the sync lanes above,
2935
- // where V8 elides it. Measured cost of the iterator form: 43ns per hook on Bun, ~9ns on
2936
- // Node, on a chain whose hooks return synchronously (the common shape).
2937
- for (let i = 0; i < entry.beforeHandle.length; i++) {
2938
- const outcome = entry.beforeHandle[i](ctx);
2939
- const early = outcome instanceof Promise ? await outcome : outcome;
2940
- if (early !== undefined)
2941
- return finalize(early, responseSet(ctx));
2942
- }
2943
- return await this.runLifecycleHandlerAsync(entry, ctx, finalize, wrapResponse);
2944
- }
2945
- catch (err) {
2946
- return this.handleLifecycleError(entry, err, ctx, finalize, wrapResponse);
2947
- }
2948
- }
2949
- async continueLifecycleAfterBefore(entry, ctx, finalize, wrapResponse, firstIndex, first) {
2950
- try {
2951
- const firstOutcome = await first;
2952
- if (firstOutcome !== undefined)
2953
- return finalize(firstOutcome, responseSet(ctx));
2954
- for (let i = firstIndex + 1; i < entry.beforeHandle.length; i++) {
2955
- const outcome = entry.beforeHandle[i](ctx);
2956
- const early = outcome instanceof Promise ? await outcome : outcome;
2957
- if (early !== undefined)
2958
- return finalize(early, responseSet(ctx));
2959
- }
2960
- return await this.runLifecycleHandlerAsync(entry, ctx, finalize, wrapResponse);
2961
- }
2962
- catch (err) {
2963
- return this.handleLifecycleError(entry, err, ctx, finalize, wrapResponse);
2964
- }
2965
- }
2966
- async runLifecycleHandlerAsync(entry, ctx, finalize, wrapResponse) {
2967
- try {
2968
- const output = entry.handler(ctx);
2969
- const result = output instanceof Promise ? await output : output;
2970
- return await this.finishLifecycleResult(entry, ctx, finalize, wrapResponse, result);
2971
- }
2972
- catch (err) {
2973
- return this.handleLifecycleError(entry, err, ctx, finalize, wrapResponse);
2974
- }
2975
- }
2976
- finishLifecycleResult(entry, ctx, finalize, wrapResponse, initial) {
2977
- try {
2978
- let result = initial;
2979
- for (let i = 0; i < entry.afterHandle.length; i++) {
2980
- const transformed = entry.afterHandle[i](result, ctx);
2981
- if (transformed instanceof Promise) {
2982
- return this.continueLifecycleAfterHandle(entry, ctx, finalize, wrapResponse, i, transformed);
2983
- }
2984
- result = transformed;
2985
- }
2986
- return this.finishLifecycleContract(entry, ctx, finalize, wrapResponse, result);
2987
- }
2988
- catch (err) {
2989
- return this.handleLifecycleError(entry, err, ctx, finalize, wrapResponse);
2990
- }
2991
- }
2992
- async continueLifecycleAfterHandle(entry, ctx, finalize, wrapResponse, firstIndex, first) {
2993
- try {
2994
- let result = await first;
2995
- for (let i = firstIndex + 1; i < entry.afterHandle.length; i++) {
2996
- const transformed = entry.afterHandle[i](result, ctx);
2997
- result = transformed instanceof Promise ? await transformed : transformed;
2998
- }
2999
- return await this.finishLifecycleContract(entry, ctx, finalize, wrapResponse, result);
3000
- }
3001
- catch (err) {
3002
- return this.handleLifecycleError(entry, err, ctx, finalize, wrapResponse);
3003
- }
3004
- }
3005
2879
  finishLifecycleContract(entry, ctx, finalize, wrapResponse, result) {
3006
2880
  try {
3007
2881
  const contract = entry.responseContract;
@@ -3073,34 +2947,23 @@ export class Server {
3073
2947
  this.logRequestError(err, ctx);
3074
2948
  return wrapResponse(plainError(500, "internal_error"));
3075
2949
  }
3076
- /** Log an unhandled request error to the (redacting) logger - shared by {@link runLifecycle} and the
2950
+ /** Log an unhandled request error to the (redacting) logger - shared by the compiled route program and the
3077
2951
  * bare fast path ({@link bareError}) so both record the same fields. Never throws; never leaks. */
3078
2952
  logRequestError(err, ctx) {
3079
2953
  emitRequestErrorLog(this.logger, this.errorLogDetail, err, ctx);
3080
2954
  }
3081
- async readAndValidateBody(req, entry, ctx) {
3082
- const contentType = headerOf(req, "content-type") ?? "";
3083
- let parsed;
3084
- if (contentType === "application/json" || contentType.includes("application/json")) {
3085
- const json = await this.readBoundedJson(req, entry.bodyLimit ?? UNLIMITED_BODY_BYTES);
3086
- if (isResponseResult(json))
3087
- return json;
3088
- parsed = json;
3089
- }
3090
- else if (isUrlEncodedForm(contentType)) {
3091
- const form = await readBoundedForm(req, entry.bodyLimit ?? UNLIMITED_BODY_BYTES);
3092
- if (isResponseResult(form))
3093
- return form;
3094
- parsed = form;
3095
- }
3096
- else {
3097
- // multipart/form-data (file uploads) stays 415 on the schema path by design - files don't
3098
- // fit a value schema; use a schema-less route + @nifrajs/uploads helpers for those.
3099
- return plainError(415, "unsupported_media_type");
3100
- }
3101
- const validation = entry.schema.body["~standard"].validate(parsed);
3102
- const result = validation instanceof Promise ? await validation : validation;
3103
- return this.applyLifecycleValidation(entry, result, ctx, "body");
2955
+ readAndValidateBody(req, entry, ctx, bodySchema = entry.schema?.body, bodyLimit = entry.bodyLimit) {
2956
+ // Keep the generic lifecycle body stage on the same framed parser/continuation as the body-only
2957
+ // lane. This avoids an async wrapper that first awaits parsing and then starts validation in a
2958
+ // second continuation, while retaining the exact bounded-read, content-type, and poisoning
2959
+ // contracts shared by every body route.
2960
+ const validate = (parsed) => {
2961
+ const validation = bodySchema["~standard"].validate(parsed);
2962
+ return validation instanceof Promise
2963
+ ? validation.then((result) => this.applyLifecycleValidation(entry, result, ctx, "body"))
2964
+ : this.applyLifecycleValidation(entry, validation, ctx, "body");
2965
+ };
2966
+ return readBodyFramed(req, bodyLimit ?? UNLIMITED_BODY_BYTES, this.protoPoisoning, validate, (response) => response, (error) => Promise.reject(error));
3104
2967
  }
3105
2968
  /** Apply validation and its recovery hook on the generic lifecycle lane. Recovery is completed
3106
2969
  * before derives/beforeHandle run, matching the body-only and query-only execution lanes. */
@@ -3154,13 +3017,6 @@ export class Server {
3154
3017
  assign(retried.value);
3155
3018
  return undefined;
3156
3019
  }
3157
- readBoundedJson(req, maxBodyBytes = this.maxBodyBytes, onResult, onError) {
3158
- // Not `async`: this is a pass-through, and wrapping the lane's own promise in a coroutine
3159
- // frame costs an extra microtask hop on every JSON body.
3160
- return onResult === undefined
3161
- ? readBoundedJsonSource(req, maxBodyBytes, this.protoPoisoning)
3162
- : readBoundedJsonSource(req, maxBodyBytes, this.protoPoisoning, onResult, onError);
3163
- }
3164
3020
  /** Adapt one route's compiled execution plan to Bun's already-matched request shape. Route
3165
3021
  * semantics remain in the plan; this closure only supplies native params and deadline fallback. */
3166
3022
  compileBunNativeHandler(entry, paramNames, fused, signal, budget) {
@@ -3324,9 +3180,12 @@ export class Server {
3324
3180
  // matched route's handler, set by `server.upgrade`.
3325
3181
  // With WS routes, the dispatcher comes from the installed `.use(websocket())` runtime - non-null
3326
3182
  // because wsRouteCount > 0 means ws() ran, and ws() requires the runtime at registration.
3183
+ // Native pub/sub when the app has WS routes and none validate outbound frames: `ws.subscribe` and
3184
+ // `app.publish` go through Bun's own (uWebSockets) broadcast instead of the JS registry loop.
3185
+ const nativePubsub = this.wsRouteCount > 0 && !this.wsHasValidatedSend;
3327
3186
  const wsHandlers = this.wsRouteCount === 0
3328
3187
  ? undefined
3329
- : this.wsRuntime.bunHandlers(this.topics);
3188
+ : this.wsRuntime.bunHandlers(this.topics, nativePubsub);
3330
3189
  const reusePort = options?.reusePort === true;
3331
3190
  // Spread rather than pass `hostname: undefined` - Bun treats an explicit undefined as a value
3332
3191
  // on some option paths, and omitting is what selects its 0.0.0.0 default.
@@ -3362,6 +3221,14 @@ export class Server {
3362
3221
  },
3363
3222
  }));
3364
3223
  this.bunServer = running;
3224
+ // Bind `app.publish` to Bun's native broadcast now that the server handle exists. Guarded on the
3225
+ // method's presence so a runtime whose handle lacks it simply keeps the registry path.
3226
+ if (nativePubsub && typeof running.publish === "function") {
3227
+ const native = running.publish.bind(running);
3228
+ this.nativePublish = (topic, data) => {
3229
+ native(topic, data);
3230
+ };
3231
+ }
3365
3232
  this.sealed = true;
3366
3233
  if (this.gracefulSignals)
3367
3234
  this.installSignalHandlers();