"use strict";

var _Reflect$construct = require("@babel/runtime-corejs2/core-js/reflect/construct");
var _Object$keys = require("@babel/runtime-corejs2/core-js/object/keys");
var _Object$getOwnPropertySymbols = require("@babel/runtime-corejs2/core-js/object/get-own-property-symbols");
var _Object$getOwnPropertyDescriptor = require("@babel/runtime-corejs2/core-js/object/get-own-property-descriptor");
var _Object$getOwnPropertyDescriptors = require("@babel/runtime-corejs2/core-js/object/get-own-property-descriptors");
var _Object$defineProperties = require("@babel/runtime-corejs2/core-js/object/define-properties");
var _Object$defineProperty = require("@babel/runtime-corejs2/core-js/object/define-property");
var _interopRequireDefault = require("@babel/runtime-corejs2/helpers/interopRequireDefault");
_Object$defineProperty(exports, "__esModule", {
  value: true
});
exports.default = void 0;
var _regenerator = _interopRequireDefault(require("@babel/runtime-corejs2/regenerator"));
var _map = _interopRequireDefault(require("@babel/runtime-corejs2/core-js/map"));
var _isArray = _interopRequireDefault(require("@babel/runtime-corejs2/core-js/array/is-array"));
var _promise = _interopRequireDefault(require("@babel/runtime-corejs2/core-js/promise"));
var _stringify = _interopRequireDefault(require("@babel/runtime-corejs2/core-js/json/stringify"));
var _now = _interopRequireDefault(require("@babel/runtime-corejs2/core-js/date/now"));
var _asyncToGenerator2 = _interopRequireDefault(require("@babel/runtime-corejs2/helpers/asyncToGenerator"));
var _slicedToArray2 = _interopRequireDefault(require("@babel/runtime-corejs2/helpers/slicedToArray"));
var _typeof2 = _interopRequireDefault(require("@babel/runtime-corejs2/helpers/typeof"));
var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime-corejs2/helpers/classCallCheck"));
var _createClass2 = _interopRequireDefault(require("@babel/runtime-corejs2/helpers/createClass"));
var _possibleConstructorReturn2 = _interopRequireDefault(require("@babel/runtime-corejs2/helpers/possibleConstructorReturn"));
var _getPrototypeOf2 = _interopRequireDefault(require("@babel/runtime-corejs2/helpers/getPrototypeOf"));
var _get2 = _interopRequireDefault(require("@babel/runtime-corejs2/helpers/get"));
var _inherits2 = _interopRequireDefault(require("@babel/runtime-corejs2/helpers/inherits"));
var _defineProperty2 = _interopRequireDefault(require("@babel/runtime-corejs2/helpers/defineProperty"));
var _events = require("events");
var _backoff = _interopRequireDefault(require("backoff"));
var _socket = _interopRequireDefault(require("./socket"));
var _errors = require("./errors");
var _constants = require("./socket/constants");
function ownKeys(e, r) { var t = _Object$keys(e); if (_Object$getOwnPropertySymbols) { var o = _Object$getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return _Object$getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { (0, _defineProperty2.default)(e, r, t[r]); }) : _Object$getOwnPropertyDescriptors ? _Object$defineProperties(e, _Object$getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { _Object$defineProperty(e, r, _Object$getOwnPropertyDescriptor(t, r)); }); } return e; }
function _callSuper(t, o, e) { return o = (0, _getPrototypeOf2.default)(o), (0, _possibleConstructorReturn2.default)(t, _isNativeReflectConstruct() ? _Reflect$construct(o, e || [], (0, _getPrototypeOf2.default)(t).constructor) : o.apply(t, e)); }
function _isNativeReflectConstruct() { try { var t = !Boolean.prototype.valueOf.call(_Reflect$construct(Boolean, [], function () {})); } catch (t) {} return (_isNativeReflectConstruct = function _isNativeReflectConstruct() { return !!t; })(); }
function _superPropGet(t, o, e, r) { var p = (0, _get2.default)((0, _getPrototypeOf2.default)(1 & r ? t.prototype : t), o, e); return 2 & r && "function" == typeof p ? function (t) { return p.apply(e, t); } : p; } /*!
 * Copyright (c) 2015-2020 Cisco Systems, Inc. See LICENSE file
 */ // @ts-ignore - backoff library does not have type definitions
var normalReconnectReasons = ['idle', 'done (forced)'];
var MOBIUS_SOCKET_NAMESPACE = 'MobiusSocket';
var TOKEN_REFRESH_INTERVAL_MS = 1 * 60 * 60 * 1000; // 1 hour

// Extended Socket type with dynamic properties

