UNPKG

@bitrix24/b24jssdk

Version:

Bitrix24 REST API JavaScript SDK

2,233 lines 69.5 kB
/**
 * @package @bitrix24/b24jssdk
 * @version 2.2.0
 * @copyright (c) 2026 Bitrix24
 * @license MIT
 * @see https://github.com/bitrix24/b24jssdk
 * @see https://bitrix24.github.io/b24jssdk/
 */
import { Type } from '../tools/type.mjs';
import { Text } from '../tools/text.mjs';
import { Browser } from '../tools/browser.mjs';
import { REDACTED_PLACEHOLDER, redactSensitiveUrl, redactSensitiveParams } from '../core/http/redact.mjs';
import { StorageManager } from './storage-manager.mjs';
import { JsonRpc } from './json-rpc.mjs';
import { SharedConfig } from './shared-config.mjs';
import { ChannelManager } from './channel-manager.mjs';
import { Receiver, IncomingMessage, RequestBatch, ResponseBatch } from './protobuf/index.mjs';
import { ConnectionType, PullStatus, CloseReasons, LsKeys, SubscriptionType, RpcMethod, ServerMode, SenderType, SystemCommands } from '../types/pull.mjs';
import { WebSocketConnector } from './web-socket-connector.mjs';
import { LongPollingConnector } from './long-polling-connector.mjs';
import { LoggerFactory } from '../logger/logger-factory.mjs';

