@nestjs/core 12.0.0-alpha.5 → 12.0.0-alpha.7

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 (35) hide show
  1. package/Readme.md +25 -46
  2. package/adapters/http-adapter.d.ts +2 -0
  3. package/adapters/http-adapter.js +3 -0
  4. package/exceptions/base-exception-filter.js +8 -3
  5. package/helpers/barrier.js +4 -1
  6. package/helpers/handler-metadata-storage.d.ts +1 -0
  7. package/helpers/router-method-factory.d.ts +1 -0
  8. package/helpers/router-method-factory.js +1 -0
  9. package/helpers/safe-instance-decorator.d.ts +10 -0
  10. package/helpers/safe-instance-decorator.js +20 -0
  11. package/hooks/before-app-shutdown.hook.js +11 -2
  12. package/hooks/on-app-shutdown.hook.js +11 -2
  13. package/hooks/on-module-destroy.hook.js +11 -2
  14. package/injector/container.js +2 -2
  15. package/injector/injector.js +20 -12
  16. package/injector/internal-core-module/internal-core-module-factory.js +1 -1
  17. package/injector/module.d.ts +6 -0
  18. package/injector/module.js +12 -1
  19. package/interceptors/interceptors-consumer.js +32 -7
  20. package/internal.d.ts +10 -6
  21. package/internal.js +10 -6
  22. package/middleware/builder.js +5 -1
  23. package/nest-application-context.js +1 -1
  24. package/nest-application.d.ts +2 -1
  25. package/nest-application.js +28 -6
  26. package/package.json +3 -14
  27. package/router/legacy-route-converter.d.ts +1 -1
  28. package/router/legacy-route-converter.js +24 -13
  29. package/router/router-execution-context.d.ts +1 -0
  30. package/router/router-execution-context.js +26 -3
  31. package/router/router-response-controller.d.ts +1 -0
  32. package/router/router-response-controller.js +126 -49
  33. package/router/sse-stream.d.ts +1 -0
  34. package/router/sse-stream.js +24 -13
  35. package/scanner.js +4 -1
