PageSourceSearch

https://alrf.ru/bitrix/js/pull/client/pull.client.js?172310890183503

js alrf.ru collected 2026-10-02 06:37:50 UTC 83,503 bytes, 3,612 lines download raw bytes

1;(function () {
2	/**
3	 * Bitrix Push & Pull
4	 * Pull client
5	 *
6	 * @package bitrix
7	 * @subpackage pull
8	 * @copyright 2001-2019 Bitrix
9	 */
10
11	/****************** ATTENTION *******************************
12	 * Please do not use Bitrix CoreJS in this class.
13	 * This class can be called on a page without Bitrix Framework
14	 *************************************************************/
15
16	if (!window.BX)
17	{
18		window.BX = {};
19	}
20	else if (window.BX.PullClient)
21	{
22		return;
23	}
24	else if (!window.BX.RestClient)
25	{
26		return;
27	}
28
29	const BX = window.BX;
30	const protobuf = window.protobuf;
31
32	const REVISION = 19; // api revision - check module/pull/include.php
33	const LONG_POLLING_TIMEOUT = 60;
34	const RESTORE_WEBSOCKET_TIMEOUT = 30 * 60;
35	const CONFIG_TTL = 24 * 60 * 60;
36	const CONFIG_CHECK_INTERVAL = 60000;
37	const MAX_IDS_TO_STORE = 10;
38	const OFFLINE_STATUS_DELAY = 5000;
39
40	const LS_SESSION = "bx-pull-session";
41	const LS_SESSION_CACHE_TIME = 20;
42
43	const ConnectionType = {
44		WebSocket: 'webSocket',
45		LongPolling: 'longPolling'
46	};
47
48	const PullStatus = {
49		Online: 'online',
50		Offline: 'offline',
51		Connecting: 'connect'
52	};
53
54	const SenderType = {
55		Unknown: 0,
56		Client: 1,
57		Backend: 2
58	};
59
60	const SubscriptionType = {
61		Server: 'server',
62		Client: 'client',
63		Online: 'online',
64		Status: 'status',
65		Revision: 'revision'
66	};
67
68	const CloseReasons = {
69		NORMAL_CLOSURE: 1000,
70		SERVER_DIE: 1001,
71		CONFIG_REPLACED: 3000,
72		CHANNEL_EXPIRED: 3001,
73		SERVER_RESTARTED: 3002,
74		CONFIG_EXPIRED: 3003,
75		MANUAL: 3004,
76		STUCK: 3005,
77		WRONG_CHANNEL_ID: 4010,
78	};
79
80	const SystemCommands = {
81		CHANNEL_EXPIRE: 'CHANNEL_EXPIRE',
82		CONFIG_EXPIRE: 'CONFIG_EXPIRE',
83		SERVER_RESTART: 'SERVER_RESTART'
84	};
85
86	const ServerMode = {
87		Shared: 'shared',
88		Personal: 'personal'
89	};
90
91	const EmptyConfig = {
92		api: {},
93		channels: {},
94		publicChannels: {},
95		server: {timeShift: 0},
96		clientId: null,
97		jwt: null,
98		exp: 0,
99	};
100
101	// Protobuf message models
102	const Response = protobuf.roots['push-server']['Response'];
103	const ResponseBatch = protobuf.roots['push-server']['ResponseBatch'];
104	const Request = protobuf.roots['push-server']['Request'];
105	const RequestBatch = protobuf.roots['push-server']['RequestBatch'];
106	const IncomingMessagesRequest = protobuf.roots['push-server']['IncomingMessagesRequest'];
107	const IncomingMessage = protobuf.roots['push-server']['IncomingMessage'];
108	const Receiver = protobuf.roots['push-server']['Receiver'];
109
110	const JSON_RPC_VERSION = "2.0"
111	const JSON_RPC_PING = "ping"
112	const JSON_RPC_PONG = "pong"
113
114	const PING_TIMEOUT = 10;
115
116	const RpcError = {
117		Parse: {code: -32700, message: "Parse error"},
118		InvalidRequest: {code: -32600, message: "Invalid Request"},
119		MethodNotFound: {code: -32601, message: "Method not found"},
120		InvalidParams: {code: -32602, message: "Invalid params"},
121		Internal: {code: -32603, message: "Internal error"},
122	};
123
124	const RpcMethod = {
125		Publish: "publish",
126		GetUsersLastSeen: "getUsersLastSeen",
127		Ping: "ping",
128		ListChannels: "listChannels",
129		SubscribeStatusChange: "subscribeStatusChange",
130		UnsubscribeStatusChange: "unsubscribeStatusChange",
131	}
132
133	class PullClient
134	{
135		constructor(params)
136		{
137			params = params || {};
138
139			if (params.restApplication)
140			{
141				if (typeof params.configGetMethod === 'undefined')
142				{
143					params.configGetMethod = 'pull.application.config.get';
144				}
145				if (typeof params.skipCheckRevision === 'undefined')
146				{
147					params.skipCheckRevision = true;
148				}
149				if (typeof params.restApplication === 'string')
150				{
151					params.siteId = params.restApplication;
152				}
153
154				params.serverEnabled = true;
155			}
156
157			this._status = PullStatus.Offline;
158
159			this.context = 'master';
160
161			this.guestMode = params.guestMode ? params.guestMode : (typeof BX.message !== 'undefined' && BX.message.pull_guest_mode ? BX.message.pull_guest_mode === 'Y' : false);
162			this.guestUserId = params.guestUserId ? params.guestUserId : (typeof BX.message !== 'undefined' && BX.message.pull_guest_user_id ? parseInt(BX.message.pull_guest_user_id, 10) : 0);
163			if (this.guestMode && this.guestUserId)
164			{
165				this.userId = this.guestUserId;
166			}
167			else
168			{
169				this.userId = params.userId ? params.userId : (typeof BX.message !== 'undefined' && BX.message.USER_ID ? BX.message.USER_ID : 0);
170			}
171
172			this.siteId = params.siteId ? params.siteId : (typeof BX.message !== 'undefined' && BX.message.SITE_ID ? BX.message.SITE_ID : 'none');
173			this.restClient = typeof params.restClient !== "undefined" ? params.restClient : new BX.RestClient(this.getRestClientOptions());
174
175			this.enabled = typeof params.serverEnabled !== 'undefined' ? (params.serverEnabled === 'Y' || params.serverEnabled === true) : (typeof BX.message !== 'undefined' && BX.message.pull_server_enabled === 'Y');
176			this.unloading = false;
177			this.starting = false;
178			this.debug = false;
179			this.connectionAttempt = 0;
180			this.connectionType = ConnectionType.WebSocket;
181			this.reconnectTimeout = null;
182			this.restartTimeout = null;
183			this.restoreWebSocketTimeout = null;
184
185			this.configGetMethod = typeof params.configGetMethod !== 'string' ? 'pull.config.get' : params.configGetMethod;
186			this.getPublicListMethod = typeof params.getPublicListMethod !== 'string' ? 'pull.channel.public.list' : params.getPublicListMethod;
187
188			this.skipStorageInit = params.skipStorageInit === true;
189
190			this.skipCheckRevision = params.skipCheckRevision === true;
191
192			this._subscribers = {};
193
194			this.watchTagsQueue = {};
195			this.watchUpdateInterval = 1740000;
196			this.watchForceUpdateInterval = 5000;
197
198			if (typeof params.configTimestamp !== 'undefined')
199			{
200				this.configTimestamp = params.configTimestamp;
201			}
202			else if (typeof BX.message !== 'undefined' && BX.message.pull_config_timestamp)
203			{
204				this.configTimestamp = BX.message.pull_config_timestamp;
205			}
206			else
207			{
208				this.configTimestamp = 0;
209			}
210
211			this.session = {
212				mid: null,
213				tag: null,
214				time: null,
215				history: {},
216				lastMessageIds: [],
217				messageCount: 0
218			};
219
220			this._connectors = {
221				webSocket: null,
222				longPolling: null
223			};
224
225			this.isSecure = document.location.href.indexOf('https') === 0;
226			this.config = null;
227
228			this.storage = null;
229
230			if (this.userId && !this.skipStorageInit)
231			{
232				this.storage = new StorageManager({
233					userId: this.userId,
234					siteId: this.siteId
235				});
236			}
237
238			this.sharedConfig = new SharedConfig({
239				onWebSocketBlockChanged: this.onWebSocketBlockChanged.bind(this),
240				storage: this.storage
241			});
242			this.channelManager = new ChannelManager({
243				restClient: this.restClient,
244				getPublicListMethod: this.getPublicListMethod
245			});
246
247			this.notificationPopup = null;
248
249			// timers
250			this.checkInterval = null;
251			this.offlineTimeout = null;
252
253			this.pingWaitTimeout = null;
254
255			// manual stop workaround
256			this.isManualDisconnect = false;
257
258			this.loggingEnabled = this.sharedConfig.isLoggingEnabled();
259
260			// bound event handlers
261			this.onPingTimeoutHandler = this.onPingTimeout.bind(this);
262
263			this.userStatusCallbacks = {}; // [userId] => array of callbacks
264		}
265
266		get connector()
267		{
268			return this._connectors[this.connectionType];
269		}
270
271		get status()
272		{
273			return this._status;
274		}
275
276		set status(status)
277		{
278			if (this._status === status)
279			{
280				return;
281			}
282
283			this._status = status;
284			if (this.offlineTimeout)
285			{
286				clearTimeout(this.offlineTimeout)
287				this.offlineTimeout = null;
288			}
289
290			if (status === PullStatus.Offline)
291			{
292				this.sendPullStatusDelayed(status, OFFLINE_STATUS_DELAY);
293			}
294			else
295			{
296				this.sendPullStatus(status);
297			}
298		}
299
300		/**
301		 * Creates a subscription to incoming messages.
302		 *
303		 * @param {Object} params
304		 * @param {string} [params.type] Subscription type (for possible values see SubscriptionType).
305		 * @param {string} [params.moduleId] Name of the module.
306		 * @param {Function} params.callback Function, that will be called for incoming messages.
307		 * @returns {Function} - Unsubscribe callback function
308		 */
309		subscribe(params)
310		{
311			/**
312			 * After modify this method, copy to follow scripts:
313			 * mobile/install/mobileapp/mobile/extensions/bitrix/pull/client/events/extension.js
314			 * mobile/install/js/mobile/pull/client/src/client.js
315			 */
316
317			if (!params)
318			{
319				console.error(Utils.getDateForLog() + ': Pull.subscribe: params for subscribe function is invali
319d. ');
320				return function () {}
321			}
322
323			if (!Utils.isPlainObject(params))
324			{
325				return this.attachCommandHandler(params);
326			}
327
328			params = params || {};
329			params.type = params.type || SubscriptionType.Server;
330			params.command = params.command || null;
331
332			if (params.type == SubscriptionType.Server || params.type == SubscriptionType.Client)
333			{
334				if (typeof (this._subscribers[params.type]) === 'undefined')
335				{
336					this._subscribers[params.type] = {};
337				}
338				if (typeof (this._subscribers[params.type][params.moduleId]) === 'undefined')
339				{
340					this._subscribers[params.type][params.moduleId] = {
341						'callbacks': [],
342						'commands': {},
343					};
344				}
345
346				if (params.command)
347				{
348					if (typeof (this._subscribers[params.type][params.moduleId]['commands'][params.command]) === 'undefined')
349					{
350						this._subscribers[params.type][params.moduleId]['commands'][params.command] = [];
351					}
352
353					this._subscribers[params.type][params.moduleId]['commands'][params.command].push(params.callback);
354
355					return function () {
356						this._subscribers[params.type][params.moduleId]['commands'][params.command] = this._subscribers[params.type][params.moduleId]['commands'][params.command].filter((element) => {
357							return element !== params.callback;
358						});
359					}.bind(this);
360				}
361				else
362				{
363					this._subscribers[params.type][params.moduleId]['callbacks'].push(params.callback);
364
365					return function () {
366						this._subscribers[params.type][params.moduleId]['callbacks'] = this._subscribers[params.type][params.moduleId]['callbacks'].filter((element) => {
367							return element !== params.callback;
368						});
369					}.bind(this);
370				}
371			}
372			else
373			{
374				if (typeof (this._subscribers[params.type]) === 'undefined')
375				{
376					this._subscribers[params.type] = [];
377				}
378
379				this._subscribers[params.type].push(params.callback);
380
381				return function () {
382					this._subscribers[params.type] = this._subscribers[params.type].filter((element) => {
383						return element !== params.callback;
384					});
385				}.bind(this);
386			}
387		}
388
389		attachCommandHandler(handler)
390		{
391			/**
392			 * After modify this method, copy to follow scripts:
393			 * mobile/install/mobileapp/mobile/extensions/bitrix/pull/client/events/extension.js
394			 */
395			if (typeof handler.getModuleId !== 'function' || typeof handler.getModuleId() !== 'string')
396			{
397				console.error(Utils.getDateForLog() + ': Pull.attachCommandHandler: result of handler.getModuleId() is not a string.');
398				return function () {}
399			}
400
401			let type = SubscriptionType.Server;
402			if (typeof handler.getSubscriptionType === 'function')
403			{
404				type = handler.getSubscriptionType();
405			}
406
407			return this.subscribe({
408				type: type,
409				moduleId: handler.getModuleId(),
410				callback: function (data) {
411					let method = null;
412
413					if (typeof handler.getMap === 'function')
414					{
415						const mapping = handler.getMap();
416						if (mapping && typeof mapping === 'object')
417						{
418							if (typeof mapping[data.command] === 'function')
419							{
420								method = mapping[data.command].bind(handler)
421							}
422							else if (typeof mapping[data.command] === 'string' && typeof handler[mapping[data.command]] === 'function')
423							{
424								method = handler[mapping[data.command]].bind(handler);
425							}
426						}
427					}
428
429					if (!method)
430					{
431						const methodName = 'handle' + data.command.charAt(0).toUpperCase() + data.command.slice(1);
432						if (typeof handler[methodName] === 'function')
433						{
434							method = handler[methodName].bind(handler);
435						}
436					}
437
438					if (method)
439					{
440						if (this.debug && this.context !== 'master')
441						{
442							console.warn(Utils.getDateForLog() + ': Pull.attachCommandHandler: receive command', data);
443						}
444						method(data.params, data.extra, data.command);
445					}
446				}.bind(this)
447			});
448		}
449
450		/**
451		 *
452		 * @param params {Object}
453		 * @returns {boolean}
454		 */
455		emit(params)
456		{
457			/**
458			 * After modify this method, copy to follow scripts:
459			 * mobile/install/mobileapp/mobile/extensions/bitrix/pull/client/events/extension.js
460			 * mobile/install/js/mobile/pull/client/src/client.js
461			 */
462			params = params || {};
463
464			if (params.type == SubscriptionType.Server || params.type == SubscriptionType.Client)
465			{
466				if (typeof (this._subscribers[params.type]) === 'undefined')
467				{
468					this._subscribers[params.type] = {};
469				}
470				if (typeof (this._subscribers[params.type][params.moduleId]) === 'undefined')
471				{
472					this._subscribers[params.type][params.moduleId] = {
473						'callbacks': [],
474						'commands': {},
475					};
476				}
477
478				if (this._subscribers[params.type][params.moduleId]['callbacks'].length > 0)
479				{
480					this._subscribers[params.type][params.moduleId]['callbacks'].forEach(function (callback) {
481						callback(params.data, {type: params.type, moduleId: params.moduleId});
482					});
483				}
484
485				if (
486					this._subscribers[params.type][params.moduleId]['commands'][params.data.command]
487					&& this._subscribers[params.type][params.moduleId]['commands'][params.data.command].length > 0)
488				{
489					this._subscribers[params.type][params.moduleId]['commands'][params.data.command].forEac
489h(function (callback) {
490						callback(params.data.params, params.data.extra, params.data.command, {
491							type: params.type,
492							moduleId: params.moduleId
493						});
494					});
495				}
496
497				return true;
498			}
499			else
500			{
501				if (typeof (this._subscribers[params.type]) === 'undefined')
502				{
503					this._subscribers[params.type] = [];
504				}
505
506				if (this._subscribers[params.type].length <= 0)
507				{
508					return true;
509				}
510
511				this._subscribers[params.type].forEach(function (callback) {
512					callback(params.data, {type: params.type});
513				});
514
515				return true;
516			}
517		}
518
519		init()
520		{
521			this._connectors.webSocket = new WebSocketConnector({
522				parent: this,
523				onOpen: this.onWebSocketOpen.bind(this),
524				onMessage: this.onIncomingMessage.bind(this),
525				onDisconnect: this.onWebSocketDisconnect.bind(this),
526				onError: this.onWebSocketError.bind(this)
527			});
528
529			this._connectors.longPolling = new LongPollingConnector({
530				parent: this,
531				onOpen: this.onLongPollingOpen.bind(this),
532				onMessage: this.onIncomingMessage.bind(this),
533				onDisconnect: this.onLongPollingDisconnect.bind(this),
534				onError: this.onLongPollingError.bind(this)
535			});
536
537			this.connectionType = this.isWebSocketAllowed() ? ConnectionType.WebSocket : ConnectionType.LongPolling;
538
539			window.addEventListener("beforeunload", this.onBeforeUnload.bind(this));
540			window.addEventListener("offline", this.onOffline.bind(this));
541			window.addEventListener("online", this.onOnline.bind(this));
542
543			if (BX && BX.addCustomEvent)
544			{
545				BX.addCustomEvent("BXLinkOpened", this.connect.bind(this));
546			}
547
548			if (BX && BX.desktop)
549			{
550				BX.addCustomEvent("onDesktopReload", () => {
551					this.session.mid = null;
552					this.session.tag = null;
553					this.session.time = null;
554				});
555
556				BX.desktop.addCustomEvent("BXLoginSuccess", () => this.restart(1000, "desktop login"));
557			}
558
559			this.jsonRpcAdapter = new JsonRpc({
560				connector: this._connectors.webSocket,
561				handlers: {
562					"incoming.message": this.handleRpcIncomingMessage.bind(this),
563				}
564			});
565		}
566
567		start(config)
568		{
569			let allowConfigCaching = true;
570
571			if (this.isConnected())
572			{
573				return Promise.resolve(true);
574			}
575
576			if (this.starting && this._startingPromise)
577			{
578				return this._startingPromise;
579			}
580
581			if (!this.userId && typeof (BX.message) !== 'undefined' && BX.message.USER_ID)
582			{
583				this.userId = BX.message.USER_ID;
584				if (!this.storage)
585				{
586					this.storage = new StorageManager({
587						userId: this.userId,
588						siteId: this.siteId
589					});
590				}
591			}
592			if (this.siteId === 'none' && typeof (BX.message) !== 'undefined' && BX.message.SITE_ID)
593			{
594				this.siteId = BX.message.SITE_ID;
595			}
596
597			let skipReconnectToLastSession = false;
598			if (Utils.isPlainObject(config))
599			{
600				if (typeof config.skipReconnectToLastSession !== 'undefined')
601				{
602					skipReconnectToLastSession = !!config.skipReconnectToLastSession;
603					delete config.skipReconnectToLastSession;
604				}
605				this.config = config;
606				allowConfigCaching = false;
607			}
608
609			if (!this.enabled)
610			{
611				return Promise.reject({
612					ex: {error: 'PULL_DISABLED', error_description: 'Push & Pull server is disabled'}
613				});
614			}
615
616			const now = (new Date()).getTime();
617			let oldSession;
618			if (!skipReconnectToLastSession && this.storage)
619			{
620				oldSession = this.storage.get(LS_SESSION);
621			}
622			if (Utils.isPlainObject(oldSession) && oldSession.hasOwnProperty('ttl') && oldSession.ttl >= now)
623			{
624				this.session.mid = oldSession.mid;
625			}
626
627			this.starting = true;
628			return new Promise((resolve, reject) => {
629				this._startingPromise = {resolve, reject};
630				this.loadConfig("client_start").then(
631					(config) => {
632						this.setConfig(config, allowConfigCaching);
633						this.init();
634						this.updateWatch();
635						this.startCheckConfig();
636						this.connect().then(
637							() => resolve(true),
638							error => reject(error)
639						);
640					},
641					(error) => {
642						this.starting = false;
643						this.status = PullStatus.Offline;
644						this.stopCheckConfig();
645						console.error(Utils.getDateForLog() + ': Pull: could not read push-server config. ', error);
646						reject(error);
647					}
648				);
649			})
650		}
651
652		getRestClientOptions()
653		{
654			let result = {};
655
656			if (this.guestMode && this.guestUserId !== 0)
657			{
658				result.queryParams = {
659					pull_guest_id: this.guestUserId
660				}
661			}
662			return result;
663		}
664
665		setLastMessageId(lastMessageId)
666		{
667			this.session.mid = lastMessageId;
668		}
669
670		/**
671		 *
672		 * @param {object[]} publicIds
673		 * @param {integer} publicIds.user_id
674		 * @param {string} publicIds.public_id
675		 * @param {string} publicIds.signature
676		 * @param {Date} publicIds.start
677		 * @param {Date} publicIds.end
678		 */
679		setPublicIds(publicIds)
680		{
681			return this.channelManager.setPublicIds(publicIds);
682		}
683
684		/**
685		 * Send single message to the specified users.
686		 *
687		 * @param {integer[]}
687 users User ids of the message receivers.
688		 * @param {string} moduleId Name of the module to receive message,
689		 * @param {string} command Command name.
690		 * @param {object} params Command parameters.
691		 * @param {integer} [expiry] Message expiry time in seconds.
692		 * @return {Promise}
693		 */
694		sendMessage(users, moduleId, command, params, expiry)
695		{
696			const message = {
697				userList: users,
698				body: {
699					module_id: moduleId,
700					command: command,
701					params: params,
702				},
703				expiry: expiry
704			};
705
706			if (this.isJsonRpc())
707			{
708				return this.jsonRpcAdapter.executeOutgoingRpcCommand(RpcMethod.Publish, message)
709			}
710			else
711			{
712				return this.sendMessageBatch([message]);
713			}
714		}
715
716		/**
717		 * Send single message to the specified public channels.
718		 *
719		 * @param {string[]} publicChannels Public ids of the channels to receive message.
720		 * @param {string} moduleId Name of the module to receive message,
721		 * @param {string} command Command name.
722		 * @param {object} params Command parameters.
723		 * @param {integer} [expiry] Message expiry time in seconds.
724		 * @return {Promise}
725		 */
726		sendMessageToChannels(publicChannels, moduleId, command, params, expiry)
727		{
728			const message = {
729				channelList: publicChannels,
730				body: {
731					module_id: moduleId,
732					command: command,
733					params: params,
734				},
735				expiry: expiry
736			};
737
738			if (this.isJsonRpc())
739			{
740				return this.jsonRpcAdapter.executeOutgoingRpcCommand(RpcMethod.Publish, message)
741			}
742			else
743			{
744				return this.sendMessageBatch([message]);
745			}
746		}
747
748		/**
749		 * Sends batch of messages to the multiple public channels.
750		 *
751		 * @param {object[]} messageBatch Array of messages to send.
752		 * @param  {int[]} messageBatch.userList User ids the message receivers.
753		 * @param  {string[]|object[]} messageBatch.channelList Public ids of the channels to send messages.
754		 * @param {string} messageBatch.moduleId Name of the module to receive message,
755		 * @param {string} messageBatch.command Command name.
756		 * @param {object} messageBatch.params Command parameters.
757		 * @param {integer} [messageBatch.expiry] Message expiry time in seconds.
758		 * @return void
759		 */
760		sendMessageBatch(messageBatch)
761		{
762			if (!this.isPublishingEnabled())
763			{
764				console.error('Client publishing is not supported or is disabled');
765				return false;
766			}
767
768			if (this.isJsonRpc())
769			{
770				let rpcRequest = this.jsonRpcAdapter.createPublishRequest(messageBatch);
771				return this.connector.send(JSON.stringify(rpcRequest));
772			}
773			else
774			{
775				let userIds = {};
776				for (let i = 0; i < messageBatch.length; i++)
777				{
778					if (messageBatch[i].userList)
779					{
780						for (let j = 0; j < messageBatch[i].userList.length; j++)
781						{
782							userIds[messageBatch[i].userList[j]] = true;
783						}
784					}
785				}
786				this.channelManager.getPublicIds(Object.keys(userIds)).then((publicIds) => {
787					return this.connector.send(this.encodeMessageBatch(messageBatch, publicIds));
788				})
789			}
790		}
791
792		encodeMessageBatch(messageBatch, publicIds)
793		{
794			let messages = [];
795			messageBatch.forEach(function (messageFields) {
796				const messageBody = messageFields.body;
797
798				let receivers;
799				if (messageFields.userList)
800				{
801					receivers = this.createMessageReceivers(messageFields.userList, publicIds);
802				}
803				else
804				{
805					receivers = [];
806				}
807
808				if (messageFields.channelList)
809				{
810					if (!Utils.isArray(messageFields.channelList))
811					{
812						throw new Error('messageFields.publicChannels must be an array');
813					}
814					messageFields.channelList.forEach(function (publicChannel) {
815						let publicId;
816						let signature;
817						if (typeof (publicChannel) === 'string' && publicChannel.includes('.'))
818						{
819							const fields = publicChannel.toString().split('.');
820							publicId = fields[0];
821							signature = fields[1];
822						}
823						else if (typeof (publicChannel) === 'object' && ('publicId' in publicChannel) && ('signature' in publicChannel))
824						{
825							publicId = publicChannel.publicId;
826							signature = publicChannel.signature;
827						}
828						else
829						{
830							throw new Error('Public channel MUST be either a string, formatted like "{publicId}.{signature}" or an object with fields \'publicId\' and \'signature\'');
831						}
832
833						receivers.push(Receiver.create({
834							id: this.encodeId(publicId),
835							signature: this.encodeId(signature)
836						}))
837					}.bind(this))
838				}
839
840				const message = IncomingMessage.create({
841					receivers: receivers,
842					body: JSON.stringify(messageBody),
843					expiry: messageFields.expiry || 0
844				});
845				messages.push(message);
846			}, this);
847
848			const requestBatch = RequestBatch.create({
849				requests: [{
850					incomingMessages: {
851						messages: messages
852					}
853				}]
854			});
855
856			return RequestBatch.encode(requestBatch).finish();
857		}
858
859		createMessageReceivers(users, publicIds)
860		{
861			let result = [];
862			for (let i = 0; i < users.length; i++)
863			{
864				let userId = users[i];
865				if (!publicIds[userId] || !publicIds[userId].publicId)
866				{
867					throw new Error('Could not determine public id for user ' + userId);
868				}
869
870				result.push(Receiver.create({
871					id: this.encodeId(publicIds[userId].publicId),
872					signature: this.encodeId(publicIds[userId].signature)
873				}));
874			}
875			return result;
876		}
877
878		/**
879		 * @param userId {number}
880		 * @param callback {UserStatusCallback}
881		 * @returns {Promise}
882		 */
883		subscribeUserStatusChange(userId, callback)
884		{
885			if (typeof (userId) !== 'number')
886			{
887				throw new Error('userId must be a number');
888			}
889
890			return new Promise((resolve, reject) => {
891				this.jsonRpcAdapter.executeOutgoingRpcCommand(RpcMethod.SubscribeStatusChange, {userId}).then(() => {
892					if (!this.userStatusCallbacks[userId])
893					{
894						this.userStatusCallbacks[userId] = [];
895					}
896					if (Utils.isFunction(callback))
897					{
898						this.userStatusCallbacks[userId].push(callback);
899					}
900
901					return resolve()
902				}).catch(err => reject(err))
903			})
904		}
905
906		/**
907		 * @param userId {number}
908		 * @param callback {UserStatusCallback}
909		 * @returns {Promise}
910		 */
911		unsubscribeUserStatusChange(userId, callback)
912		{
913			if (typeof (userId) !== 'number')
914			{
915				throw new Error('userId must be a number');
916			}
917			if (this.userStatusCallbacks[userId])
918			{
919				this.userStatusCallbacks[userId] = this.userStatusCallbacks[userId].filter(cb => cb !== callback)
920				if (this.userStatusCallbacks[userId].length === 0)
921				{
922					return this.jsonRpcAdapter.executeOutgoingRpcCommand(RpcMethod.UnsubscribeStatusChange, {userId});
923				}
924			}
925
926			return Promise.resolve();
927		}
928
929		emitUserStatusChange(userId, isOnline)
930		{
931			if (this.userStatusCallbacks[userId])
932			{
933				this.userStatusCallbacks[userId].forEach(cb => cb({userId, isOnline}));
934			}
935		}
936
937		restoreUserStatusSubscription()
938		{
939			for (const userId in this.userStatusCallbacks)
940			{
941				if (this.userStatusCallbacks.hasOwnProperty(userId) && this.userStatusCallbacks[userId].length > 0)
942				{
943					this.jsonRpcAdapter.executeOutgoingRpcCommand(RpcMethod.SubscribeStatusChange, {userId: Number(userId)});
944				}
945			}
946		}
947
948		/**
949		 * Returns "last seen" time in seconds for the users. Result format: Object{userId: int}
950		 * If the user is currently connected - will return 0.
951		 * If the user if offline - will return diff between current timestamp and last seen timestamp in seconds.
952		 * If the user was never online - the record for user will be missing from the result object.
953		 *
954		 * @param {integer[]} userList List of user ids.
955		 * @returns {Promise}
956		 */
957		getUsersLastSeen(userList)
958		{
959			if (!Utils.isArray(userList) || !userList.every(item => typeof (item) === 'number'))
960			{
961				throw new Error('userList must be an array of numbers');
962			}
963			return new Promise((resolve, reject) => {
964				this.jsonRpcAdapter.executeOutgoingRpcCommand(RpcMethod.GetUsersLastSeen, {
965					userList: userList
966				}).then(result => {
967					let unresolved = [];
968					for (let i = 0; i < userList.length; i++)
969					{
970						if (!result.hasOwnProperty(userList[i]))
971						{
972							unresolved.push(userList[i]);
973						}
974					}
975					if (unresolved.length === 0)
976					{
977						return resolve(result);
978					}
979
980					const params = {
981						userIds: unresolved,
982						sendToQueueSever: true
983					}
984					this.restClient.callMethod('pull.api.user.getLastSeen', params).then(response => {
985						let data = response.data();
986						for (let userId in data)
987						{
988							result[userId] = data[userId];
989						}
990						return resolve(result);
991					}).catch(error => {
992						console.error(error);
993					})
994				})
995			})
996		}
997
998		/**
999		 * Pings server. In case of success promise will be resolved, otherwise - rejected.
1000		 *
1001		 * @param {int} timeout Request timeout in seconds
1002		 * @returns {Promise}
1003		 */
1004		ping(timeout)
1005		{
1006			return this.jsonRpcAdapter.executeOutgoingRpcCommand(RpcMethod.Ping, {}, timeout);
1007		}
1008
1009		/**
1010		 * Returns list channels that the connection is subscribed to.
1011		 *
1012		 * @returns {Promise}
1013		 */
1014		listChannels()
1015		{
1016			return this.jsonRpcAdapter.executeOutgoingRpcCommand(RpcMethod.ListChannels, {});
1017		}
1018
1019		scheduleRestart(disconnectCode, disconnectReason, restartDelay)
1020		{
1021			clearTimeout(this.restartTimeout);
1022			if (!restartDelay || restartDelay < 1)
1023			{
1024				restartDelay = Math.ceil(Math.random() * 30) + 5;
1025			}
1026
1027			this.restartTimeout = setTimeout(
1028				() => this.restart(disconnectCode, disconnectReason),
1029				restartDelay * 1000
1030			);
1031		}
1032
1033		restart(disconnectCode, disconnectReason)
1034		{
1035			if (!disconnectCode)
1036			{
1037				disconnectCode = CloseReasons.NORMAL_CLOSURE;
1038			}
1039			if (!disconnectReason)
1040			{
1041				disconnectReason = 'manual restart'
1042			}
1043			clearTimeout(this.restartTimeout);
1044			console.warn(Utils.getDateForLog() + ': Pull: restarting with code ' + disconnectCode)
1045			this.disconnect(disconnectCode, disconnectReason);
1046			if (this.storage)
1047			{
1048				this.storage.remove('bx-pull-config');
1049			}
1050			this.config = null;
1051
1052			const loadConfigReason = disconnectCode + '_' + disconnectReason.replaceAll(' ', '_');
1053			this.loadConfig(loadConfigReason).then(
1054				(config) => {
1055					this.setConfig(config, true);
1056					this.updateWatch();
1057					this.startCheckConfig();
1058					this.connect().catch(error => console.error(error));
1059				},
1060				(error) => {
1061					console.error(Utils.getDateForLog() + ': Pull: could not read push-server config', error);
1062					this.status = PullStatus.Offline;
1063
1064					clearTimeout(this.reconnectTimeout);
1065					if (error.status == 401 || error.status == 403)
1066					{
1067						this.stopCheckConfig();
1068
1069						if (BX && BX.onCustomEvent)
1070						{
1071							BX.onCustomEvent(window, 'onPullError', ['AUTHORIZE_ERROR']);
1072						}
1073					}
1074				}
1075			);
1076		}
1077
1078		loadConfig(logTag)
1079		{
1080			if (!this.config)
1081			{
1082				this.config = Object.assign({}, EmptyConfig);
1083
1084				let config;
1085				if (this.storage)
1086				{
1087					config = this.storage.get('bx-pull-config');
1088				}
1089				if (this.isConfigActual(config) && this.checkRevision(config.api.revision_web))
1090				{
1091					return Promise.resolve(config);
1092				}
1093				else if (this.storage)
1094				{
1095					this.storage.remove('bx-pull-config')
1096				}
1097			}
1098			else if (this.isConfigActual(this.config) && this.checkRevision(this.config.api.revision_web))
1099			{
1100				return Promise.resolve(this.config);
1101			}
1102			else
1103			{
1104				this.config = Object.assign({}, EmptyConfig);
1105			}
1106
1107			return new Promise((resolve, reject) => {
1108				this.restClient.callMethod(this.configGetMethod, {'CACHE': 'N'}, undefined, undefined, logTag).then((response) => {
1109					const data = response.data();
1110					let timeShift;
1111
1112					timeShift = Math.floor((Utils.getTimestamp() - new Date(data.serverTime).getTime()) / 1000);
1113					delete data.serverTime;
1114
1115					let config = Object.assign({}, data);
1116					config.server.timeShift = timeShift;
1117
1118					resolve(config);
1119				}).catch((response) => {
1120					const error = response.error();
1121					if (error.getError().error == "AUTHORIZE_ERROR" || error.getError().error == "WRONG_AUTH_TYPE")
1122					{
1123						error.status = 403;
1124					}
1125					reject(error);
1126				});
1127			})
1128		}
1129
1130		isConfigActual(config)
1131		{
1132			if (!Utils.isPlainObject(config))
1133			{
1134				return false;
1135			}
1136
1137			if (config.server.config_timestamp < this.configTimestamp)
1138			{
1139				return false;
1140			}
1141
1142			const now = new Date();
1143
1144			if (BX.type.isNumber(config.exp) && config.exp > 0 && config.exp < now.getTime() / 1000)
1145			{
1146				return false;
1147			}
1148
1149			const channelCount = Object.keys(config.channels).length;
1150			if (channelCount === 0)
1151			{
1152				return false;
1153			}
1154
1155			for (let channelType in config.channels)
1156			{
1157				if (!config.channels.hasOwnProperty(channelType))
1158				{
1159					continue;
1160				}
1161
1162				const channel = config.channels[channelType];
1163				const channelEnd = new Date(channel.end);
1164
1165				if (channelEnd < now)
1166				{
1167					return false;
1168				}
1169			}
1170
1171			return true;
1172		}
1173
1174		startCheckConfig()
1175		{
1176			if (this.checkInterval)
1177			{
1178				clearInterval(this.checkInterval);
1179			}
1180
1181			this.checkInterval = setInterval(this.checkConfig.bind(this), CONFIG_CHECK_INTERVAL)
1182		}
1183
1184		stopCheckConfig()
1185		{
1186			if (this.checkInterval)
1187			{
1188				clearInterval(this.checkInterval);
1189			}
1190			this.checkInterval = null;
1191		}
1192
1193		checkConfig()
1194		{
1195			if (this.isConfigActual(this.config))
1196			{
1197				if (!this.checkRevision(this.config.api.revision_web))
1198				{
1199					return false;
1200				}
1201			}
1202			else
1203			{
1204				this.logToConsole("Stale config detected. Restarting");
1205				this.restart(CloseReasons.CONFIG_EXPIRED, "config expired");
1206			}
1207		}
1208
1209		setConfig(config, allowCaching)
1210		{
1211			for (let key in config)
1212			{
1213				if (config.hasOwnProperty(key) && this.config.hasOwnProperty(key))
1214				{
1215					this.config[key] = config[key];
1216				}
1217			}
1218
1219			if (config.publicChannels)
1220			{
1221				this.setPublicIds(Utils.objectValues(config.publicChannels));
1222			}
1223
1224			if (this.storage && allowCaching)
1225			{
1226				try
1227				{
1228					this.storage.set('bx-pull-config', config);
1229				} catch (e)
1230				{
1231					// try to delete the key "history" (landing site change history, see http://jabber.bx/view.php?id=136492)
1232					if (localStorage && localStorage.removeItem)
1233					{
1234						localStorage.removeItem('history');
1235					}
1236					console.error(Utils.getDateForLog() + " Pull: Could not cache config in local storage. Error: ", e);
1237				}
1238			}
1239		}
1240
1241		isWebSocketSupported()
1242		{
1243			return typeof (window.WebSocket) !== "undefined";
1244		}
1245
1246		isWebSocketAllowed()
1247		{
1248			if (this.sharedConfig.isWebSocketBlocked())
1249			{
1250				return false;
1251			}
1252
1253			return this.isWebSocketEnabled();
1254		}
1255
1256		isWebSocketEnabled()
1257		{
1258			if (!this.isWebSocketSupported())
1259			{
1260				return false;
1261			}
1262
1263			return (this.config && this.config.server && this.config.server.websocket_enabled === true);
1264		}
1265
1266		isPublishingSupported()
1267		{
1268			return this.getServerVersion() > 3;
1269		}
1270
1271		isPublishingEnabled()
1272		{
1273			if (!this.isPublishingSupported())
1274			{
1275				return false;
1276			}
1277
1278			return (this.config && this.config.server && this.config.server.publish_enabled === true);
1279		}
1280
1281		isProtobufSupported()
1282		{
1283			return (this.getServerVersion() == 4 && !Utils.browser.IsIe());
1284		}
1285
1286		isJsonRpc()
1287		{
1288			return (this.getServerVersion() >= 5);
1289		}
1290
1291		isSharedMode()
1292		{
1293			return (this.getServerMode() == ServerMode.Shared)
1294		}
1295
1296		disconnect(disconnectCode, disconnectReason)
1297		{
1298			if (this.connector)
1299			{
1300				this.isManualDisconnect = true;
1301				this.connector.disconnect(disconnectCode, disconnectReason);
1302			}
1303		}
1304
1305		stop(disconnectCode, disconnectReason)
1306		{
1307			this.disconnect(disconnectCode, disconnectReason);
1308			this.stopCheckConfig();
1309		}
1310
1311		reconnect(disconnectCode, disconnectReason, delay)
1312		{
1313			this.disconnect(disconnectCode, disconnectReason);
1314
1315			delay = delay || 1;
1316			this.scheduleReconnect(delay);
1317		}
1318
1319		restoreWebSocketConnection()
1320		{
1321			if (this.connectionType == ConnectionType.WebSocket)
1322			{
1323				return true;
1324			}
1325
1326			this._connectors.webSocket.connect();
1327		}
1328
1329		scheduleReconnect(connectionDelay)
1330		{
1331			if (!this.enabled)
1332			{
1333				return false;
1334			}
1335
1336			if (!connectionDelay)
1337			{
1338				// never fallback to long polling
1339				// TODO remove long polling support later
1340				/*if (this.connectionAttempt > 3 && this.connectionType === ConnectionType.WebSocket && !this.sharedConfig.isLongPollingBlocked())
1341				{
1342					// Websocket seems to be closed by network filter. Trying to fallback to long polling
1343					this.sharedConfig.setWebSocketBlocked(true);
1344					this.connectionType = ConnectionType.LongPolling;
1345					this.connectionAttempt = 1;
1346					connectionDelay = 1;
1347				}
1348				else*/
1349				{
1350					connectionDelay = this.getConnectionAttemptDelay(this.connectionAttempt);
1351				}
1352			}
1353			if (this.reconnectTimeout)
1354			{
1355				clearTimeout(this.reconnectTimeout);
1356			}
1357
1358			this.logToConsole('Pull: scheduling reconnection in ' + connectionDelay + ' seconds; attempt # ' + this.connectionAttempt);
1359
1360			this.reconnectTimeout = setTimeout(
1361				() => {
1362					this.connect().catch(error => {
1363						console.error(error)
1364					})
1365				},
1366				connectionDelay * 1000);
1367		}
1368
1369		scheduleRestoreWebSocketConnection()
1370		{
1371			this.logToConsole('Pull: scheduling restoration of websocket connection in ' + RESTORE_WEBSOCKET_TIMEOUT + ' seconds');
1372
1373			if (this.restoreWebSocketTimeout)
1374			{
1375				return;
1376			}
1377
1378			this.restoreWebSocketTimeout = setTimeout(() => {
1379				this.restoreWebSocketTimeout = 0;
1380				this.restoreWebSocketConnection();
1381			}, RESTORE_WEBSOCKET_TIMEOUT * 1000);
1382		}
1383
1384		/**
1385		 * @returns {Promise}
1386		 */
1387		connect()
1388		{
1389			if (!this.enabled)
1390			{
1391				return Promise.reject();
1392			}
1393			if (this.connector.connected)
1394			{
1395				return Promise.resolve();
1396			}
1397
1398			if (this.reconnectTimeout)
1399			{
1400				clearTimeout(this.reconnectTimeout);
1401			}
1402
1403			this.status = PullStatus.Connecting;
1404			this.connectionAttempt++;
1405			return new Promise((resolve, reject) => {
1406				this._connectPromise = {resolve, reject}
1407				this.connector.connect();
1408			})
1409		}
1410
1411		onIncomingMessage(message)
1412		{
1413			if (this.isJsonRpc())
1414			{
1415				(message === JSON_RPC_PING) ? this.onJsonRpcPing() : this.jsonRpcAdapter.parseJsonRpcMessage(message);
1416			}
1417			else
1418			{
1419				const events = this.extractMessages(message);
1420				this.handleIncomingEvents(events);
1421			}
1422		}
1423
1424		handleRpcIncomingMessage(messageFields)
1425		{
1426			this.session.mid = messageFields.mid;
1427			let body = messageFields.body;
1428
1429			if (!messageFields.body.extra)
1430			{
1431				body.extra = {};
1432			}
1433			body.extra.sender = messageFields.sender;
1434
1435			if ("user_params" in messageFields && Utils.isPlainObject(messageFields.user_params))
1436			{
1437				Object.assign(body.params, messageFields.user_params)
1438			}
1439
1440			if ("dictionary" in messageFields && Utils.isPlainObject(messageFields.dictionary))
1441			{
1442				Object.assign(body.params, messageFields.dictionary)
1443			}
1444
1445			if (this.checkDuplicate(messageFields.mid))
1446			{
1447				this.addMessageToStat(body);
1448				this.trimDuplicates();
1449				this.broadcastMessage(body)
1450			}
1451
1452			this.connector.send(`mack:${messageFields.mid}`)
1453
1454			return {};
1455		}
1456
1457		onJsonRpcPing()
1458		{
1459			this.updatePingWaitTimeout();
1460			this.connector.send(JSON_RPC_PONG)
1461		}
1462
1463		handleIncomingEvents(events)
1464		{
1465			let messages = [];
1466			if (events.length === 0)
1467			{
1468				this.session.mid = null;
1469				return;
1470			}
1471
1472			for (let i = 0; i < events.length; i++)
1473			{
1474				let event = events[i];
1475				this.updateSessionFromEvent(event);
1476				if (event.mid && !this.checkDuplicate(event.mid))
1477				{
1478					continue;
1479				}
1480
1481				this.addMessageToStat(event.text);
1482				messages.push(event.text);
1483			}
1484			this.trimDuplicates();
1485			this.broadcastMessages(messages);
1486		}
1487
1488		updateSessionFromEvent(event)
1489		{
1490			this.session.mid = event.mid || null;
1491			this.session.tag = event.tag || null;
1492			this.session.time = event.time || null;
1493		}
1494
1495		checkDuplicate(mid)
1496		{
1497			if (this.session.lastMessageIds.includes(mid))
1498			{
1499				console.warn("Duplicate message " + mid + " skipped");
1500				return false;
1501			}
1502			else
1503			{
1504				this.session.lastMessageIds.push(mid);
1505				return true;
1506			}
1507		}
1508
1509		trimDuplicates()
1510		{
1511			if (this.session.lastMessageIds.length > MAX_IDS_TO_STORE)
1512			{
1513				this.session.lastMessageIds = this.session.lastMessageIds.slice(-MAX_IDS_TO_STORE);
1514			}
1515		}
1516
1517		addMessageToStat(message)
1518		{
1519			if (!this.session.history[message.module_id])
1520			{
1521				this.session.history[message.module_id] = {};
1522			}
1523			if (!this.session.history[message.module_id][message.command])
1524			{
1525				this.session.history[message.module_id][message.command] = 0;
1526			}
1527			this.session.history[message.module_id][message.command]++;
1528
1529			this.session.messageCount++;
1530		}
1531
1532		extractMessages(pullEvent)
1533		{
1534			if (pullEvent instanceof ArrayBuffer)
1535			{
1536				return this.extractProtobufMessages(pullEvent);
1537			}
1538			else if (Utils.isNotEmptyString(pullEvent))
1539			{
1540				return this.extractPlainTextMessages(pullEvent)
1541			}
1542		}
1543
1544		extractProtobufMessages(pullEvent)
1545		{
1546			let result = [];
1547			try
1548			{
1549				let responseBatch = ResponseBatch.decode(new Uint8Array(pullEvent));
1550				for (let i = 0; i < responseBatch.responses.length; i++)
1551				{
1552					let response = responseBatch.responses[i];
1553					if (response.command != "outgoingMessages")
1554					{
1555						continue;
1556					}
1557
1558					let messages = response.outgoingMessages.messages;
1559					for (let m = 0; m < messages.length; m++)
1560					{
1561						const message = messages[m];
1562						let messageFields;
1563						try
1564						{
1565							messageFields = JSON.parse(message.body)
1566						} catch (e)
1567						{
1568							console.error(Utils.getDateForLog() + ": Pull: Could not parse message body", e);
1569							continue;
1570						}
1571
1572						if (!messageFields.extra)
1573						{
1574							messageFields.extra = {}
1575						}
1576						messageFields.extra.sender = {
1577							type: message.sender.type
1578						};
1579
1580						if (message.sender.id instanceof Uint8Array)
1581						{
1582							messageFields.extra.sender.id = this.decodeId(message.sender.id)
1583						}
1584
1585						const compatibleMessage = {
1586							mid: this.decodeId(message.id),
1587							text: messageFields
1588						};
1589
1590						result.push(compatibleMessage);
1591					}
1592				}
1593			} catch (e)
1594			{
1595				console.error(Utils.getDateForLog() + ": Pull: Could not parse message", e)
1596			}
1597			return result;
1598		}
1599
1600		extractPlainTextMessages(pullEvent)
1601		{
1602			let result = [];
1603			const dataArray = pullEvent.match(/#!NGINXNMS!#(.*?)#!NGINXNME!#/gm);
1604			if (dataArray === null)
1605			{
1606				const text = "\n========= PULL ERROR ===========\n" +
1607					"Error type: parseResponse error parsing message\n" +
1608					"\n" +
1609					"Data string: " + pullEvent + "\n" +
1610					"================================\n\n";
1611				console.warn(text);
1612				return result;
1613			}
1614			for (let i = 0; i < dataArray.length; i++)
1615			{
1616				dataArray[i] = dataArray[i].substring(12, dataArray[i].length - 12);
1617				if (dataArray[i].length <= 0)
1618				{
1619					continue;
1620				}
1621
1622				let data
1623				try
1624				{
1625					data = JSON.parse(dataArray[i])
1626				} catch (e)
1627				{
1628					continue;
1629				}
1630
1631				result.push(data);
1632			}
1633			return result;
1634		}
1635
1636		/**
1637		 * Converts message id from byte[] to string
1638		 * @param {Uint8Array} encodedId
1639		 * @return {string}
1640		 */
1641		decodeId(encodedId)
1642		{
1643			if (!(encodedId instanceof Uint8Array))
1644			{
1645				throw new Error("encodedId should be an instance of Uint8Array");
1646			}
1647
1648			let result = "";
1649			for (let i = 0; i < encodedId.length; i++)
1650			{
1651				const hexByte = encodedId[i].toString(16);
1652				if (hexByte.length === 1)
1653				{
1654					result += '0';
1655				}
1656				result += hexByte;
1657			}
1658			return result;
1659		}
1660
1661		/**
1662		 * Converts message id from hex-encoded string to byte[]
1663		 * @param {string} id Hex-encoded string.
1664		 * @return {Uint8Array}
1665		 */
1666		encodeId(id)
1667		{
1668			if (!id)
1669			{
1670				return new Uint8Array();
1671			}
1672
1673			let result = [];
1674			for (let i = 0; i < id.length; i += 2)
1675			{
1676				result.push(parseInt(id.substr(i, 2), 16));
1677			}
1678
1679			return new Uint8Array(result);
1680		}
1681
1682		broadcastMessages(messages)
1683		{
1684			messages.forEach(message => this.broadcastMessage(message));
1685		}
1686
1687		broadcastMessage(message)
1688		{
1689			const moduleId = message.module_id = message.module_id.toLowerCase();
1690			const command = message.command;
1691
1692			if (!message.extra)
1693			{
1694				message.extra = {};
1695			}
1696
1697			if (message.extra.server_time_unix)
1698			{
1699				message.extra.server_time_ago = ((Utils.getTimestamp() - (message.extra.server_time_unix * 1000)) / 1000) - (this.config.server.timeShift ? this.config.server.timeShift : 0);
1700				message.extra.server_time_ago = message.extra.server_time_ago > 0 ? message.extra.server_time_ago : 0;
1701			}
1702
1703			this.logMessage(message);
1704			try
1705			{
1706				if (message.extra.sender && message.extra.sender.type === SenderType.Client)
1707				{
1708					if (typeof BX.onCustomEvent !== 'undefined')
1709					{
1710						BX.onCustomEvent(window, 'onPullClientEvent-' + moduleId, [command, message.params, message.extra], true);
1711						BX.onCustomEvent(window, 'onPullClientEvent', [moduleId, command, message.params, message.extra], true);
1712					}
1713
1714					this.emit({
1715						type: SubscriptionType.Client,
1716						moduleId: moduleId,
1717						data: {
1718							command: command,
1719							params: Utils.clone(message.params),
1720							extra: Utils.clone(message.extra)
1721						}
1722					});
1723				}
1724				else if (moduleId === 'pull')
1725				{
1726					this.handleInternalPullEvent(command, message);
1727				}
1728				else if (moduleId == 'online')
1729				{
1730					if (message.extra.server_time_ago < 240)
1731					{
1732						if (typeof BX.onCustomEvent !== 'undefined')
1733						{
1734							BX.onCustomEvent(window, 'onPullOnlineEvent', [command, message.params, message.extra], true);
1735						}
1736
1737						this.emit({
1738							type: SubscriptionType.Online,
1739							data: {
1740								command: command,
1741								params: Utils.clone(message.params),
1742								extra: Utils.clone(message.extra)
1743							}
1744						});
1745					}
1746
1747					if (command === 'userStatusChange')
1748					{
1749						this.emitUserStatusChange(message.params.user_id, message.params.online);
1750					}
1751				}
1752				else
1753				{
1754					if (typeof BX.onCustomEvent !== 'undefined')
1755					{
1756						BX.onCustomEvent(window, 'onPullEvent-' + moduleId, [command, message.params, message.extra], true);
1757						BX.onCustomEvent(window, 'onPullEvent', [moduleId, command, message.params, message.extra], true);
1758					}
1759
1760					this.emit({
1761						type: SubscriptionType.Server,
1762						moduleId: moduleId,
1763						data: {
1764							command: command,
1765							params: Utils.clone(message.params),
1766							extra: Utils.clone(message.extra)
1767						}
1768					});
1769				}
1770			} catch (e)
1771			{
1772				if (typeof (console) == 'object')
1773				{
1774					console.warn(
1775						"\n========= PULL ERROR ===========\n" +
1776						"Error type: broadcastMessages execute error\n" +
1777						"Error event: ", e, "\n" +
1778						"Message: ", message, "\n" +
1779						"================================\n"
1780					);
1781					if (typeof BX.debug !== 'undefined')
1782					{
1783						BX.debug(e);
1784					}
1785				}
1786			}
1787
1788			if (message.extra && message.extra.revision_web)
1789			{
1790				this.checkRevision(message.extra.revision_web);
1791			}
1792		}
1793
1794		logToConsole(message, force)
1795		{
1796			if (this.loggingEnabled || force)
1797			{
1798				console.log(Utils.getDateForLog() + ': ' + message);
1799			}
1800		}
1801
1802		logMessage(message)
1803		{
1804			if (!this.debug)
1805			{
1806				return;
1807			}
1808
1809			if (message.extra.sender && message.extra.sender.type === SenderType.Client)
1810			{
1811				console.info('onPullClientEvent-' + message.module_id, message.command, message.params, message.extra);
1812			}
1813			else if (message.moduleId == 'online')
1814			{
1815				console.info('onPullOnlineEvent', message.command, message.params, message.extra);
1816			}
1817			else
1818			{
1819				console.info('onPullEvent', message.module_id, message.command, message.params, message.extra);
1820			}
1821		}
1822
1823		onLongPollingOpen()
1824		{
1825			this.unloading = false;
1826			this.starting = false;
1827			this.connectionAttempt = 0;
1828			this.isManualDisconnect = false;
1829			this.status = PullStatus.Online;
1830
1831			this.logToConsole('Pull: Long polling connection with push-server opened');
1832			if (this.isWebSocketEnabled())
1833			{
1834				this.scheduleRestoreWebSocketConnection();
1835			}
1836			if (this._connectPromise)
1837			{
1838				this._connectPromise.resolve();
1839			}
1840		}
1841
1842		onWebSocketBlockChanged(e)
1843		{
1844			const isWebSocketBlocked = e.isWebSocketBlocked;
1845
1846			if (isWebSocketBlocked && this.connectionType === ConnectionType.WebSocket && !this.isConnected())
1847			{
1848				clearTimeout(this.reconnectTimeout);
1849
1850				this.connectionAttempt = 0;
1851				this.connectionType = ConnectionType.LongPolling;
1852				this.scheduleReconnect(1);
1853			}
1854			else if (!isWebSocketBlocked && this.connectionType === ConnectionType.LongPolling)
1855			{
1856				clearTimeout(this.reconnectTimeout);
1857				clearTimeout(this.restoreWebSocketTimeout);
1858
1859				this.connectionAttempt = 0;
1860				this.connectionType = ConnectionType.WebSocket;
1861				this.scheduleReconnect(1);
1862			}
1863		}
1864
1865		onWebSocketOpen()
1866		{
1867			this.unloading = false;
1868			this.starting = false;
1869			this.connectionAttempt = 0;
1870			this.isManualDisconnect = false;
1871			this.status = PullStatus.Online;
1872			this.sharedConfig.setWebSocketBlocked(false);
1873
1874			// to prevent fallback to long polling in case of networking problems
1875			this.sharedConfig.setLongPollingBlocked(true);
1876
1877			if (this.connectionType == ConnectionType.LongPolling)
1878			{
1879				this.connectionType = ConnectionType.WebSocket;
1880				this._connectors.longPolling.disconnect();
1881			}
1882
1883			if (this.restoreWebSocketTimeout)
1884			{
1885				clearTimeout(this.restoreWebSocketTimeout);
1886				this.restoreWebSocketTimeout = null;
1887			}
1888			this.logToConsole('Pull: Websocket connection with push-server opened');
1889			if (this._connectPromise)
1890			{
1891				this._connectPromise.resolve();
1892			}
1893			this.restoreUserStatusSubscription();
1894		}
1895
1896		onWebSocketDisconnect(e)
1897		{
1898			if (this.connectionType === ConnectionType.WebSocket)
1899			{
1900				this.status = PullStatus.Offline;
1901			}
1902
1903			if (!e)
1904			{
1905				e = {};
1906			}
1907
1908			this.logToConsole('Pull: Websocket connection with push-server closed. Code: ' + e.code + ', reason: ' + e.reason, true);
1909			if (!this.isManualDisconnect)
1910			{
1911				if (e.code == CloseReasons.WRONG_CHANNEL_ID)
1912				{
1913					this.scheduleRestart(CloseReasons.WRONG_CHANNEL_ID, "wrong channel signature");
1914				}
1915				else
1916				{
1917					this.scheduleReconnect();
1918				}
1919			}
1920
1921			// to prevent fallback to long polling in case of networking problems
1922			this.sharedConfig.setLongPollingBlocked(true);
1923			this.isManualDisconnect = false;
1924
1925			this.clearPingWaitTimeout();
1926		}
1927
1928		onWebSocketError(e)
1929		{
1930			this.starting = false;
1931			if (this.connectionType === ConnectionType.WebSocket)
1932			{
1933				this.status = PullStatus.Offline;
1934			}
1935
1936			console.error(Utils.getDateForLog() + ": Pull: WebSocket connection error", e);
1937			this.scheduleReconnect();
1938			if (this._connectPromise)
1939			{
1940				this._connectPromise.reject();
1941			}
1942
1943			this.clearPingWaitTimeout();
1944		}
1945
1946		onLongPollingDisconnect(e)
1947		{
1948			if (this.connectionType === ConnectionType.LongPolling)
1949			{
1950				this.status = PullStatus.Offline;
1951			}
1952
1953			if (!e)
1954			{
1955				e = {};
1956			}
1957
1958			this.logToConsole('Pull: Long polling connection with push-server closed. Code: ' + e.code + ', reason: ' + e.reason);
1959			if (!this.isManualDisconnect)
1960			{
1961				this.scheduleReconnect();
1962			}
1963			this.isManualDisconnect = false;
1964			this.clearPingWaitTimeout();
1965		}
1966
1967		onLongPollingError(e)
1968		{
1969			this.starting = false;
1970			if (this.connectionType === ConnectionType.LongPolling)
1971			{
1972				this.status = PullStatus.Offline;
1973			}
1974			console.error(Utils.getDateForLog() + ': Pull: Long polling connection error', e);
1975			this.scheduleReconnect();
1976			if (this._connectPromise)
1977			{
1978				this._connectPromise.reject();
1979			}
1980			this.clearPingWaitTimeout();
1981		}
1982
1983		isConnected()
1984		{
1985			return this.connector ? this.connector.connected : false;
1986		}
1987
1988		onBeforeUnload()
1989		{
1990			this.unloading = true;
1991
1992			const session = Utils.clone(this.session);
1993			session.ttl = (new Date()).getTime() + LS_SESSION_CACHE_TIME * 1000;
1994			if (this.storage)
1995			{
1996				try
1997				{
1998					this.storage.set(LS_SESSION, JSON.stringify(session), LS_SESSION_CACHE_TIME);
1999				} catch (e)
2000				{
2001					console.error(Utils.getDateForLog() + " Pull: Could not save session info in local storage. Error: ", e);
2002				}
2003			}
2004
2005			this.scheduleReconnect(15);
2006		}
2007
2008		onOffline()
2009		{
2010			this.disconnect("1000", "offline");
2011		}
2012
2013		onOnline()
2014		{
2015			this.connect();
2016		}
2017
2018		handleInternalPullEvent(command, message)
2019		{
2020			switch (command.toUpperCase())
2021			{
2022				case SystemCommands.CHANNEL_EXPIRE:
2023				{
2024					if (message.params.action == 'reconnect')
2025					{
2026						this.config.channels[message.params.channel.type] = message.params.new_channel;
2027						this.logToConsole("Pull: new config for " + message.params.channel.type + " channel set:\n", this.config.channels[message.params.channel.type]);
2028
2029						this.reconnect(CloseReasons.CONFIG_REPLACED, "config was replaced");
2030					}
2031					else
2032					{
2033						this.restart(CloseReasons.CHANNEL_EXPIRED, "channel expired received");
2034					}
2035					break;
2036				}
2037				case SystemCommands.CONFIG_EXPIRE:
2038				{
2039					this.restart(CloseReasons.CONFIG_EXPIRED, "config expired received");
2040					break;
2041				}
2042				case SystemCommands.SERVER_RESTART:
2043				{
2044					this.reconnect(CloseReasons.SERVER_RESTARTED, "server was restarted", 15);
2045					break;
2046				}
2047				default://
2048			}
2049		}
2050
2051		checkRevision(serverRevision)
2052		{
2053			if (this.skipCheckRevision)
2054			{
2055				return true;
2056			}
2057
2058			serverRevision = parseInt(serverRevision);
2059			if (serverRevision > 0 && serverRevision != REVISION)
2060			{
2061				this.enabled = false;
2062				if (typeof BX.message !== 'undefined')
2063				{
2064					this.showNotification(BX.message('PULL_OLD_REVISION'));
2065				}
2066				this.disconnect(CloseReasons.NORMAL_CLOSURE, 'check_revision');
2067
2068				if (typeof BX.onCustomEvent !== 'undefined')
2069				{
2070					BX.onCustomEvent(window, 'onPullRevisionUp', [serverRevision, REVISION]);
2071				}
2072
2073				this.emit({
2074					type: SubscriptionType.Revision,
2075					data: {
2076						server: serverRevision,
2077						client: REVISION
2078					}
2079				});
2080
2081				this.logToConsole("Pull revision changed from " + REVISION + " to " + serverRevision + ". Reload required");
2082
2083				return false;
2084			}
2085			return true;
2086		}
2087
2088		showNotification(text)
2089		{
2090			if (this.notificationPopup || typeof BX.PopupWindow === 'undefined')
2091			{
2092				return;
2093			}
2094
2095			this.notificationPopup = new BX.PopupWindow('bx-notifier-popup-confirm', null, {
2096				zIndex: 200,
2097				autoHide: false,
2098				closeByEsc: false,
2099				overlay: true,
2100				content: BX.create("div", {
2101					props: {className: "bx-messenger-confirm"},
2102					html: text
2103				}),
2104				buttons: [
2105					new BX.PopupWindowButton({
2106						text: BX.message('JS_CORE_WINDOW_CLOSE'),
2107						className: "popup-window-button-decline",
2108						events: {
2109							click: () => this.notificationPopup.close(),
2110						}
2111					})
2112				],
2113				events: {
2114					onPopupClose: () => this.notificationPopup.destroy(),
2115					onPopupDestroy: () => this.notificationPopup = null,
2116				}
2117			});
2118			this.notificationPopup.show();
2119		}
2120
2121		getRevision()
2122		{
2123			return (this.config && this.config.api) ? this.config.api.revision_web : null;
2124		}
2125
2126		getServerVersion()
2127		{
2128			return (this.config && this.config.server) ? this.config.server.version : 0;
2129		}
2130
2131		getServerMode()
2132		{
2133			return (this.config && this.config.server) ? this.config.server.mode : null;
2134		}
2135
2136		getConfig()
2137		{
2138			return this.config;
2139		}
2140
2141		getDebugInfo()
2142		{
2143			if (!JSON || !JSON.stringify)
2144			{
2145				return false;
2146			}
2147
2148			let configDump;
2149			if (this.config && this.config.channels)
2150			{
2151				configDump = {
2152					"ChannelID": (this.config.channels.private ? this.config.channels.private.id : "n/a"),
2153					"ChannelDie": (this.config.channels.private ? this.config.channels.private.end : "n/a"),
2154					"ChannelDieShared": ("shared" in this.config.channels ? this.config.channels.shared.end : "n/a"),
2155				};
2156			}
2157			else
2158			{
2159				configDump = {"Config error": "config is not loaded"}
2160			}
2161
2162			let websocketMode = "-";
2163			if (this._connectors.webSocket && this._connectors.webSocket.socket)
2164			{
2165				if (this.isJsonRpc())
2166				{
2167					websocketMode = "json-rpc"
2168				}
2169				else
2170				{
2171					websocketMode = (this._connectors.webSocket.socket.url.search("binaryMode=true") != -1 ? "protobuf" : "text")
2172				}
2173			}
2174
2175			return {
2176				"UserId": this.userId + (this.userId > 0 ? '' : '(guest)'),
2177				"Guest userId": (this.guestMode && this.guestUserId !== 0 ? this.guestUserId : "-"),
2178				"Browser online": (navigator.onLine ? 'Y' : 'N'),
2179				"Connect": (this.isConnected() ? 'Y' : 'N'),
2180				"Server type": (this.isSharedMode() ? 'cloud' : 'local'),
2181				"WebSocket supported": (this.isWebSocketSupported() ? 'Y' : 'N'),
2182				"WebSocket connected": (this._connectors.webSocket && this._connectors.webSocket.connected ? 'Y' : 'N'),
2183				"WebSocket mode": websocketMode,
2184
2185				"Try connect": (this.reconnectTimeout ? 'Y' : 'N'),
2186				"Try number": (this.connectionAttempt),
2187
2188				"Path": (this.connector ? this.connector.path : '-'),
2189				...configDump,
2190
2191				"Last message": (this.session.mid > 0 ? this.session.mid : '-'),
2192				"Session history": this.session.history,
2193				"Watch tags": this.watchTagsQueue,
2194			}
2195		}
2196
2197		enableLogging(loggingFlag)
2198		{
2199			if (loggingFlag === undefined)
2200			{
2201				loggingFlag = true;
2202			}
2203			loggingFlag = loggingFlag === true;
2204
2205			this.sharedConfig.setLoggingEnabled(loggingFlag);
2206			this.loggingEnabled = loggingFlag;
2207		}
2208
2209		capturePullEvent(debugFlag)
2210		{
2211			if (debugFlag === undefined)
2212			{
2213				debugFlag = true;
2214			}
2215
2216			this.debug = debugFlag;
2217		}
2218
2219		getConnectionPath(connectionType)
2220		{
2221			let path;
2222			let params = {};
2223
2224			switch (connectionType)
2225			{
2226				case ConnectionType.WebSocket:
2227					path = this.isSecure ? this.config.server.websocket_secure : this.config.server.websocket;
2228					break;
2229				case ConnectionType.LongPolling:
2230					path = this.isSecure ? this.config.server.long_pooling_secure : this.config.server.long_polling;
2231					break;
2232				default:
2233					throw new Error("Unknown connection type " + connectionType);
2234			}
2235
2236			if (!Utils.isNotEmptyString(path))
2237			{
2238				return false;
2239			}
2240
2241			if (typeof (this.config.jwt) == 'string' && this.config.jwt !== '')
2242			{
2243				params['token'] = this.config.jwt;
2244			}
2245			else
2246			{
2247				let channels = [];
2248				['private', 'shared'].forEach((type) => {
2249					if (typeof this.config.channels[type] !== 'undefined')
2250					{
2251						channels.push(this.config.channels[type].id);
2252					}
2253				});
2254				if (channels.length === 0)
2255				{
2256					return false;
2257				}
2258
2259				params['CHANNEL_ID'] = channels.join('/');
2260			}
2261
2262			if (this.isJsonRpc())
2263			{
2264				params.jsonRpc = 'true';
2265			}
2266			else if (this.isProtobufSupported())
2267			{
2268				params.binaryMode = 'true';
2269			}
2270
2271			if (this.isSharedMode())
2272			{
2273				if (!this.config.clientId)
2274				{
2275					throw new Error("Push-server is in shared mode, but clientId is not set");
2276				}
2277				params.clientId = this.config.clientId;
2278			}
2279			if (this.session.mid)
2280			{
2281				params.mid = this.session.mid;
2282			}
2283			if (this.session.tag)
2284			{
2285				params.tag = this.session.tag;
2286			}
2287			if (this.session.time)
2288			{
2289				params.time = this.session.time;
2290			}
2291			params.revision = REVISION;
2292
2293			return path + '?' + Utils.buildQueryString(params);
2294		}
2295
2296		getPublicationPath()
2297		{
2298			const path = this.isSecure ? this.config.server.publish_secure : this.config.server.publish;
2299			if (!path)
2300			{
2301				return '';
2302			}
2303
2304			let channels = [];
2305			for (let type in this.config.channels)
2306			{
2307				if (!this.config.channels.hasOwnProperty(type))
2308				{
2309					continue;
2310				}
2311				channels.push(this.config.channels[type].id);
2312			}
2313
2314			const params = {
2315				CHANNEL_ID: channels.join('/')
2316			};
2317
2318			return path + '?' + Utils.buildQueryString(params);
2319		}
2320
2321		/**
2322		 * Returns reconnect delay in seconds
2323		 * @param attemptNumber
2324		 * @return {number}
2325		 */
2326		getConnectionAttemptDelay(attemptNumber)
2327		{
2328			let result;
2329			if (attemptNumber < 1)
2330			{
2331				result = 0.5;
2332			}
2333			else if (attemptNumber < 3)
2334			{
2335				result = 15;
2336			}
2337			else if (attemptNumber < 5)
2338			{
2339				result = 45;
2340			}
2341			else if (attemptNumber < 10)
2342			{
2343				result = 600;
2344			}
2345			else
2346			{
2347				result = 3600;
2348			}
2349
2350			return result + (result * Math.random() * 0.2);
2351		}
2352
2353		sendPullStatusDelayed(status, delay)
2354		{
2355			if (this.offlineTimeout)
2356			{
2357				clearTimeout(this.offlineTimeout)
2358			}
2359			this.offlineTimeout = setTimeout(
2360				() => {
2361					this.offlineTimeout = null;
2362					this.sendPullStatus(status);
2363				},
2364				delay
2365			)
2366		}
2367
2368		sendPullStatus(status)
2369		{
2370			if (this.unloading)
2371			{
2372				return;
2373			}
2374
2375			if (typeof BX.onCustomEvent !== 'undefined')
2376			{
2377				BX.onCustomEvent(window, 'onPullStatus', [status]);
2378			}
2379
2380			this.emit({
2381				type: SubscriptionType.Status,
2382				data: {
2383					status: status
2384				}
2385			});
2386		}
2387
2388		extendWatch(tag, force)
2389		{
2390			if (!tag || this.watchTagsQueue[tag])
2391			{
2392				return false;
2393			}
2394
2395			this.watchTagsQueue[tag] = true;
2396			if (force)
2397			{
2398				this.updateWatch(force);
2399			}
2400		}
2401
2402		updateWatch(force)
2403		{
2404			clearTimeout(this.watchUpdateTimeout);
2405			this.watchUpdateTimeout = setTimeout(() => {
2406				const watchTags = Object.keys(this.watchTagsQueue);
2407				if (watchTags.length > 0)
2408				{
2409					this.restClient.callMethod('pull.watch.extend', {tags: watchTags}, (result) => {
2410						if (result.error())
2411						{
2412							this.updateWatch();
2413
2414							return false;
2415						}
2416
2417						const updatedTags = result.data();
2418
2419						for (let tagId in updatedTags)
2420						{
2421							if (updatedTags.hasOwnProperty(tagId) && !updatedTags[tagId])
2422							{
2423								this.clearWatch(tagId);
2424							}
2425						}
2426						this.updateWatch();
2427					})
2428				}
2429				else
2430				{
2431					this.updateWatch();
2432				}
2433			}, force ? this.watchForceUpdateInterval : this.watchUpdateInterval);
2434		}
2435
2436		clearWatch(tagId)
2437		{
2438			delete this.watchTagsQueue[tagId];
2439		}
2440
2441		updatePingWaitTimeout()
2442		{
2443			clearTimeout(this.pingWaitTimeout);
2444			this.pingWaitTimeout = setTimeout(this.onPingTimeoutHandler, PING_TIMEOUT * 2 * 1000)
2445		}
2446
2447		clearPingWaitTimeout()
2448		{
2449			clearTimeout(this.pingWaitTimeout);
2450			this.pingWaitTimeout = null;
2451		}
2452
2453		onPingTimeout()
2454		{
2455			this.pingWaitTimeout = null;
2456			if (!this.enabled || !this.isConnected())
2457			{
2458				return;
2459			}
2460
2461			console.warn("No pings are received in " + PING_TIMEOUT * 2 + " seconds. Reconnecting")
2462			this.disconnect(CloseReasons.STUCK, "connection stuck");
2463			this.scheduleReconnect();
2464		}
2465
2466		// old functions, not used anymore.
2467		setPrivateVar() {}
2468
2469		returnPrivateVar() {}
2470
2471		expireConfig() {}
2472
2473		updateChannelID() {}
2474
2475		tryConnect() {}
2476
2477		tryConnectDelay() {}
2478
2479		tryConnectSet() {}
2480
2481		updateState() {}
2482
2483		setUpdateStateStepCount() {}
2484
2485		supportWebSocket()
2486		{
2487			return this.isWebSocketSupported();
2488		}
2489
2490		isWebSoketConnected()
2491		{
2492			return this.isConnected() && this.connectionType == ConnectionType.WebSocket;
2493		}
2494
2495		getPullServerStatus() {return this.isConnected()}
2496
2497		closeConfirm()
2498		{
2499			if (this.notificationPopup)
2500			{
2501				this.notificationPopup.destroy();
2502			}
2503		}
2504	}
2505
2506	class SharedConfig
2507	{
2508		constructor(params)
2509		{
2510			params = params || {};
2511			this.storage = params.storage || new StorageManager();
2512
2513			this.ttl = 24 * 60 * 60;
2514
2515			this.lsKeys = {
2516				websocketBlocked: 'bx-pull-websocket-blocked',
2517				longPollingBlocked: 'bx-pull-longpolling-blocked',
2518				loggingEnabled: 'bx-pull-logging-enabled'
2519			};
2520
2521			this.callbacks = {
2522				onWebSocketBlockChanged: (Utils.isFunction(params.onWebSocketBlockChanged) ? params.onWebSocketBlockChanged : function () {})
2523			};
2524
2525			if (this.storage)
2526			{
2527				window.addEventListener('storage', this.onLocalStorageSet.bind(this));
2528			}
2529		}
2530
2531		onLocalStorageSet(params)
2532		{
2533			if (
2534				this.storage.compareKey(params.key, this.lsKeys.websocketBlocked)
2535				&& params.newValue != params.oldValue
2536			)
2537			{
2538				this.callbacks.onWebSocketBlockChanged({
2539					isWebSocketBlocked: this.isWebSocketBlocked()
2540				})
2541			}
2542		}
2543
2544		isWebSocketBlocked()
2545		{
2546			if (!this.storage)
2547			{
2548				return false;
2549			}
2550
2551			return this.storage.get(this.lsKeys.websocketBlocked, 0) > Utils.getTimestamp();
2552		}
2553
2554		setWebSocketBlocked(isWebSocketBlocked)
2555		{
2556			if (!this.storage)
2557			{
2558				return false;
2559			}
2560
2561			try
2562			{
2563				this.storage.set(this.lsKeys.websocketBlocked, (isWebSocketBlocked ? Utils.getTimestamp() + this.ttl : 0));
2564			} catch (e)
2565			{
2566				console.error(Utils.getDateForLog() + " Pull: Could not save WS_blocked flag in local storage. Error: ", e);
2567			}
2568		}
2569
2570		isLongPollingBlocked()
2571		{
2572			if (!this.storage)
2573			{
2574				return false;
2575			}
2576
2577			return this.storage.get(this.lsKeys.longPollingBlocked, 0) > Utils.getTimestamp();
2578		}
2579
2580		setLongPollingBlocked(isLongPollingBlocked)
2581		{
2582			if (!this.storage)
2583			{
2584				return false;
2585			}
2586
2587			try
2588			{
2589				this.storage.set(this.lsKeys.longPollingBlocked, (isLongPollingBlocked ? Utils.getTimestamp() + this.ttl : 0));
2590			} catch (e)
2591			{
2592				console.error(Utils.getDateForLog() + " Pull: Could not save LP_blocked flag in local storage. Error: ", e);
2593			}
2594		}
2595
2596		isLoggingEnabled()
2597		{
2598			if (!this.storage)
2599			{
2600				return false;
2601			}
2602
2603			return this.storage.get(this.lsKeys.loggingEnabled, 0) > Utils.getTimestamp();
2604		}
2605
2606		setLoggingEnabled(isLoggingEnabled)
2607		{
2608			if (!this.storage)
2609			{
2610				return false;
2611			}
2612
2613			try
2614			{
2615				this.storage.set(this.lsKeys.loggingEnabled, (isLoggingEnabled ? Utils.getTimestamp() + this.ttl : 0));
2616			} catch (e)
2617			{
2618				console.error("LocalStorage error: ", e);
2619				return false;
2620			}
2621		}
2622	}
2623
2624	class AbstractConnector
2625	{
2626		_connected = false;
2627		connectionType = "";
2628
2629		disconnectCode = '';
2630		disconnectReason = '';
2631
2632		constructor(config)
2633		{
2634			this.parent = config.parent;
2635			this.callbacks = {
2636				onOpen: Utils.isFunction(config.onOpen) ? config.onOpen : function () {},
2637				onDisconnect: Utils.isFunction(config.onDisconnect) ? config.onDisconnect : function () {},
2638				onError: Utils.isFunction(config.onError) ? config.onError : function () {},
2639				onMessage: Utils.isFunction(config.onMessage) ? config.onMessage : function () {}
2640			};
2641		}
2642
2643		get connected()
2644		{
2645			return this._connected
2646		}
2647
2648		set connected(value)
2649		{
2650			if (value == this._connected)
2651			{
2652				return;
2653			}
2654
2655			this._connected = value;
2656
2657			if (this._connected)
2658			{
2659				this.callbacks.onOpen();
2660			}
2661			else
2662			{
2663				this.callbacks.onDisconnect({
2664					code: this.disconnectCode,
2665					reason: this.disconnectReason
2666				});
2667			}
2668		}
2669
2670		get path()
2671		{
2672			return this.parent.getConnectionPath(this.connectionType);
2673		}
2674	}
2675
2676	class WebSocketConnector extends AbstractConnector
2677	{
2678		constructor(config)
2679		{
2680			super(config)
2681			this.connectionType = ConnectionType.WebSocket;
2682			this.socket = null;
2683
2684			this.onSocketOpenHandler = this.onSocketOpen.bind(this);
2685			this.onSocketCloseHandler = this.onSocketClose.bind(this);
2686			this.onSocketErrorHandler = this.onSocketError.bind(this);
2687			this.onSocketMessageHandler = this.onSocketMessage.bind(this);
2688		}
2689
2690		connect()
2691		{
2692			if (this.socket)
2693			{
2694				if (this.socket.readyState === 1)
2695				{
2696					// already connected
2697					return true;
2698				}
2699				else
2700				{
2701					this.socket.removeEventListener('open', this.onSocketOpenHandler);
2702					this.socket.removeEventListener('close', this.onSocketCloseHandler);
2703					this.socket.removeEventListener('error', this.onSocketErrorHandler);
2704					this.socket.removeEventListener('message', this.onSocketMessageHandler);
2705
2706					this.socket.close();
2707					this.socket = null;
2708				}
2709			}
2710
2711			this.createSocket();
2712		}
2713
2714		disconnect(code, message)
2715		{
2716			if (this.socket !== null)
2717			{
2718				this.socket.removeEventListener('open', this.onSocketOpenHandler);
2719				this.socket.removeEventListener('close', this.onSocketCloseHandler);
2720				this.socket.removeEventListener('error', this.onSocketErrorHandler);
2721				this.socket.removeEventListener('message', this.onSocketMessageHandler);
2722
2723				this.socket.close(code, message);
2724			}
2725			this.socket = null;
2726			this.disconnectCode = code;
2727			this.disconnectReason = message;
2728			this.connected = false;
2729		}
2730
2731		createSocket()
2732		{
2733			if (this.socket)
2734			{
2735				throw new Error("Socket already exists");
2736			}
2737
2738			if (!this.path)
2739			{
2740				throw new Error("Websocket connection path is not defined");
2741			}
2742
2743			this.socket = new WebSocket(this.path);
2744			this.socket.binaryType = 'arraybuffer';
2745
2746			this.socket.addEventListener('open', this.onSocketOpenHandler);
2747			this.socket.addEventListener('close', this.onSocketCloseHandler);
2748			this.socket.addEventListener('error', this.onSocketErrorHandler);
2749			this.socket.addEventListener('message', this.onSocketMessageHandler);
2750		}
2751
2752		/**
2753		 * Sends some data to the server via websocket connection.
2754		 * @param {ArrayBuffer} buffer Data to send.
2755		 * @return {boolean}
2756		 */
2757		send(buffer)
2758		{
2759			if (!this.socket || this.socket.readyState !== 1)
2760			{
2761				console.error(Utils.getDateForLog() + ": Pull: WebSocket is not connected");
2762				return false;
2763			}
2764
2765			this.socket.send(buffer);
2766			return true;
2767		}
2768
2769		onSocketOpen()
2770		{
2771			this.connected = true;
2772		}
2773
2774		onSocketClose(e)
2775		{
2776			this.socket = null;
2777			this.disconnectCode = e.code;
2778			this.disconnectReason = e.reason;
2779			this.connected = false;
2780		}
2781
2782		onSocketError(e)
2783		{
2784			this.callbacks.onError(e);
2785		}
2786
2787		onSocketMessage(e)
2788		{
2789			this.callbacks.onMessage(e.data);
2790		}
2791
2792		destroy()
2793		{
2794			if (this.socket)
2795			{
2796				this.socket.close();
2797				this.socket = null;
2798			}
2799		}
2800	}
2801
2802	class LongPollingConnector extends AbstractConnector
2803	{
2804		constructor(config)
2805		{
2806			super(config);
2807
2808			this.active = false;
2809			this.connectionType = ConnectionType.LongPolling;
2810			this.requestTimeout = null;
2811			this.failureTimeout = null;
2812			this.xhr = this.createXhr();
2813			this.requestAborted = false;
2814		}
2815
2816		createXhr()
2817		{
2818			const result = new XMLHttpRequest();
2819			if (this.parent.isProtobufSupported() && !this.parent.isJsonRpc())
2820			{
2821				result.responseType = "arraybuffer";
2822			}
2823			result.addEventListener("readystatechange", this.onXhrReadyStateChange.bind(this));
2824			return result;
2825		}
2826
2827		connect()
2828		{
2829			this.active = true;
2830			this.performRequest();
2831		}
2832
2833		disconnect(code, reason)
2834		{
2835			this.active = false;
2836
2837			if (this.failureTimeout)
2838			{
2839				clearTimeout(this.failureTimeout);
2840				this.failureTimeout = null;
2841			}
2842			if (this.requestTimeout)
2843			{
2844				clearTimeout(this.requestTimeout);
2845				this.requestTimeout = null;
2846			}
2847
2848			if (this.xhr)
2849			{
2850				this.requestAborted = true;
2851				this.xhr.abort();
2852			}
2853
2854			this.disconnectCode = code;
2855			this.disconnectReason = reason;
2856			this.connected = false;
2857		}
2858
2859		performRequest()
2860		{
2861			if (!this.active)
2862			{
2863				return;
2864			}
2865
2866			if (!this.path)
2867			{
2868				throw new Error("Long polling connection path is not defined");
2869			}
2870			if (this.xhr.readyState !== 0 && this.xhr.readyState !== 4)
2871			{
2872				return;
2873			}
2874
2875			clearTimeout(this.failureTimeout);
2876			clearTimeout(this.requestTimeout);
2877
2878			this.failureTimeout = setTimeout(() => { this.connected = true }, 5000);
2879			this.requestTimeout = setTimeout(this.onRequestTimeout.bind(this), LONG_POLLING_TIMEOUT * 1000);
2880
2881			this.xhr.open("GET", this.path);
2882			this.xhr.send();
2883		}
2884
2885		onRequestTimeout()
2886		{
2887			this.requestAborted = true;
2888			this.xhr.abort();
2889			this.performRequest();
2890		}
2891
2892		onXhrReadyStateChange()
2893		{
2894			if (this.xhr.readyState === 4)
2895			{
2896				if (!this.requestAborted || this.xhr.status == 200)
2897				{
2898					this.onResponse(this.xhr.response);
2899				}
2900				this.requestAborted = false;
2901			}
2902		}
2903
2904		/**
2905		 * Sends some data to the server via http request.
2906		 * @param {ArrayBuffer} buffer Data to send.
2907		 * @return {bool}
2908		 */
2909		send(buffer)
2910		{
2911			const path = this.parent.getPublicationPath();
2912			if (!path)
2913			{
2914				console.error(Utils.getDateForLog() + ": Pull: publication path is empty");
2915				return false;
2916			}
2917
2918			let xhr = new XMLHttpRequest();
2919			xhr.open("POST", path);
2920			xhr.send(buffer);
2921		}
2922
2923		onResponse(response)
2924		{
2925			if (this.failureTimeout)
2926			{
2927				clearTimeout(this.failureTimeout);
2928				this.failureTimeout = 0;
2929			}
2930			if (this.requestTimeout)
2931			{
2932				clearTimeout(this.requestTimeout);
2933				this.requestTimeout = 0;
2934			}
2935
2936			if (this.xhr.status == 200)
2937			{
2938				this.connected = true;
2939				if (Utils.isNotEmptyString(response) || (response instanceof ArrayBuffer))
2940				{
2941					this.callbacks.onMessage(response);
2942				}
2943				else
2944				{
2945					this.parent.session.mid = null;
2946				}
2947				this.performRequest();
2948			}
2949			else if (this.xhr.status == 304)
2950			{
2951				this.connected = true;
2952				if (this.xhr.getResponseHeader("Expires") === "Thu, 01 Jan 1973 11:11:01 GMT")
2953				{
2954					const lastMessageId = this.xhr.getResponseHeader("Last-Message-Id");
2955					if (Utils.isNotEmptyString(lastMessageId))
2956					{
2957						this.parent.setLastMessageId(lastMessageId);
2958					}
2959				}
2960				this.performRequest();
2961			}
2962			else
2963			{
2964				this.callbacks.onError('Could not connect to the server');
2965				this.connected = false;
2966			}
2967		}
2968	}
2969
2970	class ChannelManager
2971	{
2972		constructor(params)
2973		{
2974			this.publicIds = {};
2975
2976			this.restClient = typeof params.restClient !== "undefined" ? params.restClient : BX.rest;
2977
2978			this.getPublicListMethod = params.getPublicListMethod;
2979		}
2980
2981		/**
2982		 *
2983		 * @param {Array} users Array of user ids.
2984		 * @return {Promise}
2985		 */
2986		getPublicIds(users)
2987		{
2988			const now = new Date();
2989			let result = {};
2990			let unknownUsers = [];
2991
2992			for (let i = 0; i < users.length; i++)
2993			{
2994				const userId = users[i];
2995				if (this.publicIds[userId] && this.publicIds[userId]['end'] > now)
2996				{
2997					result[userId] = this.publicIds[userId];
2998				}
2999				else
3000				{
3001					unknownUsers.push(userId);
3002				}
3003			}
3004
3005			if (unknownUsers.length === 0)
3006			{
3007				return Promise.resolve(result);
3008			}
3009
3010			return new Promise((resolve) => {
3011				this.restClient.callMethod(this.getPublicListMethod, {users: unknownUsers}).then((response) => {
3012					if (response.error())
3013					{
3014						return resolve({});
3015					}
3016
3017					const data = response.data();
3018					this.setPublicIds(Utils.objectValues(data));
3019					unknownUsers.forEach((userId) => {
3020						result[userId] = this.publicIds[userId];
3021					});
3022
3023					resolve(result);
3024				});
3025			})
3026		}
3027
3028		/**
3029		 *
3030		 * @param {object[]} publicIds
3031		 * @param {integer} publicIds.user_id
3032		 * @param {string} publicIds.public_id
3033		 * @param {string} publicIds.signature
3034		 * @param {Date} publicIds.start
3035		 * @param {Date} publicIds.end
3036		 */
3037		setPublicIds(publicIds)
3038		{
3039			for (let i = 0; i < publicIds.length; i++)
3040			{
3041				const publicIdDescriptor = publicIds[i];
3042				const userId = publicIdDescriptor.user_id;
3043				this.publicIds[userId] = {
3044					userId: userId,
3045					publicId: publicIdDescriptor.public_id,
3046					signature: publicIdDescriptor.signature,
3047					start: new Date(publicIdDescriptor.start),
3048					end: new Date(publicIdDescriptor.end)
3049				}
3050			}
3051		};
3052	}
3053
3054	class StorageManager
3055	{
3056		constructor(params)
3057		{
3058			params = params || {};
3059
3060			this.userId = params.userId ? params.userId : (typeof BX.message !== 'undefined' && BX.message.USER_ID ? BX.message.USER_ID : 0);
3061			this.siteId = params.siteId ? params.siteId : (typeof BX.message !== 'undefined' && BX.message.SITE_ID ? BX.message.SITE_ID : 'none');
3062		}
3063
3064		set(name, value)
3065		{
3066			if (typeof window.localStorage === 'undefined')
3067			{
3068				return false;
3069			}
3070			if (typeof value != 'string')
3071			{
3072				if (value)
3073				{
3074					value = JSON.stringify(value);
3075				}
3076			}
3077			return window.localStorage.setItem(this.getKey(name), value)
3078		}
3079
3080		get(name, defaultValue)
3081		{
3082			if (typeof window.localStorage === 'undefined')
3083			{
3084				return defaultValue || null;
3085			}
3086
3087			const result = window.localStorage.getItem(this.getKey(name));
3088			if (result === null)
3089			{
3090				return defaultValue || null;
3091			}
3092
3093			return JSON.parse(result);
3094		}
3095
3096		remove(name)
3097		{
3098			if (typeof window.localStorage === 'undefined')
3099			{
3100				return false;
3101			}
3102			return window.localStorage.removeItem(this.getKey(name));
3103		}
3104
3105		getKey(name)
3106		{
3107			return 'bx-pull-' + this.userId + '-' + this.siteId + '-' + name;
3108		}
3109
3110		compareKey(eventKey, userKey)
3111		{
3112			return eventKey === this.getKey(userKey);
3113		}
3114	}
3115
3116	class JsonRpc
3117	{
3118		idCounter = 0;
3119
3120		handlers = {};
3121		rpcResponseAwaiters = new Map();
3122
3123		constructor(options)
3124		{
3125			this.connector = options.connector;
3126			if (Utils.isPlainObject(options.handlers))
3127			{
3128				for (let method in options.handlers)
3129				{
3130					this.handle(method, options.handlers[method]);
3131				}
3132			}
3133		}
3134
3135		/**
3136		 * @param {string} method
3137		 * @param {function} handler
3138		 */
3139		handle(method, handler)
3140		{
3141			this.handlers[method] = handler;
3142		}
3143
3144		/**
3145		 * Sends RPC command to the server.
3146		 *
3147		 * @param {string} method Method name
3148		 * @param {object} params
3149		 * @param {int} timeout
3150		 * @returns {Promise}
3151		 */
3152		executeOutgoingRpcCommand(method, params, timeout)
3153		{
3154			if (!timeout)
3155			{
3156				timeout = 5;
3157			}
3158			return new Promise((resolve, reject) => {
3159				const request = this.createRequest(method, params);
3160
3161				if (!this.connector.send(JSON.stringify(request)))
3162				{
3163					reject(new ErrorNotConnected('websocket is not connected'));
3164				}
3165
3166				const t = setTimeout(() => {
3167					this.rpcResponseAwaiters.delete(request.id);
3168					reject(new ErrorTimeout('no response'));
3169				}, timeout * 1000);
3170				this.rpcResponseAwaiters.set(request.id, {resolve, reject, timeout: t});
3171			})
3172		}
3173
3174		/**
3175		 * Executes array or rpc commands. Returns array of promises, each promise will be resolved individually.
3176		 *
3177		 * @param {JsonRpcRequest[]} batch
3178		 * @returns {Promise[]}
3179		 */
3180		executeOutgoingRpcBatch(batch)
3181		{
3182			let requests = [];
3183			let promises = [];
3184			batch.forEach(({method, params, id}) => {
3185				const request = this.createRequest(method, params, id);
3186				requests.push(request);
3187				promises.push(new Promise((resolve, reject) => this.rpcResponseAwaiters.set(request.id, {
3188					resolve,
3189					reject
3190				})));
3191			});
3192
3193			this.connector.send(JSON.stringify(requests));
3194			return promises;
3195		}
3196
3197		processRpcResponse(response)
3198		{
3199			if ("id" in response && this.rpcResponseAwaiters.has(response.id))
3200			{
3201				const awaiter = this.rpcResponseAwaiters.get(response.id)
3202				if ("result" in response)
3203				{
3204					awaiter.resolve(response.result)
3205				}
3206				else if ("error" in response)
3207				{
3208					awaiter.reject(response.error)
3209				}
3210				else
3211				{
3212					awaiter.reject(new Error("wrong response structure"))
3213				}
3214
3215				clearTimeout(awaiter.timeout)
3216				this.rpcResponseAwaiters.delete(response.id)
3217			}
3218			else
3219			{
3220				console.error("Received rpc response with unknown id", response)
3221			}
3222		}
3223
3224		parseJsonRpcMessage(message)
3225		{
3226			let decoded
3227			try
3228			{
3229				decoded = JSON.parse(message);
3230			} catch (e)
3231			{
3232				console.error(Utils.getDateForLog() + ": Pull: Could not decode json rpc message", e);
3233			}
3234
3235			if (Utils.isArray(decoded))
3236			{
3237				return this.executeIncomingRpcBatch(decoded);
3238			}
3239			else if (Utils.isJsonRpcRequest(decoded))
3240			{
3241				return this.executeIncomingRpcCommand(decoded);
3242			}
3243			else if (Utils.isJsonRpcResponse(decoded))
3244			{
3245				return this.processRpcResponse(decoded);
3246			}
3247			else
3248			{
3249				console.error(Utils.getDateForLog() + ": Pull: unknown rpc packet", decoded);
3250			}
3251		}
3252
3253		/**
3254		 * Executes RPC command, received from the server
3255		 *
3256		 * @param {string} method
3257		 * @param {object} params
3258		 * @returns {object}
3259		 */
3260		executeIncomingRpcCommand({method, params})
3261		{
3262			if (method in this.handlers)
3263			{
3264				return this.handlers[method].call(this, params)
3265			}
3266
3267			return {
3268				"error": RpcError.MethodNotFound
3269			}
3270		}
3271
3272		executeIncomingRpcBatch(batch)
3273		{
3274			let result = [];
3275			for (let command of batch)
3276			{
3277				if ("jsonrpc" in command)
3278				{
3279					if ("method" in command)
3280					{
3281						let commandResult = this.executeIncomingRpcCommand(command)
3282						if (commandResult)
3283						{
3284							commandResult["jsonrpc"] = JSON_RPC_VERSION;
3285							commandResult["id"] = command["id"];
3286
3287							result.push(commandResult)
3288						}
3289					}
3290					else
3291					{
3292						this.processRpcResponse(command)
3293					}
3294				}
3295				else
3296				{
3297					console.error(Utils.getDateForLog() + ": Pull: unknown rpc command in batch", command);
3298					result.push({
3299						"jsonrpc": "2.0",
3300						"error": RpcError.InvalidRequest,
3301					})
3302				}
3303			}
3304
3305			return result;
3306		}
3307
3308		nextId()
3309		{
3310			return ++this.idCounter;
3311		}
3312
3313		createPublishRequest(messageBatch)
3314		{
3315			let result = messageBatch.map(message => this.createRequest('publish', message));
3316
3317			if (result.length === 0)
3318			{
3319				return result[0]
3320			}
3321
3322			return result;
3323		}
3324
3325		createRequest(method, params, id)
3326		{
3327			if (!id)
3328			{
3329				id = this.nextId()
3330			}
3331
3332			return {
3333				jsonrpc: JSON_RPC_VERSION,
3334				method: method,
3335				params: params,
3336				id: id
3337			}
3338		}
3339	}
3340
3341	class ErrorNotConnected extends Error
3342	{
3343		constructor(message)
3344		{
3345			super(message);
3346			this.name = 'ErrorNotConnected';
3347		}
3348	}
3349
3350	class ErrorTimeout extends Error
3351	{
3352		constructor(message)
3353		{
3354			super(message);
3355			this.name = 'ErrorTimeout';
3356		}
3357	}
3358
3359	const Utils = {
3360		browser: {
3361			IsChrome: function () {
3362				return navigator.userAgent.toLowerCase().indexOf('chrome') != -1;
3363			},
3364			IsFirefox: function () {
3365				return navigator.userAgent.toLowerCase().indexOf('firefox') != -1;
3366			},
3367			IsIe: function () {
3368				return navigator.userAgent.match(/(Trident\/|MSIE\/)/) !== null;
3369			}
3370		},
3371		getTimestamp: function () {
3372			return (new Date()).getTime();
3373		},
3374		/**
3375		 * Reduces errors array to single string.
3376		 * @param {array} errors
3377		 * @return {string}
3378		 */
3379		errorsToString: function (errors) {
3380			if (!this.isArray(errors))
3381			{
3382				return "";
3383			}
3384			else
3385			{
3386				return errors.reduce(function (result, currentValue) {
3387					if (result != "")
3388					{
3389						result += "; ";
3390					}
3391					return result + currentValue.code + ": " + currentValue.message;
3392				}, "");
3393			}
3394		},
3395		isString: function (item) {
3396			return item === '' ? true : (item ? (typeof (item) == "string" || item instanceof String) : false);
3397		},
3398		isArray: function (item) {
3399			return item && Object.prototype.toString.call(item) == "[object Array]";
3400		},
3401		isFunction: function (item) {
3402			return item === null ? false : (typeof (item) == "function" || item instanceof Function);
3403		},
3404		isDomNode: function (item) {
3405			return item && typeof (item) == "object" && "nodeType" in item;
3406		},
3407		isDate: function (item) {
3408			return item && Object.prototype.toString.call(item) == "[object Date]";
3409		},
3410		isPlainObject: function (item) {
3411			if (!item || typeof (item) !== "object" || item.nodeType)
3412			{
3413				return false;
3414			}
3415
3416			const hasProp = Object.prototype.hasOwnProperty;
3417			try
3418			{
3419				if (item.constructor && !hasProp.call(item, "constructor") && !hasProp.call(item.constructor.prototype, "isPrototypeOf"))
3420				{
3421					return false;
3422				}
3423			} catch (e)
3424			{
3425				return false;
3426			}
3427
3428			let key;
3429			for (key in item)
3430			{
3431			}
3432			return typeof (key) === "undefined" || hasProp.call(item, key);
3433		},
3434		isNotEmptyString: function (item) {
3435			return this.isString(item) ? item.length > 0 : false;
3436		},
3437		isJsonRpcRequest: function (item) {
3438			return (
3439				typeof (item) === "object"
3440				&& item
3441				&& "jsonrpc" in item
3442				&& Utils.isNotEmptyString(item.jsonrpc)
3443				&& "method" in item
3444				&& Utils.isNotEmptyString(item.method)
3445			);
3446		},
3447		isJsonRpcResponse: function (item) {
3448			return (
3449				typeof (item) === "object"
3450				&& item
3451				&& "jsonrpc" in item
3452				&& Utils.isNotEmptyString(item.jsonrpc)
3453				&& "id" in item
3454				&& (
3455					"result" in item
3456					|| "error" in item
3457				)
3458			);
3459
3460		},
3461		buildQueryString: function (params) {
3462			let result = '';
3463			for (let key in params)
3464			{
3465				if (!params.hasOwnProperty(key))
3466				{
3467					continue;
3468				}
3469				const value = params[key];
3470				if (Utils.isArray(value))
3471				{
3472					value.forEach((valueElement, index) => {
3473						result += encodeURIComponent(key + "[" + index + "]") + "=" + encodeURIComponent(valueElement) + "&";
3474					});
3475				}
3476				else
3477				{
3478					result += encodeURIComponent(key) + "=" + encodeURIComponent(value) + "&";
3479				}
3480			}
3481
3482			if (result.length > 0)
3483			{
3484				result = result.substr(0, result.length - 1);
3485			}
3486
3487			return result;
3488		},
3489		objectValues: function values(obj) {
3490			let result = [];
3491			for (let key in obj)
3492			{
3493				if (obj.hasOwnProperty(key) && obj.propertyIsEnumerable(key))
3494				{
3495					result.push(obj[key]);
3496				}
3497			}
3498			return result;
3499		},
3500		clone: function (obj, bCopyObj) {
3501			let _obj, i, l;
3502			if (bCopyObj !== false)
3503			{
3504				bCopyObj = true;
3505			}
3506
3507			if (obj === null)
3508			{
3509				return null;
3510			}
3511
3512			if (this.isDomNode(obj))
3513			{
3514				_obj = obj.cloneNode(bCopyObj);
3515			}
3516			else if (typeof obj == 'object')
3517			{
3518				if (this.isArray(obj))
3519				{
3520					_obj = [];
3521					for (i = 0, l = obj.length; i < l; i++)
3522					{
3523						if (typeof obj[i] == "object" && bCopyObj)
3524						{
3525							_obj[i] = this.clone(obj[i], bCopyObj);
3526						}
3527						else
3528						{
3529							_obj[i] = obj[i];
3530						}
3531					}
3532				}
3533				else
3534				{
3535					_obj = {};
3536					if (obj.constructor)
3537					{
3538						if (this.isDate(obj))
3539						{
3540							_obj = new Date(obj);
3541						}
3542						else
3543						{
3544							_obj = new obj.constructor();
3545						}
3546					}
3547
3548					for (i in obj)
3549					{
3550						if (!obj.hasOwnProperty(i))
3551						{
3552							continue;
3553						}
3554						if (typeof obj[i] == "object" && bCopyObj)
3555						{
3556							_obj[i] = this.clone(obj[i], bCopyObj);
3557						}
3558						else
3559						{
3560							_obj[i] = obj[i];
3561						}
3562					}
3563				}
3564
3565			}
3566			else
3567			{
3568				_obj = obj;
3569			}
3570
3571			return _obj;
3572		},
3573
3574		getDateForLog: function () {
3575			const d = new Date();
3576
3577			return d.getFullYear() + "-" + Utils.lpad(d.getMonth(), 2, '0') + "-" + Utils.lpad(d.getDate(), 2, '0') + " " + Utils.lpad(d.getHours(), 2, '0') + ":" + Utils.lpad(d.getMinutes(), 2, '0');
3578		},
3579
3580		lpad: function (str, length, chr) {
3581			str = str.toString();
3582			chr = chr || ' ';
3583
3584			if (str.length > length)
3585			{
3586				return str;
3587			}
3588
3589			let result = '';
3590			for (let i = 0; i < length - str.length; i++)
3591			{
3592				result += chr;
3593			}
3594
3595			return result + str;
3596		}
3597	}
3598
3599	if (
3600		typeof BX.namespace !== 'undefined'
3601		&& typeof BX.PULL === 'undefined'
3602	)
3603	{
3604		BX.PULL = new PullClient();
3605	}
3606
3607	BX.PullClient = PullClient;
3608	BX.PullClient.PullStatus = PullStatus;
3609	BX.PullClient.SubscriptionType = SubscriptionType;
3610	BX.PullClient.CloseReasons = CloseReasons;
3611	BX.PullClient.StorageManager = StorageManager;
3612})();

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.