153 lines
4.4 KiB
Dart
153 lines
4.4 KiB
Dart
import 'dart:async';
|
|
import 'dart:convert';
|
|
|
|
import 'package:localstore/localstore.dart';
|
|
|
|
import 'api_response.dart';
|
|
import 'errors/parse_exception.dart';
|
|
|
|
abstract class RequestCache<T extends ApiResponse?> {
|
|
static const int cacheNothing = 0;
|
|
static const int cacheMinute = 60;
|
|
static const int cacheHour = 60 * 60;
|
|
static const int cacheDay = 60 * 60 * 24;
|
|
|
|
static String collection = 'MarianumMobile';
|
|
|
|
int maxCacheTime;
|
|
void Function(T)? onUpdate;
|
|
|
|
/// Called only when [start] finds a cached payload in localstore. Use this
|
|
/// (instead of [onUpdate]) when callers need to distinguish stale-but-fast
|
|
/// cache hits from authoritative network responses.
|
|
void Function(T)? onCacheData;
|
|
|
|
/// Called only when [start] receives a fresh payload from the network.
|
|
void Function(T)? onNetworkData;
|
|
|
|
void Function(Exception) onError;
|
|
bool? renew;
|
|
|
|
final Completer<void> _ready = Completer<void>();
|
|
|
|
/// Resolves when [start] has finished, regardless of whether the network
|
|
/// call succeeded, failed, or was skipped due to a fresh cache. Callers
|
|
/// can await this to know when both the cache lookup and the network
|
|
/// attempt have settled.
|
|
Future<void> get ready => _ready.future;
|
|
|
|
RequestCache(
|
|
this.maxCacheTime,
|
|
this.onUpdate, {
|
|
this.onError = ignore,
|
|
this.renew = false,
|
|
this.onCacheData,
|
|
this.onNetworkData,
|
|
});
|
|
|
|
static void ignore(Exception e) {}
|
|
|
|
Future<void> start(String document) async {
|
|
try {
|
|
final tableData = await Localstore.instance
|
|
.collection(collection)
|
|
.doc(document)
|
|
.get();
|
|
if (tableData != null) {
|
|
final cached = onLocalData(tableData['json'] as String);
|
|
onUpdate?.call(cached);
|
|
onCacheData?.call(cached);
|
|
}
|
|
|
|
final lastUpdate = (tableData?['lastupdate'] as num?) ?? 0;
|
|
if (DateTime.now().millisecondsSinceEpoch - (maxCacheTime * 1000) <
|
|
lastUpdate) {
|
|
if (renew == null || !renew!) return;
|
|
}
|
|
|
|
try {
|
|
final newValue = await onLoad();
|
|
onUpdate?.call(newValue);
|
|
onNetworkData?.call(newValue);
|
|
unawaited(
|
|
Localstore.instance.collection(collection).doc(document).set({
|
|
'json': jsonEncode(newValue),
|
|
'lastupdate': DateTime.now().millisecondsSinceEpoch,
|
|
}),
|
|
);
|
|
} on Exception catch (e) {
|
|
onError(e);
|
|
}
|
|
} finally {
|
|
if (!_ready.isCompleted) _ready.complete();
|
|
}
|
|
}
|
|
|
|
T onLocalData(String json);
|
|
Future<T> onLoad();
|
|
}
|
|
|
|
/// Concrete [RequestCache] that takes the two overrides as constructor
|
|
/// callbacks instead of requiring a subclass per endpoint.
|
|
class SimpleCache<T extends ApiResponse?> extends RequestCache<T> {
|
|
final Future<T> Function() _loader;
|
|
final T Function(Map<String, dynamic> json) _fromJson;
|
|
|
|
SimpleCache({
|
|
required int cacheTime,
|
|
required Future<T> Function() loader,
|
|
required T Function(Map<String, dynamic> json) fromJson,
|
|
void Function(T)? onUpdate,
|
|
void Function(T)? onCacheData,
|
|
void Function(T)? onNetworkData,
|
|
void Function(Exception)? onError,
|
|
bool? renew,
|
|
}) : _loader = loader,
|
|
_fromJson = fromJson,
|
|
super(
|
|
cacheTime,
|
|
onUpdate,
|
|
onError: onError ?? RequestCache.ignore,
|
|
renew: renew,
|
|
onCacheData: onCacheData,
|
|
onNetworkData: onNetworkData,
|
|
);
|
|
|
|
@override
|
|
Future<T> onLoad() => _loader();
|
|
|
|
@override
|
|
T onLocalData(String json) =>
|
|
_fromJson(jsonDecode(json) as Map<String, dynamic>);
|
|
}
|
|
|
|
/// Captures the latest cache payload (cached or network) and rethrows the
|
|
/// captured network error if no payload arrived. Collapses the
|
|
/// `latest`/`capturedError`/`await ready` boilerplate that DataProviders
|
|
/// otherwise repeat per endpoint.
|
|
Future<T> resolveFromCache<T extends ApiResponse?>(
|
|
RequestCache<T> Function(
|
|
void Function(T) onUpdate,
|
|
void Function(Exception) onError,
|
|
)
|
|
build, {
|
|
void Function(Object)? onError,
|
|
String? operationName,
|
|
}) async {
|
|
T? latest;
|
|
Object? capturedError;
|
|
final cache = build((data) => latest = data, (e) {
|
|
capturedError = e;
|
|
onError?.call(e);
|
|
});
|
|
await cache.ready;
|
|
if (latest != null) return latest as T;
|
|
final err = capturedError;
|
|
if (err != null) throw err;
|
|
throw ParseException(
|
|
technicalDetails: operationName != null
|
|
? 'No data and no error from $operationName'
|
|
: null,
|
|
);
|
|
}
|