@ailura/nestjs-hono-adapter 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (118) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +411 -0
  3. package/dist/body.d.ts +35 -0
  4. package/dist/body.d.ts.map +1 -0
  5. package/dist/body.js +180 -0
  6. package/dist/body.js.map +1 -0
  7. package/dist/bridge.d.ts +64 -0
  8. package/dist/bridge.d.ts.map +1 -0
  9. package/dist/bridge.js +168 -0
  10. package/dist/bridge.js.map +1 -0
  11. package/dist/closing.d.ts +13 -0
  12. package/dist/closing.d.ts.map +1 -0
  13. package/dist/closing.js +30 -0
  14. package/dist/closing.js.map +1 -0
  15. package/dist/context.d.ts +22 -0
  16. package/dist/context.d.ts.map +1 -0
  17. package/dist/context.js +2 -0
  18. package/dist/context.js.map +1 -0
  19. package/dist/cors-middleware.d.ts +64 -0
  20. package/dist/cors-middleware.d.ts.map +1 -0
  21. package/dist/cors-middleware.js +211 -0
  22. package/dist/cors-middleware.js.map +1 -0
  23. package/dist/handler-bridge.d.ts +51 -0
  24. package/dist/handler-bridge.d.ts.map +1 -0
  25. package/dist/handler-bridge.js +122 -0
  26. package/dist/handler-bridge.js.map +1 -0
  27. package/dist/hono-lifecycle.d.ts +90 -0
  28. package/dist/hono-lifecycle.d.ts.map +1 -0
  29. package/dist/hono-lifecycle.js +169 -0
  30. package/dist/hono-lifecycle.js.map +1 -0
  31. package/dist/index.d.ts +16 -0
  32. package/dist/index.d.ts.map +1 -0
  33. package/dist/index.js +8 -0
  34. package/dist/index.js.map +1 -0
  35. package/dist/path.d.ts +3 -0
  36. package/dist/path.d.ts.map +1 -0
  37. package/dist/path.js +144 -0
  38. package/dist/path.js.map +1 -0
  39. package/dist/query.d.ts +19 -0
  40. package/dist/query.d.ts.map +1 -0
  41. package/dist/query.js +238 -0
  42. package/dist/query.js.map +1 -0
  43. package/dist/response-helpers.d.ts +16 -0
  44. package/dist/response-helpers.d.ts.map +1 -0
  45. package/dist/response-helpers.js +45 -0
  46. package/dist/response-helpers.js.map +1 -0
  47. package/dist/response-writer.d.ts +28 -0
  48. package/dist/response-writer.d.ts.map +1 -0
  49. package/dist/response-writer.js +52 -0
  50. package/dist/response-writer.js.map +1 -0
  51. package/dist/route-adapter.d.ts +53 -0
  52. package/dist/route-adapter.d.ts.map +1 -0
  53. package/dist/route-adapter.js +141 -0
  54. package/dist/route-adapter.js.map +1 -0
  55. package/dist/server-adapter.d.ts +106 -0
  56. package/dist/server-adapter.d.ts.map +1 -0
  57. package/dist/server-adapter.js +153 -0
  58. package/dist/server-adapter.js.map +1 -0
  59. package/dist/sse.d.ts +67 -0
  60. package/dist/sse.d.ts.map +1 -0
  61. package/dist/sse.js +211 -0
  62. package/dist/sse.js.map +1 -0
  63. package/dist/static-assets.d.ts +39 -0
  64. package/dist/static-assets.d.ts.map +1 -0
  65. package/dist/static-assets.js +155 -0
  66. package/dist/static-assets.js.map +1 -0
  67. package/dist/version-filter.d.ts +24 -0
  68. package/dist/version-filter.d.ts.map +1 -0
  69. package/dist/version-filter.js +107 -0
  70. package/dist/version-filter.js.map +1 -0
  71. package/dist/versioned-route.d.ts +21 -0
  72. package/dist/versioned-route.d.ts.map +1 -0
  73. package/dist/versioned-route.js +15 -0
  74. package/dist/versioned-route.js.map +1 -0
  75. package/dist/views.d.ts +42 -0
  76. package/dist/views.d.ts.map +1 -0
  77. package/dist/views.js +110 -0
  78. package/dist/views.js.map +1 -0
  79. package/dist/ws-adapter.d.ts +81 -0
  80. package/dist/ws-adapter.d.ts.map +1 -0
  81. package/dist/ws-adapter.js +214 -0
  82. package/dist/ws-adapter.js.map +1 -0
  83. package/dist/ws-client.d.ts +68 -0
  84. package/dist/ws-client.d.ts.map +1 -0
  85. package/dist/ws-client.js +135 -0
  86. package/dist/ws-client.js.map +1 -0
  87. package/dist/ws-server.d.ts +23 -0
  88. package/dist/ws-server.d.ts.map +1 -0
  89. package/dist/ws-server.js +37 -0
  90. package/dist/ws-server.js.map +1 -0
  91. package/dist/ws.d.ts +14 -0
  92. package/dist/ws.d.ts.map +1 -0
  93. package/dist/ws.js +12 -0
  94. package/dist/ws.js.map +1 -0
  95. package/package.json +99 -0
  96. package/src/body.ts +251 -0
  97. package/src/bridge.ts +308 -0
  98. package/src/closing.ts +38 -0
  99. package/src/context.ts +25 -0
  100. package/src/cors-middleware.ts +347 -0
  101. package/src/handler-bridge.ts +226 -0
  102. package/src/hono-lifecycle.ts +259 -0
  103. package/src/index.ts +27 -0
  104. package/src/path.ts +169 -0
  105. package/src/query.ts +304 -0
  106. package/src/response-helpers.ts +60 -0
  107. package/src/response-writer.ts +100 -0
  108. package/src/route-adapter.ts +261 -0
  109. package/src/server-adapter.ts +294 -0
  110. package/src/sse.ts +274 -0
  111. package/src/static-assets.ts +247 -0
  112. package/src/version-filter.ts +170 -0
  113. package/src/versioned-route.ts +30 -0
  114. package/src/views.ts +188 -0
  115. package/src/ws-adapter.ts +329 -0
  116. package/src/ws-client.ts +190 -0
  117. package/src/ws-server.ts +40 -0
  118. package/src/ws.ts +13 -0
