From 1915998648280ee0fd51a3b325a1d0c2bfa18d64 Mon Sep 17 00:00:00 2001 From: jinyus Date: Fri, 26 Jan 2024 20:38:48 -0500 Subject: [PATCH 01/11] Add whenEmpty method to Listeners class --- .../state_beacon_core/lib/src/listeners.dart | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) 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() { From 6d140208f1a76b2345cf6c530a7d3bd85fe3ebcb Mon Sep 17 00:00:00 2001 From: jinyus Date: Fri, 26 Jan 2024 20:40:21 -0500 Subject: [PATCH 02/11] make derived beacons sleep when they are not longer being watched --- .../lib/src/beacons/derived.dart | 35 ++++- .../lib/src/beacons/derived_future.dart | 29 +++- .../lib/src/beacons/future.dart | 21 ++- .../lib/src/creator/beacon_creator.dart | 50 ++++--- .../test/src/beacons/derived_future_test.dart | 124 ++++++++++++++++++ .../test/src/beacons/derived_test.dart | 92 +++++++++++++ .../test/src/wrapping_test.dart | 6 +- 7 files changed, 323 insertions(+), 34 deletions(-) diff --git a/packages/state_beacon_core/lib/src/beacons/derived.dart b/packages/state_beacon_core/lib/src/beacons/derived.dart index e6d7c9cc..1df8135f 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,30 @@ mixin DerivedMixin on ReadableBeacon { // this is only used internally class WritableDerivedBeacon extends WritableBeacon with DerivedMixin { - WritableDerivedBeacon({super.name}); + WritableDerivedBeacon({super.name}) { + _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..975d4770 100644 --- a/packages/state_beacon_core/lib/src/beacons/derived_future.dart +++ b/packages/state_beacon_core/lib/src/beacons/derived_future.dart @@ -29,12 +29,39 @@ class DerivedFutureBeacon extends FutureBeacon _status.set(DerivedFutureStatus.running); _setValue(AsyncLoading()); } + + _listeners.whenEmpty(() { + _status.set(DerivedFutureStatus.idle); + _sleeping = true; + }); + } + + var _sleeping = false; + + @override + AsyncValue get value { + if (_sleeping && _status.peek() == DerivedFutureStatus.idle) { + start(); + _sleeping = false; + } + return super.value; + } + + @override + AsyncValue peek() { + if (_sleeping && _status.peek() == DerivedFutureStatus.idle) { + 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..2f98a333 100644 --- a/packages/state_beacon_core/lib/src/creator/beacon_creator.dart +++ b/packages/state_beacon_core/lib/src/creator/beacon_creator.dart @@ -444,15 +444,21 @@ class _BeaconCreator { name: name ?? 'DerivedBeacon<$T>', ); - 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; } @@ -508,18 +514,22 @@ class _BeaconCreator { name: name ?? 'DerivedFutureBeacon<$T>', ); - final dispose = doEffect(() { - // beacon is manually triggered if in idle state - if (beacon.status() == DerivedFutureStatus.idle) return null; - - return doEffect( - () async { - await beacon.run(); - }, - supportConditional: supportConditional, - name: name ?? 'DerivedFutureBeacon<$T>', - ); - }); + final dispose = doEffect( + () { + // beacon is manually triggered if in idle state + if (beacon.status() == DerivedFutureStatus.idle) { + return null; + } + + return doEffect( + () async { + await beacon.run(); + }, + supportConditional: supportConditional, + name: name ?? 'DerivedFutureBeacon<$T>', + ); + }, + ); beacon.$setInternalEffectUnsubscriber(dispose); 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..55e6e15b 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 @@ -338,4 +338,128 @@ void main() { expect(derivedBeacon.unwrapValue(), 31); }); + + test('should stop watching dependencies when it has no more watchers', + () async { + 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); + }); } 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..20b41f68 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,96 @@ 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); + }); } 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', () { From ddbbcc0861f5e7beaeafaebb8ac6d5e3817a053b Mon Sep 17 00:00:00 2001 From: jinyus Date: Fri, 26 Jan 2024 20:47:21 -0500 Subject: [PATCH 03/11] rm unnecessary if check --- .../state_beacon_core/lib/src/beacons/derived_future.dart | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) 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 975d4770..ee6e1578 100644 --- a/packages/state_beacon_core/lib/src/beacons/derived_future.dart +++ b/packages/state_beacon_core/lib/src/beacons/derived_future.dart @@ -31,6 +31,8 @@ class DerivedFutureBeacon extends FutureBeacon } _listeners.whenEmpty(() { + // setting status to idle will dispose the internal effect + // and stop listening to dependencies _status.set(DerivedFutureStatus.idle); _sleeping = true; }); @@ -40,7 +42,7 @@ class DerivedFutureBeacon extends FutureBeacon @override AsyncValue get value { - if (_sleeping && _status.peek() == DerivedFutureStatus.idle) { + if (_sleeping) { start(); _sleeping = false; } @@ -49,7 +51,7 @@ class DerivedFutureBeacon extends FutureBeacon @override AsyncValue peek() { - if (_sleeping && _status.peek() == DerivedFutureStatus.idle) { + if (_sleeping) { start(); _sleeping = false; } From b3a216579a2e58d1370388fbbfac5cd8a8579829 Mon Sep 17 00:00:00 2001 From: jinyus Date: Fri, 26 Jan 2024 21:20:46 -0500 Subject: [PATCH 04/11] Clear current dependencies before running effect --- packages/state_beacon_core/lib/src/effect.dart | 1 + 1 file changed, 1 insertion(+) 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; From 4f13fcfb04581dda0f8f26c8ab1f83e7af96be2b Mon Sep 17 00:00:00 2001 From: jinyus Date: Fri, 26 Jan 2024 21:21:14 -0500 Subject: [PATCH 05/11] Refactor effect_test.dart --- .../test/src/effect_test.dart | 51 +++++++++++-------- 1 file changed, 30 insertions(+), 21 deletions(-) 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); From 5997fac14f0caff04f6bee20c6bcb9ab92fbae85 Mon Sep 17 00:00:00 2001 From: jinyus Date: Fri, 26 Jan 2024 21:22:41 -0500 Subject: [PATCH 06/11] rm conditional option for derivedFuture and rm test for it. --- .../lib/src/creator/beacon_creator.dart | 14 ++----- .../lib/src/creator/beacon_group_creator.dart | 2 - .../test/src/beacons/derived_future_test.dart | 40 +------------------ 3 files changed, 5 insertions(+), 51 deletions(-) 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 2f98a333..c3eabcbf 100644 --- a/packages/state_beacon_core/lib/src/creator/beacon_creator.dart +++ b/packages/state_beacon_core/lib/src/creator/beacon_creator.dart @@ -505,7 +505,6 @@ class _BeaconCreator { bool manualStart = false, bool cancelRunning = true, String? name, - bool supportConditional = true, }) { final beacon = DerivedFutureBeacon( compute, @@ -515,20 +514,15 @@ class _BeaconCreator { ); final dispose = doEffect( - () { + () async { // beacon is manually triggered if in idle state if (beacon.status() == DerivedFutureStatus.idle) { - return null; + 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..38fc95a2 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 @@ -85,14 +85,12 @@ class BeaconGroup extends _BeaconCreator { bool manualStart = false, bool cancelRunning = true, String? name, - bool supportConditional = true, }) { final beacon = super.derivedFuture( compute, manualStart: manualStart, cancelRunning: cancelRunning, name: name, - supportConditional: supportConditional, ); _beacons.add(beacon); return beacon; 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 55e6e15b..37af3fdd 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 @@ -300,47 +300,9 @@ void main() { expect(stats.isError, isTrue); }); - test('should not watch new beacon conditionally', () async { - final num1 = Beacon.writable(10); - final num2 = Beacon.writable(20); - - final derivedBeacon = Beacon.derivedFuture( - () async { - if (num2().isEven) return num2(); - return num1.value + num2.value; - }, - supportConditional: false, - manualStart: true, - ); - - expect(derivedBeacon(), isA()); - - derivedBeacon.start(); - - expect(derivedBeacon.isLoading, true); - - await Future.delayed(k10ms); - - expect(derivedBeacon.unwrapValue(), 20); - - num2.increment(); - - expect(derivedBeacon.isLoading, true); - - await Future.delayed(k10ms); - - expect(derivedBeacon.unwrapValue(), 31); - - // should not trigger recompute as it wasn't accessed on first run - num1.value = 15; - - expect(derivedBeacon.isLoading, false); - - expect(derivedBeacon.unwrapValue(), 31); - }); - 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'); From 886b97eb8d20f0db9aeef84874280f88c71776c8 Mon Sep 17 00:00:00 2001 From: jinyus Date: Fri, 26 Jan 2024 21:36:07 -0500 Subject: [PATCH 07/11] test condiitonal listening for derivedFuture --- .../test/src/beacons/derived_future_test.dart | 57 ++++++++++++++++++- 1 file changed, 56 insertions(+), 1 deletion(-) 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 37af3fdd..5f2aab46 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 { @@ -424,4 +424,59 @@ void main() { expect(ran, 4); }); + + 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); + + num2.increment(); + + expect(derivedBeacon.isLoading, true); + + await Future.delayed(k1ms); + + expect(derivedBeacon.unwrapValue(), 21); + }); } From ab52fd6c1816db4ef0c598f1cd2e9d90800a9c54 Mon Sep 17 00:00:00 2001 From: jinyus Date: Fri, 26 Jan 2024 21:41:12 -0500 Subject: [PATCH 08/11] make conditional test more robust --- .../test/src/beacons/derived_future_test.dart | 12 ++++++++++++ 1 file changed, 12 insertions(+) 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 5f2aab46..7a8b7aee 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 @@ -478,5 +478,17 @@ void main() { await Future.delayed(k1ms); expect(derivedBeacon.unwrapValue(), 21); + + guard.value = true; + + expect(num1.listenersCount, 1); + expect(num2.listenersCount, 0); + expect(num3.listenersCount, 0); + + expect(derivedBeacon.isLoading, true); + + await Future.delayed(k1ms); + + expect(derivedBeacon.unwrapValue(), 12); }); } From cf387108712927ebb6123248944a1d0324f590ed Mon Sep 17 00:00:00 2001 From: jinyus Date: Fri, 26 Jan 2024 21:59:17 -0500 Subject: [PATCH 09/11] make sleep for derived beacons configuarable with shouldSleep param --- .../lib/src/beacons/derived.dart | 7 ++++++- .../lib/src/beacons/derived_future.dart | 3 +++ .../lib/src/creator/beacon_creator.dart | 20 ++++++++++++------- .../lib/src/creator/beacon_group_creator.dart | 4 ++++ 4 files changed, 26 insertions(+), 8 deletions(-) diff --git a/packages/state_beacon_core/lib/src/beacons/derived.dart b/packages/state_beacon_core/lib/src/beacons/derived.dart index 1df8135f..e21ed43a 100644 --- a/packages/state_beacon_core/lib/src/beacons/derived.dart +++ b/packages/state_beacon_core/lib/src/beacons/derived.dart @@ -25,7 +25,12 @@ 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; 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 ee6e1578..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); @@ -30,6 +31,8 @@ class DerivedFutureBeacon extends FutureBeacon _setValue(AsyncLoading()); } + if (!shouldSleep) return; + _listeners.whenEmpty(() { // setting status to idle will dispose the internal effect // and stop listening to dependencies 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 c3eabcbf..e76b8e8d 100644 --- a/packages/state_beacon_core/lib/src/creator/beacon_creator.dart +++ b/packages/state_beacon_core/lib/src/creator/beacon_creator.dart @@ -418,9 +418,10 @@ 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`. /// @@ -438,10 +439,12 @@ class _BeaconCreator { ReadableBeacon derived( T Function() compute, { String? name, + bool shouldSleep = true, bool supportConditional = true, }) { final beacon = WritableDerivedBeacon( name: name ?? 'DerivedBeacon<$T>', + shouldSleep: shouldSleep, ); void start() { @@ -467,14 +470,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 @@ -504,12 +508,14 @@ class _BeaconCreator { FutureCallback compute, { bool manualStart = false, bool cancelRunning = true, + bool shouldSleep = true, String? name, }) { final beacon = DerivedFutureBeacon( compute, manualStart: manualStart, cancelRunning: cancelRunning, + shouldSleep: shouldSleep, name: name ?? 'DerivedFutureBeacon<$T>', ); 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 38fc95a2..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,12 +86,14 @@ class BeaconGroup extends _BeaconCreator { FutureCallback compute, { bool manualStart = false, bool cancelRunning = true, + bool shouldSleep = true, String? name, }) { final beacon = super.derivedFuture( compute, manualStart: manualStart, cancelRunning: cancelRunning, + shouldSleep: shouldSleep, name: name, ); _beacons.add(beacon); From 5351392d743e4de4a4292c6275ce4ad04f8da0ac Mon Sep 17 00:00:00 2001 From: jinyus Date: Fri, 26 Jan 2024 21:59:32 -0500 Subject: [PATCH 10/11] add test for when shouldSleep=false. --- .../test/src/beacons/derived_future_test.dart | 49 +++++++++++++++++++ .../test/src/beacons/derived_test.dart | 42 ++++++++++++++++ 2 files changed, 91 insertions(+) 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 7a8b7aee..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 @@ -425,6 +425,55 @@ void main() { 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 { + ran++; + return num1.value + num2.value; + }, + shouldSleep: false, + ); + + 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, 4); + + expect(derivedBeacon.isLoading, true); + + 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); 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 20b41f68..4500f387 100644 --- a/packages/state_beacon_core/test/src/beacons/derived_test.dart +++ b/packages/state_beacon_core/test/src/beacons/derived_test.dart @@ -181,4 +181,46 @@ void main() { 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); + }); } From d8e60b774562af2968653b6cfca0388866c7d5dd Mon Sep 17 00:00:00 2001 From: jinyus Date: Fri, 26 Jan 2024 22:08:11 -0500 Subject: [PATCH 11/11] update readme --- Readme.md | 12 +++++++++++- packages/state_beacon/README.md | 12 +++++++++++- packages/state_beacon_core/README.md | 12 +++++++++++- .../lib/src/creator/beacon_creator.dart | 1 - 4 files changed, 33 insertions(+), 4 deletions(-) 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/creator/beacon_creator.dart b/packages/state_beacon_core/lib/src/creator/beacon_creator.dart index e76b8e8d..abef21c6 100644 --- a/packages/state_beacon_core/lib/src/creator/beacon_creator.dart +++ b/packages/state_beacon_core/lib/src/creator/beacon_creator.dart @@ -423,7 +423,6 @@ class _BeaconCreator { /// /// 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