@webex/internal-plugin-mercury 3.9.0-next.6 → 3.9.0-next.7

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/dist/mercury.js CHANGED
@@ -11,11 +11,11 @@ _Object$defineProperty(exports, "__esModule", {
11
11
  value: true
12
12
  });
13
13
  exports.default = void 0;
14
+ var _now = _interopRequireDefault(require("@babel/runtime-corejs2/core-js/date/now"));
14
15
  var _promise = _interopRequireDefault(require("@babel/runtime-corejs2/core-js/promise"));
15
16
  var _keys = _interopRequireDefault(require("@babel/runtime-corejs2/core-js/object/keys"));
16
17
  var _assign = _interopRequireDefault(require("@babel/runtime-corejs2/core-js/object/assign"));
17
18
  var _deleteProperty = _interopRequireDefault(require("@babel/runtime-corejs2/core-js/reflect/delete-property"));
18
- var _now = _interopRequireDefault(require("@babel/runtime-corejs2/core-js/date/now"));
19
19
  var _getOwnPropertyDescriptor = _interopRequireDefault(require("@babel/runtime-corejs2/core-js/object/get-own-property-descriptor"));
20
20
  var _defineProperty2 = _interopRequireDefault(require("@babel/runtime-corejs2/helpers/defineProperty"));
21
21
  var _slicedToArray2 = _interopRequireDefault(require("@babel/runtime-corejs2/helpers/slicedToArray"));
@@ -102,6 +102,87 @@ var Mercury = _webexCore.WebexPlugin.extend((_dec = (0, _common.deprecated)('Mer
102
102
  }
103
103
  });
104
104
  },
