@nestjs/core 11.1.29 → 11.2.1

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.
@@ -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 @@ 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
  }
@@ -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;
@@ -19,6 +19,7 @@ exports.REQUEST_METHOD_MAP = {
19
19
  [request_method_enum_1.RequestMethod.MOVE]: 'move',
20
20
  [request_method_enum_1.RequestMethod.LOCK]: 'lock',
21
21
  [request_method_enum_1.RequestMethod.UNLOCK]: 'unlock',
22
+ [request_method_enum_1.RequestMethod.QUERY]: 'query',
22
23
  };
23
24
  class RouterMethodFactory {
24
25
  get(target, requestMethod) {
@@ -65,7 +65,7 @@ class NestContainer {
65
65
  if (this.modules.has(token)) {
66
66
  return {
67
67
  moduleRef: this.modules.get(token),
68
- inserted: true,
68
+ inserted: false,
69
69
  };
70
70
  }
71
71
  return {
@@ -91,7 +91,7 @@ class NestContainer {
91
91
  type,
92
92
  dynamicMetadata,
93
93
  }, scope),
94
- inserted: false,
94
+ inserted: true,
95
95
  };
96
96
  }
97
97
  async setModule({ token, dynamicMetadata, type }, scope) {
@@ -18,7 +18,7 @@ class InternalCoreModuleFactory {
18
18
  timestamp: false,
19
19
  });
20
20
  const injector = new injector_1.Injector({
21
- preview: container.contextOptions?.preview,
21
+ preview: container.contextOptions?.preview ?? false,
22
22
  instanceDecorator: container.contextOptions?.instrument?.instanceDecorator,
23
23
  });
24
24
  const instanceLoader = new instance_loader_1.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);
@@ -84,6 +84,14 @@ class Module {
84
84
  const moduleRef = this._providers.get(this._metatype);
85
85
  return moduleRef.instance;
86
86
  }
87
+ /**
88
+ * Modules are registered in the container before they are scanned (dynamic
89
+ * `imports` are pre-registered by `NestContainer#addDynamicMetadata`), so
90
+ * registration alone does not imply the module was ever instantiated.
91
+ */
92
+ get isInstantiated() {
93
+ return !(0, shared_utils_1.isNil)(this._providers.get(this._metatype)?.instance);
94
+ }
87
95
  get metatype() {
88
96
  return this._metatype;
89
97
  }
@@ -39,12 +39,10 @@ class InterceptorsConsumer {
39
39
  .then(res => {
40
40
  if (subscriber.closed) {
41
41
  // The outer subscription was torn down (e.g. an SSE client disconnect)
42
- // before the async handler resolved. Subscribe-and-immediately-unsubscribe
43
- // so the producer Observable's teardown/cleanup logic still runs.
44
- if (res instanceof rxjs_1.Observable) {
45
- const sub = res.subscribe({ error: () => { } });
46
- sub.unsubscribe();
47
- }
42
+ // before the async handler resolved. Do not subscribe the producer
43
+ // Observable after the consumer has already gone away — subscribing
44
+ // only to unsubscribe in the same tick would start producer side
45
+ // effects just to immediately abort them.
48
46
  return;
49
47
  }
50
48
  const isDeferred = res instanceof Promise || res instanceof rxjs_1.Observable;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nestjs/core",
3
- "version": "11.1.29",
3
+ "version": "11.2.1",
4
4
  "description": "Nest - modern, fast, powerful node.js web framework (@core)",
5
5
  "author": "Kamil Mysliwiec",
6
6
  "license": "MIT",
@@ -28,7 +28,7 @@
28
28
  "uid": "2.0.2"
29
29
  },
30
30
  "devDependencies": {
31
- "@nestjs/common": "11.1.29"
31
+ "@nestjs/common": "11.2.1"
32
32
  },
33
33
  "peerDependencies": {
34
34
  "@nestjs/common": "^11.0.0",
@@ -49,5 +49,5 @@
49
49
  "optional": true
50
50
  }
51
51
  },
52
- "gitHead": "03587a112ebbbb53a6bc3875afb5a9205aa5a5f1"
52
+ "gitHead": "4535f43b4890c9c69c57c5a5a8b49f62c83d1ed6"
53
53
  }
@@ -45,10 +45,12 @@ class LegacyRouteConverter {
45
45
  }
46
46
  // When route includes any wildcard segments in the middle.
