diff --git a/pkgs/async/CHANGELOG.md b/pkgs/async/CHANGELOG.md index 5e5d952a..16158aa2 100644 --- a/pkgs/async/CHANGELOG.md +++ b/pkgs/async/CHANGELOG.md @@ -2,7 +2,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 f86b0600..1f23c7a3 100644 --- a/pkgs/async/lib/src/async_cache.dart +++ b/pkgs/async/lib/src/async_cache.dart @@ -36,6 +36,15 @@ class AsyncCache { /// Cached results of a previous [fetch] call. Future? _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,18 @@ class AsyncCache { /// 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 +75,18 @@ class AsyncCache { 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 47204e6a..464c1e1b 100644 --- a/pkgs/async/test/async_cache_test.dart +++ b/pkgs/async/test/async_cache_test.dart @@ -18,6 +18,26 @@ void main() { cache = AsyncCache(const Duration(hours: 1)); }); + test('should not fetch when callback throws exception', () async { + cache = AsyncCache(const Duration(hours: 1), cacheErrors: false); + Future asyncFunctionThatThrows() { + throw Exception(); + } + + var errorThrowingFuture = cache.fetch(asyncFunctionThatThrows); + await expectLater(errorThrowingFuture, throwsA(isException)); + + FakeAsync().run((fakeAsync) async { + var timesCalled = 0; + Future 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'); });