sojorn/sojorn_app/lib/services/sync_manager.dart
2026-02-15 00:33:24 -06:00

169 lines
4.8 KiB
Dart

import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'auth_service.dart';
import 'chat_backup_manager.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();
// Defer heavy sync work so the UI thread isn't blocked
Future.delayed(const Duration(milliseconds: 500), () {
if (!_initialized) return;
if (kDebugMode) debugPrint('[SyncManager] Deferred startup sync starting');
// On web, skip eager chat sync — Hive/IndexedDB is too slow to open
// at startup (~130s). Chat loads lazily when user opens a conversation.
if (!kIsWeb) {
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) {
_stopTimer();
// Trigger backup when app goes to background
ChatBackupManager.instance.triggerBackupIfNeeded();
} else if (state == AppLifecycleState.inactive ||
state == AppLifecycleState.detached) {
_stopTimer();
}
}
void _handleAuthChange(AuthState state) {
if (state.event == AuthChangeEvent.signedIn ||
state.event == AuthChangeEvent.tokenRefreshed) {
_startTimer();
if (!kIsWeb) {
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 && !kIsWeb) {
_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;
}
// On web, skip eager hydration — Hive/IndexedDB startup is too slow.
if (!kIsWeb) {
await ensureHistoryLoaded();
}
await _secureChatService.syncAllConversations(force: true);
_lastSyncAt = DateTime.now();
// Schedule auto-backup after successful sync
ChatBackupManager.instance.scheduleBackup();
} 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;
}
}
}