diff --git a/README.md b/README.md index e1b434b..23d1da6 100644 --- a/README.md +++ b/README.md @@ -15,9 +15,73 @@ By running socket.io with the `socket.io-redis` adapter you can run multiple socket.io instances in different processes or servers that can all broadcast and emit events to and from each other. +`socket.io-redis` use Redis pub/sub mechanism to route events to different nodes/servers and +store rooms and sockets ids in Redis sets. + If you need to emit events to socket.io instances from a non-socket.io process, you should use [socket.io-emitter](http:///github.com/Automattic/socket.io-emitter). +## Stored schema + +The module store two different entities in Redis: **socket** and **room**. + +Each as a Redis SET. + +Every key is prefixed with "socket.io". Prefix is customizable with the *key* option. + +### socket + +The module creates a new Redis SET for each new socket. + +The socket SET key is defined as *{{PREFIX}}*#*{{SOCKET_ID}}* (e.g.: *socket.io#951wMmbBjkREmCapAAAD*). + +The socket SET is created with one record: the socket ID string. + +Then each time this socket join/leave a room module add/remove a Redis record in SET. + +Example for a socket with the ID *951wMmbBjkREmCapAAAD* in *foo* and *bar* rooms: + +``` +socket.io#951wMmbBjkREmCapAAAD + -> 951wMmbBjkREmCapAAAD + -> foo + -> bar +``` + +### room + +Each time a room is needed (= a socket join a room that not already exists) the module create a new Redis SET. + +The room SET key is defined as *{{PREFIX}}*#*{{ROOM_NAME }}* (e.g.: *socket.io#foo*). +The room SET contain the socket IDs of the room sockets. + +Then each time a socket join/leave the room the module add/remove the corresponding Redis record from the SET. + +Example for a room *foo* with the following socket in *951wMmbBjkREmCapAAAD*, *566Mm_BjkREmRff456*: + +``` +socket.io#foo + -> 951wMmbBjkREmCapAAAD + -> 566Mm_BjkREmRff456 +``` + +As with native adapter the not longer needed room SET are deleted automatically (except on application +exit, see below). + +## Known limitation + +**Warning! Current module implementation doesn't cleanup Redis storage on exit.** + +Consequence is that in a multi-node/server configuration with the out-of-the-box module, +shutting down a node process will let sockets and rooms SET remain in Redis even if the +current sockets are not longer connected. + +The reason is the non ability for node to execute asynchronous tasks (like Redis queries) +on exit. + +So, every developer should implement his proper cleanup logic in the context of +his particular project. + ## API ### adapter(uri[, opts]) @@ -36,8 +100,10 @@ The following options are allowed: be used instead of the host and port options if specified. - `pubClient`: optional, the redis client to publish events on - `subClient`: optional, the redis client to subscribe to events on +- `dataClient`: optional, the redis client used to store and read socket.io + sockets/rooms data -If you decide to supply `pubClient` and `subClient`, make sure you use +If you decide to supply `pubClient`, `subClient` or `dataClient` make sure you use [node_redis](https://github.com/mranney/node_redis) as a client or one with an equivalent API. diff --git a/index.js b/index.js index 46d9a86..758ebb7 100644 --- a/index.js +++ b/index.js @@ -45,6 +45,7 @@ function adapter(uri, opts){ var port = Number(opts.port || 6379); var pub = opts.pubClient; var sub = opts.subClient; + var data = opts.dataClient; var prefix = opts.key || 'socket.io'; // init clients if needed @@ -52,6 +53,7 @@ function adapter(uri, opts){ if (!sub) sub = socket ? redis(socket, { detect_buffers: true }) : redis(port, host, {detect_buffers: true}); + if (!data) data = socket ? redis(socket) : redis(port, host); // this server's key @@ -65,10 +67,11 @@ function adapter(uri, opts){ * @api public */ + var self = this; + function Redis(nsp){ + self = this; Adapter.call(this, nsp); - - var self = this; sub.psubscribe(prefix + '#*', function(err){ if (err) self.emit('error', err); }); @@ -79,7 +82,7 @@ function adapter(uri, opts){ * Inherits from `Adapter`. */ - Redis.prototype.__proto__ = Adapter.prototype; + Redis.prototype = Object.create(Adapter.prototype); /** * Called with a subscription message @@ -93,13 +96,93 @@ function adapter(uri, opts){ var args = msgpack.decode(msg); if (args[0] && args[0].nsp === undefined) - args[0].nsp = '/' + args[0].nsp = '/'; - if (!args[0] || args[0].nsp != this.nsp.name) return debug('ignore different namespace') + if (!args[0] || args[0].nsp != this.nsp.name) return debug('ignore different namespace'); args.push(true); this.broadcast.apply(this, args); }; + /** + * Adds a socket from a room. + * + * @param {String} socket id + * @param {String} room name + * @param {Function} callback + * @api public + */ + + Redis.prototype.add = function(id, room, fn){ + Adapter.prototype.add.call(this, id, room); + data.multi() + .sadd(prefix + '#' + room, id) + .sadd(prefix + '#' + id, room) + .exec(function(){ + if (fn) process.nextTick(fn.bind(null, null)); + }); + + }; + + /** + * Removes a socket from a room. + * + * @param {String} socket id + * @param {String} room name + * @param {Function} callback + * @api public + */ + + Redis.prototype.del = function(id, room, fn){ + Adapter.prototype.del.call(this, id, room); + data.multi() + .srem(prefix + '#' + room, id) + .srem(prefix + '#' + id, room) + .exec(function(){ + if (fn) process.nextTick(fn.bind(null, null)); + }); + }; + + + /** + * Removes a socket from all rooms it's joined. + * + * @param {String} socket id + * @api public + */ + + Redis.prototype.delAll = function(id, fn){ + Adapter.prototype.delAll.call(this, id); + + data.smembers(prefix + '#' + id, function(err, rooms){ + var multi = data.multi(); + for(var i=0; i