UNPKG

signalk-server

Version:

An implementation of a [Signal K](http://signalk.org) server for boats.

206 lines 7.15 kB
"use strict"; /** * 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