diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..b2095be --- /dev/null +++ b/.prettierrc @@ -0,0 +1,4 @@ +{ + "semi": false, + "singleQuote": true +} diff --git a/README.md b/README.md index b441aa6..202977d 100644 --- a/README.md +++ b/README.md @@ -38,11 +38,13 @@ installed, instead, you need to call its API methods yourself (primarily `request` or `block`), or use a scheduler module such as [ssb-replication-scheduler](https://github.com/ssb-ngi-pointer/ssb-replication-scheduler). -### `ssb.ebt.request(destination, replicating)` ("sync" muxrpc API) +### `ssb.ebt.request(destination, replicating, formatName)` ("sync" muxrpc API) Request that the SSB feed ID `destination` be replicated. `replication` is a boolean, where `true` indicates we want to replicate the destination. If set to -`false`, replication is stopped. +`false`, replication is stopped. `formatName` is optional and used to specify +the specific EBT instance, otherwise the first where isFeed is `true` for +`destination` is used. Returns undefined, always. @@ -56,6 +58,9 @@ them. the SSB feed ID of the peer being blocked, and `blocking` is a boolean that indicates whether to enable the block (`true`) or to unblock (`false`). +`formatName` is optional and used to specify the specific EBT instance, +otherwise the first where isFeed is `true` for `origin` is used. + Returns undefined, always. ### `ssb.ebt.peerStatus(id)` ("sync" muxrpc API) @@ -96,9 +101,78 @@ The output looks like this: } } ``` + + +### `ssb.ebt.registerFormat(methods)` ("sync" muxrpc API) + +Register a new format for replication. Note this does not have to be a +new feed format, it could also be indexed replication or sliced +replication. See `formats` folder for examples. + +By registering a format you create a new EBT instance used for +replicating feeds using that format. This means its own clock. Message +will be replicated using the `replicateFormat` API. The `methods` +argument must implement the following functions. The example below +shows the implementation for 'classic' ed25519 SSB feeds. +
+CLICK HERE + +```js +{ + name: 'classic', + // In case `isFeed` needs to load some state asynchronously + prepareForIsFeed(sbot, feedId, cb) { + cb() + }, + // used in request, block, cleanClock, sbot.post, vectorClock + isFeed(sbot, feedId) { + return ref.isFeed(feedId) + }, + getAtSequence(sbot, pair, cb) { + sbot.getAtSequence([pair.id, pair.sequence], (err, msg) => { + cb(err, msg ? msg.value : null) + }) + }, + appendMsg(sbot, msgVal, cb) { + sbot.add(msgVal, (err, msg) => { + cb(err && err.fatal ? err : null, msg) + }) + }, + // used in onAppend + convertMsg(msgVal) { + return msgVal + }, + // used in vectorClock + isReady(sbot) { + return Promise.resolve(true) + }, + + // used in ebt:stream to distinguish between messages and notes + isMsg(msgVal) { + return Number.isInteger(msgVal.sequence) && msgVal.sequence > 0 && + ref.isFeed(msgVal.author) && msgVal.content + }, + // used in ebt:events + getMsgAuthor(msgVal) { + return msgVal.author + }, + // used in ebt:events + getMsgSequence(msgVal) { + return msgVal.sequence + } +} +```
+### `ssb.ebt.setClockForSlicedReplication(feedId, sequence, formatName)` ("sync" muxrpc API) + +Sets the internal clock of a feed to a specific sequence. Note this +does not start replicating the feed, it only updates the clock. By +combining this with `clock` it is possible do to sliced replication +with a remote peer where say only the latest 100 messages of a feed is +replicated. + ### (Internal) `ssb.ebt.replicate(opts)` ("duplex" muxrpc API) Creates a duplex replication stream to the remote peer. When two peers connect, @@ -106,6 +180,20 @@ the peer who initiated the call (the client) should call this. You do not need to call this method, it is called automatically in ssb-ebt whenever our peer connects to a remote peer. `opts` is an object with one field: `version`. +### (Internal) `ssb.ebt.replicateFormat(opts)` ("duplex" muxrpc API) + +Creates a duplex replication stream to the remote peer. This behaves +similar to `replicate` except it takes an extra field `format` +specifying what is transferred over this EBT stream. Classic feeds are +still replicated using `replicate` while this will be used to +replicate other feed formats. + +### (Internal) `ssb.ebt.clock(opts, cb)` ("async" muxrpc API) + +Gets the current vector clock of a remote peer. `opts` is an object +with one field: `format` specifying what format to get the vector +clock for. Defaults to 'classic'. + ## Testing and debugging There are several scripts in `./debug` which can be used for testing EBT diff --git a/formats/bendy-butt.js b/formats/bendy-butt.js new file mode 100644 index 0000000..90c02d8 --- /dev/null +++ b/formats/bendy-butt.js @@ -0,0 +1,48 @@ +const SSBURI = require('ssb-uri2') +const bendyButt = require('ssb-bendy-butt') + +module.exports = { + name: 'bendybutt-v1', + prepareForIsFeed(sbot, feedId, cb) { + cb() + }, + // used in request, block, cleanClock, sbot.post, vectorClock + isFeed (sbot, feedId) { + return SSBURI.isBendyButtV1FeedSSBURI(feedId) + }, + getAtSequence (sbot, pair, cb) { + sbot.getAtSequence([pair.id, pair.sequence], (err, msg) => { + cb(err, msg ? bendyButt.encode(msg.value) : null) + }) + }, + appendMsg (sbot, msgVal, cb) { + sbot.add(bendyButt.decode(msgVal), (err, msg) => { + cb(err && err.fatal ? err : null, msg) + }) + }, + convertMsg (sbot, msgVal, cb) { + cb(null, bendyButt.encode(msgVal)) + }, + // used in vectorClock + isReady (sbot) { + return Promise.resolve(true) + }, + + // used in ebt:stream to distinguish between messages and notes + isMsg (bbVal) { + if (Buffer.isBuffer(bbVal)) { + const msgVal = bendyButt.decode(bbVal) + return msgVal && SSBURI.isBendyButtV1FeedSSBURI(msgVal.author) + } else { + return bbVal && SSBURI.isBendyButtV1FeedSSBURI(bbVal.author) + } + }, + // used in ebt:events + getMsgAuthor (bbVal) { + if (Buffer.isBuffer(bbVal)) { return bendyButt.decode(bbVal).author } else { return bbVal.author } + }, + // used in ebt:events + getMsgSequence (bbVal) { + if (Buffer.isBuffer(bbVal)) { return bendyButt.decode(bbVal).sequence } else { return bbVal.sequence } + } +} diff --git a/formats/classic.js b/formats/classic.js new file mode 100644 index 0000000..c3fd4fa --- /dev/null +++ b/formats/classic.js @@ -0,0 +1,44 @@ +const ref = require('ssb-ref') + +module.exports = { + name: 'classic', + prepareForIsFeed(sbot, feedId, cb) { + cb() + }, + // used in request, block, cleanClock, sbot.post, vectorClock + isFeed (sbot, feedId) { + return ref.isFeed(feedId) + }, + getAtSequence (sbot, pair, cb) { + sbot.getAtSequence([pair.id, pair.sequence], (err, msg) => { + cb(err, msg ? msg.value : null) + }) + }, + appendMsg (sbot, msgVal, cb) { + sbot.add(msgVal, (err, msg) => { + cb(err && err.fatal ? err : null, msg) + }) + }, + // used in onAppend + convertMsg (sbot, msgVal, cb) { + cb(null, msgVal) + }, + // used in vectorClock + isReady (sbot) { + return Promise.resolve(true) + }, + + // used in ebt:stream to distinguish between messages and notes + isMsg (msgVal) { + return Number.isInteger(msgVal.sequence) && msgVal.sequence > 0 && + ref.isFeed(msgVal.author) && msgVal.content + }, + // used in ebt:events + getMsgAuthor (msgVal) { + return msgVal.author + }, + // used in ebt:events + getMsgSequence (msgVal) { + return msgVal.sequence + } +} diff --git a/formats/indexed.js b/formats/indexed.js new file mode 100644 index 0000000..2c0bf23 --- /dev/null +++ b/formats/indexed.js @@ -0,0 +1,53 @@ +const pify = require('promisify-4loc') +const ref = require('ssb-ref') +const { QL0 } = require('ssb-subset-ql') + +module.exports = { + name: 'indexed', + prepareForIsFeed(sbot, feedId, cb) { + sbot.metafeeds.ensureLoaded(feedId, cb) + }, + isFeed (sbot, author) { + const info = sbot.metafeeds.findByIdSync(author) + return info && info.feedpurpose === 'index' + }, + appendMsg (sbot, msgTuple, cb) { + const [msgVal, payload] = msgTuple + sbot.db.addTransaction([msgVal], [payload], cb) + }, + getAtSequence (sbot, pair, cb) { + sbot.getAtSequence([pair.id, pair.sequence], (err, msg) => { + if (err) return cb(err) + + module.exports.convertMsg(sbot, msg.value, cb) + }) + }, + convertMsg (sbot, msgVal, cb) { + const { sequence } = msgVal.content.indexed + const authorInfo = sbot.metafeeds.findByIdSync(msgVal.author) + if (!authorInfo) return cb(new Error('Unknown author:' + msgVal.author)) + const { author } = QL0.parse(authorInfo.metadata.query) + sbot.getAtSequence([author, sequence], (err, indexedMsg) => { + if (err) return cb(err) + + cb(null, [msgVal, indexedMsg.value]) + }) + }, + isReady (sbot) { + return pify(sbot.metafeeds.loadState)() + }, + isMsg (msgTuple) { + if (Array.isArray(msgTuple) && msgTuple.length === 2) { + const [msgVal, payload] = msgTuple + return Number.isInteger(msgVal.sequence) && msgVal.sequence > 0 && + ref.isFeed(msgVal.author) && msgVal.content + } else + return false + }, + getMsgAuthor (msgTuple) { + return msgTuple[0].author + }, + getMsgSequence (msgTuple) { + return msgTuple[0].sequence + } +} diff --git a/index.js b/index.js index 3184a5e..41705c6 100644 --- a/index.js +++ b/index.js @@ -2,14 +2,13 @@ const path = require('path') const pull = require('pull-stream') const toPull = require('push-stream-to-pull-stream') const EBT = require('epidemic-broadcast-trees') -const isFeed = require('ssb-ref').isFeed const Store = require('lossy-store') const toUrlFriendly = require('base64-url').escape const getSeverity = require('ssb-network-errors') -const DeferredPromise = require('p-defer') const pullDefer = require('pull-defer') +const classicMethods = require('./formats/classic') -function hook (hookable, fn) { +function hook(hookable, fn) { if (typeof hookable === 'function' && hookable.hook) { hookable.hook(fn) } @@ -21,21 +20,23 @@ exports.version = '1.0.0' exports.manifest = { replicate: 'duplex', + replicateFormat: 'duplex', request: 'sync', block: 'sync', - peerStatus: 'sync' + peerStatus: 'sync', + clock: 'async', } exports.permissions = { anonymous: { - allow: ['replicate'] - } + allow: ['replicate', 'replicateFormat', 'clock'], + }, } // there was a bug that caused some peers // to request things that weren't feeds. // this is fixed, so just ignore anything that isn't a feed. -function cleanClock (clock) { +function cleanClock(clock, isFeed) { for (const k in clock) { if (!isFeed(k)) { delete clock[k] @@ -44,48 +45,107 @@ function cleanClock (clock) { } exports.init = function (sbot, config) { - const dir = config.path ? path.join(config.path, 'ebt') : null - const store = Store(dir, null, toUrlFriendly) - - const ebt = EBT({ - logging: config.ebt && config.ebt.logging, - id: sbot.id, - getClock (id, cb) { - store.ensure(id, function () { - const clock = store.get(id) || {} - cleanClock(clock) - cb(null, clock) - }) - }, - setClock (id, clock) { - cleanClock(clock, 'non-feed key when saving clock') - store.set(id, clock) - }, - getAt (pair, cb) { - sbot.getAtSequence([pair.id, pair.sequence], (err, data) => { - cb(err, data ? data.value : null) - }) - }, - append (msg, cb) { - sbot.add(msg, (err, msg) => { - cb(err && err.fatal ? err : null, msg) - }) - }, - isFeed: isFeed - }) + const ebts = [] + registerFormat(classicMethods) + + function registerFormat(format) { + if (!format.name) throw new Error('format must have a name') + + const dirName = 'ebt' + (format.name === 'classic' ? '' : format.name) + const dir = config.path ? path.join(config.path, dirName) : null + const store = Store(dir, null, toUrlFriendly) + + // EBT expects a function of only feedId so we bind sbot here + const isFeed = format.isFeed.bind(format, sbot) + const { isMsg, getMsgAuthor, getMsgSequence } = format + + const ebt = EBT({ + logging: config.ebt && config.ebt.logging, + id: sbot.id, + getClock(id, cb) { + store.ensure(id, function () { + const clock = store.get(id) || {} + cleanClock(clock, isFeed) + cb(null, clock) + }) + }, + setClock(id, clock) { + cleanClock(clock, isFeed) + store.set(id, clock) + }, + getAt(pair, cb) { + format.getAtSequence(sbot, pair, cb) + }, + append(msgVal, cb) { + format.appendMsg(sbot, msgVal, cb) + }, + + isFeed, + isMsg, + getMsgAuthor, + getMsgSequence, + }) + + // attach a few methods we need in this module + ebt.convertMsg = format.convertMsg.bind(format, sbot) + ebt.isReady = format.isReady.bind(format, sbot) + ebt.isFeed = isFeed + ebt.name = format.name + ebt.prepareForIsFeed = format.prepareForIsFeed.bind(format, sbot) + + const existingId = ebts.findIndex((e) => e.name === format.name) + if (existingId !== -1) ebts[existingId] = ebt + else ebts.push(ebt) + } - const initialized = DeferredPromise() + function getEBT(formatName) { + const ebt = ebts.find((ebt) => ebt.name === formatName) + if (!ebt) throw new Error('Unknown format: ' + formatName) + + return ebt + } + + let isReady = false + let waiting = [] + function onReady(fn) { + if (isReady) fn() + else waiting.push(fn) + } sbot.getVectorClock((err, clock) => { if (err) console.warn('Failed to getVectorClock in ssb-ebt because:', err) - ebt.state.clock = clock || {} - ebt.update() - initialized.resolve() + + const readies = ebts.map((ebt) => ebt.isReady()) + Promise.all(readies).then(() => { + ebts.forEach((ebt) => { + const validClock = {} + for (const k in clock) { + if (ebt.isFeed(k)) { + validClock[k] = clock[k] + } + } + + ebt.state.clock = validClock + ebt.update() + }) + + isReady = true + for (let i = 0; i < waiting.length; ++i) waiting[i]() + waiting = [] + }) }) sbot.post((msg) => { - initialized.promise.then(() => { - ebt.onAppend(msg.value) + onReady(() => { + ebts.forEach((ebt) => { + if (ebt.isFeed(msg.value.author)) { + ebt.convertMsg(msg.value, (err, converted) => { + if (err) + console.warn('Failed to convert msg in ssb-ebt because:', err) + else ebt.onAppend(converted) + }) + } + }) }) }) @@ -94,6 +154,7 @@ exports.init = function (sbot, config) { if (sbot.progress) { hook(sbot.progress, function (fn) { const _progress = fn() + const ebt = ebts.find((ebt) => ebt.name === 'classic') const ebtProg = ebt.progress() if (ebtProg.target) _progress.ebt = ebtProg return _progress @@ -103,81 +164,157 @@ exports.init = function (sbot, config) { sbot.on('rpc:connect', function (rpc, isClient) { if (rpc.id === sbot.id) return // ssb-client connecting to ssb-server if (isClient) { - initialized.promise.then(() => { - const opts = { version: 3 } - const local = toPull.duplex(ebt.createStream(rpc.id, opts.version, true)) - const remote = rpc.ebt.replicate(opts, (networkError) => { - if (networkError && getSeverity(networkError) >= 3) { - console.error('rpc.ebt.replicate exception:', networkError) - } + onReady(() => { + ebts.forEach((ebt) => { + const format = ebt.name + const opts = { version: 3, format } + const local = toPull.duplex( + ebt.createStream(rpc.id, opts.version, true) + ) + + // for backwards compatibility we always replicate classic + // feeds using existing replicate RPC + const replicate = + format === 'classic' ? rpc.ebt.replicate : rpc.ebt.replicateFormat + + const remote = replicate(opts, (networkError) => { + if (networkError && getSeverity(networkError) >= 3) { + console.error('rpc.ebt.replicate exception:', networkError) + } + }) + pull(local, remote, local) }) - pull(local, remote, local) }) } }) - function request (destFeedId, requesting) { - initialized.promise.then(() => { - if (!isFeed(destFeedId)) return - ebt.request(destFeedId, requesting) + function findEBTForFeed(feedId, formatName) { + let ebt + if (formatName) { + ebt = ebts.find((ebt) => ebt.name === formatName) + } else { + ebt = ebts.find((ebt) => ebt.isFeed(feedId)) + } + + if (!ebt) { + ebt = ebts.find((ebt) => ebt.name === 'classic') + } + + return ebt + } + + function request(destFeedId, requesting, formatName) { + onReady(() => { + if (requesting) { + const ebt = findEBTForFeed(destFeedId, formatName) + ebt.prepareForIsFeed(destFeedId, () => { + if (!ebt.isFeed(destFeedId)) return + ebt.request(destFeedId, true) + }) + } else { + // If we don't want a destFeedId, make sure it's not registered anywhere + ebts.forEach((ebt) => { + ebt.request(destFeedId, false) + }) + } }) } - function block (origFeedId, destFeedId, blocking) { - initialized.promise.then(() => { - if (!isFeed(origFeedId)) return - if (!isFeed(destFeedId)) return - if (blocking) { - ebt.block(origFeedId, destFeedId, true) - } else if ( - ebt.state.blocks[origFeedId] && + function block(origFeedId, destFeedId, blocking, formatName) { + onReady(() => { + const ebt = findEBTForFeed(origFeedId, formatName) + ebt.prepareForIsFeed(destFeedId, () => { + if (!ebt.isFeed(origFeedId)) return + if (!ebt.isFeed(destFeedId)) return + + if (blocking) { + ebt.block(origFeedId, destFeedId, true) + } else if ( + ebt.state.blocks[origFeedId] && ebt.state.blocks[origFeedId][destFeedId] - ) { - // only update unblock if they were already blocked - ebt.block(origFeedId, destFeedId, false) - } + ) { + // only update unblock if they were already blocked + ebt.block(origFeedId, destFeedId, false) + } + }) }) } - function replicate (opts) { + function replicateFormat(opts) { if (opts.version !== 3) { throw new Error('expected ebt.replicate({version: 3})') } - var deferred = pullDefer.duplex() - initialized.promise.then(() => { + const formatName = opts.format || 'classic' + const ebt = getEBT(formatName) + + const deferred = pullDefer.duplex() + onReady(() => { // `this` refers to the remote peer who called this muxrpc API - deferred.resolve(toPull.duplex(ebt.createStream(this.id, opts.version, false))) + deferred.resolve( + toPull.duplex(ebt.createStream(this.id, opts.version, false)) + ) }) return deferred } - // get replication status for feeds for this id. - function peerStatus (id) { + // get replication status for feeds for this id + function peerStatus(id) { id = id || sbot.id + + const ebt = findEBTForFeed(id) + const data = { id: id, seq: ebt.state.clock[id], - peers: {} + peers: {}, } + for (const k in ebt.state.peers) { const peer = ebt.state.peers[k] - if (peer.clock[id] != null || - (peer.replicating && peer.replicating[id] != null)) { + if ( + peer.clock[id] != null || + (peer.replicating && peer.replicating[id] != null) + ) { const rep = peer.replicating && peer.replicating[id] data.peers[k] = { seq: peer.clock[id], - replicating: rep + replicating: rep, } } } + return data } + function clock(opts, cb) { + if (!cb) { + cb = opts + opts = { format: 'classic' } + } + + onReady(() => { + const ebt = getEBT(opts.format) + cb(null, ebt.state.clock) + }) + } + + function setClockForSlicedReplication(feedId, sequence, formatName) { + onReady(() => { + const ebt = findEBTForFeed(feedId, formatName) + + ebt.state.clock[feedId] = sequence + }) + } + return { request, block, - replicate, - peerStatus + replicate: replicateFormat, + replicateFormat, + peerStatus, + clock, + setClockForSlicedReplication, + registerFormat, } } diff --git a/package.json b/package.json index a3287cd..ed2af43 100644 --- a/package.json +++ b/package.json @@ -12,45 +12,61 @@ "main": "index.js", "files": [ "*.js", - "debug/*.js" + "debug/*.js", + "formats/*.js" ], "engines": { "node": ">=10" }, "dependencies": { "base64-url": "^2.2.0", - "epidemic-broadcast-trees": "^8.0.4", + "epidemic-broadcast-trees": "^9.0.0", "lossy-store": "^1.2.3", - "p-defer": "^3.0.0", + "promisify-4loc": "^1.0.0", "pull-defer": "^0.2.3", "pull-stream": "^3.6.0", "push-stream-to-pull-stream": "^1.0.4", + "ssb-bendy-butt": "^0.12.2", "ssb-network-errors": "^1.0.0", - "ssb-ref": "^2.13.0" + "ssb-ref": "^2.13.0", + "ssb-subset-ql": "^0.6.1", + "ssb-uri2": "^1.5.2" }, "devDependencies": { "cat-names": "^3.0.0", "dog-names": "^2.0.0", + "husky": "^4.3.0", + "mkdirp": "^1.0.4", "nyc": "^15.1.0", - "promisify-4loc": "^1.0.0", + "prettier": "^2.1.2", + "pretty-quick": "^3.1.0", "pull-paramap": "^1.2.2", "rimraf": "^2.7.1", "rng": "^0.2.2", "secret-stack": "^6.4.0", + "ssb-caps": "^1.1.0", "ssb-client": "^4.9.0", "ssb-db": "^19.2.0", + "ssb-db2": "^2.6.0", "ssb-generate": "^1.0.1", + "ssb-index-feed-writer": "^0.6.0", "ssb-keys": "^8.1.0", + "ssb-meta-feeds": "^0.22.0", "ssb-validate": "^4.1.4", - "standardx": "^7.0.0", "tap-spec": "^5.0.0", "tape": "^5.2.2" }, "scripts": { - "lint": "standardx --fix '**/*.js'", "test": "set -e; for t in test/*.js; do echo \"#Test: $t\"; tape $t | tap-spec; done", "test-verbose": "TEST_VERBOSE=1 npm test", - "coverage": "nyc --reporter=lcov npm test" + "coverage": "nyc --reporter=lcov npm test", + "format-code": "prettier --write \"*.js\" \"test/*.js\"", + "format-code-staged": "pretty-quick --staged --pattern \"*.js\" --pattern \"test/*.js\"" + }, + "husky": { + "hooks": { + "pre-commit": "npm run format-code-staged" + } }, "nyc": { "exclude": [ diff --git a/test/block.js b/test/block.js index 70b2688..415e512 100644 --- a/test/block.js +++ b/test/block.js @@ -5,7 +5,7 @@ const pify = require('promisify-4loc') const u = require('./misc/util') const createSsbServer = SecretStack({ - caps: { shs: crypto.randomBytes(32).toString('base64') } + caps: { shs: crypto.randomBytes(32).toString('base64') }, }) .use(require('ssb-db')) .use(require('../')) @@ -16,19 +16,19 @@ const REPLICATION_TIMEOUT = 2 * CONNECTION_TIMEOUT const alice = createSsbServer({ temp: 'test-block-alice', timeout: CONNECTION_TIMEOUT, - keys: u.keysFor('alice') + keys: u.keysFor('alice'), }) const bob = createSsbServer({ temp: 'test-block-bob', timeout: CONNECTION_TIMEOUT, - keys: u.keysFor('bob') + keys: u.keysFor('bob'), }) tape('alice blocks bob, then unblocks', async (t) => { await Promise.all([ pify(alice.publish)({ type: 'post', text: 'hello' }), - pify(bob.publish)({ type: 'post', text: 'hello' }) + pify(bob.publish)({ type: 'post', text: 'hello' }), ]) // Self replicate @@ -37,17 +37,17 @@ tape('alice blocks bob, then unblocks', async (t) => { alice.ebt.request(bob.id, false) alice.ebt.block(alice.id, bob.id, true) - t.pass('alice does not want bob\'s data') + t.pass("alice does not want bob's data") bob.ebt.request(alice.id, true) bob.ebt.block(bob.id, alice.id, false) - t.pass('bob wants alice\'s data') + t.pass("bob wants alice's data") try { await Promise.all([ pify(bob.connect)(alice.getAddress()), u.readOnceFromDB(bob, REPLICATION_TIMEOUT), - u.readOnceFromDB(alice, REPLICATION_TIMEOUT) + u.readOnceFromDB(alice, REPLICATION_TIMEOUT), ]) t.fail('replication should not succeed') } catch (err) { @@ -63,14 +63,14 @@ tape('alice blocks bob, then unblocks', async (t) => { alice.ebt.request(bob.id, true) alice.ebt.block(alice.id, bob.id, false) - t.pass('alice now wants bob\'s data') + t.pass("alice now wants bob's data") // Silly idempotent operation just to increase test coverage alice.ebt.block(alice.id, bob.id, false) const [rpcBobToAlice, msgAtAlice] = await Promise.all([ pify(bob.connect)(alice.getAddress()), - u.readOnceFromDB(alice, REPLICATION_TIMEOUT) + u.readOnceFromDB(alice, REPLICATION_TIMEOUT), ]) t.equals(msgAtAlice.value.author, bob.id, 'alice has a msg from bob') @@ -80,9 +80,6 @@ tape('alice blocks bob, then unblocks', async (t) => { await pify(rpcBobToAlice.close)(true) - await Promise.all([ - pify(alice.close)(true), - pify(bob.close)(true) - ]) + await Promise.all([pify(alice.close)(true), pify(bob.close)(true)]) t.end() }) diff --git a/test/clock.js b/test/clock.js new file mode 100644 index 0000000..0cde7f4 --- /dev/null +++ b/test/clock.js @@ -0,0 +1,70 @@ +const tape = require('tape') +const crypto = require('crypto') +const SecretStack = require('secret-stack') +const sleep = require('util').promisify(setTimeout) +const pify = require('promisify-4loc') +const u = require('./misc/util') + +const createSsbServer = SecretStack({ + caps: { shs: crypto.randomBytes(32).toString('base64') }, +}) + .use(require('ssb-db')) + .use(require('../')) + +const CONNECTION_TIMEOUT = 500 // ms +const REPLICATION_TIMEOUT = 2 * CONNECTION_TIMEOUT + +const alice = createSsbServer({ + temp: 'test-clock-alice', + timeout: CONNECTION_TIMEOUT, + keys: u.keysFor('alice'), +}) + +const bob = createSsbServer({ + temp: 'test-clock-bob', + timeout: CONNECTION_TIMEOUT, + keys: u.keysFor('bob'), +}) + +tape('clock works', async (t) => { + await Promise.all([ + pify(alice.publish)({ type: 'post', text: 'hello' }), + pify(bob.publish)({ type: 'post', text: 'hello' }), + ]) + + const clockAlice = await pify(alice.ebt.clock)() + t.equal(clockAlice[alice.id], 1, 'self clock') + + // Self replicate + alice.ebt.request(alice.id, true) + bob.ebt.request(bob.id, true) + + alice.ebt.request(bob.id, true) + bob.ebt.request(alice.id, true) + + await pify(bob.connect)(alice.getAddress()) + + await sleep(REPLICATION_TIMEOUT) + t.pass('wait for replication to complete') + + const clockAliceAfter = await pify(alice.ebt.clock)() + t.equal(clockAliceAfter[alice.id], 1, 'clock ok') + t.equal(clockAliceAfter[bob.id], 1, 'clock ok') + + const clockBobAfter = await pify(bob.ebt.clock)() + t.equal(clockBobAfter[alice.id], 1, 'clock ok') + t.equal(clockBobAfter[bob.id], 1, 'clock ok') + + await pify(alice.publish)({ type: 'post', text: 'hello again' }), + await sleep(REPLICATION_TIMEOUT) + t.pass('wait for replication to complete') + + const clockAliceAfter2 = await pify(alice.ebt.clock)() + t.equal(clockAliceAfter2[alice.id], 2, 'clock ok') + + const clockBobAfter2 = await pify(bob.ebt.clock)() + t.equal(clockBobAfter2[alice.id], 2, 'clock ok') + + await Promise.all([pify(alice.close)(true), pify(bob.close)(true)]) + t.end() +}) diff --git a/test/formats.js b/test/formats.js new file mode 100644 index 0000000..0b12551 --- /dev/null +++ b/test/formats.js @@ -0,0 +1,433 @@ +const tape = require('tape') +const SecretStack = require('secret-stack') +const sleep = require('util').promisify(setTimeout) +const pify = require('promisify-4loc') +const u = require('./misc/util') + +const caps = require('ssb-caps') +const rimraf = require('rimraf') +const mkdirp = require('mkdirp') +const ssbKeys = require('ssb-keys') +const SSBURI = require('ssb-uri2') +const bendyButt = require('ssb-bendy-butt') +const { where, author, type, toPromise } = require('ssb-db2/operators') + +function createSSBServer() { + return SecretStack({ appKey: caps.shs }) + .use(require('ssb-db2')) + .use(require('ssb-db2/compat/ebt')) + .use(require('ssb-meta-feeds')) + .use(require('ssb-index-feed-writer')) + .use(require('../')) +} + +const CONNECTION_TIMEOUT = 500 // ms +const REPLICATION_TIMEOUT = 2 * CONNECTION_TIMEOUT + +function getFreshDir(name) { + const dir = '/tmp/test-format-' + name + rimraf.sync(dir) + mkdirp.sync(dir) + return dir +} + +const aliceDir = getFreshDir('alice') +let alice = createSSBServer().call(null, { + path: aliceDir, + timeout: CONNECTION_TIMEOUT, + keys: u.keysFor('alice'), +}) + +const bobDir = getFreshDir('bob') +let bob = createSSBServer().call(null, { + path: bobDir, + timeout: CONNECTION_TIMEOUT, + keys: u.keysFor('bob'), +}) + +function getBBMsg(mainKeys) { + // fake some keys + const mfKeys = ssbKeys.generate() + const classicUri = SSBURI.fromFeedSigil(mfKeys.id) + const { type, /* format, */ data } = SSBURI.decompose(classicUri) + const bendybuttUri = SSBURI.compose({ type, format: 'bendybutt-v1', data }) + mfKeys.id = bendybuttUri + + const content = { + type: 'metafeed/add/existing', + feedpurpose: 'main', + subfeed: mainKeys.id, + metafeed: mfKeys.id, + tangles: { + metafeed: { + root: null, + previous: null, + }, + }, + } + + const bbmsg = bendyButt.encodeNew( + content, + mainKeys, + mfKeys, + 1, + null, + Date.now(), + null + ) + + return bendyButt.decode(bbmsg) +} + +const bendyButtMethods = require('../formats/bendy-butt') + +// need them later +let aliceMFId +let bobMFId + +tape('multiple formats', async (t) => { + alice.ebt.registerFormat(bendyButtMethods) + bob.ebt.registerFormat(bendyButtMethods) + + // self replicate + alice.ebt.request(alice.id, true) + bob.ebt.request(bob.id, true) + + // publish normal messages + await Promise.all([ + pify(alice.db.publish)({ type: 'post', text: 'hello' }), + pify(bob.db.publish)({ type: 'post', text: 'hello' }), + ]) + + const aliceBBMsg = getBBMsg(alice.config.keys) + const bobBBMsg = getBBMsg(bob.config.keys) + + aliceMFId = aliceBBMsg.author + bobMFId = bobBBMsg.author + + // self replicate + alice.ebt.request(aliceMFId, true) + bob.ebt.request(bobMFId, true) + + await Promise.all([pify(alice.add)(aliceBBMsg), pify(bob.add)(bobBBMsg)]) + + alice.ebt.request(bob.id, true) + alice.ebt.request(bobMFId, true) + + bob.ebt.request(alice.id, true) + bob.ebt.request(aliceMFId, true) + + await pify(bob.connect)(alice.getAddress()) + + await sleep(REPLICATION_TIMEOUT) + t.pass('wait for replication to complete') + + const expectedClassicClock = { + [alice.id]: 1, + [bob.id]: 1, + } + const expectedBBClock = { + [aliceMFId]: 1, + [bobMFId]: 1, + } + + const clockAlice = await pify(alice.ebt.clock)({ format: 'classic' }) + t.deepEqual(clockAlice, expectedClassicClock, 'alice correct classic clock') + + const bbClockAlice = await pify(alice.ebt.clock)({ format: 'bendybutt-v1' }) + t.deepEqual(bbClockAlice, expectedBBClock, 'alice correct bb clock') + + const clockBob = await pify(bob.ebt.clock)({ format: 'classic' }) + t.deepEqual(clockBob, expectedClassicClock, 'bob correct classic clock') + + const bbClockBob = await pify(bob.ebt.clock)({ format: 'bendybutt-v1' }) + t.deepEqual(bbClockBob, expectedBBClock, 'bob correct bb clock') + + await Promise.all([pify(alice.close)(true), pify(bob.close)(true)]) + t.end() +}) + +tape('multiple formats restart', async (t) => { + alice = createSSBServer().call(null, { + path: aliceDir, + timeout: CONNECTION_TIMEOUT, + keys: u.keysFor('alice'), + }) + + bob = createSSBServer().call(null, { + path: bobDir, + timeout: CONNECTION_TIMEOUT, + keys: u.keysFor('bob'), + }) + + alice.ebt.registerFormat(bendyButtMethods) + bob.ebt.registerFormat(bendyButtMethods) + + // self replicate + alice.ebt.request(alice.id, true) + alice.ebt.request(aliceMFId, true) + bob.ebt.request(bob.id, true) + bob.ebt.request(bobMFId, true) + + const expectedClassicClock = { + [alice.id]: 1, + [bob.id]: 1, + } + const expectedBBClock = { + [aliceMFId]: 1, + [bobMFId]: 1, + } + + const clockAlice = await pify(alice.ebt.clock)({ format: 'classic' }) + t.deepEqual(clockAlice, expectedClassicClock, 'alice correct classic clock') + + const bbClockAlice = await pify(alice.ebt.clock)({ format: 'bendybutt-v1' }) + t.deepEqual(bbClockAlice, expectedBBClock, 'alice correct bb clock') + + const clockBob = await pify(bob.ebt.clock)({ format: 'classic' }) + t.deepEqual(clockBob, expectedClassicClock, 'bob correct classic clock') + + const bbClockBob = await pify(bob.ebt.clock)({ format: 'bendybutt-v1' }) + t.deepEqual(bbClockBob, expectedBBClock, 'bob correct bb clock') + + await Promise.all([pify(alice.close)(true), pify(bob.close)(true)]) + t.end() +}) + +const carolDir = getFreshDir('carol') + +tape('index format', async (t) => { + const carol = createSSBServer().call(null, { + path: carolDir, + timeout: CONNECTION_TIMEOUT, + keys: u.keysFor('carol'), + }) + + const daveDir = getFreshDir('dave') + const dave = createSSBServer().call(null, { + path: daveDir, + timeout: CONNECTION_TIMEOUT, + keys: u.keysFor('dave'), + }) + + const indexedMethods = require('../formats/indexed.js') + + carol.ebt.registerFormat(indexedMethods) + carol.ebt.registerFormat(bendyButtMethods) + dave.ebt.registerFormat(indexedMethods) + dave.ebt.registerFormat(bendyButtMethods) + + const carolIndexId = ( + await pify(carol.indexFeedWriter.start)({ + author: carol.id, + type: 'dog', + private: false, + }) + ).subfeed + const daveIndexId = ( + await pify(dave.indexFeedWriter.start)({ + author: dave.id, + type: 'dog', + private: false, + }) + ).subfeed + + // publish some messages + await Promise.all([ + pify(carol.db.publish)({ type: 'post', text: 'hello 2' }), + pify(carol.db.publish)({ type: 'dog', name: 'Buff' }), + pify(dave.db.publish)({ type: 'post', text: 'hello 2' }), + pify(dave.db.publish)({ type: 'dog', name: 'Biff' }), + ]) + + await sleep(2 * REPLICATION_TIMEOUT) + t.pass('wait for index to complete') + + // get meta feed id to replicate + const carolMetaMessages = await carol.db.query( + where(type('metafeed/announce')), + toPromise() + ) + + // get meta index feed + const carolMetaIndexMessages = await carol.db.query( + where(type('metafeed/add/derived')), + toPromise() + ) + + // get meta feed id to replicate + const daveMetaMessages = await dave.db.query( + where(type('metafeed/announce')), + toPromise() + ) + + // get meta index feed + const daveMetaIndexMessages = await dave.db.query( + where(type('metafeed/add/derived')), + toPromise() + ) + + const carolMetaId = carolMetaMessages[0].value.content.metafeed + const carolMetaIndexId = carolMetaIndexMessages[0].value.content.subfeed + + const daveMetaId = daveMetaMessages[0].value.content.metafeed + const daveMetaIndexId = daveMetaIndexMessages[0].value.content.subfeed + + // self replicate + carol.ebt.request(carol.id, true) + carol.ebt.request(carolMetaId, true) + carol.ebt.request(carolMetaIndexId, true) + carol.ebt.request(carolIndexId, true, 'indexed') + + dave.ebt.request(dave.id, true) + dave.ebt.request(daveMetaId, true) + dave.ebt.request(daveMetaIndexId, true) + dave.ebt.request(daveIndexId, true, 'indexed') + + // replication + carol.ebt.request(daveMetaId, true) + carol.ebt.request(daveMetaIndexId, true) + dave.ebt.request(carolMetaId, true) + dave.ebt.request(carolMetaIndexId, true) + + await pify(dave.connect)(carol.getAddress()) + + await sleep(2 * REPLICATION_TIMEOUT) + t.pass('wait for replication to complete') + + // debugging + const carolIndexMessages = await carol.db.query( + where(author(daveMetaIndexId)), + toPromise() + ) + t.equal(carolIndexMessages.length, 1, 'carol has dave meta index') + + const daveIndexMessages = await dave.db.query( + where(author(carolMetaIndexId)), + toPromise() + ) + t.equal(daveIndexMessages.length, 1, 'dave has carol meta index') + + // now that we have meta feeds from the other peer we can replicate + // index feeds + + carol.ebt.request(daveIndexId, true, 'indexed') + dave.ebt.request(carolIndexId, true, 'indexed') + + await sleep(2 * REPLICATION_TIMEOUT) + t.pass('wait for replication to complete') + + // we should only get the dog message and not second post + const carolDaveMessages = await carol.db.query( + where(author(dave.id)), + toPromise() + ) + t.equal(carolDaveMessages.length, 1, 'carol got dog message from dave') + + const daveCarolMessages = await dave.db.query( + where(author(carol.id)), + toPromise() + ) + t.equal(daveCarolMessages.length, 1, 'dave got dog message from carol') + + const expectedIndexClock = { + [carolIndexId]: 1, + [daveIndexId]: 1, + } + + const indexClockCarol = await pify(carol.ebt.clock)({ format: 'indexed' }) + t.deepEqual(indexClockCarol, expectedIndexClock, 'carol correct index clock') + + const indexClockDave = await pify(dave.ebt.clock)({ format: 'indexed' }) + t.deepEqual(indexClockDave, expectedIndexClock, 'dave correct index clock') + + await pify(carol.db.publish)({ type: 'dog', text: 'woof woof' }) + + await sleep(2 * REPLICATION_TIMEOUT) + t.pass('wait for replication to complete') + + const daveCarolMessages2 = await dave.db.query( + where(author(carol.id)), + toPromise() + ) + t.equal(daveCarolMessages2.length, 2, 'dave got 2 dog messages from carol') + + await Promise.all([pify(carol.close)(true), pify(dave.close)(true)]) + t.end() +}) + +tape('sliced replication', async (t) => { + alice = createSSBServer().call(null, { + path: aliceDir, + timeout: CONNECTION_TIMEOUT, + keys: u.keysFor('alice'), + }) + + const carol = createSSBServer().call(null, { + path: carolDir, + timeout: CONNECTION_TIMEOUT, + keys: u.keysFor('carol'), + }) + + await Promise.all([ + pify(alice.db.publish)({ type: 'post', text: 'hello2' }), + pify(alice.db.publish)({ type: 'post', text: 'hello3' }), + ]) + + // carol wants to slice replicate some things, so she overwrites + // classic for this purpose + + const sliced = [alice.id] + + const slicedMethods = { + ...require('../formats/classic'), + appendMsg(sbot, msgVal, cb) { + let append = sbot.add + if (sliced.includes(msgVal.author)) { + append = sbot.db.addOOO + } + + append(msgVal, (err, msg) => { + if (err) return cb(err) + else cb(null, msg) + }) + }, + } + + carol.ebt.registerFormat(slicedMethods) + + const bobId = u.keysFor('bob').id + + // self replicate + alice.ebt.request(alice.id, true) + alice.ebt.request(bob.id, true) + carol.ebt.request(carol.id, true) + + await pify(carol.connect)(alice.getAddress()) + + const clockAlice = await pify(alice.ebt.clock)({ format: 'classic' }) + t.equal(clockAlice[alice.id], 3, 'alice correct index clock') + + carol.ebt.setClockForSlicedReplication(alice.id, clockAlice[alice.id] - 2) + carol.ebt.request(alice.id, true) + + carol.ebt.request(bobId, true) // in full + + await sleep(2 * REPLICATION_TIMEOUT) + t.pass('wait for replication to complete') + + const carolAMessages = await carol.db.query( + where(author(alice.id)), + toPromise() + ) + t.equal(carolAMessages.length, 2, 'latest 2 messages from alice') + + const carolBMessages = await carol.db.query( + where(author(bob.id)), + toPromise() + ) + t.equal(carolBMessages.length, 1, 'all messages from bob') + + await Promise.all([pify(alice.close)(true), pify(carol.close)(true)]) + t.end() +}) diff --git a/test/index.js b/test/index.js index dfff013..7f498e6 100644 --- a/test/index.js +++ b/test/index.js @@ -7,7 +7,7 @@ const SecretStack = require('secret-stack') const u = require('./misc/util') const createSbot = SecretStack({ - caps: { shs: crypto.randomBytes(32).toString('base64') } + caps: { shs: crypto.randomBytes(32).toString('base64') }, }) .use(require('ssb-db')) .use(require('../')) // EBT @@ -18,25 +18,25 @@ const REPLICATION_TIMEOUT = 2 * CONNECTION_TIMEOUT const alice = createSbot({ temp: 'random-animals_alice', timeout: CONNECTION_TIMEOUT, - keys: ssbKeys.generate() + keys: ssbKeys.generate(), }) const bob = createSbot({ temp: 'random-animals_bob', timeout: CONNECTION_TIMEOUT, - keys: ssbKeys.generate() + keys: ssbKeys.generate(), }) const charles = createSbot({ temp: 'random-animals_charles', timeout: CONNECTION_TIMEOUT, - keys: ssbKeys.generate() + keys: ssbKeys.generate(), }) const names = { [alice.id]: 'alice', [bob.id]: 'bob', - [charles.id]: 'charles' + [charles.id]: 'charles', } tape('three peers replicate everything between each other', async (t) => { @@ -59,10 +59,10 @@ tape('three peers replicate everything between each other', async (t) => { const recv = { alice: new Set(), bob: new Set(), - charles: new Set() + charles: new Set(), } - function consistent (name) { + function consistent(name) { if (!name) throw new Error('name must be provided') return function (msg) { u.log(name, 'received', msg.value.content, 'by', names[msg.value.author]) @@ -78,14 +78,14 @@ tape('three peers replicate everything between each other', async (t) => { await Promise.all([ pify(alice.publish)({ type: 'post', text: 'hello world' }), pify(bob.publish)({ type: 'post', text: 'hello world' }), - pify(charles.publish)({ type: 'post', text: 'hello world' }) + pify(charles.publish)({ type: 'post', text: 'hello world' }), ]) t.pass('all peers have posted "hello world"') await Promise.all([ pify(alice.connect)(bob.getAddress()), pify(alice.connect)(charles.getAddress()), - pify(charles.connect)(bob.getAddress()) + pify(charles.connect)(bob.getAddress()), ]) t.pass('the three peers are connected to each other as a triangle') @@ -110,7 +110,7 @@ tape('three peers replicate everything between each other', async (t) => { await Promise.all([ pify(alice.close)(true), pify(bob.close)(true), - pify(charles.close)(true) + pify(charles.close)(true), ]) t.end() }) diff --git a/test/legacy.js b/test/legacy.js index d3f05aa..d4ae43a 100644 --- a/test/legacy.js +++ b/test/legacy.js @@ -6,7 +6,7 @@ const sleep = require('util').promisify(setTimeout) const SecretStack = require('secret-stack') const createSbot = SecretStack({ - caps: { shs: crypto.randomBytes(32).toString('base64') } + caps: { shs: crypto.randomBytes(32).toString('base64') }, }) .use(require('ssb-db')) .use(require('../')) // EBT @@ -18,7 +18,7 @@ const bobKeys = ssbKeys.generate() const alice = createSbot({ temp: 'random-animals', timeout: CONNECTION_TIMEOUT, - keys: ssbKeys.generate() + keys: ssbKeys.generate(), }) tape('legacy (version 1) is unsupported', async (t) => { diff --git a/test/race.js b/test/race.js index 0d4e2ec..5efaf29 100644 --- a/test/race.js +++ b/test/race.js @@ -14,13 +14,12 @@ function delayedVectorClock(sbot, config) { } const createSbot = require('secret-stack')({ - caps: { shs: crypto.randomBytes(32).toString('base64') } + caps: { shs: crypto.randomBytes(32).toString('base64') }, }) .use(require('ssb-db')) .use(delayedVectorClock) .use(require('../')) - const CONNECTION_TIMEOUT = 500 // ms const REPLICATION_TIMEOUT = 2 * CONNECTION_TIMEOUT @@ -32,14 +31,14 @@ tape('we wait for vectorclock being available before doing ebt', async (t) => { let alice = createSbot({ path: '/tmp/alice', timeout: CONNECTION_TIMEOUT, - keys: aliceKeys + keys: aliceKeys, }) const bobKeys = ssbKeys.generate() let bob = createSbot({ path: '/tmp/bob', timeout: CONNECTION_TIMEOUT, - keys: bobKeys + keys: bobKeys, }) await Promise.all([ @@ -53,23 +52,20 @@ tape('we wait for vectorclock being available before doing ebt', async (t) => { t.pass('restarting') - await Promise.all([ - pify(alice.close)(true), - pify(bob.close)(true) - ]) + await Promise.all([pify(alice.close)(true), pify(bob.close)(true)]) alice = createSbot({ path: '/tmp/alice', timeout: CONNECTION_TIMEOUT, - keys: aliceKeys + keys: aliceKeys, }) bob = createSbot({ path: '/tmp/bob', timeout: CONNECTION_TIMEOUT, - keys: bobKeys + keys: bobKeys, }) - + // self replicate alice.ebt.request(alice.id, true) bob.ebt.request(bob.id, true) @@ -87,12 +83,17 @@ tape('we wait for vectorclock being available before doing ebt', async (t) => { u.log('A', u.countClock(clockAlice), 'B', u.countClock(clockBob)) - t.deepEqual(u.countClock(clockAlice), { total: 2, sum: 2 }, 'alice has both feeds') - t.deepEqual(u.countClock(clockBob), { total: 1, sum: 1 }, 'bob only has own feed') - - await Promise.all([ - pify(alice.close)(true), - pify(bob.close)(true) - ]) + t.deepEqual( + u.countClock(clockAlice), + { total: 2, sum: 2 }, + 'alice has both feeds' + ) + t.deepEqual( + u.countClock(clockBob), + { total: 1, sum: 1 }, + 'bob only has own feed' + ) + + await Promise.all([pify(alice.close)(true), pify(bob.close)(true)]) t.end() }) diff --git a/test/realtime.js b/test/realtime.js index afaa60c..6b09584 100644 --- a/test/realtime.js +++ b/test/realtime.js @@ -7,12 +7,12 @@ const pify = require('promisify-4loc') const sleep = require('util').promisify(setTimeout) const createSsbServer = SecretStack({ - caps: { shs: crypto.randomBytes(32).toString('base64') } + caps: { shs: crypto.randomBytes(32).toString('base64') }, }) .use(require('ssb-db')) .use(require('..')) -function createHistoryStream (sbot, opts) { +function createHistoryStream(sbot, opts) { return pull( sbot.createLogStream({ keys: false, live: opts.live }), pull.filter((msg) => msg.author === opts.id) @@ -26,13 +26,13 @@ tape('replicate between 2 peers', async (t) => { const alice = createSsbServer({ temp: 'test-alice', timeout: CONNECTION_TIMEOUT, - keys: ssbKeys.generate() + keys: ssbKeys.generate(), }) const bob = createSsbServer({ temp: 'test-bob', timeout: CONNECTION_TIMEOUT, - keys: ssbKeys.generate() + keys: ssbKeys.generate(), }) // Self replicate @@ -78,9 +78,6 @@ tape('replicate between 2 peers', async (t) => { t.equal(coldMsgs.length, 10) t.deepEqual(hotMsgs, coldMsgs) - await Promise.all([ - pify(alice.close)(true), - pify(bob.close)(true) - ]) + await Promise.all([pify(alice.close)(true), pify(bob.close)(true)]) t.end() }) diff --git a/test/resync.js b/test/resync.js index 18be4f4..8e86e50 100644 --- a/test/resync.js +++ b/test/resync.js @@ -7,7 +7,7 @@ const sleep = require('util').promisify(setTimeout) const u = require('./misc/util') const createSbot = require('secret-stack')({ - caps: { shs: crypto.randomBytes(32).toString('base64') } + caps: { shs: crypto.randomBytes(32).toString('base64') }, }) .use(require('ssb-db')) .use(require('../')) @@ -19,13 +19,13 @@ tape('peer can recover and resync its content from a friend', async (t) => { const alice = createSbot({ temp: 'alice', timeout: CONNECTION_TIMEOUT, - keys: ssbKeys.generate() + keys: ssbKeys.generate(), }) const bob = createSbot({ temp: 'bob', timeout: CONNECTION_TIMEOUT, - keys: ssbKeys.generate() + keys: ssbKeys.generate(), }) t.ok(alice.getAddress(), 'alice has an address') @@ -44,7 +44,7 @@ tape('peer can recover and resync its content from a friend', async (t) => { // in this test, bob's feed is on alice, // because bob's database corrupted (but had key backup) peers.push(alice.createFeed(bob.keys)) - t.pass('alice has bob\'s content') + t.pass("alice has bob's content") await pify(gen.messages)( function (n) { @@ -52,14 +52,14 @@ tape('peer can recover and resync its content from a friend', async (t) => { return { type: 'contact', contact: u.randary(peers).id, - following: true + following: true, } } return { type: 'test', ts: Date.now(), random: Math.random(), - value: u.randbytes(u.randint(1024)).toString('base64') + value: u.randbytes(u.randint(1024)).toString('base64'), } }, peers, @@ -84,13 +84,19 @@ tape('peer can recover and resync its content from a friend', async (t) => { } } - u.log('A', u.countClock(clockAlice), 'B', u.countClock(clockBob), 'diff', diff, 'common', commonCount) + u.log( + 'A', + u.countClock(clockAlice), + 'B', + u.countClock(clockBob), + 'diff', + diff, + 'common', + commonCount + ) t.equals(diff, 0, 'no diff between alice and bob') t.equals(commonCount > 0, true, 'bob has some content') - await Promise.all([ - pify(alice.close)(true), - pify(bob.close)(true) - ]) + await Promise.all([pify(alice.close)(true), pify(bob.close)(true)]) t.end() }) diff --git a/test/server.js b/test/server.js index b9f026e..1839055 100644 --- a/test/server.js +++ b/test/server.js @@ -10,7 +10,7 @@ const sleep = require('util').promisify(setTimeout) // and get them to follow each other... const createSsbServer = SecretStack({ - caps: { shs: crypto.randomBytes(32).toString('base64') } + caps: { shs: crypto.randomBytes(32).toString('base64') }, }) .use(require('ssb-db')) .use(require('..')) @@ -25,19 +25,19 @@ tape('replicate between 3 peers', async (t) => { temp: 'server-alice', keys: ssbKeys.generate(), timeout: CONNECTION_TIMEOUT, - level: 'info' + level: 'info', }) const bob = createSsbServer({ temp: 'server-bob', keys: ssbKeys.generate(), timeout: CONNECTION_TIMEOUT, - level: 'info' + level: 'info', }) const carol = createSsbServer({ temp: 'server-carol', keys: ssbKeys.generate(), timeout: CONNECTION_TIMEOUT, - level: 'info' + level: 'info', }) // Wait for all bots to be ready @@ -52,7 +52,7 @@ tape('replicate between 3 peers', async (t) => { pify(bob.publish)({ type: 'post', text: 'world' }), pify(carol.publish)({ type: 'post', text: 'hello' }), - pify(carol.publish)({ type: 'post', text: 'world' }) + pify(carol.publish)({ type: 'post', text: 'world' }), ]) // Self replicate @@ -81,13 +81,13 @@ tape('replicate between 3 peers', async (t) => { const [connectionBA, connectionBC, connectionCA] = await Promise.all([ pify(bob.connect)(alice.getAddress()), pify(bob.connect)(carol.getAddress()), - pify(carol.connect)(alice.getAddress()) + pify(carol.connect)(alice.getAddress()), ]) const expectedClock = { [alice.id]: 2, [bob.id]: 2, - [carol.id]: 2 + [carol.id]: 2, } await sleep(REPLICATION_TIMEOUT) @@ -95,23 +95,23 @@ tape('replicate between 3 peers', async (t) => { const [clockAlice, clockBob, clockCarol] = await Promise.all([ pify(alice.getVectorClock)(), pify(bob.getVectorClock)(), - pify(carol.getVectorClock)() + pify(carol.getVectorClock)(), ]) - t.deepEqual(clockAlice, expectedClock, 'alice\'s clock is correct') - t.deepEqual(clockBob, expectedClock, 'bob\'s clock is correct') - t.deepEqual(clockCarol, expectedClock, 'carol\'s clock is correct') + t.deepEqual(clockAlice, expectedClock, "alice's clock is correct") + t.deepEqual(clockBob, expectedClock, "bob's clock is correct") + t.deepEqual(clockCarol, expectedClock, "carol's clock is correct") await Promise.all([ pify(connectionBA.close)(true), pify(connectionBC.close)(true), - pify(connectionCA.close)(true) + pify(connectionCA.close)(true), ]) await Promise.all([ pify(alice.close)(true), pify(bob.close)(true), - pify(carol.close)(true) + pify(carol.close)(true), ]) t.end() diff --git a/test/unit.js b/test/unit.js index 6985c33..d0a07e4 100644 --- a/test/unit.js +++ b/test/unit.js @@ -5,7 +5,7 @@ const pify = require('promisify-4loc') const u = require('./misc/util') const createSsbServer = SecretStack({ - caps: { shs: crypto.randomBytes(32).toString('base64') } + caps: { shs: crypto.randomBytes(32).toString('base64') }, }) .use(require('ssb-db')) .use(require('../')) @@ -16,13 +16,13 @@ const REPLICATION_TIMEOUT = 2 * CONNECTION_TIMEOUT const alice = createSsbServer({ temp: 'test-block-alice', timeout: CONNECTION_TIMEOUT, - keys: u.keysFor('alice') + keys: u.keysFor('alice'), }) const bob = createSsbServer({ temp: 'test-block-bob', timeout: CONNECTION_TIMEOUT, - keys: u.keysFor('bob') + keys: u.keysFor('bob'), }) tape('alice replicates bob', async (t) => { @@ -30,7 +30,7 @@ tape('alice replicates bob', async (t) => { pify(alice.publish)({ type: 'post', text: 'hello' }), pify(alice.publish)({ type: 'post', text: 'world' }), pify(bob.publish)({ type: 'post', text: 'hello' }), - pify(bob.publish)({ type: 'post', text: 'world' }) + pify(bob.publish)({ type: 'post', text: 'world' }), ]) // Self replicate @@ -39,16 +39,16 @@ tape('alice replicates bob', async (t) => { alice.ebt.request(bob.id, true) alice.ebt.block(alice.id, bob.id, false) - t.pass('alice wants bob\'s data') + t.pass("alice wants bob's data") bob.ebt.request(alice.id, true) bob.ebt.block(bob.id, alice.id, false) - t.pass('bob wants alice\'s data') + t.pass("bob wants alice's data") const [rpcBobToAlice, msgAtBob, msgAtAlice] = await Promise.all([ pify(bob.connect)(alice.getAddress()), u.readOnceFromDB(bob, REPLICATION_TIMEOUT), - u.readOnceFromDB(alice, REPLICATION_TIMEOUT) + u.readOnceFromDB(alice, REPLICATION_TIMEOUT), ]) t.equals(msgAtAlice.value.author, bob.id, 'alice has a msg from bob') @@ -57,7 +57,7 @@ tape('alice replicates bob', async (t) => { await pify(rpcBobToAlice.close)(true) }) -tape('ssb.progress', t => { +tape('ssb.progress', (t) => { const p = alice.progress() t.ok(p, 'progress') t.ok(p.ebt, 'progress.ebt') @@ -67,7 +67,7 @@ tape('ssb.progress', t => { t.end() }) -tape('ssb.ebt.peerStatus', t => { +tape('ssb.ebt.peerStatus', (t) => { const s = alice.ebt.peerStatus(bob.id) t.ok(s, 'response is an object') t.equals(s.id, bob.id, 'response.id is correct') @@ -75,14 +75,14 @@ tape('ssb.ebt.peerStatus', t => { t.end() }) -tape('silly ssb.ebt.request', t => { +tape('silly ssb.ebt.request', (t) => { t.doesNotThrow(() => { alice.ebt.request('not a feed id', true) }) t.end() }) -tape('silly ssb.ebt.block', t => { +tape('silly ssb.ebt.block', (t) => { t.doesNotThrow(() => { alice.ebt.block(bob.id, 'not a feed id', true) }) @@ -93,9 +93,6 @@ tape('silly ssb.ebt.block', t => { }) tape('teardown', async (t) => { - await Promise.all([ - pify(alice.close)(true), - pify(bob.close)(true) - ]) + await Promise.all([pify(alice.close)(true), pify(bob.close)(true)]) t.end() })