blob: bf36b86acb6cba4b327979e16e7e502b69b63d17 [file] [log] [blame]
part of '../core.dart';
class _ReactiveState {
/// Current batch depth. This is used to track the depth of `transaction` / `action`.
/// When the batch ends, we execute all the [pendingReactions]
int batch = 0;
/// Monotonically increasing counter for assigning a name to an action/reaction/atom
int nextIdCounter = 0;
/// Tracks the currently executing derivation (reactions or computeds).
/// The Observables used here are linked to this derivation.
Derivation? trackingDerivation;
/// The reactions that must be triggered at the end of a `transaction` or an `action`
List<Reaction> pendingReactions = [];
/// Are we in middle of executing the [pendingReactions].
bool isRunningReactions = false;
/// The atoms that must be disconnected from their observed reactions. This happens
/// if a reaction has been disposed during a batch
List<Atom> pendingUnobservations = [];
/// Tracks if within a computed property evaluation
int computationDepth = 0;
/// Tracks if observables can be mutated
bool allowStateChanges = true;
/// Are we inside an action or transaction?
bool get isWithinBatch => batch > 0;
/// Are we inside a reaction or computed?
bool get isWithinDerivation =>
trackingDerivation != null || computationDepth > 0;
List<SpyListener> spyListeners = [];
}
typedef ReactionErrorHandler = void Function(Object error, Reaction reaction);
/// Defines the behavior for observables read outside actions and reactions
///
/// `always`: If observables are read outside actions/reactions, throw an Exception
/// `never`: Allow unrestricted reading of observables everywhere. This is the default.
enum ReactiveReadPolicy { always, never }
/// Defines the behavior for observables mutated outside actions
///
/// `observed`: If there are observers for the mutated observable, then throw. Else allow mutation outside an action.
/// `always`: Always throw if an observable is mutated outside an action
/// `never`: Allow mutating observables outside actions
enum ReactiveWritePolicy { observed, always, never }
/// Configuration used by [ReactiveContext]
class ReactiveConfig {
ReactiveConfig({
this.disableErrorBoundaries = false,
this.writePolicy = ReactiveWritePolicy.observed,
this.readPolicy = ReactiveReadPolicy.never,
this.maxIterations = 100,
this.isSpyEnabled = false,
});
/// The main or default configuration used by [ReactiveContext]
static final ReactiveConfig main = ReactiveConfig(
disableErrorBoundaries: false,
writePolicy: ReactiveWritePolicy.observed,
readPolicy: ReactiveReadPolicy.never);
/// Whether MobX should throw exceptions instead of catching them and store
/// as [ReactionImpl.errorValue].
final bool disableErrorBoundaries;
/// Enforce mutation of observables inside an action
final ReactiveWritePolicy writePolicy;
/// Enforce the use of reactions for reading observables
final ReactiveReadPolicy readPolicy;
/// Max number of iterations before bailing out for a cyclic reaction
final int maxIterations;
final Set<ReactionErrorHandler> _reactionErrorHandlers = {};
final bool isSpyEnabled;
ReactiveConfig clone(
{bool? disableErrorBoundaries,
ReactiveWritePolicy? writePolicy,
ReactiveReadPolicy? readPolicy,
int? maxIterations,
bool? isSpyEnabled}) =>
ReactiveConfig(
disableErrorBoundaries:
disableErrorBoundaries ?? this.disableErrorBoundaries,
writePolicy: writePolicy ?? this.writePolicy,
readPolicy: readPolicy ?? this.readPolicy,
maxIterations: maxIterations ?? this.maxIterations,
isSpyEnabled: isSpyEnabled ?? this.isSpyEnabled);
}
class ReactiveContext {
ReactiveContext({ReactiveConfig? config}) {
this.config = config ?? ReactiveConfig.main;
}
late ReactiveConfig _config;
ReactiveConfig get config => _config;
set config(ReactiveConfig newValue) {
_config = newValue;
_state.allowStateChanges = _config.writePolicy == ReactiveWritePolicy.never;
}
_ReactiveState _state = _ReactiveState();
int get nextId => ++_state.nextIdCounter;
String nameFor(String prefix) {
assert(prefix.isNotEmpty);
return '$prefix@$nextId';
}
bool get isWithinBatch => _state.isWithinBatch;
bool get isSpyEnabled =>
_config.isSpyEnabled && _state.spyListeners.isNotEmpty;
Dispose spy(SpyListener listener) {
_state.spyListeners.add(listener);
return _once(() {
_state.spyListeners.remove(listener);
});
}
void spyReport(SpyEvent event) {
if (!isSpyEnabled) {
return;
}
for (var i = 0; i < _state.spyListeners.length; i++) {
_state.spyListeners[i](event);
}
}
void startBatch() {
_state.batch++;
}
void endBatch() {
if (--_state.batch == 0) {
runReactions();
for (var i = 0; i < _state.pendingUnobservations.length; i++) {
final ob = _state.pendingUnobservations[i]
.._isPendingUnobservation = false;
if (ob._observers.isEmpty) {
if (ob._isBeingObserved) {
// if this observable had reactive observers, trigger the hooks
ob
.._isBeingObserved = false
.._notifyOnBecomeUnobserved();
}
if (ob is Computed) {
ob._suspend();
}
}
}
_state.pendingUnobservations = [];
}
}
void enforceReadPolicy(Atom atom) {
// ---
// We are wrapping in an assert() since we don't want this code to execute at runtime.
// The dart compiler removes assert() calls from the release build.
// ---
assert(() {
switch (config.readPolicy) {
case ReactiveReadPolicy.always:
assert(_state.isWithinBatch || _state.isWithinDerivation,
'Observable values cannot be read outside Actions and Reactions. Make sure to wrap them inside an action or a reaction. Tried to read: ${atom.name}');
break;
case ReactiveReadPolicy.never:
break;
}
return true;
}());
}
void enforceWritePolicy(Atom atom) {
// Cannot mutate observables inside a computed. This is required to maintain the consistency of the reactive system.
if (_state.computationDepth > 0 && atom.hasObservers) {
throw MobXException(
'Computed values are not allowed to cause side effects by changing observables that are already being observed. Tried to modify: ${atom.name}');
}
// ---
// We are wrapping in an assert() since we don't want this code to execute at runtime.
// The dart compiler removes assert() calls from the release build.
// ---
assert(() {
switch (config.writePolicy) {
case ReactiveWritePolicy.never:
break;
case ReactiveWritePolicy.observed:
if (atom.hasObservers == false) {
break;
}
assert(_state.isWithinBatch,
'Side effects like changing state are not allowed at this point. Please wrap the code in an "action". Tried to modify: ${atom.name}');
break;
case ReactiveWritePolicy.always:
assert(_state.isWithinBatch,
'Changing observable values outside actions is not allowed. Please wrap the code in an "action" if this change is intended. Tried to modify ${atom.name}');
}
return true;
}());
}
Derivation? _startTracking(Derivation derivation) {
final prevDerivation = _state.trackingDerivation;
_state.trackingDerivation = derivation;
_resetDerivationState(derivation);
derivation._newObservables = {};
return prevDerivation;
}
void _endTracking(Derivation currentDerivation, Derivation? prevDerivation) {
_state.trackingDerivation = prevDerivation;
_bindDependencies(currentDerivation);
}
T? trackDerivation<T>(Derivation d, T Function() fn) {
final prevDerivation = _startTracking(d);
T? result;
if (config.disableErrorBoundaries == true) {
result = fn();
} else {
try {
result = fn();
d._errorValue = null;
} on Object catch (e, s) {
d._errorValue = MobXCaughtException(e, stackTrace: s);
}
}
_endTracking(d, prevDerivation);
return result;
}
void _reportObserved(Atom atom) {
final derivation = _state.trackingDerivation;
if (derivation != null) {
derivation._newObservables!.add(atom);
if (!atom._isBeingObserved) {
atom
.._isBeingObserved = true
.._notifyOnBecomeObserved();
}
}
}
void _bindDependencies(Derivation derivation) {
final staleObservables =
derivation._observables.difference(derivation._newObservables!);
final newObservables =
derivation._newObservables!.difference(derivation._observables);
var lowestNewDerivationState = DerivationState.upToDate;
// Add newly found observables
for (final observable in newObservables) {
observable._addObserver(derivation);
// Computed = Observable + Derivation
if (observable is Computed) {
if (observable._dependenciesState.index >
lowestNewDerivationState.index) {
lowestNewDerivationState = observable._dependenciesState;
}
}
}
// Remove previous observables
for (final ob in staleObservables) {
ob._removeObserver(derivation);
}
if (lowestNewDerivationState != DerivationState.upToDate) {
derivation
.._dependenciesState = lowestNewDerivationState
.._onBecomeStale();
}
derivation
.._observables = derivation._newObservables!
.._newObservables = {}; // No need for newObservables beyond this point
}
void addPendingReaction(Reaction reaction) {
_state.pendingReactions.add(reaction);
}
void runReactions() {
if (_state.batch > 0 || _state.isRunningReactions) {
return;
}
_runReactionsInternal();
}
void _runReactionsInternal() {
_state.isRunningReactions = true;
var iterations = 0;
final allReactions = _state.pendingReactions;
// While running reactions, new reactions might be triggered.
// Hence we work with two variables and check whether
// we converge to no remaining reactions after a while.
while (allReactions.isNotEmpty) {
if (++iterations == config.maxIterations) {
final failingReaction = allReactions[0];
// Resetting ensures we have no bad-state left
_resetState();
throw MobXCyclicReactionException(
"Reaction doesn't converge to a stable state after ${config.maxIterations} iterations. Probably there is a cycle in the reactive function: $failingReaction");
}
final remainingReactions = allReactions.toList(growable: false);
allReactions.clear();
for (final reaction in remainingReactions) {
reaction._run();
}
}
_state
..pendingReactions = []
..isRunningReactions = false;
}
void propagateChanged(Atom atom) {
if (atom._lowestObserverState == DerivationState.stale) {
return;
}
atom._lowestObserverState = DerivationState.stale;
for (final observer in atom._observers) {
if (observer._dependenciesState == DerivationState.upToDate) {
observer._onBecomeStale();
}
observer._dependenciesState = DerivationState.stale;
}
}
void _propagatePossiblyChanged(Atom atom) {
if (atom._lowestObserverState != DerivationState.upToDate) {
return;
}
atom._lowestObserverState = DerivationState.possiblyStale;
for (final observer in atom._observers) {
if (observer._dependenciesState == DerivationState.upToDate) {
observer
.._dependenciesState = DerivationState.possiblyStale
.._onBecomeStale();
}
}
}
void _propagateChangeConfirmed(Atom atom) {
if (atom._lowestObserverState == DerivationState.stale) {
return;
}
atom._lowestObserverState = DerivationState.stale;
for (final observer in atom._observers) {
if (observer._dependenciesState == DerivationState.possiblyStale) {
observer._dependenciesState = DerivationState.stale;
} else if (observer._dependenciesState == DerivationState.upToDate) {
atom._lowestObserverState = DerivationState.upToDate;
}
}
}
void _clearObservables(Derivation derivation) {
final observables = derivation._observables;
derivation._observables = {};
for (final x in observables) {
x._removeObserver(derivation);
}
derivation._dependenciesState = DerivationState.notTracking;
}
void _enqueueForUnobservation(Atom atom) {
if (atom._isPendingUnobservation) {
return;
}
atom._isPendingUnobservation = true;
_state.pendingUnobservations.add(atom);
}
void _resetDerivationState(Derivation d) {
if (d._dependenciesState == DerivationState.upToDate) {
return;
}
d._dependenciesState = DerivationState.upToDate;
for (final obs in d._observables) {
obs._lowestObserverState = DerivationState.upToDate;
}
}
bool _shouldCompute(Derivation derivation) {
switch (derivation._dependenciesState) {
case DerivationState.upToDate:
return false;
case DerivationState.notTracking:
case DerivationState.stale:
return true;
case DerivationState.possiblyStale:
return untracked(() {
for (final obs in derivation._observables) {
if (obs is Computed) {
// Force a computation
if (config.disableErrorBoundaries == true) {
obs.value;
} else {
try {
obs.value;
} on Object catch (_) {
return true;
}
}
if (derivation._dependenciesState == DerivationState.stale) {
return true;
}
}
}
_resetDerivationState(derivation);
return false;
});
}
}
bool _hasCaughtException(Derivation d) =>
d._errorValue is MobXCaughtException;
bool isComputingDerivation() => _state.trackingDerivation != null;
Derivation? startUntracked() {
final prevDerivation = _state.trackingDerivation;
_state.trackingDerivation = null;
return prevDerivation;
}
// ignore: use_setters_to_change_properties
void endUntracked(Derivation? prevDerivation) {
_state.trackingDerivation = prevDerivation;
}
T untracked<T>(T Function() fn) {
final prevDerivation = startUntracked();
try {
return fn();
} finally {
endUntracked(prevDerivation);
}
}
Dispose onReactionError(ReactionErrorHandler handler) {
config._reactionErrorHandlers.add(handler);
return () {
config._reactionErrorHandlers.removeWhere((f) => f == handler);
};
}
void _notifyReactionErrorHandlers(Object exception, Reaction reaction) {
// ignore: avoid_function_literals_in_foreach_calls
config._reactionErrorHandlers.forEach((f) {
f(exception, reaction);
});
}
bool startAllowStateChanges({bool allow = true}) {
final prevValue = _state.allowStateChanges;
_state.allowStateChanges = allow;
return prevValue;
}
void endAllowStateChanges({bool allow = true}) {
_state.allowStateChanges = allow;
}
void _pushComputation() {
_state.computationDepth++;
}
void _popComputation() {
_state.computationDepth--;
}
void _resetState() {
_state = _ReactiveState()
..allowStateChanges = _config.writePolicy == ReactiveWritePolicy.never;
}
}