Updated on 2026-08-14

This commit is contained in:
Tangem 2023-07-28 17:08:24 +03:00
parent 4e38602301
commit 2317703e37
29 changed files with 369 additions and 169 deletions

View file

@ -1,7 +1,8 @@
package com.tangem.datasource.di
import com.tangem.datasource.local.cache.CacheKeysStore
import com.tangem.datasource.local.cache.RuntimeCacheKeysStore
import com.tangem.datasource.local.cache.DefaultCacheKeysStore
import com.tangem.datasource.local.datastore.RuntimeDataStore
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@ -15,6 +16,8 @@ internal object CacheKeysStoreModule {
@Provides
@Singleton
fun provideCacheKeysStore(): CacheKeysStore {
return RuntimeCacheKeysStore()
return DefaultCacheKeysStore(
dataStore = RuntimeDataStore(),
)
}
}

View file

@ -1,8 +1,10 @@
package com.tangem.datasource.di
import com.squareup.moshi.Moshi
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
import com.tangem.datasource.files.FileReader
import com.tangem.datasource.local.token.FileUserTokensStore
import com.tangem.datasource.local.datastore.FileDataStore
import com.tangem.datasource.local.token.DefaultUserTokensStore
import com.tangem.datasource.local.token.UserTokensStore
import dagger.Module
import dagger.Provides
@ -17,6 +19,8 @@ internal object UserTokensStoreModule {
@Provides
@Singleton
fun provideUserTokensStore(fileReader: FileReader, @NetworkMoshi moshi: Moshi): UserTokensStore {
return FileUserTokensStore(fileReader, moshi)
return DefaultUserTokensStore(
dataStore = FileDataStore(fileReader, moshi.adapter(UserTokensResponse::class.java)),
)
}
}

View file

@ -0,0 +1,23 @@
package com.tangem.datasource.di
import com.tangem.datasource.local.datastore.RuntimeDataStore
import com.tangem.datasource.local.walletmanager.DefaultWalletManagersStore
import com.tangem.datasource.local.walletmanager.WalletManagersStore
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal object WalletManagersStoreModule {
@Provides
@Singleton
fun provideWalletManagersStore(): WalletManagersStore {
return DefaultWalletManagersStore(
dataStore = RuntimeDataStore(),
)
}
}

View file

@ -19,4 +19,8 @@ class AndroidFileReader @Inject constructor(@ApplicationContext private val cont
stream.write(content.toByteArray(), 0, content.length)
}
}
override fun removeFile(fileName: String) {
context.deleteFile(fileName)
}
}

View file

@ -1,6 +1,10 @@
package com.tangem.datasource.files
interface FileReader {
fun readFile(fileName: String): String
fun rewriteFile(content: String, fileName: String)
fun removeFile(fileName: String)
}

View file

@ -4,11 +4,11 @@ import com.tangem.datasource.local.cache.model.CacheKey
interface CacheKeysStore {
fun getOrNull(id: String): CacheKey?
suspend fun getSyncOrNull(key: String): CacheKey?
fun addOrReplace(key: CacheKey)
suspend fun store(key: CacheKey)
fun remove(id: String)
suspend fun remove(key: String)
fun clear()
suspend fun clear()
}

View file

@ -0,0 +1,18 @@
package com.tangem.datasource.local.cache
import com.tangem.datasource.local.cache.model.CacheKey
import com.tangem.datasource.local.datastore.core.StringKeyDataStore
import com.tangem.datasource.local.datastore.core.StringKeyDataStoreDecorator
internal class DefaultCacheKeysStore(
dataStore: StringKeyDataStore<CacheKey>,
) : CacheKeysStore, StringKeyDataStoreDecorator<String, CacheKey>(dataStore) {
override fun provideStringKey(key: String): String {
return key
}
override suspend fun store(key: CacheKey) {
store(key.id, key)
}
}

View file

@ -1,25 +0,0 @@
package com.tangem.datasource.local.cache
import com.tangem.datasource.local.cache.model.CacheKey
import com.tangem.datasource.local.datastore.RuntimeDataStore
internal class RuntimeCacheKeysStore : CacheKeysStore {
private val store = RuntimeDataStore(keyProvider = CacheKey::id)
override fun getOrNull(id: String): CacheKey? {
return store.getSync { it.id == id }.singleOrNull()
}
override fun addOrReplace(key: CacheKey) {
store.addOrReplace(key)
}
override fun remove(id: String) {
store.remove { it.id == id }
}
override fun clear() {
store.clear()
}
}

View file

