Skip to content

Commit

Permalink
feat: add scheduler option to autorun and reaction
Browse files Browse the repository at this point in the history
  • Loading branch information
amondnet committed Jul 16, 2024
1 parent 61d918f commit 08c1ad7
Show file tree
Hide file tree
Showing 7 changed files with 104 additions and 14 deletions.
4 changes: 4 additions & 0 deletions mobx/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 2.4.0

- Add `scheduler` to `reaction` and `autorun` to allow customizing the scheduler used to schedule the reaction. By [@amondnet]((https://github.com/amondnet).

## 2.3.3+1 - 2.3.3+2

- Analyzer fixes
Expand Down
26 changes: 22 additions & 4 deletions mobx/lib/src/api/reaction.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import 'dart:async';

import 'package:mobx/src/api/context.dart';
import 'package:mobx/src/core.dart';

Expand All @@ -6,7 +8,10 @@ import 'package:mobx/src/core.dart';
///
/// Optional configuration:
/// * [name]: debug name for this reaction
/// * [delay]: throttling delay in milliseconds
/// * [delay]: Number of milliseconds that can be used to throttle the effect function. If zero (default), no throttling happens.
/// * [context]: the [ReactiveContext] to use. By default the [mainContext] is used.
/// * [scheduler]: Set a custom scheduler to determine how re-running the autorun function should be scheduled. It takes a function that should be invoked at some point in the future.
/// * [onError]: By default, any exception thrown inside an reaction will be logged, but not further thrown. This is to make sure that an exception in one reaction does not prevent the scheduled execution of other, possibly unrelated reactions. This also allows reactions to recover from exceptions. Throwing an exception does not break the tracking done by MobX, so subsequent runs of the reaction might complete normally again if the cause for the exception is removed. This option allows overriding that behavior. It is possible to set a global error handler or to disable catching errors completely using [ReactiveConfig].
///
/// ```
/// var x = Observable(10);
Expand All @@ -27,13 +32,21 @@ ReactionDisposer autorun(Function(Reaction) fn,
{String? name,
int? delay,
ReactiveContext? context,
Timer Function(void Function())? scheduler,
void Function(Object, Reaction)? onError}) =>
createAutorun(context ?? mainContext, fn,
name: name, delay: delay, onError: onError);
name: name, delay: delay, scheduler: scheduler, onError: onError);

/// Executes the [fn] function and tracks the observables used in it. Returns
/// a function to dispose the reaction.
///
/// Optional configuration:
/// * [name]: debug name for this reaction
/// * [delay]: Number of milliseconds that can be used to throttle the effect function. If zero (default), no throttling happens.
/// * [context]: the [ReactiveContext] to use. By default the [mainContext] is used.
/// * [scheduler]: Set a custom scheduler to determine how re-running the autorun function should be scheduled. It takes a function that should be invoked at some point in the future.
/// * [onError]: By default, any exception thrown inside an reaction will be logged, but not further thrown. This is to make sure that an exception in one reaction does not prevent the scheduled execution of other, possibly unrelated reactions. This also allows reactions to recover from exceptions. Throwing an exception does not break the tracking done by MobX, so subsequent runs of the reaction might complete normally again if the cause for the exception is removed. This option allows overriding that behavior. It is possible to set a global error handler or to disable catching errors completely using [ReactiveConfig].
///
/// The [fn] is supposed to return a value of type T. When it changes, the
/// [effect] function is executed.
///
Expand All @@ -43,20 +56,25 @@ ReactionDisposer autorun(Function(Reaction) fn,
/// [fireImmediately] if you want to invoke the effect immediately without waiting for
/// the [fn] to change its value. It is possible to define a custom [equals] function
/// to override the default comparison for the value returned by [fn], to have fined
/// grained control over when the reactions should run.
/// grained control over when the reactions should run. By default, the [mainContext]
/// is used, but you can also pass in a custom [context].
/// You can also pass in an optional [onError] handler for errors thrown during the [fn] execution.
/// You can also pass in an optional [scheduler] to schedule the [effect] execution.
ReactionDisposer reaction<T>(T Function(Reaction) fn, void Function(T) effect,
{String? name,
int? delay,
bool? fireImmediately,
EqualityComparer<T>? equals,
ReactiveContext? context,
Timer Function(void Function())? scheduler,
void Function(Object, Reaction)? onError}) =>
createReaction<T>(context ?? mainContext, fn, effect,
name: name,
delay: delay,
equals: equals,
fireImmediately: fireImmediately,
onError: onError);
onError: onError,
scheduler: scheduler);

/// A one-time reaction that auto-disposes when the [predicate] becomes true. It also
/// executes the [effect] when the predicate turns true.
Expand Down
23 changes: 15 additions & 8 deletions mobx/lib/src/core/reaction_helper.dart
Original file line number Diff line number Diff line change
Expand Up @@ -23,19 +23,24 @@ class ReactionDisposer {
/// An internal helper function to create a [autorun]
ReactionDisposer createAutorun(
ReactiveContext context, Function(Reaction) trackingFn,
{String? name, int? delay, void Function(Object, Reaction)? onError}) {
{String? name,
int? delay,
Timer Function(void Function())? scheduler,
void Function(Object, Reaction)? onError}) {
late ReactionImpl rxn;

final rxnName = name ?? context.nameFor('Autorun');
final runSync = scheduler == null && delay == null;

if (delay == null) {
if (runSync) {
// Use a sync-scheduler.
rxn = ReactionImpl(context, () {
rxn.track(() => trackingFn(rxn));
}, name: rxnName, onError: onError);
} else {
// Use a delayed scheduler.
final scheduler = createDelayedScheduler(delay);
// Use a scheduler or delayed scheduler.
final schedulerFromOptions =
scheduler ?? (delay != null ? createDelayedScheduler(delay) : null);
var isScheduled = false;
Timer? timer;

Expand All @@ -46,7 +51,7 @@ ReactionDisposer createAutorun(
timer?.cancel();
timer = null;

timer = scheduler(() {
timer = schedulerFromOptions!(() {
isScheduled = false;
if (!rxn.isDisposed) {
rxn.track(() => trackingFn(rxn));
Expand All @@ -69,6 +74,7 @@ ReactionDisposer createReaction<T>(
int? delay,
bool? fireImmediately,
EqualityComparer<T>? equals,
Timer Function(void Function())? scheduler,
void Function(Object, Reaction)? onError}) {
late ReactionImpl rxn;

Expand All @@ -77,8 +83,9 @@ ReactionDisposer createReaction<T>(
final effectAction =
Action((T? value) => effect(value as T), name: '$rxnName-effect');

final runSync = delay == null;
final scheduler = delay != null ? createDelayedScheduler(delay) : null;
final runSync = scheduler == null && delay == null;
final schedulerFromOptions =
scheduler ?? (delay != null ? createDelayedScheduler(delay) : null);

var firstTime = true;
T? value;
Expand Down Expand Up @@ -124,7 +131,7 @@ ReactionDisposer createReaction<T>(
timer?.cancel();
timer = null;

timer = scheduler!(() {
timer = schedulerFromOptions!(() {
isScheduled = false;
if (!rxn.isDisposed) {
reactionRunner();
Expand Down
2 changes: 1 addition & 1 deletion mobx/lib/version.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Generated via set_version.dart. !!!DO NOT MODIFY BY HAND!!!

/// The current version as per `pubspec.yaml`.
const version = '2.3.3+2';
const version = '2.4.0';
2 changes: 1 addition & 1 deletion mobx/pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
name: mobx
version: 2.3.3+2
version: 2.4.0
description: "MobX is a library for reactively managing the state of your applications. Use the power of observables, actions, and reactions to supercharge your Dart and Flutter apps."

repository: https://github.com/mobxjs/mobx.dart
Expand Down
34 changes: 34 additions & 0 deletions mobx/test/autorun_test.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import 'dart:async';

import 'package:fake_async/fake_async.dart';
import 'package:mobx/mobx.dart';
import 'package:mocktail/mocktail.dart' as mock;
Expand Down Expand Up @@ -101,6 +103,38 @@ void main() {
dispose();
});

test('with custom scheduler', () {
late Function dispose;
const delayMs = 5000;

final x = Observable(10);
var value = 0;

fakeAsync((async) {
dispose = autorun((_) {
value = x.value + 1;
}, scheduler: (f) {
return Timer(const Duration(milliseconds: delayMs), f);
}).call;

async.elapse(const Duration(milliseconds: 2500));

expect(value, 0); // autorun() should not have executed at this time

async.elapse(const Duration(milliseconds: 2500));

expect(value, 11); // autorun() should have executed

x.value = 100;

expect(value, 11); // should still retain the last value
async.elapse(const Duration(milliseconds: delayMs));
expect(value, 101); // should change now
});

dispose();
});

test('with pre-mature disposal in tracking function', () {
final x = Observable(10);

Expand Down
27 changes: 27 additions & 0 deletions mobx/test/reaction_test.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import 'dart:async';

import 'package:fake_async/fake_async.dart';
import 'package:mobx/mobx.dart' hide when;
import 'package:mobx/src/core.dart';
Expand Down Expand Up @@ -109,6 +111,31 @@ void main() {
});
});

test('works with scheduler', () {
final x = Observable(10);
var executed = false;

final d = reaction((_) => x.value > 10, (isGreaterThan10) {
executed = true;
}, scheduler: (fn) => Timer(const Duration(milliseconds: 1000), fn));

fakeAsync((async) {
x.value = 11;

// Even though tracking function has changed, effect should not be executed
expect(executed, isFalse);
async.elapse(const Duration(milliseconds: 500));
expect(
executed, isFalse); // should still be false as 1s has not elapsed

async.elapse(
const Duration(milliseconds: 500)); // should now trigger effect
expect(executed, isTrue);

d();
});
});

test('that fires immediately', () {
final x = Observable(10);
var executed = false;
Expand Down

0 comments on commit 08c1ad7

Please sign in to comment.