Skip to content

Commit 22fcaff

Browse files
committed
Simplify evaluation mixin and prevent provider exception.
Some engine events may arrive after the provider has been disposed if ther user quits the screen while the engine is still computing. It now uses the ref.mounted check to prevent crashing the provider.
1 parent 5ffc2a9 commit 22fcaff

File tree

5 files changed

+58
-148
lines changed

5 files changed

+58
-148
lines changed

lib/src/model/analysis/analysis_controller.dart

Lines changed: 0 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -124,23 +124,6 @@ class AnalysisController extends AsyncNotifier<AnalysisState>
124124
late Root _root;
125125
late Variant _variant;
126126

127-
@override
128-
@protected
129-
EngineEvaluationPrefState get evaluationPrefs => ref.read(engineEvaluationPreferencesProvider);
130-
131-
@override
132-
@protected
133-
EngineEvaluationPreferences get evaluationPreferencesNotifier =>
134-
ref.read(engineEvaluationPreferencesProvider.notifier);
135-
136-
@override
137-
@protected
138-
EvaluationService evaluationServiceFactory() => ref.read(evaluationServiceProvider);
139-
140-
@override
141-
@protected
142-
AnalysisState get evaluationState => state.requireValue;
143-
144127
@override
145128
@protected
146129
late SocketClient socketClient;
@@ -157,7 +140,6 @@ class AnalysisController extends AsyncNotifier<AnalysisState>
157140

158141
ref.onDispose(() {
159142
_socketSubscription?.cancel();
160-
disposeEngineEvaluation();
161143
serverAnalysisService.lastAnalysisEvent.removeListener(_listenToServerAnalysisEvents);
162144
});
163145

@@ -320,11 +302,6 @@ class AnalysisController extends AsyncNotifier<AnalysisState>
320302
// analysis preferences change
321303
final prefs = ref.read(analysisPreferencesProvider);
322304

323-
final isEngineAllowed = isComputerAnalysisAllowed && engineSupportedVariants.contains(_variant);
324-
if (isEngineAllowed) {
325-
initEngineEvaluation();
326-
}
327-
328305
serverAnalysisService.lastAnalysisEvent.addListener(_listenToServerAnalysisEvents);
329306

330307
final analysisState = AnalysisState(

lib/src/model/analysis/retro_controller.dart

Lines changed: 0 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -91,24 +91,6 @@ class RetroController extends AsyncNotifier<RetroState> with EngineEvaluationMix
9191

9292
final Completer<void> _serverAnalysisCompleter = Completer<void>();
9393

94-
@override
95-
@protected
96-
EngineEvaluationPrefState get evaluationPrefs =>
97-
ref.read(engineEvaluationPreferencesProvider).copyWith(isEnabled: true, numEvalLines: 1);
98-
99-
@override
100-
@protected
101-
EngineEvaluationPreferences get evaluationPreferencesNotifier =>
102-
ref.read(engineEvaluationPreferencesProvider.notifier);
103-
104-
@override
105-
@protected
106-
EvaluationService evaluationServiceFactory() => ref.read(evaluationServiceProvider);
107-
108-
@override
109-
@protected
110-
EvaluationMixinState get evaluationState => state.requireValue;
111-
11294
@override
11395
@protected
11496
late SocketClient socketClient;
@@ -122,7 +104,6 @@ class RetroController extends AsyncNotifier<RetroState> with EngineEvaluationMix
122104
final serverAnalysisService = ref.watch(serverAnalysisServiceProvider);
123105

124106
ref.onDispose(() {
125-
disposeEngineEvaluation();
126107
serverAnalysisService.lastAnalysisEvent.removeListener(_listenToServerAnalysisEvents);
127108
});
128109

@@ -134,8 +115,6 @@ class RetroController extends AsyncNotifier<RetroState> with EngineEvaluationMix
134115
throw Exception('Variant ${_game.meta.variant} is not supported for retro mode');
135116
}
136117

137-
initEngineEvaluation();
138-
139118
_root = _game.makeTree();
140119

