Skip to content
This repository has been archived by the owner on Jun 26, 2023. It is now read-only.

feat: topology #7

Merged
merged 6 commits into from
Nov 14, 2019
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
- [Peer Discovery](./src/peer-discovery)
- [Peer Routing](./src/peer-routing)
- [Stream Muxer](./src/stream-muxer)
- [Topology](./src/topology)
- [Transport](./src/transport)

### Origin Repositories
Expand Down
150 changes: 150 additions & 0 deletions src/topology/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
interface-topology
========================

> Implementation of the topology interface used by the `js-libp2p` registrar.

This interface has two main purposes. It uniforms the registration of libp2p protocols and enables a smarter connection management.
vasco-santos marked this conversation as resolved.
Show resolved Hide resolved

## Table of Contents

- [Implementations](#implementations)
- [Install](#install)
- [Modules using the interface](#modulesUsingTheInterface)
- [Usage](#usage)
- [Api](#api)

## Implementations

### Topology

A libp2p topology with a group of common peers.

### Multicodec Topology

A libp2p topology with a group of peers that support the same protocol.

## Install

```sh
$ npm install libp2p-interfaces
```

## Modules using the interface

TBA

## Usage

### Topology

```js
const Topology = require('libp2p-interfaces/src/topology')

const toplogy = new Topology({
min: 0,
max: 50
})
```

### Multicodec Topology

```js
const MulticodecTopology = require('libp2p-interfaces/src/topology/multicodec-topology')

const toplogy = new MulticodecTopology({
min: 0,
max: 50,
multicodecs: ['/echo/1.0.0'],
handlers: {
onConnect: (peerInfo, conn) => {},
onDisconnect: (peerInfo) => {}
}
})

vasco-santos marked this conversation as resolved.
Show resolved Hide resolved
// Needs to set registrar in order to listen for peer changes
vasco-santos marked this conversation as resolved.
Show resolved Hide resolved
topology.registrar = registrar
vasco-santos marked this conversation as resolved.
Show resolved Hide resolved
```

## API

The `MulticodecTopology` extends the `Topology`, which makes the `Topology` API a subset of the `MulticodecTopology` API.

### Topology

- `Topology`
- `peers.set<function(id, PeerInfo)>`: Sets a peer in the topology.
vasco-santos marked this conversation as resolved.
Show resolved Hide resolved
- `disconnect<function(PeerInfo)>`: Disconnects a peer from the topology.
vasco-santos marked this conversation as resolved.
Show resolved Hide resolved

#### Constructor

```js
const toplogy = new Topology({
min: 0,
max: 50,
handlers: {
onConnect: (peerInfo, conn) => {},
onDisconnect: (peerInfo) => {}
}
})
```

**Parameters**
- `properties` is an `Object` containing the properties of the topology.
- `min` is a `number` with the minimum needed connections (default: 0)
- `max` is a `number` with the maximum needed connections (default: Infinity)
- `handlers` is an optional `Object` containing the handler called when a peer is connected or disconnected.
- `onConnect` is a `function` called everytime a peer is connected in the topology context.
- `onDisconnect` is a `function` called everytime a peer is disconnected in the topology context.

#### Set a peer

- `topology.peers.set(id, peerInfo)`

Add a peer to the topology.

**Parameters**
- `id` is the `b58string` that identifies the peer to add.
vasco-santos marked this conversation as resolved.
Show resolved Hide resolved
- `peerInfo` is the [PeerInfo][peer-info] of the peer to add.

#### Notify about a peer disconnected event

- `topology.disconnect(peerInfo)`

**Parameters**
- `peerInfo` is the [PeerInfo][peer-info] of the peer disconnected.

### Multicodec Topology

- `MulticodecTopology`
- `registrar<Registrar>`: Sets the registrar in the topology.
vasco-santos marked this conversation as resolved.
Show resolved Hide resolved
- `peers.set<function(id, PeerInfo)>`: Sets a peer in the topology.
vasco-santos marked this conversation as resolved.
Show resolved Hide resolved
- `disconnect<function(PeerInfo)>`: Disconnects a peer from the topology.

#### Constructor

```js
const toplogy = new MulticodecTopology({
min: 0,
max: 50,
multicodecs: ['/echo/1.0.0'],
handlers: {
onConnect: (peerInfo, conn) => {},
onDisconnect: (peerInfo) => {}
}
})
```

**Parameters**
- `properties` is an `Object` containing the properties of the topology.
- `min` is a `number` with the minimum needed connections (default: 0)
- `max` is a `number` with the maximum needed connections (default: Infinity)
- `multicodecs` is a `Array<String>` with the multicodecs associated with the topology.
- `handlers` is an optional `Object` containing the handler called when a peer is connected or disconnected.
- `onConnect` is a `function` called everytime a peer is connected in the topology context.
- `onDisconnect` is a `function` called everytime a peer is disconnected in the topology context.

#### Set the registrar
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's delete this section, I think the property overview is sufficient for now. Also, the Registrar is going to set topology.registrar on the base Topology as well during registration, every Topology will have that property once they've registered, so it might be good to add that to the Topology

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed the section, added the set to the Topology and overwrite it in the MulticodecTopology


- `topology.registrar = registrar`

Set the registrar the topology, which will be used to gather information on peers being connected and disconnected, as well as their modifications in terms of supported protocols.
40 changes: 40 additions & 0 deletions src/topology/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
'use strict'

const noop = () => {}

class Topology {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This really isn't a base Topology, it's a MulticodecTopology which should extend a base. Not all topologies are going to care about multicodecs or protocol changes. Two immediate use cases that come to mind for Topologies are Priority Peers and Bootstrap Nodes, which I would categorize as a PeerSet Topology.

Let's say a js-ipfs browser node would like to stay connected to a Preload Node, QmPreload. Currently, libp2p doesn't handle that. What could happen, is a new PeerSet Topology could be created, which we should be able to achieve with the Base Topology.

const priorityTopology = new Topology()
priorityTopology.peers.set('QmPreload', preloadPeerInfo) // We could iterate over several peers.
priorityTopology.min = priorityTopology.peers.size // We want to be connected to all our peers

// ... registration

This same setup could be used for Bootstrap nodes, instead of it being a "discovery" service. The only difference is that the min can be 0, because we shouldn't really care if we're connected to them, as long as our node has at least ConnectionManager.min connections, which will be handled elsewhere.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added multicodec-topology

/**
* @param {Object} props
* @param {number} props.min minimum needed connections (default: 0)
* @param {number} props.max maximum needed connections (default: Infinity)
* @param {Object} [props.handlers]
* @param {function} [props.handlers.onConnect] protocol "onConnect" handler
* @param {function} [props.handlers.onDisconnect] protocol "onDisconnect" handler
* @constructor
*/
constructor ({
min = 0,
max = Infinity,
handlers = {}
}) {
this.min = min
this.max = max

// Handlers
this._onConnect = handlers.onConnect || noop
this._onDisconnect = handlers.onDisconnect || noop

this.peers = new Map()
}

/**
* Notify about peer disconnected event.
* @param {PeerInfo} peerInfo
* @returns {void}
*/
disconnect (peerInfo) {
this._onDisconnect(peerInfo)
}
}

module.exports = Topology
93 changes: 93 additions & 0 deletions src/topology/multicodec-topology.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
'use strict'

const assert = require('assert')
const Topology = require('./index')

class MulticodecTopology extends Topology {
/**
* @param {Object} props
* @param {number} props.min minimum needed connections (default: 0)
* @param {number} props.max maximum needed connections (default: Infinity)
* @param {Array<string>} props.multicodecs protocol multicodecs
* @param {Object} props.handlers
* @param {function} props.handlers.onConnect protocol "onConnect" handler
* @param {function} props.handlers.onDisconnect protocol "onDisconnect" handler
* @constructor
*/
constructor ({
min,
max,
multicodecs,
handlers
}) {
super({ min, max, handlers })

assert(multicodecs, 'one or more multicodec should be provided')
assert(handlers, 'the handlers should be provided')
assert(handlers.onConnect && typeof handlers.onConnect === 'function',
'the \'onConnect\' handler must be provided')
assert(handlers.onDisconnect && typeof handlers.onDisconnect === 'function',
'the \'onDisconnect\' handler must be provided')

this.multicodecs = Array.isArray(multicodecs) ? multicodecs : [multicodecs]
this._registrar = undefined

this._onProtocolChange = this._onProtocolChange.bind(this)
}

set registrar (registrar) {
this._registrar = registrar
this._registrar.peerStore.on('change:protocols', this._onProtocolChange)

// Update topology peers
this._updatePeers(this._registrar.peerStore.peers.values())
}

/**
* Update topology.
* @param {Array<PeerInfo>} peerInfoIterable
* @returns {void}
*/
_updatePeers (peerInfoIterable) {
for (const peerInfo of peerInfoIterable) {
if (this.multicodecs.filter(multicodec => peerInfo.protocols.has(multicodec))) {
// Add the peer regardless of whether or not there is currently a connection
this.peers.set(peerInfo.id.toB58String(), peerInfo)
// If there is a connection, call _onConnect
const connection = this._registrar.getConnection(peerInfo)
connection && this._onConnect(peerInfo, connection)
} else {
// Remove any peers we might be tracking that are no longer of value to us
this.peers.delete(peerInfo.id.toB58String())
}
}
}

/**
* Check if a new peer support the multicodecs for this topology.
* @param {Object} props
* @param {PeerInfo} props.peerInfo
* @param {Array<string>} props.protocols
*/
_onProtocolChange ({ peerInfo, protocols }) {
const existingPeer = this.peers.get(peerInfo.id.toB58String())
const hasProtocol = protocols.filter(protocol => this.multicodecs.includes(protocol))

// Not supporting the protocol anymore?
if (existingPeer && hasProtocol.length === 0) {
this._onDisconnect({
peerInfo
})
}

// New to protocol support
for (const protocol of protocols) {
if (this.multicodecs.includes(protocol)) {
this._updatePeers([peerInfo])
return
}
}
}
}

module.exports = MulticodecTopology
89 changes: 89 additions & 0 deletions src/topology/tests/multicodec-topology.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
/* eslint-env mocha */

'use strict'

const chai = require('chai')
const expect = chai.expect
chai.use(require('dirty-chai'))
const sinon = require('sinon')

const PeerId = require('peer-id')
const PeerInfo = require('peer-info')
const peers = require('../../utils/peers')

module.exports = (test) => {
describe('multicodec topology', () => {
let topology, peer

beforeEach(async () => {
topology = await test.setup()
if (!topology) throw new Error('missing multicodec topology')

const id = await PeerId.createFromJSON(peers[0])
peer = await PeerInfo.create(id)
})

afterEach(async () => {
sinon.restore()
await test.teardown()
})

it('should have properties set', () => {
expect(topology.multicodecs).to.exist()
expect(topology._onConnect).to.exist()
expect(topology._onDisconnect).to.exist()
expect(topology.peers).to.exist()
expect(topology._registrar).to.exist()
})

it('should trigger "onDisconnect" on peer disconnected', () => {
sinon.spy(topology, '_onDisconnect')
topology.disconnect(peer)

expect(topology._onDisconnect.callCount).to.equal(1)
})

it('should update peers on protocol change', async () => {
sinon.spy(topology, '_updatePeers')
expect(topology.peers.size).to.eql(0)

const id2 = await PeerId.createFromJSON(peers[1])
const peer2 = await PeerInfo.create(id2)

const peerStore = topology._registrar.peerStore
peerStore.emit('change:protocols', {
peerInfo: peer2,
protocols: Array.from(topology.multicodecs)
})

expect(topology._updatePeers.callCount).to.equal(1)
expect(topology.peers.size).to.eql(1)
})

it('should disconnect if peer no longer supports a protocol', async () => {
sinon.spy(topology, '_onDisconnect')
expect(topology.peers.size).to.eql(0)

const id2 = await PeerId.createFromJSON(peers[1])
const peer2 = await PeerInfo.create(id2)
const peerStore = topology._registrar.peerStore

// Peer with the protocol
peerStore.emit('change:protocols', {
peerInfo: peer2,
protocols: Array.from(topology.multicodecs)
})

expect(topology.peers.size).to.eql(1)

// Peer does not support the protocol anymore
peerStore.emit('change:protocols', {
peerInfo: peer2,
protocols: []
})

expect(topology.peers.size).to.eql(1)
expect(topology._onDisconnect.callCount).to.equal(1)
})
})
}
Loading