diff --git a/pkgs/async/CHANGELOG.md b/pkgs/async/CHANGELOG.md index 1f6d74da..fea64685 100644 --- a/pkgs/async/CHANGELOG.md +++ b/pkgs/async/CHANGELOG.md @@ -10,6 +10,8 @@ - Require Dart 3.4. - Move to `dart-lang/core` monorepo. +- Can decide `fetch` method of `AsyncCache` will store exception or not + by using `cacheErrors` property. ## 2.11.0 diff --git a/pkgs/async/lib/src/async_cache.dart b/pkgs/async/lib/src/async_cache.dart index 6fc7cb0e..8b5db52c 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 f7c8caa6..8af090d4 100644 --- a/pkgs/async/test/async_cache_test.dart +++ b/pkgs/async/test/async_cache_test.dart @@ -18,6 +18,20 @@ 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)); + + var valueFuture = cache.fetch(() async => 'Success'); + expect(await valueFuture, 'Success'); + }); + test('should fetch via a callback when no cache exists', () async { expect(await cache.fetch(() async => 'Expensive'), 'Expensive'); });