@nifrajs/core 2.14.1 → 3.2.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.
- package/README.md +25 -0
- package/dist/assurance.d.ts +16 -8
- package/dist/assurance.d.ts.map +1 -1
- package/dist/assurance.js +33 -0
- package/dist/assurance.js.map +1 -1
- package/dist/durable-adapters.d.ts.map +1 -1
- package/dist/durable-adapters.js +26 -3
- package/dist/durable-adapters.js.map +1 -1
- package/dist/durable-approval.d.ts +56 -0
- package/dist/durable-approval.d.ts.map +1 -0
- package/dist/durable-approval.js +233 -0
- package/dist/durable-approval.js.map +1 -0
- package/dist/durable-effect.d.ts +42 -0
- package/dist/durable-effect.d.ts.map +1 -0
- package/dist/durable-effect.js +234 -0
- package/dist/durable-effect.js.map +1 -0
- package/dist/durable-execution.d.ts +8 -346
- package/dist/durable-execution.d.ts.map +1 -1
- package/dist/durable-execution.js +7 -1150
- package/dist/durable-execution.js.map +1 -1
- package/dist/durable-saga.d.ts +94 -0
- package/dist/durable-saga.d.ts.map +1 -0
- package/dist/durable-saga.js +550 -0
- package/dist/durable-saga.js.map +1 -0
- package/dist/edge-kit.d.ts +27 -0
- package/dist/edge-kit.d.ts.map +1 -0
- package/dist/edge-kit.js +24 -0
- package/dist/edge-kit.js.map +1 -0
- package/dist/errors.d.ts +1 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +23 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +18 -2
- package/dist/index.js.map +1 -1
- package/dist/internal/durable-approval-protocol.d.ts +1 -1
- package/dist/internal/durable-approval-protocol.d.ts.map +1 -1
- package/dist/internal/durable-effect-protocol.d.ts +1 -1
- package/dist/internal/durable-effect-protocol.d.ts.map +1 -1
- package/dist/internal/durable-saga-protocol.d.ts +1 -1
- package/dist/internal/durable-saga-protocol.d.ts.map +1 -1
- package/dist/internal/durable-shared.d.ts +26 -0
- package/dist/internal/durable-shared.d.ts.map +1 -0
- package/dist/internal/durable-shared.js +131 -0
- package/dist/internal/durable-shared.js.map +1 -0
- package/dist/internal/durable-types.d.ts +159 -0
- package/dist/internal/durable-types.d.ts.map +1 -0
- package/dist/internal/durable-types.js +2 -0
- package/dist/internal/durable-types.js.map +1 -0
- package/dist/internal/route-compiler.d.ts +61 -0
- package/dist/internal/route-compiler.d.ts.map +1 -0
- package/dist/internal/route-compiler.js +121 -0
- package/dist/internal/route-compiler.js.map +1 -0
- package/dist/internal/route-execution.d.ts +17 -9
- package/dist/internal/route-execution.d.ts.map +1 -1
- package/dist/internal/route-execution.js +4 -18
- package/dist/internal/route-execution.js.map +1 -1
- package/dist/internal/route-lanes.d.ts +35 -0
- package/dist/internal/route-lanes.d.ts.map +1 -0
- package/dist/internal/route-lanes.js +150 -0
- package/dist/internal/route-lanes.js.map +1 -0
- package/dist/internal/route-program.d.ts +67 -0
- package/dist/internal/route-program.d.ts.map +1 -0
- package/dist/internal/route-program.js +199 -0
- package/dist/internal/route-program.js.map +1 -0
- package/dist/response-observer.d.ts +25 -0
- package/dist/response-observer.d.ts.map +1 -0
- package/dist/response-observer.js +40 -0
- package/dist/response-observer.js.map +1 -0
- package/dist/security/baseline.d.ts +68 -0
- package/dist/security/baseline.d.ts.map +1 -0
- package/dist/security/baseline.js +118 -0
- package/dist/security/baseline.js.map +1 -0
- package/dist/server/bare-error-lane.d.ts +26 -0
- package/dist/server/bare-error-lane.d.ts.map +1 -0
- package/dist/server/bare-error-lane.js +60 -0
- package/dist/server/bare-error-lane.js.map +1 -0
- package/dist/server/body-lane.d.ts +13 -0
- package/dist/server/body-lane.d.ts.map +1 -0
- package/dist/server/body-lane.js +134 -0
- package/dist/server/body-lane.js.map +1 -0
- package/dist/server/body.d.ts +7 -0
- package/dist/server/body.d.ts.map +1 -1
- package/dist/server/body.js +7 -0
- package/dist/server/body.js.map +1 -1
- package/dist/server/bridge-markers.d.ts +25 -0
- package/dist/server/bridge-markers.d.ts.map +1 -0
- package/dist/server/bridge-markers.js +25 -0
- package/dist/server/bridge-markers.js.map +1 -0
- package/dist/server/context.d.ts +6 -1
- package/dist/server/context.d.ts.map +1 -1
- package/dist/server/contract.d.ts +40 -14
- package/dist/server/contract.d.ts.map +1 -1
- package/dist/server/contract.js +7 -0
- package/dist/server/contract.js.map +1 -1
- package/dist/server/cookies.d.ts.map +1 -1
- package/dist/server/cookies.js +16 -2
- package/dist/server/cookies.js.map +1 -1
- package/dist/server/header-case.d.ts.map +1 -1
- package/dist/server/header-case.js +2 -1
- package/dist/server/header-case.js.map +1 -1
- package/dist/server/http.d.ts +36 -3
- package/dist/server/http.d.ts.map +1 -1
- package/dist/server/http.js +49 -3
- package/dist/server/http.js.map +1 -1
- package/dist/server/install.d.ts +6 -8
- package/dist/server/install.d.ts.map +1 -1
- package/dist/server/install.js +7 -0
- package/dist/server/install.js.map +1 -1
- package/dist/server/node-outcome-hook.d.ts +2 -1
- package/dist/server/node-outcome-hook.d.ts.map +1 -1
- package/dist/server/node-outcome-hook.js.map +1 -1
- package/dist/server/node-outcome.d.ts +17 -2
- package/dist/server/node-outcome.d.ts.map +1 -1
- package/dist/server/node-outcome.js +37 -3
- package/dist/server/node-outcome.js.map +1 -1
- package/dist/server/plugin.d.ts +2 -2
- package/dist/server/plugin.d.ts.map +1 -1
- package/dist/server/plugin.js +6 -1
- package/dist/server/plugin.js.map +1 -1
- package/dist/server/query.d.ts +7 -1
- package/dist/server/query.d.ts.map +1 -1
- package/dist/server/query.js +8 -3
- package/dist/server/query.js.map +1 -1
- package/dist/server/registry.d.ts +40 -12
- package/dist/server/registry.d.ts.map +1 -1
- package/dist/server/request-context.d.ts +2 -13
- package/dist/server/request-context.d.ts.map +1 -1
- package/dist/server/request-context.js +18 -159
- package/dist/server/request-context.js.map +1 -1
- package/dist/server/respond.d.ts.map +1 -1
- package/dist/server/respond.js +58 -4
- package/dist/server/respond.js.map +1 -1
- package/dist/server/response-contract-lane.d.ts.map +1 -1
- package/dist/server/response-contract-lane.js +5 -2
- package/dist/server/response-contract-lane.js.map +1 -1
- package/dist/server/response-observer-runtime.d.ts +20 -0
- package/dist/server/response-observer-runtime.d.ts.map +1 -0
- package/dist/server/response-observer-runtime.js +95 -0
- package/dist/server/response-observer-runtime.js.map +1 -0
- package/dist/server/runtime-core.d.ts +76 -0
- package/dist/server/runtime-core.d.ts.map +1 -1
- package/dist/server/runtime-core.js +42 -1
- package/dist/server/runtime-core.js.map +1 -1
- package/dist/server/server-types.d.ts +6 -1
- package/dist/server/server-types.d.ts.map +1 -1
- package/dist/server/server.d.ts +158 -116
- package/dist/server/server.d.ts.map +1 -1
- package/dist/server/server.js +707 -844
- package/dist/server/server.js.map +1 -1
- package/dist/server/validation.d.ts +5 -0
- package/dist/server/validation.d.ts.map +1 -0
- package/dist/server/validation.js +13 -0
- package/dist/server/validation.js.map +1 -0
- package/dist/server/websocket.d.ts +20 -4
- package/dist/server/websocket.d.ts.map +1 -1
- package/dist/server/websocket.js +81 -11
- package/dist/server/websocket.js.map +1 -1
- package/dist/server/ws-bun.d.ts +12 -4
- package/dist/server/ws-bun.d.ts.map +1 -1
- package/dist/server/ws-bun.js +50 -16
- package/dist/server/ws-bun.js.map +1 -1
- package/dist/server/ws-hook.d.ts +3 -2
- package/dist/server/ws-hook.d.ts.map +1 -1
- package/dist/server.d.ts +3 -2
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +2 -1
- package/dist/server.js.map +1 -1
- package/dist/single-copy-register.d.ts +2 -0
- package/dist/single-copy-register.d.ts.map +1 -0
- package/dist/single-copy-register.js +24 -0
- package/dist/single-copy-register.js.map +1 -0
- package/dist/single-copy.d.ts +156 -0
- package/dist/single-copy.d.ts.map +1 -0
- package/dist/single-copy.js +472 -0
- package/dist/single-copy.js.map +1 -0
- package/package.json +28 -3
- package/src/assurance.ts +58 -1
- package/src/durable-adapters.ts +20 -0
- package/src/durable-approval.ts +281 -0
- package/src/durable-effect.ts +300 -0
- package/src/durable-execution.ts +8 -1627
- package/src/durable-saga.ts +744 -0
- package/src/edge-kit.ts +27 -0
- package/src/errors.ts +1 -0
- package/src/index.ts +129 -2
- package/src/internal/durable-approval-protocol.ts +1 -1
- package/src/internal/durable-effect-protocol.ts +1 -1
- package/src/internal/durable-saga-protocol.ts +1 -1
- package/src/internal/durable-shared.ts +157 -0
- package/src/internal/durable-types.ts +194 -0
- package/src/internal/route-compiler.ts +234 -0
- package/src/internal/route-execution.ts +53 -96
- package/src/internal/route-lanes.ts +221 -0
- package/src/internal/route-program.ts +404 -0
- package/src/response-observer.ts +64 -0
- package/src/security/baseline.ts +172 -0
- package/src/server/bare-error-lane.ts +77 -0
- package/src/server/body-lane.ts +203 -0
- package/src/server/body.ts +8 -0
- package/src/server/bridge-markers.ts +24 -0
- package/src/server/context.ts +6 -1
- package/src/server/contract.ts +70 -20
- package/src/server/cookies.ts +16 -2
- package/src/server/header-case.ts +3 -1
- package/src/server/http.ts +53 -3
- package/src/server/install.ts +14 -0
- package/src/server/node-outcome-hook.ts +2 -1
- package/src/server/node-outcome.ts +44 -3
- package/src/server/plugin.ts +11 -5
- package/src/server/query.ts +10 -4
- package/src/server/registry.ts +71 -16
- package/src/server/request-context.ts +13 -216
- package/src/server/respond.ts +71 -3
- package/src/server/response-contract-lane.ts +6 -2
- package/src/server/response-observer-runtime.ts +135 -0
- package/src/server/runtime-core.ts +124 -1
- package/src/server/server-types.ts +6 -1
- package/src/server/server.ts +1446 -1485
- package/src/server/validation.ts +19 -0
- package/src/server/websocket.ts +127 -17
- package/src/server/ws-bun.ts +67 -15
- package/src/server/ws-hook.ts +3 -2
- package/src/server.ts +8 -2
- package/src/single-copy-register.ts +24 -0
- package/src/single-copy.ts +526 -0
package/src/server/server.ts
CHANGED
|
@@ -12,22 +12,18 @@ import { FrameworkError, RouteConfigError } from "../errors.ts"
|
|
|
12
12
|
import type { Version } from "../index.ts"
|
|
13
13
|
import {
|
|
14
14
|
type AroundCapabilityOptions,
|
|
15
|
-
CAPABILITY_GUARD,
|
|
16
15
|
type CapabilityInterceptor,
|
|
17
16
|
type CapabilityUseEvent,
|
|
18
|
-
createCapabilityGuard,
|
|
19
17
|
DEFAULT_CAPABILITY_INTERCEPTOR_TIMEOUT_MS,
|
|
20
|
-
normalizeRouteCapabilities,
|
|
21
18
|
type RegisteredCapabilityInterceptor,
|
|
22
19
|
} from "../internal/capability-runtime.ts"
|
|
23
20
|
import {
|
|
24
21
|
type AssuranceDeclaration,
|
|
25
22
|
assuranceDeclarationsOf,
|
|
26
23
|
assuranceEvidenceFor,
|
|
27
|
-
NIFRA_ASSURANCE_IDS,
|
|
28
|
-
validEvidenceId,
|
|
29
24
|
} from "../internal/route-assurance.ts"
|
|
30
25
|
import { type CatalogRoute, RouteCatalog } from "../internal/route-catalog.ts"
|
|
26
|
+
import { compileRouteOptions } from "../internal/route-compiler.ts"
|
|
31
27
|
import {
|
|
32
28
|
compileRouteExecutionPlan,
|
|
33
29
|
type FusedBodyRunner,
|
|
@@ -40,8 +36,11 @@ import {
|
|
|
40
36
|
type RawErrorHandler,
|
|
41
37
|
type RouteEntry,
|
|
42
38
|
} from "../internal/route-execution.ts"
|
|
39
|
+
import type { RouteProgramStage } from "../internal/route-program.ts"
|
|
40
|
+
import { compileRouteProgram, executeRouteProgram } from "../internal/route-program.ts"
|
|
43
41
|
import { isSameOriginRequest } from "../internal/same-origin.ts"
|
|
44
|
-
import {
|
|
42
|
+
import type { ResponseObserverPlugin } from "../response-observer.ts"
|
|
43
|
+
import { decodeRouteParams } from "../router/pattern.ts"
|
|
45
44
|
import { EMPTY_PARAMS, type Method, Router } from "../router/router.ts"
|
|
46
45
|
import type {
|
|
47
46
|
InferOutput,
|
|
@@ -49,11 +48,13 @@ import type {
|
|
|
49
48
|
StandardResult,
|
|
50
49
|
StandardSchemaV1,
|
|
51
50
|
} from "../schema/standard.ts"
|
|
51
|
+
import { emitRequestErrorLog, renderBareError } from "./bare-error-lane.ts"
|
|
52
52
|
import {
|
|
53
53
|
assertByteLimit,
|
|
54
54
|
markTransportCap,
|
|
55
55
|
markTrustedBodyFraming,
|
|
56
56
|
type RawBodyReaders,
|
|
57
|
+
UNLIMITED_BODY_BYTES,
|
|
57
58
|
} from "./body.ts"
|
|
58
59
|
import { type ClientIpTrust, resolveClientIp } from "./client-ip.ts"
|
|
59
60
|
import type { Context, Platform, ResponseControls, RouteSchema } from "./context.ts"
|
|
@@ -63,42 +64,35 @@ import {
|
|
|
63
64
|
markLowercaseHeaderKeys,
|
|
64
65
|
} from "./header-case.ts"
|
|
65
66
|
import { headerObjectOf } from "./headers.ts"
|
|
66
|
-
import { jsonError, pathnameOf, type UrlParts, urlPartsOf } from "./http.ts"
|
|
67
|
+
import { jsonError, pathnameOf, plainError, type UrlParts, urlPartsOf } from "./http.ts"
|
|
67
68
|
import { type NodeServeOutcome, withStaticNodeHeaders } from "./node-outcome.ts"
|
|
68
|
-
import {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
recordHeadersView,
|
|
69
|
+
import type {
|
|
70
|
+
NodeOutcomeRuntime,
|
|
71
|
+
NodeRequestContext,
|
|
72
|
+
NodeRequestHook,
|
|
73
|
+
NodeResponseContext,
|
|
74
|
+
NodeResponseHook,
|
|
75
|
+
ResponseBodyHook,
|
|
76
|
+
ResponseBodyReplacement,
|
|
77
|
+
ResponseHeadersHook,
|
|
78
|
+
ResponseHeadersView,
|
|
79
79
|
} from "./node-outcome-hook.ts"
|
|
80
|
-
import {
|
|
81
|
-
|
|
82
|
-
type QueryValue,
|
|
83
|
-
queryObjectOf,
|
|
84
|
-
readBoundedForm,
|
|
85
|
-
searchOf,
|
|
86
|
-
} from "./query.ts"
|
|
87
|
-
import { RequestContext, readBoundedJsonSource } from "./request-context.ts"
|
|
80
|
+
import { type QueryValue, queryObjectOf, searchOf } from "./query.ts"
|
|
81
|
+
import { RequestContext, readBodyFramed } from "./request-context.ts"
|
|
88
82
|
import {
|
|
89
83
|
applyStaticResponseHeaders,
|
|
90
84
|
buildStaticResponseHeaders,
|
|
91
85
|
fusedRespond,
|
|
92
86
|
fusedRespondNoSet,
|
|
93
|
-
knownMutableHeaders,
|
|
94
|
-
rememberMutableHeaders,
|
|
95
|
-
taggedResponseBody,
|
|
96
87
|
toResponse,
|
|
97
88
|
} from "./respond.ts"
|
|
98
89
|
// Type-only: erased, so the kernel never pulls the lane's implementation into a bundle that does not
|
|
99
90
|
// install the plugin. The value side arrives through the symbol-keyed install seam.
|
|
100
91
|
import type { ResponseContractRuntime } from "./response-contract-lane.ts"
|
|
101
|
-
import {
|
|
92
|
+
import type {
|
|
93
|
+
ResponseObserverMethods,
|
|
94
|
+
ResponseObserverRuntime,
|
|
95
|
+
} from "./response-observer-runtime.ts"
|
|
102
96
|
import {
|
|
103
97
|
CONTEXT_SEARCH,
|
|
104
98
|
CONTEXT_SET,
|
|
@@ -107,9 +101,13 @@ import {
|
|
|
107
101
|
getUnboundedRequestBudget,
|
|
108
102
|
type HandlerResult,
|
|
109
103
|
headerOf,
|
|
104
|
+
isResponseResult,
|
|
105
|
+
type ResponseResult,
|
|
110
106
|
requestOf,
|
|
107
|
+
type StatusResponse,
|
|
111
108
|
} from "./runtime-core.ts"
|
|
112
109
|
import { normalizeStaticResponseHeaders, type StaticResponseHeaders } from "./static-headers.ts"
|
|
110
|
+
import { plainValidationError } from "./validation.ts"
|
|
113
111
|
|
|
114
112
|
// NodeServeOutcome (the nifra<->node bridge render form) now lives in `./node-outcome.ts`; re-exported
|
|
115
113
|
// so existing importers keep resolving it from the server module.
|
|
@@ -125,96 +123,6 @@ export type {
|
|
|
125
123
|
ResponseHeadersView,
|
|
126
124
|
}
|
|
127
125
|
|
|
128
|
-
const HEADER_MUTABILITY_PROBE = "x-nifra-header-probe"
|
|
129
|
-
const GUARDED_RESPONSE_HEADERS = new WeakSet<Headers>()
|
|
130
|
-
|
|
131
|
-
/**
|
|
132
|
-
* Detect a guarded Web Headers object before invoking user code. Retrying after catching any
|
|
133
|
-
* TypeError is observably wrong: user hooks are allowed to throw TypeError themselves, and a retry
|
|
134
|
-
* runs those hooks twice. The hot path never reaches the probe: every framework-constructed
|
|
135
|
-
* Response stamps its headers as known-mutable at construction (see respond.ts), so only a
|
|
136
|
-
* handler-returned foreign `Response` - the case that can actually be guarded - pays it, once per
|
|
137
|
-
* headers object. The probe uses a valid private header name and restores a pre-existing value, so
|
|
138
|
-
* the fallback is limited to the actual immutable/guarded-header case.
|
|
139
|
-
*/
|
|
140
|
-
function hasMutableResponseHeaders(headers: Headers): boolean {
|
|
141
|
-
if (knownMutableHeaders(headers)) return true
|
|
142
|
-
if (GUARDED_RESPONSE_HEADERS.has(headers)) return false
|
|
143
|
-
let previous: string | null = null
|
|
144
|
-
try {
|
|
145
|
-
previous = headers.get(HEADER_MUTABILITY_PROBE)
|
|
146
|
-
headers.set(HEADER_MUTABILITY_PROBE, "1")
|
|
147
|
-
if (previous === null) headers.delete(HEADER_MUTABILITY_PROBE)
|
|
148
|
-
else headers.set(HEADER_MUTABILITY_PROBE, previous)
|
|
149
|
-
rememberMutableHeaders(headers)
|
|
150
|
-
return true
|
|
151
|
-
} catch {
|
|
152
|
-
try {
|
|
153
|
-
if (previous === null) headers.delete(HEADER_MUTABILITY_PROBE)
|
|
154
|
-
else headers.set(HEADER_MUTABILITY_PROBE, previous)
|
|
155
|
-
} catch {
|
|
156
|
-
// The guarded object rejected the cleanup too; the clone below is authoritative.
|
|
157
|
-
}
|
|
158
|
-
GUARDED_RESPONSE_HEADERS.add(headers)
|
|
159
|
-
return false
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
/** Adapt a portable {@link ResponseBodyHook} into the Web `onResponse` walk. Only a Response
|
|
164
|
-
* carrying the framework-buffered body tag participates - a raw or streamed Response is skipped by
|
|
165
|
-
* contract, never drained. */
|
|
166
|
-
function webResponseBodyHook(
|
|
167
|
-
fn: ResponseBodyHook,
|
|
168
|
-
owners: ReadonlySet<object>,
|
|
169
|
-
): (response: Response, req: Request) => MaybePromise<Response> {
|
|
170
|
-
return (response, req) => {
|
|
171
|
-
const body = taggedResponseBody(response, owners)
|
|
172
|
-
if (body === undefined) return response
|
|
173
|
-
const out = fn(body, response.headers, webRequestView(req), response.status)
|
|
174
|
-
if (out instanceof Promise) return out.then((replaced) => withReplacedBody(response, replaced))
|
|
175
|
-
return withReplacedBody(response, out)
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
/** Minimal request view over a Web `Request` for portable header hooks on the Web serving paths. */
|
|
180
|
-
function webRequestView(req: Request): NodeRequestContext {
|
|
181
|
-
return { method: req.method, url: req.url, header: (name) => req.headers.get(name) }
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
/**
|
|
185
|
-
* Adapt a portable {@link ResponseHeadersHook} into the Web `onResponse` walk: run it against the
|
|
186
|
-
* response's own `Headers` in place (no clone). A response whose headers are GUARDED (a raw
|
|
187
|
-
* `fetch()`ed Response returned by a handler) throws on the FIRST mutation, so nothing was applied
|
|
188
|
-
* yet - rerun once against a mutable copy. Async hooks should perform their mutations before their
|
|
189
|
-
* first await for the guard fallback to cover them.
|
|
190
|
-
*/
|
|
191
|
-
function webResponseHeadersHook(
|
|
192
|
-
fn: ResponseHeadersHook,
|
|
193
|
-
): (response: Response, req: Request) => MaybePromise<Response> {
|
|
194
|
-
return (response, req) => {
|
|
195
|
-
const view = webRequestView(req)
|
|
196
|
-
if (!hasMutableResponseHeaders(response.headers)) {
|
|
197
|
-
const clone = new Response(response.body, response)
|
|
198
|
-
const out = fn(clone.headers, view, clone.status)
|
|
199
|
-
return out instanceof Promise ? out.then(() => clone) : clone
|
|
200
|
-
}
|
|
201
|
-
const out = fn(response.headers, view, response.status)
|
|
202
|
-
return out instanceof Promise ? out.then(() => response) : response
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
/** Adapt a raw-response fallback hook. Tagged framework payloads already ran through the body tier,
|
|
207
|
-
* while untagged Responses include streams, proxied fetches, and framework-generated error responses. */
|
|
208
|
-
function webResponseRawHook(
|
|
209
|
-
fn: (response: Response, req: Request) => MaybePromise<Response>,
|
|
210
|
-
owners: ReadonlySet<object>,
|
|
211
|
-
): (response: Response, req: Request) => MaybePromise<Response> {
|
|
212
|
-
return (response, req) => {
|
|
213
|
-
if (taggedResponseBody(response, owners) !== undefined) return response
|
|
214
|
-
return fn(response, req)
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
|
|
218
126
|
import type { IdempotencyRuntime } from "./idempotency-lane.ts"
|
|
219
127
|
import {
|
|
220
128
|
INSTALL_EFFECT_LEDGER,
|
|
@@ -222,10 +130,13 @@ import {
|
|
|
222
130
|
INSTALL_MCP,
|
|
223
131
|
INSTALL_NODE_DIRECT,
|
|
224
132
|
INSTALL_RESPONSE_CONTRACT,
|
|
133
|
+
INSTALL_RESPONSE_OBSERVER,
|
|
225
134
|
INSTALL_SSE,
|
|
226
135
|
INSTALL_WS,
|
|
136
|
+
NODE_NATIVE_MOUNT,
|
|
137
|
+
RESOLVE_NODE_MOUNT,
|
|
227
138
|
} from "./install.ts"
|
|
228
|
-
import type { EffectLedgerRuntime
|
|
139
|
+
import type { EffectLedgerRuntime } from "./ledger-lane.ts"
|
|
229
140
|
import { jsonLogger, type Logger } from "./logger.ts"
|
|
230
141
|
import type { McpRuntime } from "./mcp-hook.ts"
|
|
231
142
|
import type {
|
|
@@ -363,6 +274,22 @@ interface BunUpgradeServer {
|
|
|
363
274
|
|
|
364
275
|
type MountedFetchHandler = (request: Request, platform?: Platform) => MaybePromise<Response>
|
|
365
276
|
|
|
277
|
+
/** Structural native mount contract. The serving adapter owns the concrete Node request/response
|
|
278
|
+
* types; the kernel only selects a handler after proving that taking this lane cannot skip its
|
|
279
|
+
* global lifecycle. `false` means the capability was unavailable at runtime, so the adapter must
|
|
280
|
+
* retry the ordinary Web mount with the same untouched source. */
|
|
281
|
+
type NativeMountHandler = (
|
|
282
|
+
request: unknown,
|
|
283
|
+
response: unknown,
|
|
284
|
+
platform?: Platform,
|
|
285
|
+
) => MaybePromise<undefined | false>
|
|
286
|
+
|
|
287
|
+
interface NativeMountSelection {
|
|
288
|
+
readonly handler: NativeMountHandler
|
|
289
|
+
readonly path: string
|
|
290
|
+
readonly stripPrefix: boolean
|
|
291
|
+
}
|
|
292
|
+
|
|
366
293
|
interface FetchMount {
|
|
367
294
|
readonly path: string
|
|
368
295
|
readonly handler: MountedFetchHandler
|
|
@@ -432,12 +359,38 @@ function requireMcpRuntime(runtime: McpRuntime | undefined): McpRuntime {
|
|
|
432
359
|
}
|
|
433
360
|
|
|
434
361
|
/** The handler's permitted return type. When the route declares a `response` schema, the return is
|
|
435
|
-
* constrained to the contract's type (
|
|
436
|
-
* declared
|
|
362
|
+
* constrained to the contract's type (a raw `Response`, or a `status(...)` early exit - both are
|
|
363
|
+
* control flow, not the declared payload) - so the implementation can't drift from the declared
|
|
364
|
+
* contract. Without a `response` schema it's unconstrained (`HandlerResult`), exactly as before. */
|
|
437
365
|
type ResponseOf<S extends RouteSchema> = S extends { response: infer R extends StandardSchemaV1 }
|
|
438
|
-
? InferOutput<R> | Response
|
|
366
|
+
? InferOutput<R> | Response | ResponseResult
|
|
439
367
|
: HandlerResult
|
|
440
368
|
|
|
369
|
+
/** A typed status result is control flow, not a context extension. */
|
|
370
|
+
type StatusResponseOf<T> = T extends StatusResponse<number, unknown> ? T : never
|
|
371
|
+
|
|
372
|
+
type ContextExtensionPart<T> = T extends object ? T : EmptyContext
|
|
373
|
+
type ContextExtensionOf<T> = [Exclude<Awaited<T>, Response | ResponseResult>] extends [never]
|
|
374
|
+
? EmptyContext
|
|
375
|
+
: ContextExtensionPart<Exclude<Awaited<T>, Response | ResponseResult>>
|
|
376
|
+
|
|
377
|
+
/** Keep concrete lifecycle return values for route inference, but don't let an untyped hook widen every route to unknown. */
|
|
378
|
+
type HookOutputOf<T> = unknown extends Awaited<T> ? never : Exclude<Awaited<T>, void>
|
|
379
|
+
|
|
380
|
+
type ReturnOfHookField<M, K extends PropertyKey> = M extends {
|
|
381
|
+
readonly [P in K]?: infer F
|
|
382
|
+
}
|
|
383
|
+
? F extends (...args: never[]) => infer R
|
|
384
|
+
? HookOutputOf<R>
|
|
385
|
+
: never
|
|
386
|
+
: never
|
|
387
|
+
|
|
388
|
+
type MiddlewareOutputOf<M> =
|
|
389
|
+
| ReturnOfHookField<M, "beforeHandle">
|
|
390
|
+
| ReturnOfHookField<M, "afterHandle">
|
|
391
|
+
| ReturnOfHookField<M, "onError">
|
|
392
|
+
| ReturnOfHookField<M, "around">
|
|
393
|
+
|
|
441
394
|
/**
|
|
442
395
|
* Public handler shape: context typed from the path, the (optional) schema, and
|
|
443
396
|
* any accumulated middleware context `Ctx` (from `derive`/`decorate`).
|
|
@@ -471,7 +424,7 @@ export type {
|
|
|
471
424
|
// A plugin operates over arbitrary Server shapes; `any` here is the standard framework escape hatch
|
|
472
425
|
// (the precise threading happens at the `use` call site, which is generic over the *concrete* `this`).
|
|
473
426
|
// biome-ignore lint/suspicious/noExplicitAny: plugins are generic over any Server's Registry/Context
|
|
474
|
-
export type AnyServer = Server<any, any>
|
|
427
|
+
export type AnyServer = Server<any, any, any>
|
|
475
428
|
|
|
476
429
|
// Plugin definers + their types now live in `./plugin.ts`; re-exported here so `.use()` callers and
|
|
477
430
|
// existing importers keep resolving them from the server module.
|
|
@@ -497,17 +450,13 @@ const STOP_HOOK_TIMEOUT_MS = 5_000
|
|
|
497
450
|
* for the same request, so a browser that could open a socket was told its POST was cross-origin. */
|
|
498
451
|
const wsSameOrigin = isSameOriginRequest
|
|
499
452
|
|
|
500
|
-
function validationError(issues: ReadonlyArray<StandardIssue>): Response {
|
|
501
|
-
const serialized = issues.map((issue) => {
|
|
502
|
-
const path = issue.path?.map((seg) => String(typeof seg === "object" ? seg.key : seg))
|
|
503
|
-
return path !== undefined ? { message: issue.message, path } : { message: issue.message }
|
|
504
|
-
})
|
|
505
|
-
return Response.json({ ok: false, error: "validation", issues: serialized }, { status: 422 })
|
|
506
|
-
}
|
|
507
|
-
|
|
508
453
|
// `jsonError`, `urlPartsOf`, `pathnameOf` moved to `./http.ts` (a dependency-free leaf shared with the
|
|
509
454
|
// opt-in request lanes); re-exported so existing importers keep resolving from here.
|
|
510
455
|
export { pathnameOf, urlPartsOf } from "./http.ts"
|
|
456
|
+
/** The 422 as plain data - the shape every lane's response wrapper takes, so a rejected body costs
|
|
457
|
+
* what an accepted one costs: on Node it is written straight to the socket with a `content-length`
|
|
458
|
+
* instead of being built as a `Response` and drained back out. */
|
|
459
|
+
export { plainValidationError } from "./validation.ts"
|
|
511
460
|
// Query-string + urlencoded-form parsing now lives in `./query.ts`; re-exported so existing
|
|
512
461
|
// importers keep resolving `searchOf`/`queryObjectOf`/`QueryValue` from here.
|
|
513
462
|
export { type QueryValue, queryObjectOf, searchOf }
|
|
@@ -577,8 +526,17 @@ type FeatureVersionOf<V extends string> = V extends `${infer Major}.${infer Mino
|
|
|
577
526
|
*/
|
|
578
527
|
export type NifraFeatureVersion = FeatureVersionOf<Version>
|
|
579
528
|
|
|
529
|
+
/**
|
|
530
|
+
* The Web lanes' `wrapResponse` input, as a `Response`. A plain-data carrier ({@link plainError},
|
|
531
|
+
* `status(...)`) is rendered on the ordinary JSON lane rather than through its own `toResponse`, so
|
|
532
|
+
* it picks up the same prebuilt init a handler's plain return uses. `EMPTY_RESPONSE_CONTROLS` because
|
|
533
|
+
* these renders happen where no request context exists - before routing, or after it was abandoned.
|
|
534
|
+
*/
|
|
535
|
+
const webResponseOf = (result: Response | ResponseResult): Response =>
|
|
536
|
+
result instanceof Response ? result : toResponse(result, EMPTY_RESPONSE_CONTROLS)
|
|
537
|
+
|
|
580
538
|
// Stable module-level finalizers so `fetch`/`resolveNode` allocate no per-request closures.
|
|
581
|
-
const IDENTITY_RESPONSE = (response: Response): Response => response
|
|
539
|
+
const IDENTITY_RESPONSE = (response: Response | ResponseResult): Response => webResponseOf(response)
|
|
582
540
|
const RESPONSE_TIMEOUT = (): Response => jsonError(503, "request_timeout")
|
|
583
541
|
|
|
584
542
|
// The unused-order-scoped-hook audit (recorder, seal check, stack capture, message) is a dev-time
|
|
@@ -687,7 +645,7 @@ const sealHookAudit = (
|
|
|
687
645
|
* app.decorate("db", db).derive((c) => ({ user: auth(c) }))
|
|
688
646
|
* .get("/me", (c) => c.user) // c.user + c.db are typed
|
|
689
647
|
*/
|
|
690
|
-
export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
648
|
+
export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, HookOutput = never> {
|
|
691
649
|
/**
|
|
692
650
|
* Which copy of `@nifrajs/core` declared this type. Two copies in one build (a linked sibling repo,
|
|
693
651
|
* a hoisting split) produce two unrelated `Server` types whose assignment error is a wall of
|
|
@@ -706,6 +664,15 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
706
664
|
* Created by the first `app.ws()` via the `@nifrajs/core/ws` runtime - `undefined` until then, so a
|
|
707
665
|
* no-WebSocket app never constructs (or bundles) it. */
|
|
708
666
|
private topics: TopicRegistry | undefined
|
|
667
|
+
/** True once any `app.ws()` route sets `validateSend: true` - the one case where a broadcast frame
|
|
668
|
+
* is validated (and possibly dropped) per socket, so `app.publish` must stay on the JS registry loop
|
|
669
|
+
* rather than delegate to Bun's native `server.publish`. */
|
|
670
|
+
private wsHasValidatedSend = false
|
|
671
|
+
/** Bun's native topic broadcast, bound by `listen()` in native-pubsub mode; `app.publish` uses it
|
|
672
|
+
* instead of the JS registry. `undefined` off Bun, before `listen()`, or with a validated-send route. */
|
|
673
|
+
private nativePublish:
|
|
674
|
+
| ((topic: string, data: string | ArrayBufferView | ArrayBuffer) => void)
|
|
675
|
+
| undefined
|
|
709
676
|
private readonly maxBodyBytes: number
|
|
710
677
|
private readonly protoPoisoning: "reject" | "strip" | "ignore"
|
|
711
678
|
/** `trustBodyFraming`: mark every `app.fetch` request as runtime-framed (see ServerOptions). */
|
|
@@ -780,7 +747,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
780
747
|
private staticResponseHeaders: StaticResponseHeaders | undefined
|
|
781
748
|
/** `wrapResponse` for the Web lanes: identity until static headers exist to fold into the
|
|
782
749
|
* framework's own error/404/timeout renders, which are built outside the header init. */
|
|
783
|
-
private wrapWebResponse: (response: Response) => Response
|
|
750
|
+
private wrapWebResponse: (response: Response | ResponseResult) => Response
|
|
784
751
|
private webResponseTimeout: () => Response
|
|
785
752
|
/** Body/raw response hooks need a framework-payload marker; keep that decision per app. */
|
|
786
753
|
private responseBodyTag: object | undefined
|
|
@@ -925,13 +892,28 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
925
892
|
* declared *after* this call, and reaches none declared before. One added after the last route
|
|
926
893
|
* (e.g. on the app returned by a route-registering factory) covers nothing - see
|
|
927
894
|
* {@link ServerOptions.unusedScopedHooks}.
|
|
895
|
+
*
|
|
896
|
+
* Returning a `status(...)` (or a `Response`) instead of an extension ends the request there - the
|
|
897
|
+
* guard shape, and the cheap one: it renders as plain data on the same lane a handler's return
|
|
898
|
+
* takes, where `throw new Response(...)` pays ~40% of the rejection lane to build the object. That
|
|
899
|
+
* branch is control flow, so it does not widen the derived context type.
|
|
928
900
|
*/
|
|
929
|
-
derive<
|
|
901
|
+
derive<H extends (context: Context & Ctx) => MaybePromise<unknown>>(
|
|
902
|
+
fn: H,
|
|
903
|
+
): Server<
|
|
904
|
+
R,
|
|
905
|
+
Ctx & ContextExtensionOf<ReturnType<H>>,
|
|
906
|
+
HookOutput | StatusResponseOf<Awaited<ReturnType<H>>>
|
|
907
|
+
> {
|
|
930
908
|
this.assertConfigurable("derive()")
|
|
931
909
|
this.derives.push(fn as unknown as RawDerive)
|
|
932
910
|
if (hookAuditRuntime && process.env.NODE_ENV !== "production")
|
|
933
911
|
recordScopedHook(this, this.catalog.size, "derive")
|
|
934
|
-
return this as unknown as Server<
|
|
912
|
+
return this as unknown as Server<
|
|
913
|
+
R,
|
|
914
|
+
Ctx & ContextExtensionOf<ReturnType<H>>,
|
|
915
|
+
HookOutput | StatusResponseOf<Awaited<ReturnType<H>>>
|
|
916
|
+
>
|
|
935
917
|
}
|
|
936
918
|
|
|
937
919
|
/**
|
|
@@ -939,12 +921,12 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
939
921
|
* by none before; one added after the last route reaches nothing
|
|
940
922
|
* (see {@link ServerOptions.unusedScopedHooks}).
|
|
941
923
|
*/
|
|
942
|
-
decorate<const K extends string, V>(key: K, value: V): Server<R, Ctx & Record<K, V
|
|
924
|
+
decorate<const K extends string, V>(key: K, value: V): Server<R, Ctx & Record<K, V>, HookOutput> {
|
|
943
925
|
this.assertConfigurable("decorate()")
|
|
944
926
|
this.decorations[key] = value
|
|
945
927
|
if (hookAuditRuntime && process.env.NODE_ENV !== "production")
|
|
946
928
|
recordScopedHook(this, this.catalog.size, "decorate")
|
|
947
|
-
return this as unknown as Server<R, Ctx & Record<K, V
|
|
929
|
+
return this as unknown as Server<R, Ctx & Record<K, V>, HookOutput>
|
|
948
930
|
}
|
|
949
931
|
|
|
950
932
|
/**
|
|
@@ -966,12 +948,14 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
966
948
|
* it is snapshotted into every route declared *after* this call and covers none declared before, so
|
|
967
949
|
* one added after the last route reaches nothing (see {@link ServerOptions.unusedScopedHooks}).
|
|
968
950
|
*/
|
|
969
|
-
beforeHandle
|
|
951
|
+
beforeHandle<H extends (context: Context & Ctx) => MaybePromise<unknown>>(
|
|
952
|
+
fn: H,
|
|
953
|
+
): Server<R, Ctx, HookOutput | HookOutputOf<ReturnType<H>>> {
|
|
970
954
|
this.assertConfigurable("beforeHandle()")
|
|
971
955
|
this.beforeHandleHooks.push(fn as unknown as RawBeforeHandle)
|
|
972
956
|
if (hookAuditRuntime && process.env.NODE_ENV !== "production")
|
|
973
957
|
recordScopedHook(this, this.catalog.size, "beforeHandle")
|
|
974
|
-
return this
|
|
958
|
+
return this as unknown as Server<R, Ctx, HookOutput | HookOutputOf<ReturnType<H>>>
|
|
975
959
|
}
|
|
976
960
|
|
|
977
961
|
/**
|
|
@@ -982,12 +966,14 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
982
966
|
* Order-scoped: captured by every route declared *after* this call, none before; one added after the
|
|
983
967
|
* last route reaches nothing (see {@link ServerOptions.unusedScopedHooks}).
|
|
984
968
|
*/
|
|
985
|
-
around
|
|
969
|
+
around<
|
|
970
|
+
H extends (context: Context & Ctx, next: () => MaybePromise<unknown>) => MaybePromise<unknown>,
|
|
971
|
+
>(fn: H): Server<R, Ctx, HookOutput | HookOutputOf<ReturnType<H>>> {
|
|
986
972
|
this.assertConfigurable("around()")
|
|
987
973
|
this.aroundHooks.push(fn as unknown as RawAround)
|
|
988
974
|
if (hookAuditRuntime && process.env.NODE_ENV !== "production")
|
|
989
975
|
recordScopedHook(this, this.catalog.size, "around")
|
|
990
|
-
return this
|
|
976
|
+
return this as unknown as Server<R, Ctx, HookOutput | HookOutputOf<ReturnType<H>>>
|
|
991
977
|
}
|
|
992
978
|
|
|
993
979
|
/**
|
|
@@ -1029,23 +1015,27 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
1029
1015
|
/** Transform the handler's result before it is serialized. Order-scoped: captured by routes declared
|
|
1030
1016
|
* *after* this call, none before; one added after the last route reaches nothing (see
|
|
1031
1017
|
* {@link ServerOptions.unusedScopedHooks}). */
|
|
1032
|
-
afterHandle
|
|
1018
|
+
afterHandle<H extends (result: unknown, context: Context & Ctx) => MaybePromise<unknown>>(
|
|
1019
|
+
fn: H,
|
|
1020
|
+
): Server<R, Ctx, HookOutput | HookOutputOf<ReturnType<H>>> {
|
|
1033
1021
|
this.assertConfigurable("afterHandle()")
|
|
1034
1022
|
this.afterHandleHooks.push(fn as unknown as RawAfterHandle)
|
|
1035
1023
|
if (hookAuditRuntime && process.env.NODE_ENV !== "production")
|
|
1036
1024
|
recordScopedHook(this, this.catalog.size, "afterHandle")
|
|
1037
|
-
return this
|
|
1025
|
+
return this as unknown as Server<R, Ctx, HookOutput | HookOutputOf<ReturnType<H>>>
|
|
1038
1026
|
}
|
|
1039
1027
|
|
|
1040
1028
|
/** Handle a thrown error; a non-`undefined` return becomes the response (else the default 500).
|
|
1041
1029
|
* Order-scoped: captured by routes declared *after* this call, none before; one added after the last
|
|
1042
1030
|
* route reaches nothing (see {@link ServerOptions.unusedScopedHooks}). */
|
|
1043
|
-
onError
|
|
1031
|
+
onError<H extends (error: unknown, context: Context & Ctx) => MaybePromise<unknown>>(
|
|
1032
|
+
fn: H,
|
|
1033
|
+
): Server<R, Ctx, HookOutput | HookOutputOf<ReturnType<H>>> {
|
|
1044
1034
|
this.assertConfigurable("onError()")
|
|
1045
1035
|
this.onErrorHooks.push(fn as unknown as RawErrorHandler)
|
|
1046
1036
|
if (hookAuditRuntime && process.env.NODE_ENV !== "production")
|
|
1047
1037
|
recordScopedHook(this, this.catalog.size, "onError")
|
|
1048
|
-
return this
|
|
1038
|
+
return this as unknown as Server<R, Ctx, HookOutput | HookOutputOf<ReturnType<H>>>
|
|
1049
1039
|
}
|
|
1050
1040
|
|
|
1051
1041
|
/** Transform every outgoing response - success, error, 404, 405, short-circuit. Global. */
|
|
@@ -1089,11 +1079,27 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
1089
1079
|
* hook owns part of the header state - into a hook that runs in the right order. */
|
|
1090
1080
|
private addStaticResponseHeaders(record: Record<string, string>): this {
|
|
1091
1081
|
if (this.onResponseHooks.length > 0) {
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1082
|
+
const observerHeaders = (
|
|
1083
|
+
this as unknown as Pick<ResponseObserverMethods, "onResponseHeaders">
|
|
1084
|
+
).onResponseHeaders
|
|
1085
|
+
if (observerHeaders !== undefined) {
|
|
1086
|
+
observerHeaders.call(this, (headers) => {
|
|
1087
|
+
for (const name of Object.keys(record)) {
|
|
1088
|
+
if (!headers.has(name)) headers.set(name, record[name] as string)
|
|
1089
|
+
}
|
|
1090
|
+
})
|
|
1091
|
+
return this
|
|
1092
|
+
}
|
|
1093
|
+
// Route the hook through the same seam the static tier uses. On a framework response (headers
|
|
1094
|
+
// stamped mutable) it applies the defaults in place and returns the SAME object, so the
|
|
1095
|
+
// serialized-body marker survives for later `onResponseBody`/`onResponseRaw` observers and the
|
|
1096
|
+
// Node writer; reconstructing a `Response` here would strip that marker and reclassify the Node
|
|
1097
|
+
// outcome from `json` to a generic `response`. A guarded foreign response takes its clone path.
|
|
1098
|
+
const statics = buildStaticResponseHeaders(record)
|
|
1099
|
+
this.onResponseHooks.push((response) => applyStaticResponseHeaders(response, statics))
|
|
1100
|
+
this.onNodeResponseHooks.push(undefined)
|
|
1101
|
+
this.nodeResponseHooksComplete = false
|
|
1102
|
+
return this
|
|
1097
1103
|
}
|
|
1098
1104
|
const merged =
|
|
1099
1105
|
this.staticResponseHeaders === undefined
|
|
@@ -1101,7 +1107,8 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
1101
1107
|
: { ...this.staticResponseHeaders.record, ...record }
|
|
1102
1108
|
this.staticResponseHeaders = buildStaticResponseHeaders(merged)
|
|
1103
1109
|
const statics = this.staticResponseHeaders
|
|
1104
|
-
this.wrapWebResponse = (response) =>
|
|
1110
|
+
this.wrapWebResponse = (response) =>
|
|
1111
|
+
applyStaticResponseHeaders(webResponseOf(response), statics)
|
|
1105
1112
|
this.webResponseTimeout = () => applyStaticResponseHeaders(RESPONSE_TIMEOUT(), statics)
|
|
1106
1113
|
return this
|
|
1107
1114
|
}
|
|
@@ -1114,61 +1121,18 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
1114
1121
|
return this.responseBodyTag
|
|
1115
1122
|
}
|
|
1116
1123
|
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
/**
|
|
1131
|
-
* Register a PORTABLE header-only response hook - one implementation, fast on every runtime.
|
|
1132
|
-
*
|
|
1133
|
-
* The hook receives a mutable case-insensitive header view, a minimal request view, and the
|
|
1134
|
-
* status. On the Web serving paths it runs inside the normal `onResponse` walk against the
|
|
1135
|
-
* response's own `Headers` (mutating in place - no clone). On Node it self-pairs as a native
|
|
1136
|
-
* response hook against the outcome's plain header record, so registering one NEVER forces the
|
|
1137
|
-
* Node adapter off its direct socket writer the way a full `onResponse(res: Response)` hook does.
|
|
1138
|
-
* Prefer this over `onResponse` whenever the middleware only reads/writes headers.
|
|
1139
|
-
*/
|
|
1140
|
-
onResponseHeaders(fn: ResponseHeadersHook): this {
|
|
1141
|
-
this.assertConfigurable("onResponseHeaders()")
|
|
1142
|
-
this.onResponseHooks.push(webResponseHeadersHook(fn))
|
|
1143
|
-
this.onNodeResponseHooks.push((response, req) =>
|
|
1144
|
-
fn(recordHeadersView(response), req, response.status),
|
|
1145
|
-
)
|
|
1146
|
-
return this
|
|
1147
|
-
}
|
|
1148
|
-
|
|
1149
|
-
/**
|
|
1150
|
-
* Register a PORTABLE post-serialization body hook - the payload tier. The hook receives the
|
|
1151
|
-
* FINAL framework-serialized bytes (plus the mutable header view and status) and may return
|
|
1152
|
-
* replacement bytes; `undefined` keeps the body. On the Node direct writer the bytes come
|
|
1153
|
-
* straight off the outcome record; on the Web serving paths they ride the framework-built
|
|
1154
|
-
* Response as a tag, so no body stream is ever drained on any runtime. A handler-returned raw
|
|
1155
|
-
* `Response` (proxied fetch, SSE, streamed SSR) is skipped by contract - transforming those is
|
|
1156
|
-
* what the full `onResponse` hook is for.
|
|
1157
|
-
*/
|
|
1158
|
-
onResponseBody(fn: ResponseBodyHook): this {
|
|
1159
|
-
this.assertConfigurable("onResponseBody()")
|
|
1160
|
-
this.enableResponseBodyTagging()
|
|
1161
|
-
this.onResponseHooks.push(webResponseBodyHook(fn, this.responseBodyOwners))
|
|
1162
|
-
this.onNodeResponseHooks.push((response, req) => {
|
|
1163
|
-
const body = response.body
|
|
1164
|
-
if (body === null) return undefined
|
|
1165
|
-
const out = fn(body, recordHeadersView(response), req, response.status)
|
|
1166
|
-
if (out instanceof Promise)
|
|
1167
|
-
return out.then((replaced) => applyBodyReplacement(response, replaced))
|
|
1168
|
-
applyBodyReplacement(response, out)
|
|
1169
|
-
return undefined
|
|
1170
|
-
})
|
|
1171
|
-
return this
|
|
1124
|
+
private responseObserverMethods(): ResponseObserverMethods {
|
|
1125
|
+
const methods = this as unknown as Partial<ResponseObserverMethods>
|
|
1126
|
+
if (
|
|
1127
|
+
typeof methods.onResponseHeaders !== "function" ||
|
|
1128
|
+
typeof methods.onResponseBody !== "function" ||
|
|
1129
|
+
typeof methods.onResponseRaw !== "function"
|
|
1130
|
+
) {
|
|
1131
|
+
throw new TypeError(
|
|
1132
|
+
"response observation requires `.use(responseObserver())` or an observer-enabled middleware",
|
|
1133
|
+
)
|
|
1134
|
+
}
|
|
1135
|
+
return methods as ResponseObserverMethods
|
|
1172
1136
|
}
|
|
1173
1137
|
|
|
1174
1138
|
/** Observe the terminal response after all transformations. Observers are ordered and fail-open. */
|
|
@@ -1180,14 +1144,8 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
1180
1144
|
return this
|
|
1181
1145
|
}
|
|
1182
1146
|
|
|
1183
|
-
/**
|
|
1184
|
-
|
|
1185
|
-
* registers routes/hooks but doesn't change the types, so this returns `this` with the route registry
|
|
1186
|
-
* and context fully intact. This overload exists specifically so a *named* identity plugin (e.g.
|
|
1187
|
-
* `@nifrajs/better-auth`) threads the registry: its `& { pluginName }` intersection would otherwise
|
|
1188
|
-
* defeat the generic inference of the transforming overload below and collapse the result to `any`.
|
|
1189
|
-
*/
|
|
1190
|
-
use(plugin: IdentityPlugin): this
|
|
1147
|
+
/** Enable the opt-in portable response observer methods. */
|
|
1148
|
+
use(plugin: ResponseObserverPlugin): this & ResponseObserverMethods
|
|
1191
1149
|
/**
|
|
1192
1150
|
* Apply a **context** plugin ({@link ContextPlugin}, from {@link defineContextPlugin}) - it adds `D`
|
|
1193
1151
|
* to the handler context and changes nothing else, so the route registry `R` and the existing context
|
|
@@ -1195,7 +1153,17 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
1195
1153
|
* `(app: this) => Out` overload below cannot infer through a *generic* plugin signature: it erases the
|
|
1196
1154
|
* plugin's own type parameters to their constraints, which silently widens `R` to `Registry`.
|
|
1197
1155
|
*/
|
|
1198
|
-
use<D extends object>(plugin: ContextPlugin<D>): Server<R, Ctx & D>
|
|
1156
|
+
use<D extends object>(plugin: ContextPlugin<D>): Server<R, Ctx & D, HookOutput>
|
|
1157
|
+
/**
|
|
1158
|
+
* Apply a type-**identity** plugin ({@link IdentityPlugin}, from {@link defineIdentityPlugin}) - it
|
|
1159
|
+
* registers routes/hooks but doesn't change the types, so this returns `this` with the route registry
|
|
1160
|
+
* and context fully intact. This overload exists specifically so a named identity plugin (e.g.
|
|
1161
|
+
* `@nifrajs/better-auth`) threads the registry: its `& { pluginName }` intersection would otherwise
|
|
1162
|
+
* defeat the generic inference of the transforming overload below and collapse the result to `any`.
|
|
1163
|
+
*/
|
|
1164
|
+
use<AddedHookOutput = never>(
|
|
1165
|
+
plugin: IdentityPlugin<AddedHookOutput>,
|
|
1166
|
+
): Server<R, Ctx, HookOutput | AddedHookOutput>
|
|
1199
1167
|
/**
|
|
1200
1168
|
* Apply a **plugin function** - `(app) => app`, typically built with {@link definePlugin}. It's
|
|
1201
1169
|
* called with `this` and its result is returned, so an inline plugin's `derive`/`decorate` thread
|
|
@@ -1219,6 +1187,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
1219
1187
|
* the last route reaches nothing - {@link ServerOptions.unusedScopedHooks} logs that at seal. A named
|
|
1220
1188
|
* bundle already applied is skipped (idempotent).
|
|
1221
1189
|
*/
|
|
1190
|
+
use<M extends Middleware>(mw: M): Server<R, Ctx, HookOutput | MiddlewareOutputOf<M>>
|
|
1222
1191
|
use(mw: Middleware): this
|
|
1223
1192
|
use(arg: Middleware | ((app: this) => AnyServer)): AnyServer {
|
|
1224
1193
|
this.assertConfigurable("use()")
|
|
@@ -1254,6 +1223,17 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
1254
1223
|
}
|
|
1255
1224
|
this.globalAssurance.push(...evidence.filter((item) => item.scope === "global"))
|
|
1256
1225
|
this.activeAssurance.push(...evidence.filter((item) => item.scope === "subsequent"))
|
|
1226
|
+
const responseObserver = (arg as unknown as Record<symbol, unknown>)[INSTALL_RESPONSE_OBSERVER]
|
|
1227
|
+
if (responseObserver !== undefined) {
|
|
1228
|
+
if (
|
|
1229
|
+
typeof responseObserver !== "object" ||
|
|
1230
|
+
responseObserver === null ||
|
|
1231
|
+
typeof (responseObserver as { install?: unknown }).install !== "function"
|
|
1232
|
+
) {
|
|
1233
|
+
throw new TypeError("response observer middleware has an invalid runtime")
|
|
1234
|
+
}
|
|
1235
|
+
this[INSTALL_RESPONSE_OBSERVER](responseObserver as ResponseObserverRuntime)
|
|
1236
|
+
}
|
|
1257
1237
|
if (arg.onRequest !== undefined) {
|
|
1258
1238
|
this.assertConfigurable("onRequest()")
|
|
1259
1239
|
this.onRequestHooks.push(arg.onRequest as RawOnRequest)
|
|
@@ -1277,9 +1257,12 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
1277
1257
|
// Before the bundle's own hooks: a bundle declaring both means its static values are the
|
|
1278
1258
|
// defaults its hook may then override, which is the order a single bundle reads in.
|
|
1279
1259
|
if (arg.responseHeaders !== undefined) this.responseHeaders(arg.responseHeaders)
|
|
1280
|
-
if (arg.onResponseHeaders !== undefined)
|
|
1281
|
-
|
|
1282
|
-
if (arg.
|
|
1260
|
+
if (arg.onResponseHeaders !== undefined)
|
|
1261
|
+
this.responseObserverMethods().onResponseHeaders(arg.onResponseHeaders)
|
|
1262
|
+
if (arg.onResponseBody !== undefined)
|
|
1263
|
+
this.responseObserverMethods().onResponseBody(arg.onResponseBody)
|
|
1264
|
+
if (arg.onResponseRaw !== undefined)
|
|
1265
|
+
this.responseObserverMethods().onResponseRaw(arg.onResponseRaw)
|
|
1283
1266
|
if (arg.onResponseFinalized !== undefined) this.onResponseFinalized(arg.onResponseFinalized)
|
|
1284
1267
|
if (arg.onError !== undefined) this.onError(arg.onError)
|
|
1285
1268
|
return this
|
|
@@ -1289,16 +1272,24 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
1289
1272
|
path: Path,
|
|
1290
1273
|
schema: S,
|
|
1291
1274
|
handler: H,
|
|
1292
|
-
): Server<
|
|
1275
|
+
): Server<
|
|
1276
|
+
AddRoute<R, "GET", Path, RouteInfoFor<Path, S, OutputOf<H>, HookOutput>>,
|
|
1277
|
+
Ctx,
|
|
1278
|
+
HookOutput
|
|
1279
|
+
>
|
|
1293
1280
|
get<Path extends string, H extends Handler<Path, RouteSchema, Ctx>>(
|
|
1294
1281
|
path: Path,
|
|
1295
1282
|
handler: H,
|
|
1296
|
-
): Server<
|
|
1283
|
+
): Server<
|
|
1284
|
+
AddRoute<R, "GET", Path, RouteInfoFor<Path, Record<never, never>, OutputOf<H>, HookOutput>>,
|
|
1285
|
+
Ctx,
|
|
1286
|
+
HookOutput
|
|
1287
|
+
>
|
|
1297
1288
|
get(
|
|
1298
1289
|
path: string,
|
|
1299
1290
|
schemaOrHandler: RouteSchema | ErasedHandler,
|
|
1300
1291
|
handler?: ErasedHandler,
|
|
1301
|
-
): Server<Registry, Ctx> {
|
|
1292
|
+
): Server<Registry, Ctx, HookOutput> {
|
|
1302
1293
|
return this.route("GET", path, schemaOrHandler, handler)
|
|
1303
1294
|
}
|
|
1304
1295
|
|
|
@@ -1331,12 +1322,17 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
1331
1322
|
stream: TypedSSEStream<InferOutput<S["sse"]>>,
|
|
1332
1323
|
) => void | Promise<void>,
|
|
1333
1324
|
init?: SSEInit,
|
|
1334
|
-
): Server<
|
|
1325
|
+
): Server<
|
|
1326
|
+
AddRoute<R, "GET", Path, RouteInfoFor<Path, S, Response, HookOutput>>,
|
|
1327
|
+
Ctx,
|
|
1328
|
+
HookOutput
|
|
1329
|
+
> {
|
|
1335
1330
|
const handler = (context: Context<Path, S> & Ctx): Response =>
|
|
1336
1331
|
requireSseRuntime(this.sseRuntime).response(context, (stream) => run(context, stream), init)
|
|
1337
1332
|
return this.route("GET", path, schema, handler as unknown as ErasedHandler) as Server<
|
|
1338
|
-
AddRoute<R, "GET", Path, RouteInfoFor<Path, S, Response>>,
|
|
1339
|
-
Ctx
|
|
1333
|
+
AddRoute<R, "GET", Path, RouteInfoFor<Path, S, Response, HookOutput>>,
|
|
1334
|
+
Ctx,
|
|
1335
|
+
HookOutput
|
|
1340
1336
|
>
|
|
1341
1337
|
}
|
|
1342
1338
|
|
|
@@ -1344,16 +1340,24 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
1344
1340
|
path: Path,
|
|
1345
1341
|
schema: S,
|
|
1346
1342
|
handler: H,
|
|
1347
|
-
): Server<
|
|
1343
|
+
): Server<
|
|
1344
|
+
AddRoute<R, "POST", Path, RouteInfoFor<Path, S, OutputOf<H>, HookOutput>>,
|
|
1345
|
+
Ctx,
|
|
1346
|
+
HookOutput
|
|
1347
|
+
>
|
|
1348
1348
|
post<Path extends string, H extends Handler<Path, RouteSchema, Ctx>>(
|
|
1349
1349
|
path: Path,
|
|
1350
1350
|
handler: H,
|
|
1351
|
-
): Server<
|
|
1351
|
+
): Server<
|
|
1352
|
+
AddRoute<R, "POST", Path, RouteInfoFor<Path, Record<never, never>, OutputOf<H>, HookOutput>>,
|
|
1353
|
+
Ctx,
|
|
1354
|
+
HookOutput
|
|
1355
|
+
>
|
|
1352
1356
|
post(
|
|
1353
1357
|
path: string,
|
|
1354
1358
|
schemaOrHandler: RouteSchema | ErasedHandler,
|
|
1355
1359
|
handler?: ErasedHandler,
|
|
1356
|
-
): Server<Registry, Ctx> {
|
|
1360
|
+
): Server<Registry, Ctx, HookOutput> {
|
|
1357
1361
|
return this.route("POST", path, schemaOrHandler, handler)
|
|
1358
1362
|
}
|
|
1359
1363
|
|
|
@@ -1361,16 +1365,24 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
1361
1365
|
path: Path,
|
|
1362
1366
|
schema: S,
|
|
1363
1367
|
handler: H,
|
|
1364
|
-
): Server<
|
|
1368
|
+
): Server<
|
|
1369
|
+
AddRoute<R, "PUT", Path, RouteInfoFor<Path, S, OutputOf<H>, HookOutput>>,
|
|
1370
|
+
Ctx,
|
|
1371
|
+
HookOutput
|
|
1372
|
+
>
|
|
1365
1373
|
put<Path extends string, H extends Handler<Path, RouteSchema, Ctx>>(
|
|
1366
1374
|
path: Path,
|
|
1367
1375
|
handler: H,
|
|
1368
|
-
): Server<
|
|
1376
|
+
): Server<
|
|
1377
|
+
AddRoute<R, "PUT", Path, RouteInfoFor<Path, Record<never, never>, OutputOf<H>, HookOutput>>,
|
|
1378
|
+
Ctx,
|
|
1379
|
+
HookOutput
|
|
1380
|
+
>
|
|
1369
1381
|
put(
|
|
1370
1382
|
path: string,
|
|
1371
1383
|
schemaOrHandler: RouteSchema | ErasedHandler,
|
|
1372
1384
|
handler?: ErasedHandler,
|
|
1373
|
-
): Server<Registry, Ctx> {
|
|
1385
|
+
): Server<Registry, Ctx, HookOutput> {
|
|
1374
1386
|
return this.route("PUT", path, schemaOrHandler, handler)
|
|
1375
1387
|
}
|
|
1376
1388
|
|
|
@@ -1378,16 +1390,24 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
1378
1390
|
path: Path,
|
|
1379
1391
|
schema: S,
|
|
1380
1392
|
handler: H,
|
|
1381
|
-
): Server<
|
|
1393
|
+
): Server<
|
|
1394
|
+
AddRoute<R, "PATCH", Path, RouteInfoFor<Path, S, OutputOf<H>, HookOutput>>,
|
|
1395
|
+
Ctx,
|
|
1396
|
+
HookOutput
|
|
1397
|
+
>
|
|
1382
1398
|
patch<Path extends string, H extends Handler<Path, RouteSchema, Ctx>>(
|
|
1383
1399
|
path: Path,
|
|
1384
1400
|
handler: H,
|
|
1385
|
-
): Server<
|
|
1401
|
+
): Server<
|
|
1402
|
+
AddRoute<R, "PATCH", Path, RouteInfoFor<Path, Record<never, never>, OutputOf<H>, HookOutput>>,
|
|
1403
|
+
Ctx,
|
|
1404
|
+
HookOutput
|
|
1405
|
+
>
|
|
1386
1406
|
patch(
|
|
1387
1407
|
path: string,
|
|
1388
1408
|
schemaOrHandler: RouteSchema | ErasedHandler,
|
|
1389
1409
|
handler?: ErasedHandler,
|
|
1390
|
-
): Server<Registry, Ctx> {
|
|
1410
|
+
): Server<Registry, Ctx, HookOutput> {
|
|
1391
1411
|
return this.route("PATCH", path, schemaOrHandler, handler)
|
|
1392
1412
|
}
|
|
1393
1413
|
|
|
@@ -1395,16 +1415,24 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
1395
1415
|
path: Path,
|
|
1396
1416
|
schema: S,
|
|
1397
1417
|
handler: H,
|
|
1398
|
-
): Server<
|
|
1418
|
+
): Server<
|
|
1419
|
+
AddRoute<R, "DELETE", Path, RouteInfoFor<Path, S, OutputOf<H>, HookOutput>>,
|
|
1420
|
+
Ctx,
|
|
1421
|
+
HookOutput
|
|
1422
|
+
>
|
|
1399
1423
|
delete<Path extends string, H extends Handler<Path, RouteSchema, Ctx>>(
|
|
1400
1424
|
path: Path,
|
|
1401
1425
|
handler: H,
|
|
1402
|
-
): Server<
|
|
1426
|
+
): Server<
|
|
1427
|
+
AddRoute<R, "DELETE", Path, RouteInfoFor<Path, Record<never, never>, OutputOf<H>, HookOutput>>,
|
|
1428
|
+
Ctx,
|
|
1429
|
+
HookOutput
|
|
1430
|
+
>
|
|
1403
1431
|
delete(
|
|
1404
1432
|
path: string,
|
|
1405
1433
|
schemaOrHandler: RouteSchema | ErasedHandler,
|
|
1406
1434
|
handler?: ErasedHandler,
|
|
1407
|
-
): Server<Registry, Ctx> {
|
|
1435
|
+
): Server<Registry, Ctx, HookOutput> {
|
|
1408
1436
|
return this.route("DELETE", path, schemaOrHandler, handler)
|
|
1409
1437
|
}
|
|
1410
1438
|
|
|
@@ -1447,10 +1475,12 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
1447
1475
|
S["output"] extends StandardSchemaV1
|
|
1448
1476
|
? { body: S["input"]; response: S["output"] }
|
|
1449
1477
|
: { body: S["input"] },
|
|
1450
|
-
OutputOf<H
|
|
1478
|
+
OutputOf<H>,
|
|
1479
|
+
HookOutput
|
|
1451
1480
|
>
|
|
1452
1481
|
>,
|
|
1453
|
-
Ctx
|
|
1482
|
+
Ctx,
|
|
1483
|
+
HookOutput
|
|
1454
1484
|
>
|
|
1455
1485
|
tool(
|
|
1456
1486
|
name: string,
|
|
@@ -1461,7 +1491,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
1461
1491
|
annotations?: ToolAnnotations
|
|
1462
1492
|
},
|
|
1463
1493
|
handler: (input: unknown, ctx: Context & Ctx) => unknown,
|
|
1464
|
-
): Server<Registry, Ctx> {
|
|
1494
|
+
): Server<Registry, Ctx, HookOutput> {
|
|
1465
1495
|
const plan = requireMcpRuntime(this.mcpRuntime).tool(
|
|
1466
1496
|
name,
|
|
1467
1497
|
config,
|
|
@@ -1474,7 +1504,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
1474
1504
|
if (lastRoute) {
|
|
1475
1505
|
;(lastRoute as { tool?: RouteDescriptor["tool"] }).tool = plan.descriptor
|
|
1476
1506
|
}
|
|
1477
|
-
return this as unknown as Server<Registry, Ctx>
|
|
1507
|
+
return this as unknown as Server<Registry, Ctx, HookOutput>
|
|
1478
1508
|
}
|
|
1479
1509
|
|
|
1480
1510
|
/**
|
|
@@ -1487,7 +1517,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
1487
1517
|
uri: string,
|
|
1488
1518
|
config: { readonly name: string; readonly description?: string; readonly mimeType?: string },
|
|
1489
1519
|
read: McpResourceDescriptor["read"],
|
|
1490
|
-
): Server<R, Ctx> {
|
|
1520
|
+
): Server<R, Ctx, HookOutput> {
|
|
1491
1521
|
this.assertConfigurable("resource()")
|
|
1492
1522
|
this.mcpResourceList.push(requireMcpRuntime(this.mcpRuntime).resource(uri, config, read))
|
|
1493
1523
|
return this
|
|
@@ -1501,7 +1531,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
1501
1531
|
name: string,
|
|
1502
1532
|
config: { readonly description: string; readonly arguments?: readonly PromptArgument[] },
|
|
1503
1533
|
handler: McpPromptDescriptor["handler"],
|
|
1504
|
-
): Server<R, Ctx> {
|
|
1534
|
+
): Server<R, Ctx, HookOutput> {
|
|
1505
1535
|
this.assertConfigurable("prompt()")
|
|
1506
1536
|
this.mcpPromptList.push(requireMcpRuntime(this.mcpRuntime).prompt(name, config, handler))
|
|
1507
1537
|
return this
|
|
@@ -1541,12 +1571,21 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
1541
1571
|
path: Path,
|
|
1542
1572
|
handler: WebSocketHandler<Data, EnvOf<Ctx>, Schema, Send>,
|
|
1543
1573
|
): string extends Path
|
|
1544
|
-
? Server<R, Ctx>
|
|
1545
|
-
: Server<AddRoute<R, "WS", Path, WsRouteInfoFor<Path, Schema, Send>>, Ctx> {
|
|
1574
|
+
? Server<R, Ctx, HookOutput>
|
|
1575
|
+
: Server<AddRoute<R, "WS", Path, WsRouteInfoFor<Path, Schema, Send>>, Ctx, HookOutput> {
|
|
1546
1576
|
this.assertConfigurable("ws()")
|
|
1547
1577
|
// Boot-time guard: the WS runtime is a subpath (`@nifrajs/core/ws`) so no-WebSocket apps don't
|
|
1548
1578
|
// bundle it. Registration is the loud, early failure point - never the first connection.
|
|
1549
1579
|
const runtime = requireWsRuntime(this.wsRuntime)
|
|
1580
|
+
if (handler.validateSend === true && handler.sendSchema === undefined) {
|
|
1581
|
+
throw new RouteConfigError(
|
|
1582
|
+
"INVALID_WS_SEND_VALIDATION",
|
|
1583
|
+
`route WS ${path}: validateSend requires sendSchema`,
|
|
1584
|
+
)
|
|
1585
|
+
}
|
|
1586
|
+
// A validated-send route makes broadcast bytes route-dependent (drop-on-invalid per socket), so the
|
|
1587
|
+
// whole app forgoes native pub/sub and keeps the JS registry loop that runs each send through the sender.
|
|
1588
|
+
if (handler.validateSend === true) this.wsHasValidatedSend = true
|
|
1550
1589
|
this.topics ??= runtime.createTopics()
|
|
1551
1590
|
// A `messageSchema` wraps `message` with validation once, here - every adapter then dispatches
|
|
1552
1591
|
// already-validated, typed messages (Bun/Deno/Node/Workers) with no per-adapter code.
|
|
@@ -1563,6 +1602,12 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
1563
1602
|
* an external fan-out (Redis, a Durable Object) to this. A no-op when nobody is subscribed.
|
|
1564
1603
|
*/
|
|
1565
1604
|
publish(topic: string, data: string | ArrayBufferView | ArrayBuffer): void {
|
|
1605
|
+
// Native-pubsub mode (Bun, no validated-send route): one native broadcast to every subscriber, no
|
|
1606
|
+
// per-connection JS loop. Bound only after `listen()`; before that (or off Bun) the registry runs.
|
|
1607
|
+
if (this.nativePublish !== undefined) {
|
|
1608
|
+
this.nativePublish(topic, data)
|
|
1609
|
+
return
|
|
1610
|
+
}
|
|
1566
1611
|
// No `app.ws()` yet ⇒ no registry and necessarily no subscribers - a publish is a no-op anyway.
|
|
1567
1612
|
this.topics?.publish(topic, data)
|
|
1568
1613
|
}
|
|
@@ -1572,7 +1617,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
1572
1617
|
path: string,
|
|
1573
1618
|
schemaOrHandler: RouteSchema | ErasedHandler,
|
|
1574
1619
|
handler?: ErasedHandler,
|
|
1575
|
-
): Server<Registry, Ctx> {
|
|
1620
|
+
): Server<Registry, Ctx, HookOutput> {
|
|
1576
1621
|
let rawHandler: ErasedHandler
|
|
1577
1622
|
let schema: RouteSchema | undefined
|
|
1578
1623
|
if (handler !== undefined) {
|
|
@@ -1585,7 +1630,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
1585
1630
|
this.register(method, path, schema, rawHandler)
|
|
1586
1631
|
// The accumulated registry type is compile-time only; the same instance
|
|
1587
1632
|
// carries every route, so the public methods re-type `this` per call.
|
|
1588
|
-
return this as unknown as Server<Registry, Ctx>
|
|
1633
|
+
return this as unknown as Server<Registry, Ctx, HookOutput>
|
|
1589
1634
|
}
|
|
1590
1635
|
|
|
1591
1636
|
/**
|
|
@@ -1626,245 +1671,143 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
1626
1671
|
schema: RouteSchema | undefined,
|
|
1627
1672
|
handler: (context: never) => unknown,
|
|
1628
1673
|
): CatalogRoute {
|
|
1629
|
-
const
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
} catch {
|
|
1649
|
-
invalidBodyLimit('bodyLimit must be a non-negative safe integer or "unlimited"')
|
|
1650
|
-
}
|
|
1651
|
-
bodyLimit = schema.bodyLimit
|
|
1652
|
-
}
|
|
1653
|
-
const capabilities = normalizeRouteCapabilities(schema?.capabilities)
|
|
1654
|
-
const handlerAssurance = assuranceDeclarationsOf(handler as unknown as object)
|
|
1655
|
-
const invalidHandlerScope = handlerAssurance.find(
|
|
1656
|
-
(declaration) => declaration.scope !== "plugin",
|
|
1657
|
-
)
|
|
1658
|
-
if (invalidHandlerScope !== undefined) {
|
|
1659
|
-
throw new RouteConfigError(
|
|
1660
|
-
"INVALID_ASSURANCE",
|
|
1661
|
-
`route handler assurance must use plugin scope (received ${invalidHandlerScope.scope})`,
|
|
1662
|
-
)
|
|
1663
|
-
}
|
|
1664
|
-
const authenticated = assuranceEvidenceFor(
|
|
1665
|
-
[...this.activeAssurance, ...handlerAssurance, ...this.globalAssurance],
|
|
1666
|
-
method,
|
|
1667
|
-
path,
|
|
1668
|
-
).some((evidence) => evidence.id === NIFRA_ASSURANCE_IDS.AUTHENTICATED)
|
|
1669
|
-
const routeDecorations: Record<PropertyKey, unknown> = { ...this.decorations }
|
|
1670
|
-
if (capabilities.length > 0) {
|
|
1671
|
-
routeDecorations[CAPABILITY_GUARD] = createCapabilityGuard(
|
|
1672
|
-
capabilities,
|
|
1673
|
-
method,
|
|
1674
|
-
path,
|
|
1675
|
-
this.onCapabilityUse,
|
|
1676
|
-
this.idempotencyRuntime?.trackEffect,
|
|
1677
|
-
Object.freeze([...this.capabilityInterceptors]),
|
|
1678
|
-
Object.freeze([...this.capabilityObservers]),
|
|
1679
|
-
)
|
|
1680
|
-
}
|
|
1681
|
-
const hasDecorations = Reflect.ownKeys(routeDecorations).length > 0
|
|
1682
|
-
// An idempotency route runs a dedupe lane that must buffer the body and capture the response, so it
|
|
1683
|
-
// never takes the fused/native fast path - force it onto the portable matched lane (which routes
|
|
1684
|
-
// through `fetchMatched`, where the dedupe wrapper lives). Fail closed: a route may not declare
|
|
1685
|
-
// idempotency unless the idempotency runtime is installed, so the safety gate can never be silently
|
|
1686
|
-
// dropped by a missing plugin.
|
|
1687
|
-
if (schema?.idempotency !== undefined && this.idempotencyRuntime === undefined) {
|
|
1688
|
-
throw new RouteConfigError(
|
|
1689
|
-
"INVALID_IDEMPOTENCY",
|
|
1690
|
-
"route declares idempotency but the idempotency plugin is not installed; add .use(idempotency())",
|
|
1691
|
-
)
|
|
1692
|
-
}
|
|
1693
|
-
const idempotent = this.idempotencyRuntime?.resolve(schema, authenticated, this.maxBodyBytes)
|
|
1694
|
-
// A ledgered route (capabilities declared + `.use(effectLedger())`) needs a per-request
|
|
1695
|
-
// context to carry the ledger and a settle step to seal + sink it, so it too leaves the
|
|
1696
|
-
// fused/contextless fast path. Resolved per route, at registration - like the capability guard.
|
|
1697
|
-
const ledgered: ResolvedEffectLedger | undefined = this.effectLedgerRuntime?.resolve(
|
|
1698
|
-
capabilities,
|
|
1674
|
+
const compiled = compileRouteOptions(
|
|
1675
|
+
{
|
|
1676
|
+
maxBodyBytes: this.maxBodyBytes,
|
|
1677
|
+
activeAssurance: this.activeAssurance,
|
|
1678
|
+
globalAssurance: this.globalAssurance,
|
|
1679
|
+
decorations: this.decorations,
|
|
1680
|
+
onCapabilityUse: this.onCapabilityUse,
|
|
1681
|
+
capabilityInterceptors: this.capabilityInterceptors,
|
|
1682
|
+
capabilityObservers: this.capabilityObservers,
|
|
1683
|
+
effectLedgerRuntime: this.effectLedgerRuntime,
|
|
1684
|
+
idempotencyRuntime: this.idempotencyRuntime,
|
|
1685
|
+
responseContractRuntime: this.responseContractRuntime,
|
|
1686
|
+
derives: this.derives,
|
|
1687
|
+
beforeHandleHooks: this.beforeHandleHooks,
|
|
1688
|
+
afterHandleHooks: this.afterHandleHooks,
|
|
1689
|
+
onErrorHooks: this.onErrorHooks,
|
|
1690
|
+
aroundHooks: this.aroundHooks,
|
|
1691
|
+
defaultOnValidationError: this.defaultOnValidationError,
|
|
1692
|
+
},
|
|
1699
1693
|
method,
|
|
1700
1694
|
path,
|
|
1695
|
+
schema,
|
|
1696
|
+
handler,
|
|
1701
1697
|
)
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
schema?.body === undefined &&
|
|
1716
|
-
schema?.query === undefined &&
|
|
1717
|
-
idempotent === undefined &&
|
|
1718
|
-
ledgered === undefined &&
|
|
1719
|
-
contracted === undefined &&
|
|
1720
|
-
this.derives.length === 0 &&
|
|
1721
|
-
this.beforeHandleHooks.length === 0 &&
|
|
1722
|
-
this.afterHandleHooks.length === 0 &&
|
|
1723
|
-
this.onErrorHooks.length === 0
|
|
1724
|
-
// A route whose ONLY lifecycle step is a query schema can fuse too: the parse + validate +
|
|
1725
|
-
// handler + respond collapse into one closure with no lifecycle promise on the sync path. The
|
|
1726
|
-
// guards mirror the `query` lane below PLUS everything the fused dispatch skips: around hooks,
|
|
1727
|
-
// the idempotency/ledger wrappers, and validation-error recovery (schema or server default) -
|
|
1728
|
-
// the fused invalid path is exactly `validationError(issues)`, so any recovery semantics keep
|
|
1729
|
-
// the generic lane.
|
|
1730
|
-
const fusedQuery =
|
|
1731
|
-
!bare &&
|
|
1732
|
-
contracted === undefined &&
|
|
1733
|
-
schema?.query !== undefined &&
|
|
1734
|
-
schema.body === undefined &&
|
|
1735
|
-
schema.params === undefined &&
|
|
1736
|
-
schema.headers === undefined &&
|
|
1737
|
-
schema.onValidationError === undefined &&
|
|
1738
|
-
this.defaultOnValidationError === undefined &&
|
|
1739
|
-
idempotent === undefined &&
|
|
1740
|
-
ledgered === undefined &&
|
|
1741
|
-
this.derives.length === 0 &&
|
|
1742
|
-
this.beforeHandleHooks.length === 0 &&
|
|
1743
|
-
this.afterHandleHooks.length === 0 &&
|
|
1744
|
-
this.onErrorHooks.length === 0 &&
|
|
1745
|
-
this.aroundHooks.length === 0
|
|
1746
|
-
const bodyOnly =
|
|
1747
|
-
contracted === undefined &&
|
|
1748
|
-
schema?.body !== undefined &&
|
|
1749
|
-
schema.query === undefined &&
|
|
1750
|
-
schema.params === undefined &&
|
|
1751
|
-
schema.headers === undefined &&
|
|
1752
|
-
this.derives.length === 0 &&
|
|
1753
|
-
this.beforeHandleHooks.length === 0 &&
|
|
1754
|
-
this.afterHandleHooks.length === 0 &&
|
|
1755
|
-
this.onErrorHooks.length === 0
|
|
1756
|
-
const fusedBody =
|
|
1757
|
-
!bare &&
|
|
1758
|
-
bodyOnly &&
|
|
1759
|
-
schema.onValidationError === undefined &&
|
|
1760
|
-
this.defaultOnValidationError === undefined &&
|
|
1761
|
-
idempotent === undefined &&
|
|
1762
|
-
ledgered === undefined &&
|
|
1763
|
-
this.aroundHooks.length === 0
|
|
1764
|
-
// Body-only routes still need an async body read, but eligible routes can compile the validation +
|
|
1765
|
-
// handler continuation once. The runner keeps the bounded parser and all error semantics while
|
|
1766
|
-
// avoiding the generic entry/schema/lifecycle dispatch on Bun, Deno, and Node-direct.
|
|
1698
|
+
const {
|
|
1699
|
+
pattern,
|
|
1700
|
+
bodyLimit,
|
|
1701
|
+
capabilities,
|
|
1702
|
+
routeDecorations,
|
|
1703
|
+
hasDecorations,
|
|
1704
|
+
idempotent,
|
|
1705
|
+
ledgered,
|
|
1706
|
+
responseContract,
|
|
1707
|
+
lanes,
|
|
1708
|
+
routeAssurance,
|
|
1709
|
+
} = compiled
|
|
1710
|
+
const { bare, fusedQuery, fusedBody } = lanes
|
|
1767
1711
|
const fusedBodyRunner = fusedBody
|
|
1768
1712
|
? this.buildFusedBodyRunner(
|
|
1769
1713
|
handler as unknown as InternalHandler,
|
|
1770
|
-
schema
|
|
1714
|
+
schema?.body as StandardSchemaV1,
|
|
1771
1715
|
hasDecorations ? routeDecorations : undefined,
|
|
1772
|
-
bodyLimit ??
|
|
1716
|
+
bodyLimit ?? UNLIMITED_BODY_BYTES,
|
|
1773
1717
|
)
|
|
1774
1718
|
: undefined
|
|
1719
|
+
// Fused lifecycle lanes: derive + before (with or without an after), and body + the same shape.
|
|
1720
|
+
// The lane selectors in `selectRouteLanes` are exhaustive about the lifecycleHookLane /
|
|
1721
|
+
// body-derive-before-after eligibility, so we only need to check the lane and the hook counts.
|
|
1722
|
+
const lifecycleHookLane = lanes.lifecycleHookLane
|
|
1723
|
+
const isFusedLifecycle = lifecycleHookLane !== undefined && this.derives.length === 1
|
|
1724
|
+
const isFusedBodyLifecycle =
|
|
1725
|
+
lanes.fusedLane === "body-derive-before" || lanes.fusedLane === "body-derive-before-after"
|
|
1775
1726
|
const fusedWeb =
|
|
1776
1727
|
bare && this.aroundHooks.length === 0
|
|
1777
1728
|
? this.buildFusedWeb(
|
|
1778
1729
|
handler as unknown as InternalHandler,
|
|
1779
1730
|
hasDecorations ? routeDecorations : undefined,
|
|
1780
1731
|
isContextlessNoArgArrow(handler),
|
|
1781
|
-
bodyLimit ??
|
|
1732
|
+
bodyLimit ?? UNLIMITED_BODY_BYTES,
|
|
1782
1733
|
)
|
|
1783
1734
|
: fusedQuery
|
|
1784
1735
|
? this.buildFusedQueryWeb(
|
|
1785
1736
|
handler as unknown as InternalHandler,
|
|
1786
1737
|
hasDecorations ? routeDecorations : undefined,
|
|
1787
|
-
schema
|
|
1788
|
-
bodyLimit ??
|
|
1738
|
+
schema?.query as StandardSchemaV1,
|
|
1739
|
+
bodyLimit ?? UNLIMITED_BODY_BYTES,
|
|
1789
1740
|
)
|
|
1790
1741
|
: fusedBody
|
|
1791
1742
|
? this.buildFusedBodyWeb(fusedBodyRunner as FusedBodyRunner)
|
|
1792
|
-
:
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
this.
|
|
1834
|
-
this.beforeHandleHooks.length === 1 &&
|
|
1835
|
-
this.afterHandleHooks.length === 0 &&
|
|
1836
|
-
this.onErrorHooks.length === 0
|
|
1837
|
-
? "derive-before"
|
|
1838
|
-
: undefined
|
|
1743
|
+
: isFusedBodyLifecycle
|
|
1744
|
+
? this.buildFusedBodyDeriveBeforeAfter(
|
|
1745
|
+
handler as unknown as InternalHandler,
|
|
1746
|
+
this.derives[0]!,
|
|
1747
|
+
this.beforeHandleHooks[0]!,
|
|
1748
|
+
this.afterHandleHooks[0],
|
|
1749
|
+
schema?.body as StandardSchemaV1,
|
|
1750
|
+
bodyLimit ?? UNLIMITED_BODY_BYTES,
|
|
1751
|
+
)
|
|
1752
|
+
: isFusedLifecycle && lifecycleHookLane === "derive-before"
|
|
1753
|
+
? this.buildFusedDeriveBefore(
|
|
1754
|
+
handler as unknown as InternalHandler,
|
|
1755
|
+
this.derives[0]!,
|
|
1756
|
+
this.beforeHandleHooks[0]!,
|
|
1757
|
+
schema?.query,
|
|
1758
|
+
bodyLimit ?? UNLIMITED_BODY_BYTES,
|
|
1759
|
+
)
|
|
1760
|
+
: isFusedLifecycle && lifecycleHookLane === "derive-before-after"
|
|
1761
|
+
? this.buildFusedDeriveBeforeAfter(
|
|
1762
|
+
handler as unknown as InternalHandler,
|
|
1763
|
+
this.derives[0]!,
|
|
1764
|
+
this.beforeHandleHooks[0]!,
|
|
1765
|
+
this.afterHandleHooks[0]!,
|
|
1766
|
+
schema?.query,
|
|
1767
|
+
bodyLimit ?? UNLIMITED_BODY_BYTES,
|
|
1768
|
+
)
|
|
1769
|
+
: undefined
|
|
1770
|
+
const program = compileRouteProgram({
|
|
1771
|
+
schema,
|
|
1772
|
+
handler: handler as unknown as InternalHandler,
|
|
1773
|
+
derives: this.derives,
|
|
1774
|
+
beforeHandle: this.beforeHandleHooks,
|
|
1775
|
+
afterHandle: this.afterHandleHooks,
|
|
1776
|
+
onError: this.onErrorHooks,
|
|
1777
|
+
decorations: routeDecorations,
|
|
1778
|
+
hasDecorations,
|
|
1779
|
+
bodySchema: schema?.body,
|
|
1780
|
+
bodyLimit,
|
|
1781
|
+
responseContract,
|
|
1782
|
+
})
|
|
1783
|
+
const contextless =
|
|
1784
|
+
lanes.bare && this.aroundHooks.length === 0 && isContextlessNoArgArrow(handler)
|
|
1839
1785
|
const execution = compileRouteExecutionPlan({
|
|
1840
|
-
lane,
|
|
1786
|
+
lane: lanes.lane,
|
|
1841
1787
|
contextless,
|
|
1842
1788
|
hasAround: this.aroundHooks.length > 0,
|
|
1843
1789
|
hasLedger: ledgered !== undefined,
|
|
1844
|
-
lifecycleLane,
|
|
1845
1790
|
fusedWeb,
|
|
1846
1791
|
fusedBody: fusedBodyRunner,
|
|
1847
|
-
fusedLane:
|
|
1848
|
-
fusedWeb === undefined ? undefined : fusedQuery ? "query" : fusedBody ? "body" : "bare",
|
|
1792
|
+
fusedLane: lanes.fusedLane,
|
|
1849
1793
|
})
|
|
1850
1794
|
const registeredEntry: RouteEntry = {
|
|
1851
|
-
// (context: never) => unknown -> InternalHandler: the framework invokes it
|
|
1852
|
-
// with the concrete RawContext the typed handler expects, so this is sound.
|
|
1853
1795
|
handler: handler as unknown as InternalHandler,
|
|
1854
1796
|
schema,
|
|
1855
1797
|
bodyLimit,
|
|
1856
1798
|
idempotent,
|
|
1857
1799
|
ledgered,
|
|
1858
|
-
responseContract
|
|
1800
|
+
responseContract,
|
|
1859
1801
|
derives: [...this.derives],
|
|
1860
1802
|
decorations: routeDecorations,
|
|
1861
1803
|
hasDecorations,
|
|
1862
1804
|
beforeHandle: [...this.beforeHandleHooks],
|
|
1863
1805
|
afterHandle: [...this.afterHandleHooks],
|
|
1864
1806
|
onError: [...this.onErrorHooks],
|
|
1865
|
-
lifecycleHookLane,
|
|
1807
|
+
lifecycleHookLane: lanes.lifecycleHookLane,
|
|
1866
1808
|
around: [...this.aroundHooks],
|
|
1867
1809
|
execution,
|
|
1810
|
+
program,
|
|
1868
1811
|
}
|
|
1869
1812
|
const descriptor: RouteDescriptor = {
|
|
1870
1813
|
method,
|
|
@@ -1873,61 +1816,13 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
1873
1816
|
...(capabilities.length > 0 ? { capabilities } : {}),
|
|
1874
1817
|
...(schema?.family === true ? { family: true } : {}),
|
|
1875
1818
|
}
|
|
1876
|
-
const routeAssurance: AssuranceDeclaration[] = [...this.activeAssurance, ...handlerAssurance]
|
|
1877
|
-
if (contracted?.runtime.mode === "enforce" && schema?.response !== undefined) {
|
|
1878
|
-
routeAssurance.push(
|
|
1879
|
-
Object.freeze({
|
|
1880
|
-
id: NIFRA_ASSURANCE_IDS.RESPONSE_CONTRACT,
|
|
1881
|
-
source: "response-contract",
|
|
1882
|
-
scope: "plugin",
|
|
1883
|
-
}),
|
|
1884
|
-
)
|
|
1885
|
-
}
|
|
1886
|
-
// Inline `schema.assurance`: the route DECLARES its enforcement evidence adjacent to the handler, so an
|
|
1887
|
-
// in-handler-guarded route satisfies a policy `require:` clause without a `withRouteAssurance` middleware
|
|
1888
|
-
// rewrite. Each id becomes route-scoped `declared` evidence (invalid ids fail closed at registration).
|
|
1889
|
-
for (const id of schema?.assurance ?? []) {
|
|
1890
|
-
if (!validEvidenceId(id)) {
|
|
1891
|
-
throw new Error(
|
|
1892
|
-
`route assurance: invalid evidence id ${JSON.stringify(id)} on ${method} ${path} (use lowercase dot/dash segments)`,
|
|
1893
|
-
)
|
|
1894
|
-
}
|
|
1895
|
-
const declared = { id, source: "declared", scope: "plugin" } as AssuranceDeclaration
|
|
1896
|
-
Object.defineProperty(declared, "provenance", {
|
|
1897
|
-
value: "declared",
|
|
1898
|
-
enumerable: false,
|
|
1899
|
-
configurable: false,
|
|
1900
|
-
writable: false,
|
|
1901
|
-
})
|
|
1902
|
-
routeAssurance.push(Object.freeze(declared))
|
|
1903
|
-
}
|
|
1904
|
-
if (schema?.body !== undefined) {
|
|
1905
|
-
routeAssurance.push(
|
|
1906
|
-
Object.freeze({
|
|
1907
|
-
id: NIFRA_ASSURANCE_IDS.BODY_BOUNDED,
|
|
1908
|
-
source: "route-schema",
|
|
1909
|
-
scope: "plugin",
|
|
1910
|
-
}),
|
|
1911
|
-
)
|
|
1912
|
-
}
|
|
1913
|
-
// Declaring `schema.idempotency` is evidence for request replay only. It deliberately never proves
|
|
1914
|
-
// durable command execution; that stronger evidence belongs to a command/outbox adapter.
|
|
1915
|
-
if (schema?.idempotency !== undefined) {
|
|
1916
|
-
routeAssurance.push(
|
|
1917
|
-
Object.freeze({
|
|
1918
|
-
id: NIFRA_ASSURANCE_IDS.IDEMPOTENCY_KEY,
|
|
1919
|
-
source: "route-schema",
|
|
1920
|
-
scope: "plugin",
|
|
1921
|
-
}),
|
|
1922
|
-
)
|
|
1923
|
-
}
|
|
1924
1819
|
return {
|
|
1925
1820
|
method,
|
|
1926
1821
|
path,
|
|
1927
1822
|
pattern,
|
|
1928
1823
|
entry: registeredEntry,
|
|
1929
1824
|
descriptor,
|
|
1930
|
-
assurance:
|
|
1825
|
+
assurance: routeAssurance,
|
|
1931
1826
|
}
|
|
1932
1827
|
}
|
|
1933
1828
|
|
|
@@ -1961,6 +1856,29 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
1961
1856
|
this.responseContractRuntime = runtime
|
|
1962
1857
|
}
|
|
1963
1858
|
|
|
1859
|
+
/** @internal Symbol-keyed install seam for the response observer plugin. */
|
|
1860
|
+
[INSTALL_RESPONSE_OBSERVER](runtime: ResponseObserverRuntime): ResponseObserverMethods {
|
|
1861
|
+
this.assertConfigurable("responseObserver()")
|
|
1862
|
+
const methods = runtime.install({
|
|
1863
|
+
assertConfigurable: (operation) => this.assertConfigurable(operation),
|
|
1864
|
+
addResponseHook: (web, node) => {
|
|
1865
|
+
this.onResponseHooks.push(web)
|
|
1866
|
+
this.onNodeResponseHooks.push(node)
|
|
1867
|
+
if (node === undefined) this.nodeResponseHooksComplete = false
|
|
1868
|
+
},
|
|
1869
|
+
enableResponseBodyTagging: () => {
|
|
1870
|
+
if (this.responseBodyTag === undefined) {
|
|
1871
|
+
this.responseBodyTag = Object.freeze({})
|
|
1872
|
+
this.responseBodyOwners.add(this.responseBodyTag)
|
|
1873
|
+
}
|
|
1874
|
+
return this.responseBodyTag
|
|
1875
|
+
},
|
|
1876
|
+
responseBodyOwners: () => this.responseBodyOwners,
|
|
1877
|
+
})
|
|
1878
|
+
Object.assign(this, methods)
|
|
1879
|
+
return methods
|
|
1880
|
+
}
|
|
1881
|
+
|
|
1964
1882
|
/** @internal Symbol-keyed install seam for the `idempotency()` plugin. Off the public typed surface. */
|
|
1965
1883
|
[INSTALL_IDEMPOTENCY](runtime: IdempotencyRuntime): void {
|
|
1966
1884
|
this.assertConfigurable("idempotency()")
|
|
@@ -2015,7 +1933,9 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
2015
1933
|
* `RouteConfigError` at merge time, and a group with WebSocket routes is refused (register
|
|
2016
1934
|
* those on the parent).
|
|
2017
1935
|
*/
|
|
2018
|
-
merge<R2 extends Registry, Ctx2
|
|
1936
|
+
merge<R2 extends Registry, Ctx2, HookOutput2>(
|
|
1937
|
+
other: Server<R2, Ctx2, HookOutput2>,
|
|
1938
|
+
): Server<R & R2, Ctx, HookOutput> {
|
|
2019
1939
|
this.assertConfigurable("merge()")
|
|
2020
1940
|
const source = other as unknown as Server<Registry, EmptyContext>
|
|
2021
1941
|
if (source.wsRouteCount > 0) {
|
|
@@ -2100,7 +2020,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
2100
2020
|
if (!scoped) this.globalAssurance.push(...source.globalAssurance)
|
|
2101
2021
|
this.mcpResourceList.push(...source.mcpResourceList)
|
|
2102
2022
|
this.mcpPromptList.push(...source.mcpPromptList)
|
|
2103
|
-
return this as unknown as Server<R & R2, Ctx>
|
|
2023
|
+
return this as unknown as Server<R & R2, Ctx, HookOutput>
|
|
2104
2024
|
}
|
|
2105
2025
|
|
|
2106
2026
|
/** A fused renderer closes over runtime services to keep its seven-argument JSC fast path. Merging
|
|
@@ -2110,31 +2030,81 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
2110
2030
|
if (entry.execution.fusedWeb === undefined) return route
|
|
2111
2031
|
// Rebuild with the SAME builder that produced the closure - rebinding a query-fused route as
|
|
2112
2032
|
// bare would silently drop its validation.
|
|
2033
|
+
const lane = entry.execution.fusedLane
|
|
2034
|
+
// Type-erase the lane so each `===` branch sees the full union, not the narrowed remainder.
|
|
2035
|
+
const laneName: string | undefined = lane
|
|
2036
|
+
// Body+lifecycle routes have a fused Web renderer, but no body-only Node renderer: the latter
|
|
2037
|
+
// would bypass derive/before/after because the Node-direct dispatcher prefers `fusedBody` over
|
|
2038
|
+
// the generic execution plan. Only the original body-only lane may populate this slot.
|
|
2113
2039
|
const fusedBody =
|
|
2114
|
-
|
|
2040
|
+
laneName === "body"
|
|
2115
2041
|
? this.buildFusedBodyRunner(
|
|
2116
2042
|
entry.handler,
|
|
2117
2043
|
entry.schema?.body as StandardSchemaV1,
|
|
2118
2044
|
entry.hasDecorations ? entry.decorations : undefined,
|
|
2119
|
-
entry.bodyLimit ??
|
|
2045
|
+
entry.bodyLimit ?? UNLIMITED_BODY_BYTES,
|
|
2120
2046
|
)
|
|
2121
2047
|
: undefined
|
|
2048
|
+
const derive = entry.derives[0]
|
|
2049
|
+
const before = entry.beforeHandle[0]
|
|
2050
|
+
const after = entry.afterHandle[0]
|
|
2122
2051
|
const fusedWeb =
|
|
2123
|
-
|
|
2052
|
+
laneName === "body"
|
|
2124
2053
|
? this.buildFusedBodyWeb(fusedBody as FusedBodyRunner)
|
|
2125
|
-
:
|
|
2054
|
+
: laneName === "query"
|
|
2126
2055
|
? this.buildFusedQueryWeb(
|
|
2127
2056
|
entry.handler,
|
|
2128
2057
|
entry.hasDecorations ? entry.decorations : undefined,
|
|
2129
2058
|
entry.schema?.query as StandardSchemaV1,
|
|
2130
|
-
entry.bodyLimit ??
|
|
2131
|
-
)
|
|
2132
|
-
: this.buildFusedWeb(
|
|
2133
|
-
entry.handler,
|
|
2134
|
-
entry.hasDecorations ? entry.decorations : undefined,
|
|
2135
|
-
isContextlessNoArgArrow(entry.handler),
|
|
2136
|
-
entry.bodyLimit ?? this.maxBodyBytes,
|
|
2059
|
+
entry.bodyLimit ?? UNLIMITED_BODY_BYTES,
|
|
2137
2060
|
)
|
|
2061
|
+
: laneName === "body-derive-before" && derive !== undefined && before !== undefined
|
|
2062
|
+
? this.buildFusedBodyDeriveBeforeAfter(
|
|
2063
|
+
entry.handler,
|
|
2064
|
+
derive,
|
|
2065
|
+
before,
|
|
2066
|
+
undefined,
|
|
2067
|
+
entry.schema?.body as StandardSchemaV1,
|
|
2068
|
+
entry.bodyLimit ?? UNLIMITED_BODY_BYTES,
|
|
2069
|
+
)
|
|
2070
|
+
: laneName === "body-derive-before-after" &&
|
|
2071
|
+
derive !== undefined &&
|
|
2072
|
+
before !== undefined &&
|
|
2073
|
+
after !== undefined
|
|
2074
|
+
? this.buildFusedBodyDeriveBeforeAfter(
|
|
2075
|
+
entry.handler,
|
|
2076
|
+
derive,
|
|
2077
|
+
before,
|
|
2078
|
+
after,
|
|
2079
|
+
entry.schema?.body as StandardSchemaV1,
|
|
2080
|
+
entry.bodyLimit ?? UNLIMITED_BODY_BYTES,
|
|
2081
|
+
)
|
|
2082
|
+
: laneName === "derive-before" && derive !== undefined && before !== undefined
|
|
2083
|
+
? this.buildFusedDeriveBefore(
|
|
2084
|
+
entry.handler,
|
|
2085
|
+
derive,
|
|
2086
|
+
before,
|
|
2087
|
+
entry.schema?.query,
|
|
2088
|
+
entry.bodyLimit ?? UNLIMITED_BODY_BYTES,
|
|
2089
|
+
)
|
|
2090
|
+
: laneName === "derive-before-after" &&
|
|
2091
|
+
derive !== undefined &&
|
|
2092
|
+
before !== undefined &&
|
|
2093
|
+
after !== undefined
|
|
2094
|
+
? this.buildFusedDeriveBeforeAfter(
|
|
2095
|
+
entry.handler,
|
|
2096
|
+
derive,
|
|
2097
|
+
before,
|
|
2098
|
+
after,
|
|
2099
|
+
entry.schema?.query,
|
|
2100
|
+
entry.bodyLimit ?? UNLIMITED_BODY_BYTES,
|
|
2101
|
+
)
|
|
2102
|
+
: this.buildFusedWeb(
|
|
2103
|
+
entry.handler,
|
|
2104
|
+
entry.hasDecorations ? entry.decorations : undefined,
|
|
2105
|
+
isContextlessNoArgArrow(entry.handler),
|
|
2106
|
+
entry.bodyLimit ?? UNLIMITED_BODY_BYTES,
|
|
2107
|
+
)
|
|
2138
2108
|
return {
|
|
2139
2109
|
...route,
|
|
2140
2110
|
entry: {
|
|
@@ -2290,6 +2260,45 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
2290
2260
|
return outcome
|
|
2291
2261
|
}
|
|
2292
2262
|
|
|
2263
|
+
/**
|
|
2264
|
+
* Select a mounted handler's native Node lane without materializing a Web Request. This is an
|
|
2265
|
+
* adapter seam, not a second lifecycle: any app-wide behavior that the ordinary mount path would
|
|
2266
|
+
* run makes the capability ineligible and the caller falls back to `resolveNodeSource`. Typed
|
|
2267
|
+
* routes keep precedence exactly as `routeAndRun` does, including the existing mount behavior for
|
|
2268
|
+
* a path whose registered route rejects this method.
|
|
2269
|
+
*/
|
|
2270
|
+
[RESOLVE_NODE_MOUNT](source: RequestSource): NativeMountSelection | undefined {
|
|
2271
|
+
if (
|
|
2272
|
+
this.onRequestHooks.length > 0 ||
|
|
2273
|
+
this.onResponseHooks.length > 0 ||
|
|
2274
|
+
this.onResponseFinalizedHooks.length > 0 ||
|
|
2275
|
+
this.capacityGate !== undefined ||
|
|
2276
|
+
this.staticResponseHeaders !== undefined ||
|
|
2277
|
+
this.requestTimeoutMs !== 0 ||
|
|
2278
|
+
this.acceptInboundDeadlines ||
|
|
2279
|
+
this.clientIpTrust !== undefined
|
|
2280
|
+
) {
|
|
2281
|
+
return undefined
|
|
2282
|
+
}
|
|
2283
|
+
|
|
2284
|
+
const parts = source.urlParts ?? urlPartsOf(source.url)
|
|
2285
|
+
const match = this.catalog.find(source.method, parts.pathname)
|
|
2286
|
+
if (match.found) return undefined
|
|
2287
|
+
|
|
2288
|
+
for (const mount of this.fetchMounts) {
|
|
2289
|
+
if (!underMountPrefix(parts.pathname, mount.path)) continue
|
|
2290
|
+
const candidate = (mount.handler as unknown as Record<symbol, unknown>)[NODE_NATIVE_MOUNT]
|
|
2291
|
+
return typeof candidate === "function"
|
|
2292
|
+
? {
|
|
2293
|
+
handler: candidate as NativeMountHandler,
|
|
2294
|
+
path: mount.path,
|
|
2295
|
+
stripPrefix: mount.stripPrefix,
|
|
2296
|
+
}
|
|
2297
|
+
: undefined
|
|
2298
|
+
}
|
|
2299
|
+
return undefined
|
|
2300
|
+
}
|
|
2301
|
+
|
|
2293
2302
|
/**
|
|
2294
2303
|
* Like {@link fetch}, but renders a plain-data result **without** building a Web `Response` - the
|
|
2295
2304
|
* `@nifrajs/node` adapter serializes the returned primitives straight to the socket, skipping the undici
|
|
@@ -2668,7 +2677,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
2668
2677
|
source: RequestSource,
|
|
2669
2678
|
platform: Platform | undefined,
|
|
2670
2679
|
finalize: (result: unknown, set: CtxSet) => T,
|
|
2671
|
-
wrapResponse: (response: Response) => T,
|
|
2680
|
+
wrapResponse: (response: Response | ResponseResult) => T,
|
|
2672
2681
|
onTimeout: () => T,
|
|
2673
2682
|
// True only from the Web `fetch` path - unlocks each route's fused lane, whose output type IS
|
|
2674
2683
|
// `Response` (`T = Response` there by construction; the node path always passes false).
|
|
@@ -2696,7 +2705,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
2696
2705
|
source: RequestSource,
|
|
2697
2706
|
platform: Platform | undefined,
|
|
2698
2707
|
finalize: (result: unknown, set: CtxSet) => T,
|
|
2699
|
-
wrapResponse: (response: Response) => T,
|
|
2708
|
+
wrapResponse: (response: Response | ResponseResult) => T,
|
|
2700
2709
|
onTimeout: () => T,
|
|
2701
2710
|
): MaybePromise<T> {
|
|
2702
2711
|
const hooks = this.onNodeRequestHooks
|
|
@@ -2730,7 +2739,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
2730
2739
|
source: RequestSource,
|
|
2731
2740
|
platform: Platform | undefined,
|
|
2732
2741
|
finalize: (result: unknown, set: CtxSet) => T,
|
|
2733
|
-
wrapResponse: (response: Response) => T,
|
|
2742
|
+
wrapResponse: (response: Response | ResponseResult) => T,
|
|
2734
2743
|
onTimeout: () => T,
|
|
2735
2744
|
): Promise<T> {
|
|
2736
2745
|
if (first !== undefined) return wrapResponse(first)
|
|
@@ -2758,7 +2767,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
2758
2767
|
source: RequestSource,
|
|
2759
2768
|
platform: Platform | undefined,
|
|
2760
2769
|
finalize: (result: unknown, set: CtxSet) => T,
|
|
2761
|
-
wrapResponse: (response: Response) => T,
|
|
2770
|
+
wrapResponse: (response: Response | ResponseResult) => T,
|
|
2762
2771
|
onTimeout: () => T,
|
|
2763
2772
|
webFast: boolean,
|
|
2764
2773
|
): MaybePromise<T> {
|
|
@@ -2805,7 +2814,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
2805
2814
|
sourceAtAwait: RequestSource,
|
|
2806
2815
|
platform: Platform | undefined,
|
|
2807
2816
|
finalize: (result: unknown, set: CtxSet) => T,
|
|
2808
|
-
wrapResponse: (response: Response) => T,
|
|
2817
|
+
wrapResponse: (response: Response | ResponseResult) => T,
|
|
2809
2818
|
onTimeout: () => T,
|
|
2810
2819
|
webFast: boolean,
|
|
2811
2820
|
): Promise<T> {
|
|
@@ -2893,13 +2902,13 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
2893
2902
|
/**
|
|
2894
2903
|
* Route → build context → run. Synchronous through to the handler for a **bare** route
|
|
2895
2904
|
* (selected by its compiled plan), so a sync handler produces its result with zero promise allocations;
|
|
2896
|
-
* routes with validation/hooks keep the full
|
|
2905
|
+
* routes with validation/hooks keep the full compiled route program, unchanged.
|
|
2897
2906
|
*/
|
|
2898
2907
|
private routeAndRun<T>(
|
|
2899
2908
|
source: RequestSource,
|
|
2900
2909
|
platform: Platform | undefined,
|
|
2901
2910
|
finalize: (result: unknown, set: CtxSet) => T,
|
|
2902
|
-
wrapResponse: (response: Response) => T,
|
|
2911
|
+
wrapResponse: (response: Response | ResponseResult) => T,
|
|
2903
2912
|
onTimeout: () => T,
|
|
2904
2913
|
webFast: boolean,
|
|
2905
2914
|
): MaybePromise<T> {
|
|
@@ -2951,17 +2960,19 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
2951
2960
|
}
|
|
2952
2961
|
if (match.reason === "method-not-allowed") {
|
|
2953
2962
|
return wrapResponse(
|
|
2954
|
-
|
|
2963
|
+
// Lowercase on purpose: a plain render's headers go into the node outcome record verbatim,
|
|
2964
|
+
// and every other name in that record is lowercase (the Web lane normalizes either way).
|
|
2965
|
+
plainError(405, "method_not_allowed", { allow: match.allowed.join(", ") }),
|
|
2955
2966
|
)
|
|
2956
2967
|
}
|
|
2957
|
-
return wrapResponse(
|
|
2968
|
+
return wrapResponse(plainError(404, "not_found"))
|
|
2958
2969
|
}
|
|
2959
2970
|
|
|
2960
2971
|
// Inspect only captured values for escapes. Scanning the full pathname repeated work the router
|
|
2961
2972
|
// already did and made every plain dynamic route pay for unrelated static path bytes.
|
|
2962
2973
|
const params = match.params === EMPTY_PARAMS ? match.params : decodeRouteParams(match.params)
|
|
2963
2974
|
if (params === null) {
|
|
2964
|
-
return wrapResponse(
|
|
2975
|
+
return wrapResponse(plainError(400, "malformed_path"))
|
|
2965
2976
|
}
|
|
2966
2977
|
|
|
2967
2978
|
return this.runMatched(
|
|
@@ -3001,7 +3012,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
3001
3012
|
params: Record<string, string>,
|
|
3002
3013
|
search: string | undefined,
|
|
3003
3014
|
finalize: (result: unknown, set: CtxSet) => T,
|
|
3004
|
-
wrapResponse: (response: Response) => T,
|
|
3015
|
+
wrapResponse: (response: Response | ResponseResult) => T,
|
|
3005
3016
|
onTimeout: () => T,
|
|
3006
3017
|
webFast: boolean,
|
|
3007
3018
|
): MaybePromise<T> {
|
|
@@ -3032,7 +3043,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
3032
3043
|
search,
|
|
3033
3044
|
wrapResponse,
|
|
3034
3045
|
{
|
|
3035
|
-
maxBodyBytes: entry.bodyLimit ??
|
|
3046
|
+
maxBodyBytes: entry.bodyLimit ?? UNLIMITED_BODY_BYTES,
|
|
3036
3047
|
runLanes: (buffered, plat, ent, prm, srch) =>
|
|
3037
3048
|
this.idempotencyRunLanes(buffered, plat, ent as RouteEntry, prm, srch),
|
|
3038
3049
|
},
|
|
@@ -3059,7 +3070,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
3059
3070
|
params: Record<string, string>,
|
|
3060
3071
|
search: string | undefined,
|
|
3061
3072
|
finalize: (result: unknown, set: CtxSet) => T,
|
|
3062
|
-
wrapResponse: (response: Response) => T,
|
|
3073
|
+
wrapResponse: (response: Response | ResponseResult) => T,
|
|
3063
3074
|
onTimeout: () => T,
|
|
3064
3075
|
webFast: boolean,
|
|
3065
3076
|
): MaybePromise<T> {
|
|
@@ -3081,7 +3092,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
3081
3092
|
? undefined
|
|
3082
3093
|
: admitDeadline(source.headers, this.deadlineAdmissionOptions)
|
|
3083
3094
|
if (admission !== undefined && !admission.ok) {
|
|
3084
|
-
return wrapResponse(
|
|
3095
|
+
return wrapResponse(plainError(admission.status, admission.reason))
|
|
3085
3096
|
}
|
|
3086
3097
|
const effectiveTimeoutMs = admission?.timeoutMs ?? 0
|
|
3087
3098
|
|
|
@@ -3140,7 +3151,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
3140
3151
|
if (controller !== undefined && outcome instanceof Promise) {
|
|
3141
3152
|
const timedOut =
|
|
3142
3153
|
admission?.inherited === true
|
|
3143
|
-
? () => wrapResponse(
|
|
3154
|
+
? () => wrapResponse(plainError(504, "deadline_exceeded"))
|
|
3144
3155
|
: onTimeout
|
|
3145
3156
|
return this.withTimeout(
|
|
3146
3157
|
outcome,
|
|
@@ -3161,7 +3172,6 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
3161
3172
|
/** The narrowest bare route: a syntactic `() => ...` handler cannot observe the context argument, so
|
|
3162
3173
|
* successful requests can skip allocating `RequestContext`. Errors still allocate one for logging. */
|
|
3163
3174
|
// @ts-expect-error TS6133 -- invoked structurally by compiled plans (internal/route-execution.ts)
|
|
3164
|
-
// biome-ignore lint/correctness/noUnusedPrivateClassMembers: invoked structurally by compiled plans (internal/route-execution.ts)
|
|
3165
3175
|
private runContextlessBare<T>(
|
|
3166
3176
|
entry: RouteEntry,
|
|
3167
3177
|
source: RequestSource,
|
|
@@ -3171,7 +3181,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
3171
3181
|
budget: RequestBudget,
|
|
3172
3182
|
platform: Platform | undefined,
|
|
3173
3183
|
finalize: (result: unknown, set: CtxSet) => T,
|
|
3174
|
-
wrapResponse: (response: Response) => T,
|
|
3184
|
+
wrapResponse: (response: Response | ResponseResult) => T,
|
|
3175
3185
|
): MaybePromise<T> {
|
|
3176
3186
|
let result: unknown
|
|
3177
3187
|
try {
|
|
@@ -3215,9 +3225,12 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
3215
3225
|
signal: AbortSignal,
|
|
3216
3226
|
budget: RequestBudget,
|
|
3217
3227
|
platform: Platform | undefined,
|
|
3218
|
-
wrapResponse: (response: Response) => T,
|
|
3228
|
+
wrapResponse: (response: Response | ResponseResult) => T,
|
|
3219
3229
|
): T {
|
|
3220
3230
|
if (err instanceof Response) return wrapResponse(err)
|
|
3231
|
+
// Handed over as the plain data it is: `wrapResponse` renders it on the JSON lane (no `Response`
|
|
3232
|
+
// built on Node). There is no context here by construction, so there is no `c.set` to merge.
|
|
3233
|
+
if (isResponseResult(err)) return wrapResponse(err)
|
|
3221
3234
|
const ctx = new RequestContext(
|
|
3222
3235
|
source,
|
|
3223
3236
|
params,
|
|
@@ -3229,43 +3242,42 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
3229
3242
|
this.protoPoisoning,
|
|
3230
3243
|
)
|
|
3231
3244
|
this.logRequestError(err, ctx)
|
|
3232
|
-
return wrapResponse
|
|
3245
|
+
return this.internalErrorResponse(wrapResponse)
|
|
3233
3246
|
}
|
|
3234
3247
|
|
|
3235
3248
|
/**
|
|
3236
3249
|
* The synchronous fast path selected by a route's execution plan: apply static decorations, call the
|
|
3237
3250
|
* handler, render the result - **no `await`** unless the handler itself returns a promise. It mirrors
|
|
3238
|
-
* the
|
|
3251
|
+
* the same error boundary as the compiled route program (which a bare route would otherwise no-op through) and shares
|
|
3239
3252
|
* {@link logRequestError}; a bare route has no `onError` hooks, so error handling is fully synchronous
|
|
3240
3253
|
* (a thrown `Response` is control flow; anything else is a logged flat 500). This is where nifra skips
|
|
3241
3254
|
* the per-request async-frame tax - the same win codegen routers get, but without `eval`.
|
|
3242
3255
|
*/
|
|
3243
3256
|
// @ts-expect-error TS6133 -- invoked structurally by compiled plans (internal/route-execution.ts)
|
|
3244
|
-
// biome-ignore lint/correctness/noUnusedPrivateClassMembers: invoked structurally by compiled plans (internal/route-execution.ts)
|
|
3245
3257
|
private runBare<T>(
|
|
3246
3258
|
entry: RouteEntry,
|
|
3247
3259
|
ctx: RawContext,
|
|
3248
3260
|
finalize: (result: unknown, set: CtxSet) => T,
|
|
3249
|
-
wrapResponse: (response: Response) => T,
|
|
3261
|
+
wrapResponse: (response: Response | ResponseResult) => T,
|
|
3250
3262
|
): MaybePromise<T> {
|
|
3251
3263
|
let result: unknown
|
|
3252
3264
|
try {
|
|
3253
3265
|
if (entry.hasDecorations) Object.assign(ctx, entry.decorations)
|
|
3254
3266
|
result = entry.handler(ctx)
|
|
3255
3267
|
} catch (err) {
|
|
3256
|
-
return this.bareError(err, ctx, wrapResponse)
|
|
3268
|
+
return this.bareError(err, ctx, finalize, wrapResponse)
|
|
3257
3269
|
}
|
|
3258
3270
|
if (result instanceof Promise) {
|
|
3259
3271
|
// Async handler on an otherwise-bare route: finish on a microtask, with the same error handling.
|
|
3260
3272
|
return result.then(
|
|
3261
3273
|
(value) => finalize(value, responseSet(ctx)),
|
|
3262
|
-
(err) => this.bareError(err, ctx, wrapResponse),
|
|
3274
|
+
(err) => this.bareError(err, ctx, finalize, wrapResponse),
|
|
3263
3275
|
)
|
|
3264
3276
|
}
|
|
3265
3277
|
return finalize(result, responseSet(ctx))
|
|
3266
3278
|
}
|
|
3267
3279
|
|
|
3268
|
-
/** Bare-route error rendering - identical to
|
|
3280
|
+
/** Bare-route error rendering - identical to the compiled route program's catch minus the (absent) onError
|
|
3269
3281
|
* loop: a thrown `Response` is returned as deliberate control flow; anything else is logged + 500. */
|
|
3270
3282
|
/**
|
|
3271
3283
|
* Build a route's fused Web renderer. Composition happens once at
|
|
@@ -3280,13 +3292,16 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
3280
3292
|
contextless: boolean,
|
|
3281
3293
|
maxBodyBytes: number,
|
|
3282
3294
|
): FusedWebRunner {
|
|
3283
|
-
//
|
|
3284
|
-
//
|
|
3285
|
-
|
|
3286
|
-
|
|
3287
|
-
this.
|
|
3288
|
-
|
|
3289
|
-
|
|
3295
|
+
// Same thrown-value contract as the generic lane (`bare-error-lane.ts` via `bareError`); the fused
|
|
3296
|
+
// lane only injects its renderer, so a thrown `status(...)` and the flat 500 fold in static headers
|
|
3297
|
+
// through `wrapWebResponse`. One source of truth for the error surface, no per-lane copy to drift.
|
|
3298
|
+
const logError = (err: unknown, ctx: RawContext): Response =>
|
|
3299
|
+
this.bareError(
|
|
3300
|
+
err,
|
|
3301
|
+
ctx,
|
|
3302
|
+
(result, set) => this.wrapWebResponse(toResponse(result, set)),
|
|
3303
|
+
this.wrapWebResponse,
|
|
3304
|
+
)
|
|
3290
3305
|
if (contextless && decorations === undefined) {
|
|
3291
3306
|
// `() => ...` can't observe the context - skip allocating one entirely (errors still build
|
|
3292
3307
|
// one for the structured log, exactly like runContextlessBare).
|
|
@@ -3416,20 +3431,21 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
3416
3431
|
const logError = <T>(
|
|
3417
3432
|
err: unknown,
|
|
3418
3433
|
ctx: RawContext,
|
|
3419
|
-
|
|
3420
|
-
|
|
3434
|
+
finalize: (result: unknown, set: CtxSet, ctx: RawContext) => T,
|
|
3435
|
+
wrapResponse: (response: Response | ResponseResult) => T,
|
|
3436
|
+
): T => this.bareError(err, ctx, finalize, wrapResponse)
|
|
3421
3437
|
|
|
3422
3438
|
const runHandler = <T>(
|
|
3423
3439
|
ctx: RawContext,
|
|
3424
3440
|
finalize: (result: unknown, set: CtxSet, ctx: RawContext) => T,
|
|
3425
|
-
wrapResponse: (response: Response) => T,
|
|
3441
|
+
wrapResponse: (response: Response | ResponseResult) => T,
|
|
3426
3442
|
): MaybePromise<T> => {
|
|
3427
3443
|
if (decorations !== undefined) Object.assign(ctx, decorations)
|
|
3428
3444
|
let output: MaybePromise<HandlerResult>
|
|
3429
3445
|
try {
|
|
3430
3446
|
output = handler(ctx)
|
|
3431
3447
|
} catch (err) {
|
|
3432
|
-
return logError(err, ctx, wrapResponse)
|
|
3448
|
+
return logError(err, ctx, finalize, wrapResponse)
|
|
3433
3449
|
}
|
|
3434
3450
|
if (output instanceof Promise) {
|
|
3435
3451
|
return output.then(
|
|
@@ -3437,16 +3453,16 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
3437
3453
|
try {
|
|
3438
3454
|
return finalize(result, responseSet(ctx), ctx)
|
|
3439
3455
|
} catch (err) {
|
|
3440
|
-
return logError(err, ctx, wrapResponse)
|
|
3456
|
+
return logError(err, ctx, finalize, wrapResponse)
|
|
3441
3457
|
}
|
|
3442
3458
|
},
|
|
3443
|
-
(err) => logError(err, ctx, wrapResponse),
|
|
3459
|
+
(err) => logError(err, ctx, finalize, wrapResponse),
|
|
3444
3460
|
)
|
|
3445
3461
|
}
|
|
3446
3462
|
try {
|
|
3447
3463
|
return finalize(output, responseSet(ctx), ctx)
|
|
3448
3464
|
} catch (err) {
|
|
3449
|
-
return logError(err, ctx, wrapResponse)
|
|
3465
|
+
return logError(err, ctx, finalize, wrapResponse)
|
|
3450
3466
|
}
|
|
3451
3467
|
}
|
|
3452
3468
|
|
|
@@ -3454,9 +3470,9 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
3454
3470
|
result: StandardResult<unknown>,
|
|
3455
3471
|
ctx: RawContext,
|
|
3456
3472
|
finalize: (result: unknown, set: CtxSet, ctx: RawContext) => T,
|
|
3457
|
-
wrapResponse: (response: Response) => T,
|
|
3473
|
+
wrapResponse: (response: Response | ResponseResult) => T,
|
|
3458
3474
|
): MaybePromise<T> => {
|
|
3459
|
-
if (result.issues !== undefined) return wrapResponse(
|
|
3475
|
+
if (result.issues !== undefined) return wrapResponse(plainValidationError(result.issues))
|
|
3460
3476
|
ctx.body = result.value
|
|
3461
3477
|
return runHandler(ctx, finalize, wrapResponse)
|
|
3462
3478
|
}
|
|
@@ -3465,13 +3481,13 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
3465
3481
|
parsed: unknown,
|
|
3466
3482
|
ctx: RawContext,
|
|
3467
3483
|
finalize: (result: unknown, set: CtxSet, ctx: RawContext) => T,
|
|
3468
|
-
wrapResponse: (response: Response) => T,
|
|
3484
|
+
wrapResponse: (response: Response | ResponseResult) => T,
|
|
3469
3485
|
): MaybePromise<T> => {
|
|
3470
3486
|
let validation: StandardResult<unknown> | Promise<StandardResult<unknown>>
|
|
3471
3487
|
try {
|
|
3472
3488
|
validation = bodySchema["~standard"].validate(parsed)
|
|
3473
3489
|
} catch (err) {
|
|
3474
|
-
return logError(err, ctx, wrapResponse)
|
|
3490
|
+
return logError(err, ctx, finalize, wrapResponse)
|
|
3475
3491
|
}
|
|
3476
3492
|
if (validation instanceof Promise) {
|
|
3477
3493
|
return validation.then(
|
|
@@ -3479,16 +3495,16 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
3479
3495
|
try {
|
|
3480
3496
|
return runValidated(settled, ctx, finalize, wrapResponse)
|
|
3481
3497
|
} catch (err) {
|
|
3482
|
-
return logError(err, ctx, wrapResponse)
|
|
3498
|
+
return logError(err, ctx, finalize, wrapResponse)
|
|
3483
3499
|
}
|
|
3484
3500
|
},
|
|
3485
|
-
(err) => logError(err, ctx, wrapResponse),
|
|
3501
|
+
(err) => logError(err, ctx, finalize, wrapResponse),
|
|
3486
3502
|
)
|
|
3487
3503
|
}
|
|
3488
3504
|
try {
|
|
3489
3505
|
return runValidated(validation, ctx, finalize, wrapResponse)
|
|
3490
3506
|
} catch (err) {
|
|
3491
|
-
return logError(err, ctx, wrapResponse)
|
|
3507
|
+
return logError(err, ctx, finalize, wrapResponse)
|
|
3492
3508
|
}
|
|
3493
3509
|
}
|
|
3494
3510
|
|
|
@@ -3501,7 +3517,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
3501
3517
|
platform: Platform | undefined,
|
|
3502
3518
|
nativeContext: boolean,
|
|
3503
3519
|
finalize: (result: unknown, set: CtxSet, ctx: RawContext) => T,
|
|
3504
|
-
wrapResponse: (response: Response) => T,
|
|
3520
|
+
wrapResponse: (response: Response | ResponseResult) => T,
|
|
3505
3521
|
): MaybePromise<T> => {
|
|
3506
3522
|
const ctx = nativeContext
|
|
3507
3523
|
? RequestContext.native(source, params, search, maxBodyBytes, platform, this.protoPoisoning)
|
|
@@ -3517,8 +3533,13 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
3517
3533
|
)
|
|
3518
3534
|
const finish = (value: unknown): MaybePromise<T> =>
|
|
3519
3535
|
runParsed(value, ctx, finalize, wrapResponse)
|
|
3520
|
-
return
|
|
3521
|
-
|
|
3536
|
+
return readBodyFramed(
|
|
3537
|
+
source,
|
|
3538
|
+
maxBodyBytes,
|
|
3539
|
+
this.protoPoisoning,
|
|
3540
|
+
finish,
|
|
3541
|
+
wrapResponse,
|
|
3542
|
+
(err) => logError(err, ctx, finalize, wrapResponse),
|
|
3522
3543
|
)
|
|
3523
3544
|
}
|
|
3524
3545
|
}
|
|
@@ -3560,13 +3581,16 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
3560
3581
|
querySchema: StandardSchemaV1,
|
|
3561
3582
|
maxBodyBytes: number,
|
|
3562
3583
|
): FusedWebRunner {
|
|
3563
|
-
//
|
|
3564
|
-
//
|
|
3565
|
-
|
|
3566
|
-
|
|
3567
|
-
this.
|
|
3568
|
-
|
|
3569
|
-
|
|
3584
|
+
// Same thrown-value contract as the generic lane (`bare-error-lane.ts` via `bareError`); the fused
|
|
3585
|
+
// lane only injects its renderer, so a thrown `status(...)` and the flat 500 fold in static headers
|
|
3586
|
+
// through `wrapWebResponse`. One source of truth for the error surface, no per-lane copy to drift.
|
|
3587
|
+
const logError = (err: unknown, ctx: RawContext): Response =>
|
|
3588
|
+
this.bareError(
|
|
3589
|
+
err,
|
|
3590
|
+
ctx,
|
|
3591
|
+
(result, set) => this.wrapWebResponse(toResponse(result, set)),
|
|
3592
|
+
this.wrapWebResponse,
|
|
3593
|
+
)
|
|
3570
3594
|
const runHandler = (ctx: RawContext, value: unknown): MaybePromise<Response> => {
|
|
3571
3595
|
ctx.query = value
|
|
3572
3596
|
let result: unknown
|
|
@@ -3620,1042 +3644,984 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
3620
3644
|
return validation.then(
|
|
3621
3645
|
(settled) =>
|
|
3622
3646
|
settled.issues !== undefined
|
|
3623
|
-
? this.wrapWebResponse(
|
|
3647
|
+
? this.wrapWebResponse(plainValidationError(settled.issues))
|
|
3624
3648
|
: runHandler(ctx, settled.value),
|
|
3625
3649
|
(err) => logError(err, ctx),
|
|
3626
3650
|
)
|
|
3627
3651
|
}
|
|
3628
3652
|
if (validation.issues !== undefined)
|
|
3629
|
-
return this.wrapWebResponse(
|
|
3653
|
+
return this.wrapWebResponse(plainValidationError(validation.issues))
|
|
3630
3654
|
return runHandler(ctx, validation.value)
|
|
3631
3655
|
}
|
|
3632
3656
|
}
|
|
3633
3657
|
|
|
3634
|
-
|
|
3635
|
-
|
|
3636
|
-
|
|
3637
|
-
|
|
3638
|
-
|
|
3639
|
-
|
|
3640
|
-
|
|
3641
|
-
|
|
3642
|
-
|
|
3643
|
-
|
|
3644
|
-
|
|
3645
|
-
|
|
3646
|
-
|
|
3647
|
-
|
|
3648
|
-
|
|
3649
|
-
|
|
3650
|
-
|
|
3651
|
-
|
|
3652
|
-
|
|
3653
|
-
|
|
3654
|
-
|
|
3655
|
-
return outcome instanceof Promise
|
|
3656
|
-
? outcome.catch((err) => this.handleLifecycleError(entry, err, ctx, finalize, wrapResponse))
|
|
3657
|
-
: outcome
|
|
3658
|
-
}
|
|
3659
|
-
|
|
3660
|
-
private runAround<T>(
|
|
3661
|
-
hooks: ReadonlyArray<RawAround>,
|
|
3662
|
-
ctx: RawContext,
|
|
3663
|
-
run: () => MaybePromise<T>,
|
|
3664
|
-
): MaybePromise<T> {
|
|
3665
|
-
const dispatch = (index: number): MaybePromise<T> => {
|
|
3666
|
-
if (index >= hooks.length) return run()
|
|
3667
|
-
const hook = hooks[index]!
|
|
3668
|
-
let called = false
|
|
3669
|
-
return hook(ctx, () => {
|
|
3670
|
-
if (called) throw new Error("around next() called multiple times")
|
|
3671
|
-
called = true
|
|
3672
|
-
return dispatch(index + 1)
|
|
3673
|
-
})
|
|
3674
|
-
}
|
|
3675
|
-
return dispatch(0)
|
|
3676
|
-
}
|
|
3677
|
-
|
|
3678
|
-
// @ts-expect-error TS6133 -- invoked structurally by compiled plans (internal/route-execution.ts)
|
|
3679
|
-
// biome-ignore lint/correctness/noUnusedPrivateClassMembers: invoked structurally by compiled plans (internal/route-execution.ts)
|
|
3680
|
-
private runBodyOnly<T>(
|
|
3681
|
-
entry: RouteEntry,
|
|
3682
|
-
source: RequestSource,
|
|
3683
|
-
ctx: RawContext,
|
|
3684
|
-
finalize: (result: unknown, set: CtxSet) => T,
|
|
3685
|
-
wrapResponse: (response: Response) => T,
|
|
3686
|
-
): Promise<T> {
|
|
3687
|
-
return this.readBodyInput(
|
|
3688
|
-
source,
|
|
3689
|
-
entry.bodyLimit ?? this.maxBodyBytes,
|
|
3690
|
-
(parsed) => this.finishBodyOnly(entry, parsed, ctx, finalize, wrapResponse),
|
|
3691
|
-
wrapResponse,
|
|
3692
|
-
(err) => this.handleLifecycleError(entry, err, ctx, finalize, wrapResponse),
|
|
3693
|
-
)
|
|
3694
|
-
}
|
|
3695
|
-
|
|
3696
|
-
/** Shared bounded body framing/parser. Both body lanes use the same trust-boundary checks; only
|
|
3697
|
-
* the validation + handler continuation differs. */
|
|
3698
|
-
private readBodyInput<T>(
|
|
3699
|
-
source: RequestSource,
|
|
3658
|
+
/**
|
|
3659
|
+
* Compile a route's `derive + before` lifecycle to a single closure: the per-request walk through
|
|
3660
|
+
* `runSync` → `runHooksSync` is a stack of 3 frames and 2 `Object.assign`s for what is by far the
|
|
3661
|
+
* most common middleware shape (auth/trace/id derivation, an auth/policy `beforeHandle`). Folding
|
|
3662
|
+
* the same steps into one closure hits the same monomorphic inline-cache site as `buildFusedWeb`,
|
|
3663
|
+
* and `Object.assign(ctx, deriveResult)` becomes one extra property write on the context's already
|
|
3664
|
+
* hot hidden class. Behavior is byte-identical to the compiled route program: the same thrown-value
|
|
3665
|
+
* contract (`Response` is control flow, anything else logs + 500), the same `c.set` semantics, the
|
|
3666
|
+
* same `fusedRespond` for the rendered body. The `LifecycleExecutionLane` guarantee
|
|
3667
|
+
* (`schema.params === undefined && schema.headers === undefined && derives === 1 &&
|
|
3668
|
+
* beforeHandle === 1 && afterHandle === 0 && onError === 0 && !hasResponseContract &&
|
|
3669
|
+
* !hasDecorations`) is enforced by `selectRouteLanes`; this lane is selected ONLY when all of those
|
|
3670
|
+
* hold. A `query` schema (the route declares `{ query }` alongside the hooks) is validated FIRST,
|
|
3671
|
+
* mirroring the compiled route program's stage order; the fused builder runs validate → derive →
|
|
3672
|
+
* before → handler in one frame.
|
|
3673
|
+
*/
|
|
3674
|
+
private buildFusedDeriveBefore(
|
|
3675
|
+
handler: InternalHandler,
|
|
3676
|
+
derive: RawDerive,
|
|
3677
|
+
before: RawBeforeHandle,
|
|
3678
|
+
querySchema: StandardSchemaV1 | undefined,
|
|
3700
3679
|
maxBodyBytes: number,
|
|
3701
|
-
|
|
3702
|
-
|
|
3703
|
-
|
|
3704
|
-
|
|
3705
|
-
|
|
3706
|
-
|
|
3707
|
-
|
|
3708
|
-
return readBoundedForm(source, maxBodyBytes).then(
|
|
3709
|
-
(form) => (form instanceof Response ? wrapResponse(form) : onParsed(form)),
|
|
3710
|
-
onError,
|
|
3711
|
-
) as Promise<T>
|
|
3712
|
-
}
|
|
3713
|
-
return Promise.resolve(wrapResponse(jsonError(415, "unsupported_media_type")))
|
|
3714
|
-
}
|
|
3715
|
-
|
|
3716
|
-
// All JSON bodies go through readBoundedJson - the single enforcement point for the framed
|
|
3717
|
-
// fast path (post-read byte re-check), the streaming cap, and the prototype-poisoning guard.
|
|
3718
|
-
// A separate inlined native-json() fast path here would fork the trust boundary in two.
|
|
3719
|
-
try {
|
|
3720
|
-
return this.readBoundedJson(
|
|
3721
|
-
source,
|
|
3722
|
-
maxBodyBytes,
|
|
3723
|
-
(parsed) => (parsed instanceof Response ? wrapResponse(parsed) : onParsed(parsed)),
|
|
3724
|
-
onError,
|
|
3680
|
+
): FusedWebRunner {
|
|
3681
|
+
const logError = (err: unknown, ctx: RawContext): Response =>
|
|
3682
|
+
this.bareError(
|
|
3683
|
+
err,
|
|
3684
|
+
ctx,
|
|
3685
|
+
(result, set) => this.wrapWebResponse(toResponse(result, set)),
|
|
3686
|
+
this.wrapWebResponse,
|
|
3725
3687
|
)
|
|
3726
|
-
|
|
3727
|
-
|
|
3728
|
-
|
|
3729
|
-
|
|
3730
|
-
|
|
3731
|
-
|
|
3732
|
-
|
|
3733
|
-
|
|
3734
|
-
|
|
3735
|
-
|
|
3736
|
-
|
|
3737
|
-
|
|
3738
|
-
|
|
3739
|
-
|
|
3740
|
-
|
|
3741
|
-
|
|
3742
|
-
|
|
3743
|
-
|
|
3744
|
-
|
|
3745
|
-
|
|
3746
|
-
|
|
3747
|
-
|
|
3748
|
-
|
|
3749
|
-
|
|
3750
|
-
|
|
3751
|
-
|
|
3752
|
-
|
|
3753
|
-
|
|
3754
|
-
|
|
3755
|
-
|
|
3756
|
-
|
|
3757
|
-
|
|
3758
|
-
|
|
3688
|
+
return (source, params, search, signal, budget, platform, nativeContext) => {
|
|
3689
|
+
const ctx = nativeContext
|
|
3690
|
+
? RequestContext.native(source, params, search, maxBodyBytes, platform, this.protoPoisoning)
|
|
3691
|
+
: new RequestContext(
|
|
3692
|
+
source,
|
|
3693
|
+
params,
|
|
3694
|
+
search,
|
|
3695
|
+
signal,
|
|
3696
|
+
budget,
|
|
3697
|
+
platform,
|
|
3698
|
+
maxBodyBytes,
|
|
3699
|
+
this.protoPoisoning,
|
|
3700
|
+
)
|
|
3701
|
+
const runHandler = (): MaybePromise<Response> => {
|
|
3702
|
+
let result: unknown
|
|
3703
|
+
try {
|
|
3704
|
+
result = handler(ctx)
|
|
3705
|
+
} catch (err) {
|
|
3706
|
+
return logError(err, ctx)
|
|
3707
|
+
}
|
|
3708
|
+
if (result instanceof Promise) {
|
|
3709
|
+
return result.then(
|
|
3710
|
+
(value) =>
|
|
3711
|
+
fusedRespond(
|
|
3712
|
+
value,
|
|
3713
|
+
ctx,
|
|
3714
|
+
this.responseBodyTag,
|
|
3715
|
+
this.staticResponseHeaders,
|
|
3716
|
+
this.onResponseHooks.length === 0,
|
|
3717
|
+
),
|
|
3718
|
+
(err) => logError(err, ctx),
|
|
3719
|
+
)
|
|
3720
|
+
}
|
|
3721
|
+
return fusedRespond(
|
|
3722
|
+
result,
|
|
3723
|
+
ctx,
|
|
3724
|
+
this.responseBodyTag,
|
|
3725
|
+
this.staticResponseHeaders,
|
|
3726
|
+
this.onResponseHooks.length === 0,
|
|
3759
3727
|
)
|
|
3760
3728
|
}
|
|
3761
|
-
const
|
|
3762
|
-
|
|
3763
|
-
|
|
3764
|
-
|
|
3765
|
-
|
|
3766
|
-
|
|
3767
|
-
}
|
|
3768
|
-
}
|
|
3769
|
-
|
|
3770
|
-
private executeHandler<T>(
|
|
3771
|
-
entry: RouteEntry,
|
|
3772
|
-
ctx: RawContext,
|
|
3773
|
-
finalize: (result: unknown, set: CtxSet) => T,
|
|
3774
|
-
wrapResponse?: (response: Response) => T,
|
|
3775
|
-
): MaybePromise<T> {
|
|
3776
|
-
if (entry.hasDecorations) Object.assign(ctx, entry.decorations)
|
|
3777
|
-
const handlerOutput = entry.handler(ctx)
|
|
3778
|
-
if (handlerOutput instanceof Promise) {
|
|
3779
|
-
return handlerOutput.then(
|
|
3780
|
-
(value) => finalize(value, responseSet(ctx)),
|
|
3781
|
-
(err) => {
|
|
3782
|
-
if (wrapResponse) {
|
|
3783
|
-
return this.handleLifecycleError(entry, err, ctx, finalize, wrapResponse)
|
|
3784
|
-
}
|
|
3785
|
-
throw err
|
|
3786
|
-
},
|
|
3787
|
-
)
|
|
3788
|
-
}
|
|
3789
|
-
return finalize(handlerOutput, responseSet(ctx))
|
|
3790
|
-
}
|
|
3791
|
-
|
|
3792
|
-
private handleValidationErrorRecovery<T>(
|
|
3793
|
-
entry: RouteEntry,
|
|
3794
|
-
recovery: unknown,
|
|
3795
|
-
ctx: RawContext,
|
|
3796
|
-
finalize: (result: unknown, set: CtxSet) => T,
|
|
3797
|
-
wrapResponse: (response: Response) => T,
|
|
3798
|
-
originalIssues: ReadonlyArray<StandardIssue>,
|
|
3799
|
-
kind: "body" | "query",
|
|
3800
|
-
): MaybePromise<T> {
|
|
3801
|
-
if (recovery !== undefined) {
|
|
3802
|
-
if (recovery instanceof Response) {
|
|
3803
|
-
return wrapResponse(recovery)
|
|
3804
|
-
}
|
|
3805
|
-
if (kind === "body" && entry.schema?.body) {
|
|
3806
|
-
const validation = entry.schema.body["~standard"].validate(recovery)
|
|
3807
|
-
if (validation instanceof Promise) {
|
|
3808
|
-
return validation.then((settled) => {
|
|
3809
|
-
if (settled.issues !== undefined) return wrapResponse(validationError(settled.issues))
|
|
3810
|
-
ctx.body = settled.value
|
|
3811
|
-
return this.executeHandler(entry, ctx, finalize)
|
|
3812
|
-
})
|
|
3729
|
+
const runBefore = (): MaybePromise<Response> => {
|
|
3730
|
+
let early: unknown
|
|
3731
|
+
try {
|
|
3732
|
+
early = before(ctx)
|
|
3733
|
+
} catch (err) {
|
|
3734
|
+
return logError(err, ctx)
|
|
3813
3735
|
}
|
|
3814
|
-
if (
|
|
3815
|
-
|
|
3816
|
-
return this.executeHandler(entry, ctx, finalize)
|
|
3817
|
-
}
|
|
3818
|
-
if (kind === "query" && entry.schema?.query) {
|
|
3819
|
-
const validation = entry.schema.query["~standard"].validate(recovery)
|
|
3820
|
-
if (validation instanceof Promise) {
|
|
3821
|
-
return validation.then(
|
|
3736
|
+
if (early instanceof Promise) {
|
|
3737
|
+
return early.then(
|
|
3822
3738
|
(settled) => {
|
|
3823
|
-
if (settled
|
|
3824
|
-
ctx.
|
|
3825
|
-
return this.executeHandler(entry, ctx, finalize, wrapResponse)
|
|
3739
|
+
if (settled === undefined) return runHandler()
|
|
3740
|
+
return fusedRespond(settled, ctx, this.responseBodyTag, this.staticResponseHeaders)
|
|
3826
3741
|
},
|
|
3827
|
-
(err) =>
|
|
3742
|
+
(err) => logError(err, ctx),
|
|
3828
3743
|
)
|
|
3829
3744
|
}
|
|
3830
|
-
if (
|
|
3831
|
-
ctx.
|
|
3832
|
-
return this.executeHandler(entry, ctx, finalize, wrapResponse)
|
|
3745
|
+
if (early === undefined) return runHandler()
|
|
3746
|
+
return fusedRespond(early, ctx, this.responseBodyTag, this.staticResponseHeaders)
|
|
3833
3747
|
}
|
|
3834
|
-
|
|
3835
|
-
|
|
3836
|
-
|
|
3837
|
-
|
|
3838
|
-
|
|
3839
|
-
|
|
3840
|
-
|
|
3841
|
-
|
|
3842
|
-
|
|
3843
|
-
|
|
3844
|
-
|
|
3845
|
-
|
|
3846
|
-
|
|
3847
|
-
|
|
3848
|
-
|
|
3849
|
-
|
|
3850
|
-
|
|
3851
|
-
this.handleValidationErrorRecovery(
|
|
3852
|
-
entry,
|
|
3853
|
-
rec,
|
|
3854
|
-
ctx,
|
|
3855
|
-
finalize,
|
|
3856
|
-
wrapResponse,
|
|
3857
|
-
result.issues!,
|
|
3858
|
-
"body",
|
|
3859
|
-
),
|
|
3748
|
+
const runDerive = (): MaybePromise<Response> => {
|
|
3749
|
+
let derived: unknown
|
|
3750
|
+
try {
|
|
3751
|
+
derived = derive(ctx)
|
|
3752
|
+
} catch (err) {
|
|
3753
|
+
return logError(err, ctx)
|
|
3754
|
+
}
|
|
3755
|
+
if (derived instanceof Promise) {
|
|
3756
|
+
return derived.then(
|
|
3757
|
+
(settled) => {
|
|
3758
|
+
if (isResponseResult(settled) || settled instanceof Response) {
|
|
3759
|
+
return fusedRespond(settled, ctx, this.responseBodyTag, this.staticResponseHeaders)
|
|
3760
|
+
}
|
|
3761
|
+
Object.assign(ctx, settled as object)
|
|
3762
|
+
return runBefore()
|
|
3763
|
+
},
|
|
3764
|
+
(err) => logError(err, ctx),
|
|
3860
3765
|
)
|
|
3861
3766
|
}
|
|
3862
|
-
|
|
3863
|
-
|
|
3864
|
-
|
|
3865
|
-
|
|
3866
|
-
|
|
3867
|
-
|
|
3868
|
-
|
|
3869
|
-
|
|
3870
|
-
|
|
3767
|
+
if (isResponseResult(derived) || derived instanceof Response) {
|
|
3768
|
+
return fusedRespond(derived, ctx, this.responseBodyTag, this.staticResponseHeaders)
|
|
3769
|
+
}
|
|
3770
|
+
Object.assign(ctx, derived as object)
|
|
3771
|
+
return runBefore()
|
|
3772
|
+
}
|
|
3773
|
+
if (querySchema === undefined) return runDerive()
|
|
3774
|
+
let validation: MaybePromise<StandardResult<unknown>>
|
|
3775
|
+
try {
|
|
3776
|
+
validation = querySchema["~standard"].validate(queryObjectOf(ctx[CONTEXT_SEARCH]))
|
|
3777
|
+
} catch (err) {
|
|
3778
|
+
return logError(err, ctx)
|
|
3871
3779
|
}
|
|
3872
|
-
return wrapResponse(validationError(result.issues))
|
|
3873
|
-
}
|
|
3874
|
-
ctx.body = result.value
|
|
3875
|
-
return this.executeHandler(entry, ctx, finalize)
|
|
3876
|
-
}
|
|
3877
|
-
|
|
3878
|
-
// @ts-expect-error TS6133 -- invoked structurally by compiled plans (internal/route-execution.ts)
|
|
3879
|
-
// biome-ignore lint/correctness/noUnusedPrivateClassMembers: invoked structurally by compiled plans (internal/route-execution.ts)
|
|
3880
|
-
private runQueryOnly<T>(
|
|
3881
|
-
entry: RouteEntry,
|
|
3882
|
-
ctx: RawContext,
|
|
3883
|
-
finalize: (result: unknown, set: CtxSet) => T,
|
|
3884
|
-
wrapResponse: (response: Response) => T,
|
|
3885
|
-
): MaybePromise<T> {
|
|
3886
|
-
try {
|
|
3887
|
-
// Call the validator directly for the raw StandardResult (read `.issues`/`.value`) - skip
|
|
3888
|
-
// `validateStandard`'s per-request wrapper-object allocation, mirroring the bodyOnly path.
|
|
3889
|
-
const validation = entry.schema!.query!["~standard"].validate(
|
|
3890
|
-
queryObjectOf(ctx[CONTEXT_SEARCH]),
|
|
3891
|
-
)
|
|
3892
3780
|
if (validation instanceof Promise) {
|
|
3893
3781
|
return validation.then(
|
|
3894
3782
|
(settled) => {
|
|
3895
|
-
|
|
3896
|
-
|
|
3897
|
-
|
|
3898
|
-
|
|
3899
|
-
this.handleLifecycleError(entry, err, ctx, finalize, wrapResponse),
|
|
3900
|
-
)
|
|
3901
|
-
: outcome
|
|
3902
|
-
} catch (err) {
|
|
3903
|
-
return this.handleLifecycleError(entry, err, ctx, finalize, wrapResponse)
|
|
3904
|
-
}
|
|
3783
|
+
if (settled.issues !== undefined)
|
|
3784
|
+
return this.wrapWebResponse(plainValidationError(settled.issues))
|
|
3785
|
+
ctx.query = settled.value
|
|
3786
|
+
return runDerive()
|
|
3905
3787
|
},
|
|
3906
|
-
(err) =>
|
|
3788
|
+
(err) => logError(err, ctx),
|
|
3907
3789
|
)
|
|
3908
3790
|
}
|
|
3909
|
-
|
|
3910
|
-
|
|
3911
|
-
|
|
3791
|
+
if (validation.issues !== undefined)
|
|
3792
|
+
return this.wrapWebResponse(plainValidationError(validation.issues))
|
|
3793
|
+
ctx.query = validation.value
|
|
3794
|
+
return runDerive()
|
|
3912
3795
|
}
|
|
3913
3796
|
}
|
|
3914
3797
|
|
|
3915
|
-
/**
|
|
3916
|
-
*
|
|
3917
|
-
|
|
3918
|
-
|
|
3919
|
-
|
|
3920
|
-
|
|
3921
|
-
|
|
3922
|
-
|
|
3923
|
-
|
|
3924
|
-
|
|
3925
|
-
|
|
3926
|
-
|
|
3927
|
-
|
|
3928
|
-
|
|
3929
|
-
|
|
3930
|
-
|
|
3931
|
-
|
|
3932
|
-
|
|
3933
|
-
|
|
3934
|
-
|
|
3935
|
-
|
|
3936
|
-
|
|
3937
|
-
|
|
3938
|
-
|
|
3798
|
+
/**
|
|
3799
|
+
* Same shape as `buildFusedDeriveBefore`, plus one `afterHandle` step. The closure runs (validate) →
|
|
3800
|
+
* derive → before → handler → after in one frame, the shape the realistic middleware route lands on
|
|
3801
|
+
* when it also wants a response transform (e.g. attaching a request id to a successful payload's
|
|
3802
|
+
* headers). `LifecycleExecutionLane` and `selectRouteLanes` guarantee `afterHandle === 1`, the
|
|
3803
|
+
* throw contract is the same as the generic program, and the `after` step is the final transform +
|
|
3804
|
+
* respond the generic program already does.
|
|
3805
|
+
*/
|
|
3806
|
+
private buildFusedDeriveBeforeAfter(
|
|
3807
|
+
handler: InternalHandler,
|
|
3808
|
+
derive: RawDerive,
|
|
3809
|
+
before: RawBeforeHandle,
|
|
3810
|
+
after: RawAfterHandle,
|
|
3811
|
+
querySchema: StandardSchemaV1 | undefined,
|
|
3812
|
+
maxBodyBytes: number,
|
|
3813
|
+
): FusedWebRunner {
|
|
3814
|
+
const logError = (err: unknown, ctx: RawContext): Response =>
|
|
3815
|
+
this.bareError(
|
|
3816
|
+
err,
|
|
3817
|
+
ctx,
|
|
3818
|
+
(result, set) => this.wrapWebResponse(toResponse(result, set)),
|
|
3819
|
+
this.wrapWebResponse,
|
|
3820
|
+
)
|
|
3821
|
+
return (source, params, search, signal, budget, platform, nativeContext) => {
|
|
3822
|
+
const ctx = nativeContext
|
|
3823
|
+
? RequestContext.native(source, params, search, maxBodyBytes, platform, this.protoPoisoning)
|
|
3824
|
+
: new RequestContext(
|
|
3825
|
+
source,
|
|
3826
|
+
params,
|
|
3827
|
+
search,
|
|
3828
|
+
signal,
|
|
3829
|
+
budget,
|
|
3830
|
+
platform,
|
|
3831
|
+
maxBodyBytes,
|
|
3832
|
+
this.protoPoisoning,
|
|
3833
|
+
)
|
|
3834
|
+
const runAfter = (result: unknown): MaybePromise<Response> => {
|
|
3835
|
+
let transformed: unknown
|
|
3836
|
+
try {
|
|
3837
|
+
transformed = after(result, ctx)
|
|
3838
|
+
} catch (err) {
|
|
3839
|
+
return logError(err, ctx)
|
|
3840
|
+
}
|
|
3841
|
+
if (transformed instanceof Promise) {
|
|
3842
|
+
return transformed.then(
|
|
3843
|
+
(value) =>
|
|
3844
|
+
fusedRespond(
|
|
3845
|
+
value,
|
|
3846
|
+
ctx,
|
|
3847
|
+
this.responseBodyTag,
|
|
3848
|
+
this.staticResponseHeaders,
|
|
3849
|
+
this.onResponseHooks.length === 0,
|
|
3850
|
+
),
|
|
3851
|
+
(err) => logError(err, ctx),
|
|
3939
3852
|
)
|
|
3940
3853
|
}
|
|
3941
|
-
return
|
|
3942
|
-
|
|
3943
|
-
recovery,
|
|
3854
|
+
return fusedRespond(
|
|
3855
|
+
transformed,
|
|
3944
3856
|
ctx,
|
|
3945
|
-
|
|
3946
|
-
|
|
3947
|
-
|
|
3948
|
-
"query",
|
|
3857
|
+
this.responseBodyTag,
|
|
3858
|
+
this.staticResponseHeaders,
|
|
3859
|
+
this.onResponseHooks.length === 0,
|
|
3949
3860
|
)
|
|
3950
3861
|
}
|
|
3951
|
-
|
|
3952
|
-
|
|
3953
|
-
|
|
3954
|
-
|
|
3955
|
-
|
|
3956
|
-
|
|
3957
|
-
|
|
3958
|
-
|
|
3959
|
-
|
|
3960
|
-
|
|
3961
|
-
|
|
3962
|
-
|
|
3963
|
-
|
|
3964
|
-
|
|
3965
|
-
const hooks = this.onResponseHooks
|
|
3966
|
-
let current = response
|
|
3967
|
-
for (let i = 0; i < hooks.length; i++) {
|
|
3968
|
-
const next = (hooks[i] as RawOnResponse)(current, req)
|
|
3969
|
-
if (next instanceof Promise) {
|
|
3970
|
-
return next.then((settled) => this.continueOnResponse(settled, i + 1, req))
|
|
3862
|
+
const runHandler = (): MaybePromise<Response> => {
|
|
3863
|
+
let result: unknown
|
|
3864
|
+
try {
|
|
3865
|
+
result = handler(ctx)
|
|
3866
|
+
} catch (err) {
|
|
3867
|
+
return logError(err, ctx)
|
|
3868
|
+
}
|
|
3869
|
+
if (result instanceof Promise) {
|
|
3870
|
+
return result.then(
|
|
3871
|
+
(value) => runAfter(value),
|
|
3872
|
+
(err) => logError(err, ctx),
|
|
3873
|
+
)
|
|
3874
|
+
}
|
|
3875
|
+
return runAfter(result)
|
|
3971
3876
|
}
|
|
3972
|
-
|
|
3973
|
-
|
|
3974
|
-
|
|
3975
|
-
|
|
3976
|
-
|
|
3977
|
-
|
|
3978
|
-
|
|
3979
|
-
|
|
3980
|
-
|
|
3981
|
-
|
|
3982
|
-
|
|
3983
|
-
|
|
3877
|
+
const runBefore = (): MaybePromise<Response> => {
|
|
3878
|
+
let early: unknown
|
|
3879
|
+
try {
|
|
3880
|
+
early = before(ctx)
|
|
3881
|
+
} catch (err) {
|
|
3882
|
+
return logError(err, ctx)
|
|
3883
|
+
}
|
|
3884
|
+
if (early instanceof Promise) {
|
|
3885
|
+
return early.then(
|
|
3886
|
+
(settled) => {
|
|
3887
|
+
if (settled === undefined) return runHandler()
|
|
3888
|
+
return fusedRespond(settled, ctx, this.responseBodyTag, this.staticResponseHeaders)
|
|
3889
|
+
},
|
|
3890
|
+
(err) => logError(err, ctx),
|
|
3984
3891
|
)
|
|
3985
|
-
|
|
3986
|
-
|
|
3987
|
-
|
|
3988
|
-
|
|
3989
|
-
|
|
3990
|
-
|
|
3991
|
-
|
|
3992
|
-
|
|
3993
|
-
|
|
3994
|
-
|
|
3995
|
-
|
|
3996
|
-
|
|
3997
|
-
|
|
3998
|
-
|
|
3999
|
-
|
|
4000
|
-
|
|
4001
|
-
|
|
4002
|
-
|
|
4003
|
-
|
|
4004
|
-
|
|
4005
|
-
|
|
4006
|
-
|
|
4007
|
-
|
|
4008
|
-
|
|
4009
|
-
|
|
4010
|
-
|
|
4011
|
-
|
|
4012
|
-
|
|
4013
|
-
// `.then()`-style callers entirely instead of reaching their rejection handling.
|
|
4014
|
-
return Promise.reject(error)
|
|
4015
|
-
}
|
|
4016
|
-
|
|
4017
|
-
/** Notify terminal observers in order while isolating both sync and async failures. */
|
|
4018
|
-
private notifyResponseFinalized(outcome: ResponseFinalization, req: Request): MaybePromise<void> {
|
|
4019
|
-
let pending: Promise<void> | undefined
|
|
4020
|
-
for (const hook of this.onResponseFinalizedHooks) {
|
|
4021
|
-
if (pending !== undefined) {
|
|
4022
|
-
pending = pending.then(async () => {
|
|
4023
|
-
try {
|
|
4024
|
-
await hook(outcome, req)
|
|
4025
|
-
} catch {
|
|
4026
|
-
// Terminal observation must never change request behavior.
|
|
4027
|
-
}
|
|
4028
|
-
})
|
|
4029
|
-
continue
|
|
3892
|
+
}
|
|
3893
|
+
if (early === undefined) return runHandler()
|
|
3894
|
+
return fusedRespond(early, ctx, this.responseBodyTag, this.staticResponseHeaders)
|
|
3895
|
+
}
|
|
3896
|
+
const runDerive = (): MaybePromise<Response> => {
|
|
3897
|
+
let derived: unknown
|
|
3898
|
+
try {
|
|
3899
|
+
derived = derive(ctx)
|
|
3900
|
+
} catch (err) {
|
|
3901
|
+
return logError(err, ctx)
|
|
3902
|
+
}
|
|
3903
|
+
if (derived instanceof Promise) {
|
|
3904
|
+
return derived.then(
|
|
3905
|
+
(settled) => {
|
|
3906
|
+
if (isResponseResult(settled) || settled instanceof Response) {
|
|
3907
|
+
return fusedRespond(settled, ctx, this.responseBodyTag, this.staticResponseHeaders)
|
|
3908
|
+
}
|
|
3909
|
+
Object.assign(ctx, settled as object)
|
|
3910
|
+
return runBefore()
|
|
3911
|
+
},
|
|
3912
|
+
(err) => logError(err, ctx),
|
|
3913
|
+
)
|
|
3914
|
+
}
|
|
3915
|
+
if (isResponseResult(derived) || derived instanceof Response) {
|
|
3916
|
+
return fusedRespond(derived, ctx, this.responseBodyTag, this.staticResponseHeaders)
|
|
3917
|
+
}
|
|
3918
|
+
Object.assign(ctx, derived as object)
|
|
3919
|
+
return runBefore()
|
|
4030
3920
|
}
|
|
3921
|
+
if (querySchema === undefined) return runDerive()
|
|
3922
|
+
let validation: MaybePromise<StandardResult<unknown>>
|
|
4031
3923
|
try {
|
|
4032
|
-
|
|
4033
|
-
|
|
4034
|
-
|
|
4035
|
-
// Terminal observation must never change request behavior.
|
|
3924
|
+
validation = querySchema["~standard"].validate(queryObjectOf(ctx[CONTEXT_SEARCH]))
|
|
3925
|
+
} catch (err) {
|
|
3926
|
+
return logError(err, ctx)
|
|
4036
3927
|
}
|
|
4037
|
-
|
|
4038
|
-
|
|
4039
|
-
|
|
4040
|
-
|
|
4041
|
-
|
|
4042
|
-
|
|
4043
|
-
|
|
4044
|
-
|
|
4045
|
-
|
|
4046
|
-
|
|
4047
|
-
|
|
4048
|
-
|
|
4049
|
-
|
|
4050
|
-
|
|
4051
|
-
|
|
4052
|
-
return current
|
|
4053
|
-
}
|
|
4054
|
-
|
|
4055
|
-
/**
|
|
4056
|
-
* Bound the response time. On timeout we abort `ctx.signal` (so cancellation-aware
|
|
4057
|
-
* handlers can bail) and return 503; the in-flight work keeps running but its
|
|
4058
|
-
* result is discarded - JS can't forcibly cancel a promise.
|
|
4059
|
-
*/
|
|
4060
|
-
private async withTimeout<T>(
|
|
4061
|
-
work: Promise<T>,
|
|
4062
|
-
controller: AbortController,
|
|
4063
|
-
onTimeout: () => T,
|
|
4064
|
-
timeoutMs: number,
|
|
4065
|
-
): Promise<T> {
|
|
4066
|
-
let timer: ReturnType<typeof setTimeout> | undefined
|
|
4067
|
-
const timeout = new Promise<T>((resolve) => {
|
|
4068
|
-
timer = setTimeout(() => {
|
|
4069
|
-
controller.abort()
|
|
4070
|
-
resolve(onTimeout())
|
|
4071
|
-
}, timeoutMs)
|
|
4072
|
-
})
|
|
4073
|
-
try {
|
|
4074
|
-
return await Promise.race([work, timeout])
|
|
4075
|
-
} finally {
|
|
4076
|
-
if (timer !== undefined) clearTimeout(timer)
|
|
3928
|
+
if (validation instanceof Promise) {
|
|
3929
|
+
return validation.then(
|
|
3930
|
+
(settled) => {
|
|
3931
|
+
if (settled.issues !== undefined)
|
|
3932
|
+
return this.wrapWebResponse(plainValidationError(settled.issues))
|
|
3933
|
+
ctx.query = settled.value
|
|
3934
|
+
return runDerive()
|
|
3935
|
+
},
|
|
3936
|
+
(err) => logError(err, ctx),
|
|
3937
|
+
)
|
|
3938
|
+
}
|
|
3939
|
+
if (validation.issues !== undefined)
|
|
3940
|
+
return this.wrapWebResponse(plainValidationError(validation.issues))
|
|
3941
|
+
ctx.query = validation.value
|
|
3942
|
+
return runDerive()
|
|
4077
3943
|
}
|
|
4078
3944
|
}
|
|
4079
3945
|
|
|
4080
3946
|
/**
|
|
4081
|
-
*
|
|
4082
|
-
*
|
|
4083
|
-
*
|
|
3947
|
+
* Body + lifecycle hooks fused: parse → validate → derive → before → handler → (after) in one
|
|
3948
|
+
* closure, the same win `buildFusedBodyRunner` gives for the no-hooks body shape. The shape is
|
|
3949
|
+
* `lifecycleHookLane` PLUS a body schema with no other validation conflicts; the lane is selected
|
|
3950
|
+
* ONLY when `schema.body !== undefined && schema.query/params/headers === undefined &&
|
|
3951
|
+
* schema.onValidationError === undefined && !defaultOnValidationError && !hasIdempotency &&
|
|
3952
|
+
* !hasLedger && derives === 1 && beforeHandle === 1 && afterHandle <= 1 && onError === 0 &&
|
|
3953
|
+
* !hasResponseContract && !hasDecorations && around === 0`. The same single `querySchema === undefined`
|
|
3954
|
+
* trick used in the no-body lane lets the build path stay simple: a query + body route falls back
|
|
3955
|
+
* to the generic program (this is rare in practice - validated query + validated body is usually a
|
|
3956
|
+
* "search results" route with a middleware chain on top, and the generic lane is fine there).
|
|
4084
3957
|
*/
|
|
4085
|
-
|
|
4086
|
-
|
|
4087
|
-
|
|
4088
|
-
|
|
4089
|
-
|
|
4090
|
-
|
|
4091
|
-
|
|
4092
|
-
|
|
4093
|
-
|
|
4094
|
-
|
|
4095
|
-
|
|
4096
|
-
|
|
4097
|
-
|
|
4098
|
-
|
|
4099
|
-
|
|
4100
|
-
|
|
4101
|
-
|
|
4102
|
-
|
|
4103
|
-
|
|
4104
|
-
|
|
4105
|
-
|
|
4106
|
-
|
|
4107
|
-
|
|
4108
|
-
|
|
4109
|
-
|
|
4110
|
-
|
|
3958
|
+
private buildFusedBodyDeriveBeforeAfter(
|
|
3959
|
+
handler: InternalHandler,
|
|
3960
|
+
derive: RawDerive,
|
|
3961
|
+
before: RawBeforeHandle,
|
|
3962
|
+
after: RawAfterHandle | undefined,
|
|
3963
|
+
bodySchema: StandardSchemaV1,
|
|
3964
|
+
maxBodyBytes: number,
|
|
3965
|
+
): FusedWebRunner {
|
|
3966
|
+
const logError = (err: unknown, ctx: RawContext): Response =>
|
|
3967
|
+
this.bareError(
|
|
3968
|
+
err,
|
|
3969
|
+
ctx,
|
|
3970
|
+
(result, set) => this.wrapWebResponse(toResponse(result, set)),
|
|
3971
|
+
this.wrapWebResponse,
|
|
3972
|
+
)
|
|
3973
|
+
return (source, params, search, signal, budget, platform, nativeContext) => {
|
|
3974
|
+
const ctx = nativeContext
|
|
3975
|
+
? RequestContext.native(source, params, search, maxBodyBytes, platform, this.protoPoisoning)
|
|
3976
|
+
: new RequestContext(
|
|
3977
|
+
source,
|
|
3978
|
+
params,
|
|
3979
|
+
search,
|
|
3980
|
+
signal,
|
|
3981
|
+
budget,
|
|
3982
|
+
platform,
|
|
3983
|
+
maxBodyBytes,
|
|
3984
|
+
this.protoPoisoning,
|
|
3985
|
+
)
|
|
3986
|
+
const runHandlerValidated = (): MaybePromise<Response> => {
|
|
3987
|
+
const runAfter = (result: unknown): MaybePromise<Response> => {
|
|
3988
|
+
if (after === undefined) {
|
|
3989
|
+
return fusedRespond(
|
|
3990
|
+
result,
|
|
3991
|
+
ctx,
|
|
3992
|
+
this.responseBodyTag,
|
|
3993
|
+
this.staticResponseHeaders,
|
|
3994
|
+
this.onResponseHooks.length === 0,
|
|
3995
|
+
)
|
|
3996
|
+
}
|
|
3997
|
+
let transformed: unknown
|
|
3998
|
+
try {
|
|
3999
|
+
transformed = after(result, ctx)
|
|
4000
|
+
} catch (err) {
|
|
4001
|
+
return logError(err, ctx)
|
|
4002
|
+
}
|
|
4003
|
+
if (transformed instanceof Promise) {
|
|
4004
|
+
return transformed.then(
|
|
4005
|
+
(value) =>
|
|
4006
|
+
fusedRespond(
|
|
4007
|
+
value,
|
|
4008
|
+
ctx,
|
|
4009
|
+
this.responseBodyTag,
|
|
4010
|
+
this.staticResponseHeaders,
|
|
4011
|
+
this.onResponseHooks.length === 0,
|
|
4012
|
+
),
|
|
4013
|
+
(err) => logError(err, ctx),
|
|
4014
|
+
)
|
|
4015
|
+
}
|
|
4016
|
+
return fusedRespond(
|
|
4017
|
+
transformed,
|
|
4018
|
+
ctx,
|
|
4019
|
+
this.responseBodyTag,
|
|
4020
|
+
this.staticResponseHeaders,
|
|
4021
|
+
this.onResponseHooks.length === 0,
|
|
4022
|
+
)
|
|
4023
|
+
}
|
|
4024
|
+
const runHandler = (): MaybePromise<Response> => {
|
|
4025
|
+
let result: unknown
|
|
4026
|
+
try {
|
|
4027
|
+
result = handler(ctx)
|
|
4028
|
+
} catch (err) {
|
|
4029
|
+
return logError(err, ctx)
|
|
4030
|
+
}
|
|
4031
|
+
if (result instanceof Promise) {
|
|
4032
|
+
return result.then(
|
|
4033
|
+
(value) => runAfter(value),
|
|
4034
|
+
(err) => logError(err, ctx),
|
|
4035
|
+
)
|
|
4036
|
+
}
|
|
4037
|
+
return runAfter(result)
|
|
4038
|
+
}
|
|
4039
|
+
const runBefore = (): MaybePromise<Response> => {
|
|
4040
|
+
let early: unknown
|
|
4041
|
+
try {
|
|
4042
|
+
early = before(ctx)
|
|
4043
|
+
} catch (err) {
|
|
4044
|
+
return logError(err, ctx)
|
|
4045
|
+
}
|
|
4046
|
+
if (early instanceof Promise) {
|
|
4047
|
+
return early.then(
|
|
4048
|
+
(settled) => {
|
|
4049
|
+
if (settled === undefined) return runHandler()
|
|
4050
|
+
return fusedRespond(settled, ctx, this.responseBodyTag, this.staticResponseHeaders)
|
|
4051
|
+
},
|
|
4052
|
+
(err) => logError(err, ctx),
|
|
4053
|
+
)
|
|
4054
|
+
}
|
|
4055
|
+
if (early === undefined) return runHandler()
|
|
4056
|
+
return fusedRespond(early, ctx, this.responseBodyTag, this.staticResponseHeaders)
|
|
4057
|
+
}
|
|
4058
|
+
let derived: unknown
|
|
4059
|
+
try {
|
|
4060
|
+
derived = derive(ctx)
|
|
4061
|
+
} catch (err) {
|
|
4062
|
+
return logError(err, ctx)
|
|
4063
|
+
}
|
|
4064
|
+
if (derived instanceof Promise) {
|
|
4065
|
+
return derived.then(
|
|
4066
|
+
(settled) => {
|
|
4067
|
+
if (isResponseResult(settled) || settled instanceof Response) {
|
|
4068
|
+
return fusedRespond(settled, ctx, this.responseBodyTag, this.staticResponseHeaders)
|
|
4069
|
+
}
|
|
4070
|
+
Object.assign(ctx, settled as object)
|
|
4071
|
+
return runBefore()
|
|
4072
|
+
},
|
|
4073
|
+
(err) => logError(err, ctx),
|
|
4111
4074
|
)
|
|
4112
4075
|
}
|
|
4113
|
-
|
|
4114
|
-
|
|
4115
|
-
|
|
4116
|
-
|
|
4117
|
-
|
|
4118
|
-
wrapResponse,
|
|
4119
|
-
validation,
|
|
4120
|
-
)
|
|
4076
|
+
if (isResponseResult(derived) || derived instanceof Response) {
|
|
4077
|
+
return fusedRespond(derived, ctx, this.responseBodyTag, this.staticResponseHeaders)
|
|
4078
|
+
}
|
|
4079
|
+
Object.assign(ctx, derived as object)
|
|
4080
|
+
return runBefore()
|
|
4121
4081
|
}
|
|
4122
|
-
const
|
|
4123
|
-
|
|
4124
|
-
|
|
4082
|
+
const onParsed = (parsed: unknown): MaybePromise<Response> => {
|
|
4083
|
+
let validation: StandardResult<unknown> | Promise<StandardResult<unknown>>
|
|
4084
|
+
try {
|
|
4085
|
+
validation = bodySchema["~standard"].validate(parsed)
|
|
4086
|
+
} catch (err) {
|
|
4087
|
+
return logError(err, ctx)
|
|
4088
|
+
}
|
|
4125
4089
|
if (validation instanceof Promise) {
|
|
4126
4090
|
return validation.then(
|
|
4127
|
-
(
|
|
4128
|
-
|
|
4129
|
-
|
|
4130
|
-
|
|
4131
|
-
|
|
4132
|
-
|
|
4133
|
-
|
|
4134
|
-
result,
|
|
4135
|
-
),
|
|
4136
|
-
(err) => this.handleLifecycleError(entry, err, ctx, finalize, wrapResponse),
|
|
4091
|
+
(settled) => {
|
|
4092
|
+
if (settled.issues !== undefined)
|
|
4093
|
+
return this.wrapWebResponse(plainValidationError(settled.issues))
|
|
4094
|
+
ctx.body = settled.value
|
|
4095
|
+
return runHandlerValidated()
|
|
4096
|
+
},
|
|
4097
|
+
(err) => logError(err, ctx),
|
|
4137
4098
|
)
|
|
4138
4099
|
}
|
|
4139
|
-
|
|
4140
|
-
|
|
4141
|
-
|
|
4142
|
-
|
|
4143
|
-
finalize,
|
|
4144
|
-
wrapResponse,
|
|
4145
|
-
validation,
|
|
4146
|
-
)
|
|
4147
|
-
}
|
|
4148
|
-
// Registration already knows whether this route has body/params/query validation. Most real
|
|
4149
|
-
// read routes have no body or params schema, so skip the generic stage ladder and go straight to
|
|
4150
|
-
// query validation + lifecycle hooks. The fallback below remains the complete path for routes
|
|
4151
|
-
// with body/params combinations and validation recovery.
|
|
4152
|
-
if (entry.schema?.body === undefined) {
|
|
4153
|
-
return this.runQueryAndLifecycle(entry, ctx, finalize, wrapResponse)
|
|
4154
|
-
}
|
|
4155
|
-
return this.runLifecycleAfterParams(entry, source, ctx, finalize, wrapResponse, undefined)
|
|
4156
|
-
} catch (err) {
|
|
4157
|
-
return this.handleLifecycleError(entry, err, ctx, finalize, wrapResponse)
|
|
4158
|
-
}
|
|
4159
|
-
}
|
|
4160
|
-
|
|
4161
|
-
private runLifecycleAfterHeadersResult<T>(
|
|
4162
|
-
entry: RouteEntry,
|
|
4163
|
-
source: RequestSource,
|
|
4164
|
-
ctx: RawContext,
|
|
4165
|
-
finalize: (result: unknown, set: CtxSet) => T,
|
|
4166
|
-
wrapResponse: (response: Response) => T,
|
|
4167
|
-
result: StandardResult<unknown>,
|
|
4168
|
-
): MaybePromise<T> {
|
|
4169
|
-
try {
|
|
4170
|
-
const headersError = this.applyLifecycleValidation(entry, result, ctx, "headers")
|
|
4171
|
-
if (headersError instanceof Promise) {
|
|
4172
|
-
return headersError.then(
|
|
4173
|
-
(error) =>
|
|
4174
|
-
error === undefined
|
|
4175
|
-
? this.runLifecycleAfterHeaders(entry, source, ctx, finalize, wrapResponse)
|
|
4176
|
-
: wrapResponse(error),
|
|
4177
|
-
(err) => this.handleLifecycleError(entry, err, ctx, finalize, wrapResponse),
|
|
4178
|
-
)
|
|
4100
|
+
if (validation.issues !== undefined)
|
|
4101
|
+
return this.wrapWebResponse(plainValidationError(validation.issues))
|
|
4102
|
+
ctx.body = validation.value
|
|
4103
|
+
return runHandlerValidated()
|
|
4179
4104
|
}
|
|
4180
|
-
return
|
|
4181
|
-
|
|
4182
|
-
|
|
4183
|
-
|
|
4184
|
-
|
|
4105
|
+
return readBodyFramed(
|
|
4106
|
+
source,
|
|
4107
|
+
maxBodyBytes,
|
|
4108
|
+
this.protoPoisoning,
|
|
4109
|
+
onParsed,
|
|
4110
|
+
(response) => this.wrapWebResponse(response),
|
|
4111
|
+
(err) => Promise.resolve(logError(err, ctx)),
|
|
4112
|
+
) as MaybePromise<Response>
|
|
4185
4113
|
}
|
|
4186
4114
|
}
|
|
4187
4115
|
|
|
4188
|
-
private
|
|
4189
|
-
|
|
4190
|
-
source: RequestSource,
|
|
4116
|
+
private bareError<T>(
|
|
4117
|
+
err: unknown,
|
|
4191
4118
|
ctx: RawContext,
|
|
4192
|
-
finalize: (result: unknown, set: CtxSet) => T,
|
|
4193
|
-
wrapResponse: (response: Response) => T,
|
|
4194
|
-
):
|
|
4195
|
-
|
|
4196
|
-
|
|
4197
|
-
|
|
4198
|
-
|
|
4199
|
-
|
|
4200
|
-
}
|
|
4201
|
-
const validation = paramsSchema["~standard"].validate(ctx.params)
|
|
4202
|
-
if (validation instanceof Promise) {
|
|
4203
|
-
return validation.then(
|
|
4204
|
-
(result) =>
|
|
4205
|
-
this.runLifecycleAfterParamsResult(entry, source, ctx, finalize, wrapResponse, result),
|
|
4206
|
-
(err) => this.handleLifecycleError(entry, err, ctx, finalize, wrapResponse),
|
|
4207
|
-
)
|
|
4208
|
-
}
|
|
4209
|
-
return this.runLifecycleAfterParamsResult(
|
|
4210
|
-
entry,
|
|
4211
|
-
source,
|
|
4119
|
+
finalize: (result: unknown, set: CtxSet, ctx: RawContext) => T,
|
|
4120
|
+
wrapResponse: (response: Response | ResponseResult) => T,
|
|
4121
|
+
): T {
|
|
4122
|
+
// The thrown-value contract lives in `bare-error-lane.ts`, one source of truth for both this
|
|
4123
|
+
// lane and the fused body runner. `responseSet` and the logger are injected because they reach
|
|
4124
|
+
// into class state; the closure is allocated only on the throw path, never on the hot lane.
|
|
4125
|
+
return renderBareError(
|
|
4126
|
+
err,
|
|
4212
4127
|
ctx,
|
|
4213
4128
|
finalize,
|
|
4214
4129
|
wrapResponse,
|
|
4215
|
-
|
|
4130
|
+
responseSet,
|
|
4131
|
+
(e, c) => this.logRequestError(e, c),
|
|
4132
|
+
() => this.internalErrorResponse(wrapResponse),
|
|
4216
4133
|
)
|
|
4217
4134
|
}
|
|
4218
4135
|
|
|
4219
|
-
/**
|
|
4220
|
-
|
|
4136
|
+
/**
|
|
4137
|
+
* Keep the ordinary thrown-error response independent from the caught value. Deliberate Response
|
|
4138
|
+
* control flow is handled by `renderBareError`; every other throw gets this fixed public envelope.
|
|
4139
|
+
*/
|
|
4140
|
+
private internalErrorResponse<T>(wrapResponse: (response: Response | ResponseResult) => T): T {
|
|
4141
|
+
return wrapResponse(plainError(500, "internal_error"))
|
|
4142
|
+
}
|
|
4143
|
+
|
|
4144
|
+
// @ts-expect-error TS6133 -- invoked structurally by compiled plans (internal/route-execution.ts)
|
|
4145
|
+
private runWithAround<T>(
|
|
4221
4146
|
entry: RouteEntry,
|
|
4222
4147
|
ctx: RawContext,
|
|
4148
|
+
run: () => MaybePromise<T>,
|
|
4223
4149
|
finalize: (result: unknown, set: CtxSet) => T,
|
|
4224
|
-
wrapResponse: (response: Response) => T,
|
|
4150
|
+
wrapResponse: (response: Response | ResponseResult) => T,
|
|
4225
4151
|
): MaybePromise<T> {
|
|
4152
|
+
let outcome: MaybePromise<T>
|
|
4226
4153
|
try {
|
|
4227
|
-
|
|
4228
|
-
// non-null access here removes the per-request schema-presence branch from realistic GETs.
|
|
4229
|
-
const validation = entry.schema!.query!["~standard"].validate(
|
|
4230
|
-
queryObjectOf(ctx[CONTEXT_SEARCH]),
|
|
4231
|
-
)
|
|
4232
|
-
if (validation instanceof Promise) {
|
|
4233
|
-
return validation.then(
|
|
4234
|
-
(result) => {
|
|
4235
|
-
try {
|
|
4236
|
-
return this.runQueryAndLifecycleResult(entry, ctx, finalize, wrapResponse, result)
|
|
4237
|
-
} catch (err) {
|
|
4238
|
-
return this.handleLifecycleError(entry, err, ctx, finalize, wrapResponse)
|
|
4239
|
-
}
|
|
4240
|
-
},
|
|
4241
|
-
(err) => this.handleLifecycleError(entry, err, ctx, finalize, wrapResponse),
|
|
4242
|
-
)
|
|
4243
|
-
}
|
|
4244
|
-
return this.runQueryAndLifecycleResult(entry, ctx, finalize, wrapResponse, validation)
|
|
4154
|
+
outcome = this.runAround(entry.around, ctx, run)
|
|
4245
4155
|
} catch (err) {
|
|
4246
4156
|
return this.handleLifecycleError(entry, err, ctx, finalize, wrapResponse)
|
|
4247
4157
|
}
|
|
4158
|
+
return outcome instanceof Promise
|
|
4159
|
+
? outcome.catch((err) => this.handleLifecycleError(entry, err, ctx, finalize, wrapResponse))
|
|
4160
|
+
: outcome
|
|
4248
4161
|
}
|
|
4249
4162
|
|
|
4250
|
-
/**
|
|
4163
|
+
/** Execute the registration-compiled general stage program. */
|
|
4251
4164
|
// @ts-expect-error TS6133 -- invoked structurally by compiled plans (internal/route-execution.ts)
|
|
4252
|
-
|
|
4253
|
-
private runLifecycleBody<T>(
|
|
4165
|
+
private runProgram<T>(
|
|
4254
4166
|
entry: RouteEntry,
|
|
4255
4167
|
source: RequestSource,
|
|
4256
4168
|
ctx: RawContext,
|
|
4257
4169
|
finalize: (result: unknown, set: CtxSet) => T,
|
|
4258
|
-
wrapResponse: (response: Response) => T,
|
|
4170
|
+
wrapResponse: (response: Response | ResponseResult) => T,
|
|
4259
4171
|
): MaybePromise<T> {
|
|
4260
|
-
return this.
|
|
4261
|
-
(bodyError) =>
|
|
4262
|
-
bodyError === undefined
|
|
4263
|
-
? this.runLifecycleHooks(entry, ctx, finalize, wrapResponse)
|
|
4264
|
-
: wrapResponse(bodyError),
|
|
4265
|
-
(err) => this.handleLifecycleError(entry, err, ctx, finalize, wrapResponse),
|
|
4266
|
-
)
|
|
4172
|
+
return executeRouteProgram(this, entry, entry.program, source, ctx, finalize, wrapResponse)
|
|
4267
4173
|
}
|
|
4268
4174
|
|
|
4269
|
-
/**
|
|
4270
|
-
// @ts-expect-error TS6133 -- invoked structurally by
|
|
4271
|
-
|
|
4272
|
-
private runLifecycleBodyQuery<T>(
|
|
4175
|
+
/** Bind a compiled validation stage to its stable schema and the existing recovery contract. */
|
|
4176
|
+
// @ts-expect-error TS6133 -- invoked structurally by the general route program
|
|
4177
|
+
private validateProgramStage(
|
|
4273
4178
|
entry: RouteEntry,
|
|
4179
|
+
stage: Extract<RouteProgramStage, { kind: "headers" | "params" | "body" | "query" }>,
|
|
4274
4180
|
source: RequestSource,
|
|
4275
4181
|
ctx: RawContext,
|
|
4276
|
-
|
|
4277
|
-
|
|
4278
|
-
|
|
4279
|
-
|
|
4280
|
-
|
|
4281
|
-
|
|
4282
|
-
|
|
4283
|
-
|
|
4284
|
-
|
|
4285
|
-
)
|
|
4182
|
+
): MaybePromise<Response | ResponseResult | undefined> {
|
|
4183
|
+
const input =
|
|
4184
|
+
stage.kind === "headers"
|
|
4185
|
+
? headerObjectOf(source.headers)
|
|
4186
|
+
: stage.kind === "params"
|
|
4187
|
+
? ctx.params
|
|
4188
|
+
: stage.kind === "query"
|
|
4189
|
+
? queryObjectOf(ctx[CONTEXT_SEARCH])
|
|
4190
|
+
: undefined
|
|
4191
|
+
if (stage.kind === "body") return this.readProgramBody(entry, source, ctx)
|
|
4192
|
+
const validation = stage.schema["~standard"].validate(input)
|
|
4193
|
+
return validation instanceof Promise
|
|
4194
|
+
? validation.then((result) => this.applyLifecycleValidation(entry, result, ctx, stage.kind))
|
|
4195
|
+
: this.applyLifecycleValidation(entry, validation, ctx, stage.kind)
|
|
4286
4196
|
}
|
|
4287
4197
|
|
|
4288
|
-
/**
|
|
4289
|
-
private
|
|
4198
|
+
/** Preserve the existing bounded body reader and its fail-closed framing/prototype checks. */
|
|
4199
|
+
private readProgramBody(
|
|
4290
4200
|
entry: RouteEntry,
|
|
4201
|
+
source: RequestSource,
|
|
4291
4202
|
ctx: RawContext,
|
|
4292
|
-
|
|
4293
|
-
|
|
4294
|
-
|
|
4295
|
-
|
|
4296
|
-
return querySchema === undefined
|
|
4297
|
-
? this.runLifecycleHooks(entry, ctx, finalize, wrapResponse)
|
|
4298
|
-
: this.runLifecycleQuery(entry, ctx, finalize, wrapResponse)
|
|
4203
|
+
): Promise<Response | ResponseResult | undefined> {
|
|
4204
|
+
const bodySchema = entry.program.bodySchema
|
|
4205
|
+
if (bodySchema === undefined) return Promise.resolve(plainError(415, "unsupported_media_type"))
|
|
4206
|
+
return this.readAndValidateBody(source, entry, ctx, bodySchema, entry.program.bodyLimit)
|
|
4299
4207
|
}
|
|
4300
4208
|
|
|
4301
|
-
|
|
4209
|
+
/** Keep thrown Response/status and redacted 500 semantics in one existing error boundary. */
|
|
4210
|
+
// @ts-expect-error TS6133 -- invoked structurally by the general route program
|
|
4211
|
+
private handleProgramError<T>(
|
|
4302
4212
|
entry: RouteEntry,
|
|
4213
|
+
error: unknown,
|
|
4303
4214
|
ctx: RawContext,
|
|
4304
4215
|
finalize: (result: unknown, set: CtxSet) => T,
|
|
4305
|
-
wrapResponse: (response: Response) => T,
|
|
4306
|
-
result: StandardResult<unknown>,
|
|
4216
|
+
wrapResponse: (response: Response | ResponseResult) => T,
|
|
4307
4217
|
): MaybePromise<T> {
|
|
4308
|
-
|
|
4309
|
-
if (queryError instanceof Promise) {
|
|
4310
|
-
return queryError.then(
|
|
4311
|
-
(error) =>
|
|
4312
|
-
error === undefined
|
|
4313
|
-
? this.runLifecycleHooks(entry, ctx, finalize, wrapResponse)
|
|
4314
|
-
: wrapResponse(error),
|
|
4315
|
-
(err) => this.handleLifecycleError(entry, err, ctx, finalize, wrapResponse),
|
|
4316
|
-
)
|
|
4317
|
-
}
|
|
4318
|
-
if (queryError !== undefined) return wrapResponse(queryError)
|
|
4319
|
-
return this.runLifecycleHooks(entry, ctx, finalize, wrapResponse)
|
|
4218
|
+
return this.handleLifecycleError(entry, error, ctx, finalize, wrapResponse)
|
|
4320
4219
|
}
|
|
4321
4220
|
|
|
4322
|
-
|
|
4221
|
+
/** Finish a successful program result through response-contract enforcement and finalization. */
|
|
4222
|
+
// @ts-expect-error TS6133 -- invoked structurally by the general route program
|
|
4223
|
+
private finishProgramResult<T>(
|
|
4323
4224
|
entry: RouteEntry,
|
|
4324
|
-
source: RequestSource,
|
|
4325
4225
|
ctx: RawContext,
|
|
4226
|
+
result: unknown,
|
|
4326
4227
|
finalize: (result: unknown, set: CtxSet) => T,
|
|
4327
|
-
wrapResponse: (response: Response) => T,
|
|
4328
|
-
result: StandardResult<unknown>,
|
|
4228
|
+
wrapResponse: (response: Response | ResponseResult) => T,
|
|
4329
4229
|
): MaybePromise<T> {
|
|
4330
|
-
|
|
4331
|
-
return this.runLifecycleAfterParams(
|
|
4332
|
-
entry,
|
|
4333
|
-
source,
|
|
4334
|
-
ctx,
|
|
4335
|
-
finalize,
|
|
4336
|
-
wrapResponse,
|
|
4337
|
-
this.applyLifecycleValidation(entry, result, ctx, "params"),
|
|
4338
|
-
)
|
|
4339
|
-
} catch (err) {
|
|
4340
|
-
return this.handleLifecycleError(entry, err, ctx, finalize, wrapResponse)
|
|
4341
|
-
}
|
|
4230
|
+
return this.finishLifecycleContract(entry, ctx, finalize, wrapResponse, result)
|
|
4342
4231
|
}
|
|
4343
4232
|
|
|
4344
|
-
private
|
|
4345
|
-
|
|
4346
|
-
source: RequestSource,
|
|
4233
|
+
private runAround<T>(
|
|
4234
|
+
hooks: ReadonlyArray<RawAround>,
|
|
4347
4235
|
ctx: RawContext,
|
|
4348
|
-
|
|
4349
|
-
wrapResponse: (response: Response) => T,
|
|
4350
|
-
paramsError: MaybePromise<Response | undefined>,
|
|
4236
|
+
run: () => MaybePromise<T>,
|
|
4351
4237
|
): MaybePromise<T> {
|
|
4352
|
-
|
|
4353
|
-
return
|
|
4354
|
-
|
|
4355
|
-
|
|
4356
|
-
|
|
4357
|
-
|
|
4238
|
+
const dispatch = (index: number): MaybePromise<T> => {
|
|
4239
|
+
if (index >= hooks.length) return run()
|
|
4240
|
+
const hook = hooks[index]!
|
|
4241
|
+
let called = false
|
|
4242
|
+
return hook(ctx, () => {
|
|
4243
|
+
if (called) throw new Error("around next() called multiple times")
|
|
4244
|
+
called = true
|
|
4245
|
+
return dispatch(index + 1)
|
|
4246
|
+
})
|
|
4358
4247
|
}
|
|
4359
|
-
return
|
|
4360
|
-
entry,
|
|
4361
|
-
source,
|
|
4362
|
-
ctx,
|
|
4363
|
-
finalize,
|
|
4364
|
-
wrapResponse,
|
|
4365
|
-
paramsError,
|
|
4366
|
-
)
|
|
4248
|
+
return dispatch(0)
|
|
4367
4249
|
}
|
|
4368
4250
|
|
|
4369
|
-
|
|
4251
|
+
// @ts-expect-error TS6133 -- invoked structurally by compiled plans (internal/route-execution.ts)
|
|
4252
|
+
private runBodyOnly<T>(
|
|
4370
4253
|
entry: RouteEntry,
|
|
4371
4254
|
source: RequestSource,
|
|
4372
4255
|
ctx: RawContext,
|
|
4373
4256
|
finalize: (result: unknown, set: CtxSet) => T,
|
|
4374
|
-
wrapResponse: (response: Response) => T,
|
|
4375
|
-
|
|
4376
|
-
|
|
4377
|
-
|
|
4378
|
-
|
|
4379
|
-
|
|
4380
|
-
|
|
4381
|
-
|
|
4382
|
-
)
|
|
4383
|
-
|
|
4384
|
-
return this.runLifecycleAfterBody(entry, ctx, finalize, wrapResponse, undefined)
|
|
4385
|
-
}
|
|
4386
|
-
|
|
4387
|
-
private runLifecycleAfterBody<T>(
|
|
4388
|
-
entry: RouteEntry,
|
|
4389
|
-
ctx: RawContext,
|
|
4390
|
-
finalize: (result: unknown, set: CtxSet) => T,
|
|
4391
|
-
wrapResponse: (response: Response) => T,
|
|
4392
|
-
bodyError: Response | undefined,
|
|
4393
|
-
): MaybePromise<T> {
|
|
4394
|
-
if (bodyError !== undefined) return wrapResponse(bodyError)
|
|
4395
|
-
const querySchema = entry.schema?.query
|
|
4396
|
-
if (querySchema === undefined) return this.runLifecycleHooks(entry, ctx, finalize, wrapResponse)
|
|
4397
|
-
const validation = querySchema["~standard"].validate(queryObjectOf(ctx[CONTEXT_SEARCH]))
|
|
4398
|
-
if (validation instanceof Promise) {
|
|
4399
|
-
return validation.then(
|
|
4400
|
-
(result) => this.runLifecycleAfterQueryResult(entry, ctx, finalize, wrapResponse, result),
|
|
4401
|
-
(err) => this.handleLifecycleError(entry, err, ctx, finalize, wrapResponse),
|
|
4402
|
-
)
|
|
4403
|
-
}
|
|
4404
|
-
return this.runLifecycleAfterQueryResult(entry, ctx, finalize, wrapResponse, validation)
|
|
4257
|
+
wrapResponse: (response: Response | ResponseResult) => T,
|
|
4258
|
+
): Promise<T> {
|
|
4259
|
+
return readBodyFramed(
|
|
4260
|
+
source,
|
|
4261
|
+
entry.bodyLimit ?? UNLIMITED_BODY_BYTES,
|
|
4262
|
+
this.protoPoisoning,
|
|
4263
|
+
(parsed) => this.finishBodyOnly(entry, parsed, ctx, finalize, wrapResponse),
|
|
4264
|
+
wrapResponse,
|
|
4265
|
+
(err) => this.handleLifecycleError(entry, err, ctx, finalize, wrapResponse),
|
|
4266
|
+
)
|
|
4405
4267
|
}
|
|
4406
4268
|
|
|
4407
|
-
|
|
4269
|
+
/** Validate + run the handler for the bodyOnly path - shared by the inline fast path and the
|
|
4270
|
+
* streaming fallback. A method (not per-request closures) so the hot path allocates nothing
|
|
4271
|
+
* beyond the one `.then` continuation. */
|
|
4272
|
+
private finishBodyOnly<T>(
|
|
4408
4273
|
entry: RouteEntry,
|
|
4274
|
+
parsed: unknown,
|
|
4409
4275
|
ctx: RawContext,
|
|
4410
4276
|
finalize: (result: unknown, set: CtxSet) => T,
|
|
4411
|
-
wrapResponse: (response: Response) => T,
|
|
4412
|
-
result: StandardResult<unknown>,
|
|
4277
|
+
wrapResponse: (response: Response | ResponseResult) => T,
|
|
4413
4278
|
): MaybePromise<T> {
|
|
4414
4279
|
try {
|
|
4415
|
-
const
|
|
4416
|
-
|
|
4417
|
-
|
|
4418
|
-
|
|
4280
|
+
const bodySchema = entry.schema!.body!
|
|
4281
|
+
const validation = bodySchema["~standard"].validate(parsed)
|
|
4282
|
+
if (validation instanceof Promise) {
|
|
4283
|
+
return validation.then(
|
|
4284
|
+
(result) => {
|
|
4285
|
+
try {
|
|
4286
|
+
const outcome = this.applyBodyValidation(entry, result, ctx, finalize, wrapResponse)
|
|
4287
|
+
return outcome instanceof Promise
|
|
4288
|
+
? outcome.catch((err) =>
|
|
4289
|
+
this.handleLifecycleError(entry, err, ctx, finalize, wrapResponse),
|
|
4290
|
+
)
|
|
4291
|
+
: outcome
|
|
4292
|
+
} catch (err) {
|
|
4293
|
+
return this.handleLifecycleError(entry, err, ctx, finalize, wrapResponse)
|
|
4294
|
+
}
|
|
4295
|
+
},
|
|
4419
4296
|
(err) => this.handleLifecycleError(entry, err, ctx, finalize, wrapResponse),
|
|
4420
4297
|
)
|
|
4421
4298
|
}
|
|
4422
|
-
|
|
4299
|
+
const outcome = this.applyBodyValidation(entry, validation, ctx, finalize, wrapResponse)
|
|
4300
|
+
return outcome instanceof Promise
|
|
4301
|
+
? outcome.catch((err) => this.handleLifecycleError(entry, err, ctx, finalize, wrapResponse))
|
|
4302
|
+
: outcome
|
|
4423
4303
|
} catch (err) {
|
|
4424
4304
|
return this.handleLifecycleError(entry, err, ctx, finalize, wrapResponse)
|
|
4425
4305
|
}
|
|
4426
4306
|
}
|
|
4427
4307
|
|
|
4428
|
-
private
|
|
4308
|
+
private executeHandler<T>(
|
|
4429
4309
|
entry: RouteEntry,
|
|
4430
4310
|
ctx: RawContext,
|
|
4431
4311
|
finalize: (result: unknown, set: CtxSet) => T,
|
|
4432
|
-
wrapResponse
|
|
4433
|
-
queryError: Response | undefined,
|
|
4312
|
+
wrapResponse?: (response: Response | ResponseResult) => T,
|
|
4434
4313
|
): MaybePromise<T> {
|
|
4435
|
-
if (
|
|
4436
|
-
|
|
4314
|
+
if (entry.hasDecorations) Object.assign(ctx, entry.decorations)
|
|
4315
|
+
const handlerOutput = entry.handler(ctx)
|
|
4316
|
+
if (handlerOutput instanceof Promise) {
|
|
4317
|
+
return handlerOutput.then(
|
|
4318
|
+
(value) => finalize(value, responseSet(ctx)),
|
|
4319
|
+
(err) => {
|
|
4320
|
+
if (wrapResponse) {
|
|
4321
|
+
return this.handleLifecycleError(entry, err, ctx, finalize, wrapResponse)
|
|
4322
|
+
}
|
|
4323
|
+
throw err
|
|
4324
|
+
},
|
|
4325
|
+
)
|
|
4326
|
+
}
|
|
4327
|
+
return finalize(handlerOutput, responseSet(ctx))
|
|
4437
4328
|
}
|
|
4438
4329
|
|
|
4439
|
-
private
|
|
4330
|
+
private handleValidationErrorRecovery<T>(
|
|
4440
4331
|
entry: RouteEntry,
|
|
4332
|
+
recovery: unknown,
|
|
4441
4333
|
ctx: RawContext,
|
|
4442
4334
|
finalize: (result: unknown, set: CtxSet) => T,
|
|
4443
|
-
wrapResponse: (response: Response) => T,
|
|
4335
|
+
wrapResponse: (response: Response | ResponseResult) => T,
|
|
4336
|
+
originalIssues: ReadonlyArray<StandardIssue>,
|
|
4337
|
+
kind: "body" | "query",
|
|
4444
4338
|
): MaybePromise<T> {
|
|
4445
|
-
if (
|
|
4446
|
-
|
|
4447
|
-
|
|
4448
|
-
|
|
4449
|
-
if (
|
|
4450
|
-
|
|
4451
|
-
|
|
4452
|
-
|
|
4453
|
-
|
|
4339
|
+
if (recovery !== undefined) {
|
|
4340
|
+
if (recovery instanceof Response) {
|
|
4341
|
+
return wrapResponse(recovery)
|
|
4342
|
+
}
|
|
4343
|
+
if (kind === "body" && entry.schema?.body) {
|
|
4344
|
+
const validation = entry.schema.body["~standard"].validate(recovery)
|
|
4345
|
+
if (validation instanceof Promise) {
|
|
4346
|
+
return validation.then((settled) => {
|
|
4347
|
+
if (settled.issues !== undefined)
|
|
4348
|
+
return wrapResponse(plainValidationError(settled.issues))
|
|
4349
|
+
ctx.body = settled.value
|
|
4350
|
+
return this.executeHandler(entry, ctx, finalize)
|
|
4351
|
+
})
|
|
4454
4352
|
}
|
|
4455
|
-
|
|
4353
|
+
if (validation.issues !== undefined)
|
|
4354
|
+
return wrapResponse(plainValidationError(validation.issues))
|
|
4355
|
+
ctx.body = validation.value
|
|
4356
|
+
return this.executeHandler(entry, ctx, finalize)
|
|
4456
4357
|
}
|
|
4457
|
-
|
|
4458
|
-
const
|
|
4459
|
-
if (
|
|
4460
|
-
return
|
|
4358
|
+
if (kind === "query" && entry.schema?.query) {
|
|
4359
|
+
const validation = entry.schema.query["~standard"].validate(recovery)
|
|
4360
|
+
if (validation instanceof Promise) {
|
|
4361
|
+
return validation.then(
|
|
4362
|
+
(settled) => {
|
|
4363
|
+
if (settled.issues !== undefined)
|
|
4364
|
+
return wrapResponse(plainValidationError(settled.issues))
|
|
4365
|
+
ctx.query = settled.value
|
|
4366
|
+
return this.executeHandler(entry, ctx, finalize, wrapResponse)
|
|
4367
|
+
},
|
|
4368
|
+
(err) => this.handleLifecycleError(entry, err, ctx, finalize, wrapResponse),
|
|
4369
|
+
)
|
|
4461
4370
|
}
|
|
4462
|
-
if (
|
|
4371
|
+
if (validation.issues !== undefined)
|
|
4372
|
+
return wrapResponse(plainValidationError(validation.issues))
|
|
4373
|
+
ctx.query = validation.value
|
|
4374
|
+
return this.executeHandler(entry, ctx, finalize, wrapResponse)
|
|
4463
4375
|
}
|
|
4464
|
-
|
|
4465
|
-
|
|
4466
|
-
|
|
4467
|
-
|
|
4468
|
-
|
|
4376
|
+
}
|
|
4377
|
+
return wrapResponse(plainValidationError(originalIssues))
|
|
4378
|
+
}
|
|
4379
|
+
|
|
4380
|
+
private applyBodyValidation<T>(
|
|
4381
|
+
entry: RouteEntry,
|
|
4382
|
+
result: StandardResult<unknown>,
|
|
4383
|
+
ctx: RawContext,
|
|
4384
|
+
finalize: (result: unknown, set: CtxSet) => T,
|
|
4385
|
+
wrapResponse: (response: Response | ResponseResult) => T,
|
|
4386
|
+
): MaybePromise<T> {
|
|
4387
|
+
if (result.issues !== undefined) {
|
|
4388
|
+
const hook = entry.schema?.onValidationError ?? this.defaultOnValidationError
|
|
4389
|
+
if (hook) {
|
|
4390
|
+
const recovery = hook(result.issues, ctx as unknown as Context, "body")
|
|
4391
|
+
if (recovery instanceof Promise) {
|
|
4392
|
+
return recovery.then((rec) =>
|
|
4393
|
+
this.handleValidationErrorRecovery(
|
|
4394
|
+
entry,
|
|
4395
|
+
rec,
|
|
4396
|
+
ctx,
|
|
4397
|
+
finalize,
|
|
4398
|
+
wrapResponse,
|
|
4399
|
+
result.issues!,
|
|
4400
|
+
"body",
|
|
4401
|
+
),
|
|
4402
|
+
)
|
|
4403
|
+
}
|
|
4404
|
+
return this.handleValidationErrorRecovery(
|
|
4405
|
+
entry,
|
|
4406
|
+
recovery,
|
|
4407
|
+
ctx,
|
|
4408
|
+
finalize,
|
|
4409
|
+
wrapResponse,
|
|
4410
|
+
result.issues,
|
|
4411
|
+
"body",
|
|
4469
4412
|
)
|
|
4470
4413
|
}
|
|
4471
|
-
return
|
|
4472
|
-
} catch (err) {
|
|
4473
|
-
return this.handleLifecycleError(entry, err, ctx, finalize, wrapResponse)
|
|
4414
|
+
return wrapResponse(plainValidationError(result.issues))
|
|
4474
4415
|
}
|
|
4416
|
+
ctx.body = result.value
|
|
4417
|
+
return this.executeHandler(entry, ctx, finalize)
|
|
4475
4418
|
}
|
|
4476
4419
|
|
|
4477
|
-
|
|
4478
|
-
private
|
|
4420
|
+
// @ts-expect-error TS6133 -- invoked structurally by compiled plans (internal/route-execution.ts)
|
|
4421
|
+
private runQueryOnly<T>(
|
|
4479
4422
|
entry: RouteEntry,
|
|
4480
4423
|
ctx: RawContext,
|
|
4481
4424
|
finalize: (result: unknown, set: CtxSet) => T,
|
|
4482
|
-
wrapResponse: (response: Response) => T,
|
|
4425
|
+
wrapResponse: (response: Response | ResponseResult) => T,
|
|
4483
4426
|
): MaybePromise<T> {
|
|
4484
4427
|
try {
|
|
4485
|
-
|
|
4486
|
-
|
|
4487
|
-
|
|
4488
|
-
|
|
4489
|
-
|
|
4490
|
-
|
|
4491
|
-
|
|
4492
|
-
|
|
4493
|
-
|
|
4494
|
-
|
|
4495
|
-
|
|
4496
|
-
|
|
4497
|
-
|
|
4498
|
-
|
|
4499
|
-
|
|
4500
|
-
|
|
4501
|
-
|
|
4428
|
+
// Call the validator directly for the raw StandardResult (read `.issues`/`.value`) - skip
|
|
4429
|
+
// `validateStandard`'s per-request wrapper-object allocation, mirroring the bodyOnly path.
|
|
4430
|
+
const validation = entry.schema!.query!["~standard"].validate(
|
|
4431
|
+
queryObjectOf(ctx[CONTEXT_SEARCH]),
|
|
4432
|
+
)
|
|
4433
|
+
if (validation instanceof Promise) {
|
|
4434
|
+
return validation.then(
|
|
4435
|
+
(settled) => {
|
|
4436
|
+
try {
|
|
4437
|
+
const outcome = this.applyQueryValidation(entry, settled, ctx, finalize, wrapResponse)
|
|
4438
|
+
return outcome instanceof Promise
|
|
4439
|
+
? outcome.catch((err) =>
|
|
4440
|
+
this.handleLifecycleError(entry, err, ctx, finalize, wrapResponse),
|
|
4441
|
+
)
|
|
4442
|
+
: outcome
|
|
4443
|
+
} catch (err) {
|
|
4444
|
+
return this.handleLifecycleError(entry, err, ctx, finalize, wrapResponse)
|
|
4445
|
+
}
|
|
4446
|
+
},
|
|
4502
4447
|
(err) => this.handleLifecycleError(entry, err, ctx, finalize, wrapResponse),
|
|
4503
4448
|
)
|
|
4504
4449
|
}
|
|
4505
|
-
return this.
|
|
4450
|
+
return this.applyQueryValidation(entry, validation, ctx, finalize, wrapResponse)
|
|
4506
4451
|
} catch (err) {
|
|
4507
4452
|
return this.handleLifecycleError(entry, err, ctx, finalize, wrapResponse)
|
|
4508
4453
|
}
|
|
4509
4454
|
}
|
|
4510
4455
|
|
|
4511
|
-
/**
|
|
4512
|
-
|
|
4456
|
+
/** Validate-result → set `ctx.query` → run handler. A method (not a per-request closure), the
|
|
4457
|
+
* query analogue of {@link applyBodyValidation}. */
|
|
4458
|
+
private applyQueryValidation<T>(
|
|
4513
4459
|
entry: RouteEntry,
|
|
4460
|
+
result: StandardResult<unknown>,
|
|
4514
4461
|
ctx: RawContext,
|
|
4515
4462
|
finalize: (result: unknown, set: CtxSet) => T,
|
|
4516
|
-
wrapResponse: (response: Response) => T,
|
|
4517
|
-
result: unknown,
|
|
4463
|
+
wrapResponse: (response: Response | ResponseResult) => T,
|
|
4518
4464
|
): MaybePromise<T> {
|
|
4519
|
-
|
|
4520
|
-
|
|
4521
|
-
|
|
4522
|
-
|
|
4465
|
+
if (result.issues !== undefined) {
|
|
4466
|
+
const hook = entry.schema?.onValidationError ?? this.defaultOnValidationError
|
|
4467
|
+
if (hook) {
|
|
4468
|
+
const recovery = hook(result.issues, ctx as unknown as Context, "query")
|
|
4469
|
+
if (recovery instanceof Promise) {
|
|
4470
|
+
return recovery.then((rec) =>
|
|
4471
|
+
this.handleValidationErrorRecovery(
|
|
4472
|
+
entry,
|
|
4473
|
+
rec,
|
|
4474
|
+
ctx,
|
|
4475
|
+
finalize,
|
|
4476
|
+
wrapResponse,
|
|
4477
|
+
result.issues!,
|
|
4478
|
+
"query",
|
|
4479
|
+
),
|
|
4480
|
+
)
|
|
4481
|
+
}
|
|
4482
|
+
return this.handleValidationErrorRecovery(
|
|
4483
|
+
entry,
|
|
4484
|
+
recovery,
|
|
4485
|
+
ctx,
|
|
4486
|
+
finalize,
|
|
4487
|
+
wrapResponse,
|
|
4488
|
+
result.issues,
|
|
4489
|
+
"query",
|
|
4490
|
+
)
|
|
4491
|
+
}
|
|
4492
|
+
return wrapResponse(plainValidationError(result.issues))
|
|
4523
4493
|
}
|
|
4494
|
+
ctx.query = result.value
|
|
4495
|
+
return this.executeHandler(entry, ctx, finalize, wrapResponse)
|
|
4524
4496
|
}
|
|
4525
4497
|
|
|
4526
|
-
|
|
4527
|
-
|
|
4528
|
-
|
|
4529
|
-
|
|
4530
|
-
|
|
4531
|
-
|
|
4532
|
-
|
|
4533
|
-
):
|
|
4534
|
-
|
|
4535
|
-
|
|
4536
|
-
|
|
4537
|
-
|
|
4538
|
-
|
|
4498
|
+
/**
|
|
4499
|
+
* Thread the response through each global `onResponse` hook. Stays SYNCHRONOUS until a hook
|
|
4500
|
+
* actually returns a Promise - an `async` version forced a promise + microtask on EVERY response
|
|
4501
|
+
* of any app with an onResponse hook (cors/securityHeaders/etag/timing all use onResponse), the
|
|
4502
|
+
* same ~13%/req tax the onRequest walk was de-async'd to avoid. The first async hook hands the
|
|
4503
|
+
* rest to {@link continueOnResponse}.
|
|
4504
|
+
*/
|
|
4505
|
+
private applyOnResponse(response: Response, req: Request): MaybePromise<Response> {
|
|
4506
|
+
const hooks = this.onResponseHooks
|
|
4507
|
+
let current = response
|
|
4508
|
+
for (let i = 0; i < hooks.length; i++) {
|
|
4509
|
+
const next = (hooks[i] as RawOnResponse)(current, req)
|
|
4510
|
+
if (next instanceof Promise) {
|
|
4511
|
+
return next.then((settled) => this.continueOnResponse(settled, i + 1, req))
|
|
4539
4512
|
}
|
|
4540
|
-
|
|
4541
|
-
} catch (err) {
|
|
4542
|
-
return this.handleLifecycleError(entry, err, ctx, finalize, wrapResponse)
|
|
4513
|
+
current = next
|
|
4543
4514
|
}
|
|
4515
|
+
return current
|
|
4544
4516
|
}
|
|
4545
4517
|
|
|
4546
|
-
private
|
|
4547
|
-
entry: RouteEntry,
|
|
4548
|
-
ctx: RawContext,
|
|
4549
|
-
finalize: (result: unknown, set: CtxSet) => T,
|
|
4550
|
-
wrapResponse: (response: Response) => T,
|
|
4551
|
-
): Promise<T> {
|
|
4518
|
+
private applyOnResponseAndFinalize(response: Response, req: Request): MaybePromise<Response> {
|
|
4552
4519
|
try {
|
|
4553
|
-
|
|
4554
|
-
|
|
4555
|
-
|
|
4556
|
-
|
|
4557
|
-
|
|
4558
|
-
|
|
4559
|
-
|
|
4560
|
-
|
|
4561
|
-
|
|
4562
|
-
return await this.runLifecycleHandlerAsync(entry, ctx, finalize, wrapResponse)
|
|
4563
|
-
} catch (err) {
|
|
4564
|
-
return this.handleLifecycleError(entry, err, ctx, finalize, wrapResponse)
|
|
4520
|
+
const transformed = this.applyOnResponse(response, req)
|
|
4521
|
+
return transformed instanceof Promise
|
|
4522
|
+
? transformed.then(
|
|
4523
|
+
(settled) => this.completeResponseFinalization({ response: settled }, req),
|
|
4524
|
+
(error) => this.failResponseFinalization(response, error, req),
|
|
4525
|
+
)
|
|
4526
|
+
: this.completeResponseFinalization({ response: transformed }, req)
|
|
4527
|
+
} catch (error) {
|
|
4528
|
+
return this.failResponseFinalization(response, error, req)
|
|
4565
4529
|
}
|
|
4566
4530
|
}
|
|
4567
4531
|
|
|
4568
|
-
private
|
|
4569
|
-
|
|
4570
|
-
|
|
4571
|
-
|
|
4572
|
-
|
|
4573
|
-
|
|
4574
|
-
first: Promise<unknown>,
|
|
4575
|
-
): Promise<T> {
|
|
4576
|
-
try {
|
|
4577
|
-
const firstOutcome = await first
|
|
4578
|
-
if (firstOutcome !== undefined) return finalize(firstOutcome, responseSet(ctx))
|
|
4579
|
-
for (let i = firstIndex + 1; i < entry.beforeHandle.length; i++) {
|
|
4580
|
-
const outcome = entry.beforeHandle[i]!(ctx)
|
|
4581
|
-
const early = outcome instanceof Promise ? await outcome : outcome
|
|
4582
|
-
if (early !== undefined) return finalize(early, responseSet(ctx))
|
|
4583
|
-
}
|
|
4584
|
-
return await this.runLifecycleHandlerAsync(entry, ctx, finalize, wrapResponse)
|
|
4585
|
-
} catch (err) {
|
|
4586
|
-
return this.handleLifecycleError(entry, err, ctx, finalize, wrapResponse)
|
|
4587
|
-
}
|
|
4532
|
+
private completeResponseFinalization(
|
|
4533
|
+
outcome: ResponseFinalization,
|
|
4534
|
+
req: Request,
|
|
4535
|
+
): MaybePromise<Response> {
|
|
4536
|
+
const notified = this.notifyResponseFinalized(outcome, req)
|
|
4537
|
+
return notified instanceof Promise ? notified.then(() => outcome.response) : outcome.response
|
|
4588
4538
|
}
|
|
4589
4539
|
|
|
4590
|
-
private
|
|
4591
|
-
|
|
4592
|
-
|
|
4593
|
-
|
|
4594
|
-
|
|
4595
|
-
|
|
4596
|
-
|
|
4597
|
-
|
|
4598
|
-
|
|
4599
|
-
|
|
4600
|
-
} catch (err) {
|
|
4601
|
-
return this.handleLifecycleError(entry, err, ctx, finalize, wrapResponse)
|
|
4540
|
+
private failResponseFinalization(
|
|
4541
|
+
response: Response,
|
|
4542
|
+
error: unknown,
|
|
4543
|
+
req: Request,
|
|
4544
|
+
): Promise<never> {
|
|
4545
|
+
const notified = this.notifyResponseFinalized({ response, error }, req)
|
|
4546
|
+
if (notified instanceof Promise) {
|
|
4547
|
+
return notified.then(() => {
|
|
4548
|
+
throw error
|
|
4549
|
+
})
|
|
4602
4550
|
}
|
|
4551
|
+
// A hook failure must surface as a REJECTION even when every prior step ran synchronously:
|
|
4552
|
+
// `fetch()` may now resolve without a promise, but its failure contract stays promise-shaped -
|
|
4553
|
+
// a synchronous throw here would escape `Promise.resolve(app.fetch(...))` bridges and
|
|
4554
|
+
// `.then()`-style callers entirely instead of reaching their rejection handling.
|
|
4555
|
+
return Promise.reject(error)
|
|
4603
4556
|
}
|
|
4604
4557
|
|
|
4605
|
-
|
|
4606
|
-
|
|
4607
|
-
|
|
4608
|
-
|
|
4609
|
-
|
|
4610
|
-
|
|
4611
|
-
|
|
4612
|
-
|
|
4613
|
-
|
|
4614
|
-
|
|
4615
|
-
|
|
4616
|
-
|
|
4617
|
-
|
|
4618
|
-
|
|
4619
|
-
|
|
4620
|
-
|
|
4621
|
-
|
|
4622
|
-
|
|
4623
|
-
|
|
4624
|
-
)
|
|
4625
|
-
}
|
|
4626
|
-
result = transformed
|
|
4558
|
+
/** Notify terminal observers in order while isolating both sync and async failures. */
|
|
4559
|
+
private notifyResponseFinalized(outcome: ResponseFinalization, req: Request): MaybePromise<void> {
|
|
4560
|
+
let pending: Promise<void> | undefined
|
|
4561
|
+
for (const hook of this.onResponseFinalizedHooks) {
|
|
4562
|
+
if (pending !== undefined) {
|
|
4563
|
+
pending = pending.then(async () => {
|
|
4564
|
+
try {
|
|
4565
|
+
await hook(outcome, req)
|
|
4566
|
+
} catch {
|
|
4567
|
+
// Terminal observation must never change request behavior.
|
|
4568
|
+
}
|
|
4569
|
+
})
|
|
4570
|
+
continue
|
|
4571
|
+
}
|
|
4572
|
+
try {
|
|
4573
|
+
const result = hook(outcome, req)
|
|
4574
|
+
if (result instanceof Promise) pending = result.catch(() => {})
|
|
4575
|
+
} catch {
|
|
4576
|
+
// Terminal observation must never change request behavior.
|
|
4627
4577
|
}
|
|
4628
|
-
return this.finishLifecycleContract(entry, ctx, finalize, wrapResponse, result)
|
|
4629
|
-
} catch (err) {
|
|
4630
|
-
return this.handleLifecycleError(entry, err, ctx, finalize, wrapResponse)
|
|
4631
4578
|
}
|
|
4579
|
+
return pending
|
|
4632
4580
|
}
|
|
4633
4581
|
|
|
4634
|
-
|
|
4635
|
-
|
|
4636
|
-
|
|
4637
|
-
|
|
4638
|
-
|
|
4639
|
-
|
|
4640
|
-
|
|
4582
|
+
/** Async tail of {@link applyOnResponse}: runs the remaining hooks once one has gone async. */
|
|
4583
|
+
private async continueOnResponse(
|
|
4584
|
+
response: Response,
|
|
4585
|
+
nextIndex: number,
|
|
4586
|
+
req: Request,
|
|
4587
|
+
): Promise<Response> {
|
|
4588
|
+
let current = response
|
|
4589
|
+
for (let i = nextIndex; i < this.onResponseHooks.length; i++) {
|
|
4590
|
+
const next = (this.onResponseHooks[i] as RawOnResponse)(current, req)
|
|
4591
|
+
current = next instanceof Promise ? await next : next
|
|
4592
|
+
}
|
|
4593
|
+
return current
|
|
4594
|
+
}
|
|
4595
|
+
|
|
4596
|
+
/**
|
|
4597
|
+
* Bound the response time. On timeout we abort `ctx.signal` (so cancellation-aware
|
|
4598
|
+
* handlers can bail) and return 503; the in-flight work keeps running but its
|
|
4599
|
+
* result is discarded - JS can't forcibly cancel a promise.
|
|
4600
|
+
*/
|
|
4601
|
+
private async withTimeout<T>(
|
|
4602
|
+
work: Promise<T>,
|
|
4603
|
+
controller: AbortController,
|
|
4604
|
+
onTimeout: () => T,
|
|
4605
|
+
timeoutMs: number,
|
|
4641
4606
|
): Promise<T> {
|
|
4607
|
+
let timer: ReturnType<typeof setTimeout> | undefined
|
|
4608
|
+
const timeout = new Promise<T>((resolve) => {
|
|
4609
|
+
timer = setTimeout(() => {
|
|
4610
|
+
controller.abort()
|
|
4611
|
+
resolve(onTimeout())
|
|
4612
|
+
}, timeoutMs)
|
|
4613
|
+
})
|
|
4642
4614
|
try {
|
|
4643
|
-
|
|
4644
|
-
|
|
4645
|
-
|
|
4646
|
-
result = transformed instanceof Promise ? await transformed : transformed
|
|
4647
|
-
}
|
|
4648
|
-
return await this.finishLifecycleContract(entry, ctx, finalize, wrapResponse, result)
|
|
4649
|
-
} catch (err) {
|
|
4650
|
-
return this.handleLifecycleError(entry, err, ctx, finalize, wrapResponse)
|
|
4615
|
+
return await Promise.race([work, timeout])
|
|
4616
|
+
} finally {
|
|
4617
|
+
if (timer !== undefined) clearTimeout(timer)
|
|
4651
4618
|
}
|
|
4652
4619
|
}
|
|
4653
|
-
|
|
4654
4620
|
private finishLifecycleContract<T>(
|
|
4655
4621
|
entry: RouteEntry,
|
|
4656
4622
|
ctx: RawContext,
|
|
4657
4623
|
finalize: (result: unknown, set: CtxSet) => T,
|
|
4658
|
-
wrapResponse: (response: Response) => T,
|
|
4624
|
+
wrapResponse: (response: Response | ResponseResult) => T,
|
|
4659
4625
|
result: unknown,
|
|
4660
4626
|
): MaybePromise<T> {
|
|
4661
4627
|
try {
|
|
@@ -4677,7 +4643,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
4677
4643
|
private finishContractOutcome<T>(
|
|
4678
4644
|
ctx: RawContext,
|
|
4679
4645
|
finalize: (result: unknown, set: CtxSet) => T,
|
|
4680
|
-
wrapResponse: (response: Response) => T,
|
|
4646
|
+
wrapResponse: (response: Response | ResponseResult) => T,
|
|
4681
4647
|
outcome: {
|
|
4682
4648
|
readonly kind: "ok" | "warn" | "violation"
|
|
4683
4649
|
readonly value?: unknown
|
|
@@ -4690,7 +4656,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
4690
4656
|
path: pathnameOf(ctx.req.url),
|
|
4691
4657
|
detail: outcome.message,
|
|
4692
4658
|
})
|
|
4693
|
-
return wrapResponse(
|
|
4659
|
+
return wrapResponse(plainError(500, "internal_error"))
|
|
4694
4660
|
}
|
|
4695
4661
|
if (outcome.kind === "warn") {
|
|
4696
4662
|
this.logger.warn("response contract", {
|
|
@@ -4702,19 +4668,44 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
4702
4668
|
return finalize(outcome.value, responseSet(ctx))
|
|
4703
4669
|
}
|
|
4704
4670
|
|
|
4705
|
-
|
|
4671
|
+
/** Synchronous by design, and `MaybePromise` rather than `Promise` for the same reason: control flow
|
|
4672
|
+
* out of a lifecycle stage must not cost a promise. An `async` method allocates one and defers the
|
|
4673
|
+
* result a microtask even when every branch it takes returns immediately, which is the common case
|
|
4674
|
+
* here - a thrown `status(...)`/`Response`, or a route with no `onError` hook at all. Only the hook
|
|
4675
|
+
* loop, which may await, drops into {@link runErrorHooks}. */
|
|
4676
|
+
private handleLifecycleError<T>(
|
|
4706
4677
|
entry: RouteEntry,
|
|
4707
4678
|
err: unknown,
|
|
4708
4679
|
ctx: RawContext,
|
|
4709
4680
|
finalize: (result: unknown, set: CtxSet) => T,
|
|
4710
|
-
wrapResponse: (response: Response) => T,
|
|
4711
|
-
):
|
|
4681
|
+
wrapResponse: (response: Response | ResponseResult) => T,
|
|
4682
|
+
): MaybePromise<T> {
|
|
4712
4683
|
// A *thrown* Response is deliberate control flow, not an error - a guard throws a redirect/401,
|
|
4713
4684
|
// an action throws an error page. Return it as-is (Remix/SvelteKit semantics); don't run onError
|
|
4714
4685
|
// or log it as a 500. This is what makes `throw redirect(...)` / `requireSession(...)` work from
|
|
4715
4686
|
// any handler or loader.
|
|
4716
4687
|
if (err instanceof Response) return wrapResponse(err)
|
|
4717
|
-
//
|
|
4688
|
+
// Same rule for a thrown `status(...)`, but through `finalize` rather than `wrapResponse`: the
|
|
4689
|
+
// value is still plain data, so the ordinary JSON lane renders it and no `Response` is built.
|
|
4690
|
+
if (isResponseResult(err)) return finalize(err, responseSet(ctx))
|
|
4691
|
+
if (entry.onError.length === 0) {
|
|
4692
|
+
// Never crash the server or leak internals. The client gets a flat 500; the detail goes to the
|
|
4693
|
+
// (redacting) logger. Body-read failures and around-hook failures land here too.
|
|
4694
|
+
this.logRequestError(err, ctx)
|
|
4695
|
+
return wrapResponse(plainError(500, "internal_error"))
|
|
4696
|
+
}
|
|
4697
|
+
return this.runErrorHooks(entry, err, ctx, finalize, wrapResponse)
|
|
4698
|
+
}
|
|
4699
|
+
|
|
4700
|
+
/** The `onError` chain, split off {@link handleLifecycleError} so its `await` costs only the routes
|
|
4701
|
+
* that registered a hook. A hook may return a custom response; otherwise the default 500 stands. */
|
|
4702
|
+
private async runErrorHooks<T>(
|
|
4703
|
+
entry: RouteEntry,
|
|
4704
|
+
err: unknown,
|
|
4705
|
+
ctx: RawContext,
|
|
4706
|
+
finalize: (result: unknown, set: CtxSet) => T,
|
|
4707
|
+
wrapResponse: (response: Response | ResponseResult) => T,
|
|
4708
|
+
): Promise<T> {
|
|
4718
4709
|
// Indexed for the same reason as the `beforeHandle` chain above: the iterator would outlive
|
|
4719
4710
|
// the `await` and stay heap-allocated on both engines.
|
|
4720
4711
|
for (let i = 0; i < entry.onError.length; i++) {
|
|
@@ -4722,55 +4713,41 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
4722
4713
|
const handled = outcome instanceof Promise ? await outcome : outcome
|
|
4723
4714
|
if (handled !== undefined) return finalize(handled, responseSet(ctx))
|
|
4724
4715
|
}
|
|
4725
|
-
// Never crash the server or leak internals. The client gets a flat 500; the detail goes to the
|
|
4726
|
-
// (redacting) logger. Body-read failures and around-hook failures land here too.
|
|
4727
4716
|
this.logRequestError(err, ctx)
|
|
4728
|
-
return wrapResponse(
|
|
4717
|
+
return wrapResponse(plainError(500, "internal_error"))
|
|
4729
4718
|
}
|
|
4730
4719
|
|
|
4731
|
-
/** Log an unhandled request error to the (redacting) logger - shared by
|
|
4720
|
+
/** Log an unhandled request error to the (redacting) logger - shared by the compiled route program and the
|
|
4732
4721
|
* bare fast path ({@link bareError}) so both record the same fields. Never throws; never leaks. */
|
|
4733
4722
|
private logRequestError(err: unknown, ctx: RawContext): void {
|
|
4734
|
-
|
|
4735
|
-
// the app's call (`errorLogDetail`). The default keeps it: a 500 with no message and no stack is
|
|
4736
|
-
// an incident nobody can diagnose, and the framework already ships the narrower instrument for
|
|
4737
|
-
// the leak - a redacting logger with `valuePatterns`. `"none"` is there for an untrusted sink.
|
|
4738
|
-
const detail = this.errorLogDetail
|
|
4739
|
-
this.logger.error("unhandled request error", {
|
|
4740
|
-
method: ctx.req.method,
|
|
4741
|
-
path: pathnameOf(ctx.req.url),
|
|
4742
|
-
name: err instanceof Error ? err.name : "Error",
|
|
4743
|
-
// `detail`, not `message`: the logger uses `message` for its own first argument, so a field of
|
|
4744
|
-
// that name is silently overwritten and the thrown error's own text never reaches the sink. It
|
|
4745
|
-
// survived only incidentally inside `stack`, and was lost outright for a non-Error throw.
|
|
4746
|
-
...(detail === "none" ? {} : { detail: err instanceof Error ? err.message : String(err) }),
|
|
4747
|
-
...(detail === "full" && err instanceof Error ? { stack: err.stack } : {}),
|
|
4748
|
-
})
|
|
4723
|
+
emitRequestErrorLog(this.logger, this.errorLogDetail, err, ctx)
|
|
4749
4724
|
}
|
|
4750
4725
|
|
|
4751
|
-
private
|
|
4726
|
+
private readAndValidateBody(
|
|
4752
4727
|
req: RequestSource,
|
|
4753
4728
|
entry: RouteEntry,
|
|
4754
4729
|
ctx: RawContext,
|
|
4755
|
-
|
|
4756
|
-
|
|
4757
|
-
|
|
4758
|
-
|
|
4759
|
-
|
|
4760
|
-
|
|
4761
|
-
|
|
4762
|
-
|
|
4763
|
-
const
|
|
4764
|
-
|
|
4765
|
-
|
|
4766
|
-
|
|
4767
|
-
// multipart/form-data (file uploads) stays 415 on the schema path by design - files don't
|
|
4768
|
-
// fit a value schema; use a schema-less route + @nifrajs/uploads helpers for those.
|
|
4769
|
-
return jsonError(415, "unsupported_media_type")
|
|
4730
|
+
bodySchema: StandardSchemaV1 = entry.schema?.body as StandardSchemaV1,
|
|
4731
|
+
bodyLimit: number | undefined = entry.bodyLimit,
|
|
4732
|
+
): Promise<Response | ResponseResult | undefined> {
|
|
4733
|
+
// Keep the generic lifecycle body stage on the same framed parser/continuation as the body-only
|
|
4734
|
+
// lane. This avoids an async wrapper that first awaits parsing and then starts validation in a
|
|
4735
|
+
// second continuation, while retaining the exact bounded-read, content-type, and poisoning
|
|
4736
|
+
// contracts shared by every body route.
|
|
4737
|
+
const validate = (parsed: unknown): MaybePromise<Response | ResponseResult | undefined> => {
|
|
4738
|
+
const validation = bodySchema["~standard"].validate(parsed)
|
|
4739
|
+
return validation instanceof Promise
|
|
4740
|
+
? validation.then((result) => this.applyLifecycleValidation(entry, result, ctx, "body"))
|
|
4741
|
+
: this.applyLifecycleValidation(entry, validation, ctx, "body")
|
|
4770
4742
|
}
|
|
4771
|
-
|
|
4772
|
-
|
|
4773
|
-
|
|
4743
|
+
return readBodyFramed(
|
|
4744
|
+
req,
|
|
4745
|
+
bodyLimit ?? UNLIMITED_BODY_BYTES,
|
|
4746
|
+
this.protoPoisoning,
|
|
4747
|
+
validate,
|
|
4748
|
+
(response) => response,
|
|
4749
|
+
(error) => Promise.reject(error),
|
|
4750
|
+
)
|
|
4774
4751
|
}
|
|
4775
4752
|
|
|
4776
4753
|
/** Apply validation and its recovery hook on the generic lifecycle lane. Recovery is completed
|
|
@@ -4780,7 +4757,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
4780
4757
|
result: StandardResult<unknown>,
|
|
4781
4758
|
ctx: RawContext,
|
|
4782
4759
|
kind: "body" | "query" | "params" | "headers",
|
|
4783
|
-
): MaybePromise<Response | undefined> {
|
|
4760
|
+
): MaybePromise<Response | ResponseResult | undefined> {
|
|
4784
4761
|
const assign = (value: unknown): void => {
|
|
4785
4762
|
if (kind === "body") ctx.body = value
|
|
4786
4763
|
else if (kind === "query") ctx.query = value
|
|
@@ -4792,7 +4769,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
4792
4769
|
return undefined
|
|
4793
4770
|
}
|
|
4794
4771
|
const hook = entry.schema?.onValidationError ?? this.defaultOnValidationError
|
|
4795
|
-
if (hook === undefined) return
|
|
4772
|
+
if (hook === undefined) return plainValidationError(result.issues)
|
|
4796
4773
|
const attempted = hook(result.issues, ctx as unknown as Context, kind)
|
|
4797
4774
|
if (attempted instanceof Promise) {
|
|
4798
4775
|
return attempted.then((recovery) =>
|
|
@@ -4808,8 +4785,8 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
4808
4785
|
issues: ReadonlyArray<StandardIssue>,
|
|
4809
4786
|
recovery: unknown,
|
|
4810
4787
|
assign: (value: unknown) => void,
|
|
4811
|
-
): MaybePromise<Response | undefined> {
|
|
4812
|
-
if (recovery === undefined) return
|
|
4788
|
+
): MaybePromise<Response | ResponseResult | undefined> {
|
|
4789
|
+
if (recovery === undefined) return plainValidationError(issues)
|
|
4813
4790
|
if (recovery instanceof Response) return recovery
|
|
4814
4791
|
const schema =
|
|
4815
4792
|
kind === "body"
|
|
@@ -4822,43 +4799,16 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
4822
4799
|
const retried = schema!["~standard"].validate(recovery)
|
|
4823
4800
|
if (retried instanceof Promise) {
|
|
4824
4801
|
return retried.then((settled) => {
|
|
4825
|
-
if (settled.issues !== undefined) return
|
|
4802
|
+
if (settled.issues !== undefined) return plainValidationError(settled.issues)
|
|
4826
4803
|
assign(settled.value)
|
|
4827
4804
|
return undefined
|
|
4828
4805
|
})
|
|
4829
4806
|
}
|
|
4830
|
-
if (retried.issues !== undefined) return
|
|
4807
|
+
if (retried.issues !== undefined) return plainValidationError(retried.issues)
|
|
4831
4808
|
assign(retried.value)
|
|
4832
4809
|
return undefined
|
|
4833
4810
|
}
|
|
4834
4811
|
|
|
4835
|
-
/**
|
|
4836
|
-
* Read + parse the JSON body, capped at `maxBodyBytes` and screened by the server's
|
|
4837
|
-
* `protoPoisoning` policy. Rejects a `Content-Length` over the cap *before* buffering,
|
|
4838
|
-
* re-checks what was actually delivered after the read, and aborts a chunked or
|
|
4839
|
-
* length-less body mid-stream once the running byte count exceeds the cap - so a lying
|
|
4840
|
-
* or absent length can't force us to buffer an oversized payload.
|
|
4841
|
-
*/
|
|
4842
|
-
private readBoundedJson(req: RequestSource, maxBodyBytes?: number): Promise<unknown | Response>
|
|
4843
|
-
private readBoundedJson<T>(
|
|
4844
|
-
req: RequestSource,
|
|
4845
|
-
maxBodyBytes: number,
|
|
4846
|
-
onResult: (parsed: unknown | Response) => T | Promise<T>,
|
|
4847
|
-
onError: (error: unknown) => T | Promise<T>,
|
|
4848
|
-
): Promise<T>
|
|
4849
|
-
private readBoundedJson<T>(
|
|
4850
|
-
req: RequestSource,
|
|
4851
|
-
maxBodyBytes = this.maxBodyBytes,
|
|
4852
|
-
onResult?: (parsed: unknown | Response) => T | Promise<T>,
|
|
4853
|
-
onError?: (error: unknown) => T | Promise<T>,
|
|
4854
|
-
): Promise<unknown | Response | T> {
|
|
4855
|
-
// Not `async`: this is a pass-through, and wrapping the lane's own promise in a coroutine
|
|
4856
|
-
// frame costs an extra microtask hop on every JSON body.
|
|
4857
|
-
return onResult === undefined
|
|
4858
|
-
? readBoundedJsonSource(req, maxBodyBytes, this.protoPoisoning)
|
|
4859
|
-
: readBoundedJsonSource(req, maxBodyBytes, this.protoPoisoning, onResult, onError!)
|
|
4860
|
-
}
|
|
4861
|
-
|
|
4862
4812
|
/** Adapt one route's compiled execution plan to Bun's already-matched request shape. Route
|
|
4863
4813
|
* semantics remain in the plan; this closure only supplies native params and deadline fallback. */
|
|
4864
4814
|
private compileBunNativeHandler(
|
|
@@ -5046,10 +4996,13 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
5046
4996
|
// matched route's handler, set by `server.upgrade`.
|
|
5047
4997
|
// With WS routes, the dispatcher comes from the installed `.use(websocket())` runtime - non-null
|
|
5048
4998
|
// because wsRouteCount > 0 means ws() ran, and ws() requires the runtime at registration.
|
|
4999
|
+
// Native pub/sub when the app has WS routes and none validate outbound frames: `ws.subscribe` and
|
|
5000
|
+
// `app.publish` go through Bun's own (uWebSockets) broadcast instead of the JS registry loop.
|
|
5001
|
+
const nativePubsub = this.wsRouteCount > 0 && !this.wsHasValidatedSend
|
|
5049
5002
|
const wsHandlers =
|
|
5050
5003
|
this.wsRouteCount === 0
|
|
5051
5004
|
? undefined
|
|
5052
|
-
: (this.wsRuntime as WsRuntime).bunHandlers(this.topics as TopicRegistry)
|
|
5005
|
+
: (this.wsRuntime as WsRuntime).bunHandlers(this.topics as TopicRegistry, nativePubsub)
|
|
5053
5006
|
const reusePort = options?.reusePort === true
|
|
5054
5007
|
// Spread rather than pass `hostname: undefined` - Bun treats an explicit undefined as a value
|
|
5055
5008
|
// on some option paths, and omitting is what selects its 0.0.0.0 default.
|
|
@@ -5087,6 +5040,14 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
5087
5040
|
},
|
|
5088
5041
|
})) as unknown as RunningServer
|
|
5089
5042
|
this.bunServer = running
|
|
5043
|
+
// Bind `app.publish` to Bun's native broadcast now that the server handle exists. Guarded on the
|
|
5044
|
+
// method's presence so a runtime whose handle lacks it simply keeps the registry path.
|
|
5045
|
+
if (nativePubsub && typeof running.publish === "function") {
|
|
5046
|
+
const native = running.publish.bind(running)
|
|
5047
|
+
this.nativePublish = (topic, data) => {
|
|
5048
|
+
native(topic, data)
|
|
5049
|
+
}
|
|
5050
|
+
}
|
|
5090
5051
|
this.sealed = true
|
|
5091
5052
|
if (this.gracefulSignals) this.installSignalHandlers()
|
|
5092
5053
|
return running
|