- NotImplementedFailure: ajout userFriendlyMessage et icon construction (blue) - ErrorDisplayWidget: support spécial pour NotImplementedFailure (bientôt disponible) - SnackbarHelper: classe centralisée pour messages cohérents (success, error, warning, info, notImplemented) - budgets_list_page: remplace generic snackbar par SnackbarHelper.showNotImplemented - conversations_page: remplace 2 TODOs par SnackbarHelper.showNotImplemented - export_members: met à jour TODO obsolète (endpoint PDF maintenant disponible) - cache_service: fix AppLogger.error calls (error: named param) - cached_datasource_decorator: fix AppLogger.error call Task #64 - Fix Snackbar Placeholders + NotImplementedFailure UX
206 lines
5.5 KiB
Dart
206 lines
5.5 KiB
Dart
import 'dart:convert';
|
|
import 'package:shared_preferences/shared_preferences.dart';
|
|
import '../utils/logger.dart';
|
|
|
|
/// Service de cache stratégique avec TTL (Time To Live)
|
|
/// pour optimiser les performances et réduire les appels API
|
|
class CacheService {
|
|
final SharedPreferences _prefs;
|
|
|
|
CacheService(this._prefs);
|
|
|
|
/// Clés de cache avec leur TTL en secondes
|
|
static const Map<String, int> _cacheTTL = {
|
|
'dashboard_stats': 300, // 5 minutes
|
|
'parametres_lcb_ft': 1800, // 30 minutes
|
|
'user_profile': 600, // 10 minutes
|
|
'organisations': 3600, // 1 heure
|
|
'notifications_count': 60, // 1 minute
|
|
};
|
|
|
|
/// Met en cache une valeur avec un TTL automatique selon la clé
|
|
Future<bool> set(String key, dynamic value) async {
|
|
try {
|
|
final cacheData = {
|
|
'value': value,
|
|
'timestamp': DateTime.now().millisecondsSinceEpoch,
|
|
};
|
|
|
|
final jsonString = json.encode(cacheData);
|
|
final success = await _prefs.setString(key, jsonString);
|
|
|
|
if (success) {
|
|
AppLogger.debug('Cache set: $key (TTL: ${_getTTL(key)}s)');
|
|
}
|
|
|
|
return success;
|
|
} catch (e) {
|
|
AppLogger.error('Erreur lors de la mise en cache de $key', error: e);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// Récupère une valeur depuis le cache si elle n'est pas expirée
|
|
/// Retourne null si la clé n'existe pas ou si le cache est expiré
|
|
T? get<T>(String key) {
|
|
try {
|
|
final jsonString = _prefs.getString(key);
|
|
if (jsonString == null) {
|
|
return null;
|
|
}
|
|
|
|
final cacheData = json.decode(jsonString) as Map<String, dynamic>;
|
|
final timestamp = cacheData['timestamp'] as int;
|
|
final now = DateTime.now().millisecondsSinceEpoch;
|
|
|
|
// Vérifier si le cache a expiré
|
|
final ttl = _getTTL(key) * 1000; // Convertir en millisecondes
|
|
if (now - timestamp > ttl) {
|
|
AppLogger.debug('Cache expiré: $key');
|
|
remove(key); // Nettoyer
|
|
return null;
|
|
}
|
|
|
|
final value = cacheData['value'];
|
|
AppLogger.debug('Cache hit: $key');
|
|
return value as T;
|
|
} catch (e) {
|
|
AppLogger.error('Erreur lors de la lecture du cache $key', error: e);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/// Récupère une valeur String depuis le cache
|
|
String? getString(String key) => get<String>(key);
|
|
|
|
/// Récupère une valeur Map depuis le cache
|
|
Map<String, dynamic>? getMap(String key) {
|
|
final value = get<Map<String, dynamic>>(key);
|
|
if (value == null) return null;
|
|
return Map<String, dynamic>.from(value);
|
|
}
|
|
|
|
/// Récupère une valeur List depuis le cache
|
|
List<dynamic>? getList(String key) {
|
|
final value = get<List<dynamic>>(key);
|
|
if (value == null) return null;
|
|
return List<dynamic>.from(value);
|
|
}
|
|
|
|
/// Supprime une clé du cache
|
|
Future<bool> remove(String key) async {
|
|
try {
|
|
return await _prefs.remove(key);
|
|
} catch (e) {
|
|
AppLogger.error('Erreur lors de la suppression du cache $key', error: e);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// Nettoie toutes les clés d'un préfixe donné
|
|
Future<void> clearByPrefix(String prefix) async {
|
|
try {
|
|
final keys = _prefs.getKeys();
|
|
final keysToRemove = keys.where((k) => k.startsWith(prefix));
|
|
|
|
for (final key in keysToRemove) {
|
|
await remove(key);
|
|
}
|
|
|
|
AppLogger.info('Cache nettoyé pour préfixe: $prefix');
|
|
} catch (e) {
|
|
AppLogger.error('Erreur lors du nettoyage du cache $prefix', error: e);
|
|
}
|
|
}
|
|
|
|
/// Nettoie tout le cache
|
|
Future<bool> clearAll() async {
|
|
try {
|
|
return await _prefs.clear();
|
|
} catch (e) {
|
|
AppLogger.error('Erreur lors du nettoyage complet du cache', error: e);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// Nettoie les caches expirés (maintenance)
|
|
Future<void> cleanupExpired() async {
|
|
try {
|
|
final keys = _prefs.getKeys();
|
|
int cleaned = 0;
|
|
|
|
for (final key in keys) {
|
|
final jsonString = _prefs.getString(key);
|
|
if (jsonString == null) continue;
|
|
|
|
try {
|
|
final cacheData = json.decode(jsonString) as Map<String, dynamic>;
|
|
final timestamp = cacheData['timestamp'] as int;
|
|
final now = DateTime.now().millisecondsSinceEpoch;
|
|
|
|
final ttl = _getTTL(key) * 1000;
|
|
if (now - timestamp > ttl) {
|
|
await remove(key);
|
|
cleaned++;
|
|
}
|
|
} catch (_) {
|
|
// Données corrompues, supprimer
|
|
await remove(key);
|
|
cleaned++;
|
|
}
|
|
}
|
|
|
|
if (cleaned > 0) {
|
|
AppLogger.info('$cleaned entrées de cache expirées nettoyées');
|
|
}
|
|
} catch (e) {
|
|
AppLogger.error('Erreur lors du nettoyage des caches expirés', error: e);
|
|
}
|
|
}
|
|
|
|
/// Récupère le TTL d'une clé en secondes
|
|
int _getTTL(String key) {
|
|
// Chercher une correspondance exacte
|
|
if (_cacheTTL.containsKey(key)) {
|
|
return _cacheTTL[key]!;
|
|
}
|
|
|
|
// Chercher par préfixe
|
|
for (final entry in _cacheTTL.entries) {
|
|
if (key.startsWith(entry.key)) {
|
|
return entry.value;
|
|
}
|
|
}
|
|
|
|
// TTL par défaut : 5 minutes
|
|
return 300;
|
|
}
|
|
|
|
/// Vérifie si une clé existe et n'est pas expirée
|
|
bool has(String key) {
|
|
return get(key) != null;
|
|
}
|
|
|
|
/// Retourne des statistiques sur le cache
|
|
Map<String, dynamic> getStats() {
|
|
final keys = _prefs.getKeys();
|
|
int total = keys.length;
|
|
int expired = 0;
|
|
int valid = 0;
|
|
|
|
for (final key in keys) {
|
|
if (get(key) == null) {
|
|
expired++;
|
|
} else {
|
|
valid++;
|
|
}
|
|
}
|
|
|
|
return {
|
|
'total': total,
|
|
'valid': valid,
|
|
'expired': expired,
|
|
};
|
|
}
|
|
}
|