Skip to content

Instantly share code, notes, and snippets.

@lukepighetti
Last active February 13, 2024 13:03
Show Gist options
  • Save lukepighetti/82bdea189049fdebc49a563ba2c713c0 to your computer and use it in GitHub Desktop.
Save lukepighetti/82bdea189049fdebc49a563ba2c713c0 to your computer and use it in GitHub Desktop.
import '/features/architecture/logging.dart';
class CachedQuery<T> {
final Duration invalidation;
final Future<T> Function(String key) fn;
CachedQuery(
this.fn,
this.invalidation,
);
final _log = Logger('CachedQuery<$T>');
final _cache = <String, (T, DateTime expiresAt)>{};
void invalidate(String key) {
_cache.remove(key);
}
Future<T> invalidateAndFetch(String key) {
invalidate(key);
return fetch(key);
}
void invalidateAll() {
_cache.clear();
}
Future<T> call([String key = '']) {
return fetch(key);
}
Future<T> fetch([String key = '']) async {
final (cached, expiresAt) = _cache[key] ?? (null, DateTime(-1));
final now = DateTime.now();
if (cached != null && now.isBefore(expiresAt)) {
return cached;
} else {
try {
final value = await fn(key);
final expiry = now.add(invalidation);
_cache[key] = (value, expiry);
return value;
} catch (e, trace) {
_log.e('', e, trace);
rethrow;
}
}
}
}
extension ChangeNotifierExtensions on ChangeNotifier {
void setState(VoidCallback fn) {
fn();
// ignore: invalid_use_of_protected_member, invalid_use_of_visible_for_testing_member
notifyListeners();
}
}
// remote state
final userSettings = FirebaseAuth.instance
.userChanges()
.map((it) => it?.uid)
.mapStreamOrNull(FirebaseFirestore.instance.watchUserSettings)
.toStateStream(null);
main() async* {
print('latest value ${userSettings.value}');
await for (final value in userSettings) {
print('new value $value');
}
}
// ephemeral state in StatefulWidgets
// big chunks of ephemeral state
final onboardingState = OnboardingState();
class OnboardingState extends ChangeNotifier {
var onboarded = false;
void completeOnboarding() => setState(() => onboarded = true);
}
// cached futures
final userQuery = CachedQuery(
(String uid) => getUser(id),
Duration(seconds: 30),
);
main() async {
print(await userQuery('me')); // hits network
print(await userQuery('me')); // hits cache
print(await userQuery('me')); // hits cache
userQuery.invalidate('me');
print(await userQuery('me')); // hits network
await Future.delayed(Duration(seconds: 31));
print(await userQuery('me')); // hits network
}
extension StreamExtensions<T> on Stream<T> {
Stream<E> mapFuture<E>(FutureOr<E> Function(T) fn) => asyncMap(fn);
Stream<S> mapStream<S>(Stream<S> Function(T) fn) => switchMap(fn);
ValueStream<T> toStateStream(T seed) =>
distinct().shareValueSeeded(seed).keepAlive();
}
extension NullableStreamExtensions<T> on Stream<T?> {
Stream<E?> mapOrNull<E>(E Function(T) fn) {
return map((val) => val?.let(fn));
}
Stream<E?> mapFutureOrNull<E>(Future<E> Function(T) fn) {
return mapFuture((val) => val?.let(fn) ?? Future.value(null));
}
Stream<S?> mapStreamOrNull<S>(Stream<S> Function(T) fn) {
return mapStream((val) => val?.let(fn) ?? Stream.value(null));
}
}
extension ValueStreamX<T> on ValueStream<T> {
// .shareValue / .shareValueSeeded close when there are no listeners
// which happens on hot reload or app pause. So this is a shim to
// keep these ValueStreams alive
ValueStream<T> keepAlive() {
listen((_) {});
return this;
}
}
extension NullableValueStreamExtensions<T> on ValueStream<T?> {
/// Returns the first available non-null value. Tries [value] falling back
/// to waiting for matching stream events
Future<T> get firstNotNull async {
final x = value;
if (x != null) {
return x;
} else {
return await firstWhere((it) => it != null) as T;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment