├── .gitignore ├── st ├── holes.png ├── image-1.png ├── image-2.png ├── image-3.png ├── image-4.png ├── image-5.png ├── og-image.png ├── variant.png ├── app.js ├── app.css └── xtpl.min.js ├── src ├── now.js ├── get-own.js ├── debounce.js ├── uuid.js ├── module.js ├── emitter.js ├── worker.js ├── cors.js ├── store.js └── hole.js ├── tests ├── local.test.html ├── store.cors.test.html ├── universal.test.html ├── cors.test.html ├── wormhole.universal.tests.js ├── wormhole.tests.js ├── wormhole.cors.tests.js ├── index.html ├── wormhole.emitter.tests.js ├── wormhole.store.tests.js └── wormhole.hole.tests.js ├── universal.html ├── package.json ├── Gruntfile.js ├── ws.js ├── README.md ├── index.html └── wormhole.js /.gitignore: -------------------------------------------------------------------------------- 1 | temp 2 | report 3 | .DS_Store 4 | node_modules 5 | -------------------------------------------------------------------------------- /st/holes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RubaXa/wormhole/HEAD/st/holes.png -------------------------------------------------------------------------------- /st/image-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RubaXa/wormhole/HEAD/st/image-1.png -------------------------------------------------------------------------------- /st/image-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RubaXa/wormhole/HEAD/st/image-2.png -------------------------------------------------------------------------------- /st/image-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RubaXa/wormhole/HEAD/st/image-3.png -------------------------------------------------------------------------------- /st/image-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RubaXa/wormhole/HEAD/st/image-4.png -------------------------------------------------------------------------------- /st/image-5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RubaXa/wormhole/HEAD/st/image-5.png -------------------------------------------------------------------------------- /st/og-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RubaXa/wormhole/HEAD/st/og-image.png -------------------------------------------------------------------------------- /st/variant.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RubaXa/wormhole/HEAD/st/variant.png -------------------------------------------------------------------------------- /src/now.js: -------------------------------------------------------------------------------- 1 | define([], function () { 2 | var now = Date.now || /* istanbul ignore next */ function () { 3 | return +(new Date); 4 | }; 5 | 6 | // Export 7 | return now; 8 | }); 9 | -------------------------------------------------------------------------------- /src/get-own.js: -------------------------------------------------------------------------------- 1 | define([], function () { 2 | function getOwn(obj, prop) { 3 | return !(prop in getOwn) && obj && obj.hasOwnProperty(prop) ? obj[prop] : null; 4 | } 5 | 6 | // Export 7 | return getOwn; 8 | }); 9 | -------------------------------------------------------------------------------- /tests/local.test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /tests/store.cors.test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /tests/universal.test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/debounce.js: -------------------------------------------------------------------------------- 1 | define([], function () { 2 | function debounce(func, delay, immediate) { 3 | var timeout; 4 | 5 | return function() { 6 | var context = this, 7 | args = arguments; 8 | 9 | clearTimeout(timeout); 10 | 11 | timeout = setTimeout(function() { 12 | timeout = null; 13 | 14 | /* istanbul ignore else */ 15 | if (!immediate) { 16 | func.apply(context, args); 17 | } 18 | }, delay); 19 | 20 | /* istanbul ignore next */ 21 | if (immediate && !timeout) { 22 | func.apply(context, args); 23 | } 24 | }; 25 | } 26 | 27 | 28 | // Export 29 | return debounce; 30 | }); 31 | -------------------------------------------------------------------------------- /universal.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /tests/cors.test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 10 | 11 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /tests/wormhole.universal.tests.js: -------------------------------------------------------------------------------- 1 | (function (wormhole) { 2 | QUnit.module('wormhole.Universal'); 3 | 4 | 5 | QUnit.test('peers', function (assert) { 6 | var rnd = Math.random(); 7 | var log = []; 8 | var hole = new wormhole.Universal('http://localhost:4791/universal.html', true); 9 | var done = assert.async(); 10 | 11 | hole.on('local', function (val) { 12 | assert.ok(true, 'local: ' + val); 13 | log.push(val); 14 | }); 15 | 16 | hole.emit('local', 1); 17 | 18 | _createWin('remote:universal.test.html').always(function () { 19 | setTimeout(function () { 20 | console.log('hole.emit(remote, ' + rnd + ')'); 21 | hole.emit('remote', rnd); 22 | 23 | setTimeout(function () { 24 | deepEqual(log, [1, 2, rnd + '!'], 'remote -> local'); 25 | done(); 26 | }, 50); 27 | }, 500); 28 | }); 29 | }); 30 | })(wormhole); 31 | -------------------------------------------------------------------------------- /src/uuid.js: -------------------------------------------------------------------------------- 1 | define([], function () { 2 | var floor = Math.floor, 3 | random = Math.random 4 | ; 5 | 6 | 7 | function s4() { 8 | return floor(random() * 0x10000 /* 65536 */).toString(16); 9 | } 10 | 11 | 12 | /** 13 | * UUID — http://ru.wikipedia.org/wiki/UUID 14 | * @returns {String} 15 | */ 16 | function uuid() { 17 | return (s4() + s4() + "-" + s4() + "-" + s4() + "-" + s4() + "-" + s4() + s4() + s4()); 18 | } 19 | 20 | 21 | /** 22 | * Генерация hash на основе строки 23 | * @param {String} str 24 | * @returns {String} 25 | */ 26 | uuid.hash = function (str) { 27 | var hash = 0, 28 | i = 0, 29 | length = str.length 30 | ; 31 | 32 | /* istanbul ignore else */ 33 | if (length > 0) { 34 | for (; i < length; i++) { 35 | hash = ((hash << 5) - hash) + str.charCodeAt(i); 36 | hash |= 0; // Convert to 32bit integer 37 | } 38 | } 39 | 40 | return hash.toString(36); 41 | }; 42 | 43 | 44 | // Export 45 | return uuid; 46 | }); 47 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wormhole.js", 3 | "version": "0.10.1", 4 | "devDependencies": { 5 | "grunt": "0.4.5", 6 | "grunt-qunit-istanbul": "0.4.5", 7 | "grunt-contrib-jshint": "0.10.0", 8 | "grunt-contrib-connect": "0.8.0", 9 | "grunt-contrib-watch": "0.6.1", 10 | "grunt-version": "*" 11 | }, 12 | "description": "Wormhole — it's better EventEmitter for communication between tabs with supporting Master/Slave.", 13 | "main": "wormhole.js", 14 | "scripts": { 15 | "dev": "grunt dev", 16 | "test": "grunt", 17 | "build": "grunt build", 18 | "prepublishOnly": "npm run build" 19 | }, 20 | "repository": { 21 | "type": "git", 22 | "url": "git://github.com/rubaxa/wormhole.git" 23 | }, 24 | "keywords": [ 25 | "wormhole", 26 | "eventhub", 27 | "eventemitter", 28 | "tabs", 29 | "master/slave", 30 | "communication", 31 | "sharedworker", 32 | "websocket", 33 | "persistent" 34 | ], 35 | "author": "Konstantin Lebedev ", 36 | "license": "MIT", 37 | "jam": { 38 | "main": "wormhole" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /tests/wormhole.tests.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | QUnit.module('wormhole'); 3 | 4 | 5 | QUnit.test('core', function (assert) { 6 | var log = [0, 0, 0]; 7 | var holes = []; 8 | var _set = function (i) { 9 | assert.ok(true, '#' + i); 10 | log[i] = 1; 11 | }; 12 | var done = assert.async(); 13 | 14 | 15 | var hole = wormhole().on('foo', function () { 16 | _set(0); 17 | }); 18 | holes.push(hole); 19 | 20 | 21 | _createWin('local.test.html').then(function (el) { 22 | var hole = el.contentWindow.wormhole().on('foo', function () { 23 | _set(1); 24 | }); 25 | holes.push(hole); 26 | }); 27 | 28 | _createWin('local.test.html').then(function (el) { 29 | var hole = el.contentWindow.wormhole().on('foo', function () { 30 | _set(2); 31 | }); 32 | holes.push(hole); 33 | }); 34 | 35 | 36 | _createWin('local.test.html').then(function (el) { 37 | var hole = el.contentWindow.wormhole().emit('foo'); 38 | holes.push(hole); 39 | }); 40 | 41 | 42 | setTimeout(function () { 43 | assert.deepEqual(log, [1, 1, 1]); 44 | $.each(holes, function (i, hole) { 45 | hole.destroy(); 46 | }); 47 | done(); 48 | }, 1000); 49 | }); 50 | })(); 51 | -------------------------------------------------------------------------------- /src/module.js: -------------------------------------------------------------------------------- 1 | define(["./now", "./uuid", "./debounce", "./cors", "./emitter", "./store", "./worker", "./hole"], function (now, uuid, debounce, cors, Emitter, store, Worker, Hole) { 2 | var singletonHole = function () { 3 | /* istanbul ignore else */ 4 | if (!singletonHole.instance) { 5 | singletonHole.instance = new Hole(); 6 | } 7 | 8 | return singletonHole.instance; 9 | }; 10 | 11 | 12 | if (window.wormhole && window.wormhole.workers === false) { 13 | Worker.support = false; 14 | } 15 | 16 | 17 | // Export 18 | singletonHole.version = '0.10.1'; 19 | singletonHole.now = now; 20 | singletonHole.uuid = uuid; 21 | singletonHole.debounce = debounce; 22 | singletonHole.cors = cors; 23 | singletonHole.store = store; 24 | singletonHole.Emitter = Emitter; 25 | singletonHole.Worker = Worker; 26 | 27 | singletonHole.Hole = Hole; 28 | singletonHole.Universal = Hole; 29 | singletonHole['default'] = singletonHole; 30 | 31 | 32 | /* istanbul ignore next */ 33 | if (typeof define === 'function' && define.amd) { 34 | define(function () { return singletonHole; }); 35 | } 36 | else if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') { 37 | module.exports = singletonHole; 38 | } 39 | else { 40 | window.wormhole = singletonHole; 41 | } 42 | }); 43 | -------------------------------------------------------------------------------- /tests/wormhole.cors.tests.js: -------------------------------------------------------------------------------- 1 | useRemoteServer && (function () { 2 | QUnit.module('wormhole.cors'); 3 | 4 | // Взаимодействие с удаленным сервером 5 | QUnit.test('cors', function (assert) { 6 | var cors = wormhole.cors; 7 | var done = assert.async(); 8 | 9 | var pid = setTimeout(function () { 10 | assert.ok(false, 'timeout'); 11 | done(); 12 | }, 3000) 13 | 14 | // Создаем два iframe 15 | $.when(_createWin(), _createWin()).then(function (foo, bar) { 16 | var log = []; 17 | 18 | // Подписываемся по получение данных в текущем окне 19 | cors.on('data', function (data) { 20 | log.push(data); 21 | }); 22 | 23 | // Отправляем данные в iframe 24 | cors(foo).send("Wow"); 25 | 26 | // Определим команды, которые может вызвать удаленный сервер 27 | cors.well = function (data) { 28 | log.push('well ' + data); 29 | }; 30 | 31 | // Команда с ошибокй 32 | cors.fail = function () { 33 | throw "error"; 34 | }; 35 | 36 | // Вызываем удаленную команду у iframe 37 | cors(bar).call('remote', { value: 321 }, function (err, response) { 38 | log.push(response); 39 | }); 40 | 41 | // Вызываем неопределенную команду 42 | cors(bar).call('unknown', function (err) { 43 | log.push(err); 44 | }); 45 | 46 | // Команду с ошибкой 47 | cors(bar).call('fail', function (err) { 48 | log.push(err); 49 | }); 50 | 51 | // Проверям результат 52 | setTimeout(function () { 53 | clearTimeout(pid); 54 | assert.deepEqual(log, [ 55 | 'Wow!', 56 | 'well done', 57 | { bar: true, value: 642 }, 58 | 'wormhole.cors.unknown: method not found', 59 | 'wormhole.cors.fail: remote error' 60 | ]); 61 | done(); 62 | }, 100); 63 | }); 64 | }); 65 | })(); 66 | -------------------------------------------------------------------------------- /st/app.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | "use strict"; 3 | 4 | console.info('wormhole.js') 5 | console.log(' - SharedWorker:', wormhole.Worker.support); 6 | console.log(' - Use WebSocket:', !!wormhole.WS); 7 | 8 | var hole = wormhole(); 9 | var socket = wormhole.WS ? new wormhole.WS('ws://echo.websocket.org') : null; 10 | 11 | if (socket) { 12 | socket.onopen = socket.onerror = socket.onclose = function (evt) { 13 | console.warn('[wormhole.WS]', evt.type + ':', evt); 14 | }; 15 | } 16 | 17 | xtpl.ctrl('main', function (ctx) { 18 | ctx.holes = 0; 19 | ctx.images = [1, 2, 3, 4, 5]; 20 | ctx.imageNum = ctx.images[Math.random() * ctx.images.length | 0]; 21 | ctx.images.forEach(function (num) { 22 | (new Image).src = './st/image-' + num + '.png'; 23 | }); 24 | 25 | if (socket) { 26 | socket.onmaster = onmaster; 27 | socket.onmessage = function (evt) { 28 | var data = JSON.parse(evt.data); 29 | if (data.type === 'choose') { 30 | onchoose(data.num); 31 | } 32 | }; 33 | 34 | ctx.choose = function (num) { 35 | socket.send(JSON.stringify({ 36 | type: 'choose', 37 | num: num, 38 | })); 39 | }; 40 | 41 | hole.on('peers', function (peers) { 42 | socket.master && ctx.choose(ctx.imageNum); 43 | ctx.$set('holes', peers.length); 44 | }); 45 | } else { 46 | hole.on('master', onmaster); 47 | hole.on('choose', onchoose); 48 | hole.on('peers', function (peers) { 49 | hole.master && ctx.choose(ctx.imageNum); 50 | ctx.$set('holes', peers.length); 51 | }); 52 | 53 | ctx.choose = function (num) { 54 | hole.emit('choose', num); 55 | }; 56 | } 57 | 58 | function onmaster() { 59 | window.console && console.log('I master'); 60 | document.title = '⬤ ' + document.title; 61 | } 62 | 63 | function onchoose(num) { 64 | ctx.$set('imageNum', num); 65 | console.log('[' + new Date + '] image:', num); 66 | } 67 | }); 68 | })(); 69 | -------------------------------------------------------------------------------- /tests/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Wormhole :: tests 6 | 7 | 8 | 9 | 10 | 11 | 12 | 19 | 20 | 21 | 22 | 23 |
24 |
25 | 26 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /tests/wormhole.emitter.tests.js: -------------------------------------------------------------------------------- 1 | (function (Emitter) { 2 | QUnit.module('wormhole.Emitter'); 3 | 4 | 5 | // Излучатель: on/off/emit 6 | QUnit.test('core', function (assert) { 7 | var log = []; 8 | var emitter = new Emitter; 9 | 10 | var onFoo = function (data) { 11 | log.push('foo-' + data); 12 | }; 13 | 14 | var onBar = function (data) { 15 | log.push('bar-' + data); 16 | }; 17 | 18 | var onBaz = function () { 19 | log.push('baz-' + [].slice.call(arguments, 0).join('.')); 20 | }; 21 | 22 | emitter.on("foo", onFoo); 23 | emitter.on("bar", onBar); 24 | emitter.on("baz", onBaz); 25 | 26 | emitter.emit("foo", 1); 27 | emitter.emit("bar", 1); 28 | 29 | emitter.off("foo", onBar); 30 | emitter.emit("foo", 2); 31 | emitter.emit("bar", 2); 32 | 33 | emitter.off("foo", onFoo); 34 | emitter.emit("foo", 3); 35 | emitter.emit("bar", 3); 36 | 37 | emitter.emit('baz'); 38 | emitter.emit('baz', [1, 2]); 39 | emitter.emit('baz', [1, 2, 3]); 40 | emitter.emit('baz', [1, 2, 3, 4, 5]); 41 | 42 | assert.equal(log + '', 'foo-1,bar-1,foo-2,bar-2,bar-3,baz-,baz-1.2,baz-1.2.3,baz-1.2.3.4.5'); 43 | }); 44 | 45 | 46 | QUnit.test('__emitter__', function (assert) { 47 | var emitter = new Emitter; 48 | var foo = function () {/*foo*/}; 49 | var bar = function () {/*bar*/}; 50 | 51 | emitter.on('change', foo); 52 | emitter.on('change', foo); 53 | emitter.on('change', bar); 54 | emitter.on('change', bar); 55 | 56 | assert.equal(Emitter.getListeners(emitter, 'change').length, 4); 57 | 58 | emitter.off('change', bar); 59 | assert.equal(Emitter.getListeners(emitter, 'change').length, 3); 60 | 61 | emitter.off('change', bar); 62 | assert.equal(Emitter.getListeners(emitter, 'change').length, 2); 63 | 64 | emitter.off('change', foo); 65 | assert.equal(Emitter.getListeners(emitter, 'change').length, 1); 66 | 67 | emitter.off('change', foo); 68 | assert.equal(Emitter.getListeners(emitter, 'change').length, 0); 69 | }); 70 | 71 | 72 | QUnit.test('one', function (assert) { 73 | var log = []; 74 | var emitter = new Emitter; 75 | 76 | emitter.one('foo', function (x) { 77 | log.push(x); 78 | }); 79 | 80 | emitter.emit('foo', 'ok'); 81 | emitter.emit('foo', 'fail'); 82 | 83 | assert.equal(log + '', 'ok'); 84 | }); 85 | })(wormhole.Emitter); 86 | -------------------------------------------------------------------------------- /st/app.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | margin: 0; 3 | padding: 0; 4 | position: relative; 5 | min-height: 100%; 6 | height: 100%; 7 | } 8 | 9 | body { 10 | font-family: 'Roboto', sans-serif; 11 | text-align: center; 12 | background-color: #525560; 13 | } 14 | 15 | .ferule { 16 | height: 100%; 17 | display: inline-block; 18 | vertical-align: middle; 19 | } 20 | 21 | .container { 22 | margin: 30px 0; 23 | width: 90%; 24 | min-height: 70%; 25 | display: inline-block; 26 | max-width: 1300px; 27 | vertical-align: middle; 28 | position: relative; 29 | } 30 | 31 | 32 | .logo { 33 | top: 10%; 34 | left: 25%; 35 | right: 25%; 36 | position: absolute; 37 | display: inline-block; 38 | z-index: 100; 39 | } 40 | 41 | .overlay { 42 | top: -10px; 43 | right: -10px; 44 | left: -10px; 45 | bottom: -10px; 46 | position: absolute; 47 | background-color: rgba(0,0,0,.3); 48 | z-index: 50; 49 | } 50 | .is-master .overlay { 51 | border-bottom: 2px solid #000; 52 | } 53 | 54 | .image { 55 | top: 0; 56 | left: 0; 57 | width: 100%; 58 | z-index: 10; 59 | } 60 | 61 | .holes { 62 | text-decoration: none; 63 | cursor: pointer; 64 | width: 157px; 65 | height: 191px; 66 | top: 50%; 67 | left: 50%; 68 | color: #fff; 69 | padding-right: 15px; 70 | margin: -77px 0 0 -85px; 71 | padding-top: 30px; 72 | z-index: 100; 73 | position: absolute; 74 | font-size: 80px; 75 | font-weight: 500; 76 | background: url('holes.png') no-repeat; 77 | } 78 | .holes:hover { 79 | background-position: -175px 0; 80 | } 81 | 82 | .variants { 83 | width: 100%; 84 | bottom: 30px; 85 | position: absolute; 86 | z-index: 110; 87 | } 88 | .variant { 89 | cursor: pointer; 90 | width: 70px; 91 | height: 70px; 92 | background: url('variant.png') no-repeat; 93 | display: inline-block; 94 | margin-left: 20px; 95 | background-color: #fff; 96 | border: 3px solid #fff; 97 | border-radius: 100%; 98 | } 99 | .variant:first-child { 100 | margin-left: 0; 101 | } 102 | 103 | .variant:hover { 104 | border-color: #ff0; 105 | background-color: #ff0; 106 | } 107 | 108 | .variant.active { 109 | opacity: .3; 110 | } 111 | 112 | 113 | .marcus { 114 | opacity: .4; 115 | right: 10px; 116 | bottom: 10px; 117 | position: absolute; 118 | z-index: 1000; 119 | } 120 | .marcus, 121 | .marcus a { 122 | color: #fff; 123 | font-size: 13px; 124 | font-family: Arial, Geneva, sans-serif; 125 | } 126 | 127 | .marcus:hover { 128 | opacity: .8; 129 | } 130 | -------------------------------------------------------------------------------- /tests/wormhole.store.tests.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | QUnit.module('wormhole.store'); 3 | 4 | 5 | // Проверяем работу хранилища и его изменения 6 | QUnit.test('local', function (assert) { 7 | var log = []; 8 | var store = wormhole.store; 9 | var rand = Math.random(); 10 | var done = assert.async(); 11 | 12 | assert.ok(store.enabled, 'enabled'); 13 | 14 | // Подписываемся на изменение данных 15 | store.on('change:' + rand, function (data) { 16 | assert.equal(data, rand); 17 | log.push('rand-val'); 18 | }); 19 | 20 | store.set(rand, rand); 21 | store.set('rand', rand); 22 | 23 | // Подписываемся на изменение данных 24 | store.on('change', function () { 25 | log.push('change'); 26 | }); 27 | 28 | // Подписываем на конкретный ключ 29 | store.on('change:foo', function (key, val) { 30 | log.push('change:foo-' + val); 31 | }); 32 | 33 | 34 | // Создаем iframe на текущий домен 35 | _createWin('local.test.html').then(function (el) { 36 | // Получаем экземпляр store из iframe 37 | var winStore = el.contentWindow.wormhole.store; 38 | 39 | // Сверяем значения 40 | assert.equal(winStore.get('rand'), rand); 41 | 42 | // Устанавливаем какое-то значения, для проверки событий 43 | winStore.set('foo', rand); 44 | 45 | // Выставляем значение и сразу читаем его 46 | store.set('bar', rand); 47 | assert.equal(winStore.get('bar'), rand, 'bar.rand'); 48 | 49 | winStore.set('bar', rand + '!'); 50 | assert.equal(store.get('bar'), rand + '!', 'bar.rand!'); 51 | 52 | setTimeout(function () { 53 | // Проверяем события 54 | assert.deepEqual(log, [ 55 | 'rand-val', 56 | 'change', 57 | 'change', 58 | 'change:foo-' + rand, 59 | 'change' 60 | ]); 61 | 62 | // Чтение 63 | assert.equal(store.get('foo'), rand); 64 | 65 | // Удаление 66 | store.remove('foo'); 67 | assert.equal(store.get('foo'), void 0); 68 | 69 | done(); 70 | }, 100); 71 | }); 72 | }); 73 | 74 | 75 | QUnit.test('remote', function (assert) { 76 | var log = []; 77 | var rnd = Math.random(); 78 | var remoteRnd = Math.random(); 79 | var store = wormhole.store.remote('http://localhost:4791/universal.html', function (_store) { 80 | assert.equal(store, _store, 'ready'); 81 | _store.set('foo', 'self:' + rnd); 82 | }); 83 | var done = assert.async(); 84 | 85 | store.on('change', function (key, data) { 86 | log.push('all->' + key + ':' + data[key]); 87 | }); 88 | 89 | store.on('change:foo', function (key, val) { 90 | log.push('foo.prop->' + val); 91 | }); 92 | 93 | _createWin('remote:store.cors.test.html?rnd=' + remoteRnd).always(function () { 94 | setTimeout(function () { 95 | assert.deepEqual(log, [ 96 | 'all->foo:self:' + rnd, 97 | 'foo.prop->self:' + rnd, 98 | 'all->foo:remote:' + remoteRnd, 99 | 'foo.prop->remote:' + remoteRnd 100 | ]); 101 | done(); 102 | }, 2000); 103 | }); 104 | }); 105 | })(); 106 | -------------------------------------------------------------------------------- /src/emitter.js: -------------------------------------------------------------------------------- 1 | define([], function () { 2 | var __emitter__ = '__emitter__'; 3 | 4 | function getListeners(obj, name) { 5 | if (obj[__emitter__] === void 0) { 6 | obj[__emitter__] = {}; 7 | } 8 | 9 | obj = obj[__emitter__]; 10 | 11 | if (obj[name] === void 0) { 12 | obj[name] = []; 13 | } 14 | 15 | return obj[name]; 16 | } 17 | 18 | 19 | /** 20 | * @class Emitter 21 | * @desc Микро-излучатель 22 | */ 23 | function Emitter() { 24 | } 25 | 26 | 27 | Emitter.fn = Emitter.prototype = /** @lends Emitter.prototype */ { 28 | /** 29 | * Подписаться на событие 30 | * @param {String} name 31 | * @param {Function} fn 32 | * @returns {Emitter} 33 | */ 34 | on: function (name, fn) { 35 | var list = getListeners(this, name); 36 | list.push(fn); 37 | return this; 38 | }, 39 | 40 | 41 | /** 42 | * Отписаться от событие 43 | * @param {String} name 44 | * @param {Function} fn 45 | * @returns {Emitter} 46 | */ 47 | off: function (name, fn) { 48 | if (name === void 0) { 49 | delete this[__emitter__]; 50 | } 51 | else { 52 | var list = getListeners(this, name), 53 | i = list.length; 54 | 55 | while (i--) { 56 | // Ищем слушателя и удаляем (indexOf - IE > 8) 57 | if (list[i] === fn) { 58 | list.splice(i, 1); 59 | break; 60 | } 61 | } 62 | } 63 | 64 | return this; 65 | }, 66 | 67 | 68 | /** 69 | * Подписаться на событие и отписаться сразу после его получения 70 | * @param {String} name 71 | * @param {Function} fn 72 | * @returns {Emitter} 73 | */ 74 | one: function (name, fn) { 75 | var proxy = function () { 76 | this.off(name, proxy); 77 | return fn.apply(this, arguments); 78 | }; 79 | 80 | return this.on(name, proxy); 81 | }, 82 | 83 | 84 | /** 85 | * Распространить данные 86 | * @param {String} name 87 | * @param {*} [args] 88 | */ 89 | emit: function (name, args) { 90 | var listeners = getListeners(this, name), 91 | i = listeners.length, 92 | nargs 93 | ; 94 | 95 | args = (arguments.length === 1) ? [] : [].concat(args); 96 | nargs = args.length; 97 | 98 | while (i--) { 99 | if (nargs === 0) { 100 | listeners[i].call(this); 101 | } 102 | else if (nargs === 1){ 103 | listeners[i].call(this, args[0]); 104 | } 105 | else if (nargs === 2){ 106 | listeners[i].call(this, args[0], args[1]); 107 | } 108 | else { 109 | listeners[i].apply(this, args); 110 | } 111 | } 112 | } 113 | }; 114 | 115 | 116 | /** 117 | * Подмешать методы 118 | * @param {*} target 119 | * @returns {*} 120 | * @method 121 | */ 122 | Emitter.apply = function (target) { 123 | target.on = Emitter.fn.on; 124 | target.off = Emitter.fn.off; 125 | target.one = Emitter.fn.one; 126 | target.emit = Emitter.fn.emit; 127 | 128 | return target; 129 | }; 130 | 131 | 132 | Emitter.getListeners = getListeners; 133 | 134 | 135 | // Export 136 | return Emitter; 137 | }); 138 | -------------------------------------------------------------------------------- /src/worker.js: -------------------------------------------------------------------------------- 1 | define([], function () { 2 | var _stringifyJSON = JSON.stringify; 3 | 4 | 5 | /** 6 | * @type {URL} 7 | */ 8 | var URL = window.URL; 9 | 10 | 11 | /** 12 | * @type {Blob} 13 | */ 14 | var Blob = window.Blob; 15 | 16 | 17 | /** 18 | * @type {SharedWorker} 19 | */ 20 | var SharedWorker = window.SharedWorker; 21 | 22 | 23 | /* istanbul ignore next */ 24 | var Worker = { 25 | support: !!(URL && Blob && SharedWorker), 26 | 27 | 28 | /** 29 | * Создать работника 30 | * @param {String} url 31 | * @returns {SharedWorker} 32 | */ 33 | create: function (url) { 34 | return new SharedWorker(url); 35 | }, 36 | 37 | 38 | /** 39 | * Получить ссылку на работника 40 | * @param {String} id 41 | * @returns {String} 42 | * @private 43 | */ 44 | getSharedURL: function (id) { 45 | // Код воркера 46 | var source = '(' + (function (window) { 47 | var ports = []; 48 | var master = null; 49 | 50 | function checkMaster() { 51 | if (!master && (ports.length > 0)) { 52 | master = ports[0]; 53 | master.postMessage('MASTER'); 54 | } 55 | } 56 | 57 | function broadcast(data) { 58 | ports.forEach(function (port) { 59 | port.postMessage(data); 60 | }); 61 | } 62 | 63 | function removePort(port) { 64 | var idx = ports.indexOf(port); 65 | 66 | if (idx > -1) { 67 | ports.splice(idx, 1); 68 | peersUpdated(); 69 | } 70 | 71 | if (port === master) { 72 | master = null; 73 | } 74 | } 75 | 76 | function peersUpdated() { 77 | broadcast({ 78 | type: 'peers', 79 | data: ports.map(function (port) { 80 | return port.holeId; 81 | }) 82 | }); 83 | } 84 | 85 | // Опрашиваем и ищем зомби 86 | setTimeout(function next() { 87 | var i = ports.length, port; 88 | 89 | while (i--) { 90 | port = ports[i]; 91 | 92 | if (port.zombie) { 93 | // Убиваем зомби 94 | removePort(port); 95 | } 96 | else { 97 | port.zombie = true; // Помечаем как зомби 98 | port.postMessage('PING'); 99 | } 100 | } 101 | 102 | checkMaster(); 103 | setTimeout(next, 500); 104 | }, 500); 105 | 106 | window.addEventListener('connect', function (evt) { 107 | var port = evt.ports[0]; 108 | 109 | port.onmessage = function (evt) { 110 | var data = evt.data; 111 | 112 | if (data === 'PONG') { 113 | port.zombie = false; // живой порт 114 | } 115 | else if (data === 'DESTROY') { 116 | // Удаляем порт 117 | removePort(port); 118 | checkMaster(); 119 | } 120 | else if (data.hole) { 121 | // Обновление meta информации 122 | port.holeId = data.hole.id; 123 | peersUpdated(); 124 | } 125 | else { 126 | broadcast({ type: data.type, data: data.data }); 127 | } 128 | }; 129 | 130 | ports.push(port); 131 | 132 | port.start(); 133 | port.postMessage('CONNECTED'); 134 | 135 | checkMaster(); 136 | }, false); 137 | }).toString() + ')(this, ' + _stringifyJSON(name) + ')'; 138 | 139 | return URL.createObjectURL(new Blob([source], {type: 'text/javascript'})); 140 | } 141 | }; 142 | 143 | 144 | // Export 145 | return Worker; 146 | }); 147 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function (grunt) { 4 | grunt.initConfig({ 5 | pkg: grunt.file.readJSON('package.json'), 6 | 7 | 8 | connect: { 9 | cors: { 10 | options: { 11 | port: 4790, 12 | base: '.' 13 | } 14 | }, 15 | hole: { 16 | options: { 17 | port: 4791, 18 | base: '.' 19 | } 20 | } 21 | }, 22 | 23 | 24 | jshint: { 25 | all: ['src/*.js', 'tests/*.js'], 26 | 27 | options: { 28 | newcap: false, // "Tolerate uncapitalized constructors" 29 | node: true, 30 | expr: true, // - true && call() "Expected an assignment or function call and instead saw an expression." 31 | supernew: true, // - "Missing '()' invoking a constructor." 32 | laxbreak: true, 33 | white: true, 34 | globals: { 35 | define: true, 36 | test: true, 37 | expect: true, 38 | module: true, 39 | asyncTest: true, 40 | start: true, 41 | ok: true, 42 | equal: true, 43 | notEqual: true, 44 | deepEqual: true, 45 | window: true, 46 | document: true, 47 | performance: true 48 | } 49 | } 50 | }, 51 | 52 | 53 | qunit: { 54 | all: ['tests/index.html'], 55 | options: { 56 | '--web-security': 'no', 57 | coverage: { 58 | src: ['wormhole.js'], 59 | instrumentedFiles: 'temp/', 60 | htmlReport: 'report/coverage', 61 | coberturaReport: 'report/', 62 | linesThresholdPct: 100, 63 | functionsThresholdPct: 100, 64 | branchesThresholdPct: 100, 65 | statementsThresholdPct: 100 66 | } 67 | } 68 | }, 69 | 70 | 71 | requirejs: { 72 | src: 'src/module.js', 73 | dst: 'wormhole.js' 74 | }, 75 | 76 | 77 | watch: { 78 | scripts: { 79 | files: 'src/*.*', 80 | tasks: ['requirejs'], 81 | options: { interrupt: true } 82 | } 83 | }, 84 | 85 | version: { 86 | src: ['src/module.js'] 87 | } 88 | }); 89 | 90 | 91 | grunt.registerTask('requirejs', 'RequireJS to plain/javascript', function () { 92 | var deps = {}; 93 | 94 | function file(name) { 95 | return config.src.split('/').slice(0, -1).concat(name).join('/') + '.js'; 96 | } 97 | 98 | function parse(src, exports) { 99 | var content = grunt.file.read(src); 100 | 101 | content = content.trim().replace(/define\((\[.*?\]).*?\n/, function (_, str) { 102 | JSON.parse(str).forEach(function (name) { 103 | deps[name] = parse(file(name)); 104 | }); 105 | 106 | return ''; 107 | }); 108 | 109 | if (!exports) { 110 | content = content.replace(/\/\/\s+Export[\s\S]+/, ''); 111 | } 112 | 113 | return content.replace(/\}\);$/, ''); 114 | } 115 | 116 | var config = grunt.config(this.name); 117 | var content = parse(config.src, true); 118 | var intro = '(function (window, document) {\n"use strict";\n'; 119 | var outro = '})(window, document);'; 120 | 121 | for (var name in deps) { 122 | intro += deps[name] + '\n\n'; 123 | } 124 | 125 | grunt.log.oklns('Build:', config.dst); 126 | grunt.log.oklns('Deps:', Object.keys(deps).join(', ')); 127 | 128 | grunt.file.write(config.dst, intro + content + outro); 129 | }); 130 | 131 | 132 | grunt.loadNpmTasks('grunt-contrib-jshint'); 133 | grunt.loadNpmTasks('grunt-qunit-istanbul'); 134 | grunt.loadNpmTasks('grunt-contrib-connect'); 135 | grunt.loadNpmTasks('grunt-contrib-watch'); 136 | grunt.loadNpmTasks('grunt-version'); 137 | 138 | 139 | grunt.registerTask('dev', ['connect', 'requirejs', 'watch']); 140 | grunt.registerTask('build', ['version', 'requirejs']); 141 | grunt.registerTask('test', ['jshint', 'build', 'connect', 'qunit']); 142 | grunt.registerTask('default', ['test']); 143 | }; 144 | -------------------------------------------------------------------------------- /ws.js: -------------------------------------------------------------------------------- 1 | (function umd(factory) { 2 | if (typeof define === 'function' && define.amd) { 3 | define(['./wormhole'], factory); 4 | } else if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') { 5 | module.exports = factory(require('./wormhole')); 6 | } else { 7 | window.wormhole.WS = factory(window.wormhole); 8 | } 9 | })(function factory(wormhole) { 10 | 'use strict'; 11 | 12 | var ns = '__wormhole.js/ws__:'; 13 | 14 | var META_CMD = ns + 'meta:cmd'; 15 | var SEND_CMD = ns + 'send:cmd'; 16 | 17 | var META_EVENT = ns + 'meta'; 18 | var MESSAGE_EVENT = ns + 'msg'; 19 | 20 | /** 21 | * Wormhole WebSocket (proxy object) 22 | * @param {string} url 23 | * @param {[]string} [protocols] 24 | * @param {womrhole.Hole} [hole] 25 | */ 26 | function WS(url, protocols, hole) { 27 | var _this = this; 28 | 29 | _this.master = false; 30 | _this.url = url; 31 | _this.protocols = protocols; 32 | _this.hole = hole = (hole || wormhole()); 33 | 34 | // Meta info about connection state 35 | _this.meta = { 36 | error: null, 37 | closed: null, 38 | }; 39 | 40 | // Subscribe on change meta info 41 | hole.on(META_EVENT, function (meta) { 42 | _this.handleMeta(meta); 43 | }); 44 | 45 | // Subscribe on WebSocket.onmessage 46 | hole.on(MESSAGE_EVENT, function (evt) { 47 | _this.onmessage(evt); 48 | }); 49 | 50 | hole.on('master', function () { 51 | _this.connect(); 52 | }); 53 | 54 | // Register command on master for getting meta info for slaves 55 | hole[META_CMD] = function () { 56 | return _this.meta; 57 | }; 58 | 59 | // Register command for send data to WS 60 | hole[SEND_CMD] = function (data) { 61 | _this.socket.send(data); 62 | }; 63 | 64 | if (hole.master) { 65 | _this.connect(); 66 | } else { 67 | hole.call(META_CMD, null, function (err, meta) { 68 | err && console.warn('[meta] wormhole.js/ws:', err); 69 | meta && _this.handleMeta(meta) 70 | }); 71 | } 72 | } 73 | 74 | WS.prototype = { 75 | constructor: WS, 76 | 77 | /** @private */ 78 | handleMeta: function (meta) { 79 | var m = this.meta; 80 | 81 | this.meta = meta; 82 | 83 | if (meta.error) { 84 | !m.error && this.onerror(meta.error); 85 | } else if (meta.closed) { 86 | !m.closed && this.onclose(meta.closed); 87 | } else if (meta.closed === false) { 88 | (m.closed !== false) && this.onopen({type: 'open'}); 89 | } 90 | }, 91 | 92 | /** @private */ 93 | setMeta: function (meta) { 94 | this.hole.emit(META_EVENT, meta); 95 | }, 96 | 97 | /** @private */ 98 | connect: function () { 99 | var _this = this; 100 | 101 | var socket = new WebSocket(_this.url, _this.protocols); 102 | 103 | _this.socket = socket; 104 | 105 | socket.onopen = function (evt) { 106 | _this.setMeta({ 107 | closed: false, 108 | }); 109 | }; 110 | 111 | socket.onclose = function (evt) { 112 | this.setMeta({ 113 | closed: { 114 | type: evt.type, 115 | wasClean: evt.wasClean, 116 | code: evt.code, 117 | reason: evt.reason, 118 | }, 119 | }); 120 | }; 121 | 122 | socket.onerror = function (evt) { 123 | this.setMeta({ 124 | error: { 125 | type: evt.type, 126 | message: evt.message, 127 | }, 128 | }); 129 | }; 130 | 131 | socket.onmessage = function (evt) { 132 | _this.hole.emit(MESSAGE_EVENT, { 133 | type: evt.type, 134 | data: evt.data, 135 | }); 136 | }; 137 | 138 | _this.master = true; 139 | _this.onmaster({type: 'master'}); 140 | }, 141 | 142 | onmaster: function (evt) {}, 143 | onopen: function (evt) {}, 144 | onmessage: function (evt) {}, 145 | onclose: function (evt) {}, 146 | onerror: function (evt) {}, 147 | 148 | /** 149 | * Send data to WebSocket 150 | * @param {string} data 151 | */ 152 | send: function (data) { 153 | this.hole.call(SEND_CMD, data); 154 | }, 155 | }; 156 | 157 | // Export 158 | WS['default'] = WS; 159 | return WS; 160 | }); -------------------------------------------------------------------------------- /src/cors.js: -------------------------------------------------------------------------------- 1 | define(["./emitter", "./get-own"], function (Emitter, getOwn) { 2 | var _corsId = 1, 3 | _corsExpando = '__cors__', 4 | _corsCallback = {}, 5 | _parseJSON = JSON.parse, 6 | _stringifyJSON = JSON.stringify, 7 | _allowAccess = void 0 8 | ; 9 | 10 | 11 | /** 12 | * @class cors 13 | * @desc Обертка над postMessage 14 | * @param {Window} el 15 | */ 16 | function cors(el) { 17 | if (!(this instanceof cors)) { 18 | return new cors(el); 19 | } 20 | 21 | this.el = el; 22 | } 23 | 24 | 25 | cors.fn = cors.prototype = /** @lends cors.prototype */ { 26 | /** 27 | * Вызывать удаленную команду 28 | * @param {String} cmd команда 29 | * @param {*} [data] данные 30 | * @param {Function} [callback] функция обратного вызова, получает: `error` и `result` 31 | */ 32 | call: function (cmd, data, callback) { 33 | if (typeof data === 'function') { 34 | callback = data; 35 | data = void 0; 36 | } 37 | 38 | var evt = { 39 | cmd: cmd, 40 | data: data 41 | }; 42 | 43 | evt[_corsExpando] = ++_corsId; 44 | _corsCallback[_corsId] = callback; 45 | 46 | this.send(evt); 47 | }, 48 | 49 | 50 | /** 51 | * Отправить даныне 52 | * @param {*} data 53 | */ 54 | send: function (data) { 55 | var window = this.el; 56 | 57 | try { 58 | // Если это iframe 59 | window = window.contentWindow || /* istanbul ignore next */ window; 60 | } catch (err) { 61 | } 62 | 63 | try { 64 | window.postMessage(_corsExpando + _stringifyJSON(data), '*'); 65 | } 66 | catch (err) {} 67 | } 68 | }; 69 | 70 | /** 71 | * Разрешение для конкретного `origin` 72 | * @param {*} origin 73 | */ 74 | cors.allowAccess = function (origin) { 75 | if (typeof origin === 'string' || origin instanceof RegExp) { 76 | _allowAccess = origin; 77 | } 78 | }; 79 | 80 | /** 81 | * Установка кастомного префикса `expando` 82 | * @param {String} expando 83 | */ 84 | cors.setExpando = function (expando) { 85 | if (typeof expando === 'string') { 86 | _corsExpando = expando; 87 | } 88 | }; 89 | 90 | 91 | 92 | 93 | 94 | /** 95 | * Проверка на соответствие `targetOrigin` 96 | * @param {*} targetOrigin 97 | * @private 98 | */ 99 | function _checkAccess(targetOrigin) { 100 | if (_allowAccess == void 0) { 101 | return true; 102 | } else if (_allowAccess instanceof RegExp) { 103 | return _allowAccess.test(targetOrigin); 104 | } else if (typeof _allowAccess === 'string') { 105 | return targetOrigin === _allowAccess; 106 | } 107 | 108 | return false; 109 | } 110 | 111 | /** 112 | * Получение `postMessage` 113 | * @param {Event} evt 114 | * @private 115 | */ 116 | function _onmessage(evt) { 117 | var origin, 118 | id, 119 | resp = {}, 120 | data = evt.data, 121 | source = evt.source, 122 | func; 123 | 124 | evt = evt || /* istanbul ignore next */ window.event; 125 | origin = evt.origin || evt.originalEvent.origin; 126 | 127 | /* istanbul ignore else */ 128 | if (typeof data === 'string' && data.indexOf(_corsExpando) === 0 && _checkAccess(origin)) { 129 | // Наше сообщение 130 | try { 131 | // Парсим данные 132 | data = _parseJSON(evt.data.substr(_corsExpando.length)); 133 | id = data[_corsExpando]; 134 | 135 | if (id) { 136 | // Это call или ответ на него 137 | if (data.response) { 138 | /* istanbul ignore else */ 139 | if (_corsCallback[id]) { 140 | _corsCallback[id](data.error, data.result); 141 | delete _corsCallback[id]; 142 | } 143 | } 144 | else { 145 | // Фомируем ответ 146 | resp.response = 147 | resp[_corsExpando] = id; 148 | 149 | try { 150 | func = getOwn(cors, data.cmd); 151 | 152 | if (func) { 153 | resp.result = func(data.data, source); 154 | } else { 155 | throw 'method not found'; 156 | } 157 | } catch (err) { 158 | resp.error = 'wormhole.cors.' + data.cmd + ': ' + err.toString(); 159 | } 160 | 161 | cors(evt.source).send(resp); 162 | } 163 | } 164 | else { 165 | cors.emit('data', [data, source]); 166 | } 167 | 168 | } 169 | catch (err) { 170 | /* istanbul ignore next */ 171 | cors.emit('error', err); 172 | } 173 | } 174 | } 175 | 176 | 177 | // Подмешиваем 178 | Emitter.apply(cors); 179 | 180 | 181 | /* istanbul ignore else */ 182 | if (window.addEventListener) { 183 | window.addEventListener('message', _onmessage, false); 184 | } else { 185 | window.attachEvent('onmessage', _onmessage); 186 | } 187 | 188 | 189 | // Export 190 | return cors; 191 | }); 192 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Wormhole 2 | It's better EventEmitter for communication between tabs with supporting Master/Slave. 3 | 4 | ``` 5 | npm i --save-dev wormhole.js 6 | ``` 7 | 8 | 9 | ### Features 10 | * [One connection on WebSocket](#ws) for all tabs. 11 | * Support [Master/Slave](#ms) 12 | * [Cross-domain communication](#cors) 13 | * SharedWorker or fallback to localStorage 14 | * IE 8+, Chrome 10+, FireFox 10+, Opera 10+, Safari 6+ 15 | * Test coverage ([run](http://rubaxa.github.io/wormhole/tests/)) 16 | 17 | 18 | --- 19 | 20 | 21 | ### Basic example 22 | 23 | ```js 24 | import wormhole from 'wormhole.js'; // yes, with ".js", it's not mistake 25 | 26 | // All tabs 27 | wormhole().on('coords', (x, y) => { 28 | console.log(x, y); 29 | }); 30 | 31 | // Some tab 32 | wormhole().emit('coords', [5, 10]); 33 | 34 | // Master tab 35 | if (wormhole().master) { 36 | // .. 37 | } 38 | 39 | wormhole().on('master', () => { 40 | console.log('Wow!'); 41 | }); 42 | ``` 43 | 44 | 45 | --- 46 | 47 | 48 | 49 | 50 | ### One connection on WebSocket for all tabs 51 | Module `wormhole.js/ws` implements WebSocket-like interface: 52 | https://rubaxa.github.io/wormhole/?ws=y 53 | 54 | ```js 55 | import WS from 'wormhole.js/ws'; 56 | 57 | // Create WebScoket (wormhole-socket) 58 | const socket = new WS('ws://echo.websocket.org'); // OR new WS('...', null, hole); 59 | 60 | socket.onopen = () => console.log('Connected'); 61 | socket.onmessage = ({data}) => console.log('Received:', data); 62 | 63 | // Unique event 64 | socket.onmaster = () => { 65 | console.log('Yes, I\'m master!'); 66 | }; 67 | 68 | // Some tab 69 | socket.send({foo: 'bar'}) 70 | 71 | // All tabs: 72 | // "Received:" {foo: 'bar'} 73 | ``` 74 | 75 | --- 76 | 77 | 78 | 79 | 80 | ### CORS example 81 | 82 | 1. Create a subdomain, ex.: `http://wormhole.youdomain.com/`; 83 | 2. Copy-paste [universal.html](./universal.html) and [wormhole.js](./wormhole.js) into root; 84 | 3. Check access `http://wormhole.youdomain.com/universal.html`; 85 | 4. Profit: 86 | 87 | ```js 88 | // http://foo.youdomain.com/ 89 | import {Universal} from 'wormhole.js'; 90 | const hole = new Universal('http://wormhole.youdomain.com/universal.html'); 91 | 92 | hole.on('data', (data) => { 93 | console.log(data); 94 | }); 95 | 96 | 97 | // http://bar.youdomain.com/ 98 | import {Universal} from 'wormhole.js'; 99 | const hole = new Universal('http://wormhole.youdomain.com/universal.html'); 100 | 101 | hole.emit('data', 'any data'); 102 | ``` 103 | 104 | 105 | --- 106 | 107 | 108 | 109 | 110 | ### Master/slave example 111 | 112 | ```js 113 | import wormhole from 'wormhole.js'; 114 | 115 | // All tabs (main.js) 116 | // Define remote command (master) 117 | wormhole()['get-data'] = (function (_cache) { 118 | return function getData(req, callback) { 119 | if (!_cache.hasOwnProperty(req.url)) { 120 | _cache[req.url] = fetch(req.url).then(res => res.json()); 121 | } 122 | 123 | return _cache[key]; 124 | }; 125 | })({}); 126 | 127 | // Get remote data method 128 | function getData(url) { 129 | return new Promise((resolve, reject) => { 130 | // Calling command on master (from slave... or the master, is not important) 131 | wormhole().call( 132 | 'get-data', // command 133 | {url}, // arguments 134 | (err, json) => err ? reject(err) : resolve(json) // callback(err, result) 135 | ); 136 | }); 137 | }; 138 | 139 | // I'm master! 140 | wormhole().on('master', () => { 141 | // some code 142 | }); 143 | 144 | // Tab #X 145 | getData('/path/to/api').then((json) => { 146 | // Send ajax request 147 | console.log(result); 148 | }); 149 | 150 | // Tab #Y 151 | getData('/path/to/api').then((result) => { 152 | // From master cache 153 | console.log(result); 154 | }); 155 | ``` 156 | 157 | 158 | --- 159 | 160 | 161 | ### Peers 162 | 163 | ```js 164 | wormhole() 165 | .on('peers', (peers) => { 166 | console.log('ids:', peers); // ['tab-id-1', 'tab-id-2', ..] 167 | }) 168 | .on('peers:add', (id) => { 169 | // .. 170 | }) 171 | .on('peers:remove', (id) => { 172 | // .. 173 | }) 174 | ; 175 | ``` 176 | 177 | --- 178 | 179 | 180 | ### Executing the command on master 181 | 182 | 183 | ```js 184 | // Register command (all tabs) 185 | wormhole()['foo'] = (data, next) => { 186 | // bla-bla-bla 187 | next(null, data.reverse()); // or `next('error')` 188 | }; 189 | 190 | 191 | // Calling the command (some tab) 192 | wormhole().call('foo', [1, 2, 3], (err, results) => { 193 | console.log(results); // [3, 2, 1] 194 | }) 195 | ``` 196 | 197 | 198 | --- 199 | 200 | 201 | ### Modules 202 | 203 | - [Emitter](#m-emitter) — Micro event emitter 204 | - [cors](#m-cors) — Handy wrapper over `postMessage`. 205 | - [store](#m-store) — Safe and a handy wrapper over `localStorage`. 206 | 207 | 208 | --- 209 | 210 | 211 | 212 | #### wormhole.Emitter 213 | Micro event emitter. 214 | 215 | - **on**(type:`String`, fn:`Function`):`this` 216 | - **off**(type:`String`, fn:`Function`):`this` 217 | - **emit**(type:`String`[, args:`*|Array`]):`this` 218 | 219 | ```js 220 | import {Emitter} from 'wormhole.js'; 221 | 222 | const obj = Emitter.apply({}); // or new wormhole.Emitter(); 223 | 224 | obj.on('foo', () => { 225 | console.log(arguments); 226 | }); 227 | 228 | obj.emit('foo'); // [] 229 | obj.emit('foo', 1); // [1] 230 | obj.emit('foo', [1, 2, 3]); // [1, 2, 3] 231 | ``` 232 | 233 | 234 | --- 235 | 236 | 237 | 238 | #### wormhole.cors 239 | Handy wrapper over `postMessage`. 240 | 241 | ```js 242 | import {cors} from 'wormhole.js'; 243 | 244 | // Main-document 245 | cors.on('data', (data) => { 246 | console.log('Received:', data); 247 | }); 248 | 249 | cors['some:command'] = (value) => value * 2; 250 | 251 | // IFrame 252 | cors(parent).send({foo: 'bar'}); 253 | // [main-document] "Received:" {foo: 'bar'} 254 | 255 | cors(parent).call('some:command', 3, (err, result) => { 256 | console.log('Error:', err, 'Result:', result); 257 | // [iframe] "Error:" null "Result:" 6 258 | }); 259 | ``` 260 | 261 | 262 | --- 263 | 264 | 265 | 266 | #### wormhole.store 267 | Safe and a handy wrapper over `localStorage`. 268 | 269 | - **get**(key:`String`):`*` 270 | - **set**(key:`String`, value:`*`) 271 | - **remove**(key:`String`) 272 | - **on**(type:`String`, fn:`Function`) 273 | - **off**(type:`String`, fn:`Function`) 274 | 275 | ```js 276 | import {store} from 'wormhole.js'; 277 | 278 | store.on('change', (key, data) => { 279 | console.log('change -> ', key, data); 280 | }); 281 | 282 | store.on('change:prop', (key, value) => { 283 | console.log('change:prop -> ', key, value); 284 | }); 285 | 286 | store.set('foo', {bar: 'baz'}); 287 | // change -> foo {bar: 'baz'} 288 | 289 | store.set('prop', {qux: 'ok'}); 290 | // change -> prop {qux: 'ok'} 291 | // change:prop -> prop {qux: 'ok'} 292 | ``` 293 | 294 | --- 295 | 296 | 297 | ### Utils 298 | 299 | - [uuid](#uuid) 300 | - [debounce](#debounce) 301 | 302 | 303 | --- 304 | 305 | 306 | 307 | ##### wormhole.uuid():`String` 308 | A universally unique identifier (UUID) is an identifier standard used in software construction, 309 | standardized by the Open Software Foundation (OSF) as part of the Distributed Computing Environment (DCE) 310 | (c) [wiki](https://en.wikipedia.org/wiki/Universally_unique_identifier). 311 | 312 | 313 | --- 314 | 315 | 316 | 317 | ##### wormhole.debounce(fn:`Function`, delay:`Number`[, immediate:`Boolean`]):`Function` 318 | 319 | Creates and returns a new debounced version of the passed function that will postpone its execution until after wait milliseconds have elapsed since the last time it was invoked. 320 | 321 | ---- 322 | 323 | 324 | ### Development 325 | 326 | - `npm test` 327 | - `npm run dev` — run dev watcher 328 | - `npm run build` -------------------------------------------------------------------------------- /src/store.js: -------------------------------------------------------------------------------- 1 | define(["./emitter", "./cors"], function (Emitter, cors) { 2 | var store, 3 | _storage, 4 | _storageNS = '__wh.store__.', 5 | _storageData = {}, // key => Object 6 | _storageItems = {}, // key => String 7 | 8 | _parseJSON = JSON.parse, 9 | _stringifyJSON = JSON.stringify 10 | ; 11 | 12 | 13 | function _storageKey(key) { 14 | return _storageNS + key; 15 | } 16 | 17 | 18 | function _isStoreKey(key) { 19 | return key && (key !== _storageNS) && (key.indexOf(_storageNS) === 0); 20 | } 21 | 22 | 23 | function _getCleanedKey(key) { 24 | return key.substr(_storageNS.length); 25 | } 26 | 27 | 28 | /** 29 | * Получить рабочий storage по названию 30 | * @param {String} name 31 | * @returns {sessionStorage} 32 | * @private 33 | */ 34 | function _getStorage(name) { 35 | try { 36 | var storage = window[name + 'Storage']; 37 | 38 | storage.setItem(_storageNS, _storageNS); 39 | 40 | /* istanbul ignore else */ 41 | if (storage.getItem(_storageNS) == _storageNS) { 42 | storage.removeItem(_storageNS); 43 | return storage; 44 | } 45 | } catch (err) { } 46 | } 47 | 48 | 49 | // Пробуем получить sessionStorage, либо localStorage 50 | _storage = _getStorage('local'); 51 | 52 | 53 | /** 54 | * @desc Хранилище 55 | * @module {store} 56 | */ 57 | store = Emitter.apply(/** @lends store */{ 58 | /** 59 | * Статус хранилища 60 | * @type {boolean} 61 | */ 62 | enabled: !!_storage, 63 | 64 | 65 | /** 66 | * Установить значение 67 | * @param {String} key 68 | * @param {*} value 69 | */ 70 | set: function (key, value) { 71 | var fullKey = _storageKey(key); 72 | 73 | value = _stringifyJSON(value); 74 | 75 | _storage && _storage.setItem(fullKey, value); 76 | _onsync({ key: fullKey }, value); // принудительная синхронизация 77 | }, 78 | 79 | 80 | /** 81 | * Получить значение 82 | * @param {String} key 83 | * @returns {*} 84 | */ 85 | get: function (key) { 86 | var value = _storage.getItem(_storageKey(key)); 87 | return typeof value === 'string' ? _parseJSON(value) : value; 88 | }, 89 | 90 | 91 | /** 92 | * Удалить значение 93 | * @param {String} key 94 | */ 95 | remove: function (key) { 96 | delete _storageData[key]; 97 | delete _storageItems[key]; 98 | _storage && _storage.removeItem(_storageKey(key)); 99 | }, 100 | 101 | 102 | /** 103 | * Получить все данные из хранилища 104 | * @retruns {Array} 105 | */ 106 | getAll: function () { 107 | var i = 0, 108 | n, 109 | key, 110 | data = {}; 111 | 112 | if (_storage) { 113 | n = _storage.length; 114 | 115 | for (; i < n; i++ ) { 116 | key = _storage.key(i); 117 | 118 | if (_isStoreKey(key)) { 119 | data[_getCleanedKey(key)] = _parseJSON(_storage.getItem(key)); 120 | } 121 | } 122 | } 123 | 124 | return data; 125 | }, 126 | 127 | 128 | /** 129 | * Пройтись по всем ключам 130 | * @param {Function} iterator 131 | */ 132 | each: function (iterator) { 133 | if (_storage) { 134 | for (var i = 0, n = _storage.length, key; i < n; i++) { 135 | key = _storage.key(i); 136 | if (_isStoreKey(key)) { 137 | iterator(_parseJSON(_storage.getItem(key)), _getCleanedKey(key)); 138 | } 139 | } 140 | } 141 | } 142 | }); 143 | 144 | 145 | /** 146 | * Обработчик обновления хранилища 147 | * @param {Event|Object} evt 148 | * @param {String} [value] 149 | * @private 150 | */ 151 | function _onsync(evt, value) { 152 | var i = 0, 153 | n = _storage.length, 154 | fullKey = evt.key, 155 | key; 156 | 157 | // Синхронизация работает 158 | store.events = true; 159 | 160 | if (!fullKey) { 161 | // Плохой браузер, придется искать самому, что изменилось 162 | for (; i < n; i++ ) { 163 | fullKey = _storage.key(i); 164 | 165 | if (_isStoreKey(fullKey)) { 166 | value = _storage.getItem(fullKey); 167 | 168 | if (_storageItems[fullKey] !== value) { 169 | _storageItems[fullKey] = value; 170 | _onsync({ key: fullKey }, value); 171 | } 172 | } 173 | } 174 | } 175 | else if (_isStoreKey(fullKey)) { 176 | key = _getCleanedKey(fullKey); 177 | 178 | if (key) { // Фильтруем событий при проверки localStorage 179 | value = value !== void 0 ? value : _storage.getItem(fullKey); 180 | _storageData[key] = _parseJSON(value); 181 | _storageItems[fullKey] = value + ''; 182 | 183 | store.emit('change', [key, _storageData]); 184 | store.emit('change:' + key, [key, _storageData[key]]); 185 | } 186 | } 187 | } 188 | 189 | 190 | // Получаем текущее состояние 191 | _storage && (function () { 192 | var i = _storage.length, 193 | fullKey, 194 | key, 195 | value, 196 | _onsyncNext = function (evt) { 197 | setTimeout(function () { 198 | _onsync(evt); 199 | }, 0); 200 | }; 201 | 202 | /* istanbul ignore next */ 203 | while (i--) { 204 | fullKey = _storage.key(i); 205 | 206 | if (_isStoreKey(fullKey)) { 207 | key = _getCleanedKey(fullKey); 208 | value = _storage.getItem(fullKey); 209 | 210 | _storageData[key] = _parseJSON(value); 211 | _storageItems[fullKey] = value; 212 | } 213 | } 214 | 215 | /* istanbul ignore else */ 216 | if (window.addEventListener) { 217 | window.addEventListener('storage', _onsyncNext); 218 | document.addEventListener('storage', _onsyncNext); 219 | } else { 220 | window.attachEvent('onstorage', _onsyncNext); 221 | document.attachEvent('onstorage', _onsyncNext); 222 | } 223 | 224 | 225 | // Проверяем рабочесть события хранилища (Bug #136356) 226 | // _storage.setItem('ping', _storageNS); 227 | // setTimeout(function () { 228 | // _storage.removeItem('ping' + _storageNS); 229 | // 230 | // if (!store.events) { 231 | // console.log('onStorage not supported:', location.href, store.events); 232 | // setInterval(function () { _onsync({}); }, 250); 233 | // } 234 | // }, 500); 235 | })(); 236 | 237 | 238 | /** 239 | * Получить удаленное хранилище 240 | * @param {string} url 241 | * @param {function} ready 242 | * @returns {store} 243 | */ 244 | store.remote = function (url, ready) { 245 | var _data = {}; 246 | var _store = Emitter.apply({ 247 | set: function (key, name) { 248 | _data[key] = name; 249 | 250 | _store.emit('change', [key, _data]); 251 | _store.emit('change:' + key, [key, _data[key]]); 252 | }, 253 | 254 | get: function (key) { 255 | return _data[key]; 256 | }, 257 | 258 | remove: function (key) { 259 | delete _data[key]; 260 | }, 261 | 262 | getAll: function () { 263 | return _data; 264 | }, 265 | 266 | each: function (iterator) { 267 | for (var key in _data) { 268 | if (_data.hasOwnProperty(key)) { 269 | iterator(_data, key); 270 | } 271 | } 272 | } 273 | }); 274 | 275 | var iframe = document.createElement('iframe'); 276 | var facade = cors(iframe); 277 | 278 | iframe.onload = function () { 279 | facade.call('register', [], function (err, storeData) { 280 | if (storeData) { 281 | iframe.onload = null; 282 | 283 | // Получаем данные хранилища 284 | for (var key in storeData) { 285 | if (storeData.hasOwnProperty(key)) { 286 | _data[key] = storeData[key]; 287 | } 288 | } 289 | 290 | // Получаем данные от iframe 291 | cors.on('data', function (evt) { 292 | var key = evt.key; 293 | var data = evt.data; 294 | var value = data[key]; 295 | 296 | _data[key] = value; 297 | 298 | _store.emit('change', [key, data]); 299 | _store.emit('change:' + key, [key, value]); 300 | }); 301 | 302 | // Установить 303 | _store.set = function (key, value) { 304 | facade.call('store', { cmd: 'set', key: key, value: value }); 305 | }; 306 | 307 | // Удалить 308 | _store.remove = function (key) { 309 | delete _data[key]; 310 | facade.call('store', { cmd: 'remove', key: key }); 311 | }; 312 | 313 | ready && ready(_store); 314 | } 315 | }); 316 | }; 317 | 318 | iframe.src = url; 319 | iframe.style.left = '-1000px'; 320 | iframe.style.position = 'absolute'; 321 | 322 | // Пробуем вставить в body 323 | (function _tryAgain() { 324 | try { 325 | document.body.appendChild(iframe); 326 | } catch (err) { 327 | setTimeout(_tryAgain, 100); 328 | } 329 | })(); 330 | 331 | return _store; 332 | }; 333 | 334 | 335 | 336 | // Export 337 | return store; 338 | }); 339 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | wormhole.js — is EventEmitter for communication between tabs. 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | Fork me on GitHub 20 | 21 |
22 | 23 |
24 | 56 | 57 |
58 | 59 | 77 |
78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 95 | 96 | 97 | -------------------------------------------------------------------------------- /tests/wormhole.hole.tests.js: -------------------------------------------------------------------------------- 1 | (function _tryAgainTest(useStore) { 2 | QUnit.module('wormhole.Hole.' + (useStore ? 'store' : 'worker')); 3 | 4 | var ie8 = /MSIE 8/.test(navigator.userAgent); 5 | 6 | 7 | function newHole(el, url) { 8 | var Hole = wormhole.Hole; 9 | 10 | if (el) { 11 | Hole = (el.contentWindow && el.contentWindow.wormhole.Hole) || el.wormhole.Hole; 12 | } 13 | 14 | return new Hole(url || 'local.test.html', useStore); 15 | } 16 | 17 | 18 | function createHole(url) { 19 | return _createWin(url).then(function (el) { 20 | return newHole(el, url); 21 | }); 22 | } 23 | 24 | 25 | QUnit.test('core', function (assert) { 26 | var log = []; 27 | var fooLog = []; 28 | var url = 'local.test.html?core'; 29 | var main = newHole(null, url); 30 | var done = assert.async() 31 | 32 | 33 | main 34 | .emit('xxx') 35 | .on('ready', function (hole) { 36 | if (useStore) { 37 | assert.ok(hole.emit === hole._storeEmit, 'sotreEmit'); 38 | } else { 39 | assert.ok(hole.worker instanceof window.SharedWorker, 'instanceof'); 40 | assert.ok(hole.emit === hole._workerEmit, 'workerEmit'); 41 | } 42 | 43 | log.push('ready:' + hole.id); 44 | }) 45 | .on('master', function (hole) { 46 | log.push('master:' + hole.id); 47 | 48 | 49 | _createWin('local.test.html?hole=1').then(function (el) { 50 | newHole(el, url).on('ready', function (hole) { 51 | ie8 && hole.destroy(); 52 | $(el).remove(); 53 | }); 54 | 55 | _createWin('local.test.html?hole=2').then(function (el) { 56 | newHole(el, url) 57 | .on('master', function () { log.push('master:slave'); }) 58 | .on('foo', function () { fooLog.push(this.id); }) 59 | ; 60 | }); 61 | 62 | _createWin('local.test.html?hole=3').then(function (el) { 63 | newHole(el, url) 64 | .on('master', function () { log.push('master:slave'); }) 65 | .on('ready', function () { main.destroy(); }) 66 | .on('foo', function () { fooLog.push(this.id); }) 67 | .emit('foo', [1, '-', 1]) 68 | ; 69 | }); 70 | 71 | _createWin('local.test.html?hole=4').then(function (el) { 72 | newHole(el, url) 73 | .on('master', function () { log.push('master:slave'); }) 74 | .on('foo', function () { fooLog.push(this.id); }) 75 | ; 76 | }); 77 | 78 | _createWin('local.test.html?hole=5').then(function (el) { 79 | newHole(el, url).on('master', function () { log.push('master:slave'); }); 80 | }); 81 | }); 82 | }); 83 | 84 | 85 | 86 | setTimeout(function () { 87 | main.destroy(); 88 | 89 | assert.deepEqual(log, [ 90 | 'ready:' + main.id, 91 | 'master:' + main.id, 92 | 'master:slave' 93 | ]); 94 | 95 | done(); 96 | }, 1500); 97 | }); 98 | 99 | 100 | // Проверка peers 101 | QUnit.test('peers', function (assert) { 102 | var max = 10; // кол-во iframe 103 | var tabs = []; 104 | var done = assert.async(); 105 | 106 | function holes() { 107 | for (var i = 0; i < max; i++) { 108 | /* jshint loopfunc:true */ 109 | tabs.push(_createWin('local.test.html?peers=' + i).then(function (el) { 110 | var hole = newHole(el, 'local.test.html?peers'); 111 | hole.el = el; 112 | return hole; 113 | })); 114 | } 115 | 116 | return $.when.apply($, tabs); 117 | } 118 | 119 | // Первая пачка по 10 iframe 120 | holes().then(function (tab) { 121 | var count = 0; 122 | 123 | arguments[Math.floor(max/2)].on('peers', function (peers) { 124 | count = peers.length; 125 | assert.ok(true, '#1.count: ' + count); 126 | }); 127 | 128 | setTimeout(function () { 129 | assert.equal(count, max, '#2.total: ' + max); 130 | assert.equal(tab.length, max, '#2.length: ' + max); 131 | 132 | // Вторая пачка по 10 iframe 133 | holes().then(function () { 134 | setTimeout(function () { 135 | assert.equal(count, max*2, '#3.total: ' + max*2); 136 | assert.equal(tab.length, max*2, '#3.length: ' + max*2); 137 | 138 | // Третья пачка по 10 iframe 139 | holes().then(function () { 140 | var tabs = [].slice.call(arguments); 141 | var removeCnt = 7; 142 | 143 | setTimeout(function () { 144 | assert.equal(count, max*3, '#4.total: ' + max*3); 145 | assert.equal(tab.length, max*3, '#4.length: ' + max*3); 146 | 147 | tab = tabs[10].on('peers', function (peers) { 148 | count = peers.length; 149 | assert.ok(true, '#5.10.count: ' + count); 150 | }); 151 | 152 | $.each(tabs.splice(0, removeCnt), function (i, hole) { 153 | ie8 && hole.destroy(); 154 | $(hole.el).remove(); 155 | }); 156 | 157 | setTimeout(function () { 158 | assert.equal(count, max*3 - removeCnt, '#6.total: ' + (max*3 - removeCnt)); 159 | assert.equal(tab.length, max*3 - removeCnt, '#6.length: ' + (max*3 - removeCnt)); 160 | 161 | tab = tabs.pop().on('peers', function (peers) { 162 | count = peers.length; 163 | assert.ok(true, '#7.pop.count: ' + count); 164 | }); 165 | 166 | $.each(tabs, function (i, hole) { 167 | ie8 && hole.destroy(); 168 | $(hole.el).remove(); 169 | }); 170 | 171 | setTimeout(function () { 172 | assert.equal(count, 1, '#8.total: 1'); 173 | assert.equal(tab.length, 1, '#8.length: 1'); 174 | done(); 175 | }, 1000); 176 | }, 2000); 177 | }, 2000); 178 | }); 179 | }, 2000); 180 | }); 181 | }, 2000); 182 | }); 183 | }); 184 | 185 | 186 | // Проверка событий 187 | QUnit.test('peers:events', function (assert) { 188 | var actual = {}; 189 | var expected = {}; 190 | var done = assert.async(); 191 | 192 | createHole('local.test.html?peers:event').then(function (hole) { 193 | expected['add:' + hole.id] = 1; 194 | 195 | hole.on('peers:add', function (id) { 196 | actual['add:' + id] = 1; 197 | }); 198 | 199 | setTimeout(function () { 200 | createHole('local.test.html?peers:event').then(function (someHole) { 201 | expected['add:' + someHole.id] = 1; 202 | expected['some-add:' + hole.id] = 1; 203 | expected['some-add:' + someHole.id] = 1; 204 | expected['some-remove:' + hole.id] = 1; 205 | 206 | someHole.on('peers:add', function (id) { 207 | actual['some-add:' + id] = 1; 208 | }); 209 | 210 | someHole.on('peers:remove', function (id) { 211 | actual['some-remove:' + id] = 1; 212 | }); 213 | 214 | setTimeout(function () { 215 | assert.equal(someHole.length, 2, 'length'); 216 | hole.destroy(); 217 | 218 | setTimeout(function () { 219 | assert.deepEqual(actual, expected); 220 | done(); 221 | }, 100); 222 | }, 100); 223 | }); 224 | }, 100); 225 | }); 226 | }); 227 | 228 | 229 | // Проверка на мастер 230 | QUnit.test('master', function (assert) { 231 | var max = 10; // кол-во iframe 232 | var tabs = []; 233 | var log = []; 234 | var pid; 235 | var done = assert.async(); 236 | 237 | 238 | for (var i = 0; i < max; i++) { 239 | tabs.push(_createWin('local.test.html?master=' + i)); 240 | } 241 | 242 | 243 | $.when.apply($, tabs).then(function () { 244 | $.each(arguments, function (i, el) { 245 | newHole(el, 'local.test.html?master') 246 | .on('ready', function () { 247 | // console.log('hole.ready: ' + this.id); 248 | }) 249 | .on('master', function (hole) { 250 | assert.ok(true, '#' + i + ':' + hole.id); 251 | log.push(hole.id); 252 | 253 | ie8 && hole.destroy(); 254 | $(el).remove(); 255 | 256 | clearTimeout(pid); 257 | pid = setTimeout(function () { 258 | assert.equal(log.length, max); // кол-во мастеров 259 | 260 | $.each(log, function (idx, id) { 261 | for (var i = idx; i < log.length; i++) { 262 | if (log[i] === id) { 263 | assert.ok(true, '#' + idx); 264 | return; 265 | } 266 | } 267 | 268 | assert.ok(false, '#' + idx); 269 | }); 270 | 271 | done(); 272 | }, 1500); 273 | }); 274 | }); 275 | }); 276 | }); 277 | 278 | 279 | // Проверяем события между воркерами (в рамках одного домена) 280 | QUnit.test('events', function () { 281 | var max = 10; // кол-во iframe 282 | var tabs = []; 283 | var syncLogs = {}; 284 | var asyncLogs = {}; 285 | var done = assert.async(); 286 | 287 | for (var i = 0; i < max; i++) { 288 | tabs.push(_createWin('local.test.html?hole=' + i)); 289 | } 290 | 291 | $.when.apply($, tabs).then(function () { 292 | $.each(arguments, function (i, el) { 293 | var hole = newHole(el, 'local.test.html?events') 294 | .on('sync', function (data) { 295 | syncLogs[i] = (syncLogs[i] || []); 296 | syncLogs[i].push(data); 297 | }) 298 | .on('async', function (data) { 299 | asyncLogs[i] = (asyncLogs[i] || []); 300 | asyncLogs[i].push(data); 301 | }) 302 | ; 303 | 304 | hole.emit('sync', i); 305 | 306 | setTimeout(function () { 307 | hole.emit('async', i); 308 | }, 10); 309 | }); 310 | 311 | setTimeout(function () { 312 | assert.equal(syncLogs[0] && syncLogs[0].length, max, 'sync.length'); 313 | assert.equal(asyncLogs[0] && asyncLogs[0].length, max, 'async.length'); 314 | 315 | for (var i = 0; i < max; i++) { 316 | assert.deepEqual(syncLogs[i], syncLogs[0], 'hole.sync #' + i); 317 | assert.deepEqual(asyncLogs[i], asyncLogs[0], 'hole.async #' + i); 318 | } 319 | 320 | done(); 321 | }, 1500); 322 | }, function () { 323 | assert.ok(false, 'fail'); 324 | done(); 325 | }); 326 | }); 327 | 328 | 329 | // Проверка вызова удаленных команд 330 | QUnit.test('cmd', function () { 331 | var url = 'local.test.html?cmd'; 332 | var actual = {}; 333 | var expected = {}; 334 | var done = assert.async() 335 | var _finish = wormhole.debounce(function () { 336 | assert.deepEqual(actual, expected); 337 | done(); 338 | }, 600), 339 | 340 | _set = function (key, value) { 341 | assert.ok(!(key in actual), key + ((key in actual) ? ' - already added' : '')); 342 | actual[key] = value; 343 | _finish(); 344 | } 345 | ; 346 | 347 | function newTabHole() { 348 | return _createWin(url).then(function (el) { 349 | _finish(); 350 | return new newHole(el, url); 351 | }); 352 | } 353 | 354 | $.Deferred().resolve(new wormhole.Hole(url, useStore)).then(function (foo) { 355 | expected['foo:' + foo.id] = 1; 356 | expected['foo.foo'] = 1; 357 | expected['foo.master'] = 1; 358 | expected['foo.sync'] = 'ok'; 359 | expected['foo.async'] = 'aok'; 360 | 361 | foo.on('master', function () { 362 | _set('foo:' + this.id, 1); 363 | _set('foo.master', 1); 364 | }); 365 | 366 | // Определяем команду (синхронную) 367 | foo.foo = function (data) { 368 | _set('foo.foo', data); 369 | return data * 2; 370 | }; 371 | 372 | foo.sync = function _(data, next) { 373 | assert.ok(true, 'foo.async'); 374 | next(null, data); 375 | }; 376 | 377 | foo.async = function _(data, next) { 378 | assert.ok(true, 'foo.async'); 379 | setTimeout(function () { 380 | next(null, data); 381 | }, 10); 382 | }; 383 | 384 | foo.fail = function () { 385 | assert.ok(true, 'foo.fail'); 386 | throw "BOOM!"; 387 | }; 388 | 389 | newTabHole().then(function (bar) { 390 | expected['bar.foo.result'] = 2; 391 | expected['bar.fail.result'] = 'wormhole.fail: BOOM!'; 392 | expected['bar.unkown.result'] = 'wormhole.unkown: method not found'; 393 | 394 | bar.on('master', function () { 395 | _set('bar:' + bar.id, 1); 396 | }); 397 | 398 | // Вызываем команду 399 | bar.call('foo', 1, function (err, result) { 400 | _set('bar.foo.result', result); 401 | }); 402 | 403 | bar.call('fail', function (err) { 404 | _set('bar.fail.result', err); 405 | }); 406 | 407 | bar.call('sync', 'ok', function (err, result) { 408 | _set('foo.sync', result); 409 | }); 410 | bar.call('async', 'aok', function (err, result) { 411 | _set('foo.async', result); 412 | }); 413 | 414 | bar.call('unkown', function (err) { 415 | _set('bar.unkown.result', err); 416 | }); 417 | 418 | // Next Level 419 | setTimeout(function () { 420 | expected['bar:' + bar.id] = 1; 421 | 422 | // Уничтожаем foo, bar должен стать мастером 423 | foo.destroy(); 424 | 425 | // Hard level 426 | setTimeout(function () { 427 | newTabHole().then(function (baz) { 428 | expected['baz:' + baz.id] = true; 429 | expected['baz.async.result'] = ['y', 'x']; 430 | 431 | baz.on('master', function () { 432 | _set('baz:' + baz.id, true); 433 | }); 434 | 435 | baz.async = function (data, next) { 436 | assert.ok(true, 'baz.async'); 437 | 438 | setTimeout(function () { 439 | assert.ok(true, 'baz.async.next'); 440 | next(null, data.reverse()); 441 | }, 50); 442 | }; 443 | 444 | // Уничтожаем bar, теперь baz один и мастер 445 | bar.destroy(); 446 | 447 | baz.call('async', ['x', 'y'], function (err, result) { 448 | _set('baz.async.result', result); 449 | }); 450 | 451 | baz.baz = function (data) { 452 | if (data) { 453 | _set('qux.call.baz.data', data); 454 | } else { 455 | _set('qux.call.baz', 1); 456 | } 457 | return 321; 458 | }; 459 | 460 | // Bonus level 461 | setTimeout(function () { 462 | expected['qux.call.baz'] = 1; 463 | expected['qux.call.baz.data'] = 8; 464 | expected['qux.call.baz.fn'] = 321; 465 | 466 | var qux = new wormhole.Hole(url, useStore); 467 | 468 | qux.call('baz'); 469 | qux.call('baz', 8, function (err, x) { 470 | _set('qux.call.baz.fn', x); 471 | }); 472 | qux.call('baz-x', function () {}); 473 | 474 | qux.qux = function () {}; 475 | 476 | baz.call('qux'); 477 | }, 500); 478 | }); 479 | }, 500); 480 | }, 500); 481 | }); 482 | }); 483 | }); 484 | 485 | 486 | // А теперь нужно прогнать тесты с использование `store`. 487 | !useStore && _tryAgainTest(true); 488 | })(!wormhole.Worker.support); 489 | -------------------------------------------------------------------------------- /src/hole.js: -------------------------------------------------------------------------------- 1 | define(["./now", "./uuid", "./debounce", "./emitter", "./store", "./worker", "./get-own"], function (now, uuid, debounce, Emitter, store, Worker, getOwn) { 2 | var PEER_UPD_DELAY = 5 * 1000, // ms, как часто обновлять данные j gbht 3 | MASTER_VOTE_DELAY = 500, // ms, сколько времени считать мастер живым 4 | MASTER_DELAY = PEER_UPD_DELAY * 2, // ms, сколько времени считать мастер живым 5 | PEERS_DELAY = PEER_UPD_DELAY * 4, // ms, сколько времени считать peer живым 6 | QUEUE_WAIT = PEER_UPD_DELAY * 2, // ms, за какой период времени держать очередь событий 7 | 8 | _emitterEmit = Emitter.fn.emit 9 | ; 10 | 11 | 12 | /** 13 | * Проверка наличия элемента в массиве 14 | * @param {Array} array 15 | * @param {*} value 16 | * @returns {number} 17 | * @private 18 | */ 19 | function _inArray(array, value) { 20 | var i = array.length; 21 | 22 | while (i--) { 23 | if (array[i] === value) { 24 | return i; 25 | } 26 | } 27 | 28 | return -1; 29 | } 30 | 31 | 32 | /** 33 | * Выполнить команду 34 | * @param {Hole} hole 35 | * @param {Object} cmd 36 | * @private 37 | */ 38 | function _execCmd(hole, cmd) { 39 | var fn = getOwn(hole, cmd.name); 40 | var next = function (err, result) { 41 | cmd.error = err; 42 | cmd.result = result; 43 | cmd.response = true; 44 | 45 | // console.log('emit.res.cmd', cmd.name); 46 | hole.emit('CMD', cmd); 47 | }; 48 | 49 | 50 | try { 51 | if (typeof fn === 'function') { 52 | if (fn.length === 2) { 53 | // Предпологается асинхронная работа 54 | fn(cmd.data, next); 55 | } else { 56 | next(null, fn(cmd.data)); 57 | } 58 | } else { 59 | throw 'method not found'; 60 | } 61 | } catch (err) { 62 | next('wormhole.' + cmd.name + ': ' + err.toString()); 63 | } 64 | } 65 | 66 | 67 | 68 | /** 69 | * @class Hole 70 | * @extends Emitter 71 | * @desc «Дырка» — общение между табами 72 | * @param {url} url 73 | * @param {Boolean} [useStore] использовать store 74 | */ 75 | function Hole(url, useStore) { 76 | var _this = this; 77 | 78 | _this._destroyUnload = /* istanbul ignore next */ function () { 79 | _this.destroy(); 80 | }; 81 | 82 | 83 | /** 84 | * Идентификатор 85 | * @type {String} 86 | */ 87 | _this.id = uuid(); 88 | 89 | 90 | /** 91 | * Объект хранилища 92 | * @type {store} 93 | */ 94 | _this.store; 95 | 96 | 97 | /** 98 | * Название группы 99 | * @type {String} 100 | */ 101 | _this.url = (url || document.domain); 102 | 103 | 104 | /** 105 | * @type {String} 106 | * @private 107 | */ 108 | _this._storePrefix = uuid.hash(_this.url); 109 | 110 | 111 | /** 112 | * Внутренний индекс для события 113 | * @type {Number} 114 | * @private 115 | */ 116 | _this._idx; 117 | 118 | 119 | /** 120 | * Очередь событий 121 | * @type {Object[]} 122 | * @private 123 | */ 124 | _this._queue = []; 125 | 126 | /** 127 | * Список уже попробованных sharedUrl 128 | * @type {Object} 129 | * @private 130 | */ 131 | _this._excludedSharedUrls = {}; 132 | 133 | 134 | /** 135 | * Очередь команд 136 | * @type {Array} 137 | * @private 138 | */ 139 | _this._cmdQueue = []; 140 | 141 | 142 | /** 143 | * Объект функций обратного вызова 144 | * @type {Object} 145 | * @private 146 | */ 147 | _this._callbacks = {}; 148 | 149 | 150 | _this._processingCmdQueue = debounce(_this._processingCmdQueue, 30); 151 | 152 | 153 | // Подписываемя на получение команд 154 | _this.on('CMD', function (cmd) { 155 | var id = cmd.id, 156 | cmdQueue = _this._cmdQueue, 157 | callback = _this._callbacks[id], 158 | idx = cmdQueue.length; 159 | 160 | if (cmd.response) { 161 | if (!_this.master) { 162 | // Мастер обработал команду, удаляем её из очереди 163 | while (idx--) { 164 | if (cmdQueue[idx].id === id) { 165 | cmdQueue.splice(idx, 1); 166 | break; 167 | } 168 | } 169 | } 170 | 171 | if (callback) { 172 | // О, это результат для наc 173 | delete _this._callbacks[id]; 174 | callback(cmd.error, cmd.result); 175 | } 176 | } 177 | else { 178 | // Добавляем в очередь 179 | cmdQueue.push(cmd); 180 | _this._processingCmdQueue(); 181 | } 182 | }); 183 | 184 | 185 | // Опачки! 186 | _this.on('master', function () { 187 | _this._processingCmdQueue(); 188 | }); 189 | 190 | 191 | // Получи сторадж 192 | _this._initStorage(function (store) { 193 | _this.store = store; 194 | 195 | try { 196 | /* istanbul ignore next */ 197 | if (!useStore && Worker.support) { 198 | _this._initSharedWorkerTransport(); 199 | } else { 200 | throw "NOT_SUPPORTED"; 201 | } 202 | } catch (err) { 203 | _this._initStorageTransport(); 204 | } 205 | }); 206 | 207 | 208 | /* istanbul ignore next */ 209 | if (window.addEventListener) { 210 | window.addEventListener('unload', _this._destroyUnload); 211 | } else { 212 | window.attachEvent('onunload', _this._destroyUnload); 213 | } 214 | } 215 | 216 | 217 | 218 | Hole.fn = Hole.prototype = /** @lends Hole.prototype */{ 219 | _attempt: 0, 220 | 221 | /** 222 | * Готовность «дырки» 223 | * @type {Boolean} 224 | */ 225 | ready: false, 226 | 227 | /** 228 | * Мастер-флаг 229 | * @type {Boolean} 230 | */ 231 | master: false, 232 | 233 | /** 234 | * Уничтожен? 235 | * @type {Boolean} 236 | */ 237 | destroyed: false, 238 | 239 | /** 240 | * Кол-во «дырок» 241 | * @type {Number} 242 | */ 243 | length: 0, 244 | 245 | 246 | on: Emitter.fn.on, 247 | off: Emitter.fn.off, 248 | 249 | 250 | /** 251 | * Вызвать удаленную команду на мастере 252 | * @param {String} cmd 253 | * @param {*} [data] 254 | * @param {Function} [callback] 255 | */ 256 | call: function (cmd, data, callback) { 257 | if (typeof data === 'function') { 258 | callback = data; 259 | data = void 0; 260 | } 261 | 262 | // Генерируем id команды 263 | var id = uuid(); 264 | 265 | this._callbacks[id] = callback; 266 | 267 | this.emit('CMD', { 268 | id: id, 269 | name: cmd, 270 | data: data, 271 | source: this.id 272 | }); 273 | }, 274 | 275 | 276 | /** 277 | * Испустить событие 278 | * @param {String} type 279 | * @param {*} [args] 280 | * @returns {Hole} 281 | */ 282 | emit: function (type, args) { 283 | this._queue.push({ ts: now(), type: type, args: args }); 284 | return this; 285 | }, 286 | 287 | 288 | /** 289 | * Инициализиция хранилища 290 | * @private 291 | */ 292 | _initStorage: function (callback) { 293 | var match = this.url.toLowerCase().match(/^(https?:)?\/\/([^/]+)/);; 294 | 295 | if (match && match[2] !== document.domain) { 296 | store.remote(this.url, callback); 297 | } else { 298 | callback(store); 299 | } 300 | }, 301 | 302 | 303 | /** 304 | * Инициализация траспорта на основе SharedWorker 305 | * @param {Boolean} [retry] повтор 306 | * @private 307 | */ 308 | _initSharedWorkerTransport: /* istanbul ignore next */ function (retry) { 309 | var _this = this, 310 | port, 311 | worker, 312 | url = _this.url, 313 | label = location.pathname + location.search, 314 | sharedUrls = _this._getSharedUrls(), 315 | surl = sharedUrls[0], 316 | sid 317 | ; 318 | 319 | _this._store('shared.url.' + _this.id, null); 320 | _this._attempt++; 321 | 322 | if (_this._attempt > 10) { 323 | return; 324 | } 325 | 326 | try { 327 | if (!surl) { 328 | sid = url + ':' + _this.id; 329 | surl = Worker.getSharedURL(sid); 330 | } 331 | 332 | _this._excludedSharedUrls[surl] = true; 333 | _this.worker = (worker = Worker.create(surl)); 334 | _this.port = (port = worker.port); 335 | 336 | _this._store('shared.url.' + _this.id, { 337 | url: url, 338 | surl: surl, 339 | }); 340 | } 341 | catch (err) { 342 | console.warn('[wormhole] Worker error:', err); 343 | _this._initSharedWorkerTransport(true); 344 | } 345 | 346 | _this.__onPortMessage = function (evt) { 347 | _this._onPortMessage(evt); 348 | }; 349 | 350 | _this.__onWorkerError = function (evt) { 351 | console.warn('[wormhole] Worker error:', evt); 352 | worker.removeEventListener('error', _this.__onWorkerError, false); 353 | worker = null; 354 | _this._initSharedWorkerTransport(true); 355 | }; 356 | 357 | worker.addEventListener('error', _this.__onWorkerError, false); 358 | port.addEventListener('message', _this.__onPortMessage); 359 | port.start(); 360 | }, 361 | 362 | _getSharedUrls: function () { 363 | var _this = this; 364 | var surls = []; 365 | var prefix = this._storeKey('shared.url'); 366 | 367 | this.store.each(function (data, key) { 368 | if ( 369 | key.indexOf(prefix) !== -1 && 370 | data.url === _this.url && 371 | !_this._excludedSharedUrls[data.surl] 372 | ) { 373 | surls.push(data.surl); 374 | } 375 | }); 376 | 377 | return surls; 378 | }, 379 | 380 | /** 381 | * Сообщение от рабочего 382 | * @param {Event} evt 383 | * @private 384 | */ 385 | _onPortMessage: /* istanbul ignore next */ function (evt) { 386 | evt = evt.data; 387 | 388 | if (evt === 'CONNECTED') { 389 | this.emit = this._workerEmit; 390 | this.ready = true; 391 | this.port.postMessage({ hole: { id: this.id } }); 392 | 393 | this._processingQueue(); 394 | 395 | // Получили подтвреждение, что мы подсоединились 396 | _emitterEmit.call(this, 'ready', this); 397 | } 398 | else if (evt === 'PING') { 399 | // Ping? Pong! 400 | this.port.postMessage('PONG'); 401 | } 402 | else if (evt === 'MASTER') { 403 | // Сказали, что мы теперь мастер 404 | this.master = true; // ОК 405 | _emitterEmit.call(this, 'master', this); 406 | } 407 | else if (evt.type === 'peers') { 408 | // Обновляем кол-во пиров 409 | this._checkPeers(evt.data); 410 | } 411 | else { 412 | // console.log(this.id, evt.type); 413 | // Просто событие 414 | _emitterEmit.call(this, evt.type, evt.data); 415 | } 416 | }, 417 | 418 | 419 | /** 420 | * Инициализация транспорта на основе store 421 | * @private 422 | */ 423 | _initStorageTransport: function () { 424 | var _this = this, 425 | _first = true, 426 | id = _this.id; 427 | 428 | _this._idx = (_this._store('queue') || {}).idx || 0; 429 | 430 | // Запускаем проверку обновления данных peer'а 431 | _this._updPeer = function () { 432 | _this._store('peer.' + id, { 433 | id: id, 434 | ts: now(), 435 | master: _this.master, 436 | }); 437 | 438 | clearTimeout(_this._pid); 439 | _this._pid = setTimeout(_this._updPeer, PEER_UPD_DELAY); 440 | }; 441 | 442 | // Реакция на обновление storage 443 | _this.__onStorage = function (key, data) { 444 | if (key.indexOf('peer.') > -1) { 445 | //console.log('onPeer:', key, data[key]); 446 | _this._checkPeers(); 447 | 448 | // Размазываем проверку по времени 449 | clearTimeout(_this._pidMaster); 450 | _this._pidMaster = setTimeout(_this._checkMasterDelayed, MASTER_VOTE_DELAY); 451 | } 452 | else if (key === _this._storeKey('queue')) { 453 | _this._processingQueue(data[key].items); 454 | } 455 | }; 456 | 457 | _this._checkMasterDelayed = function () { 458 | _this._checkMaster(); 459 | }; 460 | 461 | _this.store.on('change', _this.__onStorage); 462 | 463 | // Разрыв для нормальной работы синхронной подписки на события (из вне) 464 | _this._pid = setTimeout(function () { 465 | _this.emit = _this._storeEmit; 466 | _this.ready = true; 467 | 468 | _emitterEmit.call(_this, 'ready', _this); 469 | 470 | _this._updPeer(); 471 | _this._processingQueue(); 472 | }, 0); 473 | }, 474 | 475 | 476 | 477 | /** 478 | * Проверка и выбор мастера 479 | * @private 480 | */ 481 | _checkMaster: function () { 482 | var peers = this.getPeers(true); 483 | 484 | if (peers.length > 0) { 485 | var mpeer = peers[0]; 486 | 487 | if (!mpeer.master || (now() - mpeer.ts) > MASTER_DELAY) { 488 | peers.forEach(function (p) { 489 | if (mpeer.ts < p.ts) { 490 | mpeer = p; 491 | } 492 | }); 493 | 494 | if (mpeer.id === this.id) { 495 | this.master = true; 496 | this._updPeer(); 497 | _emitterEmit.call(this, 'master', this); 498 | } 499 | } 500 | } 501 | }, 502 | 503 | 504 | /** 505 | * Получить все активные «дыкрки» 506 | * @param {boolean} [raw] 507 | * @return {Array} 508 | */ 509 | getPeers: function (raw) { 510 | var ts = now(), 511 | _this = this, 512 | peers = [], 513 | storeKey = _this._storeKey('peer.'); 514 | 515 | _this.store.each(function (data, key) { 516 | if (key.indexOf(storeKey) > -1) { 517 | if ((ts - data.ts) < PEERS_DELAY) { 518 | if (raw) { 519 | peers[data.master ? 'unshift' : 'push'](data); 520 | } else { 521 | peers.push(data.id); 522 | } 523 | } 524 | else if (_this.master) { 525 | _this.store.remove(key); 526 | } 527 | } 528 | }); 529 | 530 | return peers; 531 | }, 532 | 533 | 534 | /** 535 | * Обновляем кол-во и список «дырок» 536 | * @param {string[]} [peers] 537 | * @private 538 | */ 539 | _checkPeers: function (peers) { 540 | var i, 541 | id, 542 | ts = now(), 543 | _this = this, 544 | _peers = _this._peers || [], 545 | changed = false; 546 | 547 | if (!peers) { 548 | peers = this.getPeers(); 549 | } 550 | 551 | i = Math.max(peers.length, _peers.length); 552 | while (i--) { 553 | id = peers[i]; 554 | 555 | if (id && _inArray(_peers, id) === -1) { 556 | changed = true; 557 | _emitterEmit.call(this, 'peers:add', id); 558 | } 559 | 560 | if (_peers[i] != id) { 561 | id = _peers[i]; 562 | 563 | if (id && _inArray(peers, id) === -1) { 564 | changed = true; 565 | _emitterEmit.call(this, 'peers:remove', id); 566 | } 567 | } 568 | } 569 | 570 | if (changed) { 571 | this._peers = peers; 572 | this.length = peers.length; 573 | _emitterEmit.call(this, 'peers', [peers]); 574 | } 575 | }, 576 | 577 | 578 | /** 579 | * Получить ключь для store 580 | * @param {String} key 581 | * @returns {String} 582 | * @private 583 | */ 584 | _storeKey: function (key) { 585 | return this._storePrefix + '.' + key; 586 | }, 587 | 588 | 589 | /** 590 | * Записать или получить информацию из хранилища 591 | * @param {String} key 592 | * @param {*} [value] 593 | * @returns {Object} 594 | * @private 595 | */ 596 | _store: function (key, value) { 597 | key = this._storeKey(key); 598 | 599 | if (value === null) { 600 | this.store.remove(key); 601 | } 602 | else if (value === void 0) { 603 | value = this.store.get(key); 604 | } 605 | else { 606 | this.store.set(key, value); 607 | } 608 | 609 | return value; 610 | }, 611 | 612 | 613 | /** 614 | * Emit через SharedWorker 615 | * @param type 616 | * @param args 617 | * @private 618 | */ 619 | _workerEmit: /* istanbul ignore next */ function (type, args) { 620 | var ts = now(); 621 | 622 | this.port.postMessage({ 623 | ts: ts, 624 | type: type, 625 | data: args 626 | }); 627 | 628 | return this; 629 | }, 630 | 631 | 632 | /** 633 | * Emit через хранилище 634 | * @param type 635 | * @param args 636 | * @private 637 | */ 638 | _storeEmit: function (type, args) { 639 | var queue = this._store('queue') || { items: [], idx: 0 }, 640 | ts = now(), 641 | items = queue.items, 642 | i = items.length 643 | ; 644 | 645 | items.push({ 646 | ts: ts, 647 | idx: ++queue.idx, 648 | type: type, 649 | args: args, 650 | source: this.id 651 | }); 652 | 653 | while (i--) { 654 | if (ts - items[i].ts > QUEUE_WAIT) { 655 | items.splice(0, i); 656 | break; 657 | } 658 | } 659 | 660 | this._store('queue', queue); 661 | this._processingQueue(queue.items); 662 | 663 | return this; 664 | }, 665 | 666 | 667 | /** 668 | * Обработка очереди событий 669 | * @param {Object[]} [queue] 670 | * @private 671 | */ 672 | _processingQueue: function (queue) { 673 | var evt; 674 | 675 | if (queue === void 0) { 676 | queue = this._queue; 677 | 678 | while (queue.length) { 679 | evt = queue.shift(); 680 | this.emit(evt.type, evt.args); 681 | } 682 | } 683 | else { 684 | for (var i = 0, n = queue.length; i < n; i++) { 685 | evt = queue[i]; 686 | 687 | if (this._idx < evt.idx) { 688 | this._idx = evt.idx; 689 | 690 | // if (evt.source !== this.id) { 691 | _emitterEmit.call(this, evt.type, evt.args); 692 | // } 693 | } 694 | } 695 | } 696 | }, 697 | 698 | 699 | /** 700 | * Обработка очереди команд 701 | * @private 702 | */ 703 | _processingCmdQueue: function () { 704 | var cmdQueue = this._cmdQueue; 705 | 706 | /* istanbul ignore else */ 707 | if (this.master) { 708 | while (cmdQueue.length) { 709 | _execCmd(this, cmdQueue.shift()); 710 | } 711 | } 712 | }, 713 | 714 | 715 | /** 716 | * Уничтожить 717 | */ 718 | destroy: function () { 719 | if (!this.destroyed) { 720 | if (window.addEventListener) { 721 | window.removeEventListener('unload', this._destroyUnload); 722 | } else { 723 | window.detachEvent('onunload', this._destroyUnload); 724 | } 725 | 726 | this.ready = false; 727 | this.destroyed = true; 728 | this._destroyUnload = null; 729 | 730 | clearTimeout(this._pid); 731 | this._store('shared.url.' + this.id, null); 732 | 733 | // Описываем все события 734 | this.off(); 735 | store.off('change', this.__onStorage); 736 | 737 | /* istanbul ignore next */ 738 | if (this.port) { 739 | this.port.removeEventListener('message', this.__onPortMessage); 740 | this.port.postMessage('DESTROY'); 741 | this.port = null; 742 | this.worker = null; 743 | } 744 | else { 745 | this._store('peer.' + this.id, null); 746 | } 747 | 748 | this.master = false; 749 | } 750 | } 751 | }; 752 | 753 | 754 | // Export 755 | return Hole; 756 | }); 757 | -------------------------------------------------------------------------------- /st/xtpl.min.js: -------------------------------------------------------------------------------- 1 | /*! xtpl 0.1.0 - MIT | git://github.com/rubaxa/xtpl.git */ 2 | (function(e){"use strict";"function"==typeof define&&define.amd?define("xtpl",[],e):"undefined"!=typeof module&&module.exports!==void 0?module.exports=e():window.xtpl=e()})(function(){"use strict";function e(n,r){var i={exports:{}};r(i,i.exports,t),e[n]=i.exports}function t(t){return e[t]}return e("utils",function(e){var t,n=/[&<>"]/,r=/&/g,i=/>/g,a=/=0&&0 in e)for(;i>r;r++)t.call(n,e[r],r,e);else for(r in e)e.hasOwnProperty(r)&&t.call(n,e[r],r,e)}},map:Array.map||function(e,t,n){for(var r=[],i=0,a=e.length;a>i;i++)r.push(t.call(n,e[i],i,e));return r},filter:Array.filter||function(e,t,n){for(var r=[],i=0,a=e.length;a>i;i++)t.call(n,e[i],i,e)&&r.push(e[i]);return r},inArray:Array.indexOf||function(e,t){for(var n=e.length;n--;)if(e[n]===t)return n;return-1},isEmptyObject:function(e){for(var t in e)return!1;return!0},escapeHTML:function(e){return"string"==typeof e?n.test(e)&&(~e.indexOf("&")&&(e=e.replace(r,"&")),~e.indexOf("<")&&(e=e.replace(a,"<")),~e.indexOf(">")&&(e=e.replace(i,">")),~e.indexOf('"')&&(e=e.replace(o,"""))):void 0===e&&(e=""),e},notEqual:function l(e,t){var n,r=typeof t;if(typeof e!==r&&"object"===r)return!0;if(t instanceof Array){if(n=t.length,e.length!==n)return!0;for(;n--;)if(l(e[n],t[n]))return!0}else{if("object"!==r||null===e||null===t)return e!=t;for(n in t)if(t.hasOwnProperty(n)&&l(e[n],t[n]))return!0;for(n in e)if(e.hasOwnProperty(n)&&l(e[n],t[n]))return!0}return!1},simpleNotEqual:function l(e,t){var n,r=typeof t;if(typeof e!==r&&"object"===r)return!0;if(t instanceof Array){if(n=t.length,e.length!==n)return!0;for(;n--;)if(e[n]!==t[n])return!0}else{if("object"!==r)return e!=t;for(n in t)if(t.hasOwnProperty(n)&&l(e[n],t[n]))return!0;for(n in e)if(e.hasOwnProperty(n)&&l(e[n],t[n]))return!0}return!1},simpleClone:function(e){var t,n=e;if(e instanceof Array)for(t=e.length,n=[];t--;)n[t]=e[t];else if(e instanceof Function)n=e;else if(e instanceof Object){n={};for(t in e)e.hasOwnProperty(t)&&(n[t]=e[t])}return n},print:function(){console.log(t.map(arguments,function(e){return e?(""+JSON.stringify(e)).replace(/^"|"$/g,""):e}))},error:function(e,t,n){console.log(e.message+"\nline: "+(n||e.line||e.lineNumber)+"\nfile: "+(t||e.file||e.filename))},matchAll:function(e,t){for(var n,r=[];n=t.exec(e);)void 0!==n[1]&&r.push(n.slice(1));return r},throttle:function(e,t,n){var r,i,a,o,s=function(){e.apply(n,o)};return function(){a=(new Date).getTime(),o=arguments,void 0===r?(i=a,r=setTimeout(s,t)):a-i>=t&&(clearTimeout(r),r=void 0,s())}},readFile:function(e){var t=new XMLHttpRequest;return t.open("GET",e,!1),t.send(null),t.responseText}},t.isClient&&(t.support.touch=function(){var e=document.createElement("div");return"ontouchstart"in e}(),window.jQuery)){var c=t.support.touch?"toucstart":"click";jQuery.event.special.tap={delegateType:c,bindType:c}}}),e("xmlparser",function(e,t,n){function r(e,t){return new i(e,t)}function i(e,t){this.name=e||"#",this.shorty=void 0!==$[e],this.value="",this.attrs=t||{},this.length=0,this.children=[],this.binding=[]}var a=function(e){return e.charCodeAt(0)},o=/^[\r\n\s\t]+$/,s=a("\n"),l=a("<"),c=a(">"),u=a("="),d=a("a"),f=a("z"),p=a("0"),h=a("9"),x=a("-"),v=a(":"),m=a('"'),_=a("/"),g=(a("\\"),33),y=1,b=2,w=n("utils"),C=w.each,$=w.shortTags,E=function(e,t,n){function i(e){var t,r=n[e.name];void 0!==r&&(t="string"==typeof r?r:r(e,e.attrs),"string"==typeof t&&e.replace(t))}function a(e){var r,i=n["#text"],a=e.value;if(void 0!==i&&(r=i(a,e),r!==a)){var o=t.line;r=E(r,t,n),t.line=o,e.replace(r.children)}}function w(t){var n=e.indexOf("\n",S),r=S-P+1;throw 0>=n&&(n=e.length),{file:F,line:z+":"+r,message:t+"\n"+e.substring(P,n).replace(/\t/g," ")+"\n"+Array(r).join("-")+"^"}}function C(){var t=e.charCodeAt(++S);for(t!==m&&w('"'+j+'" expected value of the attribute in quotes'),L=++S;M>S;){if($=e.charCodeAt(S),t===$)return e.substring(L,S);S++}S-=0|(S-L)/2+1,w('Unclosed string literal in "'+j+'" attribute')}n=n||{},t=t||{};for(var $,k,A,T,O,j,N,S=0,L=0,M=e.length,D=r("#root"),z=t.line||1,F=t.file,P=0,q=0,B=null==t.trim?1:t.trim;M>S;)$=e.charCodeAt(S),g>$&&$===s&&(P=S+1,z++),T===y?$===c||$===_&&e.charCodeAt(++S)===c?(O=e.substring(k,q+1),T=void 0,q=S+1,"#"===D.name?(D.name=O,$===_&&(D.shorty=!0,i(D),D=D.parent)):D.name===O?(i(D),D=D.parent):w('Wrong close "'+O+'" tag should be "'+D.name+'"')):g>$?(L=S,T=b,A=void 0):$>=d&&f>=$||$>=p&&h>=$||$===x||$===v?q=S:w("Invalid character in node name"):T===b?$===_||$===c?(void 0!==A&&D.attr(e.substring(A,S).trim(),!0),S--,T=y):$===u||g>$?void 0!==A&&(j=e.substring(A,S).trim(),D.attr(j,g>$?!0:C()),A=void 0):(d>$||$>f)&&$!==x&&$!==v?w("Invalid character in attrbiute name"):void 0===A&&(A=S):$===l&&(S-q>0&&(N=e.substring(q,S),(0===B||o.test(N)===!1)&&(D.appendText(N),a(D.last))),e.charCodeAt(S+1)===_?S++:(D=D.newChild(),D.file=F,D.line=z),k=S+1,T=y),S++;return T===y?w('"'+e.substring(k,S)+'" tag should be closed'):"#root"!==D.name&&w('"'+D.name+'" tag should be closed'),S-q>0&&(N=e.substring(q,S),(0===B||o.test(N)===!1)&&(D.appendText(N),a(D.last))),D};i.prototype={create:function(e,t){return r(e,t)},attr:function(e,t){return this.attrs[e]=t,this},bind:function(e){e.binded=this.binded=!0,this.binding.push(e)},on:function(e,t,n){void 0===this.events&&(this.events=[]),this.events.push(e.replace(/^.+[:-]+/,""),t,n)},bindDecl:function(e,t){void 0===this.decl&&(this.decl=[]),this.decl.push(e,t)},closest:function(e){var t=this.parent;if(t.name!==e)for(;(t=t.parent)&&t.name!==e;);return t},index:function(){for(var e=this.parent.children,t=e.length;t--;)if(e[t]===this)return t;return-1},append:function(e,t){var n=this.children;return void 0===t?n.push(e):n.splice(t.index(),0,e),this.first=n[0],this.last=n[(this.length=n.length)-1],e.parent=this,this},appendArray:function(e){return C(e,function(e){this.append(e)},this),this},empty:function(){return this.children=[],this.length,delete this.first,delete this.last,this},remove:function(){var e=this.parent,t=e.children,n=this.index();return-1!==n&&t.splice(n,1),e.first=t[0],e.last=t[(e.length=t.length)-1],this},find:function(e){var t=[],n=">"!=e.charAt(0);return e=e.replace(">",""),C(this.children,function r(i){(i.name===e||"*"===e)&&t.push(i),n&&C(i.children,r)}),t},wrap:function(e,t){var n=r(e,t);return n.file=this.file,n.line=this.line,this.parent.append(n,this),n.append(this.remove()),n},innerWrap:function(e,t){var n=r(e,t),i=this.children,a=0,o=i.length;for(this.children=[];o>a;a++)n.append(i[a]);return this.append(n)},replace:function(e){var t=this.parent,n=[].concat(e),r=0,i=n.length;for(this.remove();i>r;r++)e=n[r],"string"==typeof e?t.appendText(e):t.append(e);return this},appendText:function(e){if(""!==e){var t=this.last;void 0===t||"#text"!==t.name?this.newChild("#text").value=e:t.value+=e}return this},newChild:function(e,t){var n=r(e,t);return n.file=this.file,n.line=this.line,this.append(n),n},exception:function(e){throw{message:'Tag "'+this.name+'", '+e,line:this.line,file:this.file}},each:function(e){e(this,this.attrs),C(this.children,function(t){t.each(e)})},clone:function(){var e=r();return C(this,function(t,n){"children"===n?C(t,function(t){e.append(t.clone())}):/first|last/.test(n)||(e[n]=w.simpleClone(t))}),e},toString:function(e,t){t=e&&t||"",e=e?"\n":"";var n=t+this.value;if(C(this.children,function(r){n+=r.toString(e," "+t)}),"#"!=this.name.charAt(0)){var r,i,a=e+t+"<"+this.name,o=[];for(r in this.attrs)i=this.attrs[r],i===!0?o.push(r):o.push(r+'="'+i+'"');o.length&&(a+=" "+o.join(" ")),n=a+(this.shorty&&!this.length?"/>":">"+n+e+t+"")}return n}},E.newNode=r,e.exports=E}),e("xparser",function(e,t,n){(function(){var t=n("xmlparser"),r=t.newNode,i=function(e){return e.charCodeAt(0)},a=function(e){return String.fromCharCode(e)},o=0,s=i("\n"),l=i("{"),c=i("}"),u=i("("),d=(i(")"),i("[")),f=i("]"),p=i("="),h=i("`"),x=i("a"),v=(i("z"),i("0")),m=i("9"),_=i("\n"),g=i(">"),y=i("&"),b=i("."),w=i("#"),C=i("|"),$=i(","),E=i(":"),k=i("-"),A=i('"'),T=i("'"),O=i("/"),j=i("\\"),N=32,S=++o,L=++o,M=++o,D=++o,z=++o,F=(++o,++o),P=++o,q=++o,B=++o,H=(++o,n("utils").each),I=/(width|height|left|right|top|bottom|font-size|text-indent)$/,R="a abbr acronym address applet area article aside audio b base basefont bdi bdo big blockquote body br canvas caption center cite code col colgroup command datalist dd del details dfn dialog dir div dl dt em embed fieldset figcaption figure font footer form frame frameset h1 to h6 head header hgroup hr html i iframe img input ins kbd keygen label legend li link map mark menu meta meter nav noframes noscript object ol optgroup option output p param pre progress q rp rt ruby s samp script section select small source span strike strong style sub summary sup table tbody td textarea tfoot th thead time title tr track tt u ul var video wbr".split(" "),W={"if":function(e){return r("x:if",{test:e})},"else":function(e,t){var n=r("x:else");return n.shorty=!0,/^if\b/.test(e)&&(n.attrs["if"]=e.substr(2).trim()),t.last.append(n)},elseif:function(e,t){return W["else"]("if "+e,t)},"for":function(e,t){return W.each(e,t)},each:function(e){return e=e.match(/^\(?(\w+)(?:\s|,)\s*(\w+)?\)?\s*in\s*([^\|]+)(\|.+)?/i),r("x:each",{data:e[3],as:e[2]||e[1],key:e[2]?e[1]:"$index",filter:/^\|/.test(e[e.length-1])?e.pop().substr(1):void 0})}};H(R,function(e){R[e]=1});var J=function(e,n,i){function o(t,n){var r=e.indexOf("\n",st),i=st-pt+1;0>=r&&(r=e.length);try{r()}catch(a){console.log(a.stack.split("\n").slice(1).join("\n"))}throw{file:ft,line:dt+":"+i,message:t+"\n"+e.substring(pt,r).replace(/\t/g," ")+"\n"+Array(i+(0|n)).join("-")+"^"}}function R(e,t){var n;try{e="string"==typeof e?e.replace(/(\w)->(\w)/g,"$1.xxx.$2"):e,n=Function("ctx,Soul","return "+e)()}catch(r){if(e=e.replace(/(\w)\.xxx\.(\w)/g,"$1->$2"),r instanceof ReferenceError||r instanceof TypeError)return t?{expr:e,str:e}:"{{"+e+"}}";o(r.message+"\n---\n"+e+"\n---")}return t?{value:e,str:e,result:n}:n}function J(e){var r,a=ut.newChild("#text"),o=i["#text"],s=a.value=e;if(void 0!==o&&(r=o(s,a),r!==s)){var l=n.line;r=t(r,n,i),n.line=l,a.replace(r.children)}return a}function Q(){var t,n;for(lt=st;ct>st;){if(Z=e.charCodeAt(st),Z===A||Z===T||Z===j&&!/["')]/.test(a(et))){for(st++,et=Z;ct>st;){if(Z=e.charCodeAt(st),Z===j)t=!t;else{if(Z===et&&!t)break;t=0}st++}st>=ct&&o("Unterminated string")}else{if(Z===$||Z===c||Z===_)return n=e.substring(lt,st).trim().replace(/[;,]$/,""),lt=st+1,st--,n;if(Z===l)break}Z>N&&(et=Z),st++}st=lt}function U(t){for(lt=st+(0|t);ct>st&&(Z=e.charCodeAt(st),Z!==s);)st++;return e.substring(lt,st)}function X(e){var t,n=i[e.name];void 0!==n&&(t="string"==typeof n?n:n(e,e.attrs),"string"==typeof t&&e.replace(t))}function G(e){var t={};return H(e.attrs,function(n,r){var i=n,a=e.name;"string"==typeof n?/^x[:-](if|each)/.test(a)||/^(x|ng)-/.test(r)||(i=R(i)):n instanceof Object&&(i="",H(n,function(e,n){e=R(e,!0),"class"===r?void 0!==e.expr?(n=n.replace(/^([^'"])/,'"$1').replace(/([^'"])$/,'$1"'),i+=" {{"+e.expr+" ? "+n+' : ""}}'):e.result&&(i+=" "+n.trim().replace(/(^["']|["']$)/g,"")):"style"===r?i+=n+":"+(void 0!==e.expr?"{{"+e.expr+"}}":e.result)+(I.test(n)&&(e.expr?!/(\+\s*["'](%|[a-z]{2,3})['"]|%)$/.test(e.str):/^\d+(\.\d+)?$/.test(e.str))?"px":"")+";":(i=void 0,t[r+"-"+n]=void 0!==e.expr?"{{"+e.expr+"}}":e.result)})),null!=i&&(t[r]=i)}),e.attrs=t,e.parent||o("Too many close"),X(e),"x:decl"==e.parent.name&&(e=e.parent,X(e)),e.parent.single?G(e.parent):e.parent}function V(t){var n=e.substring(lt+(0|t),st);return lt=st,n}function K(t){for(;ct>st;){if(Z=e.charCodeAt(st),Z===l){var n=W[t](V().trim(),ut);return n.line=dt,n.file=ft,n.parent||ut.append(n),n}st++}o("Syntax error")}i=i||{},n=n||{};for(var Y,Z,et,tt,nt,rt,it,at,ot,st=0,lt=0,ct=e.length,ut=r("#root"),dt=n.line||1,ft=n.file,pt=0;ct>st;){if(Z=e.charCodeAt(st),Z===s&&(dt++,pt=st),void 0===tt){if(Z===h){for(Y=ut.newChild("x:script"),lt=st+1;e.charCodeAt(++st)!==h;);Y.value=V().trim()+";",X(Y)}else Z===y?(lt=st,tt=B):Z===C?(J(U(1).trim().replace(/\|$/,"")),nt&&(nt=!1,ut=G(ut)),lt=st):Z===O?U():Z===w?(lt=st,ut=ut.newChild("div"),tt=S):Z===b?(ut=ut.newChild("div"),tt=z):Z===g||Z===$?lt=st:Z===l||(Z===c?(ut=G(ut),lt=st):Z>N&&(lt=st,tt=L));ut.file=ft,ut.line=dt}else if(tt===L){if((Z===u||x>et)&&(rt=e.substring(lt,st).trim(),W[rt])){lt=st,ut=K(rt),tt=void 0;continue}if(Z===b)tt=z;else if(Z===d)tt=M;else if(Z===w)tt=S;else if(Z===C)tt=void 0,nt=!0;else{if(Z===E){tt=F;continue}Z===g?tt=D:Z===l?tt=D:Z>N&&x>Z&&Z!==k&&(v>Z||Z>m)&&o("Syntax error")}tt!==L&&(ut=ut.newChild(V().trim()),rt=ut.name,st--,/^[a-z_-][a-z0-9_-]*$/i.test(rt)||o('Invalid node name "'+rt+'"',-rt.length/2))}else tt===S?(Z===b||N>=Z||Z===d)&&(tt=Z===b?z:Z===d?M:D,ut.attrs.id='"'+V(1).trim()+'"',st--):tt===z?(N>=Z||Z===C||Z===l||Z===d)&&(ut.attrs["class"]='"'+V().trim().split(".").slice(1).join(" ")+'"',st--,it=void 0,tt=Z===d?M:D):tt===M?(Z===p||Z===f)&&(void 0===it&&(it=V(1)),Z===f&&(ot=V(1),lt=st+1,ut.attrs[it]="]"===ot?"true":ot,it=void 0,e.charCodeAt(st+1)!==d&&(tt=D))):tt===D?Z===C?(J(U(1).trim().replace(/\|$/,"")),lt=st,ut=G(ut),tt=void 0):Z===g?(lt=st+1,tt=void 0,ut.single=!0):Z===l&&(tt=void 0):tt===F||tt===q?Z===E?(ot=V().trim(),tt===F?(it=ot,Y=ut.attrs[it],ut.attrs[it]="",void 0!==Y&&"class"===it&&(ut.attrs[it]=Y+" ")):(at=ot,ut.attrs[it][at]=""),tt=P):Z===c&&(tt=void 0,void 0===it&&(ut=G(ut))):tt===P?Z===l?(Y=ut.attrs[it],ut.attrs[it]={},""!==Y&&"class"===it&&(ut.attrs[it][Y]=1),lt=st+1,tt=q):Z>N&&(ot=Q(),void 0===at?ut.attrs[it]="class"===it?(ut.attrs[it]+ot).replace(/"\s"/," "):ot:(tt=q,ut.attrs[it][at]=ot,at=void 0),Z===c?(tt===q&&st++,it=void 0,at=void 0,tt=void 0):tt!==q&&(it=void 0,at=void 0,tt=void 0)):tt===B&&(Z===p?(ut=ut.newChild("x:decl",{name:V(1).trim()}),tt=void 0):Z===l?(ut=ut.newChild("x:"+V(1).trim()),lt=++st,tt=F):Z>N&&N>=et&&(ut.newChild("x:"+V(1).trim(),{"x:context":U()}),tt=void 0));et=Z,st++}return"#root"!==ut.name&&ut.exception("not closed"),X(ut),ut};e.exports=J})()}),e("compile",function(e){function t(e,n,r){return t.pre(e,n),t.code(e,r)}t.pre=function n(e,t){var r,i=e.name,a=t[i],o=e.attrs;if(!e.noPre){if(void 0===a||(r=a(e,e.attrs)),r!==!0&&(r=t["**"](e,e.attrs)),r===!0)return n(e.parent,t),void 0;for(i in o)if(a=t["@"+i],void 0!==a&&a.call(t,e,i,o[i])===!0||t["@"].call(t,e,i,o[i])===!0)return delete o[i],n(e.parent,t),void 0}for(var s=e.children.slice(0),l=0,c=s.length;c>l;l++)n(s[l],t)},t.code=function r(e,n){var i,a,o,s,l,c=e.name,u=n[c],d=e.children,f="",p=e.binding,h=e.decl,x=e.events,v=(p.length||h||x)&&t.xpath(e),m="",_="",g="";if(i=void 0===u?n["**"](c,e,e.attrs):""===u?["",""]:u(e,e.attrs),e.xctrl&&(i[0]=t.tryCatch(e,'xtpl.ctrl["_'+e.xctrl+'"](ctx)')+i[0]),a=p.length){var y,b=0;for(o=0;a>o;o++){if(l=p[o],m+=",__xbind"+o,"x:if"==l.name)s=t.getExpr(l,"test"),b=1,l.attrs.test="__xbind"+o;else if("x:else"==l.name)s=t.getExpr(l,"if"),b=1,l.attrs.test="__xbind"+o;else if("x:value"==l.name)s=t.getExpr(l,"data"),l.attrs.data="__xbind"+o;else if("x:var"==l.name){var w=l.key;if(l.expr){var C=t.getExpr(l,w,l.value);s=t.tryCatch(l,"__xval="+C),"context"==w?(_=s+"if( __xval instanceof Object ) for( var key in __xval ){"+" __xval0 = __xval[key];"+" __xargs[key] = __xval0;"+' __xargs["__"+key] = __xclone(__xval0);'+" __xc = 1;"+"}"+_,g='for( var key in __xargs ){ if( !/^__/.test(key) ){ __xval = __xargs[key]; if( __xnotEq(__xval, __xargs["__"+key]) ){'+t.tryCatch(l,C+"[key] = __xval;")+" __xc = 1;"+" }"+" }"+"}"+g):(_+=s+' __xargs["'+w+'"] = __xval;',l.sync!==!1&&(_+=' __xargs["__'+w+'"] = __xclone(__xval);'+" __xc = 1;"),l.sync===!1||/[\*\-\+\\!%\/=><]/.test(C)||(g+='__xval=__xargs["'+w+'"];'+'if( __xnotEq(__xval, __xargs["__'+w+'"]) ){'+t.tryCatch(l,C+" = __xval;")+" __xc = 1;"+"}"))}else _+='__xargs["'+w+'"]='+t.lex(l.value)+";";continue}_+=t.tryCatch(l,"__xargs["+o+"]="+s),l.attrs.safe=!0}"x:attr"==c?(b=3,y=t.lex(e.attrs.name)):"x:decl"===c&&(b=5,y=t.lex(e.declName),m=" ctx,__xsync",_="var __xc;if(apply===void 0){"+_+"}else{"+g+"}return __xc"),i[0]+="__buf.b("+b+", ["+v+"],function (__xargs,apply){"+_+"},function "+(e.fnName||"")+"("+m.substr(1)+"){",i[1]="}"+(y?","+y:"")+");"+i[1],"x:decl"===c&&(i[0]+="var ___$apply=__buf.$apply, __$apply=__buf.$apply=function(force){if( __xsync(ctx,true) || force === true ){ ___$apply(true) }};",i[1]="__buf.$apply=___$apply;"+i[1])}if(x)for(o=0,a=x.length;a>o;o+=3)i[1]+="__buf.b(4,["+v+"],"+t.lex(x[o])+",function(evt){evt.preventDefault();"+"var el = evt.currentTarget;"+x[o+1]+"\n __$apply()"+"},"+t.lex(x[o+2]||0)+");";if(h)for(o=0,a=h.length;a>o;o+=2)s=h[o+1],s instanceof Object&&(s=JSON.stringify(s)),i[1]+="__buf.b(5,["+v+"],function(__xargs){"+t.tryCatch(e,"__xargs["+o+"]="+s)+"\nreturn __xargs},"+t.lex(h[o])+");";for(a=d.length,o=0;a>o;o++)f+=r(d[o],n);return(i[0]+f+i[1]).replace(/"\)\n__buf\.s\("/g,"").replace(/{;_/g,"{_")},t.lex=function(e){return JSON.stringify(e)},t.str=function(e){return"__buf.s("+t.lex(e)+")\n"},t.variable=function(e,n,r){return t.expr(e,"__buf.w($"+n+(r?',"'+r+'"':"")+");")},t.tryCatch=function(e,t,n){return"try{"+t+"}catch(e){"+(n||"")+(e.file?';__xerr(e,"'+e.file+'",'+e.line+")}":"}")},t.expr=function(e,n){var r="",i=0;return n=n.replace(/([\$@&])([\w-]+)/g,function(n,a,o){var s=e.attrs[o];return"$"!=a||e.attrs.safe?"@"==a&&(s=t.lex(s)):(s=t.getExpr(e,o),o="__xval"+i++,r+=t.tryCatch(e,o+"="+s,o+"=void 0"),s=o),void 0===s?"undefined":s}),r+n},t.getExpr=function(e,t,n){var r=(void 0===n?e.attrs[t]:n).replace(/(\w|\))->(\w)/g,"$1.attributes.$2");try{Function('"use strict";\n'+r)}catch(i){try{Function('"use strict";\n('+r+")")}catch(a){e.exception(' attribute "'+t+'": '+i.message+"\n---\n"+r+"\n---")}}return r},t.wrap=function(e,t){if(!e.binded){do{var n=e.parent,r=n.children,i=r.length,a=i,o=/#text|value/,s=0;if(t=t||e.attrs.bind,i>1){for(;a--;)r[a].hidden&&e!==r[a]&&i--;a=i}if("both"==t&&(1!=e.length||o.test(e.first.name)))return e.innerWrap("x"),!0;if(1===i)"both"==n.bindMod&&n.innerWrap("x"),e.sys&&n.sys&&(n=e.wrap("#scope"),n.sys=1,n.hidden=1),e.manualBind||n.manualBind||n.bind(e);else if(o.test(e.name)){for(;a--;)if(!o.test(r[a].name)&&!r[a].hidden)return e.wrap("x"),!0;e.manualBind||n.manualBind||n.bind(e)}else("wrap"==t||"both"==t)&&(e.wrap("x"),s=1)}while(s);e.bindMod=t,delete e.attrs.bind}},t.xpath=function(e,t){var n=e.xpath;if(t=e.attrs.x||t,void 0!==n)return n;if(n=[],t&&console.group(e.name+": "+JSON.stringify(e.attrs)),e.parent)do for(var r=e.parent.children,i=0,a=r.length,o=0;a>i;i++)if(t&&console.log(r[i].hidden,o,""+r[i]),r[i].pseudoRoot)o=0;else if(!r[i].hidden){if(r[i]===e){n.push(o);break}o++}while((e=e.parent)&&e.parent&&!e.sys);return t&&(console.log("xpath:",n),console.groupEnd()),n},e.exports=t}),e("buffer",function(e,t,n){function r(e){var t,n=e.min,i=n,a=e.max;if(void 0!==n)for(;a>=i;i++)t=e[i],void 0!==t&&(t.zmb=1,r(t))}function i(e,t,n,r,i,s){var l=0,u=t.childNodes,d=r.extra,f=document.createElement("div"),p=r.render,h=[];x(i,function(i,c){if(void 0===d||d(i)){var x,v=s[l],m=u[l];i!==v&&(n.remove(l),e.idx=l,e.xpath=r.xpath,e.clear(),p(i,c),x=a(f,""+e),void 0===m?t.appendChild(x):o(m,x,t)),h[l]=i,l++}});for(var v=u.length-1;v>=l;v--)c(u[v],e._decl,!0),n.remove(v),t.removeChild(u[v]);return f=null,h}function a(e,t){return/^