@webex/internal-plugin-mercury 2.59.2 → 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.
package/src/mercury.js CHANGED
@@ -1,498 +1,498 @@
1
- /*!
2
- * Copyright (c) 2015-2020 Cisco Systems, Inc. See LICENSE file.
3
- */
4
-
5
- import url from 'url';
6
-
7
- import {WebexPlugin} from '@webex/webex-core';
8
- import {deprecated, oneFlight} from '@webex/common';
9
- import {camelCase, get, set} from 'lodash';
10
- import backoff from 'backoff';
11
-
12
- import Socket from './socket';
13
- import {
14
- BadRequest,
15
- Forbidden,
16
- NotAuthorized,
17
- UnknownResponse,
18
- ConnectionError,
19
- // NotFound
20
- } from './errors';
21
-
22
- const normalReconnectReasons = ['idle', 'done (forced)', 'pong not received', 'pong mismatch'];
23
-
24
- const Mercury = WebexPlugin.extend({
25
- namespace: 'Mercury',
26
-
27
- session: {
28
- connected: {
29
- default: false,
30
- type: 'boolean',
31
- },
32
- connecting: {
33
- default: false,
34
- type: 'boolean',
35
- },
36
- socket: 'object',
37
- localClusterServiceUrls: 'object',
38
- },
39
-
40
- derived: {
41
- listening: {
42
- deps: ['connected'],
43
- fn() {
44
- return this.connected;
45
- },
46
- },
47
- },
48
-
49
- @oneFlight
50
- connect(webSocketUrl) {
51
- if (this.connected) {
52
- this.logger.info('mercury: already connected, will not connect again');
53
-
54
- return Promise.resolve();
55
- }
56
-
57
- this.connecting = true;
58
-
59
- return Promise.resolve(
60
- this.webex.internal.device.registered || this.webex.internal.device.register()
61
- ).then(() => {
62
- this.logger.info('mercury: connecting');
63
-
64
- return this._connectWithBackoff(webSocketUrl);
65
- });
66
- },
67
-
68
- @oneFlight
69
- disconnect() {
70
- return new Promise((resolve) => {
71
- if (this.backoffCall) {
72
- this.logger.info('mercury: aborting connection');
73
- this.backoffCall.abort();
74
- }
75
-
76
- if (this.socket) {
77
- this.socket.removeAllListeners('message');
78
- this.once('offline', resolve);
79
- this.socket.close();
80
-
81
- return;
82
- }
83
-
84
- resolve();
85
- });
86
- },
87
-
88
- @deprecated('Mercury#listen(): Use Mercury#connect() instead')
89
- listen() {
90
- /* eslint no-invalid-this: [0] */
91
- return this.connect();
92
- },
93
-
94
- @deprecated('Mercury#stopListening(): Use Mercury#disconnect() instead')
95
- stopListening() {
96
- /* eslint no-invalid-this: [0] */
97
- return this.disconnect();
98
- },
99
-
100
- processRegistrationStatusEvent(message) {
101
- this.localClusterServiceUrls = message.localClusterServiceUrls;
102
- },
103
-
104
- _applyOverrides(event) {
105
- if (!event || !event.headers) {
106
- return;
107
- }
108
- const headerKeys = Object.keys(event.headers);
109
-
110
- headerKeys.forEach((keyPath) => {
111
- set(event, keyPath, event.headers[keyPath]);
112
- });
113
- },
114
-
115
- _prepareUrl(webSocketUrl) {
116
- if (!webSocketUrl) {
117
- webSocketUrl = this.webex.internal.device.webSocketUrl;
118
- }
119
-
120
- return this.webex.internal.feature
121
- .getFeature('developer', 'web-high-availability')
122
- .then((haMessagingEnabled) => {
123
- if (haMessagingEnabled) {
124
- return this.webex.internal.services.convertUrlToPriorityHostUrl(webSocketUrl);
125
- }
126
-
127
- return webSocketUrl;
128
- })
129
- .then((wsUrl) => {
130
- webSocketUrl = wsUrl;
131
- })
132
- .then(() => this.webex.internal.feature.getFeature('developer', 'web-shared-mercury'))
133
- .then((webSharedMercury) => {
134
- webSocketUrl = url.parse(webSocketUrl, true);
135
- Object.assign(webSocketUrl.query, {
136
- outboundWireFormat: 'text',
137
- bufferStates: true,
138
- aliasHttpStatus: true,
139
- });
140
-
141
- if (webSharedMercury) {
142
- Object.assign(webSocketUrl.query, {
143
- mercuryRegistrationStatus: true,
144
- isRegistrationRefreshEnabled: true,
145
- });
146
- Reflect.deleteProperty(webSocketUrl.query, 'bufferStates');
147
- }
148
-
149
- if (get(this, 'webex.config.device.ephemeral', false)) {
150
- webSocketUrl.query.multipleConnections = true;
151
- }
152
-
153
- return url.format(webSocketUrl);
154
- });
155
- },
156
-
157
- _attemptConnection(socketUrl, callback) {
158
- const socket = new Socket();
159
- let attemptWSUrl;
160
-
161
- socket.on('close', (...args) => this._onclose(...args));
162
- socket.on('message', (...args) => this._onmessage(...args));
163
- socket.on('sequence-mismatch', (...args) => this._emit('sequence-mismatch', ...args));
164
-
165
- Promise.all([this._prepareUrl(socketUrl), this.webex.credentials.getUserToken()])
166
- .then(([webSocketUrl, token]) => {
167
- if (!this.backoffCall) {
168
- const msg = 'mercury: prevent socket open when backoffCall no longer defined';
169
-
170
- this.logger.info(msg);
171
-
172
- return Promise.reject(new Error(msg));
173
- }
174
-
175
- attemptWSUrl = webSocketUrl;
176
-
177
- let options = {
178
- forceCloseDelay: this.config.forceCloseDelay,
179
- pingInterval: this.config.pingInterval,
180
- pongTimeout: this.config.pongTimeout,
181
- token: token.toString(),
182
- trackingId: `${this.webex.sessionId}_${Date.now()}`,
183
- logger: this.logger,
184
- };
185
-
186
- // if the consumer has supplied request options use them
187
- if (this.webex.config.defaultMercuryOptions) {
188
- this.logger.info('mercury: setting custom options');
189
- options = {...options, ...this.webex.config.defaultMercuryOptions};
190
- }
191
-
192
- // Set the socket before opening it. This allows a disconnect() to close
193
- // the socket if it is in the process of being opened.
194
- this.socket = socket;
195
-
196
- return socket.open(webSocketUrl, options);
197
- })
198
- .then(() => {
199
- this.webex.internal.metrics.submitClientMetrics('web-ha-mercury', {
200
- fields: {
201
- success: true,
202
- },
203
- tags: {
204
- action: 'connected',
205
- url: attemptWSUrl,
206
- },
207
- });
208
- callback();
209
-
210
- return this.webex.internal.feature
211
- .getFeature('developer', 'web-high-availability')
212
- .then((haMessagingEnabled) => {
213
- if (haMessagingEnabled) {
214
- return this.webex.internal.device.refresh();
215
- }
216
-
217
- return Promise.resolve();
218
- });
219
- })
220
- .catch((reason) => {
221
- // Suppress connection errors that appear to be network related. This
222
- // may end up suppressing metrics during outages, but we might not care
223
- // (especially since many of our outages happen in a way that client
224
- // metrics can't be trusted).
225
- if (reason.code !== 1006 && this.backoffCall && this.backoffCall.getNumRetries() > 0) {
226
- this._emit('connection_failed', reason, {retries: this.backoffCall.getNumRetries()});
227
- }
228
- this.logger.info('mercury: connection attempt failed', reason);
229
- // UnknownResponse is produced by IE for any 4XXX; treated it like a bad
230
- // web socket url and let WDM handle the token checking
231
- if (reason instanceof UnknownResponse) {
232
- this.logger.info(
233
- 'mercury: received unknown response code, refreshing device registration'
234
- );
235
-
236
- return this.webex.internal.device.refresh().then(() => callback(reason));
237
- }
238
- // NotAuthorized implies expired token
239
- if (reason instanceof NotAuthorized) {
240
- this.logger.info('mercury: received authorization error, reauthorizing');
241
-
242
- return this.webex.credentials.refresh({force: true}).then(() => callback(reason));
243
- }
244
- // // NotFound implies expired web socket url
245
- // else if (reason instanceof NotFound) {
246
- // this.logger.info(`mercury: received not found error, refreshing device registration`);
247
- // return this.webex.internal.device.refresh()
248
- // .then(() => callback(reason));
249
- // }
250
- // BadRequest implies current credentials are for a Service Account
251
- // Forbidden implies current user is not entitle for Webex
252
- if (reason instanceof BadRequest || reason instanceof Forbidden) {
253
- this.logger.warn('mercury: received unrecoverable response from mercury');
254
- this.backoffCall.abort();
255
-
256
- return callback(reason);
257
- }
258
- if (reason instanceof ConnectionError) {
259
- return this.webex.internal.feature
260
- .getFeature('developer', 'web-high-availability')
261
- .then((haMessagingEnabled) => {
262
- if (haMessagingEnabled) {
263
- this.logger.info(
264
- 'mercury: received a generic connection error, will try to connect to another datacenter'
265
- );
266
- this.webex.internal.metrics.submitClientMetrics('web-ha-mercury', {
267
- fields: {
268
- success: false,
269
- },
270
- tags: {
271
- action: 'failed',
272
- error: reason.message,
273
- url: attemptWSUrl,
274
- },
275
- });
276
-
277
- return this.webex.internal.services.markFailedUrl(attemptWSUrl);
278
- }
279
-
280
- return null;
281
- })
282
- .then(() => callback(reason));
283
- }
284
-
285
- return callback(reason);
286
- })
287
- .catch((reason) => {
288
- this.logger.error('mercury: failed to handle connection failure', reason);
289
- callback(reason);
290
- });
291
- },
292
-
293
- _connectWithBackoff(webSocketUrl) {
294
- return new Promise((resolve, reject) => {
295
- // eslint gets confused about whether or not call is actually used
296
- // eslint-disable-next-line prefer-const
297
- let call;
298
- const onComplete = (err) => {
299
- this.connecting = false;
300
-
301
- this.backoffCall = undefined;
302
- if (err) {
303
- this.logger.info(
304
- `mercury: failed to connect after ${call.getNumRetries()} retries; log statement about next retry was inaccurate; ${err}`
305
- );
306
-
307
- return reject(err);
308
- }
309
- this.connected = true;
310
- this._emit('online');
311
-
312
- return resolve();
313
- };
314
-
315
- // eslint-disable-next-line prefer-reflect
316
- call = backoff.call((callback) => {
317
- this.logger.info(`mercury: executing connection attempt ${call.getNumRetries()}`);
318
- this._attemptConnection(webSocketUrl, callback);
319
- }, onComplete);
320
-
321
- call.setStrategy(
322
- new backoff.ExponentialStrategy({
323
- initialDelay: this.config.backoffTimeReset,
324
- maxDelay: this.config.backoffTimeMax,
325
- })
326
- );
327
-
328
- if (this.config.maxRetries) {
329
- call.failAfter(this.config.maxRetries);
330
- }
331
-
332
- call.on('abort', () => {
333
- this.logger.info('mercury: connection aborted');
334
- reject(new Error('Mercury Connection Aborted'));
335
- });
336
-
337
- call.on('callback', (err) => {
338
- if (err) {
339
- const number = call.getNumRetries();
340
- const delay = Math.min(call.strategy_.nextBackoffDelay_, this.config.backoffTimeMax);
341
-
342
- this.logger.info(
343
- `mercury: failed to connect; attempting retry ${number + 1} in ${delay} ms`
344
- );
345
- /* istanbul ignore if */
346
- if (process.env.NODE_ENV === 'development') {
347
- this.logger.debug('mercury: ', err, err.stack);
348
- }
349
-
350
- return;
351
- }
352
- this.logger.info('mercury: connected');
353
- });
354
-
355
- call.start();
356
-
357
- this.backoffCall = call;
358
- });
359
- },
360
-
361
- _emit(...args) {
362
- try {
363
- this.trigger(...args);
364
- } catch (error) {
365
- this.logger.error('mercury: error occurred in event handler', error);
366
- }
367
- },
368
-
369
- _getEventHandlers(eventType) {
370
- const [namespace, name] = eventType.split('.');
371
- const handlers = [];
372
-
373
- if (!this.webex[namespace] && !this.webex.internal[namespace]) {
374
- return handlers;
375
- }
376
-
377
- const handlerName = camelCase(`process_${name}_event`);
378
-
379
- if ((this.webex[namespace] || this.webex.internal[namespace])[handlerName]) {
380
- handlers.push({
381
- name: handlerName,
382
- namespace,
383
- });
384
- }
385
-
386
- return handlers;
387
- },
388
-
389
- _onclose(event) {
390
- // I don't see any way to avoid the complexity or statement count in here.
391
- /* eslint complexity: [0] */
392
-
393
- try {
394
- const reason = event.reason && event.reason.toLowerCase();
395
- const socketUrl = this.socket.url;
396
-
397
- this.socket.removeAllListeners();
398
- this.unset('socket');
399
- this.connected = false;
400
- this._emit('offline', event);
401
-
402
- switch (event.code) {
403
- case 1003:
404
- // metric: disconnect
405
- this.logger.info(
406
- `mercury: Mercury service rejected last message; will not reconnect: ${event.reason}`
407
- );
408
- this._emit('offline.permanent', event);
409
- break;
410
- case 4000:
411
- // metric: disconnect
412
- this.logger.info('mercury: socket replaced; will not reconnect');
413
- this._emit('offline.replaced', event);
414
- break;
415
- case 1001:
416
- case 1005:
417
- case 1006:
418
- case 1011:
419
- this.logger.info('mercury: socket disconnected; reconnecting');
420
- this._emit('offline.transient', event);
421
- this._reconnect(socketUrl);
422
- // metric: disconnect
423
- // if (code == 1011 && reason !== ping error) metric: unexpected disconnect
424
- break;
425
- case 1000:
426
- if (normalReconnectReasons.includes(reason)) {
427
- this.logger.info('mercury: socket disconnected; reconnecting');
428
- this._emit('offline.transient', event);
429
- this._reconnect(socketUrl);
430
- // metric: disconnect
431
- // if (reason === done forced) metric: force closure
432
- } else {
433
- this.logger.info('mercury: socket disconnected; will not reconnect');
434
- this._emit('offline.permanent', event);
435
- }
436
- break;
437
- default:
438
- this.logger.info('mercury: socket disconnected unexpectedly; will not reconnect');
439
- // unexpected disconnect
440
- this._emit('offline.permanent', event);
441
- }
442
- } catch (error) {
443
- this.logger.error('mercury: error occurred in close handler', error);
444
- }
445
- },
446
-
447
- _onmessage(event) {
448
- const envelope = event.data;
449
-
450
- if (process.env.ENABLE_MERCURY_LOGGING) {
451
- this.logger.debug('mercury: message envelope: ', envelope);
452
- }
453
-
454
- const {data} = envelope;
455
-
456
- this._applyOverrides(data);
457
-
458
- return this._getEventHandlers(data.eventType)
459
- .reduce(
460
- (promise, handler) =>
461
- promise.then(() => {
462
- const {namespace, name} = handler;
463
-
464
- return new Promise((resolve) =>
465
- resolve((this.webex[namespace] || this.webex.internal[namespace])[name](data))
466
- ).catch((reason) =>
467
- this.logger.error(
468
- `mercury: error occurred in autowired event handler for ${data.eventType}`,
469
- reason
470
- )
471
- );
472
- }),
473
- Promise.resolve()
474
- )
475
- .then(() => {
476
- this._emit('event', event.data);
477
- const [namespace] = data.eventType.split('.');
478
-
479
- if (namespace === data.eventType) {
480
- this._emit(`event:${namespace}`, envelope);
481
- } else {
482
- this._emit(`event:${namespace}`, envelope);
483
- this._emit(`event:${data.eventType}`, envelope);
484
- }
485
- })
486
- .catch((reason) => {
487
- this.logger.error('mercury: error occurred processing socket message', reason);
488
- });
489
- },
490
-
491
- _reconnect(webSocketUrl) {
492
- this.logger.info('mercury: reconnecting');
493
-
494
- return this.connect(webSocketUrl);
495
- },
496
- });
497
-
498
- export default Mercury;
1
+ /*!
2
+ * Copyright (c) 2015-2020 Cisco Systems, Inc. See LICENSE file.
3
+ */
4
+
5
+ import url from 'url';
6
+
7
+ import {WebexPlugin} from '@webex/webex-core';
8
+ import {deprecated, oneFlight} from '@webex/common';
9
+ import {camelCase, get, set} from 'lodash';
10
+ import backoff from 'backoff';
11
+
12
+ import Socket from './socket';
13
+ import {
14
+ BadRequest,
15
+ Forbidden,
16
+ NotAuthorized,
17
+ UnknownResponse,
18
+ ConnectionError,
19
+ // NotFound
20
+ } from './errors';
21
+
22
+ const normalReconnectReasons = ['idle', 'done (forced)', 'pong not received', 'pong mismatch'];
23
+
24
+ const Mercury = WebexPlugin.extend({
25
+ namespace: 'Mercury',
26
+
27
+ session: {
28
+ connected: {
29
+ default: false,
30
+ type: 'boolean',
31
+ },
32
+ connecting: {
33
+ default: false,
34
+ type: 'boolean',
35
+ },
36
+ socket: 'object',
37
+ localClusterServiceUrls: 'object',
38
+ },
39
+
40
+ derived: {
41
+ listening: {
42
+ deps: ['connected'],
43
+ fn() {
44
+ return this.connected;
45
+ },
46
+ },
47
+ },
48
+
49
+ @oneFlight
50
+ connect(webSocketUrl) {
51
+ if (this.connected) {
52
+ this.logger.info('mercury: already connected, will not connect again');
53
+
54
+ return Promise.resolve();
55
+ }
56
+
57
+ this.connecting = true;
58
+
59
+ return Promise.resolve(
60
+ this.webex.internal.device.registered || this.webex.internal.device.register()
61
+ ).then(() => {
62
+ this.logger.info('mercury: connecting');
63
+
64
+ return this._connectWithBackoff(webSocketUrl);
65
+ });
66
+ },
67
+
68
+ @oneFlight
69
+ disconnect() {
70
+ return new Promise((resolve) => {
71
+ if (this.backoffCall) {
72
+ this.logger.info('mercury: aborting connection');
73
+ this.backoffCall.abort();
74
+ }
75
+
76
+ if (this.socket) {
77
+ this.socket.removeAllListeners('message');
78
+ this.once('offline', resolve);
79
+ this.socket.close();
80
+
81
+ return;
82
+ }
83
+
84
+ resolve();
85
+ });
86
+ },
87
+
88
+ @deprecated('Mercury#listen(): Use Mercury#connect() instead')
89
+ listen() {
90
+ /* eslint no-invalid-this: [0] */
91
+ return this.connect();
92
+ },
93
+
94
+ @deprecated('Mercury#stopListening(): Use Mercury#disconnect() instead')
95
+ stopListening() {
96
+ /* eslint no-invalid-this: [0] */
97
+ return this.disconnect();
98
+ },
99
+
100
+ processRegistrationStatusEvent(message) {
101
+ this.localClusterServiceUrls = message.localClusterServiceUrls;
102
+ },
103
+
104
+ _applyOverrides(event) {
105
+ if (!event || !event.headers) {
106
+ return;
107
+ }
108
+ const headerKeys = Object.keys(event.headers);
109
+
110
+ headerKeys.forEach((keyPath) => {
111
+ set(event, keyPath, event.headers[keyPath]);
112
+ });
113
+ },
114
+
115
+ _prepareUrl(webSocketUrl) {
116
+ if (!webSocketUrl) {
117
+ webSocketUrl = this.webex.internal.device.webSocketUrl;
118
+ }
119
+
120
+ return this.webex.internal.feature
121
+ .getFeature('developer', 'web-high-availability')
122
+ .then((haMessagingEnabled) => {
123
+ if (haMessagingEnabled) {
124
+ return this.webex.internal.services.convertUrlToPriorityHostUrl(webSocketUrl);
125
+ }
126
+
127
+ return webSocketUrl;
128
+ })
129
+ .then((wsUrl) => {
130
+ webSocketUrl = wsUrl;
131
+ })
132
+ .then(() => this.webex.internal.feature.getFeature('developer', 'web-shared-mercury'))
133
+ .then((webSharedMercury) => {
134
+ webSocketUrl = url.parse(webSocketUrl, true);
135
+ Object.assign(webSocketUrl.query, {
136
+ outboundWireFormat: 'text',
137
+ bufferStates: true,
138
+ aliasHttpStatus: true,
139
+ });
140
+
141
+ if (webSharedMercury) {
142
+ Object.assign(webSocketUrl.query, {
143
+ mercuryRegistrationStatus: true,
144
+ isRegistrationRefreshEnabled: true,
145
+ });
146
+ Reflect.deleteProperty(webSocketUrl.query, 'bufferStates');
147
+ }
148
+
149
+ if (get(this, 'webex.config.device.ephemeral', false)) {
150
+ webSocketUrl.query.multipleConnections = true;
151
+ }
152
+
153
+ return url.format(webSocketUrl);
154
+ });
155
+ },
156
+
157
+ _attemptConnection(socketUrl, callback) {
158
+ const socket = new Socket();
159
+ let attemptWSUrl;
160
+
161
+ socket.on('close', (...args) => this._onclose(...args));
162
+ socket.on('message', (...args) => this._onmessage(...args));
163
+ socket.on('sequence-mismatch', (...args) => this._emit('sequence-mismatch', ...args));
164
+
165
+ Promise.all([this._prepareUrl(socketUrl), this.webex.credentials.getUserToken()])
166
+ .then(([webSocketUrl, token]) => {
167
+ if (!this.backoffCall) {
168
+ const msg = 'mercury: prevent socket open when backoffCall no longer defined';
169
+
170
+ this.logger.info(msg);
171
+
172
+ return Promise.reject(new Error(msg));
173
+ }
174
+
175
+ attemptWSUrl = webSocketUrl;
176
+
177
+ let options = {
178
+ forceCloseDelay: this.config.forceCloseDelay,
179
+ pingInterval: this.config.pingInterval,
180
+ pongTimeout: this.config.pongTimeout,
181
+ token: token.toString(),
182
+ trackingId: `${this.webex.sessionId}_${Date.now()}`,
183
+ logger: this.logger,
184
+ };
185
+
186
+ // if the consumer has supplied request options use them
187
+ if (this.webex.config.defaultMercuryOptions) {
188
+ this.logger.info('mercury: setting custom options');
189
+ options = {...options, ...this.webex.config.defaultMercuryOptions};
190
+ }
191
+
192
+ // Set the socket before opening it. This allows a disconnect() to close
193
+ // the socket if it is in the process of being opened.
194
+ this.socket = socket;
195
+
196
+ return socket.open(webSocketUrl, options);
197
+ })
198
+ .then(() => {
199
+ this.webex.internal.metrics.submitClientMetrics('web-ha-mercury', {
200
+ fields: {
201
+ success: true,
202
+ },
203
+ tags: {
204
+ action: 'connected',
205
+ url: attemptWSUrl,
206
+ },
207
+ });
208
+ callback();
209
+
210
+ return this.webex.internal.feature
211
+ .getFeature('developer', 'web-high-availability')
212
+ .then((haMessagingEnabled) => {
213
+ if (haMessagingEnabled) {
214
+ return this.webex.internal.device.refresh();
215
+ }
216
+
217
+ return Promise.resolve();
218
+ });
219
+ })
220
+ .catch((reason) => {
221
+ // Suppress connection errors that appear to be network related. This
222
+ // may end up suppressing metrics during outages, but we might not care
223
+ // (especially since many of our outages happen in a way that client
224
+ // metrics can't be trusted).
225
+ if (reason.code !== 1006 && this.backoffCall && this.backoffCall.getNumRetries() > 0) {
226
+ this._emit('connection_failed', reason, {retries: this.backoffCall.getNumRetries()});
227
+ }
228
+ this.logger.info('mercury: connection attempt failed', reason);
229
+ // UnknownResponse is produced by IE for any 4XXX; treated it like a bad
230
+ // web socket url and let WDM handle the token checking
231
+ if (reason instanceof UnknownResponse) {
232
+ this.logger.info(
233
+ 'mercury: received unknown response code, refreshing device registration'
234
+ );
235
+
236
+ return this.webex.internal.device.refresh().then(() => callback(reason));
237
+ }
238
+ // NotAuthorized implies expired token
239
+ if (reason instanceof NotAuthorized) {
240
+ this.logger.info('mercury: received authorization error, reauthorizing');
241
+
242
+ return this.webex.credentials.refresh({force: true}).then(() => callback(reason));
243
+ }
244
+ // // NotFound implies expired web socket url
245
+ // else if (reason instanceof NotFound) {
246
+ // this.logger.info(`mercury: received not found error, refreshing device registration`);
247
+ // return this.webex.internal.device.refresh()
248
+ // .then(() => callback(reason));
249
+ // }
250
+ // BadRequest implies current credentials are for a Service Account
251
+ // Forbidden implies current user is not entitle for Webex
252
+ if (reason instanceof BadRequest || reason instanceof Forbidden) {
253
+ this.logger.warn('mercury: received unrecoverable response from mercury');
254
+ this.backoffCall.abort();
255
+
256
+ return callback(reason);
257
+ }
258
+ if (reason instanceof ConnectionError) {
259
+ return this.webex.internal.feature
260
+ .getFeature('developer', 'web-high-availability')
261
+ .then((haMessagingEnabled) => {
262
+ if (haMessagingEnabled) {
263
+ this.logger.info(
264
+ 'mercury: received a generic connection error, will try to connect to another datacenter'
265
+ );
266
+ this.webex.internal.metrics.submitClientMetrics('web-ha-mercury', {
267
+ fields: {
268
+ success: false,
269
+ },
270
+ tags: {
271
+ action: 'failed',
272
+ error: reason.message,
273
+ url: attemptWSUrl,
274
+ },
275
+ });
276
+
277
+ return this.webex.internal.services.markFailedUrl(attemptWSUrl);
278
+ }
279
+
280
+ return null;
281
+ })
282
+ .then(() => callback(reason));
283
+ }
284
+
285
+ return callback(reason);
286
+ })
287
+ .catch((reason) => {
288
+ this.logger.error('mercury: failed to handle connection failure', reason);
289
+ callback(reason);
290
+ });
291
+ },
292
+
293
+ _connectWithBackoff(webSocketUrl) {
294
+ return new Promise((resolve, reject) => {
295
+ // eslint gets confused about whether or not call is actually used
296
+ // eslint-disable-next-line prefer-const
297
+ let call;
298
+ const onComplete = (err) => {
299
+ this.connecting = false;
300
+
301
+ this.backoffCall = undefined;
302
+ if (err) {
303
+ this.logger.info(
304
+ `mercury: failed to connect after ${call.getNumRetries()} retries; log statement about next retry was inaccurate; ${err}`
305
+ );
306
+
307
+ return reject(err);
308
+ }
309
+ this.connected = true;
310
+ this._emit('online');
311
+
312
+ return resolve();
313
+ };
314
+
315
+ // eslint-disable-next-line prefer-reflect
316
+ call = backoff.call((callback) => {
317
+ this.logger.info(`mercury: executing connection attempt ${call.getNumRetries()}`);
318
+ this._attemptConnection(webSocketUrl, callback);
319
+ }, onComplete);
320
+
321
+ call.setStrategy(
322
+ new backoff.ExponentialStrategy({
323
+ initialDelay: this.config.backoffTimeReset,
324
+ maxDelay: this.config.backoffTimeMax,
325
+ })
326
+ );
327
+
328
+ if (this.config.maxRetries) {
329
+ call.failAfter(this.config.maxRetries);
330
+ }
331
+
332
+ call.on('abort', () => {
333
+ this.logger.info('mercury: connection aborted');
334
+ reject(new Error('Mercury Connection Aborted'));
335
+ });
336
+
337
+ call.on('callback', (err) => {
338
+ if (err) {
339
+ const number = call.getNumRetries();
340
+ const delay = Math.min(call.strategy_.nextBackoffDelay_, this.config.backoffTimeMax);
341
+
342
+ this.logger.info(
343
+ `mercury: failed to connect; attempting retry ${number + 1} in ${delay} ms`
344
+ );
345
+ /* istanbul ignore if */
346
+ if (process.env.NODE_ENV === 'development') {
347
+ this.logger.debug('mercury: ', err, err.stack);
348
+ }
349
+
350
+ return;
351
+ }
352
+ this.logger.info('mercury: connected');
353
+ });
354
+
355
+ call.start();
356
+
357
+ this.backoffCall = call;
358
+ });
359
+ },
360
+
361
+ _emit(...args) {
362
+ try {
363
+ this.trigger(...args);
364
+ } catch (error) {
365
+ this.logger.error('mercury: error occurred in event handler', error);
366
+ }
367
+ },
368
+
369
+ _getEventHandlers(eventType) {
370
+ const [namespace, name] = eventType.split('.');
371
+ const handlers = [];
372
+
373
+ if (!this.webex[namespace] && !this.webex.internal[namespace]) {
374
+ return handlers;
375
+ }
376
+
377
+ const handlerName = camelCase(`process_${name}_event`);
378
+
379
+ if ((this.webex[namespace] || this.webex.internal[namespace])[handlerName]) {
380
+ handlers.push({
381
+ name: handlerName,
382
+ namespace,
383
+ });
384
+ }
385
+
386
+ return handlers;
387
+ },
388
+
389
+ _onclose(event) {
390
+ // I don't see any way to avoid the complexity or statement count in here.
391
+ /* eslint complexity: [0] */
392
+
393
+ try {
394
+ const reason = event.reason && event.reason.toLowerCase();
395
+ const socketUrl = this.socket.url;
396
+
397
+ this.socket.removeAllListeners();
398
+ this.unset('socket');
399
+ this.connected = false;
400
+ this._emit('offline', event);
401
+
402
+ switch (event.code) {
403
+ case 1003:
404
+ // metric: disconnect
405
+ this.logger.info(
406
+ `mercury: Mercury service rejected last message; will not reconnect: ${event.reason}`
407
+ );
408
+ this._emit('offline.permanent', event);
409
+ break;
410
+ case 4000:
411
+ // metric: disconnect
412
+ this.logger.info('mercury: socket replaced; will not reconnect');
413
+ this._emit('offline.replaced', event);
414
+ break;
415
+ case 1001:
416
+ case 1005:
417
+ case 1006:
418
+ case 1011:
419
+ this.logger.info('mercury: socket disconnected; reconnecting');
420
+ this._emit('offline.transient', event);
421
+ this._reconnect(socketUrl);
422
+ // metric: disconnect
423
+ // if (code == 1011 && reason !== ping error) metric: unexpected disconnect
424
+ break;
425
+ case 1000:
426
+ if (normalReconnectReasons.includes(reason)) {
427
+ this.logger.info('mercury: socket disconnected; reconnecting');
428
+ this._emit('offline.transient', event);
429
+ this._reconnect(socketUrl);
430
+ // metric: disconnect
431
+ // if (reason === done forced) metric: force closure
432
+ } else {
433
+ this.logger.info('mercury: socket disconnected; will not reconnect');
434
+ this._emit('offline.permanent', event);
435
+ }
436
+ break;
437
+ default:
438
+ this.logger.info('mercury: socket disconnected unexpectedly; will not reconnect');
439
+ // unexpected disconnect
440
+ this._emit('offline.permanent', event);
441
+ }
442
+ } catch (error) {
443
+ this.logger.error('mercury: error occurred in close handler', error);
444
+ }
445
+ },
446
+
447
+ _onmessage(event) {
448
+ const envelope = event.data;
449
+
450
+ if (process.env.ENABLE_MERCURY_LOGGING) {
451
+ this.logger.debug('mercury: message envelope: ', envelope);
452
+ }
453
+
454
+ const {data} = envelope;
455
+
456
+ this._applyOverrides(data);
457
+
458
+ return this._getEventHandlers(data.eventType)
459
+ .reduce(
460
+ (promise, handler) =>
461
+ promise.then(() => {
462
+ const {namespace, name} = handler;
463
+
464
+ return new Promise((resolve) =>
465
+ resolve((this.webex[namespace] || this.webex.internal[namespace])[name](data))
466
+ ).catch((reason) =>
467
+ this.logger.error(
468
+ `mercury: error occurred in autowired event handler for ${data.eventType}`,
469
+ reason
470
+ )
471
+ );
472
+ }),
473
+ Promise.resolve()
474
+ )
475
+ .then(() => {
476
+ this._emit('event', event.data);
477
+ const [namespace] = data.eventType.split('.');
478
+
479
+ if (namespace === data.eventType) {
480
+ this._emit(`event:${namespace}`, envelope);
481
+ } else {
482
+ this._emit(`event:${namespace}`, envelope);
483
+ this._emit(`event:${data.eventType}`, envelope);
484
+ }
485
+ })
486
+ .catch((reason) => {
487
+ this.logger.error('mercury: error occurred processing socket message', reason);
488
+ });
489
+ },
490
+
491
+ _reconnect(webSocketUrl) {
492
+ this.logger.info('mercury: reconnecting');
493
+
494
+ return this.connect(webSocketUrl);
495
+ },
496
+ });
497
+
498
+ export default Mercury;