├── .gitignore ├── LICENSE ├── README.md ├── example.js ├── index.js ├── package.json └── test.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | sandbox.js 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Mathias Buus 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # @hyperswarm/discovery 2 | 3 | The hyperswarm peer discovery module 4 | 5 | ``` 6 | npm install @hyperswarm/discovery 7 | ``` 8 | 9 | ## Usage 10 | 11 | ``` js 12 | const discovery = require('@hyperswarm/discovery') 13 | const crypto = require('crypto') 14 | 15 | const d = discovery() 16 | const key = crypto.randomBytes(32) 17 | 18 | const ann = d.announce(key, { 19 | port: 10000 20 | }) 21 | 22 | const lookup = d.lookup(key) 23 | 24 | // emitted when a peer is found 25 | lookup.on('peer', console.log) 26 | ``` 27 | 28 | ## API 29 | 30 | #### `d = discovery([options])` 31 | 32 | Create a new discovery instance 33 | 34 | Options include: 35 | 36 | ```js 37 | { 38 | // Optionally overwrite the default set of bootstrap servers 39 | bootstrap: [addresses], 40 | // Set to false if this is a long running instance on a server 41 | // When running in ephemeral mode you don't join the DHT but just 42 | // query it instead. If unset, or set to a non-boolean (default undefined) 43 | // then the node will start in short-lived (ephemeral) mode and switch 44 | // to long-lived (non-ephemeral) mode after a certain period of uptime 45 | ephemeral: undefined, 46 | // Pass in your own udp/utp socket (needed for hole punching) 47 | socket: (a udp or utp socket) 48 | } 49 | ``` 50 | 51 | #### `topic = d.lookup(key)` 52 | 53 | Start looking for peers shared on `key`, which should be a 32 byte buffer. 54 | 55 | * `topic.destroy()` - Call this to stop looking for peers 56 | * `topic.update()` - Call this to force update 57 | * `topic.on('update')` - Emitted when a peer discovery cycle has finished 58 | * `topic.on('peer', peer)` - Emitted when a peer is found 59 | * `topic.on('close')` - Emitted when this topic is fully closed 60 | 61 | It is up to you to call `.destroy()` when you don't wanna look for anymore peers. 62 | Note that the same peer might be emitted multiple times. 63 | 64 | An update cycle indicates that you are done querying the DHT and that 65 | the topic instance will sleep for a bit (~5-10min) before querying it again 66 | 67 | #### `topic = d.announce(key, options)` 68 | 69 | Start announcing a `key`. `topic` has the same API as lookup. 70 | 71 | Options include: 72 | 73 | ```js 74 | { 75 | // If you set port: 0 the port of the discovery socket is used. 76 | port: (port you want to announce), 77 | localPort: (LAN port you wanna announce), 78 | // Set to true to also do a lookup in parallel. 79 | // More efficient than calling .lookup() in parallel yourself. 80 | lookup: false 81 | } 82 | ``` 83 | 84 | When the topic is destroyed the port will be explicitly unannounced 85 | from the network as well 86 | 87 | #### `d.lookupOne(key, cb)` 88 | 89 | Find a single peer and returns that to the callback. 90 | 91 | #### `d.ping(cb)` 92 | 93 | Ping all bootstrap servers. Returns an array of results: 94 | 95 | ``` 96 | [ 97 | { 98 | bootstrap: (bootstrap node that replied), 99 | rtt: (round trip time in ms), 100 | pong: { 101 | host: (your ip), 102 | port: (your port) 103 | } 104 | } 105 | ] 106 | ``` 107 | 108 | If your IP and port is consistent across the bootstrap nodes 109 | holepunching *usually* works. 110 | 111 | #### `d.holepunch(peer, cb)` 112 | 113 | UDP holepunch to another peer. 114 | 115 | #### `d.flush(cb)` 116 | 117 | Call the callback when all pending DHT operations are fully flushed. 118 | 119 | #### `d.destroy()` 120 | 121 | Fully destroy the discovery instance, and it's underlying resources. 122 | Will *also* destroy the socket you passed in the constructor. 123 | 124 | All running announces will be unannounced as well. 125 | 126 | Will emit `close` when the instance if fully closed. 127 | 128 | ## License 129 | 130 | MIT 131 | -------------------------------------------------------------------------------- /example.js: -------------------------------------------------------------------------------- 1 | const discovery = require('./') 2 | 3 | const d = discovery() 4 | const k = Buffer.alloc(32) 5 | 6 | // const topic = d.lookup(k) 7 | // topic.on('peer', peer => console.log('peer:', peer)) 8 | 9 | d.announce(k, { 10 | port: 10000, 11 | lookup: true 12 | }).on('peer', console.log) 13 | 14 | const ann = d.announce(k, { 15 | port: 10101 16 | }) 17 | 18 | ann.once('update', function () { 19 | console.log('onupdate') 20 | ann.destroy() 21 | ann.once('close', function () { 22 | console.log('onclose') 23 | const d2 = discovery() 24 | 25 | d2.lookup(k) 26 | .on('peer', console.log) 27 | }) 28 | }) 29 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const dht = require('@hyperswarm/dht') 2 | const multicast = require('multicast-dns') 3 | const { EventEmitter } = require('events') 4 | const crypto = require('crypto') 5 | 6 | const EMPTY = [] 7 | 8 | module.exports = opts => new Discovery(opts) 9 | 10 | class Topic extends EventEmitter { 11 | constructor (discovery, key, opts) { 12 | super() 13 | 14 | if (!opts) opts = {} 15 | 16 | this.key = key 17 | this.announce = opts.announce || null 18 | this.lookup = opts.lookup || null 19 | this.destroyed = false 20 | this.id = Buffer.concat([Buffer.from('id='), crypto.randomBytes(32)]) 21 | 22 | const port = opts.localPort || 0 23 | const name = discovery._domain(key) 24 | 25 | this._flush = [] 26 | this._flushPending = false 27 | this._discovery = discovery 28 | this._timeoutDht = null 29 | this._timeoutMdns = null 30 | this._stream = null 31 | this._domain = name 32 | this._answer = port 33 | ? { type: 'SRV', name, data: { target: '0.0.0.0', port } } 34 | : null 35 | this._idAnswer = { type: 'TXT', name, data: [ this.id ] } 36 | this._startDht() 37 | if (!this.announce || opts.lookup) this._startMdns() 38 | if (this.announce) process.nextTick(this._fireAnnounce.bind(this)) 39 | } 40 | 41 | _fireAnnounce () { // firing once right away make lookup, then announce much faster 42 | this._discovery._onmdnsquery({ 43 | questions: [{ type: 'SRV', name: this._domain }], 44 | answers: [] 45 | }, null) 46 | } 47 | 48 | update () { 49 | if (this.destroyed) return 50 | if (this._timeoutDht) { 51 | clearTimeout(this._timeoutDht) 52 | this._timeoutDht = null 53 | this._startDht() 54 | } 55 | clearTimeout(this._timeoutMdns) 56 | this._startMdns() 57 | } 58 | 59 | flush (cb) { 60 | if (this._flushPending) { 61 | this._flush.push(cb) 62 | } else { 63 | cb(null) 64 | } 65 | } 66 | 67 | destroy () { 68 | if (this.destroyed) return 69 | this.destroyed = true 70 | 71 | this._stopDht() 72 | clearTimeout(this._timeoutMdns) 73 | 74 | const set = this._discovery._domains.get(this._domain) 75 | set.delete(this) 76 | if (!set.size) this._discovery._domains.delete(this._domain) 77 | 78 | const onclose = this.emit.bind(this, 'close') 79 | 80 | if (!this.announce) return process.nextTick(onclose) 81 | this._discovery.dht.unannounce(this.key, this.announce, onclose) 82 | } 83 | 84 | _ondhtdata (data) { 85 | if (this.destroyed) return 86 | 87 | const topic = this.key 88 | const referrer = data.node 89 | const to = data.to 90 | 91 | for (const peer of (data.localPeers || EMPTY)) { 92 | this.emit('peer', { port: peer.port, host: peer.host, local: true, to, referrer: null, topic }) 93 | } 94 | for (const peer of (data.peers || EMPTY)) { 95 | this.emit('peer', { port: peer.port, host: peer.host, local: false, to, referrer, topic }) 96 | } 97 | } 98 | 99 | _startMdns () { 100 | const self = this 101 | 102 | const query = { 103 | questions: [{ 104 | type: 'SRV', 105 | name: this._domain 106 | }] 107 | } 108 | 109 | loop() 110 | 111 | function loop () { 112 | self._discovery.mdns.query(query) 113 | self._timeoutMdns = self._discovery._notify(loop, true) 114 | } 115 | } 116 | 117 | _stopDht () { 118 | clearTimeout(this._timeoutDht) 119 | this._timeoutDht = null 120 | if (this._stream) this._stream.destroy() 121 | 122 | const flush = this._flush 123 | this._flush = [] 124 | for (const cb of flush) cb(null) 125 | } 126 | 127 | _startDht () { 128 | const dht = this._discovery.dht 129 | const self = this 130 | const key = this.key 131 | const ondata = this._ondhtdata.bind(this) 132 | 133 | loop() 134 | 135 | function loop () { 136 | var called = false 137 | var flushed = false 138 | 139 | let maxReplies = 1 140 | let maxCount = 0 141 | let maxLocalReplies = 1 142 | let maxLocalCount = 0 143 | 144 | const ann = self.announce 145 | const stream = ann ? dht.announce(key, ann) : dht.lookup(key, self.lookup) 146 | 147 | self.emit('updating') 148 | 149 | self._timeoutDht = null 150 | self._flushPending = true 151 | self._stream = stream 152 | 153 | stream.on('data', function (data) { 154 | if (data.peers) { 155 | if (data.peers.length > maxReplies && data.peers.length < 16) { 156 | maxReplies = data.peers.length 157 | maxCount = 1 158 | } else if (data.peers.length >= maxReplies) { 159 | maxCount++ 160 | } 161 | } 162 | if (data.localPeers) { 163 | if (data.localPeers.length > maxReplies && data.localPeers.length < 16) { 164 | maxLocalReplies = data.localPeers.length 165 | maxLocalCount = 1 166 | } else if (data.localPeers.length >= maxLocalReplies) { 167 | maxLocalCount++ 168 | } 169 | } 170 | 171 | ondata(data) 172 | if (!flushed && (maxLocalCount >= 6 || maxCount >= 6)) onflush() 173 | }) 174 | 175 | stream.on('error', done) 176 | stream.on('end', done) 177 | stream.on('close', done) 178 | 179 | function done (err) { 180 | if (called || self.destroyed) return 181 | self._stream = null 182 | called = true 183 | self.emit('update', err) 184 | self._timeoutDht = self._discovery._notify(loop, false) 185 | onflush(err) 186 | } 187 | 188 | function onflush (err) { 189 | if (flushed) return 190 | flushed = true 191 | self._flushPending = false 192 | const flush = self._flush 193 | self._flush = [] 194 | for (const cb of flush) cb(null, !err) 195 | } 196 | } 197 | } 198 | } 199 | 200 | class Discovery extends EventEmitter { 201 | constructor (opts) { 202 | super() 203 | 204 | if (!opts) opts = {} 205 | 206 | if (!('adaptive' in opts)) { 207 | // if ephemeral is undefined, null or anything other than a boolean 208 | // then this signifies adaptive ephemerality 209 | opts.adaptive = typeof opts.ephemeral !== 'boolean' 210 | } 211 | // ephemeral defaults to true in discovery but defaults to false in dht 212 | opts.ephemeral = opts.ephemeral !== false 213 | 214 | this.destroyed = false 215 | this.dht = dht(opts) 216 | this.mdns = opts.multicast || multicast() 217 | 218 | this.mdns.on('query', this._onmdnsquery.bind(this)) 219 | this.mdns.on('response', this._onmdnsresponse.bind(this)) 220 | 221 | const domain = opts.domain || 'hyperswarm.local' 222 | 223 | this._tld = '.' + domain 224 | this._domains = new Map() 225 | this._bootstrap = this.dht.bootstrapNodes 226 | } 227 | get ephemeral () { 228 | return this.dht.ephemeral 229 | } 230 | ping (cb) { 231 | const res = [] 232 | const len = this._bootstrap.length 233 | 234 | if (!len) { 235 | return process.nextTick(cb, new Error('No bootstrap nodes available')) 236 | } 237 | 238 | var missing = len 239 | const start = Date.now() 240 | 241 | for (const bootstrap of this._bootstrap) { 242 | this.dht.ping(bootstrap, function (_, pong) { 243 | if (pong) res.push({ bootstrap, rtt: Date.now() - start, pong }) 244 | if (--missing) return 245 | if (!res.length) return cb(new Error('All bootstrap nodes failed')) 246 | cb(null, res) 247 | }) 248 | } 249 | } 250 | 251 | holepunchable (cb) { 252 | this.ping(function (err, res) { 253 | if (err) return cb(err) 254 | if (res.length < 2) return cb(new Error('Not enough bootstrap nodes replied')) 255 | const first = res[0].pong 256 | for (var i = 1; i < res.length; i++) { 257 | const pong = res[i].pong 258 | if (pong.host !== first.host || pong.port !== first.port) { 259 | return cb(null, false) 260 | } 261 | } 262 | 263 | cb(null, true) 264 | }) 265 | } 266 | 267 | flush (cb) { 268 | let missing = 1 269 | 270 | for (const set of this._domains.values()) { 271 | for (const topic of set) { 272 | missing++ 273 | topic.flush(onflush) 274 | } 275 | } 276 | 277 | onflush() 278 | 279 | function onflush () { 280 | if (!--missing) cb() 281 | } 282 | } 283 | 284 | lookupOne (key, opts, cb) { 285 | if (typeof opts === 'function') return this.lookupOne(key, null, opts) 286 | const onclose = () => cb(new Error('Lookup failed')) 287 | 288 | this.lookup(key, opts) 289 | .on('close', onclose) 290 | .once('peer', onpeer) 291 | 292 | function onpeer (peer) { 293 | this.removeListener('close', onclose) 294 | this.destroy() 295 | 296 | cb(null, peer) 297 | } 298 | } 299 | 300 | lookup (key, opts) { 301 | if (this.destroyed) throw new Error('Discovery instance is destroyed') 302 | 303 | return this._topic(key, { 304 | lookup: opts || null 305 | }) 306 | } 307 | 308 | announce (key, opts) { 309 | if (this.destroyed) throw new Error('Discovery instance is destroyed') 310 | 311 | const topic = this._topic(key, { 312 | localPort: opts.localPort || opts.port || 0, 313 | lookup: opts && opts.lookup, 314 | announce: { 315 | port: opts.port || 0, 316 | localAddress: opts.localAddress 317 | } 318 | }) 319 | 320 | return topic 321 | } 322 | 323 | holepunch (peer, cb) { 324 | if (!peer.referrer) return process.nextTick(new Error('Referrer needed to holepunch')) 325 | this.dht.holepunch(peer, cb) 326 | } 327 | 328 | destroy (opts) { 329 | if (this.destroyed) return 330 | this.destroyed = true 331 | 332 | if (!opts) opts = {} 333 | 334 | const self = this 335 | var missing = 1 336 | 337 | this.mdns.destroy() 338 | if (opts.force) return process.nextTick(done) 339 | 340 | for (const set of this._domains.values()) { 341 | for (const topic of set) { 342 | missing++ 343 | topic.destroy() 344 | topic.on('close', done) 345 | } 346 | } 347 | 348 | process.nextTick(done) 349 | 350 | function done () { 351 | if (--missing) return 352 | self.dht.destroy() 353 | self.emit('close') 354 | } 355 | } 356 | 357 | _getId (res, name) { 358 | for (const a of res.answers) { 359 | if (a.type === 'TXT' && a.name === name && a.data.length) { 360 | return a.data[0] 361 | } 362 | } 363 | return null 364 | } 365 | 366 | _topic (key, opts) { 367 | const topic = new Topic(this, key, opts) 368 | const domain = this._domain(key) 369 | if (!this._domains.has(domain)) { 370 | this._domains.set(domain, new Set()) 371 | } 372 | const set = this._domains.get(domain) 373 | set.add(topic) 374 | return topic 375 | } 376 | 377 | _onmdnsresponse (res, rinfo) { 378 | for (const a of res.answers) { 379 | const set = a.type === 'SRV' && this._domains.get(a.name) 380 | if (!set) continue 381 | 382 | const host = a.data.target === '0.0.0.0' 383 | ? rinfo.address 384 | : a.data.target 385 | const id = this._getId(res, a.name) 386 | 387 | for (const topic of set) { 388 | if (id && id.equals(topic.id)) continue 389 | topic.emit('peer', { port: a.data.port, host, local: true, referrer: null, topic: topic.key }) 390 | } 391 | } 392 | } 393 | 394 | _onmdnsquery (res, rinfo) { 395 | const r = { answers: [] } 396 | 397 | for (const q of res.questions) { 398 | const set = q.type === 'SRV' && this._domains.get(q.name) 399 | if (!set) continue 400 | 401 | const id = this._getId(res, q.name) 402 | for (const topic of set) { 403 | if (id && topic.id.equals(id)) continue 404 | if (topic._answer) { 405 | r.answers.push(topic._answer) 406 | r.answers.push(topic._idAnswer) 407 | } 408 | } 409 | } 410 | 411 | if (r.answers.length && rinfo) { 412 | r.answers.push({ 413 | type: 'A', 414 | name: 'referrer' + this._tld, 415 | data: rinfo.address 416 | }) 417 | } 418 | if (r.answers.length) { 419 | this.mdns.response(r) 420 | } 421 | } 422 | 423 | _domain (key) { 424 | return key.slice(0, 20).toString('hex') + this._tld 425 | } 426 | 427 | _notify (fn, eager) { 428 | const wait = eager 429 | ? 30000 430 | : 300000 431 | return setTimeout(fn, Math.floor(wait + Math.random() * wait)) 432 | } 433 | } 434 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@hyperswarm/discovery", 3 | "version": "2.0.1", 4 | "description": "The Hyperswarm discovery stack", 5 | "main": "index.js", 6 | "dependencies": { 7 | "@hyperswarm/dht": "^4.0.0", 8 | "multicast-dns": "^7.2.2", 9 | "timeout-refresh": "^1.0.2" 10 | }, 11 | "devDependencies": { 12 | "dht-rpc": "^4.1.2", 13 | "standard": "^12.0.1", 14 | "tape": "^4.9.1" 15 | }, 16 | "scripts": { 17 | "test": "standard && tape test.js" 18 | }, 19 | "repository": { 20 | "type": "git", 21 | "url": "https://github.com/hyperswarm/discovery.git" 22 | }, 23 | "author": "Mathias Buus (@mafintosh)", 24 | "license": "MIT", 25 | "bugs": { 26 | "url": "https://github.com/hyperswarm/discovery/issues" 27 | }, 28 | "homepage": "https://github.com/hyperswarm/discovery" 29 | } 30 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | const crypto = require('crypto') 2 | const test = require('tape') 3 | const dht = require('dht-rpc') 4 | const discovery = require('./') 5 | 6 | const bootstrap = dht({ ephemeral: true }) 7 | var bootstrapPort 8 | 9 | test('setup bootstrap', t => { 10 | bootstrap.listen(null, err => { 11 | t.error(err) 12 | 13 | bootstrapPort = bootstrap.socket.address().port 14 | t.pass(`bootstrap listening at 127.0.0.1:${bootstrapPort}`) 15 | t.end() 16 | }) 17 | }) 18 | 19 | test('ping', t => { 20 | const d1 = inst() 21 | d1.ping((err, res) => { 22 | t.error(err) 23 | 24 | t.equal(res.length, 1, 'one bootstrap pinged') 25 | t.equal(res[0].bootstrap.port, bootstrapPort, 'pinged bootstrap port is correct') 26 | t.equal(res[0].bootstrap.host, '127.0.0.1', 'pinged bootstrap host is correct') 27 | t.ok(typeof res[0].rtt === 'number', 'ping rtt number is included') 28 | t.equal(res[0].pong.host, '127.0.0.1', 'pinged pong host is correct') 29 | t.ok(typeof res[0].pong.port === 'number', 'pinged pong port number is included') 30 | 31 | d1.destroy() 32 | t.end() 33 | }) 34 | }) 35 | 36 | test('announce & lookup', t => { 37 | const key = crypto.randomBytes(32) 38 | const d1 = inst() 39 | const d2 = inst() 40 | 41 | const to = setTimeout(() => { 42 | t.fail('Timed out waiting for lookup') 43 | d1.destroy() 44 | d2.destroy() 45 | t.end() 46 | }, 5e3) 47 | 48 | const port = allocPort() 49 | d1.announce(key, { port }) 50 | const lookup = d2.lookup(key) 51 | lookup.on('peer', (peer) => { 52 | clearTimeout(to) 53 | 54 | t.equal(peer.port, port, 'peer port is as expected') 55 | t.ok(typeof peer.host === 'string', 'peer host string is included') 56 | t.equal(peer.local, true, 'peer was local') 57 | t.equal(peer.referrer, null, 'peer referrer is null') 58 | 59 | d1.destroy() 60 | d2.destroy() 61 | t.end() 62 | }) 63 | }) 64 | 65 | test('announce & lookupOne', t => { 66 | const key = crypto.randomBytes(32) 67 | const d1 = inst() 68 | const d2 = inst() 69 | 70 | const to = setTimeout(() => { 71 | t.fail('Timed out waiting for lookup') 72 | d1.destroy() 73 | d2.destroy() 74 | t.end() 75 | }, 5e3) 76 | 77 | const port = allocPort() 78 | d1.announce(key, { port }) 79 | d2.lookupOne(key, (err, peer) => { 80 | clearTimeout(to) 81 | t.error(err) 82 | 83 | t.equal(peer.port, port, 'peer port is as expected') 84 | t.ok(typeof peer.host === 'string', 'peer host string is included') 85 | t.equal(peer.local, true, 'peer was local') 86 | t.equal(peer.referrer, null, 'peer referrer is null') 87 | 88 | d1.destroy() 89 | d2.destroy() 90 | t.end() 91 | }) 92 | }) 93 | 94 | test('announce & announce with lookup = true', t => { 95 | const key = crypto.randomBytes(32) 96 | const d1 = inst() 97 | const d2 = inst() 98 | 99 | const to = setTimeout(() => { 100 | t.fail('Timed out waiting for peers') 101 | d1.destroy() 102 | d2.destroy() 103 | t.end() 104 | }, 5e3) 105 | 106 | const port1 = allocPort() 107 | const ann1 = d1.announce(key, { port: port1, lookup: true }) 108 | const port2 = allocPort() 109 | const ann2 = d2.announce(key, { port: port2, lookup: true }) 110 | 111 | var hits = 2 112 | function onPeer (port) { 113 | return (peer) => { 114 | t.equal(peer.port, port, 'peer port is as expected') 115 | t.ok(typeof peer.host === 'string', 'peer host string is included') 116 | t.equal(peer.local, true, 'peer was local') 117 | t.equal(peer.referrer, null, 'peer referrer is null') 118 | 119 | if (!(--hits)) { 120 | clearTimeout(to) 121 | d1.destroy() 122 | d2.destroy() 123 | t.end() 124 | } 125 | } 126 | } 127 | 128 | ann1.on('peer', onPeer(port2)) 129 | ann2.on('peer', onPeer(port1)) 130 | }) 131 | 132 | test('flush discovery', t => { 133 | const key = crypto.randomBytes(32) 134 | const d1 = inst({ ephemeral: false }) 135 | const d2 = inst({ ephemeral: false }) 136 | 137 | d2.dht.bootstrap(() => { 138 | d1.flush(function () { 139 | d1.announce(key, { port: 1001 }) 140 | d1.announce(key, { port: 1002 }) 141 | d1.flush(function () { 142 | const peers = [] 143 | d1.lookup(key).on('peer', function (peer) { 144 | peers.push(peer.port) 145 | 146 | if (peers.length === 4) { 147 | peers.sort() 148 | t.same(peers, [ 149 | 1001, 150 | 1001, 151 | 1002, 152 | 1002 153 | ]) 154 | d1.destroy() 155 | d2.destroy() 156 | t.end() 157 | } 158 | }) 159 | }) 160 | }) 161 | }) 162 | }) 163 | 164 | test.onFinish(() => { 165 | bootstrap.destroy() 166 | }) 167 | 168 | function inst (opts = {}) { 169 | return discovery(Object.assign({ bootstrap: [`127.0.0.1:${bootstrapPort}`] }, opts)) 170 | } 171 | 172 | var nextPort = 10000 173 | function allocPort () { 174 | return nextPort++ 175 | } 176 | --------------------------------------------------------------------------------