@webex/internal-plugin-mercury 3.9.0 → 3.10.0-next.2

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"));
@@ -79,6 +79,109 @@ var Mercury = _webexCore.WebexPlugin.extend((_dec = (0, _common.deprecated)('Mer
79
79
  _this.webex.internal.feature.updateFeature(envelope.data.featureToggle);
80
80
  }
81
81
  });
82
+ /*
83
+ * When Cluster Migrations, notify clients using ActiveClusterStatusEvent via mercury
84
+ * https://wwwin-github.cisco.com/pages/Webex/crr-docs/techdocs/rr-002.html#wip-notifying-clients-of-cluster-migrations
85
+ * */
86
+ this.on('event:ActiveClusterStatusEvent', function (envelope) {
87
+ var _this$webex$internal$;
88
+ if (typeof ((_this$webex$internal$ = _this.webex.internal.services) === null || _this$webex$internal$ === void 0 ? void 0 : _this$webex$internal$.switchActiveClusterIds) === 'function' && envelope && envelope.data) {
89
+ var _envelope$data;
90
+ _this.webex.internal.services.switchActiveClusterIds((_envelope$data = envelope.data) === null || _envelope$data === void 0 ? void 0 : _envelope$data.activeClusters);
91
+ }
92
+ });
93
+ /*
94
+ * Using cache-invalidation via mercury to instead the method of polling via the new /timestamp endpoint from u2c
95
+ * https://wwwin-github.cisco.com/pages/Webex/crr-docs/techdocs/rr-005.html#websocket-notifications
96
+ * */
97
+ this.on('event:u2c.cache-invalidation', function (envelope) {
98
+ var _this$webex$internal$2;
99
+ if (typeof ((_this$webex$internal$2 = _this.webex.internal.services) === null || _this$webex$internal$2 === void 0 ? void 0 : _this$webex$internal$2.invalidateCache) === 'function' && envelope && envelope.data) {
100
+ var _envelope$data2;
101
+ _this.webex.internal.services.invalidateCache((_envelope$data2 = envelope.data) === null || _envelope$data2 === void 0 ? void 0 : _envelope$data2.timestamp);
102
+ }
103
+ });
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
+ }
82
185
  },
