mirror of
https://github.com/fluencelabs/js-libp2p-interfaces
synced 2025-04-24 22:52:35 +00:00
Compare commits
110 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
96d4a118db | ||
|
d14c37d14b | ||
|
9340ec9d98 | ||
|
d9224d5409 | ||
|
edb8ca60e9 | ||
|
810642d8ff | ||
|
64a478d313 | ||
|
b948972c5b | ||
|
edcedf093d | ||
|
3b960d516f | ||
|
2429e7246f | ||
|
22a7f6a9f1 | ||
|
fb9fce8713 | ||
|
91dba97125 | ||
|
93d712bb0e | ||
|
ca520775eb | ||
|
ceb91c672e | ||
|
30ffad42c8 | ||
|
c365399e4f | ||
|
5b99e6b56b | ||
|
f4c19c9ef2 | ||
|
c36698f488 | ||
|
3ecbc3e889 | ||
|
e2419ea308 | ||
|
04e93d3f08 | ||
|
83d7d52d7e | ||
|
1a3ea82776 | ||
|
ad2dfa42dc | ||
|
b75f2cab48 | ||
|
8512997e76 | ||
|
269a6f5e0a | ||
|
14d09970ca | ||
|
c98c58e824 | ||
|
946b046440 | ||
|
d168c7d531 | ||
|
349c1174db | ||
|
e14844315b | ||
|
ff3bd10704 | ||
|
4ecd7d3c83 | ||
|
eacdc246da | ||
|
c43e8e26bd | ||
|
94cf0fa4fc | ||
|
ba15a48dd9 | ||
|
c4be5eed4b | ||
|
9e0d23de9c | ||
|
4130e7f098 | ||
|
d6376377d3 | ||
|
a60ae091e5 | ||
|
4902542e78 | ||
|
4d5f56d06f | ||
|
13aa6cbfa0 | ||
|
a8ba13da4b | ||
|
75f6777d89 | ||
|
71b813ad3b | ||
|
46589ce3d0 | ||
|
f2a18818f2 | ||
|
1cc943e1b2 | ||
|
4adedcc4bf | ||
|
0628d708c4 | ||
|
e10a1545c8 | ||
|
9fbf9d0331 | ||
|
aa996d2054 | ||
|
507013a724 | ||
|
a55c7c454a | ||
|
87e2e89791 | ||
|
5bcfc966f7 | ||
|
c8c249de6e | ||
|
5b138ef0a0 | ||
|
bdd2502ef6 | ||
|
1bef8d5d78 | ||
|
9a8f375d40 | ||
|
afc2aa6891 | ||
|
424a0c081e | ||
|
e824b15032 | ||
|
dba3b9932f | ||
|
c56dd3e3fa | ||
|
de64a49007 | ||
|
3cfd9714bd | ||
|
78e015cef3 | ||
|
bd5a8b9337 | ||
|
098390cd31 | ||
|
953e289e1a | ||
|
ba822856ef | ||
|
c77d8de2e7 | ||
|
6203109751 | ||
|
25eeedd20e | ||
|
5c88d77aaa | ||
|
f6871afd76 | ||
|
734c491bb0 | ||
|
c43a52d176 | ||
|
af1ba5a409 | ||
|
c5b724ab93 | ||
|
6fb45b775b | ||
|
85c68b5df4 | ||
|
27ac0a5670 | ||
|
2de533e15b | ||
|
aacb0e27d0 | ||
|
c00f94dfaf | ||
|
5967834a2f | ||
|
32ee3e18e2 | ||
|
557f6cab66 | ||
|
d98bb23fea | ||
|
6cf3723019 | ||
|
01e240420d | ||
|
ff0313721c | ||
|
2803e64969 | ||
|
e979bc9d4e | ||
|
bdbd58e897 | ||
|
79bfcacb61 | ||
|
148f3c9f43 |
53
.github/workflows/main.yml
vendored
Normal file
53
.github/workflows/main.yml
vendored
Normal 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
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -13,3 +13,4 @@ docs
|
||||
node_modules
|
||||
# Lock files
|
||||
package-lock.json
|
||||
yarn.lock
|
40
.travis.yml
40
.travis.yml
@ -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
|
332
CHANGELOG.md
332
CHANGELOG.md
@ -1,3 +1,335 @@
|
||||
# [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)
|
||||
|
||||
|
||||
### Chores
|
||||
|
||||
* update deps ([#57](https://github.com/libp2p/js-interfaces/issues/57)) ([75f6777](https://github.com/libp2p/js-interfaces/commit/75f6777))
|
||||
|
||||
|
||||
### BREAKING CHANGES
|
||||
|
||||
* - The peer id dep of this module has replaced node Buffers with Uint8Arrays
|
||||
|
||||
* chore: update gh deps
|
||||
|
||||
|
||||
|
||||
<a name="0.3.2"></a>
|
||||
## [0.3.2](https://github.com/libp2p/js-interfaces/compare/v0.3.1...v0.3.2) (2020-07-15)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* record interface ([#52](https://github.com/libp2p/js-interfaces/issues/52)) ([1cc943e](https://github.com/libp2p/js-interfaces/commit/1cc943e))
|
||||
|
||||
|
||||
|
||||
<a name="0.3.1"></a>
|
||||
## [0.3.1](https://github.com/libp2p/js-interfaces/compare/v0.2.8...v0.3.1) (2020-07-03)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* content and peer routing multiaddrs property ([#49](https://github.com/libp2p/js-interfaces/issues/49)) ([9fbf9d0](https://github.com/libp2p/js-interfaces/commit/9fbf9d0))
|
||||
* peer-routing typo ([#47](https://github.com/libp2p/js-interfaces/issues/47)) ([9a8f375](https://github.com/libp2p/js-interfaces/commit/9a8f375))
|
||||
* reconnect should trigger topology on connect if protocol stored ([#54](https://github.com/libp2p/js-interfaces/issues/54)) ([e10a154](https://github.com/libp2p/js-interfaces/commit/e10a154))
|
||||
|
||||
|
||||
### Chores
|
||||
|
||||
* remove peer-info usage on topology ([#42](https://github.com/libp2p/js-interfaces/issues/42)) ([a55c7c4](https://github.com/libp2p/js-interfaces/commit/a55c7c4))
|
||||
* update content and peer routing interfaces removing peer-info ([#43](https://github.com/libp2p/js-interfaces/issues/43)) ([87e2e89](https://github.com/libp2p/js-interfaces/commit/87e2e89))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* peer-discovery not using peer-info ([bdd2502](https://github.com/libp2p/js-interfaces/commit/bdd2502))
|
||||
|
||||
|
||||
### BREAKING CHANGES
|
||||
|
||||
* topology api now uses peer-id instead of peer-info
|
||||
* content-routing and peer-routing APIs return an object with relevant properties instead of peer-info
|
||||
* peer-discovery emits object with id and multiaddrs properties
|
||||
|
||||
|
||||
|
||||
<a name="0.3.0"></a>
|
||||
# [0.3.0](https://github.com/libp2p/js-interfaces/compare/v0.2.8...v0.3.0) (2020-04-21)
|
||||
|
||||
|
||||
### Chores
|
||||
|
||||
* remove peer-info usage on topology ([#42](https://github.com/libp2p/js-interfaces/issues/42)) ([79a7843](https://github.com/libp2p/js-interfaces/commit/79a7843))
|
||||
* update content and peer routing interfaces removing peer-info ([#43](https://github.com/libp2p/js-interfaces/issues/43)) ([d2032e6](https://github.com/libp2p/js-interfaces/commit/d2032e6))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* peer-discovery not using peer-info ([5792b13](https://github.com/libp2p/js-interfaces/commit/5792b13))
|
||||
|
||||
|
||||
### BREAKING CHANGES
|
||||
|
||||
* topology api now uses peer-id instead of peer-info
|
||||
* content-routing and peer-routing APIs return an object with relevant properties instead of peer-info
|
||||
* peer-discovery emits object with id and multiaddrs properties
|
||||
|
||||
|
||||
|
||||
<a name="0.2.8"></a>
|
||||
## [0.2.8](https://github.com/libp2p/js-interfaces/compare/v0.2.7...v0.2.8) (2020-04-21)
|
||||
|
||||
|
||||
|
||||
<a name="0.2.7"></a>
|
||||
## [0.2.7](https://github.com/libp2p/js-interfaces/compare/v0.2.6...v0.2.7) (2020-03-20)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* add buffer ([#39](https://github.com/libp2p/js-interfaces/issues/39)) ([78e015c](https://github.com/libp2p/js-interfaces/commit/78e015c))
|
||||
|
||||
|
||||
|
||||
<a name="0.2.6"></a>
|
||||
## [0.2.6](https://github.com/libp2p/js-interfaces/compare/v0.2.5...v0.2.6) (2020-02-17)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* remove use of assert module ([#34](https://github.com/libp2p/js-interfaces/issues/34)) ([c77d8de](https://github.com/libp2p/js-interfaces/commit/c77d8de))
|
||||
|
||||
|
||||
|
||||
<a name="0.2.5"></a>
|
||||
## [0.2.5](https://github.com/libp2p/js-interfaces/compare/v0.2.4...v0.2.5) (2020-02-04)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **connection:** tracks streams properly ([#25](https://github.com/libp2p/js-interfaces/issues/25)) ([5c88d77](https://github.com/libp2p/js-interfaces/commit/5c88d77))
|
||||
|
||||
|
||||
|
||||
<a name="0.2.4"></a>
|
||||
## [0.2.4](https://github.com/libp2p/js-interfaces/compare/v0.2.3...v0.2.4) (2020-02-04)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* dependencies for tests should not be needed by who requires the tests ([#18](https://github.com/libp2p/js-interfaces/issues/18)) ([c5b724a](https://github.com/libp2p/js-interfaces/commit/c5b724a))
|
||||
|
||||
|
||||
|
||||
<a name="0.2.3"></a>
|
||||
## [0.2.3](https://github.com/libp2p/js-interfaces/compare/v0.2.2...v0.2.3) (2020-01-21)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **transport:** make close listener test more resilient ([#21](https://github.com/libp2p/js-interfaces/issues/21)) ([2de533e](https://github.com/libp2p/js-interfaces/commit/2de533e))
|
||||
|
||||
|
||||
|
||||
<a name="0.2.2"></a>
|
||||
## [0.2.2](https://github.com/libp2p/js-interfaces/compare/v0.2.1...v0.2.2) (2020-01-17)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **connection:** dont require remoteAddr on creation ([#20](https://github.com/libp2p/js-interfaces/issues/20)) ([5967834](https://github.com/libp2p/js-interfaces/commit/5967834))
|
||||
|
||||
|
||||
|
||||
<a name="0.2.1"></a>
|
||||
## [0.2.1](https://github.com/libp2p/js-interfaces/compare/v0.2.0...v0.2.1) (2019-12-28)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add crypto transmission error ([#17](https://github.com/libp2p/js-interfaces/issues/17)) ([d98bb23](https://github.com/libp2p/js-interfaces/commit/d98bb23))
|
||||
|
||||
|
||||
|
||||
<a name="0.2.0"></a>
|
||||
# [0.2.0](https://github.com/libp2p/js-interfaces/compare/v0.1.7...v0.2.0) (2019-12-20)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* transport should not handle connection if upgradeInbound throws ([#16](https://github.com/libp2p/js-interfaces/issues/16)) ([ff03137](https://github.com/libp2p/js-interfaces/commit/ff03137))
|
||||
|
||||
|
||||
|
||||
<a name="0.1.7"></a>
|
||||
## [0.1.7](https://github.com/libp2p/js-interfaces/compare/v0.1.6...v0.1.7) (2019-12-15)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* export connection status' ([#15](https://github.com/libp2p/js-interfaces/issues/15)) ([bdbd58e](https://github.com/libp2p/js-interfaces/commit/bdbd58e))
|
||||
|
||||
|
||||
|
||||
<a name="0.1.6"></a>
|
||||
## [0.1.6](https://github.com/libp2p/js-interfaces/compare/v0.1.5...v0.1.6) (2019-12-02)
|
||||
|
||||
|
@ -4,6 +4,8 @@
|
||||
[](http://libp2p.io/)
|
||||
[](http://webchat.freenode.net/?channels=%23libp2p)
|
||||
[](https://discuss.libp2p.io)
|
||||
[](https://codecov.io/gh/libp2p/js-libp2p-interfaces)
|
||||
[](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,8 @@
|
||||
- [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)
|
||||
- [Transport](./src/transport)
|
||||
@ -29,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)
|
||||
|
||||
|
67
package.json
67
package.json
@ -1,13 +1,28 @@
|
||||
{
|
||||
"name": "libp2p-interfaces",
|
||||
"version": "0.1.6",
|
||||
"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",
|
||||
"test": "aegir test",
|
||||
@ -32,29 +47,43 @@
|
||||
},
|
||||
"homepage": "https://github.com/libp2p/js-interfaces#readme",
|
||||
"dependencies": {
|
||||
"@types/bl": "4.1.0",
|
||||
"abort-controller": "^3.0.0",
|
||||
"abortable-iterator": "^2.1.0",
|
||||
"async-iterator-to-pull-stream": "^1.3.0",
|
||||
"chai": "^4.2.0",
|
||||
"abortable-iterator": "^3.0.0",
|
||||
"chai": "^4.3.4",
|
||||
"chai-checkmark": "^1.0.1",
|
||||
"class-is": "^1.1.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",
|
||||
"libp2p-tcp": "^0.14.1",
|
||||
"multiaddr": "^7.1.0",
|
||||
"p-limit": "^2.2.1",
|
||||
"peer-id": "^0.13.3",
|
||||
"sinon": "^7.5.0",
|
||||
"streaming-iterables": "^4.1.0"
|
||||
"err-code": "^3.0.1",
|
||||
"it-goodbye": "^2.0.2",
|
||||
"it-length-prefixed": "^3.1.0",
|
||||
"it-pair": "^1.0.0",
|
||||
"it-pipe": "^1.1.0",
|
||||
"it-pushable": "^1.4.2",
|
||||
"libp2p-crypto": "fluencelabs/js-libp2p-crypto",
|
||||
"libp2p-tcp": "fluencelabs/js-libp2p-tcp",
|
||||
"multiaddr": "^9.0.1",
|
||||
"multibase": "^4.0.2",
|
||||
"multihashes": "^4.0.2",
|
||||
"p-defer": "^3.0.0",
|
||||
"p-limit": "^3.1.0",
|
||||
"p-wait-for": "^3.2.0",
|
||||
"peer-id": "fluencelabs/js-peer-id",
|
||||
"protons": "^2.0.0",
|
||||
"sinon": "^10.0.0",
|
||||
"streaming-iterables": "^5.0.4",
|
||||
"uint8arrays": "^2.1.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"aegir": "^20.4.1",
|
||||
"it-handshake": "^1.0.0",
|
||||
"it-pair": "^1.0.0",
|
||||
"it-pipe": "^1.0.1",
|
||||
"peer-info": "^0.17.0"
|
||||
"@types/debug": "^4.1.5",
|
||||
"aegir": "^32.1.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>",
|
||||
@ -64,6 +93,7 @@
|
||||
"Friedel Ziegelmayer <dignifiedquire@gmail.com>",
|
||||
"Greg Zuro <gregzuro@users.noreply.github.com>",
|
||||
"Jacob Heun <jacobheun@gmail.com>",
|
||||
"Jacob Heun <jacobheun@Jacobs-MacBook-Pro.local>",
|
||||
"James Ray <16969914+jamesray1@users.noreply.github.com>",
|
||||
"Jeffrey Hulten <jhulten@gmail.com>",
|
||||
"João Santos <joaosantos15@users.noreply.github.com>",
|
||||
@ -76,6 +106,7 @@
|
||||
"Sathya Narrayanan <plasmashadowx@gmail.com>",
|
||||
"Vasco Santos <vasco.santos@moxy.studio>",
|
||||
"Vasco Santos <vasco.santos@ua.pt>",
|
||||
"dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>",
|
||||
"dirkmc <dirkmdev@gmail.com>",
|
||||
"dmitriy ryajov <dryajov@dmitriys-MBP.HomeNET>",
|
||||
"greenkeeperio-bot <support@greenkeeper.io>",
|
||||
|
@ -110,7 +110,7 @@ const conn = new Connection({
|
||||
Creates a new Connection instance.
|
||||
|
||||
`localAddr` is the optional [multiaddr](https://github.com/multiformats/multiaddr) address used by the local peer to reach the remote.
|
||||
`remoteAddr` is the [multiaddr](https://github.com/multiformats/multiaddr) address used to communicate with the remote peer.
|
||||
`remoteAddr` is the optional [multiaddr](https://github.com/multiformats/multiaddr) address used to communicate with the remote peer.
|
||||
`localPeer` is the [PeerId](https://github.com/libp2p/js-peer-id) of the local peer.
|
||||
`remotePeer` is the [PeerId](https://github.com/libp2p/js-peer-id) of the remote peer.
|
||||
`newStream` is the `function` responsible for getting a new muxed+multistream-selected stream.
|
||||
@ -121,6 +121,7 @@ Creates a new Connection instance.
|
||||
- `timeline` is an `object` with the relevant events timestamps of the connection (`open`, `upgraded` and `closed`; the `closed` will be added when the connection is closed).
|
||||
- `multiplexer` is a `string` with the connection multiplexing codec (optional).
|
||||
- `encryption` is a `string` with the connection encryption method identifier (optional).
|
||||
- `status` is a `string` indicating the overall status of the connection. It is one of [`'open'`, `'closing'`, `'closed'`]
|
||||
|
||||
#### Create a new stream
|
||||
|
||||
@ -220,7 +221,17 @@ This getter returns an `Object` with the metadata of the connection, as follows:
|
||||
|
||||
- `status`:
|
||||
|
||||
This property contains the status of the connection. It can be either `open`, `closing` or `closed`. Once the connection is created it is in an `open` status. When a `conn.close()` happens, the status will change to `closing` and finally, after all the connection streams are properly closed, the status will be `closed`.
|
||||
This property contains the status of the connection. It can be either `open`, `closing` or `closed`. Once the connection is created it is in an `open` status. When a `conn.close()` happens, the status will change to `closing` and finally, after all the connection streams are properly closed, the status will be `closed`. These values can also be directly referenced by importing the `status` file:
|
||||
|
||||
```js
|
||||
const {
|
||||
OPEN, CLOSING, CLOSED
|
||||
} = require('libp2p-interfaces/src/connection/status')
|
||||
|
||||
if (connection.stat.status === OPEN) {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
- `timeline`:
|
||||
|
||||
|
@ -2,11 +2,42 @@
|
||||
|
||||
const PeerId = require('peer-id')
|
||||
const multiaddr = require('multiaddr')
|
||||
|
||||
const withIs = require('class-is')
|
||||
|
||||
const assert = require('assert')
|
||||
const errCode = require('err-code')
|
||||
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.
|
||||
@ -14,41 +45,19 @@ const errCode = require('err-code')
|
||||
*/
|
||||
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.
|
||||
* 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 }) {
|
||||
localAddr && assert(multiaddr.isMultiaddr(localAddr), 'localAddr must be an instance of multiaddr')
|
||||
assert(multiaddr.isMultiaddr(remoteAddr), 'remoteAddr must be an instance of multiaddr')
|
||||
assert(PeerId.isPeerId(localPeer), 'localPeer must be an instance of peer-id')
|
||||
assert(PeerId.isPeerId(remotePeer), 'remotePeer must be an instance of peer-id')
|
||||
assert(typeof newStream === 'function', 'new stream must be a function')
|
||||
assert(typeof close === 'function', 'close must be a function')
|
||||
assert(typeof getStreams === 'function', 'getStreams must be a function')
|
||||
assert(stat, 'connection metadata object must be provided')
|
||||
assert(stat.direction === 'inbound' || stat.direction === 'outbound', 'direction must be "inbound" or "outbound"')
|
||||
assert(stat.timeline, 'connection timeline object must be provided in the stat object')
|
||||
assert(stat.timeline.open, 'connection open timestamp must be provided')
|
||||
assert(stat.timeline.upgraded, 'connection upgraded timestamp must be provided')
|
||||
validateArgs(localAddr, localPeer, remotePeer, newStream, close, getStreams, stat)
|
||||
|
||||
/**
|
||||
* Connection identifier.
|
||||
*/
|
||||
this.id = (parseInt(Math.random() * 1e9)).toString(36) + Date.now()
|
||||
this.id = (parseInt(String(Math.random() * 1e9))).toString(36) + Date.now()
|
||||
|
||||
/**
|
||||
* Observed multiaddr of the local peer
|
||||
@ -72,10 +81,12 @@ class Connection {
|
||||
|
||||
/**
|
||||
* Connection metadata.
|
||||
*
|
||||
* @type {ConectionStat & {status: Status}}
|
||||
*/
|
||||
this._stat = {
|
||||
...stat,
|
||||
status: 'open'
|
||||
status: OPEN
|
||||
}
|
||||
|
||||
/**
|
||||
@ -100,13 +111,34 @@ class Connection {
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @return {Object}
|
||||
*
|
||||
* @this {Connection}
|
||||
*/
|
||||
get stat () {
|
||||
return this._stat
|
||||
@ -114,7 +146,8 @@ class Connection {
|
||||
|
||||
/**
|
||||
* Get all the streams of the muxer.
|
||||
* @return {Array<*>}
|
||||
*
|
||||
* @this {Connection}
|
||||
*/
|
||||
get streams () {
|
||||
return this._getStreams()
|
||||
@ -122,15 +155,16 @@ class Connection {
|
||||
|
||||
/**
|
||||
* 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
|
||||
*
|
||||
* @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') {
|
||||
if (this.stat.status === CLOSING) {
|
||||
throw errCode(new Error('the connection is being closed'), 'ERR_CONNECTION_BEING_CLOSED')
|
||||
}
|
||||
|
||||
if (this.stat.status === 'closed') {
|
||||
if (this.stat.status === CLOSED) {
|
||||
throw errCode(new Error('the connection is closed'), 'ERR_CONNECTION_CLOSED')
|
||||
}
|
||||
|
||||
@ -138,7 +172,7 @@ class Connection {
|
||||
|
||||
const { stream, protocol } = await this._newStream(protocols)
|
||||
|
||||
this.addStream(stream, protocol)
|
||||
this.addStream(stream, { protocol })
|
||||
|
||||
return {
|
||||
stream,
|
||||
@ -148,11 +182,10 @@ class Connection {
|
||||
|
||||
/**
|
||||
* 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}
|
||||
*
|
||||
* @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
|
||||
@ -164,7 +197,8 @@ class Connection {
|
||||
|
||||
/**
|
||||
* Remove stream registry after it is closed.
|
||||
* @param {string} id identifier of the stream
|
||||
*
|
||||
* @param {string} id - identifier of the stream
|
||||
*/
|
||||
removeStream (id) {
|
||||
this.registry.delete(id)
|
||||
@ -172,10 +206,11 @@ class Connection {
|
||||
|
||||
/**
|
||||
* Close the connection.
|
||||
* @return {Promise}
|
||||
*
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async close () {
|
||||
if (this.stat.status === 'closed') {
|
||||
if (this.stat.status === CLOSED) {
|
||||
return
|
||||
}
|
||||
|
||||
@ -183,14 +218,69 @@ class Connection {
|
||||
return this._closing
|
||||
}
|
||||
|
||||
this.stat.status = 'closing'
|
||||
this.stat.status = CLOSING
|
||||
|
||||
// Close raw connection
|
||||
this._closing = await this._close()
|
||||
|
||||
this._stat.timeline.close = Date.now()
|
||||
this.stat.status = 'closed'
|
||||
this.stat.status = CLOSED
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = withIs(Connection, { className: 'Connection', symbolName: '@libp2p/interface-connection/connection' })
|
||||
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.Multiaddr.isMultiaddr(localAddr)) {
|
||||
throw errCode(new Error('localAddr must be an instance of multiaddr'), 'ERR_INVALID_PARAMETERS')
|
||||
}
|
||||
|
||||
if (!PeerId.isPeerId(localPeer)) {
|
||||
throw errCode(new Error('localPeer must be an instance of peer-id'), 'ERR_INVALID_PARAMETERS')
|
||||
}
|
||||
|
||||
if (!PeerId.isPeerId(remotePeer)) {
|
||||
throw errCode(new Error('remotePeer must be an instance of peer-id'), 'ERR_INVALID_PARAMETERS')
|
||||
}
|
||||
|
||||
if (typeof newStream !== 'function') {
|
||||
throw errCode(new Error('new stream must be a function'), 'ERR_INVALID_PARAMETERS')
|
||||
}
|
||||
|
||||
if (typeof close !== 'function') {
|
||||
throw errCode(new Error('close must be a function'), 'ERR_INVALID_PARAMETERS')
|
||||
}
|
||||
|
||||
if (typeof getStreams !== 'function') {
|
||||
throw errCode(new Error('getStreams must be a function'), 'ERR_INVALID_PARAMETERS')
|
||||
}
|
||||
|
||||
if (!stat) {
|
||||
throw errCode(new Error('connection metadata object must be provided'), 'ERR_INVALID_PARAMETERS')
|
||||
}
|
||||
|
||||
if (stat.direction !== 'inbound' && stat.direction !== 'outbound') {
|
||||
throw errCode(new Error('direction must be "inbound" or "outbound"'), 'ERR_INVALID_PARAMETERS')
|
||||
}
|
||||
|
||||
if (!stat.timeline) {
|
||||
throw errCode(new Error('connection timeline object must be provided in the stat object'), 'ERR_INVALID_PARAMETERS')
|
||||
}
|
||||
|
||||
if (!stat.timeline.open) {
|
||||
throw errCode(new Error('connection open timestamp must be provided'), 'ERR_INVALID_PARAMETERS')
|
||||
}
|
||||
|
||||
if (!stat.timeline.upgraded) {
|
||||
throw errCode(new Error('connection upgraded timestamp must be provided'), 'ERR_INVALID_PARAMETERS')
|
||||
}
|
||||
}
|
||||
|
12
src/connection/status.js
Normal file
12
src/connection/status.js
Normal file
@ -0,0 +1,12 @@
|
||||
'use strict'
|
||||
|
||||
const STATUS = {
|
||||
OPEN: /** @type {'open'} */('open'),
|
||||
CLOSING: /** @type {'closing'} */('closing'),
|
||||
CLOSED: /** @type {'closed'} */('closed')
|
||||
}
|
||||
module.exports = STATUS
|
||||
|
||||
/**
|
||||
* @typedef {STATUS[keyof STATUS]} Status
|
||||
*/
|
@ -1,3 +1,4 @@
|
||||
// @ts-nocheck interface tests
|
||||
/* eslint-env mocha */
|
||||
|
||||
'use strict'
|
||||
@ -6,6 +7,7 @@ const chai = require('chai')
|
||||
const expect = chai.expect
|
||||
chai.use(require('dirty-chai'))
|
||||
const sinon = require('sinon')
|
||||
const Status = require('../status')
|
||||
|
||||
module.exports = (test) => {
|
||||
describe('connection', () => {
|
||||
@ -28,7 +30,7 @@ module.exports = (test) => {
|
||||
expect(connection.remotePeer).to.exist()
|
||||
expect(connection.localAddr).to.exist()
|
||||
expect(connection.remoteAddr).to.exist()
|
||||
expect(connection.stat.status).to.equal('open')
|
||||
expect(connection.stat.status).to.equal(Status.OPEN)
|
||||
expect(connection.stat.timeline.open).to.exist()
|
||||
expect(connection.stat.timeline.upgraded).to.exist()
|
||||
expect(connection.stat.timeline.close).to.not.exist()
|
||||
@ -40,7 +42,7 @@ module.exports = (test) => {
|
||||
it('should get the metadata of an open connection', () => {
|
||||
const stat = connection.stat
|
||||
|
||||
expect(stat.status).to.equal('open')
|
||||
expect(stat.status).to.equal(Status.OPEN)
|
||||
expect(stat.direction).to.exist()
|
||||
expect(stat.timeline.open).to.exist()
|
||||
expect(stat.timeline.upgraded).to.exist()
|
||||
@ -73,6 +75,7 @@ module.exports = (test) => {
|
||||
let timelineProxy
|
||||
const proxyHandler = {
|
||||
set () {
|
||||
// @ts-ignore - TS fails to infer here
|
||||
return Reflect.set(...arguments)
|
||||
}
|
||||
}
|
||||
@ -103,7 +106,7 @@ module.exports = (test) => {
|
||||
await connection.close()
|
||||
|
||||
expect(connection.stat.timeline.close).to.exist()
|
||||
expect(connection.stat.status).to.equal('closed')
|
||||
expect(connection.stat.status).to.equal(Status.CLOSED)
|
||||
})
|
||||
|
||||
it('should be able to close the connection after opening a stream', async () => {
|
||||
@ -116,7 +119,20 @@ module.exports = (test) => {
|
||||
await connection.close()
|
||||
|
||||
expect(connection.stat.timeline.close).to.exist()
|
||||
expect(connection.stat.status).to.equal('closed')
|
||||
expect(connection.stat.status).to.equal(Status.CLOSED)
|
||||
})
|
||||
|
||||
it('should properly track streams', async () => {
|
||||
// Open stream
|
||||
const protocol = '/echo/0.0.1'
|
||||
const { stream } = await connection.newStream(protocol)
|
||||
const trackedStream = connection.registry.get(stream.id)
|
||||
expect(trackedStream).to.have.property('protocol', protocol)
|
||||
|
||||
// Close stream
|
||||
await stream.close()
|
||||
|
||||
expect(connection.registry.get(stream.id)).to.not.exist()
|
||||
})
|
||||
|
||||
it('should support a proxy on the timeline', async () => {
|
||||
@ -124,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')
|
||||
|
@ -1,3 +1,4 @@
|
||||
// @ts-nocheck interface tests
|
||||
/* eslint-env mocha */
|
||||
|
||||
'use strict'
|
||||
|
@ -12,7 +12,7 @@ Publishing a test suite as a module lets multiple modules all ensure compatibili
|
||||
# Modules that implement the interface
|
||||
|
||||
- [JavaScript libp2p-kad-dht](https://github.com/libp2p/js-libp2p-kad-dht)
|
||||
- [JavaScript libp2p-delegated-peer-routing](https://github.com/libp2p/js-libp2p-delegated-peer-routing)
|
||||
- [JavaScript libp2p-delegated-content-routing](https://github.com/libp2p/js-libp2p-delegated-content-routing)
|
||||
|
||||
# Badge
|
||||
|
||||
@ -30,6 +30,32 @@ TBD
|
||||
|
||||
A valid (read: that follows this abstraction) Content Routing module must implement the following API.
|
||||
|
||||
### `.findProviders`
|
||||
### findProviders
|
||||
|
||||
### `.provide`
|
||||
- `findProviders(cid)`
|
||||
|
||||
Find peers in the network that can provide a specific value, given a key.
|
||||
|
||||
**Parameters**
|
||||
- [CID](https://github.com/multiformats/js-cid)
|
||||
|
||||
**Returns**
|
||||
|
||||
It returns an `AsyncIterable` containing the identification and addresses of the peers providing the given key, as follows:
|
||||
|
||||
`AsyncIterable<{ id: PeerId, multiaddrs: Multiaddr[] }>`
|
||||
|
||||
### provide
|
||||
|
||||
- `provide(cid)`
|
||||
|
||||
Announce to the network that we are providing the given value.
|
||||
|
||||
**Parameters**
|
||||
- [CID](https://github.com/multiformats/js-cid)
|
||||
|
||||
**Returns**
|
||||
|
||||
It returns a promise that is resolved on the success of the operation.
|
||||
|
||||
`Promise<void>`
|
||||
|
11
src/content-routing/types.d.ts
vendored
Normal file
11
src/content-routing/types.d.ts
vendored
Normal 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[] }>;
|
||||
}
|
@ -85,6 +85,7 @@ Common crypto errors come with the interface, and can be imported directly. All
|
||||
```js
|
||||
const {
|
||||
InvalidCryptoExchangeError,
|
||||
InvalidCryptoTransmissionError,
|
||||
UnexpectedPeerError
|
||||
} = require('libp2p-interfaces/src/crypto/errors')
|
||||
|
||||
@ -95,4 +96,5 @@ console.log(error.code === UnexpectedPeerError.code) // true
|
||||
### Error Types
|
||||
|
||||
- `InvalidCryptoExchangeError` - Should be thrown when a peer provides data that is insufficient to finish the crypto exchange.
|
||||
- `InvalidCryptoTransmissionError` - Should be thrown when an error occurs during encryption/decryption.
|
||||
- `UnexpectedPeerError` - Should be thrown when the expected peer id does not match the peer id determined via the crypto exchange.
|
||||
|
@ -22,7 +22,19 @@ class InvalidCryptoExchangeError extends Error {
|
||||
}
|
||||
}
|
||||
|
||||
class InvalidCryptoTransmissionError extends Error {
|
||||
constructor (message = 'Invalid crypto transmission') {
|
||||
super(message)
|
||||
this.code = InvalidCryptoTransmissionError.code
|
||||
}
|
||||
|
||||
static get code () {
|
||||
return 'ERR_INVALID_CRYPTO_TRANSMISSION'
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
UnexpectedPeerError,
|
||||
InvalidCryptoExchangeError
|
||||
InvalidCryptoExchangeError,
|
||||
InvalidCryptoTransmissionError
|
||||
}
|
||||
|
@ -1,16 +1,20 @@
|
||||
// @ts-nocheck interface tests
|
||||
/* eslint-env mocha */
|
||||
'use strict'
|
||||
|
||||
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
|
||||
@ -54,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
24
src/crypto/types.d.ts
vendored
Normal 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;
|
||||
}
|
@ -16,6 +16,7 @@ The API is presented with both Node.js and Go primitives, however, there is not
|
||||
- [JavaScript libp2p-kad-dht](https://github.com/libp2p/js-libp2p-kad-dht)
|
||||
- [JavaScript libp2p-webrtc-star](https://github.com/libp2p/js-libp2p-webrtc-star)
|
||||
- [JavaScript libp2p-websocket-star](https://github.com/libp2p/js-libp2p-websocket-star)
|
||||
- [TypeScript discv5](https://github.com/chainsafe/discv5)
|
||||
|
||||
Send a PR to add a new one if you happen to find or write one.
|
||||
|
||||
@ -32,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
|
||||
@ -69,6 +70,9 @@ It returns a `Promise`
|
||||
|
||||
### discoverying peers
|
||||
|
||||
- `discovery.on('peer', (peerInfo) => {})`
|
||||
- `discovery.on('peer', (peerData) => {})`
|
||||
|
||||
Everytime a peer is discovered by a discovery service, it emmits a `peer` event with the discover peer's [PeerInfo](https://github.com/libp2p/js-peer-info).
|
||||
Every time a peer is discovered by a discovery service, it emits a `peer` event with the discovered peer's information, which must contain the following properties:
|
||||
|
||||
- `<`[`PeerId`](https://github.com/libp2p/js-peer-id)`>` `peerData.id`
|
||||
- `<Array<`[`Multiaddr`](https://github.com/multiformats/js-multiaddr)`>>` `peerData.multiaddrs`
|
||||
|
@ -1,17 +1,32 @@
|
||||
// @ts-nocheck interface tests
|
||||
/* eslint-env mocha */
|
||||
'use strict'
|
||||
|
||||
const chai = require('chai')
|
||||
const expect = chai.expect
|
||||
chai.use(require('dirty-chai'))
|
||||
|
||||
const multiaddr = require('multiaddr')
|
||||
const PeerId = require('peer-id')
|
||||
|
||||
const delay = require('delay')
|
||||
const pDefer = require('p-defer')
|
||||
|
||||
module.exports = (common) => {
|
||||
describe('interface-peer-discovery', () => {
|
||||
let discovery
|
||||
|
||||
before(async () => {
|
||||
beforeEach(async () => {
|
||||
discovery = await common.setup()
|
||||
})
|
||||
|
||||
after(() => common.teardown && common.teardown())
|
||||
afterEach('ensure discovery was stopped', async () => {
|
||||
await discovery.stop()
|
||||
|
||||
afterEach('ensure discovery was stopped', () => discovery.stop())
|
||||
discovery.removeAllListeners()
|
||||
|
||||
common.teardown && common.teardown()
|
||||
})
|
||||
|
||||
it('can start the service', async () => {
|
||||
await discovery.start()
|
||||
@ -30,5 +45,49 @@ module.exports = (common) => {
|
||||
await discovery.start()
|
||||
await discovery.start()
|
||||
})
|
||||
|
||||
it('should emit a peer event after start', async () => {
|
||||
const defer = pDefer()
|
||||
await discovery.start()
|
||||
|
||||
discovery.on('peer', ({ id, multiaddrs }) => {
|
||||
expect(id).to.exist()
|
||||
expect(PeerId.isPeerId(id)).to.eql(true)
|
||||
expect(multiaddrs).to.exist()
|
||||
|
||||
multiaddrs.forEach((m) => expect(multiaddr.Multiaddr.isMultiaddr(m)).to.eql(true))
|
||||
|
||||
defer.resolve()
|
||||
})
|
||||
|
||||
await defer.promise
|
||||
})
|
||||
|
||||
it('should not receive a peer event before start', async () => {
|
||||
discovery.on('peer', () => {
|
||||
throw new Error('should not receive a peer event before start')
|
||||
})
|
||||
|
||||
await delay(2000)
|
||||
})
|
||||
|
||||
it('should not receive a peer event after stop', async () => {
|
||||
const deferStart = pDefer()
|
||||
|
||||
await discovery.start()
|
||||
|
||||
discovery.on('peer', () => {
|
||||
deferStart.resolve()
|
||||
})
|
||||
|
||||
await deferStart.promise
|
||||
await discovery.stop()
|
||||
|
||||
discovery.on('peer', () => {
|
||||
throw new Error('should not receive a peer event after stop')
|
||||
})
|
||||
|
||||
await delay(2000)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
10
src/peer-discovery/types.d.ts
vendored
Normal file
10
src/peer-discovery/types.d.ts
vendored
Normal 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;
|
||||
}
|
@ -30,12 +30,17 @@ TBD
|
||||
|
||||
A valid (read: that follows this abstraction) Peer Routing module must implement the following API.
|
||||
|
||||
### `.findPeers` - Find peers 'responsible' or 'closest' to a given key
|
||||
### findPeer
|
||||
|
||||
- `Node.js` peerRouting.findPeers(key, function (err, peersPriorityQueue) {})
|
||||
- `findPeer(peerId)`
|
||||
|
||||
In a peer to peer context, the concept of 'responsability' or 'closeness' for a given key translates to having a way to find deterministically or that at least there is a significant overlap between searches, the same group of peers when searching for the same given key.
|
||||
Query the network for all multiaddresses associated with a `PeerId`.
|
||||
|
||||
This method will query the network (route it) and return a Priority Queue datastructe with a list of PeerInfo objects, ordered by 'closeness'.
|
||||
**Parameters**
|
||||
- [peerId](https://github.com/libp2p/js-peer-id).
|
||||
|
||||
key is a multihash
|
||||
**Returns**
|
||||
|
||||
It returns the [peerId](https://github.com/libp2p/js-peer-id) together with the known peers [multiaddrs](https://github.com/multiformats/js-multiaddr), as follows:
|
||||
|
||||
`Promise<{ id: PeerId, multiaddrs: Multiaddr[] }>`
|
||||
|
10
src/peer-routing/types.d.ts
vendored
Normal file
10
src/peer-routing/types.d.ts
vendored
Normal 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
255
src/pubsub/README.md
Normal 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
46
src/pubsub/errors.js
Normal 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'
|
||||
}
|
751
src/pubsub/index.js
Normal file
751
src/pubsub/index.js
Normal file
@ -0,0 +1,751 @@
|
||||
'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 message = require('./message')
|
||||
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('./message/types').RPC} RPC
|
||||
* @typedef {import('./message/types').SubOpts} RPCSubOpts
|
||||
* @typedef {import('./message/types').Message} RPCMessage
|
||||
* @typedef {import('./signature-policy').SignaturePolicyType} SignaturePolicyType
|
||||
*/
|
||||
|
||||
/**
|
||||
* @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((/** @type {RPCSubOpts} */ 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.some((/** @type {string} */ 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 {RPCSubOpts} subOpt
|
||||
*/
|
||||
_processRpcSubOpt (id, subOpt) {
|
||||
const t = subOpt.topicID
|
||||
|
||||
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 {RPCMessage} msg - the message object
|
||||
* @returns {Uint8Array} message id as bytes
|
||||
*/
|
||||
getMsgId (msg) {
|
||||
const signaturePolicy = this.globalSignaturePolicy
|
||||
switch (signaturePolicy) {
|
||||
case SignaturePolicy.StrictSign:
|
||||
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 message.rpc.RPC.decode(bytes)
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode RPC object into a Uint8Array.
|
||||
* This can be override to use a custom router protobuf.
|
||||
*
|
||||
* @param {RPC} rpc
|
||||
* @returns {Uint8Array}
|
||||
*/
|
||||
_encodeRpc (rpc) {
|
||||
return message.rpc.RPC.encode(rpc)
|
||||
}
|
||||
|
||||
/**
|
||||
* Send an rpc object to a peer
|
||||
*
|
||||
* @param {string} id - peer id
|
||||
* @param {RPC} 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) {
|
||||
continue // eslint-disable-line
|
||||
}
|
||||
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 {RPCMessage} message
|
||||
* @returns {Promise<RPCMessage>}
|
||||
*/
|
||||
_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 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()
|
||||
let msgObject = {
|
||||
receivedFrom: from,
|
||||
data: message,
|
||||
topicIDs: [topic]
|
||||
}
|
||||
|
||||
// ensure that the message follows the signature policy
|
||||
const outMsg = await this._buildMessage(msgObject)
|
||||
msgObject = utils.normalizeInRpcMessage(outMsg)
|
||||
|
||||
// Emit to self if I'm interested and emitSelf enabled
|
||||
this.emitSelf && this._emitMessage(msgObject)
|
||||
|
||||
// send to all the other peers
|
||||
await this._publish(msgObject)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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} 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.message = message
|
||||
PubsubBaseProtocol.utils = utils
|
||||
PubsubBaseProtocol.SignaturePolicy = SignaturePolicy
|
||||
|
||||
module.exports = PubsubBaseProtocol
|
16
src/pubsub/message/index.js
Normal file
16
src/pubsub/message/index.js
Normal file
@ -0,0 +1,16 @@
|
||||
'use strict'
|
||||
|
||||
// @ts-ignore protons not typed
|
||||
const protons = require('protons')
|
||||
|
||||
const rpcProto = protons(require('./rpc.proto.js'))
|
||||
const RPC = rpcProto.RPC
|
||||
const topicDescriptorProto = protons(require('./topic-descriptor.proto.js'))
|
||||
|
||||
module.exports = {
|
||||
rpc: rpcProto,
|
||||
td: topicDescriptorProto,
|
||||
RPC,
|
||||
Message: RPC.Message,
|
||||
SubOpts: RPC.SubOpts
|
||||
}
|
20
src/pubsub/message/rpc.proto.js
Normal file
20
src/pubsub/message/rpc.proto.js
Normal file
@ -0,0 +1,20 @@
|
||||
'use strict'
|
||||
module.exports = `
|
||||
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;
|
||||
}
|
||||
}`
|
100
src/pubsub/message/sign.js
Normal file
100
src/pubsub/message/sign.js
Normal file
@ -0,0 +1,100 @@
|
||||
'use strict'
|
||||
|
||||
const PeerId = require('peer-id')
|
||||
const { Message } = require('./index')
|
||||
const uint8ArrayConcat = require('uint8arrays/concat')
|
||||
const uint8ArrayFromString = require('uint8arrays/from-string')
|
||||
const SignPrefix = uint8ArrayFromString('libp2p-pubsub:')
|
||||
|
||||
/**
|
||||
* Signs the provided message with the given `peerId`
|
||||
*
|
||||
* @param {PeerId} peerId
|
||||
* @param {Message} message
|
||||
* @returns {Promise<Message>}
|
||||
*/
|
||||
async function signMessage (peerId, message) {
|
||||
// Get the message in bytes, and prepend with the pubsub prefix
|
||||
const bytes = uint8ArrayConcat([
|
||||
SignPrefix,
|
||||
Message.encode(message)
|
||||
])
|
||||
|
||||
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,
|
||||
Message.encode({
|
||||
...message,
|
||||
from: message.from && PeerId.createFromCID(message.from).toBytes(),
|
||||
signature: undefined,
|
||||
key: undefined
|
||||
})
|
||||
])
|
||||
|
||||
// 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
|
||||
}
|
30
src/pubsub/message/topic-descriptor.proto.js
Normal file
30
src/pubsub/message/topic-descriptor.proto.js
Normal file
@ -0,0 +1,30 @@
|
||||
'use strict'
|
||||
module.exports = `
|
||||
// topicCID = cid(merkledag_protobuf(topicDescriptor)); (not the topic.name)
|
||||
message TopicDescriptor {
|
||||
optional string name = 1;
|
||||
optional AuthOpts auth = 2;
|
||||
optional EncOpts enc = 2;
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}`
|
5
src/pubsub/message/types.d.ts
vendored
Normal file
5
src/pubsub/message/types.d.ts
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
import { RPC, Message, SubOpts } from './types'
|
||||
|
||||
export type RPC = RPC
|
||||
export type Message = Message
|
||||
export type SubOpts = SubOpts
|
205
src/pubsub/peer-streams.js
Normal file
205
src/pubsub/peer-streams.js
Normal file
@ -0,0 +1,205 @@
|
||||
'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')
|
||||
|
||||
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
|
33
src/pubsub/signature-policy.js
Normal file
33
src/pubsub/signature-policy.js
Normal 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
94
src/pubsub/tests/api.js
Normal 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()
|
||||
})
|
||||
})
|
||||
}
|
70
src/pubsub/tests/emit-self.js
Normal file
70
src/pubsub/tests/emit-self.js
Normal 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
19
src/pubsub/tests/index.js
Normal 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)
|
||||
})
|
||||
}
|
116
src/pubsub/tests/messages.js
Normal file
116
src/pubsub/tests/messages.js
Normal 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)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
341
src/pubsub/tests/multiple-nodes.js
Normal file
341
src/pubsub/tests/multiple-nodes.js
Normal 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(() => { })
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
229
src/pubsub/tests/two-nodes.js
Normal file
229
src/pubsub/tests/two-nodes.js
Normal file
@ -0,0 +1,229 @@
|
||||
// @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.be.eql([{ topicID: topic, subscribe: 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.be.eql([{ topicID: topic, subscribe: 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
10
src/pubsub/tests/utils.js
Normal 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)
|
||||
}
|
137
src/pubsub/utils.js
Normal file
137
src/pubsub/utils.js
Normal file
@ -0,0 +1,137 @@
|
||||
'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')
|
||||
|
||||
/**
|
||||
* 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 & {from?:string, receivedFrom:string}} message
|
||||
* @param {string} [peerId]
|
||||
* @returns {T & {from?: string, peerId?: string }}
|
||||
*/
|
||||
const normalizeInRpcMessage = (message, peerId) => {
|
||||
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} message
|
||||
* @returns {T & {from?: Uint8Array, data?: Uint8Array}}
|
||||
*/
|
||||
const normalizeOutRpcMessage = (message) => {
|
||||
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
|
||||
}
|
91
src/record/README.md
Normal file
91
src/record/README.md
Normal file
@ -0,0 +1,91 @@
|
||||
interface-record
|
||||
==================
|
||||
|
||||
A libp2p node needs to store data in a public location (e.g. a DHT), or rely on potentially untrustworthy intermediaries to relay information. Libp2p provides an all-purpose data container called **envelope**, which includes a signature of the data, so that it its authenticity can be verified.
|
||||
|
||||
The record represents the data that will be stored inside the **envelope** when distributing records across the network. The `interface-record` aims to guarantee that any type of record created is compliant with the libp2p **envelope**.
|
||||
|
||||
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 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
|
||||
|
||||
```js
|
||||
const tests = require('libp2p-interfaces/src/record/tests')
|
||||
describe('your record', () => {
|
||||
tests({
|
||||
async setup () {
|
||||
return YourRecord
|
||||
},
|
||||
async teardown () {
|
||||
// cleanup resources created by setup()
|
||||
}
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
## Create 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 = fromString('0301', 'hex')
|
||||
|
||||
/**
|
||||
* @implements {import('libp2p-interfaces/src/record/types').Record}
|
||||
*/
|
||||
class PeerRecord {
|
||||
constructor (peerId, multiaddrs, seqNumber) {
|
||||
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
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API
|
||||
|
||||
### marshal
|
||||
|
||||
- `record.marshal()`
|
||||
|
||||
Marshal a record to be used in a libp2p envelope.
|
||||
|
||||
**Returns**
|
||||
|
||||
It returns a `Protobuf` containing the record data.
|
||||
|
||||
### equals
|
||||
|
||||
- `record.equals(other)`
|
||||
|
||||
Verifies if the other Record is identical to this one.
|
||||
|
||||
**Parameters**
|
||||
- other is a `Record` to compare with the current instance.
|
||||
|
||||
**Returns**
|
||||
- `other is Record`
|
36
src/record/tests/index.js
Normal file
36
src/record/tests/index.js
Normal file
@ -0,0 +1,36 @@
|
||||
// @ts-nocheck interface tests
|
||||
/* eslint-env mocha */
|
||||
|
||||
'use strict'
|
||||
|
||||
const chai = require('chai')
|
||||
const expect = chai.expect
|
||||
chai.use(require('dirty-chai'))
|
||||
|
||||
module.exports = (test) => {
|
||||
describe('record', () => {
|
||||
let record
|
||||
|
||||
beforeEach(async () => {
|
||||
record = await test.setup()
|
||||
if (!record) throw new Error('missing record')
|
||||
})
|
||||
|
||||
afterEach(() => test.teardown())
|
||||
|
||||
it('has domain and codec', () => {
|
||||
expect(record.domain).to.exist()
|
||||
expect(record.codec).to.exist()
|
||||
})
|
||||
|
||||
it('is able to marshal', () => {
|
||||
const rawData = record.marshal()
|
||||
expect(rawData).to.be.an.instanceof(Uint8Array)
|
||||
})
|
||||
|
||||
it('is able to compare two records', () => {
|
||||
const equals = record.equals(record)
|
||||
expect(equals).to.eql(true)
|
||||
})
|
||||
})
|
||||
}
|
21
src/record/types.d.ts
vendored
Normal file
21
src/record/types.d.ts
vendored
Normal 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
|
||||
}
|
@ -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) {
|
||||
|
@ -1,14 +1,16 @@
|
||||
// @ts-nocheck interface tests
|
||||
/* eslint-env mocha */
|
||||
/* eslint max-nested-callbacks: ["error", 8] */
|
||||
'use strict'
|
||||
|
||||
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 { 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')
|
||||
|
||||
@ -17,7 +19,7 @@ function pause (ms) {
|
||||
}
|
||||
|
||||
function randomBuffer () {
|
||||
return Buffer.from(Math.random().toString())
|
||||
return uint8arrayFromString(Math.random().toString())
|
||||
}
|
||||
|
||||
const infiniteRandom = {
|
||||
|
@ -1,3 +1,4 @@
|
||||
// @ts-nocheck interface tests
|
||||
/* eslint-env mocha */
|
||||
'use strict'
|
||||
|
||||
|
@ -1,3 +1,4 @@
|
||||
// @ts-nocheck interface tests
|
||||
/* eslint-env mocha */
|
||||
'use strict'
|
||||
|
||||
|
@ -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 }
|
||||
}
|
||||
|
@ -1,3 +1,4 @@
|
||||
// @ts-nocheck interface tests
|
||||
/* eslint-env mocha */
|
||||
'use strict'
|
||||
|
||||
|
51
src/stream-muxer/types.d.ts
vendored
Normal file
51
src/stream-muxer/types.d.ts
vendored
Normal 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>;
|
@ -56,8 +56,8 @@ const toplogy = new MulticodecTopology({
|
||||
max: 50,
|
||||
multicodecs: ['/echo/1.0.0'],
|
||||
handlers: {
|
||||
onConnect: (peerInfo, conn) => {},
|
||||
onDisconnect: (peerInfo) => {}
|
||||
onConnect: (peerId, conn) => {},
|
||||
onDisconnect: (peerId) => {}
|
||||
}
|
||||
})
|
||||
```
|
||||
@ -69,8 +69,8 @@ The `MulticodecTopology` extends the `Topology`, which makes the `Topology` API
|
||||
### Topology
|
||||
|
||||
- `Topology`
|
||||
- `peers<Map<string, PeerInfo>>`: A Map of peers belonging to the topology.
|
||||
- `disconnect<function(PeerInfo)>`: Called when a peer has been disconnected
|
||||
- `peers<Map<string, PeerId>>`: A Map of peers belonging to the topology.
|
||||
- `disconnect<function(PeerId)>`: Called when a peer has been disconnected
|
||||
|
||||
#### Constructor
|
||||
|
||||
@ -79,8 +79,8 @@ const toplogy = new Topology({
|
||||
min: 0,
|
||||
max: 50,
|
||||
handlers: {
|
||||
onConnect: (peerInfo, conn) => {},
|
||||
onDisconnect: (peerInfo) => {}
|
||||
onConnect: (peerId, conn) => {},
|
||||
onDisconnect: (peerId) => {}
|
||||
}
|
||||
})
|
||||
```
|
||||
@ -95,27 +95,27 @@ const toplogy = new Topology({
|
||||
|
||||
#### Set a peer
|
||||
|
||||
- `topology.peers.set(id, peerInfo)`
|
||||
- `topology.peers.set(id, peerId)`
|
||||
|
||||
Add a peer to the topology.
|
||||
|
||||
**Parameters**
|
||||
- `id` is the `string` that identifies the peer to add.
|
||||
- `peerInfo` is the [PeerInfo][peer-info] of the peer to add.
|
||||
- `peerId` is the [PeerId][peer-id] of the peer to add.
|
||||
|
||||
#### Notify about a peer disconnected event
|
||||
|
||||
- `topology.disconnect(peerInfo)`
|
||||
- `topology.disconnect(peerId)`
|
||||
|
||||
**Parameters**
|
||||
- `peerInfo` is the [PeerInfo][peer-info] of the peer disconnected.
|
||||
- `peerId` is the [PeerId][peer-id] of the peer disconnected.
|
||||
|
||||
### Multicodec Topology
|
||||
|
||||
- `MulticodecTopology`
|
||||
- `registrar<Registrar>`: The `Registrar` of the topology. This is set by the `Registrar` during registration.
|
||||
- `peers<Map<string, PeerInfo>>`: The Map of peers that belong to the topology
|
||||
- `disconnect<function(PeerInfo)>`: Disconnects a peer from the topology.
|
||||
- `peers<Map<string, PeerId>>`: The Map of peers that belong to the topology
|
||||
- `disconnect<function(PeerId)>`: Disconnects a peer from the topology.
|
||||
|
||||
#### Constructor
|
||||
|
||||
@ -125,8 +125,8 @@ const toplogy = new MulticodecTopology({
|
||||
max: 50,
|
||||
multicodecs: ['/echo/1.0.0'],
|
||||
handlers: {
|
||||
onConnect: (peerInfo, conn) => {},
|
||||
onDisconnect: (peerInfo) => {}
|
||||
onConnect: (peerId, conn) => {},
|
||||
onDisconnect: (peerId) => {}
|
||||
}
|
||||
})
|
||||
```
|
||||
@ -139,3 +139,5 @@ const toplogy = new MulticodecTopology({
|
||||
- `handlers` is an optional `Object` containing the handler called when a peer is connected or disconnected.
|
||||
- `onConnect` is a `function` called everytime a peer is connected in the topology context.
|
||||
- `onDisconnect` is a `function` called everytime a peer is disconnected in the topology context.
|
||||
|
||||
[peer-id]: https://github.com/libp2p/js-peer-id
|
||||
|
@ -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,
|
||||
@ -26,21 +36,48 @@ class Topology {
|
||||
this._onConnect = handlers.onConnect || noop
|
||||
this._onDisconnect = handlers.onDisconnect || noop
|
||||
|
||||
this.peers = new Map()
|
||||
/**
|
||||
* 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 {PeerInfo} peerInfo
|
||||
*
|
||||
* @param {PeerId} peerId
|
||||
* @returns {void}
|
||||
*/
|
||||
disconnect (peerInfo) {
|
||||
this._onDisconnect(peerInfo)
|
||||
disconnect (peerId) {
|
||||
this._onDisconnect(peerId)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = withIs(Topology, { className: 'Topology', symbolName: '@libp2p/js-interfaces/topology' })
|
||||
module.exports = Topology
|
||||
|
@ -1,20 +1,11 @@
|
||||
'use strict'
|
||||
|
||||
const assert = require('assert')
|
||||
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,
|
||||
@ -24,22 +15,54 @@ class MulticodecTopology extends Topology {
|
||||
}) {
|
||||
super({ min, max, handlers })
|
||||
|
||||
assert(multicodecs, 'one or more multicodec should be provided')
|
||||
assert(handlers, 'the handlers should be provided')
|
||||
assert(handlers.onConnect && typeof handlers.onConnect === 'function',
|
||||
'the \'onConnect\' handler must be provided')
|
||||
assert(handlers.onDisconnect && typeof handlers.onDisconnect === 'function',
|
||||
'the \'onDisconnect\' handler must be provided')
|
||||
if (!multicodecs) {
|
||||
throw new Error('one or more multicodec should be provided')
|
||||
}
|
||||
|
||||
if (!handlers) {
|
||||
throw new Error('the handlers should be provided')
|
||||
}
|
||||
|
||||
if (typeof handlers.onConnect !== 'function') {
|
||||
throw new Error('the \'onConnect\' handler must be provided')
|
||||
}
|
||||
|
||||
if (typeof handlers.onDisconnect !== 'function') {
|
||||
throw new Error('the \'onDisconnect\' handler must be provided')
|
||||
}
|
||||
|
||||
this.multicodecs = Array.isArray(multicodecs) ? multicodecs : [multicodecs]
|
||||
this._registrar = undefined
|
||||
|
||||
this._onProtocolChange = this._onProtocolChange.bind(this)
|
||||
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)
|
||||
|
||||
// Update topology peers
|
||||
this._updatePeers(this._registrar.peerStore.peers.values())
|
||||
@ -47,47 +70,81 @@ class MulticodecTopology extends Topology {
|
||||
|
||||
/**
|
||||
* Update topology.
|
||||
* @param {Array<PeerInfo>} peerInfoIterable
|
||||
*
|
||||
* @param {Array<{id: PeerId, multiaddrs: Array<Multiaddr>, protocols: Array<string>}>} peerDataIterable
|
||||
* @returns {void}
|
||||
*/
|
||||
_updatePeers (peerInfoIterable) {
|
||||
for (const peerInfo of peerInfoIterable) {
|
||||
if (this.multicodecs.filter(multicodec => peerInfo.protocols.has(multicodec)).length) {
|
||||
_updatePeers (peerDataIterable) {
|
||||
for (const { id, protocols } of peerDataIterable) {
|
||||
if (this.multicodecs.filter(multicodec => protocols.includes(multicodec)).length) {
|
||||
// Add the peer regardless of whether or not there is currently a connection
|
||||
this.peers.set(peerInfo.id.toB58String(), peerInfo)
|
||||
this.peers.add(id.toB58String())
|
||||
// If there is a connection, call _onConnect
|
||||
const connection = this._registrar.getConnection(peerInfo)
|
||||
connection && this._onConnect(peerInfo, connection)
|
||||
const connection = this._registrar.getConnection(id)
|
||||
connection && this._onConnect(id, connection)
|
||||
} else {
|
||||
// Remove any peers we might be tracking that are no longer of value to us
|
||||
this.peers.delete(peerInfo.id.toB58String())
|
||||
this.peers.delete(id.toB58String())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a new peer support the multicodecs for this topology.
|
||||
*
|
||||
* @param {Object} props
|
||||
* @param {PeerInfo} props.peerInfo
|
||||
* @param {PeerId} props.peerId
|
||||
* @param {Array<string>} props.protocols
|
||||
*/
|
||||
_onProtocolChange ({ peerInfo, protocols }) {
|
||||
const existingPeer = this.peers.get(peerInfo.id.toB58String())
|
||||
_onProtocolChange ({ peerId, protocols }) {
|
||||
const hadPeer = this.peers.has(peerId.toB58String())
|
||||
const hasProtocol = protocols.filter(protocol => this.multicodecs.includes(protocol))
|
||||
|
||||
// Not supporting the protocol anymore?
|
||||
if (existingPeer && hasProtocol.length === 0) {
|
||||
this._onDisconnect(peerInfo)
|
||||
if (hadPeer && hasProtocol.length === 0) {
|
||||
this._onDisconnect(peerId)
|
||||
}
|
||||
|
||||
// New to protocol support
|
||||
for (const protocol of protocols) {
|
||||
if (this.multicodecs.includes(protocol)) {
|
||||
this._updatePeers([peerInfo])
|
||||
const peerData = this._registrar.peerStore.get(peerId)
|
||||
this._updatePeers([peerData])
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)
|
||||
|
||||
if (!protocols) {
|
||||
return
|
||||
}
|
||||
|
||||
if (this.multicodecs.find(multicodec => protocols.includes(multicodec))) {
|
||||
this.peers.add(peerId.toB58String())
|
||||
this._onConnect(peerId, connection)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
|
@ -1,3 +1,4 @@
|
||||
// @ts-nocheck interface tests
|
||||
/* eslint-env mocha */
|
||||
|
||||
'use strict'
|
||||
@ -8,19 +9,18 @@ chai.use(require('dirty-chai'))
|
||||
const sinon = require('sinon')
|
||||
|
||||
const PeerId = require('peer-id')
|
||||
const PeerInfo = require('peer-info')
|
||||
|
||||
const peers = require('../../utils/peers')
|
||||
|
||||
module.exports = (test) => {
|
||||
describe('multicodec topology', () => {
|
||||
let topology, peer
|
||||
let topology, id
|
||||
|
||||
beforeEach(async () => {
|
||||
topology = await test.setup()
|
||||
if (!topology) throw new Error('missing multicodec topology')
|
||||
|
||||
const id = await PeerId.createFromJSON(peers[0])
|
||||
peer = await PeerInfo.create(id)
|
||||
id = await PeerId.createFromJSON(peers[0])
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
@ -38,7 +38,7 @@ module.exports = (test) => {
|
||||
|
||||
it('should trigger "onDisconnect" on peer disconnected', () => {
|
||||
sinon.spy(topology, '_onDisconnect')
|
||||
topology.disconnect(peer)
|
||||
topology.disconnect(id)
|
||||
|
||||
expect(topology._onDisconnect.callCount).to.equal(1)
|
||||
})
|
||||
@ -47,13 +47,16 @@ module.exports = (test) => {
|
||||
sinon.spy(topology, '_updatePeers')
|
||||
expect(topology.peers.size).to.eql(0)
|
||||
|
||||
const id2 = await PeerId.createFromJSON(peers[1])
|
||||
const peer2 = await PeerInfo.create(id2)
|
||||
topology.multicodecs.forEach((m) => peer2.protocols.add(m))
|
||||
|
||||
const peerStore = topology._registrar.peerStore
|
||||
|
||||
const id2 = await PeerId.createFromJSON(peers[1])
|
||||
peerStore.peers.set(id2.toB58String(), {
|
||||
id: id2,
|
||||
protocols: Array.from(topology.multicodecs)
|
||||
})
|
||||
|
||||
peerStore.emit('change:protocols', {
|
||||
peerInfo: peer2,
|
||||
peerId: id2,
|
||||
protocols: Array.from(topology.multicodecs)
|
||||
})
|
||||
|
||||
@ -65,28 +68,67 @@ module.exports = (test) => {
|
||||
sinon.spy(topology, '_onDisconnect')
|
||||
expect(topology.peers.size).to.eql(0)
|
||||
|
||||
const id2 = await PeerId.createFromJSON(peers[1])
|
||||
const peer2 = await PeerInfo.create(id2)
|
||||
topology.multicodecs.forEach((m) => peer2.protocols.add(m))
|
||||
|
||||
const peerStore = topology._registrar.peerStore
|
||||
|
||||
const id2 = await PeerId.createFromJSON(peers[1])
|
||||
peerStore.peers.set(id2.toB58String(), {
|
||||
id: id2,
|
||||
protocols: Array.from(topology.multicodecs)
|
||||
})
|
||||
|
||||
peerStore.emit('change:protocols', {
|
||||
peerInfo: peer2,
|
||||
peerId: id2,
|
||||
protocols: Array.from(topology.multicodecs)
|
||||
})
|
||||
|
||||
expect(topology.peers.size).to.eql(1)
|
||||
|
||||
topology.multicodecs.forEach((m) => peer2.protocols.delete(m))
|
||||
peerStore.peers.set(id2.toB58String(), {
|
||||
id: id2,
|
||||
protocols: []
|
||||
})
|
||||
// Peer does not support the protocol anymore
|
||||
peerStore.emit('change:protocols', {
|
||||
peerInfo: peer2,
|
||||
peerId: id2,
|
||||
protocols: []
|
||||
})
|
||||
|
||||
expect(topology.peers.size).to.eql(1)
|
||||
expect(topology._onDisconnect.callCount).to.equal(1)
|
||||
expect(topology._onDisconnect.calledWith(peer2)).to.equal(true)
|
||||
expect(topology._onDisconnect.calledWith(id2)).to.equal(true)
|
||||
})
|
||||
|
||||
it('should trigger "onConnect" when a peer connects and has one of the topology multicodecs in its known protocols', () => {
|
||||
sinon.spy(topology, '_onConnect')
|
||||
sinon.stub(topology._registrar.peerStore.protoBook, 'get').returns(topology.multicodecs)
|
||||
|
||||
topology._registrar.connectionManager.emit('peer:connect', {
|
||||
remotePeer: id
|
||||
})
|
||||
|
||||
expect(topology._onConnect.callCount).to.equal(1)
|
||||
})
|
||||
|
||||
it('should not trigger "onConnect" when a peer connects and has none of the topology multicodecs in its known protocols', () => {
|
||||
sinon.spy(topology, '_onConnect')
|
||||
sinon.stub(topology._registrar.peerStore.protoBook, 'get').returns([])
|
||||
|
||||
topology._registrar.connectionManager.emit('peer:connect', {
|
||||
remotePeer: id
|
||||
})
|
||||
|
||||
expect(topology._onConnect.callCount).to.equal(0)
|
||||
})
|
||||
|
||||
it('should not trigger "onConnect" when a peer connects and its protocols are not known', () => {
|
||||
sinon.spy(topology, '_onConnect')
|
||||
sinon.stub(topology._registrar.peerStore.protoBook, 'get').returns(undefined)
|
||||
|
||||
topology._registrar.connectionManager.emit('peer:connect', {
|
||||
remotePeer: id
|
||||
})
|
||||
|
||||
expect(topology._onConnect.callCount).to.equal(0)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
@ -1,3 +1,4 @@
|
||||
// @ts-nocheck interface tests
|
||||
/* eslint-env mocha */
|
||||
|
||||
'use strict'
|
||||
@ -8,19 +9,17 @@ chai.use(require('dirty-chai'))
|
||||
const sinon = require('sinon')
|
||||
|
||||
const PeerId = require('peer-id')
|
||||
const PeerInfo = require('peer-info')
|
||||
const peers = require('../../utils/peers')
|
||||
|
||||
module.exports = (test) => {
|
||||
describe('topology', () => {
|
||||
let topology, peer
|
||||
let topology, id
|
||||
|
||||
beforeEach(async () => {
|
||||
topology = await test.setup()
|
||||
if (!topology) throw new Error('missing multicodec topology')
|
||||
|
||||
const id = await PeerId.createFromJSON(peers[0])
|
||||
peer = await PeerInfo.create(id)
|
||||
id = await PeerId.createFromJSON(peers[0])
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
@ -38,7 +37,7 @@ module.exports = (test) => {
|
||||
|
||||
it('should trigger "onDisconnect" on peer disconnected', () => {
|
||||
sinon.spy(topology, '_onDisconnect')
|
||||
topology.disconnect(peer)
|
||||
topology.disconnect(id)
|
||||
|
||||
expect(topology._onDisconnect.callCount).to.equal(1)
|
||||
})
|
||||
|
@ -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')
|
||||
|
||||
|
@ -1,3 +1,4 @@
|
||||
// @ts-nocheck interface tests
|
||||
/* eslint-env mocha */
|
||||
'use strict'
|
||||
|
||||
|
@ -1,3 +1,4 @@
|
||||
// @ts-nocheck interface tests
|
||||
/* eslint-env mocha */
|
||||
'use strict'
|
||||
|
||||
|
@ -1,3 +1,4 @@
|
||||
// @ts-nocheck interface tests
|
||||
/* eslint max-nested-callbacks: ["error", 8] */
|
||||
/* eslint-env mocha */
|
||||
'use strict'
|
||||
@ -8,7 +9,9 @@ const expect = chai.expect
|
||||
chai.use(dirtyChai)
|
||||
const sinon = require('sinon')
|
||||
|
||||
const pipe = require('it-pipe')
|
||||
const pWaitFor = require('p-wait-for')
|
||||
const { pipe } = require('it-pipe')
|
||||
const uint8arrayFromString = require('uint8arrays/from-string')
|
||||
const { isValidTick } = require('./utils')
|
||||
|
||||
module.exports = (common) => {
|
||||
@ -69,12 +72,12 @@ module.exports = (common) => {
|
||||
])
|
||||
|
||||
// Give the listener a chance to finish its upgrade
|
||||
await new Promise(resolve => setTimeout(resolve, 0))
|
||||
await pWaitFor(() => listenerConns.length === 2)
|
||||
|
||||
// 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)
|
||||
@ -92,6 +95,23 @@ module.exports = (common) => {
|
||||
expect(upgradeSpy.callCount).to.equal(2)
|
||||
})
|
||||
|
||||
it('should not handle connection if upgradeInbound throws', async () => {
|
||||
sinon.stub(upgrader, 'upgradeInbound').throws()
|
||||
|
||||
const listener = transport.createListener(() => {
|
||||
throw new Error('should not handle the connection if upgradeInbound throws')
|
||||
})
|
||||
|
||||
// Listen
|
||||
await listener.listen(addrs[0])
|
||||
|
||||
// Create a connection to the listener
|
||||
const socket = await transport.dial(addrs[0])
|
||||
|
||||
await pWaitFor(() => typeof socket.timeline.close === 'number')
|
||||
await listener.close()
|
||||
})
|
||||
|
||||
describe('events', () => {
|
||||
it('connection', (done) => {
|
||||
const upgradeSpy = sinon.spy(upgrader, 'upgradeInbound')
|
||||
|
@ -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
72
src/transport/types.d.ts
vendored
Normal 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
18
src/types.d.ts
vendored
Normal 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;
|
||||
}
|
@ -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.Multiaddr('/ip4/127.0.0.1/tcp/8080')
|
||||
const remoteAddr = new multiaddr.Multiaddr('/ip4/127.0.0.1/tcp/8081')
|
||||
const [localPeer, remotePeer] = await Promise.all([
|
||||
PeerId.createFromJSON(peers[0]),
|
||||
PeerId.createFromJSON(peers[1])
|
||||
@ -25,7 +26,7 @@ describe('compliance tests', () => {
|
||||
const openStreams = []
|
||||
let streamId = 0
|
||||
|
||||
return new Connection({
|
||||
const connection = new Connection({
|
||||
localPeer,
|
||||
remotePeer,
|
||||
localAddr,
|
||||
@ -43,7 +44,10 @@ describe('compliance tests', () => {
|
||||
const id = streamId++
|
||||
const stream = pair()
|
||||
|
||||
stream.close = () => stream.sink([])
|
||||
stream.close = async () => {
|
||||
await stream.sink([])
|
||||
connection.removeStream(stream.id)
|
||||
}
|
||||
stream.id = id
|
||||
|
||||
openStreams.push(stream)
|
||||
@ -57,6 +61,7 @@ describe('compliance tests', () => {
|
||||
getStreams: () => openStreams,
|
||||
...properties
|
||||
})
|
||||
return connection
|
||||
},
|
||||
async teardown () {
|
||||
// cleanup resources created by setup()
|
||||
|
48
test/connection/index.spec.js
Normal file
48
test/connection/index.spec.js
Normal file
@ -0,0 +1,48 @@
|
||||
/* eslint-env mocha */
|
||||
'use strict'
|
||||
|
||||
const { Connection } = require('../../src/connection')
|
||||
const peers = require('../../src/utils/peers')
|
||||
const PeerId = require('peer-id')
|
||||
const pair = require('it-pair')
|
||||
|
||||
describe('connection tests', () => {
|
||||
it('should not require local or remote addrs', async () => {
|
||||
const [localPeer, remotePeer] = await Promise.all([
|
||||
PeerId.createFromJSON(peers[0]),
|
||||
PeerId.createFromJSON(peers[1])
|
||||
])
|
||||
const openStreams = []
|
||||
let streamId = 0
|
||||
|
||||
return new Connection({
|
||||
localPeer,
|
||||
remotePeer,
|
||||
stat: {
|
||||
timeline: {
|
||||
open: Date.now() - 10,
|
||||
upgraded: Date.now()
|
||||
},
|
||||
direction: 'outbound',
|
||||
encryption: '/secio/1.0.0',
|
||||
multiplexer: '/mplex/6.7.0'
|
||||
},
|
||||
newStream: (protocols) => {
|
||||
const id = streamId++
|
||||
const stream = pair()
|
||||
|
||||
stream.close = () => stream.sink([])
|
||||
stream.id = id
|
||||
|
||||
openStreams.push(stream)
|
||||
|
||||
return {
|
||||
stream,
|
||||
protocol: protocols[0]
|
||||
}
|
||||
},
|
||||
close: () => {},
|
||||
getStreams: () => openStreams
|
||||
})
|
||||
})
|
||||
})
|
@ -5,13 +5,20 @@ const tests = require('../../src/peer-discovery/tests')
|
||||
const MockDiscovery = require('./mock-discovery')
|
||||
|
||||
describe('compliance tests', () => {
|
||||
let intervalId
|
||||
|
||||
tests({
|
||||
async setup () {
|
||||
await new Promise(resolve => setTimeout(resolve, 10))
|
||||
return new MockDiscovery()
|
||||
setup () {
|
||||
const mockDiscovery = new MockDiscovery({
|
||||
discoveryDelay: 1
|
||||
})
|
||||
|
||||
intervalId = setInterval(mockDiscovery._discoverPeer, 1000)
|
||||
|
||||
return mockDiscovery
|
||||
},
|
||||
async teardown () {
|
||||
await new Promise(resolve => setTimeout(resolve, 10))
|
||||
teardown () {
|
||||
clearInterval(intervalId)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
@ -2,8 +2,8 @@
|
||||
|
||||
const { EventEmitter } = require('events')
|
||||
|
||||
const multiaddr = require('multiaddr')
|
||||
const PeerId = require('peer-id')
|
||||
const PeerInfo = require('peer-info')
|
||||
|
||||
/**
|
||||
* Emits 'peer' events on discovery.
|
||||
@ -37,10 +37,12 @@ class MockDiscovery extends EventEmitter {
|
||||
if (!this._isRunning) return
|
||||
|
||||
const peerId = await PeerId.create({ bits: 512 })
|
||||
const peerInfo = new PeerInfo(peerId)
|
||||
|
||||
this._timer = setTimeout(() => {
|
||||
this.emit('peer', peerInfo)
|
||||
this.emit('peer', {
|
||||
id: peerId,
|
||||
multiaddrs: [new multiaddr.Multiaddr('/ip4/127.0.0.1/tcp/8000')]
|
||||
})
|
||||
}, this.options.discoveryDelay || 1000)
|
||||
}
|
||||
}
|
||||
|
78
test/pubsub/emit-self.spec.js
Normal file
78
test/pubsub/emit-self.spec.js
Normal 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))
|
||||
})
|
||||
})
|
||||
})
|
54
test/pubsub/instance.spec.js
Normal file
54
test/pubsub/instance.spec.js
Normal 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()
|
||||
})
|
||||
})
|
227
test/pubsub/lifesycle.spec.js
Normal file
227
test/pubsub/lifesycle.spec.js
Normal 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)
|
||||
})
|
||||
})
|
||||
})
|
90
test/pubsub/message.spec.js
Normal file
90
test/pubsub/message.spec.js
Normal 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
358
test/pubsub/pubsub.spec.js
Normal 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
93
test/pubsub/sign.spec.js
Normal 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 { Message } = require('../../src/pubsub/message')
|
||||
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, Message.encode(message)])
|
||||
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, Message.encode(message)])
|
||||
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, Message.encode(message)])
|
||||
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)
|
||||
})
|
||||
})
|
104
test/pubsub/topic-validators.spec.js
Normal file
104
test/pubsub/topic-validators.spec.js
Normal 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
78
test/pubsub/utils.spec.js
Normal 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])
|
||||
}
|
||||
})
|
||||
})
|
85
test/pubsub/utils/index.js
Normal file
85
test/pubsub/utils/index.js
Normal file
@ -0,0 +1,85 @@
|
||||
'use strict'
|
||||
|
||||
const DuplexPair = require('it-pair/duplex')
|
||||
|
||||
const PeerId = require('peer-id')
|
||||
|
||||
const PubsubBaseProtocol = require('../../../src/pubsub')
|
||||
const { message } = require('../../../src/pubsub')
|
||||
|
||||
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 message.rpc.RPC.decode(bytes)
|
||||
}
|
||||
|
||||
_encodeRpc (rpc) {
|
||||
return message.rpc.RPC.encode(rpc)
|
||||
}
|
||||
}
|
||||
|
||||
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 })
|
||||
}
|
||||
]
|
||||
}
|
@ -6,6 +6,13 @@ class MockPeerStore extends EventEmitter {
|
||||
constructor (peers) {
|
||||
super()
|
||||
this.peers = peers
|
||||
this.protoBook = {
|
||||
get: () => {}
|
||||
}
|
||||
}
|
||||
|
||||
get (peerId) {
|
||||
return this.peers.get(peerId.toB58String())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,8 @@
|
||||
/* eslint-env mocha */
|
||||
'use strict'
|
||||
|
||||
const { EventEmitter } = require('events')
|
||||
|
||||
const tests = require('../../src/topology/tests/multicodec-topology')
|
||||
const MulticodecTopology = require('../../src/topology/multicodec-topology')
|
||||
const MockPeerStore = require('./mock-peer-store')
|
||||
@ -21,10 +23,13 @@ describe('multicodec topology compliance tests', () => {
|
||||
})
|
||||
|
||||
if (!registrar) {
|
||||
const peerStore = new MockPeerStore([])
|
||||
const peers = new Map()
|
||||
const peerStore = new MockPeerStore(peers)
|
||||
const connectionManager = new EventEmitter()
|
||||
|
||||
registrar = {
|
||||
peerStore,
|
||||
connectionManager,
|
||||
getConnection: () => { }
|
||||
}
|
||||
}
|
||||
|
9
tsconfig.json
Normal file
9
tsconfig.json
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": "./node_modules/aegir/src/config/tsconfig.aegir.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "dist"
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
]
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user