141120
if (_game.serverAnalysis == null) {

lib/src/model/broadcast/broadcast_analysis_controller.dart

Lines changed: 0 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -60,23 +60,6 @@ class BroadcastAnalysisController extends AsyncNotifier<BroadcastAnalysisState>
6060

6161
Object? _key = Object();
6262

63-
@override
64-
@protected
65-
EngineEvaluationPrefState get evaluationPrefs => ref.read(engineEvaluationPreferencesProvider);
66-
67-
@override
68-
@protected
69-
EngineEvaluationPreferences get evaluationPreferencesNotifier =>
70-
ref.read(engineEvaluationPreferencesProvider.notifier);
71-
72-
@override
73-
@protected
74-
EvaluationService evaluationServiceFactory() => ref.read(evaluationServiceProvider);
75-
76-
@override
77-
@protected
78-
BroadcastAnalysisState get evaluationState => state.requireValue;
79-
8063
@override
8164
@protected
8265
SocketClient get socketClient => _socketClient;
@@ -94,7 +77,6 @@ class BroadcastAnalysisController extends AsyncNotifier<BroadcastAnalysisState>
9477
_startEngineEvalTimer?.cancel();
9578
_appLifecycleListener?.dispose();
9679
_syncDebouncer.cancel();
97-
disposeEngineEvaluation();
9880
});
9981

10082
_socketClient = ref
@@ -163,8 +145,6 @@ class BroadcastAnalysisController extends AsyncNotifier<BroadcastAnalysisState>
163145
// `debouncedStartEngineEval` require the state to have a value.
164146
state = AsyncData(broadcastState);
165147

166-
initEngineEvaluation();
167-
168148
if (state.requireValue.isEngineAvailable(evaluationPrefs)) {
169149
requestEval();
170150
}

lib/src/model/engine/evaluation_mixin.dart

