mirror of
https://github.com/vleeuwenmenno/supplements.git
synced 2025-09-11 18:29:12 +02:00
feat: adds auto sync feature and fixes UI a bit up
This commit is contained in:
470
lib/services/auto_sync_service.dart
Normal file
470
lib/services/auto_sync_service.dart
Normal file
@@ -0,0 +1,470 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'dart:math';
|
||||
import 'package:flutter/foundation.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<AutoSyncError> _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) {
|
||||
print('AutoSyncService: Auto-sync is disabled, skipping trigger');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if auto-sync was disabled due to persistent errors
|
||||
if (_autoDisabledDueToErrors) {
|
||||
if (kDebugMode) {
|
||||
print('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) {
|
||||
print('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) {
|
||||
print('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) {
|
||||
print('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) {
|
||||
print('AutoSyncService: Auto-sync scheduled in ${debounceSeconds}s');
|
||||
}
|
||||
}
|
||||
|
||||
/// Executes the pending sync operation
|
||||
Future<void> _executePendingSync() async {
|
||||
// Double-check conditions before executing
|
||||
if (!_settingsProvider.autoSyncEnabled) {
|
||||
if (kDebugMode) {
|
||||
print('AutoSyncService: Auto-sync disabled during execution, aborting');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (_autoDisabledDueToErrors) {
|
||||
if (kDebugMode) {
|
||||
print('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) {
|
||||
print('AutoSyncService: Sync not configured during execution, aborting');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (_syncInProgress || _syncProvider.isSyncing) {
|
||||
if (kDebugMode) {
|
||||
print('AutoSyncService: Sync already in progress during execution, aborting');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
_syncInProgress = true;
|
||||
_hasPendingSync = false;
|
||||
|
||||
try {
|
||||
if (kDebugMode) {
|
||||
print('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) {
|
||||
print('AutoSyncService: Auto-sync completed successfully');
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
if (kDebugMode) {
|
||||
print('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) {
|
||||
print('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) {
|
||||
print('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) {
|
||||
print('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) {
|
||||
print('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) {
|
||||
print('AutoSyncService: Retrying auto-sync after backoff delay');
|
||||
}
|
||||
triggerAutoSync();
|
||||
});
|
||||
|
||||
if (kDebugMode) {
|
||||
print('AutoSyncService: Scheduled retry in ${retryDelay}s');
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks if network is available
|
||||
Future<bool> _isNetworkAvailable() async {
|
||||
try {
|
||||
final result = await InternetAddress.lookup('google.com');
|
||||
return result.isNotEmpty && result[0].rawAddress.isNotEmpty;
|
||||
} catch (e) {
|
||||
if (kDebugMode) {
|
||||
print('AutoSyncService: Network check failed: $e');
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Cancels any pending sync operation
|
||||
void cancelPendingSync() {
|
||||
_cancelPendingSync();
|
||||
_retryTimer?.cancel();
|
||||
_retryTimer = null;
|
||||
_hasPendingSync = false;
|
||||
|
||||
if (kDebugMode) {
|
||||
print('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) {
|
||||
print('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) {
|
||||
print('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<AutoSyncError> 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;
|
||||
}
|
Reference in New Issue
Block a user