PageSourceSearch

https://kozhzam-spb.ru/bitrix/js/pull/client/pull.client.js?172716251883600

js kozhzam-spb.ru collected 2026-09-29 18:04:21 UTC 83,600 bytes, 3,614 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 = 60 * 1000;
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 = Number(params.configTimestamp);
201			}
202			else if (typeof BX.message !== 'undefined' && BX.message.pull_config_timestamp)
203			{
204				this.configTimestamp = Number(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 (Number(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			this.configTimestamp = Number(config.server.config_timestamp);
1225
1226			if (this.storage && allowCaching)
1227			{
1228				try
1229				{
1230					this.storage.set('bx-pull-config', config);
1231				} catch (e)
1232				{
1233					// try to delete the key "history" (landing site change history, see http://jabber.bx/view.php?id=136492)
1234					if (localStorage && localStorage.removeItem)
1235					{
1236						localStorage.removeItem('history');
1237					}
1238					console.error(Utils.getDateForLog() + " Pull: Could not cache config in local storage. Error: ", e);
1239				}
1240			}
1241		}
1242
1243		isWebSocketSupported()
1244		{
1245			return typeof (window.WebSocket) !== "undefined";
1246		}
1247
1248		isWebSocketAllowed()
1249		{
1250			if (this.sharedConfig.isWebSocketBlocked())
1251			{
1252				return false;
1253			}
1254
1255			return this.isWebSocketEnabled();
1256		}
1257
1258		isWebSocketEnabled()
1259		{
1260			if (!this.isWebSocketSupported())
1261			{
1262				return false;
1263			}
1264
1265			return (this.config && this.config.server && this.config.server.websocket_enabled === true);
1266		}
1267
1268		isPublishingSupported()
1269		{
1270			return this.getServerVersion() > 3;
1271		}
1272
1273		isPublishingEnabled()
1274		{
1275			if (!this.isPublishingSupported())
1276			{
1277				return false;
1278			}
1279
1280			return (this.config && this.config.server && this.config.server.publish_enabled === true);
1281		}
1282
1283		isProtobufSupported()
1284		{
1285			return (this.getServerVersion() == 4 && !Utils.browser.IsIe());
1286		}
1287
1288		isJsonRpc()
1289		{
1290			return (this.getServerVersion() >= 5);
1291		}
1292
1293		isSharedMode()
1294		{
1295			return (this.getServerMode() == ServerMode.Shared)
1296		}
1297
1298		disconnect(disconnectCode, disconnectReason)
1299		{
1300			if (this.connector)
1301			{
1302				this.isManualDisconnect = true;
1303				this.connector.disconnect(disconnectCode, disconnectReason);
1304			}
1305		}
1306
1307		stop(disconnectCode, disconnectReason)
1308		{
1309			this.disconnect(disconnectCode, disconnectReason);
1310			this.stopCheckConfig();
1311		}
1312
1313		reconnect(disconnectCode, disconnectReason, delay)
1314		{
1315			this.disconnect(disconnectCode, disconnectReason);
1316
1317			delay = delay || 1;
1318			this.scheduleReconnect(delay);
1319		}
1320
1321		restoreWebSocketConnection()
1322		{
1323			if (this.connectionType == ConnectionType.WebSocket)
1324			{
1325				return true;
1326			}
1327
1328			this._connectors.webSocket.connect();
1329		}
1330
1331		scheduleReconnect(connectionDelay)
1332		{
1333			if (!this.enabled)
1334			{
1335				return false;
1336			}
1337
1338			if (!connectionDelay)
1339			{
1340				// never fallback to long polling
1341				// TODO remove long polling support later
1342				/*if (this.connectionAttempt > 3 && this.connectionType === ConnectionType.WebSocket && !this.sharedConfig.isLongPollingBlocked())
1343				{
1344					// Websocket seems to be closed by network filter. Trying to fallback to long polling
1345					this.sharedConfig.setWebSocketBlocked(true);
1346					this.connectionType = ConnectionType.LongPolling;
1347					this.connectionAttempt = 1;
1348					connectionDelay = 1;
1349				}
1350				else*/
1351				{
1352					connectionDelay = this.getConnectionAttemptDelay(this.connectionAttempt);
1353				}
1354			}
1355			if (this.reconnectTimeout)
1356			{
1357				clearTimeout(this.reconnectTimeout);
1358			}
1359
1360			this.logToConsole('Pull: scheduling reconnection in ' + connectionDelay + ' seconds; attempt # ' + this.connectionAttempt);
1361
1362			this.reconnectTimeout = setTimeout(
1363				() => {
1364					this.connect().catch(error => {
1365						console.error(error)
1366					})
1367				},
1368				connectionDelay * 1000);
1369		}
1370
1371		scheduleRestoreWebSocketConnection()
1372		{
1373			this.logToConsole('Pull: scheduling restoration of websocket connection in ' + RESTORE_WEBSOCKET_TIMEOUT + ' seconds');
1374
1375			if (this.restoreWebSocketTimeout)
1376			{
1377				return;
1378			}
1379
1380			this.restoreWebSocketTimeout = setTimeout(() => {
1381				this.restoreWebSocketTimeout = 0;
1382				this.restoreWebSocketConnection();
1383			}, RESTORE_WEBSOCKET_TIMEOUT * 1000);
1384		}
1385
1386		/**
1387		 * @returns {Promise}
1388		 */
1389		connect()
1390		{
1391			if (!this.enabled)
1392			{
1393				return Promise.reject();
1394			}
1395			if (this.connector.connected)
1396			{
1397				return Promise.resolve();
1398			}
1399
1400			if (this.reconnectTimeout)
1401			{
1402				clearTimeout(this.reconnectTimeout);
1403			}
1404
1405			this.status = PullStatus.Connecting;
1406			this.connectionAttempt++;
1407			return new Promise((resolve, reject) => {
1408				this._connectPromise = {resolve, reject}
1409				this.connector.connect();
1410			})
1411		}
1412
1413		onIncomingMessage(message)
1414		{
1415			if (this.isJsonRpc())
1416			{
1417				(message === JSON_RPC_PING) ? this.onJsonRpcPing() : this.jsonRpcAdapter.parseJsonRpcMessage(message);
1418			}
1419			else
1420			{
1421				const events = this.extractMessages(message);
1422				this.handleIncomingEvents(events);
1423			}
1424		}
1425
1426		handleRpcIncomingMessage(messageFields)
1427		{
1428			this.session.mid = messageFields.mid;
1429			let body = messageFields.body;
1430
1431			if (!messageFields.body.extra)
1432			{
1433				body.extra = {};
1434			}
1435			body.extra.sender = messageFields.sender;
1436
1437			if ("user_params" in messageFields && Utils.isPlainObject(messageFields.user_params))
1438			{
1439				Object.assign(body.params, messageFields.user_params)
1440			}
1441
1442			if ("dictionary" in messageFields && Utils.isPlainObject(messageFields.dictionary))
1443			{
1444				Object.assign(body.params, messageFields.dictionary)
1445			}
1446
1447			if (this.checkDuplicate(messageFields.mid))
1448			{
1449				this.addMessageToStat(body);
1450				this.trimDuplicates();
1451				this.broadcastMessage(body)
1452			}
1453
1454			this.connector.send(`mack:${messageFields.mid}`)
1455
1456			return {};
1457		}
1458
1459		onJsonRpcPing()
1460		{
1461			this.updatePingWaitTimeout();
1462			this.connector.send(JSON_RPC_PONG)
1463		}
1464
1465		handleIncomingEvents(events)
1466		{
1467			let messages = [];
1468			if (events.length === 0)
1469			{
1470				this.session.mid = null;
1471				return;
1472			}
1473
1474			for (let i = 0; i < events.length; i++)
1475			{
1476				let event = events[i];
1477				this.updateSessionFromEvent(event);
1478				if (event.mid && !this.checkDuplicate(event.mid))
1479				{
1480					continue;
1481				}
1482
1483				this.addMessageToStat(event.text);
1484				messages.push(event.text);
1485			}
1486			this.trimDuplicates();
1487			this.broadcastMessages(messages);
1488		}
1489
1490		updateSessionFromEvent(event)
1491		{
1492			this.session.mid = event.mid || null;
1493			this.session.tag = event.tag || null;
1494			this.session.time = event.time || null;
1495		}
1496
1497		checkDuplicate(mid)
1498		{
1499			if (this.session.lastMessageIds.includes(mid))
1500			{
1501				console.warn("Duplicate message " + mid + " skipped");
1502				return false;
1503			}
1504			else
1505			{
1506				this.session.lastMessageIds.push(mid);
1507				return true;
1508			}
1509		}
1510
1511		trimDuplicates()
1512		{
1513			if (this.session.lastMessageIds.length > MAX_IDS_TO_STORE)
1514			{
1515				this.session.lastMessageIds = this.session.lastMessageIds.slice(-MAX_IDS_TO_STORE);
1516			}
1517		}
1518
1519		addMessageToStat(message)
1520		{
1521			if (!this.session.history[message.module_id])
1522			{
1523				this.session.history[message.module_id] = {};
1524			}
1525			if (!this.session.history[message.module_id][message.command])
1526			{
1527				this.session.history[message.module_id][message.command] = 0;
1528			}
1529			this.session.history[message.module_id][message.command]++;
1530
1531			this.session.messageCount++;
1532		}
1533
1534		extractMessages(pullEvent)
1535		{
1536			if (pullEvent instanceof ArrayBuffer)
1537			{
1538				return this.extractProtobufMessages(pullEvent);
1539			}
1540			else if (Utils.isNotEmptyString(pullEvent))
1541			{
1542				return this.extractPlainTextMessages(pullEvent)
1543			}
1544		}
1545
1546		extractProtobufMessages(pullEvent)
1547		{
1548			let result = [];
1549			try
1550			{
1551				let responseBatch = ResponseBatch.decode(new Uint8Array(pullEvent));
1552				for (let i = 0; i < responseBatch.responses.length; i++)
1553				{
1554					let response = responseBatch.responses[i];
1555					if (response.command != "outgoingMessages")
1556					{
1557						continue;
1558					}
1559
1560					let messages = response.outgoingMessages.messages;
1561					for (let m = 0; m < messages.length; m++)
1562					{
1563						const message = messages[m];
1564						let messageFields;
1565						try
1566						{
1567							messageFields = JSON.parse(message.body)
1568						} catch (e)
1569						{
1570							console.error(Utils.getDateForLog() + ": Pull: Could not parse message body", e);
1571							continue;
1572						}
1573
1574						if (!messageFields.extra)
1575						{
1576							messageFields.extra = {}
1577						}
1578						messageFields.extra.sender = {
1579							type: message.sender.type
1580						};
1581
1582						if (message.sender.id instanceof Uint8Array)
1583						{
1584							messageFields.extra.sender.id = this.decodeId(message.sender.id)
1585						}
1586
1587						const compatibleMessage = {
1588							mid: this.decodeId(message.id),
1589							text: messageFields
1590						};
1591
1592						result.push(compatibleMessage);
1593					}
1594				}
1595			} catch (e)
1596			{
1597				console.error(Utils.getDateForLog() + ": Pull: Could not parse message", e)
1598			}
1599			return result;
1600		}
1601
1602		extractPlainTextMessages(pullEvent)
1603		{
1604			let result = [];
1605			const dataArray = pullEvent.match(/#!NGINXNMS!#(.*?)#!NGINXNME!#/gm);
1606			if (dataArray === null)
1607			{
1608				const text = "\n========= PULL ERROR ===========\n" +
1609					"Error type: parseResponse error parsing message\n" +
1610					"\n" +
1611					"Data string: " + pullEvent + "\n" +
1612					"================================\n\n";
1613				console.warn(text);
1614				return result;
1615			}
1616			for (let i = 0; i < dataArray.length; i++)
1617			{
1618				dataArray[i] = dataArray[i].substring(12, dataArray[i].length - 12);
1619				if (dataArray[i].length <= 0)
1620				{
1621					continue;
1622				}
1623
1624				let data
1625				try
1626				{
1627					data = JSON.parse(dataArray[i])
1628				} catch (e)
1629				{
1630					continue;
1631				}
1632
1633				result.push(data);
1634			}
1635			return result;
1636		}
1637
1638		/**
1639		 * Converts message id from byte[] to string
1640		 * @param {Uint8Array} encodedId
1641		 * @return {string}
1642		 */
1643		decodeId(encodedId)
1644		{
1645			if (!(encodedId instanceof Uint8Array))
1646			{
1647				throw new Error("encodedId should be an instance of Uint8Array");
1648			}
1649
1650			let result = "";
1651			for (let i = 0; i < encodedId.length; i++)
1652			{
1653				const hexByte = encodedId[i].toString(16);
1654				if (hexByte.length === 1)
1655				{
1656					result += '0';
1657				}
1658				result += hexByte;
1659			}
1660			return result;
1661		}
1662
1663		/**
1664		 * Converts message id from hex-encoded string to byte[]
1665		 * @param {string} id Hex-encoded string.
1666		 * @return {Uint8Array}
1667		 */
1668		encodeId(id)
1669		{
1670			if (!id)
1671			{
1672				return new Uint8Array();
1673			}
1674
1675			let result = [];
1676			for (let i = 0; i < id.length; i += 2)
1677			{
1678				result.push(parseInt(id.substr(i, 2), 16));
1679			}
1680
1681			return new Uint8Array(result);
1682		}
1683
1684		broadcastMessages(messages)
1685		{
1686			messages.forEach(message => this.broadcastMessage(message));
1687		}
1688
1689		broadcastMessage(message)
1690		{
1691			const moduleId = message.module_id = message.module_id.toLowerCase();
1692			const command = message.command;
1693
1694			if (!message.extra)
1695			{
1696				message.extra = {};
1697			}
1698
1699			if (message.extra.server_time_unix)
1700			{
1701				message.extra.server_time_ago = ((Utils.getTimestamp() - (message.extra.server_time_unix * 1000)) / 1000) - (this.config.server.timeShift ? this.config.server.timeShift : 0);
1702				message.extra.server_time_ago = message.extra.server_time_ago > 0 ? message.extra.server_time_ago : 0;
1703			}
1704
1705			this.logMessage(message);
1706			try
1707			{
1708				if (message.extra.sender && message.extra.sender.type === SenderType.Client)
1709				{
1710					if (typeof BX.onCustomEvent !== 'undefined')
1711					{
1712						BX.onCustomEvent(window, 'onPullClientEvent-' + moduleId, [command, message.params, message.extra], true);
1713						BX.onCustomEvent(window, 'onPullClientEvent', [moduleId, command, message.params, message.extra], true);
1714					}
1715
1716					this.emit({
1717						type: SubscriptionType.Client,
1718						moduleId: moduleId,
1719						data: {
1720							command: command,
1721							params: Utils.clone(message.params),
1722							extra: Utils.clone(message.extra)
1723						}
1724					});
1725				}
1726				else if (moduleId === 'pull')
1727				{
1728					this.handleInternalPullEvent(command, message);
1729				}
1730				else if (moduleId == 'online')
1731				{
1732					if (message.extra.server_time_ago < 240)
1733					{
1734						if (typeof BX.onCustomEvent !== 'undefined')
1735						{
1736							BX.onCustomEvent(window, 'onPullOnlineEvent', [command, message.params, message.extra], true);
1737						}
1738
1739						this.emit({
1740							type: SubscriptionType.Online,
1741							data: {
1742								command: command,
1743								params: Utils.clone(message.params),
1744								extra: Utils.clone(message.extra)
1745							}
1746						});
1747					}
1748
1749					if (command === 'userStatusChange')
1750					{
1751						this.emitUserStatusChange(message.params.user_id, message.params.online);
1752					}
1753				}
1754				else
1755				{
1756					if (typeof BX.onCustomEvent !== 'undefined')
1757					{
1758						BX.onCustomEvent(window, 'onPullEvent-' + moduleId, [command, message.params, message.extra], true);
1759						BX.onCustomEvent(window, 'onPullEvent', [moduleId, command, message.params, message.extra], true);
1760					}
1761
1762					this.emit({
1763						type: SubscriptionType.Server,
1764						moduleId: moduleId,
1765						data: {
1766							command: command,
1767							params: Utils.clone(message.params),
1768							extra: Utils.clone(message.extra)
1769						}
1770					});
1771				}
1772			} catch (e)
1773			{
1774				if (typeof (console) == 'object')
1775				{
1776					console.warn(
1777						"\n========= PULL ERROR ===========\n" +
1778						"Error type: broadcastMessages execute error\n" +
1779						"Error event: ", e, "\n" +
1780						"Message: ", message, "\n" +
1781						"================================\n"
1782					);
1783					if (typeof BX.debug !== 'undefined')
1784					{
1785						BX.debug(e);
1786					}
1787				}
1788			}
1789
1790			if (message.extra && message.extra.revision_web)
1791			{
1792				this.checkRevision(message.extra.revision_web);
1793			}
1794		}
1795
1796		logToConsole(message, force)
1797		{
1798			if (this.loggingEnabled || force)
1799			{
1800				console.log(Utils.getDateForLog() + ': ' + message);
1801			}
1802		}
1803
1804		logMessage(message)
1805		{
1806			if (!this.debug)
1807			{
1808				return;
1809			}
1810
1811			if (message.extra.sender && message.extra.sender.type === SenderType.Client)
1812			{
1813				console.info('onPullClientEvent-' + message.module_id, message.command, message.params, message.extra);
1814			}
1815			else if (message.moduleId == 'online')
1816			{
1817				console.info('onPullOnlineEvent', message.command, message.params, message.extra);
1818			}
1819			else
1820			{
1821				console.info('onPullEvent', message.module_id, message.command, message.params, message.extra);
1822			}
1823		}
1824
1825		onLongPollingOpen()
1826		{
1827			this.unloading = false;
1828			this.starting = false;
1829			this.connectionAttempt = 0;
1830			this.isManualDisconnect = false;
1831			this.status = PullStatus.Online;
1832
1833			this.logToConsole('Pull: Long polling connection with push-server opened');
1834			if (this.isWebSocketEnabled())
1835			{
1836				this.scheduleRestoreWebSocketConnection();
1837			}
1838			if (this._connectPromise)
1839			{
1840				this._connectPromise.resolve();
1841			}
1842		}
1843
1844		onWebSocketBlockChanged(e)
1845		{
1846			const isWebSocketBlocked = e.isWebSocketBlocked;
1847
1848			if (isWebSocketBlocked && this.connectionType === ConnectionType.WebSocket && !this.isConnected())
1849			{
1850				clearTimeout(this.reconnectTimeout);
1851
1852				this.connectionAttempt = 0;
1853				this.connectionType = ConnectionType.LongPolling;
1854				this.scheduleReconnect(1);
1855			}
1856			else if (!isWebSocketBlocked && this.connectionType === ConnectionType.LongPolling)
1857			{
1858				clearTimeout(this.reconnectTimeout);
1859				clearTimeout(this.restoreWebSocketTimeout);
1860
1861				this.connectionAttempt = 0;
1862				this.connectionType = ConnectionType.WebSocket;
1863				this.scheduleReconnect(1);
1864			}
1865		}
1866
1867		onWebSocketOpen()
1868		{
1869			this.unloading = false;
1870			this.starting = false;
1871			this.connectionAttempt = 0;
1872			this.isManualDisconnect = false;
1873			this.status = PullStatus.Online;
1874			this.sharedConfig.setWebSocketBlocked(false);
1875
1876			// to prevent fallback to long polling in case of networking problems
1877			this.sharedConfig.setLongPollingBlocked(true);
1878
1879			if (this.connectionType == ConnectionType.LongPolling)
1880			{
1881				this.connectionType = ConnectionType.WebSocket;
1882				this._connectors.longPolling.disconnect();
1883			}
1884
1885			if (this.restoreWebSocketTimeout)
1886			{
1887				clearTimeout(this.restoreWebSocketTimeout);
1888				this.restoreWebSocketTimeout = null;
1889			}
1890			this.logToConsole('Pull: Websocket connection with push-server opened');
1891			if (this._connectPromise)
1892			{
1893				this._connectPromise.resolve();
1894			}
1895			this.restoreUserStatusSubscription();
1896		}
1897
1898		onWebSocketDisconnect(e)
1899		{
1900			if (this.connectionType === ConnectionType.WebSocket)
1901			{
1902				this.status = PullStatus.Offline;
1903			}
1904
1905			if (!e)
1906			{
1907				e = {};
1908			}
1909
1910			this.logToConsole('Pull: Websocket connection with push-server closed. Code: ' + e.code + ', reason: ' + e.reason, true);
1911			if (!this.isManualDisconnect)
1912			{
1913				if (e.code == CloseReasons.WRONG_CHANNEL_ID)
1914				{
1915					this.scheduleRestart(CloseReasons.WRONG_CHANNEL_ID, "wrong channel signature");
1916				}
1917				else
1918				{
1919					this.scheduleReconnect();
1920				}
1921			}
1922
1923			// to prevent fallback to long polling in case of networking problems
1924			this.sharedConfig.setLongPollingBlocked(true);
1925			this.isManualDisconnect = false;
1926
1927			this.clearPingWaitTimeout();
1928		}
1929
1930		onWebSocketError(e)
1931		{
1932			this.starting = false;
1933			if (this.connectionType === ConnectionType.WebSocket)
1934			{
1935				this.status = PullStatus.Offline;
1936			}
1937
1938			console.error(Utils.getDateForLog() + ": Pull: WebSocket connection error", e);
1939			this.scheduleReconnect();
1940			if (this._connectPromise)
1941			{
1942				this._connectPromise.reject();
1943			}
1944
1945			this.clearPingWaitTimeout();
1946		}
1947
1948		onLongPollingDisconnect(e)
1949		{
1950			if (this.connectionType === ConnectionType.LongPolling)
1951			{
1952				this.status = PullStatus.Offline;
1953			}
1954
1955			if (!e)
1956			{
1957				e = {};
1958			}
1959
1960			this.logToConsole('Pull: Long polling connection with push-server closed. Code: ' + e.code + ', reason: ' + e.reason);
1961			if (!this.isManualDisconnect)
1962			{
1963				this.scheduleReconnect();
1964			}
1965			this.isManualDisconnect = false;
1966			this.clearPingWaitTimeout();
1967		}
1968
1969		onLongPollingError(e)
1970		{
1971			this.starting = false;
1972			if (this.connectionType === ConnectionType.LongPolling)
1973			{
1974				this.status = PullStatus.Offline;
1975			}
1976			console.error(Utils.getDateForLog() + ': Pull: Long polling connection error', e);
1977			this.scheduleReconnect();
1978			if (this._connectPromise)
1979			{
1980				this._connectPromise.reject();
1981			}
1982			this.clearPingWaitTimeout();
1983		}
1984
1985		isConnected()
1986		{
1987			return this.connector ? this.connector.connected : false;
1988		}
1989
1990		onBeforeUnload()
1991		{
1992			this.unloading = true;
1993
1994			const session = Utils.clone(this.session);
1995			session.ttl = (new Date()).getTime() + LS_SESSION_CACHE_TIME * 1000;
1996			if (this.storage)
1997			{
1998				try
1999				{
2000					this.storage.set(LS_SESSION, JSON.stringify(session), LS_SESSION_CACHE_TIME);
2001				} catch (e)
2002				{
2003					console.error(Utils.getDateForLog() + " Pull: Could not save session info in local storage. Error: ", e);
2004				}
2005			}
2006
2007			this.scheduleReconnect(15);
2008		}
2009
2010		onOffline()
2011		{
2012			this.disconnect("1000", "offline");
2013		}
2014
2015		onOnline()
2016		{
2017			this.connect();
2018		}
2019
2020		handleInternalPullEvent(command, message)
2021		{
2022			switch (command.toUpperCase())
2023			{
2024				case SystemCommands.CHANNEL_EXPIRE:
2025				{
2026					if (message.params.action == 'reconnect')
2027					{
2028						this.config.channels[message.params.channel.type] = message.params.new_channel;
2029						this.logToConsole("Pull: new config for " + message.params.channel.type + " channel set:\n", this.config.channels[message.params.channel.type]);
2030
2031						this.reconnect(CloseReasons.CONFIG_REPLACED, "config was replaced");
2032					}
2033					else
2034					{
2035						this.restart(CloseReasons.CHANNEL_EXPIRED, "channel expired received");
2036					}
2037					break;
2038				}
2039				case SystemCommands.CONFIG_EXPIRE:
2040				{
2041					this.restart(CloseReasons.CONFIG_EXPIRED, "config expired received");
2042					break;
2043				}
2044				case SystemCommands.SERVER_RESTART:
2045				{
2046					this.reconnect(CloseReasons.SERVER_RESTARTED, "server was restarted", 15);
2047					break;
2048				}
2049				default://
2050			}
2051		}
2052
2053		checkRevision(serverRevision)
2054		{
2055			if (this.skipCheckRevision)
2056			{
2057				return true;
2058			}
2059
2060			serverRevision = parseInt(serverRevision);
2061			if (serverRevision > 0 && serverRevision != REVISION)
2062			{
2063				this.enabled = false;
2064				if (typeof BX.message !== 'undefined')
2065				{
2066					this.showNotification(BX.message('PULL_OLD_REVISION'));
2067				}
2068				this.disconnect(CloseReasons.NORMAL_CLOSURE, 'check_revision');
2069
2070				if (typeof BX.onCustomEvent !== 'undefined')
2071				{
2072					BX.onCustomEvent(window, 'onPullRevisionUp', [serverRevision, REVISION]);
2073				}
2074
2075				this.emit({
2076					type: SubscriptionType.Revision,
2077					data: {
2078						server: serverRevision,
2079						client: REVISION
2080					}
2081				});
2082
2083				this.logToConsole("Pull revision changed from " + REVISION + " to " + serverRevision + ". Reload required");
2084
2085				return false;
2086			}
2087			return true;
2088		}
2089
2090		showNotification(text)
2091		{
2092			if (this.notificationPopup || typeof BX.PopupWindow === 'undefined')
2093			{
2094				return;
2095			}
2096
2097			this.notificationPopup = new BX.PopupWindow('bx-notifier-popup-confirm', null, {
2098				zIndex: 200,
2099				autoHide: false,
2100				closeByEsc: false,
2101				overlay: true,
2102				content: BX.create("div", {
2103					props: {className: "bx-messenger-confirm"},
2104					html: text
2105				}),
2106				buttons: [
2107					new BX.PopupWindowButton({
2108						text: BX.message('JS_CORE_WINDOW_CLOSE'),
2109						className: "popup-window-button-decline",
2110						events: {
2111							click: () => this.notificationPopup.close(),
2112						}
2113					})
2114				],
2115				events: {
2116					onPopupClose: () => this.notificationPopup.destroy(),
2117					onPopupDestroy: () => this.notificationPopup = null,
2118				}
2119			});
2120			this.notificationPopup.show();
2121		}
2122
2123		getRevision()
2124		{
2125			return (this.config && this.config.api) ? this.config.api.revision_web : null;
2126		}
2127
2128		getServerVersion()
2129		{
2130			return (this.config && this.config.server) ? this.config.server.version : 0;
2131		}
2132
2133		getServerMode()
2134		{
2135			return (this.config && this.config.server) ? this.config.server.mode : null;
2136		}
2137
2138		getConfig()
2139		{
2140			return this.config;
2141		}
2142
2143		getDebugInfo()
2144		{
2145			if (!JSON || !JSON.stringify)
2146			{
2147				return false;
2148			}
2149
2150			let configDump;
2151			if (this.config && this.config.channels)
2152			{
2153				configDump = {
2154					"ChannelID": (this.config.channels.private ? this.config.channels.private.id : "n/a"),
2155					"ChannelDie": (this.config.channels.private ? this.config.channels.private.end : "n/a"),
2156					"ChannelDieShared": ("shared" in this.config.channels ? this.config.channels.shared.end : "n/a"),
2157				};
2158			}
2159			else
2160			{
2161				configDump = {"Config error": "config is not loaded"}
2162			}
2163
2164			let websocketMode = "-";
2165			if (this._connectors.webSocket && this._connectors.webSocket.socket)
2166			{
2167				if (this.isJsonRpc())
2168				{
2169					websocketMode = "json-rpc"
2170				}
2171				else
2172				{
2173					websocketMode = (this._connectors.webSocket.socket.url.search("binaryMode=true") != -1 ? "protobuf" : "text")
2174				}
2175			}
2176
2177			return {
2178				"UserId": this.userId + (this.userId > 0 ? '' : '(guest)'),
2179				"Guest userId": (this.guestMode && this.guestUserId !== 0 ? this.guestUserId : "-"),
2180				"Browser online": (navigator.onLine ? 'Y' : 'N'),
2181				"Connect": (this.isConnected() ? 'Y' : 'N'),
2182				"Server type": (this.isSharedMode() ? 'cloud' : 'local'),
2183				"WebSocket supported": (this.isWebSocketSupported() ? 'Y' : 'N'),
2184				"WebSocket connected": (this._connectors.webSocket && this._connectors.webSocket.connected ? 'Y' : 'N'),
2185				"WebSocket mode": websocketMode,
2186
2187				"Try connect": (this.reconnectTimeout ? 'Y' : 'N'),
2188				"Try number": (this.connectionAttempt),
2189
2190				"Path": (this.connector ? this.connector.path : '-'),
2191				...configDump,
2192
2193				"Last message": (this.session.mid > 0 ? this.session.mid : '-'),
2194				"Session history": this.session.history,
2195				"Watch tags": this.watchTagsQueue,
2196			}
2197		}
2198
2199		enableLogging(loggingFlag)
2200		{
2201			if (loggingFlag === undefined)
2202			{
2203				loggingFlag = true;
2204			}
2205			loggingFlag = loggingFlag === true;
2206
2207			this.sharedConfig.setLoggingEnabled(loggingFlag);
2208			this.loggingEnabled = loggingFlag;
2209		}
2210
2211		capturePullEvent(debugFlag)
2212		{
2213			if (debugFlag === undefined)
2214			{
2215				debugFlag = true;
2216			}
2217
2218			this.debug = debugFlag;
2219		}
2220
2221		getConnectionPath(connectionType)
2222		{
2223			let path;
2224			let params = {};
2225
2226			switch (connectionType)
2227			{
2228				case ConnectionType.WebSocket:
2229					path = this.isSecure ? this.config.server.websocket_secure : this.config.server.websocket;
2230					break;
2231				case ConnectionType.LongPolling:
2232					path = this.isSecure ? this.config.server.long_pooling_secure : this.config.server.long_polling;
2233					break;
2234				default:
2235					throw new Error("Unknown connection type " + connectionType);
2236			}
2237
2238			if (!Utils.isNotEmptyString(path))
2239			{
2240				return false;
2241			}
2242
2243			if (typeof (this.config.jwt) == 'string' && this.config.jwt !== '')
2244			{
2245				params['token'] = this.config.jwt;
2246			}
2247			else
2248			{
2249				let channels = [];
2250				['private', 'shared'].forEach((type) => {
2251					if (typeof this.config.channels[type] !== 'undefined')
2252					{
2253						channels.push(this.config.channels[type].id);
2254					}
2255				});
2256				if (channels.length === 0)
2257				{
2258					return false;
2259				}
2260
2261				params['CHANNEL_ID'] = channels.join('/');
2262			}
2263
2264			if (this.isJsonRpc())
2265			{
2266				params.jsonRpc = 'true';
2267			}
2268			else if (this.isProtobufSupported())
2269			{
2270				params.binaryMode = 'true';
2271			}
2272
2273			if (this.isSharedMode())
2274			{
2275				if (!this.config.clientId)
2276				{
2277					throw new Error("Push-server is in shared mode, but clientId is not set");
2278				}
2279				params.clientId = this.config.clientId;
2280			}
2281			if (this.session.mid)
2282			{
2283				params.mid = this.session.mid;
2284			}
2285			if (this.session.tag)
2286			{
2287				params.tag = this.session.tag;
2288			}
2289			if (this.session.time)
2290			{
2291				params.time = this.session.time;
2292			}
2293			params.revision = REVISION;
2294
2295			return path + '?' + Utils.buildQueryString(params);
2296		}
2297
2298		getPublicationPath()
2299		{
2300			const path = this.isSecure ? this.config.server.publish_secure : this.config.server.publish;
2301			if (!path)
2302			{
2303				return '';
2304			}
2305
2306			let channels = [];
2307			for (let type in this.config.channels)
2308			{
2309				if (!this.config.channels.hasOwnProperty(type))
2310				{
2311					continue;
2312				}
2313				channels.push(this.config.channels[type].id);
2314			}
2315
2316			const params = {
2317				CHANNEL_ID: channels.join('/')
2318			};
2319
2320			return path + '?' + Utils.buildQueryString(params);
2321		}
2322
2323		/**
2324		 * Returns reconnect delay in seconds
2325		 * @param attemptNumber
2326		 * @return {number}
2327		 */
2328		getConnectionAttemptDelay(attemptNumber)
2329		{
2330			let result;
2331			if (attemptNumber < 1)
2332			{
2333				result = 0.5;
2334			}
2335			else if (attemptNumber < 3)
2336			{
2337				result = 15;
2338			}
2339			else if (attemptNumber < 5)
2340			{
2341				result = 45;
2342			}
2343			else if (attemptNumber < 10)
2344			{
2345				result = 600;
2346			}
2347			else
2348			{
2349				result = 3600;
2350			}
2351
2352			return result + (result * Math.random() * 0.2);
2353		}
2354
2355		sendPullStatusDelayed(status, delay)
2356		{
2357			if (this.offlineTimeout)
2358			{
2359				clearTimeout(this.offlineTimeout)
2360			}
2361			this.offlineTimeout = setTimeout(
2362				() => {
2363					this.offlineTimeout = null;
2364					this.sendPullStatus(status);
2365				},
2366				delay
2367			)
2368		}
2369
2370		sendPullStatus(status)
2371		{
2372			if (this.unloading)
2373			{
2374				return;
2375			}
2376
2377			if (typeof BX.onCustomEvent !== 'undefined')
2378			{
2379				BX.onCustomEvent(window, 'onPullStatus', [status]);
2380			}
2381
2382			this.emit({
2383				type: SubscriptionType.Status,
2384				data: {
2385					status: status
2386				}
2387			});
2388		}
2389
2390		extendWatch(tag, force)
2391		{
2392			if (!tag || this.watchTagsQueue[tag])
2393			{
2394				return false;
2395			}
2396
2397			this.watchTagsQueue[tag] = true;
2398			if (force)
2399			{
2400				this.updateWatch(force);
2401			}
2402		}
2403
2404		updateWatch(force)
2405		{
2406			clearTimeout(this.watchUpdateTimeout);
2407			this.watchUpdateTimeout = setTimeout(() => {
2408				const watchTags = Object.keys(this.watchTagsQueue);
2409				if (watchTags.length > 0)
2410				{
2411					this.restClient.callMethod('pull.watch.extend', {tags: watchTags}, (result) => {
2412						if (result.error())
2413						{
2414							this.updateWatch();
2415
2416							return false;
2417						}
2418
2419						const updatedTags = result.data();
2420
2421						for (let tagId in updatedTags)
2422						{
2423							if (updatedTags.hasOwnProperty(tagId) && !updatedTags[tagId])
2424							{
2425								this.clearWatch(tagId);
2426							}
2427						}
2428						this.updateWatch();
2429					})
2430				}
2431				else
2432				{
2433					this.updateWatch();
2434				}
2435			}, force ? this.watchForceUpdateInterval : this.watchUpdateInterval);
2436		}
2437
2438		clearWatch(tagId)
2439		{
2440			delete this.watchTagsQueue[tagId];
2441		}
2442
2443		updatePingWaitTimeout()
2444		{
2445			clearTimeout(this.pingWaitTimeout);
2446			this.pingWaitTimeout = setTimeout(this.onPingTimeoutHandler, PING_TIMEOUT * 2 * 1000)
2447		}
2448
2449		clearPingWaitTimeout()
2450		{
2451			clearTimeout(this.pingWaitTimeout);
2452			this.pingWaitTimeout = null;
2453		}
2454
2455		onPingTimeout()
2456		{
2457			this.pingWaitTimeout = null;
2458			if (!this.enabled || !this.isConnected())
2459			{
2460				return;
2461			}
2462
2463			console.warn("No pings are received in " + PING_TIMEOUT * 2 + " seconds. Reconnecting")
2464			this.disconnect(CloseReasons.STUCK, "connection stuck");
2465			this.scheduleReconnect();
2466		}
2467
2468		// old functions, not used anymore.
2469		setPrivateVar() {}
2470
2471		returnPrivateVar() {}
2472
2473		expireConfig() {}
2474
2475		updateChannelID() {}
2476
2477		tryConnect() {}
2478
2479		tryConnectDelay() {}
2480
2481		tryConnectSet() {}
2482
2483		updateState() {}
2484
2485		setUpdateStateStepCount() {}
2486
2487		supportWebSocket()
2488		{
2489			return this.isWebSocketSupported();
2490		}
2491
2492		isWebSoketConnected()
2493		{
2494			return this.isConnected() && this.connectionType == ConnectionType.WebSocket;
2495		}
2496
2497		getPullServerStatus() {return this.isConnected()}
2498
2499		closeConfirm()
2500		{
2501			if (this.notificationPopup)
2502			{
2503				this.notificationPopup.destroy();
2504			}
2505		}
2506	}
2507
2508	class SharedConfig
2509	{
2510		constructor(params)
2511		{
2512			params = params || {};
2513			this.storage = params.storage || new StorageManager();
2514
2515			this.ttl = 24 * 60 * 60;
2516
2517			this.lsKeys = {
2518				websocketBlocked: 'bx-pull-websocket-blocked',
2519				longPollingBlocked: 'bx-pull-longpolling-blocked',
2520				loggingEnabled: 'bx-pull-logging-enabled'
2521			};
2522
2523			this.callbacks = {
2524				onWebSocketBlockChanged: (Utils.isFunction(params.onWebSocketBlockChanged) ? params.onWebSocketBlockChanged : function () {})
2525			};
2526
2527			if (this.storage)
2528			{
2529				window.addEventListener('storage', this.onLocalStorageSet.bind(this));
2530			}
2531		}
2532
2533		onLocalStorageSet(params)
2534		{
2535			if (
2536				this.storage.compareKey(params.key, this.lsKeys.websocketBlocked)
2537				&& params.newValue != params.oldValue
2538			)
2539			{
2540				this.callbacks.onWebSocketBlockChanged({
2541					isWebSocketBlocked: this.isWebSocketBlocked()
2542				})
2543			}
2544		}
2545
2546		isWebSocketBlocked()
2547		{
2548			if (!this.storage)
2549			{
2550				return false;
2551			}
2552
2553			return this.storage.get(this.lsKeys.websocketBlocked, 0) > Utils.getTimestamp();
2554		}
2555
2556		setWebSocketBlocked(isWebSocketBlocked)
2557		{
2558			if (!this.storage)
2559			{
2560				return false;
2561			}
2562
2563			try
2564			{
2565				this.storage.set(this.lsKeys.websocketBlocked, (isWebSocketBlocked ? Utils.getTimestamp() + this.ttl : 0));
2566			} catch (e)
2567			{
2568				console.error(Utils.getDateForLog() + " Pull: Could not save WS_blocked flag in local storage. Error: ", e);
2569			}
2570		}
2571
2572		isLongPollingBlocked()
2573		{
2574			if (!this.storage)
2575			{
2576				return false;
2577			}
2578
2579			return this.storage.get(this.lsKeys.longPollingBlocked, 0) > Utils.getTimestamp();
2580		}
2581
2582		setLongPollingBlocked(isLongPollingBlocked)
2583		{
2584			if (!this.storage)
2585			{
2586				return false;
2587			}
2588
2589			try
2590			{
2591				this.storage.set(this.lsKeys.longPollingBlocked, (isLongPollingBlocked ? Utils.getTimestamp() + this.ttl : 0));
2592			} catch (e)
2593			{
2594				console.error(Utils.getDateForLog() + " Pull: Could not save LP_blocked flag in local storage. Error: ", e);
2595			}
2596		}
2597
2598		isLoggingEnabled()
2599		{
2600			if (!this.storage)
2601			{
2602				return false;
2603			}
2604
2605			return this.storage.get(this.lsKeys.loggingEnabled, 0) > Utils.getTimestamp();
2606		}
2607
2608		setLoggingEnabled(isLoggingEnabled)
2609		{
2610			if (!this.storage)
2611			{
2612				return false;
2613			}
2614
2615			try
2616			{
2617				this.storage.set(this.lsKeys.loggingEnabled, (isLoggingEnabled ? Utils.getTimestamp() + this.ttl : 0));
2618			} catch (e)
2619			{
2620				console.error("LocalStorage error: ", e);
2621				return false;
2622			}
2623		}
2624	}
2625
2626	class AbstractConnector
2627	{
2628		_connected = false;
2629		connectionType = "";
2630
2631		disconnectCode = '';
2632		disconnectReason = '';
2633
2634		constructor(config)
2635		{
2636			this.parent = config.parent;
2637			this.callbacks = {
2638				onOpen: Utils.isFunction(config.onOpen) ? config.onOpen : function () {},
2639				onDisconnect: Utils.isFunction(config.onDisconnect) ? config.onDisconnect : function () {},
2640				onError: Utils.isFunction(config.onError) ? config.onError : function () {},
2641				onMessage: Utils.isFunction(config.onMessage) ? config.onMessage : function () {}
2642			};
2643		}
2644
2645		get connected()
2646		{
2647			return this._connected
2648		}
2649
2650		set connected(value)
2651		{
2652			if (value == this._connected)
2653			{
2654				return;
2655			}
2656
2657			this._connected = value;
2658
2659			if (this._connected)
2660			{
2661				this.callbacks.onOpen();
2662			}
2663			else
2664			{
2665				this.callbacks.onDisconnect({
2666					code: this.disconnectCode,
2667					reason: this.disconnectReason
2668				});
2669			}
2670		}
2671
2672		get path()
2673		{
2674			return this.parent.getConnectionPath(this.connectionType);
2675		}
2676	}
2677
2678	class WebSocketConnector extends AbstractConnector
2679	{
2680		constructor(config)
2681		{
2682			super(config)
2683			this.connectionType = ConnectionType.WebSocket;
2684			this.socket = null;
2685
2686			this.onSocketOpenHandler = this.onSocketOpen.bind(this);
2687			this.onSocketCloseHandler = this.onSocketClose.bind(this);
2688			this.onSocketErrorHandler = this.onSocketError.bind(this);
2689			this.onSocketMessageHandler = this.onSocketMessage.bind(this);
2690		}
2691
2692		connect()
2693		{
2694			if (this.socket)
2695			{
2696				if (this.socket.readyState === 1)
2697				{
2698					// already connected
2699					return true;
2700				}
2701				else
2702				{
2703					this.socket.removeEventListener('open', this.onSocketOpenHandler);
2704					this.socket.removeEventListener('close', this.onSocketCloseHandler);
2705					this.socket.removeEventListener('error', this.onSocketErrorHandler);
2706					this.socket.removeEventListener('message', this.onSocketMessageHandler);
2707
2708					this.socket.close();
2709					this.socket = null;
2710				}
2711			}
2712
2713			this.createSocket();
2714		}
2715
2716		disconnect(code, message)
2717		{
2718			if (this.socket !== null)
2719			{
2720				this.socket.removeEventListener('open', this.onSocketOpenHandler);
2721				this.socket.removeEventListener('close', this.onSocketCloseHandler);
2722				this.socket.removeEventListener('error', this.onSocketErrorHandler);
2723				this.socket.removeEventListener('message', this.onSocketMessageHandler);
2724
2725				this.socket.close(code, message);
2726			}
2727			this.socket = null;
2728			this.disconnectCode = code;
2729			this.disconnectReason = message;
2730			this.connected = false;
2731		}
2732
2733		createSocket()
2734		{
2735			if (this.socket)
2736			{
2737				throw new Error("Socket already exists");
2738			}
2739
2740			if (!this.path)
2741			{
2742				throw new Error("Websocket connection path is not defined");
2743			}
2744
2745			this.socket = new WebSocket(this.path);
2746			this.socket.binaryType = 'arraybuffer';
2747
2748			this.socket.addEventListener('open', this.onSocketOpenHandler);
2749			this.socket.addEventListener('close', this.onSocketCloseHandler);
2750			this.socket.addEventListener('error', this.onSocketErrorHandler);
2751			this.socket.addEventListener('message', this.onSocketMessageHandler);
2752		}
2753
2754		/**
2755		 * Sends some data to the server via websocket connection.
2756		 * @param {ArrayBuffer} buffer Data to send.
2757		 * @return {boolean}
2758		 */
2759		send(buffer)
2760		{
2761			if (!this.socket || this.socket.readyState !== 1)
2762			{
2763				console.error(Utils.getDateForLog() + ": Pull: WebSocket is not connected");
2764				return false;
2765			}
2766
2767			this.socket.send(buffer);
2768			return true;
2769		}
2770
2771		onSocketOpen()
2772		{
2773			this.connected = true;
2774		}
2775
2776		onSocketClose(e)
2777		{
2778			this.socket = null;
2779			this.disconnectCode = e.code;
2780			this.disconnectReason = e.reason;
2781			this.connected = false;
2782		}
2783
2784		onSocketError(e)
2785		{
2786			this.callbacks.onError(e);
2787		}
2788
2789		onSocketMessage(e)
2790		{
2791			this.callbacks.onMessage(e.data);
2792		}
2793
2794		destroy()
2795		{
2796			if (this.socket)
2797			{
2798				this.socket.close();
2799				this.socket = null;
2800			}
2801		}
2802	}
2803
2804	class LongPollingConnector extends AbstractConnector
2805	{
2806		constructor(config)
2807		{
2808			super(config);
2809
2810			this.active = false;
2811			this.connectionType = ConnectionType.LongPolling;
2812			this.requestTimeout = null;
2813			this.failureTimeout = null;
2814			this.xhr = this.createXhr();
2815			this.requestAborted = false;
2816		}
2817
2818		createXhr()
2819		{
2820			const result = new XMLHttpRequest();
2821			if (this.parent.isProtobufSupported() && !this.parent.isJsonRpc())
2822			{
2823				result.responseType = "arraybuffer";
2824			}
2825			result.addEventListener("readystatechange", this.onXhrReadyStateChange.bind(this));
2826			return result;
2827		}
2828
2829		connect()
2830		{
2831			this.active = true;
2832			this.performRequest();
2833		}
2834
2835		disconnect(code, reason)
2836		{
2837			this.active = false;
2838
2839			if (this.failureTimeout)
2840			{
2841				clearTimeout(this.failureTimeout);
2842				this.failureTimeout = null;
2843			}
2844			if (this.requestTimeout)
2845			{
2846				clearTimeout(this.requestTimeout);
2847				this.requestTimeout = null;
2848			}
2849
2850			if (this.xhr)
2851			{
2852				this.requestAborted = true;
2853				this.xhr.abort();
2854			}
2855
2856			this.disconnectCode = code;
2857			this.disconnectReason = reason;
2858			this.connected = false;
2859		}
2860
2861		performRequest()
2862		{
2863			if (!this.active)
2864			{
2865				return;
2866			}
2867
2868			if (!this.path)
2869			{
2870				throw new Error("Long polling connection path is not defined");
2871			}
2872			if (this.xhr.readyState !== 0 && this.xhr.readyState !== 4)
2873			{
2874				return;
2875			}
2876
2877			clearTimeout(this.failureTimeout);
2878			clearTimeout(this.requestTimeout);
2879
2880			this.failureTimeout = setTimeout(() => { this.connected = true }, 5000);
2881			this.requestTimeout = setTimeout(this.onRequestTimeout.bind(this), LONG_POLLING_TIMEOUT * 1000);
2882
2883			this.xhr.open("GET", this.path);
2884			this.xhr.send();
2885		}
2886
2887		onRequestTimeout()
2888		{
2889			this.requestAborted = true;
2890			this.xhr.abort();
2891			this.performRequest();
2892		}
2893
2894		onXhrReadyStateChange()
2895		{
2896			if (this.xhr.readyState === 4)
2897			{
2898				if (!this.requestAborted || this.xhr.status == 200)
2899				{
2900					this.onResponse(this.xhr.response);
2901				}
2902				this.requestAborted = false;
2903			}
2904		}
2905
2906		/**
2907		 * Sends some data to the server via http request.
2908		 * @param {ArrayBuffer} buffer Data to send.
2909		 * @return {bool}
2910		 */
2911		send(buffer)
2912		{
2913			const path = this.parent.getPublicationPath();
2914			if (!path)
2915			{
2916				console.error(Utils.getDateForLog() + ": Pull: publication path is empty");
2917				return false;
2918			}
2919
2920			let xhr = new XMLHttpRequest();
2921			xhr.open("POST", path);
2922			xhr.send(buffer);
2923		}
2924
2925		onResponse(response)
2926		{
2927			if (this.failureTimeout)
2928			{
2929				clearTimeout(this.failureTimeout);
2930				this.failureTimeout = 0;
2931			}
2932			if (this.requestTimeout)
2933			{
2934				clearTimeout(this.requestTimeout);
2935				this.requestTimeout = 0;
2936			}
2937
2938			if (this.xhr.status == 200)
2939			{
2940				this.connected = true;
2941				if (Utils.isNotEmptyString(response) || (response instanceof ArrayBuffer))
2942				{
2943					this.callbacks.onMessage(response);
2944				}
2945				else
2946				{
2947					this.parent.session.mid = null;
2948				}
2949				this.performRequest();
2950			}
2951			else if (this.xhr.status == 304)
2952			{
2953				this.connected = true;
2954				if (this.xhr.getResponseHeader("Expires") === "Thu, 01 Jan 1973 11:11:01 GMT")
2955				{
2956					const lastMessageId = this.xhr.getResponseHeader("Last-Message-Id");
2957					if (Utils.isNotEmptyString(lastMessageId))
2958					{
2959						this.parent.setLastMessageId(lastMessageId);
2960					}
2961				}
2962				this.performRequest();
2963			}
2964			else
2965			{
2966				this.callbacks.onError('Could not connect to the server');
2967				this.connected = false;
2968			}
2969		}
2970	}
2971
2972	class ChannelManager
2973	{
2974		constructor(params)
2975		{
2976			this.publicIds = {};
2977
2978			this.restClient = typeof params.restClient !== "undefined" ? params.restClient : BX.rest;
2979
2980			this.getPublicListMethod = params.getPublicListMethod;
2981		}
2982
2983		/**
2984		 *
2985		 * @param {Array} users Array of user ids.
2986		 * @return {Promise}
2987		 */
2988		getPublicIds(users)
2989		{
2990			const now = new Date();
2991			let result = {};
2992			let unknownUsers = [];
2993
2994			for (let i = 0; i < users.length; i++)
2995			{
2996				const userId = users[i];
2997				if (this.publicIds[userId] && this.publicIds[userId]['end'] > now)
2998				{
2999					result[userId] = this.publicIds[userId];
3000				}
3001				else
3002				{
3003					unknownUsers.push(userId);
3004				}
3005			}
3006
3007			if (unknownUsers.length === 0)
3008			{
3009				return Promise.resolve(result);
3010			}
3011
3012			return new Promise((resolve) => {
3013				this.restClient.callMethod(this.getPublicListMethod, {users: unknownUsers}).then((response) => {
3014					if (response.error())
3015					{
3016						return resolve({});
3017					}
3018
3019					const data = response.data();
3020					this.setPublicIds(Utils.objectValues(data));
3021					unknownUsers.forEach((userId) => {
3022						result[userId] = this.publicIds[userId];
3023					});
3024
3025					resolve(result);
3026				});
3027			})
3028		}
3029
3030		/**
3031		 *
3032		 * @param {object[]} publicIds
3033		 * @param {integer} publicIds.user_id
3034		 * @param {string} publicIds.public_id
3035		 * @param {string} publicIds.signature
3036		 * @param {Date} publicIds.start
3037		 * @param {Date} publicIds.end
3038		 */
3039		setPublicIds(publicIds)
3040		{
3041			for (let i = 0; i < publicIds.length; i++)
3042			{
3043				const publicIdDescriptor = publicIds[i];
3044				const userId = publicIdDescriptor.user_id;
3045				this.publicIds[userId] = {
3046					userId: userId,
3047					publicId: publicIdDescriptor.public_id,
3048					signature: publicIdDescriptor.signature,
3049					start: new Date(publicIdDescriptor.start),
3050					end: new Date(publicIdDescriptor.end)
3051				}
3052			}
3053		};
3054	}
3055
3056	class StorageManager
3057	{
3058		constructor(params)
3059		{
3060			params = params || {};
3061
3062			this.userId = params.userId ? params.userId : (typeof BX.message !== 'undefined' && BX.message.USER_ID ? BX.message.USER_ID : 0);
3063			this.siteId = params.siteId ? params.siteId : (typeof BX.message !== 'undefined' && BX.message.SITE_ID ? BX.message.SITE_ID : 'none');
3064		}
3065
3066		set(name, value)
3067		{
3068			if (typeof window.localStorage === 'undefined')
3069			{
3070				return false;
3071			}
3072			if (typeof value != 'string')
3073			{
3074				if (value)
3075				{
3076					value = JSON.stringify(value);
3077				}
3078			}
3079			return window.localStorage.setItem(this.getKey(name), value)
3080		}
3081
3082		get(name, defaultValue)
3083		{
3084			if (typeof window.localStorage === 'undefined')
3085			{
3086				return defaultValue || null;
3087			}
3088
3089			const result = window.localStorage.getItem(this.getKey(name));
3090			if (result === null)
3091			{
3092				return defaultValue || null;
3093			}
3094
3095			return JSON.parse(result);
3096		}
3097
3098		remove(name)
3099		{
3100			if (typeof window.localStorage === 'undefined')
3101			{
3102				return false;
3103			}
3104			return window.localStorage.removeItem(this.getKey(name));
3105		}
3106
3107		getKey(name)
3108		{
3109			return 'bx-pull-' + this.userId + '-' + this.siteId + '-' + name;
3110		}
3111
3112		compareKey(eventKey, userKey)
3113		{
3114			return eventKey === this.getKey(userKey);
3115		}
3116	}
3117
3118	class JsonRpc
3119	{
3120		idCounter = 0;
3121
3122		handlers = {};
3123		rpcResponseAwaiters = new Map();
3124
3125		constructor(options)
3126		{
3127			this.connector = options.connector;
3128			if (Utils.isPlainObject(options.handlers))
3129			{
3130				for (let method in options.handlers)
3131				{
3132					this.handle(method, options.handlers[method]);
3133				}
3134			}
3135		}
3136
3137		/**
3138		 * @param {string} method
3139		 * @param {function} handler
3140		 */
3141		handle(method, handler)
3142		{
3143			this.handlers[method] = handler;
3144		}
3145
3146		/**
3147		 * Sends RPC command to the server.
3148		 *
3149		 * @param {string} method Method name
3150		 * @param {object} params
3151		 * @param {int} timeout
3152		 * @returns {Promise}
3153		 */
3154		executeOutgoingRpcCommand(method, params, timeout)
3155		{
3156			if (!timeout)
3157			{
3158				timeout = 5;
3159			}
3160			return new Promise((resolve, reject) => {
3161				const request = this.createRequest(method, params);
3162
3163				if (!this.connector.send(JSON.stringify(request)))
3164				{
3165					reject(new ErrorNotConnected('websocket is not connected'));
3166				}
3167
3168				const t = setTimeout(() => {
3169					this.rpcResponseAwaiters.delete(request.id);
3170					reject(new ErrorTimeout('no response'));
3171				}, timeout * 1000);
3172				this.rpcResponseAwaiters.set(request.id, {resolve, reject, timeout: t});
3173			})
3174		}
3175
3176		/**
3177		 * Executes array or rpc commands. Returns array of promises, each promise will be resolved individually.
3178		 *
3179		 * @param {JsonRpcRequest[]} batch
3180		 * @returns {Promise[]}
3181		 */
3182		executeOutgoingRpcBatch(batch)
3183		{
3184			let requests = [];
3185			let promises = [];
3186			batch.forEach(({method, params, id}) => {
3187				const request = this.createRequest(method, params, id);
3188				requests.push(request);
3189				promises.push(new Promise((resolve, reject) => this.rpcResponseAwaiters.set(request.id, {
3190					resolve,
3191					reject
3192				})));
3193			});
3194
3195			this.connector.send(JSON.stringify(requests));
3196			return promises;
3197		}
3198
3199		processRpcResponse(response)
3200		{
3201			if ("id" in response && this.rpcResponseAwaiters.has(response.id))
3202			{
3203				const awaiter = this.rpcResponseAwaiters.get(response.id)
3204				if ("result" in response)
3205				{
3206					awaiter.resolve(response.result)
3207				}
3208				else if ("error" in response)
3209				{
3210					awaiter.reject(response.error)
3211				}
3212				else
3213				{
3214					awaiter.reject(new Error("wrong response structure"))
3215				}
3216
3217				clearTimeout(awaiter.timeout)
3218				this.rpcResponseAwaiters.delete(response.id)
3219			}
3220			else
3221			{
3222				console.error("Received rpc response with unknown id", response)
3223			}
3224		}
3225
3226		parseJsonRpcMessage(message)
3227		{
3228			let decoded
3229			try
3230			{
3231				decoded = JSON.parse(message);
3232			} catch (e)
3233			{
3234				console.error(Utils.getDateForLog() + ": Pull: Could not decode json rpc message", e);
3235			}
3236
3237			if (Utils.isArray(decoded))
3238			{
3239				return this.executeIncomingRpcBatch(decoded);
3240			}
3241			else if (Utils.isJsonRpcRequest(decoded))
3242			{
3243				return this.executeIncomingRpcCommand(decoded);
3244			}
3245			else if (Utils.isJsonRpcResponse(decoded))
3246			{
3247				return this.processRpcResponse(decoded);
3248			}
3249			else
3250			{
3251				console.error(Utils.getDateForLog() + ": Pull: unknown rpc packet", decoded);
3252			}
3253		}
3254
3255		/**
3256		 * Executes RPC command, received from the server
3257		 *
3258		 * @param {string} method
3259		 * @param {object} params
3260		 * @returns {object}
3261		 */
3262		executeIncomingRpcCommand({method, params})
3263		{
3264			if (method in this.handlers)
3265			{
3266				return this.handlers[method].call(this, params)
3267			}
3268
3269			return {
3270				"error": RpcError.MethodNotFound
3271			}
3272		}
3273
3274		executeIncomingRpcBatch(batch)
3275		{
3276			let result = [];
3277			for (let command of batch)
3278			{
3279				if ("jsonrpc" in command)
3280				{
3281					if ("method" in command)
3282					{
3283						let commandResult = this.executeIncomingRpcCommand(command)
3284						if (commandResult)
3285						{
3286							commandResult["jsonrpc"] = JSON_RPC_VERSION;
3287							commandResult["id"] = command["id"];
3288
3289							result.push(commandResult)
3290						}
3291					}
3292					else
3293					{
3294						this.processRpcResponse(command)
3295					}
3296				}
3297				else
3298				{
3299					console.error(Utils.getDateForLog() + ": Pull: unknown rpc command in batch", command);
3300					result.push({
3301						"jsonrpc": "2.0",
3302						"error": RpcError.InvalidRequest,
3303					})
3304				}
3305			}
3306
3307			return result;
3308		}
3309
3310		nextId()
3311		{
3312			return ++this.idCounter;
3313		}
3314
3315		createPublishRequest(messageBatch)
3316		{
3317			let result = messageBatch.map(message => this.createRequest('publish', message));
3318
3319			if (result.length === 0)
3320			{
3321				return result[0]
3322			}
3323
3324			return result;
3325		}
3326
3327		createRequest(method, params, id)
3328		{
3329			if (!id)
3330			{
3331				id = this.nextId()
3332			}
3333
3334			return {
3335				jsonrpc: JSON_RPC_VERSION,
3336				method: method,
3337				params: params,
3338				id: id
3339			}
3340		}
3341	}
3342
3343	class ErrorNotConnected extends Error
3344	{
3345		constructor(message)
3346		{
3347			super(message);
3348			this.name = 'ErrorNotConnected';
3349		}
3350	}
3351
3352	class ErrorTimeout extends Error
3353	{
3354		constructor(message)
3355		{
3356			super(message);
3357			this.name = 'ErrorTimeout';
3358		}
3359	}
3360
3361	const Utils = {
3362		browser: {
3363			IsChrome: function () {
3364				return navigator.userAgent.toLowerCase().indexOf('chrome') != -1;
3365			},
3366			IsFirefox: function () {
3367				return navigator.userAgent.toLowerCase().indexOf('firefox') != -1;
3368			},
3369			IsIe: function () {
3370				return navigator.userAgent.match(/(Trident\/|MSIE\/)/) !== null;
3371			}
3372		},
3373		getTimestamp: function () {
3374			return (new Date()).getTime();
3375		},
3376		/**
3377		 * Reduces errors array to single string.
3378		 * @param {array} errors
3379		 * @return {string}
3380		 */
3381		errorsToString: function (errors) {
3382			if (!this.isArray(errors))
3383			{
3384				return "";
3385			}
3386			else
3387			{
3388				return errors.reduce(function (result, currentValue) {
3389					if (result != "")
3390					{
3391						result += "; ";
3392					}
3393					return result + currentValue.code + ": " + currentValue.message;
3394				}, "");
3395			}
3396		},
3397		isString: function (item) {
3398			return item === '' ? true : (item ? (typeof (item) == "string" || item instanceof String) : false);
3399		},
3400		isArray: function (item) {
3401			return item && Object.prototype.toString.call(item) == "[object Array]";
3402		},
3403		isFunction: function (item) {
3404			return item === null ? false : (typeof (item) == "function" || item instanceof Function);
3405		},
3406		isDomNode: function (item) {
3407			return item && typeof (item) == "object" && "nodeType" in item;
3408		},
3409		isDate: function (item) {
3410			return item && Object.prototype.toString.call(item) == "[object Date]";
3411		},
3412		isPlainObject: function (item) {
3413			if (!item || typeof (item) !== "object" || item.nodeType)
3414			{
3415				return false;
3416			}
3417
3418			const hasProp = Object.prototype.hasOwnProperty;
3419			try
3420			{
3421				if (item.constructor && !hasProp.call(item, "constructor") && !hasProp.call(item.constructor.prototype, "isPrototypeOf"))
3422				{
3423					return false;
3424				}
3425			} catch (e)
3426			{
3427				return false;
3428			}
3429
3430			let key;
3431			for (key in item)
3432			{
3433			}
3434			return typeof (key) === "undefined" || hasProp.call(item, key);
3435		},
3436		isNotEmptyString: function (item) {
3437			return this.isString(item) ? item.length > 0 : false;
3438		},
3439		isJsonRpcRequest: function (item) {
3440			return (
3441				typeof (item) === "object"
3442				&& item
3443				&& "jsonrpc" in item
3444				&& Utils.isNotEmptyString(item.jsonrpc)
3445				&& "method" in item
3446				&& Utils.isNotEmptyString(item.method)
3447			);
3448		},
3449		isJsonRpcResponse: function (item) {
3450			return (
3451				typeof (item) === "object"
3452				&& item
3453				&& "jsonrpc" in item
3454				&& Utils.isNotEmptyString(item.jsonrpc)
3455				&& "id" in item
3456				&& (
3457					"result" in item
3458					|| "error" in item
3459				)
3460			);
3461
3462		},
3463		buildQueryString: function (params) {
3464			let result = '';
3465			for (let key in params)
3466			{
3467				if (!params.hasOwnProperty(key))
3468				{
3469					continue;
3470				}
3471				const value = params[key];
3472				if (Utils.isArray(value))
3473				{
3474					value.forEach((valueElement, index) => {
3475						result += encodeURIComponent(key + "[" + index + "]") + "=" + encodeURIComponent(valueElement) + "&";
3476					});
3477				}
3478				else
3479				{
3480					result += encodeURIComponent(key) + "=" + encodeURIComponent(value) + "&";
3481				}
3482			}
3483
3484			if (result.length > 0)
3485			{
3486				result = result.substr(0, result.length - 1);
3487			}
3488
3489			return result;
3490		},
3491		objectValues: function values(obj) {
3492			let result = [];
3493			for (let key in obj)
3494			{
3495				if (obj.hasOwnProperty(key) && obj.propertyIsEnumerable(key))
3496				{
3497					result.push(obj[key]);
3498				}
3499			}
3500			return result;
3501		},
3502		clone: function (obj, bCopyObj) {
3503			let _obj, i, l;
3504			if (bCopyObj !== false)
3505			{
3506				bCopyObj = true;
3507			}
3508
3509			if (obj === null)
3510			{
3511				return null;
3512			}
3513
3514			if (this.isDomNode(obj))
3515			{
3516				_obj = obj.cloneNode(bCopyObj);
3517			}
3518			else if (typeof obj == 'object')
3519			{
3520				if (this.isArray(obj))
3521				{
3522					_obj = [];
3523					for (i = 0, l = obj.length; i < l; i++)
3524					{
3525						if (typeof obj[i] == "object" && bCopyObj)
3526						{
3527							_obj[i] = this.clone(obj[i], bCopyObj);
3528						}
3529						else
3530						{
3531							_obj[i] = obj[i];
3532						}
3533					}
3534				}
3535				else
3536				{
3537					_obj = {};
3538					if (obj.constructor)
3539					{
3540						if (this.isDate(obj))
3541						{
3542							_obj = new Date(obj);
3543						}
3544						else
3545						{
3546							_obj = new obj.constructor();
3547						}
3548					}
3549
3550					for (i in obj)
3551					{
3552						if (!obj.hasOwnProperty(i))
3553						{
3554							continue;
3555						}
3556						if (typeof obj[i] == "object" && bCopyObj)
3557						{
3558							_obj[i] = this.clone(obj[i], bCopyObj);
3559						}
3560						else
3561						{
3562							_obj[i] = obj[i];
3563						}
3564					}
3565				}
3566
3567			}
3568			else
3569			{
3570				_obj = obj;
3571			}
3572
3573			return _obj;
3574		},
3575
3576		getDateForLog: function () {
3577			const d = new Date();
3578
3579			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');
3580		},
3581
3582		lpad: function (str, length, chr) {
3583			str = str.toString();
3584			chr = chr || ' ';
3585
3586			if (str.length > length)
3587			{
3588				return str;
3589			}
3590
3591			let result = '';
3592			for (let i = 0; i < length - str.length; i++)
3593			{
3594				result += chr;
3595			}
3596
3597			return result + str;
3598		}
3599	}
3600
3601	if (
3602		typeof BX.namespace !== 'undefined'
3603		&& typeof BX.PULL === 'undefined'
3604	)
3605	{
3606		BX.PULL = new PullClient();
3607	}
3608
3609	BX.PullClient = PullClient;
3610	BX.PullClient.PullStatus = PullStatus;
3611	BX.PullClient.SubscriptionType = SubscriptionType;
3612	BX.PullClient.CloseReasons = CloseReasons;
3613	BX.PullClient.StorageManager = StorageManager;
3614})();

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.