@znight/http 0.1.2 → 0.1.4

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 (46) hide show
  1. package/README.md +154 -35
  2. package/dist/compiler.d.mts +77 -2
  3. package/dist/compiler.mjs +259 -101
  4. package/dist/compiler.mjs.map +1 -1
  5. package/dist/express.mjs +3 -2
  6. package/dist/express.mjs.map +1 -1
  7. package/dist/fastify.mjs +200 -23
  8. package/dist/fastify.mjs.map +1 -1
  9. package/dist/hono.mjs +174 -43
  10. package/dist/hono.mjs.map +1 -1
  11. package/dist/index.d.mts +36 -4
  12. package/dist/index.mjs +1005 -257
  13. package/dist/index.mjs.map +1 -1
  14. package/dist/{node-Clm1Ia7T.mjs → node-DnYv_AMI.mjs} +4 -3
  15. package/dist/{node-Clm1Ia7T.mjs.map → node-DnYv_AMI.mjs.map} +1 -1
  16. package/dist/{node-io-CVnqpVNS.mjs → node-io-BkdRSQeg.mjs} +99 -60
  17. package/dist/{node-io-CVnqpVNS.mjs.map → node-io-BkdRSQeg.mjs.map} +1 -1
  18. package/dist/node.mjs +1 -1
  19. package/dist/utils-DIWdzVht.mjs +46 -0
  20. package/dist/utils-DIWdzVht.mjs.map +1 -0
  21. package/package.json +4 -4
  22. package/src/adapters/express.ts +4 -1
  23. package/src/adapters/fastify.ts +265 -25
  24. package/src/adapters/hono.ts +225 -58
  25. package/src/adapters/node-io.ts +128 -58
  26. package/src/adapters/node.ts +4 -1
  27. package/src/body.ts +31 -2
  28. package/src/coerce.ts +31 -0
  29. package/src/compiler/analyze.ts +3 -3
  30. package/src/compiler/generate.ts +127 -22
  31. package/src/compiler/index.ts +13 -0
  32. package/src/compiler/route-decorators.ts +45 -2
  33. package/src/compiler/route-groups.ts +216 -0
  34. package/src/compiler/validate.ts +15 -126
  35. package/src/context.ts +148 -78
  36. package/src/decorators.ts +19 -3
  37. package/src/index.ts +2 -1
  38. package/src/pipeline.ts +936 -177
  39. package/src/plugin-routes.ts +19 -0
  40. package/src/plugin.ts +7 -3
  41. package/src/response.ts +6 -8
  42. package/src/serialize.ts +48 -21
  43. package/src/utils.ts +25 -2
  44. package/src/validate.ts +28 -12
  45. package/dist/utils-DCbu3a4w.mjs +0 -27
  46. 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-CVnqpVNS.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-Clm1Ia7T.mjs.map
184
+ //# sourceMappingURL=node-DnYv_AMI.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"node-Clm1Ia7T.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,9 +1421,10 @@ 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 = {}) {
1424
+ function nodeAdapterRequest(request, response, options = NO_OPTIONS) {
1404
1425
  return new NodeAdapterRequest(request, response, options);
1405
1426
  }
1427
+ const NO_OPTIONS = Object.freeze({});
1406
1428
  /** A Node.js request/response pair as an {@link AdapterRequest} (a class: no closure per request). */
1407
1429
  var NodeAdapterRequest = class {
1408
1430
  method;
@@ -1476,7 +1498,7 @@ function writeNodeReply(response, reply) {
1476
1498
  return;
1477
1499
  }
1478
1500
  try {
1479
- 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);
1480
1502
  response.writeHead(reply.status, headers);
1481
1503
  } catch {
1482
1504
  discardStream(reply.body instanceof ReadableStream ? reply.body : null);
@@ -1533,41 +1555,58 @@ function writeNodeFailure(response) {
1533
1555
  response.setHeader("content-type", "application/json; charset=utf-8");
1534
1556
  response.end("{\"statusCode\":500,\"error\":\"Internal Server Error\"}");
1535
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");
1536
1562
  /**
1537
1563
  * Tracks the connections of a `node:http` server to close them on shutdown: idle keep-alive
1538
1564
  * connections first (graceful), every connection when forced. `server.closeIdleConnections()` /
1539
- * `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).
1540
1568
  */
1541
- var ConnectionTracker = class {
1542
- /** Socket → number of requests in progress. */
1543
- #sockets = /* @__PURE__ */ new Map();
1569
+ var ConnectionTracker = class ConnectionTracker {
1570
+ #sockets = /* @__PURE__ */ new Set();
1544
1571
  #closing = false;
1545
- 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 = {}) {
1546
1577
  server.on("connection", (socket) => {
1547
- this.#sockets.set(socket, 0);
1578
+ socket[TRACKER] = this;
1579
+ socket[ACTIVE] = 0;
1580
+ this.#sockets.add(socket);
1548
1581
  socket.once("close", () => this.#sockets.delete(socket));
1549
1582
  });
1550
- server.on("request", (request, response) => {
1551
- const socket = request.socket;
1552
- if (!this.#sockets.has(socket)) return;
1553
- this.#sockets.set(socket, (this.#sockets.get(socket) ?? 0) + 1);
1554
- response.once("close", () => {
1555
- const active = this.#sockets.get(socket);
1556
- if (active === void 0) return;
1557
- this.#sockets.set(socket, active - 1);
1558
- if (active - 1 === 0 && this.#closing) socket.end();
1559
- });
1560
- });
1583
+ if (options.requests !== false) server.on("request", (request, response) => this.track(request, response));
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);
1561
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
+ };
1562
1601
  /** Ends idle connections now and the others once their requests completed. */
1563
1602
  closeIdle() {
1564
1603
  this.#closing = true;
1565
- 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();
1566
1605
  }
1567
1606
  /** Destroys every connection. */
1568
1607
  destroyAll() {
1569
1608
  this.#closing = true;
1570
- for (const socket of this.#sockets.keys()) socket.destroy();
1609
+ for (const socket of this.#sockets) socket.destroy();
1571
1610
  }
1572
1611
  };
1573
1612
  /** Closes a `node:http` server: graceful (idle connections ended), then forced when asked. */
@@ -1587,6 +1626,6 @@ function closeServer(server, tracker, closing, force) {
1587
1626
  }
1588
1627
  return promise;
1589
1628
  }
1590
- 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 };
1591
1630
 
1592
- //# sourceMappingURL=node-io-CVnqpVNS.mjs.map
1631
+ //# sourceMappingURL=node-io-BkdRSQeg.mjs.map