fix(cnb): skip local email cache and add safe email/state cache clearing

(cherry picked from commit 0e7f589df58240badc1d2340e9856882aa075b63)
This commit is contained in:
dab246
2025-12-02 11:52:48 +07:00
committed by Dat H. Pham
parent 9d4f6e73d2
commit b3e5ab0df4
21 changed files with 141 additions and 294 deletions
+6 -10
View File
@@ -31,8 +31,8 @@ class SentryManager {
if (!_isSentryAvailable) {
await fallBackRunner();
}
} catch (e, st) {
logError('[SentryManager] Init failed', exception: e, stackTrace: st);
} catch (e) {
logWarning('[SentryManager] Init failed. Exception $e');
await fallBackRunner();
}
}
@@ -90,8 +90,8 @@ class SentryManager {
try {
await Sentry.configureScope((scope) => scope.setUser(user));
log('[SentryManager] User set: ${user.email}');
} catch (e, st) {
logError('[SentryManager] Set user failed', exception: e, stackTrace: st);
} catch (e) {
logWarning('[SentryManager] Set user failed. Exception: $e');
}
}
@@ -101,12 +101,8 @@ class SentryManager {
try {
await Sentry.configureScope((scope) => scope.setUser(null));
log('[SentryManager] User cleared');
} catch (e, st) {
logError(
'[SentryManager] Clear user failed',
exception: e,
stackTrace: st,
);
} catch (e) {
logWarning('[SentryManager] Clear user failed. Exception: $e');
}
}
}
+17 -51
View File
@@ -448,7 +448,7 @@ abstract class BaseController extends GetxController
return;
}
await clearAllMailDataCached();
await cachingManager.clearMailDataCached();
_isFcmEnabled = _isFcmActivated(session, accountId);
if (isAuthenticatedWithOidc) {
@@ -503,11 +503,10 @@ abstract class BaseController extends GetxController
}
Future<void> _handleDeleteFCMAndClearData() async {
await Future.wait([
if (_isFcmEnabled)
_handleDeleteFCMRegistration(),
clearAllData(),
]);
if (_isFcmEnabled) {
await _handleDeleteFCMRegistration();
}
clearAllData();
}
Future<void> _handleDeleteFCMRegistration() async {
@@ -560,57 +559,24 @@ abstract class BaseController extends GetxController
removeAllPageAndGoToLogin();
}
Future<void> clearAllMailDataCached() async {
try {
await cachingManager.clearMailDataCached();
if (PlatformInfo.isMobile) {
await cachingManager.clearAllFileInStorage();
}
} catch (e) {
logError('BaseController::clearAllMailDataCached:Exception = $e');
}
}
Future<void> clearAllData() async {
try {
if (isAuthenticatedWithOidc) {
await _clearOidcAuthData();
} else {
await _clearBasicAuthData();
}
await Future.wait([
if (isAuthenticatedWithOidc)
deleteCredentialInteractor.execute()
else
deleteAuthorityOidcInteractor.execute(),
cachingManager.clearAll(),
languageCacheManager.removeLanguage(),
]);
authorizationInterceptors.clear();
authorizationIsolateInterceptors.clear();
await cachingManager.closeHive();
} catch (e) {
logWarning('BaseController::clearAllData:Exception = $e');
logWarning('BaseController::clearAllData: Cannot clear all data: $e');
}
}
Future<void> _clearBasicAuthData() async {
await Future.wait([
deleteCredentialInteractor.execute(),
cachingManager.clearAll(),
languageCacheManager.removeLanguage(),
]);
if (PlatformInfo.isMobile) {
await cachingManager.clearAllFileInStorage();
}
authorizationInterceptors.clear();
authorizationIsolateInterceptors.clear();
await cachingManager.closeHive();
}
Future<void> _clearOidcAuthData() async {
await Future.wait([
deleteAuthorityOidcInteractor.execute(),
cachingManager.clearAll(),
languageCacheManager.removeLanguage(),
]);
if (PlatformInfo.isMobile) {
await cachingManager.clearAllFileInStorage();
}
authorizationIsolateInterceptors.clear();
authorizationInterceptors.clear();
await cachingManager.closeHive();
}
int getMinInputLengthAutocomplete({
required Session session,
required AccountId accountId,
@@ -11,7 +11,7 @@ class UpgradeHiveDatabaseStepsV13 extends UpgradeDatabaseSteps {
@override
Future<void> onUpgrade(int oldVersion, int newVersion) async {
if (oldVersion > 0 && oldVersion < newVersion && newVersion == 13) {
await _cachingManager.clearMailboxCache();
await _cachingManager.clearMailboxAndStateCache();
}
}
}
+91 -135
View File
@@ -1,14 +1,9 @@
import 'package:core/utils/app_logger.dart';
import 'package:core/utils/file_utils.dart';
import 'package:core/utils/platform_info.dart';
import 'package:fcm/model/type_name.dart';
import 'package:jmap_dart_client/jmap/account_id.dart';
import 'package:jmap_dart_client/jmap/core/user_name.dart';
import 'package:model/extensions/account_id_extensions.dart';
import 'package:tmail_ui_user/features/caching/clients/hive_cache_version_client.dart';
import 'package:tmail_ui_user/features/caching/config/hive_cache_config.dart';
import 'package:tmail_ui_user/features/caching/manager/session_cache_manger.dart';
import 'package:tmail_ui_user/features/caching/utils/cache_utils.dart';
import 'package:tmail_ui_user/features/caching/utils/caching_constants.dart';
import 'package:tmail_ui_user/features/cleanup/data/local/recent_login_url_cache_manager.dart';
import 'package:tmail_ui_user/features/cleanup/data/local/recent_login_username_cache_manager.dart';
@@ -20,13 +15,11 @@ import 'package:tmail_ui_user/features/login/data/local/oidc_configuration_cache
import 'package:tmail_ui_user/features/login/data/local/token_oidc_cache_manager.dart';
import 'package:tmail_ui_user/features/mailbox/data/local/mailbox_cache_manager.dart';
import 'package:tmail_ui_user/features/mailbox/data/local/state_cache_manager.dart';
import 'package:tmail_ui_user/features/mailbox/data/model/state_type.dart';
import 'package:tmail_ui_user/features/offline_mode/manager/new_email_cache_manager.dart';
import 'package:tmail_ui_user/features/offline_mode/manager/opened_email_cache_manager.dart';
import 'package:tmail_ui_user/features/offline_mode/manager/sending_email_cache_manager.dart';
import 'package:tmail_ui_user/features/push_notification/data/keychain/keychain_sharing_manager.dart';
import 'package:tmail_ui_user/features/push_notification/data/local/fcm_cache_manager.dart';
import 'package:tmail_ui_user/features/push_notification/presentation/extensions/type_name_extension.dart';
import 'package:tmail_ui_user/features/thread/data/local/email_cache_manager.dart';
class CachingManager {
@@ -73,76 +66,108 @@ class CachingManager {
);
Future<void> clearAll() async {
await Future.wait([
clearMailDataCached(),
_fcmCacheManager.clear(),
_accountCacheManager.clear(),
if (PlatformInfo.isIOS)
_keychainSharingManager.delete()
], eagerError: true);
try {
await Future.wait([
clearMailDataCached(),
clearAccountDataCached(),
], eagerError: true);
} catch (e) {
logWarning('CachingManager::clearAll: Cannot clear all cache: $e');
}
}
Future<void> clearMailDataCached() async {
await Future.wait([
_stateCacheManager.clear(),
_mailboxCacheManager.clear(),
_emailCacheManager.clear(),
if (PlatformInfo.isMobile)
...[
_sessionCacheManager.clear(),
_newEmailCacheManager.clear(),
_openedEmailCacheManager.clear(),
_sendingEmailCacheManager.clearAllSendingEmails(),
],
], eagerError: true);
try {
await Future.wait([
_stateCacheManager.clear(),
_mailboxCacheManager.clear(),
_emailCacheManager.clear(),
_clearFCMStateCache(),
if (PlatformInfo.isMobile)
...[
clearDetailedEmailCache(),
_sendingEmailCacheManager.clear(),
_sessionCacheManager.clear(),
],
if (PlatformInfo.isIOS)
_keychainSharingManager.delete(),
], eagerError: true);
} catch (e) {
logWarning('CachingManager::clearMailDataCached: Cannot clear mail data cache: $e');
}
}
Future<void> clearEmailAndStateCache({AccountId? accountId, UserName? userName}) {
log('CachingManager::clearEmailAndStateCache:userName=$userName');
final hasUserContext = accountId != null && userName != null;
return Future.wait([
if (hasUserContext)
..._clearEmailAndStateWithUser(accountId, userName)
else
..._clearEmailAndStateWithoutUser(),
if (PlatformInfo.isMobile)
clearFCMEmailStateCache(accountId: accountId, userName: userName),
]);
Future<void> clearAccountDataCached() async {
try {
await Future.wait([
_accountCacheManager.clear(),
_oidcConfigurationCacheManager.clear(),
_tokenOidcCacheManager.clear(),
_authenticationInfoCacheManager.clear(),
], eagerError: true);
} catch (e) {
logWarning('CachingManager::clearAccountDataCached: Cannot clear account data cache: $e');
}
}
Future<void> clearDetailedEmailCache({AccountId? accountId, UserName? userName}) {
log('CachingManager::clearDetailedEmailCache:userName=$userName');
final hasUserContext = accountId != null && userName != null;
return Future.wait([
if (hasUserContext)
..._clearDetailedEmailWithUser(accountId, userName)
else
..._clearDetailedEmailWithoutUser(),
]);
Future<void> clearAllEmailAndStateCache() async {
try {
await Future.wait([
_emailCacheManager.clear(),
_stateCacheManager.clear(),
_clearFCMStateCache(),
if (PlatformInfo.isMobile)
clearDetailedEmailCache(),
]);
} catch (e) {
logWarning('CachingManager::_clearEmailAndStateCache: Cannot clear email and state cache: $e');
}
}
Future<void> clearAllEmailAndStateCache({AccountId? accountId, UserName? userName}) {
log('CachingManager::clearAllEmailAndStateCache:userName=$userName');
return Future.wait([
clearEmailAndStateCache(accountId: accountId, userName: userName),
if (PlatformInfo.isMobile)
clearDetailedEmailCache(accountId: accountId, userName: userName),
]);
Future<void> clearDetailedEmailCache() async {
try {
await Future.wait([
_newEmailCacheManager.clear(),
_openedEmailCacheManager.clear(),
_clearEmailContentFileInStorage(),
]);
} catch (e) {
logWarning('CachingManager::clearDetailedEmailCache: Cannot clear detailed email cache: $e');
}
}
Future<void> clearMailboxCache() {
return Future.wait([
_stateCacheManager.deleteContainKey(
StateType.mailbox.getTupleKeyStoredWithoutAccount(),
),
_mailboxCacheManager.clear(),
], eagerError: true);
Future<void> _clearEmailContentFileInStorage() async {
try {
await _fileUtils.removeFolder(
CachingConstants.openedEmailContentFolderName,
);
await _fileUtils.removeFolder(
CachingConstants.newEmailsContentFolderName,
);
} catch (e) {
logWarning(
'CachingManager::_clearEmailContentFileInStorage: Cannot clear file in storage: $e',
);
}
}
Future<void> _clearFCMStateCache() async {
try {
await _fcmCacheManager.clear();
} catch (e) {
logWarning('CachingManager::_clearFCMStateCache: Cannot clear fcm state cache: $e');
}
}
Future<void> clearMailboxAndStateCache() async {
try {
await Future.wait([
_mailboxCacheManager.clear(),
_stateCacheManager.clear(),
], eagerError: true);
} catch (e) {
logWarning('CachingManager::clearMailboxCache: Cannot clear mailbox cache: $e');
}
}
Future<bool> storeCacheVersion(int newVersion) async {
@@ -157,75 +182,6 @@ class CachingManager {
Future<void> closeHive({bool isolated = true}) =>
HiveCacheConfig.instance.closeHive(isolated: isolated);
Future<void> clearAllFileInStorage() async {
await _fileUtils.removeFolder(CachingConstants.openedEmailContentFolderName);
await _fileUtils.removeFolder(CachingConstants.newEmailsContentFolderName);
}
Future<void> clearFCMEmailStateCache({
AccountId? accountId,
UserName? userName,
}) async {
final hasUserContext = accountId != null && userName != null;
if (hasUserContext) {
await _fcmCacheManager.deleteByKey(
TypeName.emailDelivery.getTupleKeyStored(accountId, userName),
);
await _fcmCacheManager.deleteByKey(
TypeName.emailType.getTupleKeyStored(accountId, userName),
);
} else {
await _fcmCacheManager.deleteContainKey(
TypeName.emailDelivery.getTupleKeyStoredWithoutAccount(),
);
await _fcmCacheManager.deleteContainKey(
TypeName.emailType.getTupleKeyStoredWithoutAccount(),
);
}
}
List<Future<void>> _clearEmailAndStateWithUser(
AccountId accountId,
UserName userName,
) {
final emailKey = TupleKey(accountId.asString, userName.value).encodeKey;
final stateKey = StateType.email.getTupleKeyStored(accountId, userName);
return [
_emailCacheManager.deleteContainKey(emailKey),
_stateCacheManager.deleteByKey(stateKey),
];
}
List<Future<void>> _clearEmailAndStateWithoutUser() {
final stateKey = StateType.email.getTupleKeyStoredWithoutAccount();
return [
_emailCacheManager.clear(),
_stateCacheManager.deleteContainKey(stateKey),
];
}
List<Future<void>> _clearDetailedEmailWithUser(
AccountId accountId,
UserName userName,
) {
final emailKey = TupleKey(accountId.asString, userName.value).encodeKey;
return [
_newEmailCacheManager.deleteContainKey(emailKey),
_openedEmailCacheManager.deleteContainKey(emailKey),
clearAllFileInStorage(),
];
}
List<Future<void>> _clearDetailedEmailWithoutUser() {
return [
_newEmailCacheManager.clear(),
_openedEmailCacheManager.clear(),
clearAllFileInStorage(),
];
}
Future<void> clearLoginRecentData() async {
await Future.wait([
_recentLoginUrlCacheManager.clear(),
@@ -131,7 +131,7 @@ class AuthenticationOIDCDataSourceImpl extends AuthenticationOIDCDataSource {
@override
Future<void> deleteOidcConfiguration() {
return Future.sync(() async {
return await _oidcConfigurationCacheManager.deleteOidcConfiguration();
return await _oidcConfigurationCacheManager.clear();
}).catchError(_cacheExceptionThrower.throwException);
}
@@ -163,7 +163,7 @@ class AuthenticationOIDCDataSourceImpl extends AuthenticationOIDCDataSource {
@override
Future<void> deleteTokenOIDC() {
return Future.sync(() async {
return await _tokenOidcCacheManager.deleteTokenOidc();
return await _tokenOidcCacheManager.clear();
}).catchError(_cacheExceptionThrower.throwException);
}
@@ -24,8 +24,8 @@ class AuthenticationInfoCacheManager extends CacheManagerInteraction {
}
}
Future<void> removeAuthenticationInfo() {
return _authenticationInfoCacheClient.deleteItem(AuthenticationInfoCache.keyCacheValue);
Future<void> clear() {
return _authenticationInfoCacheClient.clearAllData();
}
@override
@@ -43,12 +43,9 @@ class OidcConfigurationCacheManager extends CacheManagerInteraction {
);
}
Future<void> deleteOidcConfiguration() async {
log('OidcConfigurationCacheManager::deleteOidcConfiguration()');
Future<void> clear() async {
await Future.wait([
_oidcConfigurationCacheClient.deleteItem(
CachingConstants.oidcConfigurationCacheKeyName,
),
_oidcConfigurationCacheClient.clearAllData(),
_sharedPreferences.remove(OIDCConstant.keyAuthorityOidc),
]);
}
@@ -24,7 +24,7 @@ class TokenOidcCacheManager extends CacheManagerInteraction {
Future<void> persistOneTokenOidc(TokenOIDC tokenOIDC) async {
log('TokenOidcCacheManager::persistOneTokenOidc(): $tokenOIDC');
await _tokenOidcCacheClient.clearAllData();
await clear();
log('TokenOidcCacheManager::persistOneTokenOidc(): key: ${tokenOIDC.tokenId.uuid}');
log('TokenOidcCacheManager::persistOneTokenOidc(): key\'s hash: ${tokenOIDC.tokenIdHash}');
log('TokenOidcCacheManager::persistOneTokenOidc(): token: ${tokenOIDC.token}');
@@ -32,7 +32,7 @@ class TokenOidcCacheManager extends CacheManagerInteraction {
log('TokenOidcCacheManager::persistOneTokenOidc(): done');
}
Future<void> deleteTokenOidc() async {
Future<void> clear() async {
await _tokenOidcCacheClient.clearAllData();
}
@@ -42,6 +42,6 @@ class CredentialRepositoryImpl extends CredentialRepository {
@override
Future<void> removeAuthenticationInfo() {
return _authenticationInfoCacheManager.removeAuthenticationInfo();
return _authenticationInfoCacheManager.clear();
}
}
@@ -14,7 +14,6 @@ import 'package:get/get.dart';
import 'package:jmap_dart_client/jmap/core/session/session.dart';
import 'package:jmap_dart_client/jmap/core/user_name.dart';
import 'package:model/account/password.dart';
import 'package:model/extensions/session_extension.dart';
import 'package:model/oidc/oidc_configuration.dart';
import 'package:model/oidc/request/oidc_request.dart';
import 'package:model/oidc/response/oidc_response.dart';
@@ -255,10 +254,7 @@ class LoginController extends ReloadableController {
SmartDialog.dismiss();
if (PlatformInfo.isWeb && AppConfig.isForceEmailQueryEnabled) {
await cachingManager.clearAllEmailAndStateCache(
accountId: session.safeAccountId,
userName: session.username
);
await cachingManager.clearAllEmailAndStateCache();
}
popAndPush(
@@ -612,10 +612,7 @@ class MailboxDashBoardController extends ReloadableController
Future<void> onBeforeUnloadBrowserListener(html.Event event) async {
log('MailboxDashBoardController::onBeforeUnloadBrowserListener:event = ${event.runtimeType} | hasComposer = ${twakeAppManager.hasComposer} | isExecutingBeforeReconnect = ${twakeAppManager.isExecutingBeforeReconnect}');
if (PlatformInfo.isWeb) {
await cachingManager.clearAllEmailAndStateCache(
accountId: accountId.value,
userName: sessionCurrent?.username,
);
await cachingManager.clearAllEmailAndStateCache();
}
if (event is html.BeforeUnloadEvent &&
@@ -140,10 +140,7 @@ class ManageAccountDashBoardController extends ReloadableController
@override
Future<void> onBeforeUnloadBrowserListener(html.Event event) async {
if (PlatformInfo.isWeb) {
await cachingManager.clearAllEmailAndStateCache(
accountId: accountId.value,
userName: sessionCurrent?.username,
);
await cachingManager.clearAllEmailAndStateCache();
}
}
@@ -53,7 +53,7 @@ class SendingEmailCacheManager extends CacheManagerInteraction {
return sendingEmailsCache;
}
Future<void> clearAllSendingEmails() => _hiveCacheClient.clearAllData();
Future<void> clear() => _hiveCacheClient.clearAllData();
Future<SendingEmailHiveCache> updateSendingEmail(
AccountId accountId,
@@ -89,11 +89,6 @@ class FCMCacheManager extends CacheManagerInteraction {
]);
}
Future<void> deleteByKey(String key) => _fcmCacheClient.deleteItem(key);
Future<void> deleteContainKey(String key) =>
_fcmCacheClient.clearAllDataContainKey(key);
@override
Future<void> migrateHiveToIsolatedHive() async {
await Future.wait([
@@ -77,5 +77,5 @@ abstract class ThreadDataSource {
Future<PresentationEmail> getEmailById(Session session, AccountId accountId, EmailId emailId, {Properties? properties});
Future<void> clearEmailCacheAndStateCache(AccountId accountId, Session session);
Future<void> clearEmailCacheAndStateCache();
}
@@ -133,12 +133,9 @@ class LocalThreadDataSourceImpl extends ThreadDataSource {
}
@override
Future<void> clearEmailCacheAndStateCache(AccountId accountId, Session session) {
Future<void> clearEmailCacheAndStateCache() {
return Future.sync(() async {
return await _cachingManager.clearAllEmailAndStateCache(
accountId: accountId,
userName: session.username,
);
return await _cachingManager.clearAllEmailAndStateCache();
}).catchError(_exceptionThrower.throwException);
}
}
@@ -142,7 +142,7 @@ class ThreadDataSourceImpl extends ThreadDataSource {
}
@override
Future<void> clearEmailCacheAndStateCache(AccountId accountId, Session session) {
Future<void> clearEmailCacheAndStateCache() {
throw UnimplementedError();
}
}
@@ -148,37 +148,8 @@ class ThreadRepositoryImpl extends ThreadRepository {
EmailFilter? emailFilter,
Properties? propertiesCreated,
}) async* {
final localDataSource = mapDataSource[DataSourceType.local];
final networkDataSource = mapDataSource[DataSourceType.network];
if (localDataSource == null || networkDataSource == null) {
logError(
'ThreadRepositoryImpl::forceQueryAllEmailsForWeb(): '
'Missing required data sources (local or network).',
);
return;
}
List<Email> cachedList = const [];
jmap.State? cachedState;
// Load cached emails
try {
cachedList = await localDataSource.getAllEmailCache(
accountId,
session.username,
inMailboxId: emailFilter?.mailboxId,
sort: sort,
limit: limit,
filterOption: emailFilter?.filterOption,
);
} catch (_) {
logError(
'ThreadRepositoryImpl::forceQueryAllEmailsForWeb(): '
'Failed to load cached emails',
);
}
// Load cached state
try {
cachedState = await stateDataSource.getState(
@@ -187,29 +158,19 @@ class ThreadRepositoryImpl extends ThreadRepository {
StateType.email,
);
} catch (_) {
logError(
logWarning(
'ThreadRepositoryImpl::forceQueryAllEmailsForWeb(): '
'Failed to load cached email state',
);
}
final localResponse = EmailsResponse(
emailList: cachedList,
state: cachedState,
);
log(
'ThreadRepositoryImpl::forceQueryAllEmailsForWeb(): '
'Local cache count = ${cachedList.length}; '
'State = ${cachedState?.value}',
'State cache = ${cachedState?.value}',
);
if (localResponse.hasEmails()) {
yield localResponse;
}
// Query fresh emails from server
final serverResponse = await networkDataSource.getAllEmail(
final serverResponse = await mapDataSource[DataSourceType.network]!.getAllEmail(
session,
accountId,
limit: limit,
@@ -588,13 +549,8 @@ class ThreadRepositoryImpl extends ThreadRepository {
}
@override
Future<void> clearEmailCacheAndStateCache(
AccountId accountId,
Session session,
) => mapDataSource[DataSourceType.local]!.clearEmailCacheAndStateCache(
accountId,
session,
);
Future<void> clearEmailCacheAndStateCache() =>
mapDataSource[DataSourceType.local]!.clearEmailCacheAndStateCache();
@override
Stream<EmailsResponse> loadAllEmailInFolderWithoutCache({
@@ -102,5 +102,5 @@ abstract class ThreadRepository {
StreamController<dartz.Either<Failure, Success>> onProgressController
);
Future<void> clearEmailCacheAndStateCache(AccountId accountId, Session session);
Future<void> clearEmailCacheAndStateCache();
}
@@ -36,7 +36,7 @@ class CleanAndGetEmailsInMailboxInteractor {
try {
yield Right<Failure, Success>(CleanAndGetAllEmailLoading());
await _threadRepository.clearEmailCacheAndStateCache(accountId, session);
await _threadRepository.clearEmailCacheAndStateCache();
yield* _getEmailsInMailboxInteractor.execute(
session,
@@ -480,10 +480,7 @@ class ThreadController extends BaseController with EmailActionController {
void _handleErrorGetAllOrRefreshChangesEmail(Object error, StackTrace stackTrace) async {
logWarning('ThreadController::_handleErrorGetAllOrRefreshChangesEmail():Error: $error');
if (error is CannotCalculateChangesMethodResponseException) {
await cachingManager.clearAllEmailAndStateCache(
accountId: _accountId,
userName: _session?.username,
);
await cachingManager.clearAllEmailAndStateCache();
getAllEmailAction();
} else if (error is MethodLevelErrors) {
if (currentOverlayContext != null && error.message != null) {
@@ -609,10 +606,7 @@ class ThreadController extends BaseController with EmailActionController {
Future<void> refreshAllEmail({bool forceEmailQuery = false}) async {
if (forceEmailQuery) {
await cachingManager.clearAllEmailAndStateCache(
accountId: mailboxDashBoardController.accountId.value,
userName: mailboxDashBoardController.sessionCurrent?.username,
);
await cachingManager.clearAllEmailAndStateCache();
}
if (searchController.isSearchEmailRunning) {