83
186
  /**
84
187
  * Get the last error.
@@ -88,7 +191,7 @@ var Mercury = _webexCore.WebexPlugin.extend((_dec = (0, _common.deprecated)('Mer
88
191
  return this.lastError;
89
192
  },
90
193
  connect: function connect(webSocketUrl) {
91
- var _this2 = this;
194
+ var _this4 = this;
92
195
  if (this.connected) {
93
196
  this.logger.info("".concat(this.namespace, ": already connected, will not connect again"));
94
197
  return _promise.default.resolve();
@@ -97,8 +200,8 @@ var Mercury = _webexCore.WebexPlugin.extend((_dec = (0, _common.deprecated)('Mer
97
200
  this.logger.info("".concat(this.namespace, ": starting connection attempt"));
98
201
  this.logger.info("".concat(this.namespace, ": debug_mercury_logging stack: "), new Error('debug_mercury_logging').stack);
99
202
  return _promise.default.resolve(this.webex.internal.device.registered || this.webex.internal.device.register()).then(function () {
100
- _this2.logger.info("".concat(_this2.namespace, ": connecting"));
101
- return _this2._connectWithBackoff(webSocketUrl);
203
+ _this4.logger.info("".concat(_this4.namespace, ": connecting"));
204
+ return _this4._connectWithBackoff(webSocketUrl);
102
205
  });
103
206
  },
104
207
  logout: function logout() {
@@ -110,16 +213,20 @@ var Mercury = _webexCore.WebexPlugin.extend((_dec = (0, _common.deprecated)('Mer
110
213
  } : undefined);
111
214
  },
112
215
  disconnect: function disconnect(options) {
113
- var _this3 = this;
216
+ var _this5 = this;
114
217
  return new _promise.default(function (resolve) {
115
- if (_this3.backoffCall) {
116
- _this3.logger.info("".concat(_this3.namespace, ": aborting connection"));
117
- _this3.backoffCall.abort();
218
+ if (_this5.backoffCall) {
219
+ _this5.logger.info("".concat(_this5.namespace, ": aborting connection"));
220
+ _this5.backoffCall.abort();
221
+ }
222
+ if (_this5._shutdownSwitchoverBackoffCall) {
223
+ _this5.logger.info("".concat(_this5.namespace, ": aborting shutdown switchover"));
224
+ _this5._shutdownSwitchoverBackoffCall.abort();
118
225
  }
119
- if (_this3.socket) {
120
- _this3.socket.removeAllListeners('message');
121
- _this3.once('offline', resolve);
122
- resolve(_this3.socket.close(options || undefined));
226
+ if (_this5.socket) {
227
+ _this5.socket.removeAllListeners('message');
228
+ _this5.once('offline', resolve);
229
+ resolve(_this5.socket.close(options || undefined));
123
230
  }
124
231
  resolve();
125
232
  });
@@ -145,19 +252,19 @@ var Mercury = _webexCore.WebexPlugin.extend((_dec = (0, _common.deprecated)('Mer
145
252
  });
146
253
  },
147
254
  _prepareUrl: function _prepareUrl(webSocketUrl) {
148
- var _this4 = this;
255
+ var _this6 = this;
149
256
  if (!webSocketUrl) {
150
257
  webSocketUrl = this.webex.internal.device.webSocketUrl;
151
258
  }
152
259
  return this.webex.internal.feature.getFeature('developer', 'web-high-availability').then(function (haMessagingEnabled) {
153
260
  if (haMessagingEnabled) {
154
- return _this4.webex.internal.services.convertUrlToPriorityHostUrl(webSocketUrl);
261
+ return _this6.webex.internal.services.convertUrlToPriorityHostUrl(webSocketUrl);
155
262
  }
156
263
  return webSocketUrl;
157
264
  }).then(function (wsUrl) {
158
265
  webSocketUrl = wsUrl;
159
266
  }).then(function () {
160
- return _this4.webex.internal.feature.getFeature('developer', 'web-shared-mercury');
267
+ return _this6.webex.internal.feature.getFeature('developer', 'web-shared-mercury');
161
268
  }).then(function (webSharedMercury) {
162
269
  webSocketUrl = _url.default.parse(webSocketUrl, true);
163
270
  (0, _assign.default)(webSocketUrl.query, {
@@ -172,7 +279,7 @@ var Mercury = _webexCore.WebexPlugin.extend((_dec = (0, _common.deprecated)('Mer
172
279
  });
173
280
  (0, _deleteProperty.default)(webSocketUrl.query, 'bufferStates');
174
281
  }
175
- if ((0, _lodash.get)(_this4, 'webex.config.device.ephemeral', false)) {
282
+ if ((0, _lodash.get)(_this6, 'webex.config.device.ephemeral', false)) {
176
283
  webSocketUrl.query.multipleConnections = true;
177
284
  }
178
285
  webSocketUrl.query.clientTimestamp = (0, _now.default)();
@@ -180,96 +287,94 @@ var Mercury = _webexCore.WebexPlugin.extend((_dec = (0, _common.deprecated)('Mer
180
287
  });
181
288
  },
182
289
  _attemptConnection: function _attemptConnection(socketUrl, callback) {
183
- 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;
184
296
  var socket = new _socket.default();
185
- var attemptWSUrl;
186
- socket.on('close', function () {
187
- return _this5._onclose.apply(_this5, arguments);
188
- });
189
- socket.on('message', function () {
190
- return _this5._onmessage.apply(_this5, arguments);
191
- });
192
- socket.on('pong', function () {
193
- return _this5._setTimeOffset.apply(_this5, arguments);
194
- });
195
- socket.on('sequence-mismatch', function () {
196
- for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
197
- args[_key] = arguments[_key];
198
- }
199
- return _this5._emit.apply(_this5, ['sequence-mismatch'].concat(args));
200
- });
201
- socket.on('ping-pong-latency', function () {
202
- for (var _len2 = arguments.length, args = new Array(_len2), _key2 = 0; _key2 < _len2; _key2++) {
203
- args[_key2] = arguments[_key2];
204
- }
205
- return _this5._emit.apply(_this5, ['ping-pong-latency'].concat(args));
206
- });
207
- _promise.default.all([this._prepareUrl(socketUrl), this.webex.credentials.getUserToken()]).then(function (_ref) {
208
- var _ref2 = (0, _slicedToArray2.default)(_ref, 2),
209
- webSocketUrl = _ref2[0],
210
- token = _ref2[1];
211
- if (!_this5.backoffCall) {
212
- var msg = "".concat(_this5.namespace, ": prevent socket open when backoffCall no longer defined");
213
- _this5.logger.info(msg);
214
- return _promise.default.reject(new Error(msg));
215
- }
216
- attemptWSUrl = webSocketUrl;
217
- var options = {
218
- forceCloseDelay: _this5.config.forceCloseDelay,
219
- pingInterval: _this5.config.pingInterval,
220
- pongTimeout: _this5.config.pongTimeout,
221
- token: token.toString(),
222
- trackingId: "".concat(_this5.webex.sessionId, "_").concat((0, _now.default)()),
223
- logger: _this5.logger
224
- };
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));
225
328
 
226
- // if the consumer has supplied request options use them
227
- if (_this5.webex.config.defaultMercuryOptions) {
228
- _this5.logger.info("".concat(_this5.namespace, ": setting custom options"));
229
- 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();
230
334
  }
231
335
 
232
- // Set the socket before opening it. This allows a disconnect() to close
233
- // the socket if it is in the process of being opened.
234
- _this5.socket = socket;
235
- _this5.logger.info("".concat(_this5.namespace, " connection url: ").concat(webSocketUrl));
236
- return socket.open(webSocketUrl, options);
237
- }).then(function () {
238
- _this5.logger.info("".concat(_this5.namespace, ": connected to mercury, success, action: connected, url: ").concat(attemptWSUrl));
336
+ // Default behavior for normal connection
239
337
  callback();
240
- 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) {
241
339
  if (haMessagingEnabled) {
242
- return _this5.webex.internal.device.refresh();
340
+ return _this7.webex.internal.device.refresh();
243
341
  }
244
342
  return _promise.default.resolve();
245
343
  });
246
344
  }).catch(function (reason) {
247
- var _this5$backoffCall, _this5$backoffCall3;
248
- _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
249
354
 
250
355
  // Suppress connection errors that appear to be network related. This
251
356
  // may end up suppressing metrics during outages, but we might not care
252
357
  // (especially since many of our outages happen in a way that client
253
358
  // metrics can't be trusted).
254
- if (reason.code !== 1006 && _this5.backoffCall && ((_this5$backoffCall = _this5.backoffCall) === null || _this5$backoffCall === void 0 ? void 0 : _this5$backoffCall.getNumRetries()) > 0) {
255
- var _this5$backoffCall2;
256
- _this5._emit('connection_failed', reason, {
257
- 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()
258
363
  });
259
364
  }
260
- _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 : '');
261
366
  // UnknownResponse is produced by IE for any 4XXX; treated it like a bad
262
367
  // web socket url and let WDM handle the token checking
263
368
  if (reason instanceof _errors.UnknownResponse) {
264
- _this5.logger.info("".concat(_this5.namespace, ": received unknown response code, refreshing device registration"));
265
- 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 () {
266
371
  return callback(reason);
267
372
  });
268
373
  }
269
374
  // NotAuthorized implies expired token
270
375
  if (reason instanceof _errors.NotAuthorized) {
271
- _this5.logger.info("".concat(_this5.namespace, ": received authorization error, reauthorizing"));
272
- return _this5.webex.credentials.refresh({
376
+ _this7.logger.info("".concat(_this7.namespace, ": received authorization error, reauthorizing"));
377
+ return _this7.webex.credentials.refresh({
273
378
  force: true
274
379
  }).then(function () {
275
380
  return callback(reason);
@@ -284,15 +389,15 @@ var Mercury = _webexCore.WebexPlugin.extend((_dec = (0, _common.deprecated)('Mer
284
389
  // BadRequest implies current credentials are for a Service Account
285
390
  // Forbidden implies current user is not entitle for Webex
286
391
  if (reason instanceof _errors.BadRequest || reason instanceof _errors.Forbidden) {
287
- _this5.logger.warn("".concat(_this5.namespace, ": received unrecoverable response from mercury"));
288
- _this5.backoffCall.abort();
392
+ _this7.logger.warn("".concat(_this7.namespace, ": received unrecoverable response from mercury"));
393
+ _this7.backoffCall.abort();
289
394
  return callback(reason);
290
395
  }
291
396
  if (reason instanceof _errors.ConnectionError) {
292
- 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) {
293
398
  if (haMessagingEnabled) {
294
- _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));
295
- 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);
296
401
  }
297
402
  return null;
298
403
  }).then(function () {
@@ -301,63 +406,116 @@ var Mercury = _webexCore.WebexPlugin.extend((_dec = (0, _common.deprecated)('Mer
301
406
  }
302
407
  return callback(reason);
303
408
  }).catch(function (reason) {
304
- _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);
305
410
  callback(reason);
306
411
  });
307
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
+ },
308
440
  _connectWithBackoff: function _connectWithBackoff(webSocketUrl) {
309
- 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;
310
447
  return new _promise.default(function (resolve, reject) {
311
448
  // eslint gets confused about whether or not call is actually used
312
449
  // eslint-disable-next-line prefer-const
313
450
  var call;
314
451
  var onComplete = function onComplete(err) {
315
- _this6.connecting = false;
316
- _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
+ }
317
460
  if (err) {
318
- _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));
319
463
  return reject(err);
320
464
  }
321
- _this6.connected = true;
322
- _this6.hasEverConnected = true;
323
- _this6._emit('online');
324
- _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
+ }
325
473
  return resolve();
326
474
  };
327
475
 
328
476
  // eslint-disable-next-line prefer-reflect
329
477
  call = _backoff.default.call(function (callback) {
330
- _this6.logger.info("".concat(_this6.namespace, ": executing connection attempt ").concat(call.getNumRetries()));
331
- _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);
332
482
  }, onComplete);
333
483
  call.setStrategy(new _backoff.default.ExponentialStrategy({
334
- initialDelay: _this6.config.backoffTimeReset,
335
- maxDelay: _this6.config.backoffTimeMax
484
+ initialDelay: _this9.config.backoffTimeReset,
485
+ maxDelay: _this9.config.backoffTimeMax
336
486
  }));
337
- if (_this6.config.initialConnectionMaxRetries && !_this6.hasEverConnected) {
338
- call.failAfter(_this6.config.initialConnectionMaxRetries);
339
- } else if (_this6.config.maxRetries) {
340
- 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);
341
491
  }
342
492
  call.on('abort', function () {
343
- _this6.logger.info("".concat(_this6.namespace, ": connection aborted"));
344
- 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")));
345
496
  });
346
497
  call.on('callback', function (err) {
347
498
  if (err) {
348
499
  var number = call.getNumRetries();
349
- var delay = Math.min(call.strategy_.nextBackoffDelay_, _this6.config.backoffTimeMax);
350
- _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"));
351
503
  /* istanbul ignore if */
