/
dbnrbv
/
Habit
Обзор
Документация
Войти
/
dbnrbv
/
Habit
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
develop
lib/data/sync/sync_manager.dart
235 строк
8 KB
Daba
refactor: improve architecture, logging, and streak calculation
01 июн 2026, 22:28
01 июн 2026, 22:28
7b5e4cd
Код
Авторство
О чём код?
import 'dart:async'; import '../datasources/local/habit_local_datasource.dart'; import '../datasources/remote/habit_remote_datasource.dart'; import '../../domain/models/habit.dart'; import '../../domain/models/habit_execution.dart'; import '../../core/services/connectivity_service.dart'; import '../../core/utils/app_logger.dart'; /// Менеджер синхронизации данных между локальным хранилищем (Hive) и Firebase class SyncManager { final HabitLocalDataSource _localDataSource; final HabitRemoteDataSource _remoteDataSource; final ConnectivityService _connectivityService; Timer? _syncTimer; StreamSubscription<bool>? _connectivitySubscription; bool _isSyncing = false; SyncManager({ required HabitLocalDataSource localDataSource, required HabitRemoteDataSource remoteDataSource, required ConnectivityService connectivityService, }) : _localDataSource = localDataSource, _remoteDataSource = remoteDataSource, _connectivityService = connectivityService; /// Запускает автоматическую синхронизацию void startAutoSync(String userId) { AppLogger.d('SYNC', 'Starting auto-sync for user $userId'); _connectivitySubscription?.cancel(); _connectivitySubscription = _connectivityService.onConnectivityChanged.listen((hasInternet) async { if (hasInternet) { AppLogger.d('SYNC', 'Internet restored, syncing...'); await syncAll(userId); } }); _syncTimer = Timer.periodic( const Duration(minutes: 15), (_) async { if (await _connectivityService.hasConnection()) { AppLogger.d('SYNC', 'Periodic sync triggered'); await syncIncremental(userId); } }, ); _performInitialSync(userId); } Future<void> _performInitialSync(String userId) async { final hasConnection = await _connectivityService.hasConnection(); if (hasConnection) { AppLogger.d('SYNC', 'Initial sync started...'); await syncAll(userId); AppLogger.d('SYNC', 'Initial sync completed'); } else { AppLogger.d('SYNC', 'No internet connection, working offline'); } } Future<void> syncAll(String userId) async { if (_isSyncing) { AppLogger.d('SYNC', 'Sync already in progress, skipping'); return; } await _doSyncAll(userId); } Future<void> _doSyncAll(String userId) async { _isSyncing = true; AppLogger.d('SYNC', 'Starting full sync for user $userId'); try { final modifiedHabitIds = _localDataSource.getLocallyModifiedHabitIds(); final modifiedHabits = <Habit>[]; for (final id in modifiedHabitIds) { final habit = _localDataSource.getHabit(id); if (habit != null) modifiedHabits.add(habit); } final remoteHabits = await _remoteDataSource.getHabitsForUserIncludingDeleted(userId); final remoteExecutions = await _remoteDataSource.getExecutionsForUser(userId); AppLogger.d('SYNC', 'Downloaded ${remoteHabits.length} habits and ${remoteExecutions.length} executions'); for (final remoteHabit in remoteHabits) { final isLocallyModified = modifiedHabitIds.contains(remoteHabit.id); if (isLocallyModified) { AppLogger.d('SYNC', 'Skipping remote overwrite for locally modified habit ${remoteHabit.id}'); continue; } final localHabit = _localDataSource.getHabit(remoteHabit.id); if (localHabit == null || remoteHabit.updatedAt.isAfter(localHabit.updatedAt)) { await _localDataSource.saveHabit(remoteHabit); } } await _localDataSource.saveExecutions(remoteExecutions); AppLogger.d('SYNC', 'Saved to local storage'); if (modifiedHabits.isNotEmpty) { AppLogger.d('SYNC', 'Pushing ${modifiedHabits.length} modified habits to server'); await _remoteDataSource.saveHabitsBatch(modifiedHabits); } await _localDataSource.setLastSyncTime(DateTime.now()); await _localDataSource.clearLocallyModifiedHabits(); AppLogger.d('SYNC', 'Full sync completed successfully'); } catch (e) { AppLogger.e('SYNC', 'Error during full sync', error: e); } finally { _isSyncing = false; } } Future<void> syncIncremental(String userId) async { if (_isSyncing) { AppLogger.d('SYNC', 'Sync already in progress, skipping incremental'); return; } _isSyncing = true; AppLogger.d('SYNC', 'Starting incremental sync'); try { final lastSyncTime = _localDataSource.getLastSyncTime(); if (lastSyncTime == null) { _isSyncing = false; AppLogger.d('SYNC', 'No last sync time, performing full sync instead'); await _doSyncAll(userId); return; } final remoteHabits = await _remoteDataSource.getHabitsModifiedAfter(userId, lastSyncTime); final remoteExecutions = await _remoteDataSource .getExecutionsCreatedAfter(userId, lastSyncTime); AppLogger.d('SYNC', 'Downloaded ${remoteHabits.length} habits and ${remoteExecutions.length} executions (incremental)'); for (final remoteHabit in remoteHabits) { final local = _localDataSource.getHabit(remoteHabit.id); if (local == null || remoteHabit.updatedAt.isAfter(local.updatedAt)) { await _localDataSource.saveHabit(remoteHabit); } } if (remoteExecutions.isNotEmpty) { await _localDataSource.saveExecutions(remoteExecutions); } final modifiedHabitIds = _localDataSource.getLocallyModifiedHabitIds(); final modifiedHabits = <Habit>[]; for (final id in modifiedHabitIds) { final habit = _localDataSource.getHabit(id); if (habit != null) modifiedHabits.add(habit); } if (modifiedHabits.isNotEmpty) { AppLogger.d('SYNC', 'Pushing ${modifiedHabits.length} modified habits to server'); await _remoteDataSource.saveHabitsBatch(modifiedHabits); } await _localDataSource.setLastSyncTime(DateTime.now()); await _localDataSource.clearLocallyModifiedHabits(); AppLogger.d('SYNC', 'Incremental sync completed'); } catch (e) { AppLogger.e('SYNC', 'Error during incremental sync', error: e); } finally { _isSyncing = false; } } Future<void> syncExecution(HabitExecution execution, String habitId) async { AppLogger.d('SYNC', 'Syncing execution for habit $habitId'); try { await _localDataSource.saveExecution(execution); await _localDataSource.markHabitAsModified(habitId); AppLogger.d('SYNC', 'Execution saved locally'); // Fire-and-forget — не блокируем UI ожиданием сервера _remoteDataSource.saveExecution(execution).then((_) { AppLogger.d('SYNC', 'Execution saved to server'); }).catchError((e) { AppLogger.w('SYNC', 'Failed to send execution to server (will retry later): $e'); }); } catch (e) { AppLogger.e('SYNC', 'Error syncing execution', error: e); rethrow; } } Future<void> deleteHabit(String habitId, String userId) async { AppLogger.d('SYNC', 'Deleting habit $habitId'); try { // Сохраняем локально — это быстро, UI получит ответ сразу await _localDataSource.deleteHabit(habitId); await _localDataSource.markHabitAsModified(habitId); AppLogger.d('SYNC', 'Habit deleted locally'); // Fire-and-forget — не блокируем UI ожиданием сервера. // Если офлайн — при следующем syncAll привычка с deletedAt уйдёт на сервер. _remoteDataSource.deleteHabit(habitId).then((_) { AppLogger.d('SYNC', 'Habit deleted from server'); }).catchError((e) { AppLogger.w( 'SYNC', 'Failed to delete from server (will sync later): $e'); }); } catch (e) { AppLogger.e('SYNC', 'Error deleting habit locally', error: e); rethrow; } } void dispose() { _syncTimer?.cancel(); _connectivitySubscription?.cancel(); AppLogger.d('SYNC', 'SyncManager disposed'); } }