Updated on 2026-08-14
This commit is contained in:
parent
80ec72c173
commit
e8c5c35e56
11 changed files with 89 additions and 44 deletions
|
|
@ -33,12 +33,8 @@ internal class ApiResponseCallDelegate<T : Any>(
|
|||
}
|
||||
|
||||
override fun onFailure(call: Call<T>, t: Throwable) {
|
||||
val e = if (t.isNetworkException()) {
|
||||
ApiResponseError.NetworkException
|
||||
} else {
|
||||
ApiResponseError.UnknownException(t)
|
||||
}
|
||||
val safeResponse = apiError<T>(e)
|
||||
val error = t.toApiError()
|
||||
val safeResponse = apiError<T>(error)
|
||||
|
||||
responseCallback.onResponse(this@ApiResponseCallDelegate, Response.success(safeResponse))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -73,7 +73,10 @@ sealed class ApiResponseError : Exception() {
|
|||
}
|
||||
|
||||
/** Represents a network error, typically when there's no connectivity. */
|
||||
object NetworkException : ApiResponseError()
|
||||
data object NetworkException : ApiResponseError()
|
||||
|
||||
/** Represents a timeout error, typically when the server takes too long to respond. */
|
||||
data object TimeoutException : ApiResponseError()
|
||||
|
||||
/**
|
||||
* Represents an unexpected exception that doesn't fall into one of the other categories.
|
||||
|
|
|
|||
|
|
@ -1,8 +1,11 @@
|
|||
package com.tangem.datasource.api.common.response
|
||||
|
||||
import kotlinx.coroutines.TimeoutCancellationException
|
||||
import retrofit2.Response
|
||||
import java.net.ConnectException
|
||||
import java.net.SocketTimeoutException
|
||||
import java.net.UnknownHostException
|
||||
import java.util.concurrent.TimeoutException
|
||||
import javax.net.ssl.SSLHandshakeException
|
||||
|
||||
internal fun <T : Any> Response<T>.toSafeApiResponse(): ApiResponse<T> {
|
||||
|
|
@ -23,10 +26,14 @@ internal fun <T : Any> Response<T>.toSafeApiResponse(): ApiResponse<T> {
|
|||
}
|
||||
}
|
||||
|
||||
internal fun Throwable.isNetworkException(): Boolean = when (this) {
|
||||
internal fun Throwable.toApiError(): ApiResponseError = when (this) {
|
||||
is ConnectException,
|
||||
is UnknownHostException,
|
||||
is SSLHandshakeException,
|
||||
-> true
|
||||
else -> false
|
||||
-> ApiResponseError.NetworkException
|
||||
is TimeoutException,
|
||||
is TimeoutCancellationException,
|
||||
is SocketTimeoutException,
|
||||
-> ApiResponseError.TimeoutException
|
||||
else -> ApiResponseError.UnknownException(cause = this)
|
||||
}
|
||||
|
|
@ -4,7 +4,9 @@ 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 kotlinx.coroutines.withTimeoutOrNull
|
||||
import timber.log.Timber
|
||||
import kotlin.time.Duration
|
||||
|
||||
/**
|
||||
* A wrapper around the [Raise] interface specific for [ApiResponseError]. It provides utility functions to
|
||||
|
|
@ -28,6 +30,28 @@ value class ApiResponseRaise(
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to execute an API call safely, providing error handling and a timeout.
|
||||
*
|
||||
* @param T The return type of the API call and the function.
|
||||
* @param timeoutMillis The timeout in milliseconds for the API call. Default is 30 seconds.
|
||||
* @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.
|
||||
*/
|
||||
suspend inline fun <T> safeApiCallWithTimeout(
|
||||
timeoutMillis: Duration = with(Duration) { 30.seconds },
|
||||
crossinline call: suspend ApiResponseRaise.() -> T,
|
||||
crossinline onError: suspend (ApiResponseError) -> T,
|
||||
): T = safeApiCall(
|
||||
call = {
|
||||
withTimeoutOrNull(timeoutMillis) { call() }
|
||||
?: raise(ApiResponseError.TimeoutException)
|
||||
},
|
||||
onError = onError,
|
||||
)
|
||||
|
||||
/**
|
||||
* Attempts to execute an API call safely, providing error handling.
|
||||
*
|
||||
|
|
@ -40,12 +64,10 @@ value class ApiResponseRaise(
|
|||
suspend inline fun <T> safeApiCall(
|
||||
crossinline call: suspend ApiResponseRaise.() -> T,
|
||||
crossinline onError: suspend (ApiResponseError) -> T,
|
||||
): T {
|
||||
return recover(
|
||||
block = { call(ApiResponseRaise(raise = this)) },
|
||||
recover = {
|
||||
Timber.w(it, "Unable to perform safe API call")
|
||||
onError(it)
|
||||
},
|
||||
)
|
||||
}
|
||||
): T = recover(
|
||||
block = { call(ApiResponseRaise(raise = this)) },
|
||||
recover = {
|
||||
Timber.w(it, "Unable to perform safe API call")
|
||||
onError(it)
|
||||
},
|
||||
)
|
||||
|
|
@ -249,7 +249,7 @@ internal class DefaultCurrenciesRepository(
|
|||
|
||||
launch(dispatchers.io) {
|
||||
combine(
|
||||
getMultiCurrencyWalletCurrencies(userWallet),
|
||||
getMultiCurrencyWalletCurrencies(userWallet).distinctUntilChanged(),
|
||||
isMultiCurrencyWalletCurrenciesFetching.map { it.getOrElse(userWallet.walletId) { false } },
|
||||
) { currencies, isFetching ->
|
||||
send(currencies, isStillLoading = isFetching)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
package com.tangem.data.tokens.repository
|
||||
|
||||
import com.tangem.data.common.api.safeApiCall
|
||||
import com.tangem.data.common.api.safeApiCallWithTimeout
|
||||
import com.tangem.data.common.cache.CacheRegistry
|
||||
import com.tangem.data.tokens.utils.QuotesConverter
|
||||
import com.tangem.data.tokens.utils.QuotesUnsupportedCurrenciesIdAdapter
|
||||
|
|
@ -91,30 +91,30 @@ internal class DefaultQuotesRepository(
|
|||
if (expiredCurrenciesIds.isEmpty()) return
|
||||
|
||||
quotesFetchedForAppCurrency = appCurrencyId
|
||||
|
||||
fetchQuotes(expiredCurrenciesIds, appCurrencyId)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun fetchQuotes(rawCurrenciesIds: Set<String>, appCurrencyId: String) {
|
||||
val replacementIdsResult = quotesUnsupportedCurrenciesAdapter.replaceUnsupportedCurrencies(rawCurrenciesIds)
|
||||
val response = safeApiCall(
|
||||
val response = safeApiCallWithTimeout(
|
||||
call = {
|
||||
val coinIds = replacementIdsResult.idsForRequest.joinToString(separator = ",")
|
||||
tangemTechApi.getQuotes(appCurrencyId, coinIds).bind()
|
||||
},
|
||||
onError = {
|
||||
onError = { error ->
|
||||
cacheRegistry.invalidate(rawCurrenciesIds.map(::getQuoteCacheKey))
|
||||
null
|
||||
|
||||
throw error
|
||||
},
|
||||
)
|
||||
|
||||
if (response != null) {
|
||||
val updatedResponse = quotesUnsupportedCurrenciesAdapter.getResponseWithUnsupportedCurrencies(
|
||||
response,
|
||||
replacementIdsResult.idsFiltered,
|
||||
)
|
||||
quotesStore.store(updatedResponse)
|
||||
}
|
||||
val updatedResponse = quotesUnsupportedCurrenciesAdapter.getResponseWithUnsupportedCurrencies(
|
||||
response,
|
||||
replacementIdsResult.idsFiltered,
|
||||
)
|
||||
quotesStore.store(updatedResponse)
|
||||
}
|
||||
|
||||
private suspend fun filterExpiredCurrenciesIds(
|
||||
|
|
|
|||
|
|
@ -83,13 +83,16 @@ sealed class Lce<out E : Any, out C : Any> {
|
|||
}
|
||||
|
||||
/**
|
||||
* Returns the content of this [Lce] if it's a [Lce.Content] or partial content if it's a [Lce.Loading],
|
||||
* `null` if it's a [Lce.Error].
|
||||
* Returns the content of this [Lce] if it's a [Lce.Content] or partial content if it's a [Lce.Loading]
|
||||
* and [isPartialContentAccepted] is `true`, `null` if it's a [Lce.Error].
|
||||
*
|
||||
* @param isPartialContentAccepted A flag indicating whether partial content should be accepted
|
||||
* and returned. Default is `true`.
|
||||
*
|
||||
* @return The content of this [Lce] or `null`.
|
||||
*/
|
||||
fun getOrNull(): C? = fold(
|
||||
ifLoading = ::identity,
|
||||
fun getOrNull(isPartialContentAccepted: Boolean = true): C? = fold(
|
||||
ifLoading = { if (isPartialContentAccepted) it else null },
|
||||
ifContent = ::identity,
|
||||
ifError = { null },
|
||||
)
|
||||
|
|
|
|||
|
|
@ -10,13 +10,13 @@ sealed class TotalFiatBalance {
|
|||
* Represents the loading state of the fiat balance.
|
||||
* This state indicates that the fiat balance is currently being retrieved or calculated.
|
||||
*/
|
||||
object Loading : TotalFiatBalance()
|
||||
data object Loading : TotalFiatBalance()
|
||||
|
||||
/**
|
||||
* Represents the failure state of the fiat balance.
|
||||
* This state indicates that an attempt to retrieve or calculate the fiat balance has failed.
|
||||
*/
|
||||
object Failed : TotalFiatBalance()
|
||||
data object Failed : TotalFiatBalance()
|
||||
|
||||
/**
|
||||
* Represents the successfully loaded state of the fiat balance.
|
||||
|
|
|
|||
|
|
@ -122,6 +122,9 @@ internal class CurrenciesStatusesLceOperations(
|
|||
val quotes = recover({ maybeQuotes?.bind()?.toNonEmptySetOrNull() }) {
|
||||
quotesRetrievingFailed = true
|
||||
null
|
||||
}?.ifEmpty {
|
||||
quotesRetrievingFailed = true
|
||||
null
|
||||
}
|
||||
|
||||
currencies.map { currency ->
|
||||
|
|
|
|||
|
|
@ -63,7 +63,14 @@ internal abstract class BasicTokenListSubscriber(
|
|||
transform = { maybeTokenList, maybeAppCurrency ->
|
||||
val tokenList = maybeTokenList.getOrElse(
|
||||
ifLoading = { maybeContent ->
|
||||
maybeContent ?: return@combine
|
||||
val isRefreshing = stateHolder.getWalletState(userWallet.walletId)
|
||||
?.pullToRefreshConfig
|
||||
?.isRefreshing
|
||||
?: false
|
||||
|
||||
maybeContent
|
||||
?.takeIf { !isRefreshing }
|
||||
?: return@combine
|
||||
},
|
||||
ifError = { e ->
|
||||
Timber.e("Failed to load token list: $e")
|
||||
|
|
|
|||
|
|
@ -53,9 +53,8 @@ internal class MultiWalletTokenListSubscriber(
|
|||
updateSortingIfNeeded(maybeTokenList)
|
||||
}
|
||||
|
||||
private suspend fun updateSortingIfNeeded(maybeTokenList: Lce<TokenListError, TokenList>) {
|
||||
val tokenList = maybeTokenList.getOrNull() ?: return
|
||||
if (!checkNeedSorting(tokenList)) return
|
||||
private suspend fun updateSortingIfNeeded(maybeTokenList: Lce<*, TokenList>) {
|
||||
val tokenList = getTokenList(maybeTokenList) ?: return
|
||||
|
||||
applyTokenListSortingUseCase(
|
||||
userWalletId = userWallet.walletId,
|
||||
|
|
@ -65,9 +64,14 @@ internal class MultiWalletTokenListSubscriber(
|
|||
)
|
||||
}
|
||||
|
||||
private fun checkNeedSorting(tokenList: TokenList): Boolean {
|
||||
return tokenList.totalFiatBalance !is TotalFiatBalance.Loading &&
|
||||
tokenList.sortedBy == TokenList.SortType.BALANCE
|
||||
private fun getTokenList(lce: Lce<*, TokenList>): TokenList? {
|
||||
val tokenList = lce.getOrNull(isPartialContentAccepted = false)
|
||||
?: return null
|
||||
|
||||
return tokenList.takeIf {
|
||||
tokenList.totalFiatBalance is TotalFiatBalance.Loaded &&
|
||||
tokenList.sortedBy == TokenList.SortType.BALANCE
|
||||
}
|
||||
}
|
||||
|
||||
private fun getCurrenciesIds(tokenList: TokenList): List<CryptoCurrency.ID> {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue