Updated on 2026-08-14

This commit is contained in:
Tangem 2023-07-20 20:00:23 +03:00
parent a57d077863
commit 11f4759544
13 changed files with 273 additions and 0 deletions

View file

@ -43,6 +43,7 @@ dependencies {
implementation(project(":data:source:preferences")) implementation(project(":data:source:preferences"))
implementation(projects.data.card) implementation(projects.data.card)
implementation(projects.data.tokens) implementation(projects.data.tokens)
implementation(projects.data.common)
/** Features */ /** Features */
implementation(project(":features:onboarding")) implementation(project(":features:onboarding"))

View file

@ -0,0 +1,20 @@
package com.tangem.datasource.di
import com.tangem.datasource.local.cache.CacheKeysStore
import com.tangem.datasource.local.cache.RuntimeCacheKeysStore
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 CacheKeysStoreModule {
@Provides
@Singleton
fun provideCacheKeysStore(): CacheKeysStore {
return RuntimeCacheKeysStore()
}
}

View file

@ -0,0 +1,14 @@
package com.tangem.datasource.local.cache
import com.tangem.datasource.local.cache.model.CacheKey
interface CacheKeysStore {
fun get(id: String): CacheKey?
fun addOrReplace(key: CacheKey)
fun remove(id: String)
fun clear()
}

View file

@ -0,0 +1,25 @@
package com.tangem.datasource.local.cache
import com.tangem.datasource.local.cache.model.CacheKey
import com.tangem.datasource.local.store.RuntimeStore
internal class RuntimeCacheKeysStore : CacheKeysStore {
private val store = RuntimeStore(keyProvider = CacheKey::id)
override fun get(id: String): CacheKey? {
return store.getSync { it.id == id }.firstOrNull()
}
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

@ -0,0 +1,10 @@
package com.tangem.datasource.local.cache.model
import org.joda.time.Duration
import org.joda.time.LocalDateTime
data class CacheKey(
val id: String,
val updatedAt: LocalDateTime,
val expiresIn: Duration,
)

View file

@ -0,0 +1,59 @@
package com.tangem.datasource.local.store
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.update
internal class RuntimeStore<Key, Data>(private val keyProvider: (Data) -> Key) {
private val store = MutableStateFlow<HashMap<Key, Data>>(hashMapOf())
fun get(selector: (Data) -> Boolean = { true }): Flow<List<Data>> {
return store.map { getInternal(it, selector) }
}
fun getSync(selector: (Data) -> Boolean = { true }): List<Data> {
return getInternal(store.value, selector)
}
// 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
// }
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)
}
}
store.value = storeValue
}
fun clear() {
store.update { hashMapOf() }
}
private fun getInternal(store: HashMap<Key, Data>, selector: (Data) -> Boolean): List<Data> {
return store.values.filter { selector(it) }
}
}

1
data/common/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/build

View file

@ -0,0 +1,16 @@
plugins {
alias(deps.plugins.android.library)
alias(deps.plugins.kotlin.android)
alias(deps.plugins.kotlin.kapt)
id("configuration")
}
dependencies {
implementation(projects.core.datasource)
implementation(deps.kotlin.coroutines)
implementation(deps.jodatime)
implementation(deps.hilt.android)
kapt(deps.hilt.kapt)
}

View file

@ -0,0 +1 @@
<manifest package="com.tangem.data.common" />

View file

@ -0,0 +1,52 @@
package com.tangem.data.common.cache
import org.joda.time.Duration
/**
* Represents a registry for managing cache.
*/
interface CacheRegistry {
/**
* Checks whether the cache key is expired.
*
* @param key cache key.
* @return `true` if the cache key is expired, `false` otherwise.
*/
suspend fun isExpired(key: String): Boolean
/**
* Invalidates the cache key in registry.
*
* If the key doesn't exist, or it's already invalidated, this method doesn't have any effect.
*
* @param key cache key.
*/
suspend fun invalidate(key: String)
/**
* Invalidates all cache keys in the registry.
*
* After the call, the registry doesn't contain any valid keys.
*/
suspend fun invalidateAll()
/**
* Defines a callback to be invoked when the cache key expires.
*
* @param key cache key.
* @param skipCache if `true`, the callback will be invoked regardless of whether the key has expired or not.
* @param expireIn the duration after which the cache key is considered expired.
* @param block the block of code to be executed when the cache key expires.
*/
suspend fun invokeOnExpire(
key: String,
skipCache: Boolean,
expireIn: Duration = Duration.standardMinutes(DEFAULT_CACHE_KEY_EXPIRE_IN_MINUTES),
block: suspend () -> Unit,
)
private companion object {
const val DEFAULT_CACHE_KEY_EXPIRE_IN_MINUTES = 5L
}
}

View file

@ -0,0 +1,52 @@
package com.tangem.data.common.cache
import com.tangem.datasource.local.cache.CacheKeysStore
import com.tangem.datasource.local.cache.model.CacheKey
import org.joda.time.Duration
import org.joda.time.LocalDateTime
internal class DefaultCacheRegistry(
private val cacheKeysStore: CacheKeysStore,
) : CacheRegistry {
override suspend fun isExpired(key: String): Boolean {
val cacheKey = cacheKeysStore.get(key) ?: return true
return cacheKey.updatedAt
.plus(cacheKey.expiresIn)
.isBefore(LocalDateTime.now())
}
override suspend fun invalidate(key: String) {
cacheKeysStore.remove(key)
}
override suspend fun invalidateAll() {
cacheKeysStore.clear()
}
override suspend fun invokeOnExpire(
key: String,
skipCache: Boolean,
expireIn: Duration,
block: suspend () -> Unit,
) {
val isExpired = isExpired(key) || skipCache
if (!isExpired) return
cacheKeysStore.addOrReplace(
key = CacheKey(
id = key,
updatedAt = LocalDateTime.now(),
expiresIn = expireIn,
),
)
try {
block()
} catch (e: Throwable) {
invalidate(key)
throw e
}
}
}

View file

@ -0,0 +1,21 @@
package com.tangem.data.common.cache.di
import com.tangem.data.common.cache.CacheRegistry
import com.tangem.data.common.cache.DefaultCacheRegistry
import com.tangem.datasource.local.cache.CacheKeysStore
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 CacheRegistryModule {
@Provides
@Singleton
fun provideCacheRegistry(cacheKeysStore: CacheKeysStore): CacheRegistry {
return DefaultCacheRegistry(cacheKeysStore)
}
}

View file

@ -85,6 +85,7 @@ include(":domain:tokens")
// endregion Domain modules // endregion Domain modules
// region Data modules // region Data modules
include(":data:common")
include(":data:card") include(":data:card")
include(":data:tokens") include(":data:tokens")
include(":data:source:preferences") include(":data:source:preferences")