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

133 lines
3.4 KiB
Dart

import 'dart:async';
import 'cache_policy.dart';
import 'cache_store.dart';
abstract class CachedRepository<T> {
final HybridCacheStore store;
final CachePolicy policy;
final DateTime Function() now;
final Object? Function(T value) encodeValue;
final T Function(Object? raw) decodeValue;
final Map<String, Future<void>> _refreshInFlight = <String, Future<void>>{};
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>(T value) => value;
static T _defaultDecode<T>(Object? raw) => raw as T;
Future<T> getOrLoad({
required String key,
required Future<T> 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<CacheEntry<T>?> readCacheEntry(String key) {
return _readDecodedEntry(key);
}
Future<void> writeCacheEntry(String key, T value) {
return store.write<CacheEntry<Object?>>(
key,
CacheEntry<Object?>(value: encodeValue(value), fetchedAt: now()),
);
}
Future<CacheEntry<T>?> _readDecodedEntry(String key) async {
final entry = await store.read<CacheEntry<Object?>>(key);
if (entry == null) {
return null;
}
try {
return CacheEntry<T>(
value: decodeValue(entry.value),
fetchedAt: entry.fetchedAt,
);
} catch (_) {
await store.remove(key);
return null;
}
}
Future<void> removeCacheKey(String key) {
return store.remove(key);
}
void refreshInBackground({
required String key,
required Future<T> 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<T> _refreshAndWrite(
String key,
Future<T> 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;
}
}