fix(app): Zombie-Verbindung — App zeigt 'getrennt', empfaengt aber weiter

Symptom (live beobachtet): die App steht auf 'getrennt' und nichts, was Stefan
schreibt oder spricht, erreicht den Server — ARIAs Antwort und TTS kommen aber
trotzdem in der App an.

Ursache: Beim Verwerfen einer Verbindung wurden nur onclose und onerror
entfernt, NICHT onmessage. Die aufgegebene Verbindung lieferte deshalb weiter
Nachrichten an die App-Listener (daher die ankommende Antwort), waehrend this.ws
auf die neue/tote zeigte — send() lehnte ab und der Zustand blieb
'disconnected'. Betraf beide Abriss-Pfade (Neu-Connect und TLS-Fallback).

Fix: detachSocket() klemmt eine Verbindung vollstaendig ab (onopen/onmessage/
onclose/onerror + close) und wird in beiden Pfaden genutzt. Zusaetzlich ignoriert
der onmessage-Handler Nachrichten, die nicht von der AKTUELLEN Verbindung
stammen — sonst taeuscht ein Zombie weiter Betrieb vor. tsc: 0 Fehler.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-10-02 01:08:37 +02:00
co-authored by Claude Opus 4.8
parent dd1e1ac458
commit ea159d4ed7
+26 -7
View File
@@ -104,11 +104,7 @@ class RVSConnection {
// Verbindungen + doppelte Events.
if (this.ws) {
this.log('info', 'Bestehende WS-Verbindung wird geschlossen vor Neu-Connect');
try {
this.ws.onclose = null; // verhindert dass scheduleReconnect doppelt feuert
this.ws.onerror = null;
this.ws.close();
} catch (_) {}
this.detachSocket(this.ws);
this.ws = null;
}
@@ -215,7 +211,12 @@ class RVSConnection {
this.startHeartbeat();
};
const socket = this.ws; // genau DIESE Verbindung (nicht 'this.ws' spaeter)
this.ws.onmessage = (event: WebSocketMessageEvent) => {
// Nur die AKTUELLE Verbindung darf Nachrichten einspeisen. Sonst
// koennte eine alte, laengst aufgegebene Verbindung weiter Daten
// liefern und vortaeuschen, es sei alles in Ordnung.
if (socket !== this.ws) return;
try {
const message: RVSMessage = JSON.parse(event.data as string);
this.notifyMessageListeners(message);
@@ -258,8 +259,7 @@ class RVSConnection {
this.log('warn', 'TLS fehlgeschlagen — Fallback auf ws:// (ohne TLS)');
this.clearTimers();
if (this.ws) {
this.ws.onclose = null; // onclose-Handler entfernen um Doppel-Reconnect zu verhindern
try { this.ws.close(); } catch (_) {}
this.detachSocket(this.ws); // inkl. onmessage — sonst bleibt ein Zombie, der weiter empfaengt
}
this.ws = null;
this.shouldReconnect = true;
@@ -302,6 +302,25 @@ class RVSConnection {
}, HEARTBEAT_INTERVAL_MS);
}
/**
* Klemmt eine Verbindung VOLLSTAENDIG ab, bevor sie verworfen wird.
*
* Wichtig: frueher wurden nur onclose/onerror entfernt — onmessage blieb
* haengen. Eine aufgegebene (Zombie-)Verbindung lieferte dadurch weiter
* Nachrichten an die App, waehrend gesendet wurde ueber die neue/tote.
* Symptom: die App zeigt "getrennt", ARIAs Sprachausgabe kommt aber trotzdem
* an — und nichts, was man schreibt oder spricht, erreicht den Server.
*/
private detachSocket(ws: WebSocket): void {
try {
ws.onopen = null;
ws.onmessage = null; // <- das fehlte
ws.onclose = null; // verhindert doppelten Reconnect
ws.onerror = null;
ws.close();
} catch (_) {}
}
private clearTimers(): void {
if (this.heartbeatTimer) {
clearInterval(this.heartbeatTimer);