From c6fc0d49bab9697f2b9926874368ce9dbcd5c088 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 27 Sep 2023 17:08:13 +0300 Subject: [PATCH] Updated on 2026-08-14 --- .../tangem/datasource/di/QuotesStoreModule.kt | 14 +--- .../local/datastore/FileDataStore.kt | 8 +++ .../local/datastore/RuntimeDataStore.kt | 4 ++ .../datastore/SharedPreferencesDataStore.kt | 8 +++ .../local/datastore/core/DataStore.kt | 71 +++++++++++++++++++ .../core/KeylessDataStoreDecorator.kt | 2 +- .../core/StringKeyDataStoreDecorator.kt | 4 ++ .../local/quote/DefaultQuotesStore.kt | 21 ++++-- .../CurrenciesStatusesOperations.kt | 10 +-- ...PrimaryCurrencyStatusUpdatesUseCaseTest.kt | 26 ++++--- 10 files changed, 135 insertions(+), 33 deletions(-) diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/QuotesStoreModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/QuotesStoreModule.kt index 91de405a4b..ff605b0138 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/di/QuotesStoreModule.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/di/QuotesStoreModule.kt @@ -1,15 +1,11 @@ package com.tangem.datasource.di -import android.content.Context -import com.squareup.moshi.Moshi -import com.tangem.datasource.local.datastore.JsonSharedPreferencesDataStore +import com.tangem.datasource.local.datastore.RuntimeDataStore import com.tangem.datasource.local.quote.DefaultQuotesStore import com.tangem.datasource.local.quote.QuotesStore -import com.tangem.datasource.local.quote.model.StoredQuote import dagger.Module import dagger.Provides import dagger.hilt.InstallIn -import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent import javax.inject.Singleton @@ -19,13 +15,9 @@ internal object QuotesStoreModule { @Provides @Singleton - fun provideQuotesStore(@ApplicationContext context: Context, @NetworkMoshi moshi: Moshi): QuotesStore { + fun provideQuotesStore(): QuotesStore { return DefaultQuotesStore( - dataStore = JsonSharedPreferencesDataStore( - preferencesName = "quotes", - context = context, - adapter = moshi.adapter(StoredQuote::class.java), - ), + dataStore = RuntimeDataStore(), ) } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/datastore/FileDataStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/datastore/FileDataStore.kt index 768a0e4c6a..e9b2317f35 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/datastore/FileDataStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/datastore/FileDataStore.kt @@ -17,6 +17,14 @@ internal class FileDataStore( ) : StringKeyDataStore { private val writeTrigger = Trigger() + override suspend fun isEmpty(): Boolean { + val e = NotImplementedError("`isEmpty()` function not implemented for `FileDataStore`") + Timber.e(e) + + throw e + } + + override suspend fun contains(key: String): Boolean = getSyncOrNull(key) != null override fun get(key: String): Flow { return writeTrigger diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/datastore/RuntimeDataStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/datastore/RuntimeDataStore.kt index 77aa987353..db39d064d3 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/datastore/RuntimeDataStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/datastore/RuntimeDataStore.kt @@ -11,6 +11,10 @@ internal class RuntimeDataStore : StringKeyDataStore { store.tryEmit(value = null) } + override suspend fun isEmpty(): Boolean = store.firstOrNull().isNullOrEmpty() + + override suspend fun contains(key: String): Boolean = getSyncOrNull(key) != null + override fun get(key: String): Flow { return store.mapNotNull { value -> value?.get(key) diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/datastore/SharedPreferencesDataStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/datastore/SharedPreferencesDataStore.kt index bf987439b8..417b4e9ac4 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/datastore/SharedPreferencesDataStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/datastore/SharedPreferencesDataStore.kt @@ -26,6 +26,14 @@ internal abstract class SharedPreferencesDataStore( abstract fun storeByKey(key: String, value: Value) + override suspend fun isEmpty(): Boolean { + return sharedPreferences.all.isEmpty() + } + + override suspend fun contains(key: String): Boolean { + return sharedPreferences.contains(key) + } + override fun get(key: String): Flow { return writeTrigger .mapNotNull { getInternal(key) } diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/datastore/core/DataStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/datastore/core/DataStore.kt index 828b0cb7b5..4746617f3b 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/datastore/core/DataStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/datastore/core/DataStore.kt @@ -2,23 +2,94 @@ package com.tangem.datasource.local.datastore.core import kotlinx.coroutines.flow.Flow +/** + * Represents a generic key-value data store. + * + * @param Key The type of the keys used to identify values in the store. + * @param Value The type of the values stored. + */ internal interface DataStore { + /** + * Checks if the data store is empty. + * + * @return `true` if the data store has no entries, otherwise `false`. + */ + suspend fun isEmpty(): Boolean + + /** + * Checks if the data store contains an entry with the specified key. + * + * @param key The key to check for presence in the store. + * @return `true` if the key is present, otherwise `false`. + */ + suspend fun contains(key: Key): Boolean + + /** + * Retrieves a value updates associated with the given key, as a flow. + * + * @param key The key to look up in the store. + * @return A flow emitting the value associated with the given key. + */ fun get(key: Key): Flow + /** + * Retrieves all values updates from the data store, as a flow. + * + * @return A flow emitting a list of all values in the store. + */ fun getAll(): Flow> + /** + * Retrieves a value associated with the given key synchronously. + * + * If the key does not exist, this method returns `null`. + * + * @param key The key to look up in the store. + * @return The value associated with the key, or `null` if not present. + */ suspend fun getSyncOrNull(key: Key): Value? + /** + * Retrieves all values from the data store synchronously. + * + * If the store is empty, this method returns `null`. + * + * @return A list of all values in the store, or `null` if empty. + */ suspend fun getAllSyncOrNull(): List? + /** + * Stores a value in the data store associated with the given key. + * + * @param key The key to associate with the value. + * @param value The value to store. + */ suspend fun store(key: Key, value: Value) + /** + * Stores multiple values in the data store with their associated keys. + * + * @param values A map of keys to values to store. + */ suspend fun store(values: Map) + /** + * Removes a value associated with the given key from the data store. + * + * @param key The key of the value to remove. + */ suspend fun remove(key: Key) + /** + * Removes values associated with the given keys from the data store. + * + * @param keys Keys of values to remove. + */ suspend fun remove(keys: Collection) + /** + * Clears all entries from the data store. + */ suspend fun clear() } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/datastore/core/KeylessDataStoreDecorator.kt b/core/datasource/src/main/java/com/tangem/datasource/local/datastore/core/KeylessDataStoreDecorator.kt index fd8f6b333c..421be2b698 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/datastore/core/KeylessDataStoreDecorator.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/datastore/core/KeylessDataStoreDecorator.kt @@ -13,7 +13,7 @@ internal abstract class KeylessDataStoreDecorator( open suspend fun store(item: Value) = store(key, item) - open suspend fun isEmpty() = getSyncOrNull() == null + override suspend fun isEmpty(): Boolean = getSyncOrNull() == null private companion object { const val DEFAULT_STRING_KEY = "key" diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/datastore/core/StringKeyDataStoreDecorator.kt b/core/datasource/src/main/java/com/tangem/datasource/local/datastore/core/StringKeyDataStoreDecorator.kt index d897ab9e7a..73c2afb6d4 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/datastore/core/StringKeyDataStoreDecorator.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/datastore/core/StringKeyDataStoreDecorator.kt @@ -8,6 +8,10 @@ internal abstract class StringKeyDataStoreDecorator( abstract fun provideStringKey(key: Key): String + override suspend fun isEmpty(): Boolean = wrappedDataStore.isEmpty() + + override suspend fun contains(key: Key): Boolean = wrappedDataStore.contains(provideStringKey(key)) + override fun get(key: Key): Flow { return wrappedDataStore.get(provideStringKey(key)) } diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/quote/DefaultQuotesStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/quote/DefaultQuotesStore.kt index 4d547208a2..667cb0f4b1 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/quote/DefaultQuotesStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/quote/DefaultQuotesStore.kt @@ -5,6 +5,7 @@ import com.tangem.datasource.local.datastore.core.StringKeyDataStore import com.tangem.datasource.local.quote.model.StoredQuote import com.tangem.domain.tokens.model.CryptoCurrency import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.channelFlow import kotlinx.coroutines.flow.combine internal class DefaultQuotesStore( @@ -12,16 +13,24 @@ internal class DefaultQuotesStore( ) : QuotesStore { override fun get(currenciesIds: Set): Flow> { - val flows = currenciesIds.mapNotNull { currencyId -> - dataStore.get(currencyId.rawCurrencyId ?: return@mapNotNull null) - } + return channelFlow { + val flows = currenciesIds.mapNotNull { currencyId -> + currencyId.rawCurrencyId?.let(dataStore::get) + } - return combine(flows) { quotes -> quotes.toSet() } + if (dataStore.isEmpty() || flows.isEmpty()) { + send(emptySet()) + } + + combine(flows) { quotes -> quotes.toSet() }.collect(::send) + } } override suspend fun store(response: QuotesResponse) { - response.quotes.forEach { (rawCurrencyId, quote) -> - dataStore.store(rawCurrencyId, StoredQuote(rawCurrencyId, quote)) + val quotes = response.quotes.mapValues { (id, quote) -> + StoredQuote(id, quote) } + + dataStore.store(quotes) } } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt index 445b076284..fc4ec95f89 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt @@ -99,15 +99,17 @@ internal class CurrenciesStatusesOperations( val quoteFlow = getQuotes(currenciesIds) .map { maybeQuotes -> - maybeQuotes.map { quotes -> - quotes.singleOrNull { it.rawCurrencyId == currency.id.rawCurrencyId } + maybeQuotes.flatMap { quotes -> + quotes.singleOrNull { it.rawCurrencyId == currency.id.rawCurrencyId }?.right() + ?: Error.EmptyQuotes.left() } } val statusFlow = getNetworksStatuses(networks) .map { maybeStatuses -> - maybeStatuses.map { statuses -> - statuses.singleOrNull { it.network == currency.network } + maybeStatuses.flatMap { statuses -> + statuses.singleOrNull { it.network == currency.network }?.right() + ?: Error.EmptyNetworksStatuses.left() } } diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/GetPrimaryCurrencyStatusUpdatesUseCaseTest.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/GetPrimaryCurrencyStatusUpdatesUseCaseTest.kt index 010976fe3a..4abddd57fa 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/GetPrimaryCurrencyStatusUpdatesUseCaseTest.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/GetPrimaryCurrencyStatusUpdatesUseCaseTest.kt @@ -9,10 +9,7 @@ import com.tangem.domain.tokens.mock.MockNetworks import com.tangem.domain.tokens.mock.MockQuotes import com.tangem.domain.tokens.mock.MockTokens import com.tangem.domain.tokens.mock.MockTokensStates -import com.tangem.domain.tokens.model.CryptoCurrency -import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.domain.tokens.model.NetworkStatus -import com.tangem.domain.tokens.model.Quote +import com.tangem.domain.tokens.model.* import com.tangem.domain.tokens.repository.MockCurrenciesRepository import com.tangem.domain.tokens.repository.MockNetworksRepository import com.tangem.domain.tokens.repository.MockQuotesRepository @@ -24,6 +21,7 @@ import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.runTest import org.junit.Test +import java.math.BigDecimal internal class GetPrimaryCurrencyStatusUpdatesUseCaseTest { @@ -113,9 +111,17 @@ internal class GetPrimaryCurrencyStatusUpdatesUseCaseTest { } @Test - fun `when quotes are empty and statuses are verified then loading token should be received`() = runTest { - val expectedResult = MockTokensStates.tokenState1 - .copy(value = CryptoCurrencyStatus.Loading) + fun `when quotes are empty and statuses are verified then token without quote should be received`() = runTest { + val expectedResult = with(MockTokensStates.tokenState1) { + copy( + value = CryptoCurrencyStatus.NoQuote( + amount = BigDecimal.TEN, + hasCurrentNetworkTransactions = false, + pendingTransactions = emptySet(), + networkAddress = NetworkAddress.Single(defaultAddress = "mock"), + ), + ) + } .right() val useCase = getUseCase( @@ -131,10 +137,8 @@ internal class GetPrimaryCurrencyStatusUpdatesUseCaseTest { } @Test - fun `when quotes are loaded and statuses are empty then loading token should be received`() = runTest { - val expectedResult = MockTokensStates.tokenState1 - .copy(value = CryptoCurrencyStatus.Loading) - .right() + fun `when quotes are loaded and statuses are empty then error be received`() = runTest { + val expectedResult = CurrencyStatusError.UnableToCreateCurrency.left() val useCase = getUseCase( statuses = flowOf(emptySet().right()),