diff --git a/Readme.md b/Readme.md index b8b4c811..c22b222f 100644 --- a/Readme.md +++ b/Readme.md @@ -230,6 +230,12 @@ expect(callCount, equals(1)); // There were 4 updates, but only 1 notification Creates a `DerivedBeacon` whose value is derived from a computation function. This beacon will recompute its value every time one of it's dependencies change. +If `shouldSleep` is `true`(default), the callback will not execute if the beacon is no longer being watched. +It will resume executing once a listener is added or it's value is accessed. + +If `supportConditional` is `true`(default), it will only look dependencies on its first run. +This means once a beacon is added as a dependency, it will not be removed even if it's no longer used and no new dependencies will be added. This can be used a performance optimization. + Example: ```dart @@ -249,11 +255,15 @@ Creates a `DerivedBeacon` whose value is derived from an asynchronous computatio This beacon will recompute its value every time one of its dependencies change. The result is wrapped in an `AsyncValue`, which can be in one of four states: `idle`, `loading`, `data`, or `error`. -If `manualStart` is `true` (default: false), the beacon will be in the `idle` state and the future will not execute until [start()] is called. +If `manualStart` is `true` (default: false), the beacon will be in the `idle` state and the future will not execute until `start()` is called. Calling `start()` on a beacon that's already started will have no effect. If `cancelRunning` is `true` (default: true), the results of a current execution will be discarded if another execution is triggered before the current one finishes. +If `shouldSleep` is `true`(default), the callback will not execute if the beacon is no longer being watched. +It will resume executing once a listener is added or it's value is accessed. +This means that it will enter the `loading` state when woken up. + Example: ```dart diff --git a/packages/state_beacon/README.md b/packages/state_beacon/README.md index b8b4c811..c22b222f 100644 --- a/packages/state_beacon/README.md +++ b/packages/state_beacon/README.md @@ -230,6 +230,12 @@ expect(callCount, equals(1)); // There were 4 updates, but only 1 notification Creates a `DerivedBeacon` whose value is derived from a computation function. This beacon will recompute its value every time one of it's dependencies change. +If `shouldSleep` is `true`(default), the callback will not execute if the beacon is no longer being watched. +It will resume executing once a listener is added or it's value is accessed. + +If `supportConditional` is `true`(default), it will only look dependencies on its first run. +This means once a beacon is added as a dependency, it will not be removed even if it's no longer used and no new dependencies will be added. This can be used a performance optimization. + Example: ```dart @@ -249,11 +255,15 @@ Creates a `DerivedBeacon` whose value is derived from an asynchronous computatio This beacon will recompute its value every time one of its dependencies change. The result is wrapped in an `AsyncValue`, which can be in one of four states: `idle`, `loading`, `data`, or `error`. -If `manualStart` is `true` (default: false), the beacon will be in the `idle` state and the future will not execute until [start()] is called. +If `manualStart` is `true` (default: false), the beacon will be in the `idle` state and the future will not execute until `start()` is called. Calling `start()` on a beacon that's already started will have no effect. If `cancelRunning` is `true` (default: true), the results of a current execution will be discarded if another execution is triggered before the current one finishes. +If `shouldSleep` is `true`(default), the callback will not execute if the beacon is no longer being watched. +It will resume executing once a listener is added or it's value is accessed. +This means that it will enter the `loading` state when woken up. + Example: ```dart diff --git a/packages/state_beacon_core/README.md b/packages/state_beacon_core/README.md index 71ecaab6..98c95d97 100644 --- a/packages/state_beacon_core/README.md +++ b/packages/state_beacon_core/README.md @@ -211,6 +211,12 @@ expect(callCount, equals(1)); // There were 4 updates, but only 1 notification Creates a `DerivedBeacon` whose value is derived from a computation function. This beacon will recompute its value every time one of it's dependencies change. +If `shouldSleep` is `true`(default), the callback will not execute if the beacon is no longer being watched. +It will resume executing once a listener is added or it's value is accessed. + +If `supportConditional` is `true`(default), it will only look dependencies on its first run. +This means once a beacon is added as a dependency, it will not be removed even if it's no longer used and no new dependencies will be added. This can be used a performance optimization. + Example: ```dart @@ -230,11 +236,15 @@ Creates a `DerivedBeacon` whose value is derived from an asynchronous computatio This beacon will recompute its value every time one of its dependencies change. The result is wrapped in an `AsyncValue`, which can be in one of four states: `idle`, `loading`, `data`, or `error`. -If `manualStart` is `true` (default: false), the beacon will be in the `idle` state and the future will not execute until [start()] is called. +If `manualStart` is `true` (default: false), the beacon will be in the `idle` state and the future will not execute until `start()` is called. Calling `start()` on a beacon that's already started will have no effect. If `cancelRunning` is `true` (default: true), the results of a current execution will be discarded if another execution is triggered before the current one finishes. +If `shouldSleep` is `true`(default), the callback will not execute if the beacon is no longer being watched. +It will resume executing once a listener is added or it's value is accessed. +This means that it will enter the `loading` state when woken up. + Example: ```dart diff --git a/packages/state_beacon_core/lib/src/beacons/derived.dart b/packages/state_beacon_core/lib/src/beacons/derived.dart index e6d7c9cc..e21ed43a 100644 --- a/packages/state_beacon_core/lib/src/beacons/derived.dart +++ b/packages/state_beacon_core/lib/src/beacons/derived.dart @@ -1,4 +1,4 @@ -// ignore_for_file: public_member_api_docs +// ignore_for_file: public_member_api_docs, use_setters_to_change_properties part of '../base_beacon.dart'; @@ -6,12 +6,16 @@ enum DerivedStatus { idle, running } mixin DerivedMixin on ReadableBeacon { late VoidCallback _unsubscribe; + late VoidCallback _restarter; - // ignore: use_setters_to_change_properties void $setInternalEffectUnsubscriber(VoidCallback unsubscribe) { _unsubscribe = unsubscribe; } + void $setInternalEffectRestarter(VoidCallback restarter) { + _restarter = restarter; + } + @override void dispose() { _unsubscribe(); @@ -21,5 +25,35 @@ mixin DerivedMixin on ReadableBeacon { // this is only used internally class WritableDerivedBeacon extends WritableBeacon with DerivedMixin { - WritableDerivedBeacon({super.name}); + WritableDerivedBeacon({ + super.name, + bool shouldSleep = true, + }) { + if (!shouldSleep) return; + + _listeners.whenEmpty(() { + _unsubscribe(); + _sleeping = true; + }); + } + + var _sleeping = false; + + @override + T get value { + if (_sleeping) { + _restarter(); + _sleeping = false; + } + return super.value; + } + + @override + T peek() { + if (_sleeping) { + _restarter(); + _sleeping = false; + } + return super.peek(); + } } diff --git a/packages/state_beacon_core/lib/src/beacons/derived_future.dart b/packages/state_beacon_core/lib/src/beacons/derived_future.dart index 0b75102d..5e10036d 100644 --- a/packages/state_beacon_core/lib/src/beacons/derived_future.dart +++ b/packages/state_beacon_core/lib/src/beacons/derived_future.dart @@ -21,6 +21,7 @@ class DerivedFutureBeacon extends FutureBeacon bool manualStart = false, super.cancelRunning = true, super.name, + bool shouldSleep = true, }) { if (manualStart) { _status.set(DerivedFutureStatus.idle); @@ -29,12 +30,43 @@ class DerivedFutureBeacon extends FutureBeacon _status.set(DerivedFutureStatus.running); _setValue(AsyncLoading()); } + + if (!shouldSleep) return; + + _listeners.whenEmpty(() { + // setting status to idle will dispose the internal effect + // and stop listening to dependencies + _status.set(DerivedFutureStatus.idle); + _sleeping = true; + }); + } + + var _sleeping = false; + + @override + AsyncValue get value { + if (_sleeping) { + start(); + _sleeping = false; + } + return super.value; + } + + @override + AsyncValue peek() { + if (_sleeping) { + start(); + _sleeping = false; + } + return super.peek(); } /// Runs the future. Future run() => _run(); - final _status = Beacon.lazyWritable(); + late final _status = Beacon.lazyWritable( + name: "$name's status", + ); /// The status of the future. ReadableBeacon get status => _status; diff --git a/packages/state_beacon_core/lib/src/beacons/future.dart b/packages/state_beacon_core/lib/src/beacons/future.dart index 3ed11c32..72f8a97f 100644 --- a/packages/state_beacon_core/lib/src/beacons/future.dart +++ b/packages/state_beacon_core/lib/src/beacons/future.dart @@ -20,28 +20,35 @@ abstract class FutureBeacon extends AsyncBeacon { /// Alias for peek().lastData. /// Returns the last data that was successfully loaded - T? get lastData => _value.lastData; + /// equivalent to `beacon.peek().lastData` + T? get lastData => peek().lastData; FutureCallback _operation; /// Casts its value to [AsyncData] and return /// it's value or throws `CastError` if this is not [AsyncData]. - T unwrapValue() => _value.unwrap(); + /// equivalent to `beacon.peek().unwrap()` + T unwrapValue() => peek().unwrap(); /// Returns `true` if this is [AsyncLoading]. - bool get isLoading => _value.isLoading; + /// This is equivalent to `beacon.peek().isLoading`. + bool get isLoading => peek().isLoading; /// Returns `true` if this is [AsyncIdle]. - bool get isIdle => _value.isIdle; + /// This is equivalent to `beacon.peek().isIdle`. + bool get isIdle => peek().isIdle; /// Returns `true` if this is [AsyncIdle] or [AsyncLoading]. - bool get isIdleOrLoading => _value.isIdleOrLoading; + /// This is equivalent to `beacon.peek().isIdleOrLoading`. + bool get isIdleOrLoading => peek().isIdleOrLoading; /// Returns `true` if this is [AsyncData]. - bool get isData => _value.isData; + /// This is equivalent to `beacon.peek().isData`. + bool get isData => peek().isData; /// Returns `true` if this is [AsyncError]. - bool get isError => _value.isError; + /// This is equivalent to `beacon.peek().isError`. + bool get isError => peek().isError; /// Starts executing an idle [Future] /// diff --git a/packages/state_beacon_core/lib/src/creator/beacon_creator.dart b/packages/state_beacon_core/lib/src/creator/beacon_creator.dart index f553db75..abef21c6 100644 --- a/packages/state_beacon_core/lib/src/creator/beacon_creator.dart +++ b/packages/state_beacon_core/lib/src/creator/beacon_creator.dart @@ -418,11 +418,11 @@ class _BeaconCreator { /// Creates a `DerivedBeacon` whose value is derived from a computation function. /// This beacon will recompute its value every time one of it's dependencies change. /// - /// If `manualStart` is `true`, the callback will not execute until [start()] is called. + /// If `shouldSleep` is `true`(default), the callback will not execute if the beacon is no longer being watched. + /// It will resume executing once a listener is added or it's value is accessed. /// - /// If `supportConditional` is `true`, the effect look for its dependencies on its first run. + /// If `supportConditional` is `true`(default), the effect look for its dependencies on its first run. /// This means once a beacon is added as a dependency, it will not be removed even if it's no longer used. - /// Defaults to `true`. /// /// Example: /// ```dart @@ -438,21 +438,29 @@ class _BeaconCreator { ReadableBeacon derived( T Function() compute, { String? name, + bool shouldSleep = true, bool supportConditional = true, }) { final beacon = WritableDerivedBeacon( name: name ?? 'DerivedBeacon<$T>', + shouldSleep: shouldSleep, ); - final unsub = doEffect( - () { - beacon.set(compute()); - }, - supportConditional: supportConditional, - name: name ?? 'DerivedBeacon<$T>', - ); + void start() { + final unsub = doEffect( + () { + beacon.set(compute()); + }, + supportConditional: supportConditional, + name: name ?? 'DerivedBeacon<$T>', + ); + + beacon.$setInternalEffectUnsubscriber(unsub); + } - beacon.$setInternalEffectUnsubscriber(unsub); + beacon.$setInternalEffectRestarter(start); + + start(); return beacon; } @@ -461,14 +469,15 @@ class _BeaconCreator { /// This beacon will recompute its value every time one of its dependencies change. /// The result is wrapped in an `AsyncValue`, which can be in one of three states: loading, data, or error. /// - /// If `manualStart` is `true`, the future will not execute until [start()] is called. + /// If `manualStart` is `true`(default:false), the future will not execute until [start()] is called. /// - /// If `cancelRunning` is `true`, the results of a current execution will be discarded + /// If `cancelRunning` is `true`(default), the results of a current execution will be discarded /// if another execution is triggered before the current one finishes. /// - /// If `supportConditional` is `true`, the effect look for its dependencies on its first run. - /// This means once a beacon is added as a dependency, it will not be removed even if it's no longer used. - /// Defaults to `true`. + /// If `shouldSleep` is `true`(default), the callback will not execute if the beacon is no longer being watched. + /// It will resume executing once a listener is added or it's value is accessed. + /// This means that it will enter the `loading` state when woken up. + /// /// /// Example: /// ```dart @@ -498,28 +507,28 @@ class _BeaconCreator { FutureCallback compute, { bool manualStart = false, bool cancelRunning = true, + bool shouldSleep = true, String? name, - bool supportConditional = true, }) { final beacon = DerivedFutureBeacon( compute, manualStart: manualStart, cancelRunning: cancelRunning, + shouldSleep: shouldSleep, name: name ?? 'DerivedFutureBeacon<$T>', ); - final dispose = doEffect(() { - // beacon is manually triggered if in idle state - if (beacon.status() == DerivedFutureStatus.idle) return null; + final dispose = doEffect( + () async { + // beacon is manually triggered if in idle state + if (beacon.status() == DerivedFutureStatus.idle) { + return; + } - return doEffect( - () async { - await beacon.run(); - }, - supportConditional: supportConditional, - name: name ?? 'DerivedFutureBeacon<$T>', - ); - }); + await beacon.run(); + }, + name: name ?? 'DerivedFutureBeacon<$T>', + ); beacon.$setInternalEffectUnsubscriber(dispose); diff --git a/packages/state_beacon_core/lib/src/creator/beacon_group_creator.dart b/packages/state_beacon_core/lib/src/creator/beacon_group_creator.dart index 496ec774..f49a7a54 100644 --- a/packages/state_beacon_core/lib/src/creator/beacon_group_creator.dart +++ b/packages/state_beacon_core/lib/src/creator/beacon_group_creator.dart @@ -68,11 +68,13 @@ class BeaconGroup extends _BeaconCreator { ReadableBeacon derived( T Function() compute, { String? name, + bool shouldSleep = true, bool supportConditional = true, }) { final beacon = super.derived( compute, name: name, + shouldSleep: shouldSleep, supportConditional: supportConditional, ); _beacons.add(beacon); @@ -84,15 +86,15 @@ class BeaconGroup extends _BeaconCreator { FutureCallback compute, { bool manualStart = false, bool cancelRunning = true, + bool shouldSleep = true, String? name, - bool supportConditional = true, }) { final beacon = super.derivedFuture( compute, manualStart: manualStart, cancelRunning: cancelRunning, + shouldSleep: shouldSleep, name: name, - supportConditional: supportConditional, ); _beacons.add(beacon); return beacon; diff --git a/packages/state_beacon_core/lib/src/effect.dart b/packages/state_beacon_core/lib/src/effect.dart index 89b6d425..2598e746 100644 --- a/packages/state_beacon_core/lib/src/effect.dart +++ b/packages/state_beacon_core/lib/src/effect.dart @@ -36,6 +36,7 @@ class _Effect { _parentEffect = _currentEffect; _currentEffect = this; try { + _currentDeps.clear(); cleanUpAndRun(); } finally { _currentEffect = _parentEffect; diff --git a/packages/state_beacon_core/lib/src/listeners.dart b/packages/state_beacon_core/lib/src/listeners.dart index 06519679..dde0a222 100644 --- a/packages/state_beacon_core/lib/src/listeners.dart +++ b/packages/state_beacon_core/lib/src/listeners.dart @@ -2,12 +2,16 @@ import 'dart:collection'; +import 'package:state_beacon_core/src/common.dart'; + import 'effect_closure.dart'; class Listeners { final HashSet _set = HashSet(); List _list = []; + late final _whenEmptyCallbacks = []; + int get length => _set.length; bool add(EffectClosure item) { @@ -28,13 +32,17 @@ class Listeners { if (removed) { // prevent concurrent modification _list = _list.toList()..remove(item); + + if (_set.isEmpty) { + for (final callback in _whenEmptyCallbacks) { + callback(); + } + } } return removed; } - bool contains(EffectClosure item) { - return _set.contains(item); - } + bool contains(EffectClosure item) => _set.contains(item); List get items => _list; HashSet get itemsSet => _set; @@ -44,6 +52,10 @@ class Listeners { _list.clear(); } + void whenEmpty(VoidCallback callback) { + _whenEmptyCallbacks.add(callback); + } + // coverage:ignore-start @override String toString() { diff --git a/packages/state_beacon_core/test/src/beacons/derived_future_test.dart b/packages/state_beacon_core/test/src/beacons/derived_future_test.dart index 6e08a5d0..efbdffcf 100644 --- a/packages/state_beacon_core/test/src/beacons/derived_future_test.dart +++ b/packages/state_beacon_core/test/src/beacons/derived_future_test.dart @@ -113,7 +113,7 @@ void main() { expect(fullName.value.unwrap(), 'Sally 1 Smith 2'); }); - test('should return error when dependency throws error', () async { + test('should return error when callback throws error', () async { final count = Beacon.writable(0); final firstName = Beacon.derivedFuture(() async { @@ -300,26 +300,223 @@ void main() { expect(stats.isError, isTrue); }); - test('should not watch new beacon conditionally', () async { + test('should stop watching dependencies when it has no more watchers', + () async { + // BeaconObserver.instance = LoggingObserver(); + final num1 = Beacon.writable(10, name: 'num1'); + final num2 = Beacon.writable(20, name: 'num2'); + + final derivedBeacon = Beacon.derivedFuture( + () async => num1.value + num2.value, + name: 'derived', + ); + + final status = (derivedBeacon as DerivedFutureBeacon).status; + + expect(num1.listenersCount, 1); + expect(num2.listenersCount, 1); + expect(derivedBeacon.listenersCount, 0); + + final unsub = Beacon.effect( + () => derivedBeacon.value, + name: 'custom effect', + ); + + expect(derivedBeacon.listenersCount, 1); + + expect( + status.value, + DerivedFutureStatus.running, + ); + + unsub(); + + expect(derivedBeacon.listenersCount, 0); + expect(num1.listenersCount, 0); + expect(num2.listenersCount, 0); + + // should start listening again when value is accessed + num1.value = 15; + + expect(status.value, DerivedFutureStatus.idle); + + expect(derivedBeacon.isLoading, true); + + await Future.delayed(k1ms); + + expect(derivedBeacon.unwrapValue(), 35); + + expect(derivedBeacon.listenersCount, 0); + expect(num1.listenersCount, 1); + expect(num2.listenersCount, 1); + + // should stop listening again when it has no more listeners + + final unsub2 = Beacon.effect(() => derivedBeacon.value); + + expect(derivedBeacon.listenersCount, 1); + + expect(status.value, DerivedFutureStatus.running); + + unsub2(); + + expect(status.value, DerivedFutureStatus.idle); + + expect(derivedBeacon.listenersCount, 0); + expect(num1.listenersCount, 0); + expect(num2.listenersCount, 0); + + // should start listening again when value is accessed + num1.value = 20; + + expect(derivedBeacon.value.isLoading, true); + + await Future.delayed(k1ms); + + expect(derivedBeacon.peek().unwrap(), 40); + + expect(derivedBeacon.listenersCount, 0); + expect(num1.listenersCount, 1); + expect(num2.listenersCount, 1); + }); + + test('should not run when it has no more watchers', () async { + final num1 = Beacon.writable(10); + final num2 = Beacon.writable(20); + var ran = 0; + + final derivedBeacon = Beacon.derivedFuture(() async { + ran++; + return num1.value + num2.value; + }); + + expect(ran, 1); + + final unsub = Beacon.effect(() => derivedBeacon.value); + + expect(ran, 1); + + num1.increment(); + + await Future.delayed(k1ms); + + expect(ran, 2); + + unsub(); + + // derived should not execute when it has no more watchers + num1.increment(); + num2.increment(); + + expect(ran, 2); + + expect(derivedBeacon.isLoading, true); + + expect(ran, 3); + + num1.increment(); + + expect(ran, 4); + + await Future.delayed(k1ms); + + expect(derivedBeacon.unwrapValue(), 34); + + expect(ran, 4); + }); + + test('should run when it has no more watchers when shouldSleep=false', + () async { final num1 = Beacon.writable(10); final num2 = Beacon.writable(20); + var ran = 0; final derivedBeacon = Beacon.derivedFuture( () async { - if (num2().isEven) return num2(); + ran++; return num1.value + num2.value; }, - supportConditional: false, - manualStart: true, + shouldSleep: false, ); - expect(derivedBeacon(), isA()); + expect(ran, 1); + + final unsub = Beacon.effect(() => derivedBeacon.value); + + expect(ran, 1); + + num1.increment(); - derivedBeacon.start(); + await Future.delayed(k1ms); + + expect(ran, 2); + + unsub(); + + // derived should not execute when it has no more watchers + num1.increment(); + num2.increment(); + + expect(ran, 4); expect(derivedBeacon.isLoading, true); - await Future.delayed(k10ms); + expect(ran, 4); + + num1.increment(); + + expect(ran, 5); + + await Future.delayed(k1ms); + + expect(derivedBeacon.unwrapValue(), 34); + + expect(ran, 5); + }); + + test('should conditionally stop listening to dependencies', () async { + final num1 = Beacon.writable(10); + final num2 = Beacon.writable(10); + final num3 = Beacon.writable(10); + final guard = Beacon.writable(true); + + final derivedBeacon = Beacon.derivedFuture(() async { + if (guard.value) return num1.value; + + return num2.value + num3.value; + }); + + expect(num1.listenersCount, 1); + expect(num2.listenersCount, 0); + expect(num3.listenersCount, 0); + + expect(derivedBeacon.isLoading, true); + + await Future.delayed(k1ms); + + expect(derivedBeacon.unwrapValue(), 10); + + num1.increment(); + + expect(derivedBeacon.isLoading, true); + + await Future.delayed(k1ms); + + expect(derivedBeacon.unwrapValue(), 11); + + guard.value = false; + + expect(num1.listenersCount, 0); + expect(num2.listenersCount, 1); + expect(num3.listenersCount, 1); + + expect(derivedBeacon.isLoading, true); + + await Future.delayed(k1ms); + + expect(derivedBeacon.unwrapValue(), 20); + + num1.increment(); expect(derivedBeacon.unwrapValue(), 20); @@ -327,15 +524,20 @@ void main() { expect(derivedBeacon.isLoading, true); - await Future.delayed(k10ms); + await Future.delayed(k1ms); - expect(derivedBeacon.unwrapValue(), 31); + expect(derivedBeacon.unwrapValue(), 21); - // should not trigger recompute as it wasn't accessed on first run - num1.value = 15; + guard.value = true; + + expect(num1.listenersCount, 1); + expect(num2.listenersCount, 0); + expect(num3.listenersCount, 0); - expect(derivedBeacon.isLoading, false); + expect(derivedBeacon.isLoading, true); + + await Future.delayed(k1ms); - expect(derivedBeacon.unwrapValue(), 31); + expect(derivedBeacon.unwrapValue(), 12); }); } diff --git a/packages/state_beacon_core/test/src/beacons/derived_test.dart b/packages/state_beacon_core/test/src/beacons/derived_test.dart index c3d8ce5d..4500f387 100644 --- a/packages/state_beacon_core/test/src/beacons/derived_test.dart +++ b/packages/state_beacon_core/test/src/beacons/derived_test.dart @@ -89,4 +89,138 @@ void main() { expect(derivedBeacon.value, 20); }); + + test('should stop watching dependencies when it has no more watchers', () { + final num1 = Beacon.writable(10); + final num2 = Beacon.writable(20); + + final derivedBeacon = Beacon.derived(() => num1.value + num2.value); + + expect(num1.listenersCount, 1); + expect(num2.listenersCount, 1); + expect(derivedBeacon.listenersCount, 0); + + final unsub = Beacon.effect(() => derivedBeacon.value); + + expect(derivedBeacon.listenersCount, 1); + + unsub(); + + expect(derivedBeacon.listenersCount, 0); + expect(num1.listenersCount, 0); + expect(num2.listenersCount, 0); + + // should start listening again when value is accessed + num1.value = 15; + + expect(derivedBeacon.value, 35); + + expect(derivedBeacon.listenersCount, 0); + expect(num1.listenersCount, 1); + expect(num2.listenersCount, 1); + + // should stop listening again when it has no more listeners + + final unsub2 = Beacon.effect(() => derivedBeacon.value); + + expect(derivedBeacon.listenersCount, 1); + + unsub2(); + + expect(derivedBeacon.listenersCount, 0); + expect(num1.listenersCount, 0); + expect(num2.listenersCount, 0); + + // should start listening again when value is accessed + num1.value = 20; + + expect(derivedBeacon.peek(), 40); + + expect(derivedBeacon.listenersCount, 0); + expect(num1.listenersCount, 1); + expect(num2.listenersCount, 1); + }); + + test('should not run when it has no more watchers', () { + final num1 = Beacon.writable(10); + final num2 = Beacon.writable(20); + var ran = 0; + + final derivedBeacon = Beacon.derived(() { + ran++; + return num1.value + num2.value; + }); + + expect(ran, 1); + + final unsub = Beacon.effect(() => derivedBeacon.value); + + expect(ran, 1); + + num1.increment(); + + expect(ran, 2); + + unsub(); + + // derived should not execute when it has no more watchers + num1.increment(); + num2.increment(); + + expect(ran, 2); + + expect(derivedBeacon(), 33); + + expect(ran, 3); + + num1.increment(); + + expect(ran, 4); + + expect(derivedBeacon(), 34); + + expect(ran, 4); + }); + test('should run when it has no more watchers when shouldSleep=false', () { + final num1 = Beacon.writable(10); + final num2 = Beacon.writable(20); + var ran = 0; + + final derivedBeacon = Beacon.derived( + () { + ran++; + return num1.value + num2.value; + }, + shouldSleep: false, + ); + + expect(ran, 1); + + final unsub = Beacon.effect(() => derivedBeacon.value); + + expect(ran, 1); + + num1.increment(); + + expect(ran, 2); + + unsub(); + + num1.increment(); + num2.increment(); + + expect(ran, 4); + + expect(derivedBeacon(), 33); + + expect(ran, 4); + + num1.increment(); + + expect(ran, 5); + + expect(derivedBeacon(), 34); + + expect(ran, 5); + }); } diff --git a/packages/state_beacon_core/test/src/effect_test.dart b/packages/state_beacon_core/test/src/effect_test.dart index 03fe1b9f..fdd00246 100644 --- a/packages/state_beacon_core/test/src/effect_test.dart +++ b/packages/state_beacon_core/test/src/effect_test.dart @@ -12,15 +12,18 @@ void main() { final buff = Beacon.bufferedTime(duration: k10ms); - Beacon.effect(() { - var msg = '${name()} is ${age()} years old'; + Beacon.effect( + () { + var msg = '${name()} is ${age()} years old'; - if (age.value > 21) { - msg += ' and can go to ${college.value}'; - } + if (age.value > 21) { + msg += ' and can go to ${college.value}'; + } - buff.add(msg); - }); + buff.add(msg); + }, + name: 'effect', + ); name.value = 'Alice'; age.value = 21; @@ -228,20 +231,26 @@ void main() { var effectCalled2 = 0; var effectCalled3 = 0; - final dispose = Beacon.effect(() { - effectCalled++; - beacon1.value; - - return Beacon.effect(() { - effectCalled2++; - beacon2.value; - - return Beacon.effect(() { - effectCalled3++; - beacon3.value; - }); - }, supportConditional: false,); - }, supportConditional: false,); + final dispose = Beacon.effect( + () { + effectCalled++; + beacon1.value; + + return Beacon.effect( + () { + effectCalled2++; + beacon2.value; + + return Beacon.effect(() { + effectCalled3++; + beacon3.value; + }); + }, + supportConditional: false, + ); + }, + supportConditional: false, + ); beacon1.value = 15; expect(effectCalled, 2); diff --git a/packages/state_beacon_core/test/src/wrapping_test.dart b/packages/state_beacon_core/test/src/wrapping_test.dart index f919a79f..f97bf50f 100644 --- a/packages/state_beacon_core/test/src/wrapping_test.dart +++ b/packages/state_beacon_core/test/src/wrapping_test.dart @@ -29,10 +29,10 @@ void main() { expect(doubledCount.listenersCount, 1); expect(count.listenersCount, 2); - wrapper.clearWrapped(); + wrapper.dispose(); expect(doubledCount.listenersCount, 0); - expect(count.listenersCount, 1); + expect(count.listenersCount, 0); }); test('should remove subscription for all wrapped beacons', () { @@ -53,7 +53,7 @@ void main() { wrapper.clearWrapped(); expect(doubledCount.listenersCount, 0); - expect(count.listenersCount, 1); + expect(count.listenersCount, 0); }); test('should dispose internal currentBuffer on dispose', () {