From d91f5807824ceeb6a50abf85075ed7768f002361 Mon Sep 17 00:00:00 2001 From: Jacob Heun Date: Thu, 4 Oct 2018 15:30:03 +0200 Subject: [PATCH 01/13] chore: use switch state machine branch --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 4f8ee1f168..91783f138d 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,7 @@ "libp2p-connection-manager": "~0.0.2", "libp2p-floodsub": "~0.15.0", "libp2p-ping": "~0.8.0", - "libp2p-switch": "~0.40.8", + "libp2p-switch": "github:libp2p/js-libp2p-switch#feat/state-machine", "libp2p-websockets": "~0.12.0", "mafmt": "^6.0.2", "multiaddr": "^5.0.0", From 5b4118efe51a236bde4c953f40b93751921ab936 Mon Sep 17 00:00:00 2001 From: Jacob Heun Date: Fri, 5 Oct 2018 16:16:25 +0200 Subject: [PATCH 02/13] feat: add basic fsm support --- package.json | 5 +- src/index.js | 316 ++++++++++++++++++++++++++++++++++++--------------- 2 files changed, 228 insertions(+), 93 deletions(-) diff --git a/package.json b/package.json index 91783f138d..b0f768ac25 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,9 @@ }, "dependencies": { "async": "^2.6.1", + "debug": "^4.0.1", "err-code": "^1.1.2", + "fsm-event": "^2.1.0", "joi": "^13.6.0", "joi-browser": "^13.4.0", "libp2p-connection-manager": "~0.0.2", @@ -57,16 +59,17 @@ "@nodeutils/defaults-deep": "^1.1.0", "aegir": "^15.2.0", "chai": "^4.1.2", + "chai-checkmark": "^1.0.1", "cids": "~0.5.3", "dirty-chai": "^2.0.1", "electron-webrtc": "~0.3.0", + "libp2p-bootstrap": "~0.9.3", "libp2p-circuit": "~0.2.1", "libp2p-delegated-content-routing": "~0.2.2", "libp2p-delegated-peer-routing": "~0.2.2", "libp2p-kad-dht": "~0.10.5", "libp2p-mdns": "~0.12.0", "libp2p-mplex": "~0.8.2", - "libp2p-bootstrap": "~0.9.3", "libp2p-secio": "~0.10.0", "libp2p-spdy": "~0.12.1", "libp2p-tcp": "~0.13.0", diff --git a/src/index.js b/src/index.js index 424018fa76..cc50a2e012 100644 --- a/src/index.js +++ b/src/index.js @@ -1,7 +1,11 @@ 'use strict' +const FSM = require('fsm-event') const EventEmitter = require('events').EventEmitter const assert = require('assert') +const debug = require('debug') +const log = debug('libp2p') +log.error = debug('libp2p:error') const each = require('async/each') const series = require('async/series') @@ -109,15 +113,211 @@ class Node extends EventEmitter { // Mount default protocols Ping.mount(this._switch) + + this.state = new FSM('STOPPED', { + STOPPED: { + start: 'STARTING', + stop: 'STOPPED' + }, + STARTING: { + done: 'STARTED', + stop: 'STOPPING' + }, + STARTED: { + stop: 'STOPPING', + start: 'STARTED' + }, + STOPPING: { done: 'STOPPED' } + }) + this.state.on('STARTING', () => { + log('libp2p is starting') + this._onStarting() + }) + this.state.on('STOPPING', () => { + log('libp2p is stopping') + this._onStopping() + }) + this.state.on('STARTED', () => { + log('libp2p has started') + this.emit('start') + }) + this.state.on('STOPPED', () => { + log('libp2p has stopped') + this.emit('stop') + }) + this.state.on('error', (err) => { + log.error(err) + this.emit('error', err) + }) + } + + /** + * Starts the libp2p node and all sub services + * + * @param {function(Error)} callback + * @fires Node#start + * @returns {void} + */ + start (callback = () => {}) { + this.once('start', callback) + this.state('start') + } + + /** + * Stop the libp2p node by closing its listeners and open connections + * + * @param {function(Error)} callback + * @fires Node#stop + * @returns {void} + */ + stop (callback = () => {}) { + this.once('stop', callback) + this.state('stop') + } + + isStarted () { + return this.state ? this.state._state === 'STARTED' : false + } + + /** + * Dials to the provided peer. If successful, the `PeerInfo` of the + * peer will be added to the nodes `PeerBook` + * + * @param {PeerInfo|PeerId|Multiaddr|string} peer The peer to dial + * @param {function(Error)} callback + * @returns {void} + */ + dial (peer, callback) { + assert(this.isStarted(), NOT_STARTED_ERROR_MESSAGE) + + this._getPeerInfo(peer, (err, peerInfo) => { + if (err) { return callback(err) } + + this._switch.dial(peerInfo, (err) => { + if (err) { return callback(err) } + + this.peerBook.put(peerInfo) + callback() + }) + }) + } + + /** + * Similar to `dial`, but the callback will contain a + * Connection State Machine. + * + * @param {PeerInfo|PeerId|Multiaddr|string} peer The peer to dial + * @param {function(Error, ConnectionFSM)} callback + * @returns {void} + */ + dialFSM (peer, callback) { + assert(this.isStarted(), NOT_STARTED_ERROR_MESSAGE) + + this._getPeerInfo(peer, (err, peerInfo) => { + if (err) { return callback(err) } + + const connFSM = this._switch.dialFSM(peerInfo, (err) => { + if (!err) { + this.peerBook.put(peerInfo) + } + }) + + callback(null, connFSM) + }) + } + + /** + * Dials to the provided peer and handshakes with the given protocol. + * If successful, the `PeerInfo` of the peer will be added to the nodes `PeerBook`, + * and the `Connection` will be sent in the callback + * + * @param {PeerInfo|PeerId|Multiaddr|string} peer The peer to dial + * @param {function(Error, Connection)} callback + * @returns {void} + */ + dialProtocol (peer, protocol, callback) { + assert(this.isStarted(), NOT_STARTED_ERROR_MESSAGE) + + if (typeof protocol === 'function') { + callback = protocol + protocol = undefined + } + + this._getPeerInfo(peer, (err, peerInfo) => { + if (err) { return callback(err) } + + this._switch.dial(peerInfo, protocol, (err, conn) => { + if (err) { return callback(err) } + this.peerBook.put(peerInfo) + callback(null, conn) + }) + }) } - /* - * Start the libp2p node - * - create listeners on the multiaddrs the Peer wants to listen + /** + * Similar to `dialProtocol`, but the callback will contain a + * Connection State Machine. + * + * @param {PeerInfo|PeerId|Multiaddr|string} peer The peer to dial + * @param {string} protocol + * @param {function(Error, ConnectionFSM)} callback + * @returns {void} */ - start (callback) { + dialProtocolFSM (peer, protocol, callback) { + assert(this.isStarted(), NOT_STARTED_ERROR_MESSAGE) + + if (typeof protocol === 'function') { + callback = protocol + protocol = undefined + } + + this._getPeerInfo(peer, (err, peerInfo) => { + if (err) { return callback(err) } + + const connFSM = this._switch.dialFSM(peerInfo, protocol, (err) => { + if (!err) { + this.peerBook.put(peerInfo) + } + }) + + callback(null, connFSM) + }) + } + + hangUp (peer, callback) { + assert(this.isStarted(), NOT_STARTED_ERROR_MESSAGE) + + this._getPeerInfo(peer, (err, peerInfo) => { + if (err) { return callback(err) } + + this._switch.hangUp(peerInfo, callback) + }) + } + + ping (peer, callback) { + if (!this.isStarted()) { + return callback(new Error(NOT_STARTED_ERROR_MESSAGE)) + } + + this._getPeerInfo(peer, (err, peerInfo) => { + if (err) { return callback(err) } + + callback(null, new Ping(this._switch, peerInfo)) + }) + } + + handle (protocol, handlerFunc, matchFunc) { + this._switch.handle(protocol, handlerFunc, matchFunc) + } + + unhandle (protocol) { + this._switch.unhandle(protocol) + } + + _onStarting () { if (!this._modules.transport) { - return callback(new Error('no transports were present')) + this.emit('error', new Error('no transports were present')) + return this.state('stop') } let ws @@ -214,12 +414,10 @@ class Node extends EventEmitter { // TODO: chicken-and-egg problem #2: // have to set started here because FloodSub requires libp2p is already started if (this._floodSub) { - this._floodSub.start(cb) - } else { - cb() + return this._floodSub.start(cb) } + cb() }, - (cb) => { // detect which multiaddrs we don't have a transport for and remove them const multiaddrs = this.peerInfo.multiaddrs.toArray() @@ -231,18 +429,17 @@ class Node extends EventEmitter { } }) cb() - }, - (cb) => { - this.emit('start') - cb() } - ], callback) + ], (err) => { + if (err) { + log.error(err) + this.emit('error', err) + } + this.state('done') + }) } - /* - * Stop the libp2p node by closing its listeners and open connections - */ - stop (callback) { + _onStopping () { series([ (cb) => { if (this._modules.peerDiscovery) { @@ -269,86 +466,21 @@ class Node extends EventEmitter { cb() }, (cb) => { - this.connectionManager.stop() - this._switch.stop(cb) + // Ensures idempotency for restarts + this._switch.transport.removeAll(cb) }, (cb) => { - this.emit('stop') - cb() + this.connectionManager.stop() + this._switch.stop(cb) } ], (err) => { - this._isStarted = false - callback(err) - }) - } - - isStarted () { - return this._isStarted - } - - dial (peer, callback) { - assert(this.isStarted(), NOT_STARTED_ERROR_MESSAGE) - - this._getPeerInfo(peer, (err, peerInfo) => { - if (err) { return callback(err) } - - this._switch.dial(peerInfo, (err) => { - if (err) { return callback(err) } - - this.peerBook.put(peerInfo) - callback() - }) - }) - } - - dialProtocol (peer, protocol, callback) { - assert(this.isStarted(), NOT_STARTED_ERROR_MESSAGE) - - if (typeof protocol === 'function') { - callback = protocol - protocol = undefined - } - - this._getPeerInfo(peer, (err, peerInfo) => { - if (err) { return callback(err) } - - this._switch.dial(peerInfo, protocol, (err, conn) => { - if (err) { return callback(err) } - this.peerBook.put(peerInfo) - callback(null, conn) - }) - }) - } - - hangUp (peer, callback) { - assert(this.isStarted(), NOT_STARTED_ERROR_MESSAGE) - - this._getPeerInfo(peer, (err, peerInfo) => { - if (err) { return callback(err) } - - this._switch.hangUp(peerInfo, callback) - }) - } - - ping (peer, callback) { - if (!this.isStarted()) { - return callback(new Error(NOT_STARTED_ERROR_MESSAGE)) - } - - this._getPeerInfo(peer, (err, peerInfo) => { - if (err) { return callback(err) } - - callback(null, new Ping(this._switch, peerInfo)) + if (err) { + log.error(err) + this.emit('error', err) + } + this.state('done') }) } - - handle (protocol, handlerFunc, matchFunc) { - this._switch.handle(protocol, handlerFunc, matchFunc) - } - - unhandle (protocol) { - this._switch.unhandle(protocol) - } } module.exports = Node From 8c74fb12023fdecd1b1730fbafb48343ac53aaf2 Mon Sep 17 00:00:00 2001 From: Jacob Heun Date: Fri, 5 Oct 2018 17:17:16 +0200 Subject: [PATCH 03/13] test: add fsm tests --- test/fsm.spec.js | 78 ++++++++++++++++++++++++++++++++++++++ test/transports.browser.js | 13 +++++++ test/transports.node.js | 24 +++++++++++- 3 files changed, 114 insertions(+), 1 deletion(-) create mode 100644 test/fsm.spec.js diff --git a/test/fsm.spec.js b/test/fsm.spec.js new file mode 100644 index 0000000000..3069c9ebf7 --- /dev/null +++ b/test/fsm.spec.js @@ -0,0 +1,78 @@ +/* eslint-env mocha */ +'use strict' + +const chai = require('chai') +chai.use(require('dirty-chai')) +chai.use(require("chai-checkmark")) +const expect = chai.expect +const series = require('async/series') +const parallel = require('async/parallel') +const createNode = require('./utils/create-node') +const sinon = require('sinon') + +describe('libp2p state machine (fsm)', () => { + describe('starting and stopping', () => { + let node + beforeEach((done) => { + createNode([], (err, _node) => { + node = _node + done(err) + }) + }) + afterEach(() => { + node.removeAllListeners() + }) + after((done) => { + node.stop(done) + node = null + }) + + it('should be able to start and stop several times', (done) => { + node.on('start', (err) => { + expect(err).to.not.exist().mark() + }) + node.on('stop', (err) => { + expect(err).to.not.exist().mark() + }) + + expect(4).checks(done) + + series([ + (cb) => node.start(cb), + (cb) => node.stop(cb), + (cb) => node.start(cb), + (cb) => node.stop(cb) + ], () => {}) + }) + + it('should noop when stopping a stopped node', (done) => { + node.once('start', node.stop) + node.once('stop', () => { + node.state.on('STOPPING', () => { + throw new Error('should not stop a stopped node') + }) + node.once('stop', done) + + // stop the stopped node + node.stop() + }) + node.start() + }) + + it('should noop when starting a started node', (done) => { + node.once('start', () => { + node.state.on('STARTING', () => { + throw new Error('should not start a started node') + }) + node.once('start', () => { + node.once('stop', done) + node.stop() + }) + + // start the started node + node.start() + }) + node.start() + }) + }) +}) \ No newline at end of file diff --git a/test/transports.browser.js b/test/transports.browser.js index fcf749b653..4f05c62beb 100644 --- a/test/transports.browser.js +++ b/test/transports.browser.js @@ -148,6 +148,19 @@ describe('transports', () => { }) }) + it('.dialFSMProtocol do an echo and close', (done) => { + nodeA.dialProtocolFSM(peerB, '/echo/1.0.0', (err, connFSM) => { + expect(err).to.not.exist() + connFSM.once('connection', (conn) => { + tryEcho(conn, () => { + connFSM.close() + }) + }) + connFSM.once('error', done) + connFSM.once('close', done) + }) + }) + describe('stress', () => { it('one big write', (done) => { nodeA.dialProtocol(peerB, '/echo/1.0.0', (err, conn) => { diff --git a/test/transports.node.js b/test/transports.node.js index ac71c979df..db0e961c37 100644 --- a/test/transports.node.js +++ b/test/transports.node.js @@ -185,7 +185,7 @@ describe('transports', () => { }) it('nodeA.hangUp nodeB using PeerId (third)', (done) => { - nodeA.hangUp(nodeB.peerInfo.multiaddrs.toArray()[0], (err) => { + nodeA.hangUp(nodeB.peerInfo.id, (err) => { expect(err).to.not.exist() setTimeout(check, 500) @@ -207,6 +207,28 @@ describe('transports', () => { } }) }) + + it('.dialFSMProtocol do an echo and close', (done) => { + nodeA.dialProtocolFSM(nodeB.peerInfo, '/echo/1.0.0', (err, connFSM) => { + expect(err).to.not.exist() + connFSM.once('connection', (conn) => { + expect(nodeA._switch.muxedConns).to.have.all.keys([ + nodeB.peerInfo.id.toB58String() + ]) + tryEcho(conn, () => { + connFSM.close() + }) + }) + connFSM.once('error', done) + connFSM.once('close', () => { + // ensure the connection is closed + expect(nodeA._switch.muxedConns).to.not.have.any.keys([ + nodeB.peerInfo.id.toB58String() + ]) + done() + }) + }) + }) }) describe('TCP + WebSockets', () => { From 0dd41044291036b6c736b6de973d1bbfe27b45af Mon Sep 17 00:00:00 2001 From: Jacob Heun Date: Fri, 5 Oct 2018 17:39:33 +0200 Subject: [PATCH 04/13] test: add dialFSM tests --- test/transports.browser.js | 23 +++++++++++++++++++++++ test/transports.node.js | 23 +++++++++++++++++++++++ 2 files changed, 46 insertions(+) diff --git a/test/transports.browser.js b/test/transports.browser.js index 4f05c62beb..312a52bca6 100644 --- a/test/transports.browser.js +++ b/test/transports.browser.js @@ -148,6 +148,29 @@ describe('transports', () => { }) }) + it('.dialFSM check conn and close', (done) => { + nodeA.dialFSM(peerB, (err, connFSM) => { + expect(err).to.not.exist() + + connFSM.once('muxed', () => { + expect(nodeA._switch.muxedConns).to.have.any.keys( + peerB.id.toB58String() + ) + + connFSM.once('error', done) + connFSM.once('close', () => { + // ensure the connection is closed + expect(nodeA._switch.muxedConns).to.not.have.any.keys([ + peerB.id.toB58String() + ]) + done() + }) + + connFSM.close() + }) + }) + }) + it('.dialFSMProtocol do an echo and close', (done) => { nodeA.dialProtocolFSM(peerB, '/echo/1.0.0', (err, connFSM) => { expect(err).to.not.exist() diff --git a/test/transports.node.js b/test/transports.node.js index db0e961c37..12eee30d65 100644 --- a/test/transports.node.js +++ b/test/transports.node.js @@ -208,6 +208,29 @@ describe('transports', () => { }) }) + it('.dialFSM check conn and close', (done) => { + nodeA.dialFSM(nodeB.peerInfo, (err, connFSM) => { + expect(err).to.not.exist() + + connFSM.once('muxed', () => { + expect(nodeA._switch.muxedConns).to.have.any.keys( + nodeB.peerInfo.id.toB58String() + ) + + connFSM.once('error', done) + connFSM.once('close', () => { + // ensure the connection is closed + expect(nodeA._switch.muxedConns).to.not.have.any.keys([ + nodeB.peerInfo.id.toB58String() + ]) + done() + }) + + connFSM.close() + }) + }) + }) + it('.dialFSMProtocol do an echo and close', (done) => { nodeA.dialProtocolFSM(nodeB.peerInfo, '/echo/1.0.0', (err, connFSM) => { expect(err).to.not.exist() From 4e2c090b5ff4794978a73490b9c9eb9ad7c9f78b Mon Sep 17 00:00:00 2001 From: Jacob Heun Date: Fri, 5 Oct 2018 23:02:56 +0200 Subject: [PATCH 05/13] fix: linting --- examples/discovery-mechanisms/1.js | 1 + examples/discovery-mechanisms/2.js | 1 + examples/libp2p-in-the-browser/1/src/index.js | 1 + examples/peer-and-content-routing/1.js | 1 + examples/peer-and-content-routing/2.js | 1 + examples/protocol-and-stream-muxing/3.js | 1 + examples/pubsub/1.js | 1 + examples/transports/1.js | 1 + examples/transports/2.js | 1 + examples/transports/3.js | 1 + src/index.js | 1 + test/fsm.spec.js | 6 ++---- test/transports.browser.js | 1 + test/turbolence.node.js | 1 + 14 files changed, 15 insertions(+), 4 deletions(-) diff --git a/examples/discovery-mechanisms/1.js b/examples/discovery-mechanisms/1.js index 444973db2c..13d389a0d0 100644 --- a/examples/discovery-mechanisms/1.js +++ b/examples/discovery-mechanisms/1.js @@ -1,3 +1,4 @@ +/* eslint-disable no-console */ 'use strict' const libp2p = require('../../') diff --git a/examples/discovery-mechanisms/2.js b/examples/discovery-mechanisms/2.js index 10bcf168e2..1711b3f0bd 100644 --- a/examples/discovery-mechanisms/2.js +++ b/examples/discovery-mechanisms/2.js @@ -1,3 +1,4 @@ +/* eslint-disable no-console */ 'use strict' const libp2p = require('../../') diff --git a/examples/libp2p-in-the-browser/1/src/index.js b/examples/libp2p-in-the-browser/1/src/index.js index 7100454b8a..95a98b544f 100644 --- a/examples/libp2p-in-the-browser/1/src/index.js +++ b/examples/libp2p-in-the-browser/1/src/index.js @@ -1,3 +1,4 @@ +/* eslint-disable no-console */ 'use strict' const domReady = require('detect-dom-ready') diff --git a/examples/peer-and-content-routing/1.js b/examples/peer-and-content-routing/1.js index bf121d7dea..d9a09df241 100644 --- a/examples/peer-and-content-routing/1.js +++ b/examples/peer-and-content-routing/1.js @@ -1,3 +1,4 @@ +/* eslint-disable no-console */ 'use strict' const libp2p = require('../../') diff --git a/examples/peer-and-content-routing/2.js b/examples/peer-and-content-routing/2.js index acd508d48e..f3f77d49bf 100644 --- a/examples/peer-and-content-routing/2.js +++ b/examples/peer-and-content-routing/2.js @@ -1,3 +1,4 @@ +/* eslint-disable no-console */ 'use strict' const libp2p = require('../../') diff --git a/examples/protocol-and-stream-muxing/3.js b/examples/protocol-and-stream-muxing/3.js index 730bf01ecc..453c035ade 100644 --- a/examples/protocol-and-stream-muxing/3.js +++ b/examples/protocol-and-stream-muxing/3.js @@ -1,3 +1,4 @@ +/* eslint-disable no-console */ 'use strict' const libp2p = require('../../') diff --git a/examples/pubsub/1.js b/examples/pubsub/1.js index 7331de180f..8adfd35dd1 100644 --- a/examples/pubsub/1.js +++ b/examples/pubsub/1.js @@ -1,3 +1,4 @@ +/* eslint-disable no-console */ 'use strict' const libp2p = require('../../') diff --git a/examples/transports/1.js b/examples/transports/1.js index 58b279cb01..8f334a1691 100644 --- a/examples/transports/1.js +++ b/examples/transports/1.js @@ -1,3 +1,4 @@ +/* eslint-disable no-console */ 'use strict' const libp2p = require('../../') diff --git a/examples/transports/2.js b/examples/transports/2.js index 508c01d628..64465c2206 100644 --- a/examples/transports/2.js +++ b/examples/transports/2.js @@ -1,3 +1,4 @@ +/* eslint-disable no-console */ 'use strict' const libp2p = require('../../') diff --git a/examples/transports/3.js b/examples/transports/3.js index 9bce138108..c117d648d2 100644 --- a/examples/transports/3.js +++ b/examples/transports/3.js @@ -1,3 +1,4 @@ +/* eslint-disable no-console */ 'use strict' const libp2p = require('../../') diff --git a/src/index.js b/src/index.js index cc50a2e012..40cad6ce80 100644 --- a/src/index.js +++ b/src/index.js @@ -232,6 +232,7 @@ class Node extends EventEmitter { * and the `Connection` will be sent in the callback * * @param {PeerInfo|PeerId|Multiaddr|string} peer The peer to dial + * @param {string} protocol * @param {function(Error, Connection)} callback * @returns {void} */ diff --git a/test/fsm.spec.js b/test/fsm.spec.js index 3069c9ebf7..464106716a 100644 --- a/test/fsm.spec.js +++ b/test/fsm.spec.js @@ -3,12 +3,10 @@ const chai = require('chai') chai.use(require('dirty-chai')) -chai.use(require("chai-checkmark")) +chai.use(require('chai-checkmark')) const expect = chai.expect const series = require('async/series') -const parallel = require('async/parallel') const createNode = require('./utils/create-node') -const sinon = require('sinon') describe('libp2p state machine (fsm)', () => { describe('starting and stopping', () => { @@ -75,4 +73,4 @@ describe('libp2p state machine (fsm)', () => { node.start() }) }) -}) \ No newline at end of file +}) diff --git a/test/transports.browser.js b/test/transports.browser.js index 312a52bca6..75e31c3a60 100644 --- a/test/transports.browser.js +++ b/test/transports.browser.js @@ -230,6 +230,7 @@ describe('transports', () => { }) describe('webrtc-star', () => { + /* eslint-disable-next-line no-console */ if (!w.support) { return console.log('NO WEBRTC SUPPORT') } let peer1 diff --git a/test/turbolence.node.js b/test/turbolence.node.js index 378033400c..1bc96c9dc4 100644 --- a/test/turbolence.node.js +++ b/test/turbolence.node.js @@ -42,6 +42,7 @@ describe('Turbolence tests', () => { } }) + /* eslint-disable-next-line no-console */ nodeSpawn.stderr.on('data', (data) => console.log(data.toString())) }) From 98e24c66cfc9fbb18989a6d7319df794d691af47 Mon Sep 17 00:00:00 2001 From: Jacob Heun Date: Fri, 5 Oct 2018 23:52:09 +0200 Subject: [PATCH 06/13] test: add fsm error state tests --- src/index.js | 9 +++++++-- test/fsm.spec.js | 42 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 2 deletions(-) diff --git a/src/index.js b/src/index.js index 40cad6ce80..9477ca37be 100644 --- a/src/index.js +++ b/src/index.js @@ -121,13 +121,17 @@ class Node extends EventEmitter { }, STARTING: { done: 'STARTED', + abort: 'STOPPED', stop: 'STOPPING' }, STARTED: { stop: 'STOPPING', start: 'STARTED' }, - STOPPING: { done: 'STOPPED' } + STOPPING: { + stop: 'STOPPING', + done: 'STOPPED' + } }) this.state.on('STARTING', () => { log('libp2p is starting') @@ -318,7 +322,7 @@ class Node extends EventEmitter { _onStarting () { if (!this._modules.transport) { this.emit('error', new Error('no transports were present')) - return this.state('stop') + return this.state('abort') } let ws @@ -435,6 +439,7 @@ class Node extends EventEmitter { if (err) { log.error(err) this.emit('error', err) + return this.state('stop') } this.state('done') }) diff --git a/test/fsm.spec.js b/test/fsm.spec.js index 464106716a..171fe62865 100644 --- a/test/fsm.spec.js +++ b/test/fsm.spec.js @@ -5,6 +5,7 @@ const chai = require('chai') chai.use(require('dirty-chai')) chai.use(require('chai-checkmark')) const expect = chai.expect +const sinon = require('sinon') const series = require('async/series') const createNode = require('./utils/create-node') @@ -72,5 +73,46 @@ describe('libp2p state machine (fsm)', () => { }) node.start() }) + + it('should error on start with no transports', (done) => { + let transports = node._modules.transport + node._modules.transport = null + + node.on('stop', () => { + node._modules.transport = transports + expect(node._modules.transport).to.exist().mark() + }) + node.on('error', (err) => { + expect(err).to.exist().mark() + }) + node.on('start', () => { + throw new Error('should not start') + }) + + expect(2).checks(done) + + node.start() + }) + + it('should not start if the switch fails to start', (done) => { + const error = new Error('switch didnt start') + const stub = sinon.stub(node._switch, 'start') + .callsArgWith(0, error) + + node.on('stop', () => { + expect(stub.calledOnce).to.eql(true).mark() + stub.restore() + }) + node.on('error', (err) => { + expect(err).to.eql(error).mark() + }) + node.on('start', () => { + throw new Error('should not start') + }) + + expect(2).checks(done) + + node.start() + }) }) }) From e921ac5e56e74006153066a7500d3625cd7fd5be Mon Sep 17 00:00:00 2001 From: Jacob Heun Date: Sat, 6 Oct 2018 00:09:58 +0200 Subject: [PATCH 07/13] test: add ping tests --- test/node.js | 3 ++- test/ping.node.js | 61 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+), 1 deletion(-) create mode 100644 test/ping.node.js diff --git a/test/node.js b/test/node.js index ccfa009962..9e5bd89bd1 100644 --- a/test/node.js +++ b/test/node.js @@ -4,8 +4,9 @@ require('./pnet.node') require('./transports.node') require('./stream-muxing.node') require('./peer-discovery.node') -require('./pubsub.node') require('./peer-routing.node') +require('./ping.node') +require('./pubsub.node') require('./content-routing.node') require('./circuit-relay.node') require('./multiaddr-trim.node') diff --git a/test/ping.node.js b/test/ping.node.js new file mode 100644 index 0000000000..b677ab6a43 --- /dev/null +++ b/test/ping.node.js @@ -0,0 +1,61 @@ +/* eslint-env mocha */ +'use strict' + +const chai = require('chai') +chai.use(require('dirty-chai')) +const expect = chai.expect +const parallel = require('async/parallel') + +const createNode = require('./utils/create-node.js') +const echo = require('./utils/echo') + +describe('ping', () => { + let nodeA + let nodeB + + before((done) => { + parallel([ + (cb) => createNode('/ip4/0.0.0.0/tcp/0', (err, node) => { + expect(err).to.not.exist() + nodeA = node + node.handle('/echo/1.0.0', echo) + node.start(cb) + }), + (cb) => createNode('/ip4/0.0.0.0/tcp/0', (err, node) => { + expect(err).to.not.exist() + nodeB = node + node.handle('/echo/1.0.0', echo) + node.start(cb) + }) + ], done) + }) + + after((done) => { + parallel([ + (cb) => nodeA.stop(cb), + (cb) => nodeB.stop(cb) + ], done) + }) + + it('should be able to ping another node', (done) => { + nodeA.ping(nodeB.peerInfo, (err, ping) => { + expect(err).to.not.exist() + ping.once('ping', (time) => { + expect(time).to.exist() + ping.stop() + done() + }) + + ping.start() + }) + }) + + it('should be not be able to ping when stopped', (done) => { + nodeA.stop(() => { + nodeA.ping(nodeB.peerInfo, (err) => { + expect(err).to.exist() + done() + }) + }) + }) +}) From 70a04edacf757fa040f38adcb46430de01b4881f Mon Sep 17 00:00:00 2001 From: Jacob Heun Date: Wed, 17 Oct 2018 15:29:01 +0200 Subject: [PATCH 08/13] refactor: consilidate fsm dial methods and update readme --- README.md | 12 +++++++++--- src/index.js | 28 ++-------------------------- test/transports.browser.js | 4 ++-- test/transports.node.js | 4 ++-- 4 files changed, 15 insertions(+), 33 deletions(-) diff --git a/README.md b/README.md index 3ba5808828..5da60340ab 100644 --- a/README.md +++ b/README.md @@ -219,9 +219,7 @@ Required keys in the `options` object: > Dials to another peer in the network, establishes the connection. - `peer`: can be an instance of [PeerInfo][], [PeerId][], [multiaddr][], or a multiaddr string -- `callback`: Function with signature `function (err, conn) {}` where `conn` is a [Connection](https://github.com/libp2p/interface-connection) object - -`callback` is a function with the following `function (err, conn) {}` signature, where `err` is an Error in of failure to dial the connection and `conn` is a [Connection][] instance in case of a protocol selected, if not it is undefined. +- `callback` is a function with the following `function (err, conn) {}` signature, where `err` is an Error in of failure to dial the connection and `conn` is a [Connection][] instance in case of a protocol selected, if not it is undefined. #### `libp2p.dialProtocol(peer, protocol, callback)` @@ -233,6 +231,14 @@ Required keys in the `options` object: `callback` is a function with the following `function (err, conn) {}` signature, where `err` is an Error in of failure to dial the connection and `conn` is a [Connection][] instance in case of a protocol selected, if not it is undefined. +#### `libp2p.dialFSM(peer, protocol, callback)` + +> Behaves like `.dial` and `.dialProtocol` but calls back with a Connection State Machine + +- `peer`: can be an instance of [PeerInfo][], [PeerId][], [multiaddr][], or a multiaddr string +- `protocol`: an optional String that defines the protocol (e.g '/ipfs/bitswap/1.1.0') +- `callback`: Function with signature `function (err, connFSM) {}` where `connFSM` is a [Connection State Machine]() + #### `libp2p.hangUp(peer, callback)` > Closes an open connection with a peer, graciously. diff --git a/src/index.js b/src/index.js index 9477ca37be..ef08dbdb90 100644 --- a/src/index.js +++ b/src/index.js @@ -206,30 +206,6 @@ class Node extends EventEmitter { }) } - /** - * Similar to `dial`, but the callback will contain a - * Connection State Machine. - * - * @param {PeerInfo|PeerId|Multiaddr|string} peer The peer to dial - * @param {function(Error, ConnectionFSM)} callback - * @returns {void} - */ - dialFSM (peer, callback) { - assert(this.isStarted(), NOT_STARTED_ERROR_MESSAGE) - - this._getPeerInfo(peer, (err, peerInfo) => { - if (err) { return callback(err) } - - const connFSM = this._switch.dialFSM(peerInfo, (err) => { - if (!err) { - this.peerBook.put(peerInfo) - } - }) - - callback(null, connFSM) - }) - } - /** * Dials to the provided peer and handshakes with the given protocol. * If successful, the `PeerInfo` of the peer will be added to the nodes `PeerBook`, @@ -260,7 +236,7 @@ class Node extends EventEmitter { } /** - * Similar to `dialProtocol`, but the callback will contain a + * Similar to `dial` and `dialProtocol`, but the callback will contain a * Connection State Machine. * * @param {PeerInfo|PeerId|Multiaddr|string} peer The peer to dial @@ -268,7 +244,7 @@ class Node extends EventEmitter { * @param {function(Error, ConnectionFSM)} callback * @returns {void} */ - dialProtocolFSM (peer, protocol, callback) { + dialFSM (peer, protocol, callback) { assert(this.isStarted(), NOT_STARTED_ERROR_MESSAGE) if (typeof protocol === 'function') { diff --git a/test/transports.browser.js b/test/transports.browser.js index 75e31c3a60..92daa9e642 100644 --- a/test/transports.browser.js +++ b/test/transports.browser.js @@ -171,8 +171,8 @@ describe('transports', () => { }) }) - it('.dialFSMProtocol do an echo and close', (done) => { - nodeA.dialProtocolFSM(peerB, '/echo/1.0.0', (err, connFSM) => { + it('.dialFSM with a protocol, do an echo and close', (done) => { + nodeA.dialFSM(peerB, '/echo/1.0.0', (err, connFSM) => { expect(err).to.not.exist() connFSM.once('connection', (conn) => { tryEcho(conn, () => { diff --git a/test/transports.node.js b/test/transports.node.js index 12eee30d65..4b5b5c121e 100644 --- a/test/transports.node.js +++ b/test/transports.node.js @@ -231,8 +231,8 @@ describe('transports', () => { }) }) - it('.dialFSMProtocol do an echo and close', (done) => { - nodeA.dialProtocolFSM(nodeB.peerInfo, '/echo/1.0.0', (err, connFSM) => { + it('.dialFSM with a protocol, do an echo and close', (done) => { + nodeA.dialFSM(nodeB.peerInfo, '/echo/1.0.0', (err, connFSM) => { expect(err).to.not.exist() connFSM.once('connection', (conn) => { expect(nodeA._switch.muxedConns).to.have.all.keys([ From b4bf98b246a31101b7ce06733851c8cad52b3483 Mon Sep 17 00:00:00 2001 From: Jacob Heun Date: Thu, 18 Oct 2018 15:38:31 +0200 Subject: [PATCH 09/13] refactor: consilidate dial and dialProtocol code --- src/index.js | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/src/index.js b/src/index.js index ef08dbdb90..5263cfb2fb 100644 --- a/src/index.js +++ b/src/index.js @@ -194,16 +194,7 @@ class Node extends EventEmitter { dial (peer, callback) { assert(this.isStarted(), NOT_STARTED_ERROR_MESSAGE) - this._getPeerInfo(peer, (err, peerInfo) => { - if (err) { return callback(err) } - - this._switch.dial(peerInfo, (err) => { - if (err) { return callback(err) } - - this.peerBook.put(peerInfo) - callback() - }) - }) + this.dialProtocol(peer, null, callback) } /** From d678f17c6b8b789e6ea3f6a5c876a2eb39d78911 Mon Sep 17 00:00:00 2001 From: Jacob Heun Date: Thu, 18 Oct 2018 15:49:47 +0200 Subject: [PATCH 10/13] docs: add events to readme --- README.md | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 5da60340ab..fed9b4394c 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,7 @@ We've come a long way, but this project is still in Alpha, lots of development i - [Install](#install) - [Usage](#usage) - [API](#api) + - [Events](#events) - [Development](#development) - [Tests](#tests) - [Packages](#packages) @@ -280,19 +281,35 @@ Required keys in the `options` object: - `protocol`: String that defines the protocol (e.g '/ipfs/bitswap/1.1.0') -#### `libp2p.on('peer:discovery', (peer) => {})` +#### Events + +##### `libp2p.on('start', () => {})` + +> Libp2p has started, along with all its services. + +##### `libp2p.on('stop', () => {})` + +> Libp2p has stopped, along with all its services. + +##### `libp2p.on('error', (err) => {})` + +> An error has occurred + +- `err`: instance of `Error` + +##### `libp2p.on('peer:discovery', (peer) => {})` > Peer has been discovered. - `peer`: instance of [PeerInfo][] -#### `libp2p.on('peer:connect', (peer) => {})` +##### `libp2p.on('peer:connect', (peer) => {})` > We connected to a new peer - `peer`: instance of [PeerInfo][] -#### `libp2p.on('peer:disconnect', (peer) => {})` +##### `libp2p.on('peer:disconnect', (peer) => {})` > We disconnected from Peer From c0aa220cd1b51922d14ba215e0e849c42e160f0e Mon Sep 17 00:00:00 2001 From: Jacob Heun Date: Thu, 18 Oct 2018 15:51:47 +0200 Subject: [PATCH 11/13] docs: update readme link to connectionfsm docs --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index fed9b4394c..489c59128f 100644 --- a/README.md +++ b/README.md @@ -238,7 +238,7 @@ Required keys in the `options` object: - `peer`: can be an instance of [PeerInfo][], [PeerId][], [multiaddr][], or a multiaddr string - `protocol`: an optional String that defines the protocol (e.g '/ipfs/bitswap/1.1.0') -- `callback`: Function with signature `function (err, connFSM) {}` where `connFSM` is a [Connection State Machine]() +- `callback`: Function with signature `function (err, connFSM) {}` where `connFSM` is a [Connection State Machine](https://github.com/libp2p/js-libp2p-switch#connection-state-machine) #### `libp2p.hangUp(peer, callback)` From d3fe9bae98c3c38409326a37a11ff4139d77e7eb Mon Sep 17 00:00:00 2001 From: Jacob Heun Date: Fri, 19 Oct 2018 13:26:13 +0200 Subject: [PATCH 12/13] docs: update readme signatures and @fires usage --- README.md | 18 +++++++++--------- src/index.js | 10 ++++++++-- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 489c59128f..5c642d5c17 100644 --- a/README.md +++ b/README.md @@ -207,20 +207,20 @@ Required keys in the `options` object: > Start the libp2p Node. -`callback` is a function with the following `function (err) {}` signature, where `err` is an Error in case starting the node fails. +`callback` following signature `function (err) {}`, where `err` is an Error in case starting the node fails. #### `libp2p.stop(callback)` > Stop the libp2p Node. -`callback` is a function with the following `function (err) {}` signature, where `err` is an Error in case stopping the node fails. +`callback` following signature `function (err) {}`, where `err` is an Error in case stopping the node fails. #### `libp2p.dial(peer, callback)` > Dials to another peer in the network, establishes the connection. - `peer`: can be an instance of [PeerInfo][], [PeerId][], [multiaddr][], or a multiaddr string -- `callback` is a function with the following `function (err, conn) {}` signature, where `err` is an Error in of failure to dial the connection and `conn` is a [Connection][] instance in case of a protocol selected, if not it is undefined. +- `callback` following signature `function (err, conn) {}`, where `err` is an Error in of failure to dial the connection and `conn` is a [Connection][] instance in case of a protocol selected, if not it is undefined. #### `libp2p.dialProtocol(peer, protocol, callback)` @@ -228,9 +228,9 @@ Required keys in the `options` object: - `peer`: can be an instance of [PeerInfo][], [PeerId][], [multiaddr][], or a multiaddr string - `protocol`: String that defines the protocol (e.g '/ipfs/bitswap/1.1.0') -- `callback`: Function with signature `function (err, conn) {}` where `conn` is a [Connection](https://github.com/libp2p/interface-connection) object +- `callback`: Function with signature `function (err, conn) {}`, where `conn` is a [Connection](https://github.com/libp2p/interface-connection) object -`callback` is a function with the following `function (err, conn) {}` signature, where `err` is an Error in of failure to dial the connection and `conn` is a [Connection][] instance in case of a protocol selected, if not it is undefined. +`callback` following signature `function (err, conn) {}`, where `err` is an Error in of failure to dial the connection and `conn` is a [Connection][] instance in case of a protocol selected, if not it is undefined. #### `libp2p.dialFSM(peer, protocol, callback)` @@ -238,7 +238,7 @@ Required keys in the `options` object: - `peer`: can be an instance of [PeerInfo][], [PeerId][], [multiaddr][], or a multiaddr string - `protocol`: an optional String that defines the protocol (e.g '/ipfs/bitswap/1.1.0') -- `callback`: Function with signature `function (err, connFSM) {}` where `connFSM` is a [Connection State Machine](https://github.com/libp2p/js-libp2p-switch#connection-state-machine) +- `callback`: following signature `function (err, connFSM) {}`, where `connFSM` is a [Connection State Machine](https://github.com/libp2p/js-libp2p-switch#connection-state-machine) #### `libp2p.hangUp(peer, callback)` @@ -246,7 +246,7 @@ Required keys in the `options` object: - `peer`: can be an instance of [PeerInfo][], [PeerId][] or [multiaddr][] -`callback` is a function with the following `function (err) {}` signature, where `err` is an Error in case stopping the node fails. +`callback` following signature `function (err) {}`, where `err` is an Error in case stopping the node fails. #### `libp2p.peerRouting.findPeer(id, options, callback)` @@ -272,7 +272,7 @@ Required keys in the `options` object: > Handle new protocol - `protocol`: String that defines the protocol (e.g '/ipfs/bitswap/1.1.0') -- `handlerFunc`: Function with signature `function (protocol, conn) {}` where `conn` is a [Connection](https://github.com/libp2p/interface-connection) object +- `handlerFunc`: following signature `function (protocol, conn) {}`, where `conn` is a [Connection](https://github.com/libp2p/interface-connection) object - `matchFunc`: Function for matching on protocol (exact matching, semver, etc). Default to exact match. #### `libp2p.unhandle(protocol)` @@ -597,4 +597,4 @@ The libp2p implementation in JavaScript is a work in progress. As such, there ar ## License -[MIT](LICENSE) © David Dias +[MIT](LICENSE) © Protocol Labs diff --git a/src/index.js b/src/index.js index 5263cfb2fb..9d54b85395 100644 --- a/src/index.js +++ b/src/index.js @@ -26,6 +26,14 @@ const validateConfig = require('./config').validate const NOT_STARTED_ERROR_MESSAGE = 'The libp2p node is not started yet' +/** + * @fires Node#error Emitted when an error occurs + * @fires Node#peer:connect Emitted when a peer is connected to this node + * @fires Node#peer:disconnect Emitted when a peer disconnects from this node + * @fires Node#peer:discovery Emitted when a peer is discovered + * @fires Node#start Emitted when the node and its services has started + * @fires Node#stop Emitted when the node and its services has stopped + */ class Node extends EventEmitter { constructor (_options) { super() @@ -159,7 +167,6 @@ class Node extends EventEmitter { * Starts the libp2p node and all sub services * * @param {function(Error)} callback - * @fires Node#start * @returns {void} */ start (callback = () => {}) { @@ -171,7 +178,6 @@ class Node extends EventEmitter { * Stop the libp2p node by closing its listeners and open connections * * @param {function(Error)} callback - * @fires Node#stop * @returns {void} */ stop (callback = () => {}) { From 10c839049741c9a418be85c21078180357158c6e Mon Sep 17 00:00:00 2001 From: Jacob Heun Date: Fri, 19 Oct 2018 14:21:02 +0200 Subject: [PATCH 13/13] chore: update switch version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b0f768ac25..c8d71050dc 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,7 @@ "libp2p-connection-manager": "~0.0.2", "libp2p-floodsub": "~0.15.0", "libp2p-ping": "~0.8.0", - "libp2p-switch": "github:libp2p/js-libp2p-switch#feat/state-machine", + "libp2p-switch": "~0.41.0", "libp2p-websockets": "~0.12.0", "mafmt": "^6.0.2", "multiaddr": "^5.0.0",