udx-native
Version:
udx is reliable, multiplexed, and congestion-controlled streams over udp
307 lines (233 loc) • 7.45 kB
JavaScript
const events = require('events')
const b4a = require('b4a')
const binding = require('../binding')
const ip = require('./ip')
module.exports = class UDXSocket extends events.EventEmitter {
constructor (udx, opts = {}) {
super()
this.udx = udx
this._handle = b4a.allocUnsafe(binding.sizeof_udx_napi_socket_t)
this._inited = false
this._host = null
this._family = 0
this._ipv6Only = opts.ipv6Only === true
this._reuseAddress = opts.reuseAddress === true
this._port = 0
this._reqs = []
this._free = []
this._closing = null
this._closed = false
this._view64 = new BigUint64Array(this._handle.buffer, this._handle.byteOffset, this._handle.byteLength >> 3)
this.streams = new Set()
this.userData = null
}
get bound () {
return this._port !== 0
}
get closing () {
return this._closing !== null
}
get idle () {
return this.streams.size === 0
}
get busy () {
return this.streams.size > 0
}
get bytesTransmitted () {
if (this._inited !== true) return 0
return Number(this._view64[binding.offsetof_udx_socket_t_bytes_tx >> 3])
}
get packetsTransmitted () {
if (this._inited !== true) return 0
return Number(this._view64[binding.offsetof_udx_socket_t_packets_tx >> 3])
}
get bytesReceived () {
if (this._inited !== true) return 0
return Number(this._view64[binding.offsetof_udx_socket_t_bytes_rx >> 3])
}
get packetsReceived () {
if (this._inited !== true) return 0
return Number(this._view64[binding.offsetof_udx_socket_t_packets_rx >> 3])
}
get packetsDroppedByKernel () {
if (this._inited !== true) return 0
return Number(this._view64[binding.offsetof_udx_socket_t_packets_dropped_by_kernel >> 3])
}
toJSON () {
return {
bound: this.bound,
closing: this.closing,
streams: this.streams.size,
address: this.address(),
ipv6Only: this._ipv6Only,
reuseAddress: this._reuseAddress,
idle: this.idle,
busy: this.busy
}
}
_init () {
if (this._inited) return
binding.udx_napi_socket_init(this.udx._handle, this._handle, this,
this._onsend,
this._onmessage,
this._onclose,
this._reallocMessage
)
this._inited = true
}
_onsend (id, err) {
const req = this._reqs[id]
const onflush = req.onflush
req.buffer = null
req.onflush = null
this._free.push(id)
onflush(err >= 0)
// gc the free list
if (this._free.length >= 16 && this._free.length === this._reqs.length) {
this._free = []
this._reqs = []
}
}
_onmessage (len, port, host, family) {
this.emit('message', this.udx._consumeMessage(len), { host, family, port })
return this.udx._buffer
}
_onclose () {
this.emit('close')
}
_reallocMessage () {
return this.udx._reallocMessage()
}
_onidle () {
this.emit('idle')
}
_onbusy () {
this.emit('busy')
}
_addStream (stream) {
if (this.streams.has(stream)) return false
this.streams.add(stream)
if (this.streams.size === 1) this._onbusy()
return true
}
_removeStream (stream) {
if (!this.streams.has(stream)) return false
this.streams.delete(stream)
const closed = this._closeMaybe()
if (this.idle && !closed) this._onidle()
return true
}
address () {
if (!this.bound) return null
return { host: this._host, family: this._family, port: this._port }
}
bind (port, host) {
if (this.bound) throw new Error('Already bound')
if (this.closing) throw new Error('Socket is closed')
if (!port) port = 0
let flags = 0
if (this._ipv6Only) flags |= binding.UV_UDP_IPV6ONLY
if (this._reuseAddress) flags |= binding.UV_UDP_REUSEADDR
let family
if (host) {
family = ip.isIP(host)
if (!family) throw new Error(`${host} is not a valid IP address`)
if (!this._inited) this._init()
this._port = binding.udx_napi_socket_bind(this._handle, port, host, family, flags)
} else {
if (!this._inited) this._init()
try {
host = '::'
family = 6
this._port = binding.udx_napi_socket_bind(this._handle, port, host, family, flags)
} catch {
host = '0.0.0.0'
family = 4
this._port = binding.udx_napi_socket_bind(this._handle, port, host, family, flags)
}
}
this._host = host
this._family = family
this.emit('listening')
}
async close () {
if (this._closing) return this._closing
this._closing = new Promise(resolve => this.once('close', resolve))
this._closeMaybe()
return this._closing
}
_closeMaybe () {
if (this._closed || this._closing === null) return this._closed
if (!this._inited) {
this._closed = true
this.emit('close')
return true
}
if (this.idle) {
binding.udx_napi_socket_close(this._handle)
this._closed = true
}
return this._closed
}
setTTL (ttl) {
if (!this._inited) throw new Error('Socket not active')
binding.udx_napi_socket_set_ttl(this._handle, ttl)
}
getRecvBufferSize () {
if (!this._inited) throw new Error('Socket not active')
return binding.udx_napi_socket_get_recv_buffer_size(this._handle)
}
setRecvBufferSize (size) {
if (!this._inited) throw new Error('Socket not active')
return binding.udx_napi_socket_set_recv_buffer_size(this._handle, size)
}
getSendBufferSize () {
if (!this._inited) throw new Error('Socket not active')
return binding.udx_napi_socket_get_send_buffer_size(this._handle)
}
setSendBufferSize (size) {
if (!this._inited) throw new Error('Socket not active')
return binding.udx_napi_socket_set_send_buffer_size(this._handle, size)
}
addMembership (group, ifaceAddress) {
if (!this._inited) throw new Error('Socket not active')
return binding.udx_napi_socket_set_membership(this._handle, group, ifaceAddress || '', true)
}
dropMembership (group, ifaceAddress) {
if (!this._inited) throw new Error('Socket not active')
return binding.udx_napi_socket_set_membership(this._handle, group, ifaceAddress || '', false)
}
async send (buffer, port, host, ttl) {
if (this.closing) return false
if (!host) host = '127.0.0.1'
const family = ip.isIP(host)
if (!family) throw new Error(`${host} is not a valid IP address`)
if (!this.bound) this.bind(0)
const id = this._allocSend()
const req = this._reqs[id]
req.buffer = buffer
const promise = new Promise((resolve) => {
req.onflush = resolve
})
binding.udx_napi_socket_send_ttl(this._handle, req.handle, id, buffer, port, host, family, ttl || 0)
return promise
}
trySend (buffer, port, host, ttl) {
if (this.closing) return
if (!host) host = '127.0.0.1'
const family = ip.isIP(host)
if (!family) throw new Error(`${host} is not a valid IP address`)
if (!this.bound) this.bind(0)
const id = this._allocSend()
const req = this._reqs[id]
req.buffer = buffer
req.onflush = noop
binding.udx_napi_socket_send_ttl(this._handle, req.handle, id, buffer, port, host, family, ttl || 0)
}
_allocSend () {
if (this._free.length > 0) return this._free.pop()
const handle = b4a.allocUnsafe(binding.sizeof_udx_socket_send_t)
return this._reqs.push({ handle, buffer: null, onflush: null }) - 1
}
}
function noop () {}