diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/markets/TangemTechMarketsApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/markets/TangemTechMarketsApi.kt index 0ded78dbab..014f9adf75 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/markets/TangemTechMarketsApi.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/markets/TangemTechMarketsApi.kt @@ -34,7 +34,7 @@ interface TangemTechMarketsApi { @GET("coins/history_preview") suspend fun getCoinsListCharts( - @Query("coin_ids") coinIds: List, + @Query("coin_ids") coinIds: String, @Query("currency") currency: String, @Query("interval") interval: String, ): ApiResponse diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/markets/models/response/TokenMarketChartListResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/markets/models/response/TokenMarketChartListResponse.kt index 0d1667ee24..b2f534b038 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/markets/models/response/TokenMarketChartListResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/markets/models/response/TokenMarketChartListResponse.kt @@ -1,8 +1,3 @@ package com.tangem.datasource.api.markets.models.response -import com.squareup.moshi.Json - -class TokenMarketChartListResponse( - @Json(name = "tokens") - val tokens: Map, -) \ No newline at end of file +typealias TokenMarketChartListResponse = Map \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt index d2b395d5f9..9b2c5b5d29 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt @@ -132,7 +132,7 @@ class NetworkModule { @Provides @DevTangemApi @Singleton - fun provideCoinMarketsApi( + fun provideTangemTechMarketsApi( @NetworkMoshi moshi: Moshi, @ApplicationContext context: Context, appVersionProvider: AppVersionProvider, diff --git a/core/pagination/src/main/java/com/tangem/pagination/BatchAction.kt b/core/pagination/src/main/java/com/tangem/pagination/BatchAction.kt index 691941752a..8078c699b1 100644 --- a/core/pagination/src/main/java/com/tangem/pagination/BatchAction.kt +++ b/core/pagination/src/main/java/com/tangem/pagination/BatchAction.kt @@ -1,5 +1,7 @@ package com.tangem.pagination +import java.util.UUID + /** * Action that can be dispatched to [BatchListSource]. * @@ -7,7 +9,7 @@ package com.tangem.pagination * @param TKey type of the key of the batch. * @param TUpdate type of the update request. */ -sealed class BatchAction { +sealed class BatchAction { /** * Action to load the first batch. @@ -34,10 +36,18 @@ sealed class BatchAction { * * @param keys keys of the batches to update. * @param updateRequest request to update the batches. + * @param async true if the request doesn't require to synchronize on specific batches in order to fetch update + * data, this request will be delegated to fetchAsync method in [BatchUpdateFetcher], + * false if request requires to hold the current batches data until fetch + update is completed + * @param operationId the unique identifier of the request. + * Only one request with the same hash can be executed at a time, + * the rest of the requests will be canceled as long as there is a request with this hash in progress. */ class UpdateBatches( val keys: Set, val updateRequest: TUpdate, + val async: Boolean = false, + val operationId: String = UUID.randomUUID().toString(), ) : BatchAction() /** diff --git a/core/pagination/src/main/java/com/tangem/pagination/BatchListSource.kt b/core/pagination/src/main/java/com/tangem/pagination/BatchListSource.kt index 77e58eafcf..1cc865396d 100644 --- a/core/pagination/src/main/java/com/tangem/pagination/BatchListSource.kt +++ b/core/pagination/src/main/java/com/tangem/pagination/BatchListSource.kt @@ -75,6 +75,8 @@ private class DefaultBatchListSource private val scope = context.coroutineScope private val updateJobs = MutableStateFlow, Job>>>(emptyList()) + private val updateAsyncJobs = + MutableStateFlow, Job>>>(emptyList()) private val waitingUpdateJobs = MutableStateFlow, Job>>>(emptyList()) @@ -127,38 +129,13 @@ private class DefaultBatchListSource } is BatchAction.UpdateBatches -> { if (updateFetcher == null) return + // If the request with the same operationId is in progress, skip the request + if (updateInProgressExists(action.operationId)) return - scope.launch(fetchDispatcher) { - // Lazily start a job so we can avoid batch update collisions - // by waiting for other tasks with the same keys to complete - val job = launch(start = CoroutineStart.LAZY) { - updateBatchesTask(action) - } - - val actionJob = action to job - - waitingUpdateJobs.update { it + actionJob } - - // Wait for other update tasks that mutate batches with the same keys - updateJobs.first { workingJobs -> - action.keys.intersect(workingJobs.map { it.first.keys }.flatten().toSet()).isEmpty() - } - - waitingUpdateJobs.update { it - actionJob } - - // No other task are mutating batches with the same keys, so we can start a job - val started = job.start() - - if (started) { - updateJobs.update { it + actionJob } - - job.invokeOnCompletion { cause -> - // If the job was cancelled it is up to a canceller to remove job from the updateJobs list - if (cause !is CancellationException) { - updateJobs.update { it - actionJob } - } - } - } + if (action.async) { + collectAsyncUpdateAction(action) + } else { + collectSyncUpdateAction(action) } } BatchAction.CancelAllUpdates -> { @@ -176,6 +153,57 @@ private class DefaultBatchListSource } } + private fun collectAsyncUpdateAction(action: BatchAction.UpdateBatches) { + val job = scope.launch(fetchDispatcher) { + updateBatchesAsyncTask(action) + } + val actionJob = action to job + + updateAsyncJobs.update { it + actionJob } + + job.invokeOnCompletion { cause -> + // If the job was cancelled it is up to a canceller to remove job from the updateJobs list + if (cause !is CancellationException) { + updateAsyncJobs.update { it - actionJob } + } + } + } + + private fun collectSyncUpdateAction(action: BatchAction.UpdateBatches) { + // Lazily start a job so we can avoid batch update collisions + // by waiting for other tasks with the same keys to complete + val job = scope.launch(fetchDispatcher, start = CoroutineStart.LAZY) { + updateBatchesTask(action) + } + + val actionJob = action to job + + waitingUpdateJobs.update { it + actionJob } + + scope.launch(fetchDispatcher) { + // Wait for other update tasks that mutate batches with the same keys + updateJobs.first { workingJobs -> + action.keys.intersect(workingJobs.map { it.first.keys }.flatten().toSet()).isEmpty() + } + + waitingUpdateJobs.update { it - actionJob } + + // No other task are mutating batches with the same keys, so we can start a job + val started = job.start() + + if (started) { + updateJobs.update { it + actionJob } + + job.invokeOnCompletion { cause -> + // If the job was cancelled it is up to a canceller to remove job from the updateJobs list + if (cause !is CancellationException) { + updateJobs.update { it - actionJob } + } + } + } + } + } + private suspend fun reloadTask(action: BatchAction.Reload) { state.value = BatchListState( data = emptyList(), @@ -184,7 +212,10 @@ private class DefaultBatchListSource val res = runCatching { batchFetcher.fetchFirst(action.requestParams) - }.getOrElse { BatchFetchResult.Error(it) } + }.getOrElse { + currentCoroutineContext().ensureActive() + BatchFetchResult.Error(it) + } state.value = when (res) { is BatchFetchResult.Success -> { @@ -275,6 +306,7 @@ private class DefaultBatchListSource updateRequest = action.updateRequest, ) } catch (t: Throwable) { + currentCoroutineContext().ensureActive() BatchUpdateResult.Error(t) } @@ -292,7 +324,64 @@ private class DefaultBatchListSource updateResults.emit(action.updateRequest to result) } + private suspend fun updateBatchesAsyncTask(action: BatchAction.UpdateBatches) { + if (updateFetcher == null) return + + val batches = state.value.data + val batchesToUpdate = batches.filter { action.keys.contains(it.key) } + + val updateContext = UpdateContext(request = action.updateRequest, action.keys) + + with(updateFetcher) { + updateContext.fetchUpdateAsync(batchesToUpdate, action.updateRequest) + } + } + + @Suppress("FunctionNaming") + private fun UpdateContext(request: TUpdate, keysToUpdate: Set) = + object : BatchUpdateFetcher.UpdateContext { + + override suspend fun update(update: List>.() -> BatchUpdateResult) { + val stateToFetchUpdateBasedOn = state.value.data.filter { + keysToUpdate.contains(it.key) + } + + val result = runCatching { + stateToFetchUpdateBasedOn.update() + }.getOrElse { + currentCoroutineContext().ensureActive() + BatchUpdateResult.Error(it) + } + + if (result is BatchUpdateResult.Success) { + state.update { currentState -> + val resMap = result.data.associateBy { it.key } + currentState.copy( + data = currentState.data.map { + resMap[it.key] ?: it + }, + ) + } + } + + updateResults.emit(request to result) + } + } + + private fun updateInProgressExists(operationId: String): Boolean { + return updateAsyncJobs.value.any { it.first.operationId == operationId } || + updateJobs.value.any { it.first.operationId == operationId } || + waitingUpdateJobs.value.any { it.first.operationId == operationId } + } + private fun stopAllUpdates() { + updateAsyncJobs.update { actionAsyncJobs -> + actionAsyncJobs.forEach { + it.second.cancel() + } + emptyList() + } + updateJobs.update { actionJobs -> waitingUpdateJobs.update { waitingActionJobs -> waitingActionJobs.forEach { @@ -308,6 +397,18 @@ private class DefaultBatchListSource } private fun stopUpdates(predicate: (BatchAction.UpdateBatches) -> Boolean) { + updateAsyncJobs.update { actionAsyncJobs -> + actionAsyncJobs.mapNotNull { + if (predicate(it.first)) { + it.second.cancel() + null + } else { + it + } + } + emptyList() + } + updateJobs.update { actionJobs -> waitingUpdateJobs.update { waitingActionJobs -> waitingActionJobs.mapNotNull { diff --git a/core/pagination/src/main/java/com/tangem/pagination/BatchUpdateFetcher.kt b/core/pagination/src/main/java/com/tangem/pagination/BatchUpdateFetcher.kt index 704c416316..f53deb4449 100644 --- a/core/pagination/src/main/java/com/tangem/pagination/BatchUpdateFetcher.kt +++ b/core/pagination/src/main/java/com/tangem/pagination/BatchUpdateFetcher.kt @@ -8,7 +8,7 @@ package com.tangem.pagination * @param TData type of the data. * @param TUpdate type of the update request. */ -fun interface BatchUpdateFetcher { +interface BatchUpdateFetcher { /** * Fetches updates for a batch of data. @@ -19,5 +19,45 @@ fun interface BatchUpdateFetcher { * @param updateRequest request to update the data. * @return result of the update operation. */ - suspend fun fetchUpdate(toUpdate: List>, updateRequest: TUpdate): BatchUpdateResult + suspend fun fetchUpdate( + toUpdate: List>, + updateRequest: TUpdate, + ): BatchUpdateResult = BatchUpdateResult.Error(NotImplementedError()) + + /** + * Fetches updates for a batch of data asynchronously. + * To update the data, use the [UpdateContext.update] method. + * [UpdateContext.update] could be called as many times as you want. + * + * Note that the result batch key as a result of executing the method must be presented in the [toUpdate] list, + * otherwise, updates will not be performed. + * + * @param toUpdate list of batches to update. **Attention** Data may be outdated and should be used only to make + * a request for an update, not for the actual update operation. For the actual update operation, use the batches + * provided by [UpdateContext.update]. + * @param updateRequest request to update the data. + */ + suspend fun UpdateContext.fetchUpdateAsync( + toUpdate: List>, + updateRequest: TUpdate, + ) { + } + + /** + * Context for updating the data. + * Used by [BatchListSource] to provide a way to update batches by [fetchUpdateAsync] method. + */ + interface UpdateContext { + + /** + * Updates the data of the batch. + * Could be called as many times as you want. + * + * Input batches keys and the data could not always be the same as the keys of the [toUpdate] list in + * [fetchUpdateAsync] method, but the provided set of keys will always be a subset of the [toUpdate] list. + * + * @param update lambda to update the data. + */ + suspend fun update(update: List>.() -> BatchUpdateResult) + } } \ No newline at end of file diff --git a/core/pagination/src/main/java/com/tangem/pagination/fetcher/LimitOffsetBatchFetcher.kt b/core/pagination/src/main/java/com/tangem/pagination/fetcher/LimitOffsetBatchFetcher.kt index 3dd04c24c7..de125a62ff 100644 --- a/core/pagination/src/main/java/com/tangem/pagination/fetcher/LimitOffsetBatchFetcher.kt +++ b/core/pagination/src/main/java/com/tangem/pagination/fetcher/LimitOffsetBatchFetcher.kt @@ -2,6 +2,8 @@ package com.tangem.pagination.fetcher import com.tangem.pagination.BatchFetchResult import com.tangem.pagination.exception.EndOfPaginationException +import kotlinx.coroutines.currentCoroutineContext +import kotlinx.coroutines.ensureActive import kotlinx.coroutines.flow.MutableStateFlow /** @@ -12,32 +14,42 @@ import kotlinx.coroutines.flow.MutableStateFlow * * @property prefetchDistance number of items to fetch for the first batch. * @property batchSize size of the batch. - * @property fetch function that fetches the data. + * @property subFetcher function that fetches the data. */ class LimitOffsetBatchFetcher( private val prefetchDistance: Int, private val batchSize: Int, - private val fetch: suspend (request: Request) -> BatchFetchResult, + private val subFetcher: SubFetcher, ) : BatchFetcher { - data class Request( + data class Request( val limit: Int, val offset: Int, - val request: TRequest, + val params: TRequestParams, ) + fun interface SubFetcher { + suspend fun fetch( + request: Request, + lastResult: BatchFetchResult?, + ): BatchFetchResult + } + private val lastRequest = MutableStateFlow?>(null) override suspend fun fetchFirst(requestParams: TRequestParams): BatchFetchResult { val req = Request( offset = 0, limit = prefetchDistance, - request = requestParams, + params = requestParams, ) val res = runCatching { - fetch(req) - }.getOrElse { BatchFetchResult.Error(it) } + subFetcher.fetch(req, null) + }.getOrElse { + currentCoroutineContext().ensureActive() + BatchFetchResult.Error(it) + } lastRequest.value = req return res @@ -58,15 +70,18 @@ class LimitOffsetBatchFetcher( Request( offset = last.offset + last.limit, limit = batchSize, - request = overrideRequestParams ?: last.request, + params = overrideRequestParams ?: last.params, ) } else { last } val res = runCatching { - fetch(req) - }.getOrElse { BatchFetchResult.Error(it) } + subFetcher.fetch(req, lastResult) + }.getOrElse { + currentCoroutineContext().ensureActive() + BatchFetchResult.Error(it) + } lastRequest.value = req return res diff --git a/data/markets/build.gradle.kts b/data/markets/build.gradle.kts index 8e9347ad54..98b32e7c97 100644 --- a/data/markets/build.gradle.kts +++ b/data/markets/build.gradle.kts @@ -16,6 +16,7 @@ dependencies { implementation(projects.core.pagination) implementation(projects.domain.tokens.models) implementation(projects.domain.markets) + implementation(projects.data.common) // region DI implementation(deps.hilt.android) @@ -26,6 +27,7 @@ dependencies { implementation(deps.kotlin.coroutines) implementation(deps.moshi) implementation(deps.moshi.kotlin) + implementation(deps.timber) implementation(projects.libs.blockchainSdk) // endregion diff --git a/data/markets/src/main/java/com/tangem/data/markets/DefaultMarketsTokenRepository.kt b/data/markets/src/main/java/com/tangem/data/markets/DefaultMarketsTokenRepository.kt index 65ebc717bb..a0b50723dc 100644 --- a/data/markets/src/main/java/com/tangem/data/markets/DefaultMarketsTokenRepository.kt +++ b/data/markets/src/main/java/com/tangem/data/markets/DefaultMarketsTokenRepository.kt @@ -1,6 +1,7 @@ package com.tangem.data.markets import com.tangem.data.markets.converters.* +import com.tangem.data.markets.utils.retryOnError import com.tangem.datasource.api.common.response.getOrThrow import com.tangem.datasource.api.markets.TangemTechMarketsApi import com.tangem.datasource.api.tangemTech.TangemTechApi @@ -9,7 +10,7 @@ import com.tangem.domain.markets.repositories.MarketsTokenRepository import com.tangem.pagination.* import com.tangem.pagination.fetcher.LimitOffsetBatchFetcher import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import kotlinx.coroutines.withContext +import kotlinx.coroutines.* internal class DefaultMarketsTokenRepository( private val marketsApi: TangemTechMarketsApi, @@ -18,28 +19,34 @@ internal class DefaultMarketsTokenRepository( ) : MarketsTokenRepository { private val tokenListConverter = TokenMarketListConverter() - private val tokenListChartsConverter = TokenMarketChartsConverter(TokenListChartConverter()) - private val tokenQuotesConverter = TokenQuotesConverter() private val tokenMarketsFetcher - get() = LimitOffsetBatchFetcher>( - prefetchDistance = 50, - batchSize = 30, - fetch = { params -> - withContext(dispatcherProvider.io) { - val res = marketsApi.getCoinsList( - currency = params.request.fiatPriceCurrency, - interval = params.request.priceChangeInterval.toRequestParam(), - order = params.request.priceChangeInterval.toRequestParam(), - search = params.request.searchText, - generalCoins = params.request.showUnder100kMarketCapTokens.not(), - offset = params.offset, - limit = params.limit, - ).getOrThrow() + get() = LimitOffsetBatchFetcher( + prefetchDistance = 150, + batchSize = 100, + subFetcher = object : LimitOffsetBatchFetcher.SubFetcher> { - val last = res.tokens.size < params.limit + var requestTimeStamp: Long? = null // TODO when backend is ready - BatchFetchResult.Success( + override suspend fun fetch( + request: LimitOffsetBatchFetcher.Request, + lastResult: BatchFetchResult>?, + ): BatchFetchResult> { + val res = retryOnError(priority = true) { + marketsApi.getCoinsList( + currency = request.params.fiatPriceCurrency, + interval = request.params.priceChangeInterval.toRequestParam(), + order = request.params.order.toRequestParam(), + search = request.params.searchText, + generalCoins = request.params.showUnder100kMarketCapTokens.not(), + offset = request.offset, + limit = request.limit, + ).getOrThrow() + } + + val last = res.tokens.size < request.limit + + return BatchFetchResult.Success( data = tokenListConverter.convert(res), last = last, ) @@ -47,60 +54,14 @@ internal class DefaultMarketsTokenRepository( }, ) - private val tokenMarketsUpdateFetcher - get() = BatchUpdateFetcher, TokenMarketUpdateRequest> { toUpdate, updateRequest -> - withContext(dispatcherProvider.io) { - val idsToUpdate = toUpdate.map { batch -> - batch.data.map { it.id } - }.flatten() - - val updatedBatches = when (updateRequest) { - is TokenMarketUpdateRequest.UpdateChart -> { - val res = marketsApi.getCoinsListCharts( - coinIds = idsToUpdate, - interval = updateRequest.interval.toRequestParam(), - currency = updateRequest.currency, - ).getOrThrow() - - toUpdate.map { batch -> - batch.copy( - data = batch.data.map { - it.copy( - tokenCharts = tokenListChartsConverter.convert( - chartsToCopy = it.tokenCharts, - tokenId = it.id, - interval = updateRequest.interval, - value = res, - ), - ) - }, - ) - } - } - is TokenMarketUpdateRequest.UpdateQuotes -> { - val quotesRes = tangemTechApi.getQuotes( - currencyId = updateRequest.currencyId, - coinIds = idsToUpdate.joinToString(separator = ","), - fields = quoteFields.joinToString(separator = ","), - ).getOrThrow() - - toUpdate.map { batch -> - batch.copy( - data = batch.data.map { - it.copy(tokenQuotes = tokenQuotesConverter.convert(it.id, quotesRes)) - }, - ) - } - } - } - - BatchUpdateResult.Success(updatedBatches) - } - } - - override suspend fun getTokenListFlow( + override fun getTokenListFlow( batchingContext: BatchingContext, ): BatchFlow, TokenMarketUpdateRequest> { + val tokenMarketsUpdateFetcher = MarketsBatchUpdateFetcher( + tangemTechApi = tangemTechApi, + marketsApi = marketsApi, + ) + return BatchListSource( fetchDispatcher = dispatcherProvider.io, context = batchingContext, @@ -109,13 +70,4 @@ internal class DefaultMarketsTokenRepository( updateFetcher = tokenMarketsUpdateFetcher, ).toBatchFlow() } - - companion object { - private val quoteFields = listOf( - "price", - "priceChange24h", - "priceChange1w", - "priceChange30d", - ) - } } \ No newline at end of file diff --git a/data/markets/src/main/java/com/tangem/data/markets/MarketsBatchUpdateFetcher.kt b/data/markets/src/main/java/com/tangem/data/markets/MarketsBatchUpdateFetcher.kt new file mode 100644 index 0000000000..fa24fa4144 --- /dev/null +++ b/data/markets/src/main/java/com/tangem/data/markets/MarketsBatchUpdateFetcher.kt @@ -0,0 +1,117 @@ +package com.tangem.data.markets + +import com.tangem.data.markets.converters.TokenListChartConverter +import com.tangem.data.markets.converters.TokenMarketChartsConverter +import com.tangem.data.markets.converters.TokenQuotesConverter +import com.tangem.data.markets.converters.toRequestParam +import com.tangem.data.markets.utils.retryOnError +import com.tangem.datasource.api.common.response.getOrThrow +import com.tangem.datasource.api.markets.TangemTechMarketsApi +import com.tangem.datasource.api.markets.models.response.TokenMarketChartListResponse +import com.tangem.datasource.api.tangemTech.TangemTechApi +import com.tangem.domain.markets.TokenMarket +import com.tangem.domain.markets.TokenMarketUpdateRequest +import com.tangem.pagination.Batch +import com.tangem.pagination.BatchUpdateFetcher +import com.tangem.pagination.BatchUpdateResult +import kotlinx.coroutines.* + +internal class MarketsBatchUpdateFetcher( + private val marketsApi: TangemTechMarketsApi, + private val tangemTechApi: TangemTechApi, +) : BatchUpdateFetcher, TokenMarketUpdateRequest> { + + private val tokenListChartsConverter = TokenMarketChartsConverter(TokenListChartConverter()) + private val tokenQuotesConverter = TokenQuotesConverter() + + override suspend fun BatchUpdateFetcher.UpdateContext>.fetchUpdateAsync( + toUpdate: List>>, + updateRequest: TokenMarketUpdateRequest, + ) { + val idsToUpdate = toUpdate.map { batch -> + batch.key to batch.data.map { it.id } + } + + when (updateRequest) { + is TokenMarketUpdateRequest.UpdateChart -> coroutineScope { + val updateTasks = idsToUpdate.map { batchIds -> + async { + retryOnError { + marketsApi.getCoinsListCharts( + coinIds = batchIds.second.joinToString(separator = ","), + interval = updateRequest.interval.toRequestParam(), + currency = updateRequest.currency, + ).getOrThrow() + } + } + } + + updateTasks.forEachIndexed { index, deferred -> + launch { + val res = deferred.await() + val batchToUpdate = toUpdate[index] + + update { + val resBatch = changeChartsInBatches( + updateRequest = updateRequest, + batchToUpdate = batchToUpdate, + update = res, + ) + BatchUpdateResult.Success(resBatch) + } + } + } + } + is TokenMarketUpdateRequest.UpdateQuotes -> { + val quotesRes = tangemTechApi.getQuotes( + currencyId = updateRequest.currencyId, + coinIds = idsToUpdate.joinToString(separator = ","), + fields = quoteFields.joinToString(separator = ","), + ).getOrThrow() + + update { + val res = toUpdate.map { batch -> + batch.copy( + data = batch.data.map { + it.copy(tokenQuotes = tokenQuotesConverter.convert(it.id, quotesRes)) + }, + ) + } + + BatchUpdateResult.Success(res) + } + } + } + } + + private fun List>>.changeChartsInBatches( + updateRequest: TokenMarketUpdateRequest.UpdateChart, + batchToUpdate: Batch>, + update: TokenMarketChartListResponse, + ): List>> = mapNotNull { resultBatch -> + if (batchToUpdate.key != resultBatch.key) return@mapNotNull null + + Batch( + key = batchToUpdate.key, + data = batchToUpdate.data.map { + it.copy( + tokenCharts = tokenListChartsConverter.convert( + chartsToCopy = it.tokenCharts, + tokenId = it.id, + interval = updateRequest.interval, + value = update, + ), + ) + }, + ) + } + + companion object { + private val quoteFields = listOf( + "price", + "priceChange24h", + "priceChange1w", + "priceChange30d", + ) + } +} \ No newline at end of file diff --git a/data/markets/src/main/java/com/tangem/data/markets/converters/TokenListConfigConverters.kt b/data/markets/src/main/java/com/tangem/data/markets/converters/TokenListConfigConverters.kt index eee63ef4e9..f47c0ff170 100644 --- a/data/markets/src/main/java/com/tangem/data/markets/converters/TokenListConfigConverters.kt +++ b/data/markets/src/main/java/com/tangem/data/markets/converters/TokenListConfigConverters.kt @@ -20,7 +20,7 @@ fun TokenMarketListConfig.Order.toRequestParam(): String = when (this) { fun PriceChangeInterval.toRequestParam(): String = when (this) { PriceChangeInterval.H24 -> "24h" PriceChangeInterval.WEEK -> "1w" - PriceChangeInterval.MONTH -> "1m" + PriceChangeInterval.MONTH -> "30d" PriceChangeInterval.MONTH3 -> "3m" PriceChangeInterval.MONTH6 -> "6m" PriceChangeInterval.YEAR -> "1y" diff --git a/data/markets/src/main/java/com/tangem/data/markets/converters/TokenMarketChartsConverter.kt b/data/markets/src/main/java/com/tangem/data/markets/converters/TokenMarketChartsConverter.kt index aade2d2073..dcfdee4638 100644 --- a/data/markets/src/main/java/com/tangem/data/markets/converters/TokenMarketChartsConverter.kt +++ b/data/markets/src/main/java/com/tangem/data/markets/converters/TokenMarketChartsConverter.kt @@ -14,7 +14,7 @@ class TokenMarketChartsConverter( interval: PriceChangeInterval, value: TokenMarketChartListResponse, ): TokenMarket.Charts { - val prices = requireNotNull(value.tokens[tokenId]) { + val prices = requireNotNull(value[tokenId]) { "$tokenId is not found in the response. This shouldn't have happened." } return when (interval) { diff --git a/data/markets/src/main/java/com/tangem/data/markets/converters/TokenMarketListConverter.kt b/data/markets/src/main/java/com/tangem/data/markets/converters/TokenMarketListConverter.kt index ea8587e123..8f9c571a86 100644 --- a/data/markets/src/main/java/com/tangem/data/markets/converters/TokenMarketListConverter.kt +++ b/data/markets/src/main/java/com/tangem/data/markets/converters/TokenMarketListConverter.kt @@ -20,9 +20,9 @@ class TokenMarketListConverter : Converter retryOnError(priority: Boolean = false, call: suspend () -> T): T { + while (true) { + return try { + call() + } catch (e: Exception) { + if (e is CancellationException) { + currentCoroutineContext().ensureActive() + } + Timber.e(e) + if (priority.not()) { + yield() + delay(timeMillis = 500) + } + continue + } + } +} \ No newline at end of file diff --git a/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarket.kt b/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarket.kt index c9587db1f9..b7316cf79a 100644 --- a/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarket.kt +++ b/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarket.kt @@ -21,13 +21,13 @@ data class TokenMarket( // 25x25 val imageUrlThumb = - "$imageHost/:thumb/:$id.png" + "${imageHost}thumb/$id.png" // 50x50 val imageUrlSmall = - "$imageHost/:small/:$id.png" + "${imageHost}small/$id.png" // 250x250 val imageUrlLarge = - "$imageHost/:large/:$id.png" + "${imageHost}large/$id.png" } \ No newline at end of file diff --git a/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenQuotes.kt b/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenQuotes.kt index e26b6abb9f..7a162c9aac 100644 --- a/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenQuotes.kt +++ b/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenQuotes.kt @@ -4,5 +4,15 @@ import java.math.BigDecimal data class TokenQuotes( val currentPrice: BigDecimal, - val priceChanges: Map, -) \ No newline at end of file + private val priceChanges: Map, +) { + init { + require(priceChanges.containsKey(PriceChangeInterval.H24)) + require(priceChanges.containsKey(PriceChangeInterval.WEEK)) + require(priceChanges.containsKey(PriceChangeInterval.MONTH)) + } + + fun h24Percent() = priceChanges[PriceChangeInterval.H24]!! + fun weekPercent() = priceChanges[PriceChangeInterval.WEEK]!! + fun monthPercent() = priceChanges[PriceChangeInterval.MONTH]!! +} \ No newline at end of file diff --git a/domain/markets/src/main/java/com/tangem/domain/markets/GetMarketsTokenListFlowUseCase.kt b/domain/markets/src/main/java/com/tangem/domain/markets/GetMarketsTokenListFlowUseCase.kt new file mode 100644 index 0000000000..ee13784443 --- /dev/null +++ b/domain/markets/src/main/java/com/tangem/domain/markets/GetMarketsTokenListFlowUseCase.kt @@ -0,0 +1,16 @@ +package com.tangem.domain.markets + +import com.tangem.domain.markets.repositories.MarketsTokenRepository +import com.tangem.pagination.BatchFlow +import com.tangem.pagination.BatchingContext + +typealias TokenListBatchingContext = BatchingContext +typealias TokenListBatchFlow = BatchFlow, TokenMarketUpdateRequest> + +class GetMarketsTokenListFlowUseCase( + private val marketsTokenRepository: MarketsTokenRepository, +) { + operator fun invoke(batchingContext: TokenListBatchingContext): TokenListBatchFlow { + return marketsTokenRepository.getTokenListFlow(batchingContext) + } +} \ No newline at end of file diff --git a/domain/markets/src/main/java/com/tangem/domain/markets/repositories/MarketsTokenRepository.kt b/domain/markets/src/main/java/com/tangem/domain/markets/repositories/MarketsTokenRepository.kt index 97aeb80c43..b72061f9f2 100644 --- a/domain/markets/src/main/java/com/tangem/domain/markets/repositories/MarketsTokenRepository.kt +++ b/domain/markets/src/main/java/com/tangem/domain/markets/repositories/MarketsTokenRepository.kt @@ -1,14 +1,8 @@ package com.tangem.domain.markets.repositories -import com.tangem.domain.markets.TokenMarket -import com.tangem.domain.markets.TokenMarketListConfig -import com.tangem.domain.markets.TokenMarketUpdateRequest -import com.tangem.pagination.BatchFlow -import com.tangem.pagination.BatchingContext +import com.tangem.domain.markets.* interface MarketsTokenRepository { - suspend fun getTokenListFlow( - batchingContext: BatchingContext, - ): BatchFlow, TokenMarketUpdateRequest> + fun getTokenListFlow(batchingContext: TokenListBatchingContext): TokenListBatchFlow } \ No newline at end of file