47
47
  if (normalizedRoute.includes('/*/')) {
48
- // Replace each /*/ segment with a named parameter using different name for each segment.
49
- const convertedRoute = route.replaceAll('/*/', (match, offset) => {
50
- return `/*path${offset}/`;
51
- });
48
+ // Replace each "*" segment with a named parameter, using a different name
49
+ // for each. Match "/*" with a lookahead for the following "/" so the
50
+ // trailing slash is not consumed. Consuming it made two adjacent "/*/*/"
51
+ // segments share a slash, so only the first one got converted and the
52
+ // second was left as an unnamed "*" that path-to-regexp still rejects.
53
+ const convertedRoute = route.replaceAll(/\/\*(?=\/)/g, (match, offset) => `/*path${offset}`);
52
54
  printWarning(route, convertedRoute);
53
55
  return convertedRoute;
54
56
  }
@@ -51,4 +51,5 @@ export declare class RouterExecutionContext {
51
51
  })[]): (<TRequest, TResponse>(args: any[], req: TRequest, res: TResponse, next: Function) => Promise<void>) | null;
52
52
  createHandleResponseFn(callback: (...args: unknown[]) => unknown, isResponseHandled: boolean, redirectResponse?: RedirectResponse, httpStatusCode?: number): HandleResponseFn;
53
53
  private isResponseHandled;
54
+ private attachSseAbortSignal;
54
55
  }
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.RouterExecutionContext = void 0;
4
4
  const common_1 = require("@nestjs/common");
5
5
  const constants_1 = require("@nestjs/common/constants");
6
+ const sse_signal_decorator_1 = require("@nestjs/common/decorators/http/sse-signal.decorator");
6
7
  const route_paramtypes_enum_1 = require("@nestjs/common/enums/route-paramtypes.enum");
7
8
  const shared_utils_1 = require("@nestjs/common/utils/shared.utils");
8
9
  const guards_1 = require("../guards");
@@ -43,6 +44,13 @@ class RouterExecutionContext {
43
44
  this.responseController.setStatus(res, httpStatusCode);
44
45
  hasCustomHeaders &&
45
46
  this.responseController.setHeaders(res, responseHeaders);
47
+ if (isSseHandler) {
48
+ // Attach a per-request AbortController before the handler runs so async
49
+ // @Sse() handlers can observe client disconnects via @SseSignal() during
50
+ // their setup. The controller is aborted in RouterResponseController.sse()
51
+ // when the underlying connection closes.
52
+ this.attachSseAbortSignal(req);
53
+ }
46
54
  const resultOrDeferred = this.interceptorsConsumer.intercept(interceptors, [req, res, next], instance, callback, handler(args, req, res, next), contextType);
47
55
  const result = isSseHandler ? resultOrDeferred : await resultOrDeferred;
48
56
  await fnHandleResponse(result, res, req);
@@ -190,5 +198,18 @@ class RouterExecutionContext {
190
198
  const isPassthroughEnabled = this.contextUtils.reflectPassthrough(instance, methodName);
191
199
  return hasResponseOrNextDecorator && !isPassthroughEnabled;
192
200
  }
201
+ attachSseAbortSignal(req) {
202
+ const carrier = req;
203
+ // Attach to both the framework request and its raw form (when present), since
204
+ // @SseSignal() reads from the execution-context request while
205
+ // RouterResponseController.sse() operates on the raw request.
206
+ if (!carrier[sse_signal_decorator_1.SSE_ABORT_CONTROLLER]) {
207
+ carrier[sse_signal_decorator_1.SSE_ABORT_CONTROLLER] = new AbortController();
208
+ }
209
+ if (carrier.raw && !carrier.raw[sse_signal_decorator_1.SSE_ABORT_CONTROLLER]) {
210
+ carrier.raw[sse_signal_decorator_1.SSE_ABORT_CONTROLLER] =
211
+ carrier[sse_signal_decorator_1.SSE_ABORT_CONTROLLER];
212
+ }
213
+ }
193
214
  }
194
215
  exports.RouterExecutionContext = RouterExecutionContext;
@@ -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
  }
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.RouterResponseController = void 0;
4
4
  const common_1 = require("@nestjs/common");
5
+ const sse_signal_decorator_1 = require("@nestjs/common/decorators/http/sse-signal.decorator");
5
6
  const shared_utils_1 = require("@nestjs/common/utils/shared.utils");
6
7
  const rxjs_1 = require("rxjs");
7
8
  const operators_1 = require("rxjs/operators");
