Skip to content

Commit

Permalink
Merge pull request #88 from jinyus:feat/resetIfError
Browse files Browse the repository at this point in the history
[Feat] Add FutureBeacon.toFuture() resetIfError option
  • Loading branch information
jinyus authored Mar 6, 2024
2 parents 489bd9b + 52a2c84 commit afeec94
Show file tree
Hide file tree
Showing 8 changed files with 227 additions and 33 deletions.
2 changes: 1 addition & 1 deletion Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -1016,7 +1016,7 @@ Set the global `BeaconObserver` instance to get notified of all beacon creation,
You can create your own observer by implementing `BeaconObserver` or use the provided logging observer, which logs to the console. Provide a `name` to your beacons to make it easier to identify them in the logs.

```dart
BeaconObserver.instance = LoggingObserver();
BeaconObserver.instance = LoggingObserver(); // or BeaconObserver.useLogging()
var a = Beacon.writable(10, name: 'a');
var b = Beacon.writable(20, name: 'b');
Expand Down
31 changes: 3 additions & 28 deletions packages/state_beacon_core/lib/src/beacons/async.dart
Original file line number Diff line number Diff line change
Expand Up @@ -16,32 +16,9 @@ abstract class AsyncBeacon<T> extends ReadableBeacon<AsyncValue<T>>
if (!manualStart) _start();
}

/// Exposes this as a [Future] that can be awaited in a derived future beacon.
/// This will trigger a re-run of the derived beacon when its state changes.
///
/// var count = Beacon.writable(0);
/// var firstName = Beacon.derivedFuture(() async => 'Sally ${count.value}');
///
/// var lastName = Beacon.derivedFuture(() async => 'Smith ${count.value}');
///
/// var fullName = Beacon.derivedFuture(() async {
///
/// // no need for a manual switch expression
/// final fnameFuture = firstName.toFuture();
/// final lnameFuture = lastName.toFuture();
/// final fname = await fnameFuture;
/// final lname = await lnameFuture;
///
/// return '$fname $lname';
/// });
Future<T> toFuture() {
_completer ??= Beacon.writable(Completer<T>(), name: "$name's future");
return _completer!.value.future;
}

/// Alias for peek().lastData.
/// Returns the last data that was successfully loaded
/// This is useful when you want to display old data when
/// in [AsyncError] or [AsyncLoading] state.
/// equivalent to `beacon.peek().lastData`
T? get lastData => peek().lastData;

Expand Down Expand Up @@ -142,9 +119,7 @@ abstract class AsyncBeacon<T> extends ReadableBeacon<AsyncValue<T>>
Beacon.untracked(() {
_sub = stream.listen(
(v) => _setValue(AsyncData(v)),
onError: (Object e, StackTrace s) {
_setErrorWithLastData(e, s);
},
onError: _setErrorWithLastData,
cancelOnError: cancelOnError,
);
});
Expand Down
47 changes: 47 additions & 0 deletions packages/state_beacon_core/lib/src/beacons/future.dart
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,51 @@ class FutureBeacon<T> extends AsyncBeacon<T> {
_cancel();
_wakeUp();
}

/// Exposes this as a [Future] that can be awaited in a future beacon.
/// This will trigger a re-run of the derived beacon when its state changes.
///
/// If `resetIfError` is `true` and the beacon is **currently** in an error
/// state, the beacon will be reset before the future is returned.
///
/// Example:
/// ```dart
/// var count = Beacon.writable(0);
///
/// var count = Beacon.writable(0);
/// var firstName = Beacon.future(() async => 'Sally ${count.value}');
///
/// var lastName = Beacon.future(() async => 'Smith ${count.value}');
///
/// var fullName = Beacon.future(() async {
///
/// // no need for a manual switch expression
/// final fnameFuture = firstName.toFuture();
/// final lnameFuture = lastName.toFuture();
/// final fname = await fnameFuture;
/// final lname = await lnameFuture;
///
/// return '$fname $lname';
/// });
Future<T> toFuture({bool resetIfError = false}) {
if (_completer == null) {
// first time
final completer = Completer<T>();
_completer = Beacon.writable(completer, name: "$name's future");

if (peek() case final AsyncData<T> data) {
completer.complete(data.value);
} else if (!resetIfError && isError) {
final error = peek() as AsyncError<T>;
completer.completeError(error.error, error.stackTrace);
}
}

if (resetIfError && isError) {
reset();
}

return _completer!.value.future;
}
}
18 changes: 18 additions & 0 deletions packages/state_beacon_core/lib/src/beacons/stream.dart
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,22 @@ class StreamBeacon<T> extends AsyncBeacon<T> {
void resume() {
_sub?.resume();
}

/// Exposes this as a [Future] that can be awaited in a future beacon.
/// This will trigger a re-run of the derived beacon when its state changes.
Future<T> toFuture() {
if (_completer == null) {
// first time
final completer = Completer<T>();
_completer = Beacon.writable(completer, name: "$name's future");

if (peek() case final AsyncData<T> data) {
completer.complete(data.value);
} else if (peek() case final AsyncError<T> error) {
completer.completeError(error.error, error.stackTrace);
}
}

return _completer!.value.future;
}
}
9 changes: 8 additions & 1 deletion packages/state_beacon_core/lib/src/mixins/autosleep.dart
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,16 @@ mixin _AutoSleep<T, SubT> on ReadableBeacon<T> {

@override
void _removeObserver(Consumer observer) {
// we don't want to sleep on temporary subscriptions
// such as .next() calls
// we want to include widget subscriptions so if it's not empty
// we have widgets subs and sleep if it get unmounted
final canSleep =
// ignore: deprecated_member_use_from_same_package
shouldSleep && ($$widgetSubscribers$$.isNotEmpty || observer is Effect);
super._removeObserver(observer);
if (!shouldSleep) return;
if (_observers.isEmpty) {
if (canSleep && _observers.isEmpty) {
_goToSleep();
}
}
Expand Down
101 changes: 99 additions & 2 deletions packages/state_beacon_core/test/src/beacons/future_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,6 @@ void main() {

futureBeacon.reset();

BeaconScheduler.flush();

expect(futureBeacon.isLoading, true);

await delay();
Expand Down Expand Up @@ -902,4 +900,103 @@ void main() {
]),
);
});

