diff --git a/README.md b/README.md index 9067849..c8775d0 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,8 @@ An elegant web & terminal interface for Unitech/PM2. # Features -- Curses-like dashboard +- Curses-like dashboard. +- Remoting monitor / web control. - All the heartbeats (no matter **monitor** or **tail (logs)**) are automatic destroyed. - The `PM2` processes are watched by a subscribed emitter. - Communicated with `PM2` through **RPC** socket directly. @@ -28,10 +29,10 @@ An elegant web & terminal interface for Unitech/PM2. - Monitor CPU and Memory usage of server in a real-time. - Monitor `PM2` processes in a real-time. - PM2 *restart/stop/delete*. - - *stopWatch* files before *restart/stop/delete* - - *restartWatch* files before *restart* + - *stopWatch* files before *restart/stop/delete*. + - *restartWatch* files before *restart*. - Supports [ANSI color codes](#ss_logs) by [ansi-html](https://github.com/Tjatse/ansi-html). -- High performance. In my case, there are near one hundred processes, but `pm2-gui` works fine. +- High performance. In my case, there are near one hundred processes, but `pm2-gui` works without any suck. # Cautions diff --git a/lib/blessed-widget/layout.js b/lib/blessed-widget/layout.js index f102937..50c1402 100644 --- a/lib/blessed-widget/layout.js +++ b/lib/blessed-widget/layout.js @@ -29,10 +29,6 @@ function Layout(options) { this.options = options; this._eles = {}; this._procCount = 0; - /* - Log({ - level: 1000 - });*/ }; /** @@ -54,13 +50,22 @@ Layout.prototype.render = function (monitor) { namespace: conf.NSP[ns] }, options); - monitor.connect(opts, function (err, socket) { - if (err) { - console.error('Failed due to', err.message, 'when connecting to', socket.nsp); + monitor.connect(opts, function (socket) { + console.info('Connected to', socket.nsp); + !next._called && next(null, socket); + next._called = true; + }, function (err, socket) { + console.log(err); + if (!next._called) { + next(err, socket); + next._called = true; } else { - console.info('Connected to', socket.nsp); + //Log(options.log); + } + console.error('Failed due to', err.message, 'when connecting to', socket.nsp); + if (next._called) { + process.exit(0); } - next(err, socket); }); } }); @@ -69,6 +74,9 @@ Layout.prototype.render = function (monitor) { if (err) { return process.exit(0); } + Log({ + level: 1000 + }); self.sockets = _.extend(res, options.sockets); delete options.sockets; diff --git a/lib/monitor.js b/lib/monitor.js index ee27a40..68ea60e 100644 --- a/lib/monitor.js +++ b/lib/monitor.js @@ -5,6 +5,7 @@ var fs = require('fs'), ansiHTML = require('ansi-html'), totalmem = require('os').totalmem(), pidusage = require('pidusage'), + url = require('url'), socketIOClient = require('socket.io-client'), pm = require('./pm'), stat = require('./stat'), @@ -33,25 +34,6 @@ Monitor.ACCEPT_KEYS = ['pm2', 'refresh', 'daemonize', 'max_restarts', 'port', 'l Monitor.DEF_CONF_FILE = 'pm2-gui.ini'; Monitor.PM2_DAEMON_PROPS = ['DAEMON_RPC_PORT', 'DAEMON_PUB_PORT', 'PM2_LOG_FILE_PATH']; -/** - * Resolve home path. - * @param {String} pm2Home - * @returns {*} - * @private - */ -Monitor.prototype._resolveHome = function (pm2Home) { - if (pm2Home && pm2Home.indexOf('~/') == 0) { - // Get root directory of PM2. - pm2Home = process.env.PM2_HOME || path.resolve(process.env.HOME || process.env.HOMEPATH, pm2Home.substr(2)); - - // Make sure exist. - if (!pm2Home || !fs.existsSync(pm2Home)) { - throw new Error('PM2 root can not be located, try to initialize PM2 by executing `pm2 ls` or set environment variable vi `export PM2_HOME=[ROOT]`.'); - } - } - return pm2Home; -} - /** * Run socket.io server. */ @@ -88,25 +70,47 @@ Monitor.prototype.quit = function () { /** * Connect to socket.io server. * @param {String} ns the namespace. - * @param {Function} callback + * @param {Function} success + * @param {Function} failure */ -Monitor.prototype.connect = function (options, callback) { - if (!options.port || !options.namespace) { - throw new Error('Port and namespace are both required!'); +Monitor.prototype.connect = function (options, success, failure) { + if (!options.port) { + throw new Error('Port is required!'); } - var serverUri = (options.protocol || 'http:') + '//' + (options.hostname || '127.0.0.1') + ':' + options.port + (options.path || '') + options.namespace; + var auth, + serverUri = Monitor.toConnectionString(options); + console.info('Connecting to', serverUri); var socket = socketIOClient(serverUri); socket.on('connect', function () { - !callback.__called && callback(null, socket); - callback.__called = true; + !success._called && success(socket); + success._called = true; }); - socket.on('connect_error', function (err) { - !callback.__called && callback(err, socket); - callback.__called = true; + + socket.on('error', function (err) { + !failure._called && failure(err, socket); + failure._called = true; }); }; +/** + * Resolve home path. + * @param {String} pm2Home + * @returns {*} + * @private + */ +Monitor.prototype._resolveHome = function (pm2Home) { + if (pm2Home && pm2Home.indexOf('~/') == 0) { + // Get root directory of PM2. + pm2Home = process.env.PM2_HOME || path.resolve(process.env.HOME || process.env.HOMEPATH, pm2Home.substr(2)); + + // Make sure exist. + if (!pm2Home || !fs.existsSync(pm2Home)) { + throw new Error('PM2 root can not be located, try to initialize PM2 by executing `pm2 ls` or set environment variable vi `export PM2_HOME=[ROOT]`.'); + } + } + return pm2Home; +}; /** * Initialize options and configurations. @@ -177,13 +181,13 @@ Monitor.prototype._connectSysSock = function (socket) { // Trigger actions of process. socket.on('action', function (action, id) { - console.info('[' + id + ']', action, 'sending to pm2 daemon...'); + console.debug('[pm2:' + id + ']', action, 'sending to pm2 daemon...'); pm.action(self.options.pm2Conf.DAEMON_RPC_PORT, action, id, function (err, forceRefresh) { if (err) { console.error(action, err.message); return socket.emit('action', id, err.message); } - console.info('[' + id + ']', action, 'completed!'); + console.debug('[pm2:' + id + ']', action, 'completed!'); forceRefresh && self._throttleRefresh(); }); }); @@ -252,7 +256,7 @@ Monitor.prototype._connectLogSock = function (socket) { return emitError(err, pm_id, keepANSI); } - console.info('[' + pm_id + ']', 'tail starting...'); + console.info('[pm2:' + pm_id + ']', 'tail starting...'); self._tails[pm_id] = tails; }); } @@ -282,18 +286,19 @@ Monitor.prototype._connectProcSock = function (socket) { function killObserver() { var socks = self._sockio.of(conf.NSP.PROC).sockets, canNotBeDeleted = {}; - if (socks && socks.length > 0) { + + if (Array.isArray(socks) && socks.length > 0) { socks.forEach(function (sock) { canNotBeDeleted[sock.pid.toString()] = 1; }); } - for (var pid in this._usages) { + for (var pid in self._usages) { var timer; - if (!canNotBeDeleted[pid] && (timer = this._usages[pid])) { + if (!canNotBeDeleted[pid] && (timer = self._usages[pid])) { clearInterval(timer); - delete this._usages[pid]; - console.info('[' + pid + ']', 'cpu and memory observer destroyed!'); + delete self._usages[pid]; + console.debug('[pid:' + pid + ']', 'cpu and memory observer destroyed!'); } } } @@ -306,7 +311,7 @@ Monitor.prototype._connectProcSock = function (socket) { return; } - console.info('[' + pidStr + ']', 'cpu and memory observer is running...'); + console.debug('[pid:' + pidStr + ']', 'cpu and memory observer is running...'); function runTimer() { pidusage.stat(pid, function (err, stat) { @@ -493,7 +498,7 @@ Monitor.prototype._killTailProcess = function (pm_id) { } catch (err) {} }); delete self._tails[id]; - console.info('[' + id + ']', 'tail destroyed!'); + console.info('[pm2:' + id + ']', 'tail destroyed!'); } if (!isNaN(pm_id)) { return killTail(pm_id); @@ -529,10 +534,113 @@ Monitor.prototype._listeningSocketIO = function () { console.info('Listening connection event on', nsp.toLowerCase()); } + var auth; + if (!(this.options.agent && (auth = this.options.agent.authorization))) { + return; + } this._sockio.use(function (socket, next) { - // console.log(socket.handshake); + if (auth !== socket.handshake.query.auth) { + return next(new Error('unauthorized')); + } next(); - }) + }); +}; + +/** + * List all available monitors. + * @param {Object} options + * @return {Object} + */ +Monitor.available = function (options) { + options.agent = options.agent || {}; + var remotable = options.remotes && _.keys(options.remotes).length > 0; + + if (options.agent.offline && !remotable) { + return null; + } + + options.port = options.port || 8088; + + if (!remotable) { + return options; + } + var q = { + name: 'socket_server', + message: 'Which socket server would you wanna connect to', + type: 'list', + choices: [] + }, + maxShortLength = 0; + for (var remote in options.remotes) { + var connectionString = options.remotes[remote]; + q.choices.push({ + value: connectionString, + short: remote + }); + maxShortLength = Math.max(maxShortLength, remote.length); + } + if (!options.agent.offline) { + var short = 'localhost', + connectionString = (options.agent && options.agent.authorization ? options.agent.authorization + '@' : '') + '127.0.0.1:' + options.port; + q.choices.push({ + value: connectionString, + short: short + }); + maxShortLength = Math.max(maxShortLength, short.length); + } + + if (q.choices.length > 1) { + q.choices.forEach(function (c) { + c.name = '[' + c.short + Array(maxShortLength - c.short.length + 1).join(' ') + '] ' + c.value; + }); + } + + return q; +}; + +/** + * Convert connection object to string. + * @param {Object} connection + * @return {String} + */ +Monitor.toConnectionString = function (connection) { + var uri = (connection.protocol || 'http:') + '//' + (connection.hostname || '127.0.0.1') + ':' + connection.port + + (connection.path || '') + (connection.namespace || ''); + + if (connection.authorization) { + uri += (uri.indexOf('?') > 0 ? '&' : '?') + 'auth=' + connection.authorization; + } + return uri; +}; + +/** + * Parse connection string to an uri object. + * @param {String} connectionString + * @return {Object} + */ +Monitor.parseConnectionString = function (connectionString) { + var connection = { + port: 8088, + hostname: '127.0.0.1', + authorization: '' + }; + var lastAt = connectionString.lastIndexOf('@'); + if (lastAt >= 0) { + connection.authorization = connectionString.slice(0, lastAt); + connectionString = connectionString.slice(lastAt + 1); + } + if (!/^http(s)?:\/\//i.test(connectionString)) { + connectionString = 'http://' + connectionString; + } + + if (connectionString) { + connectionString = url.parse(connectionString); + connection.hostname = connectionString.hostname; + connection.port = connectionString.port; + connection.path = _.trimLeft(connectionString.path, '/'); + connection.protocol = connectionString.protocol; + } + return connection; }; Object.defineProperty(Monitor.prototype, 'sockio', { diff --git a/pm2-gui.ini b/pm2-gui.ini index 66cac0c..847a6f2 100644 --- a/pm2-gui.ini +++ b/pm2-gui.ini @@ -31,17 +31,17 @@ date = false ; ; Log level, one of debug, log, info, warn, error. ; -level = log +level = debug [agent] ; ; This authorization will be used to authorize socket / web connections if it's set. ; -; authorization = AuTh +authorization = AuTh ; ; A value indicates whether agent offline or not. ; -; offline = false +; offline = true [remotes] ; ; the dashboard and web server will use this section to connect remoting socket server @@ -51,3 +51,4 @@ level = log ; pm2@172 = 192.168.1.172:9001 ; pm2@173 = 192.168.1.173:9000 ; +pm2@138 = AuTh107@192.168.100.138:8088 diff --git a/pm2-gui.js b/pm2-gui.js index 8877af0..2c161b0 100644 --- a/pm2-gui.js +++ b/pm2-gui.js @@ -2,7 +2,6 @@ var chalk = require('chalk'), path = require('path'), fs = require('fs'), _ = require('lodash'), - url = require('url'), socketIO = require('socket.io'), inquirer = require("inquirer"), conf = require('./lib/util/conf'), @@ -93,59 +92,28 @@ function dashboard(confFile) { var monitor = slave({ confFile: confFile }), - options = _.clone(monitor.options); + options = _.clone(monitor.options), + q = Monitor.available(options); - options.agent = options.agent || {}; - var remotable = options.remotes && _.keys(options.remotes).length > 0; - - if (options.agent.offline && remotable) { + if (!q) { console.error('No agent is online, can not start it.'); return process.exit(0); } - options.port = options.port || 8088; - - if (!remotable) { - return _connectToDashboard(monitor, options); + var ql = q.choices.length; + if (ql == 1) { + console.info('There is just one remoting server online, try to connect it.') + return _connectToDashboard(monitor, options, Monitor.parseConnectionString(q.choices[0].value)); } + + q.choices.splice(ql - 1, 0, new inquirer.Separator()); + console.info('Remoting servers are online, choose one you are intrested in.') - var q = { - name: 'socket_server', - message: 'Which socket server would you wanna connect to', - type: 'list', - choices: [] - }, - maxShortLength = 0; - for (var remote in options.remotes) { - var connectionString = options.remotes[remote]; - q.choices.push({ - value: connectionString, - short: remote - }); - maxShortLength = Math.max(maxShortLength, remote.length); - } - if (!options.agent.offline) { - q.choices.push(new inquirer.Separator()); - var short = 'localhost', - connectionString = (options.agent && options.agent.authorization ? options.agent.authorization + '@' : '') + '127.0.0.1:' + options.port; - q.choices.push({ - value: connectionString, - short: short - }); - maxShortLength = Math.max(maxShortLength, short.length); - } - - q.choices.forEach(function (c) { - if (c.type != 'separator') { - c.name = '[' + c.short + Array(maxShortLength - c.short.length + 1).join(' ') + '] ' + c.value; - } - }); - console.log(''); inquirer.prompt(q, function (answers) { console.log(''); - _connectToDashboard(monitor, options, _parseConnectionString(answers.socket_server)); + _connectToDashboard(monitor, options, Monitor.parseConnectionString(answers.socket_server)); }); } @@ -233,51 +201,24 @@ function slave(options) { } function _connectToDashboard(monitor, options, connection) { - if (!connection || !!~['127.0.0.1', '0.0.0.0', 'localhost'].indexOf(connection.hostname)) { - return monitor.connect(_.extend({ - namespace: conf.NSP.SYS - }, options), function (err, socket) { - if (err || !socket) { - console.warn('Agent is offline, try to start it.'); - var sockio = socketIO(); - sockio.listen(options.port); - monitor.sockio = sockio; - monitor.run(); - } else { - console.info('Agent is online, try to connect it in dashboard directly.'); - options.sockets = {}; - var ns = _.trimLeft(conf.NSP.SYS, '/'); - options.sockets[ns] = socket; + connection = _.extend({}, options, connection); + if (!!~['127.0.0.1', '0.0.0.0', 'localhost'].indexOf(connection.hostname)) { + return monitor.connect(connection, function (socket) { + console.info('Agent is online, try to connect it in dashboard directly.'); + layout(connection).render(monitor); + }, function (err, socket) { + if (err == 'unauthorized') { + console.error('There was an error with the authentication:', err); + return process.exit(0); } - - layout(options).render(monitor); + console.warn('Agent is offline, try to start it.'); + var sockio = socketIO(); + sockio.listen(connection.port); + monitor.sockio = sockio; + monitor.run(); + layout(connection).render(monitor); }); } layout(connection).render(monitor); -} - -function _parseConnectionString(connectionString) { - var connection = { - port: 8088, - hostname: '127.0.0.1', - authorization: '' - }; - var lastAt = connectionString.lastIndexOf('@'); - if (lastAt >= 0) { - connection.authorization = connectionString.slice(0, lastAt); - connectionString = connectionString.slice(lastAt + 1); - } - if (!/^http(s)?:\/\//i.test(connectionString)) { - connectionString = 'http://' + connectionString; - } - - if (connectionString) { - connectionString = url.parse(connectionString); - connection.hostname = connectionString.hostname; - connection.port = connectionString.port; - connection.path = _.trimLeft(connectionString.path, '/'); - connection.protocol = connectionString.protocol; - } - return connection; -} +} \ No newline at end of file diff --git a/web/public/js/index.html.js b/web/public/js/index.html.js index c71f275..bbb80ad 100644 --- a/web/public/js/index.html.js +++ b/web/public/js/index.html.js @@ -19,21 +19,6 @@ var sysStat, popupProc, scrolled; -/** - * Initialization. - */ -$(window).ready(function () { - - prepareDOM(); - - initFullPage(); - - listenSocket(); - - renderFanavi(); - -}); - /** * Prepare DOM, cache elements, templates... */ @@ -98,11 +83,11 @@ function initFullPage() { /** * Set fullPage enable or disable. * @param {Boolean} enable - * @param {Boolean} exceptScroll + * @param {Boolean} unscrollable */ -function setFPEnable(enable, exceptScroll) { +function setFPEnable(enable, unscrollable) { $.fn.fullpage.setAllowScrolling(enable); - if (!exceptScroll) { + if (!unscrollable) { $.fn.fullpage.setKeyboardScrolling(enable); eles.fpNav[enable ? 'fadeIn' : 'fadeOut'](); } @@ -112,15 +97,40 @@ function setFPEnable(enable, exceptScroll) { * Connect to socket server. */ function connectSocketServer(ns) { - var socket = io('127.0.0.1:8088' + ns + '?token=abc'); - socket.on('error', info); + var uri = GUI.connection.value, + index = uri.indexOf('?'), + query = ''; + if (index > 0) { + query = uri.slice(index); + uri = uri.slice(0, index); + } + console.log('before', uri, query, ns); + + uri = _.trimRight(uri, '/') + (ns || '') + query; + if (!ns) { + return io.connect(uri).on('error', onError); + } + var socket = io.connect(uri); + socket.on('error', onError); return socket; } +/** + * Fires on error. + * @param {String} err + */ +function onError(err) { + if (err == 'unauthorized') { + err = 'There was an error with the authentication: ' + err; + } + info(err); +} + /** * Initialize socket.io client and add listeners. */ function listenSocket() { + connectSocketServer(); sockets.sys = connectSocketServer(NSP.SYS); // information from server. sockets.sys.on('info', info); diff --git a/web/public/js/socket.io.js b/web/public/js/socket.io.js index eed35cc..fb6968c 100644 --- a/web/public/js/socket.io.js +++ b/web/public/js/socket.io.js @@ -1,350 +1,6988 @@ -!function(e){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=e();else if("function"==typeof define&&define.amd)define([],e);else{var f;"undefined"!=typeof window?f=window:"undefined"!=typeof global?f=global:"undefined"!=typeof self&&(f=self),f.io=e()}}(function(){var define,module,exports;return(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);throw new Error("Cannot find module '"+o+"'")} -var f=n[o]={exports:{}};t[o][0].call(f.exports,function(e){var n=t[o][1][e];return s(n?n:e)},f,f.exports,e,t,n,r)} -return n[o].exports} -var i=typeof require=="function"&&require;for(var o=0;o0&&!this.encoding){var pack=this.packetBuffer.shift();this.packet(pack);}};Manager.prototype.cleanup=function(){var sub;while(sub=this.subs.shift())sub.destroy();this.packetBuffer=[];this.encoding=false;this.decoder.destroy();};Manager.prototype.close=Manager.prototype.disconnect=function(){this.skipReconnect=true;this.readyState='closed';this.engine&&this.engine.close();};Manager.prototype.onclose=function(reason){debug('close');this.cleanup();this.readyState='closed';this.emit('close',reason);if(this._reconnection&&!this.skipReconnect){this.reconnect();}};Manager.prototype.reconnect=function(){if(this.reconnecting||this.skipReconnect)return this;var self=this;this.attempts++;if(this.attempts>this._reconnectionAttempts){debug('reconnect failed');this.emitAll('reconnect_failed');this.reconnecting=false;}else{var delay=this.attempts*this.reconnectionDelay();delay=Math.min(delay,this.reconnectionDelayMax());debug('will wait %dms before reconnect attempt',delay);this.reconnecting=true;var timer=setTimeout(function(){if(self.skipReconnect)return;debug('attempting reconnect');self.emitAll('reconnect_attempt',self.attempts);self.emitAll('reconnecting',self.attempts);if(self.skipReconnect)return;self.open(function(err){if(err){debug('reconnect attempt error');self.reconnecting=false;self.reconnect();self.emitAll('reconnect_error',err.data);}else{debug('reconnect success');self.onreconnect();}});},delay);this.subs.push({destroy:function(){clearTimeout(timer);}});}};Manager.prototype.onreconnect=function(){var attempt=this.attempts;this.attempts=0;this.reconnecting=false;this.emitAll('reconnect',attempt);};},{"./on":4,"./socket":5,"./url":6,"component-bind":7,"component-emitter":8,"debug":9,"engine.io-client":10,"indexof":36,"object-component":37,"socket.io-parser":40}],4:[function(_dereq_,module,exports){module.exports=on;function on(obj,ev,fn){obj.on(ev,fn);return{destroy:function(){obj.removeListener(ev,fn);}};}},{}],5:[function(_dereq_,module,exports){var parser=_dereq_('socket.io-parser');var Emitter=_dereq_('component-emitter');var toArray=_dereq_('to-array');var on=_dereq_('./on');var bind=_dereq_('component-bind');var debug=_dereq_('debug')('socket.io-client:socket');var hasBin=_dereq_('has-binary');module.exports=exports=Socket;var events={connect:1,connect_error:1,connect_timeout:1,disconnect:1,error:1,reconnect:1,reconnect_attempt:1,reconnect_failed:1,reconnect_error:1,reconnecting:1};var emit=Emitter.prototype.emit;function Socket(io,nsp){this.io=io;this.nsp=nsp;this.json=this;this.ids=0;this.acks={};if(this.io.autoConnect)this.open();this.receiveBuffer=[];this.sendBuffer=[];this.connected=false;this.disconnected=true;} -Emitter(Socket.prototype);Socket.prototype.subEvents=function(){if(this.subs)return;var io=this.io;this.subs=[on(io,'open',bind(this,'onopen')),on(io,'packet',bind(this,'onpacket')),on(io,'close',bind(this,'onclose'))];};Socket.prototype.open=Socket.prototype.connect=function(){if(this.connected)return this;this.subEvents();this.io.open();if('open'==this.io.readyState)this.onopen();return this;};Socket.prototype.send=function(){var args=toArray(arguments);args.unshift('message');this.emit.apply(this,args);return this;};Socket.prototype.emit=function(ev){if(events.hasOwnProperty(ev)){emit.apply(this,arguments);return this;} -var args=toArray(arguments);var parserType=parser.EVENT;if(hasBin(args)){parserType=parser.BINARY_EVENT;} -var packet={type:parserType,data:args};if('function'==typeof args[args.length-1]){debug('emitting packet with ack id %d',this.ids);this.acks[this.ids]=args.pop();packet.id=this.ids++;} -if(this.connected){this.packet(packet);}else{this.sendBuffer.push(packet);} -return this;};Socket.prototype.packet=function(packet){packet.nsp=this.nsp;this.io.packet(packet);};Socket.prototype.onopen=function(){debug('transport is open - connecting');if('/'!=this.nsp){this.packet({type:parser.CONNECT});}};Socket.prototype.onclose=function(reason){debug('close (%s)',reason);this.connected=false;this.disconnected=true;this.emit('disconnect',reason);};Socket.prototype.onpacket=function(packet){if(packet.nsp!=this.nsp)return;switch(packet.type){case parser.CONNECT:this.onconnect();break;case parser.EVENT:this.onevent(packet);break;case parser.BINARY_EVENT:this.onevent(packet);break;case parser.ACK:this.onack(packet);break;case parser.BINARY_ACK:this.onack(packet);break;case parser.DISCONNECT:this.ondisconnect();break;case parser.ERROR:this.emit('error',packet.data);break;}};Socket.prototype.onevent=function(packet){var args=packet.data||[];debug('emitting event %j',args);if(null!=packet.id){debug('attaching ack callback to event');args.push(this.ack(packet.id));} -if(this.connected){emit.apply(this,args);}else{this.receiveBuffer.push(args);}};Socket.prototype.ack=function(id){var self=this;var sent=false;return function(){if(sent)return;sent=true;var args=toArray(arguments);debug('sending ack %j',args);var type=hasBin(args)?parser.BINARY_ACK:parser.ACK;self.packet({type:type,id:id,data:args});};};Socket.prototype.onack=function(packet){debug('calling ack %s with %j',packet.id,packet.data);var fn=this.acks[packet.id];fn.apply(this,packet.data);delete this.acks[packet.id];};Socket.prototype.onconnect=function(){this.connected=true;this.disconnected=false;this.emit('connect');this.emitBuffered();};Socket.prototype.emitBuffered=function(){var i;for(i=0;i=hour)return(ms/hour).toFixed(1)+'h';if(ms>=min)return(ms/min).toFixed(1)+'m';if(ms>=sec)return(ms/sec|0)+'s';return ms+'ms';};debug.enabled=function(name){for(var i=0,len=debug.skips.length;i';iframe=document.createElement(html);}catch(e){iframe=document.createElement('iframe');iframe.name=self.iframeId;iframe.src='javascript:0';} -iframe.id=self.iframeId;self.form.appendChild(iframe);self.iframe=iframe;} -initIframe();data=data.replace(rEscapedNewline,'\\\n');this.area.value=data.replace(rNewline,'\\n');try{this.form.submit();}catch(e){} -if(this.iframe.attachEvent){this.iframe.onreadystatechange=function(){if(self.iframe.readyState=='complete'){complete();}};}else{this.iframe.onload=complete;}};}).call(this,typeof self!=="undefined"?self:typeof window!=="undefined"?window:{})},{"./polling":17,"component-inherit":20}],16:[function(_dereq_,module,exports){(function(global){var XMLHttpRequest=_dereq_('xmlhttprequest');var Polling=_dereq_('./polling');var Emitter=_dereq_('component-emitter');var inherit=_dereq_('component-inherit');var debug=_dereq_('debug')('engine.io-client:polling-xhr');module.exports=XHR;module.exports.Request=Request;function empty(){} -function XHR(opts){Polling.call(this,opts);if(global.location){var isSSL='https:'==location.protocol;var port=location.port;if(!port){port=isSSL?443:80;} -this.xd=opts.hostname!=global.location.hostname||port!=opts.port;this.xs=opts.secure!=isSSL;}} -inherit(XHR,Polling);XHR.prototype.supportsBinary=true;XHR.prototype.request=function(opts){opts=opts||{};opts.uri=this.uri();opts.xd=this.xd;opts.xs=this.xs;opts.agent=this.agent||false;opts.supportsBinary=this.supportsBinary;opts.enablesXDR=this.enablesXDR;return new Request(opts);};XHR.prototype.doWrite=function(data,fn){var isBinary=typeof data!=='string'&&data!==undefined;var req=this.request({method:'POST',data:data,isBinary:isBinary});var self=this;req.on('success',fn);req.on('error',function(err){self.onError('xhr post error',err);});this.sendXhr=req;};XHR.prototype.doPoll=function(){debug('xhr poll');var req=this.request();var self=this;req.on('data',function(data){self.onData(data);});req.on('error',function(err){self.onError('xhr poll error',err);});this.pollXhr=req;};function Request(opts){this.method=opts.method||'GET';this.uri=opts.uri;this.xd=!!opts.xd;this.xs=!!opts.xs;this.async=false!==opts.async;this.data=undefined!=opts.data?opts.data:null;this.agent=opts.agent;this.isBinary=opts.isBinary;this.supportsBinary=opts.supportsBinary;this.enablesXDR=opts.enablesXDR;this.create();} -Emitter(Request.prototype);Request.prototype.create=function(){var xhr=this.xhr=new XMLHttpRequest({agent:this.agent,xdomain:this.xd,xscheme:this.xs,enablesXDR:this.enablesXDR});var self=this;try{debug('xhr open %s: %s',this.method,this.uri);xhr.open(this.method,this.uri,this.async);if(this.supportsBinary){xhr.responseType='arraybuffer';} -if('POST'==this.method){try{if(this.isBinary){xhr.setRequestHeader('Content-type','application/octet-stream');}else{xhr.setRequestHeader('Content-type','text/plain;charset=UTF-8');}}catch(e){}} -if('withCredentials'in xhr){xhr.withCredentials=true;} -if(this.hasXDR()){xhr.onload=function(){self.onLoad();};xhr.onerror=function(){self.onError(xhr.responseText);};}else{xhr.onreadystatechange=function(){if(4!=xhr.readyState)return;if(200==xhr.status||1223==xhr.status){self.onLoad();}else{setTimeout(function(){self.onError(xhr.status);},0);}};} -debug('xhr data %s',this.data);xhr.send(this.data);}catch(e){setTimeout(function(){self.onError(e);},0);return;} -if(global.document){this.index=Request.requestsCount++;Request.requests[this.index]=this;}};Request.prototype.onSuccess=function(){this.emit('success');this.cleanup();};Request.prototype.onData=function(data){this.emit('data',data);this.onSuccess();};Request.prototype.onError=function(err){this.emit('error',err);this.cleanup();};Request.prototype.cleanup=function(){if('undefined'==typeof this.xhr||null===this.xhr){return;} -if(this.hasXDR()){this.xhr.onload=this.xhr.onerror=empty;}else{this.xhr.onreadystatechange=empty;} -try{this.xhr.abort();}catch(e){} -if(global.document){delete Request.requests[this.index];} -this.xhr=null;};Request.prototype.onLoad=function(){var data;try{var contentType;try{contentType=this.xhr.getResponseHeader('Content-Type').split(';')[0];}catch(e){} -if(contentType==='application/octet-stream'){data=this.xhr.response;}else{if(!this.supportsBinary){data=this.xhr.responseText;}else{data='ok';}}}catch(e){this.onError(e);} -if(null!=data){this.onData(data);}};Request.prototype.hasXDR=function(){return'undefined'!==typeof global.XDomainRequest&&!this.xs&&this.enablesXDR;};Request.prototype.abort=function(){this.cleanup();};if(global.document){Request.requestsCount=0;Request.requests={};if(global.attachEvent){global.attachEvent('onunload',unloadHandler);}else if(global.addEventListener){global.addEventListener('beforeunload',unloadHandler);}} -function unloadHandler(){for(var i in Request.requests){if(Request.requests.hasOwnProperty(i)){Request.requests[i].abort();}}}}).call(this,typeof self!=="undefined"?self:typeof window!=="undefined"?window:{})},{"./polling":17,"component-emitter":8,"component-inherit":20,"debug":9,"xmlhttprequest":19}],17:[function(_dereq_,module,exports){var Transport=_dereq_('../transport');var parseqs=_dereq_('parseqs');var parser=_dereq_('engine.io-parser');var inherit=_dereq_('component-inherit');var debug=_dereq_('debug')('engine.io-client:polling');module.exports=Polling;var hasXHR2=(function(){var XMLHttpRequest=_dereq_('xmlhttprequest');var xhr=new XMLHttpRequest({xdomain:false});return null!=xhr.responseType;})();function Polling(opts){var forceBase64=(opts&&opts.forceBase64);if(!hasXHR2||forceBase64){this.supportsBinary=false;} -Transport.call(this,opts);} -inherit(Polling,Transport);Polling.prototype.name='polling';Polling.prototype.doOpen=function(){this.poll();};Polling.prototype.pause=function(onPause){var pending=0;var self=this;this.readyState='pausing';function pause(){debug('paused');self.readyState='paused';onPause();} -if(this.polling||!this.writable){var total=0;if(this.polling){debug('we are currently polling - waiting to pause');total++;this.once('pollComplete',function(){debug('pre-pause polling complete');--total||pause();});} -if(!this.writable){debug('we are currently writing - waiting to pause');total++;this.once('drain',function(){debug('pre-pause writing complete');--total||pause();});}}else{pause();}};Polling.prototype.poll=function(){debug('polling');this.polling=true;this.doPoll();this.emit('poll');};Polling.prototype.onData=function(data){var self=this;debug('polling got data %s',data);var callback=function(packet,index,total){if('opening'==self.readyState){self.onOpen();} -if('close'==packet.type){self.onClose();return false;} -self.onPacket(packet);};parser.decodePayload(data,this.socket.binaryType,callback);if('closed'!=this.readyState){this.polling=false;this.emit('pollComplete');if('open'==this.readyState){this.poll();}else{debug('ignoring poll - transport state "%s"',this.readyState);}}};Polling.prototype.doClose=function(){var self=this;function close(){debug('writing close packet');self.write([{type:'close'}]);} -if('open'==this.readyState){debug('transport open - closing');close();}else{debug('transport not open - deferring close');this.once('open',close);}};Polling.prototype.write=function(packets){var self=this;this.writable=false;var callbackfn=function(){self.writable=true;self.emit('drain');};var self=this;parser.encodePayload(packets,this.supportsBinary,function(data){self.doWrite(data,callbackfn);});};Polling.prototype.uri=function(){var query=this.query||{};var schema=this.secure?'https':'http';var port='';if(false!==this.timestampRequests){query[this.timestampParam]=+new Date+'-'+Transport.timestamps++;} -if(!this.supportsBinary&&!query.sid){query.b64=1;} -query=parseqs.encode(query);if(this.port&&(('https'==schema&&this.port!=443)||('http'==schema&&this.port!=80))){port=':'+this.port;} -if(query.length){query='?'+query;} -return schema+'://'+this.hostname+port+this.path+query;};},{"../transport":13,"component-inherit":20,"debug":9,"engine.io-parser":21,"parseqs":29,"xmlhttprequest":19}],18:[function(_dereq_,module,exports){var Transport=_dereq_('../transport');var parser=_dereq_('engine.io-parser');var parseqs=_dereq_('parseqs');var inherit=_dereq_('component-inherit');var debug=_dereq_('debug')('engine.io-client:websocket');var WebSocket=_dereq_('ws');module.exports=WS;function WS(opts){var forceBase64=(opts&&opts.forceBase64);if(forceBase64){this.supportsBinary=false;} -Transport.call(this,opts);} -inherit(WS,Transport);WS.prototype.name='websocket';WS.prototype.supportsBinary=true;WS.prototype.doOpen=function(){if(!this.check()){return;} -var self=this;var uri=this.uri();var protocols=void(0);var opts={agent:this.agent};this.ws=new WebSocket(uri,protocols,opts);if(this.ws.binaryType===undefined){this.supportsBinary=false;} -this.ws.binaryType='arraybuffer';this.addEventListeners();};WS.prototype.addEventListeners=function(){var self=this;this.ws.onopen=function(){self.onOpen();};this.ws.onclose=function(){self.onClose();};this.ws.onmessage=function(ev){self.onData(ev.data);};this.ws.onerror=function(e){self.onError('websocket error',e);};};if('undefined'!=typeof navigator&&/iPad|iPhone|iPod/i.test(navigator.userAgent)){WS.prototype.onData=function(data){var self=this;setTimeout(function(){Transport.prototype.onData.call(self,data);},0);};} -WS.prototype.write=function(packets){var self=this;this.writable=false;for(var i=0,l=packets.length;i1){return{type:packetslist[type],data:data.substring(1)};}else{return{type:packetslist[type]};}} -var asArray=new Uint8Array(data);var type=asArray[0];var rest=sliceBuffer(data,1);if(Blob&&binaryType==='blob'){rest=new Blob([rest]);} -return{type:packetslist[type],data:rest};};exports.decodeBase64Packet=function(msg,binaryType){var type=packetslist[msg.charAt(0)];if(!global.ArrayBuffer){return{type:type,data:{base64:true,data:msg.substr(1)}};} -var data=base64encoder.decode(msg.substr(1));if(binaryType==='blob'&&Blob){data=new Blob([data]);} -return{type:type,data:data};};exports.encodePayload=function(packets,supportsBinary,callback){if(typeof supportsBinary=='function'){callback=supportsBinary;supportsBinary=null;} -if(supportsBinary){if(Blob&&!isAndroid){return exports.encodePayloadAsBlob(packets,callback);} -return exports.encodePayloadAsArrayBuffer(packets,callback);} -if(!packets.length){return callback('0:');} -function setLengthHeader(message){return message.length+':'+message;} -function encodeOne(packet,doneCallback){exports.encodePacket(packet,supportsBinary,true,function(message){doneCallback(null,setLengthHeader(message));});} -map(packets,encodeOne,function(err,results){return callback(results.join(''));});};function map(ary,each,done){var result=new Array(ary.length);var next=after(ary.length,done);var eachWithIndex=function(i,el,cb){each(el,function(error,msg){result[i]=msg;cb(error,result);});};for(var i=0;i0){var tailArray=new Uint8Array(bufferTail);var isString=tailArray[0]===0;var msgLength='';for(var i=1;;i++){if(tailArray[i]==255)break;if(msgLength.length>310){numberTooLong=true;break;} -msgLength+=tailArray[i];} -if(numberTooLong)return callback(err,0,1);bufferTail=sliceBuffer(bufferTail,2+msgLength.length);msgLength=parseInt(msgLength);var msg=sliceBuffer(bufferTail,0,msgLength);if(isString){try{msg=String.fromCharCode.apply(null,new Uint8Array(msg));}catch(e){var typed=new Uint8Array(msg);msg='';for(var i=0;ibytes){end=bytes;} -if(start>=bytes||start>=end||bytes===0){return new ArrayBuffer(0);} -var abv=new Uint8Array(arraybuffer);var result=new Uint8Array(end-start);for(var i=start,ii=0;i>2];base64+=chars[((bytes[i]&3)<<4)|(bytes[i+1]>>4)];base64+=chars[((bytes[i+1]&15)<<2)|(bytes[i+2]>>6)];base64+=chars[bytes[i+2]&63];} -if((len%3)===2){base64=base64.substring(0,base64.length-1)+"=";}else if(len%3===1){base64=base64.substring(0,base64.length-2)+"==";} -return base64;};exports.decode=function(base64){var bufferLength=base64.length*0.75,len=base64.length,i,p=0,encoded1,encoded2,encoded3,encoded4;if(base64[base64.length-1]==="="){bufferLength--;if(base64[base64.length-2]==="="){bufferLength--;}} -var arraybuffer=new ArrayBuffer(bufferLength),bytes=new Uint8Array(arraybuffer);for(i=0;i>4);bytes[p++]=((encoded2&15)<<4)|(encoded3>>2);bytes[p++]=((encoded3&3)<<6)|(encoded4&63);} -return arraybuffer;};})("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/");},{}],26:[function(_dereq_,module,exports){(function(global){var BlobBuilder=global.BlobBuilder||global.WebKitBlobBuilder||global.MSBlobBuilder||global.MozBlobBuilder;var blobSupported=(function(){try{var b=new Blob(['hi']);return b.size==2;}catch(e){return false;}})();var blobBuilderSupported=BlobBuilder&&BlobBuilder.prototype.append&&BlobBuilder.prototype.getBlob;function BlobBuilderConstructor(ary,options){options=options||{};var bb=new BlobBuilder();for(var i=0;i=0xD800&&value<=0xDBFF&&counter0xFFFF){value-=0x10000;output+=stringFromCharCode(value>>>10&0x3FF|0xD800);value=0xDC00|value&0x3FF;} -output+=stringFromCharCode(value);} -return output;} -function createByte(codePoint,shift){return stringFromCharCode(((codePoint>>shift)&0x3F)|0x80);} -function encodeCodePoint(codePoint){if((codePoint&0xFFFFFF80)==0){return stringFromCharCode(codePoint);} -var symbol='';if((codePoint&0xFFFFF800)==0){symbol=stringFromCharCode(((codePoint>>6)&0x1F)|0xC0);} -else if((codePoint&0xFFFF0000)==0){symbol=stringFromCharCode(((codePoint>>12)&0x0F)|0xE0);symbol+=createByte(codePoint,6);} -else if((codePoint&0xFFE00000)==0){symbol=stringFromCharCode(((codePoint>>18)&0x07)|0xF0);symbol+=createByte(codePoint,12);symbol+=createByte(codePoint,6);} -symbol+=stringFromCharCode((codePoint&0x3F)|0x80);return symbol;} -function utf8encode(string){var codePoints=ucs2decode(string);var length=codePoints.length;var index=-1;var codePoint;var byteString='';while(++index=byteCount){throw Error('Invalid byte index');} -var continuationByte=byteArray[byteIndex]&0xFF;byteIndex++;if((continuationByte&0xC0)==0x80){return continuationByte&0x3F;} -throw Error('Invalid continuation byte');} -function decodeSymbol(){var byte1;var byte2;var byte3;var byte4;var codePoint;if(byteIndex>byteCount){throw Error('Invalid byte index');} -if(byteIndex==byteCount){return false;} -byte1=byteArray[byteIndex]&0xFF;byteIndex++;if((byte1&0x80)==0){return byte1;} -if((byte1&0xE0)==0xC0){var byte2=readContinuationByte();codePoint=((byte1&0x1F)<<6)|byte2;if(codePoint>=0x80){return codePoint;}else{throw Error('Invalid continuation byte');}} -if((byte1&0xF0)==0xE0){byte2=readContinuationByte();byte3=readContinuationByte();codePoint=((byte1&0x0F)<<12)|(byte2<<6)|byte3;if(codePoint>=0x0800){return codePoint;}else{throw Error('Invalid continuation byte');}} -if((byte1&0xF8)==0xF0){byte2=readContinuationByte();byte3=readContinuationByte();byte4=readContinuationByte();codePoint=((byte1&0x0F)<<0x12)|(byte2<<0x0C)|(byte3<<0x06)|byte4;if(codePoint>=0x010000&&codePoint<=0x10FFFF){return codePoint;}} -throw Error('Invalid UTF-8 detected');} -var byteArray;var byteCount;var byteIndex;function utf8decode(byteString){byteArray=ucs2decode(byteString);byteCount=byteArray.length;byteIndex=0;var codePoints=[];var tmp;while((tmp=decodeSymbol())!==false){codePoints.push(tmp);} -return ucs2encode(codePoints);} -var utf8={'version':'2.0.0','encode':utf8encode,'decode':utf8decode};if(typeof define=='function'&&typeof define.amd=='object'&&define.amd){define(function(){return utf8;});}else if(freeExports&&!freeExports.nodeType){if(freeModule){freeModule.exports=utf8;}else{var object={};var hasOwnProperty=object.hasOwnProperty;for(var key in utf8){hasOwnProperty.call(utf8,key)&&(freeExports[key]=utf8[key]);}}}else{root.utf8=utf8;}}(this));}).call(this,typeof self!=="undefined"?self:typeof window!=="undefined"?window:{})},{}],28:[function(_dereq_,module,exports){(function(global){var rvalidchars=/^[\],:{}\s]*$/;var rvalidescape=/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g;var rvalidtokens=/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g;var rvalidbraces=/(?:^|:|,)(?:\s*\[)+/g;var rtrimLeft=/^\s+/;var rtrimRight=/\s+$/;module.exports=function parsejson(data){if('string'!=typeof data||!data){return null;} -data=data.replace(rtrimLeft,'').replace(rtrimRight,'');if(global.JSON&&JSON.parse){return JSON.parse(data);} -if(rvalidchars.test(data.replace(rvalidescape,'@').replace(rvalidtokens,']').replace(rvalidbraces,''))){return(new Function('return '+data))();}};}).call(this,typeof self!=="undefined"?self:typeof window!=="undefined"?window:{})},{}],29:[function(_dereq_,module,exports){exports.encode=function(obj){var str='';for(var i in obj){if(obj.hasOwnProperty(i)){if(str.length)str+='&';str+=encodeURIComponent(i)+'='+encodeURIComponent(obj[i]);}} -return str;};exports.decode=function(qs){var qry={};var pairs=qs.split('&');for(var i=0,l=pairs.length;i1)))/4)-floor((year-1901+month)/100)+floor((year-1601+month)/400);};} -if(!(isProperty={}.hasOwnProperty)){isProperty=function(property){var members={},constructor;if((members.__proto__=null,members.__proto__={"toString":1},members).toString!=getClass){isProperty=function(property){var original=this.__proto__,result=property in(this.__proto__=null,this);this.__proto__=original;return result;};}else{constructor=members.constructor;isProperty=function(property){var parent=(this.constructor||constructor).prototype;return property in this&&!(property in parent&&this[property]===parent[property]);};} -members=null;return isProperty.call(this,property);};} -var PrimitiveTypes={'boolean':1,'number':1,'string':1,'undefined':1};var isHostType=function(object,property){var type=typeof object[property];return type=='object'?!!object[property]:!PrimitiveTypes[type];};forEach=function(object,callback){var size=0,Properties,members,property;(Properties=function(){this.valueOf=0;}).prototype.valueOf=0;members=new Properties();for(property in members){if(isProperty.call(members,property)){size++;}} -Properties=members=null;if(!size){members=["valueOf","toString","toLocaleString","propertyIsEnumerable","isPrototypeOf","hasOwnProperty","constructor"];forEach=function(object,callback){var isFunction=getClass.call(object)==functionClass,property,length;var hasProperty=!isFunction&&typeof object.constructor!='function'&&isHostType(object,'hasOwnProperty')?object.hasOwnProperty:isProperty;for(property in object){if(!(isFunction&&property=="prototype")&&hasProperty.call(object,property)){callback(property);}} -for(length=members.length;property=members[--length];hasProperty.call(object,property)&&callback(property));};}else if(size==2){forEach=function(object,callback){var members={},isFunction=getClass.call(object)==functionClass,property;for(property in object){if(!(isFunction&&property=="prototype")&&!isProperty.call(members,property)&&(members[property]=1)&&isProperty.call(object,property)){callback(property);}}};}else{forEach=function(object,callback){var isFunction=getClass.call(object)==functionClass,property,isConstructor;for(property in object){if(!(isFunction&&property=="prototype")&&isProperty.call(object,property)&&!(isConstructor=property==="constructor")){callback(property);}} -if(isConstructor||isProperty.call(object,(property="constructor"))){callback(property);}};} -return forEach(object,callback);};if(!has("json-stringify")){var Escapes={92:"\\\\",34:'\\"',8:"\\b",12:"\\f",10:"\\n",13:"\\r",9:"\\t"};var leadingZeroes="000000";var toPaddedString=function(width,value){return(leadingZeroes+(value||0)).slice(-width);};var unicodePrefix="\\u00";var quote=function(value){var result='"',index=0,length=value.length,isLarge=length>10&&charIndexBuggy,symbols;if(isLarge){symbols=value.split("");} -for(;index-1/0&&value<1/0){if(getDay){date=floor(value/864e5);for(year=floor(date/365.2425)+1970-1;getDay(year+1,0)<=date;year++);for(month=floor((date-getDay(year,0))/30.42);getDay(year,month+1)<=date;month++);date=1+date-getDay(year,month);time=(value%864e5+864e5)%864e5;hours=floor(time/36e5)%24;minutes=floor(time/6e4)%60;seconds=floor(time/1e3)%60;milliseconds=time%1e3;}else{year=value.getUTCFullYear();month=value.getUTCMonth();date=value.getUTCDate();hours=value.getUTCHours();minutes=value.getUTCMinutes();seconds=value.getUTCSeconds();milliseconds=value.getUTCMilliseconds();} -value=(year<=0||year>=1e4?(year<0?"-":"+")+toPaddedString(6,year<0?-year:year):toPaddedString(4,year))+"-"+toPaddedString(2,month+1)+"-"+toPaddedString(2,date)+"T"+toPaddedString(2,hours)+":"+toPaddedString(2,minutes)+":"+toPaddedString(2,seconds)+"."+toPaddedString(3,milliseconds)+"Z";}else{value=null;}}else if(typeof value.toJSON=="function"&&((className!=numberClass&&className!=stringClass&&className!=arrayClass)||isProperty.call(value,"toJSON"))){value=value.toJSON(property);}} -if(callback){value=callback.call(object,property,value);} -if(value===null){return"null";} -className=getClass.call(value);if(className==booleanClass){return""+value;}else if(className==numberClass){return value>-1/0&&value<1/0?""+value:"null";}else if(className==stringClass){return quote(""+value);} -if(typeof value=="object"){for(length=stack.length;length--;){if(stack[length]===value){throw TypeError();}} -stack.push(value);results=[];prefix=indentation;indentation+=whitespace;if(className==arrayClass){for(index=0,length=value.length;index0){for(whitespace="",width>10&&(width=10);whitespace.length=48&&charCode<=57||charCode>=97&&charCode<=102||charCode>=65&&charCode<=70)){abort();}} -value+=fromCharCode("0x"+source.slice(begin,Index));break;default:abort();}}else{if(charCode==34){break;} -charCode=source.charCodeAt(Index);begin=Index;while(charCode>=32&&charCode!=92&&charCode!=34){charCode=source.charCodeAt(++Index);} -value+=source.slice(begin,Index);}} -if(source.charCodeAt(Index)==34){Index++;return value;} -abort();default:begin=Index;if(charCode==45){isSigned=true;charCode=source.charCodeAt(++Index);} -if(charCode>=48&&charCode<=57){if(charCode==48&&((charCode=source.charCodeAt(Index+1)),charCode>=48&&charCode<=57)){abort();} -isSigned=false;for(;Index=48&&charCode<=57);Index++);if(source.charCodeAt(Index)==46){position=++Index;for(;position=48&&charCode<=57);position++);if(position==Index){abort();} -Index=position;} -charCode=source.charCodeAt(Index);if(charCode==101||charCode==69){charCode=source.charCodeAt(++Index);if(charCode==43||charCode==45){Index++;} -for(position=Index;position=48&&charCode<=57);position++);if(position==Index){abort();} -Index=position;} -return+source.slice(begin,Index);} -if(isSigned){abort();} -if(source.slice(Index,Index+4)=="true"){Index+=4;return true;}else if(source.slice(Index,Index+5)=="false"){Index+=5;return false;}else if(source.slice(Index,Index+4)=="null"){Index+=4;return null;} -abort();}} -return"$";};var get=function(value){var results,hasMembers;if(value=="$"){abort();} -if(typeof value=="string"){if((charIndexBuggy?value.charAt(0):value[0])=="@"){return value.slice(1);} -if(value=="["){results=[];for(;;hasMembers||(hasMembers=true)){value=lex();if(value=="]"){break;} -if(hasMembers){if(value==","){value=lex();if(value=="]"){abort();}}else{abort();}} -if(value==","){abort();} -results.push(get(value));} -return results;}else if(value=="{"){results={};for(;;hasMembers||(hasMembers=true)){value=lex();if(value=="}"){break;} -if(hasMembers){if(value==","){value=lex();if(value=="}"){abort();}}else{abort();}} -if(value==","||typeof value!="string"||(charIndexBuggy?value.charAt(0):value[0])!="@"||lex()!=":"){abort();} -results[value.slice(1)]=get(lex());} -return results;} -abort();} -return value;};var update=function(source,property,callback){var element=walk(source,property,callback);if(element===undef){delete source[property];}else{source[property]=element;}};var walk=function(source,property,callback){var value=source[property],length;if(typeof value=="object"&&value){if(getClass.call(value)==arrayClass){for(length=value.length;length--;){update(value,length,callback);}}else{forEach(value,function(property){update(value,property,callback);});}} -return callback.call(source,property,value);};JSON3.parse=function(source,callback){var result,value;Index=0;Source=""+source;result=get(lex());if(lex()!="$"){abort();} -Index=Source=null;return callback&&getClass.call(callback)==functionClass?walk((value={},value[""]=result,value),"",callback):result;};}} -if(isLoader){define(function(){return JSON3;});}}(this));},{}],44:[function(_dereq_,module,exports){module.exports=toArray -function toArray(list,index){var array=[] -index=index||0 -for(var i=index||0;i 0 && !this.encoding) { + var pack = this.packetBuffer.shift(); + this.packet(pack); + } +}; + +/** + * Clean up transport subscriptions and packet buffer. + * + * @api private + */ + +Manager.prototype.cleanup = function(){ + var sub; + while (sub = this.subs.shift()) sub.destroy(); + + this.packetBuffer = []; + this.encoding = false; + + this.decoder.destroy(); +}; + +/** + * Close the current socket. + * + * @api private + */ + +Manager.prototype.close = +Manager.prototype.disconnect = function(){ + this.skipReconnect = true; + this.backoff.reset(); + this.readyState = 'closed'; + this.engine && this.engine.close(); +}; + +/** + * Called upon engine close. + * + * @api private + */ + +Manager.prototype.onclose = function(reason){ + debug('close'); + this.cleanup(); + this.backoff.reset(); + this.readyState = 'closed'; + this.emit('close', reason); + if (this._reconnection && !this.skipReconnect) { + this.reconnect(); + } +}; + +/** + * Attempt a reconnection. + * + * @api private + */ + +Manager.prototype.reconnect = function(){ + if (this.reconnecting || this.skipReconnect) return this; + + var self = this; + + if (this.backoff.attempts >= this._reconnectionAttempts) { + debug('reconnect failed'); + this.backoff.reset(); + this.emitAll('reconnect_failed'); + this.reconnecting = false; + } else { + var delay = this.backoff.duration(); + debug('will wait %dms before reconnect attempt', delay); + + this.reconnecting = true; + var timer = setTimeout(function(){ + if (self.skipReconnect) return; + + debug('attempting reconnect'); + self.emitAll('reconnect_attempt', self.backoff.attempts); + self.emitAll('reconnecting', self.backoff.attempts); + + // check again for the case socket closed in above events + if (self.skipReconnect) return; + + self.open(function(err){ + if (err) { + debug('reconnect attempt error'); + self.reconnecting = false; + self.reconnect(); + self.emitAll('reconnect_error', err.data); + } else { + debug('reconnect success'); + self.onreconnect(); + } + }); + }, delay); + + this.subs.push({ + destroy: function(){ + clearTimeout(timer); + } + }); + } +}; + +/** + * Called upon successful reconnect. + * + * @api private + */ + +Manager.prototype.onreconnect = function(){ + var attempt = this.backoff.attempts; + this.reconnecting = false; + this.backoff.reset(); + this.updateSocketIds(); + this.emitAll('reconnect', attempt); +}; + +},{"./on":4,"./socket":5,"./url":6,"backo2":7,"component-bind":8,"component-emitter":9,"debug":10,"engine.io-client":11,"indexof":40,"object-component":41,"socket.io-parser":44}],4:[function(_dereq_,module,exports){ + +/** + * Module exports. + */ + +module.exports = on; + +/** + * Helper for subscriptions. + * + * @param {Object|EventEmitter} obj with `Emitter` mixin or `EventEmitter` + * @param {String} event name + * @param {Function} callback + * @api public + */ + +function on(obj, ev, fn) { + obj.on(ev, fn); + return { + destroy: function(){ + obj.removeListener(ev, fn); + } + }; +} + +},{}],5:[function(_dereq_,module,exports){ + +/** + * Module dependencies. + */ + +var parser = _dereq_('socket.io-parser'); +var Emitter = _dereq_('component-emitter'); +var toArray = _dereq_('to-array'); +var on = _dereq_('./on'); +var bind = _dereq_('component-bind'); +var debug = _dereq_('debug')('socket.io-client:socket'); +var hasBin = _dereq_('has-binary'); + +/** + * Module exports. + */ + +module.exports = exports = Socket; + +/** + * Internal events (blacklisted). + * These events can't be emitted by the user. + * + * @api private + */ + +var events = { + connect: 1, + connect_error: 1, + connect_timeout: 1, + disconnect: 1, + error: 1, + reconnect: 1, + reconnect_attempt: 1, + reconnect_failed: 1, + reconnect_error: 1, + reconnecting: 1 +}; + +/** + * Shortcut to `Emitter#emit`. + */ + +var emit = Emitter.prototype.emit; + +/** + * `Socket` constructor. + * + * @api public + */ + +function Socket(io, nsp){ + this.io = io; + this.nsp = nsp; + this.json = this; // compat + this.ids = 0; + this.acks = {}; + if (this.io.autoConnect) this.open(); + this.receiveBuffer = []; + this.sendBuffer = []; + this.connected = false; + this.disconnected = true; +} + +/** + * Mix in `Emitter`. + */ + +Emitter(Socket.prototype); + +/** + * Subscribe to open, close and packet events + * + * @api private + */ + +Socket.prototype.subEvents = function() { + if (this.subs) return; + + var io = this.io; + this.subs = [ + on(io, 'open', bind(this, 'onopen')), + on(io, 'packet', bind(this, 'onpacket')), + on(io, 'close', bind(this, 'onclose')) + ]; +}; + +/** + * "Opens" the socket. + * + * @api public + */ + +Socket.prototype.open = +Socket.prototype.connect = function(){ + if (this.connected) return this; + + this.subEvents(); + this.io.open(); // ensure open + if ('open' == this.io.readyState) this.onopen(); + return this; +}; + +/** + * Sends a `message` event. + * + * @return {Socket} self + * @api public + */ + +Socket.prototype.send = function(){ + var args = toArray(arguments); + args.unshift('message'); + this.emit.apply(this, args); + return this; +}; + +/** + * Override `emit`. + * If the event is in `events`, it's emitted normally. + * + * @param {String} event name + * @return {Socket} self + * @api public + */ + +Socket.prototype.emit = function(ev){ + if (events.hasOwnProperty(ev)) { + emit.apply(this, arguments); + return this; + } + + var args = toArray(arguments); + var parserType = parser.EVENT; // default + if (hasBin(args)) { parserType = parser.BINARY_EVENT; } // binary + var packet = { type: parserType, data: args }; + + // event ack callback + if ('function' == typeof args[args.length - 1]) { + debug('emitting packet with ack id %d', this.ids); + this.acks[this.ids] = args.pop(); + packet.id = this.ids++; + } + + if (this.connected) { + this.packet(packet); + } else { + this.sendBuffer.push(packet); + } + + return this; +}; + +/** + * Sends a packet. + * + * @param {Object} packet + * @api private + */ + +Socket.prototype.packet = function(packet){ + packet.nsp = this.nsp; + this.io.packet(packet); +}; + +/** + * Called upon engine `open`. + * + * @api private + */ + +Socket.prototype.onopen = function(){ + debug('transport is open - connecting'); + + // write connect packet if necessary + if ('/' != this.nsp) { + this.packet({ type: parser.CONNECT }); + } +}; + +/** + * Called upon engine `close`. + * + * @param {String} reason + * @api private + */ + +Socket.prototype.onclose = function(reason){ + debug('close (%s)', reason); + this.connected = false; + this.disconnected = true; + delete this.id; + this.emit('disconnect', reason); +}; + +/** + * Called with socket packet. + * + * @param {Object} packet + * @api private + */ + +Socket.prototype.onpacket = function(packet){ + if (packet.nsp != this.nsp) return; + + switch (packet.type) { + case parser.CONNECT: + this.onconnect(); + break; + + case parser.EVENT: + this.onevent(packet); + break; + + case parser.BINARY_EVENT: + this.onevent(packet); + break; + + case parser.ACK: + this.onack(packet); + break; + + case parser.BINARY_ACK: + this.onack(packet); + break; + + case parser.DISCONNECT: + this.ondisconnect(); + break; + + case parser.ERROR: + this.emit('error', packet.data); + break; + } +}; + +/** + * Called upon a server event. + * + * @param {Object} packet + * @api private + */ + +Socket.prototype.onevent = function(packet){ + var args = packet.data || []; + debug('emitting event %j', args); + + if (null != packet.id) { + debug('attaching ack callback to event'); + args.push(this.ack(packet.id)); + } + + if (this.connected) { + emit.apply(this, args); + } else { + this.receiveBuffer.push(args); + } +}; + +/** + * Produces an ack callback to emit with an event. + * + * @api private + */ + +Socket.prototype.ack = function(id){ + var self = this; + var sent = false; + return function(){ + // prevent double callbacks + if (sent) return; + sent = true; + var args = toArray(arguments); + debug('sending ack %j', args); + + var type = hasBin(args) ? parser.BINARY_ACK : parser.ACK; + self.packet({ + type: type, + id: id, + data: args + }); + }; +}; + +/** + * Called upon a server acknowlegement. + * + * @param {Object} packet + * @api private + */ + +Socket.prototype.onack = function(packet){ + debug('calling ack %s with %j', packet.id, packet.data); + var fn = this.acks[packet.id]; + fn.apply(this, packet.data); + delete this.acks[packet.id]; +}; + +/** + * Called upon server connect. + * + * @api private + */ + +Socket.prototype.onconnect = function(){ + this.connected = true; + this.disconnected = false; + this.emit('connect'); + this.emitBuffered(); +}; + +/** + * Emit buffered events (received and emitted). + * + * @api private + */ + +Socket.prototype.emitBuffered = function(){ + var i; + for (i = 0; i < this.receiveBuffer.length; i++) { + emit.apply(this, this.receiveBuffer[i]); + } + this.receiveBuffer = []; + + for (i = 0; i < this.sendBuffer.length; i++) { + this.packet(this.sendBuffer[i]); + } + this.sendBuffer = []; +}; + +/** + * Called upon server disconnect. + * + * @api private + */ + +Socket.prototype.ondisconnect = function(){ + debug('server disconnect (%s)', this.nsp); + this.destroy(); + this.onclose('io server disconnect'); +}; + +/** + * Called upon forced client/server side disconnections, + * this method ensures the manager stops tracking us and + * that reconnections don't get triggered for this. + * + * @api private. + */ + +Socket.prototype.destroy = function(){ + if (this.subs) { + // clean subscriptions to avoid reconnections + for (var i = 0; i < this.subs.length; i++) { + this.subs[i].destroy(); + } + this.subs = null; + } + + this.io.destroy(this); +}; + +/** + * Disconnects the socket manually. + * + * @return {Socket} self + * @api public + */ + +Socket.prototype.close = +Socket.prototype.disconnect = function(){ + if (this.connected) { + debug('performing disconnect (%s)', this.nsp); + this.packet({ type: parser.DISCONNECT }); + } + + // remove socket from pool + this.destroy(); + + if (this.connected) { + // fire events + this.onclose('io client disconnect'); + } + return this; +}; + +},{"./on":4,"component-bind":8,"component-emitter":9,"debug":10,"has-binary":36,"socket.io-parser":44,"to-array":48}],6:[function(_dereq_,module,exports){ +(function (global){ + +/** + * Module dependencies. + */ + +var parseuri = _dereq_('parseuri'); +var debug = _dereq_('debug')('socket.io-client:url'); + +/** + * Module exports. + */ + +module.exports = url; + +/** + * URL parser. + * + * @param {String} url + * @param {Object} An object meant to mimic window.location. + * Defaults to window.location. + * @api public + */ + +function url(uri, loc){ + var obj = uri; + + // default to window.location + var loc = loc || global.location; + if (null == uri) uri = loc.protocol + '//' + loc.host; + + // relative path support + if ('string' == typeof uri) { + if ('/' == uri.charAt(0)) { + if ('/' == uri.charAt(1)) { + uri = loc.protocol + uri; + } else { + uri = loc.hostname + uri; + } + } + + if (!/^(https?|wss?):\/\//.test(uri)) { + debug('protocol-less url %s', uri); + if ('undefined' != typeof loc) { + uri = loc.protocol + '//' + uri; + } else { + uri = 'https://' + uri; + } + } + + // parse + debug('parse %s', uri); + obj = parseuri(uri); + } + + // make sure we treat `localhost:80` and `localhost` equally + if (!obj.port) { + if (/^(http|ws)$/.test(obj.protocol)) { + obj.port = '80'; + } + else if (/^(http|ws)s$/.test(obj.protocol)) { + obj.port = '443'; + } + } + + obj.path = obj.path || '/'; + + // define unique id + obj.id = obj.protocol + '://' + obj.host + ':' + obj.port; + // define href + obj.href = obj.protocol + '://' + obj.host + (loc && loc.port == obj.port ? '' : (':' + obj.port)); + + return obj; +} + +}).call(this,typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) +},{"debug":10,"parseuri":42}],7:[function(_dereq_,module,exports){ + +/** + * Expose `Backoff`. + */ + +module.exports = Backoff; + +/** + * Initialize backoff timer with `opts`. + * + * - `min` initial timeout in milliseconds [100] + * - `max` max timeout [10000] + * - `jitter` [0] + * - `factor` [2] + * + * @param {Object} opts + * @api public + */ + +function Backoff(opts) { + opts = opts || {}; + this.ms = opts.min || 100; + this.max = opts.max || 10000; + this.factor = opts.factor || 2; + this.jitter = opts.jitter > 0 && opts.jitter <= 1 ? opts.jitter : 0; + this.attempts = 0; +} + +/** + * Return the backoff duration. + * + * @return {Number} + * @api public + */ + +Backoff.prototype.duration = function(){ + var ms = this.ms * Math.pow(this.factor, this.attempts++); + if (this.jitter) { + var rand = Math.random(); + var deviation = Math.floor(rand * this.jitter * ms); + ms = (Math.floor(rand * 10) & 1) == 0 ? ms - deviation : ms + deviation; + } + return Math.min(ms, this.max) | 0; +}; + +/** + * Reset the number of attempts. + * + * @api public + */ + +Backoff.prototype.reset = function(){ + this.attempts = 0; +}; + +/** + * Set the minimum duration + * + * @api public + */ + +Backoff.prototype.setMin = function(min){ + this.ms = min; +}; + +/** + * Set the maximum duration + * + * @api public + */ + +Backoff.prototype.setMax = function(max){ + this.max = max; +}; + +/** + * Set the jitter + * + * @api public + */ + +Backoff.prototype.setJitter = function(jitter){ + this.jitter = jitter; +}; + + +},{}],8:[function(_dereq_,module,exports){ +/** + * Slice reference. + */ + +var slice = [].slice; + +/** + * Bind `obj` to `fn`. + * + * @param {Object} obj + * @param {Function|String} fn or string + * @return {Function} + * @api public + */ + +module.exports = function(obj, fn){ + if ('string' == typeof fn) fn = obj[fn]; + if ('function' != typeof fn) throw new Error('bind() requires a function'); + var args = slice.call(arguments, 2); + return function(){ + return fn.apply(obj, args.concat(slice.call(arguments))); + } +}; + +},{}],9:[function(_dereq_,module,exports){ + +/** + * Expose `Emitter`. + */ + +module.exports = Emitter; + +/** + * Initialize a new `Emitter`. + * + * @api public + */ + +function Emitter(obj) { + if (obj) return mixin(obj); +}; + +/** + * Mixin the emitter properties. + * + * @param {Object} obj + * @return {Object} + * @api private + */ + +function mixin(obj) { + for (var key in Emitter.prototype) { + obj[key] = Emitter.prototype[key]; + } + return obj; +} + +/** + * Listen on the given `event` with `fn`. + * + * @param {String} event + * @param {Function} fn + * @return {Emitter} + * @api public + */ + +Emitter.prototype.on = +Emitter.prototype.addEventListener = function(event, fn){ + this._callbacks = this._callbacks || {}; + (this._callbacks[event] = this._callbacks[event] || []) + .push(fn); + return this; +}; + +/** + * Adds an `event` listener that will be invoked a single + * time then automatically removed. + * + * @param {String} event + * @param {Function} fn + * @return {Emitter} + * @api public + */ + +Emitter.prototype.once = function(event, fn){ + var self = this; + this._callbacks = this._callbacks || {}; + + function on() { + self.off(event, on); + fn.apply(this, arguments); + } + + on.fn = fn; + this.on(event, on); + return this; +}; + +/** + * Remove the given callback for `event` or all + * registered callbacks. + * + * @param {String} event + * @param {Function} fn + * @return {Emitter} + * @api public + */ + +Emitter.prototype.off = +Emitter.prototype.removeListener = +Emitter.prototype.removeAllListeners = +Emitter.prototype.removeEventListener = function(event, fn){ + this._callbacks = this._callbacks || {}; + + // all + if (0 == arguments.length) { + this._callbacks = {}; + return this; + } + + // specific event + var callbacks = this._callbacks[event]; + if (!callbacks) return this; + + // remove all handlers + if (1 == arguments.length) { + delete this._callbacks[event]; + return this; + } + + // remove specific handler + var cb; + for (var i = 0; i < callbacks.length; i++) { + cb = callbacks[i]; + if (cb === fn || cb.fn === fn) { + callbacks.splice(i, 1); + break; + } + } + return this; +}; + +/** + * Emit `event` with the given args. + * + * @param {String} event + * @param {Mixed} ... + * @return {Emitter} + */ + +Emitter.prototype.emit = function(event){ + this._callbacks = this._callbacks || {}; + var args = [].slice.call(arguments, 1) + , callbacks = this._callbacks[event]; + + if (callbacks) { + callbacks = callbacks.slice(0); + for (var i = 0, len = callbacks.length; i < len; ++i) { + callbacks[i].apply(this, args); + } + } + + return this; +}; + +/** + * Return array of callbacks for `event`. + * + * @param {String} event + * @return {Array} + * @api public + */ + +Emitter.prototype.listeners = function(event){ + this._callbacks = this._callbacks || {}; + return this._callbacks[event] || []; +}; + +/** + * Check if this emitter has `event` handlers. + * + * @param {String} event + * @return {Boolean} + * @api public + */ + +Emitter.prototype.hasListeners = function(event){ + return !! this.listeners(event).length; +}; + +},{}],10:[function(_dereq_,module,exports){ + +/** + * Expose `debug()` as the module. + */ + +module.exports = debug; + +/** + * Create a debugger with the given `name`. + * + * @param {String} name + * @return {Type} + * @api public + */ + +function debug(name) { + if (!debug.enabled(name)) return function(){}; + + return function(fmt){ + fmt = coerce(fmt); + + var curr = new Date; + var ms = curr - (debug[name] || curr); + debug[name] = curr; + + fmt = name + + ' ' + + fmt + + ' +' + debug.humanize(ms); + + // This hackery is required for IE8 + // where `console.log` doesn't have 'apply' + window.console + && console.log + && Function.prototype.apply.call(console.log, console, arguments); + } +} + +/** + * The currently active debug mode names. + */ + +debug.names = []; +debug.skips = []; + +/** + * Enables a debug mode by name. This can include modes + * separated by a colon and wildcards. + * + * @param {String} name + * @api public + */ + +debug.enable = function(name) { + try { + localStorage.debug = name; + } catch(e){} + + var split = (name || '').split(/[\s,]+/) + , len = split.length; + + for (var i = 0; i < len; i++) { + name = split[i].replace('*', '.*?'); + if (name[0] === '-') { + debug.skips.push(new RegExp('^' + name.substr(1) + '$')); + } + else { + debug.names.push(new RegExp('^' + name + '$')); + } + } +}; + +/** + * Disable debug output. + * + * @api public + */ + +debug.disable = function(){ + debug.enable(''); +}; + +/** + * Humanize the given `ms`. + * + * @param {Number} m + * @return {String} + * @api private + */ + +debug.humanize = function(ms) { + var sec = 1000 + , min = 60 * 1000 + , hour = 60 * min; + + if (ms >= hour) return (ms / hour).toFixed(1) + 'h'; + if (ms >= min) return (ms / min).toFixed(1) + 'm'; + if (ms >= sec) return (ms / sec | 0) + 's'; + return ms + 'ms'; +}; + +/** + * Returns true if the given mode name is enabled, false otherwise. + * + * @param {String} name + * @return {Boolean} + * @api public + */ + +debug.enabled = function(name) { + for (var i = 0, len = debug.skips.length; i < len; i++) { + if (debug.skips[i].test(name)) { + return false; + } + } + for (var i = 0, len = debug.names.length; i < len; i++) { + if (debug.names[i].test(name)) { + return true; + } + } + return false; +}; + +/** + * Coerce `val`. + */ + +function coerce(val) { + if (val instanceof Error) return val.stack || val.message; + return val; +} + +// persist + +try { + if (window.localStorage) debug.enable(localStorage.debug); +} catch(e){} + +},{}],11:[function(_dereq_,module,exports){ + +module.exports = _dereq_('./lib/'); + +},{"./lib/":12}],12:[function(_dereq_,module,exports){ + +module.exports = _dereq_('./socket'); + +/** + * Exports parser + * + * @api public + * + */ +module.exports.parser = _dereq_('engine.io-parser'); + +},{"./socket":13,"engine.io-parser":25}],13:[function(_dereq_,module,exports){ +(function (global){ +/** + * Module dependencies. + */ + +var transports = _dereq_('./transports'); +var Emitter = _dereq_('component-emitter'); +var debug = _dereq_('debug')('engine.io-client:socket'); +var index = _dereq_('indexof'); +var parser = _dereq_('engine.io-parser'); +var parseuri = _dereq_('parseuri'); +var parsejson = _dereq_('parsejson'); +var parseqs = _dereq_('parseqs'); + +/** + * Module exports. + */ + +module.exports = Socket; + +/** + * Noop function. + * + * @api private + */ + +function noop(){} + +/** + * Socket constructor. + * + * @param {String|Object} uri or options + * @param {Object} options + * @api public + */ + +function Socket(uri, opts){ + if (!(this instanceof Socket)) return new Socket(uri, opts); + + opts = opts || {}; + + if (uri && 'object' == typeof uri) { + opts = uri; + uri = null; + } + + if (uri) { + uri = parseuri(uri); + opts.host = uri.host; + opts.secure = uri.protocol == 'https' || uri.protocol == 'wss'; + opts.port = uri.port; + if (uri.query) opts.query = uri.query; + } + + this.secure = null != opts.secure ? opts.secure : + (global.location && 'https:' == location.protocol); + + if (opts.host) { + var pieces = opts.host.split(':'); + opts.hostname = pieces.shift(); + if (pieces.length) { + opts.port = pieces.pop(); + } else if (!opts.port) { + // if no port is specified manually, use the protocol default + opts.port = this.secure ? '443' : '80'; + } + } + + this.agent = opts.agent || false; + this.hostname = opts.hostname || + (global.location ? location.hostname : 'localhost'); + this.port = opts.port || (global.location && location.port ? + location.port : + (this.secure ? 443 : 80)); + this.query = opts.query || {}; + if ('string' == typeof this.query) this.query = parseqs.decode(this.query); + this.upgrade = false !== opts.upgrade; + this.path = (opts.path || '/engine.io').replace(/\/$/, '') + '/'; + this.forceJSONP = !!opts.forceJSONP; + this.jsonp = false !== opts.jsonp; + this.forceBase64 = !!opts.forceBase64; + this.enablesXDR = !!opts.enablesXDR; + this.timestampParam = opts.timestampParam || 't'; + this.timestampRequests = opts.timestampRequests; + this.transports = opts.transports || ['polling', 'websocket']; + this.readyState = ''; + this.writeBuffer = []; + this.callbackBuffer = []; + this.policyPort = opts.policyPort || 843; + this.rememberUpgrade = opts.rememberUpgrade || false; + this.binaryType = null; + this.onlyBinaryUpgrades = opts.onlyBinaryUpgrades; + + // SSL options for Node.js client + this.pfx = opts.pfx || null; + this.key = opts.key || null; + this.passphrase = opts.passphrase || null; + this.cert = opts.cert || null; + this.ca = opts.ca || null; + this.ciphers = opts.ciphers || null; + this.rejectUnauthorized = opts.rejectUnauthorized || null; + + this.open(); +} + +Socket.priorWebsocketSuccess = false; + +/** + * Mix in `Emitter`. + */ + +Emitter(Socket.prototype); + +/** + * Protocol version. + * + * @api public + */ + +Socket.protocol = parser.protocol; // this is an int + +/** + * Expose deps for legacy compatibility + * and standalone browser access. + */ + +Socket.Socket = Socket; +Socket.Transport = _dereq_('./transport'); +Socket.transports = _dereq_('./transports'); +Socket.parser = _dereq_('engine.io-parser'); + +/** + * Creates transport of the given type. + * + * @param {String} transport name + * @return {Transport} + * @api private + */ + +Socket.prototype.createTransport = function (name) { + debug('creating transport "%s"', name); + var query = clone(this.query); + + // append engine.io protocol identifier + query.EIO = parser.protocol; + + // transport name + query.transport = name; + + // session id if we already have one + if (this.id) query.sid = this.id; + + var transport = new transports[name]({ + agent: this.agent, + hostname: this.hostname, + port: this.port, + secure: this.secure, + path: this.path, + query: query, + forceJSONP: this.forceJSONP, + jsonp: this.jsonp, + forceBase64: this.forceBase64, + enablesXDR: this.enablesXDR, + timestampRequests: this.timestampRequests, + timestampParam: this.timestampParam, + policyPort: this.policyPort, + socket: this, + pfx: this.pfx, + key: this.key, + passphrase: this.passphrase, + cert: this.cert, + ca: this.ca, + ciphers: this.ciphers, + rejectUnauthorized: this.rejectUnauthorized + }); + + return transport; +}; + +function clone (obj) { + var o = {}; + for (var i in obj) { + if (obj.hasOwnProperty(i)) { + o[i] = obj[i]; + } + } + return o; +} + +/** + * Initializes transport to use and starts probe. + * + * @api private + */ +Socket.prototype.open = function () { + var transport; + if (this.rememberUpgrade && Socket.priorWebsocketSuccess && this.transports.indexOf('websocket') != -1) { + transport = 'websocket'; + } else if (0 == this.transports.length) { + // Emit error on next tick so it can be listened to + var self = this; + setTimeout(function() { + self.emit('error', 'No transports available'); + }, 0); + return; + } else { + transport = this.transports[0]; + } + this.readyState = 'opening'; + + // Retry with the next transport if the transport is disabled (jsonp: false) + var transport; + try { + transport = this.createTransport(transport); + } catch (e) { + this.transports.shift(); + this.open(); + return; + } + + transport.open(); + this.setTransport(transport); +}; + +/** + * Sets the current transport. Disables the existing one (if any). + * + * @api private + */ + +Socket.prototype.setTransport = function(transport){ + debug('setting transport %s', transport.name); + var self = this; + + if (this.transport) { + debug('clearing existing transport %s', this.transport.name); + this.transport.removeAllListeners(); + } + + // set up transport + this.transport = transport; + + // set up transport listeners + transport + .on('drain', function(){ + self.onDrain(); + }) + .on('packet', function(packet){ + self.onPacket(packet); + }) + .on('error', function(e){ + self.onError(e); + }) + .on('close', function(){ + self.onClose('transport close'); + }); +}; + +/** + * Probes a transport. + * + * @param {String} transport name + * @api private + */ + +Socket.prototype.probe = function (name) { + debug('probing transport "%s"', name); + var transport = this.createTransport(name, { probe: 1 }) + , failed = false + , self = this; + + Socket.priorWebsocketSuccess = false; + + function onTransportOpen(){ + if (self.onlyBinaryUpgrades) { + var upgradeLosesBinary = !this.supportsBinary && self.transport.supportsBinary; + failed = failed || upgradeLosesBinary; + } + if (failed) return; + + debug('probe transport "%s" opened', name); + transport.send([{ type: 'ping', data: 'probe' }]); + transport.once('packet', function (msg) { + if (failed) return; + if ('pong' == msg.type && 'probe' == msg.data) { + debug('probe transport "%s" pong', name); + self.upgrading = true; + self.emit('upgrading', transport); + if (!transport) return; + Socket.priorWebsocketSuccess = 'websocket' == transport.name; + + debug('pausing current transport "%s"', self.transport.name); + self.transport.pause(function () { + if (failed) return; + if ('closed' == self.readyState) return; + debug('changing transport and sending upgrade packet'); + + cleanup(); + + self.setTransport(transport); + transport.send([{ type: 'upgrade' }]); + self.emit('upgrade', transport); + transport = null; + self.upgrading = false; + self.flush(); + }); + } else { + debug('probe transport "%s" failed', name); + var err = new Error('probe error'); + err.transport = transport.name; + self.emit('upgradeError', err); + } + }); + } + + function freezeTransport() { + if (failed) return; + + // Any callback called by transport should be ignored since now + failed = true; + + cleanup(); + + transport.close(); + transport = null; + } + + //Handle any error that happens while probing + function onerror(err) { + var error = new Error('probe error: ' + err); + error.transport = transport.name; + + freezeTransport(); + + debug('probe transport "%s" failed because of error: %s', name, err); + + self.emit('upgradeError', error); + } + + function onTransportClose(){ + onerror("transport closed"); + } + + //When the socket is closed while we're probing + function onclose(){ + onerror("socket closed"); + } + + //When the socket is upgraded while we're probing + function onupgrade(to){ + if (transport && to.name != transport.name) { + debug('"%s" works - aborting "%s"', to.name, transport.name); + freezeTransport(); + } + } + + //Remove all listeners on the transport and on self + function cleanup(){ + transport.removeListener('open', onTransportOpen); + transport.removeListener('error', onerror); + transport.removeListener('close', onTransportClose); + self.removeListener('close', onclose); + self.removeListener('upgrading', onupgrade); + } + + transport.once('open', onTransportOpen); + transport.once('error', onerror); + transport.once('close', onTransportClose); + + this.once('close', onclose); + this.once('upgrading', onupgrade); + + transport.open(); + +}; + +/** + * Called when connection is deemed open. + * + * @api public + */ + +Socket.prototype.onOpen = function () { + debug('socket open'); + this.readyState = 'open'; + Socket.priorWebsocketSuccess = 'websocket' == this.transport.name; + this.emit('open'); + this.flush(); + + // we check for `readyState` in case an `open` + // listener already closed the socket + if ('open' == this.readyState && this.upgrade && this.transport.pause) { + debug('starting upgrade probes'); + for (var i = 0, l = this.upgrades.length; i < l; i++) { + this.probe(this.upgrades[i]); + } + } +}; + +/** + * Handles a packet. + * + * @api private + */ + +Socket.prototype.onPacket = function (packet) { + if ('opening' == this.readyState || 'open' == this.readyState) { + debug('socket receive: type "%s", data "%s"', packet.type, packet.data); + + this.emit('packet', packet); + + // Socket is live - any packet counts + this.emit('heartbeat'); + + switch (packet.type) { + case 'open': + this.onHandshake(parsejson(packet.data)); + break; + + case 'pong': + this.setPing(); + break; + + case 'error': + var err = new Error('server error'); + err.code = packet.data; + this.emit('error', err); + break; + + case 'message': + this.emit('data', packet.data); + this.emit('message', packet.data); + break; + } + } else { + debug('packet received with socket readyState "%s"', this.readyState); + } +}; + +/** + * Called upon handshake completion. + * + * @param {Object} handshake obj + * @api private + */ + +Socket.prototype.onHandshake = function (data) { + this.emit('handshake', data); + this.id = data.sid; + this.transport.query.sid = data.sid; + this.upgrades = this.filterUpgrades(data.upgrades); + this.pingInterval = data.pingInterval; + this.pingTimeout = data.pingTimeout; + this.onOpen(); + // In case open handler closes socket + if ('closed' == this.readyState) return; + this.setPing(); + + // Prolong liveness of socket on heartbeat + this.removeListener('heartbeat', this.onHeartbeat); + this.on('heartbeat', this.onHeartbeat); +}; + +/** + * Resets ping timeout. + * + * @api private + */ + +Socket.prototype.onHeartbeat = function (timeout) { + clearTimeout(this.pingTimeoutTimer); + var self = this; + self.pingTimeoutTimer = setTimeout(function () { + if ('closed' == self.readyState) return; + self.onClose('ping timeout'); + }, timeout || (self.pingInterval + self.pingTimeout)); +}; + +/** + * Pings server every `this.pingInterval` and expects response + * within `this.pingTimeout` or closes connection. + * + * @api private + */ + +Socket.prototype.setPing = function () { + var self = this; + clearTimeout(self.pingIntervalTimer); + self.pingIntervalTimer = setTimeout(function () { + debug('writing ping packet - expecting pong within %sms', self.pingTimeout); + self.ping(); + self.onHeartbeat(self.pingTimeout); + }, self.pingInterval); +}; + +/** +* Sends a ping packet. +* +* @api public +*/ + +Socket.prototype.ping = function () { + this.sendPacket('ping'); +}; + +/** + * Called on `drain` event + * + * @api private + */ + +Socket.prototype.onDrain = function() { + for (var i = 0; i < this.prevBufferLen; i++) { + if (this.callbackBuffer[i]) { + this.callbackBuffer[i](); + } + } + + this.writeBuffer.splice(0, this.prevBufferLen); + this.callbackBuffer.splice(0, this.prevBufferLen); + + // setting prevBufferLen = 0 is very important + // for example, when upgrading, upgrade packet is sent over, + // and a nonzero prevBufferLen could cause problems on `drain` + this.prevBufferLen = 0; + + if (this.writeBuffer.length == 0) { + this.emit('drain'); + } else { + this.flush(); + } +}; + +/** + * Flush write buffers. + * + * @api private + */ + +Socket.prototype.flush = function () { + if ('closed' != this.readyState && this.transport.writable && + !this.upgrading && this.writeBuffer.length) { + debug('flushing %d packets in socket', this.writeBuffer.length); + this.transport.send(this.writeBuffer); + // keep track of current length of writeBuffer + // splice writeBuffer and callbackBuffer on `drain` + this.prevBufferLen = this.writeBuffer.length; + this.emit('flush'); + } +}; + +/** + * Sends a message. + * + * @param {String} message. + * @param {Function} callback function. + * @return {Socket} for chaining. + * @api public + */ + +Socket.prototype.write = +Socket.prototype.send = function (msg, fn) { + this.sendPacket('message', msg, fn); + return this; +}; + +/** + * Sends a packet. + * + * @param {String} packet type. + * @param {String} data. + * @param {Function} callback function. + * @api private + */ + +Socket.prototype.sendPacket = function (type, data, fn) { + if ('closing' == this.readyState || 'closed' == this.readyState) { + return; + } + + var packet = { type: type, data: data }; + this.emit('packetCreate', packet); + this.writeBuffer.push(packet); + this.callbackBuffer.push(fn); + this.flush(); +}; + +/** + * Closes the connection. + * + * @api private + */ + +Socket.prototype.close = function () { + if ('opening' == this.readyState || 'open' == this.readyState) { + this.readyState = 'closing'; + + var self = this; + + function close() { + self.onClose('forced close'); + debug('socket closing - telling transport to close'); + self.transport.close(); + } + + function cleanupAndClose() { + self.removeListener('upgrade', cleanupAndClose); + self.removeListener('upgradeError', cleanupAndClose); + close(); + } + + function waitForUpgrade() { + // wait for upgrade to finish since we can't send packets while pausing a transport + self.once('upgrade', cleanupAndClose); + self.once('upgradeError', cleanupAndClose); + } + + if (this.writeBuffer.length) { + this.once('drain', function() { + if (this.upgrading) { + waitForUpgrade(); + } else { + close(); + } + }); + } else if (this.upgrading) { + waitForUpgrade(); + } else { + close(); + } + } + + return this; +}; + +/** + * Called upon transport error + * + * @api private + */ + +Socket.prototype.onError = function (err) { + debug('socket error %j', err); + Socket.priorWebsocketSuccess = false; + this.emit('error', err); + this.onClose('transport error', err); +}; + +/** + * Called upon transport close. + * + * @api private + */ + +Socket.prototype.onClose = function (reason, desc) { + if ('opening' == this.readyState || 'open' == this.readyState || 'closing' == this.readyState) { + debug('socket close with reason: "%s"', reason); + var self = this; + + // clear timers + clearTimeout(this.pingIntervalTimer); + clearTimeout(this.pingTimeoutTimer); + + // clean buffers in next tick, so developers can still + // grab the buffers on `close` event + setTimeout(function() { + self.writeBuffer = []; + self.callbackBuffer = []; + self.prevBufferLen = 0; + }, 0); + + // stop event from firing again for transport + this.transport.removeAllListeners('close'); + + // ensure transport won't stay open + this.transport.close(); + + // ignore further transport communication + this.transport.removeAllListeners(); + + // set ready state + this.readyState = 'closed'; + + // clear session id + this.id = null; + + // emit close event + this.emit('close', reason, desc); + } +}; + +/** + * Filters upgrades, returning only those matching client transports. + * + * @param {Array} server upgrades + * @api private + * + */ + +Socket.prototype.filterUpgrades = function (upgrades) { + var filteredUpgrades = []; + for (var i = 0, j = upgrades.length; i