Skip to content

Commit 827e382

Browse files
authored
Fix provider bug that prevented to access games offline (#2519)
* Fix game provider bug Closes #2473 * Make http error detection less specific
1 parent a0e04fb commit 827e382

File tree

7 files changed

+69
-56
lines changed

7 files changed

+69
-56
lines changed

lib/src/model/analysis/analysis_controller.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,7 @@ class AnalysisController extends AsyncNotifier<AnalysisState>
171171
switch (options) {
172172
case ArchivedGame(:final gameId):
173173
{
174-
archivedGame = await ref.read(archivedGameProvider(gameId).future);
174+
archivedGame = await ref.watch(archivedGameProvider(gameId).future);
175175
_variant = archivedGame!.meta.variant;
176176
if (!_variant.isReadSupported) {
177177
throw UnsupportedVariantException(_variant, gameId);

lib/src/model/analysis/retro_controller.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ class RetroController extends AsyncNotifier<RetroState> with EngineEvaluationMix
109109

110110
socketClient = ref.watch(socketPoolProvider).open(AnalysisController.socketUri);
111111

112-
_game = await ref.read(archivedGameProvider(options.id).future);
112+
_game = await ref.watch(archivedGameProvider(options.id).future);
113113

114114
if (engineSupportedVariants.contains(_game.meta.variant) == false) {
115115
throw Exception('Variant ${_game.meta.variant} is not supported for retro mode');

lib/src/model/game/game_repository.dart

Lines changed: 30 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import 'package:lichess_mobile/src/model/common/id.dart';
66
import 'package:lichess_mobile/src/model/common/perf.dart';
77
import 'package:lichess_mobile/src/model/game/exported_game.dart';
88
import 'package:lichess_mobile/src/model/game/game_filter.dart';
9+
import 'package:lichess_mobile/src/model/game/game_storage.dart';
910
import 'package:lichess_mobile/src/model/game/playable_game.dart';
1011
import 'package:lichess_mobile/src/network/aggregator.dart';
1112
import 'package:lichess_mobile/src/network/http.dart';
@@ -14,28 +15,42 @@ import 'package:lichess_mobile/src/network/http.dart';
1415
final gameRepositoryProvider = Provider<GameRepository>((ref) {
1516
final client = ref.watch(lichessClientProvider);
1617
final aggregator = ref.watch(aggregatorProvider);
17-
return GameRepository(client, aggregator);
18+
final gameStorage = ref.watch(gameStorageProvider.future);
19+
return GameRepository(client, aggregator, gameStorage);
1820
}, name: 'GameRepositoryProvider');
1921

2022
class GameRepository {
21-
const GameRepository(this.client, this.aggregator);
23+
const GameRepository(this.client, this.aggregator, this.storage);
2224

2325
final LichessClient client;
2426
final Aggregator aggregator;
27+
final Future<GameStorage> storage;
2528

26-
Future<ExportedGame> getGame(GameId id, {bool withBookmarked = false}) {
27-
return client.readJson(
28-
Uri(
29-
path: '/game/export/$id',
30-
queryParameters: {
31-
'clocks': '1',
32-
'accuracy': '1',
33-
if (withBookmarked) 'withBookmarked': '1',
34-
},
35-
),
36-
headers: {'Accept': 'application/json'},
37-
mapper: (json) => ExportedGame.fromServerJson(json, withBookmarked: withBookmarked),
38-
);
29+
/// Fetches a game from lichess API, or from local storage if there is no connectivity.
30+
Future<ExportedGame> getGame(GameId id, {bool withBookmarked = false}) async {
31+
try {
32+
return await client.readJson(
33+
Uri(
34+
path: '/game/export/$id',
35+
queryParameters: {
36+
'clocks': '1',
37+
'accuracy': '1',
38+
if (withBookmarked) 'withBookmarked': '1',
39+
},
40+
),
41+
headers: {'Accept': 'application/json'},
42+
mapper: (json) => ExportedGame.fromServerJson(json, withBookmarked: withBookmarked),
43+
);
44+
} on ServerException {
45+
rethrow;
46+
// Catches other exceptions like no connectivity
47+
} on Exception {
48+
final storedGame = await (await storage).fetch(gameId: id);
49+
if (storedGame != null) {
50+
return storedGame;
51+
}
52+
throw Exception('Game $id cannot be found in local storage.');
53+
}
3954
}
4055

4156
Future<void> requestServerAnalysis(GameId id) async {

lib/src/model/game/game_repository_providers.dart

Lines changed: 3 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,27 +4,13 @@ import 'package:lichess_mobile/src/model/auth/auth_controller.dart';
44
import 'package:lichess_mobile/src/model/common/id.dart';
55
import 'package:lichess_mobile/src/model/game/exported_game.dart';
66
import 'package:lichess_mobile/src/model/game/game_repository.dart';
7-
import 'package:lichess_mobile/src/model/game/game_storage.dart';
87

9-
/// Fetches a game from the server or from the local storage if not available online.
108
final archivedGameProvider = FutureProvider.autoDispose.family<ExportedGame, GameId>((
119
Ref ref,
1210
GameId id,
13-
) async {
14-
ExportedGame game;
15-
try {
16-
final isLoggedIn = ref.watch(isLoggedInProvider);
17-
game = await ref.read(gameRepositoryProvider).getGame(id, withBookmarked: isLoggedIn);
18-
} catch (_) {
19-
final gameStorage = await ref.watch(gameStorageProvider.future);
20-
final storedGame = await gameStorage.fetch(gameId: id);
21-
if (storedGame != null) {
22-
game = storedGame;
23-
} else {
24-
throw Exception('Game $id not found in local storage.');
25-
}
26-
}
27-
return game;
11+
) {
12+
final isLoggedIn = ref.watch(isLoggedInProvider);
13+
return ref.read(gameRepositoryProvider).getGame(id, withBookmarked: isLoggedIn);
2814
}, name: 'ArchivedGameProvider');
2915

3016
final gamesByIdProvider = FutureProvider.autoDispose.family<IList<LightExportedGame>, ISet<GameId>>(

lib/src/network/http.dart

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -840,8 +840,8 @@ extension ClientRefExtension on Ref {
840840
///
841841
/// This is primarily used for caching network requests in a [FutureProvider].
842842
///
843-
/// If [fn] throws with a [SocketException], the provider is not kept alive, this
844-
/// allows to retry the request later.
843+
/// If [fn] throws with a [ServerException], the provider is kept alive as we don't want to retry
844+
/// server errors immediately.
845845
Future<U> withClientCacheFor<U>(Future<U> Function(LichessClient) fn, Duration duration) async {
846846
final link = keepAlive();
847847
final timer = Timer(duration, link.close);
@@ -851,10 +851,9 @@ extension ClientRefExtension on Ref {
851851
});
852852
try {
853853
return await fn(client);
854-
} on SocketException catch (_) {
855-
link.close();
854+
} on ServerException {
856855
rethrow;
857-
} on ClientException catch (_) {
856+
} on Exception {
858857
link.close();
859858
rethrow;
860859
}
@@ -864,8 +863,8 @@ extension ClientRefExtension on Ref {
864863
///
865864
/// This is primarily used for caching network requests in a [FutureProvider].
866865
///
867-
/// If [fn] throws with a [SocketException], the provider is not kept alive, this
868-
/// allows to retry the request later.
866+
/// If [fn] throws with a [ServerException], the provider is kept alive as we don't want to retry
867+
/// server errors immediately.
869868
Future<U> withAggregatorCacheFor<U>(
870869
Future<U> Function(LichessClient, Aggregator) fn,
871870
Duration duration,
@@ -879,10 +878,9 @@ extension ClientRefExtension on Ref {
879878
});
880879
try {
881880
return await fn(client, aggregator);
882-
} on SocketException catch (_) {
883-
link.close();
881+
} on ServerException {
884882
rethrow;
885-
} on ClientException catch (_) {
883+
} on Exception {
886884
link.close();
887885
rethrow;
888886
}

lib/src/view/puzzle/puzzle_screen.dart

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import 'package:lichess_mobile/src/model/analysis/analysis_controller.dart';
1111
import 'package:lichess_mobile/src/model/auth/auth_controller.dart';
1212
import 'package:lichess_mobile/src/model/common/chess.dart';
1313
import 'package:lichess_mobile/src/model/common/id.dart';
14-
import 'package:lichess_mobile/src/model/game/game_repository_providers.dart';
14+
import 'package:lichess_mobile/src/model/game/game_repository.dart';
1515
import 'package:lichess_mobile/src/model/puzzle/puzzle.dart';
1616
import 'package:lichess_mobile/src/model/puzzle/puzzle_angle.dart';
1717
import 'package:lichess_mobile/src/model/puzzle/puzzle_controller.dart';
@@ -45,6 +45,7 @@ import 'package:lichess_mobile/src/widgets/adaptive_choice_picker.dart';
4545
import 'package:lichess_mobile/src/widgets/board.dart';
4646
import 'package:lichess_mobile/src/widgets/bottom_bar.dart';
4747
import 'package:lichess_mobile/src/widgets/buttons.dart';
48+
import 'package:lichess_mobile/src/widgets/feedback.dart';
4849
import 'package:lichess_mobile/src/widgets/list.dart';
4950
import 'package:lichess_mobile/src/widgets/pgn.dart';
5051
import 'package:lichess_mobile/src/widgets/settings.dart';
@@ -778,18 +779,26 @@ class _BottomBarState extends ConsumerState<_BottomBar> {
778779
makeLabel: (context) =>
779780
Text(context.l10n.puzzleFromGameLink(puzzleState.puzzle.game.id.value)),
780781
onPressed: () async {
781-
final game = await ref.read(archivedGameProvider(puzzleState.puzzle.game.id).future);
782-
if (context.mounted) {
783-
Navigator.of(context).push(
784-
AnalysisScreen.buildRoute(
785-
context,
786-
AnalysisOptions.archivedGame(
787-
orientation: puzzleState.pov,
788-
gameId: game.id,
789-
initialMoveCursor: puzzleState.puzzle.puzzle.initialPly + 1,
782+
try {
783+
final game = await ref
784+
.read(gameRepositoryProvider)
785+
.getGame(puzzleState.puzzle.game.id);
786+
if (context.mounted) {
787+
Navigator.of(context).push(
788+
AnalysisScreen.buildRoute(
789+
context,
790+
AnalysisOptions.archivedGame(
791+
orientation: puzzleState.pov,
792+
gameId: game.id,
793+
initialMoveCursor: puzzleState.puzzle.puzzle.initialPly + 1,
794+
),
790795
),
791-
),
792-
);
796+
);
797+
}
798+
} catch (_) {
799+
if (context.mounted) {
800+
showSnackBar(context, 'Could not load the game', type: SnackBarType.error);
801+
}
793802
}
794803
},
795804
),

test/view/game/game_screen_test.dart

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,10 @@ import '../../test_provider_scope.dart';
3838
final client = MockClient((request) {
3939
if (request.url.path == '/api/board/seek') {
4040
return mockResponse('ok', 200);
41+
} else if (request.url.path == '/game/export/CCW6EEru') {
42+
return mockResponse('''
43+
{"id":"CCW6EEru","rated":true,"source":"lobby","variant":"standard","speed":"bullet","perf":"bullet","createdAt":1706185945680,"lastMoveAt":1706186170504,"status":"resign","players":{"white":{"user":{"name":"veloce","id":"veloce"},"rating":1789,"ratingDiff":9},"black":{"user":{"name":"chabrot","id":"chabrot"},"rating":1810,"ratingDiff":-9}},"winner":"white","opening":{"eco":"C52","name":"Italian Game: Evans Gambit, Main Line","ply":10},"moves":"e4 e5 Nf3 Nc6 Bc4 Bc5 b4 Bxb4 c3 Ba5 d4 Bb6 Ba3 Nf6 Qb3 d6 Bxf7+ Kf8 O-O Qe7 Nxe5 Nxe5 dxe5 Be6 Bxe6 Nxe4 Re1 Nc5 Bxc5 Bxc5 Qxb7 Re8 Bh3 dxe5 Qf3+ Kg8 Nd2 Rf8 Qd5+ Rf7 Be6 Qxe6 Qxe6","clocks":[12003,12003,11883,11811,11683,11379,11307,11163,11043,11043,10899,10707,10155,10483,10019,9995,9635,9923,8963,8603,7915,8283,7763,7459,7379,6083,6587,5819,6363,5651,6075,5507,5675,4803,5059,4515,4547,3555,3971,3411,3235,3123,3120,2742],"clock":{"initial":120,"increment":1,"totalTime":160}}
44+
''', 200);
4145
}
4246
return mockResponse('', 404);
4347
});
@@ -808,6 +812,7 @@ void main() {
808812
find.widgetWithText(AppBar, 'Analysis board'),
809813
findsOneWidget,
810814
); // analysis screen is now open
815+
expect(find.byType(Chessboard), findsOneWidget);
811816
expect(find.byKey(const Key('e6-whitequeen')), findsOneWidget);
812817
expect(find.byKey(const Key('d5-lastMove')), findsOneWidget);
813818
expect(find.byKey(const Key('e6-lastMove')), findsOneWidget);

0 commit comments

Comments
 (0)