UNPKG

wormhole.js

Version:

Wormhole - is EventEmitter for communication between tabs.

1,687 lines (1,334 loc) 34.2 kB
(function (window, document) { "use strict"; var now = Date.now || /* istanbul ignore next */ function () { return +(new Date); }; var floor = Math.floor, random = Math.random ; function s4() { return floor(random() * 0x10000 /* 65536 */).toString(16); } /** * UUID — http://ru.wikipedia.org/wiki/UUID * @returns {String} */ function uuid() { return (s4() + s4() + "-" + s4() + "-" + s4() + "-" + s4() + "-" + s4() + s4() + s4()); } /** * Генерация hash на основе строки * @param {String} str * @returns {String} */ uuid.hash = function (str) { var hash = 0, i = 0, length = str.length ; /* istanbul ignore else */ if (length > 0) { for (; i < length; i++) { hash = ((hash << 5) - hash) + str.charCodeAt(i); hash |= 0; // Convert to 32bit integer } } return hash.toString(36); }; function debounce(func, delay, immediate) { var timeout; return function() { var context = this, args = arguments; clearTimeout(timeout); timeout = setTimeout(function() { timeout = null; /* istanbul ignore else */ if (!immediate) { func.apply(context, args); } }, delay); /* istanbul ignore next */ if (immediate && !timeout) { func.apply(context, args); } }; } var __emitter__ = '__emitter__'; function getListeners(obj, name) { if (obj[__emitter__] === void 0) { obj[__emitter__] = {}; } obj = obj[__emitter__]; if (obj[name] === void 0) { obj[name] = []; } return obj[name]; } /** * @class Emitter * @desc Микро-излучатель */ function Emitter() { } Emitter.fn = Emitter.prototype = /** @lends Emitter.prototype */ { /** * Подписаться на событие * @param {String} name * @param {Function} fn * @returns {Emitter} */ on: function (name, fn) { var list = getListeners(this, name); list.push(fn); return this; }, /** * Отписаться от событие * @param {String} name * @param {Function} fn * @returns {Emitter} */ off: function (name, fn) { if (name === void 0) { delete this[__emitter__]; } else { var list = getListeners(this, name), i = list.length; while (i--) { // Ищем слушателя и удаляем (indexOf - IE > 8) if (list[i] === fn) { list.splice(i, 1); break; } } } return this; }, /** * Подписаться на событие и отписаться сразу после его получения * @param {String} name * @param {Function} fn * @returns {Emitter} */ one: function (name, fn) { var proxy = function () { this.off(name, proxy); return fn.apply(this, arguments); }; return this.on(name, proxy); }, /** * Распространить данные * @param {String} name * @param {*} [args] */ emit: function (name, args) { var listeners = getListeners(this, name), i = listeners.length, nargs ; args = (arguments.length === 1) ? [] : [].concat(args); nargs = args.length; while (i--) { if (nargs === 0) { listeners[i].call(this); } else if (nargs === 1){ listeners[i].call(this, args[0]); } else if (nargs === 2){ listeners[i].call(this, args[0], args[1]); } else { listeners[i].apply(this, args); } } } }; /** * Подмешать методы * @param {*} target * @returns {*} * @method */ Emitter.apply = function (target) { target.on = Emitter.fn.on; target.off = Emitter.fn.off; target.one = Emitter.fn.one; target.emit = Emitter.fn.emit; return target; }; Emitter.getListeners = getListeners; function getOwn(obj, prop) { return !(prop in getOwn) && obj && obj.hasOwnProperty(prop) ? obj[prop] : null; } var _corsId = 1, _corsExpando = '__cors__', _corsCallback = {}, _parseJSON = JSON.parse, _stringifyJSON = JSON.stringify, _allowAccess = void 0 ; /** * @class cors * @desc Обертка над postMessage * @param {Window} el */ function cors(el) { if (!(this instanceof cors)) { return new cors(el); } this.el = el; } cors.fn = cors.prototype = /** @lends cors.prototype */ { /** * Вызывать удаленную команду * @param {String} cmd команда * @param {*} [data] данные * @param {Function} [callback] функция обратного вызова, получает: `error` и `result` */ call: function (cmd, data, callback) { if (typeof data === 'function') { callback = data; data = void 0; } var evt = { cmd: cmd, data: data }; evt[_corsExpando] = ++_corsId; _corsCallback[_corsId] = callback; this.send(evt); }, /** * Отправить даныне * @param {*} data */ send: function (data) { var window = this.el; try { // Если это iframe window = window.contentWindow || /* istanbul ignore next */ window; } catch (err) { } try { window.postMessage(_corsExpando + _stringifyJSON(data), '*'); } catch (err) {} } }; /** * Разрешение для конкретного `origin` * @param {*} origin */ cors.allowAccess = function (origin) { if (typeof origin === 'string' || origin instanceof RegExp) { _allowAccess = origin; } }; /** * Установка кастомного префикса `expando` * @param {String} expando */ cors.setExpando = function (expando) { if (typeof expando === 'string') { _corsExpando = expando; } }; /** * Проверка на соответствие `targetOrigin` * @param {*} targetOrigin * @private */ function _checkAccess(targetOrigin) { if (_allowAccess == void 0) { return true; } else if (_allowAccess instanceof RegExp) { return _allowAccess.test(targetOrigin); } else if (typeof _allowAccess === 'string') { return targetOrigin === _allowAccess; } return false; } /** * Получение `postMessage` * @param {Event} evt * @private */ function _onmessage(evt) { var origin, id, resp = {}, data = evt.data, source = evt.source, func; evt = evt || /* istanbul ignore next */ window.event; origin = evt.origin || evt.originalEvent.origin; /* istanbul ignore else */ if (typeof data === 'string' && data.indexOf(_corsExpando) === 0 && _checkAccess(origin)) { // Наше сообщение try { // Парсим данные data = _parseJSON(evt.data.substr(_corsExpando.length)); id = data[_corsExpando]; if (id) { // Это call или ответ на него if (data.response) { /* istanbul ignore else */ if (_corsCallback[id]) { _corsCallback[id](data.error, data.result); delete _corsCallback[id]; } } else { // Фомируем ответ resp.response = resp[_corsExpando] = id; try { func = getOwn(cors, data.cmd); if (func) { resp.result = func(data.data, source); } else { throw 'method not found'; } } catch (err) { resp.error = 'wormhole.cors.' + data.cmd + ': ' + err.toString(); } cors(evt.source).send(resp); } } else { cors.emit('data', [data, source]); } } catch (err) { /* istanbul ignore next */ cors.emit('error', err); } } } // Подмешиваем Emitter.apply(cors); /* istanbul ignore else */ if (window.addEventListener) { window.addEventListener('message', _onmessage, false); } else { window.attachEvent('onmessage', _onmessage); } var store, _storage, _storageNS = '__wh.store__.', _storageData = {}, // key => Object _storageItems = {}, // key => String _parseJSON = JSON.parse, _stringifyJSON = JSON.stringify ; function _storageKey(key) { return _storageNS + key; } function _isStoreKey(key) { return key && (key !== _storageNS) && (key.indexOf(_storageNS) === 0); } function _getCleanedKey(key) { return key.substr(_storageNS.length); } /** * Получить рабочий storage по названию * @param {String} name * @returns {sessionStorage} * @private */ function _getStorage(name) { try { var storage = window[name + 'Storage']; storage.setItem(_storageNS, _storageNS); /* istanbul ignore else */ if (storage.getItem(_storageNS) == _storageNS) { storage.removeItem(_storageNS); return storage; } } catch (err) { } } // Пробуем получить sessionStorage, либо localStorage _storage = _getStorage('local'); /** * @desc Хранилище * @module {store} */ store = Emitter.apply(/** @lends store */{ /** * Статус хранилища * @type {boolean} */ enabled: !!_storage, /** * Установить значение * @param {String} key * @param {*} value */ set: function (key, value) { var fullKey = _storageKey(key); value = _stringifyJSON(value); _storage && _storage.setItem(fullKey, value); _onsync({ key: fullKey }, value); // принудительная синхронизация }, /** * Получить значение * @param {String} key * @returns {*} */ get: function (key) { var value = _storage.getItem(_storageKey(key)); return typeof value === 'string' ? _parseJSON(value) : value; }, /** * Удалить значение * @param {String} key */ remove: function (key) { delete _storageData[key]; delete _storageItems[key]; _storage && _storage.removeItem(_storageKey(key)); }, /** * Получить все данные из хранилища * @retruns {Array} */ getAll: function () { var i = 0, n, key, data = {}; if (_storage) { n = _storage.length; for (; i < n; i++ ) { key = _storage.key(i); if (_isStoreKey(key)) { data[_getCleanedKey(key)] = _parseJSON(_storage.getItem(key)); } } } return data; }, /** * Пройтись по всем ключам * @param {Function} iterator */ each: function (iterator) { if (_storage) { for (var i = 0, n = _storage.length, key; i < n; i++) { key = _storage.key(i); if (_isStoreKey(key)) { iterator(_parseJSON(_storage.getItem(key)), _getCleanedKey(key)); } } } } }); /** * Обработчик обновления хранилища * @param {Event|Object} evt * @param {String} [value] * @private */ function _onsync(evt, value) { var i = 0, n = _storage.length, fullKey = evt.key, key; // Синхронизация работает store.events = true; if (!fullKey) { // Плохой браузер, придется искать самому, что изменилось for (; i < n; i++ ) { fullKey = _storage.key(i); if (_isStoreKey(fullKey)) { value = _storage.getItem(fullKey); if (_storageItems[fullKey] !== value) { _storageItems[fullKey] = value; _onsync({ key: fullKey }, value); } } } } else if (_isStoreKey(fullKey)) { key = _getCleanedKey(fullKey); if (key) { // Фильтруем событий при проверки localStorage value = value !== void 0 ? value : _storage.getItem(fullKey); _storageData[key] = _parseJSON(value); _storageItems[fullKey] = value + ''; store.emit('change', [key, _storageData]); store.emit('change:' + key, [key, _storageData[key]]); } } } // Получаем текущее состояние _storage && (function () { var i = _storage.length, fullKey, key, value, _onsyncNext = function (evt) { setTimeout(function () { _onsync(evt); }, 0); }; /* istanbul ignore next */ while (i--) { fullKey = _storage.key(i); if (_isStoreKey(fullKey)) { key = _getCleanedKey(fullKey); value = _storage.getItem(fullKey); _storageData[key] = _parseJSON(value); _storageItems[fullKey] = value; } } /* istanbul ignore else */ if (window.addEventListener) { window.addEventListener('storage', _onsyncNext); document.addEventListener('storage', _onsyncNext); } else { window.attachEvent('onstorage', _onsyncNext); document.attachEvent('onstorage', _onsyncNext); } // Проверяем рабочесть события хранилища (Bug #136356) // _storage.setItem('ping', _storageNS); // setTimeout(function () { // _storage.removeItem('ping' + _storageNS); // // if (!store.events) { // console.log('onStorage not supported:', location.href, store.events); // setInterval(function () { _onsync({}); }, 250); // } // }, 500); })(); /** * Получить удаленное хранилище * @param {string} url * @param {function} ready * @returns {store} */ store.remote = function (url, ready) { var _data = {}, _store = Emitter.apply({ set: function (key, name) { _data[key] = name; _store.emit('change', [key, _data]); _store.emit('change:' + key, [key, _data[key]]); }, get: function (key) { return _data[key]; }, remove: function (key) { delete _data[key]; }, getAll: function () { return _data; }, each: function (iterator) { for (var key in _data) { if (_data.hasOwnProperty(key)) { iterator(_data, key); } } } }), iframe = document.createElement('iframe'), adapter = cors(iframe); iframe.onload = function () { adapter.call('register', [], function (err, storeData) { if (storeData) { iframe.onload = null; // Получаем данные хранилища for (var key in storeData) { _data[key] = storeData[key]; } // Получаем данные от iframe cors.on('data', function (evt) { var key = evt.key, data = evt.data, value = data[key]; _data[key] = value; _store.emit('change', [key, data]); _store.emit('change:' + key, [key, value]); }); // Установить _store.set = function (key, value) { adapter.call('store', { cmd: 'set', key: key, value: value }); }; // Удалить _store.remove = function (key) { delete _data[key]; adapter.call('store', { cmd: 'remove', key: key }); }; ready && ready(_store); } }); }; iframe.src = url; iframe.style.left = '-1000px'; iframe.style.position = 'absolute'; // Пробуем вставить в body (function _tryAgain() { try { document.body.appendChild(iframe); } catch (err) { setTimeout(_tryAgain, 100); } })(); return _store; }; var _stringifyJSON = JSON.stringify; /** * @type {URL} */ var URL = window.URL; /** * @type {Blob} */ var Blob = window.Blob; /** * @type {SharedWorker} */ var SharedWorker = window.SharedWorker; /* istanbul ignore next */ var Worker = { support: !!(URL && Blob && SharedWorker), /** * Создать работника * @param {String} url * @returns {SharedWorker} */ create: function (url) { return new SharedWorker(url); }, /** * Получить ссылку на работника * @param {String} id * @returns {String} * @private */ getSharedURL: function (id) { // Код воркера var source = '(' + (function (window) { var ports = []; var master = null; function checkMaster() { if (!master && (ports.length > 0)) { master = ports[0]; master.postMessage('MASTER'); } } function broadcast(data) { ports.forEach(function (port) { port.postMessage(data); }); } function removePort(port) { var idx = ports.indexOf(port); if (idx > -1) { ports.splice(idx, 1); peersUpdated(); } if (port === master) { master = null; } } function peersUpdated() { broadcast({ type: 'peers', data: ports.map(function (port) { return port.holeId; }) }); } // Опрашиваем и ищем зомби setTimeout(function next() { var i = ports.length, port; while (i--) { port = ports[i]; if (port.zombie) { // Убиваем зомби removePort(port); } else { port.zombie = true; // Помечаем как зомби port.postMessage('PING'); } } checkMaster(); setTimeout(next, 500); }, 500); window.addEventListener('connect', function (evt) { var port = evt.ports[0]; port.onmessage = function (evt) { var data = evt.data; if (data === 'PONG') { port.zombie = false; // живой порт } else if (data === 'DESTROY') { // Удаляем порт removePort(port); checkMaster(); } else if (data.hole) { // Обновление meta информации port.holeId = data.hole.id; peersUpdated(); } else { broadcast({ type: data.type, data: data.data }); } }; ports.push(port); port.start(); port.postMessage('CONNECTED'); checkMaster(); }, false); }).toString() + ')(this, ' + _stringifyJSON(name) + ')'; return URL.createObjectURL(new Blob([source], {type: 'text/javascript'})); } }; var PEER_UPD_DELAY = 5 * 1000, // ms, как часто обновлять данные j gbht MASTER_VOTE_DELAY = 500, // ms, сколько времени считать мастер живым MASTER_DELAY = PEER_UPD_DELAY * 2, // ms, сколько времени считать мастер живым PEERS_DELAY = PEER_UPD_DELAY * 4, // ms, сколько времени считать peer живым QUEUE_WAIT = PEER_UPD_DELAY * 2, // ms, за какой период времени держать очередь событий _emitterEmit = Emitter.fn.emit ; /** * Проверка наличия элемента в массиве * @param {Array} array * @param {*} value * @returns {number} * @private */ function _inArray(array, value) { var i = array.length; while (i--) { if (array[i] === value) { return i; } } return -1; } /** * Выполнить команду * @param {Hole} hole * @param {Object} cmd * @private */ function _execCmd(hole, cmd) { var fn = getOwn(hole, cmd.name); var next = function (err, result) { cmd.error = err; cmd.result = result; cmd.response = true; // console.log('emit.res.cmd', cmd.name); hole.emit('CMD', cmd); }; try { if (typeof fn === 'function') { if (fn.length === 2) { // Предпологается асинхронная работа fn(cmd.data, next); } else { next(null, fn(cmd.data)); } } else { throw 'method not found'; } } catch (err) { next('wormhole.' + cmd.name + ': ' + err.toString()); } } /** * @class Hole * @extends Emitter * @desc «Дырка» — общение между табами * @param {url} url * @param {Boolean} [useStore] использовать store */ function Hole(url, useStore) { var _this = this; _this._destroyUnload = /* istanbul ignore next */ function () { _this.destroy(); }; /** * Идентификатор * @type {String} */ _this.id = uuid(); /** * Объект хранилища * @type {store} */ _this.store; /** * Название группы * @type {String} */ _this.url = (url || document.domain); /** * @type {String} * @private */ _this._storePrefix = uuid.hash(_this.url); /** * Внутренний индекс для события * @type {Number} * @private */ _this._idx; /** * Очередь событий * @type {Object[]} * @private */ _this._queue = []; /** * Список уже попробованных sharedUrl * @type {Object} * @private */ _this._excludedSharedUrls = {}; /** * Очередь команд * @type {Array} * @private */ _this._cmdQueue = []; /** * Объект функций обратного вызова * @type {Object} * @private */ _this._callbacks = {}; _this._processingCmdQueue = debounce(_this._processingCmdQueue, 30); // Подписываемя на получение команд _this.on('CMD', function (cmd) { var id = cmd.id, cmdQueue = _this._cmdQueue, callback = _this._callbacks[id], idx = cmdQueue.length; if (cmd.response) { if (!_this.master) { // Мастер обработал команду, удаляем её из очереди while (idx--) { if (cmdQueue[idx].id === id) { cmdQueue.splice(idx, 1); break; } } } if (callback) { // О, это результат для наc delete _this._callbacks[id]; callback(cmd.error, cmd.result); } } else { // Добавляем в очередь cmdQueue.push(cmd); _this._processingCmdQueue(); } }); // Опачки! _this.on('master', function () { _this._processingCmdQueue(); }); // Получи сторадж _this._initStorage(function (store) { _this.store = store; try { /* istanbul ignore next */ if (!useStore && Worker.support) { _this._initSharedWorkerTransport(); } else { throw "NOT_SUPPORTED"; } } catch (err) { _this._initStorageTransport(); } }); /* istanbul ignore next */ if (window.addEventListener) { window.addEventListener('unload', _this._destroyUnload); } else { window.attachEvent('onunload', _this._destroyUnload); } } Hole.fn = Hole.prototype = /** @lends Hole.prototype */{ _attempt: 0, /** * Готовность «дырки» * @type {Boolean} */ ready: false, /** * Мастер-флаг * @type {Boolean} */ master: false, /** * Уничтожен? * @type {Boolean} */ destroyed: false, /** * Кол-во «дырок» * @type {Number} */ length: 0, on: Emitter.fn.on, off: Emitter.fn.off, /** * Вызвать удаленную команду на мастере * @param {String} cmd * @param {*} [data] * @param {Function} [callback] */ call: function (cmd, data, callback) { if (typeof data === 'function') { callback = data; data = void 0; } // Генерируем id команды var id = uuid(); this._callbacks[id] = callback; this.emit('CMD', { id: id, name: cmd, data: data, source: this.id }); }, /** * Испустить событие * @param {String} type * @param {*} [args] * @returns {Hole} */ emit: function (type, args) { this._queue.push({ ts: now(), type: type, args: args }); return this; }, /** * Инициализиция хранилища * @private */ _initStorage: function (callback) { var match = this.url.toLowerCase().match(/^(https?:)?\/\/([^/]+)/);; if (match && match[2] !== document.domain) { store.remote(this.url, callback); } else { callback(store); } }, /** * Инициализация траспорта на основе SharedWorker * @param {Boolean} [retry] повтор * @private */ _initSharedWorkerTransport: /* istanbul ignore next */ function (retry) { var _this = this, port, worker, url = _this.url, label = location.pathname + location.search, sharedUrls = _this._getSharedUrls(), surl = sharedUrls[0], sid ; _this._store('shared.url.' + _this.id, null); _this._attempt++; if (_this._attempt > 10) { return; } try { if (!surl) { sid = url + ':' + _this.id; surl = Worker.getSharedURL(sid); } _this._excludedSharedUrls[surl] = true; _this.worker = (worker = Worker.create(surl)); _this.port = (port = worker.port); _this._store('shared.url.' + _this.id, { url: url, surl: surl, }); } catch (err) { console.warn('[wormhole] Worker error:', err); _this._initSharedWorkerTransport(true); } _this.__onPortMessage = function (evt) { _this._onPortMessage(evt); }; _this.__onWorkerError = function (evt) { console.warn('[wormhole] Worker error:', evt); worker.removeEventListener('error', _this.__onWorkerError, false); worker = null; _this._initSharedWorkerTransport(true); }; worker.addEventListener('error', _this.__onWorkerError, false); port.addEventListener('message', _this.__onPortMessage); port.start(); }, _getSharedUrls: function () { var _this = this; var surls = []; var prefix = this._storeKey('shared.url'); this.store.each(function (data, key) { if ( key.indexOf(prefix) !== -1 && data.url === _this.url && !_this._excludedSharedUrls[data.surl] ) { surls.push(data.surl); } }); return surls; }, /** * Сообщение от рабочего * @param {Event} evt * @private */ _onPortMessage: /* istanbul ignore next */ function (evt) { evt = evt.data; if (evt === 'CONNECTED') { this.emit = this._workerEmit; this.ready = true; this.port.postMessage({ hole: { id: this.id } }); this._processingQueue(); // Получили подтвреждение, что мы подсоединились _emitterEmit.call(this, 'ready', this); } else if (evt === 'PING') { // Ping? Pong! this.port.postMessage('PONG'); } else if (evt === 'MASTER') { // Сказали, что мы теперь мастер this.master = true; // ОК _emitterEmit.call(this, 'master', this); } else if (evt.type === 'peers') { // Обновляем кол-во пиров this._checkPeers(evt.data); } else { // console.log(this.id, evt.type); // Просто событие _emitterEmit.call(this, evt.type, evt.data); } }, /** * Инициализация транспорта на основе store * @private */ _initStorageTransport: function () { var _this = this, _first = true, id = _this.id; _this._idx = (_this._store('queue') || {}).idx || 0; // Запускаем проверку обновления данных peer'а _this._updPeer = function () { _this._store('peer.' + id, { id: id, ts: now(), master: _this.master, }); clearTimeout(_this._pid); _this._pid = setTimeout(_this._updPeer, PEER_UPD_DELAY); }; // Реакция на обновление storage _this.__onStorage = function (key, data) { if (key.indexOf('peer.') > -1) { //console.log('onPeer:', key, data[key]); _this._checkPeers(); // Размазываем проверку по времени clearTimeout(_this._pidMaster); _this._pidMaster = setTimeout(_this._checkMasterDelayed, MASTER_VOTE_DELAY); } else if (key === _this._storeKey('queue')) { _this._processingQueue(data[key].items); } }; _this._checkMasterDelayed = function () { _this._checkMaster(); }; _this.store.on('change', _this.__onStorage); // Разрыв для нормальной работы синхронной подписки на события (из вне) _this._pid = setTimeout(function () { _this.emit = _this._storeEmit; _this.ready = true; _emitterEmit.call(_this, 'ready', _this); _this._updPeer(); _this._processingQueue(); }, 0); }, /** * Проверка и выбор мастера * @private */ _checkMaster: function () { var peers = this.getPeers(true); if (peers.length > 0) { var mpeer = peers[0]; if (!mpeer.master || (now() - mpeer.ts) > MASTER_DELAY) { peers.forEach(function (p) { if (mpeer.ts < p.ts) { mpeer = p; } }); if (mpeer.id === this.id) { this.master = true; this._updPeer(); _emitterEmit.call(this, 'master', this); } } } }, /** * Получить все активные «дыкрки» * @param {boolean} [raw] * @return {Array} */ getPeers: function (raw) { var ts = now(), _this = this, peers = [], storeKey = _this._storeKey('peer.'); _this.store.each(function (data, key) { if (key.indexOf(storeKey) > -1) { if ((ts - data.ts) < PEERS_DELAY) { if (raw) { peers[data.master ? 'unshift' : 'push'](data); } else { peers.push(data.id); } } else if (_this.master) { _this.store.remove(key); } } }); return peers; }, /** * Обновляем кол-во и список «дырок» * @param {string[]} [peers] * @private */ _checkPeers: function (peers) { var i, id, ts = now(), _this = this, _peers = _this._peers || [], changed = false; if (!peers) { peers = this.getPeers(); } i = Math.max(peers.length, _peers.length); while (i--) { id = peers[i]; if (id && _inArray(_peers, id) === -1) { changed = true; _emitterEmit.call(this, 'peers:add', id); } if (_peers[i] != id) { id = _peers[i]; if (id && _inArray(peers, id) === -1) { changed = true; _emitterEmit.call(this, 'peers:remove', id); } } } if (changed) { this._peers = peers; this.length = peers.length; _emitterEmit.call(this, 'peers', [peers]); } }, /** * Получить ключь для store * @param {String} key * @returns {String} * @private */ _storeKey: function (key) { return this._storePrefix + '.' + key; }, /** * Записать или получить информацию из хранилища * @param {String} key * @param {*} [value] * @returns {Object} * @private */ _store: function (key, value) { key = this._storeKey(key); if (value === null) { this.store.remove(key); } else if (value === void 0) { value = this.store.get(key); } else { this.store.set(key, value); } return value; }, /** * Emit через SharedWorker * @param type * @param args * @private */ _workerEmit: /* istanbul ignore next */ function (type, args) { var ts = now(); this.port.postMessage({ ts: ts, type: type, data: args }); return this; }, /** * Emit через хранилище * @param type * @param args * @private */ _storeEmit: function (type, args) { var queue = this._store('queue') || { items: [], idx: 0 }, ts = now(), items = queue.items, i = items.length ; items.push({ ts: ts, idx: ++queue.idx, type: type, args: args, source: this.id }); while (i--) { if (ts - items[i].ts > QUEUE_WAIT) { items.splice(0, i); break; } } this._store('queue', queue); this._processingQueue(queue.items); return this; }, /** * Обработка очереди событий * @param {Object[]} [queue] * @private */ _processingQueue: function (queue) { var evt; if (queue === void 0) { queue = this._queue; while (queue.length) { evt = queue.shift(); this.emit(evt.type, evt.args); } } else { for (var i = 0, n = queue.length; i < n; i++) { evt = queue[i]; if (this._idx < evt.idx) { this._idx = evt.idx; // if (evt.source !== this.id) { _emitterEmit.call(this, evt.type, evt.args); // } } } } }, /** * Обработка очереди команд * @private */ _processingCmdQueue: function () { var cmdQueue = this._cmdQueue; /* istanbul ignore else */ if (this.master) { while (cmdQueue.length) { _execCmd(this, cmdQueue.shift()); } } }, /** * Уничтожить */ destroy: function () { if (!this.destroyed) { if (window.addEventListener) { window.removeEventListener('unload', this._destroyUnload); } else { window.detachEvent('onunload', this._destroyUnload); } this.ready = false; this.destroyed = true; this._destroyUnload = null; clearTimeout(this._pid); this._store('shared.url.' + this.id, null); // Описываем все события this.off(); store.off('change', this.__onStorage); /* istanbul ignore next */ if (this.port) { this.port.removeEventListener('message', this.__onPortMessage); this.port.postMessage('DESTROY'); this.port = null; this.worker = null; } else { this._store('peer.' + this.id, null); } this.master = false; } } }; var singletonHole = function () { /* istanbul ignore else */ if (!singletonHole.instance) { singletonHole.instance = new Hole(); } return singletonHole.instance; }; if (window.wormhole && window.wormhole.workers === false) { Worker.support = false; } // Export singletonHole.version = '0.10.0'; singletonHole.now = now; singletonHole.uuid = uuid; singletonHole.debounce = debounce; singletonHole.cors = cors; singletonHole.store = store; singletonHole.Emitter = Emitter; singletonHole.Worker = Worker; singletonHole.Hole = Hole; singletonHole.Universal = Hole; singletonHole['default'] = singletonHole; /* istanbul ignore next */ if (typeof define === 'function' && define.amd) { define(function () { return singletonHole; }); } else if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') { module.exports = singletonHole; } else { window.wormhole = singletonHole; } })(window, document);