UNPKG

@socketsupply/socket

Version:

A Cross-Platform, Native Runtime for Desktop and Mobile Apps — Create apps using HTML, CSS, and JavaScript. Written from the ground up to be small and maintainable.

1,375 lines (1,148 loc) 35.4 kB
/** * @module dgram * * This module provides an implementation of UDP datagram sockets. It does * not (yet) provide any of the multicast methods or properties. * * Example usage: * ```js * import { createSocket } from 'socket:dgram' * ``` */ import { isArrayBufferView, isFunction, noop } from './util.js' import { murmur3, rand64 } from './crypto.js' import { InternalError } from './errors.js' import { AsyncResource } from './async/resource.js' import { Conduit } from './internal/conduit.js' import { EventEmitter } from './events.js' import diagnostics from './diagnostics.js' import { Buffer } from './buffer.js' import { isIPv4 } from './ip.js' import process from './process.js' import ipc from './ipc.js' import dns from './dns.js' import gc from './gc.js' import * as exports from './dgram.js' const BIND_STATE_UNBOUND = 0 const BIND_STATE_BINDING = 1 const BIND_STATE_BOUND = 2 const CONNECT_STATE_DISCONNECTED = 0 const CONNECT_STATE_CONNECTING = 1 const CONNECT_STATE_CONNECTED = 2 const MAX_PORT = 64 * 1024 const RECV_BUFFER = 1 const SEND_BUFFER = 0 const dc = diagnostics.channels.group('udp', [ 'send', 'send.start', 'send.end', 'message', 'connect', 'socket', 'close', 'bind' ]) function defaultCallback (socket, resource) { return (err) => { resource.runInAsyncScope(() => { if (err) { socket.emit('error', err) } }) } } function createDataListener (socket, resource) { // subscribe this socket to the firehose globalThis.addEventListener('data', ondata) return ondata function ondata ({ detail }) { const { err, data, source } = detail.params const buffer = detail.data if (err && err.id === socket.id) { return resource.runInAsyncScope(() => { return socket.emit('error', err) }) } if (!data || BigInt(data.id) !== socket.id) return if (source === 'udp.readStart') { if (buffer && buffer instanceof ArrayBuffer) { // @ts-ignore if (buffer.detached) { return } } const message = Buffer.from(buffer) const info = { ...data, family: getAddressFamily(data.address) } resource.runInAsyncScope(() => { socket.emit('message', message, info) }) dc.channel('message').publish({ socket, buffer: message, info }) } if (data.EOF) { globalThis.removeEventListener('data', ondata) } } } function destroyDataListener (socket) { if (typeof socket?.dataListener === 'function') { globalThis.removeEventListener('data', socket.dataListener) delete socket.dataListener } } function fromBufferList (list) { const newlist = new Array(list.length) for (let i = 0, l = list.length; i < l; i++) { const buf = list[i] if (typeof buf === 'string') { newlist[i] = Buffer.from(buf) } else if (!isArrayBufferView(buf)) { return null } else { newlist[i] = Buffer.from(buf.buffer, buf.byteOffset, buf.byteLength) } } return newlist } function getDefaultAddress (socket) { if (socket.type === 'udp6') return '::1' if (socket.type === 'udp4') return '0.0.0.0' return null } function getAddressFamily (address) { return isIPv4(address) ? 'IPv4' : 'IPv6' } function getSocketState (socket) { const result = ipc.sendSync('udp.getState', { id: socket.id }) if (result.err && result.err.code !== 'NOT_FOUND_ERR') { throw result.err } return result.data || null } // eslint-disable-next-line no-unused-vars function healhCheck (socket) { // @TODO(jwerle) } async function startReading (socket, callback) { let result = null if (!isFunction(callback)) { callback = noop } try { if (socket?.conduit?.isActive) { const opts = { route: 'udp.readStart' } if (!socket.conduit.send(opts, Buffer.from(''))) { console.warn('socket:dgram: Failed to send conduit payload') } result = { data: true } } else { result = await ipc.send('udp.readStart', { id: socket.id }) } callback(result.err, result.data) } catch (err) { callback(err) } return result } async function stopReading (socket, callback) { let result = null if (!isFunction(callback)) { callback = noop } try { result = await ipc.request('udp.readStop', { id: socket.id }) callback(result.err, result.data) } catch (err) { callback(err) } return result } async function getRecvBufferSize (socket, callback) { let result = null if (!isFunction(callback)) { callback = noop } try { result = await ipc.request('os.bufferSize', { id: socket.id, buffer: RECV_BUFFER }) callback(result.err, result.data) } catch (err) { callback(err) return { err } } return result } async function getSendBufferSize (socket, callback) { let result = null if (!isFunction(callback)) { callback = noop } try { result = await ipc.request('os.bufferSize', { id: socket.id, buffer: SEND_BUFFER }) callback(result.err, result.data) } catch (err) { callback(err) return { err } } return result } async function bind (socket, options, callback) { let result = null options = { ...options } if (!isFunction(callback)) { callback = noop } if (typeof options.address !== 'string') { options.address = getDefaultAddress(socket) } socket.state.bindState = BIND_STATE_BINDING if (typeof options.address === 'string' && !isIPv4(options.address)) { try { options.address = await dns.lookup(options.address, 4) } catch (err) { socket.state.bindState = BIND_STATE_UNBOUND callback(err) return { err } } } try { result = await ipc.request('udp.bind', { id: socket.id, port: options.port || 0, address: options.address, ipv6Only: Boolean(socket.state.ipv6Only), reuseAddr: Boolean(socket.state.reuseAddr) }) socket.state.bindState = BIND_STATE_BOUND if (socket.state.sendBufferSize) { await socket.setSendBufferSize(socket.state.sendBufferSize) } else { const result = await getSendBufferSize(socket) if (result.err) { callback(result.err) return { err: result.err } } socket.state.sendBufferSize = result.data.size } if (socket.state.recvBufferSize) { await socket.setRecvBufferSize(socket.state.recvBufferSize) } else { const result = await getRecvBufferSize(socket) if (result.err) { callback(result.err) return { err: result.err } } socket.state.recvBufferSize = result.data.size } callback(result.err, result.data) } catch (err) { socket.state.bindState = BIND_STATE_UNBOUND callback(err) return { err } } dc.channel('bind').publish({ socket, port: options.port || 0, address: options.address, ipv6Only: !!options.ipv6Only, reuseAddr: !!options.reuseAddr }) return result } async function connect (socket, options, callback) { let result = null options = { ...options } if (!isFunction(callback)) { callback = noop } if (typeof options.address !== 'string') { options.address = getDefaultAddress(socket) } socket.state.connectState = CONNECT_STATE_CONNECTING if (typeof options.address === 'string' && !isIPv4(options.address)) { try { options.address = await dns.lookup(options.address, 4) } catch (err) { socket.state.connectState = CONNECT_STATE_DISCONNECTED callback(err) return { err } } } const { err } = await bind(socket, { port: 0 }) if (err) { socket.state.connectState = CONNECT_STATE_DISCONNECTED callback(err) return { err } } try { result = await ipc.request('udp.connect', { id: socket.id, port: options?.port ?? 0, address: options?.address }) socket.state.connectState = CONNECT_STATE_CONNECTED if (socket.state.sendBufferSize) { await socket.setSendBufferSize(socket.state.sendBufferSize) } else { const result = await getSendBufferSize(socket) if (result.err) { callback(result.err) return { err } } socket.state.sendBufferSize = result.data.size } if (socket.state.recvBufferSize) { await socket.setRecvBufferSize(socket.state.recvBufferSize) } else { const result = await getRecvBufferSize(socket) if (result.err) { callback(result.err) return { err } } socket.state.recvBufferSize = result.data.size } callback(result.err, result.data) } catch (err) { socket.state.connectState = CONNECT_STATE_DISCONNECTED callback(err) return { err } } dc.channel('connect').publish({ socket, port: options?.port ?? 0, address: options?.address }) return result } function disconnect (socket, callback) { let result = null if (!isFunction(callback)) { callback = noop } if (socket.state.connectState === CONNECT_STATE_CONNECTED) { try { result = ipc.sendSync('udp.disconnect', { id: socket.id }) delete socket.state.remoteAddress socket.state.connectState = CONNECT_STATE_DISCONNECTED callback(result.err, result.data) } catch (err) { callback(err) return { err } } dc.channel('disconnect').publish({ socket }) } return result } async function send (socket, options, callback) { let result = null if (!isFunction(callback)) { callback = noop } options = { ...options } if (socket.state.connectState === CONNECT_STATE_DISCONNECTED) { // wait for bind to finish if (socket.state.bindState === BIND_STATE_BINDING) { const { err } = await new Promise((resolve, reject) => { socket.once('listening', () => resolve({})) socket.once('error', (err) => resolve({ err })) }) if (err) { callback(err) return { err } } } else if (socket.state.bindState === BIND_STATE_UNBOUND) { const { err } = await bind(socket, { port: 0 }) if (err) { callback(err) return { err } } } } // wait for connect to finish if (socket.state.connectState === CONNECT_STATE_CONNECTING) { const { err } = await new Promise((resolve, reject) => { socket.once('connect', () => resolve({})) socket.once('error', (err) => resolve({ err })) }) if (err) { callback(err) return { err } } } if ( !isIPv4(options.address) && typeof options.address === 'string' && socket.state.connectState !== CONNECT_STATE_CONNECTED ) { try { options.address = await dns.lookup(options.address, 4) } catch (err) { callback(err) return { err } } } // use local port/address if not given if (socket.state.bindState === BIND_STATE_BOUND) { const local = socket.address() if (!options.address) { options.address = local.address } if (!options.port) { options.port = local.port } } // use remote port/address if not given if (socket.state.connectState === CONNECT_STATE_CONNECTED) { const remote = socket.remoteAddress() if (!options.address) { options.address = remote.address } if (!options.port) { options.port = remote.port } } if (options.port && !options.address) { options.address = getDefaultAddress(socket) } try { dc.channel('send.start').publish({ socket, port: options.port, buffer: options.buffer, address: options.address }) if (socket?.conduit?.isActive) { const opts = { route: 'udp.send', port: options.port, address: options.address } socket.conduit.send(opts, options.buffer) result = { data: true } } else { result = await ipc.write('udp.send', { id: socket.id, port: options.port, address: options.address }, options.buffer) } if (result.data?.detached !== true) { callback(result.err, result.data) } } catch (err) { callback(err) return { err } } dc.channel('send.end').publish({ socket, port: options.port, buffer: options.buffer, address: options.address }) dc.channel('send').publish({ socket, port: options.port, buffer: options.buffer, address: options.address }) return result } async function close (socket, callback) { let result = null if (!isFunction(callback)) { callback = noop } socket.state.connectState = CONNECT_STATE_DISCONNECTED socket.state.bindState = BIND_STATE_UNBOUND await stopReading(socket) await disconnect(socket) try { result = await ipc.request('udp.close', { id: socket.id }) gc.unref(socket) delete socket.state.address delete socket.state.remoteAddress callback(result.err, result.data) } catch (err) { callback(err) return { err } } dc.channel('close').publish({ socket }) return result } function getPeerName (socket, callback) { let result = null if (!isFunction(callback)) { callback = noop } try { result = ipc.sendSync('udp.getPeerName', { id: socket.id }) callback(result.err, result.data) } catch (err) { callback(err) return { err } } return result } function getSockName (socket, callback) { let result = null if (!isFunction(callback)) { callback = noop } try { result = ipc.sendSync('udp.getSockName', { id: socket.id }) callback(result.err, result.data) } catch (err) { callback(err) return { err } } return result } /** * @typedef {Object} SocketOptions */ /** * Creates a `Socket` instance. * @param {string|Object} options - either a string ('udp4' or 'udp6') or an options object * @param {string=} options.type - The family of socket. Must be either 'udp4' or 'udp6'. Required. * @param {boolean=} [options.reuseAddr=false] - When true socket.bind() will reuse the address, even if another process has already bound a socket on it. Default: false. * @param {boolean=} [options.ipv6Only=false] - Default: false. * @param {number=} options.recvBufferSize - Sets the SO_RCVBUF socket value. * @param {number=} options.sendBufferSize - Sets the SO_SNDBUF socket value. * @param {AbortSignal=} options.signal - An AbortSignal that may be used to close a socket. * @param {function=} callback - Attached as a listener for 'message' events. Optional. * @return {Socket} */ export const createSocket = (options, callback) => new Socket(options, callback) /** * New instances of dgram.Socket are created using dgram.createSocket(). * The new keyword is not to be used to create dgram.Socket instances. */ export class Socket extends EventEmitter { #resource = null constructor (options, callback) { super() this.id = options?.id || rand64() this.knownIdWasGivenInSocketConstruction = Boolean(options?.id) if (typeof options === 'string') { options = { type: options } } options = { ...options } if (!['udp4', 'udp6'].includes(options.type)) { throw new ERR_SOCKET_BAD_TYPE() } this.#resource = new AsyncResource('Socket') this.#resource.handle = this this.type = options.type this.signal = options?.signal ?? null this.state = { recvBufferSize: options.recvBufferSize, sendBufferSize: options.sendBufferSize, bindState: BIND_STATE_UNBOUND, connectState: CONNECT_STATE_DISCONNECTED, reuseAddr: options.reuseAddr === true, ipv6Only: options.ipv6Only === true } if (isFunction(callback)) { this.on('message', callback) } const onabort = () => this.close() this.signal?.addEventListener('abort', onabort, { once: true }) this.once('close', () => { destroyDataListener(this) this.removeAllListeners() this.signal?.removeEventListener('abort', onabort) }) gc.ref(this, options) dc.channel('socket').publish({ socket: this }) } /** * Implements `gc.finalizer` for gc'd resource cleanup. * @return {gc.Finalizer} * @ignore */ [gc.finalizer] (options) { return { args: [this.id, this.conduit, options], async handle (id, conduit) { if (process.env.DEBUG) { console.warn('Closing Socket on garbage collection') } await ipc.request('udp.close', { id }, options) if (conduit) { conduit.close() } } } } /** * Listen for datagram messages on a named port and optional address * If the address is not specified, the operating system will attempt to * listen on all addresses. Once the binding is complete, a 'listening' * event is emitted and the optional callback function is called. * * If binding fails, an 'error' event is emitted. * * @param {number} port - The port to listen for messages on * @param {string} address - The address to bind to (0.0.0.0) * @param {function} callback - With no parameters. Called when binding is complete. * @see {@link https://nodejs.org/api/dgram.html#socketbindport-address-callback} */ bind (arg1, arg2, arg3) { const options = {} const cb = isFunction(arg2) ? arg2 : isFunction(arg3) ? arg3 : defaultCallback(this, this.#resource) if (typeof arg1 === 'number' || typeof arg2 === 'string') { options.port = parseInt(arg1) } else if (typeof arg1 === 'object') { Object.assign(options, arg1) } if (typeof arg2 === 'string') { options.address = arg2 } if ( options.port !== 0 && this.state.reuseAddr && !this.knownIdWasGivenInSocketConstruction ) { const index = globalThis.__args?.index || 0 const a = murmur3(options.address + options.port) const b = murmur3(options.address + options.port, index) this.id = BigInt(a) * BigInt(b) this.knownIdWasGivenInSocketConstruction = true } bind(this, options, (err, info) => { if (err) { return this.#resource.runInAsyncScope(() => { if ( this.knownIdWasGivenInSocketConstruction && err.code === 'ERR_SOCKET_ALREADY_BOUND' ) { this.dataListener = createDataListener(this, this.#resource) cb(null) this.emit('listening') } else { cb(err) } }) } if (!this.legacy && !this.conduit) { this.conduit = new Conduit({ id: this.id }) this.conduit.receive((_, decoded) => { if (!decoded || !decoded.options) return const rinfo = { port: Number(decoded.options.port), address: decoded.options.address, family: getAddressFamily(decoded.options.address) } const message = Buffer.from(decoded.payload) this.#resource.runInAsyncScope(() => { this.emit('message', message, rinfo) }) dc.channel('message').publish({ socket: this, buffer: message, info }) }) const onopen = () => { startReading(this, (err) => { this.#resource.runInAsyncScope(() => { if (err) { cb(err) } else { cb(null) this.emit('listening') } }) }) } if (!this.conduit.isActive) { this.conduit.addEventListener('open', onopen, { once: true }) } else { onopen() } return } startReading(this, (err) => { this.#resource.runInAsyncScope(() => { if (err) { cb(err) } else { this.dataListener = createDataListener(this, this.#resource) cb(null) this.emit('listening') } }) }) }) return this } /** * Associates the dgram.Socket to a remote address and port. Every message sent * by this handle is automatically sent to that destination. Also, the socket * will only receive messages from that remote peer. Trying to call connect() * on an already connected socket will result in an ERR_SOCKET_DGRAM_IS_CONNECTED * exception. If the address is not provided, '0.0.0.0' (for udp4 sockets) or '::1' * (for udp6 sockets) will be used by default. Once the connection is complete, * a 'connect' event is emitted and the optional callback function is called. * In case of failure, the callback is called or, failing this, an 'error' event * is emitted. * * @param {number} port - Port the client should connect to. * @param {string=} host - Host the client should connect to. * @param {function=} connectListener - Common parameter of socket.connect() methods. Will be added as a listener for the 'connect' event once. * @see {@link https://nodejs.org/api/dgram.html#socketconnectport-address-callback} */ connect (arg1, arg2, arg3) { const address = isFunction(arg2) ? undefined : arg2 const port = parseInt(arg1) const cb = isFunction(arg2) ? arg2 : isFunction(arg3) ? arg3 : defaultCallback(this, this.#resource) if (!Number.isInteger(port) || port <= 0 || port > MAX_PORT) { throw new ERR_SOCKET_BAD_PORT( `Port should be > 0 and < 65536. Received ${arg1}.` ) } if (this.state.connectState !== CONNECT_STATE_DISCONNECTED) { throw new ERR_SOCKET_DGRAM_IS_CONNECTED() } connect(this, { address, port }, (err, info) => { this.#resource.runInAsyncScope(() => { cb(err, info) if (!err && info) { this.emit('connect', info) } }) }) } /** * A synchronous function that disassociates a connected dgram.Socket from * its remote address. Trying to call disconnect() on an unbound or already * disconnected socket will result in an ERR_SOCKET_DGRAM_NOT_CONNECTED exception. * * @see {@link https://nodejs.org/api/dgram.html#socketdisconnect} */ disconnect () { if (this.state.connectState !== CONNECT_STATE_CONNECTED) { throw new ERR_SOCKET_DGRAM_NOT_CONNECTED() } const { err } = disconnect(this) if (err) { throw err } } /** * Broadcasts a datagram on the socket. For connectionless sockets, the * destination port and address must be specified. Connected sockets, on the * other hand, will use their associated remote endpoint, so the port and * address arguments must not be set. * * > The msg argument contains the message to be sent. Depending on its type, * different behavior can apply. If msg is a Buffer, any TypedArray, or a * DataView, the offset and length specify the offset within the Buffer where * the message begins and the number of bytes in the message, respectively. * If msg is a String, then it is automatically converted to a Buffer with * 'utf8' encoding. With messages that contain multi-byte characters, offset, * and length will be calculated with respect to byte length and not the * character position. If msg is an array, offset and length must not be * specified. * * > The address argument is a string. If the value of the address is a hostname, * DNS will be used to resolve the address of the host. If the address is not * provided or otherwise nullish, '0.0.0.0' (for udp4 sockets) or '::1' * (for udp6 sockets) will be used by default. * * > If the socket has not been previously bound with a call to bind, the socket * is assigned a random port number and is bound to the "all interfaces" * address ('0.0.0.0' for udp4 sockets, '::1' for udp6 sockets.) * * > An optional callback function may be specified as a way of reporting DNS * errors or for determining when it is safe to reuse the buf object. DNS * lookups delay the time to send for at least one tick of the Node.js event * loop. * * > The only way to know for sure that the datagram has been sent is by using a * callback. If an error occurs and a callback is given, the error will be * passed as the first argument to the callback. If a callback is not given, * the error is emitted as an 'error' event on the socket object. * * > Offset and length are optional but both must be set if either is used. * They are supported only when the first argument is a Buffer, a TypedArray, * or a DataView. * * @param {Buffer | TypedArray | DataView | string | Array} msg - Message to be sent. * @param {integer=} offset - Offset in the buffer where the message starts. * @param {integer=} length - Number of bytes in the message. * @param {integer=} port - Destination port. * @param {string=} address - Destination host name or IP address. * @param {Function=} callback - Called when the message has been sent. * @see {@link https://nodejs.org/api/dgram.html#socketsendmsg-offset-length-port-address-callback} */ send (buffer, ...args) { const id = this.id || rand64() let offset = 0 let length let port let address let cb = defaultCallback(this, this.#resource) if (Array.isArray(buffer)) { buffer = fromBufferList(buffer) } else if (typeof buffer === 'string' || isArrayBufferView(buffer)) { buffer = Buffer.from(buffer) } if (!Buffer.isBuffer(buffer)) { throw new TypeError('Invalid buffer') } // detect callback at end of `args` if (args.findIndex(isFunction) === args.length - 1) { cb = args.pop() } // parse argument variants if (typeof args[2] === 'number') { [offset, length, port, address] = args } else if (typeof args[1] === 'number') { [offset, length] = args } else if (typeof args[0] === 'number' && typeof args[1] === 'string') { [port, address] = args } else if (typeof args[0] === 'number') { [port] = args } if (port !== undefined || this.state.connectState !== CONNECT_STATE_CONNECTED) { // parse possible port as string port = parseInt(port) if (!Number.isInteger(port) || port <= 0 || port > (64 * 1024)) { throw new ERR_SOCKET_BAD_PORT( `Port should be > 0 and < 65536. Received ${port}.` ) } } if (offset === undefined) { offset = 0 } if (length === undefined) { length = buffer.length } if (!Number.isInteger(offset) || offset < 0) { throw new RangeError( `Offset should be >= 0 and < ${buffer.length} Received ${offset}.` ) } buffer = buffer.slice(offset) if (!Number.isInteger(length) || length < 0 || length > buffer.length) { throw new RangeError( `Length should be >= 0 and <= ${buffer.length} Received ${length}.` ) } buffer = buffer.slice(0, length) if (buffer?.buffer?.detached) { // XXX(@jwerle,@heapwolf): this is likely during a paused application state // how should handle this? maybe a warning return } return send(this, { id, port, address, buffer }, (...args) => { if (buffer.buffer?.detached) { // XXX(@jwerle,@heapwolf): see above return } if (typeof cb === 'function') { this.#resource.runInAsyncScope(() => { // eslint-disable-next-line cb(...args) }) } }) } /** * Close the underlying socket and stop listening for data on it. If a * callback is provided, it is added as a listener for the 'close' event. * * @param {function=} callback - Called when the connection is completed or on error. * * @see {@link https://nodejs.org/api/dgram.html#socketclosecallback} */ close (cb) { const state = getSocketState(this) if ( !state || !(state.connected || state.bound) || ( this.state.bindState === BIND_STATE_UNBOUND && this.state.connectState === CONNECT_STATE_DISCONNECTED ) ) { if (isFunction(cb)) { cb(new ERR_SOCKET_DGRAM_NOT_RUNNING()) return } else { throw new ERR_SOCKET_DGRAM_NOT_RUNNING() } } close(this, (err) => { if (err) { // gc might have already closed this if (!gc.finalizers.has(this)) { if (isFunction(cb)) { this.#resource.runInAsyncScope(() => { cb() }) return } } if (err.code === 'ERR_SOCKET_DGRAM_NOT_RUNNING') { err = Object.assign(new ERR_SOCKET_DGRAM_NOT_RUNNING(), { cause: err }) } this.#resource.runInAsyncScope(() => { if (isFunction(cb)) { cb(err) } else { this.emit('error', err) } }) return } if (this.conduit) { this.conduit.close() } this.#resource.runInAsyncScope(() => { if (isFunction(cb)) { cb(null) } this.emit('close') }) }) return this } /** * * Returns an object containing the address information for a socket. For * UDP sockets, this object will contain address, family, and port properties. * * This method throws EBADF if called on an unbound socket. * @returns {Object} socketInfo - Information about the local socket * @returns {string} socketInfo.address - The IP address of the socket * @returns {string} socketInfo.port - The port of the socket * @returns {string} socketInfo.family - The IP family of the socket * * @see {@link https://nodejs.org/api/dgram.html#socketaddress} */ address () { if (this.state.bindState !== BIND_STATE_BOUND) { throw new ERR_SOCKET_DGRAM_NOT_RUNNING() } if (!this.state.address) { const result = getSockName(this) if (result.err) { throw Object.assign(result.err, { syscall: 'getsockname' }) } this.state.address = result.data } return { port: this.state.address?.port ?? null, family: this.state.address?.family ?? null, address: this.state.address?.address ?? null } } /** * Returns an object containing the address, family, and port of the remote * endpoint. This method throws an ERR_SOCKET_DGRAM_NOT_CONNECTED exception * if the socket is not connected. * * @returns {Object} socketInfo - Information about the remote socket * @returns {string} socketInfo.address - The IP address of the socket * @returns {string} socketInfo.port - The port of the socket * @returns {string} socketInfo.family - The IP family of the socket * @see {@link https://nodejs.org/api/dgram.html#socketremoteaddress} */ remoteAddress () { if (this.state.connectState !== CONNECT_STATE_CONNECTED) { throw new ERR_SOCKET_DGRAM_NOT_CONNECTED() } if (!this.state.remoteAddress) { const result = getPeerName(this) if (result.err) { throw Object.assign(result.err, { syscall: 'getpeername' }) } this.state.remoteAddress = result.data } return { port: this.state.remoteAddress?.port ?? null, family: this.state.remoteAddress?.family ?? null, address: this.state.remoteAddress?.address ?? null } } /** * Sets the SO_RCVBUF socket option. Sets the maximum socket receive buffer in * bytes. * * @param {number} size - The size of the new receive buffer * @see {@link https://nodejs.org/api/dgram.html#socketsetrecvbuffersizesize} */ async setRecvBufferSize (size) { if (size > 0) { this.state.recvBufferSize = size const result = await ipc.request('os.bufferSize', { id: this.id, size, buffer: 1 }) if (result.err) { throw result.err } } } /** * Sets the SO_SNDBUF socket option. Sets the maximum socket send buffer in * bytes. * * @param {number} size - The size of the new send buffer * @see {@link https://nodejs.org/api/dgram.html#socketsetsendbuffersizesize} */ async setSendBufferSize (size) { if (size > 0) { this.state.sendBufferSize = size const result = await ipc.request('os.bufferSize', { id: this.id, size, buffer: 0 }) if (result.err) { throw result.err } } } /** * @see {@link https://nodejs.org/api/dgram.html#socketgetrecvbuffersize} */ getRecvBufferSize () { return this.state.recvBufferSize } /** * @returns {number} the SO_SNDBUF socket send buffer size in bytes. * @see {@link https://nodejs.org/api/dgram.html#socketgetsendbuffersize} */ getSendBufferSize () { return this.state.sendBufferSize } // // For now we aren't going to implement any of the multicast options, // mainly because 1. we don't need it in hyper and 2. if a user wants // to deploy their app to the app store, they will need to request the // multicast entitlement from apple. If someone really wants this they // can implement it. // setBroadcast () { throw new Error('not implemented') } setTTL () { throw new Error('not implemented') } setMulticastTTL () { throw new Error('not implemented') } setMulticastLoopback () { throw new Error('not implemented') } setMulticastMembership () { throw new Error('not implemented') } setMulticastInterface () { throw new Error('not implemented') } addMembership () { throw new Error('not implemented') } dropMembership () { throw new Error('not implemented') } addSourceSpecificMembership () { throw new Error('not implemented') } dropSourceSpecificMembership () { throw new Error('not implemented') } ref () { return this } unref () { return this } } /** * Generic error class for an error occurring on a `Socket` instance. * @ignore */ export class SocketError extends InternalError { /** * @type {string} */ get code () { return this.constructor.name } } /** * Thrown when a socket is already bound. */ export class ERR_SOCKET_ALREADY_BOUND extends SocketError { get message () { return 'Socket is already bound' } } /** * @ignore */ export class ERR_SOCKET_BAD_BUFFER_SIZE extends SocketError {} /** * @ignore */ export class ERR_SOCKET_BUFFER_SIZE extends SocketError {} /** * Thrown when the socket is already connected. */ export class ERR_SOCKET_DGRAM_IS_CONNECTED extends SocketError { get message () { return 'Alread connected' } } /** * Thrown when the socket is not connected. */ export class ERR_SOCKET_DGRAM_NOT_CONNECTED extends SocketError { syscall = 'getpeername' get message () { return 'Not connected' } } /** * Thrown when the socket is not running (not bound or connected). */ export class ERR_SOCKET_DGRAM_NOT_RUNNING extends SocketError { get message () { return 'Not running' } } /** * Thrown when a bad socket type is used in an argument. */ export class ERR_SOCKET_BAD_TYPE extends TypeError { code = 'ERR_SOCKET_BAD_TYPE' get message () { return 'Bad socket type specified. Valid types are: udp4, udp6' } } /** * Thrown when a bad port is given. */ export class ERR_SOCKET_BAD_PORT extends RangeError { code = 'ERR_SOCKET_BAD_PORT' } export default exports