diff --git a/Readme.md b/Readme.md index 2d7aceaf..542f8a09 100644 --- a/Readme.md +++ b/Readme.md @@ -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'); diff --git a/packages/state_beacon_core/lib/src/beacons/async.dart b/packages/state_beacon_core/lib/src/beacons/async.dart index 4ab72ad5..3c627eec 100644 --- a/packages/state_beacon_core/lib/src/beacons/async.dart +++ b/packages/state_beacon_core/lib/src/beacons/async.dart @@ -16,32 +16,9 @@ abstract class AsyncBeacon extends ReadableBeacon> 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 toFuture() { - _completer ??= Beacon.writable(Completer(), 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; @@ -142,9 +119,7 @@ abstract class AsyncBeacon extends ReadableBeacon> Beacon.untracked(() { _sub = stream.listen( (v) => _setValue(AsyncData(v)), - onError: (Object e, StackTrace s) { - _setErrorWithLastData(e, s); - }, + onError: _setErrorWithLastData, cancelOnError: cancelOnError, ); }); diff --git a/packages/state_beacon_core/lib/src/beacons/future.dart b/packages/state_beacon_core/lib/src/beacons/future.dart index 5de86817..f315b783 100644 --- a/packages/state_beacon_core/lib/src/beacons/future.dart +++ b/packages/state_beacon_core/lib/src/beacons/future.dart @@ -24,4 +24,51 @@ class FutureBeacon extends AsyncBeacon { _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 toFuture({bool resetIfError = false}) { + if (_completer == null) { + // first time + final completer = Completer(); + _completer = Beacon.writable(completer, name: "$name's future"); + + if (peek() case final AsyncData data) { + completer.complete(data.value); + } else if (!resetIfError && isError) { + final error = peek() as AsyncError; + completer.completeError(error.error, error.stackTrace); + } + } + + if (resetIfError && isError) { + reset(); + } + + return _completer!.value.future; + } } diff --git a/packages/state_beacon_core/lib/src/beacons/stream.dart b/packages/state_beacon_core/lib/src/beacons/stream.dart index 072b14c3..29c7be2d 100644 --- a/packages/state_beacon_core/lib/src/beacons/stream.dart +++ b/packages/state_beacon_core/lib/src/beacons/stream.dart @@ -27,4 +27,22 @@ class StreamBeacon extends AsyncBeacon { 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 toFuture() { + if (_completer == null) { + // first time + final completer = Completer(); + _completer = Beacon.writable(completer, name: "$name's future"); + + if (peek() case final AsyncData data) { + completer.complete(data.value); + } else if (peek() case final AsyncError error) { + completer.completeError(error.error, error.stackTrace); + } + } + + return _completer!.value.future; + } } diff --git a/packages/state_beacon_core/lib/src/mixins/autosleep.dart b/packages/state_beacon_core/lib/src/mixins/autosleep.dart index d33c8130..00e6f1c5 100644 --- a/packages/state_beacon_core/lib/src/mixins/autosleep.dart +++ b/packages/state_beacon_core/lib/src/mixins/autosleep.dart @@ -51,9 +51,16 @@ mixin _AutoSleep on ReadableBeacon { @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(); } } diff --git a/packages/state_beacon_core/test/src/beacons/future_test.dart b/packages/state_beacon_core/test/src/beacons/future_test.dart index 0d926b3d..8d78fa18 100644 --- a/packages/state_beacon_core/test/src/beacons/future_test.dart +++ b/packages/state_beacon_core/test/src/beacons/future_test.dart @@ -70,8 +70,6 @@ void main() { futureBeacon.reset(); - BeaconScheduler.flush(); - expect(futureBeacon.isLoading, true); await delay(); @@ -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); + }); } diff --git a/packages/state_beacon_core/test/src/beacons/stream_test.dart b/packages/state_beacon_core/test/src/beacons/stream_test.dart index b01f8b6b..56a0cc7d 100644 --- a/packages/state_beacon_core/test/src/beacons/stream_test.dart +++ b/packages/state_beacon_core/test/src/beacons/stream_test.dart @@ -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); + }); } diff --git a/run.sh b/run.sh index 018a4cec..c3b2f24e 100755 --- a/run.sh +++ b/run.sh @@ -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 &&