@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
package/src/server/server.ts
CHANGED
|
@@ -40,7 +40,12 @@ import {
|
|
|
40
40
|
type EffectLedgerOptions,
|
|
41
41
|
type RequestLedger,
|
|
42
42
|
} from "../ledger.ts"
|
|
43
|
-
import {
|
|
43
|
+
import {
|
|
44
|
+
type CompiledRoutePattern,
|
|
45
|
+
compileRoutePattern,
|
|
46
|
+
decodeRouteParams,
|
|
47
|
+
} from "../router/pattern.ts"
|
|
48
|
+
import { EMPTY_PARAMS, type Method, Router, type RouterMatch } from "../router/router.ts"
|
|
44
49
|
import type {
|
|
45
50
|
InferOutput,
|
|
46
51
|
StandardIssue,
|
|
@@ -199,6 +204,35 @@ interface ResolvedEffectLedger {
|
|
|
199
204
|
readonly declared: readonly string[]
|
|
200
205
|
}
|
|
201
206
|
|
|
207
|
+
type RouteExecutionRunner = <T, R extends Registry, Ctx>(
|
|
208
|
+
runtime: Server<R, Ctx>,
|
|
209
|
+
entry: RouteEntry,
|
|
210
|
+
source: RequestSource,
|
|
211
|
+
params: Record<string, string>,
|
|
212
|
+
search: string | undefined,
|
|
213
|
+
signal: AbortSignal,
|
|
214
|
+
budget: RequestBudget,
|
|
215
|
+
platform: Platform | undefined,
|
|
216
|
+
finalize: (result: unknown, set: CtxSet) => T,
|
|
217
|
+
wrapResponse: (response: Response) => T,
|
|
218
|
+
) => MaybePromise<T>
|
|
219
|
+
|
|
220
|
+
type ContextRouteRunner = <T, R extends Registry, Ctx>(
|
|
221
|
+
runtime: Server<R, Ctx>,
|
|
222
|
+
entry: RouteEntry,
|
|
223
|
+
source: RequestSource,
|
|
224
|
+
ctx: RawContext,
|
|
225
|
+
finalize: (result: unknown, set: CtxSet) => T,
|
|
226
|
+
wrapResponse: (response: Response) => T,
|
|
227
|
+
) => MaybePromise<T>
|
|
228
|
+
|
|
229
|
+
/** Registration-compiled route behavior. Every adapter invokes the same runner; the optional fused
|
|
230
|
+
* renderer is only a response-format specialization of that same selected route semantics. */
|
|
231
|
+
interface RouteExecutionPlan {
|
|
232
|
+
readonly run: RouteExecutionRunner
|
|
233
|
+
readonly fusedWeb: FusedWebRunner | undefined
|
|
234
|
+
}
|
|
235
|
+
|
|
202
236
|
interface RouteEntry {
|
|
203
237
|
readonly handler: InternalHandler
|
|
204
238
|
readonly schema: RouteSchema | undefined
|
|
@@ -219,33 +253,75 @@ interface RouteEntry {
|
|
|
219
253
|
readonly onError: ReadonlyArray<RawErrorHandler>
|
|
220
254
|
/** Wraps the matched route lifecycle. Empty for the common no-around path. */
|
|
221
255
|
readonly around: ReadonlyArray<RawAround>
|
|
222
|
-
/**
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
readonly
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
256
|
+
/** The single immutable execution decision consumed by portable, Node-direct, and Bun-native paths. */
|
|
257
|
+
readonly execution: RouteExecutionPlan
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/** One canonical runtime route fact. The catalog owns matching, reflection, assurance, tool metadata,
|
|
261
|
+
* replay, and native compilation input so batch registration has one commit point. */
|
|
262
|
+
interface CatalogRoute {
|
|
263
|
+
readonly method: Method
|
|
264
|
+
readonly path: string
|
|
265
|
+
readonly pattern: CompiledRoutePattern
|
|
266
|
+
readonly entry: RouteEntry
|
|
267
|
+
readonly descriptor: RouteDescriptor
|
|
268
|
+
readonly assurance: readonly AssuranceDeclaration[]
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Runtime route catalog. Single-route registration mutates directly; multi-route registration replays
|
|
273
|
+
* the existing catalog plus the candidate batch into a staged router, then swaps the complete state only
|
|
274
|
+
* after every route validates. Failed `implement()`/`merge()` batches therefore leave matching and
|
|
275
|
+
* reflection unchanged.
|
|
276
|
+
*/
|
|
277
|
+
class RouteCatalog {
|
|
278
|
+
private matcher = new Router<RouteEntry>()
|
|
279
|
+
private records: CatalogRoute[] = []
|
|
280
|
+
/** Allocation-free reflection view for the common no-assurance case. Derived only at commit time. */
|
|
281
|
+
private descriptors: RouteDescriptor[] = []
|
|
282
|
+
private assurancePresent = false
|
|
283
|
+
|
|
284
|
+
add(route: CatalogRoute): void {
|
|
285
|
+
this.matcher.add(route.method, route.pattern, route.entry)
|
|
286
|
+
this.records.push(route)
|
|
287
|
+
this.descriptors.push(route.descriptor)
|
|
288
|
+
if (route.assurance.length > 0) this.assurancePresent = true
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
addBatch(routes: readonly CatalogRoute[]): void {
|
|
292
|
+
if (routes.length === 0) return
|
|
293
|
+
const nextRecords = this.records.concat(routes)
|
|
294
|
+
const nextDescriptors = this.descriptors.concat(routes.map(({ descriptor }) => descriptor))
|
|
295
|
+
const nextAssurancePresent =
|
|
296
|
+
this.assurancePresent || routes.some((route) => route.assurance.length > 0)
|
|
297
|
+
const staged = new Router<RouteEntry>()
|
|
298
|
+
for (const route of this.records) staged.add(route.method, route.pattern, route.entry)
|
|
299
|
+
for (const route of routes) staged.add(route.method, route.pattern, route.entry)
|
|
300
|
+
this.matcher = staged
|
|
301
|
+
this.records = nextRecords
|
|
302
|
+
this.descriptors = nextDescriptors
|
|
303
|
+
this.assurancePresent = nextAssurancePresent
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
find(method: string, path: string): RouterMatch<RouteEntry> {
|
|
307
|
+
return this.matcher.find(method, path)
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
entries(): readonly CatalogRoute[] {
|
|
311
|
+
return this.records
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
routeDescriptors(): ReadonlyArray<RouteDescriptor> {
|
|
315
|
+
return this.descriptors
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
lastDescriptor(): RouteDescriptor | undefined {
|
|
319
|
+
return this.records[this.records.length - 1]?.descriptor
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
hasAssurance(): boolean {
|
|
323
|
+
return this.assurancePresent
|
|
324
|
+
}
|
|
249
325
|
}
|
|
250
326
|
|
|
251
327
|
/** The fused Web lane: same inputs `routeAndRun` would hand the generic path, a `Response` out. */
|
|
@@ -793,27 +869,6 @@ async function readBoundedForm(
|
|
|
793
869
|
return out
|
|
794
870
|
}
|
|
795
871
|
|
|
796
|
-
function decodeParams(raw: Record<string, string>): Record<string, string> | null {
|
|
797
|
-
let out: Record<string, string> | undefined
|
|
798
|
-
for (const key in raw) {
|
|
799
|
-
const value = raw[key]!
|
|
800
|
-
// `decodeURIComponent` only changes strings containing `%`; for the common
|
|
801
|
-
// unencoded param (`/users/123`) skip the call and its throw-check entirely —
|
|
802
|
-
// semantically identical (no `%` ⇒ no escapes to decode), ~3× cheaper on that path. If every
|
|
803
|
-
// param is already plain, return the router's per-request object directly and skip a clone.
|
|
804
|
-
if (!value.includes("%")) {
|
|
805
|
-
continue
|
|
806
|
-
}
|
|
807
|
-
try {
|
|
808
|
-
out ??= { ...raw }
|
|
809
|
-
out[key] = decodeURIComponent(value)
|
|
810
|
-
} catch {
|
|
811
|
-
return null
|
|
812
|
-
}
|
|
813
|
-
}
|
|
814
|
-
return out ?? raw
|
|
815
|
-
}
|
|
816
|
-
|
|
817
872
|
function hasReplacementParam(params: Record<string, string>): boolean {
|
|
818
873
|
for (const key in params) {
|
|
819
874
|
if (params[key]!.includes("\uFFFD")) return true
|
|
@@ -1289,7 +1344,7 @@ function appendCookiesToNodeHeaders(
|
|
|
1289
1344
|
* .get("/me", (c) => c.user) // c.user + c.db are typed
|
|
1290
1345
|
*/
|
|
1291
1346
|
export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
1292
|
-
private readonly
|
|
1347
|
+
private readonly catalog: RouteCatalog
|
|
1293
1348
|
/** WebSocket routes, matched separately at upgrade time (a GET + `Upgrade: websocket`). */
|
|
1294
1349
|
private readonly wsRouter: Router<WsEntry>
|
|
1295
1350
|
private wsRouteCount: number
|
|
@@ -1297,7 +1352,6 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
1297
1352
|
* Created by the first `app.ws()` via the `@nifrajs/core/ws` runtime — `undefined` until then, so a
|
|
1298
1353
|
* no-WebSocket app never constructs (or bundles) it. */
|
|
1299
1354
|
private topics: TopicRegistry | undefined
|
|
1300
|
-
private readonly routeList: RouteDescriptor[]
|
|
1301
1355
|
private readonly maxBodyBytes: number
|
|
1302
1356
|
private readonly wsMaxPayloadBytes: number
|
|
1303
1357
|
private readonly requestTimeoutMs: number
|
|
@@ -1319,6 +1373,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
1319
1373
|
/** App-wide validation-error fallback; a route's own `schema.onValidationError` takes precedence. */
|
|
1320
1374
|
private readonly defaultOnValidationError?: RouteSchema["onValidationError"]
|
|
1321
1375
|
private bunServer: RunningServer | undefined
|
|
1376
|
+
private sealed: boolean
|
|
1322
1377
|
private readonly derives: RawDerive[]
|
|
1323
1378
|
private readonly decorations: Record<string, unknown>
|
|
1324
1379
|
private readonly beforeHandleHooks: RawBeforeHandle[]
|
|
@@ -1338,26 +1393,11 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
1338
1393
|
/** App-declared MCP resources / prompts (via {@link resource} / {@link prompt}), read by `nifra mcp`. */
|
|
1339
1394
|
private readonly mcpResourceList: McpResourceDescriptor[]
|
|
1340
1395
|
private readonly mcpPromptList: McpPromptDescriptor[]
|
|
1341
|
-
/**
|
|
1342
|
-
* Every registered route's raw router entry + descriptor, in order — the replay source for
|
|
1343
|
-
* {@link merge}. The entry already carries the chains captured at registration (derives,
|
|
1344
|
-
* decorations, before/after/around/onError), so a merged route behaves exactly as it did on
|
|
1345
|
-
* the server that defined it.
|
|
1346
|
-
*/
|
|
1347
|
-
private readonly rawEntries: Array<{
|
|
1348
|
-
readonly method: Method
|
|
1349
|
-
readonly path: string
|
|
1350
|
-
readonly entry: RouteEntry
|
|
1351
|
-
readonly descriptor: RouteDescriptor
|
|
1352
|
-
readonly assurance: readonly AssuranceDeclaration[]
|
|
1353
|
-
}>
|
|
1354
|
-
|
|
1355
1396
|
constructor(options: ServerOptions = {}) {
|
|
1356
|
-
this.
|
|
1397
|
+
this.catalog = new RouteCatalog()
|
|
1357
1398
|
this.wsRouter = new Router<WsEntry>()
|
|
1358
1399
|
this.wsRouteCount = 0
|
|
1359
1400
|
this.topics = undefined
|
|
1360
|
-
this.routeList = []
|
|
1361
1401
|
this.maxBodyBytes = options.maxBodyBytes ?? DEFAULT_MAX_BODY_BYTES
|
|
1362
1402
|
this.wsMaxPayloadBytes = options.wsMaxPayloadBytes ?? this.maxBodyBytes
|
|
1363
1403
|
this.requestTimeoutMs = options.requestTimeoutMs ?? 0
|
|
@@ -1398,6 +1438,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
1398
1438
|
this.logger = options.logger ?? jsonLogger()
|
|
1399
1439
|
this.defaultOnValidationError = options.onValidationError
|
|
1400
1440
|
this.bunServer = undefined
|
|
1441
|
+
this.sealed = false
|
|
1401
1442
|
this.derives = []
|
|
1402
1443
|
this.decorations = {}
|
|
1403
1444
|
this.beforeHandleHooks = []
|
|
@@ -1413,17 +1454,27 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
1413
1454
|
this.globalAssurance = []
|
|
1414
1455
|
this.mcpResourceList = []
|
|
1415
1456
|
this.mcpPromptList = []
|
|
1416
|
-
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1459
|
+
private assertConfigurable(operation: string): void {
|
|
1460
|
+
if (this.sealed) {
|
|
1461
|
+
throw new FrameworkError(
|
|
1462
|
+
"SERVER_SEALED",
|
|
1463
|
+
`server configuration is sealed after listen(); call ${operation} before listen()`,
|
|
1464
|
+
)
|
|
1465
|
+
}
|
|
1417
1466
|
}
|
|
1418
1467
|
|
|
1419
1468
|
/** Add a per-request, computed context extension for subsequent routes. */
|
|
1420
1469
|
derive<D extends object>(fn: (context: Context & Ctx) => MaybePromise<D>): Server<R, Ctx & D> {
|
|
1470
|
+
this.assertConfigurable("derive()")
|
|
1421
1471
|
this.derives.push(fn as unknown as RawDerive)
|
|
1422
1472
|
return this as unknown as Server<R, Ctx & D>
|
|
1423
1473
|
}
|
|
1424
1474
|
|
|
1425
1475
|
/** Add a static context value for subsequent routes. */
|
|
1426
1476
|
decorate<const K extends string, V>(key: K, value: V): Server<R, Ctx & Record<K, V>> {
|
|
1477
|
+
this.assertConfigurable("decorate()")
|
|
1427
1478
|
this.decorations[key] = value
|
|
1428
1479
|
return this as unknown as Server<R, Ctx & Record<K, V>>
|
|
1429
1480
|
}
|
|
@@ -1435,12 +1486,14 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
1435
1486
|
onRequest(
|
|
1436
1487
|
fn: (req: Request, platform?: Platform<EnvOf<Ctx>>) => MaybePromise<OnRequestResult>,
|
|
1437
1488
|
): this {
|
|
1489
|
+
this.assertConfigurable("onRequest()")
|
|
1438
1490
|
this.onRequestHooks.push(fn as RawOnRequest)
|
|
1439
1491
|
return this
|
|
1440
1492
|
}
|
|
1441
1493
|
|
|
1442
1494
|
/** Run after validation, before the handler; a non-`undefined` return short-circuits. Order-scoped. */
|
|
1443
1495
|
beforeHandle(fn: (context: Context & Ctx) => MaybePromise<unknown>): this {
|
|
1496
|
+
this.assertConfigurable("beforeHandle()")
|
|
1444
1497
|
this.beforeHandleHooks.push(fn as unknown as RawBeforeHandle)
|
|
1445
1498
|
return this
|
|
1446
1499
|
}
|
|
@@ -1451,24 +1504,28 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
1451
1504
|
* a Web `Response`. The first registered wrapper is outermost.
|
|
1452
1505
|
*/
|
|
1453
1506
|
around(fn: <T>(context: Context & Ctx, next: () => MaybePromise<T>) => MaybePromise<T>): this {
|
|
1507
|
+
this.assertConfigurable("around()")
|
|
1454
1508
|
this.aroundHooks.push(fn as unknown as RawAround)
|
|
1455
1509
|
return this
|
|
1456
1510
|
}
|
|
1457
1511
|
|
|
1458
1512
|
/** Transform the handler's result before it is serialized. Order-scoped. */
|
|
1459
1513
|
afterHandle(fn: (result: unknown, context: Context & Ctx) => MaybePromise<unknown>): this {
|
|
1514
|
+
this.assertConfigurable("afterHandle()")
|
|
1460
1515
|
this.afterHandleHooks.push(fn as unknown as RawAfterHandle)
|
|
1461
1516
|
return this
|
|
1462
1517
|
}
|
|
1463
1518
|
|
|
1464
1519
|
/** Handle a thrown error; a non-`undefined` return becomes the response (else the default 500). Order-scoped. */
|
|
1465
1520
|
onError(fn: (error: unknown, context: Context & Ctx) => MaybePromise<unknown>): this {
|
|
1521
|
+
this.assertConfigurable("onError()")
|
|
1466
1522
|
this.onErrorHooks.push(fn as unknown as RawErrorHandler)
|
|
1467
1523
|
return this
|
|
1468
1524
|
}
|
|
1469
1525
|
|
|
1470
1526
|
/** Transform every outgoing response — success, error, 404, 405, short-circuit. Global. */
|
|
1471
1527
|
onResponse(fn: (response: Response, req: Request) => MaybePromise<Response>): this {
|
|
1528
|
+
this.assertConfigurable("onResponse()")
|
|
1472
1529
|
this.onResponseHooks.push(fn)
|
|
1473
1530
|
return this
|
|
1474
1531
|
}
|
|
@@ -1477,6 +1534,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
1477
1534
|
onResponseFinalized(
|
|
1478
1535
|
fn: (outcome: ResponseFinalization, req: Request) => MaybePromise<void>,
|
|
1479
1536
|
): this {
|
|
1537
|
+
this.assertConfigurable("onResponseFinalized()")
|
|
1480
1538
|
this.onResponseFinalizedHooks.push(fn)
|
|
1481
1539
|
return this
|
|
1482
1540
|
}
|
|
@@ -1504,6 +1562,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
1504
1562
|
*/
|
|
1505
1563
|
use(mw: Middleware): this
|
|
1506
1564
|
use(arg: Middleware | ((app: this) => AnyServer)): AnyServer {
|
|
1565
|
+
this.assertConfigurable("use()")
|
|
1507
1566
|
if (typeof arg === "function") {
|
|
1508
1567
|
const name = (arg as { pluginName?: string }).pluginName
|
|
1509
1568
|
if (name !== undefined) {
|
|
@@ -1720,7 +1779,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
1720
1779
|
this.register("POST", path, routeSchema, run as (context: never) => unknown)
|
|
1721
1780
|
// Tag the just-registered descriptor as an MCP tool. `tool` is readonly on RouteDescriptor (an
|
|
1722
1781
|
// introspection field), so write it through a narrow mutable view — not `any`.
|
|
1723
|
-
const lastRoute = this.
|
|
1782
|
+
const lastRoute = this.catalog.lastDescriptor()
|
|
1724
1783
|
if (lastRoute) {
|
|
1725
1784
|
;(lastRoute as { tool?: RouteDescriptor["tool"] }).tool = {
|
|
1726
1785
|
name,
|
|
@@ -1742,6 +1801,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
1742
1801
|
config: { readonly name: string; readonly description?: string; readonly mimeType?: string },
|
|
1743
1802
|
read: McpResourceDescriptor["read"],
|
|
1744
1803
|
): Server<R, Ctx> {
|
|
1804
|
+
this.assertConfigurable("resource()")
|
|
1745
1805
|
this.mcpResourceList.push({
|
|
1746
1806
|
uri,
|
|
1747
1807
|
name: config.name,
|
|
@@ -1761,6 +1821,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
1761
1821
|
config: { readonly description: string; readonly arguments?: readonly PromptArgument[] },
|
|
1762
1822
|
handler: McpPromptDescriptor["handler"],
|
|
1763
1823
|
): Server<R, Ctx> {
|
|
1824
|
+
this.assertConfigurable("prompt()")
|
|
1764
1825
|
this.mcpPromptList.push({
|
|
1765
1826
|
name,
|
|
1766
1827
|
description: config.description,
|
|
@@ -1793,6 +1854,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
1793
1854
|
path: string,
|
|
1794
1855
|
handler: WebSocketHandler<Data, EnvOf<Ctx>, Schema>,
|
|
1795
1856
|
): this {
|
|
1857
|
+
this.assertConfigurable("ws()")
|
|
1796
1858
|
// Boot-time guard: the WS runtime is a subpath (`@nifrajs/core/ws`) so no-WebSocket apps don't
|
|
1797
1859
|
// bundle it. Registration is the loud, early failure point — never the first connection.
|
|
1798
1860
|
const runtime = requireWsRuntime()
|
|
@@ -1848,6 +1910,34 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
1848
1910
|
schema: RouteSchema | undefined,
|
|
1849
1911
|
handler: (context: never) => unknown,
|
|
1850
1912
|
): void {
|
|
1913
|
+
this.assertConfigurable("route registration")
|
|
1914
|
+
this.catalog.add(this.prepareRoute(method, path, schema, handler))
|
|
1915
|
+
}
|
|
1916
|
+
|
|
1917
|
+
/** Register a contract/group route batch atomically. Every route captures the same current chain it
|
|
1918
|
+
* would capture through {@link register}; no route becomes visible unless the full batch validates. */
|
|
1919
|
+
registerBatch(
|
|
1920
|
+
routes: readonly {
|
|
1921
|
+
readonly method: Method
|
|
1922
|
+
readonly path: string
|
|
1923
|
+
readonly schema: RouteSchema | undefined
|
|
1924
|
+
readonly handler: (context: never) => unknown
|
|
1925
|
+
}[],
|
|
1926
|
+
): void {
|
|
1927
|
+
this.assertConfigurable("route registration")
|
|
1928
|
+
const staged = routes.map(({ method, path, schema, handler }) =>
|
|
1929
|
+
this.prepareRoute(method, path, schema, handler),
|
|
1930
|
+
)
|
|
1931
|
+
this.catalog.addBatch(staged)
|
|
1932
|
+
}
|
|
1933
|
+
|
|
1934
|
+
private prepareRoute(
|
|
1935
|
+
method: Method,
|
|
1936
|
+
path: string,
|
|
1937
|
+
schema: RouteSchema | undefined,
|
|
1938
|
+
handler: (context: never) => unknown,
|
|
1939
|
+
): CatalogRoute {
|
|
1940
|
+
const pattern = compileRoutePattern(path)
|
|
1851
1941
|
const capabilities = normalizeRouteCapabilities(schema?.capabilities)
|
|
1852
1942
|
const handlerAssurance = assuranceDeclarationsOf(handler as unknown as object)
|
|
1853
1943
|
const invalidHandlerScope = handlerAssurance.find(
|
|
@@ -1902,6 +1992,31 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
1902
1992
|
isContextlessNoArgArrow(handler),
|
|
1903
1993
|
)
|
|
1904
1994
|
: undefined
|
|
1995
|
+
const contextless = bare && this.aroundHooks.length === 0 && isContextlessNoArgArrow(handler)
|
|
1996
|
+
const lane = bare
|
|
1997
|
+
? "bare"
|
|
1998
|
+
: schema?.body !== undefined &&
|
|
1999
|
+
schema.query === undefined &&
|
|
2000
|
+
this.derives.length === 0 &&
|
|
2001
|
+
this.beforeHandleHooks.length === 0 &&
|
|
2002
|
+
this.afterHandleHooks.length === 0 &&
|
|
2003
|
+
this.onErrorHooks.length === 0
|
|
2004
|
+
? "body"
|
|
2005
|
+
: schema?.body === undefined &&
|
|
2006
|
+
schema?.query !== undefined &&
|
|
2007
|
+
this.derives.length === 0 &&
|
|
2008
|
+
this.beforeHandleHooks.length === 0 &&
|
|
2009
|
+
this.afterHandleHooks.length === 0 &&
|
|
2010
|
+
this.onErrorHooks.length === 0
|
|
2011
|
+
? "query"
|
|
2012
|
+
: "lifecycle"
|
|
2013
|
+
const execution = this.compileExecutionPlan(
|
|
2014
|
+
lane,
|
|
2015
|
+
contextless,
|
|
2016
|
+
this.aroundHooks.length > 0,
|
|
2017
|
+
ledgered !== undefined,
|
|
2018
|
+
fusedWeb,
|
|
2019
|
+
)
|
|
1905
2020
|
const entry: RouteEntry = {
|
|
1906
2021
|
// (context: never) => unknown -> InternalHandler: the framework invokes it
|
|
1907
2022
|
// with the concrete RawContext the typed handler expects, so this is sound.
|
|
@@ -1909,7 +2024,6 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
1909
2024
|
schema,
|
|
1910
2025
|
idempotent,
|
|
1911
2026
|
ledgered,
|
|
1912
|
-
fusedWeb,
|
|
1913
2027
|
derives: [...this.derives],
|
|
1914
2028
|
decorations: routeDecorations,
|
|
1915
2029
|
hasDecorations,
|
|
@@ -1917,35 +2031,14 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
1917
2031
|
afterHandle: [...this.afterHandleHooks],
|
|
1918
2032
|
onError: [...this.onErrorHooks],
|
|
1919
2033
|
around: [...this.aroundHooks],
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
// `routeAndRun`, while the inner validation/derive/before/after/onError lifecycle remains empty.
|
|
1923
|
-
// Most GET/no-schema routes qualify.
|
|
1924
|
-
bare,
|
|
1925
|
-
contextlessBare: bare && this.aroundHooks.length === 0 && isContextlessNoArgArrow(handler),
|
|
1926
|
-
bodyOnly:
|
|
1927
|
-
schema?.body !== undefined &&
|
|
1928
|
-
schema.query === undefined &&
|
|
1929
|
-
this.derives.length === 0 &&
|
|
1930
|
-
this.beforeHandleHooks.length === 0 &&
|
|
1931
|
-
this.afterHandleHooks.length === 0 &&
|
|
1932
|
-
this.onErrorHooks.length === 0,
|
|
1933
|
-
queryOnly:
|
|
1934
|
-
schema?.body === undefined &&
|
|
1935
|
-
schema?.query !== undefined &&
|
|
1936
|
-
this.derives.length === 0 &&
|
|
1937
|
-
this.beforeHandleHooks.length === 0 &&
|
|
1938
|
-
this.afterHandleHooks.length === 0 &&
|
|
1939
|
-
this.onErrorHooks.length === 0,
|
|
1940
|
-
}
|
|
1941
|
-
this.router.add(method, path, entry)
|
|
2034
|
+
execution,
|
|
2035
|
+
}
|
|
1942
2036
|
const descriptor: RouteDescriptor = {
|
|
1943
2037
|
method,
|
|
1944
2038
|
path,
|
|
1945
2039
|
schema,
|
|
1946
2040
|
...(capabilities.length > 0 ? { capabilities } : {}),
|
|
1947
2041
|
}
|
|
1948
|
-
this.routeList.push(descriptor)
|
|
1949
2042
|
const routeAssurance: AssuranceDeclaration[] = [...this.activeAssurance, ...handlerAssurance]
|
|
1950
2043
|
if (schema?.body !== undefined) {
|
|
1951
2044
|
routeAssurance.push(
|
|
@@ -1967,13 +2060,124 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
1967
2060
|
}),
|
|
1968
2061
|
)
|
|
1969
2062
|
}
|
|
1970
|
-
|
|
2063
|
+
return {
|
|
1971
2064
|
method,
|
|
1972
2065
|
path,
|
|
2066
|
+
pattern,
|
|
1973
2067
|
entry,
|
|
1974
2068
|
descriptor,
|
|
1975
2069
|
assurance: Object.freeze(routeAssurance),
|
|
1976
|
-
}
|
|
2070
|
+
}
|
|
2071
|
+
}
|
|
2072
|
+
|
|
2073
|
+
/** Collapse route-invariant lifecycle decisions into one runner at registration. The request path
|
|
2074
|
+
* performs no eligibility ladder: it supplies request state to this already-selected plan. */
|
|
2075
|
+
private compileExecutionPlan(
|
|
2076
|
+
lane: "bare" | "body" | "query" | "lifecycle",
|
|
2077
|
+
contextless: boolean,
|
|
2078
|
+
hasAround: boolean,
|
|
2079
|
+
hasLedger: boolean,
|
|
2080
|
+
fusedWeb: FusedWebRunner | undefined,
|
|
2081
|
+
): RouteExecutionPlan {
|
|
2082
|
+
if (contextless) {
|
|
2083
|
+
const run: RouteExecutionRunner = (
|
|
2084
|
+
runtime,
|
|
2085
|
+
entry,
|
|
2086
|
+
source,
|
|
2087
|
+
params,
|
|
2088
|
+
search,
|
|
2089
|
+
signal,
|
|
2090
|
+
budget,
|
|
2091
|
+
platform,
|
|
2092
|
+
finalize,
|
|
2093
|
+
wrapResponse,
|
|
2094
|
+
) =>
|
|
2095
|
+
runtime.runContextlessBare(
|
|
2096
|
+
entry,
|
|
2097
|
+
source,
|
|
2098
|
+
params,
|
|
2099
|
+
search,
|
|
2100
|
+
signal,
|
|
2101
|
+
budget,
|
|
2102
|
+
platform,
|
|
2103
|
+
finalize,
|
|
2104
|
+
wrapResponse,
|
|
2105
|
+
)
|
|
2106
|
+
return Object.freeze({ run, fusedWeb })
|
|
2107
|
+
}
|
|
2108
|
+
|
|
2109
|
+
let inner: ContextRouteRunner
|
|
2110
|
+
switch (lane) {
|
|
2111
|
+
case "bare":
|
|
2112
|
+
inner = (runtime, entry, _source, ctx, finalize, wrapResponse) =>
|
|
2113
|
+
runtime.runBare(entry, ctx, finalize, wrapResponse)
|
|
2114
|
+
break
|
|
2115
|
+
case "body":
|
|
2116
|
+
inner = (runtime, entry, source, ctx, finalize, wrapResponse) =>
|
|
2117
|
+
runtime.runBodyOnly(entry, source, ctx, finalize, wrapResponse)
|
|
2118
|
+
break
|
|
2119
|
+
case "query":
|
|
2120
|
+
inner = (runtime, entry, _source, ctx, finalize, wrapResponse) =>
|
|
2121
|
+
runtime.runQueryOnly(entry, ctx, finalize, wrapResponse)
|
|
2122
|
+
break
|
|
2123
|
+
default:
|
|
2124
|
+
inner = (runtime, entry, source, ctx, finalize, wrapResponse) =>
|
|
2125
|
+
runtime.runLifecycle(entry, source, ctx, finalize, wrapResponse)
|
|
2126
|
+
}
|
|
2127
|
+
const execute: ContextRouteRunner = hasAround
|
|
2128
|
+
? (runtime, entry, source, ctx, finalize, wrapResponse) =>
|
|
2129
|
+
runtime.runWithAround(
|
|
2130
|
+
entry,
|
|
2131
|
+
ctx,
|
|
2132
|
+
() => inner(runtime, entry, source, ctx, finalize, wrapResponse),
|
|
2133
|
+
finalize,
|
|
2134
|
+
wrapResponse,
|
|
2135
|
+
)
|
|
2136
|
+
: inner
|
|
2137
|
+
const run: RouteExecutionRunner = (
|
|
2138
|
+
runtime,
|
|
2139
|
+
entry,
|
|
2140
|
+
source,
|
|
2141
|
+
params,
|
|
2142
|
+
search,
|
|
2143
|
+
signal,
|
|
2144
|
+
budget,
|
|
2145
|
+
platform,
|
|
2146
|
+
finalize,
|
|
2147
|
+
wrapResponse,
|
|
2148
|
+
) => {
|
|
2149
|
+
const ctx = new RequestContext(
|
|
2150
|
+
source,
|
|
2151
|
+
params,
|
|
2152
|
+
search,
|
|
2153
|
+
signal,
|
|
2154
|
+
budget,
|
|
2155
|
+
platform,
|
|
2156
|
+
runtime.maxBodyBytes,
|
|
2157
|
+
)
|
|
2158
|
+
let ledger: RequestLedger | undefined
|
|
2159
|
+
if (hasLedger) {
|
|
2160
|
+
const resolved = entry.ledgered as ResolvedEffectLedger
|
|
2161
|
+
ledger = createRequestLedger({
|
|
2162
|
+
method: resolved.method,
|
|
2163
|
+
path: resolved.path,
|
|
2164
|
+
declared: resolved.declared,
|
|
2165
|
+
maxEntries: resolved.maxEntries,
|
|
2166
|
+
chain: resolved.chain,
|
|
2167
|
+
})
|
|
2168
|
+
attachEffectLedger(ctx, ledger)
|
|
2169
|
+
}
|
|
2170
|
+
let outcome = execute(runtime, entry, source, ctx, finalize, wrapResponse)
|
|
2171
|
+
if (ledger !== undefined) {
|
|
2172
|
+
const active = ledger
|
|
2173
|
+
const resolved = entry.ledgered as ResolvedEffectLedger
|
|
2174
|
+
outcome = (outcome instanceof Promise ? outcome : Promise.resolve(outcome)).then((value) =>
|
|
2175
|
+
runtime.settleEffectLedger(active, resolved, value),
|
|
2176
|
+
)
|
|
2177
|
+
}
|
|
2178
|
+
return outcome
|
|
2179
|
+
}
|
|
2180
|
+
return Object.freeze({ run, fusedWeb })
|
|
1977
2181
|
}
|
|
1978
2182
|
|
|
1979
2183
|
/** Resolve a route's declared idempotency into a pinned store + defaults, or `undefined` when off. */
|
|
@@ -2074,6 +2278,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
2074
2278
|
* those on the parent).
|
|
2075
2279
|
*/
|
|
2076
2280
|
merge<R2 extends Registry, Ctx2>(other: Server<R2, Ctx2>): Server<R & R2, Ctx> {
|
|
2281
|
+
this.assertConfigurable("merge()")
|
|
2077
2282
|
const source = other as unknown as Server<Registry, EmptyContext>
|
|
2078
2283
|
if (source.wsRouteCount > 0) {
|
|
2079
2284
|
throw new RouteConfigError(
|
|
@@ -2081,11 +2286,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
2081
2286
|
"merge() does not carry WebSocket routes — register .ws() routes on the parent server",
|
|
2082
2287
|
)
|
|
2083
2288
|
}
|
|
2084
|
-
|
|
2085
|
-
this.router.add(method, path, entry) // throws RouteConfigError on a duplicate — fail closed
|
|
2086
|
-
this.routeList.push(descriptor) // shared ref on purpose: carries later .tool() tagging
|
|
2087
|
-
this.rawEntries.push({ method, path, entry, descriptor, assurance })
|
|
2088
|
-
}
|
|
2289
|
+
this.catalog.addBatch(source.catalog.entries().map((route) => this.bindFusedRuntime(route)))
|
|
2089
2290
|
this.onRequestHooks.push(...source.onRequestHooks)
|
|
2090
2291
|
this.onResponseHooks.push(...source.onResponseHooks)
|
|
2091
2292
|
this.onResponseFinalizedHooks.push(...source.onResponseFinalizedHooks)
|
|
@@ -2095,6 +2296,25 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
2095
2296
|
return this as unknown as Server<R & R2, Ctx>
|
|
2096
2297
|
}
|
|
2097
2298
|
|
|
2299
|
+
/** A fused renderer closes over runtime services to keep its seven-argument JSC fast path. Merging
|
|
2300
|
+
* rebinds that closure once to the executing server; generic plans already receive the runtime. */
|
|
2301
|
+
private bindFusedRuntime(route: CatalogRoute): CatalogRoute {
|
|
2302
|
+
const { entry } = route
|
|
2303
|
+
if (entry.execution.fusedWeb === undefined) return route
|
|
2304
|
+
const fusedWeb = this.buildFusedWeb(
|
|
2305
|
+
entry.handler,
|
|
2306
|
+
entry.hasDecorations ? entry.decorations : undefined,
|
|
2307
|
+
isContextlessNoArgArrow(entry.handler),
|
|
2308
|
+
)
|
|
2309
|
+
return {
|
|
2310
|
+
...route,
|
|
2311
|
+
entry: {
|
|
2312
|
+
...entry,
|
|
2313
|
+
execution: Object.freeze({ ...entry.execution, fusedWeb }),
|
|
2314
|
+
},
|
|
2315
|
+
}
|
|
2316
|
+
}
|
|
2317
|
+
|
|
2098
2318
|
/**
|
|
2099
2319
|
* Enumerate the registered routes (method, path, input schemas), in registration
|
|
2100
2320
|
* order. Powers `toOpenAPI` and other introspection; the router trie itself no
|
|
@@ -2102,10 +2322,11 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
2102
2322
|
*/
|
|
2103
2323
|
routes(): ReadonlyArray<RouteDescriptor> {
|
|
2104
2324
|
if (this.activeAssurance.length === 0 && this.globalAssurance.length === 0) {
|
|
2105
|
-
|
|
2106
|
-
|
|
2325
|
+
if (!this.catalog.hasAssurance()) {
|
|
2326
|
+
return this.catalog.routeDescriptors()
|
|
2327
|
+
}
|
|
2107
2328
|
}
|
|
2108
|
-
return this.
|
|
2329
|
+
return this.catalog.entries().map(({ method, path, descriptor, assurance }) => {
|
|
2109
2330
|
const effective = assuranceEvidenceFor([...assurance, ...this.globalAssurance], method, path)
|
|
2110
2331
|
return effective.length > 0 ? { ...descriptor, assurance: effective } : descriptor
|
|
2111
2332
|
})
|
|
@@ -2136,7 +2357,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
2136
2357
|
platform?: Platform<EnvOf<Ctx>>,
|
|
2137
2358
|
): MaybePromise<Response> {
|
|
2138
2359
|
// Non-`async` on purpose: `dispatch` may return a `Response` *synchronously* (the bare-route fast
|
|
2139
|
-
// path,
|
|
2360
|
+
// path, selected by the compiled execution plan), and an `async fetch` would wrap every such result in a redundant
|
|
2140
2361
|
// promise + microtask. Returning `Response | Promise<Response>` matches Web/edge handlers, while
|
|
2141
2362
|
// `await app.fetch(...)` continues to work exactly as before.
|
|
2142
2363
|
const outcome = this.dispatch<Response>(
|
|
@@ -2262,7 +2483,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
2262
2483
|
? response.then((settled) => ({ kind: "response", response: settled }))
|
|
2263
2484
|
: { kind: "response", response }
|
|
2264
2485
|
}
|
|
2265
|
-
// May resolve **synchronously** for a bare route + sync handler
|
|
2486
|
+
// May resolve **synchronously** for a compiled bare route + sync handler - the `@nifrajs/node`
|
|
2266
2487
|
// adapter `await`s the result, so it transparently handles either; the sync case allocates no promise
|
|
2267
2488
|
// at all on the Node hot path.
|
|
2268
2489
|
return this.dispatch<NodeServeOutcome>(
|
|
@@ -2294,7 +2515,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
2294
2515
|
if (!match.found) return WS_PASS // upgrade header, no WS route here → normal routing decides
|
|
2295
2516
|
// Inspect only captured values for escapes. Scanning the full pathname repeated work the router
|
|
2296
2517
|
// already did and made every plain dynamic route pay for unrelated static path bytes.
|
|
2297
|
-
const params = match.params === EMPTY_PARAMS ? match.params :
|
|
2518
|
+
const params = match.params === EMPTY_PARAMS ? match.params : decodeRouteParams(match.params)
|
|
2298
2519
|
if (params === null) return { kind: "reject", response: jsonError(400, "malformed_path") }
|
|
2299
2520
|
const handler = match.payload.handler
|
|
2300
2521
|
// Non-null: wsRouteCount > 0 ⇒ ws() ran ⇒ the runtime created the registry.
|
|
@@ -2475,7 +2696,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
2475
2696
|
|
|
2476
2697
|
/**
|
|
2477
2698
|
* Route → build context → run. Synchronous through to the handler for a **bare** route
|
|
2478
|
-
* (
|
|
2699
|
+
* (selected by its compiled plan), so a sync handler produces its result with zero promise allocations;
|
|
2479
2700
|
* routes with validation/hooks keep the full async {@link runLifecycle}, unchanged.
|
|
2480
2701
|
*/
|
|
2481
2702
|
private routeAndRun<T>(
|
|
@@ -2487,7 +2708,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
2487
2708
|
webFast: boolean,
|
|
2488
2709
|
): MaybePromise<T> {
|
|
2489
2710
|
const url = urlPartsOf(source.url)
|
|
2490
|
-
const match = this.
|
|
2711
|
+
const match = this.catalog.find(source.method, url.pathname)
|
|
2491
2712
|
if (!match.found) {
|
|
2492
2713
|
if (match.reason === "method-not-allowed") {
|
|
2493
2714
|
return wrapResponse(
|
|
@@ -2499,7 +2720,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
2499
2720
|
|
|
2500
2721
|
// Inspect only captured values for escapes. Scanning the full pathname repeated work the router
|
|
2501
2722
|
// already did and made every plain dynamic route pay for unrelated static path bytes.
|
|
2502
|
-
const params = match.params === EMPTY_PARAMS ? match.params :
|
|
2723
|
+
const params = match.params === EMPTY_PARAMS ? match.params : decodeRouteParams(match.params)
|
|
2503
2724
|
if (params === null) {
|
|
2504
2725
|
return wrapResponse(jsonError(400, "malformed_path"))
|
|
2505
2726
|
}
|
|
@@ -2671,7 +2892,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
2671
2892
|
return wrapResponse(response)
|
|
2672
2893
|
}
|
|
2673
2894
|
|
|
2674
|
-
/**
|
|
2895
|
+
/** Supply request-specific deadline state to the route's precompiled execution plan. */
|
|
2675
2896
|
private runMatchedLanes<T>(
|
|
2676
2897
|
source: RequestSource,
|
|
2677
2898
|
platform: Platform | undefined,
|
|
@@ -2717,95 +2938,30 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
2717
2938
|
controller === undefined
|
|
2718
2939
|
? getUnboundedRequestBudget()
|
|
2719
2940
|
: createRequestBudget({ deadline: admission!.deadline as number, signal })
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
finalize,
|
|
2745
|
-
wrapResponse,
|
|
2746
|
-
)
|
|
2747
|
-
} else {
|
|
2748
|
-
// Build the context up front so `onError` always has it. Query, cookies, headers, and body helpers
|
|
2749
|
-
// stay lazy, but their accessors live on the prototype instead of allocating per-request closures.
|
|
2750
|
-
const ctx = new RequestContext(
|
|
2751
|
-
source,
|
|
2752
|
-
params,
|
|
2753
|
-
search,
|
|
2754
|
-
signal,
|
|
2755
|
-
budget,
|
|
2756
|
-
platform,
|
|
2757
|
-
this.maxBodyBytes,
|
|
2758
|
-
)
|
|
2759
|
-
// Ledgered routes (capabilities + effectLedger) carry a bounded per-request ledger; the
|
|
2760
|
-
// `useCapability` beacon appends to it, and the settle step below seals + sinks it. The route
|
|
2761
|
-
// was forced off the fused/contextless lanes at registration, so this branch always runs.
|
|
2762
|
-
let ledger: RequestLedger | undefined
|
|
2763
|
-
if (entry.ledgered !== undefined) {
|
|
2764
|
-
ledger = createRequestLedger({
|
|
2765
|
-
method: entry.ledgered.method,
|
|
2766
|
-
path: entry.ledgered.path,
|
|
2767
|
-
declared: entry.ledgered.declared,
|
|
2768
|
-
maxEntries: entry.ledgered.maxEntries,
|
|
2769
|
-
chain: entry.ledgered.chain,
|
|
2770
|
-
})
|
|
2771
|
-
attachEffectLedger(ctx, ledger)
|
|
2772
|
-
}
|
|
2773
|
-
|
|
2774
|
-
if (entry.around.length === 0) {
|
|
2775
|
-
outcome = entry.bare
|
|
2776
|
-
? this.runBare(entry, ctx, finalize, wrapResponse)
|
|
2777
|
-
: entry.bodyOnly
|
|
2778
|
-
? this.runBodyOnly(entry, source, ctx, finalize, wrapResponse)
|
|
2779
|
-
: entry.queryOnly
|
|
2780
|
-
? this.runQueryOnly(entry, ctx, finalize, wrapResponse)
|
|
2781
|
-
: this.runLifecycle(entry, source, ctx, finalize, wrapResponse)
|
|
2782
|
-
} else {
|
|
2783
|
-
outcome = this.runWithAround(
|
|
2784
|
-
entry,
|
|
2785
|
-
ctx,
|
|
2786
|
-
() =>
|
|
2787
|
-
entry.bare
|
|
2788
|
-
? this.runBare(entry, ctx, finalize, wrapResponse)
|
|
2789
|
-
: entry.bodyOnly
|
|
2790
|
-
? this.runBodyOnly(entry, source, ctx, finalize, wrapResponse)
|
|
2791
|
-
: entry.queryOnly
|
|
2792
|
-
? this.runQueryOnly(entry, ctx, finalize, wrapResponse)
|
|
2793
|
-
: this.runLifecycle(entry, source, ctx, finalize, wrapResponse),
|
|
2794
|
-
finalize,
|
|
2795
|
-
wrapResponse,
|
|
2796
|
-
)
|
|
2797
|
-
}
|
|
2798
|
-
// Settle step: seal the ledger and hand it to the sink BEFORE the response is released, on
|
|
2799
|
-
// success and error responses alike (partial work is exactly what an audit must capture). The
|
|
2800
|
-
// lanes above resolve handler errors to responses, so a plain resolve arm is sufficient.
|
|
2801
|
-
if (ledger !== undefined) {
|
|
2802
|
-
const resolved = entry.ledgered as ResolvedEffectLedger
|
|
2803
|
-
const active = ledger
|
|
2804
|
-
outcome = (outcome instanceof Promise ? outcome : Promise.resolve(outcome)).then((value) =>
|
|
2805
|
-
this.settleEffectLedger(active, resolved, value),
|
|
2806
|
-
)
|
|
2807
|
-
}
|
|
2808
|
-
}
|
|
2941
|
+
const plan = entry.execution
|
|
2942
|
+
const outcome: MaybePromise<T> =
|
|
2943
|
+
webFast && plan.fusedWeb !== undefined
|
|
2944
|
+
? (plan.fusedWeb(
|
|
2945
|
+
source,
|
|
2946
|
+
params,
|
|
2947
|
+
search,
|
|
2948
|
+
signal,
|
|
2949
|
+
budget,
|
|
2950
|
+
platform,
|
|
2951
|
+
false,
|
|
2952
|
+
) as MaybePromise<T>)
|
|
2953
|
+
: plan.run(
|
|
2954
|
+
this,
|
|
2955
|
+
entry,
|
|
2956
|
+
source,
|
|
2957
|
+
params,
|
|
2958
|
+
search,
|
|
2959
|
+
signal,
|
|
2960
|
+
budget,
|
|
2961
|
+
platform,
|
|
2962
|
+
finalize,
|
|
2963
|
+
wrapResponse,
|
|
2964
|
+
)
|
|
2809
2965
|
// The request timeout only bounds work that is actually pending — a synchronous (bare) result is
|
|
2810
2966
|
// already complete and can't time out, so it's returned as-is (no 503 race, no promise).
|
|
2811
2967
|
if (controller !== undefined && outcome instanceof Promise) {
|
|
@@ -2923,7 +3079,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
2923
3079
|
}
|
|
2924
3080
|
|
|
2925
3081
|
/**
|
|
2926
|
-
* The synchronous fast path
|
|
3082
|
+
* The synchronous fast path selected by a route's execution plan: apply static decorations, call the
|
|
2927
3083
|
* handler, render the result — **no `await`** unless the handler itself returns a promise. It mirrors
|
|
2928
3084
|
* the bare slice of {@link runLifecycle} (which a bare route would otherwise no-op through) and shares
|
|
2929
3085
|
* {@link logRequestError}; a bare route has no `onError` hooks, so error handling is fully synchronous
|
|
@@ -2956,7 +3112,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
2956
3112
|
/** Bare-route error rendering — identical to {@link runLifecycle}'s catch minus the (absent) onError
|
|
2957
3113
|
* loop: a thrown `Response` is returned as deliberate control flow; anything else is logged + 500. */
|
|
2958
3114
|
/**
|
|
2959
|
-
* Build a route's fused Web
|
|
3115
|
+
* Build a route's fused Web renderer. Composition happens once at
|
|
2960
3116
|
* registration; the returned closure is what every request to the route runs. Behavior is
|
|
2961
3117
|
* byte-identical to the generic `runBare`/`runContextlessBare` + `toResponse` pair — same
|
|
2962
3118
|
* decoration order, same error routing (thrown `Response` = control flow; anything else logs and
|
|
@@ -2967,7 +3123,6 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
2967
3123
|
decorations: Record<PropertyKey, unknown> | undefined,
|
|
2968
3124
|
contextless: boolean,
|
|
2969
3125
|
): FusedWebRunner {
|
|
2970
|
-
const maxBodyBytes = this.maxBodyBytes
|
|
2971
3126
|
const logError = (err: unknown, ctx: RawContext): Response => {
|
|
2972
3127
|
if (err instanceof Response) return err
|
|
2973
3128
|
this.logRequestError(err, ctx)
|
|
@@ -2985,8 +3140,16 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
2985
3140
|
return logError(
|
|
2986
3141
|
err,
|
|
2987
3142
|
nativeContext
|
|
2988
|
-
? RequestContext.native(source, params, maxBodyBytes)
|
|
2989
|
-
: new RequestContext(
|
|
3143
|
+
? RequestContext.native(source, params, this.maxBodyBytes)
|
|
3144
|
+
: new RequestContext(
|
|
3145
|
+
source,
|
|
3146
|
+
params,
|
|
3147
|
+
search,
|
|
3148
|
+
signal,
|
|
3149
|
+
budget,
|
|
3150
|
+
platform,
|
|
3151
|
+
this.maxBodyBytes,
|
|
3152
|
+
),
|
|
2990
3153
|
)
|
|
2991
3154
|
}
|
|
2992
3155
|
if (result instanceof Promise) {
|
|
@@ -2994,7 +3157,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
2994
3157
|
logError(
|
|
2995
3158
|
err,
|
|
2996
3159
|
nativeContext
|
|
2997
|
-
? RequestContext.native(source, params, maxBodyBytes)
|
|
3160
|
+
? RequestContext.native(source, params, this.maxBodyBytes)
|
|
2998
3161
|
: new RequestContext(
|
|
2999
3162
|
source,
|
|
3000
3163
|
params,
|
|
@@ -3002,7 +3165,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
3002
3165
|
signal,
|
|
3003
3166
|
budget,
|
|
3004
3167
|
platform,
|
|
3005
|
-
maxBodyBytes,
|
|
3168
|
+
this.maxBodyBytes,
|
|
3006
3169
|
),
|
|
3007
3170
|
),
|
|
3008
3171
|
)
|
|
@@ -3012,8 +3175,8 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
3012
3175
|
}
|
|
3013
3176
|
return (source, params, search, signal, budget, platform, nativeContext) => {
|
|
3014
3177
|
const ctx = nativeContext
|
|
3015
|
-
? RequestContext.native(source, params, maxBodyBytes)
|
|
3016
|
-
: new RequestContext(source, params, search, signal, budget, platform, maxBodyBytes)
|
|
3178
|
+
? RequestContext.native(source, params, this.maxBodyBytes)
|
|
3179
|
+
: new RequestContext(source, params, search, signal, budget, platform, this.maxBodyBytes)
|
|
3017
3180
|
if (decorations !== undefined) Object.assign(ctx, decorations)
|
|
3018
3181
|
let result: unknown
|
|
3019
3182
|
try {
|
|
@@ -3488,7 +3651,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
3488
3651
|
): Promise<T> {
|
|
3489
3652
|
try {
|
|
3490
3653
|
if (entry.schema?.body !== undefined) {
|
|
3491
|
-
const bodyError = await this.readAndValidateBody(source, entry
|
|
3654
|
+
const bodyError = await this.readAndValidateBody(source, entry, ctx)
|
|
3492
3655
|
if (bodyError !== undefined) return wrapResponse(bodyError)
|
|
3493
3656
|
}
|
|
3494
3657
|
if (entry.schema?.query !== undefined) {
|
|
@@ -3499,8 +3662,8 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
3499
3662
|
queryObjectOf(ctx[CONTEXT_SEARCH]),
|
|
3500
3663
|
)
|
|
3501
3664
|
const result = validation instanceof Promise ? await validation : validation
|
|
3502
|
-
|
|
3503
|
-
|
|
3665
|
+
const queryError = await this.applyLifecycleValidation(entry, result, ctx, "query")
|
|
3666
|
+
if (queryError !== undefined) return wrapResponse(queryError)
|
|
3504
3667
|
}
|
|
3505
3668
|
|
|
3506
3669
|
// Context extensions: static decorations, then per-request derives.
|
|
@@ -3581,7 +3744,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
3581
3744
|
|
|
3582
3745
|
private async readAndValidateBody(
|
|
3583
3746
|
req: RequestSource,
|
|
3584
|
-
|
|
3747
|
+
entry: RouteEntry,
|
|
3585
3748
|
ctx: RawContext,
|
|
3586
3749
|
): Promise<Response | undefined> {
|
|
3587
3750
|
const contentType = headerOf(req, "content-type") ?? ""
|
|
@@ -3599,10 +3762,36 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
3599
3762
|
// fit a value schema; use a schema-less route + @nifrajs/uploads helpers for those.
|
|
3600
3763
|
return jsonError(415, "unsupported_media_type")
|
|
3601
3764
|
}
|
|
3602
|
-
const validation = schema["~standard"].validate(parsed)
|
|
3765
|
+
const validation = entry.schema!.body!["~standard"].validate(parsed)
|
|
3603
3766
|
const result = validation instanceof Promise ? await validation : validation
|
|
3604
|
-
|
|
3605
|
-
|
|
3767
|
+
return this.applyLifecycleValidation(entry, result, ctx, "body")
|
|
3768
|
+
}
|
|
3769
|
+
|
|
3770
|
+
/** Apply validation and its recovery hook on the generic lifecycle lane. Recovery is completed
|
|
3771
|
+
* before derives/beforeHandle run, matching the body-only and query-only execution lanes. */
|
|
3772
|
+
private async applyLifecycleValidation(
|
|
3773
|
+
entry: RouteEntry,
|
|
3774
|
+
result: StandardResult<unknown>,
|
|
3775
|
+
ctx: RawContext,
|
|
3776
|
+
kind: "body" | "query",
|
|
3777
|
+
): Promise<Response | undefined> {
|
|
3778
|
+
if (result.issues === undefined) {
|
|
3779
|
+
if (kind === "body") ctx.body = result.value
|
|
3780
|
+
else ctx.query = result.value
|
|
3781
|
+
return undefined
|
|
3782
|
+
}
|
|
3783
|
+
const hook = entry.schema?.onValidationError ?? this.defaultOnValidationError
|
|
3784
|
+
if (hook === undefined) return validationError(result.issues)
|
|
3785
|
+
const attempted = hook(result.issues, ctx as unknown as Context, kind)
|
|
3786
|
+
const recovery = attempted instanceof Promise ? await attempted : attempted
|
|
3787
|
+
if (recovery === undefined) return validationError(result.issues)
|
|
3788
|
+
if (recovery instanceof Response) return recovery
|
|
3789
|
+
const schema = kind === "body" ? entry.schema?.body : entry.schema?.query
|
|
3790
|
+
const retried = schema!["~standard"].validate(recovery)
|
|
3791
|
+
const settled = retried instanceof Promise ? await retried : retried
|
|
3792
|
+
if (settled.issues !== undefined) return validationError(settled.issues)
|
|
3793
|
+
if (kind === "body") ctx.body = settled.value
|
|
3794
|
+
else ctx.query = settled.value
|
|
3606
3795
|
return undefined
|
|
3607
3796
|
}
|
|
3608
3797
|
|
|
@@ -3624,6 +3813,55 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
3624
3813
|
return readBoundedJsonSource(req, this.maxBodyBytes)
|
|
3625
3814
|
}
|
|
3626
3815
|
|
|
3816
|
+
/** Adapt one route's compiled execution plan to Bun's already-matched request shape. Route
|
|
3817
|
+
* semantics remain in the plan; this closure only supplies native params and deadline fallback. */
|
|
3818
|
+
private compileBunNativeHandler(
|
|
3819
|
+
entry: RouteEntry,
|
|
3820
|
+
paramNames: readonly string[],
|
|
3821
|
+
fused: FusedWebRunner | undefined,
|
|
3822
|
+
signal: AbortSignal | undefined,
|
|
3823
|
+
budget: RequestBudget | undefined,
|
|
3824
|
+
): BunNativeHandler {
|
|
3825
|
+
if (paramNames.length === 0) {
|
|
3826
|
+
if (fused === undefined) {
|
|
3827
|
+
return (request) => this.fetchMatched(request, entry, EMPTY_PARAMS)
|
|
3828
|
+
}
|
|
3829
|
+
if (this.acceptInboundDeadlines) {
|
|
3830
|
+
return (request) =>
|
|
3831
|
+
request.headers.get(NIFRA_DEADLINE_HEADER) !== null
|
|
3832
|
+
? this.fetchMatched(request, entry, EMPTY_PARAMS)
|
|
3833
|
+
: fused(request, EMPTY_PARAMS, undefined, signal!, budget!, undefined, true)
|
|
3834
|
+
}
|
|
3835
|
+
return (request) => fused(request, EMPTY_PARAMS, undefined, signal!, budget!, undefined, true)
|
|
3836
|
+
}
|
|
3837
|
+
|
|
3838
|
+
const malformed =
|
|
3839
|
+
paramNames.length === 1
|
|
3840
|
+
? (params: Record<string, string>) => params[paramNames[0]!]?.includes("\uFFFD") === true
|
|
3841
|
+
: hasReplacementParam
|
|
3842
|
+
if (fused === undefined) {
|
|
3843
|
+
return (request) => {
|
|
3844
|
+
const params = (request as BunRequestWithParams).params ?? EMPTY_PARAMS
|
|
3845
|
+
if (malformed(params)) return this.fetchSource(request)
|
|
3846
|
+
return this.fetchMatched(request, entry, params)
|
|
3847
|
+
}
|
|
3848
|
+
}
|
|
3849
|
+
if (this.acceptInboundDeadlines) {
|
|
3850
|
+
return (request) => {
|
|
3851
|
+
const params = (request as BunRequestWithParams).params ?? EMPTY_PARAMS
|
|
3852
|
+
if (malformed(params)) return this.fetchSource(request)
|
|
3853
|
+
return request.headers.get(NIFRA_DEADLINE_HEADER) !== null
|
|
3854
|
+
? this.fetchMatched(request, entry, params)
|
|
3855
|
+
: fused(request, params, undefined, signal!, budget!, undefined, true)
|
|
3856
|
+
}
|
|
3857
|
+
}
|
|
3858
|
+
return (request) => {
|
|
3859
|
+
const params = (request as BunRequestWithParams).params ?? EMPTY_PARAMS
|
|
3860
|
+
if (malformed(params)) return this.fetchSource(request)
|
|
3861
|
+
return fused(request, params, undefined, signal!, budget!, undefined, true)
|
|
3862
|
+
}
|
|
3863
|
+
}
|
|
3864
|
+
|
|
3627
3865
|
/** Compile portable route registrations into Bun's native route table. Apps with request-rewrite
|
|
3628
3866
|
* hooks or WebSockets retain the single portable dispatcher because those features must run before
|
|
3629
3867
|
* route selection/upgrade. Named wildcards also stay on the fallback until Bun exposes their raw
|
|
@@ -3642,143 +3880,22 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
3642
3880
|
const unboundedSignal = mayUseFusedNative ? getNeverAbortSignal() : undefined
|
|
3643
3881
|
const unboundedBudget = mayUseFusedNative ? getUnboundedRequestBudget() : undefined
|
|
3644
3882
|
let count = 0
|
|
3645
|
-
for (const { method, path, entry } of this.
|
|
3646
|
-
if (
|
|
3883
|
+
for (const { method, path, pattern, entry } of this.catalog.entries()) {
|
|
3884
|
+
if (pattern.segments.some((segment) => segment.kind === "wildcard")) continue
|
|
3647
3885
|
let methods = routes[path]
|
|
3648
3886
|
if (methods === undefined) {
|
|
3649
3887
|
methods = Object.create(null) as BunNativeMethodTable
|
|
3650
3888
|
routes[path] = methods
|
|
3651
3889
|
}
|
|
3652
|
-
const paramNames =
|
|
3653
|
-
|
|
3654
|
-
|
|
3655
|
-
|
|
3656
|
-
|
|
3657
|
-
|
|
3658
|
-
|
|
3659
|
-
|
|
3660
|
-
|
|
3661
|
-
// handler + Response.json; retaining that route-invariant branch cost ~7% on a dynamic GET.
|
|
3662
|
-
if (paramNames.length === 0) {
|
|
3663
|
-
methods[method] = this.acceptInboundDeadlines
|
|
3664
|
-
? (request) =>
|
|
3665
|
-
request.headers.get(NIFRA_DEADLINE_HEADER) !== null
|
|
3666
|
-
? this.fetchMatched(request, entry, EMPTY_PARAMS)
|
|
3667
|
-
: fused(
|
|
3668
|
-
request,
|
|
3669
|
-
EMPTY_PARAMS,
|
|
3670
|
-
undefined,
|
|
3671
|
-
unboundedSignal as AbortSignal,
|
|
3672
|
-
unboundedBudget as RequestBudget,
|
|
3673
|
-
undefined,
|
|
3674
|
-
true,
|
|
3675
|
-
)
|
|
3676
|
-
: (request) =>
|
|
3677
|
-
fused(
|
|
3678
|
-
request,
|
|
3679
|
-
EMPTY_PARAMS,
|
|
3680
|
-
undefined,
|
|
3681
|
-
unboundedSignal as AbortSignal,
|
|
3682
|
-
unboundedBudget as RequestBudget,
|
|
3683
|
-
undefined,
|
|
3684
|
-
true,
|
|
3685
|
-
)
|
|
3686
|
-
} else if (paramNames.length === 1) {
|
|
3687
|
-
const paramName = paramNames[0]!
|
|
3688
|
-
const handler = entry.handler
|
|
3689
|
-
const decorations = entry.hasDecorations ? entry.decorations : undefined
|
|
3690
|
-
const fail = (error: unknown, ctx: RawContext): Response => {
|
|
3691
|
-
if (error instanceof Response) return error
|
|
3692
|
-
this.logRequestError(error, ctx)
|
|
3693
|
-
return jsonError(500, "internal_error")
|
|
3694
|
-
}
|
|
3695
|
-
methods[method] = this.acceptInboundDeadlines
|
|
3696
|
-
? (request) => {
|
|
3697
|
-
const params = (request as BunRequestWithParams).params ?? EMPTY_PARAMS
|
|
3698
|
-
if (params[paramName]?.includes("\uFFFD") === true) return this.fetchSource(request)
|
|
3699
|
-
if (request.headers.get(NIFRA_DEADLINE_HEADER) !== null) {
|
|
3700
|
-
return this.fetchMatched(request, entry, params)
|
|
3701
|
-
}
|
|
3702
|
-
const ctx = RequestContext.native(request, params, this.maxBodyBytes)
|
|
3703
|
-
if (decorations !== undefined) Object.assign(ctx, decorations)
|
|
3704
|
-
let result: unknown
|
|
3705
|
-
try {
|
|
3706
|
-
result = handler(ctx)
|
|
3707
|
-
} catch (error) {
|
|
3708
|
-
return fail(error, ctx)
|
|
3709
|
-
}
|
|
3710
|
-
return result instanceof Promise
|
|
3711
|
-
? result.then(
|
|
3712
|
-
(value) => fusedRespond(value, ctx),
|
|
3713
|
-
(error) => fail(error, ctx),
|
|
3714
|
-
)
|
|
3715
|
-
: fusedRespond(result, ctx)
|
|
3716
|
-
}
|
|
3717
|
-
: (request) => {
|
|
3718
|
-
const params = (request as BunRequestWithParams).params ?? EMPTY_PARAMS
|
|
3719
|
-
if (params[paramName]?.includes("\uFFFD") === true) return this.fetchSource(request)
|
|
3720
|
-
const ctx = RequestContext.native(request, params, this.maxBodyBytes)
|
|
3721
|
-
if (decorations !== undefined) Object.assign(ctx, decorations)
|
|
3722
|
-
let result: unknown
|
|
3723
|
-
try {
|
|
3724
|
-
result = handler(ctx)
|
|
3725
|
-
} catch (error) {
|
|
3726
|
-
return fail(error, ctx)
|
|
3727
|
-
}
|
|
3728
|
-
return result instanceof Promise
|
|
3729
|
-
? result.then(
|
|
3730
|
-
(value) => fusedRespond(value, ctx),
|
|
3731
|
-
(error) => fail(error, ctx),
|
|
3732
|
-
)
|
|
3733
|
-
: fusedRespond(result, ctx)
|
|
3734
|
-
}
|
|
3735
|
-
} else {
|
|
3736
|
-
methods[method] = this.acceptInboundDeadlines
|
|
3737
|
-
? (request) => {
|
|
3738
|
-
const params = (request as BunRequestWithParams).params ?? EMPTY_PARAMS
|
|
3739
|
-
if (hasReplacementParam(params)) return this.fetchSource(request)
|
|
3740
|
-
return request.headers.get(NIFRA_DEADLINE_HEADER) !== null
|
|
3741
|
-
? this.fetchMatched(request, entry, params)
|
|
3742
|
-
: fused(
|
|
3743
|
-
request,
|
|
3744
|
-
params,
|
|
3745
|
-
undefined,
|
|
3746
|
-
unboundedSignal as AbortSignal,
|
|
3747
|
-
unboundedBudget as RequestBudget,
|
|
3748
|
-
undefined,
|
|
3749
|
-
true,
|
|
3750
|
-
)
|
|
3751
|
-
}
|
|
3752
|
-
: (request) => {
|
|
3753
|
-
const params = (request as BunRequestWithParams).params ?? EMPTY_PARAMS
|
|
3754
|
-
if (hasReplacementParam(params)) return this.fetchSource(request)
|
|
3755
|
-
return fused(
|
|
3756
|
-
request,
|
|
3757
|
-
params,
|
|
3758
|
-
undefined,
|
|
3759
|
-
unboundedSignal as AbortSignal,
|
|
3760
|
-
unboundedBudget as RequestBudget,
|
|
3761
|
-
undefined,
|
|
3762
|
-
true,
|
|
3763
|
-
)
|
|
3764
|
-
}
|
|
3765
|
-
}
|
|
3766
|
-
} else if (paramNames.length === 0) {
|
|
3767
|
-
methods[method] = (request) => this.fetchMatched(request, entry, EMPTY_PARAMS)
|
|
3768
|
-
} else if (paramNames.length === 1) {
|
|
3769
|
-
const paramName = paramNames[0]!
|
|
3770
|
-
methods[method] = (request) => {
|
|
3771
|
-
const params = (request as BunRequestWithParams).params ?? EMPTY_PARAMS
|
|
3772
|
-
if (params[paramName]?.includes("\uFFFD") === true) return this.fetchSource(request)
|
|
3773
|
-
return this.fetchMatched(request, entry, params)
|
|
3774
|
-
}
|
|
3775
|
-
} else {
|
|
3776
|
-
methods[method] = (request) => {
|
|
3777
|
-
const params = (request as BunRequestWithParams).params ?? EMPTY_PARAMS
|
|
3778
|
-
if (hasReplacementParam(params)) return this.fetchSource(request)
|
|
3779
|
-
return this.fetchMatched(request, entry, params)
|
|
3780
|
-
}
|
|
3781
|
-
}
|
|
3890
|
+
const paramNames = pattern.paramNames
|
|
3891
|
+
const fused = mayUseFusedNative ? entry.execution.fusedWeb : undefined
|
|
3892
|
+
methods[method] = this.compileBunNativeHandler(
|
|
3893
|
+
entry,
|
|
3894
|
+
paramNames,
|
|
3895
|
+
fused,
|
|
3896
|
+
unboundedSignal,
|
|
3897
|
+
unboundedBudget,
|
|
3898
|
+
)
|
|
3782
3899
|
count += 1
|
|
3783
3900
|
}
|
|
3784
3901
|
return count === 0 ? undefined : routes
|
|
@@ -3858,6 +3975,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
3858
3975
|
},
|
|
3859
3976
|
})) as unknown as RunningServer
|
|
3860
3977
|
this.bunServer = running
|
|
3978
|
+
this.sealed = true
|
|
3861
3979
|
if (this.gracefulSignals) this.installSignalHandlers()
|
|
3862
3980
|
return running
|
|
3863
3981
|
}
|