@stomp/stompjs 7.0.1 → 7.1.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/src/client.ts CHANGED
@@ -11,7 +11,9 @@ import {
11
11
  IPublishParams,
12
12
  IStompSocket,
13
13
  messageCallbackType,
14
+ ReconnectionTimeMode,
14
15
  StompSocketState,
16
+ TickerStrategy,
15
17
  wsErrorCallbackType,
16
18
  } from './types.js';
17
19
  import { Versions } from './versions.js';
@@ -90,6 +92,25 @@ export class Client {
90
92
  */
91
93
  public reconnectDelay: number = 5000;
92
94
 
95
+ /**
96
+ * tracking the time to the next reconnection. Initialized to [Client#reconnectDelay]{@link Client#reconnectDelay}'s value and it may
97
+ * change depending on the [Client#reconnectTimeMode]{@link Client#reconnectTimeMode} setting
98
+ */
99
+ private _nextReconnectDelay: number = 0;
100
+
101
+ /**
102
+ * Maximum time to wait between reconnects, in milliseconds. Defaults to 15 minutes.
103
+ * Only relevant when reconnectTimeMode not LINEAR (e.g. EXPONENTIAL).
104
+ * Set to 0 to wait indefinitely.
105
+ */
106
+ public maxReconnectDelay: number = 15 * 60 * 1000; // 15 minutes in ms
107
+
108
+ /**
109
+ * Reconnection wait time mode, either linear (default) or exponential.
110
+ * Note: See [Client#maxReconnectDelay]{@link Client#maxReconnectDelay} for setting the maximum delay when exponential
111
+ */
112
+ public reconnectTimeMode: ReconnectionTimeMode = ReconnectionTimeMode.LINEAR;
113
+
93
114
  /**
94
115
  * Incoming heartbeat interval in milliseconds. Set to 0 to disable.
95
116
  */
@@ -100,6 +121,23 @@ export class Client {
100
121
  */
101
122
  public heartbeatOutgoing: number = 10000;
102
123
 
124
+ /**
125
+ * Outgoing heartbeat strategy.
126
+ * See https://github.com/stomp-js/stompjs/pull/579
127
+ *
128
+ * Can be worker or interval strategy, but will always use `interval`
129
+ * if web workers are unavailable, for example, in a non-browser environment.
130
+ *
131
+ * Using Web Workers may work better on long-running pages
132
+ * and mobile apps, as the browser may suspend Timers in the main page.
133
+ * Try the `Worker` mode if you discover disconnects when the browser tab is in the background.
134
+ *
135
+ * When used in a JS environment, use 'worker' or 'interval' as valid values.
136
+ *
137
+ * Defaults to `interval` strategy.
138
+ */
139
+ public heartbeatStrategy: TickerStrategy = TickerStrategy.Interval;
140
+
103
141
  /**
104
142
  * This switches on a non-standard behavior while sending WebSocket packets.
105
143
  * It splits larger (text) packets into chunks of [maxWebSocketChunkSize]{@link Client#maxWebSocketChunkSize}.
@@ -221,7 +259,7 @@ export class Client {
221
259
  * This can be used to reliably fetch credentials, access token etc. from some other service
222
260
  * in an asynchronous way.
223
261
  */
224
- public beforeConnect: () => void | Promise<void>;
262
+ public beforeConnect: (client: Client) => void | Promise<void>;
225
263
 
226
264
  /**
227
265
  * Callback, invoked on every successful connection to the STOMP broker.
@@ -375,12 +413,24 @@ export class Client {
375
413
  public configure(conf: StompConfig): void {
376
414
  // bulk assign all properties to this
377
415
  (Object as any).assign(this, conf);
416
+
417
+ // Warn on incorrect maxReconnectDelay settings
418
+ if (
419
+ this.maxReconnectDelay > 0 &&
420
+ this.maxReconnectDelay < this.reconnectDelay
421
+ ) {
422
+ this.debug(
423
+ `Warning: maxReconnectDelay (${this.maxReconnectDelay}ms) is less than reconnectDelay (${this.reconnectDelay}ms). Using reconnectDelay as the maxReconnectDelay delay.`,
424
+ );
425
+ this.maxReconnectDelay = this.reconnectDelay;
426
+ }
378
427
  }
379
428
 
380
429
  /**
381
430
  * Initiate the connection with the broker.
382
431
  * If the connection breaks, as per [Client#reconnectDelay]{@link Client#reconnectDelay},
383
- * it will keep trying to reconnect.
432
+ * it will keep trying to reconnect. If the [Client#reconnectTimeMode]{@link Client#reconnectTimeMode}
433
+ * is set to EXPONENTIAL it will increase the wait time exponentially
384
434
  *
385
435
  * Call [Client#deactivate]{@link Client#deactivate} to disconnect and stop reconnection attempts.
386
436
  */
@@ -393,6 +443,7 @@ export class Client {
393
443
 
394
444
  this._changeState(ActivationState.ACTIVE);
395
445
 
446
+ this._nextReconnectDelay = this.reconnectDelay;
396
447
  this._connect();
397
448
  };
398
449
 
@@ -408,16 +459,18 @@ export class Client {
408
459
  }
409
460
 
410
461
  private async _connect(): Promise<void> {
411
- await this.beforeConnect();
462
+ await this.beforeConnect(this);
412
463
 
413
464
  if (this._stompHandler) {
414
- this.debug('There is already a stompHandler, skipping the call to connect');
465
+ this.debug(
466
+ 'There is already a stompHandler, skipping the call to connect',
467
+ );
415
468
  return;
416
469
  }
417
470
 
418
471
  if (!this.active) {
419
472
  this.debug(
420
- 'Client has been marked inactive, will not attempt to connect'
473
+ 'Client has been marked inactive, will not attempt to connect',
421
474
  );
422
475
  return;
423
476
  }
@@ -435,7 +488,7 @@ export class Client {
435
488
  // Connection not established, close the underlying socket
436
489
  // a reconnection will be attempted
437
490
  this.debug(
438
- `Connection not established in ${this.connectionTimeout}ms, closing socket`
491
+ `Connection not established in ${this.connectionTimeout}ms, closing socket`,
439
492
  );
440
493
  this.forceDisconnect();
441
494
  }, this.connectionTimeout);
@@ -453,6 +506,7 @@ export class Client {
453
506
  disconnectHeaders: this._disconnectHeaders,
454
507
  heartbeatIncoming: this.heartbeatIncoming,
455
508
  heartbeatOutgoing: this.heartbeatOutgoing,
509
+ heartbeatStrategy: this.heartbeatStrategy,
456
510
  splitLargeFrames: this.splitLargeFrames,
457
511
  maxWebSocketChunkSize: this.maxWebSocketChunkSize,
458
512
  forceBinaryWSFrames: this.forceBinaryWSFrames,
@@ -469,7 +523,7 @@ export class Client {
469
523
 
470
524
  if (!this.active) {
471
525
  this.debug(
472
- 'STOMP got connected while deactivate was issued, will disconnect now'
526
+ 'STOMP got connected while deactivate was issued, will disconnect now',
473
527
  );
474
528
  this._disposeStompHandler();
475
529
  return;
@@ -523,7 +577,7 @@ export class Client {
523
577
  } else if (this.brokerURL) {
524
578
  webSocket = new WebSocket(
525
579
  this.brokerURL,
526
- this.stompVersions.protocolVersions()
580
+ this.stompVersions.protocolVersions(),
527
581
  );
528
582
  } else {
529
583
  throw new Error('Either brokerURL or webSocketFactory must be provided');
@@ -533,12 +587,26 @@ export class Client {
533
587
  }
534
588
 
535
589
  private _schedule_reconnect(): void {
536
- if (this.reconnectDelay > 0) {
537
- this.debug(`STOMP: scheduling reconnection in ${this.reconnectDelay}ms`);
590
+ if (this._nextReconnectDelay > 0) {
591
+ this.debug(
592
+ `STOMP: scheduling reconnection in ${this._nextReconnectDelay}ms`,
593
+ );
538
594
 
539
595
  this._reconnector = setTimeout(() => {
596
+ if (this.reconnectTimeMode === ReconnectionTimeMode.EXPONENTIAL) {
597
+ this._nextReconnectDelay = this._nextReconnectDelay * 2;
598
+
599
+ // Truncated exponential backoff with a set limit unless disabled
600
+ if (this.maxReconnectDelay !== 0) {
601
+ this._nextReconnectDelay = Math.min(
602
+ this._nextReconnectDelay,
603
+ this.maxReconnectDelay,
604
+ );
605
+ }
606
+ }
607
+
540
608
  this._connect();
541
- }, this.reconnectDelay);
609
+ }, this._nextReconnectDelay);
542
610
  }
543
611
  }
544
612
 
@@ -577,6 +645,9 @@ export class Client {
577
645
 
578
646
  this._changeState(ActivationState.DEACTIVATING);
579
647
 
648
+ // Reset reconnection timer just to be safe
649
+ this._nextReconnectDelay = 0;
650
+
580
651
  // Clear if a reconnection was scheduled
581
652
  if (this._reconnector) {
582
653
  clearTimeout(this._reconnector);
@@ -747,7 +818,7 @@ export class Client {
747
818
  public subscribe(
748
819
  destination: string,
749
820
  callback: messageCallbackType,
750
- headers: StompHeaders = {}
821
+ headers: StompHeaders = {},
751
822
  ): StompSubscription {
752
823
  this._checkConnection();
753
824
  // @ts-ignore - we already checked that there is a _stompHandler, and it is connected
@@ -835,7 +906,7 @@ export class Client {
835
906
  public ack(
836
907
  messageId: string,
837
908
  subscriptionId: string,
838
- headers: StompHeaders = {}
909
+ headers: StompHeaders = {},
839
910
  ): void {
840
911
  this._checkConnection();
841
912
  // @ts-ignore - we already checked that there is a _stompHandler, and it is connected
@@ -858,7 +929,7 @@ export class Client {
858
929
  public nack(
859
930
  messageId: string,
860
931
  subscriptionId: string,
861
- headers: StompHeaders = {}
932
+ headers: StompHeaders = {},
862
933
  ): void {
863
934
  this._checkConnection();
864
935
  // @ts-ignore - we already checked that there is a _stompHandler, and it is connected
@@ -1,10 +1,12 @@
1
1
  import { StompHeaders } from './stomp-headers.js';
2
2
  import {
3
3
  ActivationState,
4
+ TickerStrategy,
4
5
  closeEventCallbackType,
5
6
  debugFnType,
6
7
  frameCallbackType,
7
8
  messageCallbackType,
9
+ ReconnectionTimeMode,
8
10
  wsErrorCallbackType,
9
11
  } from './types.js';
10
12
  import { Versions } from './versions.js';
@@ -42,6 +44,16 @@ export class StompConfig {
42
44
  */
43
45
  public reconnectDelay?: number;
44
46
 
47
+ /**
48
+ * See [Client#maxReconnectDelay]{@link Client#maxReconnectDelay}
49
+ */
50
+ public maxReconnectDelay?: number;
51
+
52
+ /**
53
+ * See [Client#reconnectTimeMode]{@link Client#reconnectTimeMode}
54
+ */
55
+ public reconnectTimeMode?: ReconnectionTimeMode;
56
+
45
57
  /**
46
58
  * See [Client#heartbeatIncoming]{@link Client#heartbeatIncoming}.
47
59
  */
@@ -52,6 +64,11 @@ export class StompConfig {
52
64
  */
53
65
  public heartbeatOutgoing?: number;
54
66
 
67
+ /**
68
+ * See [Client#heartbeatStrategy]{@link Client#heartbeatStrategy}.
69
+ */
70
+ public heartbeatStrategy?: TickerStrategy;
71
+
55
72
  /**
56
73
  * See [Client#splitLargeFrames]{@link Client#splitLargeFrames}.
57
74
  */
@@ -1,3 +1,4 @@
1
+ import { augmentWebsocket } from './augment-websocket.js';
1
2
  import { BYTE } from './byte.js';
2
3
  import { Client } from './client.js';
3
4
  import { FrameImpl } from './frame-impl.js';
@@ -6,6 +7,7 @@ import { ITransaction } from './i-transaction.js';
6
7
  import { Parser } from './parser.js';
7
8
  import { StompHeaders } from './stomp-headers.js';
8
9
  import { StompSubscription } from './stomp-subscription.js';
10
+ import { Ticker } from './ticker.js';
9
11
  import {
10
12
  closeEventCallbackType,
11
13
  debugFnType,
@@ -19,7 +21,6 @@ import {
19
21
  wsErrorCallbackType,
20
22
  } from './types.js';
21
23
  import { Versions } from './versions.js';
22
- import { augmentWebsocket } from './augment-websocket.js';
23
24
 
24
25
  /**
25
26
  * The STOMP protocol handler
@@ -85,7 +86,7 @@ export class StompHandler {
85
86
  private _partialData: string;
86
87
  private _escapeHeaderValues: boolean;
87
88
  private _counter: number;
88
- private _pinger: any;
89
+ private _pinger?: Ticker;
89
90
  private _ponger: any;
90
91
  private _lastServerActivityTS: number;
91
92
 
@@ -289,12 +290,14 @@ export class StompHandler {
289
290
  if (this.heartbeatOutgoing !== 0 && serverIncoming !== 0) {
290
291
  const ttl: number = Math.max(this.heartbeatOutgoing, serverIncoming);
291
292
  this.debug(`send PING every ${ttl}ms`);
292
- this._pinger = setInterval(() => {
293
+
294
+ this._pinger = new Ticker(ttl, this._client.heartbeatStrategy, this.debug);
295
+ this._pinger.start(() => {
293
296
  if (this._webSocket.readyState === StompSocketState.OPEN) {
294
297
  this._webSocket.send(BYTE.LF);
295
298
  this.debug('>>> PING');
296
299
  }
297
- }, ttl);
300
+ });
298
301
  }
299
302
 
300
303
  if (this.heartbeatIncoming !== 0 && serverOutgoing !== 0) {
@@ -426,7 +429,7 @@ export class StompHandler {
426
429
  this._connected = false;
427
430
 
428
431
  if (this._pinger) {
429
- clearInterval(this._pinger);
432
+ this._pinger.stop();
430
433
  this._pinger = undefined;
431
434
  }
432
435
  if (this._ponger) {
package/src/ticker.ts ADDED
@@ -0,0 +1,76 @@
1
+ import { debugFnType, TickerStrategy } from './types.js';
2
+
3
+ export class Ticker {
4
+ private readonly _workerScript = `
5
+ var startTime = Date.now();
6
+ setInterval(function() {
7
+ self.postMessage(Date.now() - startTime);
8
+ }, ${this._interval});
9
+ `;
10
+
11
+ private _worker?: Worker;
12
+ private _timer?: any;
13
+
14
+ constructor(
15
+ private readonly _interval: number,
16
+ private readonly _strategy = TickerStrategy.Interval,
17
+ private readonly _debug: debugFnType) {
18
+ }
19
+
20
+ public start(tick: (elapsedTime: number) => void): void {
21
+ this.stop();
22
+
23
+ if (this.shouldUseWorker()) {
24
+ this.runWorker(tick);
25
+ } else {
26
+ this.runInterval(tick);
27
+ }
28
+ }
29
+
30
+ public stop(): void {
31
+ this.disposeWorker();
32
+ this.disposeInterval();
33
+ }
34
+
35
+ private shouldUseWorker(): boolean {
36
+ return typeof(Worker) !== 'undefined' && this._strategy === TickerStrategy.Worker
37
+ }
38
+
39
+ private runWorker(tick: (elapsedTime: number) => void): void {
40
+ this._debug('Using runWorker for outgoing pings');
41
+ if (!this._worker) {
42
+ this._worker = new Worker(
43
+ URL.createObjectURL(
44
+ new Blob([this._workerScript], { type: 'text/javascript' })
45
+ )
46
+ );
47
+ this._worker.onmessage = (message) => tick(message.data);
48
+ }
49
+ }
50
+
51
+ private runInterval(tick: (elapsedTime: number) => void): void {
52
+ this._debug('Using runInterval for outgoing pings');
53
+ if (!this._timer) {
54
+ const startTime = Date.now();
55
+ this._timer = setInterval(() => {
56
+ tick(Date.now() - startTime);
57
+ }, this._interval);
58
+ }
59
+ }
60
+
61
+ private disposeWorker(): void {
62
+ if (this._worker) {
63
+ this._worker.terminate();
64
+ delete this._worker;
65
+ this._debug('Outgoing ping disposeWorker');
66
+ }
67
+ }
68
+
69
+ private disposeInterval(): void {
70
+ if (this._timer) {
71
+ clearInterval(this._timer);
72
+ delete this._timer;
73
+ this._debug('Outgoing ping disposeInterval');
74
+ }
75
+ }
76
+ }
package/src/types.ts CHANGED
@@ -157,6 +157,22 @@ export enum ActivationState {
157
157
  INACTIVE,
158
158
  }
159
159
 
160
+ /**
161
+ * Possible reconnection wait time modes
162
+ */
163
+ export enum ReconnectionTimeMode {
164
+ LINEAR,
165
+ EXPONENTIAL
166
+ }
167
+
168
+ /**
169
+ * Possible ticker strategies for outgoing heartbeat ping
170
+ */
171
+ export enum TickerStrategy {
172
+ Interval = 'interval',
173
+ Worker = 'worker'
174
+ }
175
+
160
176
  /**
161
177
  * @internal
162
178
  */
@@ -167,6 +183,7 @@ export interface IStomptHandlerConfig {
167
183
  disconnectHeaders: StompHeaders;
168
184
  heartbeatIncoming: number;
169
185
  heartbeatOutgoing: number;
186
+ heartbeatStrategy: TickerStrategy;
170
187
  splitLargeFrames: boolean;
171
188
  maxWebSocketChunkSize: number;
172
189
  forceBinaryWSFrames: boolean;