From ff9e85c042cbbad8ccf5c63140ad486bc09d4c03 Mon Sep 17 00:00:00 2001 From: Vasco Santos Date: Fri, 15 May 2020 19:39:13 +0200 Subject: [PATCH] feat: metadata book (#638) * feat: metadata book * chore: address review * chore: address review --- doc/API.md | 145 +++++++ src/peer-store/README.md | 11 +- src/peer-store/address-book.js | 1 - src/peer-store/index.js | 51 ++- src/peer-store/metadata-book.js | 161 ++++++++ src/peer-store/persistent/consts.js | 3 + src/peer-store/persistent/index.js | 68 ++++ src/peer-store/proto-book.js | 1 - test/peer-store/metadata-book.spec.js | 380 +++++++++++++++++++ test/peer-store/peer-store.spec.js | 53 +++ test/peer-store/persisted-peer-store.spec.js | 26 +- 11 files changed, 861 insertions(+), 39 deletions(-) create mode 100644 src/peer-store/metadata-book.js create mode 100644 test/peer-store/metadata-book.spec.js diff --git a/doc/API.md b/doc/API.md index b481f428d6..9c55bbfd5d 100644 --- a/doc/API.md +++ b/doc/API.md @@ -29,6 +29,11 @@ * [`peerStore.keyBook.delete`](#peerstorekeybookdelete) * [`peerStore.keyBook.get`](#peerstorekeybookget) * [`peerStore.keyBook.set`](#peerstorekeybookset) + * [`peerStore.metadataBook.delete`](#peerstoremetadatabookdelete) + * [`peerStore.metadataBook.deleteValue`](#peerstoremetadatabookdeletevalue) + * [`peerStore.metadataBook.get`](#peerstoremetadatabookget) + * [`peerStore.metadataBook.getValue`](#peerstoremetadatabookgetvalue) + * [`peerStore.metadataBook.set`](#peerstoremetadatabookset) * [`peerStore.protoBook.add`](#peerstoreprotobookadd) * [`peerStore.protoBook.delete`](#peerstoreprotobookdelete) * [`peerStore.protoBook.get`](#peerstoreprotobookget) @@ -939,6 +944,146 @@ const publicKey = peerId.pubKey peerStore.keyBook.set(peerId, publicKey) ``` +### peerStore.metadataBook.delete + +Delete the provided peer from the book. + +`peerStore.metadataBook.delete(peerId)` + +#### Parameters + +| Name | Type | Description | +|------|------|-------------| +| peerId | [`PeerId`][peer-id] | peerId to remove | + +#### Returns + +| Type | Description | +|------|-------------| +| `boolean` | true if found and removed | + +#### Example + +```js +peerStore.metadataBook.delete(peerId) +// false +peerStore.metadataBook.set(peerId, 'nickname', Buffer.from('homePeer')) +peerStore.metadataBook.delete(peerId) +// true +``` + +### peerStore.metadataBook.deleteValue + +Deletes the provided peer metadata key-value pair from the book. + +`peerStore.metadataBook.deleteValue(peerId, key)` + +#### Parameters + +| Name | Type | Description | +|------|------|-------------| +| peerId | [`PeerId`][peer-id] | peerId to remove | +| key | `string` | key of the metadata value to remove | + +#### Returns + +| Type | Description | +|------|-------------| +| `boolean` | true if found and removed | + +#### Example + +```js +peerStore.metadataBook.deleteValue(peerId, 'location') +// false +peerStore.metadataBook.set(peerId, 'location', Buffer.from('Berlin')) +peerStore.metadataBook.deleteValue(peerId, 'location') +// true +``` + +### peerStore.metadataBook.get + +Get the known metadata of a provided peer. + +`peerStore.metadataBook.get(peerId)` + +#### Parameters + +| Name | Type | Description | +|------|------|-------------| +| peerId | [`PeerId`][peer-id] | peerId to get | + +#### Returns + +| Type | Description | +|------|-------------| +| `Map` | Peer Metadata | + +#### Example + +```js +peerStore.metadataBook.get(peerId) +// undefined +peerStore.metadataBook.set(peerId, 'location', Buffer.from('Berlin')) +peerStore.metadataBook.get(peerId) +// Metadata Map +``` + +### peerStore.metadataBook.getValue + +Get specific metadata of a provided peer. + +`peerStore.metadataBook.getValue(peerId)` + +#### Parameters + +| Name | Type | Description | +|------|------|-------------| +| peerId | [`PeerId`][peer-id] | peerId to get | +| key | `string` | key of the metadata value to get | + +#### Returns + +| Type | Description | +|------|-------------| +| `Map` | Peer Metadata | + +#### Example + +```js +peerStore.metadataBook.getValue(peerId, 'location') +// undefined +peerStore.metadataBook.set(peerId, 'location', Buffer.from('Berlin')) +peerStore.metadataBook.getValue(peerId, 'location') +// Metadata Map +``` + +### peerStore.metadataBook.set + +Set known metadata of a given `peerId`. + +`peerStore.metadataBook.set(peerId, key, value)` + +#### Parameters + +| Name | Type | Description | +|------|------|-------------| +| peerId | [`PeerId`][peer-id] | peerId to set | +| key | `string` | key of the metadata value to store | +| value | `Buffer` | metadata value to store | + +#### Returns + +| Type | Description | +|------|-------------| +| `MetadataBook` | Returns the Metadata Book component | + +#### Example + +```js +peerStore.metadataBook.set(peerId, 'location', Buffer.from('Berlin')) +``` + ### peerStore.protoBook.delete Delete the provided peer from the book. diff --git a/src/peer-store/README.md b/src/peer-store/README.md index 8b23831cba..9c715c9968 100644 --- a/src/peer-store/README.md +++ b/src/peer-store/README.md @@ -75,7 +75,11 @@ A `peerId.toB58String()` identifier mapping to a `Set` of protocol identifier st #### Metadata Book -**Not Yet Implemented** +The `metadataBook` keeps track of the known metadata of a peer. Its metadata is stored in a key value fashion, where a key identifier (`string`) represents a metadata value (`Buffer`). + +`Map>` + +A `peerId.toB58String()` identifier mapping to the peer metadata Map. ### API @@ -85,6 +89,7 @@ Access to its underlying books: - `peerStore.addressBook.*` - `peerStore.keyBook.*` +- `peerStore.metadataBook.*` - `peerStore.protoBook.*` ### Events @@ -92,6 +97,8 @@ Access to its underlying books: - `peer` - emitted when a new peer is added. - `change:multiaadrs` - emitted when a known peer has a different set of multiaddrs. - `change:protocols` - emitted when a known peer supports a different set of protocols. +- `change:pubkey` - emitted when a peer's public key is known. +- `change:metadata` - emitted when known metadata of a peer changes. ## Data Persistence @@ -123,8 +130,6 @@ All public keys are stored under the following pattern: **MetadataBook** -_NOT_YET_IMPLEMENTED_ - Metadata is stored under the following key pattern: `/peers/metadata//` diff --git a/src/peer-store/address-book.js b/src/peer-store/address-book.js index 54b23013c4..7841a320e2 100644 --- a/src/peer-store/address-book.js +++ b/src/peer-store/address-book.js @@ -97,7 +97,6 @@ class AddressBook extends Book { /** * Add known addresses of a provided peer. * If the peer is not known, it is set with the given addresses. - * @override * @param {PeerId} peerId * @param {Array} multiaddrs * @returns {AddressBook} diff --git a/src/peer-store/index.js b/src/peer-store/index.js index d19b2de8c3..2c659d825b 100644 --- a/src/peer-store/index.js +++ b/src/peer-store/index.js @@ -10,6 +10,7 @@ const PeerId = require('peer-id') const AddressBook = require('./address-book') const KeyBook = require('./key-book') +const MetadataBook = require('./metadata-book') const ProtoBook = require('./proto-book') const { @@ -21,6 +22,8 @@ const { * @fires PeerStore#peer Emitted when a new peer is added. * @fires PeerStore#change:protocols Emitted when a known peer supports a different set of protocols. * @fires PeerStore#change:multiaddrs Emitted when a known peer has a different set of multiaddrs. + * @fires PeerStore#change:pubkey Emitted emitted when a peer's public key is known. + * @fires PeerStore#change:metadata Emitted when the known metadata of a peer change. */ class PeerStore extends EventEmitter { /** @@ -47,6 +50,11 @@ class PeerStore extends EventEmitter { */ this.keyBook = new KeyBook(this) + /** + * MetadataBook containing a map of peerIdStr to their metadata Map. + */ + this.metadataBook = new MetadataBook(this) + /** * ProtoBook containing a map of peerIdStr to supported protocols. */ @@ -68,31 +76,17 @@ class PeerStore extends EventEmitter { * @returns {Map} */ get peers () { - const peersData = new Map() + const storedPeers = new Set([ + ...this.addressBook.data.keys(), + ...this.keyBook.data.keys(), + ...this.protoBook.data.keys(), + ...this.metadataBook.data.keys() + ]) - // AddressBook - for (const [idStr, addresses] of this.addressBook.data.entries()) { - const id = this.keyBook.data.get(idStr) || PeerId.createFromCID(idStr) - peersData.set(idStr, { - id, - addresses, - protocols: this.protoBook.get(id) || [] - }) - } - - // ProtoBook - for (const [idStr, protocols] of this.protoBook.data.entries()) { - const pData = peersData.get(idStr) - const id = this.keyBook.data.get(idStr) || PeerId.createFromCID(idStr) - - if (!pData) { - peersData.set(idStr, { - id, - addresses: [], - protocols: Array.from(protocols) - }) - } - } + const peersData = new Map() + storedPeers.forEach((idStr) => { + peersData.set(idStr, this.get(PeerId.createFromCID(idStr))) + }) return peersData } @@ -106,8 +100,9 @@ class PeerStore extends EventEmitter { const addressesDeleted = this.addressBook.delete(peerId) const keyDeleted = this.keyBook.delete(peerId) const protocolsDeleted = this.protoBook.delete(peerId) + const metadataDeleted = this.metadataBook.delete(peerId) - return addressesDeleted || keyDeleted || protocolsDeleted + return addressesDeleted || keyDeleted || protocolsDeleted || metadataDeleted } /** @@ -122,16 +117,18 @@ class PeerStore extends EventEmitter { const id = this.keyBook.data.get(peerId.toB58String()) const addresses = this.addressBook.get(peerId) + const metadata = this.metadataBook.get(peerId) const protocols = this.protoBook.get(peerId) - if (!addresses && !protocols) { + if (!id && !addresses && !metadata && !protocols) { return undefined } return { id: id || peerId, addresses: addresses || [], - protocols: protocols || [] + protocols: protocols || [], + metadata: metadata } } } diff --git a/src/peer-store/metadata-book.js b/src/peer-store/metadata-book.js new file mode 100644 index 0000000000..148ded413a --- /dev/null +++ b/src/peer-store/metadata-book.js @@ -0,0 +1,161 @@ +'use strict' + +const errcode = require('err-code') +const debug = require('debug') +const log = debug('libp2p:peer-store:proto-book') +log.error = debug('libp2p:peer-store:proto-book:error') + +const { Buffer } = require('buffer') + +const PeerId = require('peer-id') + +const Book = require('./book') + +const { + codes: { ERR_INVALID_PARAMETERS } +} = require('../errors') + +/** + * The MetadataBook is responsible for keeping the known supported + * protocols of a peer. + * @fires MetadataBook#change:metadata + */ +class MetadataBook extends Book { + /** + * @constructor + * @param {PeerStore} peerStore + */ + constructor (peerStore) { + /** + * PeerStore Event emitter, used by the MetadataBook to emit: + * "change:metadata" - emitted when the known metadata of a peer change. + */ + super({ + peerStore, + eventName: 'change:metadata', + eventProperty: 'metadata' + }) + + /** + * Map known peers to their known protocols. + * @type {Map>} + */ + this.data = new Map() + } + + /** + * Set metadata key and value of a provided peer. + * @override + * @param {PeerId} peerId + * @param {string} key metadata key + * @param {Buffer} value metadata value + * @returns {ProtoBook} + */ + set (peerId, key, value) { + if (!PeerId.isPeerId(peerId)) { + log.error('peerId must be an instance of peer-id to store data') + throw errcode(new Error('peerId must be an instance of peer-id'), ERR_INVALID_PARAMETERS) + } + + if (typeof key !== 'string' || !Buffer.isBuffer(value)) { + log.error('valid key and value must be provided to store data') + throw errcode(new Error('valid key and value must be provided'), ERR_INVALID_PARAMETERS) + } + + this._setValue(peerId, key, value) + + return this + } + + /** + * Set data into the datastructure + * @override + */ + _setValue (peerId, key, value, { emit = true } = {}) { + const id = peerId.toB58String() + const rec = this.data.get(id) || new Map() + const recMap = rec.get(key) + + // Already exists and is equal + if (recMap && value.equals(recMap)) { + log(`the metadata provided to store is equal to the already stored for ${id} on ${key}`) + return + } + + rec.set(key, value) + this.data.set(id, rec) + + emit && this._emit(peerId, key) + } + + /** + * Get the known data of a provided peer. + * @param {PeerId} peerId + * @returns {Map} + */ + get (peerId) { + if (!PeerId.isPeerId(peerId)) { + throw errcode(new Error('peerId must be an instance of peer-id'), ERR_INVALID_PARAMETERS) + } + + return this.data.get(peerId.toB58String()) + } + + /** + * Get specific metadata value, if it exists + * @param {PeerId} peerId + * @param {string} key + * @returns {Buffer} + */ + getValue (peerId, key) { + if (!PeerId.isPeerId(peerId)) { + throw errcode(new Error('peerId must be an instance of peer-id'), ERR_INVALID_PARAMETERS) + } + + const rec = this.data.get(peerId.toB58String()) + return rec && rec.get(key) + } + + /** + * Deletes the provided peer from the book. + * @param {PeerId} peerId + * @returns {boolean} + */ + delete (peerId) { + if (!PeerId.isPeerId(peerId)) { + throw errcode(new Error('peerId must be an instance of peer-id'), ERR_INVALID_PARAMETERS) + } + + if (!this.data.delete(peerId.toB58String())) { + return false + } + + this._emit(peerId) + + return true + } + + /** + * Deletes the provided peer metadata key from the book. + * @param {PeerId} peerId + * @param {string} key + * @returns {boolean} + */ + deleteValue (peerId, key) { + if (!PeerId.isPeerId(peerId)) { + throw errcode(new Error('peerId must be an instance of peer-id'), ERR_INVALID_PARAMETERS) + } + + const rec = this.data.get(peerId.toB58String()) + + if (!rec || !rec.delete(key)) { + return false + } + + this._emit(peerId, key) + + return true + } +} + +module.exports = MetadataBook diff --git a/src/peer-store/persistent/consts.js b/src/peer-store/persistent/consts.js index 9679cc5728..591c267e66 100644 --- a/src/peer-store/persistent/consts.js +++ b/src/peer-store/persistent/consts.js @@ -8,5 +8,8 @@ module.exports.NAMESPACE_ADDRESS = '/peers/addrs/' // /peers/keys/ module.exports.NAMESPACE_KEYS = '/peers/keys/' +// /peers/metadata// +module.exports.NAMESPACE_METADATA = '/peers/metadata/' + // /peers/addrs/ module.exports.NAMESPACE_PROTOCOL = '/peers/protos/' diff --git a/src/peer-store/persistent/index.js b/src/peer-store/persistent/index.js index 366e9bc48f..2bcd38db1c 100644 --- a/src/peer-store/persistent/index.js +++ b/src/peer-store/persistent/index.js @@ -14,6 +14,7 @@ const { NAMESPACE_ADDRESS, NAMESPACE_COMMON, NAMESPACE_KEYS, + NAMESPACE_METADATA, NAMESPACE_PROTOCOL } = require('./consts') @@ -43,6 +44,12 @@ class PersistentPeerStore extends PeerStore { */ this._dirtyPeers = new Set() + /** + * Peers metadata changed mapping peer identifers to metadata changed. + * @type {Map>} + */ + this._dirtyMetadata = new Map() + this.threshold = threshold this._addDirtyPeer = this._addDirtyPeer.bind(this) } @@ -58,6 +65,7 @@ class PersistentPeerStore extends PeerStore { this.on('change:protocols', this._addDirtyPeer) this.on('change:multiaddrs', this._addDirtyPeer) this.on('change:pubkey', this._addDirtyPeer) + this.on('change:metadata', this._addDirtyPeerMetadata) // Load data for await (const entry of this._datastore.query({ prefix: NAMESPACE_COMMON })) { @@ -92,6 +100,30 @@ class PersistentPeerStore extends PeerStore { } } + /** + * Add modified metadata peer to the set. + * @private + * @param {Object} params + * @param {PeerId} params.peerId + * @param {string} params.metadata + */ + _addDirtyPeerMetadata ({ peerId, metadata }) { + const peerIdstr = peerId.toB58String() + + log('add dirty metadata peer', peerIdstr) + this._dirtyPeers.add(peerIdstr) + + // Add dirty metadata key + const mData = this._dirtyMetadata.get(peerIdstr) || new Set() + mData.add(metadata) + this._dirtyMetadata.set(peerIdstr, mData) + + if (this._dirtyPeers.size >= this.threshold) { + // Commit current data + this._commitData() + } + } + /** * Add all the peers current data to a datastore batch and commit it. * @private @@ -120,6 +152,9 @@ class PersistentPeerStore extends PeerStore { // Key Book this._batchKeyBook(peerId, batch) + // Metadata Book + this._batchMetadataBook(peerId, batch) + // Proto Book this._batchProtoBook(peerId, batch) } @@ -184,6 +219,32 @@ class PersistentPeerStore extends PeerStore { } } + /** + * Add metadata book data of the peer to the batch. + * @private + * @param {PeerId} peerId + * @param {Object} batch + */ + _batchMetadataBook (peerId, batch) { + const b32key = peerId.toString() + const dirtyMetada = this._dirtyMetadata.get(peerId.toB58String()) || [] + + try { + dirtyMetada.forEach((dirtyKey) => { + const key = new Key(`${NAMESPACE_METADATA}${b32key}/${dirtyKey}`) + const dirtyValue = this.metadataBook.getValue(peerId, dirtyKey) + + if (dirtyValue) { + batch.put(key, dirtyValue) + } else { + batch.delete(key) + } + }) + } catch (err) { + log.error(err) + } + } + /** * Add proto book data of the peer to the batch. * @private @@ -244,6 +305,13 @@ class PersistentPeerStore extends PeerStore { decoded, { emit: false }) break + case 'metadata': + this.metadataBook._setValue( + peerId, + keyParts[4], + value, + { emit: false }) + break case 'protos': decoded = Protocols.decode(value) diff --git a/src/peer-store/proto-book.js b/src/peer-store/proto-book.js index 1e42d0a41a..edd906fbb3 100644 --- a/src/peer-store/proto-book.js +++ b/src/peer-store/proto-book.js @@ -83,7 +83,6 @@ class ProtoBook extends Book { /** * Adds known protocols of a provided peer. * If the peer was not known before, it will be added. - * @override * @param {PeerId} peerId * @param {Array} protocols * @returns {ProtoBook} diff --git a/test/peer-store/metadata-book.spec.js b/test/peer-store/metadata-book.spec.js new file mode 100644 index 0000000000..9d2ee740ed --- /dev/null +++ b/test/peer-store/metadata-book.spec.js @@ -0,0 +1,380 @@ +'use strict' +/* eslint-env mocha */ + +const chai = require('chai') +chai.use(require('dirty-chai')) +chai.use(require('chai-bytes')) +const { expect } = chai + +const pDefer = require('p-defer') +const PeerStore = require('../../src/peer-store') + +const peerUtils = require('../utils/creators/peer') +const { + codes: { ERR_INVALID_PARAMETERS } +} = require('../../src/errors') + +describe('metadataBook', () => { + let peerId + + before(async () => { + [peerId] = await peerUtils.createPeerId() + }) + + describe('metadataBook.set', () => { + let peerStore, mb + + beforeEach(() => { + peerStore = new PeerStore() + mb = peerStore.metadataBook + }) + + afterEach(() => { + peerStore.removeAllListeners() + }) + + it('throws invalid parameters error if invalid PeerId is provided', () => { + try { + mb.set('invalid peerId') + } catch (err) { + expect(err.code).to.equal(ERR_INVALID_PARAMETERS) + return + } + throw new Error('invalid peerId should throw error') + }) + + it('throws invalid parameters error if no key provided', () => { + try { + mb.set(peerId) + } catch (err) { + expect(err.code).to.equal(ERR_INVALID_PARAMETERS) + return + } + throw new Error('no key provided should throw error') + }) + + it('throws invalid parameters error if no value provided', () => { + try { + mb.set(peerId, 'location') + } catch (err) { + expect(err.code).to.equal(ERR_INVALID_PARAMETERS) + return + } + throw new Error('no value provided should throw error') + }) + + it('throws invalid parameters error if value is not a buffer', () => { + try { + mb.set(peerId, 'location', 'mars') + } catch (err) { + expect(err.code).to.equal(ERR_INVALID_PARAMETERS) + return + } + throw new Error('invalid value provided should throw error') + }) + + it('stores the content and emit change event', () => { + const defer = pDefer() + const metadataKey = 'location' + const metadataValue = Buffer.from('mars') + + peerStore.once('change:metadata', ({ peerId, metadata }) => { + expect(peerId).to.exist() + expect(metadata).to.equal(metadataKey) + defer.resolve() + }) + + mb.set(peerId, metadataKey, metadataValue) + + const value = mb.getValue(peerId, metadataKey) + expect(value).to.equalBytes(metadataValue) + + const peerMetadata = mb.get(peerId) + expect(peerMetadata).to.exist() + expect(peerMetadata.get(metadataKey)).to.equalBytes(metadataValue) + + return defer.promise + }) + + it('emits on set if not storing the exact same content', () => { + const defer = pDefer() + const metadataKey = 'location' + const metadataValue1 = Buffer.from('mars') + const metadataValue2 = Buffer.from('saturn') + + let changeCounter = 0 + peerStore.on('change:metadata', () => { + changeCounter++ + if (changeCounter > 1) { + defer.resolve() + } + }) + + // set 1 + mb.set(peerId, metadataKey, metadataValue1) + + // set 2 (same content) + mb.set(peerId, metadataKey, metadataValue2) + + const value = mb.getValue(peerId, metadataKey) + expect(value).to.equalBytes(metadataValue2) + + const peerMetadata = mb.get(peerId) + expect(peerMetadata).to.exist() + expect(peerMetadata.get(metadataKey)).to.equalBytes(metadataValue2) + + return defer.promise + }) + + it('does not emit on set if it is storing the exact same content', () => { + const defer = pDefer() + const metadataKey = 'location' + const metadataValue = Buffer.from('mars') + + let changeCounter = 0 + peerStore.on('change:metadata', () => { + changeCounter++ + if (changeCounter > 1) { + defer.reject() + } + }) + + // set 1 + mb.set(peerId, metadataKey, metadataValue) + + // set 2 (same content) + mb.set(peerId, metadataKey, metadataValue) + + // Wait 50ms for incorrect second event + setTimeout(() => { + defer.resolve() + }, 50) + + return defer.promise + }) + }) + + describe('metadataBook.get', () => { + let peerStore, mb + + beforeEach(() => { + peerStore = new PeerStore() + mb = peerStore.metadataBook + }) + + it('throws invalid parameters error if invalid PeerId is provided', () => { + try { + mb.get('invalid peerId') + } catch (err) { + expect(err.code).to.equal(ERR_INVALID_PARAMETERS) + return + } + throw new Error('invalid peerId should throw error') + }) + + it('returns undefined if no metadata is known for the provided peer', () => { + const metadata = mb.get(peerId) + + expect(metadata).to.not.exist() + }) + + it('returns the metadata stored', () => { + const metadataKey = 'location' + const metadataValue = Buffer.from('mars') + + mb.set(peerId, metadataKey, metadataValue) + + const peerMetadata = mb.get(peerId) + expect(peerMetadata).to.exist() + expect(peerMetadata.get(metadataKey)).to.equalBytes(metadataValue) + }) + }) + + describe('metadataBook.getValue', () => { + let peerStore, mb + + beforeEach(() => { + peerStore = new PeerStore() + mb = peerStore.metadataBook + }) + + it('throws invalid parameters error if invalid PeerId is provided', () => { + try { + mb.getValue('invalid peerId') + } catch (err) { + expect(err.code).to.equal(ERR_INVALID_PARAMETERS) + return + } + throw new Error('invalid peerId should throw error') + }) + + it('returns undefined if no metadata is known for the provided peer', () => { + const metadataKey = 'location' + const metadata = mb.getValue(peerId, metadataKey) + + expect(metadata).to.not.exist() + }) + + it('returns the metadata value stored for the given key', () => { + const metadataKey = 'location' + const metadataValue = Buffer.from('mars') + + mb.set(peerId, metadataKey, metadataValue) + + const value = mb.getValue(peerId, metadataKey) + expect(value).to.exist() + expect(value).to.equalBytes(metadataValue) + }) + + it('returns undefined if no metadata is known for the provided peer and key', () => { + const metadataKey = 'location' + const metadataBadKey = 'nickname' + const metadataValue = Buffer.from('mars') + + mb.set(peerId, metadataKey, metadataValue) + + const metadata = mb.getValue(peerId, metadataBadKey) + + expect(metadata).to.not.exist() + }) + }) + + describe('metadataBook.delete', () => { + let peerStore, mb + + beforeEach(() => { + peerStore = new PeerStore() + mb = peerStore.metadataBook + }) + + it('throwns invalid parameters error if invalid PeerId is provided', () => { + try { + mb.delete('invalid peerId') + } catch (err) { + expect(err.code).to.equal(ERR_INVALID_PARAMETERS) + return + } + throw new Error('invalid peerId should throw error') + }) + + it('returns false if no records exist for the peer and no event is emitted', () => { + const defer = pDefer() + + peerStore.on('change:metadata', () => { + defer.reject() + }) + + const deleted = mb.delete(peerId) + + expect(deleted).to.equal(false) + + // Wait 50ms for incorrect invalid event + setTimeout(() => { + defer.resolve() + }, 50) + + return defer.promise + }) + + it('returns true if the record exists and an event is emitted', () => { + const defer = pDefer() + const metadataKey = 'location' + const metadataValue = Buffer.from('mars') + + mb.set(peerId, metadataKey, metadataValue) + + // Listen after set + peerStore.on('change:metadata', () => { + defer.resolve() + }) + + const deleted = mb.delete(peerId) + + expect(deleted).to.equal(true) + + return defer.promise + }) + }) + + describe('metadataBook.deleteValue', () => { + let peerStore, mb + + beforeEach(() => { + peerStore = new PeerStore() + mb = peerStore.metadataBook + }) + + it('throws invalid parameters error if invalid PeerId is provided', () => { + try { + mb.deleteValue('invalid peerId') + } catch (err) { + expect(err.code).to.equal(ERR_INVALID_PARAMETERS) + return + } + throw new Error('invalid peerId should throw error') + }) + + it('returns false if no records exist for the peer and no event is emitted', () => { + const defer = pDefer() + const metadataKey = 'location' + + peerStore.on('change:metadata', () => { + defer.reject() + }) + + const deleted = mb.deleteValue(peerId, metadataKey) + + expect(deleted).to.equal(false) + + // Wait 50ms for incorrect invalid event + setTimeout(() => { + defer.resolve() + }, 50) + + return defer.promise + }) + + it('returns true if the record exists and an event is emitted', () => { + const defer = pDefer() + const metadataKey = 'location' + const metadataValue = Buffer.from('mars') + + mb.set(peerId, metadataKey, metadataValue) + + // Listen after set + peerStore.on('change:metadata', () => { + defer.resolve() + }) + + const deleted = mb.deleteValue(peerId, metadataKey) + + expect(deleted).to.equal(true) + + return defer.promise + }) + + it('returns false if there is a record for the peer but not the given metadata key', () => { + const defer = pDefer() + const metadataKey = 'location' + const metadataBadKey = 'nickname' + const metadataValue = Buffer.from('mars') + + mb.set(peerId, metadataKey, metadataValue) + + peerStore.on('change:metadata', () => { + defer.reject() + }) + + const deleted = mb.deleteValue(peerId, metadataBadKey) + + expect(deleted).to.equal(false) + + // Wait 50ms for incorrect invalid event + setTimeout(() => { + defer.resolve() + }, 50) + + return defer.promise + }) + }) +}) diff --git a/test/peer-store/peer-store.spec.js b/test/peer-store/peer-store.spec.js index 1884ea4dc6..16c74732ac 100644 --- a/test/peer-store/peer-store.spec.js +++ b/test/peer-store/peer-store.spec.js @@ -158,4 +158,57 @@ describe('peer-store', () => { expect(peerListenint4[1].id.toB58String()).to.eql(peerIds[3].toB58String()) }) }) + + describe('peerStore.peers', () => { + let peerStore + + beforeEach(() => { + peerStore = new PeerStore() + }) + + it('returns peers if only addresses are known', () => { + peerStore.addressBook.set(peerIds[0], [addr1]) + + const peers = peerStore.peers + expect(peers.size).to.equal(1) + + const peerData = peers.get(peerIds[0].toB58String()) + expect(peerData).to.exist() + expect(peerData.id).to.exist() + expect(peerData.addresses).to.have.lengthOf(1) + expect(peerData.protocols).to.have.lengthOf(0) + expect(peerData.metadata).to.not.exist() + }) + + it('returns peers if only protocols are known', () => { + peerStore.protoBook.set(peerIds[0], [proto1]) + + const peers = peerStore.peers + expect(peers.size).to.equal(1) + + const peerData = peers.get(peerIds[0].toB58String()) + expect(peerData).to.exist() + expect(peerData.id).to.exist() + expect(peerData.addresses).to.have.lengthOf(0) + expect(peerData.protocols).to.have.lengthOf(1) + expect(peerData.metadata).to.not.exist() + }) + + it('returns peers if only metadata is known', () => { + const metadataKey = 'location' + const metadataValue = Buffer.from('earth') + peerStore.metadataBook.set(peerIds[0], metadataKey, metadataValue) + + const peers = peerStore.peers + expect(peers.size).to.equal(1) + + const peerData = peers.get(peerIds[0].toB58String()) + expect(peerData).to.exist() + expect(peerData.id).to.exist() + expect(peerData.addresses).to.have.lengthOf(0) + expect(peerData.protocols).to.have.lengthOf(0) + expect(peerData.metadata).to.exist() + expect(peerData.metadata.get(metadataKey)).to.equalBytes(metadataValue) + }) + }) }) diff --git a/test/peer-store/persisted-peer-store.spec.js b/test/peer-store/persisted-peer-store.spec.js index a8b0fea1ba..d786f4fd3a 100644 --- a/test/peer-store/persisted-peer-store.spec.js +++ b/test/peer-store/persisted-peer-store.spec.js @@ -118,7 +118,10 @@ describe('Persisted PeerStore', () => { peerStore.protoBook.set(peers[0], protocols) peerStore.protoBook.set(peers[1], protocols) - expect(spyDs).to.have.property('callCount', 6) // 2 AddressBook + 2 KeyBook + 2 ProtoBook + // MetadataBook + peerStore.metadataBook.set(peers[0], 'location', Buffer.from('earth')) + + expect(spyDs).to.have.property('callCount', 7) // 2 Address + 2 Key + 2 Proto + 1 Metadata expect(peerStore.peers.size).to.equal(2) await peerStore.stop() @@ -131,13 +134,14 @@ describe('Persisted PeerStore', () => { await peerStore.start() - expect(spy).to.have.property('callCount', 6) - expect(spyDs).to.have.property('callCount', 6) + expect(spy).to.have.property('callCount', 7) + expect(spyDs).to.have.property('callCount', 7) expect(peerStore.peers.size).to.equal(2) expect(peerStore.addressBook.data.size).to.equal(2) expect(peerStore.keyBook.data.size).to.equal(2) expect(peerStore.protoBook.data.size).to.equal(2) + expect(peerStore.metadataBook.data.size).to.equal(1) }) it('should delete content from the datastore on delete', async () => { @@ -151,11 +155,14 @@ describe('Persisted PeerStore', () => { peerStore.addressBook.set(peer, multiaddrs) // ProtoBook peerStore.protoBook.set(peer, protocols) + // MetadataBook + peerStore.metadataBook.set(peer, 'location', Buffer.from('earth')) const spyDs = sinon.spy(datastore, 'batch') const spyAddressBook = sinon.spy(peerStore.addressBook, 'delete') const spyKeyBook = sinon.spy(peerStore.keyBook, 'delete') const spyProtoBook = sinon.spy(peerStore.protoBook, 'delete') + const spyMetadataBook = sinon.spy(peerStore.metadataBook, 'delete') // Delete from PeerStore peerStore.delete(peer) @@ -164,7 +171,8 @@ describe('Persisted PeerStore', () => { expect(spyAddressBook).to.have.property('callCount', 1) expect(spyKeyBook).to.have.property('callCount', 1) expect(spyProtoBook).to.have.property('callCount', 1) - expect(spyDs).to.have.property('callCount', 2) + expect(spyMetadataBook).to.have.property('callCount', 1) + expect(spyDs).to.have.property('callCount', 3) // Should have zero peer records stored in the datastore const queryParams = { @@ -187,6 +195,7 @@ describe('Persisted PeerStore', () => { it('should not commit until threshold is reached', async () => { const spyDirty = sinon.spy(peerStore, '_addDirtyPeer') + const spyDirtyMetadata = sinon.spy(peerStore, '_addDirtyPeerMetadata') const spyDs = sinon.spy(datastore, 'batch') const peers = await peerUtils.createPeerId({ number: 2 }) @@ -202,11 +211,13 @@ describe('Persisted PeerStore', () => { // Add Peer0 data in multiple books peerStore.addressBook.set(peers[0], multiaddrs) peerStore.protoBook.set(peers[0], protocols) + peerStore.metadataBook.set(peers[0], 'location', Buffer.from('earth')) // Remove data from the same Peer peerStore.addressBook.delete(peers[0]) expect(spyDirty).to.have.property('callCount', 3) // 2 AddrBook ops, 1 ProtoBook op + expect(spyDirtyMetadata).to.have.property('callCount', 1) // 1 MetadataBook op expect(peerStore._dirtyPeers.size).to.equal(1) expect(spyDs).to.have.property('callCount', 0) @@ -221,14 +232,15 @@ describe('Persisted PeerStore', () => { peerStore.addressBook.set(peers[1], multiaddrs) expect(spyDirty).to.have.property('callCount', 4) + expect(spyDirtyMetadata).to.have.property('callCount', 1) expect(spyDs).to.have.property('callCount', 1) - // Should have two peer records stored in the datastore + // Should have three peer records stored in the datastore let count = 0 for await (const _ of datastore.query(queryParams)) { // eslint-disable-line count++ } - expect(count).to.equal(2) + expect(count).to.equal(3) expect(peerStore.peers.size).to.equal(2) }) @@ -241,7 +253,7 @@ describe('Persisted PeerStore', () => { await peerStore.start() - // Add Peer data in a booka + // Add Peer data in a book peerStore.protoBook.set(peer, protocols) expect(spyDs).to.have.property('callCount', 0)