Initial commit: unionflow-mobile-apps
Application Flutter complète (sans build artifacts). Signed-off-by: lions dev Team
This commit is contained in:
98
lib/core/storage/dashboard_cache_manager.dart
Normal file
98
lib/core/storage/dashboard_cache_manager.dart
Normal file
@@ -0,0 +1,98 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:async';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import '../utils/logger.dart';
|
||||
import '../../features/authentication/data/models/user_role.dart';
|
||||
|
||||
/// UnionFlow Mobile - Gestionnaire de Cache Unique (DRY)
|
||||
/// Gère le cache mémoire (L1) et disque (L2) avec SharedPreferences.
|
||||
class DashboardCacheManager {
|
||||
static final Map<String, dynamic> _memoryCache = {};
|
||||
static final Map<String, DateTime> _cacheTimestamps = {};
|
||||
static SharedPreferences? _prefs;
|
||||
|
||||
static const Duration _defaultExpiry = Duration(minutes: 15);
|
||||
|
||||
static Future<void> initialize() async {
|
||||
_prefs = await SharedPreferences.getInstance();
|
||||
debugPrint('📦 DashboardCacheManager Initialisé');
|
||||
}
|
||||
|
||||
static T? get<T>(String key) {
|
||||
// 1. Check mémoire
|
||||
if (_memoryCache.containsKey(key)) {
|
||||
final ts = _cacheTimestamps[key];
|
||||
if (ts != null && DateTime.now().difference(ts) < _defaultExpiry) {
|
||||
return _memoryCache[key] as T?;
|
||||
}
|
||||
}
|
||||
// 2. Check disque
|
||||
if (_prefs != null) {
|
||||
final jsonStr = _prefs!.getString('cache_$key');
|
||||
if (jsonStr != null) {
|
||||
try {
|
||||
final data = jsonDecode(jsonStr);
|
||||
_memoryCache[key] = data;
|
||||
_cacheTimestamps[key] = DateTime.now();
|
||||
return data as T?;
|
||||
} catch (e, st) {
|
||||
AppLogger.error('DashboardCacheManager.get: décodage JSON échoué pour key=$key', error: e, stackTrace: st);
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static Future<void> set<T>(String key, T value) async {
|
||||
_memoryCache[key] = value;
|
||||
_cacheTimestamps[key] = DateTime.now();
|
||||
if (_prefs != null) {
|
||||
try {
|
||||
await _prefs!.setString('cache_$key', jsonEncode(value));
|
||||
} catch (e, st) {
|
||||
AppLogger.error('DashboardCacheManager.set: écriture disque échouée pour key=$key', error: e, stackTrace: st);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static Future<void> invalidateForRole(UserRole role) async {
|
||||
_memoryCache.removeWhere((key, _) => key.contains(role.name));
|
||||
_cacheTimestamps.removeWhere((key, _) => key.contains(role.name));
|
||||
if (_prefs != null) {
|
||||
final keys = _prefs!.getKeys().where((k) => k.startsWith('cache_') && k.contains(role.name));
|
||||
for (final k in keys) {
|
||||
await _prefs!.remove(k);
|
||||
}
|
||||
}
|
||||
debugPrint('🗑️ Cache invalidé pour le rôle: ${role.displayName}');
|
||||
}
|
||||
|
||||
static Future<void> clear() async {
|
||||
_memoryCache.clear();
|
||||
_cacheTimestamps.clear();
|
||||
if (_prefs != null) {
|
||||
final keys = _prefs!.getKeys().where((k) => k.startsWith('cache_'));
|
||||
for (final k in keys) {
|
||||
await _prefs!.remove(k);
|
||||
}
|
||||
}
|
||||
debugPrint('🧹 Cache vidé globalement');
|
||||
}
|
||||
|
||||
/// Délégation instance pour l’injection de dépendances
|
||||
Future<void> setKey<T>(String key, T value) async => set<T>(key, value);
|
||||
|
||||
/// Délégation instance pour l’injection de dépendances
|
||||
T? getKey<T>(String key) => get<T>(key);
|
||||
|
||||
/// Statistiques du cache (entrées, etc.)
|
||||
Map<String, dynamic> getCacheStats() {
|
||||
final latest = _cacheTimestamps.isEmpty ? null : _cacheTimestamps.values.reduce((a, b) => a.isAfter(b) ? a : b);
|
||||
return {
|
||||
'entries': _memoryCache.length,
|
||||
'timestamp': latest?.toIso8601String(),
|
||||
};
|
||||
}
|
||||
}
|
||||
154
lib/core/storage/pending_operations_store.dart
Normal file
154
lib/core/storage/pending_operations_store.dart
Normal file
@@ -0,0 +1,154 @@
|
||||
/// Storage for pending operations that failed due to network issues
|
||||
library pending_operations_store;
|
||||
|
||||
import 'dart:convert';
|
||||
import 'package:injectable/injectable.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import '../utils/logger.dart' show AppLogger;
|
||||
|
||||
/// Store for persisting failed operations to retry later
|
||||
@singleton
|
||||
class PendingOperationsStore {
|
||||
static const String _keyPendingOperations = 'pending_operations';
|
||||
final SharedPreferences _preferences;
|
||||
|
||||
PendingOperationsStore(this._preferences);
|
||||
|
||||
/// Add a pending operation to the store
|
||||
Future<void> addPendingOperation({
|
||||
required String operationType,
|
||||
required String endpoint,
|
||||
required Map<String, dynamic> data,
|
||||
Map<String, String>? headers,
|
||||
}) async {
|
||||
try {
|
||||
final operations = await getPendingOperations();
|
||||
|
||||
final newOperation = {
|
||||
'id': DateTime.now().millisecondsSinceEpoch.toString(),
|
||||
'operationType': operationType,
|
||||
'endpoint': endpoint,
|
||||
'data': data,
|
||||
'headers': headers ?? {},
|
||||
'timestamp': DateTime.now().toIso8601String(),
|
||||
'retryCount': 0,
|
||||
};
|
||||
|
||||
operations.add(newOperation);
|
||||
|
||||
await _saveOperations(operations);
|
||||
|
||||
AppLogger.info('Pending operation added: $operationType on $endpoint');
|
||||
} catch (e) {
|
||||
AppLogger.error('Failed to add pending operation', error: e);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// Get all pending operations
|
||||
Future<List<Map<String, dynamic>>> getPendingOperations() async {
|
||||
try {
|
||||
final json = _preferences.getString(_keyPendingOperations);
|
||||
|
||||
if (json == null || json.isEmpty) {
|
||||
return [];
|
||||
}
|
||||
|
||||
final List<dynamic> decoded = jsonDecode(json);
|
||||
return decoded.cast<Map<String, dynamic>>();
|
||||
} catch (e) {
|
||||
AppLogger.error('Failed to get pending operations', error: e);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove a pending operation by ID
|
||||
Future<void> removePendingOperation(String id) async {
|
||||
try {
|
||||
final operations = await getPendingOperations();
|
||||
operations.removeWhere((op) => op['id'] == id);
|
||||
await _saveOperations(operations);
|
||||
|
||||
AppLogger.info('Pending operation removed: $id');
|
||||
} catch (e) {
|
||||
AppLogger.error('Failed to remove pending operation', error: e);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// Update retry count for an operation
|
||||
Future<void> incrementRetryCount(String id) async {
|
||||
try {
|
||||
final operations = await getPendingOperations();
|
||||
|
||||
final index = operations.indexWhere((op) => op['id'] == id);
|
||||
if (index != -1) {
|
||||
operations[index]['retryCount'] = (operations[index]['retryCount'] ?? 0) + 1;
|
||||
operations[index]['lastRetryTimestamp'] = DateTime.now().toIso8601String();
|
||||
await _saveOperations(operations);
|
||||
}
|
||||
} catch (e) {
|
||||
AppLogger.error('Failed to increment retry count', error: e);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// Clear all pending operations
|
||||
Future<void> clearAll() async {
|
||||
try {
|
||||
await _preferences.remove(_keyPendingOperations);
|
||||
AppLogger.info('All pending operations cleared');
|
||||
} catch (e) {
|
||||
AppLogger.error('Failed to clear pending operations', error: e);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove operations older than a certain duration
|
||||
Future<void> removeOldOperations({Duration maxAge = const Duration(days: 7)}) async {
|
||||
try {
|
||||
final operations = await getPendingOperations();
|
||||
final now = DateTime.now();
|
||||
|
||||
final filtered = operations.where((op) {
|
||||
final timestamp = DateTime.parse(op['timestamp'] as String);
|
||||
return now.difference(timestamp) < maxAge;
|
||||
}).toList();
|
||||
|
||||
if (filtered.length != operations.length) {
|
||||
await _saveOperations(filtered);
|
||||
AppLogger.info('Removed ${operations.length - filtered.length} old operations');
|
||||
}
|
||||
} catch (e) {
|
||||
AppLogger.error('Failed to remove old operations', error: e);
|
||||
}
|
||||
}
|
||||
|
||||
/// Get operations by type
|
||||
Future<List<Map<String, dynamic>>> getOperationsByType(String operationType) async {
|
||||
try {
|
||||
final operations = await getPendingOperations();
|
||||
return operations.where((op) => op['operationType'] == operationType).toList();
|
||||
} catch (e) {
|
||||
AppLogger.error('Failed to get operations by type', error: e);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/// Get count of pending operations
|
||||
Future<int> getCount() async {
|
||||
final operations = await getPendingOperations();
|
||||
return operations.length;
|
||||
}
|
||||
|
||||
/// Save operations to storage
|
||||
Future<void> _saveOperations(List<Map<String, dynamic>> operations) async {
|
||||
try {
|
||||
final json = jsonEncode(operations);
|
||||
await _preferences.setString(_keyPendingOperations, json);
|
||||
} catch (e) {
|
||||
AppLogger.error('Failed to save operations', error: e);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user