@bitrix24/b24jssdk
Version:
Bitrix24 REST API JavaScript SDK
1,586 lines (1,585 loc) • 62.9 kB
JavaScript
/**
* @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