352
504
  if (process.env.NODE_ENV === 'development') {
353
- _this6.logger.debug("".concat(_this6.namespace, ": "), err, err.stack);
505
+ _this9.logger.debug("".concat(_this9.namespace, ": "), err, err.stack);
354
506
  }
355
507
  return;
356
508
  }
357
- _this6.logger.info("".concat(_this6.namespace, ": connected"));
509
+ _this9.logger.info("".concat(_this9.namespace, ": connected"));
358
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
+ }
359
518
  call.start();
360
- _this6.backoffCall = call;
361
519
  });
362
520
  },
363
521
  _emit: function _emit() {
@@ -388,36 +546,74 @@ var Mercury = _webexCore.WebexPlugin.extend((_dec = (0, _common.deprecated)('Mer
388
546
  }
389
547
  return handlers;
390
548
  },
391
- _onclose: function _onclose(event) {
549
+ _onclose: function _onclose(event, sourceSocket) {
392
550
  // I don't see any way to avoid the complexity or statement count in here.
393
551
  /* eslint complexity: [0] */
394
552
 
395
553
  try {
554
+ var isActiveSocket = sourceSocket === this.socket;
396
555
  var reason = event.reason && event.reason.toLowerCase();
397
- var socketUrl = this.socket.url;
398
- this.socket.removeAllListeners();
399
- this.unset('socket');
400
- this.connected = false;
401
- this._emit('offline', event);
402
- 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
+ }
403
581
  switch (event.code) {
404
582
  case 1003:
405
583
  // metric: disconnect
406
584
  this.logger.info("".concat(this.namespace, ": Mercury service rejected last message; will not reconnect: ").concat(event.reason));
407
- this._emit('offline.permanent', event);
585
+ if (isActiveSocket) this._emit('offline.permanent', event);
408
586
  break;
409
587
  case 4000:
410
588
  // metric: disconnect
411
589
  this.logger.info("".concat(this.namespace, ": socket replaced; will not reconnect"));
412
- 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
+ }
413
606
  break;
414
607
  case 1001:
415
608
  case 1005:
416
609
  case 1006:
417
610
  case 1011:
418
611
  this.logger.info("".concat(this.namespace, ": socket disconnected; reconnecting"));
419
- this._emit('offline.transient', event);
420
- 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
+ }
421
617
  // metric: disconnect
422
618
  // if (code == 1011 && reason !== ping error) metric: unexpected disconnect
423
619
  break;
@@ -426,31 +622,42 @@ var Mercury = _webexCore.WebexPlugin.extend((_dec = (0, _common.deprecated)('Mer
426
622
  // 3050 indicates logout form of closure, default to old behavior, use config reason defined by consumer to proceed with the permanent block
427
623
  if (normalReconnectReasons.includes(reason)) {
428
624
  this.logger.info("".concat(this.namespace, ": socket disconnected; reconnecting"));
429
- this._emit('offline.transient', event);
430
- 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
+ }
431
630
  // metric: disconnect
432
631
  // if (reason === done forced) metric: force closure
433
632
  } else {
434
633
  this.logger.info("".concat(this.namespace, ": socket disconnected; will not reconnect: ").concat(event.reason));
435
- this._emit('offline.permanent', event);
634
+ if (isActiveSocket) this._emit('offline.permanent', event);
436
635
  }
437
636
  break;
438
637
  default:
439
638
  this.logger.info("".concat(this.namespace, ": socket disconnected unexpectedly; will not reconnect"));
440
639
  // unexpected disconnect
441
- this._emit('offline.permanent', event);
640
+ if (isActiveSocket) this._emit('offline.permanent', event);
442
641
  }
443
642
  } catch (error) {
444
643
  this.logger.error("".concat(this.namespace, ": error occurred in close handler"), error);
445
644
  }
446
645
  },
447
646
  _onmessage: function _onmessage(event) {
448
- var _this7 = this;
647
+ var _this10 = this;
449
648
  this._setTimeOffset(event);
450
649
  var envelope = event.data;
451
650
  if (process.env.ENABLE_MERCURY_LOGGING) {
452
651
  this.logger.debug("".concat(this.namespace, ": message envelope: "), envelope);
453
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
+ }
454
661
  var data = envelope.data;
455
662
  this._applyOverrides(data);
456
663
  return this._getEventHandlers(data.eventType).reduce(function (promise, handler) {
@@ -458,24 +665,24 @@ var Mercury = _webexCore.WebexPlugin.extend((_dec = (0, _common.deprecated)('Mer
458
665
  var namespace = handler.namespace,
459
666
  name = handler.name;
460
667
  return new _promise.default(function (resolve) {
461
- return resolve((_this7.webex[namespace] || _this7.webex.internal[namespace])[name](data));
668
+ return resolve((_this10.webex[namespace] || _this10.webex.internal[namespace])[name](data));
462
669
  }).catch(function (reason) {
463
- 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);
464
671
  });
465
672
  });
466
673
  }, _promise.default.resolve()).then(function () {
467
- _this7._emit('event', event.data);
674
+ _this10._emit('event', event.data);
468
675
  var _data$eventType$split = data.eventType.split('.'),
469
676
  _data$eventType$split2 = (0, _slicedToArray2.default)(_data$eventType$split, 1),
470
677
  namespace = _data$eventType$split2[0];
471
678
  if (namespace === data.eventType) {
472
- _this7._emit("event:".concat(namespace), envelope);
679
+ _this10._emit("event:".concat(namespace), envelope);
473
680
  } else {
474
- _this7._emit("event:".concat(namespace), envelope);
475
- _this7._emit("event:".concat(data.eventType), envelope);
681
+ _this10._emit("event:".concat(namespace), envelope);
682
+ _this10._emit("event:".concat(data.eventType), envelope);
476
683
  }
477
684
  }).catch(function (reason) {
478
- _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);
479
686
  });
480
687
  },
481
688
  _setTimeOffset: function _setTimeOffset(event) {
@@ -488,7 +695,7 @@ var Mercury = _webexCore.WebexPlugin.extend((_dec = (0, _common.deprecated)('Mer
488
695
  this.logger.info("".concat(this.namespace, ": reconnecting"));
489
696
  return this.connect(webSocketUrl);
490
697
  },
491
- version: "3.9.0"
698
+ version: "3.10.0-next.2"
492
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)));
493
700
  var _default = exports.default = Mercury;
494
701
  //# sourceMappingURL=mercury.js.map