UNPKG

hyperdht

Version:
295 lines (233 loc) 7.47 kB
const safetyCatch = require('safety-catch') const c = require('compact-encoding') const Signal = require('signal-promise') const { encodeUnslab } = require('./encode') const Sleeper = require('./sleeper') const m = require('./messages') const Persistent = require('./persistent') const { COMMANDS } = require('./constants') const MIN_ACTIVE = 3 module.exports = class Announcer { constructor (dht, keyPair, target, opts = {}) { this.dht = dht this.keyPair = keyPair this.target = target this.relays = [] this.relayAddresses = [] this.stopped = false this.suspended = false this.record = encodeUnslab(m.peer, { publicKey: keyPair.publicKey, relayAddresses: [] }) this.online = new Signal() this._refreshing = false this._closestNodes = null this._active = null this._sleeper = new Sleeper() this._resumed = new Signal() this._signAnnounce = opts.signAnnounce || Persistent.signAnnounce this._signUnannounce = opts.signUnannounce || Persistent.signUnannounce this._updating = null this._activeQuery = null this._unannouncing = null this._serverRelays = [ new Map(), new Map(), new Map() ] } isRelay (addr) { const id = addr.host + ':' + addr.port const [a, b, c] = this._serverRelays return a.has(id) || b.has(id) || c.has(id) } async suspend ({ log = noop } = {}) { if (this.suspended) return this.suspended = true log('Suspending announcer') // Suspend has its own sleep logic // so we don't want to hang on this one this.online.notify() if (this._activeQuery) this._activeQuery.destroy() this._sleeper.resume() if (this._updating) await this._updating log('Suspending announcer (post update)') if (this.suspended === false || this.stopped) return log('Suspending announcer (pre unannounce)') await this._unannounceCurrent() log('Suspending announcer (post unannounce)') } resume () { if (!this.suspended) return this.suspended = false this.refresh() this._sleeper.resume() this._resumed.notify() } refresh () { if (this.stopped) return this._refreshing = true } async start () { if (this.stopped) return this._active = this._runUpdate() await this._active if (this.stopped) return this._active = this._background() } async stop () { this.stopped = true this.online.notify() // Break out of the _background loop if we're offline this._sleeper.resume() this._resumed.notify() await this._active await this._unannounceCurrent() } async _unannounceCurrent () { while (this._unannouncing !== null) await this._unannouncing const un = this._unannouncing = this._unannounceAll(this._serverRelays[2].values()) await this._unannouncing if (un === this._unannouncing) this._unannouncing = null } async _background () { while (!this.dht.destroyed && !this.stopped) { try { this._refreshing = false // ~5min +- for (let i = 0; i < 100 && !this.stopped && !this._refreshing && !this.suspended; i++) { const pings = [] for (const node of this._serverRelays[2].values()) { pings.push(this.dht.ping(node)) } const active = await resolved(pings) if (active < Math.min(pings.length, MIN_ACTIVE)) { this.refresh() // we lost too many relay nodes, retry all } if (this.stopped) return if (!this.suspended && !this._refreshing) await this._sleeper.pause(3000) } while (!this.stopped && this.suspended) await this._resumed.wait() if (!this.stopped) await this._runUpdate() while (!this.dht.online && !this.stopped && !this.suspended) { // Being offline can make _background repeat very quickly // So wait until we're back online await this.online.wait() } } catch (err) { safetyCatch(err) } } } async _runUpdate () { this._updating = this._update() await this._updating this._updating = null } async _update () { while (this._unannouncing) await this._unannouncing this._cycle() const q = this._activeQuery = this.dht.findPeer(this.target, { hash: false, nodes: this._closestNodes }) try { await q.finished() } catch { // ignore failures... } this._activeQuery = null if (this.stopped || this.suspended) return const ann = [] const replies = pickBest(q.closestReplies) const relays = [] const relayAddresses = [] if (!this.dht.firewalled) { const addr = this.dht.remoteAddress() if (addr) relayAddresses.push(addr) } for (const msg of replies) { ann.push(this._commit(msg, relays, relayAddresses)) } await Promise.allSettled(ann) if (this.stopped || this.suspended) return this._closestNodes = q.closestNodes this.relays = relays this.relayAddresses = relayAddresses const removed = [] for (const [key, value] of this._serverRelays[1]) { if (!this._serverRelays[2].has(key)) removed.push(value) } await this._unannounceAll(removed) } _unannounceAll (relays) { const unann = [] for (const r of relays) unann.push(this._unannounce(r)) return Promise.allSettled(unann) } async _unannounce (to) { const unann = { peer: { publicKey: this.keyPair.publicKey, relayAddresses: [] }, refresh: null, signature: null } const { from, token, value } = await this.dht.request({ token: null, command: COMMANDS.FIND_PEER, target: this.target, value: null }, to) if (!token || !from.id || !value) return unann.signature = await this._signUnannounce(this.target, token, from.id, unann, this.keyPair) await this.dht.request({ token, command: COMMANDS.UNANNOUNCE, target: this.target, value: c.encode(m.announce, unann) }, to) } async _commit (msg, relays, relayAddresses) { const ann = { peer: { publicKey: this.keyPair.publicKey, relayAddresses: [] }, refresh: null, signature: null } ann.signature = await this._signAnnounce(this.target, msg.token, msg.from.id, ann, this.keyPair) const res = await this.dht.request({ token: msg.token, command: COMMANDS.ANNOUNCE, target: this.target, value: c.encode(m.announce, ann) }, msg.from) if (res.error !== 0) return if (relayAddresses.length < 3) relayAddresses.push({ host: msg.from.host, port: msg.from.port }) relays.push({ relayAddress: msg.from, peerAddress: msg.to }) this._serverRelays[2].set(msg.from.host + ':' + msg.from.port, msg.from) } _cycle () { const tmp = this._serverRelays[0] this._serverRelays[0] = this._serverRelays[1] this._serverRelays[1] = this._serverRelays[2] this._serverRelays[2] = tmp tmp.clear() } } function resolved (ps) { let replied = 0 let ticks = ps.length + 1 return new Promise((resolve) => { for (const p of ps) p.then(push, tick) tick() function push (v) { replied++ tick() } function tick () { if (--ticks === 0) resolve(replied) } }) } function pickBest (replies) { // TODO: pick the ones closest to us RTT wise return replies.slice(0, 3) } function noop () {}