sojorn/sojorn_app/lib/services/sync_manager.dart

147 lines
3.9 KiB
Dart

import 'dart:async';
import 'package:flutter/widgets.dart';
import 'auth_service.dart';
import 'secure_chat_service.dart';
import 'simple_e2ee_service.dart';
import 'local_message_store.dart';
class SyncManager with WidgetsBindingObserver {
final SecureChatService _secureChatService;
final AuthService _authService;
final SimpleE2EEService _e2ee = SimpleE2EEService();
final LocalMessageStore _localStore = LocalMessageStore.instance;
final Duration _resumeThreshold;
final Duration _syncInterval;
Timer? _timer;
StreamSubscription<AuthState>? _authSub;
DateTime? _lastSyncAt;
bool _syncInProgress = false;
bool _initialized = false;
bool _hydrating = false;
SyncManager({
required SecureChatService secureChatService,
required AuthService authService,
Duration resumeThreshold = const Duration(minutes: 2),
Duration syncInterval = const Duration(minutes: 5),
}) : _secureChatService = secureChatService,
_authService = authService,
_resumeThreshold = resumeThreshold,
_syncInterval = syncInterval;
void init() {
if (_initialized) return;
_initialized = true;
WidgetsBinding.instance.addObserver(this);
_authSub = _authService.authStateChanges.listen(_handleAuthChange);
if (_authService.isAuthenticated) {
_startTimer();
unawaited(ensureHistoryLoaded());
_triggerSync(reason: 'startup');
}
}
void dispose() {
if (!_initialized) return;
_initialized = false;
WidgetsBinding.instance.removeObserver(this);
_authSub?.cancel();
_authSub = null;
_stopTimer();
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (state == AppLifecycleState.resumed) {
if (_authService.isAuthenticated) {
_startTimer();
_syncIfStale();
}
} else if (state == AppLifecycleState.paused ||
state == AppLifecycleState.inactive ||
state == AppLifecycleState.detached) {
_stopTimer();
}
}
void _handleAuthChange(AuthState state) {
if (state.event == AuthChangeEvent.signedIn ||
state.event == AuthChangeEvent.tokenRefreshed) {
_startTimer();
unawaited(ensureHistoryLoaded());
_triggerSync(reason: 'auth');
} else if (state.event == AuthChangeEvent.signedOut) {
_lastSyncAt = null;
_stopTimer();
}
}
void _startTimer() {
_timer?.cancel();
_timer = Timer.periodic(_syncInterval, (_) {
if (_authService.isAuthenticated) {
_triggerSync(reason: 'timer');
}
});
}
void _stopTimer() {
_timer?.cancel();
_timer = null;
}
void _syncIfStale() {
if (_lastSyncAt == null ||
DateTime.now().difference(_lastSyncAt!) > _resumeThreshold) {
_triggerSync(reason: 'resume');
}
}
Future<void> _triggerSync({required String reason}) async {
if (!_authService.isAuthenticated || _syncInProgress) return;
_syncInProgress = true;
try {
await _e2ee.initialize();
if (!_e2ee.isReady) {
return;
}
await ensureHistoryLoaded();
await _secureChatService.syncAllConversations(force: true);
_lastSyncAt = DateTime.now();
} catch (e) {
} finally {
_syncInProgress = false;
}
}
Future<void> ensureHistoryLoaded() async {
if (!_authService.isAuthenticated || _hydrating) return;
_hydrating = true;
try {
await _e2ee.initialize();
if (!_e2ee.isReady) {
return;
}
final conversations = await _secureChatService.getConversations();
for (final conv in conversations) {
final isEmpty =
(await _localStore.getMessageIdsForConversation(conv.id)).isEmpty;
if (isEmpty) {
await _secureChatService.fetchAndDecryptHistory(conv.id, limit: 50);
}
await _secureChatService.startLiveListener(conv.id);
}
} catch (e) {
} finally {
_hydrating = false;
}
}
}