├── .eslintignore ├── .eslintrc ├── .gitignore ├── .istanbul.yml ├── .travis.yml ├── README.md ├── browser.js ├── client.js ├── index.js ├── package.json ├── server.js └── test ├── server.js └── tandem.js /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | test -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { // http://eslint.org/docs/user-guide/configuring.html#specifying-environments 3 | "browser": true, // browser global variables 4 | "node": true // Node.js global variables and Node.js-specific rules 5 | }, 6 | "ecmaFeatures": { 7 | "arrowFunctions": true, 8 | "blockBindings": true, 9 | "classes": true, 10 | "defaultParams": true, 11 | "destructuring": true, 12 | "forOf": true, 13 | "generators": false, 14 | "modules": true, 15 | "objectLiteralComputedProperties": true, 16 | "objectLiteralDuplicateProperties": false, 17 | "objectLiteralShorthandMethods": true, 18 | "objectLiteralShorthandProperties": true, 19 | "spread": true, 20 | "superInFunctions": true, 21 | "templateStrings": true, 22 | "jsx": true 23 | }, 24 | "rules": { 25 | 26 | /** 27 | * Variables 28 | */ 29 | "no-shadow": 2, // http://eslint.org/docs/rules/no-shadow 30 | "no-shadow-restricted-names": 2, // http://eslint.org/docs/rules/no-shadow-restricted-names 31 | "no-unused-vars": [2, { // http://eslint.org/docs/rules/no-unused-vars 32 | "vars": "local", 33 | "args": "after-used" 34 | }], 35 | "no-use-before-define": 0, // http://eslint.org/docs/rules/no-use-before-define 36 | 37 | /** 38 | * Possible errors 39 | */ 40 | "comma-dangle": [2, "always-multiline"], // http://eslint.org/docs/rules/comma-dangle 41 | "no-cond-assign": [2, "always"], // http://eslint.org/docs/rules/no-cond-assign 42 | "no-debugger": 1, // http://eslint.org/docs/rules/no-debugger 43 | "no-alert": 1, // http://eslint.org/docs/rules/no-alert 44 | "no-constant-condition": 1, // http://eslint.org/docs/rules/no-constant-condition 45 | "no-dupe-keys": 2, // http://eslint.org/docs/rules/no-dupe-keys 46 | "no-duplicate-case": 2, // http://eslint.org/docs/rules/no-duplicate-case 47 | "no-empty": 2, // http://eslint.org/docs/rules/no-empty 48 | "no-ex-assign": 2, // http://eslint.org/docs/rules/no-ex-assign 49 | "no-extra-boolean-cast": 0, // http://eslint.org/docs/rules/no-extra-boolean-cast 50 | "no-extra-semi": 2, // http://eslint.org/docs/rules/no-extra-semi 51 | "no-func-assign": 2, // http://eslint.org/docs/rules/no-func-assign 52 | "no-inner-declarations": 2, // http://eslint.org/docs/rules/no-inner-declarations 53 | "no-invalid-regexp": 2, // http://eslint.org/docs/rules/no-invalid-regexp 54 | "no-irregular-whitespace": 2, // http://eslint.org/docs/rules/no-irregular-whitespace 55 | "no-obj-calls": 2, // http://eslint.org/docs/rules/no-obj-calls 56 | "no-sparse-arrays": 2, // http://eslint.org/docs/rules/no-sparse-arrays 57 | "no-unreachable": 2, // http://eslint.org/docs/rules/no-unreachable 58 | "use-isnan": 2, // http://eslint.org/docs/rules/use-isnan 59 | "block-scoped-var": 2, // http://eslint.org/docs/rules/block-scoped-var 60 | 61 | /** 62 | * Best practices 63 | */ 64 | "consistent-return": 2, // http://eslint.org/docs/rules/consistent-return 65 | "curly": [2, "multi-line"], // http://eslint.org/docs/rules/curly 66 | "default-case": 2, // http://eslint.org/docs/rules/default-case 67 | "dot-notation": [2, { // http://eslint.org/docs/rules/dot-notation 68 | "allowKeywords": true 69 | }], 70 | "eqeqeq": 2, // http://eslint.org/docs/rules/eqeqeq 71 | "guard-for-in": 2, // http://eslint.org/docs/rules/guard-for-in 72 | "no-caller": 2, // http://eslint.org/docs/rules/no-caller 73 | "no-else-return": 2, // http://eslint.org/docs/rules/no-else-return 74 | "no-eq-null": 2, // http://eslint.org/docs/rules/no-eq-null 75 | "no-eval": 2, // http://eslint.org/docs/rules/no-eval 76 | "no-extend-native": 2, // http://eslint.org/docs/rules/no-extend-native 77 | "no-extra-bind": 2, // http://eslint.org/docs/rules/no-extra-bind 78 | "no-fallthrough": 2, // http://eslint.org/docs/rules/no-fallthrough 79 | "no-floating-decimal": 2, // http://eslint.org/docs/rules/no-floating-decimal 80 | "no-implied-eval": 2, // http://eslint.org/docs/rules/no-implied-eval 81 | "no-lone-blocks": 2, // http://eslint.org/docs/rules/no-lone-blocks 82 | "no-loop-func": 2, // http://eslint.org/docs/rules/no-loop-func 83 | "no-multi-str": 2, // http://eslint.org/docs/rules/no-multi-str 84 | "no-native-reassign": 2, // http://eslint.org/docs/rules/no-native-reassign 85 | "no-new": 2, // http://eslint.org/docs/rules/no-new 86 | "no-new-func": 2, // http://eslint.org/docs/rules/no-new-func 87 | "no-new-wrappers": 2, // http://eslint.org/docs/rules/no-new-wrappers 88 | "no-octal": 2, // http://eslint.org/docs/rules/no-octal 89 | "no-octal-escape": 2, // http://eslint.org/docs/rules/no-octal-escape 90 | "no-param-reassign": 2, // http://eslint.org/docs/rules/no-param-reassign 91 | "no-proto": 2, // http://eslint.org/docs/rules/no-proto 92 | "no-redeclare": 2, // http://eslint.org/docs/rules/no-redeclare 93 | "no-return-assign": 2, // http://eslint.org/docs/rules/no-return-assign 94 | "no-script-url": 2, // http://eslint.org/docs/rules/no-script-url 95 | "no-self-compare": 2, // http://eslint.org/docs/rules/no-self-compare 96 | "no-sequences": 2, // http://eslint.org/docs/rules/no-sequences 97 | "no-throw-literal": 2, // http://eslint.org/docs/rules/no-throw-literal 98 | "no-with": 2, // http://eslint.org/docs/rules/no-with 99 | "radix": 2, // http://eslint.org/docs/rules/radix 100 | "wrap-iife": [2, "any"], // http://eslint.org/docs/rules/wrap-iife 101 | "yoda": 2, // http://eslint.org/docs/rules/yoda 102 | 103 | /** 104 | * Style 105 | */ 106 | "indent": [2, 2], // http://eslint.org/docs/rules/indent 107 | "brace-style": [2, // http://eslint.org/docs/rules/brace-style 108 | "1tbs", { 109 | "allowSingleLine": true 110 | }], 111 | "quotes": [ 112 | 2, "single", "avoid-escape" // http://eslint.org/docs/rules/quotes 113 | ], 114 | "camelcase": [2, { // http://eslint.org/docs/rules/camelcase 115 | "properties": "never" 116 | }], 117 | "comma-spacing": [2, { // http://eslint.org/docs/rules/comma-spacing 118 | "before": false, 119 | "after": true 120 | }], 121 | "comma-style": [2, "last"], // http://eslint.org/docs/rules/comma-style 122 | "eol-last": 2, // http://eslint.org/docs/rules/eol-last 123 | "func-names": 1, // http://eslint.org/docs/rules/func-names 124 | "key-spacing": [2, { // http://eslint.org/docs/rules/key-spacing 125 | "beforeColon": false, 126 | "afterColon": true 127 | }], 128 | "new-cap": 0, // http://eslint.org/docs/rules/new-cap 129 | "no-multiple-empty-lines": [2, { // http://eslint.org/docs/rules/no-multiple-empty-lines 130 | "max": 2 131 | }], 132 | "no-nested-ternary": 2, // http://eslint.org/docs/rules/no-nested-ternary 133 | "no-new-object": 2, // http://eslint.org/docs/rules/no-new-object 134 | "no-spaced-func": 2, // http://eslint.org/docs/rules/no-spaced-func 135 | "no-trailing-spaces": 2, // http://eslint.org/docs/rules/no-trailing-spaces 136 | "no-extra-parens": [2, "functions"], // http://eslint.org/docs/rules/no-extra-parens 137 | "no-underscore-dangle": 0, // http://eslint.org/docs/rules/no-underscore-dangle 138 | "one-var": [2, "never"], // http://eslint.org/docs/rules/one-var 139 | "padded-blocks": [2, "never"], // http://eslint.org/docs/rules/padded-blocks 140 | "semi": [2, "always"], // http://eslint.org/docs/rules/semi 141 | "semi-spacing": [2, { // http://eslint.org/docs/rules/semi-spacing 142 | "before": false, 143 | "after": true 144 | }], 145 | "space-after-keywords": 2, // http://eslint.org/docs/rules/space-after-keywords 146 | "space-before-blocks": 2, // http://eslint.org/docs/rules/space-before-blocks 147 | "space-before-function-paren": [2, "never"], // http://eslint.org/docs/rules/space-before-function-paren 148 | "space-infix-ops": 2, // http://eslint.org/docs/rules/space-infix-ops 149 | "space-return-throw-case": 2, // http://eslint.org/docs/rules/space-return-throw-case 150 | "spaced-comment": [2, "always", {// http://eslint.org/docs/rules/spaced-comment 151 | "exceptions": ["-", "+"], 152 | "markers": ["=", "!"] // space here to support sprockets directives 153 | }] 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | -------------------------------------------------------------------------------- /.istanbul.yml: -------------------------------------------------------------------------------- 1 | instrumentation: 2 | excludes: ['test', 'node_modules'] 3 | check: 4 | global: 5 | lines: 100 6 | branches: 100 7 | statements: 100 8 | functions: 100 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 4 4 | - 5 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pouch-websocket-sync 2 | 3 | [![By](https://img.shields.io/badge/made%20by-yld!-32bbee.svg?style=flat)](http://yld.io/contact?source=github-pouch-websocket-sync) 4 | [![Build Status](https://secure.travis-ci.org/pgte/pouch-websocket-sync.svg?branch=master)](http://travis-ci.org/pgte/pouch-websocket-sync?branch=master) 5 | 6 | 7 | Sync several PouchDBs through websockets. 8 | 9 | Supports reconnection, negotiation and authentication. 10 | 11 | ## Demo 12 | 13 | See [here an example todo application](https://github.com/pgte/pouch-websocket-sync-example#readme) using React and Redux. 14 | 15 | [Demo video](http://www.youtube.com/watch?v=8jOF23dfvl4) 16 | 17 | ## Install 18 | 19 | ``` 20 | $ npm install pouch-websocket-sync --save 21 | ``` 22 | 23 | ## Server 24 | 25 | ```js 26 | var PouchSync = require('pouch-websocket-sync'); 27 | var http = require('http'); 28 | var httpServer = http.createServer(); 29 | var server = PouchSync.createServer(httpServer, onRequest); 30 | httpServer.listen(3000); 31 | 32 | function onRequest(credentials, dbName, cb) { 33 | if (credentials.token == 'some token') { 34 | cb(null, new PouchDB(dbName)); 35 | } else { 36 | cb(new Error('not allowed')); 37 | } 38 | }; 39 | ``` 40 | 41 | ## Client 42 | 43 | ```js 44 | var websocket = require('websocket-stream'); 45 | var PouchSync = require('pouch-websocket-sync'); 46 | 47 | var db = new PouchDB('todos'); 48 | var client = PouchSync.createClient(); 49 | var sync = client.sync(db, { 50 | remoteName: 'todos-server', // name remote db is known for 51 | credentials: { token: 'some token'} // arbitrary 52 | }); 53 | 54 | client.connect('ws://somehost:someport'); 55 | ``` 56 | 57 | ## API 58 | 59 | ### PouchWebsocketSync.createServer(httpServer, onRequest) 60 | 61 | Creates and returns a websocket server. 62 | 63 | Arguments: 64 | * `httpServer`: an HTTP server to bind to 65 | * `onRequest`: a function to be called when a client requests access to a database. This function must have the following signature: 66 | 67 | ```js 68 | function onRequest(credentials, dbName, callback) 69 | ``` 70 | 71 | The arguments to expect on this function are: 72 | * `credentials`: arbitrary, whatever the client sends as credentials. Defaults to `undefined`. 73 | * `dbName`: the name of the database to sync into as being requested by the client. 74 | * `callback`: the callback to call once the request is to be granted or denied. If there is a problem with the requests (invalid credentials or other error), you should pass an error as first arguments. If, otherwise, the request for a database is to proceed, you should pass `null` or `undefined` as the first argument and a PouchDB database as the second. 75 | 76 | Example of an `onRequest` function: 77 | 78 | ```js 79 | function onRequest(credentials, dbName, cb) { 80 | if (credentials.token == 'some token') { 81 | cb(null, new PouchDB(dbName)); 82 | } else { 83 | cb(new Error('not allowed')); 84 | } 85 | }; 86 | ``` 87 | 88 | ## PouchWebsocketSync.createClient() 89 | 90 | Creates and returns a webocket sync client. 91 | 92 | ### client.connect(address) 93 | 94 | Connect to a given websocket address. 95 | 96 | * `address` a websocket address, like `wss://somehost:3000` 97 | 98 | ### client.sync(database, options) 99 | 100 | Start syncing the given database. Arguments: 101 | 102 | * `database`: an instance of a PouchDB database 103 | * `options`: an object containing the follow keys and values: 104 | * remoteName: remote database name. Defaults to the `database` name. 105 | * PouchDB: PouchDB constructor. Defaults to `database.constructor`. 106 | 107 | Returns a sync object. 108 | 109 | ### client events 110 | 111 | ```js 112 | client.emit('connect') // when connects 113 | client.emit('disconnect') // when gets disconnected 114 | client.emit('reconnect') // when starts attempting to reconnect 115 | ``` 116 | 117 | ## Sync 118 | 119 | ### sync.cancel() 120 | 121 | Cancel this sync. 122 | 123 | ## client.end() or client.destroy() 124 | 125 | ## sync events 126 | 127 | ```js 128 | sync.emit('change', change) 129 | sync.emit('paused') 130 | sync.emit('active') 131 | sync.emit('denied') 132 | sync.emit('complete') 133 | sync.emit('error', err) 134 | ``` 135 | 136 | ## License 137 | 138 | ISC 139 | -------------------------------------------------------------------------------- /browser.js: -------------------------------------------------------------------------------- 1 | exports.createClient = require('./client'); 2 | -------------------------------------------------------------------------------- /client.js: -------------------------------------------------------------------------------- 1 | var Websocket = require('websocket-stream'); 2 | var PouchSync = require('pouch-stream-multi-sync'); 3 | 4 | module.exports = createClient; 5 | 6 | function createClient() { 7 | var client = PouchSync.createClient(function connect(address) { 8 | var ws = Websocket(address); 9 | ws.on('error', onError); 10 | return ws; 11 | }); 12 | return client; 13 | 14 | /* istanbul ignore next */ 15 | function onError(err) { 16 | if (err.message !== 'write after end') { 17 | client.emit('error', err); 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | exports.createServer = require('./server'); 2 | exports.createClient = require('./client'); 3 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pouch-websocket-sync", 3 | "version": "0.3.0", 4 | "description": "PouchDB live sync through websockets", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "node --harmony node_modules/istanbul/lib/cli.js cover -- lab -vl && istanbul check-coverage", 8 | "style": "eslint ." 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/pgte/pouch-websocket-sync.git" 13 | }, 14 | "author": "pgte", 15 | "license": "ISC", 16 | "bugs": { 17 | "url": "https://github.com/pgte/pouch-websocket-sync/issues" 18 | }, 19 | "homepage": "https://github.com/pgte/pouch-websocket-sync#readme", 20 | "tags": [ 21 | "pouchdb", 22 | "websocket", 23 | "stream" 24 | ], 25 | "dependencies": { 26 | "pouch-stream-multi-sync": "^0.3.0", 27 | "websocket-stream": "^2.1.0" 28 | }, 29 | "devDependencies": { 30 | "code": "^2.0.1", 31 | "eslint": "^1.10.1", 32 | "istanbul": "^0.4.0", 33 | "lab": "^7.3.0", 34 | "memdown": "^1.1.0", 35 | "pouchdb": "^5.1.0", 36 | "pre-commit": "^1.1.2" 37 | }, 38 | "tags": [ 39 | "pouchdb", 40 | "stream", 41 | "sync", 42 | "websockets" 43 | ], 44 | "browser": "browser.js", 45 | "pre-commit": [ 46 | "style", 47 | "test" 48 | ] 49 | } 50 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | var websocket = require('websocket-stream'); 2 | var PouchSync = require('pouch-stream-multi-sync'); 3 | 4 | var ignoreErrorMessages = [ 5 | 'write after end', 6 | 'not opened', 7 | ]; 8 | 9 | module.exports = createServer; 10 | 11 | function createServer(httpServer, onRequest) { 12 | if (! httpServer) { 13 | throw new Error('need a base HTTP server as first argument'); 14 | } 15 | var wsserver = websocket.createServer({server: httpServer}, handle); 16 | return wsserver; 17 | 18 | function handle(stream) { 19 | stream.on('error', propagateError); 20 | var server = PouchSync.createServer(onRequest); 21 | server.on('error', propagateError); 22 | stream.pipe(server).pipe(stream); 23 | } 24 | 25 | /* istanbul ignore next */ 26 | function propagateError(err) { 27 | if (ignoreErrorMessages.indexOf(err.message) < 0) { 28 | wsserver.emit('error', err); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /test/server.js: -------------------------------------------------------------------------------- 1 | var Lab = require('lab'); 2 | var lab = exports.lab = Lab.script(); 3 | var describe = lab.experiment; 4 | var before = lab.before; 5 | var after = lab.after; 6 | var it = lab.it; 7 | var Code = require('code'); 8 | var expect = Code.expect; 9 | 10 | var PouchWebsocketSync = require('../'); 11 | 12 | describe('pouch-websocket-sync', function() { 13 | describe('server', function() { 14 | it('fails if no http server is given', function(done) { 15 | expect(function() { 16 | PouchWebsocketSync.createServer(); 17 | }).to.throw('need a base HTTP server as first argument'); 18 | done(); 19 | }); 20 | }); 21 | }); -------------------------------------------------------------------------------- /test/tandem.js: -------------------------------------------------------------------------------- 1 | var Lab = require('lab'); 2 | var lab = exports.lab = Lab.script(); 3 | var describe = lab.experiment; 4 | var before = lab.before; 5 | var after = lab.after; 6 | var it = lab.it; 7 | var Code = require('code'); 8 | var expect = Code.expect; 9 | 10 | var PouchDB = require('pouchdb'); 11 | var http = require('http'); 12 | 13 | var port = 4654; 14 | var PouchWebsocketSync = require('../'); 15 | 16 | describe('pouch-websocket-sync', function() { 17 | var listener; 18 | var httpServer; 19 | var server; 20 | var client; 21 | 22 | var db = new PouchDB({ 23 | name: 'todos', 24 | db: require('memdown'), 25 | }); 26 | var serverDB = new PouchDB({ 27 | name: 'todos-server', 28 | db: require('memdown'), 29 | }); 30 | 31 | describe('server', function() { 32 | 33 | it('can be created', function(done) { 34 | httpServer = http.createServer(); 35 | server = PouchWebsocketSync.createServer(httpServer, onRequest); 36 | done(); 37 | }); 38 | 39 | it('can listen', function(done) { 40 | httpServer.listen(port, done); 41 | }); 42 | 43 | }); 44 | 45 | describe('client', function() { 46 | it('can be created', function(done) { 47 | client = PouchWebsocketSync.createClient(); 48 | client.on('error', function(err) { 49 | console.error('error on first client', err); 50 | done(err); 51 | }); 52 | client.connect('ws://localhost:' + port); 53 | done(); 54 | }); 55 | 56 | it('can be made to sync', function(done) { 57 | var sync = client.sync(db, { credentials: { token: 'some token'}}); 58 | sync.on('error', function(err) { 59 | expect(err.message).to.equal('no database event listener on server'); 60 | sync.cancel(); 61 | done(); 62 | }); 63 | }); 64 | }); 65 | 66 | describe('server', function() { 67 | it('can deny database requests', function(done) { 68 | listener = function(credentials, database, callback) { 69 | expect(credentials).to.deep.equal({token: 'some token'}); 70 | callback(new Error('go away')); 71 | }; 72 | 73 | client = PouchWebsocketSync.createClient(); 74 | client.on('error', function(err) { 75 | console.error('error on second client', err); 76 | done(err); 77 | }); 78 | 79 | client.connect('ws://localhost:' + port); 80 | var sync = client.sync(db, { credentials: { token: 'some token'}}); 81 | 82 | sync.on('error', function(err) { 83 | expect(err.message).to.equal('go away'); 84 | sync.cancel(); 85 | done(); 86 | }); 87 | }); 88 | 89 | it('can accept database requests', function(done) { 90 | listener = function(credentials, database, callback) { 91 | expect(credentials).to.deep.equal({token: 'some other token'}); 92 | callback(null, serverDB); 93 | }; 94 | 95 | client = PouchWebsocketSync.createClient('ws://localhost:' + port); 96 | client.on('error', function(err) { 97 | console.error('error on third client', err); 98 | // done(err); 99 | }); 100 | 101 | client.connect('ws://localhost:' + port); 102 | var sync = client.sync(db, { 103 | credentials: { token: 'some other token'}, 104 | remoteName: 'todos-server', 105 | }); 106 | 107 | db.put({_id: 'A', a:1, b: 2}, function(err, reply) { 108 | if (err) throw err; 109 | sync.once('change', function() { 110 | serverDB.get('A', function(err, doc) { 111 | expect(err).to.equal(null); 112 | expect(doc).to.deep.equal({ 113 | _id: 'A', 114 | a: 1, 115 | b: 2, 116 | _rev: reply.rev, 117 | }); 118 | done(); 119 | }); 120 | }); 121 | }); 122 | 123 | }); 124 | }); 125 | 126 | function onRequest(credentials, dbName, callback) { 127 | if (! listener) { 128 | callback(new Error('no database event listener on server')); 129 | } else { 130 | listener.apply(null, arguments); 131 | } 132 | } 133 | 134 | }); --------------------------------------------------------------------------------