Updated on 2026-08-14

This commit is contained in:
Tangem 2025-02-20 14:50:16 +04:00
parent fde7ec8688
commit 111dea42be
11 changed files with 97 additions and 44 deletions

View file

@ -72,14 +72,26 @@ internal class DefaultNetworksStatusesStore(
override suspend fun storeAll(key: UserWalletId, values: Set<NetworkStatus>) {
coroutineScope {
val updatedValues = getSyncOrNull(key).orEmpty()
.addOrReplace(items = values) { prev, new -> prev.network == new.network }
launch { runtimeDataStore.store(key = provideStringKey(key), value = updatedValues) }
launch { storeNetworkStatusInPersistence(userWalletId = key, statuses = updatedValues) }
launch { storeInRuntimeStore(key = key, statuses = values) }
launch { storeInPersistenceStore(userWalletId = key, statuses = values) }
}
}
override suspend fun refresh(key: UserWalletId, networks: Set<Network>) {
val currentStatuses = getSyncOrNull(key).orEmpty()
storeInRuntimeStore(
key = key,
statuses = networks.mapNotNullTo(hashSetOf()) { network ->
val status = currentStatuses.firstOrNull { it.network.id == network.id } ?: return@mapNotNullTo null
status.copy(
value = status.value.copySealed(source = StatusSource.CACHE),
)
},
)
}
/**
* Merge [cachedStatuses] with [runtimeStatuses]
* The resulting set contains statuses from both sets.
@ -108,7 +120,6 @@ internal class DefaultNetworksStatusesStore(
val updatedCachedStatus = when (val status = cached.value) {
is NetworkStatus.NoAccount -> status.copy(source = StatusSource.ONLY_CACHE)
is NetworkStatus.Verified -> status.copy(source = StatusSource.ONLY_CACHE)
is NetworkStatus.Refreshing,
is NetworkStatus.Unreachable,
is NetworkStatus.MissedDerivation,
-> null
@ -121,13 +132,23 @@ internal class DefaultNetworksStatusesStore(
}
}
private suspend fun storeNetworkStatusInPersistence(userWalletId: UserWalletId, statuses: Set<NetworkStatus>) {
private suspend fun storeInRuntimeStore(key: UserWalletId, statuses: Set<NetworkStatus>) {
val updatedValues = getSyncOrNull(key).orEmpty()
.addOrReplace(items = statuses) { prev, new -> prev.network == new.network }
runtimeDataStore.store(key = provideStringKey(key), value = updatedValues)
}
private suspend fun storeInPersistenceStore(userWalletId: UserWalletId, statuses: Set<NetworkStatus>) {
// Converter will return null if the network status is not supported
val newStatuses = NetworkStatusDataModelConverter.convertSet(input = statuses).filterNotNull().toSet()
persistenceDataStore.updateData { storedStatuses ->
storedStatuses.toMutableMap().apply {
this[userWalletId.stringValue] = newStatuses
val updatedValues = this[userWalletId.stringValue].orEmpty()
.addOrReplace(newStatuses) { prev, new -> prev.networkId == new.networkId }
this[userWalletId.stringValue] = updatedValues
}
}
}

View file

@ -16,4 +16,6 @@ interface NetworksStatusesStore {
suspend fun store(key: UserWalletId, value: NetworkStatus)
suspend fun storeAll(key: UserWalletId, values: Set<NetworkStatus>)
suspend fun refresh(key: UserWalletId, networks: Set<Network>)
}

View file

@ -52,17 +52,30 @@ internal class DefaultQuotesStore(
override suspend fun store(response: QuotesResponse) {
coroutineScope {
launch { storeInRuntimeStore(response = response) }
launch {
storeInRuntimeStore(
values = QuoteConverter(isCached = false).convertSet(input = response.quotes.entries),
)
}
launch { storeInPersistenceStore(response = response) }
}
}
override suspend fun storeEmptyQuotes(currenciesIds: Set<CryptoCurrency.RawID>) {
runtimeStore.update(default = emptySet()) { saved ->
val new = currenciesIds.map { Quote.Empty(it) }
storeInRuntimeStore(values = currenciesIds.map(Quote::Empty).toSet())
}
(saved + new).distinctBy { it.rawCurrencyId }.toSet()
}
override suspend fun refresh(currenciesIds: Set<CryptoCurrency.RawID>) {
val currentStatuses = getSync(currenciesIds)
storeInRuntimeStore(
values = currentStatuses.mapTo(hashSetOf()) { quote ->
when (quote) {
is Quote.Empty -> quote
is Quote.Value -> quote.copy(source = StatusSource.CACHE)
}
},
)
}
private suspend fun getCachedQuotes(currenciesIds: Set<CryptoCurrency.RawID>): Set<Quote.Value> {
@ -95,11 +108,9 @@ internal class DefaultQuotesStore(
?: Quote.Empty(currencyId)
}
private suspend fun storeInRuntimeStore(response: QuotesResponse) {
val new = QuoteConverter(isCached = false).convertSet(input = response.quotes.entries)
private suspend fun storeInRuntimeStore(values: Set<Quote>) {
runtimeStore.update(default = emptySet()) { saved ->
(saved + new).distinctBy { it.rawCurrencyId }.toSet()
(saved + values).distinctBy { it.rawCurrencyId }.toSet()
}
}

View file

@ -19,4 +19,6 @@ interface QuotesStore {
/** Store [Quote.Empty] for [currenciesIds] */
suspend fun storeEmptyQuotes(currenciesIds: Set<CryptoCurrency.RawID>)
suspend fun refresh(currenciesIds: Set<CryptoCurrency.RawID>)
}

