From f856dcb515c9f907f3e04f432ed7e47a99e1ebb4 Mon Sep 17 00:00:00 2001 From: akmalviya03 Date: Thu, 17 Oct 2024 15:41:34 +0530 Subject: [PATCH 1/4] fix: async cache storing exception fixed --- pkgs/async/lib/src/async_cache.dart | 31 +++++++++++++++++++++++---- pkgs/async/test/async_cache_test.dart | 14 ++++++++++++ 2 files changed, 41 insertions(+), 4 deletions(-) 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'); }); From 44234436df98d73bf3c8d7c5fc580024f0ddc2f5 Mon Sep 17 00:00:00 2001 From: akmalviya03 Date: Thu, 17 Oct 2024 15:50:59 +0530 Subject: [PATCH 2/4] updated changelog --- pkgs/async/CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pkgs/async/CHANGELOG.md b/pkgs/async/CHANGELOG.md index 06ac7d11..28796f17 100644 --- a/pkgs/async/CHANGELOG.md +++ b/pkgs/async/CHANGELOG.md @@ -2,6 +2,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 From 7eebaef907cdf4134703d8d641973ab70c0aebeb Mon Sep 17 00:00:00 2001 From: Abhishak Kumar Malviya Date: Fri, 25 Apr 2025 17:11:02 +0530 Subject: [PATCH 3/4] updated changelog --- pkgs/async/CHANGELOG.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkgs/async/CHANGELOG.md b/pkgs/async/CHANGELOG.md index 31877cc2..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 @@ -11,8 +13,6 @@ - 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 From 880ebb898b1cf2d09d3bab58e011451eb1b23aa9 Mon Sep 17 00:00:00 2001 From: Abhishak Kumar Malviya Date: Fri, 2 May 2025 22:03:36 +0530 Subject: [PATCH 4/4] updated test cases --- pkgs/async/test/async_cache_test.dart | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/pkgs/async/test/async_cache_test.dart b/pkgs/async/test/async_cache_test.dart index 2935d0f7..464c1e1b 100644 --- a/pkgs/async/test/async_cache_test.dart +++ b/pkgs/async/test/async_cache_test.dart @@ -20,7 +20,6 @@ void main() { test('should not fetch when callback throws exception', () async { cache = AsyncCache(const Duration(hours: 1), cacheErrors: false); - Future asyncFunctionThatThrows() { throw Exception(); } @@ -28,8 +27,15 @@ void main() { var errorThrowingFuture = cache.fetch(asyncFunctionThatThrows); await expectLater(errorThrowingFuture, throwsA(isException)); - var valueFuture = cache.fetch(() async => 'Success'); - expect(await valueFuture, 'Success'); + 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 {