mirror of
https://github.com/vleeuwenmenno/supplements.git
synced 2025-09-11 18:29:12 +02:00
- Updated date and time formatting in debug notifications screen for clarity. - Wrapped context-dependent state updates in post-frame callbacks in history screen to ensure proper context usage. - Improved layout and styling in settings screen by reordering radio list tiles. - Enhanced logging in auto sync service for better error tracking. - Added context mounted checks in notification router to prevent errors during navigation. - Updated bulk take dialog to use new UI components from shadcn_ui package. - Refactored take supplement dialog to utilize shadcn_ui for a more modern look and feel. - Adjusted info chip and supplement card widgets to use updated color schemes and layouts. - Updated pubspec.yaml and pubspec.lock to include new dependencies and versions.
471 lines
14 KiB
Dart
471 lines
14 KiB
Dart
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<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) {
|
|
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<void> _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<bool> _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<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;
|
|
} |