signalk-server
Version:
An implementation of a [Signal K](http://signalk.org) server for boats.
206 lines • 7.15 kB
JavaScript
;
/**
* Binary Stream Manager
*
* Manages WebSocket streaming of high-frequency binary data from WASM plugins.
* Handles buffering, client connections, and slow consumer disconnection.
*/
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.binaryStreamManager = exports.BinaryStreamManager = void 0;
const debug_1 = __importDefault(require("debug"));
const debug = (0, debug_1.default)('signalk:streams:binary-stream-manager');
/**
* Maximum buffer size per WebSocket before considering client too slow
*/
const MAX_WEBSOCKET_BUFFER_SIZE = 256 * 1024; // 256KB
/**
* Maximum consecutive drops before disconnecting slow client
*/
const MAX_CONSECUTIVE_DROPS = 30; // ~0.5 seconds at 60Hz
/**
* Maximum frames buffered per stream (ring buffer)
*/
const MAX_BUFFERED_FRAMES = 100;
/**
* Manages binary data streaming to WebSocket clients
*/
class BinaryStreamManager {
clients = new Map();
buffers = new Map();
/**
* Emit binary data to all clients subscribed to a stream
* Called by WASM plugins via FFI binding
*
* @param streamId - Stream identifier (e.g., "radars/radar-0")
* @param data - Binary data to send
*/
emitCount = 0;
emitData(streamId, data) {
this.emitCount++;
// Log periodically
if (this.emitCount % 500 === 0) {
const clients = this.clients.get(streamId);
debug('emitData #%d: streamId=%s, dataLen=%d, clients=%d', this.emitCount, streamId, data.length, clients?.size || 0);
}
// Add to ring buffer
let buffer = this.buffers.get(streamId);
if (!buffer) {
buffer = [];
this.buffers.set(streamId, buffer);
}
buffer.push(data);
// Maintain ring buffer size
if (buffer.length > MAX_BUFFERED_FRAMES) {
buffer.shift(); // Remove oldest frame
}
// Send to all connected clients
const clients = this.clients.get(streamId);
if (!clients || clients.size === 0) {
return; // No clients, buffered for later
}
for (const client of clients) {
this.sendToClient(client, data);
}
}
/**
* Add a WebSocket client to a stream
*
* @param streamId - Stream identifier
* @param ws - WebSocket connection
* @param principal - Security principal (authenticated user)
*/
addClient(streamId, ws, principal) {
debug('Adding client to stream: %s', streamId);
const client = {
streamId,
ws,
principal,
consecutiveDropCount: 0,
connectedAt: Date.now()
};
// Add to clients map
let clients = this.clients.get(streamId);
if (!clients) {
clients = new Set();
this.clients.set(streamId, clients);
}
clients.add(client);
// Note: We intentionally do NOT send buffered frames to new clients.
// For high-frequency streams like radar, sending 100 buffered frames
// at once overwhelms the client's WebSocket buffer, triggering
// backpressure disconnection. Fresh data will arrive immediately anyway.
debug(`Stream ${streamId} now has ${clients.size} client(s)`);
}
/**
* Remove a WebSocket client from a stream
*
* @param streamId - Stream identifier
* @param ws - WebSocket connection to remove
*/
removeClient(streamId, ws) {
const clients = this.clients.get(streamId);
if (!clients) {
return;
}
// Find and remove client
for (const client of clients) {
if (client.ws === ws) {
clients.delete(client);
debug('Removed client from stream: %s, remaining: %d', streamId, clients.size);
break;
}
}
// Clean up empty client sets
if (clients.size === 0) {
this.clients.delete(streamId);
debug(`Stream ${streamId} has no more clients`);
}
}
/**
* Clean up all clients and buffers for a stream
* Called when plugin stops
*
* @param streamId - Stream identifier
*/
cleanupStream(streamId) {
const clients = this.clients.get(streamId);
if (clients) {
for (const client of clients) {
try {
client.ws.close(1001, 'Stream ended');
}
catch (error) {
debug(`Error closing client WebSocket: ${error}`);
}
}
this.clients.delete(streamId);
}
this.buffers.delete(streamId);
debug(`Cleaned up stream: ${streamId}`);
}
/**
* Send binary data to a specific client, disconnecting slow consumers
*
* @param client - Client to send to
* @param data - Binary data
*/
sendToClient(client, data) {
// Check if client buffer is full (slow consumer)
if (client.ws.bufferedAmount > MAX_WEBSOCKET_BUFFER_SIZE) {
client.consecutiveDropCount++;
if (client.consecutiveDropCount > MAX_CONSECUTIVE_DROPS) {
debug(`Disconnecting slow client on stream ${client.streamId} ` +
`(dropped ${client.consecutiveDropCount} frames)`);
try {
client.ws.close(1008, 'Client cannot keep up with data rate');
}
catch (error) {
debug(`Error closing slow client: ${error}`);
}
// Remove from clients set
const clients = this.clients.get(client.streamId);
if (clients) {
clients.delete(client);
}
}
// Skip sending this frame
return;
}
// Reset drop counter on successful send
client.consecutiveDropCount = 0;
// Send binary frame
try {
client.ws.send(data, { binary: true });
}
catch (error) {
debug(`Error sending to client on stream ${client.streamId}: ${error}`);
}
}
/**
* Get number of buffered frames for a stream (for testing)
*
* @param streamId - Stream identifier
* @returns Number of buffered frames
*/
getBufferSize(streamId) {
const buffer = this.buffers.get(streamId);
return buffer ? buffer.length : 0;
}
/**
* Get number of connected clients for a stream (for testing)
*
* @param streamId - Stream identifier
* @returns Number of connected clients
*/
getClientCount(streamId) {
const clients = this.clients.get(streamId);
return clients ? clients.size : 0;
}
}
exports.BinaryStreamManager = BinaryStreamManager;
// Export singleton instance
exports.binaryStreamManager = new BinaryStreamManager();
//# sourceMappingURL=binary-stream-manager.js.map