fix: Abmelden ohne verspaetete Sync-Antwort (SDK-Wettlauf) + Live-Regressionstest

logoutWithoutStraySync (lib/core/session_logout.dart): Sync-Schleife
anhalten, laufenden Long-Poll per Account-Daten wecken und auslaufen lassen,
erst dann abmelden - es ist keine Anfrage mit altem Token mehr unterwegs,
die per 401 ein zweites clear() ausloesen koennte. Genutzt an allen drei
Abmelde-Stellen (Einstellungen, Ueber, Konto geloescht) und im E2E-Live-Test.
Live gemessen: die verspaetete 401 tritt real auf, kam aber stets vor dem
Abschluss einer Neuanmeldung an - Risiko war klein, ist jetzt ausgeschlossen.
test/live_logout_race_test.dart in scripts/test.ps1 -Live.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
Bernd Steckmeister
2026-10-07 17:39:37 +02:00
co-authored by Claude Opus 5.5
parent f09e4875c6
commit e90b954348
11 changed files with 269 additions and 25 deletions
+58
View File
@@ -0,0 +1,58 @@
import 'package:flutter/foundation.dart';
import 'package:matrix/matrix.dart';
/// Kennung der Account-Daten, mit denen wir den laufenden Sync aufwecken.
const logoutWakeAccountDataType = 'io.pyramid.logout_wake';
/// Meldet [client] ab, ohne dass eine verspätete Server-Antwort die NÄCHSTE
/// Anmeldung wieder löscht.
///
/// Hintergrund (SDK-Wettlauf, ROADMAP M1, gefunden 2026-10-07): matrix 6.2
/// (und Upstream `main`) prüft bei einer FEHLER-Antwort des Sync-Long-Polls
/// nicht, ob die Anfrage noch aktuell ist. Läuft beim Abmelden noch ein
/// Long-Poll (bis 30 s) mit dem alten Token, kommt dessen 401
/// (M_UNKNOWN_TOKEN) später zurück und das SDK ruft `clear()` – meldet sich
/// der Nutzer in der Zwischenzeit auf demselben Client neu an (Pyramid
/// benutzt nach dem Logout dasselbe Client-Objekt weiter), wird die frische
/// Session gelöscht: ein „Random-Logout“ direkt nach dem Login.
///
/// Ablauf hier:
/// 1. Sync-Schleife anhalten (nach der laufenden Anfrage keine neue).
/// 2. Laufenden Long-Poll WECKEN: geänderte Account-Daten lässt der Server
/// sofort antworten – statt bis zu 30 s zu warten.
/// 3. Die laufende Anfrage regulär (mit gültigem Token) zu Ende gehen lassen.
/// 4. Erst dann serverseitig abmelden – es ist keine Anfrage mehr unterwegs.
///
/// Schritt 2/3 sind „best effort“: scheitern sie (offline), wird nach
/// höchstens ~8 s trotzdem abgemeldet – nie schlechter als das bisherige
/// direkte `logout()`.
///
/// Live gemessen (test/live_logout_race_test.dart, 2026-10-07): die
/// verspätete 401 tritt real auf, kommt aber meist an, BEVOR ein Mensch sich
/// neu angemeldet hat – das Risiko ist klein, dieser Weg schließt es aus.
Future<void> logoutWithoutStraySync(
Client client, {
Duration settleTimeout = const Duration(seconds: 5),
}) async {
client.backgroundSync = false;
// Liefert den laufenden Sync (oder startet einen kurzen ohne Long-Poll).
final inFlight = client.oneShotSync(timeout: Duration.zero);
final userId = client.userID;
if (userId != null) {
try {
await client
.setAccountData(userId, logoutWakeAccountDataType, {
'ts': DateTime.now().millisecondsSinceEpoch,
})
.timeout(const Duration(seconds: 3));
} catch (e) {
debugPrint('[LOGOUT] Sync-Wecken fehlgeschlagen (egal): $e');
}
}
try {
await inFlight.timeout(settleTimeout);
} catch (e) {
debugPrint('[LOGOUT] laufender Sync nicht rechtzeitig fertig: $e');
}
await client.logout();
}