Files
social-app/apps/lib/data/cache/cache_store.dart
T

370 lines
9.4 KiB
Dart

import 'dart:async';
import 'dart:convert';
import 'package:shared_preferences/shared_preferences.dart';
import 'cache_scope.dart';
class CacheEntry<T> {
const CacheEntry({required this.value, required this.fetchedAt});
final T value;
final DateTime fetchedAt;
}
abstract class CacheStore {
Future<T?> read<T>(String key);
Future<void> write<T>(String key, T value);
Future<void> remove(String key);
}
class MemoryCacheStore implements CacheStore {
final Map<String, Object?> _values = <String, Object?>{};
@override
Future<T?> read<T>(String key) async {
final value = _values[key];
if (value is T) {
return value;
}
return null;
}
@override
Future<void> write<T>(String key, T value) async {
_values[key] = value;
}
@override
Future<void> remove(String key) async {
_values.remove(key);
}
Future<void> 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<T?> read<T>(String key) async {
final raw = _prefs.getString(key);
if (raw == null) {
return null;
}
try {
return CacheCodec.decode<T>(raw);
} catch (_) {
await _prefs.remove(key);
return null;
}
}
@override
Future<void> write<T>(String key, T value) async {
final encoded = CacheCodec.encode<T>(value);
await _prefs.setString(key, encoded);
}
@override
Future<void> remove(String key) {
return _prefs.remove(key);
}
}
class PersistentCacheStore implements CacheStore {
SharedPreferences? _prefs;
Future<SharedPreferences>? _prefsFuture;
final Map<String, Object?> _fallbackValues = <String, Object?>{};
PersistentCacheStore({SharedPreferences? prefs}) : _prefs = prefs;
Future<SharedPreferences?> _getPrefs() {
if (_prefs != null) {
return Future<SharedPreferences?>.value(_prefs);
}
final inFlight = _prefsFuture;
if (inFlight != null) {
return inFlight.then<SharedPreferences?>((prefs) => prefs);
}
final created = SharedPreferences.getInstance();
_prefsFuture = created;
return created
.then<SharedPreferences?>((prefs) {
_prefs = prefs;
return prefs;
})
.catchError((_) {
_prefsFuture = null;
return null;
});
}
@override
Future<T?> read<T>(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<T>(key);
}
@override
Future<void> write<T>(String key, T value) async {
final prefs = await _getPrefs();
if (prefs == null) {
_fallbackValues[key] = value;
return;
}
final store = SharedPrefsCacheStore(prefs: prefs);
await store.write<T>(key, value);
}
@override
Future<void> 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<void> 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<String, Future<dynamic>> _inflight = <String, Future<dynamic>>{};
HybridCacheStore({required this.memory, required this.persistent});
Future<T?> read<T>(String key) async {
final memoryValue = await memory.read<T>(key);
if (memoryValue != null) {
return memoryValue;
}
final persistentValue = await persistent.read<T>(key);
if (persistentValue != null) {
await memory.write(key, persistentValue);
}
return persistentValue;
}
Future<void> write<T>(String key, T value) async {
await memory.write<T>(key, value);
await persistent.write<T>(key, value);
}
Future<void> remove(String key) async {
await memory.remove(key);
await persistent.remove(key);
}
Future<void> 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<T> getOrLoad<T>(String key, {required Future<T> Function() loader}) {
final running = _inflight[key];
if (running != null) {
return running.then((value) => value as T);
}
final future = () async {
final cached = await read<T>(key);
if (cached != null) {
return cached;
}
final loaded = await loader();
await write<T>(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>(T value) {
final payload = <String, Object?>{'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<T>(String raw) {
final decoded = jsonDecode(raw);
if (decoded is! Map<String, dynamic>) {
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<String>(
value: decodedValue as String,
fetchedAt: fetchedAt,
)
as T;
case 'int':
return CacheEntry<int>(
value: decodedValue as int,
fetchedAt: fetchedAt,
)
as T;
case 'double':
return CacheEntry<double>(
value: decodedValue as double,
fetchedAt: fetchedAt,
)
as T;
case 'bool':
return CacheEntry<bool>(
value: decodedValue as bool,
fetchedAt: fetchedAt,
)
as T;
default:
return CacheEntry<Object?>(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<String, dynamic>.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);
}
}