@znight/http 0.0.0-stage → 0.1.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.
Files changed (88) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +2333 -2
  3. package/dist/adapter-CGSceVEN.d.mts +876 -0
  4. package/dist/compiler.d.mts +156 -0
  5. package/dist/compiler.mjs +2180 -0
  6. package/dist/compiler.mjs.map +1 -0
  7. package/dist/express.d.mts +39 -0
  8. package/dist/express.mjs +135 -0
  9. package/dist/express.mjs.map +1 -0
  10. package/dist/fastify.d.mts +33 -0
  11. package/dist/fastify.mjs +191 -0
  12. package/dist/fastify.mjs.map +1 -0
  13. package/dist/hono.d.mts +49 -0
  14. package/dist/hono.mjs +319 -0
  15. package/dist/hono.mjs.map +1 -0
  16. package/dist/index.d.mts +1254 -0
  17. package/dist/index.mjs +5618 -0
  18. package/dist/index.mjs.map +1 -0
  19. package/dist/node-BUnkdSsa.mjs +183 -0
  20. package/dist/node-BUnkdSsa.mjs.map +1 -0
  21. package/dist/node-io-BY1ZDLRQ.mjs +1566 -0
  22. package/dist/node-io-BY1ZDLRQ.mjs.map +1 -0
  23. package/dist/node.d.mts +30 -0
  24. package/dist/node.mjs +1 -0
  25. package/dist/path-JI87C4k8.d.mts +109 -0
  26. package/dist/path.d.mts +2 -0
  27. package/dist/path.mjs +245 -0
  28. package/dist/path.mjs.map +1 -0
  29. package/dist/size-D8rsQcV0.mjs +184 -0
  30. package/dist/size-D8rsQcV0.mjs.map +1 -0
  31. package/dist/utils-DCbu3a4w.mjs +27 -0
  32. package/dist/utils-DCbu3a4w.mjs.map +1 -0
  33. package/package.json +142 -4
  34. package/src/adapter.ts +240 -0
  35. package/src/adapters/express.ts +201 -0
  36. package/src/adapters/fastify.ts +280 -0
  37. package/src/adapters/hono.ts +488 -0
  38. package/src/adapters/node-io.ts +368 -0
  39. package/src/adapters/node.ts +135 -0
  40. package/src/adapters/shared.ts +508 -0
  41. package/src/body.ts +135 -0
  42. package/src/coerce.ts +195 -0
  43. package/src/compiler/analyze.ts +1513 -0
  44. package/src/compiler/ast.ts +90 -0
  45. package/src/compiler/coercion.ts +267 -0
  46. package/src/compiler/generate.ts +374 -0
  47. package/src/compiler/index.ts +76 -0
  48. package/src/compiler/model.ts +260 -0
  49. package/src/compiler/route-decorators.ts +405 -0
  50. package/src/compiler/validate.ts +288 -0
  51. package/src/context.ts +533 -0
  52. package/src/cookie-settings.ts +68 -0
  53. package/src/cookie-signer.ts +106 -0
  54. package/src/cookies.ts +726 -0
  55. package/src/cors.ts +349 -0
  56. package/src/decisions.ts +331 -0
  57. package/src/decorators.ts +486 -0
  58. package/src/event-channel.ts +197 -0
  59. package/src/exceptions.ts +257 -0
  60. package/src/files.ts +429 -0
  61. package/src/headers.ts +39 -0
  62. package/src/index.ts +185 -0
  63. package/src/inject-sse.ts +309 -0
  64. package/src/interfaces.ts +101 -0
  65. package/src/metadata.ts +162 -0
  66. package/src/mime.ts +157 -0
  67. package/src/multipart.ts +597 -0
  68. package/src/option-error.ts +31 -0
  69. package/src/path.ts +288 -0
  70. package/src/pipeline.ts +918 -0
  71. package/src/plugin-routes.ts +188 -0
  72. package/src/plugin.ts +1432 -0
  73. package/src/proxy.ts +236 -0
  74. package/src/query.ts +30 -0
  75. package/src/response.ts +240 -0
  76. package/src/router.ts +119 -0
  77. package/src/security-headers.ts +161 -0
  78. package/src/serialize.ts +264 -0
  79. package/src/size.ts +66 -0
  80. package/src/sse-parse.ts +194 -0
  81. package/src/sse.ts +650 -0
  82. package/src/standard-schema.ts +81 -0
  83. package/src/status.ts +78 -0
  84. package/src/stream.ts +200 -0
  85. package/src/tokens.ts +21 -0
  86. package/src/uploads.ts +176 -0
  87. package/src/utils.ts +31 -0
  88. package/src/validate.ts +65 -0
