Updated on 2026-08-14

This commit is contained in:
Tangem 2025-05-30 12:29:21 +04:00
parent 08da87d369
commit e736f86583
9 changed files with 42 additions and 485 deletions

View file

@ -1,54 +0,0 @@
package com.tangem.datasource.di
import android.content.Context
import androidx.datastore.core.DataStore
import androidx.datastore.core.DataStoreFactory
import androidx.datastore.dataStoreFile
import com.squareup.moshi.Moshi
import com.tangem.datasource.api.tangemTech.models.QuotesResponse
import com.tangem.datasource.local.datastore.RuntimeSharedStore
import com.tangem.datasource.local.quote.DefaultQuotesStore
import com.tangem.datasource.local.quote.QuotesStore
import com.tangem.datasource.utils.MoshiDataStoreSerializer
import com.tangem.datasource.utils.mapWithStringKeyTypes
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal object QuotesStoreModule {
@Provides
@Singleton
fun providePersistenceQuotesStore(
@NetworkMoshi moshi: Moshi,
@ApplicationContext context: Context,
dispatchers: CoroutineDispatcherProvider,
): DataStore<Map<String, QuotesResponse.Quote>> {
return DataStoreFactory.create(
serializer = MoshiDataStoreSerializer(
moshi = moshi,
types = mapWithStringKeyTypes<QuotesResponse.Quote>(),
defaultValue = emptyMap(),
),
produceFile = { context.dataStoreFile(fileName = "quotes") },
scope = CoroutineScope(context = dispatchers.io + SupervisorJob()),
)
}
@Provides
@Singleton
fun provideQuotesStore(persistenceStore: DataStore<Map<String, QuotesResponse.Quote>>): QuotesStore {
return DefaultQuotesStore(
persistenceStore = persistenceStore,
runtimeStore = RuntimeSharedStore(),
)
}
}

View file

@ -1,128 +0,0 @@
package com.tangem.datasource.local.quote
import androidx.datastore.core.DataStore
import com.tangem.datasource.api.tangemTech.models.QuotesResponse
import com.tangem.datasource.local.datastore.RuntimeSharedStore
import com.tangem.datasource.local.quote.converter.QuoteConverter
import com.tangem.domain.models.StatusSource
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.tokens.model.Quote
import com.tangem.utils.extensions.addOrReplace
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
internal typealias QuotesByCurrencyId = Map<String, QuotesResponse.Quote>
/**
* Default implementation of [QuotesStore]
*
* @property persistenceStore persistence quotes store
* @property runtimeStore runtime quotes store
*/
internal class DefaultQuotesStore(
private val persistenceStore: DataStore<QuotesByCurrencyId>,
private val runtimeStore: RuntimeSharedStore<Set<Quote>>,
) : QuotesStore {
override fun get(currenciesIds: Set<CryptoCurrency.RawID>): Flow<Set<Quote>> = channelFlow {
val cachedQuotes = getCachedQuotes(currenciesIds = currenciesIds)
if (cachedQuotes.isNotEmpty()) {
send(cachedQuotes)
}
runtimeStore.get()
.onEach {
val mergedQuotes = mergeQuotes(
currenciesIds = currenciesIds,
cachedQuotes = cachedQuotes,
runtimeQuotes = it,
)
send(element = mergedQuotes)
}
.launchIn(scope = this)
}
override suspend fun getSync(currenciesIds: Set<CryptoCurrency.RawID>): Set<Quote> {
val runtimeQuotes = runtimeStore.getSyncOrNull()
val cachedQuotes = getCachedQuotes(currenciesIds = currenciesIds)
if (runtimeQuotes.isNullOrEmpty() && cachedQuotes.isEmpty()) return emptySet()
return mergeQuotes(
currenciesIds = currenciesIds,
cachedQuotes = cachedQuotes,
runtimeQuotes = runtimeQuotes.orEmpty(),
)
}
override suspend fun store(response: QuotesResponse) {
coroutineScope {
launch {
storeInRuntimeStore(
values = QuoteConverter(isCached = false).convertSet(input = response.quotes.entries),
)
}
launch { storeInPersistenceStore(response = response) }
}
}
override suspend fun storeEmptyQuotes(currenciesIds: Set<CryptoCurrency.RawID>) {
storeInRuntimeStore(values = currenciesIds.map(Quote::Empty).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> {
val ids = currenciesIds.map(CryptoCurrency.RawID::value).toSet()
val cachedQuotes = persistenceStore.data.firstOrNull().orEmpty().filterKeys { it in ids }
return QuoteConverter(isCached = true).convertSet(input = cachedQuotes.entries)
}
private fun mergeQuotes(
currenciesIds: Set<CryptoCurrency.RawID>,
cachedQuotes: Set<Quote.Value>,
runtimeQuotes: Set<Quote>,
): Set<Quote> {
return currenciesIds
.mapTo(hashSetOf()) { currencyId ->
val runtimeQuote = runtimeQuotes.firstOrNull { it.rawCurrencyId == currencyId }
if (runtimeQuote == null || runtimeQuote is Quote.Empty) {
getCachedQuoteIfPossible(cachedStatuses = cachedQuotes, currencyId = currencyId)
} else {
runtimeQuote
}
}
}
private fun getCachedQuoteIfPossible(cachedStatuses: Set<Quote.Value>, currencyId: CryptoCurrency.RawID): Quote {
return cachedStatuses.firstOrNull { it.rawCurrencyId == currencyId }
?.copy(source = StatusSource.ONLY_CACHE)
?: Quote.Empty(currencyId)
}
private suspend fun storeInRuntimeStore(values: Set<Quote>) {
runtimeStore.update(default = emptySet()) { saved ->
saved.addOrReplace(items = values) { prev, new -> prev.rawCurrencyId == new.rawCurrencyId }
}
}
private suspend fun storeInPersistenceStore(response: QuotesResponse) {
persistenceStore.updateData { storedQuotes -> storedQuotes + response.quotes }
}
}

View file

@ -1,24 +0,0 @@
package com.tangem.datasource.local.quote
import com.tangem.datasource.api.tangemTech.models.QuotesResponse
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.tokens.model.Quote
import kotlinx.coroutines.flow.Flow
/** Quotes store */
interface QuotesStore {
/** Get flow of quotes for [currenciesIds] */
fun get(currenciesIds: Set<CryptoCurrency.RawID>): Flow<Set<Quote>>
/** Get quotes for [currenciesIds] synchronously */
suspend fun getSync(currenciesIds: Set<CryptoCurrency.RawID>): Set<Quote>
/** Store [response] from remote */
suspend fun store(response: QuotesResponse)
/** Store [Quote.Empty] for [currenciesIds] */
suspend fun storeEmptyQuotes(currenciesIds: Set<CryptoCurrency.RawID>)
suspend fun refresh(currenciesIds: Set<CryptoCurrency.RawID>)
}