fix: async cache storing exception fixed (#548)
Co-authored-by: Moritz <mosum@google.com>
diff --git a/pkgs/async/CHANGELOG.md b/pkgs/async/CHANGELOG.md
index a45dc81..d84fbfb 100644
--- a/pkgs/async/CHANGELOG.md
+++ b/pkgs/async/CHANGELOG.md
@@ -11,7 +11,9 @@
- Fix `StreamGroup.broadcast().close()` to properly complete when all streams in
the group close without being explicitly removed.
- Run `dart format` with the new style.
-
+- Can decide `fetch` method of `AsyncCache` will store exception or not
+ by using `cacheErrors` property.
+-
## 2.13.0
- Fix type check and cast in SubscriptionStream's cancelOnError wrapper
diff --git a/pkgs/async/lib/src/async_cache.dart b/pkgs/async/lib/src/async_cache.dart
index f86b060..7ce667d 100644
--- a/pkgs/async/lib/src/async_cache.dart
+++ b/pkgs/async/lib/src/async_cache.dart
@@ -36,6 +36,15 @@
/// Cached results of a previous [fetch] call.
Future<T>? _cachedValueFuture;
+ /// Whether the cache will keep a future completed with an error.
+ ///
+ /// If `false`, a non-ephemeral cache will clear the cached future
+ /// immediately if the future completes with an error, as if the
+ /// caching was ephemeral.
+ /// _(Ephemeral caches always clear when the future completes,
+ /// so this flag has no effect on those.)_
+ final bool _cacheErrors;
+
/// Fires when the cache should be considered stale.
Timer? _stale;
@@ -44,14 +53,20 @@
/// The [duration] starts counting after the Future returned by [fetch]
/// completes, or after the Stream returned by `fetchStream` emits a done
/// event.
- AsyncCache(Duration duration) : _duration = duration;
+ /// If [cacheErrors] is `false` the cache will be invalidated if the [Future]
+ /// returned by the callback completes as an error.
+ AsyncCache(Duration duration, {bool cacheErrors = true})
+ : _duration = duration,
+ _cacheErrors = cacheErrors;
/// Creates a cache that invalidates after an in-flight request is complete.
///
/// An ephemeral cache guarantees that a callback function will only be
/// executed at most once concurrently. This is useful for requests for which
/// data is updated frequently but stale data is acceptable.
- AsyncCache.ephemeral() : _duration = null;
+ AsyncCache.ephemeral()
+ : _duration = null,
+ _cacheErrors = true;
/// Returns a cached value from a previous call to [fetch], or runs [callback]
/// to compute a new one.
@@ -62,8 +77,18 @@
if (_cachedStreamSplitter != null) {
throw StateError('Previously used to cache via `fetchStream`');
}
- return _cachedValueFuture ??= callback()
- ..whenComplete(_startStaleTimer).ignore();
+ if (_cacheErrors) {
+ return _cachedValueFuture ??= callback()
+ ..whenComplete(_startStaleTimer).ignore();
+ } else {
+ return _cachedValueFuture ??= callback().then((value) {
+ _startStaleTimer();
+ return value;
+ }, onError: (Object error, StackTrace stack) {
+ invalidate();
+ throw error;
+ });
+ }
}
/// Returns a cached stream from a previous call to [fetchStream], or runs
diff --git a/pkgs/async/test/async_cache_test.dart b/pkgs/async/test/async_cache_test.dart
index 47204e6..464c1e1 100644
--- a/pkgs/async/test/async_cache_test.dart
+++ b/pkgs/async/test/async_cache_test.dart
@@ -18,6 +18,26 @@
cache = AsyncCache(const Duration(hours: 1));
});
+ test('should not fetch when callback throws exception', () async {
+ cache = AsyncCache(const Duration(hours: 1), cacheErrors: false);
+ Future<String> asyncFunctionThatThrows() {
+ throw Exception();
+ }
+
+ var errorThrowingFuture = cache.fetch(asyncFunctionThatThrows);
+ await expectLater(errorThrowingFuture, throwsA(isException));
+
+ FakeAsync().run((fakeAsync) async {
+ var timesCalled = 0;
+ Future<String> call() async => 'Called ${++timesCalled}';
+
+ expect(await cache.fetch(call), 'Called 1');
+
+ fakeAsync.elapse(const Duration(hours: 1));
+ expect(await cache.fetch(call), 'Called 2');
+ });
+ });
+
test('should fetch via a callback when no cache exists', () async {
expect(await cache.fetch(() async => 'Expensive'), 'Expensive');
});