function normalizeMobiusAuthToken(token) {
  return token.replace(/^Bearer\s+/i, '');
}
var MobiusSocket = /*#__PURE__*/function (_EventEmitter) {
  function MobiusSocket(webex) {
    var _this;
    var config = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
    (0, _classCallCheck2.default)(this, MobiusSocket);
    _this = _callSuper(this, MobiusSocket);
    (0, _defineProperty2.default)(_this, "webex", void 0);
    (0, _defineProperty2.default)(_this, "config", void 0);
    (0, _defineProperty2.default)(_this, "logger", void 0);
    (0, _defineProperty2.default)(_this, "connected", void 0);
    (0, _defineProperty2.default)(_this, "connecting", void 0);
    (0, _defineProperty2.default)(_this, "hasEverConnected", void 0);
    (0, _defineProperty2.default)(_this, "socket", void 0);
    (0, _defineProperty2.default)(_this, "backoffCall", void 0);
    // backoff library has no types
    (0, _defineProperty2.default)(_this, "shutdownSwitchoverBackoffCall", void 0);
    // backoff library has no types
    (0, _defineProperty2.default)(_this, "seenAsyncEventIds", void 0);
    (0, _defineProperty2.default)(_this, "connectPromise", void 0);
    (0, _defineProperty2.default)(_this, "socketUrl", void 0);
    (0, _defineProperty2.default)(_this, "tokenRefreshTimer", void 0);
    (0, _defineProperty2.default)(_this, "tokenRefreshInFlight", void 0);
    if (!webex) {
      throw new Error('A Webex instance is required when initializing MobiusSocket');
    }
    _this.webex = webex;
    _this.config = config;
    _this.logger = webex.logger || console;
    _this.connected = false;
    _this.connecting = false;
    _this.hasEverConnected = false;
    _this.socket = undefined;
    _this.backoffCall = undefined;
    _this.shutdownSwitchoverBackoffCall = undefined;
    _this.seenAsyncEventIds = new _map.default();
    _this.connectPromise = undefined;
    _this.tokenRefreshTimer = undefined;
    _this.tokenRefreshInFlight = undefined;
    return _this;
  }
  (0, _inherits2.default)(MobiusSocket, _EventEmitter);
  return (0, _createClass2.default)(MobiusSocket, [{
    key: "off",
    value: function off(eventName, listener) {
      if (listener) {
        return _superPropGet(MobiusSocket, "off", this, 3)([eventName, listener]);
      }
      this.removeAllListeners(eventName);
      return this;
    }

    /**
     * Attach event listeners to a socket.
     * @param socket - The socket to attach listeners to
     */
  }, {
    key: "attachSocketEventListeners",
    value: function attachSocketEventListeners(socket) {
      var _this2 = this;
      socket.on('close', function (event) {
        return _this2.onclose(event, socket);
      });
      socket.on('message', function (event) {
        return _this2.onmessage(event);
      });
    }

    /**
     * Tracks a newly seen async_event ID and reports whether a duplicate should be suppressed.
     * @param envelope - Parsed websocket message envelope
     * @returns True when the event has already been seen
     */
  }, {
    key: "trackAsyncEventAndShouldSuppressDuplicate",
    value: function trackAsyncEventAndShouldSuppressDuplicate(envelope) {
      if ((envelope === null || envelope === void 0 ? void 0 : envelope.type) !== 'async_event' || !envelope.eventId) {
        return false;
      }
      if (this.seenAsyncEventIds.has(envelope.eventId)) {
        // Refresh recency so frequently retransmitted eventIds stay protected longer.
        // This deletion and setting again makes the data recent since javascript map maintains order as well
        var previousValue = this.seenAsyncEventIds.get(envelope.eventId) || true;
        this.seenAsyncEventIds.delete(envelope.eventId);
        this.seenAsyncEventIds.set(envelope.eventId, previousValue);
        this.logger.info("".concat(MOBIUS_SOCKET_NAMESPACE, ": duplicate async_event suppressed, eventId=").concat(envelope.eventId));
        return true;
      }
      this.logger.log("".concat(MOBIUS_SOCKET_NAMESPACE, ": tracking async_event, eventId=").concat(envelope.eventId));
      this.seenAsyncEventIds.set(envelope.eventId, true);
      if (this.config.dedupCacheMaxSize && this.seenAsyncEventIds.size > this.config.dedupCacheMaxSize) {
        var oldestEventId = this.seenAsyncEventIds.keys().next().value || '';
        this.seenAsyncEventIds.delete(oldestEventId);
        this.logger.log("".concat(MOBIUS_SOCKET_NAMESPACE, ": evicted oldest async_event from dedup cache, eventId=").concat(oldestEventId));
      }
      return false;
    }

    /**
     * Handle imminent shutdown by establishing a new connection while keeping
     * the current one alive (make-before-break).
     * Idempotent: will no-op if already in progress.
     */
  }, {
    key: "handleImminentShutdown",
    value: function handleImminentShutdown() {
      var _this3 = this;
      var oldSocket = this.socket;
      try {
        if (this.shutdownSwitchoverBackoffCall) {
          this.logger.info("".concat(MOBIUS_SOCKET_NAMESPACE, ": [shutdown] switchover already in progress"));
          return;
        }
        this.logger.info("".concat(MOBIUS_SOCKET_NAMESPACE, ": [shutdown] switchover start"));
        this.connectWithBackoff(undefined, {
          isShutdownSwitchover: true,
          attemptOptions: {
            isShutdownSwitchover: true,
            onSuccess: function onSuccess(newSocket, webSocketUrl) {
              _this3.logger.info("".concat(MOBIUS_SOCKET_NAMESPACE, ": [shutdown] switchover connected, url: ").concat(webSocketUrl));

              // Promote the new socket now that the switchover succeeded
              newSocket.connecting = false;
              newSocket.connected = true;
              _this3.socket = newSocket;
              _this3.connected = true;
              _this3.emitEvent('event:mobius_shutdown_switchover_complete', {
                url: webSocketUrl
              });
              if (oldSocket) {
                _this3.logger.info("".concat(MOBIUS_SOCKET_NAMESPACE, ": [shutdown] old socket retained; server will close with 4001"));
              }
            }
          }
        }).then(function () {
          _this3.logger.info("".concat(MOBIUS_SOCKET_NAMESPACE, ": [shutdown] switchover completed successfully"));
        }).catch(function (err) {
          _this3.logger.info("".concat(MOBIUS_SOCKET_NAMESPACE, ": [shutdown] switchover exhausted retries; will fall back to normal reconnection: "), err);
          _this3.emitEvent('event:mobius_shutdown_switchover_failed', {
            reason: err
          });
        });
      } catch (e) {
        this.logger.error("".concat(MOBIUS_SOCKET_NAMESPACE, ": [shutdown] error during switchover"), e);
        this.shutdownSwitchoverBackoffCall = undefined;
        this.emitEvent('event:mobius_shutdown_switchover_failed', {
          reason: e
        });
      }
    }

    /**
     * Get the websocket URL for the currently connected socket.
     * @returns The connected websocket URL, or undefined when not connected
     */
  }, {
    key: "getConnectedWebSocketUrl",
    value: function getConnectedWebSocketUrl() {
      var _this$socket;
      if (!((_this$socket = this.socket) !== null && _this$socket !== void 0 && _this$socket.connected)) {
        return undefined;
      }
      return this.socket.url;
    }

    /**
     * Sends a websocket request and resolves when the matching response arrives.
     * @param payload - The websocket request payload
     * @param options - Additional request options
     * @returns Promise that resolves with the socket response
     */
  }, {
    key: "sendWssRequest",
    value: function sendWssRequest(payload) {
      var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
      if (!payload || (0, _typeof2.default)(payload) !== 'object' || (0, _isArray.default)(payload)) {
        return _promise.default.reject(new Error('`payload` is required'));
      }
      if (!this.socket || !this.socket.connected) {
        return _promise.default.reject(new Error('Mobius socket is not connected'));
      }
      return this.socket.sendRequest(payload, {
        timeout: options.timeout
      });
    }

    /**
     * Check if the socket is connected.
     * @returns True if connected
     */
  }, {
    key: "isConnected",
    value: function isConnected() {
      return this.connected;
    }

    /**
     * Connect to Mobius.
     * @param webSocketUrl - Optional websocket URL override. Falls back to the device websocket URL
     * @returns Promise that resolves when connection flow completes
     */
  }, {
    key: "connect",
    value: function connect(webSocketUrl) {
      var _this$socket2,
        _this$socket3,
        _this$webex$internal$,
        _this$webex$internal$2,
        _this4 = this;
      if (this.connectPromise) {
        this.logger.info("".concat(MOBIUS_SOCKET_NAMESPACE, ": connection already in progress, returning existing promise"));
        return this.connectPromise;
      }
      if ((_this$socket2 = this.socket) !== null && _this$socket2 !== void 0 && _this$socket2.connected || (_this$socket3 = this.socket) !== null && _this$socket3 !== void 0 && _this$socket3.connecting) {
        this.logger.info("".concat(MOBIUS_SOCKET_NAMESPACE, ": already connected, will not connect again"));
        return _promise.default.resolve();
      }

      // Treat a connect() call that targets a different Mobius websocket URL
      // as a fresh initial connection for retry purposes.
      if (webSocketUrl && this.socketUrl && webSocketUrl !== this.socketUrl) {
        this.hasEverConnected = false;
      }

      // Cache the caller-provided URL for reconnect
      if (webSocketUrl) {
        this.socketUrl = webSocketUrl;
      }
      this.connecting = true;
      this.logger.info("".concat(MOBIUS_SOCKET_NAMESPACE, ": starting connection attempt").concat(Number(this.config.initialConnectionMaxRetries) === 0 && !this.hasEverConnected ? ' (initial retries disabled)' : ''));
      var connectPromise = _promise.default.resolve(this.webex.internal.device.registered || ((_this$webex$internal$ = (_this$webex$internal$2 = this.webex.internal.device).register) === null || _this$webex$internal$ === void 0 ? void 0 : _this$webex$internal$.call(_this$webex$internal$2))).then(function () {
        _this4.logger.info("".concat(MOBIUS_SOCKET_NAMESPACE, ": connecting"));
        return _this4.connectWithBackoff(_this4.socketUrl);
      }).finally(function () {
        _this4.connectPromise = undefined;
      });
      this.connectPromise = connectPromise;
      return connectPromise;
    }

    /**
     * Disconnect the Mobius socket.
     * @param options - Optional websocket close options (code, reason)
     * @returns Promise that resolves after disconnect cleanup and close handling complete
     */
  }, {
    key: "disconnect",
    value: function disconnect(options) {
      var _this5 = this;
      this.logger.info("".concat(MOBIUS_SOCKET_NAMESPACE, "#disconnect: connecting state: ").concat(this.connecting, ",\n       connected state: ").concat(this.connected, ", socket exists: ").concat(!!this.socket, ",\n       options: ").concat((0, _stringify.default)(options)));
      if (this.backoffCall) {
        this.logger.info("".concat(MOBIUS_SOCKET_NAMESPACE, ": aborting connection"));
        this.backoffCall.abort();
        this.backoffCall = undefined;
      }
      if (this.shutdownSwitchoverBackoffCall) {
        this.logger.info("".concat(MOBIUS_SOCKET_NAMESPACE, ": aborting shutdown switchover connection"));
        this.shutdownSwitchoverBackoffCall.abort();
        this.shutdownSwitchoverBackoffCall = undefined;
      }
      this.connectPromise = undefined;
      this.seenAsyncEventIds.clear();
      if (!this.socket) {
        this.connected = false;
        this.stopTokenRefreshTimer();
        return _promise.default.resolve();
      }
      this.socket.removeAllListeners('message');
      this.socket.connecting = false;
      this.socket.connected = false;
      return _promise.default.resolve(this.socket.close(options || undefined)).finally(function () {
        _this5.connected = false;
        _this5.stopTokenRefreshTimer();
      });
    }
  }, {
    key: "prepareUrl",
    value: function prepareUrl(webSocketUrl) {
      if (!webSocketUrl) {
        // TODO: Circle back to this logic when mobius implements the shutdown switchover
        webSocketUrl = this.webex.internal.device.webSocketUrl || '';
      }
      return _promise.default.resolve(webSocketUrl);
    }
  }, {
    key: "attemptConnection",
    value: function attemptConnection(socketUrl, callback) {
      var _this6 = this;
      var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};
      var _options$isShutdownSw = options.isShutdownSwitchover,
        isShutdownSwitchover = _options$isShutdownSw === void 0 ? false : _options$isShutdownSw,
        _options$attemptOptio = options.attemptOptions,
        attemptOptions = _options$attemptOptio === void 0 ? {} : _options$attemptOptio;
      var _attemptOptions$onSuc = attemptOptions.onSuccess,
        onSuccess = _attemptOptions$onSuc === void 0 ? null : _attemptOptions$onSuc;
      var socket = new _socket.default();
      socket.connecting = true;
      var newWSUrl;
      this.attachSocketEventListeners(socket);
      var backoffCall = isShutdownSwitchover ? this.shutdownSwitchoverBackoffCall : this.backoffCall;

      // Check appropriate backoff call based on connection type
      if (!backoffCall) {
        var mode = isShutdownSwitchover ? 'switchover backoff call' : 'backoffCall';
        var msg = "".concat(MOBIUS_SOCKET_NAMESPACE, ": prevent socket open when ").concat(mode, " no longer defined");
        var err = new Error(msg);
        this.logger.info(msg);
        // Call the callback with the error before rejecting
        callback(err);
        return _promise.default.reject(err);
      }

      // For shutdown switchover, don't set socket yet (make-before-break)
      // For normal connection, set socket before opening to allow disconnect() to close it
      if (!isShutdownSwitchover) {
        this.socket = socket;
      }
      return this.prepareAndOpenSocket(socket, socketUrl, isShutdownSwitchover).then(function (webSocketUrl) {
        newWSUrl = webSocketUrl;
        _this6.logger.info("".concat(MOBIUS_SOCKET_NAMESPACE, ": ").concat(isShutdownSwitchover ? '[shutdown] switchover' : '', " connected to mobius socket, success, url: ").concat(newWSUrl));

        // Custom success handler for shutdown switchover
        if (onSuccess) {
          onSuccess(socket, webSocketUrl);
          callback();
          return _promise.default.resolve();
        }

        // Default behavior for normal connection
        callback();
        return _promise.default.resolve();
      }).catch(function (reason) {
        // For shutdown, simpler error handling - just callback for retry
        if (isShutdownSwitchover) {
          _this6.logger.info("".concat(MOBIUS_SOCKET_NAMESPACE, ": [shutdown] switchover attempt failed"), reason);
          return callback(reason);
        }

        // Normal connection error handling
        var backoffCallNormal = _this6.backoffCall;
        // Suppress connection errors that appear to be network related (code 1006).
        if (reason.code !== 1006 && backoffCallNormal && (backoffCallNormal === null || backoffCallNormal === void 0 ? void 0 : backoffCallNormal.getNumRetries()) > 0) {
          _this6.emitEvent('connection_failed', reason, {
            retries: backoffCallNormal === null || backoffCallNormal === void 0 ? void 0 : backoffCallNormal.getNumRetries()
          });
        }
        _this6.logger.info("".concat(MOBIUS_SOCKET_NAMESPACE, ": connection attempt failed"), reason, (backoffCallNormal === null || backoffCallNormal === void 0 ? void 0 : backoffCallNormal.getNumRetries()) === 0 ? reason.stack : '');

        // UnknownResponse is produced by IE for any 4XXX; treat it like a bad
        // web socket url and let WDM handle the token checking
        if (reason instanceof _errors.UnknownResponse) {
          var _this6$webex$internal, _this6$webex$internal2;
          _this6.logger.info("".concat(MOBIUS_SOCKET_NAMESPACE, ": received unknown response code, refreshing device registration"));
          return (_this6$webex$internal = (_this6$webex$internal2 = _this6.webex.internal.device).refresh) === null || _this6$webex$internal === void 0 ? void 0 : _this6$webex$internal.call(_this6$webex$internal2).then(function () {
            return callback(reason);
          });
        }
        // NotAuthorized implies expired token
        if (reason instanceof _errors.NotAuthorized) {
          var _this6$webex$credenti, _this6$webex$credenti2;
          _this6.logger.info("".concat(MOBIUS_SOCKET_NAMESPACE, ": received authorization error, reauthorizing"));
          return (_this6$webex$credenti = (_this6$webex$credenti2 = _this6.webex.credentials).refresh) === null || _this6$webex$credenti === void 0 ? void 0 : _this6$webex$credenti.call(_this6$webex$credenti2, {
            force: true
          }).then(function () {
            return callback(reason);
          });
        }
        if (reason instanceof _errors.BadRequest || reason instanceof _errors.Forbidden) {
          _this6.logger.warn("".concat(MOBIUS_SOCKET_NAMESPACE, ": received unrecoverable response from ").concat(MOBIUS_SOCKET_NAMESPACE));
          backoffCallNormal === null || backoffCallNormal === void 0 ? void 0 : backoffCallNormal.abort();
          return callback(reason);
        }
        return callback(reason);
      }).catch(function (reason) {
        _this6.logger.error("".concat(MOBIUS_SOCKET_NAMESPACE, ": failed to handle connection failure"), reason);
        callback(reason);
      });
    }
  }, {
    key: "prepareAndOpenSocket",
    value: function prepareAndOpenSocket(socket, socketUrl) {
      var _this7 = this;
      var isShutdownSwitchover = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false;
      var logPrefix = isShutdownSwitchover ? '[shutdown] switchover' : 'connection';
      return _promise.default.all([this.prepareUrl(socketUrl), this.webex.credentials.getUserToken()]).then(function (_ref) {
        var _ref2 = (0, _slicedToArray2.default)(_ref, 2),
          webSocketUrl = _ref2[0],
          token = _ref2[1];
        var options = {
          forceCloseDelay: _this7.config.forceCloseDelay,
          wssResponseTimeout: _this7.config.wssResponseTimeout,
          token: normalizeMobiusAuthToken(token.toString()),
          refreshToken: function refreshToken() {
            return _this7.refreshToken();
          },
          trackingId: "".concat(_this7.webex.sessionId, "_").concat((0, _now.default)()),
          logger: _this7.logger
        };
        if (_this7.webex.config.defaultMobiusSocketOptions) {
          var customOptionsMsg = isShutdownSwitchover ? 'setting custom options for switchover' : 'setting custom options';
          _this7.logger.info("".concat(MOBIUS_SOCKET_NAMESPACE, ": ").concat(customOptionsMsg));
          options = _objectSpread(_objectSpread({}, options), _this7.webex.config.defaultMobiusSocketOptions);
        }

        // Only promote the socket reference for normal connections.
        // Shutdown switchover keeps the old socket active until the new one succeeds.
        if (!isShutdownSwitchover) {
          _this7.socket = socket;
        }
        _this7.logger.info("".concat(MOBIUS_SOCKET_NAMESPACE, " ").concat(logPrefix, " url: ").concat(webSocketUrl));
        return socket.open(webSocketUrl, options).then(function () {
          return webSocketUrl;
        });
      });
    }
  }, {
    key: "connectWithBackoff",
    value: function connectWithBackoff(webSocketUrl) {
      var _this8 = this;
      var context = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
      var _context$isShutdownSw = context.isShutdownSwitchover,
        isShutdownSwitchover = _context$isShutdownSw === void 0 ? false : _context$isShutdownSw,
        _context$attemptOptio = context.attemptOptions,
        attemptOptions = _context$attemptOptio === void 0 ? {} : _context$attemptOptio;
      return new _promise.default(function (resolve, reject) {
        var call;
        var isInitialConnect = !isShutdownSwitchover && !_this8.hasEverConnected;
        var initialRetryLimit = _this8.config.initialConnectionMaxRetries == null ? null : Number(_this8.config.initialConnectionMaxRetries);
        var isInitialConnectWithoutRetries = isInitialConnect && initialRetryLimit === 0;
        var onComplete = function onComplete(err) {
          if (isShutdownSwitchover) {
            _this8.shutdownSwitchoverBackoffCall = undefined;
          } else {
            _this8.backoffCall = undefined;
          }
          if (err) {
            var msg = isShutdownSwitchover ? "[shutdown] switchover failed after ".concat(call.getNumRetries(), " retries") : "failed to connect after ".concat(call.getNumRetries(), " retries");
            _this8.logger.info("".concat(MOBIUS_SOCKET_NAMESPACE, ": ").concat(msg, "; ").concat(err));
            // Only mutate socket flags for normal connections.
            // During shutdown switchover, this.socket is the old live socket — don't touch it.
            if (!isShutdownSwitchover && _this8.socket) {
              _this8.socket.connecting = false;
              _this8.socket.connected = false;
            }
            return reject(err);
          }

          // For normal connections, mark the socket as connected.
          // Shutdown switchover promotion is handled by the onSuccess callback.
          if (!isShutdownSwitchover && _this8.socket) {
            _this8.socket.connecting = false;
            _this8.socket.connected = true;
          }
          if (!isShutdownSwitchover) {
            _this8.connecting = false;
            _this8.connected = true;
            _this8.hasEverConnected = true;
            _this8.startTokenRefreshTimer();
            _this8.emitEvent('online');
          }
          return resolve();
        };
        // eslint-disable-next-line prefer-reflect
        call = _backoff.default.call(function (callback) {
          var attemptNum = call.getNumRetries();
          var attemptLogPrefix = isShutdownSwitchover ? '[shutdown] switchover' : 'connection';
          _this8.logger.info("".concat(MOBIUS_SOCKET_NAMESPACE, ": executing ").concat(attemptLogPrefix, " attempt ").concat(attemptNum));
          _this8.attemptConnection(webSocketUrl, callback, attemptOptions);
        }, function (err) {
          return onComplete(err);
        });
        call.setStrategy(new _backoff.default.ExponentialStrategy({
          initialDelay: _this8.config.backoffTimeReset,
          maxDelay: _this8.config.backoffTimeMax
        }));
        if (isInitialConnectWithoutRetries) {
          call.retryIf(function () {
            return false;
          });
        } else if (isInitialConnect && initialRetryLimit !== null && initialRetryLimit > 0) {
          call.failAfter(initialRetryLimit);
        } else if (_this8.config.maxRetries) {
          call.failAfter(_this8.config.maxRetries);
        }

        // Store backoff call reference BEFORE starting (so it's available in attemptConnection)
        if (isShutdownSwitchover) {
          _this8.shutdownSwitchoverBackoffCall = call;
        } else {
          _this8.backoffCall = call;
        }
        call.on('abort', function () {
          var msg = isShutdownSwitchover ? 'Shutdown Switchover' : 'Connection';
          _this8.logger.info("".concat(MOBIUS_SOCKET_NAMESPACE, ": ").concat(msg, " aborted"));
          reject(new Error("MobiusSocket ".concat(msg, " Aborted")));
        });
        call.on('callback', function (err) {
          if (err) {
            if (isInitialConnectWithoutRetries) {
              _this8.logger.info("".concat(MOBIUS_SOCKET_NAMESPACE, ": initial connect failed; retries already disabled"));
              return;
            }
            var number = call.getNumRetries();
            var delay = Math.min(call.strategy_.nextBackoffDelay_, _this8.config.backoffTimeMax || Infinity);
            var callbackLogPrefix = isShutdownSwitchover ? '[shutdown] switchover' : '';
            _this8.logger.info("".concat(MOBIUS_SOCKET_NAMESPACE, ": ").concat(callbackLogPrefix, " failed to connect; attempting retry ").concat(number + 1, " in ").concat(delay, " ms"));
            /* istanbul ignore if */
            if (process.env.NODE_ENV === 'development') {
              _this8.logger.debug("".concat(MOBIUS_SOCKET_NAMESPACE, ": "), err, err.stack);
            }
            return;
          }
          _this8.logger.info("".concat(MOBIUS_SOCKET_NAMESPACE, ": connected"));
        });
        call.start();
      });
    }

    /**
     * Safely emits an event, catching and logging any errors from event handlers.
     * @param eventName - The name of the event to emit
     * @param args - Arguments to pass to event handlers
     */
  }, {
    key: "emitEvent",
    value: function emitEvent(eventName) {
      for (var _len = arguments.length, args = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
        args[_key - 1] = arguments[_key];
      }
      try {
        if (!eventName) {
          return;
        }
        this.emit.apply(this, [eventName].concat(args));
      } catch (error) {
        // Safely handle errors without causing additional issues during cleanup
        this.logger.error("".concat(MOBIUS_SOCKET_NAMESPACE, ": error occurred in event handler:"), error, ' with args: ', [eventName].concat(args));
      }
    }

    /**
     * Starts a periodic timer to refresh the authentication token.
     * Token refresh occurs every hour while connected.
     */
  }, {
    key: "startTokenRefreshTimer",
    value: function startTokenRefreshTimer() {
      var _this9 = this;
      if (this.tokenRefreshTimer || !this.connected) {
        return;
      }
      this.tokenRefreshTimer = setInterval(function () {
        _this9.refreshToken().catch(function (error) {
          _this9.logger.error("".concat(MOBIUS_SOCKET_NAMESPACE, ": periodic token refresh failed"), error);
        });
      }, TOKEN_REFRESH_INTERVAL_MS);
    }

    /**
     * Stops the periodic token refresh timer.
     */
  }, {
    key: "stopTokenRefreshTimer",
    value: function stopTokenRefreshTimer() {
      if (!this.tokenRefreshTimer) {
        return;
      }
      clearInterval(this.tokenRefreshTimer);
      this.tokenRefreshTimer = undefined;
    }

    /**
     * Refreshes the authentication token and re-authenticates the socket connection.
     * @returns Promise that resolves when token refresh and re-authentication complete
     */
  }, {
    key: "refreshToken",
    value: function refreshToken() {
      var _this$webex$credentia,
        _this$webex$credentia2,
        _this$webex$credentia3,
        _this0 = this;
      if (this.tokenRefreshInFlight) {
        return this.tokenRefreshInFlight;
      }
      if (!this.connected) {
        this.stopTokenRefreshTimer();
        return _promise.default.resolve();
      }
      var tokenPromise = this.webex.credentials.canRefresh ? (_this$webex$credentia = (_this$webex$credentia2 = this.webex.credentials).refresh) === null || _this$webex$credentia === void 0 ? void 0 : (_this$webex$credentia3 = _this$webex$credentia.call(_this$webex$credentia2, {
        force: true
      })) === null || _this$webex$credentia3 === void 0 ? void 0 : _this$webex$credentia3.then(function () {
        return _this0.webex.credentials.getUserToken();
      }) : this.webex.credentials.getUserToken();
      this.tokenRefreshInFlight = tokenPromise.then(function (token) {
        var _this0$socket;
        if (!token) {
          throw new Error('Mobius token refresh did not return a token');
        }
        var refreshedToken = normalizeMobiusAuthToken(token.toString());
        if (!((_this0$socket = _this0.socket) !== null && _this0$socket !== void 0 && _this0$socket.connected)) {
          _this0.logger.warn("".concat(MOBIUS_SOCKET_NAMESPACE, ": socket is not connected, skipping token refresh"));
          return undefined;
        }
        return _this0.socket.refresh(refreshedToken);
      }).catch(function (error) {
        _this0.logger.error("".concat(MOBIUS_SOCKET_NAMESPACE, ": failed to refresh/re-auth Mobius socket"), error);
        throw error;
      }).finally(function () {
        _this0.tokenRefreshInFlight = undefined;
      });
      return this.tokenRefreshInFlight;
    }
  }, {
    key: "onclose",
    value: function () {
      var _onclose = (0, _asyncToGenerator2.default)(/*#__PURE__*/_regenerator.default.mark(function _callee(event, sourceSocket) {
        var _this1 = this,
          _this$socket4;
        var loggerContext, reason, socketUrl, isActiveSocket, _t, _t2;
        return _regenerator.default.wrap(function (_context) {
          while (1) switch (_context.prev = _context.next) {
            case 0:
              loggerContext = {
                file: 'mobius-socket.ts',
                method: 'onclose'
              };
              _context.prev = 1;
              reason = event.reason && event.reason.toLowerCase();
              isActiveSocket = sourceSocket === this.socket;
              if (sourceSocket) {
                socketUrl = sourceSocket.url;
              }

              // Only tear down state if the currently active socket closed
              if (isActiveSocket) {
                if (this.socket) {
                  this.socket.removeAllListeners();
                  this.socket = undefined;
                  this.emitEvent('offline', event);
                }
                this.connecting = false;
                this.connected = false;
                this.stopTokenRefreshTimer();
              } else {
                // Old socket closed; do not flip connection state
                this.logger.info("[shutdown] non-active socket closed, code=".concat(event.code), loggerContext);
                // Clean up listeners from old socket now that it's closed
                if (sourceSocket) {
                  sourceSocket.removeAllListeners();
                }
              }
              _t = event.code;
              _context.next = _t === 1003 ? 2 : _t === 4000 ? 3 : _t === 4001 ? 4 : _t === 1000 ? 5 : _t === 1001 ? 5 : _t === 1005 ? 6 : _t === 1006 ? 6 : _t === 1011 ? 6 : _t === 1012 ? 6 : _t === 3050 ? 7 : _t === 4401 ? 8 : _t === 4403 ? 8 : _t === 4404 ? 8 : _t === 4429 ? 11 : 12;
              break;
            case 2:
              this.logger.info("service rejected last message; will not reconnect: ".concat(event.reason), loggerContext);
              if (isActiveSocket) this.emitEvent('offline.permanent', event);
              return _context.abrupt("continue", 13);
            case 3:
              this.logger.info("socket replaced; will not reconnect", loggerContext);
              if (isActiveSocket) this.emitEvent('offline.replaced', event);
              return _context.abrupt("continue", 13);
            case 4:
              // Handle the same way we do for registration.down event from Mobius.
              this.logger.info("socket closed with 4001; will not reconnect", loggerContext);
              this.emitEvent('event:async_event', _constants.MOBIUS_SOCKET_4001_EVENT);
              return _context.abrupt("continue", 13);
            case 5:
              this.logger.info("socket disconnected; ".concat(event.reason), loggerContext);
              if (isActiveSocket) this.emitEvent('offline.permanent', event);
              return _context.abrupt("continue", 13);
            case 6:
              this.logger.info("socket disconnected; reconnecting", loggerContext);
              if (isActiveSocket) {
                this.emitEvent('offline.transient', event);
                this.reconnect(socketUrl);
              }
              return _context.abrupt("continue", 13);
            case 7:
              if (reason && normalReconnectReasons.includes(reason)) {
                this.logger.info("socket disconnected; reconnecting", loggerContext);
                if (isActiveSocket) {
                  this.emitEvent('offline.transient', event);
                  this.reconnect(socketUrl);
                }
              } else {
                this.logger.info("socket disconnected; will not reconnect: ".concat(event.reason), loggerContext);
                if (isActiveSocket) this.emitEvent('offline.permanent', event);
              }
              return _context.abrupt("continue", 13);
            case 8:
              this.logger.error("onclose, statusCode=".concat(event.code), loggerContext);
              _context.next = 9;
              return this.refreshToken().catch(function (error) {
                _this1.logger.error("".concat(MOBIUS_SOCKET_NAMESPACE, ": periodic token refresh failed"), error);
              });
            case 9:
              _context.next = 10;
              return this.reconnect((_this$socket4 = this.socket) === null || _this$socket4 === void 0 ? void 0 : _this$socket4.url);
            case 10:
              return _context.abrupt("continue", 13);
            case 11:
              // Silently ignore too many requests
              this.logger.error("too many requests, statusCode=".concat(event.code), loggerContext);
              return _context.abrupt("continue", 13);
            case 12:
              this.logger.info("socket disconnected unexpectedly; will not reconnect", loggerContext);
              if (isActiveSocket) this.emitEvent('offline.permanent', event);
            case 13:
              _context.next = 15;
              break;
            case 14:
              _context.prev = 14;
              _t2 = _context["catch"](1);
              this.logger.error("error occurred in close handler", _t2, loggerContext);
            case 15:
            case "end":
              return _context.stop();
          }
        }, _callee, this, [[1, 14]]);
      }));
      function onclose(_x, _x2) {
        return _onclose.apply(this, arguments);
      }
      return onclose;
    }()
  }, {
    key: "onmessage",
    value: function onmessage(event) {
      var loggerContext = {
        file: 'mobius-socket.ts',
        method: 'onmessage'
      };
      var envelope = event.data;
      this.logger.debug("message envelope: ", envelope, loggerContext);

      // Handle shutdown message shape: { type: 'shutdown' }
      if (envelope && envelope.type === 'shutdown') {
        this.logger.info("[shutdown] imminent shutdown message received", loggerContext);
        this.emitEvent('event:mobius_shutdown_imminent', envelope); // This is not yet not implemented, keeping for future support

        this.handleImminentShutdown();
        return _promise.default.resolve();
      }
      if (this.trackAsyncEventAndShouldSuppressDuplicate(envelope)) {
        return _promise.default.resolve();
      }

      // Emit event:<type> for typed messages (e.g., register.response)
      if (envelope.type) {
        this.emitEvent("event:".concat(envelope.type), envelope);
      }

      // Use data/payload if present, otherwise treat the envelope itself as the data (flat format)
      var data = envelope.data || envelope;

      // Support both Mobius-enveloped (data.eventType) and flat (eventType) formats
      var eventType = (data === null || data === void 0 ? void 0 : data.eventType) || envelope.eventType;
      if (!eventType) {
        this.emitEvent('event', envelope);
        return _promise.default.resolve();
      }
      try {
        // TODO: Remove if event:namespace is not required
        this.emitEvent('event', envelope);
        var _eventType$split = eventType.split('.'),
          _eventType$split2 = (0, _slicedToArray2.default)(_eventType$split, 1),
          namespace = _eventType$split2[0];
        this.emitEvent("event:".concat(namespace), envelope);
        if (namespace !== eventType) {
          this.emitEvent("event:".concat(eventType), envelope);
        }
      } catch (reason) {
        this.logger.error("error occurred processing socket message", reason, loggerContext);
      }
      return _promise.default.resolve();
    }
  }, {
    key: "reconnect",
    value: function reconnect(webSocketUrl) {
      this.logger.info("reconnecting", {
        file: 'mobius-socket.ts',
        method: 'reconnect'
      });
      return this.connect(webSocketUrl || this.socketUrl);
    }
  }]);
}(_events.EventEmitter);
var _default = exports.default = MobiusSocket;
//# sourceMappingURL=mobius-socket.js.map
