@nifrajs/core 3.1.0 → 3.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +21 -0
- 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 +2 -2
- package/dist/edge-kit.d.ts.map +1 -1
- package/dist/edge-kit.js +2 -2
- package/dist/edge-kit.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 +6 -4
- package/dist/internal/route-execution.d.ts.map +1 -1
- package/dist/internal/route-execution.js +2 -17
- 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/server/bare-error-lane.d.ts +1 -1
- package/dist/server/bare-error-lane.d.ts.map +1 -1
- package/dist/server/bare-error-lane.js +3 -3
- package/dist/server/bare-error-lane.js.map +1 -1
- 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/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/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/registry.d.ts +40 -12
- package/dist/server/registry.d.ts.map +1 -1
- package/dist/server/request-context.d.ts +4 -28
- package/dist/server/request-context.d.ts.map +1 -1
- package/dist/server/request-context.js +10 -185
- package/dist/server/request-context.js.map +1 -1
- package/dist/server/respond.d.ts.map +1 -1
- package/dist/server/respond.js +39 -2
- package/dist/server/respond.js.map +1 -1
- package/dist/server/runtime-core.d.ts +18 -2
- package/dist/server/runtime-core.d.ts.map +1 -1
- package/dist/server/runtime-core.js +0 -23
- package/dist/server/runtime-core.js.map +1 -1
- package/dist/server/server-types.d.ts +3 -0
- package/dist/server/server-types.d.ts.map +1 -1
- package/dist/server/server.d.ts +113 -84
- package/dist/server/server.d.ts.map +1 -1
- package/dist/server/server.js +506 -639
- 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/ws-bun.d.ts +11 -3
- package/dist/server/ws-bun.d.ts.map +1 -1
- package/dist/server/ws-bun.js +42 -12
- 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 +2 -2
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +1 -1
- package/dist/server.js.map +1 -1
- package/dist/single-copy.d.ts.map +1 -1
- package/dist/single-copy.js +3 -1
- package/dist/single-copy.js.map +1 -1
- package/package.json +6 -3
- 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 +2 -2
- 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 +34 -84
- package/src/internal/route-lanes.ts +221 -0
- package/src/internal/route-program.ts +404 -0
- package/src/server/bare-error-lane.ts +3 -2
- package/src/server/body-lane.ts +203 -0
- package/src/server/contract.ts +70 -20
- package/src/server/cookies.ts +16 -2
- package/src/server/plugin.ts +11 -5
- package/src/server/registry.ts +71 -16
- package/src/server/request-context.ts +7 -262
- package/src/server/respond.ts +45 -2
- package/src/server/runtime-core.ts +28 -2
- package/src/server/server-types.ts +3 -0
- package/src/server/server.ts +1152 -1260
- package/src/server/validation.ts +19 -0
- package/src/server/ws-bun.ts +44 -11
- package/src/server/ws-hook.ts +3 -2
- package/src/server.ts +7 -2
- package/src/single-copy.ts +3 -1
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,9 +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
42
|
import type { ResponseObserverPlugin } from "../response-observer.ts"
|
|
45
|
-
import {
|
|
43
|
+
import { decodeRouteParams } from "../router/pattern.ts"
|
|
46
44
|
import { EMPTY_PARAMS, type Method, Router } from "../router/router.ts"
|
|
47
45
|
import type {
|
|
48
46
|
InferOutput,
|
|
@@ -79,14 +77,8 @@ import type {
|
|
|
79
77
|
ResponseHeadersHook,
|
|
80
78
|
ResponseHeadersView,
|
|
81
79
|
} from "./node-outcome-hook.ts"
|
|
82
|
-
import {
|
|
83
|
-
|
|
84
|
-
type QueryValue,
|
|
85
|
-
queryObjectOf,
|
|
86
|
-
readBoundedForm,
|
|
87
|
-
searchOf,
|
|
88
|
-
} from "./query.ts"
|
|
89
|
-
import { RequestContext, readBodyFramed, readBoundedJsonSource } from "./request-context.ts"
|
|
80
|
+
import { type QueryValue, queryObjectOf, searchOf } from "./query.ts"
|
|
81
|
+
import { RequestContext, readBodyFramed } from "./request-context.ts"
|
|
90
82
|
import {
|
|
91
83
|
applyStaticResponseHeaders,
|
|
92
84
|
buildStaticResponseHeaders,
|
|
@@ -112,9 +104,10 @@ import {
|
|
|
112
104
|
isResponseResult,
|
|
113
105
|
type ResponseResult,
|
|
114
106
|
requestOf,
|
|
115
|
-
|
|
107
|
+
type StatusResponse,
|
|
116
108
|
} from "./runtime-core.ts"
|
|
117
109
|
import { normalizeStaticResponseHeaders, type StaticResponseHeaders } from "./static-headers.ts"
|
|
110
|
+
import { plainValidationError } from "./validation.ts"
|
|
118
111
|
|
|
119
112
|
// NodeServeOutcome (the nifra<->node bridge render form) now lives in `./node-outcome.ts`; re-exported
|
|
120
113
|
// so existing importers keep resolving it from the server module.
|
|
@@ -143,7 +136,7 @@ import {
|
|
|
143
136
|
NODE_NATIVE_MOUNT,
|
|
144
137
|
RESOLVE_NODE_MOUNT,
|
|
145
138
|
} from "./install.ts"
|
|
146
|
-
import type { EffectLedgerRuntime
|
|
139
|
+
import type { EffectLedgerRuntime } from "./ledger-lane.ts"
|
|
147
140
|
import { jsonLogger, type Logger } from "./logger.ts"
|
|
148
141
|
import type { McpRuntime } from "./mcp-hook.ts"
|
|
149
142
|
import type {
|
|
@@ -373,6 +366,31 @@ type ResponseOf<S extends RouteSchema> = S extends { response: infer R extends S
|
|
|
373
366
|
? InferOutput<R> | Response | ResponseResult
|
|
374
367
|
: HandlerResult
|
|
375
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
|
+
|
|
376
394
|
/**
|
|
377
395
|
* Public handler shape: context typed from the path, the (optional) schema, and
|
|
378
396
|
* any accumulated middleware context `Ctx` (from `derive`/`decorate`).
|
|
@@ -406,7 +424,7 @@ export type {
|
|
|
406
424
|
// A plugin operates over arbitrary Server shapes; `any` here is the standard framework escape hatch
|
|
407
425
|
// (the precise threading happens at the `use` call site, which is generic over the *concrete* `this`).
|
|
408
426
|
// biome-ignore lint/suspicious/noExplicitAny: plugins are generic over any Server's Registry/Context
|
|
409
|
-
export type AnyServer = Server<any, any>
|
|
427
|
+
export type AnyServer = Server<any, any, any>
|
|
410
428
|
|
|
411
429
|
// Plugin definers + their types now live in `./plugin.ts`; re-exported here so `.use()` callers and
|
|
412
430
|
// existing importers keep resolving them from the server module.
|
|
@@ -432,28 +450,13 @@ const STOP_HOOK_TIMEOUT_MS = 5_000
|
|
|
432
450
|
* for the same request, so a browser that could open a socket was told its POST was cross-origin. */
|
|
433
451
|
const wsSameOrigin = isSameOriginRequest
|
|
434
452
|
|
|
435
|
-
function validationIssues(issues: ReadonlyArray<StandardIssue>): {
|
|
436
|
-
ok: false
|
|
437
|
-
error: string
|
|
438
|
-
issues: unknown[]
|
|
439
|
-
} {
|
|
440
|
-
const serialized = issues.map((issue) => {
|
|
441
|
-
const path = issue.path?.map((seg) => String(typeof seg === "object" ? seg.key : seg))
|
|
442
|
-
return path !== undefined ? { message: issue.message, path } : { message: issue.message }
|
|
443
|
-
})
|
|
444
|
-
return { ok: false, error: "validation", issues: serialized }
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
/** The 422 as plain data - the shape every lane's response wrapper takes, so a rejected body costs
|
|
448
|
-
* what an accepted one costs: on Node it is written straight to the socket with a `content-length`
|
|
449
|
-
* instead of being built as a `Response` and drained back out. */
|
|
450
|
-
export function plainValidationError(issues: ReadonlyArray<StandardIssue>): ResponseResult {
|
|
451
|
-
return status(422, validationIssues(issues))
|
|
452
|
-
}
|
|
453
|
-
|
|
454
453
|
// `jsonError`, `urlPartsOf`, `pathnameOf` moved to `./http.ts` (a dependency-free leaf shared with the
|
|
455
454
|
// opt-in request lanes); re-exported so existing importers keep resolving from here.
|
|
456
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"
|
|
457
460
|
// Query-string + urlencoded-form parsing now lives in `./query.ts`; re-exported so existing
|
|
458
461
|
// importers keep resolving `searchOf`/`queryObjectOf`/`QueryValue` from here.
|
|
459
462
|
export { type QueryValue, queryObjectOf, searchOf }
|
|
@@ -503,21 +506,6 @@ function responseSet(ctx: RawContext): CtxSet {
|
|
|
503
506
|
return ctx[CONTEXT_SET]() ?? EMPTY_RESPONSE_CONTROLS
|
|
504
507
|
}
|
|
505
508
|
|
|
506
|
-
/**
|
|
507
|
-
* A `derive` that answered with a response rather than a context extension - `status(...)`, or a bare
|
|
508
|
-
* `Response`. The request ends there, on the same lane a `beforeHandle`'s early return takes.
|
|
509
|
-
*
|
|
510
|
-
* `derive` is the one stage that could not exit early: `beforeHandle` says "done" by returning a
|
|
511
|
-
* value, but a derive's return value IS the extension, so the only way out was `throw`ing a
|
|
512
|
-
* `Response` - which measured as the single most expensive way to answer (the `Response` object
|
|
513
|
-
* costs ~40% of the rejection lane; see `PlainRender`). Neither shape had a meaning here before:
|
|
514
|
-
* `Object.assign(ctx, response)` copies nothing (a `Response`'s properties live on its prototype),
|
|
515
|
-
* so a derive that returned one silently fell through to the handler it meant to prevent.
|
|
516
|
-
*/
|
|
517
|
-
function isDeriveEarlyExit(extension: unknown): boolean {
|
|
518
|
-
return isResponseResult(extension) || extension instanceof Response
|
|
519
|
-
}
|
|
520
|
-
|
|
521
509
|
function isContextlessNoArgArrow(handler: (context: never) => unknown): boolean {
|
|
522
510
|
if (handler.length !== 0) return false
|
|
523
511
|
try {
|
|
@@ -657,7 +645,7 @@ const sealHookAudit = (
|
|
|
657
645
|
* app.decorate("db", db).derive((c) => ({ user: auth(c) }))
|
|
658
646
|
* .get("/me", (c) => c.user) // c.user + c.db are typed
|
|
659
647
|
*/
|
|
660
|
-
export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
648
|
+
export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, HookOutput = never> {
|
|
661
649
|
/**
|
|
662
650
|
* Which copy of `@nifrajs/core` declared this type. Two copies in one build (a linked sibling repo,
|
|
663
651
|
* a hoisting split) produce two unrelated `Server` types whose assignment error is a wall of
|
|
@@ -676,6 +664,15 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
676
664
|
* Created by the first `app.ws()` via the `@nifrajs/core/ws` runtime - `undefined` until then, so a
|
|
677
665
|
* no-WebSocket app never constructs (or bundles) it. */
|
|
678
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
|
|
679
676
|
private readonly maxBodyBytes: number
|
|
680
677
|
private readonly protoPoisoning: "reject" | "strip" | "ignore"
|
|
681
678
|
/** `trustBodyFraming`: mark every `app.fetch` request as runtime-framed (see ServerOptions). */
|
|
@@ -901,14 +898,22 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
901
898
|
* takes, where `throw new Response(...)` pays ~40% of the rejection lane to build the object. That
|
|
902
899
|
* branch is control flow, so it does not widen the derived context type.
|
|
903
900
|
*/
|
|
904
|
-
derive<
|
|
905
|
-
fn:
|
|
906
|
-
): Server<
|
|
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
|
+
> {
|
|
907
908
|
this.assertConfigurable("derive()")
|
|
908
909
|
this.derives.push(fn as unknown as RawDerive)
|
|
909
910
|
if (hookAuditRuntime && process.env.NODE_ENV !== "production")
|
|
910
911
|
recordScopedHook(this, this.catalog.size, "derive")
|
|
911
|
-
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
|
+
>
|
|
912
917
|
}
|
|
913
918
|
|
|
914
919
|
/**
|
|
@@ -916,12 +921,12 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
916
921
|
* by none before; one added after the last route reaches nothing
|
|
917
922
|
* (see {@link ServerOptions.unusedScopedHooks}).
|
|
918
923
|
*/
|
|
919
|
-
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> {
|
|
920
925
|
this.assertConfigurable("decorate()")
|
|
921
926
|
this.decorations[key] = value
|
|
922
927
|
if (hookAuditRuntime && process.env.NODE_ENV !== "production")
|
|
923
928
|
recordScopedHook(this, this.catalog.size, "decorate")
|
|
924
|
-
return this as unknown as Server<R, Ctx & Record<K, V
|
|
929
|
+
return this as unknown as Server<R, Ctx & Record<K, V>, HookOutput>
|
|
925
930
|
}
|
|
926
931
|
|
|
927
932
|
/**
|
|
@@ -943,12 +948,14 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
943
948
|
* it is snapshotted into every route declared *after* this call and covers none declared before, so
|
|
944
949
|
* one added after the last route reaches nothing (see {@link ServerOptions.unusedScopedHooks}).
|
|
945
950
|
*/
|
|
946
|
-
beforeHandle
|
|
951
|
+
beforeHandle<H extends (context: Context & Ctx) => MaybePromise<unknown>>(
|
|
952
|
+
fn: H,
|
|
953
|
+
): Server<R, Ctx, HookOutput | HookOutputOf<ReturnType<H>>> {
|
|
947
954
|
this.assertConfigurable("beforeHandle()")
|
|
948
955
|
this.beforeHandleHooks.push(fn as unknown as RawBeforeHandle)
|
|
949
956
|
if (hookAuditRuntime && process.env.NODE_ENV !== "production")
|
|
950
957
|
recordScopedHook(this, this.catalog.size, "beforeHandle")
|
|
951
|
-
return this
|
|
958
|
+
return this as unknown as Server<R, Ctx, HookOutput | HookOutputOf<ReturnType<H>>>
|
|
952
959
|
}
|
|
953
960
|
|
|
954
961
|
/**
|
|
@@ -959,12 +966,14 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
959
966
|
* Order-scoped: captured by every route declared *after* this call, none before; one added after the
|
|
960
967
|
* last route reaches nothing (see {@link ServerOptions.unusedScopedHooks}).
|
|
961
968
|
*/
|
|
962
|
-
around
|
|
969
|
+
around<
|
|
970
|
+
H extends (context: Context & Ctx, next: () => MaybePromise<unknown>) => MaybePromise<unknown>,
|
|
971
|
+
>(fn: H): Server<R, Ctx, HookOutput | HookOutputOf<ReturnType<H>>> {
|
|
963
972
|
this.assertConfigurable("around()")
|
|
964
973
|
this.aroundHooks.push(fn as unknown as RawAround)
|
|
965
974
|
if (hookAuditRuntime && process.env.NODE_ENV !== "production")
|
|
966
975
|
recordScopedHook(this, this.catalog.size, "around")
|
|
967
|
-
return this
|
|
976
|
+
return this as unknown as Server<R, Ctx, HookOutput | HookOutputOf<ReturnType<H>>>
|
|
968
977
|
}
|
|
969
978
|
|
|
970
979
|
/**
|
|
@@ -1006,23 +1015,27 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
1006
1015
|
/** Transform the handler's result before it is serialized. Order-scoped: captured by routes declared
|
|
1007
1016
|
* *after* this call, none before; one added after the last route reaches nothing (see
|
|
1008
1017
|
* {@link ServerOptions.unusedScopedHooks}). */
|
|
1009
|
-
afterHandle
|
|
1018
|
+
afterHandle<H extends (result: unknown, context: Context & Ctx) => MaybePromise<unknown>>(
|
|
1019
|
+
fn: H,
|
|
1020
|
+
): Server<R, Ctx, HookOutput | HookOutputOf<ReturnType<H>>> {
|
|
1010
1021
|
this.assertConfigurable("afterHandle()")
|
|
1011
1022
|
this.afterHandleHooks.push(fn as unknown as RawAfterHandle)
|
|
1012
1023
|
if (hookAuditRuntime && process.env.NODE_ENV !== "production")
|
|
1013
1024
|
recordScopedHook(this, this.catalog.size, "afterHandle")
|
|
1014
|
-
return this
|
|
1025
|
+
return this as unknown as Server<R, Ctx, HookOutput | HookOutputOf<ReturnType<H>>>
|
|
1015
1026
|
}
|
|
1016
1027
|
|
|
1017
1028
|
/** Handle a thrown error; a non-`undefined` return becomes the response (else the default 500).
|
|
1018
1029
|
* Order-scoped: captured by routes declared *after* this call, none before; one added after the last
|
|
1019
1030
|
* route reaches nothing (see {@link ServerOptions.unusedScopedHooks}). */
|
|
1020
|
-
onError
|
|
1031
|
+
onError<H extends (error: unknown, context: Context & Ctx) => MaybePromise<unknown>>(
|
|
1032
|
+
fn: H,
|
|
1033
|
+
): Server<R, Ctx, HookOutput | HookOutputOf<ReturnType<H>>> {
|
|
1021
1034
|
this.assertConfigurable("onError()")
|
|
1022
1035
|
this.onErrorHooks.push(fn as unknown as RawErrorHandler)
|
|
1023
1036
|
if (hookAuditRuntime && process.env.NODE_ENV !== "production")
|
|
1024
1037
|
recordScopedHook(this, this.catalog.size, "onError")
|
|
1025
|
-
return this
|
|
1038
|
+
return this as unknown as Server<R, Ctx, HookOutput | HookOutputOf<ReturnType<H>>>
|
|
1026
1039
|
}
|
|
1027
1040
|
|
|
1028
1041
|
/** Transform every outgoing response - success, error, 404, 405, short-circuit. Global. */
|
|
@@ -1133,14 +1146,6 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
1133
1146
|
|
|
1134
1147
|
/** Enable the opt-in portable response observer methods. */
|
|
1135
1148
|
use(plugin: ResponseObserverPlugin): this & ResponseObserverMethods
|
|
1136
|
-
/**
|
|
1137
|
-
* Apply a type-**identity** plugin ({@link IdentityPlugin}, from {@link defineIdentityPlugin}) - it
|
|
1138
|
-
* registers routes/hooks but doesn't change the types, so this returns `this` with the route registry
|
|
1139
|
-
* and context fully intact. This overload exists specifically so a named identity plugin (e.g.
|
|
1140
|
-
* `@nifrajs/better-auth`) threads the registry: its `& { pluginName }` intersection would otherwise
|
|
1141
|
-
* defeat the generic inference of the transforming overload below and collapse the result to `any`.
|
|
1142
|
-
*/
|
|
1143
|
-
use(plugin: IdentityPlugin): this
|
|
1144
1149
|
/**
|
|
1145
1150
|
* Apply a **context** plugin ({@link ContextPlugin}, from {@link defineContextPlugin}) - it adds `D`
|
|
1146
1151
|
* to the handler context and changes nothing else, so the route registry `R` and the existing context
|
|
@@ -1148,7 +1153,17 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
1148
1153
|
* `(app: this) => Out` overload below cannot infer through a *generic* plugin signature: it erases the
|
|
1149
1154
|
* plugin's own type parameters to their constraints, which silently widens `R` to `Registry`.
|
|
1150
1155
|
*/
|
|
1151
|
-
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>
|
|
1152
1167
|
/**
|
|
1153
1168
|
* Apply a **plugin function** - `(app) => app`, typically built with {@link definePlugin}. It's
|
|
1154
1169
|
* called with `this` and its result is returned, so an inline plugin's `derive`/`decorate` thread
|
|
@@ -1172,6 +1187,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
1172
1187
|
* the last route reaches nothing - {@link ServerOptions.unusedScopedHooks} logs that at seal. A named
|
|
1173
1188
|
* bundle already applied is skipped (idempotent).
|
|
1174
1189
|
*/
|
|
1190
|
+
use<M extends Middleware>(mw: M): Server<R, Ctx, HookOutput | MiddlewareOutputOf<M>>
|
|
1175
1191
|
use(mw: Middleware): this
|
|
1176
1192
|
use(arg: Middleware | ((app: this) => AnyServer)): AnyServer {
|
|
1177
1193
|
this.assertConfigurable("use()")
|
|
@@ -1256,16 +1272,24 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
1256
1272
|
path: Path,
|
|
1257
1273
|
schema: S,
|
|
1258
1274
|
handler: H,
|
|
1259
|
-
): Server<
|
|
1275
|
+
): Server<
|
|
1276
|
+
AddRoute<R, "GET", Path, RouteInfoFor<Path, S, OutputOf<H>, HookOutput>>,
|
|
1277
|
+
Ctx,
|
|
1278
|
+
HookOutput
|
|
1279
|
+
>
|
|
1260
1280
|
get<Path extends string, H extends Handler<Path, RouteSchema, Ctx>>(
|
|
1261
1281
|
path: Path,
|
|
1262
1282
|
handler: H,
|
|
1263
|
-
): Server<
|
|
1283
|
+
): Server<
|
|
1284
|
+
AddRoute<R, "GET", Path, RouteInfoFor<Path, Record<never, never>, OutputOf<H>, HookOutput>>,
|
|
1285
|
+
Ctx,
|
|
1286
|
+
HookOutput
|
|
1287
|
+
>
|
|
1264
1288
|
get(
|
|
1265
1289
|
path: string,
|
|
1266
1290
|
schemaOrHandler: RouteSchema | ErasedHandler,
|
|
1267
1291
|
handler?: ErasedHandler,
|
|
1268
|
-
): Server<Registry, Ctx> {
|
|
1292
|
+
): Server<Registry, Ctx, HookOutput> {
|
|
1269
1293
|
return this.route("GET", path, schemaOrHandler, handler)
|
|
1270
1294
|
}
|
|
1271
1295
|
|
|
@@ -1298,12 +1322,17 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
1298
1322
|
stream: TypedSSEStream<InferOutput<S["sse"]>>,
|
|
1299
1323
|
) => void | Promise<void>,
|
|
1300
1324
|
init?: SSEInit,
|
|
1301
|
-
): Server<
|
|
1325
|
+
): Server<
|
|
1326
|
+
AddRoute<R, "GET", Path, RouteInfoFor<Path, S, Response, HookOutput>>,
|
|
1327
|
+
Ctx,
|
|
1328
|
+
HookOutput
|
|
1329
|
+
> {
|
|
1302
1330
|
const handler = (context: Context<Path, S> & Ctx): Response =>
|
|
1303
1331
|
requireSseRuntime(this.sseRuntime).response(context, (stream) => run(context, stream), init)
|
|
1304
1332
|
return this.route("GET", path, schema, handler as unknown as ErasedHandler) as Server<
|
|
1305
|
-
AddRoute<R, "GET", Path, RouteInfoFor<Path, S, Response>>,
|
|
1306
|
-
Ctx
|
|
1333
|
+
AddRoute<R, "GET", Path, RouteInfoFor<Path, S, Response, HookOutput>>,
|
|
1334
|
+
Ctx,
|
|
1335
|
+
HookOutput
|
|
1307
1336
|
>
|
|
1308
1337
|
}
|
|
1309
1338
|
|
|
@@ -1311,16 +1340,24 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
1311
1340
|
path: Path,
|
|
1312
1341
|
schema: S,
|
|
1313
1342
|
handler: H,
|
|
1314
|
-
): Server<
|
|
1343
|
+
): Server<
|
|
1344
|
+
AddRoute<R, "POST", Path, RouteInfoFor<Path, S, OutputOf<H>, HookOutput>>,
|
|
1345
|
+
Ctx,
|
|
1346
|
+
HookOutput
|
|
1347
|
+
>
|
|
1315
1348
|
post<Path extends string, H extends Handler<Path, RouteSchema, Ctx>>(
|
|
1316
1349
|
path: Path,
|
|
1317
1350
|
handler: H,
|
|
1318
|
-
): Server<
|
|
1351
|
+
): Server<
|
|
1352
|
+
AddRoute<R, "POST", Path, RouteInfoFor<Path, Record<never, never>, OutputOf<H>, HookOutput>>,
|
|
1353
|
+
Ctx,
|
|
1354
|
+
HookOutput
|
|
1355
|
+
>
|
|
1319
1356
|
post(
|
|
1320
1357
|
path: string,
|
|
1321
1358
|
schemaOrHandler: RouteSchema | ErasedHandler,
|
|
1322
1359
|
handler?: ErasedHandler,
|
|
1323
|
-
): Server<Registry, Ctx> {
|
|
1360
|
+
): Server<Registry, Ctx, HookOutput> {
|
|
1324
1361
|
return this.route("POST", path, schemaOrHandler, handler)
|
|
1325
1362
|
}
|
|
1326
1363
|
|
|
@@ -1328,16 +1365,24 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
1328
1365
|
path: Path,
|
|
1329
1366
|
schema: S,
|
|
1330
1367
|
handler: H,
|
|
1331
|
-
): Server<
|
|
1368
|
+
): Server<
|
|
1369
|
+
AddRoute<R, "PUT", Path, RouteInfoFor<Path, S, OutputOf<H>, HookOutput>>,
|
|
1370
|
+
Ctx,
|
|
1371
|
+
HookOutput
|
|
1372
|
+
>
|
|
1332
1373
|
put<Path extends string, H extends Handler<Path, RouteSchema, Ctx>>(
|
|
1333
1374
|
path: Path,
|
|
1334
1375
|
handler: H,
|
|
1335
|
-
): Server<
|
|
1376
|
+
): Server<
|
|
1377
|
+
AddRoute<R, "PUT", Path, RouteInfoFor<Path, Record<never, never>, OutputOf<H>, HookOutput>>,
|
|
1378
|
+
Ctx,
|
|
1379
|
+
HookOutput
|
|
1380
|
+
>
|
|
1336
1381
|
put(
|
|
1337
1382
|
path: string,
|
|
1338
1383
|
schemaOrHandler: RouteSchema | ErasedHandler,
|
|
1339
1384
|
handler?: ErasedHandler,
|
|
1340
|
-
): Server<Registry, Ctx> {
|
|
1385
|
+
): Server<Registry, Ctx, HookOutput> {
|
|
1341
1386
|
return this.route("PUT", path, schemaOrHandler, handler)
|
|
1342
1387
|
}
|
|
1343
1388
|
|
|
@@ -1345,16 +1390,24 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
1345
1390
|
path: Path,
|
|
1346
1391
|
schema: S,
|
|
1347
1392
|
handler: H,
|
|
1348
|
-
): Server<
|
|
1393
|
+
): Server<
|
|
1394
|
+
AddRoute<R, "PATCH", Path, RouteInfoFor<Path, S, OutputOf<H>, HookOutput>>,
|
|
1395
|
+
Ctx,
|
|
1396
|
+
HookOutput
|
|
1397
|
+
>
|
|
1349
1398
|
patch<Path extends string, H extends Handler<Path, RouteSchema, Ctx>>(
|
|
1350
1399
|
path: Path,
|
|
1351
1400
|
handler: H,
|
|
1352
|
-
): Server<
|
|
1401
|
+
): Server<
|
|
1402
|
+
AddRoute<R, "PATCH", Path, RouteInfoFor<Path, Record<never, never>, OutputOf<H>, HookOutput>>,
|
|
1403
|
+
Ctx,
|
|
1404
|
+
HookOutput
|
|
1405
|
+
>
|
|
1353
1406
|
patch(
|
|
1354
1407
|
path: string,
|
|
1355
1408
|
schemaOrHandler: RouteSchema | ErasedHandler,
|
|
1356
1409
|
handler?: ErasedHandler,
|
|
1357
|
-
): Server<Registry, Ctx> {
|
|
1410
|
+
): Server<Registry, Ctx, HookOutput> {
|
|
1358
1411
|
return this.route("PATCH", path, schemaOrHandler, handler)
|
|
1359
1412
|
}
|
|
1360
1413
|
|
|
@@ -1362,16 +1415,24 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
1362
1415
|
path: Path,
|
|
1363
1416
|
schema: S,
|
|
1364
1417
|
handler: H,
|
|
1365
|
-
): Server<
|
|
1418
|
+
): Server<
|
|
1419
|
+
AddRoute<R, "DELETE", Path, RouteInfoFor<Path, S, OutputOf<H>, HookOutput>>,
|
|
1420
|
+
Ctx,
|
|
1421
|
+
HookOutput
|
|
1422
|
+
>
|
|
1366
1423
|
delete<Path extends string, H extends Handler<Path, RouteSchema, Ctx>>(
|
|
1367
1424
|
path: Path,
|
|
1368
1425
|
handler: H,
|
|
1369
|
-
): Server<
|
|
1426
|
+
): Server<
|
|
1427
|
+
AddRoute<R, "DELETE", Path, RouteInfoFor<Path, Record<never, never>, OutputOf<H>, HookOutput>>,
|
|
1428
|
+
Ctx,
|
|
1429
|
+
HookOutput
|
|
1430
|
+
>
|
|
1370
1431
|
delete(
|
|
1371
1432
|
path: string,
|
|
1372
1433
|
schemaOrHandler: RouteSchema | ErasedHandler,
|
|
1373
1434
|
handler?: ErasedHandler,
|
|
1374
|
-
): Server<Registry, Ctx> {
|
|
1435
|
+
): Server<Registry, Ctx, HookOutput> {
|
|
1375
1436
|
return this.route("DELETE", path, schemaOrHandler, handler)
|
|
1376
1437
|
}
|
|
1377
1438
|
|
|
@@ -1414,10 +1475,12 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
1414
1475
|
S["output"] extends StandardSchemaV1
|
|
1415
1476
|
? { body: S["input"]; response: S["output"] }
|
|
1416
1477
|
: { body: S["input"] },
|
|
1417
|
-
OutputOf<H
|
|
1478
|
+
OutputOf<H>,
|
|
1479
|
+
HookOutput
|
|
1418
1480
|
>
|
|
1419
1481
|
>,
|
|
1420
|
-
Ctx
|
|
1482
|
+
Ctx,
|
|
1483
|
+
HookOutput
|
|
1421
1484
|
>
|
|
1422
1485
|
tool(
|
|
1423
1486
|
name: string,
|
|
@@ -1428,7 +1491,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
1428
1491
|
annotations?: ToolAnnotations
|
|
1429
1492
|
},
|
|
1430
1493
|
handler: (input: unknown, ctx: Context & Ctx) => unknown,
|
|
1431
|
-
): Server<Registry, Ctx> {
|
|
1494
|
+
): Server<Registry, Ctx, HookOutput> {
|
|
1432
1495
|
const plan = requireMcpRuntime(this.mcpRuntime).tool(
|
|
1433
1496
|
name,
|
|
1434
1497
|
config,
|
|
@@ -1441,7 +1504,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
1441
1504
|
if (lastRoute) {
|
|
1442
1505
|
;(lastRoute as { tool?: RouteDescriptor["tool"] }).tool = plan.descriptor
|
|
1443
1506
|
}
|
|
1444
|
-
return this as unknown as Server<Registry, Ctx>
|
|
1507
|
+
return this as unknown as Server<Registry, Ctx, HookOutput>
|
|
1445
1508
|
}
|
|
1446
1509
|
|
|
1447
1510
|
/**
|
|
@@ -1454,7 +1517,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
1454
1517
|
uri: string,
|
|
1455
1518
|
config: { readonly name: string; readonly description?: string; readonly mimeType?: string },
|
|
1456
1519
|
read: McpResourceDescriptor["read"],
|
|
1457
|
-
): Server<R, Ctx> {
|
|
1520
|
+
): Server<R, Ctx, HookOutput> {
|
|
1458
1521
|
this.assertConfigurable("resource()")
|
|
1459
1522
|
this.mcpResourceList.push(requireMcpRuntime(this.mcpRuntime).resource(uri, config, read))
|
|
1460
1523
|
return this
|
|
@@ -1468,7 +1531,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
1468
1531
|
name: string,
|
|
1469
1532
|
config: { readonly description: string; readonly arguments?: readonly PromptArgument[] },
|
|
1470
1533
|
handler: McpPromptDescriptor["handler"],
|
|
1471
|
-
): Server<R, Ctx> {
|
|
1534
|
+
): Server<R, Ctx, HookOutput> {
|
|
1472
1535
|
this.assertConfigurable("prompt()")
|
|
1473
1536
|
this.mcpPromptList.push(requireMcpRuntime(this.mcpRuntime).prompt(name, config, handler))
|
|
1474
1537
|
return this
|
|
@@ -1508,8 +1571,8 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
1508
1571
|
path: Path,
|
|
1509
1572
|
handler: WebSocketHandler<Data, EnvOf<Ctx>, Schema, Send>,
|
|
1510
1573
|
): string extends Path
|
|
1511
|
-
? Server<R, Ctx>
|
|
1512
|
-
: 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> {
|
|
1513
1576
|
this.assertConfigurable("ws()")
|
|
1514
1577
|
// Boot-time guard: the WS runtime is a subpath (`@nifrajs/core/ws`) so no-WebSocket apps don't
|
|
1515
1578
|
// bundle it. Registration is the loud, early failure point - never the first connection.
|
|
@@ -1520,6 +1583,9 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
1520
1583
|
`route WS ${path}: validateSend requires sendSchema`,
|
|
1521
1584
|
)
|
|
1522
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
|
|
1523
1589
|
this.topics ??= runtime.createTopics()
|
|
1524
1590
|
// A `messageSchema` wraps `message` with validation once, here - every adapter then dispatches
|
|
1525
1591
|
// already-validated, typed messages (Bun/Deno/Node/Workers) with no per-adapter code.
|
|
@@ -1536,6 +1602,12 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
1536
1602
|
* an external fan-out (Redis, a Durable Object) to this. A no-op when nobody is subscribed.
|
|
1537
1603
|
*/
|
|
1538
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
|
+
}
|
|
1539
1611
|
// No `app.ws()` yet ⇒ no registry and necessarily no subscribers - a publish is a no-op anyway.
|
|
1540
1612
|
this.topics?.publish(topic, data)
|
|
1541
1613
|
}
|
|
@@ -1545,7 +1617,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
1545
1617
|
path: string,
|
|
1546
1618
|
schemaOrHandler: RouteSchema | ErasedHandler,
|
|
1547
1619
|
handler?: ErasedHandler,
|
|
1548
|
-
): Server<Registry, Ctx> {
|
|
1620
|
+
): Server<Registry, Ctx, HookOutput> {
|
|
1549
1621
|
let rawHandler: ErasedHandler
|
|
1550
1622
|
let schema: RouteSchema | undefined
|
|
1551
1623
|
if (handler !== undefined) {
|
|
@@ -1558,7 +1630,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
1558
1630
|
this.register(method, path, schema, rawHandler)
|
|
1559
1631
|
// The accumulated registry type is compile-time only; the same instance
|
|
1560
1632
|
// carries every route, so the public methods re-type `this` per call.
|
|
1561
|
-
return this as unknown as Server<Registry, Ctx>
|
|
1633
|
+
return this as unknown as Server<Registry, Ctx, HookOutput>
|
|
1562
1634
|
}
|
|
1563
1635
|
|
|
1564
1636
|
/**
|
|
@@ -1599,162 +1671,58 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
1599
1671
|
schema: RouteSchema | undefined,
|
|
1600
1672
|
handler: (context: never) => unknown,
|
|
1601
1673
|
): CatalogRoute {
|
|
1602
|
-
const
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
'bodyLimit: "unlimited" cannot be used with a body schema; use a finite cap',
|
|
1622
|
-
)
|
|
1623
|
-
}
|
|
1624
|
-
if (schema.idempotency !== undefined) {
|
|
1625
|
-
invalidBodyLimit('bodyLimit: "unlimited" cannot be used with idempotency')
|
|
1626
|
-
}
|
|
1627
|
-
bodyLimit = undefined
|
|
1628
|
-
} else if (schema?.bodyLimit !== undefined) {
|
|
1629
|
-
try {
|
|
1630
|
-
assertByteLimit(schema.bodyLimit, "route bodyLimit")
|
|
1631
|
-
} catch {
|
|
1632
|
-
invalidBodyLimit('bodyLimit must be a non-negative safe integer or "unlimited"')
|
|
1633
|
-
}
|
|
1634
|
-
bodyLimit = schema.bodyLimit
|
|
1635
|
-
}
|
|
1636
|
-
const capabilities = normalizeRouteCapabilities(schema?.capabilities)
|
|
1637
|
-
const handlerAssurance = assuranceDeclarationsOf(handler as unknown as object)
|
|
1638
|
-
const invalidHandlerScope = handlerAssurance.find(
|
|
1639
|
-
(declaration) => declaration.scope !== "plugin",
|
|
1640
|
-
)
|
|
1641
|
-
if (invalidHandlerScope !== undefined) {
|
|
1642
|
-
throw new RouteConfigError(
|
|
1643
|
-
"INVALID_ASSURANCE",
|
|
1644
|
-
`route handler assurance must use plugin scope (received ${invalidHandlerScope.scope})`,
|
|
1645
|
-
)
|
|
1646
|
-
}
|
|
1647
|
-
const authenticated = assuranceEvidenceFor(
|
|
1648
|
-
[...this.activeAssurance, ...handlerAssurance, ...this.globalAssurance],
|
|
1649
|
-
method,
|
|
1650
|
-
path,
|
|
1651
|
-
).some((evidence) => evidence.id === NIFRA_ASSURANCE_IDS.AUTHENTICATED)
|
|
1652
|
-
const routeDecorations: Record<PropertyKey, unknown> = { ...this.decorations }
|
|
1653
|
-
if (capabilities.length > 0) {
|
|
1654
|
-
routeDecorations[CAPABILITY_GUARD] = createCapabilityGuard(
|
|
1655
|
-
capabilities,
|
|
1656
|
-
method,
|
|
1657
|
-
path,
|
|
1658
|
-
this.onCapabilityUse,
|
|
1659
|
-
this.idempotencyRuntime?.trackEffect,
|
|
1660
|
-
Object.freeze([...this.capabilityInterceptors]),
|
|
1661
|
-
Object.freeze([...this.capabilityObservers]),
|
|
1662
|
-
)
|
|
1663
|
-
}
|
|
1664
|
-
const hasDecorations = Reflect.ownKeys(routeDecorations).length > 0
|
|
1665
|
-
// An idempotency route runs a dedupe lane that must buffer the body and capture the response, so it
|
|
1666
|
-
// never takes the fused/native fast path - force it onto the portable matched lane (which routes
|
|
1667
|
-
// through `fetchMatched`, where the dedupe wrapper lives). Fail closed: a route may not declare
|
|
1668
|
-
// idempotency unless the idempotency runtime is installed, so the safety gate can never be silently
|
|
1669
|
-
// dropped by a missing plugin.
|
|
1670
|
-
if (schema?.idempotency !== undefined && this.idempotencyRuntime === undefined) {
|
|
1671
|
-
throw new RouteConfigError(
|
|
1672
|
-
"INVALID_IDEMPOTENCY",
|
|
1673
|
-
"route declares idempotency but the idempotency plugin is not installed; add .use(idempotency())",
|
|
1674
|
-
)
|
|
1675
|
-
}
|
|
1676
|
-
const idempotent = this.idempotencyRuntime?.resolve(schema, authenticated, this.maxBodyBytes)
|
|
1677
|
-
// A ledgered route (capabilities declared + `.use(effectLedger())`) needs a per-request
|
|
1678
|
-
// context to carry the ledger and a settle step to seal + sink it, so it too leaves the
|
|
1679
|
-
// fused/contextless fast path. Resolved per route, at registration - like the capability guard.
|
|
1680
|
-
const ledgered: ResolvedEffectLedger | undefined = this.effectLedgerRuntime?.resolve(
|
|
1681
|
-
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
|
+
},
|
|
1682
1693
|
method,
|
|
1683
1694
|
path,
|
|
1695
|
+
schema,
|
|
1696
|
+
handler,
|
|
1684
1697
|
)
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
schema?.body === undefined &&
|
|
1699
|
-
schema?.query === undefined &&
|
|
1700
|
-
idempotent === undefined &&
|
|
1701
|
-
ledgered === undefined &&
|
|
1702
|
-
contracted === undefined &&
|
|
1703
|
-
this.derives.length === 0 &&
|
|
1704
|
-
this.beforeHandleHooks.length === 0 &&
|
|
1705
|
-
this.afterHandleHooks.length === 0 &&
|
|
1706
|
-
this.onErrorHooks.length === 0
|
|
1707
|
-
// A route whose ONLY lifecycle step is a query schema can fuse too: the parse + validate +
|
|
1708
|
-
// handler + respond collapse into one closure with no lifecycle promise on the sync path. The
|
|
1709
|
-
// guards mirror the `query` lane below PLUS everything the fused dispatch skips: around hooks,
|
|
1710
|
-
// the idempotency/ledger wrappers, and validation-error recovery (schema or server default) -
|
|
1711
|
-
// the fused invalid path is exactly `validationError(issues)`, so any recovery semantics keep
|
|
1712
|
-
// the generic lane.
|
|
1713
|
-
const fusedQuery =
|
|
1714
|
-
!bare &&
|
|
1715
|
-
contracted === undefined &&
|
|
1716
|
-
schema?.query !== undefined &&
|
|
1717
|
-
schema.body === undefined &&
|
|
1718
|
-
schema.params === undefined &&
|
|
1719
|
-
schema.headers === undefined &&
|
|
1720
|
-
schema.onValidationError === undefined &&
|
|
1721
|
-
this.defaultOnValidationError === undefined &&
|
|
1722
|
-
idempotent === undefined &&
|
|
1723
|
-
ledgered === undefined &&
|
|
1724
|
-
this.derives.length === 0 &&
|
|
1725
|
-
this.beforeHandleHooks.length === 0 &&
|
|
1726
|
-
this.afterHandleHooks.length === 0 &&
|
|
1727
|
-
this.onErrorHooks.length === 0 &&
|
|
1728
|
-
this.aroundHooks.length === 0
|
|
1729
|
-
const bodyOnly =
|
|
1730
|
-
contracted === undefined &&
|
|
1731
|
-
schema?.body !== undefined &&
|
|
1732
|
-
schema.query === undefined &&
|
|
1733
|
-
schema.params === undefined &&
|
|
1734
|
-
schema.headers === undefined &&
|
|
1735
|
-
this.derives.length === 0 &&
|
|
1736
|
-
this.beforeHandleHooks.length === 0 &&
|
|
1737
|
-
this.afterHandleHooks.length === 0 &&
|
|
1738
|
-
this.onErrorHooks.length === 0
|
|
1739
|
-
const fusedBody =
|
|
1740
|
-
!bare &&
|
|
1741
|
-
bodyOnly &&
|
|
1742
|
-
schema.onValidationError === undefined &&
|
|
1743
|
-
this.defaultOnValidationError === undefined &&
|
|
1744
|
-
idempotent === undefined &&
|
|
1745
|
-
ledgered === undefined &&
|
|
1746
|
-
this.aroundHooks.length === 0
|
|
1747
|
-
// Body-only routes still need an async body read, but eligible routes can compile the validation +
|
|
1748
|
-
// handler continuation once. The runner keeps the bounded parser and all error semantics while
|
|
1749
|
-
// 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
|
|
1750
1711
|
const fusedBodyRunner = fusedBody
|
|
1751
1712
|
? this.buildFusedBodyRunner(
|
|
1752
1713
|
handler as unknown as InternalHandler,
|
|
1753
|
-
schema
|
|
1714
|
+
schema?.body as StandardSchemaV1,
|
|
1754
1715
|
hasDecorations ? routeDecorations : undefined,
|
|
1755
1716
|
bodyLimit ?? UNLIMITED_BODY_BYTES,
|
|
1756
1717
|
)
|
|
1757
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"
|
|
1758
1726
|
const fusedWeb =
|
|
1759
1727
|
bare && this.aroundHooks.length === 0
|
|
1760
1728
|
? this.buildFusedWeb(
|
|
@@ -1767,87 +1735,79 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
1767
1735
|
? this.buildFusedQueryWeb(
|
|
1768
1736
|
handler as unknown as InternalHandler,
|
|
1769
1737
|
hasDecorations ? routeDecorations : undefined,
|
|
1770
|
-
schema
|
|
1738
|
+
schema?.query as StandardSchemaV1,
|
|
1771
1739
|
bodyLimit ?? UNLIMITED_BODY_BYTES,
|
|
1772
1740
|
)
|
|
1773
1741
|
: fusedBody
|
|
1774
1742
|
? this.buildFusedBodyWeb(fusedBodyRunner as FusedBodyRunner)
|
|
1775
|
-
:
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
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
|
-
this.
|
|
1817
|
-
this.beforeHandleHooks.length === 1 &&
|
|
1818
|
-
this.afterHandleHooks.length === 0 &&
|
|
1819
|
-
this.onErrorHooks.length === 0
|
|
1820
|
-
? "derive-before"
|
|
1821
|
-
: 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)
|
|
1822
1785
|
const execution = compileRouteExecutionPlan({
|
|
1823
|
-
lane,
|
|
1786
|
+
lane: lanes.lane,
|
|
1824
1787
|
contextless,
|
|
1825
1788
|
hasAround: this.aroundHooks.length > 0,
|
|
1826
1789
|
hasLedger: ledgered !== undefined,
|
|
1827
|
-
lifecycleLane,
|
|
1828
1790
|
fusedWeb,
|
|
1829
1791
|
fusedBody: fusedBodyRunner,
|
|
1830
|
-
fusedLane:
|
|
1831
|
-
fusedWeb === undefined ? undefined : fusedQuery ? "query" : fusedBody ? "body" : "bare",
|
|
1792
|
+
fusedLane: lanes.fusedLane,
|
|
1832
1793
|
})
|
|
1833
1794
|
const registeredEntry: RouteEntry = {
|
|
1834
|
-
// (context: never) => unknown -> InternalHandler: the framework invokes it
|
|
1835
|
-
// with the concrete RawContext the typed handler expects, so this is sound.
|
|
1836
1795
|
handler: handler as unknown as InternalHandler,
|
|
1837
1796
|
schema,
|
|
1838
1797
|
bodyLimit,
|
|
1839
1798
|
idempotent,
|
|
1840
1799
|
ledgered,
|
|
1841
|
-
responseContract
|
|
1800
|
+
responseContract,
|
|
1842
1801
|
derives: [...this.derives],
|
|
1843
1802
|
decorations: routeDecorations,
|
|
1844
1803
|
hasDecorations,
|
|
1845
1804
|
beforeHandle: [...this.beforeHandleHooks],
|
|
1846
1805
|
afterHandle: [...this.afterHandleHooks],
|
|
1847
1806
|
onError: [...this.onErrorHooks],
|
|
1848
|
-
lifecycleHookLane,
|
|
1807
|
+
lifecycleHookLane: lanes.lifecycleHookLane,
|
|
1849
1808
|
around: [...this.aroundHooks],
|
|
1850
1809
|
execution,
|
|
1810
|
+
program,
|
|
1851
1811
|
}
|
|
1852
1812
|
const descriptor: RouteDescriptor = {
|
|
1853
1813
|
method,
|
|
@@ -1856,72 +1816,13 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
1856
1816
|
...(capabilities.length > 0 ? { capabilities } : {}),
|
|
1857
1817
|
...(schema?.family === true ? { family: true } : {}),
|
|
1858
1818
|
}
|
|
1859
|
-
const routeAssurance: AssuranceDeclaration[] = [...this.activeAssurance, ...handlerAssurance]
|
|
1860
|
-
if (contracted?.runtime.mode === "enforce" && schema?.response !== undefined) {
|
|
1861
|
-
routeAssurance.push(
|
|
1862
|
-
Object.freeze({
|
|
1863
|
-
id: NIFRA_ASSURANCE_IDS.RESPONSE_CONTRACT,
|
|
1864
|
-
source: "response-contract",
|
|
1865
|
-
scope: "plugin",
|
|
1866
|
-
}),
|
|
1867
|
-
)
|
|
1868
|
-
}
|
|
1869
|
-
// Inline `schema.assurance`: the route DECLARES its enforcement evidence adjacent to the handler, so an
|
|
1870
|
-
// in-handler-guarded route satisfies a policy `require:` clause without a `withRouteAssurance` middleware
|
|
1871
|
-
// rewrite. Each id becomes route-scoped `declared` evidence (invalid ids fail closed at registration).
|
|
1872
|
-
for (const id of schema?.assurance ?? []) {
|
|
1873
|
-
if (!validEvidenceId(id)) {
|
|
1874
|
-
throw new Error(
|
|
1875
|
-
`route assurance: invalid evidence id ${JSON.stringify(id)} on ${method} ${path} (use lowercase dot/dash segments)`,
|
|
1876
|
-
)
|
|
1877
|
-
}
|
|
1878
|
-
const declared = { id, source: "declared", scope: "plugin" } as AssuranceDeclaration
|
|
1879
|
-
Object.defineProperty(declared, "provenance", {
|
|
1880
|
-
value: "declared",
|
|
1881
|
-
enumerable: false,
|
|
1882
|
-
configurable: false,
|
|
1883
|
-
writable: false,
|
|
1884
|
-
})
|
|
1885
|
-
routeAssurance.push(Object.freeze(declared))
|
|
1886
|
-
}
|
|
1887
|
-
// Two ways a route proves its body is bounded, both enforced by the core at read time:
|
|
1888
|
-
// `schema.body` (the validated read is bounded) and an explicit finite `schema.bodyLimit` (the
|
|
1889
|
-
// transport cap shadows every reader, including a raw `c.req.body` stream that a Content-Length
|
|
1890
|
-
// middleware cannot bound).
|
|
1891
|
-
// `bodyLimit: "unlimited"` is rejected above for schema and idempotency routes. The remaining
|
|
1892
|
-
// exemption is schema-less streaming/upload work, so it publishes NOTHING: there is no finite
|
|
1893
|
-
// framework byte ceiling for assurance to attest and the application protocol owns that bound.
|
|
1894
|
-
// The server-wide `maxBodyBytes` default is not evidence either. It applies to every route
|
|
1895
|
-
// whether or not anyone thought about it, so publishing it would make the id pass everywhere and
|
|
1896
|
-
// prove nothing; this evidence marks a bound the route chose.
|
|
1897
|
-
const unlimitedBody = schema?.bodyLimit === "unlimited"
|
|
1898
|
-
if (!unlimitedBody && (schema?.body !== undefined || typeof schema?.bodyLimit === "number")) {
|
|
1899
|
-
routeAssurance.push(
|
|
1900
|
-
Object.freeze({
|
|
1901
|
-
id: NIFRA_ASSURANCE_IDS.BODY_BOUNDED,
|
|
1902
|
-
source: "route-schema",
|
|
1903
|
-
scope: "plugin",
|
|
1904
|
-
}),
|
|
1905
|
-
)
|
|
1906
|
-
}
|
|
1907
|
-
// Declaring `schema.idempotency` is evidence for request replay only. It deliberately never proves
|
|
1908
|
-
// durable command execution; that stronger evidence belongs to a command/outbox adapter.
|
|
1909
|
-
if (schema?.idempotency !== undefined) {
|
|
1910
|
-
routeAssurance.push(
|
|
1911
|
-
Object.freeze({
|
|
1912
|
-
id: NIFRA_ASSURANCE_IDS.IDEMPOTENCY_KEY,
|
|
1913
|
-
source: "route-schema",
|
|
1914
|
-
scope: "plugin",
|
|
1915
|
-
}),
|
|
1916
|
-
)
|
|
1917
|
-
}
|
|
1918
1819
|
return {
|
|
1919
1820
|
method,
|
|
1920
1821
|
path,
|
|
1921
1822
|
pattern,
|
|
1922
1823
|
entry: registeredEntry,
|
|
1923
1824
|
descriptor,
|
|
1924
|
-
assurance:
|
|
1825
|
+
assurance: routeAssurance,
|
|
1925
1826
|
}
|
|
1926
1827
|
}
|
|
1927
1828
|
|
|
@@ -2032,7 +1933,9 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
2032
1933
|
* `RouteConfigError` at merge time, and a group with WebSocket routes is refused (register
|
|
2033
1934
|
* those on the parent).
|
|
2034
1935
|
*/
|
|
2035
|
-
merge<R2 extends Registry, Ctx2
|
|
1936
|
+
merge<R2 extends Registry, Ctx2, HookOutput2>(
|
|
1937
|
+
other: Server<R2, Ctx2, HookOutput2>,
|
|
1938
|
+
): Server<R & R2, Ctx, HookOutput> {
|
|
2036
1939
|
this.assertConfigurable("merge()")
|
|
2037
1940
|
const source = other as unknown as Server<Registry, EmptyContext>
|
|
2038
1941
|
if (source.wsRouteCount > 0) {
|
|
@@ -2117,7 +2020,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
2117
2020
|
if (!scoped) this.globalAssurance.push(...source.globalAssurance)
|
|
2118
2021
|
this.mcpResourceList.push(...source.mcpResourceList)
|
|
2119
2022
|
this.mcpPromptList.push(...source.mcpPromptList)
|
|
2120
|
-
return this as unknown as Server<R & R2, Ctx>
|
|
2023
|
+
return this as unknown as Server<R & R2, Ctx, HookOutput>
|
|
2121
2024
|
}
|
|
2122
2025
|
|
|
2123
2026
|
/** A fused renderer closes over runtime services to keep its seven-argument JSC fast path. Merging
|
|
@@ -2127,8 +2030,14 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
2127
2030
|
if (entry.execution.fusedWeb === undefined) return route
|
|
2128
2031
|
// Rebuild with the SAME builder that produced the closure - rebinding a query-fused route as
|
|
2129
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.
|
|
2130
2039
|
const fusedBody =
|
|
2131
|
-
|
|
2040
|
+
laneName === "body"
|
|
2132
2041
|
? this.buildFusedBodyRunner(
|
|
2133
2042
|
entry.handler,
|
|
2134
2043
|
entry.schema?.body as StandardSchemaV1,
|
|
@@ -2136,22 +2045,66 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
2136
2045
|
entry.bodyLimit ?? UNLIMITED_BODY_BYTES,
|
|
2137
2046
|
)
|
|
2138
2047
|
: undefined
|
|
2048
|
+
const derive = entry.derives[0]
|
|
2049
|
+
const before = entry.beforeHandle[0]
|
|
2050
|
+
const after = entry.afterHandle[0]
|
|
2139
2051
|
const fusedWeb =
|
|
2140
|
-
|
|
2052
|
+
laneName === "body"
|
|
2141
2053
|
? this.buildFusedBodyWeb(fusedBody as FusedBodyRunner)
|
|
2142
|
-
:
|
|
2054
|
+
: laneName === "query"
|
|
2143
2055
|
? this.buildFusedQueryWeb(
|
|
2144
2056
|
entry.handler,
|
|
2145
2057
|
entry.hasDecorations ? entry.decorations : undefined,
|
|
2146
2058
|
entry.schema?.query as StandardSchemaV1,
|
|
2147
2059
|
entry.bodyLimit ?? UNLIMITED_BODY_BYTES,
|
|
2148
2060
|
)
|
|
2149
|
-
:
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
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
|
+
)
|
|
2155
2108
|
return {
|
|
2156
2109
|
...route,
|
|
2157
2110
|
entry: {
|
|
@@ -2949,7 +2902,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
2949
2902
|
/**
|
|
2950
2903
|
* Route → build context → run. Synchronous through to the handler for a **bare** route
|
|
2951
2904
|
* (selected by its compiled plan), so a sync handler produces its result with zero promise allocations;
|
|
2952
|
-
* routes with validation/hooks keep the full
|
|
2905
|
+
* routes with validation/hooks keep the full compiled route program, unchanged.
|
|
2953
2906
|
*/
|
|
2954
2907
|
private routeAndRun<T>(
|
|
2955
2908
|
source: RequestSource,
|
|
@@ -3219,7 +3172,6 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
3219
3172
|
/** The narrowest bare route: a syntactic `() => ...` handler cannot observe the context argument, so
|
|
3220
3173
|
* successful requests can skip allocating `RequestContext`. Errors still allocate one for logging. */
|
|
3221
3174
|
// @ts-expect-error TS6133 -- invoked structurally by compiled plans (internal/route-execution.ts)
|
|
3222
|
-
// biome-ignore lint/correctness/noUnusedPrivateClassMembers: invoked structurally by compiled plans (internal/route-execution.ts)
|
|
3223
3175
|
private runContextlessBare<T>(
|
|
3224
3176
|
entry: RouteEntry,
|
|
3225
3177
|
source: RequestSource,
|
|
@@ -3290,19 +3242,18 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
3290
3242
|
this.protoPoisoning,
|
|
3291
3243
|
)
|
|
3292
3244
|
this.logRequestError(err, ctx)
|
|
3293
|
-
return wrapResponse
|
|
3245
|
+
return this.internalErrorResponse(wrapResponse)
|
|
3294
3246
|
}
|
|
3295
3247
|
|
|
3296
3248
|
/**
|
|
3297
3249
|
* The synchronous fast path selected by a route's execution plan: apply static decorations, call the
|
|
3298
3250
|
* handler, render the result - **no `await`** unless the handler itself returns a promise. It mirrors
|
|
3299
|
-
* the
|
|
3251
|
+
* the same error boundary as the compiled route program (which a bare route would otherwise no-op through) and shares
|
|
3300
3252
|
* {@link logRequestError}; a bare route has no `onError` hooks, so error handling is fully synchronous
|
|
3301
3253
|
* (a thrown `Response` is control flow; anything else is a logged flat 500). This is where nifra skips
|
|
3302
3254
|
* the per-request async-frame tax - the same win codegen routers get, but without `eval`.
|
|
3303
3255
|
*/
|
|
3304
3256
|
// @ts-expect-error TS6133 -- invoked structurally by compiled plans (internal/route-execution.ts)
|
|
3305
|
-
// biome-ignore lint/correctness/noUnusedPrivateClassMembers: invoked structurally by compiled plans (internal/route-execution.ts)
|
|
3306
3257
|
private runBare<T>(
|
|
3307
3258
|
entry: RouteEntry,
|
|
3308
3259
|
ctx: RawContext,
|
|
@@ -3326,7 +3277,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
3326
3277
|
return finalize(result, responseSet(ctx))
|
|
3327
3278
|
}
|
|
3328
3279
|
|
|
3329
|
-
/** Bare-route error rendering - identical to
|
|
3280
|
+
/** Bare-route error rendering - identical to the compiled route program's catch minus the (absent) onError
|
|
3330
3281
|
* loop: a thrown `Response` is returned as deliberate control flow; anything else is logged + 500. */
|
|
3331
3282
|
/**
|
|
3332
3283
|
* Build a route's fused Web renderer. Composition happens once at
|
|
@@ -3704,1010 +3655,968 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
3704
3655
|
}
|
|
3705
3656
|
}
|
|
3706
3657
|
|
|
3707
|
-
|
|
3708
|
-
|
|
3709
|
-
|
|
3710
|
-
|
|
3711
|
-
|
|
3712
|
-
|
|
3713
|
-
|
|
3714
|
-
|
|
3715
|
-
|
|
3716
|
-
|
|
3717
|
-
|
|
3718
|
-
|
|
3719
|
-
|
|
3720
|
-
|
|
3721
|
-
|
|
3722
|
-
|
|
3723
|
-
private
|
|
3724
|
-
|
|
3725
|
-
|
|
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
|
-
|
|
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,
|
|
3679
|
+
maxBodyBytes: number,
|
|
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,
|
|
3687
|
+
)
|
|
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,
|
|
3727
|
+
)
|
|
3728
|
+
}
|
|
3729
|
+
const runBefore = (): MaybePromise<Response> => {
|
|
3730
|
+
let early: unknown
|
|
3731
|
+
try {
|
|
3732
|
+
early = before(ctx)
|
|
3733
|
+
} catch (err) {
|
|
3734
|
+
return logError(err, ctx)
|
|
3735
|
+
}
|
|
3736
|
+
if (early instanceof Promise) {
|
|
3737
|
+
return early.then(
|
|
3738
|
+
(settled) => {
|
|
3739
|
+
if (settled === undefined) return runHandler()
|
|
3740
|
+
return fusedRespond(settled, ctx, this.responseBodyTag, this.staticResponseHeaders)
|
|
3741
|
+
},
|
|
3742
|
+
(err) => logError(err, ctx),
|
|
3743
|
+
)
|
|
3744
|
+
}
|
|
3745
|
+
if (early === undefined) return runHandler()
|
|
3746
|
+
return fusedRespond(early, ctx, this.responseBodyTag, this.staticResponseHeaders)
|
|
3747
|
+
}
|
|
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),
|
|
3765
|
+
)
|
|
3766
|
+
}
|
|
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)
|
|
3779
|
+
}
|
|
3780
|
+
if (validation instanceof Promise) {
|
|
3781
|
+
return validation.then(
|
|
3782
|
+
(settled) => {
|
|
3783
|
+
if (settled.issues !== undefined)
|
|
3784
|
+
return this.wrapWebResponse(plainValidationError(settled.issues))
|
|
3785
|
+
ctx.query = settled.value
|
|
3786
|
+
return runDerive()
|
|
3787
|
+
},
|
|
3788
|
+
(err) => logError(err, ctx),
|
|
3789
|
+
)
|
|
3790
|
+
}
|
|
3791
|
+
if (validation.issues !== undefined)
|
|
3792
|
+
return this.wrapWebResponse(plainValidationError(validation.issues))
|
|
3793
|
+
ctx.query = validation.value
|
|
3794
|
+
return runDerive()
|
|
3755
3795
|
}
|
|
3756
|
-
return dispatch(0)
|
|
3757
3796
|
}
|
|
3758
3797
|
|
|
3759
|
-
|
|
3760
|
-
|
|
3761
|
-
|
|
3762
|
-
|
|
3763
|
-
|
|
3764
|
-
|
|
3765
|
-
|
|
3766
|
-
|
|
3767
|
-
|
|
3768
|
-
|
|
3769
|
-
|
|
3770
|
-
|
|
3771
|
-
|
|
3772
|
-
|
|
3773
|
-
|
|
3774
|
-
|
|
3775
|
-
)
|
|
3776
|
-
|
|
3777
|
-
|
|
3778
|
-
|
|
3779
|
-
|
|
3780
|
-
|
|
3781
|
-
private finishBodyOnly<T>(
|
|
3782
|
-
entry: RouteEntry,
|
|
3783
|
-
parsed: unknown,
|
|
3784
|
-
ctx: RawContext,
|
|
3785
|
-
finalize: (result: unknown, set: CtxSet) => T,
|
|
3786
|
-
wrapResponse: (response: Response | ResponseResult) => T,
|
|
3787
|
-
): MaybePromise<T> {
|
|
3788
|
-
try {
|
|
3789
|
-
const bodySchema = entry.schema!.body!
|
|
3790
|
-
const validation = bodySchema["~standard"].validate(parsed)
|
|
3791
|
-
if (validation instanceof Promise) {
|
|
3792
|
-
return validation.then(
|
|
3793
|
-
(result) => {
|
|
3794
|
-
try {
|
|
3795
|
-
const outcome = this.applyBodyValidation(entry, result, ctx, finalize, wrapResponse)
|
|
3796
|
-
return outcome instanceof Promise
|
|
3797
|
-
? outcome.catch((err) =>
|
|
3798
|
-
this.handleLifecycleError(entry, err, ctx, finalize, wrapResponse),
|
|
3799
|
-
)
|
|
3800
|
-
: outcome
|
|
3801
|
-
} catch (err) {
|
|
3802
|
-
return this.handleLifecycleError(entry, err, ctx, finalize, wrapResponse)
|
|
3803
|
-
}
|
|
3804
|
-
},
|
|
3805
|
-
(err) => this.handleLifecycleError(entry, err, ctx, finalize, wrapResponse),
|
|
3806
|
-
)
|
|
3807
|
-
}
|
|
3808
|
-
const outcome = this.applyBodyValidation(entry, validation, ctx, finalize, wrapResponse)
|
|
3809
|
-
return outcome instanceof Promise
|
|
3810
|
-
? outcome.catch((err) => this.handleLifecycleError(entry, err, ctx, finalize, wrapResponse))
|
|
3811
|
-
: outcome
|
|
3812
|
-
} catch (err) {
|
|
3813
|
-
return this.handleLifecycleError(entry, err, ctx, finalize, wrapResponse)
|
|
3814
|
-
}
|
|
3815
|
-
}
|
|
3816
|
-
|
|
3817
|
-
private executeHandler<T>(
|
|
3818
|
-
entry: RouteEntry,
|
|
3819
|
-
ctx: RawContext,
|
|
3820
|
-
finalize: (result: unknown, set: CtxSet) => T,
|
|
3821
|
-
wrapResponse?: (response: Response | ResponseResult) => T,
|
|
3822
|
-
): MaybePromise<T> {
|
|
3823
|
-
if (entry.hasDecorations) Object.assign(ctx, entry.decorations)
|
|
3824
|
-
const handlerOutput = entry.handler(ctx)
|
|
3825
|
-
if (handlerOutput instanceof Promise) {
|
|
3826
|
-
return handlerOutput.then(
|
|
3827
|
-
(value) => finalize(value, responseSet(ctx)),
|
|
3828
|
-
(err) => {
|
|
3829
|
-
if (wrapResponse) {
|
|
3830
|
-
return this.handleLifecycleError(entry, err, ctx, finalize, wrapResponse)
|
|
3831
|
-
}
|
|
3832
|
-
throw err
|
|
3833
|
-
},
|
|
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,
|
|
3834
3820
|
)
|
|
3835
|
-
|
|
3836
|
-
|
|
3837
|
-
|
|
3838
|
-
|
|
3839
|
-
|
|
3840
|
-
|
|
3841
|
-
|
|
3842
|
-
|
|
3843
|
-
|
|
3844
|
-
|
|
3845
|
-
|
|
3846
|
-
|
|
3847
|
-
|
|
3848
|
-
|
|
3849
|
-
|
|
3850
|
-
|
|
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),
|
|
3852
|
+
)
|
|
3853
|
+
}
|
|
3854
|
+
return fusedRespond(
|
|
3855
|
+
transformed,
|
|
3856
|
+
ctx,
|
|
3857
|
+
this.responseBodyTag,
|
|
3858
|
+
this.staticResponseHeaders,
|
|
3859
|
+
this.onResponseHooks.length === 0,
|
|
3860
|
+
)
|
|
3851
3861
|
}
|
|
3852
|
-
|
|
3853
|
-
|
|
3854
|
-
|
|
3855
|
-
|
|
3856
|
-
|
|
3857
|
-
|
|
3858
|
-
ctx.body = settled.value
|
|
3859
|
-
return this.executeHandler(entry, ctx, finalize)
|
|
3860
|
-
})
|
|
3862
|
+
const runHandler = (): MaybePromise<Response> => {
|
|
3863
|
+
let result: unknown
|
|
3864
|
+
try {
|
|
3865
|
+
result = handler(ctx)
|
|
3866
|
+
} catch (err) {
|
|
3867
|
+
return logError(err, ctx)
|
|
3861
3868
|
}
|
|
3862
|
-
if (
|
|
3863
|
-
return
|
|
3864
|
-
|
|
3865
|
-
|
|
3869
|
+
if (result instanceof Promise) {
|
|
3870
|
+
return result.then(
|
|
3871
|
+
(value) => runAfter(value),
|
|
3872
|
+
(err) => logError(err, ctx),
|
|
3873
|
+
)
|
|
3874
|
+
}
|
|
3875
|
+
return runAfter(result)
|
|
3866
3876
|
}
|
|
3867
|
-
|
|
3868
|
-
|
|
3869
|
-
|
|
3870
|
-
|
|
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(
|
|
3871
3886
|
(settled) => {
|
|
3872
|
-
if (settled
|
|
3873
|
-
|
|
3874
|
-
ctx.query = settled.value
|
|
3875
|
-
return this.executeHandler(entry, ctx, finalize, wrapResponse)
|
|
3887
|
+
if (settled === undefined) return runHandler()
|
|
3888
|
+
return fusedRespond(settled, ctx, this.responseBodyTag, this.staticResponseHeaders)
|
|
3876
3889
|
},
|
|
3877
|
-
(err) =>
|
|
3890
|
+
(err) => logError(err, ctx),
|
|
3878
3891
|
)
|
|
3879
3892
|
}
|
|
3880
|
-
if (
|
|
3881
|
-
|
|
3882
|
-
ctx.query = validation.value
|
|
3883
|
-
return this.executeHandler(entry, ctx, finalize, wrapResponse)
|
|
3893
|
+
if (early === undefined) return runHandler()
|
|
3894
|
+
return fusedRespond(early, ctx, this.responseBodyTag, this.staticResponseHeaders)
|
|
3884
3895
|
}
|
|
3885
|
-
|
|
3886
|
-
|
|
3887
|
-
|
|
3888
|
-
|
|
3889
|
-
|
|
3890
|
-
|
|
3891
|
-
|
|
3892
|
-
|
|
3893
|
-
|
|
3894
|
-
|
|
3895
|
-
|
|
3896
|
-
|
|
3897
|
-
|
|
3898
|
-
|
|
3899
|
-
|
|
3900
|
-
|
|
3901
|
-
|
|
3902
|
-
this.handleValidationErrorRecovery(
|
|
3903
|
-
entry,
|
|
3904
|
-
rec,
|
|
3905
|
-
ctx,
|
|
3906
|
-
finalize,
|
|
3907
|
-
wrapResponse,
|
|
3908
|
-
result.issues!,
|
|
3909
|
-
"body",
|
|
3910
|
-
),
|
|
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),
|
|
3911
3913
|
)
|
|
3912
3914
|
}
|
|
3913
|
-
|
|
3914
|
-
|
|
3915
|
-
|
|
3916
|
-
|
|
3917
|
-
|
|
3918
|
-
|
|
3919
|
-
|
|
3920
|
-
|
|
3921
|
-
|
|
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()
|
|
3920
|
+
}
|
|
3921
|
+
if (querySchema === undefined) return runDerive()
|
|
3922
|
+
let validation: MaybePromise<StandardResult<unknown>>
|
|
3923
|
+
try {
|
|
3924
|
+
validation = querySchema["~standard"].validate(queryObjectOf(ctx[CONTEXT_SEARCH]))
|
|
3925
|
+
} catch (err) {
|
|
3926
|
+
return logError(err, ctx)
|
|
3922
3927
|
}
|
|
3923
|
-
return wrapResponse(plainValidationError(result.issues))
|
|
3924
|
-
}
|
|
3925
|
-
ctx.body = result.value
|
|
3926
|
-
return this.executeHandler(entry, ctx, finalize)
|
|
3927
|
-
}
|
|
3928
|
-
|
|
3929
|
-
// @ts-expect-error TS6133 -- invoked structurally by compiled plans (internal/route-execution.ts)
|
|
3930
|
-
// biome-ignore lint/correctness/noUnusedPrivateClassMembers: invoked structurally by compiled plans (internal/route-execution.ts)
|
|
3931
|
-
private runQueryOnly<T>(
|
|
3932
|
-
entry: RouteEntry,
|
|
3933
|
-
ctx: RawContext,
|
|
3934
|
-
finalize: (result: unknown, set: CtxSet) => T,
|
|
3935
|
-
wrapResponse: (response: Response | ResponseResult) => T,
|
|
3936
|
-
): MaybePromise<T> {
|
|
3937
|
-
try {
|
|
3938
|
-
// Call the validator directly for the raw StandardResult (read `.issues`/`.value`) - skip
|
|
3939
|
-
// `validateStandard`'s per-request wrapper-object allocation, mirroring the bodyOnly path.
|
|
3940
|
-
const validation = entry.schema!.query!["~standard"].validate(
|
|
3941
|
-
queryObjectOf(ctx[CONTEXT_SEARCH]),
|
|
3942
|
-
)
|
|
3943
3928
|
if (validation instanceof Promise) {
|
|
3944
3929
|
return validation.then(
|
|
3945
3930
|
(settled) => {
|
|
3946
|
-
|
|
3947
|
-
|
|
3948
|
-
|
|
3949
|
-
|
|
3950
|
-
this.handleLifecycleError(entry, err, ctx, finalize, wrapResponse),
|
|
3951
|
-
)
|
|
3952
|
-
: outcome
|
|
3953
|
-
} catch (err) {
|
|
3954
|
-
return this.handleLifecycleError(entry, err, ctx, finalize, wrapResponse)
|
|
3955
|
-
}
|
|
3931
|
+
if (settled.issues !== undefined)
|
|
3932
|
+
return this.wrapWebResponse(plainValidationError(settled.issues))
|
|
3933
|
+
ctx.query = settled.value
|
|
3934
|
+
return runDerive()
|
|
3956
3935
|
},
|
|
3957
|
-
(err) =>
|
|
3958
|
-
)
|
|
3959
|
-
}
|
|
3960
|
-
return this.applyQueryValidation(entry, validation, ctx, finalize, wrapResponse)
|
|
3961
|
-
} catch (err) {
|
|
3962
|
-
return this.handleLifecycleError(entry, err, ctx, finalize, wrapResponse)
|
|
3963
|
-
}
|
|
3964
|
-
}
|
|
3965
|
-
|
|
3966
|
-
/** Validate-result → set `ctx.query` → run handler. A method (not a per-request closure), the
|
|
3967
|
-
* query analogue of {@link applyBodyValidation}. */
|
|
3968
|
-
private applyQueryValidation<T>(
|
|
3969
|
-
entry: RouteEntry,
|
|
3970
|
-
result: StandardResult<unknown>,
|
|
3971
|
-
ctx: RawContext,
|
|
3972
|
-
finalize: (result: unknown, set: CtxSet) => T,
|
|
3973
|
-
wrapResponse: (response: Response | ResponseResult) => T,
|
|
3974
|
-
): MaybePromise<T> {
|
|
3975
|
-
if (result.issues !== undefined) {
|
|
3976
|
-
const hook = entry.schema?.onValidationError ?? this.defaultOnValidationError
|
|
3977
|
-
if (hook) {
|
|
3978
|
-
const recovery = hook(result.issues, ctx as unknown as Context, "query")
|
|
3979
|
-
if (recovery instanceof Promise) {
|
|
3980
|
-
return recovery.then((rec) =>
|
|
3981
|
-
this.handleValidationErrorRecovery(
|
|
3982
|
-
entry,
|
|
3983
|
-
rec,
|
|
3984
|
-
ctx,
|
|
3985
|
-
finalize,
|
|
3986
|
-
wrapResponse,
|
|
3987
|
-
result.issues!,
|
|
3988
|
-
"query",
|
|
3989
|
-
),
|
|
3990
|
-
)
|
|
3991
|
-
}
|
|
3992
|
-
return this.handleValidationErrorRecovery(
|
|
3993
|
-
entry,
|
|
3994
|
-
recovery,
|
|
3995
|
-
ctx,
|
|
3996
|
-
finalize,
|
|
3997
|
-
wrapResponse,
|
|
3998
|
-
result.issues,
|
|
3999
|
-
"query",
|
|
3936
|
+
(err) => logError(err, ctx),
|
|
4000
3937
|
)
|
|
4001
3938
|
}
|
|
4002
|
-
|
|
3939
|
+
if (validation.issues !== undefined)
|
|
3940
|
+
return this.wrapWebResponse(plainValidationError(validation.issues))
|
|
3941
|
+
ctx.query = validation.value
|
|
3942
|
+
return runDerive()
|
|
4003
3943
|
}
|
|
4004
|
-
ctx.query = result.value
|
|
4005
|
-
return this.executeHandler(entry, ctx, finalize, wrapResponse)
|
|
4006
3944
|
}
|
|
4007
3945
|
|
|
4008
3946
|
/**
|
|
4009
|
-
*
|
|
4010
|
-
*
|
|
4011
|
-
*
|
|
4012
|
-
*
|
|
4013
|
-
*
|
|
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).
|
|
4014
3957
|
*/
|
|
4015
|
-
private
|
|
4016
|
-
|
|
4017
|
-
|
|
4018
|
-
|
|
4019
|
-
|
|
4020
|
-
|
|
4021
|
-
|
|
4022
|
-
|
|
4023
|
-
|
|
4024
|
-
|
|
4025
|
-
|
|
4026
|
-
|
|
4027
|
-
|
|
4028
|
-
|
|
4029
|
-
|
|
4030
|
-
|
|
4031
|
-
|
|
4032
|
-
?
|
|
4033
|
-
|
|
4034
|
-
|
|
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,
|
|
4035
3985
|
)
|
|
4036
|
-
|
|
4037
|
-
|
|
4038
|
-
|
|
4039
|
-
|
|
4040
|
-
|
|
4041
|
-
|
|
4042
|
-
|
|
4043
|
-
|
|
4044
|
-
|
|
4045
|
-
|
|
4046
|
-
|
|
4047
|
-
|
|
4048
|
-
}
|
|
4049
|
-
|
|
4050
|
-
private failResponseFinalization(
|
|
4051
|
-
response: Response,
|
|
4052
|
-
error: unknown,
|
|
4053
|
-
req: Request,
|
|
4054
|
-
): Promise<never> {
|
|
4055
|
-
const notified = this.notifyResponseFinalized({ response, error }, req)
|
|
4056
|
-
if (notified instanceof Promise) {
|
|
4057
|
-
return notified.then(() => {
|
|
4058
|
-
throw error
|
|
4059
|
-
})
|
|
4060
|
-
}
|
|
4061
|
-
// A hook failure must surface as a REJECTION even when every prior step ran synchronously:
|
|
4062
|
-
// `fetch()` may now resolve without a promise, but its failure contract stays promise-shaped -
|
|
4063
|
-
// a synchronous throw here would escape `Promise.resolve(app.fetch(...))` bridges and
|
|
4064
|
-
// `.then()`-style callers entirely instead of reaching their rejection handling.
|
|
4065
|
-
return Promise.reject(error)
|
|
4066
|
-
}
|
|
4067
|
-
|
|
4068
|
-
/** Notify terminal observers in order while isolating both sync and async failures. */
|
|
4069
|
-
private notifyResponseFinalized(outcome: ResponseFinalization, req: Request): MaybePromise<void> {
|
|
4070
|
-
let pending: Promise<void> | undefined
|
|
4071
|
-
for (const hook of this.onResponseFinalizedHooks) {
|
|
4072
|
-
if (pending !== undefined) {
|
|
4073
|
-
pending = pending.then(async () => {
|
|
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
|
|
4074
3998
|
try {
|
|
4075
|
-
|
|
4076
|
-
} catch {
|
|
4077
|
-
|
|
3999
|
+
transformed = after(result, ctx)
|
|
4000
|
+
} catch (err) {
|
|
4001
|
+
return logError(err, ctx)
|
|
4078
4002
|
}
|
|
4079
|
-
|
|
4080
|
-
|
|
4081
|
-
|
|
4082
|
-
|
|
4083
|
-
|
|
4084
|
-
|
|
4085
|
-
|
|
4086
|
-
|
|
4087
|
-
|
|
4088
|
-
|
|
4089
|
-
|
|
4090
|
-
|
|
4091
|
-
|
|
4092
|
-
|
|
4093
|
-
|
|
4094
|
-
|
|
4095
|
-
|
|
4096
|
-
|
|
4097
|
-
|
|
4098
|
-
let current = response
|
|
4099
|
-
for (let i = nextIndex; i < this.onResponseHooks.length; i++) {
|
|
4100
|
-
const next = (this.onResponseHooks[i] as RawOnResponse)(current, req)
|
|
4101
|
-
current = next instanceof Promise ? await next : next
|
|
4102
|
-
}
|
|
4103
|
-
return current
|
|
4104
|
-
}
|
|
4105
|
-
|
|
4106
|
-
/**
|
|
4107
|
-
* Bound the response time. On timeout we abort `ctx.signal` (so cancellation-aware
|
|
4108
|
-
* handlers can bail) and return 503; the in-flight work keeps running but its
|
|
4109
|
-
* result is discarded - JS can't forcibly cancel a promise.
|
|
4110
|
-
*/
|
|
4111
|
-
private async withTimeout<T>(
|
|
4112
|
-
work: Promise<T>,
|
|
4113
|
-
controller: AbortController,
|
|
4114
|
-
onTimeout: () => T,
|
|
4115
|
-
timeoutMs: number,
|
|
4116
|
-
): Promise<T> {
|
|
4117
|
-
let timer: ReturnType<typeof setTimeout> | undefined
|
|
4118
|
-
const timeout = new Promise<T>((resolve) => {
|
|
4119
|
-
timer = setTimeout(() => {
|
|
4120
|
-
controller.abort()
|
|
4121
|
-
resolve(onTimeout())
|
|
4122
|
-
}, timeoutMs)
|
|
4123
|
-
})
|
|
4124
|
-
try {
|
|
4125
|
-
return await Promise.race([work, timeout])
|
|
4126
|
-
} finally {
|
|
4127
|
-
if (timer !== undefined) clearTimeout(timer)
|
|
4128
|
-
}
|
|
4129
|
-
}
|
|
4130
|
-
|
|
4131
|
-
/**
|
|
4132
|
-
* The post-match lifecycle: validate → derive → beforeHandle → handler → afterHandle, with onError.
|
|
4133
|
-
* Generic over the render: a success value goes through `finalize(result, set)` (a Web `Response`, or
|
|
4134
|
-
* node-direct primitives); an early/error `Response` (validation, thrown, 500) through `wrapResponse`.
|
|
4135
|
-
*/
|
|
4136
|
-
/** Synchronous until a validator, lifecycle hook, handler, or contract check actually returns a Promise. */
|
|
4137
|
-
// @ts-expect-error TS6133 -- invoked structurally by compiled plans (internal/route-execution.ts)
|
|
4138
|
-
// biome-ignore lint/correctness/noUnusedPrivateClassMembers: invoked structurally by compiled plans (internal/route-execution.ts)
|
|
4139
|
-
private runLifecycle<T>(
|
|
4140
|
-
entry: RouteEntry,
|
|
4141
|
-
source: RequestSource,
|
|
4142
|
-
ctx: RawContext,
|
|
4143
|
-
finalize: (result: unknown, set: CtxSet) => T,
|
|
4144
|
-
wrapResponse: (response: Response | ResponseResult) => T,
|
|
4145
|
-
): MaybePromise<T> {
|
|
4146
|
-
try {
|
|
4147
|
-
const headersSchema = entry.schema?.headers
|
|
4148
|
-
if (headersSchema !== undefined) {
|
|
4149
|
-
const validation = headersSchema["~standard"].validate(headerObjectOf(source.headers))
|
|
4150
|
-
if (validation instanceof Promise) {
|
|
4151
|
-
return validation.then(
|
|
4152
|
-
(result) =>
|
|
4153
|
-
this.runLifecycleAfterHeadersResult(
|
|
4154
|
-
entry,
|
|
4155
|
-
source,
|
|
4156
|
-
ctx,
|
|
4157
|
-
finalize,
|
|
4158
|
-
wrapResponse,
|
|
4159
|
-
result,
|
|
4160
|
-
),
|
|
4161
|
-
(err) => this.handleLifecycleError(entry, err, ctx, finalize, wrapResponse),
|
|
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,
|
|
4162
4022
|
)
|
|
4163
4023
|
}
|
|
4164
|
-
|
|
4165
|
-
|
|
4166
|
-
|
|
4167
|
-
|
|
4168
|
-
|
|
4169
|
-
|
|
4170
|
-
|
|
4171
|
-
|
|
4172
|
-
|
|
4173
|
-
|
|
4174
|
-
|
|
4175
|
-
|
|
4176
|
-
|
|
4177
|
-
return
|
|
4178
|
-
(result) =>
|
|
4179
|
-
this.runLifecycleAfterParamsResult(
|
|
4180
|
-
entry,
|
|
4181
|
-
source,
|
|
4182
|
-
ctx,
|
|
4183
|
-
finalize,
|
|
4184
|
-
wrapResponse,
|
|
4185
|
-
result,
|
|
4186
|
-
),
|
|
4187
|
-
(err) => this.handleLifecycleError(entry, err, ctx, finalize, wrapResponse),
|
|
4188
|
-
)
|
|
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)
|
|
4189
4038
|
}
|
|
4190
|
-
|
|
4191
|
-
|
|
4192
|
-
|
|
4193
|
-
|
|
4194
|
-
|
|
4195
|
-
|
|
4196
|
-
|
|
4197
|
-
|
|
4198
|
-
|
|
4199
|
-
|
|
4200
|
-
|
|
4201
|
-
|
|
4202
|
-
|
|
4203
|
-
|
|
4204
|
-
|
|
4205
|
-
|
|
4206
|
-
|
|
4207
|
-
|
|
4208
|
-
|
|
4209
|
-
|
|
4210
|
-
|
|
4211
|
-
|
|
4212
|
-
|
|
4213
|
-
|
|
4214
|
-
|
|
4215
|
-
|
|
4216
|
-
|
|
4217
|
-
|
|
4218
|
-
|
|
4219
|
-
|
|
4220
|
-
|
|
4221
|
-
|
|
4222
|
-
|
|
4223
|
-
|
|
4224
|
-
|
|
4225
|
-
|
|
4226
|
-
|
|
4227
|
-
|
|
4228
|
-
|
|
4229
|
-
|
|
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),
|
|
4074
|
+
)
|
|
4075
|
+
}
|
|
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()
|
|
4230
4081
|
}
|
|
4231
|
-
|
|
4232
|
-
|
|
4233
|
-
|
|
4234
|
-
|
|
4235
|
-
|
|
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
|
+
}
|
|
4089
|
+
if (validation instanceof Promise) {
|
|
4090
|
+
return validation.then(
|
|
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),
|
|
4098
|
+
)
|
|
4099
|
+
}
|
|
4100
|
+
if (validation.issues !== undefined)
|
|
4101
|
+
return this.wrapWebResponse(plainValidationError(validation.issues))
|
|
4102
|
+
ctx.body = validation.value
|
|
4103
|
+
return runHandlerValidated()
|
|
4104
|
+
}
|
|
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>
|
|
4236
4113
|
}
|
|
4237
4114
|
}
|
|
4238
4115
|
|
|
4239
|
-
private
|
|
4240
|
-
|
|
4241
|
-
source: RequestSource,
|
|
4116
|
+
private bareError<T>(
|
|
4117
|
+
err: unknown,
|
|
4242
4118
|
ctx: RawContext,
|
|
4243
|
-
finalize: (result: unknown, set: CtxSet) => T,
|
|
4119
|
+
finalize: (result: unknown, set: CtxSet, ctx: RawContext) => T,
|
|
4244
4120
|
wrapResponse: (response: Response | ResponseResult) => T,
|
|
4245
|
-
):
|
|
4246
|
-
|
|
4247
|
-
|
|
4248
|
-
|
|
4249
|
-
|
|
4250
|
-
|
|
4251
|
-
}
|
|
4252
|
-
const validation = paramsSchema["~standard"].validate(ctx.params)
|
|
4253
|
-
if (validation instanceof Promise) {
|
|
4254
|
-
return validation.then(
|
|
4255
|
-
(result) =>
|
|
4256
|
-
this.runLifecycleAfterParamsResult(entry, source, ctx, finalize, wrapResponse, result),
|
|
4257
|
-
(err) => this.handleLifecycleError(entry, err, ctx, finalize, wrapResponse),
|
|
4258
|
-
)
|
|
4259
|
-
}
|
|
4260
|
-
return this.runLifecycleAfterParamsResult(
|
|
4261
|
-
entry,
|
|
4262
|
-
source,
|
|
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,
|
|
4263
4127
|
ctx,
|
|
4264
4128
|
finalize,
|
|
4265
4129
|
wrapResponse,
|
|
4266
|
-
|
|
4130
|
+
responseSet,
|
|
4131
|
+
(e, c) => this.logRequestError(e, c),
|
|
4132
|
+
() => this.internalErrorResponse(wrapResponse),
|
|
4267
4133
|
)
|
|
4268
4134
|
}
|
|
4269
4135
|
|
|
4270
|
-
/**
|
|
4271
|
-
|
|
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>(
|
|
4272
4146
|
entry: RouteEntry,
|
|
4273
4147
|
ctx: RawContext,
|
|
4148
|
+
run: () => MaybePromise<T>,
|
|
4274
4149
|
finalize: (result: unknown, set: CtxSet) => T,
|
|
4275
4150
|
wrapResponse: (response: Response | ResponseResult) => T,
|
|
4276
4151
|
): MaybePromise<T> {
|
|
4152
|
+
let outcome: MaybePromise<T>
|
|
4277
4153
|
try {
|
|
4278
|
-
|
|
4279
|
-
// non-null access here removes the per-request schema-presence branch from realistic GETs.
|
|
4280
|
-
const validation = entry.schema!.query!["~standard"].validate(
|
|
4281
|
-
queryObjectOf(ctx[CONTEXT_SEARCH]),
|
|
4282
|
-
)
|
|
4283
|
-
if (validation instanceof Promise) {
|
|
4284
|
-
return validation.then(
|
|
4285
|
-
(result) => {
|
|
4286
|
-
try {
|
|
4287
|
-
return this.runQueryAndLifecycleResult(entry, ctx, finalize, wrapResponse, result)
|
|
4288
|
-
} catch (err) {
|
|
4289
|
-
return this.handleLifecycleError(entry, err, ctx, finalize, wrapResponse)
|
|
4290
|
-
}
|
|
4291
|
-
},
|
|
4292
|
-
(err) => this.handleLifecycleError(entry, err, ctx, finalize, wrapResponse),
|
|
4293
|
-
)
|
|
4294
|
-
}
|
|
4295
|
-
return this.runQueryAndLifecycleResult(entry, ctx, finalize, wrapResponse, validation)
|
|
4154
|
+
outcome = this.runAround(entry.around, ctx, run)
|
|
4296
4155
|
} catch (err) {
|
|
4297
4156
|
return this.handleLifecycleError(entry, err, ctx, finalize, wrapResponse)
|
|
4298
4157
|
}
|
|
4158
|
+
return outcome instanceof Promise
|
|
4159
|
+
? outcome.catch((err) => this.handleLifecycleError(entry, err, ctx, finalize, wrapResponse))
|
|
4160
|
+
: outcome
|
|
4299
4161
|
}
|
|
4300
4162
|
|
|
4301
|
-
/**
|
|
4163
|
+
/** Execute the registration-compiled general stage program. */
|
|
4302
4164
|
// @ts-expect-error TS6133 -- invoked structurally by compiled plans (internal/route-execution.ts)
|
|
4303
|
-
|
|
4304
|
-
private runLifecycleBody<T>(
|
|
4165
|
+
private runProgram<T>(
|
|
4305
4166
|
entry: RouteEntry,
|
|
4306
4167
|
source: RequestSource,
|
|
4307
4168
|
ctx: RawContext,
|
|
4308
4169
|
finalize: (result: unknown, set: CtxSet) => T,
|
|
4309
4170
|
wrapResponse: (response: Response | ResponseResult) => T,
|
|
4310
4171
|
): MaybePromise<T> {
|
|
4311
|
-
return this.
|
|
4312
|
-
(bodyError) =>
|
|
4313
|
-
bodyError === undefined
|
|
4314
|
-
? this.runLifecycleHooks(entry, ctx, finalize, wrapResponse)
|
|
4315
|
-
: wrapResponse(bodyError),
|
|
4316
|
-
(err) => this.handleLifecycleError(entry, err, ctx, finalize, wrapResponse),
|
|
4317
|
-
)
|
|
4172
|
+
return executeRouteProgram(this, entry, entry.program, source, ctx, finalize, wrapResponse)
|
|
4318
4173
|
}
|
|
4319
4174
|
|
|
4320
|
-
/**
|
|
4321
|
-
// @ts-expect-error TS6133 -- invoked structurally by
|
|
4322
|
-
|
|
4323
|
-
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(
|
|
4324
4178
|
entry: RouteEntry,
|
|
4179
|
+
stage: Extract<RouteProgramStage, { kind: "headers" | "params" | "body" | "query" }>,
|
|
4325
4180
|
source: RequestSource,
|
|
4326
4181
|
ctx: RawContext,
|
|
4327
|
-
|
|
4328
|
-
|
|
4329
|
-
|
|
4330
|
-
|
|
4331
|
-
|
|
4332
|
-
|
|
4333
|
-
|
|
4334
|
-
|
|
4335
|
-
|
|
4336
|
-
)
|
|
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)
|
|
4337
4196
|
}
|
|
4338
4197
|
|
|
4339
|
-
/**
|
|
4340
|
-
private
|
|
4198
|
+
/** Preserve the existing bounded body reader and its fail-closed framing/prototype checks. */
|
|
4199
|
+
private readProgramBody(
|
|
4341
4200
|
entry: RouteEntry,
|
|
4201
|
+
source: RequestSource,
|
|
4342
4202
|
ctx: RawContext,
|
|
4343
|
-
|
|
4344
|
-
|
|
4345
|
-
|
|
4346
|
-
|
|
4347
|
-
return querySchema === undefined
|
|
4348
|
-
? this.runLifecycleHooks(entry, ctx, finalize, wrapResponse)
|
|
4349
|
-
: 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)
|
|
4350
4207
|
}
|
|
4351
4208
|
|
|
4352
|
-
|
|
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>(
|
|
4353
4212
|
entry: RouteEntry,
|
|
4213
|
+
error: unknown,
|
|
4354
4214
|
ctx: RawContext,
|
|
4355
4215
|
finalize: (result: unknown, set: CtxSet) => T,
|
|
4356
4216
|
wrapResponse: (response: Response | ResponseResult) => T,
|
|
4357
|
-
result: StandardResult<unknown>,
|
|
4358
4217
|
): MaybePromise<T> {
|
|
4359
|
-
|
|
4360
|
-
if (queryError instanceof Promise) {
|
|
4361
|
-
return queryError.then(
|
|
4362
|
-
(error) =>
|
|
4363
|
-
error === undefined
|
|
4364
|
-
? this.runLifecycleHooks(entry, ctx, finalize, wrapResponse)
|
|
4365
|
-
: wrapResponse(error),
|
|
4366
|
-
(err) => this.handleLifecycleError(entry, err, ctx, finalize, wrapResponse),
|
|
4367
|
-
)
|
|
4368
|
-
}
|
|
4369
|
-
if (queryError !== undefined) return wrapResponse(queryError)
|
|
4370
|
-
return this.runLifecycleHooks(entry, ctx, finalize, wrapResponse)
|
|
4218
|
+
return this.handleLifecycleError(entry, error, ctx, finalize, wrapResponse)
|
|
4371
4219
|
}
|
|
4372
4220
|
|
|
4373
|
-
|
|
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>(
|
|
4374
4224
|
entry: RouteEntry,
|
|
4375
|
-
source: RequestSource,
|
|
4376
4225
|
ctx: RawContext,
|
|
4226
|
+
result: unknown,
|
|
4377
4227
|
finalize: (result: unknown, set: CtxSet) => T,
|
|
4378
4228
|
wrapResponse: (response: Response | ResponseResult) => T,
|
|
4379
|
-
result: StandardResult<unknown>,
|
|
4380
4229
|
): MaybePromise<T> {
|
|
4381
|
-
|
|
4382
|
-
return this.runLifecycleAfterParams(
|
|
4383
|
-
entry,
|
|
4384
|
-
source,
|
|
4385
|
-
ctx,
|
|
4386
|
-
finalize,
|
|
4387
|
-
wrapResponse,
|
|
4388
|
-
this.applyLifecycleValidation(entry, result, ctx, "params"),
|
|
4389
|
-
)
|
|
4390
|
-
} catch (err) {
|
|
4391
|
-
return this.handleLifecycleError(entry, err, ctx, finalize, wrapResponse)
|
|
4392
|
-
}
|
|
4230
|
+
return this.finishLifecycleContract(entry, ctx, finalize, wrapResponse, result)
|
|
4393
4231
|
}
|
|
4394
4232
|
|
|
4395
|
-
private
|
|
4396
|
-
|
|
4397
|
-
source: RequestSource,
|
|
4233
|
+
private runAround<T>(
|
|
4234
|
+
hooks: ReadonlyArray<RawAround>,
|
|
4398
4235
|
ctx: RawContext,
|
|
4399
|
-
|
|
4400
|
-
wrapResponse: (response: Response | ResponseResult) => T,
|
|
4401
|
-
paramsError: MaybePromise<Response | ResponseResult | undefined>,
|
|
4236
|
+
run: () => MaybePromise<T>,
|
|
4402
4237
|
): MaybePromise<T> {
|
|
4403
|
-
|
|
4404
|
-
return
|
|
4405
|
-
|
|
4406
|
-
|
|
4407
|
-
|
|
4408
|
-
|
|
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
|
+
})
|
|
4409
4247
|
}
|
|
4410
|
-
return
|
|
4411
|
-
entry,
|
|
4412
|
-
source,
|
|
4413
|
-
ctx,
|
|
4414
|
-
finalize,
|
|
4415
|
-
wrapResponse,
|
|
4416
|
-
paramsError,
|
|
4417
|
-
)
|
|
4248
|
+
return dispatch(0)
|
|
4418
4249
|
}
|
|
4419
4250
|
|
|
4420
|
-
|
|
4251
|
+
// @ts-expect-error TS6133 -- invoked structurally by compiled plans (internal/route-execution.ts)
|
|
4252
|
+
private runBodyOnly<T>(
|
|
4421
4253
|
entry: RouteEntry,
|
|
4422
4254
|
source: RequestSource,
|
|
4423
4255
|
ctx: RawContext,
|
|
4424
4256
|
finalize: (result: unknown, set: CtxSet) => T,
|
|
4425
4257
|
wrapResponse: (response: Response | ResponseResult) => T,
|
|
4426
|
-
|
|
4427
|
-
|
|
4428
|
-
|
|
4429
|
-
|
|
4430
|
-
|
|
4431
|
-
|
|
4432
|
-
|
|
4433
|
-
)
|
|
4434
|
-
|
|
4435
|
-
return this.runLifecycleAfterBody(entry, ctx, finalize, wrapResponse, undefined)
|
|
4436
|
-
}
|
|
4437
|
-
|
|
4438
|
-
private runLifecycleAfterBody<T>(
|
|
4439
|
-
entry: RouteEntry,
|
|
4440
|
-
ctx: RawContext,
|
|
4441
|
-
finalize: (result: unknown, set: CtxSet) => T,
|
|
4442
|
-
wrapResponse: (response: Response | ResponseResult) => T,
|
|
4443
|
-
bodyError: Response | ResponseResult | undefined,
|
|
4444
|
-
): MaybePromise<T> {
|
|
4445
|
-
if (bodyError !== undefined) return wrapResponse(bodyError)
|
|
4446
|
-
const querySchema = entry.schema?.query
|
|
4447
|
-
if (querySchema === undefined) return this.runLifecycleHooks(entry, ctx, finalize, wrapResponse)
|
|
4448
|
-
const validation = querySchema["~standard"].validate(queryObjectOf(ctx[CONTEXT_SEARCH]))
|
|
4449
|
-
if (validation instanceof Promise) {
|
|
4450
|
-
return validation.then(
|
|
4451
|
-
(result) => this.runLifecycleAfterQueryResult(entry, ctx, finalize, wrapResponse, result),
|
|
4452
|
-
(err) => this.handleLifecycleError(entry, err, ctx, finalize, wrapResponse),
|
|
4453
|
-
)
|
|
4454
|
-
}
|
|
4455
|
-
return this.runLifecycleAfterQueryResult(entry, ctx, finalize, wrapResponse, validation)
|
|
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
|
+
)
|
|
4456
4267
|
}
|
|
4457
4268
|
|
|
4458
|
-
|
|
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>(
|
|
4459
4273
|
entry: RouteEntry,
|
|
4274
|
+
parsed: unknown,
|
|
4460
4275
|
ctx: RawContext,
|
|
4461
4276
|
finalize: (result: unknown, set: CtxSet) => T,
|
|
4462
4277
|
wrapResponse: (response: Response | ResponseResult) => T,
|
|
4463
|
-
result: StandardResult<unknown>,
|
|
4464
4278
|
): MaybePromise<T> {
|
|
4465
4279
|
try {
|
|
4466
|
-
const
|
|
4467
|
-
|
|
4468
|
-
|
|
4469
|
-
|
|
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
|
+
},
|
|
4470
4296
|
(err) => this.handleLifecycleError(entry, err, ctx, finalize, wrapResponse),
|
|
4471
4297
|
)
|
|
4472
4298
|
}
|
|
4473
|
-
|
|
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
|
|
4474
4303
|
} catch (err) {
|
|
4475
4304
|
return this.handleLifecycleError(entry, err, ctx, finalize, wrapResponse)
|
|
4476
4305
|
}
|
|
4477
4306
|
}
|
|
4478
4307
|
|
|
4479
|
-
private
|
|
4308
|
+
private executeHandler<T>(
|
|
4480
4309
|
entry: RouteEntry,
|
|
4481
4310
|
ctx: RawContext,
|
|
4482
4311
|
finalize: (result: unknown, set: CtxSet) => T,
|
|
4483
|
-
wrapResponse
|
|
4484
|
-
queryError: Response | ResponseResult | undefined,
|
|
4312
|
+
wrapResponse?: (response: Response | ResponseResult) => T,
|
|
4485
4313
|
): MaybePromise<T> {
|
|
4486
|
-
if (
|
|
4487
|
-
|
|
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))
|
|
4488
4328
|
}
|
|
4489
4329
|
|
|
4490
|
-
private
|
|
4330
|
+
private handleValidationErrorRecovery<T>(
|
|
4491
4331
|
entry: RouteEntry,
|
|
4332
|
+
recovery: unknown,
|
|
4492
4333
|
ctx: RawContext,
|
|
4493
4334
|
finalize: (result: unknown, set: CtxSet) => T,
|
|
4494
4335
|
wrapResponse: (response: Response | ResponseResult) => T,
|
|
4336
|
+
originalIssues: ReadonlyArray<StandardIssue>,
|
|
4337
|
+
kind: "body" | "query",
|
|
4495
4338
|
): MaybePromise<T> {
|
|
4496
|
-
if (
|
|
4497
|
-
|
|
4498
|
-
|
|
4499
|
-
|
|
4500
|
-
if (
|
|
4501
|
-
|
|
4502
|
-
|
|
4503
|
-
|
|
4504
|
-
|
|
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
|
+
})
|
|
4505
4352
|
}
|
|
4506
|
-
if (
|
|
4507
|
-
|
|
4353
|
+
if (validation.issues !== undefined)
|
|
4354
|
+
return wrapResponse(plainValidationError(validation.issues))
|
|
4355
|
+
ctx.body = validation.value
|
|
4356
|
+
return this.executeHandler(entry, ctx, finalize)
|
|
4508
4357
|
}
|
|
4509
|
-
|
|
4510
|
-
const
|
|
4511
|
-
if (
|
|
4512
|
-
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
|
+
)
|
|
4513
4370
|
}
|
|
4514
|
-
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)
|
|
4515
4375
|
}
|
|
4516
|
-
|
|
4517
|
-
|
|
4518
|
-
|
|
4519
|
-
|
|
4520
|
-
|
|
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",
|
|
4521
4412
|
)
|
|
4522
4413
|
}
|
|
4523
|
-
return
|
|
4524
|
-
} catch (err) {
|
|
4525
|
-
return this.handleLifecycleError(entry, err, ctx, finalize, wrapResponse)
|
|
4414
|
+
return wrapResponse(plainValidationError(result.issues))
|
|
4526
4415
|
}
|
|
4416
|
+
ctx.body = result.value
|
|
4417
|
+
return this.executeHandler(entry, ctx, finalize)
|
|
4527
4418
|
}
|
|
4528
4419
|
|
|
4529
|
-
|
|
4530
|
-
private
|
|
4420
|
+
// @ts-expect-error TS6133 -- invoked structurally by compiled plans (internal/route-execution.ts)
|
|
4421
|
+
private runQueryOnly<T>(
|
|
4531
4422
|
entry: RouteEntry,
|
|
4532
4423
|
ctx: RawContext,
|
|
4533
4424
|
finalize: (result: unknown, set: CtxSet) => T,
|
|
4534
4425
|
wrapResponse: (response: Response | ResponseResult) => T,
|
|
4535
4426
|
): MaybePromise<T> {
|
|
4536
4427
|
try {
|
|
4537
|
-
|
|
4538
|
-
|
|
4539
|
-
|
|
4540
|
-
|
|
4541
|
-
|
|
4542
|
-
|
|
4543
|
-
|
|
4544
|
-
|
|
4545
|
-
|
|
4546
|
-
|
|
4547
|
-
|
|
4548
|
-
|
|
4549
|
-
|
|
4550
|
-
|
|
4551
|
-
|
|
4552
|
-
|
|
4553
|
-
|
|
4554
|
-
|
|
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
|
+
},
|
|
4555
4447
|
(err) => this.handleLifecycleError(entry, err, ctx, finalize, wrapResponse),
|
|
4556
4448
|
)
|
|
4557
4449
|
}
|
|
4558
|
-
return this.
|
|
4450
|
+
return this.applyQueryValidation(entry, validation, ctx, finalize, wrapResponse)
|
|
4559
4451
|
} catch (err) {
|
|
4560
4452
|
return this.handleLifecycleError(entry, err, ctx, finalize, wrapResponse)
|
|
4561
4453
|
}
|
|
4562
4454
|
}
|
|
4563
4455
|
|
|
4564
|
-
/**
|
|
4565
|
-
|
|
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>(
|
|
4566
4459
|
entry: RouteEntry,
|
|
4460
|
+
result: StandardResult<unknown>,
|
|
4567
4461
|
ctx: RawContext,
|
|
4568
4462
|
finalize: (result: unknown, set: CtxSet) => T,
|
|
4569
4463
|
wrapResponse: (response: Response | ResponseResult) => T,
|
|
4570
|
-
result: unknown,
|
|
4571
4464
|
): MaybePromise<T> {
|
|
4572
|
-
|
|
4573
|
-
|
|
4574
|
-
|
|
4575
|
-
|
|
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))
|
|
4576
4493
|
}
|
|
4494
|
+
ctx.query = result.value
|
|
4495
|
+
return this.executeHandler(entry, ctx, finalize, wrapResponse)
|
|
4577
4496
|
}
|
|
4578
4497
|
|
|
4579
|
-
|
|
4580
|
-
|
|
4581
|
-
|
|
4582
|
-
|
|
4583
|
-
|
|
4584
|
-
|
|
4585
|
-
|
|
4586
|
-
):
|
|
4587
|
-
|
|
4588
|
-
|
|
4589
|
-
|
|
4590
|
-
|
|
4591
|
-
|
|
4592
|
-
|
|
4593
|
-
const extension = raw instanceof Promise ? await raw : raw
|
|
4594
|
-
if (isDeriveEarlyExit(extension)) return finalize(extension, responseSet(ctx))
|
|
4595
|
-
Object.assign(ctx, extension)
|
|
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))
|
|
4596
4512
|
}
|
|
4597
|
-
|
|
4598
|
-
} catch (err) {
|
|
4599
|
-
return this.handleLifecycleError(entry, err, ctx, finalize, wrapResponse)
|
|
4513
|
+
current = next
|
|
4600
4514
|
}
|
|
4515
|
+
return current
|
|
4601
4516
|
}
|
|
4602
4517
|
|
|
4603
|
-
private
|
|
4604
|
-
entry: RouteEntry,
|
|
4605
|
-
ctx: RawContext,
|
|
4606
|
-
finalize: (result: unknown, set: CtxSet) => T,
|
|
4607
|
-
wrapResponse: (response: Response | ResponseResult) => T,
|
|
4608
|
-
): Promise<T> {
|
|
4518
|
+
private applyOnResponseAndFinalize(response: Response, req: Request): MaybePromise<Response> {
|
|
4609
4519
|
try {
|
|
4610
|
-
|
|
4611
|
-
|
|
4612
|
-
|
|
4613
|
-
|
|
4614
|
-
|
|
4615
|
-
|
|
4616
|
-
|
|
4617
|
-
|
|
4618
|
-
|
|
4619
|
-
return await this.runLifecycleHandlerAsync(entry, ctx, finalize, wrapResponse)
|
|
4620
|
-
} catch (err) {
|
|
4621
|
-
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)
|
|
4622
4529
|
}
|
|
4623
4530
|
}
|
|
4624
4531
|
|
|
4625
|
-
private
|
|
4626
|
-
|
|
4627
|
-
|
|
4628
|
-
|
|
4629
|
-
|
|
4630
|
-
|
|
4631
|
-
first: Promise<unknown>,
|
|
4632
|
-
): Promise<T> {
|
|
4633
|
-
try {
|
|
4634
|
-
const firstOutcome = await first
|
|
4635
|
-
if (firstOutcome !== undefined) return finalize(firstOutcome, responseSet(ctx))
|
|
4636
|
-
for (let i = firstIndex + 1; i < entry.beforeHandle.length; i++) {
|
|
4637
|
-
const outcome = entry.beforeHandle[i]!(ctx)
|
|
4638
|
-
const early = outcome instanceof Promise ? await outcome : outcome
|
|
4639
|
-
if (early !== undefined) return finalize(early, responseSet(ctx))
|
|
4640
|
-
}
|
|
4641
|
-
return await this.runLifecycleHandlerAsync(entry, ctx, finalize, wrapResponse)
|
|
4642
|
-
} catch (err) {
|
|
4643
|
-
return this.handleLifecycleError(entry, err, ctx, finalize, wrapResponse)
|
|
4644
|
-
}
|
|
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
|
|
4645
4538
|
}
|
|
4646
4539
|
|
|
4647
|
-
private
|
|
4648
|
-
|
|
4649
|
-
|
|
4650
|
-
|
|
4651
|
-
|
|
4652
|
-
|
|
4653
|
-
|
|
4654
|
-
|
|
4655
|
-
|
|
4656
|
-
|
|
4657
|
-
} catch (err) {
|
|
4658
|
-
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
|
+
})
|
|
4659
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)
|
|
4660
4556
|
}
|
|
4661
4557
|
|
|
4662
|
-
|
|
4663
|
-
|
|
4664
|
-
|
|
4665
|
-
|
|
4666
|
-
|
|
4667
|
-
|
|
4668
|
-
|
|
4669
|
-
|
|
4670
|
-
|
|
4671
|
-
|
|
4672
|
-
|
|
4673
|
-
|
|
4674
|
-
|
|
4675
|
-
|
|
4676
|
-
|
|
4677
|
-
|
|
4678
|
-
|
|
4679
|
-
|
|
4680
|
-
|
|
4681
|
-
)
|
|
4682
|
-
}
|
|
4683
|
-
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.
|
|
4684
4577
|
}
|
|
4685
|
-
return this.finishLifecycleContract(entry, ctx, finalize, wrapResponse, result)
|
|
4686
|
-
} catch (err) {
|
|
4687
|
-
return this.handleLifecycleError(entry, err, ctx, finalize, wrapResponse)
|
|
4688
4578
|
}
|
|
4579
|
+
return pending
|
|
4689
4580
|
}
|
|
4690
4581
|
|
|
4691
|
-
|
|
4692
|
-
|
|
4693
|
-
|
|
4694
|
-
|
|
4695
|
-
|
|
4696
|
-
|
|
4697
|
-
|
|
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,
|
|
4698
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
|
+
})
|
|
4699
4614
|
try {
|
|
4700
|
-
|
|
4701
|
-
|
|
4702
|
-
|
|
4703
|
-
result = transformed instanceof Promise ? await transformed : transformed
|
|
4704
|
-
}
|
|
4705
|
-
return await this.finishLifecycleContract(entry, ctx, finalize, wrapResponse, result)
|
|
4706
|
-
} catch (err) {
|
|
4707
|
-
return this.handleLifecycleError(entry, err, ctx, finalize, wrapResponse)
|
|
4615
|
+
return await Promise.race([work, timeout])
|
|
4616
|
+
} finally {
|
|
4617
|
+
if (timer !== undefined) clearTimeout(timer)
|
|
4708
4618
|
}
|
|
4709
4619
|
}
|
|
4710
|
-
|
|
4711
4620
|
private finishLifecycleContract<T>(
|
|
4712
4621
|
entry: RouteEntry,
|
|
4713
4622
|
ctx: RawContext,
|
|
@@ -4808,35 +4717,37 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
4808
4717
|
return wrapResponse(plainError(500, "internal_error"))
|
|
4809
4718
|
}
|
|
4810
4719
|
|
|
4811
|
-
/** 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
|
|
4812
4721
|
* bare fast path ({@link bareError}) so both record the same fields. Never throws; never leaks. */
|
|
4813
4722
|
private logRequestError(err: unknown, ctx: RawContext): void {
|
|
4814
4723
|
emitRequestErrorLog(this.logger, this.errorLogDetail, err, ctx)
|
|
4815
4724
|
}
|
|
4816
4725
|
|
|
4817
|
-
private
|
|
4726
|
+
private readAndValidateBody(
|
|
4818
4727
|
req: RequestSource,
|
|
4819
4728
|
entry: RouteEntry,
|
|
4820
4729
|
ctx: RawContext,
|
|
4730
|
+
bodySchema: StandardSchemaV1 = entry.schema?.body as StandardSchemaV1,
|
|
4731
|
+
bodyLimit: number | undefined = entry.bodyLimit,
|
|
4821
4732
|
): Promise<Response | ResponseResult | undefined> {
|
|
4822
|
-
|
|
4823
|
-
|
|
4824
|
-
|
|
4825
|
-
|
|
4826
|
-
|
|
4827
|
-
|
|
4828
|
-
|
|
4829
|
-
|
|
4830
|
-
|
|
4831
|
-
parsed = form
|
|
4832
|
-
} else {
|
|
4833
|
-
// multipart/form-data (file uploads) stays 415 on the schema path by design - files don't
|
|
4834
|
-
// fit a value schema; use a schema-less route + @nifrajs/uploads helpers for those.
|
|
4835
|
-
return plainError(415, "unsupported_media_type")
|
|
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")
|
|
4836
4742
|
}
|
|
4837
|
-
|
|
4838
|
-
|
|
4839
|
-
|
|
4743
|
+
return readBodyFramed(
|
|
4744
|
+
req,
|
|
4745
|
+
bodyLimit ?? UNLIMITED_BODY_BYTES,
|
|
4746
|
+
this.protoPoisoning,
|
|
4747
|
+
validate,
|
|
4748
|
+
(response) => response,
|
|
4749
|
+
(error) => Promise.reject(error),
|
|
4750
|
+
)
|
|
4840
4751
|
}
|
|
4841
4752
|
|
|
4842
4753
|
/** Apply validation and its recovery hook on the generic lifecycle lane. Recovery is completed
|
|
@@ -4898,36 +4809,6 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
4898
4809
|
return undefined
|
|
4899
4810
|
}
|
|
4900
4811
|
|
|
4901
|
-
/**
|
|
4902
|
-
* Read + parse the JSON body, capped at `maxBodyBytes` and screened by the server's
|
|
4903
|
-
* `protoPoisoning` policy. Rejects a `Content-Length` over the cap *before* buffering,
|
|
4904
|
-
* re-checks what was actually delivered after the read, and aborts a chunked or
|
|
4905
|
-
* length-less body mid-stream once the running byte count exceeds the cap - so a lying
|
|
4906
|
-
* or absent length can't force us to buffer an oversized payload.
|
|
4907
|
-
*/
|
|
4908
|
-
private readBoundedJson(
|
|
4909
|
-
req: RequestSource,
|
|
4910
|
-
maxBodyBytes?: number,
|
|
4911
|
-
): Promise<unknown | ResponseResult>
|
|
4912
|
-
private readBoundedJson<T>(
|
|
4913
|
-
req: RequestSource,
|
|
4914
|
-
maxBodyBytes: number,
|
|
4915
|
-
onResult: (parsed: unknown | ResponseResult) => T | Promise<T>,
|
|
4916
|
-
onError: (error: unknown) => T | Promise<T>,
|
|
4917
|
-
): Promise<T>
|
|
4918
|
-
private readBoundedJson<T>(
|
|
4919
|
-
req: RequestSource,
|
|
4920
|
-
maxBodyBytes = this.maxBodyBytes,
|
|
4921
|
-
onResult?: (parsed: unknown | ResponseResult) => T | Promise<T>,
|
|
4922
|
-
onError?: (error: unknown) => T | Promise<T>,
|
|
4923
|
-
): Promise<unknown | ResponseResult | T> {
|
|
4924
|
-
// Not `async`: this is a pass-through, and wrapping the lane's own promise in a coroutine
|
|
4925
|
-
// frame costs an extra microtask hop on every JSON body.
|
|
4926
|
-
return onResult === undefined
|
|
4927
|
-
? readBoundedJsonSource(req, maxBodyBytes, this.protoPoisoning)
|
|
4928
|
-
: readBoundedJsonSource(req, maxBodyBytes, this.protoPoisoning, onResult, onError!)
|
|
4929
|
-
}
|
|
4930
|
-
|
|
4931
4812
|
/** Adapt one route's compiled execution plan to Bun's already-matched request shape. Route
|
|
4932
4813
|
* semantics remain in the plan; this closure only supplies native params and deadline fallback. */
|
|
4933
4814
|
private compileBunNativeHandler(
|
|
@@ -5115,10 +4996,13 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
5115
4996
|
// matched route's handler, set by `server.upgrade`.
|
|
5116
4997
|
// With WS routes, the dispatcher comes from the installed `.use(websocket())` runtime - non-null
|
|
5117
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
|
|
5118
5002
|
const wsHandlers =
|
|
5119
5003
|
this.wsRouteCount === 0
|
|
5120
5004
|
? undefined
|
|
5121
|
-
: (this.wsRuntime as WsRuntime).bunHandlers(this.topics as TopicRegistry)
|
|
5005
|
+
: (this.wsRuntime as WsRuntime).bunHandlers(this.topics as TopicRegistry, nativePubsub)
|
|
5122
5006
|
const reusePort = options?.reusePort === true
|
|
5123
5007
|
// Spread rather than pass `hostname: undefined` - Bun treats an explicit undefined as a value
|
|
5124
5008
|
// on some option paths, and omitting is what selects its 0.0.0.0 default.
|
|
@@ -5156,6 +5040,14 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
5156
5040
|
},
|
|
5157
5041
|
})) as unknown as RunningServer
|
|
5158
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
|
+
}
|
|
5159
5051
|
this.sealed = true
|
|
5160
5052
|
if (this.gracefulSignals) this.installSignalHandlers()
|
|
5161
5053
|
return running
|