From c41d1b9d58f34f8ec79c4c706a0157307a2892b1 Mon Sep 17 00:00:00 2001 From: realth000 Date: Thu, 22 Aug 2024 00:38:25 +0800 Subject: [PATCH] refactor(*): Migrate to explicit exceptions: part 2 --- lib/exceptions/exceptions.dart | 10 ++ lib/features/chat/bloc/chat_bloc.dart | 21 +-- lib/features/chat/bloc/chat_history_bloc.dart | 23 +-- lib/features/chat/exceptions/exceptions.dart | 9 - .../chat/repository/chat_repository.dart | 59 +++---- lib/features/editor/bloc/emoji_bloc.dart | 17 +- .../editor/exceptions/exceptions.dart | 14 -- .../editor/repository/editor_repository.dart | 34 ++-- lib/features/forum/bloc/forum_bloc.dart | 102 +++++------ .../forum/repository/forum_repository.dart | 27 ++- .../bloc/latest_thread_bloc.dart | 31 ++-- .../repository/latest_thread_repository.dart | 21 +-- .../my_thread/bloc/my_thread_bloc.dart | 158 +++++++++--------- .../repository/my_thread_repository.dart | 21 +-- 14 files changed, 256 insertions(+), 291 deletions(-) delete mode 100644 lib/features/chat/exceptions/exceptions.dart delete mode 100644 lib/features/editor/exceptions/exceptions.dart diff --git a/lib/exceptions/exceptions.dart b/lib/exceptions/exceptions.dart index 5f5fc00b..02dae444 100644 --- a/lib/exceptions/exceptions.dart +++ b/lib/exceptions/exceptions.dart @@ -143,3 +143,13 @@ final class LogoutFormHashNotFoundException extends AppException @MappableClass() final class LogoutFailedException extends AppException with LogoutFailedExceptionMappable {} + +/// Document contains chat data not found. +@MappableClass() +final class ChatDataDocumentNotFoundException extends AppException + with ChatDataDocumentNotFoundExceptionMappable {} + +/// Failed to load emoji +@MappableClass() +final class EmojiLoadFailedException extends AppException + with EmojiLoadFailedExceptionMappable {} diff --git a/lib/features/chat/bloc/chat_bloc.dart b/lib/features/chat/bloc/chat_bloc.dart index 5b500747..72d76f43 100644 --- a/lib/features/chat/bloc/chat_bloc.dart +++ b/lib/features/chat/bloc/chat_bloc.dart @@ -3,8 +3,6 @@ import 'dart:async'; import 'package:collection/collection.dart'; import 'package:dart_mappable/dart_mappable.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:tsdm_client/exceptions/exceptions.dart'; -import 'package:tsdm_client/features/chat/exceptions/exceptions.dart'; import 'package:tsdm_client/features/chat/models/models.dart'; import 'package:tsdm_client/features/chat/repository/chat_repository.dart'; import 'package:tsdm_client/utils/logger.dart'; @@ -12,7 +10,6 @@ import 'package:universal_html/html.dart' as uh; part 'chat_bloc.mapper.dart'; part 'chat_event.dart'; - part 'chat_state.dart'; /// Emit @@ -37,16 +34,14 @@ final class ChatBloc extends Bloc with LoggerMixin { _Emit emit, ) async { emit(state.copyWith(status: ChatStatus.loading)); - try { - final document = await _chatRepository.fetchChat(event.uid); - _updateState(document, emit); - } on HttpRequestFailedException catch (e) { - error('failed to fetch chat data: $e'); - emit(state.copyWith(status: ChatStatus.failure)); - } on ChatDataDocumentNotFoundException catch (e) { - error('failed to fetch chat data: $e'); - emit(state.copyWith(status: ChatStatus.failure)); - } + + await await _chatRepository.fetchChat(event.uid).match( + (e) { + handle(e); + emit(state.copyWith(status: ChatStatus.failure)); + }, + (v) async => _updateState(v, emit), + ).run(); } void _updateState(uh.Document document, _Emit emit) { diff --git a/lib/features/chat/bloc/chat_history_bloc.dart b/lib/features/chat/bloc/chat_history_bloc.dart index 90e5f1be..53738971 100644 --- a/lib/features/chat/bloc/chat_history_bloc.dart +++ b/lib/features/chat/bloc/chat_history_bloc.dart @@ -2,7 +2,6 @@ import 'dart:async'; import 'package:bloc/bloc.dart'; import 'package:dart_mappable/dart_mappable.dart'; -import 'package:tsdm_client/exceptions/exceptions.dart'; import 'package:tsdm_client/extensions/string.dart'; import 'package:tsdm_client/features/authentication/repository/models/models.dart'; import 'package:tsdm_client/features/chat/models/models.dart'; @@ -37,16 +36,18 @@ final class ChatHistoryBloc extends Bloc } else { emit(state.copyWith(status: ChatHistoryStatus.loadingMore)); } - try { - final document = await _chatRepository.fetchChatHistory( - event.uid, - page: event.page, - ); - await _updateState(document, emit, event.page); - } on HttpRequestFailedException catch (e) { - error('failed to fetch chat history: $e'); - emit(state.copyWith(status: ChatHistoryStatus.failure)); - } + await await _chatRepository + .fetchChatHistory( + event.uid, + page: event.page, + ) + .match( + (e) { + handle(e); + emit(state.copyWith(status: ChatHistoryStatus.failure)); + }, + (v) async => _updateState(v, emit, event.page), + ).run(); } FutureOr _updateState( diff --git a/lib/features/chat/exceptions/exceptions.dart b/lib/features/chat/exceptions/exceptions.dart deleted file mode 100644 index 24b7f641..00000000 --- a/lib/features/chat/exceptions/exceptions.dart +++ /dev/null @@ -1,9 +0,0 @@ -import 'package:dart_mappable/dart_mappable.dart'; - -part 'exceptions.mapper.dart'; - -/// Chat html document not found. -@MappableClass() -final class ChatDataDocumentNotFoundException - with ChatDataDocumentNotFoundExceptionMappable - implements Exception {} diff --git a/lib/features/chat/repository/chat_repository.dart b/lib/features/chat/repository/chat_repository.dart index 4a3f2445..2998ecc0 100644 --- a/lib/features/chat/repository/chat_repository.dart +++ b/lib/features/chat/repository/chat_repository.dart @@ -1,8 +1,8 @@ import 'dart:io' if (dart.libaray.js) 'package:web/web.dart'; +import 'package:fpdart/fpdart.dart'; import 'package:tsdm_client/constants/url.dart'; import 'package:tsdm_client/exceptions/exceptions.dart'; -import 'package:tsdm_client/features/chat/exceptions/exceptions.dart'; import 'package:tsdm_client/instance.dart'; import 'package:tsdm_client/shared/providers/net_client_provider/net_client_provider.dart'; import 'package:universal_html/html.dart' as uh; @@ -14,38 +14,31 @@ final class ChatRepository { const ChatRepository(); /// Fetch the chat history with user [uid]. - /// - /// # Exceptions - /// - /// * **HttpRequestFailedException** if http request failed. - Future fetchChatHistory(String uid, {int? page}) async { - final resp = await getIt.get().get( - formatChatFullHistoryUrl(uid, page: page), - ); - if (resp.statusCode != HttpStatus.ok) { - throw HttpRequestFailedException(resp.statusCode); - } - final document = parseHtmlDocument(resp.data as String); - return document; - } + AsyncEither fetchChatHistory(String uid, {int? page}) => + AsyncEither(() async { + final resp = await getIt.get().get( + formatChatFullHistoryUrl(uid, page: page), + ); + if (resp.statusCode != HttpStatus.ok) { + return left(HttpRequestFailedException(resp.statusCode)); + } + final document = parseHtmlDocument(resp.data as String); + return right(document); + }); /// Fetch the chat history with user [uid]. - /// - /// # Exceptions - /// - /// * **HttpRequestFailedException** if http request failed. - /// * **ChatDataDocumentNotFoundException** if chat html document not found. - Future fetchChat(String uid) async { - final resp = await getIt.get().get(formatChatUrl(uid)); - if (resp.statusCode != HttpStatus.ok) { - throw HttpRequestFailedException(resp.statusCode); - } - final xmlDoc = parseXmlDocument(resp.data as String); - final htmlBodyData = xmlDoc.documentElement?.nodes.firstOrNull?.text; - if (htmlBodyData == null) { - throw ChatDataDocumentNotFoundException(); - } - final document = parseHtmlDocument(htmlBodyData); - return document; - } + AsyncEither fetchChat(String uid) => AsyncEither(() async { + final resp = + await getIt.get().get(formatChatUrl(uid)); + if (resp.statusCode != HttpStatus.ok) { + return left(HttpRequestFailedException(resp.statusCode)); + } + final xmlDoc = parseXmlDocument(resp.data as String); + final htmlBodyData = xmlDoc.documentElement?.nodes.firstOrNull?.text; + if (htmlBodyData == null) { + return left(ChatDataDocumentNotFoundException()); + } + final document = parseHtmlDocument(htmlBodyData); + return right(document); + }); } diff --git a/lib/features/editor/bloc/emoji_bloc.dart b/lib/features/editor/bloc/emoji_bloc.dart index 9afabe3b..56fd275a 100644 --- a/lib/features/editor/bloc/emoji_bloc.dart +++ b/lib/features/editor/bloc/emoji_bloc.dart @@ -1,7 +1,6 @@ import 'package:bloc/bloc.dart'; import 'package:dart_mappable/dart_mappable.dart'; import 'package:tsdm_client/exceptions/exceptions.dart'; -import 'package:tsdm_client/features/editor/exceptions/exceptions.dart'; import 'package:tsdm_client/features/editor/repository/editor_repository.dart'; import 'package:tsdm_client/shared/models/models.dart'; import 'package:tsdm_client/utils/logger.dart'; @@ -32,18 +31,18 @@ final class EmojiBloc extends Bloc with LoggerMixin { EmojiEmitter emit, ) async { emit(state.copyWith(status: EmojiStatus.loading)); - try { - await _editorRepository.loadEmojiFromCacheOrServer(); - emit( + await _editorRepository.loadEmojiFromCacheOrServer().match( + (e) { + handle(e); + emit(state.copyWith(status: EmojiStatus.failure)); + }, + (v) => emit( state.copyWith( status: EmojiStatus.success, emojiGroupList: _editorRepository.emojiGroupList, ), - ); - } on EmojiRelatedException catch (e) { - error('failed to load emoji from cache: $e'); - emit(state.copyWith(status: EmojiStatus.failure)); - } + ), + ).run(); } Future _onEmojiFetchFromServerEvent( diff --git a/lib/features/editor/exceptions/exceptions.dart b/lib/features/editor/exceptions/exceptions.dart deleted file mode 100644 index 7f08833a..00000000 --- a/lib/features/editor/exceptions/exceptions.dart +++ /dev/null @@ -1,14 +0,0 @@ -import 'package:dart_mappable/dart_mappable.dart'; - -part 'exceptions.mapper.dart'; - -/// Basic exception related to emoji. -@MappableClass() -sealed class EmojiRelatedException - with EmojiRelatedExceptionMappable - implements Exception {} - -/// Failed to load emoji -@MappableClass() -final class EmojiLoadFailedException extends EmojiRelatedException - with EmojiLoadFailedExceptionMappable {} diff --git a/lib/features/editor/repository/editor_repository.dart b/lib/features/editor/repository/editor_repository.dart index 2bf45841..18f0fbf6 100644 --- a/lib/features/editor/repository/editor_repository.dart +++ b/lib/features/editor/repository/editor_repository.dart @@ -1,7 +1,8 @@ import 'dart:io' if (dart.libaray.js) 'package:web/web.dart'; +import 'package:fpdart/fpdart.dart'; import 'package:tsdm_client/constants/url.dart'; -import 'package:tsdm_client/features/editor/exceptions/exceptions.dart'; +import 'package:tsdm_client/exceptions/exceptions.dart'; import 'package:tsdm_client/instance.dart'; import 'package:tsdm_client/shared/models/models.dart'; import 'package:tsdm_client/shared/providers/image_cache_provider/image_cache_provider.dart'; @@ -234,21 +235,18 @@ final class EditorRepository with LoggerMixin { /// Only load the emoji info, do not load the emoji image data. /// /// Currently there there is no validation on emoji and emoji groups. - /// - /// # Sealed Exceptions - /// - /// * **[EmojiRelatedException]** when failed to load emoji. - Future loadEmojiFromCacheOrServer() async { - final cacheProvider = getIt.get(); - if (await cacheProvider.validateEmojiCache()) { - // Have valid emoji cache. - emojiGroupList = await cacheProvider.loadEmojiInfo(); - } else { - // Do not have valid emoji cache, reload from server. - final ret = await loadEmojiFromServer(); - if (!ret) { - throw EmojiLoadFailedException(); - } - } - } + AsyncVoidEither loadEmojiFromCacheOrServer() => AsyncVoidEither(() async { + final cacheProvider = getIt.get(); + if (await cacheProvider.validateEmojiCache()) { + // Have valid emoji cache. + emojiGroupList = await cacheProvider.loadEmojiInfo(); + } else { + // Do not have valid emoji cache, reload from server. + final ret = await loadEmojiFromServer(); + if (!ret) { + return left(EmojiLoadFailedException()); + } + } + return rightVoid(); + }); } diff --git a/lib/features/forum/bloc/forum_bloc.dart b/lib/features/forum/bloc/forum_bloc.dart index 86b11239..65a5045e 100644 --- a/lib/features/forum/bloc/forum_bloc.dart +++ b/lib/features/forum/bloc/forum_bloc.dart @@ -1,6 +1,5 @@ import 'package:bloc/bloc.dart'; import 'package:dart_mappable/dart_mappable.dart'; -import 'package:tsdm_client/exceptions/exceptions.dart'; import 'package:tsdm_client/extensions/universal_html.dart'; import 'package:tsdm_client/features/forum/models/models.dart'; import 'package:tsdm_client/features/forum/repository/forum_repository.dart'; @@ -42,18 +41,21 @@ class ForumBloc extends Bloc with LoggerMixin { ForumLoadMoreRequested event, ForumEmitter emit, ) async { - try { - final document = await _forumRepository.fetchForum( - fid: state.fid, - pageNumber: event.pageNumber, - filterState: state.filterState, - ); - emit(await _parseFromDocument(document, event.pageNumber)); - } on HttpRequestFailedException catch (e) { - error('failed to load forum page: fid=${state.fid}, ' - 'pageNumber=${event.pageNumber}: $e'); - emit(state.copyWith(status: ForumStatus.failure)); - } + await await _forumRepository + .fetchForum( + fid: state.fid, + pageNumber: event.pageNumber, + filterState: state.filterState, + ) + .match( + (e) { + handle(e); + error('failed to load forum page: fid=${state.fid}, ' + 'pageNumber=${event.pageNumber}: $e'); + emit(state.copyWith(status: ForumStatus.failure)); + }, + (v) async => emit(await _parseFromDocument(v, event.pageNumber)), + ).run(); } Future _onForumRefreshRequested( @@ -69,16 +71,19 @@ class ForumBloc extends Bloc with LoggerMixin { ), ); - try { - final document = await _forumRepository.fetchForum( - fid: state.fid, - filterState: state.filterState, - ); - emit(await _parseFromDocument(document, 1)); - } on HttpRequestFailedException catch (e) { - error('failed to load forum page: fid=${state.fid}, pageNumber=1 : $e'); - emit(state.copyWith(status: ForumStatus.failure)); - } + await await _forumRepository + .fetchForum( + fid: state.fid, + filterState: state.filterState, + ) + .match( + (e) { + handle(e); + error('failed to load forum page: fid=${state.fid}, pageNumber=1 : $e'); + emit(state.copyWith(status: ForumStatus.failure)); + }, + (v) async => emit(await _parseFromDocument(v, 1)), + ).run(); } Future _onForumJumpPageRequested( @@ -86,18 +91,20 @@ class ForumBloc extends Bloc with LoggerMixin { ForumEmitter emit, ) async { emit(state.copyWith(status: ForumStatus.loading, normalThreadList: [])); - - try { - final document = await _forumRepository.fetchForum( - fid: state.fid, - pageNumber: event.pageNumber, - filterState: state.filterState, - ); - emit(await _parseFromDocument(document, event.pageNumber)); - } on HttpRequestFailedException catch (e) { - error('failed to load forum page: fid=${state.fid}, pageNumber=1 : $e'); - emit(state.copyWith(status: ForumStatus.failure)); - } + await await _forumRepository + .fetchForum( + fid: state.fid, + pageNumber: event.pageNumber, + filterState: state.filterState, + ) + .match( + (e) { + handle(e); + error('failed to load forum page: fid=${state.fid}, pageNumber=1 : $e'); + emit(state.copyWith(status: ForumStatus.failure)); + }, + (v) async => emit(await _parseFromDocument(v, event.pageNumber)), + ).run(); } Future _onForumChangeThreadFilterStateRequested( @@ -111,18 +118,19 @@ class ForumBloc extends Bloc with LoggerMixin { filterState: event.filterState, ), ); - - try { - final document = await _forumRepository.fetchForum( - fid: state.fid, - filterState: state.filterState, - ); - - emit(await _parseFromDocument(document, 1)); - } on HttpRequestFailedException catch (e) { - error('failed to load forum page: fid=${state.fid}, pageNumber=1 : $e'); - emit(state.copyWith(status: ForumStatus.failure)); - } + await await _forumRepository + .fetchForum( + fid: state.fid, + filterState: state.filterState, + ) + .match( + (e) { + handle(e); + error('failed to load forum page: fid=${state.fid}, pageNumber=1 : $e'); + emit(state.copyWith(status: ForumStatus.failure)); + }, + (v) async => emit(await _parseFromDocument(v, 1)), + ).run(); } Future _parseFromDocument( diff --git a/lib/features/forum/repository/forum_repository.dart b/lib/features/forum/repository/forum_repository.dart index fd022243..b8409ea2 100644 --- a/lib/features/forum/repository/forum_repository.dart +++ b/lib/features/forum/repository/forum_repository.dart @@ -1,5 +1,6 @@ import 'dart:io' if (dart.libaray.js) 'package:web/web.dart'; +import 'package:fpdart/fpdart.dart'; import 'package:tsdm_client/exceptions/exceptions.dart'; import 'package:tsdm_client/features/forum/models/models.dart'; import 'package:tsdm_client/instance.dart'; @@ -11,23 +12,21 @@ import 'package:universal_html/parsing.dart'; class ForumRepository { /// Fetch the html document of given [fid] at page [pageNumber]. /// - /// # Exception - /// - /// * **HttpRequestFailedException** if http request failed. - Future fetchForum({ + AsyncEither fetchForum({ required String fid, required FilterState filterState, int pageNumber = 1, - }) async { - final fetchUrl = _formatForumUrl(fid, pageNumber, filterState); - final netClient = getIt.get(); - final resp = await netClient.getUri(fetchUrl); - if (resp.statusCode != HttpStatus.ok) { - throw HttpRequestFailedException(resp.statusCode); - } - final document = parseHtmlDocument(resp.data as String); - return document; - } + }) => + AsyncEither(() async { + final fetchUrl = _formatForumUrl(fid, pageNumber, filterState); + final netClient = getIt.get(); + final resp = await netClient.getUri(fetchUrl); + if (resp.statusCode != HttpStatus.ok) { + return left(HttpRequestFailedException(resp.statusCode)); + } + final document = parseHtmlDocument(resp.data as String); + return right(document); + }); Uri _formatForumUrl( String fid, diff --git a/lib/features/latest_thread/bloc/latest_thread_bloc.dart b/lib/features/latest_thread/bloc/latest_thread_bloc.dart index 683c404c..6737b0a1 100644 --- a/lib/features/latest_thread/bloc/latest_thread_bloc.dart +++ b/lib/features/latest_thread/bloc/latest_thread_bloc.dart @@ -1,6 +1,5 @@ import 'package:bloc/bloc.dart'; import 'package:dart_mappable/dart_mappable.dart'; -import 'package:tsdm_client/exceptions/exceptions.dart'; import 'package:tsdm_client/extensions/string.dart'; import 'package:tsdm_client/extensions/universal_html.dart'; import 'package:tsdm_client/features/latest_thread/models/latest_thread.dart'; @@ -36,11 +35,12 @@ final class LatestThreadBloc extends Bloc if (state.nextPageUrl == null) { return; } - - try { - final document = - await _latestThreadRepository.fetchDocument(state.nextPageUrl!); - final (threadList, nextPageUrl) = _parseThreadList(document); + await _latestThreadRepository.fetchDocument(state.nextPageUrl!).match((e) { + handle(e); + error('failed to load latest thread next page: $e'); + emit(state.copyWith(status: LatestThreadStatus.failed)); + }, (v) { + final (threadList, nextPageUrl) = _parseThreadList(v); emit( state.copyWith( status: LatestThreadStatus.success, @@ -49,10 +49,7 @@ final class LatestThreadBloc extends Bloc nextPageUrl: nextPageUrl, ), ); - } on HttpRequestFailedException catch (e) { - error('failed to load latest thread next page: $e'); - emit(state.copyWith(status: LatestThreadStatus.failed)); - } + }).run(); } Future _onLatestThreadRefreshRequested( @@ -60,9 +57,12 @@ final class LatestThreadBloc extends Bloc LatestThreadEmitter emit, ) async { emit(state.copyWith(status: LatestThreadStatus.loading, threadList: [])); - try { - final document = await _latestThreadRepository.fetchDocument(event.url); - final (threadList, nextPageUrl) = _parseThreadList(document); + await _latestThreadRepository.fetchDocument(event.url).match((e) { + handle(e); + error('failed to load latest thread page: $e'); + emit(state.copyWith(status: LatestThreadStatus.failed)); + }, (v) { + final (threadList, nextPageUrl) = _parseThreadList(v); emit( state.copyWith( status: LatestThreadStatus.success, @@ -71,10 +71,7 @@ final class LatestThreadBloc extends Bloc nextPageUrl: nextPageUrl, ), ); - } on HttpRequestFailedException catch (e) { - error('failed to load latest thread page: $e'); - emit(state.copyWith(status: LatestThreadStatus.failed)); - } + }).run(); } (List?, String? nextPageUrl) _parseThreadList( diff --git a/lib/features/latest_thread/repository/latest_thread_repository.dart b/lib/features/latest_thread/repository/latest_thread_repository.dart index 5b15b633..bc5f54b1 100644 --- a/lib/features/latest_thread/repository/latest_thread_repository.dart +++ b/lib/features/latest_thread/repository/latest_thread_repository.dart @@ -1,5 +1,6 @@ import 'dart:io' if (dart.libaray.js) 'package:web/web.dart'; +import 'package:fpdart/fpdart.dart'; import 'package:tsdm_client/exceptions/exceptions.dart'; import 'package:tsdm_client/instance.dart'; import 'package:tsdm_client/shared/providers/net_client_provider/net_client_provider.dart'; @@ -9,16 +10,12 @@ import 'package:universal_html/parsing.dart'; /// Repository of the latest thread feature. class LatestThreadRepository { /// Fetch html document from [url]. - /// - /// # Exception - /// - /// * **HttpRequestFailedException** when http request failed. - Future fetchDocument(String url) async { - final resp = await getIt.get().get(url); - if (resp.statusCode != HttpStatus.ok) { - throw HttpRequestFailedException(resp.statusCode); - } - final document = parseHtmlDocument(resp.data as String); - return document; - } + AsyncEither fetchDocument(String url) => AsyncEither(() async { + final resp = await getIt.get().get(url); + if (resp.statusCode != HttpStatus.ok) { + return left(HttpRequestFailedException(resp.statusCode)); + } + final document = parseHtmlDocument(resp.data as String); + return right(document); + }); } diff --git a/lib/features/my_thread/bloc/my_thread_bloc.dart b/lib/features/my_thread/bloc/my_thread_bloc.dart index 3e845675..e9293850 100644 --- a/lib/features/my_thread/bloc/my_thread_bloc.dart +++ b/lib/features/my_thread/bloc/my_thread_bloc.dart @@ -1,7 +1,7 @@ import 'package:bloc/bloc.dart'; import 'package:dart_mappable/dart_mappable.dart'; +import 'package:fpdart/fpdart.dart'; import 'package:tsdm_client/constants/url.dart'; -import 'package:tsdm_client/exceptions/exceptions.dart'; import 'package:tsdm_client/extensions/string.dart'; import 'package:tsdm_client/extensions/universal_html.dart'; import 'package:tsdm_client/features/my_thread/models/models.dart'; @@ -11,7 +11,6 @@ import 'package:universal_html/html.dart' as uh; part 'my_thread_bloc.mapper.dart'; part 'my_thread_event.dart'; - part 'my_thread_state.dart'; /// Emitter @@ -47,38 +46,36 @@ final class MyThreadBloc extends Bloc refreshingReply: true, ), ); - // FIXME: If only one tab threw exception. - try { - final data = await Future.wait([ - _myThreadRepository.fetchDocument(myThreadThreadUrl), - _myThreadRepository.fetchDocument(myThreadReplyUrl), - ]); - final d1 = data[0]; - final d2 = data[1]; - final (threadList, threadNextPageUrl) = _parseThreadList(d1); - final (replyList, replyNextPageUrl) = _parseReplyList(d2); - emit( - state.copyWith( - status: MyThreadStatus.success, - threadList: threadList, - threadPageNumber: 1, - nextThreadPageUrl: threadNextPageUrl, - replyList: replyList, - replyPageNumber: 1, - nextReplyPageUrl: replyNextPageUrl, - refreshingThread: false, - refreshingReply: false, - ), - ); - } on HttpRequestFailedException catch (e) { - error('failed to initial my thread page data: $e'); - emit( - state.copyWith( - status: MyThreadStatus.failed, - refreshingThread: false, - refreshingReply: false, - ), - ); + final data = await Future.wait([ + _myThreadRepository.fetchDocument(myThreadThreadUrl).run(), + _myThreadRepository.fetchDocument(myThreadReplyUrl).run(), + ]); + switch ((data[0], data[1])) { + case (Right(value: final v1), Right(value: final v2)): + final (threadList, threadNextPageUrl) = _parseThreadList(v1); + final (replyList, replyNextPageUrl) = _parseReplyList(v2); + emit( + state.copyWith( + status: MyThreadStatus.success, + threadList: threadList, + threadPageNumber: 1, + nextThreadPageUrl: threadNextPageUrl, + replyList: replyList, + replyPageNumber: 1, + nextReplyPageUrl: replyNextPageUrl, + refreshingThread: false, + refreshingReply: false, + ), + ); + default: + error('failed to initial my thread page data: ${data[0]}/${data[1]}'); + emit( + state.copyWith( + status: MyThreadStatus.failed, + refreshingThread: false, + refreshingReply: false, + ), + ); } } @@ -90,26 +87,26 @@ final class MyThreadBloc extends Bloc if (state.nextThreadPageUrl == null) { return; } - try { - final document = - await _myThreadRepository.fetchDocument(state.nextThreadPageUrl!); - final (threadList, nextThreadPageUrl) = _parseThreadList(document); + await _myThreadRepository.fetchDocument(state.nextThreadPageUrl!).match( + (e) { + handle(e); + error('failed to load next page of thread tab: $e'); emit( state.copyWith( - status: MyThreadStatus.success, - threadList: [...state.threadList, ...threadList], - threadPageNumber: state.threadPageNumber + 1, - nextThreadPageUrl: nextThreadPageUrl, + status: MyThreadStatus.failed, ), ); - } on HttpRequestFailedException catch (e) { - error('failed to load next page of thread tab: $e'); + }, (v) { + final (threadList, nextThreadPageUrl) = _parseThreadList(v); emit( state.copyWith( - status: MyThreadStatus.failed, + status: MyThreadStatus.success, + threadList: [...state.threadList, ...threadList], + threadPageNumber: state.threadPageNumber + 1, + nextThreadPageUrl: nextThreadPageUrl, ), ); - } + }).run(); } Future _onMyThreadLoadMoreReplyRequested( @@ -120,26 +117,25 @@ final class MyThreadBloc extends Bloc if (state.nextReplyPageUrl == null) { return; } - try { - final document = - await _myThreadRepository.fetchDocument(state.nextReplyPageUrl!); - final (replyList, nextReplyPageUrl) = _parseReplyList(document); + await _myThreadRepository.fetchDocument(state.nextReplyPageUrl!).match((e) { + handle(e); + error('failed to load next page of reply tab: $e'); emit( state.copyWith( - status: MyThreadStatus.success, - replyList: [...state.replyList, ...replyList], - replyPageNumber: state.replyPageNumber + 1, - nextReplyPageUrl: nextReplyPageUrl, + status: MyThreadStatus.failed, ), ); - } on HttpRequestFailedException catch (e) { - error('failed to load next page of reply tab: $e'); + }, (v) { + final (replyList, nextReplyPageUrl) = _parseReplyList(v); emit( state.copyWith( - status: MyThreadStatus.failed, + status: MyThreadStatus.success, + replyList: [...state.replyList, ...replyList], + replyPageNumber: state.replyPageNumber + 1, + nextReplyPageUrl: nextReplyPageUrl, ), ); - } + }).run(); } Future _onMyThreadRefreshThreadRequested( @@ -147,28 +143,27 @@ final class MyThreadBloc extends Bloc MyThreadEmitter emit, ) async { emit(state.copyWith(refreshingThread: true)); - try { - final document = - await _myThreadRepository.fetchDocument(myThreadThreadUrl); - final (threadList, nextThreadPageUrl) = _parseThreadList(document); + await _myThreadRepository.fetchDocument(myThreadThreadUrl).match((e) { + handle(e); + error('failed to load next page of thread tab: $e'); emit( state.copyWith( - status: MyThreadStatus.success, - threadList: threadList, - threadPageNumber: 1, - nextThreadPageUrl: nextThreadPageUrl, + status: MyThreadStatus.failed, refreshingThread: false, ), ); - } on HttpRequestFailedException catch (e) { - error('failed to load next page of thread tab: $e'); + }, (v) { + final (threadList, nextThreadPageUrl) = _parseThreadList(v); emit( state.copyWith( - status: MyThreadStatus.failed, + status: MyThreadStatus.success, + threadList: threadList, + threadPageNumber: 1, + nextThreadPageUrl: nextThreadPageUrl, refreshingThread: false, ), ); - } + }).run(); } Future _onMyThreadRefreshReplyRequested( @@ -176,28 +171,27 @@ final class MyThreadBloc extends Bloc MyThreadEmitter emit, ) async { emit(state.copyWith(refreshingReply: true)); - try { - final document = - await _myThreadRepository.fetchDocument(myThreadReplyUrl); - final (replyList, nextReplyPageUrl) = _parseReplyList(document); + await _myThreadRepository.fetchDocument(myThreadReplyUrl).match((e) { + handle(e); + error('failed to load next page of reply tab: $e'); emit( state.copyWith( - status: MyThreadStatus.success, - replyList: replyList, - replyPageNumber: 1, - nextReplyPageUrl: nextReplyPageUrl, + status: MyThreadStatus.failed, refreshingReply: false, ), ); - } on HttpRequestFailedException catch (e) { - error('failed to load next page of reply tab: $e'); + }, (v) { + final (replyList, nextReplyPageUrl) = _parseReplyList(v); emit( state.copyWith( - status: MyThreadStatus.failed, + status: MyThreadStatus.success, + replyList: replyList, + replyPageNumber: 1, + nextReplyPageUrl: nextReplyPageUrl, refreshingReply: false, ), ); - } + }).run(); } (List, String? nextPageurl) _parseThreadList(uh.Document document) { diff --git a/lib/features/my_thread/repository/my_thread_repository.dart b/lib/features/my_thread/repository/my_thread_repository.dart index 751af0d5..69e0c65b 100644 --- a/lib/features/my_thread/repository/my_thread_repository.dart +++ b/lib/features/my_thread/repository/my_thread_repository.dart @@ -1,5 +1,6 @@ import 'dart:io' if (dart.libaray.js) 'package:web/web.dart'; +import 'package:fpdart/fpdart.dart'; import 'package:tsdm_client/exceptions/exceptions.dart'; import 'package:tsdm_client/instance.dart'; import 'package:tsdm_client/shared/providers/net_client_provider/net_client_provider.dart'; @@ -9,16 +10,12 @@ import 'package:universal_html/parsing.dart'; /// Repository of MyThread. class MyThreadRepository { /// Fetch html document from [url]. - /// - /// # Exception - /// - /// * **HttpRequestFailedException** when http request failed. - Future fetchDocument(String url) async { - final resp = await getIt.get().get(url); - if (resp.statusCode != HttpStatus.ok) { - throw HttpRequestFailedException(resp.statusCode); - } - final document = parseHtmlDocument(resp.data as String); - return document; - } + AsyncEither fetchDocument(String url) => AsyncEither(() async { + final resp = await getIt.get().get(url); + if (resp.statusCode != HttpStatus.ok) { + return left(HttpRequestFailedException(resp.statusCode)); + } + final document = parseHtmlDocument(resp.data as String); + return right(document); + }); }