UNPKG

sbis3-node-ws

Version:

Модуль позволяет использовать ядро интерфейсного фреймворка sbis3-ws(`core.js`) и модуль работы с данными (`Source.js`) в приложении на nodejs.

1,126 lines (989 loc) 38.3 kB
define('js!SBIS3.CORE.ServerEventBus', [ 'browser!cdn!/sockjs/1.0.3-p1/sockjs-min.js', 'js!SBIS3.CORE.ServerEventBus/resources/stomp' ], function(SockJS) { /** * @typedef {Object} ChannelInfo * @property {String} hashedNames[] * @property {Object} receipts * @property {Object} channels * @property {$ws.proto.Deferred} dHashes * @property {$ws.proto.Deferred} dSubscriptionDone * * @typedef {Object} Stomp * @property {Object} heartbeat * @property {Function} over * @property {Function} connect * @property {Function} subscribe * * @typedef {Object} ClientInfo * @property {Boolean} connected * @property {Stomp} client * @property {ChannelInfo} channels[] * * @typedef {Object} ServerChannelOptions * @property {String} channel Имя канала * @property {String} [host=CURRENT_HOST] Хост для подключения * @property {String} [vhost='/'] * @property {String} [scheme=CURRENT_SCHEME] Способ подключения (http/https) * @property {String} [dir='/stomp/'] Путь до Stomp */ var global = (function(){ return this || (0,eval)('this'); }()), STORAGE = global.localStorage, OLD_IE = $ws._const.browser.isIE8 || $ws._const.browser.isIE9, MULTIPLIER = OLD_IE ? 4 : 1, MODE_MASTERSLAVE = 1, MODE_MASTERONLY = 2, ID = $ws.helpers.randomId(), PREFIX = 'shared-bus', KEY_ACK = [ PREFIX, 'ack', ID ].join('-'), KEY_MASTER = [ PREFIX, 'master' ].join('-'), KEY_DEBUG = [ PREFIX, 'debug' ].join('-'), KEY_QUEUE = [ PREFIX, 'queue' ].join('-'), KEY_MASTER_READY = [ PREFIX, 'master-ready' ].join('-'), KEY_IE_FIX_EVENT = [ PREFIX, 'ie-fix-event' ].join('-'), KEY_DUMP = [ PREFIX, 'dump' ].join('-'), KEY_DUMP_NOTIFY = [ PREFIX, 'dump-notify' ].join('-'), KEY_PERSISTENT_ID = [ PREFIX, 'presistent-id'].join('-'), PERSISTENT_ID = STORAGE && STORAGE.getItem && STORAGE.getItem(KEY_PERSISTENT_ID) || null, ACK_INTERVAL = MULTIPLIER * 250, ACK_HEARTBEAT_INTERVAL = MULTIPLIER * 10000, ackMatch = new RegExp('^' + PREFIX + '-ack-(.+)$'), roleChangeChannel = new $ws.proto.EventBusChannel(), indic = [ '|', '/', '-', '\\' ], pos = 0, msgPassed = 0, browserDetection = $ws._const.browser, isMobile = browserDetection.isMobileAndroid || browserDetection.isMobileSafari || browserDetection.isIE12, mode = isMobile ? MODE_MASTERONLY : MODE_MASTERSLAVE, state, lookAroundTimer, shuttingDown, shuttingDownMsg, isMaster, localStorageReceiver, oldNeighbours, masterAckDesync = true; /** * Установлен ли режим вывода отладочных сообщений * Устанавливает/снимает режим вывода оладочных сообщений, * если передан первый параметр * @return {boolean} */ function isDebug() { if (arguments.length) { STORAGE.setItem(KEY_DEBUG, !!arguments[0]); } else { return STORAGE.getItem(KEY_DEBUG) === 'true'; } } function sharedBusDump() { if (Dispatcher) { var tabs = {}; var result = new $ws.proto.Deferred(); var dump = Dispatcher.getInstance().getDump(); function receiverDump(e) { if (e.key == KEY_DUMP && e.newValue) { try { var value = JSON.parse(e.newValue); tabs[value.current] = value; } catch(e) { //ignore } } } if (window.addEventListener) { window.addEventListener('storage', receiverDump); } else { document.attachEvent('onstorage', receiverDump); } tabs[dump.current] = dump; STORAGE.setItem(KEY_DUMP_NOTIFY, '1'); STORAGE.removeItem(KEY_DUMP_NOTIFY); setTimeout(function removeListener() { if (window.removeEventListener) { window.removeEventListener('storage', receiverDump); } else { document.detachEvent('onstorage', receiverDump); } result.callback(tabs); }, window.addEventListener ? 1000 : 5000); return result; } } function sharedBusDumpListener() { if (Dispatcher) { function receiveDump(e) { if (e.key == KEY_DUMP_NOTIFY && e.newValue) { try { // Заполним поле и сразу удалим, чтобы не занимать место STORAGE.setItem(KEY_DUMP, JSON.stringify(Dispatcher.getInstance().getDump())); STORAGE.removeItem(KEY_DUMP); } catch(e) { //ignore } } } if (window.addEventListener) { window.addEventListener('storage', receiveDump); } else { document.attachEvent('onstorage', receiveDump); } } } if (typeof window !== 'undefined') { window.sharedBusDump = sharedBusDump; window.sharedBusDebug = isDebug; } /** * Вывести в лог отладочное сообщение * Можно передавать строки через запятую */ function debug() { if (isDebug() && console) { console.log($ws.helpers.reduce(arguments, function(str, item) { return str + item; }, '')); } } /** * Выводит отладочную инфографику в заголовке * Формат заголовка: %id %s %m %i * %id - идентификатор вкладки * %s - состояние подключение: * + - вкладка мастер, еще нет подключения * - - вкалдка слейв * | - на каждом этапе подключения добавляется по палке * %m - количество полученных сообщений * %ш - индикатор lookAround */ function titleDebug() { if (isDebug()) { if (shuttingDown) { document.title = ID.substr(0, 10) + ' # ' + (shuttingDownMsg || ''); } else { var stateLog = isMaster && state && new Array(state + 1).join('|') || '+'; document.title = ID.substr(0, 10) + ' ' + (isMaster ? stateLog : '-') + ' ' + msgPassed + ' ' + indic[pos++ % indic.length]; } } } /** * Проверяет стали ли мы мастером или нет * Если статус нашей вкладки изменился, то стреляет * событие onRoleChange * @param {String} masterID - идентификатор мастера */ function gotMasterID(masterID) { if (shuttingDown) { return; } var isMeMaster = masterID == ID; // Наш статус изменился if (isMeMaster !== isMaster) { // Нас выбрали мастером. Создадим контроллер isMaster = isMeMaster; debug('Changing state to ' + isMaster); titleDebug(); roleChangeChannel.notify('onRoleChange', isMaster); } } /** * Получить пары ключ-значение (KEY_ACK) соседних вкладок * @return {Object} */ function getNeighbours() { // Возьмем всех соседей перебором из STORAGE // Себя не заносим в список соседей var neighbours = getKeys(ackMatch); delete neighbours[KEY_ACK]; return neighbours; } /** * Здесь проводится запись информации о себе и вычищение устаревших ключей * Если работает на мобильных устройствах, то считаем, что мы мастер всегда * Иначе надо следить за состоянием соседей и обновлять свое состояние: * * Постоянно обновляем ключь KEY_ACK * * Удаляем ключи зависших вкладок * * Проверяем, кто теперь мастер * Срабатывает по таймеру */ function lookAround() { if (shuttingDown) { return; } titleDebug(); if (mode == MODE_MASTERONLY) { // В режиме работы "только мастер" сразу объявляем себя мастером и успокаиваемся навсегда debug('Master only mode. Fallback to master without elections'); gotMasterID(ID); } else { // Расскажем о себе соседям STORAGE.setItem(KEY_ACK, +new Date()); $ws.helpers.forEach(getNeighbours(), function(ackTime, key) { if (+new Date() - ackTime < ACK_HEARTBEAT_INTERVAL) { // сосед живой } else { // сосед помер STORAGE.removeItem(key); } }); checkMaster(function() { lookAroundTimer = setTimeout(lookAround, ACK_INTERVAL); }); } } /** * Обработчик события onstorage * Следит, был ли почищен ключик KEY_ACK соседних вкладок * Если ключь был удален, проверяет, кто теперь мастер * * Событие срабатывает практически мгновенно на фоновых вкладках, * в отличии от таймера и lookAround * @param e */ function detectKeyAckRemove(e) { if (ackMatch.test(e.key)) { if (e.newValue === null && e.key !== KEY_ACK) { clearTimeout(lookAroundTimer); checkMaster(function() { lookAroundTimer = setTimeout(lookAround, ACK_INTERVAL); }); } } } function detectKeyAckRemove_OLD_IE() { var neighbours = getNeighbours(), needCheckMaster = false; // Проверим, не удалили ли кто ключик $ws.helpers.forEach(oldNeighbours, function(value, key) { if (neighbours[key] === undefined) { // Значит ключик удалили needCheckMaster = true; } }); oldNeighbours = neighbours; if (needCheckMaster) { clearTimeout(lookAroundTimer); checkMaster(function() { lookAroundTimer = setTimeout(lookAround, ACK_INTERVAL); }); } } function onRoleChange_OLD_IE(event, isMaster) { var receiver, checkQueue, queue = [], dup = this._createDuplicator(queue), self = this; checkQueue = function() { if (queue.length) { STORAGE.setItem(KEY_QUEUE, JSON.stringify(queue)); queue.length = 0; } self._interval = setTimeout(checkQueue, 500); }.bind(this); if (isMaster) { receiver = new StompReceiver(); // Дублируем приходящие события в STORAGE только при работе в режиме мастер-слейв if (mode == MODE_MASTERSLAVE) { receiver.subscribe('onFrame', dup); receiver.once('onReady', dup); checkQueue(); } } else { // FIXME а что если на момент смены ресивера в очереди еще будут пакеты? clearTimeout(this._interval); receiver = new localStorageReceiver(); } this._setReceiver(receiver); } function createDuplicator_OLD_IE(queue) { return function(event, frame) { if (event._eventName == 'onFrame') { queue.unshift({ type: 'frame', data: frame }); } else if (event._eventName == 'onReady') { queue.unshift({ type: 'ready' }); } } } /** * Здесь проверяется есть ли мастер и если нет, осуществляется попытка им стать * Алгоритм выбора мастера по наибльшему идентификатору, * чей идентификатор больше, тот и мастер * @param {Function} done - callback */ function checkMaster(done) { var master, masterAck; master = STORAGE.getItem(KEY_MASTER); masterAck = STORAGE.getItem([PREFIX, 'ack', master].join('-')); /*Если произошла расинхронизация вкладок слейв и мастер, например, по причине долгого выполнения кода, ключ мастера в localstorage слейва затирается. * Это провоцирует последнего объявить мастером себя. * Чтобы этого не происходило, дадим вкладкам синхронизироваться до следующего вызова checkMaster и если это не помогло, вернемся к дефолтному поведению. * */ if (master && masterAck == null && !masterAckDesync) { masterAckDesync = true; debug('Desynchronization of master and slave tabs'); return done(); } else { masterAckDesync = false; } if (master && (!masterAck || +new Date() - masterAck > ACK_HEARTBEAT_INTERVAL)) { debug('Current master is considered dead. Key: ', master, ' Last ACK ', masterAck, ' Delta ', +new Date() - masterAck); // Мастер объявлен, но от него нет ACK или он слишком старый STORAGE.removeItem(KEY_MASTER); master = null; } if (!master) { debug('No master declared. Trying to become master'); // Соберем сведенья о других вкладках, самый большой ключик и будет мастером master = KEY_ACK; $ws.helpers.forEach(getNeighbours(), function(ackTime, key) { if (key > master) { master = key; } }); master = master.replace([PREFIX, 'ack', ''].join('-'), ''); if (master == ID) { debug('Declaring myself master'); STORAGE.setItem(KEY_MASTER, master); } else { debug('The master is ', master, ' me is ', ID); } gotMasterID(master); done(); } else { gotMasterID(master); done(); } } /** * Получить все ключи из STORAGE, которые соответствуют re * @param {RegExp} re * @return {Object} */ function getKeys(re) { var rv = {}, l = STORAGE.length, i, key; for (i = 0; i < l; i++) { key = STORAGE.key(i); if (re.test(key)) { rv[key] = STORAGE.getItem(key); } } return rv; } /** * Общая функция завершения работы */ function shutdown(kill, msg) { clearTimeout(lookAroundTimer); shuttingDown = true; shuttingDownMsg = msg; titleDebug(); // Пока не будем чистить каналы Dispatcher.destroy(true); STORAGE.removeItem(KEY_ACK); if (isMaster) { STORAGE.removeItem(KEY_MASTER); STORAGE.removeItem(KEY_MASTER_READY); } if (!kill) { setTimeout(function() { setTimeout(function() { // Если мы остались на странице, надо опять оживить диспетчер shuttingDown = false; shuttingDownMsg = ''; isMaster = undefined; lookAroundTimer = setTimeout(lookAround, ACK_INTERVAL); }, 1000); }, 1); } } /** * Инициализцаия вкладки. Запуск lookAround */ var init = function() { if (!PERSISTENT_ID) { // Создадим ключ для мастера PERSISTENT_ID = $ws.helpers.createGUID(); STORAGE.setItem(KEY_PERSISTENT_ID, PERSISTENT_ID); } if (!OLD_IE) { window.addEventListener('beforeunload', function() { shutdown(false, "BEFORE UNLOAD"); }); localStorageReceiver = LocalStorageReceiver; /** * В режиме мастер слейв будем ловить закрытие вкладок */ if (mode !== MODE_MASTERONLY) { window.addEventListener('storage', detectKeyAckRemove); } } else { window.attachEvent('onbeforeunload', function() { shutdown(false, "BEFORE UNLOAD"); }); localStorageReceiver = LocalStorageReceiver_OLD_IE; Dispatcher.prototype._onRoleChange = onRoleChange_OLD_IE; Dispatcher.prototype._createDuplicator = createDuplicator_OLD_IE; /** * В режиме мастер слейв будем ловить закрытие вкладок */ if (mode !== MODE_MASTERONLY) { oldNeighbours = getNeighbours(); document.attachEvent('onstorage', detectKeyAckRemove_OLD_IE); } } /** * IE10 при клике на ссылку с href=javascript:void(0) делает onbeforeunload * Чтобы это предотвратить, на capture-фазе захватим все клики, и если они от ссылки с нужным нам href * то сделаем preventDefault */ if (browserDetection.IEVersion === 10) { document.body.addEventListener('click', function(e) { var href = e.target.getAttribute('href'); if (href && href.indexOf('javascript:void(0)') === 0) { e.preventDefault(); } }, true); } sharedBusDumpListener(); lookAround(); }.once(); /** * Метод задаёт канал, в который публикуется событие. * @param {String} channelName Имя канала * @param {Object} [opts.exclusive] При задании значения true будет создано своё собственное подключение к серверу. * @returns {$ws.proto.EventBusChannel} * @example * <pre> * define('js!SBIS3.MyArea.ModuleName', [..., 'js!SBIS3.CORE.ServerEventBus'], function(..., bus) { * // в модуле должна быть объявлена именованная функция * // на неё производим подписку или отписку * function MyHandler (event, data) { * // event - объект события (payload) * // data - фрейм WebSocket-сообщения * if (data === "Поощрение") { * updatePromotion(); * } * if (data === "Взыскание") { * updatePenalty(); * } * } * var moduleClass = CompoundControl.extend(// @lends SBIS3.MyArea.ModuleName.prototype * { * //... * bus.serverChannel('ИнформерПоощренийИВзысканий').subscribe('onReady', function() { * this.subscribe('onMessage', MyHandler); * }); * //... * }); * }); * </pre> */ function serverChannel(channelName, opts) { var exclusive = opts && opts.exclusive; return Dispatcher.getInstance(exclusive).channel(channelName); } //<editor-fold desc="Receiver"> /** * @class Receiver * @public * @constructor */ function Receiver() { this._channel = $ws.single.EventBus.channel(); this._dReady = new $ws.proto.Deferred(); this._dReady.addCallback(function() { this._channel.notify('onReady'); }.bind(this)); this._channel.setEventQueueSize('onReady', 1); this._destroyed = false; } Receiver.prototype.ready = function() { if (!this._dReady.isReady()) { this._dReady.callback(); } }; Receiver.prototype.frame = function(frame) { msgPassed++; titleDebug(); this._channel.notify('onFrame', frame); }; Receiver.prototype.subscribe = function(event, handler, ctx) { this._channel.subscribe(event, handler, ctx); }; Receiver.prototype.once = function(event, handler, ctx) { this._channel.once(event, handler, ctx); }; Receiver.prototype.destroy = function() { this._channel.destroy(); this._dReady = null; this._destroyed = true; }; //</editor-fold> //<editor-fold desc="StompReceiver"> var // проверяем живость сервиса каждую минуту STOMP_HEARTBEAT_OUT = 60000, // не проверяем ответы STOMP_HEARTBEAT_IN = 0, LOGIN = 'cloud_service', PASSWORD = 'cloud_service', sid = typeof window === 'undefined' ? '' : $.cookie('sid'), dHashes; var PROTOCOLS = [ 'xhr-streaming', 'xhr-polling', 'xdr-streaming', 'xdr-polling', 'iframe-xhr-polling' ]; if (typeof window !== 'undefined' && window.WebSocket) { PROTOCOLS.unshift('websocket'); } function getSubscriptionHashes() { if (!dHashes) { dHashes = $ws.single.ioc.resolve('ITransport', { url: '/!hash/', method: 'GET', dataType: 'json' }).execute().addCallback(function(response) { return response.result; }); } return dHashes; } /** * @class StompReceiver * @extends Receiver * @constructor * @public */ function StompReceiver(exclusive) { Receiver.call(this); this._exclusive = exclusive; this._sessionMonitor = null; this._reconnectFailures = 0; if (!this._exclusive) { STORAGE.removeItem(KEY_MASTER_READY); } this._setupSessionChangeMonitor(); this._reconnect(); } $ws.core.classicExtend(StompReceiver, Receiver); StompReceiver.prototype._setupSessionChangeMonitor = function() { if (sid !== $.cookie('sid')) { sid = $.cookie('sid'); dHashes = null; } clearInterval(this._sessionMonitor); this._sessionMonitor = setInterval(function() { if (sid !== $.cookie('sid')) { dHashes = null; var cookieSID = $.cookie('sid'), w = (sid && sid.split("-")) || [], n = (cookieSID && cookieSID.split("-")) || []; if (!cookieSID || w[0] !== n[0] || w[1] !== n[1]) { // Больше не проверяем sid clearInterval(this._sessionMonitor); shutdown(true, "CHANGE SID"); // Дернем метод БЛ, чтобы стрельнуть ошибкой авторизации getSubscriptionHashes(); } else { sid = $.cookie('sid'); this._reconnect(); } } }.bind(this), 1000); }; StompReceiver.prototype._reconnectInterval = function() { // Максимальный интервал переподключения - 1 час return Math.min(Math.exp(this._reconnectFailures / 2) * 300 + 1000, 60 * 60 * 1000); }; StompReceiver.prototype._reconnect = function() { this._disconnect(); this._createConnection(); }; StompReceiver.prototype.frame = function(frame) { StompReceiver.superclass.frame.call(this, frame); if (localStorage.getItem('allowAck') == 'true' && this._client.ws.transport == 'websocket') { frame.ack(); } }; StompReceiver.prototype._createConnection = function() { var sid = $.cookie('sid'), host = window.location.host, scheme = window.location.protocol, dir = '/stomp/s-' + sid + '/', stompUrl = (scheme.indexOf(':') == -1 ? scheme + ':' : scheme) + '//' + ('stomp-' + host) + dir, self = this, dSubHashes, ws, client; if (!sid || sid == 'null') { shutdown(true, 'EMPTY SID'); return; } dSubHashes = getSubscriptionHashes(); // Костыль для удаления лидирующего "admin-" из имени exchange host = host.match(/^(?:admin-|xp-)?([\S\s]*)$/)[1]; state = 1; // Получаем hash, а потом коннектимся dSubHashes.addCallback(function(hashes) { state = 2; ws = new SockJS(stompUrl, null, { transports: PROTOCOLS }); state = 3; client = Stomp.over(ws); client.heartbeat.outgoing = STOMP_HEARTBEAT_OUT; client.heartbeat.incoming = STOMP_HEARTBEAT_IN; client.debug = function(msg) { if (sharedBusDebug()) { console.log('Stomp: ' + msg); } }; self._client = client; state = 4; client.connect(LOGIN, PASSWORD, function() { state = 5; var hash = hashes.user, persistent = mode !== MODE_MASTERONLY && !self._exclusive, headers = { receipt: hash, id: persistent ? PERSISTENT_ID + '-' + hash : '', persistent: persistent, 'auto-delete': !persistent }; if (localStorage.getItem('allowAck') == 'true' && client.ws.transport == 'websocket') { headers.ack = 'client'; } client.onreceipt = function(frame) { var receiptID = frame.headers['receipt-id']; state = 6; if (receiptID == hash) { state = 7; self.ready(); } }; client.subscribe( '/exchange/' + host + ':' + hash, self.frame.bind(self), headers ); }, function() { if (!client.connected) { self._reconnectFailures++; var interval = self._reconnectInterval(); if (sharedBusDebug()) { console.log('ServerEventBus', 'Planning to reconnect in ' + interval, stompUrl); } self._reconnectTimeout = setTimeout(function() { self._createConnection(); }, interval); } else { //ignore } }, '/'); return hashes; }).addErrback(function() { shutdown(true, 'NOT AUTH'); }); }; StompReceiver.prototype._disconnect = function() { clearTimeout(this._reconnectTimeout); if (this._client) { this._client.disconnect(); this._client = null; } }; StompReceiver.prototype.destroy = function() { this._disconnect(); STORAGE.removeItem(KEY_MASTER); if (!this._exclusive) { STORAGE.removeItem(KEY_MASTER_READY); } StompReceiver.superclass.destroy.apply(this, arguments); }; StompReceiver.prototype.ready = function() { if (!this._exclusive) { STORAGE.setItem(KEY_MASTER_READY, ID); } StompReceiver.superclass.ready.apply(this, arguments); }; //</editor-fold> //<editor-fold desc="LocalStorageReceiver"> /** * @class LocalStorageReceiver * @extends Receiver * @public * * @constructor */ function LocalStorageReceiver() { Receiver.call(this); var self = this; state = 0; var event = ''; this._storageListener = function(e) { function notify(value) { event = JSON.parse(value || '{}'); if (event.type == 'frame') { self.frame(event.data); } else if (event.type == 'ready') { self.ready(); } event = null; } if (!$ws._const.browser.isIE && e.key == KEY_QUEUE) { notify(e.newValue || '{}'); } else if (e.key == KEY_IE_FIX_EVENT) { notify(STORAGE.getItem(KEY_QUEUE) || '{}'); } }; var masterId = STORAGE.getItem(KEY_MASTER), masterReady = STORAGE.getItem(KEY_MASTER_READY); if (masterReady && masterReady == masterId) { this.ready(); } window.addEventListener('storage', this._storageListener); } $ws.core.classicExtend(LocalStorageReceiver, Receiver); LocalStorageReceiver.prototype.destroy = function() { window.removeEventListener('storage', this._storageListener); LocalStorageReceiver.superclass.destroy.apply(this, arguments); }; //</editor-fold> //<editor-fold desc="LocalStorageReceiver_OLD_IE"> /** * @class LocalStorageReceiver_OLD_IE * @extends Receiver * @public * * @constructor */ function LocalStorageReceiver_OLD_IE() { Receiver.call(this); var self = this, oldQueue = ''; state = 0; this._storageListener = function() { var queue = STORAGE.getItem(KEY_QUEUE) || '[]'; if (oldQueue !== queue) { if (!self._destroyed) { var ourQueue = JSON.parse(queue), event; while (ourQueue && (event = ourQueue.pop())) { if (event.type == 'frame') { self.frame(event.data); } else if (event.type == 'ready') { self.ready(); } } } oldQueue = queue; } }; var masterId = STORAGE.getItem(KEY_MASTER), masterReady = STORAGE.getItem(KEY_MASTER_READY); if (masterReady && masterReady == masterId) { this.ready(); } document.attachEvent('onstorage', this._storageListener); } $ws.core.classicExtend(LocalStorageReceiver_OLD_IE, Receiver); LocalStorageReceiver_OLD_IE.prototype.destroy = function() { document.detachEvent('onstorage', this._storageListener); LocalStorageReceiver_OLD_IE.superclass.destroy.apply(this, arguments); }; //</editor-fold> //<editor-fold desc="Dispatcher"> function jsonReviver(key, value) { if ($ws.helpers.detectCommonDataContainer(value)) { return $ws.helpers.commonDataContainerReviver(key, value); } else { return value; } } function Dispatcher(exclusive) { init(); this._channels = {}; this._channelsFrameCounter = {}; this._receiver = null; this._dReady = new $ws.proto.Deferred(); if (exclusive) { this._setReceiver(new StompReceiver(true)); return; } roleChangeChannel.subscribe('onRoleChange', this._onRoleChange, this); } Dispatcher.prototype._onRoleChange = function(event, isMaster) { var receiver, dup = this._createDuplicator(); if (isMaster) { receiver = new StompReceiver(); // Дублируем приходящие события в STORAGE только при работе в режиме мастер-слейв if (mode == MODE_MASTERSLAVE) { receiver.subscribe('onFrame', dup); receiver.once('onReady', dup); } } else { receiver = new localStorageReceiver(); } this._setReceiver(receiver); }; Dispatcher.prototype._createDuplicator = function() { return function(event, frame) { if (event._eventName == 'onFrame') { STORAGE.setItem(KEY_QUEUE, JSON.stringify({ type: 'frame', data: frame })); // Фикс для IE, т.к событие onstorage не стрельнет, если длина сообщения больше 2143 символа if ($ws._const.browser.isIE) { STORAGE.setItem(KEY_IE_FIX_EVENT, '1'); } } else if (event._eventName == 'onReady') { STORAGE.setItem(KEY_QUEUE, JSON.stringify({ type: 'ready' })); } } }; Dispatcher.prototype._setReceiver = function(receiver) { if (this._receiver) { this._receiver.destroy(); } this._receiver = receiver; receiver.subscribe('onFrame', function(e, frame) { var eventName = frame.headers && frame.headers['event-type'], body; if (eventName) { if (eventName in this._channels) { body = JSON.parse(frame.body, jsonReviver); this._channels[eventName].notify('onMessage', body, frame); this._channelsFrameCounter[eventName].count += this._channelsFrameCounter[eventName].count < 10000 ? 1 : 0; this._channelsFrameCounter[eventName].lastFrameReceiveDate = new Date(); } } }, this); receiver.once('onReady', function() { if (!this._dReady.isReady()) { this._dReady.callback(); } }, this) }; Dispatcher.prototype.channel = function(eventName) { if (!eventName) { throw new Error('Channel name can\'t be empty and must be a string. You use ' + eventName); } var channel, readableEventName = eventName; eventName = eventName.toLocaleLowerCase(); if (!(eventName in this._channels)) { channel = this._channels[eventName] = $ws.single.EventBus.channel(readableEventName); channel.setEventQueueSize('onReady', 1); this._channelsFrameCounter[eventName] = { count: 0, lastFrameReceiveDate: null }; } else { channel = this._channels[eventName]; } this._dReady.addCallback(function() { channel.notify('onReady'); }); return channel; }; Dispatcher.prototype.destroy = function(saveChannel) { if (this._receiver) { this._receiver.destroy(); } this._receiver = null; if (!saveChannel) { $ws.helpers.forEach(this._channels, function(chnl) { chnl.destroy(); }); this._channels = {}; // Всегда должно быть объектом this._channelsFrameCounter = {}; } }; Dispatcher.prototype.getDump = function() { // Соберем всю информацию о подключении var self = this; var dump = {}; // 0. Время dump.time = new Date().toLocaleString(); // 1. Узнаем кто мастер dump.master = STORAGE.getItem(KEY_MASTER); // 2. Готов ли мастер dump.masterReady = STORAGE.getItem(KEY_MASTER_READY); // 3. Выведем свой ID dump.current = ID; // 4. Выведем состояние подключение наше (не SockJS) dump.state = state; // 5. Заершали ли работу SEB dump.shuttingDown = shuttingDown; // 6. Сообщение о завершении работы, может быть мы сменили SID // То что страница релоадится по onBeforeUnload мы не получим ( dump.shuttingDownMsg = shuttingDownMsg; // 7. Получим список соседей и их состояние dump.neighbours = {}; $ws.helpers.forEach(getNeighbours(), function(ackTime, key) { dump.neighbours[key.replace(PREFIX + '-ack-', '')] = (+new Date() - ackTime < ACK_HEARTBEAT_INTERVAL) ? 'alive' : 'dead'; }); // 8. Список каналов dump.channels = {}; $ws.helpers.forEach(this._channels, function(item, key) { var tmp = self._channelsFrameCounter[key]; dump.channels[key] = { count: tmp.count, lastFrameReceiveDate: tmp.lastFrameReceiveDate ? tmp.lastFrameReceiveDate.toLocaleString() : '' } }); if (this._receiver) { // 9. Стрельнул ли onReady dump.ready = this._receiver._dReady.isReady(); // 10. Живы ли мы еще dump.destroyed = this._receiver._destroyed; // 11. Если мы мастер, то получим состояние подключения if (this._receiver instanceof StompReceiver) { dump.connectionState = this._receiver._client ? this._receiver._client.ws.readyState : 0; dump.connectionTransport = this._receiver._client ? this._receiver._client.ws.transport : 0; } } return dump; }; /** * * @returns {Dispatcher} */ Dispatcher.getInstance = function(exclusive) { if (exclusive) { if (!this._instanceExclusive) { this._instanceExclusive = new Dispatcher(true); } return this._instanceExclusive; } else { if (!this._instance) { this._instance = new Dispatcher(); } return this._instance; } }; Dispatcher.destroy = function(saveChannel) { if (this._instance) { this._instance.destroy(saveChannel); } if (this._instanceExclusive) { this._instanceExclusive.destroy(saveChannel); } }; //</editor-fold> roleChangeChannel.setEventQueueSize(1); $ws.single.EventBus.serverChannel = serverChannel; return $ws.single.EventBus; });