Comparing version
@@ -0,1 +1,7 @@ | ||
1.0.2 / 2015-06-11 | ||
------------------ | ||
- Added locks support. | ||
1.0.1 / 2015-06-08 | ||
@@ -2,0 +8,0 @@ ------------------ |
@@ -1,2 +0,2 @@ | ||
/*! tabex 1.0.1 https://github.com//nodeca/tabex @license MIT */(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.tabex = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){ | ||
/*! tabex 1.0.2 https://github.com//nodeca/tabex @license MIT */(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.tabex = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){ | ||
'use strict'; | ||
@@ -46,2 +46,34 @@ | ||
}); | ||
// Lock handlers | ||
this.__lock_handlers__ = {}; | ||
// If client make lock request - store handler and remove it from message | ||
this.filterOut(function (channel, message, callback) { | ||
if (channel === '!sys.lock.request') { | ||
var fn = message.data.fn; | ||
var lockId = message.data.id; | ||
delete message.data.fn; | ||
// Wrap handler to pass unlock function into it | ||
self.__lock_handlers__[message.id] = function () { | ||
fn(function unlock() { | ||
self.emit('!sys.lock.release', { id: lockId }); | ||
}); | ||
}; | ||
} | ||
callback(channel, message); | ||
}); | ||
// If lock acquired - execute handler | ||
this.filterIn(function (channel, message, callback) { | ||
if (channel === '!sys.lock.acquired' && self.__lock_handlers__[message.data.request_id]) { | ||
self.__lock_handlers__[message.data.request_id](); | ||
delete self.__lock_handlers__[message.data.request_id]; | ||
} | ||
callback(channel, message); | ||
}); | ||
} | ||
@@ -114,2 +146,18 @@ | ||
// Try acquire lock and exec `fn` if success | ||
// | ||
// - id - lock identifier | ||
// - timeout - optional, lock lifetime in ms, default `5000` | ||
// - fn - handler will be executed if lock is acquired | ||
// | ||
Client.prototype.lock = function (id, timeout, fn) { | ||
if (!fn) { | ||
fn = timeout; | ||
timeout = 5000; | ||
} | ||
this.emit('!sys.lock.request', { id: id, timeout: timeout, fn: fn }); | ||
}; | ||
// Filter input messages | ||
@@ -315,3 +363,3 @@ // | ||
/* global window */ | ||
/* global document, window */ | ||
var LocalStorage = require('./local_storage'); | ||
@@ -321,3 +369,5 @@ var $$ = require('./utils'); | ||
// Max lifetime of router record in storage | ||
var TIMEOUT = 4000; | ||
// Update router record frequency | ||
var UPDATE_INTERVAL = TIMEOUT / 4; | ||
@@ -341,5 +391,8 @@ | ||
this.__handlers__ = []; | ||
this.__router_channels__ = {}; | ||
// Constants for convenience | ||
this.__router_id_prefix__ = this.__namespace__ + 'router_'; | ||
this.__router_channels_prefix__ = this.__namespace__ + 'subscribed_'; | ||
this.__router_channels__ = {}; | ||
this.__lock_prefix__ = this.__namespace__ + 'lock_'; | ||
@@ -360,7 +413,12 @@ // IE broadcasts storage events also to the same window, we should filter that messages | ||
$$.addEvent(window, 'storage', function (e) { | ||
// In IE 9 without delay `e.newValue` will be broken | ||
// http://stackoverflow.com/questions/9292576/localstorage-getitem-returns-old-data-in-ie-9 | ||
setTimeout(function () { | ||
self.__on_changed__(e); | ||
}, 1); | ||
// IE needs kludge because event fire before data was saved | ||
if ('onstoragecommit' in document) { | ||
setTimeout(function () { | ||
self.__on_changed__(e); | ||
}, 1); | ||
return; | ||
} | ||
self.__on_changed__(e); | ||
}); | ||
@@ -384,2 +442,7 @@ | ||
}, UPDATE_INTERVAL); | ||
// Remove outdated lock records | ||
setInterval(function () { | ||
self.__locks_cleanup__(); | ||
}, 1000); | ||
} | ||
@@ -412,2 +475,16 @@ | ||
// If it is system lock message - try acquire lock | ||
if (channel === '!sys.lock.request') { | ||
this.__lock__(message.data.id, message.id, message.data.timeout); | ||
return; | ||
} | ||
// If it is system unlock message - remove lock data | ||
if (channel === '!sys.lock.release') { | ||
this.__ls__.removeItem(this.__lock_prefix__ + message.data.id); | ||
return; | ||
} | ||
var serializedMessage = JSON.stringify({ | ||
@@ -458,2 +535,85 @@ channel: channel, | ||
// Try acquire lock | ||
// | ||
// - lockId (String) | ||
// - requestId (String) | ||
// - timeout (Number) | ||
// | ||
Router.prototype.__lock__ = function (lockId, requestId, timeout) { | ||
var self = this; | ||
var lockKey = this.__lock_prefix__ + lockId; | ||
var lockValue = this.__ls__.getItem(lockKey); | ||
if (lockValue) { | ||
try { | ||
lockValue = JSON.parse(lockValue); | ||
} catch (__) { | ||
lockValue = null; | ||
} | ||
} | ||
// If `expire` not in past - lock already acquired, exit here | ||
if (lockValue && lockValue.expire > Date.now()) { | ||
return; | ||
} | ||
// Try acquire lock | ||
this.__ls__.setItem(lockKey, JSON.stringify({ expire: timeout + Date.now(), requestId: requestId })); | ||
// Read lock value again to check `requestId` (race condition here - other tab may rewrite value in store) | ||
lockValue = this.__ls__.getItem(lockKey); | ||
if (lockValue) { | ||
try { | ||
lockValue = JSON.parse(lockValue); | ||
} catch (__) { | ||
lockValue = null; | ||
} | ||
} | ||
// If `requestId` is not same - other tab acquire lock, exit here | ||
if (!lockValue || lockValue.requestId !== requestId) { | ||
return; | ||
} | ||
// Here lock acquired - send message to clients | ||
this.__handlers__.forEach(function (handler) { | ||
handler('!sys.lock.acquired', { | ||
data: { | ||
request_id: requestId | ||
}, | ||
node_id: self.__node_id__, | ||
id: self.__node_id__ + '_' + (self.__last_message_cnt__++) | ||
}); | ||
}); | ||
}; | ||
// Remove outdated lock records from storage | ||
// | ||
Router.prototype.__locks_cleanup__ = function () { | ||
for (var i = 0, key, val; i < this.__ls__.length; i++) { | ||
key = this.__ls__.key(i); | ||
// Filter localStorage records by prefix | ||
if (key.indexOf(this.__lock_prefix__) !== 0) { | ||
continue; | ||
} | ||
val = this.__ls__.getItem(key); | ||
try { | ||
val = JSON.parse(val); | ||
} catch (__) { | ||
val = null; | ||
} | ||
// If lock expire or record is broken - remove it | ||
if (!val || val.expire < Date.now()) { | ||
this.__ls__.removeItem(key); | ||
} | ||
} | ||
}; | ||
// Update master id, if current tab is master - init connect and subscribe channels | ||
@@ -460,0 +620,0 @@ // |
@@ -1,2 +0,2 @@ | ||
/*! tabex 1.0.1 https://github.com//nodeca/tabex @license MIT */ | ||
!function(_){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=_();else if("function"==typeof define&&define.amd)define([],_);else{var e;e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this,e.tabex=_()}}(function(){return function _(e,t,n){function i(r,o){if(!t[r]){if(!e[r]){var a="function"==typeof require&&require;if(!o&&a)return a(r,!0);if(s)return s(r,!0);var h=new Error("Cannot find module '"+r+"'");throw h.code="MODULE_NOT_FOUND",h}var c=t[r]={exports:{}};e[r][0].call(c.exports,function(_){var t=e[r][1][_];return i(t?t:_)},c,c.exports,_,e,t,n)}return t[r].exports}for(var s="function"==typeof require&&require,r=0;r<n.length;r++)i(n[r]);return i}({1:[function(_,e){"use strict";e.exports=_("./lib")},{"./lib":3}],2:[function(_,e){"use strict";function t(_){var e=this;this.__filters_in__=[],this.__filters_out__=[],this.__subscriptions__=[],this.__node_id__=Math.floor(1e10*Math.random())+1,this.__last_message_cnt__=0,this.__ignore_list__={},this.__router__=_.router,this.__router__.onmessage(function(_,t){e.__onmessage__(_,t)})}var n=_("./utils");t.prototype.emit=function(_,e,t){var i=this,s={id:this.__node_id__+"_"+this.__last_message_cnt__++,node_id:this.__node_id__,data:e};t||(this.__ignore_list__[s.id]=!0),n.asyncEach(this.__filters_out__,_,s,function(_,e){i.__router__.broadcast(_,e)})},t.prototype.on=function(_,e){return this.__subscriptions__.push({channel:_,handler:e}),this.emit("!sys.channels.add",{channel:_}),this},t.prototype.off=function(_,e){var t=this;this.__subscriptions__=this.__subscriptions__.reduce(function(n,i){return i.channel!==_||e&&e!==i.handler?(n.push(i),n):(t.emit("!sys.channels.remove",{channel:_}),n)},[])},t.prototype.filterIn=function(_){return this.__filters_in__.push(_),this},t.prototype.filterOut=function(_){return this.__filters_out__.push(_),this},t.prototype.__onmessage__=function(_,e){var t=this;n.asyncEach(this.__filters_in__,_,e,function(_,e){t.__ignore_list__[e.id]||t.__subscriptions__.forEach(function(t){t.channel===_&&t.handler(e.data,_)})})},e.exports=t},{"./utils":7}],3:[function(_,e){"use strict";var t=_("./router"),n=_("./client"),i=_("./tunnel"),s={},r={_:{}};r._.Router=t,r._.Client=n,r._.Tunnel=i,r.client=function(_){_=_||{};var e,r=_.namespace||"tabex_default_";return _.iframe?e=new i.TunnelClient(_):(s[r]||(s[r]=new t({namespace:r})),e=s[r]),new n({router:e})},r.router=function(_){_=_||{};var e=_.namespace||"tabex_default_";return s[e]||(s[e]=new t({namespace:e})),new i.TunnelRouter({router:s[e],namespace:e,origin:_.origin}),s[e]},e.exports=r},{"./client":2,"./router":5,"./tunnel":6}],4:[function(_,e){"use strict";function t(){}var n=window.localStorage,i={},s=function(){if(document.documentMode&&document.documentMode<9)return!1;if(!n)return!1;try{n.setItem("live_local_storage_is_writable_test",""),n.removeItem("live_local_storage_is_writable_test")}catch(_){return!1}return!0}();Object.defineProperty(t.prototype,"length",{get:function(){return s?n.length:Object.keys(i).length}}),t.prototype.getItem=function(_){return s?n.getItem(_):i.hasOwnProperty(_)?i[_]:null},t.prototype.setItem=function(_,e){s?n.setItem(_,e):i[_]=e},t.prototype.removeItem=function(_){s?n.removeItem(_):i[_]=null},t.prototype.key=function(_){return s?n.key(_):Object.keys(i)[_]},e.exports=t},{}],5:[function(_,e){"use strict";function t(_){var e=this;_=_||{},this.__namespace__=_.namespace||"tabex_default_",this.__node_id__=Math.floor(1e10*Math.random())+1,this.__last_message_cnt__=0,this.__handlers__=[],this.__router_id_prefix__=this.__namespace__+"router_",this.__router_channels_prefix__=this.__namespace__+"subscribed_",this.__router_channels__={},this.__storage_events_filter__=[];for(var t=0;100>t;t++)this.__storage_events_filter__.push("");this.__ls__=new n,this.__master_id__=null,i.addEvent(window,"storage",function(_){setTimeout(function(){e.__on_changed__(_)},1)}),this.__destroyed__=!1,i.addEvent(window,"beforeunload",function(){e.__destroy__()}),i.addEvent(window,"unload",function(){e.__destroy__()}),this.__check_master__(),setInterval(function(){e.__check_master__()},r)}var n=_("./local_storage"),i=_("./utils"),s=4e3,r=s/4;t.prototype.broadcast=function(_,e){if("!sys.channels.add"===_)return this.__router_channels__[e.data.channel]=this.__router_channels__[e.data.channel]||0,this.__router_channels__[e.data.channel]++,void this.__update_channels_list__();if("!sys.channels.remove"===_)return this.__router_channels__[e.data.channel]=this.__router_channels__[e.data.channel]||0,this.__router_channels__[e.data.channel]--,void this.__update_channels_list__();var t=JSON.stringify({channel:_,message:e,random:Math.floor(1e10*Math.random())});this.__storage_events_filter__.shift(),this.__storage_events_filter__.push(this.__namespace__+"broadcast_"+t),this.__ls__.setItem(this.__namespace__+"broadcast",t),this.__handlers__.forEach(function(t){t(_,e)})},t.prototype.onmessage=function(_){var e=this;this.__handlers__.push(_),setTimeout(function(){_("!sys.master",{data:{node_id:e.__node_id__,master_id:e.__master_id__},node_id:e.__node_id__,id:e.__node_id__+"_"+e.__last_message_cnt__++}),e.__on_channels_list_changed__()},0)},t.prototype.__on_master_changed__=function(_){var e=this;return _?(this.__master_id__=+_,void this.__handlers__.forEach(function(_){_("!sys.master",{data:{node_id:e.__node_id__,master_id:e.__master_id__},node_id:e.__node_id__,id:e.__node_id__+"_"+e.__last_message_cnt__++})})):void(this.__get_alive_router_ids__().sort()[0]===this.__node_id__&&(this.__storage_events_filter__.pop(),this.__storage_events_filter__.push(this.__namespace__+"master_"+this.__node_id__),this.__ls__.setItem(this.__namespace__+"master",this.__node_id__),this.__on_master_changed__(this.__node_id__)))},t.prototype.__on_changed__=function(_){if(-1===this.__storage_events_filter__.indexOf(_.key+"_"+_.newValue)&&(_.key===this.__namespace__+"master"&&this.__on_master_changed__(_.newValue),0===_.key.indexOf(this.__router_channels_prefix__)&&this.__on_channels_list_changed__(),_.key===this.__namespace__+"broadcast")){var e=JSON.parse(_.newValue);this.__handlers__.forEach(function(_){_(e.channel,e.message)})}},t.prototype.__destroy__=function(){this.__destroyed__||(this.__destroyed__=!0,this.__ls__.removeItem(this.__router_id_prefix__+this.__node_id__),this.__ls__.removeItem(this.__router_channels_prefix__+this.__node_id__),this.__master_id__===this.__node_id__&&this.__ls__.removeItem(this.__namespace__+"master"))},t.prototype.__get_alive_router_ids__=function(){for(var _,e,t=Date.now()-s,n=[],i=0;i<this.__ls__.length;i++)e=this.__ls__.key(i),0===e.indexOf(this.__router_id_prefix__)&&(_=+e.substr(this.__router_id_prefix__.length),this.__ls__.getItem(e)<t?(this.__ls__.removeItem(e),this.__ls__.removeItem(this.__router_channels_prefix__+_)):n.push(_));return n},t.prototype.__update_channels_list__=function(){var _=this,e=[];Object.keys(this.__router_channels__).forEach(function(t){_.__router_channels__[t]>0&&e.push(t)});var t=JSON.stringify(e.sort());this.__ls__.getItem(this.__router_channels_prefix__+this.__node_id__)!==t&&(this.__storage_events_filter__.pop(),this.__storage_events_filter__.push(this.__router_channels_prefix__+this.__node_id__+"_"+t),this.__ls__.setItem(this.__router_channels_prefix__+this.__node_id__,t),this.__on_channels_list_changed__())},t.prototype.__on_channels_list_changed__=function(){for(var _,e=this,t=[],n=0;n<this.__ls__.length;n++)_=this.__ls__.key(n),0===_.indexOf(this.__router_channels_prefix__)&&(t=t.concat(JSON.parse(this.__ls__.getItem(_))));t=t.reduce(function(_,e){return-1===_.indexOf(e)&&_.push(e),_},[]),this.__handlers__.forEach(function(_){_("!sys.channels.refresh",{id:e.__node_id__+"_"+e.__last_message_cnt__++,node_id:e.__node_id__,data:{channels:t}})})},t.prototype.__check_master__=function(){this.__ls__.setItem(this.__router_id_prefix__+this.__node_id__,Date.now()),this.__master_id__=+this.__ls__.getItem(this.__namespace__+"master"),-1===this.__get_alive_router_ids__().indexOf(this.__master_id__)&&(this.__storage_events_filter__.pop(),this.__storage_events_filter__.push(this.__namespace__+"master_"+this.__node_id__),this.__ls__.setItem(this.__namespace__+"master",this.__node_id__),this.__on_master_changed__(this.__node_id__))},e.exports=t},{"./local_storage":4,"./utils":7}],6:[function(_,e,t){"use strict";function n(_){var e=this;this.__namespace__=_.namespace||"tabex_default_",this.__handlers__=[],this.__iframe_url__=_.iframe,this.__iframe_done__=!1,this.__pending__=[],this.__iframe__=document.createElement("iframe"),this.__iframe__.style.left="-1000px",this.__iframe__.style.position="absolute",this.__iframe__.onload=function(){e.__iframe__.contentWindow.postMessage(JSON.stringify({origin:window.location.origin||window.location.protocol+"//"+window.location.host,namespace:e.__namespace__}),e.__iframe_url__),e.__iframe_done__=!0,e.__pending__.forEach(function(_){e.__iframe__.contentWindow.postMessage(JSON.stringify(_),e.__iframe_url__)}),e.__pending__=null},s.addEvent(window,"message",function(_){if(0===e.__iframe_url__.indexOf(_.origin)){var t;try{t=JSON.parse(_.data)}catch(n){return}t.namespace===e.__namespace__&&e.__handlers__.forEach(function(_){_(t.channel,t.message)})}}),this.__iframe__.src=this.__iframe_url__,s.addEvent(document,"DOMContentLoaded",function(){document.querySelector("body").appendChild(e.__iframe__)})}function i(_){var e,t=this;for(this.__namespace__=_.namespace||"tabex_default_",this.__origin_first_check__=_.origin||window.location.origin||window.location.protocol+"//"+window.location.host,Array.isArray(this.__origin_first_check__)||(this.__origin_first_check__=[this.__origin_first_check__]),e=0;e<this.__origin_first_check__.length;e++)this.__origin_first_check__[e]=this.__origin_first_check__[e].replace(/[-\/\\^$+?.()|[\]{}]/g,"\\$&"),this.__origin_first_check__[e]=this.__origin_first_check__[e].replace(/[*]/g,".+?"),this.__origin_first_check__[e]=new RegExp(this.__origin_first_check__[e]);this.__origin__=null,this.__router__=_.router,s.addEvent(window,"message",function(_){var n=!1;if(!t.__origin__||t.__origin__!==_.origin){for(e=0;e<t.__origin_first_check__.length;e++)if(t.__origin_first_check__[e].test(_.origin)){n=!0;break}if(!n)return}var i;try{i=JSON.parse(_.data)}catch(s){return}if(i.namespace===t.__namespace__)return!t.__origin__&&i.origin?(t.__origin__=i.origin,void t.__router__.onmessage(function(_,e){window.parent.postMessage(JSON.stringify({channel:_,message:e,namespace:t.__namespace__}),t.__origin__)})):void t.__router__.broadcast(i.channel,i.message)})}var s=_("./utils");n.prototype.broadcast=function(_,e){this.__iframe_done__?this.__iframe__.contentWindow.postMessage(JSON.stringify({channel:_,message:e,namespace:this.__namespace__}),this.__iframe_url__):this.__pending__.push({channel:_,message:e,namespace:this.__namespace__})},n.prototype.onmessage=function(_){this.__handlers__.push(_)},t.TunnelClient=n,t.TunnelRouter=i},{"./utils":7}],7:[function(_,e,t){"use strict";t.asyncEach=function(_){function e(){if(0===_.length)return void t.apply(this,arguments);var n=_.shift();n.apply(this,Array.prototype.slice.call(arguments,0).concat(e))}_=_.slice(0);var t=arguments[arguments.length-1],n=Array.prototype.slice.call(arguments,1);n.pop(),e.apply(this,n)},t.addEvent=function(_,e,t){return document.addEventListener?void _.addEventListener(e,t):void _.attachEvent("on"+e,t)}},{}]},{},[1])(1)}); | ||
/*! tabex 1.0.2 https://github.com//nodeca/tabex @license MIT */ | ||
!function(_){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=_();else if("function"==typeof define&&define.amd)define([],_);else{var e;e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this,e.tabex=_()}}(function(){return function _(e,t,i){function n(r,o){if(!t[r]){if(!e[r]){var a="function"==typeof require&&require;if(!o&&a)return a(r,!0);if(s)return s(r,!0);var h=new Error("Cannot find module '"+r+"'");throw h.code="MODULE_NOT_FOUND",h}var c=t[r]={exports:{}};e[r][0].call(c.exports,function(_){var t=e[r][1][_];return n(t?t:_)},c,c.exports,_,e,t,i)}return t[r].exports}for(var s="function"==typeof require&&require,r=0;r<i.length;r++)n(i[r]);return n}({1:[function(_,e){"use strict";e.exports=_("./lib")},{"./lib":3}],2:[function(_,e){"use strict";function t(_){var e=this;this.__filters_in__=[],this.__filters_out__=[],this.__subscriptions__=[],this.__node_id__=Math.floor(1e10*Math.random())+1,this.__last_message_cnt__=0,this.__ignore_list__={},this.__router__=_.router,this.__router__.onmessage(function(_,t){e.__onmessage__(_,t)}),this.__lock_handlers__={},this.filterOut(function(_,t,i){if("!sys.lock.request"===_){var n=t.data.fn,s=t.data.id;delete t.data.fn,e.__lock_handlers__[t.id]=function(){n(function(){e.emit("!sys.lock.release",{id:s})})}}i(_,t)}),this.filterIn(function(_,t,i){"!sys.lock.acquired"===_&&e.__lock_handlers__[t.data.request_id]&&(e.__lock_handlers__[t.data.request_id](),delete e.__lock_handlers__[t.data.request_id]),i(_,t)})}var i=_("./utils");t.prototype.emit=function(_,e,t){var n=this,s={id:this.__node_id__+"_"+this.__last_message_cnt__++,node_id:this.__node_id__,data:e};t||(this.__ignore_list__[s.id]=!0),i.asyncEach(this.__filters_out__,_,s,function(_,e){n.__router__.broadcast(_,e)})},t.prototype.on=function(_,e){return this.__subscriptions__.push({channel:_,handler:e}),this.emit("!sys.channels.add",{channel:_}),this},t.prototype.off=function(_,e){var t=this;this.__subscriptions__=this.__subscriptions__.reduce(function(i,n){return n.channel!==_||e&&e!==n.handler?(i.push(n),i):(t.emit("!sys.channels.remove",{channel:_}),i)},[])},t.prototype.lock=function(_,e,t){t||(t=e,e=5e3),this.emit("!sys.lock.request",{id:_,timeout:e,fn:t})},t.prototype.filterIn=function(_){return this.__filters_in__.push(_),this},t.prototype.filterOut=function(_){return this.__filters_out__.push(_),this},t.prototype.__onmessage__=function(_,e){var t=this;i.asyncEach(this.__filters_in__,_,e,function(_,e){t.__ignore_list__[e.id]||t.__subscriptions__.forEach(function(t){t.channel===_&&t.handler(e.data,_)})})},e.exports=t},{"./utils":7}],3:[function(_,e){"use strict";var t=_("./router"),i=_("./client"),n=_("./tunnel"),s={},r={_:{}};r._.Router=t,r._.Client=i,r._.Tunnel=n,r.client=function(_){_=_||{};var e,r=_.namespace||"tabex_default_";return _.iframe?e=new n.TunnelClient(_):(s[r]||(s[r]=new t({namespace:r})),e=s[r]),new i({router:e})},r.router=function(_){_=_||{};var e=_.namespace||"tabex_default_";return s[e]||(s[e]=new t({namespace:e})),new n.TunnelRouter({router:s[e],namespace:e,origin:_.origin}),s[e]},e.exports=r},{"./client":2,"./router":5,"./tunnel":6}],4:[function(_,e){"use strict";function t(){}var i=window.localStorage,n={},s=function(){if(document.documentMode&&document.documentMode<9)return!1;if(!i)return!1;try{i.setItem("live_local_storage_is_writable_test",""),i.removeItem("live_local_storage_is_writable_test")}catch(_){return!1}return!0}();Object.defineProperty(t.prototype,"length",{get:function(){return s?i.length:Object.keys(n).length}}),t.prototype.getItem=function(_){return s?i.getItem(_):n.hasOwnProperty(_)?n[_]:null},t.prototype.setItem=function(_,e){s?i.setItem(_,e):n[_]=e},t.prototype.removeItem=function(_){s?i.removeItem(_):n[_]=null},t.prototype.key=function(_){return s?i.key(_):Object.keys(n)[_]},e.exports=t},{}],5:[function(_,e){"use strict";function t(_){var e=this;_=_||{},this.__namespace__=_.namespace||"tabex_default_",this.__node_id__=Math.floor(1e10*Math.random())+1,this.__last_message_cnt__=0,this.__handlers__=[],this.__router_channels__={},this.__router_id_prefix__=this.__namespace__+"router_",this.__router_channels_prefix__=this.__namespace__+"subscribed_",this.__lock_prefix__=this.__namespace__+"lock_",this.__storage_events_filter__=[];for(var t=0;100>t;t++)this.__storage_events_filter__.push("");this.__ls__=new i,this.__master_id__=null,n.addEvent(window,"storage",function(_){return"onstoragecommit"in document?void setTimeout(function(){e.__on_changed__(_)},1):void e.__on_changed__(_)}),this.__destroyed__=!1,n.addEvent(window,"beforeunload",function(){e.__destroy__()}),n.addEvent(window,"unload",function(){e.__destroy__()}),this.__check_master__(),setInterval(function(){e.__check_master__()},r),setInterval(function(){e.__locks_cleanup__()},1e3)}var i=_("./local_storage"),n=_("./utils"),s=4e3,r=s/4;t.prototype.broadcast=function(_,e){if("!sys.channels.add"===_)return this.__router_channels__[e.data.channel]=this.__router_channels__[e.data.channel]||0,this.__router_channels__[e.data.channel]++,void this.__update_channels_list__();if("!sys.channels.remove"===_)return this.__router_channels__[e.data.channel]=this.__router_channels__[e.data.channel]||0,this.__router_channels__[e.data.channel]--,void this.__update_channels_list__();if("!sys.lock.request"===_)return void this.__lock__(e.data.id,e.id,e.data.timeout);if("!sys.lock.release"===_)return void this.__ls__.removeItem(this.__lock_prefix__+e.data.id);var t=JSON.stringify({channel:_,message:e,random:Math.floor(1e10*Math.random())});this.__storage_events_filter__.shift(),this.__storage_events_filter__.push(this.__namespace__+"broadcast_"+t),this.__ls__.setItem(this.__namespace__+"broadcast",t),this.__handlers__.forEach(function(t){t(_,e)})},t.prototype.onmessage=function(_){var e=this;this.__handlers__.push(_),setTimeout(function(){_("!sys.master",{data:{node_id:e.__node_id__,master_id:e.__master_id__},node_id:e.__node_id__,id:e.__node_id__+"_"+e.__last_message_cnt__++}),e.__on_channels_list_changed__()},0)},t.prototype.__lock__=function(_,e,t){var i=this,n=this.__lock_prefix__+_,s=this.__ls__.getItem(n);if(s)try{s=JSON.parse(s)}catch(r){s=null}if(!(s&&s.expire>Date.now())){if(this.__ls__.setItem(n,JSON.stringify({expire:t+Date.now(),requestId:e})),s=this.__ls__.getItem(n))try{s=JSON.parse(s)}catch(r){s=null}s&&s.requestId===e&&this.__handlers__.forEach(function(_){_("!sys.lock.acquired",{data:{request_id:e},node_id:i.__node_id__,id:i.__node_id__+"_"+i.__last_message_cnt__++})})}},t.prototype.__locks_cleanup__=function(){for(var _,e,t=0;t<this.__ls__.length;t++)if(_=this.__ls__.key(t),0===_.indexOf(this.__lock_prefix__)){e=this.__ls__.getItem(_);try{e=JSON.parse(e)}catch(i){e=null}(!e||e.expire<Date.now())&&this.__ls__.removeItem(_)}},t.prototype.__on_master_changed__=function(_){var e=this;return _?(this.__master_id__=+_,void this.__handlers__.forEach(function(_){_("!sys.master",{data:{node_id:e.__node_id__,master_id:e.__master_id__},node_id:e.__node_id__,id:e.__node_id__+"_"+e.__last_message_cnt__++})})):void(this.__get_alive_router_ids__().sort()[0]===this.__node_id__&&(this.__storage_events_filter__.pop(),this.__storage_events_filter__.push(this.__namespace__+"master_"+this.__node_id__),this.__ls__.setItem(this.__namespace__+"master",this.__node_id__),this.__on_master_changed__(this.__node_id__)))},t.prototype.__on_changed__=function(_){if(-1===this.__storage_events_filter__.indexOf(_.key+"_"+_.newValue)&&(_.key===this.__namespace__+"master"&&this.__on_master_changed__(_.newValue),0===_.key.indexOf(this.__router_channels_prefix__)&&this.__on_channels_list_changed__(),_.key===this.__namespace__+"broadcast")){var e=JSON.parse(_.newValue);this.__handlers__.forEach(function(_){_(e.channel,e.message)})}},t.prototype.__destroy__=function(){this.__destroyed__||(this.__destroyed__=!0,this.__ls__.removeItem(this.__router_id_prefix__+this.__node_id__),this.__ls__.removeItem(this.__router_channels_prefix__+this.__node_id__),this.__master_id__===this.__node_id__&&this.__ls__.removeItem(this.__namespace__+"master"))},t.prototype.__get_alive_router_ids__=function(){for(var _,e,t=Date.now()-s,i=[],n=0;n<this.__ls__.length;n++)e=this.__ls__.key(n),0===e.indexOf(this.__router_id_prefix__)&&(_=+e.substr(this.__router_id_prefix__.length),this.__ls__.getItem(e)<t?(this.__ls__.removeItem(e),this.__ls__.removeItem(this.__router_channels_prefix__+_)):i.push(_));return i},t.prototype.__update_channels_list__=function(){var _=this,e=[];Object.keys(this.__router_channels__).forEach(function(t){_.__router_channels__[t]>0&&e.push(t)});var t=JSON.stringify(e.sort());this.__ls__.getItem(this.__router_channels_prefix__+this.__node_id__)!==t&&(this.__storage_events_filter__.pop(),this.__storage_events_filter__.push(this.__router_channels_prefix__+this.__node_id__+"_"+t),this.__ls__.setItem(this.__router_channels_prefix__+this.__node_id__,t),this.__on_channels_list_changed__())},t.prototype.__on_channels_list_changed__=function(){for(var _,e=this,t=[],i=0;i<this.__ls__.length;i++)_=this.__ls__.key(i),0===_.indexOf(this.__router_channels_prefix__)&&(t=t.concat(JSON.parse(this.__ls__.getItem(_))));t=t.reduce(function(_,e){return-1===_.indexOf(e)&&_.push(e),_},[]),this.__handlers__.forEach(function(_){_("!sys.channels.refresh",{id:e.__node_id__+"_"+e.__last_message_cnt__++,node_id:e.__node_id__,data:{channels:t}})})},t.prototype.__check_master__=function(){this.__ls__.setItem(this.__router_id_prefix__+this.__node_id__,Date.now()),this.__master_id__=+this.__ls__.getItem(this.__namespace__+"master"),-1===this.__get_alive_router_ids__().indexOf(this.__master_id__)&&(this.__storage_events_filter__.pop(),this.__storage_events_filter__.push(this.__namespace__+"master_"+this.__node_id__),this.__ls__.setItem(this.__namespace__+"master",this.__node_id__),this.__on_master_changed__(this.__node_id__))},e.exports=t},{"./local_storage":4,"./utils":7}],6:[function(_,e,t){"use strict";function i(_){var e=this;this.__namespace__=_.namespace||"tabex_default_",this.__handlers__=[],this.__iframe_url__=_.iframe,this.__iframe_done__=!1,this.__pending__=[],this.__iframe__=document.createElement("iframe"),this.__iframe__.style.left="-1000px",this.__iframe__.style.position="absolute",this.__iframe__.onload=function(){e.__iframe__.contentWindow.postMessage(JSON.stringify({origin:window.location.origin||window.location.protocol+"//"+window.location.host,namespace:e.__namespace__}),e.__iframe_url__),e.__iframe_done__=!0,e.__pending__.forEach(function(_){e.__iframe__.contentWindow.postMessage(JSON.stringify(_),e.__iframe_url__)}),e.__pending__=null},s.addEvent(window,"message",function(_){if(0===e.__iframe_url__.indexOf(_.origin)){var t;try{t=JSON.parse(_.data)}catch(i){return}t.namespace===e.__namespace__&&e.__handlers__.forEach(function(_){_(t.channel,t.message)})}}),this.__iframe__.src=this.__iframe_url__,s.addEvent(document,"DOMContentLoaded",function(){document.querySelector("body").appendChild(e.__iframe__)})}function n(_){var e,t=this;for(this.__namespace__=_.namespace||"tabex_default_",this.__origin_first_check__=_.origin||window.location.origin||window.location.protocol+"//"+window.location.host,Array.isArray(this.__origin_first_check__)||(this.__origin_first_check__=[this.__origin_first_check__]),e=0;e<this.__origin_first_check__.length;e++)this.__origin_first_check__[e]=this.__origin_first_check__[e].replace(/[-\/\\^$+?.()|[\]{}]/g,"\\$&"),this.__origin_first_check__[e]=this.__origin_first_check__[e].replace(/[*]/g,".+?"),this.__origin_first_check__[e]=new RegExp(this.__origin_first_check__[e]);this.__origin__=null,this.__router__=_.router,s.addEvent(window,"message",function(_){var i=!1;if(!t.__origin__||t.__origin__!==_.origin){for(e=0;e<t.__origin_first_check__.length;e++)if(t.__origin_first_check__[e].test(_.origin)){i=!0;break}if(!i)return}var n;try{n=JSON.parse(_.data)}catch(s){return}if(n.namespace===t.__namespace__)return!t.__origin__&&n.origin?(t.__origin__=n.origin,void t.__router__.onmessage(function(_,e){window.parent.postMessage(JSON.stringify({channel:_,message:e,namespace:t.__namespace__}),t.__origin__)})):void t.__router__.broadcast(n.channel,n.message)})}var s=_("./utils");i.prototype.broadcast=function(_,e){this.__iframe_done__?this.__iframe__.contentWindow.postMessage(JSON.stringify({channel:_,message:e,namespace:this.__namespace__}),this.__iframe_url__):this.__pending__.push({channel:_,message:e,namespace:this.__namespace__})},i.prototype.onmessage=function(_){this.__handlers__.push(_)},t.TunnelClient=i,t.TunnelRouter=n},{"./utils":7}],7:[function(_,e,t){"use strict";t.asyncEach=function(_){function e(){if(0===_.length)return void t.apply(this,arguments);var i=_.shift();i.apply(this,Array.prototype.slice.call(arguments,0).concat(e))}_=_.slice(0);var t=arguments[arguments.length-1],i=Array.prototype.slice.call(arguments,1);i.pop(),e.apply(this,i)},t.addEvent=function(_,e,t){return document.addEventListener?void _.addEventListener(e,t):void _.attachEvent("on"+e,t)}},{}]},{},[1])(1)}); |
@@ -40,2 +40,34 @@ // Base client class | ||
}); | ||
// Lock handlers | ||
this.__lock_handlers__ = {}; | ||
// If client make lock request - store handler and remove it from message | ||
this.filterOut(function (channel, message, callback) { | ||
if (channel === '!sys.lock.request') { | ||
var fn = message.data.fn; | ||
var lockId = message.data.id; | ||
delete message.data.fn; | ||
// Wrap handler to pass unlock function into it | ||
self.__lock_handlers__[message.id] = function () { | ||
fn(function unlock() { | ||
self.emit('!sys.lock.release', { id: lockId }); | ||
}); | ||
}; | ||
} | ||
callback(channel, message); | ||
}); | ||
// If lock acquired - execute handler | ||
this.filterIn(function (channel, message, callback) { | ||
if (channel === '!sys.lock.acquired' && self.__lock_handlers__[message.data.request_id]) { | ||
self.__lock_handlers__[message.data.request_id](); | ||
delete self.__lock_handlers__[message.data.request_id]; | ||
} | ||
callback(channel, message); | ||
}); | ||
} | ||
@@ -108,2 +140,18 @@ | ||
// Try acquire lock and exec `fn` if success | ||
// | ||
// - id - lock identifier | ||
// - timeout - optional, lock lifetime in ms, default `5000` | ||
// - fn - handler will be executed if lock is acquired | ||
// | ||
Client.prototype.lock = function (id, timeout, fn) { | ||
if (!fn) { | ||
fn = timeout; | ||
timeout = 5000; | ||
} | ||
this.emit('!sys.lock.request', { id: id, timeout: timeout, fn: fn }); | ||
}; | ||
// Filter input messages | ||
@@ -110,0 +158,0 @@ // |
@@ -6,3 +6,3 @@ // LocalStorage router | ||
/* global window */ | ||
/* global document, window */ | ||
var LocalStorage = require('./local_storage'); | ||
@@ -12,3 +12,5 @@ var $$ = require('./utils'); | ||
// Max lifetime of router record in storage | ||
var TIMEOUT = 4000; | ||
// Update router record frequency | ||
var UPDATE_INTERVAL = TIMEOUT / 4; | ||
@@ -32,5 +34,8 @@ | ||
this.__handlers__ = []; | ||
this.__router_channels__ = {}; | ||
// Constants for convenience | ||
this.__router_id_prefix__ = this.__namespace__ + 'router_'; | ||
this.__router_channels_prefix__ = this.__namespace__ + 'subscribed_'; | ||
this.__router_channels__ = {}; | ||
this.__lock_prefix__ = this.__namespace__ + 'lock_'; | ||
@@ -51,7 +56,12 @@ // IE broadcasts storage events also to the same window, we should filter that messages | ||
$$.addEvent(window, 'storage', function (e) { | ||
// In IE 9 without delay `e.newValue` will be broken | ||
// http://stackoverflow.com/questions/9292576/localstorage-getitem-returns-old-data-in-ie-9 | ||
setTimeout(function () { | ||
self.__on_changed__(e); | ||
}, 1); | ||
// IE needs kludge because event fire before data was saved | ||
if ('onstoragecommit' in document) { | ||
setTimeout(function () { | ||
self.__on_changed__(e); | ||
}, 1); | ||
return; | ||
} | ||
self.__on_changed__(e); | ||
}); | ||
@@ -75,2 +85,7 @@ | ||
}, UPDATE_INTERVAL); | ||
// Remove outdated lock records | ||
setInterval(function () { | ||
self.__locks_cleanup__(); | ||
}, 1000); | ||
} | ||
@@ -103,2 +118,16 @@ | ||
// If it is system lock message - try acquire lock | ||
if (channel === '!sys.lock.request') { | ||
this.__lock__(message.data.id, message.id, message.data.timeout); | ||
return; | ||
} | ||
// If it is system unlock message - remove lock data | ||
if (channel === '!sys.lock.release') { | ||
this.__ls__.removeItem(this.__lock_prefix__ + message.data.id); | ||
return; | ||
} | ||
var serializedMessage = JSON.stringify({ | ||
@@ -149,2 +178,85 @@ channel: channel, | ||
// Try acquire lock | ||
// | ||
// - lockId (String) | ||
// - requestId (String) | ||
// - timeout (Number) | ||
// | ||
Router.prototype.__lock__ = function (lockId, requestId, timeout) { | ||
var self = this; | ||
var lockKey = this.__lock_prefix__ + lockId; | ||
var lockValue = this.__ls__.getItem(lockKey); | ||
if (lockValue) { | ||
try { | ||
lockValue = JSON.parse(lockValue); | ||
} catch (__) { | ||
lockValue = null; | ||
} | ||
} | ||
// If `expire` not in past - lock already acquired, exit here | ||
if (lockValue && lockValue.expire > Date.now()) { | ||
return; | ||
} | ||
// Try acquire lock | ||
this.__ls__.setItem(lockKey, JSON.stringify({ expire: timeout + Date.now(), requestId: requestId })); | ||
// Read lock value again to check `requestId` (race condition here - other tab may rewrite value in store) | ||
lockValue = this.__ls__.getItem(lockKey); | ||
if (lockValue) { | ||
try { | ||
lockValue = JSON.parse(lockValue); | ||
} catch (__) { | ||
lockValue = null; | ||
} | ||
} | ||
// If `requestId` is not same - other tab acquire lock, exit here | ||
if (!lockValue || lockValue.requestId !== requestId) { | ||
return; | ||
} | ||
// Here lock acquired - send message to clients | ||
this.__handlers__.forEach(function (handler) { | ||
handler('!sys.lock.acquired', { | ||
data: { | ||
request_id: requestId | ||
}, | ||
node_id: self.__node_id__, | ||
id: self.__node_id__ + '_' + (self.__last_message_cnt__++) | ||
}); | ||
}); | ||
}; | ||
// Remove outdated lock records from storage | ||
// | ||
Router.prototype.__locks_cleanup__ = function () { | ||
for (var i = 0, key, val; i < this.__ls__.length; i++) { | ||
key = this.__ls__.key(i); | ||
// Filter localStorage records by prefix | ||
if (key.indexOf(this.__lock_prefix__) !== 0) { | ||
continue; | ||
} | ||
val = this.__ls__.getItem(key); | ||
try { | ||
val = JSON.parse(val); | ||
} catch (__) { | ||
val = null; | ||
} | ||
// If lock expire or record is broken - remove it | ||
if (!val || val.expire < Date.now()) { | ||
this.__ls__.removeItem(key); | ||
} | ||
} | ||
}; | ||
// Update master id, if current tab is master - init connect and subscribe channels | ||
@@ -151,0 +263,0 @@ // |
{ | ||
"name": "tabex", | ||
"version": "1.0.1", | ||
"version": "1.0.2", | ||
"description": "Cross-tab message bus for browsers.", | ||
@@ -5,0 +5,0 @@ "keywords": [ |
@@ -114,2 +114,10 @@ tabex | ||
#### client.lock(id, [timeout, ] fn): | ||
- __id__ - lock identifier | ||
- __timeout__ - optional, lock lifetime in ms, default 5000 | ||
- __fn(unlock)__ - handler will be executed if lock is acquired | ||
- __unlock__ - function to release acquired lock | ||
#### client.filterIn(fn), client.filterOut(fn) | ||
@@ -199,5 +207,12 @@ | ||
- `channel` - channel name | ||
- __!sys.lock.request__ - emitted by `tabex.client` to try acquire lock. Message data: | ||
- __id__ - lock identifier | ||
- __timeout__ - lock lifetime in ms | ||
- __!sys.lock.acquired__ - emitted when router acquire lock for client | ||
- __request_id__ - request message id | ||
- __!sys.lock.release__ - emitted by `tabex.client` to release already acquired | ||
lock. Message data: | ||
- __id__ - lock identifier | ||
- __!sys.error__ - emitted on internal errors, for debug. | ||
- __!sys.master__ - sepecific for localStorage-based router. Emitted when tab | ||
become master. Message data: | ||
- __!sys.master__ - sepecific for localStorage-based router. Message data: | ||
- `node_id` - id of "local" router node | ||
@@ -207,3 +222,4 @@ - `master_id` - id of node that become master | ||
__Note.__ `!sys.master` event is broadcasted only when `localStorage` router | ||
used. You should NOT rely on it in your general application logic. | ||
used. You should NOT rely on it in your general application logic. Use locks | ||
instead to filter single handler on broadcasts. | ||
@@ -210,0 +226,0 @@ |
83242
13.99%1668
17.96%376
4.44%