@@ -51,18 +52,38 @@ class RouterResponseController {
51
52
  async sse(result, response, request, options) {
52
53
  // It's possible that we sent headers already so don't use a stream
53
54
  if (response.writableEnded) {
55
+ // The response is already gone: abort the request-scoped signal so
56
+ // handler cleanup wired to @SseSignal() still runs, and swallow late
57
+ // handler rejections that can no longer be delivered to the client.
58
+ this.getOrCreateAbortController(request).abort();
59
+ Promise.resolve(result).catch((err) => this.logger.error(err));
54
60
  return;
55
61
  }
56
62
  const stream = new sse_stream_1.SseStream(request);
57
63
  const statusCode = options?.statusCode ??
58
64
  response.statusCode ??
59
65
  200;
66
+ // Create a per-request AbortController and expose its signal on the request
67
+ // object so async @Sse() handlers can observe client disconnects (via the
68
+ // @SseSignal() parameter decorator) and stop/clean up in-flight setup work.
69
+ // The controller is reused if one was already attached upstream (e.g. when the
70
+ // handler is wrapped by interceptors and the signal was created earlier).
71
+ const abortController = this.getOrCreateAbortController(request);
60
72
  return new Promise((resolve, reject) => {
61
73
  let settled = false;
62
74
  let closeRequested = false;
63
75
  let subscription;
64
76
  const disconnectSource = request.socket ?? response;
65
- const cleanup = () => disconnectSource.removeListener('close', onClose);
77
+ // Ends the request-scoped lifetime: stops listening for disconnects and
78
+ // aborts the signal handed to the route handler. Every terminal path of
79
+ // the SSE lifecycle (disconnect, completion, error) funnels through here,
80
+ // so a handler that ties its resources to the signal releases them once,
81
+ // regardless of how the stream ended. `abort()` is idempotent, so paths
82
+ // that already aborted on disconnect are unaffected.
83
+ const finalize = () => {
84
+ disconnectSource.removeListener('close', onClose);
85
+ abortController.abort();
86
+ };
66
87
  const endStream = () => {
67
88
  if (!stream.writableEnded) {
68
89
  stream.end();
@@ -74,11 +95,11 @@ class RouterResponseController {
74
95
  }
75
96
  closeRequested = true;
76
97
  if (!subscription) {
77
- cleanup();
98
+ finalize();
78
99
  return;
79
100
  }
80
101
  settled = true;
81
- cleanup();
102
+ finalize();
82
103
  subscription?.unsubscribe();
83
104
  endStream();
84
105
  response.end();
@@ -92,10 +113,10 @@ class RouterResponseController {
92
113
  }
93
114
  this.assertObservable(observableResult);
94
115
  if (closeRequested) {
95
- const cleanupSubscription = observableResult.subscribe({
96
- error: () => undefined,
97
- });
98
- cleanupSubscription.unsubscribe();
116
+ // The client disconnected while the async handler was resolving.
117
+ // Do not subscribe the producer Observable after the consumer has
118
+ // already gone away — subscribing only to abort it in the same tick
119
+ // would start producer side effects just to immediately cancel them.
99
120
  settled = true;
100
121
  endStream();
101
122
  response.end();
@@ -130,7 +151,7 @@ class RouterResponseController {
130
151
  return;
131
152
  }
132
153
  settled = true;
133
- cleanup();
154
+ finalize();
134
155
  endStream();
135
156
  reject(err);
136
157
  },
@@ -139,7 +160,7 @@ class RouterResponseController {
139
160
  return;
140
161
  }
141
162
  settled = true;
142
- cleanup();
163
+ finalize();
143
164
  endStream();
144
165
  resolve();
145
166
  },
@@ -167,7 +188,7 @@ class RouterResponseController {
167
188
  return;
168
189
  }
169
190
  settled = true;
170
- cleanup();
191
+ finalize();
171
192
  endStream();
172
193
  reject(err);
173
194
  });
@@ -178,5 +199,12 @@ class RouterResponseController {
178
199
  throw new ReferenceError('You must return an Observable stream to use Server-Sent Events (SSE).');
179
200
  }
180
201
  }
202
+ getOrCreateAbortController(request) {
203
+ const carrier = request;
204
+ if (!carrier[sse_signal_decorator_1.SSE_ABORT_CONTROLLER]) {
205
+ carrier[sse_signal_decorator_1.SSE_ABORT_CONTROLLER] = new AbortController();
206
+ }
207
+ return carrier[sse_signal_decorator_1.SSE_ABORT_CONTROLLER];
208
+ }
181
209
  }
182
210
  exports.RouterResponseController = RouterResponseController;
package/scanner.js CHANGED
@@ -40,6 +40,9 @@ 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 @@ 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);