@@ -0,0 +1,294 @@
1
+ import { toByteLimit } from './body.ts';
2
+ import type { NestHandler, NestRequest } from './bridge.ts';
3
+ import type { NestContext } from './context.ts';
4
+ import type { CorsOptions } from './cors-middleware.ts';
5
+ import {
6
+ createExceptionRunner,
7
+ createRouteHandler,
8
+ runNestHandler,
9
+ } from './handler-bridge.ts';
10
+ import type {
11
+ BridgeOptions,
12
+ NestExceptionHandler,
13
+ } from './handler-bridge.ts';
14
+ import { HonoLifecycle } from './hono-lifecycle.ts';
15
+ import type { TransportOptions } from './hono-lifecycle.ts';
16
+ import { toHonoPath } from './path.ts';
17
+ import { ResponseWriter } from './response-writer.ts';
18
+ import { ALL_METHOD } from './route-adapter.ts';
19
+ import { mountStaticAssets } from './static-assets.ts';
20
+ import type { StaticAssetsOptions } from './static-assets.ts';
21
+ import { ViewRenderer } from './views.ts';
22
+ import type { ViewOptions } from './views.ts';
23
+
24
+ /** The options the adapter itself reads. */
25
+ interface ServerAdapterOptions extends TransportOptions {
26
+ /**
27
+ * The views a handler may render, when the application has
28
+ * any.
29
+ */
30
+ readonly views?: ViewOptions;
31
+ }
32
+
33
+ /** The options Nest hands the parser middleware. */
34
+ interface BodyParserOptions {
35
+ readonly limit?: number | string;
36
+ }
37
+
38
+ /**
39
+ * HTTP adapter that runs Nest on Hono.
40
+ *
41
+ * There is no official Hono adapter for Nest, so this one lives
42
+ * in the repository. It implements the `AbstractHttpAdapter`
43
+ * contract rather than wrapping another framework: routes are
44
+ * registered on a Hono application, and Hono's Web `Request`
45
+ * and `Response` are translated to and from the objects Nest
46
+ * reads and writes. The application, the middleware chain and
47
+ * the server lifecycle are inherited from {@link HonoLifecycle};
48
+ * this class implements the rest of the Nest contract.
49
+ */
50
+ class ServerAdapter extends HonoLifecycle {
51
+ private readonly writer = new ResponseWriter();
52
+ private readonly views: ViewRenderer;
53
+
54
+ public constructor(options: ServerAdapterOptions = {}) {
55
+ super(options);
56
+ this.views = new ViewRenderer(options.views);
57
+ }
58
+
59
+ public override status(
60
+ response: NestContext,
61
+ statusCode: number,
62
+ ): void {
63
+ this.writer.status(response, statusCode);
64
+ }
65
+
66
+ public override reply(
67
+ response: NestContext,
68
+ body: unknown,
69
+ statusCode?: number,
70
+ ): void {
71
+ this.writer.reply(response, body, statusCode);
72
+ }
73
+
74
+ public override end(
75
+ response: NestContext,
76
+ message?: string,
77
+ ): void {
78
+ this.writer.end(response, message);
79
+ }
80
+
81
+ public override redirect(
82
+ response: NestContext,
83
+ statusCode: number,
84
+ url: string,
85
+ ): void {
86
+ this.writer.redirect(response, statusCode, url);
87
+ }
88
+
89
+ public override setHeader(
90
+ response: NestContext,
91
+ name: string,
92
+ value: string,
93
+ ): void {
94
+ this.writer.setHeader(response, name, value);
95
+ }
96
+
97
+ public override getHeader(
98
+ response: NestContext,
99
+ name: string,
100
+ ): string | undefined {
101
+ return this.writer.getHeader(response, name);
102
+ }
103
+
104
+ public override appendHeader(
105
+ response: NestContext,
106
+ name: string,
107
+ value: string,
108
+ ): void {
109
+ this.writer.appendHeader(response, name, value);
110
+ }
111
+
112
+ public override isHeadersSent(
113
+ response: NestContext,
114
+ ): boolean {
115
+ return this.writer.isHeadersSent(response);
116
+ }
117
+
118
+ /** Renders one view with the engine the deployment gave. */
119
+ public override render(
120
+ response: NestContext,
121
+ view: string,
122
+ options: unknown,
123
+ ): Promise<void> {
124
+ return this.views.render(response, view, options);
125
+ }
126
+
127
+ /** Names the engine, by the extension it renders. */
128
+ public override setViewEngine(engine: string): void {
129
+ this.views.useEngine(engine);
130
+ }
131
+
132
+ /** Names the directories a view is read from. */
133
+ public setBaseViewsDir(
134
+ directory: string | readonly string[],
135
+ ): void {
136
+ this.views.useDirectories(directory);
137
+ }
138
+
139
+ /**
140
+ * Serves the files in a directory, under the prefix asked
141
+ * for.
142
+ */
143
+ public override useStaticAssets(
144
+ path: string | readonly string[],
145
+ options?: StaticAssetsOptions,
146
+ ): void {
147
+ mountStaticAssets(this.hono, path, options ?? {});
148
+ }
149
+
150
+ /**
151
+ * Turns on the CORS middleware for this service.
152
+ *
153
+ * It has to be configured before the first request is served,
154
+ * which is how Nest itself is used: the application is built,
155
+ * `enableCors` is called on it, and only then does it listen.
156
+ * Until it is called no origin is allowed.
157
+ */
158
+ public override enableCors(options?: CorsOptions): void {
159
+ this.corsOptions = options ?? {};
160
+ }
161
+
162
+ public override getRequestMethod(
163
+ request: NestRequest,
164
+ ): string {
165
+ return request.method;
166
+ }
167
+
168
+ public override getRequestUrl(request: NestRequest): string {
169
+ return request.originalUrl;
170
+ }
171
+
172
+ public override getRequestHostname(
173
+ request: NestRequest,
174
+ ): string {
175
+ return request.hostname;
176
+ }
177
+
178
+ /**
179
+ * Records that Nest wants parsed bodies. The payload itself
180
+ * is read per route in the handler bridge, because Hono
181
+ * parses a body on demand rather than through middleware.
182
+ */
183
+ public override registerParserMiddleware(
184
+ _prefix?: string,
185
+ rawBody?: boolean,
186
+ ): void {
187
+ this.bodyParsingEnabled = true;
188
+ this.keepRawBody(rawBody);
189
+ }
190
+
191
+ /**
192
+ * `app.useBodyParser()` reaches the adapter here. Every
193
+ * parser reads the same payload, so the type is not branched
194
+ * on; the size limit is, because it is the option a
195
+ * deployment sets.
196
+ */
197
+ public useBodyParser(
198
+ _type?: string,
199
+ rawBody?: boolean,
200
+ options?: BodyParserOptions,
201
+ ): void {
202
+ this.bodyParsingEnabled = true;
203
+ this.keepRawBody(rawBody);
204
+ this.applyLimit(options);
205
+ }
206
+
207
+ public override setNotFoundHandler(
208
+ handler: NestHandler,
209
+ ): void {
210
+ this.hono.notFound((context) =>
211
+ runNestHandler(handler, context, this.bridgeOptions),
212
+ );
213
+ }
214
+
215
+ public override setErrorHandler(
216
+ handler: NestExceptionHandler,
217
+ ): void {
218
+ const run = createExceptionRunner(
219
+ handler,
220
+ this.bridgeOptions,
221
+ );
222
+ this.hono.onError((error, context) => run(error, context));
223
+ }
224
+
225
+ protected override register(
226
+ method: string,
227
+ path: string,
228
+ handler: NestHandler,
229
+ ): void {
230
+ const honoPath = toHonoPath(path);
231
+ const honoHandler = createRouteHandler(
232
+ handler,
233
+ this.bridgeOptions,
234
+ );
235
+ if (method === ALL_METHOD) {
236
+ this.hono.all(honoPath, honoHandler);
237
+ return;
238
+ }
239
+ this.hono.on(method, honoPath, honoHandler);
240
+ }
241
+
242
+ private get bridgeOptions(): BridgeOptions {
243
+ return {
244
+ bodyLimit: () => this.effectiveBodyLimit(),
245
+ bodyParsingEnabled: () => this.bodyParsingEnabled,
246
+ pendingStatus: (context) => this.writer.statusOf(context),
247
+ rawBody: () => this.rawBodyEnabled,
248
+ trustProxy: () => this.trustProxy,
249
+ };
250
+ }
251
+
252
+ private applyLimit(options?: BodyParserOptions): void {
253
+ if (options === undefined) {
254
+ return;
255
+ }
256
+ if (options.limit === undefined) {
257
+ return;
258
+ }
259
+ this.bodyLimit = toByteLimit(options.limit);
260
+ }
261
+
262
+ /** A limit of nothing means the size is not checked. */
263
+ private effectiveBodyLimit(): number | undefined {
264
+ if (this.bodyLimit === 0) {
265
+ return undefined;
266
+ }
267
+ return this.bodyLimit;
268
+ }
269
+ }
270
+
271
+ declare module '@nestjs/common' {
272
+ /**
273
+ * The application methods this adapter implements. Nest
274
+ * declares them on its own platform interfaces, which an
275
+ * application on Hono does not otherwise have.
276
+ */
277
+ interface INestApplication<TServer> {
278
+ /** Serves the files in a directory, at the prefix asked for. */
279
+ useStaticAssets: (
280
+ path: string | readonly string[],
281
+ options?: StaticAssetsOptions,
282
+ ) => this;
283
+ /** Names the directories a view is read from. */
284
+ setBaseViewsDir: (
285
+ directory: string | readonly string[],
286
+ ) => this;
287
+ /** Names the view engine, by the extension it renders. */
288
+ setViewEngine: (engine: string) => this;
289
+ }
290
+ }
291
+
292
+ export { ServerAdapter };
293
+
294
+ export type { ServerAdapterOptions };
package/src/sse.ts ADDED
@@ -0,0 +1,274 @@
1
+ import { once } from 'node:events';
2
+ import { Writable } from 'node:stream';
3
+
4
+ import { Logger } from '@nestjs/common';
5
+
6
+ import type { NestContext } from './context.ts';
7
+
8
+ /** The callback every Node stream write and finalizer takes. */
9
+ type StreamCallback = (error?: Error | null) => void;
10
+
11
+ /** The status an event stream answers with when Nest names none. */
12
+ const DEFAULT_STATUS = 200;
13
+
14
+ /** The event a stream emits once its headers are on the wire. */
15
+ const STARTED = 'started';
16
+
17
+ /** The header that says a proxy must not buffer the stream. */
18
+ const NO_BUFFERING = 'no';
19
+
20
+ /** The headers a stream is opened with, whatever Nest added. */
21
+ const STREAM_HEADERS: Readonly<Record<string, string>> = {
22
+ 'cache-control': 'no-cache',
23
+ connection: 'keep-alive',
24
+ 'content-type': 'text/event-stream',
25
+ 'x-accel-buffering': NO_BUFFERING,
26
+ };
27
+
28
+ /** Says why a stream failed, without taking the process down. */
29
+ const logger = new Logger('SseResponse');
30
+
31
+ function toBytes(chunk: unknown): Uint8Array | undefined {
32
+ if (typeof chunk === 'string') {
33
+ return new TextEncoder().encode(chunk);
34
+ }
35
+ if (chunk instanceof Uint8Array) {
36
+ return chunk;
37
+ }
38
+ return undefined;
39
+ }
40
+
41
+ /** What Nest calls once the stream's headers are chosen. */
42
+ type CommitListener = (response: Response) => void;
43
+
44
+ /**
45
+ * The Node writable Nest pipes an event stream into, and the
46
+ * web stream the Hono response reads its frames from.
47
+ *
48
+ * Nest answers `@Sse()` through `SseStream`, a `Transform` that
49
+ * is piped onto the response object. That object has to be a
50
+ * genuine `Writable`, so this one collects what Nest writes and
51
+ * hands the same bytes to a `ReadableStream`, which becomes the
52
+ * Hono response the client reads. The members Nest touches
53
+ * around the pipe — the status, the headers, the flushing and
54
+ * the end — are all here, so it needs nothing from Node's own
55
+ * `ServerResponse`. A client that walks away cancels the web
56
+ * stream, and the writes that follow are dropped rather than
57
+ * thrown at the process.
58
+ */
59
+ class SseResponse extends Writable {
60
+ private readonly onCommit: CommitListener;
61
+ private readonly status: () => number | undefined;
62
+ private readonly stream: ReadableStream<Uint8Array>;
63
+ private controller:
64
+ | ReadableStreamDefaultController<Uint8Array>
65
+ | undefined;
66
+ private committed = false;
67
+ private cancelled = false;
68
+ private headers: Record<string, string> = {};
69
+ private answerStatus: number = DEFAULT_STATUS;
70
+
71
+ public constructor(
72
+ status: () => number | undefined,
73
+ onCommit: CommitListener,
74
+ ) {
75
+ super();
76
+ this.status = status;
77
+ this.onCommit = onCommit;
78
+ this.stream = new ReadableStream<Uint8Array>({
79
+ cancel: (): void => {
80
+ this.cancelled = true;
81
+ },
82
+ start: (controller): void => {
83
+ this.controller = controller;
84
+ },
85
+ });
86
+ // A client that walked away makes the next write fail; that
87
+ // is the client's business, not a reason to crash.
88
+ this.on('error', (error: Error): void => {
89
+ this.logFailure(error);
90
+ });
91
+ }
92
+
93
+ /**
94
+ * The status Nest read from the adapter before the handler
95
+ * ran, so `@HttpCode()` and a POST default reach the stream.
96
+ */
97
+ public get statusCode(): number | undefined {
98
+ return this.status();
99
+ }
100
+
101
+ /** Chooses the answer's status and headers, once. */
102
+ public writeHead(
103
+ status: number,
104
+ headers?: Record<string, string>,
105
+ ): this {
106
+ this.answerStatus = status;
107
+ this.headers = headers ?? {};
108
+ this.commit();
109
+ return this;
110
+ }
111
+
112
+ /**
113
+ * Nothing is buffered here, so there is nothing left to
114
+ * flush: Hono already has the response by the time Nest
115
+ * asks.
116
+ */
117
+ public flushHeaders(): void {
118
+ this.commit();
119
+ }
120
+
121
+ public setHeader(name: string, value: string): void {
122
+ this.headers[name] = value;
123
+ }
124
+
125
+ /**
126
+ * Turns what Nest wrote into the Hono response, exactly once.
127
+ * The response is handed over before the start event is
128
+ * emitted, so whoever waits on the event reads a live body.
129
+ */
130
+ private commit(): void {
131
+ if (this.committed) {
132
+ return;
133
+ }
134
+ this.committed = true;
135
+ const headers = new Headers(STREAM_HEADERS);
136
+ for (const [name, value] of Object.entries(this.headers)) {
137
+ headers.set(name, value);
138
+ }
139
+ this.onCommit(
140
+ new Response(this.stream, {
141
+ headers,
142
+ status: this.answerStatus,
143
+ }),
144
+ );
145
+ this.emit(STARTED);
146
+ }
147
+
148
+ private enqueue(chunk: Uint8Array): void {
149
+ const { controller } = this;
150
+ if (this.cancelled || controller === undefined) {
151
+ return;
152
+ }
153
+ controller.enqueue(chunk);
154
+ }
155
+
156
+ private finish(): void {
157
+ const { controller } = this;
158
+ if (this.cancelled || controller === undefined) {
159
+ return;
160
+ }
161
+ this.cancelled = true;
162
+ controller.close();
163
+ }
164
+
165
+ private logFailure(error: Error): void {
166
+ logger.debug(error.message);
167
+ }
168
+
169
+ public override _write(
170
+ chunk: unknown,
171
+ _encoding: BufferEncoding,
172
+ callback: StreamCallback,
173
+ ): void {
174
+ this.commit();
175
+ const bytes = toBytes(chunk);
176
+ if (bytes === undefined) {
177
+ callback(
178
+ new TypeError('An event stream carries bytes only.'),
179
+ );
180
+ return;
181
+ }
182
+ this.enqueue(bytes);
183
+ callback();
184
+ }
185
+
186
+ public override _final(callback: StreamCallback): void {
187
+ this.finish();
188
+ callback();
189
+ }
190
+
191
+ public override _destroy(
192
+ error: Error | null,
193
+ callback: StreamCallback,
194
+ ): void {
195
+ this.finish();
196
+ callback(error);
197
+ }
198
+ }
199
+
200
+ /** The headers Hono already recorded for the answer. */
201
+ function recordedHeaders(
202
+ context: NestContext,
203
+ ): Record<string, string> {
204
+ const record: Record<string, string> = {};
205
+ for (const [name, value] of context.res.headers) {
206
+ if (name !== 'content-type') {
207
+ record[name] = value;
208
+ }
209
+ }
210
+ return record;
211
+ }
212
+
213
+ /**
214
+ * Gives the object Nest reads as its response the surface its
215
+ * SSE path touches, so the same object serves every supported
216
+ * Nest major.
217
+ *
218
+ * Nest 11 and 12 both prefer `res.raw` when it is set, and Nest
219
+ * 12 never reads the response's own members then. Pointing
220
+ * `raw` at this writable is what keeps the frames travelling
221
+ * through Hono: writing them straight into `c.env.outgoing`
222
+ * would bypass the `Response` this adapter returns, and the
223
+ * transport would then write that response into the same socket
224
+ * a second time. `req.raw` is the real incoming message, which
225
+ * is what tunes the socket and reports a disconnect.
226
+ */
227
+ function installSurface(
228
+ context: NestContext,
229
+ response: SseResponse,
230
+ ): void {
231
+ Object.defineProperties(context, {
232
+ raw: { configurable: true, value: response },
233
+ statusCode: {
234
+ configurable: true,
235
+ get: (): number | undefined => response.statusCode,
236
+ },
237
+ writableEnded: {
238
+ configurable: true,
239
+ get: (): boolean => response.writableEnded,
240
+ },
241
+ });
242
+ Object.assign(context, {
243
+ emit: response.emit.bind(response),
244
+ end: response.end.bind(response),
245
+ flushHeaders: response.flushHeaders.bind(response),
246
+ getHeaders: (): Record<string, string> =>
247
+ recordedHeaders(context),
248
+ on: response.on.bind(response),
249
+ once: response.once.bind(response),
250
+ removeListener: response.removeListener.bind(response),
251
+ setHeader: response.setHeader.bind(response),
252
+ write: response.write.bind(response),
253
+ writeHead: response.writeHead.bind(response),
254
+ });
255
+ }
256
+
257
+ /**
258
+ * Opens an event stream on the response Nest reads, and says
259
+ * when it started. The stream is the destination `SseStream`
260
+ * pipes into, so nothing is buffered until Nest commits the
261
+ * headers, which is also the moment the promise settles.
262
+ */
263
+ function mountSse(
264
+ context: NestContext,
265
+ status: () => number | undefined,
266
+ ): Promise<unknown> {
267
+ const response = new SseResponse(status, (answer): void => {
268
+ context.res = answer;
269
+ });
270
+ installSurface(context, response);
271
+ return once(response, STARTED) as Promise<unknown>;
272
+ }
273
+
274
+ export { SseResponse, mountSse };