import 'dart:async'; import 'dart:convert'; import 'package:localstore/localstore.dart'; import 'api_response.dart'; abstract class RequestCache { 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 _ready = Completer(); /// 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 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 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 onLoad(); } /// Concrete [RequestCache] that delegates the two overrides to functions /// passed in the constructor. Used to collapse the dozens of one-class-per- /// endpoint cache files that all just forward to `().run()` and /// `.fromJson(jsonDecode(...))`. class SimpleCache extends RequestCache { final Future Function() _loader; final T Function(Map json) _fromJson; SimpleCache({ required int cacheTime, required Future Function() loader, required T Function(Map 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 onLoad() => _loader(); @override T onLocalData(String json) => _fromJson(jsonDecode(json) as Map); }