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); } }; }());