diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 75db227f9d..8b1db8e62c 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -43,6 +43,7 @@ dependencies { implementation(project(":data:source:preferences")) implementation(projects.data.card) implementation(projects.data.tokens) + implementation(projects.data.common) /** Features */ implementation(project(":features:onboarding")) diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/CacheKeysStoreModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/CacheKeysStoreModule.kt new file mode 100644 index 0000000000..ae96179015 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/di/CacheKeysStoreModule.kt @@ -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() + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/cache/CacheKeysStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/cache/CacheKeysStore.kt new file mode 100644 index 0000000000..fc32a2b16e --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/cache/CacheKeysStore.kt @@ -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() +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/cache/RuntimeCacheKeysStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/cache/RuntimeCacheKeysStore.kt new file mode 100644 index 0000000000..229b5ab30c --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/cache/RuntimeCacheKeysStore.kt @@ -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() + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/cache/model/CacheKey.kt b/core/datasource/src/main/java/com/tangem/datasource/local/cache/model/CacheKey.kt new file mode 100644 index 0000000000..733e61e2b0 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/cache/model/CacheKey.kt @@ -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, +) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/store/RuntimeStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/store/RuntimeStore.kt new file mode 100644 index 0000000000..37a05f8d3e --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/store/RuntimeStore.kt @@ -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(private val keyProvider: (Data) -> Key) { + + private val store = MutableStateFlow>(hashMapOf()) + + fun get(selector: (Data) -> Boolean = { true }): Flow> { + return store.map { getInternal(it, selector) } + } + + fun getSync(selector: (Data) -> Boolean = { true }): List { + return getInternal(store.value, selector) + } + + // TODO: Uncomment if needed + // fun addOrReplace(items: Collection) { + // 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, selector: (Data) -> Boolean): List { + return store.values.filter { selector(it) } + } +} \ No newline at end of file diff --git a/data/common/.gitignore b/data/common/.gitignore new file mode 100644 index 0000000000..796b96d1c4 --- /dev/null +++ b/data/common/.gitignore @@ -0,0 +1 @@ +/build diff --git a/data/common/build.gradle.kts b/data/common/build.gradle.kts new file mode 100644 index 0000000000..7ddd807653 --- /dev/null +++ b/data/common/build.gradle.kts @@ -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) +} \ No newline at end of file diff --git a/data/common/src/main/AndroidManifest.xml b/data/common/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..5a762109a1 --- /dev/null +++ b/data/common/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + diff --git a/data/common/src/main/kotlin/com/tangem/data/common/cache/CacheRegistry.kt b/data/common/src/main/kotlin/com/tangem/data/common/cache/CacheRegistry.kt new file mode 100644 index 0000000000..fb698486ad --- /dev/null +++ b/data/common/src/main/kotlin/com/tangem/data/common/cache/CacheRegistry.kt @@ -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 + } +} \ No newline at end of file diff --git a/data/common/src/main/kotlin/com/tangem/data/common/cache/DefaultCacheRegistry.kt b/data/common/src/main/kotlin/com/tangem/data/common/cache/DefaultCacheRegistry.kt new file mode 100644 index 0000000000..1aabdd2e77 --- /dev/null +++ b/data/common/src/main/kotlin/com/tangem/data/common/cache/DefaultCacheRegistry.kt @@ -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 + } + } +} \ No newline at end of file diff --git a/data/common/src/main/kotlin/com/tangem/data/common/cache/di/CacheRegistryModule.kt b/data/common/src/main/kotlin/com/tangem/data/common/cache/di/CacheRegistryModule.kt new file mode 100644 index 0000000000..84359cb2e6 --- /dev/null +++ b/data/common/src/main/kotlin/com/tangem/data/common/cache/di/CacheRegistryModule.kt @@ -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) + } +} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 4cd9f9833e..276169d26e 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -85,6 +85,7 @@ include(":domain:tokens") // endregion Domain modules // region Data modules +include(":data:common") include(":data:card") include(":data:tokens") include(":data:source:preferences")