import WebSocket from 'ws'; // ── Inbound message types (client → server) ────────────────────────────────── export type ClientMessageType = | 'heartbeat' | 'offer' | 'answer' | 'ice-candidate' | 'message' | 'get-upload-url' | 'relay' | 'friend'; // ── Outbound message types (server → client) ────────────────────────────────── export type ServerMessageType = | 'connected' | 'ice-servers' | 'offer' | 'answer' | 'ice-candidate' | 'message' | 'upload-url' | 'relay' | 'peer-online' | 'peer-offline' | 'offline-flush' | 'error'; export interface ClientMessage { type: ClientMessageType; payload: Record; // requestId allows client to correlate responses requestId?: string; } export interface ServerMessage { type: ServerMessageType; payload: Record; requestId?: string; ts: number; // epoch ms } /** * Parse and validate an incoming raw WS message. * Returns null if the data is malformed — caller should silently drop. */ export function parseClientMessage(raw: WebSocket.RawData): ClientMessage | null { try { const str = raw.toString('utf8'); if (str.length > 65536) return null; // 64 KB hard cap — protects against flood const obj = JSON.parse(str) as unknown; if ( typeof obj !== 'object' || obj === null || typeof (obj as Record).type !== 'string' ) { return null; } return obj as ClientMessage; } catch { return null; } } /** * Serialize a server message. * Using a single Buffer allocation avoids repeated JSON.stringify on hot paths. */ export function buildServerMessage( type: ServerMessageType, payload: Record, requestId?: string ): Buffer { const msg: ServerMessage = { type, payload, requestId, ts: Date.now() }; return Buffer.from(JSON.stringify(msg), 'utf8'); } /** * Send a message to a WebSocket, catching send errors silently. * Avoids crashing the process when a socket closes mid-send. */ export function safeSend( ws: WebSocket, type: ServerMessageType, payload: Record, requestId?: string ): void { if (ws.readyState !== WebSocket.OPEN) return; try { ws.send(buildServerMessage(type, payload, requestId)); } catch { // Socket probably closed between the readyState check and send — ignore } }