Skip to content

Commit

Permalink
Merge pull request #51 from jinyus:feat/sleep
Browse files Browse the repository at this point in the history
[Feat] Sleep derived when there are not more watchers
  • Loading branch information
jinyus authored Jan 27, 2024
2 parents 95d7f45 + d8e60b7 commit f503f90
Show file tree
Hide file tree
Showing 14 changed files with 557 additions and 85 deletions.
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

0 comments on commit f503f90

Please sign in to comment.