├── .gitignore ├── .jshintrc ├── LICENSE ├── README.md ├── index.js ├── lib ├── PubSubClient.js ├── adapter.js └── messageEncoder.js ├── package.json └── tests ├── index.js ├── messageEncoder.fixture.js ├── mongoCli.fixture.js ├── mubsubCli.fixture.js └── socketio-mongodb.fixture.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 18 | .grunt 19 | 20 | # node-waf configuration 21 | .lock-wscript 22 | 23 | # Compiled binary addons (http://nodejs.org/api/addons.html) 24 | build/Release 25 | 26 | # Dependency directory 27 | # https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git 28 | node_modules 29 | 30 | # Optional npm cache directory 31 | .npm 32 | 33 | # Optional REPL history 34 | .node_repl_history 35 | 36 | environment 37 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "maxerr" : 50, 3 | "bitwise" : true, 4 | "camelcase" : true, 5 | "curly" : true, 6 | "eqeqeq" : true, 7 | "forin" : true, 8 | "freeze" : true, 9 | "immed" : true, 10 | "indent" : 4, 11 | "latedef" : false, 12 | "newcap" : false, 13 | "noarg" : true, 14 | "noempty" : true, 15 | "nonbsp" : true, 16 | "nonew" : false, 17 | "plusplus" : false, 18 | "quotmark" : "single", 19 | "undef" : true, 20 | "unused" : true, 21 | "strict" : true, 22 | "maxparams" : false, 23 | "maxdepth" : false, 24 | "maxstatements" : false, 25 | "maxcomplexity" : false, 26 | "maxlen" : false, 27 | "asi" : false, 28 | "boss" : false, 29 | "debug" : false, 30 | "eqnull" : false, 31 | "es5" : false, 32 | "esnext" : false, 33 | "moz" : false, 34 | "evil" : false, 35 | "expr" : false, 36 | "funcscope" : false, 37 | "globalstrict" : false, 38 | "iterator" : false, 39 | "lastsemic" : false, 40 | "laxbreak" : true, 41 | "laxcomma" : false, 42 | "loopfunc" : false, 43 | "multistr" : false, 44 | "noyield" : false, 45 | "notypeof" : false, 46 | "proto" : false, 47 | "scripturl" : false, 48 | "shadow" : false, 49 | "sub" : false, 50 | "supernew" : false, 51 | "validthis" : false, 52 | "browser" : true, 53 | "browserify" : false, 54 | "couch" : false, 55 | "devel" : false, 56 | "dojo" : false, 57 | "jasmine" : false, 58 | "jquery" : false, 59 | "mocha" : false, 60 | "mootools" : false, 61 | "node" : true, 62 | "nonstandard" : false, 63 | "phantom" : false, 64 | "prototypejs" : false, 65 | "qunit" : false, 66 | "rhino" : false, 67 | "shelljs" : false, 68 | "typed" : false, 69 | "worker" : false, 70 | "wsh" : false, 71 | "yui" : false, 72 | "globals" : { 73 | 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 &y (Andy Wright) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # socket.io-mongodb 2 | A MongoDB Adapter for socket.io, based on [socket.io-redis](https://github.com/socketio/socket.io-redis). 3 | 4 | This adapter: 5 | * Implements the full socket.io-adapter, including rooms and room deletion 6 | * Supports SSL connections to the database 7 | * Leverages [mubsub](https://github.com/scttnlsn/mubsub) 8 | 9 | ## How to use 10 | 11 | ```JavaScript 12 | var io = require('socket.io')(3000), 13 | mongoAdapter = require('socket.io-mongodb'); 14 | 15 | io.adapter(mongoAdapter('mongodb://localhost:27017/socket-io')); 16 | ``` 17 | 18 | By running socket.io with the `socket.io-mongodb` adapter you can run 19 | multiple socket.io instances in different processes or servers that can 20 | all broadcast and emit events to and from each other. 21 | 22 | If you need to emit events to socket.io instances from a non-socket.io 23 | process, you should use [socket.io-emitter](https://github.com/socketio/socket.io-emitter). 24 | 25 | 26 | ## API 27 | 28 | ### adapter(uri[, opts]) 29 | The first argument, `uri`, expects a MongoDB connection string (i.e. `mongodb://localhost:27017/socket-io`). The options argument is explained below. 30 | 31 | ```JavaScript 32 | var io = require('socket.io')(3000), 33 | mongoAdapter = require('socket.io-mongodb'), 34 | adapter; 35 | 36 | adapter = mongoAdapter('mongodb://localhost:27017/socket-io', { 37 | prefix: 'myprefix', 38 | collectionName: 'mypubsub' 39 | }); 40 | 41 | io.adapter(adapter); 42 | ``` 43 | 44 | ### adapter(opts) 45 | The options described here can be passed in as the first argument, or as the second argument, when the first argument is your MongoDB connection string. 46 | 47 | * **uri** (_required_ string or object): a MongoDB connection string, or a URI object that can be parsed by [mongodb-uri](https://github.com/mongolab/mongodb-uri-node) 48 | * **prefix** (_optional_ string): a prefix to be used when publishing events (default is _socket-io_) 49 | * **collectionName** (_optional_ string): the name of the MongoDB collection that mubsub should create/use (default is _pubsub_). This is ignored if the `mongoClient` or `pubsubClient` properties are defined. 50 | * **mongoClient** (_optional_ instance of MongoDB node driver): the MongoDB driver to use. This is ignored if the `pubsubClient` is defined. @alsoSee [Existing database connection](https://github.com/losandes/socket.io-mongodb#existing-database-connection) 51 | * **pubsubClient** (_optional_ instance of mubsub): the mubsub client to use. This can be replaced by another library that implements the `channel`, `channel.subscribe`, and `channel.publish` interfaces. @alsoSee [Custom client](https://github.com/losandes/socket.io-mongodb#custom-client) 52 | * **channel** (_optional_ mubsub channel): the mubsub channel to use. This is only respected if the `pubsubClient` is also defined. @alsoSee [Custom client](https://github.com/losandes/socket.io-mongodb#custom-client) 53 | * **messageEncoder** (_optional_ object): an object with `encode` and `decode` functions, to be used when encoding/decoding pubsub messages. The default uses `JSON.stringify` and `JSON.parse` to encode and decode, respectively. @alsoSee: [Overriding the messageEncoder](https://github.com/losandes/socket.io-mongodb#overriding-the-messageencoder) 54 | 55 | ```JavaScript 56 | var io = require('socket.io')(3000), 57 | mongoAdapter = require('socket.io-mongodb'), 58 | adapter; 59 | //fs = require('fs'), 60 | //sslCA = [fs.readFileSync(__dirname + '/mySSLCA.pem')]; // you may want to switch this to `readFile` so as not to block - it's just here for example 61 | 62 | adapter = mongoAdapter({ 63 | "uri": { 64 | "hosts": [ 65 | { 66 | "host": "db01.mysite.com", 67 | "port": 27017 68 | }, 69 | { 70 | "host": "db02.mysite.com", 71 | "port": 27017 72 | } 73 | ], 74 | "username": "admin", 75 | "password": "password", 76 | "database": "socket-io", 77 | "options": { 78 | "authSource": "admin", 79 | "replicaSet": "myreplset", 80 | "ssl": true 81 | } 82 | }, 83 | "server": { 84 | //"sslCA": sslCA 85 | "sslValidate": false 86 | }, 87 | "replSet": { 88 | //"sslCA": sslCA 89 | "sslValidate": false 90 | } 91 | "prefix": 'myprefix', 92 | "collectionName": 'mypubsub' 93 | }); 94 | 95 | io.adapter(adapter); 96 | ``` 97 | 98 | > The options that are described here are passed through to [mubsub](https://github.com/scttnlsn/mubsub), which in turn passes them to the [native MongoDB driver](https://github.com/mongodb/node-mongodb-native). So you can include options that are relevant to those libraries. Also, if you pass an object in as the `uri` property, it is processed by [mongodb-uri](https://github.com/mongolab/mongodb-uri-node). 99 | 100 | 101 | ## Client error handling 102 | This adapter exposes the `pubsubClient` property (mubsub), as well as the `channel` that was created. You can bind to events on each of these resources: 103 | 104 | ```JavaScript 105 | var io = require('socket.io')(3000), 106 | mongoAdapter = require('socket.io-mongodb'), 107 | adapter = mongoAdapter('mongodb://localhost:27017/socket-io'); 108 | 109 | io.adapter(adapter); 110 | adapter.pubsubClient.on('error', console.error); 111 | adapter.channel.on('error', console.error); 112 | ``` 113 | 114 | 115 | ## Custom client 116 | You can inject your own pubsub client (i.e. if you already have an instance of mubsub you wish to use), using the `pubsubClient` property of the options. 117 | 118 | ```JavaScript 119 | var io = require('socket.io')(3000), 120 | mubsub = require('mubsub'), 121 | mongoAdapter = require('socket.io-mongodb'), 122 | client; 123 | 124 | client = mubsub('mongodb://localhost:27017/io-example'); 125 | channel = client.channel('test'); // the channel is optional 126 | 127 | io.adapter(mongoAdapter({ 128 | pubsubClient: mubsub, 129 | channel: channel // optional 130 | })); 131 | ``` 132 | 133 | ## Existing database connection 134 | You can inject an existing database connection, if you are _not_ injecting the `pubsubClient`. 135 | 136 | ```JavaScript 137 | var io = require('socket.io')(3000), 138 | MongoClient = require('mongodb').MongoClient, 139 | mongoAdapter = require('socket.io-mongodb'), 140 | client; 141 | 142 | MongoClient.connect('mongodb://localhost:27017/io-example', function(err, db) { 143 | io.adapter(mongoAdapter({ 144 | mongoClient: db 145 | })); 146 | }); 147 | ``` 148 | 149 | > The MongoClient is exposed as `db` on the adapter, so you can leverage it if you need to: `myAdapter.db` 150 | 151 | 152 | ## Overriding the messageEncoder 153 | If you don't want or need to be able to read the message data in the database, you may benefit from overriding the messageEncoder. The following example uses `msgpack-js` to encode/decode the message data. 154 | 155 | ```JavaScript 156 | var io = require('socket.io')(3000), 157 | mongoAdapter = require('socket.io-mongodb'), 158 | msgpack = require('msgpack-js'); 159 | 160 | io.adapter(mongoAdapter({ 161 | uri: 'mongodb://localhost:27017/socket-io', 162 | messageEncoder: { 163 | encode: function (data) { 164 | return msgpack.encode(data); 165 | }, 166 | decode: function (data) { 167 | return msgpack.decode(data.buffer); 168 | } 169 | } 170 | })); 171 | ``` 172 | 173 | ## Protocol 174 | The `socket.io-mongodb` adapter broadcasts and receives messages on particularly named channels. For global broadcasts the channel name is: 175 | ``` 176 | prefix + '#' + namespace + '#' 177 | ``` 178 | 179 | In broadcasting to a single room the channel name is: 180 | ``` 181 | prefix + '#' + namespace + '#' + room + '#' 182 | ``` 183 | 184 | * **prefix**: a prefix to be used when publishing events (default is _socket-io_). You can change this by setting the `prefix` value in the constructor `options` 185 | * **namespace**: see https://github.com/socketio/socket.io#namespace. 186 | * **room** : used if targeting a specific room. 187 | 188 | 189 | ## License 190 | MIT 191 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var adapter = require('./lib/adapter.js'); 2 | 3 | module.exports = adapter; 4 | -------------------------------------------------------------------------------- /lib/PubSubClient.js: -------------------------------------------------------------------------------- 1 | var mubsub = require('mubsub'), 2 | mongodbUri = require('mongodb-uri'); 3 | 4 | module.exports = function (options) { 5 | 'use strict'; 6 | 7 | var uri; 8 | 9 | if (options.pubsubClient) { 10 | return options.pubsubClient; 11 | } 12 | 13 | if (options.mongoClient) { 14 | return mubsub(options.mongoClient); 15 | } 16 | 17 | 18 | if (typeof options.uri === 'string') { 19 | uri = options.uri; 20 | } else { 21 | uri = (mongodbUri.format(options.uri)); 22 | } 23 | 24 | var mongoDBO = Object.assign({}, options); 25 | 26 | delete mongoDBO.uri; 27 | delete mongoDBO.prefix; 28 | delete mongoDBO.channel; 29 | delete mongoDBO.collectionName; 30 | delete mongoDBO.messageEncoder; 31 | 32 | return mubsub(uri, mongoDBO); 33 | }; 34 | -------------------------------------------------------------------------------- /lib/adapter.js: -------------------------------------------------------------------------------- 1 | /*jshint bitwise: false*/ 2 | var Adapter = require('socket.io-adapter'), 3 | async = require('async'), 4 | PubSubClient = require('./PubSubClient.js'), 5 | messageEncoder = require('./messageEncoder'), 6 | makeUid; 7 | 8 | makeUid = function () { 9 | 'use strict'; 10 | 11 | var templateString = 'xxxxxxxx'; 12 | 13 | return templateString.replace(/[xy]/g, function (c) { 14 | var r = Math.random() * 16 | 0, v = c === 'x' ? r : r & 3 | 8; 15 | return v.toString(16); 16 | }); 17 | }; 18 | 19 | module.exports = function (uri, options) { 20 | 'use strict'; 21 | 22 | var pubsubCli, 23 | serverId = makeUid(), 24 | channel, 25 | MongoAdapter, 26 | makeChannelName, 27 | channels = {}; 28 | 29 | // handle options only 30 | if (typeof uri === 'object') { 31 | options = uri; 32 | uri = null; 33 | } else { 34 | options = options || {}; 35 | options.uri = uri; 36 | } 37 | 38 | options.prefix = options.prefix || 'socket-io'; 39 | options.collectionName = options.collectionName || 'pubsub'; 40 | options.messageEncoder = options.messageEncoder || messageEncoder; 41 | 42 | pubsubCli = new PubSubClient(options); 43 | // only accept the channel if the pubsubClient was also defined 44 | channel = (options.pubsubClient && options.channel) || pubsubCli.channel(options.collectionName); 45 | 46 | makeChannelName = function (namespaceName, roomName) { 47 | var name = options.prefix + '::' + (!namespaceName || namespaceName === '/' ? '' : (namespaceName + '::')); 48 | 49 | if (roomName) { 50 | name += roomName + '::'; 51 | } 52 | 53 | return name; 54 | }; 55 | 56 | /** 57 | // Adapter constructor 58 | // 59 | // @param namespace (string): The namespace for this adapter 60 | */ 61 | MongoAdapter = function (namespace) { 62 | var self = this; 63 | 64 | Adapter.call(self, namespace); 65 | 66 | self.uid = serverId; 67 | self.prefix = options.prefix; 68 | self.pubsubClient = pubsubCli; 69 | self.channel = channel; 70 | self.db = pubsubCli.db; 71 | 72 | self.channel.subscribe(makeChannelName(namespace.name), self.onmessage.bind(self)); 73 | }; 74 | 75 | /** 76 | // MongoAdapter inherits Adapter 77 | */ 78 | MongoAdapter.prototype.__proto__ = Adapter.prototype; 79 | 80 | /** 81 | // Subscriber callback is called when a new message is inserted into the collection 82 | */ 83 | MongoAdapter.prototype.onmessage = function (msg) { 84 | var self = this, packet, opts; 85 | 86 | if (msg.uid === serverId || !msg.uid) { 87 | // 'the message is from this server - ignoring' 88 | return false; 89 | } 90 | 91 | packet = options.messageEncoder.decode(msg.packet); 92 | opts = options.messageEncoder.decode(msg.options); 93 | 94 | packet.nsp = packet.nsp || '/'; 95 | 96 | if (packet.nsp !== self.nsp.name) { 97 | // 'the message is for a different namespace - ignoring' 98 | return false; 99 | } 100 | 101 | // make sure to add remote=true to the args, so broadcast doesn't cause an infinite loop 102 | self.broadcast.apply(self, [packet, opts, true /*remote*/]); 103 | }; 104 | 105 | /** 106 | // Broadcast a message (packet) 107 | // 108 | // @param packet (Object): the message to broadcase 109 | // @param opts (Object): the options 110 | // @param remote (Boolean): whether or not the packet is from another node 111 | */ 112 | MongoAdapter.prototype.broadcast = function (packet, opts, remote) { 113 | var self = this; 114 | 115 | Adapter.prototype.broadcast.call(self, packet, opts); 116 | 117 | if (!remote) { 118 | if (opts.rooms) { 119 | opts.rooms.forEach(function (room) { 120 | self.channel.publish( 121 | makeChannelName(packet.nsp, room), 122 | { 123 | uid: serverId, 124 | packet: options.messageEncoder.encode(packet), 125 | options: options.messageEncoder.encode(opts) 126 | } 127 | ); 128 | }); 129 | } else { 130 | self.channel.publish( 131 | makeChannelName(packet.nsp), 132 | { 133 | uid: serverId, 134 | packet: options.messageEncoder.encode(packet), 135 | options: options.messageEncoder.encode(opts) 136 | } 137 | ); 138 | } 139 | } 140 | }; 141 | 142 | 143 | /** 144 | // Subscribe the client to room messages 145 | // 146 | // @param clientId (string): the client id 147 | // @param roomName (string): the room name 148 | // @param callback (function): optional callback when subscribing to a room 149 | */ 150 | MongoAdapter.prototype.add = function (clientId, roomName, callback) { 151 | var self = this, 152 | channelName; 153 | 154 | //debug('adding %s to %s ', clientId, roomName); 155 | Adapter.prototype.add.call(self, clientId, roomName); 156 | channelName = makeChannelName(self.nsp.name, roomName); 157 | 158 | channels[channelName] = self.channel.subscribe(channelName, self.onmessage.bind(self)); 159 | 160 | if (typeof callback === 'function') { 161 | process.nextTick(callback.bind(null, null)); 162 | } 163 | }; 164 | 165 | /** 166 | // Unsubscribe the client from a room 167 | // 168 | // @param clientId (string): the client id 169 | // @param roomName (string): the room name 170 | // @param callback (function): optional callback when subscribing to a room 171 | */ 172 | MongoAdapter.prototype.del = function (clientId, roomName, callback) { 173 | var self = this, 174 | channelName, 175 | hasRoom; 176 | 177 | //debug('removing %s from %s', clientId, roomName); 178 | hasRoom = self.rooms.hasOwnProperty(roomName); 179 | Adapter.prototype.del.call(self, clientId, roomName); 180 | 181 | if (hasRoom && !self.rooms[roomName]) { 182 | channelName = makeChannelName(self.nsp.name, roomName); 183 | 184 | if (channels[channelName]) { 185 | channels[channelName].unsubscribe(); 186 | } 187 | } else if (typeof callback === 'function') { 188 | process.nextTick(callback.bind(null, null)); 189 | } 190 | }; 191 | 192 | /** 193 | // Unsubscribe the client from all rooms 194 | // 195 | // @param clientId (string): the client id 196 | // @param callback (function): optional callback when subscribing to a room 197 | */ 198 | MongoAdapter.prototype.delAll = function (clientId, callback) { 199 | var self = this, 200 | rooms = self.sids[clientId]; 201 | 202 | //debug('removing %s from all rooms', clientId); 203 | if (!rooms) { 204 | if (typeof callback === 'function') { 205 | process.nextTick(callback.bind(null, null)); 206 | } 207 | return; 208 | } 209 | 210 | async.forEach(Object.keys(rooms), function (room, next) { 211 | self.del(clientId, room, next); 212 | }, function (err) { 213 | if (err) { 214 | self.emit('error', err); //adapter.emit('error', err); 215 | if (typeof callback === 'function') { 216 | callback(err); 217 | } 218 | } else if (typeof callback === 'function') { 219 | delete self.sids[clientId]; 220 | callback(null); 221 | } 222 | } 223 | ); 224 | }; 225 | 226 | MongoAdapter.uid = serverId; 227 | MongoAdapter.prefix = options.prefix; 228 | MongoAdapter.pubsubClient = pubsubCli; 229 | MongoAdapter.channel = channel; 230 | MongoAdapter.db = pubsubCli.db; 231 | 232 | return MongoAdapter; 233 | }; 234 | -------------------------------------------------------------------------------- /lib/messageEncoder.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | encode: JSON.stringify, 3 | decode: JSON.parse 4 | }; 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "socket.io-mongodb", 3 | "version": "1.1.1", 4 | "description": "A MongoDB Adapter for socket.io", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "mocha ./tests" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/losandes/socket.io-mongodb.git" 12 | }, 13 | "keywords": [ 14 | "socket.io", 15 | "mongo", 16 | "mongodb", 17 | "adapter", 18 | "backplane" 19 | ], 20 | "author": "Andy Wright (https://github.com/losandes)", 21 | "license": "MIT", 22 | "bugs": { 23 | "url": "https://github.com/losandes/socket.io-mongodb/issues" 24 | }, 25 | "homepage": "https://github.com/losandes/socket.io-mongodb#readme", 26 | "dependencies": { 27 | "async": "^1.5.2", 28 | "mongodb-uri": "^0.9.7", 29 | "mubsub": "^1.1.2", 30 | "socket.io-adapter": "^0.4.0" 31 | }, 32 | "devDependencies": { 33 | "chai": "^3.3.0", 34 | "mocha": "^2.3.4", 35 | "mongodb": "^2.1.4", 36 | "msgpack-js": "^0.3.0", 37 | "nconf": "^0.8.2", 38 | "socket.io": "^1.4.4", 39 | "socket.io-client": "^1.4.4" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /tests/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var http = require('http'), 4 | io = require('socket.io'), 5 | socketioClient = require('socket.io-client'), 6 | chaiBdd = require('chai'), 7 | expect = chaiBdd.expect, 8 | mubsub = require('mubsub'), 9 | MongoClient = require('mongodb').MongoClient, 10 | async = require('async'), 11 | mongoAdapter = require('../index.js'), 12 | nconf = require('nconf'), 13 | env = nconf.env().argv().file('environment', './environment/env.json'), 14 | fixture01 = require('./socketio-mongodb.fixture.js'), 15 | fixture02 = require('./mubsubCli.fixture.js'), 16 | fixture03 = require('./mongoCli.fixture.js'), 17 | fixture04 = require('./messageEncoder.fixture.js'), 18 | db = env.get('db') || { 19 | connx: 'mongodb://localhost:27017/socket-io-tests', 20 | options: null 21 | }, 22 | makeServer; 23 | 24 | console.log('Connecting to:', db.connx || db.hosts); 25 | 26 | makeServer = function (adapter, namespace, callback) { 27 | var server = http.Server(), 28 | sio = io(server); 29 | 30 | sio.adapter(adapter); 31 | 32 | server.listen(function (err) { 33 | var address, 34 | url; 35 | 36 | if (err) { 37 | throw err; 38 | } 39 | 40 | if (typeof namespace === 'function') { 41 | callback = namespace; 42 | namespace = '/'; 43 | } 44 | 45 | namespace = namespace || '/'; 46 | address = server.address(); 47 | url = 'http://localhost:' + address.port + namespace; 48 | 49 | if (typeof callback === 'function') { 50 | callback(sio.of(namespace), socketioClient(url), adapter); 51 | } 52 | }); 53 | }; 54 | 55 | async.series([ 56 | function (callback) { 57 | fixture02(mubsub, makeServer, expect, mongoAdapter, async, callback); 58 | }, 59 | function (callback) { 60 | fixture03(MongoClient, makeServer, expect, mongoAdapter, async, callback); 61 | }, 62 | function (callback) { 63 | fixture04(makeServer, expect, mongoAdapter, async, callback); 64 | }, 65 | function (callback) { 66 | fixture01(db, makeServer, expect, mongoAdapter, async, callback); 67 | } 68 | ], function () {}); 69 | -------------------------------------------------------------------------------- /tests/messageEncoder.fixture.js: -------------------------------------------------------------------------------- 1 | /*globals describe, it, after*/ 2 | 'use strict'; 3 | 4 | var msgpack = require('msgpack-js'); 5 | 6 | module.exports = function (makeServer, expect, mongoAdapter, async, next) { 7 | var makeSocketIOServer; 8 | 9 | // Setup 10 | (function () { 11 | makeSocketIOServer = function (namespace, callback) { 12 | var adapter = mongoAdapter({ 13 | uri: 'mongodb://localhost:27017/socket-io', 14 | messageEncoder: { 15 | encode: function (data) { 16 | return msgpack.encode(data); 17 | }, 18 | decode: function (data) { 19 | return msgpack.decode(data.buffer); 20 | } 21 | } 22 | }); 23 | 24 | makeServer(adapter, namespace, callback); 25 | }; 26 | }()); 27 | 28 | describe('when injecting a messageEncoder', function () { 29 | var serverTask; 30 | 31 | this.timeout(30000); 32 | 33 | after(function() { 34 | next(); 35 | }); 36 | 37 | serverTask = function (callback) { 38 | makeSocketIOServer('/mongo', function (server, client, adapter) { 39 | callback(null, { 40 | server: server, 41 | client: client, 42 | adapter: adapter 43 | }); 44 | }); 45 | }; 46 | 47 | it('should broadcast on the default namespace', function (done) { 48 | async.parallel([serverTask, serverTask], function (err, results) { 49 | results[0].client.on('test', function(a, b){ 50 | expect(Array.isArray(a)).to.equal(true); 51 | expect(a.length).to.equal(0); 52 | expect(b.a).to.equal('a'); 53 | 54 | done(); 55 | }); 56 | 57 | results[1].server.on('connection', function(client){ 58 | client.broadcast.emit('test', [], { a: 'a' }); 59 | }); 60 | }); // /async 61 | }); // /it 62 | }); 63 | }; 64 | -------------------------------------------------------------------------------- /tests/mongoCli.fixture.js: -------------------------------------------------------------------------------- 1 | /*globals describe, it, after*/ 2 | 'use strict'; 3 | 4 | module.exports = function (MongoClient, makeServer, expect, mongoAdapter, async, next) { 5 | var makeSocketIOServer; 6 | 7 | // Setup 8 | (function () { 9 | makeSocketIOServer = function (namespace, callback) { 10 | MongoClient.connect('mongodb://localhost:27017/socket-io-tests', function (err, db) { 11 | var adapter; 12 | 13 | if (err) { 14 | throw err; 15 | } 16 | 17 | adapter = mongoAdapter({ 18 | mongoClient: db, 19 | collectionName: 'mongoclitest' 20 | }); 21 | 22 | makeServer(adapter, namespace, callback); 23 | }); 24 | }; 25 | }()); 26 | 27 | describe('when injecting a mongoClient', function () { 28 | var serverTask; 29 | 30 | this.timeout(30000); 31 | 32 | after(function() { 33 | next(); 34 | }); 35 | 36 | serverTask = function (callback) { 37 | makeSocketIOServer('/mongo', function (server, client, adapter) { 38 | callback(null, { 39 | server: server, 40 | client: client, 41 | adapter: adapter 42 | }); 43 | }); 44 | }; 45 | 46 | it('should broadcast on the default namespace', function (done) { 47 | async.parallel([serverTask, serverTask], function (err, results) { 48 | results[0].client.on('test', function(a, b){ 49 | expect(Array.isArray(a)).to.equal(true); 50 | expect(a.length).to.equal(0); 51 | expect(b.a).to.equal('a'); 52 | 53 | expect(typeof results[0].adapter.db).to.equal('object'); 54 | 55 | done(); 56 | }); 57 | 58 | results[1].server.on('connection', function(client){ 59 | client.broadcast.emit('test', [], { a: 'a' }); 60 | }); 61 | }); // /async 62 | }); // /it 63 | }); 64 | }; 65 | -------------------------------------------------------------------------------- /tests/mubsubCli.fixture.js: -------------------------------------------------------------------------------- 1 | /*globals describe, it, after*/ 2 | 'use strict'; 3 | 4 | module.exports = function (mubsub, makeServer, expect, mongoAdapter, async, next) { 5 | var channelName = 'mubsubclitest', 6 | makeSocketIOServer; 7 | 8 | // Setup 9 | (function () { 10 | makeSocketIOServer = function (namespace, callback) { 11 | var client, 12 | channel, 13 | adapter; 14 | 15 | client = mubsub('mongodb://localhost:27017/socket-io-tests'); 16 | channel = client.channel(channelName); 17 | adapter = mongoAdapter({ 18 | pubsubClient: client, 19 | channel: channel 20 | }); 21 | 22 | makeServer(adapter, namespace, callback); 23 | }; 24 | }()); 25 | 26 | describe('when injecting a pubsubClient', function () { 27 | var serverTask; 28 | 29 | this.timeout(30000); 30 | 31 | after(function() { 32 | next(); 33 | }); 34 | 35 | serverTask = function (callback) { 36 | makeSocketIOServer('/mubsub', function (server, client, adapter) { 37 | callback(null, { 38 | server: server, 39 | client: client, 40 | adapter: adapter 41 | }); 42 | }); 43 | }; 44 | 45 | it('should broadcast on the default namespace', function (done) { 46 | async.parallel([serverTask, serverTask], function (err, results) { 47 | results[0].client.on('test', function(a, b){ 48 | expect(Array.isArray(a)).to.equal(true); 49 | expect(a.length).to.equal(0); 50 | expect(b.a).to.equal('a'); 51 | 52 | expect(results[0].adapter.pubsubClient.channels[channelName].name).to.equal(channelName); 53 | expect(results[0].adapter.channel.name).to.equal(channelName); 54 | 55 | done(); 56 | }); 57 | 58 | results[1].server.on('connection', function(client){ 59 | client.broadcast.emit('test', [], { a: 'a' }); 60 | }); 61 | }); // /async 62 | }); // /it 63 | }); 64 | }; 65 | -------------------------------------------------------------------------------- /tests/socketio-mongodb.fixture.js: -------------------------------------------------------------------------------- 1 | /*globals describe, it, xit, after*/ 2 | module.exports = function (db, makeServer, expect, mongoAdapter, async, next) { 3 | 'use strict'; 4 | 5 | var makeSocketIOServer; 6 | 7 | // Setup 8 | (function () { 9 | makeSocketIOServer = function (namespace, callback) { 10 | var adapter; 11 | 12 | if (db.connx) { 13 | adapter = mongoAdapter(db.connx, db.options); 14 | } else { 15 | adapter = mongoAdapter(db); 16 | } 17 | 18 | makeServer(adapter, namespace, callback); 19 | }; 20 | }()); 21 | 22 | describe('socket.io-mongodb', function () { 23 | var makeServerTask, broadcastToRoomsSpec; 24 | 25 | this.timeout(30000); 26 | 27 | makeServerTask = function (name) { 28 | if (name) { 29 | return function (callback) { 30 | makeSocketIOServer(name, function (server, client) { 31 | callback(null, { 32 | server: server, 33 | client: client 34 | }); 35 | }); 36 | }; 37 | } else { 38 | return function (callback) { 39 | makeSocketIOServer(function (server, client) { 40 | callback(null, { 41 | server: server, 42 | client: client 43 | }); 44 | }); 45 | }; 46 | } 47 | }; 48 | 49 | after(function() { 50 | next(); 51 | }); 52 | 53 | it('should broadcast on the default namespace', function (done) { 54 | async.parallel([makeServerTask(), makeServerTask()], function (err, results) { 55 | results[0].client.on('test', function(a, b){ 56 | expect(Array.isArray(a)).to.equal(true); 57 | expect(a.length).to.equal(0); 58 | expect(b.a).to.equal('a'); 59 | done(); 60 | }); 61 | 62 | results[1].server.on('connection', function(client){ 63 | client.broadcast.emit('test', [], { a: 'a' }); 64 | }); 65 | }); // /async 66 | }); // /it 67 | 68 | it('should broadcast on a specific namespace', function (done) { 69 | async.parallel([makeServerTask('/myns'), makeServerTask('/myns')], function (err, results) { 70 | results[0].client.on('test', function(a, b){ 71 | 72 | expect(Array.isArray(a)).to.equal(true); 73 | expect(a.length).to.equal(0); 74 | expect(b.b).to.equal('b'); 75 | done(); 76 | }); 77 | 78 | results[1].server.on('connection', function(c2){ 79 | c2.broadcast.emit('test', [], { b: 'b' }); 80 | }); 81 | }); 82 | }); // /it 83 | 84 | broadcastToRoomsSpec = function (results, done) { 85 | results[0].server.on('connection', function(client){ 86 | client.join('test'); 87 | }); 88 | 89 | results[1].server.on('connection', function(client){ 90 | // does not join, performs broadcast 91 | client.on('do broadcast', function(){ 92 | client.broadcast.to('test').emit('broadcast', [], { c: 'c' }); 93 | }); 94 | }); 95 | 96 | results[2].server.on('connection', function(/*client*/){ 97 | results[0].client.on('broadcast', function(a, b){ 98 | expect(Array.isArray(a)).to.equal(true); 99 | expect(a.length).to.equal(0); 100 | expect(b.c).to.equal('c'); 101 | 102 | results[0].client.disconnect(); 103 | results[1].client.disconnect(); 104 | results[2].client.disconnect(); 105 | 106 | setTimeout(done, 100); 107 | }); 108 | 109 | results[1].client.on('broadcast', function(){ 110 | throw new Error('Not in room'); 111 | }); 112 | 113 | results[2].client.on('broadcast', function(){ 114 | throw new Error('Not in room'); 115 | }); 116 | 117 | // does not join, signals broadcast 118 | results[1].client.emit('do broadcast'); 119 | }); 120 | }; 121 | 122 | it('should broadcast to rooms on the default namespace', function (done) { 123 | async.parallel([makeServerTask(), makeServerTask(), makeServerTask()], function (err, results) { 124 | broadcastToRoomsSpec(results, done); 125 | }); // /async 126 | 127 | }); // /it 128 | 129 | it('should broadcast to rooms on a specific namespace', function (done) { 130 | async.parallel([makeServerTask('/spns'), makeServerTask('/spns'), makeServerTask('/spns')], function (err, results) { 131 | broadcastToRoomsSpec(results, done); 132 | }); // /async 133 | 134 | }); // /it 135 | 136 | it('should NOT broadcast to rooms that have been left', function (done) { 137 | async.parallel([makeServerTask(), makeServerTask(), makeServerTask()], function (err, results) { 138 | results[0].server.on('connection', function (client) { 139 | client.join('leavetest'); 140 | client.leave('leavetest'); 141 | }); 142 | 143 | results[1].server.on('connection', function (client) { 144 | client.on('do broadcast', function () { 145 | client.broadcast.to('leavetest').emit('broadcast', [], { d: 'd' }); 146 | 147 | setTimeout(function () { 148 | results[0].client.disconnect(); 149 | results[1].client.disconnect(); 150 | results[2].client.disconnect(); 151 | done(); 152 | }, 100); 153 | }); 154 | }); 155 | 156 | results[2].server.on('connection', function (/*client*/) { 157 | results[1].client.emit('do broadcast'); 158 | }); 159 | 160 | results[0].client.on('broadcast', function () { 161 | throw new Error('Not in room'); 162 | }); 163 | }); // /async 164 | }); // /it 165 | 166 | it('should delete rooms upon disconnection', function (done) { 167 | async.parallel([makeServerTask()], function (err, results) { 168 | results[0].server.on('connection', function (client) { 169 | client.join('leavealltest'); 170 | 171 | client.on('disconnect', function () { 172 | var prop, roomCount = 0, sidsCount = 0; 173 | 174 | for (prop in client.adapter.rooms) { 175 | if (client.adapter.rooms.hasOwnProperty(prop)) { 176 | roomCount += 1; 177 | } 178 | } 179 | 180 | for (prop in client.adapter.sids[client.id]) { 181 | if (client.adapter.sids[client.id].hasOwnProperty(prop)) { 182 | sidsCount += 1; 183 | } 184 | } 185 | 186 | expect(sidsCount).to.equal(0); 187 | expect(roomCount).to.equal(0); 188 | client.disconnect(); 189 | done(); 190 | }); 191 | 192 | client.disconnect(); 193 | }); 194 | }); // /async 195 | }); // /it 196 | 197 | }); 198 | }; 199 | --------------------------------------------------------------------------------