import 'dart:async'; import 'cache_policy.dart'; import 'cache_store.dart'; abstract class CachedRepository { final HybridCacheStore store; final CachePolicy policy; final DateTime Function() now; final Object? Function(T value) encodeValue; final T Function(Object? raw) decodeValue; final Map> _refreshInFlight = >{}; CachedRepository({ required this.store, required this.policy, DateTime Function()? now, Object? Function(T value)? encodeValue, T Function(Object? raw)? decodeValue, }) : now = now ?? DateTime.now, encodeValue = encodeValue ?? _defaultEncode, decodeValue = decodeValue ?? _defaultDecode; static Object? _defaultEncode(T value) => value; static T _defaultDecode(Object? raw) => raw as T; Future getOrLoad({ required String key, required Future Function() loadFromRemote, bool Function(T loaded)? shouldWriteLoaded, bool forceRefresh = false, }) async { if (forceRefresh) { return _refreshAndWrite( key, loadFromRemote, shouldWriteLoaded: shouldWriteLoaded, ); } final cached = await readCacheEntry(key); if (cached == null) { return _refreshAndWrite( key, loadFromRemote, shouldWriteLoaded: shouldWriteLoaded, ); } final decision = policy.evaluate(now: now(), fetchedAt: cached.fetchedAt); if (decision.shouldRefreshInBackground) { refreshInBackground( key: key, loadFromRemote: loadFromRemote, shouldWriteLoaded: shouldWriteLoaded, ); } if (decision.mustBlockForNetwork || !decision.canUseCached) { return _refreshAndWrite( key, loadFromRemote, shouldWriteLoaded: shouldWriteLoaded, ); } return cached.value; } Future?> readCacheEntry(String key) { return _readDecodedEntry(key); } Future writeCacheEntry(String key, T value) { return store.write>( key, CacheEntry(value: encodeValue(value), fetchedAt: now()), ); } Future?> _readDecodedEntry(String key) async { final entry = await store.read>(key); if (entry == null) { return null; } try { return CacheEntry( value: decodeValue(entry.value), fetchedAt: entry.fetchedAt, ); } catch (_) { await store.remove(key); return null; } } Future removeCacheKey(String key) { return store.remove(key); } void refreshInBackground({ required String key, required Future Function() loadFromRemote, bool Function(T loaded)? shouldWriteLoaded, }) { if (_refreshInFlight.containsKey(key)) { return; } final task = _refreshAndWrite( key, loadFromRemote, shouldWriteLoaded: shouldWriteLoaded, ).then((_) {}); final tracked = task.whenComplete(() { _refreshInFlight.remove(key); }); _refreshInFlight[key] = tracked; unawaited(tracked); } Future _refreshAndWrite( String key, Future Function() loadFromRemote, { bool Function(T loaded)? shouldWriteLoaded, }) async { final remote = await loadFromRemote(); if (shouldWriteLoaded != null && !shouldWriteLoaded(remote)) { return remote; } await writeCacheEntry(key, remote); return remote; } }