import 'dart:async'; import 'dart:io'; import 'dart:math'; import 'package:flutter/foundation.dart'; import 'package:supplements/logging.dart'; import '../providers/settings_provider.dart'; import '../providers/simple_sync_provider.dart'; /// Error types for auto-sync operations enum AutoSyncErrorType { network, configuration, authentication, server, unknown, } /// Represents an auto-sync error with context class AutoSyncError { final AutoSyncErrorType type; final String message; final DateTime timestamp; final dynamic originalError; AutoSyncError({ required this.type, required this.message, required this.timestamp, this.originalError, }); @override String toString() => 'AutoSyncError($type): $message'; } /// Service that handles automatic synchronization with debouncing logic /// to prevent excessive sync requests when multiple data changes occur rapidly. class AutoSyncService { Timer? _debounceTimer; bool _syncInProgress = false; bool _hasPendingSync = false; // Error handling and retry logic final List _recentErrors = []; int _consecutiveFailures = 0; DateTime? _lastFailureTime; Timer? _retryTimer; bool _autoDisabledDueToErrors = false; // Exponential backoff configuration static const int _maxRetryAttempts = 5; static const int _baseRetryDelaySeconds = 30; static const int _maxRetryDelaySeconds = 300; // 5 minutes static const int _errorHistoryMaxSize = 10; static const int _autoDisableThreshold = 3; // Consecutive failures before auto-disable final SimpleSyncProvider _syncProvider; final SettingsProvider _settingsProvider; AutoSyncService({ required SimpleSyncProvider syncProvider, required SettingsProvider settingsProvider, }) : _syncProvider = syncProvider, _settingsProvider = settingsProvider; /// Triggers an auto-sync if enabled in settings. /// Uses debouncing to prevent excessive sync requests. void triggerAutoSync() { // Check if auto-sync is enabled if (!_settingsProvider.autoSyncEnabled) { if (kDebugMode) { printLog('AutoSyncService: Auto-sync is disabled, skipping trigger'); } return; } // Check if auto-sync was disabled due to persistent errors if (_autoDisabledDueToErrors) { if (kDebugMode) { printLog('AutoSyncService: Auto-sync disabled due to persistent errors, skipping trigger'); } return; } // Check if sync is configured if (!_syncProvider.isConfigured) { _recordError(AutoSyncError( type: AutoSyncErrorType.configuration, message: 'Sync not configured. Please configure cloud sync settings.', timestamp: DateTime.now(), )); if (kDebugMode) { printLog('AutoSyncService: Sync not configured, skipping auto-sync'); } return; } // If sync is already in progress, mark that we have a pending sync if (_syncInProgress || _syncProvider.isSyncing) { _hasPendingSync = true; if (kDebugMode) { printLog('AutoSyncService: Sync in progress, marking pending sync'); } return; } // Cancel existing timer if one is running _cancelPendingSync(); // Check if we should apply exponential backoff final backoffDelay = _calculateBackoffDelay(); if (backoffDelay > 0) { if (kDebugMode) { printLog('AutoSyncService: Applying backoff delay of ${backoffDelay}s due to recent failures'); } _debounceTimer = Timer(Duration(seconds: backoffDelay), () { _executePendingSync(); }); return; } // Start new debounce timer final debounceSeconds = _settingsProvider.autoSyncDebounceSeconds; _debounceTimer = Timer(Duration(seconds: debounceSeconds), () { _executePendingSync(); }); if (kDebugMode) { printLog('AutoSyncService: Auto-sync scheduled in ${debounceSeconds}s'); } } /// Executes the pending sync operation Future _executePendingSync() async { // Double-check conditions before executing if (!_settingsProvider.autoSyncEnabled) { if (kDebugMode) { printLog('AutoSyncService: Auto-sync disabled during execution, aborting'); } return; } if (_autoDisabledDueToErrors) { if (kDebugMode) { printLog('AutoSyncService: Auto-sync disabled due to errors during execution, aborting'); } return; } if (!_syncProvider.isConfigured) { _recordError(AutoSyncError( type: AutoSyncErrorType.configuration, message: 'Sync not configured during execution', timestamp: DateTime.now(), )); if (kDebugMode) { printLog('AutoSyncService: Sync not configured during execution, aborting'); } return; } if (_syncInProgress || _syncProvider.isSyncing) { if (kDebugMode) { printLog('AutoSyncService: Sync already in progress during execution, aborting'); } return; } _syncInProgress = true; _hasPendingSync = false; try { if (kDebugMode) { printLog('AutoSyncService: Executing auto-sync (attempt ${_consecutiveFailures + 1})'); } // Check network connectivity before attempting sync if (!await _isNetworkAvailable()) { throw AutoSyncError( type: AutoSyncErrorType.network, message: 'Network is not available', timestamp: DateTime.now(), ); } await _syncProvider.syncDatabase(isAutoSync: true); // Reset failure count on successful sync _consecutiveFailures = 0; _lastFailureTime = null; _autoDisabledDueToErrors = false; if (kDebugMode) { printLog('AutoSyncService: Auto-sync completed successfully'); } } catch (e) { if (kDebugMode) { printLog('AutoSyncService: Auto-sync failed: $e'); } // Handle specific error types _handleSyncError(e); } finally { _syncInProgress = false; // If there was a pending sync request while we were syncing, trigger it if (_hasPendingSync && !_autoDisabledDueToErrors) { if (kDebugMode) { printLog('AutoSyncService: Processing queued sync request'); } _hasPendingSync = false; // Use a small delay to avoid immediate re-triggering Timer(const Duration(milliseconds: 500), () { triggerAutoSync(); }); } } } /// Handles sync errors with appropriate recovery strategies void _handleSyncError(dynamic error) { _consecutiveFailures++; _lastFailureTime = DateTime.now(); final autoSyncError = _categorizeError(error); _recordError(autoSyncError); // Check if we should disable auto-sync due to persistent errors if (_consecutiveFailures >= _autoDisableThreshold) { _autoDisabledDueToErrors = true; if (kDebugMode) { printLog('AutoSyncService: Auto-sync disabled due to $_consecutiveFailures consecutive failures'); } // For configuration errors, disable immediately if (autoSyncError.type == AutoSyncErrorType.configuration || autoSyncError.type == AutoSyncErrorType.authentication) { if (kDebugMode) { printLog('AutoSyncService: Auto-sync disabled due to configuration/authentication error'); } } } // Schedule retry for recoverable errors (unless auto-disabled) if (!_autoDisabledDueToErrors && _shouldRetry(autoSyncError.type)) { _scheduleRetry(); } } /// Categorizes an error into a specific AutoSyncError type AutoSyncError _categorizeError(dynamic error) { final errorString = error.toString().toLowerCase(); // Network-related errors if (error is SocketException || errorString.contains('network') || errorString.contains('connection') || errorString.contains('timeout') || errorString.contains('unreachable') || errorString.contains('host lookup failed') || errorString.contains('no route to host')) { return AutoSyncError( type: AutoSyncErrorType.network, message: 'Network connection failed. Check your internet connection.', timestamp: DateTime.now(), originalError: error, ); } // Configuration-related errors if (errorString.contains('not configured') || errorString.contains('invalid url') || errorString.contains('malformed url')) { return AutoSyncError( type: AutoSyncErrorType.configuration, message: 'Sync configuration is invalid. Please check your sync settings.', timestamp: DateTime.now(), originalError: error, ); } // Authentication errors if (errorString.contains('authentication') || errorString.contains('unauthorized') || errorString.contains('401') || errorString.contains('403') || errorString.contains('invalid credentials')) { return AutoSyncError( type: AutoSyncErrorType.authentication, message: 'Authentication failed. Please check your username and password.', timestamp: DateTime.now(), originalError: error, ); } // Server errors if (errorString.contains('500') || errorString.contains('502') || errorString.contains('503') || errorString.contains('504') || errorString.contains('server error')) { return AutoSyncError( type: AutoSyncErrorType.server, message: 'Server error occurred. The sync server may be temporarily unavailable.', timestamp: DateTime.now(), originalError: error, ); } // Unknown errors return AutoSyncError( type: AutoSyncErrorType.unknown, message: 'An unexpected error occurred during sync: ${error.toString()}', timestamp: DateTime.now(), originalError: error, ); } /// Records an error in the recent errors list void _recordError(AutoSyncError error) { _recentErrors.add(error); // Keep only recent errors if (_recentErrors.length > _errorHistoryMaxSize) { _recentErrors.removeAt(0); } if (kDebugMode) { printLog('AutoSyncService: Recorded error: $error'); } } /// Determines if we should retry for a given error type bool _shouldRetry(AutoSyncErrorType errorType) { switch (errorType) { case AutoSyncErrorType.network: case AutoSyncErrorType.server: return _consecutiveFailures < _maxRetryAttempts; case AutoSyncErrorType.configuration: case AutoSyncErrorType.authentication: return false; // Don't retry config/auth errors case AutoSyncErrorType.unknown: return _consecutiveFailures < _maxRetryAttempts; } } /// Calculates the backoff delay based on consecutive failures int _calculateBackoffDelay() { if (_consecutiveFailures == 0 || _lastFailureTime == null) { return 0; } // Calculate exponential backoff: base * (2^failures) final backoffSeconds = min( _baseRetryDelaySeconds * pow(2, _consecutiveFailures - 1).toInt(), _maxRetryDelaySeconds, ); // Check if enough time has passed since last failure final timeSinceLastFailure = DateTime.now().difference(_lastFailureTime!).inSeconds; if (timeSinceLastFailure >= backoffSeconds) { return 0; // No additional delay needed } return backoffSeconds - timeSinceLastFailure; } /// Schedules a retry attempt with exponential backoff void _scheduleRetry() { final retryDelay = _calculateBackoffDelay(); if (retryDelay <= 0) return; _retryTimer?.cancel(); _retryTimer = Timer(Duration(seconds: retryDelay), () { if (kDebugMode) { printLog('AutoSyncService: Retrying auto-sync after backoff delay'); } triggerAutoSync(); }); if (kDebugMode) { printLog('AutoSyncService: Scheduled retry in ${retryDelay}s'); } } /// Checks if network is available Future _isNetworkAvailable() async { try { final result = await InternetAddress.lookup('google.com'); return result.isNotEmpty && result[0].rawAddress.isNotEmpty; } catch (e) { if (kDebugMode) { printLog('AutoSyncService: Network check failed: $e'); } return false; } } /// Cancels any pending sync operation void cancelPendingSync() { _cancelPendingSync(); _retryTimer?.cancel(); _retryTimer = null; _hasPendingSync = false; if (kDebugMode) { printLog('AutoSyncService: Cancelled pending sync and retry timer'); } } /// Internal method to cancel the debounce timer void _cancelPendingSync() { _debounceTimer?.cancel(); _debounceTimer = null; } /// Resets error state and re-enables auto-sync if it was disabled void resetErrorState() { _consecutiveFailures = 0; _lastFailureTime = null; _autoDisabledDueToErrors = false; _recentErrors.clear(); _retryTimer?.cancel(); _retryTimer = null; if (kDebugMode) { printLog('AutoSyncService: Error state reset, auto-sync re-enabled'); } } /// Disposes of the service and cleans up resources void dispose() { _cancelPendingSync(); _retryTimer?.cancel(); _retryTimer = null; _hasPendingSync = false; _syncInProgress = false; _recentErrors.clear(); if (kDebugMode) { printLog('AutoSyncService: Disposed'); } } /// Returns true if there is a pending sync operation bool get hasPendingSync => _hasPendingSync || _debounceTimer != null; /// Returns true if a sync is currently in progress bool get isSyncInProgress => _syncInProgress; /// Returns true if auto-sync was disabled due to persistent errors bool get isAutoDisabledDueToErrors => _autoDisabledDueToErrors; /// Returns the number of consecutive failures int get consecutiveFailures => _consecutiveFailures; /// Returns a copy of recent errors List get recentErrors => List.unmodifiable(_recentErrors); /// Returns the last error message suitable for display to users String? get lastErrorMessage { if (_recentErrors.isEmpty) return null; return _recentErrors.last.message; } /// Returns true if a retry is currently scheduled bool get hasScheduledRetry => _retryTimer != null; }