311 lines
10 KiB
Dart
311 lines
10 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: 6);
|
|
static const Duration _userPostsCacheDuration = Duration(seconds: 30);
|
|
|
|
// Polling intervals
|
|
static const Duration _postsPollingInterval = Duration(seconds: 5);
|
|
static const Duration _invitationsPollingInterval = Duration(seconds: 5);
|
|
static const Duration _userPollingInterval = Duration(hours: 3);
|
|
static const Duration _userPostsPollingInterval = Duration(seconds: 5);
|
|
|
|
/// 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 == userPostsKey && data is List<Post>) {
|
|
print('📱 Notifying user 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';
|
|
static const String userPostsKey = 'user_posts';
|
|
|
|
// Helper methods for specific cache durations
|
|
static Duration get postsCacheDuration => _postsCacheDuration;
|
|
static Duration get invitationsCacheDuration => _invitationsCacheDuration;
|
|
static Duration get userCacheDuration => _userCacheDuration;
|
|
static Duration get userPostsCacheDuration => _userPostsCacheDuration;
|
|
|
|
static Duration get postsPollingInterval => _postsPollingInterval;
|
|
static Duration get invitationsPollingInterval => _invitationsPollingInterval;
|
|
static Duration get userPollingInterval => _userPollingInterval;
|
|
static Duration get userPostsPollingInterval => _userPostsPollingInterval;
|
|
} |