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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/Readme.md CHANGED
@@ -59,11 +59,11 @@ Nest is an MIT-licensed open source project. It can grow thanks to the sponsors
59
59
 
60
60
  <table style="text-align:center;">
61
61
  <tr>
62
- <td><a href="https://trilon.io" target="_blank"><img src="https://nestjs.com/img/trilon.svg" width="200" valign="middle" /></a></td>
63
- <td><a href="https://microsoft.com/" target="_blank"><img src="https://nestjs.com/img/logos/microsoft-logo.png" width="180" valign="middle" /></a></td>
64
- <td><a href="https://mojam.co" target="_blank"><img src="https://nestjs.com/img/logos/mojam-logo.png" width="80" valign="middle" /></a></td>
62
+ <td><a href="https://trilon.io" target="_blank"><img src="https://nestjs.com/img/github/trilon-logo.svg" width="200" valign="middle" /></a></td>
63
+ <td><a href="https://microsoft.com/" target="_blank"><img src="https://nestjs.com/img/github/microsoft-logo.png" width="180" valign="middle" /></a></td>
64
+ <td><a href="https://mojam.co" target="_blank"><img src="https://images.opencollective.com/mojamco/981e9de/logo/256.png?height=256" width="80" valign="middle" /></a></td>
65
65
  <td><a href="https://valor-software.com/" target="_blank"><img src="https://docs.nestjs.com/assets/sponsors/valor-software.png" width="170" valign="middle" /></a></td>
66
- <td><a href="https://serpapi.com/" target="_blank"><img src="https://nestjs.com/img/logos/serpapi-logo.png" width="150" valign="middle" /></a></td>
66
+ <td><a href="https://serpapi.com/" target="_blank"><img src="https://nestjs.com/img/github/serpapi-logo.png" width="150" valign="middle" /></a></td>
67
67
  </tr>
68
68
  </table>
69
69
 
@@ -71,15 +71,15 @@ Nest is an MIT-licensed open source project. It can grow thanks to the sponsors
71
71
 
72
72
  <table style="text-align:center;">
73
73
  <tr>
74
- <td><a href="https://www.redhat.com" target="_blank"><img src="https://nestjs.com/img/logos/red-hat-logo.svg" width="200" valign="middle" /></a></td>
75
- <td><a href="https://github.com/Sanofi-IADC" target="_blank"><img src="https://docs.nestjs.com/assets/sponsors/sanofi.png" width="180" valign="middle" /></a></td>
76
- <td><a href="https://nx.dev" target="_blank"><img src="https://nestjs.com/img/logos/nx-logo.png" height="45" valign="middle" /></a></td>
77
- <td><a href="https://intrinsic.ventures/" target="_blank"><img src="https://nestjs.com/img/logos/intrinisic-logo.png" width="210" valign="middle" /></a></td>
78
- <td><a href="https://jetbrains.com/" target="_blank"><img src="https://nestjs.com/img/logos/jetbrains-logo.svg" width="90" valign="middle" /></a></td>
74
+ <td><a href="https://www.redhat.com" target="_blank"><img src="https://nestjs.com/img/github/red-hat-logo.svg" width="150" valign="middle" /></a></td>
75
+ <td><a href="https://github.com/Sanofi-IADC" target="_blank"><img src="https://docs.nestjs.com/assets/sponsors/sanofi.png" width="120" valign="middle" /></a></td>
76
+ <td><a href="https://nx.dev" target="_blank"><img src="https://nestjs.com/img/github/nx-logo.png" height="45" valign="middle" /></a></td>
77
+ <td><a href="https://intrinsic.ventures/" target="_blank"><img src="https://nestjs.com/img/github/intrinisic-logo.png" width="150" valign="middle" /></a></td>
78
+ <td><a href="https://jetbrains.com/" target="_blank"><img src="https://nestjs.com/img/github/jetbrains-logo.svg" width="70" valign="middle" /></a></td>
79
79
  </tr>
80
80
  <tr>
