├── .gitignore ├── Makefile ├── Readme.md ├── examples ├── json.js ├── late-connect.js ├── patterns.js ├── simple.js ├── unsubscribe-patterns.js └── unsubscribe.js ├── index.js ├── lib ├── client.js └── node.js └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | test: 3 | @./node_modules/.bin/mocha \ 4 | --require should \ 5 | --reporter dot \ 6 | --bail 7 | 8 | .PHONY: test -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | 2 | # multipub 3 | 4 | Multi-redis pub/sub allowing you to publish and subscribe to and from N redis nodes. 5 | 6 | ## Installation 7 | 8 | ``` 9 | $ npm install multipub 10 | ``` 11 | 12 | ## Example 13 | 14 | Launch some redis nodes: 15 | 16 | ``` 17 | $ redis-server --port 4000 & 18 | $ redis-server --port 4001 & 19 | $ redis-server --port 4002 & 20 | ``` 21 | 22 | PUB/SUB: 23 | 24 | ```js 25 | var multipub = require('multipub'); 26 | 27 | var client = multipub(); 28 | 29 | client.connect('localhost:4000'); 30 | client.connect('localhost:4001'); 31 | client.connect('localhost:4002'); 32 | 33 | client.subscribe('user:*'); 34 | 35 | client.on('message', function(channel, msg){ 36 | console.log('%s - %s', channel, msg); 37 | }); 38 | 39 | setInterval(function(){ 40 | client.publish('user:login', { name: 'tobi' }); 41 | }, 50); 42 | 43 | setInterval(function(){ 44 | client.publish('user:login', { name: 'loki' }); 45 | }, 500); 46 | ``` 47 | 48 | ## API 49 | 50 | ### multipub(options) 51 | 52 | Options are passed to node_redis's `.createClient()`. 53 | 54 | ```js 55 | var multipub = require('multipub'); 56 | var client = multipub({ 57 | options: 'here' 58 | }); 59 | ``` 60 | 61 | ### Client#connect(addr) 62 | 63 | Add connection to `addr`, for example "0.0.0.0:4000". 64 | 65 | ### Client#subscribe(pattern) 66 | 67 | Subscribe to a channel or pattern, such as "user:login" or "user:*". 68 | 69 | ### Client#unsubscribe(pattern) 70 | 71 | Unsubscribe from a channel or pattern, must match _exactly_ what you 72 | passed to `.subscribe()`. 73 | 74 | # License 75 | 76 | MIT -------------------------------------------------------------------------------- /examples/json.js: -------------------------------------------------------------------------------- 1 | 2 | var multipub = require('..'); 3 | 4 | // launch: 5 | // $ redis-server --port 4000 & 6 | // $ redis-server --port 4001 & 7 | // $ redis-server --port 4002 & 8 | 9 | var client = multipub(); 10 | 11 | client.connect('localhost:4000'); 12 | client.connect('localhost:4001'); 13 | client.connect('localhost:4002'); 14 | 15 | client.subscribe('*'); 16 | 17 | client.on('message', function(channel, msg){ 18 | console.log('%s - %s', channel, msg); 19 | }); 20 | 21 | setInterval(function(){ 22 | client.publish('user:login', { name: 'tobi' }); 23 | }, 50); 24 | 25 | setInterval(function(){ 26 | client.publish('user:login', { name: 'loki' }); 27 | }, 500); -------------------------------------------------------------------------------- /examples/late-connect.js: -------------------------------------------------------------------------------- 1 | 2 | var multipub = require('..'); 3 | 4 | // launch: 5 | // $ redis-server --port 4000 & 6 | // $ redis-server --port 4001 & 7 | // $ redis-server --port 4002 & 8 | 9 | var client = multipub(); 10 | 11 | client.subscribe('user:login'); 12 | 13 | client.on('message', function(_, msg){ 14 | console.log(msg); 15 | }); 16 | 17 | client.on('error', function(err){ 18 | console.error('ERROR: %s', err.message); 19 | }); 20 | 21 | setInterval(function(){ 22 | client.publish('user:login', { name: 'tobi' }); 23 | }, 200); 24 | 25 | setTimeout(function(){ 26 | client.connect('localhost:4000'); 27 | }, 1000); 28 | 29 | setTimeout(function(){ 30 | client.connect('localhost:4001'); 31 | }, 3000); 32 | 33 | setTimeout(function(){ 34 | client.connect('localhost:4002'); 35 | }, 6000); -------------------------------------------------------------------------------- /examples/patterns.js: -------------------------------------------------------------------------------- 1 | 2 | var multipub = require('..'); 3 | 4 | // launch: 5 | // $ redis-server --port 4000 & 6 | // $ redis-server --port 4001 & 7 | // $ redis-server --port 4002 & 8 | 9 | var client = multipub(); 10 | 11 | client.connect('localhost:4000'); 12 | client.connect('localhost:4001'); 13 | client.connect('localhost:4002'); 14 | 15 | client.subscribe('user:*'); 16 | 17 | client.on('message', function(channel, msg){ 18 | console.log('%s - %s', channel, msg); 19 | }); 20 | 21 | setInterval(function(){ 22 | client.publish('user:luna', 'logged in'); 23 | }, 50); 24 | 25 | setInterval(function(){ 26 | client.publish('user:tobi', 'logged out'); 27 | }, 500); -------------------------------------------------------------------------------- /examples/simple.js: -------------------------------------------------------------------------------- 1 | 2 | var multipub = require('..'); 3 | 4 | // launch: 5 | // $ redis-server --port 4000 & 6 | // $ redis-server --port 4001 & 7 | // $ redis-server --port 4002 & 8 | 9 | var client = multipub(); 10 | 11 | client.on('error', function(){}); 12 | 13 | client.connect('localhost:4000'); 14 | client.connect('localhost:4001'); 15 | client.connect('localhost:4002'); 16 | 17 | client.subscribe('user:tobi'); 18 | client.subscribe('user:luna'); 19 | 20 | client.on('message', function(channel, msg){ 21 | console.log('%s - %s', channel, msg); 22 | }); 23 | 24 | setInterval(function(){ 25 | client.publish('user:luna', 'logged in'); 26 | }, 50); 27 | 28 | setInterval(function(){ 29 | client.publish('user:tobi', 'logged out'); 30 | }, 500); -------------------------------------------------------------------------------- /examples/unsubscribe-patterns.js: -------------------------------------------------------------------------------- 1 | 2 | var multipub = require('..'); 3 | 4 | // launch: 5 | // $ redis-server --port 4000 & 6 | // $ redis-server --port 4001 & 7 | // $ redis-server --port 4002 & 8 | 9 | var client = multipub(); 10 | 11 | client.on('error', function(){}); 12 | 13 | client.connect('localhost:4000'); 14 | client.connect('localhost:4001'); 15 | client.connect('localhost:4002'); 16 | 17 | client.subscribe('user:*'); 18 | 19 | client.on('message', function(channel, msg){ 20 | console.log('%s - %s', channel, msg); 21 | }); 22 | 23 | setInterval(function(){ 24 | client.publish('user:login', 'manny'); 25 | }, 500); 26 | 27 | setInterval(function(){ 28 | client.publish('user:logout', 'tobi'); 29 | }, 250); 30 | 31 | setTimeout(function(){ 32 | client.unsubscribe('user:*'); 33 | }, 2000); 34 | -------------------------------------------------------------------------------- /examples/unsubscribe.js: -------------------------------------------------------------------------------- 1 | 2 | var multipub = require('..'); 3 | 4 | // launch: 5 | // $ redis-server --port 4000 & 6 | // $ redis-server --port 4001 & 7 | // $ redis-server --port 4002 & 8 | 9 | var client = multipub(); 10 | 11 | client.on('error', function(){}); 12 | 13 | client.connect('localhost:4000'); 14 | client.connect('localhost:4001'); 15 | client.connect('localhost:4002'); 16 | 17 | client.subscribe('user:login'); 18 | client.subscribe('user:logout'); 19 | 20 | client.on('message', function(channel, msg){ 21 | console.log('%s - %s', channel, msg); 22 | }); 23 | 24 | setInterval(function(){ 25 | client.publish('user:login', 'manny'); 26 | }, 500); 27 | 28 | setInterval(function(){ 29 | client.publish('user:logout', 'tobi'); 30 | }, 250); 31 | 32 | setTimeout(function(){ 33 | client.unsubscribe('user:logout'); 34 | }, 2000); 35 | 36 | setTimeout(function(){ 37 | client.unsubscribe('user:login'); 38 | }, 4000); -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var Client = require('./lib/client'); 7 | 8 | /** 9 | * Return a Client. 10 | * 11 | * @param {Object} [opts] 12 | * @return {Client} 13 | * @api public 14 | */ 15 | 16 | module.exports = function(opts){ 17 | return new Client(opts); 18 | }; -------------------------------------------------------------------------------- /lib/client.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var Emitter = require('events').EventEmitter; 7 | var debug = require('debug')('multipub:client'); 8 | var fwd = require('forward-events'); 9 | var assert = require('assert'); 10 | var Node = require('./node'); 11 | 12 | /** 13 | * Expose `Client`. 14 | */ 15 | 16 | module.exports = Client; 17 | 18 | /** 19 | * Initialize a Client with the given `opts`. 20 | * 21 | * A muxpub Client consists of zero or more redis Nodes, 22 | * each Node may have a number of connections to that node. 23 | * 24 | * A noop "error" handler is attached so that reconnection 25 | * can be performed on errors without forcing the user to 26 | * add an "error" handler of their own, however one should 27 | * be used for logging. 28 | * 29 | * Options are passed to the redis.createClient() call. 30 | * 31 | * @param {Object} opts 32 | * @api public 33 | */ 34 | 35 | function Client(opts) { 36 | this.opts = opts || {}; 37 | this.subscriptions = {}; 38 | this.nodes = []; 39 | this.n = 0; 40 | } 41 | 42 | /** 43 | * Inherit from `Emitter.prototype`. 44 | */ 45 | 46 | Client.prototype.__proto__ = Emitter.prototype; 47 | 48 | /** 49 | * Add connection to `addr`. 50 | * 51 | * @param {String} addr 52 | * @api public 53 | */ 54 | 55 | Client.prototype.connect = function(addr){ 56 | debug('connect %s', addr); 57 | assert('string' == typeof addr, 'address string required'); 58 | var node = new Node(addr, this.opts); 59 | fwd(node, this); 60 | this.watchState(node); 61 | }; 62 | 63 | /** 64 | * Watch the state of `node` and remove it 65 | * from the .nodes set when it is not writable. 66 | * 67 | * @param {Node} node 68 | * @api private 69 | */ 70 | 71 | Client.prototype.watchState = function(node){ 72 | var self = this; 73 | 74 | node.pub.on('ready', function(){ 75 | debug('add %s', node.addr); 76 | self.nodes.push(node); 77 | self.ensureSubscriptions(); 78 | }); 79 | 80 | node.pub.on('end', function(){ 81 | debug('remove %s', node.addr); 82 | var i = self.nodes.indexOf(node); 83 | self.nodes.splice(i, 1); 84 | }); 85 | }; 86 | 87 | /** 88 | * Subscribe to `pattern`. 89 | * 90 | * @param {String} pattern 91 | * @api public 92 | */ 93 | 94 | Client.prototype.subscribe = function(pattern){ 95 | debug('subscribe %j', pattern) 96 | this.subscriptions[pattern] = true; 97 | this.ensureSubscriptions(); 98 | }; 99 | 100 | 101 | /** 102 | * Usubscribe from `pattern`. 103 | * 104 | * @param {String} pattern 105 | * @api public 106 | */ 107 | 108 | Client.prototype.unsubscribe = function(pattern){ 109 | debug('unsubscribe %j', pattern); 110 | delete this.subscriptions[pattern]; 111 | this.nodes.forEach(function(node){ 112 | node.unsubscribe(pattern); 113 | }); 114 | }; 115 | 116 | /** 117 | * Ensure all nodes are subscribed to all 118 | * patterns that have been specified. This 119 | * is idempotent. 120 | * 121 | * @api private 122 | */ 123 | 124 | Client.prototype.ensureSubscriptions = function(){ 125 | var patterns = Object.keys(this.subscriptions); 126 | this.nodes.forEach(function(node){ 127 | patterns.forEach(function(pattern){ 128 | node.subscribe(pattern); 129 | }); 130 | }); 131 | }; 132 | 133 | /** 134 | * Publish `msg` on `channel` round-robin. 135 | * 136 | * @param {String} channel 137 | * @param {String|Object} msg 138 | * @api public 139 | */ 140 | 141 | Client.prototype.publish = function(channel, msg){ 142 | if (msg && 'object' == typeof msg) { 143 | msg = JSON.stringify(msg); 144 | } 145 | 146 | var len = this.nodes.length; 147 | if (!len) return debug('no connections - message dropped'); 148 | var i = this.n++ % len; 149 | var node = this.nodes[i]; 150 | node.publish(channel, msg); 151 | }; 152 | -------------------------------------------------------------------------------- /lib/node.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var Emitter = require('events').EventEmitter; 7 | var debug = require('debug')('multipub:node'); 8 | var fwd = require('forward-events'); 9 | var parse = require('url').parse; 10 | var redis = require('redis'); 11 | 12 | /** 13 | * Expose `Node`. 14 | */ 15 | 16 | module.exports = Node; 17 | 18 | /** 19 | * Initialize a redis node with the given `opts`. 20 | * 21 | * A muxpub Node may have zero or more connections to 22 | * the redis instance. Options passed here are passed to 23 | * redis.createClient(). 24 | * 25 | * @param {String} addr 26 | * @param {Object} [opts] 27 | * @api public 28 | */ 29 | 30 | function Node(addr, opts) { 31 | this.url = parse('tcp://' + addr); 32 | this.addr = addr; 33 | this.opts = opts || {}; 34 | this.subscriptions = {}; 35 | this.pub = this.client(); 36 | this.sub = this.client(); 37 | } 38 | 39 | /** 40 | * Inherit from `Emitter.prototype`. 41 | */ 42 | 43 | Node.prototype.__proto__ = Emitter.prototype; 44 | 45 | /** 46 | * Return a connection to the redis node. 47 | * 48 | * @return {RedisClient} 49 | * @api private 50 | */ 51 | 52 | Node.prototype.connection = function(){ 53 | var host = this.url.hostname; 54 | var port = this.url.port; 55 | var opts = this.opts; 56 | var auth = opts.auth; 57 | var client = redis.createClient(port, host, opts); 58 | if (auth) client.auth(auth); 59 | return client; 60 | }; 61 | 62 | /** 63 | * Return a connection and handle: 64 | * 65 | * - message forwarding 66 | * - pmessage -> message normalization 67 | * 68 | * @return {RedisClient} 69 | * @api private 70 | */ 71 | 72 | Node.prototype.client = function(){ 73 | var conn = this.connection(); 74 | var self = this; 75 | 76 | fwd(conn, this); 77 | 78 | conn.on('pmessage', function(pattern, channel, msg){ 79 | self.emit('message', channel, msg, pattern); 80 | }); 81 | 82 | return conn; 83 | }; 84 | 85 | /** 86 | * Publish `msg` to `channel`. 87 | * 88 | * @param {String} channel 89 | * @param {String} msg 90 | * @api private 91 | */ 92 | 93 | Node.prototype.publish = function(channel, msg){ 94 | debug('%s - publish %s %j', this.addr, channel, msg); 95 | this.pub.publish(channel, msg); 96 | }; 97 | 98 | /** 99 | * Subscribe to `pattern` unless already subscribed. 100 | * 101 | * @param {String} pattern 102 | * @api private 103 | */ 104 | 105 | Node.prototype.subscribe = function(pattern){ 106 | if (this.subscriptions[pattern]) return; 107 | this.subscriptions[pattern] = true; 108 | debug('%s - subscribe %j', this.addr, pattern); 109 | 110 | if (~pattern.indexOf('*')) { 111 | this.sub.psubscribe(pattern); 112 | } else { 113 | this.sub.subscribe(pattern); 114 | } 115 | }; 116 | 117 | /** 118 | * Unsubscribe from `pattern`. 119 | * 120 | * @param {String} pattern 121 | * @api private 122 | */ 123 | 124 | Node.prototype.unsubscribe = function(pattern){ 125 | debug('%s - unsubscribe %j', this.addr, pattern); 126 | if (~pattern.indexOf('*')) { 127 | this.sub.punsubscribe(pattern); 128 | } else { 129 | this.sub.unsubscribe(pattern); 130 | } 131 | }; 132 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "multipub", 3 | "version": "1.0.0", 4 | "repository": "segmentio/multipub", 5 | "description": "multi-redis pub/sub", 6 | "keywords": [ 7 | "redis", 8 | "pub", 9 | "sub", 10 | "publish", 11 | "subscribe" 12 | ], 13 | "dependencies": { 14 | "redis": "~0.10.1", 15 | "debug": "~0.7.4", 16 | "forward-events": "0.0.1" 17 | }, 18 | "devDependencies": { 19 | "mocha": "*", 20 | "should": "*" 21 | }, 22 | "license": "MIT" 23 | } 24 | --------------------------------------------------------------------------------