diff --git a/History.md b/History.md
new file mode 100644
index 0000000000..c8aa68fa88
--- /dev/null
+++ b/History.md
@@ -0,0 +1,5 @@
+
+0.0.1 / 2010-01-03
+==================
+
+ * Initial release
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000000..c304ec2896
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,15 @@
+
+ALL_TESTS = $(shell find test/ -name '*.test.js')
+
+run-tests:
+ @./support/expresso/bin/expresso \
+ -I support/should.js/lib \
+ -I support \
+ -I lib \
+ --serial \
+ $(TESTS)
+
+test:
+ @$(MAKE) TESTS="$(ALL_TESTS)" run-tests
+
+.PHONY: test
diff --git a/Readme.md b/Readme.md
new file mode 100644
index 0000000000..454e7dd7c8
--- /dev/null
+++ b/Readme.md
@@ -0,0 +1,283 @@
+
+# Socket.IO
+
+Socket.IO is a Node.JS project that makes WebSockets and realtime possible in
+all browsers. It also enhances WebSockets by providing built-in multiplexing,
+horizontal scalability, automatic JSON encoding/decoding, and more.
+
+## How to Install
+
+ npm install socket.io
+
+## How to use
+
+First, require `socket.io`:
+
+ var io = require('socket.io');
+
+Next, attach it to a HTTP/HTTPS server. If you're using the fantastic `express`
+web framework:
+
+ var app = express.createServer();
+ , sockets = io.listen(app);
+
+ app.listen(80);
+
+ sockets.on('connection', function (socket) {
+ socket.send({ hello: 'world' });
+ });
+
+Finally, load it from the client side code:
+
+
+
+
+For more thorough examples, look at the `examples/` directory.
+
+## Short recipes
+
+### Sending and receiving events.
+
+Socket.IO allows you to emit and receive custom events.
+Besides `connect`, `message` and `disconnect`, you can emit custom events:
+
+ // note, io.listen() will create a http server for you
+ var io = require('socket.io');
+ , sockets = io.listen(80);
+
+ sockets.on('connection', function (socket) {
+ sockets.emit('this', { will: 'be received by everyone');
+
+ socket.on('private message', function (from, msg) {
+ console.log('I received a private message by ', from, ' saying ', msg);
+ });
+
+ socket.on('disconnect', function () {
+ sockets.emit('user disconnected');
+ });
+ });
+
+### Storing data associated to a client
+
+Sometimes it's necessary to store data associated with a client that's
+necessary for the duration of the session.
+
+#### Server side
+
+ var io = require('socket.io')
+ , sockets = io.listen(80);
+
+ sockets.on('connection', function (socket) {
+ socket.on('set nickname', function (name) {
+ socket.set('nickname', name, function () { socket.emit('ready'); });
+ });
+
+ socket.on('msg', function () {
+ socket.get('nickname', function (name) {
+ console.log('Chat message by ', name);
+ });
+ });
+ });
+
+#### Client side
+
+
+
+### Restricting yourself to a namespace
+
+If you have control over all the messages and events emitted for a particular
+application, using the default `/` namespace works.
+
+If you want to leverage 3rd-party code, or produce code to share with others,
+socket.io provides a way of namespacing a `socket`.
+
+This has the benefit of `multiplexing` a single connection. Instead of
+socket.io using two `WebSocket` connections, it'll use one.
+
+The following example defines a socket that listens on '/chat' and one for
+'/news':
+
+#### Server side
+
+ var io = require('socket.io')
+ , sockets = io.listen(80);
+
+ var chat = sockets
+ .for('/chat');
+ .on('connection', function (socket) {
+ socket.emit('a message', { that: 'only', '/chat': 'will get' });
+ chat.emit('a message', { everyone: 'in', '/chat': 'will get' });
+ });
+
+ var news = sockets
+ .for('/news');
+ .on('connection', function (socket) {
+ socket.emit('item', { news: 'item' });
+ });
+
+#### Client side:
+
+
+
+### Sending volatile messages.
+
+Sometimes certain messages can be dropped. Let's say you have an app that
+shows realtime tweets for the keyword `bieber`.
+
+If a certain client is not ready to receive messages (because of network slowness
+or other issues, or because he's connected through long polling and is in the
+middle of a request-response cycle), if he doesn't receive ALL the tweets related
+to bieber your application won't suffer.
+
+In that case, you might want to send those messages as volatile messages.
+
+#### Server side
+
+ var io = require('socket.io')
+ , sockets = io.listen(80);
+
+ sockets.on('connection', function (socket) {
+ var tweets = setInterval(function () {
+ getBieberTweet(function (tweet) {
+ socket.volatile.emit('bieber tweeet', tweet);
+ });
+ }, 100);
+
+ socket.on('disconnect', function () {
+ clearInterval(tweets);
+ });
+ });
+
+#### Client side
+
+In the client side, messages are received the same way whether they're volatile
+or not.
+
+### Getting acknowledgements
+
+Sometimes, you might want to get a callback when the client confirmed the message
+receiption.
+
+To do this, simply pass a function as the last parameter of `.send` or `.emit`.
+What's more, you can also perform a manual acknowledgement, like in the example
+below. Socket.IO won't perform a manual acknowledgement when the arity of the
+function is `0` when you `emit` or `send`.
+
+#### Server side
+
+ var io = require('socket.io')
+ , sockets = io.listen(80);
+
+ sockets.on('connection', function (socket) {
+ socket.on('ferret', function (name, fn) {
+ fn('woot');
+ });
+ });
+
+#### Client side
+
+
+
+### Using it just as a cross-browser WebSocket
+
+If you just want the WebSocket semantics, you can do that too.
+Simply leverage `send` and listen on the `message` event:
+
+#### Server side
+
+ var io = require('socket.io')
+ , sockets = io.listen(80);
+
+#### Client side
+
+
+
+### Changing configuration
+
+Configuration in socket.io is TJ-style:
+
+#### Server side
+
+ var io = require('socket.io')
+ , sockets = io.listen(80);
+
+ sockets.configure(function () {
+ sockets.set('transports', ['websocket', 'flashsocket', 'xhr-polling']);
+ });
+
+ sockets.configure('development', function () {
+ sockets.set('transports', ['websocket', 'xhr-polling']);
+ sockets.enable('log');
+ });
+
+## [API docs](http://socket.io/api.html)
+
+## License
+
+(The MIT License)
+
+Copyright (c) 2011 Guillermo Rauch <guillermo@learnboost.com>
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+'Software'), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/index.js b/index.js
new file mode 100644
index 0000000000..cc00c103e2
--- /dev/null
+++ b/index.js
@@ -0,0 +1,8 @@
+
+/*!
+ * socket.io-node
+ * Copyright(c) 2011 LearnBoost
+ * MIT Licensed
+ */
+
+module.exports = require('./lib/socket.io');
diff --git a/lib/client b/lib/client
new file mode 160000
index 0000000000..ec023737a4
--- /dev/null
+++ b/lib/client
@@ -0,0 +1 @@
+Subproject commit ec023737a4d54df1fe6e6f198cd87ed8677b6676
diff --git a/lib/logger.js b/lib/logger.js
new file mode 100644
index 0000000000..8e156ebe5e
--- /dev/null
+++ b/lib/logger.js
@@ -0,0 +1,96 @@
+
+/*!
+ * socket.io-node
+ * Copyright(c) 2011 LearnBoost
+ * MIT Licensed
+ */
+
+/**
+ * Module dependencies.
+ */
+
+const util = require('./util')
+ , toArray = util.toArray;
+
+/**
+ * Log levels.
+ */
+
+const levels = [
+ 'error'
+ , 'warn'
+ , 'info'
+ , 'debug'
+];
+
+/**
+ * Colors for log levels.
+ */
+
+const colors = [
+ 31
+ , 33
+ , 36
+ , 90
+];
+
+/**
+ * Pads the nice output to the longest log level.
+ */
+
+function pad (str) {
+ var max = 0;
+
+ for (var i = 0, l = levels.length; i < l; i++)
+ max = Math.max(max, levels[i].length);
+
+ if (str.length < max)
+ return str + new Array(max - str.length + 1).join(' ');
+
+ return str;
+};
+
+/**
+ * Logger (console).
+ *
+ * @api public
+ */
+
+var Logger = module.exports = function (opts) {
+ opts = opts || {}
+ this.colors = false !== opts.colors;
+ this.level = 3;
+};
+
+/**
+ * Log method.
+ *
+ * @api public
+ */
+
+Logger.prototype.log = function (type) {
+ var index = levels.indexOf(type);
+
+ if (index > this.level)
+ return this;
+
+ console.error.apply(
+ console
+ , [this.colors
+ ? ' \033[' + colors[index] + 'm' + pad(type) + ' -\033[39m'
+ : type + ':'
+ ].concat(toArray(arguments).slice(1))
+ );
+
+ return this;
+};
+
+/**
+ * Generate methods.
+ */
+
+levels.forEach(function (name) {
+ Logger.prototype[name] = function () {
+ this.log.apply(this, [name].concat(toArray(arguments)));
+ };
+});
diff --git a/lib/manager.js b/lib/manager.js
new file mode 100644
index 0000000000..6822194c4f
--- /dev/null
+++ b/lib/manager.js
@@ -0,0 +1,553 @@
+
+/*!
+ * socket.io-node
+ * Copyright(c) 2011 LearnBoost
+ * MIT Licensed
+ */
+
+/**
+ * Module dependencies.
+ */
+
+const http = require('http')
+ , https = require('https')
+ , fs = require('fs')
+ , qs = require('querystring')
+ , url = require('url')
+ , util = require('./util')
+ , store = require('./store')
+ , transports = require('./transports')
+ , Logger = require('./logger')
+ , Socket = require('./socket')
+ , MemoryStore = require('./stores/memory')
+ , SocketNamespace = require('./namespace');
+
+/**
+ * Export the constructor.
+ */
+
+exports = module.exports = Manager;
+
+/**
+ * Default transports.
+ */
+
+var defaultTransports = exports.defaultTransports = [
+ 'websocket'
+ , 'flashsocket'
+ , 'htmlfile'
+ , 'xhr-polling'
+ , 'jsonp-polling'
+];
+
+/**
+ * Inherited defaults.
+ */
+
+var parent = module.parent.exports
+ , protocol = parent.protocol
+ , clientVersion = parent.clientVersion;
+
+/**
+ * Manager constructor.
+ *
+ * @param {HTTPServer} server
+ * @param {Object} options, optional
+ * @api public
+ */
+
+function Manager (server) {
+ this.server = server;
+ this.namespaces = {};
+ this.sockets = new SocketNamespace(this);
+ this.settings = {
+ origins: '*:*'
+ , log: true
+ , store: new MemoryStore
+ , logger: new Logger
+ , heartbeats: true
+ , resource: '/socket.io'
+ , transports: defaultTransports
+ , authorization: false
+ , 'log level': 3
+ , 'close timeout': 15
+ , 'heartbeat timeout': 15
+ , 'heartbeat interval': 20
+ , 'polling duration': 50
+ , 'flash policy server': true
+ , 'destroy upgrade': true
+ };
+
+ // reset listeners
+ this.oldListeners = server.listeners('request');
+ server.removeAllListeners('request');
+
+ var self = this;
+
+ server.on('request', function (req, res) {
+ self.handleRequest(req, res);
+ });
+
+ server.on('upgrade', function (req, socket, head) {
+ self.handleUpgrade(req, socket, head);
+ });
+
+ this.log.info('socket.io started');
+};
+
+/**
+ * Store accessor shortcut.
+ *
+ * @api public
+ */
+
+Manager.prototype.__defineGetter__('store', function () {
+ var store = this.get('store');
+ store.manager = this;
+ return store;
+});
+
+/**
+ * Logger accessor.
+ *
+ * @api public
+ */
+
+Manager.prototype.__defineGetter__('log', function () {
+ if (this.disabled('log')) return;
+
+ var logger = this.get('logger');
+ logger.level = this.set('log level');
+
+ return logger;
+});
+
+/**
+ * Get settings.
+ *
+ * @api public
+ */
+
+Manager.prototype.get = function (key) {
+ return this.settings[key];
+};
+
+/**
+ * Set settings
+ *
+ * @api public
+ */
+
+Manager.prototype.set = function (key, value) {
+ if (arguments.length == 1) return this.get(key);
+ this.settings[key] = value;
+ return this;
+};
+
+/**
+ * Enable a setting
+ *
+ * @api public
+ */
+
+Manager.prototype.enable = function (key) {
+ this.settings[key] = true;
+ return this;
+};
+
+/**
+ * Disable a setting
+ *
+ * @api public
+ */
+
+Manager.prototype.disable = function (key) {
+ this.settings[key] = false;
+ return this;
+};
+
+/**
+ * Checks if a setting is enabled
+ *
+ * @api public
+ */
+
+Manager.prototype.enabled = function (key) {
+ return !!this.settings[key];
+};
+
+/**
+ * Checks if a setting is disabled
+ *
+ * @api public
+ */
+
+Manager.prototype.disabled = function (key) {
+ return !this.settings[key];
+};
+
+/**
+ * Configure callbacks.
+ *
+ * @api public
+ */
+
+Manager.prototype.configure = function (env, fn) {
+ if ('function' == typeof env)
+ env();
+ else if (env == process.env.NODE_ENV)
+ fn();
+
+ return this;
+};
+
+/**
+ * Handles an HTTP request.
+ *
+ * @api private
+ */
+
+Manager.prototype.handleRequest = function (req, res) {
+ var data = this.checkRequest(req);
+
+ if (!data) {
+ this.log.debug('ignoring request outside socket.io namespace');
+
+ for (var i = 0, l = this.oldListeners.length; i < l; i++)
+ this.oldListeners[i].call(this, req, res);
+
+ return;
+ }
+
+ if (!data.transport && !data.protocol) {
+ if (data.path == '/socket.io.js') {
+ this.handleClientRequest(req, res);
+ } else {
+ res.writeHead(200);
+ res.end('Welcome to socket.io.');
+
+ this.log.info('unhandled socket.io url');
+ }
+
+ return;
+ }
+
+ if (data.protocol != protocol) {
+ res.writeHead(500);
+ res.end('Protocol version not supported.');
+
+ this.log.info('client protocol version unsupported');
+ } else {
+ // flag the connection
+ if (!req.connection.__io)
+ req.connection.__io = 1;
+ else
+ req.connection.__io++;
+
+ if (data.id) {
+ this.handleHTTPRequest(data, req, res);
+ } else {
+ this.handleHandshake(data, req, res);
+ }
+ }
+};
+
+/**
+ * Handles an HTTP Upgrade.
+ *
+ * @api private
+ */
+
+Manager.prototype.handleUpgrade = function (req, socket, head) {
+ var data = this.checkRequest(req)
+ , self = this;
+
+ if (!data) {
+ if (this.enabled('destroy upgrade')) {
+ socket.end();
+ this.log.debug('destroying non-socket.io upgrade');
+ }
+
+ return;
+ }
+
+ req.head = head;
+ this.handleClient(data, req);
+};
+
+/**
+ * Handles a normal handshaken HTTP request (eg: long-polling)
+ *
+ * @api private
+ */
+
+Manager.prototype.handleHTTPRequest = function (data, req, res) {
+ req.res = res;
+ this.handleClient(data, req);
+};
+
+/**
+ * Intantiantes a new client.
+ *
+ * @api private
+ */
+
+Manager.prototype.handleClient = function (data, req) {
+ var socket = req.socket
+ , self = this;
+
+ if (!socket.__ioTransport)
+ socket.__ioTransport = new transports[data.transport](this, data);
+
+ var transport = socket.__ioTransport;
+ transport.pause();
+ transport.request = req;
+
+ if (!~this.get('transports').indexOf(data.transport)) {
+ transport.error('transport not supported', 'reconnect');
+ return;
+ }
+
+ if (!this.verifyOrigin(req)) {
+ transport.error('unauthorized');
+ return;
+ }
+
+ this.store.isHandshaken(data.id, function (err, handshaken) {
+ if (handshaken) {
+ if (data.query.disconnect) {
+ self.log.error('handle forced disconnection here');
+ } else {
+ self.store.client(data.id).count(function (err, count) {
+ if (count == 1)
+ self.sockets.emit('connection', new Socket(self, data.id, ''));
+
+ transport.resume();
+ });
+ }
+ } else {
+ transport.error('client not handshaken');
+ }
+ });
+};
+
+/**
+ * Serves the client.
+ *
+ * @api private
+ */
+
+Manager.prototype.handleClientRequest = function (req, res) {
+ var self = this;
+
+ function serve () {
+ if (!self.clientLength)
+ self.clientLength = Buffer.byteLength(self.client);
+
+ var headers = {
+ 'Content-Type': 'application/javascript'
+ , 'Content-Length': self.clientLength
+ };
+
+ if (self.clientEtag)
+ headers.ETag = self.clientEtag;
+
+ res.writeHead(200, headers);
+ res.end(self.client);
+
+ self.log.debug('served client');
+ };
+
+ if (!this.client) {
+ if (this.get('browser client')) {
+ this.client = this.get('browser client');
+ this.clientEtag = this.get('browser client etag');
+
+ this.log.debug('caching custom client');
+
+ serve();
+ } else {
+ var self = this;
+
+ fs.readFile(__dirname + '/client/socket.io.min.js', function (err, data) {
+ if (err) {
+ res.writeHead(500);
+ res.end('Error serving socket.io client.');
+
+ self.log.warn('Can\'t cache socket.io client');
+ return;
+ }
+
+ self.client = data.toString();
+ self.clientEtag = clientVersion;
+ self.log.debug('caching', clientVersion, 'client');
+
+ serve();
+ });
+ }
+ }
+};
+
+/**
+ * Handles a handshake request.
+ *
+ * @api private
+ */
+
+Manager.prototype.handleHandshake = function (data, req, res) {
+ var self = this;
+
+ function error (err) {
+ res.writeHead(500);
+ res.end('Handshake error');
+
+ self.log.warn('handshake error ' + err);
+ };
+
+ this.authorize(data, function (err, authorized) {
+ if (err) return error(err);
+
+ self.log.info('handshake ' + (authorized ? 'authorized' : 'unauthorized'));
+
+ if (authorized) {
+ self.store.handshake(data, function (err, id) {
+ if (err) return error(err);
+
+ res.writeHead(200);
+ res.end([
+ id
+ , self.get('heartbeat timeout') || ''
+ , self.get('close timeout') || ''
+ , self.transports(data).join(',')
+ ].join(':'));
+ });
+ } else {
+ res.writeHead(403);
+ res.end('Handshake unauthorized');
+
+ self.log.info('handshake unauthorized');
+ }
+ })
+};
+
+/**
+ * Verifies the origin of a request.
+ *
+ * @api private
+ */
+
+Manager.prototype.verifyOrigin = function (request) {
+ var origin = request.headers.origin
+ , origins = this.get('origins');
+
+ if (origin === 'null') origin = '*';
+
+ if (origins.indexOf('*:*') !== -1) {
+ return true;
+ }
+
+ if (origin) {
+ try {
+ var parts = url.parse(origin);
+
+ return
+ ~origins.indexOf(parts.host + ':' + parts.port) ||
+ ~origins.indexOf(parts.host + ':*') ||
+ ~origins.indexOf('*:' + parts.port);
+ } catch (ex) {}
+ }
+
+ return false;
+};
+
+/**
+ * Performs authentication.
+ *
+ * @param Object client request data
+ * @api private
+ */
+
+Manager.prototype.authorize = function (data, fn) {
+ if (this.get('authorization')) {
+ var self = this;
+
+ this.get('authorization').call(this, data, function (err, authorized) {
+ self.log.debug('client ' + authorized ? 'authorized' : 'unauthorized');
+ fn(err, authorized);
+ })
+ } else {
+ this.log.debug('client authorized');
+ fn(null, true);
+ }
+
+ return this;
+};
+
+/**
+ * Retrieves the transports adviced to the user.
+ *
+ * @api private
+ */
+
+Manager.prototype.transports = function (data) {
+ var transp = this.get('transports')
+ , ret = [];
+
+ for (var i = 0, l = transp.length; i < l; i++) {
+ var transport = transp[i];
+
+ if (transport) {
+ if (!transport.checkClient || !transport.checkClient(data)) {
+ ret.push(transport);
+ }
+ }
+ }
+
+ return ret;
+};
+
+/**
+ * Checks whether a request is a socket.io one.
+ *
+ * @return {Object} a client request data object or `false`
+ * @api private
+ */
+
+Manager.prototype.checkRequest = function (req) {
+ var resource = this.get('resource')
+ , url = req.url;
+
+ if (url.substr(0, resource.length) == resource) {
+ var path = url.substr(resource.length)
+ , pieces = path.match(/^\/([^\/]+)\/?([^\/]+)?\/?([^\/]+)?\/?$/);
+
+ // client request data
+ var data = {
+ query: req.url.query ? qs.parse(req.url.query) : {}
+ , headers: req.headers
+ , request: req
+ , path: path
+ };
+
+ if (pieces) {
+ data.protocol = Number(pieces[1]);
+ data.transport = pieces[2];
+ data.id = pieces[3];
+ };
+
+ return data;
+ }
+
+ return false;
+};
+
+/**
+ * Declares a socket namespace
+ */
+
+Manager.prototype.for = function (nsp) {
+ if (this.namespaces[nsp])
+ return this.namespaces[nsp];
+
+ return this.namespaces[nsp] = new SocketNamespace(this, nsp);
+};
diff --git a/lib/namespace.js b/lib/namespace.js
new file mode 100644
index 0000000000..423230daa2
--- /dev/null
+++ b/lib/namespace.js
@@ -0,0 +1,62 @@
+
+/**
+ * Module dependencies.
+ */
+
+const EventEmitter = process.EventEmitter;
+
+/**
+ * Exports the constructor.
+ */
+
+exports = module.exports = SocketNamespace;
+
+/**
+ * Constructor.
+ *
+ * @api public.
+ */
+
+function SocketNamespace (mgr, nsp) {
+ this.manager = mgr;
+ this.nsp = nsp || '';
+ this.flags = {};
+};
+
+/**
+ * Inherits from EventEmitter.
+ */
+
+SocketNamespace.prototype.__proto__ = EventEmitter.prototype;
+
+/**
+ * JSON message flag.
+ *
+ * @api public
+ */
+
+SocketNamespace.prototype.__defineGetter__('json', function () {
+ this.flags.json = true;
+ return this;
+});
+
+/**
+ * Volatile message flag.
+ *
+ * @api public
+ */
+
+SocketNamespace.prototype.__defineGetter__('volatile', function () {
+ this.flags.volatile = true;
+ return this;
+});
+
+/**
+ * Writes to everyone.
+ *
+ * @api public
+ */
+
+SocketNamespace.prototype.send = function () {
+ this.flags = {};
+};
diff --git a/lib/parser.js b/lib/parser.js
new file mode 100644
index 0000000000..fc9d2aeb06
--- /dev/null
+++ b/lib/parser.js
@@ -0,0 +1,243 @@
+
+/*!
+ * socket.io-node
+ * Copyright(c) 2011 LearnBoost
+ * MIT Licensed
+ */
+
+/**
+ * Module dependencies.
+ */
+
+/**
+ * Packet types.
+ */
+
+const packets = exports.packets = [
+ 'disconnect'
+ , 'connect'
+ , 'heartbeat'
+ , 'message'
+ , 'json'
+ , 'event'
+ , 'ack'
+ , 'error'
+];
+
+/**
+ * Errors reasons.
+ */
+
+const reasons = exports.reasons = [
+ 'transport not supported'
+ , 'client not handshaken'
+ , 'unauthorized'
+];
+
+/**
+ * Errors advice.
+ */
+
+const advice = exports.advice = [
+ 'reconnect'
+];
+
+/**
+ * Encodes a packet.
+ *
+ * @api private
+ */
+
+exports.encodePacket = function (packet) {
+ var type = packets.indexOf(packet.type)
+ , id = packet.id || ''
+ , endpoint = packet.endpoint || ''
+ , ack = packet.ack
+ , data = null;
+
+ switch (packet.type) {
+ case 'error':
+ var reason = packet.reason ? reasons.indexOf(packet.reason) : ''
+ , adv = packet.advice ? advice.indexOf(packet.advice) : ''
+
+ if (reason !== '' || adv !== '')
+ data = reason + (adv !== '' ? ('+' + adv) : '')
+
+ break;
+
+ case 'message':
+ if (packet.data !== '')
+ data = packet.data;
+ break;
+
+ case 'event':
+ var params = packet.args && packet.args.length
+ ? JSON.stringify(packet.args) : '';
+ data = packet.name + (params !== '' ? ('\ufffd' + params) : '');
+ break;
+
+ case 'json':
+ data = JSON.stringify(packet.data);
+ break;
+
+ case 'connect':
+ if (packet.qs)
+ data = packet.qs;
+ break;
+
+ case 'ack':
+ data = packet.ackId
+ + (packet.args && packet.args.length
+ ? '+' + JSON.stringify(packet.args) : '');
+ break;
+
+ case 'heartbeat':
+ case 'disconect':
+ break;
+ }
+
+ // construct packet with required fragments
+ var encoded = [
+ type
+ , id + (ack == 'data' ? '+' : '')
+ , endpoint
+ ];
+
+ // data fragment is optional
+ if (data !== null && data !== undefined)
+ encoded.push(data);
+
+ return encoded.join(':');
+};
+
+/**
+ * Encodes multiple messages (payload).
+ *
+ * @param {Array} messages
+ * @api private
+ */
+
+exports.encodePayload = function (packets) {
+ var decoded = '';
+
+ if (packets.length == 1)
+ return packets[0];
+
+ for (var i = 0, l = packets.length; i < l; i++) {
+ var packet = packets[i];
+ decoded += '\ufffd' + packet.length + '\ufffd' + packets[i]
+ }
+
+ return decoded;
+};
+
+/**
+ * Decodes a packet
+ *
+ * @api private
+ */
+
+var regexp = /^([^:]+):([0-9]+)?(\+)?:([^:]+)?:?(.*)?$/;
+
+exports.decodePacket = function (data) {
+ var pieces = data.match(regexp);
+
+ if (!pieces) return {};
+
+ var id = pieces[2] || ''
+ , data = pieces[5] || ''
+ , packet = {
+ type: packets[pieces[1]]
+ , endpoint: pieces[4] || ''
+ };
+
+ // whether we need to acknowledge the packet
+ if (id) {
+ packet.id = id;
+ if (pieces[3])
+ packet.ack = 'data';
+ else
+ packet.ack = true;
+ }
+
+ // handle different packet types
+ switch (packet.type) {
+ case 'error':
+ var pieces = data.split('+');
+ packet.reason = reasons[pieces[0]] || '';
+ packet.advice = advice[pieces[1]] || '';
+ break;
+
+ case 'message':
+ packet.data = data || '';
+ break;
+
+ case 'event':
+ var pieces = data.match(/([^\ufffd]+)(\ufffd)?(.*)/);
+ packet.name = pieces[1] || '';
+ packet.args = [];
+
+ if (pieces[3]) {
+ try {
+ packet.args = JSON.parse(pieces[3]);
+ } catch (e) { }
+ }
+ break;
+
+ case 'json':
+ try {
+ packet.data = JSON.parse(data);
+ } catch (e) { }
+ break;
+
+ case 'connect':
+ packet.qs = data || '';
+ break;
+
+ case 'ack':
+ var pieces = data.match(/^([0-9]+)(\+)?(.*)/);
+ if (pieces) {
+ packet.ackId = pieces[1];
+ packet.args = [];
+
+ if (pieces[3]) {
+ try {
+ packet.args = pieces[3] ? JSON.parse(pieces[3]) : [];
+ } catch (e) { }
+ }
+ }
+ break;
+
+ case 'disconnect':
+ case 'heartbeat':
+ break;
+ };
+
+ return packet;
+};
+
+/**
+ * Decodes data payload. Detects multiple messages
+ *
+ * @return {Array} messages
+ * @api public
+ */
+
+exports.decodePayload = function (data) {
+ if (data[0] == '\ufffd') {
+ var ret = [];
+
+ for (var i = 1, length = ''; i < data.length; i++) {
+ if (data[i] == '\ufffd') {
+ ret.push(exports.decodePacket(data.substr(i + 1).substr(0, length)));
+ i += Number(length);
+ } else {
+ length += data[i];
+ }
+ }
+
+ return ret;
+ } else {
+ return [exports.decodePacket(data)];
+ }
+};
diff --git a/lib/socket.io.js b/lib/socket.io.js
new file mode 100644
index 0000000000..6b6de1ba1b
--- /dev/null
+++ b/lib/socket.io.js
@@ -0,0 +1,106 @@
+
+/*!
+ * socket.io-node
+ * Copyright(c) 2011 LearnBoost
+ * MIT Licensed
+ */
+
+/**
+ * Module dependencies.
+ */
+
+const http = require('http')
+ , https = require('https')
+ , client = require('./client/lib/io');
+
+/**
+ * Version.
+ */
+
+exports.version = '0.7.0';
+
+/**
+ * Supported protocol version.
+ */
+
+exports.protocol = 1;
+
+/**
+ * Client that we serve.
+ */
+
+exports.clientVersion = client.version;
+
+/**
+ * Attaches a manager
+ *
+ * @api public
+ */
+
+exports.listen = function (server, options) {
+ if ('undefined' == typeof server) {
+ // create a server that listens on port 80
+ server = 80;
+ }
+
+ if ('number' == typeof server) {
+ // if a port number is passed
+ var port = server;
+
+ if (options && options.key)
+ server = https.createServer(options, server);
+ else
+ server = http.createServer();
+
+ // default response
+ server.on('request', function (req, res) {
+ res.writeHead(200);
+ res.end('Welcome to socket.io.');
+ });
+
+ server.listen(port);
+ }
+
+ // otherwise assume a http/s server
+ return new exports.Manager(server);
+};
+
+/**
+ * Manager constructor.
+ *
+ * @api public
+ */
+
+exports.Manager = require('./manager');
+
+/**
+ * Transport constructor.
+ *
+ * @api public
+ */
+
+exports.Transport = require('./transport');
+
+/**
+ * Socket constructor.
+ *
+ * @api public
+ */
+
+exports.Socket = require('./socket');
+
+/**
+ * Store constructor.
+ *
+ * @api public
+ */
+
+exports.Store = require('./store');
+
+/**
+ * Parser.
+ *
+ * @api public
+ */
+
+exports.parser = require('./parser');
diff --git a/lib/socket.js b/lib/socket.js
new file mode 100644
index 0000000000..7d1d9108c0
--- /dev/null
+++ b/lib/socket.js
@@ -0,0 +1,225 @@
+
+/*!
+ * socket.io-node
+ * Copyright(c) 2011 LearnBoost
+ * MIT Licensed
+ */
+
+/**
+ * Module dependencies.
+ */
+
+const util = require('./util')
+ , EventEmitter = process.EventEmitter;
+
+/**
+ * Export the constructor.
+ */
+
+exports = module.exports = Socket;
+
+/**
+ * Reserved event names.
+ */
+
+const events = {
+ message: 1
+ , connect: 1
+ , disconnect: 1
+ , open: 1
+ , close: 1
+ , error: 1
+ , retry: 1
+ , reconnect: 1
+};
+
+/**
+ * ReadyStates -> event map.
+ */
+
+const readyStates = {
+ 0: 'disconnect'
+ , 1: 'connect'
+ , 2: 'open'
+ , 3: 'close'
+};
+
+/**
+ * Socket constructor.
+ *
+ * @api public
+ */
+
+function Socket (manager, id, nsp, readonly) {
+ this.id = id;
+ this.namespace = nsp;
+ this.manager = manager;
+ this.flags = {};
+ this.rs = 1;
+ this.packets = 0;
+
+ if (!readonly)
+ this.store.on('message:' + id, function () {
+
+ });
+};
+
+/**
+ * Inherits from EventEmitter.
+ */
+
+Socket.prototype.__proto__ = EventEmitter.prototype;
+
+/**
+ * readyState getter.
+ *
+ * @api private
+ */
+
+Socket.prototype.__defineGetter__('readyState', function (st) {
+ return this.rs;
+});
+
+/**
+ * readyState setter.
+ *
+ * @api private
+ */
+
+Socket.prototype.__defineSetter__('readyState', function (state) {
+ this.rs = state;
+ this.emit('readyState', state);
+ this.emit(readyStates[state]);
+});
+
+/**
+ * Accessor shortcut for the store.
+ *
+ * @api private
+ */
+
+Socket.prototype.__defineGetter__('store', function () {
+ return this.manager.store;
+});
+
+/**
+ * JSON message flag.
+ *
+ * @api public
+ */
+
+Socket.prototype.__defineGetter__('json', function () {
+ this.flags.json = true;
+ return this;
+});
+
+/**
+ * Volatile message flag.
+ *
+ * @api public
+ */
+
+Socket.prototype.__defineGetter__('volatile', function () {
+ this.flags.volatile = true;
+ return this;
+});
+
+/**
+ * Transmits a packet.
+ *
+ * @api private
+ */
+
+Socket.prototype.packet = function (msg, volatile) {
+ if (volatile) {
+ this.store.publish('volatile:' + this.id, msg);
+ } else {
+ this.store.client(this.id).publish(msg);
+ }
+
+ return this;
+};
+
+/**
+ * Stores data for the client.
+ *
+ * @api public
+ */
+
+Socket.prototype.set = function (key, value, fn) {
+ this.store.client(this.id).set(key, value, fn);
+ return this;
+};
+
+/**
+ * Retrieves data for the client
+ *
+ * @api public
+ */
+
+Socket.prototype.get = function (key, fn) {
+ this.store.client(this.id).get(key, fn);
+ return this;
+};
+
+/**
+ * Kicks client
+ *
+ * @api public
+ */
+
+Socket.prototype.disconnect = function () {
+ this.packet({ type: 'disconnect' });
+ return this;
+};
+
+/**
+ * Send a message.
+ *
+ * @api public
+ */
+
+Socket.prototype.send = function (data, fn) {
+ var packet = {
+ type: this.flags.json ? 'json' : 'message'
+ , data: data
+ };
+
+ if (fn) {
+ packet.id = ++this.packets;
+ packet.ack = fn.length ? 'data' : true;
+ }
+
+ this.packet(packet, this.flags.volatile);
+ this.flags = {};
+ return this;
+};
+
+/**
+ * Emit override for custom events.
+ *
+ * @api public
+ */
+
+Socket.prototype.emit = function (ev) {
+ if (events.ev)
+ return EventEmitter.protype.emit.apply(this, arguments);
+
+ var args = util.toArray(arguments).slice(1)
+ , lastArg = args[args.length - 1];
+
+ // prepare packet to send
+ var packet = {
+ type: 'event'
+ , args: args
+ };
+
+ if ('function' == typeof lastArg) {
+ packet.id = ++this.packets;
+ packet.ack = lastArg.length ? 'data' : true;
+ }
+
+ this.packet(packet, this.flags.volatile);
+ this.flags = {};
+ return this;
+};
diff --git a/lib/store.js b/lib/store.js
new file mode 100644
index 0000000000..2887628e1d
--- /dev/null
+++ b/lib/store.js
@@ -0,0 +1,55 @@
+
+/*!
+ * socket.io-node
+ * Copyright(c) 2011 LearnBoost
+ * MIT Licensed
+ */
+
+/**
+ * Module dependencies.
+ */
+
+const EventEmitter = process.EventEmitter;
+
+/**
+ * Expose the constructor.
+ */
+
+exports = module.exports = Store;
+
+/**
+ * Store interface
+ *
+ * @api public
+ */
+
+function Store () {};
+
+/**
+ * Inherits from EventEmitter
+ */
+
+Store.prototype.__proto__ = EventEmitter.prototype;
+
+/**
+ * Log accessor.
+ *
+ * @api public
+ */
+
+Store.prototype.__defineGetter__('log', function () {
+ return this.manager.log;
+});
+
+/**
+ * Client.
+ *
+ * @api public
+ */
+
+Store.Client = function (store, id) {
+ this.store = store;
+ this.id = id;
+ this.buffer = [];
+ this.dict = {};
+};
diff --git a/lib/stores/memory.js b/lib/stores/memory.js
new file mode 100644
index 0000000000..777d2bc288
--- /dev/null
+++ b/lib/stores/memory.js
@@ -0,0 +1,252 @@
+
+/*!
+ * socket.io-node
+ * Copyright(c) 2011 LearnBoost
+ * MIT Licensed
+ */
+
+/**
+ * Module dependencies.
+ */
+
+const crypto = require('crypto')
+ , Store = require('../store')
+
+/**
+ * Exports the constructor.
+ */
+
+exports = module.exports = Memory;
+Memory.Client = Client;
+
+/**
+ * Memory store
+ *
+ * @api public
+ */
+
+function Memory (opts) {
+ this.handshaken = [];
+ this.clients = {};
+};
+
+/**
+ * Inherits from Store.
+ */
+
+Memory.prototype.__proto__ = Store.prototype;
+
+/**
+ * Handshake a client.
+ *
+ * @param {Object} client request object
+ * @param {Function} callback
+ * @api public
+ */
+
+Memory.prototype.handshake = function (data, fn) {
+ var id = this.generateId();
+ this.handshaken.push(id);
+ fn(null, id);
+ return this;
+};
+
+/**
+ * Checks if a client is handshaken.
+ *
+ * @api public
+ */
+
+Memory.prototype.isHandshaken = function (id, fn) {
+ fn(null, ~this.handshaken.indexOf(id));
+ return this;
+};
+
+/**
+ * Generates a random id.
+ *
+ * @api private
+ */
+
+Memory.prototype.generateId = function () {
+ var rand = String(Math.random() * Math.random() * Date.now());
+ return crypto.createHash('md5').update(rand).digest('hex');
+};
+
+/**
+ * Retrieves a client store instance.
+ *
+ * @api public
+ */
+
+Memory.prototype.client = function (id) {
+ if (!this.clients[id]) {
+ this.clients[id] = new Memory.Client(this, id);
+ this.log.debug('initializing client store for', id);
+ }
+
+ return this.clients[id];
+};
+
+/**
+ * Destroys a client store.
+ *
+ * @api public
+ */
+
+Memory.prototype.destroy = function (id) {
+ this.clients[id].destroy();
+ this.clients[id] = null;
+};
+
+/**
+ * Simple publish
+ *
+ * @api public
+ */
+
+Memory.prototype.publish = function (ev, data) {
+ this.emit(ev, data);
+ return this;
+};
+
+/**
+ * Simple subscribe
+ *
+ * @api public
+ */
+
+Memory.prototype.subscribe = function (chn, fn) {
+ this.on(chn, fn);
+ return this;
+};
+
+/**
+ * Simple unsubscribe
+ *
+ * @api public
+ */
+
+Memory.prototype.unsubscribe = function (chn) {
+ this.removeAllListeners(chn);
+};
+
+/**
+ * Client constructor
+ *
+ * @api private
+ */
+
+function Client () {
+ Store.Client.apply(this, arguments);
+ this.reqs = 0;
+ this.paused = true;
+};
+
+/**
+ * Inherits from Store.Client
+ */
+
+Client.prototype.__proto__ = Store.Client;
+
+/**
+ * Counts transport requests.
+ *
+ * @api public
+ */
+
+Client.prototype.count = function (fn) {
+ fn(null, ++this.reqs);
+ return this;
+};
+
+/**
+ * Sets up queue consumption
+ *
+ * @api public
+ */
+
+Client.prototype.consume = function (fn) {
+ this.paused = false;
+
+ if (this.buffer.length) {
+ fn(this.buffer, null);
+ this.buffer = [];
+ } else {
+ this.consumer = fn;
+ }
+
+ return this;
+};
+
+/**
+ * Publishes a message to be sent to the client.
+ *
+ * @String encoded message
+ * @api public
+ */
+
+Client.prototype.publish = function (msg) {
+ if (this.paused) {
+ this.buffer.push(msg);
+ } else {
+ this.consumer(null, msg);
+ }
+
+ return this;
+};
+
+/**
+ * Pauses the stream.
+ *
+ * @api public
+ */
+
+Client.prototype.pause = function () {
+ this.paused = true;
+ return this;
+};
+
+/**
+ * Destroys the client.
+ *
+ * @api public
+ */
+
+Client.prototype.destroy = function () {
+ this.buffer = null;
+};
+
+/**
+ * Gets a key
+ *
+ * @api public
+ */
+
+Client.prototype.get = function (key, fn) {
+ fn(null, this.dict[key]);
+ return this;
+};
+
+/**
+ * Sets a key
+ *
+ * @api public
+ */
+
+Client.prototype.set = function (key, value, fn) {
+ this.dict[key] = value;
+ fn(null);
+ return this;
+};
+
+/**
+ * Emits a message incoming from client.
+ *
+ * @api private
+ */
+
+Client.prototype.onMessage = function (msg) {
+ this.store.emit('message:' + id, msg);
+};
+
diff --git a/lib/transport.js b/lib/transport.js
new file mode 100644
index 0000000000..d5f9003c7f
--- /dev/null
+++ b/lib/transport.js
@@ -0,0 +1,376 @@
+
+/*!
+ * socket.io-node
+ * Copyright(c) 2011 LearnBoost
+ * MIT Licensed
+ */
+
+/**
+ * Module dependencies.
+ */
+
+const parser = require('./parser');
+
+/**
+ * Expose the constructor.
+ */
+
+exports = module.exports = Transport;
+
+/**
+ * Transport constructor.
+ *
+ * @api public
+ */
+
+function Transport (mng, data) {
+ this.manager = mng;
+ this.id = data.id;
+ this.paused = true;
+};
+
+/**
+ * Sets the corresponding request object.
+ */
+
+Transport.prototype.__defineSetter__('request', function (req) {
+ this.log.debug('setting request');
+ this.handleRequest(req);
+});
+
+/**
+ * Access the logger.
+ *
+ * @api public
+ */
+
+Transport.prototype.__defineGetter__('log', function () {
+ return this.manager.log;
+});
+
+/**
+ * Access the store.
+ *
+ * @api public
+ */
+
+Transport.prototype.__defineGetter__('store', function () {
+ return this.manager.store;
+});
+
+/**
+ * Handles a request when it's set.
+ *
+ * @api private
+ */
+
+Transport.prototype.handleRequest = function (req) {
+ this.req = req;
+
+ if (req.method == 'GET') {
+ this.socket = req.socket;
+ this.open = true;
+
+ var self = this;
+
+ this.log.debug('publishing that', this.id, 'connected');
+ this.store.publish('open:' + this.id, function () {
+ self.store.once('open:' + this.id, function () {
+ self.log.debug('request for existing session connection change');
+ self.end();
+ });
+
+ if (!self.paused)
+ self.subscribe();
+ });
+
+ if (!req.socket.__ioHandler) {
+ // add a handler only once per socket
+ this.socket.setNoDelay(true);
+ this.socket.on('end', this.onSocketEnd.bind(this));
+ this.socket.on('error', this.onSocketError.bind(this));
+ this.socket.__ioHandler = true;
+ }
+ }
+};
+
+/**
+ * Called when the connection dies
+ *
+ * @api private
+ */
+
+Transport.prototype.onSocketEnd = function () {
+ if (this.open) {
+ this.setCloseTimeout();
+ }
+};
+
+/**
+ * Called when the connection has an error.
+ *
+ * @api private
+ */
+
+Transport.prototype.onSocketError = function (err) {
+ if (this.open) {
+ this.end();
+ this.socket.destroy();
+ }
+
+ this.log.info('socket error, should set close timeout');
+};
+
+/**
+ * Sets the close timeout.
+ */
+
+Transport.prototype.setCloseTimeout = function () {
+ if (!this.closeTimeout) {
+ var self = this;
+
+ this.closeTimeout = setTimeout(function () {
+ self.log.debug('fired close timeout for client', self.id);
+ self.closeTimeout = null;
+ self.socket.end();
+ self.end();
+ }, this.manager.get('close timeout') * 1000);
+
+ this.log.debug('set close timeout for client', this.id);
+ }
+};
+
+/**
+ * Clears the close timeout.
+ */
+
+Transport.prototype.clearCloseTimeout = function () {
+ if (this.closeTimeout) {
+ clearTimeout(this.closeTimeout);
+ this.closeTimeout = null;
+
+ this.log.debug('cleared close timeout for client', this.id);
+ }
+};
+
+/**
+ * Sets the heartbeat timeout
+ */
+
+Transport.prototype.setHeartbeatTimeout = function () {
+ if (!this.heartbeatTimeout) {
+ var self = this;
+
+ this.heartbeatTimeout = setTimeout(function () {
+ self.log.debug('fired heartbeat timeout for client', self.id);
+ self.heartbeatTimeout = null;
+ self.end();
+ }, this.manager.get('hearbeat timeout') * 1000);
+
+ this.log.debug('set heartbeat timeout for client', this.id);
+ }
+};
+
+/**
+ * Clears the heartbeat timeout
+ *
+ * @param text
+ */
+
+Transport.prototype.clearHeartbeatTimeout = function () {
+ if (this.heartbeatTimeout) {
+ clearTimeout(this.heartbeatTimeout);
+ this.heartbeatTimeout = null;
+ this.log.debug('cleared heartbeat timeout for client', this.id);
+ }
+};
+
+/**
+ * Sets the heartbeat interval. To be called when a connection opens and when
+ * a heartbeat is received.
+ *
+ * @api private
+ */
+
+Transport.prototype.setHeartbeatInterval = function () {
+ if (!this.heartbeatTimeout) {
+ var self = this;
+
+ this.heartbeatInterval = setTimeout(function () {
+ self.log.debug('emitting heartbeat for client', self.id);
+ self.packet({ type: 'heartbeat' });
+ }, this.manager.get('heartbeat interval') * 1000);
+
+ this.log.debug('set heartbeat interval for client', this.id);
+ }
+};
+
+/**
+ * Sends a heartbeat
+ *
+ * @api private
+ */
+
+Transport.prototype.hearbeat = function () {
+};
+
+
+/**
+ * Clears the heartbeat interval
+ *
+ * @api private
+ */
+
+Transport.prototype.clearHeartbeatInterval = function () {
+ if (this.heartbeatInterval) {
+ clearTimeout(this.heartbeatInterval);
+ this.heartbeatInterval = null;
+ this.log.debug('cleared heartbeat interval for client', this.id);
+ }
+};
+
+/**
+ * Finishes the connection and makes sure client doesn't reopen
+ *
+ * @api private
+ */
+
+Transport.prototype.disconnect = function () {
+ this.packet({ type: 'disconnect' });
+ this.end();
+
+ return this;
+};
+
+/**
+ * Cleans up the connection, considers the client disconnected.
+ *
+ * @api private
+ */
+
+Transport.prototype.end = function () {
+ this.log.info('ending socket');
+
+ this.clearCloseTimeout();
+ this.clearHeartbeatTimeout();
+ this.clearHeartbeatInterval();
+
+ if (this.subscribed) {
+ this.unsubscribe();
+ this.store.destroy(this.id);
+ }
+
+ this.open = false;
+};
+
+/**
+ * Signals that the transport can start flushing buffers.
+ *
+ * @api public
+ */
+
+Transport.prototype.resume = function () {
+ this.paused = false;
+ this.subscribe();
+ return this;
+};
+
+/**
+ * Signals that the transport should pause and buffer data.
+ *
+ * @api public
+ */
+
+Transport.prototype.pause = function () {
+ this.paused = true;
+ return this;
+};
+
+/**
+ * Writes an error packet with the specified reason and advice.
+ *
+ * @param {Number} advice
+ * @param {Number} reason
+ * @api public
+ */
+
+Transport.prototype.error = function (reason, advice) {
+ this.packet({
+ type: 'error'
+ , reason: reason
+ , advice: advice
+ });
+
+ this.log.warn(reason, advice ? ('client should ' + advice) : '');
+ this.end();
+};
+
+/**
+ * Write a packet.
+ *
+ * @api public
+ */
+
+Transport.prototype.packet = function (obj) {
+ return this.write(parser.encodePacket(obj));
+};
+
+/**
+ * Subscribe client.
+ *
+ * @api private
+ */
+
+Transport.prototype.subscribe = function () {
+ if (!this.subscribed) {
+ this.log.debug('subscribing', this.id);
+
+ var self = this;
+
+ // subscribe to buffered + normal messages
+ this.store.client(this.id).consume(function (payload, packet) {
+ if (payload) {
+ self.payload(payload.map(function (packet) {
+ return parser.encodePacket(packet);
+ }));
+ } else {
+ self.write(packet);
+ }
+ });
+
+ // subscribe to volatile messages
+ self.store.subscribe('volatile:' + this.id, function (packet) {
+ self.writeVolatile(packet);
+ });
+
+ this.subscribed = true;
+ }
+};
+
+/**
+ * Unsubscribe client.
+ *
+ * @api private
+ */
+
+Transport.prototype.unsubscribe = function () {
+ if (this.subscribed) {
+ this.log.info('unsubscribing', this.id);
+
+ this.store.unsubscribe('volatile:' + this.id);
+ this.store.client(this.id).pause();
+ this.subscribed = false;
+ }
+};
+
+/**
+ * Handles a message
+ *
+ * @param {Object} message object
+ * @api private
+ */
+
+Transport.prototype.onMessage = function (msg) {
+ this.store.publish('message:' + this.id, msg);
+};
+
diff --git a/lib/transports/htmlfile.js b/lib/transports/htmlfile.js
new file mode 100644
index 0000000000..db69dba8df
--- /dev/null
+++ b/lib/transports/htmlfile.js
@@ -0,0 +1,68 @@
+
+/*!
+ * socket.io-node
+ * Copyright(c) 2011 LearnBoost
+ * MIT Licensed
+ */
+
+/**
+ * Module requirements.
+ */
+
+const HTTPTransport = require('./http');
+
+/**
+ * Export the constructor.
+ */
+
+exports = module.exports = HTMLFile;
+
+/**
+ * HTMLFile transport constructor.
+ *
+ * @api public
+ */
+
+function HTMLFile (data, request) {
+ HTTPTransport.call(this, data, request);
+};
+
+/**
+ * Inherits from Transport.
+ */
+
+HTMLFile.prototype.__proto__ = HTTPTransport.prototype;
+
+/**
+ * Handles the request.
+ *
+ * @api private
+ */
+
+HTMLFile.prototype.handleRequest = function (req) {
+ HTTPTransport.prototype.handleRequest.call(this, req);
+
+ if (req.method == 'GET') {
+ req.res.writeHead(200, {
+ 'Content-Type': 'text/html',
+ 'Connection': 'keep-alive',
+ 'Transfer-Encoding': 'chunked'
+ });
+
+ req.res.write('' + new Array(245).join(' '));
+ }
+};
+
+
+/**
+ * Performs the write.
+ *
+ * @api private
+ */
+
+HTMLFile.prototype.doWrite = function (data) {
+ data = '';
+
+ this.response.write(data);
+ this.log.debug('writing', data);
+};
diff --git a/lib/transports/http-polling.js b/lib/transports/http-polling.js
new file mode 100644
index 0000000000..ce33094ecd
--- /dev/null
+++ b/lib/transports/http-polling.js
@@ -0,0 +1,128 @@
+
+/*!
+ * socket.io-node
+ * Copyright(c) 2011 LearnBoost
+ * MIT Licensed
+ */
+
+/**
+ * Module requirements.
+ */
+
+const HTTPTransport = require('./http');
+
+/**
+ * Exports the constructor.
+ */
+
+exports = module.exports = HTTPPolling;
+
+/**
+ * HTTP polling constructor.
+ *
+ * @api public.
+ */
+
+function HTTPPolling (mng, data) {
+ HTTPTransport.call(this, mng, data);
+};
+
+/**
+ * Inherits from HTTPTransport.
+ *
+ * @api public.
+ */
+
+HTTPPolling.prototype.__proto__ = HTTPTransport.prototype;
+
+/**
+ * Removes heartbeat timeouts for polling.
+ */
+
+HTTPPolling.prototype.setHeartbeatTimeout = function () {
+ return;
+};
+
+/**
+ * Removes the heartbeat timeouts for polling.
+ */
+
+HTTPPolling.prototype.clearHeartbeatTimeout = function () {
+ return;
+};
+
+/**
+ * Handles a request
+ *
+ * @api private
+ */
+
+HTTPPolling.prototype.handleRequest = function (req) {
+ HTTPTransport.prototype.handleRequest.call(this, req);
+
+ if (req.method == 'GET') {
+ var self = this;
+
+ this.pollTimeout = setTimeout(function () {
+ self.write('');
+ self.log.debug('polling closed due to exceeded duration');
+ }, this.manager.get('polling duration') * 1000);
+
+ this.log.debug('setting poll timeout');
+ }
+};
+
+/**
+ * Performs a write.
+ *
+ * @api private.
+ */
+
+HTTPPolling.prototype.write = function (data, close) {
+ if (this.pollTimeout) {
+ clearTimeout(this.pollTimeout);
+ this.pollTimeout = null;
+ }
+
+ this.doWrite(data);
+ this.response.end();
+ this.setCloseTimeout();
+ this.unsubscribe();
+ this.open = false;
+};
+
+/**
+ * Performs a volatile write
+ *
+ * @api private
+ */
+
+HTTPPolling.prototype.writeVolatile = HTTPPolling.prototype.write;
+
+/**
+ * Handles data payload.
+ *
+ * @api private
+ */
+
+HTTPPolling.prototype.onData = function (data) {
+ var messages = parser.decodeMany(data);
+
+ for (var i = 0, l = messages.length; i < l; i++) {
+ this.onMessage(messages[i]);
+ }
+};
+
+/**
+ * Override end.
+ *
+ * @api private
+ */
+
+HTTPPolling.prototype.end = function () {
+ if (this.open)
+ this.response.end();
+
+ return HTTPTransport.prototype.end.call(this);
+};
+
diff --git a/lib/transports/http.js b/lib/transports/http.js
new file mode 100644
index 0000000000..82a02f5cd7
--- /dev/null
+++ b/lib/transports/http.js
@@ -0,0 +1,70 @@
+
+/*!
+ * socket.io-node
+ * Copyright(c) 2011 LearnBoost
+ * MIT Licensed
+ */
+
+/**
+ * Module requirements.
+ */
+
+const Transport = require('../transport')
+ , parser = require('../parser');
+
+/**
+ * Export the constructor.
+ */
+
+exports = module.exports = HTTPTransport;
+
+/**
+ * HTTP interface constructor. For all non-websocket transports.
+ *
+ * @api public
+ */
+
+function HTTPTransport (mng, data) {
+ Transport.call(this, mng, data);
+};
+
+/**
+ * Inherits from Transport.
+ */
+
+HTTPTransport.prototype.__proto__ = Transport.prototype;
+
+/**
+ * Handles a request.
+ *
+ * @api private
+ */
+
+HTTPTransport.prototype.handleRequest = function (req) {
+ if (req.method == 'POST') {
+ var buffer = ''
+ , res = req.res;
+
+ res.on('data', function (data) {
+ buffer += data;
+ });
+
+ res.on('end', function () {
+ self.onData(buffer);
+ });
+ } else {
+ this.response = req.res;
+ this.open = true;
+ Transport.prototype.handleRequest.call(this, req);
+ }
+};
+
+/**
+ * Writes a payload of messages
+ *
+ * @api private
+ */
+
+HTTPTransport.prototype.payload = function (msgs) {
+ this.write(parser.encodePayload(msgs));
+};
diff --git a/lib/transports/index.js b/lib/transports/index.js
new file mode 100644
index 0000000000..79c12a005d
--- /dev/null
+++ b/lib/transports/index.js
@@ -0,0 +1,11 @@
+
+/**
+ * Export transports.
+ */
+
+module.exports = {
+ websocket: require('./websocket')
+ , htmlfile: require('./htmlfile')
+ , 'xhr-polling': require('./xhr-polling')
+ , 'jsonp-polling': require('./jsonp-polling')
+};
diff --git a/lib/transports/jsonp-polling.js b/lib/transports/jsonp-polling.js
new file mode 100644
index 0000000000..e114e37f0b
--- /dev/null
+++ b/lib/transports/jsonp-polling.js
@@ -0,0 +1,52 @@
+
+/*!
+ * socket.io-node
+ * Copyright(c) 2011 LearnBoost
+ * MIT Licensed
+ */
+
+/**
+ * Module requirements.
+ */
+
+const HTTPPolling = require('./http-polling');
+
+/**
+ * Export the constructor.
+ */
+
+exports = module.exports = JSONPPolling;
+
+/**
+ * HTTP interface constructor. Interface compatible with all transports that
+ * depend on request-response cycles.
+ *
+ * @api public
+ */
+
+function JSONPPolling (data, request) {
+ HTTPPolling.call(this, data, request);
+};
+
+/**
+ * Inherits from Transport.
+ */
+
+JSONPPolling.prototype.__proto__ = HTTPPolling.prototype;
+
+/**
+ * Performs the write.
+ *
+ * @api private
+ */
+
+JSONPPolling.prototype.doWrite = function (data) {
+ this.response.writeHead(200, {
+ 'Content-Type': 'text/javascript; charset=UTF-8'
+ , 'Content-Length': Buffer.byteLength(data)
+ , 'Connection': 'Keep-Alive'
+ });
+
+ this.response.write(data);
+ this.log.debug('writing', data);
+};
diff --git a/lib/transports/websocket.js b/lib/transports/websocket.js
new file mode 100644
index 0000000000..3e20e066d4
--- /dev/null
+++ b/lib/transports/websocket.js
@@ -0,0 +1,64 @@
+
+/*!
+ * socket.io-node
+ * Copyright(c) 2011 LearnBoost
+ * MIT Licensed
+ */
+
+/**
+ * Module requirements.
+ */
+
+const Transport = require('../transport');
+
+/**
+ * Export the constructor.
+ */
+
+exports = module.exports = WebSocket;
+
+/**
+ * HTTP interface constructor. Interface compatible with all transports that
+ * depend on request-response cycles.
+ *
+ * @api public
+ */
+
+function WebSocket (data, request) {
+ Transport.call(this, data, request);
+};
+
+/**
+ * Inherits from Transport.
+ */
+
+WebSocket.prototype.__proto__ = Transport.prototype;
+
+/**
+ * Writes a payload.
+ *
+ * @api private
+ */
+
+WebSocket.prototype.payload = function (msgs) {
+ for (var i = 0, l = msgs.length; i < l; i++) {
+ this.write(msgs[i]);
+ }
+
+ return this;
+};
+
+
+/**
+ * Override end
+ *
+ * @api private
+ */
+
+WebSocket.prototype.end = function () {
+ if (this.open) {
+ this.socket.end();
+ }
+
+ return Transport.prototype.end.call(this);
+};
diff --git a/lib/transports/xhr-polling.js b/lib/transports/xhr-polling.js
new file mode 100644
index 0000000000..c74929f090
--- /dev/null
+++ b/lib/transports/xhr-polling.js
@@ -0,0 +1,60 @@
+
+/*!
+ * socket.io-node
+ * Copyright(c) 2011 LearnBoost
+ * MIT Licensed
+ */
+
+/**
+ * Module requirements.
+ */
+
+const HTTPPolling = require('./http-polling');
+
+/**
+ * Export the constructor.
+ */
+
+exports = module.exports = XHRPolling;
+
+/**
+ * Ajax polling transport.
+ *
+ * @api public
+ */
+
+function XHRPolling (data, request) {
+ HTTPPolling.call(this, data, request);
+};
+
+/**
+ * Inherits from Transport.
+ */
+
+XHRPolling.prototype.__proto__ = HTTPPolling.prototype;
+
+/**
+ * Frames data prior to write.
+ *
+ * @api private
+ */
+
+XHRPolling.prototype.doWrite = function (data) {
+ var origin = this.req.headers.origin
+ , headers = {
+ 'Content-Type': 'text/plain; charset=UTF-8'
+ , 'Content-Length': Buffer.byteLength(data)
+ , 'Connection': 'Keep-Alive'
+ };
+
+ // https://developer.mozilla.org/En/HTTP_Access_Control
+ headers['Access-Control-Allow-Origin'] = '*';
+
+ if (this.req.headers.cookie) {
+ headers['Access-Control-Allow-Credentials'] = 'true';
+ }
+
+ this.response.writeHead(200, headers);
+ this.response.write(data);
+ this.log.debug('writing', data);
+};
diff --git a/lib/util.js b/lib/util.js
new file mode 100644
index 0000000000..25f31f2bd8
--- /dev/null
+++ b/lib/util.js
@@ -0,0 +1,25 @@
+
+/*!
+ * socket.io-node
+ * Copyright(c) 2011 LearnBoost
+ * MIT Licensed
+ */
+
+/**
+ * Module dependencies.
+ */
+
+/**
+ * Converts an enumerable to an array.
+ *
+ * @api public
+ */
+
+exports.toArray = function (enu) {
+ var arr = [];
+
+ for (var i = 0, l = enu.length; i < l; i++)
+ arr.push(enu[i]);
+
+ return arr;
+};
diff --git a/package.json b/package.json
new file mode 100644
index 0000000000..42cb8244cb
--- /dev/null
+++ b/package.json
@@ -0,0 +1,14 @@
+{
+ "name": "socket.io-node",
+ , "version": "0.7.0",
+ , "description": "WebSockets everywhere",
+ , "keywords": ["websocket", "socket", "realtime"],
+ , "author": "Guillermo Rauch ",
+ , "contributors": [
+ { "name": "Guillermo Rauch", "email": "rauchg@gmail.com" }
+ , { "name": "Arnout Kazemier", "email": "info@3rd-eden.com" }
+ ]
+ , "dependencies": {}
+ , "main": "index"
+ , "engines": { "node": "0.4.x" }
+}
diff --git a/support/expresso b/support/expresso
new file mode 160000
index 0000000000..1d2c78ab4b
--- /dev/null
+++ b/support/expresso
@@ -0,0 +1 @@
+Subproject commit 1d2c78ab4b0b7dc5655b313f54167bcb3df53f76
diff --git a/support/should.js b/support/should.js
new file mode 160000
index 0000000000..c05fac17d9
--- /dev/null
+++ b/support/should.js
@@ -0,0 +1 @@
+Subproject commit c05fac17d9894f1daeefded4db6609f962424831
diff --git a/support/socket.io-node-client b/support/socket.io-node-client
new file mode 160000
index 0000000000..f6c5f54cc0
--- /dev/null
+++ b/support/socket.io-node-client
@@ -0,0 +1 @@
+Subproject commit f6c5f54cc055d29b843cef5e4095ed83b6f50ef0
diff --git a/test/common.js b/test/common.js
new file mode 100644
index 0000000000..de5173b4f0
--- /dev/null
+++ b/test/common.js
@@ -0,0 +1,67 @@
+
+/*!
+ * socket.io-node
+ * Copyright(c) 2011 LearnBoost
+ * MIT Licensed
+ */
+
+/**
+ * Test dependencies.
+ */
+
+const io = require('socket.io')
+ , should = module.exports = require('should')
+ , http = require('http')
+ , https = require('https');
+
+/**
+ * Get utility.
+ */
+
+get = function (opts, fn) {
+ opts = opts || {};
+ opts.path = opts.path.replace(/{protocol}/g, io.protocol);
+ opts.headers = {
+ 'Host': 'localhost'
+ , 'Connection': 'Keep-Alive'
+ };
+
+ var req = (opts.secure ? https : http).request(opts, function (res) {
+ var buf = '';
+
+ res.on('data', function (chunk) {
+ buf += chunk;
+ });
+
+ res.on('end', function () {
+ fn(res, buf);
+ });
+ });
+
+ req.end();
+ return req;
+};
+
+/**
+ * Handshake utility
+ */
+
+handshake = function (port, fn) {
+ get({
+ port: port
+ , path: '/socket.io/{protocol}'
+ }, function (res, data) {
+ fn.apply(null, data.split(':'));
+ });
+};
+
+/**
+ * Silence logging.
+ */
+
+var old = io.listen;
+
+io.listen = function () {
+ console.log('');
+ return old.apply(this, arguments);
+};
diff --git a/test/fixtures/cert.crt b/test/fixtures/cert.crt
new file mode 100644
index 0000000000..5883cd4496
--- /dev/null
+++ b/test/fixtures/cert.crt
@@ -0,0 +1,21 @@
+-----BEGIN CERTIFICATE-----
+MIIDXTCCAkWgAwIBAgIJAMUSOvlaeyQHMA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNV
+BAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX
+aWRnaXRzIFB0eSBMdGQwHhcNMTAxMTE2MDkzMjQ5WhcNMTMxMTE1MDkzMjQ5WjBF
+MQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50
+ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB
+CgKCAQEAz+LXZOjcQCJq3+ZKUFabj71oo/ex/XsBcFqtBThjjTw9CVEVwfPQQp4X
+wtPiB204vnYXwQ1/R2NdTQqCZu47l79LssL/u2a5Y9+0NEU3nQA5qdt+1FAE0c5o
+exPimXOrR3GWfKz7PmZ2O0117IeCUUXPG5U8umhDe/4mDF4ZNJiKc404WthquTqg
+S7rLQZHhZ6D0EnGnOkzlmxJMYPNHSOY1/6ivdNUUcC87awNEA3lgfhy25IyBK3QJ
+c+aYKNTbt70Lery3bu2wWLFGtmNiGlQTS4JsxImRsECTI727ObS7/FWAQsqW+COL
+0Sa5BuMFrFIpjPrEe0ih7vRRbdmXRwIDAQABo1AwTjAdBgNVHQ4EFgQUDnV4d6mD
+tOnluLoCjkUHTX/n4agwHwYDVR0jBBgwFoAUDnV4d6mDtOnluLoCjkUHTX/n4agw
+DAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQUFAAOCAQEAFwV4MQfTo+qMv9JMiyno
+IEiqfOz4RgtmBqRnXUffcjS2dhc7/z+FPZnM79Kej8eLHoVfxCyWRHFlzm93vEdv
+wxOCrD13EDOi08OOZfxWyIlCa6Bg8cMAKqQzd2OvQOWqlRWBTThBJIhWflU33izX
+Qn5GdmYqhfpc+9ZHHGhvXNydtRQkdxVK2dZNzLBvBlLlRmtoClU7xm3A+/5dddeP
+AQHEPtyFlUw49VYtZ3ru6KqPms7MKvcRhYLsy9rwSfuuniMlx4d0bDR7TOkw0QQS
+A0N8MGQRQpzl4mw4jLzyM5d5QtuGBh2P6hPGa0YQxtI3RPT/p6ENzzBiAKXiSfzo
+xw==
+-----END CERTIFICATE-----
diff --git a/test/fixtures/key.key b/test/fixtures/key.key
new file mode 100644
index 0000000000..f31ff3d944
--- /dev/null
+++ b/test/fixtures/key.key
@@ -0,0 +1,27 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIEowIBAAKCAQEAz+LXZOjcQCJq3+ZKUFabj71oo/ex/XsBcFqtBThjjTw9CVEV
+wfPQQp4XwtPiB204vnYXwQ1/R2NdTQqCZu47l79LssL/u2a5Y9+0NEU3nQA5qdt+
+1FAE0c5oexPimXOrR3GWfKz7PmZ2O0117IeCUUXPG5U8umhDe/4mDF4ZNJiKc404
+WthquTqgS7rLQZHhZ6D0EnGnOkzlmxJMYPNHSOY1/6ivdNUUcC87awNEA3lgfhy2
+5IyBK3QJc+aYKNTbt70Lery3bu2wWLFGtmNiGlQTS4JsxImRsECTI727ObS7/FWA
+QsqW+COL0Sa5BuMFrFIpjPrEe0ih7vRRbdmXRwIDAQABAoIBAGe4+9VqZfJN+dsq
+8Osyuz01uQ8OmC0sAWTIqUlQgENIyf9rCJsUBlYmwR5BT6Z69XP6QhHdpSK+TiAR
+XUz0EqG9HYzcxHIBaACP7j6iRoQ8R4kbbiWKo0z3WqQGIOqFjvD/mKEuQdE5mEYw
+eOUCG6BnX1WY2Yr8WKd2AA/tp0/Y4d8z04u9eodMpSTbHTzYMJb5SbBN1vo6FY7q
+8zSuO0BMzXlAxUsCwHsk1GQHFr8Oh3zIR7bQGtMBouI+6Lhh7sjFYsfxJboqMTBV
+IKaA216M6ggHG7MU1/jeKcMGDmEfqQLQoyWp29rMK6TklUgipME2L3UD7vTyAVzz
+xbVOpZkCgYEA8CXW4sZBBrSSrLR5SB+Ubu9qNTggLowOsC/kVKB2WJ4+xooc5HQo
+mFhq1v/WxPQoWIxdYsfg2odlL+JclK5Qcy6vXmRSdAQ5lK9gBDKxZSYc3NwAw2HA
+zyHCTK+I0n8PBYQ+yGcrxu0WqTGnlLW+Otk4CejO34WlgHwbH9bbY5UCgYEA3ZvT
+C4+OoMHXlmICSt29zUrYiL33IWsR3/MaONxTEDuvgkOSXXQOl/8Ebd6Nu+3WbsSN
+bjiPC/JyL1YCVmijdvFpl4gjtgvfJifs4G+QHvO6YfsYoVANk4u6g6rUuBIOwNK4
+RwYxwDc0oysp+g7tPxoSgDHReEVKJNzGBe9NGGsCgYEA4O4QP4gCEA3B9BF2J5+s
+n9uPVxmiyvZUK6Iv8zP4pThTBBMIzNIf09G9AHPQ7djikU2nioY8jXKTzC3xGTHM
+GJZ5m6fLsu7iH+nDvSreDSeNkTBfZqGAvoGYQ8uGE+L+ZuRfCcXYsxIOT5s6o4c3
+Dle2rVFpsuKzCY00urW796ECgYBn3go75+xEwrYGQSer6WR1nTgCV29GVYXKPooy
+zmmMOT1Yw80NSkEw0pFD4cTyqVYREsTrPU0mn1sPfrOXxnGfZSVFpcR/Je9QVfQ7
+eW7GYxwfom335aqHVj10SxRqteP+UoWWnHujCPz94VRKZMakBddYCIGSan+G6YdS
+7sdmwwKBgBc2qj0wvGXDF2kCLwSGfWoMf8CS1+5fIiUIdT1e/+7MfDdbmLMIFVjF
+QKS3zVViXCbrG5SY6wS9hxoc57f6E2A8vcaX6zy2xkZlGHQCpWRtEM5R01OWJQaH
+HsHMmQZGUQVoDm1oRkDhrTFK4K3ukc3rAxzeTZ96utOQN8/KJsTv
+-----END RSA PRIVATE KEY-----
diff --git a/test/io.test.js b/test/io.test.js
new file mode 100644
index 0000000000..3678438f04
--- /dev/null
+++ b/test/io.test.js
@@ -0,0 +1,116 @@
+
+/*!
+ * socket.io-node
+ * Copyright(c) 2011 LearnBoost
+ * MIT Licensed
+ */
+
+/**
+ * Test dependencies.
+ */
+
+var sio = require('socket.io')
+ , fs = require('fs')
+ , http = require('http')
+ , https = require('https')
+ , should = require('./common')
+ , ports = 15000;
+
+/**
+ * Test.
+ */
+
+module.exports = {
+
+ 'test that protocol version is present': function (done) {
+ sio.protocol.should.be.a('number');
+ done();
+ },
+
+ 'test that default transports are present': function (done) {
+ sio.Manager.defaultTransports.should.be.an.instanceof(Array);
+ done();
+ },
+
+ 'test that version is present': function (done) {
+ sio.version.should.match(/([0-9]+)\.([0-9]+)\.([0-9]+)/);
+ done();
+ },
+
+ 'test listening with a port': function (done) {
+ var port = ++ports
+ , io = sio.listen(port);
+
+ io.server.should.be.an.instanceof(http.Server);
+
+ get({
+ port: port
+ , path: '/'
+ }, function (res, data) {
+ res.statusCode.should.eql(200);
+ data.should.eql('Welcome to socket.io.');
+ io.server.close();
+ done();
+ });
+ },
+
+ 'test listening with a server': function (done) {
+ var server = http.createServer()
+ , io = sio.listen(server)
+ , port = ++ports;
+
+ server.listen(port);
+
+ get({
+ port: port
+ , path: '/socket.io'
+ }, function (res, data) {
+ res.statusCode.should.eql(200);
+ data.should.eql('Welcome to socket.io.');
+
+ server.close();
+ done();
+ });
+ },
+
+ 'test listening with a https server': function (done) {
+ var server = https.createServer({
+ key: fs.readFileSync(__dirname + '/fixtures/key.key')
+ , cert: fs.readFileSync(__dirname + '/fixtures/cert.crt')
+ }, function () { })
+ , io = sio.listen(server)
+ , port = ++ports;
+
+ server.listen(port);
+
+ get({
+ port: port
+ , path: '/socket.io'
+ , secure: true
+ }, function (res, data) {
+ res.statusCode.should.eql(200);
+ data.should.eql('Welcome to socket.io.');
+
+ server.close();
+ done();
+ });
+ },
+
+ 'test listening with no arguments listens on 80': function (done) {
+ try {
+ var io = sio.listen();
+ get({
+ post: 80
+ , path: '/socket.io'
+ }, function (res) {
+ res.statusCode.should.eql(200);
+ io.server.close();
+ done();
+ });
+ } catch (e) {
+ e.should.match(/EACCES/);
+ done();
+ }
+ }
+
+};
diff --git a/test/manager.test.js b/test/manager.test.js
new file mode 100644
index 0000000000..a4ec682ea4
--- /dev/null
+++ b/test/manager.test.js
@@ -0,0 +1,292 @@
+
+/*!
+ * socket.io-node
+ * Copyright(c) 2011 LearnBoost
+ * MIT Licensed
+ */
+
+/**
+ * Test dependencies.
+ */
+
+var sio = require('socket.io')
+ , http = require('http')
+ , should = require('./common')
+ , ports = 15100;
+
+/**
+ * Test.
+ */
+
+module.exports = {
+
+ 'test setting and getting a configuration flag': function (done) {
+ done();
+ },
+
+ 'test enabling and disabling a configuration flag': function (done) {
+ var port = ++ports
+ , io = sio.listen(http.createServer());
+
+ io.enable('flag');
+ io.enabled('flag').should.be.true;
+ io.disabled('flag').should.be.false;
+
+ io.disable('flag');
+ var port = ++ports
+ , io = sio.listen(http.createServer());
+
+ io.configure(function () {
+ io.set('a', 'b');
+ io.enable('tobi');
+ });
+
+ io.get('a').should.eql('b');
+ io.enabled('tobi').should.be.true;
+
+ done();
+ },
+
+ 'test configuration callbacks with envs': function (done) {
+ var port = ++ports
+ , io = sio.listen(http.createServer());
+
+ process.env.NODE_ENV = 'development';
+
+ io.configure('production', function () {
+ io.set('ferret', 'tobi');
+ });
+
+ io.configure('development', function () {
+ io.set('ferret', 'jane');
+ });
+
+ io.get('ferret').should.eql('jane');
+ done();
+ },
+
+ 'test that normal requests are still served': function (done) {
+ var server = http.createServer(function (req, res) {
+ res.writeHead(200);
+ res.end('woot');
+ });
+
+ var io = sio.listen(server)
+ , port = ++ports;
+
+ server.listen(ports);
+
+ get({
+ port: port
+ , path: '/socket.io'
+ }, function (res, data) {
+ res.statusCode.should.eql(200);
+ data.should.eql('Welcome to socket.io.');
+
+ get({
+ port: port
+ , path: '/woot'
+ }, function (res, data) {
+ server.close();
+ done();
+ });
+ });
+ },
+
+ 'test that the client is served': function (done) {
+ var port = ++ports
+ , io = sio.listen(port);
+
+ get({
+ port: port
+ , path: '/socket.io/socket.io.js'
+ }, function (res, data) {
+ res.headers['content-type'].should.eql('application/javascript');
+ res.headers['content-length'].should.be.match(/([0-9]+)/);
+ res.headers.etag.should.match(/([0-9]+)\.([0-9]+)\.([0-9]+)/);
+
+ data.should.match(/XMLHttpRequest/);
+
+ io.server.close();
+ done();
+ });
+ },
+
+ 'test that you can serve custom clients': function (done) {
+ var port = ++ports
+ , io = sio.listen(port);
+
+ io.configure(function () {
+ io.set('browser client', 'custom_client');
+ io.set('browser client etag', '1.0');
+ });
+
+ get({
+ port: port
+ , path: '/socket.io/socket.io.js'
+ }, function (res, data) {
+ res.headers['content-type'].should.eql('application/javascript');
+ res.headers['content-length'].should.eql(13);
+ res.headers.etag.should.eql('1.0');
+
+ data.should.eql('custom_client');
+
+ io.server.close();
+ done();
+ });
+ },
+
+ 'test handshake': function (done) {
+ var port = ++ports
+ , io = sio.listen(port);
+
+ get({
+ port: port
+ , path: '/socket.io/{protocol}/'
+ }, function (res, data) {
+ res.statusCode.should.eql(200);
+ data.should.match(/([^:]+):([0-9]+)?:([0-9]+)?:(.+)/);
+ io.server.close();
+ done();
+ });
+ },
+
+ 'test handshake with unsupported protocol version': function (done) {
+ var port = ++ports
+ , io = sio.listen(port);
+
+ get({
+ port: port
+ , path: '/socket.io/-1/'
+ }, function (res, data) {
+ res.statusCode.should.eql(500);
+ data.should.match(/Protocol version not supported/);
+ io.server.close();
+ done();
+ });
+ },
+
+ 'test authorization failure in handshake': function (done) {
+ var port = ++ports
+ , io = sio.listen(port);
+
+ io.configure(function () {
+ function auth (data, fn) {
+ fn(null, false);
+ };
+
+ io.set('authorization', auth);
+ });
+
+ get({
+ port: port
+ , path: '/socket.io/{protocol}/'
+ }, function (res, data) {
+ res.statusCode.should.eql(403);
+ data.should.match(/Handshake unauthorized/);
+ io.server.close();
+ done();
+ });
+ },
+
+ 'test a handshake error': function (done) {
+ var port = ++ports
+ , io = sio.listen(port);
+
+ io.configure(function () {
+ function auth (data, fn) {
+ fn(new Error);
+ };
+
+ io.set('authorization', auth);
+ });
+
+ get({
+ port: port
+ , path: '/socket.io/{protocol}/'
+ }, function (res, data) {
+ res.statusCode.should.eql(500);
+ data.should.match(/Handshake error/);
+ io.server.close();
+ done();
+ });
+ },
+
+ 'test limiting the supported transports for a manager': function (done) {
+ var port = ++ports
+ , io = sio.listen(port);
+
+ io.configure(function () {
+ io.set('transports', ['tobi', 'jane']);
+ });
+
+ get({
+ port: port
+ , path: '/socket.io/{protocol}/'
+ }, function (res, data) {
+ res.statusCode.should.eql(200);
+ data.should.match(/([^:]+):([0-9]+)?:([0-9]+)?:tobi,jane/);
+ io.server.close();
+ done();
+ });
+ },
+
+ 'test setting a custom close timeout': function (done) {
+ var port = ++ports
+ , io = sio.listen(port);
+
+ io.configure(function () {
+ io.set('close timeout', 66);
+ });
+
+ get({
+ port: port
+ , path: '/socket.io/{protocol}/'
+ }, function (res, data) {
+ res.statusCode.should.eql(200);
+ data.should.match(/([^:]+):([0-9]+)?:66?:(.*)/);
+ io.server.close();
+ done();
+ });
+ },
+
+ 'test setting a custom heartbeat timeout': function (done) {
+ var port = ++ports
+ , io = sio.listen(port);
+
+ io.configure(function () {
+ io.set('heartbeat timeout', 33);
+ });
+
+ get({
+ port: port
+ , path: '/socket.io/{protocol}/'
+ }, function (res, data) {
+ res.statusCode.should.eql(200);
+ data.should.match(/([^:]+):33:([0-9]+)?:(.*)/);
+ io.server.close();
+ done();
+ });
+ },
+
+ 'test disabling timeouts': function (done) {
+ var port = ++ports
+ , io = sio.listen(port);
+
+ io.configure(function () {
+ io.set('heartbeat timeout', null);
+ io.set('close timeout', '');
+ });
+
+ get({
+ port: port
+ , path: '/socket.io/{protocol}/'
+ }, function (res, data) {
+ res.statusCode.should.eql(200);
+ data.should.match(/([^:]+)::?:(.*)/);
+ io.server.close();
+ done();
+ });
+ }
+
+};
diff --git a/test/parser.test.js b/test/parser.test.js
new file mode 100644
index 0000000000..ffc23cef2f
--- /dev/null
+++ b/test/parser.test.js
@@ -0,0 +1,335 @@
+
+/**
+ * Test dependencies.
+ */
+
+const parser = require('socket.io').parser
+ , decode = parser.decode
+ , should = require('./common');
+
+/**
+ * Test.
+ */
+
+module.exports = {
+
+ 'decoding error packet': function () {
+ parser.decodePacket('7:::').should.eql({
+ type: 'error'
+ , reason: ''
+ , advice: ''
+ , endpoint: ''
+ });
+ },
+
+ 'decoding error packet with reason': function () {
+ parser.decodePacket('7:::0').should.eql({
+ type: 'error'
+ , reason: 'transport not supported'
+ , advice: ''
+ , endpoint: ''
+ });
+ },
+
+ 'decoding error packet with reason and advice': function () {
+ parser.decodePacket('7:::2+0').should.eql({
+ type: 'error'
+ , reason: 'unauthorized'
+ , advice: 'reconnect'
+ , endpoint: ''
+ });
+ },
+
+ 'decoding error packet with endpoint': function () {
+ parser.decodePacket('7::/woot').should.eql({
+ type: 'error'
+ , reason: ''
+ , advice: ''
+ , endpoint: '/woot'
+ });
+ },
+
+ 'decoding ack packet': function () {
+ parser.decodePacket('6:::140').should.eql({
+ type: 'ack'
+ , ackId: '140'
+ , endpoint: ''
+ , args: []
+ });
+ },
+
+ 'decoding ack packet with args': function () {
+ parser.decodePacket('6:::12+' + JSON.stringify(['woot', 'wa'])).should.eql({
+ type: 'ack'
+ , ackId: '12'
+ , endpoint: ''
+ , args: ['woot', 'wa']
+ });
+ },
+
+ 'decoding ack packet with bad json': function () {
+ var thrown = false;
+
+ try {
+ parser.decodePacket('6:::1+{"++]').should.eql({
+ type: 'ack'
+ , ackId: '1'
+ , endpoint: ''
+ , args: []
+ });
+ } catch (e) {
+ thrown = true;
+ }
+
+ thrown.should.be.false;
+ },
+
+ 'decoding json packet': function () {
+ parser.decodePacket('4:::"2"').should.eql({
+ type: 'json'
+ , endpoint: ''
+ , data: '2'
+ });
+ },
+
+ 'decoding json packet with message id and ack data': function () {
+ parser.decodePacket('4:1+::{"a":"b"}').should.eql({
+ type: 'json'
+ , id: 1
+ , ack: 'data'
+ , endpoint: ''
+ , data: { a: 'b' }
+ });
+ },
+
+ 'decoding an event packet': function () {
+ parser.decodePacket('5:::woot').should.eql({
+ type: 'event'
+ , name: 'woot'
+ , endpoint: ''
+ , args: []
+ });
+ },
+
+ 'decoding an event packet with message id and ack': function () {
+ parser.decodePacket('5:1+::tobi').should.eql({
+ type: 'event'
+ , id: 1
+ , ack: 'data'
+ , endpoint: ''
+ , name: 'tobi'
+ , args: []
+ });
+ },
+
+ 'decoding an event packet with data': function () {
+ parser.decodePacket('5:::edwald\ufffd[{"a": "b"},2,"3"]').should.eql({
+ type: 'event'
+ , name: 'edwald'
+ , endpoint: ''
+ , args: [{a: 'b'}, 2, '3']
+ });
+ },
+
+ 'decoding a message packet': function () {
+ parser.decodePacket('3:::woot').should.eql({
+ type: 'message'
+ , endpoint: ''
+ , data: 'woot'
+ });
+ },
+
+ 'decoding a message packet with id and endpoint': function () {
+ parser.decodePacket('3:5:/tobi').should.eql({
+ type: 'message'
+ , id: 5
+ , ack: true
+ , endpoint: '/tobi'
+ , data: ''
+ });
+ },
+
+ 'decoding a heartbeat packet': function () {
+ parser.decodePacket('2:::').should.eql({
+ type: 'heartbeat'
+ , endpoint: ''
+ });
+ },
+
+ 'decoding a connection packet': function () {
+ parser.decodePacket('1::/tobi').should.eql({
+ type: 'connect'
+ , endpoint: '/tobi'
+ , qs: ''
+ });
+ },
+
+ 'decoding a connection packet with query string': function () {
+ parser.decodePacket('1::/test:?test=1').should.eql({
+ type: 'connect'
+ , endpoint: '/test'
+ , qs: '?test=1'
+ });
+ },
+
+ 'decoding a disconnection packet': function () {
+ parser.decodePacket('0::/woot').should.eql({
+ type: 'disconnect'
+ , endpoint: '/woot'
+ });
+ },
+
+ 'encoding error packet': function () {
+ parser.encodePacket({
+ type: 'error'
+ , reason: ''
+ , advice: ''
+ , endpoint: ''
+ }).should.eql('7::');
+ },
+
+ 'encoding error packet with reason': function () {
+ parser.encodePacket({
+ type: 'error'
+ , reason: 'transport not supported'
+ , advice: ''
+ , endpoint: ''
+ }).should.eql('7:::0');
+ },
+
+ 'encoding error packet with reason and advice': function () {
+ parser.encodePacket({
+ type: 'error'
+ , reason: 'unauthorized'
+ , advice: 'reconnect'
+ , endpoint: ''
+ }).should.eql('7:::2+0');
+ },
+
+ 'encoding error packet with endpoint': function () {
+ parser.encodePacket({
+ type: 'error'
+ , reason: ''
+ , advice: ''
+ , endpoint: '/woot'
+ }).should.eql('7::/woot');
+ },
+
+ 'encoding ack packet': function () {
+ parser.encodePacket({
+ type: 'ack'
+ , ackId: '140'
+ , endpoint: ''
+ , args: []
+ }).should.eql('6:::140');
+ },
+
+ 'encoding ack packet with args': function () {
+ parser.encodePacket({
+ type: 'ack'
+ , ackId: '12'
+ , endpoint: ''
+ , args: ['woot', 'wa']
+ }).should.eql('6:::12+' + JSON.stringify(['woot', 'wa']));
+ },
+
+ 'encoding json packet': function () {
+ parser.encodePacket({
+ type: 'json'
+ , endpoint: ''
+ , data: '2'
+ }).should.eql('4:::"2"');
+ },
+
+ 'encoding json packet with message id and ack data': function () {
+ parser.encodePacket({
+ type: 'json'
+ , id: 1
+ , ack: 'data'
+ , endpoint: ''
+ , data: { a: 'b' }
+ }).should.eql('4:1+::{"a":"b"}');
+ },
+
+ 'encoding an event packet': function () {
+ parser.encodePacket({
+ type: 'event'
+ , name: 'woot'
+ , endpoint: ''
+ , args: []
+ }).should.eql('5:::woot');
+ },
+
+ 'encoding an event packet with message id and ack': function () {
+ parser.encodePacket({
+ type: 'event'
+ , id: 1
+ , ack: 'data'
+ , endpoint: ''
+ , name: 'tobi'
+ , args: []
+ }).should.eql('5:1+::tobi');
+ },
+
+ 'encoding an event packet with data': function () {
+ parser.encodePacket({
+ type: 'event'
+ , name: 'edwald'
+ , endpoint: ''
+ , args: [{a: 'b'}, 2, '3']
+ }).should.eql('5:::edwald\ufffd[{"a":"b"},2,"3"]');
+ },
+
+ 'encoding a message packet': function () {
+ parser.encodePacket({
+ type: 'message'
+ , endpoint: ''
+ , data: 'woot'
+ }).should.eql('3:::woot');
+ },
+
+ 'encoding a message packet with id and endpoint': function () {
+ parser.encodePacket({
+ type: 'message'
+ , id: 5
+ , ack: true
+ , endpoint: '/tobi'
+ , data: ''
+ }).should.eql('3:5:/tobi');
+ },
+
+ 'encoding a heartbeat packet': function () {
+ parser.encodePacket({
+ type: 'heartbeat'
+ , endpoint: ''
+ }).should.eql('2::');
+ },
+
+ 'encoding a connection packet': function () {
+ parser.encodePacket({
+ type: 'connect'
+ , endpoint: '/tobi'
+ , qs: ''
+ }).should.eql('1::/tobi');
+ },
+
+ 'encoding a connection packet with query string': function () {
+ parser.encodePacket({
+ type: 'connect'
+ , endpoint: '/test'
+ , qs: '?test=1'
+ }).should.eql('1::/test:?test=1');
+ },
+
+ 'encoding a disconnection packet': function () {
+ parser.encodePacket({
+ type: 'disconnect'
+ , endpoint: '/woot'
+ }).should.eql('0::/woot');
+ },
+
+ 'test decoding a payload': function () {
+ parser.decodePayload('3:::5')
+ }
+
+};
diff --git a/test/socket.js b/test/socket.js
new file mode 100644
index 0000000000..41e46751e3
--- /dev/null
+++ b/test/socket.js
@@ -0,0 +1,15 @@
+
+/**
+ * Test dependencies.
+ */
+
+const sio = require('socket.io')
+ , should = require('./common');
+
+/**
+ * Test.
+ */
+
+module.exports = {
+
+};
diff --git a/test/transports.xhr-polling.test.js b/test/transports.xhr-polling.test.js
new file mode 100644
index 0000000000..cca6dd0c24
--- /dev/null
+++ b/test/transports.xhr-polling.test.js
@@ -0,0 +1,120 @@
+
+/*!
+ * socket.io-node
+ * Copyright(c) 2011 LearnBoost
+ * MIT Licensed
+ */
+
+/**
+ * Test dependencies.
+ */
+
+var sio = require('socket.io')
+ , should = require('./common')
+ , parser = sio.parser
+ , ports = 15200;
+
+/**
+ * Test.
+ */
+
+module.exports = {
+
+ 'test handshake': function (done) {
+ var port = ++ports
+ , io = sio.listen(port);
+
+ io.configure(function () {
+ io.set('polling duration', .2);
+ io.set('close timeout', .2);
+ });
+
+ function finish () {
+ req.res.socket.end();
+ io.server.close();
+ done();
+ };
+
+ var req = get({
+ port: port
+ , path: '/socket.io/{protocol}/'
+ }, function (res, data) {
+ var sessid = data.split(':')[0]
+ , total = 2;
+
+ get({
+ port: port
+ , path: '/socket.io/{protocol}/xhr-polling/jiasdasjid'
+ }, function (res, data) {
+ res.statusCode.should.eql(200);
+
+ data.should.eql(parser.encodePacket({
+ type: 'error'
+ , reason: 'client not handshaken'
+ }));
+
+ --total || finish();
+ });
+
+ get({
+ port: port
+ , path: '/socket.io/{protocol}/xhr-polling/' + sessid
+ }, function (res, data) {
+ res.statusCode.should.eql(200);
+ data.should.eql('');
+ --total || finish();
+ });
+ });
+ },
+
+ 'test the connection event': function (done) {
+ var port = ++ports
+ , io = sio.listen(port)
+ , sessid, req;
+
+ io.configure(function () {
+ io.set('polling duration', .2);
+ io.set('close timeout', .2);
+ });
+
+ io.sockets.on('connection', function (socket) {
+ socket.id.should.eql(sessid);
+ io.server.close();
+ done();
+ });
+
+ handshake(port, function (sid) {
+ sessid = sid;
+ req = get({
+ port: port
+ , path: '/socket.io/{protocol}/xhr-polling/' + sid
+ }, function() {});
+ });
+ },
+
+ 'test sending back data': function (data) {
+ var port = ++ports
+ , io = sio.listen(port);
+
+ io.configure(function () {
+ io.set('polling duration', .2);
+ io.set('close timeout', .2);
+ });
+
+ io.sockets.on('connection', function (socket) {
+ socket.send('woot');
+ });
+
+ handshake(port, function (sid) {
+ get({
+ port: port
+ , path: '/socket.io/{protocol}/xhr-polling/' + sid
+ }, function (res, data) {
+ var packet = parser.decodePacket(data);
+ packet.type.should.eql('message');
+ packet.data.should.eql('woot');
+ });
+ });
+ }
+
+};