Skip to content

Instantly share code, notes, and snippets.

@jangnezda
Created July 23, 2013 21:13
Show Gist options
  • Save jangnezda/6066229 to your computer and use it in GitHub Desktop.
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.
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