@nestjs/core 12.0.0-alpha.4 → 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.
Files changed (65) hide show
  1. package/Readme.md +25 -45
  2. package/adapters/http-adapter.d.ts +2 -0
  3. package/adapters/http-adapter.js +3 -0
  4. package/application-config.d.ts +8 -2
  5. package/application-config.js +14 -0
  6. package/errors/exceptions/index.d.ts +1 -0
  7. package/errors/exceptions/index.js +1 -0
  8. package/errors/exceptions/invalid-class-module.exception.d.ts +1 -1
  9. package/errors/exceptions/invalid-class-module.exception.js +2 -2
  10. package/errors/exceptions/invalid-module.exception.d.ts +1 -1
  11. package/errors/exceptions/invalid-module.exception.js +2 -2
  12. package/errors/exceptions/route-conflict.exception.d.ts +4 -0
  13. package/errors/exceptions/route-conflict.exception.js +7 -0
  14. package/errors/messages.d.ts +5 -2
  15. package/errors/messages.js +37 -4
  16. package/exceptions/base-exception-filter.js +8 -3
  17. package/helpers/barrier.js +4 -1
  18. package/helpers/handler-metadata-storage.d.ts +1 -0
  19. package/helpers/router-method-factory.d.ts +1 -0
  20. package/helpers/router-method-factory.js +1 -0
  21. package/hooks/before-app-shutdown.hook.js +11 -2
  22. package/hooks/on-app-shutdown.hook.js +11 -2
  23. package/hooks/on-module-destroy.hook.js +11 -2
  24. package/injector/container.js +2 -2
  25. package/injector/helpers/transient-instances.d.ts +12 -0
  26. package/injector/helpers/transient-instances.js +28 -0
  27. package/injector/injector.d.ts +4 -3
  28. package/injector/injector.js +34 -13
  29. package/injector/instance-wrapper.d.ts +2 -0
  30. package/injector/instance-wrapper.js +25 -0
  31. package/injector/internal-core-module/internal-core-module-factory.js +1 -1
  32. package/injector/module.d.ts +6 -0
  33. package/injector/module.js +8 -0
  34. package/interceptors/interceptors-consumer.js +32 -7
  35. package/middleware/builder.js +5 -1
  36. package/nest-application-context.js +1 -1
  37. package/nest-application.d.ts +3 -1
  38. package/nest-application.js +96 -6
  39. package/package.json +3 -14
  40. package/router/interfaces/index.d.ts +3 -0
  41. package/router/interfaces/index.js +3 -0
  42. package/router/interfaces/resolved-route.interface.d.ts +32 -0
  43. package/router/interfaces/resolved-route.interface.js +1 -0
  44. package/router/interfaces/resolver.interface.d.ts +5 -1
  45. package/router/interfaces/route-conflict.interface.d.ts +14 -0
  46. package/router/interfaces/route-conflict.interface.js +1 -0
  47. package/router/interfaces/route-resolution-options.interface.d.ts +24 -0
  48. package/router/interfaces/route-resolution-options.interface.js +1 -0
  49. package/router/legacy-route-converter.d.ts +1 -1
  50. package/router/legacy-route-converter.js +24 -13
  51. package/router/route-conflict-detector.d.ts +71 -0
  52. package/router/route-conflict-detector.js +276 -0
  53. package/router/route-specificity-sorter.d.ts +23 -0
  54. package/router/route-specificity-sorter.js +59 -0
  55. package/router/router-execution-context.d.ts +1 -0
  56. package/router/router-execution-context.js +26 -3
  57. package/router/router-explorer.d.ts +11 -2
  58. package/router/router-explorer.js +61 -19
  59. package/router/router-response-controller.d.ts +1 -0
  60. package/router/router-response-controller.js +126 -49
  61. package/router/routes-resolver.d.ts +7 -4
  62. package/router/routes-resolver.js +9 -7
  63. package/router/sse-stream.d.ts +1 -0
  64. package/router/sse-stream.js +24 -13
  65. package/scanner.js +13 -6
@@ -45,10 +45,10 @@ export class RouterExplorer {
45
45
  const interceptorsConsumer = new InterceptorsConsumer();
46
46
  this.executionContextCreator = new RouterExecutionContext(routeParamsFactory, pipesContextCreator, pipesConsumer, guardsContextCreator, guardsConsumer, interceptorsContextCreator, interceptorsConsumer, container.getHttpAdapterRef());
47
47
  }
