@znight/http 0.1.1 → 0.1.3

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 (47) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +83 -32
  3. package/dist/{adapter-CGSceVEN.d.mts → adapter-J0UXFnNM.d.mts} +7 -1
  4. package/dist/compiler.d.mts +1 -1
  5. package/dist/compiler.mjs +82 -13
  6. package/dist/compiler.mjs.map +1 -1
  7. package/dist/express.d.mts +1 -1
  8. package/dist/express.mjs +3 -2
  9. package/dist/express.mjs.map +1 -1
  10. package/dist/fastify.d.mts +1 -1
  11. package/dist/fastify.mjs +254 -42
  12. package/dist/fastify.mjs.map +1 -1
  13. package/dist/hono.d.mts +1 -1
  14. package/dist/hono.mjs +272 -67
  15. package/dist/hono.mjs.map +1 -1
  16. package/dist/index.d.mts +10 -2
  17. package/dist/index.mjs +930 -278
  18. package/dist/index.mjs.map +1 -1
  19. package/dist/{node-BUnkdSsa.mjs → node-DnYv_AMI.mjs} +4 -3
  20. package/dist/{node-BUnkdSsa.mjs.map → node-DnYv_AMI.mjs.map} +1 -1
  21. package/dist/{node-io-BY1ZDLRQ.mjs → node-io-BkdRSQeg.mjs} +151 -86
  22. package/dist/{node-io-BY1ZDLRQ.mjs.map → node-io-BkdRSQeg.mjs.map} +1 -1
  23. package/dist/node.d.mts +1 -1
  24. package/dist/node.mjs +1 -1
  25. package/dist/utils-DIWdzVht.mjs +46 -0
  26. package/dist/utils-DIWdzVht.mjs.map +1 -0
  27. package/package.json +4 -4
  28. package/src/adapter.ts +6 -0
  29. package/src/adapters/express.ts +4 -1
  30. package/src/adapters/fastify.ts +329 -44
  31. package/src/adapters/hono.ts +341 -86
  32. package/src/adapters/node-io.ts +185 -82
  33. package/src/adapters/node.ts +4 -1
  34. package/src/body.ts +31 -2
  35. package/src/coerce.ts +31 -0
  36. package/src/compiler/generate.ts +127 -22
  37. package/src/context.ts +151 -78
  38. package/src/index.ts +1 -1
  39. package/src/pipeline.ts +809 -208
  40. package/src/plugin.ts +10 -4
  41. package/src/query.ts +7 -1
  42. package/src/response.ts +6 -8
  43. package/src/serialize.ts +83 -16
  44. package/src/utils.ts +25 -2
  45. package/src/validate.ts +28 -12
  46. package/dist/utils-DCbu3a4w.mjs +0 -27
  47. package/dist/utils-DCbu3a4w.mjs.map +0 -1
@@ -1,4 +1,4 @@
1
- import { d as writeNodeReply, f as bufferInjectStream, i as nodeAdapterRequest, m as injectNodeStream, n as closeServer, t as ConnectionTracker, u as writeNodeFailure, y as serverAddress } from "./node-io-BY1ZDLRQ.mjs";
1
+ import { d as writeNodeReply, f as bufferInjectStream, h as injectNodeStream, i as nodeAdapterRequest, n as closeServer, t as ConnectionTracker, u as writeNodeFailure, x as serverAddress } from "./node-io-BkdRSQeg.mjs";
2
2
  import { createServer } from "node:http";
3
3
  // src/router.ts
