PageSourceSearch

https://www.lode.by/bitrix/js/pull/client/pull.client.js?169702770881012

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

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.