48
- explore(instanceWrapper, moduleKey, httpAdapterRef, host, routePathMetadata) {
48
+ explore(instanceWrapper, moduleKey, httpAdapterRef, host, routePathMetadata, options = {}) {
49
49
  const { instance } = instanceWrapper;
50
50
  const routerPaths = this.pathsExplorer.scanForPaths(instance);
51
- this.applyPathsToRouterProxy(httpAdapterRef, routerPaths, instanceWrapper, moduleKey, routePathMetadata, host);
51
+ this.applyPathsToRouterProxy(httpAdapterRef, routerPaths, instanceWrapper, moduleKey, routePathMetadata, host, options);
52
52
  }
53
53
  extractRouterPath(metatype) {
54
54
  const path = Reflect.getMetadata(PATH_METADATA, metatype);
@@ -60,14 +60,15 @@ export class RouterExplorer {
60
60
  }
61
61
  return [addLeadingSlash(path)];
62
62
  }
63
- applyPathsToRouterProxy(router, routeDefinitions, instanceWrapper, moduleKey, routePathMetadata, host) {
63
+ applyPathsToRouterProxy(router, routeDefinitions, instanceWrapper, moduleKey, routePathMetadata, host, options = {}) {
64
64
  (routeDefinitions || []).forEach(routeDefinition => {
65
65
  const { version: methodVersion } = routeDefinition;
66
66
  routePathMetadata.methodVersion = methodVersion;
67
- this.applyCallbackToRouter(router, routeDefinition, instanceWrapper, moduleKey, routePathMetadata, host);
67
+ this.applyCallbackToRouter(router, routeDefinition, instanceWrapper, moduleKey, routePathMetadata, host, options);
68
68
  });
69
69
  }
70
- applyCallbackToRouter(router, routeDefinition, instanceWrapper, moduleKey, routePathMetadata, host) {
70
+ applyCallbackToRouter(router, routeDefinition, instanceWrapper, moduleKey, routePathMetadata, host, options = {}) {
71
+ const { onRouteResolved, deferRegistration = false } = options;
71
72
  const { path: paths, requestMethod, targetCallback, methodName, } = routeDefinition;
72
73
  const { instance } = instanceWrapper;
73
74
  const routerMethodRef = this.routerMethodFactory
@@ -90,6 +91,9 @@ export class RouterExplorer {
90
91
  routePathMetadata.methodPath = path;
91
92
  const pathsToRegister = this.routePathFactory.create(routePathMetadata, requestMethod);
92
93
  pathsToRegister.forEach(path => {
94
+ const normalizedPath = router.normalizePath
95
+ ? router.normalizePath(path)
96
+ : path;
93
97
  const entrypointDefinition = {
94
98
  type: 'http-endpoint',
95
99
  methodName,
@@ -103,21 +107,34 @@ export class RouterExplorer {
103
107
  controllerVersion: routePathMetadata.controllerVersion,
104
108
  },
105
109
  };
106
- this.copyMetadataToCallback(targetCallback, routeHandler);
107
- const normalizedPath = router.normalizePath
108
- ? router.normalizePath(path)
109
- : path;
110
- const httpAdapter = this.container.getHttpAdapterRef();
111
- const onRouteTriggered = httpAdapter.getOnRouteTriggered?.();
112
- if (onRouteTriggered) {
113
- routerMethodRef(normalizedPath, (...args) => {
114
- onRouteTriggered(requestMethod, path);
115
- return routeHandler(...args);
116
- });
117
- }
118
- else {
119
- routerMethodRef(normalizedPath, routeHandler);
110
+ if (!deferRegistration) {
111
+ this.copyMetadataToCallback(targetCallback, routeHandler);
112
+ const httpAdapter = this.container.getHttpAdapterRef();
113
+ const onRouteTriggered = httpAdapter.getOnRouteTriggered?.();
114
+ if (onRouteTriggered) {
115
+ routerMethodRef(normalizedPath, (...args) => {
116
+ onRouteTriggered(requestMethod, path);
117
+ return routeHandler(...args);
118
+ });
119
+ }
120
+ else {
121
+ routerMethodRef(normalizedPath, routeHandler);
122
+ }
120
123
  }
124
+ onRouteResolved?.({
125
+ method: requestMethod,
126
+ path: normalizedPath,
127
+ rawPath: path,
128
+ host,
129
+ version: routePathMetadata.methodVersion ??
130
+ routePathMetadata.controllerVersion,
131
+ methodVersion: routePathMetadata.methodVersion,
132
+ controllerVersion: routePathMetadata.controllerVersion,
133
+ handler: routeHandler,
134
+ targetCallback,
135
+ methodName,
136
+ instanceWrapper,
137
+ });
121
138
  this.graphInspector.insertEntrypointDefinition(entrypointDefinition, instanceWrapper.id);
122
139
  });
123
140
  const pathsToLog = this.routePathFactory.create({
@@ -135,6 +152,31 @@ export class RouterExplorer {
135
152
  });
136
153
  });
137
154
  }
155
+ /**
156
+ * Registers a previously resolved route on the underlying HTTP adapter.
157
+ * Used when route registration has been deferred (e.g. when sorting
158
+ * routes by specificity) so the caller can choose the order in which
159
+ * routes are installed on the adapter.
160
+ */
161
+ registerResolvedRoute(router, route) {
162
+ const routerMethodRef = this.routerMethodFactory
163
+ .get(router, route.method)
164
+ .bind(router);
165
+ this.copyMetadataToCallback(route.targetCallback, route.handler);
166
+ const normalizedPath = route.path;
167
+ const rawPath = route.rawPath ?? route.path;
168
+ const httpAdapter = this.container.getHttpAdapterRef();
169
+ const onRouteTriggered = httpAdapter.getOnRouteTriggered?.();
170
+ if (onRouteTriggered) {
171
+ routerMethodRef(normalizedPath, (...args) => {
172
+ onRouteTriggered(route.method, rawPath);
173
+ return route.handler(...args);
174
+ });
175
+ }
176
+ else {
177
+ routerMethodRef(normalizedPath, route.handler);
178
+ }
179
+ }
138
180
  applyHostFilter(host, handler) {
139
181
  if (!host) {
140
182
  return handler;
@@ -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
  }
@@ -1,11 +1,13 @@
1
+ import { type HttpServer } from '@nestjs/common';
2
+ import { type Controller } from '@nestjs/common/internal';
1
3
  import { ApplicationConfig } from '../application-config.js';
2
4
  import { NestContainer } from '../injector/container.js';
3
5
  import { Injector } from '../injector/injector.js';
4
6
  import { InstanceWrapper } from '../injector/instance-wrapper.js';
5
7
  import { GraphInspector } from '../inspector/graph-inspector.js';
8
+ import { ResolvedRoute } from './interfaces/resolved-route.interface.js';
6
9
  import { Resolver } from './interfaces/resolver.interface.js';
7
- import { type Controller } from '@nestjs/common/internal';
8
- import { type HttpServer } from '@nestjs/common';
10
+ import { RouteResolutionOptions } from './interfaces/route-resolution-options.interface.js';
9
11
  export declare class RoutesResolver implements Resolver {
10
12
  private readonly container;
11
13
  private readonly applicationConfig;
@@ -16,8 +18,9 @@ export declare class RoutesResolver implements Resolver {
16
18
  private readonly routerExceptionsFilter;
17
19
  private readonly routerExplorer;
18
20
  constructor(container: NestContainer, applicationConfig: ApplicationConfig, injector: Injector, graphInspector: GraphInspector);
19
- resolve<T extends HttpServer>(applicationRef: T, globalPrefix: string): void;
20
- registerRouters(routes: Map<string | symbol | Function, InstanceWrapper<Controller>>, moduleName: string, globalPrefix: string, modulePath: string, applicationRef: HttpServer): void;
21
+ resolve<T extends HttpServer>(applicationRef: T, globalPrefix: string, options?: RouteResolutionOptions): void;
22
+ registerResolvedRoute<T extends HttpServer>(applicationRef: T, route: ResolvedRoute): void;
23
+ registerRouters(routes: Map<string | symbol | Function, InstanceWrapper<Controller>>, moduleName: string, globalPrefix: string, modulePath: string, applicationRef: HttpServer, options?: RouteResolutionOptions): void;
21
24
  registerNotFoundHandler(): void;
22
25
  registerExceptionHandler(): void;
23
26
  private getModulePathMetadata;
@@ -1,12 +1,11 @@
1
- import { NotFoundException, } from '@nestjs/common';
1
+ import { Logger, NotFoundException, } from '@nestjs/common';
2
+ import { HOST_METADATA, MODULE_PATH, VERSION_METADATA, } from '@nestjs/common/internal';
2
3
  import { CONTROLLER_MAPPING_MESSAGE, VERSIONED_CONTROLLER_MAPPING_MESSAGE, } from '../helpers/messages.js';
3
4
  import { MetadataScanner } from '../metadata-scanner.js';
4
5
  import { RoutePathFactory } from './route-path-factory.js';
5
6
  import { RouterExceptionFilters } from './router-exception-filters.js';
6
7
  import { RouterExplorer } from './router-explorer.js';
7
8
  import { RouterProxy } from './router-proxy.js';
8
- import { HOST_METADATA, MODULE_PATH, VERSION_METADATA, } from '@nestjs/common/internal';
9
- import { Logger } from '@nestjs/common';
10
9
  export class RoutesResolver {
11
10
  container;
12
11
  applicationConfig;
@@ -28,14 +27,17 @@ export class RoutesResolver {
28
27
  const metadataScanner = new MetadataScanner();
29
28
  this.routerExplorer = new RouterExplorer(metadataScanner, this.container, this.injector, this.routerProxy, this.routerExceptionsFilter, this.applicationConfig, this.routePathFactory, graphInspector);
30
29
  }
31
- resolve(applicationRef, globalPrefix) {
30
+ resolve(applicationRef, globalPrefix, options = {}) {
32
31
  const modules = this.container.getModules();
33
32
  modules.forEach(({ controllers, metatype }, moduleName) => {
34
33
  const modulePath = this.getModulePathMetadata(metatype);
35
- this.registerRouters(controllers, moduleName, globalPrefix, modulePath, applicationRef);
34
+ this.registerRouters(controllers, moduleName, globalPrefix, modulePath, applicationRef, options);
36
35
  });
37
36
  }
38
- registerRouters(routes, moduleName, globalPrefix, modulePath, applicationRef) {
37
+ registerResolvedRoute(applicationRef, route) {
38
+ this.routerExplorer.registerResolvedRoute(applicationRef, route);
39
+ }
40
+ registerRouters(routes, moduleName, globalPrefix, modulePath, applicationRef, options = {}) {
39
41
  routes.forEach(instanceWrapper => {
40
42
  const { metatype } = instanceWrapper;
41
43
  const host = this.getHostMetadata(metatype);
@@ -68,7 +70,7 @@ export class RoutesResolver {
68
70
  controllerVersion,
69
71
  versioningOptions,
70
72
  };
71
- this.routerExplorer.explore(instanceWrapper, moduleName, applicationRef, host, routePathMetadata);
73
+ this.routerExplorer.explore(instanceWrapper, moduleName, applicationRef, host, routePathMetadata, options);
72
74
  });
73
75
  });
74
76
  }
@@ -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;
@@ -64,7 +67,7 @@ export class DependenciesScanner {
64
67
  throw new UndefinedModuleException(moduleDefinition, index, scope);
65
68
  }
66
69
  if (!innerModule) {
67
- throw new InvalidModuleException(moduleDefinition, index, scope);
70
+ throw new InvalidModuleException(moduleDefinition, index, scope, innerModule);
68
71
  }
69
72
  if (ctxRegistry.includes(innerModule)) {
70
73
  continue;
@@ -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);
@@ -90,10 +93,14 @@ export class DependenciesScanner {
90
93
  const moduleToAdd = this.isForwardReference(moduleDefinition)
91
94
  ? moduleDefinition.forwardRef()
92
95
  : moduleDefinition;
93
- if (this.isInjectable(moduleToAdd) ||
94
- this.isController(moduleToAdd) ||
95
- this.isExceptionFilter(moduleToAdd)) {
96
- throw new InvalidClassModuleException(moduleDefinition, scope);
96
+ if (this.isInjectable(moduleToAdd)) {
97
+ throw new InvalidClassModuleException(moduleDefinition, scope, 'provider');
98
+ }
99
+ if (this.isController(moduleToAdd)) {
100
+ throw new InvalidClassModuleException(moduleDefinition, scope, 'controller');
101
+ }
102
+ if (this.isExceptionFilter(moduleToAdd)) {
103
+ throw new InvalidClassModuleException(moduleDefinition, scope, 'filter');
97
104
  }
98
105
  return this.container.addModule(moduleToAdd, scope);
99
106
  }