├── .npmrc ├── .gitignore ├── .npmignore ├── .travis.yml ├── ready.js ├── package.json ├── example.js ├── test ├── mux.js ├── existing-protocol.js ├── regression.js └── basic.js ├── README.md ├── mux.js └── index.js /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | db/ 2 | node_modules/ 3 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | curse.js 2 | fun.js 3 | stress.js 4 | db 5 | todo.json 6 | test/ 7 | .travis.yml 8 | example.js 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '10' 4 | - '12' 5 | - '14' 6 | os: 7 | - windows 8 | - osx 9 | - linux 10 | notifications: 11 | email: false 12 | -------------------------------------------------------------------------------- /ready.js: -------------------------------------------------------------------------------- 1 | module.exports = function (work, noauto) { 2 | var ready = false 3 | var fns = [] 4 | 5 | var startWork = function () { 6 | process.nextTick(function () { 7 | work(function () { 8 | ready = true 9 | fns.forEach(process.nextTick) 10 | }) 11 | }) 12 | } 13 | 14 | if (!noauto) startWork() // default behaviour 15 | 16 | return function (fn) { 17 | if (noauto) { // start on first invocation 18 | noauto = false 19 | startWork() 20 | } 21 | 22 | if (!ready) fns.push(fn) 23 | else process.nextTick(fn) 24 | } 25 | } 26 | 27 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "multifeed", 3 | "description": "multi-writer hypercore", 4 | "author": "Stephen Whitmore ", 5 | "version": "6.0.0", 6 | "repository": { 7 | "url": "git://github.com/noffle/multifeed.git" 8 | }, 9 | "homepage": "https://github.com/noffle/multifeed", 10 | "bugs": "https://github.com/noffle/multifeed/issues", 11 | "main": "index.js", 12 | "scripts": { 13 | "test": "tape test/*.js", 14 | "lint": "standard" 15 | }, 16 | "keywords": [], 17 | "dependencies": { 18 | "debug": "^4.1.0", 19 | "hypercore": "^9.6.0", 20 | "hypercore-protocol": "^8.0.7", 21 | "inherits": "^2.0.3", 22 | "mutexify": "^1.2.0", 23 | "once": "^1.4.0", 24 | "random-access-file": "^2.0.1", 25 | "random-access-memory": "^3.1.1", 26 | "through2": "^3.0.0" 27 | }, 28 | "devDependencies": { 29 | "hypercore-crypto": "^1.0.0", 30 | "pump": "^3.0.0", 31 | "pumpify": "^1.5.1", 32 | "random-access-latency": "^1.0.0", 33 | "rimraf": "^2.6.3", 34 | "standard": "^17.1.2", 35 | "tape": "~4.6.2", 36 | "tmp": "0.0.33" 37 | }, 38 | "license": "ISC" 39 | } 40 | -------------------------------------------------------------------------------- /example.js: -------------------------------------------------------------------------------- 1 | var multifeed = require('.') 2 | var ram = require('random-access-memory') 3 | 4 | var multi = multifeed('./db', { valueEncoding: 'json' }) 5 | 6 | // a multifeed starts off empty 7 | console.log(multi.feeds().length) // => 0 8 | 9 | // create as many writeable feeds as you want; returns hypercores 10 | multi.writer('local', function (err, w) { 11 | console.log(w.key, w.writable, w.readable) // => Buffer <0x..> true true 12 | console.log(multi.feeds().length) // => 1 13 | 14 | // write data to any writeable feed, just like with hypercore 15 | w.append('foo', function () { 16 | var m2 = multifeed(ram, { valueEncoding: 'json' }) 17 | m2.writer('local', function (err, w2) { 18 | w2.append('bar', function () { 19 | replicate(multi, m2, function () { 20 | console.log(m2.feeds().length) // => 2 21 | m2.feeds()[1].get(0, function (_, data) { 22 | console.log(data) // => foo 23 | }) 24 | multi.feeds()[1].get(0, function (_, data) { 25 | console.log(data) // => bar 26 | }) 27 | }) 28 | }) 29 | }) 30 | }) 31 | }) 32 | 33 | function replicate (a, b, cb) { 34 | var r = a.replicate(true) 35 | r.pipe(b.replicate(false)).pipe(r) 36 | .once('end', cb) 37 | .once('error', cb) 38 | } 39 | -------------------------------------------------------------------------------- /test/mux.js: -------------------------------------------------------------------------------- 1 | var test = require('tape') 2 | var hypercore = require('hypercore') 3 | var ram = require('random-access-memory') 4 | var multiplexer = require('../mux.js') 5 | var pump = require('pump') 6 | var through = require('through2') 7 | var debug = require('debug')('multifeed/protodump') 8 | 9 | test('key exchange API', function(t){ 10 | t.plan(6) 11 | var encryptionKey = Buffer.from('deadbeefdeadbeefdeadbeefdeadbeef') // used to encrypt the connection 12 | 13 | var mux1 = multiplexer(true, encryptionKey) 14 | var mux2 = multiplexer(false, encryptionKey) 15 | 16 | mux1.ready(function(client){ 17 | mux2.on('manifest', function(m, req) { 18 | t.ok(m.keys instanceof Array, 'Manifest contains an array of feed keys') 19 | t.deepEqual(m.keys, ['A', 'B', 'C']) 20 | req(['A','C','X']) 21 | }) 22 | var countEv = 0 23 | 24 | // replicate event init missing: 25 | mux1.on('replicate', function(keys, repl) { 26 | t.deepEqual(keys, ['A','C'], 'List of filtered keys to initialize') 27 | t.equal(typeof repl, 'function') 28 | if (++countEv == 2) t.end() 29 | }) 30 | mux2.on('replicate', function(keys, repl) { 31 | t.deepEqual(keys, ['A','C'], 'List of filtered keys to initialize') 32 | t.equal(typeof repl, 'function') 33 | if (++countEv == 2) t.end() 34 | }) 35 | mux1.offerFeeds(['A', 'B', 'C']) 36 | }) 37 | mux1.on('finalize', t.error) 38 | pump(mux1.stream,mux2.stream,mux1.stream) 39 | }) 40 | 41 | test('regression: ensure we\'re receiving remote handshake', function(t){ 42 | t.plan(2) 43 | var encryptionKey = Buffer.from('deadbeefdeadbeefdeadbeefdeadbeef') // used to encrypt the connection 44 | 45 | var id1 = Math.random() 46 | var id2 = Math.random() 47 | var mux1 = multiplexer(true, encryptionKey, { userData: id1 }) 48 | var mux2 = multiplexer(false, encryptionKey, { userData: id2 }) 49 | 50 | mux1.once('ready', function (header) { 51 | t.equal(header.userData, id2) 52 | }) 53 | mux2.once('ready', function (header) { 54 | t.equal(header.userData, id1) 55 | }) 56 | 57 | pump(mux1.stream, mux2.stream, mux1.stream) 58 | }) 59 | -------------------------------------------------------------------------------- /test/existing-protocol.js: -------------------------------------------------------------------------------- 1 | var test = require('tape') 2 | var crypto = require('hypercore-crypto') 3 | var Protocol = require('hypercore-protocol') 4 | var multifeed = require('..') 5 | var ram = require('random-access-memory') 6 | var ral = require('random-access-latency') 7 | var tmp = require('tmp').tmpNameSync 8 | var rimraf = require('rimraf') 9 | 10 | test('replicate from existing protocols', function (t) { 11 | t.plan(26) 12 | 13 | var m1 = multifeed(ram, { valueEncoding: 'json' }) 14 | var m2 = multifeed(ram, { valueEncoding: 'json' }) 15 | 16 | var feedEvents1 = 0 17 | var feedEvents2 = 0 18 | m1.on('feed', function (feed, name) { 19 | t.equals(name, String(feedEvents1)) 20 | feedEvents1++ 21 | }) 22 | m2.on('feed', function (feed, name) { 23 | t.equals(name, String(feedEvents2)) 24 | feedEvents2++ 25 | }) 26 | 27 | function setup (m, buf, cb) { 28 | m.writer(function (err, w) { 29 | t.error(err) 30 | w.append(buf, function (err) { 31 | t.error(err) 32 | w.get(0, function (err, data) { 33 | t.error(err) 34 | t.equals(data, buf) 35 | t.deepEquals(m.feeds(), [w]) 36 | cb() 37 | }) 38 | }) 39 | }) 40 | } 41 | 42 | setup(m1, 'foo', function () { 43 | setup(m2, 'bar', function () { 44 | var p1 = new Protocol(true) 45 | var p2 = new Protocol(false) 46 | var r1 = m1.replicate(p1) 47 | var r2 = m2.replicate(p2) 48 | r1.pipe(r2).pipe(r1) 49 | .once('end', check) 50 | r1.once('remote-feeds', function () { 51 | t.ok(true, 'got r1 "remote-feeds" event') 52 | t.equals(m1.feeds().length, 2, 'm1 feeds length is 2') 53 | }) 54 | r2.once('remote-feeds', function () { 55 | t.ok(true, 'got r2 "remote-feeds" event') 56 | t.equals(m2.feeds().length, 2, 'm2 feeds length is 2') 57 | }) 58 | }) 59 | }) 60 | 61 | function check () { 62 | t.equals(m1.feeds().length, 2) 63 | t.equals(m2.feeds().length, 2) 64 | m1.feeds()[1].get(0, function (err, data) { 65 | t.error(err) 66 | t.equals(data, 'bar') 67 | }) 68 | m2.feeds()[1].get(0, function (err, data) { 69 | t.error(err) 70 | t.equals(data, 'foo') 71 | }) 72 | t.equals(feedEvents1, 2) 73 | t.equals(feedEvents2, 2) 74 | } 75 | }) 76 | 77 | test('live replicate from existing protocols', function (t) { 78 | t.plan(22) 79 | 80 | var m1 = multifeed(ram, { valueEncoding: 'json' }) 81 | var m2 = multifeed(ram, { valueEncoding: 'json' }) 82 | 83 | var feedEvents1 = 0 84 | var feedEvents2 = 0 85 | m1.on('feed', function (feed, name) { 86 | t.equals(name, String(feedEvents1)) 87 | feedEvents1++ 88 | }) 89 | m2.on('feed', function (feed, name) { 90 | t.equals(name, String(feedEvents2)) 91 | feedEvents2++ 92 | }) 93 | 94 | function setup (m, buf, cb) { 95 | m.writer(function (err, w) { 96 | t.error(err) 97 | w.append(buf, function (err) { 98 | t.error(err) 99 | w.get(0, function (err, data) { 100 | t.error(err) 101 | t.equals(data, buf) 102 | t.deepEquals(m.feeds(), [w]) 103 | cb() 104 | }) 105 | }) 106 | }) 107 | } 108 | 109 | setup(m1, 'foo', function () { 110 | setup(m2, 'bar', function () { 111 | var p1 = new Protocol(true) 112 | var p2 = new Protocol(false) 113 | var r = m1.replicate(p1, {live:true}) 114 | r.pipe(m2.replicate(p2, {live:true})).pipe(r) 115 | setTimeout(check, 1000) 116 | }) 117 | }) 118 | 119 | function check () { 120 | t.equals(m1.feeds().length, 2) 121 | t.equals(m2.feeds().length, 2) 122 | m1.feeds()[1].get(0, function (err, data) { 123 | t.error(err) 124 | t.equals(data, 'bar') 125 | }) 126 | m2.feeds()[1].get(0, function (err, data) { 127 | t.error(err) 128 | t.equals(data, 'foo') 129 | }) 130 | t.equals(feedEvents1, 2) 131 | t.equals(feedEvents2, 2) 132 | } 133 | }) 134 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # multifeed 2 | 3 | > multi-writer hypercore 4 | 5 | Multifeed lets you: 6 | 7 | 1. manage many hypercores, stored together 8 | 2. replicate a local set of hypercores with a remote set of hypercores (union-syle) 9 | 10 | It solves the problem of [hypercore](https://github.com/mafintosh/hypercore) 11 | only allowing one writer by making it easy to manage and sync a set of 12 | hypercores -- by a variety of authors -- across peers. 13 | 14 | Replication works by extending the regular hypercore exchange mechanism to 15 | include a meta-exchange, where peers share information about the feeds they 16 | have locally, and choose which of the remote feeds they'd like to download in 17 | exchange. Right now, the replication mechanism defaults to sharing all local 18 | feeds and downloading all remote feeds. 19 | 20 | ## Usage 21 | 22 | ```js 23 | var multifeed = require('multifeed') 24 | var ram = require('random-access-memory') 25 | 26 | var multi = multifeed('./db', { valueEncoding: 'json' }) 27 | 28 | // a multifeed starts off empty 29 | console.log(multi.feeds().length) // => 0 30 | 31 | // create as many writeable feeds as you want; returns hypercores 32 | multi.writer('local', function (err, w) { 33 | console.log(w.key, w.writeable, w.readable) // => Buffer <0x..> true true 34 | console.log(multi.feeds().length) // => 1 35 | 36 | // write data to any writeable feed, just like with hypercore 37 | w.append('foo', function () { 38 | var m2 = multifeed(ram, { valueEncoding: 'json' }) 39 | m2.writer('local', function (err, w2) { 40 | w2.append('bar', function () { 41 | replicate(multi, m2, function () { 42 | console.log(m2.feeds().length) // => 2 43 | m2.feeds()[1].get(0, function (_, data) { 44 | console.log(data) // => foo 45 | }) 46 | multi.feeds()[1].get(0, function (_, data) { 47 | console.log(data) // => bar 48 | }) 49 | }) 50 | }) 51 | }) 52 | }) 53 | }) 54 | 55 | function replicate (a, b, cb) { 56 | var r = a.replicate() 57 | r.pipe(b.replicate()).pipe(r) 58 | .once('end', cb) 59 | .once('error', cb) 60 | } 61 | ``` 62 | 63 | ## API 64 | 65 | ```js 66 | var multifeed = require('multifeed') 67 | ``` 68 | 69 | ### var multi = multifeed(storage[, opts]) 70 | 71 | Create a multifeed. 72 | 73 | `storage` is a [random-access-storage](https://github.com/random-access-storage) function, or a string. If a string is given, [random-access-file](https://github.com/random-access-storage/random-access-storage) is used with that string as the filename. 74 | 75 | Included `opts` are passed into new hypercores created, and are the same as [hypercore](https://github.com/mafintosh/hypercore#var-feed--hypercorestorage-key-options)'s. 76 | 77 | Valid `opts` include: 78 | - `opts.encryptionKey` (string): optional encryption key to use during replication. If not provided, a default insecure key will be used. 79 | - `opts.hypercore`: constructor of a hypercore implementation. `hypercore@8.x.x` is used from npm if not provided. 80 | 81 | ### multi.writer([name], [options], cb) 82 | 83 | If no `name` is given, a new local writeable feed is created and returned via 84 | `cb`. 85 | 86 | If `name` is given and was created in the past on this local machine, it is 87 | returned. Otherwise it is created. These names are purely local & are not 88 | synced over the network. This is useful for managing multiple local feeds, e.g. 89 | 90 | ```js 91 | var main = multi.writer('main') // created if doesn't exist 92 | var content = multi.writer('content') // created if doesn't exist 93 | 94 | main === multi.writer('main') // => true 95 | ``` 96 | 97 | `options` is an optional object which may contain: 98 | - `options.keypair` - an object with a custom keypair for the new writer. This should have properties `keypair.publicKey` and `keypair.secretKey`, both of which should be buffers. 99 | 100 | ### var feeds = multi.feeds() 101 | 102 | An array of all hypercores in the multifeed. Check a feed's `key` to 103 | find the one you want, or check its `writable` / `readable` properties. 104 | 105 | Only populated once `multi.ready(fn)` is fired. 106 | 107 | ### var feed = multi.feed(key) 108 | 109 | Fetch a feed by its key `key` (a `Buffer` or hex string). 110 | 111 | ### var stream = multi.replicate(isInitiator, [opts]) 112 | 113 | Create an encrypted duplex stream for replication. 114 | 115 | Ensure that `isInitiator` to `true` to one side, and `false` on the other. This is necessary for setting up the encryption mechanism. 116 | 117 | `isInitiator` may also be a hypercore-protocol instance. 118 | 119 | Works just like hypercore, except *all* local hypercores are exchanged between 120 | replication endpoints. 121 | 122 | ### stream.on('remote-feeds', function () { ... }) 123 | 124 | Emitted when a new batch (1 or more) of remote feeds have begun to replicate with this multifeed instance. 125 | 126 | This is useful for knowing when `multi.feeds()` contains the full set of feeds from the remote side. 127 | 128 | ### multi.on('feed', function (feed, name) { ... }) 129 | 130 | Emitted whenever a new feed is added, whether locally or remotely. 131 | 132 | ## multi.close(cb) 133 | 134 | Close all file resources being held by the multifeed instance. `cb` is called once this is complete. 135 | 136 | ## multi.closed 137 | 138 | `true` if `close()` was run successfully, falsey otherwise. 139 | 140 | # Errors 141 | 142 | The duplex stream returned by `.replicate()` can emit, in addition to regular 143 | stream errors, two fatal errors specific to multifeed: 144 | 145 | - `ERR_VERSION_MISMATCH` 146 | - `err.code = 'ERR_VERSION_MISMATCH'` 147 | - `err.usVersion = 'X.Y.Z'` (semver) 148 | - `err.themVersion = 'A.B.C'` (semver) 149 | 150 | - `ERR_CLIENT_MISMATCH` 151 | - `err.code = 'ERR_CLIENT_MISMATCH'` 152 | - `err.usClient = 'MULTIFEED'` 153 | - `err.themClient = '???'` 154 | 155 | ## Install 156 | 157 | With [npm](https://npmjs.org/) installed, run 158 | 159 | ``` 160 | $ npm install multifeed 161 | ``` 162 | 163 | ## See Also 164 | 165 | - [multifeed-index](https://github.com/noffle/multifeed-index) 166 | - [hypercore](https://github.com/mafintosh/hypercore) 167 | - [kappa-core](https://github.com/noffle/kappa-core) 168 | 169 | ## License 170 | 171 | ISC 172 | -------------------------------------------------------------------------------- /mux.js: -------------------------------------------------------------------------------- 1 | var Protocol = require('hypercore-protocol') 2 | var readify = require('./ready') 3 | var inherits = require('inherits') 4 | var events = require('events') 5 | var debug = require('debug')('multifeed') 6 | var once = require('once') 7 | 8 | // constants 9 | var MULTIFEED = 'MULTIFEED' 10 | var PROTOCOL_VERSION = '4.0.0' 11 | 12 | // extensions 13 | var EXT_HANDSHAKE = 'MULTIFEED_HANDSHAKE' 14 | var EXT_MANIFEST = 'MULTIFEED_MANIFEST' 15 | var EXT_REQUEST_FEEDS = 'MULTIFEED_REQUEST_FEEDS' 16 | var EXT_REPLICATE_FEEDS = 'MULTIFEED_REPLICATE_FEEDS' 17 | 18 | // errors 19 | var ERR_VERSION_MISMATCH = 'ERR_VERSION_MISMATCH' 20 | var ERR_CLIENT_MISMATCH = 'ERR_CLIENT_MISMATCH' 21 | 22 | // `key` - protocol encryption key 23 | function Multiplexer (isInitiator, key, opts) { 24 | if (!(this instanceof Multiplexer)) return new Multiplexer(isInitiator, key, opts) 25 | var self = this 26 | self._opts = opts = opts || {} 27 | this._id = opts._id || Math.floor(Math.random() * 10000).toString(16) 28 | this._initiator = isInitiator 29 | debug(this._id + ' [REPLICATION] New mux initialized', opts) 30 | 31 | // initialize 32 | self._localOffer = [] 33 | self._requestedFeeds = [] 34 | self._remoteOffer = [] 35 | self._activeFeedStreams = {} 36 | 37 | var onFirstKey = true 38 | if (Protocol.isProtocolStream(isInitiator)) { 39 | var stream = this.stream = isInitiator 40 | stream.on('discovery-key', ondiscoverykey) 41 | } else { 42 | var stream = this.stream = new Protocol(isInitiator, Object.assign({}, opts, { 43 | ondiscoverykey 44 | })) 45 | } 46 | function ondiscoverykey (key) { 47 | if (onFirstKey) { 48 | onFirstKey = false 49 | if (!self.stream.remoteVerified(key)) { 50 | self._finalize(new Error('Exchange key did not match remote')) 51 | } 52 | } 53 | } 54 | 55 | this._handshakeExt = this.stream.registerExtension(EXT_HANDSHAKE, { 56 | onmessage: onHandshake, 57 | onerror: function (err) { 58 | self._finalize(err) 59 | }, 60 | encoding: 'json' 61 | }) 62 | 63 | function onHandshake (header) { 64 | debug(self._id + ' [REPLICATION] recv\'d handshake: ', JSON.stringify(header)) 65 | var err 66 | 67 | if (!compatibleVersions(header.version, PROTOCOL_VERSION)) { 68 | debug(self._id + ' [REPLICATION] aborting; version mismatch (us=' + PROTOCOL_VERSION + ')') 69 | err = new Error('protocol version mismatch! us=' + PROTOCOL_VERSION + ' them=' + header.version) 70 | err.code = ERR_VERSION_MISMATCH 71 | err.usVersion = PROTOCOL_VERSION 72 | err.themVersion = header.version 73 | self._finalize(err) 74 | return 75 | } 76 | 77 | if (header.client !== MULTIFEED) { 78 | debug(self._id + ' [REPLICATION] aborting; Client mismatch! expected ', MULTIFEED, 'but got', header.client) 79 | err = new Error('Client mismatch! expected ' + MULTIFEED + ' but got ' + header.client) 80 | err.code = ERR_CLIENT_MISMATCH 81 | err.usClient = MULTIFEED 82 | err.themClient = header.client 83 | self._finalize(err) 84 | return 85 | } 86 | 87 | // Wait a tick, otherwise the _ready handler below won't be listening for this event yet. 88 | process.nextTick(function () { 89 | self.emit('ready', header) 90 | }) 91 | } 92 | 93 | // Open a virtual feed that has the key set to the shared key. 94 | this._feed = stream.open(key, { 95 | onopen: function () { 96 | onFirstKey = false 97 | if (!stream.remoteVerified(key)) { 98 | debug(self._id + ' [REPLICATION] aborting; shared key mismatch') 99 | self._finalize(new Error('shared key version mismatch!')) 100 | return 101 | } 102 | 103 | // send handshake 104 | self._handshakeExt.send(Object.assign({}, opts, { 105 | client: MULTIFEED, 106 | version: PROTOCOL_VERSION, 107 | userData: opts.userData 108 | })) 109 | } 110 | }) 111 | 112 | this._manifestExt = stream.registerExtension(EXT_MANIFEST, { 113 | onmessage: function (msg) { 114 | debug(self._id, 'RECV\'D Ext MANIFEST:', JSON.stringify(msg)) 115 | self._remoteOffer = uniq(self._remoteOffer.concat(msg.keys)) 116 | self.emit('manifest', msg, self.requestFeeds.bind(self)) 117 | }, 118 | onerror: function (err) { 119 | self._finalize(err) 120 | }, 121 | encoding: 'json' 122 | }) 123 | 124 | this._requestFeedsExt = stream.registerExtension(EXT_REQUEST_FEEDS, { 125 | onmessage: function (msg) { 126 | debug(self._id, 'RECV\'D Ext REQUEST_FEEDS:', msg) 127 | self._onRequestFeeds(msg) 128 | }, 129 | onerror: function (err) { 130 | self._finalize(err) 131 | }, 132 | encoding: 'json' 133 | }) 134 | 135 | this._replicateFeedsExt = stream.registerExtension(EXT_REPLICATE_FEEDS, { 136 | onmessage: function (msg) { 137 | debug(self._id, 'RECV\'D Ext REPLICATE_FEEDS:', msg) 138 | self._onRemoteReplicate(msg) 139 | }, 140 | onerror: function (err) { 141 | self._finalize(err) 142 | }, 143 | encoding: 'json' 144 | }) 145 | 146 | if (!self._opts.live) { 147 | self.stream.on('prefinalize', onPrefinalize) 148 | function onPrefinalize () { 149 | self.stream.removeListener('prefinalize', onPrefinalize) 150 | self._feed.close() 151 | debug(self._id + ' [REPLICATION] feed finish/prefinalize (' + self.stream.prefinalize._tick + ')') 152 | } 153 | } 154 | 155 | this._ready = readify(function (done) { 156 | self.once('ready', function (remote) { 157 | debug(self._id + ' [REPLICATION] remote connected and ready') 158 | done(remote) 159 | }) 160 | }) 161 | } 162 | 163 | inherits(Multiplexer, events.EventEmitter) 164 | 165 | Multiplexer.prototype.ready = function (cb) { 166 | this._ready(cb) 167 | } 168 | 169 | Multiplexer.prototype._finalize = function (err) { 170 | if (err) { 171 | debug(this._id + ' [REPLICATION] destroyed due to', err) 172 | this.stream.emit('error', err) 173 | this.stream.destroy(err) 174 | } else { 175 | debug(this._id + ' [REPLICATION] finalized', err) 176 | this.stream.finalize() 177 | } 178 | } 179 | 180 | // Calls to this method results in the creation of a 'manifest' 181 | // that gets transmitted to the other end. 182 | // application is allowed to provide optional custom data in the opts for higher-level 183 | // 'want' selections. 184 | // The manifest-prop `keys` is required, and must equal an array of strings. 185 | Multiplexer.prototype.offerFeeds = function (keys, opts) { 186 | var manifest = Object.assign(opts || {}, { 187 | keys: extractKeys(keys) 188 | }) 189 | debug(this._id + ' [REPLICATON] sending manifest:', manifest) 190 | manifest.keys.forEach(function (key) { this._localOffer.push(key) }.bind(this)) 191 | this._manifestExt.send(manifest) 192 | } 193 | 194 | // Sends your wishlist to the remote 195 | // for classical multifeed `ACCEPT_ALL` behaviour both parts must call `want(remoteHas)` 196 | Multiplexer.prototype.requestFeeds = function (keys) { 197 | keys = extractKeys(keys) 198 | keys.forEach(function (k) { this._requestedFeeds.push(k) }.bind(this)) 199 | debug(this._id + ' [REPLICATION] Sending feeds request', keys) 200 | this._requestFeedsExt.send(keys) 201 | } 202 | 203 | Multiplexer.prototype._onRequestFeeds = function (keys) { 204 | var self = this 205 | var filtered = keys.filter(function (key) { 206 | if (self._localOffer.indexOf(key) === -1) { 207 | debug('[REPLICATION] Warning, remote requested feed that is not in offer', key) 208 | return false 209 | } 210 | 211 | // All good, we accept the key request 212 | return true 213 | }) 214 | filtered = uniq(filtered) 215 | // Tell remote which keys we will replicate 216 | debug(this._id, '[REPLICATION] Sending REPLICATE_FEEDS') 217 | this._replicateFeedsExt.send(filtered) 218 | 219 | // Start replicating as promised. 220 | this._replicateFeeds(filtered, false) 221 | } 222 | 223 | Multiplexer.prototype._onRemoteReplicate = function (keys) { 224 | var self = this 225 | var filtered = keys.filter(function (key) { 226 | return self._requestedFeeds.indexOf(key) !== -1 227 | }) 228 | 229 | // Start replicating as requested. 230 | this._replicateFeeds(filtered, true, function () { 231 | self.stream.emit('remote-feeds') 232 | }) 233 | } 234 | 235 | // Initializes new replication streams for feeds and joins their streams into 236 | // the main stream. 237 | Multiplexer.prototype._replicateFeeds = function (keys, terminateIfNoFeeds, cb) { 238 | if (!cb) cb = noop 239 | 240 | var self = this 241 | keys = uniq(keys) 242 | debug(this._id, '[REPLICATION] _replicateFeeds', keys.length, keys) 243 | 244 | // Postpone stream finalization until all pending cores are added. Otherwise 245 | // a non-live replication might terminate because it thinks all feeds have 246 | // been synced, even though new ones are still in the process of being set up 247 | // for sync. 248 | this.stream.prefinalize.wait() 249 | 250 | this.emit('replicate', keys, once(startFeedReplication)) 251 | 252 | return keys 253 | 254 | function startFeedReplication (feeds) { 255 | if (!Array.isArray(feeds)) feeds = [feeds] 256 | 257 | var pending = feeds.length 258 | 259 | // Stop postponement of prefinalization. 260 | self.stream.prefinalize.continue() 261 | 262 | // only the feeds passed to `feeds` option will be replicated (sent or received) 263 | // hypercore-protocol has built in protection against receiving unexpected/not asked for data. 264 | feeds.forEach(function (feed) { 265 | feed.ready(function () { // wait for each feed to be ready before replicating. 266 | var hexKey = feed.key.toString('hex') 267 | 268 | // prevent a feed from being folded into the main stream twice. 269 | if (typeof self._activeFeedStreams[hexKey] !== 'undefined') { 270 | if (!--pending) cb() 271 | return 272 | } 273 | 274 | debug(self._id, '[REPLICATION] replicating feed:', hexKey) 275 | var fStream = feed.replicate(self._initiator, Object.assign({}, { 276 | live: self._opts.live, 277 | download: self._opts.download, 278 | upload: self._opts.upload, 279 | encrypt: self._opts.encrypt, 280 | stream: self.stream 281 | })) 282 | 283 | // Store reference to this particular feed stream 284 | self._activeFeedStreams[hexKey] = fStream 285 | 286 | var cleanup = function (_, res) { 287 | fStream.removeListener('end', cleanup) 288 | fStream.removeListener('error', cleanup) 289 | if (!self._activeFeedStreams[hexKey]) return 290 | // delete feed stream reference 291 | delete self._activeFeedStreams[hexKey] 292 | debug(self._id, '[REPLICATION] feedStream closed:', hexKey.substr(0, 8)) 293 | } 294 | fStream.once('end', cleanup) 295 | fStream.once('error', cleanup) 296 | 297 | if (!--pending) cb() 298 | }) 299 | }) 300 | 301 | // Bail on replication entirely if there were no feeds to add, and none are pending or active. 302 | if (feeds.length === 0 && Object.keys(self._activeFeedStreams).length === 0 && terminateIfNoFeeds) { 303 | debug(self._id, '[REPLICATION] terminating mux: no feeds to sync') 304 | self._feed.close() 305 | process.nextTick(cb) 306 | } else if (feeds.length === 0) { 307 | process.nextTick(cb) 308 | } 309 | } 310 | } 311 | 312 | Multiplexer.prototype.knownFeeds = function () { 313 | return this._localOffer.concat(this._remoteOffer) 314 | } 315 | 316 | module.exports = Multiplexer 317 | 318 | // String, String -> Boolean 319 | function compatibleVersions (v1, v2) { 320 | var major1 = v1.split('.')[0] 321 | var major2 = v2.split('.')[0] 322 | return parseInt(major1) === parseInt(major2) 323 | } 324 | 325 | function extractKeys (keys) { 326 | if (!Array.isArray(keys)) keys = [keys] 327 | return keys.map(function (o) { 328 | if (typeof o === 'string') return o 329 | if (typeof o === 'object' && o.key) return o.key.toString('hex') 330 | if (o instanceof Buffer) return o.toString('utf8') 331 | }) 332 | .filter(function (o) { return !!o }) // remove invalid entries 333 | } 334 | 335 | function uniq (arr) { 336 | return Object.keys(arr.reduce(function (m, i) { 337 | m[i] = true 338 | return m 339 | }, {})).sort() 340 | } 341 | 342 | function noop () {} 343 | -------------------------------------------------------------------------------- /test/regression.js: -------------------------------------------------------------------------------- 1 | var test = require('tape') 2 | var multifeed = require('..') 3 | var hypercrypto = require('hypercore-crypto') 4 | var ram = require('random-access-memory') 5 | var ral = require('random-access-latency') 6 | var tmp = require('tmp').tmpNameSync 7 | var pump = require('pump') 8 | var crypto = require('crypto') 9 | var fs = require('fs') 10 | 11 | test('regression: concurrency of writer creation', function (t) { 12 | t.plan(3) 13 | 14 | var storage = tmp() 15 | var key 16 | 17 | var multi = multifeed(storage, { valueEncoding: 'json' }) 18 | 19 | multi.writer('minuette', function (err, w) { 20 | t.error(err) 21 | t.ok(w.key) 22 | key = w.key 23 | }) 24 | 25 | multi.ready(function () { 26 | t.equals(multi.feeds().length, 0) 27 | }) 28 | }) 29 | 30 | test('regression: MF with no writer replicate to MF with 1 writer', function (t) { 31 | var m1 = multifeed(ram, { valueEncoding: 'json' }) 32 | var m2 = multifeed(ram, { valueEncoding: 'json' }) 33 | 34 | function setup1 (m, buf, cb) { 35 | m.writer(function (err, w) { 36 | t.error(err) 37 | var bufs = [] 38 | for(var i=0; i < 1000; i++) { 39 | bufs.push(buf) 40 | } 41 | w.append(bufs, function (err) { 42 | t.error(err) 43 | w.get(13, function (err, data) { 44 | t.error(err) 45 | t.equals(data, buf) 46 | t.deepEquals(m.feeds(), [w], 'read matches write') 47 | cb() 48 | }) 49 | }) 50 | }) 51 | } 52 | 53 | function setup2 (m, buf, cb) { 54 | m.writer(function (err, w) { 55 | t.error(err) 56 | var bufs = [] 57 | for(var i=0; i < 10; i++) { 58 | bufs.push(buf) 59 | } 60 | w.append(bufs, function (err) { 61 | t.error(err) 62 | w.get(3, function (err, data) { 63 | t.error(err) 64 | t.equals(data, buf) 65 | t.deepEquals(m.feeds(), [w], 'read matches write') 66 | cb() 67 | }) 68 | }) 69 | }) 70 | } 71 | 72 | setup1(m1, 'foo', function () { 73 | setup2(m2, 'bar', function () { 74 | var r = m1.replicate(true) 75 | r.once('end', done) 76 | var s = m2.replicate(false) 77 | s.once('end', done) 78 | r.pipe(s).pipe(r) 79 | 80 | var pending = 2 81 | function done () { 82 | if (!--pending) check() 83 | } 84 | }) 85 | }) 86 | 87 | function check () { 88 | t.equals(m1.feeds().length, 2, '2 feeds') 89 | t.equals(m2.feeds().length, 2, '2 feeds') 90 | t.equals(m1.feeds()[0].length, 1000, 'writer sees 1000 entries') 91 | t.equals(m1.feeds()[1].length, 10, 'writer sees 10 entries') 92 | t.equals(m2.feeds()[0].length, 10, 'receiver sees 10 entries') 93 | t.equals(m2.feeds()[1].length, 1000, 'receiver sees 1000 entries') 94 | m1.feeds()[1].get(0, function (err, data) { 95 | t.error(err) 96 | t.equals(data, 'bar', 'feed 1 has feed 2 data') 97 | m2.feeds()[1].get(0, function (err, data) { 98 | t.error(err) 99 | t.equals(data, 'foo', 'feed 2 has feed 1 data') 100 | t.end() 101 | }) 102 | }) 103 | } 104 | }) 105 | 106 | test('regression: start replicating before feeds are loaded', function (t) { 107 | t.plan(22) 108 | 109 | var m1 = multifeed(ram, { valueEncoding: 'json' }) 110 | var m2 = multifeed(ram, { valueEncoding: 'json' }) 111 | 112 | var feedEvents1 = 0 113 | var feedEvents2 = 0 114 | m1.on('feed', function (feed, name) { 115 | t.equals(name, String(feedEvents1)) 116 | feedEvents1++ 117 | }) 118 | m2.on('feed', function (feed, name) { 119 | t.equals(name, String(feedEvents2)) 120 | feedEvents2++ 121 | }) 122 | 123 | function setup (m, buf, cb) { 124 | m.writer(function (err, w) { 125 | t.error(err) 126 | w.append(buf, function (err) { 127 | t.error(err) 128 | w.get(0, function (err, data) { 129 | t.error(err) 130 | t.equals(data, buf) 131 | t.deepEquals(m.feeds(), [w]) 132 | cb() 133 | }) 134 | }) 135 | }) 136 | } 137 | 138 | setup(m1, 'foo', function () { 139 | setup(m2, 'bar', function () { 140 | var r = m1.replicate(true) 141 | r.pipe(m2.replicate(false)).pipe(r) 142 | .once('end', check) 143 | }) 144 | }) 145 | 146 | function check () { 147 | t.equals(m1.feeds().length, 2) 148 | t.equals(m2.feeds().length, 2) 149 | m1.feeds()[1].get(0, function (err, data) { 150 | t.error(err) 151 | t.equals(data, 'bar') 152 | }) 153 | m2.feeds()[1].get(0, function (err, data) { 154 | t.error(err) 155 | t.equals(data, 'foo') 156 | }) 157 | t.equals(feedEvents1, 2) 158 | t.equals(feedEvents2, 2) 159 | } 160 | }) 161 | 162 | test('regression: announce new feed on existing connections', function(t) { 163 | t.plan(21); 164 | var m1 = multifeed(ram, { valueEncoding: 'json' }) 165 | var m2 = multifeed(ram, { valueEncoding: 'json' }) 166 | var m3 = multifeed(ram, { valueEncoding: 'json' }) 167 | 168 | setup(m1, "First", function() { 169 | setup(m2, "Second", function() { 170 | setup(m3, "Third", function() { 171 | var feedsReplicated = 0; 172 | var r1 = null, r2 = null; // forward declare replication streams. 173 | 174 | m1.on('feed', function(feed, name) { 175 | feed.get(0, function(err, entry) { 176 | t.error(err) 177 | feedsReplicated++ 178 | switch(feedsReplicated) { 179 | case 1: // First we should see M2's writer 180 | m2.writer('local', function(err, w) { 181 | t.equal(feed.key.toString('hex'), w.key.toString('hex'), "should see m2's writer") 182 | t.equals(entry, "Second", "m2's writer should have been replicated") 183 | }) 184 | break; 185 | case 2: 186 | m3.writer('local', function(err, w) { 187 | t.equal(feed.key.toString('hex'), w.key.toString('hex'), "should see m3's writer") 188 | t.equals(entry, "Third", "m3's writer should have been forwarded via m2") 189 | // close active streams and end the test. 190 | r1.end() 191 | r2.end() 192 | t.end() 193 | }) 194 | break; 195 | default: 196 | t.ok(false, "Only expected to see 2 feed events, got: " + feedsReplicated) 197 | } 198 | }) 199 | }) 200 | 201 | // m1 and m2 are now live connected. 202 | r1 = m1.replicate(true, {live: true}) 203 | r1.pipe(m2.replicate(false, {live: true})).pipe(r1) 204 | 205 | // When m3 is attached to m2, m2 should forward m3's writer to m1. 206 | r2 = m3.replicate(true, {live:true}) 207 | r2.pipe(m2.replicate(false, {live:true})).pipe(r2) 208 | 209 | }) 210 | }) 211 | }) 212 | 213 | function setup (m, buf, cb) { 214 | m.writer('local', function (err, w) { 215 | t.error(err) 216 | w.append(buf, function (err) { 217 | t.error(err) 218 | w.get(0, function (err, data) { 219 | t.error(err) 220 | t.equals(data, buf) 221 | t.deepEquals(m.feeds(), [w]) 222 | cb() 223 | }) 224 | }) 225 | }) 226 | } 227 | }) 228 | 229 | test('regression: replicate before multifeed is ready', function (t) { 230 | t.plan(1) 231 | 232 | var storage = tmp() 233 | var key 234 | 235 | var multi = multifeed(storage, { valueEncoding: 'json' }) 236 | var res = multi.replicate(true) 237 | res.on('error', function () { 238 | t.ok('error hit') 239 | }) 240 | }) 241 | 242 | test('regression: MFs with different root keys cannot replicate', function (t) { 243 | var key = hypercrypto.keyPair().publicKey 244 | var m1, m2 245 | 246 | m1 = multifeed(ram, { valueEncoding: 'json', encryptionKey: key }) 247 | m2 = multifeed(ram, { valueEncoding: 'json' }) // default encryption key 248 | 249 | setup(m1, 'foo', function () { 250 | setup(m2, 'bar', function () { 251 | var r = m1.replicate(true) 252 | var s = m2.replicate(false) 253 | pump(r, s, r, function (err) { 254 | t.same(err.toString(), 'Error: Exchange key did not match remote') 255 | t.end() 256 | }) 257 | }) 258 | }) 259 | 260 | function setup (m, buf, cb) { 261 | m.writer(function (err, w) { 262 | t.error(err) 263 | var bufs = [] 264 | for(var i=0; i < 1000; i++) { 265 | bufs.push(buf) 266 | } 267 | w.append(bufs, function (err) { 268 | t.error(err) 269 | w.get(13, function (err, data) { 270 | t.error(err) 271 | t.equals(data, buf) 272 | t.deepEquals(m.feeds(), [w], 'read matches write') 273 | cb() 274 | }) 275 | }) 276 | }) 277 | } 278 | }) 279 | 280 | test('regression: calling close while closing should not throw errors', function (t) { 281 | var multi = multifeed(ram, { valueEncoding: 'json' }) 282 | multi.ready(function () { 283 | multi.writer('default', function (err, wr) { 284 | t.error(err) 285 | wr.append(Buffer.from('some data'), function (err) { 286 | t.error(err) 287 | var p = 2 288 | multi.close(function () { 289 | t.ok('initial close finished') 290 | t.equal(multi.closed, true) 291 | if (!--p) t.end() 292 | }) 293 | t.equal(multi.closed, false, 'Multi not *yet* closed') 294 | multi.close(function () { 295 | t.ok('second close finished') 296 | t.equal(multi.closed, true) 297 | if (!--p) t.end() 298 | }) 299 | }) 300 | }) 301 | }) 302 | }) 303 | 304 | test('regression: sync two single-core multifeeds /w different storage speeds', function (t) { 305 | t.plan(5) 306 | 307 | function slowram (delay) { 308 | return function (name) { 309 | return ral([delay,delay], ram()) 310 | } 311 | } 312 | 313 | var m1 = multifeed(slowram(1), { valueEncoding: 'json' }) 314 | var m2 = multifeed(slowram(500), { valueEncoding: 'json' }) 315 | 316 | function setup (m, cb) { 317 | m.writer(function (err, w) { 318 | t.error(err) 319 | cb() 320 | }) 321 | } 322 | 323 | setup(m1, function () { 324 | setup(m2, function () { 325 | var r = m1.replicate(true) 326 | var s = m2.replicate(false) 327 | pump(r, s, r, function (err) { 328 | t.error(err) 329 | check() 330 | }) 331 | }) 332 | }) 333 | 334 | function check () { 335 | t.equals(m1.feeds().length, 2, '2 feeds') 336 | t.equals(m2.feeds().length, 2, '2 feeds') 337 | } 338 | }) 339 | 340 | test('regression: ensure encryption key is not written to disk', function (t) { 341 | t.plan(6) 342 | 343 | var storage = tmp() 344 | var key = crypto.randomBytes(32) 345 | 346 | var multi = multifeed(storage, { 347 | encryptionKey: key, 348 | valueEncoding: 'json' 349 | }) 350 | 351 | multi.writer(function (err, w) { 352 | t.error(err) 353 | w.append('foo', function (err) { 354 | t.error(err) 355 | multi.close(function (err) { 356 | t.error(err) 357 | fs.readdir(storage, function (err, res) { 358 | t.error(err) 359 | t.equals(res.length, 1) 360 | t.equals(res[0], '0') 361 | }) 362 | }) 363 | }) 364 | }) 365 | }) 366 | 367 | test('replicate two multifeeds, twice', function (t) { 368 | t.plan(20) 369 | 370 | var m1 = multifeed(ram, { valueEncoding: 'json' }) 371 | var m2 = multifeed(ram, { valueEncoding: 'json' }) 372 | 373 | function setup (m, buf, cb) { 374 | m.writer(function (err, w) { 375 | t.error(err) 376 | w.append(buf, function (err) { 377 | t.error(err) 378 | w.get(0, function (err, data) { 379 | t.error(err) 380 | t.equals(data, buf) 381 | t.deepEquals(m.feeds(), [w]) 382 | cb() 383 | }) 384 | }) 385 | }) 386 | } 387 | 388 | setup(m1, 'foo', function () { 389 | setup(m2, 'bar', function () { 390 | sync(() => { 391 | t.ok(true, 'first sync ok') 392 | sync(() => { 393 | t.ok(true, 'second sync ok') 394 | }) 395 | }) 396 | }) 397 | }) 398 | 399 | function sync (cb) { 400 | var pending = 2 401 | var r1 = m1.replicate(true) 402 | var r2 = m2.replicate(false) 403 | r1.once('remote-feeds', function () { 404 | t.ok(true, 'got r1 "remote-feeds" event') 405 | t.equals(m1.feeds().length, 2, 'm1 feeds length is 2') 406 | if (!--pending) cb() 407 | }) 408 | r2.once('remote-feeds', function () { 409 | t.ok(true, 'got r2 "remote-feeds" event') 410 | t.equals(m2.feeds().length, 2, 'm2 feeds length is 2') 411 | if (!--pending) cb() 412 | }) 413 | r1.pipe(r2).pipe(r1) 414 | } 415 | }) 416 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var hypercore = require('hypercore') 2 | var raf = require('random-access-file') 3 | var ram = require('random-access-memory') 4 | var path = require('path') 5 | var events = require('events') 6 | var inherits = require('inherits') 7 | var readyify = require('./ready') 8 | var mutexify = require('mutexify') 9 | var through = require('through2') 10 | var debug = require('debug')('multifeed') 11 | var multiplexer = require('./mux') 12 | var version = require('./package.json').version 13 | 14 | // Key-less constant hypercore to bootstrap hypercore-protocol replication. 15 | var defaultEncryptionKey = Buffer.from('bee80ff3a4ee5e727dc44197cb9d25bf8f19d50b0f3ad2984cfe5b7d14e75de7', 'hex') 16 | 17 | module.exports = Multifeed 18 | 19 | function Multifeed (storage, opts) { 20 | if (!(this instanceof Multifeed)) return new Multifeed(storage, opts) 21 | this._id = (opts || {})._id || Math.floor(Math.random() * 1000).toString(16) // for debugging 22 | debug(this._id, 'multifeed @ ' + version) 23 | this._feeds = {} 24 | this._feedKeyToFeed = {} 25 | this._streams = [] 26 | 27 | opts = opts || {} 28 | 29 | // Support legacy opts.key 30 | if (opts.key) opts.encryptionKey = opts.key 31 | 32 | this._hypercore = opts.hypercore || hypercore 33 | this._opts = opts 34 | 35 | this.writerLock = mutexify() 36 | 37 | this._close = readyify(_close.bind(this), true) 38 | this.closed = false 39 | 40 | // random-access-storage wrapper that wraps all hypercores in a directory 41 | // structures. (dir/0, dir/1, ...) 42 | this._storage = function (dir) { 43 | return function (name) { 44 | var s = storage 45 | if (typeof storage === 'string') return raf(path.join(storage, dir, name)) 46 | else return s(dir + '/' + name) 47 | } 48 | } 49 | 50 | var self = this 51 | this._ready = readyify(function (done) { 52 | var encryptionKey = defaultEncryptionKey 53 | if (self._opts.encryptionKey) { 54 | if (typeof self._opts.encryptionKey === 'string') encryptionKey = Buffer.from(self._opts.encryptionKey, 'hex') 55 | else encryptionKey = self._opts.encryptionKey 56 | } else { 57 | debug(self._id + ' Warning, running multifeed with unsecure default key') 58 | } 59 | 60 | debug(self._id, 'Using encryption key:', encryptionKey.toString('hex').substring(0,5) + '..') 61 | 62 | var feed = hypercore(ram, encryptionKey) 63 | 64 | feed.on('error', function (err) { 65 | self.emit('error', err) 66 | }) 67 | 68 | feed.ready(function () { 69 | self._root = feed 70 | self._loadFeeds(function (err) { 71 | if (err) { 72 | debug(self._id + ' [INIT] failed to load feeds: ' + err.message) 73 | self.emit('error', err) 74 | return 75 | } 76 | debug(self._id + ' [INIT] finished loading feeds') 77 | done() 78 | }) 79 | }) 80 | }) 81 | 82 | this.setMaxListeners(Infinity) 83 | } 84 | 85 | inherits(Multifeed, events.EventEmitter) 86 | 87 | Multifeed.prototype._addFeed = function (feed, name) { 88 | this._feeds[name] = feed 89 | this._feedKeyToFeed[feed.key.toString('hex')] = feed 90 | feed.setMaxListeners(Infinity) 91 | this.emit('feed', feed, name) 92 | this._forwardLiveFeedAnnouncements(feed, name) 93 | } 94 | 95 | Multifeed.prototype.ready = function (cb) { 96 | this._ready(cb) 97 | } 98 | 99 | Multifeed.prototype.close = function (cb) { 100 | if (typeof cb !== 'function') cb = function noop () {} 101 | return this._close(cb) 102 | } 103 | 104 | function _close (cb) { 105 | var self = this 106 | this.writerLock(function (release) { 107 | function done (err) { 108 | release(function () { 109 | if (!err) self.closed = true 110 | cb(err) 111 | }) 112 | } 113 | 114 | var feeds = values(self._feeds).concat(self._root) 115 | 116 | function next (n) { 117 | if (n >= feeds.length) { 118 | self._feeds = [] 119 | self._feedKeyToFeed = {} 120 | self._root = undefined 121 | self._streams.forEach((mux) => { 122 | mux._finalize() 123 | }) 124 | return done() 125 | } 126 | feeds[n].close(function (err) { 127 | if (err) return done(err) 128 | next(++n) 129 | }) 130 | } 131 | 132 | next(0) 133 | }) 134 | } 135 | 136 | Multifeed.prototype._loadFeeds = function (cb) { 137 | var self = this 138 | 139 | // Hypercores are stored starting at 0 and incrementing by 1. A failed read 140 | // at position 0 implies non-existance of the hypercore. 141 | var pending = 1 142 | function next (n) { 143 | var storage = self._storage('' + n) 144 | var st = storage('key') 145 | st.read(0, 4, function (err) { 146 | if (err) return done() // means there are no more feeds to read 147 | debug(self._id + ' [INIT] loading feed #' + n) 148 | pending++ 149 | var feed = self._hypercore(storage, self._opts) 150 | process.nextTick(next, n + 1) 151 | 152 | feed.ready(function () { 153 | readStringFromStorage(storage('localname'), function (err, name) { 154 | if (!err && name) { 155 | self._addFeed(feed, name) 156 | } else { 157 | self._addFeed(feed, String(n)) 158 | } 159 | st.close(function (err) { 160 | if (err) return done(err) 161 | debug(self._id + ' [INIT] loaded feed #' + n) 162 | done() 163 | }) 164 | }) 165 | }) 166 | }) 167 | } 168 | 169 | function done (err) { 170 | if (err) { 171 | pending = Infinity 172 | return cb(err) 173 | } 174 | if (!--pending) cb() 175 | } 176 | 177 | next(0) 178 | } 179 | 180 | Multifeed.prototype.writer = function (name, opts, cb) { 181 | if (typeof name === 'function' && !cb) { 182 | cb = name 183 | name = undefined 184 | opts = {} 185 | } 186 | if (typeof opts === 'function' && !cb) { 187 | cb = opts 188 | opts = {} 189 | } 190 | 191 | var self = this 192 | const keypair = opts.keypair 193 | 194 | this.ready(function () { 195 | // Short-circuit if already loaded 196 | if (self._feeds[name]) { 197 | process.nextTick(cb, null, self._feeds[name]) 198 | return 199 | } 200 | 201 | debug(self._id + ' [WRITER] creating new writer: ' + name) 202 | 203 | self.writerLock(function (release) { 204 | var len = Object.keys(self._feeds).length 205 | var storage = self._storage('' + len) 206 | 207 | var idx = name || String(len) 208 | 209 | var nameStore = storage('localname') 210 | writeStringToStorage(idx, nameStore, function (err) { 211 | if (err) { 212 | release(function () { 213 | cb(err) 214 | }) 215 | return 216 | } 217 | 218 | var feed = keypair 219 | ? self._hypercore(storage, keypair.publicKey, Object.assign({}, self._opts, { secretKey: keypair.secretKey })) 220 | : self._hypercore(storage, self._opts) 221 | feed.on('error', function (err) { 222 | self.emit('error', err) 223 | }) 224 | 225 | feed.ready(function () { 226 | self._addFeed(feed, String(idx)) 227 | release(function () { 228 | if (err) cb(err) 229 | else cb(null, feed, idx) 230 | }) 231 | }) 232 | }) 233 | }) 234 | }) 235 | } 236 | 237 | Multifeed.prototype.feeds = function () { 238 | return values(this._feeds) 239 | } 240 | 241 | Multifeed.prototype.feed = function (key) { 242 | if (Buffer.isBuffer(key)) key = key.toString('hex') 243 | if (typeof key === 'string') return this._feedKeyToFeed[key] 244 | else return null 245 | } 246 | 247 | Multifeed.prototype.replicate = function (isInitiator, opts) { 248 | if (!this._root) { 249 | var tmp = through() 250 | process.nextTick(function () { 251 | tmp.emit('error', new Error('tried to use "replicate" before multifeed is ready')) 252 | }) 253 | return tmp 254 | } 255 | 256 | if (!opts) opts = {} 257 | var self = this 258 | var mux = multiplexer(isInitiator, self._root.key, Object.assign({}, opts, {_id: this._id})) 259 | 260 | // Add key exchange listener 261 | var onManifest = function (m) { 262 | mux.requestFeeds(m.keys) 263 | } 264 | mux.on('manifest', onManifest) 265 | 266 | // Add replication listener 267 | var onReplicate = function (keys, repl) { 268 | addMissingKeys(keys, function (err) { 269 | if (err) return mux.stream.destroy(err) 270 | 271 | // Create a look up table with feed-keys as keys 272 | // (since not all keys in self._feeds are actual feed-keys) 273 | var key2feed = values(self._feeds).reduce(function (h, feed) { 274 | h[feed.key.toString('hex')] = feed 275 | return h 276 | }, {}) 277 | 278 | // Select feeds by key from LUT 279 | var feeds = keys.map(function (k) { return key2feed[k] }) 280 | repl(feeds) 281 | }) 282 | } 283 | mux.on('replicate', onReplicate) 284 | 285 | // Start streaming 286 | this.ready(function (err) { 287 | if (err) return mux.stream.destroy(err) 288 | if (mux.stream.destroyed) return 289 | mux.ready(function () { 290 | var keys = values(self._feeds).map(function (feed) { return feed.key.toString('hex') }) 291 | mux.offerFeeds(keys) 292 | }) 293 | 294 | // Push session to _streams array 295 | self._streams.push(mux) 296 | 297 | // Register removal 298 | var cleanup = function (err) { 299 | mux.removeListener('manifest', onManifest) 300 | mux.removeListener('replicate', onReplicate) 301 | mux.stream.removeListener('end', cleanup) 302 | mux.stream.removeListener('close', cleanup) 303 | mux.stream.removeListener('error', cleanup) 304 | mux.stream.finalize() 305 | self._streams.splice(self._streams.indexOf(mux), 1) 306 | debug('[REPLICATION] Client connection destroyed', err) 307 | } 308 | mux.stream.once('end', cleanup) 309 | mux.stream.once('close', cleanup) 310 | mux.stream.once('error', cleanup) 311 | }) 312 | 313 | return mux.stream 314 | 315 | // Helper functions 316 | 317 | function addMissingKeys (keys, cb) { 318 | self.ready(function (err) { 319 | if (err) return cb(err) 320 | self.writerLock(function (release) { 321 | addMissingKeysLocked(keys, function (err) { 322 | release(cb, err) 323 | }) 324 | }) 325 | }) 326 | } 327 | 328 | function addMissingKeysLocked (keys, cb) { 329 | var pending = 0 330 | debug(self._id + ' [REPLICATION] recv\'d ' + keys.length + ' keys') 331 | var filtered = keys.filter(function (key) { 332 | return !Number.isNaN(parseInt(key, 16)) && key.length === 64 333 | }) 334 | 335 | var numFeeds = Object.keys(self._feeds).length 336 | var keyId = numFeeds 337 | filtered.forEach(function (key) { 338 | var feeds = values(self._feeds).filter(function (feed) { 339 | return feed.key.toString('hex') === key 340 | }) 341 | if (!feeds.length) { 342 | var myKey = String(keyId) 343 | var storage = self._storage(myKey) 344 | keyId++ 345 | pending++ 346 | var feed 347 | try { 348 | debug(self._id + ' [REPLICATION] trying to create new local hypercore, key=' + key.toString('hex')) 349 | feed = self._hypercore(storage, Buffer.from(key, 'hex'), self._opts) 350 | } catch (e) { 351 | debug(self._id + ' [REPLICATION] failed to create new local hypercore, key=' + key.toString('hex')) 352 | debug(self._id + e.toString()) 353 | if (!--pending) cb() 354 | return 355 | } 356 | feed.ready(function () { 357 | self._addFeed(feed, myKey) 358 | keyId++ 359 | debug(self._id + ' [REPLICATION] succeeded in creating new local hypercore, key=' + key.toString('hex')) 360 | if (!--pending) cb() 361 | }) 362 | } 363 | }) 364 | if (!pending) cb() 365 | } 366 | } 367 | 368 | Multifeed.prototype._forwardLiveFeedAnnouncements = function (feed, name) { 369 | if (!this._streams.length) return // no-op if no live-connections 370 | var hexKey = feed.key.toString('hex') 371 | // Tell each remote that we have a new key available unless 372 | // it's already being replicated 373 | this._streams.forEach(function (mux) { 374 | if (mux.knownFeeds().indexOf(hexKey) === -1) { 375 | debug('Forwarding new feed to existing peer:', hexKey) 376 | mux.offerFeeds([hexKey]) 377 | } 378 | }) 379 | } 380 | 381 | // TODO: what if the new data is shorter than the old data? things will break! 382 | function writeStringToStorage (string, storage, cb) { 383 | var buf = Buffer.from(string, 'utf8') 384 | storage.write(0, buf, function (err) { 385 | storage.close(function (err2) { 386 | cb(err || err2) 387 | }) 388 | }) 389 | } 390 | 391 | function readStringFromStorage (storage, cb) { 392 | storage.stat(function (err, stat) { 393 | if (err) return cb(err) 394 | var len = stat.size 395 | storage.read(0, len, function (err, buf) { 396 | if (err) return cb(err) 397 | var str = buf.toString() 398 | storage.close(function (err) { 399 | cb(err, err ? null : str) 400 | }) 401 | }) 402 | }) 403 | } 404 | 405 | function values (obj) { 406 | return Object.keys(obj).map(function (k) { return obj[k] }) 407 | } 408 | -------------------------------------------------------------------------------- /test/basic.js: -------------------------------------------------------------------------------- 1 | var test = require('tape') 2 | var crypto = require('hypercore-crypto') 3 | var multifeed = require('..') 4 | var ram = require('random-access-memory') 5 | var ral = require('random-access-latency') 6 | var tmp = require('tmp').tmpNameSync 7 | var rimraf = require('rimraf') 8 | 9 | test('no feeds', function (t) { 10 | var multi = multifeed(ram, { valueEncoding: 'json' }) 11 | 12 | t.deepEquals(multi.feeds(), []) 13 | t.end() 14 | }) 15 | 16 | test('create writer', function (t) { 17 | t.plan(5) 18 | 19 | var multi = multifeed(ram, { valueEncoding: 'json' }) 20 | 21 | multi.writer(function (err, w) { 22 | t.error(err) 23 | w.append('foo', function (err) { 24 | t.error(err) 25 | w.get(0, function (err, data) { 26 | t.error(err) 27 | t.equals(data.toString(), 'foo') 28 | t.deepEquals(multi.feeds(), [w]) 29 | }) 30 | }) 31 | }) 32 | }) 33 | 34 | test('get feed by key', function (t) { 35 | t.plan(3) 36 | 37 | var multi = multifeed(ram, { valueEncoding: 'json' }) 38 | 39 | multi.writer(function (err, w) { 40 | t.error(err, 'valid writer created') 41 | var feed = multi.feed(w.key) 42 | t.deepEquals(feed, w, 'writer is the same as retrieved feed (buffer key)') 43 | feed = multi.feed(w.key.toString('hex')) 44 | t.deepEquals(feed, w, 'writer is the same as retrieved feed (hex key)') 45 | }) 46 | }) 47 | 48 | test('get localfeed by name', function (t) { 49 | t.plan(3) 50 | 51 | var multi = multifeed(ram, { valueEncoding: 'json' }) 52 | 53 | multi.writer('bob', function (err, w) { 54 | t.error(err, 'valid writer created') 55 | multi.writer('bob', function (err, w2) { 56 | t.error(err, 'valid writer retrieved') 57 | t.deepEquals(w2, w, 'writer is the same as retrieved feed') 58 | }) 59 | }) 60 | }) 61 | 62 | test('replicate two multifeeds, one empty', function (t) { 63 | t.plan(3) 64 | 65 | var m1 = multifeed(ram, { valueEncoding: 'json' }) 66 | var m2 = multifeed(ram, { valueEncoding: 'json' }) 67 | 68 | m1.writer(function () { 69 | m2.ready(function () { 70 | var r = m1.replicate(true) 71 | r.pipe(m2.replicate(false)).pipe(r) 72 | .once('end', check) 73 | .once('remote-feeds', function () { 74 | t.ok(true, 'got "remote-feeds" event') 75 | }) 76 | }) 77 | }) 78 | 79 | function check () { 80 | t.equals(m1.feeds().length, 1) 81 | t.equals(m2.feeds().length, 1) 82 | } 83 | }) 84 | 85 | test('replicate two empty multifeeds', function (t) { 86 | t.plan(3) 87 | 88 | var m1 = multifeed(ram, { valueEncoding: 'json' }) 89 | var m2 = multifeed(ram, { valueEncoding: 'json' }) 90 | 91 | m1.ready(function () { 92 | m2.ready(function () { 93 | var r = m1.replicate(true) 94 | r.pipe(m2.replicate(false)).pipe(r) 95 | .once('end', check) 96 | .once('remote-feeds', function () { 97 | t.ok(true, 'got "remote-feeds" event') 98 | }) 99 | }) 100 | }) 101 | 102 | function check () { 103 | t.equals(m1.feeds().length, 0) 104 | t.equals(m2.feeds().length, 0) 105 | } 106 | }) 107 | 108 | test('replicate two multifeeds', function (t) { 109 | t.plan(26) 110 | 111 | var m1 = multifeed(ram, { valueEncoding: 'json' }) 112 | var m2 = multifeed(ram, { valueEncoding: 'json' }) 113 | 114 | var feedEvents1 = 0 115 | var feedEvents2 = 0 116 | m1.on('feed', function (feed, name) { 117 | t.equals(name, String(feedEvents1)) 118 | feedEvents1++ 119 | }) 120 | m2.on('feed', function (feed, name) { 121 | t.equals(name, String(feedEvents2)) 122 | feedEvents2++ 123 | }) 124 | 125 | function setup (m, buf, cb) { 126 | m.writer(function (err, w) { 127 | t.error(err) 128 | w.append(buf, function (err) { 129 | t.error(err) 130 | w.get(0, function (err, data) { 131 | t.error(err) 132 | t.equals(data, buf) 133 | t.deepEquals(m.feeds(), [w]) 134 | cb() 135 | }) 136 | }) 137 | }) 138 | } 139 | 140 | setup(m1, 'foo', function () { 141 | setup(m2, 'bar', function () { 142 | var r1 = m1.replicate(true) 143 | var r2 = m2.replicate(false) 144 | r1.pipe(r2).pipe(r1) 145 | .once('end', check) 146 | r1.once('remote-feeds', function () { 147 | t.ok(true, 'got r1 "remote-feeds" event') 148 | t.equals(m1.feeds().length, 2, 'm1 feeds length is 2') 149 | }) 150 | r2.once('remote-feeds', function () { 151 | t.ok(true, 'got r2 "remote-feeds" event') 152 | t.equals(m2.feeds().length, 2, 'm2 feeds length is 2') 153 | }) 154 | }) 155 | }) 156 | 157 | function check () { 158 | t.equals(m1.feeds().length, 2) 159 | t.equals(m2.feeds().length, 2) 160 | m1.feeds()[1].get(0, function (err, data) { 161 | t.error(err) 162 | t.equals(data, 'bar') 163 | }) 164 | m2.feeds()[1].get(0, function (err, data) { 165 | t.error(err) 166 | t.equals(data, 'foo') 167 | }) 168 | t.equals(feedEvents1, 2) 169 | t.equals(feedEvents2, 2) 170 | } 171 | }) 172 | 173 | test('live replicate two multifeeds', function (t) { 174 | t.plan(22) 175 | 176 | var m1 = multifeed(ram, { valueEncoding: 'json' }) 177 | var m2 = multifeed(ram, { valueEncoding: 'json' }) 178 | 179 | var feedEvents1 = 0 180 | var feedEvents2 = 0 181 | m1.on('feed', function (feed, name) { 182 | t.equals(name, String(feedEvents1)) 183 | feedEvents1++ 184 | }) 185 | m2.on('feed', function (feed, name) { 186 | t.equals(name, String(feedEvents2)) 187 | feedEvents2++ 188 | }) 189 | 190 | function setup (m, buf, cb) { 191 | m.writer(function (err, w) { 192 | t.error(err) 193 | w.append(buf, function (err) { 194 | t.error(err) 195 | w.get(0, function (err, data) { 196 | t.error(err) 197 | t.equals(data, buf) 198 | t.deepEquals(m.feeds(), [w]) 199 | cb() 200 | }) 201 | }) 202 | }) 203 | } 204 | 205 | setup(m1, 'foo', function () { 206 | setup(m2, 'bar', function () { 207 | var r = m1.replicate(true, {live:true}) 208 | r.pipe(m2.replicate(false, {live:true})).pipe(r) 209 | setTimeout(check, 1000) 210 | }) 211 | }) 212 | 213 | function check () { 214 | t.equals(m1.feeds().length, 2) 215 | t.equals(m2.feeds().length, 2) 216 | m1.feeds()[1].get(0, function (err, data) { 217 | t.error(err) 218 | t.equals(data, 'bar') 219 | }) 220 | m2.feeds()[1].get(0, function (err, data) { 221 | t.error(err) 222 | t.equals(data, 'foo') 223 | }) 224 | t.equals(feedEvents1, 2) 225 | t.equals(feedEvents2, 2) 226 | } 227 | }) 228 | 229 | test('get localfeed by name across disk loads', function (t) { 230 | t.plan(5) 231 | 232 | var storage = tmp() 233 | var multi = multifeed(storage, { valueEncoding: 'json' }) 234 | 235 | multi.writer('minuette', function (err, w) { 236 | t.error(err) 237 | t.ok(w.key) 238 | 239 | multi.close(function () { 240 | var multi2 = multifeed(storage, { valueEncoding: 'json' }) 241 | multi2.writer('minuette', function (err, w2) { 242 | t.error(err) 243 | t.ok(w.key) 244 | t.deepEquals(w2.key, w.key, 'keys match') 245 | }) 246 | }) 247 | }) 248 | }) 249 | 250 | test('close', function (t) { 251 | var storage = tmp() 252 | var multi = multifeed(storage, { valueEncoding: 'json' }) 253 | 254 | multi.writer('minuette', function (err, w) { 255 | t.error(err) 256 | 257 | multi.close(function () { 258 | t.deepEquals(multi.feeds(), [], 'no feeds present') 259 | t.equals(multi.closed, true) 260 | rimraf(storage, function (err) { 261 | t.error(err, 'Deleted folder without error') 262 | t.end() 263 | }) 264 | }) 265 | }) 266 | }) 267 | 268 | test('close after double-open', function (t) { 269 | var storage = tmp() 270 | 271 | openWriteClose(function (err) { 272 | t.error(err) 273 | openWriteClose(function (err) { 274 | t.error(err) 275 | rimraf(storage, function (err) { 276 | t.error(err, 'Deleted folder without error') 277 | t.end() 278 | }) 279 | }) 280 | }) 281 | 282 | function openWriteClose (cb) { 283 | var multi = multifeed(storage, { valueEncoding: 'json' }) 284 | multi.writer('minuette', function (err, w) { 285 | t.error(err) 286 | w.append({type: 'node'}, function (err) { 287 | t.error(err) 288 | multi.close(cb) 289 | }) 290 | }) 291 | } 292 | }) 293 | 294 | test('can provide custom encryption key', function (t) { 295 | t.plan(2) 296 | 297 | var key = crypto.keyPair().publicKey 298 | var multi = multifeed(ram, { valueEncoding: 'json', encryptionKey: key }) 299 | multi.ready(function () { 300 | t.same(multi._opts.encryptionKey, key, 'encryption key set') 301 | t.same(multi._root.key, key, 'fake key set') 302 | }) 303 | }) 304 | 305 | test('replicate slow-to-open multifeeds', function (t) { 306 | t.plan(22) 307 | 308 | function slow (delay) { 309 | return function (name) { 310 | return ral([delay,delay], ram()) 311 | } 312 | } 313 | 314 | var m1 = multifeed(slow(100), { valueEncoding: 'json' }) 315 | var m2 = multifeed(slow(100), { valueEncoding: 'json' }) 316 | 317 | var feedEvents1 = 0 318 | var feedEvents2 = 0 319 | m1.on('feed', function (feed, name) { 320 | t.equals(name, String(feedEvents1)) 321 | feedEvents1++ 322 | }) 323 | m2.on('feed', function (feed, name) { 324 | t.equals(name, String(feedEvents2)) 325 | feedEvents2++ 326 | }) 327 | 328 | function setup (m, buf, cb) { 329 | m.writer(function (err, w) { 330 | t.error(err) 331 | w.append(buf, function (err) { 332 | t.error(err) 333 | w.get(0, function (err, data) { 334 | t.error(err) 335 | t.equals(data, buf) 336 | t.deepEquals(m.feeds(), [w]) 337 | cb() 338 | }) 339 | }) 340 | }) 341 | } 342 | 343 | setup(m1, 'foo', function () { 344 | setup(m2, 'bar', function () { 345 | var r = m1.replicate(true) 346 | r.pipe(m2.replicate(false)).pipe(r) 347 | .once('end', check) 348 | }) 349 | }) 350 | 351 | function check () { 352 | t.equals(m1.feeds().length, 2) 353 | t.equals(m2.feeds().length, 2) 354 | m1.feeds()[1].get(0, function (err, data) { 355 | t.error(err) 356 | t.equals(data, 'bar') 357 | }) 358 | m2.feeds()[1].get(0, function (err, data) { 359 | t.error(err) 360 | t.equals(data, 'foo') 361 | }) 362 | t.equals(feedEvents1, 2) 363 | t.equals(feedEvents2, 2) 364 | } 365 | }) 366 | 367 | test('can create writer with custom keypair', function (t) { 368 | t.plan(7) 369 | 370 | const keypair = { 371 | publicKey: Buffer.from('ce1f0639f6559736d5c98f9df9af111ff20f0980674297e4eb40cc8f00f1157e', 'hex'), 372 | secretKey: Buffer.from('559f807745b2dd136ec96ebdffa81f0631bfc4bc6ee4bc86f5666b24db91665ace1f0639f6559736d5c98f9df9af111ff20f0980674297e4eb40cc8f00f1157e', 'hex') 373 | } 374 | 375 | var multi = multifeed(ram, { valueEncoding: 'json' }) 376 | multi.ready(function () { 377 | multi.writer('moose', { keypair }, function (err, w) { 378 | t.error(err, 'valid writer created') 379 | t.same(w.key.toString('hex'), keypair.publicKey.toString('hex'), 'public keys match') 380 | t.same(w.secretKey.toString('hex'), keypair.secretKey.toString('hex'), 'secret keys match') 381 | w.append('foo', function (err) { 382 | t.error(err, 'no error when appending to feed') 383 | w.get(0, function (err, data) { 384 | t.error(err) 385 | t.equals(data.toString(), 'foo') 386 | t.deepEquals(multi.feeds(), [w]) 387 | }) 388 | }) 389 | }) 390 | }) 391 | }) 392 | 393 | test('can replicate with custom keypairs', function (t) { 394 | t.plan(16) 395 | 396 | const keypair1 = { 397 | publicKey: Buffer.from('731e8277432cad15c39f275de593a50cf2e689b0139f2d1ad2a130b84a8b1407', 'hex'), 398 | secretKey: Buffer.from('bf54c2aa004c76e7575839ff1fd7c242f9ba14b019afeed0e0536a6c3483e78c731e8277432cad15c39f275de593a50cf2e689b0139f2d1ad2a130b84a8b1407', 'hex') 399 | } 400 | 401 | const keypair2 = { 402 | publicKey: Buffer.from('ce1f0639f6559736d5c98f9df9af111ff20f0980674297e4eb40cc8f00f1157e', 'hex'), 403 | secretKey: Buffer.from('559f807745b2dd136ec96ebdffa81f0631bfc4bc6ee4bc86f5666b24db91665ace1f0639f6559736d5c98f9df9af111ff20f0980674297e4eb40cc8f00f1157e', 'hex') 404 | } 405 | 406 | var m1 = multifeed(ram, { valueEncoding: 'json' }) 407 | var m2 = multifeed(ram, { valueEncoding: 'json' }) 408 | 409 | setup(m1, keypair1, 'foo', () => { 410 | setup(m2, keypair2, 'bar', (r) => { 411 | var r = m1.replicate(true) 412 | r.pipe(m2.replicate(false)).pipe(r) 413 | .once('end', check) 414 | }) 415 | }) 416 | 417 | function setup (m, keypair, buf, cb) { 418 | m.writer('local', { keypair }, function (err, w) { 419 | t.error(err) 420 | w.append(buf, function (err) { 421 | t.error(err) 422 | w.get(0, function (err, data) { 423 | t.error(err) 424 | t.equals(data, buf) 425 | t.deepEquals(m.feeds(), [w]) 426 | cb() 427 | }) 428 | }) 429 | }) 430 | } 431 | 432 | function check () { 433 | t.equals(m1.feeds().length, 2) 434 | t.equals(m2.feeds().length, 2) 435 | m1.feeds()[1].get(0, function (err, data) { 436 | t.error(err) 437 | t.equals(data, 'foo') 438 | }) 439 | m2.feeds()[1].get(0, function (err, data) { 440 | t.error(err) 441 | t.equals(data, 'bar') 442 | }) 443 | } 444 | }) 445 | --------------------------------------------------------------------------------