├── .github └── workflows │ └── test-node.yml ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── example.js ├── index.js ├── lib ├── bulk-timer.js ├── connection-set.js ├── peer-discovery.js ├── peer-info.js └── retry-timer.js ├── package.json └── test ├── all.js ├── bulk-timer.js ├── chaos.js ├── dups.js ├── firewall.js ├── helpers └── index.js ├── manual └── measure-reconnect.js ├── peer-join.js ├── retry-timer.js ├── stats.js ├── suspend.js ├── swarm.js └── update.js /.github/workflows/test-node.yml: -------------------------------------------------------------------------------- 1 | name: Build Status 2 | on: 3 | push: 4 | branches: 5 | - main 6 | tags: # To trigger the canary 7 | - '*' 8 | pull_request: 9 | branches: 10 | - main 11 | jobs: 12 | build: 13 | if: ${{ !startsWith(github.ref, 'refs/tags/')}} # Already runs for the push of the commit, no need to run again for the tag 14 | strategy: 15 | matrix: 16 | node-version: [lts/*] 17 | os: [ubuntu-latest, macos-latest, windows-latest] 18 | runs-on: ${{ matrix.os }} 19 | steps: 20 | - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 https://github.com/actions/checkout/releases/tag/v4.1.1 21 | - name: Use Node.js ${{ matrix.node-version }} 22 | uses: actions/setup-node@1a4442cacd436585916779262731d5b162bc6ec7 # v3.8.2 https://github.com/actions/setup-node/releases/tag/v3.8.2 23 | with: 24 | node-version: ${{ matrix.node-version }} 25 | - run: npm install 26 | - run: npm test 27 | trigger_canary: 28 | if: startsWith(github.ref, 'refs/tags/') # Only run when a new package is published (detects when a new tag is pushed) 29 | runs-on: ubuntu-latest 30 | steps: 31 | - name: trigger canary 32 | run: | 33 | curl -L -X POST \ 34 | -H "Accept: application/vnd.github+json" \ 35 | -H "Authorization: Bearer ${{ secrets.CANARY_DISPATCH_PAT }}" \ 36 | -H "X-GitHub-Api-Version: 2022-11-28" \ 37 | https://api.github.com/repos/holepunchto/canary-tests/dispatches \ 38 | -d '{"event_type":"triggered-by-${{ github.event.repository.name }}-${{ github.ref_name }}"}' 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package-lock.json 3 | sandbox.js 4 | sandbox 5 | .nyc_output 6 | coverage 7 | *.0x 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | sudo: true 3 | node_js: 4 | - 10 5 | - 12 6 | - 14 7 | os: 8 | - windows 9 | - linux 10 | - osx 11 | script: 12 | - npm run ci 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019 Mathias Buus, Paul Frazee, David Mark Clements and contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # hyperswarm 2 | 3 | ### [See the full API docs at docs.holepunch.to](https://docs.holepunch.to/building-blocks/hyperswarm) 4 | 5 | A high-level API for finding and connecting to peers who are interested in a "topic." 6 | 7 | ## Installation 8 | ``` 9 | npm install hyperswarm 10 | ``` 11 | 12 | ## Usage 13 | ```js 14 | const Hyperswarm = require('hyperswarm') 15 | 16 | const swarm1 = new Hyperswarm() 17 | const swarm2 = new Hyperswarm() 18 | 19 | swarm1.on('connection', (conn, info) => { 20 | // swarm1 will receive server connections 21 | conn.write('this is a server connection') 22 | conn.end() 23 | }) 24 | 25 | swarm2.on('connection', (conn, info) => { 26 | conn.on('data', data => console.log('client got message:', data.toString())) 27 | }) 28 | 29 | const topic = Buffer.alloc(32).fill('hello world') // A topic must be 32 bytes 30 | const discovery = swarm1.join(topic, { server: true, client: false }) 31 | await discovery.flushed() // Waits for the topic to be fully announced on the DHT 32 | 33 | swarm2.join(topic, { server: false, client: true }) 34 | await swarm2.flush() // Waits for the swarm to connect to pending peers. 35 | 36 | // After this point, both client and server should have connections 37 | ``` 38 | 39 | ## Hyperswarm API 40 | 41 | #### `const swarm = new Hyperswarm(opts = {})` 42 | Construct a new Hyperswarm instance. 43 | 44 | `opts` can include: 45 | * `keyPair`: A Noise keypair that will be used to listen/connect on the DHT. Defaults to a new key pair. 46 | * `seed`: A unique, 32-byte, random seed that can be used to deterministically generate the key pair. 47 | * `maxPeers`: The maximum number of peer connections to allow. 48 | * `firewall`: A sync function of the form `remotePublicKey => (true|false)`. If true, the connection will be rejected. Defaults to allowing all connections. 49 | * `dht`: A DHT instance. Defaults to a new instance. 50 | 51 | #### `swarm.connecting` 52 | Number that indicates connections in progress. 53 | 54 | #### `swarm.connections` 55 | A set of all active client/server connections. 56 | 57 | #### `swarm.peers` 58 | A Map containing all connected peers, of the form: `(Noise public key hex string) -> PeerInfo object` 59 | 60 | See the [`PeerInfo`](https://github.com/holepunchto/hyperswarm/blob/v3/README.md#peerinfo-api) API for more details. 61 | 62 | #### `swarm.dht` 63 | A [`hyperdht`](https://github.com/holepunchto/hyperdht) instance. Useful if you want lower-level control over Hyperswarm's networking. 64 | 65 | #### `swarm.on('connection', (socket, peerInfo) => {})` 66 | Emitted whenever the swarm connects to a new peer. 67 | 68 | `socket` is an end-to-end (Noise) encrypted Duplex stream 69 | 70 | `peerInfo` is a [`PeerInfo`](https://github.com/holepunchto/hyperswarm/blob/v3/README.md#peerinfo-api) instance 71 | 72 | #### `swarm.on('update', () => {})` 73 | Emitted when internal values are changed, useful for user interfaces. 74 | 75 | For example: emitted when `swarm.connecting` or `swarm.connections` changes. 76 | 77 | #### `const discovery = swarm.join(topic, opts = {})` 78 | Start discovering and connecting to peers sharing a common topic. As new peers are connected to, they will be emitted from the swarm as `connection` events. 79 | 80 | `topic` must be a 32-byte Buffer 81 | `opts` can include: 82 | * `server`: Accept server connections for this topic by announcing yourself to the DHT. Defaults to `true`. 83 | * `client`: Actively search for and connect to discovered servers. Defaults to `true`. 84 | 85 | Returns a [`PeerDiscovery`](https://github.com/holepunchto/hyperswarm/blob/v3/README.md#peerdiscovery-api) object. 86 | 87 | #### Clients and Servers 88 | In Hyperswarm, there are two ways for peers to join the swarm: client mode and server mode. If you've previously used Hyperswarm v2, these were called "lookup" and "announce", but we now think "client" and "server" are more descriptive. 89 | 90 | When you join a topic as a server, the swarm will start accepting incoming connections from clients (peers that have joined the same topic in client mode). Server mode will announce your keypair to the DHT, so that other peers can discover your server. When server connections are emitted, they are not associated with a specific topic -- the server only knows it received an incoming connection. 91 | 92 | When you join a topic as a client, the swarm will do a query to discover available servers, and will eagerly connect to them. As with server mode, these connections will be emitted as `connection` events, but in client mode they __will__ be associated with the topic (`info.topics` will be set in the `connection` event). 93 | 94 | #### `await swarm.leave(topic)` 95 | Stop discovering peers for the given topic. 96 | 97 | `topic` must be a 32-byte Buffer 98 | 99 | If a topic was previously joined in server mode, `leave` will stop announcing the topic on the DHT. If a topic was previously joined in client mode, `leave` will stop searching for servers announcing the topic. 100 | 101 | `leave` will __not__ close any existing connections. 102 | 103 | #### `swarm.joinPeer(noisePublicKey)` 104 | Establish a direct connection to a known peer. 105 | 106 | `noisePublicKey` must be a 32-byte Buffer 107 | 108 | As with the standard `join` method, `joinPeer` will ensure that peer connections are reestablished in the event of failures. 109 | 110 | #### `swarm.leavePeer(noisePublicKey)` 111 | Stop attempting direct connections to a known peer. 112 | 113 | `noisePublicKey` must be a 32-byte Buffer 114 | 115 | If a direct connection is already established, that connection will __not__ be destroyed by `leavePeer`. 116 | 117 | #### `const discovery = swarm.status(topic)` 118 | Get the [`PeerDiscovery`](https://github.com/holepunchto/hyperswarm/blob/v3/README.md#peerdiscovery-api) object associated with the topic, if it exists. 119 | 120 | #### `await swarm.listen()` 121 | Explicitly start listening for incoming connections. This will be called internally after the first `join`, so it rarely needs to be called manually. 122 | 123 | #### `await swarm.flush()` 124 | Wait for any pending DHT announces, and for the swarm to connect to any pending peers (peers that have been discovered, but are still in the queue awaiting processing). 125 | 126 | Once a `flush()` has completed, the swarm will have connected to every peer it can discover from the current set of topics it's managing. 127 | 128 | `flush()` is not topic-specific, so it will wait for every pending DHT operation and connection to be processed -- it's quite heavyweight, so it could take a while. In most cases, it's not necessary, as connections are emitted by `swarm.on('connection')` immediately after they're opened. 129 | 130 | ## PeerDiscovery API 131 | 132 | `swarm.join` returns a `PeerDiscovery` instance which allows you to both control discovery behavior, and respond to lifecycle changes during discovery. 133 | 134 | #### `await discovery.flushed()` 135 | Wait until the topic has been fully announced to the DHT. This method is only relevant in server mode. When `flushed()` has completed, the server will be available to the network. 136 | 137 | #### `await discovery.refresh({ client, server })` 138 | Update the `PeerDiscovery` configuration, optionally toggling client and server modes. This will also trigger an immediate re-announce of the topic, when the `PeerDiscovery` is in server mode. 139 | 140 | #### `await discovery.destroy()` 141 | Stop discovering peers for the given topic. 142 | 143 | If a topic was previously joined in server mode, `leave` will stop announcing the topic on the DHT. If a topic was previously joined in client mode, `leave` will stop searching for servers announcing the topic. 144 | 145 | ## PeerInfo API 146 | 147 | `swarm.on('connection', ...)` emits a `PeerInfo` instance whenever a new connection is established. 148 | 149 | There is a one-to-one relationship between connections and `PeerInfo` objects -- if a single peer announces multiple topics, those topics will be multiplexed over a single connection. 150 | 151 | #### `peerInfo.publicKey` 152 | The peer's Noise public key. 153 | 154 | #### `peerInfo.topics` 155 | An Array of topics that this Peer is associated with -- `topics` will only be updated when the Peer is in client mode. 156 | 157 | #### `peerInfo.prioritized` 158 | If true, the swarm will rapidly attempt to reconnect to this peer. 159 | 160 | #### `peerInfo.ban(banStatus = false)` 161 | Ban or unban the peer. Banning will prevent any future reconnection attempts, but it will __not__ close any existing connections. 162 | 163 | ## License 164 | MIT 165 | -------------------------------------------------------------------------------- /example.js: -------------------------------------------------------------------------------- 1 | const Swarm = require('.') 2 | 3 | start() 4 | 5 | async function start () { 6 | const swarm1 = new Swarm({ seed: Buffer.alloc(32).fill(4) }) 7 | const swarm2 = new Swarm({ seed: Buffer.alloc(32).fill(5) }) 8 | 9 | console.log('SWARM 1 KEYPAIR:', swarm1.keyPair) 10 | console.log('SWARM 2 KEYPAIR:', swarm2.keyPair) 11 | 12 | swarm1.on('connection', function (connection, info) { 13 | console.log('swarm 1 got a server connection:', connection.remotePublicKey, connection.publicKey, connection.handshakeHash) 14 | connection.on('error', err => console.error('1 CONN ERR:', err)) 15 | // Do something with `connection` 16 | // `info` is a PeerInfo object 17 | }) 18 | swarm2.on('connection', function (connection, info) { 19 | console.log('swarm 2 got a client connection:', connection.remotePublicKey, connection.publicKey, connection.handshakeHash) 20 | connection.on('error', err => console.error('2 CONN ERR:', err)) 21 | }) 22 | 23 | const key = Buffer.alloc(32).fill(7) 24 | 25 | const discovery1 = swarm1.join(key) 26 | await discovery1.flushed() // Wait for the first lookup/annnounce to complete. 27 | 28 | swarm2.join(key) 29 | 30 | // await swarm2.flush() 31 | // await discovery.destroy() // Stop lookup up and announcing this topic. 32 | } 33 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const { EventEmitter } = require('events') 2 | const DHT = require('hyperdht') 3 | const spq = require('shuffled-priority-queue') 4 | const b4a = require('b4a') 5 | const unslab = require('unslab') 6 | 7 | const PeerInfo = require('./lib/peer-info') 8 | const RetryTimer = require('./lib/retry-timer') 9 | const ConnectionSet = require('./lib/connection-set') 10 | const PeerDiscovery = require('./lib/peer-discovery') 11 | 12 | const MAX_PEERS = 64 13 | const MAX_PARALLEL = 3 14 | const MAX_CLIENT_CONNECTIONS = Infinity // TODO: Change 15 | const MAX_SERVER_CONNECTIONS = Infinity 16 | 17 | const ERR_MISSING_TOPIC = 'Topic is required and must be a 32-byte buffer' 18 | const ERR_DESTROYED = 'Swarm has been destroyed' 19 | const ERR_DUPLICATE = 'Duplicate connection' 20 | 21 | module.exports = class Hyperswarm extends EventEmitter { 22 | constructor (opts = {}) { 23 | super() 24 | const { 25 | seed, 26 | relayThrough, 27 | keyPair = DHT.keyPair(seed), 28 | maxPeers = MAX_PEERS, 29 | maxClientConnections = MAX_CLIENT_CONNECTIONS, 30 | maxServerConnections = MAX_SERVER_CONNECTIONS, 31 | maxParallel = MAX_PARALLEL, 32 | firewall = allowAll 33 | } = opts 34 | this.keyPair = keyPair 35 | 36 | this.dht = opts.dht || new DHT({ 37 | bootstrap: opts.bootstrap, 38 | nodes: opts.nodes, 39 | port: opts.port 40 | }) 41 | this.server = this.dht.createServer({ 42 | firewall: this._handleFirewall.bind(this), 43 | relayThrough: this._maybeRelayConnection.bind(this) 44 | }, this._handleServerConnection.bind(this)) 45 | 46 | this.destroyed = false 47 | this.suspended = false 48 | this.maxPeers = maxPeers 49 | this.maxClientConnections = maxClientConnections 50 | this.maxServerConnections = maxServerConnections 51 | this.maxParallel = maxParallel 52 | this.relayThrough = relayThrough || null 53 | 54 | this.connecting = 0 55 | this.connections = new Set() 56 | this.peers = new Map() 57 | this.explicitPeers = new Set() 58 | this.listening = null 59 | this.stats = { 60 | updates: 0, 61 | connects: { 62 | client: { 63 | opened: 0, 64 | closed: 0, 65 | attempted: 0 66 | }, 67 | server: { 68 | // Note: there is no notion of 'attempts' for server connections 69 | opened: 0, 70 | closed: 0 71 | } 72 | } 73 | } 74 | 75 | this._discovery = new Map() 76 | this._timer = new RetryTimer(this._requeue.bind(this), { 77 | backoffs: opts.backoffs, 78 | jitter: opts.jitter 79 | }) 80 | this._queue = spq() 81 | 82 | this._allConnections = new ConnectionSet() 83 | this._pendingFlushes = [] 84 | this._flushTick = 0 85 | 86 | this._drainingQueue = false 87 | this._clientConnections = 0 88 | this._serverConnections = 0 89 | this._firewall = firewall 90 | 91 | this.dht.on('network-change', this._handleNetworkChange.bind(this)) 92 | this.on('update', this._handleUpdate) 93 | } 94 | 95 | _maybeRelayConnection (force) { 96 | if (!this.relayThrough) return null 97 | return this.relayThrough(force) 98 | } 99 | 100 | _enqueue (peerInfo) { 101 | if (peerInfo.queued) return 102 | peerInfo.queued = true 103 | peerInfo._flushTick = this._flushTick 104 | this._queue.add(peerInfo) 105 | 106 | this._attemptClientConnections() 107 | } 108 | 109 | _requeue (batch) { 110 | if (this.suspended) return 111 | for (const peerInfo of batch) { 112 | peerInfo.waiting = false 113 | 114 | if ((peerInfo._updatePriority() === false) || this._allConnections.has(peerInfo.publicKey) || peerInfo.queued) continue 115 | peerInfo.queued = true 116 | peerInfo._flushTick = this._flushTick 117 | this._queue.add(peerInfo) 118 | } 119 | 120 | this._attemptClientConnections() 121 | } 122 | 123 | _flushMaybe (peerInfo) { 124 | for (let i = 0; i < this._pendingFlushes.length; i++) { 125 | const flush = this._pendingFlushes[i] 126 | if (peerInfo._flushTick > flush.tick) continue 127 | if (--flush.missing > 0) continue 128 | flush.onflush(true) 129 | this._pendingFlushes.splice(i--, 1) 130 | } 131 | } 132 | 133 | _flushAllMaybe () { 134 | if (this.connecting > 0 || (this._allConnections.size < this.maxPeers && this._clientConnections < this.maxClientConnections)) { 135 | return false 136 | } 137 | 138 | while (this._pendingFlushes.length) { 139 | const flush = this._pendingFlushes.pop() 140 | flush.onflush(true) 141 | } 142 | 143 | return true 144 | } 145 | 146 | _shouldConnectExplicit () { 147 | return !this.destroyed && 148 | !this.suspended && 149 | this.connecting < this.maxParallel 150 | } 151 | 152 | _shouldConnect () { 153 | return !this.destroyed && 154 | !this.suspended && 155 | this.connecting < this.maxParallel && 156 | this._allConnections.size < this.maxPeers && 157 | this._clientConnections < this.maxClientConnections 158 | } 159 | 160 | _shouldRequeue (peerInfo) { 161 | if (this.suspended) return false 162 | if (peerInfo.explicit) return true 163 | for (const topic of peerInfo.topics) { 164 | if (this._discovery.has(b4a.toString(topic, 'hex')) && !this.destroyed) { 165 | return true 166 | } 167 | } 168 | return false 169 | } 170 | 171 | _connect (peerInfo, queued) { 172 | if (peerInfo.banned || this._allConnections.has(peerInfo.publicKey)) { 173 | if (queued) this._flushMaybe(peerInfo) 174 | return 175 | } 176 | 177 | // TODO: Support async firewalling at some point. 178 | if (this._handleFirewall(peerInfo.publicKey, null)) { 179 | peerInfo.ban(true) 180 | if (queued) this._flushMaybe(peerInfo) 181 | return 182 | } 183 | 184 | const relayThrough = this._maybeRelayConnection(peerInfo.forceRelaying) 185 | const conn = this.dht.connect(peerInfo.publicKey, { 186 | relayAddresses: peerInfo.relayAddresses, 187 | keyPair: this.keyPair, 188 | relayThrough 189 | }) 190 | this._allConnections.add(conn) 191 | 192 | this.stats.connects.client.attempted++ 193 | 194 | this.connecting++ 195 | this._clientConnections++ 196 | let opened = false 197 | 198 | const onerror = (err) => { 199 | if (this.relayThrough && shouldForceRelaying(err.code)) { 200 | peerInfo.forceRelaying = true 201 | // Reset the attempts in order to fast connect to relay 202 | peerInfo.attempts = 0 203 | } 204 | } 205 | 206 | // Removed once a connection is opened 207 | conn.on('error', onerror) 208 | 209 | conn.on('open', () => { 210 | opened = true 211 | this.stats.connects.client.opened++ 212 | 213 | this._connectDone() 214 | this.connections.add(conn) 215 | conn.removeListener('error', onerror) 216 | peerInfo._connected() 217 | peerInfo.client = true 218 | this.emit('connection', conn, peerInfo) 219 | if (queued) this._flushMaybe(peerInfo) 220 | 221 | this.emit('update') 222 | }) 223 | conn.on('close', () => { 224 | if (!opened) this._connectDone() 225 | this.stats.connects.client.closed++ 226 | 227 | this.connections.delete(conn) 228 | this._allConnections.delete(conn) 229 | this._clientConnections-- 230 | peerInfo._disconnected() 231 | 232 | peerInfo.waiting = this._shouldRequeue(peerInfo) && this._timer.add(peerInfo) 233 | this._maybeDeletePeer(peerInfo) 234 | 235 | if (!opened && queued) this._flushMaybe(peerInfo) 236 | 237 | this._attemptClientConnections() 238 | 239 | this.emit('update') 240 | }) 241 | 242 | this.emit('update') 243 | } 244 | 245 | _connectDone () { 246 | this.connecting-- 247 | 248 | if (this.connecting < this.maxParallel) this._attemptClientConnections() 249 | if (this.connecting === 0) this._flushAllMaybe() 250 | } 251 | 252 | // Called when the PeerQueue indicates a connection should be attempted. 253 | _attemptClientConnections () { 254 | // Guard against re-entries - unsure if it still needed but doesn't hurt 255 | if (this._drainingQueue) return 256 | this._drainingQueue = true 257 | 258 | for (const peerInfo of this.explicitPeers) { 259 | if (!this._shouldConnectExplicit()) break 260 | if (peerInfo.attempts >= 5 || (Date.now() - peerInfo.disconnectedTime) < peerInfo.attempts * 1000) continue 261 | this._connect(peerInfo, false) 262 | } 263 | 264 | while (this._queue.length && this._shouldConnect()) { 265 | const peerInfo = this._queue.shift() 266 | peerInfo.queued = false 267 | this._connect(peerInfo, true) 268 | } 269 | this._drainingQueue = false 270 | if (this.connecting === 0) this._flushAllMaybe() 271 | } 272 | 273 | _handleFirewall (remotePublicKey, payload) { 274 | if (this.suspended) return true 275 | if (b4a.equals(remotePublicKey, this.keyPair.publicKey)) return true 276 | 277 | const peerInfo = this.peers.get(b4a.toString(remotePublicKey, 'hex')) 278 | if (peerInfo && peerInfo.banned) return true 279 | 280 | return this._firewall(remotePublicKey, payload) 281 | } 282 | 283 | _handleServerConnectionSwap (existing, conn) { 284 | let closed = false 285 | 286 | existing.on('close', () => { 287 | if (closed) return 288 | 289 | conn.removeListener('error', noop) 290 | conn.removeListener('close', onclose) 291 | 292 | this._handleServerConnection(conn) 293 | }) 294 | 295 | conn.on('error', noop) 296 | conn.on('close', onclose) 297 | 298 | function onclose () { 299 | closed = true 300 | } 301 | } 302 | 303 | // Called when the DHT receives a new server connection. 304 | _handleServerConnection (conn) { 305 | if (this.destroyed || this.suspended) { 306 | // TODO: Investigate why a final server connection can be received after close 307 | conn.on('error', noop) 308 | return conn.destroy(ERR_DESTROYED) 309 | } 310 | 311 | const existing = this._allConnections.get(conn.remotePublicKey) 312 | 313 | if (existing) { 314 | // If both connections are from the same peer, 315 | // - pick the new one if the existing stream is already established (has sent and received bytes), 316 | // because the other client must have lost that connection and be reconnecting 317 | // - otherwise, pick the one thats expected to initiate in a tie break 318 | const existingIsOutdated = existing.rawBytesRead > 0 && existing.rawBytesWritten > 0 319 | const expectedInitiator = b4a.compare(conn.publicKey, conn.remotePublicKey) > 0 320 | const keepNew = existingIsOutdated || (expectedInitiator === conn.isInitiator) 321 | 322 | if (keepNew === false) { 323 | existing.sendKeepAlive() 324 | conn.on('error', noop) 325 | conn.destroy(new Error(ERR_DUPLICATE)) 326 | return 327 | } 328 | 329 | existing.on('error', noop) 330 | existing.destroy(new Error(ERR_DUPLICATE)) 331 | this._handleServerConnectionSwap(existing, conn) 332 | return 333 | } 334 | 335 | // When reaching here, the connection will always be 'opened' next tick 336 | this.stats.connects.server.opened++ 337 | 338 | const peerInfo = this._upsertPeer(conn.remotePublicKey, null) 339 | 340 | this.connections.add(conn) 341 | this._allConnections.add(conn) 342 | this._serverConnections++ 343 | 344 | conn.on('close', () => { 345 | this.connections.delete(conn) 346 | this._allConnections.delete(conn) 347 | this._serverConnections-- 348 | this.stats.connects.server.closed++ 349 | 350 | this._maybeDeletePeer(peerInfo) 351 | 352 | this._attemptClientConnections() 353 | 354 | this.emit('update') 355 | }) 356 | peerInfo.client = false 357 | this.emit('connection', conn, peerInfo) 358 | 359 | this.emit('update') 360 | } 361 | 362 | _upsertPeer (publicKey, relayAddresses) { 363 | if (b4a.equals(publicKey, this.keyPair.publicKey)) return null 364 | const keyString = b4a.toString(publicKey, 'hex') 365 | let peerInfo = this.peers.get(keyString) 366 | 367 | if (peerInfo) { 368 | peerInfo.relayAddresses = relayAddresses // new is always better 369 | return peerInfo 370 | } 371 | 372 | peerInfo = new PeerInfo({ 373 | publicKey, 374 | relayAddresses 375 | }) 376 | 377 | this.peers.set(keyString, peerInfo) 378 | return peerInfo 379 | } 380 | 381 | _handleUpdate () { 382 | this.stats.updates++ 383 | } 384 | 385 | _maybeDeletePeer (peerInfo) { 386 | if (!peerInfo.shouldGC()) return 387 | 388 | const hasActiveConn = this._allConnections.has(peerInfo.publicKey) 389 | if (hasActiveConn) return 390 | 391 | const keyString = b4a.toString(peerInfo.publicKey, 'hex') 392 | this.peers.delete(keyString) 393 | } 394 | 395 | /* 396 | * Called when a peer is actively discovered during a lookup. 397 | * 398 | * Three conditions: 399 | * 1. Not a known peer -- insert into queue 400 | * 2. A known peer with normal priority -- do nothing 401 | * 3. A known peer with low priority -- bump priority, because it's been rediscovered 402 | */ 403 | _handlePeer (peer, topic) { 404 | const peerInfo = this._upsertPeer(peer.publicKey, peer.relayAddresses) 405 | if (peerInfo) peerInfo._topic(topic) 406 | if (!peerInfo || this._allConnections.has(peer.publicKey)) return 407 | if (!peerInfo.prioritized || peerInfo.server) peerInfo._reset() 408 | if (peerInfo._updatePriority()) { 409 | this._enqueue(peerInfo) 410 | } 411 | } 412 | 413 | async _handleNetworkChange () { 414 | // prioritize figuring out if existing connections are dead 415 | for (const conn of this._allConnections) { 416 | conn.sendKeepAlive() 417 | } 418 | 419 | const refreshes = [] 420 | 421 | for (const discovery of this._discovery.values()) { 422 | refreshes.push(discovery.refresh()) 423 | } 424 | 425 | await Promise.allSettled(refreshes) 426 | } 427 | 428 | status (key) { 429 | return this._discovery.get(b4a.toString(key, 'hex')) || null 430 | } 431 | 432 | listen () { 433 | if (!this.listening) this.listening = this.server.listen(this.keyPair) 434 | return this.listening 435 | } 436 | 437 | // Object that exposes a cancellation method (destroy) 438 | // TODO: When you rejoin, it should reannounce + bump lookup priority 439 | join (topic, opts = {}) { 440 | if (!topic) throw new Error(ERR_MISSING_TOPIC) 441 | topic = unslab(topic) 442 | 443 | const topicString = b4a.toString(topic, 'hex') 444 | 445 | let discovery = this._discovery.get(topicString) 446 | 447 | if (discovery && !discovery.destroyed) { 448 | return discovery.session(opts) 449 | } 450 | 451 | discovery = new PeerDiscovery(this, topic, { 452 | limit: opts.limit, 453 | wait: discovery ? discovery.destroy() : null, 454 | suspended: this.suspended, 455 | onpeer: peer => this._handlePeer(peer, topic) 456 | }) 457 | this._discovery.set(topicString, discovery) 458 | return discovery.session(opts) 459 | } 460 | 461 | // Returns a promise 462 | async leave (topic) { 463 | if (!topic) throw new Error(ERR_MISSING_TOPIC) 464 | const topicString = b4a.toString(topic, 'hex') 465 | if (!this._discovery.has(topicString)) return Promise.resolve() 466 | 467 | const discovery = this._discovery.get(topicString) 468 | 469 | try { 470 | await discovery.destroy() 471 | } catch { 472 | // ignore, prop network 473 | } 474 | 475 | if (this._discovery.get(topicString) === discovery) { 476 | this._discovery.delete(topicString) 477 | } 478 | } 479 | 480 | joinPeer (publicKey) { 481 | const peerInfo = this._upsertPeer(publicKey, null) 482 | if (!peerInfo) return 483 | if (!this.explicitPeers.has(peerInfo)) { 484 | peerInfo.explicit = true 485 | this.explicitPeers.add(peerInfo) 486 | } 487 | if (this._allConnections.has(publicKey)) return 488 | if (peerInfo._updatePriority()) { 489 | this._enqueue(peerInfo) 490 | } 491 | } 492 | 493 | leavePeer (publicKey) { 494 | const keyString = b4a.toString(publicKey, 'hex') 495 | if (!this.peers.has(keyString)) return 496 | 497 | const peerInfo = this.peers.get(keyString) 498 | peerInfo.explicit = false 499 | this.explicitPeers.delete(peerInfo) 500 | this._maybeDeletePeer(peerInfo) 501 | } 502 | 503 | // Returns a promise 504 | async flush () { 505 | const allFlushed = [...this._discovery.values()].map(v => v.flushed()) 506 | await Promise.all(allFlushed) 507 | if (this._flushAllMaybe()) return true 508 | const pendingSize = this._allConnections.size - this.connections.size 509 | if (!this._queue.length && !pendingSize) return true 510 | return new Promise((resolve) => { 511 | this._pendingFlushes.push({ 512 | onflush: resolve, 513 | missing: this._queue.length + pendingSize, 514 | tick: this._flushTick++ 515 | }) 516 | }) 517 | } 518 | 519 | async clear () { 520 | const cleared = Promise.allSettled([...this._discovery.values()].map(d => d.destroy())) 521 | this._discovery.clear() 522 | return cleared 523 | } 524 | 525 | async destroy ({ force } = {}) { 526 | if (this.destroyed && !force) return 527 | this.destroyed = true 528 | 529 | this._timer.destroy() 530 | 531 | if (!force) await this.clear() 532 | 533 | await this.server.close() 534 | 535 | while (this._pendingFlushes.length) { 536 | const flush = this._pendingFlushes.pop() 537 | flush.onflush(false) 538 | } 539 | 540 | await this.dht.destroy({ force }) 541 | } 542 | 543 | async suspend ({ log = noop } = {}) { 544 | if (this.suspended) return 545 | 546 | const promises = [] 547 | 548 | promises.push(this.server.suspend({ log })) 549 | 550 | for (const discovery of this._discovery.values()) { 551 | promises.push(discovery.suspend({ log })) 552 | } 553 | 554 | for (const connection of this._allConnections) { 555 | connection.destroy() 556 | } 557 | 558 | this.suspended = true 559 | 560 | log('Suspending server and discovery... (' + promises.length + ')') 561 | await Promise.allSettled(promises) 562 | log('Done, suspending the dht...') 563 | await this.dht.suspend({ log }) 564 | log('Done, swarm fully suspended') 565 | } 566 | 567 | async resume ({ log = noop } = {}) { 568 | if (!this.suspended) return 569 | 570 | log('Resuming the dht') 571 | await this.dht.resume() 572 | log('Done, resuming the server') 573 | await this.server.resume() 574 | log('Done, all discovery') 575 | 576 | for (const discovery of this._discovery.values()) { 577 | discovery.resume() 578 | } 579 | 580 | this._attemptClientConnections() 581 | this.suspended = false 582 | } 583 | 584 | topics () { 585 | return this._discovery.values() 586 | } 587 | } 588 | 589 | function noop () { } 590 | 591 | function allowAll () { 592 | return false 593 | } 594 | 595 | function shouldForceRelaying (code) { 596 | return (code === 'HOLEPUNCH_ABORTED') || 597 | (code === 'HOLEPUNCH_DOUBLE_RANDOMIZED_NATS') || 598 | (code === 'REMOTE_NOT_HOLEPUNCHABLE') 599 | } 600 | -------------------------------------------------------------------------------- /lib/bulk-timer.js: -------------------------------------------------------------------------------- 1 | module.exports = class BulkTimer { 2 | constructor (time, fn) { 3 | this._time = time 4 | this._fn = fn 5 | this._interval = null 6 | this._next = [] 7 | this._pending = [] 8 | this._destroyed = false 9 | } 10 | 11 | destroy () { 12 | if (this._destroyed) return 13 | this._destroyed = true 14 | clearInterval(this._interval) 15 | this._interval = null 16 | } 17 | 18 | _ontick () { 19 | if (!this._next.length && !this._pending.length) return 20 | if (this._next.length) this._fn(this._next) 21 | this._next = this._pending 22 | this._pending = [] 23 | } 24 | 25 | add (info) { 26 | if (this._destroyed) return 27 | if (!this._interval) { 28 | this._interval = setInterval(this._ontick.bind(this), Math.floor(this._time * 0.66)) 29 | } 30 | 31 | this._pending.push(info) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /lib/connection-set.js: -------------------------------------------------------------------------------- 1 | const b4a = require('b4a') 2 | 3 | module.exports = class ConnectionSet { 4 | constructor () { 5 | this._byPublicKey = new Map() 6 | } 7 | 8 | [Symbol.iterator] () { 9 | return this._byPublicKey.values() 10 | } 11 | 12 | get size () { 13 | return this._byPublicKey.size 14 | } 15 | 16 | has (publicKey) { 17 | return this._byPublicKey.has(b4a.toString(publicKey, 'hex')) 18 | } 19 | 20 | get (publicKey) { 21 | return this._byPublicKey.get(b4a.toString(publicKey, 'hex')) 22 | } 23 | 24 | add (connection) { 25 | this._byPublicKey.set(b4a.toString(connection.remotePublicKey, 'hex'), connection) 26 | } 27 | 28 | delete (connection) { 29 | const keyString = b4a.toString(connection.remotePublicKey, 'hex') 30 | const existing = this._byPublicKey.get(keyString) 31 | if (existing !== connection) return 32 | this._byPublicKey.delete(keyString) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /lib/peer-discovery.js: -------------------------------------------------------------------------------- 1 | const safetyCatch = require('safety-catch') 2 | const b4a = require('b4a') 3 | 4 | const REFRESH_INTERVAL = 1000 * 60 * 10 // 10 min 5 | const RANDOM_JITTER = 1000 * 60 * 2 // 2 min 6 | const DELAY_GRACE_PERIOD = 1000 * 30 // 30s 7 | 8 | module.exports = class PeerDiscovery { 9 | constructor (swarm, topic, { limit = Infinity, wait = null, suspended = false, onpeer = noop, onerror = safetyCatch }) { 10 | this.limit = limit 11 | this.swarm = swarm 12 | this.topic = topic 13 | this.isClient = false 14 | this.isServer = false 15 | this.destroyed = false 16 | this.destroying = null 17 | this.suspended = suspended 18 | 19 | this._sessions = [] 20 | this._clientSessions = 0 21 | this._serverSessions = 0 22 | 23 | this._onpeer = onpeer 24 | this._onerror = onerror 25 | 26 | this._activeQuery = null 27 | this._timer = null 28 | this._currentRefresh = null 29 | this._closestNodes = null 30 | this._firstAnnounce = true 31 | this._needsUnannounce = false 32 | this._refreshes = 0 33 | this._wait = wait 34 | } 35 | 36 | session ({ server = true, client = true, limit = Infinity, onerror = safetyCatch }) { 37 | if (this.destroyed) throw new Error('PeerDiscovery is destroyed') 38 | const session = new PeerDiscoverySession(this) 39 | session.refresh({ server, client, limit }).catch(onerror) 40 | this._sessions.push(session) 41 | return session 42 | } 43 | 44 | _refreshLater (eager) { 45 | const jitter = Math.round(Math.random() * RANDOM_JITTER) 46 | const delay = !eager 47 | ? REFRESH_INTERVAL + jitter 48 | : jitter 49 | 50 | if (this._timer) clearTimeout(this._timer) 51 | 52 | const startTime = Date.now() 53 | this._timer = setTimeout(() => { 54 | // If your laptop went to sleep, and is coming back online... 55 | const overdue = Date.now() - startTime > delay + DELAY_GRACE_PERIOD 56 | if (overdue) this._refreshLater(true) 57 | else this.refresh().catch(this._onerror) 58 | }, delay) 59 | } 60 | 61 | _isActive () { 62 | return !this.destroyed && !this.suspended 63 | } 64 | 65 | // TODO: Allow announce to be an argument to this 66 | // TODO: Maybe announce should be a setter? 67 | async _refresh () { 68 | if (this.suspended) return 69 | const clock = ++this._refreshes 70 | 71 | if (this._wait) { 72 | await this._wait 73 | this._wait = null 74 | if (clock !== this._refreshes || !this._isActive()) return 75 | } 76 | 77 | const clear = this.isServer && this._firstAnnounce 78 | if (clear) this._firstAnnounce = false 79 | 80 | const opts = { 81 | clear, 82 | closestNodes: this._closestNodes 83 | } 84 | 85 | if (this.isServer) { 86 | await this.swarm.listen() 87 | // if a parallel refresh is happening, yield to the new one 88 | if (clock !== this._refreshes || !this._isActive()) return 89 | this._needsUnannounce = true 90 | } 91 | 92 | const announcing = this.isServer 93 | const query = this._activeQuery = announcing 94 | ? this.swarm.dht.announce(this.topic, this.swarm.keyPair, this.swarm.server.relayAddresses, opts) 95 | : this._needsUnannounce 96 | ? this.swarm.dht.lookupAndUnannounce(this.topic, this.swarm.keyPair, opts) 97 | : this.swarm.dht.lookup(this.topic, opts) 98 | 99 | try { 100 | for await (const data of this._activeQuery) { 101 | if (!this.isClient || !this._isActive()) continue 102 | for (const peer of data.peers) { 103 | if (this.limit === 0) return 104 | this.limit-- 105 | this._onpeer(peer, data) 106 | } 107 | } 108 | } catch (err) { 109 | if (this._isActive()) throw err 110 | } finally { 111 | if (this._activeQuery === query) { 112 | this._activeQuery = null 113 | if (!this.destroyed && !this.suspended) this._refreshLater(false) 114 | } 115 | } 116 | 117 | // This is set at the very end, when the query completes successfully. 118 | this._closestNodes = query.closestNodes 119 | 120 | if (clock !== this._refreshes) return 121 | 122 | // In this is the latest query, unannounce has been fulfilled as well 123 | if (!announcing) this._needsUnannounce = false 124 | } 125 | 126 | async refresh () { 127 | if (this.destroyed) throw new Error('PeerDiscovery is destroyed') 128 | 129 | const server = this._serverSessions > 0 130 | const client = this._clientSessions > 0 131 | 132 | if (this.suspended) return 133 | 134 | if (server === this.isServer && client === this.isClient) { 135 | if (this._currentRefresh) return this._currentRefresh 136 | this._currentRefresh = this._refresh() 137 | } else { 138 | if (this._activeQuery) this._activeQuery.destroy() 139 | this.isServer = server 140 | this.isClient = client 141 | this._currentRefresh = this._refresh() 142 | } 143 | 144 | const refresh = this._currentRefresh 145 | try { 146 | await refresh 147 | } catch { 148 | return false 149 | } finally { 150 | if (refresh === this._currentRefresh) { 151 | this._currentRefresh = null 152 | } 153 | } 154 | 155 | return true 156 | } 157 | 158 | async flushed () { 159 | if (this.swarm.listening) await this.swarm.listening 160 | 161 | try { 162 | await this._currentRefresh 163 | return true 164 | } catch { 165 | return false 166 | } 167 | } 168 | 169 | async _destroyMaybe () { 170 | if (this.destroyed) return 171 | 172 | try { 173 | if (this._sessions.length === 0) await this.swarm.leave(this.topic) 174 | else if (this._serverSessions === 0 && this._needsUnannounce) await this.refresh() 175 | } catch (err) { // ignore network failures here, as we are tearing down 176 | safetyCatch(err) 177 | } 178 | } 179 | 180 | destroy () { 181 | if (this.destroying) return this.destroying 182 | this.destroying = this._destroy() 183 | return this.destroying 184 | } 185 | 186 | async _abort (log) { 187 | const id = log === noop ? '' : b4a.toString(this.topic, 'hex') 188 | 189 | log('Aborting discovery', id) 190 | if (this._wait) await this._wait 191 | log('Aborting discovery (post wait)', id) 192 | 193 | if (this._activeQuery) { 194 | this._activeQuery.destroy() 195 | this._activeQuery = null 196 | } 197 | if (this._timer) { 198 | clearTimeout(this._timer) 199 | this._timer = null 200 | } 201 | 202 | let nodes = this._closestNodes 203 | 204 | if (this._currentRefresh) { 205 | try { 206 | await this._currentRefresh 207 | } catch { 208 | // If the destroy causes the refresh to fail, suppress it. 209 | } 210 | } 211 | 212 | log('Aborting discovery (post refresh)', id) 213 | if (this._isActive()) return 214 | 215 | if (!nodes) nodes = this._closestNodes 216 | else if (this._closestNodes !== nodes) { 217 | const len = nodes.length 218 | for (const newer of this._closestNodes) { 219 | if (newer.id && !hasNode(nodes, len, newer)) nodes.push(newer) 220 | } 221 | } 222 | 223 | if (this._needsUnannounce) { 224 | log('Unannouncing discovery', id) 225 | if (nodes && nodes.length) await this.swarm.dht.unannounce(this.topic, this.swarm.keyPair, { closestNodes: nodes, onlyClosestNodes: true, force: true }) 226 | this._needsUnannounce = false 227 | log('Unannouncing discovery (done)', id) 228 | } 229 | } 230 | 231 | _destroy () { 232 | if (this.destroyed) return 233 | this.destroyed = true 234 | return this._abort(noop) 235 | } 236 | 237 | async suspend ({ log = noop } = {}) { 238 | if (this.suspended) return 239 | this.suspended = true 240 | try { 241 | await this._abort(log) 242 | } catch { 243 | // ignore 244 | } 245 | } 246 | 247 | resume () { 248 | if (!this.suspended) return 249 | this.suspended = false 250 | this.refresh().catch(noop) 251 | } 252 | } 253 | 254 | class PeerDiscoverySession { 255 | constructor (discovery) { 256 | this.discovery = discovery 257 | this.isClient = false 258 | this.isServer = false 259 | this.destroyed = false 260 | } 261 | 262 | get swarm () { 263 | return this.discovery.swarm 264 | } 265 | 266 | get topic () { 267 | return this.discovery.topic 268 | } 269 | 270 | async refresh ({ client = this.isClient, server = this.isServer, limit = Infinity } = {}) { 271 | if (this.destroyed) throw new Error('PeerDiscovery is destroyed') 272 | if (!client && !server) throw new Error('Cannot refresh with neither client nor server option') 273 | 274 | if (client !== this.isClient) { 275 | this.isClient = client 276 | this.discovery._clientSessions += client ? 1 : -1 277 | } 278 | 279 | if (server !== this.isServer) { 280 | this.isServer = server 281 | this.discovery._serverSessions += server ? 1 : -1 282 | } 283 | 284 | this.discovery.limit = limit 285 | 286 | return this.discovery.refresh() 287 | } 288 | 289 | async flushed () { 290 | return this.discovery.flushed() 291 | } 292 | 293 | async destroy () { 294 | if (this.destroyed) return 295 | this.destroyed = true 296 | 297 | if (this.isClient) this.discovery._clientSessions-- 298 | if (this.isServer) this.discovery._serverSessions-- 299 | 300 | const index = this.discovery._sessions.indexOf(this) 301 | const head = this.discovery._sessions.pop() 302 | 303 | if (head !== this) this.discovery._sessions[index] = head 304 | 305 | return this.discovery._destroyMaybe() 306 | } 307 | } 308 | 309 | function hasNode (nodes, len, node) { 310 | for (let i = 0; i < len; i++) { 311 | const existing = nodes[i] 312 | if (existing.id && b4a.equals(existing.id, node.id)) return true 313 | } 314 | 315 | return false 316 | } 317 | 318 | function noop () {} 319 | -------------------------------------------------------------------------------- /lib/peer-info.js: -------------------------------------------------------------------------------- 1 | const { EventEmitter } = require('events') 2 | const b4a = require('b4a') 3 | const unslab = require('unslab') 4 | 5 | const MIN_CONNECTION_TIME = 15000 6 | 7 | const VERY_LOW_PRIORITY = 0 8 | const LOW_PRIORITY = 1 9 | const NORMAL_PRIORITY = 2 10 | const HIGH_PRIORITY = 3 11 | const VERY_HIGH_PRIORITY = 4 12 | 13 | module.exports = class PeerInfo extends EventEmitter { 14 | constructor ({ publicKey, relayAddresses }) { 15 | super() 16 | 17 | this.publicKey = unslab(publicKey) 18 | this.relayAddresses = relayAddresses 19 | 20 | this.reconnecting = true 21 | this.proven = false 22 | this.connectedTime = -1 23 | this.disconnectedTime = 0 24 | this.banned = false 25 | this.tried = false 26 | this.explicit = false 27 | this.waiting = false 28 | this.forceRelaying = false 29 | 30 | // Set by the Swarm 31 | this.queued = false 32 | this.client = false 33 | this.topics = [] // TODO: remove on next major (check with mafintosh for context) 34 | 35 | this.attempts = 0 36 | this.priority = NORMAL_PRIORITY 37 | 38 | // Used by shuffled-priority-queue 39 | this._index = 0 40 | 41 | // Used for flush management 42 | this._flushTick = 0 43 | 44 | // Used for topic multiplexing 45 | this._seenTopics = new Set() 46 | } 47 | 48 | get server () { 49 | return !this.client 50 | } 51 | 52 | get prioritized () { 53 | return this.priority >= NORMAL_PRIORITY 54 | } 55 | 56 | _getPriority () { 57 | const peerIsStale = this.tried && !this.proven 58 | if (peerIsStale || this.attempts > 3) return VERY_LOW_PRIORITY 59 | if (this.attempts === 3) return LOW_PRIORITY 60 | if (this.attempts === 2) return HIGH_PRIORITY 61 | if (this.attempts === 1) return VERY_HIGH_PRIORITY 62 | return NORMAL_PRIORITY 63 | } 64 | 65 | _connected () { 66 | this.proven = true 67 | this.connectedTime = Date.now() 68 | } 69 | 70 | _disconnected () { 71 | this.disconnectedTime = Date.now() 72 | if (this.connectedTime > -1) { 73 | if ((this.disconnectedTime - this.connectedTime) >= MIN_CONNECTION_TIME) this.attempts = 0 // fast retry 74 | this.connectedTime = -1 75 | } 76 | this.attempts++ 77 | } 78 | 79 | _deprioritize () { 80 | this.attempts = 3 81 | } 82 | 83 | _reset () { 84 | this.client = false 85 | this.proven = false 86 | this.tried = false 87 | this.attempts = 0 88 | } 89 | 90 | _updatePriority () { 91 | if (this.explicit && this.attempts > 3) this._deprioritize() 92 | if (this.banned || this.queued || this.attempts > 3) return false 93 | this.priority = this._getPriority() 94 | return true 95 | } 96 | 97 | _topic (topic) { 98 | const topicString = b4a.toString(topic, 'hex') 99 | if (this._seenTopics.has(topicString)) return 100 | this._seenTopics.add(topicString) 101 | this.topics.push(topic) 102 | this.emit('topic', topic) 103 | } 104 | 105 | reconnect (val) { 106 | this.reconnecting = !!val 107 | } 108 | 109 | ban (val) { 110 | this.banned = !!val 111 | } 112 | 113 | shouldGC () { 114 | return !(this.banned || this.queued || this.explicit || this.waiting) 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /lib/retry-timer.js: -------------------------------------------------------------------------------- 1 | const BulkTimer = require('./bulk-timer') 2 | 3 | const BACKOFF_JITTER = 500 4 | const BACKOFF_S = 1000 + Math.round(BACKOFF_JITTER * Math.random()) 5 | const BACKOFF_M = 5000 + Math.round(2 * BACKOFF_JITTER * Math.random()) 6 | const BACKOFF_L = 15000 + Math.round(4 * BACKOFF_JITTER * Math.random()) 7 | const BACKOFF_X = 1000 * 60 * 10 + Math.round(240 * BACKOFF_JITTER * Math.random()) 8 | 9 | module.exports = class RetryTimer { 10 | constructor (push, { backoffs = [BACKOFF_S, BACKOFF_M, BACKOFF_L, BACKOFF_X], jitter = BACKOFF_JITTER } = {}) { 11 | this.jitter = jitter 12 | this.backoffs = backoffs 13 | 14 | this._sTimer = new BulkTimer(backoffs[0] + Math.round(jitter * Math.random()), push) 15 | this._mTimer = new BulkTimer(backoffs[1] + Math.round(jitter * Math.random()), push) 16 | this._lTimer = new BulkTimer(backoffs[2] + Math.round(jitter * Math.random()), push) 17 | this._xTimer = new BulkTimer(backoffs[3] + Math.round(jitter * Math.random()), push) 18 | } 19 | 20 | _selectRetryTimer (peerInfo) { 21 | if (peerInfo.banned || !peerInfo.reconnecting) return null 22 | 23 | if (peerInfo.attempts > 3) { 24 | return peerInfo.explicit ? this._xTimer : null 25 | } 26 | 27 | if (peerInfo.attempts === 0) return this._sTimer 28 | if (peerInfo.proven) { 29 | switch (peerInfo.attempts) { 30 | case 1: return this._sTimer 31 | case 2: return this._mTimer 32 | case 3: return this._lTimer 33 | } 34 | } else { 35 | switch (peerInfo.attempts) { 36 | case 1: return this._mTimer 37 | case 2: return this._lTimer 38 | case 3: return this._lTimer 39 | } 40 | } 41 | 42 | return null 43 | } 44 | 45 | add (peerInfo) { 46 | const timer = this._selectRetryTimer(peerInfo) 47 | if (!timer) return false 48 | 49 | timer.add(peerInfo) 50 | return true 51 | } 52 | 53 | destroy () { 54 | this._sTimer.destroy() 55 | this._mTimer.destroy() 56 | this._lTimer.destroy() 57 | this._xTimer.destroy() 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hyperswarm", 3 | "version": "4.11.7", 4 | "description": "A distributed networking stack for connecting peers", 5 | "files": [ 6 | "index.js", 7 | "lib/**.js" 8 | ], 9 | "imports": { 10 | "events": { 11 | "bare": "bare-events", 12 | "default": "events" 13 | } 14 | }, 15 | "dependencies": { 16 | "b4a": "^1.3.1", 17 | "bare-events": "^2.2.0", 18 | "hyperdht": "^6.11.0", 19 | "safety-catch": "^1.0.2", 20 | "shuffled-priority-queue": "^2.1.0", 21 | "unslab": "^1.3.0" 22 | }, 23 | "devDependencies": { 24 | "brittle": "^3.0.2", 25 | "hypercore-crypto": "^3.4.0", 26 | "standard": "^17.0.0" 27 | }, 28 | "scripts": { 29 | "test": "standard && node test/all.js", 30 | "test:generate": "brittle -r test/all.js test/*.js" 31 | }, 32 | "repository": { 33 | "type": "git", 34 | "url": "https://github.com/holepunchto/hyperswarm.git" 35 | }, 36 | "author": "Mathias Buus (@mafintosh)", 37 | "contributors": [ 38 | "David Mark Clements (@davidmarkclem)", 39 | "Andrew Osheroff (@andrewosh)" 40 | ], 41 | "license": "MIT", 42 | "bugs": { 43 | "url": "https://github.com/holepunchto/hyperswarm/issues" 44 | }, 45 | "homepage": "https://github.com/holepunchto/hyperswarm" 46 | } 47 | -------------------------------------------------------------------------------- /test/all.js: -------------------------------------------------------------------------------- 1 | // This runner is auto-generated by Brittle 2 | 3 | runTests() 4 | 5 | async function runTests () { 6 | const test = (await import('brittle')).default 7 | 8 | test.pause() 9 | 10 | await import('./bulk-timer.js') 11 | await import('./chaos.js') 12 | await import('./dups.js') 13 | await import('./firewall.js') 14 | await import('./peer-join.js') 15 | await import('./retry-timer.js') 16 | await import('./suspend.js') 17 | await import('./stats.js') 18 | await import('./swarm.js') 19 | await import('./update.js') 20 | 21 | test.resume() 22 | } 23 | -------------------------------------------------------------------------------- /test/bulk-timer.js: -------------------------------------------------------------------------------- 1 | const test = require('brittle') 2 | 3 | const BulkTimer = require('../lib/bulk-timer') 4 | 5 | const TEST_INTERVAL = 500 6 | 7 | test('bulk timer queue', async (t) => { 8 | t.plan(1) 9 | 10 | const timer = new BulkTimer(TEST_INTERVAL, batch => { 11 | t.alike(batch, [1, 2]) 12 | }) 13 | 14 | timer.add(1) 15 | timer.add(2) 16 | 17 | await waitForCalls(1) 18 | timer.destroy() 19 | }) 20 | 21 | test('bulk timer queue (async)', async (t) => { 22 | t.plan(1) 23 | 24 | const timer = new BulkTimer(TEST_INTERVAL, batch => { 25 | t.alike(batch, [1, 2]) 26 | timer.destroy() 27 | }) 28 | 29 | timer.add(1) 30 | await new Promise(resolve => setImmediate(resolve)) 31 | timer.add(2) 32 | 33 | await waitForCalls(1) 34 | }) 35 | 36 | test('bulk timer queue different batch', async (t) => { 37 | t.plan(2) 38 | 39 | let calls = 0 40 | const timer = new BulkTimer(TEST_INTERVAL, batch => { 41 | if (calls++ === 0) { 42 | t.alike(batch, [1]) 43 | return 44 | } 45 | t.alike(batch, [2]) 46 | timer.destroy() 47 | }) 48 | 49 | timer.add(1) 50 | await waitForCalls(1) 51 | 52 | timer.add(2) 53 | await waitForCalls(1) 54 | }) 55 | 56 | test('bulk timer - nothing pending', async (t) => { 57 | let calls = 0 58 | const timer = new BulkTimer(TEST_INTERVAL, () => calls++) 59 | 60 | timer.add(1) 61 | await waitForCalls(1) // nothing should be pending after this 62 | t.alike(calls, 1) 63 | 64 | await waitForCalls(1) 65 | t.alike(calls, 1) 66 | 67 | timer.destroy() 68 | }) 69 | 70 | function waitForCalls (n) { 71 | return new Promise(resolve => setTimeout(resolve, n * (TEST_INTERVAL * 1.5))) 72 | } 73 | -------------------------------------------------------------------------------- /test/chaos.js: -------------------------------------------------------------------------------- 1 | const test = require('brittle') 2 | const crypto = require('hypercore-crypto') 3 | const createTestnet = require('hyperdht/testnet') 4 | const { timeout } = require('./helpers') 5 | 6 | const Hyperswarm = require('..') 7 | 8 | const NUM_SWARMS = 10 9 | const NUM_TOPICS = 15 10 | const NUM_FORCE_DISCONNECTS = 30 11 | 12 | const STARTUP_DURATION = 1000 * 5 13 | const TEST_DURATION = 1000 * 45 14 | const CHAOS_DURATION = 1000 * 10 15 | 16 | const BACKOFFS = [ 17 | 100, 18 | 1000, 19 | CHAOS_DURATION, // Summed value till here should be > CHAOS_DURATION, and this particular value should be less than TEST_DURATION - CHAOS_DURATION 20 | 10000 // Note: the fourth backoff is irrelevant for this test, as it only triggers when peerInfo.explicit is true 21 | ] 22 | 23 | test('chaos - recovers after random disconnections (takes ~60s)', async (t) => { 24 | t.timeout(90000) 25 | 26 | const { bootstrap } = await createTestnet(3, t.teardown) 27 | 28 | const swarms = [] 29 | const topics = [] 30 | const connections = [] 31 | const peersBySwarm = new Map() 32 | 33 | for (let i = 0; i < NUM_SWARMS; i++) { 34 | const swarm = new Hyperswarm({ bootstrap, backoffs: BACKOFFS, jitter: 0 }) 35 | swarms.push(swarm) 36 | peersBySwarm.set(swarm, new Set()) 37 | swarm.on('connection', conn => { 38 | connections.push(conn) 39 | 40 | conn.on('error', noop) 41 | conn.on('close', () => { 42 | clearInterval(timer) 43 | const idx = connections.indexOf(conn) 44 | if (idx === -1) return 45 | connections.splice(idx, 1) 46 | }) 47 | 48 | const timer = setInterval(() => { 49 | conn.write(Buffer.alloc(10)) 50 | }, 100) 51 | conn.write(Buffer.alloc(10)) 52 | }) 53 | } 54 | for (let i = 0; i < NUM_TOPICS; i++) { 55 | const topic = crypto.randomBytes(32) 56 | topics.push(topic) 57 | } 58 | 59 | for (const topic of topics) { 60 | const numSwarms = Math.round(Math.random() * NUM_SWARMS) 61 | const topicSwarms = new Set() 62 | for (let i = 0; i < numSwarms; i++) { 63 | topicSwarms.add(swarms[Math.floor(Math.random() * NUM_SWARMS)]) 64 | } 65 | for (const swarm of topicSwarms) { 66 | const peers = peersBySwarm.get(swarm) 67 | for (const s of topicSwarms) { 68 | if (swarm === s) continue 69 | peers.add(s.keyPair.publicKey.toString('hex')) 70 | } 71 | await swarm.join(topic).flushed() 72 | } 73 | } 74 | 75 | for (const s of swarms) await s.flush() 76 | await timeout(STARTUP_DURATION) 77 | 78 | for (const [swarm, expectedPeers] of peersBySwarm) { 79 | t.alike(swarm.connections.size, expectedPeers.size, 'swarm has the correct number of connections after startup') 80 | const missingKeys = [] 81 | for (const conn of swarm.connections) { 82 | const key = conn.remotePublicKey.toString('hex') 83 | if (!expectedPeers.has(key)) missingKeys.push(key) 84 | } 85 | t.alike(missingKeys.length, 0, 'swarm is not missing any expected peers after startup') 86 | } 87 | 88 | // Randomly destroy connections during the chaos period. 89 | for (let i = 0; i < NUM_FORCE_DISCONNECTS; i++) { 90 | const timeout = Math.floor(Math.random() * CHAOS_DURATION) // Leave a lot of room at the end for reestablishing connections (timeouts) 91 | setTimeout(() => { 92 | if (!connections.length) return 93 | const idx = Math.floor(Math.random() * connections.length) 94 | const conn = connections[idx] 95 | conn.destroy() 96 | }, timeout) 97 | } 98 | 99 | await timeout(TEST_DURATION) // Wait for the chaos to resolve 100 | 101 | for (const [swarm, expectedPeers] of peersBySwarm) { 102 | t.alike(swarm.connections.size, expectedPeers.size, 'swarm has the correct number of connections') 103 | const missingKeys = [] 104 | for (const conn of swarm.connections) { 105 | const key = conn.remotePublicKey.toString('hex') 106 | if (!expectedPeers.has(key)) missingKeys.push(key) 107 | } 108 | t.alike(missingKeys.length, 0, 'swarm is not missing any expected peers') 109 | } 110 | 111 | for (const swarm of swarms) await swarm.destroy() 112 | }) 113 | 114 | function noop () {} 115 | -------------------------------------------------------------------------------- /test/dups.js: -------------------------------------------------------------------------------- 1 | const test = require('brittle') 2 | const createTestnet = require('hyperdht/testnet') 3 | const Hyperswarm = require('../') 4 | 5 | test('many servers', async t => { 6 | const { bootstrap } = await createTestnet(3, t.teardown) 7 | const topic = Buffer.alloc(32).fill('hello') 8 | 9 | const sub = t.test() 10 | const cnt = 10 11 | 12 | sub.plan(cnt) 13 | 14 | const swarms = [] 15 | for (let i = 0; i < cnt; i++) { 16 | swarms.push(new Hyperswarm({ bootstrap })) 17 | } 18 | 19 | for (const swarm of swarms) { 20 | const missing = new Set() 21 | let done = false 22 | 23 | swarm.on('connection', conn => { 24 | missing.add(conn.remotePublicKey.toString('hex')) 25 | 26 | conn.on('error', noop) 27 | conn.on('close', function () { 28 | missing.delete(conn.remotePublicKey.toString('hex')) 29 | }) 30 | 31 | if (!done && missing.size === cnt - 1) { 32 | done = true 33 | sub.pass('swarm fully connected') 34 | } 35 | }) 36 | } 37 | 38 | const discovery = swarms[0].join(topic, { server: true }) 39 | await discovery.flushed() 40 | 41 | for (const swarm of swarms) swarm.join(topic, { client: false, server: true }) 42 | 43 | await Promise.all(swarms.map(s => s.flush())) 44 | 45 | for (const swarm of swarms) swarm.join(topic) 46 | 47 | const then = Date.now() 48 | await sub 49 | 50 | t.pass('fully connected swarm in ' + (Date.now() - then) + 'ms') 51 | 52 | for (const swarm of swarms) await swarm.destroy() 53 | }) 54 | 55 | function noop () {} 56 | -------------------------------------------------------------------------------- /test/firewall.js: -------------------------------------------------------------------------------- 1 | const test = require('brittle') 2 | const createTestnet = require('hyperdht/testnet') 3 | const { timeout, flushConnections } = require('./helpers') 4 | 5 | const Hyperswarm = require('..') 6 | 7 | const BACKOFFS = [ 8 | 100, 9 | 200, 10 | 300, 11 | 400 12 | ] 13 | 14 | test('firewalled server - bad client is rejected', async (t) => { 15 | const { bootstrap } = await createTestnet(3, t.teardown) 16 | 17 | const swarm1 = new Hyperswarm({ bootstrap, backoffs: BACKOFFS, jitter: 0 }) 18 | const swarm2 = new Hyperswarm({ 19 | bootstrap, 20 | backoffs: BACKOFFS, 21 | jitter: 0, 22 | firewall: remotePublicKey => { 23 | return remotePublicKey.equals(swarm1.keyPair.publicKey) 24 | } 25 | }) 26 | 27 | let serverConnections = 0 28 | swarm2.on('connection', () => serverConnections++) 29 | 30 | const topic = Buffer.alloc(32).fill('hello world') 31 | await swarm2.join(topic, { client: false, server: true }).flushed() 32 | 33 | swarm1.join(topic, { client: true, server: false }) 34 | await flushConnections(swarm1) 35 | 36 | t.alike(serverConnections, 0, 'server did not receive an incoming connection') 37 | 38 | await swarm1.destroy() 39 | await swarm2.destroy() 40 | }) 41 | 42 | test('firewalled client - bad server is rejected', async (t) => { 43 | const { bootstrap } = await createTestnet(3, t.teardown) 44 | t.plan(2) 45 | 46 | const swarm1 = new Hyperswarm({ bootstrap, backoffs: BACKOFFS, jitter: 0 }) 47 | const swarm2 = new Hyperswarm({ 48 | bootstrap, 49 | backoffs: BACKOFFS, 50 | jitter: 0, 51 | firewall: remotePublicKey => { 52 | const firewalled = remotePublicKey.equals(swarm1.keyPair.publicKey) 53 | t.ok(firewalled, 'The peer got firewalled') 54 | return firewalled 55 | } 56 | }) 57 | 58 | let clientConnections = 0 59 | swarm2.on('connection', () => clientConnections++) 60 | 61 | const topic = Buffer.alloc(32).fill('hello world') 62 | await swarm1.join(topic, { client: false, server: true }).flushed() 63 | 64 | swarm2.join(topic, { client: true, server: false }) 65 | await flushConnections(swarm2) 66 | 67 | t.alike(clientConnections, 0, 'client did not receive an incoming connection') 68 | 69 | await swarm1.destroy() 70 | await swarm2.destroy() 71 | }) 72 | 73 | test('firewalled server - rejection does not trigger retry cascade', async (t) => { 74 | const { bootstrap } = await createTestnet(3, t.teardown) 75 | 76 | const swarm1 = new Hyperswarm({ bootstrap, backoffs: BACKOFFS, jitter: 0 }) 77 | 78 | let firewallCalls = 0 79 | const swarm2 = new Hyperswarm({ 80 | bootstrap, 81 | backoffs: BACKOFFS, 82 | jitter: 0, 83 | firewall: remotePublicKey => { 84 | firewallCalls++ 85 | return remotePublicKey.equals(swarm1.keyPair.publicKey) 86 | } 87 | }) 88 | 89 | let serverConnections = 0 90 | swarm2.on('connection', () => serverConnections++) 91 | 92 | const topic = Buffer.alloc(32).fill('hello world') 93 | await swarm2.join(topic).flushed() 94 | 95 | swarm1.join(topic) 96 | 97 | await timeout(BACKOFFS[2] * 5) // Wait for many retries -- there should only be 3 98 | 99 | t.alike(serverConnections, 0, 'server did not receive an incoming connection') 100 | t.alike(firewallCalls, 1, 'client retried mulitple times but server cached it') 101 | 102 | await swarm1.destroy() 103 | await swarm2.destroy() 104 | }) 105 | -------------------------------------------------------------------------------- /test/helpers/index.js: -------------------------------------------------------------------------------- 1 | exports.timeout = function timeout (ms) { 2 | return new Promise((resolve) => setTimeout(resolve, ms)) 3 | } 4 | 5 | exports.flushConnections = async function (swarm) { 6 | await swarm.flush() 7 | await Promise.all(Array.from(swarm.connections).map(e => e.flush())) 8 | await new Promise(resolve => setImmediate(resolve)) 9 | } 10 | -------------------------------------------------------------------------------- /test/manual/measure-reconnect.js: -------------------------------------------------------------------------------- 1 | /** 2 | * The goal of this test is to measure how quickly a client reconnects 3 | * after manually switching networks / e.g. from wifi to mobile data. 4 | * 5 | * It requires some extra modules to get the relays: 6 | * npm install --no-save hypertrace hypercore-id-encoding @holepunchto/keet-default-config 7 | */ 8 | 9 | function customLogger (data) { 10 | console.log(` ... ${data.id} ${Object.keys(data.caller.props || []).join(',')} ${data.caller.filename}:${data.caller.line}:${data.caller.column}`) 11 | } 12 | require('hypertrace').setTraceFunction(customLogger) 13 | 14 | const { DEV_BLIND_RELAY_KEYS } = require('@holepunchto/keet-default-config') 15 | const HypercoreId = require('hypercore-id-encoding') 16 | const DEV_RELAY_KEYS = DEV_BLIND_RELAY_KEYS.map(HypercoreId.decode) 17 | const relayThrough = (force) => force ? DEV_RELAY_KEYS : null 18 | 19 | const Hyperswarm = require('../..') 20 | 21 | const topic = Buffer.alloc(32).fill('measure-reconnect') 22 | const seed = Buffer.alloc(32).fill('measure-reconnect' + require('os').hostname()) 23 | 24 | const swarm = new Hyperswarm({ seed, relayThrough }) 25 | 26 | swarm.dht.on('network-change', () => { 27 | console.log('NETWORK CHANGE') 28 | console.time('RECONNECTION TIME') 29 | }) 30 | 31 | let connected = false 32 | 33 | swarm.on('connection', async (conn) => { 34 | console.log(conn.rawStream.remoteHost) 35 | conn.on('error', console.log.bind(console)) 36 | conn.on('close', console.log.bind(console)) 37 | conn.on('data', (data) => console.log(data.toString('utf8'))) 38 | conn.setKeepAlive(5000) 39 | conn.write('hello') 40 | if (!connected) { 41 | connected = true 42 | console.timeEnd('INITIAL CONNECTION TIME') 43 | return 44 | } 45 | console.timeEnd('RECONNECTION TIME') 46 | }) 47 | 48 | console.time('INITIAL CONNECTION TIME') 49 | swarm.join(topic) 50 | 51 | // process.on('SIGINT', () => { 52 | // swarm.leave(topic).then(() => process.exit()) 53 | // }) 54 | -------------------------------------------------------------------------------- /test/peer-join.js: -------------------------------------------------------------------------------- 1 | const test = require('brittle') 2 | const createTestnet = require('hyperdht/testnet') 3 | 4 | const Hyperswarm = require('..') 5 | 6 | test('join peer - can establish direct connections to public keys', async (t) => { 7 | const { bootstrap } = await createTestnet(3, t.teardown) 8 | 9 | const swarm1 = new Hyperswarm({ bootstrap }) 10 | const swarm2 = new Hyperswarm({ bootstrap }) 11 | 12 | await swarm2.listen() // Ensure that swarm2's public key is being announced 13 | 14 | const firstConnection = t.test('first connection') 15 | firstConnection.plan(2) 16 | 17 | const connections = t.test('connections') 18 | connections.plan(4) 19 | 20 | let s2Connected = false 21 | let s1Connected = false 22 | 23 | swarm2.on('connection', conn => { 24 | conn.on('error', noop) 25 | if (!s2Connected) { 26 | firstConnection.pass('swarm2 got its first connection') 27 | s2Connected = true 28 | } 29 | connections.pass('swarm2 got a connection') 30 | }) 31 | swarm1.on('connection', conn => { 32 | conn.on('error', noop) 33 | if (!s1Connected) { 34 | firstConnection.pass('swarm1 got its first connection') 35 | s1Connected = true 36 | } 37 | connections.pass('swarm1 got a connection') 38 | }) 39 | 40 | swarm1.joinPeer(swarm2.keyPair.publicKey) 41 | await firstConnection 42 | 43 | for (const conn of swarm1.connections) { 44 | conn.end() 45 | } 46 | for (const conn of swarm2.connections) { 47 | conn.end() 48 | } 49 | await swarm1.flush() // Should reconnect 50 | 51 | await connections 52 | 53 | await swarm1.destroy() 54 | await swarm2.destroy() 55 | }) 56 | 57 | test('join peer - attempt to connect to self is a no-op', async (t) => { 58 | const { bootstrap } = await createTestnet(3, t.teardown) 59 | 60 | const swarm = new Hyperswarm({ bootstrap }) 61 | await swarm.listen() 62 | 63 | swarm.joinPeer(swarm.keyPair.publicKey) 64 | t.alike(swarm._queue.length, 0) 65 | 66 | await swarm.destroy() 67 | }) 68 | 69 | test('leave peer - will stop reconnecting to previously joined peers', async (t) => { 70 | const { bootstrap } = await createTestnet(3, t.teardown) 71 | 72 | const swarm1 = new Hyperswarm({ bootstrap }) 73 | const swarm2 = new Hyperswarm({ bootstrap }) 74 | 75 | await swarm2.listen() // Ensure that swarm2's public key is being announced 76 | 77 | const open = t.test('open') 78 | open.plan(2) 79 | 80 | const close = t.test('close') 81 | close.plan(2) 82 | 83 | swarm2.on('connection', conn => { 84 | conn.once('close', () => close.pass('swarm2 connection closed')) 85 | open.pass('swarm2 got a connection') 86 | }) 87 | swarm1.on('connection', conn => { 88 | conn.once('close', conn => close.pass('swarm1 connection closed')) 89 | open.pass('swarm1 got a connection') 90 | }) 91 | 92 | swarm1.joinPeer(swarm2.keyPair.publicKey) 93 | 94 | await open 95 | 96 | swarm1.removeAllListeners('connection') 97 | swarm2.removeAllListeners('connection') 98 | 99 | swarm1.leavePeer(swarm2.keyPair.publicKey) 100 | t.alike(swarm1.explicitPeers.size, 0) 101 | t.alike(swarm1.connections.size, 1) 102 | t.alike(swarm2.connections.size, 1) 103 | 104 | swarm2.on('connection', conn => { 105 | t.fail('swarm2 got a connection after leave') 106 | }) 107 | swarm1.on('connection', conn => { 108 | t.fail('swarm1 got a connection after leave') 109 | }) 110 | 111 | for (const conn of swarm1.connections) { 112 | conn.end() 113 | } 114 | for (const conn of swarm2.connections) { 115 | conn.end() 116 | } 117 | await close 118 | 119 | t.alike(swarm1.connections.size, 0) 120 | t.alike(swarm2.connections.size, 0) 121 | 122 | await swarm1.destroy() 123 | await swarm2.destroy() 124 | }) 125 | 126 | test('leave peer - no memory leak if other side closed connection first', async (t) => { 127 | const { bootstrap } = await createTestnet(3, t.teardown) 128 | 129 | t.plan(9) 130 | 131 | // No need to wait between retries, we just want to test 132 | // that it cleans up after the failed retry 133 | const instaBackoffs = [0, 0, 0, 0] 134 | const swarm1 = new Hyperswarm({ bootstrap, backoffs: instaBackoffs, jitter: 0 }) 135 | const swarm2 = new Hyperswarm({ bootstrap }) 136 | 137 | let hasBeen1 = false 138 | swarm1.on('update', async () => { 139 | if (swarm1.peers.size > 0) hasBeen1 = true 140 | if (hasBeen1 && swarm1.peers.size === 0) { 141 | t.pass('No peerInfo memory leak') 142 | t.is(swarm1.explicitPeers.size, 0) 143 | t.is(swarm1.connections.size, 0) 144 | 145 | swarm1.destroy() 146 | } 147 | }) 148 | 149 | await swarm2.listen() // Ensure that swarm2's public key is being announced 150 | 151 | const open = t.test('open') 152 | open.plan(2) 153 | 154 | const close = t.test('close') 155 | close.plan(2) 156 | 157 | swarm2.on('connection', conn => { 158 | conn.once('close', () => close.pass('swarm2 connection closed')) 159 | open.pass('swarm2 got a connection') 160 | conn.on('error', noop) 161 | }) 162 | swarm1.on('connection', conn => { 163 | conn.once('close', () => close.pass('swarm1 connection closed')) 164 | open.pass('swarm1 got a connection') 165 | conn.on('error', noop) 166 | }) 167 | 168 | swarm1.joinPeer(swarm2.keyPair.publicKey) 169 | 170 | await open 171 | 172 | swarm1.removeAllListeners('connection') 173 | swarm2.removeAllListeners('connection') 174 | 175 | t.is(swarm1.connections.size, 1) 176 | 177 | await swarm2.destroy() 178 | await close 179 | 180 | t.is(swarm1.connections.size, 0) 181 | t.is(swarm1.peers.size, 1) 182 | t.is(swarm1.explicitPeers.size, 1) 183 | 184 | swarm1.leavePeer(swarm2.keyPair.publicKey) 185 | }) 186 | 187 | function noop () {} 188 | -------------------------------------------------------------------------------- /test/retry-timer.js: -------------------------------------------------------------------------------- 1 | const test = require('brittle') 2 | const crypto = require('hypercore-crypto') 3 | const { timeout } = require('./helpers') 4 | 5 | const RetryTimer = require('../lib/retry-timer') 6 | const PeerInfo = require('../lib/peer-info') 7 | 8 | const BACKOFFS = [ 9 | 50, 10 | 150, 11 | 250, 12 | 350 13 | ] 14 | const MAX_JITTER = 20 15 | 16 | const isLinux = process.platform === 'linux' 17 | 18 | // Windows and Mac CI are slow, running on Linux only is enough 19 | test('retry timer - proven peer reinsertion', { skip: !isLinux }, async (t) => { 20 | let calls = 0 21 | const rt = new RetryTimer(() => calls++, { 22 | backoffs: BACKOFFS, 23 | jitter: MAX_JITTER 24 | }) 25 | 26 | const peerInfo = randomPeerInfo() 27 | 28 | rt.add(peerInfo) 29 | 30 | const msMargin = 50 31 | await timeout(BACKOFFS[0] + MAX_JITTER + msMargin) 32 | t.is(calls, 1) 33 | 34 | setQuickRetry(peerInfo) 35 | rt.add(peerInfo) 36 | 37 | await timeout(BACKOFFS[0] + MAX_JITTER + msMargin) 38 | 39 | t.is(calls, 2) 40 | 41 | rt.destroy() 42 | }) 43 | 44 | test('retry timer - forget unresponsive', async (t) => { 45 | let calls = 0 46 | const rt = new RetryTimer(() => calls++, { 47 | backoffs: BACKOFFS, 48 | jitter: MAX_JITTER 49 | }) 50 | 51 | const peerInfo = randomPeerInfo() 52 | 53 | rt.add(peerInfo) 54 | 55 | await timeout(BACKOFFS[0] + MAX_JITTER) 56 | 57 | setUnresponsive(peerInfo) 58 | rt.add(peerInfo) 59 | 60 | await timeout(BACKOFFS[2] + MAX_JITTER) 61 | 62 | t.is(calls, 1) // The second `add` should not trigger any more retries 63 | 64 | rt.destroy() 65 | }) 66 | 67 | test('retry timer - does not retry banned peers', async (t) => { 68 | let calls = 0 69 | const rt = new RetryTimer(() => calls++, { 70 | backoffs: BACKOFFS, 71 | jitter: MAX_JITTER 72 | }) 73 | 74 | const peerInfo = randomPeerInfo() 75 | rt.add(peerInfo) 76 | 77 | await timeout(BACKOFFS[0] + MAX_JITTER) 78 | 79 | peerInfo.ban(true) 80 | rt.add(peerInfo) 81 | 82 | await timeout(BACKOFFS[2] + MAX_JITTER) 83 | 84 | t.is(calls, 1) // The second `add` should not trigger any more retries 85 | 86 | rt.destroy() 87 | }) 88 | 89 | function randomPeerInfo () { 90 | return new PeerInfo({ 91 | publicKey: crypto.randomBytes(32) 92 | }) 93 | } 94 | 95 | function setQuickRetry (peerInfo) { 96 | peerInfo.proven = true 97 | peerInfo.reconnect(true) 98 | peerInfo.attempts = 1 99 | } 100 | 101 | function setUnresponsive (peerInfo) { 102 | peerInfo.proven = false 103 | peerInfo.reconnect(true) 104 | peerInfo.attempts = 4 105 | } 106 | -------------------------------------------------------------------------------- /test/stats.js: -------------------------------------------------------------------------------- 1 | const test = require('brittle') 2 | const createTestnet = require('hyperdht/testnet') 3 | 4 | const Hyperswarm = require('..') 5 | 6 | test('connectionsOpened and connectionsClosed stats', async (t) => { 7 | const { bootstrap } = await createTestnet(3, t.teardown) 8 | 9 | const swarm1 = new Hyperswarm({ bootstrap }) 10 | const swarm2 = new Hyperswarm({ bootstrap }) 11 | 12 | const tOpen = t.test('Open connection') 13 | tOpen.plan(3) 14 | const tClose = t.test('Close connection') 15 | tClose.plan(4) 16 | 17 | t.teardown(async () => { 18 | await swarm1.destroy() 19 | await swarm2.destroy() 20 | }) 21 | 22 | swarm2.on('connection', (conn) => { 23 | conn.on('error', noop) 24 | 25 | tOpen.is(swarm2.stats.connects.client.opened, 1, 'opened connection is in stats') 26 | tOpen.is(swarm2.stats.connects.client.attempted, 1, 'attemped connection is in stats') 27 | tClose.is(swarm2.stats.connects.client.closed, 0, 'sanity check') 28 | 29 | conn.on('close', () => { 30 | tClose.is(swarm2.stats.connects.client.closed, 1, 'closed connection is in stats') 31 | }) 32 | 33 | conn.end() 34 | }) 35 | 36 | swarm1.on('connection', (conn) => { 37 | conn.on('error', () => noop) 38 | 39 | conn.on('open', () => { 40 | tOpen.is(swarm1.stats.connects.server.opened, 1, 'opened server connection is in stats') 41 | tClose.is(swarm1.stats.connects.server.closed, 0, 'Sanity check') 42 | }) 43 | 44 | conn.on('close', () => { 45 | tClose.is(swarm1.stats.connects.server.closed, 1, 'closed connections is in stats') 46 | }) 47 | 48 | conn.end() 49 | }) 50 | 51 | const topic = Buffer.alloc(32).fill('hello world') 52 | await swarm1.join(topic, { server: true, client: false }).flushed() 53 | swarm2.join(topic, { client: true, server: false }) 54 | 55 | await tClose 56 | }) 57 | 58 | function noop () {} 59 | -------------------------------------------------------------------------------- /test/suspend.js: -------------------------------------------------------------------------------- 1 | const test = require('brittle') 2 | const createTestnet = require('hyperdht/testnet') 3 | 4 | const Hyperswarm = require('..') 5 | 6 | test('suspend + resume', async (t) => { 7 | t.plan(4) 8 | 9 | const { bootstrap } = await createTestnet(3, t.teardown) 10 | 11 | const swarm1 = new Hyperswarm({ bootstrap }) 12 | const swarm2 = new Hyperswarm({ bootstrap }) 13 | 14 | t.teardown(async () => { 15 | await swarm1.destroy() 16 | await swarm2.destroy() 17 | }) 18 | 19 | const topic = Buffer.alloc(32).fill('hello world') 20 | 21 | swarm1.on('connection', function (socket) { 22 | t.pass('swarm1 received connection') 23 | socket.on('error', () => {}) 24 | }) 25 | 26 | swarm2.on('connection', function (socket) { 27 | t.pass('swarm2 received connection') 28 | socket.on('error', () => {}) 29 | }) 30 | 31 | const discovery = swarm1.join(topic, { server: true, client: false }) 32 | await discovery.flushed() 33 | 34 | swarm2.join(topic, { client: true, server: false }) 35 | await swarm2.flush() 36 | 37 | t.comment('suspended swarm2') 38 | swarm2.suspend() 39 | 40 | setTimeout(() => { 41 | t.comment('resumed swarm2') 42 | swarm2.resume() 43 | }, 2000) 44 | }) 45 | -------------------------------------------------------------------------------- /test/swarm.js: -------------------------------------------------------------------------------- 1 | const test = require('brittle') 2 | const createTestnet = require('hyperdht/testnet') 3 | const { timeout, flushConnections } = require('./helpers') 4 | const b4a = require('b4a') 5 | 6 | const Hyperswarm = require('..') 7 | 8 | const BACKOFFS = [ 9 | 100, 10 | 200, 11 | 300, 12 | 400 13 | ] 14 | 15 | test('one server, one client - first connection', async (t) => { 16 | const { bootstrap } = await createTestnet(3, t.teardown) 17 | 18 | const swarm1 = new Hyperswarm({ bootstrap }) 19 | const swarm2 = new Hyperswarm({ bootstrap }) 20 | 21 | t.plan(1) 22 | 23 | t.teardown(async () => { 24 | await swarm1.destroy() 25 | await swarm2.destroy() 26 | }) 27 | 28 | swarm2.on('connection', (conn) => { 29 | t.pass('swarm2') 30 | conn.on('error', noop) 31 | conn.end() 32 | }) 33 | swarm1.on('connection', (conn) => { 34 | conn.on('error', noop) 35 | conn.end() 36 | }) 37 | 38 | const topic = Buffer.alloc(32).fill('hello world') 39 | await swarm1.join(topic, { server: true, client: false }).flushed() 40 | swarm2.join(topic, { client: true, server: false }) 41 | }) 42 | 43 | test('two servers - first connection', async (t) => { 44 | const { bootstrap } = await createTestnet(3, t.teardown) 45 | 46 | const swarm1 = new Hyperswarm({ bootstrap }) 47 | const swarm2 = new Hyperswarm({ bootstrap }) 48 | 49 | const connection1Test = t.test('connection1') 50 | const connection2Test = t.test('connection2') 51 | 52 | connection1Test.plan(1) 53 | connection2Test.plan(1) 54 | 55 | t.teardown(async () => { 56 | await swarm1.destroy() 57 | await swarm2.destroy() 58 | }) 59 | 60 | swarm1.on('connection', (conn) => { 61 | conn.on('error', noop) 62 | connection1Test.pass('swarm1') 63 | conn.end() 64 | }) 65 | swarm2.on('connection', (conn) => { 66 | conn.on('error', noop) 67 | connection2Test.pass('swarm2') 68 | conn.end() 69 | }) 70 | 71 | const topic = Buffer.alloc(32).fill('hello world') 72 | 73 | await swarm1.join(topic).flushed() 74 | await swarm2.join(topic).flushed() 75 | }) 76 | 77 | test('one server, one client - single reconnect', async (t) => { 78 | const { bootstrap } = await createTestnet(3, t.teardown) 79 | 80 | const swarm1 = new Hyperswarm({ bootstrap, backoffs: BACKOFFS, jitter: 0 }) 81 | const swarm2 = new Hyperswarm({ bootstrap, backoffs: BACKOFFS, jitter: 0 }) 82 | 83 | const serverReconnectsTest = t.test('server reconnects') 84 | const clientReconnectsTest = t.test('client reconnects') 85 | 86 | serverReconnectsTest.plan(1) 87 | clientReconnectsTest.plan(1) 88 | 89 | t.teardown(async () => { 90 | await swarm1.destroy() 91 | await swarm2.destroy() 92 | }) 93 | 94 | let hasClientConnected = false 95 | let serverDisconnected = false 96 | 97 | swarm2.on('connection', (conn) => { 98 | conn.on('error', noop) 99 | 100 | if (!hasClientConnected) { 101 | hasClientConnected = true 102 | return 103 | } 104 | 105 | clientReconnectsTest.pass('client reconnected') 106 | }) 107 | 108 | swarm1.on('connection', async (conn) => { 109 | conn.on('error', noop) 110 | 111 | if (!serverDisconnected) { 112 | serverDisconnected = true 113 | 114 | // Ensure connection is setup for client too 115 | // before we destroy it 116 | await flushConnections(swarm2) 117 | if (!hasClientConnected) t.fail('Logical error in the test: the client should be connected by now') 118 | 119 | conn.destroy() 120 | return 121 | } 122 | serverReconnectsTest.pass('Server reconnected') 123 | }) 124 | 125 | const topic = Buffer.alloc(32).fill('hello world') 126 | await swarm1.join(topic, { client: false, server: true }).flushed() 127 | swarm2.join(topic, { client: true, server: false }) 128 | }) 129 | 130 | test('one server, one client - maximum reconnects', async (t) => { 131 | const { bootstrap } = await createTestnet(3, t.teardown) 132 | 133 | const swarm1 = new Hyperswarm({ bootstrap, backoffs: BACKOFFS, jitter: 0 }) 134 | const swarm2 = new Hyperswarm({ bootstrap, backoffs: BACKOFFS, jitter: 0 }) 135 | 136 | let connections = 0 137 | swarm2.on('connection', (conn, info) => { 138 | connections++ 139 | conn.on('error', noop) 140 | conn.destroy() 141 | }) 142 | swarm1.on('connection', (conn) => { 143 | conn.on('error', noop) 144 | }) 145 | 146 | const topic = Buffer.alloc(32).fill('hello world') 147 | await swarm1.join(topic, { client: false, server: true }).flushed() 148 | swarm2.join(topic, { client: true, server: false }) 149 | 150 | await timeout(BACKOFFS[2] * 4) 151 | t.ok(connections > 1, 'client saw more than one retry (' + connections + ')') 152 | t.ok(connections < 5, 'client saw less than five attempts') 153 | 154 | await swarm1.destroy() 155 | await swarm2.destroy() 156 | }) 157 | 158 | test('one server, one client - banned peer does not reconnect', async (t) => { 159 | const { bootstrap } = await createTestnet(3, t.teardown) 160 | 161 | const swarm1 = new Hyperswarm({ bootstrap, backoffs: BACKOFFS, jitter: 0 }) 162 | const swarm2 = new Hyperswarm({ bootstrap, backoffs: BACKOFFS, jitter: 0 }) 163 | 164 | let connections = 0 165 | swarm2.on('connection', (conn, info) => { 166 | connections++ 167 | info.ban(true) 168 | conn.on('error', noop) 169 | conn.destroy() 170 | }) 171 | swarm1.on('connection', (conn) => { 172 | conn.on('error', noop) 173 | }) 174 | 175 | const topic = Buffer.alloc(32).fill('hello world') 176 | await swarm1.join(topic, { client: false, server: true }).flushed() 177 | swarm2.join(topic, { client: true, server: false }) 178 | 179 | await timeout(BACKOFFS[2] * 2) // Wait for 2 long backoffs 180 | t.is(connections, 1, 'banned peer was not retried') 181 | 182 | await swarm1.destroy() 183 | await swarm2.destroy() 184 | }) 185 | 186 | test('two servers, two clients - simple deduplication', async (t) => { 187 | const connection1Test = t.test('connection1') 188 | const connection2Test = t.test('connection2') 189 | 190 | connection1Test.plan(1) 191 | connection2Test.plan(1) 192 | 193 | const { bootstrap } = await createTestnet(3, t.teardown) 194 | 195 | const swarm1 = new Hyperswarm({ bootstrap, backoffs: BACKOFFS, jitter: 0 }) 196 | const swarm2 = new Hyperswarm({ bootstrap, backoffs: BACKOFFS, jitter: 0 }) 197 | 198 | t.teardown(async () => { 199 | await swarm1.destroy() 200 | await swarm2.destroy() 201 | }) 202 | 203 | swarm1.on('connection', (conn) => { 204 | connection1Test.pass('Swarm 1 connection') 205 | conn.on('error', noop) 206 | }) 207 | swarm2.on('connection', (conn) => { 208 | connection2Test.pass('Swarm 2 connection') 209 | conn.on('error', noop) 210 | }) 211 | 212 | const topic = Buffer.alloc(32).fill('hello world') 213 | await swarm1.join(topic).flushed() 214 | await swarm2.join(topic).flushed() 215 | }) 216 | 217 | test('one server, two clients - topic multiplexing', async (t) => { 218 | const { bootstrap } = await createTestnet(3, t.teardown) 219 | 220 | const swarm1 = new Hyperswarm({ bootstrap, backoffs: BACKOFFS, jitter: 0 }) 221 | const swarm2 = new Hyperswarm({ bootstrap, backoffs: BACKOFFS, jitter: 0 }) 222 | 223 | let clientConnections = 0 224 | let peerInfo = null 225 | 226 | swarm2.on('connection', (conn, info) => { 227 | clientConnections++ 228 | peerInfo = info 229 | conn.on('error', noop) 230 | }) 231 | 232 | swarm1.on('connection', (conn) => conn.on('error', noop)) 233 | 234 | const topic1 = Buffer.alloc(32).fill('hello world') 235 | const topic2 = Buffer.alloc(32).fill('hi world') 236 | 237 | await swarm1.join(topic1, { client: false, server: true }).flushed() 238 | await swarm1.join(topic2, { client: false, server: true }).flushed() 239 | swarm2.join(topic1, { client: true, server: false }) 240 | swarm2.join(topic2, { client: true, server: false }) 241 | 242 | await swarm2.flush() 243 | await swarm1.flush() 244 | 245 | t.is(clientConnections, 1) 246 | t.is(peerInfo.topics.length, 2) 247 | 248 | await swarm1.destroy() 249 | await swarm2.destroy() 250 | }) 251 | 252 | test('one server, two clients - first connection', async (t) => { 253 | const { bootstrap } = await createTestnet(3, t.teardown) 254 | 255 | const swarm1 = new Hyperswarm({ bootstrap }) 256 | const swarm2 = new Hyperswarm({ bootstrap }) 257 | const swarm3 = new Hyperswarm({ bootstrap }) 258 | 259 | const connection1To2Test = t.test('connection 1 to 2') 260 | const connection1To3Test = t.test('connection 1 to 3') 261 | 262 | const connection2Test = t.test('connection2') 263 | const connection3Test = t.test('connection3') 264 | 265 | connection1To2Test.plan(1) 266 | connection1To3Test.plan(1) 267 | connection2Test.plan(1) 268 | connection3Test.plan(1) 269 | 270 | t.teardown(async () => { 271 | await swarm1.destroy() 272 | await swarm2.destroy() 273 | await swarm3.destroy() 274 | }) 275 | 276 | swarm1.on('connection', (conn, info) => { 277 | if (b4a.equals(info.publicKey, swarm2.keyPair.publicKey)) { 278 | connection1To2Test.pass('Swarm1 connected with swarm2') 279 | } else if (b4a.equals(info.publicKey, swarm3.keyPair.publicKey)) { 280 | connection1To3Test.pass('Swarm1 connected with swarm3') 281 | } else { 282 | t.fail('Unexpected connection') 283 | } 284 | conn.on('error', noop) 285 | }) 286 | swarm2.on('connection', (conn, info) => { 287 | connection2Test.ok(b4a.equals(info.publicKey, swarm1.keyPair.publicKey), 'swarm2 connected with swarm1') 288 | conn.on('error', noop) 289 | }) 290 | swarm3.on('connection', (conn, info) => { 291 | connection3Test.ok(b4a.equals(info.publicKey, swarm1.keyPair.publicKey), 'swarm3 connected with swarm1') 292 | conn.on('error', noop) 293 | }) 294 | 295 | const topic = Buffer.alloc(32).fill('hello world') 296 | await swarm1.join(topic, { server: true, client: false }).flushed() 297 | swarm2.join(topic, { server: false, client: true }) 298 | swarm3.join(topic, { server: false, client: true }) 299 | }) 300 | 301 | test('one server, two clients - if a second client joins after the server leaves, they will not connect', async (t) => { 302 | t.plan(2) 303 | 304 | const { bootstrap } = await createTestnet(3, t.teardown) 305 | 306 | const swarm1 = new Hyperswarm({ bootstrap, backoffs: BACKOFFS, jitter: 0 }) 307 | const swarm2 = new Hyperswarm({ bootstrap, backoffs: BACKOFFS, jitter: 0 }) 308 | const swarm3 = new Hyperswarm({ bootstrap, backoffs: BACKOFFS, jitter: 0 }) 309 | 310 | swarm1.on('connection', (conn) => { 311 | conn.on('error', noop) 312 | }) 313 | 314 | swarm2.on('connection', (conn) => conn.on('error', noop)) 315 | swarm3.on('connection', (conn) => conn.on('error', noop)) 316 | 317 | const topic = Buffer.alloc(32).fill('hello world') 318 | await swarm1.join(topic).flushed() 319 | 320 | swarm2.join(topic, { client: true, server: false }) 321 | 322 | await flushConnections(swarm2) 323 | 324 | await swarm1.leave(topic) 325 | await flushConnections(swarm1) 326 | 327 | swarm3.join(topic, { client: true, server: false }) 328 | await flushConnections(swarm3) 329 | 330 | t.is(swarm2.connections.size, 1) 331 | t.is(swarm3.connections.size, 0) 332 | 333 | await swarm1.destroy() 334 | await swarm2.destroy() 335 | await swarm3.destroy() 336 | }) 337 | 338 | test('two servers, one client - refreshing a peer discovery instance discovers new server', async (t) => { 339 | const { bootstrap } = await createTestnet(3, t.teardown) 340 | 341 | const swarm1 = new Hyperswarm({ bootstrap, backoffs: BACKOFFS, jitter: 0 }) 342 | const swarm2 = new Hyperswarm({ bootstrap, backoffs: BACKOFFS, jitter: 0 }) 343 | const swarm3 = new Hyperswarm({ bootstrap, backoffs: BACKOFFS, jitter: 0 }) 344 | 345 | let clientConnections = 0 346 | swarm3.on('connection', (conn) => { 347 | clientConnections++ 348 | conn.on('error', noop) 349 | }) 350 | 351 | swarm1.on('connection', (conn) => conn.on('error', noop)) 352 | swarm2.on('connection', (conn) => conn.on('error', noop)) 353 | 354 | const topic = Buffer.alloc(32).fill('hello world') 355 | await swarm1.join(topic).flushed() 356 | const discovery = swarm3.join(topic, { client: true, server: false }) 357 | 358 | await flushConnections(swarm3) 359 | t.is(clientConnections, 1) 360 | 361 | await swarm2.join(topic).flushed() 362 | await flushConnections(swarm2) 363 | t.is(clientConnections, 1) 364 | 365 | await discovery.refresh() 366 | await flushConnections(swarm3) 367 | t.is(clientConnections, 2) 368 | 369 | await swarm1.destroy() 370 | await swarm2.destroy() 371 | await swarm3.destroy() 372 | }) 373 | 374 | test('one server, one client - correct deduplication when a client connection is destroyed', async (t) => { 375 | t.plan(4) 376 | const { bootstrap } = await createTestnet(3, t.teardown) 377 | 378 | const swarm1 = new Hyperswarm({ bootstrap, backoffs: BACKOFFS, jitter: 0 }) 379 | const swarm2 = new Hyperswarm({ bootstrap, backoffs: BACKOFFS, jitter: 0 }) 380 | t.teardown(async () => { 381 | await swarm1.destroy() 382 | await swarm2.destroy() 383 | }) 384 | 385 | let clientConnections = 0 386 | let serverConnections = 0 387 | let clientData = 0 388 | let serverData = 0 389 | 390 | swarm1.on('connection', (conn) => { 391 | serverConnections++ 392 | conn.on('error', noop) 393 | conn.on('data', () => { 394 | if (++serverData >= 2) { 395 | t.is(serverConnections, 2, 'Server opened second connection') 396 | t.pass(serverData, 2, 'Received data from second connection') 397 | } 398 | }) 399 | conn.write('hello world') 400 | }) 401 | swarm2.on('connection', (conn) => { 402 | clientConnections++ 403 | conn.on('error', noop) 404 | conn.on('data', () => { 405 | if (++clientData >= 2) { 406 | t.is(clientConnections, 2, 'Client opened second connection') 407 | t.is(clientData, 2, 'Received data from second connection') 408 | } 409 | }) 410 | conn.write('hello world') 411 | 412 | if (clientConnections === 1) setTimeout(() => conn.destroy(), 50) // Destroy the first client connection 413 | }) 414 | 415 | const topic = Buffer.alloc(32).fill('hello world') 416 | 417 | await swarm1.join(topic, { server: true, client: false }).flushed() 418 | swarm2.join(topic, { server: false, client: true }) 419 | }) 420 | 421 | test('flush when max connections reached', async (t) => { 422 | const { bootstrap } = await createTestnet(3, t.teardown) 423 | 424 | const swarm1 = new Hyperswarm({ bootstrap }) 425 | const swarm2 = new Hyperswarm({ bootstrap, maxPeers: 1 }) 426 | const swarm3 = new Hyperswarm({ bootstrap, maxPeers: 1 }) 427 | 428 | const topic = Buffer.alloc(32).fill('hello world') 429 | 430 | await swarm1.join(topic, { server: true }).flushed() 431 | 432 | await swarm2 433 | .on('connection', (conn) => conn.on('error', noop)) 434 | .join(topic, { client: true }) 435 | .flushed() 436 | 437 | await swarm3 438 | .on('connection', (conn) => conn.on('error', noop)) 439 | .join(topic, { client: true }) 440 | .flushed() 441 | 442 | await swarm2.flush() 443 | await swarm3.flush() 444 | 445 | t.pass('flush resolved') 446 | 447 | await swarm1.destroy() 448 | await swarm2.destroy() 449 | await swarm3.destroy() 450 | }) 451 | 452 | test('rejoining with different client/server opts refreshes', async (t) => { 453 | const { bootstrap } = await createTestnet(3, t.teardown) 454 | 455 | const swarm1 = new Hyperswarm({ bootstrap }) 456 | const swarm2 = new Hyperswarm({ bootstrap }) 457 | 458 | const topic = Buffer.alloc(32).fill('hello world') 459 | 460 | swarm1.join(topic, { client: true, server: false }) 461 | await swarm1.join(topic, { client: true, server: true }).flushed() 462 | 463 | await swarm2 464 | .on('connection', (conn) => conn.on('error', noop)) 465 | .join(topic, { client: true }) 466 | .flushed() 467 | 468 | await swarm2.flush() 469 | 470 | t.is(swarm2.connections.size, 1) 471 | 472 | await swarm1.destroy() 473 | await swarm2.destroy() 474 | }) 475 | 476 | test('topics returns peer-discovery objects', async (t) => { 477 | const { bootstrap } = await createTestnet(3, t.teardown) 478 | 479 | const swarm = new Hyperswarm({ bootstrap }) 480 | const topic1 = Buffer.alloc(32).fill('topic 1') 481 | const topic2 = Buffer.alloc(32).fill('topic 2') 482 | 483 | swarm.join(topic1) 484 | swarm.join(topic2) 485 | 486 | const peerDiscoveries = swarm.topics() 487 | 488 | t.alike(peerDiscoveries.next().value.topic, topic1) 489 | t.alike(peerDiscoveries.next().value.topic, topic2) 490 | 491 | await swarm.destroy() 492 | }) 493 | 494 | test('multiple discovery sessions with different opts', async (t) => { 495 | const { bootstrap } = await createTestnet(3, t.teardown) 496 | 497 | const swarm1 = new Hyperswarm({ bootstrap }) 498 | const swarm2 = new Hyperswarm({ bootstrap }) 499 | 500 | const topic = Buffer.alloc(32).fill('hello world') 501 | 502 | const connection1Test = t.test('connection1') 503 | const connection2Test = t.test('connection2') 504 | 505 | connection1Test.plan(1) 506 | connection2Test.plan(1) 507 | 508 | t.teardown(async () => { 509 | await swarm1.destroy() 510 | await swarm2.destroy() 511 | }) 512 | 513 | swarm1.on('connection', (conn) => { 514 | connection1Test.pass('swarm1') 515 | conn.on('error', noop) 516 | }) 517 | 518 | swarm2.on('connection', (conn) => { 519 | connection2Test.pass('swarm2') 520 | conn.on('error', noop) 521 | }) 522 | 523 | await swarm1.join(topic).flushed() 524 | await swarm1.flush() 525 | 526 | const discovery1 = swarm2.join(topic, { client: true, server: false }) 527 | swarm2.join(topic, { client: false, server: true }) 528 | 529 | await discovery1.destroy() // should not prevent server connections 530 | }) 531 | 532 | test('closing all discovery sessions clears all peer-discovery objects', async t => { 533 | const { bootstrap } = await createTestnet(3, t.teardown) 534 | 535 | const swarm = new Hyperswarm({ bootstrap }) 536 | 537 | const topic1 = Buffer.alloc(32).fill('hello') 538 | const topic2 = Buffer.alloc(32).fill('world') 539 | 540 | const discovery1 = swarm.join(topic1, { client: true, server: false }) 541 | const discovery2 = swarm.join(topic2, { client: false, server: true }) 542 | 543 | t.is(swarm._discovery.size, 2) 544 | 545 | await Promise.all([discovery1.destroy(), discovery2.destroy()]) 546 | 547 | t.is(swarm._discovery.size, 0) 548 | 549 | await swarm.destroy() 550 | }) 551 | 552 | test('peer-discovery object deleted when corresponding connection closes (server)', async t => { 553 | const { bootstrap } = await createTestnet(3, t.teardown) 554 | 555 | const swarm1 = new Hyperswarm({ bootstrap }) 556 | const swarm2 = new Hyperswarm({ bootstrap }) 557 | 558 | const connected = t.test('connection') 559 | connected.plan(1) 560 | 561 | const otherConnected = t.test('connection') 562 | otherConnected.plan(1) 563 | 564 | swarm2.on('connection', (conn) => { 565 | connected.pass('swarm2') 566 | conn.on('error', noop) 567 | }) 568 | 569 | let resolveConnClosed = null 570 | const connClosed = new Promise(resolve => { 571 | resolveConnClosed = resolve 572 | }) 573 | swarm1.on('connection', (conn) => { 574 | otherConnected.pass('swarm1') 575 | conn.on('error', noop) 576 | conn.on('close', resolveConnClosed) 577 | }) 578 | 579 | const topic = Buffer.alloc(32).fill('hello world') 580 | await swarm1.join(topic, { server: true, client: false }).flushed() 581 | 582 | swarm2.join(topic, { client: true, server: false }) 583 | await swarm2.flush() 584 | 585 | await connected 586 | await otherConnected 587 | 588 | t.is(swarm1.peers.size, 1) 589 | await swarm2.destroy() 590 | 591 | // Ensure other side detects closed connection 592 | await connClosed 593 | 594 | t.is(swarm1.peers.size, 0, 'No peerInfo memory leak') 595 | 596 | await swarm1.destroy() 597 | }) 598 | 599 | test('peer-discovery object deleted when corresponding connection closes (client)', async t => { 600 | const { bootstrap } = await createTestnet(3, t.teardown) 601 | 602 | t.plan(3) 603 | // We want to test it eventually gets gc'd after all the retries 604 | // so we don't care about waiting between retries 605 | const instaBackoffs = [0, 0, 0, 0] 606 | const swarm1 = new Hyperswarm({ bootstrap, backoffs: instaBackoffs, jitter: 0 }) 607 | const swarm2 = new Hyperswarm({ bootstrap, backoffs: instaBackoffs, jitter: 0 }) 608 | 609 | let hasBeen1 = false 610 | swarm2.on('update', async () => { 611 | if (swarm2.peers.size > 0) hasBeen1 = true 612 | if (hasBeen1 && swarm2.peers.size === 0) { 613 | t.pass('No peerInfo memory leak') 614 | swarm2.destroy() 615 | } 616 | }) 617 | 618 | const connected = t.test('connection') 619 | connected.plan(1) 620 | 621 | swarm2.on('connection', (conn) => { 622 | connected.pass('swarm2') 623 | conn.on('error', noop) 624 | }) 625 | swarm1.on('connection', (conn) => { 626 | conn.on('error', noop) 627 | }) 628 | 629 | const topic = Buffer.alloc(32).fill('hello world') 630 | await swarm1.join(topic, { server: true, client: false }).flushed() 631 | 632 | swarm2.join(topic, { client: true, server: false }) 633 | await swarm2.flush() 634 | 635 | t.is(swarm2.peers.size, 1) 636 | await swarm1.destroy() 637 | }) 638 | 639 | test('no default error handler set when connection event is emitted', async (t) => { 640 | t.plan(2) 641 | 642 | const { bootstrap } = await createTestnet(3, t.teardown) 643 | 644 | const swarm1 = new Hyperswarm({ bootstrap }) 645 | const swarm2 = new Hyperswarm({ bootstrap }) 646 | 647 | t.teardown(async () => { 648 | await swarm1.destroy() 649 | await swarm2.destroy() 650 | }) 651 | 652 | swarm2.on('connection', (conn) => { 653 | t.is(conn.listeners('error').length, 0, 'no error listeners') 654 | conn.on('error', noop) 655 | conn.end() 656 | }) 657 | swarm1.on('connection', (conn) => { 658 | t.is(conn.listeners('error').length, 0, 'no error listeners') 659 | conn.on('error', noop) 660 | conn.end() 661 | }) 662 | 663 | const topic = Buffer.alloc(32).fill('hello world') 664 | await swarm1.join(topic, { server: true, client: false }).flushed() 665 | swarm2.join(topic, { client: true, server: false }) 666 | }) 667 | 668 | test('peerDiscovery has unslabbed closestNodes', async (t) => { 669 | const { bootstrap } = await createTestnet(3, t.teardown) 670 | 671 | const swarm1 = new Hyperswarm({ bootstrap }) 672 | const swarm2 = new Hyperswarm({ bootstrap }) 673 | 674 | const tConnect = t.test('connected') 675 | tConnect.plan(2) 676 | 677 | t.teardown(async () => { 678 | await swarm1.destroy() 679 | await swarm2.destroy() 680 | }) 681 | 682 | swarm2.on('connection', (conn) => { 683 | conn.on('error', noop) 684 | tConnect.pass('swarm2 connected') 685 | }) 686 | swarm1.on('connection', (conn) => { 687 | conn.on('error', noop) 688 | tConnect.pass('swarm1 connected') 689 | }) 690 | 691 | const topic = Buffer.alloc(32).fill('hello world') 692 | await swarm1.join(topic, { server: true, client: false }).flushed() 693 | swarm2.join(topic, { client: true, server: false }) 694 | 695 | await tConnect 696 | 697 | const closestNodes = [...swarm1._discovery.values()][0]._closestNodes 698 | const bufferSizes = closestNodes.map(n => n.id.buffer.byteLength) 699 | t.is(bufferSizes[0], 32, 'unslabbed clostestNodes entry') 700 | 701 | const hasUnslabbeds = bufferSizes.filter(s => s !== 32).length !== 0 702 | t.is(hasUnslabbeds, false, 'sanity check: all are unslabbed') 703 | }) 704 | 705 | test('topic and peer get unslabbed in PeerInfo', async (t) => { 706 | const { bootstrap } = await createTestnet(3, t.teardown) 707 | 708 | const swarm1 = new Hyperswarm({ bootstrap }) 709 | const swarm2 = new Hyperswarm({ bootstrap }) 710 | 711 | t.plan(3) 712 | 713 | t.teardown(async () => { 714 | await swarm1.destroy() 715 | await swarm2.destroy() 716 | }) 717 | 718 | swarm2.on('connection', (conn) => { 719 | t.is( 720 | [...swarm2.peers.values()][0].publicKey.buffer.byteLength, 721 | 32, 722 | 'unslabbed publicKey in peerInfo' 723 | ) 724 | t.is([...swarm2.peers.values()][0].topics[0].buffer.byteLength, 725 | 32, 726 | 'unslabbed topic in peerInfo' 727 | ) 728 | 729 | conn.on('error', noop) 730 | conn.end() 731 | }) 732 | swarm1.on('connection', (conn) => { 733 | t.is( 734 | [...swarm1.peers.values()][0].publicKey.buffer.byteLength, 735 | 32, 736 | 'unslabbed publicKey in peerInfo' 737 | ) 738 | 739 | conn.on('error', noop) 740 | conn.end() 741 | }) 742 | 743 | const topic = Buffer.alloc(32).fill('hello world') 744 | await swarm1.join(topic, { server: true, client: false }).flushed() 745 | swarm2.join(topic, { client: true, server: false }) 746 | }) 747 | 748 | test('port opt gets passed on to hyperdht', async (t) => { 749 | const { bootstrap } = await createTestnet(3, t.teardown) 750 | 751 | const swarm1 = new Hyperswarm({ bootstrap, port: [10000, 10100] }) 752 | t.alike(swarm1.dht.io.portRange, [10000, 10100]) 753 | await swarm1.destroy() 754 | }) 755 | 756 | function noop () {} 757 | -------------------------------------------------------------------------------- /test/update.js: -------------------------------------------------------------------------------- 1 | const test = require('brittle') 2 | const Hyperswarm = require('..') 3 | const createTestnet = require('hyperdht/testnet') 4 | 5 | test('connecting', async (t) => { 6 | t.plan(5) 7 | 8 | const { bootstrap } = await createTestnet(3, t.teardown) 9 | 10 | const swarm1 = new Hyperswarm({ bootstrap }) 11 | const swarm2 = new Hyperswarm({ bootstrap }) 12 | const topic = Buffer.alloc(32).fill('hello world') 13 | 14 | t.teardown(async () => { 15 | await swarm1.destroy() 16 | await swarm2.destroy() 17 | }) 18 | 19 | t.is(swarm2.connecting, 0) 20 | 21 | swarm2.on('update', function onUpdate1 () { 22 | if (swarm2.connecting === 1) { 23 | t.pass('connecting (1)') 24 | 25 | swarm2.off('update', onUpdate1) 26 | 27 | swarm2.on('update', function onUpdate0 () { 28 | if (swarm2.connecting === 0) { 29 | t.pass('connecting (0)') 30 | swarm2.off('update', onUpdate0) 31 | } 32 | }) 33 | } 34 | }) 35 | 36 | swarm1.on('connection', function (socket) { 37 | socket.end() 38 | socket.on('close', () => t.pass()) 39 | }) 40 | 41 | swarm2.on('connection', function (socket) { 42 | socket.end() 43 | socket.on('close', () => t.pass()) 44 | }) 45 | 46 | const discovery = swarm1.join(topic, { server: true, client: false }) 47 | await discovery.flushed() 48 | 49 | swarm2.join(topic, { client: true, server: false }) 50 | await swarm2.flush() 51 | }) 52 | --------------------------------------------------------------------------------