wesal/frontend/lib/services/cache_service.dart

303 lines
9.7 KiB
Dart

import 'dart:async';
import 'dart:convert';
import 'package:crypto/crypto.dart';
import '../models/post_models.dart';
import '../models/invitation_models.dart';
class CachedData<T> {
final T data;
final DateTime timestamp;
final String jsonHash;
CachedData({
required this.data,
required this.timestamp,
required this.jsonHash,
});
bool isExpired(Duration maxAge) {
return DateTime.now().difference(timestamp) > maxAge;
}
}
class CacheService {
static final Map<String, CachedData> _cache = {};
static final Map<String, Timer> _pollingTimers = {};
static final Map<String, StreamController> _streamControllers = {};
// Cache durations
static const Duration _postsCacheDuration = Duration(seconds: 30);
static const Duration _invitationsCacheDuration = Duration(seconds: 30);
static const Duration _userCacheDuration = Duration(hours: 3);
// Polling intervals
static const Duration _postsPollingInterval = Duration(seconds: 5);
static const Duration _invitationsPollingInterval = Duration(seconds: 5);
static const Duration _userPollingInterval = Duration(hours: 3);
/// Get cached data if available and not expired, otherwise return null
static T? getCached<T>(String key) {
final cached = _cache[key];
if (cached != null) {
return cached.data as T?;
}
return null;
}
/// Get cached data regardless of expiration (for showing stale data while loading)
static T? getCachedStale<T>(String key) {
final cached = _cache[key];
if (cached != null) {
return cached.data as T?;
}
return null;
}
/// Check if cache is expired
static bool isCacheExpired(String key, Duration maxAge) {
final cached = _cache[key];
if (cached == null) return true;
return cached.isExpired(maxAge);
}
/// Set cached data with JSON hash for change detection
static void setCached<T>(String key, T data) {
final jsonHash = _generateDataHash(data);
_cache[key] = CachedData(
data: data,
timestamp: DateTime.now(),
jsonHash: jsonHash,
);
}
/// Check if data has changed by comparing hashes
static bool hasDataChanged(String key, dynamic newData) {
final cached = _cache[key];
if (cached == null) return true;
final newJsonHash = _generateDataHash(newData);
final hasChanged = cached.jsonHash != newJsonHash;
if (hasChanged) {
print(
'Data changed for $key: old hash ${cached.jsonHash}, new hash $newJsonHash',
);
} else {
print('No data changes for $key: hash remains ${cached.jsonHash}');
}
return hasChanged;
}
/// Generate a reliable hash for data comparison
static String _generateDataHash(dynamic data) {
try {
String jsonString;
if (data is List) {
// Handle lists of objects (like List<Post>)
final jsonList = data.map((item) {
if (item is Map) {
return item;
} else if (item.runtimeType.toString().contains('Post')) {
// Call toJson() on Post objects
return (item as dynamic).toJson();
} else if (item.runtimeType.toString().contains('Invitation')) {
// Call toJson() on Invitation objects
return (item as dynamic).toJson();
} else {
return item.toString();
}
}).toList();
jsonString = jsonEncode(jsonList);
} else if (data is Map) {
jsonString = jsonEncode(data);
} else if (data.runtimeType.toString().contains('Post') ||
data.runtimeType.toString().contains('Invitation')) {
// Call toJson() on model objects
jsonString = jsonEncode((data as dynamic).toJson());
} else {
jsonString = data.toString();
}
// Generate MD5 hash for comparison
final bytes = utf8.encode(jsonString);
final digest = md5.convert(bytes);
final hashResult = digest.toString();
// Debug: Show first 100 chars of JSON for debugging
final debugJson = jsonString.length > 100
? jsonString.substring(0, 100) + '...'
: jsonString;
print('Generated hash $hashResult for JSON: $debugJson');
return hashResult;
} catch (e) {
print('Error generating data hash: $e');
// Fallback to simple string hash
return data.toString().hashCode.toString();
}
}
/// Get stream for cache updates
static Stream<T> getStream<T>(String key) {
if (!_streamControllers.containsKey(key)) {
_streamControllers[key] = StreamController<T>.broadcast();
}
return (_streamControllers[key] as StreamController<T>).stream;
}
/// Notify stream listeners of cache updates (safely) - Force UI refresh
static void notifyStreamListeners<T>(String key, T data) {
try {
// Ensure we have a stream controller
if (!_streamControllers.containsKey(key)) {
print('Creating new stream controller for $key');
_streamControllers[key] = StreamController<T>.broadcast();
}
final controller = _streamControllers[key] as StreamController<T>;
if (!controller.isClosed) {
print('Notifying stream listeners for $key with ${data.runtimeType}');
controller.add(data);
} else {
print('Stream controller for $key is closed, creating new one');
_streamControllers[key] = StreamController<T>.broadcast();
(_streamControllers[key] as StreamController<T>).add(data);
}
} catch (e) {
print('Error notifying stream listeners for $key: $e');
// Recreate controller on error
try {
_streamControllers[key]?.close();
_streamControllers[key] = StreamController<T>.broadcast();
(_streamControllers[key] as StreamController<T>).add(data);
} catch (recreateError) {
print('Failed to recreate stream controller for $key: $recreateError');
}
}
}
/// Start polling for a specific endpoint (silent - no UI notifications)
static void startPolling(
String key,
Future<Map<String, dynamic>> Function() fetchFunction,
Duration interval,
Duration cacheMaxAge,
) {
// Cancel existing timer if any
stopPolling(key);
_pollingTimers[key] = Timer.periodic(interval, (timer) async {
try {
print('Polling $key...');
final result = await fetchFunction();
if (result['success'] == true) {
// Use the parsed data from the result (e.g., result['posts'] for posts)
final data = result['posts'] ?? result['invitations'] ?? result['data'];
print('📊 Polling $key got data type: ${data.runtimeType}, length: ${data is List ? data.length : 'N/A'}');
// Check if data has changed BEFORE updating cache
final dataChanged = hasDataChanged(key, data);
if (dataChanged) {
print(
'🔄 Data changed for $key, updating cache and forcing UI refresh',
);
setCached(key, data);
// Force refresh by notifying stream listeners with appropriate types
if (key == postsAllKey && data is List<Post>) {
print('📱 Notifying posts stream with ${data.length} posts');
notifyStreamListeners<List<Post>>(key, data);
} else if (key == invitationsAllKey && data is InvitationsData) {
print('📱 Notifying invitations stream');
notifyStreamListeners<InvitationsData>(key, data);
} else {
print('📱 Notifying generic stream for $key with ${data.runtimeType}');
notifyStreamListeners<dynamic>(key, data);
}
} else {
print('No data changes for $key - not notifying streams');
}
} else {
print('Polling failed for $key: ${result['message']}');
}
} catch (e) {
print('Polling error for $key: $e');
}
});
}
/// Stop polling for a specific endpoint
static void stopPolling(String key) {
_pollingTimers[key]?.cancel();
_pollingTimers.remove(key);
}
/// Stop all polling
static void stopAllPolling() {
for (final timer in _pollingTimers.values) {
timer.cancel();
}
_pollingTimers.clear();
}
/// Clear all caches
static void clearAll() {
_cache.clear();
stopAllPolling();
_cleanupStreamControllers();
}
/// Safely cleanup stream controllers
static void _cleanupStreamControllers() {
for (final controller in _streamControllers.values) {
if (!controller.isClosed) {
controller.close();
}
}
_streamControllers.clear();
}
/// Clean up specific stream controller
static void cleanupStream(String key) {
if (_streamControllers.containsKey(key)) {
if (!_streamControllers[key]!.isClosed) {
_streamControllers[key]!.close();
}
_streamControllers.remove(key);
}
}
/// Clear specific cache
static void clearCache(String key) {
_cache.remove(key);
}
/// Get cache info for debugging
static Map<String, dynamic> getCacheInfo() {
return {
'cacheSize': _cache.length,
'activePollers': _pollingTimers.length,
'activeStreams': _streamControllers.length,
'cacheKeys': _cache.keys.toList(),
};
}
// Predefined cache keys
static const String postsAllKey = 'posts_all';
static const String invitationsAllKey = 'invitations_all';
static const String userProfileKey = 'user_profile';
// Helper methods for specific cache durations
static Duration get postsCacheDuration => _postsCacheDuration;
static Duration get invitationsCacheDuration => _invitationsCacheDuration;
static Duration get userCacheDuration => _userCacheDuration;
static Duration get postsPollingInterval => _postsPollingInterval;
static Duration get invitationsPollingInterval => _invitationsPollingInterval;
static Duration get userPollingInterval => _userPollingInterval;
}