PageSourceSearch

https://www.barrier.ru/bitrix/js/pull/client/pull.client.js?175016891083861

js barrier.ru collected 2026-10-01 15:07:49 UTC 83,861 bytes, 3,624 lines download raw bytes

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

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.