@webex/internal-plugin-mercury 2.59.1 → 2.59.3-next.1

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.
@@ -1,481 +1,481 @@
1
- /*!
2
- * Copyright (c) 2015-2020 Cisco Systems, Inc. See LICENSE file.
3
- */
4
-
5
- import {EventEmitter} from 'events';
6
-
7
- import {checkRequired} from '@webex/common';
8
- import {safeSetTimeout} from '@webex/common-timers';
9
- import {defaults, has, isObject} from 'lodash';
10
- import uuid from 'uuid';
11
-
12
- import {
13
- BadRequest,
14
- ConnectionError,
15
- Forbidden,
16
- NotAuthorized,
17
- UnknownResponse,
18
- // NotFound
19
- } from '../errors';
20
-
21
- const sockets = new WeakMap();
22
-
23
- /**
24
- * Generalized socket abstraction
25
- */
26
- export default class Socket extends EventEmitter {
27
- /**
28
- * constructor
29
- * @returns {Socket}
30
- */
31
- constructor() {
32
- super();
33
- this.onmessage = this.onmessage.bind(this);
34
- this.onclose = this.onclose.bind(this);
35
- }
36
-
37
- /**
38
- * @see https://developer.mozilla.org/en-US/docs/Web/API/WebSocket
39
- * @returns {string}
40
- */
41
- get binaryType() {
42
- return sockets.get(this).binaryType;
43
- }
44
-
45
- /**
46
- * @see https://developer.mozilla.org/en-US/docs/Web/API/WebSocket
47
- * @returns {number}
48
- */
49
- get bufferedAmount() {
50
- return sockets.get(this).bufferedAmount;
51
- }
52
-
53
- /**
54
- * @see https://developer.mozilla.org/en-US/docs/Web/API/WebSocket
55
- * @returns {string}
56
- */
57
- get extensions() {
58
- return sockets.get(this).extensions;
59
- }
60
-
61
- /**
62
- * @see https://developer.mozilla.org/en-US/docs/Web/API/WebSocket
63
- * @returns {string}
64
- */
65
- get protocol() {
66
- return sockets.get(this).protocol;
67
- }
68
-
69
- /**
70
- * @see https://developer.mozilla.org/en-US/docs/Web/API/WebSocket
71
- * @returns {number}
72
- */
73
- get readyState() {
74
- return sockets.get(this).readyState;
75
- }
76
-
77
- /**
78
- * @see https://developer.mozilla.org/en-US/docs/Web/API/WebSocket
79
- * @returns {string}
80
- */
81
- get url() {
82
- return sockets.get(this).url;
83
- }
84
-
85
- /**
86
- * Provides the environmentally appropriate constructor (ws in NodeJS,
87
- * WebSocket in browsers)
88
- * @returns {WebSocket}
89
- */
90
- static getWebSocketConstructor() {
91
- throw new Error(
92
- 'Socket.getWebSocketConstructor() must be implemented in an environmentally appropriate way'
93
- );
94
- }
95
-
96
- /**
97
- * Closes the socket
98
- * @param {Object} options
99
- * @param {string} options.reason
100
- * @param {number} options.code
101
- * @returns {Promise}
102
- */
103
- close(options) {
104
- return new Promise((resolve, reject) => {
105
- const socket = sockets.get(this);
106
-
107
- if (!socket) {
108
- // Open has not been called yet so there is no socket to close
109
- resolve();
110
-
111
- return;
112
- }
113
- // logger is defined once open is called
114
- this.logger.info('socket: closing');
115
-
116
- if (socket.readyState === 2 || socket.readyState === 3) {
117
- this.logger.info('socket: already closed');
118
- resolve();
119
-
120
- return;
121
- }
122
-
123
- options = options || {};
124
- if (options.code && options.code !== 1000 && (options.code < 3000 || options.code > 4999)) {
125
- reject(new Error('`options.code` must be 1000 or between 3000 and 4999 (inclusive)'));
126
-
127
- return;
128
- }
129
-
130
- options = defaults(options, {
131
- code: 1000,
132
- reason: 'Done',
133
- });
134
-
135
- const closeTimer = safeSetTimeout(() => {
136
- try {
137
- this.logger.info('socket: no close event received, forcing closure');
138
- resolve(
139
- this.onclose({
140
- code: 1000,
141
- reason: 'Done (forced)',
142
- })
143
- );
144
- } catch (error) {
145
- this.logger.warn('socket: force-close failed', error);
146
- }
147
- }, this.forceCloseDelay);
148
-
149
- socket.onclose = (event) => {
150
- this.logger.info('socket: close event fired', event.code, event.reason);
151
- clearTimeout(closeTimer);
152
- this.onclose(event);
153
- resolve(event);
154
- };
155
-
156
- socket.close(options.code, options.reason);
157
- });
158
- }
159
-
160
- /**
161
- * Opens a WebSocket
162
- * @param {string} url
163
- * @param {options} options
164
- * @param {number} options.forceCloseDelay (required)
165
- * @param {number} options.pingInterval (required)
166
- * @param {number} options.pongTimeout (required)
167
- * @param {string} options.token (required)
168
- * @param {string} options.trackingId (required)
169
- * @param {Logger} options.logger (required)
170
- * @param {string} options.logLevelToken
171
- * @returns {Promise}
172
- */
173
- open(url, options) {
174
- return new Promise((resolve, reject) => {
175
- /* eslint complexity: [0] */
176
- if (!url) {
177
- reject(new Error('`url` is required'));
178
-
179
- return;
180
- }
181
-
182
- if (sockets.get(this)) {
183
- reject(new Error('Socket#open() can only be called once per instance'));
184
-
185
- return;
186
- }
187
-
188
- options = options || {};
189
-
190
- checkRequired(
191
- ['forceCloseDelay', 'pingInterval', 'pongTimeout', 'token', 'trackingId', 'logger'],
192
- options
193
- );
194
-
195
- Object.keys(options).forEach((key) => {
196
- Reflect.defineProperty(this, key, {
197
- enumerable: false,
198
- value: options[key],
199
- });
200
- });
201
-
202
- const WebSocket = Socket.getWebSocketConstructor();
203
-
204
- this.logger.info('socket: creating WebSocket');
205
- const socket = new WebSocket(url, [], options);
206
-
207
- socket.binaryType = 'arraybuffer';
208
- socket.onmessage = this.onmessage;
209
-
210
- socket.onclose = (event) => {
211
- event = this._fixCloseCode(event);
212
- this.logger.info('socket: closed before open', event.code, event.reason);
213
- switch (event.code) {
214
- case 1005:
215
- // IE 11 doesn't seem to allow 4XXX codes, so if we get a 1005, assume
216
- // it's a bad websocket url. That'll trigger a device refresh; if it
217
- // turns out we had a bad token, the device refresh should 401 and
218
- // trigger a token refresh.
219
- return reject(new UnknownResponse(event));
220
- case 4400:
221
- return reject(new BadRequest(event));
222
- case 4401:
223
- return reject(new NotAuthorized(event));
224
- case 4403:
225
- return reject(new Forbidden(event));
226
- // case 4404:
227
- // return reject(new NotFound(event));
228
- default:
229
- return reject(new ConnectionError(event));
230
- }
231
- };
232
-
233
- socket.onopen = () => {
234
- this.logger.info('socket: connected');
235
- this._authorize()
236
- .then(() => {
237
- this.logger.info('socket: authorized');
238
- socket.onclose = this.onclose;
239
- resolve();
240
- })
241
- .catch(reject);
242
- };
243
-
244
- socket.onerror = (event) => {
245
- this.logger.warn('socket: error event fired', event);
246
- };
247
-
248
- sockets.set(this, socket);
249
- this.logger.info('socket: waiting for server');
250
- });
251
- }
252
-
253
- /**
254
- * Handles incoming CloseEvents
255
- * @param {CloseEvent} event
256
- * @returns {undefined}
257
- */
258
- onclose(event) {
259
- this.logger.info('socket: closed', event.code, event.reason);
260
- clearTimeout(this.pongTimer);
261
- clearTimeout(this.pingTimer);
262
-
263
- event = this._fixCloseCode(event);
264
- this.emit('close', event);
265
-
266
- // Remove all listeners to (a) avoid reacting to late pongs and (b) ensure
267
- // we don't have a retain cycle.
268
- this.removeAllListeners();
269
- }
270
-
271
- /**
272
- * Handles incoming message events
273
- * @param {MessageEvent} event
274
- * @returns {undefined}
275
- */
276
- onmessage(event) {
277
- try {
278
- const data = JSON.parse(event.data);
279
- const sequenceNumber = parseInt(data.sequenceNumber, 10);
280
-
281
- this.logger.debug('socket: sequence number: ', sequenceNumber);
282
- if (this.expectedSequenceNumber && sequenceNumber !== this.expectedSequenceNumber) {
283
- this.logger.debug(
284
- `socket: sequence number mismatch indicates lost mercury message. expected: ${this.expectedSequenceNumber}, actual: ${sequenceNumber}`
285
- );
286
- this.emit('sequence-mismatch', sequenceNumber, this.expectedSequenceNumber);
287
- }
288
- this.expectedSequenceNumber = sequenceNumber + 1;
289
-
290
- // Yes, it's a little weird looking; we want to emit message events that
291
- // look like normal socket message events, but event.data cannot be
292
- // modified and we don't actually care about anything but the data property
293
- const processedEvent = {data};
294
-
295
- this._acknowledge(processedEvent);
296
- if (data.type === 'pong') {
297
- this.emit('pong', processedEvent);
298
- } else {
299
- this.emit('message', processedEvent);
300
- }
301
- } catch (error) {
302
- // The above code should only be able to throw if we receive an unparsable
303
- // message from Mercury. At this time, the only action we have is to
304
- // ignore it and move on.
305
- /* istanbul ignore next */
306
- this.logger.warn('socket: error while receiving WebSocket message', error);
307
- }
308
- }
309
-
310
- /**
311
- * Sends a message up the socket
312
- * @param {mixed} data
313
- * @returns {Promise}
314
- */
315
- send(data) {
316
- return new Promise((resolve, reject) => {
317
- if (this.readyState !== 1) {
318
- return reject(new Error('INVALID_STATE_ERROR'));
319
- }
320
-
321
- if (isObject(data)) {
322
- data = JSON.stringify(data);
323
- }
324
-
325
- const socket = sockets.get(this);
326
-
327
- socket.send(data);
328
-
329
- return resolve();
330
- });
331
- }
332
-
333
- /**
334
- * Sends an acknowledgment for a specific event
335
- * @param {MessageEvent} event
336
- * @returns {Promise}
337
- */
338
- _acknowledge(event) {
339
- if (!event) {
340
- return Promise.reject(new Error('`event` is required'));
341
- }
342
-
343
- if (!has(event, 'data.id')) {
344
- return Promise.reject(new Error('`event.data.id` is required'));
345
- }
346
-
347
- return this.send({
348
- messageId: event.data.id,
349
- type: 'ack',
350
- });
351
- }
352
-
353
- /**
354
- * Sends an auth message up the socket
355
- * @private
356
- * @returns {Promise}
357
- */
358
- _authorize() {
359
- return new Promise((resolve) => {
360
- this.logger.info('socket: authorizing');
361
- this.send({
362
- id: uuid.v4(),
363
- type: 'authorization',
364
- data: {
365
- token: this.token,
366
- },
367
- trackingId: this.trackingId,
368
- logLevelToken: this.logLevelToken,
369
- });
370
-
371
- const waitForBufferState = (event) => {
372
- if (
373
- !event.data.type &&
374
- (event.data.data.eventType === 'mercury.buffer_state' ||
375
- event.data.data.eventType === 'mercury.registration_status')
376
- ) {
377
- this.removeListener('message', waitForBufferState);
378
- this._ping();
379
- resolve();
380
- }
381
- };
382
-
383
- this.once('message', waitForBufferState);
384
- });
385
- }
386
-
387
- /**
388
- * Deals with the fact that some browsers drop some close codes (but not
389
- * close reasons).
390
- * @param {CloseEvent} event
391
- * @private
392
- * @returns {CloseEvent}
393
- */
394
- _fixCloseCode(event) {
395
- if (event.code === 1005 && event.reason) {
396
- switch (event.reason.toLowerCase()) {
397
- case 'replaced':
398
- this.logger.info('socket: fixing CloseEvent code for reason: ', event.reason);
399
- event.code = 4000;
400
- break;
401
- case 'authentication failed':
402
- case 'authentication did not happen within the timeout window of 30000 seconds.':
403
- this.logger.info('socket: fixing CloseEvent code for reason: ', event.reason);
404
- event.code = 1008;
405
- break;
406
- default:
407
- // do nothing
408
- }
409
- }
410
-
411
- return event;
412
- }
413
-
414
- /**
415
- * Sends a ping up the socket and confirms we get it back
416
- * @param {[type]} id
417
- * @private
418
- * @returns {[type]}
419
- */
420
- _ping(id) {
421
- const confirmPongId = (event) => {
422
- try {
423
- this.logger.debug('socket: pong', event.data.id);
424
- 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);
427
- this.close({
428
- code: 1000,
429
- reason: 'Pong mismatch',
430
- });
431
- }
432
- } catch (error) {
433
- // This try/catch block was added as a debugging step; to the best of my
434
- // knowledge, the above can never throw.
435
- /* istanbul ignore next */
436
- this.logger.error('socket: error occurred in confirmPongId', error);
437
- }
438
- };
439
-
440
- const onPongNotReceived = () => {
441
- try {
442
- this.logger.info('socket: pong not receive in expected period, closing socket');
443
- this.close({
444
- code: 1000,
445
- reason: 'Pong not received',
446
- }).catch((reason) => {
447
- this.logger.warn('socket: failed to close socket after missed pong', reason);
448
- });
449
- } catch (error) {
450
- // This try/catch block was added as a debugging step; to the best of my
451
- // knowledge, the above can never throw.
452
- /* istanbul ignore next */
453
- this.logger.error('socket: error occurred in onPongNotReceived', error);
454
- }
455
- };
456
-
457
- const scheduleNextPingAndCancelPongTimer = () => {
458
- try {
459
- clearTimeout(this.pongTimer);
460
- this.pingTimer = safeSetTimeout(() => this._ping(), this.pingInterval);
461
- } catch (error) {
462
- // This try/catch block was added as a debugging step; to the best of my
463
- // knowledge, the above can never throw.
464
- /* istanbul ignore next */
465
- this.logger.error('socket: error occurred in scheduleNextPingAndCancelPongTimer', error);
466
- }
467
- };
468
-
469
- id = id || uuid.v4();
470
- this.pongTimer = safeSetTimeout(onPongNotReceived, this.pongTimeout);
471
- this.once('pong', scheduleNextPingAndCancelPongTimer);
472
- this.once('pong', confirmPongId);
473
-
474
- this.logger.debug(`socket: ping ${id}`);
475
-
476
- return this.send({
477
- id,
478
- type: 'ping',
479
- });
480
- }
481
- }
1
+ /*!
2
+ * Copyright (c) 2015-2020 Cisco Systems, Inc. See LICENSE file.
3
+ */
4
+
5
+ import {EventEmitter} from 'events';
6
+
7
+ import {checkRequired} from '@webex/common';
8
+ import {safeSetTimeout} from '@webex/common-timers';
9
+ import {defaults, has, isObject} from 'lodash';
10
+ import uuid from 'uuid';
11
+
12
+ import {
13
+ BadRequest,
14
+ ConnectionError,
15
+ Forbidden,
16
+ NotAuthorized,
17
+ UnknownResponse,
18
+ // NotFound
19
+ } from '../errors';
20
+
21
+ const sockets = new WeakMap();
22
+
23
+ /**
24
+ * Generalized socket abstraction
25
+ */
26
+ export default class Socket extends EventEmitter {
27
+ /**
28
+ * constructor
29
+ * @returns {Socket}
30
+ */
31
+ constructor() {
32
+ super();
33
+ this.onmessage = this.onmessage.bind(this);
34
+ this.onclose = this.onclose.bind(this);
35
+ }
36
+
37
+ /**
38
+ * @see https://developer.mozilla.org/en-US/docs/Web/API/WebSocket
39
+ * @returns {string}
40
+ */
41
+ get binaryType() {
42
+ return sockets.get(this).binaryType;
43
+ }
44
+
45
+ /**
46
+ * @see https://developer.mozilla.org/en-US/docs/Web/API/WebSocket
47
+ * @returns {number}
48
+ */
49
+ get bufferedAmount() {
50
+ return sockets.get(this).bufferedAmount;
51
+ }
52
+
53
+ /**
54
+ * @see https://developer.mozilla.org/en-US/docs/Web/API/WebSocket
55
+ * @returns {string}
56
+ */
57
+ get extensions() {
58
+ return sockets.get(this).extensions;
59
+ }
60
+
61
+ /**
62
+ * @see https://developer.mozilla.org/en-US/docs/Web/API/WebSocket
63
+ * @returns {string}
64
+ */
65
+ get protocol() {
66
+ return sockets.get(this).protocol;
67
+ }
68
+
69
+ /**
70
+ * @see https://developer.mozilla.org/en-US/docs/Web/API/WebSocket
71
+ * @returns {number}
72
+ */
73
+ get readyState() {
74
+ return sockets.get(this).readyState;
75
+ }
76
+
77
+ /**
78
+ * @see https://developer.mozilla.org/en-US/docs/Web/API/WebSocket
79
+ * @returns {string}
80
+ */
81
+ get url() {
82
+ return sockets.get(this).url;
83
+ }
84
+
85
+ /**
86
+ * Provides the environmentally appropriate constructor (ws in NodeJS,
87
+ * WebSocket in browsers)
88
+ * @returns {WebSocket}
89
+ */
90
+ static getWebSocketConstructor() {
91
+ throw new Error(
92
+ 'Socket.getWebSocketConstructor() must be implemented in an environmentally appropriate way'
93
+ );
94
+ }
95
+
96
+ /**
97
+ * Closes the socket
98
+ * @param {Object} options
99
+ * @param {string} options.reason
100
+ * @param {number} options.code
101
+ * @returns {Promise}
102
+ */
103
+ close(options) {
104
+ return new Promise((resolve, reject) => {
105
+ const socket = sockets.get(this);
106
+
107
+ if (!socket) {
108
+ // Open has not been called yet so there is no socket to close
109
+ resolve();
110
+
111
+ return;
112
+ }
113
+ // logger is defined once open is called
114
+ this.logger.info('socket: closing');
115
+
116
+ if (socket.readyState === 2 || socket.readyState === 3) {
117
+ this.logger.info('socket: already closed');
118
+ resolve();
119
+
120
+ return;
121
+ }
122
+
123
+ options = options || {};
124
+ if (options.code && options.code !== 1000 && (options.code < 3000 || options.code > 4999)) {
125
+ reject(new Error('`options.code` must be 1000 or between 3000 and 4999 (inclusive)'));
126
+
127
+ return;
128
+ }
129
+
130
+ options = defaults(options, {
131
+ code: 1000,
132
+ reason: 'Done',
133
+ });
134
+
135
+ const closeTimer = safeSetTimeout(() => {
136
+ try {
137
+ this.logger.info('socket: no close event received, forcing closure');
138
+ resolve(
139
+ this.onclose({
140
+ code: 1000,
141
+ reason: 'Done (forced)',
142
+ })
143
+ );
144
+ } catch (error) {
145
+ this.logger.warn('socket: force-close failed', error);
146
+ }
147
+ }, this.forceCloseDelay);
148
+
149
+ socket.onclose = (event) => {
150
+ this.logger.info('socket: close event fired', event.code, event.reason);
151
+ clearTimeout(closeTimer);
152
+ this.onclose(event);
153
+ resolve(event);
154
+ };
155
+
156
+ socket.close(options.code, options.reason);
157
+ });
158
+ }
159
+
160
+ /**
161
+ * Opens a WebSocket
162
+ * @param {string} url
163
+ * @param {options} options
164
+ * @param {number} options.forceCloseDelay (required)
165
+ * @param {number} options.pingInterval (required)
166
+ * @param {number} options.pongTimeout (required)
167
+ * @param {string} options.token (required)
168
+ * @param {string} options.trackingId (required)
169
+ * @param {Logger} options.logger (required)
170
+ * @param {string} options.logLevelToken
171
+ * @returns {Promise}
172
+ */
173
+ open(url, options) {
174
+ return new Promise((resolve, reject) => {
175
+ /* eslint complexity: [0] */
176
+ if (!url) {
177
+ reject(new Error('`url` is required'));
178
+
179
+ return;
180
+ }
181
+
182
+ if (sockets.get(this)) {
183
+ reject(new Error('Socket#open() can only be called once per instance'));
184
+
185
+ return;
186
+ }
187
+
188
+ options = options || {};
189
+
190
+ checkRequired(
191
+ ['forceCloseDelay', 'pingInterval', 'pongTimeout', 'token', 'trackingId', 'logger'],
192
+ options
193
+ );
194
+
195
+ Object.keys(options).forEach((key) => {
196
+ Reflect.defineProperty(this, key, {
197
+ enumerable: false,
198
+ value: options[key],
199
+ });
200
+ });
201
+
202
+ const WebSocket = Socket.getWebSocketConstructor();
203
+
204
+ this.logger.info('socket: creating WebSocket');
205
+ const socket = new WebSocket(url, [], options);
206
+
207
+ socket.binaryType = 'arraybuffer';
208
+ socket.onmessage = this.onmessage;
209
+
210
+ socket.onclose = (event) => {
211
+ event = this._fixCloseCode(event);
212
+ this.logger.info('socket: closed before open', event.code, event.reason);
213
+ switch (event.code) {
214
+ case 1005:
215
+ // IE 11 doesn't seem to allow 4XXX codes, so if we get a 1005, assume
216
+ // it's a bad websocket url. That'll trigger a device refresh; if it
217
+ // turns out we had a bad token, the device refresh should 401 and
218
+ // trigger a token refresh.
219
+ return reject(new UnknownResponse(event));
220
+ case 4400:
221
+ return reject(new BadRequest(event));
222
+ case 4401:
223
+ return reject(new NotAuthorized(event));
224
+ case 4403:
225
+ return reject(new Forbidden(event));
226
+ // case 4404:
227
+ // return reject(new NotFound(event));
228
+ default:
229
+ return reject(new ConnectionError(event));
230
+ }
231
+ };
232
+
233
+ socket.onopen = () => {
234
+ this.logger.info('socket: connected');
235
+ this._authorize()
236
+ .then(() => {
237
+ this.logger.info('socket: authorized');
238
+ socket.onclose = this.onclose;
239
+ resolve();
240
+ })
241
+ .catch(reject);
242
+ };
243
+
244
+ socket.onerror = (event) => {
245
+ this.logger.warn('socket: error event fired', event);
246
+ };
247
+
248
+ sockets.set(this, socket);
249
+ this.logger.info('socket: waiting for server');
250
+ });
251
+ }
252
+
253
+ /**
254
+ * Handles incoming CloseEvents
255
+ * @param {CloseEvent} event
256
+ * @returns {undefined}
257
+ */
258
+ onclose(event) {
259
+ this.logger.info('socket: closed', event.code, event.reason);
260
+ clearTimeout(this.pongTimer);
261
+ clearTimeout(this.pingTimer);
262
+
263
+ event = this._fixCloseCode(event);
264
+ this.emit('close', event);
265
+
266
+ // Remove all listeners to (a) avoid reacting to late pongs and (b) ensure
267
+ // we don't have a retain cycle.
268
+ this.removeAllListeners();
269
+ }
270
+
271
+ /**
272
+ * Handles incoming message events
273
+ * @param {MessageEvent} event
274
+ * @returns {undefined}
275
+ */
276
+ onmessage(event) {
277
+ try {
278
+ const data = JSON.parse(event.data);
279
+ const sequenceNumber = parseInt(data.sequenceNumber, 10);
280
+
281
+ this.logger.debug('socket: sequence number: ', sequenceNumber);
282
+ if (this.expectedSequenceNumber && sequenceNumber !== this.expectedSequenceNumber) {
283
+ this.logger.debug(
284
+ `socket: sequence number mismatch indicates lost mercury message. expected: ${this.expectedSequenceNumber}, actual: ${sequenceNumber}`
285
+ );
286
+ this.emit('sequence-mismatch', sequenceNumber, this.expectedSequenceNumber);
287
+ }
288
+ this.expectedSequenceNumber = sequenceNumber + 1;
289
+
290
+ // Yes, it's a little weird looking; we want to emit message events that
291
+ // look like normal socket message events, but event.data cannot be
292
+ // modified and we don't actually care about anything but the data property
293
+ const processedEvent = {data};
294
+
295
+ this._acknowledge(processedEvent);
296
+ if (data.type === 'pong') {
297
+ this.emit('pong', processedEvent);
298
+ } else {
299
+ this.emit('message', processedEvent);
300
+ }
301
+ } catch (error) {
302
+ // The above code should only be able to throw if we receive an unparsable
303
+ // message from Mercury. At this time, the only action we have is to
304
+ // ignore it and move on.
305
+ /* istanbul ignore next */
306
+ this.logger.warn('socket: error while receiving WebSocket message', error);
307
+ }
308
+ }
309
+
310
+ /**
311
+ * Sends a message up the socket
312
+ * @param {mixed} data
313
+ * @returns {Promise}
314
+ */
315
+ send(data) {
316
+ return new Promise((resolve, reject) => {
317
+ if (this.readyState !== 1) {
318
+ return reject(new Error('INVALID_STATE_ERROR'));
319
+ }
320
+
321
+ if (isObject(data)) {
322
+ data = JSON.stringify(data);
323
+ }
324
+
325
+ const socket = sockets.get(this);
326
+
327
+ socket.send(data);
328
+
329
+ return resolve();
330
+ });
331
+ }
332
+
333
+ /**
334
+ * Sends an acknowledgment for a specific event
335
+ * @param {MessageEvent} event
336
+ * @returns {Promise}
337
+ */
338
+ _acknowledge(event) {
339
+ if (!event) {
340
+ return Promise.reject(new Error('`event` is required'));
341
+ }
342
+
343
+ if (!has(event, 'data.id')) {
344
+ return Promise.reject(new Error('`event.data.id` is required'));
345
+ }
346
+
347
+ return this.send({
348
+ messageId: event.data.id,
349
+ type: 'ack',
350
+ });
351
+ }
352
+
353
+ /**
354
+ * Sends an auth message up the socket
355
+ * @private
356
+ * @returns {Promise}
357
+ */
358
+ _authorize() {
359
+ return new Promise((resolve) => {
360
+ this.logger.info('socket: authorizing');
361
+ this.send({
362
+ id: uuid.v4(),
363
+ type: 'authorization',
364
+ data: {
365
+ token: this.token,
366
+ },
367
+ trackingId: this.trackingId,
368
+ logLevelToken: this.logLevelToken,
369
+ });
370
+
371
+ const waitForBufferState = (event) => {
372
+ if (
373
+ !event.data.type &&
374
+ (event.data.data.eventType === 'mercury.buffer_state' ||
375
+ event.data.data.eventType === 'mercury.registration_status')
376
+ ) {
377
+ this.removeListener('message', waitForBufferState);
378
+ this._ping();
379
+ resolve();
380
+ }
381
+ };
382
+
383
+ this.once('message', waitForBufferState);
384
+ });
385
+ }
386
+
387
+ /**
388
+ * Deals with the fact that some browsers drop some close codes (but not
389
+ * close reasons).
390
+ * @param {CloseEvent} event
391
+ * @private
392
+ * @returns {CloseEvent}
393
+ */
394
+ _fixCloseCode(event) {
395
+ if (event.code === 1005 && event.reason) {
396
+ switch (event.reason.toLowerCase()) {
397
+ case 'replaced':
398
+ this.logger.info('socket: fixing CloseEvent code for reason: ', event.reason);
399
+ event.code = 4000;
400
+ break;
401
+ case 'authentication failed':
402
+ case 'authentication did not happen within the timeout window of 30000 seconds.':
403
+ this.logger.info('socket: fixing CloseEvent code for reason: ', event.reason);
404
+ event.code = 1008;
405
+ break;
406
+ default:
407
+ // do nothing
408
+ }
409
+ }
410
+
411
+ return event;
412
+ }
413
+
414
+ /**
415
+ * Sends a ping up the socket and confirms we get it back
416
+ * @param {[type]} id
417
+ * @private
418
+ * @returns {[type]}
419
+ */
420
+ _ping(id) {
421
+ const confirmPongId = (event) => {
422
+ try {
423
+ this.logger.debug('socket: pong', event.data.id);
424
+ 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);
427
+ this.close({
428
+ code: 1000,
429
+ reason: 'Pong mismatch',
430
+ });
431
+ }
432
+ } catch (error) {
433
+ // This try/catch block was added as a debugging step; to the best of my
434
+ // knowledge, the above can never throw.
435
+ /* istanbul ignore next */
436
+ this.logger.error('socket: error occurred in confirmPongId', error);
437
+ }
438
+ };
439
+
440
+ const onPongNotReceived = () => {
441
+ try {
442
+ this.logger.info('socket: pong not receive in expected period, closing socket');
443
+ this.close({
444
+ code: 1000,
445
+ reason: 'Pong not received',
446
+ }).catch((reason) => {
447
+ this.logger.warn('socket: failed to close socket after missed pong', reason);
448
+ });
449
+ } catch (error) {
450
+ // This try/catch block was added as a debugging step; to the best of my
451
+ // knowledge, the above can never throw.
452
+ /* istanbul ignore next */
453
+ this.logger.error('socket: error occurred in onPongNotReceived', error);
454
+ }
455
+ };
456
+
457
+ const scheduleNextPingAndCancelPongTimer = () => {
458
+ try {
459
+ clearTimeout(this.pongTimer);
460
+ this.pingTimer = safeSetTimeout(() => this._ping(), this.pingInterval);
461
+ } catch (error) {
462
+ // This try/catch block was added as a debugging step; to the best of my
463
+ // knowledge, the above can never throw.
464
+ /* istanbul ignore next */
465
+ this.logger.error('socket: error occurred in scheduleNextPingAndCancelPongTimer', error);
466
+ }
467
+ };
468
+
469
+ id = id || uuid.v4();
470
+ this.pongTimer = safeSetTimeout(onPongNotReceived, this.pongTimeout);
471
+ this.once('pong', scheduleNextPingAndCancelPongTimer);
472
+ this.once('pong', confirmPongId);
473
+
474
+ this.logger.debug(`socket: ping ${id}`);
475
+
476
+ return this.send({
477
+ id,
478
+ type: 'ping',
479
+ });
480
+ }
481
+ }