View file

@ -72,15 +72,26 @@ internal class DefaultStakingBalanceStore(
override suspend fun store(userWalletId: UserWalletId, items: Set<YieldBalanceWrapperDTO>) {
coroutineScope {
launch {
storeInRuntimeStore(
userWalletId = userWalletId,
items = YieldBalanceConverter(isCached = false).convertSet(input = items),
)
updateRuntimeStore(userWalletId = userWalletId) {
YieldBalanceConverter(isCached = false).convertSet(input = items)
}
}
launch { storeInPersistenceStore(userWalletId = userWalletId, items = items) }
}
}
override suspend fun refresh(userWalletId: UserWalletId) {
updateRuntimeStore(userWalletId = userWalletId) { saved ->
saved.mapTo(hashSetOf()) {
when (it) {
is YieldBalance.Data -> it.copy(source = StatusSource.CACHE)
is YieldBalance.Empty -> it.copy(source = StatusSource.CACHE)
is YieldBalance.Error -> it
}
}
}
}
override suspend fun store(
userWalletId: UserWalletId,
integrationId: String,
@ -107,13 +118,16 @@ internal class DefaultStakingBalanceStore(
?.addOrReplace(newBalance) { it.integrationId == integrationId && it.address == address }
?: setOf(newBalance)
storeInRuntimeStore(userWalletId = userWalletId, items = balances)
updateRuntimeStore(userWalletId = userWalletId) { balances }
}
private suspend fun storeInRuntimeStore(userWalletId: UserWalletId, items: Set<YieldBalance>) {
runtimeStore.update(default = emptyMap()) {
it.toMutableMap().apply {
this[userWalletId] = items
private suspend fun updateRuntimeStore(
userWalletId: UserWalletId,
function: (Set<YieldBalance>) -> Set<YieldBalance>,
) {
runtimeStore.update(default = emptyMap()) { saved ->
saved.toMutableMap().apply {
this[userWalletId] = function(this[userWalletId].orEmpty())
}
}
}

View file

@ -26,4 +26,6 @@ interface StakingBalanceStore {
/** Store [item] by [userWalletId], [integrationId] and [address] */
suspend fun store(userWalletId: UserWalletId, integrationId: String, address: String, item: YieldBalanceWrapperDTO)
suspend fun refresh(userWalletId: UserWalletId)
}

View file

@ -402,6 +402,10 @@ internal class DefaultStakingRepository(
cryptoCurrencies: List<CryptoCurrency>,
refresh: Boolean,
) = withContext(dispatchers.io) {
if (refresh) {
stakingBalanceStore.refresh(userWalletId = userWalletId)
}
cacheRegistry.invokeOnExpire(
key = getYieldBalancesKey(userWalletId),
skipCache = refresh,

View file

@ -50,6 +50,7 @@ internal class DefaultNetworksRepository(
networks: Set<Network>,
): Flow<Set<NetworkStatus>> {
return networksStatusesStore.get(userWalletId, networks)
.distinctUntilChanged()
.flowOn(dispatchers.io)
}
@ -120,17 +121,7 @@ internal class DefaultNetworksRepository(
refresh: Boolean,
) = coroutineScope {
if (refresh) {
val statusesToRefresh = networksStatusesStore.getSyncOrNull(userWalletId)?.mapNotNull {
if (it.network in networks) {
it.copy(value = NetworkStatus.Refreshing)
} else {
null
}
}
if (statusesToRefresh != null) {
networksStatusesStore.storeAll(key = userWalletId, values = statusesToRefresh.toSet())
}
networksStatusesStore.refresh(key = userWalletId, networks = networks)
}
val currencies = getCurrencies(userWalletId, networks)

View file

@ -100,6 +100,10 @@ internal class DefaultQuotesRepository(
// it changes after filterExpiredCurrenciesIds
// calls with different coroutines and lead to fetchQuotes
mutex.withLock {
if (refresh) {
quotesStore.refresh(currenciesIds)
}
val expiredCurrenciesIds = filterExpiredCurrenciesIds(
currenciesIds = currenciesIds,
refresh = refresh || quotesFetchedForAppCurrency != appCurrencyId,

View file

@ -23,13 +23,16 @@ data class NetworkStatus(
sealed class Value {
abstract val source: StatusSource
}
/**
* Represents the state where the network is refreshing.
*/
data object Refreshing : Value() {
override val source: StatusSource = StatusSource.ACTUAL
fun copySealed(source: StatusSource): Value {
return when (this) {
is NoAccount -> copy(source = source)
is Verified -> copy(source = source)
is Unreachable,
is MissedDerivation,
-> this
}
}
}
/**

View file

@ -30,7 +30,6 @@ internal class CurrencyStatusOperations(
private fun createStatus(): CryptoCurrencyStatus.Value {
return when (val status = networkStatus?.value) {
null,
is NetworkStatus.Refreshing,
-> CryptoCurrencyStatus.Loading
is NetworkStatus.MissedDerivation -> createMissedDerivationStatus()
is NetworkStatus.Unreachable -> createUnreachableStatus(status)