@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.
@@ -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>;
@@ -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, mergeMap, share, takeUntil, } from 'rxjs/operators';
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
- httpServer?.listen(port);
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(mergeMap(data => this.bindMessageHandler(data, handlersMap, transform).pipe(filter(result => !isNil(result)),
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
- }))), takeUntil(close$));
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
- client.send(payload);
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.1.3",
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": "4514623279f6087808e3c8f08fbf9f37d3c17482"
37
+ "gitHead": "af37111368de7fd2dfab0bdec2f530c7845510c2"
37
38
  }