hyperdht
Version:
The DHT powering Hyperswarm
220 lines (171 loc) • 4.37 kB
JavaScript
const b4a = require('b4a')
const LINGER_TIME = 3000
module.exports = class SocketPool {
constructor (dht, host) {
this._dht = dht
this._sockets = new Map()
this._lingering = new Set() // updated by the ref
this._host = host
this.routes = new SocketRoutes(this)
}
_onmessage (ref, data, address) {
this._dht.onmessage(ref.socket, data, address)
}
_add (ref) {
this._sockets.set(ref.socket, ref)
}
_remove (ref) {
this._sockets.delete(ref.socket)
this._lingering.delete(ref)
}
lookup (socket) {
return this._sockets.get(socket) || null
}
setReusable (socket, bool) {
const ref = this.lookup(socket)
if (ref) ref.reusable = bool
}
acquire () {
// TODO: Enable socket reuse
return new SocketRef(this)
}
async destroy () {
const closing = []
for (const ref of this._sockets.values()) {
ref._unlinger()
closing.push(ref.socket.close())
}
await Promise.allSettled(closing)
}
}
class SocketRoutes {
constructor (pool) {
this._pool = pool
this._routes = new Map()
}
add (publicKey, rawStream) {
if (rawStream.socket) this._onconnect(publicKey, rawStream)
else rawStream.on('connect', this._onconnect.bind(this, publicKey, rawStream))
}
get (publicKey) {
const id = b4a.toString(publicKey, 'hex')
const route = this._routes.get(id)
if (!route) return null
return route
}
_onconnect (publicKey, rawStream) {
const id = b4a.toString(publicKey, 'hex')
const socket = rawStream.socket
let route = this._routes.get(id)
if (!route) {
const gc = () => {
if (this._routes.get(id) === route) this._routes.delete(id)
socket.removeListener('close', gc)
}
route = {
socket,
address: { host: rawStream.remoteHost, port: rawStream.remotePort },
gc
}
this._routes.set(id, route)
socket.on('close', gc)
}
this._pool.setReusable(socket, true)
rawStream.on('error', () => {
this._pool.setReusable(socket, false)
if (!route) route = this._routes.get(id)
if (route && route.socket === socket) route.gc()
})
}
}
// TODO: we should just make some "user data" object on udx to allow to attach this info
class SocketRef {
constructor (pool) {
this._pool = pool
// Events
this.onholepunchmessage = noop
// Whether it should teardown immediately or wait a bit
this.reusable = false
this.socket = pool._dht.udx.createSocket()
this.socket
.on('close', this._onclose.bind(this))
.on('message', this._onmessage.bind(this))
.on('idle', this._onidle.bind(this))
.on('busy', this._onbusy.bind(this))
.bind(0, this._pool._host)
this._refs = 1
this._released = false
this._closed = false
this._timeout = null
this._wasBusy = false
this._pool._add(this)
}
_onclose () {
this._pool._remove(this)
}
_onmessage (data, address) {
if (data.byteLength > 1) {
this._pool._onmessage(this, data, address)
} else {
this.onholepunchmessage(data, address, this)
}
}
_onidle () {
this._closeMaybe()
}
_onbusy () {
this._wasBusy = true
this._unlinger()
}
_reset () {
this.onholepunchmessage = noop
}
_closeMaybe () {
if (this._refs === 0 && this.socket.idle && !this._timeout) this._close()
}
_lingeringClose () {
this._pool._lingering.delete(this)
this._timeout = null
this._closeMaybe()
}
_close () {
this._unlinger()
if (this.reusable && this._wasBusy) {
this._wasBusy = false
this._pool._lingering.add(this)
this._timeout = setTimeout(this._lingeringClose.bind(this), LINGER_TIME)
return
}
this._closed = true
this.socket.close()
}
_unlinger () {
if (this._timeout !== null) {
clearTimeout(this._timeout)
this._pool._lingering.delete(this)
this._timeout = null
}
}
get free () {
return this._refs === 0
}
active () {
this._refs++
this._unlinger()
}
inactive () {
this._refs--
this._closeMaybe()
}
address () {
return this.socket.address()
}
release () {
if (this._released) return
this._released = true
this._reset()
this._refs--
this._closeMaybe()
}
}
function noop () {}