@webex/internal-plugin-mercury 3.0.0-bnr.5 → 3.0.0-next.10

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.
@@ -30,6 +30,7 @@ export default class Socket extends EventEmitter {
30
30
  */
31
31
  constructor() {
32
32
  super();
33
+ this._domain = 'unknown-domain';
33
34
  this.onmessage = this.onmessage.bind(this);
34
35
  this.onclose = this.onclose.bind(this);
35
36
  }
@@ -111,10 +112,10 @@ export default class Socket extends EventEmitter {
111
112
  return;
112
113
  }
113
114
  // logger is defined once open is called
114
- this.logger.info('socket: closing');
115
+ this.logger.info(`socket,${this._domain}: closing`);
115
116
 
116
117
  if (socket.readyState === 2 || socket.readyState === 3) {
117
- this.logger.info('socket: already closed');
118
+ this.logger.info(`socket,${this._domain}: already closed`);
118
119
  resolve();
119
120
 
120
121
  return;
@@ -134,7 +135,7 @@ export default class Socket extends EventEmitter {
134
135
 
135
136
  const closeTimer = safeSetTimeout(() => {
136
137
  try {
137
- this.logger.info('socket: no close event received, forcing closure');
138
+ this.logger.info(`socket,${this._domain}: no close event received, forcing closure`);
138
139
  resolve(
139
140
  this.onclose({
140
141
  code: 1000,
@@ -142,12 +143,12 @@ export default class Socket extends EventEmitter {
142
143
  })
143
144
  );
144
145
  } catch (error) {
145
- this.logger.warn('socket: force-close failed', error);
146
+ this.logger.warn(`socket,${this._domain}: force-close failed`, error);
146
147
  }
147
148
  }, this.forceCloseDelay);
148
149
 
149
150
  socket.onclose = (event) => {
150
- this.logger.info('socket: close event fired', event.code, event.reason);
151
+ this.logger.info(`socket,${this._domain}: close event fired`, event.code, event.reason);
151
152
  clearTimeout(closeTimer);
152
153
  this.onclose(event);
153
154
  resolve(event);
@@ -171,6 +172,12 @@ export default class Socket extends EventEmitter {
171
172
  * @returns {Promise}
172
173
  */
173
174
  open(url, options) {
175
+ try {
176
+ this._domain = new URL(url).hostname;
177
+ } catch {
178
+ this._domain = url;
179
+ }
180
+
174
181
  return new Promise((resolve, reject) => {
175
182
  /* eslint complexity: [0] */
176
183
  if (!url) {
@@ -201,7 +208,7 @@ export default class Socket extends EventEmitter {
201
208
 
202
209
  const WebSocket = Socket.getWebSocketConstructor();
203
210
 
204
- this.logger.info('socket: creating WebSocket');
211
+ this.logger.info(`socket,${this._domain}: creating WebSocket`);
205
212
  const socket = new WebSocket(url, [], options);
206
213
 
207
214
  socket.binaryType = 'arraybuffer';
@@ -209,7 +216,7 @@ export default class Socket extends EventEmitter {
209
216
 
210
217
  socket.onclose = (event) => {
211
218
  event = this._fixCloseCode(event);
212
- this.logger.info('socket: closed before open', event.code, event.reason);
219
+ this.logger.info(`socket,${this._domain}: closed before open`, event.code, event.reason);
213
220
  switch (event.code) {
214
221
  case 1005:
215
222
  // IE 11 doesn't seem to allow 4XXX codes, so if we get a 1005, assume
@@ -231,10 +238,10 @@ export default class Socket extends EventEmitter {
231
238
  };
232
239
 
233
240
  socket.onopen = () => {
234
- this.logger.info('socket: connected');
241
+ this.logger.info(`socket,${this._domain}: connected`);
235
242
  this._authorize()
236
243
  .then(() => {
237
- this.logger.info('socket: authorized');
244
+ this.logger.info(`socket,${this._domain}: authorized`);
238
245
  socket.onclose = this.onclose;
239
246
  resolve();
240
247
  })
@@ -242,11 +249,11 @@ export default class Socket extends EventEmitter {
242
249
  };
243
250
 
244
251
  socket.onerror = (event) => {
245
- this.logger.warn('socket: error event fired', event);
252
+ this.logger.warn(`socket,${this._domain}: error event fired`, event);
246
253
  };
247
254
 
248
255
  sockets.set(this, socket);
249
- this.logger.info('socket: waiting for server');
256
+ this.logger.info(`socket,${this._domain}: waiting for server`);
250
257
  });
251
258
  }
252
259
 
@@ -256,7 +263,7 @@ export default class Socket extends EventEmitter {
256
263
  * @returns {undefined}
257
264
  */
258
265
  onclose(event) {
259
- this.logger.info('socket: closed', event.code, event.reason);
266
+ this.logger.info(`socket,${this._domain}: closed`, event.code, event.reason);
260
267
  clearTimeout(this.pongTimer);
261
268
  clearTimeout(this.pingTimer);
262
269
 
@@ -278,10 +285,10 @@ export default class Socket extends EventEmitter {
278
285
  const data = JSON.parse(event.data);
279
286
  const sequenceNumber = parseInt(data.sequenceNumber, 10);
280
287
 
281
- this.logger.debug('socket: sequence number: ', sequenceNumber);
288
+ this.logger.debug(`socket,${this._domain}: sequence number: `, sequenceNumber);
282
289
  if (this.expectedSequenceNumber && sequenceNumber !== this.expectedSequenceNumber) {
283
290
  this.logger.debug(
284
- `socket: sequence number mismatch indicates lost mercury message. expected: ${this.expectedSequenceNumber}, actual: ${sequenceNumber}`
291
+ `socket,${this._domain}: sequence number mismatch indicates lost mercury message. expected: ${this.expectedSequenceNumber}, actual: ${sequenceNumber}`
285
292
  );
286
293
  this.emit('sequence-mismatch', sequenceNumber, this.expectedSequenceNumber);
287
294
  }
@@ -303,7 +310,7 @@ export default class Socket extends EventEmitter {
303
310
  // message from Mercury. At this time, the only action we have is to
304
311
  // ignore it and move on.
305
312
  /* istanbul ignore next */
306
- this.logger.warn('socket: error while receiving WebSocket message', error);
313
+ this.logger.warn(`socket,${this._domain}: error while receiving WebSocket message`, error);
307
314
  }
308
315
  }
309
316
 
@@ -357,7 +364,7 @@ export default class Socket extends EventEmitter {
357
364
  */
358
365
  _authorize() {
359
366
  return new Promise((resolve) => {
360
- this.logger.info('socket: authorizing');
367
+ this.logger.info(`socket,${this._domain}: authorizing`);
361
368
  this.send({
362
369
  id: uuid.v4(),
363
370
  type: 'authorization',
@@ -395,12 +402,18 @@ export default class Socket extends EventEmitter {
395
402
  if (event.code === 1005 && event.reason) {
396
403
  switch (event.reason.toLowerCase()) {
397
404
  case 'replaced':
398
- this.logger.info('socket: fixing CloseEvent code for reason: ', event.reason);
405
+ this.logger.info(
406
+ `socket,${this._domain}: fixing CloseEvent code for reason: `,
407
+ event.reason
408
+ );
399
409
  event.code = 4000;
400
410
  break;
401
411
  case 'authentication failed':
402
412
  case 'authentication did not happen within the timeout window of 30000 seconds.':
403
- this.logger.info('socket: fixing CloseEvent code for reason: ', event.reason);
413
+ this.logger.info(
414
+ `socket,${this._domain}: fixing CloseEvent code for reason: `,
415
+ event.reason
416
+ );
404
417
  event.code = 1008;
405
418
  break;
406
419
  default:
@@ -420,10 +433,12 @@ export default class Socket extends EventEmitter {
420
433
  _ping(id) {
421
434
  const confirmPongId = (event) => {
422
435
  try {
423
- this.logger.debug('socket: pong', event.data.id);
436
+ this.logger.debug(`socket,${this._domain}: pong`, event.data.id);
424
437
  if (event.data && event.data.id !== id) {
425
- this.logger.info('socket: received pong for wrong ping id, closing socket');
426
- this.logger.debug('socket: expected', id, 'received', event.data.id);
438
+ this.logger.info(
439
+ `socket,${this._domain}: received pong for wrong ping id, closing socket`
440
+ );
441
+ this.logger.debug(`socket,${this._domain}: expected`, id, 'received', event.data.id);
427
442
  this.close({
428
443
  code: 1000,
429
444
  reason: 'Pong mismatch',
@@ -433,24 +448,29 @@ export default class Socket extends EventEmitter {
433
448
  // This try/catch block was added as a debugging step; to the best of my
434
449
  // knowledge, the above can never throw.
435
450
  /* istanbul ignore next */
436
- this.logger.error('socket: error occurred in confirmPongId', error);
451
+ this.logger.error(`socket,${this._domain}: error occurred in confirmPongId`, error);
437
452
  }
438
453
  };
439
454
 
440
455
  const onPongNotReceived = () => {
441
456
  try {
442
- this.logger.info('socket: pong not receive in expected period, closing socket');
457
+ this.logger.info(
458
+ `socket,${this._domain}: pong not receive in expected period, closing socket`
459
+ );
443
460
  this.close({
444
461
  code: 1000,
445
462
  reason: 'Pong not received',
446
463
  }).catch((reason) => {
447
- this.logger.warn('socket: failed to close socket after missed pong', reason);
464
+ this.logger.warn(
465
+ `socket,${this._domain}: failed to close socket after missed pong`,
466
+ reason
467
+ );
448
468
  });
449
469
  } catch (error) {
450
470
  // This try/catch block was added as a debugging step; to the best of my
451
471
  // knowledge, the above can never throw.
452
472
  /* istanbul ignore next */
453
- this.logger.error('socket: error occurred in onPongNotReceived', error);
473
+ this.logger.error(`socket,${this._domain}: error occurred in onPongNotReceived`, error);
454
474
  }
455
475
  };
456
476
 
@@ -462,16 +482,29 @@ export default class Socket extends EventEmitter {
462
482
  // This try/catch block was added as a debugging step; to the best of my
463
483
  // knowledge, the above can never throw.
464
484
  /* istanbul ignore next */
465
- this.logger.error('socket: error occurred in scheduleNextPingAndCancelPongTimer', error);
485
+ this.logger.error(
486
+ `socket,${this._domain}: error occurred in scheduleNextPingAndCancelPongTimer`,
487
+ error
488
+ );
466
489
  }
467
490
  };
468
491
 
492
+ const calculateLatency = (pingTimestamp) => {
493
+ const now = performance.now();
494
+ const latency = now - pingTimestamp;
495
+
496
+ this.logger.debug(`socket,${this._domain}: latency: `, latency);
497
+ this.emit('ping-pong-latency', latency);
498
+ };
499
+
469
500
  id = id || uuid.v4();
501
+ const pingTimestamp = performance.now();
502
+
470
503
  this.pongTimer = safeSetTimeout(onPongNotReceived, this.pongTimeout);
471
504
  this.once('pong', scheduleNextPingAndCancelPongTimer);
472
505
  this.once('pong', confirmPongId);
473
-
474
- this.logger.debug(`socket: ping ${id}`);
506
+ this.once('pong', () => calculateLatency(pingTimestamp));
507
+ this.logger.debug(`socket,${this._domain}: ping ${id}`);
475
508
 
476
509
  return this.send({
477
510
  id,
@@ -32,12 +32,13 @@ describe('plugin-mercury', function () {
32
32
  );
33
33
 
34
34
  describe('onBeforeLogout()', () => {
35
- it('disconnects the web socket', () =>
35
+ it('disconnects the web socket', () => {
36
36
  webex.logout({noRedirect: true}).then(() => {
37
37
  assert.called(webex.internal.mercury.disconnect);
38
38
  assert.isFalse(webex.internal.mercury.connected);
39
39
  assert.called(webex.internal.device.unregister);
40
40
  assert.isFalse(webex.internal.device.registered);
41
- }));
41
+ });
42
+ });
42
43
  });
43
44
  });
@@ -152,9 +152,8 @@ describe('plugin-mercury', () => {
152
152
  });
153
153
 
154
154
  describe('when `maxRetries` is set', () => {
155
- // skipping due to apparent bug with lolex in all browsers but Chrome.
156
- skipInBrowser(it)('fails after `maxRetries` attempts', () => {
157
- mercury.config.maxRetries = 2;
155
+
156
+ const check = () => {
158
157
  socketOpenStub.restore();
159
158
  socketOpenStub = sinon.stub(Socket.prototype, 'open');
160
159
  socketOpenStub.returns(Promise.reject(new ConnectionError()));
@@ -182,12 +181,42 @@ describe('plugin-mercury', () => {
182
181
  .then(() => {
183
182
  assert.calledThrice(Socket.prototype.open);
184
183
  clock.tick(5 * mercury.config.backoffTimeReset);
185
-
186
184
  return assert.isRejected(promise);
187
185
  })
188
186
  .then(() => {
189
187
  assert.calledThrice(Socket.prototype.open);
190
188
  });
189
+ }
190
+
191
+ // skipping due to apparent bug with lolex in all browsers but Chrome.
192
+ // if initial retries is zero and mercury has never connected max retries is used
193
+ skipInBrowser(it)('fails after `maxRetries` attempts', () => {
194
+ mercury.config.maxRetries = 2;
195
+ mercury.config.initialConnectionMaxRetries = 0;
196
+
197
+ return check();
198
+ });
199
+
200
+ // initial retries is non-zero so takes precedence over maxRetries when mercury has never connected
201
+ skipInBrowser(it)('fails after `initialConnectionMaxRetries` attempts', () => {
202
+ mercury.config.maxRetries = 0;
203
+ mercury.config.initialConnectionMaxRetries = 2;
204
+ return check();
205
+ });
206
+
207
+ // initial retries is non-zero so takes precedence over maxRetries when mercury has never connected
208
+ skipInBrowser(it)('fails after `initialConnectionMaxRetries` attempts', () => {
209
+ mercury.config.initialConnectionMaxRetries = 2;
210
+ mercury.config.maxRetries = 5;
211
+ return check();
212
+ });
213
+
214
+ // when mercury has connected maxRetries is used and the initialConnectionMaxRetries is ignored
215
+ skipInBrowser(it)('fails after `initialConnectionMaxRetries` attempts', () => {
216
+ mercury.config.initialConnectionMaxRetries = 5;
217
+ mercury.config.maxRetries = 2;
218
+ mercury.hasEverConnected = true;
219
+ return check();
191
220
  });
192
221
  });
193
222
 
@@ -541,7 +570,10 @@ describe('plugin-mercury', () => {
541
570
  // The socket will never be unset (which seems bad)
542
571
  assert.isDefined(mercury.socket, 'Mercury socket is not defined');
543
572
 
544
- return assert.isRejected(promise);
573
+ return assert.isRejected(promise).then((error) => {
574
+ // connection did not fail, so no last error
575
+ assert.isUndefined(mercury.getLastError());
576
+ });
545
577
  });
546
578
  });
547
579
 
@@ -560,20 +592,53 @@ describe('plugin-mercury', () => {
560
592
  return promiseTick(webex.internal.mercury.config.backoffTimeReset).then(() => {
561
593
  assert.equal(
562
594
  reason.message,
563
- 'mercury: prevent socket open when backoffCall no longer defined'
595
+ 'Mercury: prevent socket open when backoffCall no longer defined'
564
596
  );
565
597
  });
566
598
  });
599
+
600
+ it('sets lastError when retrying', () => {
601
+ const realError = new Error('FORCED');
602
+
603
+ socketOpenStub.restore();
604
+ socketOpenStub = sinon.stub(Socket.prototype, 'open');
605
+ socketOpenStub.onCall(0).returns(Promise.reject(realError));
606
+ const promise = mercury.connect();
607
+
608
+ // Wait for the connect call to setup
609
+ return promiseTick(webex.internal.mercury.config.backoffTimeReset).then(() => {
610
+ // Calling disconnect will abort the backoffCall, close the socket, and
611
+ // reject the connect
612
+ mercury.disconnect();
613
+
614
+ return assert.isRejected(promise).then((error) => {
615
+ const lastError = mercury.getLastError();
616
+
617
+ assert.equal(error.message, "Mercury Connection Aborted");
618
+ assert.isDefined(lastError);
619
+ assert.equal(lastError, realError);
620
+ });
621
+ });
622
+ });
567
623
  });
568
624
  });
569
625
 
570
626
  describe('#_emit()', () => {
571
- it('emits Error-safe events', () => {
627
+ it('emits Error-safe events and log the error with the call parameters', () => {
628
+ const error = 'error';
629
+ const event = {data: 'some data'};
572
630
  mercury.on('break', () => {
573
- throw new Error();
631
+ throw error;
574
632
  });
633
+ sinon.stub(mercury.logger, 'error');
575
634
 
576
- return Promise.resolve(mercury._emit('break'));
635
+ return Promise.resolve(mercury._emit('break', event)).then((res) => {
636
+ assert.calledWith(mercury.logger.error, 'Mercury: error occurred in event handler', {
637
+ error,
638
+ arguments: ['break', event],
639
+ });
640
+ return res;
641
+ });
577
642
  });
578
643
  });
579
644
 
@@ -707,5 +772,18 @@ describe('plugin-mercury', () => {
707
772
  .then((wsUrl) => assert.match(wsUrl, /multipleConnections/)));
708
773
  });
709
774
  });
775
+
776
+ describe('ping pong latency event is forwarded', () => {
777
+ it('should forward ping pong latency event', () => {
778
+ const spy = sinon.spy();
779
+
780
+ mercury.on('ping-pong-latency', spy);
781
+
782
+ return mercury.connect().then(() => {
783
+ assert.calledWith(spy, 0);
784
+ assert.calledOnce(spy);
785
+ });
786
+ });
787
+ });
710
788
  });
711
789
  });
@@ -39,7 +39,7 @@ describe('plugin-mercury', () => {
39
39
  clock.uninstall();
40
40
  });
41
41
 
42
- beforeEach('mock WebSocket and open a Socket', () => {
42
+ beforeEach(() => {
43
43
  sinon.stub(Socket, 'getWebSocketConstructor').callsFake(
44
44
  () =>
45
45
  function (...args) {
@@ -809,6 +809,23 @@ describe('plugin-mercury', () => {
809
809
  reason: 'Pong mismatch',
810
810
  });
811
811
  });
812
+
813
+ it('emits ping pong latency correctly', () => {
814
+ const spy = sinon.spy();
815
+
816
+ socket.on('ping-pong-latency', spy);
817
+
818
+ socket._ping(123);
819
+ mockWebSocket.emit('message', {
820
+ data: JSON.stringify({
821
+ type: 'pong',
822
+ id: 123,
823
+ }),
824
+ });
825
+
826
+ assert.calledWith(spy, 0);
827
+ assert.calledOnce(spy);
828
+ });
812
829
  });
813
830
  });
814
831
  });
@@ -1,10 +0,0 @@
1
- declare namespace _default {
2
- namespace mercury {
3
- const pingInterval: number;
4
- const pongTimeout: number;
5
- const backoffTimeMax: number;
6
- const backoffTimeReset: number;
7
- const forceCloseDelay: [type];
8
- }
9
- }
10
- export default _default;
@@ -1,31 +0,0 @@
1
- /**
2
- * Exception thrown when a websocket gets closed
3
- */
4
- export class ConnectionError {
5
- static defaultMessage: string;
6
- /**
7
- * @param {CloseEvent} event
8
- * @returns {string}
9
- */
10
- parse(event?: CloseEvent): string;
11
- }
12
- /**
13
- * thrown for CloseCode 4400
14
- */
15
- export class UnknownResponse extends ConnectionError {
16
- }
17
- /**
18
- * thrown for CloseCode 4400
19
- */
20
- export class BadRequest extends ConnectionError {
21
- }
22
- /**
23
- * thrown for CloseCode 4401
24
- */
25
- export class NotAuthorized extends ConnectionError {
26
- }
27
- /**
28
- * thrown for CloseCode 4403
29
- */
30
- export class Forbidden extends ConnectionError {
31
- }
@@ -1,4 +0,0 @@
1
- export { default as Socket } from "./socket";
2
- export { default as config } from "./config";
3
- export { default, default as Mercury } from "./mercury";
4
- export { BadRequest, ConnectionError, Forbidden, NotAuthorized, UnknownResponse } from "./errors";
@@ -1,2 +0,0 @@
1
- export default Mercury;
2
- declare const Mercury: any;
@@ -1 +0,0 @@
1
- export { default } from "./socket";
@@ -1,120 +0,0 @@
1
- /**
2
- * Generalized socket abstraction
3
- */
4
- export default class Socket extends EventEmitter {
5
- /**
6
- * Provides the environmentally appropriate constructor (ws in NodeJS,
7
- * WebSocket in browsers)
8
- * @returns {WebSocket}
9
- */
10
- static getWebSocketConstructor(): WebSocket;
11
- /**
12
- * constructor
13
- * @returns {Socket}
14
- */
15
- constructor();
16
- /**
17
- * Handles incoming message events
18
- * @param {MessageEvent} event
19
- * @returns {undefined}
20
- */
21
- onmessage(event: MessageEvent): undefined;
22
- /**
23
- * Handles incoming CloseEvents
24
- * @param {CloseEvent} event
25
- * @returns {undefined}
26
- */
27
- onclose(event: CloseEvent): undefined;
28
- /**
29
- * @see https://developer.mozilla.org/en-US/docs/Web/API/WebSocket
30
- * @returns {string}
31
- */
32
- get binaryType(): string;
33
- /**
34
- * @see https://developer.mozilla.org/en-US/docs/Web/API/WebSocket
35
- * @returns {number}
36
- */
37
- get bufferedAmount(): number;
38
- /**
39
- * @see https://developer.mozilla.org/en-US/docs/Web/API/WebSocket
40
- * @returns {string}
41
- */
42
- get extensions(): string;
43
- /**
44
- * @see https://developer.mozilla.org/en-US/docs/Web/API/WebSocket
45
- * @returns {string}
46
- */
47
- get protocol(): string;
48
- /**
49
- * @see https://developer.mozilla.org/en-US/docs/Web/API/WebSocket
50
- * @returns {number}
51
- */
52
- get readyState(): number;
53
- /**
54
- * @see https://developer.mozilla.org/en-US/docs/Web/API/WebSocket
55
- * @returns {string}
56
- */
57
- get url(): string;
58
- /**
59
- * Closes the socket
60
- * @param {Object} options
61
- * @param {string} options.reason
62
- * @param {number} options.code
63
- * @returns {Promise}
64
- */
65
- close(options: {
66
- reason: string;
67
- code: number;
68
- }): Promise<any>;
69
- /**
70
- * Opens a WebSocket
71
- * @param {string} url
72
- * @param {options} options
73
- * @param {number} options.forceCloseDelay (required)
74
- * @param {number} options.pingInterval (required)
75
- * @param {number} options.pongTimeout (required)
76
- * @param {string} options.token (required)
77
- * @param {string} options.trackingId (required)
78
- * @param {Logger} options.logger (required)
79
- * @param {string} options.logLevelToken
80
- * @returns {Promise}
81
- */
82
- open(url: string, options: any): Promise<any>;
83
- expectedSequenceNumber: any;
84
- /**
85
- * Sends a message up the socket
86
- * @param {mixed} data
87
- * @returns {Promise}
88
- */
89
- send(data: mixed): Promise<any>;
90
- /**
91
- * Sends an acknowledgment for a specific event
92
- * @param {MessageEvent} event
93
- * @returns {Promise}
94
- */
95
- _acknowledge(event: MessageEvent): Promise<any>;
96
- /**
97
- * Sends an auth message up the socket
98
- * @private
99
- * @returns {Promise}
100
- */
101
- private _authorize;
102
- /**
103
- * Deals with the fact that some browsers drop some close codes (but not
104
- * close reasons).
105
- * @param {CloseEvent} event
106
- * @private
107
- * @returns {CloseEvent}
108
- */
109
- private _fixCloseCode;
110
- /**
111
- * Sends a ping up the socket and confirms we get it back
112
- * @param {[type]} id
113
- * @private
114
- * @returns {[type]}
115
- */
116
- private _ping;
117
- pingTimer: any;
118
- pongTimer: any;
119
- }
120
- import { EventEmitter } from "events";
@@ -1,2 +0,0 @@
1
- export default Socket;
2
- import Socket from "./socket-base";
@@ -1,2 +0,0 @@
1
- export default Socket;
2
- import Socket from "./socket-base";
@@ -1,5 +0,0 @@
1
- /*!
2
- * Copyright (c) 2015-2020 Cisco Systems, Inc. See LICENSE file.
3
- */
4
-
5
- console.debug = console.debug || console.info;