Lines changed: 58 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -56,78 +56,61 @@ abstract class EvaluationMixinState {
5656

5757
/// A mixin to provide engine evaluation functionality to an [AsyncNotifier].
5858
///
59-
/// The parent must initialize the engine evaluation by calling [initEngineEvaluation] and dispose it
60-
/// by calling [disposeEngineEvaluation].
61-
///
6259
/// The parent must implement the following:
63-
/// - [evaluationState]
6460
/// - [socketClient] to provide the [SocketClient] to use for cloud evaluations. If `null`, the
6561
/// cloud evaluations will not be requested.
6662
/// - [positionTree] to provide the tree where the evaluations are stored.
67-
/// - [evaluationServiceFactory]
68-
/// - [evaluationPreferencesNotifier]
69-
/// - [evaluationPrefs]
7063
///
7164
/// The parent can implement:
7265
/// - [onCurrentPathEvalChanged] to refresh the current node after an evaluation.
73-
mixin EngineEvaluationMixin {
74-
/// Direct access to underlying [EvaluationMixinState].
75-
///
76-
/// This is a workaround to use this mixin in both [Notifier] and [AsyncNotifier].
77-
/// Parent must ensure that [AsyncNotifier.state] is loaded before using methods that require
78-
/// [EvaluationMixinState].
79-
EvaluationMixinState get evaluationState;
80-
81-
EngineEvaluationPrefState get evaluationPrefs;
82-
EngineEvaluationPreferences get evaluationPreferencesNotifier;
83-
EvaluationService evaluationServiceFactory();
66+
mixin EngineEvaluationMixin<T extends EvaluationMixinState> on AnyNotifier<AsyncValue<T>, T> {
67+
late EvaluationService _evaluationService;
68+
8469
SocketClient? get socketClient;
8570
Node get positionTree;
8671

72+
EngineEvaluationPrefState get evaluationPrefs => ref.read(engineEvaluationPreferencesProvider);
73+
74+
EngineEvaluationPreferences get _evaluationPreferencesNotifier =>
75+
ref.read(engineEvaluationPreferencesProvider.notifier);
76+
8777
final _evalRequestDebounce = Debouncer(kRequestEvalDebounceDelay);
8878
final _localEngineAfterDelayDebounce = Debouncer(kLocalEngineAfterCloudEvalDelay);
8979

9080
StreamSubscription<SocketEvent>? _socketSubscription;
9181

92-
EvaluationService? _evaluationService;
93-
9482
/// Called when a received evaluation is for the current path.
9583
///
9684
/// If the evaluation string is the same for both the received and the current evaluation, the
9785
/// [isSameEvalString] parameter will be `true`. It can be used to avoid refreshing the UI if the
9886
/// evaluation string is the same.
9987
void onCurrentPathEvalChanged(bool isSameEvalString) {}
10088

101-
/// Initializes the engine evaluation.
102-
///
103-
/// Will start listening to the [SocketClient] for cloud evaluations.
104-
///
105-
/// The local engine is not started here, but only when [requestEval] is called.
106-
@nonVirtual
107-
void initEngineEvaluation() {
108-
_socketSubscription = socketClient?.stream.listen(_handleSocketEvent);
109-
_evaluationService = evaluationServiceFactory();
110-
}
89+
@override
90+
void runBuild() {
91+
_evaluationService = ref.read(evaluationServiceProvider);
11192

112-
/// Disposes all resources related to the engine evaluation.
113-
@nonVirtual
114-
void disposeEngineEvaluation() {
115-
_evalRequestDebounce.cancel();
116-
_localEngineAfterDelayDebounce.cancel();
117-
_socketSubscription?.cancel();
118-
_evaluationService?.disposeEngine();
119-
_evaluationService = null;
93+
ref.onDispose(() {
94+
_evalRequestDebounce.cancel();
95+
_localEngineAfterDelayDebounce.cancel();
96+
_socketSubscription?.cancel();
97+
_evaluationService.disposeEngine();
98+
});
99+
100+
super.runBuild();
101+
102+
_socketSubscription = socketClient?.stream.listen(_handleSocketEvent);
120103
}
121104

122105
/// Toggles the engine evaluation on/off.
123106
@mustCallSuper
124107
Future<void> toggleEngine() async {
125-
await evaluationPreferencesNotifier.toggle();
108+
await _evaluationPreferencesNotifier.toggle();
126109

127-
if (evaluationState.isEngineAvailable(evaluationPrefs)) {
110+
if (state.requireValue.isEngineAvailable(evaluationPrefs)) {
128111
requestEval(forceRestart: true);
129112
} else {
130-
await _evaluationService?.disposeEngine();
113+
await _evaluationService.disposeEngine();
131114
}
132115
}
133116

@@ -137,33 +120,35 @@ mixin EngineEvaluationMixin {
137120
positionTree.updateAll((node) => node.eval = null);
138121
onCurrentPathEvalChanged(false);
139122

140-
evaluationPreferencesNotifier.setNumEvalLines(numEvalLines);
123+
_evaluationPreferencesNotifier.setNumEvalLines(numEvalLines);
141124

142-
_evaluationService?.options = evaluationPrefs.evaluationOptions;
125+
_evaluationService.options = evaluationPrefs.evaluationOptions;
143126

144127
requestEval(forceRestart: true);
145128
}
146129

147130
@mustCallSuper
148131
void setEngineCores(int numEngineCores) {
149-
evaluationPreferencesNotifier.setEngineCores(numEngineCores);
132+
_evaluationPreferencesNotifier.setEngineCores(numEngineCores);
150133

151-
_evaluationService?.options = evaluationPrefs.evaluationOptions;
134+
_evaluationService.options = evaluationPrefs.evaluationOptions;
152135

153136
requestEval(forceRestart: true);
154137
}
155138

156139
@mustCallSuper
157140
void setEngineSearchTime(Duration searchTime) {
158-
evaluationPreferencesNotifier.setEngineSearchTime(searchTime);
141+
_evaluationPreferencesNotifier.setEngineSearchTime(searchTime);
159142

160-
_evaluationService?.options = evaluationPrefs.evaluationOptions;
143+
_evaluationService.options = evaluationPrefs.evaluationOptions;
161144

162145
requestEval(forceRestart: true);
163146
}
164147

165148
/// Requests an engine evaluation if available.
166149
///
150+
/// This must be called after the AsyncNotifier's[state] has been initialized.
151+
///
167152
/// This sends an `evalGet` event to the server to get the cloud evaluation and starts the local
168153
/// engine evaluation.
169154
///
@@ -177,10 +162,10 @@ mixin EngineEvaluationMixin {
177162
/// moves.
178163
@nonVirtual
179164
void requestEval({bool goDeeper = false, bool forceRestart = false}) {
180-
if (!evaluationState.isEngineAvailable(evaluationPrefs)) return;
165+
if (!state.requireValue.isEngineAvailable(evaluationPrefs)) return;
181166

182167
final delayLocalEngine =
183-
evaluationState.alwaysRequestCloudEval &&
168+
state.requireValue.alwaysRequestCloudEval &&
184169
evaluationPrefs.engineSearchTime != kMaxEngineSearchTime;
185170

186171
_evalRequestDebounce(() {
@@ -233,25 +218,28 @@ mixin EngineEvaluationMixin {
233218
node.eval = eval;
234219
});
235220

236-
if (evaluationState.currentPath == path) {
221+
if (!ref.mounted) return;
222+
223+
if (state.requireValue.currentPath == path) {
237224
onCurrentPathEvalChanged(isSameEvalString);
238225
}
239226
}
240227

241228
bool _canCloudEval() {
242-
if (evaluationState.currentPosition!.ply >= 15 && !evaluationState.alwaysRequestCloudEval) {
229+
if (state.requireValue.currentPosition!.ply >= 15 &&
230+
!state.requireValue.alwaysRequestCloudEval) {
243231
return false;
244232
}
245-
if (positionTree.nodeAt(evaluationState.currentPath).eval is CloudEval) return false;
233+
if (positionTree.nodeAt(state.requireValue.currentPath).eval is CloudEval) return false;
246234

247235
// cloud eval does not support threefold repetition
248236
final Set<String> fens = <String>{};
249-
final nodeList = positionTree.branchesOn(evaluationState.currentPath).toList();
237+
final nodeList = positionTree.branchesOn(state.requireValue.currentPath).toList();
250238
for (var i = nodeList.length - 1; i >= 0; i--) {
251239
final node = nodeList[i];
252240
final epd = fenToEpd(node.position.fen);
253241
if (fens.contains(epd)) return false;
254-
if (node.sanMove.isIrreversible(evaluationState.evaluationContext.variant)) {
242+
if (node.sanMove.isIrreversible(state.requireValue.evaluationContext.variant)) {
255243
return true;
256244
}
257245
fens.add(epd);
@@ -261,30 +249,30 @@ mixin EngineEvaluationMixin {
261249
}
262250

263251
void _sendEvalGetEvent() {
264-
if (!evaluationState.isEngineAvailable(evaluationPrefs)) return;
252+
if (!state.requireValue.isEngineAvailable(evaluationPrefs)) return;
265253
if (evaluationPrefs.engineSearchTime == kMaxEngineSearchTime) return;
266254
if (!_canCloudEval()) return;
267-
final curPosition = evaluationState.currentPosition;
255+
final curPosition = state.requireValue.currentPosition;
268256
if (curPosition == null) return;
269257
final numEvalLines = evaluationPrefs.numEvalLines;
270258

271259
socketClient?.send('evalGet', {
272260
'fen': curPosition.fen,
273-
'path': evaluationState.currentPath.value,
261+
'path': state.requireValue.currentPath.value,
274262
'mpv': numEvalLines,
275263
'up': true,
276264
});
277265
}
278266

279267
Future<void> _startEngineEval({bool goDeeper = false, bool forceRestart = false}) async {
280-
final curState = evaluationState;
268+
final curState = state.requireValue;
281269
if (!curState.isEngineAvailable(evaluationPrefs)) return;
282-
await _evaluationService?.ensureEngineInitialized(
283-
evaluationState.evaluationContext,
270+
await _evaluationService.ensureEngineInitialized(
271+
state.requireValue.evaluationContext,
284272
initOptions: evaluationPrefs.evaluationOptions,
285273
);
286274
_evaluationService
287-
?.start(
275+
.start(
288276
curState.currentPath,
289277
positionTree.branchesOn(curState.currentPath).map(Step.fromNode),
290278
initialPositionEval: positionTree.eval,
@@ -312,7 +300,7 @@ mixin EngineEvaluationMixin {
312300
// if the cloud eval is likely better, stop the local engine
313301
// nps varies with positional complexity so this is rough, but save planet earth
314302
if (likelyNodes < nodeEval.nodes) {
315-
_evaluationService?.stop();
303+
_evaluationService.stop();
316304
}
317305
return;
318306
}
@@ -324,11 +312,17 @@ mixin EngineEvaluationMixin {
324312
isSameEvalString = eval.evalString == nodeEval?.evalString;
325313
node.eval = eval;
326314
});
327-
if (work.path == evaluationState.currentPath) {
315+
316+
if (!ref.mounted) return;
317+
318+
if (work.path == state.requireValue.currentPath) {
328319
onCurrentPathEvalChanged(isSameEvalString);
329320
}
330321
});
331322
}
332323

333-
bool _shouldEmit(Work work) => work.path == evaluationState.currentPath;
324+
bool _shouldEmit(Work work) {
325+
if (!ref.mounted) return false;
326+
return work.path == state.requireValue.currentPath;
327+
}
334328
}

0 commit comments

Comments
 (0)