/* * NchanSubscriber * usage: var sub = new NchanSubscriber(url, opt); * * opt = { * subscriber: 'longpoll', 'eventsource', or 'websocket', * //or an array of the above indicating subscriber type preference * reconnect: undefined or 'session' or 'persist' * //if the HTML5 sessionStore or localStore should be used to resume * //connections interrupted by a page load * shared: true or undefined * //share connection to same subscriber url between browser * //windows and tabs using localStorage. In shared mode, * //only 1 running subscriber is allowed per url per window/tab. * } * * sub.on("message", function(message, message_metadata) { * // message is a string * // message_metadata may contain 'id' and 'content-type' * }); * * sub.on('connect', function(evt) { * //fired when first connected. * }); * * sub.on('disconnect', function(evt) { * // when disconnected. * }); * * sub.on('error', function(code, message) { * //error callback. not sure about the parameters yet * }); * * sub.reconnect; // should subscriber try to reconnect? true by default. * sub.reconnectTimeout; //how long to wait to reconnect? does not apply to EventSource, which reconnects on its own. * sub.lastMessageId; //last message id. useful for resuming a connection without loss or repetition. * * sub.start(); // begin (or resume) subscribing * sub.stop(); // stop subscriber. do not reconnect. */ //Thanks Darren Whitlen ( @prawnsalad ) for your feedback ;(function (global, moduleName, factory) { // eslint-disable-line "use strict"; /* eslint-disable no-undef */ var newModule = factory(global); if (typeof module === "object" && module != null && module.exports) { module.exports = newModule; } else if (typeof define === "function" && define.amd) { define(function () { return newModule; }); } else { global[moduleName] = newModule; } /* eslint-enable no-undef */ })(typeof window !== "undefined" ? window : this, "NchanSubscriber", function factory(global, undefined) { // eslint-disable-line // https://github.com/yanatan16/nanoajax var nanoajax={}; (function(){var e=["responseType","withCredentials","timeout","onprogress"];nanoajax.ajax=function(r,o){var a=r.headers||{},u=r.body,s=r.method||(u?"POST":"GET"),i=false;var f=t(r.cors);function l(e,t){return function(){if(!i){o(f.status===undefined?e:f.status,f.status===0?"Error":f.response||f.responseText||t,f);i=true}}}f.open(s,r.url,true);var d=f.onload=l(200);f.onreadystatechange=function(){if(f.readyState===4)d()};f.onerror=l(null,"Error");f.ontimeout=l(null,"Timeout");f.onabort=l(null,"Abort");if(u){n(a,"X-Requested-With","XMLHttpRequest");if(!global.FormData||!(u instanceof global.FormData)){n(a,"Content-Type","application/x-www-form-urlencoded")}}for(var p=0,c=e.length,g;pr;++r)i[r].apply(this,e)}return this},Emitter.prototype.listeners=function(t){return this._callbacks=this._callbacks||{},this._callbacks["$"+t]||[]},Emitter.prototype.hasListeners=function(t){return!!this.listeners(t).length};// eslint-disable-line var ughbind = (Function.prototype.bind ? function ughbind(fn, thisObj) { return fn.bind(thisObj); } : function ughbind(fn, thisObj) { return function() { fn.apply(thisObj, arguments); }; } ); var sharedSubscriberTable={}; "use strict"; function NchanSubscriber(url, opt) { if(typeof window !== "undefined" && this === window) { throw "use 'new NchanSubscriber(...)' to initialize"; } this.url = url; opt = opt || {}; //which transport should i use? if(typeof opt === "string") { opt = {subscriber: opt}; } if(opt.transport && !opt.subscriber) { opt.subscriber = opt.transport; } if(typeof opt.subscriber === "string") { opt.subscriber = [ opt.subscriber ]; } this.desiredTransport = opt.subscriber; if(opt.shared) { if (!("localStorage" in global)) { throw "localStorage unavailable for use in shared NchanSubscriber"; } var pre = "NchanSubscriber:" + this.url + ":shared:"; var sharedKey = function(key) { return pre + key; }; var localStorage = global.localStorage; this.shared = { id: "" + Math.random() + Math.random(), key: sharedKey, get: function(key) { return localStorage.getItem(sharedKey(key)); }, set: function(key, val) { return localStorage.setItem(sharedKey(key), val); }, setWithId: ughbind(function(key, val) { return this.shared.set(key, "##" + this.shared.id + ":" + val); }, this), getWithId: ughbind(function(key) { return this.shared.stripIdFromVal(this.shared.get(key)); }, this), stripIdFromVal: function(val) { if(!val) { return val; } var sep = val.indexOf(":"); if(val[0]!=val[1] || val[0]!="#" || !sep) { //throw "not an event value with id"; return val; //for backwards-compatibility } return val.substring(sep+1, val.length); }, remove: function(key) { return localStorage.removeItem(sharedKey(key)); }, matchEventKey: ughbind(function(ev, key) { if(ev.storageArea && ev.storageArea != localStorage){ return false; } return ev.key == sharedKey(key); }, this), matchEventKeyWithId: ughbind(function(ev, key) { if(this.shared.matchEventKey(ev, key)) { var val = ev.newValue; var sep = val.indexOf(":"); if(val[0]!=val[1] || val[0]!="#" || !sep) { //throw "not an event value with id"; return true; //for backwards-compatibility } var id = val.substring(2, sep); return (id != this.shared.id); //ignore own events (accommodations for IE. Fuckin' IE, even after all these years...) } else { return false; } }, this), setRole: ughbind(function(role) { //console.log(this.url, "set shared role to ", role); if(role == "master" && this.shared.role != "master") { var now = new Date().getTime()/1000; this.shared.setWithId("master:created", now); this.shared.setWithId("master:lastSeen", now); } if(role == "slave" && !this.lastMessageId) { this.lastMessageId = this.shared.get("msg:id"); } this.shared.role = role; return this; }, this), demoteToSlave: ughbind(function() { //console.log("demote to slave"); if(this.shared.role != "master") { throw "can't demote non-master to slave"; } if(this.running) { this.stop(); this.shared.setRole("slave"); this.initializeTransport(); this.start(); } else { this.initializeTransport(); } }, this), maybePromoteToMaster: ughbind(function() { if(!(this.running || this.starting)) { //console.log(this.url, "stopped Subscriber won't be promoted to master"); return this; } if(this.shared.maybePromotingToMaster) { //console.log(this.url, " already maybePromotingToMaster"); return; } this.shared.maybePromotingToMaster = true; //console.log(this.url, "maybe promote to master"); var processRoll; var lotteryRoundDuration = 2000; var currentContenders = 0; //roll the dice var roll = Math.random(); var bestRoll = roll; var checkRollInterval; var checkRoll = ughbind(function(dontProcess) { var latestSharedRollTime = parseFloat(this.shared.getWithId("lotteryTime")); var latestSharedRoll = parseFloat(this.shared.getWithId("lottery")); var notStale = !latestSharedRollTime || (latestSharedRollTime > (new Date().getTime() - lotteryRoundDuration * 2)); if(notStale && latestSharedRoll && (!bestRoll || latestSharedRoll > bestRoll)) { bestRoll = latestSharedRoll; } if(!dontProcess) { processRoll(); } }, this); checkRoll(true); this.shared.setWithId("lottery", roll); this.shared.setWithId("lotteryTime", new Date().getTime() / 1000); var rollCallback = ughbind(function(ev) { if(this.shared.matchEventKeyWithId(ev, "lottery") && ev.newValue) { currentContenders += 1; var newVal = parseFloat(this.shared.stripIdFromVal(ev.newValue)); var oldVal = parseFloat(this.shared.stripIdFromVal(ev.oldValue)); if(oldVal > newVal) { this.shared.setWithId("lottery", oldVal); } if(!bestRoll || newVal >= bestRoll) { //console.log("new bestRoll", newVal); bestRoll = newVal; } } }, this); global.addEventListener("storage", rollCallback); var finish = ughbind(function() { //console.log("finish"); this.shared.maybePromotingToMaster = false; //console.log(this.url, this.shared.role); global.removeEventListener("storage", rollCallback); if(checkRollInterval) { clearInterval(checkRollInterval); } if(this.shared && this.shared.role == "master") { this.shared.remove("lottery"); this.shared.remove("lotteryTime"); } if(this.running) { this.stop(); this.initializeTransport(); this.start(); } else { this.initializeTransport(); if(this.starting) { this.start(); } } }, this); processRoll = ughbind(function() { //console.log("roll, bestroll", roll, bestRoll); if(roll < bestRoll) { //console.log(this.url, "loser"); this.shared.setRole("slave"); finish(); } else if(roll >= bestRoll) { //var now = new Date().getTime() / 1000; //var lotteryTime = parseFloat(this.shared.getWithId("lotteryTime")); //console.log(lotteryTime, now - lotteryRoundDuration/1000); if(currentContenders == 0) { //console.log("winner, no more contenders!"); this.shared.setRole("master"); finish(); } else { //console.log("winning, but have contenders", currentContenders); currentContenders = 0; } } }, this); checkRollInterval = global.setInterval(checkRoll, lotteryRoundDuration); }, this), masterCheckInterval: 10000 }; } this.lastMessageId = opt.id || opt.msgId; this.reconnect = typeof opt.reconnect == "undefined" ? true : opt.reconnect; this.reconnectTimeout = opt.reconnectTimeout || 1000; var saveConnectionState; if(!opt.reconnect) { saveConnectionState = function() {}; } else { var index = "NchanSubscriber:" + url + ":lastMessageId"; var storage; if(opt.reconnect == "persist") { storage = ("localStorage" in global) && global.localStorage; if(!storage) throw "can't use reconnect: 'persist' option: localStorage not available"; } else if(opt.reconnect == "session") { storage = ("sessionStorage" in global) && global.sessionStorage; if(!storage) throw "can't use reconnect: 'session' option: sessionStorage not available"; } else { throw "invalid 'reconnect' option value " + opt.reconnect; } saveConnectionState = ughbind(function(msgid) { if(this.shared && this.shared.role == "slave") return; storage.setItem(index, msgid); }, this); this.lastMessageId = storage.getItem(index); } var onUnloadEvent = ughbind(function() { if(this.running) { this.stop(); } if(this.shared && this.shared.role == "master") { this.shared.setWithId("status", "disconnected"); } }, this); global.addEventListener("beforeunload", onUnloadEvent, false); // swap `beforeunload` to `unload` after DOM is loaded global.addEventListener("DOMContentLoaded", function() { global.removeEventListener("beforeunload", onUnloadEvent, false); global.addEventListener("unload", onUnloadEvent, false); }, false); var notifySharedSubscribers; if(this.shared) { notifySharedSubscribers = ughbind(function(name, data) { if(this.shared.role != "master") { return; } if(name == "message") { this.shared.set("msg:id", data[1] && data[1].id || ""); this.shared.set("msg:content-type", data[1] && data[1]["content-type"] || ""); this.shared.set("msg", data[0]); } else if(name == "error") { //TODO } else if(name == "connecting") { this.shared.setWithId("status", "connecting"); } else if(name == "connect") { this.shared.setWithId("status", "connected"); } else if(name == "reconnect") { this.shared.setWithId("status", "reconnecting"); } else if(name == "disconnect") { this.shared.setWithId("status", "disconnected"); } }, this); } else { notifySharedSubscribers = function(){}; } var restartTimeoutIndex; var stopHandler = ughbind(function() { if(!restartTimeoutIndex && this.running && this.reconnect && !this.transport.reconnecting && !this.transport.doNotReconnect) { //console.log("stopHAndler reconnect plz", this.running, this.reconnect); notifySharedSubscribers("reconnect"); restartTimeoutIndex = global.setTimeout(ughbind(function() { restartTimeoutIndex = null; this.stop(); this.start(); }, this), this.reconnectTimeout); } else { notifySharedSubscribers("disconnect"); } }, this); this.on("message", function msg(msg, meta) { this.lastMessageId=meta.id; if(meta.id) { saveConnectionState(meta.id); } notifySharedSubscribers("message", [msg, meta]); //console.log(msg, meta); }); this.on("error", function fail(code, text) { stopHandler(code, text); notifySharedSubscribers("error", [code, text]); //console.log("failure", code, text); }); this.on("connect", function() { this.connected = true; notifySharedSubscribers("connect"); }); this.on("__disconnect", function fail(code, text) { this.connected = false; this.emit("disconnect", code, text); stopHandler(code, text); //console.log("__disconnect", code, text); }); } Emitter(NchanSubscriber.prototype); NchanSubscriber.prototype.initializeTransport = function(possibleTransports) { if(possibleTransports) { this.desiredTransport = possibleTransports; } if(this.shared && this.shared.role == "slave") { this.transport = new this.SubscriberClass["__slave"](ughbind(this.emit, this)); //try it } else { var tryInitializeTransport = ughbind(function(name) { if(!this.SubscriberClass[name]) { throw "unknown subscriber type " + name; } try { this.transport = new this.SubscriberClass[name](ughbind(this.emit, this)); //try it return this.transport; } catch(err) { /*meh...*/ } }, this); var i; if(this.desiredTransport) { for(i=0; i= 200 && code <= 210) { //legit reply var content_type = req.getResponseHeader("Content-Type"); if (!this.parseMultipartMixedMessage(content_type, response_text, req)) { this.emit("message", response_text || "", {"content-type": content_type, "id": this.msgIdFromResponseHeaders(req)}); } this.reqStartTime = new Date().getTime(); this.req = nanoajax.ajax({url: this.url, headers: this.headers}, requestCallback); } else if((code == 0 && response_text == "Error" && req.readyState == 4) || (code === null && response_text != "Abort")) { //console.log("abort!!!"); this.emit("__disconnect", code || 0, response_text); delete this.req; } else if(code !== null) { //HTTP error this.emit("error", code, response_text); delete this.req; } else { //don't care about abortions delete this.req; this.emit("__disconnect"); //console.log("abort!"); } }, this); this.reqStartTime = new Date().getTime(); this.req = nanoajax.ajax({url: this.url, headers: this.headers}, requestCallback); this.emit("connect"); return this; }; Longpoll.prototype.parseMultipartMixedMessage = function(content_type, text, req) { var m = content_type && content_type.match(/^multipart\/mixed;\s+boundary=(.*)$/); if(!m) { return false; } var boundary = m[1]; var msgs = text.split("--" + boundary); if(msgs[0] != "" || !msgs[msgs.length-1].match(/--\r?\n/)) { throw "weird multipart/mixed split"; } msgs = msgs.slice(1, -1); for(var i in msgs) { m = msgs[i].match(/^(.*)\r?\n\r?\n([\s\S]*)\r?\n$/m); var hdrs = m[1].split("\n"); var meta = {}; for(var j in hdrs) { var hdr = hdrs[j].match(/^([^:]+):\s+(.*)/); if(hdr && hdr[1] == "Content-Type") { meta["content-type"] = hdr[2]; } } if(i == msgs.length - 1) { meta["id"] = this.msgIdFromResponseHeaders(req); } this.emit("message", m[2], meta); } return true; }; Longpoll.prototype.msgIdFromResponseHeaders = function(req) { var lastModified, etag; lastModified = req.getResponseHeader("Last-Modified"); etag = req.getResponseHeader("Etag"); if(lastModified) { return "" + Date.parse(lastModified)/1000 + ":" + (etag || "0"); } else if(etag) { return etag; } else { return null; } }; Longpoll.prototype.cancel = function() { if(this.req) { this.req.abort(); delete this.req; } return this; }; return Longpoll; })(), "__slave": (function() { function LocalStoreSlaveTransport(emit) { this.emit = emit; this.doNotReconnect = true; } LocalStoreSlaveTransport.prototype.listen = function(url, shared) { this.shared = shared; this.statusChangeChecker = ughbind(function(ev) { if(this.shared.matchEventKey(ev, "msg")) { var msgId = this.shared.get("msg:id"); var contentType = this.shared.get("msg:content-type"); var msg = this.shared.get("msg"); this.emit("message", msg, {"id": msgId == "" ? undefined : msgId, "content-type": contentType == "" ? undefined : contentType}); } }, this); global.addEventListener("storage", this.statusChangeChecker); }; LocalStoreSlaveTransport.prototype.cancel = function() { global.removeEventListener("storage", this.statusChangeChecker); }; return LocalStoreSlaveTransport; })() }; return NchanSubscriber; });