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

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();
}
}