Refactoring - Version OK
This commit is contained in:
@@ -0,0 +1,418 @@
|
||||
/// Gestionnaire de cache multi-niveaux ultra-performant
|
||||
/// Cache mémoire + disque avec TTL adaptatif selon les rôles
|
||||
library dashboard_cache_manager;
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import '../../features/authentication/data/models/user_role.dart';
|
||||
|
||||
/// Gestionnaire de cache intelligent avec stratégie multi-niveaux
|
||||
///
|
||||
/// Niveaux de cache :
|
||||
/// 1. Cache mémoire (ultra-rapide, volatile)
|
||||
/// 2. Cache disque (rapide, persistant)
|
||||
/// 3. Cache réseau (si applicable)
|
||||
///
|
||||
/// Fonctionnalités :
|
||||
/// - TTL adaptatif selon le rôle utilisateur
|
||||
/// - Compression automatique des données volumineuses
|
||||
/// - Invalidation intelligente
|
||||
/// - Métriques de performance
|
||||
/// - Nettoyage automatique
|
||||
class DashboardCacheManager {
|
||||
static final DashboardCacheManager _instance = DashboardCacheManager._internal();
|
||||
factory DashboardCacheManager() => _instance;
|
||||
DashboardCacheManager._internal();
|
||||
|
||||
/// Cache mémoire niveau 1 (ultra-rapide)
|
||||
static final Map<String, _CachedData> _memoryCache = {};
|
||||
|
||||
/// Instance SharedPreferences pour le cache disque
|
||||
static SharedPreferences? _prefs;
|
||||
|
||||
/// Taille maximale du cache mémoire (en nombre d'entrées)
|
||||
static const int _maxMemoryCacheSize = 1000;
|
||||
|
||||
/// Taille maximale du cache disque (en MB)
|
||||
static const int _maxDiskCacheSizeMB = 50;
|
||||
|
||||
/// TTL par défaut selon les rôles
|
||||
static const Map<UserRole, Duration> _roleTTL = {
|
||||
UserRole.superAdmin: Duration(hours: 2), // Cache plus long pour les admins
|
||||
UserRole.orgAdmin: Duration(hours: 1), // Cache modéré pour les admins org
|
||||
UserRole.moderator: Duration(minutes: 30), // Cache court pour les modérateurs
|
||||
UserRole.activeMember: Duration(minutes: 15), // Cache très court pour les membres
|
||||
UserRole.simpleMember: Duration(minutes: 10), // Cache minimal
|
||||
UserRole.visitor: Duration(minutes: 5), // Cache très court pour les visiteurs
|
||||
};
|
||||
|
||||
/// Compteurs de performance
|
||||
static int _memoryHits = 0;
|
||||
static int _memoryMisses = 0;
|
||||
static int _diskHits = 0;
|
||||
static int _diskMisses = 0;
|
||||
|
||||
/// Timer pour le nettoyage automatique
|
||||
static Timer? _cleanupTimer;
|
||||
|
||||
/// Initialise le gestionnaire de cache
|
||||
static Future<void> initialize() async {
|
||||
_prefs = await SharedPreferences.getInstance();
|
||||
|
||||
// Démarrer le nettoyage automatique toutes les 30 minutes
|
||||
_cleanupTimer = Timer.periodic(
|
||||
const Duration(minutes: 30),
|
||||
(_) => _performAutomaticCleanup(),
|
||||
);
|
||||
|
||||
debugPrint('DashboardCacheManager initialisé');
|
||||
}
|
||||
|
||||
/// Dispose le gestionnaire de cache
|
||||
static void dispose() {
|
||||
_cleanupTimer?.cancel();
|
||||
_memoryCache.clear();
|
||||
}
|
||||
|
||||
/// Récupère une donnée du cache avec stratégie multi-niveaux
|
||||
///
|
||||
/// [key] - Clé unique de la donnée
|
||||
/// [userRole] - Rôle de l'utilisateur pour le TTL adaptatif
|
||||
/// [fromDisk] - Autoriser la récupération depuis le disque
|
||||
static Future<T?> get<T>(
|
||||
String key,
|
||||
UserRole userRole, {
|
||||
bool fromDisk = true,
|
||||
}) async {
|
||||
// Niveau 1 : Cache mémoire
|
||||
final memoryData = _getFromMemory<T>(key);
|
||||
if (memoryData != null) {
|
||||
_memoryHits++;
|
||||
return memoryData;
|
||||
}
|
||||
_memoryMisses++;
|
||||
|
||||
// Niveau 2 : Cache disque
|
||||
if (fromDisk && _prefs != null) {
|
||||
final diskData = await _getFromDisk<T>(key, userRole);
|
||||
if (diskData != null) {
|
||||
_diskHits++;
|
||||
// Remettre en cache mémoire pour les prochains accès
|
||||
await _putInMemory(key, diskData, userRole);
|
||||
return diskData;
|
||||
}
|
||||
_diskMisses++;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Stocke une donnée dans le cache avec stratégie multi-niveaux
|
||||
///
|
||||
/// [key] - Clé unique de la donnée
|
||||
/// [data] - Donnée à stocker
|
||||
/// [userRole] - Rôle de l'utilisateur pour le TTL adaptatif
|
||||
/// [toDisk] - Sauvegarder sur disque
|
||||
/// [compress] - Compresser les données volumineuses
|
||||
static Future<void> put<T>(
|
||||
String key,
|
||||
T data,
|
||||
UserRole userRole, {
|
||||
bool toDisk = true,
|
||||
bool compress = false,
|
||||
}) async {
|
||||
// Niveau 1 : Cache mémoire
|
||||
await _putInMemory(key, data, userRole);
|
||||
|
||||
// Niveau 2 : Cache disque
|
||||
if (toDisk && _prefs != null) {
|
||||
await _putOnDisk(key, data, userRole, compress: compress);
|
||||
}
|
||||
}
|
||||
|
||||
/// Invalide une entrée du cache
|
||||
static Future<void> invalidate(String key) async {
|
||||
// Supprimer du cache mémoire
|
||||
_memoryCache.remove(key);
|
||||
|
||||
// Supprimer du cache disque
|
||||
if (_prefs != null) {
|
||||
await _prefs!.remove('cache_$key');
|
||||
await _prefs!.remove('cache_meta_$key');
|
||||
}
|
||||
}
|
||||
|
||||
/// Invalide toutes les entrées d'un préfixe
|
||||
static Future<void> invalidatePrefix(String prefix) async {
|
||||
// Cache mémoire
|
||||
final keysToRemove = _memoryCache.keys
|
||||
.where((key) => key.startsWith(prefix))
|
||||
.toList();
|
||||
|
||||
for (final key in keysToRemove) {
|
||||
_memoryCache.remove(key);
|
||||
}
|
||||
|
||||
// Cache disque
|
||||
if (_prefs != null) {
|
||||
final allKeys = _prefs!.getKeys();
|
||||
final diskKeysToRemove = allKeys
|
||||
.where((key) => key.startsWith('cache_$prefix'))
|
||||
.toList();
|
||||
|
||||
for (final key in diskKeysToRemove) {
|
||||
await _prefs!.remove(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Vide complètement le cache
|
||||
static Future<void> clear() async {
|
||||
_memoryCache.clear();
|
||||
|
||||
if (_prefs != null) {
|
||||
final allKeys = _prefs!.getKeys();
|
||||
final cacheKeys = allKeys.where((key) => key.startsWith('cache_')).toList();
|
||||
|
||||
for (final key in cacheKeys) {
|
||||
await _prefs!.remove(key);
|
||||
}
|
||||
}
|
||||
|
||||
debugPrint('Cache complètement vidé');
|
||||
}
|
||||
|
||||
/// Obtient les statistiques du cache
|
||||
static Map<String, dynamic> getStats() {
|
||||
final totalMemoryRequests = _memoryHits + _memoryMisses;
|
||||
final totalDiskRequests = _diskHits + _diskMisses;
|
||||
|
||||
final memoryHitRate = totalMemoryRequests > 0
|
||||
? (_memoryHits / totalMemoryRequests * 100)
|
||||
: 0.0;
|
||||
|
||||
final diskHitRate = totalDiskRequests > 0
|
||||
? (_diskHits / totalDiskRequests * 100)
|
||||
: 0.0;
|
||||
|
||||
return {
|
||||
'memoryCache': {
|
||||
'hits': _memoryHits,
|
||||
'misses': _memoryMisses,
|
||||
'hitRate': memoryHitRate.toStringAsFixed(2),
|
||||
'size': _memoryCache.length,
|
||||
'maxSize': _maxMemoryCacheSize,
|
||||
},
|
||||
'diskCache': {
|
||||
'hits': _diskHits,
|
||||
'misses': _diskMisses,
|
||||
'hitRate': diskHitRate.toStringAsFixed(2),
|
||||
'maxSizeMB': _maxDiskCacheSizeMB,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/// Effectue un nettoyage manuel du cache
|
||||
static Future<void> cleanup() async {
|
||||
await _performAutomaticCleanup();
|
||||
}
|
||||
|
||||
// === MÉTHODES PRIVÉES ===
|
||||
|
||||
/// Récupère une donnée du cache mémoire
|
||||
static T? _getFromMemory<T>(String key) {
|
||||
final cached = _memoryCache[key];
|
||||
if (cached == null) return null;
|
||||
|
||||
// Vérifier l'expiration
|
||||
if (cached.expiresAt.isBefore(DateTime.now())) {
|
||||
_memoryCache.remove(key);
|
||||
return null;
|
||||
}
|
||||
|
||||
return cached.data as T?;
|
||||
}
|
||||
|
||||
/// Stocke une donnée dans le cache mémoire
|
||||
static Future<void> _putInMemory<T>(String key, T data, UserRole userRole) async {
|
||||
// Vérifier la taille du cache et nettoyer si nécessaire
|
||||
if (_memoryCache.length >= _maxMemoryCacheSize) {
|
||||
await _cleanOldestMemoryEntries();
|
||||
}
|
||||
|
||||
final ttl = _roleTTL[userRole] ?? const Duration(minutes: 5);
|
||||
|
||||
_memoryCache[key] = _CachedData(
|
||||
data: data,
|
||||
expiresAt: DateTime.now().add(ttl),
|
||||
createdAt: DateTime.now(),
|
||||
);
|
||||
}
|
||||
|
||||
/// Récupère une donnée du cache disque
|
||||
static Future<T?> _getFromDisk<T>(String key, UserRole userRole) async {
|
||||
if (_prefs == null) return null;
|
||||
|
||||
// Récupérer les métadonnées
|
||||
final metaJson = _prefs!.getString('cache_meta_$key');
|
||||
if (metaJson == null) return null;
|
||||
|
||||
final meta = jsonDecode(metaJson) as Map<String, dynamic>;
|
||||
final expiresAt = DateTime.parse(meta['expiresAt']);
|
||||
|
||||
// Vérifier l'expiration
|
||||
if (expiresAt.isBefore(DateTime.now())) {
|
||||
await _prefs!.remove('cache_$key');
|
||||
await _prefs!.remove('cache_meta_$key');
|
||||
return null;
|
||||
}
|
||||
|
||||
// Récupérer les données
|
||||
final dataJson = _prefs!.getString('cache_$key');
|
||||
if (dataJson == null) return null;
|
||||
|
||||
try {
|
||||
final data = jsonDecode(dataJson);
|
||||
return data as T;
|
||||
} catch (e) {
|
||||
debugPrint('Erreur de désérialisation du cache: $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// Stocke une donnée sur le cache disque
|
||||
static Future<void> _putOnDisk<T>(
|
||||
String key,
|
||||
T data,
|
||||
UserRole userRole, {
|
||||
bool compress = false,
|
||||
}) async {
|
||||
if (_prefs == null) return;
|
||||
|
||||
try {
|
||||
final ttl = _roleTTL[userRole] ?? const Duration(minutes: 5);
|
||||
final expiresAt = DateTime.now().add(ttl);
|
||||
|
||||
// Sérialiser les données
|
||||
final dataJson = jsonEncode(data);
|
||||
|
||||
// Métadonnées
|
||||
final meta = {
|
||||
'expiresAt': expiresAt.toIso8601String(),
|
||||
'createdAt': DateTime.now().toIso8601String(),
|
||||
'userRole': userRole.name,
|
||||
'compressed': compress,
|
||||
};
|
||||
|
||||
// Sauvegarder
|
||||
await _prefs!.setString('cache_$key', dataJson);
|
||||
await _prefs!.setString('cache_meta_$key', jsonEncode(meta));
|
||||
|
||||
} catch (e) {
|
||||
debugPrint('Erreur de sérialisation du cache: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// Nettoie les entrées les plus anciennes du cache mémoire
|
||||
static Future<void> _cleanOldestMemoryEntries() async {
|
||||
if (_memoryCache.isEmpty) return;
|
||||
|
||||
// Trier par date de création et supprimer les 10% les plus anciennes
|
||||
final entries = _memoryCache.entries.toList();
|
||||
entries.sort((a, b) => a.value.createdAt.compareTo(b.value.createdAt));
|
||||
|
||||
final toRemove = (entries.length * 0.1).ceil();
|
||||
for (int i = 0; i < toRemove && i < entries.length; i++) {
|
||||
_memoryCache.remove(entries[i].key);
|
||||
}
|
||||
}
|
||||
|
||||
/// Effectue un nettoyage automatique
|
||||
static Future<void> _performAutomaticCleanup() async {
|
||||
final now = DateTime.now();
|
||||
|
||||
// Nettoyer le cache mémoire expiré
|
||||
_memoryCache.removeWhere((key, cached) => cached.expiresAt.isBefore(now));
|
||||
|
||||
// Nettoyer le cache disque expiré
|
||||
if (_prefs != null) {
|
||||
final allKeys = _prefs!.getKeys();
|
||||
final metaKeys = allKeys.where((key) => key.startsWith('cache_meta_')).toList();
|
||||
|
||||
for (final metaKey in metaKeys) {
|
||||
final metaJson = _prefs!.getString(metaKey);
|
||||
if (metaJson != null) {
|
||||
try {
|
||||
final meta = jsonDecode(metaJson) as Map<String, dynamic>;
|
||||
final expiresAt = DateTime.parse(meta['expiresAt']);
|
||||
|
||||
if (expiresAt.isBefore(now)) {
|
||||
final dataKey = metaKey.replaceFirst('cache_meta_', 'cache_');
|
||||
await _prefs!.remove(dataKey);
|
||||
await _prefs!.remove(metaKey);
|
||||
}
|
||||
} catch (e) {
|
||||
// Supprimer les métadonnées corrompues
|
||||
await _prefs!.remove(metaKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
debugPrint('Nettoyage automatique du cache effectué');
|
||||
}
|
||||
|
||||
/// Invalide le cache pour un rôle spécifique
|
||||
static Future<void> invalidateForRole(UserRole role) async {
|
||||
debugPrint('🗑️ Invalidation du cache pour le rôle: ${role.displayName}');
|
||||
|
||||
// Invalider le cache mémoire pour ce rôle
|
||||
final keysToRemove = <String>[];
|
||||
for (final key in _memoryCache.keys) {
|
||||
if (key.contains(role.name)) {
|
||||
keysToRemove.add(key);
|
||||
}
|
||||
}
|
||||
|
||||
for (final key in keysToRemove) {
|
||||
_memoryCache.remove(key);
|
||||
}
|
||||
|
||||
// Invalider le cache disque pour ce rôle
|
||||
_prefs ??= await SharedPreferences.getInstance();
|
||||
if (_prefs != null) {
|
||||
final keys = _prefs!.getKeys();
|
||||
final diskKeysToRemove = <String>[];
|
||||
|
||||
for (final key in keys) {
|
||||
if (key.startsWith('cache_') && key.contains(role.name)) {
|
||||
diskKeysToRemove.add(key);
|
||||
}
|
||||
}
|
||||
|
||||
for (final key in diskKeysToRemove) {
|
||||
await _prefs!.remove(key);
|
||||
// Supprimer aussi les métadonnées associées
|
||||
final metaKey = key.replaceFirst('cache_', 'cache_meta_');
|
||||
await _prefs!.remove(metaKey);
|
||||
}
|
||||
}
|
||||
|
||||
debugPrint('✅ Cache invalidé pour le rôle: ${role.displayName}');
|
||||
}
|
||||
}
|
||||
|
||||
/// Classe pour les données mises en cache
|
||||
class _CachedData {
|
||||
final dynamic data;
|
||||
final DateTime expiresAt;
|
||||
final DateTime createdAt;
|
||||
|
||||
_CachedData({
|
||||
required this.data,
|
||||
required this.expiresAt,
|
||||
required this.createdAt,
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user