81
- <td><a href="https://snyk.co/nestjs" target="_blank"><img src="https://nestjs.com/img/logos/snyk-logo-black.png" width="185" valign="middle" /></a></td>
82
- <td><a href="https://www.movavi.com/imovie-for-windows.html" target="_blank"><img src="https://nestjs.com/img/logos/movavi-logo.svg" width="105" valign="middle" /></a></td>
81
+ <td><a href="https://snyk.co/nestjs" target="_blank"><img src="https://nestjs.com/img/github/snyk-logo-black.png" width="125" valign="middle" /></a></td>
82
+ <td align="center"><a href="https://www.movavi.com/imovie-for-windows.html" target="_blank"><img src="https://images.opencollective.com/movavi/1fb15e5/avatar/256.png?height=256" width="60" valign="middle" /></a></td>
83
83
  </tr>
84
84
  </table>
85
85
 
@@ -87,15 +87,15 @@ Nest is an MIT-licensed open source project. It can grow thanks to the sponsors
87
87
 
88
88
  <table style="text-align:center;">
89
89
  <tr>
90
- <td><a href="https://www.mercedes-benz.com/" target="_blank"><img src="https://nestjs.com/img/logos/mercedes-logo.png" width="100" valign="middle" /></a></td>
91
- <td><a href="https://www.dinii.jp/" target="_blank"><img src="https://nestjs.com/img/logos/dinii-logo.png" width="65" valign="middle" /></a></td>
92
- <td><a href="https://handsontable.com/docs/react-data-grid/?utm_source=NestJS_GH&utm_medium=sponsorship&utm_campaign=library_sponsorship_2024" target="_blank"><img src="https://nestjs.com/img/logos/handsontable-dark-logo.svg#2" width="150" valign="middle" /></a></td>
93
- <td align="center" valign="middle"><a href="https://www.itflashcards.com/" target="_blank"><img src="https://nestjs.com/img/logos/it_flashcards-logo.png" width="170" valign="middle" /></a></td>
94
- <td align="center" valign="middle"><a href="https://arcjet.com/?ref=nestjs" target="_blank"><img src="https://nestjs.com/img/logos/arcjet-logo.svg" width="170" valign="middle" /></a></td>
95
- <td align="center" valign="middle"><a href="https://crawljobs.com" target="_blank"><img src="https://nestjs.com/img/logos/crawljobs-logo.svg" width="130" valign="middle" /></a></td>
90
+ <td><a href="https://www.mercedes-benz.com/" target="_blank"><img src="https://nestjs.com/img/github/mercedes-logo.png" width="100" valign="middle" /></a></td>
91
+ <td><a href="https://handsontable.com/docs/react-data-grid/?utm_source=NestJS_GH&utm_medium=sponsorship&utm_campaign=library_sponsorship_2024" target="_blank"><img src="https://nestjs.com/img/github/handsontable-dark-logo.svg#2" width="150" valign="middle" /></a></td>
92
+ <td align="center" valign="middle"><a href="https://www.itflashcards.com/" target="_blank"><img src="https://nestjs.com/img/github/it_flashcards-logo.png" width="170" valign="middle" /></a></td>
93
+ <td align="center" valign="middle"><a href="https://arcjet.com/?ref=nestjs" target="_blank"><img src="https://nestjs.com/img/github/arcjet-logo.svg" width="170" valign="middle" /></a></td>
94
+ <td align="center" valign="middle"><a href="https://pandektes.com" target="_blank"><img src="https://nestjs.com/img/github/pandektes-logo.png" width="65" valign="middle" /></a></td>
96
95
  </tr><tr>
97
- <td align="center" valign="middle"><a href="https://pandektes.com" target="_blank"><img src="https://nestjs.com/img/logos/pandektes-logo.png" width="65" valign="middle" /></a></td>
98
- <td align="center" valign="middle"><a href="https://www.fintechcrafts.com/" target="_blank"><img src="https://nestjs.com/img/logos/fintechcrafts-logo.svg" width="65" valign="middle" /></a></td>
96
+ <td align="center" valign="middle"><a href="https://www.fintechcrafts.com/" target="_blank"><img src="https://opencollective-production.s3.us-west-1.amazonaws.com/account-avatar/935813df-48a9-4b41-b923-7d70b5621c6b/2026-05-06%2010%2021%2033.png" width="65" valign="middle" /></a></td>
97
+ <td align="center" valign="middle"><a href="https://talordata.com/?campaignid=Ya5Xpw2gmhyee6Tg&utm_source=nestjs&utm_term=nestjs" target="_blank"><img src="https://images.opencollective.com/elowen/9ccac7f/avatar/512.png" width="65" valign="middle" /></a></td>
98
+ <td align="center" valign="middle"><a href="https://memory2.co/" target="_blank"><img src="https://images.opencollective.com/memory-squared/bbe37f5/avatar/256.png?height=50" width="65" valign="middle" /></a></td>
99
99
  </tr>
