UNPKG

@bitrix24/b24jssdk

Version:

Bitrix24 REST API JavaScript SDK

1,586 lines (1,585 loc) 62.9 kB
/** * @package @bitrix24/b24jssdk * @version 1.0.1 * @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 { 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, SubscriptionType, LsKeys, 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; // manual stop workaround //// _isManualDisconnect = false; _loggingEnabled = false; // bound event handlers //// _onPingTimeoutHandler; // [userId] => array of callbacks _userStatusCallbacks = {}; _connectPromise = null; _startingPromise = null; // 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 = 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); } 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; } destroy() { this.stop(CloseReasons.NORMAL_CLOSURE, "manual stop"); this.onBeforeUnload(); } init() { 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; window.addEventListener("beforeunload", this.onBeforeUnload.bind(this)); window.addEventListener("offline", this.onOffline.bind(this)); window.addEventListener("online", this.onOnline.bind(this)); 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.`); 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 } ); } method(data.params, data.extra, data.command); } }, "callback") }); } /** * @param config */ async start(config = null) { 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; return this._startingPromise = new Promise((resolve, reject) => { this.loadConfig("client_start").then((config2) => { 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 } ); reject(error); }); }); } /** * @param disconnectCode * @param disconnectReason */ restart(disconnectCode = CloseReasons.NORMAL_CLOSURE, disconnectReason = "manual restart") { if (this._restartTimeout) { clearTimeout(this._restartTimeout); this._restartTimeout = null; } this.getLogger().debug( `${Text.getDateForLog()}: Pull: restarting with code ${disconnectCode}` ); 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 }); }); }, (error) => { this.getLogger().error( `${Text.getDateForLog()}: Pull: could not read push-server config `, { error } ); 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(); } 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 }); reject(error); }); }).catch((error) => { this.getLogger().error("getUsersLastSeen", { error }); 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 = { ChannelID: this._config.channels.private?.id || "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"; } } return { "UserId": this._userId + (this._userId > 0 ? "" : "(guest)"), "Guest userId": this._guestMode && this._guestUserId !== 0 ? this._guestUserId : "-", "Browser online": 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, "Path": this.connector?.connectionPath || "-", ...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) { channels.push(this._config.channels.private?.id || ""); } 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.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, message }); } 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`); 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._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) { try { this._storage.set(LsKeys.PullConfig, config); } catch (error) { if (localStorage && localStorage.removeItem) { localStorage.removeItem("history"); } this.getLogger().error( `${Text.getDateForLog()}: Pull: Could not cache config in local storage.`, { error } ); } } } 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._enabled) { return; } if (!connectionDelay) { { connectionDelay = this.getConnectionAttemptDelay( this._connectionAttempt ); } } if (this._reconnectTimeout) { clearTimeout(this._reconnectTimeout); this._reconnectTimeout = null; } this.logToConsole( `Pull: scheduling reconnection in ${connectionDelay} seconds; attempt # ${this._connectionAttempt}` ); this._reconnectTimeout = setTimeout(() => { this.connect().catch((error) => { this.getLogger().error("scheduleReconnect", { error }); }); }, connectionDelay * 1e3); } scheduleRestoreWebSocketConnection() { this.logToConsole( `Pull: scheduling restoration of websocket connection in ${RESTORE_WEBSOCKET_TIMEOUT} seconds` ); if (this._restoreWebSocketTimeout) { return; } this._restoreWebSocketTimeout = setTimeout(() => { this._restoreWebSocketTimeout = null; this.restoreWebSocketConnection(); }, RESTORE_WEBSOCKET_TIMEOUT * 1e3); } /** * @returns {Promise} */ async connect() { if (!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._restartTimeout) { clearTimeout(this._restartTimeout); this._restartTimeout = null; } if (restartDelay < 1) { restartDelay = Math.ceil(Math.random() * 30) + 5; } this._restartTimeout = setTimeout( () => 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: ${this._config.channels.private}` ); } 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: ${this._config.channels.shared}` ); } 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 } ); 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._r