signalk-server
Version:
An implementation of a [Signal K](http://signalk.org) server for boats.
699 lines • 25.9 kB
JavaScript
;
/* eslint-disable @typescript-eslint/no-unused-vars */
/**
* WASM Socket Manager
*
* Manages UDP and TCP sockets for WASM plugins that need raw network access
* (e.g., radar plugins, NMEA receivers, etc.)
*
* Uses Node.js dgram and net modules, bridged to WASM via FFI
*/
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.tcpSocketManager = exports.socketManager = void 0;
const dgram = __importStar(require("dgram"));
const net = __importStar(require("net"));
const debug_1 = __importDefault(require("debug"));
const debug = (0, debug_1.default)('signalk:wasm:sockets');
/**
* Socket Manager - singleton for managing plugin sockets
*/
class SocketManager {
sockets = new Map();
nextSocketId = 1;
/**
* Create a new UDP socket
* @param pluginId - Plugin that owns the socket
* @param type - Socket type: 'udp4' or 'udp6'
* @returns Socket ID, or -1 on error
*/
createSocket(pluginId, type = 'udp4') {
try {
const socketId = this.nextSocketId++;
const socket = dgram.createSocket({
type,
reuseAddr: true // Allow multiple plugins to bind to same port
});
const managed = {
socket,
pluginId,
bound: false,
bindPromise: null,
receiveBuffer: [],
maxBufferSize: 1000, // Max buffered datagrams
multicastGroups: new Set(),
pendingOptions: []
};
// Set up message handler to buffer incoming data
socket.on('message', (msg, rinfo) => {
if (managed.receiveBuffer.length >= managed.maxBufferSize) {
// Drop oldest message if buffer full
managed.receiveBuffer.shift();
}
managed.receiveBuffer.push({
data: Buffer.from(msg), // Copy the buffer
address: rinfo.address,
port: rinfo.port,
timestamp: Date.now()
});
});
socket.on('error', (err) => {
debug(`[${pluginId}] Socket ${socketId} error: ${err.message}`);
});
socket.on('close', () => {
debug(`[${pluginId}] Socket ${socketId} closed`);
this.sockets.delete(socketId);
});
this.sockets.set(socketId, managed);
debug(`[${pluginId}] Created socket ${socketId} (${type})`);
return socketId;
}
catch (error) {
debug(`Failed to create socket: ${error}`);
return -1;
}
}
/**
* Bind socket to a port
* @param socketId - Socket to bind
* @param port - Port number (0 for any available port)
* @param address - Address to bind to (optional, defaults to all interfaces)
* @returns 0 on success, -1 on error
*/
bind(socketId, port, address) {
const managed = this.sockets.get(socketId);
if (!managed) {
debug(`Socket ${socketId} not found`);
return Promise.resolve(-1);
}
// Store the promise so setBroadcast etc. can wait for it
managed.bindPromise = new Promise((resolve) => {
try {
managed.socket.bind(port, address, () => {
managed.bound = true;
const addr = managed.socket.address();
debug(`[${managed.pluginId}] Socket ${socketId} bound to ${addr.address}:${addr.port}`);
// Apply any pending socket options now that we're bound
for (const option of managed.pendingOptions) {
try {
if (option.type === 'broadcast') {
managed.socket.setBroadcast(option.value);
debug(`[${managed.pluginId}] Applied deferred setBroadcast(${option.value})`);
}
else if (option.type === 'multicastTTL') {
managed.socket.setMulticastTTL(option.value);
debug(`[${managed.pluginId}] Applied deferred setMulticastTTL(${option.value})`);
}
else if (option.type === 'multicastLoopback') {
managed.socket.setMulticastLoopback(option.value);
debug(`[${managed.pluginId}] Applied deferred setMulticastLoopback(${option.value})`);
}
else if (option.type === 'joinMulticast') {
const { multicastAddress, interfaceAddress } = option.value;
if (interfaceAddress) {
managed.socket.addMembership(multicastAddress, interfaceAddress);
}
else {
managed.socket.addMembership(multicastAddress);
}
managed.multicastGroups.add(multicastAddress);
debug(`[${managed.pluginId}] Applied deferred joinMulticast(${multicastAddress})`);
}
else if (option.type === 'leaveMulticast') {
const { multicastAddress, interfaceAddress } = option.value;
if (interfaceAddress) {
managed.socket.dropMembership(multicastAddress, interfaceAddress);
}
else {
managed.socket.dropMembership(multicastAddress);
}
managed.multicastGroups.delete(multicastAddress);
debug(`[${managed.pluginId}] Applied deferred leaveMulticast(${multicastAddress})`);
}
}
catch (optionError) {
debug(`[${managed.pluginId}] Error applying deferred option ${option.type}: ${optionError}`);
}
}
managed.pendingOptions = [];
resolve(0);
});
}
catch (error) {
debug(`[${managed.pluginId}] Bind error: ${error}`);
resolve(-1);
}
});
return managed.bindPromise;
}
/**
* Join a multicast group
* @param socketId - Socket to use
* @param multicastAddress - Multicast group address (e.g., "239.254.2.0")
* @param interfaceAddress - Interface address to use (optional)
* @returns 0 on success, -1 on error
*/
joinMulticast(socketId, multicastAddress, interfaceAddress) {
const managed = this.sockets.get(socketId);
if (!managed) {
debug(`Socket ${socketId} not found`);
return -1;
}
// If socket is not yet bound, defer the multicast join until bind completes
if (!managed.bound) {
debug(`[${managed.pluginId}] Deferring joinMulticast(${multicastAddress}) until socket is bound`);
managed.pendingOptions.push({
type: 'joinMulticast',
value: { multicastAddress, interfaceAddress }
});
return 0;
}
try {
if (interfaceAddress) {
managed.socket.addMembership(multicastAddress, interfaceAddress);
}
else {
managed.socket.addMembership(multicastAddress);
}
managed.multicastGroups.add(multicastAddress);
debug(`[${managed.pluginId}] Socket ${socketId} joined multicast ${multicastAddress}`);
return 0;
}
catch (error) {
debug(`[${managed.pluginId}] Join multicast error: ${error}`);
return -1;
}
}
/**
* Leave a multicast group
* @param socketId - Socket to use
* @param multicastAddress - Multicast group address
* @param interfaceAddress - Interface address (optional)
* @returns 0 on success, -1 on error
*/
leaveMulticast(socketId, multicastAddress, interfaceAddress) {
const managed = this.sockets.get(socketId);
if (!managed) {
debug(`Socket ${socketId} not found`);
return -1;
}
// If socket is not yet bound, defer the multicast leave until bind completes
if (!managed.bound) {
debug(`[${managed.pluginId}] Deferring leaveMulticast(${multicastAddress}) until socket is bound`);
managed.pendingOptions.push({
type: 'leaveMulticast',
value: { multicastAddress, interfaceAddress }
});
return 0;
}
try {
if (interfaceAddress) {
managed.socket.dropMembership(multicastAddress, interfaceAddress);
}
else {
managed.socket.dropMembership(multicastAddress);
}
managed.multicastGroups.delete(multicastAddress);
debug(`[${managed.pluginId}] Socket ${socketId} left multicast ${multicastAddress}`);
return 0;
}
catch (error) {
debug(`[${managed.pluginId}] Leave multicast error: ${error}`);
return -1;
}
}
/**
* Set socket options
*/
setMulticastTTL(socketId, ttl) {
const managed = this.sockets.get(socketId);
if (!managed)
return -1;
// If socket is not yet bound, defer the option
if (!managed.bound) {
debug(`[${managed.pluginId}] Deferring setMulticastTTL(${ttl}) until socket is bound`);
managed.pendingOptions.push({ type: 'multicastTTL', value: ttl });
return 0;
}
try {
managed.socket.setMulticastTTL(ttl);
return 0;
}
catch (error) {
debug(`[${managed.pluginId}] setMulticastTTL error: ${error}`);
return -1;
}
}
setMulticastLoopback(socketId, enabled) {
const managed = this.sockets.get(socketId);
if (!managed)
return -1;
// If socket is not yet bound, defer the option
if (!managed.bound) {
debug(`[${managed.pluginId}] Deferring setMulticastLoopback(${enabled}) until socket is bound`);
managed.pendingOptions.push({ type: 'multicastLoopback', value: enabled });
return 0;
}
try {
managed.socket.setMulticastLoopback(enabled);
return 0;
}
catch (error) {
debug(`[${managed.pluginId}] setMulticastLoopback error: ${error}`);
return -1;
}
}
setBroadcast(socketId, enabled) {
const managed = this.sockets.get(socketId);
if (!managed)
return -1;
// If socket is not yet bound, defer the option
if (!managed.bound) {
debug(`[${managed.pluginId}] Deferring setBroadcast(${enabled}) until socket is bound`);
managed.pendingOptions.push({ type: 'broadcast', value: enabled });
return 0;
}
try {
managed.socket.setBroadcast(enabled);
return 0;
}
catch (error) {
debug(`[${managed.pluginId}] setBroadcast error: ${error}`);
return -1;
}
}
/**
* Send data via UDP
* @param socketId - Socket to use
* @param data - Data to send
* @param address - Destination address
* @param port - Destination port
* @returns Bytes sent, or -1 on error
*/
send(socketId, data, address, port) {
return new Promise((resolve) => {
const managed = this.sockets.get(socketId);
if (!managed) {
debug(`Socket ${socketId} not found`);
resolve(-1);
return;
}
managed.socket.send(data, port, address, (err, bytes) => {
if (err) {
debug(`[${managed.pluginId}] Send error: ${err}`);
resolve(-1);
}
else {
resolve(bytes);
}
});
});
}
/**
* Receive data from buffer (non-blocking)
* @param socketId - Socket to receive from
* @returns Buffered datagram, or null if buffer empty
*/
receive(socketId) {
const managed = this.sockets.get(socketId);
if (!managed) {
debug(`Socket ${socketId} not found`);
return null;
}
return managed.receiveBuffer.shift() || null;
}
/**
* Get number of buffered datagrams
*/
getBufferedCount(socketId) {
const managed = this.sockets.get(socketId);
return managed ? managed.receiveBuffer.length : 0;
}
/**
* Close a socket
* @param socketId - Socket to close
*/
close(socketId) {
const managed = this.sockets.get(socketId);
if (!managed) {
debug(`Socket ${socketId} not found`);
return;
}
try {
// Leave all multicast groups first
for (const group of managed.multicastGroups) {
try {
managed.socket.dropMembership(group);
}
catch (e) {
// Ignore errors when leaving groups during close
}
}
managed.socket.close();
this.sockets.delete(socketId);
debug(`[${managed.pluginId}] Socket ${socketId} closed`);
}
catch (error) {
debug(`[${managed.pluginId}] Close error: ${error}`);
}
}
/**
* Close all sockets for a plugin (cleanup on plugin stop)
*/
closeAllForPlugin(pluginId) {
const toClose = [];
for (const [id, managed] of this.sockets) {
if (managed.pluginId === pluginId) {
toClose.push(id);
}
}
for (const id of toClose) {
this.close(id);
}
debug(`[${pluginId}] Closed ${toClose.length} sockets`);
}
/**
* Get socket statistics
*/
getStats() {
const socketsPerPlugin = {};
for (const managed of this.sockets.values()) {
socketsPerPlugin[managed.pluginId] =
(socketsPerPlugin[managed.pluginId] || 0) + 1;
}
return {
totalSockets: this.sockets.size,
socketsPerPlugin
};
}
}
// Export singleton instance
exports.socketManager = new SocketManager();
/**
* TCP Socket Manager - manages TCP connections for WASM plugins
*
* Key differences from UDP:
* - Connection-oriented (connect before send)
* - Line-buffered receive (splits on \r\n or \n)
*/
class TcpSocketManager {
sockets = new Map();
nextSocketId = 1;
/**
* Create a new TCP socket
* @param pluginId - Plugin that owns the socket
* @returns Socket ID, or -1 on error
*/
createSocket(pluginId) {
try {
const socketId = this.nextSocketId++;
const socket = new net.Socket();
const managed = {
socket,
pluginId,
connected: false,
connecting: false,
receiveBuffer: [],
rawBuffer: [],
partialLine: '',
maxBufferSize: 1000,
error: null,
useLineBuffering: true // Default to line buffering
};
// Set up data handler
socket.on('data', (data) => {
if (managed.useLineBuffering) {
// Line-buffered mode for text protocols
managed.partialLine += data.toString();
// Split on line endings (\r\n or \n)
const lines = managed.partialLine.split(/\r?\n/);
// Last element is either empty (if data ended with newline) or partial
managed.partialLine = lines.pop() || '';
// Add complete lines to buffer
for (const line of lines) {
if (line.length > 0) {
if (managed.receiveBuffer.length >= managed.maxBufferSize) {
managed.receiveBuffer.shift(); // Drop oldest
}
managed.receiveBuffer.push(line);
}
}
}
else {
// Raw mode for binary protocols
if (managed.rawBuffer.length >= managed.maxBufferSize) {
managed.rawBuffer.shift(); // Drop oldest
}
managed.rawBuffer.push(Buffer.from(data));
}
});
socket.on('connect', () => {
managed.connected = true;
managed.connecting = false;
managed.error = null;
debug(`[${pluginId}] TCP socket ${socketId} connected`);
});
socket.on('error', (err) => {
managed.error = err.message;
managed.connected = false;
managed.connecting = false;
debug(`[${pluginId}] TCP socket ${socketId} error: ${err.message}`);
});
socket.on('close', () => {
managed.connected = false;
managed.connecting = false;
this.sockets.delete(socketId);
debug(`[${pluginId}] TCP socket ${socketId} closed`);
});
socket.on('end', () => {
managed.connected = false;
debug(`[${pluginId}] TCP socket ${socketId} ended by remote`);
});
this.sockets.set(socketId, managed);
debug(`[${pluginId}] Created TCP socket ${socketId}`);
return socketId;
}
catch (error) {
debug(`Failed to create TCP socket: ${error}`);
return -1;
}
}
/**
* Connect to a remote host
* @param socketId - Socket to connect
* @param address - Remote host address
* @param port - Remote port
* @returns 0 if connection initiated, -1 on error
*/
connect(socketId, address, port) {
const managed = this.sockets.get(socketId);
if (!managed) {
debug(`TCP socket ${socketId} not found`);
return -1;
}
if (managed.connected || managed.connecting) {
debug(`[${managed.pluginId}] TCP socket ${socketId} already connected/connecting`);
return -1;
}
try {
managed.connecting = true;
managed.error = null;
managed.socket.connect(port, address);
debug(`[${managed.pluginId}] TCP socket ${socketId} connecting to ${address}:${port}`);
return 0;
}
catch (error) {
managed.connecting = false;
managed.error = String(error);
debug(`[${managed.pluginId}] TCP connect error: ${error}`);
return -1;
}
}
/**
* Check if socket is connected
* @param socketId - Socket to check
* @returns 1 if connected, 0 if not, -1 if socket not found
*/
isConnected(socketId) {
const managed = this.sockets.get(socketId);
if (!managed) {
return -1;
}
return managed.connected ? 1 : 0;
}
/**
* Send data over TCP
* @param socketId - Socket to use
* @param data - Data to send
* @returns Bytes sent, or -1 on error
*/
send(socketId, data) {
return new Promise((resolve) => {
const managed = this.sockets.get(socketId);
if (!managed) {
debug(`TCP socket ${socketId} not found`);
resolve(-1);
return;
}
if (!managed.connected) {
debug(`[${managed.pluginId}] TCP socket ${socketId} not connected`);
resolve(-1);
return;
}
managed.socket.write(data, (err) => {
if (err) {
debug(`[${managed.pluginId}] TCP send error: ${err}`);
resolve(-1);
}
else {
resolve(data.length);
}
});
});
}
/**
* Receive a complete line (non-blocking)
* @param socketId - Socket to receive from
* @returns Complete line without line ending, or null if no complete line available
*/
receiveLine(socketId) {
const managed = this.sockets.get(socketId);
if (!managed) {
debug(`TCP socket ${socketId} not found`);
return null;
}
return managed.receiveBuffer.shift() || null;
}
/**
* Receive raw data (non-blocking)
* @param socketId - Socket to receive from
* @returns Raw data buffer, or null if no data available
*/
receiveRaw(socketId) {
const managed = this.sockets.get(socketId);
if (!managed) {
debug(`TCP socket ${socketId} not found`);
return null;
}
return managed.rawBuffer.shift() || null;
}
/**
* Set buffering mode
* @param socketId - Socket to configure
* @param lineBuffering - true for line-buffered (text), false for raw (binary)
* @returns 0 on success, -1 on error
*/
setLineBuffering(socketId, lineBuffering) {
const managed = this.sockets.get(socketId);
if (!managed) {
return -1;
}
managed.useLineBuffering = lineBuffering;
debug(`[${managed.pluginId}] TCP socket ${socketId} buffering mode: ${lineBuffering ? 'line' : 'raw'}`);
return 0;
}
/**
* Get number of buffered items (lines or raw chunks)
*/
getBufferedCount(socketId) {
const managed = this.sockets.get(socketId);
if (!managed)
return 0;
return managed.useLineBuffering
? managed.receiveBuffer.length
: managed.rawBuffer.length;
}
/**
* Get last error message
*/
getError(socketId) {
const managed = this.sockets.get(socketId);
return managed ? managed.error : null;
}
/**
* Close a TCP socket
* @param socketId - Socket to close
*/
close(socketId) {
const managed = this.sockets.get(socketId);
if (!managed) {
debug(`TCP socket ${socketId} not found`);
return;
}
try {
managed.socket.destroy();
this.sockets.delete(socketId);
debug(`[${managed.pluginId}] TCP socket ${socketId} closed`);
}
catch (error) {
debug(`[${managed.pluginId}] TCP close error: ${error}`);
}
}
/**
* Close all TCP sockets for a plugin
*/
closeAllForPlugin(pluginId) {
const toClose = [];
for (const [id, managed] of this.sockets) {
if (managed.pluginId === pluginId) {
toClose.push(id);
}
}
for (const id of toClose) {
this.close(id);
}
debug(`[${pluginId}] Closed ${toClose.length} TCP sockets`);
}
/**
* Get TCP socket statistics
*/
getStats() {
const socketsPerPlugin = {};
for (const managed of this.sockets.values()) {
socketsPerPlugin[managed.pluginId] =
(socketsPerPlugin[managed.pluginId] || 0) + 1;
}
return {
totalSockets: this.sockets.size,
socketsPerPlugin
};
}
}
// Export TCP socket manager singleton
exports.tcpSocketManager = new TcpSocketManager();
//# sourceMappingURL=socket-manager.js.map