PageSourceSearch

https://izumclub.ru/bitrix/js/pull/client/pull.client.js?170664136283427

js izumclub.ru collected 2026-10-07 18:00:41 UTC 83,427 bytes, 3,608 lines download raw bytes

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

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.