v2.55.1 — Code-Review-Fixes: Singleton, Race-Conditions, Fallback-Timer, Log-Security
CRITICAL (C1): RealtimeSyncService jetzt Singleton (Factory-Pattern) - Verhindert doppelte SSE-Verbindungen durch home_screen + settings - UI-Notifier (istVerbundenNotifier) app-weit konsistent HIGH (H1): _reconnecteOderFallback + starteWennAktiviert setzen cloud_interval vor starteAutoSyncTimer() — sonst bleibt Timer bei Fallback auf 0 (kein Sync) HIGH (H2): Race-Condition in _verbinde() nach await request.close() — _pausiert-Flag re-check; bei true → clean exit mit _laeuft=false HIGH (H3): fortsetzen() re-checkt _pausiert nach starteWennAktiviert() — verhindert Leak bei schnellem App-Umschalten MEDIUM (M1): _syncModusSetzen stoppt SSE bei Wechsel auf manual/interval (vorher lief die Verbindung weiter) MEDIUM (M2): Event-Logging loggt nur noch Event-Typ, nicht rohe Daten (Privacy: Song-IDs/Titel/Künstler nicht im Log) LOW (L1): _verbinde() setzt _laeuft=false bei Token-Fehler (kein Deadlock)
This commit is contained in:
@@ -33,8 +33,13 @@ enum SyncModus {
|
||||
/// **Lifecycle** (via WidgetsBindingObserver in main.dart):
|
||||
/// - resumed → fortsetzen() (SSE neu verbinden)
|
||||
/// - paused/hidden → pausiere() (Batterie sparen)
|
||||
///
|
||||
/// **Singleton**: Nur EINE Instanz pro App-Lebenszyklus, damit
|
||||
/// home_screen.dart und settings_screen.dart dieselbe Verbindung teilen.
|
||||
class RealtimeSyncService {
|
||||
RealtimeSyncService()
|
||||
static final RealtimeSyncService _instanz = RealtimeSyncService._();
|
||||
factory RealtimeSyncService() => _instanz;
|
||||
RealtimeSyncService._()
|
||||
: _db = DbHelper(),
|
||||
_auth = AuthService(),
|
||||
_favoriten = FavoritenService();
|
||||
@@ -123,6 +128,9 @@ class RealtimeSyncService {
|
||||
// Fallback: sync_modus auf interval setzen + Auto-Sync-Timer starten
|
||||
final p = await SharedPreferences.getInstance();
|
||||
await p.setString('sync_modus', 'interval');
|
||||
// cloud_interval setzen, damit starteAutoSyncTimer() den Timer startet
|
||||
final intervallStunden = await cloudIntervallStunden();
|
||||
await p.setInt('cloud_interval', intervallStunden);
|
||||
await SyncService.starteAutoSyncTimer();
|
||||
stoppe();
|
||||
return;
|
||||
@@ -164,6 +172,7 @@ class RealtimeSyncService {
|
||||
final token = _auth.token;
|
||||
if (token == null || token.isEmpty) {
|
||||
_fehlerBericht('Kein Auth-Token');
|
||||
_laeuft = false;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -179,6 +188,14 @@ class RealtimeSyncService {
|
||||
|
||||
_sseResponse = await request.close();
|
||||
|
||||
// Nach await prüfen: pausiere() könnte während des Connects
|
||||
// aufgerufen worden sein (WidgetsBindingObserver-Race).
|
||||
if (_pausiert) {
|
||||
_sseResponse = null;
|
||||
_laeuft = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (_sseResponse!.statusCode != 200) {
|
||||
_fehlerBericht('HTTP ${_sseResponse!.statusCode}');
|
||||
await _reconnecteOderFallback();
|
||||
@@ -249,7 +266,8 @@ class RealtimeSyncService {
|
||||
|
||||
MeloLogger().aktion('realtime_event', {
|
||||
'type': eventTyp,
|
||||
'data': daten,
|
||||
// Daten nicht loggen — könnten sensitive User-Informationen enthalten
|
||||
// (Song-IDs, Titel, Künstler). Nur Event-Typ wird geloggt.
|
||||
});
|
||||
|
||||
switch (eventTyp) {
|
||||
@@ -391,6 +409,10 @@ class RealtimeSyncService {
|
||||
MeloLogger().aktion('realtime_fallback_to_interval', {});
|
||||
final p = await SharedPreferences.getInstance();
|
||||
await p.setString('sync_modus', 'interval');
|
||||
// cloud_interval (alter Key) auf Default 48h setzen, damit
|
||||
// SyncService.starteAutoSyncTimer() den Timer auch wirklich startet.
|
||||
final intervallStunden = await cloudIntervallStunden();
|
||||
await p.setInt('cloud_interval', intervallStunden);
|
||||
await SyncService.starteAutoSyncTimer();
|
||||
}
|
||||
}
|
||||
@@ -435,6 +457,12 @@ class RealtimeSyncService {
|
||||
if (!_pausiert) return;
|
||||
_pausiert = false;
|
||||
await starteWennAktiviert();
|
||||
// Nach await prüfen: pausiere() könnte während starteWennAktiviert()
|
||||
// aufgerufen worden sein (z.B. schnelles App-Umschalten).
|
||||
// Falls ja, Verbindung sofort wieder trennen.
|
||||
if (_pausiert) {
|
||||
stoppe();
|
||||
}
|
||||
}
|
||||
|
||||
/// Stoppt die SSE-Verbindung endgültig (z. B. Logout, Modus-Wechsel).
|
||||
|
||||
Reference in New Issue
Block a user