Updated on 2026-08-14
This commit is contained in:
parent
f7412dfd86
commit
0cd5575681
20 changed files with 437 additions and 162 deletions
|
|
@ -34,7 +34,7 @@ interface TangemTechMarketsApi {
|
|||
|
||||
@GET("coins/history_preview")
|
||||
suspend fun getCoinsListCharts(
|
||||
@Query("coin_ids") coinIds: List<String>,
|
||||
@Query("coin_ids") coinIds: String,
|
||||
@Query("currency") currency: String,
|
||||
@Query("interval") interval: String,
|
||||
): ApiResponse<TokenMarketChartListResponse>
|
||||
|
|
|
|||
|
|
@ -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<String, TokenMarketChartResponse>,
|
||||
)
|
||||
typealias TokenMarketChartListResponse = Map<String, TokenMarketChartResponse>
|
||||
|
|
@ -132,7 +132,7 @@ class NetworkModule {
|
|||
@Provides
|
||||
@DevTangemApi
|
||||
@Singleton
|
||||
fun provideCoinMarketsApi(
|
||||
fun provideTangemTechMarketsApi(
|
||||
@NetworkMoshi moshi: Moshi,
|
||||
@ApplicationContext context: Context,
|
||||
appVersionProvider: AppVersionProvider,
|
||||
|
|
|
|||
|
|
@ -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<TKey, TRequestParams, TUpdate> {
|
||||
sealed class BatchAction<out TKey, out TRequestParams, out TUpdate> {
|
||||
|
||||
/**
|
||||
* Action to load the first batch.
|
||||
|
|
@ -34,10 +36,18 @@ sealed class BatchAction<TKey, TRequestParams, TUpdate> {
|
|||
*
|
||||
* @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<TKey, TUpdate>(
|
||||
val keys: Set<TKey>,
|
||||
val updateRequest: TUpdate,
|
||||
val async: Boolean = false,
|
||||
val operationId: String = UUID.randomUUID().toString(),
|
||||
) : BatchAction<TKey, Nothing, TUpdate>()
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -75,6 +75,8 @@ private class DefaultBatchListSource<TKey, TData, TRequestParams : Any, TUpdate>
|
|||
|
||||
private val scope = context.coroutineScope
|
||||
private val updateJobs = MutableStateFlow<List<Pair<BatchAction.UpdateBatches<TKey, TUpdate>, Job>>>(emptyList())
|
||||
private val updateAsyncJobs =
|
||||
MutableStateFlow<List<Pair<BatchAction.UpdateBatches<TKey, TUpdate>, Job>>>(emptyList())
|
||||
private val waitingUpdateJobs =
|
||||
MutableStateFlow<List<Pair<BatchAction.UpdateBatches<TKey, TUpdate>, Job>>>(emptyList())
|
||||
|
||||
|
|
@ -127,38 +129,13 @@ private class DefaultBatchListSource<TKey, TData, TRequestParams : Any, TUpdate>
|
|||
}
|
||||
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<TKey, TData, TRequestParams : Any, TUpdate>
|
|||
}
|
||||
}
|
||||
|
||||
private fun collectAsyncUpdateAction(action: BatchAction.UpdateBatches<TKey, TUpdate>) {
|
||||
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<TKey, TUpdate>) {
|
||||
// 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<TRequestParams>) {
|
||||
state.value = BatchListState(
|
||||
data = emptyList(),
|
||||
|
|
@ -184,7 +212,10 @@ private class DefaultBatchListSource<TKey, TData, TRequestParams : Any, TUpdate>
|
|||
|
||||
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<TKey, TData, TRequestParams : Any, TUpdate>
|
|||
updateRequest = action.updateRequest,
|
||||
)
|
||||
} catch (t: Throwable) {
|
||||
currentCoroutineContext().ensureActive()
|
||||
BatchUpdateResult.Error(t)
|
||||
}
|
||||
|
||||
|
|
@ -292,7 +324,64 @@ private class DefaultBatchListSource<TKey, TData, TRequestParams : Any, TUpdate>
|
|||
updateResults.emit(action.updateRequest to result)
|
||||
}
|
||||
|
||||
private suspend fun updateBatchesAsyncTask(action: BatchAction.UpdateBatches<TKey, TUpdate>) {
|
||||
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<TKey>) =
|
||||
object : BatchUpdateFetcher.UpdateContext<TKey, TData> {
|
||||
|
||||
override suspend fun update(update: List<Batch<TKey, TData>>.() -> BatchUpdateResult<TKey, TData>) {
|
||||
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<TKey, TData, TRequestParams : Any, TUpdate>
|
|||
}
|
||||
|
||||
private fun stopUpdates(predicate: (BatchAction.UpdateBatches<TKey, TUpdate>) -> 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 {
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ package com.tangem.pagination
|
|||
* @param TData type of the data.
|
||||
* @param TUpdate type of the update request.
|
||||
*/
|
||||
fun interface BatchUpdateFetcher<TKey, TData, TUpdate> {
|
||||
interface BatchUpdateFetcher<TKey, TData, TUpdate> {
|
||||
|
||||
/**
|
||||
* Fetches updates for a batch of data.
|
||||
|
|
@ -19,5 +19,45 @@ fun interface BatchUpdateFetcher<TKey, TData, TUpdate> {
|
|||
* @param updateRequest request to update the data.
|
||||
* @return result of the update operation.
|
||||
*/
|
||||
suspend fun fetchUpdate(toUpdate: List<Batch<TKey, TData>>, updateRequest: TUpdate): BatchUpdateResult<TKey, TData>
|
||||
suspend fun fetchUpdate(
|
||||
toUpdate: List<Batch<TKey, TData>>,
|
||||
updateRequest: TUpdate,
|
||||
): BatchUpdateResult<TKey, TData> = 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<TKey, TData>.fetchUpdateAsync(
|
||||
toUpdate: List<Batch<TKey, TData>>,
|
||||
updateRequest: TUpdate,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Context for updating the data.
|
||||
* Used by [BatchListSource] to provide a way to update batches by [fetchUpdateAsync] method.
|
||||
*/
|
||||
interface UpdateContext<TKey, TData> {
|
||||
|
||||
/**
|
||||
* 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<Batch<TKey, TData>>.() -> BatchUpdateResult<TKey, TData>)
|
||||
}
|
||||
}
|
||||
|
|
@ -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<TRequestParams : Any, TData>(
|
||||
private val prefetchDistance: Int,
|
||||
private val batchSize: Int,
|
||||
private val fetch: suspend (request: Request<TRequestParams>) -> BatchFetchResult<TData>,
|
||||
private val subFetcher: SubFetcher<TRequestParams, TData>,
|
||||
) : BatchFetcher<TRequestParams, TData> {
|
||||
|
||||
data class Request<TRequest>(
|
||||
data class Request<TRequestParams>(
|
||||
val limit: Int,
|
||||
val offset: Int,
|
||||
val request: TRequest,
|
||||
val params: TRequestParams,
|
||||
)
|
||||
|
||||
fun interface SubFetcher<TRequestParams : Any, TData> {
|
||||
suspend fun fetch(
|
||||
request: Request<TRequestParams>,
|
||||
lastResult: BatchFetchResult<TData>?,
|
||||
): BatchFetchResult<TData>
|
||||
}
|
||||
|
||||
private val lastRequest = MutableStateFlow<Request<TRequestParams>?>(null)
|
||||
|
||||
override suspend fun fetchFirst(requestParams: TRequestParams): BatchFetchResult<TData> {
|
||||
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<TRequestParams : Any, TData>(
|
|||
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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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<TokenMarketListConfig, List<TokenMarket>>(
|
||||
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<TokenMarketListConfig, List<TokenMarket>> {
|
||||
|
||||
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<TokenMarketListConfig>,
|
||||
lastResult: BatchFetchResult<List<TokenMarket>>?,
|
||||
): BatchFetchResult<List<TokenMarket>> {
|
||||
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<Int, List<TokenMarket>, 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<Int, TokenMarketListConfig, TokenMarketUpdateRequest>,
|
||||
): BatchFlow<Int, List<TokenMarket>, 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",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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<Int, List<TokenMarket>, TokenMarketUpdateRequest> {
|
||||
|
||||
private val tokenListChartsConverter = TokenMarketChartsConverter(TokenListChartConverter())
|
||||
private val tokenQuotesConverter = TokenQuotesConverter()
|
||||
|
||||
override suspend fun BatchUpdateFetcher.UpdateContext<Int, List<TokenMarket>>.fetchUpdateAsync(
|
||||
toUpdate: List<Batch<Int, List<TokenMarket>>>,
|
||||
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<Batch<Int, List<TokenMarket>>>.changeChartsInBatches(
|
||||
updateRequest: TokenMarketUpdateRequest.UpdateChart,
|
||||
batchToUpdate: Batch<Int, List<TokenMarket>>,
|
||||
update: TokenMarketChartListResponse,
|
||||
): List<Batch<Int, List<TokenMarket>>> = 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",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -20,9 +20,9 @@ class TokenMarketListConverter : Converter<TokenMarketListResponse, List<TokenMa
|
|||
tokenQuotes = TokenQuotes(
|
||||
currentPrice = token.currentPrice,
|
||||
priceChanges = mapOf(
|
||||
PriceChangeInterval.H24 to token.priceChangePercentage.h24,
|
||||
PriceChangeInterval.WEEK to token.priceChangePercentage.week1,
|
||||
PriceChangeInterval.MONTH to token.priceChangePercentage.day30,
|
||||
PriceChangeInterval.H24 to token.priceChangePercentage.h24.movePointLeft(2),
|
||||
PriceChangeInterval.WEEK to token.priceChangePercentage.week1.movePointLeft(2),
|
||||
PriceChangeInterval.MONTH to token.priceChangePercentage.day30.movePointLeft(2),
|
||||
),
|
||||
),
|
||||
tokenCharts = TokenMarket.Charts(null, null, null),
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package com.tangem.data.markets.converters
|
|||
import com.tangem.datasource.api.tangemTech.models.QuotesResponse
|
||||
import com.tangem.domain.markets.PriceChangeInterval
|
||||
import com.tangem.domain.markets.TokenQuotes
|
||||
import java.math.BigDecimal
|
||||
|
||||
class TokenQuotesConverter {
|
||||
|
||||
|
|
@ -15,15 +16,9 @@ class TokenQuotesConverter {
|
|||
"Price is not found in the QuotesResponse. This shouldn't have happened."
|
||||
},
|
||||
priceChanges = mapOf(
|
||||
PriceChangeInterval.H24 to requireNotNull(quote.priceChange1w) {
|
||||
"priceChange1w is not found in the QuotesResponse. This shouldn't have happened."
|
||||
},
|
||||
PriceChangeInterval.WEEK to requireNotNull(quote.priceChange1w) {
|
||||
"priceChange1w is not found in the QuotesResponse. This shouldn't have happened."
|
||||
},
|
||||
PriceChangeInterval.MONTH to requireNotNull(quote.priceChange30d) {
|
||||
"priceChange30d is not found in the QuotesResponse. This shouldn't have happened."
|
||||
},
|
||||
PriceChangeInterval.H24 to (quote.priceChange24h ?: BigDecimal.ZERO).movePointLeft(2),
|
||||
PriceChangeInterval.WEEK to (quote.priceChange1w ?: BigDecimal.ZERO).movePointLeft(2),
|
||||
PriceChangeInterval.MONTH to (quote.priceChange30d ?: BigDecimal.ZERO).movePointLeft(2),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package com.tangem.data.markets.di
|
|||
import com.tangem.data.markets.DefaultMarketsTokenRepository
|
||||
import com.tangem.datasource.api.markets.TangemTechMarketsApi
|
||||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||
import com.tangem.datasource.di.DevTangemApi
|
||||
import com.tangem.domain.markets.repositories.MarketsTokenRepository
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import dagger.Module
|
||||
|
|
@ -18,8 +19,8 @@ internal object MarketsDataModule {
|
|||
@Provides
|
||||
@Singleton
|
||||
fun provideMarketsRepository(
|
||||
marketsApi: TangemTechMarketsApi,
|
||||
tangemTechApi: TangemTechApi,
|
||||
@DevTangemApi marketsApi: TangemTechMarketsApi,
|
||||
@DevTangemApi tangemTechApi: TangemTechApi,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
): MarketsTokenRepository {
|
||||
return DefaultMarketsTokenRepository(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,27 @@
|
|||
package com.tangem.data.markets.utils
|
||||
|
||||
import kotlinx.coroutines.currentCoroutineContext
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.ensureActive
|
||||
import kotlinx.coroutines.yield
|
||||
import timber.log.Timber
|
||||
import kotlin.coroutines.cancellation.CancellationException
|
||||
|
||||
@Suppress("UnconditionalJumpStatementInLoop")
|
||||
internal suspend fun <T> 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -4,5 +4,15 @@ import java.math.BigDecimal
|
|||
|
||||
data class TokenQuotes(
|
||||
val currentPrice: BigDecimal,
|
||||
val priceChanges: Map<PriceChangeInterval, BigDecimal>,
|
||||
)
|
||||
private val priceChanges: Map<PriceChangeInterval, BigDecimal>,
|
||||
) {
|
||||
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]!!
|
||||
}
|
||||
|
|
@ -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<Int, TokenMarketListConfig, TokenMarketUpdateRequest>
|
||||
typealias TokenListBatchFlow = BatchFlow<Int, List<TokenMarket>, TokenMarketUpdateRequest>
|
||||
|
||||
class GetMarketsTokenListFlowUseCase(
|
||||
private val marketsTokenRepository: MarketsTokenRepository,
|
||||
) {
|
||||
operator fun invoke(batchingContext: TokenListBatchingContext): TokenListBatchFlow {
|
||||
return marketsTokenRepository.getTokenListFlow(batchingContext)
|
||||
}
|
||||
}
|
||||
|
|
@ -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<Int, TokenMarketListConfig, TokenMarketUpdateRequest>,
|
||||
): BatchFlow<Int, List<TokenMarket>, TokenMarketUpdateRequest>
|
||||
fun getTokenListFlow(batchingContext: TokenListBatchingContext): TokenListBatchFlow
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue