@znight/http 0.1.2 → 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/dist/express.mjs CHANGED
@@ -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 { toRegExpSource } from "./path.mjs";
3
3
  import { createServer } from "node:http";
4
4
  import express from "express";
@@ -66,6 +66,7 @@ var ExpressHttpAdapter = class {
66
66
  this.#fallback(adapterRequest(request, response), error).then((reply) => writeNodeReply(response, reply), () => writeNodeFailure(response));
67
67
  }
68
68
  #listener = (request, response) => {
69
+ this.#tracker?.track(request, response);
69
70
  this.app(request, response);
70
71
  };
71
72
  async listen(port, host) {
@@ -73,7 +74,7 @@ var ExpressHttpAdapter = class {
73
74
  const server = this.#givenServer ?? createServer();
74
75
  if (!server.listeners("request").includes(this.#listener)) server.on("request", this.#listener);
75
76
  this.#server = server;
76
- this.#tracker ??= new ConnectionTracker(server);
77
+ this.#tracker ??= new ConnectionTracker(server, { requests: false });
77
78
  this.#closing = void 0;
78
79
  await new Promise((resolve, reject) => {
79
80
  const onError = (error) => reject(error);
@@ -1 +1 @@
1
- {"version":3,"file":"express.mjs","names":[],"sources":["../src/adapters/express.ts"],"sourcesContent":["/**\n * `@znight/http/express`: serves Znight routes with [Express](https://expressjs.com) 5 (optional\n * peer dependency `express`). Routes are mounted on a dedicated router, after the middleware of the\n * application you pass. `inject()` uses the optional peer `light-my-request`.\n */\nimport type { IncomingMessage, ServerResponse } from 'node:http'\nimport type {\n AdapterCloseOptions,\n AdapterRoute,\n FallbackHandler,\n HttpAdapter,\n HttpServerAddress,\n InjectRequest,\n InjectResponse,\n InjectStreamRequest,\n InjectStreamResponse,\n} from '../adapter.ts'\n\nimport { createServer, type Server } from 'node:http'\nimport express, { type Express, type NextFunction, type Request, type Response, type Router } from 'express'\n\nimport { toRegExpSource } from '../path.ts'\nimport { ConnectionTracker, closeServer, nodeAdapterRequest, writeNodeFailure, writeNodeReply } from './node-io.ts'\nimport { bufferInjectStream, injectNodeStream, serverAddress } from './shared.ts'\n\n/** Options of {@link expressAdapter}. */\nexport interface ExpressAdapterOptions {\n /**\n * An existing application: its middleware (CORS, helmet, sessions...) runs before the Znight\n * routes. Do not add body parsers for Znight routes: Znight reads bodies itself.\n */\n readonly app?: Express\n /**\n * The server to listen with (e.g. `https.createServer({ key, cert })`): the adapter adds its\n * `request` listener. Default: a new `http.createServer()`.\n */\n readonly server?: Server\n}\n\n/** The Express adapter. */\nexport interface ExpressAdapter extends HttpAdapter {\n readonly name: 'express'\n /** The Express application. */\n readonly app: Express\n /** The router holding the Znight routes (mounted on {@link app}). */\n readonly router: Router\n /** The HTTP server, once listening. */\n readonly server: Server | undefined\n}\n\n/**\n * Creates an Express adapter.\n *\n * ```ts\n * import { expressAdapter } from '@znight/http/express'\n *\n * const app = express()\n * app.use(cors())\n * HttpPlugin.configure({ adapter: expressAdapter({ app }) })\n * ```\n */\nexport function expressAdapter(options: ExpressAdapterOptions = {}): ExpressAdapter {\n return new ExpressHttpAdapter(options)\n}\n\ntype Method = 'get' | 'head' | 'post' | 'put' | 'patch' | 'delete' | 'options'\n\nclass ExpressHttpAdapter implements ExpressAdapter {\n readonly name = 'express' as const\n readonly app: Express\n readonly router: Router\n #fallback: FallbackHandler | undefined\n #mounted = false\n #server: Server | undefined\n #tracker: ConnectionTracker | undefined\n #closing: Promise<void> | undefined\n\n readonly #givenServer: Server | undefined\n\n constructor(options: ExpressAdapterOptions) {\n this.#givenServer = options.server\n if (options.app === undefined) {\n this.app = express()\n this.app.disable('x-powered-by')\n } else {\n this.app = options.app\n }\n this.router = express.Router({ caseSensitive: true, strict: true })\n }\n\n get server(): Server | undefined {\n return this.#server\n }\n\n registerRoute(route: AdapterRoute): void {\n const handler = route.handler\n // anchored regular expressions without capture groups: Express decodes nothing, Znight extracts parameters\n this.router[route.method.toLowerCase() as Method](new RegExp(toRegExpSource(route.path)), (request, response) => {\n handler(adapterRequest(request, response)).then(\n reply => writeNodeReply(response, reply),\n () => writeNodeFailure(response),\n )\n })\n }\n\n setFallbackHandler(handler: FallbackHandler): void {\n this.#fallback = handler\n }\n\n #mount(): void {\n if (this.#mounted) return\n this.#mounted = true\n // Express answers unmatched OPTIONS requests itself (200): route them to the Znight fallback\n this.router.options(/^.*$/, (request: Request, response: Response) =>\n this.#dispatchFallback(request, response, undefined),\n )\n this.app.use(this.router)\n this.app.use((request: Request, response: Response) => this.#dispatchFallback(request, response, undefined))\n this.app.use((error: unknown, request: Request, response: Response, _next: NextFunction) =>\n this.#dispatchFallback(request, response, error),\n )\n }\n\n #dispatchFallback(request: Request, response: Response, error: unknown): void {\n if (this.#fallback === undefined) {\n writeNodeFailure(response)\n return\n }\n this.#fallback(adapterRequest(request, response), error).then(\n reply => writeNodeReply(response, reply),\n () => writeNodeFailure(response),\n )\n }\n\n readonly #listener = (request: IncomingMessage, response: ServerResponse): void => {\n this.app(request as Request, response as Response)\n }\n\n async listen(port: number, host: string): Promise<HttpServerAddress> {\n this.#mount()\n const server = this.#givenServer ?? createServer()\n if (!server.listeners('request').includes(this.#listener)) server.on('request', this.#listener)\n this.#server = server\n this.#tracker ??= new ConnectionTracker(server)\n this.#closing = undefined\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 this.#mount()\n return injectNodeStream('express', this.#injectListener as (req: never, res: never) => void, request)\n }\n\n /**\n * Express replaces the prototype of `req`/`res` with its own, which drops the overrides of the\n * light-my-request fakes (payload capture, body streaming): pin them as own properties first.\n */\n readonly #injectListener = (request: IncomingMessage, response: ServerResponse): void => {\n pinPrototype(request)\n pinPrototype(response)\n this.#listener(request, response)\n }\n}\n\nfunction pinPrototype(target: object): void {\n const prototype: unknown = Object.getPrototypeOf(target)\n if (prototype === null || typeof prototype !== 'object') return\n for (const key of Object.getOwnPropertyNames(prototype)) {\n if (key === 'constructor' || Object.hasOwn(target, key)) continue\n const descriptor = Object.getOwnPropertyDescriptor(prototype, key)\n if (typeof descriptor?.value === 'function') {\n Object.defineProperty(target, key, { value: descriptor.value, writable: true, configurable: true })\n }\n }\n}\n\nfunction adapterRequest(request: Request, response: Response): ReturnType<typeof nodeAdapterRequest> {\n return nodeAdapterRequest(request, response, { url: request.originalUrl ?? request.url, ip: request.ip })\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"express.mjs","names":[],"sources":["../src/adapters/express.ts"],"sourcesContent":["/**\n * `@znight/http/express`: serves Znight routes with [Express](https://expressjs.com) 5 (optional\n * peer dependency `express`). Routes are mounted on a dedicated router, after the middleware of the\n * application you pass. `inject()` uses the optional peer `light-my-request`.\n */\nimport type { IncomingMessage, ServerResponse } from 'node:http'\nimport type {\n AdapterCloseOptions,\n AdapterRoute,\n FallbackHandler,\n HttpAdapter,\n HttpServerAddress,\n InjectRequest,\n InjectResponse,\n InjectStreamRequest,\n InjectStreamResponse,\n} from '../adapter.ts'\n\nimport { createServer, type Server } from 'node:http'\nimport express, { type Express, type NextFunction, type Request, type Response, type Router } from 'express'\n\nimport { toRegExpSource } from '../path.ts'\nimport { ConnectionTracker, closeServer, nodeAdapterRequest, writeNodeFailure, writeNodeReply } from './node-io.ts'\nimport { bufferInjectStream, injectNodeStream, serverAddress } from './shared.ts'\n\n/** Options of {@link expressAdapter}. */\nexport interface ExpressAdapterOptions {\n /**\n * An existing application: its middleware (CORS, helmet, sessions...) runs before the Znight\n * routes. Do not add body parsers for Znight routes: Znight reads bodies itself.\n */\n readonly app?: Express\n /**\n * The server to listen with (e.g. `https.createServer({ key, cert })`): the adapter adds its\n * `request` listener. Default: a new `http.createServer()`.\n */\n readonly server?: Server\n}\n\n/** The Express adapter. */\nexport interface ExpressAdapter extends HttpAdapter {\n readonly name: 'express'\n /** The Express application. */\n readonly app: Express\n /** The router holding the Znight routes (mounted on {@link app}). */\n readonly router: Router\n /** The HTTP server, once listening. */\n readonly server: Server | undefined\n}\n\n/**\n * Creates an Express adapter.\n *\n * ```ts\n * import { expressAdapter } from '@znight/http/express'\n *\n * const app = express()\n * app.use(cors())\n * HttpPlugin.configure({ adapter: expressAdapter({ app }) })\n * ```\n */\nexport function expressAdapter(options: ExpressAdapterOptions = {}): ExpressAdapter {\n return new ExpressHttpAdapter(options)\n}\n\ntype Method = 'get' | 'head' | 'post' | 'put' | 'patch' | 'delete' | 'options'\n\nclass ExpressHttpAdapter implements ExpressAdapter {\n readonly name = 'express' as const\n readonly app: Express\n readonly router: Router\n #fallback: FallbackHandler | undefined\n #mounted = false\n #server: Server | undefined\n #tracker: ConnectionTracker | undefined\n #closing: Promise<void> | undefined\n\n readonly #givenServer: Server | undefined\n\n constructor(options: ExpressAdapterOptions) {\n this.#givenServer = options.server\n if (options.app === undefined) {\n this.app = express()\n this.app.disable('x-powered-by')\n } else {\n this.app = options.app\n }\n this.router = express.Router({ caseSensitive: true, strict: true })\n }\n\n get server(): Server | undefined {\n return this.#server\n }\n\n registerRoute(route: AdapterRoute): void {\n const handler = route.handler\n // anchored regular expressions without capture groups: Express decodes nothing, Znight extracts parameters\n this.router[route.method.toLowerCase() as Method](new RegExp(toRegExpSource(route.path)), (request, response) => {\n handler(adapterRequest(request, response)).then(\n reply => writeNodeReply(response, reply),\n () => writeNodeFailure(response),\n )\n })\n }\n\n setFallbackHandler(handler: FallbackHandler): void {\n this.#fallback = handler\n }\n\n #mount(): void {\n if (this.#mounted) return\n this.#mounted = true\n // Express answers unmatched OPTIONS requests itself (200): route them to the Znight fallback\n this.router.options(/^.*$/, (request: Request, response: Response) =>\n this.#dispatchFallback(request, response, undefined),\n )\n this.app.use(this.router)\n this.app.use((request: Request, response: Response) => this.#dispatchFallback(request, response, undefined))\n this.app.use((error: unknown, request: Request, response: Response, _next: NextFunction) =>\n this.#dispatchFallback(request, response, error),\n )\n }\n\n #dispatchFallback(request: Request, response: Response, error: unknown): void {\n if (this.#fallback === undefined) {\n writeNodeFailure(response)\n return\n }\n this.#fallback(adapterRequest(request, response), error).then(\n reply => writeNodeReply(response, reply),\n () => writeNodeFailure(response),\n )\n }\n\n readonly #listener = (request: IncomingMessage, response: ServerResponse): void => {\n // the listening server's connections (an injected request is ignored)\n this.#tracker?.track(request, response)\n this.app(request as Request, response as Response)\n }\n\n async listen(port: number, host: string): Promise<HttpServerAddress> {\n this.#mount()\n const server = this.#givenServer ?? createServer()\n if (!server.listeners('request').includes(this.#listener)) server.on('request', this.#listener)\n this.#server = server\n // `#listener` counts the requests\n this.#tracker ??= new ConnectionTracker(server, { requests: false })\n this.#closing = undefined\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 this.#mount()\n return injectNodeStream('express', this.#injectListener as (req: never, res: never) => void, request)\n }\n\n /**\n * Express replaces the prototype of `req`/`res` with its own, which drops the overrides of the\n * light-my-request fakes (payload capture, body streaming): pin them as own properties first.\n */\n readonly #injectListener = (request: IncomingMessage, response: ServerResponse): void => {\n pinPrototype(request)\n pinPrototype(response)\n this.#listener(request, response)\n }\n}\n\nfunction pinPrototype(target: object): void {\n const prototype: unknown = Object.getPrototypeOf(target)\n if (prototype === null || typeof prototype !== 'object') return\n for (const key of Object.getOwnPropertyNames(prototype)) {\n if (key === 'constructor' || Object.hasOwn(target, key)) continue\n const descriptor = Object.getOwnPropertyDescriptor(prototype, key)\n if (typeof descriptor?.value === 'function') {\n Object.defineProperty(target, key, { value: descriptor.value, writable: true, configurable: true })\n }\n }\n}\n\nfunction adapterRequest(request: Request, response: Response): ReturnType<typeof nodeAdapterRequest> {\n return nodeAdapterRequest(request, response, { url: request.originalUrl ?? request.url, ip: request.ip })\n}\n"],"mappings":"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"}
package/dist/fastify.mjs CHANGED
@@ -1,6 +1,7 @@
1
- import { N as discardStream, _ as lightStreamResponse, a as nodeBodyStream, c as readNodeBody, d as writeNodeReply, f as bufferInjectStream, g as lightInjectOptions, l as withNativeCookies, o as normalizeNodeHeaders, r as isEncrypted, s as onResponseAbort, t as ConnectionTracker, y as serverAddress } from "./node-io-CVnqpVNS.mjs";
1
+ import { F as discardStream, _ as lightInjectOptions, a as nodeBodyStream, c as readNodeBody, d as writeNodeReply, f as bufferInjectStream, l as withNativeCookies, o as normalizeNodeHeaders, r as isEncrypted, s as onResponseAbort, t as ConnectionTracker, v as lightStreamResponse, x as serverAddress } from "./node-io-BkdRSQeg.mjs";
2
2
  import { toFastifyPath } from "./path.mjs";
3
3
  import { Readable } from "node:stream";
4
+ import { tracingChannel } from "node:diagnostics_channel";
4
5
  import Fastify from "fastify";
5
6
  // src/adapters/fastify.ts
6
7
  /**
@@ -18,11 +19,30 @@ function fastifyAdapter(options = {}) {
18
19
  const MAX_PARAM_LENGTH = 8192;
19
20
  /** `content-type` headers hidden from Fastify (see `#register`), by request. */
20
21
  const HIDDEN_CONTENT_TYPES = /* @__PURE__ */ new WeakMap();
22
+ /**
23
+ * Fastify's tracing channel of route handlers: while it has subscribers (an APM), every request runs
24
+ * Fastify's whole lifecycle, so that the handler is traced.
25
+ */
26
+ const HANDLER_CHANNEL = tracingChannel("fastify.request.handler");
27
+ /** The `config` key marking a Znight route that may be served from the `onRequest` hook (see `#onRoute`). */
28
+ const DIRECT = Symbol("znight.direct");
29
+ /** The hooks that run between `onRequest` and the route handler. */
30
+ const HANDLER_HOOKS = [
31
+ "onRequest",
32
+ "preParsing",
33
+ "preValidation",
34
+ "preHandler"
35
+ ];
21
36
  var FastifyHttpAdapter = class {
22
37
  name = "fastify";
23
38
  instance;
24
39
  #routes = [];
25
40
  #tracker;
41
+ /** The Fastify handler of each Znight route (before Fastify binds it). */
42
+ #handlers = /* @__PURE__ */ new Map();
43
+ /** By Fastify route context: how its requests are served (`null`: through Fastify's lifecycle). */
44
+ #lanes = /* @__PURE__ */ new WeakMap();
45
+ #keys;
26
46
  #fallback;
27
47
  #ready;
28
48
  #closing;
@@ -58,22 +78,19 @@ var FastifyHttpAdapter = class {
58
78
  const routes = this.#routes;
59
79
  await this.instance.register((child, _options, done) => {
60
80
  child.removeAllContentTypeParsers();
61
- child.addContentTypeParser("*", (_request, _payload, done) => done(null, void 0));
62
- child.addHook("onRequest", (request, _reply, done) => {
63
- const contentType = request.headers["content-type"];
64
- if (contentType !== void 0 && request.mediaType === void 0) {
65
- HIDDEN_CONTENT_TYPES.set(request.raw, contentType);
66
- delete request.raw.headers["content-type"];
67
- }
68
- done();
69
- });
81
+ child.addContentTypeParser("*", ignoreBody);
82
+ child.addContentTypeParser(/^/, ignoreBody);
83
+ child.addHook("onRequest", this.#onRequest);
84
+ child.addHook("onRoute", this.#onRoute);
70
85
  for (const route of routes) {
71
86
  const handler = route.handler;
87
+ const fastifyHandler = (request, reply) => this.#reply(reply, handler(adapterRequest(request, reply)));
88
+ this.#handlers.set(fastifyHandler, handler);
72
89
  child.route({
73
90
  method: route.method,
74
91
  url: toFastifyPath(route.path),
75
92
  exposeHeadRoute: false,
76
- handler: (request, reply) => this.#reply(reply, handler(adapterRequest(request, reply)))
93
+ handler: fastifyHandler
77
94
  });
78
95
  }
79
96
  child.setNotFoundHandler((request, reply) => this.#reply(reply, this.#dispatchFallback(request, reply, void 0)));
@@ -82,6 +99,115 @@ var FastifyHttpAdapter = class {
82
99
  });
83
100
  await this.instance.ready();
84
101
  }
102
+ /**
103
+ * Records in the `config` of a Znight route whether it may be served from the `onRequest` hook:
104
+ * its handler is still the adapter's (no `onRoute` hook wrapped it) and no route-level hook or
105
+ * schema runs between `onRequest` and the handler. The hooks of the instance are checked on the
106
+ * first request (see `#lane`).
107
+ */
108
+ #onRoute = (options) => {
109
+ const handler = this.#handlers.get(options.handler);
110
+ if (handler === void 0 || options.schema !== void 0) return;
111
+ if (HANDLER_HOOKS.some((hook) => hasHooks(options[hook]))) return;
112
+ const route = {
113
+ handler,
114
+ bare: !hasHooks(options.onSend) && !hasHooks(options.onResponse)
115
+ };
116
+ options.config = {
117
+ ...options.config,
118
+ [DIRECT]: route
119
+ };
120
+ };
121
+ /**
122
+ * The `onRequest` hook of the Znight context. A route request that no later hook, schema or
123
+ * tracing subscriber needs (see `#lane`) is served from here: Fastify skips its content type
124
+ * parsing and the steps between this hook and the handler, which do nothing for Znight routes.
125
+ * Otherwise the request goes on through Fastify's lifecycle.
126
+ */
127
+ #onRequest = (request, reply, done) => {
128
+ const lane = this.#lane(request, reply);
129
+ if (lane !== null && !HANDLER_CHANNEL.hasSubscribers) {
130
+ this.#serve(lane, request, reply);
131
+ return;
132
+ }
133
+ const contentType = request.headers["content-type"];
134
+ if (contentType !== void 0 && request.mediaType === void 0) {
135
+ HIDDEN_CONTENT_TYPES.set(request.raw, contentType);
136
+ delete request.raw.headers["content-type"];
137
+ }
138
+ done();
139
+ };
140
+ /**
141
+ * How the requests of the route of `request` are served, decided once per route context: from the
142
+ * `onRequest` hook when `#onRoute` marked the route and the instance has no `preParsing`,
143
+ * `preValidation` or `preHandler` hook (the `onRequest` hook of the Znight context is then the
144
+ * last one); with a text reply written on the raw response when no `onSend` or `onResponse` hook
145
+ * runs either (see `#writeRaw`). `null`: through Fastify's lifecycle (404s, hooks, a Fastify
146
+ * version whose route context differs).
147
+ */
148
+ #lane(request, reply) {
149
+ this.#keys ??= {
150
+ context: ownSymbol(request, "fastify.context"),
151
+ headers: ownSymbol(reply, "fastify.reply.headers"),
152
+ trailers: ownSymbol(reply, "fastify.reply.trailers"),
153
+ serializer: ownSymbol(reply, "fastify.reply.serializer")
154
+ };
155
+ const key = this.#keys.context;
156
+ const context = key === null ? void 0 : request[key];
157
+ if (typeof context !== "object" || context === null) return null;
158
+ let lane = this.#lanes.get(context);
159
+ if (lane === void 0) {
160
+ lane = laneOf(context, this.#keys);
161
+ this.#lanes.set(context, lane);
162
+ }
163
+ return lane;
164
+ }
165
+ /** Serves a route request from the `onRequest` hook (see `#lane`). */
166
+ #serve(lane, request, reply) {
167
+ let pending;
168
+ try {
169
+ pending = lane.handler(new FastifyAdapterRequest(request, reply, reply));
170
+ } catch (error) {
171
+ reply.send(asError(error));
172
+ return;
173
+ }
174
+ pending.then((result) => this.#settle(lane, reply, result), () => this.#settle(lane, reply, internalError()));
175
+ }
176
+ /** Writes the reply of a request served from the `onRequest` hook; a failure is handled like Fastify does. */
177
+ #settle(lane, reply, result) {
178
+ try {
179
+ if (!lane.raw || !this.#writeRaw(reply, result)) this.#write(reply, result);
180
+ } catch (error) {
181
+ if (reply.sent) reply.log.error({ err: error }, "Promise errored, but reply.sent = true was set");
182
+ else reply.send(asError(error));
183
+ }
184
+ }
185
+ /**
186
+ * Writes a text reply on the raw response, as `reply.send(text)` would without `onSend` hook: one
187
+ * `writeHead()` with the same headers in the same order, then the text. Only when nothing set
188
+ * reply headers or trailers before (an `onRequest` hook) and Fastify would send the headers and
189
+ * text unchanged (content type, exact `content-length`). Returns whether it wrote the reply.
190
+ */
191
+ #writeRaw(reply, result) {
192
+ const text = result.text;
193
+ const body = result.body;
194
+ if (text === void 0 || !(body instanceof Uint8Array) || result.raw === true) return false;
195
+ const headers = result.headers;
196
+ const length = headers["content-length"];
197
+ if (typeof length !== "string" || Number(length) !== body.byteLength) return false;
198
+ const type = headers["content-type"];
199
+ if (!sendsTextAsIs(type) || type !== JSON_TYPE && !MEDIA_TYPE.test(type)) return false;
200
+ const response = reply.raw;
201
+ if (reply.sent || response.destroyed || response.headersSent || reply.request.raw.httpVersionMajor !== 1) return false;
202
+ const keys = this.#keys;
203
+ const state = reply;
204
+ if (keys.headers === null || keys.trailers === null || state[keys.trailers] !== null) return false;
205
+ if (keys.serializer === null || state[keys.serializer] !== null) return false;
206
+ if (!isEmptyRecord(state[keys.headers])) return false;
207
+ response.writeHead(result.status, headers);
208
+ response.end(text);
209
+ return true;
210
+ }
85
211
  #dispatchFallback(request, reply, error) {
86
212
  if (this.#fallback === void 0) return Promise.reject(error ?? /* @__PURE__ */ new Error("No fallback handler"));
87
213
  return this.#fallback(adapterRequest(request, void 0, reply), error);
@@ -91,15 +217,16 @@ var FastifyHttpAdapter = class {
91
217
  try {
92
218
  result = await pending;
93
219
  } catch {
94
- result = {
95
- status: 500,
96
- headers: { "content-type": "application/json; charset=utf-8" },
97
- body: new TextEncoder().encode("{\"statusCode\":500,\"error\":\"Internal Server Error\"}")
98
- };
220
+ result = internalError();
99
221
  }
222
+ this.#write(reply, result);
223
+ return reply;
224
+ }
225
+ /** Writes a reply through Fastify (`reply.send()`: `onSend` hooks...). */
226
+ #write(reply, result) {
100
227
  if (result.raw === true || reply.sent || reply.raw.destroyed) {
101
228
  discardStream(result.body instanceof ReadableStream ? result.body : null);
102
- return reply;
229
+ return;
103
230
  }
104
231
  if (result.eventStream === true) {
105
232
  const native = lowerCaseHeaders(reply.getHeaders());
@@ -112,18 +239,23 @@ var FastifyHttpAdapter = class {
112
239
  ...result,
113
240
  headers
114
241
  });
115
- return reply;
242
+ return;
116
243
  }
117
244
  reply.code(result.status);
118
- for (const name of Object.keys(result.headers)) reply.header(name, result.headers[name]);
245
+ const headers = result.headers;
246
+ for (const name of Object.keys(headers)) reply.header(name, headers[name]);
119
247
  const body = result.body;
120
- if (body === null) return reply.send();
248
+ if (body === null) {
249
+ reply.send();
250
+ return;
251
+ }
121
252
  if (body instanceof Uint8Array) {
122
- if (result.text !== void 0 && sendsTextAsIs(result.headers["content-type"])) return reply.send(result.text);
123
- return reply.send(Buffer.isBuffer(body) ? body : Buffer.from(body.buffer, body.byteOffset, body.byteLength));
253
+ if (result.text !== void 0 && sendsTextAsIs(headers["content-type"])) reply.send(result.text);
254
+ else reply.send(Buffer.isBuffer(body) ? body : Buffer.from(body.buffer, body.byteOffset, body.byteLength));
255
+ return;
124
256
  }
125
257
  const stream = body instanceof Blob ? body.stream() : body;
126
- return reply.send(Readable.fromWeb(stream));
258
+ reply.send(Readable.fromWeb(stream));
127
259
  }
128
260
  async listen(port, host) {
129
261
  await this.#prepare();
@@ -157,12 +289,57 @@ var FastifyHttpAdapter = class {
157
289
  return lightStreamResponse(response, request.signal);
158
290
  }
159
291
  };
292
+ /**
293
+ * The lane of a route context (see `FastifyHttpAdapter.#lane`). Once the instance is ready, every
294
+ * lifecycle hook list of a context is `null` or a non-empty array.
295
+ */
296
+ function laneOf(context, keys) {
297
+ const route = context.config?.[DIRECT];
298
+ if (route === void 0) return null;
299
+ if (context.preParsing !== null || context.preValidation !== null || context.preHandler !== null) return null;
300
+ const raw = route.bare && context.onSend === null && context.onResponse === null;
301
+ return {
302
+ handler: route.handler,
303
+ raw: raw && keys.headers !== null && keys.trailers !== null
304
+ };
305
+ }
306
+ /** The content type parser of the Znight context: the body is left unread (Znight reads it). */
307
+ function ignoreBody(_request, _payload, done) {
308
+ done(null, void 0);
309
+ }
310
+ /** Whether a route option holds hooks (a function or a non-empty array). */
311
+ function hasHooks(value) {
312
+ return value !== void 0 && value !== null && !(Array.isArray(value) && value.length === 0);
313
+ }
314
+ /** An own symbol-keyed property of a Fastify object, by the symbol's description (internal state). */
315
+ function ownSymbol(target, description) {
316
+ for (const symbol of Object.getOwnPropertySymbols(target)) if (symbol.description === description) return symbol;
317
+ return null;
318
+ }
319
+ function isEmptyRecord(value) {
320
+ if (typeof value !== "object" || value === null) return false;
321
+ for (const _key in value) return false;
322
+ return true;
323
+ }
324
+ function asError(error) {
325
+ return error instanceof Error ? error : new Error(String(error), { cause: error });
326
+ }
327
+ /** The last-resort 500 of a handler promise that rejected (the pipeline maps errors: should not happen). */
328
+ function internalError() {
329
+ return {
330
+ status: 500,
331
+ headers: { "content-type": "application/json; charset=utf-8" },
332
+ body: new TextEncoder().encode("{\"statusCode\":500,\"error\":\"Internal Server Error\"}")
333
+ };
334
+ }
160
335
  /** The JSON content type of the plugin (with a charset). */
161
336
  const JSON_TYPE = "application/json; charset=utf-8";
162
337
  /**
163
338
  * Whether Fastify sends a string with this content type unchanged: it adds a charset to a JSON
164
339
  * type without one, and a content type to a string without one.
165
340
  */
341
+ /** A `type/subtype` media type, optionally followed by parameters: what Fastify's `ContentType` accepts. */
342
+ const MEDIA_TYPE = /^\s*[\w!#$%&'*+.^`|~-]+\/[\w!#$%&'*+.^`|~-]+\s*(?:;|$)/;
166
343
  function sendsTextAsIs(type) {
167
344
  if (typeof type !== "string") return false;
168
345
  if (type === JSON_TYPE) return true;
@@ -1 +1 @@
1
- {"version":3,"file":"fastify.mjs","names":[],"sources":["../src/adapters/fastify.ts"],"sourcesContent":["/**\n * `@znight/http/fastify`: serves Znight routes with [Fastify](https://fastify.dev) 5 (optional\n * peer dependency `fastify`). Routing uses Fastify's router; body parsing, validation and\n * serialisation stay Znight's, so responses are identical to the other adapters.\n */\nimport type {\n AdapterCloseOptions,\n AdapterHandler,\n AdapterReply,\n AdapterRequest,\n AdapterRoute,\n FallbackHandler,\n HttpAdapter,\n HttpServerAddress,\n InjectRequest,\n InjectResponse,\n InjectStreamRequest,\n InjectStreamResponse,\n} from '../adapter.ts'\nimport type { HeaderValue } from '../exceptions.ts'\n\nimport { Readable } from 'node:stream'\nimport Fastify, {\n type FastifyInstance,\n type FastifyReply,\n type FastifyRequest,\n type FastifyServerOptions,\n} from 'fastify'\n\nimport { toFastifyPath } from '../path.ts'\nimport { discardStream } from '../stream.ts'\nimport {\n ConnectionTracker,\n isEncrypted,\n nodeBodyStream,\n normalizeNodeHeaders,\n onResponseAbort,\n readNodeBody,\n withNativeCookies,\n writeNodeReply,\n} from './node-io.ts'\nimport {\n bufferInjectStream,\n type LightInjectOptions,\n lightInjectOptions,\n lightStreamResponse,\n serverAddress,\n} from './shared.ts'\n\n/** Options of {@link fastifyAdapter}. */\nexport interface FastifyAdapterOptions {\n /**\n * An existing instance (with your plugins and hooks). Znight registers its routes in an\n * encapsulated child context (its own body handling and 404 handler) and closes the instance on\n * shutdown. Malformed URLs are then answered by Fastify's `frameworkErrors` handler.\n */\n readonly instance?: FastifyInstance\n /**\n * Options of the instance created when `instance` is absent. Znight sets `exposeHeadRoutes: false`\n * (HEAD is served by Znight), a `maxParamLength` of 8 KiB and a `frameworkErrors` handler.\n */\n readonly options?: FastifyServerOptions\n}\n\n/** The Fastify adapter. */\nexport interface FastifyAdapter extends HttpAdapter {\n readonly name: 'fastify'\n /** The Fastify instance. */\n readonly instance: FastifyInstance\n}\n\n/**\n * Creates a Fastify adapter.\n *\n * ```ts\n * import { fastifyAdapter } from '@znight/http/fastify'\n *\n * HttpPlugin.configure({ adapter: fastifyAdapter({ options: { trustProxy: true } }) })\n * ```\n */\nexport function fastifyAdapter(options: FastifyAdapterOptions = {}): FastifyAdapter {\n return new FastifyHttpAdapter(options)\n}\n\nconst MAX_PARAM_LENGTH = 8192\n\n/** `content-type` headers hidden from Fastify (see `#register`), by request. */\nconst HIDDEN_CONTENT_TYPES = new WeakMap<object, string>()\n\nclass FastifyHttpAdapter implements FastifyAdapter {\n readonly name = 'fastify' as const\n readonly instance: FastifyInstance\n readonly #routes: AdapterRoute[] = []\n readonly #tracker: ConnectionTracker\n #fallback: FallbackHandler | undefined\n #ready: Promise<void> | undefined\n #closing: Promise<void> | undefined\n\n constructor(options: FastifyAdapterOptions) {\n if (options.instance === undefined) {\n const serverOptions = options.options ?? {}\n this.instance = Fastify({\n exposeHeadRoutes: false,\n frameworkErrors: (error, request, reply) => {\n this.#reply(reply, this.#dispatchFallback(request, reply, error)).catch(() => undefined)\n },\n ...serverOptions,\n routerOptions: { maxParamLength: MAX_PARAM_LENGTH, ...serverOptions.routerOptions },\n })\n } else {\n this.instance = options.instance\n }\n this.#tracker = new ConnectionTracker(this.instance.server)\n }\n\n registerRoute(route: AdapterRoute): void {\n if (this.#ready !== undefined) throw new Error('fastifyAdapter: routes must be registered before listen()/inject()')\n this.#routes.push(route)\n }\n\n setFallbackHandler(handler: FallbackHandler): void {\n this.#fallback = handler\n }\n\n #prepare(): Promise<void> {\n this.#ready ??= this.#register()\n return this.#ready\n }\n\n async #register(): Promise<void> {\n const routes = this.#routes\n await this.instance.register((child, _options, done) => {\n // Znight reads and parses bodies itself: accept every content type without consuming it\n child.removeAllContentTypeParsers()\n child.addContentTypeParser('*', (_request, _payload, done) => done(null, undefined))\n // Fastify answers 415 to a content type it cannot parse (`foo`, an empty one) before the route\n // runs: hide such a header from Fastify (Znight reads it, and parses the body as bytes)\n child.addHook('onRequest', (request, _reply, done) => {\n const contentType = request.headers['content-type']\n if (contentType !== undefined && request.mediaType === undefined) {\n HIDDEN_CONTENT_TYPES.set(request.raw, contentType)\n delete request.raw.headers['content-type']\n }\n done()\n })\n for (const route of routes) {\n const handler = route.handler\n child.route({\n method: route.method,\n url: toFastifyPath(route.path),\n exposeHeadRoute: false,\n handler: (request, reply) => this.#reply(reply, handler(adapterRequest(request, reply))),\n })\n }\n child.setNotFoundHandler((request, reply) =>\n this.#reply(reply, this.#dispatchFallback(request, reply, undefined)),\n )\n child.setErrorHandler((error, request, reply) =>\n this.#reply(reply, this.#dispatchFallback(request, reply, error)),\n )\n done()\n })\n await this.instance.ready()\n }\n\n #dispatchFallback(request: FastifyRequest, reply: FastifyReply, error: unknown): Promise<AdapterReply> {\n if (this.#fallback === undefined) return Promise.reject(error ?? new Error('No fallback handler'))\n return this.#fallback(adapterRequest(request, undefined, reply), error)\n }\n\n async #reply(reply: FastifyReply, pending: ReturnType<AdapterHandler>): Promise<FastifyReply> {\n let result: AdapterReply\n try {\n result = await pending\n } catch {\n result = {\n status: 500,\n headers: { 'content-type': 'application/json; charset=utf-8' },\n body: new TextEncoder().encode('{\"statusCode\":500,\"error\":\"Internal Server Error\"}'),\n }\n }\n if (result.raw === true || reply.sent || reply.raw.destroyed) {\n discardStream(result.body instanceof ReadableStream ? result.body : null)\n return reply\n }\n if (result.eventStream === true) {\n // no onSend hook (compression, ETag...) may rewrite an event stream: write it on the raw\n // response, with the headers Fastify hooks already set (`onRequest`...) under the plugin's,\n // and their cookies before the plugin's (as reply.header('set-cookie') does on the other path)\n const native = lowerCaseHeaders(reply.getHeaders())\n const headers = { ...native, ...withNativeCookies(native['set-cookie'], result.headers) }\n reply.hijack()\n writeNodeReply(reply.raw, { ...result, headers })\n return reply\n }\n reply.code(result.status)\n for (const name of Object.keys(result.headers)) reply.header(name, result.headers[name])\n const body = result.body\n if (body === null) return reply.send()\n if (body instanceof Uint8Array) {\n // a string is written with the headers at once (bytes take a second write)\n if (result.text !== undefined && sendsTextAsIs(result.headers['content-type'])) return reply.send(result.text)\n return reply.send(Buffer.isBuffer(body) ? body : Buffer.from(body.buffer, body.byteOffset, body.byteLength))\n }\n const stream = body instanceof Blob ? body.stream() : body\n return reply.send(Readable.fromWeb(stream as import('node:stream/web').ReadableStream))\n }\n\n async listen(port: number, host: string): Promise<HttpServerAddress> {\n await this.#prepare()\n this.#closing = undefined\n await this.instance.listen({ port, host })\n const address = this.instance.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 if (this.#closing === undefined) {\n this.#tracker.closeIdle()\n this.#closing = this.instance.close()\n }\n if (options.force === true) {\n this.instance.server.closeAllConnections?.()\n this.#tracker.destroyAll()\n }\n return this.#closing\n }\n\n inject(request: InjectRequest): Promise<InjectResponse> {\n return bufferInjectStream(stream => this.injectStream(stream), request)\n }\n\n async injectStream(request: InjectStreamRequest): Promise<InjectStreamResponse> {\n await this.#prepare()\n const options: LightInjectOptions = await lightInjectOptions(request)\n // Fastify forwards the options to light-my-request (`payloadAsStream`)\n const inject = this.instance.inject as unknown as (options: LightInjectOptions) => Promise<unknown>\n const response = await inject.call(this.instance, options)\n return lightStreamResponse(response as Parameters<typeof lightStreamResponse>[0], request.signal)\n }\n}\n\n/** The JSON content type of the plugin (with a charset). */\nconst JSON_TYPE = 'application/json; charset=utf-8'\n\n/**\n * Whether Fastify sends a string with this content type unchanged: it adds a charset to a JSON\n * type without one, and a content type to a string without one.\n */\nfunction sendsTextAsIs(type: HeaderValue | undefined): boolean {\n if (typeof type !== 'string') return false\n if (type === JSON_TYPE) return true\n const semicolon = type.indexOf(';')\n return !(semicolon === -1 ? type : type.slice(0, semicolon)).toLowerCase().includes('json')\n}\n\nfunction lowerCaseHeaders(\n headers: Readonly<Record<string, string | number | readonly string[] | undefined>>,\n): Record<string, string | string[]> {\n const result: Record<string, string | string[]> = {}\n for (const [name, value] of Object.entries(headers)) {\n if (value !== undefined) result[name.toLowerCase()] = typeof value === 'object' ? [...value] : String(value)\n }\n return result\n}\n\nfunction adapterRequest(\n request: FastifyRequest,\n reply: FastifyReply | undefined,\n response: FastifyReply | undefined = reply,\n): AdapterRequest {\n const hidden = HIDDEN_CONTENT_TYPES.get(request.raw)\n if (hidden !== undefined) {\n // Fastify is done with the content type: restore it (for Znight and `@Req()`)\n HIDDEN_CONTENT_TYPES.delete(request.raw)\n request.raw.headers['content-type'] = hidden\n }\n return new FastifyAdapterRequest(request, reply, response)\n}\n\n/** A Fastify request as an {@link AdapterRequest} (a class: no closure per request). */\nclass FastifyAdapterRequest implements AdapterRequest {\n readonly method: string\n readonly url: string\n readonly headers: Readonly<Record<string, string | undefined>>\n readonly ip: string | undefined\n readonly remoteAddress: string | undefined\n readonly encrypted: boolean\n readonly raw: FastifyRequest\n /** `@Res()`: the reply of a route, `undefined` for the fallback handler. */\n readonly rawResponse: FastifyReply | undefined\n /** The reply whose disconnection aborts the request. */\n readonly #response: FastifyReply | undefined\n #body: Promise<Uint8Array> | undefined\n\n constructor(request: FastifyRequest, reply: FastifyReply | undefined, response: FastifyReply | undefined) {\n this.method = request.raw.method ?? request.method\n this.url = request.raw.url ?? request.url\n this.headers = normalizeNodeHeaders(request.headers)\n this.ip = request.ip\n this.remoteAddress = request.raw.socket?.remoteAddress\n this.encrypted = isEncrypted(request.raw)\n this.raw = request\n this.rawResponse = reply\n this.#response = response\n }\n\n readBody(limit: number): Promise<Uint8Array> {\n this.#body ??= readNodeBody(this.raw.raw, limit)\n return this.#body\n }\n\n streamBody(limit: number): ReadableStream<Uint8Array> {\n return nodeBodyStream(this.raw.raw, limit)\n }\n\n rawStatus(): number | undefined {\n return this.rawResponse?.statusCode\n }\n\n onAbort(listener: () => void): void {\n if (this.#response !== undefined) onResponseAbort(this.#response.raw, listener)\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"fastify.mjs","names":[],"sources":["../src/adapters/fastify.ts"],"sourcesContent":["/**\n * `@znight/http/fastify`: serves Znight routes with [Fastify](https://fastify.dev) 5 (optional\n * peer dependency `fastify`). Routing uses Fastify's router; body parsing, validation and\n * serialisation stay Znight's, so responses are identical to the other adapters.\n */\n\nimport type { OutgoingHttpHeaders } from 'node:http'\nimport type {\n AdapterCloseOptions,\n AdapterHandler,\n AdapterReply,\n AdapterRequest,\n AdapterRoute,\n FallbackHandler,\n HttpAdapter,\n HttpServerAddress,\n InjectRequest,\n InjectResponse,\n InjectStreamRequest,\n InjectStreamResponse,\n} from '../adapter.ts'\nimport type { HeaderValue } from '../exceptions.ts'\n\nimport { tracingChannel } from 'node:diagnostics_channel'\nimport { Readable } from 'node:stream'\nimport Fastify, {\n type FastifyInstance,\n type FastifyReply,\n type FastifyRequest,\n type FastifyServerOptions,\n type HookHandlerDoneFunction,\n type RouteOptions,\n} from 'fastify'\n\nimport { toFastifyPath } from '../path.ts'\nimport { discardStream } from '../stream.ts'\nimport {\n ConnectionTracker,\n isEncrypted,\n nodeBodyStream,\n normalizeNodeHeaders,\n onResponseAbort,\n readNodeBody,\n withNativeCookies,\n writeNodeReply,\n} from './node-io.ts'\nimport {\n bufferInjectStream,\n type LightInjectOptions,\n lightInjectOptions,\n lightStreamResponse,\n serverAddress,\n} from './shared.ts'\n\n/** Options of {@link fastifyAdapter}. */\nexport interface FastifyAdapterOptions {\n /**\n * An existing instance (with your plugins and hooks). Znight registers its routes in an\n * encapsulated child context (its own body handling and 404 handler) and closes the instance on\n * shutdown. Malformed URLs are then answered by Fastify's `frameworkErrors` handler.\n */\n readonly instance?: FastifyInstance\n /**\n * Options of the instance created when `instance` is absent. Znight sets `exposeHeadRoutes: false`\n * (HEAD is served by Znight), a `maxParamLength` of 8 KiB and a `frameworkErrors` handler.\n */\n readonly options?: FastifyServerOptions\n}\n\n/** The Fastify adapter. */\nexport interface FastifyAdapter extends HttpAdapter {\n readonly name: 'fastify'\n /** The Fastify instance. */\n readonly instance: FastifyInstance\n}\n\n/**\n * Creates a Fastify adapter.\n *\n * ```ts\n * import { fastifyAdapter } from '@znight/http/fastify'\n *\n * HttpPlugin.configure({ adapter: fastifyAdapter({ options: { trustProxy: true } }) })\n * ```\n */\nexport function fastifyAdapter(options: FastifyAdapterOptions = {}): FastifyAdapter {\n return new FastifyHttpAdapter(options)\n}\n\nconst MAX_PARAM_LENGTH = 8192\n\n/** `content-type` headers hidden from Fastify (see `#register`), by request. */\nconst HIDDEN_CONTENT_TYPES = new WeakMap<object, string>()\n\n/**\n * Fastify's tracing channel of route handlers: while it has subscribers (an APM), every request runs\n * Fastify's whole lifecycle, so that the handler is traced.\n */\nconst HANDLER_CHANNEL = tracingChannel('fastify.request.handler')\n\n/** The `config` key marking a Znight route that may be served from the `onRequest` hook (see `#onRoute`). */\nconst DIRECT = Symbol('znight.direct')\n\n/** The hooks that run between `onRequest` and the route handler. */\nconst HANDLER_HOOKS = ['onRequest', 'preParsing', 'preValidation', 'preHandler'] as const\n\n/** What `#onRoute` records about a Znight route (in its `config`). */\ninterface DirectRoute {\n readonly handler: AdapterHandler\n /** No route-level `onSend` or `onResponse` hook. */\n readonly bare: boolean\n}\n\n/**\n * How the requests of a Fastify route context are served, decided on its first request (see\n * `#lane`): `handler` from the `onRequest` hook, and `raw` when a text reply may be written on the\n * raw response.\n */\ninterface Lane {\n readonly handler: AdapterHandler\n readonly raw: boolean\n}\n\n/**\n * The fields of Fastify's route context (`request[Symbol('fastify.context')]`) read by the adapter.\n * They are internal: the adapter only skips Fastify's lifecycle when they have the expected shape.\n */\ninterface RouteContext {\n readonly config?: Readonly<Record<PropertyKey, unknown>>\n readonly preParsing?: unknown\n readonly preValidation?: unknown\n readonly preHandler?: unknown\n readonly onSend?: unknown\n readonly onResponse?: unknown\n}\n\n/** The keys of Fastify's internal request and reply state read by the adapter (`null`: not found). */\ninterface InternalKeys {\n /** The route context of a request. */\n readonly context: symbol | null\n /** The headers set with `reply.header()`. */\n readonly headers: symbol | null\n /** The trailers set with `reply.trailer()`. */\n readonly trailers: symbol | null\n /** The serializer set with `reply.serializer()` (`null` when none). */\n readonly serializer: symbol | null\n}\n\nclass FastifyHttpAdapter implements FastifyAdapter {\n readonly name = 'fastify' as const\n readonly instance: FastifyInstance\n readonly #routes: AdapterRoute[] = []\n readonly #tracker: ConnectionTracker\n /** The Fastify handler of each Znight route (before Fastify binds it). */\n readonly #handlers = new Map<unknown, AdapterHandler>()\n /** By Fastify route context: how its requests are served (`null`: through Fastify's lifecycle). */\n readonly #lanes = new WeakMap<object, Lane | null>()\n #keys: InternalKeys | undefined\n #fallback: FallbackHandler | undefined\n #ready: Promise<void> | undefined\n #closing: Promise<void> | undefined\n\n constructor(options: FastifyAdapterOptions) {\n if (options.instance === undefined) {\n const serverOptions = options.options ?? {}\n this.instance = Fastify({\n exposeHeadRoutes: false,\n frameworkErrors: (error, request, reply) => {\n this.#reply(reply, this.#dispatchFallback(request, reply, error)).catch(() => undefined)\n },\n ...serverOptions,\n routerOptions: { maxParamLength: MAX_PARAM_LENGTH, ...serverOptions.routerOptions },\n })\n } else {\n this.instance = options.instance\n }\n this.#tracker = new ConnectionTracker(this.instance.server)\n }\n\n registerRoute(route: AdapterRoute): void {\n if (this.#ready !== undefined) throw new Error('fastifyAdapter: routes must be registered before listen()/inject()')\n this.#routes.push(route)\n }\n\n setFallbackHandler(handler: FallbackHandler): void {\n this.#fallback = handler\n }\n\n #prepare(): Promise<void> {\n this.#ready ??= this.#register()\n return this.#ready\n }\n\n async #register(): Promise<void> {\n const routes = this.#routes\n await this.instance.register((child, _options, done) => {\n // Znight reads and parses bodies itself: accept every content type without consuming it. The\n // RegExp parser matches every content type like '*' does, but Fastify caches what a RegExp\n // matched (it parses the content type again on every request to find the '*' parser)\n child.removeAllContentTypeParsers()\n child.addContentTypeParser('*', ignoreBody)\n child.addContentTypeParser(/^/, ignoreBody)\n child.addHook('onRequest', this.#onRequest)\n // added last: sees the route options once every other `onRoute` hook changed them\n child.addHook('onRoute', this.#onRoute)\n for (const route of routes) {\n const handler = route.handler\n const fastifyHandler = (request: FastifyRequest, reply: FastifyReply): Promise<FastifyReply> =>\n this.#reply(reply, handler(adapterRequest(request, reply)))\n this.#handlers.set(fastifyHandler, handler)\n child.route({\n method: route.method,\n url: toFastifyPath(route.path),\n exposeHeadRoute: false,\n handler: fastifyHandler,\n })\n }\n child.setNotFoundHandler((request, reply) =>\n this.#reply(reply, this.#dispatchFallback(request, reply, undefined)),\n )\n child.setErrorHandler((error, request, reply) =>\n this.#reply(reply, this.#dispatchFallback(request, reply, error)),\n )\n done()\n })\n await this.instance.ready()\n }\n\n /**\n * Records in the `config` of a Znight route whether it may be served from the `onRequest` hook:\n * its handler is still the adapter's (no `onRoute` hook wrapped it) and no route-level hook or\n * schema runs between `onRequest` and the handler. The hooks of the instance are checked on the\n * first request (see `#lane`).\n */\n readonly #onRoute = (options: RouteOptions): void => {\n const handler = this.#handlers.get(options.handler)\n if (handler === undefined || options.schema !== undefined) return\n if (HANDLER_HOOKS.some(hook => hasHooks(options[hook]))) return\n const route: DirectRoute = { handler, bare: !hasHooks(options.onSend) && !hasHooks(options.onResponse) }\n options.config = { ...options.config, [DIRECT]: route }\n }\n\n /**\n * The `onRequest` hook of the Znight context. A route request that no later hook, schema or\n * tracing subscriber needs (see `#lane`) is served from here: Fastify skips its content type\n * parsing and the steps between this hook and the handler, which do nothing for Znight routes.\n * Otherwise the request goes on through Fastify's lifecycle.\n */\n readonly #onRequest = (request: FastifyRequest, reply: FastifyReply, done: HookHandlerDoneFunction): void => {\n const lane = this.#lane(request, reply)\n if (lane !== null && !HANDLER_CHANNEL.hasSubscribers) {\n this.#serve(lane, request, reply)\n return\n }\n // Fastify answers 415 to a content type it cannot parse (`foo`, an empty one) before the route\n // runs: hide such a header from Fastify (Znight reads it, and parses the body as bytes)\n const contentType = request.headers['content-type']\n if (contentType !== undefined && request.mediaType === undefined) {\n HIDDEN_CONTENT_TYPES.set(request.raw, contentType)\n delete request.raw.headers['content-type']\n }\n done()\n }\n\n /**\n * How the requests of the route of `request` are served, decided once per route context: from the\n * `onRequest` hook when `#onRoute` marked the route and the instance has no `preParsing`,\n * `preValidation` or `preHandler` hook (the `onRequest` hook of the Znight context is then the\n * last one); with a text reply written on the raw response when no `onSend` or `onResponse` hook\n * runs either (see `#writeRaw`). `null`: through Fastify's lifecycle (404s, hooks, a Fastify\n * version whose route context differs).\n */\n #lane(request: FastifyRequest, reply: FastifyReply): Lane | null {\n this.#keys ??= {\n context: ownSymbol(request, 'fastify.context'),\n headers: ownSymbol(reply, 'fastify.reply.headers'),\n trailers: ownSymbol(reply, 'fastify.reply.trailers'),\n serializer: ownSymbol(reply, 'fastify.reply.serializer'),\n }\n const key = this.#keys.context\n const context = key === null ? undefined : (request as unknown as Record<symbol, unknown>)[key]\n if (typeof context !== 'object' || context === null) return null\n let lane = this.#lanes.get(context)\n if (lane === undefined) {\n lane = laneOf(context as RouteContext, this.#keys)\n this.#lanes.set(context, lane)\n }\n return lane\n }\n\n /** Serves a route request from the `onRequest` hook (see `#lane`). */\n #serve(lane: Lane, request: FastifyRequest, reply: FastifyReply): void {\n let pending: Promise<AdapterReply>\n try {\n pending = lane.handler(new FastifyAdapterRequest(request, reply, reply))\n } catch (error) {\n // what Fastify does when a route handler throws\n reply.send(asError(error))\n return\n }\n pending.then(\n result => this.#settle(lane, reply, result),\n () => this.#settle(lane, reply, internalError()),\n )\n }\n\n /** Writes the reply of a request served from the `onRequest` hook; a failure is handled like Fastify does. */\n #settle(lane: Lane, reply: FastifyReply, result: AdapterReply): void {\n try {\n if (!lane.raw || !this.#writeRaw(reply, result)) this.#write(reply, result)\n } catch (error) {\n // what Fastify does when the promise of a route handler rejects\n if (reply.sent) reply.log.error({ err: error }, 'Promise errored, but reply.sent = true was set')\n else reply.send(asError(error))\n }\n }\n\n /**\n * Writes a text reply on the raw response, as `reply.send(text)` would without `onSend` hook: one\n * `writeHead()` with the same headers in the same order, then the text. Only when nothing set\n * reply headers or trailers before (an `onRequest` hook) and Fastify would send the headers and\n * text unchanged (content type, exact `content-length`). Returns whether it wrote the reply.\n */\n #writeRaw(reply: FastifyReply, result: AdapterReply): boolean {\n const text = result.text\n const body = result.body\n if (text === undefined || !(body instanceof Uint8Array) || result.raw === true) return false\n const headers = result.headers\n const length = headers['content-length']\n if (typeof length !== 'string' || Number(length) !== body.byteLength) return false\n // a content type Fastify cannot parse is replaced by `reply.send()` (text/plain)\n const type = headers['content-type']\n if (!sendsTextAsIs(type) || (type !== JSON_TYPE && !MEDIA_TYPE.test(type as string))) return false\n const response = reply.raw\n if (reply.sent || response.destroyed || response.headersSent || reply.request.raw.httpVersionMajor !== 1) {\n return false\n }\n const keys = this.#keys as InternalKeys\n const state = reply as unknown as Record<symbol, unknown>\n if (keys.headers === null || keys.trailers === null || state[keys.trailers] !== null) return false\n // a serializer set by a hook transforms text sent with `reply.send()`\n if (keys.serializer === null || state[keys.serializer] !== null) return false\n if (!isEmptyRecord(state[keys.headers])) return false\n response.writeHead(result.status, headers as OutgoingHttpHeaders)\n response.end(text)\n return true\n }\n\n #dispatchFallback(request: FastifyRequest, reply: FastifyReply, error: unknown): Promise<AdapterReply> {\n if (this.#fallback === undefined) return Promise.reject(error ?? new Error('No fallback handler'))\n return this.#fallback(adapterRequest(request, undefined, reply), error)\n }\n\n async #reply(reply: FastifyReply, pending: ReturnType<AdapterHandler>): Promise<FastifyReply> {\n let result: AdapterReply\n try {\n result = await pending\n } catch {\n result = internalError()\n }\n this.#write(reply, result)\n return reply\n }\n\n /** Writes a reply through Fastify (`reply.send()`: `onSend` hooks...). */\n #write(reply: FastifyReply, result: AdapterReply): void {\n if (result.raw === true || reply.sent || reply.raw.destroyed) {\n discardStream(result.body instanceof ReadableStream ? result.body : null)\n return\n }\n if (result.eventStream === true) {\n // no onSend hook (compression, ETag...) may rewrite an event stream: write it on the raw\n // response, with the headers Fastify hooks already set (`onRequest`...) under the plugin's,\n // and their cookies before the plugin's (as reply.header('set-cookie') does on the other path)\n const native = lowerCaseHeaders(reply.getHeaders())\n const headers = { ...native, ...withNativeCookies(native['set-cookie'], result.headers) }\n reply.hijack()\n writeNodeReply(reply.raw, { ...result, headers })\n return\n }\n reply.code(result.status)\n const headers = result.headers\n for (const name of Object.keys(headers)) reply.header(name, headers[name])\n const body = result.body\n if (body === null) {\n reply.send()\n return\n }\n if (body instanceof Uint8Array) {\n // a string is written with the headers at once (bytes take a second write)\n if (result.text !== undefined && sendsTextAsIs(headers['content-type'])) reply.send(result.text)\n else reply.send(Buffer.isBuffer(body) ? body : Buffer.from(body.buffer, body.byteOffset, body.byteLength))\n return\n }\n const stream = body instanceof Blob ? body.stream() : body\n reply.send(Readable.fromWeb(stream as import('node:stream/web').ReadableStream))\n }\n\n async listen(port: number, host: string): Promise<HttpServerAddress> {\n await this.#prepare()\n this.#closing = undefined\n await this.instance.listen({ port, host })\n const address = this.instance.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 if (this.#closing === undefined) {\n this.#tracker.closeIdle()\n this.#closing = this.instance.close()\n }\n if (options.force === true) {\n this.instance.server.closeAllConnections?.()\n this.#tracker.destroyAll()\n }\n return this.#closing\n }\n\n inject(request: InjectRequest): Promise<InjectResponse> {\n return bufferInjectStream(stream => this.injectStream(stream), request)\n }\n\n async injectStream(request: InjectStreamRequest): Promise<InjectStreamResponse> {\n await this.#prepare()\n const options: LightInjectOptions = await lightInjectOptions(request)\n // Fastify forwards the options to light-my-request (`payloadAsStream`)\n const inject = this.instance.inject as unknown as (options: LightInjectOptions) => Promise<unknown>\n const response = await inject.call(this.instance, options)\n return lightStreamResponse(response as Parameters<typeof lightStreamResponse>[0], request.signal)\n }\n}\n\n/**\n * The lane of a route context (see `FastifyHttpAdapter.#lane`). Once the instance is ready, every\n * lifecycle hook list of a context is `null` or a non-empty array.\n */\nfunction laneOf(context: RouteContext, keys: InternalKeys): Lane | null {\n const route = context.config?.[DIRECT] as DirectRoute | undefined\n if (route === undefined) return null\n if (context.preParsing !== null || context.preValidation !== null || context.preHandler !== null) return null\n const raw = route.bare && context.onSend === null && context.onResponse === null\n return { handler: route.handler, raw: raw && keys.headers !== null && keys.trailers !== null }\n}\n\n/** The content type parser of the Znight context: the body is left unread (Znight reads it). */\nfunction ignoreBody(_request: FastifyRequest, _payload: unknown, done: (error: null, body: undefined) => void): void {\n done(null, undefined)\n}\n\n/** Whether a route option holds hooks (a function or a non-empty array). */\nfunction hasHooks(value: unknown): boolean {\n return value !== undefined && value !== null && !(Array.isArray(value) && value.length === 0)\n}\n\n/** An own symbol-keyed property of a Fastify object, by the symbol's description (internal state). */\nfunction ownSymbol(target: object, description: string): symbol | null {\n for (const symbol of Object.getOwnPropertySymbols(target)) if (symbol.description === description) return symbol\n return null\n}\n\nfunction isEmptyRecord(value: unknown): boolean {\n if (typeof value !== 'object' || value === null) return false\n for (const _key in value) return false\n return true\n}\n\nfunction asError(error: unknown): Error {\n return error instanceof Error ? error : new Error(String(error), { cause: error })\n}\n\n/** The last-resort 500 of a handler promise that rejected (the pipeline maps errors: should not happen). */\nfunction internalError(): AdapterReply {\n return {\n status: 500,\n headers: { 'content-type': 'application/json; charset=utf-8' },\n body: new TextEncoder().encode('{\"statusCode\":500,\"error\":\"Internal Server Error\"}'),\n }\n}\n\n/** The JSON content type of the plugin (with a charset). */\nconst JSON_TYPE = 'application/json; charset=utf-8'\n\n/**\n * Whether Fastify sends a string with this content type unchanged: it adds a charset to a JSON\n * type without one, and a content type to a string without one.\n */\n/** A `type/subtype` media type, optionally followed by parameters: what Fastify's `ContentType` accepts. */\nconst MEDIA_TYPE = /^\\s*[\\w!#$%&'*+.^`|~-]+\\/[\\w!#$%&'*+.^`|~-]+\\s*(?:;|$)/\n\nfunction sendsTextAsIs(type: HeaderValue | undefined): boolean {\n if (typeof type !== 'string') return false\n if (type === JSON_TYPE) return true\n const semicolon = type.indexOf(';')\n return !(semicolon === -1 ? type : type.slice(0, semicolon)).toLowerCase().includes('json')\n}\n\nfunction lowerCaseHeaders(\n headers: Readonly<Record<string, string | number | readonly string[] | undefined>>,\n): Record<string, string | string[]> {\n const result: Record<string, string | string[]> = {}\n for (const [name, value] of Object.entries(headers)) {\n if (value !== undefined) result[name.toLowerCase()] = typeof value === 'object' ? [...value] : String(value)\n }\n return result\n}\n\nfunction adapterRequest(\n request: FastifyRequest,\n reply: FastifyReply | undefined,\n response: FastifyReply | undefined = reply,\n): AdapterRequest {\n const hidden = HIDDEN_CONTENT_TYPES.get(request.raw)\n if (hidden !== undefined) {\n // Fastify is done with the content type: restore it (for Znight and `@Req()`)\n HIDDEN_CONTENT_TYPES.delete(request.raw)\n request.raw.headers['content-type'] = hidden\n }\n return new FastifyAdapterRequest(request, reply, response)\n}\n\n/** A Fastify request as an {@link AdapterRequest} (a class: no closure per request). */\nclass FastifyAdapterRequest implements AdapterRequest {\n readonly method: string\n readonly url: string\n readonly headers: Readonly<Record<string, string | undefined>>\n readonly ip: string | undefined\n readonly remoteAddress: string | undefined\n readonly encrypted: boolean\n readonly raw: FastifyRequest\n /** `@Res()`: the reply of a route, `undefined` for the fallback handler. */\n readonly rawResponse: FastifyReply | undefined\n /** The reply whose disconnection aborts the request. */\n readonly #response: FastifyReply | undefined\n #body: Promise<Uint8Array> | undefined\n\n constructor(request: FastifyRequest, reply: FastifyReply | undefined, response: FastifyReply | undefined) {\n this.method = request.raw.method ?? request.method\n this.url = request.raw.url ?? request.url\n this.headers = normalizeNodeHeaders(request.headers)\n this.ip = request.ip\n this.remoteAddress = request.raw.socket?.remoteAddress\n this.encrypted = isEncrypted(request.raw)\n this.raw = request\n this.rawResponse = reply\n this.#response = response\n }\n\n readBody(limit: number): Promise<Uint8Array> {\n this.#body ??= readNodeBody(this.raw.raw, limit)\n return this.#body\n }\n\n streamBody(limit: number): ReadableStream<Uint8Array> {\n return nodeBodyStream(this.raw.raw, limit)\n }\n\n rawStatus(): number | undefined {\n return this.rawResponse?.statusCode\n }\n\n onAbort(listener: () => void): void {\n if (this.#response !== undefined) onResponseAbort(this.#response.raw, listener)\n }\n}\n"],"mappings":"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"}