169 lines
4.7 KiB
Dart
169 lines
4.7 KiB
Dart
import 'dart:async';
|
|
|
|
import 'cache_scope.dart';
|
|
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 {
|
|
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<CacheEntry<T>?> readCacheEntry(String key, {String? scopeToken}) {
|
|
final resolvedScope = _resolveScopeToken(scopeToken);
|
|
if (resolvedScope == null) {
|
|
return Future<CacheEntry<T>?>.value(null);
|
|
}
|
|
return _readDecodedEntry(
|
|
CacheScope.scopedKey(key, scopeToken: resolvedScope),
|
|
);
|
|
}
|
|
|
|
Future<void> writeCacheEntry(String key, T value, {String? scopeToken}) {
|
|
final resolvedScope = _resolveScopeToken(scopeToken);
|
|
if (resolvedScope == null) {
|
|
return Future<void>.value();
|
|
}
|
|
return store.write<CacheEntry<Object?>>(
|
|
_scopedKey(key, scopeToken: resolvedScope),
|
|
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, {String? scopeToken}) {
|
|
final resolvedScope = _resolveScopeToken(scopeToken);
|
|
if (resolvedScope == null) {
|
|
return Future<void>.value();
|
|
}
|
|
return store.remove(CacheScope.scopedKey(key, scopeToken: resolvedScope));
|
|
}
|
|
|
|
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 scopedKey,
|
|
Future<T> Function() loadFromRemote, {
|
|
bool Function(T loaded)? shouldWriteLoaded,
|
|
}) async {
|
|
final remote = await loadFromRemote();
|
|
if (shouldWriteLoaded != null && !shouldWriteLoaded(remote)) {
|
|
return remote;
|
|
}
|
|
await store.write<CacheEntry<Object?>>(
|
|
scopedKey,
|
|
CacheEntry<Object?>(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();
|
|
}
|
|
}
|