Created
July 23, 2013 21:13
-
-
Save jangnezda/6066229 to your computer and use it in GitHub Desktop.
A small component handling connections to the server. Note that there is no actual connecting code -> that is handled by clients. This is more like a manager that handles reconnects, reconnect schedule and edge cases like computer waking up from sleep.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
var voxioConnector = (function() { | |
var clients = [], | |
clientTimeout = 15000, // 15 seconds | |
clientTimeoutId, | |
clientState = { DISCONNECTED: "disconnected", CONNECTED: "connected", CONNECTING: "connecting" }, | |
currentClientIndex = -1, | |
active = false; | |
function handleError(e) { | |
console.log(e); | |
} | |
// ensures that supplied function is executed only once | |
// (could be moved to some utility module) | |
function once(fn) { | |
var done = false; | |
return function () { | |
if (done) { | |
return undefined; | |
} | |
done = true; | |
return fn.apply(this, arguments); | |
}; | |
} | |
// utility object for working with time intervals | |
var TimeIntervals = function(maxLength) { | |
// just make sure that an instance is created even if caller | |
// invoked this function without 'new' or 'Object.create' | |
if(!(this instanceof TimeIntervals)) { | |
return new TimeIntervals(maxLength); | |
} | |
var intervals = []; | |
return { | |
add: function(dateStart, dateEnd) { | |
// only keep in memory last 'maxLength' intervals | |
if(intervals.length >= maxLength) { | |
intervals = intervals.slice(1); | |
} | |
intervals[intervals.length] = { | |
begin: dateStart, | |
end: dateEnd | |
}; | |
}, | |
lastInterval: function() { | |
if(intervals.length > 0) { | |
return intervals[intervals.length - 1]; | |
} | |
return null; | |
}, | |
size: function() { | |
return intervals.length; | |
}, | |
toString: function() { | |
if(intervals.length < 1) { | |
return "[]"; | |
} | |
var intervalString = [], begin, end, i; | |
for(i = 0;i < intervals.length;i++) { | |
begin = (intervals[i].begin === null) ? "null" : intervals[i].begin.toTimeString(); | |
end = (intervals[i].end === null) ? "unfinished" : intervals[i].end.toTimeString(); | |
intervalString[i] = begin + " - " + end; | |
} | |
return "[" + intervalString + "]"; | |
} | |
}; | |
}; //END TimeIntervals | |
// repeatedly checks if computer has waken up from sleep | |
var watcher = (function () { | |
var delta = 3000, // 3 seconds between runs | |
lastTime = 0, | |
active = false, | |
timeoutId, | |
suspendData = new TimeIntervals(10), | |
suspendCallbacks = []; | |
function loop() { | |
var currentTime = (new Date()).getTime(), | |
afterSuspend = (currentTime > (lastTime + 2 * delta)), | |
i; | |
if (afterSuspend) { | |
suspendData.add(new Date(lastTime), new Date(currentTime)); | |
for(i = 0;i < suspendCallbacks.length;i++) { | |
suspendCallbacks[i](suspendData.lastInterval()); | |
} | |
} | |
lastTime = currentTime; | |
if (active) { | |
timeoutId = setTimeout(loop, delta); | |
} | |
} | |
return { | |
init: once(function() { | |
active = true; | |
lastTime = (new Date()).getTime(); | |
timeoutId = setTimeout(loop, delta); | |
}), | |
stop: function() { | |
// cancel timeout | |
clearTimeout(timeoutId); | |
// also, disable active flag, in case we are | |
// in the middle of loop function execution | |
active = false; | |
}, | |
lastSuspend: function() { | |
return suspendData.lastInterval(); | |
}, | |
registerCallback: function(callback) { | |
suspendCallbacks[suspendCallbacks.size] = callback; | |
}, | |
toString: function() { | |
var msg = (suspendData.size() < 1) ? | |
"There are no known suspend/wakeup cycles" : "Known suspends: " + suspendData.toString(); | |
return msg; | |
} | |
}; | |
}()); // END watcher | |
// each client will have one Scheduler | |
var Scheduler = function(index) { | |
if(!(this instanceof Scheduler)) { | |
return new Scheduler(); | |
} | |
var timetable, | |
active = false, | |
timeoutId, | |
runFn; | |
function action() { | |
if(active) { | |
runFn.call(this, index); | |
} | |
} | |
function nextTimeout() { | |
var timeout = timetable[0]; | |
if(timetable.length > 1) { | |
timetable = timetable.slice(1); | |
} | |
return timeout * 1000; | |
} | |
return { | |
init: function(timeouts, fn) { | |
if(timeouts === undefined || timeouts === null || timeouts.lentgh < 1) { | |
timetable = [3, 3, 5, 5, 10]; // default timeouts in seconds | |
} else { | |
timetable = timeouts.slice(0); | |
} | |
runFn = fn; | |
active = true; | |
}, | |
scheduleNext: function() { | |
if(active) { | |
timeoutId = setTimeout(action, nextTimeout()); | |
} | |
}, | |
cancel: function() { | |
clearTimeout(timeoutId); | |
active = false; | |
} | |
}; | |
}; //END Scheduler | |
// object template which has to be used and extended by external clients | |
var Client = function (name, connectFn) { | |
// just make sure, that an instance is created even if caller | |
// invoked this function without 'new' or 'Object.create' | |
if(!(this instanceof Client)) { | |
return new Client(name, connectFn); | |
} | |
var callbacks = {}; | |
function event(ctx, callback) { | |
if(callbacks.hasOwnProperty(callback)) { | |
callbacks[callback].apply(ctx, null); | |
} | |
} | |
return { | |
id: name, | |
init: once(function() { | |
voxioConnector.register(this); | |
}), | |
stop: function() { | |
var prop; | |
for(prop in callbacks) { | |
if(callbacks.hasOwnProperty(prop)) { | |
delete callbacks[prop]; | |
} | |
} | |
}, | |
//manager registers callbacks via this method | |
listener: function(name, fn) { callbacks[name] = fn; }, | |
//concrete clients signal that an event has happened | |
onConnected: function() { event(this, 'connected'); }, | |
onDisconnected: function() { event(this, 'disconnected'); }, | |
onError: function() { event(this, 'error'); } | |
}; | |
}; // END Client | |
function disconnectedInternal(index, endDate) { | |
var c = clients[index]; | |
if(c.state !== clientState.DISCONNECTED) { | |
if(c.state === clientState.CONNECTED) { | |
c.history.lastInterval().end = endDate; | |
} | |
c.state = clientState.DISCONNECTED; | |
c.scheduler.scheduleNext(); | |
} | |
} | |
function errorInternal(index) { | |
if(clients[index].state === clientState.CONNECTED) { | |
disconnectedInternal(index, new Date()); | |
} | |
clients[index].state = clientState.DISCONNECTED; | |
} | |
function connectInternal(index) { | |
// first, set up a timeout to handle a client that is stuck | |
clientTimeoutId = setTimeout(function() { | |
if(clients[index].state === clientState.CONNECTING) { | |
// oops, client wasn't able to connect in time | |
try { | |
clients[index].client.disconnect(); | |
} catch(e) { | |
handleError(e); | |
disconnectedInternal(index, new Date()); | |
} | |
} | |
}, clientTimeout); | |
clients[index].state = clientState.CONNECTING; | |
try { | |
clients[index].client.connect(); | |
} catch(e) { | |
handleError(e); | |
errorInternal(index); | |
} | |
} | |
function reconnectInternal(index) { | |
clearTimeout(clientTimeoutId); | |
clients[i].scheduler.cancel(); | |
clients[i].scheduler.init(null, connectInternal); | |
connectInternal(i); | |
} | |
function nextInChain() { | |
currentClientIndex = currentClientIndex + 1; | |
if(currentClientIndex < clients.length) { | |
connectInternal(currentClientIndex); | |
} | |
} | |
return { | |
init: once(function(chained) { | |
active = true; | |
watcher.init(); | |
if(clients.length > 0) { | |
watcher.registerCallback(function(suspendInterval) { | |
var i; | |
for(i = 0;i < clients.length;i++) { | |
disconnectedInternal(i, suspendInterval.begin); | |
reconnectInternal(i); | |
} | |
}); | |
if(chained) { | |
// the next client is initialized only after the previous is connected | |
nextInChain(); | |
} else { | |
var i; | |
// initialize all clients immediately | |
for(i = 0;i < clients.length;i++) { | |
connectInternal(i); | |
} | |
} | |
} | |
}), | |
register: function(client) { | |
// test if obj conforms to prescribed structure | |
if(client.connect === undefined || client.disconnect === undefined) { | |
throw new TypeError("Object is missing at least one of the required methods [connect, disconnect]"); | |
} | |
var obj = {}, | |
index = clients.length; | |
obj.client = client; | |
obj.state = clientState.DISCONNECTED; | |
obj.history = new TimeIntervals(10); | |
obj.scheduler = new Scheduler(index); | |
obj.scheduler.init(null, connectInternal); | |
obj.client.listener('connected', function() { | |
if(obj.state === clientState.CONNECTED) { | |
// do nothing | |
return; | |
} | |
clearTimeout(clientTimeoutId); | |
obj.state = clientState.CONNECTED; | |
obj.scheduler.cancel(); | |
obj.scheduler.init(null, connectInternal); | |
obj.history.add(new Date(), null); | |
if(currentClientIndex > -1) { | |
nextInChain(); | |
} | |
}); | |
obj.client.listener('disconnected', function() { | |
disconnectedInternal(index, new Date()); | |
}); | |
obj.client.listener('error', function() { | |
errorInternal(index); | |
}); | |
clients[index] = obj; | |
// check, if we're already initialized | |
if(active === true) { | |
connectInternal(index); | |
} | |
}, | |
createClient: function(id) { | |
return new Client(id); | |
}, | |
forceReconnect: function(name) { | |
var i; | |
for(i = 0;i < clients.length;i++) { | |
if(clients[i].client.id === name) { | |
if(clients[i].state !== clientState.CONNECTED) { | |
try { | |
clients[i].client.disconnect(); | |
} catch(e) { | |
handleError(e); | |
disconnectedInternal(i, new Date()); | |
} | |
} | |
reconnectInternal(i); | |
} | |
} | |
}, | |
stop: function() { | |
var i; | |
watcher.stop(); | |
active = false; | |
for(i = 0;i < clients.length;i++) { | |
clients[i].scheduler.cancel(); | |
clients[i].client.stop(); | |
try { | |
clients[i].client.disconnect(); | |
} catch(e) { | |
handleError(e); | |
} | |
disconnectedInternal(i, new Date()); | |
} | |
}, | |
toString: function() { | |
var i, msg; | |
msg = "Connections manager at " + new Date().toTimeString() + ":\n"; | |
msg += "\t* " + watcher.toString(); | |
for(i = 0;i < clients.length;i++) { | |
msg += "\n\t* " + clients[i].client.id + "\n" + | |
"\t\tcurrent status: " + clients[i].state + "\n" + | |
"\t\thistory: " + clients[i].history.toString(); | |
} | |
return msg; | |
}, | |
pageLoaded: function() { | |
// make clients independent of each other by passing 'false' flag | |
this.init(false); | |
} | |
}; | |
}()); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment