Updated on 2026-08-14
This commit is contained in:
parent
f3f29813e0
commit
b74e2b9168
25 changed files with 405 additions and 72 deletions
|
|
@ -1,6 +1,7 @@
|
|||
package com.tangem.data.appcurrency
|
||||
|
||||
import com.tangem.data.appcurrency.utils.AppCurrencyConverter
|
||||
import com.tangem.data.common.api.safeApiCall
|
||||
import com.tangem.data.common.cache.CacheRegistry
|
||||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||
import com.tangem.datasource.api.tangemTech.models.CurrenciesResponse
|
||||
|
|
@ -15,7 +16,6 @@ import kotlinx.coroutines.flow.map
|
|||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.joda.time.Duration
|
||||
import timber.log.Timber
|
||||
|
||||
internal class DefaultAppCurrencyRepository(
|
||||
private val tangemTechApi: TangemTechApi,
|
||||
|
|
@ -80,17 +80,15 @@ internal class DefaultAppCurrencyRepository(
|
|||
}
|
||||
|
||||
private suspend fun fetchAvailableCurrencies() {
|
||||
try {
|
||||
val response = tangemTechApi.getCurrencyList()
|
||||
val response = safeApiCall(
|
||||
call = { tangemTechApi.getCurrencyList().bind() },
|
||||
onError = {
|
||||
cacheRegistry.invalidate(AVAILABLE_CURRENCIES_CACHE_KEY)
|
||||
getDefaultCurrenciesResponse()
|
||||
},
|
||||
)
|
||||
|
||||
availableAppCurrenciesStore.store(response)
|
||||
} catch (e: Throwable) {
|
||||
Timber.e(e, "Unable to fetch available currencies")
|
||||
|
||||
availableAppCurrenciesStore.store(getDefaultCurrenciesResponse())
|
||||
|
||||
throw e
|
||||
}
|
||||
availableAppCurrenciesStore.store(response)
|
||||
}
|
||||
|
||||
private fun getDefaultCurrenciesResponse(): CurrenciesResponse = CurrenciesResponse(
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ dependencies {
|
|||
implementation(deps.kotlin.coroutines)
|
||||
implementation(deps.jodatime)
|
||||
implementation(deps.timber)
|
||||
implementation(deps.arrow.core)
|
||||
|
||||
implementation(deps.hilt.android)
|
||||
kapt(deps.hilt.kapt)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,47 @@
|
|||
package com.tangem.data.common.api
|
||||
|
||||
import arrow.core.raise.Raise
|
||||
import arrow.core.raise.recover
|
||||
import com.tangem.datasource.api.common.response.ApiResponse
|
||||
import com.tangem.datasource.api.common.response.ApiResponseError
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
* A wrapper around the [Raise] interface specific for [ApiResponseError]. It provides utility functions to
|
||||
* operate on [ApiResponse] instances.
|
||||
*
|
||||
* @property raise A [Raise] instance for raising [ApiResponseError].
|
||||
*/
|
||||
@JvmInline
|
||||
value class ApiResponseRaise(
|
||||
private val raise: Raise<ApiResponseError>,
|
||||
) : Raise<ApiResponseError> by raise {
|
||||
|
||||
/**
|
||||
* Binds the given [ApiResponse] to its underlying value or raises an error.
|
||||
*
|
||||
* @return The underlying data of the response if it's successful.
|
||||
*/
|
||||
fun <T : Any> ApiResponse<T>.bind(): T = when (this) {
|
||||
is ApiResponse.Success -> data
|
||||
is ApiResponse.Error -> raise.raise(cause)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to execute an API call safely, providing error handling.
|
||||
*
|
||||
* @param call The API call block to execute.
|
||||
* @param onError A function to handle errors and return a fallback value of type [T].
|
||||
*
|
||||
* @return The result of the API call or the fallback value provided by [onError] if an error occurs.
|
||||
*/
|
||||
inline fun <T> safeApiCall(call: ApiResponseRaise.() -> T, onError: (ApiResponseError) -> T): T {
|
||||
return recover(
|
||||
block = { call(ApiResponseRaise(raise = this)) },
|
||||
recover = {
|
||||
Timber.w(it, "Unable to perform safe API call")
|
||||
onError(it)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
@ -24,6 +24,15 @@ interface CacheRegistry {
|
|||
*/
|
||||
suspend fun invalidate(key: String)
|
||||
|
||||
/**
|
||||
* Invalidates cache keys in registry.
|
||||
*
|
||||
* If the key doesn't exist, or it's already invalidated, this method doesn't have any effect.
|
||||
*
|
||||
* @param keys cache keys.
|
||||
*/
|
||||
suspend fun invalidate(keys: Collection<String>)
|
||||
|
||||
/**
|
||||
* Invalidates all cache keys in the registry.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -23,6 +23,11 @@ internal class DefaultCacheRegistry(
|
|||
cacheKeysStore.remove(key)
|
||||
}
|
||||
|
||||
override suspend fun invalidate(keys: Collection<String>) {
|
||||
Timber.d("Invalidate cache keys: $keys")
|
||||
cacheKeysStore.remove(keys)
|
||||
}
|
||||
|
||||
override suspend fun invalidateAll() {
|
||||
Timber.d("Invalidate all cache keys")
|
||||
cacheKeysStore.clear()
|
||||
|
|
@ -32,14 +37,14 @@ internal class DefaultCacheRegistry(
|
|||
key: String,
|
||||
skipCache: Boolean,
|
||||
expireIn: Duration,
|
||||
action: suspend () -> Unit,
|
||||
block: suspend () -> Unit,
|
||||
) {
|
||||
val isExpired = isExpired(key) || skipCache
|
||||
if (!isExpired) return
|
||||
|
||||
try {
|
||||
Timber.d("Invoke the action associated with the cache key: $key")
|
||||
action()
|
||||
block()
|
||||
} catch (e: Throwable) {
|
||||
Timber.w(e, "The action related to the cache key has failed: $key")
|
||||
throw e
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
package com.tangem.data.tokens.repository
|
||||
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.data.common.api.safeApiCall
|
||||
import com.tangem.data.common.cache.CacheRegistry
|
||||
import com.tangem.data.tokens.utils.*
|
||||
import com.tangem.datasource.api.common.response.ApiResponseError
|
||||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
|
||||
import com.tangem.datasource.local.token.UserMarketCoinsStore
|
||||
|
|
@ -21,10 +23,7 @@ import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
|||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import retrofit2.HttpException
|
||||
import timber.log.Timber
|
||||
import java.net.ConnectException
|
||||
import java.net.UnknownHostException
|
||||
|
||||
internal class DefaultCurrenciesRepository(
|
||||
private val tangemTechApi: TangemTechApi,
|
||||
|
|
@ -259,14 +258,14 @@ internal class DefaultCurrenciesRepository(
|
|||
private suspend fun fetchTokens(userWallet: UserWallet) {
|
||||
val userWalletId = userWallet.walletId
|
||||
|
||||
val response = try {
|
||||
with(tangemTechApi.getUserTokens(userWalletId.stringValue)) {
|
||||
// The response may contain repeated tokens
|
||||
copy(tokens = tokens.distinct())
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
handleFetchTokensError(userWallet, e)
|
||||
}
|
||||
val response = safeApiCall(
|
||||
call = {
|
||||
tangemTechApi.getUserTokens(userWalletId.stringValue).bind().let {
|
||||
it.copy(tokens = it.tokens.distinct())
|
||||
}
|
||||
},
|
||||
onError = { handleFetchTokensError(userWallet, it) },
|
||||
)
|
||||
|
||||
userTokensStore.store(userWallet.walletId, response)
|
||||
fetchUserMarketCoinsByIds(userWalletId, response)
|
||||
|
|
@ -288,7 +287,7 @@ internal class DefaultCurrenciesRepository(
|
|||
}
|
||||
}
|
||||
|
||||
private suspend fun handleFetchTokensError(userWallet: UserWallet, throwable: Throwable): UserTokensResponse {
|
||||
private suspend fun handleFetchTokensError(userWallet: UserWallet, e: ApiResponseError): UserTokensResponse {
|
||||
val userWalletId = userWallet.walletId
|
||||
val response = userTokensStore.getSyncOrNull(userWalletId)
|
||||
?: userTokensResponseFactory.createUserTokensResponse(
|
||||
|
|
@ -297,27 +296,12 @@ internal class DefaultCurrenciesRepository(
|
|||
isSortedByBalance = false,
|
||||
)
|
||||
|
||||
when (throwable) {
|
||||
is ConnectException,
|
||||
is UnknownHostException,
|
||||
-> {
|
||||
Timber.e("Unable to fetch currencies due to lack of internet connection")
|
||||
}
|
||||
is HttpException -> {
|
||||
if (throwable.code() == NOT_FOUND_HTTP_CODE) {
|
||||
Timber.w(
|
||||
throwable,
|
||||
"Requested currencies could not be found in the remote store for: $userWalletId",
|
||||
)
|
||||
if (e is ApiResponseError.HttpException && e.code == ApiResponseError.HttpException.Code.NOT_FOUND) {
|
||||
Timber.w(e, "Requested currencies could not be found in the remote store for: $userWalletId")
|
||||
|
||||
tangemTechApi.saveUserTokens(userWalletId.stringValue, response)
|
||||
} else {
|
||||
Timber.e(throwable, "Unable to fetch currencies for: $userWalletId")
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
throw throwable
|
||||
}
|
||||
tangemTechApi.saveUserTokens(userWalletId.stringValue, response)
|
||||
} else {
|
||||
cacheRegistry.invalidate(getTokensCacheKey(userWalletId))
|
||||
}
|
||||
|
||||
return response
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package com.tangem.data.tokens.repository
|
||||
|
||||
import com.tangem.data.common.api.safeApiCall
|
||||
import com.tangem.data.common.cache.CacheRegistry
|
||||
import com.tangem.data.tokens.utils.QuotesConverter
|
||||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||
|
|
@ -12,7 +13,6 @@ import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
|||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
|
||||
internal class DefaultQuotesRepository(
|
||||
private val tangemTechApi: TangemTechApi,
|
||||
|
|
@ -72,15 +72,20 @@ internal class DefaultQuotesRepository(
|
|||
}
|
||||
|
||||
private suspend fun fetchQuotes(rawCurrenciesIds: Set<String>, appCurrencyId: String) {
|
||||
val response = try {
|
||||
val coinIds = rawCurrenciesIds.joinToString(separator = ",")
|
||||
tangemTechApi.getQuotes(appCurrencyId, coinIds)
|
||||
} catch (e: Throwable) {
|
||||
Timber.e(e, "Unable to fetch quotes for: $rawCurrenciesIds")
|
||||
throw e
|
||||
}
|
||||
val response = safeApiCall(
|
||||
call = {
|
||||
val coinIds = rawCurrenciesIds.joinToString(separator = ",")
|
||||
tangemTechApi.getQuotes(appCurrencyId, coinIds).bind()
|
||||
},
|
||||
onError = {
|
||||
cacheRegistry.invalidate(rawCurrenciesIds.map(::getQuoteCacheKey))
|
||||
null
|
||||
},
|
||||
)
|
||||
|
||||
quotesStore.store(response)
|
||||
if (response != null) {
|
||||
quotesStore.store(response)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun filterExpiredCurrenciesIds(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue