sbis3-node-ws
Version:
Модуль позволяет использовать ядро интерфейсного фреймворка sbis3-ws(`core.js`) и модуль работы с данными (`Source.js`) в приложении на nodejs.
1,126 lines (989 loc) • 38.3 kB
JavaScript
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;
});