@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.
- package/LICENSE +1 -1
- package/README.md +83 -32
- package/dist/{adapter-CGSceVEN.d.mts → adapter-J0UXFnNM.d.mts} +7 -1
- package/dist/compiler.d.mts +1 -1
- package/dist/compiler.mjs +82 -13
- package/dist/compiler.mjs.map +1 -1
- package/dist/express.d.mts +1 -1
- package/dist/express.mjs +3 -2
- package/dist/express.mjs.map +1 -1
- package/dist/fastify.d.mts +1 -1
- package/dist/fastify.mjs +254 -42
- package/dist/fastify.mjs.map +1 -1
- package/dist/hono.d.mts +1 -1
- package/dist/hono.mjs +272 -67
- package/dist/hono.mjs.map +1 -1
- package/dist/index.d.mts +10 -2
- package/dist/index.mjs +930 -278
- package/dist/index.mjs.map +1 -1
- package/dist/{node-BUnkdSsa.mjs → node-DnYv_AMI.mjs} +4 -3
- package/dist/{node-BUnkdSsa.mjs.map → node-DnYv_AMI.mjs.map} +1 -1
- package/dist/{node-io-BY1ZDLRQ.mjs → node-io-BkdRSQeg.mjs} +151 -86
- package/dist/{node-io-BY1ZDLRQ.mjs.map → node-io-BkdRSQeg.mjs.map} +1 -1
- package/dist/node.d.mts +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/adapter.ts +6 -0
- package/src/adapters/express.ts +4 -1
- package/src/adapters/fastify.ts +329 -44
- package/src/adapters/hono.ts +341 -86
- package/src/adapters/node-io.ts +185 -82
- package/src/adapters/node.ts +4 -1
- package/src/body.ts +31 -2
- package/src/coerce.ts +31 -0
- package/src/compiler/generate.ts +127 -22
- package/src/context.ts +151 -78
- package/src/index.ts +1 -1
- package/src/pipeline.ts +809 -208
- package/src/plugin.ts +10 -4
- package/src/query.ts +7 -1
- package/src/response.ts +6 -8
- package/src/serialize.ts +83 -16
- 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-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
|
-
|
|
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,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
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
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
|
|
1443
|
-
*
|
|
1487
|
+
* Writes a reply to a `ServerResponse`, piping a stream body (a failing stream destroys the
|
|
1488
|
+
* response). Never throws.
|
|
1444
1489
|
*/
|
|
1445
|
-
|
|
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
|
|
1523
|
+
const fail = () => {
|
|
1524
|
+
if (!response.destroyed) response.destroy();
|
|
1525
|
+
};
|
|
1479
1526
|
try {
|
|
1480
|
-
|
|
1527
|
+
const stream = body instanceof Blob ? body.stream() : body;
|
|
1528
|
+
pipeline(Readable.fromWeb(stream), response).catch(fail);
|
|
1481
1529
|
} catch {
|
|
1482
|
-
|
|
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
|
-
|
|
1517
|
-
#sockets = /* @__PURE__ */ new Map();
|
|
1569
|
+
var ConnectionTracker = class ConnectionTracker {
|
|
1570
|
+
#sockets = /* @__PURE__ */ new Set();
|
|
1518
1571
|
#closing = false;
|
|
1519
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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 {
|
|
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-
|
|
1631
|
+
//# sourceMappingURL=node-io-BkdRSQeg.mjs.map
|