From 37de158945e1c806dc10acae7ca1554e5d8a1c85 Mon Sep 17 00:00:00 2001 From: Arun Prakash Date: Sat, 6 Jul 2024 22:43:24 +0530 Subject: [PATCH] feat: Add EverCachedState class to represent the state of the cached value --- CHANGELOG.md | 9 +++ README.md | 6 +- lib/ever_cache.dart | 1 + lib/src/ever_cache_base.dart | 127 +++++++++++++++++++++++---------- lib/src/ever_cached_state.dart | 63 ++++++++++++++++ lib/src/ever_value_base.dart | 4 ++ lib/src/helpers.dart | 13 ++-- lib/src/lockable_base.dart | 7 +- pubspec.yaml | 2 +- 9 files changed, 184 insertions(+), 48 deletions(-) create mode 100644 lib/src/ever_cached_state.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index 13d50f7..dbae101 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,3 +22,12 @@ ## 0.0.6 - Fix the issue with value getter + +## 0.0.7 + +- Introduce EverCachedValue class to wrap the actual to prevent cases which can occur if compute method returned null + +## 0.0.8 + +- Fix the issue with EverCachedValue class +- Refactors diff --git a/README.md b/README.md index d3bd489..efd34bf 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ Integrate `ever_cache` into your project effortlessly. Just sprinkle this into y ```yaml dependencies: - ever_cache: ^0.0.6 + ever_cache: ^0.0.8 ``` then run `pub get` or `flutter pub get`. @@ -59,6 +59,10 @@ final cache = EverCache( // if you want the cache to be computed as soon as this constructor is called in the background earlyCompute: true, ); + +// access the computed value +// cache.value +// or to safely access it, cache.state ``` ### 📚 Additional Methods diff --git a/lib/ever_cache.dart b/lib/ever_cache.dart index a8ce324..d4e7d4b 100644 --- a/lib/ever_cache.dart +++ b/lib/ever_cache.dart @@ -2,6 +2,7 @@ library; export 'src/ever_cache_base.dart'; +export 'src/ever_cached_state.dart'; export 'src/ever_events.dart'; export 'src/ever_ttl.dart'; export 'src/ever_value_base.dart' show IEverValue; diff --git a/lib/src/ever_cache_base.dart b/lib/src/ever_cache_base.dart index b3c42c0..80c36ec 100644 --- a/lib/src/ever_cache_base.dart +++ b/lib/src/ever_cache_base.dart @@ -32,6 +32,10 @@ final class EverCache extends ILockable { if (ttl != null) { _scheduleInvalidation(); } + + if (placeholder != null) { + _state = EverCachedState.placeholder(placeholder!()); + } } /// A function that asynchronously fetches the value to be cached. @@ -41,7 +45,7 @@ final class EverCache extends ILockable { final T Function()? placeholder; /// An optional function that allows to define custom disposing logic for the object. - final void Function(T? value)? disposer; + final void Function(EverCachedState value)? disposer; /// An optional instance of [EverEvents] to handle custom events. final EverEvents? events; @@ -49,42 +53,67 @@ final class EverCache extends ILockable { /// An optional instance of [EverTTL] to set a TTL for the cached value. final EverTTL? ttl; - T? _value; - + EverCachedState _state = EverCachedState.empty(); Timer? _timer; - bool _isComputing = false; - bool _isDisposed = false; /// Indicates whether the value has been computed and cached. - bool get computed => _value != null; + bool get computed { + return !_state.isEmpty && !_state.isPlaceholder; + } /// Indicates whether the value is being computed. bool get computing => _isComputing; /// Indicates whether the value has been disposed. - bool get disposed => _isDisposed; - - @override - // ignore: avoid_equals_and_hash_code_on_mutable_classes - int get hashCode { - return _value?.hashCode ?? - computed.hashCode ^ - disposed.hashCode ^ - computing.hashCode ^ - scheduled.hashCode ^ - _fetch.hashCode; - } + bool get disposed => _isDisposed && _state.disposed; /// Indicates whether a timer for invalidation (based on TTL) is scheduled. bool get scheduled => _timer != null; - /// The cached value of type [T]. + /// The cached state of type [T]. /// - /// Throws an [EverStateException] if the value has been disposed or is being evaluated. + /// Please note the difference between `state` and `value`: /// - /// If the value is not yet computed, it will be fetched in the background as soon as possible. + /// - `state` returns an instance of [EverCachedState] that contains the value and metadata. + /// - `value` returns the actual value of type [T]. + /// + /// The `state` property is useful when you need to access the cache state safely. + /// For example, you can check if the computation returned a null value or if the value is a placeholder. + /// ```dart + /// final state = cache.state; + /// + /// if (state.hasValue) { + /// print(state.value); + /// } + /// + /// if (state.isPlaceholder) { + /// print('Value is a placeholder.'); + /// } + /// ``` + /// + /// Throws an [EverStateException] if the value has been disposed. + /// + /// If the value is not yet computed, it will be computed in the background as soon as possible. + @override + EverCachedState get state { + if (disposed) { + throw const EverStateException('Value has been disposed.'); + } + + if (!computed && !computing) { + computeSync(); + } + + return _state; + } + + /// The underlying cached value of type [T]. + /// + /// Throws an [EverStateException] if the value has been disposed, not computed or is being computed. + /// + /// It also throws an [EverStateException] if the computed value is null. @override T get value { if (disposed) { @@ -92,18 +121,30 @@ final class EverCache extends ILockable { } if (!computed) { - if (!computing) { - computeSync(); + if (_state.isPlaceholder) { + return _state.value!; } - if (placeholder != null) { - return placeholder!(); - } + throw const EverStateException('Value is not yet computed.'); + } + if (computing) { throw const EverStateException('Value is being evaluated.'); } - return _value!; + if (!_state.hasValue) { + throw const EverStateException( + 'The computation resulted in a null value. Please use `placeholder()` to provide a default value.', + ); + } + + return _state.value!; + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + int get hashCode { + return _state.hashCode; } @override @@ -111,7 +152,7 @@ final class EverCache extends ILockable { bool operator ==(covariant EverCache other) { if (identical(this, other)) return true; - return other._value == _value; + return other._state == _state; } /// Returns the cached value of type [T] synchronously. @@ -139,8 +180,13 @@ final class EverCache extends ILockable { return true; } - _value = await guard( - () async => _fetch(), + _state = await guard>( + () async { + final result = await _fetch(); + + return EverCachedState(result); + }, + EverCachedState.empty, onError: events?.onError?.call, onStart: () { _isComputing = true; @@ -175,9 +221,14 @@ final class EverCache extends ILockable { return; } - return backgrounded( - () async => _fetch(), - then: (object) => _value = object, + backgrounded>( + () async { + final result = await _fetch(); + + return EverCachedState(result); + }, + EverCachedState.empty, + then: (object) async => _state = object, onStart: () { _isComputing = true; events?.onComputing?.call(); @@ -193,22 +244,22 @@ final class EverCache extends ILockable { // Disposes the cache and resets the value. void dispose() { unschedule(); - disposer?.call(_value); - _value = null; + disposer?.call(_state); + _state = EverCachedState.disposed(); _isDisposed = true; events?.onDisposed?.call(); } /// Invalidates the cached value. void invalidate() { - _value = null; + _state = EverCachedState.empty(); unschedule(); events?.onInvalidated?.call(); } @override String toString() { - return 'EverCache(_fetch: $_fetch, placeholder: $placeholder, events: $events, ttl: $ttl)'; + return 'EverCache<$T>(computed: $computed, computing: $computing, disposed: $disposed, scheduled: $scheduled)'; } /// Unschedules the timer for invalidation (based on TTL). @@ -234,7 +285,7 @@ final class EverCache extends ILockable { /// If the value is locked, an [EverStateException] is thrown. static Future synced( ILockable lockable, - Future Function(T value) callback, { + Future Function(EverCachedState value) callback, { void Function(Object error, StackTrace stackTrace)? onError, }) async { return lockable.use( diff --git a/lib/src/ever_cached_state.dart b/lib/src/ever_cached_state.dart new file mode 100644 index 0000000..273f825 --- /dev/null +++ b/lib/src/ever_cached_state.dart @@ -0,0 +1,63 @@ +// ignore_for_file: avoid_equals_and_hash_code_on_mutable_classes + +final class EverCachedState { + factory EverCachedState(T value) { + return EverCachedState._( + value: value, + computedAt: DateTime.now(), + ); + } + + factory EverCachedState.empty() { + return EverCachedState._( + computedAt: DateTime.now(), + isEmpty: true, + ); + } + + factory EverCachedState.placeholder(T value) { + return EverCachedState._( + value: value, + computedAt: DateTime.now(), + isPlaceholder: true, + ); + } + + factory EverCachedState.disposed() { + return const EverCachedState._( + disposed: true, + ); + } + + const EverCachedState._({ + this.value, + this.computedAt, + this.isPlaceholder = false, + this.isEmpty = false, + this.disposed = false, + }); + + final T? value; + final DateTime? computedAt; + final bool isPlaceholder; + final bool isEmpty; + final bool disposed; + + bool get hasValue { + return !isEmpty && value != null; + } + + bool get hasComputedValue { + return hasValue && computedAt != null && !isPlaceholder; + } + + @override + int get hashCode => value.hashCode; + + @override + bool operator ==(covariant EverCachedState other) { + if (identical(this, other)) return true; + + return other.value == value; + } +} diff --git a/lib/src/ever_value_base.dart b/lib/src/ever_value_base.dart index c4dd803..3771179 100644 --- a/lib/src/ever_value_base.dart +++ b/lib/src/ever_value_base.dart @@ -1,3 +1,7 @@ +import 'ever_cached_state.dart'; + abstract class IEverValue { T get value; + + EverCachedState get state; } diff --git a/lib/src/helpers.dart b/lib/src/helpers.dart index 90a8c41..3370905 100644 --- a/lib/src/helpers.dart +++ b/lib/src/helpers.dart @@ -2,8 +2,9 @@ import 'dart:async'; /// Runs a function in the background. void backgrounded( - FutureOr Function() callback, { - FutureOr Function(T object)? then, + Future Function() callback, + T Function() orElse, { + Future Function(T object)? then, void Function(Object error, StackTrace stackTrace)? onError, void Function()? onStart, void Function()? onEnd, @@ -17,6 +18,7 @@ void backgrounded( await then(result); } }, + orElse, onStart: onStart, onEnd: onEnd, onError: onError, @@ -25,8 +27,9 @@ void backgrounded( } /// Guards an asynchronous function. -FutureOr guard( - FutureOr Function() function, { +Future guard( + Future Function() function, + T Function() orElse, { void Function(Object error, StackTrace stackTrace)? onError, void Function()? onStart, void Function()? onEnd, @@ -36,7 +39,7 @@ FutureOr guard( return await function(); } catch (error, stackTrace) { onError?.call(error, stackTrace); - return null; + return orElse(); } finally { onEnd?.call(); } diff --git a/lib/src/lockable_base.dart b/lib/src/lockable_base.dart index fe821d0..e74978b 100644 --- a/lib/src/lockable_base.dart +++ b/lib/src/lockable_base.dart @@ -21,15 +21,16 @@ abstract base class ILockable extends IEverValue { /// /// If the value is locked, an [EverStateException] is thrown. Future use( - Future Function(T value) callback, { + Future Function(EverCachedState value) callback, { void Function(Object error, StackTrace stackTrace)? onError, }) async { if (locked) { throw const EverStateException('Value is locked.'); } - return await guard( - () async => callback(value), + return guard( + () async => callback(state), + () => null, onStart: lock, onEnd: unlock, onError: onError, diff --git a/pubspec.yaml b/pubspec.yaml index 01f08d4..9073d3f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: ever_cache description: Allows to cache a computed value for a specific duration. -version: 0.0.6 +version: 0.0.8 repository: https://github.com/ArunPrakashG/ever_cache environment: