fix(app): App-Reconnect nach Hintergrund — Sticky-Fallback, Zombie-WS, AppState-Hook

Stefan musste seit der HTTPS-Umstellung nach jedem Hintergrund-Rueckkehr
manuell auf "Verbinden" tippen, meist 3x bis es ging. Gleiche Bug-Klasse
wie auf der Bridge davor (Sticky-Fallback), plus zwei App-spezifische
Symptome.

Drei Ursachen:

1. usingTLSFallback klebt: einmal nach onerror auf true gesetzt, blieb
   es bei allen folgenden Reconnects → App versuchte ws://...:443 gegen
   den TLS-only Caddy → HTTP 400 → endlos. Reset war NUR im manuellen
   connect(), nicht in onclose oder scheduleReconnect.
   Fix: in onclose `usingTLSFallback = false` damit der naechste
   Reconnect wieder primary (wss://) probiert.

2. Zombie-WebSocket: Android kann den TCP-Socket im Background still
   killen, der JS-State zeigt aber noch readyState === OPEN. Stefans
   manueller "Verbinden"-Klick rief connect() → "Bereits verbunden"
   No-Op statt sich neu aufzubauen.
   Fix: connect(force=true) optional, bestehendes WS-Objekt wird hart
   geschlossen (mit onclose=null gegen Doppel-Reconnect) bevor neuer
   Aufbau startet.

3. Keine aktive Reconnect-Sequence bei Foreground-Resume: App war
   abhaengig von onclose-Events die bei Zombie-WS nicht zwingend
   feuern.
   Fix: AppState-Listener in App.tsx, bei background → active
   automatischer rvs.connect(true).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-25 10:09:30 +02:00
parent 72277098af
commit ad87c807de
2 changed files with 50 additions and 4 deletions
+19 -1
View File
@@ -6,7 +6,7 @@
*/
import React, { useEffect } from 'react';
import { PermissionsAndroid, Platform, StatusBar, StyleSheet } from 'react-native';
import { AppState, AppStateStatus, PermissionsAndroid, Platform, StatusBar, StyleSheet } from 'react-native';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { NavigationContainer, DefaultTheme } from '@react-navigation/native';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
@@ -107,8 +107,26 @@ const App: React.FC = () => {
console.warn('[App] GPS-Tracking restore fehlgeschlagen:', err?.message || err);
});
// AppState-Listener: nach Hintergrund-Rueckkehr aktiv die WS-
// Verbindung neu aufbauen. Hintergrund: Android kann den TCP-Socket
// im Background killen, JS-State zeigt aber noch OPEN → Stefan musste
// manuell in Settings auf "Verbinden" tippen, oft mehrfach. Mit dem
// force-Reconnect bei "active" greift das automatisch.
let lastAppState: AppStateStatus = AppState.currentState;
const appStateSub = AppState.addEventListener('change', (next) => {
const wasBg = lastAppState !== 'active';
lastAppState = next;
if (next === 'active' && wasBg) {
console.log('[App] Foreground-Resume — force-reconnect zum RVS');
try { rvs.connect(true); } catch (e: any) {
console.warn('[App] force-reconnect fehlgeschlagen:', e?.message || e);
}
}
});
// Beim Beenden: Verbindung sauber trennen
return () => {
appStateSub.remove();
rvs.disconnect();
};
}, []);
+31 -3
View File
@@ -83,21 +83,39 @@ class RVSConnection {
// --- Verbindung ---
/** Verbindung zum RVS aufbauen */
connect(): void {
/** Verbindung zum RVS aufbauen. force=true: bestehende Connection hart
* schliessen + neu verbinden (auch wenn JS denkt readyState=OPEN — kann
* nach Hintergrund-Pause ein Zombie-WS sein wo TCP tot ist aber JS-State
* noch OPEN zeigt; in dem Fall war "Bereits verbunden" ein No-Op und
* Stefan musste manuell zigmal klicken). */
connect(force: boolean = false): void {
if (!this.config) {
this.log('warn', 'Keine Verbindungskonfiguration vorhanden');
return;
}
if (this.ws?.readyState === WebSocket.OPEN) {
if (!force && this.ws?.readyState === WebSocket.OPEN) {
this.log('info', 'Bereits verbunden');
return;
}
// Wenn ein WS-Objekt da ist (Zombie oder lebend), sauber abreissen
// bevor wir einen neuen aufbauen — sonst gibt's zwei parallele
// 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.ws = null;
}
this.shouldReconnect = true;
this.reconnectDelay = INITIAL_RECONNECT_DELAY_MS;
this.usingTLSFallback = false;
this.clearTimers();
this.log('info', `Verbindungsaufbau zu ${this.config.host}:${this.config.port} (TLS: ${this.config.useTLS ? 'ja' : 'nein'})`);
this.establishConnection();
}
@@ -212,6 +230,16 @@ class RVSConnection {
this.ws = null;
this.setState('disconnected');
// Sticky-Fallback-Reset: beim naechsten Reconnect wieder primary
// (wss://) versuchen statt fuer immer auf ws:// zu kleben. War
// der Hauptgrund warum die App nach Hintergrund-Rueckkehr nicht
// mehr verband — TLS-Handshake-Timeout in einem Reconnect → Fallback
// auf ws:// → Caddy refused → endlos im Fallback haengen.
if (this.usingTLSFallback) {
this.log('info', 'Reset TLS-Fallback fuer naechsten Reconnect (zurueck zu wss://)');
this.usingTLSFallback = false;
}
if (this.shouldReconnect) {
this.scheduleReconnect();
}