var __defProp = Object.defineProperty;
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
const REVISION = 19;
const RESTORE_WEBSOCKET_TIMEOUT = 30 * 60;
const OFFLINE_STATUS_DELAY = 5e3;
const CONFIG_CHECK_INTERVAL = 60 * 1e3;
const MAX_IDS_TO_STORE = 10;
const PING_TIMEOUT = 10;
const JSON_RPC_PING = "ping";
const JSON_RPC_PONG = "pong";
const LS_SESSION = "bx-pull-session";
const LS_SESSION_CACHE_TIME = 20;
const EmptyConfig = {
  api: {},
  channels: {},
  publicChannels: {},
  server: { timeShift: 0 },
  clientId: null,
  jwt: null,
  exp: 0
};
class PullClient {
  static {
    __name(this, "PullClient");
  }
  // region Params ////
  _logger;
  _restClient;
  _status;
  _context;
  _guestMode;
  _guestUserId;
  _userId;
  _configGetMethod;
  _getPublicListMethod;
  _siteId;
  _enabled;
  _unloading = false;
  _starting = false;
  _debug = false;
  _connectionAttempt = 0;
  _connectionType = ConnectionType.WebSocket;
  _skipStorageInit;
  _skipCheckRevision;
  _subscribers = {};
  _watchTagsQueue = /* @__PURE__ */ new Map();
  _watchUpdateInterval = 174e4;
  _watchForceUpdateInterval = 5e3;
  _configTimestamp = 0;
  _session = {
    mid: null,
    tag: null,
    time: null,
    history: {},
    lastMessageIds: [],
    messageCount: 0
  };
  _connectors = {
    [ConnectionType.Undefined]: null,
    [ConnectionType.WebSocket]: null,
    [ConnectionType.LongPolling]: null
  };
  _isSecure;
  _config = null;
  _storage = null;
  _sharedConfig;
  _channelManager;
  _jsonRpcAdapter = null;
  /**
   * @depricate
   */
  // private _notificationPopup: null = null
  // timers ////
  _reconnectTimeout = null;
  _restartTimeout = null;
  _restoreWebSocketTimeout = null;
  _checkInterval = null;
  _offlineTimeout = null;
  _watchUpdateTimeout = null;
  _pingWaitTimeout = null;
  // Single source of truth for every tracked timer field. armTimeout()/
  // clearAllTimers() route through this list so a future timer cannot be added
  // without a matching teardown — the leak class behind #141 (#222). Individual
  // fields are kept because call-sites read their identity (e.g. `if
  // (this._reconnectTimeout)`), but clearAllTimers() is the one teardown path.
  _timerFields = [
    "_reconnectTimeout",
    "_restartTimeout",
    "_restoreWebSocketTimeout",
    "_checkInterval",
    "_offlineTimeout",
    "_watchUpdateTimeout",
    "_pingWaitTimeout"
  ];
  // Records every window listener init() registered so removeAllWindowListeners()
  // can drop exactly what was added, keeping arm/teardown symmetric (#222).
  _windowListeners = [];
  // manual stop workaround ////
  _isManualDisconnect = false;
  // set once destroy() has run; gates reconnect / watch / online so a torn-down
  // client never schedules new work (#141) ////
  _disposed = false;
  _loggingEnabled = false;
  // bound event handlers, stored so they can be removed in destroy() (#141) ////
  _onPingTimeoutHandler;
  _onBeforeUnloadHandler;
  _onOfflineHandler;
  _onOnlineHandler;
  // [userId] => array of callbacks
  _userStatusCallbacks = {};
  _connectPromise = null;
  _startingPromise = null;
  // Monotonic token bumped on every start() and on destroy(). start()'s
  // loadConfig()/connect() continuations capture the value at kick-off and bail
  // if it no longer matches — deterministically neutralising an in-flight start()
  // when destroy() (or a fresh start()) supersedes it, even for overlapping
  // start/destroy/start. The underlying rest transport
  // (this._restClient.actions.v2.call.make) does not accept an AbortSignal, so
  // this generation-token approach is used instead of an AbortController; the
  // network round-trip itself is not cancelled (remaining follow-up). (#222)
  _startGeneration = 0;
  // endregion ////
  // region Init ////
  /**
   * @param params
   */
  constructor(params) {
    this._logger = LoggerFactory.createNullLogger();
    this._restClient = params.b24;
    this._status = PullStatus.Offline;
    this._context = "master";
    if (params.restApplication) {
      if (typeof params.configGetMethod === "undefined") {
        params.configGetMethod = "pull.application.config.get";
      }
      if (typeof params.skipCheckRevision === "undefined") {
        params.skipCheckRevision = true;
      }
      if (Type.isStringFilled(params.restApplication)) {
        params.siteId = params.restApplication;
      }
      params.serverEnabled = true;
    }
    this._guestMode = params.guestMode ? Text.toBoolean(params.guestMode) : false;
    this._guestUserId = params.guestUserId ? Text.toInteger(params.guestUserId) : 0;
    if (this._guestMode && this._guestUserId > 0) {
      this._userId = this._guestUserId;
    } else {
      this._guestMode = false;
      this._userId = params.userId ? Text.toInteger(params.userId) : 0;
    }
    this._siteId = params.siteId ?? "none";
    this._enabled = !Type.isUndefined(params.serverEnabled) ? params.serverEnabled === true : true;
    this._configGetMethod = !Type.isStringFilled(params.configGetMethod) ? "pull.config.get" : params.configGetMethod || "";
    this._getPublicListMethod = !Type.isStringFilled(params.getPublicListMethod) ? "pull.channel.public.list" : params.getPublicListMethod || "";
    this._skipStorageInit = params.skipStorageInit === true;
    this._skipCheckRevision = params.skipCheckRevision === true;
    if (!Type.isUndefined(params.configTimestamp)) {
      this._configTimestamp = Text.toInteger(params.configTimestamp);
    }
    this._isSecure = typeof document !== "undefined" && document?.location.href.indexOf("https") === 0;
    if (this._userId && !this._skipStorageInit) {
      this._storage = new StorageManager({
        userId: this._userId,
        siteId: this._siteId
      });
    }
    this._sharedConfig = new SharedConfig({
      onWebSocketBlockChanged: this.onWebSocketBlockChanged.bind(this),
      storage: this._storage
    });
    this._channelManager = new ChannelManager({
      b24: this._restClient,
      getPublicListMethod: this._getPublicListMethod
    });
    this._loggingEnabled = this._sharedConfig.isLoggingEnabled();
    this._onPingTimeoutHandler = this.onPingTimeout.bind(this);
    this._onBeforeUnloadHandler = this.onBeforeUnload.bind(this);
    this._onOfflineHandler = this.onOffline.bind(this);
    this._onOnlineHandler = this.onOnline.bind(this);
  }
  setLogger(logger) {
    this._logger = logger;
    this._jsonRpcAdapter?.setLogger(this.getLogger());
    this._storage?.setLogger(this.getLogger());
    this._sharedConfig.setLogger(this.getLogger());
    this._channelManager.setLogger(this.getLogger());
    this._connectors.webSocket?.setLogger(this.getLogger());
    this._connectors.longPolling?.setLogger(this.getLogger());
  }
  getLogger() {
    return this._logger;
  }
  /**
   * Terminal teardown: removes the window listeners, cancels every pending timer,
   * persists the session for a quick re-init, and disconnects. Irreversible — a
   * destroyed client schedules no further work and `start()` rejects with
   * `PULL_DISPOSED`; create a new instance to reconnect.
   */
  destroy() {
    this._disposed = true;
    this._startGeneration++;
    this.stop(CloseReasons.NORMAL_CLOSURE, "manual stop");
    this.removeAllWindowListeners();
    if (this._storage) {
      this._storage.remove(LsKeys.PullConfig);
    }
    this.persistSession();
  }
  init() {
    if (this._disposed) {
      return;
    }
    this._connectors.webSocket = new WebSocketConnector({
      parent: this,
      onOpen: this.onWebSocketOpen.bind(this),
      onMessage: this.onIncomingMessage.bind(this),
      onDisconnect: this.onWebSocketDisconnect.bind(this),
      onError: this.onWebSocketError.bind(this)
    });
    this._connectors.longPolling = new LongPollingConnector({
      parent: this,
      onOpen: this.onLongPollingOpen.bind(this),
      onMessage: this.onIncomingMessage.bind(this),
      onDisconnect: this.onLongPollingDisconnect.bind(this),
      onError: this.onLongPollingError.bind(this)
    });
    this._connectionType = this.isWebSocketAllowed() ? ConnectionType.WebSocket : ConnectionType.LongPolling;
    this.addWindowListener("beforeunload", this._onBeforeUnloadHandler);
    this.addWindowListener("offline", this._onOfflineHandler);
    this.addWindowListener("online", this._onOnlineHandler);
    this._jsonRpcAdapter = new JsonRpc({
      connector: this._connectors.webSocket,
      handlers: {
        "incoming.message": this.handleRpcIncomingMessage.bind(this)
      }
    });
  }
  // endregion ////
  // region Get-Set ////
  get connector() {
    return this._connectors[this._connectionType];
  }
  get status() {
    return this._status;
  }
  /**
   * @param status
   */
  set status(status) {
    if (this._status === status) {
      return;
    }
    this._status = status;
    if (this._offlineTimeout) {
      clearTimeout(this._offlineTimeout);
      this._offlineTimeout = null;
    }
    if (status === PullStatus.Offline) {
      this.sendPullStatusDelayed(status, OFFLINE_STATUS_DELAY);
    } else {
      this.sendPullStatus(status);
    }
  }
  get session() {
    return this._session;
  }
  // endregion ////
  // region Public /////
  /**
   * Creates a subscription to incoming messages.
   *
   * @param {TypeSubscriptionOptions | TypeSubscriptionCommandHandler} params
   * @returns { () => void } - Unsubscribe callback function
   */
  subscribe(params) {
    if (!Type.isPlainObject(params)) {
      return this.attachCommandHandler(params);
    }
    params = params;
    params.type = params.type || SubscriptionType.Server;
    params.command = params.command || null;
    if (params.type == SubscriptionType.Server || params.type == SubscriptionType.Client) {
      if (typeof params.moduleId === "undefined") {
        throw new TypeError(
          `${Text.getDateForLog()}: Pull.subscribe: parameter moduleId is not specified`
        );
      }
      if (typeof this._subscribers[params.type] === "undefined") {
        this._subscribers[params.type] = {};
      }
      if (typeof this._subscribers[params.type][params.moduleId] === "undefined") {
        this._subscribers[params.type][params.moduleId] = {
          callbacks: [],
          commands: {}
        };
      }
      if (params.command) {
        if (typeof this._subscribers[params.type][params.moduleId]["commands"][params.command] === "undefined") {
          this._subscribers[params.type][params.moduleId]["commands"][params.command] = [];
        }
        this._subscribers[params.type][params.moduleId]["commands"][params.command].push(params.callback);
        return () => {
          if (typeof params.type === "undefined" || typeof params.moduleId === "undefined" || typeof params.command === "undefined" || null === params.command) {
            return;
          }
          this._subscribers[params.type][params.moduleId]["commands"][params.command] = this._subscribers[params.type][params.moduleId]["commands"][params.command].filter((element) => {
            return element !== params.callback;
          });
        };
      } else {
        this._subscribers[params.type][params.moduleId]["callbacks"].push(
          params.callback
        );
        return () => {
          if (typeof params.type === "undefined" || typeof params.moduleId === "undefined") {
            return;
          }
          this._subscribers[params.type][params.moduleId]["callbacks"] = this._subscribers[params.type][params.moduleId]["callbacks"].filter(
            (element) => {
              return element !== params.callback;
            }
          );
        };
      }
    } else {
      if (typeof this._subscribers[params.type] === "undefined") {
        this._subscribers[params.type] = [];
      }
      this._subscribers[params.type].push(params.callback);
      return () => {
        if (typeof params.type === "undefined") {
          return;
        }
        this._subscribers[params.type] = this._subscribers[params.type].filter(
          (element) => {
            return element !== params.callback;
          }
        );
      };
    }
  }
  /**
   * @param {TypeSubscriptionCommandHandler} handler
   * @returns {() => void} - Unsubscribe callback function
   */
  attachCommandHandler(handler) {
    if (typeof handler.getModuleId !== "function" || typeof handler.getModuleId() !== "string") {
      this.getLogger().error(`${Text.getDateForLog()}: Pull.attachCommandHandler: result of handler.getModuleId() is not a string.`).catch(() => {
      });
      return () => {
      };
    }
    let type = SubscriptionType.Server;
    if (typeof handler.getSubscriptionType === "function") {
      type = handler.getSubscriptionType();
    }
    return this.subscribe({
      type,
      moduleId: handler.getModuleId(),
      callback: /* @__PURE__ */ __name((data) => {
        let method = null;
        if (typeof handler.getMap === "function") {
          const mapping = handler.getMap();
          if (mapping && typeof mapping === "object") {
            const rowMapping = mapping[data.command];
            if (typeof rowMapping === "function") {
              method = rowMapping.bind(handler);
            } else if (typeof rowMapping === "string" && typeof handler[rowMapping] === "function") {
              method = handler[rowMapping].bind(handler);
            }
          }
        }
        if (!method) {
          const methodName = `handle${Text.capitalize(data.command)}`;
          if (typeof handler[methodName] === "function") {
            method = handler[methodName].bind(handler);
          }
        }
        if (method) {
          if (this._debug && this._context !== "master") {
            this.getLogger().warning(
              `${Text.getDateForLog()}: Pull.attachCommandHandler: result of handler.getModuleId() is not a string`,
              // data.params / data.extra are app-defined and may carry a credential key (#43)
              redactSensitiveParams({ data })
            ).catch(() => {
            });
          }
          method(data.params, data.extra, data.command);
        }
      }, "callback")
    });
  }
  /**
   * Connects the client and begins receiving events.
   *
   * @param config
   * @throws Rejects with `{ ex: { error: 'PULL_DISPOSED' } }` when called after
   *   `destroy()` — a destroyed client cannot be restarted; create a new instance.
   */
  async start(config = null) {
    if (this._disposed) {
      return Promise.reject({
        ex: {
          error: "PULL_DISPOSED",
          error_description: "PullClient has been destroyed; create a new instance"
        }
      });
    }
    let allowConfigCaching = true;
    if (this.isConnected()) {
      return Promise.resolve(true);
    }
    if (this._starting && this._startingPromise) {
      return this._startingPromise;
    }
    if (!this._userId) {
      throw new Error("Not set userId");
    }
    if (this._siteId === "none") {
      throw new Error("Not set siteId");
    }
    let skipReconnectToLastSession = false;
    if (!!config && Type.isPlainObject(config)) {
      if (typeof config?.skipReconnectToLastSession !== "undefined") {
        skipReconnectToLastSession = config.skipReconnectToLastSession;
        delete config.skipReconnectToLastSession;
      }
      this._config = config;
      allowConfigCaching = false;
    }
    if (!this._enabled) {
      return Promise.reject({
        ex: {
          error: "PULL_DISABLED",
          error_description: "Push & Pull server is disabled"
        }
      });
    }
    const now = Date.now();
    let oldSession;
    if (!skipReconnectToLastSession && this._storage) {
      oldSession = this._storage.get(LS_SESSION, null);
    }
    if (Type.isPlainObject(oldSession) && Object.prototype.hasOwnProperty.call(oldSession, "ttl") && oldSession["ttl"] >= now) {
      this._session.mid = oldSession["mid"];
    }
    this._starting = true;
    const startGeneration = ++this._startGeneration;
    return this._startingPromise = new Promise((resolve, reject) => {
      this.loadConfig("client_start").then((config2) => {
        if (this._disposed || this._startGeneration !== startGeneration) {
          this._starting = false;
          reject({
            ex: {
              error: "PULL_DISPOSED",
              error_description: "PullClient has been destroyed; create a new instance"
            }
          });
          return;
        }
        this.setConfig(config2, allowConfigCaching);
        this.init();
        this.updateWatch(true);
        this.startCheckConfig();
        this.connect().then(
          () => resolve(true),
          (error) => reject(error)
        );
      }).catch((error) => {
        this._starting = false;
        this.status = PullStatus.Offline;
        this.stopCheckConfig();
        this.getLogger().error(
          `${Text.getDateForLog()}: Pull: could not read push-server config`,
          { error }
        ).catch(() => {
        });
        reject(error);
      });
    });
  }
  /**
   * @param disconnectCode
   * @param disconnectReason
   */
  restart(disconnectCode = CloseReasons.NORMAL_CLOSURE, disconnectReason = "manual restart") {
    if (this._disposed) {
      return;
    }
    if (this._restartTimeout) {
      clearTimeout(this._restartTimeout);
      this._restartTimeout = null;
    }
    this.getLogger().debug(
      `${Text.getDateForLog()}: Pull: restarting with code ${disconnectCode}`
    ).catch(() => {
    });
    this.disconnect(disconnectCode, disconnectReason);
    if (this._storage) {
      this._storage.remove(LsKeys.PullConfig);
    }
    this._config = null;
    const loadConfigReason = `${disconnectCode}_${disconnectReason.replaceAll(" ", "_")}`;
    this.loadConfig(loadConfigReason).then(
      (config) => {
        this.setConfig(config, true);
        this.updateWatch();
        this.startCheckConfig();
        this.connect().catch((error) => {
          this.getLogger().error("restart error", { error }).catch(() => {
          });
        });
      },
      (error) => {
        this.getLogger().error(
          `${Text.getDateForLog()}: Pull: could not read push-server config `,
          { error }
        ).catch(() => {
        });
        this.status = PullStatus.Offline;
        if (this._reconnectTimeout) {
          clearTimeout(this._reconnectTimeout);
          this._reconnectTimeout = null;
        }
        if (error?.status == 401 || error?.status == 403) {
          this.stopCheckConfig();
          this.onCustomEvent("onPullError", ["AUTHORIZE_ERROR"]);
        }
      }
    );
  }
  stop(disconnectCode = CloseReasons.NORMAL_CLOSURE, disconnectReason = "manual stop") {
    this.disconnect(disconnectCode, disconnectReason);
    this.stopCheckConfig();
    this.clearAllTimers();
  }
  // Cancel every pending timer so a stopped/destroyed client leaves nothing
  // running. Previously only _checkInterval was cleared, so the other six timers
  // (including the self-rescheduling watch-extend) survived teardown (#141).
  clearAllTimers() {
    for (const field of this._timerFields) {
      const timer = this[field];
      if (timer) {
        if (field === "_checkInterval") {
          clearInterval(timer);
        } else {
          clearTimeout(timer);
        }
      }
      this[field] = null;
    }
  }
  // Arm a tracked setTimeout: clears any existing id for the field first (same
  // clear-before-set the call-sites did by hand), then stores the new id. The
  // interval-backed _checkInterval keeps its own setInterval path. (#222)
  armTimeout(field, fn, ms) {
    const existing = this[field];
    if (existing) {
      clearTimeout(existing);
    }
    this[field] = setTimeout(fn, ms);
  }
  // Window-listener registry: init() adds through here, destroy() drops through
  // removeAllWindowListeners(), so a listener can't be added without teardown. (#222)
  addWindowListener(type, handler) {
    if (typeof window === "undefined") {
      return;
    }
    window.addEventListener(type, handler);
    this._windowListeners.push({ type, handler });
  }
  removeAllWindowListeners() {
    if (typeof window !== "undefined") {
      for (const { type, handler } of this._windowListeners) {
        window.removeEventListener(type, handler);
      }
    }
    this._windowListeners = [];
  }
  reconnect(disconnectCode, disconnectReason, delay = 1) {
    this.disconnect(disconnectCode, disconnectReason);
    this.scheduleReconnect(delay);
  }
  /**
   * @param lastMessageId
   */
  setLastMessageId(lastMessageId) {
    this._session.mid = lastMessageId;
  }
  /**
   * Send a single message to the specified users.
   *
   * @param users User ids of the message receivers.
   * @param moduleId Name of the module to receive a message,
   * @param command Command name.
   * @param {object} params Command parameters.
   * @param [expiry] Message expiry time in seconds.
   * @return {Promise}
   */
  async sendMessage(users, moduleId, command, params, expiry) {
    const message = {
      userList: users,
      body: {
        module_id: moduleId,
        command,
        params
      },
      expiry
    };
    if (this.isJsonRpc()) {
      return this._jsonRpcAdapter?.executeOutgoingRpcCommand(
        RpcMethod.Publish,
        message
      );
    } else {
      return this.sendMessageBatch([message]);
    }
  }
  /**
   * Send a single message to the specified public channels.
   *
   * @param  publicChannels Public ids of the channels to receive a message.
   * @param moduleId Name of the module to receive a message,
   * @param command Command name.
   * @param {object} params Command parameters.
   * @param [expiry] Message expiry time in seconds.
   * @return {Promise}
   */
  async sendMessageToChannels(publicChannels, moduleId, command, params, expiry) {
    const message = {
      channelList: publicChannels,
      body: {
        module_id: moduleId,
        command,
        params
      },
      expiry
    };
    if (this.isJsonRpc()) {
      return this._jsonRpcAdapter?.executeOutgoingRpcCommand(
        RpcMethod.Publish,
        message
      );
    } else {
      return this.sendMessageBatch([message]);
    }
  }
  /**
   * @param debugFlag
   */
  capturePullEvent(debugFlag = true) {
    this._debug = debugFlag;
  }
  /**
   * @param loggingFlag
   */
  enableLogging(loggingFlag = true) {
    this._sharedConfig.setLoggingEnabled(loggingFlag);
    this._loggingEnabled = loggingFlag;
  }
  /**
   * Returns list channels that the connection is subscribed to.
   *
   * @returns {Promise}
   */
  async listChannels() {
    return this._jsonRpcAdapter?.executeOutgoingRpcCommand(
      RpcMethod.ListChannels,
      {}
    ) || Promise.reject(new Error("jsonRpcAdapter not init"));
  }
  /**
   * Returns "last seen" time in seconds for the users.
   * Result format: Object{userId: int}
   * If the user is currently connected - will return 0.
   * If the user is offline - will return the diff between the current timestamp and the last seen timestamp in seconds.
   * If the user was never online - the record for the user will be missing from the result object.
   *
   * @param {integer[]} userList List of user ids.
   * @returns {Promise}
   */
  async getUsersLastSeen(userList) {
    if (!Type.isArray(userList) || !userList.every((item) => typeof item === "number")) {
      throw new Error("userList must be an array of numbers");
    }
    const result = {};
    return new Promise((resolve, reject) => {
      this._jsonRpcAdapter?.executeOutgoingRpcCommand(RpcMethod.GetUsersLastSeen, {
        userList
      }).then((response) => {
        const unresolved = [];
        for (let i = 0; i < userList.length; i++) {
          if (!Object.prototype.hasOwnProperty.call(response, userList[i])) {
            unresolved.push(userList[i]);
          }
        }
        if (unresolved.length === 0) {
          return resolve(result);
        }
        const params = {
          userIds: unresolved,
          sendToQueueSever: true
        };
        this._restClient.actions.v2.call.make({
          method: "pull.api.user.getLastSeen",
          params
        }).then((response2) => {
          const data = response2.getData().result;
          for (const userId in data) {
            result[Number(userId)] = Number(data[userId]);
          }
          return resolve(result);
        }).catch((error) => {
          this.getLogger().error("getUsersLastSeen", { error }).catch(() => {
          });
          reject(error);
        });
      }).catch((error) => {
        this.getLogger().error("getUsersLastSeen", { error }).catch(() => {
        });
        reject(error);
      });
    });
  }
  /**
   * Pings server.
   * In case of success promise will be resolved, otherwise - rejected.
   *
   * @param {number} timeout Request timeout in seconds
   * @returns {Promise}
   */
  async ping(timeout = 5) {
    return this._jsonRpcAdapter?.executeOutgoingRpcCommand(
      RpcMethod.Ping,
      {},
      timeout
    );
  }
  /**
   * @param userId {number}
   * @param callback {UserStatusCallback}
   * @returns {Promise}
   */
  async subscribeUserStatusChange(userId, callback) {
    return new Promise((resolve, reject) => {
      this._jsonRpcAdapter?.executeOutgoingRpcCommand(RpcMethod.SubscribeStatusChange, {
        userId
      }).then(() => {
        if (!this._userStatusCallbacks[userId]) {
          this._userStatusCallbacks[userId] = [];
        }
        if (Type.isFunction(callback)) {
          this._userStatusCallbacks[userId].push(callback);
        }
        return resolve();
      }).catch((error) => reject(error));
    });
  }
  /**
   * @param {number} userId
   * @param {UserStatusCallback} callback
   * @returns {Promise}
   */
  async unsubscribeUserStatusChange(userId, callback) {
    if (this._userStatusCallbacks[userId]) {
      this._userStatusCallbacks[userId] = this._userStatusCallbacks[userId].filter((cb) => cb !== callback);
      if (this._userStatusCallbacks[userId].length === 0) {
        return this._jsonRpcAdapter?.executeOutgoingRpcCommand(
          RpcMethod.UnsubscribeStatusChange,
          {
            userId
          }
        );
      }
    }
    return Promise.resolve();
  }
  // endregion ////
  // region Get ////
  getRevision() {
    return this._config && this._config.api ? this._config.api.revision_web : null;
  }
  getServerVersion() {
    return this._config && this._config.server ? this._config.server.version : 0;
  }
  getServerMode() {
    return this._config && this._config.server ? this._config.server.mode : null;
  }
  getConfig() {
    return this._config;
  }
  getDebugInfo() {
    if (!JSON || !JSON.stringify) {
      return {};
    }
    let configDump;
    if (this._config && this._config.channels) {
      configDump = {
        // The private channel id is a subscription secret — mask it here too, not
        // just in `Path` below, so the debug dump never surfaces it (#148). Expiry
        // timestamps are non-sensitive and kept for diagnostics.
        ChannelID: this._config.channels.private?.id ? REDACTED_PLACEHOLDER : "n/a",
        ChannelDie: this._config.channels.private?.end || "n/a",
        ChannelDieShared: this._config.channels.shared?.end || "n/a"
      };
    } else {
      configDump = {
        ConfigError: "config is not loaded"
      };
    }
    let websocketMode = "-";
    if (this._connectors.webSocket && this._connectors.webSocket?.socket) {
      if (this.isJsonRpc()) {
        websocketMode = "json-rpc";
      } else {
        websocketMode = this._connectors.webSocket?.socket?.url.search("binaryMode=true") != -1 ? "protobuf" : "text";
      }
    }
    const connectionPath = this.connector?.connectionPath;
    return {
      "UserId": this._userId + (this._userId > 0 ? "" : "(guest)"),
      "Guest userId": this._guestMode && this._guestUserId !== 0 ? this._guestUserId : "-",
      "Browser online": typeof navigator !== "undefined" && navigator.onLine ? "Y" : "N",
      "Connect": this.isConnected() ? "Y" : "N",
      "Server type": this.isSharedMode() ? "cloud" : "local",
      "WebSocket supported": this.isWebSocketSupported() ? "Y" : "N",
      "WebSocket connected": this._connectors.webSocket && this._connectors.webSocket.connected ? "Y" : "N",
      "WebSocket mode": websocketMode,
      "Try connect": this._reconnectTimeout ? "Y" : "N",
      "Try number": this._connectionAttempt,
      // Mask the push JWT (`token`) and private `CHANNEL_ID`s before exposing
      // the connection path through this developer-facing debug dump (#148).
      "Path": connectionPath ? redactSensitiveUrl(connectionPath, ["CHANNEL_ID"]) : "-",
      ...configDump,
      "Last message": this._session.mid || "-",
      "Session history": this._session.history,
      "Watch tags": this._watchTagsQueue.entries()
    };
  }
  /**
   * @process
   * @param connectionType
   */
  getConnectionPath(connectionType) {
    let path;
    const params = {};
    switch (connectionType) {
      case ConnectionType.WebSocket:
        path = this._isSecure ? this._config?.server.websocket_secure : this._config?.server.websocket;
        break;
      case ConnectionType.LongPolling:
        path = this._isSecure ? this._config?.server.long_pooling_secure : this._config?.server.long_polling;
        break;
      default:
        throw new Error(`Unknown connection type ${connectionType}`);
    }
    if (!Type.isStringFilled(path)) {
      throw new Error(`Empty path`);
    }
    if (typeof this._config?.jwt === "string" && this._config?.jwt !== "") {
      params["token"] = this._config?.jwt;
    } else {
      const channels = [];
      if (this._config?.channels?.private?.id) {
        channels.push(this._config.channels.private.id);
      }
      if (this._config?.channels?.shared?.id) {
        channels.push(this._config.channels.shared.id);
      }
      if (channels.length === 0) {
        throw new Error(`Empty channels`);
      }
      params["CHANNEL_ID"] = channels.join("/");
    }
    if (this.isJsonRpc()) {
      params.jsonRpc = "true";
    } else if (this.isProtobufSupported()) {
      params.binaryMode = "true";
    }
    if (this.isSharedMode()) {
      if (!this._config?.clientId) {
        throw new Error(
          "Push-server is in shared mode, but clientId is not set"
        );
      }
      params.clientId = this._config.clientId;
    }
    if (this._session.mid) {
      params.mid = this._session.mid;
    }
    if (this._session.tag) {
      params.tag = this._session.tag;
    }
    if (this._session.time) {
      params.time = this._session.time;
    }
    params.revision = REVISION;
    return `${path}?${Text.buildQueryString(params)}`;
  }
  /**
   * @process
   */
  getPublicationPath() {
    const path = this._isSecure ? this._config?.server.publish_secure : this._config?.server.publish;
    if (!path) {
      return "";
    }
    const channels = [];
    if (this._config?.channels.private?.id) {
      channels.push(this._config.channels.private.id);
    }
    if (this._config?.channels.shared?.id) {
      channels.push(this._config.channels.shared.id);
    }
    const params = {
      CHANNEL_ID: channels.join("/")
    };
    return path + "?" + Text.buildQueryString(params);
  }
  // endregion ////
  // region Is* ////
  isConnected() {
    return this.connector ? this.connector.connected : false;
  }
  isWebSocketSupported() {
    return typeof window !== "undefined" && typeof window.WebSocket !== "undefined";
  }
  isWebSocketAllowed() {
    if (this._sharedConfig.isWebSocketBlocked()) {
      return false;
    }
    return this.isWebSocketEnabled();
  }
  isWebSocketEnabled() {
    if (!this.isWebSocketSupported()) {
      return false;
    }
    if (!this._config) {
      return false;
    }
    if (!this._config.server) {
      return false;
    }
    return this._config.server.websocket_enabled;
  }
  isPublishingSupported() {
    return this.getServerVersion() > 3;
  }
  isPublishingEnabled() {
    if (!this.isPublishingSupported()) {
      return false;
    }
    return this._config?.server.publish_enabled === true;
  }
  isProtobufSupported() {
    return this.getServerVersion() == 4 && !Browser.isIE();
  }
  isJsonRpc() {
    return this.getServerVersion() >= 5;
  }
  isSharedMode() {
    return this.getServerMode() === ServerMode.Shared;
  }
  // endregion ////
  // region Events ////
  /**
   * @param {TypePullClientEmitConfig} params
   * @returns {boolean}
   */
  emit(params) {
    if (params.type == SubscriptionType.Server || params.type == SubscriptionType.Client) {
      if (typeof this._subscribers[params.type] === "undefined") {
        this._subscribers[params.type] = {};
      }
      if (typeof params.moduleId === "undefined") {
        throw new TypeError(
          `${Text.getDateForLog()}: Pull.emit: parameter moduleId is not specified`
        );
      }
      if (typeof this._subscribers[params.type][params.moduleId] === "undefined") {
        this._subscribers[params.type][params.moduleId] = {
          callbacks: [],
          commands: {}
        };
      }
      if (this._subscribers[params.type][params.moduleId]["callbacks"].length > 0) {
        this._subscribers[params.type][params.moduleId]["callbacks"].forEach(
          (callback) => {
            callback(params.data, {
              type: params.type,
              moduleId: params.moduleId ?? "?"
            });
          }
        );
      }
      if (!(typeof params.data === "undefined") && !(typeof params.data["command"] === "undefined") && this._subscribers[params.type][params.moduleId]["commands"][params.data["command"]] && this._subscribers[params.type][params.moduleId]["commands"][params.data["command"]].length > 0) {
        this._subscribers[params.type][params.moduleId]["commands"][params.data["command"]].forEach((callback) => {
          if (typeof params.data === "undefined") {
            return;
          }
          callback(
            params.data["params"],
            params.data["extra"],
            params.data["command"],
            {
              type: params.type,
              moduleId: params.moduleId
            }
          );
        });
      }
      return true;
    } else {
      if (typeof this._subscribers[params.type] === "undefined") {
        this._subscribers[params.type] = [];
      }
      if (this._subscribers[params.type].length <= 0) {
        return true;
      }
      this._subscribers[params.type].forEach(
        (callback) => {
          callback(params.data, {
            type: params.type
          });
        }
      );
      return true;
    }
  }
  /**
   * @process
   *
   * @param message
   */
  broadcastMessage(message) {
    const moduleId = message.module_id = message.module_id.toLowerCase();
    const command = message.command;
    if (!message.extra) {
      message.extra = {};
    }
    if (message.extra.server_time_unix) {
      message.extra.server_time_ago = (Date.now() - message.extra.server_time_unix * 1e3) / 1e3 - (this._config?.server.timeShift || 0);
      message.extra.server_time_ago = Math.max(message.extra.server_time_ago, 0);
    }
    this.logMessage(message);
    try {
      if (message.extra.sender && message.extra.sender.type === SenderType.Client) {
        this.onCustomEvent(
          "onPullClientEvent-" + moduleId,
          [command, message.params, message.extra],
          true
        );
        this.onCustomEvent(
          "onPullClientEvent",
          [moduleId, command, message.params, message.extra],
          true
        );
        this.emit({
          type: SubscriptionType.Client,
          moduleId,
          data: {
            command,
            params: Type.clone(message.params),
            extra: Type.clone(message.extra)
          }
        });
      } else if (moduleId === "pull") {
        this.handleInternalPullEvent(command, message);
      } else if (moduleId == "online") {
        if ((message?.extra?.server_time_ago || 0) < 240) {
          this.onCustomEvent(
            "onPullOnlineEvent",
            [command, message.params, message.extra],
            true
          );
          this.emit({
            type: SubscriptionType.Online,
            data: {
              command,
              params: Type.clone(message.params),
              extra: Type.clone(message.extra)
            }
          });
        }
        if (command === "userStatusChange") {
          this.emitUserStatusChange(
            message.params.user_id,
            message.params.online
          );
        }
      } else {
        this.onCustomEvent(
          "onPullEvent-" + moduleId,
          [command, message.params, message.extra],
          true
        );
        this.onCustomEvent(
          "onPullEvent",
          [moduleId, command, message.params, message.extra],
          true
        );
        this.emit({
          type: SubscriptionType.Server,
          moduleId,
          data: {
            command,
            params: Type.clone(message.params),
            extra: Type.clone(message.extra)
          }
        });
      }
    } catch (error) {
      this.getLogger().warning("PULL ERROR", {
        errorType: "broadcastMessages execute error",
        errorEvent: error,
        // app-defined message.params / extra may carry a credential key (#43)
        message: redactSensitiveParams(message)
      }).catch(() => {
      });
    }
    if (message.extra && message.extra.revision_web) {
      this.checkRevision(Text.toInteger(message.extra.revision_web));
    }
  }
  /**
   * @process
   *
   * @param messages
   */
  broadcastMessages(messages) {
    for (const message of messages) {
      this.broadcastMessage(message);
    }
  }
  // endregion ////
  // region sendMessage ////
  /**
   * Sends batch of messages to the multiple public channels.
   *
   * @param messageBatchList Array of messages to send.
   * @return void
   */
  async sendMessageBatch(messageBatchList) {
    if (!this.isPublishingEnabled()) {
      this.getLogger().error(`Client publishing is not supported or is disabled`).catch(() => {
      });
      return Promise.reject(
        new Error(`Client publishing is not supported or is disabled`)
      );
    }
    if (this.isJsonRpc()) {
      const rpcRequest = this._jsonRpcAdapter?.createPublishRequest(messageBatchList);
      this.connector?.send(JSON.stringify(rpcRequest));
      return Promise.resolve(true);
    } else {
      const userIds = {};
      for (const messageBatch of messageBatchList) {
        if (typeof messageBatch.userList !== "undefined") {
          for (const user of messageBatch.userList) {
            const userId = Number(user);
            userIds[userId] = userId;
          }
        }
      }
      this._channelManager?.getPublicIds(Object.values(userIds)).then((publicIds) => {
        const response = this.connector?.send(
          this.encodeMessageBatch(messageBatchList, publicIds)
        );
        return Promise.resolve(response);
      });
    }
  }
  /**
   * @param messageBatchList
   * @param publicIds
   */
  encodeMessageBatch(messageBatchList, publicIds) {
    const messages = [];
    messageBatchList.forEach((messageFields) => {
      const messageBody = messageFields.body;
      let receivers = [];
      if (messageFields.userList) {
        receivers = this.createMessageReceivers(
          messageFields.userList,
          publicIds
        );
      }
      if (messageFields.channelList) {
        if (!Type.isArray(messageFields.channelList)) {
          throw new TypeError("messageFields.publicChannels must be an array");
        }
        messageFields.channelList.forEach((publicChannel) => {
          let publicId;
          let signature;
          if (typeof publicChannel === "string" && publicChannel.includes(".")) {
            const fields = publicChannel.toString().split(".");
            publicId = fields[0];
            signature = fields[1];
          } else if (typeof publicChannel === "object" && "publicId" in publicChannel && "signature" in publicChannel) {
            publicId = publicChannel?.publicId;
            signature = publicChannel?.signature;
          } else {
            throw new Error(
              `Public channel MUST be either a string, formatted like "{publicId}.{signature}" or an object with fields 'publicId' and 'signature'`
            );
          }
          receivers.push(
            Receiver.create({
              id: this.encodeId(publicId),
              signature: this.encodeId(signature)
            })
          );
        });
      }
      const message = IncomingMessage.create({
        receivers,
        body: JSON.stringify(messageBody),
        expiry: messageFields.expiry || 0
      });
      messages.push(message);
    });
    const requestBatch = RequestBatch.create({
      requests: [
        {
          incomingMessages: {
            messages
          }
        }
      ]
    });
    return RequestBatch.encode(requestBatch).finish();
  }
  /**
   * @memo fix return type
   * @param users
   * @param publicIds
   */
  createMessageReceivers(users, publicIds) {
    const result = [];
    for (const userId of users) {
      if (!publicIds[userId] || !publicIds[userId].publicId) {
        throw new Error(`Could not determine public id for user ${userId}`);
      }
      result.push(
        Receiver.create({
          id: this.encodeId(publicIds[userId].publicId),
          signature: this.encodeId(publicIds[userId].signature)
        })
      );
    }
    return result;
  }
  // endregion ////
  // region _userStatusCallbacks ////
  /**
   * @param userId
   * @param isOnline
   */
  emitUserStatusChange(userId, isOnline) {
    if (this._userStatusCallbacks[userId]) {
      for (const callback of this._userStatusCallbacks[userId]) {
        callback({
          userId,
          isOnline
        });
      }
    }
  }
  restoreUserStatusSubscription() {
    for (const userId in this._userStatusCallbacks) {
      if (Object.prototype.hasOwnProperty.call(this._userStatusCallbacks, userId) && this._userStatusCallbacks[userId].length > 0) {
        this._jsonRpcAdapter?.executeOutgoingRpcCommand(
          RpcMethod.SubscribeStatusChange,
          {
            userId
          }
        );
      }
    }
  }
  // endregion ////
  // region Config ////
  async loadConfig(_logTag) {
    if (!this._config) {
      this._config = Object.assign({}, EmptyConfig);
      let config;
      if (this._storage) {
        config = this._storage.get(LsKeys.PullConfig, null);
      }
      if (this.isConfigActual(config) && this.checkRevision(config.api.revision_web)) {
        return Promise.resolve(config);
      } else if (this._storage) {
        this._storage.remove(LsKeys.PullConfig);
      }
    } else if (this.isConfigActual(this._config) && this.checkRevision(this._config.api.revision_web)) {
      return Promise.resolve(this._config);
    } else {
      this._config = Object.assign({}, EmptyConfig);
    }
    return new Promise((resolve, reject) => {
      this._restClient.actions.v2.call.make({
        method: this._configGetMethod,
        params: { CACHE: "N" }
      }).then((response) => {
        const data = response.getData().result;
        const timeShift = Math.floor(
          (Date.now() - new Date(data.serverTime).getTime()) / 1e3
        );
        delete data.serverTime;
        const config = Object.assign({}, data);
        config.server.timeShift = timeShift;
        resolve(config);
      }).catch((error) => {
        reject(error);
      });
    });
  }
  /**
   * @param config
   */
  isConfigActual(config) {
    if (!Type.isPlainObject(config)) {
      return false;
    }
    if (Number(config["server"].config_timestamp) !== this._configTimestamp) {
      return false;
    }
    const now = /* @__PURE__ */ new Date();
    if (Type.isNumber(config["exp"]) && config["exp"] > 0 && config["exp"] < now.getTime() / 1e3) {
      return false;
    }
    const channelCount = Object.keys(config["channels"]).length;
    if (channelCount === 0) {
      return false;
    }
    for (const channelType in config["channels"]) {
      if (!Object.prototype.hasOwnProperty.call(config["channels"], channelType)) {
        continue;
      }
      const channel = config["channels"][channelType];
      const channelEnd = new Date(channel.end);
      if (channelEnd < now) {
        return false;
      }
    }
    return true;
  }
  startCheckConfig() {
    if (this._disposed) {
      return;
    }
    if (this._checkInterval) {
      clearInterval(this._checkInterval);
      this._checkInterval = null;
    }
    this._checkInterval = setInterval(
      this.checkConfig.bind(this),
      CONFIG_CHECK_INTERVAL
    );
  }
  stopCheckConfig() {
    if (this._checkInterval) {
      clearInterval(this._checkInterval);
    }
    this._checkInterval = null;
  }
  checkConfig() {
    if (this.isConfigActual(this._config)) {
      if (!this.checkRevision(Text.toInteger(this._config?.api.revision_web))) {
        return false;
      }
    } else {
      this.logToConsole("Stale config detected. Restarting");
      this.restart(CloseReasons.CONFIG_EXPIRED, "config expired");
    }
    return true;
  }
  /**
   * @param config
   * @param allowCaching
   */
  setConfig(config, allowCaching) {
    for (const key in config) {
      if (Object.prototype.hasOwnProperty.call(config, key) && Object.prototype.hasOwnProperty.call(this._config, key)) {
        this._config[key] = config[key];
      }
    }
    if (config.publicChannels) {
      this.setPublicIds(Object.values(config.publicChannels));
    }
    this._configTimestamp = Number(config.server.config_timestamp);
    if (this._storage && allowCaching && !this._disposed) {
      try {
        this._storage.set(LsKeys.PullConfig, config);
      } catch (error) {
        if (typeof localStorage !== "undefined" && localStorage.removeItem) {
          localStorage.removeItem("history");
        }
        this.getLogger().error(
          `${Text.getDateForLog()}: Pull: Could not cache config in local storage.`,
          { error }
        ).catch(() => {
        });
      }
    }
  }
  setPublicIds(publicIds) {
    this._channelManager.setPublicIds(publicIds);
  }
  /**
   * @param serverRevision
   */
  checkRevision(serverRevision) {
    if (this._skipCheckRevision) {
      return true;
    }
    if (serverRevision > 0 && serverRevision !== REVISION) {
      this._enabled = false;
      this.showNotification("PULL_OLD_REVISION");
      this.disconnect(CloseReasons.NORMAL_CLOSURE, "check_revision");
      this.onCustomEvent("onPullRevisionUp", [serverRevision, REVISION]);
      this.emit({
        type: SubscriptionType.Revision,
        data: {
          server: serverRevision,
          client: REVISION
        }
      });
      this.logToConsole(
        `Pull revision changed from ${REVISION} to ${serverRevision}. Reload required`
      );
      return false;
    }
    return true;
  }
  // endregion ////
  // region Connect|ReConnect|DisConnect ////
  disconnect(disconnectCode, disconnectReason) {
    if (this.connector) {
      this._isManualDisconnect = true;
      this.connector.disconnect(disconnectCode, disconnectReason);
    }
  }
  restoreWebSocketConnection() {
    if (this._connectionType === ConnectionType.WebSocket) {
      return;
    }
    this._connectors.webSocket?.connect();
  }
  /**
   * @param connectionDelay
   */
  scheduleReconnect(connectionDelay = 0) {
    if (this._disposed || !this._enabled) {
      return;
    }
    if (!connectionDelay) {
      {
        connectionDelay = this.getConnectionAttemptDelay(
          this._connectionAttempt
        );
      }
    }
    this.logToConsole(
      `Pull: scheduling reconnection in ${connectionDelay} seconds; attempt # ${this._connectionAttempt}`
    );
    this.armTimeout("_reconnectTimeout", () => {
      this.connect().catch((error) => {
        this.getLogger().error("scheduleReconnect", { error }).catch(() => {
        });
      });
    }, connectionDelay * 1e3);
  }
  scheduleRestoreWebSocketConnection() {
    if (this._disposed) {
      return;
    }
    this.logToConsole(
      `Pull: scheduling restoration of websocket connection in ${RESTORE_WEBSOCKET_TIMEOUT} seconds`
    );
    if (this._restoreWebSocketTimeout) {
      return;
    }
    this.armTimeout("_restoreWebSocketTimeout", () => {
      this._restoreWebSocketTimeout = null;
      this.restoreWebSocketConnection();
    }, RESTORE_WEBSOCKET_TIMEOUT * 1e3);
  }
  /**
   * @returns {Promise}
   */
  async connect() {
    if (this._disposed || !this._enabled) {
      return Promise.reject();
    }
    if (this.connector?.connected) {
      return Promise.resolve();
    }
    if (this._reconnectTimeout) {
      clearTimeout(this._reconnectTimeout);
      this._reconnectTimeout = null;
    }
    this.status = PullStatus.Connecting;
    this._connectionAttempt++;
    return new Promise((resolve, reject) => {
      this._connectPromise = {
        resolve,
        reject
      };
      this.connector?.connect();
    });
  }
  /**
   * @param disconnectCode
   * @param disconnectReason
   * @param restartDelay
   */
  scheduleRestart(disconnectCode, disconnectReason, restartDelay = 0) {
    if (this._disposed) {
      return;
    }
    if (restartDelay < 1) {
      restartDelay = Math.ceil(Math.random() * 30) + 5;
    }
    this.armTimeout(
      "_restartTimeout",
      () => this.restart(disconnectCode, disconnectReason),
      restartDelay * 1e3
    );
  }
  // endregion ////
  // region Handlers ////
  /**
   * @param messageFields
   */
  handleRpcIncomingMessage(messageFields) {
    this._session.mid = messageFields.mid;
    const body = messageFields.body;
    if (!messageFields.body.extra) {
      body.extra = {};
    }
    body.extra.sender = messageFields.sender;
    if ("user_params" in messageFields && Type.isPlainObject(messageFields.user_params)) {
      Object.assign(body.params, messageFields.user_params);
    }
    if ("dictionary" in messageFields && Type.isPlainObject(messageFields.dictionary)) {
      Object.assign(body.params, messageFields.dictionary);
    }
    if (this.checkDuplicate(messageFields.mid)) {
      this.addMessageToStat(body);
      this.trimDuplicates();
      this.broadcastMessage(body);
    }
    this.connector?.send(`mack:${messageFields.mid}`);
    return {};
  }
  /**
   * @param events
   */
  handleIncomingEvents(events) {
    const messages = [];
    if (events.length === 0) {
      this._session.mid = null;
      return;
    }
    for (const event of events) {
      this.updateSessionFromEvent(event);
      if (event.mid && !this.checkDuplicate(event.mid)) {
        continue;
      }
      this.addMessageToStat(
        event.text
      );
      messages.push(event.text);
    }
    this.trimDuplicates();
    this.broadcastMessages(messages);
  }
  /**
   * @param event
   */
  updateSessionFromEvent(event) {
    this._session.mid = event.mid || null;
    this._session.tag = event.tag || null;
    this._session.time = event.time || null;
  }
  /**
   * @process
   *
   * @param command
   * @param message
   */
  handleInternalPullEvent(command, message) {
    switch (command.toUpperCase()) {
      case SystemCommands.CHANNEL_EXPIRE: {
        if (message.params.action === "reconnect") {
          const typeChanel = message.params?.channel.type;
          if (typeChanel === "private" && this._config?.channels?.private) {
            this._config.channels.private = message.params.new_channel;
            this.logToConsole(
              `Pull: new config for ${message.params.channel.type} channel set: [updated]`
            );
          }
          if (typeChanel === "shared" && this._config?.channels?.shared) {
            this._config.channels.shared = message.params.new_channel;
            this.logToConsole(
              `Pull: new config for ${message.params.channel.type} channel set: [updated]`
            );
          }
          this.reconnect(CloseReasons.CONFIG_REPLACED, "config was replaced");
        } else {
          this.restart(CloseReasons.CHANNEL_EXPIRED, "channel expired received");
        }
        break;
      }
      case SystemCommands.CONFIG_EXPIRE: {
        this.restart(CloseReasons.CONFIG_EXPIRED, "config expired received");
        break;
      }
      case SystemCommands.SERVER_RESTART: {
        this.reconnect(
          CloseReasons.SERVER_RESTARTED,
          "server was restarted",
          15
        );
        break;
      }
    }
  }
  // region Handlers For Message ////
  /**
   * @param response
   */
  onIncomingMessage(response) {
    if (this.isJsonRpc()) {
      if (response === JSON_RPC_PING) {
        this.onJsonRpcPing();
      } else {
        this._jsonRpcAdapter?.parseJsonRpcMessage(response);
      }
    } else {
      const events = this.extractMessages(response);
      this.handleIncomingEvents(events);
    }
  }
  // region onLongPolling ////
  onLongPollingOpen() {
    this._unloading = false;
    this._starting = false;
    this._connectionAttempt = 0;
    this._isManualDisconnect = false;
    this.status = PullStatus.Online;
    this.logToConsole("Pull: Long polling connection with push-server opened");
    if (this.isWebSocketEnabled()) {
      this.scheduleRestoreWebSocketConnection();
    }
    if (this._connectPromise) {
      this._connectPromise.resolve({});
    }
  }
  /**
   * @param response
   */
  onLongPollingDisconnect(response) {
    if (this._connectionType === ConnectionType.LongPolling) {
      this.status = PullStatus.Offline;
    }
    this.logToConsole(
      `Pull: Long polling connection with push-server closed. Code: ${response.code}, reason: ${response.reason}`
    );
    if (!this._isManualDisconnect) {
      this.scheduleReconnect();
    }
    this._isManualDisconnect = false;
    this.clearPingWaitTimeout();
  }
  /**
   * @param error
   */
  onLongPollingError(error) {
    this._starting = false;
    if (this._connectionType === ConnectionType.LongPolling) {
      this.status = PullStatus.Offline;
    }
    this.getLogger().error(
      `${Text.getDateForLog()}: Pull: Long polling connection error`,
      { error }
    ).catch(() => {
    });
    this.scheduleReconnect();
    if (this._connectPromise) {
      this._connectPromise.reject(error);
    }
    this.clearPingWaitTimeout();
  }
  // endregion ////
  // region onWebSocket ////
  /**
   * @param response
   */
  onWebSocketBlockChanged(response) {
    const isWebSocketBlocked = response.isWebSocketBlocked;
    if (isWebSocketBlocked && this._connectionType === ConnectionType.WebSocket && !this.isConnected()) {
      if (this._reconnectTimeout) {
        clearTimeout(this._reconnectTimeout);
        this._reconnectTimeout = null;
      }
      this._connectionAttempt = 0;
      this._connectionType = ConnectionType.LongPolling;
      this.scheduleReconnect(1);
    } else if (!isWebSocketBlocked && this._connectionType === ConnectionType.LongPolling) {
      if (this._reconnectTimeout) {
        clearTimeout(this._reconnectTimeout);
        this._reconnectTimeout = null;
      }
      if (this._restoreWebSocketTimeout) {
        clearTimeout(this._restoreWebSocketTimeout);
        this._restoreWebSocketTimeout = null;
      }
      this._connectionAttempt = 0;
      this._connectionType = ConnectionType.WebSocket;
      this.scheduleReconnect(1);
    }
  }
  onWebSocketOpen() {
    this._unloading = false;
    this._starting = false;
    this._connectionAttempt = 0;
    this._isManualDisconnect = false;
    this.status = PullStatus.Online;
    this._sharedConfig.setWebSocketBlocked(false);
    this._sharedConfig.setLongPollingBlocked(true);
    if (this._connectionType == ConnectionType.LongPolling) {
      this._connectionType = ConnectionType.WebSocket;
      this._connectors.longPolling?.disconnect(
        CloseReasons.CONFIG_REPLACED,
        "Fire at onWebSocketOpen"
      );
    }
    if (this._restoreWebSocketTimeout) {
      clearTimeout(this._restoreWebSocketTimeout);
      this._restoreWebSocketTimeout = null;
    }
    this.logToConsole("Pull: Websocket connection with push-server opened");
    if (this._connectPromise) {
      this._connectPromise.resolve({});
    }
    this.restoreUserStatusSubscription();
  }
  /**
   * @param response
   */
  onWebSocketDisconnect(response) {
    if (this._connectionType === ConnectionType.WebSocket) {
      this.status = PullStatus.Offline;
    }
    this.logToConsole(
      `Pull: Websocket connection with push-server closed. Code: ${response.code}, reason: ${response.reason}`,
      true
    );
    if (!this._isManualDisconnect) {
      if (response.code == CloseReasons.WRONG_CHANNEL_ID) {
        this.scheduleRestart(
          CloseReasons.WRONG_CHANNEL_ID,
          "wrong channel signature"
        );
      } else {
        this.scheduleReconnect();
      }
    }
    this._sharedConfig.setLongPollingBlocked(true);
    this._isManualDisconnect = false;
    this.clearPingWaitTimeout();
  }
  /**
   * @param error
   */
  onWebSocketError(error) {
    this._starting = false;
    if (this._connectionType === ConnectionType.WebSocket) {
      this.status = PullStatus.Offline;
    }
    this.getLogger().error(
      `${Text.getDateForLog()}: Pull: WebSocket connection error`,
      { error }
    ).catch(() => {
    });
    this.scheduleReconnect();
    if (this._connectPromise) {
      this._connectPromise.reject(error);
    }
    this.clearPingWaitTimeout();
  }
  // endregion ////
  // endregion ////
  // endregion ////
  // region extractMessages ////
  /**
   * @param pullEvent
   */
  extractMessages(pullEvent) {
    if (pullEvent instanceof ArrayBuffer) {
      return this.extractProtobufMessages(pullEvent);
    } else if (Type.isStringFilled(pullEvent)) {
      return this.extractPlainTextMessages(pullEvent);
    }
    throw new Error("Error pullEvent type");
  }
  /**
   * @param pullEvent
   */
  extractProtobufMessages(pullEvent) {
    const result = [];
    try {
      const responseBatch = ResponseBatch.decode(new Uint8Array(pullEvent));
      for (let i = 0; i < responseBatch.responses.length; i++) {
        const response = responseBatch.responses[i];
        if (response.command !== "outgoingMessages") {
          continue;
        }
        const messages = response.outgoingMessages.messages;
        for (const message of messages) {
          let messageFields;
          try {
            messageFields = JSON.parse(message.body);
          } catch (error) {
            this.getLogger().error(
              `${Text.getDateForLog()}: Pull: Could not parse message body`,
              { error }
            ).catch(() => {
            });
            continue;
          }
          if (!messageFields.extra) {
            messageFields.extra = {};
          }
          messageFields.extra.sender = {
            type: message.sender.type
          };
          if (message.sender.id instanceof Uint8Array) {
            messageFields.extra.sender.id = this.decodeId(message.sender.id);
          }
          const compatibleMessage = {
            mid: this.decodeId(message.id),
            text: messageFields
          };
          result.push(compatibleMessage);
        }
      }
    } catch (error) {
      this.getLogger().error(
        `${Text.getDateForLog()}: Pull: Could not parse message`,
        { error }
      ).catch(() => {
      });
    }
    return result;
  }
  /**
   * @param pullEvent
   */
  extractPlainTextMessages(pullEvent) {
    const result = [];
    const dataArray = pullEvent.match(/#!NGINXNMS!#(.*?)#!NGINXNME!#/g);
    if (dataArray === null) {
      this.getLogger().warning("PULL ERROR", {
        errorType: "parseResponse error parsing message",
        // The frame failed the NGINX-delimiter match, so its content is
        // unparseable anyway and could carry a credential (e.g. a channel
        // `signature`); log only its size, never the bytes (#43).
        byteLength: pullEvent.length
      }).catch(() => {
      });
      return [];
    }
    for (let i = 0; i < dataArray.length; i++) {
      dataArray[i] = dataArray[i].substring(12, dataArray[i].length - 12);
      if (dataArray[i].length <= 0) {
        continue;
      }
      let data;
      try {
        data = JSON.parse(dataArray[i]);
      } catch {
        continue;
      }
      result.push(data);
    }
    return result;
  }
  /**
   * Converts message id from byte[] to string
   * @param {Uint8Array} encodedId
   * @return {string}
   */
  decodeId(encodedId) {
    let result = "";
    for (const element_ of encodedId) {
      const hexByte = element_.toString(16);
      if (hexByte.length === 1) {
        result += "0";
      }
      result += hexByte;
    }
    return result;
  }
  /**
   * Converts message id from hex-encoded string to byte[]
   * @param {string} id Hex-encoded string.
   * @return {Uint8Array}
   */
  encodeId(id) {
    if (!id) {
      return new Uint8Array();
    }
    const result = [];
    for (let i = 0; i < id.length; i += 2) {
      result.push(Number.parseInt(id.slice(i, i + 2), 16));
    }
    return new Uint8Array(result);
  }
  // endregion ////
  // region Events.Status /////
  onOffline() {
    if (this._disposed) {
      return;
    }
    this.disconnect(CloseReasons.NORMAL_CLOSURE, "offline");
  }
  onOnline() {
    if (this._disposed) {
      return;
    }
    this.connect().catch((error) => {
      this.getLogger().error("onOnline", { error }).catch(() => {
      });
    });
  }
  onBeforeUnload() {
    this._unloading = true;
    this.persistSession();
    this.scheduleReconnect(15);
  }
  // Persist the current session (with a short TTL) so a quick reload / re-init can
  // resume it. Shared by onBeforeUnload and destroy() — destroy() saves WITHOUT
  // scheduling a reconnect (#141).
  persistSession() {
    const session = Type.clone(this.session);
    session.ttl = Date.now() + LS_SESSION_CACHE_TIME * 1e3;
    if (this._storage) {
      try {
        this._storage.set(
          LS_SESSION,
          JSON.stringify(session)
          // LS_SESSION_CACHE_TIME
        );
      } catch (error) {
        this.getLogger().error(
          `${Text.getDateForLog()}: Pull: Could not save session info in local storage. Error: `,
          { error }
        ).catch(() => {
        });
      }
    }
  }
  // endregion ////
  // region PullStatus ////
  /**
   * @param status
   * @param delay
   */
  sendPullStatusDelayed(status, delay) {
    if (this._disposed) {
      return;
    }
    this.armTimeout("_offlineTimeout", () => {
      this._offlineTimeout = null;
      this.sendPullStatus(status);
    }, delay);
  }
  /**
   * @param status
   */
  sendPullStatus(status) {
    if (this._unloading) {
      return;
    }
    this.onCustomEvent("onPullStatus", [status]);
    this.emit({
      type: SubscriptionType.Status,
      data: {
        status
      }
    });
  }
  // endregion ////
  // region _watchTagsQueue ////
  /**
   * @memo if private?
   * @param tagId
   * @param force
   */
  // @ts-expect-error When we rewrite it to something more modern, then we'll remove this
  extendWatch(tagId, force = false) {
    if (this._watchTagsQueue.get(tagId)) {
      return;
    }
    this._watchTagsQueue.set(tagId, true);
    if (force) {
      this.updateWatch(force);
    }
  }
  /**
   * @param force
   */
  updateWatch(force = false) {
    if (this._disposed) {
      return;
    }
    this.armTimeout(
      "_watchUpdateTimeout",
      () => {
        const watchTags = [...this._watchTagsQueue.keys()];
        if (watchTags.length > 0) {
          this._restClient.actions.v2.call.make({
            method: "pull.watch.extend",
            params: { tags: watchTags }
          }).then((response) => {
            const updatedTags = response.getData().result;
            for (const tagId of updatedTags) {
              this.clearWatch(tagId);
            }
            this.updateWatch();
          }).catch(() => {
            this.updateWatch();
          });
        } else {
          this.updateWatch();
        }
      },
      force ? this._watchForceUpdateInterval : this._watchUpdateInterval
    );
  }
  /**
   * @param tagId
   */
  clearWatch(tagId) {
    this._watchTagsQueue.delete(tagId);
  }
  // endregion ////
  // region Ping ////
  onJsonRpcPing() {
    this.updatePingWaitTimeout();
    this.connector?.send(JSON_RPC_PONG);
  }
  updatePingWaitTimeout() {
    if (this._disposed) {
      return;
    }
    this.armTimeout(
      "_pingWaitTimeout",
      this._onPingTimeoutHandler,
      PING_TIMEOUT * 2 * 1e3
    );
  }
  clearPingWaitTimeout() {
    if (this._pingWaitTimeout) {
      clearTimeout(this._pingWaitTimeout);
    }
    this._pingWaitTimeout = null;
  }
  onPingTimeout() {
    this._pingWaitTimeout = null;
    if (this._disposed || !this._enabled || !this.isConnected()) {
      return;
    }
    this.getLogger().warning(`No pings are received in ${PING_TIMEOUT * 2} seconds. Reconnecting`).catch(() => {
    });
    this.disconnect(CloseReasons.STUCK, "connection stuck");
    this.scheduleReconnect();
  }
  // endregion ////
  // region Time ////
  /**
   * Returns reconnect delay in seconds
   *
   * @param attemptNumber
   * @return {number}
   */
  getConnectionAttemptDelay(attemptNumber) {
    let result;
    if (attemptNumber < 1) {
      result = 0.5;
    } else if (attemptNumber < 3) {
      result = 15;
    } else if (attemptNumber < 5) {
      result = 45;
    } else if (attemptNumber < 10) {
      result = 600;
    } else {
      result = 3600;
    }
    return result + result * Math.random() * 0.2;
  }
  // endregion ////
  // region Tools ////
  /**
   * @param mid
   */
  checkDuplicate(mid) {
    if (this._session.lastMessageIds.includes(mid)) {
      this.getLogger().warning(`Duplicate message ${mid} skipped`).catch(() => {
      });
      return false;
    } else {
      this._session.lastMessageIds.push(mid);
      return true;
    }
  }
  trimDuplicates() {
    if (this._session.lastMessageIds.length > MAX_IDS_TO_STORE) {
      this._session.lastMessageIds = this._session.lastMessageIds.slice(-MAX_IDS_TO_STORE);
    }
  }
  // endregion ////
  // region Logging ////
  /**
   * @param message
   */
  logMessage(message) {
    if (!this._debug) {
      return;
    }
    if (message.extra?.sender && message.extra.sender.type === SenderType.Client) {
      this.getLogger().info(
        `onPullClientEvent-${message.module_id}`,
        redactSensitiveParams({
          command: message.command,
          params: message.params,
          extra: message.extra
        })
      ).catch(() => {
      });
    } else if (message.module_id == "online") {
      this.getLogger().info(
        `onPullOnlineEvent`,
        redactSensitiveParams({
          command: message.command,
          params: message.params,
          extra: message.extra
        })
      ).catch(() => {
      });
    } else {
      this.getLogger().info(
        `onPullEvent`,
        redactSensitiveParams({
          moduleId: message.module_id,
          command: message.command,
          params: message.params,
          extra: message.extra
        })
      ).catch(() => {
      });
    }
  }
  /**
   * @param message
   * @param force
   */
  logToConsole(message, force = false) {
    if (this._loggingEnabled || force) {
      this.getLogger().debug(`${Text.getDateForLog()}: ${message}`).catch(() => {
      });
    }
  }
  /**
   * @param message
   */
  addMessageToStat(message) {
    if (!this._session.history[message.module_id]) {
      this._session.history[message.module_id] = {};
    }
    if (!this._session.history[message.module_id][message.command]) {
      this.session.history[message.module_id][message.command] = 0;
    }
    this._session.history[message.module_id][message.command]++;
    this._session.messageCount++;
  }
  /**
   * @param text
   */
  showNotification(text) {
    this.getLogger().notice(text).catch(() => {
    });
  }
  // endregion ////
  // region onCustomEvent ////
  /**
   * @memo may be need to use onCustomEvent
   * @memo ? force
   */
  onCustomEvent(eventName, data, force = false) {
  }
  // endregion ////
}

export { PullClient };
//# sourceMappingURL=client.mjs.map