import 'dart:async'; import 'cache_scope.dart'; 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 { final scopeToken = CacheScope.maybeToken(); if (scopeToken == null) { return loadFromRemote(); } final scopedKey = CacheScope.scopedKey(key, scopeToken: scopeToken); if (forceRefresh) { return _refreshAndWrite( scopedKey, loadFromRemote, shouldWriteLoaded: shouldWriteLoaded, ); } final cached = await _readDecodedEntry(scopedKey); if (cached == null) { return _refreshAndWrite( scopedKey, loadFromRemote, shouldWriteLoaded: shouldWriteLoaded, ); } final decision = policy.evaluate(now: now(), fetchedAt: cached.fetchedAt); if (decision.shouldRefreshInBackground) { refreshInBackground( key: scopedKey, loadFromRemote: loadFromRemote, shouldWriteLoaded: shouldWriteLoaded, ); } if (decision.mustBlockForNetwork || !decision.canUseCached) { return _refreshAndWrite( scopedKey, loadFromRemote, shouldWriteLoaded: shouldWriteLoaded, ); } return cached.value; } Future?> readCacheEntry(String key, {String? scopeToken}) { final resolvedScope = _resolveScopeToken(scopeToken); if (resolvedScope == null) { return Future?>.value(null); } return _readDecodedEntry( CacheScope.scopedKey(key, scopeToken: resolvedScope), ); } Future writeCacheEntry(String key, T value, {String? scopeToken}) { final resolvedScope = _resolveScopeToken(scopeToken); if (resolvedScope == null) { return Future.value(); } return store.write>( _scopedKey(key, scopeToken: resolvedScope), 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, {String? scopeToken}) { final resolvedScope = _resolveScopeToken(scopeToken); if (resolvedScope == null) { return Future.value(); } return store.remove(CacheScope.scopedKey(key, scopeToken: resolvedScope)); } 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 scopedKey, Future Function() loadFromRemote, { bool Function(T loaded)? shouldWriteLoaded, }) async { final remote = await loadFromRemote(); if (shouldWriteLoaded != null && !shouldWriteLoaded(remote)) { return remote; } await store.write>( scopedKey, CacheEntry(value: encodeValue(remote), fetchedAt: now()), ); return remote; } String _scopedKey(String key, {String? scopeToken}) { return CacheScope.scopedKey(key, scopeToken: scopeToken); } String? _resolveScopeToken(String? scopeToken) { final scoped = scopeToken?.trim(); if (scoped != null && scoped.isNotEmpty) { return scoped; } return CacheScope.maybeToken(); } }