From ae7a26c0475bac384ddf9940df15c4f002a0bd66 Mon Sep 17 00:00:00 2001 From: Aleksha Date: Sun, 7 Jul 2024 21:05:15 +0300 Subject: [PATCH] caching logic --- .../compose/dataflow/cache/basic/usage.md | 1 - .../cache/basic/BasicCacheViewModel.kt | 4 +- .../shared/data/source/cache/CacheEntry.kt | 35 ++++++++------- .../shared/data/source/cache/CacheSource.kt | 2 +- .../data/source/cache/InMemoryCacheSource.kt | 45 ++++++++++--------- .../source/cache/InMemoryCacheSourceTest.kt | 14 +++--- 6 files changed, 52 insertions(+), 49 deletions(-) diff --git a/processor/src/main/resources/kotli/template/multiplatform/compose/dataflow/cache/basic/usage.md b/processor/src/main/resources/kotli/template/multiplatform/compose/dataflow/cache/basic/usage.md index 907e27ae..57c5a468 100644 --- a/processor/src/main/resources/kotli/template/multiplatform/compose/dataflow/cache/basic/usage.md +++ b/processor/src/main/resources/kotli/template/multiplatform/compose/dataflow/cache/basic/usage.md @@ -13,7 +13,6 @@ Facade **CacheSource** provides the following methods: - `invalidate(key: K)` - Invalidates the cache entry associated with the specified key. - `remove(type: Class)` - Removes all cache entries associated with the specified key type. - `remove(key: K)` - Removes the cache entry associated with the specified key. -- `put(key: CacheKey, value: T)` - Associates the specified value with the specified key in the cache. - `clear()` - Clears all entries from the cache. ## Example diff --git a/template/app/src/commonMain/kotlin/kotli/app/presentation/showcases/dataflow/cache/basic/BasicCacheViewModel.kt b/template/app/src/commonMain/kotlin/kotli/app/presentation/showcases/dataflow/cache/basic/BasicCacheViewModel.kt index 94e5aca6..bc097731 100644 --- a/template/app/src/commonMain/kotlin/kotli/app/presentation/showcases/dataflow/cache/basic/BasicCacheViewModel.kt +++ b/template/app/src/commonMain/kotlin/kotli/app/presentation/showcases/dataflow/cache/basic/BasicCacheViewModel.kt @@ -24,11 +24,11 @@ class BasicCacheViewModel( launchAsync { val cacheKey = SimpleCacheKey() val cacheEntry = cacheSource.get(cacheKey, ::getDateAsFormattedString) - cacheEntry.getChanges().collectLatest(cacheState::set) + cacheEntry.changes().collectLatest(cacheState::set) } } - private fun getDateAsFormattedString(): String { + private fun getDateAsFormattedString(key: SimpleCacheKey): String { val time = Clock.System.now() return time.format(DateTimeComponents.Format { byUnicodePattern("yyyy-MM-dd HH:mm:ss") diff --git a/template/shared/data/src/commonMain/kotlin/shared/data/source/cache/CacheEntry.kt b/template/shared/data/src/commonMain/kotlin/shared/data/source/cache/CacheEntry.kt index 5147d540..e3adf036 100644 --- a/template/shared/data/src/commonMain/kotlin/shared/data/source/cache/CacheEntry.kt +++ b/template/shared/data/src/commonMain/kotlin/shared/data/source/cache/CacheEntry.kt @@ -9,45 +9,45 @@ import kotlinx.coroutines.flow.update * * @param T The type of the cached value. */ -interface CacheEntry { +interface CacheEntry> { /** The key associated with this cache entry. */ - val key: CacheKey + val key: K /** * Sets the specified value to the given entry. * * @param value The value to be stored in the entry. */ - suspend fun setValue(value: T?) + suspend fun set(value: T?) /** * Retrieves the cached value. * * @return The cached value, or new one if the value is not present in the cache or expired. */ - suspend fun getValue(): T? + suspend fun get(): T? /** * Retrieves the last cached value. * * @return The last cached value, or null if the value is not present in the cache. */ - suspend fun getLast(): T? + suspend fun last(): T? /** * Retrieves a fresh copy of the cached value. * * @return A fresh copy of the cached value, or null if the value is not available. */ - suspend fun getFresh(): T? + suspend fun fresh(): T? /** * Retrieves the last cached value if available, otherwise retrieves a fresh copy of the value. * * @return The last cached value if available, or a fresh copy of the value. Returns null if the value is not present in the cache. */ - suspend fun getLastOrFresh() = getLast() ?: getFresh() + suspend fun lastOrFresh() = last() ?: fresh() /** * Emits the cached value whenever it changes. @@ -55,7 +55,7 @@ interface CacheEntry { * * @return A flow representing the changes to the cached value. */ - suspend fun getChanges(): Flow + suspend fun changes(): Flow companion object { /** @@ -65,16 +65,17 @@ interface CacheEntry { * @param value The cached value. * @return A CacheState instance representing the single cached value. */ - fun of(key: CacheKey, value: T): CacheEntry = object : CacheEntry { - private val valueChanges = MutableStateFlow(value) + fun > of(key: K, value: T): CacheEntry = + object : CacheEntry { + private val valueChanges = MutableStateFlow(value) - override val key: CacheKey = key - override suspend fun getValue(): T? = value - override suspend fun getLast(): T? = value - override suspend fun getFresh(): T? = value - override suspend fun getChanges(): Flow = valueChanges - override suspend fun setValue(value: T?) = valueChanges.update { value } - } + override val key: K = key + override suspend fun get(): T? = value + override suspend fun last(): T? = value + override suspend fun fresh(): T? = value + override suspend fun changes(): Flow = valueChanges + override suspend fun set(value: T?) = valueChanges.update { value } + } } } \ No newline at end of file diff --git a/template/shared/data/src/commonMain/kotlin/shared/data/source/cache/CacheSource.kt b/template/shared/data/src/commonMain/kotlin/shared/data/source/cache/CacheSource.kt index 1d93fd66..4dbdbf3a 100644 --- a/template/shared/data/src/commonMain/kotlin/shared/data/source/cache/CacheSource.kt +++ b/template/shared/data/src/commonMain/kotlin/shared/data/source/cache/CacheSource.kt @@ -20,7 +20,7 @@ interface CacheSource : DataSource { * @param valueProvider A suspend function that provides the value if the cache entry is not found. * @return A CacheState object representing the state of the cache entry. */ - fun get(key: CacheKey, valueProvider: suspend () -> T?): CacheEntry + fun > get(key: K, valueProvider: suspend (key: K) -> T?): CacheEntry /** * Invalidates all cache entries associated with the specified key type. diff --git a/template/shared/data/src/commonMain/kotlin/shared/data/source/cache/InMemoryCacheSource.kt b/template/shared/data/src/commonMain/kotlin/shared/data/source/cache/InMemoryCacheSource.kt index 5b56a5ed..1500abc4 100644 --- a/template/shared/data/src/commonMain/kotlin/shared/data/source/cache/InMemoryCacheSource.kt +++ b/template/shared/data/src/commonMain/kotlin/shared/data/source/cache/InMemoryCacheSource.kt @@ -41,13 +41,16 @@ open class InMemoryCacheSource( private val dispatcher = Dispatchers.Default private val jobs = ConcurrentMutableMap, Deferred<*>>() - private val cache = ConcurrentMutableMap, EntryData<*>>() + private val cache = ConcurrentMutableMap, EntryData<*, *>>() - override fun get(key: CacheKey, valueProvider: suspend () -> T?): CacheEntry { + override fun > get( + key: K, + valueProvider: suspend (key: K) -> T? + ): CacheEntry { val keyData = KeyData(key) val entryData = cache.computeIfAbsent(keyData) { EntryData(keyData, valueProvider) - } as CacheEntry + } as CacheEntry return entryData } @@ -108,8 +111,8 @@ open class InMemoryCacheSource( cache.remove(cacheKey) } - private data class KeyData( - val key: CacheKey, + private data class KeyData>( + val key: K, val type: KClass<*> = key::class ) @@ -118,25 +121,25 @@ open class InMemoryCacheSource( val updateTime: Long = Clock.System.now().toEpochMilliseconds() ) - private inner class EntryData( - private val keyData: KeyData, - private val valueProvider: suspend () -> T? - ) : CacheEntry { + private inner class EntryData>( + private val keyData: KeyData, + private val valueProvider: suspend (key: K) -> T? + ) : CacheEntry { @Transient private var invalidated = false private val liveChanges by lazy { fetchLiveChanges() } private val changes = MutableStateFlow?>(null) - override val key: CacheKey = keyData.key + override val key: K = keyData.key - override suspend fun setValue(value: T?) = changes.update { value?.let(::EntrySnapshot) } + override suspend fun set(value: T?) = changes.update { value?.let(::EntrySnapshot) } - override suspend fun getFresh(): T? = invalidate().run { getValue() } + override suspend fun fresh(): T? = invalidate().run { get() } - override suspend fun getLast(): T? = changes.value?.value + override suspend fun last(): T? = changes.value?.value - override suspend fun getValue(): T? { + override suspend fun get(): T? { return if (!isValid(key.ttl)) { val newValue = fetchValue() changes.updateAndGet { newValue?.let(::EntrySnapshot) }?.value @@ -145,7 +148,7 @@ open class InMemoryCacheSource( } } - override suspend fun getChanges(): Flow = liveChanges + override suspend fun changes(): Flow = liveChanges .flatMapLatest { changes } .map { snapshot -> snapshot?.value } .retry { th -> !th.isCancellationException().also { delay(changesRetryInterval) } } @@ -180,12 +183,12 @@ open class InMemoryCacheSource( ?: run { if (key.immortal()) { jobs.computeIfAbsent(keyData) { - GlobalScope.async { valueProvider() } + GlobalScope.async { valueProvider(key) } } } else { withContext(dispatcher) { jobs.computeIfAbsent(keyData) { - async { valueProvider() } + async { valueProvider(key) } } } } @@ -199,8 +202,8 @@ open class InMemoryCacheSource( emit(true) var retryAttempt = 0 while (currentCoroutineContext().isActive) { - try { - getValue() + runCatching { + get() val updateTime = changes.value?.updateTime if (updateTime == null) { @@ -212,10 +215,10 @@ open class InMemoryCacheSource( } retryAttempt = 0 - } catch (e: Exception) { + }.onFailure { th -> retryAttempt++ when { - retryAttempt >= exceptionRetryCount -> throw e + retryAttempt >= exceptionRetryCount -> throw th else -> delay(exceptionRetryInterval) } } diff --git a/template/shared/data/src/jvmTest/kotlin/shared/data/source/cache/InMemoryCacheSourceTest.kt b/template/shared/data/src/jvmTest/kotlin/shared/data/source/cache/InMemoryCacheSourceTest.kt index 219d06db..9657ad0e 100644 --- a/template/shared/data/src/jvmTest/kotlin/shared/data/source/cache/InMemoryCacheSourceTest.kt +++ b/template/shared/data/src/jvmTest/kotlin/shared/data/source/cache/InMemoryCacheSourceTest.kt @@ -29,7 +29,7 @@ class InMemoryCacheSourceTest { delay(2.seconds) iteration } - .getValue() + .get() ?.let(cached::add) } } @@ -50,7 +50,7 @@ class InMemoryCacheSourceTest { delay(1.seconds) UUID.randomUUID() } - .getValue() + .get() ?.let(cached::add) } } @@ -71,7 +71,7 @@ class InMemoryCacheSourceTest { delay(1.seconds) UUID.randomUUID() } - .getChanges() + .changes() .filterNotNull() .take(1) .first() @@ -86,14 +86,14 @@ class InMemoryCacheSourceTest { fun `check cached state logic`() = runBlocking { val key = UUIDCacheKey(Int.MAX_VALUE, ttl = 100) val entry = cache.get(key) { UUID.randomUUID() } - val value1 = entry.getValue() - val value1Last = entry.getLast() + val value1 = entry.get() + val value1Last = entry.last() delay(100) - val value2 = entry.getValue() + val value2 = entry.get() delay(100) assertNotEquals(value1, value2) assertEquals(value1, value1Last) - assertNotEquals(value2, entry.getValue()) + assertNotEquals(value2, entry.get()) } private data class TestCacheKey(