105
+ /**
106
+ * Attach event listeners to a socket.
107
+ * @param {Socket} socket - The socket to attach listeners to
108
+ * @returns {void}
109
+ */
110
+ _attachSocketEventListeners: function _attachSocketEventListeners(socket) {
111
+ var _this2 = this;
112
+ socket.on('close', function (event) {
113
+ return _this2._onclose(event, socket);
114
+ });
115
+ socket.on('message', function () {
116
+ return _this2._onmessage.apply(_this2, arguments);
117
+ });
118
+ socket.on('pong', function () {
119
+ return _this2._setTimeOffset.apply(_this2, arguments);
120
+ });
121
+ socket.on('sequence-mismatch', function () {
122
+ for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
123
+ args[_key] = arguments[_key];
124
+ }
125
+ return _this2._emit.apply(_this2, ['sequence-mismatch'].concat(args));
126
+ });
127
+ socket.on('ping-pong-latency', function () {
128
+ for (var _len2 = arguments.length, args = new Array(_len2), _key2 = 0; _key2 < _len2; _key2++) {
129
+ args[_key2] = arguments[_key2];
130
+ }
131
+ return _this2._emit.apply(_this2, ['ping-pong-latency'].concat(args));
132
+ });
133
+ },
134
+ /**
135
+ * Handle imminent shutdown by establishing a new connection while keeping
136
+ * the current one alive (make-before-break).
137
+ * Idempotent: will no-op if already in progress.
138
+ * @returns {void}
139
+ */
140
+ _handleImminentShutdown: function _handleImminentShutdown() {
141
+ var _this3 = this;
142
+ try {
143
+ if (this._shutdownSwitchoverInProgress) {
144
+ this.logger.info("".concat(this.namespace, ": [shutdown] switchover already in progress"));
145
+ return;
146
+ }
147
+ this._shutdownSwitchoverInProgress = true;
148
+ this._shutdownSwitchoverId = "".concat((0, _now.default)());
149
+ this.logger.info("".concat(this.namespace, ": [shutdown] switchover start, id=").concat(this._shutdownSwitchoverId));
150
+ this._connectWithBackoff(undefined, {
151
+ isShutdownSwitchover: true,
152
+ attemptOptions: {
153
+ isShutdownSwitchover: true,
154
+ onSuccess: function onSuccess(newSocket, webSocketUrl) {
155
+ _this3.logger.info("".concat(_this3.namespace, ": [shutdown] switchover connected, url: ").concat(webSocketUrl));
156
+ var oldSocket = _this3.socket;
157
+ // Atomically switch active socket reference
158
+ _this3.socket = newSocket;
159
+ _this3.connected = true; // remain connected throughout
160
+
161
+ _this3._emit('event:mercury_shutdown_switchover_complete', {
162
+ url: webSocketUrl
163
+ });
164
+ if (oldSocket) {
165
+ _this3.logger.info("".concat(_this3.namespace, ": [shutdown] old socket retained; server will close with 4001"));
166
+ }
167
+ }
168
+ }
169
+ }).then(function () {
170
+ _this3.logger.info("".concat(_this3.namespace, ": [shutdown] switchover completed successfully"));
171
+ }).catch(function (err) {
172
+ _this3.logger.info("".concat(_this3.namespace, ": [shutdown] switchover exhausted retries; will fall back to normal reconnection"), err);
173
+ _this3._emit('event:mercury_shutdown_switchover_failed', {
174
+ reason: err
175
+ });
176
+ // Old socket will eventually close with 4001, triggering normal reconnection
177
+ });
178
+ } catch (e) {
179
+ this.logger.error("".concat(this.namespace, ": [shutdown] error during switchover"), e);
180
+ this._shutdownSwitchoverInProgress = false;
181
+ this._emit('event:mercury_shutdown_switchover_failed', {
182
+ reason: e
183
+ });
184
+ }
185
+ },
105
186
  /**
106
187
  * Get the last error.
107
188
  * @returns {any} The last error.
@@ -110,7 +191,7 @@ var Mercury = _webexCore.WebexPlugin.extend((_dec = (0, _common.deprecated)('Mer
110
191
  return this.lastError;
111
192
  },
112
193
  connect: function connect(webSocketUrl) {
113
- var _this2 = this;
194
+ var _this4 = this;
114
195
  if (this.connected) {
115
196
  this.logger.info("".concat(this.namespace, ": already connected, will not connect again"));
116
197
  return _promise.default.resolve();
@@ -119,8 +200,8 @@ var Mercury = _webexCore.WebexPlugin.extend((_dec = (0, _common.deprecated)('Mer
119
200
  this.logger.info("".concat(this.namespace, ": starting connection attempt"));
120
201
  this.logger.info("".concat(this.namespace, ": debug_mercury_logging stack: "), new Error('debug_mercury_logging').stack);
121
202
  return _promise.default.resolve(this.webex.internal.device.registered || this.webex.internal.device.register()).then(function () {
122
- _this2.logger.info("".concat(_this2.namespace, ": connecting"));
123
- return _this2._connectWithBackoff(webSocketUrl);
203
+ _this4.logger.info("".concat(_this4.namespace, ": connecting"));
204
+ return _this4._connectWithBackoff(webSocketUrl);
124
205
  });
125
206
  },
126
207
  logout: function logout() {
@@ -132,16 +213,20 @@ var Mercury = _webexCore.WebexPlugin.extend((_dec = (0, _common.deprecated)('Mer
132
213
  } : undefined);
133
214
  },
134
215
  disconnect: function disconnect(options) {
135
- var _this3 = this;
216
+ var _this5 = this;
136
217
  return new _promise.default(function (resolve) {
137
- if (_this3.backoffCall) {
138
- _this3.logger.info("".concat(_this3.namespace, ": aborting connection"));
139
- _this3.backoffCall.abort();
218
+ if (_this5.backoffCall) {
219
+ _this5.logger.info("".concat(_this5.namespace, ": aborting connection"));
220
+ _this5.backoffCall.abort();
140
221
  }
141
- if (_this3.socket) {
142
- _this3.socket.removeAllListeners('message');
143
- _this3.once('offline', resolve);
144
- resolve(_this3.socket.close(options || undefined));
222
+ if (_this5._shutdownSwitchoverBackoffCall) {
223
+ _this5.logger.info("".concat(_this5.namespace, ": aborting shutdown switchover"));
224
+ _this5._shutdownSwitchoverBackoffCall.abort();
225
+ }
226
+ if (_this5.socket) {
227
+ _this5.socket.removeAllListeners('message');
228
+ _this5.once('offline', resolve);
229
+ resolve(_this5.socket.close(options || undefined));
145
230
  }
146
231
  resolve();
147
232
  });
@@ -167,19 +252,19 @@ var Mercury = _webexCore.WebexPlugin.extend((_dec = (0, _common.deprecated)('Mer
167
252
  });
168
253
  },
169
254
  _prepareUrl: function _prepareUrl(webSocketUrl) {
170
- var _this4 = this;
255
+ var _this6 = this;
171
256
  if (!webSocketUrl) {
172
257
  webSocketUrl = this.webex.internal.device.webSocketUrl;
173
258
  }
174
259
  return this.webex.internal.feature.getFeature('developer', 'web-high-availability').then(function (haMessagingEnabled) {
175
260
  if (haMessagingEnabled) {
176
- return _this4.webex.internal.services.convertUrlToPriorityHostUrl(webSocketUrl);
261
+ return _this6.webex.internal.services.convertUrlToPriorityHostUrl(webSocketUrl);
177
262
  }
178
263
  return webSocketUrl;
179
264
  }).then(function (wsUrl) {
180
265
  webSocketUrl = wsUrl;
181
266
  }).then(function () {
182
- return _this4.webex.internal.feature.getFeature('developer', 'web-shared-mercury');
267
+ return _this6.webex.internal.feature.getFeature('developer', 'web-shared-mercury');
183
268
  }).then(function (webSharedMercury) {
184
269
  webSocketUrl = _url.default.parse(webSocketUrl, true);
185
270
  (0, _assign.default)(webSocketUrl.query, {
@@ -194,7 +279,7 @@ var Mercury = _webexCore.WebexPlugin.extend((_dec = (0, _common.deprecated)('Mer
194
279
  });
195
280
  (0, _deleteProperty.default)(webSocketUrl.query, 'bufferStates');
196
281
  }
197
- if ((0, _lodash.get)(_this4, 'webex.config.device.ephemeral', false)) {
282
+ if ((0, _lodash.get)(_this6, 'webex.config.device.ephemeral', false)) {
198
283
  webSocketUrl.query.multipleConnections = true;
199
284
  }
200
285
  webSocketUrl.query.clientTimestamp = (0, _now.default)();
@@ -202,96 +287,94 @@ var Mercury = _webexCore.WebexPlugin.extend((_dec = (0, _common.deprecated)('Mer
202
287
  });
203
288
  },
204
289
  _attemptConnection: function _attemptConnection(socketUrl, callback) {
205
- var _this5 = this;
290
+ var _this7 = this;
291
+ var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};
292
+ var _options$isShutdownSw = options.isShutdownSwitchover,
293
+ isShutdownSwitchover = _options$isShutdownSw === void 0 ? false : _options$isShutdownSw,
294
+ _options$onSuccess = options.onSuccess,
295
+ onSuccess = _options$onSuccess === void 0 ? null : _options$onSuccess;
206
296
  var socket = new _socket.default();
207
- var attemptWSUrl;
208
- socket.on('close', function () {
209
- return _this5._onclose.apply(_this5, arguments);
210
- });
211
- socket.on('message', function () {
212
- return _this5._onmessage.apply(_this5, arguments);
213
- });
214
- socket.on('pong', function () {
215
- return _this5._setTimeOffset.apply(_this5, arguments);
216
- });
217
- socket.on('sequence-mismatch', function () {
218
- for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
219
- args[_key] = arguments[_key];
220
- }
221
- return _this5._emit.apply(_this5, ['sequence-mismatch'].concat(args));
222
- });
223
- socket.on('ping-pong-latency', function () {
224
- for (var _len2 = arguments.length, args = new Array(_len2), _key2 = 0; _key2 < _len2; _key2++) {
225
- args[_key2] = arguments[_key2];
226
- }
227
- return _this5._emit.apply(_this5, ['ping-pong-latency'].concat(args));
228
- });
229
- _promise.default.all([this._prepareUrl(socketUrl), this.webex.credentials.getUserToken()]).then(function (_ref) {
230
- var _ref2 = (0, _slicedToArray2.default)(_ref, 2),
231
- webSocketUrl = _ref2[0],
232
- token = _ref2[1];
233
- if (!_this5.backoffCall) {
234
- var msg = "".concat(_this5.namespace, ": prevent socket open when backoffCall no longer defined");
235
- _this5.logger.info(msg);
236
- return _promise.default.reject(new Error(msg));
237
- }
238
- attemptWSUrl = webSocketUrl;
239
- var options = {
240
- forceCloseDelay: _this5.config.forceCloseDelay,
241
- pingInterval: _this5.config.pingInterval,
242
- pongTimeout: _this5.config.pongTimeout,
243
- token: token.toString(),
244
- trackingId: "".concat(_this5.webex.sessionId, "_").concat((0, _now.default)()),
245
- logger: _this5.logger
246
- };
297
+ var newWSUrl;
298
+ this._attachSocketEventListeners(socket);
299
+
300
+ // Check appropriate backoff call based on connection type
301
+ if (isShutdownSwitchover && !this._shutdownSwitchoverBackoffCall) {
302
+ var msg = "".concat(this.namespace, ": prevent socket open when switchover backoff call no longer defined");
303
+ var err = new Error(msg);
304
+ this.logger.info(msg);
305
+
306
+ // Call the callback with the error before rejecting
307
+ callback(err);
308
+ return _promise.default.reject(err);
309
+ }
310
+ if (!isShutdownSwitchover && !this.backoffCall) {
311
+ var _msg = "".concat(this.namespace, ": prevent socket open when backoffCall no longer defined");
312
+ var _err = new Error(_msg);
313
+ this.logger.info(_msg);
314
+
315
+ // Call the callback with the error before rejecting
316
+ callback(_err);
317
+ return _promise.default.reject(_err);
318
+ }
319
+
320
+ // For shutdown switchover, don't set socket yet (make-before-break)
321
+ // For normal connection, set socket before opening to allow disconnect() to close it
322
+ if (!isShutdownSwitchover) {
323
+ this.socket = socket;
324
+ }
325
+ return this._prepareAndOpenSocket(socket, socketUrl, isShutdownSwitchover).then(function (webSocketUrl) {
326
+ newWSUrl = webSocketUrl;
327
+ _this7.logger.info("".concat(_this7.namespace, ": ").concat(isShutdownSwitchover ? '[shutdown] switchover' : '', " connected to mercury, success, action: connected, url: ").concat(newWSUrl));
247
328
 
248
- // if the consumer has supplied request options use them
249
- if (_this5.webex.config.defaultMercuryOptions) {
250
- _this5.logger.info("".concat(_this5.namespace, ": setting custom options"));
251
- options = _objectSpread(_objectSpread({}, options), _this5.webex.config.defaultMercuryOptions);
329
+ // Custom success handler for shutdown switchover
330
+ if (onSuccess) {
331
+ onSuccess(socket, webSocketUrl);
332
+ callback();
333
+ return _promise.default.resolve();
252
334
  }
253
335
 
254
- // Set the socket before opening it. This allows a disconnect() to close
255
- // the socket if it is in the process of being opened.
256
- _this5.socket = socket;
257
- _this5.logger.info("".concat(_this5.namespace, " connection url: ").concat(webSocketUrl));
258
- return socket.open(webSocketUrl, options);
259
- }).then(function () {
260
- _this5.logger.info("".concat(_this5.namespace, ": connected to mercury, success, action: connected, url: ").concat(attemptWSUrl));
336
+ // Default behavior for normal connection
261
337
  callback();
262
- return _this5.webex.internal.feature.getFeature('developer', 'web-high-availability').then(function (haMessagingEnabled) {
338
+ return _this7.webex.internal.feature.getFeature('developer', 'web-high-availability').then(function (haMessagingEnabled) {
263
339
  if (haMessagingEnabled) {
264
- return _this5.webex.internal.device.refresh();
340
+ return _this7.webex.internal.device.refresh();
265
341
  }
266
342
  return _promise.default.resolve();
267
343
  });
268
344
  }).catch(function (reason) {
269
- var _this5$backoffCall, _this5$backoffCall3;
270
- _this5.lastError = reason; // remember the last error
345
+ var _this7$backoffCall, _this7$backoffCall3;
346
+ // For shutdown, simpler error handling - just callback for retry
347
+ if (isShutdownSwitchover) {
348
+ _this7.logger.info("".concat(_this7.namespace, ": [shutdown] switchover attempt failed"), reason);
349
+ return callback(reason);
350
+ }
351
+
352
+ // Normal connection error handling (existing complex logic)
353
+ _this7.lastError = reason; // remember the last error
271
354
 
272
355
  // Suppress connection errors that appear to be network related. This
273
356
  // may end up suppressing metrics during outages, but we might not care
274
357
  // (especially since many of our outages happen in a way that client
275
358
  // metrics can't be trusted).
276
- if (reason.code !== 1006 && _this5.backoffCall && ((_this5$backoffCall = _this5.backoffCall) === null || _this5$backoffCall === void 0 ? void 0 : _this5$backoffCall.getNumRetries()) > 0) {
277
- var _this5$backoffCall2;
278
- _this5._emit('connection_failed', reason, {
279
- retries: (_this5$backoffCall2 = _this5.backoffCall) === null || _this5$backoffCall2 === void 0 ? void 0 : _this5$backoffCall2.getNumRetries()
359
+ if (reason.code !== 1006 && _this7.backoffCall && ((_this7$backoffCall = _this7.backoffCall) === null || _this7$backoffCall === void 0 ? void 0 : _this7$backoffCall.getNumRetries()) > 0) {
360
+ var _this7$backoffCall2;
361
+ _this7._emit('connection_failed', reason, {
362
+ retries: (_this7$backoffCall2 = _this7.backoffCall) === null || _this7$backoffCall2 === void 0 ? void 0 : _this7$backoffCall2.getNumRetries()
280
363
  });
281
364
  }
282
- _this5.logger.info("".concat(_this5.namespace, ": connection attempt failed"), reason, ((_this5$backoffCall3 = _this5.backoffCall) === null || _this5$backoffCall3 === void 0 ? void 0 : _this5$backoffCall3.getNumRetries()) === 0 ? reason.stack : '');
365
+ _this7.logger.info("".concat(_this7.namespace, ": connection attempt failed"), reason, ((_this7$backoffCall3 = _this7.backoffCall) === null || _this7$backoffCall3 === void 0 ? void 0 : _this7$backoffCall3.getNumRetries()) === 0 ? reason.stack : '');
283
366
  // UnknownResponse is produced by IE for any 4XXX; treated it like a bad
284
367
  // web socket url and let WDM handle the token checking
285
368
  if (reason instanceof _errors.UnknownResponse) {
286
- _this5.logger.info("".concat(_this5.namespace, ": received unknown response code, refreshing device registration"));
287
- return _this5.webex.internal.device.refresh().then(function () {
369
+ _this7.logger.info("".concat(_this7.namespace, ": received unknown response code, refreshing device registration"));
370
+ return _this7.webex.internal.device.refresh().then(function () {
288
371
  return callback(reason);
289
372
  });
290
373
  }
291
374
  // NotAuthorized implies expired token
292
375
  if (reason instanceof _errors.NotAuthorized) {
293
- _this5.logger.info("".concat(_this5.namespace, ": received authorization error, reauthorizing"));
294
- return _this5.webex.credentials.refresh({
376
+ _this7.logger.info("".concat(_this7.namespace, ": received authorization error, reauthorizing"));
377
+ return _this7.webex.credentials.refresh({
295
378
  force: true
296
379
  }).then(function () {
297
380
  return callback(reason);
@@ -306,15 +389,15 @@ var Mercury = _webexCore.WebexPlugin.extend((_dec = (0, _common.deprecated)('Mer
306
389
  // BadRequest implies current credentials are for a Service Account
307
390
  // Forbidden implies current user is not entitle for Webex
308
391
  if (reason instanceof _errors.BadRequest || reason instanceof _errors.Forbidden) {
309
- _this5.logger.warn("".concat(_this5.namespace, ": received unrecoverable response from mercury"));
310
- _this5.backoffCall.abort();
392
+ _this7.logger.warn("".concat(_this7.namespace, ": received unrecoverable response from mercury"));
393
+ _this7.backoffCall.abort();
311
394
  return callback(reason);
312
395
  }
313
396
  if (reason instanceof _errors.ConnectionError) {
314
- return _this5.webex.internal.feature.getFeature('developer', 'web-high-availability').then(function (haMessagingEnabled) {
397
+ return _this7.webex.internal.feature.getFeature('developer', 'web-high-availability').then(function (haMessagingEnabled) {
315
398
  if (haMessagingEnabled) {
316
- _this5.logger.info("".concat(_this5.namespace, ": received a generic connection error, will try to connect to another datacenter. failed, action: 'failed', url: ").concat(attemptWSUrl, " error: ").concat(reason.message));
317
- return _this5.webex.internal.services.markFailedUrl(attemptWSUrl);
399
+ _this7.logger.info("".concat(_this7.namespace, ": received a generic connection error, will try to connect to another datacenter. failed, action: 'failed', url: ").concat(newWSUrl, " error: ").concat(reason.message));
400
+ return _this7.webex.internal.services.markFailedUrl(newWSUrl);
318
401
  }
319
402
  return null;
320
403
  }).then(function () {
@@ -323,63 +406,116 @@ var Mercury = _webexCore.WebexPlugin.extend((_dec = (0, _common.deprecated)('Mer
323
406
  }
324
407
  return callback(reason);
325
408
  }).catch(function (reason) {
326
- _this5.logger.error("".concat(_this5.namespace, ": failed to handle connection failure"), reason);
409
+ _this7.logger.error("".concat(_this7.namespace, ": failed to handle connection failure"), reason);
327
410
  callback(reason);
328
411
  });
329
412
  },
413
+ _prepareAndOpenSocket: function _prepareAndOpenSocket(socket, socketUrl) {
414
+ var _this8 = this;
415
+ var isShutdownSwitchover = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false;
416
+ var logPrefix = isShutdownSwitchover ? '[shutdown] switchover' : 'connection';
417
+ return _promise.default.all([this._prepareUrl(socketUrl), this.webex.credentials.getUserToken()]).then(function (_ref) {
418
+ var _ref2 = (0, _slicedToArray2.default)(_ref, 2),
419
+ webSocketUrl = _ref2[0],
420
+ token = _ref2[1];
421
+ var options = {
422
+ forceCloseDelay: _this8.config.forceCloseDelay,
423
+ pingInterval: _this8.config.pingInterval,
424
+ pongTimeout: _this8.config.pongTimeout,
425
+ token: token.toString(),
426
+ trackingId: "".concat(_this8.webex.sessionId, "_").concat((0, _now.default)()),
427
+ logger: _this8.logger
428
+ };
429
+ if (_this8.webex.config.defaultMercuryOptions) {
430
+ var customOptionsMsg = isShutdownSwitchover ? 'setting custom options for switchover' : 'setting custom options';
431
+ _this8.logger.info("".concat(_this8.namespace, ": ").concat(customOptionsMsg));
432
+ options = _objectSpread(_objectSpread({}, options), _this8.webex.config.defaultMercuryOptions);
433
+ }
434
+ _this8.logger.info("".concat(_this8.namespace, ": ").concat(logPrefix, " url: ").concat(webSocketUrl));
435
+ return socket.open(webSocketUrl, options).then(function () {
436
+ return webSocketUrl;
437
+ });
438
+ });
439
+ },
330
440
  _connectWithBackoff: function _connectWithBackoff(webSocketUrl) {
331
- var _this6 = this;
441
+ var _this9 = this;
442
+ var context = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
443
+ var _context$isShutdownSw = context.isShutdownSwitchover,
444
+ isShutdownSwitchover = _context$isShutdownSw === void 0 ? false : _context$isShutdownSw,
445
+ _context$attemptOptio = context.attemptOptions,
446
+ attemptOptions = _context$attemptOptio === void 0 ? {} : _context$attemptOptio;
332
447
  return new _promise.default(function (resolve, reject) {
333
448
  // eslint gets confused about whether or not call is actually used
334
449
  // eslint-disable-next-line prefer-const
335
450
  var call;
336
451
  var onComplete = function onComplete(err) {
337
- _this6.connecting = false;
338
- _this6.backoffCall = undefined;
452
+ // Clear state flags based on connection type
453
+ if (isShutdownSwitchover) {
454
+ _this9._shutdownSwitchoverInProgress = false;
455
+ _this9._shutdownSwitchoverBackoffCall = undefined;
456
+ } else {
457
+ _this9.connecting = false;
458
+ _this9.backoffCall = undefined;
459
+ }
339
460
  if (err) {
340
- _this6.logger.info("".concat(_this6.namespace, ": failed to connect after ").concat(call.getNumRetries(), " retries; log statement about next retry was inaccurate; ").concat(err));
461
+ var msg = isShutdownSwitchover ? "[shutdown] switchover failed after ".concat(call.getNumRetries(), " retries") : "failed to connect after ".concat(call.getNumRetries(), " retries");
462
+ _this9.logger.info("".concat(_this9.namespace, ": ").concat(msg, "; log statement about next retry was inaccurate; ").concat(err));
341
463
  return reject(err);
342
464
  }
343
- _this6.connected = true;
344
- _this6.hasEverConnected = true;
345
- _this6._emit('online');
346
- _this6.webex.internal.newMetrics.callDiagnosticMetrics.setMercuryConnectedStatus(true);
465
+
466
+ // Default success handling for normal connections
467
+ if (!isShutdownSwitchover) {
468
+ _this9.connected = true;
469
+ _this9.hasEverConnected = true;
470
+ _this9._emit('online');
471
+ _this9.webex.internal.newMetrics.callDiagnosticMetrics.setMercuryConnectedStatus(true);
472
+ }
347
473
  return resolve();
348
474
  };
349
475
 
350
476
  // eslint-disable-next-line prefer-reflect
351
477
  call = _backoff.default.call(function (callback) {
352
- _this6.logger.info("".concat(_this6.namespace, ": executing connection attempt ").concat(call.getNumRetries()));
353
- _this6._attemptConnection(webSocketUrl, callback);
478
+ var attemptNum = call.getNumRetries();
479
+ var logPrefix = isShutdownSwitchover ? '[shutdown] switchover' : 'connection';
480
+ _this9.logger.info("".concat(_this9.namespace, ": executing ").concat(logPrefix, " attempt ").concat(attemptNum));
481
+ _this9._attemptConnection(webSocketUrl, callback, attemptOptions);
354
482
  }, onComplete);
355
483
  call.setStrategy(new _backoff.default.ExponentialStrategy({
356
- initialDelay: _this6.config.backoffTimeReset,
357
- maxDelay: _this6.config.backoffTimeMax
484
+ initialDelay: _this9.config.backoffTimeReset,
485
+ maxDelay: _this9.config.backoffTimeMax
358
486
  }));
359
- if (_this6.config.initialConnectionMaxRetries && !_this6.hasEverConnected) {
360
- call.failAfter(_this6.config.initialConnectionMaxRetries);
361
- } else if (_this6.config.maxRetries) {
362
- call.failAfter(_this6.config.maxRetries);
487
+ if (_this9.config.initialConnectionMaxRetries && !_this9.hasEverConnected && !isShutdownSwitchover) {
488
+ call.failAfter(_this9.config.initialConnectionMaxRetries);
489
+ } else if (_this9.config.maxRetries) {
490
+ call.failAfter(_this9.config.maxRetries);
363
491
  }
364
492
  call.on('abort', function () {
365
- _this6.logger.info("".concat(_this6.namespace, ": connection aborted"));
366
- reject(new Error('Mercury Connection Aborted'));
493
+ var msg = isShutdownSwitchover ? 'Shutdown Switchover' : 'Connection';
494
+ _this9.logger.info("".concat(_this9.namespace, ": ").concat(msg, " aborted"));
495
+ reject(new Error("Mercury ".concat(msg, " Aborted")));
367
496
  });
368
497
  call.on('callback', function (err) {
369
498
  if (err) {
370
499
  var number = call.getNumRetries();
371
- var delay = Math.min(call.strategy_.nextBackoffDelay_, _this6.config.backoffTimeMax);
372
- _this6.logger.info("".concat(_this6.namespace, ": failed to connect; attempting retry ").concat(number + 1, " in ").concat(delay, " ms"));
500
+ var delay = Math.min(call.strategy_.nextBackoffDelay_, _this9.config.backoffTimeMax);
501
+ var logPrefix = isShutdownSwitchover ? '[shutdown] switchover' : '';
502
+ _this9.logger.info("".concat(_this9.namespace, ": ").concat(logPrefix, " failed to connect; attempting retry ").concat(number + 1, " in ").concat(delay, " ms"));
373
503
  /* istanbul ignore if */