@@ -74,7 +74,11 @@ export class MiddlewareBuilder {
74
74
  .map(route => ({
75
75
  method: route.method,
76
76
  path: route.path,
77
- regex: new RegExp('^(' + route.path.replace(regexMatchParams, wildcard) + ')$', 'g'),
77
+ // No `g` flag: each regex is reused across every route below, and a
78
+ // global regex advances `lastIndex` on a match, so the next `test()`
79
+ // would resume mid-string and fail the `^` anchor. The pattern is
80
+ // anchored and only used with `test()`, so `g` buys nothing anyway.
81
+ regex: new RegExp('^(' + route.path.replace(regexMatchParams, wildcard) + ')$'),
78
82
  }));
79
83
  return routes.filter(route => {
80
84
  const isOverlapped = (item) => {
@@ -164,7 +164,7 @@ export class NestApplicationContext extends AbstractInstanceResolver {
164
164
  * @returns {this} The Nest application context instance
165
165
  */
166
166
  enableShutdownHooks(signals = [], options = {}) {
167
- if (isEmptyArray(signals)) {
167
+ if (!signals || isEmptyArray(signals)) {
168
168
  signals = Object.values(ShutdownSignal);
169
169
  }
170
170
  else {
@@ -32,7 +32,7 @@ export declare class NestApplication extends NestApplicationContext<NestApplicat
32
32
  applyOptions(): void;
33
33
  createServer<T = any>(): T;
34
34
  registerModules(): Promise<void>;
35
- registerWsModule(): void;
35
+ registerWsModule(): Promise<void>;
36
36
  init(): Promise<this>;
37
37
  registerParserMiddleware(): void;
38
38
  registerRouter(): Promise<void>;
@@ -68,6 +68,7 @@ export declare class NestApplication extends NestApplicationContext<NestApplicat
68
68
  private getProtocol;
69
69
  private registerMiddleware;
70
70
  private applyInstanceDecoratorIfRegistered;
71
+ private applyFunctionDecoratorIfRegistered;
71
72
  private loadSocketModule;
72
73
  private loadMicroservicesModule;
73
74
  }
@@ -4,6 +4,7 @@ import { platform } from 'os';
4
4
  import { ApplicationConfig } from './application-config.js';
5
5
  import { MESSAGES } from './constants.js';
6
6
  import { optionalRequire } from './helpers/optional-require.js';
7
+ import { makeSafeInstanceDecorator } from './helpers/safe-instance-decorator.js';
7
8
  import { Injector } from './injector/injector.js';
8
9
  import { MiddlewareContainer } from './middleware/container.js';
9
10
  import { MiddlewareModule } from './middleware/middleware-module.js';
@@ -85,18 +86,18 @@ export class NestApplication extends NestApplicationContext {
85
86
  return this.httpAdapter.getHttpServer();
86
87
  }
87
88
  async registerModules() {
88
- this.registerWsModule();
89
+ await this.registerWsModule();
89
90
  if (this.microservicesModule) {
90
91
  this.microservicesModule.register(this.container, this.graphInspector, this.config, this.appOptions);
91
92
  this.microservicesModule.setupClients(this.container);
92
93
  }
93
94
  await this.middlewareModule.register(this.middlewareContainer, this.container, this.config, this.injector, this.httpAdapter, this.graphInspector, this.appOptions);
94
95
  }
95
- registerWsModule() {
96
+ async registerWsModule() {
96
97
  if (!this.socketModule) {
97
98
  return;
98
99
  }
99
- this.socketModule.register(this.container, this.config, this.graphInspector, this.appOptions, this.httpServer);
100
+ await this.socketModule.register(this.container, this.config, this.graphInspector, this.appOptions, this.httpServer);
100
101
  this.isWsModuleRegistered = true;
101
102
  }
102
103
  async init() {
@@ -204,7 +205,7 @@ export class NestApplication extends NestApplicationContext {
204
205
  this.routesResolver.registerExceptionHandler();
205
206
  }
206
207
  connectMicroservice(microserviceOptions, hybridAppOptions = {}) {
207
- const { NestMicroservice } = loadPackageCached('@nestjs/microservices');
208
+ const { NestMicroservice } = loadPackageCached('@nestjs/microservices', 'NestFactory');
208
209
  const { inheritAppConfig } = hybridAppOptions;
209
210
  const applicationConfig = inheritAppConfig
210
211
  ? this.config
@@ -230,7 +231,7 @@ export class NestApplication extends NestApplicationContext {
230
231
  return this;
231
232
  }
232
233
  use(...args) {
233
- this.httpAdapter.use(...args);
234
+ this.httpAdapter.use(...this.applyFunctionDecoratorIfRegistered(args));
234
235
  return this;
235
236
  }
236
237
  useBodyParser(...args) {
@@ -411,10 +412,31 @@ export class NestApplication extends NestApplicationContext {
411
412
  }
412
413
  applyInstanceDecoratorIfRegistered(...instances) {
413
414
  if (this.appOptions.instrument?.instanceDecorator) {
414
- return instances.map(instance => this.appOptions.instrument.instanceDecorator(instance));
415
+ const decorate = makeSafeInstanceDecorator(this.appOptions.instrument.instanceDecorator);
416
+ return instances.map(instance => decorate(instance));
415
417
  }
416
418
  return instances;
417
419
  }
420
+ applyFunctionDecoratorIfRegistered(args) {
421
+ if (!this.appOptions.instrument?.instanceDecorator) {
422
+ return args;
423
+ }
424
+ const decorate = makeSafeInstanceDecorator(this.appOptions.instrument.instanceDecorator);
425
+ // Decorators may return a non-function value for plain middleware
426
+ // functions; fall back to the original argument so the HTTP adapter
427
+ // always receives a valid handler.
428
+ const decorateFunction = (arg) => {
429
+ if (!isFunction(arg)) {
430
+ return arg;
431
+ }
432
+ const decorated = decorate(arg);
433
+ return isFunction(decorated) ? decorated : arg;
434
+ };
435
+ // Map over the original arguments to preserve arity: appending a trailing
436
+ // `undefined` to a single-argument `use(fn)` call would make Express 5's
437
+ // router throw "argument handler must be a function".
438
+ return args.map(decorateFunction);
439
+ }
418
440
  async loadSocketModule() {
419
441
  if (!this.socketModule) {
420
442
  const socketModule = await optionalRequire('@nestjs/websockets/socket-module', () => import('@nestjs/websockets/socket-module.js'));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nestjs/core",
3
- "version": "12.0.0-alpha.5",
3
+ "version": "12.0.0-alpha.7",
4
4
  "description": "Nest - modern, fast, powerful node.js web framework (@core)",
5
5
  "author": "Kamil Mysliwiec",
6
6
  "license": "MIT",
@@ -28,18 +28,7 @@
28
28
  "publishConfig": {
29
29
  "access": "public"
30
30
  },
31
- "scripts": {
32
- "postinstall": "opencollective || exit 0"
33
- },
34
- "collective": {
35
- "type": "opencollective",
36
- "url": "https://opencollective.com/nest",
37
- "donation": {
38
- "text": "Become a partner:"
39
- }
40
- },
41
31
  "dependencies": {
42
- "@nuxt/opencollective": "0.4.1",
43
32
  "fast-safe-stringify": "2.1.1",
44
33
  "iterare": "1.2.1",
45
34
  "path-to-regexp": "8.4.2",
@@ -47,7 +36,7 @@
47
36
  "uid": "2.0.2"
48
37
  },
49
38
  "devDependencies": {
50
- "@nestjs/common": "^12.0.0-alpha.5"
39
+ "@nestjs/common": "^12.0.0-alpha.7"
51
40
  },
52
41
  "peerDependencies": {
53
42
  "@nestjs/common": "^11.0.0",
@@ -68,5 +57,5 @@
68
57
  "optional": true
69
58
  }
70
59
  },
71
- "gitHead": "d91b72cc9567e6a09e3ad6075b2fb801e71adfc8"
60
+ "gitHead": "66e89ffc3d14e47d572bd6ea3f8714fa6916160b"
72
61
  }
@@ -12,5 +12,5 @@ export declare class LegacyRouteConverter {
12
12
  logs?: boolean;
13
13
  }): string;
14
14
  static printError(route: string): void;
15
- static printWarning(route: string): void;
15
+ static printWarning(route: string, convertedRoute?: string): void;
16
16
  }
@@ -21,37 +21,48 @@ export class LegacyRouteConverter {
21
21
  ? this.printWarning.bind(this)
22
22
  : () => { };
23
23
  if (normalizedRoute.endsWith('/(.*)/')) {
24
+ const convertedRoute = route.replace('(.*)', '{*path}');
24
25
  // Skip printing warning for the "all" wildcard.
25
26
  if (normalizedRoute !== '/(.*)/') {
26
- printWarning(route);
27
+ printWarning(route, convertedRoute);
27
28
  }
28
- return route.replace('(.*)', '{*path}');
29
+ return convertedRoute;
29
30
  }
30
31
  if (normalizedRoute.endsWith('/*/')) {
32
+ const convertedRoute = route.replace('*', '{*path}');
31
33
  // Skip printing warning for the "all" wildcard.
32
34
  if (normalizedRoute !== '/*/') {
33
- printWarning(route);
35
+ printWarning(route, convertedRoute);
34
36
  }
35
- return route.replace('*', '{*path}');
37
+ return convertedRoute;
36
38
  }
37
39
  if (normalizedRoute.endsWith('/+/')) {
38
- printWarning(route);
39
- return route.replace('/+', '/*path');
40
+ const convertedRoute = route.replace('/+', '/*path');
41
+ printWarning(route, convertedRoute);
42
+ return convertedRoute;
40
43
  }
41
44
  // When route includes any wildcard segments in the middle.
42
45
  if (normalizedRoute.includes('/*/')) {
43
- printWarning(route);
44
- // Replace each /*/ segment with a named parameter using different name for each segment.
45
- return route.replaceAll('/*/', (match, offset) => {
46
- return `/*path${offset}/`;
47
- });
46
+ // Replace each "*" segment with a named parameter, using a different name
47
+ // for each. Match "/*" with a lookahead for the following "/" so the
48
+ // trailing slash is not consumed. Consuming it made two adjacent "/*/*/"
49
+ // segments share a slash, so only the first one got converted and the
50
+ // second was left as an unnamed "*" that path-to-regexp still rejects.
51
+ const convertedRoute = route.replaceAll(/\/\*(?=\/)/g, (match, offset) => `/*path${offset}`);
52
+ printWarning(route, convertedRoute);
53
+ return convertedRoute;
48
54
  }
49
55
  return route;
50
56
  }
51
57
  static printError(route) {
52
58
  this.logger.error(UNSUPPORTED_PATH_MESSAGE `${route}`);
53
59
  }
54
- static printWarning(route) {
55
- this.logger.warn(UNSUPPORTED_PATH_MESSAGE `${route}` + ' Attempting to auto-convert...');
60
+ static printWarning(route, convertedRoute) {
61
+ // Surface the auto-converted result so users can map the flagged path to a
62
+ // concrete fix, instead of only seeing the (often prefixed) offending path.
63
+ const autoConvertMessage = convertedRoute
64
+ ? ` Attempting to auto-convert to "${convertedRoute}"...`
65
+ : ' Attempting to auto-convert...';
66
+ this.logger.warn(UNSUPPORTED_PATH_MESSAGE `${route}` + autoConvertMessage);
56
67
  }
57
68
  }
@@ -48,4 +48,5 @@ export declare class RouterExecutionContext {
48
48
  })[]): (<TRequest, TResponse>(args: any[], req: TRequest, res: TResponse, next: Function) => Promise<void>) | null;
49
49
  createHandleResponseFn(callback: (...args: unknown[]) => unknown, isResponseHandled: boolean, redirectResponse?: RedirectResponse, httpStatusCode?: number): HandleResponseFn;
50
50
  private isResponseHandled;
51
+ private attachSseAbortSignal;
51
52
  }
@@ -1,4 +1,4 @@
1
- import { ForbiddenException, } from '@nestjs/common';
1
+ import { ForbiddenException, SSE_ABORT_CONTROLLER, } from '@nestjs/common';
2
2
  import { CUSTOM_ROUTE_ARGS_METADATA, HEADERS_METADATA, HTTP_CODE_METADATA, isEmptyArray, isString, REDIRECT_METADATA, RENDER_METADATA, ROUTE_ARGS_METADATA, RouteParamtypes, SSE_METADATA, } from '@nestjs/common/internal';
3
3
  import { FORBIDDEN_MESSAGE, } from '../guards/index.js';
4
4
  import { ContextUtils } from '../helpers/context-utils.js';
@@ -30,7 +30,7 @@ export class RouterExecutionContext {
30
30
  }
31
31
  create(instance, callback, methodName, moduleKey, requestMethod, contextId = STATIC_CONTEXT, inquirerId) {
32
32
  const contextType = 'http';
33
- const { argsLength, fnHandleResponse, paramtypes, getParamsMetadata, httpStatusCode, responseHeaders, hasCustomHeaders, } = this.getMetadata(instance, callback, methodName, moduleKey, requestMethod, contextType);
33
+ const { argsLength, fnHandleResponse, isSseHandler, paramtypes, getParamsMetadata, httpStatusCode, responseHeaders, hasCustomHeaders, } = this.getMetadata(instance, callback, methodName, moduleKey, requestMethod, contextType);
34
34
  const paramsOptions = this.contextUtils.mergeParamsMetatypes(getParamsMetadata(moduleKey, contextId, inquirerId), paramtypes);
35
35
  const pipes = this.pipesContextCreator.create(instance, callback, moduleKey, contextId, inquirerId);
36
36
  const guards = this.guardsContextCreator.create(instance, callback, moduleKey, contextId, inquirerId);
@@ -47,7 +47,15 @@ export class RouterExecutionContext {
47
47
  this.responseController.setStatus(res, httpStatusCode);
48
48
  hasCustomHeaders &&
49
49
  this.responseController.setHeaders(res, responseHeaders);
50
- const result = await this.interceptorsConsumer.intercept(interceptors, [req, res, next], instance, callback, handler(args, req, res, next), contextType);
50
+ if (isSseHandler) {
51
+ // Attach a per-request AbortController before the handler runs so async
52
+ // @Sse() handlers can observe client disconnects via @SseSignal() during
53
+ // their setup. The controller is aborted in RouterResponseController.sse()
54
+ // when the underlying connection closes.
55
+ this.attachSseAbortSignal(req);
56
+ }
57
+ const resultOrDeferred = this.interceptorsConsumer.intercept(interceptors, [req, res, next], instance, callback, handler(args, req, res, next), contextType);
58
+ const result = isSseHandler ? resultOrDeferred : await resultOrDeferred;
51
59
  await fnHandleResponse(result, res, req);
52
60
  };
53
61
  }
@@ -66,6 +74,7 @@ export class RouterExecutionContext {
66
74
  const isResponseHandled = this.isResponseHandled(instance, methodName, paramsMetadata);
67
75
  const httpRedirectResponse = this.reflectRedirect(callback);
68
76
  const fnHandleResponse = this.createHandleResponseFn(callback, isResponseHandled, httpRedirectResponse);
77
+ const isSseHandler = !!this.reflectSse(callback);
69
78
  const httpCode = this.reflectHttpStatusCode(callback);
70
79
  const httpStatusCode = httpCode ?? this.responseController.getStatusByMethod(requestMethod);
71
80
  const responseHeaders = this.reflectResponseHeaders(callback);
@@ -73,6 +82,7 @@ export class RouterExecutionContext {
73
82
  const handlerMetadata = {
74
83
  argsLength,
75
84
  fnHandleResponse,
85
+ isSseHandler,
76
86
  paramtypes,
77
87
  getParamsMetadata,
78
88
  httpStatusCode,
@@ -196,4 +206,17 @@ export class RouterExecutionContext {
196
206
  const isPassthroughEnabled = this.contextUtils.reflectPassthrough(instance, methodName);
197
207
  return hasResponseOrNextDecorator && !isPassthroughEnabled;
198
208
  }
209
+ attachSseAbortSignal(req) {
210
+ const carrier = req;
211
+ // Attach to both the framework request and its raw form (when present), since
212
+ // @SseSignal() reads from the execution-context request while
213
+ // RouterResponseController.sse() operates on the raw request.
214
+ if (!carrier[SSE_ABORT_CONTROLLER]) {
215
+ carrier[SSE_ABORT_CONTROLLER] = new AbortController();
216
+ }
217
+ if (carrier.raw && !carrier.raw[SSE_ABORT_CONTROLLER]) {
218
+ carrier.raw[SSE_ABORT_CONTROLLER] =
219
+ carrier[SSE_ABORT_CONTROLLER];
220
+ }
221
+ }
199
222
  }
@@ -26,4 +26,5 @@ export declare class RouterResponseController {
26
26
  statusCode?: number;
27
27
  }): Promise<void>;
28
28
  private assertObservable;
29
+ private getOrCreateAbortController;
29
30
  }
@@ -1,4 +1,4 @@
1
- import { HttpStatus, Logger, RequestMethod, } from '@nestjs/common';
1
+ import { HttpStatus, Logger, RequestMethod, SSE_ABORT_CONTROLLER, } from '@nestjs/common';
2
2
  import { EMPTY, lastValueFrom, isObservable } from 'rxjs';
3
3
  import { catchError, concatMap, map } from 'rxjs/operators';
4
4
  import { SseStream, } from './sse-stream.js';
@@ -49,76 +49,146 @@ export class RouterResponseController {
49
49
  async sse(result, response, request, options) {
50
50
  // It's possible that we sent headers already so don't use a stream
51
51
  if (response.writableEnded) {
52
+ // The response is already gone: abort the request-scoped signal so
53
+ // handler cleanup wired to @SseSignal() still runs, and swallow late
54
+ // handler rejections that can no longer be delivered to the client.
55
+ this.getOrCreateAbortController(request).abort();
56
+ Promise.resolve(result).catch((err) => this.logger.error(err));
52
57
  return;
53
58
  }
54
- const observableResult = await Promise.resolve(result);
55
- this.assertObservable(observableResult);
56
59
  const stream = new SseStream(request);
57
60
  const statusCode = options?.statusCode ??
58
61
  response.statusCode ??
59
62
  200;
60
- stream.pipe(response, {
61
- additionalHeaders: options?.additionalHeaders,
62
- statusCode,
63
- });
63
+ // Create a per-request AbortController and expose its signal on the request
64
+ // object so async @Sse() handlers can observe client disconnects (via the
65
+ // @SseSignal() parameter decorator) and stop/clean up in-flight setup work.
66
+ // The controller is reused if one was already attached upstream (e.g. when the
67
+ // handler is wrapped by interceptors and the signal was created earlier).
68
+ const abortController = this.getOrCreateAbortController(request);
64
69
  return new Promise((resolve, reject) => {
65
70
  let settled = false;
66
- const onClose = () => {
67
- settled = true;
68
- subscription.unsubscribe();
71
+ let closeRequested = false;
72
+ let subscription;
73
+ const disconnectSource = request.socket ?? response;
74
+ // Ends the request-scoped lifetime: stops listening for disconnects and
75
+ // aborts the signal handed to the route handler. Every terminal path of
76
+ // the SSE lifecycle (disconnect, completion, error) funnels through here,
77
+ // so a handler that ties its resources to the signal releases them once,
78
+ // regardless of how the stream ended. `abort()` is idempotent, so paths
79
+ // that already aborted on disconnect are unaffected.
80
+ const finalize = () => {
81
+ disconnectSource.removeListener('close', onClose);
82
+ abortController.abort();
83
+ };
84
+ const endStream = () => {
69
85
  if (!stream.writableEnded) {
70
86
  stream.end();
71
87
  }
88
+ };
89
+ const onClose = () => {
90
+ if (settled || closeRequested) {
91
+ return;
92
+ }
93
+ closeRequested = true;
94
+ if (!subscription) {
95
+ finalize();
96
+ return;
97
+ }
98
+ settled = true;
99
+ finalize();
100
+ subscription?.unsubscribe();
101
+ endStream();
72
102
  response.end();
73
103
  resolve();
74
104
  };
75
- const subscription = observableResult
76
- .pipe(map((message) => {
77
- if (isObject(message)) {
78
- return message;
105
+ disconnectSource.once('close', onClose);
106
+ Promise.resolve(result)
107
+ .then(observableResult => {
108
+ if (settled) {
109
+ return;
79
110
  }
80
- return { data: message };
81
- }), concatMap(message => new Promise(resolve => stream.writeMessage(message, () => resolve()))), catchError(err => {
82
- if (!stream.headersCommitted) {
83
- throw err;
111
+ this.assertObservable(observableResult);
112
+ if (closeRequested) {
113
+ // The client disconnected while the async handler was resolving.
114
+ // Do not subscribe the producer Observable after the consumer has
115
+ // already gone away — subscribing only to abort it in the same tick
116
+ // would start producer side effects just to immediately cancel them.
117
+ settled = true;
118
+ endStream();
119
+ response.end();
120
+ resolve();
121
+ return;
84
122
  }
85
- const data = err instanceof Error ? err.message : err;
86
- stream.writeMessage({ type: 'error', data }, writeError => {
87
- if (writeError) {
88
- this.logger.error(writeError);
123
+ stream.pipe(response, {
124
+ additionalHeaders: options?.additionalHeaders,
125
+ statusCode,
126
+ });
127
+ subscription = observableResult
128
+ .pipe(map((message) => {
129
+ if (isObject(message)) {
130
+ return message;
131
+ }
132
+ return { data: message };
133
+ }), concatMap(message => new Promise(resolve => stream.writeMessage(message, () => resolve()))), catchError(err => {
134
+ if (!stream.headersCommitted) {
135
+ throw err;
89
136
  }
137
+ const data = err instanceof Error ? err.message : err;
138
+ stream.writeMessage({ type: 'error', data }, writeError => {
139
+ if (writeError) {
140
+ this.logger.error(writeError);
141
+ }
142
+ });
143
+ return EMPTY;
144
+ }))
145
+ .subscribe({
146
+ error: err => {
147
+ if (settled) {
148
+ return;
149
+ }
150
+ settled = true;
151
+ finalize();
152
+ endStream();
153
+ reject(err);
154
+ },
155
+ complete: () => {
156
+ if (settled) {
157
+ return;
158
+ }
159
+ settled = true;
160
+ finalize();
161
+ endStream();
162
+ resolve();
163
+ },
90
164
  });
91
- return EMPTY;
92
- }))
93
- .subscribe({
94
- error: err => {
95
- settled = true;
96
- request.removeListener('close', onClose);
97
- if (!stream.writableEnded) {
98
- stream.end();
165
+ // Commit SSE headers on the next macrotask. Pipe validation errors
166
+ // propagate through microtasks (which complete before macrotasks),
167
+ // so if the lifecycle errored, `settled` is already true and we
168
+ // skip the write. Otherwise headers are sent immediately rather
169
+ // than waiting for the first Observable emission.
170
+ setTimeout(() => {
171
+ if (!settled) {
172
+ stream.commitHeaders();
99
173
  }
100
- reject(err);
101
- },
102
- complete: () => {
174
+ }, 0);
175
+ })
176
+ .catch(err => {
177
+ if (settled) {
178
+ return;
179
+ }
180
+ if (closeRequested) {
103
181
  settled = true;
104
- request.removeListener('close', onClose);
105
- if (!stream.writableEnded) {
106
- stream.end();
107
- }
182
+ endStream();
183
+ response.end();
108
184
  resolve();
109
- },
110
- });
111
- // Commit SSE headers on the next macrotask. Pipe validation errors
112
- // propagate through microtasks (which complete before macrotasks),
113
- // so if the lifecycle errored, `settled` is already true and we
114
- // skip the write. Otherwise headers are sent immediately rather
115
- // than waiting for the first Observable emission.
116
- setTimeout(() => {
117
- if (!settled) {
118
- stream.commitHeaders();
185
+ return;
119
186
  }
120
- }, 0);
121
- request.on('close', onClose);
187
+ settled = true;
188
+ finalize();
189
+ endStream();
190
+ reject(err);
191
+ });
122
192
  });
123
193
  }
124
194
  assertObservable(value) {
@@ -126,4 +196,11 @@ export class RouterResponseController {
126
196
  throw new ReferenceError('You must return an Observable stream to use Server-Sent Events (SSE).');
127
197
  }
128
198
  }
199
+ getOrCreateAbortController(request) {
200
+ const carrier = request;
201
+ if (!carrier[SSE_ABORT_CONTROLLER]) {
202
+ carrier[SSE_ABORT_CONTROLLER] = new AbortController();
203
+ }
204
+ return carrier[SSE_ABORT_CONTROLLER];
205
+ }
129
206
  }
@@ -22,6 +22,7 @@ export type HeaderStream = WritableHeaderStream & ReadHeaders;
22
22
  * - type
23
23
  * - id
24
24
  * - retry
25
+ * - comment
25
26
  *
26
27
  * If constructed with a HTTP Request, it will optimise the socket for streaming.
27
28
  * If this stream is piped to an HTTP Response, it will set appropriate headers.
@@ -1,13 +1,25 @@
1
1
  import { Transform } from 'stream';
2
- import { isObject } from '@nestjs/common/internal';
2
+ import { isNil, isObject, isUndefined } from '@nestjs/common/internal';
3
+ function serializeSseLines(value, prefix) {
4
+ return value
5
+ .split(/\r\n|\r|\n/)
6
+ .map(line => `${prefix}${line}\n`)
7
+ .join('');
8
+ }
3
9
  function toDataString(data) {
4
10
  if (isObject(data)) {
5
11
  return toDataString(JSON.stringify(data));
6
12
  }
7
- return data
8
- .split(/\r\n|\r|\n/)
9
- .map(line => `data: ${line}\n`)
10
- .join('');
13
+ return serializeSseLines(data, 'data: ');
14
+ }
15
+ function toCommentString(comment) {
16
+ return serializeSseLines(comment, ': ');
17
+ }
18
+ function isCommentOnly(message) {
19
+ return (!isNil(message.comment) &&
20
+ isUndefined(message.data) &&
21
+ isUndefined(message.type) &&
22
+ isUndefined(message.retry));
11
23
  }
12
24
  /**
13
25
  * Adapted from https://raw.githubusercontent.com/EventSource/node-ssestream
@@ -18,6 +30,7 @@ function toDataString(data) {
18
30
  * - type
19
31
  * - id
20
32
  * - retry
33
+ * - comment
21
34
  *
22
35
  * If constructed with a HTTP Request, it will optimise the socket for streaming.
23
36
  * If this stream is piped to an HTTP Response, it will set appropriate headers.
@@ -64,7 +77,7 @@ export class SseStream extends Transform {
64
77
  if (this._destination.writeHead) {
65
78
  this._destination.writeHead(statusCode, {
66
79
  ...additionalHeaders,
67
- // See https://github.com/dunglas/mercure/blob/master/hub/subscribe.go#L124-L130
80
+ // See https://github.com/dunglas/mercure/blob/main/subscribe.go#L347-L362
68
81
  'Content-Type': 'text/event-stream',
69
82
  Connection: 'keep-alive',
70
83
  // Disable cache, even for old browsers and proxies
@@ -82,12 +95,10 @@ export class SseStream extends Transform {
82
95
  this.commitHeaders();
83
96
  const sanitize = (val) => String(val).replace(/[\r\n]/g, '');
84
97
  let data = message.type ? `event: ${sanitize(message.type)}\n` : '';
85
- data +=
86
- message.id !== undefined && message.id !== null
87
- ? `id: ${sanitize(message.id)}\n`
88
- : '';
89
- data += message.retry ? `retry: ${sanitize(message.retry)}\n` : '';
90
- data += message.data ? toDataString(message.data) : '';
98
+ data += !isNil(message.id) ? `id: ${sanitize(message.id)}\n` : '';
99
+ data += !isNil(message.retry) ? `retry: ${sanitize(message.retry)}\n` : '';
100
+ data += !isNil(message.comment) ? toCommentString(message.comment) : '';
101
+ data += !isNil(message.data) ? toDataString(message.data) : '';
91
102
  data += '\n';
92
103
  this.push(data);
93
104
  callback();
@@ -96,7 +107,7 @@ export class SseStream extends Transform {
96
107
  * Calls `.write` but handles the drain if needed
97
108
  */
98
109
  writeMessage(message, cb) {
99
- if (message.id === undefined || message.id === null) {
110
+ if (isNil(message.id) && !isCommentOnly(message)) {
100
111
  this.lastEventId++;
101
112
  message.id = this.lastEventId.toString();
102
113
  }
package/scanner.js CHANGED
@@ -40,6 +40,9 @@ export class DependenciesScanner {
40
40
  async scanForModules({ moduleDefinition, lazy, scope = [], ctxRegistry = [], overrides = [], }) {
41
41
  const { moduleRef: moduleInstance, inserted: moduleInserted } = (await this.insertOrOverrideModule(moduleDefinition, overrides, scope)) ??
42
42
  {};
43
+ if (lazy && !moduleInserted && moduleInstance?.isInstantiated) {
44
+ return [];
45
+ }
43
46
  moduleDefinition =
44
47
  this.getOverrideModuleByModule(moduleDefinition, overrides)?.newModule ??
45
48
  moduleDefinition;
@@ -81,7 +84,7 @@ export class DependenciesScanner {
81
84
  if (!moduleInstance) {
82
85
  return registeredModuleRefs;
83
86
  }
84
- if (lazy && moduleInserted) {
87
+ if (lazy) {
85
88
  this.container.bindGlobalsToImports(moduleInstance);
86
89
  }
87
90
  return [moduleInstance].concat(registeredModuleRefs);