133 lines
3.4 KiB
Dart
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;
|
|
}
|
|
}
|