Skip to content

Commit

Permalink
feat: Add EverCachedState class to represent the state of the cached …
Browse files Browse the repository at this point in the history
…value
  • Loading branch information
ArunPrakashG committed Jul 6, 2024
1 parent 31a36ca commit 37de158
Show file tree
Hide file tree
Showing 9 changed files with 184 additions and 48 deletions.
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,12 @@
## 0.0.6

- Fix the issue with value getter

## 0.0.7

- Introduce EverCachedValue<T> class to wrap the actual to prevent cases which can occur if compute method returned null

## 0.0.8

- Fix the issue with EverCachedValue<T> class
- Refactors
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down Expand Up @@ -59,6 +59,10 @@ final cache = EverCache<String>(
// 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
Expand Down
1 change: 1 addition & 0 deletions lib/ever_cache.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
127 changes: 89 additions & 38 deletions lib/src/ever_cache_base.dart
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ final class EverCache<T> extends ILockable<T> {
if (ttl != null) {
_scheduleInvalidation();
}

if (placeholder != null) {
_state = EverCachedState<T>.placeholder(placeholder!());
}
}

/// A function that asynchronously fetches the value to be cached.
Expand All @@ -41,77 +45,114 @@ final class EverCache<T> extends ILockable<T> {
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<T> value)? disposer;

/// An optional instance of [EverEvents] to handle custom events.
final EverEvents? events;

/// An optional instance of [EverTTL] to set a TTL for the cached value.
final EverTTL? ttl;

T? _value;

EverCachedState<T> _state = EverCachedState<T>.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<T> 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) {
throw const EverStateException('Value has been disposed.');
}

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
// ignore: avoid_equals_and_hash_code_on_mutable_classes
bool operator ==(covariant EverCache<T> other) {
if (identical(this, other)) return true;

return other._value == _value;
return other._state == _state;
}

/// Returns the cached value of type [T] synchronously.
Expand Down Expand Up @@ -139,8 +180,13 @@ final class EverCache<T> extends ILockable<T> {
return true;
}

_value = await guard(
() async => _fetch(),
_state = await guard<EverCachedState<T>>(
() async {
final result = await _fetch();

return EverCachedState<T>(result);
},
EverCachedState<T>.empty,
onError: events?.onError?.call,
onStart: () {
_isComputing = true;
Expand Down Expand Up @@ -175,9 +221,14 @@ final class EverCache<T> extends ILockable<T> {
return;
}

return backgrounded(
() async => _fetch(),
then: (object) => _value = object,
backgrounded<EverCachedState<T>>(
() async {
final result = await _fetch();

return EverCachedState<T>(result);
},
EverCachedState<T>.empty,
then: (object) async => _state = object,
onStart: () {
_isComputing = true;
events?.onComputing?.call();
Expand All @@ -193,22 +244,22 @@ final class EverCache<T> extends ILockable<T> {
// Disposes the cache and resets the value.
void dispose() {
unschedule();
disposer?.call(_value);
_value = null;
disposer?.call(_state);
_state = EverCachedState<T>.disposed();
_isDisposed = true;
events?.onDisposed?.call();
}

/// Invalidates the cached value.
void invalidate() {
_value = null;
_state = EverCachedState<T>.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).
Expand All @@ -234,7 +285,7 @@ final class EverCache<T> extends ILockable<T> {
/// If the value is locked, an [EverStateException] is thrown.
static Future<R?> synced<R, T>(
ILockable<T> lockable,
Future<R> Function(T value) callback, {
Future<R> Function(EverCachedState<T> value) callback, {
void Function(Object error, StackTrace stackTrace)? onError,
}) async {
return lockable.use<R>(
Expand Down
63 changes: 63 additions & 0 deletions lib/src/ever_cached_state.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// ignore_for_file: avoid_equals_and_hash_code_on_mutable_classes

final class EverCachedState<T> {
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<T> other) {
if (identical(this, other)) return true;

return other.value == value;
}
}
4 changes: 4 additions & 0 deletions lib/src/ever_value_base.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
import 'ever_cached_state.dart';

abstract class IEverValue<T> {
T get value;

EverCachedState<T> get state;
}
13 changes: 8 additions & 5 deletions lib/src/helpers.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ import 'dart:async';

/// Runs a function in the background.
void backgrounded<T>(
FutureOr<T> Function() callback, {
FutureOr<void> Function(T object)? then,
Future<T> Function() callback,
T Function() orElse, {
Future<void> Function(T object)? then,
void Function(Object error, StackTrace stackTrace)? onError,
void Function()? onStart,
void Function()? onEnd,
Expand All @@ -17,6 +18,7 @@ void backgrounded<T>(
await then(result);
}
},
orElse,
onStart: onStart,
onEnd: onEnd,
onError: onError,
Expand All @@ -25,8 +27,9 @@ void backgrounded<T>(
}

/// Guards an asynchronous function.
FutureOr<T?> guard<T>(
FutureOr<T> Function() function, {
Future<T> guard<T>(
Future<T> Function() function,
T Function() orElse, {
void Function(Object error, StackTrace stackTrace)? onError,
void Function()? onStart,
void Function()? onEnd,
Expand All @@ -36,7 +39,7 @@ FutureOr<T?> guard<T>(
return await function();
} catch (error, stackTrace) {
onError?.call(error, stackTrace);
return null;
return orElse();
} finally {
onEnd?.call();
}
Expand Down
7 changes: 4 additions & 3 deletions lib/src/lockable_base.dart
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,16 @@ abstract base class ILockable<T> extends IEverValue<T> {
///
/// If the value is locked, an [EverStateException] is thrown.
Future<R?> use<R>(
Future<R> Function(T value) callback, {
Future<R> Function(EverCachedState<T> value) callback, {
void Function(Object error, StackTrace stackTrace)? onError,
}) async {
if (locked) {
throw const EverStateException('Value is locked.');
}

return await guard<R>(
() async => callback(value),
return guard<R?>(
() async => callback(state),
() => null,
onStart: lock,
onEnd: unlock,
onError: onError,
Expand Down
Loading

0 comments on commit 37de158

Please sign in to comment.