Compare commits

..

49 Commits

Author SHA1 Message Date
d224d15507 chore: update multiaddr to 9 2021-04-08 17:28:49 +02:00
0b4fd039f8 chore: update pubsub tests 2021-04-07 20:37:07 +02:00
0cb709635f chore: update pubsub interface 2021-04-07 18:09:16 +02:00
edb8ca60e9 chore: release version v0.9.0 2021-04-07 09:44:27 +02:00
810642d8ff chore: update contributors 2021-04-07 09:44:26 +02:00
64a478d313 chore: update types (#87) 2021-04-07 09:39:48 +02:00
b948972c5b chore: release version v0.8.4 2021-03-22 10:03:40 +01:00
edcedf093d chore: update contributors 2021-03-22 10:03:39 +01:00
3b960d516f fix: specify connection direction (#86)
The comment says it can be 'inbound' or 'outbound' so constrain the type to those values.
2021-03-22 09:58:10 +01:00
2429e7246f chore: release version v0.8.3 2021-01-26 09:24:52 +01:00
22a7f6a9f1 chore: update contributors 2021-01-26 09:24:52 +01:00
fb9fce8713 chore: update deps (#82)
* chore: update deps

* chore: add prepare script
2021-01-26 09:11:00 +01:00
91dba97125 chore: release version v0.8.2 2021-01-20 10:19:40 +01:00
93d712bb0e chore: update contributors 2021-01-20 10:19:40 +01:00
ca520775eb fix: event emitter types with local types (#80) 2021-01-20 10:15:40 +01:00
ceb91c672e chore: add github actions badge (#78) 2020-12-17 11:31:12 +01:00
30ffad42c8 chore: release version v0.8.1 2020-12-11 10:06:07 +01:00
c365399e4f chore: update contributors 2020-12-11 10:06:07 +01:00
5b99e6b56b fix: pubsub publish message should be uint8array (#77) 2020-12-11 10:01:39 +01:00
f4c19c9ef2 chore: release version v0.8.0 2020-12-10 14:29:13 +01:00
c36698f488 chore: update contributors 2020-12-10 14:29:13 +01:00
3ecbc3e889 chore: remove old pre release script (#76) 2020-12-10 14:23:35 +01:00
e2419ea308 feat: add types (#74) 2020-12-10 14:03:17 +01:00
04e93d3f08 chore: add pubsub interface to readme (#72) 2020-11-25 18:57:25 +01:00
83d7d52d7e chore: release version v0.7.2 2020-11-11 17:20:34 +01:00
1a3ea82776 chore: update contributors 2020-11-11 17:20:34 +01:00
ad2dfa42dc chore: pubsub conformance test updates (#70) 2020-11-11 17:16:49 +01:00
b75f2cab48 chore: release version v0.7.1 2020-11-03 22:43:57 +01:00
8512997e76 chore: update contributors 2020-11-03 22:43:57 +01:00
269a6f5e0a fix: typescript types (#69) 2020-11-03 22:35:18 +01:00
14d09970ca chore: release version v0.7.0 2020-11-03 18:26:50 +01:00
c98c58e824 chore: update contributors 2020-11-03 18:26:49 +01:00
946b046440 feat: pubsub: add global signature policy (#66)
BREAKING CHANGE:
`signMessages` and `strictSigning` pubsub configuration options replaced
with a `globalSignaturePolicy` option
2020-11-03 18:22:03 +01:00
d168c7d531 chore: release version v0.6.0 2020-10-05 16:40:42 +02:00
349c1174db chore: update contributors 2020-10-05 16:40:41 +02:00
e14844315b feat: update pubsub getMsgId return type to Uint8Array (#65)
BREAKING CHANGE:
new getMsgId return type is not backwards compatible with prior `string`
return type.
2020-10-05 16:36:29 +02:00
ff3bd10704 chore: release version v0.5.2 2020-09-30 11:27:32 +02:00
4ecd7d3c83 chore: update contributors 2020-09-30 11:27:31 +02:00
eacdc246da chore: add type generation from jsdoc (#64) 2020-09-30 11:21:11 +02:00
c43e8e26bd chore: release version v0.5.1 2020-08-25 13:09:56 +02:00
94cf0fa4fc chore: update contributors 2020-08-25 13:09:56 +02:00
ba15a48dd9 feat: interface pubsub (#60)
* feat: interface pubsub

* chore: pubsub router tests

* chore: move pubsub abstractions from gossipsub

* chore: address review

* chore: revamp docs

* chore: add emit self tests to interface

* chore: refactor base tests

* chore: publish should only accept one topic per api call

* chore: normalize msg before emit

* chore: do not reset inbound stream

* chore: apply suggestions from code review

Co-authored-by: Jacob Heun <jacobheun@gmail.com>

* chore: address review

* fix: remove subscribe handler

* chore: remove bits from create peerId

Co-authored-by: Jacob Heun <jacobheun@gmail.com>

* chore: remove delay from topic validators tests

* chore: add event emitter information

* fix: topic validator docs

Co-authored-by: Jacob Heun <jacobheun@gmail.com>
2020-08-25 13:05:58 +02:00
c4be5eed4b chore: release version v0.5.0 2020-08-24 12:23:18 +02:00
9e0d23de9c chore: update contributors 2020-08-24 12:23:18 +02:00
4130e7f098 fix: replace remaining Buffer usage with Uint8Array (#62)
* fix: marshal record as Uint8Array

BREAKING CHANGE: records now marshal as Uint8Array instead of Buffer

* fix: refactor remaining Buffer usage to Uint8Array
2020-08-24 12:20:24 +02:00
d6376377d3 docs: update discovery tests usage (#61) 2020-08-17 18:10:55 +02:00
a60ae091e5 chore: release version v0.4.1 2020-08-11 15:26:28 +02:00
4902542e78 chore: update contributors 2020-08-11 15:26:27 +02:00
4d5f56d06f chore: update libp2p-tcp (#59)
Updates libp2p-tcp to a version that depends on the same multiaddr as this module.
2020-08-11 15:22:43 +02:00
70 changed files with 6246 additions and 350 deletions

53
.github/workflows/main.yml vendored Normal file
View File

@ -0,0 +1,53 @@
name: ci
on:
push:
branches:
- master
pull_request:
branches:
- master
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- run: yarn
- run: yarn lint
- uses: gozala/typescript-error-reporter-action@v1.0.4
- run: yarn build
- run: yarn aegir dep-check
- uses: ipfs/aegir/actions/bundle-size@master
name: size
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
test-node:
needs: check
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [windows-latest, ubuntu-latest, macos-latest]
node: [14, 15]
fail-fast: true
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node }}
- run: yarn
- run: npx nyc --reporter=lcov aegir test -t node -- --bail
- uses: codecov/codecov-action@v1
test-chrome:
needs: check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- run: yarn
- run: npx aegir test -t browser -t webworker --bail
test-firefox:
needs: check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- run: yarn
- run: npx aegir test -t browser -t webworker --bail -- --browsers FirefoxHeadless

View File

@ -1,40 +0,0 @@
language: node_js
cache: npm
stages:
- check
- test
- cov
node_js:
- '10'
- '12'
os:
- linux
- osx
- windows
script: npx nyc -s npm run test:node -- --bail
after_success: npx nyc report --reporter=text-lcov > coverage.lcov && npx codecov
jobs:
include:
- stage: check
script:
- npx aegir dep-check
- npm run lint
- stage: test
name: chrome
addons:
chrome: stable
script: npx aegir test -t browser -t webworker
- stage: test
name: firefox
addons:
firefox: latest
script: npx aegir test -t browser -t webworker -- --browsers FirefoxHeadless
notifications:
email: false

View File

@ -1,3 +1,160 @@
# [0.9.0](https://github.com/libp2p/js-interfaces/compare/v0.8.4...v0.9.0) (2021-04-07)
## [0.8.4](https://github.com/libp2p/js-interfaces/compare/v0.8.3...v0.8.4) (2021-03-22)
### Bug Fixes
* specify connection direction ([#86](https://github.com/libp2p/js-interfaces/issues/86)) ([3b960d5](https://github.com/libp2p/js-interfaces/commit/3b960d516f70f7e198574a736cb09000ddd7a94c))
## [0.8.3](https://github.com/libp2p/js-interfaces/compare/v0.8.2...v0.8.3) (2021-01-26)
## [0.8.2](https://github.com/libp2p/js-interfaces/compare/v0.8.1...v0.8.2) (2021-01-20)
### Bug Fixes
* event emitter types with local types ([#80](https://github.com/libp2p/js-interfaces/issues/80)) ([ca52077](https://github.com/libp2p/js-interfaces/commit/ca520775eb26f5ed501375fdb24ba698c9a8c8c8))
## [0.8.1](https://github.com/libp2p/js-interfaces/compare/v0.8.0...v0.8.1) (2020-12-11)
### Bug Fixes
* pubsub publish message should be uint8array ([#77](https://github.com/libp2p/js-interfaces/issues/77)) ([5b99e6b](https://github.com/libp2p/js-interfaces/commit/5b99e6b56b10439a82ee88fb4e31fb95c182264f))
# [0.8.0](https://github.com/libp2p/js-interfaces/compare/v0.7.2...v0.8.0) (2020-12-10)
### Features
* add types ([#74](https://github.com/libp2p/js-interfaces/issues/74)) ([e2419ea](https://github.com/libp2p/js-interfaces/commit/e2419ea308b5db38966850ba6349602c93ce3b0e))
<a name="0.7.2"></a>
## [0.7.2](https://github.com/libp2p/js-interfaces/compare/v0.7.1...v0.7.2) (2020-11-11)
<a name="0.7.1"></a>
## [0.7.1](https://github.com/libp2p/js-interfaces/compare/v0.7.0...v0.7.1) (2020-11-03)
### Bug Fixes
* typescript types ([#69](https://github.com/libp2p/js-interfaces/issues/69)) ([269a6f5](https://github.com/libp2p/js-interfaces/commit/269a6f5))
<a name="0.7.0"></a>
# [0.7.0](https://github.com/libp2p/js-interfaces/compare/v0.5.2...v0.7.0) (2020-11-03)
### Features
* pubsub: add global signature policy ([#66](https://github.com/libp2p/js-interfaces/issues/66)) ([946b046](https://github.com/libp2p/js-interfaces/commit/946b046))
* update pubsub getMsgId return type to Uint8Array ([#65](https://github.com/libp2p/js-interfaces/issues/65)) ([e148443](https://github.com/libp2p/js-interfaces/commit/e148443))
### BREAKING CHANGES
* `signMessages` and `strictSigning` pubsub configuration options replaced
with a `globalSignaturePolicy` option
* new getMsgId return type is not backwards compatible with prior `string`
return type.
<a name="0.6.0"></a>
# [0.6.0](https://github.com/libp2p/js-interfaces/compare/v0.5.2...v0.6.0) (2020-10-05)
### Features
* update pubsub getMsgId return type to Uint8Array ([#65](https://github.com/libp2p/js-interfaces/issues/65)) ([e148443](https://github.com/libp2p/js-interfaces/commit/e148443))
### BREAKING CHANGES
* new getMsgId return type is not backwards compatible with prior `string`
return type.
<a name="0.5.2"></a>
## [0.5.2](https://github.com/libp2p/js-interfaces/compare/v0.3.1...v0.5.2) (2020-09-30)
### Bug Fixes
* replace remaining Buffer usage with Uint8Array ([#62](https://github.com/libp2p/js-interfaces/issues/62)) ([4130e7f](https://github.com/libp2p/js-interfaces/commit/4130e7f))
### Chores
* update deps ([#57](https://github.com/libp2p/js-interfaces/issues/57)) ([75f6777](https://github.com/libp2p/js-interfaces/commit/75f6777))
### Features
* interface pubsub ([#60](https://github.com/libp2p/js-interfaces/issues/60)) ([ba15a48](https://github.com/libp2p/js-interfaces/commit/ba15a48))
* record interface ([#52](https://github.com/libp2p/js-interfaces/issues/52)) ([1cc943e](https://github.com/libp2p/js-interfaces/commit/1cc943e))
### BREAKING CHANGES
* records now marshal as Uint8Array instead of Buffer
* fix: refactor remaining Buffer usage to Uint8Array
* - The peer id dep of this module has replaced node Buffers with Uint8Arrays
* chore: update gh deps
<a name="0.5.1"></a>
## [0.5.1](https://github.com/libp2p/js-interfaces/compare/v0.5.0...v0.5.1) (2020-08-25)
### Features
* interface pubsub ([#60](https://github.com/libp2p/js-interfaces/issues/60)) ([ba15a48](https://github.com/libp2p/js-interfaces/commit/ba15a48))
<a name="0.5.0"></a>
# [0.5.0](https://github.com/libp2p/js-interfaces/compare/v0.4.1...v0.5.0) (2020-08-24)
### Bug Fixes
* replace remaining Buffer usage with Uint8Array ([#62](https://github.com/libp2p/js-interfaces/issues/62)) ([4130e7f](https://github.com/libp2p/js-interfaces/commit/4130e7f))
### BREAKING CHANGES
* records now marshal as Uint8Array instead of Buffer
* fix: refactor remaining Buffer usage to Uint8Array
<a name="0.4.1"></a>
## [0.4.1](https://github.com/libp2p/js-interfaces/compare/v0.4.0...v0.4.1) (2020-08-11)
<a name="0.4.0"></a>
# [0.4.0](https://github.com/libp2p/js-interfaces/compare/v0.3.2...v0.4.0) (2020-08-10)

View File

@ -4,6 +4,8 @@
[![](https://img.shields.io/badge/project-libp2p-yellow.svg?style=flat-square)](http://libp2p.io/)
[![](https://img.shields.io/badge/freenode-%23libp2p-yellow.svg?style=flat-square)](http://webchat.freenode.net/?channels=%23libp2p)
[![](https://img.shields.io/discourse/https/discuss.libp2p.io/posts.svg)](https://discuss.libp2p.io)
[![codecov](https://img.shields.io/codecov/c/github/libp2p/js-libp2p-interfaces.svg?style=flat-square)](https://codecov.io/gh/libp2p/js-libp2p-interfaces)
[![GitHub Workflow Status](https://img.shields.io/github/workflow/status/libp2p/js-libp2p-interfaces/ci?label=ci&style=flat-square)](https://github.com/libp2p/js-libp2p-interfaces/actions?query=branch%3Amaster+workflow%3Aci+)
> Contains test suites and interfaces you can use to implement the various components of libp2p.
@ -18,6 +20,7 @@
- [Crypto](./src/crypto)
- [Peer Discovery](./src/peer-discovery)
- [Peer Routing](./src/peer-routing)
- [Pubsub](./src/pubsub)
- [Record](./src/record)
- [Stream Muxer](./src/stream-muxer)
- [Topology](./src/topology)
@ -30,6 +33,7 @@ For posterity, here are links to the original repositories for each of the inter
- [Content Routing](https://github.com/libp2p/interface-content-routing)
- [Peer Discovery](https://github.com/libp2p/interface-peer-discovery)
- [Peer Routing](https://github.com/libp2p/interface-peer-routing)
- [Pubsub](https://github.com/libp2p/js-libp2p-pubsub)
- [Stream Muxer](https://github.com/libp2p/interface-stream-muxer)
- [Transport](https://github.com/libp2p/interface-transport)

View File

@ -1,16 +1,37 @@
{
"name": "libp2p-interfaces",
"version": "0.4.0",
"version": "0.9.0",
"description": "Interfaces for JS Libp2p",
"leadMaintainer": "Jacob Heun <jacobheun@gmail.com>",
"main": "src/index.js",
"files": [
"src",
"types",
"dist"
],
"types": "dist/src/index.d.ts",
"typesVersions": {
"*": {
"src/*": [
"dist/src/*",
"dist/src/*/index"
]
}
},
"eslintConfig": {
"extends": "ipfs"
},
"scripts": {
"prepare": "aegir build --no-bundle",
"lint": "aegir lint",
"build": "aegir build",
"build": "npm run build:proto && npm run build:proto-types && npm run build:types",
"build:types": "aegir build --no-bundle",
"build:proto": "npm run build:proto:rpc && npm run build:proto:topic-descriptor",
"build:proto:rpc": "pbjs -t static-module -w commonjs --force-number --no-verify --no-delimited --no-create --no-beautify --no-defaults --lint eslint-disable -o src/pubsub/message/rpc.js ./src/pubsub/message/rpc.proto",
"build:proto:topic-descriptor": "pbjs -t static-module -w commonjs --force-number --no-verify --no-delimited --no-create --no-beautify --no-defaults --lint eslint-disable -o src/pubsub/message/topic-descriptor.js ./src/pubsub/message/topic-descriptor.proto",
"build:proto-types": "npm run build:proto-types:rpc && npm run build:proto-types:topic-descriptor",
"build:proto-types:rpc": "pbts -o src/pubsub/message/rpc.d.ts src/pubsub/message/rpc.js",
"build:proto-types:topic-descriptor": "pbts -o src/pubsub/message/topic-descriptor.d.ts src/pubsub/message/topic-descriptor.js",
"test": "aegir test",
"test:node": "aegir test --target node",
"test:browser": "aegir test --target browser",
@ -33,31 +54,43 @@
},
"homepage": "https://github.com/libp2p/js-interfaces#readme",
"dependencies": {
"@types/bl": "^4.1.0",
"abort-controller": "^3.0.0",
"abortable-iterator": "^3.0.0",
"buffer": "^5.6.0",
"chai": "^4.2.0",
"chai": "^4.3.4",
"chai-checkmark": "^1.0.1",
"class-is": "^1.1.0",
"delay": "^4.3.0",
"debug": "^4.3.1",
"delay": "^5.0.0",
"detect-node": "^2.0.4",
"dirty-chai": "^2.0.1",
"err-code": "^2.0.0",
"it-goodbye": "^2.0.1",
"err-code": "^3.0.1",
"it-goodbye": "^3.0.0",
"it-length-prefixed": "^5.0.0",
"it-pair": "^1.0.0",
"it-pipe": "^1.1.0",
"libp2p-tcp": "^0.14.5",
"multiaddr": "^8.0.0",
"it-pushable": "^1.4.2",
"libp2p-crypto": "^0.19.0",
"libp2p-tcp": "^0.15.3",
"multiaddr": "^9.0.1",
"multibase": "^4.0.2",
"multihashes": "^4.0.2",
"p-defer": "^3.0.0",
"p-limit": "^2.3.0",
"p-wait-for": "^3.1.0",
"peer-id": "^0.14.0",
"sinon": "^9.0.2",
"streaming-iterables": "^5.0.2"
"p-limit": "^3.1.0",
"p-wait-for": "^3.2.0",
"peer-id": "^0.14.2",
"protobufjs": "^6.10.2",
"sinon": "^10.0.0",
"streaming-iterables": "^5.0.4",
"uint8arrays": "^2.1.3"
},
"devDependencies": {
"aegir": "^25.0.0",
"it-handshake": "^1.0.1"
"@types/debug": "^4.1.5",
"aegir": "^33.0.0",
"cids": "^1.1.6",
"events": "^3.3.0",
"it-handshake": "^1.0.2",
"rimraf": "^3.0.2",
"util": "^0.12.3"
},
"contributors": [
"Alan Shaw <alan.shaw@protocol.ai>",

View File

@ -1,13 +1,246 @@
'use strict'
const PeerId = require('peer-id')
const multiaddr = require('multiaddr')
const withIs = require('class-is')
const { Multiaddr } = require('multiaddr')
const errCode = require('err-code')
const Status = require('./status')
const { OPEN, CLOSING, CLOSED } = require('./status')
const connectionSymbol = Symbol.for('@libp2p/interface-connection/connection')
/**
* @typedef {import('../stream-muxer/types').MuxedStream} MuxedStream
* @typedef {import('./status').Status} Status
*/
/**
* @typedef {Object} Timeline
* @property {number} open - connection opening timestamp.
* @property {number} [upgraded] - connection upgraded timestamp.
* @property {number} [close]
*
* @typedef {Object} ConectionStat
* @property {'inbound' | 'outbound'} direction - connection establishment direction
* @property {Timeline} timeline - connection relevant events timestamp.
* @property {string} [multiplexer] - connection multiplexing identifier.
* @property {string} [encryption] - connection encryption method identifier.
*
* @typedef {Object} ConnectionOptions
* @property {Multiaddr} [localAddr] - local multiaddr of the connection if known.
* @property {Multiaddr} remoteAddr - remote multiaddr of the connection.
* @property {PeerId} localPeer - local peer-id.
* @property {PeerId} remotePeer - remote peer-id.
* @property {(protocols: string|string[]) => Promise<{stream: MuxedStream, protocol: string}>} newStream - new stream muxer function.
* @property {() => Promise<void>} close - close raw connection function.
* @property {() => MuxedStream[]} getStreams - get streams from muxer function.
* @property {ConectionStat} stat - metadata of the connection.
*
* @typedef {Object} StreamData
* @property {string} protocol - the protocol used by the stream
* @property {Object} [metadata] - metadata of the stream
*/
/**
* An implementation of the js-libp2p connection.
* Any libp2p transport should use an upgrader to return this connection.
*/
class Connection {
/**
* An implementation of the js-libp2p connection.
* Any libp2p transport should use an upgrader to return this connection.
*
* @class
* @param {ConnectionOptions} options
*/
constructor ({ localAddr, remoteAddr, localPeer, remotePeer, newStream, close, getStreams, stat }) {
validateArgs(localAddr, localPeer, remotePeer, newStream, close, getStreams, stat)
/**
* Connection identifier.
*/
this.id = (parseInt(String(Math.random() * 1e9))).toString(36) + Date.now()
/**
* Observed multiaddr of the local peer
*/
this.localAddr = localAddr
/**
* Observed multiaddr of the remote peer
*/
this.remoteAddr = remoteAddr
/**
* Local peer id.
*/
this.localPeer = localPeer
/**
* Remote peer id.
*/
this.remotePeer = remotePeer
/**
* Connection metadata.
*
* @type {ConectionStat & {status: Status}}
*/
this._stat = {
...stat,
status: OPEN
}
/**
* Reference to the new stream function of the multiplexer
*/
this._newStream = newStream
/**
* Reference to the close function of the raw connection
*/
this._close = close
/**
* Reference to the getStreams function of the muxer
*/
this._getStreams = getStreams
/**
* Connection streams registry
*/
this.registry = new Map()
/**
* User provided tags
*
* @type {string[]}
*/
this.tags = []
}
get [Symbol.toStringTag] () {
return 'Connection'
}
get [connectionSymbol] () {
return true
}
/**
* Checks if the given value is a `Connection` instance.
*
* @param {any} other
* @returns {other is Connection}
*/
static isConnection (other) {
return Boolean(other && other[connectionSymbol])
}
/**
* Get connection metadata
*
* @this {Connection}
*/
get stat () {
return this._stat
}
/**
* Get all the streams of the muxer.
*
* @this {Connection}
*/
get streams () {
return this._getStreams()
}
/**
* Create a new stream from this connection
*
* @param {string|string[]} protocols - intended protocol for the stream
* @returns {Promise<{stream: MuxedStream, protocol: string}>} with muxed+multistream-selected stream and selected protocol
*/
async newStream (protocols) {
if (this.stat.status === CLOSING) {
throw errCode(new Error('the connection is being closed'), 'ERR_CONNECTION_BEING_CLOSED')
}
if (this.stat.status === CLOSED) {
throw errCode(new Error('the connection is closed'), 'ERR_CONNECTION_CLOSED')
}
if (!Array.isArray(protocols)) protocols = [protocols]
const { stream, protocol } = await this._newStream(protocols)
this.addStream(stream, { protocol })
return {
stream,
protocol
}
}
/**
* Add a stream when it is opened to the registry.
*
* @param {MuxedStream} muxedStream - a muxed stream
* @param {StreamData} data - the stream data to be registered
* @returns {void}
*/
addStream (muxedStream, { protocol, metadata = {} }) {
// Add metadata for the stream
this.registry.set(muxedStream.id, {
protocol,
...metadata
})
}
/**
* Remove stream registry after it is closed.
*
* @param {string} id - identifier of the stream
*/
removeStream (id) {
this.registry.delete(id)
}
/**
* Close the connection.
*
* @returns {Promise<void>}
*/
async close () {
if (this.stat.status === CLOSED) {
return
}
if (this._closing) {
return this._closing
}
this.stat.status = CLOSING
// Close raw connection
this._closing = await this._close()
this._stat.timeline.close = Date.now()
this.stat.status = CLOSED
}
}
module.exports = Connection
/**
* @param {Multiaddr|undefined} localAddr
* @param {PeerId} localPeer
* @param {PeerId} remotePeer
* @param {(protocols: string | string[]) => Promise<{ stream: import("../stream-muxer/types").MuxedStream; protocol: string; }>} newStream
* @param {() => Promise<void>} close
* @param {() => import("../stream-muxer/types").MuxedStream[]} getStreams
* @param {{ direction: any; timeline: any; multiplexer?: string | undefined; encryption?: string | undefined; }} stat
*/
function validateArgs (localAddr, localPeer, remotePeer, newStream, close, getStreams, stat) {
if (localAddr && !multiaddr.isMultiaddr(localAddr)) {
if (localAddr && !Multiaddr.isMultiaddr(localAddr)) {
throw errCode(new Error('localAddr must be an instance of multiaddr'), 'ERR_INVALID_PARAMETERS')
}
@ -51,179 +284,3 @@ function validateArgs (localAddr, localPeer, remotePeer, newStream, close, getSt
throw errCode(new Error('connection upgraded timestamp must be provided'), 'ERR_INVALID_PARAMETERS')
}
}
/**
* An implementation of the js-libp2p connection.
* Any libp2p transport should use an upgrader to return this connection.
*/
class Connection {
/**
* Creates an instance of Connection.
* @param {object} properties properties of the connection.
* @param {multiaddr} [properties.localAddr] local multiaddr of the connection if known.
* @param {multiaddr} [properties.remoteAddr] remote multiaddr of the connection.
* @param {PeerId} properties.localPeer local peer-id.
* @param {PeerId} properties.remotePeer remote peer-id.
* @param {function} properties.newStream new stream muxer function.
* @param {function} properties.close close raw connection function.
* @param {function} properties.getStreams get streams from muxer function.
* @param {object} properties.stat metadata of the connection.
* @param {string} properties.stat.direction connection establishment direction ("inbound" or "outbound").
* @param {object} properties.stat.timeline connection relevant events timestamp.
* @param {string} properties.stat.timeline.open connection opening timestamp.
* @param {string} properties.stat.timeline.upgraded connection upgraded timestamp.
* @param {string} [properties.stat.multiplexer] connection multiplexing identifier.
* @param {string} [properties.stat.encryption] connection encryption method identifier.
*/
constructor ({ localAddr, remoteAddr, localPeer, remotePeer, newStream, close, getStreams, stat }) {
validateArgs(localAddr, localPeer, remotePeer, newStream, close, getStreams, stat)
/**
* Connection identifier.
*/
this.id = (parseInt(Math.random() * 1e9)).toString(36) + Date.now()
/**
* Observed multiaddr of the local peer
*/
this.localAddr = localAddr
/**
* Observed multiaddr of the remote peer
*/
this.remoteAddr = remoteAddr
/**
* Local peer id.
*/
this.localPeer = localPeer
/**
* Remote peer id.
*/
this.remotePeer = remotePeer
/**
* Connection metadata.
*/
this._stat = {
...stat,
status: Status.OPEN
}
/**
* Reference to the new stream function of the multiplexer
*/
this._newStream = newStream
/**
* Reference to the close function of the raw connection
*/
this._close = close
/**
* Reference to the getStreams function of the muxer
*/
this._getStreams = getStreams
/**
* Connection streams registry
*/
this.registry = new Map()
/**
* User provided tags
*/
this.tags = []
}
/**
* Get connection metadata
* @return {Object}
*/
get stat () {
return this._stat
}
/**
* Get all the streams of the muxer.
* @return {Array<*>}
*/
get streams () {
return this._getStreams()
}
/**
* Create a new stream from this connection
* @param {string[]} protocols intended protocol for the stream
* @return {Promise<object>} with muxed+multistream-selected stream and selected protocol
*/
async newStream (protocols) {
if (this.stat.status === Status.CLOSING) {
throw errCode(new Error('the connection is being closed'), 'ERR_CONNECTION_BEING_CLOSED')
}
if (this.stat.status === Status.CLOSED) {
throw errCode(new Error('the connection is closed'), 'ERR_CONNECTION_CLOSED')
}
if (!Array.isArray(protocols)) protocols = [protocols]
const { stream, protocol } = await this._newStream(protocols)
this.addStream(stream, { protocol })
return {
stream,
protocol
}
}
/**
* Add a stream when it is opened to the registry.
* @param {*} muxedStream a muxed stream
* @param {object} properties the stream properties to be registered
* @param {string} properties.protocol the protocol used by the stream
* @param {object} properties.metadata metadata of the stream
* @return {void}
*/
addStream (muxedStream, { protocol, metadata = {} }) {
// Add metadata for the stream
this.registry.set(muxedStream.id, {
protocol,
...metadata
})
}
/**
* Remove stream registry after it is closed.
* @param {string} id identifier of the stream
*/
removeStream (id) {
this.registry.delete(id)
}
/**
* Close the connection.
* @return {Promise}
*/
async close () {
if (this.stat.status === Status.CLOSED) {
return
}
if (this._closing) {
return this._closing
}
this.stat.status = Status.CLOSING
// Close raw connection
this._closing = await this._close()
this._stat.timeline.close = Date.now()
this.stat.status = Status.CLOSED
}
}
module.exports = withIs(Connection, { className: 'Connection', symbolName: '@libp2p/interface-connection/connection' })

View File

@ -1,7 +1,12 @@
'use strict'
module.exports = {
OPEN: 'open',
CLOSING: 'closing',
CLOSED: 'closed'
const STATUS = {
OPEN: /** @type {'open'} */('open'),
CLOSING: /** @type {'closing'} */('closing'),
CLOSED: /** @type {'closed'} */('closed')
}
module.exports = STATUS
/**
* @typedef {STATUS[keyof STATUS]} Status
*/

View File

@ -1,3 +1,4 @@
// @ts-nocheck interface tests
/* eslint-env mocha */
'use strict'
@ -74,6 +75,7 @@ module.exports = (test) => {
let timelineProxy
const proxyHandler = {
set () {
// @ts-ignore - TS fails to infer here
return Reflect.set(...arguments)
}
}
@ -138,7 +140,9 @@ module.exports = (test) => {
expect(connection.stat.timeline.close).to.not.exist()
await connection.close()
// @ts-ignore - fails to infer callCount
expect(proxyHandler.set.callCount).to.equal(1)
// @ts-ignore - fails to infer getCall
const [obj, key, value] = proxyHandler.set.getCall(0).args
expect(obj).to.eql(connection.stat.timeline)
expect(key).to.equal('close')

View File

@ -1,3 +1,4 @@
// @ts-nocheck interface tests
/* eslint-env mocha */
'use strict'

11
src/content-routing/types.d.ts vendored Normal file
View File

@ -0,0 +1,11 @@
export = ContentRouting;
import PeerId from 'peer-id'
import Multiaddr from 'multiaddr'
import CID from 'cids'
declare class ContentRouting {
constructor (options: Object);
provide (cid: CID): Promise<void>;
findProviders (cid: CID, options: Object): AsyncIterable<{ id: PeerId, multiaddrs: Multiaddr[] }>;
}

View File

@ -1,17 +1,20 @@
// @ts-nocheck interface tests
/* eslint-env mocha */
'use strict'
const { Buffer } = require('buffer')
const duplexPair = require('it-pair/duplex')
const pipe = require('it-pipe')
const peers = require('../../utils/peers')
const { UnexpectedPeerError } = require('../errors')
const PeerId = require('peer-id')
const { collect } = require('streaming-iterables')
const chai = require('chai')
const expect = chai.expect
chai.use(require('dirty-chai'))
const duplexPair = require('it-pair/duplex')
const { pipe } = require('it-pipe')
const PeerId = require('peer-id')
const { collect } = require('streaming-iterables')
const uint8arrayFromString = require('uint8arrays/from-string')
const peers = require('../../utils/peers')
const { UnexpectedPeerError } = require('../errors')
module.exports = (common) => {
describe('interface-crypto', () => {
let crypto
@ -55,7 +58,7 @@ module.exports = (common) => {
pipe(inboundResult.conn, inboundResult.conn)
// Send some data and collect the result
const input = Buffer.from('data to encrypt')
const input = uint8arrayFromString('data to encrypt')
const result = await pipe(
[input],
outboundResult.conn,

24
src/crypto/types.d.ts vendored Normal file
View File

@ -0,0 +1,24 @@
import PeerId from 'peer-id'
import { MultiaddrConnection } from '../transport/types'
/**
* A libp2p crypto module must be compliant to this interface
* to ensure all exchanged data between two peers is encrypted.
*/
export interface Crypto {
protocol: string;
/**
* Encrypt outgoing data to the remote party.
*/
secureOutbound(localPeer: PeerId, connection: MultiaddrConnection, remotePeer: PeerId): Promise<SecureOutbound>;
/**
* Decrypt incoming data.
*/
secureInbound(localPeer: PeerId, connection: MultiaddrConnection, remotePeer?: PeerId): Promise<SecureOutbound>;
}
export type SecureOutbound = {
conn: MultiaddrConnection;
remoteEarlyData: Buffer;
remotePeer: PeerId;
}

View File

@ -33,19 +33,19 @@ Include this badge in your readme if you make a new module that uses interface-p
Install `interface-discovery` as one of the dependencies of your project and as a test file. Then, using `mocha` (for JavaScript) or a test runner with compatible API, do:
```js
const test = require('interface-discovery')
const tests = require('libp2p-interfaces/src/peer-discovery/tests')
const common = {
setup () {
return YourDiscovery
},
teardown () {
// Clean up any resources created by setup()
}
}
// use all of the test suits
test(common)
describe('your discovery', () => {
// use all of the test suits
tests({
setup () {
return YourDiscovery
},
teardown () {
// Clean up any resources created by setup()
}
})
})
```
## API

View File

@ -1,3 +1,4 @@
// @ts-nocheck interface tests
/* eslint-env mocha */
'use strict'
@ -5,7 +6,7 @@ const chai = require('chai')
const expect = chai.expect
chai.use(require('dirty-chai'))
const multiaddr = require('multiaddr')
const { Multiaddr } = require('multiaddr')
const PeerId = require('peer-id')
const delay = require('delay')
@ -54,7 +55,7 @@ module.exports = (common) => {
expect(PeerId.isPeerId(id)).to.eql(true)
expect(multiaddrs).to.exist()
multiaddrs.forEach((m) => expect(multiaddr.isMultiaddr(m)).to.eql(true))
multiaddrs.forEach((m) => expect(Multiaddr.isMultiaddr(m)).to.eql(true))
defer.resolve()
})

10
src/peer-discovery/types.d.ts vendored Normal file
View File

@ -0,0 +1,10 @@
export = PeerDiscovery;
import events from 'events';
declare class PeerDiscovery extends events.EventEmitter {
constructor (options: Object);
start (): Promise<void>;
stop (): Promise<void>;
tag: string;
}

10
src/peer-routing/types.d.ts vendored Normal file
View File

@ -0,0 +1,10 @@
export = PeerRouting;
import PeerId from 'peer-id'
import Multiaddr from 'multiaddr'
declare class PeerRouting {
constructor (options?: Object);
findPeer (peerId: PeerId, options?: Object): Promise<{ id: PeerId, multiaddrs: Multiaddr[] }>;
getClosestPeers(key: Uint8Array, options?: Object): AsyncIterable<{ id: PeerId, multiaddrs: Multiaddr[] }>;
}

255
src/pubsub/README.md Normal file
View File

@ -0,0 +1,255 @@
interface-pubsub
==================
The `interface-pubsub` contains the base implementation for a libp2p pubsub router implementation. This interface should be used to implement a pubsub router compatible with libp2p. It includes a test suite that pubsub routers should run, in order to ensure compatibility with libp2p.
Table of Contents
=================
* [Implementations using this interface](#implementations-using-this-interface)
* [Interface usage](#interface-usage)
* [Extend interface](#extend-interface)
* [Example](#example)
* [API](#api)
* [Constructor](#constructor)
* [new Pubsub(options)](#new-pubsuboptions)
* [Parameters](#parameters)
* [Start](#start)
* [pubsub.start()](#pubsubstart)
* [Returns](#returns)
* [Stop](#stop)
* [pubsub.stop()](#pubsubstop)
* [Returns](#returns-1)
* [Publish](#publish)
* [pubsub.publish(topics, message)](#pubsubpublishtopics-message)
* [Parameters](#parameters-1)
* [Returns](#returns-2)
* [Subscribe](#subscribe)
* [pubsub.subscribe(topic)](#pubsubsubscribetopic)
* [Parameters](#parameters-2)
* [Unsubscribe](#unsubscribe)
* [pubsub.unsubscribe(topic)](#pubsubunsubscribetopic)
* [Parameters](#parameters-3)
* [Get Topics](#get-topics)
* [pubsub.getTopics()](#pubsubgettopics)
* [Returns](#returns-3)
* [Get Peers Subscribed to a topic](#get-peers-subscribed-to-a-topic)
* [pubsub.getSubscribers(topic)](#pubsubgetsubscriberstopic)
* [Parameters](#parameters-4)
* [Returns](#returns-4)
* [Validate](#validate)
* [pubsub.validate(message)](#pubsubvalidatemessage)
* [Parameters](#parameters-5)
* [Returns](#returns-5)
* [Test suite usage](#test-suite-usage)
## Implementations using this interface
You can check the following implementations as examples for building your own pubsub router.
- [libp2p/js-libp2p-floodsub](https://github.com/libp2p/js-libp2p-floodsub)
- [ChainSafe/js-libp2p-gossipsub](https://github.com/ChainSafe/js-libp2p-gossipsub)
## Interface usage
`interface-pubsub` abstracts the implementation protocol registration within `libp2p` and takes care of all the protocol connections and streams, as well as the subscription management and the features describe in the libp2p [pubsub specs](https://github.com/libp2p/specs/tree/master/pubsub). This way, a pubsub implementation can focus on its message routing algorithm, instead of also needing to create the setup for it.
### Extend interface
A pubsub router implementation should start by extending the `interface-pubsub` class and **MUST** override the `_publish` function, according to the router algorithms. This function is responsible for forwarding publish messages to other peers, as well as forwarding received messages if the router provides the `canRelayMessage` option to the base implementation.
Other functions, such as `start`, `stop`, `subscribe`, `unsubscribe`, `_encodeRpc`, `_decodeRpc`, `_processRpcMessage`, `_addPeer` and `_removePeer` may be overwritten if the pubsub implementation needs to customize their logic. Implementations overriding these functions **MUST** call `super`.
The `start` and `stop` functions are responsible for the registration of the pubsub protocol with libp2p. The `stop` function also guarantees that the open streams in the protocol are properly closed.
The `subscribe` and `unsubscribe` functions take care of the subscription management and its inherent message propagation.
When using a custom protobuf definition for message marshalling, you should override `_encodeRpc` and `_decodeRpc` to use the new protobuf instead of the default one.
`_processRpcMessage` is responsible for handling messages received from other peers. This should be extended if further operations/validations are needed by the router.
The `_addPeer` and `_removePeer` functions are called when new peers running the pubsub router protocol establish a connection with the peer. They are used for tracking the open streams between the peers.
All the remaining functions **MUST NOT** be overwritten.
### Example
The following example aims to show how to create your pubsub implementation extending this base protocol. The pubsub implementation will handle the subscriptions logic.
```JavaScript
const Pubsub = require('libp2p-interfaces/src/pubsub')
class PubsubImplementation extends Pubsub {
constructor({ libp2p, options })
super({
debugName: 'libp2p:pubsub',
multicodecs: '/pubsub-implementation/1.0.0',
libp2p,
globalSigningPolicy: options.globalSigningPolicy
})
}
_publish (message) {
// Required to be implemented by the subclass
// Routing logic for the message
}
}
```
## API
The interface aims to specify a common interface that all pubsub router implementation should follow. A pubsub router implementation should extend the [EventEmitter](https://nodejs.org/api/events.html#events_class_eventemitter). When peers receive pubsub messages, these messages will be emitted by the event emitter where the `eventName` will be the `topic` associated with the message.
### Constructor
The base class constructor configures the pubsub instance for use with a libp2p instance. It includes settings for logging, signature policies, etc.
#### `new Pubsub({options})`
##### Parameters
| Name | Type | Description | Default |
|------|------|-------------|---------|
| options.libp2p | `Libp2p` | libp2p instance | required, no default |
| options.debugName | `string` | log namespace | required, no default |
| options.multicodecs | `string \| Array<string>` | protocol identifier(s) | required, no default |
| options.globalSignaturePolicy | `'StrictSign' \| 'StrictNoSign'` | signature policy to be globally applied | `'StrictSign'` |
| options.canRelayMessage | `boolean` | if can relay messages if not subscribed | `false` |
| options.emitSelf | `boolean` | if `publish` should emit to self, if subscribed | `false` |
### Start
Starts the pubsub subsystem. The protocol will be registered to `libp2p`, which will result in pubsub being notified when peers who support the protocol connect/disconnect to `libp2p`.
#### `pubsub.start()`
### Stop
Stops the pubsub subsystem. The protocol will be unregistered from `libp2p`, which will remove all listeners for the protocol and the established connections will be closed.
#### `pubsub.stop()`
### Publish
Publish data message to pubsub topics.
#### `pubsub.publish(topic, message)`
##### Parameters
| Name | Type | Description |
|------|------|-------------|
| topic | `string` | pubsub topic |
| message | `Uint8Array` | message to publish |
##### Returns
| Type | Description |
|------|-------------|
| `Promise<void>` | resolves once the message is published to the network |
### Subscribe
Subscribe to the given topic.
#### `pubsub.subscribe(topic)`
##### Parameters
| Name | Type | Description |
|------|------|-------------|
| topic | `string` | pubsub topic |
### Unsubscribe
Unsubscribe from the given topic.
#### `pubsub.unsubscribe(topic)`
##### Parameters
| Name | Type | Description |
|------|------|-------------|
| topic | `string` | pubsub topic |
### Get Topics
Get the list of topics which the peer is subscribed to.
#### `pubsub.getTopics()`
##### Returns
| Type | Description |
|------|-------------|
| `Array<String>` | Array of subscribed topics |
### Get Peers Subscribed to a topic
Get a list of the [PeerId](https://github.com/libp2p/js-peer-id) strings that are subscribed to one topic.
#### `pubsub.getSubscribers(topic)`
##### Parameters
| Name | Type | Description |
|------|------|-------------|
| topic | `string` | pubsub topic |
##### Returns
| Type | Description |
|------|-------------|
| `Array<string>` | Array of base-58 PeerId's |
### Validate
Validates a message according to the signature policy and topic-specific validation function.
#### `pubsub.validate(message)`
##### Parameters
| Name | Type | Description |
|------|------|-------------|
| message | `Message` | a pubsub message |
#### Returns
| Type | Description |
|------|-------------|
| `Promise<void>` | resolves if the message is valid |
## Test suite usage
```js
'use strict'
const tests = require('libp2p-interfaces/src/pubsub/tests')
const YourPubsubRouter = require('../src')
describe('compliance', () => {
let peers
let pubsubNodes = []
tests({
async setup (number = 1, options = {}) {
// Create number pubsub nodes with libp2p
peers = await createPeers({ number })
peers.forEach((peer) => {
const ps = new YourPubsubRouter(peer, options)
pubsubNodes.push(ps)
})
return pubsubNodes
},
async teardown () {
// Clean up any resources created by setup()
await Promise.all(pubsubNodes.map(ps => ps.stop()))
peers.length && await Promise.all(peers.map(peer => peer.stop()))
}
})
})
```

46
src/pubsub/errors.js Normal file
View File

@ -0,0 +1,46 @@
'use strict'
exports.codes = {
/**
* Signature policy is invalid
*/
ERR_INVALID_SIGNATURE_POLICY: 'ERR_INVALID_SIGNATURE_POLICY',
/**
* Signature policy is unhandled
*/
ERR_UNHANDLED_SIGNATURE_POLICY: 'ERR_UNHANDLED_SIGNATURE_POLICY',
// Strict signing codes
/**
* Message expected to have a `signature`, but doesn't
*/
ERR_MISSING_SIGNATURE: 'ERR_MISSING_SIGNATURE',
/**
* Message expected to have a `seqno`, but doesn't
*/
ERR_MISSING_SEQNO: 'ERR_MISSING_SEQNO',
/**
* Message `signature` is invalid
*/
ERR_INVALID_SIGNATURE: 'ERR_INVALID_SIGNATURE',
// Strict no-signing codes
/**
* Message expected to not have a `from`, but does
*/
ERR_UNEXPECTED_FROM: 'ERR_UNEXPECTED_FROM',
/**
* Message expected to not have a `signature`, but does
*/
ERR_UNEXPECTED_SIGNATURE: 'ERR_UNEXPECTED_SIGNATURE',
/**
* Message expected to not have a `key`, but does
*/
ERR_UNEXPECTED_KEY: 'ERR_UNEXPECTED_KEY',
/**
* Message expected to not have a `seqno`, but does
*/
ERR_UNEXPECTED_SEQNO: 'ERR_UNEXPECTED_SEQNO'
}

756
src/pubsub/index.js Normal file
View File

@ -0,0 +1,756 @@
'use strict'
const debug = require('debug')
/** @typedef {import('../types').EventEmitterFactory} Events */
/** @type Events */
const EventEmitter = require('events')
const errcode = require('err-code')
const { pipe } = require('it-pipe')
const MulticodecTopology = require('../topology/multicodec-topology')
const { codes } = require('./errors')
const { RPC } = require('./message/rpc')
const PeerStreams = require('./peer-streams')
const { SignaturePolicy } = require('./signature-policy')
const utils = require('./utils')
const {
signMessage,
verifySignature
} = require('./message/sign')
/**
* @typedef {any} Libp2p
* @typedef {import('peer-id')} PeerId
* @typedef {import('bl')} BufferList
* @typedef {import('../stream-muxer/types').MuxedStream} MuxedStream
* @typedef {import('../connection/connection')} Connection
* @typedef {import('./signature-policy').SignaturePolicyType} SignaturePolicyType
* @typedef {import('./message/rpc').IRPC} IRPC
* @typedef {import('./message/rpc').RPC.SubOpts} RPCSubOpts
* @typedef {import('./message/rpc').RPC.Message} RPCMessage
*/
/**
* @typedef {Object} InMessage
* @property {string} [from]
* @property {string} receivedFrom
* @property {string[]} topicIDs
* @property {Uint8Array} [seqno]
* @property {Uint8Array} data
* @property {Uint8Array} [signature]
* @property {Uint8Array} [key]
*
* @typedef {Object} PubsubProperties
* @property {string} debugName - log namespace
* @property {Array<string>|string} multicodecs - protocol identificers to connect
* @property {Libp2p} libp2p
*
* @typedef {Object} PubsubOptions
* @property {SignaturePolicyType} [globalSignaturePolicy = SignaturePolicy.StrictSign] - defines how signatures should be handled
* @property {boolean} [canRelayMessage = false] - if can relay messages not subscribed
* @property {boolean} [emitSelf = false] - if publish should emit to self, if subscribed
*/
/**
* PubsubBaseProtocol handles the peers and connections logic for pubsub routers
* and specifies the API that pubsub routers should have.
*/
class PubsubBaseProtocol extends EventEmitter {
/**
* @param {PubsubProperties & PubsubOptions} props
* @abstract
*/
constructor ({
debugName,
multicodecs,
libp2p,
globalSignaturePolicy = SignaturePolicy.StrictSign,
canRelayMessage = false,
emitSelf = false
}) {
if (typeof debugName !== 'string') {
throw new Error('a debugname `string` is required')
}
if (!multicodecs) {
throw new Error('multicodecs are required')
}
if (!libp2p) {
throw new Error('libp2p is required')
}
super()
this.log = Object.assign(debug(debugName), {
err: debug(`${debugName}:error`)
})
/**
* @type {Array<string>}
*/
this.multicodecs = utils.ensureArray(multicodecs)
this._libp2p = libp2p
this.registrar = libp2p.registrar
/**
* @type {PeerId}
*/
this.peerId = libp2p.peerId
this.started = false
/**
* Map of topics to which peers are subscribed to
*
* @type {Map<string, Set<string>>}
*/
this.topics = new Map()
/**
* List of our subscriptions
*
* @type {Set<string>}
*/
this.subscriptions = new Set()
/**
* Map of peer streams
*
* @type {Map<string, import('./peer-streams')>}
*/
this.peers = new Map()
// validate signature policy
if (!SignaturePolicy[globalSignaturePolicy]) {
throw errcode(new Error('Invalid global signature policy'), codes.ERR_INVALID_SIGNATURE_POLICY)
}
/**
* The signature policy to follow by default
*
* @type {string}
*/
this.globalSignaturePolicy = globalSignaturePolicy
/**
* If router can relay received messages, even if not subscribed
*
* @type {boolean}
*/
this.canRelayMessage = canRelayMessage
/**
* if publish should emit to self, if subscribed
*
* @type {boolean}
*/
this.emitSelf = emitSelf
/**
* Topic validator function
*
* @typedef {function(string, InMessage): Promise<void>} validator
*/
/**
* Topic validator map
*
* Keyed by topic
* Topic validators are functions with the following input:
*
* @type {Map<string, validator>}
*/
this.topicValidators = new Map()
this._registrarId = undefined
this._onIncomingStream = this._onIncomingStream.bind(this)
this._onPeerConnected = this._onPeerConnected.bind(this)
this._onPeerDisconnected = this._onPeerDisconnected.bind(this)
}
// LIFECYCLE METHODS
/**
* Register the pubsub protocol onto the libp2p node.
*
* @returns {void}
*/
start () {
if (this.started) {
return
}
this.log('starting')
// Incoming streams
// Called after a peer dials us
this.registrar.handle(this.multicodecs, this._onIncomingStream)
// register protocol with topology
// Topology callbacks called on connection manager changes
const topology = new MulticodecTopology({
multicodecs: this.multicodecs,
handlers: {
onConnect: this._onPeerConnected,
onDisconnect: this._onPeerDisconnected
}
})
this._registrarId = this.registrar.register(topology)
this.log('started')
this.started = true
}
/**
* Unregister the pubsub protocol and the streams with other peers will be closed.
*
* @returns {void}
*/
stop () {
if (!this.started) {
return
}
// unregister protocol and handlers
this.registrar.unregister(this._registrarId)
this.log('stopping')
this.peers.forEach((peerStreams) => peerStreams.close())
this.peers = new Map()
this.subscriptions = new Set()
this.started = false
this.log('stopped')
}
/**
* On an inbound stream opened.
*
* @protected
* @param {Object} props
* @param {string} props.protocol
* @param {MuxedStream} props.stream
* @param {Connection} props.connection - connection
*/
_onIncomingStream ({ protocol, stream, connection }) {
const peerId = connection.remotePeer
const idB58Str = peerId.toB58String()
const peer = this._addPeer(peerId, protocol)
const inboundStream = peer.attachInboundStream(stream)
this._processMessages(idB58Str, inboundStream, peer)
}
/**
* Registrar notifies an established connection with pubsub protocol.
*
* @protected
* @param {PeerId} peerId - remote peer-id
* @param {Connection} conn - connection to the peer
*/
async _onPeerConnected (peerId, conn) {
const idB58Str = peerId.toB58String()
this.log('connected', idB58Str)
try {
const { stream, protocol } = await conn.newStream(this.multicodecs)
const peer = this._addPeer(peerId, protocol)
await peer.attachOutboundStream(stream)
} catch (err) {
this.log.err(err)
}
// Immediately send my own subscriptions to the newly established conn
this._sendSubscriptions(idB58Str, Array.from(this.subscriptions), true)
}
/**
* Registrar notifies a closing connection with pubsub protocol.
*
* @protected
* @param {PeerId} peerId - peerId
* @param {Error} [err] - error for connection end
*/
_onPeerDisconnected (peerId, err) {
const idB58Str = peerId.toB58String()
this.log('connection ended', idB58Str, err ? err.message : '')
this._removePeer(peerId)
}
/**
* Notifies the router that a peer has been connected
*
* @protected
* @param {PeerId} peerId
* @param {string} protocol
* @returns {PeerStreams}
*/
_addPeer (peerId, protocol) {
const id = peerId.toB58String()
const existing = this.peers.get(id)
// If peer streams already exists, do nothing
if (existing) {
return existing
}
// else create a new peer streams
this.log('new peer', id)
const peerStreams = new PeerStreams({
id: peerId,
protocol
})
this.peers.set(id, peerStreams)
peerStreams.once('close', () => this._removePeer(peerId))
return peerStreams
}
/**
* Notifies the router that a peer has been disconnected.
*
* @protected
* @param {PeerId} peerId
* @returns {PeerStreams | undefined}
*/
_removePeer (peerId) {
if (!peerId) return
const id = peerId.toB58String()
const peerStreams = this.peers.get(id)
if (!peerStreams) return
// close peer streams
peerStreams.removeAllListeners()
peerStreams.close()
// delete peer streams
this.log('delete peer', id)
this.peers.delete(id)
// remove peer from topics map
for (const peers of this.topics.values()) {
peers.delete(id)
}
return peerStreams
}
// MESSAGE METHODS
/**
* Responsible for processing each RPC message received by other peers.
*
* @param {string} idB58Str - peer id string in base58
* @param {AsyncIterable<Uint8Array|BufferList>} stream - inbound stream
* @param {PeerStreams} peerStreams - PubSub peer
* @returns {Promise<void>}
*/
async _processMessages (idB58Str, stream, peerStreams) {
try {
await pipe(
stream,
async (source) => {
for await (const data of source) {
const rpcBytes = data instanceof Uint8Array ? data : data.slice()
const rpcMsg = this._decodeRpc(rpcBytes)
this._processRpc(idB58Str, peerStreams, rpcMsg)
}
}
)
} catch (err) {
this._onPeerDisconnected(peerStreams.id, err)
}
}
/**
* Handles an rpc request from a peer
*
* @param {string} idB58Str
* @param {PeerStreams} peerStreams
* @param {RPC} rpc
* @returns {boolean}
*/
_processRpc (idB58Str, peerStreams, rpc) {
this.log('rpc from', idB58Str)
const subs = rpc.subscriptions
const msgs = rpc.msgs
if (subs.length) {
// update peer subscriptions
subs.forEach((subOpt) => {
this._processRpcSubOpt(idB58Str, subOpt)
})
this.emit('pubsub:subscription-change', peerStreams.id, subs)
}
if (!this._acceptFrom(idB58Str)) {
this.log('received message from unacceptable peer %s', idB58Str)
return false
}
if (msgs.length) {
// @ts-ignore RPC message is modified
msgs.forEach((message) => {
if (!(this.canRelayMessage || (message.topicIDs && message.topicIDs.some((topic) => this.subscriptions.has(topic))))) {
this.log('received message we didn\'t subscribe to. Dropping.')
return
}
const msg = utils.normalizeInRpcMessage(message, idB58Str)
this._processRpcMessage(msg)
})
}
return true
}
/**
* Handles a subscription change from a peer
*
* @param {string} id
* @param {RPC.ISubOpts} subOpt
*/
_processRpcSubOpt (id, subOpt) {
const t = subOpt.topicID
if (!t) {
return
}
let topicSet = this.topics.get(t)
if (!topicSet) {
topicSet = new Set()
this.topics.set(t, topicSet)
}
if (subOpt.subscribe) {
// subscribe peer to new topic
topicSet.add(id)
} else {
// unsubscribe from existing topic
topicSet.delete(id)
}
}
/**
* Handles an message from a peer
*
* @param {InMessage} msg
* @returns {Promise<void>}
*/
async _processRpcMessage (msg) {
if (this.peerId.toB58String() === msg.from && !this.emitSelf) {
return
}
// Ensure the message is valid before processing it
try {
await this.validate(msg)
} catch (err) {
this.log('Message is invalid, dropping it. %O', err)
return
}
// Emit to self
this._emitMessage(msg)
this._publish(utils.normalizeOutRpcMessage(msg))
}
/**
* Emit a message from a peer
*
* @param {InMessage} message
*/
_emitMessage (message) {
message.topicIDs.forEach((topic) => {
if (this.subscriptions.has(topic)) {
this.emit(topic, message)
}
})
}
/**
* The default msgID implementation
* Child class can override this.
*
* @param {InMessage} msg - the message object
* @returns {Uint8Array} message id as bytes
*/
getMsgId (msg) {
const signaturePolicy = this.globalSignaturePolicy
switch (signaturePolicy) {
case SignaturePolicy.StrictSign:
// @ts-ignore seqno is optional in protobuf definition but it will exist
return utils.msgId(msg.from, msg.seqno)
case SignaturePolicy.StrictNoSign:
return utils.noSignMsgId(msg.data)
default:
throw errcode(new Error('Cannot get message id: unhandled signature policy: ' + signaturePolicy), codes.ERR_UNHANDLED_SIGNATURE_POLICY)
}
}
/**
* Whether to accept a message from a peer
* Override to create a graylist
*
* @override
* @param {string} id
* @returns {boolean}
*/
_acceptFrom (id) {
return true
}
/**
* Decode Uint8Array into an RPC object.
* This can be override to use a custom router protobuf.
*
* @param {Uint8Array} bytes
* @returns {RPC}
*/
_decodeRpc (bytes) {
return RPC.decode(bytes)
}
/**
* Encode RPC object into a Uint8Array.
* This can be override to use a custom router protobuf.
*
* @param {IRPC} rpc
* @returns {Uint8Array}
*/
_encodeRpc (rpc) {
return RPC.encode(rpc).finish()
}
/**
* Send an rpc object to a peer
*
* @param {string} id - peer id
* @param {IRPC} rpc
* @returns {void}
*/
_sendRpc (id, rpc) {
const peerStreams = this.peers.get(id)
if (!peerStreams || !peerStreams.isWritable) {
const msg = `Cannot send RPC to ${id} as there is no open stream to it available`
this.log.err(msg)
return
}
peerStreams.write(this._encodeRpc(rpc))
}
/**
* Send subscroptions to a peer
*
* @param {string} id - peer id
* @param {string[]} topics
* @param {boolean} subscribe - set to false for unsubscriptions
* @returns {void}
*/
_sendSubscriptions (id, topics, subscribe) {
return this._sendRpc(id, {
subscriptions: topics.map(t => ({ topicID: t, subscribe: subscribe }))
})
}
/**
* Validates the given message. The signature will be checked for authenticity.
* Throws an error on invalid messages
*
* @param {InMessage} message
* @returns {Promise<void>}
*/
async validate (message) { // eslint-disable-line require-await
const signaturePolicy = this.globalSignaturePolicy
switch (signaturePolicy) {
case SignaturePolicy.StrictNoSign:
if (message.from) {
throw errcode(new Error('StrictNoSigning: from should not be present'), codes.ERR_UNEXPECTED_FROM)
}
if (message.signature) {
throw errcode(new Error('StrictNoSigning: signature should not be present'), codes.ERR_UNEXPECTED_SIGNATURE)
}
if (message.key) {
throw errcode(new Error('StrictNoSigning: key should not be present'), codes.ERR_UNEXPECTED_KEY)
}
if (message.seqno) {
throw errcode(new Error('StrictNoSigning: seqno should not be present'), codes.ERR_UNEXPECTED_SEQNO)
}
break
case SignaturePolicy.StrictSign:
if (!message.signature) {
throw errcode(new Error('StrictSigning: Signing required and no signature was present'), codes.ERR_MISSING_SIGNATURE)
}
if (!message.seqno) {
throw errcode(new Error('StrictSigning: Signing required and no seqno was present'), codes.ERR_MISSING_SEQNO)
}
if (!(await verifySignature(message))) {
throw errcode(new Error('StrictSigning: Invalid message signature'), codes.ERR_INVALID_SIGNATURE)
}
break
default:
throw errcode(new Error('Cannot validate message: unhandled signature policy: ' + signaturePolicy), codes.ERR_UNHANDLED_SIGNATURE_POLICY)
}
for (const topic of message.topicIDs) {
const validatorFn = this.topicValidators.get(topic)
if (validatorFn) {
await validatorFn(topic, message)
}
}
}
/**
* Normalizes the message and signs it, if signing is enabled.
* Should be used by the routers to create the message to send.
*
* @protected
* @param {InMessage} message
* @returns {Promise<InMessage>}
*/
_buildMessage (message) {
const signaturePolicy = this.globalSignaturePolicy
switch (signaturePolicy) {
case SignaturePolicy.StrictSign:
message.from = this.peerId.toB58String()
message.seqno = utils.randomSeqno()
return signMessage(this.peerId, utils.normalizeOutRpcMessage(message))
case SignaturePolicy.StrictNoSign:
return Promise.resolve(message)
default:
throw errcode(new Error('Cannot build message: unhandled signature policy: ' + signaturePolicy), codes.ERR_UNHANDLED_SIGNATURE_POLICY)
}
}
// API METHODS
/**
* Get a list of the peer-ids that are subscribed to one topic.
*
* @param {string} topic
* @returns {Array<string>}
*/
getSubscribers (topic) {
if (!this.started) {
throw errcode(new Error('not started yet'), 'ERR_NOT_STARTED_YET')
}
if (!topic || typeof topic !== 'string') {
throw errcode(new Error('a string topic must be provided'), 'ERR_NOT_VALID_TOPIC')
}
const peersInTopic = this.topics.get(topic)
if (!peersInTopic) {
return []
}
return Array.from(peersInTopic)
}
/**
* Publishes messages to all subscribed peers
*
* @override
* @param {string} topic
* @param {Uint8Array} message
* @returns {Promise<void>}
*/
async publish (topic, message) {
if (!this.started) {
throw new Error('Pubsub has not started')
}
this.log('publish', topic, message)
const from = this.peerId.toB58String()
const msgObject = {
receivedFrom: from,
data: message,
topicIDs: [topic]
}
// ensure that the message follows the signature policy
const outMsg = await this._buildMessage(msgObject)
// @ts-ignore different type as from is converted
const msg = utils.normalizeInRpcMessage(outMsg)
// Emit to self if I'm interested and emitSelf enabled
this.emitSelf && this._emitMessage(msg)
// send to all the other peers
await this._publish(msg)
}
/**
* Overriding the implementation of publish should handle the appropriate algorithms for the publish/subscriber implementation.
* For example, a Floodsub implementation might simply publish each message to each topic for every peer
*
* @abstract
* @param {InMessage|RPCMessage} message
* @returns {Promise<void>}
*
*/
_publish (message) {
throw errcode(new Error('publish must be implemented by the subclass'), 'ERR_NOT_IMPLEMENTED')
}
/**
* Subscribes to a given topic.
*
* @abstract
* @param {string} topic
* @returns {void}
*/
subscribe (topic) {
if (!this.started) {
throw new Error('Pubsub has not started')
}
if (!this.subscriptions.has(topic)) {
this.subscriptions.add(topic)
this.peers.forEach((_, id) => this._sendSubscriptions(id, [topic], true))
}
}
/**
* Unsubscribe from the given topic.
*
* @override
* @param {string} topic
* @returns {void}
*/
unsubscribe (topic) {
if (!this.started) {
throw new Error('Pubsub is not started')
}
if (this.subscriptions.has(topic) && this.listenerCount(topic) === 0) {
this.subscriptions.delete(topic)
this.peers.forEach((_, id) => this._sendSubscriptions(id, [topic], false))
}
}
/**
* Get the list of topics which the peer is subscribed to.
*
* @override
* @returns {Array<string>}
*/
getTopics () {
if (!this.started) {
throw new Error('Pubsub is not started')
}
return Array.from(this.subscriptions)
}
}
PubsubBaseProtocol.utils = utils
PubsubBaseProtocol.SignaturePolicy = SignaturePolicy
module.exports = PubsubBaseProtocol

222
src/pubsub/message/rpc.d.ts vendored Normal file
View File

@ -0,0 +1,222 @@
import * as $protobuf from "protobufjs";
/** Properties of a RPC. */
export interface IRPC {
/** RPC subscriptions */
subscriptions?: (RPC.ISubOpts[]|null);
/** RPC msgs */
msgs?: (RPC.IMessage[]|null);
}
/** Represents a RPC. */
export class RPC implements IRPC {
/**
* Constructs a new RPC.
* @param [p] Properties to set
*/
constructor(p?: IRPC);
/** RPC subscriptions. */
public subscriptions: RPC.ISubOpts[];
/** RPC msgs. */
public msgs: RPC.IMessage[];
/**
* Encodes the specified RPC message. Does not implicitly {@link RPC.verify|verify} messages.
* @param m RPC message or plain object to encode
* @param [w] Writer to encode to
* @returns Writer
*/
public static encode(m: IRPC, w?: $protobuf.Writer): $protobuf.Writer;
/**
* Decodes a RPC message from the specified reader or buffer.
* @param r Reader or buffer to decode from
* @param [l] Message length if known beforehand
* @returns RPC
* @throws {Error} If the payload is not a reader or valid buffer
* @throws {$protobuf.util.ProtocolError} If required fields are missing
*/
public static decode(r: ($protobuf.Reader|Uint8Array), l?: number): RPC;
/**
* Creates a RPC message from a plain object. Also converts values to their respective internal types.
* @param d Plain object
* @returns RPC
*/
public static fromObject(d: { [k: string]: any }): RPC;
/**
* Creates a plain object from a RPC message. Also converts values to other types if specified.
* @param m RPC
* @param [o] Conversion options
* @returns Plain object
*/
public static toObject(m: RPC, o?: $protobuf.IConversionOptions): { [k: string]: any };
/**
* Converts this RPC to JSON.
* @returns JSON object
*/
public toJSON(): { [k: string]: any };
}
export namespace RPC {
/** Properties of a SubOpts. */
interface ISubOpts {
/** SubOpts subscribe */
subscribe?: (boolean|null);
/** SubOpts topicID */
topicID?: (string|null);
}
/** Represents a SubOpts. */
class SubOpts implements ISubOpts {
/**
* Constructs a new SubOpts.
* @param [p] Properties to set
*/
constructor(p?: RPC.ISubOpts);
/** SubOpts subscribe. */
public subscribe: boolean;
/** SubOpts topicID. */
public topicID: string;
/**
* Encodes the specified SubOpts message. Does not implicitly {@link RPC.SubOpts.verify|verify} messages.
* @param m SubOpts message or plain object to encode
* @param [w] Writer to encode to
* @returns Writer
*/
public static encode(m: RPC.ISubOpts, w?: $protobuf.Writer): $protobuf.Writer;
/**
* Decodes a SubOpts message from the specified reader or buffer.
* @param r Reader or buffer to decode from
* @param [l] Message length if known beforehand
* @returns SubOpts
* @throws {Error} If the payload is not a reader or valid buffer
* @throws {$protobuf.util.ProtocolError} If required fields are missing
*/
public static decode(r: ($protobuf.Reader|Uint8Array), l?: number): RPC.SubOpts;
/**
* Creates a SubOpts message from a plain object. Also converts values to their respective internal types.
* @param d Plain object
* @returns SubOpts
*/
public static fromObject(d: { [k: string]: any }): RPC.SubOpts;
/**
* Creates a plain object from a SubOpts message. Also converts values to other types if specified.
* @param m SubOpts
* @param [o] Conversion options
* @returns Plain object
*/
public static toObject(m: RPC.SubOpts, o?: $protobuf.IConversionOptions): { [k: string]: any };
/**
* Converts this SubOpts to JSON.
* @returns JSON object
*/
public toJSON(): { [k: string]: any };
}
/** Properties of a Message. */
interface IMessage {
/** Message from */
from?: (Uint8Array|null);
/** Message data */
data?: (Uint8Array|null);
/** Message seqno */
seqno?: (Uint8Array|null);
/** Message topicIDs */
topicIDs?: (string[]|null);
/** Message signature */
signature?: (Uint8Array|null);
/** Message key */
key?: (Uint8Array|null);
}
/** Represents a Message. */
class Message implements IMessage {
/**
* Constructs a new Message.
* @param [p] Properties to set
*/
constructor(p?: RPC.IMessage);
/** Message from. */
public from: Uint8Array;
/** Message data. */
public data: Uint8Array;
/** Message seqno. */
public seqno: Uint8Array;
/** Message topicIDs. */
public topicIDs: string[];
/** Message signature. */
public signature: Uint8Array;
/** Message key. */
public key: Uint8Array;
/**
* Encodes the specified Message message. Does not implicitly {@link RPC.Message.verify|verify} messages.
* @param m Message message or plain object to encode
* @param [w] Writer to encode to
* @returns Writer
*/
public static encode(m: RPC.IMessage, w?: $protobuf.Writer): $protobuf.Writer;
/**
* Decodes a Message message from the specified reader or buffer.
* @param r Reader or buffer to decode from
* @param [l] Message length if known beforehand
* @returns Message
* @throws {Error} If the payload is not a reader or valid buffer
* @throws {$protobuf.util.ProtocolError} If required fields are missing
*/
public static decode(r: ($protobuf.Reader|Uint8Array), l?: number): RPC.Message;
/**
* Creates a Message message from a plain object. Also converts values to their respective internal types.
* @param d Plain object
* @returns Message
*/
public static fromObject(d: { [k: string]: any }): RPC.Message;
/**
* Creates a plain object from a Message message. Also converts values to other types if specified.
* @param m Message
* @param [o] Conversion options
* @returns Plain object
*/
public static toObject(m: RPC.Message, o?: $protobuf.IConversionOptions): { [k: string]: any };
/**
* Converts this Message to JSON.
* @returns JSON object
*/
public toJSON(): { [k: string]: any };
}
}

645
src/pubsub/message/rpc.js Normal file
View File

@ -0,0 +1,645 @@
/*eslint-disable*/
"use strict";
var $protobuf = require("protobufjs/minimal");
// Common aliases
var $Reader = $protobuf.Reader, $Writer = $protobuf.Writer, $util = $protobuf.util;
// Exported root namespace
var $root = $protobuf.roots["default"] || ($protobuf.roots["default"] = {});
$root.RPC = (function() {
/**
* Properties of a RPC.
* @exports IRPC
* @interface IRPC
* @property {Array.<RPC.ISubOpts>|null} [subscriptions] RPC subscriptions
* @property {Array.<RPC.IMessage>|null} [msgs] RPC msgs
*/
/**
* Constructs a new RPC.
* @exports RPC
* @classdesc Represents a RPC.
* @implements IRPC
* @constructor
* @param {IRPC=} [p] Properties to set
*/
function RPC(p) {
this.subscriptions = [];
this.msgs = [];
if (p)
for (var ks = Object.keys(p), i = 0; i < ks.length; ++i)
if (p[ks[i]] != null)
this[ks[i]] = p[ks[i]];
}
/**
* RPC subscriptions.
* @member {Array.<RPC.ISubOpts>} subscriptions
* @memberof RPC
* @instance
*/
RPC.prototype.subscriptions = $util.emptyArray;
/**
* RPC msgs.
* @member {Array.<RPC.IMessage>} msgs
* @memberof RPC
* @instance
*/
RPC.prototype.msgs = $util.emptyArray;
/**
* Encodes the specified RPC message. Does not implicitly {@link RPC.verify|verify} messages.
* @function encode
* @memberof RPC
* @static
* @param {IRPC} m RPC message or plain object to encode
* @param {$protobuf.Writer} [w] Writer to encode to
* @returns {$protobuf.Writer} Writer
*/
RPC.encode = function encode(m, w) {
if (!w)
w = $Writer.create();
if (m.subscriptions != null && m.subscriptions.length) {
for (var i = 0; i < m.subscriptions.length; ++i)
$root.RPC.SubOpts.encode(m.subscriptions[i], w.uint32(10).fork()).ldelim();
}
if (m.msgs != null && m.msgs.length) {
for (var i = 0; i < m.msgs.length; ++i)
$root.RPC.Message.encode(m.msgs[i], w.uint32(18).fork()).ldelim();
}
return w;
};
/**
* Decodes a RPC message from the specified reader or buffer.
* @function decode
* @memberof RPC
* @static
* @param {$protobuf.Reader|Uint8Array} r Reader or buffer to decode from
* @param {number} [l] Message length if known beforehand
* @returns {RPC} RPC
* @throws {Error} If the payload is not a reader or valid buffer
* @throws {$protobuf.util.ProtocolError} If required fields are missing
*/
RPC.decode = function decode(r, l) {
if (!(r instanceof $Reader))
r = $Reader.create(r);
var c = l === undefined ? r.len : r.pos + l, m = new $root.RPC();
while (r.pos < c) {
var t = r.uint32();
switch (t >>> 3) {
case 1:
if (!(m.subscriptions && m.subscriptions.length))
m.subscriptions = [];
m.subscriptions.push($root.RPC.SubOpts.decode(r, r.uint32()));
break;
case 2:
if (!(m.msgs && m.msgs.length))
m.msgs = [];
m.msgs.push($root.RPC.Message.decode(r, r.uint32()));
break;
default:
r.skipType(t & 7);
break;
}
}
return m;
};
/**
* Creates a RPC message from a plain object. Also converts values to their respective internal types.
* @function fromObject
* @memberof RPC
* @static
* @param {Object.<string,*>} d Plain object
* @returns {RPC} RPC
*/
RPC.fromObject = function fromObject(d) {
if (d instanceof $root.RPC)
return d;
var m = new $root.RPC();
if (d.subscriptions) {
if (!Array.isArray(d.subscriptions))
throw TypeError(".RPC.subscriptions: array expected");
m.subscriptions = [];
for (var i = 0; i < d.subscriptions.length; ++i) {
if (typeof d.subscriptions[i] !== "object")
throw TypeError(".RPC.subscriptions: object expected");
m.subscriptions[i] = $root.RPC.SubOpts.fromObject(d.subscriptions[i]);
}
}
if (d.msgs) {
if (!Array.isArray(d.msgs))
throw TypeError(".RPC.msgs: array expected");
m.msgs = [];
for (var i = 0; i < d.msgs.length; ++i) {
if (typeof d.msgs[i] !== "object")
throw TypeError(".RPC.msgs: object expected");
m.msgs[i] = $root.RPC.Message.fromObject(d.msgs[i]);
}
}
return m;
};
/**
* Creates a plain object from a RPC message. Also converts values to other types if specified.
* @function toObject
* @memberof RPC
* @static
* @param {RPC} m RPC
* @param {$protobuf.IConversionOptions} [o] Conversion options
* @returns {Object.<string,*>} Plain object
*/
RPC.toObject = function toObject(m, o) {
if (!o)
o = {};
var d = {};
if (o.arrays || o.defaults) {
d.subscriptions = [];
d.msgs = [];
}
if (m.subscriptions && m.subscriptions.length) {
d.subscriptions = [];
for (var j = 0; j < m.subscriptions.length; ++j) {
d.subscriptions[j] = $root.RPC.SubOpts.toObject(m.subscriptions[j], o);
}
}
if (m.msgs && m.msgs.length) {
d.msgs = [];
for (var j = 0; j < m.msgs.length; ++j) {
d.msgs[j] = $root.RPC.Message.toObject(m.msgs[j], o);
}
}
return d;
};
/**
* Converts this RPC to JSON.
* @function toJSON
* @memberof RPC
* @instance
* @returns {Object.<string,*>} JSON object
*/
RPC.prototype.toJSON = function toJSON() {
return this.constructor.toObject(this, $protobuf.util.toJSONOptions);
};
RPC.SubOpts = (function() {
/**
* Properties of a SubOpts.
* @memberof RPC
* @interface ISubOpts
* @property {boolean|null} [subscribe] SubOpts subscribe
* @property {string|null} [topicID] SubOpts topicID
*/
/**
* Constructs a new SubOpts.
* @memberof RPC
* @classdesc Represents a SubOpts.
* @implements ISubOpts
* @constructor
* @param {RPC.ISubOpts=} [p] Properties to set
*/
function SubOpts(p) {
if (p)
for (var ks = Object.keys(p), i = 0; i < ks.length; ++i)
if (p[ks[i]] != null)
this[ks[i]] = p[ks[i]];
}
/**
* SubOpts subscribe.
* @member {boolean} subscribe
* @memberof RPC.SubOpts
* @instance
*/
SubOpts.prototype.subscribe = false;
/**
* SubOpts topicID.
* @member {string} topicID
* @memberof RPC.SubOpts
* @instance
*/
SubOpts.prototype.topicID = "";
/**
* Encodes the specified SubOpts message. Does not implicitly {@link RPC.SubOpts.verify|verify} messages.
* @function encode
* @memberof RPC.SubOpts
* @static
* @param {RPC.ISubOpts} m SubOpts message or plain object to encode
* @param {$protobuf.Writer} [w] Writer to encode to
* @returns {$protobuf.Writer} Writer
*/
SubOpts.encode = function encode(m, w) {
if (!w)
w = $Writer.create();
if (m.subscribe != null && Object.hasOwnProperty.call(m, "subscribe"))
w.uint32(8).bool(m.subscribe);
if (m.topicID != null && Object.hasOwnProperty.call(m, "topicID"))
w.uint32(18).string(m.topicID);
return w;
};
/**
* Decodes a SubOpts message from the specified reader or buffer.
* @function decode
* @memberof RPC.SubOpts
* @static
* @param {$protobuf.Reader|Uint8Array} r Reader or buffer to decode from
* @param {number} [l] Message length if known beforehand
* @returns {RPC.SubOpts} SubOpts
* @throws {Error} If the payload is not a reader or valid buffer
* @throws {$protobuf.util.ProtocolError} If required fields are missing
*/
SubOpts.decode = function decode(r, l) {
if (!(r instanceof $Reader))
r = $Reader.create(r);
var c = l === undefined ? r.len : r.pos + l, m = new $root.RPC.SubOpts();
while (r.pos < c) {
var t = r.uint32();
switch (t >>> 3) {
case 1:
m.subscribe = r.bool();
break;
case 2:
m.topicID = r.string();
break;
default:
r.skipType(t & 7);
break;
}
}
return m;
};
/**
* Creates a SubOpts message from a plain object. Also converts values to their respective internal types.
* @function fromObject
* @memberof RPC.SubOpts
* @static
* @param {Object.<string,*>} d Plain object
* @returns {RPC.SubOpts} SubOpts
*/
SubOpts.fromObject = function fromObject(d) {
if (d instanceof $root.RPC.SubOpts)
return d;
var m = new $root.RPC.SubOpts();
if (d.subscribe != null) {
m.subscribe = Boolean(d.subscribe);
}
if (d.topicID != null) {
m.topicID = String(d.topicID);
}
return m;
};
/**
* Creates a plain object from a SubOpts message. Also converts values to other types if specified.
* @function toObject
* @memberof RPC.SubOpts
* @static
* @param {RPC.SubOpts} m SubOpts
* @param {$protobuf.IConversionOptions} [o] Conversion options
* @returns {Object.<string,*>} Plain object
*/
SubOpts.toObject = function toObject(m, o) {
if (!o)
o = {};
var d = {};
if (o.defaults) {
d.subscribe = false;
d.topicID = "";
}
if (m.subscribe != null && m.hasOwnProperty("subscribe")) {
d.subscribe = m.subscribe;
}
if (m.topicID != null && m.hasOwnProperty("topicID")) {
d.topicID = m.topicID;
}
return d;
};
/**
* Converts this SubOpts to JSON.
* @function toJSON
* @memberof RPC.SubOpts
* @instance
* @returns {Object.<string,*>} JSON object
*/
SubOpts.prototype.toJSON = function toJSON() {
return this.constructor.toObject(this, $protobuf.util.toJSONOptions);
};
return SubOpts;
})();
RPC.Message = (function() {
/**
* Properties of a Message.
* @memberof RPC
* @interface IMessage
* @property {Uint8Array|null} [from] Message from
* @property {Uint8Array|null} [data] Message data
* @property {Uint8Array|null} [seqno] Message seqno
* @property {Array.<string>|null} [topicIDs] Message topicIDs
* @property {Uint8Array|null} [signature] Message signature
* @property {Uint8Array|null} [key] Message key
*/
/**
* Constructs a new Message.
* @memberof RPC
* @classdesc Represents a Message.
* @implements IMessage
* @constructor
* @param {RPC.IMessage=} [p] Properties to set
*/
function Message(p) {
this.topicIDs = [];
if (p)
for (var ks = Object.keys(p), i = 0; i < ks.length; ++i)
if (p[ks[i]] != null)
this[ks[i]] = p[ks[i]];
}
/**
* Message from.
* @member {Uint8Array} from
* @memberof RPC.Message
* @instance
*/
Message.prototype.from = $util.newBuffer([]);
/**
* Message data.
* @member {Uint8Array} data
* @memberof RPC.Message
* @instance
*/
Message.prototype.data = $util.newBuffer([]);
/**
* Message seqno.
* @member {Uint8Array} seqno
* @memberof RPC.Message
* @instance
*/
Message.prototype.seqno = $util.newBuffer([]);
/**
* Message topicIDs.
* @member {Array.<string>} topicIDs
* @memberof RPC.Message
* @instance
*/
Message.prototype.topicIDs = $util.emptyArray;
/**
* Message signature.
* @member {Uint8Array} signature
* @memberof RPC.Message
* @instance
*/
Message.prototype.signature = $util.newBuffer([]);
/**
* Message key.
* @member {Uint8Array} key
* @memberof RPC.Message
* @instance
*/
Message.prototype.key = $util.newBuffer([]);
/**
* Encodes the specified Message message. Does not implicitly {@link RPC.Message.verify|verify} messages.
* @function encode
* @memberof RPC.Message
* @static
* @param {RPC.IMessage} m Message message or plain object to encode
* @param {$protobuf.Writer} [w] Writer to encode to
* @returns {$protobuf.Writer} Writer
*/
Message.encode = function encode(m, w) {
if (!w)
w = $Writer.create();
if (m.from != null && Object.hasOwnProperty.call(m, "from"))
w.uint32(10).bytes(m.from);
if (m.data != null && Object.hasOwnProperty.call(m, "data"))
w.uint32(18).bytes(m.data);
if (m.seqno != null && Object.hasOwnProperty.call(m, "seqno"))
w.uint32(26).bytes(m.seqno);
if (m.topicIDs != null && m.topicIDs.length) {
for (var i = 0; i < m.topicIDs.length; ++i)
w.uint32(34).string(m.topicIDs[i]);
}
if (m.signature != null && Object.hasOwnProperty.call(m, "signature"))
w.uint32(42).bytes(m.signature);
if (m.key != null && Object.hasOwnProperty.call(m, "key"))
w.uint32(50).bytes(m.key);
return w;
};
/**
* Decodes a Message message from the specified reader or buffer.
* @function decode
* @memberof RPC.Message
* @static
* @param {$protobuf.Reader|Uint8Array} r Reader or buffer to decode from
* @param {number} [l] Message length if known beforehand
* @returns {RPC.Message} Message
* @throws {Error} If the payload is not a reader or valid buffer
* @throws {$protobuf.util.ProtocolError} If required fields are missing
*/
Message.decode = function decode(r, l) {
if (!(r instanceof $Reader))
r = $Reader.create(r);
var c = l === undefined ? r.len : r.pos + l, m = new $root.RPC.Message();
while (r.pos < c) {
var t = r.uint32();
switch (t >>> 3) {
case 1:
m.from = r.bytes();
break;
case 2:
m.data = r.bytes();
break;
case 3:
m.seqno = r.bytes();
break;
case 4:
if (!(m.topicIDs && m.topicIDs.length))
m.topicIDs = [];
m.topicIDs.push(r.string());
break;
case 5:
m.signature = r.bytes();
break;
case 6:
m.key = r.bytes();
break;
default:
r.skipType(t & 7);
break;
}
}
return m;
};
/**
* Creates a Message message from a plain object. Also converts values to their respective internal types.
* @function fromObject
* @memberof RPC.Message
* @static
* @param {Object.<string,*>} d Plain object
* @returns {RPC.Message} Message
*/
Message.fromObject = function fromObject(d) {
if (d instanceof $root.RPC.Message)
return d;
var m = new $root.RPC.Message();
if (d.from != null) {
if (typeof d.from === "string")
$util.base64.decode(d.from, m.from = $util.newBuffer($util.base64.length(d.from)), 0);
else if (d.from.length)
m.from = d.from;
}
if (d.data != null) {
if (typeof d.data === "string")
$util.base64.decode(d.data, m.data = $util.newBuffer($util.base64.length(d.data)), 0);
else if (d.data.length)
m.data = d.data;
}
if (d.seqno != null) {
if (typeof d.seqno === "string")
$util.base64.decode(d.seqno, m.seqno = $util.newBuffer($util.base64.length(d.seqno)), 0);
else if (d.seqno.length)
m.seqno = d.seqno;
}
if (d.topicIDs) {
if (!Array.isArray(d.topicIDs))
throw TypeError(".RPC.Message.topicIDs: array expected");
m.topicIDs = [];
for (var i = 0; i < d.topicIDs.length; ++i) {
m.topicIDs[i] = String(d.topicIDs[i]);
}
}
if (d.signature != null) {
if (typeof d.signature === "string")
$util.base64.decode(d.signature, m.signature = $util.newBuffer($util.base64.length(d.signature)), 0);
else if (d.signature.length)
m.signature = d.signature;
}
if (d.key != null) {
if (typeof d.key === "string")
$util.base64.decode(d.key, m.key = $util.newBuffer($util.base64.length(d.key)), 0);
else if (d.key.length)
m.key = d.key;
}
return m;
};
/**
* Creates a plain object from a Message message. Also converts values to other types if specified.
* @function toObject
* @memberof RPC.Message
* @static
* @param {RPC.Message} m Message
* @param {$protobuf.IConversionOptions} [o] Conversion options
* @returns {Object.<string,*>} Plain object
*/
Message.toObject = function toObject(m, o) {
if (!o)
o = {};
var d = {};
if (o.arrays || o.defaults) {
d.topicIDs = [];
}
if (o.defaults) {
if (o.bytes === String)
d.from = "";
else {
d.from = [];
if (o.bytes !== Array)
d.from = $util.newBuffer(d.from);
}
if (o.bytes === String)
d.data = "";
else {
d.data = [];
if (o.bytes !== Array)
d.data = $util.newBuffer(d.data);
}
if (o.bytes === String)
d.seqno = "";
else {
d.seqno = [];
if (o.bytes !== Array)
d.seqno = $util.newBuffer(d.seqno);
}
if (o.bytes === String)
d.signature = "";
else {
d.signature = [];
if (o.bytes !== Array)
d.signature = $util.newBuffer(d.signature);
}
if (o.bytes === String)
d.key = "";
else {
d.key = [];
if (o.bytes !== Array)
d.key = $util.newBuffer(d.key);
}
}
if (m.from != null && m.hasOwnProperty("from")) {
d.from = o.bytes === String ? $util.base64.encode(m.from, 0, m.from.length) : o.bytes === Array ? Array.prototype.slice.call(m.from) : m.from;
}
if (m.data != null && m.hasOwnProperty("data")) {
d.data = o.bytes === String ? $util.base64.encode(m.data, 0, m.data.length) : o.bytes === Array ? Array.prototype.slice.call(m.data) : m.data;
}
if (m.seqno != null && m.hasOwnProperty("seqno")) {
d.seqno = o.bytes === String ? $util.base64.encode(m.seqno, 0, m.seqno.length) : o.bytes === Array ? Array.prototype.slice.call(m.seqno) : m.seqno;
}
if (m.topicIDs && m.topicIDs.length) {
d.topicIDs = [];
for (var j = 0; j < m.topicIDs.length; ++j) {
d.topicIDs[j] = m.topicIDs[j];
}
}
if (m.signature != null && m.hasOwnProperty("signature")) {
d.signature = o.bytes === String ? $util.base64.encode(m.signature, 0, m.signature.length) : o.bytes === Array ? Array.prototype.slice.call(m.signature) : m.signature;
}
if (m.key != null && m.hasOwnProperty("key")) {
d.key = o.bytes === String ? $util.base64.encode(m.key, 0, m.key.length) : o.bytes === Array ? Array.prototype.slice.call(m.key) : m.key;
}
return d;
};
/**
* Converts this Message to JSON.
* @function toJSON
* @memberof RPC.Message
* @instance
* @returns {Object.<string,*>} JSON object
*/
Message.prototype.toJSON = function toJSON() {
return this.constructor.toObject(this, $protobuf.util.toJSONOptions);
};
return Message;
})();
return RPC;
})();
module.exports = $root;

View File

@ -0,0 +1,20 @@
syntax = "proto3";
message RPC {
repeated SubOpts subscriptions = 1;
repeated Message msgs = 2;
message SubOpts {
optional bool subscribe = 1; // subscribe or unsubcribe
optional string topicID = 2;
}
message Message {
optional bytes from = 1;
optional bytes data = 2;
optional bytes seqno = 3;
repeated string topicIDs = 4;
optional bytes signature = 5;
optional bytes key = 6;
}
}

105
src/pubsub/message/sign.js Normal file
View File

@ -0,0 +1,105 @@
'use strict'
const PeerId = require('peer-id')
const { RPC } = require('./rpc')
const uint8ArrayConcat = require('uint8arrays/concat')
const uint8ArrayFromString = require('uint8arrays/from-string')
const SignPrefix = uint8ArrayFromString('libp2p-pubsub:')
/**
* @typedef {import('..').InMessage}
*/
/**
* Signs the provided message with the given `peerId`
*
* @param {PeerId} peerId
* @param {RPC.Message} message
* @returns {Promise<any>}
*/
async function signMessage (peerId, message) {
// Get the message in bytes, and prepend with the pubsub prefix
const bytes = uint8ArrayConcat([
SignPrefix,
RPC.Message.encode(message).finish()
])
const signature = await peerId.privKey.sign(bytes)
return {
...message,
signature: signature,
key: peerId.pubKey.bytes
}
}
/**
* Verifies the signature of the given message
*
* @param {InMessage} message
* @returns {Promise<boolean>}
*/
async function verifySignature (message) {
if (!message.signature) {
throw new Error('Message must contain a signature to be verified')
}
// Get message sans the signature
const bytes = uint8ArrayConcat([
SignPrefix,
RPC.Message.encode({
...message,
// @ts-ignore message.from needs to exist
from: PeerId.createFromCID(message.from).toBytes(),
signature: undefined,
key: undefined
}).finish()
])
// Get the public key
const pubKey = await messagePublicKey(message)
// verify the base message
return pubKey.verify(bytes, message.signature)
}
/**
* Returns the PublicKey associated with the given message.
* If no, valid PublicKey can be retrieved an error will be returned.
*
* @param {InMessage} message
* @returns {Promise<PublicKey>}
*/
async function messagePublicKey (message) {
// should be available in the from property of the message (peer id)
if (!message.from) {
throw new Error('Could not get the public key from the originator id')
}
const from = PeerId.createFromCID(message.from)
if (message.key) {
const keyPeerId = await PeerId.createFromPubKey(message.key)
// the key belongs to the sender, return the key
if (keyPeerId.equals(from)) return keyPeerId.pubKey
// We couldn't validate pubkey is from the originator, error
throw new Error('Public Key does not match the originator')
} else if (from.pubKey) {
return from.pubKey
} else {
throw new Error('Could not get the public key from the originator id')
}
}
/**
* @typedef {import('..').InMessage} InMessage
* @typedef {import('libp2p-crypto').PublicKey} PublicKey
*/
module.exports = {
messagePublicKey,
signMessage,
SignPrefix,
verifySignature
}

224
src/pubsub/message/topic-descriptor.d.ts vendored Normal file
View File

@ -0,0 +1,224 @@
import * as $protobuf from "protobufjs";
/** Properties of a TopicDescriptor. */
export interface ITopicDescriptor {
/** TopicDescriptor name */
name?: (string|null);
/** TopicDescriptor auth */
auth?: (TopicDescriptor.IAuthOpts|null);
/** TopicDescriptor enc */
enc?: (TopicDescriptor.IEncOpts|null);
}
/** Represents a TopicDescriptor. */
export class TopicDescriptor implements ITopicDescriptor {
/**
* Constructs a new TopicDescriptor.
* @param [p] Properties to set
*/
constructor(p?: ITopicDescriptor);
/** TopicDescriptor name. */
public name: string;
/** TopicDescriptor auth. */
public auth?: (TopicDescriptor.IAuthOpts|null);
/** TopicDescriptor enc. */
public enc?: (TopicDescriptor.IEncOpts|null);
/**
* Encodes the specified TopicDescriptor message. Does not implicitly {@link TopicDescriptor.verify|verify} messages.
* @param m TopicDescriptor message or plain object to encode
* @param [w] Writer to encode to
* @returns Writer
*/
public static encode(m: ITopicDescriptor, w?: $protobuf.Writer): $protobuf.Writer;
/**
* Decodes a TopicDescriptor message from the specified reader or buffer.
* @param r Reader or buffer to decode from
* @param [l] Message length if known beforehand
* @returns TopicDescriptor
* @throws {Error} If the payload is not a reader or valid buffer
* @throws {$protobuf.util.ProtocolError} If required fields are missing
*/
public static decode(r: ($protobuf.Reader|Uint8Array), l?: number): TopicDescriptor;
/**
* Creates a TopicDescriptor message from a plain object. Also converts values to their respective internal types.
* @param d Plain object
* @returns TopicDescriptor
*/
public static fromObject(d: { [k: string]: any }): TopicDescriptor;
/**
* Creates a plain object from a TopicDescriptor message. Also converts values to other types if specified.
* @param m TopicDescriptor
* @param [o] Conversion options
* @returns Plain object
*/
public static toObject(m: TopicDescriptor, o?: $protobuf.IConversionOptions): { [k: string]: any };
/**
* Converts this TopicDescriptor to JSON.
* @returns JSON object
*/
public toJSON(): { [k: string]: any };
}
export namespace TopicDescriptor {
/** Properties of an AuthOpts. */
interface IAuthOpts {
/** AuthOpts mode */
mode?: (TopicDescriptor.AuthOpts.AuthMode|null);
/** AuthOpts keys */
keys?: (Uint8Array[]|null);
}
/** Represents an AuthOpts. */
class AuthOpts implements IAuthOpts {
/**
* Constructs a new AuthOpts.
* @param [p] Properties to set
*/
constructor(p?: TopicDescriptor.IAuthOpts);
/** AuthOpts mode. */
public mode: TopicDescriptor.AuthOpts.AuthMode;
/** AuthOpts keys. */
public keys: Uint8Array[];
/**
* Encodes the specified AuthOpts message. Does not implicitly {@link TopicDescriptor.AuthOpts.verify|verify} messages.
* @param m AuthOpts message or plain object to encode
* @param [w] Writer to encode to
* @returns Writer
*/
public static encode(m: TopicDescriptor.IAuthOpts, w?: $protobuf.Writer): $protobuf.Writer;
/**
* Decodes an AuthOpts message from the specified reader or buffer.
* @param r Reader or buffer to decode from
* @param [l] Message length if known beforehand
* @returns AuthOpts
* @throws {Error} If the payload is not a reader or valid buffer
* @throws {$protobuf.util.ProtocolError} If required fields are missing
*/
public static decode(r: ($protobuf.Reader|Uint8Array), l?: number): TopicDescriptor.AuthOpts;
/**
* Creates an AuthOpts message from a plain object. Also converts values to their respective internal types.
* @param d Plain object
* @returns AuthOpts
*/
public static fromObject(d: { [k: string]: any }): TopicDescriptor.AuthOpts;
/**
* Creates a plain object from an AuthOpts message. Also converts values to other types if specified.
* @param m AuthOpts
* @param [o] Conversion options
* @returns Plain object
*/
public static toObject(m: TopicDescriptor.AuthOpts, o?: $protobuf.IConversionOptions): { [k: string]: any };
/**
* Converts this AuthOpts to JSON.
* @returns JSON object
*/
public toJSON(): { [k: string]: any };
}
namespace AuthOpts {
/** AuthMode enum. */
enum AuthMode {
NONE = 0,
KEY = 1,
WOT = 2
}
}
/** Properties of an EncOpts. */
interface IEncOpts {
/** EncOpts mode */
mode?: (TopicDescriptor.EncOpts.EncMode|null);
/** EncOpts keyHashes */
keyHashes?: (Uint8Array[]|null);
}
/** Represents an EncOpts. */
class EncOpts implements IEncOpts {
/**
* Constructs a new EncOpts.
* @param [p] Properties to set
*/
constructor(p?: TopicDescriptor.IEncOpts);
/** EncOpts mode. */
public mode: TopicDescriptor.EncOpts.EncMode;
/** EncOpts keyHashes. */
public keyHashes: Uint8Array[];
/**
* Encodes the specified EncOpts message. Does not implicitly {@link TopicDescriptor.EncOpts.verify|verify} messages.
* @param m EncOpts message or plain object to encode
* @param [w] Writer to encode to
* @returns Writer
*/
public static encode(m: TopicDescriptor.IEncOpts, w?: $protobuf.Writer): $protobuf.Writer;
/**
* Decodes an EncOpts message from the specified reader or buffer.
* @param r Reader or buffer to decode from
* @param [l] Message length if known beforehand
* @returns EncOpts
* @throws {Error} If the payload is not a reader or valid buffer
* @throws {$protobuf.util.ProtocolError} If required fields are missing
*/
public static decode(r: ($protobuf.Reader|Uint8Array), l?: number): TopicDescriptor.EncOpts;
/**
* Creates an EncOpts message from a plain object. Also converts values to their respective internal types.
* @param d Plain object
* @returns EncOpts
*/
public static fromObject(d: { [k: string]: any }): TopicDescriptor.EncOpts;
/**
* Creates a plain object from an EncOpts message. Also converts values to other types if specified.
* @param m EncOpts
* @param [o] Conversion options
* @returns Plain object
*/
public static toObject(m: TopicDescriptor.EncOpts, o?: $protobuf.IConversionOptions): { [k: string]: any };
/**
* Converts this EncOpts to JSON.
* @returns JSON object
*/
public toJSON(): { [k: string]: any };
}
namespace EncOpts {
/** EncMode enum. */
enum EncMode {
NONE = 0,
SHAREDKEY = 1,
WOT = 2
}
}
}

View File

@ -0,0 +1,586 @@
/*eslint-disable*/
"use strict";
var $protobuf = require("protobufjs/minimal");
// Common aliases
var $Reader = $protobuf.Reader, $Writer = $protobuf.Writer, $util = $protobuf.util;
// Exported root namespace
var $root = $protobuf.roots["default"] || ($protobuf.roots["default"] = {});
$root.TopicDescriptor = (function() {
/**
* Properties of a TopicDescriptor.
* @exports ITopicDescriptor
* @interface ITopicDescriptor
* @property {string|null} [name] TopicDescriptor name
* @property {TopicDescriptor.IAuthOpts|null} [auth] TopicDescriptor auth
* @property {TopicDescriptor.IEncOpts|null} [enc] TopicDescriptor enc
*/
/**
* Constructs a new TopicDescriptor.
* @exports TopicDescriptor
* @classdesc Represents a TopicDescriptor.
* @implements ITopicDescriptor
* @constructor
* @param {ITopicDescriptor=} [p] Properties to set
*/
function TopicDescriptor(p) {
if (p)
for (var ks = Object.keys(p), i = 0; i < ks.length; ++i)
if (p[ks[i]] != null)
this[ks[i]] = p[ks[i]];
}
/**
* TopicDescriptor name.
* @member {string} name
* @memberof TopicDescriptor
* @instance
*/
TopicDescriptor.prototype.name = "";
/**
* TopicDescriptor auth.
* @member {TopicDescriptor.IAuthOpts|null|undefined} auth
* @memberof TopicDescriptor
* @instance
*/
TopicDescriptor.prototype.auth = null;
/**
* TopicDescriptor enc.
* @member {TopicDescriptor.IEncOpts|null|undefined} enc
* @memberof TopicDescriptor
* @instance
*/
TopicDescriptor.prototype.enc = null;
/**
* Encodes the specified TopicDescriptor message. Does not implicitly {@link TopicDescriptor.verify|verify} messages.
* @function encode
* @memberof TopicDescriptor
* @static
* @param {ITopicDescriptor} m TopicDescriptor message or plain object to encode
* @param {$protobuf.Writer} [w] Writer to encode to
* @returns {$protobuf.Writer} Writer
*/
TopicDescriptor.encode = function encode(m, w) {
if (!w)
w = $Writer.create();
if (m.name != null && Object.hasOwnProperty.call(m, "name"))
w.uint32(10).string(m.name);
if (m.auth != null && Object.hasOwnProperty.call(m, "auth"))
$root.TopicDescriptor.AuthOpts.encode(m.auth, w.uint32(18).fork()).ldelim();
if (m.enc != null && Object.hasOwnProperty.call(m, "enc"))
$root.TopicDescriptor.EncOpts.encode(m.enc, w.uint32(26).fork()).ldelim();
return w;
};
/**
* Decodes a TopicDescriptor message from the specified reader or buffer.
* @function decode
* @memberof TopicDescriptor
* @static
* @param {$protobuf.Reader|Uint8Array} r Reader or buffer to decode from
* @param {number} [l] Message length if known beforehand
* @returns {TopicDescriptor} TopicDescriptor
* @throws {Error} If the payload is not a reader or valid buffer
* @throws {$protobuf.util.ProtocolError} If required fields are missing
*/
TopicDescriptor.decode = function decode(r, l) {
if (!(r instanceof $Reader))
r = $Reader.create(r);
var c = l === undefined ? r.len : r.pos + l, m = new $root.TopicDescriptor();
while (r.pos < c) {
var t = r.uint32();
switch (t >>> 3) {
case 1:
m.name = r.string();
break;
case 2:
m.auth = $root.TopicDescriptor.AuthOpts.decode(r, r.uint32());
break;
case 3:
m.enc = $root.TopicDescriptor.EncOpts.decode(r, r.uint32());
break;
default:
r.skipType(t & 7);
break;
}
}
return m;
};
/**
* Creates a TopicDescriptor message from a plain object. Also converts values to their respective internal types.
* @function fromObject
* @memberof TopicDescriptor
* @static
* @param {Object.<string,*>} d Plain object
* @returns {TopicDescriptor} TopicDescriptor
*/
TopicDescriptor.fromObject = function fromObject(d) {
if (d instanceof $root.TopicDescriptor)
return d;
var m = new $root.TopicDescriptor();
if (d.name != null) {
m.name = String(d.name);
}
if (d.auth != null) {
if (typeof d.auth !== "object")
throw TypeError(".TopicDescriptor.auth: object expected");
m.auth = $root.TopicDescriptor.AuthOpts.fromObject(d.auth);
}
if (d.enc != null) {
if (typeof d.enc !== "object")
throw TypeError(".TopicDescriptor.enc: object expected");
m.enc = $root.TopicDescriptor.EncOpts.fromObject(d.enc);
}
return m;
};
/**
* Creates a plain object from a TopicDescriptor message. Also converts values to other types if specified.
* @function toObject
* @memberof TopicDescriptor
* @static
* @param {TopicDescriptor} m TopicDescriptor
* @param {$protobuf.IConversionOptions} [o] Conversion options
* @returns {Object.<string,*>} Plain object
*/
TopicDescriptor.toObject = function toObject(m, o) {
if (!o)
o = {};
var d = {};
if (o.defaults) {
d.name = "";
d.auth = null;
d.enc = null;
}
if (m.name != null && m.hasOwnProperty("name")) {
d.name = m.name;
}
if (m.auth != null && m.hasOwnProperty("auth")) {
d.auth = $root.TopicDescriptor.AuthOpts.toObject(m.auth, o);
}
if (m.enc != null && m.hasOwnProperty("enc")) {
d.enc = $root.TopicDescriptor.EncOpts.toObject(m.enc, o);
}
return d;
};
/**
* Converts this TopicDescriptor to JSON.
* @function toJSON
* @memberof TopicDescriptor
* @instance
* @returns {Object.<string,*>} JSON object
*/
TopicDescriptor.prototype.toJSON = function toJSON() {
return this.constructor.toObject(this, $protobuf.util.toJSONOptions);
};
TopicDescriptor.AuthOpts = (function() {
/**
* Properties of an AuthOpts.
* @memberof TopicDescriptor
* @interface IAuthOpts
* @property {TopicDescriptor.AuthOpts.AuthMode|null} [mode] AuthOpts mode
* @property {Array.<Uint8Array>|null} [keys] AuthOpts keys
*/
/**
* Constructs a new AuthOpts.
* @memberof TopicDescriptor
* @classdesc Represents an AuthOpts.
* @implements IAuthOpts
* @constructor
* @param {TopicDescriptor.IAuthOpts=} [p] Properties to set
*/
function AuthOpts(p) {
this.keys = [];
if (p)
for (var ks = Object.keys(p), i = 0; i < ks.length; ++i)
if (p[ks[i]] != null)
this[ks[i]] = p[ks[i]];
}
/**
* AuthOpts mode.
* @member {TopicDescriptor.AuthOpts.AuthMode} mode
* @memberof TopicDescriptor.AuthOpts
* @instance
*/
AuthOpts.prototype.mode = 0;
/**
* AuthOpts keys.
* @member {Array.<Uint8Array>} keys
* @memberof TopicDescriptor.AuthOpts
* @instance
*/
AuthOpts.prototype.keys = $util.emptyArray;
/**
* Encodes the specified AuthOpts message. Does not implicitly {@link TopicDescriptor.AuthOpts.verify|verify} messages.
* @function encode
* @memberof TopicDescriptor.AuthOpts
* @static
* @param {TopicDescriptor.IAuthOpts} m AuthOpts message or plain object to encode
* @param {$protobuf.Writer} [w] Writer to encode to
* @returns {$protobuf.Writer} Writer
*/
AuthOpts.encode = function encode(m, w) {
if (!w)
w = $Writer.create();
if (m.mode != null && Object.hasOwnProperty.call(m, "mode"))
w.uint32(8).int32(m.mode);
if (m.keys != null && m.keys.length) {
for (var i = 0; i < m.keys.length; ++i)
w.uint32(18).bytes(m.keys[i]);
}
return w;
};
/**
* Decodes an AuthOpts message from the specified reader or buffer.
* @function decode
* @memberof TopicDescriptor.AuthOpts
* @static
* @param {$protobuf.Reader|Uint8Array} r Reader or buffer to decode from
* @param {number} [l] Message length if known beforehand
* @returns {TopicDescriptor.AuthOpts} AuthOpts
* @throws {Error} If the payload is not a reader or valid buffer
* @throws {$protobuf.util.ProtocolError} If required fields are missing
*/
AuthOpts.decode = function decode(r, l) {
if (!(r instanceof $Reader))
r = $Reader.create(r);
var c = l === undefined ? r.len : r.pos + l, m = new $root.TopicDescriptor.AuthOpts();
while (r.pos < c) {
var t = r.uint32();
switch (t >>> 3) {
case 1:
m.mode = r.int32();
break;
case 2:
if (!(m.keys && m.keys.length))
m.keys = [];
m.keys.push(r.bytes());
break;
default:
r.skipType(t & 7);
break;
}
}
return m;
};
/**
* Creates an AuthOpts message from a plain object. Also converts values to their respective internal types.
* @function fromObject
* @memberof TopicDescriptor.AuthOpts
* @static
* @param {Object.<string,*>} d Plain object
* @returns {TopicDescriptor.AuthOpts} AuthOpts
*/
AuthOpts.fromObject = function fromObject(d) {
if (d instanceof $root.TopicDescriptor.AuthOpts)
return d;
var m = new $root.TopicDescriptor.AuthOpts();
switch (d.mode) {
case "NONE":
case 0:
m.mode = 0;
break;
case "KEY":
case 1:
m.mode = 1;
break;
case "WOT":
case 2:
m.mode = 2;
break;
}
if (d.keys) {
if (!Array.isArray(d.keys))
throw TypeError(".TopicDescriptor.AuthOpts.keys: array expected");
m.keys = [];
for (var i = 0; i < d.keys.length; ++i) {
if (typeof d.keys[i] === "string")
$util.base64.decode(d.keys[i], m.keys[i] = $util.newBuffer($util.base64.length(d.keys[i])), 0);
else if (d.keys[i].length)
m.keys[i] = d.keys[i];
}
}
return m;
};
/**
* Creates a plain object from an AuthOpts message. Also converts values to other types if specified.
* @function toObject
* @memberof TopicDescriptor.AuthOpts
* @static
* @param {TopicDescriptor.AuthOpts} m AuthOpts
* @param {$protobuf.IConversionOptions} [o] Conversion options
* @returns {Object.<string,*>} Plain object
*/
AuthOpts.toObject = function toObject(m, o) {
if (!o)
o = {};
var d = {};
if (o.arrays || o.defaults) {
d.keys = [];
}
if (o.defaults) {
d.mode = o.enums === String ? "NONE" : 0;
}
if (m.mode != null && m.hasOwnProperty("mode")) {
d.mode = o.enums === String ? $root.TopicDescriptor.AuthOpts.AuthMode[m.mode] : m.mode;
}
if (m.keys && m.keys.length) {
d.keys = [];
for (var j = 0; j < m.keys.length; ++j) {
d.keys[j] = o.bytes === String ? $util.base64.encode(m.keys[j], 0, m.keys[j].length) : o.bytes === Array ? Array.prototype.slice.call(m.keys[j]) : m.keys[j];
}
}
return d;
};
/**
* Converts this AuthOpts to JSON.
* @function toJSON
* @memberof TopicDescriptor.AuthOpts
* @instance
* @returns {Object.<string,*>} JSON object
*/
AuthOpts.prototype.toJSON = function toJSON() {
return this.constructor.toObject(this, $protobuf.util.toJSONOptions);
};
/**
* AuthMode enum.
* @name TopicDescriptor.AuthOpts.AuthMode
* @enum {number}
* @property {number} NONE=0 NONE value
* @property {number} KEY=1 KEY value
* @property {number} WOT=2 WOT value
*/
AuthOpts.AuthMode = (function() {
var valuesById = {}, values = Object.create(valuesById);
values[valuesById[0] = "NONE"] = 0;
values[valuesById[1] = "KEY"] = 1;
values[valuesById[2] = "WOT"] = 2;
return values;
})();
return AuthOpts;
})();
TopicDescriptor.EncOpts = (function() {
/**
* Properties of an EncOpts.
* @memberof TopicDescriptor
* @interface IEncOpts
* @property {TopicDescriptor.EncOpts.EncMode|null} [mode] EncOpts mode
* @property {Array.<Uint8Array>|null} [keyHashes] EncOpts keyHashes
*/
/**
* Constructs a new EncOpts.
* @memberof TopicDescriptor
* @classdesc Represents an EncOpts.
* @implements IEncOpts
* @constructor
* @param {TopicDescriptor.IEncOpts=} [p] Properties to set
*/
function EncOpts(p) {
this.keyHashes = [];
if (p)
for (var ks = Object.keys(p), i = 0; i < ks.length; ++i)
if (p[ks[i]] != null)
this[ks[i]] = p[ks[i]];
}
/**
* EncOpts mode.
* @member {TopicDescriptor.EncOpts.EncMode} mode
* @memberof TopicDescriptor.EncOpts
* @instance
*/
EncOpts.prototype.mode = 0;
/**
* EncOpts keyHashes.
* @member {Array.<Uint8Array>} keyHashes
* @memberof TopicDescriptor.EncOpts
* @instance
*/
EncOpts.prototype.keyHashes = $util.emptyArray;
/**
* Encodes the specified EncOpts message. Does not implicitly {@link TopicDescriptor.EncOpts.verify|verify} messages.
* @function encode
* @memberof TopicDescriptor.EncOpts
* @static
* @param {TopicDescriptor.IEncOpts} m EncOpts message or plain object to encode
* @param {$protobuf.Writer} [w] Writer to encode to
* @returns {$protobuf.Writer} Writer
*/
EncOpts.encode = function encode(m, w) {
if (!w)
w = $Writer.create();
if (m.mode != null && Object.hasOwnProperty.call(m, "mode"))
w.uint32(8).int32(m.mode);
if (m.keyHashes != null && m.keyHashes.length) {
for (var i = 0; i < m.keyHashes.length; ++i)
w.uint32(18).bytes(m.keyHashes[i]);
}
return w;
};
/**
* Decodes an EncOpts message from the specified reader or buffer.
* @function decode
* @memberof TopicDescriptor.EncOpts
* @static
* @param {$protobuf.Reader|Uint8Array} r Reader or buffer to decode from
* @param {number} [l] Message length if known beforehand
* @returns {TopicDescriptor.EncOpts} EncOpts
* @throws {Error} If the payload is not a reader or valid buffer
* @throws {$protobuf.util.ProtocolError} If required fields are missing
*/
EncOpts.decode = function decode(r, l) {
if (!(r instanceof $Reader))
r = $Reader.create(r);
var c = l === undefined ? r.len : r.pos + l, m = new $root.TopicDescriptor.EncOpts();
while (r.pos < c) {
var t = r.uint32();
switch (t >>> 3) {
case 1:
m.mode = r.int32();
break;
case 2:
if (!(m.keyHashes && m.keyHashes.length))
m.keyHashes = [];
m.keyHashes.push(r.bytes());
break;
default:
r.skipType(t & 7);
break;
}
}
return m;
};
/**
* Creates an EncOpts message from a plain object. Also converts values to their respective internal types.
* @function fromObject
* @memberof TopicDescriptor.EncOpts
* @static
* @param {Object.<string,*>} d Plain object
* @returns {TopicDescriptor.EncOpts} EncOpts
*/
EncOpts.fromObject = function fromObject(d) {
if (d instanceof $root.TopicDescriptor.EncOpts)
return d;
var m = new $root.TopicDescriptor.EncOpts();
switch (d.mode) {
case "NONE":
case 0:
m.mode = 0;
break;
case "SHAREDKEY":
case 1:
m.mode = 1;
break;
case "WOT":
case 2:
m.mode = 2;
break;
}
if (d.keyHashes) {
if (!Array.isArray(d.keyHashes))
throw TypeError(".TopicDescriptor.EncOpts.keyHashes: array expected");
m.keyHashes = [];
for (var i = 0; i < d.keyHashes.length; ++i) {
if (typeof d.keyHashes[i] === "string")
$util.base64.decode(d.keyHashes[i], m.keyHashes[i] = $util.newBuffer($util.base64.length(d.keyHashes[i])), 0);
else if (d.keyHashes[i].length)
m.keyHashes[i] = d.keyHashes[i];
}
}
return m;
};
/**
* Creates a plain object from an EncOpts message. Also converts values to other types if specified.
* @function toObject
* @memberof TopicDescriptor.EncOpts
* @static
* @param {TopicDescriptor.EncOpts} m EncOpts
* @param {$protobuf.IConversionOptions} [o] Conversion options
* @returns {Object.<string,*>} Plain object
*/
EncOpts.toObject = function toObject(m, o) {
if (!o)
o = {};
var d = {};
if (o.arrays || o.defaults) {
d.keyHashes = [];
}
if (o.defaults) {
d.mode = o.enums === String ? "NONE" : 0;
}
if (m.mode != null && m.hasOwnProperty("mode")) {
d.mode = o.enums === String ? $root.TopicDescriptor.EncOpts.EncMode[m.mode] : m.mode;
}
if (m.keyHashes && m.keyHashes.length) {
d.keyHashes = [];
for (var j = 0; j < m.keyHashes.length; ++j) {
d.keyHashes[j] = o.bytes === String ? $util.base64.encode(m.keyHashes[j], 0, m.keyHashes[j].length) : o.bytes === Array ? Array.prototype.slice.call(m.keyHashes[j]) : m.keyHashes[j];
}
}
return d;
};
/**
* Converts this EncOpts to JSON.
* @function toJSON
* @memberof TopicDescriptor.EncOpts
* @instance
* @returns {Object.<string,*>} JSON object
*/
EncOpts.prototype.toJSON = function toJSON() {
return this.constructor.toObject(this, $protobuf.util.toJSONOptions);
};
/**
* EncMode enum.
* @name TopicDescriptor.EncOpts.EncMode
* @enum {number}
* @property {number} NONE=0 NONE value
* @property {number} SHAREDKEY=1 SHAREDKEY value
* @property {number} WOT=2 WOT value
*/
EncOpts.EncMode = (function() {
var valuesById = {}, values = Object.create(valuesById);
values[valuesById[0] = "NONE"] = 0;
values[valuesById[1] = "SHAREDKEY"] = 1;
values[valuesById[2] = "WOT"] = 2;
return values;
})();
return EncOpts;
})();
return TopicDescriptor;
})();
module.exports = $root;

View File

@ -0,0 +1,30 @@
syntax = "proto3";
// topicCID = cid(merkledag_protobuf(topicDescriptor)); (not the topic.name)
message TopicDescriptor {
optional string name = 1;
optional AuthOpts auth = 2;
optional EncOpts enc = 3;
message AuthOpts {
optional AuthMode mode = 1;
repeated bytes keys = 2; // root keys to trust
enum AuthMode {
NONE = 0; // no authentication, anyone can publish
KEY = 1; // only messages signed by keys in the topic descriptor are accepted
WOT = 2; // web of trust, certificates can allow publisher set to grow
}
}
message EncOpts {
optional EncMode mode = 1;
repeated bytes keyHashes = 2; // the hashes of the shared keys used (salted)
enum EncMode {
NONE = 0; // no encryption, anyone can read
SHAREDKEY = 1; // messages are encrypted with shared key
WOT = 2; // web of trust, certificates can allow publisher set to grow
}
}
}

206
src/pubsub/peer-streams.js Normal file
View File

@ -0,0 +1,206 @@
'use strict'
const debug = require('debug')
const log = Object.assign(debug('libp2p-pubsub:peer-streams'), {
error: debug('libp2p-pubsub:peer-streams:err')
})
/** @typedef {import('../types').EventEmitterFactory} Events */
/** @type Events */
const EventEmitter = require('events')
// @ts-ignore TODO: https://github.com/alanshaw/it-length-prefixed/pull/15
const lp = require('it-length-prefixed')
const pushable = require('it-pushable')
const { pipe } = require('it-pipe')
const { source: abortable } = require('abortable-iterator')
const AbortController = require('abort-controller').default
/**
* @typedef {import('../stream-muxer/types').MuxedStream} MuxedStream
* @typedef {import('peer-id')} PeerId
* @typedef {import('it-pushable').Pushable<Uint8Array>} PushableStream
*/
/**
* Thin wrapper around a peer's inbound / outbound pubsub streams
*/
class PeerStreams extends EventEmitter {
/**
* @param {object} properties - properties of the PeerStreams.
* @param {PeerId} properties.id
* @param {string} properties.protocol
*/
constructor ({ id, protocol }) {
super()
/**
* @type {import('peer-id')}
*/
this.id = id
/**
* Established protocol
*
* @type {string}
*/
this.protocol = protocol
/**
* The raw outbound stream, as retrieved from conn.newStream
*
* @private
* @type {null|MuxedStream}
*/
this._rawOutboundStream = null
/**
* The raw inbound stream, as retrieved from the callback from libp2p.handle
*
* @private
* @type {null|MuxedStream}
*/
this._rawInboundStream = null
/**
* An AbortController for controlled shutdown of the inbound stream
*
* @private
* @type {AbortController}
*/
this._inboundAbortController = new AbortController()
/**
* Write stream -- its preferable to use the write method
*
* @type {null|PushableStream}
*/
this.outboundStream = null
/**
* Read stream
*
* @type {null| AsyncIterable<Uint8Array>}
*/
this.inboundStream = null
}
/**
* Do we have a connection to read from?
*
* @type {boolean}
*/
get isReadable () {
return Boolean(this.inboundStream)
}
/**
* Do we have a connection to write on?
*
* @type {boolean}
*/
get isWritable () {
return Boolean(this.outboundStream)
}
/**
* Send a message to this peer.
* Throws if there is no `stream` to write to available.
*
* @param {Uint8Array} data
* @returns {void}
*/
write (data) {
if (!this.outboundStream) {
const id = this.id.toB58String()
throw new Error('No writable connection to ' + id)
}
this.outboundStream.push(data)
}
/**
* Attach a raw inbound stream and setup a read stream
*
* @param {MuxedStream} stream
* @returns {AsyncIterable<Uint8Array>}
*/
attachInboundStream (stream) {
// Create and attach a new inbound stream
// The inbound stream is:
// - abortable, set to only return on abort, rather than throw
// - transformed with length-prefix transform
this._rawInboundStream = stream
this.inboundStream = abortable(
pipe(
this._rawInboundStream,
lp.decode()
),
this._inboundAbortController.signal,
{ returnOnAbort: true }
)
this.emit('stream:inbound')
return this.inboundStream
}
/**
* Attach a raw outbound stream and setup a write stream
*
* @param {MuxedStream} stream
* @returns {Promise<void>}
*/
async attachOutboundStream (stream) {
// If an outbound stream already exists, gently close it
const _prevStream = this.outboundStream
if (this.outboundStream) {
// End the stream without emitting a close event
await this.outboundStream.end()
}
this._rawOutboundStream = stream
this.outboundStream = pushable({
onEnd: (shouldEmit) => {
// close writable side of the stream
this._rawOutboundStream && this._rawOutboundStream.reset && this._rawOutboundStream.reset()
this._rawOutboundStream = null
this.outboundStream = null
if (shouldEmit) {
this.emit('close')
}
}
})
pipe(
this.outboundStream,
lp.encode(),
this._rawOutboundStream
).catch(/** @param {Error} err */ err => {
log.error(err)
})
// Only emit if the connection is new
if (!_prevStream) {
this.emit('stream:outbound')
}
}
/**
* Closes the open connection to peer
*
* @returns {void}
*/
close () {
// End the outbound stream
if (this.outboundStream) {
this.outboundStream.end()
}
// End the inbound stream
if (this.inboundStream) {
this._inboundAbortController.abort()
}
this._rawOutboundStream = null
this.outboundStream = null
this._rawInboundStream = null
this.inboundStream = null
this.emit('close')
}
}
module.exports = PeerStreams

View File

@ -0,0 +1,33 @@
'use strict'
/**
* Enum for Signature Policy
* Details how message signatures are produced/consumed
*/
const SignaturePolicy = {
/**
* On the producing side:
* * Build messages with the signature, key (from may be enough for certain inlineable public key types), from and seqno fields.
*
* On the consuming side:
* * Enforce the fields to be present, reject otherwise.
* * Propagate only if the fields are valid and signature can be verified, reject otherwise.
*/
StrictSign: /** @type {'StrictSign'} */ ('StrictSign'),
/**
* On the producing side:
* * Build messages without the signature, key, from and seqno fields.
* * The corresponding protobuf key-value pairs are absent from the marshalled message, not just empty.
*
* On the consuming side:
* * Enforce the fields to be absent, reject otherwise.
* * Propagate only if the fields are absent, reject otherwise.
* * A message_id function will not be able to use the above fields, and should instead rely on the data field. A commonplace strategy is to calculate a hash.
*/
StrictNoSign: /** @type {'StrictNoSign'} */ ('StrictNoSign')
}
exports.SignaturePolicy = SignaturePolicy
/**
* @typedef {SignaturePolicy[keyof SignaturePolicy]} SignaturePolicyType
*/

94
src/pubsub/tests/api.js Normal file
View File

@ -0,0 +1,94 @@
// @ts-nocheck interface tests
/* eslint-env mocha */
'use strict'
const chai = require('chai')
const { expect } = chai
const sinon = require('sinon')
const pDefer = require('p-defer')
const pWaitFor = require('p-wait-for')
const uint8ArrayFromString = require('uint8arrays/from-string')
const topic = 'foo'
const data = uint8ArrayFromString('bar')
module.exports = (common) => {
describe('pubsub api', () => {
let pubsub
// Create pubsub router
beforeEach(async () => {
[pubsub] = await common.setup(1)
})
afterEach(async () => {
sinon.restore()
pubsub && pubsub.stop()
await common.teardown()
})
it('can start correctly', () => {
sinon.spy(pubsub.registrar, '_handle')
sinon.spy(pubsub.registrar, 'register')
pubsub.start()
expect(pubsub.started).to.eql(true)
expect(pubsub.registrar._handle.callCount).to.eql(1)
expect(pubsub.registrar.register.callCount).to.eql(1)
})
it('can stop correctly', () => {
sinon.spy(pubsub.registrar, 'unregister')
pubsub.start()
pubsub.stop()
expect(pubsub.started).to.eql(false)
expect(pubsub.registrar.unregister.callCount).to.eql(1)
})
it('can subscribe and unsubscribe correctly', async () => {
const handler = () => {
throw new Error('a message should not be received')
}
pubsub.start()
pubsub.subscribe(topic)
pubsub.on('topic', handler)
await pWaitFor(() => {
const topics = pubsub.getTopics()
return topics.length === 1 && topics[0] === topic
})
pubsub.unsubscribe(topic)
await pWaitFor(() => !pubsub.getTopics().length)
// Publish to guarantee the handler is not called
await pubsub.publish(topic, data)
pubsub.stop()
})
it('can subscribe and publish correctly', async () => {
const defer = pDefer()
const handler = (msg) => {
expect(msg).to.not.eql(undefined)
defer.resolve()
}
pubsub.start()
pubsub.subscribe(topic)
pubsub.on(topic, handler)
await pubsub.publish(topic, data)
await defer.promise
pubsub.stop()
})
})
}

View File

@ -0,0 +1,70 @@
// @ts-nocheck interface tests
/* eslint-env mocha */
'use strict'
const chai = require('chai')
const { expect } = chai
const sinon = require('sinon')
const uint8ArrayFromString = require('uint8arrays/from-string')
const topic = 'foo'
const data = uint8ArrayFromString('bar')
const shouldNotHappen = (_) => expect.fail()
module.exports = (common) => {
describe('emit self', () => {
let pubsub
describe('enabled', () => {
before(async () => {
[pubsub] = await common.setup(1, { emitSelf: true })
})
before(() => {
pubsub.start()
pubsub.subscribe(topic)
})
after(async () => {
sinon.restore()
pubsub && pubsub.stop()
await common.teardown()
})
it('should emit to self on publish', () => {
const promise = new Promise((resolve) => pubsub.once(topic, resolve))
pubsub.publish(topic, data)
return promise
})
})
describe('disabled', () => {
before(async () => {
[pubsub] = await common.setup(1, { emitSelf: false })
})
before(() => {
pubsub.start()
pubsub.subscribe(topic)
})
after(async () => {
sinon.restore()
pubsub && pubsub.stop()
await common.teardown()
})
it('should not emit to self on publish', () => {
pubsub.once(topic, (m) => shouldNotHappen)
pubsub.publish(topic, data)
// Wait 1 second to guarantee that self is not noticed
return new Promise((resolve) => setTimeout(resolve, 1000))
})
})
})
}

19
src/pubsub/tests/index.js Normal file
View File

@ -0,0 +1,19 @@
// @ts-nocheck interface tests
/* eslint-env mocha */
'use strict'
const apiTest = require('./api')
const emitSelfTest = require('./emit-self')
const messagesTest = require('./messages')
const twoNodesTest = require('./two-nodes')
const multipleNodesTest = require('./multiple-nodes')
module.exports = (common) => {
describe('interface-pubsub', () => {
apiTest(common)
emitSelfTest(common)
messagesTest(common)
twoNodesTest(common)
multipleNodesTest(common)
})
}

View File

@ -0,0 +1,116 @@
// @ts-nocheck interface tests
/* eslint-env mocha */
'use strict'
const chai = require('chai')
const { expect } = chai
const sinon = require('sinon')
const PeerId = require('peer-id')
const uint8ArrayFromString = require('uint8arrays/from-string')
const { utils } = require('..')
const PeerStreams = require('../peer-streams')
const { SignaturePolicy } = require('../signature-policy')
const topic = 'foo'
const data = uint8ArrayFromString('bar')
module.exports = (common) => {
describe('messages', () => {
let pubsub
// Create pubsub router
beforeEach(async () => {
[pubsub] = await common.setup(1)
pubsub.start()
})
afterEach(async () => {
sinon.restore()
pubsub && pubsub.stop()
await common.teardown()
})
it('should emit normalized signed messages on publish', async () => {
pubsub.globalSignaturePolicy = SignaturePolicy.StrictSign
sinon.spy(pubsub, '_emitMessage')
await pubsub.publish(topic, data)
expect(pubsub._emitMessage.callCount).to.eql(1)
const [messageToEmit] = pubsub._emitMessage.getCall(0).args
expect(messageToEmit.seqno).to.not.eql(undefined)
expect(messageToEmit.key).to.not.eql(undefined)
expect(messageToEmit.signature).to.not.eql(undefined)
})
it('should drop unsigned messages', async () => {
sinon.spy(pubsub, '_emitMessage')
sinon.spy(pubsub, '_publish')
sinon.spy(pubsub, 'validate')
const peerStream = new PeerStreams({
id: await PeerId.create(),
protocol: 'test'
})
const rpc = {
subscriptions: [],
msgs: [{
receivedFrom: peerStream.id.toB58String(),
from: peerStream.id.toBytes(),
data,
seqno: utils.randomSeqno(),
topicIDs: [topic]
}]
}
pubsub.subscribe(topic)
pubsub._processRpc(peerStream.id.toB58String(), peerStream, rpc)
return new Promise((resolve) => {
setTimeout(() => {
expect(pubsub.validate.callCount).to.eql(1)
expect(pubsub._emitMessage.called).to.eql(false)
expect(pubsub._publish.called).to.eql(false)
resolve()
}, 50)
})
})
it('should not drop unsigned messages if strict signing is disabled', async () => {
pubsub.globalSignaturePolicy = SignaturePolicy.StrictNoSign
sinon.spy(pubsub, '_emitMessage')
sinon.spy(pubsub, '_publish')
sinon.spy(pubsub, 'validate')
const peerStream = new PeerStreams({
id: await PeerId.create(),
protocol: 'test'
})
const rpc = {
subscriptions: [],
msgs: [{
data,
topicIDs: [topic]
}]
}
pubsub.subscribe(topic)
pubsub._processRpc(peerStream.id.toB58String(), peerStream, rpc)
return new Promise((resolve) => {
setTimeout(() => {
expect(pubsub.validate.callCount).to.eql(1)
expect(pubsub._emitMessage.called).to.eql(true)
expect(pubsub._publish.called).to.eql(true)
resolve()
}, 50)
})
})
})
}

View File

@ -0,0 +1,341 @@
// @ts-nocheck interface tests
/* eslint-env mocha */
/* eslint max-nested-callbacks: ["error", 6] */
'use strict'
const chai = require('chai')
const { expect } = chai
const sinon = require('sinon')
const delay = require('delay')
const pDefer = require('p-defer')
const pWaitFor = require('p-wait-for')
const uint8ArrayFromString = require('uint8arrays/from-string')
const uint8ArrayToString = require('uint8arrays/to-string')
const { expectSet } = require('./utils')
module.exports = (common) => {
describe('pubsub with multiple nodes', function () {
this.timeout(10e3)
describe('every peer subscribes to the topic', () => {
describe('line', () => {
// line
// ◉────◉────◉
// a b c
let psA, psB, psC
// Create and start pubsub nodes
beforeEach(async () => {
[psA, psB, psC] = await common.setup(3)
// Start pubsub mpdes
;[psA, psB, psC].map((p) => p.start())
})
// Connect nodes
beforeEach(async () => {
await psA._libp2p.dial(psB.peerId)
await psB._libp2p.dial(psC.peerId)
// Wait for peers to be ready in pubsub
await pWaitFor(() =>
psA.peers.size === 1 &&
psC.peers.size === 1 &&
psA.peers.size === 1
)
})
afterEach(async () => {
sinon.restore()
;[psA, psB, psC].map((p) => p.stop())
await common.teardown()
})
it('subscribe to the topic on node a', async () => {
const topic = 'Z'
psA.subscribe(topic)
expectSet(psA.subscriptions, [topic])
await new Promise((resolve) => psB.once('pubsub:subscription-change', resolve))
expect(psB.peers.size).to.equal(2)
const aPeerId = psA.peerId.toB58String()
expectSet(psB.topics.get(topic), [aPeerId])
expect(psC.peers.size).to.equal(1)
expect(psC.topics.get(topic)).to.eql(undefined)
})
it('subscribe to the topic on node b', async () => {
const topic = 'Z'
psB.subscribe(topic)
expectSet(psB.subscriptions, [topic])
await Promise.all([
new Promise((resolve) => psA.once('pubsub:subscription-change', resolve)),
new Promise((resolve) => psC.once('pubsub:subscription-change', resolve))
])
expect(psA.peers.size).to.equal(1)
expectSet(psA.topics.get(topic), [psB.peerId.toB58String()])
expect(psC.peers.size).to.equal(1)
expectSet(psC.topics.get(topic), [psB.peerId.toB58String()])
})
it('subscribe to the topic on node c', () => {
const topic = 'Z'
const defer = pDefer()
psC.subscribe(topic)
expectSet(psC.subscriptions, [topic])
psB.once('pubsub:subscription-change', () => {
expect(psA.peers.size).to.equal(1)
expect(psB.peers.size).to.equal(2)
expectSet(psB.topics.get(topic), [psC.peerId.toB58String()])
defer.resolve()
})
return defer.promise
})
it('publish on node a', async () => {
const topic = 'Z'
const defer = pDefer()
psA.subscribe(topic)
psB.subscribe(topic)
psC.subscribe(topic)
// await subscription change
await Promise.all([
new Promise(resolve => psA.once('pubsub:subscription-change', () => resolve(null))),
new Promise(resolve => psB.once('pubsub:subscription-change', () => resolve(null))),
new Promise(resolve => psC.once('pubsub:subscription-change', () => resolve(null)))
])
// await a cycle
await delay(1000)
let counter = 0
psA.on(topic, incMsg)
psB.on(topic, incMsg)
psC.on(topic, incMsg)
psA.publish(topic, uint8ArrayFromString('hey'))
function incMsg (msg) {
expect(uint8ArrayToString(msg.data)).to.equal('hey')
check()
}
function check () {
if (++counter === 3) {
psA.removeListener(topic, incMsg)
psB.removeListener(topic, incMsg)
psC.removeListener(topic, incMsg)
defer.resolve()
}
}
return defer.promise
})
// since the topology is the same, just the publish
// gets sent by other peer, we reused the same peers
describe('1 level tree', () => {
// 1 level tree
// ┌◉┐
// │b│
// ◉─┘ └─◉
// a c
it('publish on node b', async () => {
const topic = 'Z'
const defer = pDefer()
let counter = 0
psA.subscribe(topic)
psB.subscribe(topic)
psC.subscribe(topic)
// await subscription change
await Promise.all([
new Promise(resolve => psA.once('pubsub:subscription-change', () => resolve(null))),
new Promise(resolve => psB.once('pubsub:subscription-change', () => resolve(null))),
new Promise(resolve => psC.once('pubsub:subscription-change', () => resolve(null)))
])
psA.on(topic, incMsg)
psB.on(topic, incMsg)
psC.on(topic, incMsg)
// await a cycle
await delay(1000)
psB.publish(topic, uint8ArrayFromString('hey'))
function incMsg (msg) {
expect(uint8ArrayToString(msg.data)).to.equal('hey')
check()
}
function check () {
if (++counter === 3) {
psA.removeListener(topic, incMsg)
psB.removeListener(topic, incMsg)
psC.removeListener(topic, incMsg)
defer.resolve()
}
}
return defer.promise
})
})
})
describe('2 level tree', () => {
// 2 levels tree
// ┌◉┐
// │c│
// ┌◉─┘ └─◉┐
// │b d│
// ◉─┘ └─◉
// a
let psA, psB, psC, psD, psE
// Create and start pubsub nodes
beforeEach(async () => {
[psA, psB, psC, psD, psE] = await common.setup(5)
// Start pubsub nodes
;[psA, psB, psC, psD, psE].map((p) => p.start())
})
// connect nodes
beforeEach(async () => {
await psA._libp2p.dial(psB.peerId)
await psB._libp2p.dial(psC.peerId)
await psC._libp2p.dial(psD.peerId)
await psD._libp2p.dial(psE.peerId)
// Wait for peers to be ready in pubsub
await pWaitFor(() =>
psA.peers.size === 1 &&
psB.peers.size === 2 &&
psC.peers.size === 2 &&
psD.peers.size === 2 &&
psE.peers.size === 1
)
})
afterEach(async () => {
[psA, psB, psC, psD, psE].map((p) => p.stop())
await common.teardown()
})
it('subscribes', () => {
psA.subscribe('Z')
expectSet(psA.subscriptions, ['Z'])
psB.subscribe('Z')
expectSet(psB.subscriptions, ['Z'])
psC.subscribe('Z')
expectSet(psC.subscriptions, ['Z'])
psD.subscribe('Z')
expectSet(psD.subscriptions, ['Z'])
psE.subscribe('Z')
expectSet(psE.subscriptions, ['Z'])
})
it('publishes from c', async function () {
this.timeout(30 * 1000)
const defer = pDefer()
let counter = 0
psA.subscribe('Z')
psA.on('Z', incMsg)
psB.subscribe('Z')
psB.on('Z', incMsg)
psC.subscribe('Z')
psC.on('Z', incMsg)
psD.subscribe('Z')
psD.on('Z', incMsg)
psE.subscribe('Z')
psE.on('Z', incMsg)
await Promise.all([
new Promise((resolve) => psA.once('pubsub:subscription-change', resolve)),
new Promise((resolve) => psB.once('pubsub:subscription-change', resolve)),
new Promise((resolve) => psC.once('pubsub:subscription-change', resolve)),
new Promise((resolve) => psD.once('pubsub:subscription-change', resolve)),
new Promise((resolve) => psE.once('pubsub:subscription-change', resolve))
])
// await a cycle
await delay(1000)
psC.publish('Z', uint8ArrayFromString('hey from c'))
function incMsg (msg) {
expect(uint8ArrayToString(msg.data)).to.equal('hey from c')
check()
}
function check () {
if (++counter === 5) {
psA.unsubscribe('Z', incMsg)
psB.unsubscribe('Z', incMsg)
psC.unsubscribe('Z', incMsg)
psD.unsubscribe('Z', incMsg)
psE.unsubscribe('Z', incMsg)
defer.resolve()
}
}
return defer.promise
})
})
})
describe('only some nodes subscribe the networks', () => {
describe('line', () => {
// line
// ◉────◎────◉
// a b c
before(() => { })
after(() => { })
})
describe('1 level tree', () => {
// 1 level tree
// ┌◉┐
// │b│
// ◎─┘ └─◉
// a c
before(() => { })
after(() => { })
})
describe('2 level tree', () => {
// 2 levels tree
// ┌◉┐
// │c│
// ┌◎─┘ └─◉┐
// │b d│
// ◉─┘ └─◎
// a e
before(() => { })
after(() => { })
})
})
})
}

View File

@ -0,0 +1,233 @@
// @ts-nocheck interface tests
/* eslint-env mocha */
/* eslint max-nested-callbacks: ["error", 6] */
'use strict'
const chai = require('chai')
const { expect } = chai
const sinon = require('sinon')
const pDefer = require('p-defer')
const pWaitFor = require('p-wait-for')
const uint8ArrayFromString = require('uint8arrays/from-string')
const uint8ArrayToString = require('uint8arrays/to-string')
const {
first,
expectSet
} = require('./utils')
const topic = 'foo'
function shouldNotHappen (_) {
expect.fail()
}
module.exports = (common) => {
describe('pubsub with two nodes', () => {
describe('fresh nodes', () => {
let psA, psB
// Create pubsub nodes and connect them
before(async () => {
[psA, psB] = await common.setup(2)
expect(psA.peers.size).to.be.eql(0)
expect(psB.peers.size).to.be.eql(0)
// Start pubsub and connect nodes
psA.start()
psB.start()
await psA._libp2p.dial(psB.peerId)
// Wait for peers to be ready in pubsub
await pWaitFor(() => psA.peers.size === 1 && psB.peers.size === 1)
})
after(async () => {
sinon.restore()
psA && psA.stop()
psB && psB.stop()
await common.teardown()
})
it('Subscribe to a topic in nodeA', () => {
const defer = pDefer()
psB.once('pubsub:subscription-change', (changedPeerId, changedSubs) => {
expectSet(psA.subscriptions, [topic])
expect(psB.peers.size).to.equal(1)
expectSet(psB.topics.get(topic), [psA.peerId.toB58String()])
expect(changedPeerId.toB58String()).to.equal(first(psB.peers).id.toB58String())
expect(changedSubs).to.have.lengthOf(1)
expect(changedSubs[0].topicID).to.equal(topic)
expect(changedSubs[0].subscribe).to.equal(true)
defer.resolve()
})
psA.subscribe(topic)
return defer.promise
})
it('Publish to a topic in nodeA', () => {
const defer = pDefer()
psA.once(topic, (msg) => {
expect(uint8ArrayToString(msg.data)).to.equal('hey')
psB.removeListener(topic, shouldNotHappen)
defer.resolve()
})
psB.once(topic, shouldNotHappen)
psA.publish(topic, uint8ArrayFromString('hey'))
return defer.promise
})
it('Publish to a topic in nodeB', () => {
const defer = pDefer()
psA.once(topic, (msg) => {
psA.once(topic, shouldNotHappen)
expect(uint8ArrayToString(msg.data)).to.equal('banana')
setTimeout(() => {
psA.removeListener(topic, shouldNotHappen)
psB.removeListener(topic, shouldNotHappen)
defer.resolve()
}, 100)
})
psB.once(topic, shouldNotHappen)
psB.publish(topic, uint8ArrayFromString('banana'))
return defer.promise
})
it('Publish 10 msg to a topic in nodeB', () => {
const defer = pDefer()
let counter = 0
psB.once(topic, shouldNotHappen)
psA.on(topic, receivedMsg)
function receivedMsg (msg) {
expect(uint8ArrayToString(msg.data)).to.equal('banana')
expect(msg.from).to.be.eql(psB.peerId.toB58String())
expect(msg.seqno).to.be.a('Uint8Array')
expect(msg.topicIDs).to.be.eql([topic])
if (++counter === 10) {
psA.removeListener(topic, receivedMsg)
psB.removeListener(topic, shouldNotHappen)
defer.resolve()
}
}
Array.from({ length: 10 }, (_, i) => psB.publish(topic, uint8ArrayFromString('banana')))
return defer.promise
})
it('Unsubscribe from topic in nodeA', () => {
const defer = pDefer()
psA.unsubscribe(topic)
expect(psA.subscriptions.size).to.equal(0)
psB.once('pubsub:subscription-change', (changedPeerId, changedSubs) => {
expect(psB.peers.size).to.equal(1)
expectSet(psB.topics.get(topic), [])
expect(changedPeerId.toB58String()).to.equal(first(psB.peers).id.toB58String())
expect(changedSubs).to.have.lengthOf(1)
expect(changedSubs[0].topicID).to.equal(topic)
expect(changedSubs[0].subscribe).to.equal(false)
defer.resolve()
})
return defer.promise
})
it('Publish to a topic:Z in nodeA nodeB', () => {
const defer = pDefer()
psA.once('Z', shouldNotHappen)
psB.once('Z', shouldNotHappen)
setTimeout(() => {
psA.removeListener('Z', shouldNotHappen)
psB.removeListener('Z', shouldNotHappen)
defer.resolve()
}, 100)
psB.publish('Z', uint8ArrayFromString('banana'))
psA.publish('Z', uint8ArrayFromString('banana'))
return defer.promise
})
})
describe('nodes send state on connection', () => {
let psA, psB
// Create pubsub nodes and connect them
before(async () => {
[psA, psB] = await common.setup(2)
expect(psA.peers.size).to.be.eql(0)
expect(psB.peers.size).to.be.eql(0)
// Start pubsub and connect nodes
psA.start()
psB.start()
})
// Make subscriptions prior to nodes connected
before(() => {
psA.subscribe('Za')
psB.subscribe('Zb')
expect(psA.peers.size).to.equal(0)
expectSet(psA.subscriptions, ['Za'])
expect(psB.peers.size).to.equal(0)
expectSet(psB.subscriptions, ['Zb'])
})
after(async () => {
sinon.restore()
psA && psA.stop()
psB && psB.stop()
await common.teardown()
})
it('existing subscriptions are sent upon peer connection', async function () {
this.timeout(10e3)
await Promise.all([
psA._libp2p.dial(psB.peerId),
new Promise((resolve) => psA.once('pubsub:subscription-change', resolve)),
new Promise((resolve) => psB.once('pubsub:subscription-change', resolve))
])
expect(psA.peers.size).to.equal(1)
expect(psB.peers.size).to.equal(1)
expectSet(psA.subscriptions, ['Za'])
expectSet(psB.topics.get('Za'), [psA.peerId.toB58String()])
expectSet(psB.subscriptions, ['Zb'])
expectSet(psA.topics.get('Zb'), [psB.peerId.toB58String()])
})
})
})
}

10
src/pubsub/tests/utils.js Normal file
View File

@ -0,0 +1,10 @@
// @ts-nocheck interface tests
'use strict'
const { expect } = require('chai')
exports.first = (map) => map.values().next().value
exports.expectSet = (set, subs) => {
expect(Array.from(set.values())).to.eql(subs)
}

147
src/pubsub/utils.js Normal file
View File

@ -0,0 +1,147 @@
'use strict'
// @ts-ignore libp2p crypto has no types
const randomBytes = require('libp2p-crypto/src/random-bytes')
const uint8ArrayToString = require('uint8arrays/to-string')
const uint8ArrayFromString = require('uint8arrays/from-string')
const PeerId = require('peer-id')
const multihash = require('multihashes')
/**
* @typedef {import('./message/rpc').RPC.IMessage} IMessage
* @typedef {import('./message/rpc').RPC.Message} Message
* @typedef {import('.').InMessage} NormalizedIMessage
*/
/**
* Generatea random sequence number.
*
* @returns {Uint8Array}
* @private
*/
const randomSeqno = () => {
return randomBytes(8)
}
/**
* Generate a message id, based on the `from` and `seqno`.
*
* @param {string} from
* @param {Uint8Array} seqno
* @returns {Uint8Array}
* @private
*/
const msgId = (from, seqno) => {
const fromBytes = PeerId.createFromB58String(from).id
const msgId = new Uint8Array(fromBytes.length + seqno.length)
msgId.set(fromBytes, 0)
msgId.set(seqno, fromBytes.length)
return msgId
}
/**
* Generate a message id, based on message `data`.
*
* @param {Uint8Array} data
* @returns {Uint8Array}
* @private
*/
const noSignMsgId = (data) => multihash.encode(data, 'sha2-256')
/**
* Check if any member of the first set is also a member
* of the second set.
*
* @param {Set<number>|Array<number>} a
* @param {Set<number>|Array<number>} b
* @returns {boolean}
* @private
*/
const anyMatch = (a, b) => {
let bHas
if (Array.isArray(b)) {
/**
* @param {number} val
*/
bHas = (val) => b.indexOf(val) > -1
} else {
/**
* @param {number} val
*/
bHas = (val) => b.has(val)
}
for (const val of a) {
if (bHas(val)) {
return true
}
}
return false
}
/**
* Make everything an array.
*
* @template T
* @param {T|T[]} maybeArray
* @returns {T[]}
* @private
*/
const ensureArray = (maybeArray) => {
if (!Array.isArray(maybeArray)) {
return [maybeArray]
}
return maybeArray
}
/**
* Ensures `message.from` is base58 encoded
*
* @template {{from?:any}} T
* @param {T & IMessage} message
* @param {string} [peerId]
* @returns {NormalizedIMessage}
*/
const normalizeInRpcMessage = (message, peerId) => {
/** @type {NormalizedIMessage} */
// @ts-ignore receivedFrom not yet defined
const m = Object.assign({}, message)
if (message.from instanceof Uint8Array) {
m.from = uint8ArrayToString(message.from, 'base58btc')
}
if (peerId) {
m.receivedFrom = peerId
}
return m
}
/**
* @template {{from?:any, data?:any}} T
*
* @param {T & NormalizedIMessage} message
* @returns {Message}
*/
const normalizeOutRpcMessage = (message) => {
/** @type {Message} */
// @ts-ignore from not yet defined
const m = Object.assign({}, message)
if (typeof message.from === 'string') {
m.from = uint8ArrayFromString(message.from, 'base58btc')
}
if (typeof message.data === 'string') {
m.data = uint8ArrayFromString(message.data)
}
return m
}
module.exports = {
randomSeqno,
msgId,
noSignMsgId,
anyMatch,
ensureArray,
normalizeInRpcMessage,
normalizeOutRpcMessage
}

View File

@ -7,7 +7,7 @@ The record represents the data that will be stored inside the **envelope** when
Taking into account that a record might be used in different contexts, an **envelope** signature made for a specific purpose **must not** be considered valid for a different purpose. Accordingly, each record has a short and descriptive string representing the record use case, known as **domain**. The data to be signed will be prepended with the domain string, in order to create a domain signature.
A record can also contain a Buffer codec (ideally registered as a [multicodec](https://github.com/multiformats/multicodec)). This codec will prefix the record data in the **envelope** , so that it can be deserialized deterministically.
A record can also contain a Uint8Array codec (ideally registered as a [multicodec](https://github.com/multiformats/multicodec)). This codec will prefix the record data in the **envelope** , so that it can be deserialized deterministically.
## Usage
@ -30,20 +30,36 @@ describe('your record', () => {
```js
const multicodec = require('multicodec')
const Record = require('libp2p-interfaces/src/record')
const fromString = require('uint8arrays/from-string')
// const Protobuf = require('./record.proto')
const ENVELOPE_DOMAIN_PEER_RECORD = 'libp2p-peer-record'
const ENVELOPE_PAYLOAD_TYPE_PEER_RECORD = Buffer.from('0301', 'hex')
const ENVELOPE_PAYLOAD_TYPE_PEER_RECORD = fromString('0301', 'hex')
class PeerRecord extends Record {
/**
* @implements {import('libp2p-interfaces/src/record/types').Record}
*/
class PeerRecord {
constructor (peerId, multiaddrs, seqNumber) {
super (ENVELOPE_DOMAIN_PEER_RECORD, ENVELOPE_PAYLOAD_TYPE_PEER_RECORD)
this.domain = ENVELOPE_DOMAIN_PEER_RECORD
this.codec = ENVELOPE_PAYLOAD_TYPE_PEER_RECORD
}
/**
* Marshal a record to be used in an envelope.
*
* @returns {Uint8Array}
*/
marshal () {
// Implement and return using Protobuf
}
/**
* Returns true if `this` record equals the `other`.
*
* @param {PeerRecord} other
* @returns {other is Record}
*/
equals (other) {
// Verify
}
@ -72,4 +88,4 @@ Verifies if the other Record is identical to this one.
- other is a `Record` to compare with the current instance.
**Returns**
- `boolean`
- `other is Record`

View File

@ -1,35 +0,0 @@
'use strict'
const errcode = require('err-code')
/**
* Record is the base implementation of a record that can be used as the payload of a libp2p envelope.
*/
class Record {
/**
* @constructor
* @param {String} domain signature domain
* @param {Buffer} codec identifier of the type of record
*/
constructor (domain, codec) {
this.domain = domain
this.codec = codec
}
/**
* Marshal a record to be used in an envelope.
*/
marshal () {
throw errcode(new Error('marshal must be implemented by the subclass'), 'ERR_NOT_IMPLEMENTED')
}
/**
* Verifies if the other provided Record is identical to this one.
* @param {Record} other
*/
equals (other) {
throw errcode(new Error('equals must be implemented by the subclass'), 'ERR_NOT_IMPLEMENTED')
}
}
module.exports = Record

View File

@ -1,3 +1,4 @@
// @ts-nocheck interface tests
/* eslint-env mocha */
'use strict'
@ -24,7 +25,7 @@ module.exports = (test) => {
it('is able to marshal', () => {
const rawData = record.marshal()
expect(Buffer.isBuffer(rawData)).to.eql(true)
expect(rawData).to.be.an.instanceof(Uint8Array)
})
it('is able to compare two records', () => {

21
src/record/types.d.ts vendored Normal file
View File

@ -0,0 +1,21 @@
/**
* Record is the base implementation of a record that can be used as the payload of a libp2p envelope.
*/
export interface Record {
/**
* signature domain.
*/
domain: string;
/**
* identifier of the type of record
*/
codec: Uint8Array;
/**
* Marshal a record to be used in an envelope.
*/
marshal(): Uint8Array;
/**
* Verifies if the other provided Record is identical to this one.
*/
equals(other: unknown): boolean
}

View File

@ -1,3 +1,4 @@
// @ts-nocheck interface tests
/* eslint-env mocha */
'use strict'
@ -5,7 +6,7 @@ const chai = require('chai')
chai.use(require('chai-checkmark'))
const { expect } = chai
const pair = require('it-pair/duplex')
const pipe = require('it-pipe')
const { pipe } = require('it-pipe')
const { collect, map, consume } = require('streaming-iterables')
function close (stream) {
@ -20,8 +21,9 @@ async function closeAndWait (stream) {
/**
* A tick is considered valid if it happened between now
* and `ms` milliseconds ago
* @param {number} date Time in ticks
* @param {number} ms max milliseconds that should have expired
*
* @param {number} date - Time in ticks
* @param {number} ms - max milliseconds that should have expired
* @returns {boolean}
*/
function isValidTick (date, ms = 5000) {

View File

@ -1,24 +1,25 @@
// @ts-nocheck interface tests
/* eslint-env mocha */
/* eslint max-nested-callbacks: ["error", 8] */
'use strict'
const { Buffer } = require('buffer')
const pair = require('it-pair/duplex')
const pipe = require('it-pipe')
const { pipe } = require('it-pipe')
const { consume } = require('streaming-iterables')
const Tcp = require('libp2p-tcp')
const multiaddr = require('multiaddr')
const abortable = require('abortable-iterator')
const AbortController = require('abort-controller')
const { Multiaddr } = require('multiaddr')
const { source: abortable } = require('abortable-iterator')
const AbortController = require('abort-controller').default
const uint8arrayFromString = require('uint8arrays/from-string')
const mh = multiaddr('/ip4/127.0.0.1/tcp/0')
const mh = new Multiaddr('/ip4/127.0.0.1/tcp/0')
function pause (ms) {
return new Promise(resolve => setTimeout(resolve, ms))
}
function randomBuffer () {
return Buffer.from(Math.random().toString())
return uint8arrayFromString(Math.random().toString())
}
const infiniteRandom = {

View File

@ -1,3 +1,4 @@
// @ts-nocheck interface tests
/* eslint-env mocha */
'use strict'

View File

@ -1,3 +1,4 @@
// @ts-nocheck interface tests
/* eslint-env mocha */
'use strict'

View File

@ -1,8 +1,10 @@
// @ts-nocheck interface tests
'use strict'
const { expect } = require('chai')
const pair = require('it-pair/duplex')
const pipe = require('it-pipe')
const { pipe } = require('it-pipe')
const pLimit = require('p-limit')
const { collect, tap, consume } = require('streaming-iterables')
@ -61,8 +63,11 @@ module.exports = async (Muxer, nStreams, nMsg, limit) => {
}
function marker (n) {
/** @type {Function} */
let check
let i = 0
/** @type {Promise<void>} */
const done = new Promise((resolve, reject) => {
check = err => {
i++
@ -78,5 +83,7 @@ function marker (n) {
}
}
})
// @ts-ignore - TS can't see that assignement occured
return { check, done }
}

View File

@ -1,3 +1,4 @@
// @ts-nocheck interface tests
/* eslint-env mocha */
'use strict'

51
src/stream-muxer/types.d.ts vendored Normal file
View File

@ -0,0 +1,51 @@
import BufferList from 'bl'
export interface MuxerFactory {
new (options: MuxerOptions): Muxer;
multicodec: string;
}
/**
* A libp2p stream muxer
*/
export interface Muxer {
readonly streams: Array<MuxedStream>;
/**
* Initiate a new stream with the given name. If no name is
* provided, the id of th stream will be used.
*/
newStream (name?: string): MuxedStream;
/**
* A function called when receiving a new stream from the remote.
*/
onStream (stream: MuxedStream): void;
/**
* A function called when a stream ends.
*/
onStreamEnd (stream: MuxedStream): void;
}
export type MuxerOptions = {
onStream: (stream: MuxedStream) => void;
onStreamEnd: (stream: MuxedStream) => void;
maxMsgSize?: number;
}
export type MuxedTimeline = {
open: number;
close?: number;
}
export interface MuxedStream extends AsyncIterable<Uint8Array | BufferList> {
close: () => void;
abort: () => void;
reset: () => void;
sink: Sink;
source: AsyncIterable<Uint8Array | BufferList>;
timeline: MuxedTimeline;
id: string;
}
export type Sink = (source: Uint8Array) => Promise<void>;

View File

@ -1,18 +1,28 @@
'use strict'
const withIs = require('class-is')
const noop = () => {}
const topologySymbol = Symbol.for('@libp2p/js-interfaces/topology')
/**
* @typedef {import('peer-id')} PeerId
*/
/**
* @typedef {Object} Options
* @property {number} [min=0] - minimum needed connections.
* @property {number} [max=Infinity] - maximum needed connections.
* @property {Handlers} [handlers]
*
* @typedef {Object} Handlers
* @property {(peerId: PeerId, conn: Connection) => void} [onConnect] - protocol "onConnect" handler
* @property {(peerId: PeerId, error?:Error) => void} [onDisconnect] - protocol "onDisconnect" handler
*
* @typedef {import('../connection/connection')} Connection
*/
class 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
* @param {Options} options
*/
constructor ({
min = 0,
@ -28,17 +38,40 @@ class Topology {
/**
* Set of peers that support the protocol.
*
* @type {Set<string>}
*/
this.peers = new Set()
}
set registrar (registrar) {
get [Symbol.toStringTag] () {
return 'Topology'
}
get [topologySymbol] () {
return true
}
/**
* Checks if the given value is a Topology instance.
*
* @param {any} other
* @returns {other is Topology}
*/
static isTopology (other) {
return Boolean(other && other[topologySymbol])
}
/**
* @param {any} registrar
*/
set registrar (registrar) { // eslint-disable-line
this._registrar = registrar
}
/**
* Notify about peer disconnected event.
*
* @param {PeerId} peerId
* @returns {void}
*/
@ -47,4 +80,4 @@ class Topology {
}
}
module.exports = withIs(Topology, { className: 'Topology', symbolName: '@libp2p/js-interfaces/topology' })
module.exports = Topology

View File

@ -1,19 +1,11 @@
'use strict'
const withIs = require('class-is')
const Topology = require('./index')
const multicodecTopologySymbol = Symbol.for('@libp2p/js-interfaces/topology/multicodec-topology')
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
* @param {TopologyOptions & MulticodecOptions} props
*/
constructor ({
min,
@ -46,7 +38,28 @@ class MulticodecTopology extends Topology {
this._onPeerConnect = this._onPeerConnect.bind(this)
}
set registrar (registrar) {
get [Symbol.toStringTag] () {
return 'Topology'
}
get [multicodecTopologySymbol] () {
return true
}
/**
* Checks if the given value is a `MulticodecTopology` instance.
*
* @param {any} other
* @returns {other is MulticodecTopology}
*/
static isMulticodecTopology (other) {
return Boolean(other && other[multicodecTopologySymbol])
}
/**
* @param {any} registrar
*/
set registrar (registrar) { // eslint-disable-line
this._registrar = registrar
this._registrar.peerStore.on('change:protocols', this._onProtocolChange)
this._registrar.connectionManager.on('peer:connect', this._onPeerConnect)
@ -57,6 +70,7 @@ class MulticodecTopology extends Topology {
/**
* Update topology.
*
* @param {Array<{id: PeerId, multiaddrs: Array<Multiaddr>, protocols: Array<string>}>} peerDataIterable
* @returns {void}
*/
@ -77,6 +91,7 @@ class MulticodecTopology extends Topology {
/**
* Check if a new peer support the multicodecs for this topology.
*
* @param {Object} props
* @param {PeerId} props.peerId
* @param {Array<string>} props.protocols
@ -102,10 +117,12 @@ class MulticodecTopology extends Topology {
/**
* Verify if a new connected peer has a topology multicodec and call _onConnect.
*
* @param {Connection} connection
* @returns {void}
*/
_onPeerConnect (connection) {
// @ts-ignore - remotePeer does not existist on Connection
const peerId = connection.remotePeer
const protocols = this._registrar.peerStore.protoBook.get(peerId)
@ -120,4 +137,14 @@ class MulticodecTopology extends Topology {
}
}
module.exports = withIs(MulticodecTopology, { className: 'MulticodecTopology', symbolName: '@libp2p/js-interfaces/topology/multicodec-topology' })
/**
* @typedef {import('peer-id')} PeerId
* @typedef {import('multiaddr')} Multiaddr
* @typedef {import('../connection/connection')} Connection
* @typedef {import('.').Options} TopologyOptions
* @typedef {Object} MulticodecOptions
* @property {string[]} multicodecs - protocol multicodecs
* @property {Required<Handlers>} handlers
* @typedef {import('.').Handlers} Handlers
*/
module.exports = MulticodecTopology

View File

@ -1,3 +1,4 @@
// @ts-nocheck interface tests
/* eslint-env mocha */
'use strict'

View File

@ -1,3 +1,4 @@
// @ts-nocheck interface tests
/* eslint-env mocha */
'use strict'

View File

@ -1,3 +1,4 @@
// @ts-nocheck interface tests
/* eslint-env mocha */
'use strict'
@ -9,8 +10,8 @@ chai.use(dirtyChai)
const { isValidTick } = require('./utils')
const goodbye = require('it-goodbye')
const { collect } = require('streaming-iterables')
const pipe = require('it-pipe')
const AbortController = require('abort-controller')
const { pipe } = require('it-pipe')
const AbortController = require('abort-controller').default
const AbortError = require('../errors').AbortError
const sinon = require('sinon')

View File

@ -1,3 +1,4 @@
// @ts-nocheck interface tests
/* eslint-env mocha */
'use strict'

View File

@ -1,3 +1,4 @@
// @ts-nocheck interface tests
/* eslint-env mocha */
'use strict'

View File

@ -1,8 +1,8 @@
// @ts-nocheck interface tests
/* eslint max-nested-callbacks: ["error", 8] */
/* eslint-env mocha */
'use strict'
const { Buffer } = require('buffer')
const chai = require('chai')
const dirtyChai = require('dirty-chai')
const expect = chai.expect
@ -10,7 +10,8 @@ chai.use(dirtyChai)
const sinon = require('sinon')
const pWaitFor = require('p-wait-for')
const pipe = require('it-pipe')
const { pipe } = require('it-pipe')
const uint8arrayFromString = require('uint8arrays/from-string')
const { isValidTick } = require('./utils')
module.exports = (common) => {
@ -76,7 +77,7 @@ module.exports = (common) => {
// Wait for the data send and close to finish
await Promise.all([
pipe(
[Buffer.from('Some data that is never handled')],
[uint8arrayFromString('Some data that is never handled')],
socket1
),
// Closer the listener (will take a couple of seconds to time out)

View File

@ -4,8 +4,9 @@ module.exports = {
/**
* A tick is considered valid if it happened between now
* and `ms` milliseconds ago
* @param {number} date Time in ticks
* @param {number} ms max milliseconds that should have expired
*
* @param {number} date - Time in ticks
* @param {number} ms - max milliseconds that should have expired
* @returns {boolean}
*/
isValidTick: function isValidTick (date, ms = 5000) {

72
src/transport/types.d.ts vendored Normal file
View File

@ -0,0 +1,72 @@
import BufferList from 'bl'
import events from 'events'
import Multiaddr from 'multiaddr'
import Connection from '../connection/connection'
import { Sink } from '../stream-muxer/types'
export interface TransportFactory<DialOptions extends { signal?: AbortSignal }, ListenerOptions> {
new(upgrader: Upgrader): Transport<DialOptions, ListenerOptions>;
}
/**
* A libp2p transport is understood as something that offers a dial and listen interface to establish connections.
*/
export interface Transport <DialOptions extends { signal?: AbortSignal }, ListenerOptions> {
/**
* Dial a given multiaddr.
*/
dial(ma: Multiaddr, options?: DialOptions): Promise<Connection>;
/**
* Create transport listeners.
*/
createListener(options: ListenerOptions, handler?: (connection: Connection) => void): Listener;
/**
* Takes a list of `Multiaddr`s and returns only valid addresses for the transport
*/
filter(multiaddrs: Multiaddr[]): Multiaddr[];
}
export interface Listener extends events.EventEmitter {
/**
* Start a listener
*/
listen(multiaddr: Multiaddr): Promise<void>;
/**
* Get listen addresses
*/
getAddrs(): Multiaddr[];
/**
* Close listener
*
* @returns {Promise<void>}
*/
close(): Promise<void>;
}
export interface Upgrader {
/**
* Upgrades an outbound connection on `transport.dial`.
*/
upgradeOutbound(maConn: MultiaddrConnection): Promise<Connection>;
/**
* Upgrades an inbound connection on transport listener.
*/
upgradeInbound(maConn: MultiaddrConnection): Promise<Connection>;
}
export type MultiaddrConnectionTimeline = {
open: number;
upgraded?: number;
close?: number;
}
export type MultiaddrConnection = {
sink: Sink;
source: AsyncIterable<Uint8Array | BufferList>;
close: (err?: Error) => Promise<void>;
conn: unknown;
remoteAddr: Multiaddr;
localAddr?: Multiaddr;
timeline: MultiaddrConnectionTimeline;
}

18
src/types.d.ts vendored Normal file
View File

@ -0,0 +1,18 @@
export interface EventEmitterFactory {
new(): EventEmitter;
}
export interface EventEmitter {
addListener(event: string | symbol, listener: (...args: any[]) => void): EventEmitter;
on(event: string | symbol, listener: (...args: any[]) => void): EventEmitter;
once(event: string | symbol, listener: (...args: any[]) => void): EventEmitter;
removeListener(event: string | symbol, listener: (...args: any[]) => void): EventEmitter;
off(event: string | symbol, listener: (...args: any[]) => void): EventEmitter;
removeAllListeners(event?: string | symbol): EventEmitter;
setMaxListeners(n: number): EventEmitter;
getMaxListeners(): number;
listeners(event: string | symbol): Function[]; // eslint-disable-line @typescript-eslint/ban-types
rawListeners(event: string | symbol): Function[]; // eslint-disable-line @typescript-eslint/ban-types
emit(event: string | symbol, ...args: any[]): boolean;
listenerCount(event: string | symbol): number;
}

View File

@ -5,7 +5,7 @@ const tests = require('../../src/connection/tests')
const { Connection } = require('../../src/connection')
const peers = require('../../src/utils/peers')
const PeerId = require('peer-id')
const multiaddr = require('multiaddr')
const { Multiaddr } = require('multiaddr')
const pair = require('it-pair')
describe('compliance tests', () => {
@ -13,11 +13,12 @@ describe('compliance tests', () => {
/**
* Test setup. `properties` allows the compliance test to override
* certain values for testing.
*
* @param {*} properties
*/
async setup (properties) {
const localAddr = multiaddr('/ip4/127.0.0.1/tcp/8080')
const remoteAddr = multiaddr('/ip4/127.0.0.1/tcp/8081')
const localAddr = new Multiaddr('/ip4/127.0.0.1/tcp/8080')
const remoteAddr = new Multiaddr('/ip4/127.0.0.1/tcp/8081')
const [localPeer, remotePeer] = await Promise.all([
PeerId.createFromJSON(peers[0]),
PeerId.createFromJSON(peers[1])

View File

@ -2,7 +2,7 @@
const { EventEmitter } = require('events')
const multiaddr = require('multiaddr')
const { Multiaddr } = require('multiaddr')
const PeerId = require('peer-id')
/**
@ -41,7 +41,7 @@ class MockDiscovery extends EventEmitter {
this._timer = setTimeout(() => {
this.emit('peer', {
id: peerId,
multiaddrs: [multiaddr('/ip4/127.0.0.1/tcp/8000')]
multiaddrs: [new Multiaddr('/ip4/127.0.0.1/tcp/8000')]
})
}, this.options.discoveryDelay || 1000)
}

View File

@ -0,0 +1,78 @@
/* eslint-env mocha */
'use strict'
const { expect } = require('aegir/utils/chai')
const {
createPeerId,
mockRegistrar,
PubsubImplementation
} = require('./utils')
const uint8ArrayFromString = require('uint8arrays/from-string')
const protocol = '/pubsub/1.0.0'
const topic = 'foo'
const data = uint8ArrayFromString('bar')
const shouldNotHappen = (_) => expect.fail()
describe('emitSelf', () => {
let pubsub
describe('enabled', () => {
before(async () => {
const peerId = await createPeerId()
pubsub = new PubsubImplementation(protocol, {
peerId,
registrar: mockRegistrar
}, { emitSelf: true })
})
before(() => {
pubsub.start()
pubsub.subscribe(topic)
})
after(() => {
pubsub.stop()
})
it('should emit to self on publish', () => {
const promise = new Promise((resolve) => pubsub.once(topic, resolve))
pubsub.publish(topic, data)
return promise
})
})
describe('disabled', () => {
before(async () => {
const peerId = await createPeerId()
pubsub = new PubsubImplementation(protocol, {
peerId,
registrar: mockRegistrar
}, { emitSelf: false })
})
before(() => {
pubsub.start()
pubsub.subscribe(topic)
})
after(() => {
pubsub.stop()
})
it('should not emit to self on publish', () => {
pubsub.once(topic, (m) => shouldNotHappen)
pubsub.publish(topic, data)
// Wait 1 second to guarantee that self is not noticed
return new Promise((resolve) => setTimeout(() => resolve(), 1000))
})
})
})

View File

@ -0,0 +1,54 @@
/* eslint-env mocha */
'use strict'
const { expect } = require('aegir/utils/chai')
const PubsubBaseImpl = require('../../src/pubsub')
const {
createPeerId,
mockRegistrar
} = require('./utils')
describe('pubsub instance', () => {
let peerId
before(async () => {
peerId = await createPeerId()
})
it('should throw if no debugName is provided', () => {
expect(() => {
new PubsubBaseImpl() // eslint-disable-line no-new
}).to.throw()
})
it('should throw if no multicodec is provided', () => {
expect(() => {
new PubsubBaseImpl({ // eslint-disable-line no-new
debugName: 'pubsub'
})
}).to.throw()
})
it('should throw if no libp2p is provided', () => {
expect(() => {
new PubsubBaseImpl({ // eslint-disable-line no-new
debugName: 'pubsub',
multicodecs: '/pubsub/1.0.0'
})
}).to.throw()
})
it('should accept valid parameters', () => {
expect(() => {
new PubsubBaseImpl({ // eslint-disable-line no-new
debugName: 'pubsub',
multicodecs: '/pubsub/1.0.0',
libp2p: {
peerId: peerId,
registrar: mockRegistrar
}
})
}).not.to.throw()
})
})

View File

@ -0,0 +1,227 @@
/* eslint-env mocha */
'use strict'
const { expect } = require('aegir/utils/chai')
const sinon = require('sinon')
const PubsubBaseImpl = require('../../src/pubsub')
const {
createPeerId,
createMockRegistrar,
PubsubImplementation,
ConnectionPair
} = require('./utils')
describe('pubsub base lifecycle', () => {
describe('should start and stop properly', () => {
let pubsub
let sinonMockRegistrar
beforeEach(async () => {
const peerId = await createPeerId()
sinonMockRegistrar = {
handle: sinon.stub(),
register: sinon.stub(),
unregister: sinon.stub()
}
pubsub = new PubsubBaseImpl({
debugName: 'pubsub',
multicodecs: '/pubsub/1.0.0',
libp2p: {
peerId: peerId,
registrar: sinonMockRegistrar
}
})
expect(pubsub.peers.size).to.be.eql(0)
})
afterEach(() => {
sinon.restore()
})
it('should be able to start and stop', async () => {
await pubsub.start()
expect(sinonMockRegistrar.handle.calledOnce).to.be.true()
expect(sinonMockRegistrar.register.calledOnce).to.be.true()
await pubsub.stop()
expect(sinonMockRegistrar.unregister.calledOnce).to.be.true()
})
it('starting should not throw if already started', async () => {
await pubsub.start()
await pubsub.start()
expect(sinonMockRegistrar.handle.calledOnce).to.be.true()
expect(sinonMockRegistrar.register.calledOnce).to.be.true()
await pubsub.stop()
expect(sinonMockRegistrar.unregister.calledOnce).to.be.true()
})
it('stopping should not throw if not started', async () => {
await pubsub.stop()
expect(sinonMockRegistrar.register.calledOnce).to.be.false()
expect(sinonMockRegistrar.unregister.calledOnce).to.be.false()
})
})
describe('should be able to register two nodes', () => {
const protocol = '/pubsub/1.0.0'
let pubsubA, pubsubB
let peerIdA, peerIdB
const registrarRecordA = {}
const registrarRecordB = {}
// mount pubsub
beforeEach(async () => {
peerIdA = await createPeerId()
peerIdB = await createPeerId()
pubsubA = new PubsubImplementation(protocol, {
peerId: peerIdA,
registrar: createMockRegistrar(registrarRecordA)
})
pubsubB = new PubsubImplementation(protocol, {
peerId: peerIdB,
registrar: createMockRegistrar(registrarRecordB)
})
})
// start pubsub
beforeEach(() => {
pubsubA.start()
pubsubB.start()
expect(Object.keys(registrarRecordA)).to.have.lengthOf(1)
expect(Object.keys(registrarRecordB)).to.have.lengthOf(1)
})
afterEach(() => {
sinon.restore()
return Promise.all([
pubsubA.stop(),
pubsubB.stop()
])
})
it('should handle onConnect as expected', async () => {
const onConnectA = registrarRecordA[protocol].onConnect
const handlerB = registrarRecordB[protocol].handler
// Notice peers of connection
const [c0, c1] = ConnectionPair()
await onConnectA(peerIdB, c0)
await handlerB({
protocol,
stream: c1.stream,
connection: {
remotePeer: peerIdA
}
})
expect(pubsubA.peers.size).to.be.eql(1)
expect(pubsubB.peers.size).to.be.eql(1)
})
it('should use the latest connection if onConnect is called more than once', async () => {
const onConnectA = registrarRecordA[protocol].onConnect
const handlerB = registrarRecordB[protocol].handler
// Notice peers of connection
const [c0, c1] = ConnectionPair()
const [c2] = ConnectionPair()
sinon.spy(c0, 'newStream')
await onConnectA(peerIdB, c0)
await handlerB({
protocol,
stream: c1.stream,
connection: {
remotePeer: peerIdA
}
})
expect(c0.newStream).to.have.property('callCount', 1)
sinon.spy(pubsubA, '_removePeer')
sinon.spy(c2, 'newStream')
await onConnectA(peerIdB, c2)
expect(c2.newStream).to.have.property('callCount', 1)
expect(pubsubA._removePeer).to.have.property('callCount', 0)
// Verify the first stream was closed
const { stream: firstStream } = await c0.newStream.returnValues[0]
try {
await firstStream.sink(['test'])
} catch (err) {
expect(err).to.exist()
return
}
expect.fail('original stream should have ended')
})
it('should handle newStream errors in onConnect', async () => {
const onConnectA = registrarRecordA[protocol].onConnect
const handlerB = registrarRecordB[protocol].handler
// Notice peers of connection
const [c0, c1] = ConnectionPair()
const error = new Error('new stream error')
sinon.stub(c0, 'newStream').throws(error)
await onConnectA(peerIdB, c0)
await handlerB({
protocol,
stream: c1.stream,
connection: {
remotePeer: peerIdA
}
})
expect(c0.newStream).to.have.property('callCount', 1)
})
it('should handle onDisconnect as expected', async () => {
const onConnectA = registrarRecordA[protocol].onConnect
const onDisconnectA = registrarRecordA[protocol].onDisconnect
const handlerB = registrarRecordB[protocol].handler
const onDisconnectB = registrarRecordB[protocol].onDisconnect
// Notice peers of connection
const [c0, c1] = ConnectionPair()
await onConnectA(peerIdB, c0)
await handlerB({
protocol,
stream: c1.stream,
connection: {
remotePeer: peerIdA
}
})
// Notice peers of disconnect
onDisconnectA(peerIdB)
onDisconnectB(peerIdA)
expect(pubsubA.peers.size).to.be.eql(0)
expect(pubsubB.peers.size).to.be.eql(0)
})
it('should handle onDisconnect for unknown peers', () => {
const onDisconnectA = registrarRecordA[protocol].onDisconnect
expect(pubsubA.peers.size).to.be.eql(0)
// Notice peers of disconnect
onDisconnectA(peerIdB)
expect(pubsubA.peers.size).to.be.eql(0)
})
})
})

View File

@ -0,0 +1,90 @@
/* eslint-env mocha */
'use strict'
const { expect } = require('aegir/utils/chai')
const sinon = require('sinon')
const PubsubBaseImpl = require('../../src/pubsub')
const { SignaturePolicy } = require('../../src/pubsub/signature-policy')
const {
createPeerId,
mockRegistrar
} = require('./utils')
describe('pubsub base messages', () => {
let peerId
let pubsub
before(async () => {
peerId = await createPeerId()
pubsub = new PubsubBaseImpl({
debugName: 'pubsub',
multicodecs: '/pubsub/1.0.0',
libp2p: {
peerId: peerId,
registrar: mockRegistrar
}
})
})
afterEach(() => {
sinon.restore()
})
it('_buildMessage normalizes and signs messages', async () => {
const message = {
receivedFrom: peerId.id,
data: 'hello',
topicIDs: ['test-topic']
}
const signedMessage = await pubsub._buildMessage(message)
expect(pubsub.validate(signedMessage)).to.not.be.rejected()
})
it('validate with StrictNoSign will reject a message with from, signature, key, seqno present', async () => {
const message = {
receivedFrom: peerId.id,
data: 'hello',
topicIDs: ['test-topic']
}
sinon.stub(pubsub, 'globalSignaturePolicy').value(SignaturePolicy.StrictSign)
const signedMessage = await pubsub._buildMessage(message)
sinon.stub(pubsub, 'globalSignaturePolicy').value(SignaturePolicy.StrictNoSign)
await expect(pubsub.validate(signedMessage)).to.be.rejected()
delete signedMessage.from
await expect(pubsub.validate(signedMessage)).to.be.rejected()
delete signedMessage.signature
await expect(pubsub.validate(signedMessage)).to.be.rejected()
delete signedMessage.key
await expect(pubsub.validate(signedMessage)).to.be.rejected()
delete signedMessage.seqno
await expect(pubsub.validate(signedMessage)).to.not.be.rejected()
})
it('validate with StrictNoSign will validate a message without a signature, key, and seqno', async () => {
const message = {
receivedFrom: peerId.id,
data: 'hello',
topicIDs: ['test-topic']
}
sinon.stub(pubsub, 'globalSignaturePolicy').value(SignaturePolicy.StrictNoSign)
const signedMessage = await pubsub._buildMessage(message)
expect(pubsub.validate(signedMessage)).to.not.be.rejected()
})
it('validate with StrictSign requires a signature', async () => {
const message = {
receivedFrom: peerId.id,
data: 'hello',
topicIDs: ['test-topic']
}
await expect(pubsub.validate(message)).to.be.rejectedWith(Error, 'Signing required and no signature was present')
})
})

358
test/pubsub/pubsub.spec.js Normal file
View File

@ -0,0 +1,358 @@
/* eslint-env mocha */
/* eslint max-nested-callbacks: ["error", 6] */
'use strict'
const { expect } = require('aegir/utils/chai')
const sinon = require('sinon')
const pWaitFor = require('p-wait-for')
const uint8ArrayFromString = require('uint8arrays/from-string')
const PeerStreams = require('../../src/pubsub/peer-streams')
const {
createPeerId,
createMockRegistrar,
ConnectionPair,
mockRegistrar,
PubsubImplementation
} = require('./utils')
const protocol = '/pubsub/1.0.0'
const topic = 'test-topic'
const message = uint8ArrayFromString('hello')
describe('pubsub base implementation', () => {
describe('publish', () => {
let pubsub
beforeEach(async () => {
const peerId = await createPeerId()
pubsub = new PubsubImplementation(protocol, {
peerId: peerId,
registrar: mockRegistrar
})
})
afterEach(() => pubsub.stop())
it('calls _publish for router to forward messages', async () => {
sinon.spy(pubsub, '_publish')
pubsub.start()
await pubsub.publish(topic, message)
expect(pubsub._publish.callCount).to.eql(1)
})
it('should sign messages on publish', async () => {
sinon.spy(pubsub, '_publish')
pubsub.start()
await pubsub.publish(topic, message)
// Get the first message sent to _publish, and validate it
const signedMessage = pubsub._publish.getCall(0).lastArg
try {
await pubsub.validate(signedMessage)
} catch (e) {
expect.fail('validation should not throw')
}
})
})
describe('subscribe', () => {
describe('basics', () => {
let pubsub
beforeEach(async () => {
const peerId = await createPeerId()
pubsub = new PubsubImplementation(protocol, {
peerId: peerId,
registrar: mockRegistrar
})
pubsub.start()
})
afterEach(() => pubsub.stop())
it('should add subscription', () => {
pubsub.subscribe(topic)
expect(pubsub.subscriptions.size).to.eql(1)
expect(pubsub.subscriptions.has(topic)).to.be.true()
})
})
describe('two nodes', () => {
let pubsubA, pubsubB
let peerIdA, peerIdB
const registrarRecordA = {}
const registrarRecordB = {}
beforeEach(async () => {
peerIdA = await createPeerId()
peerIdB = await createPeerId()
pubsubA = new PubsubImplementation(protocol, {
peerId: peerIdA,
registrar: createMockRegistrar(registrarRecordA)
})
pubsubB = new PubsubImplementation(protocol, {
peerId: peerIdB,
registrar: createMockRegistrar(registrarRecordB)
})
})
// start pubsub and connect nodes
beforeEach(async () => {
pubsubA.start()
pubsubB.start()
const onConnectA = registrarRecordA[protocol].onConnect
const handlerB = registrarRecordB[protocol].handler
// Notice peers of connection
const [c0, c1] = ConnectionPair()
await onConnectA(peerIdB, c0)
await handlerB({
protocol,
stream: c1.stream,
connection: {
remotePeer: peerIdA
}
})
})
afterEach(() => {
pubsubA.stop()
pubsubB.stop()
})
it('should send subscribe message to connected peers', async () => {
sinon.spy(pubsubA, '_sendSubscriptions')
sinon.spy(pubsubB, '_processRpcSubOpt')
pubsubA.subscribe(topic)
// Should send subscriptions to a peer
expect(pubsubA._sendSubscriptions.callCount).to.eql(1)
// Other peer should receive subscription message
await pWaitFor(() => {
const subscribers = pubsubB.getSubscribers(topic)
return subscribers.length === 1
})
expect(pubsubB._processRpcSubOpt.callCount).to.eql(1)
})
})
})
describe('unsubscribe', () => {
describe('basics', () => {
let pubsub
beforeEach(async () => {
const peerId = await createPeerId()
pubsub = new PubsubImplementation(protocol, {
peerId: peerId,
registrar: mockRegistrar
})
pubsub.start()
})
afterEach(() => pubsub.stop())
it('should remove all subscriptions for a topic', () => {
pubsub.subscribe(topic, (msg) => {})
pubsub.subscribe(topic, (msg) => {})
expect(pubsub.subscriptions.size).to.eql(1)
pubsub.unsubscribe(topic)
expect(pubsub.subscriptions.size).to.eql(0)
})
})
describe('two nodes', () => {
let pubsubA, pubsubB
let peerIdA, peerIdB
const registrarRecordA = {}
const registrarRecordB = {}
beforeEach(async () => {
peerIdA = await createPeerId()
peerIdB = await createPeerId()
pubsubA = new PubsubImplementation(protocol, {
peerId: peerIdA,
registrar: createMockRegistrar(registrarRecordA)
})
pubsubB = new PubsubImplementation(protocol, {
peerId: peerIdB,
registrar: createMockRegistrar(registrarRecordB)
})
})
// start pubsub and connect nodes
beforeEach(async () => {
pubsubA.start()
pubsubB.start()
const onConnectA = registrarRecordA[protocol].onConnect
const handlerB = registrarRecordB[protocol].handler
// Notice peers of connection
const [c0, c1] = ConnectionPair()
await onConnectA(peerIdB, c0)
await handlerB({
protocol,
stream: c1.stream,
connection: {
remotePeer: peerIdA
}
})
})
afterEach(() => {
pubsubA.stop()
pubsubB.stop()
})
it('should send unsubscribe message to connected peers', async () => {
sinon.spy(pubsubA, '_sendSubscriptions')
sinon.spy(pubsubB, '_processRpcSubOpt')
pubsubA.subscribe(topic)
// Should send subscriptions to a peer
expect(pubsubA._sendSubscriptions.callCount).to.eql(1)
// Other peer should receive subscription message
await pWaitFor(() => {
const subscribers = pubsubB.getSubscribers(topic)
return subscribers.length === 1
})
expect(pubsubB._processRpcSubOpt.callCount).to.eql(1)
// Unsubscribe
pubsubA.unsubscribe(topic)
// Should send subscriptions to a peer
expect(pubsubA._sendSubscriptions.callCount).to.eql(2)
// Other peer should receive subscription message
await pWaitFor(() => {
const subscribers = pubsubB.getSubscribers(topic)
return subscribers.length === 0
})
expect(pubsubB._processRpcSubOpt.callCount).to.eql(2)
})
it('should not send unsubscribe message to connected peers if not subscribed', () => {
sinon.spy(pubsubA, '_sendSubscriptions')
sinon.spy(pubsubB, '_processRpcSubOpt')
// Unsubscribe
pubsubA.unsubscribe(topic)
// Should send subscriptions to a peer
expect(pubsubA._sendSubscriptions.callCount).to.eql(0)
})
})
})
describe('getTopics', () => {
let peerId
let pubsub
beforeEach(async () => {
peerId = await createPeerId()
pubsub = new PubsubImplementation(protocol, {
peerId: peerId,
registrar: mockRegistrar
})
pubsub.start()
})
afterEach(() => pubsub.stop())
it('returns the subscribed topics', () => {
let subsTopics = pubsub.getTopics()
expect(subsTopics).to.have.lengthOf(0)
pubsub.subscribe(topic)
subsTopics = pubsub.getTopics()
expect(subsTopics).to.have.lengthOf(1)
expect(subsTopics[0]).to.eql(topic)
})
})
describe('getSubscribers', () => {
let peerId
let pubsub
beforeEach(async () => {
peerId = await createPeerId()
pubsub = new PubsubImplementation(protocol, {
peerId: peerId,
registrar: mockRegistrar
})
})
afterEach(() => pubsub.stop())
it('should fail if pubsub is not started', () => {
const topic = 'topic-test'
try {
pubsub.getSubscribers(topic)
} catch (err) {
expect(err).to.exist()
expect(err.code).to.eql('ERR_NOT_STARTED_YET')
return
}
throw new Error('should fail if pubsub is not started')
})
it('should fail if no topic is provided', () => {
// start pubsub
pubsub.start()
try {
pubsub.getSubscribers()
} catch (err) {
expect(err).to.exist()
expect(err.code).to.eql('ERR_NOT_VALID_TOPIC')
return
}
throw new Error('should fail if no topic is provided')
})
it('should get peer subscribed to one topic', () => {
const topic = 'topic-test'
// start pubsub
pubsub.start()
let peersSubscribed = pubsub.getSubscribers(topic)
expect(peersSubscribed).to.be.empty()
// Set mock peer subscribed
const peer = new PeerStreams({ id: peerId })
const id = peer.id.toB58String()
pubsub.topics.set(topic, new Set([id]))
pubsub.peers.set(id, peer)
peersSubscribed = pubsub.getSubscribers(topic)
expect(peersSubscribed).to.not.be.empty()
expect(peersSubscribed[0]).to.eql(id)
})
})
})

93
test/pubsub/sign.spec.js Normal file
View File

@ -0,0 +1,93 @@
/* eslint-env mocha */
/* eslint max-nested-callbacks: ["error", 5] */
'use strict'
const { expect } = require('aegir/utils/chai')
const uint8ArrayConcat = require('uint8arrays/concat')
const uint8ArrayFromString = require('uint8arrays/from-string')
const { RPC } = require('../../src/pubsub/message/rpc')
const {
signMessage,
SignPrefix,
verifySignature
} = require('../../src/pubsub/message/sign')
const PeerId = require('peer-id')
const { randomSeqno } = require('../../src/pubsub/utils')
describe('message signing', () => {
let peerId
before(async () => {
peerId = await PeerId.create({
bits: 1024
})
})
it('should be able to sign and verify a message', async () => {
const message = {
from: peerId.id,
data: uint8ArrayFromString('hello'),
seqno: randomSeqno(),
topicIDs: ['test-topic']
}
const bytesToSign = uint8ArrayConcat([SignPrefix, RPC.Message.encode(message).finish()])
const expectedSignature = await peerId.privKey.sign(bytesToSign)
const signedMessage = await signMessage(peerId, message)
// Check the signature and public key
expect(signedMessage.signature).to.eql(expectedSignature)
expect(signedMessage.key).to.eql(peerId.pubKey.bytes)
// Verify the signature
const verified = await verifySignature(signedMessage)
expect(verified).to.eql(true)
})
it('should be able to extract the public key from an inlined key', async () => {
const secPeerId = await PeerId.create({ keyType: 'secp256k1' })
const message = {
from: secPeerId.id,
data: uint8ArrayFromString('hello'),
seqno: randomSeqno(),
topicIDs: ['test-topic']
}
const bytesToSign = uint8ArrayConcat([SignPrefix, RPC.Message.encode(message).finish()])
const expectedSignature = await secPeerId.privKey.sign(bytesToSign)
const signedMessage = await signMessage(secPeerId, message)
// Check the signature and public key
expect(signedMessage.signature).to.eql(expectedSignature)
signedMessage.key = undefined
// Verify the signature
const verified = await verifySignature(signedMessage)
expect(verified).to.eql(true)
})
it('should be able to extract the public key from the message', async () => {
const message = {
from: peerId.id,
data: uint8ArrayFromString('hello'),
seqno: randomSeqno(),
topicIDs: ['test-topic']
}
const bytesToSign = uint8ArrayConcat([SignPrefix, RPC.Message.encode(message).finish()])
const expectedSignature = await peerId.privKey.sign(bytesToSign)
const signedMessage = await signMessage(peerId, message)
// Check the signature and public key
expect(signedMessage.signature).to.eql(expectedSignature)
expect(signedMessage.key).to.eql(peerId.pubKey.bytes)
// Verify the signature
const verified = await verifySignature(signedMessage)
expect(verified).to.eql(true)
})
})

View File

@ -0,0 +1,104 @@
/* eslint-env mocha */
'use strict'
const { expect } = require('aegir/utils/chai')
const sinon = require('sinon')
const pWaitFor = require('p-wait-for')
const errCode = require('err-code')
const PeerId = require('peer-id')
const uint8ArrayEquals = require('uint8arrays/equals')
const uint8ArrayFromString = require('uint8arrays/from-string')
const PeerStreams = require('../../src/pubsub/peer-streams')
const { SignaturePolicy } = require('../../src/pubsub/signature-policy')
const {
createPeerId,
mockRegistrar,
PubsubImplementation
} = require('./utils')
const protocol = '/pubsub/1.0.0'
describe('topic validators', () => {
let pubsub
beforeEach(async () => {
const peerId = await createPeerId()
pubsub = new PubsubImplementation(protocol, {
peerId: peerId,
registrar: mockRegistrar
}, {
globalSignaturePolicy: SignaturePolicy.StrictNoSign
})
pubsub.start()
})
afterEach(() => {
sinon.restore()
})
it('should filter messages by topic validator', async () => {
// use _publish.callCount() to see if a message is valid or not
sinon.spy(pubsub, '_publish')
sinon.stub(pubsub.peers, 'get').returns({})
const filteredTopic = 't'
const peer = new PeerStreams({ id: await PeerId.create() })
// Set a trivial topic validator
pubsub.topicValidators.set(filteredTopic, (topic, message) => {
if (!uint8ArrayEquals(message.data, uint8ArrayFromString('a message'))) {
throw errCode(new Error(), 'ERR_TOPIC_VALIDATOR_REJECT')
}
})
// valid case
const validRpc = {
subscriptions: [],
msgs: [{
data: uint8ArrayFromString('a message'),
topicIDs: [filteredTopic]
}]
}
// process valid message
pubsub.subscribe(filteredTopic)
pubsub._processRpc(peer.id.toB58String(), peer, validRpc)
await pWaitFor(() => pubsub._publish.callCount === 1)
// invalid case
const invalidRpc = {
subscriptions: [],
msgs: [{
data: uint8ArrayFromString('a different message'),
topicIDs: [filteredTopic]
}]
}
// process invalid message
pubsub._processRpc(peer.id.toB58String(), peer, invalidRpc)
expect(pubsub._publish.callCount).to.eql(1)
// remove topic validator
pubsub.topicValidators.delete(filteredTopic)
// another invalid case
const invalidRpc2 = {
subscriptions: [],
msgs: [{
data: uint8ArrayFromString('a different message'),
topicIDs: [filteredTopic]
}]
}
// process previously invalid message, now is valid
pubsub._processRpc(peer.id.toB58String(), peer, invalidRpc2)
pubsub.unsubscribe(filteredTopic)
await pWaitFor(() => pubsub._publish.callCount === 2)
})
})

78
test/pubsub/utils.spec.js Normal file
View File

@ -0,0 +1,78 @@
/* eslint-env mocha */
'use strict'
const { expect } = require('aegir/utils/chai')
const utils = require('../../src/pubsub/utils')
const uint8ArrayFromString = require('uint8arrays/from-string')
describe('utils', () => {
it('randomSeqno', () => {
const first = utils.randomSeqno()
const second = utils.randomSeqno()
expect(first).to.have.length(8)
expect(second).to.have.length(8)
expect(first).to.not.eql(second)
})
it('msgId should not generate same ID for two different Uint8Arrays', () => {
const peerId = 'QmPNdSYk5Rfpo5euNqwtyizzmKXMNHdXeLjTQhcN4yfX22'
const msgId0 = utils.msgId(peerId, uint8ArrayFromString('15603533e990dfde', 'base16'))
const msgId1 = utils.msgId(peerId, uint8ArrayFromString('15603533e990dfe0', 'base16'))
expect(msgId0).to.not.deep.equal(msgId1)
})
it('anyMatch', () => {
[
[[1, 2, 3], [4, 5, 6], false],
[[1, 2], [1, 2], true],
[[1, 2, 3], [4, 5, 1], true],
[[5, 6, 1], [1, 2, 3], true],
[[], [], false],
[[1], [2], false]
].forEach((test) => {
expect(utils.anyMatch(new Set(test[0]), new Set(test[1])))
.to.eql(test[2])
expect(utils.anyMatch(new Set(test[0]), test[1]))
.to.eql(test[2])
})
})
it('ensureArray', () => {
expect(utils.ensureArray('hello')).to.be.eql(['hello'])
expect(utils.ensureArray([1, 2])).to.be.eql([1, 2])
})
it('converts an IN msg.from to b58', () => {
const binaryId = uint8ArrayFromString('1220e2187eb3e6c4fb3e7ff9ad4658610624a6315e0240fc6f37130eedb661e939cc', 'base16')
const stringId = 'QmdZEWgtaWAxBh93fELFT298La1rsZfhiC2pqwMVwy3jZM'
const m = [
{ from: binaryId },
{ from: stringId }
]
const expected = [
{ from: stringId },
{ from: stringId }
]
for (let i = 0; i < m.length; i++) {
expect(utils.normalizeInRpcMessage(m[i])).to.deep.eql(expected[i])
}
})
it('converts an OUT msg.from to binary', () => {
const binaryId = uint8ArrayFromString('1220e2187eb3e6c4fb3e7ff9ad4658610624a6315e0240fc6f37130eedb661e939cc', 'base16')
const stringId = 'QmdZEWgtaWAxBh93fELFT298La1rsZfhiC2pqwMVwy3jZM'
const m = [
{ from: binaryId },
{ from: stringId }
]
const expected = [
{ from: binaryId },
{ from: binaryId }
]
for (let i = 0; i < m.length; i++) {
expect(utils.normalizeOutRpcMessage(m[i])).to.deep.eql(expected[i])
}
})
})

View File

@ -0,0 +1,85 @@
'use strict'
const DuplexPair = require('it-pair/duplex')
const PeerId = require('peer-id')
const PubsubBaseProtocol = require('../../../src/pubsub')
const { RPC } = require('../../../src/pubsub/message/rpc')
exports.createPeerId = async () => {
const peerId = await PeerId.create({ bits: 1024 })
return peerId
}
class PubsubImplementation extends PubsubBaseProtocol {
constructor (protocol, libp2p, options = {}) {
super({
debugName: 'libp2p:pubsub',
multicodecs: protocol,
libp2p,
...options
})
}
_publish (message) {
// ...
}
_decodeRpc (bytes) {
return RPC.decode(bytes)
}
_encodeRpc (rpc) {
return RPC.encode(rpc).finish()
}
}
exports.PubsubImplementation = PubsubImplementation
exports.mockRegistrar = {
handle: () => {},
register: () => {},
unregister: () => {}
}
exports.createMockRegistrar = (registrarRecord) => ({
handle: (multicodecs, handler) => {
const rec = registrarRecord[multicodecs[0]] || {}
registrarRecord[multicodecs[0]] = {
...rec,
handler
}
},
register: ({ multicodecs, _onConnect, _onDisconnect }) => {
const rec = registrarRecord[multicodecs[0]] || {}
registrarRecord[multicodecs[0]] = {
...rec,
onConnect: _onConnect,
onDisconnect: _onDisconnect
}
return multicodecs[0]
},
unregister: (id) => {
delete registrarRecord[id]
}
})
exports.ConnectionPair = () => {
const [d0, d1] = DuplexPair()
return [
{
stream: d0,
newStream: () => Promise.resolve({ stream: d0 })
},
{
stream: d1,
newStream: () => Promise.resolve({ stream: d1 })
}
]
}

13
tsconfig.json Normal file
View File

@ -0,0 +1,13 @@
{
"extends": "./node_modules/aegir/src/config/tsconfig.aegir.json",
"compilerOptions": {
"outDir": "dist"
},
"include": [
"src"
],
"exclude": [
"src/pubsub/message/rpc.js", // exclude generated file
"src/pubsub/message/topic-descriptor.js" // exclude generated file
]
}