Initial engine.io/websocketrpc port
authorDarren <darren@darrenwhitlen.com>
Tue, 27 Aug 2013 19:13:22 +0000 (20:13 +0100)
committerDarren <darren@darrenwhitlen.com>
Sun, 1 Sep 2013 13:18:18 +0000 (14:18 +0100)
client/assets/libs/engine.io.js [new file with mode: 0644]
client/assets/libs/websocketrpc.js [new file with mode: 0644]
client/assets/src/models/gateway.js
client/assets/src/models/newconnection.js
server/client.js
server/httphandler.js
server/weblistener.js
server/websocketrpc.js [new file with mode: 0644]

diff --git a/client/assets/libs/engine.io.js b/client/assets/libs/engine.io.js
new file mode 100644 (file)
index 0000000..f38b4df
--- /dev/null
@@ -0,0 +1,3129 @@
+;(function(){
+
+/**
+ * Require the given path.
+ *
+ * @param {String} path
+ * @return {Object} exports
+ * @api public
+ */
+
+function require(path, parent, orig) {
+  var resolved = require.resolve(path);
+
+  // lookup failed
+  if (null == resolved) {
+    orig = orig || path;
+    parent = parent || 'root';
+    var err = new Error('Failed to require "' + orig + '" from "' + parent + '"');
+    err.path = orig;
+    err.parent = parent;
+    err.require = true;
+    throw err;
+  }
+
+  var module = require.modules[resolved];
+
+  // perform real require()
+  // by invoking the module's
+  // registered function
+  if (!module.exports) {
+    module.exports = {};
+    module.client = module.component = true;
+    module.call(this, module.exports, require.relative(resolved), module);
+  }
+
+  return module.exports;
+}
+
+/**
+ * Registered modules.
+ */
+
+require.modules = {};
+
+/**
+ * Registered aliases.
+ */
+
+require.aliases = {};
+
+/**
+ * Resolve `path`.
+ *
+ * Lookup:
+ *
+ *   - PATH/index.js
+ *   - PATH.js
+ *   - PATH
+ *
+ * @param {String} path
+ * @return {String} path or null
+ * @api private
+ */
+
+require.resolve = function(path) {
+  if (path.charAt(0) === '/') path = path.slice(1);
+  var index = path + '/index.js';
+
+  var paths = [
+    path,
+    path + '.js',
+    path + '.json',
+    path + '/index.js',
+    path + '/index.json'
+  ];
+
+  for (var i = 0; i < paths.length; i++) {
+    var path = paths[i];
+    if (require.modules.hasOwnProperty(path)) return path;
+  }
+
+  if (require.aliases.hasOwnProperty(index)) {
+    return require.aliases[index];
+  }
+};
+
+/**
+ * Normalize `path` relative to the current path.
+ *
+ * @param {String} curr
+ * @param {String} path
+ * @return {String}
+ * @api private
+ */
+
+require.normalize = function(curr, path) {
+  var segs = [];
+
+  if ('.' != path.charAt(0)) return path;
+
+  curr = curr.split('/');
+  path = path.split('/');
+
+  for (var i = 0; i < path.length; ++i) {
+    if ('..' == path[i]) {
+      curr.pop();
+    } else if ('.' != path[i] && '' != path[i]) {
+      segs.push(path[i]);
+    }
+  }
+
+  return curr.concat(segs).join('/');
+};
+
+/**
+ * Register module at `path` with callback `definition`.
+ *
+ * @param {String} path
+ * @param {Function} definition
+ * @api private
+ */
+
+require.register = function(path, definition) {
+  require.modules[path] = definition;
+};
+
+/**
+ * Alias a module definition.
+ *
+ * @param {String} from
+ * @param {String} to
+ * @api private
+ */
+
+require.alias = function(from, to) {
+  if (!require.modules.hasOwnProperty(from)) {
+    throw new Error('Failed to alias "' + from + '", it does not exist');
+  }
+  require.aliases[to] = from;
+};
+
+/**
+ * Return a require function relative to the `parent` path.
+ *
+ * @param {String} parent
+ * @return {Function}
+ * @api private
+ */
+
+require.relative = function(parent) {
+  var p = require.normalize(parent, '..');
+
+  /**
+   * lastIndexOf helper.
+   */
+
+  function lastIndexOf(arr, obj) {
+    var i = arr.length;
+    while (i--) {
+      if (arr[i] === obj) return i;
+    }
+    return -1;
+  }
+
+  /**
+   * The relative require() itself.
+   */
+
+  function localRequire(path) {
+    var resolved = localRequire.resolve(path);
+    return require(resolved, parent, path);
+  }
+
+  /**
+   * Resolve relative to the parent.
+   */
+
+  localRequire.resolve = function(path) {
+    var c = path.charAt(0);
+    if ('/' == c) return path.slice(1);
+    if ('.' == c) return require.normalize(p, path);
+
+    // resolve deps by returning
+    // the dep in the nearest "deps"
+    // directory
+    var segs = parent.split('/');
+    var i = lastIndexOf(segs, 'deps') + 1;
+    if (!i) i = 0;
+    path = segs.slice(0, i + 1).join('/') + '/deps/' + path;
+    return path;
+  };
+
+  /**
+   * Check if module is defined at `path`.
+   */
+
+  localRequire.exists = function(path) {
+    return require.modules.hasOwnProperty(localRequire.resolve(path));
+  };
+
+  return localRequire;
+};
+require.register("component-emitter/index.js", function(exports, require, module){
+
+/**
+ * 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 = 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);
+  }
+
+  fn._off = on;
+  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 = 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 i = callbacks.indexOf(fn._off || fn);
+  if (~i) callbacks.splice(i, 1);
+  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;
+};
+
+});
+require.register("component-indexof/index.js", function(exports, require, module){
+
+var indexOf = [].indexOf;
+
+module.exports = function(arr, obj){
+  if (indexOf) return arr.indexOf(obj);
+  for (var i = 0; i < arr.length; ++i) {
+    if (arr[i] === obj) return i;
+  }
+  return -1;
+};
+});
+require.register("LearnBoost-engine.io-protocol/lib/index.js", function(exports, require, module){
+/**
+ * Module dependencies.
+ */
+
+var keys = require('./keys');
+
+/**
+ * Current protocol version.
+ */
+exports.protocol = 2;
+
+/**
+ * Packet types.
+ */
+
+var packets = exports.packets = {
+    open:     0    // non-ws
+  , close:    1    // non-ws
+  , ping:     2
+  , pong:     3
+  , message:  4
+  , upgrade:  5
+  , noop:     6
+};
+
+var packetslist = keys(packets);
+
+/**
+ * Premade error packet.
+ */
+
+var err = { type: 'error', data: 'parser error' };
+
+/**
+ * Encodes a packet.
+ *
+ *     <packet type id> [ `:` <data> ]
+ *
+ * Example:
+ *
+ *     5:hello world
+ *     3
+ *     4
+ *
+ * @api private
+ */
+
+exports.encodePacket = function (packet) {
+  var encoded = packets[packet.type];
+
+  // data fragment is optional
+  if (undefined !== packet.data) {
+    encoded += String(packet.data);
+  }
+
+  return '' + encoded;
+};
+
+/**
+ * Decodes a packet.
+ *
+ * @return {Object} with `type` and `data` (if any)
+ * @api private
+ */
+
+exports.decodePacket = function (data) {
+  var type = data.charAt(0);
+
+  if (Number(type) != type || !packetslist[type]) {
+    return err;
+  }
+
+  if (data.length > 1) {
+    return { type: packetslist[type], data: data.substring(1) };
+  } else {
+    return { type: packetslist[type] };
+  }
+};
+
+/**
+ * Encodes multiple messages (payload).
+ *
+ *     <length>:data
+ *
+ * Example:
+ *
+ *     11:hello world2:hi
+ *
+ * @param {Array} packets
+ * @api private
+ */
+
+exports.encodePayload = function (packets) {
+  if (!packets.length) {
+    return '0:';
+  }
+
+  var encoded = '';
+  var message;
+
+  for (var i = 0, l = packets.length; i < l; i++) {
+    message = exports.encodePacket(packets[i]);
+    encoded += message.length + ':' + message;
+  }
+
+  return encoded;
+};
+
+/*
+ * Decodes data when a payload is maybe expected.
+ *
+ * @param {String} data, callback method
+ * @api public
+ */
+
+exports.decodePayload = function (data, callback) {
+  var packet;
+  if (data == '') {
+    // parser error - ignoring payload
+    return callback(err, 0, 1);
+  }
+
+  var length = ''
+    , n, msg;
+
+  for (var i = 0, l = data.length; i < l; i++) {
+    var chr = data.charAt(i);
+
+    if (':' != chr) {
+      length += chr;
+    } else {
+      if ('' == length || (length != (n = Number(length)))) {
+        // parser error - ignoring payload
+        return callback(err, 0, 1);
+      }
+
+      msg = data.substr(i + 1, n);
+
+      if (length != msg.length) {
+        // parser error - ignoring payload
+        return callback(err, 0, 1);
+      }
+
+      if (msg.length) {
+        packet = exports.decodePacket(msg);
+
+        if (err.type == packet.type && err.data == packet.data) {
+          // parser error in individual packet - ignoring payload
+          return callback(err, 0, 1);
+        }
+
+        var ret = callback(packet, i + n, l);
+        if (false === ret) return;
+      }
+
+      // advance cursor
+      i += n;
+      length = '';
+    }
+  }
+
+  if (length != '') {
+    // parser error - ignoring payload
+    return callback(err, 0, 1);
+  }
+
+};
+
+});
+require.register("LearnBoost-engine.io-protocol/lib/keys.js", function(exports, require, module){
+
+/**
+ * Gets the keys for an object.
+ *
+ * @return {Array} keys
+ * @api private
+ */
+
+module.exports = Object.keys || function keys (obj){
+  var arr = [];
+  var has = Object.prototype.hasOwnProperty;
+
+  for (var i in obj) {
+    if (has.call(obj, i)) {
+      arr.push(i);
+    }
+  }
+  return arr;
+};
+
+});
+require.register("visionmedia-debug/index.js", function(exports, require, module){
+if ('undefined' == typeof window) {
+  module.exports = require('./lib/debug');
+} else {
+  module.exports = require('./debug');
+}
+
+});
+require.register("visionmedia-debug/debug.js", function(exports, require, module){
+
+/**
+ * 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
+
+if (window.localStorage) debug.enable(localStorage.debug);
+
+});
+require.register("engine.io/lib/index.js", function(exports, require, module){
+
+module.exports = require('./socket');
+
+/**
+ * Exports parser
+ *
+ * @api public
+ *
+ */
+module.exports.parser = require('engine.io-parser');
+
+});
+require.register("engine.io/lib/socket.js", function(exports, require, module){
+/**
+ * Module dependencies.
+ */
+
+var util = require('./util')
+  , transports = require('./transports')
+  , Emitter = require('./emitter')
+  , debug = require('debug')('engine-client:socket')
+  , index = require('indexof')
+  , parser = require('engine.io-parser');
+
+/**
+ * Module exports.
+ */
+
+module.exports = Socket;
+
+/**
+ * Global reference.
+ */
+
+var global = util.global();
+
+/**
+ * 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 ('object' == typeof uri) {
+    opts = uri;
+    uri = null;
+  }
+
+  if (uri) {
+    uri = util.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();
+  }
+
+  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 = util.qsParse(this.query);
+  this.upgrade = false !== opts.upgrade;
+  this.path = (opts.path || '/engine.io').replace(/\/$/, '') + '/';
+  this.forceJSONP = !!opts.forceJSONP;
+  this.timestampParam = opts.timestampParam || 't';
+  this.timestampRequests = !!opts.timestampRequests;
+  this.flashPath = opts.flashPath || '';
+  this.transports = opts.transports || ['polling', 'websocket', 'flashsocket'];
+  this.readyState = '';
+  this.writeBuffer = [];
+  this.callbackBuffer = [];
+  this.policyPort = opts.policyPort || 843;
+  this.open();
+
+  Socket.sockets.push(this);
+  Socket.sockets.evs.emit('add', this);
+};
+
+/**
+ * Mix in `Emitter`.
+ */
+
+Emitter(Socket.prototype);
+
+/**
+ * Protocol version.
+ *
+ * @api public
+ */
+
+Socket.protocol = parser.protocol; // this is an int
+
+/**
+ * Static EventEmitter.
+ */
+
+Socket.sockets = [];
+Socket.sockets.evs = new Emitter;
+
+/**
+ * Expose deps for legacy compatibility
+ * and standalone browser access.
+ */
+
+Socket.Socket = Socket;
+Socket.Transport = require('./transport');
+Socket.Emitter = require('./emitter');
+Socket.transports = require('./transports');
+Socket.util = require('./util');
+Socket.parser = require('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]({
+    hostname: this.hostname,
+    port: this.port,
+    secure: this.secure,
+    path: this.path,
+    query: query,
+    forceJSONP: this.forceJSONP,
+    timestampRequests: this.timestampRequests,
+    timestampParam: this.timestampParam,
+    flashPath: this.flashPath,
+    policyPort: this.policyPort
+  });
+
+  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 () {
+  this.readyState = 'opening';
+  var transport = this.createTransport(this.transports[0]);
+  transport.open();
+  this.setTransport(transport);
+};
+
+/**
+ * Sets the current transport. Disables the existing one (if any).
+ *
+ * @api private
+ */
+
+Socket.prototype.setTransport = function (transport) {
+  var self = this;
+
+  if (this.transport) {
+    debug('clearing existing transport');
+    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;
+
+  transport.once('open', function () {
+    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);
+
+        debug('pausing current transport "%s"', self.transport.name);
+        self.transport.pause(function () {
+          if (failed) return;
+          if ('closed' == self.readyState || 'closing' == self.readyState) {
+            return;
+          }
+          debug('changing transport and sending upgrade packet');
+          transport.removeListener('error', onerror);
+          self.emit('upgrade', transport);
+          self.setTransport(transport);
+          transport.send([{ type: 'upgrade' }]);
+          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('error', err);
+      }
+    });
+  });
+
+  transport.once('error', onerror);
+  function onerror(err) {
+    if (failed) return;
+
+    // Any callback called by transport should be ignored since now
+    failed = true;
+
+    var error = new Error('probe error: ' + err);
+    error.transport = transport.name;
+
+    transport.close();
+    transport = null;
+
+    debug('probe transport "%s" failed because of error: %s', name, err);
+
+    self.emit('error', error);
+  };
+
+  transport.open();
+
+  this.once('close', function () {
+    if (transport) {
+      debug('socket closed prematurely - aborting probe');
+      failed = true;
+      transport.close();
+      transport = null;
+    }
+  });
+
+  this.once('upgrading', function (to) {
+    if (transport && to.name != transport.name) {
+      debug('"%s" works - aborting "%s"', to.name, transport.name);
+      transport.close();
+      transport = null;
+    }
+  });
+};
+
+/**
+ * Called when connection is deemed open.
+ *
+ * @api public
+ */
+
+Socket.prototype.onOpen = function () {
+  debug('socket open');
+  this.readyState = 'open';
+  this.emit('open');
+  this.onopen && this.onopen.call(this);
+  this.flush();
+
+  // we check for `readyState` in case an `open`
+  // listener alreay 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(util.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);
+        var event = { data: packet.data };
+        event.toString = function () {
+          return packet.data;
+        };
+        this.onmessage && this.onmessage.call(this, event);
+        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();
+  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) {
+  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.onClose('forced close');
+    debug('socket closing - telling transport to close');
+    this.transport.close();
+  }
+
+  return this;
+};
+
+/**
+ * Called upon transport error
+ *
+ * @api private
+ */
+
+Socket.prototype.onError = function (err) {
+  debug('socket error %j', err);
+  this.emit('error', err);
+  this.onerror && this.onerror.call(this, 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) {
+    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 = [];
+    }, 0);
+
+    // ignore further transport communication
+    this.transport.removeAllListeners();
+
+    // set ready state
+    var prev = this.readyState;
+    this.readyState = 'closed';
+
+    // clear session id
+    this.id = null;
+
+    // emit events
+    if (prev == 'open') {
+      this.emit('close', reason, desc);
+      this.onclose && this.onclose.call(this);
+    }
+  }
+};
+
+/**
+ * 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<j; i++) {
+    if (~index(this.transports, upgrades[i])) filteredUpgrades.push(upgrades[i]);
+  }
+  return filteredUpgrades;
+};
+
+});
+require.register("engine.io/lib/transport.js", function(exports, require, module){
+
+/**
+ * Module dependencies.
+ */
+
+var util = require('./util')
+  , parser = require('engine.io-parser')
+  , Emitter = require('./emitter');
+
+/**
+ * Module exports.
+ */
+
+module.exports = Transport;
+
+/**
+ * Transport abstract constructor.
+ *
+ * @param {Object} options.
+ * @api private
+ */
+
+function Transport (opts) {
+  this.path = opts.path;
+  this.hostname = opts.hostname;
+  this.port = opts.port;
+  this.secure = opts.secure;
+  this.query = opts.query;
+  this.timestampParam = opts.timestampParam;
+  this.timestampRequests = opts.timestampRequests;
+  this.readyState = '';
+};
+
+/**
+  * Mix in `Emitter`.
+ */
+
+Emitter(Transport.prototype);
+
+/**
+ * Emits an error.
+ *
+ * @param {String} str
+ * @return {Transport} for chaining
+ * @api public
+ */
+
+Transport.prototype.onError = function (msg, desc) {
+  var err = new Error(msg);
+  err.type = 'TransportError';
+  err.description = desc;
+  this.emit('error', err);
+  return this;
+};
+
+/**
+ * Opens the transport.
+ *
+ * @api public
+ */
+
+Transport.prototype.open = function () {
+  if ('closed' == this.readyState || '' == this.readyState) {
+    this.readyState = 'opening';
+    this.doOpen();
+  }
+
+  return this;
+};
+
+/**
+ * Closes the transport.
+ *
+ * @api private
+ */
+
+Transport.prototype.close = function () {
+  if ('opening' == this.readyState || 'open' == this.readyState) {
+    this.doClose();
+    this.onClose();
+  }
+
+  return this;
+};
+
+/**
+ * Sends multiple packets.
+ *
+ * @param {Array} packets
+ * @api private
+ */
+
+Transport.prototype.send = function(packets){
+  if ('open' == this.readyState) {
+    this.write(packets);
+  } else {
+    throw new Error('Transport not open');
+  }
+};
+
+/**
+ * Called upon open
+ *
+ * @api private
+ */
+
+Transport.prototype.onOpen = function () {
+  this.readyState = 'open';
+  this.writable = true;
+  this.emit('open');
+};
+
+/**
+ * Called with data.
+ *
+ * @param {String} data
+ * @api private
+ */
+
+Transport.prototype.onData = function (data) {
+  this.onPacket(parser.decodePacket(data));
+};
+
+/**
+ * Called with a decoded packet.
+ */
+
+Transport.prototype.onPacket = function (packet) {
+  this.emit('packet', packet);
+};
+
+/**
+ * Called upon close.
+ *
+ * @api private
+ */
+
+Transport.prototype.onClose = function () {
+  this.readyState = 'closed';
+  this.emit('close');
+};
+
+});
+require.register("engine.io/lib/emitter.js", function(exports, require, module){
+
+/**
+ * Module dependencies.
+ */
+
+var Emitter = require('emitter');
+
+/**
+ * Module exports.
+ */
+
+module.exports = Emitter;
+
+/**
+ * Compatibility with `WebSocket#addEventListener`.
+ *
+ * @api public
+ */
+
+Emitter.prototype.addEventListener = Emitter.prototype.on;
+
+/**
+ * Compatibility with `WebSocket#removeEventListener`.
+ *
+ * @api public
+ */
+
+Emitter.prototype.removeEventListener = Emitter.prototype.off;
+
+/**
+ * Node-compatible `EventEmitter#removeListener`
+ *
+ * @api public
+ */
+
+Emitter.prototype.removeListener = Emitter.prototype.off;
+
+});
+require.register("engine.io/lib/util.js", function(exports, require, module){
+/**
+ * Status of page load.
+ */
+
+var pageLoaded = false;
+
+/**
+ * Returns the global object
+ *
+ * @api private
+ */
+
+exports.global = function () {
+  return 'undefined' != typeof window ? window : global;
+};
+
+/**
+ * Inheritance.
+ *
+ * @param {Function} ctor a
+ * @param {Function} ctor b
+ * @api private
+ */
+
+exports.inherits = function inherits (a, b) {
+  function c () { }
+  c.prototype = b.prototype;
+  a.prototype = new c;
+};
+
+/**
+ * Object.keys
+ */
+
+exports.keys = Object.keys || function (obj) {
+  var ret = [];
+  var has = Object.prototype.hasOwnProperty;
+
+  for (var i in obj) {
+    if (has.call(obj, i)) {
+      ret.push(i);
+    }
+  }
+
+  return ret;
+};
+
+/**
+ * Adds an event.
+ *
+ * @api private
+ */
+
+exports.on = function (element, event, fn, capture) {
+  if (element.attachEvent) {
+    element.attachEvent('on' + event, fn);
+  } else if (element.addEventListener) {
+    element.addEventListener(event, fn, capture);
+  }
+};
+
+/**
+ * Load utility.
+ *
+ * @api private
+ */
+
+exports.load = function (fn) {
+  var global = exports.global();
+  if (global.document && document.readyState === 'complete' || pageLoaded) {
+    return fn();
+  }
+
+  exports.on(global, 'load', fn, false);
+};
+
+/**
+ * Change the internal pageLoaded value.
+ */
+
+if ('undefined' != typeof window) {
+  exports.load(function () {
+    pageLoaded = true;
+  });
+}
+
+/**
+ * Defers a function to ensure a spinner is not displayed by the browser.
+ *
+ * @param {Function} fn
+ * @api private
+ */
+
+exports.defer = function (fn) {
+  if (!exports.ua.webkit || 'undefined' != typeof importScripts) {
+    return fn();
+  }
+
+  exports.load(function () {
+    setTimeout(fn, 100);
+  });
+};
+
+/**
+ * JSON parse.
+ *
+ * @see Based on jQuery#parseJSON (MIT) and JSON2
+ * @api private
+ */
+
+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+$/;
+
+exports.parseJSON = function (data) {
+  var global = exports.global();
+
+  if ('string' != typeof data || !data) {
+    return null;
+  }
+
+  data = data.replace(rtrimLeft, '').replace(rtrimRight, '');
+
+  // Attempt to parse using the native JSON parser first
+  if (global.JSON && JSON.parse) {
+    return JSON.parse(data);
+  }
+
+  if (rvalidchars.test(data.replace(rvalidescape, '@')
+      .replace(rvalidtokens, ']')
+      .replace(rvalidbraces, ''))) {
+    return (new Function('return ' + data))();
+  }
+};
+
+/**
+ * UA / engines detection namespace.
+ *
+ * @namespace
+ */
+
+exports.ua = {};
+
+/**
+ * Whether the UA supports CORS for XHR.
+ *
+ * @api private
+ */
+
+exports.ua.hasCORS = 'undefined' != typeof XMLHttpRequest && (function () {
+  var a;
+  try {
+    a = new XMLHttpRequest();
+  } catch (e) {
+    return false;
+  }
+
+  return a.withCredentials != undefined;
+})();
+
+/**
+ * Detect webkit.
+ *
+ * @api private
+ */
+
+exports.ua.webkit = 'undefined' != typeof navigator &&
+  /webkit/i.test(navigator.userAgent);
+
+/**
+ * Detect gecko.
+ *
+ * @api private
+ */
+
+exports.ua.gecko = 'undefined' != typeof navigator &&
+  /gecko/i.test(navigator.userAgent);
+
+/**
+ * Detect android;
+ */
+
+exports.ua.android = 'undefined' != typeof navigator &&
+  /android/i.test(navigator.userAgent);
+
+/**
+ * Detect iOS.
+ */
+
+exports.ua.ios = 'undefined' != typeof navigator &&
+  /^(iPad|iPhone|iPod)$/.test(navigator.platform);
+exports.ua.ios6 = exports.ua.ios && /OS 6_/.test(navigator.userAgent);
+
+/**
+ * XHR request helper.
+ *
+ * @param {Boolean} whether we need xdomain
+ * @api private
+ */
+
+exports.request = function request (xdomain) {
+  try {
+    var _XMLHttpRequest = require('xmlhttprequest').XMLHttpRequest;
+    return new _XMLHttpRequest();
+  } catch (e) {}
+
+  if (xdomain && 'undefined' != typeof XDomainRequest && !exports.ua.hasCORS) {
+    return new XDomainRequest();
+  }
+
+  // XMLHttpRequest can be disabled on IE
+  try {
+    if ('undefined' != typeof XMLHttpRequest && (!xdomain || exports.ua.hasCORS)) {
+      return new XMLHttpRequest();
+    }
+  } catch (e) { }
+
+  if (!xdomain) {
+    try {
+      return new ActiveXObject('Microsoft.XMLHTTP');
+    } catch(e) { }
+  }
+};
+
+/**
+ * Parses an URI
+ *
+ * @author Steven Levithan <stevenlevithan.com> (MIT license)
+ * @api private
+ */
+
+var re = /^(?:(?![^:@]+:[^:@\/]*@)([^:\/?#.]+):)?(?:\/\/)?((?:(([^:@]*)(?::([^:@]*))?)?@)?([^:\/?#]*)(?::(\d*))?)(((\/(?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?([^?#\/]*))(?:\?([^#]*))?(?:#(.*))?)/;
+
+var parts = [
+    'source', 'protocol', 'authority', 'userInfo', 'user', 'password', 'host'
+  , 'port', 'relative', 'path', 'directory', 'file', 'query', 'anchor'
+];
+
+exports.parseUri = function (str) {
+  var m = re.exec(str || '')
+    , uri = {}
+    , i = 14;
+
+  while (i--) {
+    uri[parts[i]] = m[i] || '';
+  }
+
+  return uri;
+};
+
+/**
+ * Compiles a querystring
+ *
+ * @param {Object}
+ * @api private
+ */
+
+exports.qs = 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;
+};
+
+/**
+ * Parses a simple querystring.
+ *
+ * @param {String} qs
+ * @api private
+ */
+
+exports.qsParse = function(qs){
+  var qry = {};
+  var pairs = qs.split('&');
+  for (var i = 0, l = pairs.length; i < l; i++) {
+    var pair = pairs[i].split('=');
+    qry[decodeURIComponent(pair[0])] = decodeURIComponent(pair[1]);
+  }
+  return qry;
+};
+
+});
+require.register("engine.io/lib/transports/index.js", function(exports, require, module){
+
+/**
+ * Module dependencies
+ */
+
+var XHR = require('./polling-xhr')
+  , JSONP = require('./polling-jsonp')
+  , websocket = require('./websocket')
+  , flashsocket = require('./flashsocket')
+  , util = require('../util');
+
+/**
+ * Export transports.
+ */
+
+exports.polling = polling;
+exports.websocket = websocket;
+exports.flashsocket = flashsocket;
+
+/**
+ * Global reference.
+ */
+
+var global = util.global()
+
+/**
+ * Polling transport polymorphic constructor.
+ * Decides on xhr vs jsonp based on feature detection.
+ *
+ * @api private
+ */
+
+function polling (opts) {
+  var xhr
+    , xd = false
+    , isXProtocol = false;
+
+  if (global.location) {
+    var isSSL = 'https:' == location.protocol;
+    var port = location.port;
+
+    // some user agents have empty `location.port`
+    if (Number(port) !== port) {
+      port = isSSL ? 443 : 80;
+    }
+
+    xd = opts.hostname != location.hostname || port != opts.port;
+    isXProtocol = opts.secure != isSSL;
+  }
+
+  xhr = util.request(xd);
+  /* See #7 at http://blogs.msdn.com/b/ieinternals/archive/2010/05/13/xdomainrequest-restrictions-limitations-and-workarounds.aspx */
+  if (isXProtocol && global.XDomainRequest && xhr instanceof global.XDomainRequest) {
+    return new JSONP(opts);
+  }
+
+  if (xhr && !opts.forceJSONP) {
+    return new XHR(opts);
+  } else {
+    return new JSONP(opts);
+  }
+};
+
+});
+require.register("engine.io/lib/transports/polling.js", function(exports, require, module){
+/**
+ * Module dependencies.
+ */
+
+var Transport = require('../transport')
+  , util = require('../util')
+  , parser = require('engine.io-parser')
+  , debug = require('debug')('engine.io-client:polling');
+
+/**
+ * Module exports.
+ */
+
+module.exports = Polling;
+
+/**
+ * Global reference.
+ */
+
+var global = util.global();
+
+/**
+ * Polling interface.
+ *
+ * @param {Object} opts
+ * @api private
+ */
+
+function Polling(opts){
+  Transport.call(this, opts);
+}
+
+/**
+ * Inherits from Transport.
+ */
+
+util.inherits(Polling, Transport);
+
+/**
+ * Transport name.
+ */
+
+Polling.prototype.name = 'polling';
+
+/**
+ * Opens the socket (triggers polling). We write a PING message to determine
+ * when the transport is open.
+ *
+ * @api private
+ */
+
+Polling.prototype.doOpen = function(){
+  this.poll();
+};
+
+/**
+ * Pauses polling.
+ *
+ * @param {Function} callback upon buffers are flushed and transport is paused
+ * @api private
+ */
+
+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();
+  }
+};
+
+/**
+ * Starts polling cycle.
+ *
+ * @api public
+ */
+
+Polling.prototype.poll = function(){
+  debug('polling');
+  this.polling = true;
+  this.doPoll();
+  this.emit('poll');
+};
+
+/**
+ * Overloads onData to detect payloads.
+ *
+ * @api private
+ */
+
+Polling.prototype.onData = function(data){
+  var self = this;
+  debug('polling got data %s', data);
+
+  // decode payload
+  parser.decodePayload(data, function(packet, index, total) {
+    // if its the first message we consider the transport open
+    if ('opening' == self.readyState) {
+      self.onOpen();
+    }
+
+    // if its a close packet, we close the ongoing requests
+    if ('close' == packet.type) {
+      self.onClose();
+      return false;
+    }
+
+    // otherwise bypass onData and handle the message
+    self.onPacket(packet);
+  });
+
+  // if an event did not trigger closing
+  if ('closed' != this.readyState) {
+    // if we got data we're not polling
+    this.polling = false;
+    this.emit('pollComplete');
+
+    if ('open' == this.readyState) {
+      this.poll();
+    } else {
+      debug('ignoring poll - transport state "%s"', this.readyState);
+    }
+  }
+};
+
+/**
+ * For polling, send a close packet.
+ *
+ * @api private
+ */
+
+Polling.prototype.doClose = function(){
+  var self = this;
+
+  function close(){
+    debug('writing close packet');
+    self.write([{ type: 'close' }]);
+  }
+
+  if (this.open) {
+    debug('transport open - closing');
+    close();
+  } else {
+    // in case we're trying to close while
+    // handshaking is in progress (GH-164)
+    debug('transport not open - defering close');
+    this.once('open', close);
+  }
+};
+
+/**
+ * Writes a packets payload.
+ *
+ * @param {Array} data packets
+ * @param {Function} drain callback
+ * @api private
+ */
+
+Polling.prototype.write = function(packets){
+  var self = this;
+  this.writable = false;
+  this.doWrite(parser.encodePayload(packets), function(){
+    self.writable = true;
+    self.emit('drain');
+  });
+};
+
+/**
+ * Generates uri for connection.
+ *
+ * @api private
+ */
+
+Polling.prototype.uri = function(){
+  var query = this.query || {};
+  var schema = this.secure ? 'https' : 'http';
+  var port = '';
+
+  // cache busting is forced for IE / android / iOS6 à² _ಠ
+  if (global.ActiveXObject || util.ua.android || util.ua.ios6 ||
+      this.timestampRequests) {
+    query[this.timestampParam] = +new Date;
+  }
+
+  query = util.qs(query);
+
+  // avoid port if default for schema
+  if (this.port && (('https' == schema && this.port != 443) ||
+     ('http' == schema && this.port != 80))) {
+    port = ':' + this.port;
+  }
+
+  // prepend ? to query
+  if (query.length) {
+    query = '?' + query;
+  }
+
+  return schema + '://' + this.hostname + port + this.path + query;
+};
+
+});
+require.register("engine.io/lib/transports/polling-xhr.js", function(exports, require, module){
+/**
+ * Module requirements.
+ */
+
+var Polling = require('./polling')
+  , util = require('../util')
+  , Emitter = require('../emitter')
+  , debug = require('debug')('engine.io-client:polling-xhr');
+
+/**
+ * Module exports.
+ */
+
+module.exports = XHR;
+module.exports.Request = Request;
+
+/**
+ * Global reference.
+ */
+
+var global = util.global();
+
+
+/**
+ * Obfuscated key for Blue Coat.
+ */
+
+var xobject = global[['Active'].concat('Object').join('X')];
+
+/**
+ * Empty function
+ */
+
+function empty(){}
+
+/**
+ * XHR Polling constructor.
+ *
+ * @param {Object} opts
+ * @api public
+ */
+
+function XHR(opts){
+  Polling.call(this, opts);
+
+  if (global.location) {
+    var isSSL = 'https:' == location.protocol;
+    var port = location.port;
+
+    // some user agents have empty `location.port`
+    if (Number(port) !== port) {
+      port = isSSL ? 443 : 80;
+    }
+
+    this.xd = opts.hostname != global.location.hostname ||
+      port != opts.port;
+  }
+};
+
+/**
+ * Inherits from Polling.
+ */
+
+util.inherits(XHR, Polling);
+
+/**
+ * Opens the socket
+ *
+ * @api private
+ */
+
+XHR.prototype.doOpen = function(){
+  var self = this;
+  util.defer(function(){
+    Polling.prototype.doOpen.call(self);
+  });
+};
+
+/**
+ * Creates a request.
+ *
+ * @param {String} method
+ * @api private
+ */
+
+XHR.prototype.request = function(opts){
+  opts = opts || {};
+  opts.uri = this.uri();
+  opts.xd = this.xd;
+  return new Request(opts);
+};
+
+/**
+ * Sends data.
+ *
+ * @param {String} data to send.
+ * @param {Function} called upon flush.
+ * @api private
+ */
+
+XHR.prototype.doWrite = function(data, fn){
+  var req = this.request({ method: 'POST', data: data });
+  var self = this;
+  req.on('success', fn);
+  req.on('error', function(err){
+    self.onError('xhr post error', err);
+  });
+  this.sendXhr = req;
+};
+
+/**
+ * Starts a poll cycle.
+ *
+ * @api private
+ */
+
+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;
+};
+
+/**
+ * Request constructor
+ *
+ * @param {Object} options
+ * @api public
+ */
+
+function Request(opts){
+  this.method = opts.method || 'GET';
+  this.uri = opts.uri;
+  this.xd = !!opts.xd;
+  this.async = false !== opts.async;
+  this.data = undefined != opts.data ? opts.data : null;
+  this.create();
+}
+
+/**
+ * Mix in `Emitter`.
+ */
+
+Emitter(Request.prototype);
+
+/**
+ * Creates the XHR object and sends the request.
+ *
+ * @api private
+ */
+
+Request.prototype.create = function(){
+  var xhr = this.xhr = util.request(this.xd);
+  var self = this;
+
+  xhr.open(this.method, this.uri, this.async);
+
+  if ('POST' == this.method) {
+    try {
+      if (xhr.setRequestHeader) {
+        // xmlhttprequest
+        xhr.setRequestHeader('Content-type', 'text/plain;charset=UTF-8');
+      } else {
+        // xdomainrequest
+        xhr.contentType = 'text/plain';
+      }
+    } catch (e) {}
+  }
+
+  if (this.xd && global.XDomainRequest && xhr instanceof XDomainRequest) {
+    xhr.onerror = function(e){
+      self.onError(e);
+    };
+    xhr.onload = function(){
+      self.onData(xhr.responseText);
+    };
+    xhr.onprogress = empty;
+  } else {
+    // ie6 check
+    if ('withCredentials' in xhr) {
+      xhr.withCredentials = true;
+    }
+
+    xhr.onreadystatechange = function(){
+      var data;
+
+      try {
+        if (4 != xhr.readyState) return;
+        if (200 == xhr.status || 1223 == xhr.status) {
+          data = xhr.responseText;
+        } else {
+          self.onError(xhr.status);
+        }
+      } catch (e) {
+        self.onError(e);
+      }
+
+      if (undefined !== data) {
+        self.onData(data);
+      }
+    };
+  }
+
+  debug('sending xhr with url %s | data %s', this.uri, this.data);
+  xhr.send(this.data);
+
+  if (xobject) {
+    this.index = Request.requestsCount++;
+    Request.requests[this.index] = this;
+  }
+};
+
+/**
+ * Called upon successful response.
+ *
+ * @api private
+ */
+
+Request.prototype.onSuccess = function(){
+  this.emit('success');
+  this.cleanup();
+};
+
+/**
+ * Called if we have data.
+ *
+ * @api private
+ */
+
+Request.prototype.onData = function(data){
+  this.emit('data', data);
+  this.onSuccess();
+};
+
+/**
+ * Called upon error.
+ *
+ * @api private
+ */
+
+Request.prototype.onError = function(err){
+  this.emit('error', err);
+  this.cleanup();
+};
+
+/**
+ * Cleans up house.
+ *
+ * @api private
+ */
+
+Request.prototype.cleanup = function(){
+  if ('undefined' == typeof this.xhr ) {
+    return;
+  }
+  // xmlhttprequest
+  this.xhr.onreadystatechange = empty;
+
+  // xdomainrequest
+  this.xhr.onload = this.xhr.onerror = empty;
+
+  try {
+    this.xhr.abort();
+  } catch(e) {}
+
+  if (xobject) {
+    delete Request.requests[this.index];
+  }
+
+  this.xhr = null;
+};
+
+/**
+ * Aborts the request.
+ *
+ * @api public
+ */
+
+Request.prototype.abort = function(){
+  this.cleanup();
+};
+
+if (xobject) {
+  Request.requestsCount = 0;
+  Request.requests = {};
+
+  global.attachEvent('onunload', function(){
+    for (var i in Request.requests) {
+      if (Request.requests.hasOwnProperty(i)) {
+        Request.requests[i].abort();
+      }
+    }
+  });
+}
+
+});
+require.register("engine.io/lib/transports/polling-jsonp.js", function(exports, require, module){
+
+/**
+ * Module requirements.
+ */
+
+var Polling = require('./polling')
+  , util = require('../util');
+
+/**
+ * Module exports.
+ */
+
+module.exports = JSONPPolling;
+
+/**
+ * Global reference.
+ */
+
+var global = util.global();
+
+/**
+ * Cached regular expressions.
+ */
+
+var rNewline = /\n/g;
+
+/**
+ * Global JSONP callbacks.
+ */
+
+var callbacks;
+
+/**
+ * Callbacks count.
+ */
+
+var index = 0;
+
+/**
+ * Noop.
+ */
+
+function empty () { }
+
+/**
+ * JSONP Polling constructor.
+ *
+ * @param {Object} opts.
+ * @api public
+ */
+
+function JSONPPolling (opts) {
+  Polling.call(this, opts);
+
+  // define global callbacks array if not present
+  // we do this here (lazily) to avoid unneeded global pollution
+  if (!callbacks) {
+    // we need to consider multiple engines in the same page
+    if (!global.___eio) global.___eio = [];
+    callbacks = global.___eio;
+  }
+
+  // callback identifier
+  this.index = callbacks.length;
+
+  // add callback to jsonp global
+  var self = this;
+  callbacks.push(function (msg) {
+    self.onData(msg);
+  });
+
+  // append to query string
+  this.query.j = this.index;
+};
+
+/**
+ * Inherits from Polling.
+ */
+
+util.inherits(JSONPPolling, Polling);
+
+/**
+ * Opens the socket.
+ *
+ * @api private
+ */
+
+JSONPPolling.prototype.doOpen = function () {
+  var self = this;
+  util.defer(function () {
+    Polling.prototype.doOpen.call(self);
+  });
+};
+
+/**
+ * Closes the socket
+ *
+ * @api private
+ */
+
+JSONPPolling.prototype.doClose = function () {
+  if (this.script) {
+    this.script.parentNode.removeChild(this.script);
+    this.script = null;
+  }
+
+  if (this.form) {
+    this.form.parentNode.removeChild(this.form);
+    this.form = null;
+  }
+
+  Polling.prototype.doClose.call(this);
+};
+
+/**
+ * Starts a poll cycle.
+ *
+ * @api private
+ */
+
+JSONPPolling.prototype.doPoll = function () {
+       var self = this;
+  var script = document.createElement('script');
+
+  if (this.script) {
+    this.script.parentNode.removeChild(this.script);
+    this.script = null;
+  }
+
+  script.async = true;
+  script.src = this.uri();
+       script.onerror = function(e){
+               self.onError('jsonp poll error',e);
+       }
+
+  var insertAt = document.getElementsByTagName('script')[0];
+  insertAt.parentNode.insertBefore(script, insertAt);
+  this.script = script;
+
+
+  if (util.ua.gecko) {
+    setTimeout(function () {
+      var iframe = document.createElement('iframe');
+      document.body.appendChild(iframe);
+      document.body.removeChild(iframe);
+    }, 100);
+  }
+};
+
+/**
+ * Writes with a hidden iframe.
+ *
+ * @param {String} data to send
+ * @param {Function} called upon flush.
+ * @api private
+ */
+
+JSONPPolling.prototype.doWrite = function (data, fn) {
+  var self = this;
+
+  if (!this.form) {
+    var form = document.createElement('form');
+    var area = document.createElement('textarea');
+    var id = this.iframeId = 'eio_iframe_' + this.index;
+    var iframe;
+
+    form.className = 'socketio';
+    form.style.position = 'absolute';
+    form.style.top = '-1000px';
+    form.style.left = '-1000px';
+    form.target = id;
+    form.method = 'POST';
+    form.setAttribute('accept-charset', 'utf-8');
+    area.name = 'd';
+    form.appendChild(area);
+    document.body.appendChild(form);
+
+    this.form = form;
+    this.area = area;
+  }
+
+  this.form.action = this.uri();
+
+  function complete () {
+    initIframe();
+    fn();
+  };
+
+  function initIframe () {
+    if (self.iframe) {
+      try {
+        self.form.removeChild(self.iframe);
+      } catch (e) {
+        self.onError('jsonp polling iframe removal error', e);
+      }
+    }
+
+    try {
+      // ie6 dynamic iframes with target="" support (thanks Chris Lambacher)
+      var html = '<iframe src="javascript:0" name="'+ self.iframeId +'">';
+      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();
+
+  // escape \n to prevent it from being converted into \r\n by some UAs
+  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;
+  }
+};
+
+});
+require.register("engine.io/lib/transports/websocket.js", function(exports, require, module){
+/**
+ * Module dependencies.
+ */
+
+var Transport = require('../transport')
+  , parser = require('engine.io-parser')
+  , util = require('../util')
+  , debug = require('debug')('engine.io-client:websocket');
+
+/**
+ * Module exports.
+ */
+
+module.exports = WS;
+
+/**
+ * Global reference.
+ */
+
+var global = util.global();
+
+/**
+ * WebSocket transport constructor.
+ *
+ * @api {Object} connection options
+ * @api public
+ */
+
+function WS(opts){
+  Transport.call(this, opts);
+};
+
+/**
+ * Inherits from Transport.
+ */
+
+util.inherits(WS, Transport);
+
+/**
+ * Transport name.
+ *
+ * @api public
+ */
+
+WS.prototype.name = 'websocket';
+
+/**
+ * Opens socket.
+ *
+ * @api private
+ */
+
+WS.prototype.doOpen = function(){
+  if (!this.check()) {
+    // let probe timeout
+    return;
+  }
+
+  var self = this;
+
+  this.socket = new (ws())(this.uri());
+  this.socket.onopen = function(){
+    self.onOpen();
+  };
+  this.socket.onclose = function(){
+    self.onClose();
+  };
+  this.socket.onmessage = function(ev){
+    self.onData(ev.data);
+  };
+  this.socket.onerror = function(e){
+    self.onError('websocket error', e);
+  };
+};
+
+/**
+ * Override `onData` to use a timer on iOS.
+ * See: https://gist.github.com/mloughran/2052006
+ *
+ * @api private
+ */
+
+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);
+  };
+}
+
+/**
+ * Writes data to socket.
+ *
+ * @param {Array} array of packets.
+ * @api private
+ */
+
+WS.prototype.write = function(packets){
+  var self = this;
+  this.writable = false;
+  // encodePacket efficient as it uses WS framing
+  // no need for encodePayload
+  for (var i = 0, l = packets.length; i < l; i++) {
+    this.socket.send(parser.encodePacket(packets[i]));
+  }
+  function ondrain() {
+    self.writable = true;
+    self.emit('drain');
+  }
+  // check periodically if we're done sending
+  if ('bufferedAmount' in this.socket) {
+    this.bufferedAmountId = setInterval(function() {
+      if (self.socket.bufferedAmount == 0) {
+        clearInterval(self.bufferedAmountId);
+        ondrain();
+      }
+    }, 50);
+  } else {
+    // fake drain
+    // defer to next tick to allow Socket to clear writeBuffer
+    setTimeout(ondrain, 0);
+  }
+};
+
+/**
+ * Called upon close
+ *
+ * @api private
+ */
+
+WS.prototype.onClose = function(){
+  // stop checking to see if websocket is done sending buffer
+  clearInterval(this.bufferedAmountId);
+  Transport.prototype.onClose.call(this);
+};
+
+/**
+ * Closes socket.
+ *
+ * @api private
+ */
+
+WS.prototype.doClose = function(){
+  if (typeof this.socket !== 'undefined') {
+    this.socket.close();
+  }
+};
+
+/**
+ * Generates uri for connection.
+ *
+ * @api private
+ */
+
+WS.prototype.uri = function(){
+  var query = this.query || {};
+  var schema = this.secure ? 'wss' : 'ws';
+  var port = '';
+
+  // avoid port if default for schema
+  if (this.port && (('wss' == schema && this.port != 443)
+    || ('ws' == schema && this.port != 80))) {
+    port = ':' + this.port;
+  }
+
+  // append timestamp to URI
+  if (this.timestampRequests) {
+    query[this.timestampParam] = +new Date;
+  }
+
+  query = util.qs(query);
+
+  // prepend ? to query
+  if (query.length) {
+    query = '?' + query;
+  }
+
+  return schema + '://' + this.hostname + port + this.path + query;
+};
+
+/**
+ * Feature detection for WebSocket.
+ *
+ * @return {Boolean} whether this transport is available.
+ * @api public
+ */
+
+WS.prototype.check = function(){
+  var websocket = ws();
+  return !!websocket && !('__initialize' in websocket && this.name === WS.prototype.name);
+};
+
+/**
+ * Getter for WS constructor.
+ *
+ * @api private
+ */
+
+function ws(){
+  if ('undefined' == typeof window) {
+    return require('ws');
+  }
+
+  return global.WebSocket || global.MozWebSocket;
+}
+
+});
+require.register("engine.io/lib/transports/flashsocket.js", function(exports, require, module){
+/**
+ * Module dependencies.
+ */
+
+var WS = require('./websocket')
+  , util = require('../util')
+  , debug = require('debug')('engine.io-client:flashsocket');
+
+/**
+ * Module exports.
+ */
+
+module.exports = FlashWS;
+
+/**
+ * Global reference.
+ */
+
+var global = util.global()
+
+/**
+ * Obfuscated key for Blue Coat.
+ */
+
+var xobject = global[['Active'].concat('Object').join('X')];
+
+/**
+ * FlashWS constructor.
+ *
+ * @api public
+ */
+
+function FlashWS (options) {
+  WS.call(this, options);
+  this.flashPath = options.flashPath;
+  this.policyPort = options.policyPort;
+};
+
+/**
+ * Inherits from WebSocket.
+ */
+
+util.inherits(FlashWS, WS);
+
+/**
+ * Transport name.
+ *
+ * @api public
+ */
+
+FlashWS.prototype.name = 'flashsocket';
+
+/**
+ * Opens the transport.
+ *
+ * @api public
+ */
+
+FlashWS.prototype.doOpen = function () {
+  if (!this.check()) {
+    // let the probe timeout
+    return;
+  }
+
+  // instrument websocketjs logging
+  function log (type) {
+    return function(){
+      var str = Array.prototype.join.call(arguments, ' ');
+      debug('[websocketjs %s] %s', type, str);
+    };
+  };
+
+  WEB_SOCKET_LOGGER = { log: log('debug'), error: log('error') };
+  WEB_SOCKET_SUPPRESS_CROSS_DOMAIN_SWF_ERROR = true;
+  WEB_SOCKET_DISABLE_AUTO_INITIALIZATION = true;
+
+  if ('undefined' == typeof WEB_SOCKET_SWF_LOCATION) {
+    WEB_SOCKET_SWF_LOCATION = this.flashPath + 'WebSocketMainInsecure.swf';
+  }
+
+  // dependencies
+  var deps = [this.flashPath + 'web_socket.js'];
+
+  if ('undefined' == typeof swfobject) {
+    deps.unshift(this.flashPath + 'swfobject.js');
+  }
+
+  var self = this;
+
+  load(deps, function () {
+    self.ready(function () {
+      WebSocket.__addTask(function () {
+        WS.prototype.doOpen.call(self);
+      });
+    });
+  });
+};
+
+/**
+ * Override to prevent closing uninitialized flashsocket.
+ *
+ * @api private
+ */
+
+FlashWS.prototype.doClose = function () {
+  if (!this.socket) return;
+  var self = this;
+  WebSocket.__addTask(function() {
+    WS.prototype.doClose.call(self);
+  });
+};
+
+/**
+ * Writes to the Flash socket.
+ *
+ * @api private
+ */
+
+FlashWS.prototype.write = function() {
+  var self = this, args = arguments;
+  WebSocket.__addTask(function () {
+    WS.prototype.write.apply(self, args);
+  });
+};
+
+/**
+ * Called upon dependencies are loaded.
+ *
+ * @api private
+ */
+
+FlashWS.prototype.ready = function (fn) {
+  if (typeof WebSocket == 'undefined' ||
+    !('__initialize' in WebSocket) || !swfobject) {
+    return;
+  }
+
+  if (swfobject.getFlashPlayerVersion().major < 10) {
+    return;
+  }
+
+  function init () {
+    // Only start downloading the swf file when the checked that this browser
+    // actually supports it
+    if (!FlashWS.loaded) {
+      if (843 != self.policyPort) {
+        WebSocket.loadFlashPolicyFile('xmlsocket://' + self.host + ':' + self.policyPort);
+      }
+
+      WebSocket.__initialize();
+      FlashWS.loaded = true;
+    }
+
+    fn.call(self);
+  }
+
+  var self = this;
+  if (document.body) {
+    return init();
+  }
+
+  util.load(init);
+};
+
+/**
+ * Feature detection for flashsocket.
+ *
+ * @return {Boolean} whether this transport is available.
+ * @api public
+ */
+
+FlashWS.prototype.check = function () {
+  if ('undefined' == typeof window) {
+    return false;
+  }
+
+  if (typeof WebSocket != 'undefined' && !('__initialize' in WebSocket)) {
+    return false;
+  }
+
+  if (xobject) {
+    var control = null;
+    try {
+      control = new xobject('ShockwaveFlash.ShockwaveFlash');
+    } catch (e) { }
+    if (control) {
+      return true;
+    }
+  } else {
+    for (var i = 0, l = navigator.plugins.length; i < l; i++) {
+      for (var j = 0, m = navigator.plugins[i].length; j < m; j++) {
+        if (navigator.plugins[i][j].description == 'Shockwave Flash') {
+          return true;
+        }
+      }
+    }
+  }
+
+  return false;
+};
+
+/**
+ * Lazy loading of scripts.
+ * Based on $script by Dustin Diaz - MIT
+ */
+
+var scripts = {};
+
+/**
+ * Injects a script. Keeps tracked of injected ones.
+ *
+ * @param {String} path
+ * @param {Function} callback
+ * @api private
+ */
+
+function create (path, fn) {
+  if (scripts[path]) return fn();
+
+  var el = document.createElement('script');
+  var loaded = false;
+
+  debug('loading "%s"', path);
+  el.onload = el.onreadystatechange = function () {
+    if (loaded || scripts[path]) return;
+    var rs = el.readyState;
+    if (!rs || 'loaded' == rs || 'complete' == rs) {
+      debug('loaded "%s"', path);
+      el.onload = el.onreadystatechange = null;
+      loaded = true;
+      scripts[path] = true;
+      fn();
+    }
+  };
+
+  el.async = 1;
+  el.src = path;
+
+  var head = document.getElementsByTagName('head')[0];
+  head.insertBefore(el, head.firstChild);
+};
+
+/**
+ * Loads scripts and fires a callback.
+ *
+ * @param {Array} paths
+ * @param {Function} callback
+ */
+
+function load (arr, fn) {
+  function process (i) {
+    if (!arr[i]) return fn();
+    create(arr[i], function () {
+      process(++i);
+    });
+  };
+
+  process(0);
+};
+
+});
+require.alias("component-emitter/index.js", "engine.io/deps/emitter/index.js");
+require.alias("component-emitter/index.js", "emitter/index.js");
+
+require.alias("component-indexof/index.js", "engine.io/deps/indexof/index.js");
+require.alias("component-indexof/index.js", "indexof/index.js");
+
+require.alias("LearnBoost-engine.io-protocol/lib/index.js", "engine.io/deps/engine.io-parser/lib/index.js");
+require.alias("LearnBoost-engine.io-protocol/lib/keys.js", "engine.io/deps/engine.io-parser/lib/keys.js");
+require.alias("LearnBoost-engine.io-protocol/lib/index.js", "engine.io/deps/engine.io-parser/index.js");
+require.alias("LearnBoost-engine.io-protocol/lib/index.js", "engine.io-parser/index.js");
+require.alias("LearnBoost-engine.io-protocol/lib/index.js", "LearnBoost-engine.io-protocol/index.js");
+
+require.alias("visionmedia-debug/index.js", "engine.io/deps/debug/index.js");
+require.alias("visionmedia-debug/debug.js", "engine.io/deps/debug/debug.js");
+require.alias("visionmedia-debug/index.js", "debug/index.js");
+
+require.alias("engine.io/lib/index.js", "engine.io/index.js");
+
+if (typeof exports == "object") {
+  module.exports = require("engine.io");
+} else if (typeof define == "function" && define.amd) {
+  define(function(){ return require("engine.io"); });
+} else {
+  this["eio"] = require("engine.io");
+}})();
\ No newline at end of file
diff --git a/client/assets/libs/websocketrpc.js b/client/assets/libs/websocketrpc.js
new file mode 100644 (file)
index 0000000..11d0c07
--- /dev/null
@@ -0,0 +1,262 @@
+/*
+    Create a document explaining the protocol
+*/
+
+function WebsocketRpc(eio_socket) {
+    var self = this;
+
+    this._next_id = 0;
+    this._callbacks = {};
+    this._socket = eio_socket;
+
+    this._root_namespace = this;
+    this._namespaces = new Object(null);
+
+    this._mixinEmitter();
+    this._bindSocketListeners();
+}
+
+
+WebsocketRpc.prototype._bindSocketListeners = function() {
+    var self = this;
+
+    // Proxy the onMessage listener
+    this._onMessageProxy = function rpcOnMessageBoundFunction(){
+        self._onMessage.apply(self, arguments);
+    };
+    this._socket.on('message', this._onMessageProxy);
+};
+
+
+
+WebsocketRpc.prototype.dispose = function() {
+    if (this._onMessageProxy) {
+        this._socket.off('message', this._onMessageProxy);
+        delete this._onMessageProxy;
+    }
+
+    this.disposeNamespaces();
+};
+
+
+WebsocketRpc.prototype.disposeNamespaces = function() {
+    for (var namespace in this._namespaces) {
+        this._namespaces[namespace].dispose();
+        this._namespaces[namespace] = null;
+    }
+};
+
+
+/**
+ * The engine.io socket already has an emitter mixin so steal it from there
+ */
+WebsocketRpc.prototype._mixinEmitter = function() {
+    var funcs = ['on', 'once', 'off', 'removeListener', 'removeAllListeners', 'emit', 'listeners', 'hasListeners'];
+
+    for (var i=0; i<funcs.length; i++) {
+        this[funcs[i]] = this._socket[funcs[i]];
+    }
+};
+
+
+/**
+ * Check if a packet is a valid RPC call
+ */
+WebsocketRpc.prototype._isCall = function(packet) {
+    return (typeof packet.method !== 'undefined' &&
+            typeof packet.params !== 'undefined');
+};
+
+
+/**
+ * Check if a packet is a valid RPC response
+ */
+WebsocketRpc.prototype._isResponse = function(packet) {
+    return (typeof packet.id !== 'undefined' &&
+            typeof packet.response !== 'undefined');
+};
+
+
+
+WebsocketRpc.prototype.namespace = function(namespace) {
+    // Does this namespace already exist?
+    if (this._namespaces[namespace]) {
+        return this._namespaces[namespace];
+    }
+
+    var ns = new WebsocketRpcNamespace(this, namespace);
+    ns._root_namespace = this._root_namespace;
+console.log('Created namespace', namespace);
+    this._namespaces[namespace] = ns;
+
+    return ns;
+};
+
+
+/**
+ * Make an RPC call
+ * First argument must be the method name to call
+ * If the last argument is a function, it is used as a callback
+ * All other arguments are passed to the RPC method
+ * Eg. Rpc.call('namespace.method_name', 1, 2, 3, callbackFn)
+ */
+WebsocketRpc.prototype.call = function(method) {
+    var params, callback, packet;
+
+    // Get a normal array of passed in params
+    params = Array.prototype.slice.call(arguments, 1, arguments.length);
+
+    // If the last param is a function, take it as a callback and strip it out
+    if (typeof params[params.length-1] === 'function') {
+        callback = params[params.length-1];
+        params = params.slice(0, params.length-1);
+    }
+
+    packet = {
+        method: method,
+        params: params
+    };
+
+    if (typeof callback === 'function') {
+        packet.id = this._next_id;
+
+        this._next_id++;
+        this._callbacks[packet.id] = callback;
+    }
+
+    this.send(packet);
+};
+
+
+/**
+ * Encode the packet into JSON and send it over the websocket
+ */
+WebsocketRpc.prototype.send = function(packet) {
+    if (this._socket)
+        this._socket.send(JSON.stringify(packet));
+};
+
+
+/**
+ * Handler for the websocket `message` event
+ */
+WebsocketRpc.prototype._onMessage = function(message_raw) {
+    var self = this,
+        packet,
+        returnFn;
+
+    try {
+        packet = JSON.parse(message_raw);
+        if (!packet) throw 'Corrupt packet';
+    } catch(err) {
+        return;
+    }
+
+    if (this._isResponse(packet)) {
+        this._callbacks[packet.id].apply(this, packet.response);
+        delete this._callbacks[packet.id];
+
+    } else if (this._isCall(packet)) {
+        // Calls with an ID may be responded to
+        if (typeof packet.id !== 'undefined') {
+            returnFn = function returnCallFn() {
+                var value = Array.prototype.slice.call(arguments, 0);
+
+                var ret_packet = {
+                    id: packet.id,
+                    response: value
+                };
+
+                self.send(ret_packet);
+            };
+
+        } else {
+            returnFn = function noop(){};
+        }
+console.log(packet.method);
+        this.emit.apply(this, [packet.method, returnFn].concat(packet.params));
+
+        // Forward the call on to any matching namespaces
+        this._forwardCall(packet, returnFn);
+    }
+};
+
+
+/**
+ * Take a call and forward it on to any matching namespaces
+ */
+WebsocketRpc.prototype._forwardCall = function(packet, returnFn) {
+    var ns;
+
+    for (var namespace in this._namespaces) {
+        ns = this._namespaces[namespace];
+
+        // If the method name is in this namespace, strip the namespace string off
+        // and emit the remaining method on the namespace object
+        // (namespace.api.method_name() becomes method_name())
+        if (packet.method.indexOf(namespace) === 0) {
+            ns.emit.apply(ns, [packet.method.replace(namespace + '.', ''), returnFn].concat(packet.params));
+            ns._forwardCall(packet, returnFn);
+        }
+    }
+};
+
+
+
+
+
+function WebsocketRpcNamespace(parent_rpc, new_namespace) {
+    var self = this,
+        to_bind, i;
+
+    this._namespaces = new Object(null);
+
+    this._parent = parent_rpc;
+    this._namespace = new_namespace;
+
+    if (this._namespace.substr(-1) !== '.')
+        this._namespace += '.';
+
+    // Proxy these functions from _parent, appending our namespace in the first argument
+    to_bind = ['namespace', 'on', 'once', 'off', 'removeListener', 'removeAllListeners', 'emit', 'listeners', 'hasListeners'];
+    for (i=0; i<to_bind.length; i++)
+        this.bindFnOverride(to_bind[i], true);
+
+    this.bindFnOverride('call', false, this._parent);
+
+    // Proxy these functions from _parent
+    to_bind = ['disposeNamespaces', '_forwardCall'];
+    for (i=0; i<to_bind.length; i++)
+        this[to_bind[i]] = this._parent[to_bind[i]];
+}
+
+
+WebsocketRpcNamespace.prototype.bindFnOverride = function(fn_name, append_first_arg, context) {
+    var self = this;
+
+    this[fn_name] = function appendNamespaceFnOverrideBound() {
+        var args;
+
+        if (append_first_arg) {
+            args = Array.prototype.slice.call(arguments, 0, arguments.length);
+            args[0] = self._namespace + args[0];
+        }
+
+        return self._parent[fn_name].apply(context || self, args || arguments);
+    };
+};
+
+
+WebsocketRpcNamespace.prototype.dispose = function() {
+    this.disposeNamespaces();
+    this.call = this.on = this.off = this.namespace = this.disposeNamespaces = null;
+    this._parent = null;
+    this.off();
+};
+
+
+
+// If running a node module, set the exports
+if (typeof module === 'object' && typeof module.exports !== 'undefined') {
+    module.exports = WebsocketRpc;
+}
index a2cc8b2e5a1f16584e323d14b4444cc0b19b74c3..325064c18f58d023898a5e34b505385ea2519b98 100644 (file)
@@ -138,7 +138,11 @@ _kiwi.model.Gateway = function () {
             transport_path;\r
 \r
         this.disconnect_requested = true;\r
-        this.socket.disconnect();\r
+        this.socket.close();\r
+\r
+        this.socket = eio();\r
+        this.connect();\r
+        return;\r
 \r
         // To get around the allow-origin issues for requests, completely reload the\r
         // transport source from the new server\r
@@ -163,26 +167,15 @@ _kiwi.model.Gateway = function () {
     *   @param  {Function}  callback    A callback function to be invoked once Kiwi's server has connected to the IRC server\r
     */\r
     this.connect = function (callback) {\r
-        var resource;\r
-\r
-        // Work out the resource URL for socket.io\r
-        if (_kiwi.app.get('base_path').substr(0, 1) === '/') {\r
-            resource = _kiwi.app.get('base_path');\r
-            resource = resource.substr(1, resource.length-1);\r
-            resource += '/transport';\r
-        } else {\r
-            resource = _kiwi.app.get('base_path') + '/transport';\r
-        }\r
+        this.socket = new reconnectingEioSocket(this.get('kiwi_server'), {\r
+            path: _kiwi.app.get('base_path') + '/transport',\r
+            transports: ['websocket', 'polling', 'flashsocket'],\r
+            reconnect_max_attempts: 5,\r
+            reconnect_delay: 2000\r
+        });\r
 \r
-        this.socket = io.connect(this.get('kiwi_server'), {\r
-            'resource': resource,\r
+        this.rpc = new WebsocketRpc(this.socket);\r
 \r
-            'try multiple transports': true,\r
-            'connect timeout': 3000,\r
-            'max reconnection attempts': 7,\r
-            'reconnection delay': 2000,\r
-            'sync disconnect on unload': false\r
-        });\r
         this.socket.on('connect_failed', function (reason) {\r
             this.socket.disconnect();\r
             this.trigger("connect_fail", {reason: reason});\r
@@ -204,40 +197,36 @@ _kiwi.model.Gateway = function () {
          * A `connect` event is sent from the kiwi server once connected to the\r
          * IRCD and the nick has been accepted.\r
          */\r
-        this.socket.on('connect', function () {\r
+        this.socket.on('open', function () {\r
             // Reset the disconnect_requested flag\r
             that.disconnect_requested = false;\r
 \r
             callback && callback();\r
         });\r
 \r
-        this.socket.on('too_many_connections', function () {\r
+        this.rpc.on('too_many_connections', function () {\r
             that.trigger("connect_fail", {reason: 'too_many_connections'});\r
         });\r
 \r
-        this.socket.on('irc', function (data, callback) {\r
+        this.rpc.on('irc', function (response, data) {\r
             that.parse(data.command, data.data);\r
         });\r
 \r
-        this.socket.on('kiwi', function (data, callback) {\r
+        this.rpc.on('kiwi', function (response, data) {\r
             that.parseKiwi(data.command, data.data);\r
         });\r
 \r
-        this.socket.on('disconnect', function () {\r
-            that.trigger("disconnect", {});\r
-            console.log("_kiwi.gateway.socket.on('disconnect')");\r
-        });\r
-\r
         this.socket.on('close', function () {\r
+            that.trigger("disconnect", {});\r
             console.log("_kiwi.gateway.socket.on('close')");\r
         });\r
 \r
-        this.socket.on('reconnecting', function (reconnectionDelay, reconnectionAttempts) {\r
+        this.socket.on('reconnecting', function (status) {\r
             console.log("_kiwi.gateway.socket.on('reconnecting')");\r
-            that.trigger("reconnecting", {delay: reconnectionDelay, attempts: reconnectionAttempts});\r
+            that.trigger("reconnecting", {delay: status.delay, attempts: status.attempts});\r
         });\r
 \r
-        this.socket.on('reconnect_failed', function () {\r
+        this.socket.on('reconnecting_failed', function () {\r
             console.log("_kiwi.gateway.socket.on('reconnect_failed')");\r
         });\r
     };\r
@@ -294,7 +283,7 @@ _kiwi.model.Gateway = function () {
         if (connection_info.options.encoding)\r
             server_info.encoding = connection_info.options.encoding;\r
 \r
-        this.socket.emit('kiwi', server_info, function (err, server_num) {\r
+        this.rpc.call('kiwi', server_info, function (err, server_num) {\r
             if (!err) {\r
                 callback_fn && callback_fn(err, server_num);\r
 \r
@@ -306,7 +295,8 @@ _kiwi.model.Gateway = function () {
 \r
 \r
     this.isConnected = function () {\r
-        return this.socket.socket.connected;\r
+        // TODO: Check this. Might want to use .readyState\r
+        return this.socket;\r
     };\r
 \r
 \r
@@ -400,7 +390,7 @@ _kiwi.model.Gateway = function () {
             server: connection_id,\r
             data: JSON.stringify(data)\r
         };\r
-        this.socket.emit('irc', data_buffer, callback);\r
+        this.rpc.call('irc', data_buffer, callback);\r
     };\r
 \r
     /**\r
index d829f944d6fb405524dd4cd3a83920746fec70f7..9899c926cdde2970b080b699f61d25f534a0865d 100644 (file)
@@ -14,28 +14,12 @@ _kiwi.model.NewConnection = Backbone.Collection.extend({
 
         this.view.networkConnecting();
 
-        
-        // If we don't have socket.io loaded, load it before opening a new connection
-        if (!window.io) {
-            // Path to get the socket.io transport code
-            transport_path = _kiwi.app.kiwi_server + _kiwi.app.get('base_path') + '/transport/socket.io.js?ts='+(new Date().getTime());
-                        
-            $script(transport_path, function() {
-                if (!window.io) {
-                    that.onKiwiServerNotFound();
-                    return;
-                }
-
-                _kiwi.gateway.set('kiwi_server', _kiwi.app.kiwi_server + '/kiwi');
-                _kiwi.gateway.connect(function() {
-                    that.makeConnection(new_connection_event);
-                });
-            });
-
-        } else {
-            this.makeConnection(new_connection_event);
 
-        }
+        _kiwi.gateway.set('kiwi_server', _kiwi.app.kiwi_server);
+        _kiwi.gateway.connect(function() {
+            that.makeConnection(new_connection_event);
+        });
+
 
     },
 
index c9c13827234c946411f6b13a0136f7d01a0275ae..707ca54052936cd98a69c23ce16218118df7a24c 100755 (executable)
@@ -4,17 +4,19 @@ var util             = require('util'),
     _                = require('lodash'),
     State            = require('./irc/state.js'),
     IrcConnection    = require('./irc/connection.js').IrcConnection,
-    ClientCommands   = require('./clientcommands.js');
+    ClientCommands   = require('./clientcommands.js'),
+    WebsocketRpc     = require('./websocketrpc.js');
 
 
 var Client = function (websocket) {
     var that = this;
-    
+
     events.EventEmitter.call(this);
     this.websocket = websocket;
+    this.rpc = new WebsocketRpc(this.websocket);
 
     // Clients address
-    this.real_address = this.websocket.handshake.real_address;
+    this.real_address = this.websocket.kiwi.real_address;
 
     // A hash to identify this client instance
     this.hash = crypto.createHash('sha256')
@@ -22,24 +24,24 @@ var Client = function (websocket) {
         .update('' + Date.now())
         .update(Math.floor(Math.random() * 100000).toString())
         .digest('hex');
-    
+
     this.state = new State(this);
-    
+
     this.buffer = {
         list: [],
         motd: ''
     };
-    
+
     // Handler for any commands sent from the client
     this.client_commands = new ClientCommands(this);
 
-    websocket.on('irc', function () {
-        handleClientMessage.apply(that, arguments);
+    this.rpc.on('irc', function (response, data) {
+        handleClientMessage.call(that, data, response);
     });
-    websocket.on('kiwi', function () {
-        kiwiCommand.apply(that, arguments);
+    this.rpc.on('kiwi', function (response, data) {
+        kiwiCommand.call(that, data, response);
     });
-    websocket.on('disconnect', function () {
+    websocket.on('close', function () {
         websocketDisconnect.apply(that, arguments);
     });
     websocket.on('error', function () {
@@ -60,12 +62,12 @@ module.exports.Client = Client;
 
 Client.prototype.sendIrcCommand = function (command, data, callback) {
     var c = {command: command, data: data};
-    this.websocket.emit('irc', c, callback);
+    this.rpc.call('irc', c, callback);
 };
 
 Client.prototype.sendKiwiCommand = function (command, data, callback) {
     var c = {command: command, data: data};
-    this.websocket.emit('kiwi', c, callback);
+    this.rpc.call('kiwi', c, callback);
 };
 
 Client.prototype.dispose = function () {
@@ -128,7 +130,7 @@ function kiwiCommand(command, callback) {
                         global.config.restrict_server_ssl :
                         command.ssl),
                     command.nick,
-                    {hostname: this.websocket.handshake.revdns, address: this.websocket.handshake.real_address},
+                    {hostname: this.websocket.kiwi.revdns, address: this.websocket.kiwi.real_address},
                     options,
                     callback);
             } else {
@@ -144,7 +146,7 @@ function kiwiCommand(command, callback) {
 // Websocket has disconnected, so quit all the IRC connections
 function websocketDisconnect() {
     this.emit('disconnect');
-    
+
     this.dispose();
 }
 
index f7293d37670b0260e48fa39f1184359ef5715d51..9e83ccc07814674216f7728a24218a0044d88a63 100644 (file)
@@ -51,6 +51,7 @@ HttpHandler.prototype.serve = function (request, response) {
 
     // Any asset request to head into the asset dir
     request.url = request.url.replace(base_path + '/assets/', '/assets/');
+console.log('request url:', request.url);
 
     // Any requests for /client to load the index file
     if (request.url.match(new RegExp('^' + base_path_regex + '([/$]|$)', 'i'))) {
@@ -231,7 +232,7 @@ function generateSettings(request, debug, callback) {
                 'src/models/memberlist.js',
                 'src/models/network.js'
             ],
-            
+
             [
                 'src/models/query.js',
                 'src/models/channel.js',
index b316258d57cc97ae9a93920d4e6f104b12292819..ed307474a72b05015f6c9b55c74d7aed4ee74156 100644 (file)
@@ -1,17 +1,18 @@
-var ws          = require('socket.io'),
-    events      = require('events'),
-    http        = require('http'),
-    https       = require('https'),
-    util        = require('util'),
-    fs          = require('fs'),
-    dns         = require('dns'),
-    url         = require('url'),
-    _           = require('lodash'),
-    spdy        = require('spdy'),
-    ipaddr      = require('ipaddr.js'),
-    Client      = require('./client.js').Client,
-    HttpHandler = require('./httphandler.js').HttpHandler,
-    rehash      = require('./rehash.js');
+var engine       = require('engine.io'),
+    WebsocketRpc = require('./websocketrpc.js');
+    events       = require('events'),
+    http         = require('http'),
+    https        = require('https'),
+    util         = require('util'),
+    fs           = require('fs'),
+    dns          = require('dns'),
+    url          = require('url'),
+    _            = require('lodash'),
+    spdy         = require('spdy'),
+    ipaddr       = require('ipaddr.js'),
+    Client       = require('./client.js').Client,
+    HttpHandler  = require('./httphandler.js').HttpHandler,
+    rehash       = require('./rehash.js');
 
 
 
@@ -60,8 +61,6 @@ var WebListener = function (web_config, transports) {
 
         hs = spdy.createServer(opts, handleHttpRequest);
 
-        // Start socket.io listening on this weblistener
-        this.ws = ws.listen(hs, _.extend({ssl: true}, ws_opts));
         hs.listen(web_config.port, web_config.address, function () {
             that.emit('listening');
         });
@@ -70,8 +69,6 @@ var WebListener = function (web_config, transports) {
         // Start some plain-text server up
         hs = http.createServer(handleHttpRequest);
 
-        // Start socket.io listening on this weblistener
-        this.ws = ws.listen(hs, _.extend({ssl: false}, ws_opts));
         hs.listen(web_config.port, web_config.address, function () {
             that.emit('listening');
         });
@@ -81,17 +78,35 @@ var WebListener = function (web_config, transports) {
         that.emit('error', err);
     });
 
-    this.ws.enable('browser client minification');
-    this.ws.enable('browser client etag');
-    this.ws.set('transports', transports);
-    this.ws.set('resource', (global.config.http_base_path || '') + '/transport');
+    this.ws = engine.attach(hs, {
+        transports: ['websocket', 'polling', 'flashsocket'],
+        path: (global.config.http_base_path || '') + '/transport'
+    });
+    console.log((global.config.http_base_path || '') + '/transport');
 
-    this.ws.of('/kiwi').authorization(authoriseConnection)
-        .on('connection', function () {
-            newConnection.apply(that, arguments);
-        }
-    );
-    this.ws.of('/kiwi').on('error', console.log);
+    //this.ws.enable('browser client minification');
+    //this.ws.enable('browser client etag');
+    //this.ws.set('transports', transports);
+    //this.ws.set('resource', (global.config.http_base_path || '') + '/transport');
+
+    this.ws.on('connection', function(socket) {
+        console.log('Connection!');
+        initialiseSocket(socket, function(err, authorised) {
+            var client;
+
+            if (!authorised) {
+                socket.close();
+                return;
+            }
+
+            client = new Client(socket);
+            client.on('dispose', function () {
+                that.emit('client_dispose', this);
+            });
+
+            that.emit('connection', client);
+        });
+    });
 };
 util.inherits(WebListener, events.EventEmitter);
 
@@ -123,11 +138,16 @@ function rangeCheck(addr, range) {
  * Get the reverse DNS entry for this connection.
  * Used later on for webirc, etc functionality
  */
-function authoriseConnection(handshakeData, callback) {
-    var address = handshakeData.address.address;
+function initialiseSocket(socket, callback) {
+    var request = socket.request,
+        address = request.connection.remoteAddress;
+
+    // Key/val data stored to the socket to be read later on
+    // May also be synced to a redis DB to lookup clients
+    socket.kiwi = {};
 
     // If a forwarded-for header is found, switch the source address
-    if (handshakeData.headers[global.config.http_proxy_ip_header || 'x-forwarded-for']) {
+    if (request.headers[global.config.http_proxy_ip_header || 'x-forwarded-for']) {
         // Check we're connecting from a whitelisted proxy
         if (!global.config.http_proxies || !rangeCheck(address, global.config.http_proxies)) {
             console.log('Unlisted proxy:', address);
@@ -136,10 +156,10 @@ function authoriseConnection(handshakeData, callback) {
         }
 
         // We're sent from a whitelisted proxy, replace the hosts
-        address = handshakeData.headers['x-forwarded-for'];
+        address = request.headers[global.config.http_proxy_ip_header || 'x-forwarded-for'];
     }
 
-    handshakeData.real_address = address;
+    socket.kiwi.real_address = address;
 
     // If enabled, don't go over the connection limit
     if (global.config.max_client_conns && global.config.max_client_conns > 0) {
@@ -152,29 +172,20 @@ function authoriseConnection(handshakeData, callback) {
     try {
         dns.reverse(address, function (err, domains) {
             if (err || domains.length === 0) {
-                handshakeData.revdns = address;
+                socket.kiwi.revdns = address;
             } else {
-                handshakeData.revdns = _.first(domains) || address;
+                socket.kiwi.revdns = _.first(domains) || address;
             }
 
             // All is well, authorise the connection
             callback(null, true);
         });
     } catch (err) {
-        handshakeData.revdns = address;
+        socket.kiwi.revdns = address;
         callback(null, true);
     }
 }
 
-function newConnection(websocket) {
-    var client, that = this;
-    client = new Client(websocket);
-    client.on('dispose', function () {
-        that.emit('client_dispose', this);
-    });
-    this.emit('connection', client);
-}
-
 
 
 
diff --git a/server/websocketrpc.js b/server/websocketrpc.js
new file mode 100644 (file)
index 0000000..1fc5815
--- /dev/null
@@ -0,0 +1,161 @@
+/*
+    Create a document explaining the protocol
+*/
+
+function WebsocketRpc(eio_socket) {
+    var self = this;
+
+    this._next_id = 0;
+    this._callbacks = {};
+    this._socket = eio_socket;
+
+    this._mixinEmitter();
+    this._bindSocketListeners();
+}
+
+
+WebsocketRpc.prototype._bindSocketListeners = function() {
+    var self = this;
+
+    // Proxy the onMessage listener
+    this._onMessageProxy = function rpcOnMessageBoundFunction(){
+        self._onMessage.apply(self, arguments);
+    };
+    this._socket.on('message', this._onMessageProxy);
+};
+
+
+
+WebsocketRpc.prototype.dispose = function() {
+    if (this._onMessageProxy) {
+        this._socket.off('message', this._onMessageProxy);
+        delete this._onMessageProxy;
+    }
+};
+
+
+
+
+/**
+ * The engine.io socket already has an emitter mixin so steal it from there
+ */
+WebsocketRpc.prototype._mixinEmitter = function() {
+    var funcs = ['on', 'once', 'off', 'removeListener', 'removeAllListeners', 'emit', 'listeners', 'hasListeners'];
+
+    for (var i=0; i<funcs.length; i++) {
+        this[funcs[i]] = this._socket[funcs[i]];
+    }
+};
+
+
+/**
+ * Check if a packet is a valid RPC call
+ */
+WebsocketRpc.prototype._isCall = function(packet) {
+    return (typeof packet.method !== 'undefined' &&
+            typeof packet.params !== 'undefined');
+};
+
+
+/**
+ * Check if a packet is a valid RPC response
+ */
+WebsocketRpc.prototype._isResponse = function(packet) {
+    return (typeof packet.id !== 'undefined' &&
+            typeof packet.response !== 'undefined');
+};
+
+
+
+/**
+ * Make an RPC call
+ * First argument must be the method name to call
+ * If the last argument is a function, it is used as a callback
+ * All other arguments are passed to the RPC method
+ * Eg. Rpc.call('namespace.method_name', 1, 2, 3, callbackFn)
+ */
+WebsocketRpc.prototype.call = function(method) {
+    var params, callback, packet;
+
+    // Get a normal array of passed in params
+    params = Array.prototype.slice.call(arguments, 1, arguments.length);
+
+    // If the last param is a function, take it as a callback and strip it out
+    if (typeof params[params.length-1] === 'function') {
+        callback = params[params.length-1];
+        params = params.slice(0, params.length-1);
+    }
+
+    packet = {
+        method: method,
+        params: params
+    };
+
+    if (typeof callback === 'function') {
+        packet.id = this._next_id;
+
+        this._next_id++;
+        this._callbacks[packet.id] = callback;
+    }
+
+    this.send(packet);
+};
+
+
+/**
+ * Encode the packet into JSON and send it over the websocket
+ */
+WebsocketRpc.prototype.send = function(packet) {
+    if (this._socket)
+        this._socket.send(JSON.stringify(packet));
+};
+
+
+/**
+ * Handler for the websocket `message` event
+ */
+WebsocketRpc.prototype._onMessage = function(message_raw) {
+    var self = this,
+        packet,
+        returnFn;
+
+    try {
+        packet = JSON.parse(message_raw);
+        if (!packet) throw 'Corrupt packet';
+    } catch(err) {
+        return;
+    }
+
+    if (this._isResponse(packet)) {
+        this._callbacks[packet.id].apply(this, packet.response);
+        delete this._callbacks[packet.id];
+
+    } else if (this._isCall(packet)) {
+        // Calls with an ID may be responded to
+        if (typeof packet.id !== 'undefined') {
+            returnFn = function returnCallFn() {
+                var value = Array.prototype.slice.call(arguments, 0);
+
+                var ret_packet = {
+                    id: packet.id,
+                    response: value
+                };
+
+                self.send(ret_packet);
+            };
+
+        } else {
+            returnFn = function noop(){};
+        }
+
+        this.emit.apply(this, [packet.method, returnFn].concat(packet.params));
+    }
+};
+
+
+
+
+// If running a node module, set the exports
+if (typeof module === 'object' && typeof module.exports !== 'undefined') {
+    module.exports = WebsocketRpc;
+}