├── package.json ├── ex.js ├── index.js ├── test └── basic.js └── README.md /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ssb-exchange", 3 | "description": "", 4 | "author": "Stephen Whitmore ", 5 | "version": "1.0.0", 6 | "repository": { 7 | "url": "git://github.com/noffle/ssb-exchange.git" 8 | }, 9 | "homepage": "https://github.com/noffle/ssb-exchange", 10 | "bugs": "https://github.com/noffle/ssb-exchange/issues", 11 | "main": "index.js", 12 | "scripts": { 13 | "test": "tape test/*.js", 14 | "lint": "standard" 15 | }, 16 | "keywords": [], 17 | "dependencies": { 18 | "pull-cat": "^1.1.11", 19 | "pull-stream": "^3.6.8", 20 | "secure-scuttlebutt": "^18.0.6", 21 | "ssb-keys": "^7.0.16", 22 | "through2": "^2.0.3" 23 | }, 24 | "devDependencies": { 25 | "rimraf": "^2.6.2", 26 | "standard": "~10.0.0", 27 | "tape": "~4.6.2" 28 | }, 29 | "license": "ISC" 30 | } 31 | -------------------------------------------------------------------------------- /ex.js: -------------------------------------------------------------------------------- 1 | var pull = require('pull-stream') 2 | var path = require('path') 3 | var replicate = require('.') 4 | 5 | var alice = createDbAndFeed('alice') 6 | var bob = createDbAndFeed('bob') 7 | 8 | alice.feed.add({ type: 'post', text: 'alice\'s First Post!' }, function (err, msg, hash) { 9 | alice.feed.add({ type: 'post', text: 'alice\'s Second Post!' }, function (err, msg, hash) { 10 | bob.feed.add({ type: 'post', text: 'bob\'s First Post!' }, function (err, msg, hash) { 11 | sync() 12 | }) 13 | }) 14 | }) 15 | 16 | function sync () { 17 | var r1 = replicate(alice.ssb, 'alice') 18 | var r2 = replicate(bob.ssb, 'bob') 19 | 20 | r1.pipe(r2).pipe(r1) 21 | 22 | r1.on('end', function () { 23 | pull( 24 | alice.ssb.createLogStream({keys: false, values: true}), 25 | pull.drain(console.log) 26 | ) 27 | }) 28 | } 29 | 30 | function createDbAndFeed (dir) { 31 | var keys = require('ssb-keys').loadOrCreateSync(path.join(dir, 'secret')) 32 | var ssb = require('secure-scuttlebutt/create')(path.join(dir, 'db')) 33 | var feed = ssb.createFeed(keys) 34 | return {ssb: ssb, feed: feed} 35 | } 36 | 37 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var pull = require('pull-stream') 2 | var cat = require('pull-cat') 3 | var path = require('path') 4 | var fs = require('fs') 5 | var ssbKeys = require('ssb-keys') 6 | var through = require('through2') 7 | 8 | module.exports = function (ssb, name) { 9 | var t = through.obj(write) 10 | 11 | var localClock = null 12 | var remoteClock = null 13 | var localDone = false 14 | var remoteDone = false 15 | 16 | ssb.getVectorClock(function (err, clock) { 17 | if (err) return t.emit('error', err) 18 | // console.log('local', clock) 19 | localClock = clock 20 | t.push(clock) 21 | computeAndSend() 22 | }) 23 | 24 | return t 25 | 26 | function write (data, _, next) { 27 | if (!remoteClock) { 28 | remoteClock = data 29 | computeAndSend() 30 | next() 31 | } else if (data === 'done') { 32 | remoteDone = true 33 | if (localDone && remoteDone) t.push(null) 34 | next() 35 | } else if (remoteDone) { 36 | next() 37 | } else { 38 | // console.log('add to', name, 'db', data) 39 | ssb.add(data, function (err) { 40 | next(err) 41 | }) 42 | } 43 | } 44 | 45 | function computeAndSend () { 46 | if (!localClock || !remoteClock) return 47 | var toSend = computeWhatToSend(localClock, remoteClock) 48 | // console.log('gonna send from', name, toSend) 49 | var sources = Object.keys(toSend) 50 | .map(function (key) { 51 | return ssb.createHistoryStream({id: key, seq: toSend[key], keys: false, values: true}) 52 | }) 53 | pull( 54 | cat(sources), 55 | pull.drain(function (msg) { 56 | // console.log('sending from', name, msg) 57 | t.push(msg) 58 | }, function () { 59 | t.push('done') 60 | localDone = true 61 | if (localDone && remoteDone) t.push(null) 62 | }) 63 | ) 64 | } 65 | } 66 | 67 | function computeWhatToSend (myClock, yourClock) { 68 | var res = {} 69 | Object.keys(myClock).forEach(function (key) { 70 | // remote feed knows at least as much as we do 71 | if (yourClock[key] && yourClock[key] >= myClock[key]) return 72 | 73 | // send their clock seq and onward 74 | res[key] = yourClock[key] || 0 75 | }) 76 | return res 77 | } 78 | -------------------------------------------------------------------------------- /test/basic.js: -------------------------------------------------------------------------------- 1 | var test = require('tape') 2 | var pull = require('pull-stream') 3 | var path = require('path') 4 | var fs = require('fs') 5 | var ssbKeys = require('ssb-keys') 6 | var through = require('through2') 7 | var rimraf = require('rimraf') 8 | var exchange = require('../') 9 | 10 | function createDbAndFeed (dir) { 11 | var keys = require('ssb-keys').loadOrCreateSync(path.join(dir, 'secret')) 12 | var ssb = require('secure-scuttlebutt/create')(path.join(dir, 'db')) 13 | var feed = ssb.createFeed(keys) 14 | return {ssb: ssb, feed: feed} 15 | } 16 | 17 | // stream all messages for a particular keypair. 18 | function printFeed (db) { 19 | pull( 20 | db.ssb.createHistoryStream({id: db.feed.id, seq: 0}), 21 | pull.drain(function (msg) { 22 | console.log(msg) 23 | }) 24 | ) 25 | } 26 | 27 | test('alice & bob', function (t) { 28 | t.plan(5) 29 | 30 | rimraf.sync('alice') 31 | rimraf.sync('bob') 32 | 33 | var alice = createDbAndFeed('alice') 34 | var bob = createDbAndFeed('bob') 35 | 36 | alice.feed.add({ type: 'post', text: 'alice\'s First Post!' }, function (err, msg, hash) { 37 | t.error(err) 38 | alice.feed.add({ type: 'post', text: 'alice\'s Second Post!' }, function (err, msg, hash) { 39 | t.error(err) 40 | bob.feed.add({ type: 'post', text: 'bob\'s First Post!' }, function (err, msg, hash) { 41 | t.error(err) 42 | sync() 43 | }) 44 | }) 45 | }) 46 | 47 | function sync () { 48 | var r1 = exchange(alice.ssb, 'alice') 49 | var r2 = exchange(bob.ssb, 'bob') 50 | 51 | r1.pipe(r2).pipe(r1) 52 | 53 | r1.on('end', function () { 54 | pull( 55 | alice.ssb.createLogStream({keys: false, values: true}), 56 | pull.map(function (msg) { 57 | return [ msg.author, msg.sequence ] 58 | }), 59 | pull.collect(function (err, actual) { 60 | var expected = [ 61 | [ alice.feed.id, 1 ], 62 | [ alice.feed.id, 2 ], 63 | [ bob.feed.id, 1 ] 64 | ] 65 | t.deepEquals(actual, expected) 66 | }) 67 | ) 68 | }) 69 | 70 | r2.on('end', function () { 71 | pull( 72 | bob.ssb.createLogStream({keys: false, values: true}), 73 | pull.map(function (msg) { 74 | return [ msg.author, msg.sequence ] 75 | }), 76 | pull.collect(function (err, actual) { 77 | var expected = [ 78 | [ bob.feed.id, 1 ], 79 | [ alice.feed.id, 1 ], 80 | [ alice.feed.id, 2 ] 81 | ] 82 | t.deepEquals(actual, expected) 83 | }) 84 | ) 85 | }) 86 | } 87 | }) 88 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ssb-exchange 2 | 3 | > fully exchange two secure-scuttlebutt databases over a duplex stream 4 | 5 | ## Usage 6 | 7 | ```js 8 | var replicate = require('ssb-exchange') 9 | var path = require('path') 10 | var pull = require('pull-stream') 11 | 12 | var alice = createDbAndFeed('alice') 13 | var bob = createDbAndFeed('bob') 14 | 15 | alice.feed.add({ type: 'post', text: 'alice\'s First Post!' }, function (err, msg, hash) { 16 | alice.feed.add({ type: 'post', text: 'alice\'s Second Post!' }, function (err, msg, hash) { 17 | bob.feed.add({ type: 'post', text: 'bob\'s First Post!' }, function (err, msg, hash) { 18 | sync() 19 | }) 20 | }) 21 | }) 22 | 23 | function sync () { 24 | var r1 = replicate(alice.ssb, 'alice') 25 | var r2 = replicate(bob.ssb, 'bob') 26 | 27 | r1.pipe(r2).pipe(r1) 28 | 29 | r1.on('end', function () { 30 | pull( 31 | alice.ssb.createLogStream({keys: false, values: true}), 32 | pull.drain(console.log) 33 | ) 34 | }) 35 | } 36 | 37 | function createDbAndFeed (dir) { 38 | var keys = require('ssb-keys').loadOrCreateSync(path.join(dir, 'secret')) 39 | var ssb = require('secure-scuttlebutt/create')(path.join(dir, 'db')) 40 | var feed = ssb.createFeed(keys) 41 | return {ssb: ssb, feed: feed} 42 | } 43 | ``` 44 | 45 | outputs 46 | 47 | ``` 48 | { previous: null, 49 | sequence: 1, 50 | author: '@6VKBcLTLgL8vDuLHtgFlYEg/yTgYqI1WoPENQrzJNs4=.ed25519', 51 | timestamp: 1528823339650, 52 | hash: 'sha256', 53 | content: { type: 'post', text: 'alice\'s First Post!' }, 54 | signature: 'aBLQHwohD2FlTtLU1tsrFovoRQk+cTjgEi2SWZw+A1/Z+RlkDuRvFDplwI9oATzAprWhm1KuA69D9dUAnzMICA==.sig.ed25519' } 55 | { previous: '%spHHU8AUpoLU7+tTvdPrfFoGmST/twD/xRH0Jd2Jhgw=.sha256', 56 | sequence: 2, 57 | author: '@6VKBcLTLgL8vDuLHtgFlYEg/yTgYqI1WoPENQrzJNs4=.ed25519', 58 | timestamp: 1528823339700, 59 | hash: 'sha256', 60 | content: { type: 'post', text: 'alice\'s Second Post!' }, 61 | signature: 'mZJqo73/jHAyoOajkInsAMicSx/iTvZg0f5FONuXspuXfzkxKtVUH8atYFmL5JNPxEXc6IR0dhpeMhAN/CHZDg==.sig.ed25519' } 62 | { previous: null, 63 | sequence: 1, 64 | author: '@SWgLOnMBG7wEnmyP+vjueVluNj46IYRiktfyErVbjhA=.ed25519', 65 | timestamp: 1528823339717, 66 | hash: 'sha256', 67 | content: { type: 'post', text: 'bob\'s First Post!' }, 68 | signature: 'xrhWyHx129zjk7Bg2CPUQtASqehoeK7Yp6m9n3Yd2DgtMLzDlj1LPBCQIEglnUK2h7uLzKZQnAFrDrYjLYopAQ==.sig.ed25519' } 69 | ``` 70 | 71 | ## API 72 | 73 | ```js 74 | var replicate = require('ssb-exchange') 75 | ``` 76 | 77 | ### var stream = replicate(ssb) 78 | 79 | Returns a duplex stream, `stream`, that can be piped into another ssb-exchange 80 | duplex stream. The two will exchange information about what feeds they have, and 81 | send only new information to the other end. The stream terminates once all data 82 | has been sent and written to the local ssb database. 83 | 84 | ## Install 85 | 86 | With [npm](https://npmjs.org/) installed, run 87 | 88 | ``` 89 | $ npm install ssb-exchange 90 | ``` 91 | 92 | ## License 93 | 94 | ISC 95 | --------------------------------------------------------------------------------