Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feat] Sleep derived when there are no more watchers #51

Merged
merged 11 commits into from
Jan 27, 2024
12 changes: 11 additions & 1 deletion Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
12 changes: 11 additions & 1 deletion packages/state_beacon/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
12 changes: 11 additions & 1 deletion packages/state_beacon_core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
40 changes: 37 additions & 3 deletions packages/state_beacon_core/lib/src/beacons/derived.dart
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
// ignore_for_file: public_member_api_docs
// ignore_for_file: public_member_api_docs, use_setters_to_change_properties

part of '../base_beacon.dart';

enum DerivedStatus { idle, running }

mixin DerivedMixin<T> on ReadableBeacon<T> {
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();
Expand All @@ -21,5 +25,35 @@ mixin DerivedMixin<T> on ReadableBeacon<T> {

// this is only used internally
class WritableDerivedBeacon<T> extends WritableBeacon<T> with DerivedMixin<T> {
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();
}
}
34 changes: 33 additions & 1 deletion packages/state_beacon_core/lib/src/beacons/derived_future.dart
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ class DerivedFutureBeacon<T> extends FutureBeacon<T>
bool manualStart = false,
super.cancelRunning = true,
super.name,
bool shouldSleep = true,
}) {
if (manualStart) {
_status.set(DerivedFutureStatus.idle);
Expand All @@ -29,12 +30,43 @@ class DerivedFutureBeacon<T> extends FutureBeacon<T>
_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<T> get value {
if (_sleeping) {
start();
_sleeping = false;
}
return super.value;
}

@override
AsyncValue<T> peek() {
if (_sleeping) {
start();
_sleeping = false;
}
return super.peek();
}

/// Runs the future.
Future<void> run() => _run();

final _status = Beacon.lazyWritable<DerivedFutureStatus>();
late final _status = Beacon.lazyWritable<DerivedFutureStatus>(
name: "$name's status",
);

/// The status of the future.
ReadableBeacon<DerivedFutureStatus> get status => _status;
Expand Down
21 changes: 14 additions & 7 deletions packages/state_beacon_core/lib/src/beacons/future.dart
Original file line number Diff line number Diff line change
Expand Up @@ -20,28 +20,35 @@ abstract class FutureBeacon<T> extends AsyncBeacon<T> {

/// 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<T> _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]
///
Expand Down
65 changes: 37 additions & 28 deletions packages/state_beacon_core/lib/src/creator/beacon_creator.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -438,21 +438,29 @@ class _BeaconCreator {
ReadableBeacon<T> derived<T>(
T Function() compute, {
String? name,
bool shouldSleep = true,
bool supportConditional = true,
}) {
final beacon = WritableDerivedBeacon<T>(
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;
}
Expand All @@ -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
Expand Down Expand Up @@ -498,28 +507,28 @@ class _BeaconCreator {
FutureCallback<T> compute, {
bool manualStart = false,
bool cancelRunning = true,
bool shouldSleep = true,
String? name,
bool supportConditional = true,
}) {
final beacon = DerivedFutureBeacon<T>(
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);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,11 +68,13 @@ class BeaconGroup extends _BeaconCreator {
ReadableBeacon<T> derived<T>(
T Function() compute, {
String? name,
bool shouldSleep = true,
bool supportConditional = true,
}) {
final beacon = super.derived<T>(
compute,
name: name,
shouldSleep: shouldSleep,
supportConditional: supportConditional,
);
_beacons.add(beacon);
Expand All @@ -84,15 +86,15 @@ class BeaconGroup extends _BeaconCreator {
FutureCallback<T> compute, {
bool manualStart = false,
bool cancelRunning = true,
bool shouldSleep = true,
String? name,
bool supportConditional = true,
}) {
final beacon = super.derivedFuture<T>(
compute,
manualStart: manualStart,
cancelRunning: cancelRunning,
shouldSleep: shouldSleep,
name: name,
supportConditional: supportConditional,
);
_beacons.add(beacon);
return beacon;
Expand Down
Loading