4
4
  function createNode() {
@@ -134,6 +134,7 @@ var NodeHttpAdapter = class {
134
134
  this.#fallback = handler;
135
135
  }
136
136
  handler = (request, response) => {
137
+ this.#tracker?.track(request, response);
137
138
  const url = request.url ?? "/";
138
139
  const query = url.indexOf("?");
139
140
  const handler = this.#routes.find(request.method ?? "GET", query === -1 ? url : url.slice(0, query));
@@ -149,7 +150,7 @@ var NodeHttpAdapter = class {
149
150
  async listen(port, host) {
150
151
  const server = this.#server ?? createServer(this.#options.serverOptions ?? {});
151
152
  this.#server = server;
152
- this.#tracker ??= new ConnectionTracker(server);
153
+ this.#tracker ??= new ConnectionTracker(server, { requests: false });
153
154
  this.#closing = void 0;
154
155
  if (!server.listeners("request").includes(this.handler)) server.on("request", this.handler);
155
156
  await new Promise((resolve, reject) => {
@@ -180,4 +181,4 @@ var NodeHttpAdapter = class {
180
181
  };
181
182
  export { RouteTable as n, nodeAdapter as t };
182
183
 
183
- //# sourceMappingURL=node-BUnkdSsa.mjs.map
184
+ //# sourceMappingURL=node-DnYv_AMI.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"node-BUnkdSsa.mjs","names":[],"sources":["../src/router.ts","../src/adapters/node.ts"],"sourcesContent":["import type { RoutePath } from './path.ts'\n\ninterface RouteNode<T> {\n statics: Map<string, RouteNode<T>> | undefined\n param: RouteNode<T> | undefined\n /** Values of routes ending with a wildcard at this position, by method. */\n wildcard: Map<string, T> | undefined\n /** Values of routes ending at this node, by method. */\n values: Map<string, T> | undefined\n}\n\nfunction createNode<T>(): RouteNode<T> {\n return { statics: undefined, param: undefined, wildcard: undefined, values: undefined }\n}\n\n/**\n * A segment trie mapping `(method, route path)` to values. Lookups prefer static segments over\n * parameters over the wildcard, with backtracking (the same semantics as find-my-way), and are\n * case-sensitive on the raw request path. Used by the built-in Node adapter for routing and by the\n * plugin to tell `404 Not Found` from `405 Method Not Allowed`.\n */\nexport class RouteTable<T> {\n readonly #root: RouteNode<T> = createNode()\n #size = 0\n\n /** Number of `(method, route)` entries. */\n get size(): number {\n return this.#size\n }\n\n /**\n * Adds a value for `method` (upper case) and `route`.\n *\n * @returns the value already registered for the same method and shape, in which case nothing is\n * added.\n */\n add(method: string, route: RoutePath, value: T): T | undefined {\n let node = this.#root\n for (const segment of route.segments) {\n if (segment.kind === 'static') {\n node.statics ??= new Map()\n let child = node.statics.get(segment.value)\n if (child === undefined) {\n child = createNode()\n node.statics.set(segment.value, child)\n }\n node = child\n } else if (segment.kind === 'param') {\n node.param ??= createNode()\n node = node.param\n } else {\n node.wildcard ??= new Map()\n return this.#set(node.wildcard, method, value)\n }\n }\n node.values ??= new Map()\n return this.#set(node.values, method, value)\n }\n\n #set(map: Map<string, T>, method: string, value: T): T | undefined {\n const existing = map.get(method)\n if (existing !== undefined) return existing\n map.set(method, value)\n this.#size++\n return undefined\n }\n\n /** The value of the most specific route matching `method` and the raw `path` (no query string). */\n find(method: string, path: string): T | undefined {\n if (path.charCodeAt(0) !== 47 /* / */) return undefined\n if (path === '/') return this.#root.values?.get(method) ?? this.#root.wildcard?.get(method)\n return search(this.#root, path.split('/'), 1, method)\n }\n\n /** Methods of every route matching the raw `path`, sorted. */\n allowedMethods(path: string): string[] {\n const methods = new Set<string>()\n if (path.charCodeAt(0) === 47 /* / */) {\n if (path === '/') {\n addKeys(methods, this.#root.values)\n addKeys(methods, this.#root.wildcard)\n } else {\n collect(this.#root, path.split('/'), 1, methods)\n }\n }\n return [...methods].sort()\n }\n}\n\nfunction search<T>(node: RouteNode<T>, segments: readonly string[], index: number, method: string): T | undefined {\n if (index === segments.length) return node.values?.get(method)\n const segment = segments[index] as string\n const child = node.statics?.get(segment)\n if (child !== undefined) {\n const found = search(child, segments, index + 1, method)\n if (found !== undefined) return found\n }\n if (segment !== '' && node.param !== undefined) {\n const found = search(node.param, segments, index + 1, method)\n if (found !== undefined) return found\n }\n return node.wildcard?.get(method)\n}\n\nfunction collect<T>(node: RouteNode<T>, segments: readonly string[], index: number, methods: Set<string>): void {\n if (index === segments.length) {\n addKeys(methods, node.values)\n return\n }\n const segment = segments[index] as string\n const child = node.statics?.get(segment)\n if (child !== undefined) collect(child, segments, index + 1, methods)\n if (segment !== '' && node.param !== undefined) collect(node.param, segments, index + 1, methods)\n addKeys(methods, node.wildcard)\n}\n\nfunction addKeys(target: Set<string>, source: Map<string, unknown> | undefined): void {\n if (source !== undefined) for (const key of source.keys()) target.add(key)\n}\n","/**\n * `@znight/http/node`: the built-in, zero-dependency adapter on `node:http` (default adapter of\n * `HttpPlugin`). Works on Node.js and Bun. `inject()` uses the optional peer `light-my-request`.\n */\nimport type {\n AdapterCloseOptions,\n AdapterHandler,\n AdapterRoute,\n FallbackHandler,\n HttpAdapter,\n HttpServerAddress,\n InjectRequest,\n InjectResponse,\n InjectStreamRequest,\n InjectStreamResponse,\n} from '../adapter.ts'\n\nimport { createServer, type IncomingMessage, type Server, type ServerOptions, type ServerResponse } from 'node:http'\n\nimport { RouteTable } from '../router.ts'\nimport { ConnectionTracker, closeServer, nodeAdapterRequest, writeNodeFailure, writeNodeReply } from './node-io.ts'\nimport { bufferInjectStream, injectNodeStream, serverAddress } from './shared.ts'\n\n/** Options of {@link nodeAdapter}. */\nexport interface NodeAdapterOptions {\n /** Options of `http.createServer()` (timeouts, `maxHeaderSize`...). Ignored with {@link server}. */\n readonly serverOptions?: ServerOptions\n /**\n * An existing server (e.g. `https.createServer({ key, cert })`). The adapter adds its `request`\n * listener on `listen()` and closes the server on shutdown.\n */\n readonly server?: Server\n}\n\n/** The built-in Node.js adapter. */\nexport interface NodeAdapter extends HttpAdapter {\n readonly name: 'node'\n /** Request listener serving the routes, to mount the application in a server you manage. */\n readonly handler: (request: IncomingMessage, response: ServerResponse) => void\n /** The server, once listening (or the one given in the options). */\n readonly server: Server | undefined\n}\n\n/**\n * Creates the built-in `node:http` adapter: a segment-trie router (static > `:param` > `*`, with\n * backtracking), zero dependencies.\n *\n * ```ts\n * HttpPlugin.configure({ adapter: nodeAdapter({ serverOptions: { requestTimeout: 30_000 } }) })\n * ```\n */\nexport function nodeAdapter(options: NodeAdapterOptions = {}): NodeAdapter {\n return new NodeHttpAdapter(options)\n}\n\nclass NodeHttpAdapter implements NodeAdapter {\n readonly name = 'node' as const\n readonly #routes = new RouteTable<AdapterHandler>()\n readonly #options: NodeAdapterOptions\n #fallback: FallbackHandler | undefined\n #server: Server | undefined\n #tracker: ConnectionTracker | undefined\n #closing: Promise<void> | undefined\n\n constructor(options: NodeAdapterOptions) {\n this.#options = options\n this.#server = options.server\n }\n\n get server(): Server | undefined {\n return this.#server\n }\n\n registerRoute(route: AdapterRoute): void {\n if (this.#routes.add(route.method, route.path, route.handler) !== undefined) {\n throw new Error(`Duplicate route ${route.method} ${route.path.path}`)\n }\n }\n\n setFallbackHandler(handler: FallbackHandler): void {\n this.#fallback = handler\n }\n\n readonly handler = (request: IncomingMessage, response: ServerResponse): void => {\n const url = request.url ?? '/'\n const query = url.indexOf('?')\n const handler = this.#routes.find(request.method ?? 'GET', query === -1 ? url : url.slice(0, query))\n const adapterRequest = nodeAdapterRequest(request, response)\n let reply: ReturnType<AdapterHandler>\n if (handler !== undefined) reply = handler(adapterRequest)\n else if (this.#fallback === undefined) {\n writeNodeFailure(response)\n return\n } else reply = this.#fallback(adapterRequest)\n reply.then(\n result => writeNodeReply(response, result),\n () => writeNodeFailure(response),\n )\n }\n\n async listen(port: number, host: string): Promise<HttpServerAddress> {\n const server = this.#server ?? createServer(this.#options.serverOptions ?? {})\n this.#server = server\n this.#tracker ??= new ConnectionTracker(server)\n this.#closing = undefined\n if (!server.listeners('request').includes(this.handler)) server.on('request', this.handler)\n await new Promise<void>((resolve, reject) => {\n const onError = (error: Error): void => reject(error)\n server.once('error', onError)\n server.listen(port, host, () => {\n server.off('error', onError)\n resolve()\n })\n })\n const address = server.address()\n if (address === null || typeof address === 'string') return serverAddress(host, port)\n return serverAddress(address.address, address.port, address.family)\n }\n\n close(options: AdapterCloseOptions = {}): Promise<void> {\n const server = this.#server\n const tracker = this.#tracker\n if (server === undefined || tracker === undefined) return Promise.resolve()\n this.#closing = closeServer(server, tracker, this.#closing, options.force === true)\n return this.#closing\n }\n\n inject(request: InjectRequest): Promise<InjectResponse> {\n return bufferInjectStream(stream => this.injectStream(stream), request)\n }\n\n injectStream(request: InjectStreamRequest): Promise<InjectStreamResponse> {\n return injectNodeStream('node', this.handler as (req: never, res: never) => void, request)\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"node-DnYv_AMI.mjs","names":[],"sources":["../src/router.ts","../src/adapters/node.ts"],"sourcesContent":["import type { RoutePath } from './path.ts'\n\ninterface RouteNode<T> {\n statics: Map<string, RouteNode<T>> | undefined\n param: RouteNode<T> | undefined\n /** Values of routes ending with a wildcard at this position, by method. */\n wildcard: Map<string, T> | undefined\n /** Values of routes ending at this node, by method. */\n values: Map<string, T> | undefined\n}\n\nfunction createNode<T>(): RouteNode<T> {\n return { statics: undefined, param: undefined, wildcard: undefined, values: undefined }\n}\n\n/**\n * A segment trie mapping `(method, route path)` to values. Lookups prefer static segments over\n * parameters over the wildcard, with backtracking (the same semantics as find-my-way), and are\n * case-sensitive on the raw request path. Used by the built-in Node adapter for routing and by the\n * plugin to tell `404 Not Found` from `405 Method Not Allowed`.\n */\nexport class RouteTable<T> {\n readonly #root: RouteNode<T> = createNode()\n #size = 0\n\n /** Number of `(method, route)` entries. */\n get size(): number {\n return this.#size\n }\n\n /**\n * Adds a value for `method` (upper case) and `route`.\n *\n * @returns the value already registered for the same method and shape, in which case nothing is\n * added.\n */\n add(method: string, route: RoutePath, value: T): T | undefined {\n let node = this.#root\n for (const segment of route.segments) {\n if (segment.kind === 'static') {\n node.statics ??= new Map()\n let child = node.statics.get(segment.value)\n if (child === undefined) {\n child = createNode()\n node.statics.set(segment.value, child)\n }\n node = child\n } else if (segment.kind === 'param') {\n node.param ??= createNode()\n node = node.param\n } else {\n node.wildcard ??= new Map()\n return this.#set(node.wildcard, method, value)\n }\n }\n node.values ??= new Map()\n return this.#set(node.values, method, value)\n }\n\n #set(map: Map<string, T>, method: string, value: T): T | undefined {\n const existing = map.get(method)\n if (existing !== undefined) return existing\n map.set(method, value)\n this.#size++\n return undefined\n }\n\n /** The value of the most specific route matching `method` and the raw `path` (no query string). */\n find(method: string, path: string): T | undefined {\n if (path.charCodeAt(0) !== 47 /* / */) return undefined\n if (path === '/') return this.#root.values?.get(method) ?? this.#root.wildcard?.get(method)\n return search(this.#root, path.split('/'), 1, method)\n }\n\n /** Methods of every route matching the raw `path`, sorted. */\n allowedMethods(path: string): string[] {\n const methods = new Set<string>()\n if (path.charCodeAt(0) === 47 /* / */) {\n if (path === '/') {\n addKeys(methods, this.#root.values)\n addKeys(methods, this.#root.wildcard)\n } else {\n collect(this.#root, path.split('/'), 1, methods)\n }\n }\n return [...methods].sort()\n }\n}\n\nfunction search<T>(node: RouteNode<T>, segments: readonly string[], index: number, method: string): T | undefined {\n if (index === segments.length) return node.values?.get(method)\n const segment = segments[index] as string\n const child = node.statics?.get(segment)\n if (child !== undefined) {\n const found = search(child, segments, index + 1, method)\n if (found !== undefined) return found\n }\n if (segment !== '' && node.param !== undefined) {\n const found = search(node.param, segments, index + 1, method)\n if (found !== undefined) return found\n }\n return node.wildcard?.get(method)\n}\n\nfunction collect<T>(node: RouteNode<T>, segments: readonly string[], index: number, methods: Set<string>): void {\n if (index === segments.length) {\n addKeys(methods, node.values)\n return\n }\n const segment = segments[index] as string\n const child = node.statics?.get(segment)\n if (child !== undefined) collect(child, segments, index + 1, methods)\n if (segment !== '' && node.param !== undefined) collect(node.param, segments, index + 1, methods)\n addKeys(methods, node.wildcard)\n}\n\nfunction addKeys(target: Set<string>, source: Map<string, unknown> | undefined): void {\n if (source !== undefined) for (const key of source.keys()) target.add(key)\n}\n","/**\n * `@znight/http/node`: the built-in, zero-dependency adapter on `node:http` (default adapter of\n * `HttpPlugin`). Works on Node.js and Bun. `inject()` uses the optional peer `light-my-request`.\n */\nimport type {\n AdapterCloseOptions,\n AdapterHandler,\n AdapterRoute,\n FallbackHandler,\n HttpAdapter,\n HttpServerAddress,\n InjectRequest,\n InjectResponse,\n InjectStreamRequest,\n InjectStreamResponse,\n} from '../adapter.ts'\n\nimport { createServer, type IncomingMessage, type Server, type ServerOptions, type ServerResponse } from 'node:http'\n\nimport { RouteTable } from '../router.ts'\nimport { ConnectionTracker, closeServer, nodeAdapterRequest, writeNodeFailure, writeNodeReply } from './node-io.ts'\nimport { bufferInjectStream, injectNodeStream, serverAddress } from './shared.ts'\n\n/** Options of {@link nodeAdapter}. */\nexport interface NodeAdapterOptions {\n /** Options of `http.createServer()` (timeouts, `maxHeaderSize`...). Ignored with {@link server}. */\n readonly serverOptions?: ServerOptions\n /**\n * An existing server (e.g. `https.createServer({ key, cert })`). The adapter adds its `request`\n * listener on `listen()` and closes the server on shutdown.\n */\n readonly server?: Server\n}\n\n/** The built-in Node.js adapter. */\nexport interface NodeAdapter extends HttpAdapter {\n readonly name: 'node'\n /** Request listener serving the routes, to mount the application in a server you manage. */\n readonly handler: (request: IncomingMessage, response: ServerResponse) => void\n /** The server, once listening (or the one given in the options). */\n readonly server: Server | undefined\n}\n\n/**\n * Creates the built-in `node:http` adapter: a segment-trie router (static > `:param` > `*`, with\n * backtracking), zero dependencies.\n *\n * ```ts\n * HttpPlugin.configure({ adapter: nodeAdapter({ serverOptions: { requestTimeout: 30_000 } }) })\n * ```\n */\nexport function nodeAdapter(options: NodeAdapterOptions = {}): NodeAdapter {\n return new NodeHttpAdapter(options)\n}\n\nclass NodeHttpAdapter implements NodeAdapter {\n readonly name = 'node' as const\n readonly #routes = new RouteTable<AdapterHandler>()\n readonly #options: NodeAdapterOptions\n #fallback: FallbackHandler | undefined\n #server: Server | undefined\n #tracker: ConnectionTracker | undefined\n #closing: Promise<void> | undefined\n\n constructor(options: NodeAdapterOptions) {\n this.#options = options\n this.#server = options.server\n }\n\n get server(): Server | undefined {\n return this.#server\n }\n\n registerRoute(route: AdapterRoute): void {\n if (this.#routes.add(route.method, route.path, route.handler) !== undefined) {\n throw new Error(`Duplicate route ${route.method} ${route.path.path}`)\n }\n }\n\n setFallbackHandler(handler: FallbackHandler): void {\n this.#fallback = handler\n }\n\n readonly handler = (request: IncomingMessage, response: ServerResponse): void => {\n // the listening server's connections (a request of another server is ignored)\n this.#tracker?.track(request, response)\n const url = request.url ?? '/'\n const query = url.indexOf('?')\n const handler = this.#routes.find(request.method ?? 'GET', query === -1 ? url : url.slice(0, query))\n const adapterRequest = nodeAdapterRequest(request, response)\n let reply: ReturnType<AdapterHandler>\n if (handler !== undefined) reply = handler(adapterRequest)\n else if (this.#fallback === undefined) {\n writeNodeFailure(response)\n return\n } else reply = this.#fallback(adapterRequest)\n reply.then(\n result => writeNodeReply(response, result),\n () => writeNodeFailure(response),\n )\n }\n\n async listen(port: number, host: string): Promise<HttpServerAddress> {\n const server = this.#server ?? createServer(this.#options.serverOptions ?? {})\n this.#server = server\n // `handler` counts the requests\n this.#tracker ??= new ConnectionTracker(server, { requests: false })\n this.#closing = undefined\n if (!server.listeners('request').includes(this.handler)) server.on('request', this.handler)\n await new Promise<void>((resolve, reject) => {\n const onError = (error: Error): void => reject(error)\n server.once('error', onError)\n server.listen(port, host, () => {\n server.off('error', onError)\n resolve()\n })\n })\n const address = server.address()\n if (address === null || typeof address === 'string') return serverAddress(host, port)\n return serverAddress(address.address, address.port, address.family)\n }\n\n close(options: AdapterCloseOptions = {}): Promise<void> {\n const server = this.#server\n const tracker = this.#tracker\n if (server === undefined || tracker === undefined) return Promise.resolve()\n this.#closing = closeServer(server, tracker, this.#closing, options.force === true)\n return this.#closing\n }\n\n inject(request: InjectRequest): Promise<InjectResponse> {\n return bufferInjectStream(stream => this.injectStream(stream), request)\n }\n\n injectStream(request: InjectStreamRequest): Promise<InjectStreamResponse> {\n return injectNodeStream('node', this.handler as (req: never, res: never) => void, request)\n }\n}\n"],"mappings":"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"}
@@ -1291,47 +1291,68 @@ function readNodeBody(request, limit) {
1291
1291
  request.resume();
1292
1292
  return Promise.reject(payloadTooLarge(limit));
1293
1293
  }
1294
+ const reading = request[BODY_READ];
1295
+ if (reading != null) return reading.promise;
1294
1296
  if (consumedElsewhere(request)) return Promise.reject(consumedError());
1295
1297
  if (request.readableEnded) return Promise.resolve(/* @__PURE__ */ new Uint8Array(0));
1296
1298
  if (request.destroyed) return Promise.reject(new BadRequestException("The request was aborted"));
1297
- return new Promise((resolve, reject) => {
1298
- const chunks = [];
1299
- let size = 0;
1300
- const cleanup = () => {
1301
- request.off("data", onData);
1302
- request.off("end", onEnd);
1303
- request.off("error", onError);
1304
- request.off("close", onClose);
1305
- };
1306
- const onData = (chunk) => {
1307
- const bytes = typeof chunk === "string" ? Buffer.from(chunk) : chunk;
1308
- size += bytes.byteLength;
1309
- if (size > limit) {
1310
- cleanup();
1311
- request.resume();
1312
- reject(payloadTooLarge(limit));
1313
- return;
1314
- }
1315
- chunks.push(bytes);
1316
- };
1317
- const onEnd = () => {
1318
- cleanup();
1319
- resolve(concat(chunks, size));
1320
- };
1321
- const onError = (error) => {
1322
- cleanup();
1323
- reject(new BadRequestException("Failed to read the request body", void 0, { cause: error }));
1324
- };
1325
- const onClose = () => {
1326
- if (request.complete) return;
1327
- cleanup();
1328
- reject(new BadRequestException("The request was aborted"));
1329
- };
1330
- request.on("data", onData);
1331
- request.on("end", onEnd);
1332
- request.on("error", onError);
1333
- request.on("close", onClose);
1299
+ let resolve;
1300
+ let reject;
1301
+ const promise = new Promise((onRead, onError) => {
1302
+ resolve = onRead;
1303
+ reject = onError;
1334
1304
  });
1305
+ request[BODY_READ] = {
1306
+ limit,
1307
+ chunks: [],
1308
+ size: 0,
1309
+ resolve,
1310
+ reject,
1311
+ promise
1312
+ };
1313
+ if (reading === void 0) {
1314
+ request.on("data", onBodyData);
1315
+ request.on("end", onBodyEnd);
1316
+ request.on("error", onBodyError);
1317
+ request.on("close", onBodyClose);
1318
+ }
1319
+ return promise;
1320
+ }
1321
+ /**
1322
+ * The state of the body read by {@link readNodeBody}, on its request: `null` once it is over (the
1323
+ * listeners stay), `undefined` before.
1324
+ */
1325
+ const BODY_READ = Symbol("znight.bodyRead");
1326
+ /** Stops reading: returns the state of the read (`undefined` when it is already over). */
1327
+ function stopBodyRead(request) {
1328
+ const read = request[BODY_READ];
1329
+ if (read == null) return void 0;
1330
+ request[BODY_READ] = null;
1331
+ return read;
1332
+ }
1333
+ function onBodyData(chunk) {
1334
+ const read = this[BODY_READ];
1335
+ if (read == null) return;
1336
+ const bytes = typeof chunk === "string" ? Buffer.from(chunk) : chunk;
1337
+ read.size += bytes.byteLength;
1338
+ if (read.size > read.limit) {
1339
+ stopBodyRead(this);
1340
+ this.resume();
1341
+ read.reject(payloadTooLarge(read.limit));
1342
+ return;
1343
+ }
1344
+ read.chunks.push(bytes);
1345
+ }
1346
+ function onBodyEnd() {
1347
+ const read = stopBodyRead(this);
1348
+ read?.resolve(concat(read.chunks, read.size));
1349
+ }
1350
+ function onBodyError(error) {
1351
+ stopBodyRead(this)?.reject(new BadRequestException("Failed to read the request body", void 0, { cause: error }));
1352
+ }
1353
+ function onBodyClose() {
1354
+ if (this.complete) return;
1355
+ stopBodyRead(this)?.reject(new BadRequestException("The request was aborted"));
1335
1356
  }
1336
1357
  /**
1337
1358
  * A Node.js request body as a Web stream (see `AdapterRequest.streamBody`): the request is paused
@@ -1400,26 +1421,50 @@ function nodeBodyStream(request, limit) {
1400
1421
  });
1401
1422
  }
1402
1423
  /** Wraps a Node.js request/response pair as an {@link AdapterRequest}. */
1403
- function nodeAdapterRequest(request, response, options = {}) {
1404
- let body;
1405
- return {
1406
- method: request.method ?? "GET",
1407
- url: options.url ?? request.url ?? "/",
1408
- headers: normalizeNodeHeaders(request.headers),
1409
- ip: options.ip ?? request.socket?.remoteAddress,
1410
- remoteAddress: request.socket?.remoteAddress,
1411
- encrypted: isEncrypted(request),
1412
- raw: options.raw ?? request,
1413
- rawResponse: options.rawResponse ?? response,
1414
- readBody: (limit) => {
1415
- body ??= readNodeBody(request, limit);
1416
- return body;
1417
- },
1418
- streamBody: (limit) => nodeBodyStream(request, limit),
1419
- rawStatus: () => response.statusCode,
1420
- onAbort: (listener) => onResponseAbort(response, listener)
1421
- };
1422
- }
1424
+ function nodeAdapterRequest(request, response, options = NO_OPTIONS) {
1425
+ return new NodeAdapterRequest(request, response, options);
1426
+ }
1427
+ const NO_OPTIONS = Object.freeze({});
1428
+ /** A Node.js request/response pair as an {@link AdapterRequest} (a class: no closure per request). */
1429
+ var NodeAdapterRequest = class {
1430
+ method;
1431
+ url;
1432
+ headers;
1433
+ ip;
1434
+ remoteAddress;
1435
+ encrypted;
1436
+ raw;
1437
+ rawResponse;
1438
+ #request;
1439
+ #response;
1440
+ #body;
1441
+ constructor(request, response, options) {
1442
+ const remoteAddress = request.socket?.remoteAddress;
1443
+ this.method = request.method ?? "GET";
1444
+ this.url = options.url ?? request.url ?? "/";
1445
+ this.headers = normalizeNodeHeaders(request.headers);
1446
+ this.ip = options.ip ?? remoteAddress;
1447
+ this.remoteAddress = remoteAddress;
1448
+ this.encrypted = isEncrypted(request);
1449
+ this.raw = options.raw ?? request;
1450
+ this.rawResponse = options.rawResponse ?? response;
1451
+ this.#request = request;
1452
+ this.#response = response;
1453
+ }
1454
+ readBody(limit) {
1455
+ this.#body ??= readNodeBody(this.#request, limit);
1456
+ return this.#body;
1457
+ }
1458
+ streamBody(limit) {
1459
+ return nodeBodyStream(this.#request, limit);
1460
+ }
1461
+ rawStatus() {
1462
+ return this.#response.statusCode;
1463
+ }
1464
+ onAbort(listener) {
1465
+ onResponseAbort(this.#response, listener);
1466
+ }
1467
+ };
1423
1468
  /**
1424
1469
  * Calls `listener` once when `response` closes before it was fully written (the client left): on
1425
1470
  * the next microtask when it is already destroyed, never after a complete response.
@@ -1439,10 +1484,10 @@ function isEncrypted(request) {
1439
1484
  return request.socket?.encrypted === true;
1440
1485
  }
1441
1486
  /**
1442
- * Writes a reply to a `ServerResponse`. Resolves when the response is complete (streams included);
1443
- * never rejects (a failing stream destroys the response).
1487
+ * Writes a reply to a `ServerResponse`, piping a stream body (a failing stream destroys the
1488
+ * response). Never throws.
1444
1489
  */
1445
- async function writeNodeReply(response, reply) {
1490
+ function writeNodeReply(response, reply) {
1446
1491
  if (reply.raw === true) {
1447
1492
  discardStream(reply.body instanceof ReadableStream ? reply.body : null);
1448
1493
  return;
@@ -1453,7 +1498,7 @@ async function writeNodeReply(response, reply) {
1453
1498
  return;
1454
1499
  }
1455
1500
  try {
1456
- const headers = withNativeCookies(response.getHeader("set-cookie"), reply.headers);
1501
+ const headers = reply.headers["set-cookie"] === void 0 ? reply.headers : withNativeCookies(response.getHeader("set-cookie"), reply.headers);
1457
1502
  response.writeHead(reply.status, headers);
1458
1503
  } catch {
1459
1504
  discardStream(reply.body instanceof ReadableStream ? reply.body : null);
@@ -1472,14 +1517,17 @@ async function writeNodeReply(response, reply) {
1472
1517
  }
1473
1518
  if (reply.eventStream === true && response.isLightMyRequest !== true) response.setTimeout(0);
1474
1519
  if (body instanceof Uint8Array) {
1475
- response.end(body);
1520
+ response.end(reply.text ?? body);
1476
1521
  return;
1477
1522
  }
1478
- const stream = body instanceof Blob ? body.stream() : body;
1523
+ const fail = () => {
1524
+ if (!response.destroyed) response.destroy();
1525
+ };
1479
1526
  try {
1480
- await pipeline(Readable.fromWeb(stream), response);
1527
+ const stream = body instanceof Blob ? body.stream() : body;
1528
+ pipeline(Readable.fromWeb(stream), response).catch(fail);
1481
1529
  } catch {
1482
- if (!response.destroyed) response.destroy();
1530
+ fail();
1483
1531
  }
1484
1532
  }
1485
1533
  /**
@@ -1507,41 +1555,58 @@ function writeNodeFailure(response) {
1507
1555
  response.setHeader("content-type", "application/json; charset=utf-8");
1508
1556
  response.end("{\"statusCode\":500,\"error\":\"Internal Server Error\"}");
1509
1557
  }
1558
+ /** The tracker of a socket (see {@link ConnectionTracker}). */
1559
+ const TRACKER = Symbol("znight.tracker");
1560
+ /** The number of requests in progress on a tracked socket. */
1561
+ const ACTIVE = Symbol("znight.active");
1510
1562
  /**
1511
1563
  * Tracks the connections of a `node:http` server to close them on shutdown: idle keep-alive
1512
1564
  * connections first (graceful), every connection when forced. `server.closeIdleConnections()` /
1513
- * `closeAllConnections()` are not reliable on every runtime (Bun), so sockets are tracked here.
1565
+ * `closeAllConnections()` are not reliable on every runtime (Bun), so sockets are tracked here. The
1566
+ * count of requests in progress lives on the socket, and one shared `close` listener per response
1567
+ * decrements it (no closure, no map lookup per request).
1514
1568
  */
1515
- var ConnectionTracker = class {
1516
- /** Socket → number of requests in progress. */
1517
- #sockets = /* @__PURE__ */ new Map();
1569
+ var ConnectionTracker = class ConnectionTracker {
1570
+ #sockets = /* @__PURE__ */ new Set();
1518
1571
  #closing = false;
1519
- constructor(server) {
1572
+ /**
1573
+ * `requests: false` when the caller's own `request` listener calls {@link track} for every request
1574
+ * of the server (one listener less to call per request).
1575
+ */
1576
+ constructor(server, options = {}) {
1520
1577
  server.on("connection", (socket) => {
1521
- this.#sockets.set(socket, 0);
1578
+ socket[TRACKER] = this;
1579
+ socket[ACTIVE] = 0;
1580
+ this.#sockets.add(socket);
1522
1581
  socket.once("close", () => this.#sockets.delete(socket));
1523
1582
  });
1524
- server.on("request", (request, response) => {
1525
- const socket = request.socket;
1526
- if (!this.#sockets.has(socket)) return;
1527
- this.#sockets.set(socket, (this.#sockets.get(socket) ?? 0) + 1);
1528
- response.once("close", () => {
1529
- const active = this.#sockets.get(socket);
1530
- if (active === void 0) return;
1531
- this.#sockets.set(socket, active - 1);
1532
- if (active - 1 === 0 && this.#closing) socket.end();
1533
- });
1534
- });
1583
+ if (options.requests !== false) server.on("request", (request, response) => this.track(request, response));
1535
1584
  }
1585
+ /** Counts a request in progress on its connection, until its response closes (other servers' requests are ignored). */
1586
+ track(request, response) {
1587
+ const socket = request.socket;
1588
+ if (socket?.[TRACKER] !== this) return;
1589
+ socket[ACTIVE] = (socket[ACTIVE] ?? 0) + 1;
1590
+ response.on("close", ConnectionTracker.#onResponseClose);
1591
+ }
1592
+ /** A response closed (`this`): one request less on its socket, ended when idle while closing. */
1593
+ static #onResponseClose = function() {
1594
+ const socket = this.req?.socket;
1595
+ const tracker = socket?.[TRACKER];
1596
+ if (socket == null || tracker === void 0) return;
1597
+ const active = (socket[ACTIVE] ?? 1) - 1;
1598
+ socket[ACTIVE] = active;
1599
+ if (active === 0 && tracker.#closing) socket.end();
1600
+ };
1536
1601
  /** Ends idle connections now and the others once their requests completed. */
1537
1602
  closeIdle() {
1538
1603
  this.#closing = true;
1539
- for (const [socket, active] of this.#sockets) if (active === 0) socket.end();
1604
+ for (const socket of this.#sockets) if (socket[ACTIVE] === 0) socket.end();
1540
1605
  }
1541
1606
  /** Destroys every connection. */
1542
1607
  destroyAll() {
1543
1608
  this.#closing = true;
1544
- for (const socket of this.#sockets.keys()) socket.destroy();
1609
+ for (const socket of this.#sockets) socket.destroy();
1545
1610
  }
1546
1611
  };
1547
1612
  /** Closes a `node:http` server: graceful (idle connections ended), then forced when asked. */
@@ -1561,6 +1626,6 @@ function closeServer(server, tracker, closing, force) {
1561
1626
  }
1562
1627
  return promise;
1563
1628
  }
1564
- export { UnprocessableEntityException as $, serializeCookie as A, ConflictException as B, decodeCookieValue as C, parseCookieHeader as D, isCookieName as E, isStreamSource as F, InternalServerErrorException as G, GatewayTimeoutException as H, toByteStream as I, NotImplementedException as J, MethodNotAllowedException as K, trackStream as L, captureContext as M, discardStream as N, parseSetCookie as O, isByteStream as P, UnauthorizedException as Q, BadGatewayException as R, checkCookie as S, givenCookieKey as T, GoneException as U, ForbiddenException as V, HttpException as W, ServiceUnavailableException as X, PayloadTooLargeException as Y, TooManyRequestsException as Z, lightStreamResponse as _, nodeBodyStream as a, isBodylessStatus as at, webBodyStream as b, readNodeBody as c, writeNodeReply as d, UnsupportedMediaTypeException as et, bufferInjectStream as f, lightInjectOptions as g, isEventStreamType as h, nodeAdapterRequest as i, toIssueDetail as it, serializeResponseCookie as j, serializeClearCookie as k, withNativeCookies as l, injectNodeStream as m, closeServer as n, isHttpException as nt, normalizeNodeHeaders as o, isValidStatus as ot, injectFetchStream as p, NotFoundException as q, isEncrypted as r, issue as rt, onResponseAbort as s, statusText as st, ConnectionTracker as t, ValidationException as tt, writeNodeFailure as u, readWebStream as v, encodeCookieValue as w, DEFAULT_COOKIE_OPTIONS as x, serverAddress as y, BadRequestException as z };
1629
+ export { TooManyRequestsException as $, parseSetCookie as A, BadGatewayException as B, DEFAULT_COOKIE_OPTIONS as C, givenCookieKey as D, encodeCookieValue as E, discardStream as F, GoneException as G, ConflictException as H, isByteStream as I, MethodNotAllowedException as J, HttpException as K, isStreamSource as L, serializeCookie as M, serializeResponseCookie as N, isCookieName as O, captureContext as P, ServiceUnavailableException as Q, toByteStream as R, webBodyStream as S, decodeCookieValue as T, ForbiddenException as U, BadRequestException as V, GatewayTimeoutException as W, NotImplementedException as X, NotFoundException as Y, PayloadTooLargeException as Z, lightInjectOptions as _, nodeBodyStream as a, issue as at, readWebStream as b, readNodeBody as c, isValidStatus as ct, writeNodeReply as d, UnauthorizedException as et, bufferInjectStream as f, isEventStreamType as g, injectNodeStream as h, nodeAdapterRequest as i, isHttpException as it, serializeClearCookie as j, parseCookieHeader as k, withNativeCookies as l, statusText as lt, injectFetchStream as m, closeServer as n, UnsupportedMediaTypeException as nt, normalizeNodeHeaders as o, toIssueDetail as ot, exceedsDeclaredLength as p, InternalServerErrorException as q, isEncrypted as r, ValidationException as rt, onResponseAbort as s, isBodylessStatus as st, ConnectionTracker as t, UnprocessableEntityException as tt, writeNodeFailure as u, lightStreamResponse as v, checkCookie as w, serverAddress as x, payloadTooLarge as y, trackStream as z };
1565
1630
 
1566
- //# sourceMappingURL=node-io-BY1ZDLRQ.mjs.map
1631
+ //# sourceMappingURL=node-io-BkdRSQeg.mjs.map