@@ -0,0 +1,368 @@
1
+ /** Node.js `IncomingMessage` / `ServerResponse` helpers shared by the Node and Express adapters (internal). */
2
+ import type { IncomingHttpHeaders, IncomingMessage, OutgoingHttpHeaders, Server, ServerResponse } from 'node:http'
3
+ import type { Socket } from 'node:net'
4
+ import type { AdapterReply, AdapterRequest } from '../adapter.ts'
5
+ import type { HeaderValue } from '../exceptions.ts'
6
+
7
+ import { Readable } from 'node:stream'
8
+ import { pipeline } from 'node:stream/promises'
9
+
10
+ import { BadRequestException } from '../exceptions.ts'
11
+ import { discardStream, failedStream } from '../stream.ts'
12
+ import { concat, exceedsDeclaredLength, payloadTooLarge } from './shared.ts'
13
+
14
+ /** Bytes buffered by a request body stream before the request is paused. */
15
+ const BODY_HIGH_WATER_MARK = 64 * 1024
16
+
17
+ /** Request headers with array values (only `set-cookie` in Node.js) joined with `, `. */
18
+ export function normalizeNodeHeaders(headers: IncomingHttpHeaders): Record<string, string | undefined> {
19
+ for (const name in headers) {
20
+ if (typeof headers[name] !== 'string') return copyHeaders(headers)
21
+ }
22
+ return headers as Record<string, string | undefined>
23
+ }
24
+
25
+ function copyHeaders(headers: IncomingHttpHeaders): Record<string, string | undefined> {
26
+ const result: Record<string, string | undefined> = {}
27
+ for (const name in headers) {
28
+ const value = headers[name]
29
+ result[name] = Array.isArray(value) ? value.join(', ') : value
30
+ }
31
+ return result
32
+ }
33
+
34
+ /**
35
+ * Whether the body was already read by someone else: an Express body parser (`express.json()`...)
36
+ * marks the request with `_body`; otherwise a declared body that already ended.
37
+ */
38
+ function consumedElsewhere(request: IncomingMessage): boolean {
39
+ if ((request as { _body?: unknown })._body === true) return true
40
+ if (!request.readableEnded) return false
41
+ const length = request.headers['content-length']
42
+ return request.headers['transfer-encoding'] !== undefined || (length !== undefined && length.trim() !== '0')
43
+ }
44
+
45
+ /** The error of a body consumed before Znight read it (an empty body would be silently wrong). */
46
+ function consumedError(): Error {
47
+ return new Error(
48
+ 'The request body was already read by a middleware (an Express body parser such as express.json()?): ' +
49
+ 'Znight reads the bodies of its routes itself, do not register body parsers for them',
50
+ )
51
+ }
52
+
53
+ /** Reads a Node.js request body, enforcing `limit` (on `content-length`, then while streaming). */
54
+ export function readNodeBody(request: IncomingMessage, limit: number): Promise<Uint8Array> {
55
+ if (exceedsDeclaredLength(request.headers['content-length'], limit)) {
56
+ request.resume()
57
+ return Promise.reject(payloadTooLarge(limit))
58
+ }
59
+ if (consumedElsewhere(request)) return Promise.reject(consumedError())
60
+ if (request.readableEnded) return Promise.resolve(new Uint8Array(0))
61
+ if (request.destroyed) return Promise.reject(new BadRequestException('The request was aborted'))
62
+ return new Promise((resolve, reject) => {
63
+ const chunks: Uint8Array[] = []
64
+ let size = 0
65
+ const cleanup = (): void => {
66
+ request.off('data', onData)
67
+ request.off('end', onEnd)
68
+ request.off('error', onError)
69
+ request.off('close', onClose)
70
+ }
71
+ const onData = (chunk: Uint8Array | string): void => {
72
+ const bytes = typeof chunk === 'string' ? Buffer.from(chunk) : chunk
73
+ size += bytes.byteLength
74
+ if (size > limit) {
75
+ cleanup()
76
+ request.resume() // discard the rest; the 413 response closes the connection
77
+ reject(payloadTooLarge(limit))
78
+ return
79
+ }
80
+ chunks.push(bytes)
81
+ }
82
+ const onEnd = (): void => {
83
+ cleanup()
84
+ resolve(concat(chunks, size))
85
+ }
86
+ const onError = (error: Error): void => {
87
+ cleanup()
88
+ reject(new BadRequestException('Failed to read the request body', undefined, { cause: error }))
89
+ }
90
+ const onClose = (): void => {
91
+ if (request.complete) return
92
+ cleanup()
93
+ reject(new BadRequestException('The request was aborted'))
94
+ }
95
+ request.on('data', onData)
96
+ request.on('end', onEnd)
97
+ request.on('error', onError)
98
+ request.on('close', onClose)
99
+ })
100
+ }
101
+
102
+ /**
103
+ * A Node.js request body as a Web stream (see `AdapterRequest.streamBody`): the request is paused
104
+ * while the consumer does not read (backpressure); past `limit` bytes the stream errors with 413 and
105
+ * the rest of the body is discarded (`request.resume()`), like when the consumer cancels the stream,
106
+ * so the response can still be written (it carries `connection: close`).
107
+ */
108
+ export function nodeBodyStream(request: IncomingMessage, limit: number): ReadableStream<Uint8Array> {
109
+ if (exceedsDeclaredLength(request.headers['content-length'], limit)) {
110
+ request.resume()
111
+ return failedStream(payloadTooLarge(limit))
112
+ }
113
+ if (consumedElsewhere(request)) return failedStream(consumedError())
114
+ if (request.readableEnded) return new ReadableStream<Uint8Array>({ start: controller => controller.close() })
115
+ if (request.destroyed) return failedStream(new BadRequestException('The request was aborted'))
116
+ let size = 0
117
+ let cleanup = (): void => undefined
118
+ return new ReadableStream<Uint8Array>(
119
+ {
120
+ start(controller) {
121
+ const onData = (chunk: Uint8Array | string): void => {
122
+ const bytes = typeof chunk === 'string' ? Buffer.from(chunk) : chunk
123
+ size += bytes.byteLength
124
+ if (size > limit) {
125
+ cleanup()
126
+ request.resume() // discard the rest; the 413 response closes the connection
127
+ controller.error(payloadTooLarge(limit))
128
+ return
129
+ }
130
+ controller.enqueue(bytes)
131
+ if ((controller.desiredSize ?? 0) <= 0) request.pause()
132
+ }
133
+ const onEnd = (): void => {
134
+ cleanup()
135
+ controller.close()
136
+ }
137
+ const onError = (error: Error): void => {
138
+ cleanup()
139
+ controller.error(new BadRequestException('Failed to read the request body', undefined, { cause: error }))
140
+ }
141
+ const onClose = (): void => {
142
+ if (request.complete) return
143
+ cleanup()
144
+ controller.error(new BadRequestException('The request was aborted'))
145
+ }
146
+ cleanup = (): void => {
147
+ request.off('data', onData)
148
+ request.off('end', onEnd)
149
+ request.off('error', onError)
150
+ request.off('close', onClose)
151
+ }
152
+ request.on('data', onData)
153
+ request.on('end', onEnd)
154
+ request.on('error', onError)
155
+ request.on('close', onClose)
156
+ },
157
+ pull() {
158
+ if (request.isPaused()) request.resume()
159
+ },
160
+ cancel() {
161
+ cleanup()
162
+ request.resume() // discard the rest of the body
163
+ },
164
+ },
165
+ { highWaterMark: BODY_HIGH_WATER_MARK, size: (chunk: Uint8Array | undefined) => chunk?.byteLength ?? 0 },
166
+ )
167
+ }
168
+
169
+ /** Options of {@link nodeAdapterRequest}. */
170
+ export interface NodeRequestOptions {
171
+ /** Request target (default: `request.url`). */
172
+ readonly url?: string
173
+ /** `@Req()` value (default: the `IncomingMessage`). */
174
+ readonly raw?: unknown
175
+ /** `@Res()` value (default: the `ServerResponse`). */
176
+ readonly rawResponse?: unknown
177
+ readonly ip?: string | undefined
178
+ }
179
+
180
+ /** Wraps a Node.js request/response pair as an {@link AdapterRequest}. */
181
+ export function nodeAdapterRequest(
182
+ request: IncomingMessage,
183
+ response: ServerResponse,
184
+ options: NodeRequestOptions = {},
185
+ ): AdapterRequest {
186
+ let body: Promise<Uint8Array> | undefined
187
+ return {
188
+ method: request.method ?? 'GET',
189
+ url: options.url ?? request.url ?? '/',
190
+ headers: normalizeNodeHeaders(request.headers),
191
+ ip: options.ip ?? request.socket?.remoteAddress,
192
+ remoteAddress: request.socket?.remoteAddress,
193
+ encrypted: isEncrypted(request),
194
+ raw: options.raw ?? request,
195
+ rawResponse: options.rawResponse ?? response,
196
+ readBody: (limit: number): Promise<Uint8Array> => {
197
+ body ??= readNodeBody(request, limit)
198
+ return body
199
+ },
200
+ streamBody: (limit: number): ReadableStream<Uint8Array> => nodeBodyStream(request, limit),
201
+ rawStatus: (): number => response.statusCode,
202
+ onAbort: (listener: () => void): void => onResponseAbort(response, listener),
203
+ }
204
+ }
205
+
206
+ /**
207
+ * Calls `listener` once when `response` closes before it was fully written (the client left): on
208
+ * the next microtask when it is already destroyed, never after a complete response.
209
+ */
210
+ export function onResponseAbort(response: ServerResponse, listener: () => void): void {
211
+ if (response.writableFinished) return
212
+ if (response.destroyed) {
213
+ queueMicrotask(listener)
214
+ return
215
+ }
216
+ response.once('close', () => {
217
+ if (!response.writableFinished) listener()
218
+ })
219
+ }
220
+
221
+ /** Whether the request came over TLS (`node:https`, `node:http2` secure servers). */
222
+ export function isEncrypted(request: IncomingMessage): boolean {
223
+ return (request.socket as { readonly encrypted?: unknown } | undefined)?.encrypted === true
224
+ }
225
+
226
+ /**
227
+ * Writes a reply to a `ServerResponse`. Resolves when the response is complete (streams included);
228
+ * never rejects (a failing stream destroys the response).
229
+ */
230
+ export async function writeNodeReply(response: ServerResponse, reply: AdapterReply): Promise<void> {
231
+ if (reply.raw === true) {
232
+ discardStream(reply.body instanceof ReadableStream ? reply.body : null)
233
+ return
234
+ }
235
+ if (response.headersSent || response.destroyed) {
236
+ discardStream(reply.body instanceof ReadableStream ? reply.body : null)
237
+ if (!response.writableEnded && !response.destroyed) response.end()
238
+ return
239
+ }
240
+ try {
241
+ // one call (merged over headers set earlier with setHeader(), e.g. by Express middleware); the
242
+ // cookies set earlier are kept before the reply's own (writeHead would replace them)
243
+ const headers = withNativeCookies(response.getHeader('set-cookie'), reply.headers)
244
+ response.writeHead(reply.status, headers as OutgoingHttpHeaders)
245
+ } catch {
246
+ // invalid header name or value set by user code
247
+ discardStream(reply.body instanceof ReadableStream ? reply.body : null)
248
+ if (!response.headersSent) {
249
+ for (const name of response.getHeaderNames()) response.removeHeader(name)
250
+ response.statusCode = 500
251
+ response.setHeader('content-type', 'application/json; charset=utf-8')
252
+ response.end('{"statusCode":500,"error":"Internal Server Error"}')
253
+ }
254
+ return
255
+ }
256
+ const body = reply.body
257
+ if (body === null) {
258
+ response.end()
259
+ return
260
+ }
261
+ // an event stream may stay quiet longer than a socket timeout (`server.timeout`, Bun's emulation)
262
+ if (reply.eventStream === true && (response as { isLightMyRequest?: boolean }).isLightMyRequest !== true) {
263
+ response.setTimeout(0)
264
+ }
265
+ if (body instanceof Uint8Array) {
266
+ response.end(body)
267
+ return
268
+ }
269
+ const stream = body instanceof Blob ? body.stream() : body
270
+ try {
271
+ await pipeline(Readable.fromWeb(stream as import('node:stream/web').ReadableStream), response)
272
+ } catch {
273
+ if (!response.destroyed) response.destroy()
274
+ }
275
+ }
276
+
277
+ /**
278
+ * `headers` with the `set-cookie` lines already set on the native response (Express middleware
279
+ * `res.cookie()`, Fastify hooks `reply.header('set-cookie', ...)`) before its own: `headers`
280
+ * unchanged when either side has none.
281
+ */
282
+ export function withNativeCookies(
283
+ native: number | string | readonly string[] | undefined,
284
+ headers: Readonly<Record<string, HeaderValue>>,
285
+ ): Readonly<Record<string, HeaderValue>> {
286
+ const own = headers['set-cookie']
287
+ if (own === undefined || native === undefined || typeof native === 'number') return headers
288
+ const before = typeof native === 'string' ? [native] : native
289
+ if (before.length === 0) return headers
290
+ return { ...headers, 'set-cookie': [...before, ...(typeof own === 'string' ? [own] : own)] }
291
+ }
292
+
293
+ /** Last-resort 500 when a handler promise rejected (should not happen: the pipeline maps errors). */
294
+ export function writeNodeFailure(response: ServerResponse): void {
295
+ if (response.headersSent) {
296
+ if (!response.writableEnded) response.destroy()
297
+ return
298
+ }
299
+ response.statusCode = 500
300
+ response.setHeader('content-type', 'application/json; charset=utf-8')
301
+ response.end('{"statusCode":500,"error":"Internal Server Error"}')
302
+ }
303
+
304
+ /**
305
+ * Tracks the connections of a `node:http` server to close them on shutdown: idle keep-alive
306
+ * connections first (graceful), every connection when forced. `server.closeIdleConnections()` /
307
+ * `closeAllConnections()` are not reliable on every runtime (Bun), so sockets are tracked here.
308
+ */
309
+ export class ConnectionTracker {
310
+ /** Socket → number of requests in progress. */
311
+ readonly #sockets = new Map<Socket, number>()
312
+ #closing = false
313
+
314
+ constructor(server: Server) {
315
+ server.on('connection', (socket: Socket) => {
316
+ this.#sockets.set(socket, 0)
317
+ socket.once('close', () => this.#sockets.delete(socket))
318
+ })
319
+ server.on('request', (request: IncomingMessage, response: ServerResponse) => {
320
+ const socket = request.socket
321
+ if (!this.#sockets.has(socket)) return
322
+ this.#sockets.set(socket, (this.#sockets.get(socket) ?? 0) + 1)
323
+ response.once('close', () => {
324
+ const active = this.#sockets.get(socket)
325
+ if (active === undefined) return
326
+ this.#sockets.set(socket, active - 1)
327
+ if (active - 1 === 0 && this.#closing) socket.end()
328
+ })
329
+ })
330
+ }
331
+
332
+ /** Ends idle connections now and the others once their requests completed. */
333
+ closeIdle(): void {
334
+ this.#closing = true
335
+ for (const [socket, active] of this.#sockets) if (active === 0) socket.end()
336
+ }
337
+
338
+ /** Destroys every connection. */
339
+ destroyAll(): void {
340
+ this.#closing = true
341
+ for (const socket of this.#sockets.keys()) socket.destroy()
342
+ }
343
+ }
344
+
345
+ /** Closes a `node:http` server: graceful (idle connections ended), then forced when asked. */
346
+ export function closeServer(
347
+ server: Server,
348
+ tracker: ConnectionTracker,
349
+ closing: Promise<void> | undefined,
350
+ force: boolean,
351
+ ): Promise<void> {
352
+ const promise =
353
+ closing ??
354
+ new Promise<void>(resolve => {
355
+ if (!server.listening) {
356
+ resolve()
357
+ return
358
+ }
359
+ server.close(() => resolve())
360
+ server.closeIdleConnections?.()
361
+ tracker.closeIdle()
362
+ })
363
+ if (force) {
364
+ server.closeAllConnections?.()
365
+ tracker.destroyAll()
366
+ }
367
+ return promise
368
+ }
@@ -0,0 +1,135 @@
1
+ /**
2
+ * `@znight/http/node`: the built-in, zero-dependency adapter on `node:http` (default adapter of
3
+ * `HttpPlugin`). Works on Node.js and Bun. `inject()` uses the optional peer `light-my-request`.
4
+ */
5
+ import type {
6
+ AdapterCloseOptions,
7
+ AdapterHandler,
8
+ AdapterRoute,
9
+ FallbackHandler,
10
+ HttpAdapter,
11
+ HttpServerAddress,
12
+ InjectRequest,
13
+ InjectResponse,
14
+ InjectStreamRequest,
15
+ InjectStreamResponse,
16
+ } from '../adapter.ts'
17
+
18
+ import { createServer, type IncomingMessage, type Server, type ServerOptions, type ServerResponse } from 'node:http'
19
+
20
+ import { RouteTable } from '../router.ts'
21
+ import { ConnectionTracker, closeServer, nodeAdapterRequest, writeNodeFailure, writeNodeReply } from './node-io.ts'
22
+ import { bufferInjectStream, injectNodeStream, serverAddress } from './shared.ts'
23
+
24
+ /** Options of {@link nodeAdapter}. */
25
+ export interface NodeAdapterOptions {
26
+ /** Options of `http.createServer()` (timeouts, `maxHeaderSize`...). Ignored with {@link server}. */
27
+ readonly serverOptions?: ServerOptions
28
+ /**
29
+ * An existing server (e.g. `https.createServer({ key, cert })`). The adapter adds its `request`
30
+ * listener on `listen()` and closes the server on shutdown.
31
+ */
32
+ readonly server?: Server
33
+ }
34
+
35
+ /** The built-in Node.js adapter. */
36
+ export interface NodeAdapter extends HttpAdapter {
37
+ readonly name: 'node'
38
+ /** Request listener serving the routes, to mount the application in a server you manage. */
39
+ readonly handler: (request: IncomingMessage, response: ServerResponse) => void
40
+ /** The server, once listening (or the one given in the options). */
41
+ readonly server: Server | undefined
42
+ }
43
+
44
+ /**
45
+ * Creates the built-in `node:http` adapter: a segment-trie router (static > `:param` > `*`, with
46
+ * backtracking), zero dependencies.
47
+ *
48
+ * ```ts
49
+ * HttpPlugin.configure({ adapter: nodeAdapter({ serverOptions: { requestTimeout: 30_000 } }) })
50
+ * ```
51
+ */
52
+ export function nodeAdapter(options: NodeAdapterOptions = {}): NodeAdapter {
53
+ return new NodeHttpAdapter(options)
54
+ }
55
+
56
+ class NodeHttpAdapter implements NodeAdapter {
57
+ readonly name = 'node' as const
58
+ readonly #routes = new RouteTable<AdapterHandler>()
59
+ readonly #options: NodeAdapterOptions
60
+ #fallback: FallbackHandler | undefined
61
+ #server: Server | undefined
62
+ #tracker: ConnectionTracker | undefined
63
+ #closing: Promise<void> | undefined
64
+
65
+ constructor(options: NodeAdapterOptions) {
66
+ this.#options = options
67
+ this.#server = options.server
68
+ }
69
+
70
+ get server(): Server | undefined {
71
+ return this.#server
72
+ }
73
+
74
+ registerRoute(route: AdapterRoute): void {
75
+ if (this.#routes.add(route.method, route.path, route.handler) !== undefined) {
76
+ throw new Error(`Duplicate route ${route.method} ${route.path.path}`)
77
+ }
78
+ }
79
+
80
+ setFallbackHandler(handler: FallbackHandler): void {
81
+ this.#fallback = handler
82
+ }
83
+
84
+ readonly handler = (request: IncomingMessage, response: ServerResponse): void => {
85
+ const url = request.url ?? '/'
86
+ const query = url.indexOf('?')
87
+ const handler = this.#routes.find(request.method ?? 'GET', query === -1 ? url : url.slice(0, query))
88
+ const adapterRequest = nodeAdapterRequest(request, response)
89
+ let reply: ReturnType<AdapterHandler>
90
+ if (handler !== undefined) reply = handler(adapterRequest)
91
+ else if (this.#fallback === undefined) {
92
+ writeNodeFailure(response)
93
+ return
94
+ } else reply = this.#fallback(adapterRequest)
95
+ reply.then(
96
+ result => writeNodeReply(response, result),
97
+ () => writeNodeFailure(response),
98
+ )
99
+ }
100
+
101
+ async listen(port: number, host: string): Promise<HttpServerAddress> {
102
+ const server = this.#server ?? createServer(this.#options.serverOptions ?? {})
103
+ this.#server = server
104
+ this.#tracker ??= new ConnectionTracker(server)
105
+ this.#closing = undefined
106
+ if (!server.listeners('request').includes(this.handler)) server.on('request', this.handler)
107
+ await new Promise<void>((resolve, reject) => {
108
+ const onError = (error: Error): void => reject(error)
109
+ server.once('error', onError)
110
+ server.listen(port, host, () => {
111
+ server.off('error', onError)
112
+ resolve()
113
+ })
114
+ })
115
+ const address = server.address()
116
+ if (address === null || typeof address === 'string') return serverAddress(host, port)
117
+ return serverAddress(address.address, address.port, address.family)
118
+ }
119
+
120
+ close(options: AdapterCloseOptions = {}): Promise<void> {
121
+ const server = this.#server
122
+ const tracker = this.#tracker
123
+ if (server === undefined || tracker === undefined) return Promise.resolve()
124
+ this.#closing = closeServer(server, tracker, this.#closing, options.force === true)
125
+ return this.#closing
126
+ }
127
+
128
+ inject(request: InjectRequest): Promise<InjectResponse> {
129
+ return bufferInjectStream(stream => this.injectStream(stream), request)
130
+ }
131
+
132
+ injectStream(request: InjectStreamRequest): Promise<InjectStreamResponse> {
133
+ return injectNodeStream('node', this.handler as (req: never, res: never) => void, request)
134
+ }
135
+ }