/// Offline-first manager for connectivity monitoring and operation queueing library offline_manager; import 'dart:async'; import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:injectable/injectable.dart'; import '../storage/pending_operations_store.dart'; import '../utils/logger.dart' show AppLogger; /// Status of network connectivity enum ConnectivityStatus { online, offline, unknown, } /// Offline manager that monitors connectivity and manages offline operations @singleton class OfflineManager { final Connectivity _connectivity; final PendingOperationsStore _operationsStore; ConnectivityStatus _currentStatus = ConnectivityStatus.unknown; final _statusController = StreamController.broadcast(); StreamSubscription>? _connectivitySubscription; OfflineManager( this._connectivity, this._operationsStore, ) { _initConnectivityMonitoring(); } /// Current connectivity status ConnectivityStatus get currentStatus => _currentStatus; /// Stream of connectivity status changes Stream get statusStream => _statusController.stream; /// Check if device is currently online Future get isOnline async { final result = await _connectivity.checkConnectivity(); return result.any((r) => r != ConnectivityResult.none); } /// Initialize connectivity monitoring void _initConnectivityMonitoring() { // Check initial connectivity _connectivity.checkConnectivity().then((result) { _updateStatus(result); }); // Listen for connectivity changes _connectivitySubscription = _connectivity.onConnectivityChanged.listen( _updateStatus, onError: (error) { AppLogger.error('Connectivity monitoring error', error: error); _updateStatus([ConnectivityResult.none]); }, ); } /// Update connectivity status void _updateStatus(List results) { final isConnected = results.any((r) => r != ConnectivityResult.none); final newStatus = isConnected ? ConnectivityStatus.online : ConnectivityStatus.offline; if (newStatus != _currentStatus) { final previousStatus = _currentStatus; _currentStatus = newStatus; _statusController.add(newStatus); AppLogger.info('Connectivity changed: $previousStatus → $newStatus'); // When back online, process pending operations if (newStatus == ConnectivityStatus.online && previousStatus == ConnectivityStatus.offline) { _processPendingOperations(); } } } /// Queue an operation for later retry when offline Future queueOperation({ required String operationType, required String endpoint, required Map data, Map? headers, }) async { try { await _operationsStore.addPendingOperation( operationType: operationType, endpoint: endpoint, data: data, headers: headers, ); AppLogger.info('Operation queued: $operationType on $endpoint'); } catch (e) { AppLogger.error('Failed to queue operation', error: e); } } /// Process all pending operations when back online Future _processPendingOperations() async { AppLogger.info('Processing pending operations...'); try { final operations = await _operationsStore.getPendingOperations(); if (operations.isEmpty) { AppLogger.info('No pending operations to process'); return; } AppLogger.info('Found ${operations.length} pending operations'); // Process operations one by one for (final operation in operations) { try { // Note: Actual retry logic is delegated to the calling code // This manager only provides the queuing mechanism AppLogger.info('Pending operation ready for retry: ${operation['operationType']}'); } catch (e) { AppLogger.error('Error processing pending operation', error: e); } } } catch (e) { AppLogger.error('Failed to process pending operations', error: e); } } /// Manually trigger processing of pending operations Future retryPendingOperations() async { if (_currentStatus == ConnectivityStatus.online) { await _processPendingOperations(); } else { AppLogger.warning('Cannot retry pending operations while offline'); } } /// Clear all pending operations Future clearPendingOperations() async { try { await _operationsStore.clearAll(); AppLogger.info('Pending operations cleared'); } catch (e) { AppLogger.error('Failed to clear pending operations', error: e); } } /// Get count of pending operations Future getPendingOperationsCount() async { try { final operations = await _operationsStore.getPendingOperations(); return operations.length; } catch (e) { AppLogger.error('Failed to get pending operations count', error: e); return 0; } } /// Dispose resources void dispose() { _connectivitySubscription?.cancel(); _statusController.close(); } }