100
100
  </table>
101
101
 
@@ -103,33 +103,12 @@ Nest is an MIT-licensed open source project. It can grow thanks to the sponsors
103
103
 
104
104
  <table>
105
105
  <tr>
106
- <td align="center" valign="middle"><a href="https://www.swingdev.io" target="_blank"><img src="https://nestjs.com/img/logos/swingdev-logo.svg#1" width="110" valign="middle" /></a></td>
107
- <td align="center" valign="middle"><a href="https://www.novologic.com/" target="_blank"><img src="https://nestjs.com/img/logos/novologic.png" width="110" valign="middle" /></a></td>
108
- <td align="center" valign="middle"><a href="https://mantro.net/" target="_blank"><img src="https://nestjs.com/img/logos/mantro-logo.svg" width="95" valign="middle" /></a></td>
109
- <td align="center" valign="middle"><a href="https://triplebyte.com/" target="_blank"><img src="https://nestjs.com/img/logos/triplebyte.png" width="107" valign="middle" /></a></td>
110
- <td align="center" valign="middle"><a href="https://nearpod.com/" target="_blank"><img src="https://nestjs.com/img/logos/nearpod-logo.svg" width="100" valign="middle" /></a></td>
111
- <td align="center" valign="middle"><a href="https://genuinebee.com/" target="_blank"><img src="https://nestjs.com/img/logos/genuinebee.svg" width="97" valign="middle" /></a></td>
112
- </tr>
113
- <tr>
114
- <td align="center" valign="middle"><a href="https://vpn-review.com/vpn-for-torrenting" target="_blank"><img src="https://nestjs.com/img/logos/vpn-review-logo.png" width="85" valign="middle" /></a></td>
115
- <td align="center" valign="middle"><a href="https://lambda-it.ch/" target="_blank"><img src="https://nestjs.com/img/logos/lambda-it-logo.svg" width="115" valign="middle" /></a></td>
116
- <td align="center" valign="middle"><a href="https://rocketech.it/cases/?utm_source=google&utm_medium=badge&utm_campaign=nestjs" target="_blank"><img src="https://nestjs.com/img/logos/rocketech-logo.svg" width="110" valign="middle" /></a></td>
117
- <td align="center" valign="middle"><a href="https://www.anonymistic.com/" target="_blank"><img src="https://nestjs.com/img/logos/anonymistic-logo.png" width="125" valign="middle" /></a></td>
118
- <td align="center" valign="middle"><a href="https://www.naologic.com/" target="_blank"><img src="https://nestjs.com/img/logos/naologic-logo.svg" width="125" valign="middle" /></a></td>
119
- <td align="center" valign="middle"><a href="https://triplecore.io" target="_blank"><img src="https://nestjs.com/img/logos/triplecore-logo.svg" width="50" valign="middle" /></a></td>
120
- </tr>
121
- <tr>
122
- <td align="center" valign="middle"><a href="https://polygon-software.ch/" target="_blank"><img src="https://nestjs.com/img/logos/polygon-logo.svg" width="120" valign="middle" /></a></td>
123
- <td align="center" valign="middle"><a href="https://boringowl.io/" target="_blank"><img src="https://nestjs.com/img/logos/boringowl-logo.svg" width="120" valign="middle" /></a></td>
124
- <td align="center" valign="middle"><a href="https://nordbot.app/" target="_blank"><img src="https://nestjs.com/img/logos/nordbot-logo.png" width="120" valign="middle" /></a></td>
125
- <td align="center" valign="middle"><a href="https://doppio.sh/" target="_blank"><img src="https://nestjs.com/img/logos/dopiosh-logo.png" width="50" valign="middle" /></a></td>
126
- <td align="center" valign="middle"><a href="https://www.hingehealth.com/" target="_blank"><img src="https://nestjs.com/img/logos/hinge-health-logo.svg" width="100" valign="middle" /></a></td>
127
- <td align="center" valign="middle"><a href="https://www.tripoffice.com/" target="_blank"><img src="https://nestjs.com/img/logos/tripoffice-logo.png" width="140" valign="middle" /></a></td>
128
- </tr>
129
- <tr>
130
- <td align="center" valign="middle"><a href="https://solcellsforetag.se/" target="_blank"><img src="https://nestjs.com/img/logos/solcellsforetag-logo.svg" width="140" valign="middle" /></a></td>
131
- <td align="center" valign="middle"><a href="https://www.route4me.com/" target="_blank"><img src="https://nestjs.com/img/logos/route4me-logo.svg" width="100" valign="middle" /></a></td>
132
- <td align="center" valign="middle"><a href="https://memory2.co/" target="_blank"><img src="https://images.opencollective.com/memory-squared/bbe37f5/avatar/256.png?height=50" width="50" valign="middle" /></a></td>
106
+ <td align="center" valign="middle"><a href="https://mantro.net/" target="_blank"><img src="https://nestjs.com/img/github/mantro-logo.svg" width="95" valign="middle" /></a></td>
107
+ <td align="center" valign="middle"><a href="https://triplebyte.com/" target="_blank"><img src="https://nestjs.com/img/github/triplebyte-logo.png" width="107" valign="middle" /></a></td>
108
+ <td align="center" valign="middle"><a href="https://nearpod.com/" target="_blank"><img src="https://nestjs.com/img/github/nearpod-logo.svg" width="100" valign="middle" /></a></td>
109
+ <td align="center" valign="middle"><a href="https://www.naologic.com/" target="_blank"><img src="https://nestjs.com/img/github/naologic-logo.svg" width="125" valign="middle" /></a></td>
110
+ <td align="center" valign="middle"><a href="https://triplecore.io" target="_blank"><img src="https://nestjs.com/img/github/triplecore-logo.svg" width="50" valign="middle" /></a>
111
+ <td align="center" valign="middle"><a href="https://www.hingehealth.com/" target="_blank"><img src="https://nestjs.com/img/github/hinge-health-logo.svg" width="100" valign="middle" /></a></td>
133
112
  </tr>
