PageSourceSearch

https://samtambooks.com/bitrix/js/pull/client/pull.client.js?169822970680598

js samtambooks.com collected 2026-10-02 18:45:38 UTC 80,598 bytes, 3,557 lines download raw bytes

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

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.