Updated on 2026-08-14
This commit is contained in:
parent
017eece8b7
commit
d96dd19f01
23 changed files with 456 additions and 8 deletions
|
|
@ -0,0 +1,44 @@
|
|||
package com.tangem.data.account.cleaner
|
||||
|
||||
import com.tangem.data.account.store.AccountsResponseStoreFactory
|
||||
import com.tangem.data.account.store.LegacyUserTokensResponseStore
|
||||
import com.tangem.data.common.cache.etag.ETagsStore
|
||||
import com.tangem.domain.common.wallets.UserWalletDataCleaner
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.utils.coroutines.runSuspendCatching
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Clears per-wallet accounts and user tokens caches when wallets are deleted.
|
||||
*
|
||||
* Since [UserWalletId] is derived from the wallet key, re-adding the same wallet reuses the same id. Without this
|
||||
* cleanup the stale accounts response and its ETag survive the deletion, the server answers `304 Not Modified` on
|
||||
* re-add and the cached (non-empty) accounts short-circuit the fetch flow, so public key derivation never runs.
|
||||
*
|
||||
* @property accountsResponseStoreFactory factory owning the per-wallet accounts response stores
|
||||
* @property legacyUserTokensResponseStore legacy per-wallet user tokens store
|
||||
* @property eTagsStore store of caching ETags
|
||||
*/
|
||||
internal class AccountsUserWalletDataCleaner @Inject constructor(
|
||||
private val accountsResponseStoreFactory: AccountsResponseStoreFactory,
|
||||
private val legacyUserTokensResponseStore: LegacyUserTokensResponseStore,
|
||||
private val eTagsStore: ETagsStore,
|
||||
) : UserWalletDataCleaner {
|
||||
|
||||
override suspend fun clear(userWalletIds: List<UserWalletId>) {
|
||||
// Best-effort: isolate failures per store so one failing store does not cancel the others.
|
||||
coroutineScope {
|
||||
launch { clearSafely(store = "accounts response") { accountsResponseStoreFactory.clear(userWalletIds) } }
|
||||
launch { clearSafely(store = "legacy user tokens") { legacyUserTokensResponseStore.clear(userWalletIds) } }
|
||||
launch { clearSafely(store = "ETags") { eTagsStore.clear(userWalletIds) } }
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun clearSafely(store: String, clear: suspend () -> Unit) {
|
||||
runSuspendCatching { clear() }
|
||||
.onFailure { TangemLogger.e("Failed to clear $store store", it) }
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
package com.tangem.data.account.di
|
||||
|
||||
import android.content.Context
|
||||
import com.tangem.data.account.cleaner.AccountsUserWalletDataCleaner
|
||||
import com.tangem.data.account.converter.AccountConverterFactoryContainer
|
||||
import com.tangem.data.account.fetcher.DefaultWalletAccountsFetcher
|
||||
import com.tangem.data.account.repository.DefaultAccountsCRUDRepository
|
||||
|
|
@ -16,6 +17,7 @@ import com.tangem.datasource.local.accounts.AccountTokenMigrationStore
|
|||
import com.tangem.datasource.local.datastore.RuntimeStateStore
|
||||
import com.tangem.domain.account.repository.AccountsCRUDRepository
|
||||
import com.tangem.domain.account.tokens.MainAccountTokensMigration
|
||||
import com.tangem.domain.common.wallets.UserWalletDataCleaner
|
||||
import com.tangem.utils.coroutines.AppCoroutineScope
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import dagger.Module
|
||||
|
|
@ -23,6 +25,7 @@ import dagger.Provides
|
|||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import dagger.multibindings.IntoSet
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
|
|
@ -53,6 +56,10 @@ internal object AccountDataModule {
|
|||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@IntoSet
|
||||
fun provideAccountsUserWalletDataCleaner(impl: AccountsUserWalletDataCleaner): UserWalletDataCleaner = impl
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideWalletAccountsFetcher(impl: DefaultWalletAccountsFetcher): WalletAccountsFetcher = impl
|
||||
|
|
|
|||
|
|
@ -11,8 +11,12 @@ import com.tangem.datasource.api.tangemTech.models.account.GetWalletAccountsResp
|
|||
import com.tangem.datasource.di.NetworkMoshi
|
||||
import com.tangem.datasource.utils.MoshiDataStoreSerializer
|
||||
import com.tangem.utils.coroutines.AppCoroutineScope
|
||||
import com.tangem.utils.coroutines.runSuspendCatching
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
|
@ -56,6 +60,26 @@ internal class AccountsResponseStoreFactory @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the persisted accounts responses for the given [userWalletIds].
|
||||
*
|
||||
* Each wallet owns a separate store file, so they are reset concurrently. The store instances are kept alive
|
||||
* (DataStore forbids multiple instances for the same file), only their content is reset to the default `null`.
|
||||
*
|
||||
* @param userWalletIds the unique identifiers of the user's wallets
|
||||
*/
|
||||
suspend fun clear(userWalletIds: List<UserWalletId>) = coroutineScope {
|
||||
// Best-effort: a failure clearing one wallet must not cancel clearing the others.
|
||||
userWalletIds.forEach { userWalletId ->
|
||||
launch {
|
||||
runSuspendCatching { create(userWalletId).updateData { null } }
|
||||
.onFailure {
|
||||
TangemLogger.e("Failed to clear accounts response for wallet: ${userWalletId.stringValue}", it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
fun getAllStores(): Map<UserWalletId, AccountsResponseStore> = createdDataStores.toMap()
|
||||
|
||||
|
|
|
|||
|
|
@ -25,10 +25,18 @@ internal class LegacyUserTokensResponseStore @Inject constructor(
|
|||
}
|
||||
|
||||
suspend fun clear(userWalletId: UserWalletId) {
|
||||
appPreferencesStore.updateData { preferences ->
|
||||
val key = createPreferencesKey(userWalletId = userWalletId.stringValue)
|
||||
clear(userWalletIds = listOf(userWalletId))
|
||||
}
|
||||
|
||||
preferences.toMutablePreferences().apply { remove(key) }
|
||||
suspend fun clear(userWalletIds: List<UserWalletId>) {
|
||||
if (userWalletIds.isEmpty()) return
|
||||
|
||||
val keys = userWalletIds.map { createPreferencesKey(userWalletId = it.stringValue) }
|
||||
|
||||
appPreferencesStore.updateData { preferences ->
|
||||
preferences.toMutablePreferences().apply {
|
||||
keys.forEach { key -> remove(key) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,57 @@
|
|||
package com.tangem.data.account.cleaner
|
||||
|
||||
import com.tangem.data.account.store.AccountsResponseStoreFactory
|
||||
import com.tangem.data.account.store.LegacyUserTokensResponseStore
|
||||
import com.tangem.data.common.cache.etag.ETagsStore
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import io.mockk.Runs
|
||||
import io.mockk.clearMocks
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.just
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
internal class AccountsUserWalletDataCleanerTest {
|
||||
|
||||
private val accountsResponseStoreFactory: AccountsResponseStoreFactory = mockk()
|
||||
private val legacyUserTokensResponseStore: LegacyUserTokensResponseStore = mockk()
|
||||
private val eTagsStore: ETagsStore = mockk()
|
||||
|
||||
private val cleaner = AccountsUserWalletDataCleaner(
|
||||
accountsResponseStoreFactory = accountsResponseStoreFactory,
|
||||
legacyUserTokensResponseStore = legacyUserTokensResponseStore,
|
||||
eTagsStore = eTagsStore,
|
||||
)
|
||||
|
||||
@BeforeEach
|
||||
fun resetMocks() {
|
||||
clearMocks(accountsResponseStoreFactory, legacyUserTokensResponseStore, eTagsStore)
|
||||
coEvery { accountsResponseStoreFactory.clear(any<List<UserWalletId>>()) } just Runs
|
||||
coEvery { legacyUserTokensResponseStore.clear(any<List<UserWalletId>>()) } just Runs
|
||||
coEvery { eTagsStore.clear(any<List<UserWalletId>>()) } just Runs
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN wallets WHEN clear THEN each store is cleared once with all ids in a single call`() = runTest {
|
||||
// Arrange
|
||||
val ids = listOf(WALLET_A, WALLET_B)
|
||||
|
||||
// Act
|
||||
cleaner.clear(ids)
|
||||
|
||||
// Assert
|
||||
coVerify(exactly = 1) { accountsResponseStoreFactory.clear(ids) }
|
||||
coVerify(exactly = 1) { legacyUserTokensResponseStore.clear(ids) }
|
||||
coVerify(exactly = 1) { eTagsStore.clear(ids) }
|
||||
}
|
||||
|
||||
private companion object {
|
||||
val WALLET_A = UserWalletId("011")
|
||||
val WALLET_B = UserWalletId("022")
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue