@nestjs/core 12.0.0-alpha.5 → 12.0.0-alpha.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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/helpers/safe-instance-decorator.d.ts +10 -0
- package/helpers/safe-instance-decorator.js +20 -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 +20 -12
- package/injector/internal-core-module/internal-core-module-factory.js +1 -1
- package/injector/module.d.ts +6 -0
- package/injector/module.js +12 -1
- package/interceptors/interceptors-consumer.js +32 -7
- package/internal.d.ts +10 -6
- package/internal.js +10 -6
- package/middleware/builder.js +5 -1
- package/nest-application-context.js +1 -1
- package/nest-application.d.ts +2 -1
- package/nest-application.js +28 -6
- 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/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
|
@@ -4,6 +4,7 @@ import { platform } from 'os';
|
|
|
4
4
|
import { ApplicationConfig } from './application-config.js';
|
|
5
5
|
import { MESSAGES } from './constants.js';
|
|
6
6
|
import { optionalRequire } from './helpers/optional-require.js';
|
|
7
|
+
import { makeSafeInstanceDecorator } from './helpers/safe-instance-decorator.js';
|
|
7
8
|
import { Injector } from './injector/injector.js';
|
|
8
9
|
import { MiddlewareContainer } from './middleware/container.js';
|
|
9
10
|
import { MiddlewareModule } from './middleware/middleware-module.js';
|
|
@@ -85,18 +86,18 @@ export class NestApplication extends NestApplicationContext {
|
|
|
85
86
|
return this.httpAdapter.getHttpServer();
|
|
86
87
|
}
|
|
87
88
|
async registerModules() {
|
|
88
|
-
this.registerWsModule();
|
|
89
|
+
await this.registerWsModule();
|
|
89
90
|
if (this.microservicesModule) {
|
|
90
91
|
this.microservicesModule.register(this.container, this.graphInspector, this.config, this.appOptions);
|
|
91
92
|
this.microservicesModule.setupClients(this.container);
|
|
92
93
|
}
|
|
93
94
|
await this.middlewareModule.register(this.middlewareContainer, this.container, this.config, this.injector, this.httpAdapter, this.graphInspector, this.appOptions);
|
|
94
95
|
}
|
|
95
|
-
registerWsModule() {
|
|
96
|
+
async registerWsModule() {
|
|
96
97
|
if (!this.socketModule) {
|
|
97
98
|
return;
|
|
98
99
|
}
|
|
99
|
-
this.socketModule.register(this.container, this.config, this.graphInspector, this.appOptions, this.httpServer);
|
|
100
|
+
await this.socketModule.register(this.container, this.config, this.graphInspector, this.appOptions, this.httpServer);
|
|
100
101
|
this.isWsModuleRegistered = true;
|
|
101
102
|
}
|
|
102
103
|
async init() {
|
|
@@ -204,7 +205,7 @@ export class NestApplication extends NestApplicationContext {
|
|
|
204
205
|
this.routesResolver.registerExceptionHandler();
|
|
205
206
|
}
|
|
206
207
|
connectMicroservice(microserviceOptions, hybridAppOptions = {}) {
|
|
207
|
-
const { NestMicroservice } = loadPackageCached('@nestjs/microservices');
|
|
208
|
+
const { NestMicroservice } = loadPackageCached('@nestjs/microservices', 'NestFactory');
|
|
208
209
|
const { inheritAppConfig } = hybridAppOptions;
|
|
209
210
|
const applicationConfig = inheritAppConfig
|
|
210
211
|
? this.config
|
|
@@ -230,7 +231,7 @@ export class NestApplication extends NestApplicationContext {
|
|
|
230
231
|
return this;
|
|
231
232
|
}
|
|
232
233
|
use(...args) {
|
|
233
|
-
this.httpAdapter.use(...args);
|
|
234
|
+
this.httpAdapter.use(...this.applyFunctionDecoratorIfRegistered(args));
|
|
234
235
|
return this;
|
|
235
236
|
}
|
|
236
237
|
useBodyParser(...args) {
|
|
@@ -411,10 +412,31 @@ export class NestApplication extends NestApplicationContext {
|
|
|
411
412
|
}
|
|
412
413
|
applyInstanceDecoratorIfRegistered(...instances) {
|
|
413
414
|
if (this.appOptions.instrument?.instanceDecorator) {
|
|
414
|
-
|
|
415
|
+
const decorate = makeSafeInstanceDecorator(this.appOptions.instrument.instanceDecorator);
|
|
416
|
+
return instances.map(instance => decorate(instance));
|
|
415
417
|
}
|
|
416
418
|
return instances;
|
|
417
419
|
}
|
|
420
|
+
applyFunctionDecoratorIfRegistered(args) {
|
|
421
|
+
if (!this.appOptions.instrument?.instanceDecorator) {
|
|
422
|
+
return args;
|
|
423
|
+
}
|
|
424
|
+
const decorate = makeSafeInstanceDecorator(this.appOptions.instrument.instanceDecorator);
|
|
425
|
+
// Decorators may return a non-function value for plain middleware
|
|
426
|
+
// functions; fall back to the original argument so the HTTP adapter
|
|
427
|
+
// always receives a valid handler.
|
|
428
|
+
const decorateFunction = (arg) => {
|
|
429
|
+
if (!isFunction(arg)) {
|
|
430
|
+
return arg;
|
|
431
|
+
}
|
|
432
|
+
const decorated = decorate(arg);
|
|
433
|
+
return isFunction(decorated) ? decorated : arg;
|
|
434
|
+
};
|
|
435
|
+
// Map over the original arguments to preserve arity: appending a trailing
|
|
436
|
+
// `undefined` to a single-argument `use(fn)` call would make Express 5's
|
|
437
|
+
// router throw "argument handler must be a function".
|
|
438
|
+
return args.map(decorateFunction);
|
|
439
|
+
}
|
|
418
440
|
async loadSocketModule() {
|
|
419
441
|
if (!this.socketModule) {
|
|
420
442
|
const socketModule = await optionalRequire('@nestjs/websockets/socket-module', () => import('@nestjs/websockets/socket-module.js'));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nestjs/core",
|
|
3
|
-
"version": "12.0.0-alpha.
|
|
3
|
+
"version": "12.0.0-alpha.7",
|
|
4
4
|
"description": "Nest - modern, fast, powerful node.js web framework (@core)",
|
|
5
5
|
"author": "Kamil Mysliwiec",
|
|
6
6
|
"license": "MIT",
|
|
@@ -28,18 +28,7 @@
|
|
|
28
28
|
"publishConfig": {
|
|
29
29
|
"access": "public"
|
|
30
30
|
},
|
|
31
|
-
"scripts": {
|
|
32
|
-
"postinstall": "opencollective || exit 0"
|
|
33
|
-
},
|
|
34
|
-
"collective": {
|
|
35
|
-
"type": "opencollective",
|
|
36
|
-
"url": "https://opencollective.com/nest",
|
|
37
|
-
"donation": {
|
|
38
|
-
"text": "Become a partner:"
|
|
39
|
-
}
|
|
40
|
-
},
|
|
41
31
|
"dependencies": {
|
|
42
|
-
"@nuxt/opencollective": "0.4.1",
|
|
43
32
|
"fast-safe-stringify": "2.1.1",
|
|
44
33
|
"iterare": "1.2.1",
|
|
45
34
|
"path-to-regexp": "8.4.2",
|
|
@@ -47,7 +36,7 @@
|
|
|
47
36
|
"uid": "2.0.2"
|
|
48
37
|
},
|
|
49
38
|
"devDependencies": {
|
|
50
|
-
"@nestjs/common": "^12.0.0-alpha.
|
|
39
|
+
"@nestjs/common": "^12.0.0-alpha.7"
|
|
51
40
|
},
|
|
52
41
|
"peerDependencies": {
|
|
53
42
|
"@nestjs/common": "^11.0.0",
|
|
@@ -68,5 +57,5 @@
|
|
|
68
57
|
"optional": true
|
|
69
58
|
}
|
|
70
59
|
},
|
|
71
|
-
"gitHead": "
|
|
60
|
+
"gitHead": "66e89ffc3d14e47d572bd6ea3f8714fa6916160b"
|
|
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);
|