@nifrajs/core 1.12.0 → 1.13.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/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/router/pattern.d.ts +42 -0
- package/dist/router/pattern.d.ts.map +1 -0
- package/dist/router/pattern.js +127 -0
- package/dist/router/pattern.js.map +1 -0
- package/dist/router/router.d.ts +2 -1
- package/dist/router/router.d.ts.map +1 -1
- package/dist/router/router.js +11 -48
- package/dist/router/router.js.map +1 -1
- package/dist/server/contract.d.ts +23 -6
- package/dist/server/contract.d.ts.map +1 -1
- package/dist/server/contract.js +25 -6
- package/dist/server/contract.js.map +1 -1
- package/dist/server/server.d.ts +28 -13
- package/dist/server/server.d.ts.map +1 -1
- package/dist/server/server.js +281 -253
- package/dist/server/server.js.map +1 -1
- package/package.json +6 -1
- package/src/index.ts +2 -1
- package/src/router/pattern.ts +176 -0
- package/src/router/router.ts +11 -69
- package/src/server/contract.ts +42 -18
- package/src/server/server.ts +465 -347
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { RouteConfigError } from "../errors.ts"
|
|
2
|
+
|
|
3
|
+
const PARAM_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/
|
|
4
|
+
const RESERVED_PARAM_NAMES = new Set(["__proto__", "constructor", "prototype"])
|
|
5
|
+
const COMPILED_ROUTE_PATTERN: unique symbol = Symbol("nifra.compiled-route-pattern")
|
|
6
|
+
|
|
7
|
+
export type RoutePatternSegment =
|
|
8
|
+
| { readonly kind: "static"; readonly value: string }
|
|
9
|
+
| { readonly kind: "param"; readonly name: string }
|
|
10
|
+
| { readonly kind: "wildcard"; readonly name: string }
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Compiled route grammar shared by runtime routers, browser navigation, mocks, and adapters.
|
|
14
|
+
*
|
|
15
|
+
* The result, its two arrays, and every segment are frozen because consumers share one instance. In
|
|
16
|
+
* particular, a segment must not diverge from the lazily cached regex after first match. The measured
|
|
17
|
+
* sub-microsecond cost per route is paid only at registration and preserves that runtime invariant.
|
|
18
|
+
*/
|
|
19
|
+
export interface CompiledRoutePattern {
|
|
20
|
+
readonly [COMPILED_ROUTE_PATTERN]: true
|
|
21
|
+
readonly pattern: string
|
|
22
|
+
readonly segments: readonly RoutePatternSegment[]
|
|
23
|
+
readonly paramNames: readonly string[]
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export type RoutePatternMatch =
|
|
27
|
+
| { readonly matched: true; readonly params: Record<string, string> }
|
|
28
|
+
| { readonly matched: false; readonly reason: "not-found" | "malformed" }
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Regex per compiled pattern, derived on first use. The core trie matches by descending segments and
|
|
32
|
+
* never asks for one, so building it during {@link compileRoutePattern} would charge every server's
|
|
33
|
+
* boot for the browser/mock adapters alone. Keyed by the frozen pattern, so the cache dies with it.
|
|
34
|
+
*/
|
|
35
|
+
const REGEX_CACHE = new WeakMap<CompiledRoutePattern, RegExp>()
|
|
36
|
+
|
|
37
|
+
function regexOf(compiled: CompiledRoutePattern): RegExp {
|
|
38
|
+
let regex = REGEX_CACHE.get(compiled)
|
|
39
|
+
if (regex === undefined) {
|
|
40
|
+
if (compiled[COMPILED_ROUTE_PATTERN] !== true) {
|
|
41
|
+
throw new TypeError("route pattern was not produced by compileRoutePattern()")
|
|
42
|
+
}
|
|
43
|
+
const parts = compiled.segments.map((segment) =>
|
|
44
|
+
segment.kind === "static"
|
|
45
|
+
? escapeRegex(segment.value)
|
|
46
|
+
: segment.kind === "param"
|
|
47
|
+
? "([^/]+)"
|
|
48
|
+
: "(.+)",
|
|
49
|
+
)
|
|
50
|
+
regex = new RegExp(parts.length === 0 ? "^/$" : `^/${parts.join("/")}$`)
|
|
51
|
+
REGEX_CACHE.set(compiled, regex)
|
|
52
|
+
}
|
|
53
|
+
return regex
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function validParamName(name: string): boolean {
|
|
57
|
+
return PARAM_NAME.test(name) && !RESERVED_PARAM_NAMES.has(name)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function escapeRegex(value: string): string {
|
|
61
|
+
return value.replace(/[.*+?^()|[\]\\{}$]/g, "\\$&")
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Parse and validate Nifra's strict route grammar once. Trailing slashes remain significant. */
|
|
65
|
+
export function compileRoutePattern(pattern: string): CompiledRoutePattern {
|
|
66
|
+
if (pattern.length === 0 || pattern.charCodeAt(0) !== 47 /* / */) {
|
|
67
|
+
throw new RouteConfigError("INVALID_PATH", `path must start with "/": "${pattern}"`)
|
|
68
|
+
}
|
|
69
|
+
const raw = pattern === "/" ? [] : pattern.slice(1).split("/")
|
|
70
|
+
const segments: RoutePatternSegment[] = []
|
|
71
|
+
const paramNames: string[] = []
|
|
72
|
+
for (let i = 0; i < raw.length; i++) {
|
|
73
|
+
const value = raw[i]!
|
|
74
|
+
if (value.charCodeAt(0) === 58 /* : */) {
|
|
75
|
+
const name = value.slice(1)
|
|
76
|
+
if (!validParamName(name)) {
|
|
77
|
+
throw new RouteConfigError(
|
|
78
|
+
"INVALID_PARAM_NAME",
|
|
79
|
+
`invalid parameter ":${name}" in "${pattern}"`,
|
|
80
|
+
)
|
|
81
|
+
}
|
|
82
|
+
if (paramNames.includes(name)) {
|
|
83
|
+
throw new RouteConfigError(
|
|
84
|
+
"DUPLICATE_PARAM",
|
|
85
|
+
`duplicate parameter ":${name}" in "${pattern}"`,
|
|
86
|
+
)
|
|
87
|
+
}
|
|
88
|
+
paramNames.push(name)
|
|
89
|
+
segments.push(Object.freeze({ kind: "param", name }))
|
|
90
|
+
continue
|
|
91
|
+
}
|
|
92
|
+
if (value.charCodeAt(0) === 42 /* * */) {
|
|
93
|
+
if (i !== raw.length - 1) {
|
|
94
|
+
throw new RouteConfigError(
|
|
95
|
+
"WILDCARD_NOT_LAST",
|
|
96
|
+
`wildcard must be the final segment in "${pattern}"`,
|
|
97
|
+
)
|
|
98
|
+
}
|
|
99
|
+
const name = value.length === 1 ? "*" : value.slice(1)
|
|
100
|
+
if (name !== "*" && !validParamName(name)) {
|
|
101
|
+
throw new RouteConfigError(
|
|
102
|
+
"INVALID_PARAM_NAME",
|
|
103
|
+
`invalid wildcard "*${name}" in "${pattern}"`,
|
|
104
|
+
)
|
|
105
|
+
}
|
|
106
|
+
if (paramNames.includes(name)) {
|
|
107
|
+
throw new RouteConfigError(
|
|
108
|
+
"DUPLICATE_PARAM",
|
|
109
|
+
`duplicate parameter "${name}" in "${pattern}"`,
|
|
110
|
+
)
|
|
111
|
+
}
|
|
112
|
+
paramNames.push(name)
|
|
113
|
+
segments.push(Object.freeze({ kind: "wildcard", name }))
|
|
114
|
+
continue
|
|
115
|
+
}
|
|
116
|
+
segments.push(Object.freeze({ kind: "static", value }))
|
|
117
|
+
}
|
|
118
|
+
const compiled = Object.freeze({
|
|
119
|
+
[COMPILED_ROUTE_PATTERN]: true,
|
|
120
|
+
pattern,
|
|
121
|
+
segments: Object.freeze(segments),
|
|
122
|
+
paramNames: Object.freeze(paramNames),
|
|
123
|
+
}) as CompiledRoutePattern
|
|
124
|
+
return compiled
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Core precedence: static > param > wildcard at the first differing segment, independent of order. */
|
|
128
|
+
export function compareRoutePatternSpecificity(
|
|
129
|
+
left: CompiledRoutePattern,
|
|
130
|
+
right: CompiledRoutePattern,
|
|
131
|
+
): number {
|
|
132
|
+
const length = Math.max(left.segments.length, right.segments.length)
|
|
133
|
+
for (let i = 0; i < length; i++) {
|
|
134
|
+
const a = left.segments[i]
|
|
135
|
+
const b = right.segments[i]
|
|
136
|
+
if (a === undefined || b === undefined) return b === undefined ? -1 : 1
|
|
137
|
+
const aWeight = a.kind === "static" ? 3 : a.kind === "param" ? 2 : 1
|
|
138
|
+
const bWeight = b.kind === "static" ? 3 : b.kind === "param" ? 2 : 1
|
|
139
|
+
if (aWeight !== bWeight) return bWeight - aWeight
|
|
140
|
+
}
|
|
141
|
+
return 0
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Decode router captures under one rule. Plain values take the zero-allocation path; malformed
|
|
145
|
+
* escapes return `null`, allowing HTTP to emit 400 while client navigation declines the match. */
|
|
146
|
+
export function decodeRouteParams(raw: Record<string, string>): Record<string, string> | null {
|
|
147
|
+
let out: Record<string, string> | undefined
|
|
148
|
+
for (const key in raw) {
|
|
149
|
+
const value = raw[key]!
|
|
150
|
+
if (!value.includes("%")) continue
|
|
151
|
+
try {
|
|
152
|
+
out ??= { ...raw }
|
|
153
|
+
out[key] = decodeURIComponent(value)
|
|
154
|
+
} catch {
|
|
155
|
+
return null
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return out ?? raw
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** Match one compiled pattern and return decoded captures. The caller decides cross-pattern order. */
|
|
162
|
+
export function matchRoutePattern(
|
|
163
|
+
compiled: CompiledRoutePattern,
|
|
164
|
+
pathname: string,
|
|
165
|
+
): RoutePatternMatch {
|
|
166
|
+
const match = regexOf(compiled).exec(pathname)
|
|
167
|
+
if (match === null) return { matched: false, reason: "not-found" }
|
|
168
|
+
const params: Record<string, string> = {}
|
|
169
|
+
for (let i = 0; i < compiled.paramNames.length; i++) {
|
|
170
|
+
params[compiled.paramNames[i]!] = match[i + 1] ?? ""
|
|
171
|
+
}
|
|
172
|
+
const decoded = decodeRouteParams(params)
|
|
173
|
+
return decoded === null
|
|
174
|
+
? { matched: false, reason: "malformed" }
|
|
175
|
+
: { matched: true, params: decoded }
|
|
176
|
+
}
|
package/src/router/router.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { RouteConfigError } from "../errors.ts"
|
|
2
|
+
import { type CompiledRoutePattern, compileRoutePattern } from "./pattern.ts"
|
|
2
3
|
|
|
3
4
|
/** HTTP methods the router accepts. */
|
|
4
5
|
export const METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"] as const
|
|
@@ -11,24 +12,7 @@ function isMethod(value: string): value is Method {
|
|
|
11
12
|
return METHOD_SET.has(value)
|
|
12
13
|
}
|
|
13
14
|
|
|
14
|
-
/** Parameter names must be valid identifiers — mirrors the global identifier rule. */
|
|
15
|
-
const PARAM_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/
|
|
16
|
-
|
|
17
|
-
/** Object-prototype keys are rejected as param names: a route like `/:__proto__` could let a captured
|
|
18
|
-
* value reach a prototype-polluting key. Param *names* come from the route pattern (never from request
|
|
19
|
-
* input) and are validated here at registration, so a reserved key can never reach `buildParams` —
|
|
20
|
-
* this boot-time guard is the whole protection. The runtime params object stays a plain `{}` (only
|
|
21
|
-
* user-controlled *values* land in it, under these validated keys), keeping the param path
|
|
22
|
-
* allocation-cheap; a null-prototype object would add cost for no added safety. */
|
|
23
|
-
const RESERVED_PARAM_NAMES = new Set(["__proto__", "constructor", "prototype"])
|
|
24
|
-
|
|
25
|
-
/** A param name is valid when it's an identifier AND not a reserved object-prototype key. */
|
|
26
|
-
const validParamName = (name: string): boolean =>
|
|
27
|
-
PARAM_NAME.test(name) && !RESERVED_PARAM_NAMES.has(name)
|
|
28
|
-
|
|
29
15
|
const SLASH = 47
|
|
30
|
-
const COLON = 58
|
|
31
|
-
const STAR = 42
|
|
32
16
|
export const EMPTY_PARAMS: Record<string, string> = Object.freeze({})
|
|
33
17
|
const DYNAMIC_MATCH_CACHE_MAX = 2048
|
|
34
18
|
|
|
@@ -245,7 +229,7 @@ export class Router<T> {
|
|
|
245
229
|
* (boot-time, L2) on a duplicate route, a malformed pattern, or conflicting
|
|
246
230
|
* parameter names for the same path shape.
|
|
247
231
|
*/
|
|
248
|
-
add(method: Method,
|
|
232
|
+
add(method: Method, pattern: string | CompiledRoutePattern, payload: T): void {
|
|
249
233
|
DYNAMIC_MATCH_CACHES.delete(this)
|
|
250
234
|
const upper = method.toUpperCase()
|
|
251
235
|
// The `Method` parameter type is the compile-time guard; this re-checks at
|
|
@@ -253,66 +237,24 @@ export class Router<T> {
|
|
|
253
237
|
if (!isMethod(upper)) {
|
|
254
238
|
throw new RouteConfigError("INVALID_METHOD", `unsupported HTTP method "${method}"`)
|
|
255
239
|
}
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
const segments = path === "/" ? [] : path.slice(1).split("/")
|
|
261
|
-
const paramNames: string[] = []
|
|
262
|
-
let isStatic = true
|
|
240
|
+
const compiled = typeof pattern === "string" ? compileRoutePattern(pattern) : pattern
|
|
241
|
+
const path = compiled.pattern
|
|
242
|
+
const paramNames = compiled.paramNames
|
|
243
|
+
const isStatic = paramNames.length === 0
|
|
263
244
|
let node = this.root
|
|
264
245
|
|
|
265
|
-
for (
|
|
266
|
-
|
|
267
|
-
const marker = seg.charCodeAt(0)
|
|
268
|
-
|
|
269
|
-
if (marker === COLON) {
|
|
270
|
-
isStatic = false
|
|
271
|
-
const name = seg.slice(1)
|
|
272
|
-
if (!validParamName(name)) {
|
|
273
|
-
throw new RouteConfigError(
|
|
274
|
-
"INVALID_PARAM_NAME",
|
|
275
|
-
`invalid parameter ":${name}" in "${path}"`,
|
|
276
|
-
)
|
|
277
|
-
}
|
|
278
|
-
if (paramNames.includes(name)) {
|
|
279
|
-
throw new RouteConfigError(
|
|
280
|
-
"DUPLICATE_PARAM",
|
|
281
|
-
`duplicate parameter ":${name}" in "${path}"`,
|
|
282
|
-
)
|
|
283
|
-
}
|
|
284
|
-
paramNames.push(name)
|
|
246
|
+
for (const segment of compiled.segments) {
|
|
247
|
+
if (segment.kind === "param") {
|
|
285
248
|
node.paramChild ??= createNode<T>()
|
|
286
249
|
node = node.paramChild
|
|
287
|
-
} else if (
|
|
288
|
-
isStatic = false
|
|
289
|
-
if (i !== segments.length - 1) {
|
|
290
|
-
throw new RouteConfigError(
|
|
291
|
-
"WILDCARD_NOT_LAST",
|
|
292
|
-
`wildcard must be the final segment in "${path}"`,
|
|
293
|
-
)
|
|
294
|
-
}
|
|
295
|
-
const name = seg.length === 1 ? "*" : seg.slice(1)
|
|
296
|
-
if (name !== "*" && !validParamName(name)) {
|
|
297
|
-
throw new RouteConfigError(
|
|
298
|
-
"INVALID_PARAM_NAME",
|
|
299
|
-
`invalid wildcard "*${name}" in "${path}"`,
|
|
300
|
-
)
|
|
301
|
-
}
|
|
302
|
-
if (paramNames.includes(name)) {
|
|
303
|
-
throw new RouteConfigError(
|
|
304
|
-
"DUPLICATE_PARAM",
|
|
305
|
-
`duplicate parameter "${name}" in "${path}"`,
|
|
306
|
-
)
|
|
307
|
-
}
|
|
308
|
-
paramNames.push(name)
|
|
250
|
+
} else if (segment.kind === "wildcard") {
|
|
309
251
|
node.wildcardChild ??= createNode<T>()
|
|
310
252
|
node = node.wildcardChild
|
|
311
253
|
} else {
|
|
312
|
-
let child = node.staticChildren.get(
|
|
254
|
+
let child = node.staticChildren.get(segment.value)
|
|
313
255
|
if (child === undefined) {
|
|
314
256
|
child = createNode<T>()
|
|
315
|
-
node.staticChildren.set(
|
|
257
|
+
node.staticChildren.set(segment.value, child)
|
|
316
258
|
}
|
|
317
259
|
node = child
|
|
318
260
|
}
|
package/src/server/contract.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { normalizeRouteCapabilities } from "../internal/capability-runtime.ts"
|
|
|
4
4
|
import { METHODS, type Method } from "../router/router.ts"
|
|
5
5
|
import type { InferOutput, StandardSchemaV1 } from "../schema/standard.ts"
|
|
6
6
|
import type { Context, IdempotencyConfig, Params, RouteSchema } from "./context.ts"
|
|
7
|
-
import type { OutputOf } from "./registry.ts"
|
|
7
|
+
import type { EmptyRegistry, OutputOf, Registry } from "./registry.ts"
|
|
8
8
|
import { Server } from "./server.ts"
|
|
9
9
|
|
|
10
10
|
/** An additional (non-success) response a contract operation can document, e.g. a `404`. */
|
|
@@ -140,9 +140,13 @@ type HandlerReturnForOp<O extends OperationDef> = O extends {
|
|
|
140
140
|
? Response | InferOutput<R>
|
|
141
141
|
: unknown
|
|
142
142
|
|
|
143
|
-
/**
|
|
144
|
-
|
|
145
|
-
|
|
143
|
+
/**
|
|
144
|
+
* The handlers `implement` requires: one per operation, typed from the op's input + response contract,
|
|
145
|
+
* intersected with the host app's accumulated `derive`/`decorate` context - the same
|
|
146
|
+
* `Context & Ctx` an inline {@link Handler} receives, so a handler graduates either way unchanged.
|
|
147
|
+
*/
|
|
148
|
+
export type HandlersFor<C extends ContractShape, Ctx = NonNullable<unknown>> = {
|
|
149
|
+
[K in keyof C]: (context: ContextForOp<C[K]> & Ctx) => MaybePromise<HandlerReturnForOp<C[K]>>
|
|
146
150
|
}
|
|
147
151
|
|
|
148
152
|
const METHOD_SET: ReadonlySet<string> = new Set(METHODS)
|
|
@@ -189,7 +193,11 @@ type AnyFn = (...args: never[]) => unknown
|
|
|
189
193
|
* the implemented server stays route-for-route identical to the equivalent inline server (the
|
|
190
194
|
* mode-conformance guarantee), and a contract-typed client and a `typeof app`-typed client agree.
|
|
191
195
|
*/
|
|
192
|
-
export type RegistryFromImpl<
|
|
196
|
+
export type RegistryFromImpl<
|
|
197
|
+
C extends ContractShape,
|
|
198
|
+
H extends HandlersFor<C, Ctx>,
|
|
199
|
+
Ctx = NonNullable<unknown>,
|
|
200
|
+
> = {
|
|
193
201
|
[P in C[keyof C]["path"]]: {
|
|
194
202
|
[K in keyof C as C[K]["path"] extends P ? C[K]["method"] : never]: {
|
|
195
203
|
readonly params: Params<C[K]["path"]>
|
|
@@ -215,13 +223,28 @@ export type RegistryFromImpl<C extends ContractShape, H extends HandlersFor<C>>
|
|
|
215
223
|
* builder, so the result is identical to writing the routes inline — handlers
|
|
216
224
|
* lift over **unchanged** ("graduation"), and body/query schemas validate at the
|
|
217
225
|
* request boundary exactly as in inline mode.
|
|
226
|
+
*
|
|
227
|
+
* Pass a pre-configured `app` to give the contract's routes a middleware chain. A route captures the
|
|
228
|
+
* server's `derive`/`decorate`/assurance chain **at registration**, so anything applied to the
|
|
229
|
+
* returned server afterwards reaches the contract's routes not at all - the app must already carry it:
|
|
230
|
+
*
|
|
231
|
+
* ```ts
|
|
232
|
+
* const app = implement(contract, handlers, server().use(auth).derive(sessionOf))
|
|
233
|
+
* ```
|
|
234
|
+
*
|
|
235
|
+
* That is also the seam that lets `nifra assure` prove rather than merely classify a contract-first
|
|
236
|
+
* app: the plugin that installs the enforcement is what declares the evidence
|
|
237
|
+
* ({@link withRouteAssurance}), and only a plugin installed *before* registration is captured. Handlers
|
|
238
|
+
* then see the app's `Ctx`, and the returned server keeps any routes the app already had.
|
|
218
239
|
*/
|
|
219
|
-
export function implement<
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
240
|
+
export function implement<
|
|
241
|
+
const C extends ContractShape,
|
|
242
|
+
H extends HandlersFor<C, Ctx>,
|
|
243
|
+
R extends Registry = EmptyRegistry,
|
|
244
|
+
Ctx = NonNullable<unknown>,
|
|
245
|
+
>(contract: C, handlers: H, app?: Server<R, Ctx>): Server<R & RegistryFromImpl<C, H, Ctx>, Ctx> {
|
|
246
|
+
const target = (app ?? new Server()) as Server<R, Ctx>
|
|
247
|
+
const routes = Object.entries(contract).map(([name, op]) => {
|
|
225
248
|
// body/query are validated at the request boundary; `response` is a type + introspection contract
|
|
226
249
|
// ONLY — never validated at runtime and never read on the request hot path (the lifecycle reads
|
|
227
250
|
// schema.body/query by name; the `bare`/bodyOnly/queryOnly fast-path gates ignore `response`). Built
|
|
@@ -243,14 +266,15 @@ export function implement<const C extends ContractShape, H extends HandlersFor<C
|
|
|
243
266
|
...(op.classification !== undefined ? { classification: op.classification } : {}),
|
|
244
267
|
}
|
|
245
268
|
: undefined
|
|
246
|
-
|
|
247
|
-
op.method,
|
|
248
|
-
op.path,
|
|
269
|
+
return {
|
|
270
|
+
method: op.method,
|
|
271
|
+
path: op.path,
|
|
249
272
|
schema,
|
|
250
|
-
handlers[name as keyof H] as (context: never) => unknown,
|
|
251
|
-
|
|
252
|
-
}
|
|
273
|
+
handler: handlers[name as keyof H] as (context: never) => unknown,
|
|
274
|
+
}
|
|
275
|
+
})
|
|
276
|
+
target.registerBatch(routes)
|
|
253
277
|
// Runtime registered exactly the contract's routes through the inline path; the
|
|
254
278
|
// registry type is computed from the contract inputs + handler return types.
|
|
255
|
-
return
|
|
279
|
+
return target as unknown as Server<R & RegistryFromImpl<C, H, Ctx>, Ctx>
|
|
256
280
|
}
|