mirror of
https://github.com/unraid/webgui.git
synced 2026-01-12 12:40:08 -06:00
864 lines
30 KiB
JavaScript
864 lines
30 KiB
JavaScript
/*
|
|
* 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;p<c;p++){g=e[p];if(r[g]!==undefined)f[g]=r[g]}for(var g in a)f.setRequestHeader(g,a[g]);f.send(u);return f};function t(e){if(e&&global.XDomainRequest&&!/MSIE 1/.test(navigator.userAgent))return new XDomainRequest;if(global.XMLHttpRequest)return new XMLHttpRequest}function n(e,t,n){e[t]=e[t]||n}})(); // eslint-disable-line
|
|
|
|
|
|
// https://github.com/component/emitter
|
|
function Emitter(t){return t?mixin(t):void 0}function mixin(t){for(var e in Emitter.prototype)t[e]=Emitter.prototype[e];return t}Emitter.prototype.on=Emitter.prototype.addEventListener=function(t,e){return this._callbacks=this._callbacks||{},(this._callbacks["$"+t]=this._callbacks["$"+t]||[]).push(e),this},Emitter.prototype.once=function(t,e){function i(){this.off(t,i),e.apply(this,arguments)}return i.fn=e,this.on(t,i),this},Emitter.prototype.off=Emitter.prototype.removeListener=Emitter.prototype.removeAllListeners=Emitter.prototype.removeEventListener=function(t,e){if(this._callbacks=this._callbacks||{},0==arguments.length)return this._callbacks={},this;var i=this._callbacks["$"+t];if(!i)return this;if(1==arguments.length)return delete this._callbacks["$"+t],this;for(var r,s=0;s<i.length;s++)if(r=i[s],r===e||r.fn===e){i.splice(s,1);break}return this},Emitter.prototype.emit=function(t){this._callbacks=this._callbacks||{};var e=[].slice.call(arguments,1),i=this._callbacks["$"+t];if(i){i=i.slice(0);for(var r=0,s=i.length;s>r;++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<this.desiredTransport.length; i++) {
|
|
if(tryInitializeTransport(this.desiredTransport[i])) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
for(i in this.SubscriberClass) {
|
|
if (this.SubscriberClass.hasOwnProperty(i) && i[0] != "_" && tryInitializeTransport(i)) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if(! this.transport) {
|
|
throw "can't use any transport type";
|
|
}
|
|
};
|
|
|
|
var storageEventListener;
|
|
|
|
NchanSubscriber.prototype.start = function() {
|
|
if(this.running)
|
|
throw "Can't start NchanSubscriber, it's already started.";
|
|
|
|
this.starting = true;
|
|
|
|
if(this.shared) {
|
|
if(sharedSubscriberTable[this.url] && sharedSubscriberTable[this.url] != this) {
|
|
throw "Only 1 shared subscriber allowed per url per window/tab.";
|
|
}
|
|
sharedSubscriberTable[this.url] = this;
|
|
|
|
if(!this.shared.role) {
|
|
var status = this.shared.getWithId("status");
|
|
storageEventListener = ughbind(function(ev) {
|
|
if(this.shared.matchEventKeyWithId(ev, "status")) {
|
|
var newValue = this.shared.stripIdFromVal(ev.newValue);
|
|
if(newValue == "disconnected") {
|
|
if(this.shared.role == "slave") {
|
|
//play the promotion lottery
|
|
//console.log(this.url, "status changed to disconnected, maybepromotetomaster", ev.newValue, ev.oldValue);
|
|
this.shared.maybePromoteToMaster();
|
|
}
|
|
else if(this.shared.role == "master") {
|
|
//do nothing
|
|
//console.log(this.url, "current role is master, do nothing?...");
|
|
}
|
|
}
|
|
}
|
|
else if(this.shared.role == "master" && this.shared.matchEventKeyWithId(ev, "master:created") && ev.newValue) {
|
|
//a new master has arrived. demote to slave.
|
|
this.shared.demoteToSlave();
|
|
}
|
|
}, this);
|
|
global.addEventListener("storage", storageEventListener);
|
|
if(status == "disconnected") {
|
|
//console.log(this.url, "status == disconnected, maybepromotetomaster");
|
|
this.shared.maybePromoteToMaster();
|
|
}
|
|
else {
|
|
this.shared.setRole(status ? "slave" : "master");
|
|
this.initializeTransport();
|
|
}
|
|
}
|
|
|
|
if(this.shared.role == "master") {
|
|
this.shared.setWithId("status", "connecting");
|
|
this.transport.listen(this.url, this.lastMessageId);
|
|
this.running = true;
|
|
delete this.starting;
|
|
|
|
//master checkin interval
|
|
this.shared.masterIntervalCheckID = global.setInterval(ughbind(function() {
|
|
this.shared.setWithId("master:lastSeen", new Date().getTime() / 1000);
|
|
}, this), this.shared.masterCheckInterval * 0.8);
|
|
}
|
|
else if(this.shared.role == "slave") {
|
|
this.transport.listen(this.url, this.shared);
|
|
this.running = true;
|
|
delete this.starting;
|
|
|
|
//slave check if master is around
|
|
this.shared.masterIntervalCheckID = global.setInterval(ughbind(function() {
|
|
var lastCheckin = parseFloat(this.shared.getWithId("master:lastSeen"));
|
|
if(!lastCheckin || lastCheckin < (new Date().getTime() / 1000) - this.shared.masterCheckInterval / 1000) {
|
|
//master hasn't checked in for too long. assume it's gone.
|
|
this.shared.maybePromoteToMaster();
|
|
}
|
|
}, this), this.shared.masterCheckInterval);
|
|
}
|
|
}
|
|
else {
|
|
if(!this.transport) {
|
|
this.initializeTransport();
|
|
}
|
|
this.transport.listen(this.url, this.lastMessageId);
|
|
this.running = true;
|
|
delete this.starting;
|
|
}
|
|
return this;
|
|
};
|
|
|
|
NchanSubscriber.prototype.stop = function() {
|
|
if(!this.running)
|
|
throw "Can't stop NchanSubscriber, it's not running.";
|
|
|
|
this.running = false;
|
|
if(storageEventListener) {
|
|
global.removeEventListener("storage", storageEventListener);
|
|
}
|
|
this.transport.cancel();
|
|
if(this.shared) {
|
|
delete sharedSubscriberTable[this.url];
|
|
if(this.shared.masterIntervalCheckID) {
|
|
clearInterval(this.shared.masterIntervalCheckID);
|
|
delete this.shared.masterIntervalCheckID;
|
|
}
|
|
}
|
|
return this;
|
|
};
|
|
|
|
function addLastMsgIdToQueryString(url, msgid) {
|
|
if(msgid) {
|
|
var m = url.match(/(\?.*)$/);
|
|
url += (m ? "&" : "?") + "last_event_id=" + encodeURIComponent(msgid);
|
|
}
|
|
return url;
|
|
}
|
|
|
|
NchanSubscriber.prototype.SubscriberClass = {
|
|
|
|
"eventsource": (function() {
|
|
function ESWrapper(emit) {
|
|
EventSource;
|
|
this.emit = emit;
|
|
}
|
|
|
|
ESWrapper.prototype.listen= function(url, msgid) {
|
|
url = addLastMsgIdToQueryString(url, msgid);
|
|
if(this.listener) {
|
|
throw "there's a ES listener running already";
|
|
}
|
|
this.listener = new EventSource(url);
|
|
var l = this.listener;
|
|
l.onmessage = ughbind(function(evt){
|
|
//console.log("message", evt);
|
|
this.emit("message", evt.data, {id: evt.lastEventId});
|
|
}, this);
|
|
|
|
l.onopen = ughbind(function(evt) {
|
|
this.reconnecting = false;
|
|
//console.log("connect", evt);
|
|
this.emit("connect", evt);
|
|
}, this);
|
|
|
|
l.onerror = ughbind(function(evt) {
|
|
//EventSource will try to reconnect by itself
|
|
//console.log("onerror", this.listener.readyState, evt);
|
|
if(this.listener.readyState == EventSource.CONNECTING && !this.reconnecting) {
|
|
if(!this.reconnecting) {
|
|
this.reconnecting = true;
|
|
this.emit("__disconnect", evt);
|
|
}
|
|
}
|
|
else {
|
|
this.emit("__disconnect", evt);
|
|
//console.log('other __disconnect', evt);
|
|
}
|
|
}, this);
|
|
};
|
|
|
|
ESWrapper.prototype.cancel= function() {
|
|
if(this.listener) {
|
|
this.listener.close();
|
|
delete this.listener;
|
|
}
|
|
};
|
|
|
|
return ESWrapper;
|
|
})(),
|
|
|
|
"websocket": (function() {
|
|
function WSWrapper(emit) {
|
|
WebSocket;
|
|
this.emit = emit;
|
|
}
|
|
|
|
WSWrapper.prototype.websocketizeURL = function(url) {
|
|
var m = url.match(/^((\w+:)?\/\/([^/]+))?(\/)?(.*)/);
|
|
var protocol = m[2];
|
|
var host = m[3];
|
|
var absolute = m[4];
|
|
var path = m[5];
|
|
|
|
var loc;
|
|
if(typeof window == "object") {
|
|
loc = window.location;
|
|
}
|
|
else if(typeof document == "object") {
|
|
loc = document.location;
|
|
}
|
|
|
|
if(!protocol && loc) {
|
|
protocol = loc.protocol;
|
|
}
|
|
if(protocol == "https:") {
|
|
protocol = "wss:";
|
|
}
|
|
else if(protocol == "http:") {
|
|
protocol = "ws:";
|
|
}
|
|
else {
|
|
protocol = "wss:"; //default setting: secure
|
|
}
|
|
|
|
if(!host && loc) {
|
|
host = loc.host;
|
|
}
|
|
|
|
if(!absolute) {
|
|
path = loc ? loc.pathname.match(/(.*\/)[^/]*/)[1] + path : "/" + path;
|
|
}
|
|
else {
|
|
path = "/" + path;
|
|
}
|
|
|
|
return protocol + "//" + host + path;
|
|
};
|
|
|
|
WSWrapper.prototype.listen = function(url, msgid) {
|
|
url = this.websocketizeURL(url);
|
|
url = addLastMsgIdToQueryString(url, msgid);
|
|
//console.log(url);
|
|
if(this.listener) {
|
|
throw "websocket already listening";
|
|
}
|
|
this.listener = new WebSocket(url, "ws+meta.nchan");
|
|
var l = this.listener;
|
|
l.onmessage = ughbind(function(evt) {
|
|
var m = evt.data.match(/^id: (.*)\n(content-type: (.*)\n)?\n/m);
|
|
this.emit("message", evt.data.substr(m[0].length), {"id": m[1], "content-type": m[3]});
|
|
}, this);
|
|
|
|
l.onopen = ughbind(function(evt) {
|
|
this.emit("connect", evt);
|
|
//console.log("connect", evt);
|
|
}, this);
|
|
|
|
l.onerror = ughbind(function(evt) {
|
|
//console.log("error", evt);
|
|
this.emit("error", evt, l);
|
|
delete this.listener;
|
|
}, this);
|
|
|
|
l.onclose = ughbind(function(evt) {
|
|
this.emit("__disconnect", evt);
|
|
delete this.listener;
|
|
}, this);
|
|
};
|
|
|
|
WSWrapper.prototype.cancel = function() {
|
|
if(this.listener) {
|
|
this.listener.close();
|
|
delete this.listener;
|
|
}
|
|
};
|
|
|
|
return WSWrapper;
|
|
})(),
|
|
|
|
"longpoll": (function () {
|
|
function Longpoll(emit) {
|
|
this.headers = {};
|
|
this.longPollStartTime = null;
|
|
this.maxLongPollTime = 5*60*1000; //5 minutes
|
|
this.emit = emit;
|
|
}
|
|
|
|
Longpoll.prototype.listen = function(url, msgid) {
|
|
if(this.req) {
|
|
throw "already listening";
|
|
}
|
|
if(url) { this.url=url; }
|
|
var setHeader = ughbind(function(incoming, name) {
|
|
if(incoming) { this.headers[name]= incoming; }
|
|
}, this);
|
|
|
|
if(msgid) {
|
|
this.headers = {"Etag": msgid};
|
|
}
|
|
|
|
this.reqStartTime = new Date().getTime();
|
|
|
|
var requestCallback;
|
|
requestCallback = ughbind(function (code, response_text, req) {
|
|
setHeader(req.getResponseHeader("Last-Modified"), "If-Modified-Since");
|
|
setHeader(req.getResponseHeader("Etag"), "If-None-Match");
|
|
|
|
if(code >= 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;
|
|
});
|
|
|