@nestjs/platform-ws 12.1.3 → 12.2.0
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/adapters/ws-adapter.d.ts +25 -0
- package/adapters/ws-adapter.js +95 -7
- package/package.json +3 -2
package/adapters/ws-adapter.d.ts
CHANGED
|
@@ -11,6 +11,20 @@ type WsMessageParser = (data: WsData) => {
|
|
|
11
11
|
} | void;
|
|
12
12
|
type WsAdapterOptions = {
|
|
13
13
|
messageParser?: WsMessageParser;
|
|
14
|
+
/**
|
|
15
|
+
* How many messages a client may have handled at the same time. Later ones
|
|
16
|
+
* wait for their turn. Set it to `Infinity` to lift the limit.
|
|
17
|
+
*
|
|
18
|
+
* @default 1000
|
|
19
|
+
*/
|
|
20
|
+
maxConcurrentMessages?: number;
|
|
21
|
+
/**
|
|
22
|
+
* How many bytes may be queued for a client that does not read what it is
|
|
23
|
+
* sent before it is disconnected. Set it to `0` to lift the limit.
|
|
24
|
+
*
|
|
25
|
+
* @default 134217728 (128 MB)
|
|
26
|
+
*/
|
|
27
|
+
maxSendBufferSize?: number;
|
|
14
28
|
};
|
|
15
29
|
/**
|
|
16
30
|
* @publicApi
|
|
@@ -21,6 +35,8 @@ export declare class WsAdapter extends AbstractWsAdapter {
|
|
|
21
35
|
protected readonly wsServersRegistry: Map<number, WsServerRegistryEntry>;
|
|
22
36
|
private readonly upgradeListenersRegistry;
|
|
23
37
|
protected messageParser: WsMessageParser;
|
|
38
|
+
protected readonly maxConcurrentMessages: number;
|
|
39
|
+
protected readonly maxSendBufferSize: number;
|
|
24
40
|
constructor(appOrHttpServer?: INestApplicationContext | object, options?: WsAdapterOptions);
|
|
25
41
|
create(port: number, options?: Record<string, any> & {
|
|
26
42
|
namespace?: string;
|
|
@@ -29,6 +45,15 @@ export declare class WsAdapter extends AbstractWsAdapter {
|
|
|
29
45
|
}): any;
|
|
30
46
|
bindMessageHandlers(client: any, handlers: MessageMappingProperties[], transform: (data: any) => Observable<any>): void;
|
|
31
47
|
bindMessageHandler(buffer: any, handlersMap: Map<string, MessageMappingProperties>, transform: (data: any) => Observable<any>): Observable<any>;
|
|
48
|
+
/**
|
|
49
|
+
* Not reading from a client keeps it from asking for more, but what is
|
|
50
|
+
* already on its way to it still queues up: the replies to the messages
|
|
51
|
+
* being handled, values of Observables returned by handlers, and what
|
|
52
|
+
* exception filters send. This caps how much may be held for one client,
|
|
53
|
+
* like "maxSendBufferSize" of the TCP transport. Returns `false` once the
|
|
54
|
+
* client has been closed for exceeding it.
|
|
55
|
+
*/
|
|
56
|
+
protected ensureSendBufferWithinLimit(client: any): boolean;
|
|
32
57
|
bindErrorHandler(server: any): any;
|
|
33
58
|
bindClientDisconnect(client: any, callback: Function): void;
|
|
34
59
|
close(server: any): Promise<void>;
|
package/adapters/ws-adapter.js
CHANGED
|
@@ -3,9 +3,9 @@ import { AbstractWsAdapter } from '@nestjs/websockets';
|
|
|
3
3
|
import * as http from 'http';
|
|
4
4
|
import { createRequire } from 'module';
|
|
5
5
|
import { EMPTY, fromEvent } from 'rxjs';
|
|
6
|
-
import { catchError, filter, first,
|
|
6
|
+
import { catchError, filter, first, share, takeUntil } from 'rxjs/operators';
|
|
7
7
|
import { loadPackageSync, isNil, normalizePath } from '@nestjs/common/internal';
|
|
8
|
-
import { CLOSE_EVENT, CONNECTION_EVENT, ERROR_EVENT, } from '@nestjs/websockets/internal';
|
|
8
|
+
import { CLOSE_EVENT, CONNECTION_EVENT, DEFAULT_MAX_CONCURRENT_MESSAGES, ERROR_EVENT, mergeMapBounded, } from '@nestjs/websockets/internal';
|
|
9
9
|
let wsPackage = {};
|
|
10
10
|
var READY_STATE;
|
|
11
11
|
(function (READY_STATE) {
|
|
@@ -15,11 +15,25 @@ var READY_STATE;
|
|
|
15
15
|
READY_STATE[READY_STATE["CLOSED_STATE"] = 3] = "CLOSED_STATE";
|
|
16
16
|
})(READY_STATE || (READY_STATE = {}));
|
|
17
17
|
const UNDERLYING_HTTP_SERVER_PORT = 0;
|
|
18
|
+
// Reading from a client stops while more than this is queued for it
|
|
19
|
+
const SEND_BUFFER_HIGH_WATER_MARK = 1024 * 1024;
|
|
20
|
+
const DEFAULT_MAX_SEND_BUFFER_SIZE = 128 * 1024 * 1024;
|
|
18
21
|
// Kept out of the class so that an adapter can be compared against it to tell
|
|
19
22
|
// whether the parser in use is still the built-in one.
|
|
20
23
|
const defaultMessageParser = (data) => {
|
|
21
24
|
return JSON.parse(data.toString());
|
|
22
25
|
};
|
|
26
|
+
// What ws answers on the servers it creates itself. A request left without an
|
|
27
|
+
// answer keeps its connection open, and Node goes on queueing the requests
|
|
28
|
+
// pipelined behind it until the heap runs out.
|
|
29
|
+
const rejectNonUpgradeRequest = (_, res) => {
|
|
30
|
+
const body = http.STATUS_CODES[426];
|
|
31
|
+
res.writeHead(426, {
|
|
32
|
+
'Content-Length': body.length,
|
|
33
|
+
'Content-Type': 'text/plain',
|
|
34
|
+
});
|
|
35
|
+
res.end(body);
|
|
36
|
+
};
|
|
23
37
|
/**
|
|
24
38
|
* @publicApi
|
|
25
39
|
*/
|
|
@@ -29,6 +43,8 @@ export class WsAdapter extends AbstractWsAdapter {
|
|
|
29
43
|
wsServersRegistry = new Map();
|
|
30
44
|
upgradeListenersRegistry = new Map();
|
|
31
45
|
messageParser = defaultMessageParser;
|
|
46
|
+
maxConcurrentMessages;
|
|
47
|
+
maxSendBufferSize;
|
|
32
48
|
constructor(appOrHttpServer, options) {
|
|
33
49
|
super(appOrHttpServer);
|
|
34
50
|
wsPackage = loadPackageSync('ws', 'WsAdapter', () => createRequire(import.meta.url)('ws'));
|
|
@@ -40,6 +56,10 @@ export class WsAdapter extends AbstractWsAdapter {
|
|
|
40
56
|
if (options?.messageParser) {
|
|
41
57
|
this.messageParser = options.messageParser;
|
|
42
58
|
}
|
|
59
|
+
this.maxConcurrentMessages =
|
|
60
|
+
options?.maxConcurrentMessages ?? DEFAULT_MAX_CONCURRENT_MESSAGES;
|
|
61
|
+
this.maxSendBufferSize =
|
|
62
|
+
options?.maxSendBufferSize ?? DEFAULT_MAX_SEND_BUFFER_SIZE;
|
|
43
63
|
}
|
|
44
64
|
create(port, options) {
|
|
45
65
|
const { server, path, ...wsOptions } = (options ?? {});
|
|
@@ -65,7 +85,9 @@ export class WsAdapter extends AbstractWsAdapter {
|
|
|
65
85
|
// sharing a single HTTP/S server running on different port
|
|
66
86
|
// than a regular HTTP application
|
|
67
87
|
const httpServer = this.ensureHttpServerExists(port);
|
|
68
|
-
|
|
88
|
+
// ws listens with "host" and "backlog" only on a server it creates
|
|
89
|
+
// itself, and the one below has none (noServer)
|
|
90
|
+
httpServer?.listen(port, wsOptions.host, wsOptions.backlog);
|
|
69
91
|
const wsServer = this.bindErrorHandler(new wsPackage.Server({
|
|
70
92
|
noServer: true,
|
|
71
93
|
...wsOptions,
|
|
@@ -83,15 +105,47 @@ export class WsAdapter extends AbstractWsAdapter {
|
|
|
83
105
|
bindMessageHandlers(client, handlers, transform) {
|
|
84
106
|
const handlersMap = new Map();
|
|
85
107
|
handlers.forEach(handler => handlersMap.set(handler.message, handler));
|
|
108
|
+
// ws reads everything a client sends, whether or not its messages can be
|
|
109
|
+
// handled yet and whether or not it reads the replies. Reading stops while
|
|
110
|
+
// messages wait for their turn, or while replies the client has not read
|
|
111
|
+
// pile up, and resumes once it has caught up.
|
|
112
|
+
let isBacklogged = false;
|
|
113
|
+
let pendingReplies = 0;
|
|
114
|
+
let isPaused = false;
|
|
115
|
+
const updatePaused = () => {
|
|
116
|
+
const shouldPause = isBacklogged ||
|
|
117
|
+
(pendingReplies > 0 &&
|
|
118
|
+
client.bufferedAmount > SEND_BUFFER_HIGH_WATER_MARK);
|
|
119
|
+
if (shouldPause === isPaused) {
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
isPaused = shouldPause;
|
|
123
|
+
if (isPaused) {
|
|
124
|
+
client.pause?.();
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
client.resume?.();
|
|
128
|
+
}
|
|
129
|
+
};
|
|
86
130
|
const close$ = fromEvent(client, CLOSE_EVENT).pipe(share(), first());
|
|
87
|
-
const source$ = fromEvent(client, 'message').pipe(
|
|
131
|
+
const source$ = fromEvent(client, 'message').pipe(
|
|
132
|
+
// ws still emits what a client sent before it got the close frame, e.g.
|
|
133
|
+
// after handleConnection() called client.close(). Like socket.io does
|
|
134
|
+
// for a disconnected socket, drop those instead of handling them.
|
|
135
|
+
filter(() => client.readyState === READY_STATE.OPEN_STATE), filter(() => this.ensureSendBufferWithinLimit(client)),
|
|
136
|
+
// How many messages are pending at once is up to the client: ws emits
|
|
137
|
+
// all the messages of a read before any handler can settle.
|
|
138
|
+
mergeMapBounded(data => this.bindMessageHandler(data, handlersMap, transform).pipe(filter(result => !isNil(result)),
|
|
88
139
|
// a handler that rejects through an error filter that rethrows
|
|
89
140
|
// would error the client's stream and silence every later message;
|
|
90
141
|
// that rethrow is an app bug, so it gets logged, not dropped silently
|
|
91
142
|
catchError(err => {
|
|
92
143
|
this.logger.error(err);
|
|
93
144
|
return EMPTY;
|
|
94
|
-
}))
|
|
145
|
+
})), this.maxConcurrentMessages, backlogged => {
|
|
146
|
+
isBacklogged = backlogged;
|
|
147
|
+
updatePaused();
|
|
148
|
+
}), takeUntil(close$));
|
|
95
149
|
const onMessage = (response) => {
|
|
96
150
|
if (client.readyState !== READY_STATE.OPEN_STATE) {
|
|
97
151
|
return;
|
|
@@ -108,7 +162,14 @@ export class WsAdapter extends AbstractWsAdapter {
|
|
|
108
162
|
this.logger.error(err);
|
|
109
163
|
return;
|
|
110
164
|
}
|
|
111
|
-
|
|
165
|
+
pendingReplies++;
|
|
166
|
+
client.send(payload, () => {
|
|
167
|
+
pendingReplies--;
|
|
168
|
+
updatePaused();
|
|
169
|
+
});
|
|
170
|
+
if (this.ensureSendBufferWithinLimit(client)) {
|
|
171
|
+
updatePaused();
|
|
172
|
+
}
|
|
112
173
|
};
|
|
113
174
|
source$.subscribe(onMessage);
|
|
114
175
|
}
|
|
@@ -147,6 +208,27 @@ export class WsAdapter extends AbstractWsAdapter {
|
|
|
147
208
|
return EMPTY;
|
|
148
209
|
}
|
|
149
210
|
}
|
|
211
|
+
/**
|
|
212
|
+
* Not reading from a client keeps it from asking for more, but what is
|
|
213
|
+
* already on its way to it still queues up: the replies to the messages
|
|
214
|
+
* being handled, values of Observables returned by handlers, and what
|
|
215
|
+
* exception filters send. This caps how much may be held for one client,
|
|
216
|
+
* like "maxSendBufferSize" of the TCP transport. Returns `false` once the
|
|
217
|
+
* client has been closed for exceeding it.
|
|
218
|
+
*/
|
|
219
|
+
ensureSendBufferWithinLimit(client) {
|
|
220
|
+
const bufferedAmount = client.bufferedAmount;
|
|
221
|
+
if (!this.maxSendBufferSize || !(bufferedAmount > this.maxSendBufferSize)) {
|
|
222
|
+
return true;
|
|
223
|
+
}
|
|
224
|
+
// The queue is dropped only once the socket has closed, so a client that
|
|
225
|
+
// is already closing is not closed and reported again
|
|
226
|
+
if (client.readyState === READY_STATE.OPEN_STATE) {
|
|
227
|
+
this.logger.warn(`Closing a client with ${bufferedAmount} bytes queued for it, more than "maxSendBufferSize" (${this.maxSendBufferSize})`);
|
|
228
|
+
client.terminate();
|
|
229
|
+
}
|
|
230
|
+
return false;
|
|
231
|
+
}
|
|
150
232
|
bindErrorHandler(server) {
|
|
151
233
|
server.on(CONNECTION_EVENT, (ws) => ws.on(ERROR_EVENT, (err) => this.logger.error(err)));
|
|
152
234
|
server.on(ERROR_EVENT, (err) => this.logger.error(err));
|
|
@@ -184,7 +266,7 @@ export class WsAdapter extends AbstractWsAdapter {
|
|
|
184
266
|
setMessageParser(parser) {
|
|
185
267
|
this.messageParser = parser;
|
|
186
268
|
}
|
|
187
|
-
ensureHttpServerExists(port, httpServer = http.createServer()) {
|
|
269
|
+
ensureHttpServerExists(port, httpServer = http.createServer(rejectNonUpgradeRequest)) {
|
|
188
270
|
if (this.httpServersRegistry.has(port)) {
|
|
189
271
|
return;
|
|
190
272
|
}
|
|
@@ -209,6 +291,12 @@ export class WsAdapter extends AbstractWsAdapter {
|
|
|
209
291
|
}
|
|
210
292
|
}
|
|
211
293
|
catch (err) {
|
|
294
|
+
// Node hands the socket over without listeners of its own, and end()
|
|
295
|
+
// only half-closes it. A client resetting the connection afterwards
|
|
296
|
+
// would raise an unhandled 'error' event, and one that never closes
|
|
297
|
+
// would keep the socket open. ws's own abortHandshake() does the same.
|
|
298
|
+
socket.on(ERROR_EVENT, () => socket.destroy());
|
|
299
|
+
socket.once('finish', () => socket.destroy());
|
|
212
300
|
socket.end(`HTTP/1.1 400\r\n${err.message}`);
|
|
213
301
|
}
|
|
214
302
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nestjs/platform-ws",
|
|
3
|
-
"version": "12.
|
|
3
|
+
"version": "12.2.0",
|
|
4
4
|
"description": "Nest - modern, fast, powerful node.js web framework (@platform-ws)",
|
|
5
5
|
"author": "Kamil Mysliwiec",
|
|
6
6
|
"license": "MIT",
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
"main": "./index.js",
|
|
9
9
|
"exports": {
|
|
10
10
|
".": "./index.js",
|
|
11
|
+
"./package.json": "./package.json",
|
|
11
12
|
"./*.js": "./*.js",
|
|
12
13
|
"./*": "./*.js"
|
|
13
14
|
},
|
|
@@ -33,5 +34,5 @@
|
|
|
33
34
|
"@nestjs/websockets": "^12.0.0",
|
|
34
35
|
"rxjs": "^7.1.0"
|
|
35
36
|
},
|
|
36
|
-
"gitHead": "
|
|
37
|
+
"gitHead": "af37111368de7fd2dfab0bdec2f530c7845510c2"
|
|
37
38
|
}
|