@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.
- package/README.md +154 -35
- package/dist/compiler.d.mts +77 -2
- package/dist/compiler.mjs +259 -101
- package/dist/compiler.mjs.map +1 -1
- package/dist/express.mjs +3 -2
- package/dist/express.mjs.map +1 -1
- package/dist/fastify.mjs +200 -23
- package/dist/fastify.mjs.map +1 -1
- package/dist/hono.mjs +174 -43
- package/dist/hono.mjs.map +1 -1
- package/dist/index.d.mts +36 -4
- package/dist/index.mjs +1005 -257
- package/dist/index.mjs.map +1 -1
- package/dist/{node-Clm1Ia7T.mjs → node-DnYv_AMI.mjs} +4 -3
- package/dist/{node-Clm1Ia7T.mjs.map → node-DnYv_AMI.mjs.map} +1 -1
- package/dist/{node-io-CVnqpVNS.mjs → node-io-BkdRSQeg.mjs} +99 -60
- package/dist/{node-io-CVnqpVNS.mjs.map → node-io-BkdRSQeg.mjs.map} +1 -1
- package/dist/node.mjs +1 -1
- package/dist/utils-DIWdzVht.mjs +46 -0
- package/dist/utils-DIWdzVht.mjs.map +1 -0
- package/package.json +4 -4
- package/src/adapters/express.ts +4 -1
- package/src/adapters/fastify.ts +265 -25
- package/src/adapters/hono.ts +225 -58
- package/src/adapters/node-io.ts +128 -58
- package/src/adapters/node.ts +4 -1
- package/src/body.ts +31 -2
- package/src/coerce.ts +31 -0
- package/src/compiler/analyze.ts +3 -3
- package/src/compiler/generate.ts +127 -22
- package/src/compiler/index.ts +13 -0
- package/src/compiler/route-decorators.ts +45 -2
- package/src/compiler/route-groups.ts +216 -0
- package/src/compiler/validate.ts +15 -126
- package/src/context.ts +148 -78
- package/src/decorators.ts +19 -3
- package/src/index.ts +2 -1
- package/src/pipeline.ts +936 -177
- package/src/plugin-routes.ts +19 -0
- package/src/plugin.ts +7 -3
- package/src/response.ts +6 -8
- package/src/serialize.ts +48 -21
- package/src/utils.ts +25 -2
- package/src/validate.ts +28 -12
- package/dist/utils-DCbu3a4w.mjs +0 -27
- package/dist/utils-DCbu3a4w.mjs.map +0 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { d as writeNodeReply, f as bufferInjectStream,
|
|
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-
|
|
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
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
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
|
-
|
|
1543
|
-
#sockets = /* @__PURE__ */ new Map();
|
|
1569
|
+
var ConnectionTracker = class ConnectionTracker {
|
|
1570
|
+
#sockets = /* @__PURE__ */ new Set();
|
|
1544
1571
|
#closing = false;
|
|
1545
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
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
|
|
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
|
|
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 {
|
|
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-
|
|
1631
|
+
//# sourceMappingURL=node-io-BkdRSQeg.mjs.map
|