@nifrajs/core 3.1.0 → 3.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +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
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Registration-time route compilation.
|
|
3
|
+
*
|
|
4
|
+
* This module owns the policy decisions that turn a route declaration into the immutable facts the
|
|
5
|
+
* execution-plan builder and the route catalog consume. It deliberately runs only from `Server.register`
|
|
6
|
+
* and `Server.registerBatch`; no function here is reachable from the request path.
|
|
7
|
+
*
|
|
8
|
+
* The public Server remains the fluent registration surface. This module is the deep implementation seam
|
|
9
|
+
* behind that surface: body limits, capability and assurance evidence, optional runtimes, decorations, and
|
|
10
|
+
* lane selection are resolved once, together, so adding a new route policy does not require editing the
|
|
11
|
+
* kernel's dispatch code.
|
|
12
|
+
*/
|
|
13
|
+
import type { EffectLifecycleObserver } from "../effect-lifecycle.ts"
|
|
14
|
+
import { RouteConfigError } from "../errors.ts"
|
|
15
|
+
import { type CompiledRoutePattern, compileRoutePattern } from "../router/pattern.ts"
|
|
16
|
+
import type { Method } from "../router/router.ts"
|
|
17
|
+
import type { StandardSchemaV1 } from "../schema/standard.ts"
|
|
18
|
+
import { assertByteLimit } from "../server/body.ts"
|
|
19
|
+
import type { RouteSchema } from "../server/context.ts"
|
|
20
|
+
import type { IdempotencyRuntime, ResolvedIdempotency } from "../server/idempotency-lane.ts"
|
|
21
|
+
import type { EffectLedgerRuntime, ResolvedEffectLedger } from "../server/ledger-lane.ts"
|
|
22
|
+
import type { ResponseContractRuntime } from "../server/response-contract-lane.ts"
|
|
23
|
+
import {
|
|
24
|
+
CAPABILITY_GUARD,
|
|
25
|
+
type CapabilityUseEvent,
|
|
26
|
+
createCapabilityGuard,
|
|
27
|
+
normalizeRouteCapabilities,
|
|
28
|
+
type RegisteredCapabilityInterceptor,
|
|
29
|
+
} from "./capability-runtime.ts"
|
|
30
|
+
import {
|
|
31
|
+
type AssuranceDeclaration,
|
|
32
|
+
assuranceDeclarationsOf,
|
|
33
|
+
assuranceEvidenceFor,
|
|
34
|
+
NIFRA_ASSURANCE_IDS,
|
|
35
|
+
validEvidenceId,
|
|
36
|
+
} from "./route-assurance.ts"
|
|
37
|
+
import type {
|
|
38
|
+
RawAfterHandle,
|
|
39
|
+
RawAround,
|
|
40
|
+
RawBeforeHandle,
|
|
41
|
+
RawDerive,
|
|
42
|
+
RawErrorHandler,
|
|
43
|
+
} from "./route-execution.ts"
|
|
44
|
+
import { type RouteLaneSelection, selectRouteLanes } from "./route-lanes.ts"
|
|
45
|
+
|
|
46
|
+
export interface RouteCompilerContext {
|
|
47
|
+
readonly maxBodyBytes: number
|
|
48
|
+
readonly activeAssurance: readonly AssuranceDeclaration[]
|
|
49
|
+
readonly globalAssurance: readonly AssuranceDeclaration[]
|
|
50
|
+
readonly decorations: Record<string, unknown>
|
|
51
|
+
readonly onCapabilityUse: ((event: CapabilityUseEvent) => void) | undefined
|
|
52
|
+
readonly capabilityInterceptors: readonly RegisteredCapabilityInterceptor[]
|
|
53
|
+
readonly capabilityObservers: readonly EffectLifecycleObserver[]
|
|
54
|
+
readonly effectLedgerRuntime: EffectLedgerRuntime | undefined
|
|
55
|
+
readonly idempotencyRuntime: IdempotencyRuntime | undefined
|
|
56
|
+
readonly responseContractRuntime: ResponseContractRuntime | undefined
|
|
57
|
+
readonly derives: readonly RawDerive[]
|
|
58
|
+
readonly beforeHandleHooks: readonly RawBeforeHandle[]
|
|
59
|
+
readonly afterHandleHooks: readonly RawAfterHandle[]
|
|
60
|
+
readonly onErrorHooks: readonly RawErrorHandler[]
|
|
61
|
+
readonly aroundHooks: readonly RawAround[]
|
|
62
|
+
readonly defaultOnValidationError: RouteSchema["onValidationError"] | undefined
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface CompiledRouteOptions {
|
|
66
|
+
readonly pattern: CompiledRoutePattern
|
|
67
|
+
readonly bodyLimit: number | undefined
|
|
68
|
+
readonly capabilities: readonly string[]
|
|
69
|
+
readonly handlerAssurance: readonly AssuranceDeclaration[]
|
|
70
|
+
readonly routeDecorations: Record<PropertyKey, unknown>
|
|
71
|
+
readonly hasDecorations: boolean
|
|
72
|
+
readonly idempotent: ResolvedIdempotency | undefined
|
|
73
|
+
readonly ledgered: ResolvedEffectLedger | undefined
|
|
74
|
+
readonly responseContract:
|
|
75
|
+
| { readonly runtime: ResponseContractRuntime; readonly schema: StandardSchemaV1 }
|
|
76
|
+
| undefined
|
|
77
|
+
readonly lanes: RouteLaneSelection
|
|
78
|
+
readonly routeAssurance: readonly AssuranceDeclaration[]
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Resolve all registration-time policy and lane facts for one route. */
|
|
82
|
+
export function compileRouteOptions(
|
|
83
|
+
context: RouteCompilerContext,
|
|
84
|
+
method: Method,
|
|
85
|
+
path: string,
|
|
86
|
+
schema: RouteSchema | undefined,
|
|
87
|
+
handler: (context: never) => unknown,
|
|
88
|
+
): CompiledRouteOptions {
|
|
89
|
+
const pattern = compileRoutePattern(path)
|
|
90
|
+
let bodyLimit: number | undefined = context.maxBodyBytes
|
|
91
|
+
const invalidBodyLimit = (message: string): never => {
|
|
92
|
+
throw new RouteConfigError("INVALID_BODY_LIMIT", `route ${method} ${path}: ${message}`)
|
|
93
|
+
}
|
|
94
|
+
if (schema?.bodyLimitReason !== undefined && schema.bodyLimit !== "unlimited") {
|
|
95
|
+
invalidBodyLimit('bodyLimitReason is only valid with bodyLimit: "unlimited"')
|
|
96
|
+
}
|
|
97
|
+
if (schema?.bodyLimit === "unlimited") {
|
|
98
|
+
if (typeof schema.bodyLimitReason !== "string" || schema.bodyLimitReason.trim().length === 0) {
|
|
99
|
+
invalidBodyLimit('bodyLimit: "unlimited" requires a non-empty bodyLimitReason')
|
|
100
|
+
}
|
|
101
|
+
if (schema.body !== undefined) {
|
|
102
|
+
invalidBodyLimit('bodyLimit: "unlimited" cannot be used with a body schema; use a finite cap')
|
|
103
|
+
}
|
|
104
|
+
if (schema.idempotency !== undefined) {
|
|
105
|
+
invalidBodyLimit('bodyLimit: "unlimited" cannot be used with idempotency')
|
|
106
|
+
}
|
|
107
|
+
bodyLimit = undefined
|
|
108
|
+
} else if (schema?.bodyLimit !== undefined) {
|
|
109
|
+
try {
|
|
110
|
+
assertByteLimit(schema.bodyLimit, "route bodyLimit")
|
|
111
|
+
} catch {
|
|
112
|
+
invalidBodyLimit('bodyLimit must be a non-negative safe integer or "unlimited"')
|
|
113
|
+
}
|
|
114
|
+
bodyLimit = schema.bodyLimit
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const capabilities = normalizeRouteCapabilities(schema?.capabilities)
|
|
118
|
+
const handlerAssurance = assuranceDeclarationsOf(handler as unknown as object)
|
|
119
|
+
const invalidHandlerScope = handlerAssurance.find((declaration) => declaration.scope !== "plugin")
|
|
120
|
+
if (invalidHandlerScope !== undefined) {
|
|
121
|
+
throw new RouteConfigError(
|
|
122
|
+
"INVALID_ASSURANCE",
|
|
123
|
+
`route handler assurance must use plugin scope (received ${invalidHandlerScope.scope})`,
|
|
124
|
+
)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const authenticated = assuranceEvidenceFor(
|
|
128
|
+
[...context.activeAssurance, ...handlerAssurance, ...context.globalAssurance],
|
|
129
|
+
method,
|
|
130
|
+
path,
|
|
131
|
+
).some((evidence) => evidence.id === NIFRA_ASSURANCE_IDS.AUTHENTICATED)
|
|
132
|
+
const routeDecorations: Record<PropertyKey, unknown> = { ...context.decorations }
|
|
133
|
+
if (capabilities.length > 0) {
|
|
134
|
+
routeDecorations[CAPABILITY_GUARD] = createCapabilityGuard(
|
|
135
|
+
capabilities,
|
|
136
|
+
method,
|
|
137
|
+
path,
|
|
138
|
+
context.onCapabilityUse,
|
|
139
|
+
context.idempotencyRuntime?.trackEffect,
|
|
140
|
+
Object.freeze([...context.capabilityInterceptors]),
|
|
141
|
+
Object.freeze([...context.capabilityObservers]),
|
|
142
|
+
)
|
|
143
|
+
}
|
|
144
|
+
const hasDecorations = Reflect.ownKeys(routeDecorations).length > 0
|
|
145
|
+
|
|
146
|
+
if (schema?.idempotency !== undefined && context.idempotencyRuntime === undefined) {
|
|
147
|
+
throw new RouteConfigError(
|
|
148
|
+
"INVALID_IDEMPOTENCY",
|
|
149
|
+
"route declares idempotency but the idempotency plugin is not installed; add .use(idempotency())",
|
|
150
|
+
)
|
|
151
|
+
}
|
|
152
|
+
const idempotent = context.idempotencyRuntime?.resolve(
|
|
153
|
+
schema,
|
|
154
|
+
authenticated,
|
|
155
|
+
context.maxBodyBytes,
|
|
156
|
+
)
|
|
157
|
+
const ledgered = context.effectLedgerRuntime?.resolve(capabilities, method, path)
|
|
158
|
+
const contracted =
|
|
159
|
+
context.responseContractRuntime !== undefined && schema?.response !== undefined
|
|
160
|
+
? { runtime: context.responseContractRuntime, schema: schema.response }
|
|
161
|
+
: undefined
|
|
162
|
+
const lanes = selectRouteLanes({
|
|
163
|
+
schema,
|
|
164
|
+
hasIdempotency: idempotent !== undefined,
|
|
165
|
+
hasLedger: ledgered !== undefined,
|
|
166
|
+
hasResponseContract: contracted !== undefined,
|
|
167
|
+
hasDecorations,
|
|
168
|
+
derives: context.derives.length,
|
|
169
|
+
beforeHandle: context.beforeHandleHooks.length,
|
|
170
|
+
afterHandle: context.afterHandleHooks.length,
|
|
171
|
+
onError: context.onErrorHooks.length,
|
|
172
|
+
around: context.aroundHooks.length,
|
|
173
|
+
defaultOnValidationError: context.defaultOnValidationError !== undefined,
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
const routeAssurance: AssuranceDeclaration[] = [...context.activeAssurance, ...handlerAssurance]
|
|
177
|
+
if (contracted?.runtime.mode === "enforce" && schema?.response !== undefined) {
|
|
178
|
+
routeAssurance.push(
|
|
179
|
+
Object.freeze({
|
|
180
|
+
id: NIFRA_ASSURANCE_IDS.RESPONSE_CONTRACT,
|
|
181
|
+
source: "response-contract",
|
|
182
|
+
scope: "plugin",
|
|
183
|
+
}),
|
|
184
|
+
)
|
|
185
|
+
}
|
|
186
|
+
for (const id of schema?.assurance ?? []) {
|
|
187
|
+
if (!validEvidenceId(id)) {
|
|
188
|
+
throw new Error(
|
|
189
|
+
`route assurance: invalid evidence id ${JSON.stringify(id)} on ${method} ${path} (use lowercase dot/dash segments)`,
|
|
190
|
+
)
|
|
191
|
+
}
|
|
192
|
+
const declared = { id, source: "declared", scope: "plugin" } as AssuranceDeclaration
|
|
193
|
+
Object.defineProperty(declared, "provenance", {
|
|
194
|
+
value: "declared",
|
|
195
|
+
enumerable: false,
|
|
196
|
+
configurable: false,
|
|
197
|
+
writable: false,
|
|
198
|
+
})
|
|
199
|
+
routeAssurance.push(Object.freeze(declared))
|
|
200
|
+
}
|
|
201
|
+
const unlimitedBody = schema?.bodyLimit === "unlimited"
|
|
202
|
+
if (!unlimitedBody && (schema?.body !== undefined || typeof schema?.bodyLimit === "number")) {
|
|
203
|
+
routeAssurance.push(
|
|
204
|
+
Object.freeze({
|
|
205
|
+
id: NIFRA_ASSURANCE_IDS.BODY_BOUNDED,
|
|
206
|
+
source: "route-schema",
|
|
207
|
+
scope: "plugin",
|
|
208
|
+
}),
|
|
209
|
+
)
|
|
210
|
+
}
|
|
211
|
+
if (schema?.idempotency !== undefined) {
|
|
212
|
+
routeAssurance.push(
|
|
213
|
+
Object.freeze({
|
|
214
|
+
id: NIFRA_ASSURANCE_IDS.IDEMPOTENCY_KEY,
|
|
215
|
+
source: "route-schema",
|
|
216
|
+
scope: "plugin",
|
|
217
|
+
}),
|
|
218
|
+
)
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return {
|
|
222
|
+
pattern,
|
|
223
|
+
bodyLimit,
|
|
224
|
+
capabilities,
|
|
225
|
+
handlerAssurance,
|
|
226
|
+
routeDecorations,
|
|
227
|
+
hasDecorations,
|
|
228
|
+
idempotent,
|
|
229
|
+
ledgered,
|
|
230
|
+
responseContract: contracted,
|
|
231
|
+
lanes,
|
|
232
|
+
routeAssurance: Object.freeze(routeAssurance),
|
|
233
|
+
}
|
|
234
|
+
}
|
|
@@ -29,6 +29,7 @@ import { RequestContext } from "../server/request-context.ts"
|
|
|
29
29
|
import type { ResponseContractRuntime } from "../server/response-contract-lane.ts"
|
|
30
30
|
import type { HandlerResult, ResponseResult } from "../server/runtime-core.ts"
|
|
31
31
|
import type { CtxSet, MaybePromise, RawContext, RequestSource, Server } from "../server/server.ts"
|
|
32
|
+
import type { RouteProgram } from "./route-program.ts"
|
|
32
33
|
|
|
33
34
|
export type InternalHandler = (ctx: RawContext) => MaybePromise<HandlerResult>
|
|
34
35
|
|
|
@@ -98,33 +99,7 @@ interface RouteExecutionRuntime {
|
|
|
98
99
|
finalize: (result: unknown, set: CtxSet) => T,
|
|
99
100
|
wrapResponse: (response: Response | ResponseResult) => T,
|
|
100
101
|
): MaybePromise<T>
|
|
101
|
-
|
|
102
|
-
entry: RouteEntry,
|
|
103
|
-
ctx: RawContext,
|
|
104
|
-
finalize: (result: unknown, set: CtxSet) => T,
|
|
105
|
-
wrapResponse: (response: Response | ResponseResult) => T,
|
|
106
|
-
): MaybePromise<T>
|
|
107
|
-
runLifecycleQuery<T>(
|
|
108
|
-
entry: RouteEntry,
|
|
109
|
-
ctx: RawContext,
|
|
110
|
-
finalize: (result: unknown, set: CtxSet) => T,
|
|
111
|
-
wrapResponse: (response: Response | ResponseResult) => T,
|
|
112
|
-
): MaybePromise<T>
|
|
113
|
-
runLifecycleBody<T>(
|
|
114
|
-
entry: RouteEntry,
|
|
115
|
-
source: RequestSource,
|
|
116
|
-
ctx: RawContext,
|
|
117
|
-
finalize: (result: unknown, set: CtxSet) => T,
|
|
118
|
-
wrapResponse: (response: Response | ResponseResult) => T,
|
|
119
|
-
): MaybePromise<T>
|
|
120
|
-
runLifecycleBodyQuery<T>(
|
|
121
|
-
entry: RouteEntry,
|
|
122
|
-
source: RequestSource,
|
|
123
|
-
ctx: RawContext,
|
|
124
|
-
finalize: (result: unknown, set: CtxSet) => T,
|
|
125
|
-
wrapResponse: (response: Response | ResponseResult) => T,
|
|
126
|
-
): MaybePromise<T>
|
|
127
|
-
runLifecycle<T>(
|
|
102
|
+
runProgram<T>(
|
|
128
103
|
entry: RouteEntry,
|
|
129
104
|
source: RequestSource,
|
|
130
105
|
ctx: RawContext,
|
|
@@ -158,13 +133,19 @@ export function compileRouteExecutionPlan(options: {
|
|
|
158
133
|
readonly contextless: boolean
|
|
159
134
|
readonly hasAround: boolean
|
|
160
135
|
readonly hasLedger: boolean
|
|
161
|
-
readonly lifecycleLane: LifecycleExecutionLane
|
|
162
136
|
readonly fusedWeb: FusedWebRunner | undefined
|
|
163
137
|
readonly fusedBody: FusedBodyRunner | undefined
|
|
164
|
-
readonly fusedLane:
|
|
138
|
+
readonly fusedLane:
|
|
139
|
+
| "bare"
|
|
140
|
+
| "body"
|
|
141
|
+
| "query"
|
|
142
|
+
| "derive-before"
|
|
143
|
+
| "derive-before-after"
|
|
144
|
+
| "body-derive-before"
|
|
145
|
+
| "body-derive-before-after"
|
|
146
|
+
| undefined
|
|
165
147
|
}): RouteExecutionPlan {
|
|
166
|
-
const { lane, contextless, hasAround, hasLedger,
|
|
167
|
-
options
|
|
148
|
+
const { lane, contextless, hasAround, hasLedger, fusedWeb, fusedBody, fusedLane } = options
|
|
168
149
|
|
|
169
150
|
if (contextless) {
|
|
170
151
|
const run: RouteExecutionRunner = (
|
|
@@ -191,7 +172,7 @@ export function compileRouteExecutionPlan(options: {
|
|
|
191
172
|
finalize,
|
|
192
173
|
wrapResponse,
|
|
193
174
|
)
|
|
194
|
-
return Object.freeze({ run, fusedWeb, fusedBody, fusedLane })
|
|
175
|
+
return Object.freeze({ run, fusedWeb, fusedBody, fusedLane }) as RouteExecutionPlan
|
|
195
176
|
}
|
|
196
177
|
|
|
197
178
|
let inner: ContextRouteRunner
|
|
@@ -220,55 +201,14 @@ export function compileRouteExecutionPlan(options: {
|
|
|
220
201
|
)
|
|
221
202
|
break
|
|
222
203
|
default:
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
)
|
|
232
|
-
break
|
|
233
|
-
case "query":
|
|
234
|
-
inner = (runtime, entry, _source, ctx, finalize, wrapResponse) =>
|
|
235
|
-
(runtime as unknown as RouteExecutionRuntime).runLifecycleQuery(
|
|
236
|
-
entry,
|
|
237
|
-
ctx,
|
|
238
|
-
finalize,
|
|
239
|
-
wrapResponse,
|
|
240
|
-
)
|
|
241
|
-
break
|
|
242
|
-
case "body":
|
|
243
|
-
inner = (runtime, entry, source, ctx, finalize, wrapResponse) =>
|
|
244
|
-
(runtime as unknown as RouteExecutionRuntime).runLifecycleBody(
|
|
245
|
-
entry,
|
|
246
|
-
source,
|
|
247
|
-
ctx,
|
|
248
|
-
finalize,
|
|
249
|
-
wrapResponse,
|
|
250
|
-
)
|
|
251
|
-
break
|
|
252
|
-
case "body-query":
|
|
253
|
-
inner = (runtime, entry, source, ctx, finalize, wrapResponse) =>
|
|
254
|
-
(runtime as unknown as RouteExecutionRuntime).runLifecycleBodyQuery(
|
|
255
|
-
entry,
|
|
256
|
-
source,
|
|
257
|
-
ctx,
|
|
258
|
-
finalize,
|
|
259
|
-
wrapResponse,
|
|
260
|
-
)
|
|
261
|
-
break
|
|
262
|
-
default:
|
|
263
|
-
inner = (runtime, entry, source, ctx, finalize, wrapResponse) =>
|
|
264
|
-
(runtime as unknown as RouteExecutionRuntime).runLifecycle(
|
|
265
|
-
entry,
|
|
266
|
-
source,
|
|
267
|
-
ctx,
|
|
268
|
-
finalize,
|
|
269
|
-
wrapResponse,
|
|
270
|
-
)
|
|
271
|
-
}
|
|
204
|
+
inner = (runtime, entry, source, ctx, finalize, wrapResponse) =>
|
|
205
|
+
(runtime as unknown as RouteExecutionRuntime).runProgram(
|
|
206
|
+
entry,
|
|
207
|
+
source,
|
|
208
|
+
ctx,
|
|
209
|
+
finalize,
|
|
210
|
+
wrapResponse,
|
|
211
|
+
)
|
|
272
212
|
}
|
|
273
213
|
|
|
274
214
|
const execute: ContextRouteRunner = hasAround
|
|
@@ -329,7 +269,7 @@ export function compileRouteExecutionPlan(options: {
|
|
|
329
269
|
}
|
|
330
270
|
return outcome
|
|
331
271
|
}
|
|
332
|
-
return Object.freeze({ run, fusedWeb, fusedBody, fusedLane })
|
|
272
|
+
return Object.freeze({ run, fusedWeb, fusedBody, fusedLane }) as RouteExecutionPlan
|
|
333
273
|
}
|
|
334
274
|
|
|
335
275
|
/** Registration-compiled route behavior. Every adapter invokes the same runner; the optional fused
|
|
@@ -341,7 +281,15 @@ export interface RouteExecutionPlan {
|
|
|
341
281
|
/** Which builder produced {@link fusedWeb} - a merge rebinds the closure to the executing server
|
|
342
282
|
* and must rebuild it with the SAME semantics (a query-fused route rebuilt as bare would skip its
|
|
343
283
|
* validation). `undefined` iff `fusedWeb` is. */
|
|
344
|
-
readonly fusedLane:
|
|
284
|
+
readonly fusedLane:
|
|
285
|
+
| "bare"
|
|
286
|
+
| "body"
|
|
287
|
+
| "query"
|
|
288
|
+
| "derive-before"
|
|
289
|
+
| "derive-before-after"
|
|
290
|
+
| "body-derive-before"
|
|
291
|
+
| "body-derive-before-after"
|
|
292
|
+
| undefined
|
|
345
293
|
}
|
|
346
294
|
|
|
347
295
|
export interface RouteEntry {
|
|
@@ -377,11 +325,13 @@ export interface RouteEntry {
|
|
|
377
325
|
readonly afterHandle: ReadonlyArray<RawAfterHandle>
|
|
378
326
|
readonly onError: ReadonlyArray<RawErrorHandler>
|
|
379
327
|
/** Registration-specialized hook shape for the common derive + before middleware route. */
|
|
380
|
-
readonly lifecycleHookLane: "derive-before" | undefined
|
|
328
|
+
readonly lifecycleHookLane: "derive-before" | "derive-before-after" | undefined
|
|
381
329
|
/** Wraps the matched route lifecycle. Empty for the common no-around path. */
|
|
382
330
|
readonly around: ReadonlyArray<RawAround>
|
|
383
331
|
/** The single immutable execution decision consumed by portable, Node-direct, and Bun-native paths. */
|
|
384
332
|
readonly execution: RouteExecutionPlan
|
|
333
|
+
/** One immutable general lifecycle program compiled at registration. */
|
|
334
|
+
readonly program: RouteProgram
|
|
385
335
|
}
|
|
386
336
|
|
|
387
337
|
/** The fused Web lane: same inputs `routeAndRun` would hand the generic path, a `Response` out. */
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Registration-time route-lane selection.
|
|
3
|
+
*
|
|
4
|
+
* A route's lane is a property of its registered shape, not of an individual request. Keeping this
|
|
5
|
+
* decision in a small internal module gives the server kernel one vocabulary for bare/body/query/
|
|
6
|
+
* lifecycle execution while preserving the important invariant: this module is never called from the
|
|
7
|
+
* request path. The returned booleans are consumed by the existing compiled execution plan and fused
|
|
8
|
+
* builders; no adapter or extra dispatch hop is introduced at runtime.
|
|
9
|
+
*/
|
|
10
|
+
import type { RouteSchema } from "../server/context.ts"
|
|
11
|
+
import type { LifecycleExecutionLane, RouteExecutionLane } from "./route-execution.ts"
|
|
12
|
+
|
|
13
|
+
export interface RouteLaneSelection {
|
|
14
|
+
readonly bare: boolean
|
|
15
|
+
readonly fusedQuery: boolean
|
|
16
|
+
readonly bodyOnly: boolean
|
|
17
|
+
readonly fusedBody: boolean
|
|
18
|
+
readonly lane: RouteExecutionLane
|
|
19
|
+
readonly lifecycleLane: LifecycleExecutionLane
|
|
20
|
+
readonly lifecycleHookLane: "derive-before" | "derive-before-after" | undefined
|
|
21
|
+
readonly fusedLane:
|
|
22
|
+
| "bare"
|
|
23
|
+
| "body"
|
|
24
|
+
| "query"
|
|
25
|
+
| "derive-before"
|
|
26
|
+
| "derive-before-after"
|
|
27
|
+
| "body-derive-before"
|
|
28
|
+
| "body-derive-before-after"
|
|
29
|
+
| undefined
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function selectRouteLanes(options: {
|
|
33
|
+
readonly schema: RouteSchema | undefined
|
|
34
|
+
readonly hasIdempotency: boolean
|
|
35
|
+
readonly hasLedger: boolean
|
|
36
|
+
readonly hasResponseContract: boolean
|
|
37
|
+
readonly hasDecorations: boolean
|
|
38
|
+
readonly derives: number
|
|
39
|
+
readonly beforeHandle: number
|
|
40
|
+
readonly afterHandle: number
|
|
41
|
+
readonly onError: number
|
|
42
|
+
readonly around: number
|
|
43
|
+
readonly defaultOnValidationError: boolean
|
|
44
|
+
}): RouteLaneSelection {
|
|
45
|
+
const {
|
|
46
|
+
schema,
|
|
47
|
+
hasIdempotency,
|
|
48
|
+
hasLedger,
|
|
49
|
+
hasResponseContract,
|
|
50
|
+
hasDecorations,
|
|
51
|
+
derives,
|
|
52
|
+
beforeHandle,
|
|
53
|
+
afterHandle,
|
|
54
|
+
onError,
|
|
55
|
+
around,
|
|
56
|
+
defaultOnValidationError,
|
|
57
|
+
} = options
|
|
58
|
+
|
|
59
|
+
const bare =
|
|
60
|
+
schema?.params === undefined &&
|
|
61
|
+
schema?.headers === undefined &&
|
|
62
|
+
schema?.body === undefined &&
|
|
63
|
+
schema?.query === undefined &&
|
|
64
|
+
!hasIdempotency &&
|
|
65
|
+
!hasLedger &&
|
|
66
|
+
!hasResponseContract &&
|
|
67
|
+
derives === 0 &&
|
|
68
|
+
beforeHandle === 0 &&
|
|
69
|
+
afterHandle === 0 &&
|
|
70
|
+
onError === 0
|
|
71
|
+
|
|
72
|
+
// A route whose ONLY lifecycle step is a query schema can fuse too: the parse + validate + handler +
|
|
73
|
+
// respond collapse into one closure with no lifecycle promise on the sync path. The guards mirror the
|
|
74
|
+
// query lane below PLUS everything the fused dispatch skips: around hooks, the idempotency/ledger
|
|
75
|
+
// wrappers, and validation-error recovery (schema or server default) - the fused invalid path is
|
|
76
|
+
// exactly `validationError(issues)`, so any recovery semantics keep the generic lane.
|
|
77
|
+
const fusedQuery =
|
|
78
|
+
!bare &&
|
|
79
|
+
!hasResponseContract &&
|
|
80
|
+
schema?.query !== undefined &&
|
|
81
|
+
schema.body === undefined &&
|
|
82
|
+
schema.params === undefined &&
|
|
83
|
+
schema.headers === undefined &&
|
|
84
|
+
schema.onValidationError === undefined &&
|
|
85
|
+
!defaultOnValidationError &&
|
|
86
|
+
!hasIdempotency &&
|
|
87
|
+
!hasLedger &&
|
|
88
|
+
derives === 0 &&
|
|
89
|
+
beforeHandle === 0 &&
|
|
90
|
+
afterHandle === 0 &&
|
|
91
|
+
onError === 0 &&
|
|
92
|
+
around === 0
|
|
93
|
+
|
|
94
|
+
const bodyOnly =
|
|
95
|
+
!hasResponseContract &&
|
|
96
|
+
schema?.body !== undefined &&
|
|
97
|
+
schema.query === undefined &&
|
|
98
|
+
schema.params === undefined &&
|
|
99
|
+
schema.headers === undefined &&
|
|
100
|
+
derives === 0 &&
|
|
101
|
+
beforeHandle === 0 &&
|
|
102
|
+
afterHandle === 0 &&
|
|
103
|
+
onError === 0
|
|
104
|
+
|
|
105
|
+
const fusedBody =
|
|
106
|
+
!bare &&
|
|
107
|
+
bodyOnly &&
|
|
108
|
+
schema.onValidationError === undefined &&
|
|
109
|
+
!defaultOnValidationError &&
|
|
110
|
+
!hasIdempotency &&
|
|
111
|
+
!hasLedger &&
|
|
112
|
+
around === 0
|
|
113
|
+
|
|
114
|
+
const lane: RouteExecutionLane = bare
|
|
115
|
+
? "bare"
|
|
116
|
+
: bodyOnly
|
|
117
|
+
? "body"
|
|
118
|
+
: !hasResponseContract &&
|
|
119
|
+
schema?.body === undefined &&
|
|
120
|
+
schema?.query !== undefined &&
|
|
121
|
+
schema.params === undefined &&
|
|
122
|
+
schema.headers === undefined &&
|
|
123
|
+
derives === 0 &&
|
|
124
|
+
beforeHandle === 0 &&
|
|
125
|
+
afterHandle === 0 &&
|
|
126
|
+
onError === 0
|
|
127
|
+
? "query"
|
|
128
|
+
: "lifecycle"
|
|
129
|
+
|
|
130
|
+
// Lifecycle routes with no params schema are the common middleware shape: derive/before plus an
|
|
131
|
+
// optional query or body schema. Select their complete validation stage at registration so the
|
|
132
|
+
// request path never re-checks params/body presence. Parameter-schema routes retain the generic
|
|
133
|
+
// lifecycle runner until their more involved recovery matrix is selected explicitly.
|
|
134
|
+
const lifecycleLane: LifecycleExecutionLane =
|
|
135
|
+
lane !== "lifecycle" || schema?.params !== undefined || schema?.headers !== undefined
|
|
136
|
+
? undefined
|
|
137
|
+
: schema?.body !== undefined
|
|
138
|
+
? schema.query !== undefined
|
|
139
|
+
? "body-query"
|
|
140
|
+
: "body"
|
|
141
|
+
: schema?.query !== undefined
|
|
142
|
+
? "query"
|
|
143
|
+
: "hooks"
|
|
144
|
+
|
|
145
|
+
// The realistic middleware shape is commonly exactly one synchronous-or-async derive followed by
|
|
146
|
+
// one before hook. Keep the generic runner for every route that can observe decorations, after hooks,
|
|
147
|
+
// error hooks, or response contracts; this lane only removes the two per-request hook-loop dispatches
|
|
148
|
+
// and preserves the same async continuations and error handling. A params or headers schema routes
|
|
149
|
+
// back to the generic lane because the fused builder has no `params` / `headers` validation step
|
|
150
|
+
// baked in - adding them would be more code than it saves. A `body` schema is the same story
|
|
151
|
+
// because the fused derive-before / derive-before-after builders don't parse the body: routes with a
|
|
152
|
+
// body schema are routed to the `body-derive-before` / `body-derive-before-after` lane below.
|
|
153
|
+
const lifecycleHookLane =
|
|
154
|
+
lane === "lifecycle" &&
|
|
155
|
+
!hasResponseContract &&
|
|
156
|
+
!hasDecorations &&
|
|
157
|
+
!hasIdempotency &&
|
|
158
|
+
!hasLedger &&
|
|
159
|
+
schema?.onValidationError === undefined &&
|
|
160
|
+
!defaultOnValidationError &&
|
|
161
|
+
schema?.params === undefined &&
|
|
162
|
+
schema?.headers === undefined &&
|
|
163
|
+
schema?.body === undefined &&
|
|
164
|
+
derives === 1 &&
|
|
165
|
+
beforeHandle === 1 &&
|
|
166
|
+
afterHandle <= 1 &&
|
|
167
|
+
onError === 0 &&
|
|
168
|
+
around === 0
|
|
169
|
+
? afterHandle === 0
|
|
170
|
+
? "derive-before"
|
|
171
|
+
: "derive-before-after"
|
|
172
|
+
: undefined
|
|
173
|
+
|
|
174
|
+
// Body + lifecycle hooks in a closed shape: a body schema plus the same 1-derive + 1-before +
|
|
175
|
+
// 0-or-1-after + 0-onError shape, with no other body/query/path conflict. The fused closure runs
|
|
176
|
+
// parse → validate → derive → before → handler → after → respond in one function frame, the same
|
|
177
|
+
// win `fusedBody` already gives for the no-hooks body shape.
|
|
178
|
+
const fusedBodyLifecycle =
|
|
179
|
+
lane === "lifecycle" &&
|
|
180
|
+
schema?.body !== undefined &&
|
|
181
|
+
schema?.query === undefined &&
|
|
182
|
+
schema?.params === undefined &&
|
|
183
|
+
schema?.headers === undefined &&
|
|
184
|
+
schema?.onValidationError === undefined &&
|
|
185
|
+
!defaultOnValidationError &&
|
|
186
|
+
!hasResponseContract &&
|
|
187
|
+
!hasDecorations &&
|
|
188
|
+
!hasIdempotency &&
|
|
189
|
+
!hasLedger &&
|
|
190
|
+
derives === 1 &&
|
|
191
|
+
beforeHandle === 1 &&
|
|
192
|
+
afterHandle <= 1 &&
|
|
193
|
+
onError === 0 &&
|
|
194
|
+
around === 0
|
|
195
|
+
|
|
196
|
+
return {
|
|
197
|
+
bare,
|
|
198
|
+
fusedQuery,
|
|
199
|
+
bodyOnly,
|
|
200
|
+
fusedBody,
|
|
201
|
+
lane,
|
|
202
|
+
lifecycleLane,
|
|
203
|
+
lifecycleHookLane,
|
|
204
|
+
fusedLane:
|
|
205
|
+
around === 0
|
|
206
|
+
? fusedQuery
|
|
207
|
+
? "query"
|
|
208
|
+
: fusedBody
|
|
209
|
+
? "body"
|
|
210
|
+
: bare
|
|
211
|
+
? "bare"
|
|
212
|
+
: lifecycleHookLane !== undefined
|
|
213
|
+
? (lifecycleHookLane as "derive-before" | "derive-before-after")
|
|
214
|
+
: fusedBodyLifecycle
|
|
215
|
+
? afterHandle === 0
|
|
216
|
+
? "body-derive-before"
|
|
217
|
+
: "body-derive-before-after"
|
|
218
|
+
: undefined
|
|
219
|
+
: undefined,
|
|
220
|
+
}
|
|
221
|
+
}
|