@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 +25 -46
- package/adapters/http-adapter.d.ts +2 -0
- package/adapters/http-adapter.js +3 -0
- package/exceptions/base-exception-filter.js +8 -3
- package/helpers/barrier.js +4 -1
- package/helpers/handler-metadata-storage.d.ts +1 -0
- package/helpers/router-method-factory.d.ts +1 -0
- package/helpers/router-method-factory.js +1 -0
- package/hooks/before-app-shutdown.hook.js +11 -2
- package/hooks/on-app-shutdown.hook.js +11 -2
- package/hooks/on-module-destroy.hook.js +11 -2
- package/injector/container.js +2 -2
- package/injector/injector.js +6 -10
- package/injector/internal-core-module/internal-core-module-factory.js +1 -1
- package/injector/module.d.ts +6 -0
- package/injector/module.js +8 -0
- package/interceptors/interceptors-consumer.js +32 -7
- package/middleware/builder.js +5 -1
- package/nest-application-context.js +1 -1
- package/nest-application.d.ts +2 -1
- package/nest-application.js +19 -5
- package/package.json +3 -14
- package/router/legacy-route-converter.d.ts +1 -1
- package/router/legacy-route-converter.js +24 -13
- package/router/router-execution-context.d.ts +1 -0
- package/router/router-execution-context.js +26 -3
- package/router/router-response-controller.d.ts +1 -0
- package/router/router-response-controller.js +126 -49
- package/router/sse-stream.d.ts +1 -0
- package/router/sse-stream.js +24 -13
- package/scanner.js +4 -1
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/
|
|
64
|
-
<td><a href="https://mojam.co" target="_blank"><img src="https://
|
|
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/
|
|
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/
|
|
75
|
-
<td><a href="https://github.com/Sanofi-IADC" target="_blank"><img src="https://docs.nestjs.com/assets/sponsors/sanofi.png" width="
|
|
76
|
-
<td><a href="https://nx.dev" target="_blank"><img src="https://nestjs.com/img/
|
|
77
|
-
<td><a href="https://intrinsic.ventures/" target="_blank"><img src="https://nestjs.com/img/
|
|
78
|
-
<td><a href="https://jetbrains.com/" target="_blank"><img src="https://nestjs.com/img/
|
|
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/
|
|
82
|
-
<td><a href="https://www.movavi.com/imovie-for-windows.html" target="_blank"><img src="https://
|
|
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/
|
|
91
|
-
<td><a href="https://
|
|
92
|
-
<td><a href="https://
|
|
93
|
-
<td align="center" valign="middle"><a href="https://
|
|
94
|
-
<td align="center" valign="middle"><a href="https://
|
|
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://
|
|
98
|
-
<td align="center" valign="middle"><a href="https://
|
|
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://
|
|
107
|
-
<td align="center" valign="middle"><a href="https://
|
|
108
|
-
<td align="center" valign="middle"><a href="https://
|
|
109
|
-
<td align="center" valign="middle"><a href="https://
|
|
110
|
-
<td align="center" valign="middle"><a href="https://
|
|
111
|
-
<td align="center" valign="middle"><a href="https://
|
|
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;
|
package/adapters/http-adapter.js
CHANGED
|
@@ -63,16 +63,21 @@ export class BaseExceptionFilter {
|
|
|
63
63
|
if (!err || typeof err !== 'object') {
|
|
64
64
|
return false;
|
|
65
65
|
}
|
|
66
|
-
if (err.constructor
|
|
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
|
-
|
|
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([
|
package/helpers/barrier.js
CHANGED
|
@@ -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
|
|
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[];
|
|
@@ -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.
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
48
|
+
try {
|
|
49
|
+
await moduleClassInstance.onModuleDestroy();
|
|
50
|
+
}
|
|
51
|
+
catch (err) {
|
|
52
|
+
Logger.error(err, err?.stack);
|
|
53
|
+
}
|
|
45
54
|
}
|
|
46
55
|
}
|
package/injector/container.js
CHANGED
|
@@ -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:
|
|
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:
|
|
95
|
+
inserted: true,
|
|
96
96
|
};
|
|
97
97
|
}
|
|
98
98
|
async setModule({ token, dynamicMetadata, type }, scope) {
|
package/injector/injector.js
CHANGED
|
@@ -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
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
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);
|
package/injector/module.d.ts
CHANGED
|
@@ -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);
|
package/injector/module.js
CHANGED
|
@@ -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
|
|
3
|
-
import { mergeAll
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
}
|
package/middleware/builder.js
CHANGED
|
@@ -74,7 +74,11 @@ export class MiddlewareBuilder {
|
|
|
74
74
|
.map(route => ({
|
|
75
75
|
method: route.method,
|
|
76
76
|
path: route.path,
|
|
77
|
-
|
|
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 {
|
package/nest-application.d.ts
CHANGED
|
@@ -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
|
}
|
package/nest-application.js
CHANGED
|
@@ -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.
|
|
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.
|
|
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": "
|
|
60
|
+
"gitHead": "4c38a5ab100f1d2a0d50602998968a25509525c7"
|
|
72
61
|
}
|
|
@@ -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
|
|
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
|
|
37
|
+
return convertedRoute;
|
|
36
38
|
}
|
|
37
39
|
if (normalizedRoute.endsWith('/+/')) {
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
44
|
-
//
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|
|
@@ -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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
105
|
+
disconnectSource.once('close', onClose);
|
|
106
|
+
Promise.resolve(result)
|
|
107
|
+
.then(observableResult => {
|
|
108
|
+
if (settled) {
|
|
109
|
+
return;
|
|
79
110
|
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
if (!
|
|
98
|
-
stream.
|
|
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
|
-
|
|
101
|
-
|
|
102
|
-
|
|
174
|
+
}, 0);
|
|
175
|
+
})
|
|
176
|
+
.catch(err => {
|
|
177
|
+
if (settled) {
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
if (closeRequested) {
|
|
103
181
|
settled = true;
|
|
104
|
-
|
|
105
|
-
|
|
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
|
-
|
|
121
|
-
|
|
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
|
}
|
package/router/sse-stream.d.ts
CHANGED
|
@@ -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.
|
package/router/sse-stream.js
CHANGED
|
@@ -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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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/
|
|
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
|
-
|
|
87
|
-
|
|
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
|
|
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
|
|
87
|
+
if (lazy) {
|
|
85
88
|
this.container.bindGlobalsToImports(moduleInstance);
|
|
86
89
|
}
|
|
87
90
|
return [moduleInstance].concat(registeredModuleRefs);
|