134
113
  </table>
135
114
 
@@ -41,6 +41,8 @@ export declare abstract class AbstractHttpAdapter<TServer = any, TRequest = any,
41
41
  all(path: any, handler: RequestHandler): any;
42
42
  search(handler: RequestHandler): any;
43
43
  search(path: any, handler: RequestHandler): any;
44
+ query(handler: RequestHandler): any;
45
+ query(path: any, handler: RequestHandler): any;
44
46
  options(handler: RequestHandler): any;
45
47
  options(path: any, handler: RequestHandler): any;
46
48
  listen(port: string | number, callback?: () => void): any;
@@ -57,6 +57,9 @@ export class AbstractHttpAdapter {
57
57
  search(...args) {
58
58
  return this.instance.search(...args);
59
59
  }
60
+ query(...args) {
61
+ return this.instance.query(...args);
62
+ }
60
63
  options(...args) {
61
64
  return this.instance.options(...args);
62
65
  }
@@ -63,16 +63,21 @@ export class BaseExceptionFilter {
63
63
  if (!err || typeof err !== 'object') {
64
64
  return false;
65
65
  }
66
- if (err.constructor.name === 'FastifyError' &&
66
+ if (err.constructor?.name === 'FastifyError' &&
67
67
  typeof err.code === 'string' &&
68
68
  typeof err.statusCode === 'number') {
69
69
  return true;
70
70
  }
71
71
  // "http-errors" error signature
72
- return (typeof err.expose === 'boolean' &&
72
+ if (typeof err.expose === 'boolean' &&
73
73
  typeof err.statusCode === 'number' &&
74
74
  err.status === err.statusCode &&
75
- err instanceof Error);
75
+ err instanceof Error) {
76
+ return true;
77
+ }
78
+ // Plain "http error"-shaped values (e.g. objects thrown by third-party
79
+ // middleware) that carry a status code and a message
80
+ return !!(err.statusCode && err.message);
76
81
  }
77
82
  }
78
83
  __decorate([
@@ -12,6 +12,9 @@ export class Barrier {
12
12
  this.promise = new Promise(resolve => {
13
13
  this.resolve = resolve;
14
14
  });
15
+ if (targetCount === 0) {
16
+ this.resolve();
17
+ }
15
18
  }
16
19
  /**
17
20
  * Signal that a participant has reached the barrier.
@@ -20,7 +23,7 @@ export class Barrier {
20
23
  */
21
24
  signal() {
22
25
  this.currentCount += 1;
23
- if (this.currentCount === this.targetCount) {
26
+ if (this.currentCount >= this.targetCount) {
24
27
  this.resolve();
25
28
  }
26
29
  }
@@ -10,6 +10,7 @@ export type HandlerResponseBasicFn = <TResult, TResponse>(result: TResult, res:
10
10
  export type HandleSseResponseFn = <TResult extends Observable<unknown> = any, TResponse extends HeaderStream = any, TRequest extends IncomingMessage = any>(result: TResult, res: TResponse, req: TRequest) => any;
11
11
  export interface HandlerMetadata {
12
12
  argsLength: number;
13
+ isSseHandler: boolean;
13
14
  paramtypes: any[];
14
15
  httpStatusCode: number;
15
16
  responseHeaders: any[];
@@ -17,6 +17,7 @@ export declare const REQUEST_METHOD_MAP: {
17
17
  readonly 13: "move";
18
18
  readonly 14: "lock";
19
19
  readonly 15: "unlock";
20
+ readonly 16: "query";
20
21
  };
21
22
  export declare class RouterMethodFactory {
22
23
  get(target: HttpServer, requestMethod: RequestMethod): Function;
@@ -16,6 +16,7 @@ export const REQUEST_METHOD_MAP = {
16
16
  [RequestMethod.MOVE]: 'move',
17
17
  [RequestMethod.LOCK]: 'lock',
18
18
  [RequestMethod.UNLOCK]: 'unlock',
19
+ [RequestMethod.QUERY]: 'query',
19
20
  };
20
21
  export class RouterMethodFactory {
21
22
  get(target, requestMethod) {
@@ -1,3 +1,4 @@
1
+ import { Logger } from '@nestjs/common';
1
2
  import { isFunction, isNil } from '@nestjs/common/internal';
2
3
  import { iterate } from 'iterare';
3
4
  import { getInstancesGroupedByHierarchyLevel } from './utils/get-instances-grouped-by-hierarchy-level.js';
@@ -33,12 +34,20 @@ export async function callBeforeAppShutdownHook(moduleRef, signal) {
33
34
  const groupedInstances = getInstancesGroupedByHierarchyLevel(moduleRef.controllers, moduleRef.injectables, moduleRef.middlewares, providers);
34
35
  const levels = getSortedHierarchyLevels(groupedInstances, 'DESC');
35
36
  for (const level of levels) {
36
- await Promise.all(callOperator(groupedInstances.get(level), signal));
37
+ const results = await Promise.allSettled(callOperator(groupedInstances.get(level), signal));
38
+ results
39
+ .filter((result) => result.status === 'rejected')
40
+ .forEach(result => Logger.error(result.reason, result.reason?.stack));
37
41
  }
38
42
  const moduleClassInstance = moduleClassHost.instance;
39
43
  if (moduleClassInstance &&
40
44
  hasBeforeApplicationShutdownHook(moduleClassInstance) &&
41
45
  moduleClassHost.isDependencyTreeStatic()) {
42
- await moduleClassInstance.beforeApplicationShutdown(signal);
46
+ try {
47
+ await moduleClassInstance.beforeApplicationShutdown(signal);
48
+ }
49
+ catch (err) {
50
+ Logger.error(err, err?.stack);
51
+ }
43
52
  }
44
53
  }
@@ -1,3 +1,4 @@
1
+ import { Logger } from '@nestjs/common';
1
2
  import { isFunction, isNil } from '@nestjs/common/internal';
2
3
  import { iterate } from 'iterare';
3
4
  import { getInstancesGroupedByHierarchyLevel } from './utils/get-instances-grouped-by-hierarchy-level.js';
@@ -35,13 +36,21 @@ export async function callAppShutdownHook(moduleRef, signal) {
35
36
  const groupedInstances = getInstancesGroupedByHierarchyLevel(moduleRef.controllers, moduleRef.injectables, moduleRef.middlewares, providers);
36
37
  const levels = getSortedHierarchyLevels(groupedInstances, 'DESC');
37
38
  for (const level of levels) {
38
- await Promise.all(callOperator(groupedInstances.get(level), signal));
39
+ const results = await Promise.allSettled(callOperator(groupedInstances.get(level), signal));
40
+ results
41
+ .filter((result) => result.status === 'rejected')
42
+ .forEach(result => Logger.error(result.reason, result.reason?.stack));
39
43
  }
40
44
  // Call the instance itself
41
45
  const moduleClassInstance = moduleClassHost.instance;
42
46
  if (moduleClassInstance &&
43
47
  hasOnAppShutdownHook(moduleClassInstance) &&
44
48
  moduleClassHost.isDependencyTreeStatic()) {
45
- await moduleClassInstance.onApplicationShutdown(signal);
49
+ try {
50
+ await moduleClassInstance.onApplicationShutdown(signal);
51
+ }
52
+ catch (err) {
53
+ Logger.error(err, err?.stack);
54
+ }
46
55
  }
47
56
  }
@@ -1,3 +1,4 @@
1
+ import { Logger } from '@nestjs/common';
1
2
  import { isFunction, isNil } from '@nestjs/common/internal';
2
3
  import { iterate } from 'iterare';
3
4
  import { getInstancesGroupedByHierarchyLevel } from './utils/get-instances-grouped-by-hierarchy-level.js';
@@ -34,13 +35,21 @@ export async function callModuleDestroyHook(moduleRef) {
34
35
  const groupedInstances = getInstancesGroupedByHierarchyLevel(moduleRef.controllers, moduleRef.injectables, moduleRef.middlewares, providers);
35
36
  const levels = getSortedHierarchyLevels(groupedInstances, 'DESC');
36
37
  for (const level of levels) {
37
- await Promise.all(callOperator(groupedInstances.get(level)));
38
+ const results = await Promise.allSettled(callOperator(groupedInstances.get(level)));
39
+ results
40
+ .filter((result) => result.status === 'rejected')
41
+ .forEach(result => Logger.error(result.reason, result.reason?.stack));
38
42
  }
39
43
  // Call the module instance itself
40
44
  const moduleClassInstance = moduleClassHost.instance;
41
45
  if (moduleClassInstance &&
42
46
  hasOnModuleDestroyHook(moduleClassInstance) &&
43
47
  moduleClassHost.isDependencyTreeStatic()) {
44
- await moduleClassInstance.onModuleDestroy();
48
+ try {
49
+ await moduleClassInstance.onModuleDestroy();
50
+ }
51
+ catch (err) {
52
+ Logger.error(err, err?.stack);
53
+ }
45
54
  }
46
55
  }
@@ -66,7 +66,7 @@ export class NestContainer {
66
66
  if (this.modules.has(token)) {
67
67
  return {
68
68
  moduleRef: this.modules.get(token),
69
- inserted: true,
69
+ inserted: false,
70
70
  };
71
71
  }
72
72
  return {
@@ -92,7 +92,7 @@ export class NestContainer {
92
92
  type,
93
93
  dynamicMetadata,
94
94
  }, scope),
95
- inserted: false,
95
+ inserted: true,
96
96
  };
97
97
  }
98
98
  async setModule({ token, dynamicMetadata, type }, scope) {
@@ -329,16 +329,12 @@ export class Injector {
329
329
  this.printFoundInModuleLog(name, relatedModule);
330
330
  instanceWrapperRef = providers.get(name);
331
331
  this.addDependencyMetadata(keyOrIndex, wrapper, instanceWrapperRef);
332
- const inquirerId = this.getContextInquirerId(resolutionContext);
333
- const instanceHost = instanceWrapperRef.getInstanceByContextId(this.getContextId(resolutionContext.contextId, instanceWrapperRef), inquirerId);
334
- if (!instanceHost.isResolved && !instanceWrapperRef.forwardRef) {
335
- /*
336
- * Provider will be loaded shortly in resolveComponentHost() once we pass the current
337
- * Barrier. We cannot load it here because doing so could incorrectly evaluate the
338
- * staticity of the dependency tree and lead to undefined / null injection.
339
- */
340
- break;
341
- }
332
+ /*
333
+ * Stop at the first direct export match. Continuing when the provider is
334
+ * already resolved would let a later import (e.g. a global forRoot module)
335
+ * override an explicit forFeature import for the same token.
336
+ */
337
+ break;
342
338
  }
343
339
  return instanceWrapperRef;
344
340
  }
@@ -15,7 +15,7 @@ export class InternalCoreModuleFactory {
15
15
  timestamp: false,
16
16
  });
17
17
  const injector = new Injector({
18
- preview: container.contextOptions?.preview,
18
+ preview: container.contextOptions?.preview ?? false,
19
19
  instanceDecorator: container.contextOptions?.instrument?.instanceDecorator,
20
20
  });
21
21
  const instanceLoader = new InstanceLoader(container, injector, graphInspector, logger);
@@ -35,6 +35,12 @@ export declare class Module {
35
35
  get entryProviders(): Array<InstanceWrapper<Injectable>>;
36
36
  get exports(): Set<InjectionToken>;
37
37
  get instance(): NestModule;
38
+ /**
39
+ * Modules are registered in the container before they are scanned (dynamic
40
+ * `imports` are pre-registered by `NestContainer#addDynamicMetadata`), so
41
+ * registration alone does not imply the module was ever instantiated.
42
+ */
43
+ get isInstantiated(): boolean;
38
44
  get metatype(): Type<any>;
39
45
  get distance(): number;
40
46
  set distance(value: number);
@@ -83,6 +83,14 @@ export class Module {
83
83
  const moduleRef = this._providers.get(this._metatype);
84
84
  return moduleRef.instance;
85
85
  }
86
+ /**
87
+ * Modules are registered in the container before they are scanned (dynamic
88
+ * `imports` are pre-registered by `NestContainer#addDynamicMetadata`), so
89
+ * registration alone does not imply the module was ever instantiated.
90
+ */
91
+ get isInstantiated() {
92
+ return !isNil(this._providers.get(this._metatype)?.instance);
93
+ }
86
94
  get metatype() {
87
95
  return this._metatype;
88
96
  }
@@ -1,11 +1,11 @@
1
1
  import { AsyncResource } from 'async_hooks';
2
- import { Observable, defer, from as fromPromise } from 'rxjs';
3
- import { mergeAll, switchMap } from 'rxjs/operators';
2
+ import { Observable, defer, from } from 'rxjs';
3
+ import { mergeAll } from 'rxjs/operators';
4
4
  import { ExecutionContextHost } from '../helpers/execution-context-host.js';
5
5
  import { isEmptyArray } from '@nestjs/common/internal';
6
6
  export class InterceptorsConsumer {
7
7
  async intercept(interceptors, args, instance, callback, next, type) {
8
- if (isEmptyArray(interceptors)) {
8
+ if (!interceptors || isEmptyArray(interceptors)) {
9
9
  return next();
10
10
  }
11
11
  const context = this.createContext(args, instance, callback);
@@ -25,9 +25,34 @@ export class InterceptorsConsumer {
25
25
  return new ExecutionContextHost(args, instance.constructor, callback);
26
26
  }
27
27
  transformDeferred(next) {
28
- return fromPromise(next()).pipe(switchMap(res => {
29
- const isDeferred = res instanceof Promise || res instanceof Observable;
30
- return isDeferred ? res : Promise.resolve(res);
31
- }));
28
+ // Call next() eagerly here — this method is invoked inside
29
+ // defer(AsyncResource.bind(...)), so the async context (e.g. AsyncLocalStorage)
30
+ // is correctly inherited. Deferring next() into the subscriber function would
31
+ // lose that context because the subscriber is called outside the bound scope.
32
+ const nextPromise = next();
33
+ return new Observable(subscriber => {
34
+ let innerSub;
35
+ nextPromise
36
+ .then(res => {
37
+ if (subscriber.closed) {
38
+ // The outer subscription was torn down (e.g. an SSE client disconnect)
39
+ // before the async handler resolved. Do not subscribe the producer
40
+ // Observable after the consumer has already gone away — subscribing
41
+ // only to unsubscribe in the same tick would start producer side
42
+ // effects just to immediately abort them.
43
+ return;
44
+ }
45
+ const isDeferred = res instanceof Promise || res instanceof Observable;
46
+ innerSub = from(isDeferred ? res : Promise.resolve(res)).subscribe(subscriber);
47
+ })
48
+ .catch(err => {
49
+ if (!subscriber.closed) {
50
+ subscriber.error(err);
51
+ }
52
+ });
53
+ return () => {
54
+ innerSub?.unsubscribe();
55
+ };
56
+ });
32
57
  }
33
58
  }
@@ -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
  }
@@ -85,18 +85,18 @@ export class NestApplication extends NestApplicationContext {
85
85
  return this.httpAdapter.getHttpServer();
86
86
  }
87
87
  async registerModules() {
88
- this.registerWsModule();
88
+ await this.registerWsModule();
89
89
  if (this.microservicesModule) {
90
90
  this.microservicesModule.register(this.container, this.graphInspector, this.config, this.appOptions);
91
91
  this.microservicesModule.setupClients(this.container);
92
92
  }
93
93
  await this.middlewareModule.register(this.middlewareContainer, this.container, this.config, this.injector, this.httpAdapter, this.graphInspector, this.appOptions);
94
94
  }
95
- registerWsModule() {
95
+ async registerWsModule() {
96
96
  if (!this.socketModule) {
97
97
  return;
98
98
  }
99
- this.socketModule.register(this.container, this.config, this.graphInspector, this.appOptions, this.httpServer);
99
+ await this.socketModule.register(this.container, this.config, this.graphInspector, this.appOptions, this.httpServer);
100
100
  this.isWsModuleRegistered = true;
101
101
  }
102
102
  async init() {
@@ -204,7 +204,7 @@ export class NestApplication extends NestApplicationContext {
204
204
  this.routesResolver.registerExceptionHandler();
205
205
  }
206
206
  connectMicroservice(microserviceOptions, hybridAppOptions = {}) {
207
- const { NestMicroservice } = loadPackageCached('@nestjs/microservices');
207
+ const { NestMicroservice } = loadPackageCached('@nestjs/microservices', 'NestFactory');
208
208
  const { inheritAppConfig } = hybridAppOptions;
209
209
  const applicationConfig = inheritAppConfig
210
210
  ? this.config
@@ -230,7 +230,7 @@ export class NestApplication extends NestApplicationContext {
230
230
  return this;
231
231
  }
232
232
  use(...args) {
233
- this.httpAdapter.use(...args);
233
+ this.httpAdapter.use(...this.applyFunctionDecoratorIfRegistered(args));
234
234
  return this;
235
235
  }
236
236
  useBodyParser(...args) {
@@ -415,6 +415,20 @@ export class NestApplication extends NestApplicationContext {
415
415
  }
416
416
  return instances;
417
417
  }
418
+ applyFunctionDecoratorIfRegistered(args) {
419
+ if (!this.appOptions.instrument?.instanceDecorator) {
420
+ return args;
421
+ }
422
+ const [firstArg, secondArg] = args;
423
+ return [
424
+ isFunction(firstArg)
425
+ ? this.appOptions.instrument.instanceDecorator(firstArg)
426
+ : firstArg,
427
+ isFunction(secondArg)
428
+ ? this.appOptions.instrument.instanceDecorator(secondArg)
429
+ : secondArg,
430
+ ];
431
+ }
418
432
  async loadSocketModule() {
419
433
  if (!this.socketModule) {
420
434
  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.6",
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.6"
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": "4c38a5ab100f1d2a0d50602998968a25509525c7"
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);