├── .eslintrc.js ├── .github └── workflows │ └── test.yml ├── .gitignore ├── .mocharc.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── index.js ├── package.json └── test └── test.js /.eslintrc.js: -------------------------------------------------------------------------------- 1 | // The ESLint ecmaVersion argument is inconsistently used. Some rules will ignore it entirely, so if the rule has 2 | // been set, it will still error even if it's not applicable to that version number. Since Google sets these 3 | // rules, we have to turn them off ourselves. 4 | var DISABLED_ES6_OPTIONS = { 5 | 'no-var': 'off', 6 | 'prefer-rest-params': 'off' 7 | }; 8 | 9 | var SHAREDB_RULES = { 10 | // Comma dangle is not supported in ES3 11 | 'comma-dangle': ['error', 'never'], 12 | // We control our own objects and prototypes, so no need for this check 13 | 'guard-for-in': 'off', 14 | // Google prescribes different indents for different cases. Let's just use 2 spaces everywhere. Note that we have 15 | // to override ESLint's default of 0 indents for this. 16 | indent: ['error', 2, { 17 | SwitchCase: 1 18 | }], 19 | // Less aggressive line length than Google, which is especially useful when we have a lot of callbacks in our code 20 | 'max-len': ['error', 21 | { 22 | code: 120, 23 | tabWidth: 2, 24 | ignoreUrls: true 25 | } 26 | ], 27 | // Google overrides the default ESLint behaviour here, which is slightly better for catching erroneously unused 28 | // variables 29 | 'no-unused-vars': ['error', {vars: 'all', args: 'after-used'}], 30 | // It's more readable to ensure we only have one statement per line 31 | 'max-statements-per-line': ['error', {max: 1}], 32 | // ES3 doesn't support spread 33 | 'prefer-spread': 'off', 34 | // as-needed quote props are easier to write 35 | 'quote-props': ['error', 'as-needed'], 36 | 'require-jsdoc': 'off', 37 | 'valid-jsdoc': 'off' 38 | }; 39 | 40 | module.exports = { 41 | extends: 'google', 42 | parserOptions: { 43 | ecmaVersion: 3 44 | }, 45 | rules: Object.assign( 46 | {}, 47 | DISABLED_ES6_OPTIONS, 48 | SHAREDB_RULES 49 | ) 50 | }; 51 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | test: 13 | name: Node.js ${{ matrix.node }} - Redis ${{ matrix.redis }} 14 | runs-on: ubuntu-latest 15 | strategy: 16 | matrix: 17 | node: 18 | - 16 19 | - 18 20 | - 20 21 | redis: 22 | - 4 23 | - 5 24 | services: 25 | redis: 26 | image: redis 27 | ports: 28 | - 6379:6379 29 | timeout-minutes: 10 30 | steps: 31 | - uses: actions/checkout@v4 32 | - uses: actions/setup-node@v4 33 | with: 34 | node-version: ${{ matrix.node }} 35 | - name: Install 36 | run: npm install 37 | - name: Install correct redis version 38 | run: npm install redis@${{ matrix.redis }} 39 | - name: Lint 40 | run: npm run lint 41 | - name: Test 42 | run: npm run test-cover 43 | - name: Coveralls 44 | uses: coverallsapp/github-action@master 45 | with: 46 | github-token: ${{ secrets.GITHUB_TOKEN }} 47 | flag-name: node-${{ matrix.node }} 48 | parallel: true 49 | 50 | finish: 51 | needs: test 52 | runs-on: ubuntu-latest 53 | steps: 54 | - name: Submit coverage 55 | uses: coverallsapp/github-action@master 56 | with: 57 | github-token: ${{ secrets.GITHUB_TOKEN }} 58 | parallel-finished: true 59 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | *.DS_Store 3 | node_modules 4 | coverage 5 | package-lock.json 6 | -------------------------------------------------------------------------------- /.mocharc.yml: -------------------------------------------------------------------------------- 1 | reporter: spec 2 | check-leaks: true 3 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## v1.0-beta 2 | 3 | Beginning of changelog. 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Licensed under the standard MIT license: 2 | 3 | Copyright 2015 Nate Smith 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # sharedb-redis-pubsub 2 | 3 | [![NPM Version](https://img.shields.io/npm/v/sharedb-redis-pubsub.svg)](https://npmjs.org/package/sharedb-redis-pubsub) 4 | [![Build Status](https://travis-ci.org/share/sharedb-redis-pubsub.svg?branch=master)](https://travis-ci.org/share/sharedb-redis-pubsub) 5 | [![Coverage Status](https://coveralls.io/repos/github/share/sharedb-redis-pubsub/badge.svg?branch=master)](https://coveralls.io/github/share/sharedb-redis-pubsub?branch=master) 6 | 7 | Redis pub/sub adapter adapter for ShareDB. 8 | 9 | This ShareDB add-on gives you horizontal scalability; the ability to have a cluster of multiple server nodes rather than just a single server. Using this adapter, clients can connect to any machine in your cluster, and the ops they submit will be forwarded clients connected through other nodes, and there will be no race conditions with regard to persistence. 10 | 11 | ## Usage 12 | 13 | This snippet shows how to load this library and pass it into a new ShareDB instance. 14 | 15 | ```js 16 | // Redis client is an existing redis client connection 17 | var redisPubsub = require('sharedb-redis-pubsub')({client: redisClient}); 18 | 19 | var backend = new ShareDB({ 20 | db: db, // db would be your mongo db or other storage location 21 | pubsub: redisPubsub 22 | }); 23 | ``` 24 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var redis = require('redis'); 2 | var PubSub = require('sharedb').PubSub; 3 | 4 | // Redis pubsub driver for ShareDB. 5 | // 6 | // The redis driver requires two redis clients (a single redis client can't do 7 | // both pubsub and normal messaging). These clients will be created 8 | // automatically if you don't provide them. 9 | function RedisPubSub(options) { 10 | if (!(this instanceof RedisPubSub)) return new RedisPubSub(options); 11 | PubSub.call(this, options); 12 | options || (options = {}); 13 | 14 | this.client = options.client || redis.createClient(options); 15 | this._clientConnection = null; 16 | 17 | // Redis doesn't allow the same connection to both listen to channels and do 18 | // operations. Make an extra redis connection for subscribing with the same 19 | // options if not provided 20 | this.observer = options.observer || redis.createClient(this.client.options); 21 | this._observerConnection = null; 22 | 23 | this._connect(); 24 | } 25 | module.exports = RedisPubSub; 26 | 27 | RedisPubSub.prototype = Object.create(PubSub.prototype); 28 | 29 | RedisPubSub.prototype.close = function(callback) { 30 | if (!callback) { 31 | callback = function(err) { 32 | if (err) throw err; 33 | }; 34 | } 35 | var pubsub = this; 36 | PubSub.prototype.close.call(this, function(err) { 37 | if (err) return callback(err); 38 | pubsub._close().then(function() { 39 | callback(); 40 | }, callback); 41 | }); 42 | }; 43 | 44 | RedisPubSub.prototype._close = function() { 45 | var pubsub = this; 46 | 47 | if (!this._closing) { 48 | this._closing = this._connect() 49 | .then(function() { 50 | return Promise.all([ 51 | close(pubsub.client), 52 | close(pubsub.observer) 53 | ]); 54 | }); 55 | } 56 | 57 | return this._closing; 58 | }; 59 | 60 | RedisPubSub.prototype._subscribe = function(channel, callback) { 61 | var pubsub = this; 62 | pubsub.observer 63 | .subscribe(channel, function(message) { 64 | var data = JSON.parse(message); 65 | pubsub._emit(channel, data); 66 | }) 67 | .then(function() { 68 | callback(); 69 | }, callback); 70 | }; 71 | 72 | RedisPubSub.prototype._unsubscribe = function(channel, callback) { 73 | this.observer.unsubscribe(channel) 74 | .then(function() { 75 | callback(); 76 | }, callback); 77 | }; 78 | 79 | RedisPubSub.prototype._publish = function(channels, data, callback) { 80 | var message = JSON.stringify(data); 81 | var args = [message].concat(channels); 82 | this.client.eval(PUBLISH_SCRIPT, {arguments: args}).then(function() { 83 | callback(); 84 | }, callback); 85 | }; 86 | 87 | RedisPubSub.prototype._connect = function() { 88 | this._clientConnection = this._clientConnection || connect(this.client); 89 | this._observerConnection = this._observerConnection || connect(this.observer); 90 | return Promise.all([ 91 | this._clientConnection, 92 | this._observerConnection 93 | ]); 94 | }; 95 | 96 | function connect(client) { 97 | return client.isOpen ? Promise.resolve() : client.connect(); 98 | } 99 | 100 | var PUBLISH_SCRIPT = 101 | 'for i = 2, #ARGV do ' + 102 | 'redis.call("publish", ARGV[i], ARGV[1]) ' + 103 | 'end'; 104 | 105 | function close(client) { 106 | if (client.close) { 107 | return client.close(); 108 | } 109 | 110 | // The quit is deprecated for node redis >= 5.0.0 111 | // This call should be removed after we stop supporting redis < 5.0.0 112 | return client.quit(); 113 | } 114 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sharedb-redis-pubsub", 3 | "version": "5.1.0", 4 | "description": "Redis pub/sub adapter adapter for ShareDB", 5 | "main": "index.js", 6 | "dependencies": { 7 | "redis": "^4.0.0 || ^5.0.0", 8 | "sharedb": "^1.0.0 || ^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0" 9 | }, 10 | "devDependencies": { 11 | "chai": "^4.2.0", 12 | "coveralls": "^3.0.7", 13 | "eslint": "^7.23.0", 14 | "eslint-config-google": "^0.14.0", 15 | "mocha": "^9.1.1", 16 | "nyc": "^15.1.0" 17 | }, 18 | "scripts": { 19 | "lint": "./node_modules/.bin/eslint --ignore-path .gitignore '**/*.js'", 20 | "test": "mocha", 21 | "test-cover": "nyc --temp-dir=coverage -r text -r lcov npm test" 22 | }, 23 | "repository": "git://github.com/share/sharedb-redis-pubsub.git", 24 | "author": "Nate Smith", 25 | "license": "MIT" 26 | } 27 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | var redisPubSub = require('../index'); 2 | var redis = require('redis'); 3 | var runTestSuite = require('sharedb/test/pubsub'); 4 | 5 | describe('default options', function() { 6 | runTestSuite(function(callback) { 7 | callback(null, redisPubSub()); 8 | }); 9 | }); 10 | 11 | describe('unconnected client', function() { 12 | runTestSuite(function(callback) { 13 | callback(null, redisPubSub({ 14 | client: redis.createClient() 15 | })); 16 | }); 17 | }); 18 | 19 | describe('connected client', function() { 20 | var client; 21 | 22 | beforeEach(function(done) { 23 | client = redis.createClient(); 24 | client.connect().then(function() { 25 | done(); 26 | }, done); 27 | }); 28 | 29 | runTestSuite(function(callback) { 30 | callback(null, redisPubSub({ 31 | client: client 32 | })); 33 | }); 34 | }); 35 | 36 | describe('connecting client', function() { 37 | runTestSuite(function(callback) { 38 | var client = redis.createClient(); 39 | client.connect(); 40 | callback(null, redisPubSub({ 41 | client: client 42 | })); 43 | }); 44 | }); 45 | --------------------------------------------------------------------------------