/** * RVS (Rendezvous Server) - WebSocket-Verbindungsmanager * * Verwaltet die persistente WebSocket-Verbindung zwischen der ARIA Cockpit App * und dem Rendezvous Server. Unterstützt Auto-Reconnect, Heartbeat und * typisierte Nachrichten. */ import AsyncStorage from '@react-native-async-storage/async-storage'; // --- Typen --- export type ConnectionState = 'connecting' | 'connected' | 'disconnected'; export type MessageType = 'chat' | 'audio' | 'file' | 'location' | 'mode' | 'log' | 'event'; export interface RVSMessage { type: MessageType; payload: Record; timestamp: number; } export interface ConnectionConfig { host: string; port: number; token: string; useTLS: boolean; } type MessageCallback = (message: RVSMessage) => void; type StateCallback = (state: ConnectionState) => void; // --- Konstanten --- const HEARTBEAT_INTERVAL_MS = 25_000; const INITIAL_RECONNECT_DELAY_MS = 1_000; const MAX_RECONNECT_DELAY_MS = 30_000; const RECONNECT_BACKOFF_FACTOR = 2; // --- RVS-Klasse --- class RVSConnection { private ws: WebSocket | null = null; private config: ConnectionConfig | null = null; private state: ConnectionState = 'disconnected'; private heartbeatTimer: ReturnType | null = null; private reconnectTimer: ReturnType | null = null; private reconnectDelay: number = INITIAL_RECONNECT_DELAY_MS; private shouldReconnect: boolean = false; private messageListeners: MessageCallback[] = []; private stateListeners: StateCallback[] = []; // --- Konfiguration --- /** Verbindungsdaten setzen (z.B. nach QR-Scan) */ setConfig(config: ConnectionConfig): void { this.config = config; this.saveConfig(config); } getConfig(): ConnectionConfig | null { return this.config; } getState(): ConnectionState { return this.state; } // --- Verbindung --- /** Verbindung zum RVS aufbauen */ connect(): void { if (!this.config) { console.warn('[RVS] Keine Verbindungskonfiguration vorhanden'); return; } if (this.ws?.readyState === WebSocket.OPEN) { console.log('[RVS] Bereits verbunden'); return; } this.shouldReconnect = true; this.reconnectDelay = INITIAL_RECONNECT_DELAY_MS; this.establishConnection(); } /** Verbindung trennen (kein Auto-Reconnect) */ disconnect(): void { this.shouldReconnect = false; this.clearTimers(); if (this.ws) { this.ws.close(1000, 'Benutzer hat getrennt'); this.ws = null; } this.setState('disconnected'); } /** Nachricht an den RVS senden */ send(type: MessageType, payload: Record): void { if (!this.ws || this.ws.readyState !== WebSocket.OPEN) { console.warn('[RVS] Kann nicht senden - nicht verbunden'); return; } const message: RVSMessage = { type, payload, timestamp: Date.now(), }; this.ws.send(JSON.stringify(message)); } // --- Event-Listener --- /** Callback fuer eingehende Nachrichten registrieren */ onMessage(callback: MessageCallback): () => void { this.messageListeners.push(callback); // Gibt Unsubscribe-Funktion zurueck return () => { this.messageListeners = this.messageListeners.filter(cb => cb !== callback); }; } /** Callback fuer Verbindungsstatus-Aenderungen registrieren */ onStateChange(callback: StateCallback): () => void { this.stateListeners.push(callback); return () => { this.stateListeners = this.stateListeners.filter(cb => cb !== callback); }; } // --- Interne Methoden --- private establishConnection(): void { if (!this.config) return; this.setState('connecting'); const protocol = this.config.useTLS ? 'wss' : 'ws'; const url = `${protocol}://${this.config.host}:${this.config.port}?token=${this.config.token}`; try { this.ws = new WebSocket(url); this.ws.onopen = () => { console.log('[RVS] Verbunden'); this.setState('connected'); this.reconnectDelay = INITIAL_RECONNECT_DELAY_MS; this.startHeartbeat(); }; this.ws.onmessage = (event: WebSocketMessageEvent) => { try { const message: RVSMessage = JSON.parse(event.data as string); this.notifyMessageListeners(message); } catch (err) { console.error('[RVS] Fehler beim Parsen der Nachricht:', err); } }; this.ws.onclose = (event) => { console.log(`[RVS] Verbindung geschlossen (Code: ${event.code})`); this.clearTimers(); this.ws = null; this.setState('disconnected'); if (this.shouldReconnect) { this.scheduleReconnect(); } }; this.ws.onerror = (error) => { console.error('[RVS] WebSocket-Fehler:', error); }; } catch (err) { console.error('[RVS] Verbindungsfehler:', err); this.setState('disconnected'); if (this.shouldReconnect) { this.scheduleReconnect(); } } } /** Reconnect mit exponentiellem Backoff planen */ private scheduleReconnect(): void { console.log(`[RVS] Reconnect in ${this.reconnectDelay / 1000}s...`); this.reconnectTimer = setTimeout(() => { this.establishConnection(); }, this.reconnectDelay); // Exponentieller Backoff: 1s -> 2s -> 4s -> 8s -> ... -> max 30s this.reconnectDelay = Math.min( this.reconnectDelay * RECONNECT_BACKOFF_FACTOR, MAX_RECONNECT_DELAY_MS, ); } /** Heartbeat starten (alle 25 Sekunden) */ private startHeartbeat(): void { this.heartbeatTimer = setInterval(() => { if (this.ws?.readyState === WebSocket.OPEN) { this.ws.send(JSON.stringify({ type: 'heartbeat', timestamp: Date.now() })); } }, HEARTBEAT_INTERVAL_MS); } private clearTimers(): void { if (this.heartbeatTimer) { clearInterval(this.heartbeatTimer); this.heartbeatTimer = null; } if (this.reconnectTimer) { clearTimeout(this.reconnectTimer); this.reconnectTimer = null; } } private setState(state: ConnectionState): void { if (this.state !== state) { this.state = state; this.stateListeners.forEach(cb => cb(state)); } } private notifyMessageListeners(message: RVSMessage): void { this.messageListeners.forEach(cb => cb(message)); } // --- Persistenz --- private static readonly STORAGE_KEY = 'rvs_config'; private async saveConfig(config: ConnectionConfig): Promise { try { await AsyncStorage.setItem(RVSConnection.STORAGE_KEY, JSON.stringify(config)); console.log('[RVS] Konfiguration gespeichert'); } catch (err) { console.error('[RVS] Fehler beim Speichern:', err); } } async loadConfig(): Promise { try { const data = await AsyncStorage.getItem(RVSConnection.STORAGE_KEY); if (data) { this.config = JSON.parse(data); console.log('[RVS] Konfiguration geladen'); return this.config; } return null; } catch (err) { console.error('[RVS] Fehler beim Laden:', err); return null; } } } // Singleton-Instanz const rvs = new RVSConnection(); export default rvs;