import 'dart:async'; import 'dart:convert'; import 'package:shared_preferences/shared_preferences.dart'; import 'cache_scope.dart'; class CacheEntry { const CacheEntry({required this.value, required this.fetchedAt}); final T value; final DateTime fetchedAt; } abstract class CacheStore { Future read(String key); Future write(String key, T value); Future remove(String key); } class MemoryCacheStore implements CacheStore { final Map _values = {}; @override Future read(String key) async { final value = _values[key]; if (value is T) { return value; } return null; } @override Future write(String key, T value) async { _values[key] = value; } @override Future remove(String key) async { _values.remove(key); } Future removeByPrefix(String prefix) async { _values.removeWhere((key, _) => key.startsWith(prefix)); } } class SharedPrefsCacheStore implements CacheStore { SharedPrefsCacheStore({required SharedPreferences prefs}) : _prefs = prefs; final SharedPreferences _prefs; @override Future read(String key) async { final raw = _prefs.getString(key); if (raw == null) { return null; } try { return CacheCodec.decode(raw); } catch (_) { await _prefs.remove(key); return null; } } @override Future write(String key, T value) async { final encoded = CacheCodec.encode(value); await _prefs.setString(key, encoded); } @override Future remove(String key) { return _prefs.remove(key); } } class PersistentCacheStore implements CacheStore { SharedPreferences? _prefs; Future? _prefsFuture; final Map _fallbackValues = {}; PersistentCacheStore({SharedPreferences? prefs}) : _prefs = prefs; Future _getPrefs() { if (_prefs != null) { return Future.value(_prefs); } final inFlight = _prefsFuture; if (inFlight != null) { return inFlight.then((prefs) => prefs); } final created = SharedPreferences.getInstance(); _prefsFuture = created; return created .then((prefs) { _prefs = prefs; return prefs; }) .catchError((_) { _prefsFuture = null; return null; }); } @override Future read(String key) async { final prefs = await _getPrefs(); if (prefs == null) { final value = _fallbackValues[key]; if (value is T) { return value; } return null; } final store = SharedPrefsCacheStore(prefs: prefs); return store.read(key); } @override Future write(String key, T value) async { final prefs = await _getPrefs(); if (prefs == null) { _fallbackValues[key] = value; return; } final store = SharedPrefsCacheStore(prefs: prefs); await store.write(key, value); } @override Future remove(String key) async { final prefs = await _getPrefs(); if (prefs == null) { _fallbackValues.remove(key); return; } final store = SharedPrefsCacheStore(prefs: prefs); await store.remove(key); } Future removeByPrefix(String prefix) async { final prefs = await _getPrefs(); if (prefs == null) { _fallbackValues.removeWhere((key, _) => key.startsWith(prefix)); return; } final targets = prefs.getKeys().where((key) => key.startsWith(prefix)); for (final key in targets) { await prefs.remove(key); } } } class HybridCacheStore { final CacheStore memory; final CacheStore persistent; final Map> _inflight = >{}; HybridCacheStore({required this.memory, required this.persistent}); Future read(String key) async { final memoryValue = await memory.read(key); if (memoryValue != null) { return memoryValue; } final persistentValue = await persistent.read(key); if (persistentValue != null) { await memory.write(key, persistentValue); } return persistentValue; } Future write(String key, T value) async { await memory.write(key, value); await persistent.write(key, value); } Future remove(String key) async { await memory.remove(key); await persistent.remove(key); } Future clearByPrefix(String prefix) async { if (memory is MemoryCacheStore) { await (memory as MemoryCacheStore).removeByPrefix(prefix); } if (persistent is PersistentCacheStore) { await (persistent as PersistentCacheStore).removeByPrefix(prefix); } } Future getOrLoad(String key, {required Future Function() loader}) { final running = _inflight[key]; if (running != null) { return running.then((value) => value as T); } final future = () async { final cached = await read(key); if (cached != null) { return cached; } final loaded = await loader(); await write(key, loaded); return loaded; }(); _inflight[key] = future; return future.whenComplete(() { _inflight.remove(key); }); } } class CacheInvalidator { final HybridCacheStore? _store; CacheInvalidator({HybridCacheStore? store}) : _store = store; void invalidate(String key) { final store = _store; if (store != null) { unawaited(store.remove(CacheScope.scopedKey(key))); } } void invalidateCalendarDay(DateTime date) { final month = '${date.year}-${date.month.toString().padLeft(2, '0')}'; final day = '$month-${date.day.toString().padLeft(2, '0')}'; invalidate('calendar:day:$day'); invalidate('calendar:month:$month'); } } class CacheCodec { static String encode(T value) { final payload = {'type': '$T'}; if (value is CacheEntry) { payload['entryType'] = '${value.value.runtimeType}'; payload['fetchedAt'] = value.fetchedAt.toIso8601String(); payload['value'] = _encodeValue(value.value); } else { payload['value'] = _encodeValue(value); } return jsonEncode(payload); } static T decode(String raw) { final decoded = jsonDecode(raw); if (decoded is! Map) { throw const FormatException('Invalid cache payload'); } final value = decoded['value']; if (T.toString().startsWith('CacheEntry<')) { final fetchedAtRaw = decoded['fetchedAt']; final fetchedAt = DateTime.parse(fetchedAtRaw as String); final entryType = (decoded['entryType'] as String?) ?? 'Object?'; final decodedValue = _decodeValue(entryType, value); switch (entryType) { case 'String': return CacheEntry( value: decodedValue as String, fetchedAt: fetchedAt, ) as T; case 'int': return CacheEntry( value: decodedValue as int, fetchedAt: fetchedAt, ) as T; case 'double': return CacheEntry( value: decodedValue as double, fetchedAt: fetchedAt, ) as T; case 'bool': return CacheEntry( value: decodedValue as bool, fetchedAt: fetchedAt, ) as T; default: return CacheEntry(value: decodedValue, fetchedAt: fetchedAt) as T; } } final type = (decoded['type'] as String?) ?? '$T'; return _decodeValue(type, value) as T; } static Object? _encodeValue(Object? value) { if (value == null || value is String || value is num || value is bool) { return value; } if (value is DateTime) { return value.toIso8601String(); } if (value is List) { return value.map(_encodeValue).toList(); } if (value is Map) { return value.map((k, v) => MapEntry(k.toString(), _encodeValue(v))); } throw StateError('Unsupported cached value type: ${value.runtimeType}'); } static Object? _decodeValue(String type, Object? raw) { switch (type) { case 'String': return raw as String? ?? ''; case 'int': if (raw is int) { return raw; } if (raw is num) { return raw.toInt(); } throw const FormatException('Invalid int cache payload'); case 'double': if (raw is double) { return raw; } if (raw is num) { return raw.toDouble(); } throw const FormatException('Invalid double cache payload'); case 'bool': return raw as bool? ?? false; case 'DateTime': return DateTime.parse(raw as String); default: break; } final listType = _extractListType(type); if (listType != null) { if (raw is! List) { throw FormatException('Invalid list cache payload for type $type'); } return raw .map((item) => _decodeValue(listType, item)) .toList(growable: false); } if (raw is Map) { return Map.from(raw); } return raw; } static String? _extractListType(String type) { final listMatch = RegExp(r'^List<(.+)>$').firstMatch(type); if (listMatch == null) { return null; } return listMatch.group(1); } }