├── .gitignore ├── index.js ├── Makefile ├── bench ├── pubsub.js ├── storage.js └── index.js ├── package.json ├── readme.md ├── lib └── mongo.js └── test └── mongo.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./lib/mongo'); 2 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | test: 2 | node_modules/.bin/qunit -c ./lib/mongo -t ./test/mongo 3 | 4 | .PHONY: test 5 | -------------------------------------------------------------------------------- /bench/pubsub.js: -------------------------------------------------------------------------------- 1 | exports.run = function(amount, data, create, callback) { 2 | var sub = create(), 3 | pub = create(), 4 | i = 0, 5 | received = 0; 6 | 7 | sub.subscribe('myevent', function() { 8 | received++; 9 | if (received == amount) { 10 | callback(); 11 | } 12 | }); 13 | 14 | (function publish() { 15 | pub.publish('myevent', data); 16 | if (i < amount) { 17 | process.nextTick(publish); 18 | } 19 | }()); 20 | }; -------------------------------------------------------------------------------- /bench/storage.js: -------------------------------------------------------------------------------- 1 | exports.run = function(amount, data, create, callback) { 2 | var client = create().client(Date.now()); 3 | 4 | (function run() { 5 | client.set('a', data, function() { 6 | client.get('a', function(err, val) { 7 | if (val != data) { 8 | console.error('Failed'); 9 | process.exit(1); 10 | } 11 | client.del('a', function() { 12 | if (amount--) { 13 | run(); 14 | } else { 15 | callback(); 16 | } 17 | }); 18 | }); 19 | }); 20 | 21 | }()); 22 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name" : "socket.io-mongo", 3 | "description": "Socket.io store implementation backed by mongodb.", 4 | "version" : "0.0.8", 5 | "engines" : { 6 | "node": "0.6.x" 7 | }, 8 | "keywords": [ "mongodb", "socket.io", "pubsub"], 9 | "author": "Oleg Slobodskoi ", 10 | "repository": "git://github.com/kof/socket.io-mongo.git", 11 | "dependencies": { 12 | "mubsub": "0.0.2", 13 | "socket.io": "0.9.x", 14 | "underscore": "1.3.3" 15 | }, 16 | "devDependencies": { 17 | "qunit": "0.5.3", 18 | "chainer": "0.0.5", 19 | "argsparser": "0.0.6" 20 | }, 21 | "licenses": [ 22 | { 23 | "type": "MIT", 24 | "url" : "http://www.opensource.org/licenses/mit-license.php" 25 | } 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /bench/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Pubsub bench should show how much and how fast events can be published and received comparing redis and mongo. 3 | * Store bench should show how much and how fast key/value can be get, set and del comparing redis and mongo. 4 | */ 5 | 6 | var Mongo = require('../'), 7 | Redis = require('socket.io').RedisStore, 8 | opts = require('argsparser').parse(); 9 | 10 | if (!opts['--db'] || !opts['--test']) { 11 | console.error('Usage: node bench --db redis|mongo --test pubsub|storage'); 12 | process.exit(1); 13 | } 14 | 15 | var db = opts['--db'], 16 | test = opts['--test'], 17 | amount = opts['--amount'] || 15000, 18 | data = opts['--data'] || 'mytestdata'; 19 | 20 | console.error('Testing', test, ', using', db, ', amount:', amount, ', data:', data); 21 | console.time(test); 22 | require('./' + test).run(amount, data, create, function() { 23 | console.timeEnd(test); 24 | process.exit(); 25 | }); 26 | 27 | function create() { 28 | var store; 29 | 30 | if (db == 'mongo') { 31 | store = new Mongo({ 32 | url: 'mongodb://localhost:27017/socketio', 33 | size: 1000000 34 | }); 35 | 36 | store.on('error', console.error); 37 | } else if (db == 'redis') { 38 | store = new Redis(); 39 | } 40 | 41 | return store; 42 | } 43 | 44 | 45 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | ## Socket.io store implementation backed by mongodb. 2 | 3 | This store is for people who is already using mongodb and don't want to add redis. Pubsub is implemented via "mubsub" module, which is using capped collections and tailable cursors. 4 | 5 | Any benches or experiences in comparison to redis are welcome. 6 | 7 | ### Install 8 | 9 | If "msgpack" is installed, it will be used instead of JSON.stringify/parse. 10 | 11 | npm i socket.io-mongo 12 | 13 | ### Usage example 14 | 15 | var socketio = require('socket.io'), 16 | express = require('express'), 17 | MongoStore = require('socket.io-mongo'), 18 | app = express.createServer(), 19 | io = io.listen(app); 20 | 21 | app.listen(8000); 22 | 23 | io.configure(function() { 24 | var store = new MongoStore({url: 'mongodb://localhost:27017/yourdb'}); 25 | store.on('error', console.error); 26 | io.set('store', store); 27 | }); 28 | 29 | io.sockets.on('connection', function (socket) { 30 | socket.emit('news', { hello: 'world' }); 31 | socket.on('my other event', function (data) { 32 | console.log(data); 33 | }); 34 | }); 35 | 36 | ### Options 37 | 38 | // Default options 39 | { 40 | // collection name is prefix + name 41 | collectionPrefix: 'socket.io.', 42 | // capped collection name 43 | streamCollection: 'stream', 44 | // collection name used for key/value storage 45 | storageCollection: 'storage', 46 | // id that uniquely identifies this node 47 | nodeId: null, 48 | // max size in bytes for capped collection 49 | size: 100000, 50 | // max number of documents inside of capped collection 51 | num: null, 52 | // db url e.g. "mongodb://localhost:27017/yourdb" 53 | url: null, 54 | // optionally you can pass everything separately 55 | host: 'localhost', 56 | port: 27017, 57 | db: 'socketio' 58 | } 59 | 60 | new MongoStore(options); 61 | 62 | ### Benchmarks 63 | 64 | On my mb air with locally installed db's, using absolutely the same code, only different storages. Testing pubsub means each event is published and received, testing storage means every key is set, read and deleted. 65 | 66 | node bench --db mongo --test pubsub --amount 50000 67 | Testing pubsub , using mongo , amount: 50000 , data: mytestdata 68 | pubsub: 5772ms 69 | 70 | node bench --db redis --test pubsub --amount 50000 71 | Testing pubsub , using redis , amount: 50000 , data: mytestdata 72 | pubsub: 5106ms 73 | 74 | node bench --db mongo --test storage --amount 20000 75 | Testing storage , using mongo , amount: 20000 , data: mytestdata 76 | storage: 8382ms 77 | 78 | node bench --db redis --test storage --amount 20000 79 | Testing storage , using redis , amount: 20000 , data: mytestdata 80 | storage: 6224ms 81 | 82 | ### Run tests 83 | 84 | npm i 85 | make test 86 | 87 | -------------------------------------------------------------------------------- /lib/mongo.js: -------------------------------------------------------------------------------- 1 | var util = require('util'), 2 | _ = require('underscore'), 3 | mubsub = require('mubsub'), 4 | Store = require('socket.io').Store; 5 | 6 | var noop = function() {}, 7 | msgpack, 8 | stringify = JSON.stringify, 9 | parse = JSON.parse, 10 | connected = false, 11 | instances = 0; 12 | 13 | try { 14 | msgpack = require('msgpack'); 15 | stringify = msgpack.pack; 16 | parse = msgpack.unpack; 17 | } catch(e) {} 18 | 19 | /** 20 | * Mongo store constructor. 21 | * 22 | * @see Mongo.options 23 | * @api public 24 | */ 25 | function Mongo(options) { 26 | var self = this; 27 | 28 | options = _.extend({}, Mongo.options, options); 29 | 30 | // Node id to uniquely identify this node. 31 | this._nodeId = options.nodeId || Math.round(Math.random() * Date.now()); 32 | this._subscriptions = {}; 33 | this._channel = mubsub.channel(options.collectionPrefix + options.streamCollection, options); 34 | this._error = this._error.bind(this); 35 | 36 | mubsub.connection.db.then(function(err, db) { 37 | self.emit('connect', err, db); 38 | }); 39 | 40 | // all instances share one connection 41 | if (!connected) { 42 | connected = true; 43 | mubsub.connect(options); 44 | } 45 | 46 | instances++; 47 | this.setMaxListeners(0); 48 | Store.call(this, options); 49 | } 50 | 51 | util.inherits(Mongo, Store); 52 | 53 | module.exports = Mongo; 54 | Mongo.Mongo = Mongo; 55 | 56 | /** 57 | * Module version. 58 | * 59 | * @api public 60 | */ 61 | Mongo.version = require('../package.json').version; 62 | 63 | /** 64 | * Default options. 65 | * 66 | * @api public 67 | */ 68 | Mongo.options = { 69 | // collection name is prefix + name 70 | collectionPrefix: 'socket.io.', 71 | // capped collection name 72 | streamCollection: 'stream', 73 | // collection name used for key/value storage 74 | storageCollection: 'storage', 75 | // id that uniquely identifies this node 76 | nodeId: null, 77 | // max size in bytes for capped collection 78 | size: 100000, 79 | // max number of documents inside of capped collection 80 | num: null, 81 | // db url e.g. "mongodb://localhost:27017/yourdb" 82 | url: null, 83 | // optionally you can pass everything separately 84 | host: 'localhost', 85 | port: 27017, 86 | db: 'socketio' 87 | }; 88 | 89 | 90 | /** 91 | * Publishes a message. 92 | * Everything after 1. param will be published as a data. 93 | * 94 | * @param {String} event name. 95 | * @param {Mixed} any data. 96 | * @api public 97 | */ 98 | Mongo.prototype.publish = function(name, value) { 99 | var args = [].slice.call(arguments, 1); 100 | 101 | this._channel.publish({ 102 | name: name, 103 | nodeId: this._nodeId, 104 | args: stringify(args) 105 | }, this._error); 106 | 107 | this.emit.apply(this, ['publish', name].concat(args)); 108 | 109 | return this; 110 | }; 111 | 112 | /** 113 | * Subscribes to a channel. 114 | * 115 | * @param {String} event name. 116 | * @param {Function} callback. 117 | * @api public 118 | */ 119 | Mongo.prototype.subscribe = function(name, callback) { 120 | var self = this, 121 | // we check that the message consumed wasn't emitted by this node 122 | query = {name: name, nodeId: {$ne: this._nodeId}}; 123 | 124 | this._subscriptions[name] = this._channel.subscribe(query, function(err, doc) { 125 | if (err) { 126 | return self._error(err); 127 | } 128 | 129 | callback.apply(null, parse(doc.args)); 130 | }); 131 | 132 | this.emit('subscribe', name, callback); 133 | 134 | return this; 135 | }; 136 | 137 | /** 138 | * Unsubscribes. 139 | * 140 | * @param {String} [name] event name, if no name passed - all subscriptions 141 | * will be unsubscribed. 142 | * @param {Function} [callback] 143 | * @api public 144 | */ 145 | Mongo.prototype.unsubscribe = function(name, callback) { 146 | if (name) { 147 | if (this._subscriptions[name]) { 148 | this._subscriptions[name].unsubscribe(); 149 | delete this._subscriptions[name]; 150 | } 151 | } else { 152 | _.each(this._subscriptions, function(subscr) { 153 | subscr.unsubscribe(); 154 | }); 155 | this._subscriptions = {}; 156 | } 157 | 158 | (callback || noop)(); 159 | 160 | this.emit('unsubscribe', name, callback); 161 | 162 | return this; 163 | }; 164 | 165 | /** 166 | * Destroy the store. Close connection. 167 | * 168 | * @api public 169 | */ 170 | Mongo.prototype.destroy = function() { 171 | Store.prototype.destroy.call(this); 172 | this.removeAllListeners(); 173 | instances--; 174 | 175 | this._channel.close(); 176 | 177 | // only close db connection if this is the only instance, because 178 | // all instances sharing the same connection 179 | if (instances <= 0) { 180 | connected = false; 181 | instances = 0; 182 | mubsub.connection.close(this._error); 183 | } 184 | 185 | this.emit('destroy'); 186 | 187 | return this; 188 | }; 189 | 190 | /** 191 | * Emit error, create Error instance if error is a string. 192 | * 193 | * @param {String|Error} err. 194 | * @api private 195 | */ 196 | Mongo.prototype._error = function(err) { 197 | if (!err) { 198 | return this; 199 | } 200 | 201 | if (typeof err == 'string') { 202 | err = new Error(err); 203 | } 204 | 205 | this.emit('error', err); 206 | 207 | return this; 208 | }; 209 | 210 | /** 211 | * Get a collection for persistent data. 212 | * 213 | * @param {Function} callback. 214 | * @api protected 215 | */ 216 | Mongo.prototype.getPersistentCollection_ = function(callback) { 217 | var self = this, 218 | opts = this.options; 219 | 220 | if (this._persistentCollection) { 221 | return callback(null, this._persistentCollection); 222 | } 223 | 224 | mubsub.connection.db.then(function(err, db) { 225 | var name = opts.collectionPrefix + opts.storageCollection; 226 | 227 | if (err) { 228 | return callback(err); 229 | } 230 | 231 | db.collection(name, function(err, collection) { 232 | if (err) { 233 | return callback(err); 234 | } 235 | 236 | self._persistentCollection = collection; 237 | callback(null, collection); 238 | }); 239 | }); 240 | 241 | return this; 242 | }; 243 | 244 | /** 245 | * Client constructor 246 | * 247 | * @api private 248 | */ 249 | function Client () { 250 | Store.Client.apply(this, arguments); 251 | } 252 | 253 | util.inherits(Client, Store.Client); 254 | 255 | Mongo.Client = Client; 256 | 257 | /** 258 | * Gets a key. 259 | * 260 | * @param {String} key. 261 | * @param {Function} callback. 262 | * @api public 263 | */ 264 | Client.prototype.get = function(key, callback) { 265 | var self = this; 266 | 267 | this.store.getPersistentCollection_(function(err, collection) { 268 | if (err) { 269 | return callback(err); 270 | } 271 | 272 | collection.findOne({_id: self.id + key}, function(err, data) { 273 | if (err) { 274 | return callback(err); 275 | } 276 | 277 | callback(null, data ? data.value : null); 278 | }); 279 | }); 280 | 281 | return this; 282 | }; 283 | 284 | /** 285 | * Sets a key 286 | * 287 | * @param {String} key. 288 | * @param {Mixed} value. 289 | * @param {Function} [callback] 290 | * @api public 291 | */ 292 | Client.prototype.set = function(key, value, callback) { 293 | var self = this; 294 | 295 | callback || (callback = noop); 296 | 297 | this.store.getPersistentCollection_(function(err, collection) { 298 | if (err) { 299 | return callback(err); 300 | } 301 | 302 | collection.update( 303 | {_id: self.id + key}, 304 | {$set: {value: value, clientId: self.id}}, 305 | {upsert: true}, 306 | callback 307 | ); 308 | }); 309 | 310 | return this; 311 | }; 312 | 313 | /** 314 | * Has a key 315 | * 316 | * @param {String} key. 317 | * @param {Function} callback. 318 | * @api public 319 | */ 320 | Client.prototype.has = function(key, callback) { 321 | var self = this; 322 | 323 | this.store.getPersistentCollection_(function(err, collection) { 324 | if (err) { 325 | return callback(err); 326 | } 327 | 328 | collection.findOne({_id: self.id + key}, {_id: 1}, function(err, data) { 329 | if (err) { 330 | return callback(err); 331 | } 332 | 333 | callback(null, Boolean(data)); 334 | }); 335 | }); 336 | 337 | return this; 338 | }; 339 | 340 | /** 341 | * Deletes a key 342 | * 343 | * @param {String} key. 344 | * @param {Function} [callback]. 345 | * @api public 346 | */ 347 | Client.prototype.del = function(key, callback) { 348 | var self = this; 349 | 350 | callback || (callback = noop); 351 | 352 | this.store.getPersistentCollection_(function(err, collection) { 353 | if (err) { 354 | return callback(err); 355 | } 356 | 357 | collection.remove({_id: self.id + key}, function(err, data) { 358 | if (err) { 359 | return callback(err); 360 | } 361 | 362 | callback(null); 363 | }); 364 | }); 365 | 366 | return this; 367 | }; 368 | 369 | /** 370 | * Destroys the client. 371 | * 372 | * @param {Number} [expiration] number of seconds to expire data 373 | * @param {Function} [callback]. 374 | * @api public 375 | */ 376 | Client.prototype.destroy = function(expiration, callback) { 377 | var self = this; 378 | 379 | callback || (callback = noop); 380 | 381 | if (typeof expiration == 'number') { 382 | setTimeout(function() { 383 | self.destroy(null, callback); 384 | }, expiration * 1000); 385 | 386 | return this; 387 | } 388 | 389 | this.store.getPersistentCollection_(function(err, collection) { 390 | if (err) { 391 | return callback(err); 392 | } 393 | 394 | collection.remove({clientId: self.id}, callback); 395 | }); 396 | 397 | return this; 398 | }; -------------------------------------------------------------------------------- /test/mongo.js: -------------------------------------------------------------------------------- 1 | var chainer = require('chainer'); 2 | 3 | function create() { 4 | var store; 5 | 6 | store = new Mongo({ 7 | url: 'mongodb://localhost:27017/socketio' 8 | }); 9 | 10 | store.on('error', console.error); 11 | 12 | return store; 13 | } 14 | 15 | // since we are sharing the same connection between all instances, 16 | // connection will be closed only if the last instance was destroyed, 17 | // prevent this 18 | create(); 19 | 20 | test('test publishing doesnt get caught by the own store subscriber', function() { 21 | var a = create(), 22 | b = create(); 23 | 24 | stop(); 25 | expect(1); 26 | 27 | a.subscribe('myevent', function(arg) { 28 | equal(arg, 'bb', 'got event from correct server'); 29 | a.destroy(); 30 | b.destroy(); 31 | start(); 32 | }); 33 | 34 | a.publish('myevent', 'aa'); 35 | b.publish('myevent', 'bb'); 36 | }); 37 | 38 | test('unsubscribe', function() { 39 | var a = create(), 40 | b = create(); 41 | 42 | stop(); 43 | expect(1); 44 | 45 | a.subscribe('myevent', function(arg) { 46 | equal(arg, 'aa', 'got subscribed event before unsubscribe'); 47 | a.unsubscribe('myevent', function() { 48 | b.publish('myevent'); 49 | start(); 50 | }); 51 | }); 52 | 53 | b.publish('myevent', 'aa'); 54 | }); 55 | 56 | test('test publishing to multiple subscribers', function() { 57 | var a = create(), 58 | b = create(), 59 | c = create(), 60 | messages = 0; 61 | 62 | stop(); 63 | expect(19); 64 | 65 | function subscription(arg1, arg2, arg3) { 66 | equal(arg1, 1, 'arg1 is correct'); 67 | equal(arg2, 2, 'arg2 is correct'); 68 | equal(arg3, 3, 'arg3 is correct'); 69 | messages++; 70 | if (messages == 6) { 71 | equal(messages, 6, 'amount of received messages is correct'); 72 | a.destroy(); 73 | b.destroy(); 74 | c.destroy(); 75 | start(); 76 | } 77 | } 78 | 79 | 80 | a.subscribe('myevent', subscription); 81 | b.subscribe('myevent', subscription); 82 | c.subscribe('myevent', subscription); 83 | 84 | a.publish('myevent', 1, 2, 3); 85 | a.publish('myevent', 1, 2, 3); 86 | a.publish('myevent', 1, 2, 3); 87 | }); 88 | 89 | test('test storing data for a client', function() { 90 | var chain = chainer(), 91 | store = create(), 92 | rand = 'test-' + Date.now(), 93 | client = store.client(rand); 94 | 95 | stop(); 96 | expect(15); 97 | 98 | equal(client.id, rand, 'client id was set'); 99 | 100 | chain.add(function() { 101 | client.set('a', 'b', function(err) { 102 | equal(err, null, 'set without errors'); 103 | chain.next(); 104 | }); 105 | }); 106 | 107 | chain.add(function() { 108 | client.get('a', function(err, val) { 109 | equal(err, null, 'get without errors'); 110 | equal(val, 'b', 'get correct value'); 111 | chain.next(); 112 | }); 113 | }); 114 | 115 | chain.add(function() { 116 | client.has('a', function(err, has) { 117 | equal(err, null, 'has without errors'); 118 | equal(has, true, 'has correct value'); 119 | chain.next(); 120 | }); 121 | }); 122 | 123 | chain.add(function() { 124 | client.has('b', function(err, has) { 125 | equal(err, null, 'has negative without errors'); 126 | equal(has, false, 'has negative correct value'); 127 | chain.next(); 128 | }); 129 | }); 130 | 131 | chain.add(function() { 132 | client.del('a', function(err) { 133 | equal(err, null, 'del without errors'); 134 | chain.next(); 135 | }); 136 | }); 137 | 138 | chain.add(function() { 139 | client.has('a', function(err, has) { 140 | equal(err, null, 'has after del without errors'); 141 | equal(has, false, 'has after del correct value'); 142 | chain.next(); 143 | }); 144 | }); 145 | 146 | chain.add(function() { 147 | client.set('c', {a: 1}, function(err) { 148 | equal(err, null, 'set object without errors'); 149 | chain.next(); 150 | }); 151 | }); 152 | 153 | chain.add(function() { 154 | client.set('c', {a: 3}, function(err) { 155 | equal(err, null, 'modify object without errors'); 156 | chain.next(); 157 | }); 158 | }); 159 | 160 | chain.add(function() { 161 | client.get('c', function(err, val) { 162 | equal(err, null, 'get modified obj without errors'); 163 | deepEqual(val, {a: 3}, 'get modified obj'); 164 | chain.next(); 165 | }); 166 | }); 167 | 168 | chain.add(function() { 169 | store.destroy(); 170 | start(); 171 | }); 172 | 173 | chain.start(); 174 | }); 175 | 176 | test('test cleaning up clients data', function() { 177 | var chain = chainer(), 178 | store = create(), 179 | client1 = store.client(Date.now()), 180 | client2 = store.client(Date.now() + 1); 181 | 182 | stop(); 183 | expect(10); 184 | 185 | chain.add(function() { 186 | client1.set('a', 'b', function(err) { 187 | equal(err, null, 'client1 set without errors'); 188 | chain.next(); 189 | }); 190 | }); 191 | 192 | chain.add(function() { 193 | client2.set('c', 'd', function(err) { 194 | equal(err, null, 'client2 set without errors'); 195 | chain.next(); 196 | }); 197 | }); 198 | 199 | chain.add(function() { 200 | client1.has('a', function(err, has) { 201 | equal(err, null, 'client1 has without errors'); 202 | equal(has, true, 'client1 has correct value'); 203 | chain.next(); 204 | }); 205 | }); 206 | 207 | chain.add(function() { 208 | client2.has('c', function(err, has) { 209 | equal(err, null, 'client2 has without errors'); 210 | equal(has, true, 'client2 has correct value'); 211 | chain.next(); 212 | }); 213 | }); 214 | 215 | chain.add(function() { 216 | store.destroy(); 217 | store = create(); 218 | client1 = store.client(Date.now()); 219 | client2 = store.client(Date.now() + 1); 220 | chain.next(); 221 | }); 222 | 223 | chain.add(function() { 224 | client1.has('a', function(err, has) { 225 | equal(err, null, 'client1 after destroy has without errors'); 226 | equal(has, false, 'client1 after destroy has correct value'); 227 | chain.next(); 228 | }); 229 | }); 230 | 231 | chain.add(function() { 232 | client2.has('c', function(err, has) { 233 | equal(err, null, 'client2after destroy has without errors'); 234 | equal(has, false, 'client2 after destroy has correct value'); 235 | chain.next(); 236 | }); 237 | }); 238 | 239 | chain.add(start); 240 | 241 | chain.start(); 242 | }); 243 | 244 | test('test cleaning up a particular client', function() { 245 | var chain = chainer(), 246 | store = create(), 247 | id1 = Date.now(), 248 | id2 = Date.now() + 1, 249 | client1 = store.client(id1), 250 | client2 = store.client(id2); 251 | 252 | stop(); 253 | expect(12); 254 | 255 | chain.add(function() { 256 | client1.set('a', 'b', function(err) { 257 | equal(err, null, 'client1 set without errors'); 258 | chain.next(); 259 | }); 260 | }); 261 | 262 | chain.add(function() { 263 | client2.set('c', 'd', function(err) { 264 | equal(err, null, 'client2 set without errors'); 265 | chain.next(); 266 | }); 267 | }); 268 | 269 | chain.add(function() { 270 | client1.has('a', function(err, has) { 271 | equal(err, null, 'client1 has without errors'); 272 | equal(has, true, 'client1 has correct value'); 273 | chain.next(); 274 | }); 275 | }); 276 | 277 | chain.add(function() { 278 | client2.has('c', function(err, has) { 279 | equal(err, null, 'client2 has without errors'); 280 | equal(has, true, 'client2 has correct value'); 281 | chain.next(); 282 | }); 283 | }); 284 | 285 | chain.add(function() { 286 | equal(id1 in store.clients, true, 'client1 is in clients'); 287 | equal(id2 in store.clients, true, 'client2 is in clients'); 288 | store.destroyClient(id1); 289 | equal(id1 in store.clients, false, 'client1 is in clients'); 290 | equal(id2 in store.clients, true, 'client2 is in clients'); 291 | chain.next(); 292 | }); 293 | 294 | chain.add(function() { 295 | client1.has('a', function(err, has) { 296 | equal(err, null, 'client1 after destroy has without errors'); 297 | equal(has, false, 'client1 after destroy has correct value'); 298 | chain.next(); 299 | }); 300 | }); 301 | 302 | chain.add(function() { 303 | store.destroy(); 304 | start(); 305 | }); 306 | 307 | chain.start(); 308 | }); 309 | 310 | test('test destroy expiration', function() { 311 | var chain = chainer(), 312 | store = create(), 313 | id = Date.now(); 314 | client = store.client(id); 315 | 316 | stop(); 317 | expect(5); 318 | 319 | chain.add(function() { 320 | client.set('a', 'b', function(err) { 321 | equal(err, null, 'set without errors'); 322 | chain.next(); 323 | }); 324 | }); 325 | 326 | chain.add(function() { 327 | store.destroyClient(id, 1); 328 | setTimeout(function() { 329 | chain.next(); 330 | }, 500); 331 | }); 332 | 333 | chain.add(function() { 334 | client.get('a', function(err, val) { 335 | equal(err, null, 'get without errors'); 336 | equal(val, 'b', 'get correct value'); 337 | setTimeout(function() { 338 | chain.next(); 339 | }, 2000); 340 | }); 341 | }); 342 | 343 | chain.add(function() { 344 | client.get('a', function(err, val) { 345 | equal(err, null, 'get without errors after expiration'); 346 | equal(val, null, 'get correct value after expiration'); 347 | chain.next(); 348 | }); 349 | }); 350 | 351 | chain.add(function() { 352 | store.destroy(); 353 | start(); 354 | }); 355 | 356 | chain.start(); 357 | }); 358 | 359 | --------------------------------------------------------------------------------