test('toFuture() should reset when in error state', () async {
var called = 0;

final f1 = Beacon.future(() async {
called++;

await delay(k10ms);

if (called == 1) {
throw Exception('error');
}

return called;
});

final next = await f1.next();

expect(next.isError, true);
expect(called, 1);

final val = await f1.toFuture(resetIfError: true);

expect(val, 2);
expect(called, 2);
});

test('toFuture() should NOT reset when in data state', () async {
// BeaconObserver.useLogging();
var called = 0;

final f1 = Beacon.future(() async {
called++;

await delay(k10ms);

return called;
});

final next = await f1.next();

expect(next.unwrap(), 1);
expect(called, 1);

final val = await f1.toFuture(resetIfError: true);

expect(val, 1);
expect(called, 1);
});

test('toFuture() should return data instantly', () async {
// BeaconObserver.useLogging();
var called = 0;

final f1 = Beacon.future(() async {
called++;

await delay(k10ms);

return called;
});

final next = await f1.next();

expect(next.isData, true);

expect(called, 1);

await expectLater(f1.toFuture(), completion(1));

expect(called, 1);
});

test('toFuture() should return error instantly', () async {
// BeaconObserver.useLogging();
var called = 0;

final f1 = Beacon.future(() async {
called++;

await delay(k10ms);

if (called == 1) {
throw Exception('error');
}

return called;
});

final next = await f1.next();

expect(next.isError, true);

expect(called, 1);

await expectLater(f1.toFuture(), throwsException);

expect(called, 1);
});
}
50 changes: 50 additions & 0 deletions packages/state_beacon_core/test/src/beacons/stream_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -412,4 +412,54 @@ void main() {
// expect(num1.listenersCount, 1);
expect(beacon.listenersCount, 0);
});

test('toFuture() should return data instantly', () async {
// BeaconObserver.useLogging();
var called = 0;

final f1 = Beacon.stream(() async* {
called++;

await delay(k10ms);

yield called;
});

final next = await f1.next();

expect(next.isData, true);

expect(called, 1);

await expectLater(f1.toFuture(), completion(1));

expect(called, 1);
});

test('toFuture() should return error instantly', () async {
// BeaconObserver.useLogging();
var called = 0;

final f1 = Beacon.stream(() async* {
called++;

await delay(k10ms);

if (called == 1) {
throw Exception('error');
}

yield called;
});

final next = await f1.next();

expect(next.isError, true);

expect(called, 1);

await expectLater(f1.toFuture(), throwsException);

expect(called, 1);
});
}
2 changes: 1 addition & 1 deletion run.sh
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ test_target() {
if [ "$1" == "core" ]; then
echo "testing core"
cd packages/state_beacon_core &&
flutter test --coverage
flutter test --coverage --timeout 5s
elif [ "$1" == "flutter" ]; then
echo "testing flutter"
cd packages/state_beacon &&
Expand Down

0 comments on commit afeec94

Please sign in to comment.