@ -2,47 +2,59 @@ package com.tangem.datasource.local.datastore
import com.squareup.moshi.JsonAdapter
import com.tangem.datasource.files.FileReader
import com.tangem.datasource.local.datastore.core.StringKeyDataStore
import com.tangem.datasource.local.datastore.model.WriteTrigger
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.*
import timber.log.Timber
internal class FileDataStore<Data : Any>(
private val fileNameProvider: (key: String) -> String,
internal class FileDataStore<Value : Any>(
private val fileReader: FileReader,
private val adapter: JsonAdapter<Data>,
) {
private val adapter: JsonAdapter<Value>,
) : StringKeyDataStore<Value> {
private val writeTrigger = MutableSharedFlow<WriteTrigger>(
replay = 1,
onBufferOverflow = BufferOverflow.DROP_OLDEST,
)
fun get(key: String): Flow<Data> {
override fun get(key: String): Flow<Value> {
return writeTrigger
.onEmpty { emit(WriteTrigger) }
.map { getInternal(fileNameProvider(key)) }
.map { getInternal(key) }
.filterNotNull()
}
fun getSyncOrNull(key: String): Data? {
return getInternal(fileNameProvider(key))
override suspend fun getSyncOrNull(key: String): Value? {
return getInternal(key)
}
fun store(key: String, content: Data) {
val fileName = fileNameProvider(key)
override suspend fun store(key: String, item: Value) {
try {
val json = adapter.toJson(content)
val json = adapter.toJson(item)
fileReader.rewriteFile(json, fileName)
fileReader.rewriteFile(json, key)
writeTrigger.tryEmit(WriteTrigger)
} catch (e: Throwable) {
Timber.e(e, "Unable to write file: $fileName")
Timber.e(e, "Unable to write file: $key")
}
}
private fun getInternal(fileName: String): Data? {
override suspend fun store(items: Map<String, Value>) {
items.forEach { (key, item) ->
store(key, item)
}
}
override suspend fun remove(key: String) {
fileReader.removeFile(key)
}
override suspend fun clear() {
// TODO: Implement if needed
}
private fun getInternal(fileName: String): Value? {
return try {
val json = fileReader.readFile(fileName)

View file

@ -1,59 +1,49 @@
package com.tangem.datasource.local.datastore
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.update
import com.tangem.datasource.local.datastore.core.StringKeyDataStore
import kotlinx.coroutines.flow.*
internal class RuntimeDataStore<Key : Any, Data : Any>(private val keyProvider: (Data) -> Key) {
internal class RuntimeDataStore<Data : Any> : StringKeyDataStore<Data> {
private val store = MutableStateFlow<HashMap<Key, Data>>(hashMapOf())
private val store = MutableStateFlow<HashMap<String, Data>>(hashMapOf())
fun get(selector: (Data) -> Boolean = { true }): Flow<List<Data>> {
return store.map { getInternal(it, selector) }
override fun get(key: String): Flow<Data> {
return store
.map { value -> value[key] }
.filterNotNull()
}
fun getSync(selector: (Data) -> Boolean = { true }): List<Data> {
return getInternal(store.value, selector)
override suspend fun getSyncOrNull(key: String): Data? {
return store.value[key]
}
// TODO: Uncomment if needed
// fun addOrReplace(items: Collection<Data>) {
// if (items.isEmpty()) return
//
// val storeValue = store.value
//
// items.forEach { item ->
// storeValue[keyProvider(item)] = item
// }
//
// store.value = storeValue
// }
override suspend fun store(key: String, item: Data) {
store.update { value ->
value[key] = item
fun addOrReplace(item: Data) {
val storeValue = store.value
storeValue[keyProvider(item)] = item
store.value = storeValue
}
fun remove(selector: (Data) -> Boolean) {
val storeValue = store.value
storeValue.forEach { (key, item) ->
if (selector(item)) {
storeValue.remove(key)
}
value
}
store.value = storeValue
}
fun clear() {
override suspend fun store(items: Map<String, Data>) {
store.update { value ->
items.forEach { (key, item) ->
value[key] = item
}
value
}
}
override suspend fun remove(key: String) {
store.update { value ->
value.remove(key)
value
}
}
override suspend fun clear() {
store.update { hashMapOf() }
}
private fun getInternal(store: HashMap<Key, Data>, selector: (Data) -> Boolean): List<Data> {
return store.values.filter { selector(it) }
}
}

View file

@ -0,0 +1,18 @@
package com.tangem.datasource.local.datastore.core
import kotlinx.coroutines.flow.Flow
internal interface DataStore<Key : Any, Value : Any> {
fun get(key: Key): Flow<Value>
suspend fun getSyncOrNull(key: Key): Value?
suspend fun store(key: Key, item: Value)
suspend fun store(items: Map<Key, Value>)
suspend fun remove(key: Key)
suspend fun clear()
}

View file

@ -0,0 +1,3 @@
package com.tangem.datasource.local.datastore.core
internal interface StringKeyDataStore<Value : Any> : DataStore<String, Value>

View file

@ -0,0 +1,36 @@
package com.tangem.datasource.local.datastore.core
import kotlinx.coroutines.flow.Flow
internal abstract class StringKeyDataStoreDecorator<Key : Any, Value : Any>(
private val dataStore: StringKeyDataStore<Value>,
) : DataStore<Key, Value> {
abstract fun provideStringKey(key: Key): String
override fun get(key: Key): Flow<Value> {
return dataStore.get(provideStringKey(key))
}
override suspend fun getSyncOrNull(key: Key): Value? {
return dataStore.getSyncOrNull(provideStringKey(key))
}
override suspend fun store(key: Key, item: Value) {
dataStore.store(provideStringKey(key), item)
}
override suspend fun store(items: Map<Key, Value>) {
dataStore.store(
items = items.mapKeys { (key, _) -> provideStringKey(key) },
)
}
override suspend fun remove(key: Key) {
dataStore.remove(provideStringKey(key))
}
override suspend fun clear() {
dataStore.clear()
}
}

View file

@ -0,0 +1,15 @@
package com.tangem.datasource.local.token
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
import com.tangem.datasource.local.datastore.core.StringKeyDataStore
import com.tangem.datasource.local.datastore.core.StringKeyDataStoreDecorator
import com.tangem.domain.wallets.models.UserWalletId
internal class DefaultUserTokensStore(
dataStore: StringKeyDataStore<UserTokensResponse>,
) : UserTokensStore, StringKeyDataStoreDecorator<UserWalletId, UserTokensResponse>(dataStore) {
override fun provideStringKey(key: UserWalletId): String {
return key.stringValue
}
}

View file

@ -1,29 +0,0 @@
package com.tangem.datasource.local.token
import com.squareup.moshi.Moshi
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
import com.tangem.datasource.files.FileReader
import com.tangem.datasource.local.datastore.FileDataStore
import com.tangem.domain.wallets.models.UserWalletId
import kotlinx.coroutines.flow.Flow
internal class FileUserTokensStore(fileReader: FileReader, moshi: Moshi) : UserTokensStore {
private val store = FileDataStore<UserTokensResponse>(
fileNameProvider = { key -> "user_tokens_$key" },
fileReader = fileReader,
adapter = moshi.adapter(UserTokensResponse::class.java),
)
override fun get(userWalletId: UserWalletId): Flow<UserTokensResponse> {
return store.get(userWalletId.stringValue)
}
override suspend fun getSyncOrNull(userWalletId: UserWalletId): UserTokensResponse? {
return store.getSyncOrNull(userWalletId.stringValue)
}
override suspend fun store(userWalletId: UserWalletId, tokens: UserTokensResponse) {
store.store(userWalletId.stringValue, tokens)
}
}

View file

@ -6,9 +6,9 @@ import kotlinx.coroutines.flow.Flow
interface UserTokensStore {
fun get(userWalletId: UserWalletId): Flow<UserTokensResponse>
fun get(key: UserWalletId): Flow<UserTokensResponse>
suspend fun getSyncOrNull(userWalletId: UserWalletId): UserTokensResponse?
suspend fun getSyncOrNull(key: UserWalletId): UserTokensResponse?
suspend fun store(userWalletId: UserWalletId, tokens: UserTokensResponse)
suspend fun store(key: UserWalletId, item: UserTokensResponse)
}

View file

@ -5,5 +5,5 @@ import com.tangem.domain.wallets.models.UserWalletId
interface UserWalletsStore {
suspend fun getOrNull(userWalletId: UserWalletId): UserWallet?
suspend fun getSyncOrNull(key: UserWalletId): UserWallet?
}

View file

@ -0,0 +1,43 @@
package com.tangem.datasource.local.walletmanager
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.WalletManager
import com.tangem.datasource.local.datastore.core.StringKeyDataStore
import com.tangem.datasource.local.datastore.core.StringKeyDataStoreDecorator
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.utils.extensions.plusOrReplace
internal class DefaultWalletManagersStore(
dataStore: StringKeyDataStore<List<WalletManager>>,
) : WalletManagersStore, StringKeyDataStoreDecorator<UserWalletId, List<WalletManager>>(dataStore) {
override fun provideStringKey(key: UserWalletId): String {
return key.stringValue
}
override suspend fun getSyncOrNull(
userWalletId: UserWalletId,
blockchain: Blockchain,
derivationPath: String?,
): WalletManager? {
val walletManagers = getSyncOrNull(userWalletId)
return walletManagers?.singleOrNull {
it.wallet.blockchain == blockchain &&
it.wallet.publicKey.derivationPath?.rawPath == derivationPath
}
}
override suspend fun store(userWalletId: UserWalletId, walletManager: WalletManager) {
val walletManagers = getSyncOrNull(userWalletId)
val updatedWalletManagers = walletManagers
?.plusOrReplace(walletManager) {
it.wallet.blockchain == walletManager.wallet.blockchain &&
it.wallet.publicKey == walletManager.wallet.publicKey
}
?: listOf(walletManager)
store(userWalletId, updatedWalletManagers)
}
}

View file

@ -0,0 +1,18 @@
package com.tangem.datasource.local.walletmanager
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.WalletManager
import com.tangem.domain.wallets.models.UserWalletId
interface WalletManagersStore {
suspend fun getSyncOrNull(
userWalletId: UserWalletId,
blockchain: Blockchain,
derivationPath: String?,
): WalletManager?
suspend fun store(userWalletId: UserWalletId, walletManager: WalletManager)
suspend fun clear()
}