374
504
  if (process.env.NODE_ENV === 'development') {
375
- _this6.logger.debug("".concat(_this6.namespace, ": "), err, err.stack);
505
+ _this9.logger.debug("".concat(_this9.namespace, ": "), err, err.stack);
376
506
  }
377
507
  return;
378
508
  }
379
- _this6.logger.info("".concat(_this6.namespace, ": connected"));
509
+ _this9.logger.info("".concat(_this9.namespace, ": connected"));
380
510
  });
511
+
512
+ // Store backoff call reference BEFORE starting (so it's available in _attemptConnection)
513
+ if (isShutdownSwitchover) {
514
+ _this9._shutdownSwitchoverBackoffCall = call;
515
+ } else {
516
+ _this9.backoffCall = call;
517
+ }
381
518
  call.start();
382
- _this6.backoffCall = call;
383
519
  });
384
520
  },
385
521
  _emit: function _emit() {
@@ -410,36 +546,74 @@ var Mercury = _webexCore.WebexPlugin.extend((_dec = (0, _common.deprecated)('Mer
410
546
  }
411
547
  return handlers;
412
548
  },
413
- _onclose: function _onclose(event) {
549
+ _onclose: function _onclose(event, sourceSocket) {
414
550
  // I don't see any way to avoid the complexity or statement count in here.
415
551
  /* eslint complexity: [0] */
416
552
 
417
553
  try {
554
+ var isActiveSocket = sourceSocket === this.socket;
418
555
  var reason = event.reason && event.reason.toLowerCase();
419
- var socketUrl = this.socket.url;
420
- this.socket.removeAllListeners();
421
- this.unset('socket');
422
- this.connected = false;
423
- this._emit('offline', event);
424
- this.webex.internal.newMetrics.callDiagnosticMetrics.setMercuryConnectedStatus(false);
556
+ var socketUrl;
557
+ if (isActiveSocket && this.socket) {
558
+ // Active socket closed - get URL from current socket reference
559
+ socketUrl = this.socket.url;
560
+ } else if (sourceSocket) {
561
+ // Old socket closed - get URL from the closed socket
562
+ socketUrl = sourceSocket.url;
563
+ }
564
+ if (isActiveSocket) {
565
+ // Only tear down state if the currently active socket closed
566
+ if (this.socket) {
567
+ this.socket.removeAllListeners();
568
+ }
569
+ this.unset('socket');
570
+ this.connected = false;
571
+ this._emit('offline', event);
572
+ this.webex.internal.newMetrics.callDiagnosticMetrics.setMercuryConnectedStatus(false);
573
+ } else {
574
+ // Old socket closed; do not flip connection state
575
+ this.logger.info("".concat(this.namespace, ": [shutdown] non-active socket closed, code=").concat(event.code));
576
+ // Clean up listeners from old socket now that it's closed
577
+ if (sourceSocket) {
578
+ sourceSocket.removeAllListeners();
579
+ }
580
+ }
425
581
  switch (event.code) {
426
582
  case 1003:
427
583
  // metric: disconnect
428
584
  this.logger.info("".concat(this.namespace, ": Mercury service rejected last message; will not reconnect: ").concat(event.reason));
429
- this._emit('offline.permanent', event);
585
+ if (isActiveSocket) this._emit('offline.permanent', event);
430
586
  break;
431
587
  case 4000:
432
588
  // metric: disconnect
433
589
  this.logger.info("".concat(this.namespace, ": socket replaced; will not reconnect"));
434
- this._emit('offline.replaced', event);
590
+ if (isActiveSocket) this._emit('offline.replaced', event);
591
+ // If not active, nothing to do
592
+ break;
593
+ case 4001:
594
+ // replaced during shutdown
595
+ if (isActiveSocket) {
596
+ // Server closed active socket with 4001, meaning it expected this connection
597
+ // to be replaced, but the switchover in _handleImminentShutdown failed.
598
+ // This is a permanent failure - do not reconnect.
599
+ this.logger.warn("".concat(this.namespace, ": active socket closed with 4001; shutdown switchover failed"));
600
+ this._emit('offline.permanent', event);
601
+ } else {
602
+ // Expected: old socket closed after successful switchover
603
+ this.logger.info("".concat(this.namespace, ": old socket closed with 4001 (replaced during shutdown); no reconnect needed"));
604
+ this._emit('offline.replaced', event);
605
+ }
435
606
  break;
436
607
  case 1001:
437
608
  case 1005:
438
609
  case 1006:
439
610
  case 1011:
440
611
  this.logger.info("".concat(this.namespace, ": socket disconnected; reconnecting"));
441
- this._emit('offline.transient', event);
442
- this._reconnect(socketUrl);
612
+ if (isActiveSocket) {
613
+ this._emit('offline.transient', event);
614
+ this.logger.info("".concat(this.namespace, ": [shutdown] reconnecting active socket to recover"));
615
+ this._reconnect(socketUrl);
616
+ }
443
617
  // metric: disconnect
444
618
  // if (code == 1011 && reason !== ping error) metric: unexpected disconnect
445
619
  break;
@@ -448,31 +622,42 @@ var Mercury = _webexCore.WebexPlugin.extend((_dec = (0, _common.deprecated)('Mer
448
622
  // 3050 indicates logout form of closure, default to old behavior, use config reason defined by consumer to proceed with the permanent block
449
623
  if (normalReconnectReasons.includes(reason)) {
450
624
  this.logger.info("".concat(this.namespace, ": socket disconnected; reconnecting"));
451
- this._emit('offline.transient', event);
452
- this._reconnect(socketUrl);
625
+ if (isActiveSocket) {
626
+ this._emit('offline.transient', event);
627
+ this.logger.info("".concat(this.namespace, ": [shutdown] reconnecting due to normal close"));
628
+ this._reconnect(socketUrl);
629
+ }
453
630
  // metric: disconnect
454
631
  // if (reason === done forced) metric: force closure
455
632
  } else {
456
633
  this.logger.info("".concat(this.namespace, ": socket disconnected; will not reconnect: ").concat(event.reason));
457
- this._emit('offline.permanent', event);
634
+ if (isActiveSocket) this._emit('offline.permanent', event);
458
635
  }
459
636
  break;
460
637
  default:
461
638
  this.logger.info("".concat(this.namespace, ": socket disconnected unexpectedly; will not reconnect"));
462
639
  // unexpected disconnect
463
- this._emit('offline.permanent', event);
640
+ if (isActiveSocket) this._emit('offline.permanent', event);
464
641
  }
465
642
  } catch (error) {
466
643
  this.logger.error("".concat(this.namespace, ": error occurred in close handler"), error);
467
644
  }
468
645
  },
469
646
  _onmessage: function _onmessage(event) {
470
- var _this7 = this;
647
+ var _this10 = this;
471
648
  this._setTimeOffset(event);
472
649
  var envelope = event.data;
473
650
  if (process.env.ENABLE_MERCURY_LOGGING) {
474
651
  this.logger.debug("".concat(this.namespace, ": message envelope: "), envelope);
475
652
  }
653
+
654
+ // Handle shutdown message shape: { type: 'shutdown' }
655
+ if (envelope && envelope.type === 'shutdown') {
656
+ this.logger.info("".concat(this.namespace, ": [shutdown] imminent shutdown message received"));
657
+ this._emit('event:mercury_shutdown_imminent', envelope);
658
+ this._handleImminentShutdown();
659
+ return _promise.default.resolve();
660
+ }
476
661
  var data = envelope.data;
477
662
  this._applyOverrides(data);
478
663
  return this._getEventHandlers(data.eventType).reduce(function (promise, handler) {
@@ -480,24 +665,24 @@ var Mercury = _webexCore.WebexPlugin.extend((_dec = (0, _common.deprecated)('Mer
480
665
  var namespace = handler.namespace,
481
666
  name = handler.name;
482
667
  return new _promise.default(function (resolve) {
483
- return resolve((_this7.webex[namespace] || _this7.webex.internal[namespace])[name](data));
668
+ return resolve((_this10.webex[namespace] || _this10.webex.internal[namespace])[name](data));
484
669
  }).catch(function (reason) {
485
- return _this7.logger.error("".concat(_this7.namespace, ": error occurred in autowired event handler for ").concat(data.eventType), reason);
670
+ return _this10.logger.error("".concat(_this10.namespace, ": error occurred in autowired event handler for ").concat(data.eventType), reason);
486
671
  });
487
672
  });
488
673
  }, _promise.default.resolve()).then(function () {
489
- _this7._emit('event', event.data);
674
+ _this10._emit('event', event.data);
490
675
  var _data$eventType$split = data.eventType.split('.'),
491
676
  _data$eventType$split2 = (0, _slicedToArray2.default)(_data$eventType$split, 1),
492
677
  namespace = _data$eventType$split2[0];
493
678
  if (namespace === data.eventType) {
494
- _this7._emit("event:".concat(namespace), envelope);
679
+ _this10._emit("event:".concat(namespace), envelope);
495
680
  } else {
496
- _this7._emit("event:".concat(namespace), envelope);
497
- _this7._emit("event:".concat(data.eventType), envelope);
681
+ _this10._emit("event:".concat(namespace), envelope);
682
+ _this10._emit("event:".concat(data.eventType), envelope);
498
683
  }
499
684
  }).catch(function (reason) {
500
- _this7.logger.error("".concat(_this7.namespace, ": error occurred processing socket message"), reason);
685
+ _this10.logger.error("".concat(_this10.namespace, ": error occurred processing socket message"), reason);
501
686
  });
502
687
  },
503
688
  _setTimeOffset: function _setTimeOffset(event) {
@@ -510,7 +695,7 @@ var Mercury = _webexCore.WebexPlugin.extend((_dec = (0, _common.deprecated)('Mer
510
695
  this.logger.info("".concat(this.namespace, ": reconnecting"));
511
696
  return this.connect(webSocketUrl);
512
697
  },
513
- version: "3.9.0-next.6"
698
+ version: "3.9.0-next.7"
514
699
  }, ((0, _applyDecoratedDescriptor2.default)(_obj, "connect", [_common.oneFlight], (0, _getOwnPropertyDescriptor.default)(_obj, "connect"), _obj), (0, _applyDecoratedDescriptor2.default)(_obj, "disconnect", [_common.oneFlight], (0, _getOwnPropertyDescriptor.default)(_obj, "disconnect"), _obj), (0, _applyDecoratedDescriptor2.default)(_obj, "listen", [_dec], (0, _getOwnPropertyDescriptor.default)(_obj, "listen"), _obj), (0, _applyDecoratedDescriptor2.default)(_obj, "stopListening", [_dec2], (0, _getOwnPropertyDescriptor.default)(_obj, "stopListening"), _obj)), _obj)));
515
700
  var _default = exports.default = Mercury;
516
701
  //# sourceMappingURL=mercury.js.map