@nestjs/websockets 12.1.1 → 12.1.3
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/errors/ws-exception.js
CHANGED
|
@@ -15,9 +15,9 @@ export class WsException extends Error {
|
|
|
15
15
|
this.message = this.error.message;
|
|
16
16
|
}
|
|
17
17
|
else if (this.constructor) {
|
|
18
|
-
this.message =
|
|
19
|
-
.match(/[A-Z][a-z]+|[0-9]+/g)
|
|
20
|
-
|
|
18
|
+
this.message =
|
|
19
|
+
this.constructor.name.match(/[A-Z][a-z]+|[0-9]+/g)?.join(' ') ??
|
|
20
|
+
'Error';
|
|
21
21
|
}
|
|
22
22
|
}
|
|
23
23
|
getError() {
|
|
@@ -10,9 +10,11 @@ export class BaseWsExceptionFilter {
|
|
|
10
10
|
static logger = new Logger('WsExceptionsHandler');
|
|
11
11
|
constructor(options = {}) {
|
|
12
12
|
this.options = options;
|
|
13
|
-
this.options
|
|
14
|
-
|
|
15
|
-
this.options.
|
|
13
|
+
this.options = {
|
|
14
|
+
...this.options,
|
|
15
|
+
includeCause: this.options.includeCause ?? true,
|
|
16
|
+
causeFactory: this.options.causeFactory ?? ((pattern, data) => ({ pattern, data })),
|
|
17
|
+
};
|
|
16
18
|
}
|
|
17
19
|
catch(exception, host) {
|
|
18
20
|
const client = host.switchToWs().getClient();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nestjs/websockets",
|
|
3
|
-
"version": "12.1.
|
|
3
|
+
"version": "12.1.3",
|
|
4
4
|
"description": "Nest - modern, fast, powerful node.js web framework (@websockets)",
|
|
5
5
|
"author": "Kamil Mysliwiec",
|
|
6
6
|
"license": "MIT",
|
|
@@ -26,8 +26,8 @@
|
|
|
26
26
|
"tslib": "2.8.1"
|
|
27
27
|
},
|
|
28
28
|
"devDependencies": {
|
|
29
|
-
"@nestjs/common": "12.1.
|
|
30
|
-
"@nestjs/core": "12.1.
|
|
29
|
+
"@nestjs/common": "12.1.3",
|
|
30
|
+
"@nestjs/core": "12.1.3"
|
|
31
31
|
},
|
|
32
32
|
"peerDependencies": {
|
|
33
33
|
"@nestjs/common": "^12.0.0",
|
|
@@ -41,5 +41,5 @@
|
|
|
41
41
|
"optional": true
|
|
42
42
|
}
|
|
43
43
|
},
|
|
44
|
-
"gitHead": "
|
|
44
|
+
"gitHead": "4514623279f6087808e3c8f08fbf9f37d3c17482"
|
|
45
45
|
}
|
|
@@ -22,6 +22,7 @@ export declare class WebSocketsController {
|
|
|
22
22
|
private readonly logger;
|
|
23
23
|
private readonly metadataExplorer;
|
|
24
24
|
private readonly exceptionFiltersCache;
|
|
25
|
+
private readonly pendingDisconnects;
|
|
25
26
|
constructor(socketServerProvider: SocketServerProvider, config: ApplicationConfig, contextCreator: WsContextCreator, container: NestContainer, injector: Injector, exceptionFiltersContext: ExceptionFiltersContext, graphInspector: GraphInspector, appOptions?: NestApplicationContextOptions);
|
|
26
27
|
connectGatewayToServer(instanceWrapper: InstanceWrapper<NestGateway>, moduleKey: string): void;
|
|
27
28
|
connectGatewayToServer(instance: NestGateway, metatype: Type<unknown> | Function, moduleKey: string, instanceWrapperId: string): void;
|
|
@@ -37,6 +38,15 @@ export declare class WebSocketsController {
|
|
|
37
38
|
inspectEntrypointDefinitions(instance: NestGateway, port: number, messageHandlers: MessageMappingProperties[], instanceWrapperId: string): void;
|
|
38
39
|
createRequestScopedHandler(instanceWrapper: InstanceWrapper<NestGateway>, moduleRef: Module, moduleKey: string, methodName: string): (...args: unknown[]) => Promise<void>;
|
|
39
40
|
createRequestScopedEventHandler(instanceWrapper: InstanceWrapper<NestGateway>, moduleRef: Module, moduleKey: string, methodName: 'handleConnection' | 'handleDisconnect', server: object): (...args: unknown[]) => Promise<unknown>;
|
|
41
|
+
private areGlobalScopedEnhancersDurable;
|
|
42
|
+
/**
|
|
43
|
+
* Runs a lifecycle hook of a default-scoped gateway and passes the errors it
|
|
44
|
+
* throws or rejects with to the gateway's exception filters, as for message
|
|
45
|
+
* handlers. The hook still runs synchronously.
|
|
46
|
+
*/
|
|
47
|
+
createStaticEventHandler(instance: NestGateway, moduleKey: string, methodName: 'handleConnection' | 'handleDisconnect'): ((...args: unknown[]) => unknown) | undefined;
|
|
48
|
+
private handleLifecycleHookError;
|
|
49
|
+
private getGlobalScopedEnhancers;
|
|
40
50
|
getContextId<T extends Record<any, unknown> = any>(request: T, isTreeDurable: boolean): ContextId;
|
|
41
51
|
private cleanupRequestScopedContext;
|
|
42
52
|
private assignServerToProperties;
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { from as fromPromise, isObservable, of, } from 'rxjs';
|
|
2
2
|
import { distinctUntilChanged, mergeAll } from 'rxjs/operators';
|
|
3
|
-
import { GATEWAY_OPTIONS, PORT_METADATA } from './constants.js';
|
|
3
|
+
import { GATEWAY_OPTIONS, MESSAGE_METADATA, PORT_METADATA, } from './constants.js';
|
|
4
4
|
import { InvalidSocketPortException } from './errors/invalid-socket-port.exception.js';
|
|
5
5
|
import { GatewayMetadataExplorer, } from './gateway-metadata-explorer.js';
|
|
6
6
|
import { compareElementAt } from './utils/compare-element.util.js';
|
|
7
|
+
import { isFunction } from '@nestjs/common/internal';
|
|
7
8
|
import { Logger } from '@nestjs/common';
|
|
8
9
|
import { ContextIdFactory, MetadataScanner, } from '@nestjs/core';
|
|
9
10
|
import { ExecutionContextHost, REQUEST_CONTEXT_ID, STATIC_CONTEXT, } from '@nestjs/core/internal';
|
|
@@ -21,6 +22,7 @@ export class WebSocketsController {
|
|
|
21
22
|
});
|
|
22
23
|
metadataExplorer = new GatewayMetadataExplorer(new MetadataScanner());
|
|
23
24
|
exceptionFiltersCache = new WeakMap();
|
|
25
|
+
pendingDisconnects = new WeakMap();
|
|
24
26
|
constructor(socketServerProvider, config, contextCreator, container, injector, exceptionFiltersContext, graphInspector, appOptions = {}) {
|
|
25
27
|
this.socketServerProvider = socketServerProvider;
|
|
26
28
|
this.config = config;
|
|
@@ -54,6 +56,7 @@ export class WebSocketsController {
|
|
|
54
56
|
id: targetInstanceWrapperId,
|
|
55
57
|
isDependencyTreeStatic: () => true,
|
|
56
58
|
isDependencyTreeDurable: () => false,
|
|
59
|
+
getEnhancersMetadata: () => [],
|
|
57
60
|
};
|
|
58
61
|
const gatewayMetatype = metatype ?? instance.constructor;
|
|
59
62
|
const options = Reflect.getMetadata(GATEWAY_OPTIONS, gatewayMetatype) || {};
|
|
@@ -72,15 +75,17 @@ export class WebSocketsController {
|
|
|
72
75
|
id: instanceWrapperId,
|
|
73
76
|
isDependencyTreeStatic: () => true,
|
|
74
77
|
isDependencyTreeDurable: () => false,
|
|
78
|
+
getEnhancersMetadata: () => [],
|
|
75
79
|
};
|
|
76
80
|
const { instance } = instanceWrapper;
|
|
77
81
|
const nativeMessageHandlers = this.metadataExplorer.explore(instance);
|
|
78
82
|
const isStatic = instanceWrapper.isDependencyTreeStatic();
|
|
83
|
+
const hasGlobalScopedEnhancers = this.getGlobalScopedEnhancers().length > 0;
|
|
79
84
|
const moduleRef = this.container.getModuleByKey(moduleKey);
|
|
80
85
|
const messageHandlers = nativeMessageHandlers.map(({ callback, isAckHandledManually, message, methodName }) => ({
|
|
81
86
|
message,
|
|
82
87
|
methodName,
|
|
83
|
-
callback: isStatic
|
|
88
|
+
callback: isStatic && !hasGlobalScopedEnhancers
|
|
84
89
|
? this.contextCreator.create(instance, callback, moduleKey, methodName, STATIC_CONTEXT)
|
|
85
90
|
: this.createRequestScopedHandler(instanceWrapper, moduleRef, moduleKey, methodName),
|
|
86
91
|
isAckHandledManually,
|
|
@@ -92,9 +97,9 @@ export class WebSocketsController {
|
|
|
92
97
|
const observableServer = this.socketServerProvider.scanForSocketServer(options, port);
|
|
93
98
|
this.assignServerToProperties(instance, observableServer.server);
|
|
94
99
|
this.subscribeEvents(instanceWrapper, messageHandlers, observableServer, isStatic
|
|
95
|
-
?
|
|
96
|
-
: this.createRequestScopedEventHandler(instanceWrapper, moduleRef, moduleKey, 'handleConnection', observableServer.server), isStatic
|
|
97
|
-
?
|
|
100
|
+
? this.createStaticEventHandler(instance, moduleKey, 'handleConnection')
|
|
101
|
+
: this.createRequestScopedEventHandler(instanceWrapper, moduleRef, moduleKey, 'handleConnection', observableServer.server), isStatic && !hasGlobalScopedEnhancers
|
|
102
|
+
? this.createStaticEventHandler(instance, moduleKey, 'handleDisconnect')
|
|
98
103
|
: this.createRequestScopedEventHandler(instanceWrapper, moduleRef, moduleKey, 'handleDisconnect', observableServer.server));
|
|
99
104
|
}
|
|
100
105
|
subscribeEvents(instanceWrapper, subscribersMap, observableServer, connectionHandler, disconnectHandler) {
|
|
@@ -193,12 +198,25 @@ export class WebSocketsController {
|
|
|
193
198
|
createRequestScopedHandler(instanceWrapper, moduleRef, moduleKey, methodName) {
|
|
194
199
|
const { instance } = instanceWrapper;
|
|
195
200
|
const collection = moduleRef.providers;
|
|
196
|
-
const
|
|
201
|
+
const isStatic = instanceWrapper.isDependencyTreeStatic();
|
|
202
|
+
const isTreeDurable = isStatic
|
|
203
|
+
? this.areGlobalScopedEnhancersDurable()
|
|
204
|
+
: instanceWrapper.isDependencyTreeDurable();
|
|
205
|
+
const scopedEnhancers = [
|
|
206
|
+
...(isStatic ? instanceWrapper.getEnhancersMetadata() || [] : []),
|
|
207
|
+
...this.getGlobalScopedEnhancers(),
|
|
208
|
+
];
|
|
209
|
+
const pattern = Reflect.getMetadata(MESSAGE_METADATA, instance[methodName]) ?? 'unknown';
|
|
197
210
|
return async (...args) => {
|
|
198
211
|
const [client] = args;
|
|
199
212
|
try {
|
|
200
213
|
const contextId = this.getContextId(client, isTreeDurable);
|
|
201
|
-
const contextInstance =
|
|
214
|
+
const contextInstance = isStatic
|
|
215
|
+
? instance
|
|
216
|
+
: await this.injector.loadPerContext(instance, moduleRef, collection, contextId);
|
|
217
|
+
// Resolve global enhancers without adding them to the gateway's dependency
|
|
218
|
+
// tree, so a singleton gateway keeps its state and lifecycle hooks.
|
|
219
|
+
await this.injector.loadEnhancersPerContext(instanceWrapper, contextId, instanceWrapper, scopedEnhancers);
|
|
202
220
|
return this.contextCreator.create(contextInstance, contextInstance[methodName], moduleKey, methodName, contextId, instanceWrapper.id)(...args);
|
|
203
221
|
}
|
|
204
222
|
catch (err) {
|
|
@@ -207,7 +225,9 @@ export class WebSocketsController {
|
|
|
207
225
|
exceptionFilter = this.exceptionFiltersContext.create(instance, instance[methodName], moduleKey);
|
|
208
226
|
this.exceptionFiltersCache.set(instance[methodName], exceptionFilter);
|
|
209
227
|
}
|
|
210
|
-
|
|
228
|
+
// The context failed to resolve before `WsProxy` could append the
|
|
229
|
+
// pattern, so add it here the same way.
|
|
230
|
+
const host = new ExecutionContextHost([...args, pattern]);
|
|
211
231
|
host.setType('ws');
|
|
212
232
|
exceptionFilter.handle(err, host);
|
|
213
233
|
}
|
|
@@ -216,30 +236,37 @@ export class WebSocketsController {
|
|
|
216
236
|
createRequestScopedEventHandler(instanceWrapper, moduleRef, moduleKey, methodName, server) {
|
|
217
237
|
const { instance } = instanceWrapper;
|
|
218
238
|
const collection = moduleRef.providers;
|
|
219
|
-
const
|
|
239
|
+
const isStatic = instanceWrapper.isDependencyTreeStatic();
|
|
240
|
+
const isTreeDurable = isStatic
|
|
241
|
+
? this.areGlobalScopedEnhancersDurable()
|
|
242
|
+
: instanceWrapper.isDependencyTreeDurable();
|
|
220
243
|
const targetCallback = instance[methodName];
|
|
221
244
|
return async (...args) => {
|
|
222
245
|
const [client] = args;
|
|
223
246
|
let contextId;
|
|
224
247
|
try {
|
|
225
248
|
contextId = this.getContextId(client, isTreeDurable);
|
|
226
|
-
|
|
227
|
-
|
|
249
|
+
if (methodName === 'handleDisconnect') {
|
|
250
|
+
this.pendingDisconnects.set(contextId, (this.pendingDisconnects.get(contextId) ?? 0) + 1);
|
|
251
|
+
}
|
|
252
|
+
const contextInstance = isStatic
|
|
253
|
+
? instance
|
|
254
|
+
: await this.injector.loadPerContext(instance, moduleRef, collection, contextId);
|
|
255
|
+
if (!isStatic) {
|
|
256
|
+
this.assignServerToProperties(contextInstance, server);
|
|
257
|
+
}
|
|
228
258
|
const scopedMethod = contextInstance[methodName];
|
|
229
|
-
return scopedMethod?.apply(contextInstance, args);
|
|
259
|
+
return await scopedMethod?.apply(contextInstance, args);
|
|
230
260
|
}
|
|
231
261
|
catch (err) {
|
|
232
262
|
if (!targetCallback) {
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
this.exceptionFiltersCache.set(targetCallback, exceptionFilter);
|
|
263
|
+
// The gateway failed to resolve and has no hook whose exception
|
|
264
|
+
// filters could handle the error. Nothing awaits this handler, so
|
|
265
|
+
// rethrowing would end as an unhandled rejection.
|
|
266
|
+
this.logger.error(`Failed to resolve ${instance.constructor.name} on client ${methodName === 'handleConnection' ? 'connection' : 'disconnection'}`, err?.stack);
|
|
267
|
+
return;
|
|
239
268
|
}
|
|
240
|
-
|
|
241
|
-
host.setType('ws');
|
|
242
|
-
exceptionFilter.handle(err, host);
|
|
269
|
+
this.handleLifecycleHookError(instance, moduleKey, methodName, args, err);
|
|
243
270
|
}
|
|
244
271
|
finally {
|
|
245
272
|
if (methodName === 'handleDisconnect' && contextId) {
|
|
@@ -248,6 +275,63 @@ export class WebSocketsController {
|
|
|
248
275
|
}
|
|
249
276
|
};
|
|
250
277
|
}
|
|
278
|
+
// A default-scoped gateway is resolved per connection only for its global
|
|
279
|
+
// scoped enhancers, so their trees decide whether the context is durable
|
|
280
|
+
// (mirrors InstanceWrapper#isDependencyTreeDurable).
|
|
281
|
+
areGlobalScopedEnhancersDurable() {
|
|
282
|
+
const scopedEnhancers = this.getGlobalScopedEnhancers().filter(wrapper => !wrapper.isDependencyTreeStatic());
|
|
283
|
+
return (scopedEnhancers.length > 0 &&
|
|
284
|
+
scopedEnhancers.every(wrapper => wrapper.isDependencyTreeDurable()));
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Runs a lifecycle hook of a default-scoped gateway and passes the errors it
|
|
288
|
+
* throws or rejects with to the gateway's exception filters, as for message
|
|
289
|
+
* handlers. The hook still runs synchronously.
|
|
290
|
+
*/
|
|
291
|
+
createStaticEventHandler(instance, moduleKey, methodName) {
|
|
292
|
+
const hook = instance[methodName];
|
|
293
|
+
if (!hook) {
|
|
294
|
+
return undefined;
|
|
295
|
+
}
|
|
296
|
+
return (...args) => {
|
|
297
|
+
const handleError = (err) => this.handleLifecycleHookError(instance, moduleKey, methodName, args, err);
|
|
298
|
+
try {
|
|
299
|
+
const result = hook.apply(instance, args);
|
|
300
|
+
return isFunction(result?.then)
|
|
301
|
+
? result.catch(handleError)
|
|
302
|
+
: result;
|
|
303
|
+
}
|
|
304
|
+
catch (err) {
|
|
305
|
+
handleError(err);
|
|
306
|
+
}
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
handleLifecycleHookError(instance, moduleKey, methodName, args, err) {
|
|
310
|
+
// Only called for gateways that implement the hook
|
|
311
|
+
const targetCallback = instance[methodName];
|
|
312
|
+
let exceptionFilter = this.exceptionFiltersCache.get(targetCallback);
|
|
313
|
+
if (!exceptionFilter) {
|
|
314
|
+
exceptionFilter = this.exceptionFiltersContext.create(instance, targetCallback, moduleKey);
|
|
315
|
+
this.exceptionFiltersCache.set(targetCallback, exceptionFilter);
|
|
316
|
+
}
|
|
317
|
+
const [client, disconnectReason] = args;
|
|
318
|
+
// Client first, data second and the pattern last, as for message handlers.
|
|
319
|
+
// With the raw hook arguments, the filter would send the last one (the
|
|
320
|
+
// socket itself with socket.io, the upgrade request with ws) back to the
|
|
321
|
+
// client as the pattern. The disconnect reason is safe to pass as data,
|
|
322
|
+
// as nothing is sent to a client that has already disconnected.
|
|
323
|
+
const data = methodName === 'handleDisconnect' ? disconnectReason : undefined;
|
|
324
|
+
const host = new ExecutionContextHost([client, data, methodName]);
|
|
325
|
+
host.setType('ws');
|
|
326
|
+
exceptionFilter.handle(err, host);
|
|
327
|
+
}
|
|
328
|
+
getGlobalScopedEnhancers() {
|
|
329
|
+
return [
|
|
330
|
+
...this.config.getGlobalRequestGuards(),
|
|
331
|
+
...this.config.getGlobalRequestPipes(),
|
|
332
|
+
...this.config.getGlobalRequestInterceptors(),
|
|
333
|
+
];
|
|
334
|
+
}
|
|
251
335
|
getContextId(request, isTreeDurable) {
|
|
252
336
|
const contextId = ContextIdFactory.getByRequest(request);
|
|
253
337
|
if (!request[REQUEST_CONTEXT_ID]) {
|
|
@@ -264,8 +348,18 @@ export class WebSocketsController {
|
|
|
264
348
|
}
|
|
265
349
|
return contextId;
|
|
266
350
|
}
|
|
267
|
-
cleanupRequestScopedContext(_instanceWrapper,
|
|
268
|
-
|
|
351
|
+
cleanupRequestScopedContext(_instanceWrapper, contextId, request) {
|
|
352
|
+
const remaining = (this.pendingDisconnects.get(contextId) ?? 1) - 1;
|
|
353
|
+
if (remaining > 0) {
|
|
354
|
+
this.pendingDisconnects.set(contextId, remaining);
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
this.pendingDisconnects.delete(contextId);
|
|
358
|
+
// Other gateways can still be executing their asynchronous disconnect hooks.
|
|
359
|
+
// A finished cycle must also never remove a newer context on the same client.
|
|
360
|
+
if (request[REQUEST_CONTEXT_ID] === contextId) {
|
|
361
|
+
Reflect.deleteProperty(request, REQUEST_CONTEXT_ID);
|
|
362
|
+
}
|
|
269
363
|
}
|
|
270
364
|
assignServerToProperties(instance, server) {
|
|
271
365
|
for (const propertyKey of this.metadataExplorer.scanForServerHooks(instance)) {
|